本系列將會從原理、開發、優化、對比四個方面給你們介紹webpack的工做流程。【默認是以webpack v3爲例子】javascript
// 模塊引入
let moduleA = require('./a.js')
// 模塊導出
module.exports = () => {}
複製代碼
// 模塊引入
import {moduleA} from './a.js'
// 模塊導出
export default () => {}
複製代碼
咱們能夠把webpack看作一個黑盒,只要會用就能夠。先來體驗一次很簡單的webpack打包過程html
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,能夠在命令行直接輸入 webpackwebpack
若是隻是項目文件夾安裝,須要輸入 node_modules/.bin/webpackgit
在 npmV5版本 會贈送一個npxes6
npx 會自動查找當前依賴包中的可執行文件,若是找不到,就會去 PATH 裏找。若是依然找不到,就會幫你安裝github
因此也能夠經過npx執行webpackweb
npx webpack
複製代碼
實現一個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的封裝規範,傳入幾個必須的參數便可。
把剛剛打包以後的 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會在特定的時間點廣播特定的事件 插件在監聽到感興趣的事件後會執行特定的邏輯 **
其實以上流程能夠簡化爲三個階段
在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 繼承自 tapable 能夠進行事件的廣播和監聽
compiler 進行事件的廣播和監聽的方式爲
// 廣播事件 params 爲附帶參數
compiler.apply('event-name', params)
// 監聽 名爲 event-name 的事件
compiler.plugin('event-name', function (params) {
})
複製代碼
webpack 在初始化的時候 會將 compiler對象傳入到plugin中 可使用它來訪問 webpack 的主環境
compiler 對象表明了完整的 webpack 環境配置。這個對象在啓動 webpack 時被一次性創建,並配置好全部可操做的設置,包括 options,loader 和 plugin。
compilation 繼承自 tapable 能夠進行事件的廣播和監聽
compilation 對象表明了一次資源版本構建。當運行 webpack 開發環境中間件時,每當檢測到一個文件變化,就會建立一個新的 compilation,從而生成一組新的編譯資源。
一個 compilation 對象表現了當前的模塊資源、編譯生成資源、變化的文件、以及被跟蹤依賴的狀態信息
在webpack的編譯流程,每個階段都會廣播不一樣的事件,好比 run, done 等事件。plugin會監聽到這些事件,一旦事件發生,就會執行註冊好的函數方法
每個plugin都是 一個具備 apply 屬性的 JavaScript 對象
class MPlugin {
// 這裏獲取用戶爲插件傳入的配置參數
constructor (options) {
}
// webpack 會調用 MPlugin 實例的apply方法 爲插件實例傳入 compiler 對象
apply (compiler) {
compiler.plugin('compilation', function (compilation) {
// 回調函數中 傳入了 compilation 對象
})
}
}
複製代碼
在webpack初始化的階段 會往plugin中傳遞compiler對象
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 '
})
]
複製代碼
實現原理: 根據打包的模板格式 讀取文件信息並輸入到指定的位置
藉助ejs
將簡化的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頁面
這樣就完成了一個很是很是簡單的webpack
若是入口文件中 有使用到 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)
})
]);
複製代碼
咱們使用這個模板來從新編寫一個簡易的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
能夠。只要能拿到 compiler或者compilation對象 就能夠廣播事件,爲其餘插件監聽使用