webpack源碼分析之四:plugin

前言

插件plugin,webpack重要的組成部分。它以事件流的方式讓用戶能夠直接接觸到webpack的整個編譯過程。plugin在編譯的關鍵地方觸發對應的事件,極大的加強了webpack的擴展性。它的出現讓webpack從一個面向過程的打包工具,變成了一套完整的打包生態系統。webpack

功能分析

Tapable

既然說到了事件流,那麼就得介紹Tapable了,Tapable是webpack裏面的一個小型庫,它容許你自定義一個事件,並在觸發後訪問到觸發者的上下文。固然他也支持異步觸發,多個事件同步,異步觸發。本次實現用的是較早的v0.1.9版,具體文檔可查看tapable v0.19文檔git

在webpack內使用,如SingleEntryPlugin中github

compiler.plugin("make",function(compilation,callback){
   compilation.addEntry(this.context, new SingleEntryDependency({request: this.entry}), this.name, callback);
})

在compiler內部觸發。web

this.applyPluginsParallel('make',compilation, err => {
     /* do something */
 })

解析入口文件時,經過EntryOptionPlugin解析entry類型並實例化SingleEntryPlugin, SingleEntryPlugin在調用compilation的addEntry函數開啓編譯。這種觀察者模式的設計,解耦了compiler, compilation,並使它們提供的功能更加純粹,進而增長擴展性。express

流程劃分

縱觀整個打包過程,能夠流程劃分爲四塊。編程

  1. 初始化
  2. 構建
  3. 封裝
  4. 文件寫入

模塊劃分

接入plugin後,webpack對parse,resolve,build,writeSource等功能的大規模重構。
目前拆分模塊爲數組

  • Parser模塊,負責編譯module。
  • Resolver模塊,負責對文件路徑進行解析。
  • ModuleFactory模塊,負責完成module的實例化。
  • Module模塊,負責解析出modules依賴,chunk依賴。構建出打包後自身module的源碼。
  • Template模塊,負責提供bundle,chunk模塊文件寫入的模版。
  • Compilation模塊,負責文件編譯細節,構建並封裝出assets對象供Compiler模塊進行文件寫入。
  • Compiler模塊,負責實例化compilation,bundle文件的寫入。監聽modules的變化,並從新編譯。

核心類關係圖

clipboard.png

功能實現

Parser模塊

經過exprima將源碼解析爲AST樹,並拆分statements,以及expression直至Identifier基礎模塊。緩存

  1. 解析到CallExpression時觸發call事件。
  2. 解析到MemberExpression,Identifier時觸發expression事件。
  3. 提供evaluateExpression函數,訂閱Literal,ArrayExpression,CallExpression,ConditionalExpression等顆粒化的事件供evaluateExpression調用。
case 'CallExpression':
            //do something
            this.applyPluginsBailResult('call ' + calleeName, expression);
            //do something
            break;
 case 'MemberExpression':
            //do something
            this.applyPluginsBailResult('expression ' + memberName, expression);
            //do something
            break;
 case 'Identifier':
            //do something
            this.applyPluginsBailResult('expression ' + idenName, expression);
               //do something
            break;
this.plugin('evaluate Literal', (expr) => {})
 this.plugin('evaluate ArrayExpression', (expr) => {})
 this.plugin('evaluate CallExpression', (expr) => {})
 ...

如須要解析require("a"),require.ensure(["b"],function(){})的時候,註冊plugin去訂閱"call require",以及"call require.ensure",再在回調函數調用evaluateExpression解析expression。數據結構

Resolver模塊

封裝在enhanced-resolve庫,提供異步解析文件路徑,以及可配置的filestream能力。在webpack用於緩存文件流以及如下三種類型模塊的路徑解析。app

  • 普通的module模塊
  • 帶context的module模塊
  • loader模塊

用法如

ResolverFactory.createResolver(Object.assign({
            fileSystem: compiler.inputFileSystem,
            resolveToContext: true
        }, options.resolve));

具體配置可去查看github文檔

ModuleFactory模塊

子類有NormalModuleFactory,ContextModuleFactory。經常使用的NormalModuleFactory功能以下

  1. 實例化module以前,調用Resolver模塊解析出module和preloaders的絕對路徑。
  2. 經過正則匹配module文件名,匹配出rules內的loaders,並和preloaders合併。
  3. 實例化module

這裏主要是使用async庫的parallel函數並行的解析loaders和module的路徑,並整合運行結果。

async.parallel([
                (callback) => {
                    this.requestResolverArray( context, loader, resolver, callback)
                },
                (callback) => {
                    resolver.normal.resolve({}, context, req, function (err, result) {
                        callback(null, result)
                    });
                },
            ], (err, result) => {
                    let loaders = result[0];
                const resource = result[1];
                //do something
            })

async模塊是一整套異步編程的解決方案。async官方文檔

Module模塊

  1. 運行loaders數組內的函數,支持同步,異步loaders,獲得編譯前源碼。
  2. 源碼交由Parser進行解析,分析出modules依賴和blocks切割文件依賴
  3. 提供替換函數,將源碼替換,如require('./a')替換爲__webpack_require__(1)

一個編譯好的module對象包含modules依賴ModuleDependency和blocks依賴RequireEnsureDependenciesBlock,loaders,源碼_source,其數據結構以下:

{
  chunks: [],
  id: null,
  parser: 
   Tapable {
     _plugins: 
      { 'evaluate Literal': [Array],
        'evaluate ArrayExpression': [Array],
        'evaluate CallExpression': [Array],
        'call require': [Array],
        'call require:commonjs:item': [Array],
        'call require.ensure': [Array] },
     options: {},
     scope: { declarations: [] },
     state: { current: [Circular], module: [Circular] },
     _currentPluginApply: undefined },
  fileDependencies: 
   [ '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/a.js' ],
  dependencies: 
   [ ModuleDependency {
       request: './module!d',
       range: [Array],
       class: [Function: ModuleDependency],
       type: 'cms require' },
     ModuleDependency {
       request: './assets/test',
       range: [Array],
       class: [Function: ModuleDependency],
       type: 'cms require' } ],
  blocks: 
   [ RequireEnsureDependenciesBlock {
       blocks: [],
       dependencies: [Array],
       requires: [Array],
       chunkName: '',
       beforeRange: [Array],
       afterRange: [Array] } ],
  loaders: [],
  request: '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/a.js',
  fileName: 'a.js',
  requires: [ [ 0, 7 ], [ 23, 30 ] ],
  context: '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example',
  built: true,
  _source: 
   RawSource {
     _result: 
      { source: 'require(\'./module!d\');\nrequire(\'./assets/test\');\nrequire.ensure([\'./e\',\'./b\'], function () {\n    console.log(1)\n    console.log(1)\n    console.log(1)\n    console.log(1)\n    require(\'./m\');\n    require(\'./e\');\n});\n' },
     _source: 'require(\'./module!d\');\nrequire(\'./assets/test\');\nrequire.ensure([\'./e\',\'./b\'], function () {\n    console.log(1)\n    console.log(1)\n    console.log(1)\n    console.log(1)\n    require(\'./m\');\n    require(\'./e\');\n});\n' 
             } 
     }

Compilation模塊

  1. 經過entry和context,獲取到入口module對象,並建立入口chunk。
  2. 經過module的modules依賴和blocks切割文件構建出含有chunk和modules包含關係的chunk對象。
  3. 給modules和chunks的排序並生成id,觸發一系列optimize相關的事件(如CommonsChunkPlugin就是使用optimize-chunks事件進行開發),最終構建出有文件名和源碼映射關係的assets對象

一個典型的含有切割文件的多入口entry的assets對象數據結構以下:

assets: 
   { '0.bundle.js': 
      Chunk {
        name: '',
        parents: [Array],
        modules: [Array],
        id: 0,
        source: [Object] },
     'main.bundle.js': 
      Chunk {
        name: 'main',
        parents: [],
        modules: [Array],
        id: 1,
        entry: true,
        chunks: [Array],
        blocks: true,
        source: [Object] },
     'multiple.bundle.js': 
      Chunk {
        name: 'multiple',
        parents: [],
        modules: [Array],
        id: 2,
        entry: true,
        chunks: [Array],
        source: [Object] } 
  }

Compiler模塊

  1. 解析CLI, webpack配置獲取options對象,初始化resolver,parser對象。
  2. 實例化compilation對象,觸發make 並行事件調用compilation對象的addEntry開啓編譯。
  3. 獲取到assets對象,經過觸發before-emit事件開啓文件寫入。經過JsonMainTemplate模版完成主入口bundle文件的寫入,JsonpChunkTemplate模版完成chunk切割文件的寫入。 使用async.forEach管理異步多文件寫入的結果。
  4. 監聽modules的變化,並從新編譯。

考慮到多入口entry的可能,make調用的是並行異步事件

this.applyPluginsParallel('make', compilation, err => {
    //do something
    compilation.seal(err=>{})
    //do something
}

代碼實現

本人的簡易版webpack實現simple-webpack

總結

相信你們都有設計過業務/開源代碼,不少狀況是越日後寫,越難維護。一次次的定製化的需求,將原有的設計改的支離破碎。這個時候能夠試試借鑑webpak的思想,充分思考並抽象出穩定的基礎模塊,劃分生命週期,將模塊之間的業務邏輯,特殊需求交由插件去解決。

完。

相關文章
相關標籤/搜索