|
相信開(kāi)發(fā)過(guò)插件的同學(xué),都看過(guò)Writing a Plugin 或類(lèi)似的文章,因?yàn)?mini-program-webpack-loader 這個(gè)工具開(kāi)發(fā)時(shí)正好 webpack 4 發(fā)布了,所以就閱讀了這篇文章,順便看了以下幾篇文檔。 如果你看過(guò)文檔,相信你一定知道:
如果感覺(jué)無(wú)從著手,可以繼續(xù)看看我是如何一步步開(kāi)發(fā)并完善 mini-program-webpack-loader 來(lái)打包小程序的。 小程序有一個(gè)固定的套路,首先需要有一個(gè) app.json 文件來(lái)定義所有的頁(yè)面路徑,然后每個(gè)頁(yè)面有四個(gè)文件組成:.js,.json,.wxml,.wxss。所以我以 app.json 作為 webpack entry,當(dāng) webpack 執(zhí)行插件的 apply 的時(shí)候,通過(guò)獲取 entry 來(lái)知道小程序都有哪些頁(yè)面。大概流程像下面一張圖,一個(gè)小程序打包插件差不多就這樣完成了。
這里使用了兩個(gè)插件 MultiEntryPlugin,SingleEntryPlugin。為什么要這樣做呢?因?yàn)?webpack 會(huì)根據(jù)你的 entry 配置(這里的 entry 不只是 webpack 配置里的 entry,import(), require.ensure() 都會(huì)生成一個(gè) entry)來(lái)決定生成文件的個(gè)數(shù),我們不希望把所有頁(yè)面的 js 打包到一個(gè)文件,需要使用 SingleEntryPlugin 來(lái)生成一個(gè)新的 entry module;而那些靜態(tài)資源,我們可以使用 MultiEntryPlugin 插件來(lái)處理,把這些文件作為一個(gè) entry module 的依賴(lài),在 loader 中配置 file-loader 即可把靜態(tài)文件輸出。偽代碼如下:
const MultiEntryPlugin = require('webpack/lib/MultiEntryPlugin');
const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');
class MiniPlugin {
apply (compiler) {
let options = compiler.options
let context = compiler.rootContext
let entry = options.entry
let files = loadFiles(entry)
let scripts = files.filter(file => /\.js$/.test(file))
let assets = files.filter(file => !/\.js$/.test(file))
new MultiEntryPlugin(context, assets, '__assets__').apply(compiler)
scripts.forEach((file => {
let fileName = relative(context, file).replace(extname(file), '');
new SingleEntryPlugin(context, file, fileName).apply(compiler);
})
}
}
復(fù)制代碼
當(dāng)然,如果像上面那樣做,你會(huì)發(fā)現(xiàn)最后會(huì)多出一個(gè) main.js,xxx.js(使用 MultiEntryPlugin 時(shí)填的名字),main.js 對(duì)應(yīng)的是配置的 entry 生成的文件,xxx.js 則是 MultiEntryPlugin 生成的。這些文件不是我們需要的,所以需要去掉他。如果熟悉 webpack 文檔,我們有很多地方可以修改最終打包出來(lái)的文件,如 compiler 的 emit 事件,compilation 的 optimizeChunks 相關(guān)的事件都可以實(shí)現(xiàn)。其本質(zhì)上就是去修改 compilation.assets 對(duì)象。 在 mini-program-webpack-loader 中就使用了 emit 事件來(lái)處理這種不需要輸出的內(nèi)容。大概流程就像下面這樣:
小程序打包當(dāng)然沒(méi)這么簡(jiǎn)單,還得支持wxml、wxss、wxs和自定義組件的引用,所以這個(gè)時(shí)候就需要一個(gè) loader 來(lái)完成了,loader 需要做的事情也非常簡(jiǎn)單 —— 解析依賴(lài)的文件,如 .wxml 需要解析 import 組件的 src,wxs 的 src,.wxss 需要解析 @import,wxs 的 require,最后在 loader 中使用 loadModule 方法添加即可。自定義組件一開(kāi)始在 add entry 步驟的時(shí)候直接獲取了,所以不需要 loader 來(lái)完成。這個(gè)時(shí)候的圖:
這樣做也沒(méi)什么問(wèn)題,可是開(kāi)發(fā)體驗(yàn)是比較差的,如再添加一個(gè)自定義組件,一個(gè)頁(yè)面,webpack 是無(wú)感知的,所以需要在頁(yè)面中的 .json 發(fā)生改變時(shí)檢查是不是新增了自定義組件或者新增了頁(yè)面。這個(gè)時(shí)候遇到一個(gè)問(wèn)題,自定義組件的 js 是不能通過(guò) addModule 的方式來(lái)添加的,因?yàn)樽远x組件的 js 必須作為獨(dú)立的入口文件。在 loader 中是做不了,所以嘗試把文件傳到 plugin 中(因?yàn)?plugin 先于 loader 執(zhí)行,所以是可以建立 loader 和 plugin 通信的)。簡(jiǎn)單粗暴的方式:
// loader.js
class MiniLoader {}
module.exports = function (content) {
new MiniLoader(this, content)
}
module.exports.$applyPluginInstance = function (plugin) {
MiniLoader.prototype.$plugin = plugin
}
// plugin.js
const loader = require('./loader')
class MiniPlugin {
apply (compiler) {
loader.$applyPluginInstance(this);
}
}
復(fù)制代碼
但是...。文件是傳到 plugin 了,可是再使用 SingleEntryPlugin 時(shí)你會(huì)發(fā)現(xiàn),沒(méi)效果。因?yàn)樵?compiler make 之后 webpack 已經(jīng)不能感知新的 module 添加了,所以是沒(méi)有用的,這個(gè)時(shí)候就需要根據(jù)文檔猜,怎么樣才能讓 webpack 感知到新的 module,根據(jù)文檔中的事件做關(guān)鍵字查詢(xún),可以發(fā)現(xiàn)在編譯完成的時(shí)候會(huì)調(diào)用 compilation needAdditionalPass 事件鉤子:
this.emitAssets(compilation, err => {
if (err) return finalCallback(err);
if (compilation.hooks.needAdditionalPass.call()) {
compilation.needAdditionalPass = true;
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
this.hooks.done.callAsync(stats, err => {
if (err) return finalCallback(err);
this.hooks.additionalPass.callAsync(err => {
if (err) return finalCallback(err);
this.compile(onCompiled);
});
});
return;
}
this.emitRecords(err => {
if (err) return finalCallback(err);
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
this.hooks.done.callAsync(stats, err => {
if (err) return finalCallback(err);
return finalCallback(null, stats);
});
});
});
復(fù)制代碼
如果在這個(gè)事件鉤子返回一個(gè) true 值,則可以使 webpack 調(diào)用 compiler additionalPass 事件鉤子,嘗試在這里添加文件,果然是可以的。這個(gè)時(shí)候的圖就成了這樣:
當(dāng)然,小程序打包還有些不同的地方,比如分包,如何用好 splitchunk,就不在啰嗦了,當(dāng)你開(kāi)始以后你會(huì)發(fā)現(xiàn)有很多的方法來(lái)實(shí)現(xiàn)想要的效果。 插件開(kāi)發(fā)到這里差不多了,總的來(lái)說(shuō),webpack 就是變著花樣的回調(diào),當(dāng)你知道每個(gè)回調(diào)該做什么的時(shí)候,webpack 用起來(lái)就輕松了。明顯我不知道,因?yàn)樵陂_(kāi)發(fā)過(guò)程中遇到了一些問(wèn)題。 遇到的問(wèn)題1.如何在小程序代碼中支持 resolve alias,node_modules? 既然是工具,當(dāng)然需要做更多的事情,有贊的小程序那么復(fù)雜,如果支持 resolve alias,node_modules 可以使得項(xiàng)目更方便維護(hù),或許你會(huì)說(shuō)這不是 webpack 最基本的功能嗎,不是的,我們當(dāng)然是希望可以在任何文件中使用 alias,node_modules 支持的不僅僅是 js。當(dāng)然這樣做就意味著事情將變得復(fù)雜,首先就是獲取文件路徑,必須是異步的,因?yàn)樵?webpack 4 中 resolve 不再支持 sync。其次就是小程序的目錄名不能是 node_modules,這時(shí)就需要一種計(jì)算相對(duì)路徑的規(guī)則,還是相對(duì)打包輸出的,而不是相對(duì)當(dāng)前項(xiàng)目目錄。 2.多個(gè)小程序項(xiàng)目的合并 有贊從小程序來(lái)講,有微商城版,有零售版,以及公共版,其中大多基礎(chǔ)功能,業(yè)務(wù)都是相同的,當(dāng)然不能再每個(gè)小程序在開(kāi)發(fā)一次,所以這個(gè)工具具備合并多個(gè)小程序當(dāng)然是必須的。這樣的合并稍微又要比從 node_modules 中取文件復(fù)雜一些,因?yàn)樾枰WC多個(gè)小程序合并后的頁(yè)面是正確的,而且要保證路徑不變。 這兩個(gè)問(wèn)題的最終的解決方案既是以 webpack rootContext 的 src 目錄為基準(zhǔn)目錄,以該目錄所在路徑計(jì)算打包文件的絕對(duì)路徑,然后根據(jù)入口文件的 app.json 所在目錄的路徑計(jì)算出最終輸出路徑。
exports.getDistPath = (compilerContext, entryContexts) => {
/**
* webpack 以 config 所在目錄的 src 為打包入口
* 所以可以根據(jù)該目錄追溯源文件地址
*/
return (path) => {
let fullPath = compilerContext
let npmReg = /node_modules/g
let pDirReg = /^[_|\.\.]\//g
if (isAbsolute(path)) {
fullPath = path
} else {
// 相對(duì)路徑:webpack 最后生成的路徑,打包入口外的文件都以 '_' 表示上級(jí)目錄
while (pDirReg.test(path)) {
path = path.substr(pDirReg.lastIndex)
fullPath = join(fullPath, '../')
}
if (fullPath !== compilerContext) {
fullPath = join(fullPath, path)
}
}
// 根據(jù) entry 中定義的 json 文件目錄獲取打包后所在目錄,如果不能獲取就返回原路徑
let contextReg = new RegExp(entryContexts.join('|'), 'g')
if (fullPath !== compilerContext && contextReg.exec(fullPath)) {
path = fullPath.substr(contextReg.lastIndex + 1)
console.assert(!npmReg.test(path), `文件${path}路徑錯(cuò)誤:不應(yīng)該還包含 node_modules`)
}
/**
* 如果有 node_modules 字符串,則去模塊名稱(chēng)
* 如果 app.json 在 node_modules 中,那 path 不應(yīng)該包含 node_modules
*/
if (npmReg.test(path)) {
path = path.substr(npmReg.lastIndex + 1)
}
return path
}
}
復(fù)制代碼
3.如何把子包單獨(dú)依賴(lài)的內(nèi)容打包到子包內(nèi) 解決這個(gè)問(wèn)題的方法是通過(guò) optimizeChunks 事件,在每個(gè) chunk 的依賴(lài)的 module 中添加這個(gè) chunk 的入口文件,然后在 splitChunk 的 test 配置中檢查 module 被依賴(lài)的數(shù)量。如果只有一個(gè),并且是被子包依賴(lài),則打包到子包內(nèi)。 4.webpack 支持單文件失敗 這是一個(gè)未解決的問(wèn)題,當(dāng)嘗試使用 webpack 來(lái)支持單文件的時(shí)候,好像沒(méi)那么方便:
|