引言
大概了解下,知道有哪些基础的概念先
完整配置篇,webpack 基础配置
下面的这些内容呢,有点杂乱,看极客时间视频学习记录,踩个脚印,知道都有啥,各项深入学习留待后续。。。。
自动构建更新
在生产环境中不需要webpack的更新
自动构建更新的两种形态方式: watch配置 与 热更新配置
一、Webpack文件监听: 在发现文件资源源码发现变化时自动重新构建输出新的资源文件
开启监听方式 watch 配置两种:
PS: 但该监听方式存在一个缺陷: 每次资源文件发生变化, webpack监听编译完成之后,需要开发者手动去刷新浏览器页面进行查看效果,监听生成的文件 放置于磁盘文件中
- 启动webpack命令时 带上—watch参数
- 在配置webpack.config.js中 设置 watch:true
Tips: watch监听原理解析 --- 轮询判断文件的最后编辑时间是否变化,当某文件发生变化,并不会立刻告诉监听着,而是先缓存起来,等待aggregateTimeout
二、热更新
热更新方式一: WDS
webpack-dev-server : 不输出文件,存放于内存中,不存于磁盘os中; 配合插件HotModuleReplacementPlugin(自动更新浏览器), 配置devServer
热更新方式二: WDM
Webpack-dev-middleware 需要引入node server,通常使用express 或者 koa 会将webpack输出的文件传输给到服务器 适用于较灵活的定制场景
Tips: 热更新原理实现
- webpack compiler 将JS编译成bundle
- HMR server 将热更新的文件输出给到HMR Runtime
- Bundle server 提供文件在浏览器的访问
- HMR Runtime 会被注入到浏览器中
启动阶段: 文件系统进行编译,将初始代码经过webpack comllier打包,传输给到bundle server,然后传递给到hmr runtime
更新阶段: 经过webpack compiler编译,hmr server传输给到hmr runtime (以json数据格式传输),hmr runtime 注入到浏览器中驱动更新且不用刷新页面
文件指纹策略
- 概念: 文件名添加的hash字符串,即文件指纹
- 作用: 版本管理,防止缓存,同时最大限度的利用缓存
文件指纹生成:
- Hash: 和整个项目的构建相关,只要项目文件出现修改,整个项目构建的hash就会改变,适用于图片,字体等资源文件
- chunkhash: 与webpack打包的chunk相关,不同的entry会生成不同的chunkhash值,一般用于js文件
- contenthash: 根据文件内容定义,文件内容不变则contenthash不变, 适用于css文件
module.exports = {
mode: 'development',
// ...
output: {
path: path.join(__dirname, 'dist'),
filename: '[name]_[hash:8].js' // js文件指纹设置,给定8位数hash值,name为占位符
},
// ...
}
代码压缩:
- Html压缩: 修改html-webpack-plugin,设置压缩参数
- Css压缩: 使用optimize-css-assets-webpack-plugin。同时使用cssnano(css预处理器);
- Js压缩: webpack4+中内置了uglifyjs-webpack-plugin,默认打包出来的js文件就是压缩的了,也可手动安装设置相关参数
Css压缩, 在webpack3之后可通过设置css-loader 参数进行压缩,但是css-loader在1.0版本之后移除了该参数,so,现在做css压缩无法通过css-loader参数设置进行压缩
- optimize-css-assets-webpack-plugin
- cssnano 预处理器
build: webpack —config webpack.prod.js。// 指定环境执行配置文件, 生产环境prod
dev: webpack-dev-server —config webpack.dev.js
自动清理构建目录
作用: 避免每次构建前手动删除dist文件夹
方法:
1. 使用依赖 clean-webpack-plugin(默认删除output指定输出目录)
2. 在命令行处添加rm -rf ./dist 语句
CSS3属性前缀自动补全
- 兼容不同浏览器内核
- 使用: postcss-loader插件autoprefixer CSS3样式使用 查阅网址: Can I use
移动端CSS px自动转换rem
- 引用loader: px2rem-loader
module.exports = {
// ...
module: {
rules: [
{
test: /.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader',
{
loader: 'px2rem-loader',
options: {
remUnit: 75, // 适合750设计稿,
remPrecision: 8 // 转换为rem后小数点8位
}
}
]
}
]
}
}
- 在output html中内联前置引入lib-flexible脚本 手淘的lib-flexible库: 动态计算px转换为rem单位的值, 在页面打开时自动计算转换值
资源内联的意义
-
代码层面:
- 页面脚本的初始化脚本
- 上报相关点 --- js/css初始化/加载完成节点的上报,
- css内联避免页面闪动
-
请求层面:减少http网络请求数量
- 小图片或者字体内联(url-loader)
html内联,js内联 可借助raw-loader进行
css内联存在两种方式:
1. style-loader
2. html-inline-css-webpack-plugin插件
<!DOCTYPE html>
<html lang="en">
<head>
<!-- 使用了htmlwebpackplugin插件,该插件内置默认使用的ejs模版,以下 <%= ‘code’ %> 为ejs语法 -->
<%= require('raw-loader!./meta.html') %>
<title>Document</title>
<script>
<%= require('raw-loader!babel-loader!../node_modules/lib-flexible/flexible.js') %>
</script>
</head>
<body>
<div id="root"></div>
</body>
</html>
占位符
| type | desc |
|---|---|
| [ext] | 资源后缀名 |
| [name] | 文件名称 |
| [path] | 相对路径 |
| [folder] | 文件所在的文件夹 |
| [contenthash] | 文件内容hash, 默认由md5生成 |
| [hash] | 文件内容hash, 默认由md5生成 |
| [chunkhash] | 构建生成的chunk文件hash, 默认由md5生成 |
| [emoji] | 一个随机指代文件内容的emoji |
| example: filename: '[name]_[hash:8].js' |
devtool
延伸阅读:
一文看懂 webpack 的所有 source map
Webpack devtool source map
source-map
作用: 通过source map 定位到源文件
开发环境默认开启,线上环境关闭
线上环境开启会暴露源码
排查错误问题时可以降sourcemap上传到错误监控系统中
关键字
devtool: 'source-map', // eval, inline, source -map, inline-source -map ....
- source map: 产生 .map文件, 单独列出文件,并在js文件中最后一行注明
在不使用source-map 配置的情况下打debugger断点调试时无法看到源码,只能看到压缩后的代码,难以进行定位
使用source-map的情况下,可以很清楚的看到源码:
-
Cheap: 不包含列信息
-
inline: 将.map作为dataURI嵌入,补单副产生map文件
-
module: 包含loader的source map,在出错时可以定位错误信息
-
Eval: 包裹模块代码
多页应用打包 MAP
- 每一次页面跳转,服务器都会返回一个新的html文档,这种类型的网站即多页网站,也称多页应用。
- 打包思路
- 每一个页面对应一个entry,一个html-webpack-plugin
- 缺点: 每次新增或删除页面,均需要修改webpack配置
约定:所有入口文件均放置于src文件中
改进做法:动态获取entry和设置html-webpack-plugin数量 利用glob.sync
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const glob = require('glob');
// 约定: 所有入口文件均放置于src文件中, 在src文件夹下各自的文件夹中以index.js的形式存在
const setMPA = () => {
const entry = {}, HtmlWebpackPlugins = [];
const entryFiles = glob.sync(path.join(__dirname, './src/*/index.js'));
// 当前的index不是item,是index
Object.keys(entryFiles).map((index) => {
const entryFile = entryFiles[index];
const match = entryFile.match(/src\/(.*)\/index\.js/);
const pageName = match && match[1];
entry[pageName] = entryFile;
HtmlWebpackPlugins.push(new HtmlWebpackPlugin({
template: path.join(__dirname, `src/${pageName}/index.html`),
filename: `${pageName}.html`,
chunks: ['vendors', pageName], //利用了splitChunks动态添加抽离的chunk,vendors为占位符
}))
})
return {
entry,
HtmlWebpackPlugins
}
}
// 将entry和HtmlWebpackPlugins置于module.exports对象中的相应位置即可
const { entry, HtmlWebpackPlugins } = setMPA();
体积优化 ~~~ start
tree shaking 摇树优化
MDN概念:
Tree shaking 是一个通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code) 行为的术语。
它依赖于ES2015中的 import 和 export 语句,用来检测代码模块是否被导出、导入,且被 JavaScript 文件使用。
在现代 JavaScript 应用程序中,我们使用模块打包(如webpack或Rollup)将多个 JavaScript 文件打包为单个文件时自动删除未引用的代码。这对于准备预备发布代码的工作非常重要,这样可以使最终文件具有简洁的结构和最小化大小。简化理解: tree shaking 就是将文件中需要用到的方法代码打入到bundle中,没有没用到的方法将在uglify阶段被擦除掉。
原理 & 注意事项
- 必须使用ES6语法,不支持CJS方式
- 使用时在.babelrc中设置
modules: false或设置mode: none即可;production mode模式下默认开启 - 方法中存在副作用则tree shaking会失效,即该方法代码不会被擦除
- 理解
DCE (Elimination)
Scope Hoisting
现象: 项目构建后的代码存在大量闭包代码
导致问题: 大量函数闭包包裹代码,导致bundle体积增大;运行代码时创建的函数作用域变多,内存开销变大
原理: 将所有模块代码按照引用顺序放在一个函数作用域中,然后适当的重命名变量以防止变量名冲突
对比: 通过scope hoisting 可以减少 函数声明代码量 和 内存开销。
了不起的 Webpack Scope Hoisting 学习指南
注意事项
- webpack3中需要添加代码:
- 必须使用ES6语法,不支持CJS方式
- 使用时设置
mode: none即可;production mode模式下默认开启
代码分割, 动态import
代码分割的意义: 将代码库分割成chunks,当代码运行到需要他们的时候再进行加载
使用场景:
- 抽离代码到相同的共享快中
- 脚本懒加载,使得出事下载的代码更小
懒加载JS脚本的方式
-
commonjs: require.ensure
-
ES6: 动态import (目前没有原生支持,需要babel支持转换): babel-plugin-syntax-dynamic-import
// 在 .babelrc 文件中的plugins中添加babel-plugin-syntax-dynamic-import,
// 在代码中使用的时候才import导入
function click() {
// 这种import方式返回的是一个promise函数,需要在then中获取结果值
import(‘../clickTimes.js’).then((res) => { console.log(res) })
}
webpack splitChunks
// 通过webpack4内置的splitchunks提取公共包
// 使用的时候需要将name: vendor放置到html模版chunks中,如下
module.exports = {
// output: ...,
// module: {},
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, `src/search/index.html`),
filename: `search.html`,
chunks: ['vendors', 'search'] // 'vendors' 为固定占位符
})
],
optimization: {
splitChunks: {
cacheGroups: {
// minSize: 2, // 设置引用的chunk模块的大小
deVendors: {
// test: /(react|react-dom)/,
// filename: '[name].js',
// 使用name字段时automaticNameDelimiter设置无效,展示为 deVendors.js
name: 'deVendors',
chunks: 'all',
minChunks: 2, // 要求最少被引用次数为2
// 当使用filename 而非name字段时,可设置连接符号,如当前效果展示为 deVendors--home--personal.js
// automaticNameDelimiter: '--',
},
},
},
},
}
公共资源包分离
-
外部CDN引入: 将不需要被修改的依赖进行外部CDN引入,降低bundle包的size
- 利用插件HtmlWebpackExternalsPlugin
module.exports = { entry: '', module: {}, plugins: [ new HtmlWebpackExternalsPlugin({ externals: [ { module: 'react', entry: 'https://unpkg.com/react@17/umd/react.development.js', global: 'React', }, { module: 'react-dom', entry: 'https://unpkg.com/react-dom@17/umd/react-dom.development.js', global: 'ReactDOM', }, ], }) ], }- 直接externals引入
module.exports = { entry: '', module: {}, externals: ['react', 'react-dom'], plugins: [], }上述两种cdn引入,使用的时候都需要在对应的html文件中引入cdn script链接
-
利用splitChunks将react , react-dom等公共资源包单独打包
module.exports = {
entry: '',
//...
optimization: {
splitChunks: {
cacheGroups: {
commons: {
test: /(react|react-dom)/,
name: 'vendors',
chunks: 'all',
}
},
},
}
}
动态polyfill
图片压缩
体积优化 ~~~ end
ESlint的落地
-
webpack 和CI/CD系统、集成
-
本地开发阶段增加precommit钩子
安装husky npm install husky —save-dev
package.json文件中增加npm script,通过lint-staged增量检查修改的文件,如果代码中存在预发问题,就会阻止代码的commit提交// package.json { “scripts”: { "precommit": "lint-staged" } “Lint-staged”: { “linters”: { “*.{js,css}”: [ “eslint —fix”, “git add” ] } } } -
在CI/CD阶段增加一个lint pipline,发布代码的时候在build的时候 进行检查 (可以避免本地开发的时候绕过本地precommit的检查机制后没有进行检查的bug)
-
-
webpack 和 ESlint。 (不适合老项目介入,默认检查所有的require文件)
已被弃用: eslint-loader 、babel-eslint
推荐使用: eslint-webpack-plugin @babel/eslint-parser
webpack打包库和组件
打包应用/js库
打包输出的库的名称
非压缩版: xxxx.js
压缩版:xxxx.min.js
暴露库:
library 指定库的全局变量,打包出来的库名;
librarytarget 指定库的引入方式
demo: 发布一个npm包、 npm 发包遇到的问题
webpack实现ssr打包
| 客户端渲染 | 服务端渲染 | |
|---|---|---|
| 请求 | 多个请求(HTML,数据等) | 1个请求 |
| 加载过程 | HTML&数据串行加载 | 1个请求返回HTML&数据 |
| 渲染 | 前端渲染 | 服务端渲染 |
| 可交互 | 图片等静态资源加载完成,js逻辑完成可交互 | 图片等静态资源加载完成,js逻辑完成可交互 |
总结: 服务端渲染(SSR)的核心是减少请求 (减少白屏时间,对SEO友好)
实现思路:
- 服务端:
- 使用react-dom/server 的renderToString方法将React组件渲染成字符串
- 由服务端路由返回对应的字符串模版
- 客户端:
- 打包出针对服务端的组件
webpack ssr打包存在的问题
- 浏览器的全局变量(Node.js中没有document,window)
- 组件适配: 将不兼容的组件根据不同的环境进行适配
- 请求适配: 将fetch或者ajax发送请求的写法改成isomorphic-fetch 或者axios
- 样式问题
- 方案一: 服务端打包通过 ignore-loader忽略css的解析
- 方案二: 将style-loader替换成isomorphic-style-loader
优化构建时命令行显示的log日志
侧重开发者关注点的输出日志
统计信息stats结合依赖 friendly-errors-webpack-plugin使用
module.exports = {
//...
stats: 'errors-only'
};
| Preset | Alternative | Description |
|---|---|---|
"errors-only" | none | 只在发生错误时输出 |
"minimal" | none | 只在发生错误或有新的编译时输出 |
"none" | false | 没有输出 |
"normal" | true | 标准输出 |
"verbose" | none | 全部输出 |
webpack 构建异常 及其 中断处理
如何判断构建的成功与否
在CI/CD 的pipline 或者发布系统需要知道当前的构建状态
每次构建完成之后输入 echo $? 获取错误码
webpack4之前的版本构建失败不会抛出错误码
nodejs中的process.exit规范
如何主动捕获并处理构建错误
module.exports = {
// ...
plugins: [
function () {
// compiler 在每次构建结束后都会触发done这个hook
this.hooks.done.tap('done', (stats) => {
if (
stats.compilation.errors
&& stats.compilation.errors.length
&& process.argv.indexOf('--watch') === -1
) {
console.log('build error')
process.exit(1);// 主动处理构建报错
}
})
}
],
}
编写可维护的webpack配置包
环境配置抽离成npm包的意义
- 通用性: 业务开发者无需关注构建配置,统一团队的构建脚本
- 可维护性: 构建配置合理的拆分,README文档、changelog文档等
- 质量: 冒烟测试、单元测试、测试覆盖率,持续集成
构建配置管理的可选方案
- 通过多个配置文件管理不同环境的构建,webpack --config 参数进行控制
- 将构建配置设计成一个库,比如: hjs-webpack、Nuutrino、webpack-blocks
- 抽成一个工具进行管理,比如: create-react-app、kyt、nwb等 将所有的配置放在一个文件中,通过--env参数控制分支进行选择
构建配置包的设计
- 通过多个文件管理不同环境的webpack配置
- 抽里成一个npm依赖包管理
通过webpack-merge合并基础配置和不同的环境配置
构建包的功能模块设计和目录结构设计
冒烟测试
- 构建是否成功
- 每次构建完成build目录是否有内容输出
- 是否有JS、CSS资源文件
- 是否有HTML文件
.....
单元测试 & 测试覆盖率
编写单元测试用例
- 技术选型: Mocha + Chai
- 测试代码: describe,it, except
- 测试命令: mocha add.test.js 单元测试接入
- 安装mocha + chai:
npm i mocha chai -D - 新建test目录,并增加xxxx.test.js
- 在package.json中的scripts字段中增加test命令
"test": "node_modules/mocha/bin/mocha" - 执行测试命令:
npm run test
持续集成
- 作用:
- 快速发现错误
- 防止分支的功能大幅偏离主干
- 核心措施: 代码集成到主干志气,必须通过自动化测试,只要有一个测试失败,就不能集成
。。。。。。
Git commit规范 & changelog
webpack4+ 性能更加的原因
V8带来的优化for of替代forEachMap和Set替代Objectincludes替代indexOf
- 默认使用更快的
md4 hash算法 webpacks AST可以直接从loader传递给AST,减少解析时间- 使用
字符串方法代替正则表达式
webpack 优化
分析构建速度 / 构建体积
-
初级分析: webpack内置的stats
-
nodejs中使用stats
缺点: 上诉两点的颗粒度比较粗,看不出问题所在 -
速度分析: speed-measure-webpack-plugin
- 每个loader和插件的耗时
- 分析整个包的耗时
-
体积分析: webpack-bundle-analyzer: 构建完成后会在8888端口展示大小
构建速度优化
- 使用高版本的webpack 和 node.js (构建时间差异大)
- 多进程多实例构建: 资源并行解析可选方案
官方:webpack4内置thread-loader
可选方案: HappyPack(可选中的主流^_^)、 parallel-webpack
多进程多实例并行压缩
- parallel-uglify-plugin 插件, 参考 深入浅出的webpack构建工具---ParallelUglifyPlugin优化压缩(十)
- uglifyjs-webpack-plugin 开启
parallel参数 - terser-webpack-plugin 开启
parallel参数
进一步分包,预编译资源模块
- 分包: 设置Externals
- 将react/react-dom基础包通过cdn引入,不打入到bundle中
- 使用html-webpack-plugin
- 分包: DLLPlugin
- 使用
DllReferencePlugin引用manifest.json
缓存提高二次构建速度
缓存思路
babel-loader开启缓存(解析阶段)terser-webpack-plugin开启缓存 (代码压缩阶段)- 使用
cache-loader或者hard-source-webpack-plugiin(模块转换阶段)
最小构建目标
目的: 尽可能的少构建模块,如babel-loader不解析node_modules文件
模块解析的过程中减少文件搜索范围,合理使用alias
- 优化resolve.modules配置(减少模块搜索层级)
- 优化resolve.mainFields配置
- 优化resolve.extensions配置 (文件后缀配置)
构建体积优化
- 图片压缩
- 基于Node库的imagemin 或者 tinypng API
- 使用: 配置image-webpack-loader
imagemin优点分析 : 定制选项、可引入第三方优化插件(如pngquant)、可处理多种图片格式
Errors 集合
神文参考:
webpack配置
由浅至深了解webpack异步加载背后的原理
webpack学习日志-12-文件指纹与代码拆分
webpack进阶篇(十七):静态资源内联