mina-loader 源碼淺析

好風光

團隊最近須要對小程序進行工程化升級,在網上看了《小程序工程化實踐》一文收穫滿滿,恰好文章中提到的 mina-webpack 也是咱們團隊正在使用的工具。mina-webpack 是 tinaJS 配套的工程化工具。(有關 tinaJS源碼在個人另外一篇文章有講述)mina-webpack 包括好幾個包,其中 mina-runtime-webpack-pluginmina-entry-webpack-pluginmina-loader 最爲重要。《小程序工程化實踐》已經講解了 mina-runtime-webpack-pluginmina-entry-webpack-plugin 的原理,本文主要講解 mina-loader 原理。可結合 demo 項目一塊兒閱讀。css

webpack 信息分析

mina-loader 的做用是將 .mina 文件分離成 .json.wxml.jswxss 四種類型文件。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

  • 第一個處理的 loader 都是 selector-loader
  • 最後一個 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-loaderfile-loader 之間的 loader,咱們暫時稱他們爲中間 loader,不一樣類型的內容由特定的中間 loader 進行處理。

到此咱們大概瞭解 mina-loader 的工做流程,就是分別按類型提取 app.mina 的內容,而後用特定的 loader 處理特定的內容,最後使用 file-loader 輸出文件。npm

源碼分析

mina-loader 執行流程以下,跟上面說的流程差很少json

mina-loader流程.png

// 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

前文提到 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;
}

感謝如下項目以及文章

相關文章
相關標籤/搜索