Skip to content
微信公众号

mpvue-loader 源码解析

mpvue 运行时和构建时基本概念

mpvue 框架从运行环境来分,可以分为运行时和构建时,mpvue 运行时指 mpvue 编写的小程序在运行时的环境,小程序在运行时会调用 mpvue.js 库,mpvue.js 基于 Vue.js,它 fork 了一份 Vue.js 源码,并对其 Compiler 和 mount 部分源码进行了修改,从而使其能够在小程序环境中应用。但是运行时有一个前提,就是需要将 .vue 编写的 mpvue 源码编译为小程序源码,所以我们需要应用 webpack 对其进行构建,构建的过程我们成为 mpvue 构建时,构建时最关键的一个步骤是将 .vue 源码编译为小程序源码。如微信小程序需要将 .vue 编译为 wxml、js 和 wxss 文件,其中 wxml 对应 template 标签,js 对应 script 标签 而 wxss 对应 style 标签。

Vue SFC 规范

.vue 文件是 Vue.js 自定义的文件类型,它符合 SFC 规范,SFC 指 Single File Component,简称单文件组件,使用 SFC 之前,我们组件的布局、逻辑和样式文件是割裂的,下面是一个 非 SFC 组件的案例:

html
<!DOCTYPE html>
<html>
  <head>
    <title>vue测试</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <style>
      .msg {
        color: red;
      }
    </style>
  </head>
  <body>
    <div id="root">
      <Message :data="message"></Message>
    </div>
    <script>
      Vue.component('Message', {
        template: '<div class="msg">{{data}}</div>',
        props: {
          data: String
        }
      })
      new Vue({
        el: '#root',
        data() {
          return {
            message: 'Hello Mpvue!'
          }
        }
      })
    </script>
  </body>
</html>

上述定义了一个 Message 组件,它是一个非 SFC 组件,它的布局、逻辑和样式部分是割裂的,我们需要仔细观察代码才能找到:

布局:位于 Vue.component 的 template 属性中:

vue
<div class="msg">{{data}}</div>

逻辑:位于 Vue.component 的属性中:

js
Vue.component('Message', {
  template: '<div class="msg">{{data}}</div>',
  props: {
    data: String
  }
})

样式:位于 head 的 style 标签内:

html
<style>
  .msg {
    color: red;
  }
</style>

这样会造成我们对组件难以维护的问题,同时样式容器污染其他组件,为了解决这个问题,Vue 创建了 SFC 规范,并支持在样式中定义 scoped 缩小样式的可用范围解决这个问题。Message 组件经过 SFC 改造后如下:

vue
<template>
	<div class="msg">{{data}}</div>
</template>
<script>
	export default {
    props: {
    	data: String
  	}
  }
</script>
<style scoped>
  .msg {
    color: red;
  }
</style>

可以看到逻辑非常清晰,而且便于维护。SFC 规范的主要内容如下:

  • 模板
    • 每个 .vue 文件最多包含一个 <template> 块;
    • 内容将被提取并传递给 vue-template-compiler 为字符串,预处理为 JavaScript 渲染函数,并最终注入到从 <script> 导出的组件中。
  • 脚本
    • 每个 .vue 文件最多包含一个 <script> 块;
    • 这个脚本会作为一个 ES Module 来执行;
    • 它的默认导出应该是一个 Vue.js 的组件选项对象。
  • 样式
    • 默认匹配:/.css$/;
    • 一个 .vue 文件可以包含多个 <style> 标签;
    • <style> 标签可以有 scoped 或者 module 属性 (查看 scoped CSS 和 CSS Modules) 以帮助你将样式封装到当前组件。具有不同封装模式的多个 <style> 标签可以在同一个组件中混合使用;
    • 任何匹配 .css 文件 (或通过它的 lang 特性指定的扩展名) 的 webpack 规则都将会运用到这个 <style> 块的内容。

要实现以上功能,Vue 提供了两个核心库:vue-loader 和 vue-template-compiler,vue-loader 的功能是解析 SFC 文件,并将模板、脚本和样式部分提取出来,输出到对应的文件中,vue-template-compiler 的用途是预先编译 template 模板为 js 的 render 函数,减少实时解析 template 所造成的性能开销。

mpvue SFC 规范

mpvue 延续了 Vue 的 SFC 规范基础内容,并在其基础上进行了改良,主要改进点包括:

  • 模板
    • 每个 .vue 文件最多包含一个 <template> 块;
    • <template> 的布局文件会被提取到小程序对应的布局文件中,如:微信小程序是 .wxml 文件,支付宝小程序是 .axml 文件; 内容将被提取并传递给 mpvue-template-compiler 为字符串,预处理为 JavaScript 渲染函数,并最终注入到从 <script> 导出的组件中。
  • 脚本
    • 每个 .vue 文件最多包含一个 <script> 块;
    • 这个脚本会作为一个 ES Module 来执行;
    • 它的默认导出应该是一个 Vue.js 的组件选项对象;
    • 脚本最终会输出到 mpvue 的主 js 文件中,并替换组件内容,如:
js
import Vue from 'vue'
import App from './index'

const app = new Vue(App)
app.$mount()

这里的 new Vue(App) 构建后代码如下:

js
var app = new __WEBPACK_IMPORTED_MODULE_0_vue___default.a(__WEBPACK_IMPORTED_MODULE_1__index__["a" /* default */]);
app.$mount();

可以看到 App 组件被导出为一个组件对象,名称为 a,组件 a 的定义如下:

js
/* harmony default export */ __webpack_exports__["a"] = (Component.exports);

请大家记住这里的 Component.exports,因为在 mpvue-loader 的构建产物中我们会再次看到这个对象。

  • 样式
    • 默认匹配:/.css$/;
    • 一个 .vue 文件可以包含多个 <style> 标签;
    • <style> 标签会导出到小程序样式文件中,如微信小程序的 wxss 文件,支付宝小程序的 acss 文件。

mpvue 构建时核心内容

通过 mpvue SFC 和 vue SFC 的对比,我们可以看到 mpvue 构建时的主要改动如下:

  • 修改 vue-loader 为 mpvue-loader:
    • template 导出小程序布局文件;
    • style 导出小程序样式文件。
  • 修改 vue-template-compiler 为 mpvue-template-compiler,编译 mpvue 的 template。 本章将从 mpvue-loader 和 mpvue-template-compiler 两个维度对 mpvue 的构建时进行分析。

mpvue-loader 源码解析

mpvue-loader 是以 vue-loader 作为基础的定制版本,mpvue-loader 仓库 README 对其解释为:

本仓库是 fork 自 vue-loader 修改而来,主要为 webpack 打包 mpvue components 提供能力。

我们在阅读 mpvue-loader 源码时会发现与 vue-loader 有很多类似之处,vue-loader 的关键功能如下:

  • 允许为 Vue 组件的每个部分使用其它的 webpack loader,例如在 <style> 的部分使用 Sass 和在 <template> 的部分使用 Pug;
  • 允许在一个 .vue 文件中使用自定义块,并对其运用自定义的 loader 链;
  • 使用 webpack loader 将 <style><template> 中引用的资源当作模块依赖来处理。

mpvue-loader 延续了这个功能,在后续章节中我们会为大家详细分析,届时大家可以看到 mpvue 为不同的标签添加不同的 webpack loader,从而实现不同标签的解析和文件分割。

自定义 webpack loader 入门

关键自定义 webpack loader 我们需要了解以下四个知识点:

  • 自定义 loader 通过 resolveLoader 引入;
  • 自定义 loader 需要输出一个 function,参数是上一个 loader 输出的源码文件,返回值是传递给下一个 loader 的源码文件;
  • 自定义 loader 自下而上执行,类似栈结构,loader 执行过程中呈链式结构,逐一向上传递;
  • 自定义 loader 通过 test 参数匹配合适的文件,每一个命中的文件都会调用 loader 输出的 function。

下面我们尝试自定义一个 webpack loader 来演示自定义 loader 的用法,我们在 build 目录下创建自定义 loader test-loader,目录结构如下:

js
./build/
├── rules
│   └── test-loader
│       └── index.js

index.js 中只做简单的代码传递,不做任何逻辑:

js
module.exports = function (content) {
  console.log('test-loader', content)
  return content
}

接下来我们进入 webpack.base.conf.js 配置文件修改 loader 的配置,首先加入 resolveLoader 配置,用来解析自定义 loader 的路径:

js
resolveLoader: {
  alias: {
    'test-loader': path.resolve(__dirname, './rules/test-loader/index.js')
  },
  modules: [
    path.resolve(__dirname, './rules'),
    'node_modules'
  ]
},

以上代码用了两种方式解析自定义 loader,第一种是通过 alias 别名的方式,第二种是通过 modules 批量引入自定义 loader,推荐使用第二种,更为简洁,第一种方式主要是出现 loader 重名时的一种解决方案。解析 loader 后我们就可以在 loader 解析链中加入自定义 loader 了:

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

上述配置调用了两个 loader,第一个 loader 调用 mpvue-loader 解析 .vue 文件,第二个 loader 调用 test-loader 解析 .vue 文件,第二个 loader 就是我们自定义的 loader。

mpvue-loader 源码调试

mpvue-loader 的源码位于 node_modules/mpvue-loader/lib/loader.js 路径下,我们可以通过调试 node 源码调试的方法来调试 mpvue-loader,具体方法如下:

  • 创建 node 应用启动;
  • 填入 Node parameters 为:build/build.js wx;

在 mpvue-loader 源码中添加断点:

启动 node 应用,会开始执行 build/build.js 文件,开始 webpack 构建,当解析 .vue 文件时,会在 mpvue-loader 的断点处中断:

mpvue-loader 源码输出

我们编写一个测试组件:

vue
<template>
  <div>{{message}}</div>
</template>

<script>
export default {
  data () {
    return {
      message: 'Hello World'
    }
  }
}
</script>

<style scoped>
div {
  color: red;
}
</style>

通过 mpvue-loader 调试,我们可以看到输出的代码文件为:

js
function injectStyle (ssrContext) {
  require("!!../../../node_modules/_extract-text-webpack-plugin@3.0.2@extract-text-webpack-plugin/dist/loader.js?{\"omit\":1,\"remove\":true}!vue-style-loader!css-loader?{\"minimize\":true,\"sourceMap\":false}!../../../node_modules/_mpvue-loader@2.0.1@mpvue-loader/lib/style-compiler/index?{\"vue\":true,\"id\":\"data-v-32ccf774\",\"scoped\":true,\"hasInlineConfig\":false}!px2rpx-loader?{\"baseDpr\":1,\"rpxUnit\":0.5}!postcss-loader?{\"sourceMap\":true}!../../../node_modules/_mpvue-loader@2.0.1@mpvue-loader/lib/selector?type=styles&index=0!../../../build/rules/test-loader/index.js!./index.vue")
}
var normalizeComponent = require("!../../../node_modules/_mpvue-loader@2.0.1@mpvue-loader/lib/component-normalizer")
/* script */
import __vue_script__ from "!!babel-loader!../../../node_modules/_mpvue-loader@2.0.1@mpvue-loader/lib/selector?type=script&index=0!../../../build/rules/test-loader/index.js!./index.vue"
/* template */
import __vue_template__ from "!!../../../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\"}}!../../../node_modules/_mpvue-loader@2.0.1@mpvue-loader/lib/selector?type=template&index=0!../../../build/rules/test-loader/index.js!./index.vue"
/* styles */
var __vue_styles__ = injectStyle
/* scopeId */
var __vue_scopeId__ = "data-v-32ccf774"
/* moduleIdentifier (server only) */
var __vue_module_identifier__ = null
var Component = normalizeComponent(
  __vue_script__,
  __vue_template__,
  __vue_styles__,
  __vue_scopeId__,
  __vue_module_identifier__
)

export default Component.exports

还记得上节中介绍 SFC 时 a 组件经过构建后的源码吗 ?

js
/* harmony default export */ __webpack_exports__["a"] = (Component.exports);

这里的 Component.exports 就是 mpvue-loader 输出的 Component.exports,所以 mpvue-loader 的主要功能就是将 mpvue 的 .vue 文件输出为一个组件对象。

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