毋庸置疑,NodeJS全棧開發包括NodeJS在前端的應用,也包括NodeJS在後端的應用😅。CabloyJS前端採用Vue+Framework7,採用Webpack進行打包。CabloyJS後端是基於EggJS開發的上層框架。咱們知道,EggJS採用的是約定優於配置
的原則,當服務啓動時,會在約定的目錄加載controller
、service
諸如此類的文件。那麼,咱們基於EggJS開發的後端代碼,是否也能夠像前端同樣進行Webpack打包呢?javascript
爲何要提出這樣一個命題:NodeJS後端編譯打包?
由於NodeJS後端編譯打包
有以下兩個顯著的好處:css
編譯打包
,能夠將源碼進行醜化,知足保護商業代碼的需求。雖然醜化javascript代碼沒法徹底避免反編譯,但咱們要基於一個原則:醜化最主要的目的是保護開發團隊的工做量
。能夠想象,反編譯
及以反編譯
爲基礎的二次開發,工做量並不小前端
編譯打包
,能夠將衆多散亂的javascript文件合併成一個文件,從而提高後端服務的啓動性能。這在大型項目的開發中,效果更加顯著java
在接下來的案例中,咱們會以模塊egg-born-module-test-party
爲例。該模塊後端有63
個js源碼文件,經過編譯打包後只生成一個backend.js
文件。當後端服務啓動時,一個模塊只需加載一個文件,性能確定優於加載63
個文件。若是一個大型項目包含100
個業務模塊,這種性能優點就會更加明顯node
進行JS文件打包的工具備不少,因爲CabloyJS前端是採用Webpack進行打包,所以,在這裏,咱們也只探討Webpack在後端的打包方式webpack
咱們知道,Webpack是從一個入口文件開始,經過檢索require
方法,獲得一棵完整的文件依賴樹,而後把這些依賴樹合併成一個文件,最後進行醜化git
而EggJS採用的是約定優於配置
的原則,文件之間的依賴關係是隱性約定的,而不是經過require
顯式聲明的。所以,在這種機制下面,Webpack打包是不起做用的github
可是EggJS的定位就是框架的框架
,使得咱們能夠在EggJS的基礎之上開發新的框架。CabloyJS後端就是在EggJS的基礎之上,進行了進一步的擴展和封裝,使得controller
、service
、middleware
、config
等諸如此類的定義文件,能夠經過require
方法顯式聲明,從而可讓Webpack提煉出一棵完整的文件依賴樹,進而完成編譯打包工做web
這篇文章的重點,不是要說明CabloyJS後端是如何對EggJS進行的擴展和封裝,而是要說明,在已經實現require
顯式聲明的前提條件下,NodeJS後端如何進行編譯打包npm
egg-born-module-test-party
是CabloyJS的測試模塊,包含大量測試用例。咱們以該模塊爲例來講明NodeJS後端編譯打包的方方面面
咱們先將模塊源碼下載到本地
$ git clone https://github.com/zhennann/egg-born-module-test-party.git
若是沒有git命令行工具,能夠直接從GitHub官網下載: https://github.com/zhennann/e...
$ npm i
npm run build:backend
只要咱們指定了入口文件,Webpack就會自動經過require
檢索文件依賴樹。所以,剩下的核心工做,就是經過配置文件來調整Webpack的行爲
文件:/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, }, };
經過entry/output
的組合,咱們指定了一個入口文件src/main.js
,最終編譯打包成一個輸出文件backend.js
Webpack是一個通用的打包工具,既能夠用於前端瀏覽器,也能夠用於後端NodeJS。所以,咱們須要指定target爲node
,從而爲後端NodeJS打包。好比,在後端node
場景下,一些內置的模塊就會被排除在打包之列,如fs
、path
等等
爲了讓本來爲後端NodeJS開發的代碼能夠在前端瀏覽器中運行,Webpack提供了模擬策略。好比,global
、process
、__filename
和__dirname
都是NodeJS內置的對象。若是代碼中包含了這些對象,而代碼又須要在前端運行,就須要進行模擬。咱們這裏討論的是後端編譯,因此,就直接統一賦值false
,從而禁用模擬行爲
若是咱們在使用require
引用源碼文件時沒有指定文件擴展名,那麼Webpack會經過resolve.extensions
幫咱們匹配合適的文件名
Webpack除了能夠打包js文件,還能夠打包css/image/text等資源文件。由於這裏是後端打包,因此,不須要設置module.rules
在這裏重點要說的是節點externals
在實際的業務開發中,咱們不免會用到大量第三方模塊,這些模塊通常都安裝在node_modules目錄,好比moment
。由於咱們也是經過const moment=require('moment')
的方式引用第三方庫,因此,Webpack也會嘗試把moment
打包進來
一方面,第三方模塊數量衆多,若是進行打包,最終輸出文件過大。另外一方面,對於保護商業代碼沒有任何意義。因此,咱們須要想一個辦法把這些第三方模塊從打包依賴樹中排除掉
若是咱們要排除moment,能夠這樣配置:
externals: { moment: 'commonjs2 moment' }
若是咱們要排除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
: https://github.com/zhennann/require3
咱們只須要在externals中排除require3
這一個模塊就能夠了。其他的模塊都經過require3進行引用,從而輕鬆避免了被打包的行爲
const nodeModules = { require3: 'commonjs2 require3', }; module.exports = { ... externals: nodeModules ... }
在實際業務代碼中,通常這樣引用:
const require3 = require('require3'); const moment = require3('moment');
moment經過
require3
引用,從而避免被Webpack打包
文件:/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;
經過指定mode爲production
,指示Webpack使用與production
相關的內置的優化策略
指示Webpack是否生成source map文件,若是要生成,source map的文件格式是什麼
詳細的格式清單,請參考: https://webpack.js.org/configuration/devtool/
因爲咱們只需輸出一個單文件,因此只需經過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', ], }, }, }, ], }, ...
咱們僅對後綴名爲.js
的文件進行babel轉譯
排除node_modules
目錄下的js文件
使用babel-loader
對js文件進行轉譯
babel-loader
的轉譯參數
轉譯參數既能夠在options
中直接配置,也能夠在項目根目錄建立一個.babelrc
文件,而後在文件中配置。在這裏,咱們直接在options
中配置轉譯參數
babel的轉譯工做都是經過一系列插件的組合來完成的。咱們能夠把一系列插件的組合定義爲preset。@babel/preset-env
是babel提供的預配置組合,包含大量的插件。可是這些預配置的插件組合若是都生效的話,會破壞後端NodeJS代碼的某些特性,產生不可預期的問題。因此,咱們把presets參數註釋掉,手工添加咱們所須要的插件組合
啓用太多的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插件的更詳細信息,請參考:https://babeljs.io/docs/en/plugins
最後,讓咱們再執行一次NodeJS後端的編譯打包指令
npm run build:backend