2016註定不是個平凡年,不管是中秋節問世的angular2,仍是全面走向穩定的React,都免不了面對另外一個競爭對手vue2。喜歡vue
在設計思路上的「先進性」(原諒我用了這麼一個詞),敬佩做者尤小右本人的「國際範兒」,使得各框架之間的競爭略顯妖嬈(雖然從已存在問題的解決方案上看,各框架都有部分類似之處)。javascript
由於vue2
已經正式release,本教程作了一些修改(針對vue2
)
所謂設計上的先進性,如下幾點是我比較喜歡的:css
不一樣於AngularJS
裏基於digest cycle
的髒檢查機制,執行效率更高。內部基於Object.defineProperty
特性作漂亮的hack實現(並且不支持IE8,大快人心)。更多細節,看這裏html
由於這個機制的出現,咱們再也也不須要顧慮雙向綁定的效率問題;亦或是像React
那樣搞什麼immutability(對這塊感興趣能夠看(譯)JavaScript中的不可變性),由於Object.definePropery
洞悉你的一切,媽媽不再用擔憂你忘記實現shouldComponentUpdate
了.vue
到這裏你可能還不能體會vue
的精妙,是時候來個栗子了!java
假設咱們有一個字段fullName
,它依賴其餘字段的變化,在AngularJS
裏,咱們或許會用命令式這樣寫道:node
$scope.user = { firstName: '', lastName: '' } $scope.fullName = '' //告訴程序主動「監視」user的變化,而後修改fullName的值 $scope.$watch('user', function(user) { $scope.fullName = user.firstName + ' ' + user.lastName }, true)
如果vue
,改用聲明式,寫法如何?react
data() { return { firstName: '', lastName: '' } }, computed: { fullName() { // 生命一個fullName的計算屬性,並告訴程序它是由firstName和lastName組成。 // 至於具體是何時/如何完成數據拼裝的,你就不用管了 return this.firstName + ' ' + this.lastName } }
相對於AngularJS
裏命令式的告訴框架,fullName
必定要監視user
對象的變化(注意裏面仍是deepWatch,效率更差),而且隨之改變;vue
以數據驅動爲本質,聲明式的定義fullName
就是由firstName
和lastName
組成,不管怎麼變化,都是如此。這種寫法,更優雅有沒有?webpack
若是有興趣看看用
angular2
如何實現相同的小遊戲,
走這裏
還在爲一堆代碼文件,到底哪一個是JavaScript
邏輯部分、哪一個是css/less/sass
樣式部分、哪一個是html/template
模板部分;他們又該如何組織,怎麼「編譯」、如何發佈?git
有了單文件組件範式,配合webpack4(雖然文檔依舊WIP),組件自包含,完美、沒毛病!還有強大的開發工具支持,看着都賞心悅目,來個效果圖:es6
用了這麼多版面,說了一些好處,那麼當咱們真正須要面對一個應用,須要上規模開發時,vue
又能帶來怎樣的變化呢?憋了幾天,我想今天就寫一個小遊戲來試試總體感受,先來看看咱們今天的目標:
完整源碼在這裏:vue-memory-game
看了效果,知道源碼在哪裏了,那咱們繼續?
Break the UI into a component hierarchy
,相信寫過React
的朋友對這句話都不陌生,在使用一種基於組件開發的模式時,最早考慮,並且也尤其重要的一件事,就是組件分解。下面咱們看看組件分解示意圖:
咱們根據分解圖,先把將來要實現的組件挨個兒列出來:
Game
, 最外層的遊戲面板Dashboard
, 上面的logo
,遊戲進度
,最佳戰績
的容器Logo
,左上角的logo
MatchInfo
, 正中上方的遊戲進度組件Score
, 右上角的最佳戰績組件Chessboard
, 正中大棋盤Card
, 中間那十六個棋牌PlayStatus
, 最下方的遊戲狀態信息欄#建立目錄 mkdir vue-memory-game #建立一個package.json npm init #進入目錄 cd vue-memory-game #安裝開發環境依賴 npm install --save-dev babel-core babel-loader babel-plugin-transform-object-rest-spread babel-plugin-transform-runtime babel-preset-env css-loader file-loader html-webpack-plugin style-loader vue-hot-reload-api vue-html-loader vue-loader vue-style-loader vue-template-compiler webpack webpack-cli webpack-dev-server webpack-merge #安裝運行時依賴 npm install vue vuex
這裏開發環境依賴內容有點多,但不要懼怕,大部分時候你不太關內心面的東西(固然,若是你要進階,你要升職、加薪、迎娶白富美,那你最好搞清楚他們每一項都是什麼東西)
另外在運行時依賴裏不只看到了vue
,還看到了vuex
。這又是個什麼鬼?先不要慌,也別急着罵娘,咱們來考慮一個問題,試想下,整個遊戲按照上面分解的組件開發時,各個組件之間想必在邏輯上多少是有關係的,譬如:Card
在Chessboard
中的翻牌、配對,固然會影響到上方的Dashboard
和下面的PlayStatus
。那麼「通訊」,就成了待解決問題。
之前咱們試圖用事件廣播來作,但隨之而來的問題是,在應用不斷的擴展、變化中,事件變得愈來愈複雜,愈來愈不可預料,以致於愈來愈難調試,愈來愈難追蹤錯誤的root cause。這固然不是咱們想要的,咱們但願應用的各個部分都易維護、可擴展、好調試、能預測。
因而一種叫單向數據流的方式就冒了出來,用過React
的人想必也不陌生,各組件的間的數據走向永遠是單向、可預期的:
這固然也不是facebook
的專利,都說vue
牛逼了,那必定也有一個單向數據流的實現,就是咱們這裏用到的vuex
。
vue-memory-game ├── css │ └── main.css ├── img │ ├── ... │ └── zeppelin.png ├── js │ ├── components │ │ ├── card │ │ │ ├── Card.vue │ │ │ └── Chessboard.vue │ │ ├── dashboard │ │ │ ├── Dashboard.vue │ │ │ ├── Logo.vue │ │ │ ├── MatchInfo.vue │ │ │ └── Score.vue │ │ ├── footer │ │ │ └── PlayStatus.vue │ │ │ │ │ └── Game.vue │ │ │ ├── vuex │ │ ├── actions │ │ │ └── index.js │ │ ├── getters │ │ │ └── index.js │ │ ├── mutations │ │ │ └── index.js │ │ └── store │ │ ├── index.js │ │ └── statusEnum.js │ │ │ └── index.js │ ├── index.html_vm ├── package.json ├── webpack.config.js └── webpack.config.prod.js
webpack
看了上面的文件目錄結構圖,要配置webpack
,已經沒有難度了,直接上代碼:
const { resolve, join } = require('path') const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { mode: 'development', entry: { index: './js/index.js' }, output: { filename: '[name].[hash].bundle.js', path: resolve(__dirname, 'build') }, devtool: '#source-map', devServer: { contentBase: join(__dirname, 'build'), compress: false, port: 8080, host: '0.0.0.0', hot: true, inline: true }, module: { rules: [ { test: /\.vue$/, use: [ { loader: 'vue-loader' } ], exclude: /node_modules/ }, { test: /\.js$/, use: ['babel-loader'], exclude: /node_modules/ }, { test: /\.css$/, use: ['style-loader', 'css-loader'] }, { test: /\.(png)$/, use: ['file-loader'] } ] }, resolve: { extensions: ['.js', '.vue'] }, plugins: [ new HtmlWebpackPlugin({ filename: 'index.html', inject: 'body', template: 'index.html_vm', favicon: 'img/favicon.ico', hash: false }) ] }
我在這兒沒有過多的涉及webpack
的基本使用,反正webpack4
的文檔還在進行中,翻源碼去吧(~逃)這裏咱們用了html-webpack-plugin裏自動將編譯後的bundle注入
index.html_vm
裏,並生成最終的html
。因此index.html_vm
做爲模板,咱們也要先寫出來:
touch index.html_vm
再將以下內容填入其中:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>vue-memory-game</title> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimal-ui"/> <meta name="renderer" content="webkit"/> <meta http-equiv="Cache-Control" content="no-siteapp" /> </head> <body> <!-- 這裏以一個div#application做爲入口,vue2使用body做爲入口已廢棄 --> <div id="application"></div </body> </html>
在webpack.config.js
裏,咱們看到了
entry: { index: './js/index.js' }
這也是本章整個vue
應用的入口:
// 引入一些初始化的簡單樣式 import '../css/main.css' // 引入vue庫 import Vue from 'vue' // 引入本遊戲核心入口組件 import Game from './components/Game' // 引入狀態管理機 import store from './vuex/store' /* eslint-disable no-new */ new Vue({ el: '#application', render(h) { return h(Game) }, store })
本章代碼本採用ES2015
語法編寫,譬如:components: {Game}
,至關於components: {Game: Game}
,這是 enhanced-object-literals我在這裏沒有過多介紹
vue2
的基本使用,不過我儘可能列出可能涉及的知識點,便於學習
上面js/index.js
裏第一行就引用了全局初始化樣式的css/main.css
,咱們就先把它寫了吧:
* { box-sizing: border-box; padding: 0; margin: 0; } html, body { width: 100%; height: 100%; } body { display: flex; justify-content: center; align-items: center; }
本章大量使用 flexbox來佈局排版,不瞭解的能夠學習一下(雖然我也是半吊子)
這段css/main.css
之因此能被加載成功,多虧了webpack.config.js
中的這段配置:
{ test: /\.css$/, use: ['style-loader', 'css-loader'] },
得利於css-loader
和style-loader
,上述css
能夠成功從index.js
文件裏引入,並被webpack
處理到dom
的<style />
標籤裏
Game
剛纔的入口js/index.js
裏,咱們注入了遊戲主界面組件js/components/Game
,下面就來建立它吧:
<template> <div class="game-panel"> TBD... </div> </template> <script> export default { //TBD } </script> <style scoped> .game-panel { width: 450px; height: 670px; border: 4px solid #BDBDBD; border-radius: 2px; background-color: #faf8ef; padding: 10px; display: flex; flex-direction: column; } </style>
單文件組件的魅力,到這裏終於能夠瞄一眼了,第一部分是模板<template></template>
,第二部分是邏輯<script></script>
,第三部分是樣式<style></style>
。
這裏<style>
上還有個scoped
屬性,表示樣式僅對當前組件以及其子組件的模板部分生效。
單文件組件的加載由webpack.config.js
中的配置:
{ test: /\.vue$/, use: [ { loader: 'vue-loader' } ], exclude: /node_modules/ },
因此咱們能夠在.vue
文件中使用ES2015
語法進行開發。
寫了這麼多,不運行一下,都說不過去了,如今請打開package.json
文件,爲其添加以下代碼:
"scripts": { "start": "webpack-dev-server --hot --inline --host 0.0.0.0 --port 8080" }
而後在項目根目錄調用:
#啓動調試 npm start
瀏覽器訪問:http://localhost:8080/,能夠看到以下效果:
注意js/components/Game
裏的兩個"TBD"部分,咱們如今來補齊:
<template> <div class="game-panel"> <!-- 組裝上、中、下三個部分組件 --> <Dashboard></Dashboard> <Chessboard></Chessboard> <Status></Status> </div> </template> <script> import Dashboard from './dashboard/Dashboard' import Chessboard from './card/Chessboard' import Status from './footer/PlayStatus' //從vuex中拿出mapActions工具 import { mapActions } from 'vuex' //狀態枚舉 import { STATUS } from 'vuex/store/statusEnum' export default { //經過mapActions將actions映射到methods裏 methods: { ...mapActions([ 'updateStatus', 'reset' ]) }, //生命週期鉤子,組件實例建立後自動被調用 created() { //觸發一個狀態更新的action this.updateStatus(STATUS.READY) //觸發一個遊戲重置的action this.reset() }, //子組件注入 components: {Dashboard, Chessboard, Status} } </script> <style scoped> .game-panel{ width: 450px; height: 670px; border: 4px solid #BDBDBD; border-radius: 2px; background-color: #faf8ef; padding: 10px; display: flex; flex-direction: column; } @media screen and (max-width: 450px) { .game-panel{ width: 100%; height: 100%; justify-content: space-around; } } </style>
這裏 vuex/actions/index.js和 vuex/store/statusEnum.js,我就不分別在這裏寫源碼了,內容很簡單, 官網基本教程讀完理解無障礙。
由於功能比較簡單,大部分組件僅樣式有差異,爲了節省時間,我只挑一個最具表明性的components/card/Chessboard.vue來說講
<template> <div class="chessboard"> <Card v-for="(card, index) of cards" :key="index" :option="card" v-on:flipped="onFlipped"></Card> </div> </template> <script> // 引入Card子組件 import Card from './Card'; //從vuex中拿出mapActions和mapGetters工具 import { mapActions, mapGetters } from 'vuex'; import { STATUS } from 'js/vuex/store/statusEnum'; export default { data() { return { // 初始化一個空的lastCard lastCard: null } }, // 經過mapGetters映射各getter爲computed屬性 // 能夠響應vuex對state的mutation // 咱們壓根兒不用關心這些數據何時被改的 // 只管拿來用,數據和UI就是up-to-date // 這個feel倍兒爽 computed: { ...mapGetters(['leftMatched', 'cards', 'status']) }, methods: { // 經過mapActions映射各action爲local method ...mapActions(['updateStatus', 'match', 'flipCards']), onFlipped(e) { // 遊戲開始後,第一次翻牌時,開始爲遊戲計時 if (this.status === STATUS.READY) { this.updateStatus(STATUS.PLAYING) } // 若是以前沒有牌被翻開,把這張牌賦值給lastCard if (!this.lastCard) { return (this.lastCard = e) } // 若是以前有牌被翻了,並且當前翻的這張又正好和以前那張花色相同 if (this.lastCard !== e && this.lastCard.cardName === e.cardName) { // 將lastCard置空 this.lastCard = null // 觸發配對成功的action this.match() // 若是棋盤內全部牌都配對完畢,觸發狀態變動action,並告知已過關 return this.leftMatched || this.updateStatus(STATUS.PASS) } // 以前有牌被翻了,當前翻的這張花色與以前的不一樣 const lastCard = this.lastCard this.lastCard = null setTimeout(() => { // 一秒鐘後將以前那種牌,當前牌再翻回去 this.flipCards([lastCard, e]) }, 1000) } }, // 這裏只用到了Card子組件 components: { Card } } </script> <style scoped> .chessboard { margin-top: 20px; width: 100%; background-color: #fff; height: 530px; border-radius: 4px; padding: 10px 5px; display: flex; flex-wrap: wrap; justify-content: center; align-items: center; align-content: space-around; } .container:nth-child(4n) { margin-right: 0px; } @media screen and (max-width: 450px) { .chessboard { height: 480px; padding: 10px 0px; } } @media screen and (max-width: 370px) { .chessboard { height: 450px; } } </style>
寫在最後,總體寫完的效果,能夠在這裏把玩。
線上demo另加入了排行榜功能,如需查看源碼的,請git checkout stage-1
切換到stage-1
分支
整個項目結構清晰,尤爲單文件組件的表現力尤其突出,使得每一個組件的邏輯都沒有過於複雜,並且在vuex
的統籌下,action
-> mutation
-> state
的單向數據流模式使得全部的變化都在可控制、可預期的範圍內。這點很是利於大型、複雜應用的開發。
另,vue2
已經問世,對於以前跟着一塊兒操做過vue
版的朋友,發現源碼裏有疑惑的變動,請參考升級指南。
vue
做爲一個僅7000
多行的輕量級框架而言,不管生態系統、社區、工具的發展都很是均衡、成熟,徹底能夠適應多業務場景以及穩定性需求。並且,vue2
中對服務器端渲染的支持(並且是史無前例的流式支持),使得你沒必要再爲單頁應用的SEO
問題、首屏渲染加速問題而擔心。欲知詳情,看SSR
總的來講,2016年,vue
讓你的編程生涯,又多了一絲情懷(原諒我實在找不到什麼好詞兒了)。