<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          前端推薦!玩轉(zhuǎn)Webpack共需幾步?

          共 17195字,需瀏覽 35分鐘

           ·

          2022-02-11 14:34


          導語?|?本文主要介紹webpack的打包流程,及其插件系統(tǒng)Tabable,并手寫了一下簡易打包器。通過這篇文章讀者可以了解webpack的具體實現(xiàn)過程,并且自己也可以理解其打包原理,有利于更好的使用這些工具。


          一、開始


          Webpack打包原理是從入口文件開始分析AST,遞歸收集依賴,然后生成最終的code。Webpack的插件是貫穿始終的,其插件系統(tǒng)借助了Tapable,Tapable也是Webpack團隊開發(fā)的,其本質(zhì)是一種發(fā)布訂閱模式。


          深入理解插件對于深入理解Webpack非常重要。想一下,任何復雜的邏輯都可以抽象成一個插件,在相應的生命周期階段去觸發(fā)即可。


          下面先介紹下插件用到的系統(tǒng)Tapable。



          二、Tapable


          (一)基礎鉤子


          Tapable是獨立發(fā)布的,也就是其可以搭配任何庫。本次分析的是V2.2.1版本。


          下面是Tapable的一些常見用法:


          const { Synchook, AsyncParallelHook } = require('tapable')
          class Car { constructor() { this.hooks = { // 創(chuàng)建新的鉤子 accelerate: new Synchook(['newSpeed']), brake: new SynHook(), calcRoutes: new AsyncParallelHook(['source', 'target', 'routeList']) } }}


          上面在Car類定義的時候定義了實例的hooks屬性,它可以包含多個不同的hook。下面為這些hook增加訂閱者。


          const car = new Car();
          // 為brake鉤子增加訂閱者,通常為插件,第一個參數(shù)為插件名稱,第二個參數(shù)為回調(diào)函數(shù)。car.hooks.brake.tap('WarningLampPlugin', () => warningLamp.on())// 為accelerate鉤子增加訂閱者car.hooks.accelerate.tap('LoggerPlugin', (newSpeed) => console.log(`現(xiàn)在車速為:${newSpeed}`))

          ??

          SynHook代表是同步鉤子,其只能用tap增加訂閱者。相應的,還有異步鉤子,支持異步插件。


          // 使用tapPromise添加插件car.hooks.calcRoutes.tapPromise('GoogleMapsPlugin', (source, target, routesList) => {  // 返回一個Promise  return google.maps.findRoute(source, target).then(route => {    routesList.add(route);  });})
          // 也可通過tapAsync添加插件,與tapPromise的不同之處是回調(diào)放在了最后一個參數(shù)car.hooks.calcRoutes.tapAsync('', (source, target, routesList, callback) => { bing.findRoute(source, target, (err, route) => { if (err) return callback(err) routesList.add(route); // 調(diào)用callback callback(); })})
          // 異步鉤子也可以添加同步插件car.hooks.calcRoutes.tap("CachedRoutesPlugin", (source, target, routesList) => { const cachedRoute = cache.get(source, target); if(cachedRoute) routesList.add(cachedRoute);})


          通過tap/tapPromise/tapAsync等添加訂閱者后,就可以在相應的時機觸發(fā)它們。


          class Car {  // ...  setSpeed(speed) {    // 觸發(fā)同步鉤子    this.hooks.accelerate.call(speed)  }
          useNavSystemPromise(source, target) { const routeList = new List(); // promise的方式觸發(fā)異步鉤子,對應tapPromise return this.hooks.calcRoutes.promise(source, target, routeList).then(res => { return routesList.getRoutes(); }) }
          useNavSystemAsync(source, target) { const routesList = new List(); // callAsync的方式觸發(fā)異步鉤子,對應tapAsync this.hooks.calcRoutes.callAsync(source, target, routesList, err => { if(err) return callback(err); callback(null, routesList.getRoutes()); }); }}


          上面是最簡單的例子,Tapable的鉤子按照回調(diào)執(zhí)行方式可分為以下幾種:


          • Basic。鉤子名稱中不帶Waterfall、Bail、Loop,其回調(diào)的執(zhí)行方式是按照添加的順序依次執(zhí)行。


          • Waterfall。也是依次執(zhí)行,不同的是執(zhí)行過程中會把上一個回調(diào)的結果傳給下一個回調(diào)。


          • Bail。允許提前退出,當某一個回調(diào)返回非空值時,不再繼續(xù)進行。


          • Loop。插件執(zhí)行中如果有一個不返回空,則又從第一個開始。也就是除非所有回調(diào)都返回空,否則會一直進行。


          注意上面所說的返回空,僅指undefined,不包含null、''等


          另外,Tapable的鉤子又可按照同步異步分為以下類型:


          • Sync。同步鉤子,只能用hook.tap()注冊回調(diào)。


          • AsyncSeries。異步鉤子串行執(zhí)行,可以用hook.tap()、hook.tapAsync()、hook.tapPromise()等方法注冊回調(diào)。


          • AsyncParallel。異步鉤子并行執(zhí)行,注冊回調(diào)的方式同AsyncSeries。


          上述兩種分類的組合就是Tapable鉤子真正的類型,體現(xiàn)在其暴露出的鉤子名稱上。比如AsyncSeriesWaterfallHook,就是Waterfall和AsyncSeries的結合,其允許異步回調(diào)并依次執(zhí)行,并且前一個回調(diào)的返回值回傳入下一個回調(diào)的參數(shù)中。


          由于AsycnParallel異步并行鉤子不能和WaterFall、Loop結合,因為前者是同時執(zhí)行,后者是順序執(zhí)行,二者矛盾。所以最終結合后的鉤子類型有3*4-2=10種:


          • SyncHook


          • SyncBailHook


          • SyncWaterfallHook


          • SyncLoopHook


          • AsyncParallelHook


          • AsyncParallelBailHook


          • AsyncSeriesHook


          • AsyncSeriesBailHook


          • AsyncSeriesLoopHook


          • AsyncSeriesWaterfallHook


          注意


          • AsyncParallelBailHook執(zhí)行過程中注冊的回調(diào)返回非undefined時就會直接執(zhí)行 callAsync或者promise中的函數(shù)(由于并行執(zhí)行的原因,注冊的其他回調(diào)依然會執(zhí)行)。


          • AsyncSeriesBailHook執(zhí)行過程中注冊的回調(diào)返回(resolve)非undefined時就會直接執(zhí)行callAsync或者promise中的函數(shù),并且注冊的后續(xù)回調(diào)都不會執(zhí)行。


          • AsyncSeriesWaterfallHook中上一個注冊的異步回調(diào)執(zhí)行之后的返回值會傳遞給下一個注冊的回調(diào)。



          (二)攔截器


          Tapable中也實現(xiàn)了攔截器功能,其可以在注冊/執(zhí)行回調(diào)等過程中觸發(fā)。

          攔截器的類型有:


          • register:定義tap/tapAsync/tapPromise時觸發(fā)。


          • call:執(zhí)行call/callAsync/promise時觸發(fā)。


          • tap:執(zhí)行tap/tapAsync/tapPromise定義的內(nèi)容時觸發(fā)。


          • loop:loop類型的鉤子執(zhí)行時觸發(fā)。


          car.hooks.calcRoutes.intercept({  call: (source, target, routesList) => {    console.log("Starting to calculate routes");  },  register: (tapInfo) => {    console.log(`${tapInfo.name} is doing its job`);    return tapInfo;   }})



          (三)HookMap/MultiHook


          另外Tapable還提供了HookMap和MultiHook等功能。


          HookMap是一個Hooks映射的幫助類,實際就是一個hook的key-value數(shù)組。MultiHook就是把其他的hook轉(zhuǎn)化為一個新的hook。


          Tapable的核心還是上面,這些是輔助工具。



          (四)Tapable實現(xiàn)原理


          Tapable實現(xiàn)原理比較簡單,其暴露出來的各種Hook,比如SyncHook、SyncBailHook等,都繼承自Hook類。



          tap方法定義在Hook基類上,調(diào)用tap的時候,會執(zhí)行this._insert方法,最終在this.taps[i]處增加一個新的回調(diào)。


          call方法也定義在Hook基類上,但是它的調(diào)用鏈比較長,call=> this._createCall=>this.compile,最后這個compile方法是在每個具體的Hook類上重新定義的:


          class SyncHookCodeFactory extends HookCodeFactory {  // ...}const factory = new SyncHookCodeFactory();
          const COMPILE = function(options) { factory.setup(this, options); return factory.create(options);}function SyncHook() { // ... hook.compile = COMPILE}


          看上面的COMPILE方法,factory.setup方法只是賦值了一些變量,所以call方法核心是通過factory.create方法創(chuàng)建的,其會根據(jù)每個Hook的不同種類生成不同的調(diào)用方法,比如同步/異步執(zhí)行、提前終止、傳入上一個回調(diào)的結果等。


          class HookCodeFactory {  // ...  create(options) {    switch (options.type) {      case "sync":        fn = new Function(          this.args(),          '"use strict";\n' +          this.header() +          this.contentWithInterceptors({            onError: err => `throw ${err};\n`,            onResult: result => `return ${result};\n`,            resultReturns: true,            onDone: () => "",            rethrowIfPossible: true          })        );      break;      // ...    }  }}


          由于不是本文的重點,Tapable就不再展開了。



          三、Webpack原理


          下面僅分析主要流程,對于watch、HMR等功能暫不涉及。本次分析的Webpack版本是V5.62.1。


          (一)總覽



          Webpack打包流程包含三個階段:


          • 初始化階段:包含了初始化參數(shù),創(chuàng)建Compiler,開始執(zhí)行compiler.run。


          • 構建階段:從entry開始創(chuàng)建Module,調(diào)用loader轉(zhuǎn)為JS,解析JS為AST,收集依賴,并遞歸創(chuàng)建Module。


          • 生成階段:根據(jù)入口和模塊的依賴關系,生成Chunk,輸出到文件。


          Webpack打包流程中有很重要的兩個概念:compilercompilation。


          • compiler:一次打包流程只會創(chuàng)建一個,貫穿整個編譯過程。


          • compilation:在watch為true的時候,每次文件變更觸發(fā)更新都會生成新的compilation。




          (二)初始化階段


          下圖是Webpack初始化階段的流程圖:



          我們使用Webpack的方式一般是通過wepback-cli,從webpack-cli的bin文件開始,其調(diào)用鏈大致如下:


          class WebpackCLI {  async run() {    // 加載webpack,可以理解為require('webpack')    this.webpack = await this.loadWebpack()    const options = [].concat(/* shell配置和配置文件 */)    this.runWebpack(options)  }
          async runWebpack(options) { const callback = () => {/* 錯誤處理等回調(diào) */} await this.createCompiler(options, callback); } await createCompiler(options, callback) { this.webpack( options, callback ) }}
          const runCLI = (args) => { const cli = new WebpackCLI() cli.run(args)}
          runCLI(process.argv);


          可以看到最終調(diào)用了Webpack庫的webpack方法。webpack方法定義在lib/webpack.js中:


          function webpack = (options, callback) => {  const create = () => {    const compiler = createCompiler(options)    return { compiler }  }  // ...  if (callback) {    const { compiler } = create()    compiler.run(() => {})  }}


          webpack首先調(diào)用createCompiler創(chuàng)建了一個compiler,然后調(diào)用了compiler.run方法。


          下面先看一下createCompiler方法:


          function createCompiler(rawOptions) {  const options = getNormalizedWwebpackOptions(rawOptions)  const compiler = new Compiler(options.context, options)  if (Array.isArray(options.plugins)) {    for (const plugin of options.plugins) {      if (typeof plugin === "function") {        plugin.call(compiler, compiler);      } else {        plugin.apply(compiler);      }    }  }
          compiler.hooks.environment.call(); compiler.hooks.afterEnvironment.call(); new WebpackOptionsApply().process(options, compiler); compiler.hooks.initialize.call(); return compiler}


          createCompiler先對options進行了一些標準化,然后通過new Compiler創(chuàng)建了一個compiler,然后依次執(zhí)行了optiosn.plugins和compiler.hooks上的幾個鉤子。然后調(diào)用了new WebpackOptionsApply().process()。


          下面看一下Compiler:


          class Compiler {  constructor() {    this.hooks = Object.freeze({      initialize: new SyncHook([]),      shouldEmit: new SyncBailHook(['compilation']),      // 其他一系列鉤子的定義    })  }}


          可以看到Compiler的構造函數(shù)中主要是定義了一系列鉤子,這些鉤子在構建的生命周期中會被依次調(diào)用。


          然后再看一下WebpackOptionsApply:


          class WebpackOptionsApply {  process(options, compiler) {    // 執(zhí)行plugins    // new SomePlugin().apply()        new EntryOptionPlugin().apply(compiler);    compiler.hooks.entryOption.call(options.context, options.entry);    //...  }}


          在new WebpackOptionsApply().process()方法中,執(zhí)行了很多內(nèi)部plugin,其中比較重要的是EntryOptionPlugin的執(zhí)行。


          class EntryOptionPlugin {  apply(compiler) {    compiler.hooks.entryOption.tap('EntryOptionPlugin', (context, entry) => {      EntryOptionPlugin.applyEntryOption(compiler, context, entry);      return true;    })  }
          static applyEntryOption(compiler, context, entry) { const EntryPlugin = require("./EntryPlugin"); for (const name of Object.keys(entry)) { const desc = entry[name]; // 整理entry的options,使之返回統(tǒng)一的格式 const options = EntryOptionPlugin.entryDescriptionToOptions( compiler, name, desc ); for (const entry of desc.import) { new EntryPlugin(context, entry, options).apply(compiler); } } }}


          看一下new EntryPlugin().apply()做了什么:


          class EntryPlugin {  apply(compiler) {    compiler.hooks.compilation.tap('EntryPlugin',       (compilation, { normalModuleFactory }) => {        compilation.dependencyFactories.set(          EntryDependency,          normalModuleFactory        );      })
          const { entry, options, context } = this; const dep = EntryPlugin.createDependency(entry, options);
          compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => { compilation.addEntry(context, dep, options, err => { callback(err); }); }); }}


          EntryPlugin在apply方法執(zhí)行的時候,會在compiler.hooks.make上注冊一個插件,其回調(diào)為compilation.addEntry,即開始從入口解析、收集依賴等,從這步開始進入了我們的構建階段。


          回到webpack方法中,創(chuàng)建compiler后,調(diào)用了compiler.run方法,來看一下run方法:


          class Compiler {  run (callback) {    const onCompiled = (err, compilation) => {      this.hooks.shouldEmit.call(compilation)      this.hooks.done.callAsync(() => {})      this.emitAssets(compilation, () => {        this.emitRecords(() => {})      })    }    this.hooks.beforeRun.callAsync(() => {      this.hooks.run.callAsync(() => {        this.compile(onCompiled)      })    })  }  compile(callback) {    const params = this.newCompilationParams();    this.hooks.beforeCompile.callAsync(params, () => {      this.hooks.compile.call(params);      const compilation = this.newCompilation(params);
          this.hooks.make.callAsync(compilation, () => { this.hooks.finishMake.callAsync(compilation, () => { compilation.finish(() => { compilation.seal(() => { this.hooks.afterCompile.callAsync((err) => { if (err) return callback(err) return callback(err, compilation) }) }) }) }) }) }) } emitRecords(callback) { this.outputFileSystem.writeFile(); }}


          compiler.run方法先觸發(fā)了hooks.beforeRun和hooks.run兩個鉤子,然后執(zhí)行了this.compile方法。


          compile方法也是也觸發(fā)了幾個前置回調(diào)hooks.beforeCompile和hooks.Compile,然后創(chuàng)建了compilation對象,之后觸發(fā)了make等回調(diào),make是構建的核心,他注冊的就是上面提到的entryPlugin,此時已經(jīng)進入構建階段。


          我們先忽略階段,把compile方法看完。


          compile接著調(diào)用了compilation.finish、compilation.seal、hooks.afterCompile,之后調(diào)用了傳入的callback,這里的callback就是run方法中onCompiled。onCompiled就是編譯完成做的事情,也是執(zhí)行了一些回調(diào):shouldEmit、done和this.emitAssets、this.emitRecords。


          compilation.seal方法就到了生成階段,我們下面會講到。


          小結下初始化階段做的事情:合并options、創(chuàng)建compiler、注冊插件、執(zhí)行compiler.run、創(chuàng)建compilation等。



          (三)構建階段


          下圖是構建階段的流程圖:



          構建階段從make鉤子觸發(fā)的compilation.addEntry開始,我們上面講過構建階段的本質(zhì)是從入口開始分析AST,收集依賴。


          Webpack的構建階段調(diào)用鏈比較長:


          • this.addEntry =>

          (https://github.com/webpack/webpack/blob/v5.62.1/lib/Compilation.js#L2100)


          • this._addEntryItem =>

          (https://github.com/webpack/webpack/blob/v5.62.1/lib/Compilation.js#L2135)


          • this.addModuleTree =>

          (https://github.com/webpack/webpack/blob/v5.62.1/lib/Compilation.js#L2049)


          • this.handleModuleCreation =>

          (https://github.com/webpack/webpack/blob/v5.62.1/lib/Compilation.js#L1743)


          • this.addModule =>

          (https://github.com/webpack/webpack/blob/v5.62.1/lib/Compilation.js#L1255)


          • this._handleModuleBuildAndDependencies =>

          (https://github.com/webpack/webpack/blob/v5.62.1/lib/Compilation.js#L1879)


          • this.buildModule =>

          (https://github.com/webpack/webpack/blob/v5.62.1/lib/Compilation.js#L1329?


          • this._buildModule =>

          (https://github.com/webpack/webpack/blob/v5.62.1/lib/Compilation.js#L1340)


          • module.build ?=>

          (https://github.com/webpack/webpack/blob/v5.62.1/lib/NormalModule.js#L929)


          • module._doBuild =>

          (https://github.com/webpack/webpack/blob/v5.62.1/lib/NormalModule.js#L736)


          • runLoaders =>

          (https://github.com/webpack/webpack/blob/v5.62.1/lib/NormalModule.js#L812)


          • this.parser.parse =>

          (https://github.com/webpack/webpack/blob/v5.62.1/lib/javascript/JavascriptParser.js#L3282)


          • handleParseResult =>

          (https://github.com/webpack/webpack/blob/v5.62.1/lib/NormalModule.js#L975)


          • processModuleDependencies =>

          (https://github.com/webpack/webpack/blob/v5.62.1/lib/Compilation.js#L1939)


          • handleModuleCreation

          (https://github.com/webpack/webpack/blob/v5.62.1/lib/Compilation.js#L1743)


          看一下上面比較重要的環(huán)節(jié):


          • handleParseResult作用是處理模塊依賴。


          • processModuleDependencies是buildModule的回調(diào),其會調(diào)用handleModuleCreation,這樣對于新增的依賴,會創(chuàng)建新的module,回到了第一步,遞歸就是在這里實現(xiàn)的。


          注意這里的AST解析是利用acorn實現(xiàn)的:


          const { Parser: parser } = require("acorn");
          class JavascriptParser extends Parse { parse(source) { // ... const ast = parser.parse(source) }}


          其他部分大家可點擊上面方法的鏈接網(wǎng)址去查看源代碼,這里就不展開了,下面我們還會手寫一個例子深入理解這部分。



          (四)生成階段


          生成階段的流程圖如下:



          生成階段從上面提到的compilation.seal開始:


          class Compilation {  seal(callback) {   const chunkGraph = new ChunkGraph()   this.addChunk();   buildChunkGraph();   this.hooks.optimizeModules.call()   this.hooks.afterOptimizeModules.call()   this.hooks.optimizeChunks.call()   this.hooks.afterOptimizeChunks.call()   this.hooks.optimizeTree.call()   // ...   this.createModuleAssets();   this.createChunkAssets();  }
          createChunkAssets() { this.emitAsset() // ... } createModuleAssets() { this.emitAsset() }}


          執(zhí)行完compilation.seal后,會執(zhí)行其回調(diào),也就是上面提到的onCompiled方法,也就是this.outputFileSystem.writeFile(),即把內(nèi)存中的chunk信息存入文件系統(tǒng)。


          下面總結下生成階段做的事情:


          • 創(chuàng)建ChunkGraph。


          • 遍歷modules,將module分配給不同的chunk。


          • 調(diào)用createChunkAssets和createModuleAssets分別將chunk和module將assets信息寫入到compilation.assets中。


          • 調(diào)用seal回調(diào),執(zhí)行outputFileSystem.writeFile,寫入文件。



          四、手寫打包核心原理


          如果看到這里你還是云里霧里,可以手寫一下打包核心原理來加深印象。


          Webpack功能復雜、模塊眾多,其核心邏輯被一層層封裝。熟讀其源碼可以理解其架構,但對于核心原理,還是手寫一下印象最深。


          下面這個打包例子與Webpack用的庫不一致,但是打包思想是一樣的。

          先創(chuàng)建parser.js,導出幾個方法:


          • getAST:利用@babel/parser生成AST。


          • getDependencies:利用@babel/traverse遍歷AST,獲取依賴。


          • transform:將AST轉(zhuǎn)為code,并轉(zhuǎn)化其中的ES6語法。


          const fs = require('fs');const parser = require('@babel/parser')const traverse = require('@babel/traverse').defaultconst { transformFromAst } = require('babel-core')
          module.exports = { getAST: path => { const source = fs.readFileSync(path, 'utf-8') return parser.parse(source, { sourceType: 'module' }) }, getDependencies: ast => { const depencies = [] traverse(ast, { ImportDeclaration: ({node}) => { depencies.push(node.source.value) } }) return depencies; }, transform: ast => { const { code } = transformFromAst(ast, null, { presets: ['env'] }) return code; }}


          再創(chuàng)建compiler.js:


          const path = require('path');const fs = require('fs')const { getAST, getDependencies, transform } = require('./parser')
          class Compiler { constructor(options) { const { entry, output } = options this.entry = entry; this.output = output this.modules = []; }
          run () { const entryModule = this.buildModule(this.entry, true); this.modules.push(entryModule); this.walk(entryModule); // console.log('modules', this.modules) this.emitFiles(); }
          walk(module) { const moduleNameMap = this.modules.map(item => item.filename) module.dependencies.map(dep => { if (!moduleNameMap.includes(dep)) { const newModule = this.buildModule(dep) this.modules.push(newModule); this.walk(newModule) } }) }
          buildModule(filename, isEntry) { let ast; if (isEntry) { ast = getAST(filename) } else { const absolutePath = path.resolve(process.cwd(), './webpack/demo', filename) ast = getAST(absolutePath) } return { filename, dependencies: getDependencies(ast), transformCode: transform(ast) } }
          emitFiles() { const outputPath = path.join(this.output.path, this.output.filename) let modules = '' this.modules.map(_module => { modules += `'${_module.filename}': function(require, module, exports) {${_module.transformCode}},` })
          const bundle = ` (function(modules) { function require(filename) { const fn = modules[filename]; const module = { exports: {} }; fn(require, module, module.exports); return module.exports; } require('${this.entry}') })({${modules}}) `
          fs.writeFileSync(outputPath, bundle, 'utf-8') }}


          compiler.js的核心邏輯在run方法中,其從入口開始創(chuàng)建module,然后遞歸的收集依賴,最后調(diào)用emitFiles輸出到文件中。


          注意輸出的時候,創(chuàng)建了自執(zhí)行函數(shù),此時的傳入的參數(shù)是一個對象,其key值為模塊地址,value值為模塊內(nèi)容。然后調(diào)用自定義的require函數(shù),傳入第一個module,然后依次執(zhí)行。


          一個基本的打包器就是這么簡單。


          下面創(chuàng)建幾個文件測試一下:


          // a.jsexport const a = 1;
          // b.jsimport { a } from './a.js'export const b = a + 1;
          // index.jsimport { b } from './b.js'console.log(b + 1)


          然后引入我們的簡易打包器,打包試一下:


          // main.jsconst Compiler = require('./compiler')const config = {  entry: path.join(__dirname, './index.js'),  output: {    path: path.join(__dirname, '../dist'),    filename: 'bundle.js'  }}
          function main(options) { new Compiler(options).run();}
          main(config)


          執(zhí)行上面的main.js,就可以在dist目錄下看到打包出的bundle.js。


          我們可以新建一個index.html引入我們的bundle文件,打開console面板,就能看到打印的內(nèi)容了。



          上面例子的地址:

          (https://github.com/novlan1/rollup-intro/tree/master/webpack)



          五、總結


          本文分析了Tapable插件的使用和原理,講解了Webpack主要流程,并手寫了一個簡易打包器。Webpack和Rollup打包原理大同小異,理解其打包原理有利于更好的使用這些工具。


          參考資料:

          1.Tapable

          2.關于tapable你需要知道這些

          3.tapable詳解

          4.Tapable(一)

          5.一文吃透Webpack核心原理



          ?作者簡介


          楊國旺

          騰訊前端開發(fā)工程師

          騰訊前端開發(fā)工程師,歡迎討論前端問題。



          ?推薦閱讀


          技術人專屬年味盡在這里!云加社區(qū)祝您虎年大吉

          從C++轉(zhuǎn)向Rust:兩大主題值得關注!

          關于Go并發(fā)編程,你不得不知的“左膀右臂”——并發(fā)與通道!

          一文入魂:媽媽再也不用擔心我不懂C++移動語義了!



          瀏覽 60
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  成年男女啪啪网视频 | 免费看又黄又无码的网站 | 亚洲精品七区 | 黄片在现免费观看 | 【乱子伦】国产精品 |