【第562期】用 webpack 構建 node 後端代碼,使其支持 js 新特性並實現熱重載

前言
ok,昨天那篇文章看完有什麼感受呢?今天的文章又是比較高大上的,來自美信FED前端團隊的@luoye童鞋投稿的。

正文從這開始~

webpack 在前端領域的模塊化和代碼構建方面有着無比強大的功能,通過一些特殊的配置甚至可以實現前端代碼的實時構建、ES6/7新特性支持以及熱重載,這些功能同樣可以運用於後臺 nodejs 的應用,讓後臺的開發更加順暢,服務更加靈活,怎麼來呢?往下看。

先梳理下我們將要解決的問題:

  • node端代碼構建

  • ES6/7 新特性支持

  • node服務代碼熱重載

node端代碼構建

node端的代碼其實是不用編譯或者構建的,整個node的環境有它自己的一個模塊化或者依賴機制,但是即使是現在最新的node版本,對ES6/7的支持還是捉襟見肘。當然使用一些第三方庫可以做到支持類似async/await 這樣的語法,但是畢竟不是規範不是標準,這樣看來,node端的代碼還是有構建的需要的。這裏我們選取的工具就是 webpack 以及它的一些 loader

首先,一個 node app 必定有一個入口文件 app.js ,按照 webpack 的規則,我們可以把所有的代碼打包成一個文件 bundle.js ,然後運行這個 bundle.js 即可,webpack.config.js如下:

var webpcak = require('webpack');module.exports = {
    entry: [        './app.js'
    ],
    output: {
        path: path.resolve(__dirname, 'build'),
        filename: 'bundle.js'
    }
}

但是有一個很嚴重的問題,這樣打包的話,一些 npm 中的模塊也會被打包進這個 bundle.js,還有 node 的一些原生模塊,比如 fs/path 也會被打包進來,這明顯不是我們想要的。所以我們得告訴 webpack,你打包的是 node 的代碼,原生模塊就不要打包了,還有 node_modules 目錄下的模塊也不要打包了,webpack.config.js 如下:

var webpcak = require('webpack');var nodeModules = {};
fs.readdirSync('node_modules')
    .filter(function(x) {        return ['.bin'].indexOf(x) === -1;
    })
    .forEach(function(mod) {
        nodeModules[mod] = 'commonjs ' + mod;
    });module.exports = {
    entry: [        './app.js'
    ],
    output: {
        path: path.resolve(__dirname, 'build'),
        filename: 'bundle.js'
    },
    target: 'node',
    externals: nodeModules
}

主要就是在 webpack 的配置中加上 target: 'node' 告訴 webpack打包的對象是 node 端的代碼,這樣一些原生模塊 webpack 就不會做處理。另一個就是 webpack 的 externals 屬性,這個屬性的主要作用就是告知 webpack 在打包過程中,遇到 externals 中聲明的模塊不用處理。

比如在前端中, jQuery 的包通過 CDN 的方式以 script 標籤引入,如果此時在代碼中出現 require('jQuery') ,並且直接用 webpack 打包比定會報錯。因爲在本地並沒有這樣的一個模塊,此時就必須在 externals 中聲明 jQuery 的存在。也就是 externals中的模塊,雖然沒有被打包,但是是代碼運行是所要依賴的,而這些依賴是直接存在在整個代碼運行環境中,並不用做特殊處理。

在 node 端所要做的處理就是過濾出 node_modules 中所有模塊,並且放到 externals中。

這個時候我們的代碼應該可以構建成功了,並且是我們期望的形態,但是不出意外的話,你還是跑不起來,因爲有不小的坑存在,繼續往下看。

  • 坑1:__durname __filename 指向問題

    打包之後的代碼你會發現 __durname __filename 全部都是 / ,這兩個變量在 webpack 中做了一些自定義處理,如果想要正確使用,在配置中加上

    context: __dirname,
    node: {
      __filename: false,
      __dirname: false},
  • 坑2:動態 require 的上下文問題

    這一塊比較大,放到後面講,跟具體代碼有關,和配置無關

  • 坑n:其它的還沒發現,估摸不少,遇到了谷歌吧…

ES6/7 新特性支持

構建 node 端代碼的目標之一就是使用ES6/7中的新特性,要實現這樣的目標 babel 是我們的不二選擇。

首先,先安裝 babel 的各種包 npm install babel-core babel-loader babel-plugin-transform-runtime babel-preset-es2015 babel-preset-stage-0 --save-dev json-loader -d  

然後修改 webpack.config.js ,如下:

var webpcak = require('webpack');var nodeModules = {};
fs.readdirSync('node_modules')
    .filter(function(x) {        return ['.bin'].indexOf(x) === -1;
    })
    .forEach(function(mod) {
        nodeModules[mod] = 'commonjs ' + mod;
    });module.exports = {
    entry: [        './app.js'
    ],
    output: {
        path: path.resolve(__dirname, 'build'),
        filename: 'bundle.js'
    },
    target: 'node',
    externals: nodeModules,
    context: __dirname,
    node: {
        __filename: false,
        __dirname: false
    },    module: {
        loaders: [{
            test: /\.js$/,
            loader: 'babel-loader',
            exclude: [
                path.resolve(__dirname, "node_modules"),
            ],
            query: {
                plugins: ['transform-runtime'],
                presets: ['es2015', 'stage-0'],
            }
        }, {
            test: /\.json$/,
            loader: 'json-loader'
        }]
    },
    resolve: {
        extensions: ['', '.js', '.json']
    }
}

主要就是配置 webpack 中的 loader ,藉此來編譯代碼。

node服務代碼熱重載

webpack 極其牛叉的地方之一,開發的時候,實時的構建代碼,並且,實時的更新你已經加載的代碼,也就是說,不用手動去刷新瀏覽器,即可以獲取最新的代碼並執行。

這一點同樣可以運用在 node 端,實現即時修改即時生效,而不是 pm2 那種重啓的方式。

首先,修改配置文件,如下:

entry: [    'webpack/hot/poll?1000',    './app.js'],// ...plugins: [    new webpack.HotModuleReplacementPlugin()
]

這個時候,如果執行 webpack --watch & node app.js ,你的代碼修改之後就可以熱重載而不用重啓應用,當然,代碼中也要做相應改動,如下:

var hotModule = require('./hotModule');// do something else// 如果想要 hotModule 模塊熱重載if (module.hot) {    module.hot.accept('./hotModule.js', function() {        var newHotModule = require('./hotModule.js');        // do something else
    });
}

思路就是,如果需要某模塊熱重載,就把它包一層,如果修改了,webpack 重新打包了,重新 require 一遍,然後代碼即是最新的代碼。

當然,如果你在某個需要熱重載的模塊中又依賴另一個模塊,或者說動態的依賴了另一個模塊,這樣的模塊並不會熱重載。

webpack 動態 require

動態 require 的場景包括:

  • 場景一:在代碼運行過程中遍歷某個目錄,動態 reauire,比如

      //app.js
      var rd = require('rd');  // 遍歷路由文件夾,自動掛載路由
      var routers = rd.readFileFilterSync('./routers', /\.js/);
      routers.forEach(function(item) {      require(item);
      })

    這個時候你會發現 './routers' 下的require都不是自己想要的,然後在 bundle.js 中找到打包之後的相應模塊後,你可以看到,動態 require 的對象都是 app.js 同級目錄下的 js 文件,而不是 './routers' 文件下的 js 文件。爲什麼呢?

    webpack 在打包的時候,必須把你可能依賴的文件都打包進來,並且編上號,然後在運行的時候 require 相應的模塊 ID 即可,這個時候 webpack 獲取的動態模塊,就不再是你指定的目錄'./routers' 了,而是相對於當前文件的目錄,所以,必須修正 require 的上下文,修改如下:

      // 獲取正確的模塊
      var req = require.context("./routers", true, /\.js$/);  var routers = rd.readFileFilterSync('./routers', /\.js/);
      routers.forEach(function(item) {      // 使用包涵正確模塊的已經被修改過的 `require` 去獲取模塊
          req(item);
      })
  • 場景二:在 require 的模塊中含有變量,比如

      var myModule = require(isMe ? './a.js' : './b.js');  // 或者
      var testMoule = require('./mods' + name + '.js');

    第一種的處理方式在 webpack 中的處理是把模塊 ./a.js ./b.js 都包涵進來,根據變量不同 require 不同的模塊。

    第二種的處理方式和場景一類似,獲取 ./mods/ 目錄下的所有模塊,然後重寫了 require ,然後根據變量不同加載不通的模塊,所以自己處理的時候方法類似。

用 ES6/7 寫 webpack.config.js

項目都用 ES6/7 了,配置文件也必須跟上。

安裝好 babel 編譯所需要的幾個依賴包,然後把 webpack.config.js 改爲 webpack.config.babel.js ,然後新建 .babelrc 的 babel 配置文件,加入

{
  "presets": ["es2015"]}

然後和往常一樣執行 webpack 的相關命令即可。

完整 webpack.config.babel.js 如下:

import webpack from 'webpack';
import fs from 'fs';
import path from 'path';let nodeModules = {};
fs.readdirSync('node_modules')
    .filter((x) => {        return ['.bin'].indexOf(x) === -1;
    })
    .forEach((mod) => {
        nodeModules[mod] = 'commonjs ' + mod;
    });

export default {
    cache: true,
    entry: [        'webpack/hot/poll?1000',        './app.js'
    ],
    output: {
        path: path.resolve(__dirname, 'build'),
        filename: 'bundle.js'
    },
    context: __dirname,
    node: {
        __filename: false,
        __dirname: false
    },
    target: 'node',
    externals: nodeModules,    module: {
        loaders: [{
            test: /\.js$/,
            loader: 'babel-loader',
            exclude: [
                path.resolve(__dirname, "node_modules"),
            ],
            query: {
                plugins: ['transform-runtime'],
                presets: ['es2015', 'stage-0'],
            }
        }, {
            test: /\.json$/,
            loader: 'json-loader'
        }]
    },
    plugins: [        new webpack.HotModuleReplacementPlugin()
    ],
    resolve: {
        extensions: ['', '.js', '.json']
    }
}

大致流程就是如此,坑肯定還有,遇到的話手動谷歌吧~


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章