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 组件的案例:
<!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 属性中:
<div class="msg">{{data}}</div>
逻辑:位于 Vue.component 的属性中:
Vue.component('Message', {
template: '<div class="msg">{{data}}</div>',
props: {
data: String
}
})
样式:位于 head 的 style 标签内:
<style>
.msg {
color: red;
}
</style>
这样会造成我们对组件难以维护的问题,同时样式容器污染其他组件,为了解决这个问题,Vue 创建了 SFC 规范,并支持在样式中定义 scoped 缩小样式的可用范围解决这个问题。Message 组件经过 SFC 改造后如下:
<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 文件最多包含一个
- 脚本
- 每个 .vue 文件最多包含一个
<script>
块; - 这个脚本会作为一个 ES Module 来执行;
- 它的默认导出应该是一个 Vue.js 的组件选项对象。
- 每个 .vue 文件最多包含一个
- 样式
- 默认匹配:/.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 文件最多包含一个
- 脚本
- 每个 .vue 文件最多包含一个
<script>
块; - 这个脚本会作为一个 ES Module 来执行;
- 它的默认导出应该是一个 Vue.js 的组件选项对象;
- 脚本最终会输出到 mpvue 的主 js 文件中,并替换组件内容,如:
- 每个 .vue 文件最多包含一个
import Vue from 'vue'
import App from './index'
const app = new Vue(App)
app.$mount()
这里的 new Vue(App) 构建后代码如下:
var app = new __WEBPACK_IMPORTED_MODULE_0_vue___default.a(__WEBPACK_IMPORTED_MODULE_1__index__["a" /* default */]);
app.$mount();
可以看到 App 组件被导出为一个组件对象,名称为 a,组件 a 的定义如下:
/* 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,目录结构如下:
./build/
├── rules
│ └── test-loader
│ └── index.js
index.js 中只做简单的代码传递,不做任何逻辑:
module.exports = function (content) {
console.log('test-loader', content)
return content
}
接下来我们进入 webpack.base.conf.js 配置文件修改 loader 的配置,首先加入 resolveLoader 配置,用来解析自定义 loader 的路径:
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 了:
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 源码输出
我们编写一个测试组件:
<template>
<div>{{message}}</div>
</template>
<script>
export default {
data () {
return {
message: 'Hello World'
}
}
}
</script>
<style scoped>
div {
color: red;
}
</style>
通过 mpvue-loader 调试,我们可以看到输出的代码文件为:
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 组件经过构建后的源码吗 ?
/* harmony default export */ __webpack_exports__["a"] = (Component.exports);
这里的 Component.exports 就是 mpvue-loader 输出的 Component.exports,所以 mpvue-loader 的主要功能就是将 mpvue 的 .vue 文件输出为一个组件对象。