webpack系列之-原理篇

行走的柯南發表於2018-05-05

本系列將會從原理、開發、優化、對比四個方面給大家介紹webpack的工作流程。【預設是以webpack v3為例子】

儲備知識

CommonJS 規範

// 模組引入
let moduleA = require('./a.js')

// 模組匯出
module.exports = () => {}

複製程式碼

es6規範

// 模組引入
import {moduleA} from './a.js'

// 模組匯出
export default () => {}
複製程式碼

黑盒體驗

我們可以把webpack看做一個黑盒,只要會用就可以。先來體驗一次很簡單的webpack打包過程

webpack

const webpack = require('webpack')
const path = require('path')

module.exports = {
  entry: './index.js',
  output: {
    filename: 'index.js',
    path: path.resolve(__dirname, 'public')
  }
}
複製程式碼

啟動編譯,在命令列輸入 node_modules/.bin/webpack 就可看到一次打包過程

檢視打包結果

關於如何啟動webpack

如果是全域性安裝了webpack,可以在命令列直接輸入 webpack

如果只是專案資料夾安裝,需要輸入 node_modules/.bin/webpack

  • npx

在 npmV5版本 會贈送一個npx

npx 會自動查詢當前依賴包中的可執行檔案,如果找不到,就會去 PATH 裡找。如果依然找不到,就會幫你安裝

所以也可以通過npx執行webpack

npx webpack
複製程式碼

require方法

實現一個require方法

common.js的規範中 引入一個模組需要

let getA = require('./a')
複製程式碼

自己寫一個require方法

let fs = require('fs')
// 查詢module
function myReq (myModule) {
  // 讀取檔案資訊
  let cont = fs.readFileSync(myModule, 'utf-8')
  /* function (exports, require, module, __filename, __dirname) {
    moduel.exports = {a: 'apple'}
    return moduel.exports
  } */
  let nodeFn = new Function('exports', 'require', 'module', '__filename', '__dirname', cont + 'return module.exports')
  let module = {
    exports: {}
  }
  return nodeFn(module.exports, myReq, module, __filename, __dirname)
}
// let getA = require('./a')
let getA = myReq('./a.js')
console.log(getA, 'getA')
複製程式碼

思路:讀取檔案內容,根據node的封裝規範,傳入幾個必須的引數即可。

  • 刪減 webpack 編譯後的檔案

把剛剛打包之後的 dist/index.js 刪減掉一些不用的程式碼

(function(modules) {
	function myRequire(moduleId) {
		var module = {
			exports: {}
		};
    modules[moduleId].call(module.exports, module, module.exports, myRequire);
        // call 用於讓  modules[moduleId] 函式執行 執行的是傳入後面的引數
		return module.exports;
	}
  return myRequire(/* 下面的第一個函式引數 */);
})
([
  (function(module, exports) {
    console.log('123')
  })
]);

複製程式碼

線上檢視

可以看出來, webpack打包生成之後的檔案內容就和編譯的require方法類似。這就是為什麼打包之後的js檔案可直接在瀏覽器中執行的原因

編譯流程

常見名詞解釋

引數 說明
entry 專案入口
module 開發中每一個檔案都可以看做module
chunk 程式碼塊
loader 模組轉化器
plugin 擴充套件外掛 自定義webpack打包過程
bundle 最終打包完成的檔案

打包流程

webpack的執行流程是一個序列的過程,從啟動到結束,會依次執行以下流程

  • 引數初始化

從配置檔案 【webpack.config.js】和 shell 語句中讀取與合併引數

  • 開始編譯

初始化一個compiler物件 載入所有外掛 執行物件的run方法開始編譯

  • 確定入口檔案

根據配置檔案找到專案所有的入口檔案

  • 編譯模組

從入口開始 呼叫配置的loader對模組進行編譯 【有一個遞迴尋找依賴模組的流程】

模組編譯完成後 得到模組被轉化後的最後內容以及他們之間的依賴關係

  • 資源輸出

根據入口檔案和模組之間的依賴關係 組成chunk檔案 【一個chunk可能包含多個模組】每一個chunk將會被轉化成一個單獨的檔案加入輸出列表中

  • 輸出

根據配置的輸出引數 【路徑和檔名】將輸出內容寫入檔案系統

** 在以上的過程 WP會在特定的時間點廣播特定的事件 外掛在監聽到感興趣的事件後會執行特定的邏輯 **

簡化流程

其實以上流程可以簡化為三個階段

webpack

原始碼分析

核心庫 tapable

在node中有一個事件發射器 EventEmitter ,可以進行事件監聽與發射。

var EventEmitter = require('events').EventEmitter;
var event = new EventEmitter();
event.on('some_event', function () {
    console.log('some_event 事件觸發');
});
setTimeout(function () {
    event.emit('some_event');
}, 1000);
複製程式碼

webpack核心庫 tapable 的原理和 EventEmitter 類似,通過事件的註冊和監聽,觸發各個編譯週期中的函式方法. Tapable 還允許你通過回撥函式的引數,訪問事件的“觸發者(emittee)”或“提供者(producer)”

檢視線上程式碼

核心物件 compiler

compiler 繼承自 tapable 可以進行事件的廣播和監聽

compiler 進行事件的廣播和監聽的方式為


// 廣播事件  params 為附帶引數
compiler.apply('event-name', params)

// 監聽 名為 event-name 的事件
compiler.plugin('event-name', function (params) {

})

複製程式碼

檢視177行程式碼

webpack 在初始化的時候 會將 compiler物件傳入到plugin中 可以使用它來訪問 webpack 的主環境

檢視45行程式碼

compiler 物件代表了完整的 webpack 環境配置。這個物件在啟動 webpack 時被一次性建立,並配置好所有可操作的設定,包括 options,loader 和 plugin。

核心物件 compilation

compilation 繼承自 tapable 可以進行事件的廣播和監聽

檢視57行程式碼

compilation 物件代表了一次資源版本構建。當執行 webpack 開發環境中介軟體時,每當檢測到一個檔案變化,就會建立一個新的 compilation,從而生成一組新的編譯資源。

一個 compilation 物件表現了當前的模組資源、編譯生成資源、變化的檔案、以及被跟蹤依賴的狀態資訊

plugin 實現機制

作用原理

在webpack的編譯流程,每一個階段都會廣播不同的事件,比如 run, done 等事件。plugin會監聽到這些事件,一旦事件發生,就會執行註冊好的函式方法

plugin分析

每一個plugin都是 一個具有 apply 屬性的 JavaScript 物件

class MPlugin {
  // 這裡獲取使用者為外掛傳入的配置引數
  constructor (options) {

  }
  // webpack 會呼叫 MPlugin 例項的apply方法 為外掛例項傳入 compiler 物件
  apply (compiler) {
    compiler.plugin('compilation', function (compilation) {
      // 回撥函式中 傳入了 compilation 物件

    })
  }
}

複製程式碼

在webpack初始化的階段 會往plugin中傳遞compiler物件

檢視45行程式碼

編寫plugin

class StartWp {
    constructor(options) {
        this.options = options
    }
    apply(compiler) {
        let {name} = this.options
        // 監聽事件 這是非同步的 所以要執行cb  不然會卡到這裡不動了
        compiler.plugin('run', function (compilation, cb) {
            console.log('run', name)
            // 每一次重新編譯的時候又會觸發
            // compilation.plugin('')
            cb();
        })
        compiler.plugin('done', function (compilation) {
            console.log('done', name)
        })
    }
}
module.exports = StartWp
複製程式碼
  • 傳遞給外掛的compiler和compilation是相同的 也就是某一個外掛有修改物件的話會影響後面的外掛的使用

  • 有的事件是非同步的,所以在使用的時候,要執行 cb() 去通知webpack 本次事件監聽結束了 要往下繼續執行否則會卡到這裡

如何使用此外掛

plugins: [
  new StartWp({
    name: 'v3 - plugin '
  })
]
複製程式碼

自己來寫一個簡易版本的webpack打包器

實現原理: 根據打包的模板格式 讀取檔案資訊並輸入到指定的位置

  • 藉助ejs

  • 將簡化的webpack打包結果拿出來作為 字串模板

最簡易的webpack

const fs = require('fs')

// 入口檔案
let input = './index.js'
// 輸出地址
let output = './dist/index.js'

const ejs = require('ejs')

const getIntry = fs.readFileSync(input, 'utf-8')

let template = `(function(modules) { 
	function __webpack_require__(moduleId) {
		var module = {
			exports: {}
		};
		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
		return module.exports;
	}
	return __webpack_require__(0);
})
([
  (function(module, exports) {
    <%- getIntry %>
  })
])`

let result = ejs.render(template, {
  getIntry
})

// 將結果輸出到 dist 
fs.writeFileSync(output, result)
複製程式碼

在命令列執行一次 node webpack.0.1.0.js

執行之後的編譯結果

可以看到在dist目錄有index.js生成 將其引入 html頁面

myWebpack

這樣就完成了一個非常非常簡單的webpack

線上檢視簡單的webpack

加入 require 處理

如果入口檔案中 有使用到 require 則需要將其替換為webpack提供的 webpack_require

先看一下如果有使用 require 之後的打包之後的結果 [簡化版本]

bundle.js

 (function(modules) {
 	function __webpack_require__(moduleId) {
 		var module = {
 			exports: {}
 		};
 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
 		return module.exports;
 	}
 	return __webpack_require__(0);
 })
 ([
  (function(module, exports, __webpack_require__) {
  __webpack_require__(1)
  console.log('index.js')
  }),
  (function(module, exports) {
    console.log(123)
  })
]);
複製程式碼

檢視bundle.js線上程式碼

我們使用這個模板來重新編寫一個簡易的webpack

const fs = require('fs')
const path = require('path')

// 入口檔案
let input = './index.js'
// 輸出地址
let output = './dist/index.js'

const ejs = require('ejs')

const getIntry = fs.readFileSync(input, 'utf-8')

// 將getIntry 中的 require 進行處理
const contAry = []
let dealIntry = getIntry.replace(/(require)\(['"](.+?)['"]\)/g, ($1, $2, $3, $4) => {
	let cont = fs.readFileSync($3, 'utf-8')
	contAry.push(cont)
	return $2 = `__webpack_require__(${contAry.length})`
})

let template = `(function(modules) {
 	function __webpack_require__(moduleId) {
 		var module = {
 			exports: {}
 		};
 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
 		return module.exports;
 	}
 	return __webpack_require__(0);
 })
 ([
  (function(module, exports, __webpack_require__) {
	  <%- dealIntry %>
  }),
	<% for(var i=0;i < contAry.length; i++){ %>
		(function(module, exports) {
	    <%- contAry[i] %>
	  }),
  <%}%>
])`

let result = ejs.render(template, {
  dealIntry,
  contAry
})

// 將結果輸出到 dist
fs.writeFileSync(output, result)

複製程式碼

在命令列執行一次 node webpack.1.0.0.js

執行之後的編譯結果

myWebpack

線上檢視簡單的webpack

原始碼篇提問

  • 在自己構建的plugin中 是否可以進行事件廣播

可以。只要能拿到 compiler或者compilation物件 就可以廣播事件,為其他外掛監聽使用

參考文章

相關文章