本身寫一個Webpack

Webpack學習概論

在使用過Webpack後,它強大的的靈活性給我留下了深入印象,經過Plugin和Loader幾乎能夠隨意擴展功能,因此決定探究Webpack的實現原理,學習做者的編程思想。node

可是在學習源碼過程當中仍是遇到了挺大困難,一是它的插件系統設計的錯綜複雜,剛開始看容易被繞暈,另外是它功能實現覆蓋的場景廣,有不少內置功能不太熟悉。所以在這記錄下學習的過程,將Webpack實現的精華內容提取出來,供後續學習參考。webpack

由於Webpack的代碼量也不算少,並且比較繞,若是光看代碼會比較枯燥。因此決定以本身實現一個簡易Webpack爲目標,分步探索實現細節,從構建運行到實現一個能打包代碼的工具。爲了簡化邏輯,不會徹底像Webpack同樣實現,如下部分是差別較大的地方:web

  • 使用TS實現:由於方便看類型。
  • 不使用Webpack的插件機制:即不會用Tapable實現Hooks,由於看得太麻煩。

如下是完成計劃,但願能堅持 😄typescript

基礎概念

首先咱們須要瞭解一些基礎的Webpack概念,Webpack的構建流程基本是圍繞如下概念進行:shell

  • context: 絕對路徑目錄,默認使用當前目錄,加載文件以該目錄爲基礎。
  • Entry: Webpack分析文件的入口點,指定了入口文件後,Webpack會遞歸分析出這個文件下的全部依賴文件,供後續輸出。
  • Module: Webpack將全部文件都當作模塊,包含了文件的全部信息。
  • Plugin: Webpack的運行過程就是一個個插件相互調用處理的過程,插件會在編譯階段的各個生命週期中被調用。
  • Loader: 在加載文件後,解析文件前,對文件內容做自定義處理,如對文件內容替換刪除等操做。
  • Chunk: 封裝了Module,是模塊依賴和輸出模版代碼的橋樑

打包流程分析

初始化流程 webpack.js

  • WebpackOptionsDefaulter合併默認配置,在webpack裏已經默認了部分配置,如context設置爲當前目錄等。
  • 建立編譯器Compiler
  • 加載自定義插件
  • WebpackOptionsApply將選項設置給compiler,並加載各類默認插件,如用於引導入口的EntryOptionPlugin插件,加載js文件解析的JavascriptModulesPlugin
  • 運行compiler

初始化編譯器 Compiler.js

  • 初始化模塊工廠NormalModuleFactoryContextModuleFactory,模塊工廠主要用於在後續建立和初始化模塊
  • 建立編譯工具Compilation,在這裏會經過鉤子調用各類插件來初始化編譯工具,如爲入口模塊添加解析器,爲js類型文件添加解析器,添加模版處理方法等
  • 調用make鉤子執行EntryPlugin,運行compilation.addEntry進入模塊解析

模塊解析流程 Compilation.js

  • 調用ModuleFactory建立入口模塊 / 建立依賴模塊
    • 解析資源路徑,加載原始文件
    • 加載須要的Loader
    • 加載模塊解析器
  • 調用buildModule解析模塊,輸出依賴列表
    • 運行Loader
    • Parser解析出AST
    • walkStatements解析出依賴
  • 調用addModuleDependencies遞歸建立依賴模塊

模塊輸出流程 Compilation.js

  • 建立ChunkGroupChunk,根據模塊依賴解析出ChunkGraph
  • 優化ChunkGraph
  • Template根據Chunk建立輸出內容
  • 輸出文件

實現一個簡易版Webpack

示例代碼

本次咱們實現的效果是將兩個簡單文件打包成一個js,而且能夠在瀏覽器運行,採用Commonjs模塊化,咱們再實現一個簡單的loader,將代碼中的log轉換爲warn:編程

// example/index.js
const inc = require('./increment')
const dec = require('./decrement')
console.log(inc(8))
console.log(dec(8))

// example/increment.js
exports.default = function(val) {
    return val + 1;
};

// example/decrement.js
exports.default = function(val) {
    return val - 1;
};

// example/loader.js
module.exports = function loader(source) {
    return source.replace(/console.log/g, 'console.warn')
}
複製代碼

環境搭建

代碼使用typescript編寫,因此先安裝typescript相關依賴json

# typescript
"typescript": "^3.7.4"
# 幫助識別node相關的類型定義
"@types/node": "^13.1.4",
# 快速編譯運行ts項目
"ts-node": "^8.5.4",
複製代碼

在package.json添加運行腳本瀏覽器

"start": "npx ts-node index.js",
複製代碼

入口文件

入口文件就是咱們運行Webpack的地方,這裏咱們定義一些簡單的配置,包括編譯入口文件entry,輸出文件bundle.js,還有自定義loader。引入咱們的核心編譯器Compiler,傳入配置運行。bash

// index.js
const path = require('path')
const Compiler = require('./lib/Compiler').default

const options = {
    entry: path.resolve(__dirname, './example/index.js'),
    output: path.resolve(__dirname, './dist/bundle.js'),
    loader: path.resolve(__dirname, './example/loader.js')
}

const compiler = new Compiler(options)
compiler.run()
複製代碼

核心編譯器Compiler

Compiler建立

編譯器負責封裝打包過程,輸入是用戶配置,輸出是打包結果,對外提供一個run函數啓動編譯。
入口模塊是編譯器解析的起點,從入口文件開始遞歸加載模塊文件,這裏咱們沒有遞歸解析只簡單地解析了入口文件的依賴,收集到全部依賴後渲染出合併後的代碼,最後寫出到文件。模塊化

// lib/Compiler.ts
import * as fs from 'fs'
import * as path from 'path'
import Module from './Module'
export default class Compiler {
    options: any
    constructor(options: any) {
        this.options = options
    }
    run() {
        // 建立入口模塊
        const name = path.basename(this.options.entry)
        const entryModule = this.createModule(name, this.options.entry)
        // 解析依賴模塊
        const dependencies = this.parse(entryModule.source)
        this.addModuleDependencies(entryModule, dependencies)
        // 渲染出結果
        const source = this.renderTemplate(entryModule)
        // 寫入文件
        this.write(source, this.options.output)
    }
    // ...
}
複製代碼

建立模塊

Webpack中將一切資源都當作模塊,因此咱們要解析的一個個js文件也是用模塊表示,首先先定義一個Module類來表示模塊:

// lib/Module.ts
export default class Module {
    id: string // 模塊惟一標誌,這裏咱們用文件名錶示
    source: string // 文件源碼
    absPath: string // 文件絕對路徑
    dependencies: Module[] // 文件全部依賴
}
複製代碼

有了模塊類咱們就能夠封裝建立模塊功能了,除了初始化數據外,咱們還在這裏將文件讀取出來,而後使用loader對源碼進行處理。

// Compiler.createModule
createModule(id: string, absPath: string) {
    const module = new Module()
    module.id = id
    module.absPath = absPath
    module.source = fs.readFileSync(absPath).toString()
    module.dependencies = []

    const loader = require(this.options.loader)
    module.source = loader(module.source)

    return module
}
複製代碼

分析模塊依賴

webpack的基本功能就是將模塊化代碼打包成瀏覽器可運行代碼。因爲瀏覽器不能直接識別模塊化代碼,就須要咱們將多個文件按依賴順序合併成一個文件,因此識別出模塊依賴是咱們要解決的第一個問題。
咱們使用CommonJS來組織代碼,就要在代碼中識別出require這樣的關鍵字,因此這裏咱們簡單地使用正則匹配,通過循環匹配後,就能取出包含require('xxx')中的依賴項了。
用正則匹配還須要考慮註釋換行等麻煩的校驗。Webpack則是將代碼解析成AST樹來分析依賴,AST裏包含了更豐富的信息且不容易出錯。

// Compiler.parse
parse(source: string) {
    const dependencies: any[] = []
    let result = []
    let reg = /require[('"].([^']*)[)'"]./g
    while((result = reg.exec(source))) {
        dependencies.push({
            id: result[1]
        })
    }
    return dependencies
}
複製代碼

建立依賴模塊

在這裏咱們已經獲取到了父模塊和他的全部依賴項,此時咱們就要將依賴也轉成一個個模塊,由於一個依賴也是一個文件,一個文件在webpack中就是一個模塊。

// Compiler.addModuleDependencies
addModuleDependencies(module: Module, dependencies: any[]) {
    const dir = path.dirname(module.absPath)
    for (const dependent of dependencies) {
        const depModule = this.createModule(dependent.id, path.resolve(dir, dependent.id) + '.js')
        module.dependencies.push(depModule)
    }
    return
}
複製代碼

渲染模版

上面說了,要想將模塊化代碼轉換成在瀏覽器環境下執行的代碼,咱們應該將全部將要執行的代碼合併在一塊兒,用一個js文件給瀏覽器執行,並且瀏覽器不識別的CommonJS語法也須要咱們給打上補丁,讓瀏覽器能正確識別requireexports,因此咱們的目標代碼應該長這樣:

(function (modules) {
    function require(moduleId) {
        var module = {
            id: moduleId,
            exports: {}
        }
        modules[moduleId](module, require)
        return module.exports;
    }
    require("index.js");
})({
    'index.js': (function (module, require) {
        const inc = require('./increment')
        const dec = require('./decrement')
        console.warn(inc(8))
        console.warn(dec(8))
    }),
    './increment': (function (module, require) {
        module.exports = function (val) {
            return val + 1;
        };
    }),
    './decrement': (function (module, require) {
        module.exports = function (val) {
            return val - 1;
        };
    }),
})
複製代碼

當即執行函數傳入合併後的全部代碼,並建立了require函數來加載合併後的對象,在咱們的代碼中遇到了require就會帶入相應的函數,只要初始化後調用一次入口模塊代碼就能執行了。能夠看到除了傳參的代碼,其餘都是固定的模版代碼,參數代碼咱們能夠用前面解析的依賴來建立。

// Compiler.renderTemplate
renderTemplate(module: Module) {
    const buffer = []
    buffer.push(`(function(modules) { function require(moduleId) { var module = { id: moduleId, exports: {} } modules[moduleId](module, require) return module.exports; } require("${module.id}"); })({`)

    buffer.push(`'${module.id}': (function(module, require) { \n ${module.source} \n }),`)
    for (const dependent of module.dependencies) {
        const src = `(function(module, require) { \n ${dependent.source.replace('exports.default', 'module.exports')} \n })`
        buffer.push(`'${dependent.id}':${src},`)
    }

    buffer.push(`})`)
    return buffer.reduce((pre, cur) => pre + cur, '')
}
複製代碼

輸出文件

輸出了模版代碼後,只要調用系統方法將其輸出到硬盤就能夠了,很是簡單

// Compiler.write
write(source: string, output: string) {
    fs.writeFileSync(output, source)
}
複製代碼
相關文章
相關標籤/搜索