CabloyJS全棧開發之旅(1):NodeJS後端編譯打包全攻略

zhennann發表於2019-12-18

背景

毋庸置疑,NodeJS全棧開發包括NodeJS在前端的應用,也包括NodeJS在後端的應用?。CabloyJS前端採用Vue+Framework7,採用Webpack進行打包。CabloyJS後端是基於EggJS開發的上層框架。我們知道,EggJS採用的是約定優於配置的原則,當服務啟動時,會在約定的目錄載入controllerservice諸如此類的檔案。那麼,我們基於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的基礎之上,進行了進一步的擴充套件和封裝,使得controllerservicemiddlewareconfig等諸如此類的定義檔案,可以通過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場景下,一些內建的模組就會被排除在打包之列,如fspath等等

3. node

為了讓原本為後端NodeJS開發的程式碼可以在前端瀏覽器中執行,Webpack提供了模擬策略。比如,globalprocess__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模組require3github.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
複製程式碼

相關文章