Webpack 常見插件原理分析【轉】


轉自:https://www.jianshu.com/p/108d07de0e01

 

本章內容主要講解一下 Webpack 幾個稍微簡單的插件原理,通過本章節的學習,對前面的知識應該會有一個更加深入的理解。
prepack-webpack-plugin 的說明今年 Facebook 開源了一個 prepack,當時就很好奇,它到底和 Webpack 之間的關系是什么?於是各種搜索,最后還是去官網上看了下各種例子。例子都很好理解,但是對於其和 Webpack 的關系還是有點迷糊。最后找到了一個好用的插件,即 prepack-webpack-plugin,這才恍然大悟~

解析 prepack-webpack-plugin 源碼

下面直接給出這個插件的 apply 源碼,因為 Webpack 的 plugin 的所有邏輯都是在 apply 方法中處理的。內容如下:

import ModuleFilenameHelpers from 'webpack/lib/ModuleFilenameHelpers'; import { RawSource } from 'webpack-sources'; import { prepack } from 'prepack'; import type { PluginConfigurationType, UserPluginConfigurationType } from './types'; const defaultConfiguration = { prepack: {}, test: /\.js($|\?)/i }; export default class PrepackPlugin { configuration: PluginConfigurationType; constructor (userConfiguration?: UserPluginConfigurationType) { this.configuration = { ...defaultConfiguration, ...userConfiguration }; } apply (compiler: Object) { const configuration = this.configuration; compiler.plugin('compilation', (compilation) => { compilation.plugin('optimize-chunk-assets', (chunks, callback) => { for (const chunk of chunks) { const files = chunk.files; //chunk.files 獲取該 chunk 產生的所有的輸出文件,記住是輸出文件 for (const file of files) { const matchObjectConfiguration = { test: configuration.test }; if (!ModuleFilenameHelpers.matchObject(matchObjectConfiguration, file)) { // eslint-disable-next-line no-continue continue; } const asset = compilation.assets[file]; //獲取文件本身 const code = asset.source(); //獲取文件的代碼內容 const prepackedCode = prepack(code, { ...configuration.prepack, filename: file }); //所以,這里是在 Webpack 打包后對 ES5 代碼的處理 compilation.assets[file] = new RawSource(prepackedCode.code); } } callback(); }); }); } } 

首先對於 Webpack 各種鈎子函數時機不了解的可以 點擊這里。如果對於 Webpack 中各個對象的屬性不了解的可以點擊這里。接下來對上面的代碼進行簡單的剖析:
(1)首先看 for 循環的前面那幾句:

const files = chunk.files; //chunk.files 獲取該 chunk 產生的所有的輸出文件,記住是輸出文件 for (const file of files) { //這里只會對該 chunk 包含的文件中符合 test 規則的文件進行后續處理 const matchObjectConfiguration = { test: configuration.test }; if (!ModuleFilenameHelpers.matchObject(matchObjectConfiguration, file)) { // eslint-disable-next-line no-continue continue; } } 

這里給出 ModuleFilenameHelpers.matchObject 的代碼:

/將字符串轉化為 regex
function asRegExp(test) { if(typeof test === "string") test = new RegExp("^" + test.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&")); return test; } ModuleFilenameHelpers.matchPart = function matchPart(str, test) { if(!test) return true; test = asRegExp(test); if(Array.isArray(test)) { return test.map(asRegExp).filter(function(regExp) { return regExp.test(str); }).length > 0; } else { return test.test(str); } }; ModuleFilenameHelpers.matchObject = function matchObject(obj, str) { if(obj.test) if(!ModuleFilenameHelpers.matchPart(str, obj.test)) return false; //獲取 test,如果這個文件名稱符合 test 規則返回 true,否則為 false if(obj.include) if(!ModuleFilenameHelpers.matchPart(str, obj.include)) return false; if(obj.exclude) if(ModuleFilenameHelpers.matchPart(str, obj.exclude)) return false; return true; }; 

這幾句代碼是一目了然的,如果這個產生的文件名稱符合 test 規則返回 true,否則為 false。
(2)繼續看后面對於符合規則的文件的處理

 //如果滿足規則繼續處理~ const asset = compilation.assets[file]; //獲取編譯產生的資源 const code = asset.source(); //獲取文件的代碼內容 const prepackedCode = prepack(code, { ...configuration.prepack, filename: file }); //所以,這里是在 Webpack 打包后對 ES5 代碼的處理 compilation.assets[file] = new RawSource(prepackedCode.code); 

其中 asset.source 表示的是模塊的內容,可以
點擊這里查看。假如模塊是一個 html,內容如下:

<header class="header">{{text}}</header> 

最后打包的結果為:

module.exports = "<header class=\\"header\\">{{text}}</header>";' } 

這也是為什么會有下面的代碼:

compilation.assets[basename] = {
      source: function () { return results.source; }, //source 是文件的內容,通過 fs.readFileAsync 完成 size: function () { return results.size.size; //size 通過 fs.statAsync(filename) 完成 } }; return basename; }); 

前面兩句代碼都分析過了,繼續看下面的內容:

const prepackedCode = prepack(code, { ...configuration.prepack, filename: file }); //所以,這里是在 Webpack 打包后對 ES5 代碼的處理 compilation.assets[file] = new RawSource(prepackedCode.code); 

此時才真正的對 Webpack 打包后的代碼進行處理,prepack的nodejs 用法可以 查看這里。最后一句代碼其實就是操作我們的輸出資源,在輸出資源中添加一個文件,文件的內容就是 prepack 打包后的代碼。其中 webpack-source 的內容可以 點擊這里。按照官方的說明,該對象可以獲取源代碼、hash、內容大小、sourceMap 等所有信息。我們給出對 RowSourceMap 的說明:

RawSource
Represents source code without SourceMap. new RawSource(sourceCode: String) 

很顯然,就是顯示源代碼而不包含 sourceMap。

prepack-webpack-plugin 總結

所以,prepack 作用於 Webpack 的時機在於:將源代碼轉化為 ES5 以后。從上面的 html 的編譯結果就可以知道了,至於它到底做了什么,以及如何做的,還請查看 官網

BannerPlugin 插件分析

我們現在講述一下 BannerPlugin 內部的原理。它的主要用法如下:

{
  banner: string, // the banner as string, it will be wrapped in a comment raw: boolean, //如果配置了 raw,那么 banner 會被包裹到注釋當中 entryOnly: boolean, //如果設置為 true,那么 banner 僅僅會被添加到入口文件產生的 chunk 中 test: string | RegExp | Array, include: string | RegExp | Array, exclude: string | RegExp | Array, } 

我們看看它的內部代碼:

"use strict"; const ConcatSource = require("webpack-sources").ConcatSource; const ModuleFilenameHelpers = require("./ModuleFilenameHelpers"); //'This file is created by liangklfangl' =>/*! This file is created by liangklfangl */ function wrapComment(str) { if(!str.includes("\n")) return `/*! ${str} */`; return `/*!\n * ${str.split("\n").join("\n * ")}\n */`; } class BannerPlugin { constructor(options) { if(arguments.length > 1) throw new Error("BannerPlugin only takes one argument (pass an options object)"); if(typeof options === "string") options = { banner: options }; this.options = options || {}; //配置參數 this.banner = this.options.raw ? options.banner : wrapComment(options.banner); } apply(compiler) { let options = this.options; let banner = this.banner; compiler.plugin("compilation", (compilation) => { compilation.plugin("optimize-chunk-assets", (chunks, callback) => { chunks.forEach((chunk) => { //入口文件都是默認首次加載的,即 isInitial為true 和 require.ensure 按需加載是完全不一樣的 if(options.entryOnly && !chunk.isInitial()) return; chunk.files .filter(ModuleFilenameHelpers.matchObject.bind(undefined, options)) //只要滿足 test 正則表達式的文件才會被處理 .forEach((file) => compilation.assets[file] = new ConcatSource( banner, "\n", compilation.assets[file] //在原來的輸出文件頭部添加我們的 banner 信息 ) ); }); callback(); }); }); } } module.exports = BannerPlugin; 

EnvironmentPlugin 插件分析
該插件的使用方法如下:

new webpack.EnvironmentPlugin(['NODE_ENV', 'DEBUG']) 

此時相當於以以下方式使用 DefinePlugin 插件:

new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 'process.env.DEBUG': JSON.stringify(process.env.DEBUG) }) 

當然,該插件也可以傳入一個對象:

new webpack.EnvironmentPlugin({ NODE_ENV: 'development', // use 'development' unless process.env.NODE_ENV is defined DEBUG: false }) 

假如有如下的 entry 文件:

if (process.env.NODE_ENV === 'production') { console.log('Welcome to production'); } if (process.env.DEBUG) { console.log('Debugging output'); } 

如果執行 NODE_ENV=production webpack 命令,那么會發現輸出文件為如下內容:

if ('production' === 'production') { // <-- 'production' from NODE_ENV is taken console.log('Welcome to production'); } if (false) { // <-- default value is taken console.log('Debugging output'); } 

上面講述了這個插件如何使用,來看看它的內部原理是什么?

"use strict"; const DefinePlugin = require("./DefinePlugin"); //1.EnvironmentPlugin 內部直接調用 DefinePlugin class EnvironmentPlugin { constructor(keys) { this.keys = Array.isArray(keys) ? keys : Object.keys(arguments); } apply(compiler) { //2.這里直接使用 compiler.apply 方法來執行 DefinePlugin 插件 compiler.apply(new DefinePlugin(this.keys.reduce((definitions, key) => { const value = process.env[key]; //獲取 process.env 中的參數 if(value === undefined) { compiler.plugin("this-compilation", (compilation) => { const error = new Error(key + " environment variable is undefined."); error.name = "EnvVariableNotDefinedError"; //3.可以往 compilation.warning 里面填充編譯 warning 信息 compilation.warnings.push(error); }); } definitions["process.env." + key] = value ? JSON.stringify(value) : "undefined"; //4.將所有的 key 都封裝到 process.env 上面了並返回(注意這里是向 process.env 上賦值) return definitions; }, {}))); } } module.exports = EnvironmentPlugin; 
MinChunkSizePlugin 插件分析

這個插件的作用在於,如果產生的某個 Chunk 的大小小於閾值,那么直接和其他的 Chunk 合並,其主要使用方法如下:

new webpack.optimize.MinChunkSizePlugin({ minChunkSize: 10000 }) 

來看下它的內部原理是如何實現的:

class MinChunkSizePlugin { constructor(options) { if(typeof options !== "object" || Array.isArray(options)) { throw new Error("Argument should be an options object.\nFor more info on options, see https://webpack.github.io/docs/list-of-plugins.html"); } this.options = options; } apply(compiler) { const options = this.options; const minChunkSize = options.minChunkSize; compiler.plugin("compilation", (compilation) => { compilation.plugin("optimize-chunks-advanced", (chunks) => { let combinations = []; chunks.forEach((a, idx) => { for(let i = 0; i < idx; i++) { const b = chunks[i]; combinations.push([b, a]); } }); const equalOptions = { chunkOverhead: 1, // an additional overhead for each chunk in bytes (default 10000, to reflect request delay) entryChunkMultiplicator: 1 //a multiplicator for entry chunks (default 10, entry chunks are merged 10 times less likely) //入口文件乘以的權重,所以如果含有入口文件,那么更加不容易小於 minChunkSize,所以入口文件過小不容易被集成到別的 chunk 中 }; combinations = combinations.filter((pair) => { return pair[0].size(equalOptions) < minChunkSize || pair[1].size(equalOptions) < minChunkSize; }); //對數組中元素進行刪選,至少有一個 chunk 的值是小於 minChunkSize 的 combinations.forEach((pair) => { const a = pair[0].size(options); const b = pair[1].size(options); const ab = pair[0].integratedSize(pair[1], options); //得到第一個 chunk 集成了第二個 chunk 后的文件大小 pair.unshift(a + b - ab, ab); //這里的 pair 是如[0,1]、[0,2]等這樣的數組元素,前面加上兩個元素:集成后總體積的變化量;集成后的體積 }); //此時 combinations 的元素至少有一個的大小是小於 minChunkSize 的 combinations = combinations.filter((pair) => { return pair[1] !== false; }); if(combinations.length === 0) return; //如果沒有需要優化的,直接返回 combinations.sort((a, b) => { const diff = b[0] - a[0]; if(diff !== 0) return diff; return a[1] - b[1]; }); //按照集成后變化的體積來比較,從大到小排序 const pair = combinations[0]; //得到第一個元素 pair[2].integrate(pair[3], "min-size"); //pair[2] 是 chunk,pair[3] 也是 chunk chunks.splice(chunks.indexOf(pair[3]), 1); //從 chunks 集合中刪除集成后的 chunk return true; }); }); } } module.exports = MinChunkSizePlugin; 

下面給出主要的代碼:

var combinations = []; var chunks=[0,1,2,3] chunks.forEach((a, idx) => { for(let i = 0; i < idx; i++) { const b = chunks[i]; combinations.push([b, a]); } }); 

變量 combinations 是組合形式,把自己和前面比自己小的元素組合成為一個元素。之所以是選擇比自己的小的情況是為了減少重復的個數,如 [0,2] 和 [2,0] 必須只有一個。

本章小結

在本章節中主要講了幾個稍微簡單一點的 Webpack 的 Plugin,如果對於 Plugin 的原理比較感興趣,在前面介紹的那些基礎知識已經夠用了。至於很多復雜的 Plugin 就需要在平時開發的時候多關注和學習了。更多 Webpack 插件的分析也可以

點擊這里,而至於插件本身的用法,官網

就已經足夠了



作者:Dabao123
鏈接:https://www.jianshu.com/p/108d07de0e01
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM