mpvue 源码结构与构建原理分析
mpvue 源码结构
mpvue 不是单一的项目,而是一个项目生态的集合,就像 Vue 生态一样。Vue 生态具备以下三个最重要的项目:
- Vue.js:Vue 的 Web 核心库;
- Vue-CLI:Vue 的脚手架,帮助用户快速搭建基于 Webpack 的 Vue 项目,实现模块化和工程化;
- vue-loader:实现了自定义 .vue 文件的解析。
通过上述三个项目,我们可以快速地将 Vue 应用或集成到我们的项目中。mpvue 借鉴了 Vue 的实现思路,它也实现了类似的生态:
- mpvue.js:mpvue 的小程序核心库;
- mpvue-quickstart:基于 Vue-CLI 2.0 的 mpvue 脚手架;
- mpvue-loader:实现自定义 .vue 文件的解析,将其解析为 wxml、wxss 和 js 文件,对应小程序源码。
通过 mpvue-quickstart 我们可以快速构建 mpvue 项目:
vue init mpvue/mpvue-quickstart mpvue-project
该项目中结构如下:
├── README.md
├── build
│ ├── build.js
│ ├── check-versions.js
│ ├── dev-client.js
│ ├── dev-server.js
│ ├── utils.js
│ ├── vue-loader.conf.js
│ ├── webpack.base.conf.js
│ ├── webpack.dev.conf.js
│ └── webpack.prod.conf.js
├── config
│ ├── dev.env.js
│ ├── index.js
│ └── prod.env.js
├── index.html
├── package.json
├── package.swan.json
├── project.config.json
├── project.swan.json
├── src
│ ├── App.vue
│ ├── app.json
│ ├── components
│ ├── main.js
│ ├── pages
│ └── utils
└── static
其中 src 是源码目录,在 main.js 中使用了 mpvue.js 库来创建小程序实例。
$ cat src/main.js
import Vue from 'vue'
import App from './App'
Vue.config.productionTip = false
App.mpType = 'app'
const app = new Vue(App)
app.$mount()
这里的 vue 是别名,它指向 mpvue 库,配置位置在 build/webpack.base.conf.js:
resolve: {
alias: {
'vue': 'mpvue',
'@': resolve('src')
}
}
mpvue
mpvue 库位于 node_modules/mpvue 目录下,源码结构如下:
$ tree node_modules/mpvue
node_modules/mpvue
├── README.md
├── index.js
└── package.json
所以引用 mpvue 实际引用的是 node_modules/mpvue/index.js,该文件是 mpvue 库源码打包后生成的,mpvue 库的源码位于:https://github.com/mpvue/mpvue。它是基于 Vue 2.0 源码进行了修改,所以和 Vue 2.0 源码结构一致。mpvue 源码结构如下:
├── BACKERS.md
├── LICENSE
├── README.md
├── benchmarks
├── build
├── circle.yml
├── dist
├── examples
├── flow
├── node_modules
├── package.json
├── packages
├── src
├── test
├── types
由于 Vue 2.0 采用 flow 进行类型判断,所以包含了一个 flow 文件夹,build 目录下代码构建的相关的文件,构建产物会输出到 dist 和 packages 目录下,关于 flow 和 Vue 源码构建的知识可以参考我在慕课网发布的 rollup.js 相关手记。Vue 源码通过 rollup.js 最终构建生成了我们在 node_modules 中看到的 index.js 文件。
mpvue-loader
mpvue 通过 webpack 构建将 .vue 文件转化为小程序能够识别的 wxml、wxss 和 js 文件,完成这一步骤依赖 mpvue 提供的的 mpvue-loader,其配置文件位于 build/webpack.base.conf.js:
module: {
rules: [
{
test: /\.vue$/,
loader: 'mpvue-loader',
options: vueLoaderConfig
}
]
}
该 loader 针对 .vue 文件,该 loader 的 options 为 vueLoaderConfig,其中包含了 fileExt 属性,该属性会根据不同的平台,决定导出文件的格式:
module.exports = {
fileExt: config.build.fileExt
}
config.build.fileExt 的定义如下:
var fileExtConfig = {
swan: {
template: 'swan',
script: 'js',
style: 'css',
platform: 'swan'
},
tt: {
template: 'ttml',
script: 'js',
style: 'ttss',
platform: 'tt'
},
wx: {
template: 'wxml',
script: 'js',
style: 'wxss',
platform: 'wx'
},
my: {
template: 'axml',
script: 'js',
style: 'acss',
platform: 'my'
}
}
var fileExt = fileExtConfig[process.env.PLATFORM]
由此可以看出,mpvue-loader 会根据 process.env.PLATFORM 环境变量决定选择哪一个 fileExt,如支付宝小程序下,mpvue-loader 就会输出 axml、js 和 acss 文件,而到了百度小程序下,就会输出 swan、js 和 css 文件。
mpvue-webpack-target
在 mpvue-quickstart 脚手架构建的项目的 package.json 配置文件中还包含了 mpvue-webpack-target 库,这个库的用途是生成 mpvue 项目在 webpack 中的 target。我们知道 webpack 的 target 用来指定构建模式,因 rget 为 javascript 可以运行在不同的环境下,如 web、node、electron 等,所以可以通过 target 来打包不同的 javascript 运行环境源码。webpack 的 target 可以传入 string 或 function 两种形式,mpvue 的 target 配置如下:
let baseWebpackConfig = {
target: require('mpvue-webpack-target')
}
我们查看一下 mpvue-webpack-target 的源码:
module.exports = function (compiler) {
const { options } = compiler
compiler.apply(
new JsonpTemplatePlugin(options.output),
new FunctionModulePlugin(options.output),
new NodeSourcePlugin(options.node),
new LoaderTargetPlugin(options.target)
);
};
该源码与选择 target 为 web 是一致的。我们可以查看 webpack 的源码来论证这一点,查看 node_modules/webpack/libs/WebpackOptionsApply.js,找到对 target 的判断:
switch(options.target) {
case "web":
JsonpTemplatePlugin = require("./JsonpTemplatePlugin");
NodeSourcePlugin = require("./node/NodeSourcePlugin");
compiler.apply(
new JsonpTemplatePlugin(options.output),
new FunctionModulePlugin(options.output),
new NodeSourcePlugin(options.node),
new LoaderTargetPlugin(options.target)
);
break;
}
可以看到与 mpvue-webpack-target 的功能一致。
mpvue-template-compiler
在 mpvue-loader 中,使用了 mpvue-template-compiler 对 .vue 文件进行了解析,可以说 mpvue-loader 只是完成了解析的流程,而核心的解析逻辑全部包含在 mpvue-template-compiler 库中,mpvue-template-compiler 实际也来自于 mpvue 库,它是通过 rollup.js 打包的另外一个库文件,配置文件位于 mpvue 源码的 build/config.js 下:
// Web compiler (CommonJS).
'web-compiler': {
entry: resolve('web/entry-compiler.js'),
dest: resolve('packages/vue-template-compiler/build.js'),
format: 'cjs',
external: Object.keys(require('../packages/vue-template-compiler/package.json').dependencies)
},
我们只需要执行 npm run dev:mpvue:compiler 即可构建出该库,并进入 watch 的开发模式。它的源码位于 mpvue/packages/vue-template-compiler/build.js
"dev:mpvue:compiler": "rollup -w -c build/config.js --environment TARGET:mpvue-template-compiler "
而 vue-template-compiler 的源码位于:src/platforms/web/entry-compiler.js,这部分源码十分复杂,足足 6000+ 行。
mpvue 构建原理
dev 模式和 build 模式
首先 mpvue 的构建过程中分为 dev 模式和 build 模式,两者的主要区别如下:
- 配置内容不同,如 dev 使用 source-map 模式,而 build 模式关闭了 source-map 并对源码进行了压缩;
- dev 模式增加了对文件变化的监听,而 build 模式只是一次性的构建。
所以总体来说 dev 模式下构建的代码体积较大,因为包含了源码部分,方便了我们进行调试,但是如果用这种源码上线势必会降低访问速度,所以上线时请大家务必使用 build 模式。
dev 构建流程
dev 构建的流程如下:首先 mpvue 通过 webpack.dev.conf.js 获取配置文件,然后通过:
var compiler = webpack(webpackConfig)
完成 webpack 的构建工作,之后通过 Express 启动了 HTTP 服务,启动 HTTP 服务后进行了用户修改行为的监听:
require('webpack-dev-middleware-hard-disk')(compiler, {
publicPath: webpackConfig.output.publicPath,
quiet: true
})
mvpue 使用了 webpack 官方提供的 webpack-dev-middleware-hard-disk 进行文件修改的监听,我们每次对代码进行修改,都会触发 webpack 重新 compiler,从而实现了开发模式。
build 构建流程
build 模式的构建流程如下:首先删除了 dist 目录下对应的平台目录:
rm(path.join(config.build.assetsRoot, '*'), err => {
// ...
})
这里的 config.build.assetsRoot 对应:
assetsRoot: path.resolve(__dirname, `../dist/${fileExt.platform}`),
如使用 npm run build:wx 时,fileExt.platform 为 wx,所以删除的目录为 …/dist/wx。删除成功后进行了 webpack 构建:
webpack(webpackConfig, function (err, stats) {
// ...
})
构建成功了后 mpvue 做了一些日志打印和错误提示的工作。