您好,登錄后才能下訂單哦!
這篇文章主要介紹webpack中l(wèi)oader和plugin的示例分析,文中介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們一定要看完!
1 基礎(chǔ)回顧
首先我們先回顧一下webpack常見配置,因為后面會用到,所以簡單介紹一下。
1.1 webpack常見配置
// 入口文件 entry: { app: './src/js/index.js', }, // 輸出文件 output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist'), publicPath: '/' //確保文件資源能夠在 http://localhost:3000 下正確訪問 }, // 開發(fā)者工具 source-map devtool: 'inline-source-map', // 創(chuàng)建開發(fā)者服務(wù)器 devServer: { contentBase: './dist', hot: true // 熱更新 }, plugins: [ // 刪除dist目錄 new CleanWebpackPlugin(['dist']), // 重新穿件html文件 new HtmlWebpackPlugin({ title: 'Output Management' }), // 以便更容易查看要修補(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' ] } ] }
這里面我們重點關(guān)注 module和plugins屬性,因為今天的重點是編寫loader和plugin,需要配置這兩個屬性。
1.2 打包原理
識別入口文件
通過逐層識別模塊依賴。(Commonjs、amd或者es6的import,webpack都會對其進行分析。來獲取代碼的依賴)
webpack做的就是分析代碼。轉(zhuǎn)換代碼,編譯代碼,輸出代碼
最終形成打包后的代碼
這些都是webpack的一些基礎(chǔ)知識,對于理解webpack的工作機制很有幫助。
2 loader
OK今天第一個主角登場
2.1 什么是loader?
loader是文件加載器,能夠加載資源文件,并對這些文件進行一些處理,諸如編譯、壓縮等,最終一起打包到指定的文件中
處理一個文件可以使用多個loader,loader的執(zhí)行順序是和本身的順序是相反的,即最后一個loader最先執(zhí)行,第一個loader最后執(zhí)行。
第一個執(zhí)行的loader接收源文件內(nèi)容作為參數(shù),其他loader接收前一個執(zhí)行的loader的返回值作為參數(shù)。最后執(zhí)行的loader會返回此模塊的JavaScript源碼
2.2 手寫一個loader
需求:
處理.txt文件
對字符串做反轉(zhuǎn)操作
首字母大寫
例如:abcdefg轉(zhuǎn)換后為Gfedcba
OK,我們開始
1)首先創(chuàng)建兩個loader(這里以本地loader為例)
為什么要創(chuàng)建兩個laoder?理由后面會介紹
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) } // 這里為什么要這么寫?因為直接返回轉(zhuǎn)換后的字符串會報語法錯誤, // 這么寫import后轉(zhuǎn)換成可以使用的字符串 return `module.exports = '${src}'` }
看,loader結(jié)構(gòu)是不是很簡單,接收一個參數(shù),并且return一個內(nèi)容就ok了。
然后創(chuàng)建一個txt文件
2)mytest.txt
abcdefg
3)現(xiàn)在開始配置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)我們在入口文件中導(dǎo)入這個腳本
為什么這里需要導(dǎo)入呢,我們不是配置了webapck處理所有的.txt文件么?
因為webpack會做過濾,如果不引用該文件的話,webpack是不會對該文件進行打包處理的,那么你的loader也不會執(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àn)在回答為什么要寫兩個loader?
看到執(zhí)行的順序沒,我們的配置的是這樣的
use: [ './loader/uppercase-loader.js', './loader/reverse-loader.js' ]
正如前文所說, 處理一個文件可以使用多個loader,loader的執(zhí)行順序是和本身的順序是相反的
我們也可以自己寫loader解析自定義模板,像vue-loader是非常復(fù)雜的,它內(nèi)部會寫大量的對.vue文件的解析,然后會生成對應(yīng)的html、js和css。
我們這里只是講述了一個最基礎(chǔ)的用法,如果有更多的需要,可以查看《loader官方文檔》
3 plugin
3.1 什么是plugin?
在 Webpack 運行的生命周期中會廣播出許多事件,Plugin 可以監(jiān)聽這些事件,在合適的時機通過 Webpack 提供的 API 改變輸出結(jié)果。
plugin和loader的區(qū)別是什么?
對于loader,它就是一個轉(zhuǎn)換器,將A文件進行編譯形成B文件,這里操作的是文件,比如將A.scss或A.less轉(zhuǎn)變?yōu)锽.css,單純的文件轉(zhuǎn)換過程
plugin是一個擴展器,它豐富了wepack本身,針對是loader結(jié)束后,webpack打包的整個過程,它并不直接操作文件,而是基于事件機制工作,會監(jiān)聽webpack打包過程中的某些節(jié)點,執(zhí)行廣泛的任務(wù)。
3.2 一個最簡的插件
/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'}) ], ... };
這就是一個最簡單的插件(雖然我們什么都沒干)
webpack 啟動后,在讀取配置的過程中會先執(zhí)行 new MyPlugin(options) 初始化一個 MyPlugin 獲得其實例。
在初始化 compiler 對象后,再調(diào)用 myPlugin.apply(compiler) 給插件實例傳入 compiler 對象。
插件實例在獲取到 compiler 對象后,就可以通過 compiler.plugin(事件名稱, 回調(diào)函數(shù)) 監(jiān)聽到 Webpack 廣播出來的事件。
并且可以通過 compiler 對象去操作 webpack。
看到這里可能會問compiler是啥,compilation又是啥?
Compiler 對象包含了 Webpack 環(huán)境所有的的配置信息,包含 options,loaders,plugins 這些信息,這個對象在 Webpack 啟動時候被實例化,它是全局唯一的,可以簡單地把它理解為 Webpack 實例;
Compilation 對象包含了當(dāng)前的模塊資源、編譯生成資源、變化的文件等。當(dāng) Webpack 以開發(fā)模式運行時,每當(dāng)檢測到一個文件變化,一次新的 Compilation 將被創(chuàng)建。Compilation 對象也提供了很多事件回調(diào)供插件做擴展。通過 Compilation 也能讀取到 Compiler 對象。
Compiler 和 Compilation 的區(qū)別在于:
Compiler 代表了整個 Webpack 從啟動到關(guān)閉的生命周期,而 Compilation 只是代表了一次新的編譯。
3.3 事件流
webpack 通過 Tapable 來組織這條復(fù)雜的生產(chǎn)線。
webpack 的事件流機制保證了插件的有序性,使得整個系統(tǒng)擴展性很好。
webpack 的事件流機制應(yīng)用了觀察者模式,和 Node.js 中的 EventEmitter 非常相似。
綁定事件
compiler.plugin('event-name', params => { ... });
觸發(fā)事件
compiler.apply('event-name',params)
3.4 需要注意的點
只要能拿到 Compiler 或 Compilation 對象,就能廣播出新的事件,所以在新開發(fā)的插件中也能廣播出事件,給其它插件監(jiān)聽使用。
傳給每個插件的 Compiler 和 Compilation 對象都是同一個引用。也就是說在一個插件中修改了 Compiler 或 Compilation 對象上的屬性,會影響到后面的插件。
有些事件是異步的,這些異步的事件會附帶兩個參數(shù),第二個參數(shù)為回調(diào)函數(shù),在插件處理完任務(wù)時需要調(diào)用回調(diào)函數(shù)通知 webpack,才會進入下一處理流程 。例如:
compiler.plugin('emit',function(compilation, callback) { ... // 處理完畢后執(zhí)行 callback 以通知 Webpack // 如果不執(zhí)行 callback,運行流程將會一直卡在這不往下執(zhí)行 callback(); });
關(guān)于complier和compilation,webpack定義了大量的鉤子事件。開發(fā)者可以根據(jù)自己的需要在任何地方進行自定義處理。
3.5 手寫一個plugin
場景:
小程序mpvue項目,通過webpack編譯,生成子包(我們作為分包引入到主程序中),然后考入主包當(dāng)中。生成子包后,里面的公共靜態(tài)資源wxss引用地址需要加入分包的前綴:/subPages/enjoy_given。
在未編寫插件前,生成的資源是這樣的,這個路徑如果作為分包引入主包,是沒法正常訪問資源的。
所以需求來了:
修改dist/static/css/pages目錄下,所有頁面的樣式文件(wxss文件)引入公共資源的路徑。
因為所有頁面的樣式都會引用通用樣式vender.wxss
那么就需要把@import "/static/css/vendor.wxss"; 改為:@import "/subPages/enjoy_given/static/css/vendor.wxss";復(fù)制代碼
OK 開始!
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) { // 查看對應(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) // 重寫指定輸出模塊內(nèi)容 compilation.assets[filePathName] = { source () { return content; }, size () { return content.length; } } } } callback() }) } } module.exports = CssPathTransfor
看著挺多,實際就是遍歷compilation.assets模塊。對符合要求的文件進行正則替換。
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(), ] })
插件編寫完成后,執(zhí)行編譯命令
以上是“webpack中l(wèi)oader和plugin的示例分析”這篇文章的所有內(nèi)容,感謝各位的閱讀!希望分享的內(nèi)容對大家有幫助,更多相關(guān)知識,歡迎關(guān)注億速云行業(yè)資訊頻道!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。