前端工程化(1)- Webpack
当然前端工程化包含的东西太多,思维导图里只是一部分我用过的选型。那从Webpack开始,如果有机会别的工具小鱼也会一点点的讲到。
文章目录
Webpack
Webpack 主要作用
Webpack 是一个静态模块打包工具。当 webpack 处理应用程序时,它会在内部构建一个依赖图,此依赖图对应映射到项目所需的每个模块(不局限于js文件),并生成一个或多个bundle。
Webpack 主要是配合各种 Loader 和 Plug 完成对不同类型文件的处理、优化、打包等工作。
编译代码能力:可以通过 Babel loader 将 ES6 编译成 ES5,提高效率,解决浏览器兼容问题。
模块整合能力:提高性能,可维护性,解决浏览器频繁请求文件的问题。
万物皆可模块能力,项目维护性增强,支持不同种类的前端模块类型( ES6 模块语法或 CommonJS 规范),统一的模块化方案,所有资源文件的加载都可以通过代码控制。
基础配置属性
mode:模式,默认production。
entry: 入口
output:输出
loader:模块转换器,用于把模块原内容按照需求转换成新内容
plugin:扩展插件,在webpack构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要做的事情
const path = require('path')
module.exports = {
mode: 'development', // 模式
entry: './src/index.js', // 打包入口地址
output: {
filename: 'bundle.js', // 输出文件名
path: path.join(__dirname, 'dist') // 输出文件目录
},
module: {
rules: [ // 转换规则
{
test: /\.css$/, //匹配所有的 css 文件
use: 'css-loader' // use: 对应的 Loader 名称
}
]
},
plugins: [
new webpack.ProgressPlugin(),
new HtmlWebpackPlugin({ template: './src/index.html' }),
],
}
mode
development 开发模式,打包更加快速,省了代码优化步骤
production 生产模式,打包比较慢,会开启 tree-shaking 和 压缩代码
none 不使用任何默认优化选项
可以直接在配置文件里写,也可以在命令参数里提供(可以根据不同的命令打包不同环境)。
$ webpack --mode=development
"scripts": {
"dev": "cross-env NODE_ENV=dev webpack serve --mode development",
"test": "cross-env NODE_ENV=test webpack --mode production",
"build": "cross-env NODE_ENV=prod webpack --mode production"
}
npm run build / npm run test / npm run dev
entry
大多时候是main.js或者、src/index.js文件
多个入口文件可以是数组或者对象形式,传递给entry属性。数组形式是一次性注入多个依赖文件,并将他们的依赖关系绘制成一个依赖图。对象形式是创建多个独立分离的依赖图。
loader 和 plugin
不同作用:
Loader直译为"加载器"。Webpack将一切文件视为模块,但是webpack原生是只能解析js文件,如果想将其他文件也打包的话,就会用到loader。 所以Loader的作用是让webpack拥有了加载和解析非JavaScript文件的能力。
Plugin直译为"插件"。Plugin可以扩展webpack的功能,让webpack具有更多的灵活性。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
不同用法:
Loader 在 module.rules 中配置,也就是说他作为模块的解析规则而存在。类型为数组,每一项都是一个 Object,里面描述了对于什么类型的文件(test),使用什么加载(loader)和使用的参数(options)。
Plugin 在 plugins 中单独配置。类型为数组,每一项是一个 plugin 的实例,一般情况,通过配置文件导出对象中 plugins 属性传入 new 实例对象。
常用loader
css-loader:对 css 文件进行解析,如果只通过 css-loader 加载文件,这时候页面代码设置的样式并没有生效。原因在于,css-loader 只是负责将 .css 文件进行一个解析,而并不会将解析后的 css 插入到页面中。
如果我们希望再完成插入 style 的操作,那么我们还需要另外一个 loader,就是 style-loader
rules: [
...,
{
test: /\.css$/,
use: {
loader: "css-loader",
options: {
// 启用/禁用 url() 处理
url: true,
// 启用/禁用 @import 处理
import: true,
// 启用/禁用 Sourcemap
sourceMap: false
}
}
}
]
style-loader:把 css-loader 生成的内容,用 style 标签挂载到 html 中。
rules: [
...,
{
test: /\.css$/,
use: ["style-loader", "css-loader"]
}
]
同一个任务的 loader 可以同时挂载多个,处理顺序为:从右到左,从下往上
引入 Less 或 Sass 文件要引入 less-loader 或 sass-loader + node-sass
const config = {
// ...
rules: [
{
test: /\.(s[ac]|c)ss$/i, //匹配所有的 sass/scss/css 文件
use: [
'style-loader',
'css-loader',
'postcss-loader',
'sass-loader',
]
},
]
},
// ...
}
raw-loader:引入 txt 文件。在 webpack 中通过 import 方式导入文件内容,该 loader 并不是内置的,所以首先要安装。
module.exports = {
...,
module: {
rules: [
{
test: /\.(txt|md)$/,
use: 'raw-loader'
}
]
}
}
file-loader: 引入图片等。把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件。
rules: [
...,
{
test: /\.(png|jpe?g|gif)$/,
use: {
loader: "file-loader",
options: {
// placeholder 占位符 [name] 源资源模块的名称
// [ext] 源资源模块的后缀
name: "[name]_[hash].[ext]",
//打包后的存放位置
outputPath: "./images",
// 打包后文件的 url
publicPath: './images',
}
}
}
]
url-loader: 类似 file-loader,它是将图片转成 base64 格式的字符串,并打包到 js 中,对小体积的图片比较合适,大图片不合适。
rules: [
...,
{
test: /\.(png|jpe?g|gif)$/,
use: {
loader: "url-loader",
options: {
// placeholder 占位符 [name] 源资源模块的名称
// [ext] 源资源模块的后缀
name: "[name]_[hash].[ext]",
//打包后的存放位置
outputPath: "./images"
// 打包后文件的 url
publicPath: './images',
// 小于 100 字节转成 base64 格式
limit: 100
}
}
}
]
https://www.cnblogs.com/frank-link/p/14836316.html
常用plugin
HtmlWebpackPlugin: 在打包结束后,⾃动生成⼀个 html ⽂文件,并把打包生成的 js 模块引⼊到该 html 中。
// webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
...
plugins: [
new HtmlWebpackPlugin({
title: "My App",
filename: "app.html",
template: "./src/html/index.html"
})
]
};
clean-webpack-plugin: 每次打包的时候,打包目录都会遗留上次打包的文件,为了保持打包目录的纯净,可以在打包前将打包目录清空。
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
// ...
plugins:[ // 配置插件
...
new CleanWebpackPlugin() // 引入插件
]
}
mini-css-extract-plugin: 可以通过style-loader 将样式通过 style 标签的形式添加到页面上,也可以通过 CSS 文件的形式引入到页面上。
// ...
// 引入插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const config = {
// ...
module: {
rules: [
// ...
{
test: /\.(s[ac]|c)ss$/i, //匹配所有的 sass/scss/css 文件
use: [
// 'style-loader',
MiniCssExtractPlugin.loader, // 添加 loader
'css-loader',
'postcss-loader',
'sass-loader',
]
},
]
},
// ...
plugins:[ // 配置插件
// ...
new MiniCssExtractPlugin({ // 添加插件
filename: '[name].[hash:8].css'
}),
// ...
]
}
DefinePlugin:可以创建一个在编译时可以配置的全局常量。主要针对我们在编译时,区分 开发、测试、生产环境。
因为node.js里的环境变量,process.env.NODE_ENV,只能在node的环境里拿到。而webpack.DefinePlugin提供的可以在浏览器环境里拿到。
new webpack.DefinePlugin({
PROCESS.VERSION: JSON.stringify('2.0.1'),
PROCESS.ENVIRONMENT: '"dev"'
})
// 在其他js里,就可以直接使用,比如:
const env = PROCESS.ENVIRONMENT;
https://blog.youkuaiyun.com/weixin_44677431/article/details/90345201
https://www.cnblogs.com/frank-link/p/14836898.html
模块打包运行原理
https://juejin.cn/post/6943468761575849992
- 读取 webpack 的配置参数;
- 用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
- 从入口文件(entry)开始解析,并且找到其导入的依赖模块,递归遍历分析,形成依赖关系树;
- 对不同文件类型的依赖模块文件使用对应的Loader进行编译,最终转为Javascript文件;
- 整个过程中webpack会通过发布订阅模式,向外抛出一些hooks,而webpack的插件即可通过监听这些关键的事件节点,执行插件任务进而达到干预输出结果的目的;
- 输出:将编译后的模块组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,输出到文件系统中。
其中文件的解析与构建是一个比较复杂的过程,在webpack源码中主要依赖于compiler和compilation两个核心对象实现。
compiler对象是一个全局单例,他负责把控整个webpack打包的构建流程。
compilation对象是每一次构建的上下文对象,它包含了当次构建所需要的所有信息,每次热更新和重新构建,compiler都会重新生成一个新的compilation对象,负责此次更新的构建过程。
Webpack 优化
https://juejin.cn/post/7023242274876162084
构建结果分析:借助插件 webpack-bundle-analyzer 我们可以直观的看到打包结果中,文件的体积大小、各模块依赖关系、文件是够重复等问题,极大的方便我们在进行项目优化的时候,进行问题诊断。
压缩CSS:optimize-css-assets-webpack-plugin
// ...
// 压缩css
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const config = {
// ...
optimization: {
minimize: true,
minimizer: [
// 添加 css 压缩配置
new OptimizeCssAssetsPlugin({}),
]
},
// ...
}
压缩JS:在生成环境下打包默认会开启 js 压缩,但是当我们手动配置 optimization 选项之后,就不再默认对 js 进行压缩,需要我们手动去配置。
optimization: {
minimize: true,
minimizer: [
// ...
new TerserPlugin({})
]
},
清除无用的CSS:purgecss-webpack-plugin 会单独提取 CSS 并清除用不到的 CSS。
const config = {
plugins:[ // 配置插件
// ...
new PurgecssPlugin({
paths: glob.sync(`${PATHS.src}/**/*`, {nodir: true})
}),
]
}
Tree-shaking: 剔除没有使用的代码,以降低包的体积。
webpack 默认支持,需要在 .bablerc 里面设置 model:false,即可在生产环境下默认开启。
Scope Hoisting:即作用域提升,原理是将多个模块放在同一个作用域下,并重命名防止命名冲突,通过这种方式可以减少函数声明和内存开销。
webpack 默认支持,在生产环境下默认开启;只支持 es6 代码
Code Splitting: 将代码按路由维度或者组件分块(chunk),这样做到按需加载,同时可以充分利用浏览器缓存。
sourceMap
https://juejin.cn/post/6943468761575849992
sourceMap是一项将编译、打包、压缩后的代码映射回源代码的技术,由于打包压缩后的代码并没有阅读性可言,一旦在开发中报错或者遇到问题,直接在混淆代码中debug问题会带来非常糟糕的体验,sourceMap可以帮助我们快速定位到源代码的位置,提高我们的开发效率。sourceMap其实并不是Webpack特有的功能,而是Webpack支持sourceMap,像JQuery也支持souceMap。
既然是一种源码的映射,那必然就需要有一份映射的文件,来标记混淆代码里对应的源码的位置,通常这份映射文件以.map结尾,里边的数据结构大概长这样:
{
"version" : 3, // Source Map版本
"file": "out.js", // 输出文件(可选)
"sourceRoot": "", // 源文件根目录(可选)
"sources": ["foo.js", "bar.js"], // 源文件列表
"sourcesContent": [null, null], // 源内容列表(可选,和源文件列表顺序一致)
"names": ["src", "maps", "are", "fun"], // mappings使用的符号名称列表
"mappings": "A,AAAB;;ABCDE;" // 带有编码映射数据的字符串
}
有了这份映射文件,我们只需要在我们的压缩代码的最末端加上这句注释,即可让sourceMap生效://# sourceURL=/path/to/file.js.map
有了这段注释后,浏览器就会通过sourceURL去获取这份映射文件,通过解释器解析后,实现源码和混淆代码之间的映射。因此sourceMap其实也是一项需要浏览器支持的技术。
https://blog.youkuaiyun.com/weixin_44730897/article/details/123922500 sourcemap的使用
Webpack 热更新原理
https://juejin.cn/post/6844904094281236487
Webpack 的热更新又称热替换(Hot Module Replacement),缩写为 HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。
热更新的核心就是客户端从服务端拉去更新后的文件,准确的说是 chunk diff (chunk 需要更新的部分),实际上服务器 (Webpack-dev-server) 与浏览器之间维护了一个 Websocket,当本地资源发生变化时,服务器端会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向服务器端发起 Ajax 请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向服务器端发起 jsonp 请求获取该chunk的增量更新。
Loader原理及开发
Webpack 最后打包出来的成果是一份 Javascript 代码,实际上在 Webpack 内部默认也只能够处理 JS 模块代码,在打包过程中,会默认把所有遇到的文件都当作 JavaScript 代码进行解析,因此当项目存在非 JS 类型文件时,我们需要先对其进行必要的转换,才能继续执行打包任务,这也是 Loader 机制存在的意义。
Loader 支持链式调用,所以开发上需要严格遵循“单一职责”,每个 Loader 只负责自己需要负责的事情。
Loader的配置使用:
// webpack.config.js
module.exports = {
// ...other config
module: {
rules: [
{
test: /^your-regExp$/,
use: [
{
loader: 'loader-name-A',
},
{
loader: 'loader-name-B',
}
]
},
]
}
}
通过配置可以看出,针对每个文件类型,loader是支持以数组的形式配置多个的,因此当Webpack在转换该文件类型的时候,会按顺序链式调用每一个loader,前一个loader返回的内容会作为下一个loader的入参。因此loader的开发需要遵循一些规范,比如返回值必须是标准的JS代码字符串,以保证下一个loader能够正常工作。
Loader的开发:
module.exports = function(source) {
const content = doSomeThing2JsString(source);
// 如果 loader 配置了 options 对象,那么this.query将指向 options
const options = this.query;
// 可以用作解析其他模块路径的上下文
console.log('this.context');
/*
* this.callback 参数:
* error:Error | null,当 loader 出错时向外抛出一个 error
* content:String | Buffer,经过 loader 编译后需要导出的内容
* sourceMap:为方便调试生成的编译后内容的 source map
* ast:本次编译生成的 AST 静态语法树,之后执行的 loader 可以直接使用这个 AST,进而省去重复生成 AST 的过程
*/
this.callback(null, content);
// or return content;
}
loader函数中的this上下文由webpack提供,可以通过this对象提供的相关属性,获取当前loader需要的各种信息数据,事实上,这个this指向了一个叫loaderContext的loader-runner特有对象。
Plugin 原理及开发
Plugin的开发和开发Loader一样,需要遵循一些开发上的规范和原则:
插件必须是一个函数或者是一个包含 apply 方法的对象,这样才能访问compiler实例;
传给每个插件的 compiler 和 compilation 对象都是同一个引用,若在一个插件中修改了它们身上的属性,会影响后面的插件;
异步的事件需要在插件处理完任务时调用回调函数通知 Webpack 进入下一个流程,不然会卡住;
class MyPlugin {
apply (compiler) {
// 找到合适的事件钩子,实现自己的插件功能
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation: 当前打包构建流程的上下文
console.log(compilation);
// do something...
})
}
}
类似构建工具Vite
https://fe.ecool.fun/topic/0d0de429-d6eb-4eda-a614-0a3a22c05237
模块化标准
为什么JavaScript会有多种共存的模块化标准?因为js在设计之初并没有模块化的概念,随着前端业务复杂度不断提高,模块化越来越受到开发者的重视,社区开始涌现多种模块化解决方案,它们相互借鉴,也争议不断,形成多个派系,从CommonJS开始,到ES6正式推出ES Modules规范结束,所有争论,终成历史,ES Modules也成为前端重要的基础设施。
- CommonJS:现主要用于Node.js(Node@13.2.0开始支持直接使用ES Module)
- AMD:require.js 依赖前置,市场存量不建议使用
- CMD:sea.js 就近执行,市场存量不建议使用
- ES Module:ES语言规范,标准,趋势,未来
Webpack痛点
现在常用的构建工具如Webpack,主要是通过抓取-编译-构建整个应用的代码(也就是常说的打包过程),生成一份编译、优化后能良好兼容各个浏览器的的生产环境代码。在开发环境流程也基本相同,需要先将整个应用构建打包后,再把打包后的代码交给dev server(开发服务器)。
Webpack等构建工具的诞生给前端开发带来了极大的便利,但随着前端业务的复杂化,js代码量呈指数增长,打包构建时间越来越久,dev server(开发服务器)性能遇到瓶颈:
- 缓慢的服务启动: 大型项目中dev server启动时间达到几十秒甚至几分钟。
- 缓慢的HMR热更新: 即使采用了 HMR 模式,其热更新速度也会随着应用规模的增长而显著下降,已达到性能瓶颈,无多少优化空间。
缓慢的开发环境,大大降低了开发者的幸福感,在以上背景下Vite应运而生。
处理流程对比
Webpack通过先将整个应用打包,再将打包后代码提供给dev server,开发者才能开始开发。
Vite直接将源码交给浏览器,实现dev server秒开,浏览器显示页面需要相关模块时,再向dev server发起请求,服务器简单处理后,将该模块返回给浏览器,实现真正意义的按需加载。
优缺点
优势:快
不足:Vue仍为第一优先支持,量身定做的编译插件,对React的支持不如Vue强大。