webpack是現代前端開發中最火的模塊打包工具,只須要經過簡單的配置,即可以完成模塊的加載和打包。那它是怎麼作到經過對一些插件的配置,即可以輕鬆實現對代碼的構建呢?javascript
const path = require('path');
module.exports = {
entry: "./app/entry", // string | object | array
// Webpack打包的入口
output: { // 定義webpack如何輸出的選項
path: path.resolve(__dirname, "dist"), // string
// 全部輸出文件的目標路徑
filename: "[chunkhash].js", // string
// 「入口(entry chunk)」文件命名模版
publicPath: "/assets/", // string
// 構建文件的輸出目錄
/* 其它高級配置 */
},
module: { // 模塊相關配置
rules: [ // 配置模塊loaders,解析規則
{
test: /\.jsx?$/, // RegExp | string
include: [ // 和test同樣,必須匹配選項
path.resolve(__dirname, "app")
],
exclude: [ // 必不匹配選項(優先級高於test和include)
path.resolve(__dirname, "app/demo-files")
],
loader: "babel-loader", // 模塊上下文解析
options: { // loader的可選項
presets: ["es2015"]
},
},
},
resolve: { // 解析模塊的可選項
modules: [ // 模塊的查找目錄
"node_modules",
path.resolve(__dirname, "app")
],
extensions: [".js", ".json", ".jsx", ".css"], // 用到的文件的擴展
alias: { // 模塊別名列表
"module": "new-module"
},
},
devtool: "source-map", // enum
// 爲瀏覽器開發者工具添加元數據加強調試
plugins: [ // 附加插件列表
// ...
],
}
複製代碼
從上面咱們能夠看到,webpack配置中須要理解幾個核心的概念Entry
、Output
、Loaders
、Plugins
、 Chunk
css
Loaders
將各種型的文件處理成webpack可以處理的模塊,plugins
有着很強的能力。插件的範圍包括,從打包優化和壓縮,一直到從新定義環境中的變量。但也是最複雜的一個。好比對js文件進行壓縮優化的UglifyJsPlugin
插件CommonsChunkPlugin
將一些公共代碼分割成一個chunk,實現單獨加載。在webpack4 中CommonsChunkPlugin
被廢棄,使用SplitChunksPlugin
讀到這裏,或許你對webpack有一個大概的瞭解,那webpack 是怎麼運行的呢?咱們都知道,webpack是高度複雜抽象的插件集合,理解webpack的運行機制,對於咱們平常定位構建錯誤以及寫一些插件處理構建任務有很大的幫助。前端
webpack本質上是一種事件流的機制,它的工做流程就是將各個插件串聯起來,而實現這一切的核心就是Tapable,webpack中最核心的負責編譯的Compiler
和負責建立bundles的Compilation
都是Tapable的實例。在Tapable1.0以前,也就是webpack3及其之前使用的Tapable,提供了包括java
plugin(name:string, handler:function)
註冊插件到Tapable對象中apply(…pluginInstances: (AnyPlugin|function)[])
調用插件的定義,將事件監聽器註冊到Tapable實例註冊表中applyPlugins*(name:string, …)
多種策略細緻地控制事件的觸發,包括applyPluginsAsync
、applyPluginsParallel
等方法實現對事件觸發的控制,實現(1)多個事件連續順序執行 (2)並行執行 (3)異步執行 (4)一個接一個地執行插件,前面的輸出是後一個插件的輸入的瀑布流執行順序 (5)在容許時中止執行插件,即某個插件返回了一個undefined
的值,即退出執行 咱們能夠看到,Tapable就像nodejs中EventEmitter
,提供對事件的註冊on
和觸發emit
,理解它很重要,看個栗子:好比咱們來寫一個插件node
function CustomPlugin() {}
CustomPlugin.prototype.apply = function(compiler) {
compiler.plugin('emit', pluginFunction);
}
複製代碼
在webpack的生命週期中會適時的執行webpack
this.apply*("emit",options)
複製代碼
固然上面提到的Tapable都是1.0版本以前的,若是想深刻學習,能夠查看Tapable 和 事件流 那1.0的Tapable又是什麼樣的呢?1.0版本發生了巨大的改變,再也不是此前的經過plugin
註冊事件,經過applyPlugins*
觸發事件調用,那1.0的Tapable是什麼呢?git
暴露出不少的鉤子,可使用它們爲插件建立鉤子函數github
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
複製代碼
咱們來看看 怎麼使用。web
class Order {
constructor() {
this.hooks = { //hooks
goods: new SyncHook(['goodsId', 'number']),
consumer: new AsyncParallelHook(['userId', 'orderId'])
}
}
queryGoods(goodsId, number) {
this.hooks.goods.call(goodsId, number);
}
consumerInfoPromise(userId, orderId) {
this.hooks.consumer.promise(userId, orderId).then(() => {
//TODO
})
}
consumerInfoAsync(userId, orderId) {
this.hooks.consumer.callAsync(userId, orderId, (err, data) => {
//TODO
})
}
}
複製代碼
對於全部的hook的構造函數均接受一個可選的string類型的數組shell
const hook = new SyncHook(["arg1", "arg2", "arg3"]);
複製代碼
// 調用tap方法註冊一個consument
order.hooks.goods.tap('QueryPlugin', (goodsId, number) => {
return fetchGoods(goodsId, number);
})
// 再添加一個
order.hooks.goods.tap('LoggerPlugin', (goodsId, number) => {
logger(goodsId, number);
})
// 調用
order.queryGoods('10000000', 1)
複製代碼
對於一個 SyncHook
,咱們經過tap
來添加消費者,經過call
來觸發鉤子的順序執行。
對於一個非sync*
類型的鉤子,即async*
類型的鉤子,咱們還能夠經過其它方式註冊消費者和調用
// 註冊一個sync 鉤子
order.hooks.consumer.tap('LoggerPlugin', (userId, orderId) => {
logger(userId, orderId);
})
order.hooks.consumer.tapAsync('LoginCheckPlugin', (userId, orderId, callback) => {
LoginCheck(userId, callback);
})
order.hooks.consumer.tapPromise('PayPlugin', (userId, orderId) => {
return Promise.resolve();
})
// 調用
// 返回Promise
order.consumerInfoPromise('user007', '1024');
//回調函數
order.consumerInfoAsync('user007', '1024')
複製代碼
經過上面的栗子,你可能已經大體瞭解了Tapable
的用法,它的用法
interception
Sync*
類型的鉤子來講。
tap
註冊,不能使用tapPromise
和tapAsync
註冊// 全部的鉤子都繼承於Hook
class Sync* extends Hook {
tapAsync() { // Sync*類型的鉤子不支持tapAsync
throw new Error("tapAsync is not supported on a Sync*");
}
tapPromise() {// Sync*類型的鉤子不支持tapPromise
throw new Error("tapPromise is not supported on a Sync*");
}
compile(options) { // 編譯代碼來按照必定的策略執行Plugin
factory.setup(this, options);
return factory.create(options);
}
}
複製代碼
對於Async*
類型鉤子
tap
、tapPromise
、tapAsync
註冊class AsyncParallelHook extends Hook {
constructor(args) {
super(args);
this.call = this._call = undefined;
}
compile(options) {
factory.setup(this, options);
return factory.create(options);
}
}
複製代碼
class Hook {
constructor(args) {
if(!Array.isArray(args)) args = [];
this._args = args; // 實例鉤子的時候的string類型的數組
this.taps = []; // 消費者
this.interceptors = []; // interceptors
this.call = this._call = // 以sync類型方式來調用鉤子
this._createCompileDelegate("call", "sync");
this.promise =
this._promise = // 以promise方式
this._createCompileDelegate("promise", "promise");
this.callAsync =
this._callAsync = // 以async類型方式來調用
this._createCompileDelegate("callAsync", "async");
this._x = undefined; //
}
_createCall(type) {
return this.compile({
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: type
});
}
_createCompileDelegate(name, type) {
const lazyCompileHook = (...args) => {
this[name] = this._createCall(type);
return this[name](...args);
};
return lazyCompileHook;
}
// 調用tap 類型註冊
tap(options, fn) {
// ...
options = Object.assign({ type: "sync", fn: fn }, options);
// ...
this._insert(options); // 添加到 this.taps中
}
// 註冊 async類型的鉤子
tapAsync(options, fn) {
// ...
options = Object.assign({ type: "async", fn: fn }, options);
// ...
this._insert(options); // 添加到 this.taps中
}
註冊 promise類型鉤子
tapPromise(options, fn) {
// ...
options = Object.assign({ type: "promise", fn: fn }, options);
// ...
this._insert(options); // 添加到 this.taps中
}
}
複製代碼
每次都是調用tap
、tapSync
、tapPromise
註冊不一樣類型的插件鉤子,經過調用call
、callAsync
、promise
方式調用。其實調用的時候爲了按照必定的執行策略執行,調用compile
方法快速編譯出一個方法來執行這些插件。
const factory = new Sync*CodeFactory();
class Sync* extends Hook {
// ...
compile(options) { // 編譯代碼來按照必定的策略執行Plugin
factory.setup(this, options);
return factory.create(options);
}
}
class Sync*CodeFactory extends HookCodeFactory {
content({ onError, onResult, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible
});
}
}
複製代碼
compile
中調用HookCodeFactory#create
方法編譯生成執行代碼。
class HookCodeFactory {
constructor(config) {
this.config = config;
this.options = undefined;
}
create(options) {
this.init(options);
switch(this.options.type) {
case "sync": // 編譯生成sync, 結果直接返回
return new Function(this.args(),
"\"use strict\";\n" + this.header() + this.content({
// ...
onResult: result => `return ${result};\n`,
// ...
}));
case "async": // async類型, 異步執行,最後將調用插件執行結果來調用callback,
return new Function(this.args({
after: "_callback"
}), "\"use strict\";\n" + this.header() + this.content({
// ...
onResult: result => `_callback(null, ${result});\n`,
onDone: () => "_callback();\n"
}));
case "promise": // 返回promise類型,將結果放在resolve中
// ...
code += "return new Promise((_resolve, _reject) => {\n";
code += "var _sync = true;\n";
code += this.header();
code += this.content({
// ...
onResult: result => `_resolve(${result});\n`,
onDone: () => "_resolve();\n"
});
// ...
return new Function(this.args(), code);
}
}
// callTap 就是執行一些插件,並將結果返回
callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) {
let code = "";
let hasTapCached = false;
// ...
code += `var _fn${tapIndex} = ${this.getTapFn(tapIndex)};\n`;
const tap = this.options.taps[tapIndex];
switch(tap.type) {
case "sync":
// ...
if(onResult) {
code += `var _result${tapIndex} = _fn${tapIndex}(${this.args({ before: tap.context ? "_context" : undefined })});\n`;
} else {
code += `_fn${tapIndex}(${this.args({ before: tap.context ? "_context" : undefined })});\n`;
}
if(onResult) { // 結果透傳
code += onResult(`_result${tapIndex}`);
}
if(onDone) { // 通知插件執行完畢,能夠執行下一個插件
code += onDone();
}
break;
case "async": //異步執行,插件運行完後再將結果經過執行callback透傳
let cbCode = "";
if(onResult)
cbCode += `(_err${tapIndex}, _result${tapIndex}) => {\n`;
else
cbCode += `_err${tapIndex} => {\n`;
cbCode += `if(_err${tapIndex}) {\n`;
cbCode += onError(`_err${tapIndex}`);
cbCode += "} else {\n";
if(onResult) {
cbCode += onResult(`_result${tapIndex}`);
}
cbCode += "}\n";
cbCode += "}";
code += `_fn${tapIndex}(${this.args({ before: tap.context ? "_context" : undefined, after: cbCode //cbCode將結果透傳 })});\n`;
break;
case "promise": // _fn${tapIndex} 就是第tapIndex 個插件,它必須是個Promise類型的插件
code += `var _hasResult${tapIndex} = false;\n`;
code += `_fn${tapIndex}(${this.args({ before: tap.context ? "_context" : undefined })}).then(_result${tapIndex} => {\n`;
code += `_hasResult${tapIndex} = true;\n`;
if(onResult) {
code += onResult(`_result${tapIndex}`);
}
// ...
break;
}
return code;
}
// 按照插件的註冊順序,按照順序遞歸調用執行插件
callTapsSeries({ onError, onResult, onDone, rethrowIfPossible }) {
// ...
const firstAsync = this.options.taps.findIndex(t => t.type !== "sync");
const next = i => {
// ...
const done = () => next(i + 1);
// ...
return this.callTap(i, {
// ...
onResult: onResult && ((result) => {
return onResult(i, result, done, doneBreak);
}),
// ...
});
};
return next(0);
}
callTapsLooping({ onError, onDone, rethrowIfPossible }) {
const syncOnly = this.options.taps.every(t => t.type === "sync");
let code = "";
if(!syncOnly) {
code += "var _looper = () => {\n";
code += "var _loopAsync = false;\n";
}
code += "var _loop;\n";
code += "do {\n";
code += "_loop = false;\n";
// ...
code += this.callTapsSeries({
// ...
onResult: (i, result, next, doneBreak) => { // 一旦某個插件返回不爲undefined, 即一隻調用某個插件執行,若是爲undefined,開始調用下一個
let code = "";
code += `if(${result} !== undefined) {\n`;
code += "_loop = true;\n";
if(!syncOnly)
code += "if(_loopAsync) _looper();\n";
code += doneBreak(true);
code += `} else {\n`;
code += next();
code += `}\n`;
return code;
},
// ...
})
code += "} while(_loop);\n";
// ...
return code;
}
// 並行調用插件執行
callTapsParallel({ onError, onResult, onDone, rethrowIfPossible, onTap = (i, run) => run() }) {
// ...
// 遍歷註冊都全部插件,並調用
for(let i = 0; i < this.options.taps.length; i++) {
// ...
code += "if(_counter <= 0) break;\n";
code += onTap(i, () => this.callTap(i, {
// ...
onResult: onResult && ((result) => {
let code = "";
code += "if(_counter > 0) {\n";
code += onResult(i, result, done, doneBreak);
code += "}\n";
return code;
}),
// ...
}), done, doneBreak);
}
// ...
return code;
}
}
複製代碼
在HookCodeFactory#create
中調用到content
方法,此方法將按照此鉤子的執行策略,調用不一樣的方法來執行編譯 生成最終的代碼。
SyncHook中調用`callTapsSeries`編譯生成最終執行插件的函數,`callTapsSeries`作的就是將插件列表中插件按照註冊順序遍歷執行。
複製代碼
class SyncHookCodeFactory extends HookCodeFactory {
content({ onError, onResult, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible
});
}
}
複製代碼
undefined
便結束執行列表中的插件class SyncBailHookCodeFactory extends HookCodeFactory {
content({ onError, onResult, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
// ...
onResult: (i, result, next) => `if(${result} !== undefined) {\n${onResult(result)};\n} else {\n${next()}}\n`,
// ...
});
}
}
複製代碼
class SyncWaterfallHookCodeFactory extends HookCodeFactory {
content({ onError, onResult, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
// ...
onResult: (i, result, next) => {
let code = "";
code += `if(${result} !== undefined) {\n`;
code += `${this._args[0]} = ${result};\n`;
code += `}\n`;
code += next();
return code;
},
onDone: () => onResult(this._args[0]),
});
}
}
複製代碼
callTapsParallel
並行執行插件class AsyncParallelHookCodeFactory extends HookCodeFactory {
content({ onError, onDone }) {
return this.callTapsParallel({
onError: (i, err, done, doneBreak) => onError(err) + doneBreak(true),
onDone
});
}
}
複製代碼
本文關於webpack 的流程講解是基於webpack4的。
從webpack項目的package.json文件中咱們找到了入口執行函數,在函數中引入webpack,那麼入口將會是lib/webpack.js
,而若是在shell中執行,那麼將會走到./bin/webpack.js
,咱們就以lib/webpack.js
爲入口開始吧!
{
"name": "webpack",
"version": "4.1.1",
...
"main": "lib/webpack.js",
"web": "lib/webpack.web.js",
"bin": "./bin/webpack.js",
...
}
複製代碼
const webpack = (options, callback) => {
// ...
// 驗證options正確性
// 預處理options
options = new WebpackOptionsDefaulter().process(options); // webpack4的默認配置
compiler = new Compiler(options.context); // 實例Compiler
// ...
// 若options.watch === true && callback 則開啓watch線程
compiler.watch(watchOptions, callback);
compiler.run(callback);
return compiler;
};
複製代碼
webpack 的入口文件其實就實例了Compiler
並調用了run
方法開啓了編譯,webpack的編譯都按照下面的鉤子調用順序執行。
webpack中負責構建和編譯都是Compilation
class Compilation extends Tapable {
constructor(compiler) {
super();
this.hooks = {
// hooks
};
// ...
this.compiler = compiler;
// ...
// template
this.mainTemplate = new MainTemplate(this.outputOptions);
this.chunkTemplate = new ChunkTemplate(this.outputOptions);
this.hotUpdateChunkTemplate = new HotUpdateChunkTemplate(
this.outputOptions
);
this.runtimeTemplate = new RuntimeTemplate(
this.outputOptions,
this.requestShortener
);
this.moduleTemplates = {
javascript: new ModuleTemplate(this.runtimeTemplate),
webassembly: new ModuleTemplate(this.runtimeTemplate)
};
// 構建生成的資源
this.chunks = [];
this.chunkGroups = [];
this.modules = [];
this.additionalChunkAssets = [];
this.assets = {};
this.children = [];
// ...
}
//
buildModule(module, optional, origin, dependencies, thisCallback) {
// ...
// 調用module.build方法進行編譯代碼,build中 實際上是利用acorn編譯生成AST
this.hooks.buildModule.call(module);
module.build(/**param*/);
}
// 將模塊添加到列表中,並編譯模塊
_addModuleChain(context, dependency, onModule, callback) {
// ...
// moduleFactory.create建立模塊,這裏會先利用loader處理文件,而後生成模塊對象
moduleFactory.create(
{
contextInfo: {
issuer: "",
compiler: this.compiler.name
},
context: context,
dependencies: [dependency]
},
(err, module) => {
const addModuleResult = this.addModule(module);
module = addModuleResult.module;
onModule(module);
dependency.module = module;
// ...
// 調用buildModule編譯模塊
this.buildModule(module, false, null, null, err => {});
}
});
}
// 添加入口模塊,開始編譯&構建
addEntry(context, entry, name, callback) {
// ...
this._addModuleChain( // 調用_addModuleChain添加模塊
context,
entry,
module => {
this.entries.push(module);
},
// ...
);
}
seal(callback) {
this.hooks.seal.call();
// ...
const chunk = this.addChunk(name);
const entrypoint = new Entrypoint(name);
entrypoint.setRuntimeChunk(chunk);
entrypoint.addOrigin(null, name, preparedEntrypoint.request);
this.namedChunkGroups.set(name, entrypoint);
this.entrypoints.set(name, entrypoint);
this.chunkGroups.push(entrypoint);
GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk);
GraphHelpers.connectChunkAndModule(chunk, module);
chunk.entryModule = module;
chunk.name = name;
// ...
this.hooks.beforeHash.call();
this.createHash();
this.hooks.afterHash.call();
this.hooks.beforeModuleAssets.call();
this.createModuleAssets();
if (this.hooks.shouldGenerateChunkAssets.call() !== false) {
this.hooks.beforeChunkAssets.call();
this.createChunkAssets();
}
// ...
}
createHash() {
// ...
}
// 生成 assets 資源並 保存到 Compilation.assets 中 給webpack寫插件的時候會用到
createModuleAssets() {
for (let i = 0; i < this.modules.length; i++) {
const module = this.modules[i];
if (module.buildInfo.assets) {
for (const assetName of Object.keys(module.buildInfo.assets)) {
const fileName = this.getPath(assetName);
this.assets[fileName] = module.buildInfo.assets[assetName];
this.hooks.moduleAsset.call(module, fileName);
}
}
}
}
createChunkAssets() {
// ...
}
}
複製代碼
在webpack make
鉤子中, tapAsync
註冊了一個DllEntryPlugin
, 就是將入口模塊經過調用compilation.addEntry
方法將全部的入口模塊添加到編譯構建隊列中,開啓編譯流程。
compiler.hooks.make.tapAsync("DllEntryPlugin", (compilation, callback) => {
compilation.addEntry(
this.context,
new DllEntryDependency(
this.entries.map((e, idx) => {
const dep = new SingleEntryDependency(e);
dep.loc = `${this.name}:${idx}`;
return dep;
}),
this.name
),
// ...
);
});
複製代碼
隨後在addEntry
中調用_addModuleChain
開始編譯。在_addModuleChain
首先會生成模塊,最後構建。
class NormalModuleFactory extends Tapable {
// ...
create(data, callback) {
// ...
this.hooks.beforeResolve.callAsync(
{
contextInfo,
resolveOptions,
context,
request,
dependencies
},
(err, result) => {
if (err) return callback(err);
// Ignored
if (!result) return callback();
// factory 鉤子會觸發 resolver 鉤子執行,而resolver鉤子中會利用acorn 處理js生成AST,再利用acorn處理前,會使用loader加載文件
const factory = this.hooks.factory.call(null);
factory(result, (err, module) => {
if (err) return callback(err);
if (module && this.cachePredicate(module)) {
for (const d of dependencies) {
d.__NormalModuleFactoryCache = module;
}
}
callback(null, module);
});
}
);
}
}
複製代碼
在編譯完成後,調用compilation.seal
方法封閉,生成資源,這些資源保存在compilation.assets
, compilation.chunk
, 在給webpack寫插件的時候會用到
class Compiler extends Tapable {
constructor(context) {
super();
this.hooks = {
beforeRun: new AsyncSeriesHook(["compilation"]),
run: new AsyncSeriesHook(["compilation"]),
emit: new AsyncSeriesHook(["compilation"]),
afterEmit: new AsyncSeriesHook(["compilation"]),
compilation: new SyncHook(["compilation", "params"]),
beforeCompile: new AsyncSeriesHook(["params"]),
compile: new SyncHook(["params"]),
make: new AsyncParallelHook(["compilation"]),
afterCompile: new AsyncSeriesHook(["compilation"]),
// other hooks
};
// ...
}
run(callback) {
const startTime = Date.now();
const onCompiled = (err, compilation) => {
// ...
this.emitAssets(compilation, err => {
if (err) return callback(err);
if (compilation.hooks.needAdditionalPass.call()) {
compilation.needAdditionalPass = true;
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
this.hooks.done.callAsync(stats, err => {
if (err) return callback(err);
this.hooks.additionalPass.callAsync(err => {
if (err) return callback(err);
this.compile(onCompiled);
});
});
return;
}
// ...
});
};
this.hooks.beforeRun.callAsync(this, err => {
if (err) return callback(err);
this.hooks.run.callAsync(this, err => {
if (err) return callback(err);
this.readRecords(err => {
if (err) return callback(err);
this.compile(onCompiled);
});
});
});
}
// 輸出文件到構建目錄
emitAssets(compilation, callback) {
// ...
this.hooks.emit.callAsync(compilation, err => {
if (err) return callback(err);
outputPath = compilation.getPath(this.outputPath);
this.outputFileSystem.mkdirp(outputPath, emitFiles);
});
}
newCompilationParams() {
const params = {
normalModuleFactory: this.createNormalModuleFactory(),
contextModuleFactory: this.createContextModuleFactory(),
compilationDependencies: new Set()
};
return params;
}
compile(callback) {
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
if (err) return callback(err);
this.hooks.compile.call(params);
const compilation = this.newCompilation(params);
this.hooks.make.callAsync(compilation, err => {
if (err) return callback(err);
compilation.finish();
// make 鉤子執行後,調用seal生成資源
compilation.seal(err => {
if (err) return callback(err);
this.hooks.afterCompile.callAsync(compilation, err => {
if (err) return callback(err);
// emit, 生成最終文件
return callback(null, compilation);
});
});
});
});
}
}
複製代碼
在seal
執行後,便會調用emit
鉤子,根據webpack config文件的output配置的path屬性,將文件輸出到指定的path.
《IVWEB 技術週刊》 震撼上線了,關注公衆號:IVWEB社區,每週定時推送優質文章。