2016注定不是个平凡年,无论是中秋节问世的angular2,还是全面走向稳定的React,都免不了面对另一个竞争对手vue2。喜欢vue
在设计思路上的“先进性”(原谅我用了这么一个词),敬佩作者尤小右本人的“国际范儿”,使得各框架之间的竞争略显妖娆(虽然从已存在问题的解决方案上看,各框架都有部分相似之处)。
因为vue2
已经正式release,本教程做了一些修改(针对vue2
)
所谓设计上的先进性,以下几点是我比较喜欢的:
数据驱动的响应式编程体验
不同于AngularJS
里基于digest cycle
的脏检查机制,执行效率更高。内部基于Object.defineProperty
特性做漂亮的hack实现(而且不支持IE8,大快人心)。更多细节,看这里
因为这个机制的出现,我们再也也不需要顾虑双向绑定的效率问题;亦或是像React
那样搞什么immutability(对这块感兴趣可以看(译)JavaScript中的不可变性),因为Object.definePropery
洞悉你的一切,妈妈再也不用担心你忘记实现shouldComponentUpdate
了.
到这里你可能还不能体会vue
的精妙,是时候来个栗子了!
假设我们有一个字段fullName
,它依赖其他字段的变化,在AngularJS
里,我们或许会用命令式这样写道:
$scope.user = {
firstName: '',
lastName: ''
}
$scope.fullName = ''
//告诉程序主动“监视”user的变化,然后修改fullName的值
$scope.$watch('user', function(user) {
$scope.fullName = user.firstName + ' ' + user.lastName
}, true)
若是vue
,改用声明式,写法如何?
data() {
return {
firstName: '',
lastName: ''
}
},
computed: {
fullName() {
// 生命一个fullName的计算属性,并告诉程序它是由firstName和lastName组成。
// 至于具体是什么时候/如何完成数据拼装的,你就不用管了
return this.firstName + ' ' + this.lastName
}
}
相对于AngularJS
里命令式的告诉框架,fullName
一定要监视user
对象的变化(注意里面还是deepWatch,效率更差),并且随之改变;vue
以数据驱动为本质,声明式的定义fullName
就是由firstName
和lastName
组成,无论怎么变化,都是如此。这种写法,更优雅有没有?
如果有兴趣看看用
angular2
如何实现相同的小游戏,
走这里
单文件组件模式
还在为一堆代码文件,到底哪个是JavaScript
逻辑部分、哪个是css/less/sass
样式部分、哪个是html/template
模板部分;他们又该如何组织,怎么“编译”、如何发布?
有了单文件组件范式,配合webpack4(虽然文档依旧WIP),组件自包含,完美、没毛病!还有强大的开发工具支持,看着都赏心悦目,来个效果图:
用了这么多版面,说了一些好处,那么当我们真正需要面对一个应用,需要上规模开发时,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来讲讲
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
让你的编程生涯,又多了一丝情怀(原谅我实在找不到什么好词儿了)。