本系列將會從原理、開發、優化、對比四個方面給你們介紹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對象 就能夠廣播事件,爲其餘插件監聽使用