当前位置: 首页 > news >正文

【webpack4系列】编写可维护的webpack构建配置(四)

文章目录

    • 构建配置包设计
    • 功能模块设计和目录结构设计
      • 功能模块设计
      • 目录结构设计
    • 使用ESLint规范构建脚本
    • 冒烟测试介绍和实际运用
      • 冒烟测试 (smoke testing)
      • 冒烟测试执行
      • 判断构建是否成功
      • 判断基本功能是否正常
    • 单元测试和测试覆盖率
      • 测试框架
      • 编写单元测试用例
      • 单元测试接入
      • 测试覆盖率
    • 持续集成和Travis CI
      • 持续集成的作用
      • Github 最流行的 CI
      • 接入 Travis CI
      • travis.yml 文件内容
    • 发布构建包到npm社区
    • Git 提交规范和changelog生成

构建配置包设计

构建配置管理的可选方案:

  • 通过多个配置文件管理不同环境的构建,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-allmocha

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_jssudo: falsecache:apt: truedirectories:- node_modulesnode_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)

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • Ubuntu 安装包下载(以20版本 阿里镜像站为例子)
  • Spring Boot-静态资源管理问题
  • Windows环境本地部署Oracle 19c及卸载实操手册
  • Vue3项目打包报错-内存溢出解决方法
  • vmvare如何给centos7 设置静态IP地址
  • 前端——JS基础
  • 第五章 继承、多态、抽象类与接口 (1)
  • 连续时间,离散频率 傅里叶
  • UVA-225 黄金图形 题解答案代码 算法竞赛入门经典第二版
  • Vue路由配置、网络请求访问框架项目、element组件介绍学习
  • 数据在内存中的存储方式
  • 测试-Gatling 与性能测试
  • 达梦数据库对象管理(一):分区表、外部表、临时表
  • Big Data 流处理框架 Flink
  • 【LeetCode】每日一题 2024_9_16 公交站间的距离(模拟)
  • Apache Zeppelin在Apache Trafodion上的可视化
  • Eureka 2.0 开源流产,真的对你影响很大吗?
  • Hexo+码云+git快速搭建免费的静态Blog
  • JavaScript函数式编程(一)
  • JAVA之继承和多态
  • js继承的实现方法
  • Leetcode 27 Remove Element
  • Linux链接文件
  • Octave 入门
  • PAT A1120
  • python大佬养成计划----difflib模块
  • react-core-image-upload 一款轻量级图片上传裁剪插件
  • Shell编程
  • SpiderData 2019年2月25日 DApp数据排行榜
  • Vue 重置组件到初始状态
  • webgl (原生)基础入门指南【一】
  • webpack入门学习手记(二)
  • 互联网大裁员:Java程序员失工作,焉知不能进ali?
  • 记一次和乔布斯合作最难忘的经历
  • 检测对象或数组
  • 理解在java “”i=i++;”所发生的事情
  • 码农张的Bug人生 - 初来乍到
  • 批量截取pdf文件
  • 七牛云假注销小指南
  • 悄悄地说一个bug
  • 区块链分支循环
  • 使用API自动生成工具优化前端工作流
  • 网络应用优化——时延与带宽
  • 温故知新之javascript面向对象
  • 用element的upload组件实现多图片上传和压缩
  • 主流的CSS水平和垂直居中技术大全
  • SAP CRM里Lead通过工作流自动创建Opportunity的原理讲解 ...
  • 如何在 Intellij IDEA 更高效地将应用部署到容器服务 Kubernetes ...
  • ​ssh-keyscan命令--Linux命令应用大词典729个命令解读
  • ​插件化DPI在商用WIFI中的价值
  • ​软考-高级-系统架构设计师教程(清华第2版)【第20章 系统架构设计师论文写作要点(P717~728)-思维导图】​
  • ‌移动管家手机智能控制汽车系统
  • #nginx配置案例
  • #systemverilog# 之 event region 和 timeslot 仿真调度(十)高层次视角看仿真调度事件的发生
  • (02)Cartographer源码无死角解析-(03) 新数据运行与地图保存、加载地图启动仅定位模式