前言
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']
}
}
大致流程就是如此,坑肯定還有,遇到的話手動谷歌吧~