作者介紹:趙鵬,美團點評點餐團隊成員
webpack是一個模組打包器(module bundler),提供了一個核心,核心提供了很多開箱即用的功能,同時它可以用loader和plugin來擴充套件。webpack本身結構精巧,基於tapable的外掛架構,擴充套件性強,眾多的loader或者plugin讓webpack顯得很複雜。
webpack常用配置包括:devtool、entry、 output、module、resolve、plugins、externals等,本文主要介紹下webpack常用的loader和plugin
webpack允許我們使用loader來處理檔案,loader是一個匯出為function的node模組。可以將匹配到的檔案進行一次轉換,同時loader可以鏈式傳遞。
loader的使用方式
一般loader的使用方式分為三種:
1:在配置檔案webpack.config.js中配置
module.exports = {
module: {
rules: [
{
test: /\.txt$/,
use: 'raw-loader'
}
]
}
}複製程式碼
2:通過命令列引數方式
webpack --module-bind 'txt=raw-loader'複製程式碼
3:通過內聯使用
import txt from 'raw-loader!./file.txt';複製程式碼
webpack常用的loader
- 樣式:style-loader、css-loader、less-loader、sass-loader等
- 檔案:raw-loader、file-loader 、url-loader等
- 編譯:babel-loader、coffee-loader 、ts-loader等
- 校驗測試:mocha-loader、jshint-loader 、eslint-loader等
比如下面配置,可以匹配.scss的檔案,分別經過sass-loader、css-loader、style-loader的處理。sass-loader
轉化sass為css檔案,並且包一層module.exports成為一個js module。style-loader
將建立一個style標籤將css檔案嵌入到html中。css-loader
則處理其中的@import和url()。
module.exports = {
module: {
rules: [
{
test: /\.scss$/,
use:[
{loader:'style-loader'},
{loader:'css-loader',options:{sourceMap:true,modules:true}},
{loader:'sass-loader',options:{sourceMap:true}}
],
exclude:/node_modules/
}
]
}
}複製程式碼
vue-loader、coffee-loader、babel-loader
等可以將特定檔案格式轉成js模組、將其他語言轉化為js語言和編譯下一代js語言file-loader、url-loader
等可以處理資源,file-loader可以複製和放置資源位置,並可以指定檔名模板,用hash命名更好利用快取。url-loader
可以將小於配置limit大小的檔案轉換成內斂Data Url的方式,減少請求。raw-loader
可以將檔案已字串的形式返回imports-loader、exports-loader
等可以向模組注入變數或者提供匯出模組功能,常見場景是:
1:jquery外掛注入$,imports-loader?$=jquery
2:禁用AMD,imports-loader?define=false
等同於:var $ = require("jquery") 和 var define = false;expose-loader
:暴露物件為全域性變數
如何寫一個loader:官網介紹how to write a loader
下面是一個簡單的raw-loader,它可以將文字類檔案轉成字串到js檔案中。其中this.cacheable、this.value等是loader的api,分別是將結果標記為可快取和把值傳遞給下一個loader。module.exports = function(content) { this.cacheable && this.cacheable(); this.value = content; return "module.exports = " + JSON.stringify(content); }複製程式碼
webpack的plugin比loader強大,通過鉤子可以涉及整個構建流程,可以做一些在構建範圍內的事情。
webpack常用的plugin
- 官網介紹plugins
- 第三方外掛awesome-webpack
- 首先webpack內建
UglifyJsPlugin
,壓縮和混淆程式碼。 - webpack內建
CommonsChunkPlugin
,提高打包效率,將第三方庫和業務程式碼分開打包。 ProvidePlugin
:自動載入模組,代替require和importnew webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery' })複製程式碼
html-webpack-plugin
可以根據模板自動生成html程式碼,並自動引用css和js檔案extract-text-webpack-plugin
將js檔案中引用的樣式單獨抽離成css檔案DefinePlugin
編譯時配置全域性變數,這對開發模式和釋出模式的構建允許不同的行為非常有用。new webpack.DefinePlugin({ PRODUCTION: JSON.stringify(true), VERSION: JSON.stringify("5fa3b9"), BROWSER_SUPPORTS_HTML5: true, TWO: "1+1", "typeof window": JSON.stringify("object") })複製程式碼
HotModuleReplacementPlugin
熱更新- 新增HotModuleReplacementPlugin
- entry中新增 "webpack-dev-server/client?http://localhost:8080/",
- entry中新增 "webpack/hot/dev-server"
- (熱更新還可以直接用webpack_dev_server --hot --inline,原理也是在entry中新增了上述程式碼)
webpack 內建的
DllPlugin
和DllReferencePlugin
相互配合,前置第三方包的構建,只構建業務程式碼,同時能解決Externals多次引用問題。DllReferencePlugin引用DllPlugin配置生成的manifest.json檔案,manifest.json包含了依賴模組和module id的對映關係babili-webpack-plugin、transform-runtime 、transform-object-rest-spread
- babili-webpack-plugin:構建在babel之上 why
- transform-runtime :解決了babel在每個檔案都插入了輔助程式碼,程式碼體積過大的問題。
- transform-object-rest-spread:
Transform rest properties for object destructuring assignment and spread properties for object literals
為物件字面量新增解構賦值和spread屬性
optimize-css-assets-webpack-plugin
不同元件中重複的css可以快速去重webpack-bundle-analyzer
一個webpack的bundle檔案分析工具,將bundle檔案以可互動縮放的treemap的形式展示。compression-webpack-plugin
生產環境可採用gzip壓縮JS和CSShappypack
:通過多程式模型,來加速程式碼構建const os = require('os'); let HappyPack = require('happypack'); let happyThreadPool = HappyPack.ThreadPool({size: os.cpus().length}); exports.plugins = [ new HappyPack({ id: 'jsx', threadPool: happyThreadPool, loaders: [ 'babel-loader' ] }), new HappyPack({ id: 'coffeescripts', threadPool: happyThreadPool, loaders: [ 'coffee-loader' ] }) ]; exports.module.loaders = [ { test: /\.js$/, loaders: [ 'happypack/loader?id=jsx' ] }, { test: /\.coffee$/, loaders: [ 'happypack/loader?id=coffeescripts' ] }, ]複製程式碼
寫一個webpack外掛:
主要的步驟如下:
- 編寫一個JavaScript命名函式。
- 在它的原型上定義一個apply方法。
- 指定掛載的webpack事件鉤子。
- 處理webpack內部例項的特定資料。
- 功能完成後呼叫webpack提供的回撥。
編寫外掛之前要理解compiler和compilation兩個物件,以及webpack生命週期的各個階段和鉤子,plugin比loader強大,通過plugin你可以訪問compliler和compilation過程,通過鉤子攔截webpack的執行。
比如我們可以在構建生成檔案時,將所有生成的檔名生成到filelist.md的檔案中
webpack會將compilation.assets的內容生成檔案,所以可以在構建中利用它生成我們想要的檔案。
function FileListPlugin(options) {}
FileListPlugin.prototype.apply = function(compiler) {
compiler.plugin('emit', function(compilation, callback) {
var filelist = 'In this build:\n\n';
for (var filename in compilation.assets) {
filelist += ('- '+ filename +'\n');
}
compilation.assets['filelist.md'] = {
source: function() {
return filelist;
},
size: function() {
return filelist.length;
}
};
callback();
});
};
module.exports = FileListPlugin;複製程式碼
比如我們可以在html-webpack-plugin生成檔案後重新整理頁面,完成熱更新效果。
var webpack = require('webpack')
var webpackConfig = require('./webpack.config')
var compiler = webpack(webpackConfig)
var hotMiddleware = require('webpack-hot-middleware')(compiler, {
log: () => {}
})
compiler.plugin('compilation', function (compilation) {
compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
hotMiddleware.publish({ action: 'reload' })
cb()
})
})複製程式碼
比如我們可以在構建完成後,開啟一個提示視窗。
class Notifier {
apply(compiler) {
compiler.plugin("done", (stats) => {
const pkg = require("./package.json");
const notifier = require("node-notifier");
const time = ((stats.endTime - stats.startTime) / 1000).toFixed(2);
notifier.notify({
title: pkg.name,
message: `WebPack is done!\n${stats.compilation.errors.length} errors in ${time}s`,
contentImage: "https://path/to/your/logo.png",
});
});
}
}
module.exports = Notifier;複製程式碼
webpack 外掛分析
- 首先介紹webpack原始碼分析方法
- node --inspect-brk ./node_modules/webpack/bin/webpack.js --config ./webpack.config.js
- chrome輸入 chrome://inspect/
- 主要的流程是:
- webpack構建的主要鉤子:
主要包括編譯,分析模組及依賴關係,構建模組,封裝結果,生成檔案等
- compiler和compilation都繼承於Tapable
webpack的外掛是基於Tapable的,Tapable允許你新增和應用外掛到javascript模組中,類似於 NodeJS的EventEmitter,可以被繼承和mixin到其他模組中,詳情見官網Tapable
其中關鍵的方法是
- plugin(name:string, handler:function)
- apply(...pluginInstances: (AnyPlugin|function)[])
- applyPlugins*(name:string, ...)
- mixin(pt: Object)
tapable主要負責處理事件,採用的是釋出訂閱模式,apply相當於trigger,plugin相當於addEventListener
Tapable.prototype.plugin = function plugin(name, fn) {
if(Array.isArray(name)) {
name.forEach(function(name) {
this.plugin(name, fn);
}, this);
return;
}
if(!this._plugins[name]) this._plugins[name] = [fn];
else this._plugins[name].push(fn);
};複製程式碼
plugin方法將外掛對應的方法加入一個陣列中、註冊到事件(name)上,等待apply的時候序列呼叫/觸發
Compilation中做了很多事情,處理編譯過程。所對應的方法,如addEntry ,buildModule,processModuleDependencies,createChunkAssets,seal等
後記:
webpack的設計思想、外掛機制值得我們深入學習和交流,還有一些特性,比如tree shaking,scope hoisting……
參考文章: