1. 程式人生 > >一套程式碼小程式&Web&Native執行的探索07——mpvue簡單調研

一套程式碼小程式&Web&Native執行的探索07——mpvue簡單調研

前言

最近工作比較忙,加之上個月生了小孩,小情人是各種折騰他爸媽,我們可以使用的獨立時間片不多,雖然這塊研究進展緩慢,但是一直做下去,肯定還是會有一些收穫的

之前我們這個課題研究一直是做獨立的研究,沒有去看已有的解決方案,這個是為了保證一個自己獨立的思維,無論獨立的思維還是人格都是很重要的東西,然獨學而無友則孤陋而寡聞,稍微有點自己的東西后,還是應該看看外面已有的東西了,今天的目標是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 程式碼轉換編譯成小程式目的碼的能力

似乎,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 <[email protected]>",
  "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

裡面比較關鍵的程式碼在此:

  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 本身所具備的能力之外,它還會產出微信小程式所需要的檔案結構和模組內容。

我們這裡簡單看看他是怎麼做的,這裡以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再繼續學習吧......