转载

单文件组件下的vue,可以擦出怎样的火花

2016注定不是个平凡年,无论是即将问世的 angular2 (虽然到目前为止2016-05-20仍没有release date),还是全面走向稳定的 React ,都免不了面对另一个竞争对手 vue 。由于 vue 设计思路的“先进性”(原谅我用了这么一个词),以及作者尤小右本人的“国际范儿”(原谅我又用了一个怪词),使得各框架之间的竞争略显妖娆(虽然从已存在问题的解决方案上看,各框架都有部分相似之处)。

所谓设计上的先进性,以下几点是我比较喜欢的:

数据驱动的响应式编程体验

不同于 AngularJS 里基于 digest cycle 的脏检查机制,执行效率更高。内部基于 Object.defineProperty 特性做漂亮的hack实现(而且不支持IE8,大快人心)。更多细节, 看这里

因为这个机制的出现,我们再也也不需要顾虑双向绑定的效率问题;亦或是像 React 那样搞什么 immutability (对这块感兴趣可以看 [译]JavaScript中的不可变性 ),因为 Object.definePropery 洞悉你的一切,妈妈再也不用担心你忘记实现 shouldComponentUpdate 了.

我猜不来个栗子你是不会买账的!假设我们有一个字段 fullName ,它依赖其他字段的变化,在 AngularJS 里,我们这样写:

$scope.user = {     firstName: '',     lastName: '' };        $scope.fullName = '';        $scope.$watch('user', function(user) {     $scope.fullName = user.firstName + ' ' + user.lastName; }, true);

若是 vue ,写法如何?

data: {     firstName: '',     lastName: '' }, computed: {     fullName: function () {         return this.firstName + ' ' + this.lastName;     } }

相对于 AngularJS 里命令式的告诉框架, fullName 一定要监视 user 对象的变化(注意里面还是deepWatch,效率更差),并且随之改变; vue 以数据驱动为本质,声明式的定义 fullName 就是由 firstNamelastName 组成,无论怎么变化,都是如此。这种写法,更优雅有没有?

单文件组件模式

还在为一堆代码文件,到底哪个是 JavaScript 逻辑部分、哪个是 css/less/sass 样式部分、哪个是 html/template 模板部分;他们又该如何组织,怎么“编译”、如何发布?

有了单文件组件范式,配合 webpack ,组件自包含,完美、没毛病!还有强大的开发工具支持,看着都赏心悦目,来个效果图:

单文件组件下的vue,可以擦出怎样的火花

用了这么多版面,说了一些好处,那么当我们真正需要面对一个应用,需要上规模开发时, vue 又能带来怎样的变化呢?憋了几天,我想今天就写一个小游戏来试试整体感觉,先来看看我们今天的目标:

单文件组件下的vue,可以擦出怎样的火花

完整源码在这里: vue-memory-game

看了效果,知道源码在哪里了,那我们继续?

组件分解

Break the UI into a component hierarchy ,相信写过 React 的朋友对这句话都不陌生,在使用一种基于组件开发的模式时,最先考虑,而且也尤为重要的一件事,就是组件分解。下面我们看看组件分解示意图:

单文件组件下的vue,可以擦出怎样的火花

我们根据分解图,先把未来要实现的组件挨个儿列出来:

  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-runtime babel-preset-es2015 css-loader file-loader html-webpack-plugin style-loader vue-hot-reload-api vue-html-loader vue-loader vue-style-loader webpack webpack-dev-server  #安装运行时依赖 npm install --save vue vuex

这里开发环境依赖内容有点多,但不要害怕,大部分时候你不太关心里面的东西(当然,如果你要进阶,你要升职、加薪、迎娶白富美,那你最好搞清楚他们每一项都是什么东西)

另外在运行时依赖里不仅看到了 vue ,还看到了 vuex 。这又是个什么鬼?先不要慌,也别急着骂娘,我们来考虑一个问题,试想下,整个游戏按照上面分解的组件开发时,各个组件之间想必在逻辑上多少是有关系的,譬如: CardChessboard 中的翻牌、配对,当然会影响到上方的 Dashboard 和下面的 PlayStatus 。那么“通信”,就成了待解决问题。

以前我们试图用事件广播来做,但随之而来的问题是,在应用不断的扩展、变化中,事件变得越来越复杂,越来越不可预料,以至于越来越难调试,越来越难追踪错误的root cause。这当然不是我们想要的,我们希望应用的各个部分都易维护、可扩展、好调试、能预测。

于是一种叫单向数据流的方式就冒了出来,用过 React 的人想必也不陌生,各组件的间的数据走向永远是单向、可预期的:

单文件组件下的vue,可以擦出怎样的火花

这当然也不是 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 │   │ │   ├── lib │   │   └── shuffle.js │   │ │   ├── vuex │   │   ├── actions │   │   │   ├── controlCenter.js │   │   │   └── types.js │   │   ├── getters │   │   │   └── stateHolder.js │   │   └── store │   │       ├── index.js │   │       └── statusEnum.js │   │ │   └── index.js │ ├── index.html_vm ├── package.json ├── webpack.config.js └── webpack.config.prod.js

配置 webpack

看了上面的文件目录结构图,要配置 webpack ,已经没有难度了,直接上代码:

var path = require('path'); var HtmlWebpackPlugin = require('html-webpack-plugin');  module.exports = {     entry: {         index: './js/index.js'     },     output: {         path: path.resolve(__dirname, 'build'),         filename: '[name].[hash].bundle.js'     },     debug: true,     devtool: '#eval-source-map',     module: {         loaders: [             {                 test: //.vue$/,                 loader: 'babel?{"presets":["es2015"]}!vue',                 exclude: /node_modules/             },             {                 test: //.js$/,                 loader: 'babel?{"presets":["es2015"]}',                 exclude: /node_modules/             },             {                 test: //.css$/,                 loader: 'style!css'             },             {                 test: //.(png)$/,                 loader: 'file'             }         ]     },     resolve: {         root: [             path.resolve(__dirname),             path.resolve(__dirname, 'js')         ],         extensions: [             '',             '.js',             '.vue'         ]     },     plugins: [         new HtmlWebpackPlugin({             filename: 'index.html',             inject: 'body',             template: 'index.html_vm',             favicon: 'img/favicon.ico',             hash: false         })     ] };

这里我们用了 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>     <!-- 注意,这里就是我们整个游戏应用的组件,Game -->     <Game></Game> </body> </html>

编写应用入口

webpack.config.js 里,我们看到了

entry: {     index: './js/index.js' }

这也是本章整个 vue 应用的入口:

//载入一些初始化的简单样式 import 'css/main.css'; import Vue from 'vue'; import Game from './components/Game'; //store就是vuex里用来存储组件树用到的所有状态的对象 import store from 'js/vuex/store';  //the main entrance /* eslint-disable no-new */ new Vue({el: 'body', components: {Game}, store});//在这里注入store,vue会自动将实例注入到所有子组件中

本章代码本采用 ES2015 语法编写,譬如: components: {Game} ,相当于 components: {Game: Game} ,这是 enhanced-object-literals

全局初始化样式

上面 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 来布局排版,不了解的可以学习一下(虽然我也是半吊子)

第一个组件 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 属性,表示样式仅对当前组件以及其子组件的模板部分生效。

写了这么多,不运行一下,都说不过去了,现在请打开 package.json 文件,为其添加如下代码:

"scripts": {     "start": "webpack-dev-server --content-base build/ --hot --inline" }

然后在项目根目录调用:

#启动调试 npm start

浏览器访问: http://localhost:8080/ ,可以看到如下效果:

单文件组件下的vue,可以擦出怎样的火花

注意 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';  //从actions里拿两个action,一个重置游戏、一个更新状态 import { reset, updateStatus } from 'vuex/actions/controlCenter'; //状态枚举 import { STATUS } from 'vuex/store/statusEnum';  export default {     //vuex是一个特殊的属性,actions放在里面,     //省却了我们手动传入this.$store的麻烦     vuex: {         actions: {             reset,             updateStatus         }     },     //生命周期钩子,组件实例创建后自动被调用     created: function() {         //触发一个状态更新的action         this.updateStatus(STATUS.READY);         //触发一个游戏充值的action         this.reset();     },     //子组件注入     components: {Dashboard, Chessboard, Status} } </script>

子组件注入的方式,和 angular2 类似,有木有?

这里 vuex/actions/controlCenter.js 和 vuex/store/statusEnum.js ,我就不分别在这里写源码了,内容很简单, 官网基本教程 读完理解无障碍。

因为功能比较简单,大部分组件仅样式有差别,为了节省时间,我只挑一个最具代表性的 components/card/Chessboard.vue 来讲讲

components/card/Chessboard.vue

<template>     <div class="chessboard">         <!-- 遍历Card组件,为每个Card传入option定制其状态;并接收一个flipped的事件hook -->         <Card v-for="cart in cards" :option="cart" v-on:flipped="onFlipped"></Card>     </div> </template>  <script> //引入Card子组件 import Card from './Card';  //从控制中心拿各种actions import { updateStatus, match, flipCards } from 'js/vuex/actions/controlCenter'; //从状态管理器里拿各种数据的getter import { leftMatched, cards, status } from 'js/vuex/getters/stateHolder';  import { STATUS } from 'js/vuex/store/statusEnum';  export default {      data: function() {         return {             //初始化一个空的lastCard属性             lastCard: null         };     },      vuex: {         actions: {             updateStatus,             match,             flipCards         },         //getters类似computed property         //可以响应vuex对state的mutation         //我们压根儿不用关心这些数据什么时候被改的         //只管拿来用,数据和UI就是up-to-date         //这个feel倍儿爽         getters: {             leftMatched,             cards,             status         }     },      methods: {          onFlipped: function(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);             }             //之前有牌被翻了,当前翻的这张花色与之前的不同             let 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; } </style>

写在最后,整体写完的效果,可以 在这里 把玩。

整个项目结构清晰,尤其单文件组件的表现力尤为突出,使得每个组件的逻辑都没有过于复杂,而且在 vuex 的统筹下, action -> mutation -> state 的单向数据流模式使得所有的变化都在可控制、可预期的范围内。这点非常利于大型、复杂应用的开发。

另, vue2 即将问世,为避免升级障碍,本章中代码尽量规避 vue2 中已明确被废弃的方法, vue2 变更列表 看这里 。

vue 作为一个仅 4000 多行的轻量级框架而言,无论生态系统、社区、工具的发展都非常均衡、成熟,完全可以适应多业务场景以及稳定性需求。而且, vue2 中对服务器端渲染的支持(而且是前所未有的流式支持),使得你不必再为单页应用的 SEO 问题、首屏渲染加速问题而担忧。

总的来说,2016年, vue 让你的编程生涯,又多了一丝情怀(原谅我实在找不到什么好词儿了)。

如果关于代码有疑问,欢迎 issue

原文  https://segmentfault.com/a/1190000005168085
正文到此结束
Loading...