代码改变世界
[登录 · 注册]
  • 一套代码小程序&Web&Native运行的探索07——mpvue简单调研
  • 前言

    接上文:【一套代码小程序&Native&Web阶段总结篇】可以这样阅读Vue源码

    最近工作比较忙,加之上个月生了小孩,小情人是各种折腾他爸妈,我们可以使用的独立时间片不多,虽然这块研究进展缓慢,但是一直做下去,肯定还是会有一些收获的

    之前我们这个课题研究一直是做独立的研究,没有去看已有的解决方案,这个是为了保证一个自己独立的思维,无论独立的思维还是人格都是很重要的东西,然独学而无友则孤陋而寡闻,稍微有点自己的东西后,还是应该看看外面已有的东西了,今天的目标是mpvue,我们来看看其官方描述:

    mpvue (github 地址请参见)是一个使用 Vue.js 开发小程序的前端框架。框架基于 Vue.js 核心,mpvue 修改了 Vue.js 的 runtime 和 compiler 实现,使其可以运行在小程序环境中,从而为小程序开发引入了整套 Vue.js 开发体验。

    使用 mpvue 开发小程序,你将在小程序技术体系的基础上获取到这样一些能力:

    • 彻底的组件化开发能力:提高代码复用性
    • 完整的 Vue.js 开发体验
    • 方便的 Vuex 数据管理方案:方便构建复杂应用
    • 快捷的 webpack 构建机制:自定义构建策略、开发阶段 hotReload
    • 支持使用 npm 外部依赖
    • 使用 Vue.js 命令行工具 vue-cli 快速初始化项目
    • H5 代码转换编译成小程序目标代码的能力

    其它特性正在等着你去探索,详细文档,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结构,比如:

    1 <ul id='list'>
    2   <li class='item'>Item 1</li>
    3   <li class='item'>Item 2</li>
    4   <li class='item'>Item 3</li>
    5 </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的代码(详情看这里):

    在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)为中心,我们只要给一个入口文件,他会根据这个入口文件找到所有的依赖文件,最后捆绑到一起,这里盗个图:

    这里几个核心概念是:

    ① 入口 - 指示webpack应该以哪个模块(一般是个js文件),作为内部依赖图的开始

    ② 输出 - 告诉将打包后的文件输出到哪里,或者文件名是什么

    ③ loader - 这个非常关键,这个让webpack能够去处理那些非JavaScript文件,或者是自定义文件,转换为可用的文件,比如将jsx转换为js,将less转换为css

    test就是正则标志,标识哪些文件会被处理;use表示用哪个loader 

    ④ 插件(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 + '
    ')
    //   // 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

    里面比较关键的代码在此:

      1 var path = require('path')
      2 var fs = require('fs')
      3 var utils = require('./utils')
      4 var config = require('../config')
      5 var vueLoaderConfig = require('./vue-loader.conf')
      6 var MpvuePlugin = require('webpack-mpvue-asset-plugin')
      7 var glob = require('glob')
      8 var CopyWebpackPlugin = require('copy-webpack-plugin')
      9 var relative = require('relative')
     10 
     11 function resolve (dir) {
     12   return path.join(__dirname, '..', dir)
     13 }
     14 
     15 function getEntry (rootSrc) {
     16   var map = {};
     17   glob.sync(rootSrc + '/pages/**/main.js')
     18   .forEach(file => {
     19     var key = relative(rootSrc, file).replace('.js', '');
     20     map[key] = file;
     21   })
     22    return map;
     23 }
     24 
     25 const appEntry = { app: resolve('./src/main.js') }
     26 const pagesEntry = getEntry(resolve('./src'), 'pages/**/main.js')
     27 const entry = Object.assign({}, appEntry, pagesEntry)
     28 
     29 module.exports = {
     30   // 如果要自定义生成的 dist 目录里面的文件路径,
     31   // 可以将 entry 写成 {'toPath': 'fromPath'} 的形式,
     32   // toPath 为相对于 dist 的路径, 例:index/demo,则生成的文件地址为 dist/index/demo.js
     33   entry,
     34   target: require('mpvue-webpack-target'),
     35   output: {
     36     path: config.build.assetsRoot,
     37     filename: '[name].js',
     38     publicPath: process.env.NODE_ENV === 'production'
     39       ? config.build.assetsPublicPath
     40       : config.dev.assetsPublicPath
     41   },
     42   resolve: {
     43     extensions: ['.js', '.vue', '.json'],
     44     alias: {
     45       'vue': 'mpvue',
     46       '@': resolve('src')
     47     },
     48     symlinks: false,
     49     aliasFields: ['mpvue', 'weapp', 'browser'],
     50     mainFields: ['browser', 'module', 'main']
     51   },
     52   module: {
     53     rules: [
     54       {
     55         test: /.(js|vue)$/,
     56         loader: 'eslint-loader',
     57         enforce: 'pre',
     58         include: [resolve('src'), resolve('test')],
     59         options: {
     60           formatter: require('eslint-friendly-formatter')
     61         }
     62       },
     63       {
     64         test: /.vue$/,
     65         loader: 'mpvue-loader',
     66         options: vueLoaderConfig
     67       },
     68       {
     69         test: /.js$/,
     70         include: [resolve('src'), resolve('test')],
     71         use: [
     72           'babel-loader',
     73           {
     74             loader: 'mpvue-loader',
     75             options: Object.assign({checkMPEntry: true}, vueLoaderConfig)
     76           },
     77         ]
     78       },
     79       {
     80         test: /.(png|jpe?g|gif|svg)(?.*)?$/,
     81         loader: 'url-loader',
     82         options: {
     83           limit: 10000,
     84           name: utils.assetsPath('img/[name].[ext]')
     85         }
     86       },
     87       {
     88         test: /.(mp4|webm|ogg|mp3|wav|flac|aac)(?.*)?$/,
     89         loader: 'url-loader',
     90         options: {
     91           limit: 10000,
     92           name: utils.assetsPath('media/[name].[ext]')
     93         }
     94       },
     95       {
     96         test: /.(woff2?|eot|ttf|otf)(?.*)?$/,
     97         loader: 'url-loader',
     98         options: {
     99           limit: 10000,
    100           name: utils.assetsPath('fonts/[name].[ext]')
    101         }
    102       }
    103     ]
    104   },
    105   plugins: [
    106     new MpvuePlugin(),
    107     new CopyWebpackPlugin([{
    108       from: '**/*.json',
    109       to: ''
    110     }], {
    111       context: 'src/'
    112     }),
    113     new CopyWebpackPlugin([
    114       {
    115         from: path.resolve(__dirname, '../static'),
    116         to: path.resolve(config.build.assetsRoot, './static'),
    117         ignore: ['.*']
    118       }
    119     ])
    120   ]
    121 }
    webpack.base.conf.js
    {
      test: /.vue$/,
      loader: 'mpvue-loader',
      options: vueLoaderConfig
    },

    关键就落到了我们这里的mpvue-loader了,他是自 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再继续学习吧......

  • 上一篇:当代前端应该怎么写这个hello world?
    下一篇:【一套代码小程序&Native&Web阶段总结篇】可以这样阅读Vue源码
  • 【推广】 阿里云小站-上云优惠聚集地(新老客户同享)更有每天限时秒杀!
    【推广】 云服务器低至0.95折 1核2G ECS云服务器8.1元/月
    【推广】 阿里云老用户升级四重礼遇享6.5折限时折扣!
  • 原文:https://www.cnblogs.com/yexiaochai/p/9792086.html
走看看 - 开发者的网上家园