單文件組件下的vue,能夠擦出怎樣的火花

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就是由firstNamelastName組成,不管怎麼變化,都是如此。這種寫法,更優雅有沒有?webpack

若是有興趣看看用 angular2如何實現相同的小遊戲, 走這裏

單文件組件模式

還在爲一堆代碼文件,到底哪一個是JavaScript邏輯部分、哪一個是css/less/sass樣式部分、哪一個是html/template模板部分;他們又該如何組織,怎麼「編譯」、如何發佈?git

有了單文件組件範式,配合webpack4(雖然文檔依舊WIP),組件自包含,完美、沒毛病!還有強大的開發工具支持,看着都賞心悅目,來個效果圖:es6

用了這麼多版面,說了一些好處,那麼當咱們真正須要面對一個應用,須要上規模開發時,vue又能帶來怎樣的變化呢?憋了幾天,我想今天就寫一個小遊戲來試試總體感受,先來看看咱們今天的目標:

圖片描述

完整源碼在這裏:vue-memory-game

看了效果,知道源碼在哪裏了,那咱們繼續?

組件分解

Break the UI into a component hierarchy,相信寫過React的朋友對這句話都不陌生,在使用一種基於組件開發的模式時,最早考慮,並且也尤其重要的一件事,就是組件分解。下面咱們看看組件分解示意圖:

圖片描述

咱們根據分解圖,先把將來要實現的組件挨個兒列出來:

  1. Game, 最外層的遊戲面板
  2. Dashboard, 上面的logo遊戲進度最佳戰績的容器
  3. Logo,左上角的logo
  4. MatchInfo, 正中上方的遊戲進度組件
  5. Score, 右上角的最佳戰績組件
  6. Chessboard, 正中大棋盤
  7. Card, 中間那十六個棋牌
  8. 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。這又是個什麼鬼?先不要慌,也別急着罵娘,咱們來考慮一個問題,試想下,整個遊戲按照上面分解的組件開發時,各個組件之間想必在邏輯上多少是有關係的,譬如:CardChessboard中的翻牌、配對,固然會影響到上方的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-loaderstyle-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.jsvuex/store/statusEnum.js,我就不分別在這裏寫源碼了,內容很簡單, 官網基本教程讀完理解無障礙。

由於功能比較簡單,大部分組件僅樣式有差異,爲了節省時間,我只挑一個最具表明性的components/card/Chessboard.vue來說講

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讓你的編程生涯,又多了一絲情懷(原諒我實在找不到什麼好詞兒了)。

若是關於代碼有疑問,歡迎issue,也歡迎start

相關文章
相關標籤/搜索