跳到主要内容

webpack

什么是Webpack​

webpack is a module bundler.(模块打包⼯工具)

Webpack可以看做是模块打包机:它做的事情是,分析你的项目结构,找到JavaScript模块以及其它的⼀一些浏览器器不能直接运⾏的扩展语⾔(Scss, TypeScript等),并将其打包为合适的格式以供浏览器器使⽤用。

Webpack和Vite对比​

热更新区别:

  • Webpack热更新随着模块越多,热更新的速度就会越慢,每次热更新都会重新生成 bundle
  • Vite是利用浏览器解析 imports,服务器端按需编译返回,热更新的速度不会随着模块增多而变慢

打包区别:

  • Webpack分析入口,递归分析模块依赖,转换代码输出
  • Vite启动不需要打包

Webpack的相关使用场景​

提高效率的插件​

HotModuleReplacementPlugin​

text
const webpack = require('webpack')
module.exports = {
devServer:{
port:4000,
open:true,
//即便HMR不生效,浏览器也不⾃动刷新,就开启hotOnly (启动HMR)
hotOnly:true
},
plugins:[
//使用热模块
new webpack.HotModuleReplacementPlugin()
]
}
处理js模块HMR​
  • 之前我们热模块更新,修改css代码,页面上不用刷新浏览器即可发生样式变化,但是当我们修改js代码,页面并无更新, 此时需要使用module.hot.accept来观察模块更新 从而更新
  • 查看文档:https://webpack.docschina.org/guides/hot-module-replacement/#%E9%97%AE%E9%A2%98
  • module.hot.accept() 告诉 Webpack,当前模块更新不用刷新
  • module.hot.decline() 告诉 Webpack,当前模块更新时一定要刷新
  • 主要代码
    • 主要是把DOM节点删除remove掉,然后再执行函数添加DOM节点append,从而达到了重新渲染更新页面
text
//number.js
function number() {
var div = document.createElement("div");
div.setAttribute("id", "number");
div.innerHTML = 13000;
document.body.appendChild(div);
}
export default number;
import number from "./number";
if (module.hot) {
module.hot.accept("./b", function() {
document.body.removeChild(document.getElementById("number"));
number();
});
}

babel处理ES6​

1、Babel 是一个 JavaScript 编译器。

2、简单来说 :Babel可以ES6以上的js语法 转化为ES5,为了让js可以在浏览器兼容

babel-loader是webpack 与 babel的通信桥梁,不会做把es6转成es5的工作,这部分工作需要用到 @babel/preset-env里包含了es6转es5的转换规则

text
npm install babel-loader @babel/core @babel/preset-env -D
npm install --save @babel/polyfill

总结:理解这3个作用

babel-loader :webpack 与 babel的通信桥梁

@babel/preset-env:包含比较新的ES语法 ,es6转es5的转换规则

@babel/core:这个库负责解析,方便各个插件分析语法进行相应的处理

@babel-polyfill: 把es的新特性都装进来,来弥补低版本浏览器器中缺失的特性

webpack.config.js

text
module: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/, //排除node_modules
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
import "@babel/polyfill";
const arr = [new Promise(() => {}), new Promise(() => {})];
arr.map(item => {
console.log(item);
});

会发现打包的体积大了很多,这是因为polyfill默认会把所有特性注入进来,假如我想我用到的es6+,才会注入,没用到的不注⼊,从而减少打包的体积,可不可以呢 当然可以 修改Webpack.config.js

  • useBuiltIns是babel7新特性:http://babel.docschina.org/docs/en/babel-preset-env#docsNav

  • useBuiltIns: "usage" 会根据配置的浏览器兼容,以及你代码中用到的 API 来进行 polyfill,实现了按需添加。 (简单来说根据你的代码用到的API来按需引入polyfill)

    • useBuiltIns 选项是babel 7 的新功能,这个选项告诉babel 如何配 置@babel/polyfill 。 它有三个参数可以使用:

      ①entry: 需要在webpack 的⼊口⽂件⾥里import "@babel/polyfill" 一次。babel 会根据你的使⽤情况导⼊垫⽚,没有使用的功能不会被导⼊相应的垫片。 ②usage: 不需要import ,全⾃动检测,但是要安装@babel/polyfill 。 (试验阶段)

      ③false: 如果你import "@babel/polyfill" ,它不不会排除 掉没有使⽤用的垫⽚片,程序体积会庞⼤大。(不不推荐)

  • built-ins :表示嵌入部件,在这里表示嵌入特性

text
options: {
presets: [
[
"@babel/preset-env",
{
//目标浏览器版本
targets: {
edge: "17",
firefox: "60",
chrome: "67",
safari: "11.1"
},
useBuiltIns: "usage"//按需注入
}
]
]
}

当我们开发的是组件库,工具库这些场景的时候,polyfill就不适合了,因为polyfill是注入到全局变量 window下的,会污染全局环境,所以推荐闭包方式:@babel/plugin-transform-runtime

  • 我们开发的是组件库,工具库这些场景的时候 (开源的时候) (别人使用的时候你这个组件库时,你使用polyfill,可能会对别人开发环境造成影响),而当我们开发项目的时候,工作的时候,使用polyfill是非常合适的
  • @babel/plugin-transform-runtime 插件以沙箱垫片的方式防止污染全局
  • 文档:http://babel.docschina.org/docs/en/babel-plugin-transform-runtime#docsNav
  • 注意:@babel/plugin-transform-runtime 和@babel-polyfill作用是一样的 (都是为了填补浏览器新特性),只是面向的对象不同,前者防止污染,后者全局污染@babel/plugin-transform-runtime 用于组件库、工具库,,而@babel-polyfill用于开发项目,工作项目的是非常合适的,
text
npm install --save-dev @babel/plugin-transform-runtime
  • 创建 .babelrc文件夹

    当我们babel配置写在webpack.config.js会造成文件过长,看起来臃肿

    因此我们可以新建一个.babelrc文件夹,用来配置babel的

    • 把我们原来写在webpack.config.js的babel配置的options部分移入到该文件中,就可以了
  • .babelrc

text
{
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": false,
"helpers": true,
"regenerator": true,
"useESModules": false
}
]
]
}
  • webapck.config.js
text
module: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/, //排除node_modules
use: ['babel-loader']
}
]
}

配置React​

text
npm install react react-dom
npm install --save-dev @babel/preset-react

新建 .babelrc 文件

text
{
"presets": [
[
"@babel/preset-react",
{
"pragma": "dom", // default pragma is React.createElement
"pragmaFrag": "DomFrag", // default is React.Fragment
"throwIfNamespace": false // defaults to true
}
]
]
}

tree Shaking (减少包体积)​

  • 文档:https://webpack.docschina.org/guides/tree-shaking/
  • webpack2.x开始⽀支持 tree shaking概念,顾名思义,"摇树"
    • 通常用于移除 JavaScript 上下文中的未引用代码(dead-code 无生命的代码)
    • 摇树 可以理解为 把没有用到的代码给移除下去,消除无用代码,不进行打包,
  • 只支持ES module的引入方式!!!!

原理

  • treeShaking依赖ES6的静态语法 【ES module】
  • ES6 Module引入进行静态分析,故而编译的时候正确判断到底加载了那些模块
    • import命令会被js引擎静态分析
  • 静态分析程序流,判断那些模块和变量未被使用或者引用,进而删除对应代码

如何使用tree-shaking?

1).确保代码是es6格式,即 export,import

2).package.json中,设置sideEffects

3).确保tree-shaking的函数没有副作用

4).babelrc中设置presets [["env", { "modules": false }]] 禁止转换模块,交由webpack进行模块化处理

webpack.config.js

  • 注意:

在开发模式中,只会在打包文件里添加注释说明,并不会摇掉代码,

而生产模式中,会把没有使用到的代码摇下去,不进行打包

text
optimization: {
usedExports: true
}
  • 通过 package.json 的 "sideEffects" 属性,来实现这种方式。
    • 如果我们代码含有css, css是直接导入的,tree Shaking会把我们的css给摇掉了,为了避免这种情况,处理这种副作用,摇树的时候不要针对于css
    • 最好设置:"sideEffects":['*.css','@babel/polyfill']
text
{
"name": "your-project",
"sideEffects":['*.css','@babel/polyfill']
}
  • 如果所有代码都不包含 side effect,我们就可以简单地将该属性标记为 false,来告知 webpack,它可以安全地删除未用到的 export

code Splitting代码分割 (optimization)​

其实code Splitting概念 与 webpack并没有直接的关系,只不过webpack中提供了一种更加方便的方法供我们实现代码分割

  • 假如我们引⼊一个第三方的工具库,体积为1mb,而我们的业务逻辑代码也有1mb,那么打包出来的体积大小会在2mb 导致问题: 体积大,加载时间长 业务逻辑会变化,第三⽅方工具库不会,所以业务逻辑一变更,第三方工具库也要跟着变。

对于动态导入模块,默认使用 webpack v4+ 提供的全新的通用分块策略(common chunk strategy)。请在 SplitChunksPlugin 页面中查看配置其行为的可用选项。

text
optimization:{
//帮我们⾃动做代码分割
splitChunks:{
chunks:"all",//默认是⽀持异步,我们使⽤all
}
}
  • 此时,我们打包的时候,会多出来一个vendors~index.js,表示代码分割 ,抽取出来的公共模块
    • vendors.js 表示 第三方的公共模块库、插件
  • 以下是splitChunks的配置
text
optimization: {
splitChunks: {
chunks: 'async',//对同步 initial,异步 async,所有的模块有效all
minSize: 30000,//最⼩尺寸,当模块大于30kb
maxSize: 0,//对模块进行二次分割时使用,不推荐使⽤
minChunks: 1,//打包⽣成的chunk文件最少有⼏个chunk引用了这个模块
maxAsyncRequests: 5,//最⼤异步请求数,默认5
maxInitialRequests: 3,//最⼤初始化请求书,⼊口文件同步请求,默认3
automaticNameDelimiter: '~',//打包分割符号 默认~
name: true,//打包后的名称,除了布尔值,还可以接收⼀个函数 function
cacheGroups: {//缓存组
vendors: {
test: /[\\/]node_modules[\\/]/,
name:"vendor", // 要缓存的 分隔出来的 chunk 名称
priority: -10//缓存组优先级 数字越大,优先级越⾼
},
other:{
// 使⽤用下面配置即可:
chunks: "initial", // 必须三选一: "initial" | "all" | "async"(默认就是async)
test: /react|lodash/, // 正则规则验证,如果符合就提取
chunk,
name:"other",
minSize: 30000,
minChunks: 1,
},
commons:{
test:/(react|react-dom)/,
name:"react_vendors",
chunks:"all"
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true//可设置是否重⽤该chunk
}
}
}
}

(预取/预加载模块)prefetch/preload module​

  • prefetch(预取):将来某些导航下可能需要的资源
  • preload(预加载):当前导航下可能需要资源

prefetch 和 preload​

与 prefetch 指令相比,preload 指令有许多不同之处:

  • preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
  • preload 是以并行的方式加载,也就说多占了一个请求,对性能、响应时间请求会造成影响过慢
  • prefetch是浏览器闲置下载文件
  • preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
  • preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。
  • 浏览器支持程度不同。
  • 因此一般推荐使用prefetch,而不使用preload

  • webpackChunkName: 在 import 里以注释的形式为 chunk 文件取别名:

  • webpackPrefetch:也是以注释形式 对文件进行预取

    text
    async function getAsyncComponent() {
    text
    var element = document.createElement('div');
    text
    // 在 import 的括号里 加注释 /* webpackChunkName: "lodash" */ ,为引入的文件取别名
    text
    // const { default: _ } = await import(/* webpackChunkName: "lodash" */ 'lodash');
    text
    const { default: _ } = await import(/* webpackChunkName: "lodash" */ /* webpackPrefetch: true */ 'lodash');
    text
    element.innerHTML = _.join(['Hello!', 'dynamic', 'imports', 'async'], ' ');
    text
    return element;
    text
    }

1、网页中引入的静态资源多了有什么问题?​

1、网页加载速度慢,因为,我们要发起很多的二次请求

2、要处理错综复杂的依赖关系

解决上述两个问题​

使用Webpack,让开发过程更加高效

2、Webpack可以做什么?​

  • webpack可以用来做自动化打包,处理es6转换,图片处理,css样式压缩等等

压缩处理​

  • js压缩 (减小 JS 文件的大小,加速加载速度)
    • uglifyjs-webpack-plugin
  • css压缩
    • optimize-css-assets-webpack-plugin 插件
  • html文件压缩
  • 图片压缩
    • 通过url-loader小于 limit 一定限制的图片可以转化成 base64 格式
    • 比较大的图片我们可以用 image-webpack-loader 来压缩图片

减少包的体积​

  • Tree Shaking
text
optimization: {
usedExports: true
}
  • 代码分割
    • 也是在optimizaton属性的splitChunks去配置,
    • 也可以使用split-chunks-plugin插件去配置
text
optimization:{
//帮我们⾃动做代码分割
splitChunks:{
chunks:"all",//默认是⽀持异步,我们使⽤all
}
}

排除无用的 CSS​

  • purgecss-webpack-plugin插件

3、基本打包机制【原理】​

参考:https://juejin.cn/post/6987180860852142093#comment

本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。

打包过程可以拆分为四步:

  1. 利用babel完成代码转换,并生成单个文件的依赖
  2. 从入口开始递归分析,并生成依赖图谱
  3. 将各个引用模块打包为一个立即执行函数
  4. 将最终的bundle.js写入bundle.js中

简单的说

  • 1、找到入口文件,分析内容,获取AST抽象语法树
  • 2、递归分析找出模块依赖, 生成依赖关系图,拿到依赖路径,转换代码
  • 3、输出源代码,把bundle.js输出到 dist目录中

手写一个bundle.js​

1、借助@babel/parser :https://babeljs.io/docs/en/babel-parser#docsNav

  • 这是 babel7的工具,来帮助我们分析内部的语法,包括es6,返回一个AST抽象语法树
  • AST:Abstract Syntax Tree :抽象语法树

2、借助@babel/traverse : https://www.babeljs.cn/docs/babel-traverse

  • 根据AST的对象中body:Ast.program.body里面分析结果,遍历出所有的引入模块,但是比较麻烦
  • 这里还是推荐babel推荐的一个模块@babel/traverse来帮我们处理
  • @babel/traverse 可以用来遍历@babel/parser生成的AST

3、借助@babel/core 、@babel/preset-env

  • @babel/core: 这个库负责解析,方便各个插件分析语法进行相应的处理
  • @babel/preset-env: 包含比较新的ES语法 ,es6转es5的转换规则,转换当前浏览器可支持的语法特性
text
npm install @babel/parser --save //返回一个AST抽象语法树
npm install --save @babel/traverse //用于遍历AST

1、拿到入口文件名字,读取入口文件,分析代码

2、若入口文件有依赖,拿到对应的依赖模块

3、循环递归分析出所有依赖关系

4、对依赖模块进行语法分析,转换对应的代码

js
//找到入口文件,分析内容,有依赖的话,拿到依赖路径,转换代码(浏览器里可以运行的)
const path = require("path");
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const { transformFromAst } = require("@babel/core");

const entry = entryFile => {
const content = fs.readFileSync(entryFile, "utf-8");
const ast = parser.parse(content, {
sourceType: "module"
});

const dependecies = {};
traverse(ast, {
//以函数的方式

ImportDeclaration({ node }) {
const dirname = path.dirname(entryFile);
const newPath = "./" + path.join(dirname, node.source.value);
dependecies[node.source.value] = newPath;
}
});

const { code } = transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
});

return {
entryFile,
dependecies,
code
};
};

//分析出所有依赖关系

const Dependecies = entryFile => {
const info = entry(entryFile);

const modules = [];
modules.push(info);

for (let i = 0; i < modules.length; i++) {
const item = modules[i];
const { dependecies } = item;
if (dependecies) {
for (let j in dependecies) {
modules.push(entry(dependecies[j]));
}
}
}

const obj = {};
modules.forEach(item => {
obj[item.entryFile] = {
dependecies: item.dependecies,
code: item.code
};
});
return obj;
};

//生成代码

const genCode = entryFile => {
const obj = Dependecies("./src/index.js");
const graph = JSON.stringify(obj);
const bundle = `(function(graph){
function require(module){
function localRequire(relativePath){
return require(graph[module].dependecies[relativePath])
}
var exports = {};
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[module].code);
return exports;
}
require('${entryFile}')
})(${graph})`;

fs.writeFileSync(path.resolve(__dirname, "./dist/main.js"), bundle, "utf-8");
};

genCode("./src/index.js");

4、webpack中hash、chunkhash、contenthash区别​

webpack中对于输出文件名可以有三种hash值:

  • hash
  • chunkhash
  • contenthash

webpack给我们提供了三种哈希值计算方式,分别是hash、chunkhash和contenthash。那么这三者有什么区别呢?

  • hash:跟整个项目的构建相关,构建生成的文件hash值都是一样的,只要项目里有文件更改,整个项目构建的hash值都会更改。
  • chunkhash:根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的hash值。
    • chunk:可以理解为块,代码块、模块的意思,js文件模块相当于chunkhash
    • 一个入口文件对应一个chunk,即每个chunk都是独立的
    • 也就说当文件模块发生改变,那么对应的chunkhask也会发生改变,否则不改变
  • contenthash:由文件内容产生的hash值,内容不同产生的contenthash值也不一样。

显然,我们是不会使用第一种的。改了一个文件,打包之后,其他文件的hash都变了,缓存自然都失效了。这不是我们想要的。

那chunkhash和contenthash的主要应用场景是什么呢?在实际在项目中,我们一般会把项目中的css都抽离出对应的css文件来加以引用。如果我们使用chunkhash,当我们改了css代码之后,会发现css文件hash值改变的同时,js文件的hash值也会改变。这时候,contenthash就派上用场了。

  • 简单来说 : js使用chunkhash, css使用contenthash
    • 如果css和js都使用chunkhash的话,js改变,css没有改变,我们会发现js和css的hash值都改变了
    • css内容没有发生改变,则contenthash不会改变

5、有哪些常见的Loader?你用过哪些Loader?​

raw-loader:加载文件原始内容(utf-8)

file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件 (处理图片和字体)

url-loader:与 file-loader 类似,区别是用户可以设置一个阈值,大于阈值会交给 file-loader 处理,小于阈值时返回文件 base64 形式编码 (处理图片和字体)

source-map-loader:加载额外的 Source Map 文件,以方便断点调试

svg-inline-loader:将压缩后的 SVG 内容注入代码中

image-loader:加载并且压缩图片文件

json-loader 加载 JSON 文件(默认包含)

handlebars-loader: 将 Handlebars 模版编译成函数并返回

babel-loader:把 ES6 转换成 ES5

ts-loader: 将 TypeScript 转换成 JavaScript

awesome-typescript-loader:将 TypeScript 转换成 JavaScript,性能优于 ts-loader

sass-loader:将SCSS/SASS代码转换成CSS

css-loader:加载 CSS,支持模块化、压缩、文件导入等特性

style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS

postcss-loader:扩展 CSS 语法,使用下一代 CSS,可以配合 autoprefixer 插件自动补齐 CSS3 前缀

eslint-loader:通过 ESLint 检查 JavaScript 代码

tslint-loader:通过 TSLint检查 TypeScript 代码

mocha-loader:加载 Mocha 测试用例的代码

coverjs-loader:计算测试的覆盖率

vue-loader:加载 Vue.js 单文件组件

i18n-loader: 国际化

cache-loader: 可以在一些性能开销较大的 Loader 之前添加,目的是将结果缓存到磁盘里

6.有哪些常见的Plugin?你用过哪些Plugin?​

  • define-plugin:定义环境变量 (Webpack4 之后指定 mode 会自动配置)
  • ignore-plugin:忽略部分文件
  • html-webpack-plugin:简化 HTML 文件创建 (依赖于 html-loader)
  • web-webpack-plugin:可方便地为单页应用输出 HTML,比 html-webpack-plugin 好用
  • uglifyjs-webpack-plugin:不支持 ES6 压缩 (Webpack4 以前)
  • terser-webpack-plugin: 支持压缩 ES6 (Webpack4)
  • webpack-parallel-uglify-plugin: 多进程执行代码压缩,提升构建速度
  • mini-css-extract-plugin: 分离样式文件,CSS 提取为独立文件,支持按需加载 (替代extract-text-webpack-plugin)
  • serviceworker-webpack-plugin:为网页应用增加离线缓存功能
  • clean-webpack-plugin: 目录清理
  • ModuleConcatenationPlugin: 开启 Scope Hoisting
  • speed-measure-webpack-plugin: 可以看到每个 Loader 和 Plugin 执行耗时 (整个打包耗时、每个 Plugin 和 Loader 耗时)
  • webpack-bundle-analyzer: 可视化 Webpack 输出文件的体积 (业务组件、依赖第三方模块)
  • split-chunks-plugin :代码分割 ,抽取公共模块

7、使用webpack开发时,你用过哪些可以提高效率的插件?​

  • webpack-dashboard:可以更友好的展示相关打包信息。
  • webpack-merge:提取公共配置,减少重复配置代码
  • speed-measure-webpack-plugin:简称 SMP,分析出 Webpack 打包过程中 Loader 和 Plugin 的耗时,有助于找到构建过程中的性能瓶颈。
  • size-plugin:监控资源体积变化,尽早发现问题
  • HotModuleReplacementPlugin:模块热替换

8、 介绍下 webpack 热更新原理,是如何做到在不刷新浏览器的前提下更新页面的​

热更新,是指 Hot Module Replacement,缩写为 HMR。

从名字上解读,就是把“热”的模块进行替换。热,是指这个模块已经在运行中。

这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。

  • 1、HMR的核心就是客户端从服务端拉去更新后的文件
  • 2、实际上 WDS (webpack-dev-server)与浏览器之间维护了一个 Websocket,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比
  • 3、客户端对比出差异后会向 WDS 发起 Ajax 请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp 请求获取该chunk的增量更新。

后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin 来完成,提供了相关 API 以供开发者针对自身场景进行处理,像 react-hot-loader 和 vue-loader 都是借助这些 API 实现 HMR。

简单来说 :热更新原理

1、服务端和客户端使用websocket实现长连接

2、webpack内部实现的watch就会监听文件修改,只要有修改就webpack会重新打包编译到内存中 , webpack (webpack-dev-server)监听源文件的变化

  • 当本地资源发生变化时,都会触发webpack的重新编译
  • 每次编译都会生成 hash值

3、通过这个hash值与让客户端与上一次进行对比 (websocker传递的也是hash值,内部机制通过hash值检查进行热更新)

  • 一致则通过缓存获取数据资源
  • 不一致则通过 ajax和 jsonp向服务端获取最新资源

4、 对模块进行添加和删除,保证内容的局部刷新,浏览器不刷新

text
if (module.hot) {
module.hot.accept('./print.js', function() {
console.log('Accepting the updated printMe module!');
printMe();
document.body.removeChild(element);
element = component(); // 重新渲染 "component",以便更新 click 事件处理函数
document.body.appendChild(element);
})
}

HMR过程:

1、第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。

  • 保存在内存:这样做的理由就是能更快的访问文件,减少代码写入的开销

2、webpack 和 webpack-dev-server 之间的交互,其中,主要是利用 webpack-dev-server 里的 webpack-dev-middleware 这个中间件调用 webpack 暴露给外部的 API 对代码变化进行的监控。

3、对模块进行添加和删除,保证内容的局部刷新,浏览器不刷新

9、为何webpack-dev-server打包没有dist目录​

  • webpack-dev-server会先打包,但是打包后不会直接放在本地的资源,会放到内存里,提升打包速度

10、 理解webpack的bundle.js​

  • 配置将单个 bundle.js文件输出到 dist目录中。
  • bundle是最后打包后的文件,最终文件可以和chunk长的一模一样,但是大部分情况下他是多个chunk的集合

11、.webpack 中 module,chunk 和 bundle 的区别是什么?​

module,chunk 和 bundle 其实就是同一份逻辑代码在不同转换场景下的取了三个名字:

我们直接写出来的是 module,webpack 处理时是 chunk,最后生成浏览器可以直接运行的 bundle。

12、Webpack配置区别开发环境和生产环境​

在我们搭建好Vue-cli脚手架之后,我们的build文件夹会分别自动的生成webpack.base.conf.js、webpack.dev.conf.js、webpack.prod.conf.js三个webpack配置文件;

如果我们自己搭建webpack项目的话,得需要区别分开,自己新建以下文件夹

  webpack.base.conf.js:webpack的开发环境和生产环境的共有配置(开发环境和生产环境都是需要执行的配置)

  webpack.dev.conf.js:webpack的开发环境的特有配置(只在开发环境中执行,生产环境中不执行)

  webpack.prod.conf.js:webpack的生产环境的特有配置(只在生产环境中执行,开发环境中不执行)

webpack合并配置使用​

text
npm install --save-dev webpack-merge

webpack.base.conf.js​

text
const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
entry: {
app: './src/index.js'
},
plugins: [
new CleanWebpackPlugin(['dist']),
new HtmlWebpackPlugin({
title: 'Production'
})
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};

webpack.dev.conf.js​

text
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'development',
devtool: 'inline-source-map',
devServer: {
contentBase: './dist'
}
});

webpack.prod.conf.js​

text
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'production',
});

修改package.json​

text
"scripts": {
- "start": "webpack-dev-server --open",
+ "start": "webpack-dev-server --open --config webpack.dev.conf.js",
- "build": "webpack"
+ "build": "webpack --config webpack.prod.conf.js"
},

13、说说@babel-polyfill和@babel/plugin-transform-runtime的区别​

当我们开发的是组件库,工具库这些场景的时候,polyfill就不适合了,因为polyfill是注入到全局变量 window下的,会污染全局环境,所以推荐闭包方式:@babel/plugin-transform-runtime

  • 我们开发的是组件库,工具库这些场景的时候 (开源的时候) (别人使用的时候你这个组件库时,你使用polyfill,可能会对别人开发环境造成影响),而当我们开发项目的时候,工作的时候,使用polyfill是非常合适的
  • @babel/plugin-transform-runtime 插件以沙箱垫片的方式防止污染全局
  • 文档:http://babel.docschina.org/docs/en/babel-plugin-transform-runtime#docsNav
  • 注意:@babel/plugin-transform-runtime 和@babel-polyfill作用是一样的 (都是为了填补浏览器新特性),只是面向的对象不同,前者防止污染,后者全局污染@babel/plugin-transform-runtime 用于组件库、工具库,,而@babel-polyfill用于开发项目,工作项目的是非常合适的,

14、如何⾃⼰编写一个Loader​

参考官网文档:

loader 只是一个导出为函数的 JavaScript 模块

1、Loader就是一个函数,声明式函数,不能用箭头函数

2、匹配(test)单个 loader,你可以简单通过在 rule 对象设置 path.resolve 指向这个本地文件 (即自己写的loader文件)

3、使用 loader-utils 中的 getOptions 方法来提取给定 loader 的 option配置选项。

text
npm i loader-utils -D

4、Loader 运行在 Node.js 中,所以我们可以调用任意 Node.js 自带的 API ,或者安装第三方模块进行调用

5、this.callback :如何返回多个信息,不止是处理好的源码呢,可以使⽤this.callback来处理

6、Webpack 传给 Loader 的原内容都是 UTF-8 格式编码的字符串,当某些场景下 Loader 处理二进制文件时,需要通过 exports.raw = true 告诉 Webpack 该 Loader 是否需要二进制数据

7、Loader 是无状态的,我们不应该在 Loader 中保留状态

8、处理loader的异步: 使用this.async , 返回this.callback

text
module.exports = function(source) {
const options = loaderUtils.getOptions(this);
//定义⼀一个异步处理,告诉webpack,这个loader⾥有异步事件,在⾥面调⽤下这个异步
//callback 就是 this.callback 注意参数的使⽤用
const callback = this.async();
setTimeout(() => {
const result = source.replace("kkb", options.name);
callback(null, result);
}, 3000);
};

9、执行多个loader时,需要注意顺序,下到上,右到左

10、加载本地 Loader 方法

  • npm link
  • 将本地的一个正在开发的模块 (如loader)的源码链接到项目的 node_modules 目录下,让项目可以直接使 用本地的 npm 模块
  • 完成npm link的步骤如下:

1、确保正在开发的本地npm模块的package.json已经正确配置好;

2、在本地的npm模块根目录下执行npm link,将本地模块注册到全局;

3、在项目根目录下执行npm link loader-name,将第二步注册到全局的本地npm模块链接到项目的node_modules下,其中的loader-name是指在第一步的package.json文件中配置的模块名称

  • resolveLoader :解析loader,处理loader的路径问题
  • ResolveLoader 用于配置 Webpack 如何寻找 Loader ,它在默认情况下只会去 node_modules 目录下寻找。为了让 Webpack 加载放在本地项目中的 Loader,需要修改 resolveLoader.modules。
  • https://www.webpackjs.com/configuration/resolve/#resolveloader
text
resolveLoader: {
//loader默认会在node_modules查找,若找不到则在loader文件夹里找
modules: ["node_modules", "./loader"]
},

例子1 (替换源码中字符串)​

loader文件夹​

my-loader.js​
text
//loader的基本结构
const loaderUtils = require('loader-utils') //获取loader中options配置选项
//声明式函数
module.exports = function (source) {
//不能使用箭头函数,因为需要this的正确指向
let content = source //传递给 Loader 的一个文件的原内容,
let newStr = content.replace('hello', 'this is my-loader')
//对内容进行处理,返回相应的结果
// console.log(content)
// console.log(this) //使用 this 可以访问的一些方法或属性
// console.log(this.query) //获取options选项配置 (原始方法)
const options = loaderUtils.getOptions(this)
// console.log(options)
// return content
return this.callback(null, newStr) //可返回多个结果
}
othter-loader.js​
text
module.exports = function (source) {
let newStr = source.replace('loader', 'this other loader')
return this.callback(null, newStr)
}

webpack.config.js​

text
const path = require('path')

module.exports = {
entry: './src/index.js',
mode: 'development',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'main.js',
},
resolveLoader: {
modules: ['node_modules', './loader'],
},
module: {
rules: [
{
test: /\.js$/,
use: [
//在解析JS模块时,会使用到这个loader
//加载本地loader文件
// loader: path.resolve(__dirname, './loader/my-loader.js'),
'other-loader',
{
loader: 'my-loader',
//options配置项
options: {
name: 'my-loader',
},
},
],
},
],
},
}

15、如何自己编写一个Plugins​

官方:

1、Plugin: 开始打包,在某个时刻,帮助我们处理一些事情的机制

2、plugin要比loader稍微复杂⼀一些,在webpack的源码中,用plugin的机制 还是占有非常大的场景,可以说plugin是webpack的灵魂

3、plugin是一个类,里面包含一个apply函数,接受一个参数,compiler

4、可以在 compiler 对象实例上注册各种事件钩子函数,来影响 webpack 的所有构建流程,以便完成更多其他的构建任务

5、同步的钩子使用 tap, 如果是异步的事件钩子,那么可以使用 tapPromise 或者 tapAsync 来注册事件函数,tapPromise 要求方法返回 Promise 以便处理异步,而 tapAsync 则是需要用 callback 来返回结果

是一个独立的模块

模块对外暴露一个 js 函数

函数的原型 (prototype) 上定义了一个注入 compiler 对象的 apply 方法 apply 函数中需要有通过 compiler 对象挂载的 webpack 事件钩子,钩子的回调中能拿到当前编译的 compilation 对象,如果是异步编译插件的话可以拿到回调 callback

完成自定义子编译流程并处理 complition 对象的内部数据

如果异步编译插件的话,数据处理完成后执行 callback 回调。

步骤​

1、暴露一个 js 函数

2、在函数的原型对象添加一个apply 方法,接受一个参数compiler

3、可以在 compiler 对象实例上注册各种事件钩子函数,来影响 webpack 的所有构建流程,以便完成更多其他的构建任务

4、

例子1 (生成文件列表)​

plugin文件夹​

my-plugins.js​
text
class MyPlugin {
constructor(options) {
console.log(options, 'options配置项')
}
apply(compiler) {
//emit:表示生成output目录之前执行
//plugins类型
//同步 (同步没有回调callback)
compiler.hooks.compile.tap('MyPlugin', (compilation) => {
console.log('同步钩子')
})
//异步
//异步需要使用callback回调,来确保执行下一个异步事件
compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
// // 将这个列表作为一个新的文件资源,插入到 webpack 构建中:
var filelist = '这是创建的文件'
compilation.assets['filelist.md'] = {
source: function () {
return filelist
},
size: function () {
return filelist.length //返回字节长度
},
}
callback()
})
}
}
module.exports = MyPlugin

webpack.config.js​

text
const path = require('path')
const myPlugin = require('./plugin/my-plugin')
module.exports = {
entry: './src/index.js',
mode: 'development',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'main.js',
},
resolveLoader: {
modules: ['node_modules', './loader'],
},
plugins: [
new myPlugin({
name: 'this is myPlugin',
}),
],
}

16、提升打包速度​

分析影响打包速度环节

  • 1、找到入口文件,分析内容
  • 优化解析时间
  • 2、找出所有模块依赖, 拿到依赖路径,转换代码
  • 优化搜索时间
  • 3、把所有的依赖模块输出源代码,把bundle.js输出到 dist目录中
  • 把源代码进行压缩,通常 webpack 需要卡好一会,这是因为压缩 JS 需要先将代码解析成 AST 语法树,然后需要根据复杂的规则去分析和处理 AST,最后将 AST 还原成 JS,这个过程涉及到大量计算,因此比较耗时,打包就容易卡住。
  • 优化压缩时间。

1、优化解析时间 (开启多进程)​

thread-loader(webpack4 官方推荐)​

文档:https://webpack.docschina.org/loaders/thread-loader/

thread-loader 使用起来也非常简单,只要把 thread-loader 放置在其他 loader 之前, 那 thread-loader 之后的 loader 就会在一个单独的 worker 池(worker pool)中运行。

  • 在 worker 池(worker pool)中运行的 loader 是受到限制的。例如:
  • 这些 loader 不能产生新的文件。
  • 这些 loader 不能使用定制的 loader API(也就是说,通过插件)。
  • 这些 loader 无法获取 webpack 的选项设置。

每个 worker 都是一个单独的有 600ms 限制的 node.js 进程。同时跨进程的数据交换也会被限制。

请仅在耗时的 loader 上使用

例子

text
module.exports = {
// ...
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
// 创建一个 js worker 池
use: [
'thread-loader',
'babel-loader'
]
},
{
test: /\.s?css$/,
exclude: /node_modules/,
// 创建一个 css worker 池
use: [
'style-loader',
'thread-loader',
{
loader: 'css-loader',
options: {
modules: true,
localIdentName: '[name]__[local]--[hash:base64:5]',
importLoaders: 1
}
},
'postcss-loader'
]
}
// ...
]
// ...
}
// ...
}

HappyPack (推荐使用 thread-loader,不再继续维护 happypack)​

1、在webpack构建过程中,实际上耗费时间大多数用在 loader 解析转换以及代码的压缩中,

2、HappyPack 可以让 Webpack 同一时间处理多个任务,发挥多核 CPU 的能力,将任务分解给多个子进程去并发的执行,子进程处理完后,再把结果发送给主进程。

3、使用 HappyPack 也有一些限制,它只兼容部分主流的 loader,具体可以查看官方给出的 兼容性列表。

2、优化压缩时间​

text
npm install terser-webpack-plugin --save-dev
terser 是什么?​

所谓 terser,官方给出的定义是:

用于 ES6+ 的 JavaScript 解析器、mangler/compressor(压缩器)工具包。

为什么 webpack 选择 terser?

不再维护 uglify-es ,并且 uglify-js 不支持 ES6 +。

terser 是 uglify-es 的一个分支,主要保留了与 uglify-es 和 uglify-js@3 的 API 和 CLI 兼容性。

terser 启动多进程​
  • 使用多进程并行运行来提高构建速度。并发运行的默认数量为 os.cpus().length - 1 。
text
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({
cache: true,
//开启多进程并行运行提高构建速度
parallel: true,
}),
],
},
};

3、优化搜索时间 (缩小文件搜索范围,减少不必要的编译工作)​

1、根据导入语句去寻找对应的要导入的文件

2、根据找到的要导入文件的后缀,使用配置中的 Loader 去处理文件

  • 使用 ES6 开发的 JavaScript 文件需要使用 babel-loader 去处理。

1、优化 loader 配置​

使用 Loader 时可以通过 test 、 include 、 exclude 三个配置项来命中 Loader 要应用规则的文件

2、优化 resolve.modules 配置​

  • resolve.modules 用于配置 webpack 去哪些目录下寻找第三方模块,resolve.modules 的默认值是 ['node_modules'] ,含义是先去当前目录下的 ./node_modules 目录下去找想找的模块,如果没找到就去上一级目录 ../node_modules 中找,再没有就去 ../../node_modules 中找,以此类推。

3、优化 resolve.alias 配置​

4、优化 resolve.extensions 配置​

  • 在导入语句没带文件后缀时,webpack 会根据 resolve.extension 自动带上后缀后去尝试询问文件是否存在,所以在配置 resolve.extensions 应尽可能注意以下几点:
    • resolve.extensions 列表要尽可能的小,不要把项目中不可能存在的情况写到后缀尝试列表中。
    • 频率出现最高的文件后缀要优先放在最前面,以做到尽快的退出寻找过程。
    • 在源码中写导入语句时,要尽可能的带上后缀,从而可以避免寻找过程。

5、优化 resolve.mainFields 配置​

  • 有一些第三方模块会针对不同环境提供几分代码。 例如分别提供采用 ES5 和 ES6 的2份代码,这2份代码的位置写在 package.json 文件里,如下:

6、优化 module.noParse 配置​

  • module.noParse 配置项可以让 Webpack 忽略对部分没采用模块化的文件的递归解析处理,这样做的好处是能提高构建性能。 原因是一些库,例如 jQuery 、ChartJS, 它们庞大又没有采用模块化标准,让 Webpack 去解析这些文件耗时又没有意义。

配置例子​

text
// 编译代码的基础配置
module.exports = {
// ...
module: {
// 项目中使用的 jquery 并没有采用模块化标准,webpack 忽略它
noParse: /jquery/,
rules: [
{
// 这里编译 js、jsx
// 注意:如果项目源码中没有 jsx 文件就不要写 /\.jsx?$/,提升正则表达式性能
test: /\.(js|jsx)$/,
// babel-loader 支持缓存转换出的结果,通过 cacheDirectory 选项开启
use: ['babel-loader?cacheDirectory'],
// 排除 node_modules 目录下的文件
// node_modules 目录下的文件都是采用的 ES5 语法,没必要再通过 Babel 去转换
exclude: /node_modules/,
},
]
},
resolve: {
// 设置模块导入规则,import/require时会直接在这些目录找文件
// 可以指明存放第三方模块的绝对路径,以减少寻找
modules: [
path.resolve(`${project}/client/components`),
path.resolve('h5_commonr/components'),
'node_modules'
],
// import导入时省略后缀
// 注意:尽可能的减少后缀尝试的可能性
extensions: ['.js', '.jsx', '.react.js', '.css', '.json'],
// import导入时别名,减少耗时的递归解析操作
alias: {
'@compontents': path.resolve(`${project}/compontents`),
}
},
};

4、优化二次打包时间 (利用缓存)​

当更改项目中一个小小的文件时,我们需要重新打包,所有的文件都必须要重新打包,需要花费同初次打包相同的时间,但项目中大部分文件都没有变更,尤其是第三方库。

  • 合理利用缓存,来减少二次打包时间
  • 比如 cache-loader HardSourceWebpackPlugin、 babel-loader的cacheDirectory标志

cache-loader​

安装依赖:

text
npm install cache-loader -D

webpack.config.js

text
module.exports = {
module: {
rules: [
{
//ext后缀是一些js编程库
test: /\.ext$/,
use: ['cache-loader', ...loaders],
include: path.resolve('src'),
},
],
},
};

HardSourceWebpackPlugin​

text
npm install --save-dev hard-source-webpack-plugin
// webpack.config.js
var HardSourceWebpackPlugin = require('hard-source-webpack-plugin');

module.exports = {
context: // ...
entry: // ...
output: // ...
plugins: [
new HardSourceWebpackPlugin()
]
}

配置

text
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')
const clientWebpackConfig = {
// ...
plugins: [
new HardSourceWebpackPlugin({
// cacheDirectory是在高速缓存写入。默认情况下,将缓存存储在node_modules下的目录中
// 'node_modules/.cache/hard-source/[confighash]'
cacheDirectory: path.join(__dirname, './lib/.cache/hard-source/[confighash]'),
// configHash在启动webpack实例时转换webpack配置,
// 并用于cacheDirectory为不同的webpack配置构建不同的缓存
configHash: function(webpackConfig) {
// node-object-hash on npm can be used to build this.
return require('node-object-hash')({sort: false}).hash(webpackConfig);
},
// 当加载器、插件、其他构建时脚本或其他动态依赖项发生更改时,
// hard-source需要替换缓存以确保输出正确。
// environmentHash被用来确定这一点。如果散列与先前的构建不同,则将使用新的缓存
environmentHash: {
root: process.cwd(),
directories: [],
files: ['package-lock.json', 'yarn.lock'],
},
// An object. 控制来源
info: {
// 'none' or 'test'.
mode: 'none',
// 'debug', 'log', 'info', 'warn', or 'error'.
level: 'debug',
},
// Clean up large, old caches automatically.
cachePrune: {
// Caches younger than `maxAge` are not considered for deletion. They must
// be at least this (default: 2 days) old in milliseconds.
maxAge: 2 * 24 * 60 * 60 * 1000,
// All caches together must be larger than `sizeThreshold` before any
// caches will be deleted. Together they must be at least this
// (default: 50 MB) big in bytes.
sizeThreshold: 50 * 1024 * 1024
},
}),
new HardSourceWebpackPlugin.ExcludeModulePlugin([
{
test: /.*\.DS_Store/
}
]),
]
}

babel-loader的cacheDirectory标志​

如果设置了一个空值 (loader: 'babel-loader?cacheDirectory') 或者 true (loader: 'babel-loader?cacheDirectory=true'),loader 将使用默认的缓存目录 node_modules/.cache/babel-loader,

17、说一说Babel原理吧​

  • 解析: 代码在编译阶段解析成抽象语法树(AST)
    • @babel/parser来帮助我们分析内部的语法. 返回一个AST抽象语法树
  • 转换: 然后经过一系列的遍历和转换抽象语法树
    • @babel/traverse 可以用来遍历@babel/parser生成的AST
  • 生成: 以新的 抽象语法树AST 生成代码
    • 借助@babel/core (负责解析)、@babel/preset-env (转换js语法)

18、webpack插件的compiler、complication区别​

  • Compiler 模块是 webpack 的支柱引擎,它通过 CLI 或 Node API 传递的所有选项,创建出一个 compilation 实例,该对象代表了完整的webpack环境配置, 代表了整个构建的过程
  • Compilation 模块会被 Compiler 用来创建新的编译(或新的构建),它表示的是某一个模块的资源、编译生成的资源、变化的文件等等

19、loader和plugin区别​

loader:

  • 加载器、转换器
  • loader 可以让 webpack 能够去处理其他类型的文件,并将它们转换为模块,程序就可以识别使用

因为webpack不支持css、png、ts、jsx、vue等,因此需要借助loader来处理这些类型文件,转换为模块,让浏览器支持和识别这些模块

场景:

如果要加载css文件,使用css-loader

要使用ts文件,使用ts-loader

要使用eslint,使用eslint-loader

要使用vue,使用vue-loader

。。。。。

plugin:

  • plugin 可以在webpack运行到某个阶段的时候,帮你做一些事情,类似于生命周期的概念
  • webpack在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在特定的阶段钩入想要添加的自定义功能
  • 可以扩展 Webpack 的功能

20、webpack5特点​

webpack4: 2018-2发布

webpack5:2020-10-10发布

模块联邦 (Module Federation)​

  • 模块联邦目的就是更好的共享代码
    • 在之前,共享代码一般需要用npm发包来解决,npm需要构建,发布,引用这些阶段
  • 模块联邦的打包机制:修改了 __webpack_require__ 的部分实现,在require的时候从远程加载资源,然后合并到 __webpack_modules__ 中
text
// host
module.exports = {
plugins: [new ModuleFederationPlugin({
name: "host",
remotes: {
"app": "app@http://localhost:3001/remote-entry.js"
},
shared: {
react: {
eager: true
}
}
})]
}

// remote
module.exports = {
plugins: [new ModuleFederationPlugin({
name: "app",
filename: 'remote-entry.js',
exposes: {
"button": "./src/button.js"
},
shared: ['react']
})]
}

21、打包一个库​

  • 可设置output字段配置
text
module.exports = {
//...
output: {
library: 'someLibName',
libraryTarget: 'umd',
filename: 'someLibName.js',
auxiliaryComment: 'Test Comment',
},
};

22、如何解决webpack重复打包同名依赖包?​

  • 可用webpack配置的spiltchunk、SplitChunksPlugin、进行代码分离、代码拆分、分包,避免他们之间的重复依赖
  • external 配置

external配置​

  • 防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)
    • 比如可以从CDN去引入包,而不用打包

SplitChunks​

  • SplitChunksPlugin插件作用:简单的来说就是Webpack中一个提取或分离代码的插件,主要作用是提取公共代码,防止代码被重复打包、拆分过大的js文件,合并零散的js文件