相信開(kāi)發(fā)過(guò)插件的同學(xué),都看過(guò)Writing a Plugin 或類似的文章,因?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 的依賴,在 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)單 —— 解析依賴的文件,如 .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)鍵字查詢,可以發(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 字符串,則去模塊名稱 * 如果 app.json 在 node_modules 中,那 path 不應(yīng)該包含 node_modules */ if (npmReg.test(path)) { path = path.substr(npmReg.lastIndex + 1) } return path } } 復(fù)制代碼 3.如何把子包單獨(dú)依賴的內(nèi)容打包到子包內(nèi) 解決這個(gè)問(wèn)題的方法是通過(guò) optimizeChunks 事件,在每個(gè) chunk 的依賴的 module 中添加這個(gè) chunk 的入口文件,然后在 splitChunk 的 test 配置中檢查 module 被依賴的數(shù)量。如果只有一個(gè),并且是被子包依賴,則打包到子包內(nèi)。 4.webpack 支持單文件失敗 這是一個(gè)未解決的問(wèn)題,當(dāng)嘗試使用 webpack 來(lái)支持單文件的時(shí)候,好像沒(méi)那么方便:
|
工作日 8:30-12:00 14:30-18:00
周六及部分節(jié)假日提供值班服務(wù)