人之所以不高兴,次要是缘于过来和将来:为过来耿耿于怀,为将来惴惴不安
大家好,我是柒八九。
在后期,咱们开了一个对于前端工程化的系列文章。曾经从
概念介绍
- 何为脚手架
SourceMap
的惯例概念- 在Webpack 中针对
SourceMap
的配置
构建工具
- 构建解决的问题
- 包管理工具
- 模块化常见形式
等角度进行了一些惯例概念的介绍和梳理。而明天,咱们抉择了一个在前端范畴内,占很大比重的构建工具--Webpack
。
近一年的npm
下载量
github对应的stars
能够看到,无论是从npm 下载量和github的star的数量,Webpack
都遥遥领先于其余工具(grunt
/gulp
/rollup
/swc
)
Webpack
是一个十分弱小的构建工具,它能够被认为是当今许多技术中的一个根本组件,前端开发人员应用它来构建他们的应用程序。
好了,话不多说,持续赶路。
你能所学到的知识点
Webpack
常见概念(entry/output/loader
)- 从
entry
对象{a:'./a.js'}
如何构建一个模块树
`entry`=>`EntryPlugin`=>`EntryDependency`=>`NormalModuleFactory`=>模块树
ModuleGraph
如何跟踪已建模块间接的关系ModuleGraph
是如何被构建的Module/Chunk/ ChunkGroup /EntryPoint
具体是啥并它们间接的关系ChunkGraph
是如何被构建的
文章概要
Webpack
基本概念简讲- 打包流程总览
entry
对象- 深刻了解 ModuleGraph
- 构建ModuleGraph
- Module/Chunk/ ChunkGroup /EntryPoint 是个啥
- 提交chunk资源
0. Webpack
基本概念简讲
实质上,webpack
是一个古代JavaScript
应用程序的动态<span style="font-weight:800;color:#FFA500;font-size:18px">{模块打包器| module bundler}</span>
当webpack
解决应用程序时,它会递归地构建一个<span style="font-weight:800;color:#FFA500;font-size:18px">{依赖关系图| dependency graph}</span>,其中蕴含应用程序须要的每个模块,而后将所有这些模块打包成一个或多个bundle
。
运行形式
在我的项目中有两种运行 Webpack
的形式:
- 基于命令行的形式
webpack --config webpack.config.js
基于代码的形式
var webpack = require('webpack') var config = require('./webpack.config')webpack(config, (err, stats) => {})
重要概念
针对
Webpack
有几个重要的概念须要通晓。
关键字 | 作用 |
---|---|
Entry | Webpack 的入口文件 指的是应该从哪个模块作为入口,来构建外部依赖图 |
Output | 通知 Webpack 在哪输入它所创立的 bundle 文件以及输入的 bundle 文件该如何命名、输入到哪个门路下等规定 |
Loader | 模块代码转化器 使得 Webpack 有能力去解决除了 JS、JSON 以外的其余类型的文件 |
Plugin | Plugin 提供执行更广的工作的性能 包含:打包优化,资源管理,注入环境变量等 |
Mode | 依据不同运行环境执行不同优化参数时的必要参数 |
Browser Compatibility | 反对所有 ES5 规范的浏览器(IE8 以上) |
1. 打包流程总览
在此篇文章中,咱们只针对NormalModules
进行解说。当然,还有其余类型的模块 如 ExternalModule
和 ConcatenatedModule
(当应用require.context()
时)这些都不在咱们探讨范畴内。
先来一个总体流程,润润嗓子。(莫慌,要害的点,前面都会做解释)
2. entry
对象
所有都从entry
对象开始
咱们用一个简略例子来阐明它的作用,在这个例子中,entry
对象只是一个键值对的汇合。
// webpack.config.jsentry: { a: './a.js', b: './b.js', /* ... */}
webpack
中的模块与文件是一一对应的。
因而,在下面的代码中,a.js
会产生一个新的模块,而b.js
也会产生。
模块是文件的升级版。
模块,一旦创立和构建,除了源代码,还蕴含很多有意义的信息,如:
- 应用的加载器
- 它的依赖关系
- 它的进口(如果有的话)
- 它的哈希值
同时entry
对象中的每一项都能够被认为是模块树中的根模块。模块树是因为根模块可能须要一些其余的模块,这些模块可能须要其余的模块,等等。所有这些模块树都被贮存在 ModuleGraph
中。
咱们须要提到的下一件事是,webpack是建设在很多插件之上的。人们有很多办法能够退出自定义逻辑。webpack的可扩展性是通过hook
实现的。例如,你能够在 ModuleGraph
建设后,当一个新的<span style="font-weight:800;color:#FFA500;font-size:18px">{资源|asset }</span>被生成时,在模块行将被建设前(运行加载器和解析源代码),增加自定义逻辑。大多数时候,hook
是依据它们的目标分组的,对于任何定义好的目标,都有一个<span style="font-weight:800;color:#FFA500;font-size:18px">{插件| plugin}</span>。例如,有一个插件负责解决import()
函数(负责解析正文和参数)--它被称为 ImportParserPlugin
,它所做的就是在 AST
解析过程中遇到import()
调用时,增加一个hook
。
有几个插件负责解决entry
对象。有一个 EntryOptionPlugin
,它实际上是接管entry
对象,并为对象中的每个我的项目创立一个 EntryPlugin
对象。entry
对象的每个我的项目都会产生一棵模块树(所有这些树都是互相拆散的)。基本上,EntryPlugin
开始创立一个模块树,每个模块都会在同一个中央(ModuleGraph
)增加信息。
EntryPlugin
也是创立 EntryDependency
的中央。
基于上图,让咱们简略地实现自定义的EntryOptionsPlugin
。
class CustomEntryOptionPlugin { // 这是创立插件的规范办法(实现apply办法) // 就是一个简略的函数 apply(compiler) { compiler.hooks.entryOption.tap('CustomEntryOptionPlugin', entryObject => { // 对于`entryObject`中的每个项,咱们为其创立一个模块树。 // 记住,每个模块树都是独立的 // `entryObject`能够是这样的。`{ a: './a.js' }` for (const name in entryObject) { const fileName = entryObject[name]; // 当打包过程开始时,筹备为这个entryObject创立一个模块树。 new EntryOption({ name, fileName }).apply(compiler); }; }); }};
代码解析:
hook
为咱们提供了染指打包过程的可能性。- 在
entryOption
hook的帮忙下,咱们增加了一个逻辑。 - 此时基本上意味着打包过程的开始。
entryObject
参数将保留来自配置文件的entry
对象。- 配置文件中的
entry
对象,将用它来设置创立模块树。
EntryOption插件的定义
// `EntryOption`类将解决模块树的创立。class EntryOption { constructor (options) { this.options = options; }; // 这依然是一个插件,所以咱们要恪守规范(实现apply) apply(compiler) { compiler.hooks.start('EntryOption', ({ createModuleTree }) => { // 基于这个插件的配置,创立新的模块树。 createModuleTree(new EntryDependency(this.options)); }); };};
代码解析:
start
钩子标记着打包过程的开始。 它将在调用hooks.entryOption
之后被调用。options
蕴含entry名称(实质上就是块的名称)和文件名。EntryDependency
封装了这些选项,同时也提供了创立模块的办法。- 在调用
createModuleTree
后,文件的源代码将被找到。 - 而后,一个模块实例将被创立,而后webpack将失去它的
AST
,并且将在打包过程中进一步应用
下面代码中,咱们提到了EntryDependency
,咱们来一步理解一下。
当波及到创立新模块时,这所有都归结为一个形象过程。简略地说,一个<span style="font-weight:800;color:#FFA500;font-size:18px">{依赖关系|dependency }</span>只是一个理论模块实例的初步入口。例如,在 webpack
的观点中,甚至entry
对象的项也是依赖关系,它们表明了创立模块实例的最低限度:它的门路(例如./a.js
, ./b.js
)。如果没有依赖关系,模块的创立就无奈开始,因为依赖关系除其余重要信息外,还蕴含模块的申请,即能够找到模块源代码的文件的门路(例如"./a.js")。
依赖关系还表明如何构建该模块,它通过<span style="font-weight:800;color:#FFA500;font-size:18px">{模块工厂|ModuleFactory}</span>来实现。模块工厂晓得如何从一个原始状态(例如源代码是一个简略的字符串)开始,而后达到具体的实体文件,而后被webpack利用。EntryDependency
实际上是 ModuleDependency
的一种类型,意味着它必定会持有模块的申请,它所指向的模块工厂是 NormalModuleFactory
。那么,NormalModuleFactory
就晓得要做什么,以便从一个门路中创立对webpack有意义的货色。
另一种思考形式是,模块起初只是一个简略的门路(要么在entry
对象中,要么是import
语句的一部分),而后它变成了一个依赖关系,最初变成了一个模块。
因而,EntryDependency
在一开始创立模块树的根模块时就被应用。
对于其余的模块,还有其余类型的依赖关系。例如,如果你应用import
语句,比方从./a.js
导入 defaultFn
,那么就会有一个 HarmonyImportSideEffectDependency
,./a.js
持有模块的申请,也映射到 NormalModuleFactory
。因而,将有一个新的模块用于'a.js'文件。
疾速回顾一下咱们在本节中所学到的内容:
- 对于
entry
对象中的每一项,都会有一个EntryPlugin
实例,其中创立了一个EntryDependency
。- 这个
EntryDependency
保留了模块的申请(即文件的门路),并且通过映射到一个模块工厂,即NormalModuleFactory
,提供了一种使该申请有用的办法。- <span style="font-weight:800;color:#FFA500;font-size:18px">{模块工厂|ModuleFactory}</span>晓得如何从一个文件门路中创立对webpack有用的实体。
- <span style="font-weight:800;color:#FFA500;font-size:18px">{依赖关系|dependency }</span>对创立一个模块至关重要,因为它领有重要的信息,比方模块的申请和如何解决该申请。依赖关系有几种类型,并不是所有的依赖关系都对创立一个新模块有用。
- 从每个
EntryPlugin
实例,在新创建的EntryDependency
的帮忙下,将创立一个模块树。模块树是建设在模块和它们的依赖关系之上的,这些模块也能够有依赖关系。
3. 深刻了解 ModuleGraph
ModuleGraph
是一种跟踪已建模块的办法。
它在很大水平上依赖于<span style="font-weight:800;color:#FFA500;font-size:18px">{依赖关系|dependency }</span>,因为它们提供了连贯两个不同模块的办法。
比如说。
// a.jsimport defaultBFn from '.b.js/';// b.jsexport default function () { console.log('我是大帅 B!'); }
这里咱们有两个文件,所以有两个模块。文件a
须要文件b
的一些货色,所以在a
中存在一个依赖关系,这个依赖关系是通过导入语句建设的。就 ModuleGraph
而言,依赖关系定义了一种连贯两个模块的形式。下面的片段能够被可视化为:
ModuleGraph
的节点被称为 ModuleGraphModule
,它只是一个装璜过的NormalModule实例。ModuleGraph
在一个map
对象的帮忙下跟踪这些装璜过的模块,它有这样的签名:Map<Module, ModuleGraphModule>
。
例如,如果只有 NormalModule
实例,那么你对它们就没有什么可做的,它们不晓得如何互相交换。ModuleGraph
赋予这些原始模块能通过上述map
的帮忙将它们相互连接起来的能力,该map
为每个NormalModule调配了一个ModuleGraphModule。咱们将把属于ModuleGraph的模块也称为模块,因为NormalModule
和ModuleGraphModule
区别在于只包含一些额定的无关紧要属性。
对于一个属于 ModuleGraph
的节点来说,有几件事件是明确的:传入的连贯和传出的连贯。连贯是 ModuleGraph
的另一个小实体,它领有有意义的信息,如:起源模块、指标模块和连贯上述两个模块的依赖关系。具体来说,基于上图,一个新的连贯曾经被创立。
//这是以下面的图和代码为根底的造成的连贯关系Connection: { originModule: A, // 起源模块 destinationModule: B, // 指标模块 dependency: ImportDependency // 依赖关系}
而上述连贯对象将被增加到A.outgoingConnections
集和和B.incomingConnections
集和中。
所有从entry
中创立的模块树都将向同一个繁多的中央,即向ModuleGraph
输入有意义的信息。这是因为所有这些模块树最终都将与空模块(ModuleGraph的根模块)相连。与空模块的连贯是通过 EntryDependency
和从entry
文件中创立的模块建设的。
空模块与每个模块树的根模块有一个连贯,该模块由entry
对象中的一个我的项目生成。图中的每条边都代表2个模块之间的连贯,每个连贯都有对于源节点、指标节点和依赖关系的信息。
4. 构建ModuleGraph
ModuleGraph
从一个空模块开始,其直系子孙是模块树的根模块,这些模块是由entry
对象项构建的
首批创立的模块
咱们从一个简略的entry
对象开始。
entry: { a: './a.js',}
正如咱们上文说到,在某些时候,咱们最终会失去一个 EntryDependency
,其申请是./a.js
。这个 EntryDependency
提供了一种从该申请创立有意义的货色的办法,因为它映射到一个模块工厂,即 NormalModuleFactory
。
这个过程的下一步是 NormalModuleFactory
发挥作用的中央。NormalModuleFactory
,如果它胜利地实现了它的工作,将创立一个NormalModule。
NormalModule只是一个文件源代码的反序列化版本,它只不过是一个原始字符串。
一个原始的字符串不会带来太多的价值,所以webpack不能用它做什么。一个NormalModule会将源代码存储为一个字符串,然而,与此同时,它也会蕴含其余有意义的信息和性能,比方:
- 利用于它的加载器
- 构建模块的逻辑
- 生成运行时代码的逻辑
- 它的哈希值等等
换句话说,从 webpack
的角度来看,NormalModule
是一个简略原始文件的有用版本。
为了让 NormalModuleFactory
输入一个 NormalModule
,它必须要通过一些步骤。在模块被创立后,还有一些事件要做,比方构建模块和解决其依赖关系(如果有的话)。
NormalModuleFactory
通过调用create()
办法开始。而后,解析过程开始。在这里,申请(文件的门路)被解析,以及该类型文件的加载器。
留神,只有加载器的文件门路将被确定,加载器在这一步还没有被调用。
module 的构建过程
在所有必要的文件门路被解决后,NormalModule被创立。然而,在这步,模块的价值不大。很多相干的信息将在模块建设后呈现。NormalModule
的构建过程还包含一些其余步骤。
- 首先,
loader
将在原始源代码上被调用;如果有多个加载器,那么一个加载器的输入可能是另一个加载器的输出(配置文件中提供加载器的程序很重要)。 - 其次,通过所有加载器运行后失去的字符串将被
acorn
(JavaScript
解析器)解析,失去给定文件的AST
。 最初,
AST
将被剖析;- 在这个阶段,以后模块的依赖关系(如其余模块)将被确定,
webpack
能够检测其它的性能(如require.context
,module.hot
)等; AST
剖析产生在JavascriptParser
中, 这个过程的一部分是最重要的,因为打包过程中接下来的很多事件都取决于这个局部。
- 在这个阶段,以后模块的依赖关系(如其余模块)将被确定,
通过剖析AST发现依赖关系
其中 moduleInstance
是指从index.js
文件中创立的 NormalModule
。红色的dep
是指从第一个import
语句中创立的依赖关系,蓝色的dep
是指第二个import
语句。
当初AST
曾经被查看过了,到建设模块树的过程了。下一步是解决在上一步发现的依赖关系。依照上图,index
模块有两个依赖关系,也是模块,即math.js
和utils.js
。但在这些依赖关系成为理论的模块之前,咱们只有index模块,为了把它们变成模块,咱们须要应用这些依赖关系映射到的ModuleFactory
,并反复下面形容的步骤(本节结尾的图中的虚线箭头示意反复)。在解决完以后模块的依赖关系后,这些依赖关系也可能有依赖关系,这个过程始终继续到没有更多的依赖关系。这就是模块树的建设过程,当然也要确保父模块和子模块之间的连贯被正确设置。
让咱们实现一个自定义插件的办法,它将使咱们可能遍历 ModuleGraph
。上面是形容模块如何相互依赖的图。
简略的做一个介绍:a.js
文件导入b.js
文件,b.js
文件同时导入b1.js
和c.js
,而后c.js
导入c1.js
和d.js
,最初,d.js
导入d1.js
。ROOT
指的是null
模块,它是 ModuleGraph
的根。
入口选项只包含一个值,即a.js
。
// webpack.config.jsconst config = { entry: path.resolve(__dirname, './src/a.js'), /* ... */};
当初让咱们看看咱们的自定义插件会是什么样子
class UnderstandingModuleGraphPlugin { apply(compiler) { const className = this.constructor.name; compiler.hooks.compilation.tap(className, (compilation) => { compilation.hooks.finishModules.tap(className, (modules) => { // 检索 Map const { moduleGraph: { _moduleMap: moduleMap }, } = compilation; // 以DFS的形式遍历模块图 const dfs = () => { // "模块图 "的根模块是*空模块*。 const root = null; const visited = new Map(); const traverse = (crtNode) => { if (visited.get(crtNode)) { return; } visited.set(crtNode, true); console.log( crtNode?.resource ? path.basename(crtNode?.resource) : 'ROOT' ); // 取得相干的`ModuleGraphModule`,它只有一些 //除了`NormalModule'之外的额定属性,咱们能够用它来进一步遍历图形。 const correspondingGraphModule = moduleMap.get(crtNode); // 通过指定字段构建`children`信息 const children = new Set( Array.from( correspondingGraphModule.outgoingConnections || [], (c) => c.module ) ); for (const c of children) { traverse(c); } }; // 从root节点开始遍历 traverse(root); }; dfs(); }); }); }}
对代码最一个简略的解释
在现有的
webpack hooks
中增加逻辑的形式是应用tap
办法- 函数签名为
tap(string, callback)
- 其中
string
次要是为了调试的目标,示意自定义逻辑是由哪个起源增加的。 callback
的参数取决于咱们要增加自定义性能的hook
- 函数签名为
在
compilation
对象上:它蕴含大部分打包过程状态- 模块图(module graph)
- 创立的
chunks
- 创立的
modules
- 生成的
assets
- 以及更多的信息
finishModules
是在所有模块(包含它们的依赖关系和依赖关系的依赖关系等等)构建结束后才被被调用modules
是一个蕴含所有已建模块的汇合- 一个
NormalModule
是由NormalModuleFactory
产生的
- 一个
模块map(
Map<Module, ModuleGraphModule>
)- 它蕴含了咱们须要的所有信息,以便遍历图
- 以
DFS
的形式遍历模块图 ModuleGraphModule
,它只有一些除了`NormalModule'之外的额定属性Connection
的字段信息Connection
的originModule
是箭头开始的中央。Connection
的module
是箭头的起点。所以,
Connection
的module
是一个子节点。
correspondingGraphModule.outgoingConnections
是一个Set
或者undefined
(在节点没有子节点的状况下)。- 应用
new Set
是因为一个模块能够通过多个连贯援用同一个模块。 例如,一个
import foo from 'file.js'
将导致2个连贯:- 一个是简略的导入
- 一个用于`foo'默认指定器
- 应用
依据模块的层次结构,失去如下的输入。
a.jsb.jsb1.jsc.jsc1.jsd.jsd1.js
5. Module/Chunk/ ChunkGroup /EntryPoint 是个啥
Module
前文其实曾经解释过了,这里再做一次总结哇。
模块是一个文件的升级版。
一个模块,一旦创立和构建,除了源代码,还蕴含很多有意义的信息,如:
- 应用的加载器
- 它的依赖关系
- 它的进口(如果有的话)
- 它的哈希值
Chunk
一个Chunk封装了一个或多个模块
个别状况下,entry
文件(一个entry
文件=entry
对象的一个我的项目)的数量与所产生的Chunk
的数量成正比。
- 因为
entry
对象可能只有一个我的项目,而后果块的数量可能大于1。确实,对于每一个entry
我的项目,在dist
目录中都会有一个相应的chunk
但也可能是隐式创立其余的
chunk
,例如在应用import()
函数时。但不论它是如何被创立的,每个chunk
在dist
目录下都会有一个对应的文件。
ChunkGroup
一个ChunkGroup蕴含一个或多个chunks
一个ChunkGroup
能够是另一个ChunkGroup
的父或子。
- 例如,当应用动静导入时,每应用一个
import()
函数,就会有一个ChunkGroup被创立,它的父级是一个现有的ChunkGroup
,即包含应用import()
函数的文件(即模块)的那个。
EntryPoint
EntryPoint是ChunkGroup的一种类型,它是为entry
对象中的每一个我的项目创立的。
构建 ChunkGraph
从整体的流程图上看,ModuleGraph
只是打包过程中的一个必要局部。为了使代码宰割等性能成为可能,它必须被利用起来。
在打包过程的这一点上,对于entry
对象中的每个我的项目,都会有一个 EntryPoint
。 因为它是 ChunkGroup
的一种类型,它至多会蕴含一个chunk。所以,如果entry
对象有3个我的项目,就会有3个 EntryPoint实例
,每个实例都有一个chunk,也叫Entrypoint chunk
,其名称是entry
我的项目key
的值。与entry
文件相关联的模块被称为entry
模块,它们每个都将属于它们的入口块。它们之所以重要是因为它们是 ChunkGraph
构建过程的终点。请留神,一个chunk能够有多个入口模块。
// webpack.config.jsentry: { foo: ['./a.js', './b.js'],},
在下面的例子中,有一个名为foo
(entry
的key
)的 chunk
将有2个入口模块:一个与a.js文件相干,另一个与b.js文件相干。当然,该chunk
将属于依据entry
我的项目创立的 EntryPoint实例
。
在具体探讨之前,让咱们先列出一个例子,在此基础上探讨构建过程。
entry: { foo: [path.join(__dirname, 'src', 'a.js'), path.join(__dirname, 'src', 'a1.js')], bar: path.join(__dirname, 'src', 'c.js'), },
这个例子将包含后面提到的货色:ChunkGroups
的父子关系、chunks
和 EntryPoints
。
ChunkGraph
是以递归的形式建设的。它首先将所有的entry
模块增加到一个队列中。而后,当一个entry
模块被解决时,意味着其依赖关系(也是模块)将被查看,每个依赖关系也将被增加到队列中。这样始终反复上来,直到队列变空。这个过程的这一部分是模块被拜访的中央。然而,这只是第一局部。
ChunkGroups
能够是其余 ChunkGroups
的父/子。这些分割在第二局部失去解决。例如,如前所述,一个动静导入(即import()
函数)会产生一个新的子ChunkGroup。在webpack的说法中,import()
表达式定义了一个异步的依赖关系块。
当初,让咱们先看看从上述配置中创立的ChunkGraph
的图表。
从上图中,能够看到有4个 chunk
,所以会有4个输入文件。foo chunk
将有4个模块,其中2个是entry
模块。bar chunk
将只有一个entry
模块,另一个能够被认为是一般模块。每个import()表达式都会产生一个新的ChunkGroup(其父级是bar EntryPoint
)。
产生的文件的内容是依据 ChunkGraph
来决定的,所以这就是为什么它对整个打包过程十分重要。
与 ModuleGraph
相似,属于 ChunkGraph
的节点被称为 ChunkGraphChunk
,它只是一个装璜过的chunk,他们之间的关系如下:WeakMap<Chunk, ChunkGraphChunk>
。
6. 提交chunk资源
所产生的文件并不是原始文件的正本,因为为了实现其性能,webpack
须要增加一些自定义代码,使所有都按预期工作。
这就引出了一个问题:webpack如何晓得要生成什么代码?
这所有都从最根本的局部开始:<span style="font-weight:800;color:#FFA500;font-size:18px">{模块| module}</span>。一个模块能够导出成员,导入其余成员,应用import()
导入,应用webpack特定的函数(例如require.resolve
)等等。
依据模块的源代码,webpack能够决定生成哪些代码以实现所需的性能。并且在剖析AST
时发现对应模块的依赖关系。
例如,从./foo
导入 { aFunction }
将导致两个依赖关系(一个是导入语句自身,另一个是指定器,即 aFunction
),从中将创立一个模块。
另一个例子是 import()
函数。这将导致一个异步的依赖关系块,其中一个依赖关系是 ImportDependency
,它是动静导入所特有的。
这些依赖关系是必不可少的,因为它们带有一些对于应该生成什么代码的提醒。例如,ImportDependency
确切地晓得要通知 webpack
一些信息,以便异步地获取导入的模块并应用其导出的成员。这些提醒能够被称为运行时申请。
总而言之,一个模块会有它的运行工夫要求,这取决于该模块在其源代码中应用的内容。当初,webpack晓得了一个chunk
的所有需要,它将可能正确地生成运行时代码。
这也被称为渲染过程,渲染过程在很大水平上依赖于 ChunkGraph
,因为它蕴含Chunk组(即 ChunkGroup
,EntryPoint
),这些组蕴含Chunks,这些Chunks蕴含模块,这些模块以一种细化的形式蕴含无关webpack将生成的运行时代码的信息和提醒。
后记
分享是一种态度。
参考资料:
- webpacks-bundling-process
- 效率工程化
全文完,既然看到这里了,如果感觉不错,顺手点个赞和“在看”吧。
本文由mdnice多平台公布