深入 mpvue compiler 原理
深入 mpvue-loader 原理
mpvue-loader 源码位于:node_modules/mpvue-loader/lib/loader.js,webpack 会向 loader 中传入 content 参数,content 即 .vue 源码字符串,mpvue 项目至少需要包含两个 .vue 文件,第一个是 App.vue,第二个是页面组件,这里我们只分析页面组件的解析逻辑,组件的源码如下:
<template>
<div>{{message}}</div>
</template>
<script>
export default {
data () {
return {
message: 'Hello World'
}
}
}
</script>
<style scoped>
div {
color: red;
}
</style>
以上内容也是 content 的内容,下面我们就具体分析 mpvue-loader 如何完成解析过程。
options 解析
mpvue-loader 开始执行时会获取 options,源码如下:
const mpOptions = loaderUtils.getOptions(this) || {}
该 options 由 /build/webpack.base.conf.js 传入:
{
test: /\.vue$/,
loader: 'mpvue-loader',
options: vueLoaderConfig
}
该 options 主要包含以下几类信息:
- fileExt:文件类型与后缀名的对应关系,微信小程序的内容如下:
{
"template": "wxml",
"script": "js",
"style": "wxss",
"platform": "wx"
}
- loaders:loaders 对象用于将不同文件类型映射到不同的 loader 中。之前章节中我们提到 mpvue-loader 会对不同的文件类型予以不同的 loader 进行处理,具体文件类型对应哪个 loader 就是从这里选取的,这里 loaders 中属性即 webpack 的 loader 配置,这里节选 css 和 wxss 的处理 loader:
{
"css": [
{
"loader": "/Users/sam/Desktop/mpvue-test-project/node_modules/_extract-text-webpack-plugin@3.0.2@extract-text-webpack-plugin/dist/loader.js",
"options": {
"omit": 1,
"remove": true
}
},
{
"loader": "vue-style-loader"
},
{
"loader": "css-loader",
"options": {
"minimize": true,
"sourceMap": false
}
},
{
"loader": "px2rpx-loader",
"options": {
"baseDpr": 1,
"rpxUnit": 0.5
}
},
{
"loader": "postcss-loader",
"options": {
"sourceMap": true
}
}
],
"wxss": [
{
"loader": "/Users/sam/Desktop/mpvue-test-project/node_modules/_extract-text-webpack-plugin@3.0.2@extract-text-webpack-plugin/dist/loader.js",
"options": {
"omit": 1,
"remove": true
}
},
{
"loader": "vue-style-loader"
},
{
"loader": "css-loader",
"options": {
"minimize": true,
"sourceMap": false
}
},
{
"loader": "px2rpx-loader",
"options": {
"baseDpr": 1,
"rpxUnit": 0.5
}
},
{
"loader": "postcss-loader",
"options": {
"sourceMap": true
}
}
]
}
可以看到对于 css 而言,需要依次经历 postcss-loader、px2rpx-loader、css-loader、vue-style-loader 和 extract-text-webpack-plugin/dist/loader.js 5 个 loader 的处理,这里的 px2rpx-loader 是专门针对小程序的,将 px 转化为 rpx 的处理 loader,这里就是 mpvue-loader 对小程序的专属定制部分。从 loaders 的获取我们可以看到后续 mpvue-loader 将对源码文件进行分割,将不同的源码文件映射到不同的 loader 中进行处理。
文件分割准备
mpvue-loader 首先会获取源码文件的路径,源码如下:
var rawRequest = getRawRequest(this, options.excludedPreLoaders)
var filePath = this.resourcePath
var fileName = path.basename(filePath)
这里 rawRequest 和 filePath 都指向 vue 源码文件的绝对路径,fileName 则通过 path 库提取了文件名,执行后这三个变量的结果如下:
- rawRequest:/Users/username/Desktop/mpvue-test-project/src/pages/index/index.vue
- filePath:/Users/username/Desktop/mpvue-test-project/src/pages/index/index.vue
- fileName:index.vue
接下来 mpvue-loader 会进一步对源码文件的路径进行解析:
var context = (this._compiler && this._compiler.context) || this.options.context || process.cwd()
var moduleId = 'data-v-' + genId(filePath, context, options.hashKey)
var shortFilePath = path.relative(context, filePath).replace(/^(\.\.\/)+/, '')
这里的 context 并不是我们常见的运行时执行上下文,而是指令执行的路径,这里它的值为:
/Users/username/Desktop/mpvue-test-project
该路径即我们执行 node build/build.js 命令的路径。moduleId 为根据 filePath、context 和 options.hashKey 混合生成的 id,在我的环境下 moduleId 的值为 data-v-32ccf774,shortFilePath 为 context 到源码文件的相对路径,在我的环境下它的值为:src/pages/index/index.vue。接下来 mpvue-loader 会准备 cssLoader 的参数:
var cssLoaderOptions = ''
if (!isProduction && this.sourceMap && options.cssSourceMap !== false) {
cssLoaderOptions += '?sourceMap'
}
if (isProduction) {
cssLoaderOptions += (cssLoaderOptions ? '&' : '?') + 'minimize'
}
这里的 cssLoaderOptions 是一个字符串,它会根据 options 中的配置而发生变化,主要配置是以下两个:
- sourceMap:在非 production 模式下,如果开启了 cssSourceMap 配置会添加 sourceMap 配置;
- minimize:仅在 production 模式下生效,用于启用 css 压缩,减小 css 文件体积。
vue 源码解析
在完成 options 获取和文件分割的准备工作中,mpvue-loader 会开始对 vue 源码进行处理:
var parts = parse(content, fileName, this.sourceMap)
这里完成源码解析的是 parse 方法,该方法位于 node_modules/mpvue-loader/lib/parse.js,解析的核心方法仍然是 mpvue-template-compiler,这里由于篇幅关系不详细展开,我们直接分析解析的结果 parts 对象,parts 对象包含三个属性:
- template:vue 源码中的 template 标签内容,本案例中 template 对象的值为:
{
"type": "template",
"content": "\n<div>{{message}}</div>\n",
"start": 10,
"attrs": {},
"end": 36
}
可以看到 parse 方法会解析出 template 的开始字符和结束字符位置,并将内容转为字符串存入 content 属性中。
- script:vue 源码中的 script 标签内容,本案例中 script 对象的值为:
{
"type": "script",
"content": "//\n//\n//\n//\n\nexport default {\n data () {\n return {\n message: 'Hello World'\n }\n }\n}\n",
"start": 57,
"attrs": {},
"end": 141,
"map": {
"version": 3,
"sources": [
"index.vue?c734d08c"
],
"names": [],
"mappings": ";;;;;AAKA;AACA;AACA;AACA;AACA;AACA;AACA",
"sourcesContent": [
"<template>\n <div>{{message}}</div>\n</template>\n\n<script>\nexport default {\n data () {\n return {\n message: 'Hello World'\n }\n }\n}\n</script>\n\n<style scoped>\ndiv {\n color: red;\n}\n</style>\n"
]
}
}
由于我们启用了 sourceMap,所以解析的 script 对象中包含了 map 对象,map 对象对应了 sourceMap 的内容,content 中包含了 script 中的源码字符串。
- styles:vue 源码中的 style 标签内容,由于 style 标签可以存在多个,所以用数组来存储,本案例中的 styles 对象的值为:
[{
"type": "style",
"content": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\ndiv {\n color: red;\n}\n",
"start": 166,
"attrs": {
"scoped": true
},
"scoped": true,
"end": 189,
"map": {}
}]
可以看到 content 中记录了具体的 css 源码。
vue 源码分割
在 vue 源码解析后,mpvue-loader 会做分割前的准备工作,主要源码如下:
var hasScoped = parts.styles.some(function (s) { return s.scoped })
var hasComment = parts.template && parts.template.attrs && parts.template.attrs.comments
var templateCompilerOptions = '?' + JSON.stringify({
id: moduleId,
hasScoped: hasScoped,
hasComment: hasComment,
transformToRequire: options.transformToRequire,
preserveWhitespace: options.preserveWhitespace,
fileExt: options.fileExt || {
template: 'wxml',
style: 'wxss',
script: 'js'
},
buble: options.buble,
compilerModules: typeof options.compilerModules === 'string'
? options.compilerModules
: undefined
})
const babelLoaderOptions = mpOptions.globalBabelrc ? {
loader: 'babel-loader',
options: {
extends: mpOptions.globalBabelrc
}
} : 'babel-loader'
上述代码的主要工作有:
- 判断 style 中是否存在 scoped 标签,存储在 hasScoped 变量中;
- 判断 template 中是否存在注释,存储在 hasComment 变量中;
- 生成 template loader 的处理参数,存储在 templateCompilerOptions 变量中;
- 生成 babel-loader 的处理参数,用于处理 es6 部分的 js 源码,存储在 babelLoaderOptions 中。
之后 mpvue-loader 会生成具体的 loader,源码如下:
var defaultLoaders = {
html: templateCompilerPath + templateCompilerOptions,
css: options.extractCSS
? getCSSExtractLoader()
: styleLoaderPath + '!' + 'css-loader' + cssLoaderOptions,
js: hasBuble ? ('buble-loader' + bubleOptions) : hasBabel ? babelLoaderOptions : ''
}
var loaders = Object.assign({}, defaultLoaders, options.loaders)
可以看到 mpvue-loader 会首先生成 defaultLoaders,然后将 options 中的 loaders 与 defaultLoaders 合并,defaultLoaders 包含对 html、css 和 js 的处理 loader,最终合并后的 loaders 如下:
{
"html": "/Users/sam/Desktop/mpvue-test-project/node_modules/_mpvue-loader@2.0.1@mpvue-loader/lib/template-compiler/index?{\"id\":\"data-v-32ccf774\",\"hasScoped\":true,\"transformToRequire\":{\"video\":\"src\",\"source\":\"src\",\"img\":\"src\",\"image\":\"xlink:href\"},\"fileExt\":{\"template\":\"wxml\",\"script\":\"js\",\"style\":\"wxss\",\"platform\":\"wx\"}}",
"css": [],
"js": "babel-loader",
"wxss": [],
"postcss": [],
"less": [],
"sass": [],
"scss": [],
"stylus": [],
"styl": []
}
这里省略了 css 相关的 loader,因为上面的内容中已经介绍过,这里重点看 html 和 js 的 loader:
- html:loader 为 mpvue-loader/lib/template-compiler,template-compiler 我们在上节中已经分析过了,它的主要用途是解析 template,将其转化为 ast 和 render 函数,html 的 loader 后面携带了很多参数,这些参数都是从我们上面分析的参数中获取的,这些参数的具体应用逻辑大家可以在 template-compiler 中找到答案;
- js:loader 为 babel-loader,可以看到 mpvue 主要对 es6 的 js 源码进行处理。
生成 loaders 后 mpvue-loader 会对 parts 中的各部分,根据 type 调用不同的 loaders 完成文件解析,如果需要输出文件,会最终输出文件,如 template 源码在微信小程序中会输出到 wxml 文件中,这里 mpvue 是通过 emitFile 实现的,该方法位于 lib/mp-compiler/index.js 的 createPageMPML 方法中:
function createPageMPML (emitFile, resourcePath, rootComponent, context, fileExt) {
const { src } = getFileInfo(resourcePath) || {}
const { name, filePath } = getCompInfo(context, rootComponent, fileExt)
const MPMLContent = genPageML(name, filePath, fileExt)
emitFile(`${src}.${fileExt.template}`, MPMLContent)
}
可以看到 emitFile 方法实现了生成文件的功能,主要需要两个参数:
- 文件路径:通过 src + fileExt.template 生成文件路径;
- MPMLContent:即 template 的源码内容。