【webpack4系列】编写可维护的webpack构建配置(四)
构建配置包设计
构建配置管理的可选方案:
- 通过多个配置文件管理不同环境的构建,webpack --config 参数进行控制
- 将构建配置设计成一个库,比如:xxx-webpack
- 抽成一个工具进行管理,比如:create-vue-app
- 将所有的配置放在一个文件,通过 --env 参数控制分支选择
通过多个配置文件管理不同环境的 webpack 配置
- 基础配置:webpack.base.js
- 开发环境:webpack.dev.js
- 生产环境:webpack.prod.js
- SSR环境:webpack.ssr.js
- ……
抽离成一个 npm 包统一管理
- 规范:Git commit日志、README、ESLint 规范、Semver 规范
- 质量:冒烟测试、单元测试、测试覆盖率和 CI
通过 webpack-merge 组合配置
const merge = require("webpack-merge")
// 省略其他代码
module.exports = merge(baseConfig, devConfig);
功能模块设计和目录结构设计
功能模块设计
构建包设计:
- 基础配置:webpack.base.js
- 资源解析
- 解析ES6
- 解析vue
- 解析react
- 解析css
- 解析less
- 解析scss
- 解析图片
- 解析字体
- 样式增强
- CSS前缀补齐
- CSS px转成rem
- 目录清理
- 多页面打包
- 命令行信息显示优化
- 错误捕获和处理
- CSS提取成一个单独的文件
- 资源解析
- 开发配置:webpack.dev.js
- 代码热更新
- css热更新
- js热更新
- sourcemap
- 代码热更新
- 生产配置:webpack.prod.js
- 代码压缩
- 文件指纹
- Tree Shaking(webpack4自带)
- Scope Hositing(webpack4自带)
- 速度优化(基础包CDN等)
- 体积优化(代码分割)
- SSR 配置:webpack.ssr.js
- output的libraryTarget设置
- css解析ignore
目录结构设计
- lib 放置源代码
- test 放置测试代码
结构如下:
+ |- /test
+ |- /lib
+ |- webpack.dev.js
+ |- webpack.prod.js
+ |- webpack.ssr.js
+ |- webpack.base.js
+ |- README.md
+ |- CHANGELOG.md
+ |- .eslinrc.js
+ |- package.json
+ |- index.js`
使用ESLint规范构建脚本
使用 eslint-config-airbnb-base
eslint --fix
可以自动处理空格。
安装的插件:
npm i eslint@7 babel-eslint eslint-config-airbnb-base -D
我们在工程根目录下新建.eslintrc.js,代码如下:
module.exports = {
parser: "babel-eslint",
extends: "airbnb-base",
env: {
browser: true,
node: true
}
};
我们在packagejson中新建一条命令:
"scripts": {
"eslint": "eslint ./lib --fix"
},
webpack.base.js代码:
const path = require('path');
const glob = require('glob');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
const setMPA = () => {
const entry = {};
const htmlWebpackPlugins = [];
const entryFiles = glob.sync(path.join(__dirname, './src/*/index.js'));
Object.keys(entryFiles).map((index) => {
const entryFile = entryFiles[index];
const match = entryFile.match(/src\/(.*)\/index\.js/);
const pageName = match && match[1];
entry[pageName] = entryFile;
return htmlWebpackPlugins.push(
new HtmlWebpackPlugin({
template: path.join(__dirname, `src/${pageName}/index.html`),
filename: `${pageName}.html`,
chunks: [pageName],
inject: true,
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: false,
},
}),
);
});
return {
entry,
htmlWebpackPlugins,
};
};
const { entry, htmlWebpackPlugins } = setMPA();
module.exports = {
entry,
output: {
path: path.join(__dirname, 'dist'),
filename: '[name]_[chunkhash:8].js',
},
module: {
rules: [
{
test: /.js$/,
use: ['babel-loader'],
},
{
test: /.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
{
test: /.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
[
'autoprefixer',
{
overrideBrowserslist: ['last 2 version', '>1%', 'ios 7'],
},
],
],
},
},
},
{
loader: 'px2rem-loader',
options: {
remUnit: 75,
remPrecision: 8,
},
},
],
},
{
test: /.(png|jpe?g|gif)$/,
use: [
{
loader: 'file-loader',
options: { name: '[name]_[hash:8].[ext]' },
},
],
},
{
test: /.(woff|woff2|eot|otf|ttf)$/,
use: [
{
loader: 'file-loader',
options: { name: '[name]_[hash:8].[ext]' },
},
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name]_[contenthash:8].css',
}),
new FriendlyErrorsWebpackPlugin(),
function errorPlugin() {
this.hooks.done.tap('done', (stats) => {
if (stats.compilation.errors && stats.compilation.errors.length && process.argv.indexOf('--watch') === -1) {
process.exit(1); // 1表示错误码并退出
}
});
},
new CleanWebpackPlugin(),
].concat(htmlWebpackPlugins),
};
webpack.dev.js代码:
const { merge } = require('webpack-merge');
const webpack = require('webpack');
const baseConfig = require('./webpack.base');
const devConfig = {
mode: 'development',
plugins: [new webpack.HotModuleReplacementPlugin()],
devServer: {
contentBase: './dist',
hot: true,
stats: 'errors-only',
},
devtool: 'cheap-source-map',
};
module.exports = merge(baseConfig, devConfig);
webpack.prod.js代码:
const { merge } = require('webpack-merge');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
const cssnano = require('cssnano');
const baseConfig = require('./webpack.base');
const prodConfig = {
mode: 'production',
plugins: [
new OptimizeCssAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: cssnano,
}),
new HtmlWebpackExternalsPlugin({
externals: [
{
module: 'react',
entry: 'https://unpkg.com/react@18.2.0/umd/react.production.min.js',
global: 'React',
},
{
module: 'react-dom',
entry: 'https://unpkg.com/react-dom@18/umd/react-dom.production.min.js',
global: 'ReactDOM',
},
],
}),
],
optimization: {
splitChunks: {
minSize: 0,
cacheGroups: {
commons: {
name: 'commons',
chunks: 'all',
minChunks: 2,
},
},
},
},
};
module.exports = merge(baseConfig, prodConfig);
webpack.ssr.js代码:
const { merge } = require('webpack-merge');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
const cssnano = require('cssnano');
const baseConfig = require('./webpack.base');
const prodConfig = {
mode: 'production',
module: {
rules: [
{
test: /\.css$/,
use: 'ignore-loader',
},
{
test: /\.less$/,
use: 'ignore-loader',
},
],
},
plugins: [
new OptimizeCssAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: cssnano,
}),
new HtmlWebpackExternalsPlugin({
externals: [
{
module: 'react',
entry: 'https://unpkg.com/react@18.2.0/umd/react.production.min.js',
global: 'React',
},
{
module: 'react-dom',
entry: 'https://unpkg.com/react-dom@18/umd/react-dom.production.min.js',
global: 'ReactDOM',
},
],
}),
],
optimization: {
splitChunks: {
minSize: 0,
cacheGroups: {
commons: {
name: 'commons',
chunks: 'all',
minChunks: 2,
},
},
},
},
};
module.exports = merge(baseConfig, prodConfig);
package.json配置:
{
"name": "builder-webpack",
"version": "1.0.0",
"description": "",
"main": "index.js",
"directories": {
"lib": "lib",
"test": "test"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"eslint": "eslint ./lib --fix"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"babel-eslint": "^10.1.0",
"eslint": "^7.32.0",
"eslint-config-airbnb-base": "^15.0.0"
},
"dependencies": {
"autoprefixer": "^10.4.15",
"babel-loader": "^8.3.0",
"clean-webpack-plugin": "^3.0.0",
"css-loader": "^3.6.0",
"cssnano": "^4.1.11",
"express": "^4.18.2",
"file-loader": "^6.2.0",
"friendly-errors-webpack-plugin": "^1.7.0",
"glob": "^7.2.3",
"html-webpack-externals-plugin": "^3.8.0",
"html-webpack-plugin": "^4.5.2",
"less": "^4.2.0",
"less-loader": "^6.2.0",
"mini-css-extract-plugin": "^1.0.0",
"optimize-css-assets-webpack-plugin": "^5.0.8",
"postcss": "^8.4.28",
"postcss-loader": "^4.3.0",
"prettier": "^2.8.8",
"px2rem-loader": "^0.1.9",
"raw-loader": "^0.5.1",
"style-loader": "^2.0.0",
"url-loader": "^4.1.1",
"webpack": "^4.46.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.3",
"webpack-merge": "^5.9.0"
}
}
由于我们最终是要发布到npm上,并且要执行构建,所以依赖需要安装到dependencies。
冒烟测试介绍和实际运用
冒烟测试 (smoke testing)
冒烟测试是指对提交测试的软件在进行详细深入的测试之前而进行的预测试,这种
预测试的主要目的是暴露导致软件需重新发布的基本功能失效等严重问题。
冒烟测试执行
构建是否成
功
每次构建完成 build 目录是否有内容输出
- 是否有 JS、CSS 等静态资源文件
- 是否有 HTML 文件
判断构建是否成功
在示例项目里面运行构建,看看是否有报错
安装rimraf
插件
npm i rimraf -D
示例代码:
const path = require("path");
const webpack = require("webpack");
const { rimraf } = require("rimraf");
process.chdir(path.join(__dirname, "template"));
rimraf("./dist")
.then(() => {
const prodConfig = require("../../lib/webpack.prod.js");
webpack(prodConfig, (err, stats) => {
if (err) {
console.error(err);
process.exit(2);
}
console.log(
stats.toString({
colors: true,
modules: false,
children: false
})
);
console.log("webpack build success, run test start...");
});
})
.catch((err) => {
console.error(err);
});
判断基本功能是否正常
编写 mocha 测试用例
- 是否有 JS、CSS 等静态资源文件
- 是否有 HTML 文件
安装插件glob-all
、mocha
:
npm i glob-all mocha -D
编写一个检测html的html-test.js:
const glob = require("glob-all");
describe("checking generated html files", () => {
it("should generate html files", (done) => {
const files = glob.sync(["./dist/index.html", "./dist/search.html"]);
if (files.length > 0) {
done();
} else {
throw new Error("no html files generated");
}
});
});
编写一个检测css、js的css-js-test.js:
const glob = require("glob-all");
describe("checking generated html files", () => {
it("should generate html files", (done) => {
const files = glob.sync(["./dist/index_*.js", "./dist/index_*.css", "./dist/search_*.js", "./dist/search_*.css"]);
if (files.length > 0) {
done();
} else {
throw new Error("no css js files generated");
}
});
});
最后测试代码:
const path = require("path");
const webpack = require("webpack");
const { rimraf } = require("rimraf");
const Mocha = require("mocha");
const mocha = new Mocha({
timeout: "10000ms"
});
process.chdir(path.join(__dirname, "template"));
rimraf("./dist")
.then(() => {
const prodConfig = require("../../lib/webpack.prod.js");
webpack(prodConfig, (err, stats) => {
if (err) {
console.error(err);
process.exit(2);
}
console.log(
stats.toString({
colors: true,
modules: false,
children: false
})
);
console.log("webpack build success, run test start...");
mocha.addFile(path.join(__dirname, "html-test.js"));
mocha.addFile(path.join(__dirname, "css-js-test.js"));
mocha.run();
});
})
.catch((err) => {
console.error(err);
});
最后使用node执行这个代码js即可。
单元测试和测试覆盖率
测试框架
单纯的测试框架(mocha),需要断言库
- chai
- should.js
- expect
- better-assert
集成框架,开箱即用
- Jasmine
- Jest
编写单元测试用例
- 技术选型:Mocha + Chai
- 测试代码:describe, it, except
- 测试命令:mocha add.test.js
add.test.js示例代码:
const expect = require('chai').expect;
const add = require('../src/add');
describe('use expect: src/add.js', () => {
it('add(1, 2) === 3', () => {
expect(add(1, 2).to.equal(3));
});
});
单元测试接入
mocha官网:https://mochajs.org/,官网示例代码:
var assert = require('assert');
describe('Array', function () {
describe('#indexOf()', function () {
it('should return -1 when the value is not present', function () {
assert.equal([1, 2, 3].indexOf(4), -1);
});
});
});
- 1、安装 mocha + chai
npm i mocha chai -D
- 2、新建 test 目录,并增加 index.js 单位测试文件入口。
const path = require("path");
process.chdir(path.join(__dirname, "smoke/template")); // 修改当前工作目录
describe("builder-webpack test case", () => {
require("./unit/webpack-base-test.js");
});
- 3、test目录下新建unit目录,存放单元测试文件。
安装断言插件assert
:
npm i assert -D
例如test/unit/webpack-base-test.js代码:
const assert = require("assert");
describe("webpack.base.js test case", () => {
const baseConfig = require("../../lib/webpack.base.js");
it("entry", () => {
assert.equal(baseConfig.entry.index, "D:/builder-webpack/test/smoke/template/src/index/index.js");
assert.equal(baseConfig.entry.search, "D:/builder-webpack/test/smoke/template/src/search/index.js");
});
});
- 4、在 package.json 中的 scripts 字段增加 test 命令
"scripts": {
"test": "./node_modules/mocha/bin/_mocha"
},
mocha默认会执行工程目录下的test文件下的index.js。
其中
- 5、执行测试命令
npm run test
结果:
测试覆盖率
使用istanbul工具。istanbul 是一个 JavaScript 的代码覆盖率检查工具。
特征:
- 可检查包括语句、分支和函数覆盖,以及反向工程的代码行覆盖
- 模块加载钩子 可随时跟踪代码
- 命令行工具 可运行带覆盖率检查的 node 单元测试,不需要对测试运行进行协作
- 可生成 HTML 和 LCOV 报表
- 可作为中间件使用,在浏览器进行测试
- 可在命令行中以库的形式使用
- 基于 esprima 解析器和 escodegen 代码生成器
官网地址:https://github.com/gotwarlost/istanbul
安装:
npm i istanbul -D
当然也可以全局安装。
基本用法:
$ cd /path/to/your/source/root
$ istanbul cover test.js
例如上面单元测试入口scripts调整test命令:
"scripts": {
"test": "istanbul cover ./node_modules/mocha/bin/_mocha"
},
执行npm run test
结果如图:
说明:
- Statements:覆盖的语句
- Branches:覆盖的分支
- Functions:覆盖的函数
- Lines:覆盖的行数
持续集成和Travis CI
持续集成的作用
优点:
- 快速发现错误
- 防止分支大幅偏离主干
核心措施是,代码集成到主干之前,必须通过自动化测试。只要有一个测试用例失败,就不能集成。
Github 最流行的 CI
接入 Travis CI
-
1、首先 在github上创建一个新项目,上传存放的工程代码,创建示例:
-
2、https://travis-ci.org/ 使用 GitHub 账号登录
目前使用这个Travis CI现在已经改变运营策略,对开源项目也收费了。
- 3、在 https://travis-ci.org/account/repositories 为项目开启
- 4、项目根目录下新增 .travis.yml
travis.yml 文件内容
- install 安装项目依赖
- script 运行测试用例
language: node_js
sudo: false
cache:
apt: true
directories:
- node_modules
node_js:stable #设置相应的版本
install:
-npm install-D #安装构建器依赖
-cd ./test/template-project
- npm install -D #安装模板项目依赖
script:
- npm test
发布构建包到npm社区
添加用户: npm adduser
升级版本
- 升级补丁版本号:npm version patch
- 升级小版本号:npm version minor
- 升级大版本号:npm version major
发布版本:npm publish
具体操作:
- 1、先去github上拷贝创建的新项目到本地。
- 2、然后把本地的工程代码拷贝到git目录工程下。
- 3、npm login登录到npm上
若果以前用的淘宝镜像,那么需要切换回npm.
npm config set registry https://registry.npmjs.org/
然后再执行 npm login,后面输入账号密码以及游戏登录进去
-4 、npm publish 发布到npm上
Git 提交规范和changelog生成
良好的 Git commit 规范优势:
- 加快 Code Review 的流程
- 根据 Git Commit 的元数据生成 Changelog
- 后续维护者可以知道 Feature 被修改的原因
提交格式要求:
- feat:新增feature
- fix:修复bug
- docs:仅仅修改了文档,比如README,CHANGELOG, CONTRIBUTE等等
- style:仅仅修改了空格、格式缩进、都好等等,不改变代码逻辑
- refactor:代码重构,没有加新功能或者修复bug
- perf:优化相关,比如提升性能、体验
- test:测试用例,包括单元测试、集成测试等
- chore:改变构建流程、或者增加依赖库、工具等
- revert:回滚到上一个版本
本地开发阶段增加 precommit 钩子:
- 安装 husky
- 通过 commitmsg 钩子校验信息
- npm install husky --save-dev
"scripts": {
"commitmsg": "validate-commit-msg",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0"
},
"devDependencies": {
"validate-commit-msg": "^2.11.1",
"conventional-changelog-cli": "^1.2.0",
"husky": "^0.13.1"
}
遵守 semver 规范
概念:语义化的版本控制(Semantic Versioning),简称语义化版本,英文缩写为 SemVer
优势:
- 避免出现循环依赖
- 依赖冲突减少
语义化版本(Semantic Versioning)规范格式
- 主版本号: 做了不兼容的 API 修改(进行不向下兼容的修改)
- 次版本号: 做了向下兼容的功能性增加(API 保持向下兼容的新增及修改)
- 修订号: 做了向下兼容的问题修正(修复问题但不影响 API)