参考链接:blog.csdn.net/weixin_4261…
webpack的打包过程
webpack的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:
- 初始化参数:从配置文件和shell语句中读取参数并合并,得到最终的参数;
- 开始编译: 用上一步得到的参数初始化compiler对象,加载所有配置的插件,执行对象的run方法开始执行编译;
- 确定入口: 根据配置中的entry找到所有的入口文件;
- 编译模块: 从入口文件出发,调用所有配置的loader对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过本步骤的处理;
- 完成模块编译:在经过第4步使用loader翻译完所有的模块后,得到了每个模块翻译后的最终内容和它们的依赖关系;
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的chunk,再把每个chunk转化成一个单独的文件加入到输出列表,这一步是修改输出内容的最后机会。
- 输出完成:在确定好输出内容以后,根据配置确认输出的路径和文件名,把输出内容写入到文件系统;
在以上过程中,webpack会在特定的时间点广播出特定的事件(钩子),插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用webpack提供的API改变webpack的运行结果。
compiler和compilation
- 相同点:
- compiler扩展(extend)自Tapable;
- compilation类扩展(extend)自Tapable;
- 不同点:
- compiler用来注册和调用插件;
- compilation对应用程序的依赖图中所有模块,进行字面上的编译。
热更新HMR原理
- 在启动webpack-dev-server的时候,sockjs在服务器和浏览器端建立一个WebSocket长连接;
- webpack-dev-server监听compiler的done钩子。当编译完成后,webpack-dev-server将新的hash通过WebSocket推送给浏览器;
- 嵌在浏览器端的webpack-dev-client代码接收到type为hash的数据后,将hash值暂存起来;
- webpack-dev-server通过WebSocket发送type为ok的消息给浏览器;
- 嵌在浏览器端的webpack-dev-client接收到ok的消息后,根据hot配置决定是刷新页面还是对页面进行热更新;
- 如果是热更新,webpack-dev-client基于node events的emit和on方法,将步骤3中获取到的hash提交给webpack-client,再将控制权交给webpack-client;
- webpack-client先判断模块是否有更新,即判断最新的hash和上一次缓存的hash值是否一致;
- 不一致的话,代表模块已更新。此时,webpack-client通过AJAX向服务器请求最新文件,如果有更新webpack将返回最新的文件hash列表;
- webpack-client通过JSONP和hash列表请求最新的代码块;
- 新的代码替换旧的模块代码;
- 替换后,我们的业务代码并不知道代码已经发生了变化,所以我们需要在业务代码中调用HMR的Module.hot.accept方法(添加模块更新后的处理函数);我们手动在业务代码中添加容易犯错,也很麻烦,目前已经有很多的loader可以帮忙处理。
webpack拆包依据
官网链接:webpack.docschina.org/plugins/spl…
- 最初,chunks(以及内部导入的模块)是通过内部webpack图谱中的父子关系关联的。
- CommonsChunkPlugin曾被用来避免他们之间的重复依赖,但是不可能再做进一步的优化。
- 从webpack v4开始,移除了CommonsChunkPlugin,取而代之的是optimization.splitChunks。
- 开箱即用的SplitChunksPlugin对于大部分用户来说非常友好。
- webpack将根据以下条件自动拆分chunks:
- 新的chunk可以被共享,或者模块来自于node_modules文件夹;
- 新的chunk体积大于20kb(在进行min+gz之前的体积);
- 当按需加载chunks时,并行请求的最大数量小于或等于30;
- 当加载初始化页面时,并发请求的最大数量小于或等于30。
- 当尝试满足最后两个条件时,最好使用较大的chunks。
webpack如何解决循环依赖?
webpack 处理 image 是用哪个 loader,限制成 image 大小的是...;
- file-loader和url-loader
- file-loader打包之后,每个图片在加载时,都会发送一个http请求,当页面图片过多,会严重拖慢网页加载速度,这种情况下,我们可以选择用url-loader进行打包,通过配置规则,让较小的图片打包成base64的形式存放在打包后的js中,不再需要单独发送http请求加载图片。
- 注:要求被编码的图片要特别小,否则编码字节长度过长,即使压缩后也得不偿失,一般适合在几KB。也有一些特例:如是loading等一些使用太频繁的组件化图片,哪怕20K一般也会转成base64。一是便于UI组件的维护,而是使用时不用每次都发送请求。
webpack 将 css 合并成一个;
- 使用extract-text-webpack-plugin插件
- 官方链接:v4.webpack.docschina.org/plugins/ext…
- 它会将所有的入口chunk中引用的*.css,移动到独立分离的CSS文件中,因此你的样式不再内嵌到JS bundle中,而是会放到一个单独的CSS文件中。如果你的样式文件大小较大,这会做更快提前加载,因为CSS bundle会跟JS bundle并行加载。
webpack 的摇树对 commonjs 和 es6 module 都生效么,原理是;
- 官方链接: v4.webpack.docschina.org/guides/tree…
- 参考链接: juejin.cn/post/695652…
- 只对es6 module生效;
- 它依赖于ES2015模块语法的静态结构特性,例如import和export。这个术语和概念实际上是有ES2015模块打包工具rollup普及起来的。
- webpack做tree-shaking时,要有几个条件:
- 模块系统必须为ESmodule;
- 在package.json文件标识sideEffect字段;
- 把webpack设置为生产环境。
模块系统必须为ESmodule
- 为什么要是es模块系统才行,common.js不行吗?是的,只有es模块系统可以,原因是ESModule是静态的,依赖关系在编译时就确定了。而commonjs是node环境默认的模块系统,是动态的。而webpack只会在编译时做处理,所以只有es模块才行。
- 但是在实际项目中,在进入webpack优化前,我们一般会用一系列loader对源码进行处理。问题是preset为env时,模块转换默认为cjs。此时,我们把babel配置为以下即可:
{
"presets": [
["env": {modules: false}]
]
}
- 这样babel就不会把我们原来的ESmodule转换为其他模块系统。
webpack 中 hash、chunkHash 与 contentHash 区别;
原文链接:blog.csdn.net/bubbling_co…
- hash hash是跟整个项目的构建相关,只有项目里有文件更改,整个项目构建的hash值都会更改,并且全部文件都共用相同的hash值。
- chunkHash chunkHash,根据不同的入口文件进行依赖文件解析,构建对应的chunk,生成对应的哈希值。我们在生产环境里把一些公共库和程序入口文件区分开,单独打包构建,接着我们采用chunkHash的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不受影响。
- contentHash contentHash表示由文件内容产生的hash值,内容不同产生的contentHash值不一样。在项目中,通常做法是把项目中的css都抽离出对应的css文件来加以引用。 在这里我用mini-css-extract-plugin替代了extract-text-webpack-plugin。
const miniCssExtractPlugin=require("mini-css-extract-plugin");
module.exports={
module:{
rules:[
{
test: /\.css$/,
use:[
miniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins:[
new miniExtractPlugin({
filename: 'main.[contenthash:7].css'
})
}
}
打包结果如图:
打包后即使css文件所处的模块里就算其他文件内容改变,只要css文件内容不变,那么就不会重复构建。
附加: 如果对css使用了chunkhash之后,它与依赖它的chunk共用chunkhash,测试后会发现,css与js文件名的chunkhash值是一样的,如果我修改了js文件,js的hash值会变化,css的文件名的hash还是和变化后的js文件的hash值一样,如果我修改了css文件,也会导致重新构建,css的hash值和js的hash值还是一样的,即使js文件没有被修改。这样会导致缓存作用失效,所以css文件最好使用contenthash。
loader
编写一个loader
babel-loader
参考链接: www.jiangruitao.com/babel/babel…
babel的转译过程
babel的转译过程分为三个阶段:parsing、transforming、generating
以Es6转Es5为例,具体过程:
- 编写ES6代码
- babylon进行解析
- 解析得到AST
- 用babel-traverse对AST进行遍历转译
- 得到新的AST树
- 用babel-generator通过AST树生成ES5代码
- babylon是babel中使用的JavaScript解析器。
- babylon支持JSX、Flow、Typescript语法。
babel预设
Babel官方的preset,我们实际可能会使用到的起始就只有4个
- @babel/preset-env
- @babel/preset-flow
- @babel/preset-react
- @babel/preset-typescript 一个普通的vue工程,Babel官方的preset只需要配一个@babel-preset-env就可以了。
babel插件
babel7官方有90多个插件,不过大半已经整合在@babel/preset-env和@babel-react等预设里了,我们再开发的时候直接使用预设就可以了。
目前比较常用的插件只有@babel/plugin-transform-runtime。
@babel/preset-env
在Babel6时代,这个预设名字是babel-preset-env,在babel7之后,改成@babel/preset-env。
@babel/core的作用是把js代码分析成ast
loader的执行顺序
plugin 插件
新建一个基本的webpack工程
- 新建一个文件夹webpack-loader-demo
- cd到上叙目录下,执行npm init,根目录下生成文件package.json
{
"name": "webpack-loader-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
- 安装webpack和webpack-cli
- npm install webpack --save-dev
- npm install webpack-cli --save-dev
- 根目录下生成package-lock.json文件和node_modules文件夹
- 在package.json的script中增加build
"scripts": {
"build": "webpack"
},
- 根目录下新建文件夹src,src下新建文件index.js,
- package.json文件中的配置"main": "index.js",指定入口文件为src下面的index.js
new Promise((resolve, reject) => {
setTimeout(resolve(1),1000)
}).then((value) => {
console.log(value)
})
- 运行npm run build,根目录下生成dist文件夹,文件夹下生成mian.js文件
new Promise(((e,o)=>{setTimeout(e(1),1e3)})).then((e=>{console.log(e)}));
由此可见,默认情况下webpack会对文件进行压缩。
调试webpack
调试方法
- 在根目录的package.json文件中的script中添加"debug": "node --inspect-brk=5858 ./node_modules/webpack/bin/webpack"
- vcode的debug模式下,修改launch.json修改如下
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "启动程序",
"stopOnEntry": true,
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"debug"
],
"port": 5858
}
]
}
- 启动Debug
调试中源码的执行过程
1、判断webpack-cli包是否已安装 2、已安装,判断webpack-cli根目录下的package.json是否存在 3. 存在,则引入webpack-cli根目录下的package.json文件 4. 引入webpack-cli根目录下的bin/cli.js文件 5. 执行webpack-cli根目录下的lib/webpack-cli.js文件下的run()方法。 6. 执行lib/webpack-cli.js文件下的createCompiler()方法
- resolveConfig() 获取到根目录下webpack.config.js文件内容
- 执行webpack根目录下的lib/webpack.js的createCompiler()方法
- getNormalizedWebpackOptions()
代码分离
官方介绍: v4.webpack.docschina.org/guides/code…
使用SplitChunksPlugin分离公共依赖
SplitChunksPlugin 插件可以将公共的依赖模块提取到已有的 entry chunk 中,或者提取到一个新生成的 chunk。
- 项目有两个入口文件
entry: {
index: './src/index.js',
b : './src/pages/b.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: "[name].bundle.js"
},
- 上叙两个入口文件中都调用了Vue src/index.js
import Vue from 'vue'
new Vue()
function index(){}
module.exports = { index }
src/pages/b.js
import Vue from 'vue'
new Vue()
function b(){}
module.exports = { b}
-
打包后,生成的dist/index.bundle.js和dist/b.bundle.js文件有vue的重复代码
-
打包后上叙两个文件的大小如下
index.bundle.js 234 KiB
b.bundle.js 234 KiB
- 在webpack.config.js配置文件的optimization选项中加入splitChunks
// 优化选项
optimization: {
splitChunks : {
chunks(chunk){
return true
}
}
}
-
优化后打包生成三个文件
-
使用SplitChunksPlugin优化后的文件
index.bundle.js 9.46 KiB
b.bundle.js 9.4 KiB
vendors-node_modules_vue_dist_vue_runtime_esm_js.bundle.js 229 KiB
使用动态导入分离依赖
var a = 1
function a(){
return import(/* webpackChunkName:"lodash_000" */ 'lodash')
.then( ({ default : _}) => {
var str = _.join(['Hello', 'webpack'], ' ')
console.log(str)
})
}
module.exports = { a }
生成两个文件
lodash_000.bundle.js 550 KiB
a.bundle.js 14 KiB
构建目标(Target)
由于JavaScript既可以编写服务器代码也可以编写浏览器代码,所以webpack提供了多种部署target。
target的配置
webpack.config.js
module.exports = {
target: 'node',
};
告知webpack为目标(target)指定一个环境。默认值为“browserslist”,如果没有找到browserslist的配置,则默认为“web”
模块解析(Module Resolution)
resolver是一个帮助寻找模块绝对路径的库。
一个模块可以作为另一个模块的依赖模块,然后被后者引用,如下:
import foo from 'path/to/module'
// 或者
const foo = require('path/to/module')
所依赖的模块可以是来自应用程序的代码或第三方库。
resolver帮助webpack从每个require/import语句中,找到需要引入到bundle中的模块代码。
打包模块时,webpack使用enhanced-resolve来解析文件路径。
webpack中的解析规则
使用enhanced-resolve,webpack能解析三种文件路径:
- 绝对路径 由于已经获得文件的绝对路径,因此不需要做进一步解析。
- 相对路径 使用import或require的资源文件所处的目录,被认为是上下文目标。在import/require中给定的相对路径,会拼接此上下文路径,来生成模块的绝对路径。
- 模块路径 在resolve.modules中指定的所有目录中检索模块。你可以通过配置别名的方式来替换初始模块路径。
一旦根据上述规则解析路径后,resolver将会检查路径是指向文件还是文件夹。
如果路径指向文件:
- 如果文件具有扩展名,则直接将文件打包
- 否则,将使用resolve.extensions选项作为文件扩展名来解析,此选项会告诉解析器在解析中能够接受哪些扩展名(例如 .js , .jsx)
如果路径指向一个文件夹:
-
如果文件夹中包含package.json文件,则会根据resolve.mainFields配置中的字段顺序查找,并根据package.json中的符合配置要求的第一个字段来确定文件路径。
-
如果不存在package.json文件或resolve.mainFields没有返回路径,则会根据resolve.mainFiles配置选项中指定的文件名顺序查找,看是否能在import/require的目录下匹配到一个存在的文件名
解析的配置
这些选项能设置模块如何被解析。webpack提供合理的默认值,但是还是可能会修改一些解析的细节。
resolve
webpack.config.js
module.exports = {
//...
resolve: {
}
}
resolve.alias
创建import或require的别名,来确保模块引入变得更简单。
// 配置
module.exports = {
//...
resolve: {
alias: {
'vue': 'vue/dist/vue.esm-bundler.js',
'@': path.resolve(__dirname,'../src'),
'jquery': path.join(commonDir,'assets/js/jquery-2.1.1.js'),
'_': path.join(commonDir,'assets/js/lodash.js')
}
}
}
// 使用
import { createApp } from 'vue'
resolve.extensions
module.exports = {
//...
resolve: {
extensions: ['.js','.vue','.jsx','.css'],
}
}
尝试按顺序解析这些后缀名。如果有多个文件有相同的名字,但后缀不同,webpack会解析列在数组首位的后缀的文件 并跳过其余的后缀。
那个使用户在引入模块时不带扩展。
请注意,以上这样使用resolve.extensions会覆盖默认数组,这就意味着webpack将不再尝试使用默认扩展来解析模块。然而你可以使用'...'访问默认扩展名:
module.exports = {
//...
resolve: {
extensions: ['.ts', '...'],
},
};
resolve.modules
module.exports = {
//...
resolve: {
modules: ['node_modules'],
},
};
告诉webpack解析模块时应该搜素的目录。
resolve.mainFields
当从npm包中导入模块时(例如,import * d D3 from 'd3'),此选项将决定在package.json中使用哪个字段导入模块。 根据webpack配置中指定的target不同,配置值也会有所不同。
- 当target属性设置为web、webworker或者没有指定,mainFields的默认值如下:
//webpack.config.js
module.exports = {
//...
resolve: {
mainFields: ['browser', 'module', 'main'], // 优先级排序为browser > module > main
},
};
- 对于其他任意的target(包括node),默认值为:
//webpack.config.js
module.exports = {
//...
resolve: {
mainFields: ['module', 'main'],
},
};
package.json中的browser / module / main
- main:定义了npm包的入口文件,browser环境和node环境均可使用
- module: 定义了npm包的ESM规范的入口,browser环境和node环境均可使用
- browser: 定义npm包在browser环境下的入口文件
reslove.mainFiles
解析目录时要使用的文件名
// webpack.config.js
module.exports = {
//...
resolve: {
mainFiles: ['index'],
},
};