背景
毋庸置疑,NodeJS全棧開發包括NodeJS在前端的應用,也包括NodeJS在後端的應用?。CabloyJS前端採用Vue+Framework7,採用Webpack進行打包。CabloyJS後端是基於EggJS開發的上層框架。我們知道,EggJS採用的是約定優於配置
的原則,當服務啟動時,會在約定的目錄載入controller
、service
諸如此類的檔案。那麼,我們基於EggJS開發的後端程式碼,是否也可以像前端一樣進行Webpack打包呢?
意義
為什麼要提出這樣一個命題:NodeJS後端編譯打包?
因為NodeJS後端編譯打包
有如下兩個顯著的好處:
1. 保護商業程式碼
編譯打包
,可以將原始碼進行醜化,滿足保護商業程式碼的需求。雖然醜化javascript程式碼無法完全避免反編譯,但我們要基於一個原則:醜化最主要的目的是保護開發團隊的工作量
。可以想象,反編譯
及以反編譯
為基礎的二次開發,工作量並不小
2. 提升啟動效能
編譯打包
,可以將眾多散亂的javascript檔案合併成一個檔案,從而提升後端服務的啟動效能。這在大型專案的開發中,效果更加顯著
在接下來的案例中,我們會以模組egg-born-module-test-party
為例。該模組後端有63
個js原始碼檔案,通過編譯打包後只生成一個backend.js
檔案。當後端服務啟動時,一個模組只需載入一個檔案,效能肯定優於載入63
個檔案。如果一個大型專案包含100
個業務模組,這種效能優勢就會更加明顯
目標
進行JS檔案打包的工具有很多,由於CabloyJS前端是採用Webpack進行打包,因此,在這裡,我們也只探討Webpack在後端的打包方式
前提條件
我們知道,Webpack是從一個入口檔案開始,通過檢索require
方法,得到一棵完整的檔案依賴樹,然後把這些依賴樹合併成一個檔案,最後進行醜化
而EggJS採用的是約定優於配置
的原則,檔案之間的依賴關係是隱性約定的,而不是通過require
顯式宣告的。因此,在這種機制下面,Webpack打包是不起作用的
但是EggJS的定位就是框架的框架
,使得我們可以在EggJS的基礎之上開發新的框架。CabloyJS後端就是在EggJS的基礎之上,進行了進一步的擴充套件和封裝,使得controller
、service
、middleware
、config
等諸如此類的定義檔案,可以通過require
方法顯式宣告,從而可以讓Webpack提煉出一棵完整的檔案依賴樹,進而完成編譯打包工作
這篇文章的重點,不是要說明CabloyJS後端是如何對EggJS進行的擴充套件和封裝,而是要說明,在已經實現require
顯式宣告的前提條件下,NodeJS後端如何進行編譯打包
準備工作
egg-born-module-test-party
是CabloyJS的測試模組,包含大量測試用例。我們以該模組為例來說明NodeJS後端編譯打包的方方面面
1. 下載模組
我們先將模組原始碼下載到本地
$ git clone https://github.com/zhennann/egg-born-module-test-party.git
複製程式碼
如果沒有git命令列工具,可以直接從GitHub官網下載:github.com/zhennann/eg…
2. 安裝依賴
$ npm i
複製程式碼
3. 編譯打包
npm run build:backend
複製程式碼
核心概念
只要我們指定了入口檔案,Webpack就會自動通過require
檢索檔案依賴樹。因此,剩下的核心工作,就是通過配置檔案來調整Webpack的行為
webpack.base.conf.js
檔案:/build/backend/webpack.base.conf.js
const path = require('path');
const config = require('./config.js');
const nodeModules = {
require3: 'commonjs2 require3',
};
function resolve(dir) {
return path.join(__dirname, '../../backend', dir);
}
module.exports = {
entry: {
backend: resolve('src/main.js'),
},
target: 'node',
output: {
path: config.build.assetsRoot,
filename: '[name].js',
library: 'backend',
libraryTarget: 'commonjs2',
},
externals: nodeModules,
resolve: {
extensions: [ '.js', '.json' ],
},
module: {
rules: [],
},
node: {
console: false,
global: false,
process: false,
__filename: false,
__dirname: false,
Buffer: false,
setImmediate: false,
},
};
複製程式碼
1. entry/output
通過entry/output
的組合,我們指定了一個入口檔案src/main.js
,最終編譯打包成一個輸出檔案backend.js
2. target: 'node'
Webpack是一個通用的打包工具,既可以用於前端瀏覽器,也可以用於後端NodeJS。因此,我們需要指定target為node
,從而為後端NodeJS打包。比如,在後端node
場景下,一些內建的模組就會被排除在打包之列,如fs
、path
等等
3. node
為了讓原本為後端NodeJS開發的程式碼可以在前端瀏覽器中執行,Webpack提供了模擬策略。比如,global
、process
、__filename
和__dirname
都是NodeJS內建的物件。如果程式碼中包含了這些物件,而程式碼又需要在前端執行,就需要進行模擬。我們這裡討論的是後端編譯,所以,就直接統一賦值false
,從而禁用模擬行為
4. resolve.extensions
如果我們在使用require
引用原始碼檔案時沒有指定副檔名,那麼Webpack會通過resolve.extensions
幫我們匹配合適的檔名
5. module.rules
Webpack除了可以打包js檔案,還可以打包css/image/text等資原始檔。因為這裡是後端打包,所以,不需要設定module.rules
6. externals
在這裡重點要說的是節點externals
在實際的業務開發中,我們難免會用到大量第三方模組,這些模組一般都安裝在node_modules目錄,比如moment
。因為我們也是通過const moment=require('moment')
的方式引用第三方庫,所以,Webpack也會嘗試把moment
打包進來
一方面,第三方模組數量眾多,如果進行打包,最終輸出檔案過大。另一方面,對於保護商業程式碼沒有任何意義。所以,我們需要想一個辦法把這些第三方模組從打包依賴樹中排除掉
- 排除moment
如果我們要排除moment,可以這樣配置:
externals: {
moment: 'commonjs2 moment'
}
複製程式碼
- 排除node_modules
如果我們要排除node_modules目錄下的所有第三方模組,可以這樣配置:
var fs = require('fs');
var nodeModules = {};
fs.readdirSync('node_modules')
.filter(function(x) {
return ['.bin'].indexOf(x) === -1;
})
.forEach(function(mod) {
nodeModules[mod] = 'commonjs2 ' + mod;
});
module.exports = {
...
externals: nodeModules
...
}
複製程式碼
- 更優雅的策略
針對這種場景,CabloyJS單獨開發了一個NPM模組require3
: github.com/zhennann/re…
我們只需要在externals中排除require3
這一個模組就可以了。其餘的模組都通過require3進行引用,從而輕鬆避免了被打包的行為
const nodeModules = {
require3: 'commonjs2 require3',
};
module.exports = {
...
externals: nodeModules
...
}
複製程式碼
在實際業務程式碼中,一般這樣引用:
const require3 = require('require3');
const moment = require3('moment');
複製程式碼
moment通過
require3
引用,從而避免被Webpack打包
webpack.prod.conf.js
檔案:/build/backend/webpack.prod.conf.js
const webpack = require('webpack');
const config = require('./config.js');
const merge = require('webpack-merge');
const baseWebpackConfig = require('./webpack.base.conf');
const env = config.build.env;
const plugins = [
new webpack.DefinePlugin({
'process.env': env,
}),
];
const webpackConfig = merge(baseWebpackConfig, {
mode: 'production',
devtool: config.build.productionSourceMap ? 'source-map' : false,
plugins,
optimization: {
runtimeChunk: false,
splitChunks: false,
minimize: config.build.uglify,
},
});
module.exports = webpackConfig;
複製程式碼
1. mode: 'production'
通過指定mode為production
,指示Webpack使用與production
相關的內建的優化策略
2. devtool
指示Webpack是否生成source map檔案,如果要生成,source map的檔案格式是什麼
詳細的格式清單,請參考:webpack.js.org/configurati…
3. optimization.minimize
由於我們只需輸出一個單檔案,所以只需通過optimization.minimize
指示Webpack是否需要最小化(醜化)即可
===> 殺手鐗
經過前面的配置,我們已經可以非常便利的進行後端NodeJS打包了,而且打包後的檔案已經進行了醜化。可是,有些網友認為這些工作還不夠,希望打包之後的檔案可以再亂一些
下面我們就借用babel對js檔案做進一步的程式碼轉譯工作。先把配置放出來,然後再一一解釋
檔案:/build/backend/webpack.base.conf.js
...
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
babelrc: false,
// presets: [ '@babel/preset-env' ],
plugins: [
'@babel/plugin-transform-arrow-functions',
'@babel/plugin-transform-for-of',
'@babel/plugin-transform-parameters',
'@babel/plugin-transform-shorthand-properties',
'@babel/plugin-transform-spread',
'@babel/plugin-transform-template-literals',
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-transform-async-to-generator',
],
},
},
},
],
},
...
複製程式碼
1. test
我們僅對字尾名為.js
的檔案進行babel轉譯
2. exclude
排除node_modules
目錄下的js檔案
3. use.loader
使用babel-loader
對js檔案進行轉譯
4. use.options
babel-loader
的轉譯引數
4.1 babelrc: false
轉譯引數既可以在options
中直接配置,也可以在專案根目錄建立一個.babelrc
檔案,然後在檔案中配置。在這裡,我們直接在options
中配置轉譯引數
4.2 presets
babel的轉譯工作都是通過一系列外掛的組合來完成的。我們可以把一系列外掛的組合定義為preset。@babel/preset-env
是babel提供的預配置組合,包含大量的外掛。但是這些預配置的外掛組合如果都生效的話,會破壞後端NodeJS程式碼的某些特性,產生不可預期的問題。所以,我們把presets引數註釋掉,手工新增我們所需要的外掛組合
4.3 plugins
啟用太多的babel外掛,一方面會影響編譯的效率,另一方面,有些babel外掛會破壞後端NodeJS程式碼的某些特性,產生不可預期的問題。經過實際測試,啟用以下babel外掛即可把後端NodeJS程式碼轉譯到慘不忍睹
的地步。前面我們也提到一個原則:醜化最主要的目的是保護開發團隊的工作量
外掛名稱 | 用途 |
---|---|
arrow-functions | 轉譯箭頭函式 |
for-of | 轉譯for-of迴圈 |
parameters | 轉譯ES2015函式引數 |
shorthand-properties | 轉譯簡寫屬性 |
spread | 轉譯... 展開形式 |
template-literals | 轉譯模版字串 |
object-rest-spread | 轉譯物件展開表示式 |
async-to-generator | 將async 方法轉譯為生成器 |
async/await
本質上就是生成器+Promise
的語法糖。因此,把async
方法轉譯為生成器
,不僅可以顯著打亂NodeJS程式碼的邏輯流,而且也是迴歸到了本質,反而提升了NodeJS程式碼的效能
關於Babel外掛的更詳細資訊,請參考:babeljs.io/docs/en/plu…
編譯打包
最後,讓我們再執行一次NodeJS後端的編譯打包指令
npm run build:backend
複製程式碼