第四篇伴学笔记~ 今天的内容是webpack工具的使用以及性能优化,属于方向三
webpack构建流程:
webpack构建从开始到结束是一个串行的过程,主要经历以下几个步骤,
1、初始化参数,读取webpack.config.js配置文件以及从shell语句中获取到一些必要的参数,融合配置,最终会得到一个初始化参数
2、将得到的初始化参数去初始化compiler这个对象,然后将所需要的webpack插件进行实例化,最后执行我们对象的run方法准备开始编译
3、开始编译,从配置文件中找到entry入口
4、从entry入口出发进行一个依赖收集,使用loaders进行模块文件的编译,并将模块id保存在dependencies的数组里,将模块导入导出语法替换成_webpack_require,保存转换后的模块代码,找到该模块所依赖的模块递归这个过程,直到所有入口依赖的文件都被编译收集到,最终生成一个AST抽象语法树
5、在第四步完成之后chunk会生成一个模块列表,列表包含模块之内的依赖关系,包含模块id(就是文件名),和模块转换的代码,也就是后面立即执行函数中参数module的key与value
6、根据输出路径和文件名把每个chunk转换成单一的文件bundle,(bundle实际上就是一个立即执行函数),加载到我们的输出列表中,写入文件系统
7、生成Source map提供构建报告
8、runtime监视文件变化,实现热模块(HMR)替换
webpack构建工具打包代码的性能优化:
1、SourceMap(源代码映射)
使用原因:因为开发时写错浏览器提示错误是编译后的代码中的错误,有时很难发现编译前哪里出错
它会生成一个xxx.map文件,里面包含了源代码和编译后代码之间的映射关系
使用:
(1)开发模式:cheap-module-source-map(只提示行问题)
(2)生产模式:source-map(行列问题都提示)
2、开发环境中提高打包构建速度
HotModuleReplacement(热模块替换HMR)
开发时修改某一个模块的代码,webpack就会默认将所有模块都进行打包编译,所以需要做到修改某一个模块代码就只有这个模块需要打包编译,其他不变,加快打包速度
devServer: {
hot: true //HMR,css会热更新,但是js还是强制更新
}
css可以,但是js还不行原因:是因为原理是style-loader带有热模块替换功能
js要实现热模块替换功能需要在main.js中写如下代码进行处理
`main.js`
if(module.hot){ //判断是否支持热模块替换功能
module.hot.accept("./js/sum") //将需要热模块替换的文件进行处理
}
但是vue-loader以及react-hot-loader也可以实现热模块替换
HMR需要搭配相应的插件使用,常用的是webpack.HotModuleReplacementPlugin。
plugins: [
new webpack.HotModuleReplacementPlugin(),
// ...其他插件
],
3、OneOf
一个文件在匹配loader时,就算前面匹配成功也会继续往下看去,但是一个文件只需要一个loader匹配,所以需要oneOf只让一个文件匹配一个loader来提高速度很有必要
`webpack.config.js`
modules:{
rules:[
{
//让每一个文件只经历一个loader的处理
oneOf:[
{
test:/.css$/,
use:[]
},
{
test://,
use:[]
}
]
}
]
}
4、exclude/include
因为外界的文件,比如node_module里的文件都是已经编译处理过的,所以不需要自己再编译处理,使用exclude将这些文件排除在外会提高编译速度(在eslint和loader规则中都需要配置)
- include //只处理xxx文件
- exclude //除了xxx文件其他都处理
主要针对js文件,css不处理(因为1、一般不引入外部css 2、就算引入也想和自己写的css打包在一起)
include和exclude只能出现其中一个配置项,否则报错
5、cache缓存
每次打包js文件都要经过eslint检查和babel编译,速度比较慢,我们可以缓存之前的eslint检查和babel编译结果,后面打包只需要对修改的文件进行检查和编译,可以让后面打包速度变快
babel缓存——在loader匹配中配置
{
test: /.js$/, //正则匹配js文件
exclude: /node_modules/, //排除node_modules中的js文件
loader: "babel-loader",
options: {
cacheDirectory: true,//开启babel缓存
cacheCompression:false //关闭缓存文件的压缩(不压缩只是会占自己电脑内存,上线的时候不影响,打包速度快,所以选择不压缩)
}
}
eslint缓存——在插件中配置
new eslintplugin({
context: path.resolve(__dirname, "src"),
exclude: /node_modules/,
cache: true,
cacheLocation:path.resolve(__dirname,"../node_modules/.cache/eslintcache")//配置缓存文件路径
})
6、多进程打包
js文件处理时使用eslint,babel,Terser(虽然没有配置过,但是一般js的自动压缩就是自动开启了这个工具)三个工具完成的,之前是一个个完成是单进程所以慢,现在使用多进程打包
但是注意:多个进程一般是要在特别耗时间的地方使用,因为每个进程启动就有大约600ms左右的开销
如何使用
1、获取CPU核数,因为启动进程的最大数量就是CPU的核数
const os = require("os")//nodejs核心模块
//cpu核数
const threads = os.cpus().length
2、配置thread-loader,因为要处理babel-loader所以放在babel-loader前面
//处理babel-loader
{
//js处理:eslint(使用插件)=>babel(使用loader)
test: /.js$/, //正则匹配js文件
exclude: /node_modules/,
use: [
{
loader: "thread-loader",
options: {
works:threads//进程数量
}
},
{
loader: "babel-loader",
options: {
cacheDirectory: true,
cacheCompression:false
}
}
]
}
//处理eslint插件
new eslintplugin({
//检测哪些文件,但是在vscode中使用eslint插件是检测所有文件的,可以在eslintignore文件里进行忽略配置
context: path.resolve(__dirname, "src"),
exclude: /node_modules/,
cache: true,
cacheLocation: path.resolve(__dirname, "../node_modules/.cache/eslintcache"),//配置缓存文件路径
threads //开启多进程
})
//处理terser(js压缩的内置插件)
const TerserWebpackPlugin = require("terser-webpack-plugin")
new TerserWebpackPlugin({
parallel:threads //开启多进程
})
7、减少代码体积TreeShaking
tree-shaking:是webpack中的一种代码优化策略,它会在运行过程中静态分析模块之间的导入和导出,确定esmodule中哪些导出值没有被其他模块使用,将死代码剔除从而提升应用程序的性能,treeshaking是在rollup中率先实现,后来才在webpack2中引入
原理步骤:(1)做静态分析,会对webpack所有模块进行静态分析,构建出一个依赖图(2)标记未使用代码(3)webpack会根据标记结果,生成输出文件
commonjs不支持treeshaking,因为可以动态导入
optimization: {
usedExports: true, // 启用Tree Shaking
}
8、对于babel
babel默认只转换新的 JavaScript 语法,比如箭头函数、扩展运算(spread),不转换新的 API:promise,async等,如果要转换需要polyfill
这里有三种polyfill:(1)babel-polyfill:解决了Babel不转换新API的问题,但是直接在代码中插入帮助函数,会导致污染了全局环境,并且不同的代码文件中包含重复的代码,导致编译后的代码体积变大(2)babel-runtime,提供了单独的包babel-runtime用以提供编译模块的工具函数,但是得手动引入(3)babel-plugin-transform-runtime插件,自动识别并替换代码中的新特性,解决了第二点手动引入的问题
普通babel不用polyfill也会加入辅助代码解析新语法,所以要使用这个插件,禁用了babel自动对每个文件的runtime注入而是引入,避免了辅助代码的重复引用,减小代码体积
怎么用(插件+corejs这个polyfill):
1、下载包
npm i @babel/plugin-transform-runtime -D
2、配置(因为babel开发模式和生产模式都要所以这个也是)
{
loader: "babel-loader",
options: {
cacheDirectory: true,//开启babel缓存
cacheCompression: false, //关闭缓存文件的压缩(不压缩只是会占自己电脑内存,上线的时候不影响,打包速度快,所以选择不压缩)
plugins:["@babel/plugin-transform-runtime"] //减少代码体积
}
}
3、使用core-js这个polyfill
npm下载core-js
方式一、全部引入core-js
import "core-js"
方式二、手动按需加载,手动引入需要的语法
比如引入promise语法
import "core-js/es/promise"
方式三、配置babel智能预设,自动按需加载
`babel配置文件中`
module.exports = {
presets: [
["@babel/preset-env", {
useBuiltIns: "usage",//自动按需引入
corejs:3,
targets:"> 0.25%, not dead" // 浏览器支持范围
}]
]
}
9、压缩
css压缩,js压缩,图片压缩ImageMinimizerWebpackPlugin
图片打包失败的话有一些错误可以看看这个视频,可能是有两个文件没下载成功41-高级-压缩图片哔哩哔哩bilibili
10、代码分割Code Split
chunk:打包进来的一个入口文件及所有它依赖为一个模块,这个模块就是chunk
bundle:webpack输出代码叫做bundle
(1)原因:我们想要在进入首页时只渲染首页的代码,但是此时所有的js代码都被打包到一个文件里,无法做到,此时就需要代码分割优化代码运行性能
(2)是什么:
1、分割文件:将打包生成的文件进行分割,生成多个js文件,将公共部分提取出来,将第三方库也提取出来
2、按需加载,动态导入:需要哪个文件就加载文件
(3)怎么做:
1、多页面应用要弄成多入口文件(但问题是:有可能重复引入相同代码)
提取公共部分:如果不同文件引入同一段逻辑代码,会重复加载,所以解决方法是将这同一段逻辑代码拆出来作为一个独立文件,其他的文件将这个文件引入即可,但webpack并不会默认加载一次,而是每次引入时都加载一次,要有配置,但是这时将所有引入多次的文件的代码独立出一个文件,第三方库要变成另外独立的文件要有其他配置
按需加载:使用imoprt动态导入语法,打包时会将动态导入的文件代码分割(拆分成单独模块),在需要使用的时候自动加载
`main.js`
document.getElementById("btn").onclick = function(){
import("./count")
.then((res)=>{
log("模块加载成功")
})
.catch((err)=>{
log("模块加载失败")
})
}
2、单页面应用——单入口文件
动态导入解决资源大的问题,如果是单入口文件,打包后会将所有的资源全在一个文件,那么在看这个页面的时候一下要加载所有的资源会太大了,加载速度慢,所以用到代码分割,将有些后面用到的且很大的资源使用异步处理后面使用,但是也会增加http请求
代码分割配置:
optimization:{
splitChunks:{
chunks:"all"
}
}//只需要配置这个,其他都用默认值即可
做的事1、node_module打包成一个文件 2、将公共重复代码抽离出来 3、将动态导入的文件抽离出来
不过eslint不识别动态导入语法,需要使用插件
eslint配置文件中要使用动态导入插件
plugins:["import"]
但是如果项目中有一些特别大且不会去改动的第三方库可以使用dllplugin提前打包减少代码的多次编译并且实现代码复用,但是特别注意!dll提前打包的文件不会自动在html引入需要手动引入
dll优化步骤:
(1)配置webpack.dll.config.js文件
const webpack = require('webpack')
module.exports = {
mode: 'production',
entry: {
vendor: [ //写用到的第三方库
"axios",
"lodash"
]
},
output: {
path: __dirname + "/dist/dll",
filename: "[name].dill.js",//记得要在html中手动引入
library: '[name]_library' //用于通知,定位哪些被打包
},
plugins: [
new webpack.DllPlugin({//webpack内置插件
path: __dirname + "/[name]-manifest.json", //通过这个插件会生成json,由这个json通知打包
name: '[name]_library',//必须和library同名
context: __dirname
})
]
}
(2)在正式打包前提前使用命令打包
{
"scripts": {
"build:dll": "webpack --config webpack.dll.config.js"
}
}
(3)让主配置知道哪些文件被dll打包过
`webpack.config.js`
plugins:[
new webpack.DllReferencePlugin({
manifest: require(__dirname + '/vendor-manifest.json')
}),
]
3、给动态打包的文件命名
import(/*webpackChunkName:"math"*/'./js/math').then(res => {
console.log(res.default)//这个default是a文件中暴露的值
})
//输出那里加个chunkFileName(打包输出其他文件命名),告诉webpack要用上这个名字
output:{
...
chunkFilename:"static/js/[name].js"
}