你不知道的webpack靜態文件生成過程

做者:崔靜javascript

本文須要你對webpack有必定的瞭解,若是你比較感興趣,能夠參考咱們以前的webpack源碼解析系列:webpack系列-總覽前端

一些概念說明

Compilation 初始化的時候會初始化下面幾個變量:vue

this.mainTemplate = new MainTemplate(...)
this.chunkTemplate = new ChunkTemplate(...)
this.runtimeTemplate = new RuntimeTemplate
this.moduleTemplates = {
   javascript: new ModuleTemplate(this.runtimeTemplate, "javascript"),
   webassembly: new ModuleTemplate(this.runtimeTemplate, "webassembly")
}
this.hotUpdateChunkTemplate // 暫時不關注
複製代碼

mainTemplate: 用來生成執行主流程的代碼,裏面包含了 webpack 的啓動代碼等等。 chunkTemplate: 獲得的最終代碼則會經過 JsonP 的方式來加載。java

下面的例子: 咱們有一個入口文件:webpack

// main.js
import { Vue } from 'vue'
new Vue(...)
複製代碼

這樣的文件打包後生成一個 app.js ,一個 chunk-vendor.js。git

app.js 結構以下:github

(function(modules) { // webpackBootstrap
   // webpack 的啓動函數
   // webpack 內置的方法
 }){{
   moduleId: (function(module, exports, __webpack_require__) {
      // 咱們寫的 js 代碼都在各個 module 中
   },
   // ...
 })
複製代碼

chunk-vendors.js 結構以下:web

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["chunk-vendors"],{
   moduleId: (function(module, exports, __webpack_require__) {
     // ...
   },
   // ...
})
複製代碼

app.js 裏面包含了 webpack 的 bootstrap 代碼,這個代碼總體的框架就在 mainTemplate。express

app.js 會經過 jonsP 的方式加載 chunk-vendor.js ,這個 js 代碼的框架就放在 chunkTemplate 中。bootstrap

app.js 和 chunk-vendors.js 中各個 module 的代碼生成過程就在 ModuleTempalte 中。

代碼生成主流程

chunk 代碼生成在 seal 階段。從 Compilation.createChunkAssets 中開始。

主流程圖以下

create-asset.png

**說明1:**在 JavascriptModulePlugin中會肯定 render 函數。這個 render 函數後續在 createChunkAssets 中會調用。

**說明2:**這裏 moduleTemplate 在 Compilation 一開始初始化會生成

this.moduleTemplates = {
	javascript: new ModuleTemplate(this.runtimeTemplate, "javascript"),
	webassembly: new ModuleTemplate(this.runtimeTemplate, "webassembly")
};
複製代碼

因爲走到是 mainTemplate,在最開始獲取 render 各類信息的函數中 renderManifest 爲觸發 JavascriptModulesPlugin 中註冊的函數,而這個裏面肯定了 module 所使用的模板爲 moduleTemplates.javascript

compilation.mainTemplate.hooks.renderManifest.tap(
  "JavascriptModulesPlugin",
  (result, options) => {
    //...
    result.push({
      render: () =>
      compilation.mainTemplate.render(
        hash,
        chunk,
        moduleTemplates.javascript,
        dependencyTemplates
      ),
      //...
    });
    return result;
  }
);
複製代碼

說明3: module-source 的過程見最後附加內容

首先肯定當前結構是使用 mainTemplate 仍是走 chunkTemplate。這兩個 Tempalte 中會有本身的 render 流程。咱們以 mainTempalte 爲例,看 render 的流程。

render 主流程中會生成主結構的代碼,也就是前面咱們 app.js demo 生成的代碼框架部分。而後生成各個 moulde 的代碼。這個流程由 ModuleTemplate 中的函數完成。

在 module 生成的時候,會調用 hook.content, hook.module, hook.render, hook.package這幾個 hook。在每個 hook 獲得結果以後,傳入到下一個 hook 中。hook.module 這個 hook 執行完後,會獲得 module 的代碼。而後在 hook.render 中,將這些代碼包裹成一個函數。若是咱們在 webpack.config.js 中配置了 output.pathinfo=true (配置說明),那麼在 hook.package這裏就會給最終生成的代碼添加一些路徑和 tree-shaking 相關的註釋,能夠方便咱們閱讀代碼。

獲得全部的 module 代碼以後,將它們包裹成數組或者對象。

修改代碼

  • 利用上面文件生成的 hook, 在某個 module 中添加額外內容

BannerPlugin 是在 chunk 文件開頭添加額外的內容。若是咱們僅僅是但願在某個 module 中添加內容如何作呢?回顧一下上面代碼生成的流程圖,module 代碼生成有幾個關鍵的 hook 例如 hook.content,hook.module,hook.render。能夠在這幾個 hook 中註冊函數來進行修改。一個簡單的 demo 以下

const { ConcatSource } = require("webpack-sources");
class AddExternalPlugin {
  constructor(options) {
    // plugin 初始化。這裏處理一些參數格式化等
    this.content = options.content // 獲取要添加的內容
  }
  apply(compiler) {
    const content = this.content
    compiler.hooks.compilation.tap('AddExternal', compilation => {
      compilation.moduleTemplates.javascript.hooks.render.tap('AddExternal', ( moduleSource, module ) => {
          // 這裏會傳入 module 參數,咱們能夠配置,指定在某一 module 中執行下面的邏輯
          // ConcatSource 意味着最後處理的時候,咱們 add 到裏面的代碼,會直接拼接。
          const source = new ConcatSource()
          // 在最開始插入咱們要添加的內容
          source.add(content)
          // 插入源碼
          source.add(moduleSource)
          // 返回新的源碼
          return source
      })
    })
  }
}
複製代碼
  • 在 chunk 執行代碼外再包裹一層額外的邏輯。

咱們曾經配置過 umd 的模式,或者 output.library 參數。配置了這倆內容以後,最後生成的代碼結構就和最開始 app.js demo 中的結果不同了。以 output.library='someLibName' 爲例,會變成下面這樣

var someLibName =
(function(modules){
// webpackBootstrap
})([
//... 各個module
])
複製代碼

這個的實現,就是在上面 hooks.renderWithEntry 環節對 mainTemplate 生成的代碼進行了修改。

若是咱們在某些狀況下,想額外包裹一些本身的邏輯。能夠就在這裏處理。給一個簡單的 demo

const { ConcatSource } = require("webpack-sources");
class MyWrapPlugin {
  constructor(options) {
  }
  apply(compiler) {
    const onRenderWithEntry = (source, chunk, hash) => {
      const newSource = new ConcatSource()
      newSource.add(`var myLib =`)
      newSource.add(source)
      newSource.add(`\nconsole.log(myLib)`)
      return newSource
    }
    compiler.hooks.compilation.tap('MyWrapPlugin', compilation => {
      const { mainTemplate } = compilation
      mainTemplate.hooks.renderWithEntry.tap(
        "MyWrapPlugin",
        onRenderWithEntry
      )
      // 若是咱們支持一些變量的配置化,那麼就須要把咱們配置的信息寫入 hash 中。不然,當咱們修改配置的時候,會發現 hash 值不會變化。
      // mainTemplate.hooks.hash.tap("SetVarMainTemplatePlugin", hash => {
      // hash.update()
      // });
    })
  }
}

module.exports = MyWrapPlugin
複製代碼

webpack編譯後結果

var myLib =/******/ (function(modules) {
//... webpack bootstrap 代碼
/******/  return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports) {
// ...
/***/ })
/******/ ])
console.log(myLib);
複製代碼
  • BannerPlugin

相似內置的 BannerPlugin。在上面 chunk 文件生成以後,也就是createChunkAssets執行完成以後,對總體的 chunk 文件內容進行修改。例如 bannerPlugin 是在 optimizaChunkAssets hook 中

在這個 hook 裏面能夠拿到一個參數 chunks 全部的 chunk,而後在這裏能夠添加額外的內容。

chunkAssets 後文件內容修改

createChunkAssets 執行事後,其餘的 hook 中可能夠拿到文件內容,進行修改。

  • sourcemap 的影響,afterOptimizeChunkAssets 這個 hook 以後,webpack 生成了 sourcemap。若是在這個以後進行代碼的修改,例如 optimizeAssets 或者更後面的 emit hook 中,會發現 sourcemap 不對了。像下面的例子

    compiler.hooks.compilation.tap('AddExternal', compilation => {
      compilation.hooks.optimizeAssets.tap('AddExternal', assets => {
        let main = assets["main.js"]
        main = main.children.unshift('//test\n//test\n')
      })
    })
    複製代碼
  • 對 hash 的影響。當上面 chunk 代碼生成結束後,其實 hash 也就隨着生成了。在hash生成完以後的 hook 中對代碼的修改,好比增長點啥,不會影響到 hash 的結果。例如上面修改 chunk 代碼的例子。假如咱們的 plugin 進行了升級,修改的內容變了,可是生成的 hash 並不會隨着改變。因此須要在 hash 生成相關的 hook 中,把 plugin 的內容寫入 hash 中。

module-source 生成

module-source 的過程當中會對 parser 階段生成的各個 dependency 進行處理,根據 dependency.Template 實現對咱們縮寫的源碼的轉換。這裏咱們結合最開始 parser 來一塊兒看 module-source。如下面 demo 爲例:

// main.js
import { test } from './b.js'
function some() {
  test()
}
some()

// b.js
export function test() {
  console.log('b2')
}
複製代碼

main.js parser 中轉成的 AST:

ast.png

對 ast 進行 parser ,這個過程當中,會經歷

if (this.hooks.program.call(ast, comments) === undefined) {
  this.detectMode(ast.body);
  this.prewalkStatements(ast.body);
  this.blockPrewalkStatements(ast.body);
  this.walkStatements(ast.body);
}
複製代碼
  • program

    檢測有沒有用到 import/export ,會增長 HarmonyCompatibilityDependency, HarmonyInitDependency(做用後面介紹)

  • detectMode

    檢測最開始是否有 use strictuse asm,爲了保證咱們代碼編譯以後開頭寫的 use strict 仍然在最開始

  • prewalkStatements

    遍歷當前做用域下全部的變量定義。這個過程當中import { test } from './b.js' 中 test 也是在當前做用域下的,因此 import 在這裏會被處理(過程見 javascript-parser)。針對這句 import 會額外被添加ConstDependencyHarmonyImportSideEffectDependency

    ast-prewalk.png

  • blockPrewalk

    處理當前做用域下 let/const(在 prewalk 的時候只會處理var),class 名,export 和 export default

  • walkStatements

    開始深刻每個節點進行處理。這裏會找到代碼中全部使用 test 的地方,而後添加 HarmonyImportSpecifierDependency

    ast-walk.png

經理過這些以後,對於上面的 demo 就會加入

HarmonyCompatibilityDependency

HarmonyInitDependency

ConstDependency

HarmonyImportSideEffectDependency

HarmonyImportSpecifierDependency

這些 dependency 分紅兩大類:

  • moduleDependency: 有對應的 denpendencyFactory,在 processModuleDependencies 過程當中會對這個 dependency 進行處理,獲得對應的 module

    HarmonyImportSideEffectDependency --> NormalModuleFactory

    HarmonyImportSpecifierDependency --> NormalModuleFactory

    兩個指向的是同一個 module(./b.js),因此會被去重。而後 webpack 沿着 dependency ,處理 b.js... 直到將全部的 moduleDependency 處理完

  • 僅文件生成的時候,用來生成代碼

module.source

首先拿到源碼代碼,而後處理各個 dependency

  • HarmonyCompatibilityDependency

    在開始插入 __webpack_require__.r(__webpack_exports__);,標識這是一個 esModule

  • HarmonyInitDependency

    遍歷全部的 dependency, 負責生成 import {test} from './b.js' 對應的引入 './b.js' 模塊的代碼

    /* harmony import */ var _b_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0);

  • ConstDependency

    在 HarmonyInitDependency 階段中,已經插入了 import 語句對應的內容,因此源碼中的 import {test} from './b.js'須要刪除掉。ConstDependency 的做用就是把這句替換成空,即刪除

  • HarmonyImportSideEffectDependency

做用階段在 HarmonyInitDependency 過程當中

  • HarmonyImportSpecifierDependency

    代碼中 test() 所生成的依賴。做用就是替換代碼中的 test

    • 獲取到 './b.js' 模塊對應的變量名 _b_js__WEBPACK_IMPORTED_MODULE_0__

    • 獲取 test 對應到 b.js 中的屬性名(由於通過 webpack 編譯,爲了簡化代碼,咱們在 b.js 中的 export test,可能會被轉爲 export a = test)

      Object(_b_js__WEBPACK_IMPORTED_MODULE_0__[/* test */ "a"])

      若是是被調用的話,會走一個邏輯??

      if (isCall) {
      				if (callContext === false && asiSafe) {
      					return `(0,${access})`;
      				} else if (callContext === false) {
      					return `Object(${access})`;
      				}
      			}
      複製代碼
    • 而後替換代碼中 test

通過全部的 dependency 以後:

before-after-code.png

瞭解了這個過程以後,若是咱們須要對源代碼中進行一些簡單的修改,能夠利用 parser 階段的各個 hook 來實現。在這裏修改有一個好處,不用擔憂搞壞 sourcemap 和 影響 hash 的生成。

  • parser 中插入代碼的 demo

例如,咱們使用某個插件的時候,須要下面的寫法

import MainFunction from './a.js'
import { test } from './b.js'
MainFunction.use(test)
複製代碼

實際中利用 webpack 插件,在檢測到有 test 引入時候,自動插入

import MainFunction from './a.js'
MainFunction.use(test)
複製代碼

實現的關鍵,就是上面提到的 HarmonyImportSideEffectDependency, HarmonyImportSpecifierDependencyConstDependency

代碼以下

const path = require('path')
const ConstDependency = require("webpack/lib/dependencies/ConstDependency");
const HarmonyImportSideEffectDependency = require("webpack/lib/dependencies/HarmonyImportSideEffectDependency")
const HarmonyImportSpecifierDependency = require("webpack/lib/dependencies/HarmonyImportSpecifierDependency")
const NullFactory = require("webpack/lib/NullFactory");

// 要引入的 a.js 的路徑。這個路徑後面會通過 webpack 的 resolve
const externalJSPath = `${path.join(__dirname, './a.js')}`

class ProvidePlugin {
	constructor() {
	}
	apply(compiler) {
		compiler.hooks.compilation.tap(
			"InjectPlugin",
			(compilation, { normalModuleFactory }) => {
				const handler = (parser, parserOptions) => {
          // 在 parser 處理 import 語句的時候
          parser.hooks.import.tap('InjectPlugin', (statement, source) => {
            parser.state.lastHarmonyImportOrder = (parser.state.lastHarmonyImportOrder || 0) + 1;
            // 新建一個 './a.js' 的依賴
            const sideEffectDep = new HarmonyImportSideEffectDependency(
              externalJSPath,
              parser.state.module,
              parser.state.lastHarmonyImportOrder,
              parser.state.harmonyParserScope
            );
            // 爲 dependency 設置一個位置。這裏設置爲和 import { test } from './b.js' 相同的位置,在代碼進行插入的時候會插入到改句所在的地方。
            sideEffectDep.loc = {
              start: statement.start,
              end: statement.end
            }
            // 設置一下 renames,標識代碼中 mainFunction 是從外部引入的
            parser.scope.renames.set('mainFunction', "imported var");
            // 把這個依賴加入到 module 的依賴中
            parser.state.module.addDependency(sideEffectDep);
            
            // -------------處理插入 mainFunction.use(test)------------
            if (!parser.state.harmonySpecifier) {
              parser.state.harmonySpecifier = new Map()
            }
            parser.state.harmonySpecifier.set('mainFunction', {
              source: externalJSPath,
              id: 'default',
              sourceOrder: parser.state.lastHarmonyImportOrder
            })
            // 針對 mainFunction.use 中的 mainFunction
            const mainFunction = new HarmonyImportSpecifierDependency(
              externalJSPath,
              parser.state.module,
              -1,
              parser.state.harmonyParserScope,
              'default',
              'mainFunction',
              [-1, -1], // 插入到代碼最開始
              false
            )
            parser.state.module.addDependency(mainFunction)
            
            // 插入代碼片斷 .use(
            const constDep1 = new ConstDependency(
              '.use(',
              -1,
              true
            )
            parser.state.module.addDependency(constDep1)
            
            // 插入代碼片斷 test
            const useArgument = new HarmonyImportSpecifierDependency(
              source,
              parser.state.module,
              -1,
              parser.state.harmonyParserScope,
              'test',
              'test',
              [-1, -1],
              false
            )
            parser.state.module.addDependency(useArgument)
            
            // 插入代碼片斷 )
            const constDep2 = new ConstDependency(
              ')\n',
              -1,
              true
            )
            parser.state.module.addDependency(constDep2)
          });
        }
				normalModuleFactory.hooks.parser
					.for("javascript/auto")
					.tap("ProvidePlugin", handler);
				normalModuleFactory.hooks.parser
					.for("javascript/dynamic")
					.tap("ProvidePlugin", handler);
			}
		);
	}
}
module.exports = ProvidePlugin;

複製代碼

生成的代碼以下

/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
 "use strict";
const mainFunction = function () {
  console.log('mainFunction')
}

mainFunction.use = function(name) {
  console.log('load something')
}
/* harmony default export */ __webpack_exports__["a"] = (mainFunction);

/***/ }),
/* 2 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
 "use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _Users_didi_Documents_learn_webpack_4_demo_banner_demo_a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
/* harmony import */ var _b_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(0);
_Users_didi_Documents_learn_webpack_4_demo_banner_demo_a_js__WEBPACK_IMPORTED_MODULE_0__[/* default */ "a"].use(_b_js__WEBPACK_IMPORTED_MODULE_1__[/* test */ "a"])

Object(_b_js__WEBPACK_IMPORTED_MODULE_1__[/* test */ "a"])()

/***/ })
複製代碼
  • DefinePlugin

    DefinePlugin 介紹

    可使用這個插件在編譯階段對一些常量進行替換的時候,例如:

    • 經常使用到的 js 代碼中根據process.env.NODE_ENV的值,區分不一樣 dev 環境 和 production 環境。從而實如今不一樣環境下走不一樣的分支邏輯。
    new DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
    })
    複製代碼
    • 能夠配置 api URL

      new DefinePlugin({
        API_DOMAIN: process.env.NODE_ENV === 'dev' ? '"//10.96.95.200"' : '"//api.didi.cn"'
      })
      複製代碼

      實現 dev 和 production 下 api 請求域名的切換。

    簡單介紹一些原理:一個最簡單的例子

    new DefinePlugin({
      'TEST': "'test'"
    })
    複製代碼

    代碼中使用 const a = TEST, 在 parser 的時候遍歷到 = 號右邊的時候,會觸發表達式解析的鉤子

    // key 是 TEST
    parser.hooks.expression.for(key).tap("DefinePlugin", expr => {
      const strCode = toCode(code, parser); // 結果爲咱們設置的 'test'
      if (/__webpack_require__/.test(strCode)) {
        // 若是用到了 __webpack_require__ ,生成的 ConstantDependency 中 requireWebpackRequire=true
        // 在後期生成代碼,用 function(module, exports){} 將代碼包裹起來的時候,參數裏面會有 __webpack_require__,即 function(module, exports, __webpack_require__){} 
        return ParserHelpers.toConstantDependencyWithWebpackRequire(
          parser,
          strCode
        )(expr);
      } else {
        // ParserHelpers.toConstantDependency 會生成一個 ConstDependency,而且添加到當前的 module 中
        // ConstDependency.expression = "'test'",位置就是咱們代碼中 TEST 對應的位置
        return ParserHelpers.toConstantDependency(
          parser,
          strCode
        )(expr);
      }
    });
    複製代碼

    前面說過,ConstDependency 會對源碼對應內容進行替換。因此在後面代碼生成階段執行下面的操做

    ConstDependency.Template = class ConstDependencyTemplate {
    	apply(dep, source) {
        // 若是 range 是一個數字,則爲插入;若是是一個區間,則爲替換
    		if (typeof dep.range === "number") {
    			source.insert(dep.range, dep.expression);
    			return;
    		}
    		// 把源碼中對應的地方替換成了 dep.expression,即 "test" 
    		source.replace(dep.range[0], dep.range[1] - 1, dep.expression);
    	}
    };
    複製代碼

    這樣便實現了,對源碼中 TEST 的替換。

總結

相信經過上邊詳細的過程分析以及對應的一些demo的實踐,對於webpack是如何生成靜態文件的整個過程都已經瞭解了。但願在將來裏,你遇到相似的場景,且現有的生態插件不能知足需求的時候,是能夠本身動手搞定。

咱們想要深刻了解一個細節的最大動力就是來自於咱們的需求,在咱們開源的小程序框架mpx中就有不少不少上述靜態文件生成的大量應用。若是你感興趣,歡迎你們去了解、去使用、去共建。

額外的,滴滴前端技術團隊的團隊號也已經上線,咱們也同步了必定的招聘信息,咱們也會持續增長更多職位,有興趣的同窗能夠一塊兒聊聊。

相關文章
相關標籤/搜索