单文件组件下的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

相关文章
相关标签/搜索