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
相关文章
相关标签/搜索