阅读本文前建议先了解一下webpack基本使用(B站都有视频,两个小时基本就能讲完基本使用),以及跟着视频手写 Webpack 明白打包原理手写一个webpack(了解一下webpack打包原理,这个视频是我看的,时长70min),如果大致webpack使用及原理都知道,建议直接阅读。
1,前端框架设计MVC,MVVM以及MVP三种模式。
-
MVC,MVP,MVVM三者都是软件架构模式,且MVP与MVVM均是在MVC基础上改进而来,他们的目的都是将视图层与模型层进行分离,他们的MV都是一样的,至于C,P,VM都是用来协调视图层与模型层。
-
MVC:M(model):模型层(保存数据处理数据逻辑的地方),V(view):视图层(数据展示的地方),C(controller):控制层。MVC作为一种软件架构模式,主要目的就是将数据与视图(数据展示)进行分离协调。
- 用户访问视图层触发交互操作,视图层将交互操作交给控制层处理交互逻辑(当然用户也可以直接触发控制层执行交互逻辑。),
- 控制层处理完毕将交互结果交给模型层,
- 模型层更新数据同时通知视图层的展示更新
-
MVP:M(model):模型层,V(view):视图层,P(presenter):中间者。MVP即MVC的改良模式,即presenter在协调视图层与模型层的同时,也将二者完全解耦,模型层不再通知视图层视图展示,视图层也不可以访问模型层。
- 视图层接收到用户事件,将事件交给presenter处理,同时视图层暴露视图更新接口
- presenter接收到用户事件,对事件进行逻辑处理以及模型层的数据更新
- 当模型层发生变化时,将改变的数据交给presenter,presenter根据变化的数据以及先前视图层暴露的视图更新接口更新视图
-
MVVM:M(model):模型层,V(view):视图层,VM(view-model):视图模型层。它与MVP很像,区别于MVP即将P替换成VM,它采用了双向绑定,V的变动自动体现在VM中,反之亦然。
2,js模块化(Commonjs,AMD,CMD,es6)
-
模块化的作用:防止全局污染,更好的组织项目代码。
-
Commonjs:主要用于服务端,Commonjs中一个文件即一个模块,require同步加载模块,exports或者module.exports导出数据。Commonjs输出的是值拷贝(对于基础类型值),且是运行时加载,即代码执行时遇到require代码时加载该模块,加载执行完毕之后继续执行其他代码 服务端加载资源一般存在本地硬盘即使同步加载速度也很快,而浏览器端同步加载意味着浏览器资源加载阻塞,所以这不适用浏览器环境。(比如js加载执行阻塞dom解析,当前js文件reuqire大量其他资源,其他资源同步加载,导致dom解析阻塞很久)
-
AMD:AMD即异步模块定义,主要用于浏览器端,requirejs实现了该规范。它的特点是模块的异步加载与前置依赖。即浏览器加载资源AMD模块资源时相当于这些资源加上了async,所以资源下载时不会阻塞html解析,同时当前模块所有依赖模块都先加载解析才执行当前模块回调。
// define:定义模块 // callBack为当前模块加载完依赖模块时执行的回调函数,参数对应dependencies中依赖模块,返回值即当前定义模块需要导出的数据 define(dependencyID = '当前模块名', dependencies = ['当前模块使用到的其他模块'], callBack); // require:使用模块, // callBack为当前模块加载完依赖模块时执行的回调函数,参数对应dependencies中依赖模块 require(dependencies = ['回调函数使用到的其他模块'], callBack) // index.html 引入requirejs // <script data-main='./AMD/main.js' src="https://requirejs.org/docs/release/2.3.6/minified/require.js"></script> -
CMD:CMD即通用模块定义,seajs实现了该规范。它与AMD与Commonjs都很像,但它是按需加载,依赖就近,即CMD模块中代码执行过程中需要使用其他模块即可使用require加载该模块,而AMD为其回调执行之前先把模块加载完毕
// define:定义模块 // require, exports, module 类似commonjs用法 define(function (require, exports, module) { // require按需加载当前模块用到的其他模块 const m3 = require('./m3.js') // exports/module.exports导出当前模块数据 exports.data = m3 }); // seajs.use:使用模块,类似于AMD的require // dependencies为回调函数回调函数使用到的其他模块 // callback为当前模块加载完依赖模块时执行的回调函数,参数对应dependencies中依赖模块 seajs.use(dependencies = ["./CMD/module/m1.js", "./CMD/module/m2.js"], callback = function (m1, m2) { }); // index.html 引入seajs // <script src="https://cdn.bootcdn.net/ajax/libs/seajs/3.0.3/sea.js"></script> // <script src="./CMD/main.js"></script> -
es6模块:es6使用import...from...导入数据,export/export default导出数据,输出的是值引用,编译时加载。即引入的数据为当前导出数据的引用,导出数据发生变化,引入数据发生变化,同时,因为我们浏览器当前不支持es6模块,一般使用webpack环境先编译,即编译时就对es6模块进行加载,所以浏览器运行时运行的是编译后的es6模块
-
es6模块与commonjs区别
- CJS输出的是数据的拷贝(引用类型浅拷贝,基本类型直接拷贝),es6模块输出的是对模块数据的引用
- CJS是运行时加载,ESM是编译时输出接口
- CJS中require是同步加载,ESM中import是异步加载,有一个独立加载解析模块的阶段
- CJS模块内部默认非严格模式(this指向当前模块),ESM模块内部为严格模式(this指向undefined)
- CJS输出值可以修改,ESM输出值不可修改(只读)
3,什么是webpack及webpack的构建流程
-
什么是webpack: webpack是一个用于现代js应用程序的 静态资源打包工具, 当webpack处理应用程序时,它会从入口文件开始根据文件之间的依赖关系(依赖关系根据模块化语句来判定)递归构建一个依赖关系图,依赖图对应映射到项目所需要的每个模块,最后将这些模块打包成一个或多个bundle。
-
webpack的构建流程:
-
1:webpack读取命令行配置与webpack.config.js文件配置,合并初始化本次webpack配置参数,并执行配置中的插件实例化语句,之后webpack生成compiler对象并注册插件对象的apply方法。。
-
2,webpack从入口文件开始,对所有(依赖)文件,递归遍历。
-
3,webpack进入其中一个入口文件,开始编译(compilation),首先使用配置loader对文件内容进行处理,再将文件处理成AST静态语法树,并继续分析文件依赖关系逐个拉取依赖模块重复该过程,最后将所有模块中的require替换成__webpack_require__模拟模块化操作。
-
4,最后进入emit(提交)阶段,此时所有模块文件的编译转化完成,将这些模块整合后的资源输出至输出目录。我们可以在传入事件回调的compilation.assets拿到这些数据,包括即将输出的资源,代码块chunk等信息。
-
3,关于loader
-
什么是loader:
-
loader本质是一个函数,该函数接收参数即loader所匹配到文件的内容,我们可以在该函数内对文件内容进行预处理,最后返回处理完毕的文件内容。
-
loader用于对模块的源代码进行转换,即当我们在'import'或者加载模块时loader会对文件进行预处理。 loader的存在为webpack处理模块提供了更强大的能力,例如压缩,打包,语言转译等。
-
-
实现loader: 实现的loader需要在每个js文件后面添加一行代码,用于在控制台输出作者名字。
// printAuthorLoader实现 // 文件路径:./myLoaders/printAuthorLoader const loaderUtils = require('loader-utils') // 1,loader本质就是一个函数,该函数接受一个参数,该参数即loader匹配到的文件的内容 module.exports = function printAuthorLoader(content) { // 2,安装loaderUtils,loaderUtils.getOptions(this) 可以帮助我们获取配置文件中loader中的option const options = loaderUtils.getOptions(this) // 3,在原来JS文件内容后面加上代码 ;console.log('${options.authorName}'); 并输出该文件内容(实现我们的业务需求) return content + `;console.log('${options.authorName}');` // 4,或者使用this.callBack 输出该文件内容,这种输出方式是推荐的, // this.callBack第一个参数是loader处理中将要抛出的错误 // 第二个参数为loader处理后文件的内容 this.callBack(null, content + `;console.log('${options.authorName}');`) }// 文件路径:./webpack.config.js // webpack.config.js中其他的配置省略 ...... module: { rules: [ { // 1,匹配所有除去node_modules中的js文件应用当前loader test: /\.js$/, exclude: /node_modules/, use: { // 2,使用我们的自定义loader:printAuthorLoader // './myLoaders/printAuthorLoader' 即自定义loader在项目中的路径 loader: path.resolve('./myLoaders/printAuthorLoader'), // 3,传入自定义loader的配置选项 // 在我们loader中使用loaderUtils.getOptions(this)即可获取到该配置对象 options: { authorName: 'Tsuki' } } } ] } -
异步loader实现: 在上面loader基础上我们可能有某些异步任务需要处理,处理完毕后才在js文件后面添加一行输出作者信息代码,我们可以使用async函数实现该异步loader。如下:
const loaderUtils = require('loader-utils') module.exports = async function printAuthorLoader(content) { // 处理一些异步任务 await new Promise(r => setTimeout(r, 2000)) // 获取loader配置 const options = loaderUtils.getOptions(this) // 输出loader处理后的文件内容 this.callback(null, content + `;console.log('${options.authorName}');`) }
4,js压缩,合并,打包原理是什么以及为什么需要压缩,合并,打包
- 压缩原理:通过去掉注释代码、换行符、空格,缩短变量名长度(规范的变量名函数名对于代码阅读有很大帮助,而对于机器而言没有太大意义)来减少代码体积。常用插件uglifyjs-webpack-plugin
- 合并原理:将多个模块代码根据依赖关系合并成一个模块,减少http资源请求数量,提高资源请求速度。常用工具combo
- 打包原理:模块化打包一般至少有一个入口文件,从入口文件开始根据代码中出现的import/require等语法判断模块间依赖关系,递归解析模块内容及模块依赖资源,其中可能涉及到模块中代码的编译转换优化等,然后整合出所有用到模块的依赖关系树,最终根据依赖关系树将结果打包到目标文件中。常用工具webpack。
- 为什么要压缩合并打包:这些操作可以减小资源体积,降低http资源请求数量,从而加快http资源请求速度。
5,webpack中的compiler与compilation对象以及他们之间区别。
-
compiler:compiler只在webpack启动时构建一次,由webpack所有配置项构建生成。它代表webpack从启动到关闭的整个生命周期。
-
compilation:compilation代表的只是一次新的编译,只要文件有改动,compilation就会被重新创建,从而生成一组新的编译资源。一个
Compilation对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息,简单来讲就是把本次打包编译的内容存到内存里。Compilation对象也提供了插件需要自定义功能的回调,以供插件做自定义处理时选择使用拓展。 -
compiler,compilation区别:compiler代表webpack从启动到关闭的整个生命周期,而compilation代表的只是一次新的编译,只要文件有改动,compilation就会被重新创建
6,什么是hash值,webpack中hash值的作用,介绍webpack中的几种hash值以及如何避免存在随机值
- hash值:对文件内容进行加密运算得到的一组二进制值,不同的文件hash值是不一样的,hash值是唯一的,主要用于文件校验与签名,比如传输一个文件与该文件hash值,接收者接收该文件计算接收到的文件hash值与传输过来的hash值比对是否一样即可知道该文件是否是原文件(即传输过程中数据有没有被修改)。
- webpack中hash值的作用:合理的使用hash便于客户端对于服务端部署的webpack打包资源的缓存处理,文件内容变化则文件名中的hash值变化,文件名变化导致资源请求路径变化,使得浏览器及时弃用缓存使用最新资源。
- webpack中有三种hash值(占位符中常使用的
[name].[hash].js,[name].[chunkhash].js,[name].[conetnthash].js),分别是hash,chunkhash,contenthash。-
hash:hash即根据compilation对象生成的hash值,项目中的任何一个文件改变,项目的compilation对象就会发生变化,跟着hash也会变化。所以配置文件名中使用的hash都相同,一旦其中任何文件发生了变化,所有使用hash都跟着变化(即使当前文件并没有发生变化)。
-
chunkhash:顾名思义根据chunk生成的hash值,和当前chunk有关联的文件变化(或者说js入口文件关联的所有文件),将导致chunkhash的变化,不同chunk的chunkhash不同。output配置中filename可以选择hash与chunkhash配置,一般选择chunkhash比较合适。
-
contenthash:
-
入口js引入css,所以计算打包后的js文件与css文件的chunkhash是相同的,只不过css文件内容被抽离了,但是这样有个缺陷,即js文件改动,css文件不改动,我们期望的只是js文件hash变化,但是css文件hash不变,对于这种情况,chunkhash是做不到的,所以就需要contenthash。
-
顾名思义,根据文件内容生成的hash值,当前文件内容变化,contenthash变化。比如当我们使用extract-text-webpack-plugin单独编译输出css文件时,该插件提供了另外一种hash值:contenthash,当我们应用上contenthash时,css文件与打包输出的js文件hash就不同了。只要css文件不发生变化,不管同一入口文件中相关其他文件如何变化,css文件的contenthash都不会发生变化。
-
-
7,webpack如何把js,css,html文件单独打成一个包
-
javascript:webpack会根据入口js文件根据其内部依赖引用关系最终将js打成一个包
-
抽离css:使用mini-css-extract-plugin/extract-text-webpack-plugin抽离css文件。以mini-css-extract-plugin配置为例:
module: { rules: [ { test: /\.css$/, use: [ // MiniCSSExtractPlugin.loader配合MiniCSSExtractPlugin插件 抽离最终css代码到一个样式文件中,并在html文件头部使用link标签引入 MiniCSSExtractPlugin.loader, 'css-loader', // ... 根据需求添加其他样式loader ] }, ] } plugins: [ new MiniCSSExtractPlugin({ filename: 'main_[contenthash].css', // 规定最终抽离css文件名 }), ] -
html:使用html-webpack-plugin将自定义html文件作为模板单独打包输出,多个html文件可以使用多个html-webpack-plugin插件处理。
plugins:[ new HtmlWebpackPlugin({ template: './src/index.html', // 使用的html模板路径 // filename:'index.html' , // 打包输出的html文件名称,默认index.html // chunks:['main'], // 指定chunks(即对象形式入口配置,对应入口的key)作为引入js资源 }), // // 指定打包多个html // new HtmlWebpackPlugin({ // template: './src/ccc.html', // filename:'ccc.html' // chunks:['others'], // }), ]
8,webpack中css-loader与style-loader区别,file-loader与url-loader区别
-
css-loader:根据样式模块引用关系 将多个模块css资源整合成一个css资源
-
style-loader:将样式资源使用js动态插入到html文件head的style标签中。 所以我们在打包后的html资源头部看不到style标签及内容,而当文件使用浏览器打开发现html头部出现了style标签及内容,这就是浏览器执行js插入css样式的结果。(如果期望将css资源直接在打包后的html文件头部style标签中生成,可以使用html-inline-css-webpack-plugin)
-
file-loader:根据import/require将引用资源转换成url,并将该文件发送到输出目录
-
url-loader:url-loader中整合了file-loader,可以设置limit根据文件大小判断使用file-loader还是将资源转换成base64资源放到bundle.js中。file-loader与url-loader可以处理各种类型文件。
9,webpack如何将样式资源直接输出到html的head的style标签内
使用html-inline-css-webpack-plugin,配合mini-css-webpack-plugin,html-inline-css-webpack-plugin需要放在html-webpack-plugin后。
const HtmlInlineCSSWebpackPlugin = require('html-inline-css-webpack-plugin').default
module: {
rules: [
{
use: [
MiniCSSExtractPlugin.loader,
'css-loader',
]
}
]
},
plugins: [
new MiniCSSExtractPlugin({
filename: "[name][contenthash:8].css",
}),
new HtmlWebpackPlugin({
template: './src/index.html', // 使用的html模板路径
}),
new HtmlInlineCSSWebpackPlugin(),
]
10,一般怎么组织css(webpack)
我觉得他可能想问webapck如何处理css
- 使用style-loader,css-loader,postcss-loader,sass-loader处理,注意postcss-loader需要配置postcss.config.js使用autoprefixer插件,同时在package.json中表明兼容的浏览器列表,sass-loader除了安装sass-loader还需要安装sass包。
module: { rules: [ { test: /\.(css|scss)$/, use: [ 'style-loader', // 将css-loader处理后的css放到html文件头部:安装style-loader 'css-loader', // webpack只认识js/json 通过css-loader认识css文件,并处理css之间引用关系:安装css-loader // postcss配合autoprefixer插件处理浏览器样式兼容 自动加上浏览器厂商, // 需配置postcss.config.js使用autoprefixer插件, // module.exports={ // plugins:[ // require('autoprefixer') // ] // } // 需配置package.json厂商列表 // "browserslist": [ // "> 1%", // "last 2 versions" // ], // 安装postcss-loader,autoprefixer 'postcss-loader', 'sass-loader', // 处理引入sass样式(css-loader不认识sass模块),需要安装sass,sass-loader ] } ] },
11,使用import时,webpack对node_modules依赖做了什么
- 首先使用import引入依赖时,import只是生成一个对模块的引用,而使用该模块的时候才会去模块中获取数据,这也是与commonjs最大的区别。
- 当使用import引入node_modules,webpack会将import转换成类似commonjs方式去引入依赖路径,然后将node_modules中内容处理,最终与其他依赖生成依赖关系表,最后使用立即执行函数传入这些依赖关系表,立即执行函数中实现了commonjs中的require,module,exports属性,立即执行函数执行,所有代码按照依赖关系执行合并从而生成最终打包代码输出。
12,webpack如何配置sass,需要配哪些loader,配置CSS需要哪些loader
- 配置sass:下载sass,sass-loader,css-loader,style-loader,同时webpack配置文件中loader匹配sass文件,loader按照sass-loader,css-loader,style-loader执行处理sass文件
- 配置CSS需要哪些loader:css-loader,style-loader。
13,webpack如何处理图片
-
webpack处理图片可以使用file-loader,浏览器即可http请求图片资源,如果期望减少http请求,可以使用url-loader配合file-loader使用,url-loader限定大小之内将图片处理成base64资源,浏览器直接使用,大于限定则使用file-loader处理,浏览器http请求该图片资源。
-
file-loader:图片资源全部依靠浏览器请求http资源
module: { rules: [ { test: /\.(jpg|png)$/, use: 'file-loader' // 安装file-loader }, ] } -
url-loader:根据图片大小处理图片资源
module: { rules: [ { test: /\.(png|jpg)$/, use: { loader: 'url-loader', // 安装file-loader url-loader options: { name: '[name]_[hash].[ext]', // 对于输出文件重命名为name_hash.后缀 outputPath: 'images/', // 图片输出文件位于输出文件夹下images文件夹中 limit: 3 * 1024, // 小于3kb使用base64,大于使用资源请求方式 } } }, ] }
14,webpack多入口打包(即如何实现分模块打包)
- 将entry配置成对象形式,出口使用占位符号形式(filename:"[name].js"),如果多个html,插件需要使用多个html-webpack-plugin插件对应多个html,html-webpack-plugin中chunks配置指定当前html依赖的js文件。
entry: { // 对main与other入口打包生成两个chunk main: './src/index.js', other:'./src/other.js', // 也可以通过数组的方式,将多个文件打包成一个chunk cache_: ['./src/javascript/cache1.js','./src/javascript/cache2.js',] // 这样最终会生成三个chunk,即main.js, other.js, cache_.js }, output: { path: path.resolve(__dirname, 'dist'), // 指定打包文件输出目录,默认根目录下创建dist文件夹作为输出目录 filename: '[name].js', // 使用占位符形式输出多个入口文件打包后的chunk }, plugins:[ new HtmlWebpackPlugin({ template:'./src/index1.html', // 指定html模板文件生成输出html chunks:['main'], // 指定当前html文件依赖的js文件(chunk) }), new HtmlWebpackPlugin({ template:'./src/index2.html', // html-webpack-plugin中chunks配置当前html文件使用哪个入口打包的chunk,这里是数组形式,用到那个chunk放哪个,可以放多个,如果不设置该属性,默认所有chunk都使用。 chunks:['other'] }), ]
15,webpack抽取公共文件如何配置
对于 多个入口(注意是多个入口) 重复引用的依赖(可能是自定义公共代码,也有可能是第三方库),我们没必要把公共文件打进每个入口文件的chunk中,否则会造成打包代码冗余(即在每个入口文件打成的chunk中都存在公共文件代码)我们可以使用webpack4.x中内置的splitChunksPlugin插件来提取公共文件成一个单独chunk,在html中插入对应的入口文件chunk与公共文件chunk,。注意再webpack4之前使用的CommonsChunkPlugin,本篇只介绍splitChunksPlugin配置。如下:注意minChunks配置含义
optimization: {
splitChunks: {
// 除了 test、 priority 和 reuseExistingChunk 属性,其他属性都可以在该级(与cacheGroups平级)定义,cacheGroup会继承这些属性(即抽离cacheGroup中公共配置在这一级),这里为了方便看全写在cacheGroups中。
// cacheGroups(缓存组):这里自定义分割抽离代码规则
cacheGroups: {
// 这里是vendor规则(规则名字可以自定义),该规则最终目的是抽离第三方模块文件
vendor: {
// name: 用于自定义抽离出去的代码块(chunk)名称,这里抽离出去代码块[name]为vendor
// name详解:终抽离的文件也是一个chunk,所以其路径位置信息与output中filename配置相同。
// 我们这里output配置为 output: { path: path.resolve(__dirname, 'dist'),filename: './js/[name]_[chunkhash:8].js'},
// 所以vendor代码块最终路径为 根路径/js/vendor_[chunkhash:8].js
name: 'vendor',
// minSize:用于判定多大文件才需要抽离,过小文件可以不抽离,这里的界限是0kb,即不管多大都抽离。
// minSize详解:类似url-loader中limit,这里意思minSize之内体积的模块可以不用抽离,大于minSize体积代码抽离,因为有些公共文件体积很小就没有抽离的必要。
minSize: 0 * 1024,
// maxSize:用于判定多大文件才需要抽离,与minSize相反,大于该值才抽离,minSize判断优先级>maxSize。
maxSize: 0,
// minChunks: 根据依赖模块引用次数判定是否抽离,注意这里是根据入口文件引用公共模块次数来判断,比如当前打包多个入口文件,多个入口文件对公共模块A引用次数为m(一个入口文件内引用多次公共模块也只算做该入口文件只引用该公共模块一次),minChunks设置为n,只有当m>=n的时候,该公共模块A才会被单独打包
// minSize详解:minChunks: 2即当前代码块被重复引用>=2次即抽离,该值最小为1
minChunks: 1,
// priority: 当前匹配规则优先级大小,优先级大的先匹配,先进行抽离,匹配完剩下未匹配文件交给优先级小的匹配抽离
// priority详解:此处vendor规则优先级为10,后面common优先级为1,所以先匹配vendor规则抽离公共代码,后匹配common规则
priority: 10,
// test:匹配当前test值的文件才可以进行抽离,不设置则任何文件均可能被抽离
// test详解:类似于loader中test,这里的test: /[\\/]node_modules[\\/]/ 即匹配模式为文件来自node_modules文件夹,也就是第三方模块才可以抽离,当然你也可以设置正则匹配任意你想抽离的文件类型
test: /[\\/]node_modules[\\/]/,
// chunks:该属性有三个值:async,all,initial,一般设置all,默认值为async
// async意味着只有动态导入(异步引入)模块会被优化
// initial意味着只有静态导入模块会被优化
// all意味着所有模块都将被splitChunksPlugin优化
chunks: 'all',
// reuseExistingChunk:true表示允许重用已有的代码块,不必再次发现该块引用时再次创建新快
reuseExistingChunk: true
},
// 这里是common规则,该规则最终目的是抽离自定义公共文件
common: {
name: 'common',
chunks: 'all',
minSize: 0,
minChunks: 2,
priority: 1,
reuseExistingChunk: true
}
}
}
},
- chunks:该属性有三个值:async,all,initial,一般设置all,默认值为async
- async意味着只有动态导入(异步引入)模块会被优化
- initial意味着只有静态导入模块会被优化
- all意味着所有模块都将被splitChunksPlugin优化
16,什么是tapable,与webpack关系,tapable几种钩子介绍。
- tapable:tapable是一个用于处理各种事件流的库,和EventEmmiter很像,但tapable更专注于自定义事件的处理与触发(tapable的多种钩子)。
- 与webpack关系:tapable是webpack的核心依赖库,webpack通过tapable提供的钩子对插件进行处理。将插件监听保存到hook队列中,在webpack运行阶段触发对应队列中的插件监听函数,同时webpack将自身api暴露给插件监听函数,实现插件对wbepack功能的扩展。
- tapable钩子:分为同步钩子与异步钩子
- 同步钩子:SyncHook,SyncBailHook, SyncWaterfallHook, SyncLoopHook。后面三种钩子相当于SyncHook的扩展。
- SyncHook:使用.tap添加监听函数至队列,.call执行监听队列中函数,队列中函数按照添加顺序依次执行,监听函数参数即call传入参数。
const { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook } = require('tapable') // 1,构造钩子对象,类似于发布订阅中的event对象(const event = new Event()) const hook = new SyncHook(['name']); // 2,添加监听函数至当前钩子监听队列中 类似于发布订阅中向event队列中添加订阅函数(event.add('xxx',fn)) hook.tap('hello', v => { console.log(`hello ${v}`); }); hook.tap('hello again', v => { console.log(`hello ${v}, again`); }); // 3,使用call执行hook队列中的函数 类似于发布订阅中触发当前队列函数执行(event.run('xxx',...args)) hook.call('Tsuki'); // 4,注意 const hook = new SyncHook(['name'])与hook.call('Tsuki')之间参数得对应 // 即 const hook = new SyncHook(['name1','name2])对应hook.call('Tsuki1','Tsukie2') // node运行该文件即可 - SyncBailHook:区别与SyncHook,队列中任何函数return非undefined,则队列后面的函数不再执行
- SyncWaterfallHook:区别与SyncHook,队列中某个函数入参即上个函数的返回值,因此该钩子开始时至少接收一个参数。
- SyncLoopHook:区别于SyncHook,当队列中某个函数return非undefined时,会重复执行该函数(如果有输出打印,你会看到控制到疯狂输出)。
- SyncHook:使用.tap添加监听函数至队列,.call执行监听队列中函数,队列中函数按照添加顺序依次执行,监听函数参数即call传入参数。
- 异步钩子:AsyncParallelHook, AsyncSeriesHook, AsyncParallelBailHook, AsyncSeriesBailHook, AsyncSeriesWaterfallHook。后面三种相当于前两种钩子的扩展。
- AsyncParallelHook:Parallel(中译:平行的),区别与SyncHook,AsyncParallelHook队列保存的是异步函数,AsyncParallelHook使用.tapAsync/.tapPromise向任务队列中添加异步函数,.callAsync/.promise执行,队列中异步函数并行执行,当队列中所有异步函数执行完毕,最后执行.callAsync/.promise中回调函数。
const { AsyncParallelHook, AsyncSeriesHook, AsyncParallelBailHook, AsyncSeriesBailHook, AsyncSeriesWaterfallHook } = require('tapable'); // 1,构建钩子对象 const hook = new AsyncParallelHook(['name']); // 2,tapAsync/tapPromise添加异步监听函数 hook.tapAsync('hello', (name, cb) => { setTimeout(() => { console.log(`hello ${name}`); cb(); }, 1000); }); hook.tapPromise('hello again', (name) => { return new Promise((resolve, reject) => { setTimeout(() => { console.log(`hello ${name}, again`); resolve(); }, 1000); }); }); // 3,callAsync/promise执行任务队列,任务队列任务执行完毕,执行回调 hook.callAsync('ahonn', () => { console.log('done'); }); // 或者通过 hook.promise() 调用 // hook.promise('ahonn').then(() => { // console.log('done'); // console.timeEnd('cost'); // }); - AsyncSeriesHook:区别于AsyncParallelHook,AsyncSeriesHook是按照添加顺序,顺序执行。
- AsyncParallelBailHook:区别于AsyncParallelHook,AsyncParallelBailHook如果队列中某个异步函数return非undefined,就会直接执行callAsync/proimise中回调函数,队列中其他函数不会停止执行,只不过callAsync/proimise中回调函数不再执行。
- AsyncSeriesBailHook:区别于AsyncSeriesHook,AsyncSeriesBailHook如果队列中某个异步函数return非undefined,就会直接执行callAsync/proimise中回调函数,同时队列中后面注册的异步函数不再执行。
- AsyncSeriesWaterfallHook:区别于AsyncSeriesHook,队列中上一个异步函数执行的返回值交给下一个异步函数作为参数。
- AsyncParallelHook:Parallel(中译:平行的),区别与SyncHook,AsyncParallelHook队列保存的是异步函数,AsyncParallelHook使用.tapAsync/.tapPromise向任务队列中添加异步函数,.callAsync/.promise执行,队列中异步函数并行执行,当队列中所有异步函数执行完毕,最后执行.callAsync/.promise中回调函数。
- 同步钩子:SyncHook,SyncBailHook, SyncWaterfallHook, SyncLoopHook。后面三种钩子相当于SyncHook的扩展。
17,webpack插件如何实现的(如何手写一个自己的webpack插件)
-
webpack事件流: webpack就像一条生产线,要经过一系列流程才能将源文件转换成输出结果,这条生产线上每一个处理流程职责都是单一的,只有当前流程处理完才能进入下一个流程,而插件就像是插入到生产线中的一个功能,在特定的时机对生产线上的资源进行处理。webapck事件流通过tapable实现。
-
插件的具体工作方式: webpack在运行过程中的不同时机会广播不同的事件,插件监听对应的事件,执行对应回调函数完成webpack资源的处理,从而实现对webpack功能的扩展。
-
webpack插件实现步骤: 插件功能要求:在资源输出之前在控制台上打印所有输出资源路径。
-
1,插件是一个带有apply方法的对象,那么我们可以创建一个插件类来实例该插件对象
-
2,webpack会向插件对象apply方法通过参数注入compiler对象,从而接收到webpack暴露出来的一些api对webpack流程进行操作。
-
3,apply方法内通过compiler.hooks.xxxHook.xxxTap('插件名','插件执行函数')的方式选择合适的compiler时机执行当前插件功能。合适的时机即xxxHook,这个xxxHooks可以理解为react的生命周期,只不过这里对应的是compiler的不同阶段,xxxTap即向xxxHook对应时机添加监听,当该时机到了触发监听函数。
-
所以实现一个webpack插件并不复杂,当然我们需要先了解tapable库,因为compiler.hooks里面这些钩子对象就是tapable库中不同类型钩子的具体实现,webpack会在合适的时机通知钩子内的监听队列函数执行。同时向这些监听函数暴露webpack自身API从而监听函数能够操作webpack实现当前插件功能。
// 该插件目的即在资源输出之前在控制台上打印所有输出资源路径。 // 所以这里的插件执行时机是【资源输出之前】,插件做的事是【控制台打印所有输出资源路径】 class MyPlugin { // tips:这里并不需要向插件传配置 所以没写constructor // 2,实现类原型apply方法 apply(compiler) { // 3,选中emit时机(即资源输出之前)使用tapAsync(因为emit是tapable中asyncSeriesHook具体实现,所以使用tapAsync添加)添加监听 // 你要是想在其他时机做某事,可以查阅compiler.hooks有哪些,以及其对应时机 // 4,监听函数接收到webpack暴露的api(complication) // complication也有complication.hooks供使用,对应complication各个阶段 compiler.hooks.emit.tapAsync('MyPlugin', (complication, callback) => { // 5,监听函数内部根据webpack的api实现控制台打印所有输出资源路径,complication.assets即所有输出资源信息 for (const path in complication.assets) { console.log('path:', path); } // 6,执行异步串行函数回调,这一步不用太在意,有callback执行就可以了,callback是干什么的看之前的tapable就知道了 callback() }) } } // 7,导出插件 module.exports = MyPlugin; // 8,在webpack.config.js使用require引入该插件,plugins内使用即可 // const MyPlugin = require('./plugin/MyPlugin.js') // module.exports = { // plugins: [ // new MyPlugin(), // ] // // ...其他webapck配置 // }
-
-
实现自定义事件: 前面实现的插件用到的事件是
compiler.hooks.emit,emit事件对象对应的时机是资源输出之前。现在我们想在资源输出之前使用我们自定义事件,即这个事件触发时机和emit一样的,但是用我们自定义事件实现。const { SyncHook } = require("tapable"); class MyHook { apply(compiler) { // 1,compiler(compiler.hooks)上挂上我们自定义的事件对象(myHook) compiler.hooks.myHook = new SyncHook(['info']) // 2,资源输出之前,触发我们自定义事件,myHook上订阅函数将全部执行 compiler.hooks.emit.tabAsync('myHook', (complication, callback) => { compiler.hooks.myHook.call('自定义事件myHook触发') callback() }) } } -
loader与plugin区别: loader面向模块,plugin面向webpack执行过程,loader功能是对模块文件的转换,plugin是对webpack功能的一种拓展。
18,关于tree shaking
tree shaking: 移除JS上下文中未使用的代码,在webpack中它具体指的是只有导入和具体使用的模块内容才打包到文件中,从而尽可能消除打包文件中未使用的代码。
tree shaking实现依赖于ESM: 目前在ESM下才可以有效的工作,因为ESM语法的静态结构特性,我们在webpack对代码编译的时候就可以根据顶层导入,判定哪些资源是需要的,哪些资源是不需要的。而Commonjs的动态特性不能实现tree shaking,比如我们在语句中通过某些判断结果去确定是否导入Commonjs模块。显然这样我们不能在代码执行之前确定不需要哪些模块。
babel-loader与tree shaking一些冲突的地方: babel默认会将ESM编译成Commonjs模块,但这就会导致tree shaking失效,显然这不是我们期望的,所以我们可以设置babel-loader中module为false,告诉babel不要编译我们的模块代码。
// webpack.config.js
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: '/node_modules/',
use: {
loader: 'babel-loader',
options: {
// presets:处理JS文件规则,将ESnext 处理成 ES5
// { modules: false } 即告诉babel不要把我们的ESM编译成Commonjs
presets: [['@babel/preset-env', { modules: false }]],
// plugins:处理JS文件的插件,该插件用于处理JSX,如果不需要处理JSX,可不写该行代码
plugins: ['@babel/plugin-transform-react-jsx']
}
}
},
]
}
如何开启tree shaking:
-
1,使用ESM模块语法(同时确保编译器没有把我们的ESM转换成CommonJS)
-
2,使用生产模式(
mode : production)
关于sideEffects: 我们可以在package.json文件中设置该属性,它属于tree shaking的一种优化方式,sideEffects作用于模块层面,该属性主要目的即告诉编译器,哪些模块有副作用,哪些模块没有副作用。
- side effect(副作用)的定义是: 在导入时会执行特殊行为的代码,而不是仅仅暴露一个export或多个export。比如polyfill,它将影响全局作用域,但是通常它并不会提供export。
sideEffects三种配置详细说明:
-
sideEffects:false即告知编译器所有依赖文件都不存在副作用,直接影响:所有依赖模块如果没有明确的导出使用,将从最终打包文件中被剔除。如果依赖的模块有明确导入使用,那么依赖导入部分则不会被剔除。** -
sideEffects:true即告知编译器所有依赖文件都存在副作用 直接影响即:所有依赖模块不管有没有明确的导出使用,都将进行该模块的副作用分析评估。-
对于存在明确导出使用的模块部分,会被打包
-
对于不存在明确导出使用,会进行副作用分析,存在副作用的模块(css文件导入,polyfill导入)会被打包,不存在副作用的模块将被剔除
-
-
sideEffects: ["*.css","@babel/polyfill"]即告知编译器sideEffects指定依赖模块都具有副作用,而对于未指定模块则不存在副作用。
以webpack官网示例继续说明sideEffects行为:
这是一个Shopify Polaris的例子,即使用Shopify Polaris中的Button组件。
import { Button } from "@shopify/polaris";(即从./esnext/index.js导出Button组件。)
在这个例子我们将分析./esnext/index.js及其相关联依赖模块的打包情况。
-
文件目录:
-
具体相关文件内容:
// filepath: ./esnext/index.js import './configure'; export * from './types'; export * from './components'// filepath: ./esnext/components/index.js // ... export { default as Breadcrumbs } from './Breadcrumbs'; export { default as Button, buttonFrom, buttonsFrom } from './Button'; export { default as ButtonGroup } from './ButtonGroup'; // ...// filepath: package.json // ... "sideEffects":[ "**/*.css", "**/*.scss", "./esnext/index.js", "./esnext/configure.js" ], // ...以下是每个资源匹配到的情况:
-
./esnext/index.js:没有明确的导出使用,但被标记为有副作用(in sideEffects),导入该模块,并对其继续进行依赖模块分析。 -
./esnext/configure.js:没有导出被使用,但被标记有副作用,导入该模块,并对其继续进行行副作用评估与依赖分析。 -
./esnext/types/index.js: 没有导出被使用,没有被标记为有副作用,不导入该模块,且不对其进行副作用评估与依赖分析 -
./esnext/components/index.js: 没有导出被使用,没有被标记为有副作用,但重新导出的导出内容Button被使用了,不导入该模块,不对其进行副作用评估,但继续进行依赖分析 -
./esnext/components/Breadcrumbs.js: 没有导出被使用,没有被标记为有副作用,不导入该模块,且不对其进行副作用评估与依赖分析。 -
./esnext/components/Breadcrumbs.css: 在Breadcrumbs.js中被导入,但没有明确的导出(export)与使用(in Breadcrumbs.js),但被标记为有副作用(in sideEffects),不导入该模块,且不对其进行副作用评估与依赖分析,因为不需要对Breadcrumbs.js进行副作用评估与依赖分析。Breadcrumbs.css属于Breadcrumbs.js的依赖模块。// filepath: ./esnext/components/Breadcrumbs.js // ... import './Breadcrumbs.css'; // ... -
./esnext/components/Button.js: 直接的导出被使用(即最终从./esnext/index.js中暴露除去的Button组件),没有被标记为有副作用,导入该模块,并对其继续进行行副作用评估与依赖分析。 -
./esnext/components/Button.css: 在Button.js中被导入,但没有明确的导出(export)与使用(in Button.js),但被标记为有副作用(in sideEffects),导入该模块,并对其继续进行行副作用评估与依赖分析。因为需要对Button.js进行副作用评估与依赖分析。Button.css属于Button.js的依赖模块。且Button.css存在副作用,因此需要导入与继续评估分析。// filepath: ./esnext/components/Button.js // ... import './Button.css'; // ...
-
19,import {Button} from 'antd',打包的时候只打包button,分模块加载,是如何做到的
- antd官网有说明antd支持es6模块的tree-shaking,所以直接使用import {Button} from 'antd'是可以实现按需引入的。关于这个问题我觉得可能是想问当使用webpack1.x或者开发环境等导致tree-shaking失效,我们怎么实现Button的按需引入,而不是把antd模块全都加载进来。解决方法很好办,即使用babel-plugin-import插件,这个插件可以实现对这些库中内容的按需加载。
{ plugins:[ [ "import",{ "libraryName":"antd", // 按需引入库名 "libraryDirectory":"es",// 按需引入的文件所在目录,不设置默认为'lib'文件夹 "style": true // 导入相关样式文件(css/less/sass) } ] ] } // 这相当于 把import { Button } from 'antd' 转换成去antd包内找到button组件以及button组件样式单独引入,即: // import Button from 'antd/es/button' // import 'antd/es/button/style/css'
20,一个活动项目包含多个活动,webpack如何实现单独打包某个活动
- 如果是期望提取某个模块成单独的chunk,我们可以是使用splitChunksPlugin(webpack4之前是 CommonsChunkPlugin)进行提取,splitChunksPlugin在前面《webpack抽取公共文件如何配置》有说明使用方法
- 如果是期望对一个库内的对个内容只打包使用到的内容(即目前不能tree-shaking的时候),可以是使用babel-plugin-import,前面《import {Button} from 'antd',打包的时候只打包button,分模块加载,是如何做到的》有说明。
21,webpack如何做到异步加载,为什么要异步加载模块,以及两种异步加载模块方式区别
- webpack如何做到异步加载:webpack4之前主要是用require.ensure进行异步加载,webpack4之后主要使用import().then进行异步加载模块。
- 为什么要异步加载模块:有些包体积很大,而且在页面初次加载我们并不需要使用它,那么为了提升首屏速度可以选择对该包进行异步加载的方式,即用到的时候再加载该资源。
- require.ensure进行异步加载:没用过,webpack4之前的模块异步加载方案,原理时webpack静态加载require.ensure,将异步模块分离成单独chunk,当用到该异步模块时,该模块会被webpack通过jsonp加载到浏览器。(即再没用到该模块之前,浏览器并不加载该模块)
- import函数(import().then())进行异步加载:webpack4之后使用import函数进行异步加载,该函数返回Promise对象,我们可以在then中接收到加载的模块数据进行处理。具体流程为:
- 1,使用import函数异步加载模块的时候会将异步模块单独打包成一个chunk文件
- 2, 当需要加载异步模块的时候,会创建搞一个script标签,src为异步模块的url,并将该标签添加到html的head中,进行网络请求
- 3,异步模块资源请求成功后,会将异步模块添加到全局的__webpack_require__变量中,而import异步加载模块编译后的代码会去__webpack_require__变量中找到对应模块
- 4,最后执行该异步加载模块有关代码并删除之前创建的script标签(所以你会注意到当进行模块异步加载的时候,控制台中head标签会一闪一下,然后什么都不变。)
- tips
- import导入同步模块的时候,import必须放在代码顶层(最前面),而使用import函数异步加载模块的时候可以随便放在代码中的某个位置,即import函数对于异步加载模块是动态加载的
// 当前为入口文件 app.onclick = function () { // 点击该标签时异步加载asyncjs模块 import('./javascript/asyncjs').then(e => { console.log('asyncjs_content,', e); }) } - 对于打包后的的异步加载模块chunk名称我们可以再output中指定chunkFilename规定其名称,如果不设置该项,则默认名称规则匹配filename属性。
output: { path: path.resolve(__dirname, 'dist'), filename: '[name]_[chunkhash:8].js', // 指定输出入口文件chunk名称 chunkFilename: '[name]_[chunkhash:4].js', // 指定单独输出chunk名称 },
- import导入同步模块的时候,import必须放在代码顶层(最前面),而使用import函数异步加载模块的时候可以随便放在代码中的某个位置,即import函数对于异步加载模块是动态加载的
22,webpack如何利用localstorage离线缓存资源
我觉得这个问题很奇怪,使用contenthash就可以配合浏览器缓存就可以实现资源持久化存储,为啥还要使用localstorage呢
- 实现过程:
- 我们需要在浏览器的loacalstorage中缓存我们的另一个入口文件cache的chunk,这里我就用文字写出大概操作步骤
- 1,在webpack配置中生成cache的chunk,但是cache的chunk先不引入到html中,cache的chunk就叫它cache_chunk吧
- 2,写一个自定义插件在资源输出之前获取到cache_chunk的url,并将cache_chunk的url注入到当前入口文件的变量中保存起来,这样浏览器就能在入口文件的chunk中获取到cache_chunk的url
- 3,前两步骤主要在webpack中实现
- 4,下面的的步骤就是 入口文件中实现的缓存cache_chunk到浏览器中的具体js操作步骤(js代码太多了就不写了直接文字表述,我也没具体实现过,这是参考网上文章整理出来的思路)
- 5,我们首先判断浏览器缓存中是否有cache_chunk的缓存,有的话比对缓存中的cache_chunk的url与webpack注入的url是否一样,因为一旦服务器cache资源改变,其hash值也会变,url跟着变,所以我们需要通过二者的url是否一样判断cache资源有没有被改动过
- 6,如果浏览器有cache_chunk的缓存,且缓存中cache_chunk的url与当前新的入口文件中保存的cache_chunk的url相同,那么说明cache资源没有变化,直接读取缓存中的cache资源添加进html中
- 7,如果浏览器没有cache_chunk的缓存或者有cache_chunk的缓存,但是其url与新的入口文件中保存的cache_chunk的url不同,那么直接生成script标签,script标签的src为新的入口文件中保存的cache_chunk的url,defer设置成true,插入到html中,实现对新的cache_chunk资源的网络请求,请求到资源后,将cache_chunk资源缓存到localstorage中,或者覆盖localstorage中原有的cache_chunk资源
- webpack一些主要配置
entry: { // index:入口文件 index: './src/index.js', // cache_:需要使用localstorage进行缓存的资源 cache_: './src/javascript/cache1.js' }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name]_[chunkhash:8].js', // 指定输出入口文件chunk名称,chunkhash判断资源是否有变化 }, plugins: [ new HtmlWebpackPlugin({ template: './src/index.html', // chunks:chunks只引入入口文件资源,缓存资源暂不引入,缓存资源的处理会在入口文件中实现 chunks: ['index'] }), // 自己实现一个插件GetCacheUrlPlugin,功能是获取缓存资源chunk的url注入到入口文件js中保存起来,以便入口文件chunk执行的时候能通过网络请求加载到缓存资源的chunk // new GetCacheUrlPlugin() ]
23, 如何实现webpack持久化存储
- webpack持久化存储:webpack持久化存储原理就是保证在模块内容不发生变化的同时,每次打包出来的chunk也不会发生变化,这样,浏览器缓存策略会缓存当前chunk,只要chunk名不发生变化且服务器设置的缓存时间够长,那么浏览器就可以一直从缓存中读取chunk,从而实现webpack输出内容在浏览器上的持久化存储,而这里我们需要做的就是保证对于相同内容的文件输出的chunk也是相同的。具体实现如下
- 1,服务器端得设置HTTP缓存头信息开启浏览器缓存策略
- 2,抽离不经常变化的文件到单独的chunk,并使用contenthash作为文件名(或文件名的一部分),这里不变的文件比如css,稳定的第三方库,异步加载代码等。
- 2.1:抽离css:使用mini-css-extract-plugin抽离css代码,css输出chunk的文件名需要使用contenthash,具体配置如下:。
module:{ rules:[ { test:/\.css$/, use:[ MiniCSSExtractPlugin.loader, // style-loader是将样式插入head,该loader抽离css 'css-loader' ] } ] }, plugins:[ new MiniCSSExtractPlugin({ // 使用contenthash作为文件名一部分 filename:'[name]_[contenthash].css' }) ] - 为什么css文件不使用chunkhash?:因为,css一般在入口js文件引入,视为js文件一部分,如果与入口文件共用一个chunkhash,那么单独改变css或者入口文件js内容都会导致chunkhash变化,所以为了避免这种情况,我们使用contenthash根据当前文件内容生成hash,保证css的chunk变化只根据css内容变化,不会收到其他文件影响。
- 2.2:抽离稳定的第三方库/异步加载代码成为单独chunk:使用webpack的splitchunkplugin抽离第三方库代码,且输出chunk的文件名需要使用contenthash。使用contenthash即保证第三方库内容不变,其输出的chunk的hash不变,具体配置如下:。
output:{ path:path.resolve(__dirname,'dist'), // 第三方库与入口文件使用filename filename:'[name]-[contenthash].js', // 异步加载代码使用chunkFilename chunkFilename:'[name]-[contenthash].js', }, optimization:{ splitChunks:{ cacheGroups:{ // 抽离第三方库到单独chunk中,output中filename配置了contenthash。 vendor:{ name:'vendor', minSize:0, minChunks:0, test:/[\\/]node_modules[\\/]/, chunks:'all' } } } }
- 2.1:抽离css:使用mini-css-extract-plugin抽离css代码,css输出chunk的文件名需要使用contenthash,具体配置如下:。
- 3,固定moduleID:chunk内容还包括了模块的id,模块的id如果使用的是数字,该数字会随着模块引用顺序变化而变化,这样就会导致即使模块内容没变,但是引用顺序发生变化,那么最后输出的chunk中的模块id也发生变化,chunk发生变化,contenthash就跟着变化,所以我们要保证moduleID是固定的,我们一般将moduleID设置成路径或者路径生成的3位hash,具体配置如下:
optimization:{ moduleIds: 'named' // 或者deterministic,即三位hash,不要使用natural,natural会使用数字作为moduleID } - 4,固定chunkID:同moduleID一样,chunk顺序的变化(入口配置中多个入口位置变化),也会导致contentHash发生变化,所以需要固定chunkID。不能让chunk使用数字作为ID。
optimization:{ chunkIds: 'named' // 或者deterministic,即三位hash,不要使用natural,natural会使用数字作为chunkID }
24,关于webpack热更新原理
简单的webpack5热更新配置:
module.exports = {
// ... other webpack options code
target: 'web', // webpack5开启热更新还需要配置这个选项
devServer: {
// 启动命令:" webpack serve",需要的包:webpack,webpack-cli,webpack-dev-server
port: 8080, // 服务端端口8080
contentBase: './dist', // 服务器静态资源文件夹
hot: true // 开启热更新
},
// ... other webpack options code
// ......
}
1,启动dev-server,向entry文件中插入websocket相关代码,使用websocket与客户端建立连接,方便服务端主动通知客户端
2,添加compiler.hooks.done的订阅,既监听每一次编译(compilation)完成时机,拿到编译后的hash,通过websocket交给客户端
3,客户端通过websocket获取新的hash,与旧hash对比,发生变化则向服务端请求相关的xxx.hot-update.json文件,该文件主要记录了哪些chunk发生了变化,如下,.hot-update.json中c(既chunks)保存了发生变动的chunk标识,既main02对应的chunk发生了变化
4,再根据chunkID向服务端请求对应发生变化的chunk更新代码,如下,拿到main02对应的xxx.hot-update.js代码
5,执行xxx.hot-update.js中的代码(self[webpackHotUpdate_xxx](chunk标识,updateCode,webpack_require)),来执行最新的chunk更新代码完成更新,再通过hot.accet()完成页面重新渲染,最后完成热更新。
// 去除注释后的xxx.hot_update.js文件
self["webpackHotUpdate_6_webapck_test"](
"main02",
{
"./node_modules/css-loader/dist/cjs.js!./src/CSS/index.css"
:
((module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../node_modules/css-loader/dist/runtime/api.js */ \"./node_modules/css-loader/dist/runtime/api.js\");\n/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_0__);\n// Imports\n\nvar ___CSS_LOADER_EXPORT___ = _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_0___default()(function(i){return i[1]});\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"html, body {\\n width: 100%;\\n height: 100%;\\n background-color: violet;\\n}\", \"\"]);\n// Exports\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___);\n\n\n//# sourceURL=webpack://6.webapck_test/./src/CSS/index.css?./node_modules/css-loader/dist/cjs.js");
})
},
function (__webpack_require__) {
(() => {
__webpack_require__.h = () => ("48d5a0b5590c715097d4")
})();
}
);
最后借用一张图回顾,该图出处见底部感谢参考《webpack5 热更新从配置到原理》
25,webapck中devServer如何使用
- 可以将当前打包资源部署在webpack服务器中,当前webpack版本默认HMR(热更新),可以通过hot选项设置HMR开启,同时可以实现静态资源中的请求转发。
mode: 'development', // 当前开发环境 devServer: { // 启动命令:" webpack serve",需要的包:webpack,webpack-cli,webpack-dev-server port: 8080, // 服务端端口8080 contentBase: './dist', // 服务器静态资源文件夹 progress: true, // 打包显示进度条 open: true, // 启动服务器后自动打开浏览器对应当前资源页面 compress: true, // 开启gzip压缩 proxy: { // 资源请求转发 // 将"/api/xxx"的请求添加target路径进行请求转发 // pathRewrite 替换匹配路径部分 "/api/xxx": { target: 'http://127.0.0.1/', pathRewrite: { '^api': '' } }, // changeOrigin true修改转发请求头host,默认前端请求域名,true则为资源请求转发target changeOrigin: true } },
26,webpack优化(打包体积优化与打包速度优化)
-
体积优化
-
1,mode设置成production:生产模式默认会使用tree-shaking删除无用代码,开发模式不会开启tree-shaking
-
2,开发环境中使用babel-plugin-import插件对组件库按需加载
-
3,使用splitChunksPlugin将不会变化的第三方库还有共用代码抽离进行单独打包:splitChunks配置前面有写。
-
4,对于不是需要立即获取的资源可以采取异步加载的方式(import().then),异步加载资源也会被单独打包成chunk,减小主包体积。
-
5,开启@baebl/polyfill使用按需加载,既babel-loader配置presets添加
{useBuiltsIns:usage}(不指定的话,polyfill会将所有api都实现)module:{ rules:[ { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: [ ['@babel/preset-env'], // 主要翻译工作由@babel/preset-env实现 { useBuiltIns: 'usage' } // 开启@babel/polyfill按需加载 ] } } }, ] } -
6,使用mini-css-webpack-plugin提取css成单独文件,减小主包体积
-
7,合理使用url-loader,过大的文件使用file-loader更为合理。对于大图还可以使用image-webapck-loader进行压缩。
-
-
打包速度优化
- 1,限定loader作用范围,比如我们只希望对src文件夹下面内容进行babel转义,node_modules不需要。
module:{ rules:[ { test:/\.js$/, use:{ loader:'babel-loader', options:{ presets:[ ['@babel/preset-env'], ] } } // babel转义src文件夹内文件,因为文件夹形式需要使用绝对路径,所以path.resolve(__dirname,'src') include: path.resolve(__dirname,'src'), // 当然如果我们只是不想处理node_modules中文件,也可以使用exclude exclude: /node_modules/ } ] } - 2,使用noParse通知webpack哪些模块不需要解析其依赖关系:因为依赖关系解析也是耗时操作,而有些第三方依赖内部肯定不会存在依赖,比如jquery,所以我们可以手动指定这些模块不需要进行依赖关系的解析。(这些模块中没有import,require等模块化的语句,所以不需要解析依赖关系,只需要对其进行打包处理)
module:{ noParse:/jquery|bootstrap|lodash/, // 匹配当前正则的文的内部不需要进行依赖关系的解析 rules:[] // 其他loader } - 3,使用resolve.modules指明模块查找时的路径,避免不必要的查找
module:{ resolve:{ // 指明模块查找从下面两个路径开始,src优先于node_modules,这个配置含义是因为正常情况下我们的代码存在src目录下,node_modules中是我们用到的第三方依赖,其他地方基本么有项目用到的代码。 modules:[path.resolve(__dirname,'src'),/node_modules/] } rules:[] // 其他loader } - 4,代码书写尽量添加文件后缀名,减少文件匹配resolve.extentions后缀时间。
- 5,使用高版本webpack,一般版本越高,工具优化越好。
- 6,使用thread-loader对loader单开线程处理:因为node中webpack是单线程的,使用thread-loader可以对loader处理模块单开线程进行处理,提高打包速度。thread-loader只需要放在耗时的loader前面即可,但不能放在style-loader之前,一般放在style-loader之后,因为thread-loader之后的loader没办法存取文件以及获取wbepack的配置
module:{ rules:[ { test:/\.img$/, use:[ 'thread-loader', 'url-loader' ] } ] }- 7,使用cache-loader对loader处理结果缓存到磁盘,二次打包时候读取缓存,提高打包速度。注意:缓存的读写也需要时间,所以只在必要的时候使用。 harder-source-webapack-plugin也可以提升二次打包时间,不过没用过。
module:{ rules:[ { test:/\.ext$/, use:[ 'cache-loader', // cache-loader如果与thread-loader同时出现,先放cache-loader,再放置thread-loader ...otherloaders ] } ] }- 8,当然babel-loader也可以开启缓存处理,将之前转义的结果缓存起来,如果二次打包没有变化,可以直接使用缓存:
module:{ rules:[ { test:/\.js$/, use:{ loader: "babel-loader", options:{ presets:[['@babel/preset-env']], cacheDirectory: true, // 开启babel-loader缓存 } } } ] } - 1,限定loader作用范围,比如我们只希望对src文件夹下面内容进行babel转义,node_modules不需要。
27,你是如何配置开发环境的(webpack配置)
- 开发环境配置:
- 1,使用html-webapck-plugin输出html文件:可以通过配置指定输出html按照什么模板生成,也可以指定输出html引入的chunks,还可以配置多个html-webapck-plugin输出多个html
- 2,使用(style-loader|mini-css-extract-plugin.loader),css-loader,postcss-loader,sass-loader处理样式文件:
- style-loader将最终css插入heml的head中(style标签)
- mini-css-extract-plugin.loader:配合该插件将css样式提取到单独文件
- css-loader将css样式模块处理成一个样式资源
- postcss-loader:配合autoprefixer插件将样式对不同浏览器中做兼容处理
- sass-loader:处理sass样式文件
- 3,使用url-loader处理图片等其他格式文件:可以配置limit控制输出是具体文件还是base64格式数据
- 4,使用babel-loader等loader处理es6+模块:需要安装的模块由babel-loader,@babel/core,@babel/preset-env,@babel/polyfill,@babel/polyfill配置useBuiltIns开启polyfill按需加载。
- 5,配置webpack.config.js中的devServer:建立当前资源服务器,可以实现资源热加载,资源转发,资源压缩等功能
- 6,使用mini-css-extract-plugin提取css资源
- 7,使用webapck自带splitChunksPlugin对不经常改变第三方库以及公共代码提取到单独文件输出
- 8,使用contentHash生成hash,实现资源缓存:相较于hash/chunkHash,这两个可以更精确的生成hash,避免每次改动代码所有hash都变化,尽量控制到只发生改动的模块的hash值发生变化。
- 9,使用devtool使用sourcemap定位出问题的源代码位置,而不是打包之后的代码位置:devtool在开发环境默认设置为source-map,即开启问题代码定位功能,设置fasle,则关闭,一般开发环境设置cheap-module-eval-source-map,生产环境设置cheap-module-source-map。
devtool:'cheap-module-eval-source-map' // 开发环境 // devtool:'cheap-module-source-map' // 生产环境 - 10,cache-loader与babel-loader开启缓存,提高第二次打包速度:babel-loader设置cacheDirectory:true,cache-loader则放在需要缓存的loader前面。
- 11,loader配置中使用exclude/include缩小loader匹配范围
- 12,配置resolve.alias配置模块路径别名,使得模块引入时更简单
resolve:{ alias:{ Common: path.resove(__dirname,'src/common/') // import { debounce } from '../../common/debounce.js'就可以使用下面方式引入当前模块 // import { debounce } from 'Common/debounce.js' } } - 13,使用module中的noParse指明一些没有依赖引入的模块,告诉webpack打包时候不需要对其进行依赖关系解析。
28,devServer原理简述
devServe其实使用express创建服务端,同时将打包资源打进内存中(打进内存而不是磁盘是因为读写更快,所以dist目录下我们看不见打包后的资源),express服务器静态资源指向该内存,这样我们就可以通过浏览器对打包后的资源进行访问了。
29,webpack打包过程中处理import与commonjs区别
runtime代码:即用来处理模块之间连接的代码,由webpack提供,开发环境下不做特殊配置我们可以看到打包代码中有__webpack_require__函数,这就属于runtime代码一部分,再者说我们引入导出模块,这些功能我们并没有实现,而是webpack在实现,而runtime就是具体实现方式。如下图,即抽离出来的runtime代码。
-
commonjs:在处理commonjs模块时,webpack中的runtime代码实现了commonjs的功能,其中commonjs中的require对应的就是runtime中的__webpack_require__函数,实现了commonjs对模块导出的数据的浅拷贝。
-
import:相对于commonjs模块处理,webpack runtime在ES模块中除了实现了__webpack_require__,还实现了
__webpack_require__.r,__webpack_require__.o,__webpack_require__.d函数,ES模块核心是实现对模块中导出数据的引用,所以webpack在处理import引入模块时,使用了闭包的方式实现了对数据的引用,具体就是__webpack_require__.d重写了数据的getter函数,重写后函数是个闭包,保持着对模块导出数据的引用。
感谢参考资源
- 浅谈 MVC、MVP 和 MVVM 架构模式
- 活动作品【前端面试必备】JavaScript模块化全面解析
- Webpack的构建流程主要有哪些环节?如果可以请尽可能详尽的描述Webpack打包的整个过程
- Webpack中hash与chunkhash的区别,以及js与css的hash指纹解耦方案
- 关于 tapable 你需要知道这些
- Tree-shaking及副作用
- 前端性能优化:webpack分离 + LocalStorage缓存
- webpack4 的30个步骤打造优化到极致的 react 开发环境,如约而至
- webapck模块化必读(8千字长文!!) -【webpack系列】
- 关于静态资源内联
- 前端进阶之 JS 抽象语法树
- Webpack揭秘——走向高阶前端的必经之路
- Webpack: What is the difference between "all" and "initial" options in optimization.splitChunks.chunks
- webpack5 热更新从配置到原理