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

本文轉自:github.com/gomeplusFED/

作者:github.com/luoye-fe

-----------------------

團隊小夥伴滿滿的乾貨,使用webpack構建nodejs代碼,支持熱部署最新技術實踐,並已經運用到公司項目。

-----------------------

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);nnmodule.exports = {n entry: [n ./app.jsn ],n output: {n path: path.resolve(__dirname, build),n filename: bundle.jsn }n}n

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

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

主要就是在 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,nnode: {n __filename: false,n __dirname: falsen},n

  • 坑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);nnvar nodeModules = {};nfs.readdirSync(node_modules)n .filter(function(x) {n return [.bin].indexOf(x) === -1;n })n .forEach(function(mod) {n nodeModules[mod] = commonjs + mod;n });nnmodule.exports = {n entry: [n ./app.jsn ],n output: {n path: path.resolve(__dirname, build),n filename: bundle.jsn },n target: node,n externals: nodeModules,n context: __dirname,n node: {n __filename: false,n __dirname: falsen },n module: {n loaders: [{n test: /.js$/,n loader: babel-loader,n exclude: [n path.resolve(__dirname, "node_modules"),n ],n query: {n plugins: [transform-runtime],n presets: [es2015, stage-0],n }n }, {n test: /.json$/,n loader: json-loadern }]n },n resolve: {n extensions: [, .js, .json]n }n}n

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

node服務代碼熱重載

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

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

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

entry: [n webpack/hot/poll?1000,n ./app.jsn],n// ...nplugins: [n new webpack.HotModuleReplacementPlugin()n]n

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

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

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

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

webpack 動態 require

動態 require 的場景包括:

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

    //app.jsnvar rd = require(rd);nn// 遍歷路由文件夾,自動掛載路由nvar routers = rd.readFileFilterSync(./routers, /.js/);nrouters.forEach(function(item) {n require(item);n})n

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

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

    // 獲取正確的模塊nvar req = require.req("./routers", true, /.js$/);nvar routers = rd.readFileFilterSync(./routers, /.js/);nrouters.forEach(function(item) {n // 使用包涵正確模塊的已經被修改過的 `require` 去獲取模塊n req(item);n})n

  • 場景二:在 require 的模塊中含有變數,比如

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

    第一種的處理方式在 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 配置文件,加入

{n "presets": ["es2015"]n}n

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

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

import webpack from webpack;nimport fs from fs;nimport path from path;nnlet nodeModules = {};nfs.readdirSync(node_modules)n .filter((x) => {n return [.bin].indexOf(x) === -1;n })n .forEach((mod) => {n nodeModules[mod] = commonjs + mod;n });nnexport default {n cache: true,n entry: [n webpack/hot/poll?1000,n ./app.jsn ],n output: {n path: path.resolve(__dirname, build),n filename: bundle.jsn },n context: __dirname,n node: {n __filename: false,n __dirname: falsen },n target: node,n externals: nodeModules,n module: {n loaders: [{n test: /.js$/,n loader: babel-loader,n exclude: [n path.resolve(__dirname, "node_modules"),n ],n query: {n plugins: [transform-runtime],n presets: [es2015, stage-0],n }n }, {n test: /.json$/,n loader: json-loadern }]n },n plugins: [n new webpack.HotModuleReplacementPlugin()n ],n resolve: {n extensions: [, .js, .json]n }n}n

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


推薦閱讀:

編寫自己的Webpack Loader
webpack之loader和plugin簡介
webpack打包之 緩存
webpack增量打包

TAG:Nodejs | webpack | ECMAScript2015 |