源碼篇(三):手寫webpack版mini源碼分析項目構建過程。附送簡版webpack源碼(6K字+)

適合人羣

本文適合2~4年的前端開發人員的進階,且對vue或者react搭配webpack有必定的經驗。若是webpack的基本使用都未了解,建議實踐後再看本文。javascript

本文是原創,均爲本人手寫。部分思惟, 借鑑了「宮小白」的webpack文章。文章結尾標註了「感謝」。css

再談談博客源碼的進度html

此前有人關心博客源碼的進度問題。筆者是打算利用額外的時間,把腦殼中的源碼都擼一份。即源碼篇,具體可參考以下:前端

序號 博客主題 相關連接
1 手寫vue_mini源碼解析 juejin.im/post/5f0326…
2 手寫react_mini源碼解析 juejin.im/post/5f154c…
3 手寫webpack_mini源碼解析(即本文) juejin.im/post/5f1793…
4 手寫jquery_mini源碼解析 juejin.im/post/5f1e38…
5 手寫vuex_mini源碼解析 預計下週
6 手寫vue_router源碼解析 預計8月
7 手寫diff算法源碼解析 預計8月
8 手寫promis源碼解析 預計8月
9 手寫原生js源碼解析(手動實現常見api) 預計8月
10 手寫react_redux,fiberd源碼解析等 待定,本計劃先出該文,整理有些難度
11 手寫koa2_mini 預計9月,前端優先

期間除了除了源碼篇,可能會寫一兩篇優化篇,基礎篇等。有興趣的歡迎持續關注。vue

講講廢話

當前主流的前端,不管angular,vue仍是react,都離不開的構建工具的編譯與協助。比較有名氣是有Webpack、Gulp 和 Grunt。java

Grunt筆者沒有實踐經驗,且相對另外二者比較偏冷門些,本文不作比較。node

來個簡單的高頻面試題:react

gulp與webpack有什麼區別?jquery

講述一下我的理解(僅供參考):webpack

gulp官方原話:gulp 將開發流程中讓人痛苦或耗時的任務自動化,從而減小你所浪費的時間、創造更大價值。他更像是一個流水線上的過濾器,定義多task(即便多個過濾條件),講咱們的文件進行轉譯。好比咱們將sass統一轉譯成css,或者文件的合併壓縮等,專一於task的執行。筆者開發一個門戶網站的時候,就將起引入,他精簡的 API 集,只需一個文件代碼,即幫咱們完成了一個項目針對某些文件的改造,且使用於多頁面的構建。

但單頁面的構建,不得不用webpack。webpack有人稱模塊打包機,更側重於模塊打包。本質是爲了打包js設計的,可是後期由於要針對整個前端項目,即支持loader對其餘文件進行編譯。

寫源碼以前的必備知識點

手寫loader

寫webpack的源碼,你首先得懂得loader跟plugins是如何去加載的。本文寫一個簡單的栗子,讓你們理解loader是個什麼東西。

那咱們就先看看大神寫的loader是個什麼東西。咱們以less-loader爲栗子:

npm install less-loader安裝後,咱們直接進入node_moudels/less-loader/package.json

查看main變量 咱們能夠看到: "main": "dist/cjs.js",

那麼咱們再看dist/cjs.js又指向index。他的源碼在index.js文件中,咱們來看index.js的代碼:

圖解中,咱們能夠分析到,他的首個參數,便是接受到文本內容。loaderContext便是將less轉換爲css的過程。可是整個loader,其實能夠很清晰到看到。接受到source,而後經過轉換,再暴露出去。按着這個思路,咱們手寫一個loader應該不難。

咱們定義一個簡單的需求:

  • 正式環境中,去除項目中全部的console.log()

  • 最後再打印出一個,關注博主博客

    const join = require('path').join; const fs = require('fs');

    function wzJsLoader(source) {

    /*過濾輸出*/
      const mode = process.env.NODE_ENV === 'production'
      if( mode ){//正式環境
          source = source.replace(/console.log\(.*?\);/ig, value => "" );
      }
    
      /* 打上我的標記 */
      const tip = "關注博主博客:https://juejin.im/user/5e8fddc9e51d4546be39a558/posts ";
      source  =  source + `console.log("${tip}");`
      return source;
    複製代碼

    };

    module.exports = wzJsLoader;

而後再咱們的配置文件中,配置上上述的loader。

{
    test: /\.js$/, //js文件加載器
    //loader: 'happypack/loader?id=happyBabel',
    exclude: /node_modules/,
    use: [
        {
          loader: 'babel-loader?cacheDirectory=ture',
          options: {
            presets: ['@babel/preset-env']
          }
        },
        {
            loader: require.resolve("./myWebPackConfig/wz-js-loader"),
            options: { name: __dirname  },
        },
    ]
  },
複製代碼

那麼,咱們就完成咱們的第一個手寫loader。

其實plugins也同樣的原理,這裏不作重複講解,本文demo中會提到plugins。

tapable

tapable一些前端朋友(沒了解過的webpack)可能很陌生,包括筆者以前也是,直到了解到webpack時纔去理解這個概念。咱們首先要了解他是個什麼東西,再去寫webpack可能更好了解一下。這裏分爲簡版說明,跟代碼解釋。如知識想了解webpack的執行機制,可能看簡版說明便可。

簡版說明

同步:

名稱 解釋
SyncHook 同步執行,無需返回值
SyncBailHook 同步執行,無需返回值,返回undefined終止
SyncWaterfallHook 同步執行,上一個處理函數的返回值是下一個的輸入,返回undefined終止
SyncLoopHook 同步執行, 訂閱的處理函數有一個的返回值不是undefined就一直循環它

異步:

名稱 解釋
AsyncSeriesHook 異步執行,無需返回值
AsyncParallelHook
AsyncSeriesBailHook 異步執行,無需返回值,返回undefined終止
AsyncSeriesWaterfallHook 異步執行,上一個處理函數的返回值是下一個的輸入,返回undefined終止

看了tapable具體的幾個方法的做用,大概瞭解,那麼爲何webpack要扯上他呢? 由於webpack的構建流程,不少的跟着tapable的執行順序方案有關。

SyncHook就最好理解,同步執行,咱們不一樣的plugins之間,只須要同步執行下去便可。這時候咱們能夠利用SyncHook按順序同步執行SyncHook執行咱們的plugins便可。那麼另外的場景是什麼?

HappyPack知道是什麼麼?例如開啓多個線程,同步打包。是否是符合AsyncSeriesHook的場景。

再舉個最簡單的栗子: 咱們生成文件時候,是否是有一個hash值呢?假設這個hash值,有多個插件用到,咱們是否是應該在第一個方法生成一個hash,而後後續用到的方法,讀取到這個值。那麼,這個場景,SyncWaterfallHook是否派上用場?此時的hash一直傳遞下去,就完成了不一樣plugins之間的hash值傳遞。

仍是不明白?案例實現會根據這個栗子實現。 若是簡版本的介紹,仍是一臉懵逼,建議看一下下列源碼。

代碼解釋

代碼的解釋,是爲了讓你更瞭解tapable。若是你只想知道webpack,能夠直接忽略。

tapable這裏,他自己是暴露了8個方法(上述)。可是咱們簡版沒有那麼複雜,咱們講解一下下述用到SyncHook跟AsyncSeriesWaterfallHook。

SyncHook

SyncHook咱們大體理解,他有一個tap將任務添加的內部的執行隊列中。此時,若是調用call方法,將執行同步執行全部tap過的方法。具體能夠經過下述代碼理解:

class SyncHook {
	constructor(args){//args => ['name']
		this.tasks = [];
	}
	call(...args){
		this.tasks.forEach((task) => task(...args))
	}
	tap(name,task){
		this.tasks.push(task);
	}
}

let hook = new SyncHook(['name']);
hook.tap('plugins_0',function(name){
	console.log('plugins_0',name)
})
hook.tap('plugins_1',function(name){
	console.log('plugins_1',name)
})
hook.call('hello word');
複製代碼
AsyncSeriesWaterfallHook

AsyncSeriesWaterfallHook咱們大體理解,他有一個tap將任務添加的內部的執行隊列中。此時,若是調用call方法,將執行同步執行全部tap過的方法,且需上一個返回值,給下一個函數隊列。具體能夠經過下述代碼理解:

class SyncWaterfallHook {
    //通常是能夠接收一個數組參數的,可是下面沒有用到
    constructor(args) {
        this.tasks = []
    }
    tap(name, cb) {
        let obj = {}
        obj.name = name
        obj.cb = cb
        this.tasks.push(obj)
    }
    call(...arg) {
        let [first, ...others] = this.tasks
        let ret = first.cb(...arg)
        others.reduce((pre, next) => {
            return next.cb(pre)
        }, ret)
    }
}
複製代碼

AST語法樹

該章節,建議移步文章:www.jianshu.com/p/019d449a9…

手寫webpack過程

1)基本架子的搭建

手動新建

新建一個package.json,入口文件咱們制定了bin/index.js

{
        "name": "zwzpack",
        "version": "0.1.0",
        "private": true,
        "scripts": {
            "start": "node  ./bin/index.js"
        },
        "devDependencies": {},
        "dependencies": {
        }
    }
複製代碼

再新建bin/index.js表示咱們的入口文件,參考咱們webpack常規寫法,引入自寫的webpack的lib包,再引入咱們的配置文件webpack.config.js。啓動他

const path = require('path')
    
    const config = require(path.resolve('webpack.config.js'))
    const WebpackCompiler = require('../lib/WebpackCompiler.js')
    
    const webpackCompiler = new WebpackCompiler(config)
    webpackCompiler.run();
複製代碼

根目錄定義webpack.config.js,咱們首先定義一個webpack的基本配置文件。

module.exports = {
        mode: 'development',
        entry: './src/index.js',
        output: {
            filename: 'main.js',
            path: path.join(__dirname, './dist')
        },
        module: {
            rules: []
        },
        plugins: [
        ]
    }
複製代碼

public/index.html,做爲咱們測試webpack是否打包成功測試Html,引入打包後的js:

<html>
    <head>
        <meta charset="UTF-8">
        <title>我的webpack</title>
        <meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" />    
    </head>
    <script type="text/javascript" src="./main.js"  ></script>
    <body>
        <div class="my_page" >
            <h1 class="">歡迎小夥子小姑涼!</h1>
            <div class="">歡迎關注掘金:<a target="_blank" href="https://juejin.im/user/5e8fddc9e51d4546be39a558">點擊</a></div>
            <div class="">手寫mini_webpack教程:<a  target="_blank"  href="https://juejin.im/editor/drafts/5f1793716fb9a07e8b215a62">點擊</a></div>
        </div>
    </body>
    <style>
        
    </style>
</html>
複製代碼

WebpackCompiler是咱們的核心編譯類,下方重點講解。

class WebpackCompiler {
        constructor(config) {
            this.config = config;
        }
        
        run() {
            //編譯開始
        }
    }

module.exports = WebpackCompiler;
複製代碼

就這樣,完成咱們基本的架子,此部分暫時未涉及wepback的編譯過程。

快速拷貝

直接拷貝:github.com/zhuangweizh…

2) 新建編譯模板

webpack的最終目的,是爲了生成js文件。(暫不考慮其餘文件因素) 咱們要本身手寫一個webpack生成的js文件,那麼首先,咱們須要瞭解官方webpack生成的js文件究竟是個啥?

咱們新建一個官方webpack項目(百度一下很簡單)

新建test.js,跟testChildren.js進行編譯:

test.js:

const obj = require("./testChildren.js");
module.exports = { name: "weizhan",  obj }
複製代碼

testChildren.js

module.exports = { age: 18}
複製代碼

咱們來觀察編譯結果:

圖1:

圖2:

截圖你們還看不懂生成了什麼的話,我再簡化一下官方最後生成的文件,其中有變化的部分爲:

((function(modules) {
...
//固定代碼
    return __webpack_require__(__webpack_require__.s = "<%-文件路徑 %>");
})

 "./src/js/test.js": (function (module, exports,__webpack_require__) {
    eval(` const obj = __webpack_require__("./testChildren.js");  test1.js 剩餘文件代碼`);
}),
"./src/js/testChildren.js": (function (module, exports,__webpack_require__) {
    eval(`testChildren.js 文件代碼`);
}),
複製代碼

咱們能夠觀察到,生成的js文件中,將生成原來的js文件,套入一個模板中,而咱們本身js代碼給嵌入到結尾部分。 觀察我簡化後的文件,咱們能夠看到,webpack官方生成的js文件,他的生成規律:

  • 1.先快速拷貝一個編譯模板,中間嵌入咱們本身寫的代碼
  • 2.須要暴露咱們的文件路徑
  • 3.會將咱們的js文件中的require轉化爲__webpack_require__,而後將全部require的js(包含本身)都用嵌入到function中的eval中。

瞭解完官方的生成規矩後,咱們來模擬這個過程:

  • 根據規則1,首先有一個固定的編譯模板,咱們能夠將模板內容先寫在一個js中,可是爲方便渲染嵌入,咱們借用ejs的快速渲染模板
  • 根據規則2,咱們須要知道咱們的文件路徑,ok,咱們定義一個變量entryPath來標識文件路徑
  • 根據規則3,須要把每一個模塊的代碼到嵌入文件中,一樣的咱們要以路徑爲Key。那咱們用modules變量來做爲數組。(且原文件還有require轉化爲__webpack_require__)

根據規則,咱們新建lib/main.ejs文件:

(function(modules) {
...
//固定代碼
    return __webpack_require__(__webpack_require__.s = "<%- entryPath %>");
})

({
    <% for(let key in modules){ %>
        "<%- key %>": (function (module, exports,__webpack_require__) {
            eval(`<%-modules[key] %>`);
    }),
     <% } %>
});
複製代碼

3) 獲取模板參數

生成JS的模板有了,那麼咱們剩餘的問題就是,怎麼拿到modules跟entryPath的問題。

entryPath好處理,用過webpack的都知道,入口文件便是webpack.config.js的entry變量。 那麼modules呢?首先咱們須要讀取原來的文件內容。上文提到,兩個重點:

  • 1.將require替換成__webpack_require__
  • 2.再全部的頁面路徑,跟頁面內容封裝一個數組。

需求便是如此,我首先想到的時候,寫一個正則表達式,將require替換成__webpack_require__,且用node的fs,將文件內容讀取。可是後續發現,兼容性實在是太爛了,空白符,註釋等,都會成爲頭疼的地方。

後續發現大神的解析方法很是贊,用的是babylon 轉換 AST轉換的方法,咱們來看看代碼:

// babylon主要把源碼轉成ast。Babylon 是 Babel 中使用的 JavaScript 解析器。
// @babel/traverse 對ast解析遍歷語法樹 負責替換,刪除和添加節點
// @babel/types 用於AST節點的Lodash-esque實用程序庫
// @babel/generator 結果生成

const babylon = require('babylon')
const traverse = require('@babel/traverse').default;
const type = require('@babel/types');
const generator = require('@babel/generator').default

// 根據路徑解析源碼
parse(source, parentPath) {
    let ast = babylon.parse(source)//
    // 用於存取依賴
    let dependencies = []
    traverse(ast, {//對ast解析遍歷語法樹 負責替換,刪除和添加節點
        CallExpression(p) {
            let node = p.node
            if (node.callee.name === 'require') {
                node.callee.name = '__webpack_require__';//將require替換成__webpack_require__
                const moduledName = './' + path.join(parentPath, node.arguments[0].value )
                dependencies.push(moduledName);//記錄包含的requeir的名稱,後邊須要遍歷替換成源碼
                node.arguments = [type.stringLiteral(moduledName)] // 源碼替換
            }
        }
    })
    let sourceCode = generator(ast).code
    return { sourceCode, dependencies };
}
複製代碼

AST的內容若是不清楚,上述已經給了連接,先繼續步驟繼續下去,後續能夠根據連接再補充一下本身。 js轉換的方法已經抒寫完畢,咱們來寫轉換的入口。

// 編譯生成完成的main文件,完成遞歸
buildMoudle(modulePath, isEntry) {
    const source = this.getSourceByPath(modulePath);//根據路徑拿到源碼
    const moduleName = './' + path.relative(this.root, modulePath);//轉換一下路徑名稱
    const { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName))//根據路徑拿到源碼,以及源碼中已經require的文件名稱數組
    this.modules[moduleName] = sourceCode;// 每一個模塊的代碼都經過路徑爲Key,存入到modules對象中
    dependencies.forEach(item => {  // 遞歸須要轉換的文件名稱
        this.buildMoudle(path.resolve(this.root, item));////再對應的文件名稱,替換成對應的源碼
    })
}

getSourceByPath(modulePath){
    let content = fs.readFileSync(modulePath, 'utf8')
    return content
}
複製代碼

此時,若是程序執行完畢,咱們就能夠拿到咱們的modules參數。

4) 將模板生成js文件輸出

這個章節很好理解,根據文件路徑,拿到模板內容,模板變量的值,輸出到指定位置便可。

//輸出文件
outputFile() {
    let templateStr = this.getSourceByPath(path.join(__dirname, 'main.ejs'));  // 拿到步驟1寫好的模板
    let code = ejs.render(templateStr, {
        entryPath: this.entryPath,
        modules: this.modules,
    })// 填充模板數據
    let outPath = path.join(this.config.output.path, this.config.output.filename)// 拿到輸出地址
    fs.writeFileSync(outPath, code )// 寫入
}
複製代碼

咱們改一下咱們的編譯類入口,在run方法執行他們:

class WebpackCompiler {
        constructor(config) {
            this.config = config;
            this.modules = {}
            this.root = process.cwd() //當前項目地址      
            this.entryPath = './' + path.relative(this.root, this.config.entry);
        }
        
        run() {
            this.buildMoudle( this.entryPath )
            this.outputFile();
        }
}
複製代碼

此時,執行咱們咱們的基本架子,已經能夠輸出webpack的編譯後的js文件。

5)嵌入loader

普通的js文件,已經抒寫完畢。那咱們來看看怎麼嵌入咱們的loader呢?看完了"寫源碼以前的必備知識點",咱們應該明白,loader便是將咱們的對應的文件後綴,經過loader的轉移成咱們的js文件支持的語法。

咱們作一下前期工做。

  • 新建一個新的loader文件:

    const less = require('less') function loader(source) { let css = '' less.render(source, function(err, output) { css = output.css })

    css = css.replace(/\n/g, '\\n')
      let style = `
      let style = document.createElement('style')
      style.innerHTML = \n${JSON.stringify(css)}
      document.head.appendChild(style)
      `
      return style
    複製代碼

    } module.exports = loader;

  • 具體less的轉換規則比較複雜我就不折騰了。這裏借用一下官方的less,賦值到js中。修改一下入口:webpack.config.js的module:

    module: {
          rules: [{
              test: /\.less$/,
              use: [path.join(__dirname, './lib/loader/less-loader.js')]
          }]
      },
    複製代碼
  • 再在咱們的src文件中新建test.less: body{ text-align: center; .my_page{ margin-top: 50px; line-height: 50px; } }

  • 在測試文件(src/index.js)引入他:

    require("./test.less");
      const index2Obj = require("./index2.js");
      alert("index文件告訴你:小夥子很帥"  );
      alert("index2文件告訴你:" + index2Obj.value );
      module.exports = {}
    複製代碼

作完前期工做,咱們來實現編譯過程:

咱們上述獲取源文件代碼用了:

getSourceByPath(modulePath){
    let content = fs.readFileSync(modulePath, 'utf8')
    return content
}
複製代碼

loader的實現,就是在拿到源文件後,經過rules的規則匹配後綴,多作一層轉換:

getSourceByPath(modulePath) {
    let content = fs.readFileSync(modulePath, 'utf8')
    // 事先拿module中的匹配規則與路徑進行匹配
    const rules = this.config.module.rules
    for (let i = 0; i < rules.length; i++) {
        let { test, use } = rules[i]
        let len = use.length
        if (test.test(modulePath)) {
            function changeLoader() {
                // 先拿最後一個
                let loader = require(use[--len])//倒敘執行
                content = loader(content)
                if (len > 0) {
                    changeLoader()
                }
            }
            changeLoader()
        }
    }
    return content
}
複製代碼

用了閉包,是由於一個後綴可能對應多個loader,因此這裏寫了循環執行。 再執行一次npm run start,此時你會發現,js中已經包含了less文件的內容。此時咱們完成了loader的嵌入。

6)嵌入webpack的生命週期

理論上咱們應該講plugins的手寫,可是plugins有一個問題,plugins能夠設置在不一樣的編譯階段。例如在編譯前,作什麼?編譯中,需作什麼?這涉及到webpack的生命週期。咱們來處理一下生命週期。

咱們上述描述了tapable,SyncHook咱們大體理解,他有一個tap將任務添加的內部的執行隊列中,而後最後經過執行call方法,一次執行他們。

咱們的plugins的執行也是如何,咱們能夠將咱們的每一個plugin經過tap任務放置在SyncHook中,等時機到了,調用call方法便可。

咱們給咱們的webpack定義五個生命週期,並在run方法適當的時機嵌入他們:

class WebpackCompiler {
        constructor(config) {
            this.config = config;
            this.modules = {}
            this.root = process.cwd() //當前項目地址      
            this.entryPath = './' + path.relative(this.root, this.config.entry);
            this.hooks = {
                entryInit: new tapable.SyncHook(),
                beforeCompile: new tapable.SyncHook(),
                afterCompile: new tapable.SyncHook(),
                afterPlugins: new tapable.SyncHook(),
                afteremit: new tapable.SyncWaterfallHook(['hash']),
            }
        }
        
        run() {
            this.hooks.entryInit.call(); //啓動項目
            this.hooks.beforeCompile.call();  //編譯前運行
            this.buildMoudle( this.entryPath )
            this.hooks.afterCompile.call( ); //編譯後運行
            this.outputFile();
            this.hooks.afterPlugins.call( );//執行完plugins後運行
            this.hooks.afteremit.call( );//結束後運行
        }
}
複製代碼

該webpack生命週期定義結束,下邊咱們手寫plugins嵌入到生命週期中。

7)嵌入plugins

定義一個需求: 1)啓動時,添加打印:前端小夥子,編譯開始咯。 2)編譯前:清空原來的dist文件夾內容 3)輸出文件後:將咱們的生成文件main.js,更名main.${hash}.js,並在index.html正確引入該文件。

  • 咱們手寫一個InitPlugin,該plugin是在啓動時執行,即tap到entryInit週期中:

class InitPlugin { run(compiler) { // 將的在執行期放到剛開始解析入口前 compiler.hooks.entryInit.tap('Init', function(res) { console.log(前端小夥子,編譯開始咯。); }) } }

  • 清空dist,在編譯前:

class CleanDistPlugins { run(compiler) { // 將自身方法訂閱到hook以備使用 //假設它的運行期在編譯完成以後 compiler.hooks.beforeCompile.tap('CleanDistPlugins', function(res) { delFileFolderByName('./dist/'); }) } }

  • 編譯後:

重命名文件:

class JsCopyPlugins {
    run(compiler) {
        compiler.hooks.afterPlugins.tap('JsCopyPlugins', function(res) {
            const ranNum = parseInt( Math.random() * 100000000 );
            fs.copyFile('./dist/main.js',`./dist/main.${ranNum}.js`,function(err){
                if(err) console.log('獲取文件失敗');
                delFileByName('./dist/main.js');
            })
            console.log("從新生成js成功" );
            return ranNum;
        })
    }
}
複製代碼

修改html的js引入:

class HtmlReloadPlugins {
    run(compiler) {
        compiler.hooks.afterPlugins.tap('HtmlReloadPlugins', function(res) {
            let content = fs.readFileSync('./public/index.html', 'utf8')
            content = content.replace('main.js', `main.${res}.js`);
            fs.writeFileSync( './dist/index.html', content)
        })
    }
}
複製代碼

因爲「重命名文件」「修改html的js引入」須要用到同一個hash值,上述SyncWaterfallHook能夠傳遞值,SyncHook則無傳遞值,這就是我爲何將最後一步改成SyncWaterfallHook的緣由

全部的pulgins都寫好了,咱們來寫webpack如何執行pulgins的過程:

其實生命週期幫咱們實現了SyncHook的start函數。咱們只須要將plugins註冊到編譯類中接口。plugins中已經將方法 tap到SyncHook中,咱們只須要執行plugins便可。方法很簡單:

class WebpackCompiler {
    constructor(config) {
        this.config = config
        //...省略
        
        const plugins = this.config.plugins
        if (Array.isArray(plugins)) {
            plugins.forEach(item => {
                // 每一個均是實例,調用實例上的一個方法便可,傳入當前Compiler實例
                item.run(this)
            })
        }

    }
}
複製代碼

至此,咱們已經完成了webpack_mini的手寫。

簡版webpack源碼

給於核心類WebpackCompiler的完整代碼,如還不清晰,建議導下項目查看,連接:github.com/zhuangweizh…

WebpackCompiler源碼:

const path = require('path')
const fs = require('fs')
const { assert } = require('console')
    // babylon  將源碼轉成ast Babylon 是 Babel 中使用的 JavaScript 解析器。
    // @babel/traverse 對ast解析遍歷語法樹
    // @babel/types 用於AST節點的Lodash-esque實用程序庫
    // @babel/generator 結果生成

const babylon = require('babylon')
const traverse = require('@babel/traverse').default;
const type = require('@babel/types');
const generator = require('@babel/generator').default
const ejs = require('ejs')
const tapable = require('tapable')

class WebpackCompiler {
    constructor(config) {
        this.config = config
        this.modules = {}
        this.root = process.cwd() //當前項目地址      
        this.entryPath = './' + path.relative(this.root, this.config.entry);
        this.hooks = {
            entryInit: new tapable.SyncHook(),
            beforeCompile: new tapable.SyncHook(),
            afterCompile: new tapable.SyncHook(),
            afterPlugins: new tapable.SyncHook(),
            afteremit: new tapable.SyncWaterfallHook(['hash']),
        }
        const plugins = this.config.plugins
        if (Array.isArray(plugins)) {
            plugins.forEach(item => {
                // 每一個均是實例,調用實例上的一個方法便可,傳入當前Compiler實例
                item.run(this)
            })
        }

    }

    // 獲取源碼
    getSourceByPath(modulePath) {
        // 事先拿module中的匹配規則與路徑進行匹配
        const rules = this.config.module.rules
        let content = fs.readFileSync(modulePath, 'utf8')
        for (let i = 0; i < rules.length; i++) {
            let { test, use } = rules[i]
            let len = use.length

            // 匹配到了開始走loader,特色從後往前
            if (test.test(modulePath)) {
                function changeLoader() {
                    // 先拿最後一個
                    let loader = require(use[--len])
                    content = loader(content)
                    if (len > 0) {
                        changeLoader()
                    }
                }
                changeLoader()
            }
        }
        return content
    }

    // 根據路徑解析源碼
    parse(source, parentPath) {
        let ast = babylon.parse(source)//
        // 用於存取依賴
        let dependencies = []
        traverse(ast, {//對ast解析遍歷語法樹 負責替換,刪除和添加節點
            CallExpression(p) {
                let node = p.node
                if (node.callee.name === 'require') {
                    node.callee.name = '__webpack_require__';//將require替換成__webpack_require__
                    const moduledName = './' + path.join(parentPath, node.arguments[0].value )
                    dependencies.push(moduledName);//記錄包含的requeir的名稱,後邊須要遍歷替換成源碼
                    node.arguments = [type.stringLiteral(moduledName)] // 源碼替換
                }
            }
        })
        let sourceCode = generator(ast).code
        return { sourceCode, dependencies };
    }

    // 構建模塊
    buildMoudle(modulePath) {
        const source = this.getSourceByPath(modulePath);//根據路徑拿到源碼
        const moduleName = './' + path.relative(this.root, modulePath);//轉換一下路徑名稱
        const { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName))//根據路徑拿到源碼,以及源碼中已經require的文件名稱數組
        this.modules[moduleName] = sourceCode;// 每一個模塊的代碼都經過路徑爲Key,存入到modules對象中
        dependencies.forEach(item => {  // 遞歸須要轉換的文件名稱
            this.buildMoudle(path.resolve(this.root, item));////再對應的文件名稱,替換成對應的源碼
        })
    }

    //輸出文件
    outputFile() {
        let templateStr = this.getSourceByPath(path.join(__dirname, 'main.ejs'));  // 拿到步驟1寫好的模板
        let code = ejs.render(templateStr, {
            entryPath: this.entryPath,
            modules: this.modules,
        })// 填充模板數據
        let outPath = path.join(this.config.output.path, this.config.output.filename)// 拿到輸出地址
        fs.writeFileSync(outPath, code )// 寫入
    }

    run() {
        this.hooks.entryInit.call(); //啓動項目
        this.hooks.beforeCompile.call();  //編譯前運行
        this.buildMoudle( this.entryPath )
        this.hooks.afterCompile.call( ); //編譯後運行
        this.outputFile();
        this.hooks.afterPlugins.call( );//執行完plugins後運行
        this.hooks.afteremit.call( );//結束後運行
    }

}

module.exports = WebpackCompiler;
複製代碼

文章結尾

感謝

筆者本來即將完成mini_webpack源碼,後續發現有人的源碼核心原理的實現,寫的比我本來的要好一些,因此後期我作了改動。本文部分原理跟部分源碼借鑑了「宮小白」,他的原文連接是:juejin.im/post/5f0494…

此外, AST語法樹不清晰建議文章:www.jianshu.com/p/019d449a9…

計劃

本計劃將webpack如何優化,寫在本文中。發現文章已經差很少6K+字,太長小夥伴們也看不完,後續會單獨一篇文章基於本文描述。

筆者也會繼續寫本身的mini框架源碼,有興趣繼續關注:

序號 博客主題 相關連接
1 手寫vue_mini源碼解析 juejin.im/post/5f0326…
2 手寫react_mini源碼解析 juejin.im/post/5f154c…
3 手寫webpack_mini源碼解析(即本文) juejin.im/post/5f1793…
4 手寫jquery_mini源碼解析 juejin.im/post/5f1e38…
5 手寫vuex_mini源碼解析 預計下週
6 手寫vue_router源碼解析 預計8月
7 手寫diff算法源碼解析 預計8月
8 手寫promis源碼解析 預計8月
9 手寫原生js源碼解析(手動實現常見api) 預計8月
10 手寫react_redux,fiberd源碼解析等 待定,本計劃先出該文,整理有些難度
11 手寫koa2_mini 預計9月,前端優先
相關文章
相關標籤/搜索