團隊最近須要對小程序進行工程化升級,在網上看了《小程序工程化實踐》一文收穫滿滿,恰好文章中提到的 mina-webpack 也是咱們團隊正在使用的工具。mina-webpack 是 tinaJS 配套的工程化工具。(有關 tinaJS源碼在個人另外一篇文章有講述)mina-webpack
包括好幾個包,其中 mina-runtime-webpack-plugin
、mina-entry-webpack-plugin
、mina-loader
最爲重要。《小程序工程化實踐》已經講解了 mina-runtime-webpack-plugin
、mina-entry-webpack-plugin
的原理,本文主要講解 mina-loader
原理。可結合 demo 項目一塊兒閱讀。css
mina-loader 的做用是將 .mina
文件分離成 .json
、.wxml
、.js
、wxss
四種類型文件。node
// app.mina <config> { "tabBar": {} } </config> <template> <page>Hello World</page> </template> <script> App({ onLaunch () { console.log('Hello onLauch') } }) </script> <style> page { background: #fff; } </style>
對於 mina-loader 內部作了什麼咱們不從源碼入手,
而是從 webpack 輸出的信息進行的推導。進入 demo 項目的 example 目錄,執行 npm run dev
,能夠看到以下信息webpack
Asset Size Chunks Chunk Names ./app.json 18 bytes [emitted] ./app.wxml 24 bytes [emitted] ./app.wxss 28 bytes [emitted] app.js 314 bytes 0 [emitted] app.js common.js 5.76 kB 1 [emitted] common.js [0] ./app.mina 95 bytes {0} [built] [1] ../node_modules/@tinajs/mina-loader/lib/loaders/parser.js!./app.mina 390 bytes [built] [2] ../node_modules/@tinajs/mina-loader/lib/loaders/selector.js?type=script!./app.mina 66 bytes {0} [built] [3] ../node_modules/@tinajs/mina-loader/node_modules/file-loader/dist/cjs.js?name=./[name].json!../node_modules/@tinajs/mina-loader/lib/loaders/mina-json-file.js?{"publicPath":"/"}!../node_modules/@tinajs/mina-loader/lib/loaders/selector.js?type=config!./app.mina 56 bytes [built] [4] ../node_modules/@tinajs/mina-loader/node_modules/file-loader/dist/cjs.js?name=./[name].wxml!../node_modules/wxml-loader/lib?{"publicPath":"/"}!../node_modules/@tinajs/mina-loader/lib/loaders/selector.js?type=template!./app.mina 56 bytes [built] [5] ../node_modules/@tinajs/mina-loader/node_modules/file-loader/dist/cjs.js?name=./[name].wxss!../node_modules/extract-loader/lib/extractLoader.js?{"publicPath":"/"}!../node_modules/css-loader!../node_modules/@tinajs/mina-loader/lib/loaders/selector.js?type=style!./app.mina 56 bytes [built]
能夠看到輸出了 6 個 module
,webpack loader 是用 !
號做爲分隔的,咱們能夠對上面最後 3 個 module 的信息作一下格式化git
[3] file-loader/dist/cjs.js?name=./[name].json! mina-loader/lib/loaders/mina-json-file.js?{"publicPath":"/"}! selector.js?type=config! app.mina [4] file-loader/dist/cjs.js?name=./[name].wxml! wxml-loader/lib?{"publicPath":"/"}! selector.js?type=template! app.mina [5] file-loader/dist/cjs.js?name=./[name].wxss! extractLoader.js?{"publicPath":"/"}! css-loader! selector.js?type=style! app.mina
webpack loader 是從右到左對於上面信息來講也就是下到上執行的。對於上面的 module 咱們能夠找到一些規律。對於每一個 module,webpack 先是讀取 app.mina
文件而後從下往上依次執行 loader 處理 app.mina
。從上面三個 module 能夠看出github
selector-loader
file-loader
,也就是最後確定有文件輸出,輸出的是 [name].json
、[name].wxml
、[name].wxss
這幾個文件。selecor-loader
有個 type
參數,type=style
輸出的是 [name].wxss
文件、type=template
輸出的是 [name].wxml
文件、type=style
輸出的是 [name].wxss
文件,由此能夠推斷出 selector-loader
的做用是按照 type
參數
對 app.mina
提取相關類型內容。web
selector-loader
與 file-loader
之間的 loader,咱們暫時稱他們爲中間 loader,不一樣類型的內容由特定的中間 loader 進行處理。到此咱們大概瞭解 mina-loader
的工做流程,就是分別按類型提取 app.mina
的內容,而後用特定的 loader 處理特定的內容,最後使用 file-loader
輸出文件。npm
mina-loader
執行流程以下,跟上面說的流程差很少json
// process on 導出圖片 parts 那部分有些問題,實際內容以下 parts = { "style": { "type": "style", "content": /*...*/, }, "config": { "type": "config", "content": /*...*/, }, "script": { "type": "script", "content": /*...*/, }, "template": { "type": "template", "content": /*...*/, } }
配合流程圖看源碼的話理解起來不困難小程序
// mina-loader/lib/loaders/mina // 不一樣類型文件對應的中間 loader const LOADERS = { template: ({ publicPath }) => `${resolve('wxml-loader')}?${JSON.stringify({ publicPath })}`, style: ({ publicPath }) => `${resolve('extract-loader')}?${JSON.stringify({ publicPath })}!${resolve('css-loader')}`, script: () => '', config: ({ publicPath }) => `${minaJSONFileLoaderPath}?${JSON.stringify({ publicPath })}`, } const EXTNAMES = { template: 'wxml', style: 'wxss', script: 'js', config: 'json', } const TYPES_FOR_FILE_LOADER = ['template', 'style', 'config'] const TYPES_FOR_OUTPUT = ['script'] module.exports = function () { // ..... const done = this.async() const options = /*....*/ // 獲取剩餘請求資源的路徑,也就是 xx.mina 的路徑 // 例如 /Users/jinzhanye/Desktop/dev/github/mini/mina-webpack/example/src/app.mina const url = loaderUtils.getRemainingRequest(this) // 前置 !! 表示只執行行內 loader,其餘 loader 都不執行 // 拼接上 parserLoader 的路徑 const parsedUrl = `!!${parserLoaderPath}!${url}` const loadModule = helpers.loadModule.bind(this) // 獲取對應類型的中間 loader const getLoaderOf = (type, options) => { let loader = LOADERS[type](options) || '' // ..... return loader } loadModule(parsedUrl) .then((source) => { let parts = this.exec(source, parsedUrl) // parts 爲如下對象 // { // config: { // content: '.....' // } // wxml: { // content: '.....' // } // // } // compute output // 拼接 selector loader 路徑 // require("!!../node_modules/@tinajs/mina-loader/lib/loaders/selector.js?type=script!./app.mina") let output = parts.script && parts.script.content ? TYPES_FOR_OUTPUT.map((type) => `require(${loaderUtils.stringifyRequest(this, `!!${getLoaderOf(type, options)}${selectorLoaderPath}?type=script!${url}`)})`).join(';') : '' return Promise // emit files .all(TYPES_FOR_FILE_LOADER.map((type) => { if (!parts[type] || !parts[type].content) { return Promise.resolve() } // dirname 爲 '.' let dirname = compose(ensurePosix, helpers.toSafeOutputPath, path.dirname)(path.relative(this.options.context, url)) // 拼接 wxml、json、wxss 請求路徑 let request = `!!${resolve('file-loader')}?name=${dirname}/[name].${EXTNAMES[type]}!${getLoaderOf(type, options)}${selectorLoaderPath}?type=${type}!${url}` return loadModule(request) })) .then(() => done(null, output)) }) .catch(done) }
前文一直沒提到 loadModule 是哪裏來的,其實 loadModule 是 webpack loader 暴露給開發者使用的一個 api,用於在執行 loader 時也能去加載一個模塊。而後再看看 selector-loadersegmentfault
前文提到 selector-loader
是用來提取某一類型的文件內容
module.exports = function (rawSource) { this.cacheable() const cb = this.async() const { type } = loaderUtils.getOptions(this) || {} // url = '!!parser.js!app.mina' const url = `!!${parserLoaderPath}!${loaderUtils.getRemainingRequest(this)}` this.loadModule(url, (err, source) => { if (err) { return cb(err) } const parts = this.exec(source, url) cb(null, parts[type].content) }) }
操做跟 mina-loader
很像,利用 parser-loader
將 .mina
文件轉換成以下對象
parts = { "style": { "type": "style", "content": /*...*/, }, "config": { "type": "config", "content": /*...*/, }, "script": { "type": "script", "content": /*...*/, }, "template": { "type": "template", "content": /*...*/, } }
而後根據 type
返回對應的內容便可。可是這裏有一個問題,咱們看看下面代碼
// mina-loader/lib/loaders/mina.js // ... const parsedUrl = `!!${parserLoaderPath}!${url}` // ... loadModule(parsedUrl) // mina-loader/lib/loaders/parser.js const url = `!!${parserLoaderPath}!${loaderUtils.getRemainingRequest(this)}` this.loadModule(url,/*....*/)
能夠看到 loaderModule(parseredUrl)
也就是 loadModule('!!parser.js!app.mina')
這個模塊被重複加載了屢次,這樣的話會不會帶來性能的損耗呢?答案是不會的,每次 loadModule
後 webpack 會將加載完的 module 以請求路徑爲 key 保存在 Compilation
對象。參考源碼以下
// webpack/lib/Compilation.js addModule(module, cacheGroup) { const identifier = module.identifier(); // identifier 是 request 路徑,在這個案例就是 'parser.js!app.mina' if(this._modules[identifier]) { return false; } // 緩存模塊 this._modules[identifier] = module; if(this.cache) { this.cache[cacheName] = module; } this.modules.push(module); return true; }