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

背景

毋庸置疑,NodeJS全棧開發包括NodeJS在前端的應用,也包括NodeJS在後端的應用😅。CabloyJS前端採用Vue+Framework7,採用Webpack進行打包。CabloyJS後端是基於EggJS開發的上層框架。咱們知道,EggJS採用的是約定優於配置的原則,當服務啓動時,會在約定的目錄加載controllerservice諸如此類的文件。那麼,咱們基於EggJS開發的後端代碼,是否也能夠像前端同樣進行Webpack打包呢?javascript

意義

爲何要提出這樣一個命題:NodeJS後端編譯打包?

由於NodeJS後端編譯打包有以下兩個顯著的好處:css

1. 保護商業代碼

編譯打包,能夠將源碼進行醜化,知足保護商業代碼的需求。雖然醜化javascript代碼沒法徹底避免反編譯,但咱們要基於一個原則:醜化最主要的目的是保護開發團隊的工做量。能夠想象,反編譯及以反編譯爲基礎的二次開發,工做量並不小前端

2. 提高啓動性能

編譯打包,能夠將衆多散亂的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的基礎之上,進行了進一步的擴展和封裝,使得controllerservicemiddlewareconfig等諸如此類的定義文件,能夠經過require方法顯式聲明,從而可讓Webpack提煉出一棵完整的文件依賴樹,進而完成編譯打包工做web

這篇文章的重點,不是要說明CabloyJS後端是如何對EggJS進行的擴展和封裝,而是要說明,在已經實現require顯式聲明的前提條件下,NodeJS後端如何進行編譯打包npm

準備工做

egg-born-module-test-party是CabloyJS的測試模塊,包含大量測試用例。咱們以該模塊爲例來講明NodeJS後端編譯打包的方方面面

1. 下載模塊

咱們先將模塊源碼下載到本地

$ git clone https://github.com/zhennann/egg-born-module-test-party.git
若是沒有git命令行工具,能夠直接從GitHub官網下載: https://github.com/zhennann/e...

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模塊require3https://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打包

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的文件格式是什麼

詳細的格式清單,請參考: https://webpack.js.org/configuration/devtool/

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插件的更詳細信息,請參考:https://babeljs.io/docs/en/plugins

編譯打包

最後,讓咱們再執行一次NodeJS後端的編譯打包指令

npm run build:backend
相關文章
相關標籤/搜索