寫在前面
弄清楚了tapable,我們基本上對webpack的plugin沒有什么問題了,下面我們將webpack的plugin和loader組合起來看一下。我們主要從以下幾個方面來深入研究。照着網上的資料寫幾個例子(模仿),然后大致了解loader是什么(作用),它是怎么執行的(原理)以及找一個比較有代表性的loader來看看它的代碼(源碼),最后我們可以自己寫一個自定義的loader(學以致用)。plugin也可以套用這樣的模式來進行學習。參考了官網給出的一些例子之后plugin和loader之后
,我開了兩個工程webpack-demo-loader和webpack-demo-plugin來寫一些示例來深入研究下webpack的loader和plugin
loader
loader即加載器,由於webpack只能打包js模塊,如果要打包其他文件,如jsx、vue、css等文件,必須要將其轉化成js模塊形式。而這個轉化的中間工具就是loader,我們可以理解為loader即文件的預處理。例如我們要打包css文件,我們就要使用css-loader和style-loader,要打包圖片文件我們要使用file-loader或者url-loader。我們常用的loader有如下幾個:
style-loader、css-loader、vue-loader、babel-loader、ts-loader、file-loader、url-loader等
loader使用
loader的使用方法大概就是在webpack.config.js配置文件中的module的rules中添加項。具體的使用方法在官網的文檔loader以及Loader Interface寫的非常清楚,照着寫幾遍大概就能熟練使用了。我們主要看下Loader Interface這節,這里解釋了loader的原理,對我們理解loader以及后面寫自定義loader有很重大的意義。loader 本質上是導出為函數的 JavaScript 模塊。loader runner 會調用此函數,然后將上一個 loader 產生的結果或者資源文件傳入進去。loader也給出了很多鈎子,都存在上下文對象this上的,這個上下文對外開放了很多方法,我們能很輕松的操作源碼字符串。
loader執行原理
為了研究loader的執行原理,我們建了一個webpack應用實例來專門探索loader。webpack-demo-loader,通過上述的例子我們可以了解到如何在項目中配置使用loader。通過使用上述幾個常用的loader我們可以大致窺探出webpack執行loader的工作的過程,大概的過程就是先注冊,可以理解成數據結構--棧。
vue-loader研究
vue-loader是一個比較有代表性的loader,我們找到其源碼來研究一下vue的模版語法。基本上就是基於webpack從0開始創建一個vue腳手架項目webpack-demo-vueloader
創建該工程也比較簡單,按照如下步驟即可
- 創建目錄並初始化
mkdir webpack-demo-vueloader && cd webpack-demo-vueloader
npm init -y
- 安裝vue vue-loader vue-template-compiler
- 安裝 webpack webpack-cli webpack-dev-server(這三個包版本會相互影響,所以固定版本了)
npm install --save vue vue-loader vue-template-compiler
npm install --save webpack webpack-cli webpack-dev-server@3.11.0
根目錄新建webpack.config.js
const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.exports = {
entry: './src/index.js',
mode: 'development',
output: {
filename: '[name].bundle.js',
path: __dirname + '/dist'
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
},
devServer: {
contentBase: __dirname + '/dist',
host: '127.0.0.1',
port: 8000
},
plugins: [
new VueLoaderPlugin()
],
}
- 新建src目錄,創建index.js和app.vue
import Vue from 'vue';
import App from './app.vue';
new Vue({
el: '#root',
render: h => h(App)
});
<template>
<div class="example">{{ msg }}</div>
</template>
<script>
export default {
data() {
return {
msg: "Hello world!",
};
},
created: function () {
console.log("create");
},
mounted: function () {
console.log("mounted");
},
};
</script>
- 最后在package.json的script下添加執行命令就完成了一個最基本的vue腳手架
"start": "webpack serve",
"build": "webpack",
測試項目可以完全跑起來了,我們下一步來研究一下vue-loader是如何工作的,從上面創建的工程中package.json中依賴的包我們可以看到,要讓vue項目完整的在webpack中跑起來,最少要依賴三個包,分別是vue、vue-loader和vue-template-compiler。我們知道vue-loader和vue-template-compiler是webpack和vue的中間連接接工具,查看源碼我們得知,vue-template-compiler主要的作用是將vue的模版代碼的字符串編譯成ast和渲染函數的形式。vue-loader的作用在官網解釋的很清楚,總結起來就是一句話,vue-loader的作用就是能將.vue文件轉化成渲染函數的形式,可以讓webpack完成打包。webpack只能打包js模塊,在單頁應用中,一個js文件可以理解為一個js模塊,vue-loader的作用就是能將.vue文件編譯成webpack能夠識別的模塊形式.
寫一個自定義的loader
我們知道,loader能對js模塊字符串的形式進行操作,那我們就寫一個能夠幫助我們清除console.log的loader。源碼地址在webpack-demo-vueloader里的loaders/removeLog.js
plugin
上一節我們知道了webpack的plugin的核心是tapable,本質上是一個訂閱-發布模式,先把插件寫好注冊到webpack.config.js文件中,在編譯源代碼的時候再調用它。我們呢常用的plugin有如下幾個:
webpack-bundle-analyzer、CommonsChunkPlugin、DllPlugin、ExtractTextWebpackPlugin、HtmlWebpackPlugin、HotModuleReplacementPlugin等
plugin使用
我們說loader就是一個js函數,可以操作js字符串形式的函數,那么plugin就是一個類。plugin使用非常簡單,如果是引用第三方插件,也就是別人寫好的npm包,只需在webpack.config.js配置一下即可,首先require進來,然后在plugins中new一個實例,如果有參數,直接傳進去即可。示例(html-webpack-plugin)如下
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
...
plugins: [
new HtmlWebpackPlugin()
]
...
}
自定義的plugin,首先需要你先開發好plugin(參考tapable demo、compiler和compilation),然后用同樣的方式引入即可。
plugin會深入到代碼編譯和構建的各個階段,學習plugin可以幫助我們窺探代碼編譯的全過程。plugin對外提供了兩個主要的鈎子,一個是compiler,另一個是compilation。
- Compiler 對象包含了 Webpack 環境所有的的配置信息,包含 options,loaders,plugins 這些信息,這個對象在 Webpack 啟動時候被實例化,它是全局唯一的,可以簡單地把它理解為 Webpack 實例;
- Compilation 對象包含了當前的模塊資源、編譯生成資源、變化的文件等。當 Webpack 以開發模式運行時,每當檢測到一個文件變化,一次新的 Compilation 將被創建。Compilation 對象也提供了很多事件回調供插件做擴展。通過 Compilation 也能讀取到 Compiler 對象。
VueLoaderPlugin研究
VueLoaderPlugin是有兩個plugin分別兼容webpack的<=4、5兩個版本。為什么會有兼容問題,查看文檔發現webpack<=4 normalModuleLoader已經廢棄,也就是說webpack<=4可以使用normalModuleLoader鈎子來訪問loader,webpack5就要使用NormalModule.getCompilationHooks(compilation).loader來訪問loader。
plugin-webpack4.js
plugin-webpack5.js
兩個plugin的代碼量都不多,我們就來讀一下plugin-webpack5.js的代碼,看下這個plugin究竟做了什么事。官網上也說了很清楚,vueLoaderPlugin的職責是將你定義過的其它規則復制並應用到.vue 文件里相應語言的塊。例如,如果你有一條匹配 /.js$/ 的規則,那么它會應用到.vue 文件里的<script>塊。一個vue文件包含template模版,script和style等3個部分,編譯的時候需要將他們區分開,並且把loader應用到各個部分,因此需要這個plugin來做這件事。
寫一個自定義plugin
在寫自定義plugin之前,我們要了解webpack打包過程,大概有如下幾個步驟
- 讀取配置
- 生成compiler(編譯器)對象
- 初始化
- run/watch
- 生成compilation(構建包)
- emit:文件內容准備完成,准備生成文件
- afterEmit:文件已經寫入磁盤完成
- done: 完成編譯
在compilation生成之后,emit階段之前我們都可以操作源碼模塊,compilation也提供了很多對外的鈎子,以便開發者能更好的操作模塊。
弄清楚大概的打包過程之后,我們就知道怎么使用webpack plugin compiler鈎子和compilation鈎子的使用時機了。
上面的demo我先把構建之后的包輸出到一個json文件中,以便我們查看構建之后的模塊是什么結構還有里面有什么內容。通過構建完成之后的包可以發現我們的js代碼已經完全轉化成ast的形式,我們可以在astexplorer網站上測試我們的js代碼。