CabloyJS全栈开发之旅(1):NodeJS后端编译打包全攻略
背景
毋庸置疑,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官网下载:
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
场景下,一些内置的模块就会被排除在打包之列,如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
: 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打包
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
原文地址:https://segmentfault.com/a/1190000021331082
相关推荐
-
详谈js防抖和节流 javascript/jquery
2018-12-5
-
php 实现抽奖功能 javascript/jquery
2020-6-26
-
CORS原理及@koa/cors源码解析 javascript/jquery
2019-6-29
-
正确地缩写 document.querySelector javascript/jquery
2019-4-2
-
ReactNative0.62更新内容,速看 javascript/jquery
2020-6-27
-
TestNG测试用例重跑详解及实践优化 javascript/jquery
2020-6-11
-
H5 唤醒APP javascript/jquery
2020-5-28
-
vue移动端 px2rem-loader与vux移动端组件库样式错乱,应该使用postcss-plugin-px2rem完美解决 javascript/jquery
2020-6-16
-
前端面试中的常见的算法问题 javascript/jquery
2017-12-12
-
前端架构之 JAMStack javascript/jquery
2020-5-26