Skip to content
微信公众号

深入 mpvue compiler 原理

深入 mpvue-loader 原理

mpvue-loader 源码位于:node_modules/mpvue-loader/lib/loader.js,webpack 会向 loader 中传入 content 参数,content 即 .vue 源码字符串,mpvue 项目至少需要包含两个 .vue 文件,第一个是 App.vue,第二个是页面组件,这里我们只分析页面组件的解析逻辑,组件的源码如下:

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,源码如下:

js
const mpOptions = loaderUtils.getOptions(this) || {}

该 options 由 /build/webpack.base.conf.js 传入:

js
{
  test: /\.vue$/,
  loader: 'mpvue-loader',
  options: vueLoaderConfig
}

该 options 主要包含以下几类信息:

  • fileExt:文件类型与后缀名的对应关系,微信小程序的内容如下:
js
{
  "template": "wxml",
  "script": "js",
  "style": "wxss",
  "platform": "wx"
}
  • loaders:loaders 对象用于将不同文件类型映射到不同的 loader 中。之前章节中我们提到 mpvue-loader 会对不同的文件类型予以不同的 loader 进行处理,具体文件类型对应哪个 loader 就是从这里选取的,这里 loaders 中属性即 webpack 的 loader 配置,这里节选 css 和 wxss 的处理 loader:
js
{
  "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 首先会获取源码文件的路径,源码如下:

js
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 会进一步对源码文件的路径进行解析:

js
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 并不是我们常见的运行时执行上下文,而是指令执行的路径,这里它的值为:

js
/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 的参数:

js
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 源码进行处理:

js
var parts = parse(content, fileName, this.sourceMap)

这里完成源码解析的是 parse 方法,该方法位于 node_modules/mpvue-loader/lib/parse.js,解析的核心方法仍然是 mpvue-template-compiler,这里由于篇幅关系不详细展开,我们直接分析解析的结果 parts 对象,parts 对象包含三个属性:

  • template:vue 源码中的 template 标签内容,本案例中 template 对象的值为:
js
{
  "type": "template",
  "content": "\n<div>{{message}}</div>\n",
  "start": 10,
  "attrs": {},
  "end": 36
}

可以看到 parse 方法会解析出 template 的开始字符和结束字符位置,并将内容转为字符串存入 content 属性中。

  • script:vue 源码中的 script 标签内容,本案例中 script 对象的值为:
js
{
  "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 对象的值为:
js
[{
  "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 会做分割前的准备工作,主要源码如下:

js
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,源码如下:

js
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 如下:

js
{
  "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 方法中:

js
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 的源码内容。

本站总访问量次,本站总访客数人次
Released under the MIT License.