前端工程化-webpack总结
工程化
从前端的开发流程来看,前端工程化主要包括技术选型、统一规范、测试、部署、监控、性能优化、重构和文档。
- 技术选型
这个环节最简单,只需从几个框架(如React、Angular、Vue等)中选择一个即可。当选择框架时,主要考虑2个因素:
- 团队或者技术负责人对所选框架的熟悉程度,是否有快速解决疑难问题的能力。
- 框架的流行度比较高,有更多的“轮子”可以用。最简便的刞断方法是看Google Trends、GitHub上star的数量和npm包上相兲库的数量,这些都是可参考的数据。
- 代码规范
在开发过程中,代码规范一直占较大的权重,因为一致的代码规范能促使团队更好地协作,降低代码维护的成本,更好地促进项目成员的成长,可以更容易地构建和编译代码,并对代码进行检视和重构。
- Airbnb规范,GitHub上的star数量为107k+,足见它的受欢迎程度。这套规范不仅包含了JavaScript的,还包含了React、CSS、Ruby、Css-in-JavaScript和Sass的,非常方便。
- standard规范:GitHub上的star数量为25.3k+。这个规范是通过npm包的形式安装的,然后在package.json文件的scripts中添加如下命令进行检查。
往往也会从工具层面来约束规范,如使用ESlint,Prettier等。
提交代码可分为以下几种情冴:工程结构变更、功能(feature)开发、bug修复、性能优化、代码文档变更、测试用例变更、代码回退和持续集成文件变更等。如此多的场景如果不能通过代码提交记彔快速辨别出提交的修改内容,那么会让问题追踪变得复杂和低效。
[业务表示][描述][提交人]
3. 、测试、部署、监控
测试包括单元测试、集成测试、样式测试、E2E测试以及测试覆盖率与代码变异测试等。这里接触不多,后面再补充。
部署的话,我是使用git+jenkins自动化构建部署环境,主要是提高开发效率,减少重复性工作。
监控的话。暂时没接触,后面补充。
4.性能优化
性能优化是一个很大话题,像路由懒加载等。
5. 重构
SPA,多页,多trunk
webpack原理
什么是 webpack ?
本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。webpack 通过 Tapable 来组织这条复杂的生产线。webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。-- 深入浅出 webpack 吴浩麟
webpack 构建流程
Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程 :
- 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。
- 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。
- 确定入口:根据配置中的 entry 找出所有的入口文件。
- 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
- 完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。
devtool
此选项控制是否生成sourcemap
以及如何生成sourcemap
。
使用 SourceMapDevToolPlugin
进行更细粒度的配置。查看 source-map-loader
来处理已有的 source map。
我这里记录的webpack4的devtool,webpack5多了几个配置,后续再更新
devtool | build | rebuild | production | quality |
---|---|---|---|---|
(none) | fastest | fastest | yes | bundled code |
eval | fastest | fastest | no | generated code |
cheap-eval-source-map | fast | fastest | no | transformed code (lines only) |
cheap-module-eval-source-map | slow | faster | no | original source (lines only) |
eval-source-map | slowest | fast | no | original source |
cheap-source-map | fast | slow | yes | transformed code (lines only) |
cheap-module-source-map | slow | slower | yes | original source (lines only) |
inline-cheap-source-map | fast | slow | no | transformed code (lines only) |
inline-cheap-module-source-map | slow | slower | no | original source (lines only) |
source-map | slowest | slowest | yes | original source |
inline-source-map | slowest | slowest | no | original source |
hidden-source-map | slowest | slowest | yes | original source |
nosources-source-map | slowest | slowest | yes | without source content |
实际效果对比:
- eval
每个模块被转化为字符串,在尾部添加//# souceURL
(指明eval前文件)后,被eval
包裹起来
- sourcemap
最原始的source-map实现方式,打包代码的同时生成一个sourcemap文件,并在打包文件的末尾添加//# souceURL
,注释会告诉JS引擎原始文件位置
3. hidden-source-map
打包结果与source-map
一致,但是.map
文件结尾不显示//# sourceMappingURL
- inline-source-map
为打包前的每个文件添加sourcemap的dataUrl,追加到打包后文件内容的结尾;此处,dataUrl包含一个文件完整 souremap 信息的 Base64 格式化后的字符串
- eval-source-map
将每个模块转化为字符串,使用eval包裹,并将打包前每个模块的sourcemap信息转换为Base64编码,拼接在每个打包后文件的结尾
- cheap-source-map
同source-map
,但不包含列信息,不包含 loader 的 sourcemap,(譬如 babel 的 sourcemap)
- cheap-module-source-map
不包含列信息,同时 loader 的 sourcemap 也被简化为只包含对应行的。最终的 sourcemap 只有一份,它是 webpack 对 loader 生成的 sourcemap 进行简化,然后再次生成的
推荐配置:
开发环境推荐:
cheap-module-eval-source-map
生产环境推荐:
cheap-module-source-map
tree shaking
tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块系统中的静态结构特性,例如 import 和 export。最早由rollup实现。
tree shaking的原理
- ES6 Module引入进行静态分析,故而编译的时候正确判断到底加载了那些模块
- 静态分析程序流,判断那些模块和变量未被使用或者引用,进而删除对应代码
// test
export const name = 123;
export const age = 9999;
//index.js
import {name, age} from './test.js';
console.log(name);
- webpack.config.js
const path = require("path");
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist"),
},
optimization: {
usedExports: false
},
devtool: "cheap-source-map" // 为了方便查看编译后的源码
};
编译后:
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "name", function() { return name; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "age", function() { return age; });
optimization.usedExports
告诉webpack为每个模块确定使用的导出
将 optimization.usedExports
改为true。编译后
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return name; });
/* unused harmony export age */
未被使用的export会被标记为/ unused harmony export name /,不会使用__webpack_require__.d进行exports绑定;
// define getter function for harmony exports
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};
develop模式开启压缩:
- minimize: true
/*! exports provided: name, age */
/*! exports used: name */function(e,t,r){"use strict";r.d(t,"a",(function(){return n}));const n=123}});
可以看到压缩的代码之引入了导出的name
编写Loader
loader 是导出为一个函数的 node 模块。该函数在 loader 转换资源的时候调用。给定的函数将调用 loader API,并通过 this
上下文访问。
配置环境:
module.exports = {
resolveLoader:{
modules:['node_modules', path.resolve(__dirname, "./loaders"),]
}
}
默认的loader都是从node_modules文件夹查找的,为了方便本地调试,需要用到resolveLoader
属性。
- 清除console.log语句
module:{
rules:[{
test: /\.(js)$/,
use:"cleanlog-loader"
}]
},
loaders文件
夹下新建cleanlog-loader.js
:
module.exports = function(source){
return source.replace(/console\.log\(.*\);?\n?/g, '');
}
- 获取参数
npm i loader-utils@2.0.2
注意:我使用webpak版本是V4,所有需要下载loader-utilsV2,如果是webpack5,直接调用this.getOptions
即可,无需loader-utils
loaders文件
夹下新建replace-name-loader.js
:
const loaderUtils = require('loader-utils');
module.exports = function(source) {
const opts = loaderUtils.getOptions(this);
return source.replace('__NAME__', opts.name);
}
webpack.config.js
module:{
rules:[{
test: /\.(js)$/,
use: ["cleanlog-loader", {
loader: "replace-name-loader",
options: {
name: "Loader",
}
}]
}]
},
这个loader会自动替换__NAME__
成Loader
,要注意loader函数的执行顺序是从右往左。
Plugin
官网: 插件目的在于解决 loader 无法实现的其他事。
webpack 插件是一个具有 apply
属性的 JavaScript 对象。apply
属性会被 webpack compiler 调用,并且 compiler 对象可在整个编译生命周期访问。
class myPlugin {
constructor(doneCallback, failCallback) {
this.doneCallback = doneCallback;
this.failCallback = failCallback;
}
apply(compiler) {
compiler.hooks.done.tap('myPlugin', (stats) => {
this.doneCallback(stats);
});
compiler.hooks.failed.tap('myPlugin', (err) => {
this.failCallback(err);
});
}
}
module.exports = myPlugin;
plugins: [
new myPlugin(
() => {
//throw new Error('Error!')
console.log("成功监听到结束事件,可以执行你想要的函数!");
},
(error) => {
console.log(error);
}
),
],
执行结果:
Plugin与Loader的区别
- 对于loader,它是一个转换器,将A文件进行编译形成B文件,这里操作的是文件,比如将A.scss转换为A.css,单纯的文件转换过程
- plugin是一个扩展器,它丰富了webpack本身,针对是loader结束后,webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听webpack打包过程中的某些节点,执行广泛的任务
参考资料
转载自:https://segmentfault.com/a/1190000041267559