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
让你的编程生涯,又多了一丝情怀(原谅我实在找不到什么好词儿了)。