前端知识体系4.前端工程化1.Webpack专题
本文目录:
- 1.webpack的定义及基础核心概念
- 2.webpack构建原理
- 3.webpack运行的基本流程
- 4.webpack 动态加载的实现原理及使用方法
- 5.loader的原理及手写loader的思路
- 6.plugin的原理及手写plugin的思路
- 7.loader和plugin的区别
- 8.tree sharking是什么
- 9.什么是webpack热更新
- 10.介绍下webpack5的新特性
- 11.Webpack性能优化
1.webpack的定义及基础核心概念
webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
webpack有四个核心概念:
入口(entry),输出(output),loader,插件(plugins)
webpack的入口文件模板结构:
module.exports = {
//入口配置
entry: '',
//出口配置
output: '',
//模块配置
module: {
rules: [
{
test: /\.css/,
use: ["style-loader", "css-loader"]
}
]
},
//插件配置
plugins: {},
//模式配置,开发模式还是生产模式
mode:'',
//开发服务器配置
devServer: {},
//解析配置
resolve: {}
}
2.webpack构建原理
webpack.config.js导出一个Object对象(或者导出一个Function,或者导出一个Promise函数,还可以导出一个数组包含多份配置)。Webpack从入口文件开始,识别出源码中的模块化导入语句,递归地找出所有依赖,然后把入口文件和所有依赖打包到一个单独的文件中(即一个chunk),这就是所谓的模块打包。
3.webpack运行的基本流程
webpack运行的基本流程分为初始化、编译、输出三个阶段.
初始化:
从配置文件和shell文件读取、合并参数;
加载plugin
实例化compiler
编译:
从entry发出,针对每个module串行调用对应loader编译文件内容
找到module依赖的module,递归进行编译处理
输出:
把编译后module组合成chunk
把chunk转换成文件,输出到文件系统
4.webpack 动态加载的实现原理及使用方法
在代码中所有被import()的模块,都将打成一个单独的包,放在chunk存储的目录下。在浏览器运行到这一行代码时,就会自动请求这个资源,实现异步加载。
ES6的import语法告诉我们,模块只能做静态加载。
所谓静态加载,就是你不能写成如下形式:
let filename = 'module.js';
import {mod} from './' + filename.
//也不能写成如下形式:
if(condition) {
import {mod} from './path1'
} else {
import {mod} from './path2'
}
先webpack4以后的版本的支持下,import可以进行动态加载,大致用法如下:import()接收一个路径参数,然后通过then的方式引入模块
let filename = 'module.js';
import('./' + filename). then(module =>{
console(module);
}).catch(err => {
console(err.message);
});
//如果你知道 export的函数名
import('./' + filename). then(({fnName}) =>{
console(fnName);
}).catch(err => {
console(err.message);
});
这里有一点要注意的是:
import的加载是加载的模块的引用。而import()加载的是模块的拷贝,就是类似于require(),怎么来说明?看下面的例子:
module.js 文件:
export let counter = 3;
export function incCounter() {
counter++;
}
main.js 文件:
let filename = 'module.js';
import('./' + filename).then(({counter, incCounter})=>{
console.log(counter); //3
incCounter();
console.log(counter); //3
});
原本的import写法:
import {counter, incCounter} from './module.js';
console.log(counter); //3
incCounter();
console.log(counter); //4
5.loader的原理及手写loader的思路
loader是 webpack 用于在编译过程中解析各类文件格式,并输出;
loader(加载器)是一个代码转换器,它由 webpack 的 loader runner
执行调用,接收原始资源数据作为参数(当多个加载器联合使用时,上一个loader的结果会传入下一个loader),最终输出 javascript 代码(和可选的 source map)给 webpack 做进一步编译。
手写loader的思路:
loader本质上就是一个 node 模块,通过写一个函数来完成自动化的过程。
这里通过写一个最简单的loader来理解手写loader的思路。
当只有一个 loader 应用于资源文件时,它接收源码作为参数,输出转换后的 js 代码。文件路径:loaders/simple-loader.js
module.exports = function loader (source) {
console.log('simple-loader is working');
return source;
}
这就是一个最简单的 loader 了,这个 loader 啥也没干,就是接收源码,然后原样返回,为了证明这个loader被调用了,我在里面打印了一句话‘simple-loader is working’。
测试这个 loader:
若是使用 npm 安装的第三方 loader,直接写 loader 的名字就可以了。但是现在用的是自己开发的本地 loader,需要我们手动配置路径,告诉 webpack 这些 loader 在哪里。
// webpack.config.js
const path = require('path');
module.exports = {
entry: {...},
output: {...},
module: {
rules: [
{
test: /\.js$/,
// 直接指明 loader 的绝对路径
use: path.resolve(__dirname, 'loaders/simple-loader')
}
]
}
}
执行webpack编译,可以看到,控制台输出 ‘simple-loader is working’。说明 loader 成功被调用。
6.plugin的原理及手写plugin的思路
wenpack根据自己的工作机制提供了许多hooks,类似于Vue的生命周期。
例如:run(开始编译阶段),make( 从 entry 开始递归分析依赖,准备对每个模块进行 build),done(完成所有的编译过程)
plugin必须是一个函数,或者是一个包含apply的对象。一般来说我们都会定义一个类型,然后在这个类型中定义apply方法,最后再通过这个类型来创建一个实例对象去使用这个插件。
例如下面这段代码
const pluginName = 'myplugin'
module.exports = class myplugin {
apply(){}
}
这个apply方法接收一个叫compiler的参数对象,这个对象是webpack工作中最核心的对象,包含了此次打包构建的所有配置信息,我们就可以通过这个对象去注册钩子函数。
const pluginName = 'myplugin'
module.exports = class myplugin {
apply(compiler){
compiler.hooks.run.tap(pluginName, () =>{
{
console.log('开始编译');
}
})
}
}
我们想在run阶段输出‘开始编译’这句话,在webpack.config.js中引入并配置
const myplugin = require('./myplugin')
...
plugins:[
new myplugin()
]
...
进行webpack编译,在控制台可以看到在开始阶段输出了内容,说明plugin生效了。
7.loader和plugin的区别
对于loader,它是一个转换器,将A文件进行编译形成B文件,这里操作的是文件,比如将A.scss转换为A.css,单纯的文件转换过程。
plugin是一个扩展器,它丰富了webpack本身,针对是loader结束后,webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听webpack打包过程中的某些节点,执行广泛的任务。
8.tree sharking是什么
Tree shaking 是一种通过清除多余代码方式来优化项目打包体积的技术。
我们在项目中创建一个utils.js文件:
export function add(a, b) {
console.log('add');
return a + b;
}
export function minus(a, b) {
console.log('minus');
return a - b;
}
export function multiply(a, b) {
console.log('multiply');
return a * b;
}
export function divide(a, b) {
console.log('divide');
return a / b;
}
index.js文件中导入utils.js的add方法并调用:
import { add } from './utils';
add(10, 2);
运行npm run build后查看dist/bundle.js文件,可以发现utils.js中所有的代码都打包了,并没有像我们预期的那样只打包add()函数。
CommonJS的动态特性模块意味着tree shaking不适用。因为它是不可能确定哪些模块实际运行之前是需要的或者是不需要的。在ES6中,进入了完全静态的导入语法:import。ES6的import语法完美可以使用tree shaking,因为可以在代码不运行的情况下就能分析出不需要的代码。
webpack4以后的版本,只需要将mode设置为production即可开启tree shaking。
9.什么是webpack热更新
模块热替换(HMR - Hot Module Replacement)允许在运行时替换,添加,删除各种模块,而无需进行完全刷新重新加载整个页面。
一个带有热替换功能的webpack.config.js 文件的配置如下,做了这么几件事
- 引入了webpack库
- 使用了new webpack.HotModuleReplacementPlugin()
- 设置devServer选项中的hot字段为true
10.介绍下webpack5的新特性
1.通过
嵌套tree-shaking的实现
移除Node.js polyfills 自动加载功能
有效减少打包后的文件体积。
2.生成的代码不再仅仅是ES5,也会生成 ES6 的代码
3.optimization配置中优化了minSize&maxSize的配置方式,对js和css有了区分,单位是kb
optimization: {
runtimeChunks: {},
splitChunks: {},
// 在文件大小为0-30kb的情况下进行文件分割
minSize: {
javaScript: 0,
style: 0
},
maxSize: {
javaScript: 30,
style: 30
}
}
4.在配置文件中使用cache: {type: "filesystem"}配置实现持久化缓存,提高构建速度
11.Webpack性能优化
优化可以从两个方面考虑,一个是优化开发体验,一个是优化输出质量。
优化开发体验
①缩小文件搜索范围
resolve字段告诉webpack怎么去搜索文件,所以首先要重视resolve字段的配置:
由于loader对文件转换操作很耗时,应该尽量减少loader处理的文件,可以使用include命中需要处理的文件,缩小命中范围。
②DllPlugin可以将特定的类库提前打包然后引入
DllPlugin是webpack的内置插件,这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案
③HappyPack
因为Node是单线程运行的,所以Webpack在打包的过程中也是单线程的,特别是在执行Loader的时候,这样会导致等待的情况,HappyPack可以将Loader的同步执行转换为并行的,HappyPack插件需要另外安装。
④使用source-map优化代码调试
在webpack.config.js中加入devtool:'source-map'可以让构建后代码出错,会通过映射关系追踪源代码错误。
实际开发中我们往往只需要在开发环境中开启source-map
const isProd = process.env.NODE_ENV === 'production';
module.exports = {
devtool: isProd
? false
: '#cheap-module-source-map',
}
⑤热更新HMR
利用webpack内置插件HotModuleReplacementPlugin,无需在每次更改内容时都重新加载整个页面。
优化输出质量
优化输出质量最大的好处就是可以减少首屏的加载时间
①按需加载路由
如果我们把十几个页面甚至更多的路由页面,把这些页面全部打包进一个JS文件的话,虽然将多个请求合并了,但是同样也加载了很多并不需要的代码,耗费了更长的时间。那么为了首页能更快地呈现给客户,这时候我们就可以使用按需加载,将每个路由页面单独打包为一个文件
②使用Tree Shaking,删除项目中未被引用的代码。
③开启Scope Hoisting
Scope Hoisting直译就是作用域提升,Scope Hoisting会分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中,让Webpack打包出来的代码更小、运行更快。
Scope Hoisting 是webpack内置的功能,只要配置一个插件即可
module.exports = {
plugins: [
// 开启 Scope Hoisting 功能
new webpack.optimize.ModuleConcatenationPlugin()
]
}
④区分环境--减小生产环境代码体积
代码运行环境分为开发环境和生产环境,代码需要根据不同环境做不同的操作,许多第三方库中也有大量的根据开发环境判断的if else代码,构建也需要根据不同环境输出不同的代码,所以需要一套机制可以在源码中区分环境,区分环境之后可以使输出的生产环境的代码体积减小。Webpack中使用内置DefinePlugin插件来定义配置文件适用的环境。
plugins:[
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify('production')
}
})
]
注意,JSON.stringify('production') 的原因是,环境变量值需要一个双引号包裹的字符串,而stringify后的值是'"production"'
然后就可以在源码中使用定义的环境:
if(process.env.NODE_ENV === 'production'){
console.log('你在生产环境')
doSth();
}else{
console.log('你在开发环境')
doSthElse();
}
⑤使用terser-webpack-plugin插件压缩JS代码
如果使用的是 webpack v5 或以上版本,你不需要安装这个插件。webpack v5 自带最新的 terser-webpack-plugin。如果使用 webpack v4,则必须安装 terser-webpack-plugin v4 的版本。
npm install terser-webpack-plugin --save-dev
然后将插件添加到你的 webpack 配置文件中
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
optimization: {
minimize: true,
minimizer: [new TerserPlugin()],
},
};
⑥压缩图片资源
对于某些网站,图像占据了页面很大部分,虽然它们不会阻塞页面渲染,但是它们仍然占用了很大一部分带宽,在webpack中可以使用url-loader来优化。
url-loader 可以将小型静态文件内联到应用程序中。如果不进行配置,它将把接受一个传递的文件,将其放在已编译的包旁边,并返回该文件的url。但是,如果指定 limit 选项,它将把小于这个限制的文件编码为Base64 数据的 url 并返回这个url,这会将图像内联到 JavaScript 代码中,从而可以减少一个HTTP请求。
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g|png|gif)$/,
loader: 'url-loader',
options: {
// Inline files smaller than 10 kB (10240 bytes)
limit: 10 * 1024,
},
},
],
}
};