接上文:【一套代碼小程序&Native&Web階段總結篇】能夠這樣閱讀Vue源碼css
最近工做比較忙,加之上個月生了小孩,小情人是各類折騰他爸媽,咱們可使用的獨立時間片很少,雖然這塊研究進展緩慢,可是一直作下去,確定仍是會有一些收穫的html
以前咱們這個課題研究一直是作獨立的研究,沒有去看已有的解決方案,這個是爲了保證一個本身獨立的思惟,不管獨立的思惟仍是人格都是很重要的東西,然獨學而無友則孤陋而寡聞,稍微有點本身的東西后,仍是應該看看外面已有的東西了,今天的目標是mpvue,咱們來看看其官方描述:前端
mpvue
(github 地址請參見)是一個使用 Vue.js 開發小程序的前端框架。框架基於 Vue.js
核心,mpvue
修改了 Vue.js
的 runtime 和 compiler 實現,使其能夠運行在小程序環境中,從而爲小程序開發引入了整套 Vue.js
開發體驗。vue
使用 mpvue
開發小程序,你將在小程序技術體系的基礎上獲取到這樣一些能力:node
Vue.js
開發體驗Vuex
數據管理方案:方便構建複雜應用webpack
構建機制:自定義構建策略、開發階段 hotReloadVue.js
命令行工具 vue-cli 快速初始化項目其它特性正在等着你去探索,詳細文檔,github地址:https://github.com/Meituan-Dianping/mpvuereact
彷佛,mpvue已經完成了咱們想要作的工做,若是他真的好用,咱們去學習吸取他也不失爲一個好的方式,因而咱們便去試試他的5分鐘上手教程,簡單編譯後便造成了小程序代碼,運行之:webpack
這裏最終生成的代碼已經能夠徹底適配小程序了,咱們這裏主要來看看其app.json的配置:git
{ "pages": [ "pages/index/main", "pages/logs/main", "pages/counter/main" ], "window": { "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#fff", "navigationBarTitleText": "WeChat", "navigationBarTextStyle": "black" } }
這裏設置了起始頁面,每一個目錄下都是main做爲入口,咱們簡單看一下main的寫法github
<import src="/pages/index/index.vue.wxml" /><template is="b26bd43a" data="{{ ...$root['0'], $root }}"/>
都很一致,其中奇怪的template id就是真實的模板,而後咱們看看源文件src:web
<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>
import Vue from 'vue' import App from './index' const app = new Vue(App) app.$mount()
能夠看到,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是如今最經常使用的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" ] }
"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) }) })
裏面比較關鍵的代碼在此:
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 }
{ 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再繼續學習吧......