webpack系列之-原理篇

本系列將會從原理、開發、優化、對比四個方面給你們介紹webpack的工做流程。【默認是以webpack v3爲例子】javascript

儲備知識

CommonJS 規範

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

// 模塊導出
module.exports = () => {}

複製代碼

es6規範

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

// 模塊導出
export default () => {}
複製代碼

黑盒體驗

咱們能夠把webpack看作一個黑盒,只要會用就能夠。先來體驗一次很簡單的webpack打包過程html

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 就可看到一次打包過程java

查看打包結果node

關於如何啓動webpack

若是是全局安裝了webpack,能夠在命令行直接輸入 webpackwebpack

若是隻是項目文件夾安裝,須要輸入 node_modules/.bin/webpackgit

  • npx

在 npmV5版本 會贈送一個npxes6

npx 會自動查找當前依賴包中的可執行文件,若是找不到,就會去 PATH 裏找。若是依然找不到,就會幫你安裝github

因此也能夠經過npx執行webpackweb

npx webpack
複製代碼

require方法

實現一個require方法shell

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對象 就能夠廣播事件,爲其餘插件監聽使用

參考文章

相關文章
相關標籤/搜索