webpack

227 阅读14分钟

参考链接:blog.csdn.net/weixin_4261…

webpack的打包过程

webpack的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

  1. 初始化参数:从配置文件和shell语句中读取参数并合并,得到最终的参数;
  2. 开始编译: 用上一步得到的参数初始化compiler对象,加载所有配置的插件,执行对象的run方法开始执行编译;
  3. 确定入口: 根据配置中的entry找到所有的入口文件;
  4. 编译模块: 从入口文件出发,调用所有配置的loader对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过本步骤的处理;
  5. 完成模块编译:在经过第4步使用loader翻译完所有的模块后,得到了每个模块翻译后的最终内容和它们的依赖关系;
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的chunk,再把每个chunk转化成一个单独的文件加入到输出列表,这一步是修改输出内容的最后机会。
  7. 输出完成:在确定好输出内容以后,根据配置确认输出的路径和文件名,把输出内容写入到文件系统;

在以上过程中,webpack会在特定的时间点广播出特定的事件(钩子),插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用webpack提供的API改变webpack的运行结果。

compiler和compilation

  • 相同点:
  1. compiler扩展(extend)自Tapable;
  2. compilation类扩展(extend)自Tapable;
  • 不同点:
  1. compiler用来注册和调用插件;
  2. compilation对应用程序的依赖图中所有模块,进行字面上的编译。

热更新HMR原理

  1. 在启动webpack-dev-server的时候,sockjs在服务器和浏览器端建立一个WebSocket长连接;
  2. webpack-dev-server监听compiler的done钩子。当编译完成后,webpack-dev-server将新的hash通过WebSocket推送给浏览器;
  3. 嵌在浏览器端的webpack-dev-client代码接收到type为hash的数据后,将hash值暂存起来;
  4. webpack-dev-server通过WebSocket发送type为ok的消息给浏览器;
  5. 嵌在浏览器端的webpack-dev-client接收到ok的消息后,根据hot配置决定是刷新页面还是对页面进行热更新;
  6. 如果是热更新,webpack-dev-client基于node events的emit和on方法,将步骤3中获取到的hash提交给webpack-client,再将控制权交给webpack-client;
  7. webpack-client先判断模块是否有更新,即判断最新的hash和上一次缓存的hash值是否一致;
  8. 不一致的话,代表模块已更新。此时,webpack-client通过AJAX向服务器请求最新文件,如果有更新webpack将返回最新的文件hash列表;
  9. webpack-client通过JSONP和hash列表请求最新的代码块;
  10. 新的代码替换旧的模块代码;
  11. 替换后,我们的业务代码并不知道代码已经发生了变化,所以我们需要在业务代码中调用HMR的Module.hot.accept方法(添加模块更新后的处理函数);我们手动在业务代码中添加容易犯错,也很麻烦,目前已经有很多的loader可以帮忙处理。

webpack拆包依据

官网链接:webpack.docschina.org/plugins/spl…

  • 最初,chunks(以及内部导入的模块)是通过内部webpack图谱中的父子关系关联的。
  • CommonsChunkPlugin曾被用来避免他们之间的重复依赖,但是不可能再做进一步的优化。
  • 从webpack v4开始,移除了CommonsChunkPlugin,取而代之的是optimization.splitChunks。
  • 开箱即用的SplitChunksPlugin对于大部分用户来说非常友好。
  • webpack将根据以下条件自动拆分chunks:
  1. 新的chunk可以被共享,或者模块来自于node_modules文件夹;
  2. 新的chunk体积大于20kb(在进行min+gz之前的体积);
  3. 当按需加载chunks时,并行请求的最大数量小于或等于30;
  4. 当加载初始化页面时,并发请求的最大数量小于或等于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时,要有几个条件:
  1. 模块系统必须为ESmodule;
  2. 在package.json文件标识sideEffect字段;
  3. 把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'
		})
	}
}

打包结果如图:

image.png

打包后即使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为例,具体过程:

  1. 编写ES6代码
  2. babylon进行解析
  3. 解析得到AST
  4. 用babel-traverse对AST进行遍历转译
  5. 得到新的AST树
  6. 用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工程

  1. 新建一个文件夹webpack-loader-demo
  2. 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"
}
  1. 安装webpack和webpack-cli
  • npm install webpack --save-dev
  • npm install webpack-cli --save-dev
  • 根目录下生成package-lock.json文件和node_modules文件夹
  1. 在package.json的script中增加build
"scripts": {
    "build": "webpack"
  },
  1. 根目录下新建文件夹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)
})
  1. 运行npm run build,根目录下生成dist文件夹,文件夹下生成mian.js文件
new Promise(((e,o)=>{setTimeout(e(1),1e3)})).then((e=>{console.log(e)}));

由此可见,默认情况下webpack会对文件进行压缩。

调试webpack

调试方法

  1. 在根目录的package.json文件中的script中添加"debug": "node --inspect-brk=5858 ./node_modules/webpack/bin/webpack"
  2. vcode的debug模式下,修改launch.json修改如下
{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "启动程序",
            "stopOnEntry": true,
            "runtimeExecutable": "npm",
            "runtimeArgs": [
                "run",
                "debug"
            ],
            "port": 5858
        }
    ]
}
  1. 启动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文件内容
  1. 执行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能解析三种文件路径:

  1. 绝对路径 由于已经获得文件的绝对路径,因此不需要做进一步解析。
  2. 相对路径 使用import或require的资源文件所处的目录,被认为是上下文目标。在import/require中给定的相对路径,会拼接此上下文路径,来生成模块的绝对路径。
  3. 模块路径 在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不同,配置值也会有所不同。

  1. 当target属性设置为web、webworker或者没有指定,mainFields的默认值如下:
//webpack.config.js

module.exports = {
  //...
  resolve: {
    mainFields: ['browser', 'module', 'main'],  // 优先级排序为browser > module > main
  },
};
  1. 对于其他任意的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'],
  },
};