前言
接上文:[一套代码小程序 & Native&Web 阶段总结篇] 可以这样阅读 Vue 源码
最近工作比较忙, 加之上个月生了小孩, 小情人是各种折腾他爸妈, 我们可以使用的独立时间片不多, 虽然这块研究进展缓慢, 但是一直做下去, 肯定还是会有一些收获的
之前我们这个课题研究一直是做独立的研究, 没有去看已有的解决方案, 这个是为了保证一个自己独立的思维, 无论独立的思维还是人格都是很重要的东西, 然独学而无友则孤陋而寡闻, 稍微有点自己的东西后, 还是应该看看外面已有的东西了, 今天的目标是 mpvue, 我们来看看其官方描述:
mpvue (GitHub 地址请参见 https://github.com/Meituan-Dianping/mpvue )是一个使用 Vue.JS https://vuejs.org/ 开发小程序的前端框架. 框架基于 Vue.JS 核心, mpvue 修改了 Vue.JS 的 runtime http://mpvue.com/mpvue 和 http://mpvue.com/mpvue-template-compiler 实现, 使其可以运行在小程序环境中, 从而为小程序开发引入了整套 Vue.JS 开发体验.
使用 mpvue 开发小程序, 你将在小程序技术体系的基础上获取到这样一些能力:
彻底的组件化开发能力: 提高代码复用性
完整的 Vue.JS 开发体验
方便的 Vuex 数据管理方案: 方便构建复杂应用
快捷的 webpack 构建机制: 自定义构建策略, 开发阶段 hotReload
支持使用 NPM 外部依赖
使用 Vue.JS 命令行工具 vue-cli 快速初始化项目
H5 代码转换编译成小程序目标代码的能力
其它特性正在等着你去探索, 详细文档 http://mpvue.com/mpvue ,GitHub 地址: https://github.com/Meituan-Dianping/mpvue
似乎, mpvue 已经完成了我们想要做的工作, 如果他真的好用, 我们去学习吸收他也不失为一个好的方式, 于是我们便去试试他的 5 分钟上手教程, 简单编译后便形成了小程序代码, 运行之:
这里最终生成的代码已经可以完全适配小程序了, 我们这里主要来看看其 App.JSON 的配置:
- {
- "pages": [
- "pages/index/main",
- "pages/logs/main",
- "pages/counter/main"
- ],
- "window": {
- "backgroundTextStyle": "light",
- "navigationBarBackgroundColor": "#fff",
- "navigationBarTitleText": "WeChat",
- "navigationBarTextStyle": "black"
- }
- }
这里设置了起始页面, 每个目录下都是 main 作为入口, 我们简单看一下 main 的写法
<import src="/pages/index/index.vue.wxml" /><template is="b26bd43a" data="{{ ...$root['0'], $root }}"/>
都很一致, 其中奇怪的 template id 就是真实的模板, 然后我们看看源文件 src:
- <template>
- <div class="container" @click="clickHandle('test click', $event)">
- <div class="userinfo" @click="bindViewTap">
- <img class="userinfo-avatar" v-if="userInfo.avatarUrl" :src="userInfo.avatarUrl" background-size="cover" />
- <div class="userinfo-nickname">
- <card :text="userInfo.nickName"></card>
- </div>
- </div>
- <div class="usermotto">
- <div class="user-motto">
- <card :text="motto"></card>
- </div>
- </div>
- <form class="form-container">
- <input type="text" class="form-control" v-model="motto" placeholder="v-model" />
- <input type="text" class="form-control" v-model.lazy="motto" placeholder="v-model.lazy" />
- </form>
- <a href="/pages/counter/main" class="counter">去往 Vuex 示例页面</a>
- </div>
- </template>
- <script>
- import card from '@/components/card'
- export default {
- data () {
- return {
- motto: 'Hello World',
- userInfo: {}
- }
- },
- components: {
- card
- },
- methods: {
- bindViewTap () {
- const url = '../logs/main'
- wx.navigateTo({ url })
- },
- getUserInfo () {
- // 调用登录接口
- wx.login({
- success: () => {
- wx.getUserInfo({
- success: (res) => {
- this.userInfo = res.userInfo
- }
- })
- }
- })
- },
- clickHandle (msg, ev) {
- console.log('clickHandle:', msg, ev)
- }
- },
- created () {
- // 调用应用实例的方法获取全局数据
- this.getUserInfo()
- }
- }
- </script>
- <style scoped>
- .userinfo {
- display: flex;
- flex-direction: column;
- align-items: center;
- }
- .userinfo-avatar {
- width: 128rpx;
- height: 128rpx;
- margin: 20rpx;
- border-radius: 50%;
- }
- .userinfo-nickname {
- color: #aaa;
- }
- .usermotto {
- margin-top: 150px;
- }
- .form-control {
- display: block;
- padding: 0 12px;
- margin-bottom: 5px;
- border: 1px solid #ccc;
- }
- .counter {
- display: inline-block;
- margin: 10px auto;
- padding: 5px 10px;
- color: blue;
- border: 1px solid blue;
- }
- </style>
index.vue
- import Vue from 'vue'
- import App from './index'
- const App = new Vue(App)
- App.$mount()
mpvue 原理研究
可以看到, mpvue 经过一次编译后, 通过一个制定的规则, 将 vue 的写法的页面, 变成了小程序可以识别的代码, 这里我们再回看其实现部分描述:
mpvue 修改了 Vue.JS 的 runtime 和 compiler 实现, 使其可以运行在小程序环境中, 从而为小程序开发引入了整套 Vue.JS 开发体验
mpvue-template-compiler 提供了将 vue 的模板语法转换到小程序的 wxml 语法的能力
这里我们回到 Vue 的部分, 稍加说明, Vue 现在已经做为了一套完整的解决方案而存在, 特别是 weex 的出现后, 框架出了 platforms 目录, 经过之前的学习, 我们知道:
我们在项目中写的 HTML 结构会被翻译为虚拟 dom vnode 从而形成 ast(虚拟语法树), 只要有了这个 ast, 不管后续容器是什么, 可以是浏览器, 可以是服务器端, 也可以是 native 端, 我们可以轻易的根据这个 ast 生成我们要的 HTML 结构:
- el('ul', {id: 'list'}, [
- el('li', {class: 'item'}, ['Item 1']),
- el('li', {class: 'item'}, ['Item 2']),
- el('li', {class: 'item'}, ['Item 3'])
- ])
这段代码 (数据结构) 可以很轻易被翻译为 HTML 结构, 比如:
- <ul id='list'>
- <li class='item'>Item 1</li>
- <li class='item'>Item 2</li>
- <li class='item'>Item 3</li>
- </ul>
也可以很轻易的被映射成 Native 视图代码, 而我们知道其实我们习惯中的 HTML 代码其实并不是必须的, 比如这段代码事实上会被变成这个样子:
- new Vue({
- template: '<div a="aaa"><div></div></div>'
- })
===>等价的
- new Vue({
- render: function () {
- return this._h('div', {
- attrs:{
- a: 'aaa'
- }
- }, [
- this._h('div')
- ])
- }
- })
platforms 的工作就是解决的是要讲 ast 转换为 HTML 结构还是 native 页面, 显然, 我们拿着 vue 解析后的 ast 可以形成小程序能够识别的代码片段, 为了证明我们的猜想我们来看看 mpvue 的代码(详情看这里 https://github.com/Meituan-Dianping/mpvue ):
在 Web 环境下, 我们直接借用 snabbdom 对比两颗虚拟 DOM 的差异, 直接完成渲染生成 HTML, 而 mpvue 完成的工作是将 Vue 的 HTML 模板编译为小程序识别的代码, 其中一些差异, 比如 vue 模板中指令处理会全部被磨平, 我们这里来看一段代码, 以下是 for 指令编译时候的处理:
- export default {
- if: 'wx:if',
- iterator1: 'wx:for-index',
- key: 'wx:key',
- alias: 'wx:for-item',
- 'v-for': 'wx:for'
- }
- import astMap from '../config/astMap'
- export default function (ast) {
- const { iterator1, for: forText, key, alias, attrsMap } = ast
- if (forText) {
- attrsMap[astMap['v-for']] = `{{${forText}}}`
- if (iterator1) {
- attrsMap[astMap['iterator1']] = iterator1
- }
- if (key) {
- attrsMap[astMap['key']] = key
- }
- if (alias) {
- attrsMap[astMap['alias']] = alias
- }
- delete attrsMap['v-for']
- }
- return ast
- }
可以看到, mpvue 其实在 vue 的基础上, 在 vue 标签的处理下, 改变 ast 中的一些属性, 对等翻译成了小程序识别的代码, 当然截止此时都还只是一些猜想, 我们接下来深入 demo 的核心看看
代码编译 - webpack
对于前端工程师来说, webpack 已经成为了一种必备技能了, 他包含了本地开发, 编译压缩, 性能优化的所有工作, 这个是工程化统一化思维集大成的结果(虽然绕不开但有点难用)
webpack 是现在最常用的 JavaScript 程序的静态模块打包器 (module bundler), 他的特点就是以模块(module) 为中心, 我们只要给一个入口文件, 他会根据这个入口文件找到所有的依赖文件, 最后捆绑到一起, 这里盗个图:
这里几个核心概念是:
1 入口 - 指示 webpack 应该以哪个模块(一般是个 JS 文件), 作为内部依赖图的开始
2 输出 - 告诉将打包后的文件输出到哪里, 或者文件名是什么
3 loader - 这个非常关键, 这个让 webpack 能够去处理那些非 JavaScript 文件, 或者是自定义文件, 转换为可用的文件, 比如将 jsx 转换为 JS, 将 Less 转换为 CSS
test 就是正则标志, 标识哪些文件会被处理; use 表示用哪个 loader
4 插件(plugins)
插件被用于转换某些类型的模块, 适用于的范围更广, 包括打包优化, 压缩, 重新定义环境中的变量等等, 这里举一个小例子进行说明, react 中的 jsx 这种事实上是浏览器直接不能识别的, 但是我们却可以利用 webpack 将之进行一次编译:
- // 原 JSX 语法代码
- return <h1>Hello,Webpack</h1>
- // 被转换成正常的 JavaScript 代码
- return React.createElement('h1', null, 'Hello,Webpack')
这个便是 Babel 所做的工作, 我们要做的就是为我们的项目提供这样的解析器, 比如 babel-preset-react
我们前面说过, mpvue 是我们以 vue 的语法写代码, 并将其编译为小程序识别的代码, 而这个工作是由 webpack 执行的, 所以我们来看看 mpvue 的几个配置文件:
- {
- "name": "my-project",
- "version": "1.0.0",
- "description": "A Mpvue project",
- "author": "yexiaochai <549265480@qq.com>",
- "private": true,
- "scripts": {
- "dev:wx": "node build/dev-server.js wx",
- "start:wx": "npm run dev:wx",
- "build:wx": "node build/build.js wx",
- "dev:swan": "node build/dev-server.js swan",
- "start:swan": "npm run dev:swan",
- "build:swan": "node build/build.js swan",
- "dev": "node build/dev-server.js wx",
- "start": "npm run dev",
- "build": "node build/build.js wx",
- "lint": "eslint --ext .js,.vue src"
- },
- "dependencies": {
- "mpvue": "^1.0.11",
- "vuex": "^3.0.1"
- },
- "devDependencies": {
- "mpvue-loader": "^1.1.2",
- "mpvue-webpack-target": "^1.0.0",
- "mpvue-template-compiler": "^1.0.11",
- "portfinder": "^1.0.13",
- "postcss-mpvue-wxss": "^1.0.0",
- "prettier": "~1.12.1",
- "px2rpx-loader": "^0.1.10",
- "babel-core": "^6.22.1",
- "glob": "^7.1.2",
- "webpack-mpvue-asset-plugin": "^0.1.1",
- "relative": "^3.0.2",
- "babel-eslint": "^8.2.3",
- "babel-loader": "^7.1.1",
- "babel-plugin-transform-runtime": "^6.22.0",
- "babel-preset-env": "^1.3.2",
- "babel-preset-stage-2": "^6.22.0",
- "babel-register": "^6.22.0",
- "chalk": "^2.4.0",
- "connect-history-api-fallback": "^1.3.0",
- "copy-webpack-plugin": "^4.5.1",
- "css-loader": "^0.28.11",
- "cssnano": "^3.10.0",
- "eslint": "^4.19.1",
- "eslint-friendly-formatter": "^4.0.1",
- "eslint-loader": "^2.0.0",
- "eslint-plugin-import": "^2.11.0",
- "eslint-plugin-node": "^6.0.1",
- "eslint-plugin-html": "^4.0.3",
- "eslint-config-standard": "^11.0.0",
- "eslint-plugin-promise": "^3.4.0",
- "eslint-plugin-standard": "^3.0.1",
- "eventsource-polyfill": "^0.9.6",
- "express": "^4.16.3",
- "extract-text-webpack-plugin": "^3.0.2",
- "file-loader": "^1.1.11",
- "friendly-errors-webpack-plugin": "^1.7.0",
- "html-webpack-plugin": "^3.2.0",
- "http-proxy-middleware": "^0.18.0",
- "webpack-bundle-analyzer": "^2.2.1",
- "semver": "^5.3.0",
- "shelljs": "^0.8.1",
- "uglifyjs-webpack-plugin": "^1.2.5",
- "optimize-css-assets-webpack-plugin": "^3.2.0",
- "ora": "^2.0.0",
- "rimraf": "^2.6.0",
- "url-loader": "^1.0.1",
- "vue-style-loader": "^4.1.0",
- "webpack": "^3.11.0",
- "webpack-dev-middleware-hard-disk": "^1.12.0",
- "webpack-merge": "^4.1.0",
- "postcss-loader": "^2.1.4"
- },
- "engines": {
- "node": ">= 4.0.0",
- "npm": ">= 3.0.0"
- },
- "browserslist": [
- "> 1%",
- "last 2 versions",
- "not ie <= 8"
- ]
- }
- package.JSON
- "scripts": {
- "dev:wx": "node build/dev-server.js wx",
- "start:wx": "npm run dev:wx",
- "build:wx": "node build/build.js wx",
- "dev:swan": "node build/dev-server.js swan",
- "start:swan": "npm run dev:swan",
- "build:swan": "node build/build.js swan",
- "dev": "node build/dev-server.js wx",
- "start": "npm run dev",
- "build": "node build/build.js wx",
- "lint": "eslint --ext .js,.vue src"
- },
然后我们看看其 webpack 的配置(build/dev-server.JS):
- require('./check-versions')()
- process.env.PLATFORM = process.argv[process.argv.length - 1] || 'wx'
- var config = require('../config')
- if (!process.env.NODE_ENV) {
- process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
- }
- // var opn = require('opn')
- var path = require('path')
- var express = require('express')
- var webpack = require('webpack')
- var proxyMiddleware = require('http-proxy-middleware')
- var portfinder = require('portfinder')
- var webpackConfig = require('./webpack.dev.conf')
- // default port where dev server listens for incoming traffic
- var port = process.env.PORT || config.dev.port
- // automatically open browser, if not set will be false
- var autoOpenBrowser = !!config.dev.autoOpenBrowser
- // Define HTTP proxies to your custom API backend
- // https://github.com/chimurai/http-proxy-middleware
- var proxyTable = config.dev.proxyTable
- console.log('========')
- console.log(webpackConfig)
- var App = express()
- var compiler = webpack(webpackConfig)
- // var devMiddleware = require('webpack-dev-middleware')(compiler, {
- // publicPath: webpackConfig.output.publicPath,
- // quiet: true
- // })
- // var hotMiddleware = require('webpack-hot-middleware')(compiler, {
- // log: false,
- // heartbeat: 2000
- // })
- // force page reload when HTML-webpack-plugin template changes
- // compiler.plugin('compilation', function (compilation) {
- // compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
- // hotMiddleware.publish({ action: 'reload' })
- // cb()
- // })
- // })
- // proxy API requests
- Object.keys(proxyTable).forEach(function (context) {
- var options = proxyTable[context]
- if (typeof options === 'string') {
- options = { target: options }
- }
- App.use(proxyMiddleware(options.filter || context, options))
- })
- // handle fallback for HTML5 history API
- App.use(require('connect-history-api-fallback')())
- // serve webpack bundle output
- // App.use(devMiddleware)
- // enable hot-reload and state-preserving
- // compilation error display
- // App.use(hotMiddleware)
- // serve pure static assets
- var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
- App.use(staticPath, express.static('./static'))
- // var uri = 'http://localhost:' + port
- var _resolve
- var readyPromise = new Promise(resolve => {
- _resolve = resolve
- })
- // console.log('> Starting dev server...')
- // devMiddleware.waitUntilValid(() => {
- // console.log('> Listening at' + uri + '\n')
- // // when env is testing, don't need open it
- // if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
- // opn(uri)
- // }
- // _resolve()
- // })
- module.exports = new Promise((resolve, reject) => {
- portfinder.basePort = port
- portfinder.getPortPromise()
- .then(newPort => {
- if (port !== newPort) {
- console.log(`${port}端口被占用, 开启新端口 ${newPort}`)
- }
- var server = App.listen(newPort, 'localhost')
- // for 小程序的文件保存机制
- require('webpack-dev-middleware-hard-disk')(compiler, {
- publicPath: webpackConfig.output.publicPath,
- quiet: true
- })
- resolve({
- ready: readyPromise,
- close: () => {
- server.close()
- }
- })
- }).catch(error => {
- console.log('没有找到空闲端口, 请打开任务管理器杀死进程端口再试', error)
- })
- })
- View Code
里面比较关键的代码在此:
- var path = require('path')
- var fs = require('fs')
- var utils = require('./utils')
- var config = require('../config')
- var vueLoaderConfig = require('./vue-loader.conf')
- var MpvuePlugin = require('webpack-mpvue-asset-plugin')
- var glob = require('glob')
- var CopyWebpackPlugin = require('copy-webpack-plugin')
- var relative = require('relative')
- function resolve (dir) {
- return path.join(__dirname, '..', dir)
- }
- function getEntry (rootSrc) {
- var map = {};
- glob.sync(rootSrc + '/pages/**/main.js')
- .forEach(file => {
- var key = relative(rootSrc, file).replace('.js', '');
- map[key] = file;
- })
- return map;
- }
- const appEntry = { App: resolve('./src/main.js') }
- const pagesEntry = getEntry(resolve('./src'), 'pages/**/main.js')
- const entry = Object.assign({}, appEntry, pagesEntry)
- module.exports = {
- // 如果要自定义生成的 dist 目录里面的文件路径,
- // 可以将 entry 写成 {'toPath': 'fromPath'} 的形式,
- // toPath 为相对于 dist 的路径, 例: index/demo, 则生成的文件地址为 dist/index/demo.JS
- entry,
- target: require('mpvue-webpack-target'),
- output: {
- path: config.build.assetsRoot,
- filename: '[name].js',
- publicPath: process.env.NODE_ENV === 'production'
- ? config.build.assetsPublicPath
- : config.dev.assetsPublicPath
- },
- resolve: {
- extensions: ['.js', '.vue', '.json'],
- alias: {
- 'vue': 'mpvue',
- '@': resolve('src')
- },
- symlinks: false,
- aliasFields: ['mpvue', 'weapp', 'browser'],
- mainFields: ['browser', 'module', 'main']
- },
- module: {
- rules: [
- {
- test: /\.(JS|vue)$/,
- loader: 'eslint-loader',
- enforce: 'pre',
- include: [resolve('src'), resolve('test')],
- options: {
- formatter: require('eslint-friendly-formatter')
- }
- },
- {
- test: /\.vue$/,
- loader: 'mpvue-loader',
- options: vueLoaderConfig
- },
- {
- test: /\.JS$/,
- include: [resolve('src'), resolve('test')],
- use: [
- 'babel-loader',
- {
- loader: 'mpvue-loader',
- options: Object.assign({checkMPEntry: true}, vueLoaderConfig)
- },
- ]
- },
- {
- test: /\.(PNG|jpe?g|gif|svg)(\?.*)?$/,
- loader: 'url-loader',
- options: {
- limit: 10000,
- name: utils.assetsPath('img/[name].[ext]')
- }
- },
- {
- test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
- loader: 'url-loader',
- options: {
- limit: 10000,
- name: utils.assetsPath('media/[name].[ext]')
- }
- },
- {
- test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
- loader: 'url-loader',
- options: {
- limit: 10000,
- name: utils.assetsPath('fonts/[name].[ext]')
- }
- }
- ]
- },
- plugins: [
- new MpvuePlugin(),
- new CopyWebpackPlugin([{
- from: '**/*.json',
- to: ''
- }], {
- context: 'src/'
- }),
- new CopyWebpackPlugin([
- {
- from: path.resolve(__dirname, '../static'),
- to: path.resolve(config.build.assetsRoot, './static'),
- ignore: ['.*']
- }
- ])
- ]
- }
- webpack.base.conf.JS
- {
- test: /\.vue$/,
- loader: 'mpvue-loader',
- options: vueLoaderConfig
- },
关键就落到了我们这里的 mpvue-loader 了, 他是自 https://github.com/vuejs/vue-loader 修改而来, 主要为 webpack 打包 mpvue components 提供能力, mpvue-loader 是 vue-loader 的一个扩展延伸版, 类似于超集的关系, 除了 vue-loader 本身所具备的能力之外, 它还会产出微信小程序所需要的文件结构和模块内容.
详细的说明文档在: http://mpvue.com/build/mpvue-loader/ , https://github.com/mpvue/mpvue-loader
我们这里简单看看他是怎么做的, 这里以 wxml 做下研究, 先看看这个简单的转换:
- <template>
- <div class="my-component">
- <h1>{{msg}}</h1>
- <other-component :msg="msg"></other-component>
- </div>
- </template>
模板部分会变成这个样子:
- <import src="components/other-component$hash.wxml" />
- <template name="component$hash">
- <view class="my-component">
- <view class="_h1">{{msg}}</view>
- <template is="other-component$hash" wx:if="{{ $c[0] }}" data="{{ ...$c[0] }}"></template>
- </view>
- </template>
而这块工作的进行在这: mpvue-template-compiler 提供了将 vue 的模板语法转换到小程序的 wxml 语法的能力
这里的代码有点多, 涉及到了很多东西, 今天篇幅很大了, 等我们明天研究下 vue-loader 再继续学习吧......
来源: https://www.cnblogs.com/yexiaochai/p/9792086.html