进阶用法
另一份笔记,基于webpack4留作查询
1.自动清理构建目录
- 做法一:在npm scripts中修改命令:
rm -rf ./dist && webpack或rimraf ./dist && webpack - 做法二:使用
clean-webpack-plugin,默认会删除指定的输出目录
2.PostCSS插件autoprefixer自动补齐
适配多内核浏览器,CSS需补齐前缀。
autoprefixer是一个CSS后置处理插件(less、sass是预处理,指在打包前处理,而autoprefixer是在压缩之后,再进行处理)
PostCSS功能较全,除了补齐,还可以支持CSS Modules和style Lint等。
3.移动端px rem转换
使用px2rem-loader
。。。。。。。。。。。。没有看,后续做
4.静态资源内联
(1)资源内联的好处
-
代码层面
- 页面框架的初始化脚本(计算……)
- 上报相关打点(page start、css初始化、js初始化、js加载完成)
- css内联避免页面重新渲染导致页面闪动
-
请求层面
- 减少网络请求数量(
url-loader将小的图片/字体等内联到网页里面去)
- 减少网络请求数量(
(2)HTML和JS的内联
raw-loader:
- HTML:
<script>${require('raw-loader!babel-loader!./meta.html')}</script> - JS:
<script>${require('raw-loader!babel-loader!../node_modules/lib-flexible')}</script>
注:以上加babel-loader是为了在内联之前解析ES6语法并翻译成ES5
(3)CSS内联
-
借助
style-loadermodule.exports={ ... module:{ rules:[ { test:/.scss$/, use:[ { loader:'style-loader', options:{ insertAt:'top',//将样式插入到<head>标签中 singleton:true//是否将所有style标签合并 }, 'css-loader', 'sass-loader' } ] } ] } ... } -
常用:
html-inline-css-webpack-plugin
5.多页面打包方案
多页面应用(MPA):每次页面跳转时,web服务器都会返回一个新的html文档。
(1)多页面打包思路
-
每个页面对应一个
entry,一个html-webpack-plugin。缺点:每次新增或删除页面都会需要修改webpack配置(增删entry)。 -
动态获取entry并设置html-webpack-plugin的数量。➡️利用
glob.sync设置通用entry写法glob库的设计实现类似于linux的文件通配const glob = require('glob'); ... 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; HtmlWebpackPlugins.push(new HtmlWebpackPlugin({ template: path.join(__dirname, `src/${pageName}/index.html`), filename: `${pageName}.html`, chunks: [pageName], inject: true, minify: { html5: true, collapseWhitesapce: true, preserveLineBreaks: false, minifyCSS: true, minifyJS: true, removeComments:false } })) }) console.log('entryFiles',entryFiles); return { entry, HtmlWebpackPlugins } } const { entry,HtmlWebpackPlugins}=setMPA(); module.exports={ entry: entry, ... plugins:[ ... ].concat(HtmlWebpackPlugins), }
6.代码转换的位置信息source map
简单说,Source map就是一个信息文件,里面储存着位置信息。也就是说,转换后的代码的每一个位置,所对应的转换前的位置。
(1)webpack中source map关键字
- eval:使用eval包裹模块代码,每个代码块后面会有一个source map文件的URL,已确定指定的source map文件
- source map:产生的.map文件,浏览器开启后可调试
- cheap:不包含列信息,定位不到行列信息
- inline:将.map作为DataURI嵌入,不单独生成文件,内联到打包文件内
- module:包含loader的source map
(2)source map类型
webpack.docschina.org/configurati…
| devtool | 首次构建速度 | 二次构建 | 是否适合生产化境 | 可以定位的代码 |
|---|---|---|---|---|
| (none) 默认 | +++ | +++ | ✅ | 最终输出代码 |
| eval | +++ | +++ | ❌ | webpack生成的代码(一个个模块) |
| cheap-module-eval-source-map | o | ++ | ❌ | 源代码(只能看到行) |
| cheap-eval-source-map | + | ++ | ❌ | 经过loader转换后的代码(只能看到行) |
| cheap-module-source-map | o | - | ✅ | 源代码(只能看到行) |
| cheap-source-map | + | o | ✅ | 经过loader转换后的代码(只能看到行) |
| inline-cheap-module-source-map | o | - | ❌ | 源代码(只能看到行) |
| inline-cheap-source-map | + | o | ❌ | 经过loader转换后的代码(只能看到行) |
| inline-source-map | -- | -- | ❌ | 源代码 |
| hidden-source-map | -- | -- | ✅ | 源代码 |
| source-map | -- | -- | ✅ | 源代码 |
| eval-source-map | -- | + | ❌ | 源代码 |
7.提取页面公共资源
(1)基础库分离
-
将基础库(例如react、react-dom)通过cdn引入,不打入bundle中。
使用html-webpack-externals-plugin。
注:该库(3.8.0)与html-webpack-plugin(3.2.0)有冲突,使用
--force安装cdn引入react、react-dom要在body里引入,否则渲染之前找不到js
-
利用
splitChunksPlugin进行公共脚本分离webpack4内置,替代
CommonsChunkPlugin插件。配置:
module.exports={ optimization:{ splitChunks:{ chunks:'async', //设置包的大小 minSize:30000, maxSize:0, minChunks:1,//当引用次数大于1时,提取公共 maxAsyncRequests:5,//最多同时请求的异步资源的数量 maxInitialRequest:3, automaticNameLimiter:'~', name:true, cacheGroups:{ vendors:{ //匹配需要分离出的部分 test:/[\/]node_modules[\/]/ priority:-10 } } } } }chunks选项说明:
- async:(默认)异步引入的库进行分离
- Initinal:同步引入的库进行分离
- all:(推荐)所有引入的库进行分离
-
splitChunksPlugin分离基础包module.exports={ optimization:{ splitChunks:{ cacheGroups:{ commons:{ //匹配分离出来的库 test:/(react|react-dom)/, //命名为‘vendors’ name:'vendors', chunks:'all' } } } } } -
提取页面公共文件
module.exports={ optimization:{ splitChunks:{ //设置包的大小 minSize:0, cacheGroups:{ commons:{ //命名为‘vendors’ name:'commons', chunks:'all', //设置最小引用次数为2 minChunks:2 } } } } }
8.tree shaking摇树优化
(1)概念
一个模块有多个方法,当一个方法被使用时,整个文件都会被打包进bundle里去,tree shaking就是把用到的方法打到bundle里,不用的方法会在uglify阶段被擦除掉。
(2)使用
webpack默认支持;mode为production时默认开启;.babelrc文件中的modules为false即可
(3)条件
必须是ES6语法,CJS不支持。
(4)原理
利用ES6模块的特点:
- import、export只能作为模块的顶层语句出现
- import的模块名只能是字符常量(避免了动态设置的出现)
- import binding是immutable的(无法修改)
实际上是对代码的静态分析,不可以有不确定的代码
(5)其他概念
DCE(Dead code elimination)
死码消除。
死码消除的范围:
-
不会执行到,不可到达
例如:
if(false){……} -
代码的结果未被用到
-
代码只影响了死变量(只读不写)
9.Scope Hoisting
现象:
构建后代码存在大量闭包代码
(1)模块转换
模块转换为模块初始化函数。
结论:被webpack转换后的模块会带上一层包裹;import会被转化为__webpack_require
(2)模块机制
示例:
- 打包出来是一个IIFE(匿名闭包)
- modules是一个数组,每一项是一个模块初始化函数
- __webpack_require用来加载模块,返回module.exports
- 通过WEBPACK_REQUIRE_METHOD(0)启动程序
(3)原理
将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当重命名一些变量以防止变量名冲突
通过scope hoisting可以减少函数声明代码和内存开销
(4)使用
在webpack mode 为production时默认开启(实际上是开启了一个内置插件:webpack.optimize.ModuleConcatenationPlugin)
10.代码分割&&动态import
(1)场景
大型web项目如果所有文件都放在一个文件里会有太高的消耗,因此最好将代码库分割成chunks(语块),当代码运行到需要他们时再进行加载。
举例:
- 抽离相同代码到一个共享块
- js懒加载,使得初始下载的代码更小
(2)js懒加载
1-方式
- CommonJS:require.ensure
- ES6:动态import(目前还没有原生支持,需要babel转换)
2-使用
- 安装babel插件:
npm install @babel/plugin-syntax-dynamic-import --save-dev - 配置babel插件:
plugins:[ ... @babel/plugin-syntax-dynamic-import, ... ]
11.ESLint
规范实践:
- alloyteam团队 eslint-config-alloy(github.com/AlloyTeam/e…)
- ivweb 团队:eslint-config-ivweb(github.com/feflow/esli…)
大团队最好制定规则,示例:
- 不重复造轮子,基于
eslint:recommend配置并改进 - 能够帮助发现代码错误的规则并全部开启
- 在不限制开发体验的前提下,统一代码风格
(1)落地
1-与CI/CD集成
2-与webpack集成
使用eslint-loader,构建时检查。
配置:
module.exports={
...
module: {
rules: [
{
test: /.js$/,
use: [
"babel-loader",
"eslint-loader"
]
}
]
}
...
}
.eslintrc.js(依照个人的习惯设置、修改规则):
module.exports={
"parser":'babel-eslint',
"extends":"airbnb",
"env": {
"browser": true,
"node": true
},
"rules":{
"indent": ["error", 2]
}
}
12.打包组件和库
(1)步骤
- 实现组件
- 设置打包(是否压缩,min版需压缩)
- 编写入口文件index.js
- 修改package.json(有时需要)
(2)示例
const path=require('path');
const TerserPlugin=require('terser-webpack-plugin')
module.exports={
mode:'none',
entry:{
'myView':'./src/index.js',
'myView.min':'./src/index.js'
},
output:{
path: path.resolve(__dirname, 'dist/assets'),
filename:'[name].js',
library: 'myView',
libraryTarget:'umd',
//简化调用
libraryExport:'default'
},
optimization:{
//是否压缩
minimize:true,
//压缩配置
minimizer:[
new TerserPlugin({
include:/.min.js$/
})
]
}
}
说明:
libraryTarget:'umd'是为了在不同包管理模式下可用- 使用
TerserPlugin可以保证在ES6下可兼容,注意这个包的版本语法变化 - 入口文件index.js主要是用于区分环境加载版本
- 可以设置
prepublic等钩子做相应操作
13.SSR打包
SSR的核心就是减少请求数。
渲染:HTML+JS+CSS+Data => 渲染后页面
服务端渲染的优势:
- 资源都在服务端,可并行加载(相对的客户端只能串行加载)
- 内网资源,拉数据更快
- 合并后可减少请求,一个HTML返回所有
优点:
- 减少白屏时间
- SEO友好
(1)实现思路
服务端:
- 使用
react-dom/server的renderToString方法将react组件渲染成字符串 - 服务端路由返回对应模板
客户端:
打包出针对服务端的组件
(2)客户端模板实现
- 编写对应的webpack.ssr.js:入口文件更改;输出文件更改后缀;
libraryTarget需要是umd - 源代码的服务端适配(解决问题:ESM规范不可用;nodejs中没有浏览器全局变量document、window;nodejs无法解析css)
(3)问题解决方案
-
浏览器全局变量:
- 将不兼容的组件依据打包环境适配
- 将Fetch、ajax的请求改写为isomorphic-fetch或者axios
-
样式问题:
- 将style-loader替换为isomorphic-style-loader(会改变编写习惯,不推荐)
- 服务端打包通过ignore-loader忽略css的解析
- 使用原来打包出来的浏览器端html为模板,设置占位符,动态插入组件
(4)首屏数据处理
服务端处理数据,替换占位符。处理方式与模板相同,数据导入需要使用script标签包裹
14.优化构建时命令行的显示日志
(1)统计信息stats
| Preset | Alternative | Description |
|---|---|---|
"errors-only" | none | 仅发生错误时输出 |
"minimal" | none | 只在发生错误或有新的编译时输出 |
"none" | false | 没有输出 |
"normal" | true | 标准输出 |
"verbose" | none | 全部输出 |
默认全输出
(2)配置
在最外层/devServer添加
stats:'errors-only'
(3)利用插件优化
使用friendly-errors-webpack-plugin,plugins添加插件即可(需设置stats为errors-only)
- success:构建成功的日志提示
- warning:构建警告的日志提示
- error:构建报错的日志提示
15.构建异常与中断处理
查看构建错误码:构建完成后输入echo $?
(1)构建异常的处理
nodejs的process.exit规范
- 0表示成功完成,回调函数中,err值为null
- 非0表示执行失败,回调函数中,err不为null,
err.code就是传给exit的数字。
(2)构建异常的捕获
compiler在每次构建结束后会触发done这个hook
process.exit主动处理构建报错
plugins:[
function(){
//this对象即是compiler
this.hooks.done.tap('done',(stats)=>{
if(stats.compilation.errors&&stats.compilation.errors.length&&process.argv.indexOf('--watch')==-1){
console.error('build error')
process.exit(1)
}
})
}
]