团队最近需要对小程序进行工程化升级,在网上看了《小程序工程化实践》一文收获满满,刚好文章中提到的 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 项目一起阅读。
webpack 信息分析
mina-loader 的作用是将 .mina
文件分离成 .json
、.wxml
、.js
、wxss
四种类型文件。
// 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
,可以看到如下信息
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 的信息做一下格式化
[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 可以看出
- 第一个处理的 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
提取相关类型内容。 - 还有就是在
selector-loader
与file-loader
之间的 loader,我们暂时称他们为中间 loader,不同类型的内容由特定的中间 loader 进行处理。
到此我们大概了解 mina-loader
的工作流程,就是分别按类型提取 app.mina
的内容,然后用特定的 loader 处理特定的内容,最后使用 file-loader
输出文件。
源码分析
mina-loader
执行流程如下,跟上面说的流程差不多
// 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-loader
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;
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。