【面试官来了】— 谈谈webpack5吧
前言
这“讨厌”的面试官又来了,挺着个油腻的大肚子,格子衫堪堪裹住他的腰,摸了摸他的地中海发型,张开自认为性感的厚嘴唇,说了句:“年轻人,怎么又是你?上次没把你虐惨吗?不死心的话,这次我们来聊聊 webpack5 吧,嘿嘿~”。
webpack简述
webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。它会根据配置文件的entry属性作为入口,找到各模块的依赖关系,然后将所有这些模块打包成一个或多个 bundle。webpack 只能理解 JavaScript 和 JSON 文件,因此如果想要增强它的能力,那么就需要相应的loader和plugin。另外plugin的串联执行,则用到的是 Tapable 这个库。
Tapable
Tapable 是一个专门用来实现事件订阅或者他自己称为hook(钩子)的工具库,其根本原理还是发布订阅模式,它对外暴露了多个 hook ,以便实现整个应用程序的事件流程。下面列举了主要的几个:
序号 | 钩子名称 | 执行方式 | 概要 |
---|---|---|---|
1 | SyncHook | 同步串行 | 不关心监听函数的返回值 |
2 | SyncBailHook | 同步串行 | 只要监听函数中有一个函数的返回值不为 null,则跳过剩下所有的逻辑 |
3 | SyncWaterfallHook | 同步串行 | 上一个监听函数的返回值可以传给下一个监听函数 |
4 | SyncLoopHook | 同步循环 | 当监听函数被触发的时候,如果该监听函数返回true时则这个监听函数会反复执行,如果返回 undefined 则表示退出循环 |
5 | AsyncParallelHook | 异步并发 | 不关心监听函数的返回值 |
6 | AsyncParallelBailHook | 异步并发 | 只要监听函数的返回值不为 null,就会忽略后面的监听函数执行,直接跳跃到callAsync等触发函数绑定的回调函数,然后执行这个被绑定的回调函数 |
7 | AsyncSeriesHook | 异步串行 | 不关系callback()的参数 |
8 | AsyncSeriesBailHook | 异步串行 | callback()的参数不为null,就会直接执行callAsync等触发函数绑定的回调函数 |
9 | AsyncSeriesWaterfallHook | 异步串行 | 上一个监听函数的中的callback(err, data)的第二个参数,可以作为下一个监听函数的参数 |
loader和plugin的区别
-
loader
上面提到过 webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效模块,以供应用程序使用,以及被添加到依赖图中,大白话就是loader一般是用来处理webpack不认识的文件类型,比如css、image、vue文件。。。所以才有css-loader、image-loader、vue-loader这些东西的存在。
-
plugin
loader 用于转换某些类型的模块,而plugins(插件)则可以用于增强 webpack,它的执行范围更广。比如:打包优化,资源管理,注入环境变量。简单来说,webpack 在运行时会对外广播事件,插件则监听它所订阅的事件,去改变执行结果。
webpack5 新特性:
启动命令
开发环境:webpack serve
生产环境:webpack
内置清除输出目录
之前的版本我们经常需要一个叫 clean-webpack-plugin 的插件,来帮助我们清除上次构建的dist产物,现在我们只要一个参数的配置就可以搞定:
//webpack.config.js
module.exports = {
output: {
clean: true,
}
}
缓存
会缓存生成的 webpack 模块和 chunk,来改善构建速度。webpack 追踪了每个模块的依赖,并创建了文件快照,与真实的文件系统进行对比,当发生差异时,触发对应的模块重新构建。默认开启缓存,总的来说有两种类型。
cache: { type: 'memory' }
这是默认配置,cache: { type: 'filesystem' }
缓存到本地文件系统,默认的缓存目录是 node_modules/.cache/webpack,当然也可以自己通过 cacheDirectory 属性配置,生成的目录结构大概是这样的:
资源模块
原生支持 json、png、jpeg、jpg、txt 等格式文件。也就是说无需配置额外的 loader,比如raw-loader、file-loader、url-loader 等等
// 'javascript/auto' | 'javascript/dynamic' | 'javascript/esm' | 'json' | 'webassembly/sync' | 'webassembly/async' | 'asset' | 'asset/source' | 'asset/resource' | 'asset/inline'
{
test: /\.png$/i,
type: "asset",
parser: {
dataUrlCondition: {
maxSize: 4 * 1024,
},
},
},
/*
与之对应的是之前 url-loader 的用法
use:[{
loader: 'url-loader',
options: {
limit: 8192,
}
}]
*/
moduleIds & chunkIds 的优化
在 webpack5 之前,没有从 entry 打包的 chunk 文件,都会以 1、2、3。。。的文件命名方式输出,这样删除某些文件由于顺序变了可能会导致缓存失效; 在 webpack5 中,生产环境下默认使用了 deterministic 的方式生成短 hash 值来分配给 modules 和 chunks 来解决上述问题
optimization: {
moduleIds: 'deterministic',
chunkIds: 'deterministic',
},
更智能的 tree shaking
webpack4 tree-shaking 是通过扫描文件中未引用到的函数实现再将其剔除实现的,作用很小,如果使用场景有嵌套的方法引用,就不管用了;比如,有如下的引用关系:
在 webpack5 中设置:
optimization: {
usedExports: true,
},
可以清楚的看到打包结果:
function2、function4 函数已经被标记为未被使用,在打包生产环境时,将会被剔除。
另外还可以在 package.json 中配置 sideEffects:false 表示整个项目都没有副作用,webpack 在打包时会自动剔除具有副作用代码; 当然也可以指定类型或文件保留副作用,比如配置 sideEffects: ['*.css'] 表示保留 import './index.css' 类似的代码
模块联邦
在介绍这个新特性之前,让我们来先假想一个场景:有两个独立的项目A、B,如果在这两个项目间有公共依赖,我们通常的做法是什么?貌似能想到的最优解就是将公共依赖做成npm包,然后两个项目分别安装,但是在每次对这个npm包升级的时候,两个项目都需要重新更新版本号。项目一旦多了,这样是不是有点繁琐?
基于此,webpack5 推出了模块联邦(Module Federation)这一新特性。用大白话来解释就是,webpack 提供了一种解决方案,将公共依赖打包放在远程地址,各项目间通过 CDN 的方式引用,以达到一种在线 runtime 的效果,它们的关系类似于这样:
搭建模块联邦
先初始化两个项目 provider、comsumer
pnpm install webpack webpack-cli webpack-dev-server html-webpack-plugin babel-loader @babel/preset-env @babel/preset-react @babel/core style-loader css-loader -D
pnpm install react react-dom
在熟悉这个功能之前,我们先理清两个重要的角色,webpack 官网上提出了两个概念:remotes 和 host,但为了方便理解,我个人更倾向于叫它们为 provider 和 comsumer,provider作为依赖的提供方,comsumer作为依赖的消费方。
// provider/src/webpack.config.js
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "provider", // 必须唯一 模块的名称
filename: "remoteEntry.js", // 必须 生成的模块名称
exposes: {
// 很明显,需要对外暴露的模块 注意该对象的key必须这么写
"./Search": "./src/Search",
"./utils": "./src/utils",
},
}),
],
};
// comsumer/src/webpack.config.js
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "comsumer", // 必须唯一 模块的名称
// 很明显,需要映射的远程provider
remotes: {
/**
* 这个地方来拆解下这个对象的参数
* key: 无所谓随意取,但在后续消费的时候有用
* value: "provider@http://localhost:9000/remoteEntry.js"
* 这里的provider:依然是上面project-a配置的name
* http://localhost:9000/: 这个表示provider项目部署后的远程地址
* remoteEntry.js:指的是上面provider项目中定义的filename
*/
module1: "provider@http://localhost:9000/remoteEntry.js",
},
}),
],
};
两个项目的配置定义好了,下面来看下在 comsumer 项目中怎么用吧
import React, { lazy, Suspense, useEffect } from "react";
/**
* import("module1/Search")
* 这里的module1 指的是上面comsumer配置中定义remotes时设置的key
* Search 指的是上面provider配置中定义exposes时设置的key
*/
const ProviderSearch = lazy(() => import("module1/Search"));
const App = () => {
return (
<div>
<h1>这是comsumer项目</h1>
<Suspense>
<ProviderSearch />
</Suspense>
</div>
);
};
export default App;
除此之外还有一种全局调用的方法:
在 provider 中加上
// provider
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
...
library: { type: "var", name: "provider" },
}),
],
};
在需要使用的地方,注意这里可以不用区别在 provider、comsumer 项目中
function loadComponent(scope, module) {
return async () => {
await __webpack_init_sharing__("default");
const container = window[scope];
await container.init(__webpack_share_scopes__.default);
const factory = await window[scope].get(module);
const Module = factory();
return Module;
};
}
// provider 指的是上面在library配置中定义的name属性,utils 指的是provider向外暴露的公共依赖
loadComponent("provider", "utils");
共享模块
如果上面的 provider 项目和 comsumer 项目都引入了 react 那么如何才能共用一个实例呢?我们只需要在原有的配置加上 shared 属性:
// 给两个项目都配置上shared
shared: {
react: {
singleton: true,
},
},
可以在 comsumer 项目中看到,引用了 provider 项目中 react 版本
这里需要注意的是:shared 默认选择的是高版本的共享模块,如果需要指定版本可以添加requiredVersion属性。
最后
还是说回我们这“讨厌的面试官”吧,此刻他心里一阵嘀咕:“这小子一日不见,当刮目相看啊!这次虐不了他了,算了,再找找下一个倒霉蛋吧”,故作镇定的说到:”webpack5 的新特性你没说完吧,算了,看你也不知道,给你份文档回去研究吧,webpack5 changelog,另外,你刚刚说的也有瑕疵哈,先回去等通知吧。”
各位吃瓜群众,我上面说的有瑕疵吗?请一定指正啊,好让我下次暴虐这“面试官”!
转载自:https://juejin.cn/post/7138237488099196964