【写在前面】
在上一篇《教你搭建一个自己的脚手架(一)》中,我们介绍了一些脚手架的公共配置。完成了dev-server和对多文件格式的加载处理功能,这一节中,我们将继续完善配置,以期完成基础版的所有功能。
完整的code在这里Github 点进去。与此同时,我们再回顾一下我们的目标。
基础版 | 标准版 |
---|---|
dev-server 处理html/JS/VUE/CSS/LESS/SASS/IMG/JPG等各种文件 路由配置 mock数据 代理接口 打包分析 单元测试 编译构建 | dev-server 处理html/JS/VUE/CSS/LESS/SASS/IMG/JPG等各种文件 mock数据 代理接口 打包分析 单元测试 编译构建 基本布局 导航配置 支持TS 支持Markdown 风格检查 |
【看这里】
在上一节里,webpack.config.base.js
基本已经完成。我们需要针对开发和生产环境做一些差异化的配置。正如最开始设计的那样,我们将开发环境需要的配置放置到webpack.config.dev.js
中,生产环境需要的配置放置到webpack.config.prod.js
中。
对比我们的设计目标,不难发现mock数据、接口代理和打包分析是开发场景中特有的需求,在生产环境中不需要。因此,我们先搞定研发场景。
mock数据
数据mock其实原理比较简单,即拦截请求,加载用户事先配置好的mock数据,作为response返回即可。
社区中常见的方法可以分为两类,一类是直接在client的层面直接集成拦截钩子,比如axios有对应的axios-mock-adapter作为插件可以mock数据。另一类,是在server中间件的层面拦截请求。两种方式的区别在于,第一种方式并未真正的发出ajax请求,即在网络控制台中看不到network的记录;第二种方式发出了ajax请求,但请求在本地server被拦截了,请求并未真正从本机发出,但network面板中有记录存在。这里,我选择了第二种方式,原因显而易见,因为我们希望从chrome/firefox等用户代理的network面板中看到请求记录,方便我们调试和排查问题。
通过查询webpack-dev-server
的github,发现其实现是基于express的。express的中间件机制,允许我们注册自己的逻辑到调用链中。又因为并未看到有特别好的社区插件,我决定自己实现一个。
首先,它最后的使用方式应该是很简单的, 比如类似这样子:
/**
* @file mock/api/user.js
* @description mock api for user model
* @author nimingdexiaohai(nimingdexiaohai@163.com)
*/
module.exports = {
queryUser: {
url: /\/user\/\d$/,
method: 'get',
status: 200,
response: {
success: true,
message: 'get user info success',
data: {
id: 1,
name: 'John'
}
}
},
listUsers: {
url: /\/user$/,
method: 'get',
status: 200,
response: {
success: true,
message: 'list user info success',
data: {
users: [
{ id: 1, name: 'John' },
{ id: 2, name: 'Sharon' }
]
}
}
}
};
样例中给了两条mock配置,一个是获取某个特定用户的信息,另一个是获取所有的用户列表。它的字段语义还是很明确的:
// 需要拦截的路由path,支持字符串和正则匹配
url: string/reg
// 需要拦截的方法类型
method: get/post/put/delete/option
// 响应的状态码
status: HTTP_STATUS_CODE
// 响应的结构体,支持对象结构体和方法动态生成数据
response: Object/Function
下面,我们实现对路由的拦截和响应数据的加载功能。它应该提供一个单例模式的实例,应该有对外暴露的方法,可以设置状态、注册配置、加载钩子等。我们先来看下最后的实现,然后一一解读一下:
/**
* @file mock/lib.js
* @description export mock
* @author nimingdexiaohai(nimingdexiaohai@163.com)
*/
const _ = require('lodash');
const Mock = {
on: true,
rules: [],
};
Mock.Use = () => {
Mock.on = true;
};
Mock.Restore = () => {
Mock.on = false;
};
Mock.Reset = () => {
Mock.on = false;
Mock.rules = [];
};
Mock.Register = (c) => {
c['on'] = c.hasOwnProperty('on') ? c['on'] : true;
Mock.rules.push(c);
};
Mock.findRule = (path, method) => {
const result = {willMock: false, rule: null};
if (Mock.on) {
for (let r of Mock.rules) {
if (r.on
&& r.method.toUpperCase() === method
&& ((_.isString(r.url) && r.url === path) || (_.isRegExp(r.url) && r.url.test(path)))
) {
result.willMock = true;
result.rule = r;
break;
}
}
}
return result;
};
Mock.LoadMock = (req, res, next, app, server, compiler) => {
if (req && req.path && req.method) {
const {willMock, rule} = Mock.findRule(req.path, req.method);
if (willMock) {
console.log(`[${req.method.toUpperCase()}]${req.path} mocked by mocker...`);
if (_.isFunction(rule.response)) {
res.status(rule.status).send(rule.response(req, res));
}
else {
res.status(rule.status).send(rule.response);
}
return;
}
}
next();
};
module.exports = Mock;
我们定义了一个Mock对象,它的属性包括了字段Mock.on
和Mock.rules
,分别用来管控mock的生效状态和路由拦截配置。同时暴露了多个方法支持对内部状态进行修改,其中Mock.use
、Mock.Restore
、Mock.Reset
是对生效状态的修改,Mock.Register
和Mock.LoadMock
是注册路由拦截规则和加载mock主逻辑的接口。导出的方法使用大驼峰命名方式,内部方法采用小驼峰命名方式。
Mock.LoadMock
中传入请求和响应对象,对请求的方法、路由进行判断,一旦命中用户配置的规则,则加载配置的响应数据返回给用户代理,并直接return,阻断后续的调用链。否则执行next(), 继续下一个中间件逻辑。这里的response支持配置成一个方法,动态的生成数据或者远程加载数据等。
到这里,mock数据的主逻辑就有了。那这个中间件到底怎样注册呢?我们通过查询文档,发现devServer.before
中暴露了接口,可以让我们自己定义的逻辑先于所有其它中间件执行,所以我们把拦截逻辑放到这里就可以了。
// webpack.config.dev.js
const MockUp = require('../mock/index');
devServer: {
before: function(app, server, compiler) {
MockUp.registerAll();
server.use(function(req, res, next) {
MockUp.loadMock(req, res, next);
});
},
}
devServer.before
中首先执行了MockUp.registerAll
方法,这个方法并未出现在我们对Mock的定义中。其实这里是为了更好的解耦应用和库逻辑,也为了使用插件的时候更方便,又封装了一下Mock导出的方法。它的具体实现如下:
/**
* @file mock/index.js
* @description mock api entry
* @author nimingdexiaohai(nimingdexiaohai@163.com)
*/
const Mock = require('./lib');
const User = require('./api/user');
const apiModels = [User];
module.exports = {
registerAll: () => {
apiModels.forEach(model => {
Object.keys(model).forEach(key => {
Mock.Register(model[key]);
});
});
},
loadMock: (req, res, next, app, server, compiler) => {
Mock.LoadMock(req, res, next, app, server, compiler);
}
};
到这里,mock的整体脉络就比较清晰了。我们定义按实体model对象作为一个模块来划分mock的规则配置是一种好的实践,因此mock文件夹下有了这样的组织方式:
当然,你还可以根据自己的业务需求,在model下面再进行拆分。比api/user/common.js
和api/user/vip.js
,一切都是可以调整的。
接口代理
接口的代理作为一个常规的研发需求,已经有很多优秀的社区开源插件可以支持。比较知名的有http-proxy-middleware,可以直接拿来使用。同样因为webpack-dev-server
是基于express实现的原因,它可以用与mock同样的方式注册到devServer的中间件调用链中。
除此之外,devServer.proxy
默认集成了对外暴露的代理接口。我们可以直接使用,不再引入第三方插件,相关文档可以参考这里。它的使用方式,类似这样:
/**
* @file proxy.js
*
* @author nimingdexiaohai(nimingdexiaohai@163.com)
* @see https://webpack.js.org/configuration/dev-server/#devserverproxy
* @see https://github.com/chimurai/http-proxy-middleware
*/
module.exports = {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
pathRewrite: { '^/api': '/path' }
}
};
打包分析
在很多工作场景中,产品发布到了线上,会出现产出包size过大,导致首次加载较慢的问题出现。这种状态的改善,一般依赖于多个方面:启用Gzip压缩、使用CDN、拆分产出包。我们这里只关注第三个部分。
拆分代码是一个相对完整的体系范畴,它包含了多种拆分的原则和操作方法。这一点,我们在生产环境的配置中再详细解读。线上的包体积是要严格控制的,但优化的路径确是提前设计好的,因此我们需要一个工具来分析一下打包出来的产出体积和结构。
webpack-bundle-analyzer
是一个比较知名的开源实现,它以可视化的方式,展示了各种产出包的Size和依赖关系。
// webpack.config.dev.js
const webpack = require('webpack');
const { merge } = require('webpack-merge');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const baseWebpackConfig = require('./webpack.config.base.js');
module.exports = merge(baseWebpackConfig, {
plugins: [
new BundleAnalyzerPlugin({
// 'disabled' will close analyser
analyzerMode: 'server',
}),
],
});
这个插件会随着启动dev
脚本的时候,同步启动一个单独的server,并展示各种构建包和依赖关系。鼠标悬浮到某个方块,可以看到它的size信息。
其它配置
除了上面的配置,开发环境还需要有热加载、sourceMap等功能。完整的开发环境配置如下:
/**
* @file webpack.config.dev.js
* @author nimingdexiaohai(nimingdexiaohai@163.com)
*/
const webpack = require('webpack');
const MockUp = require('../mock/index');
const proxy = require('../src/common/proxy');
const { merge } = require('webpack-merge');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const baseWebpackConfig = require('./webpack.config.base.js');
module.exports = merge(baseWebpackConfig, {
mode: 'development',
devtool: 'eval-cheap-source-map',
stats: 'minimal',
plugins: [
new BundleAnalyzerPlugin({
// 'disabled' will close analyser
analyzerMode: 'server',
}),
new webpack.HotModuleReplacementPlugin(),
],
devServer: {
hot: true,
open: true,
before: function(app, server, compiler) {
MockUp.registerAll();
server.use(function(req, res, next) {
MockUp.loadMock(req, res, next);
});
},
// proxy: proxy
}
});
webpack.HotModuleReplacementPlugin
是启用热加载功能,是webpack^5
最新的写法。devtool
是配置了sourceMap的生成方式,根据不同的取值,其打包构建的速度也不同,最后生成的map信息粒度也不同。webpack^5
中的该字段取值,也发生了一些变化,具体可以参考devtool官方文档。
sourceMap的作用原理在另一篇文章《UNDERSCORE.js 源码解析(一)》中,做过详细的说明,这里不再赘述了。
生产环境的配置较为简单,出了产出构建包,再加一个代码拆分的配置就可以了。但为了配合在开发环节就设计好拆分原则,我们将拆分代码的配置抽取到了webapck.config.base.js
中。
/**
* @file webpack.config.base.js
* @author nimingdexiaohai(nimingdexiaohai@163.com)
*/
const path = require('path');
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const config = require('./config');
module.exports = {
...
plugins: [
new CleanWebpackPlugin(),
new MiniCssExtractPlugin({
filename: utils.genFilePathWithName('[name].css')
}),
],
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'initial',
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
};
optimization.splitChunks.cacheGroups
中定义了两个分组, priority字段定义了优先级,表示当资源同时满足两个分组的匹配时,优先使用哪个规则对资源进行处理。这里是将node_modules
中的三方依赖都打包到了vendor.js
中,initial表示只抽取与入口文件依赖共享的三方文件。default是另外一个默认分组,表示未命中vendor分组的都使用这条规则。minChunks表示当依赖同时被2个以上的资源文件引用时,就将其抽取成一个单独的包。yarn dev
启动脚手架,并访问http://localhost:8080/webpack-dev-server
就可以看到拆分后的包结构了:
当然,这里面包含了一些devServer生成的支持运行时调试的文件,但不影响我们关注和评估自己的配置所产生的拆分效果。关于代码拆分的其它配置项使用,可以参看codeSplitting专题和split-chunks-plugin插件。所以,生产环境的配置就只剩下一个mode
字段的配置,用来标识其环境场景:
/**
* @file webpack.config.prod.js
* @author nimingdexiaohai(nimingdexiaohai@163.com)
*/
const config = require('./config');
const webpack = require('webpack');
const { merge } = require('webpack-merge');
const baseWebpackConfig = require('./webpack.config.base.js');
module.exports = merge(baseWebpackConfig, {
mode: 'production',
stats: 'minimal',
});
测试
完成了开发环境和生产环境的基础配置。还需要集成一个测试框架来支持我们对一些核心逻辑配套单元测试。
"scripts": {
"dev": "cross-env NODE_ENV=development webpack serve --config ./build/webpack.config.dev.js",
"build": "cross-env NODE_ENV=production webpack --config ./build/webpack.config.prod.js --progress --color",
"test": "cross-env NODE_ENV=test karma start test/karma.conf.js"
},
我们选用了karma
+ mocha
+ karma-spec-reporter
构建测试框架。其中,karma是一个用于跑测试case的工具。mocha是一个可以同时支持运行在Node环境和Browser环境的测试框架,它。karma-spec-reporter是用来生成测试结果报告的。
在集成过程中,发现karma-webpack
还未支持webpack^5
。因此测试集成并未完全完成,又因为不愿意妥协降级到webpack4
,所以决定等待webpack^5
的支持后,再做进一步的动作。支持计划见github.com/ryanclark/k…
【总结】
到这里,脚手架的基础版就基本完成了。欢迎交流沟通~
微信同步更新: