詳解webpack loader和plugin編寫(xiě)
1 基礎(chǔ)回顧
首先我們先回顧一下webpack常見(jiàn)配置,因?yàn)楹竺鏁?huì)用到,所以簡(jiǎn)單介紹一下。
1.1 webpack常見(jiàn)配置
// 入口文件 entry: { app: './src/js/index.js', }, // 輸出文件 output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist'), publicPath: '/' //確保文件資源能夠在 http://localhost:3000 下正確訪問(wèn) }, // 開(kāi)發(fā)者工具 source-map devtool: 'inline-source-map', // 創(chuàng)建開(kāi)發(fā)者服務(wù)器 devServer: { contentBase: './dist', hot: true // 熱更新 }, plugins: [ // 刪除dist目錄 new CleanWebpackPlugin(['dist']), // 重新穿件html文件 new HtmlWebpackPlugin({ title: 'Output Management' }), // 以便更容易查看要修補(bǔ)(patch)的依賴 new webpack.NamedModulesPlugin(), // 熱更新模塊 new webpack.HotModuleReplacementPlugin() ], // 環(huán)境 mode: "development", // loader配置 module: { rules: [ { test: /\.css$/, use: [ 'style-loader', 'css-loader' ] }, { test: /\.(png|svg|jpg|gif)$/, use: [ 'file-loader' ] } ] }
這里面我們重點(diǎn)關(guān)注 module和plugins屬性,因?yàn)榻裉斓闹攸c(diǎn)是編寫(xiě)loader和plugin,需要配置這兩個(gè)屬性。
1.2 打包原理
- 識(shí)別入口文件
- 通過(guò)逐層識(shí)別模塊依賴。(Commonjs、amd或者es6的import,webpack都會(huì)對(duì)其進(jìn)行分析。來(lái)獲取代碼的依賴)
- webpack做的就是分析代碼。轉(zhuǎn)換代碼,編譯代碼,輸出代碼
- 最終形成打包后的代碼
這些都是webpack的一些基礎(chǔ)知識(shí),對(duì)于理解webpack的工作機(jī)制很有幫助。
2 loader
OK今天第一個(gè)主角登場(chǎng)
2.1 什么是loader?
loader是文件加載器,能夠加載資源文件,并對(duì)這些文件進(jìn)行一些處理,諸如編譯、壓縮等,最終一起打包到指定的文件中
- 處理一個(gè)文件可以使用多個(gè)loader,loader的執(zhí)行順序是和本身的順序是相反的,即最后一個(gè)loader最先執(zhí)行,第一個(gè)loader最后執(zhí)行。
- 第一個(gè)執(zhí)行的loader接收源文件內(nèi)容作為參數(shù),其他loader接收前一個(gè)執(zhí)行的loader的返回值作為參數(shù)。最后執(zhí)行的loader會(huì)返回此模塊的JavaScript源碼
2.2 手寫(xiě)一個(gè)loader
需求:
- 處理.txt文件
- 對(duì)字符串做反轉(zhuǎn)操作
- 首字母大寫(xiě)
例如:abcdefg轉(zhuǎn)換后為Gfedcba
OK,我們開(kāi)始
1)首先創(chuàng)建兩個(gè)loader(這里以本地loader為例)
為什么要?jiǎng)?chuàng)建兩個(gè)laoder?理由后面會(huì)介紹
reverse-loader.js
module.exports = function (src) { if (src) { console.log('--- reverse-loader input:', src) src = src.split('').reverse().join('') console.log('--- reverse-loader output:', src) } return src; }
uppercase-loader.js
module.exports = function (src) { if (src) { console.log('--- uppercase-loader input:', src) src = src.charAt(0).toUpperCase() + src.slice(1) console.log('--- uppercase-loader output:', src) } // 這里為什么要這么寫(xiě)?因?yàn)橹苯臃祷剞D(zhuǎn)換后的字符串會(huì)報(bào)語(yǔ)法錯(cuò)誤, // 這么寫(xiě)import后轉(zhuǎn)換成可以使用的字符串 return `module.exports = '${src}'` }
看,loader結(jié)構(gòu)是不是很簡(jiǎn)單,接收一個(gè)參數(shù),并且return一個(gè)內(nèi)容就ok了。
然后創(chuàng)建一個(gè)txt文件
2)mytest.txt
abcdefg
3)現(xiàn)在開(kāi)始配置webpack
module.exports = { entry: { index: './src/js/index.js' }, plugins: [...], optimization: {...}, output: {...}, module: { rules: [ ..., { test: /\.txt$/, use: [ './loader/uppercase-loader.js', './loader/reverse-loader.js' ] } ] } }
這樣就配置完成了
4)我們?cè)谌肟谖募袑?dǎo)入這個(gè)腳本
為什么這里需要導(dǎo)入呢,我們不是配置了webapck處理所有的.txt文件么?
因?yàn)閣ebpack會(huì)做過(guò)濾,如果不引用該文件的話,webpack是不會(huì)對(duì)該文件進(jìn)行打包處理的,那么你的loader也不會(huì)執(zhí)行
import _ from 'lodash'; import txt from '../txt/mytest.txt' import '../css/style.css' function component() { var element = document.createElement('div'); var button = document.createElement('button'); var br = document.createElement('br'); button.innerHTML = 'Click me and look at the console!'; element.innerHTML = _.join('【' + txt + '】'); element.className = 'hello' element.appendChild(br); element.appendChild(button); // Note that because a network request is involved, some indication // of loading would need to be shown in a production-level site/app. button.onclick = e => import(/* webpackChunkName: "print" */ './print').then(module => { var print = module.default; print(); }); return element; } document.body.appendChild(component());
package.json配置
{ ..., "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack --config webpack.prod.js", "start": "webpack-dev-server --open --config webpack.dev.js", "server": "node server.js" }, ... }
然后執(zhí)行命令
npm run build
這樣我們的loader就寫(xiě)完了。
現(xiàn)在回答為什么要寫(xiě)兩個(gè)loader?
看到執(zhí)行的順序沒(méi),我們的配置的是這樣的
use: [ './loader/uppercase-loader.js', './loader/reverse-loader.js' ]
正如前文所說(shuō), 處理一個(gè)文件可以使用多個(gè)loader,loader的執(zhí)行順序是和本身的順序是相反的
我們也可以自己寫(xiě)loader解析自定義模板,像vue-loader是非常復(fù)雜的,它內(nèi)部會(huì)寫(xiě)大量的對(duì).vue文件的解析,然后會(huì)生成對(duì)應(yīng)的html、js和css。
我們這里只是講述了一個(gè)最基礎(chǔ)的用法,如果有更多的需要,可以查看《loader官方文檔》
3 plugin
3.1 什么是plugin?
在 Webpack 運(yùn)行的生命周期中會(huì)廣播出許多事件,Plugin 可以監(jiān)聽(tīng)這些事件,在合適的時(shí)機(jī)通過(guò) Webpack 提供的 API 改變輸出結(jié)果。
plugin和loader的區(qū)別是什么?
對(duì)于loader,它就是一個(gè)轉(zhuǎn)換器,將A文件進(jìn)行編譯形成B文件,這里操作的是文件,比如將A.scss或A.less轉(zhuǎn)變?yōu)锽.css,單純的文件轉(zhuǎn)換過(guò)程
plugin是一個(gè)擴(kuò)展器,它豐富了wepack本身,針對(duì)是loader結(jié)束后,webpack打包的整個(gè)過(guò)程,它并不直接操作文件,而是基于事件機(jī)制工作,會(huì)監(jiān)聽(tīng)webpack打包過(guò)程中的某些節(jié)點(diǎn),執(zhí)行廣泛的任務(wù)。
3.2 一個(gè)最簡(jiǎn)的插件
/plugins/MyPlugin.js(本地插件)
class MyPlugin { // 構(gòu)造方法 constructor (options) { console.log('MyPlugin constructor:', options) } // 應(yīng)用函數(shù) apply (compiler) { // 綁定鉤子事件 compiler.plugin('compilation', compilation => { console.log('MyPlugin') )) } } module.exports = MyPlugin
webpack配置
const MyPlugin = require('./plugins/MyPlugin') module.exports = { entry: { index: './src/js/index.js' }, plugins: [ ..., new MyPlugin({param: 'xxx'}) ], ... };
這就是一個(gè)最簡(jiǎn)單的插件(雖然我們什么都沒(méi)干)
- webpack 啟動(dòng)后,在讀取配置的過(guò)程中會(huì)先執(zhí)行 new MyPlugin(options) 初始化一個(gè) MyPlugin 獲得其實(shí)例。
- 在初始化 compiler 對(duì)象后,再調(diào)用 myPlugin.apply(compiler) 給插件實(shí)例傳入 compiler 對(duì)象。
- 插件實(shí)例在獲取到 compiler 對(duì)象后,就可以通過(guò) compiler.plugin(事件名稱, 回調(diào)函數(shù)) 監(jiān)聽(tīng)到 Webpack 廣播出來(lái)的事件。
- 并且可以通過(guò) compiler 對(duì)象去操作 webpack。
看到這里可能會(huì)問(wèn)compiler是啥,compilation又是啥?
Compiler 對(duì)象包含了 Webpack 環(huán)境所有的的配置信息,包含 options,loaders,plugins 這些信息,這個(gè)對(duì)象在 Webpack 啟動(dòng)時(shí)候被實(shí)例化,它是全局唯一的,可以簡(jiǎn)單地把它理解為 Webpack 實(shí)例;
Compilation 對(duì)象包含了當(dāng)前的模塊資源、編譯生成資源、變化的文件等。當(dāng) Webpack 以開(kāi)發(fā)模式運(yùn)行時(shí),每當(dāng)檢測(cè)到一個(gè)文件變化,一次新的 Compilation 將被創(chuàng)建。Compilation 對(duì)象也提供了很多事件回調(diào)供插件做擴(kuò)展。通過(guò) Compilation 也能讀取到 Compiler 對(duì)象。
Compiler 和 Compilation 的區(qū)別在于:
Compiler 代表了整個(gè) Webpack 從啟動(dòng)到關(guān)閉的生命周期,而 Compilation 只是代表了一次新的編譯。
3.3 事件流
- webpack 通過(guò) Tapable 來(lái)組織這條復(fù)雜的生產(chǎn)線。
- webpack 的事件流機(jī)制保證了插件的有序性,使得整個(gè)系統(tǒng)擴(kuò)展性很好。
- webpack 的事件流機(jī)制應(yīng)用了觀察者模式,和 Node.js 中的 EventEmitter 非常相似。
綁定事件
compiler.plugin('event-name', params => { ... });
觸發(fā)事件
compiler.apply('event-name',params)
3.4 需要注意的點(diǎn)
- 只要能拿到 Compiler 或 Compilation 對(duì)象,就能廣播出新的事件,所以在新開(kāi)發(fā)的插件中也能廣播出事件,給其它插件監(jiān)聽(tīng)使用。
- 傳給每個(gè)插件的 Compiler 和 Compilation 對(duì)象都是同一個(gè)引用。也就是說(shuō)在一個(gè)插件中修改了 Compiler 或 Compilation 對(duì)象上的屬性,會(huì)影響到后面的插件。
- 有些事件是異步的,這些異步的事件會(huì)附帶兩個(gè)參數(shù),第二個(gè)參數(shù)為回調(diào)函數(shù),在插件處理完任務(wù)時(shí)需要調(diào)用回調(diào)函數(shù)通知 webpack,才會(huì)進(jìn)入下一處理流程 。例如:
compiler.plugin('emit',function(compilation, callback) { ... // 處理完畢后執(zhí)行 callback 以通知 Webpack // 如果不執(zhí)行 callback,運(yùn)行流程將會(huì)一直卡在這不往下執(zhí)行 callback(); });
關(guān)于complier和compilation,webpack定義了大量的鉤子事件。開(kāi)發(fā)者可以根據(jù)自己的需要在任何地方進(jìn)行自定義處理。
3.5 手寫(xiě)一個(gè)plugin
場(chǎng)景:
小程序mpvue項(xiàng)目,通過(guò)webpack編譯,生成子包(我們作為分包引入到主程序中),然后考入主包當(dāng)中。生成子包后,里面的公共靜態(tài)資源wxss引用地址需要加入分包的前綴:/subPages/enjoy_given。
在未編寫(xiě)插件前,生成的資源是這樣的,這個(gè)路徑如果作為分包引入主包,是沒(méi)法正常訪問(wèn)資源的。
所以需求來(lái)了:
修改dist/static/css/pages目錄下,所有頁(yè)面的樣式文件(wxss文件)引入公共資源的路徑。
因?yàn)樗许?yè)面的樣式都會(huì)引用通用樣式vender.wxss
那么就需要把@import "/static/css/vendor.wxss"; 改為:@import "/subPages/enjoy_given/static/css/vendor.wxss";復(fù)制代碼
OK 開(kāi)始!
1)創(chuàng)建插件文件 CssPathTransfor.js
CssPathTransfor.js
class CssPathTransfor { apply (compiler) { compiler.plugin('emit', (compilation, callback) => { console.log('--CssPathTransfor emit') // 遍歷所有資源文件 for (var filePathName in compilation.assets) { // 查看對(duì)應(yīng)的文件是否符合指定目錄下的文件 if (/static\/css\/pages/i.test(filePathName)) { // 引入路徑正則 const reg = /\/static\/css\/vendor\.wxss/i // 需要替換的最終字符串 const finalStr = '/subPages/enjoy_given/static/css/vendor.wxss' // 獲取文件內(nèi)容 let content = compilation.assets[filePathName].source() || '' content = content.replace(reg, finalStr) // 重寫(xiě)指定輸出模塊內(nèi)容 compilation.assets[filePathName] = { source () { return content; }, size () { return content.length; } } } } callback() }) } } module.exports = CssPathTransfor
看著挺多,實(shí)際就是遍歷compilation.assets模塊。對(duì)符合要求的文件進(jìn)行正則替換。
2)修改webpack配置
var baseWebpackConfig = require('./webpack.base.conf') var CssPathTransfor = require('../plugins/CssPathTransfor.js') var webpackConfig = merge(baseWebpackConfig, { module: {...}, devtool: config.build.productionSourceMap ? '#source-map' : false, output: {...}, plugins: [ ..., // 配置插件 new CssPathTransfor(), ] })
插件編寫(xiě)完成后,執(zhí)行編譯命令
搞定~
如果有更多的需求可以參考《如何寫(xiě)一個(gè)插件》
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
小程序中使用css var變量(使js可以動(dòng)態(tài)設(shè)置css樣式屬性)
這篇文章主要介紹了小程序中使用css var變量,使js可以動(dòng)態(tài)設(shè)置css樣式屬性,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03微信小程序以ssm做后臺(tái)開(kāi)發(fā)的實(shí)現(xiàn)示例
這篇文章主要介紹了微信小程序以ssm做后臺(tái)開(kāi)發(fā)的實(shí)現(xiàn)示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04JS實(shí)現(xiàn)的RGB網(wǎng)頁(yè)顏色在線取色器完整實(shí)例
這篇文章主要介紹了JS實(shí)現(xiàn)的RGB網(wǎng)頁(yè)顏色在線取色器,結(jié)合完整實(shí)例形式分析了基于JS運(yùn)算及鼠標(biāo)事件響應(yīng)來(lái)操作頁(yè)面元素實(shí)現(xiàn)取色器功能的方法,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2016-12-12javascript 高級(jí)語(yǔ)法之繼承的基本使用方法示例
這篇文章主要介紹了javascript 高級(jí)語(yǔ)法之繼承的基本使用方法,結(jié)合實(shí)例形式分析了JavaScript繼承的基本使用方法與操作注意事項(xiàng),需要的朋友可以參考下2019-11-11JS實(shí)現(xiàn)給對(duì)象動(dòng)態(tài)添加屬性的方法
這篇文章主要介紹了JS實(shí)現(xiàn)給對(duì)象動(dòng)態(tài)添加屬性的方法,涉及JS屬性的遍歷、動(dòng)態(tài)賦值及eval方法的簡(jiǎn)單使用技巧,需要的朋友可以參考下2017-01-01基于JavaScript實(shí)現(xiàn)帶縮略圖的輪播效果
這篇文章主要為大家詳細(xì)介紹了基于JavaScript實(shí)現(xiàn)帶縮略圖的輪播效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-01-01