Skip to content

Commit b1aecce

Browse files
author
binaryify
committed
first commit
1 parent 5494fdb commit b1aecce

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

87 files changed

+9175
-2
lines changed

.babelrc

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"presets": [
3+
["env", { "modules": false }],
4+
"stage-2",
5+
"es2015"
6+
],
7+
"plugins": ["transform-runtime","transform-vue-jsx"],
8+
"comments": false,
9+
"env": {
10+
"test": {
11+
"presets": ["env", "stage-2"],
12+
"plugins": [ "istanbul" ]
13+
}
14+
}
15+
}

.editorconfig

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
indent_style = space
6+
indent_size = 2
7+
end_of_line = lf
8+
insert_final_newline = true
9+
trim_trailing_whitespace = true

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.DS_Store
2+
node_modules/
3+
dist/
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*

.postcssrc.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// https://github.com/michael-ciniawsky/postcss-load-config
2+
3+
module.exports = {
4+
"plugins": {
5+
// to edit target browsers: use "browserlist" field in package.json
6+
"autoprefixer": {}
7+
}
8+
}

README.md

Lines changed: 177 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,177 @@
1-
# vue-tetris
2-
Use Vue, Vuex, Immutable to code Tetris.
1+
----
2+
## 用Vue、Vuex、Immutable做俄罗斯方块
3+
4+
----
5+
本项目灵感来源于 React 版的俄罗斯方块,由于对其实现原理较感兴趣,而且相比于 React 更喜欢 Vue, 于是把 React 版的重构为了 Vue 版的,大致思路是把组件当成一个个函数,保证一个输入(props)能得到一个确定的输出(view),然后对不同方法也是做同样处理,对于 Redux 使用 Vuex 精简化
6+
7+
戳:[http://binaryify.github.io/vue-tetris/](http://binaryify.github.io/vue-tetris/) 玩一玩!
8+
9+
----
10+
### 效果预览
11+
![效果预览](https://img.alicdn.com/tps/TB1Ag7CNXXXXXaoXXXXXXXXXXXX-320-483.gif)
12+
13+
正常速度的录制,体验流畅。
14+
15+
### 响应式
16+
![响应式](https://img.alicdn.com/tps/TB1AdjZNXXXXXcCapXXXXXXXXXX-480-343.gif)
17+
18+
不仅指屏幕的自适应,而是`在PC使用键盘、在手机使用手指的响应式操作`
19+
20+
![手机](https://img.alicdn.com/tps/TB1kvJyOVXXXXbhaFXXXXXXXXXX-320-555.gif)
21+
22+
### 数据持久化
23+
![数据持久化](http://7xkm8j.com1.z0.glb.clouddn.com/persistence.gif)
24+
25+
玩单机游戏最怕什么?断电。通过订阅 `store.subscribe`,将state储存在localStorage,精确记录所有状态。网页关了刷新了、程序崩溃了、手机没电了,重新打开连接,都可以继续。
26+
27+
### Vuex 状态预览([Vue DevTools extension](https://github.com/vuejs/vue-devtools)
28+
![Vuex状态预览](http://7xkm8j.com1.z0.glb.clouddn.com/vuex.gif)
29+
30+
Vuex 设计管理了所有应存的状态,这是上面持久化的保证。
31+
32+
----
33+
游戏框架使用的是 Vue + Vuex,其中再加入了 Immutable,确保性能和数据可靠性
34+
35+
36+
## 1、什么是 Immutable?
37+
Immutable 是一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。
38+
39+
### 初识:
40+
让我们看下面一段代码:
41+
``` JavaScript
42+
function keyLog(touchFn) {
43+
let data = { key: 'value' };
44+
f(data);
45+
console.log(data.key); // 猜猜会打印什么?
46+
}
47+
```
48+
不查看f,不知道它对 `data` 做了什么,无法确认会打印什么。但如果 `data` 是 Immutable,你可以确定打印的是 `value`
49+
``` JavaScript
50+
function keyLog(touchFn) {
51+
let data = Immutable.Map({ key: 'value' });
52+
f(data);
53+
console.log(data.get('key')); // value
54+
}
55+
```
56+
57+
JavaScript 中的`Object``Array`等使用的是引用赋值,新的对象简单的引用了原始对象,改变新也将影响旧的:
58+
``` JavaScript
59+
foo = {a: 1}; bar = foo; bar.a = 2;
60+
foo.a // 2
61+
```
62+
虽然这样做可以节约内存,但当应用复杂后,造成了状态不可控,是很大的隐患,节约的内存优点变得得不偿失。
63+
64+
Immutable则不一样,相应的:
65+
``` JavaScript
66+
foo = Immutable.Map({ a: 1 }); bar = foo.set('a', 2);
67+
foo.get('a') // 1
68+
```
69+
70+
### 关于 “===”:
71+
我们知道对于`Object``Array``===`比较,是对引用地址的比较而不是“值比较”,如:
72+
``` JavaScript
73+
{a:1, b:2, c:3} === {a:1, b:2, c:3}; // false
74+
[1, 2, [3, 4]] === [1, 2, [3, 4]]; // false
75+
```
76+
对于上面只能采用 `deepCopy``deepCompare`来遍历比较,不仅麻烦且好性能。
77+
78+
我们感受来一下`Immutable`的做法!
79+
``` JavaScript
80+
map1 = Immutable.Map({a:1, b:2, c:3});
81+
map2 = Immutable.Map({a:1, b:2, c:3});
82+
Immutable.is(map1, map2); // true
83+
84+
// List1 = Immutable.List([1, 2, Immutable.List[3, 4]]);
85+
List1 = Immutable.fromJS([1, 2, [3, 4]]);
86+
List2 = Immutable.fromJS([1, 2, [3, 4]]);
87+
Immutable.is(List1, List2); // true
88+
```
89+
90+
91+
Immutable学习资料:
92+
* [Immutable.js](http://facebook.github.io/immutable-js/)
93+
94+
95+
## 2、Web Audio Api
96+
游戏里有很多不同的音效,而实际上只引用了一个音效文件:[/build/music.mp3](https://github.com/chvin/react-tetris/blob/master/build/music.mp3)。借助`Web Audio Api`能够以毫秒级精确、高频率的播放音效,这是`<audio>`标签所做不到的。在游戏进行中按住方向键移动方块,便可以听到高频率的音效。
97+
98+
![网页音效进阶](https://img.alicdn.com/tps/TB1fYgzNXXXXXXnXpXXXXXXXXXX-633-358.png)
99+
100+
`WAA` 是一套全新的相对独立的接口系统,对音频文件拥有更高的处理权限以及更专业的内置音频效果,是W3C的推荐接口,能专业处理“音速、音量、环境、音色可视化、高频、音向”等需求,下图介绍了WAA的使用流程。
101+
102+
![流程](https://img.alicdn.com/tps/TB1nBf1NXXXXXagapXXXXXXXXXX-520-371.png)
103+
104+
其中Source代表一个音频源,Destination代表最终的输出,多个Source合成出了Destination。
105+
源代码:[/src/unit/music.js](https://github.com/chvin/react-tetris/blob/master/src/unit/music.js) 实现了ajax加载mp3,并转为WAA,控制播放的过程。
106+
107+
`WAA` 在各个浏览器的最新2个版本下的支持情况([CanIUse](http://caniuse.com/#search=webaudio)
108+
109+
![浏览器兼容](https://img.alicdn.com/tps/TB15z4VOVXXXXahaXXXXXXXXXXX-679-133.png)
110+
111+
可以看到IE阵营与大部分安卓机不能使用,其他ok。
112+
113+
114+
Web Audio Api 学习资料:
115+
* [Web API 接口| MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Audio_API)
116+
* [Getting Started with Web Audio API](http://www.html5rocks.com/en/tutorials/webaudio/intro/)
117+
118+
----
119+
## 3、游戏在体验上的优化
120+
* 技术:
121+
* 按下方向键水平移动和竖直移动的触发频率是不同的,游戏可以定义触发频率,代替原生的事件频率,源代码:[/src/unit/event.js](https://github.com/chvin/react-tetris/blob/master/src/unit/event.js) ;
122+
* 左右移动可以 delay 掉落的速度,但在撞墙移动的时候 delay 的稍小;在速度为6级时 通过delay 会保证在一行内水平完整移动一次;
123+
* 对按钮同时注册`touchstart`和`mousedown`事件,以供响应式游戏。当`touchstart`发生时,不会触发`mousedown`,而当`mousedown`发生时,由于鼠标移开事件元素可以不触发`mouseup`,将同时监听`mouseout` 模拟 `mouseup`。源代码:[/src/components/keyboard/index.js](https://github.com/Binaryify/vue-tetris/blob/master/src/components/keyboard/index.js);
124+
* 监听了 `visibilitychange` 事件,当页面被隐藏\切换的时候,游戏将不会进行,切换回来将继续,这个`focus`状态也被写进了 Vuex 中。所以当用手机玩来`电话`时,游戏进度将保存;PC开着游戏干别的也不会听到gameover,这有点像 `ios` 应用的切换。
125+
* 在`任意`时刻刷新网页,(比如消除方块时、游戏结束时)也能还原当前状态;
126+
* 游戏中唯一用到的图片是![image](https://img.alicdn.com/tps/TB1qq7kNXXXXXacXFXXXXXXXXXX-400-186.png),其他都是CSS;
127+
* 游戏兼容 Chrome、Firefox、IE9+、Edge等;
128+
* 玩法:
129+
* 可以在游戏未开始时制定初始的棋盘(十个级别)和速度(六个级别);
130+
* 一次消除1行得100分、2行得300分、3行得700分、4行得1500分;
131+
* 方块掉落速度会随着消除的行数增加(每20行增加一个级别);
132+
133+
----
134+
135+
## 4、开发中的经验梳理
136+
Vue 版本和 React 版本核心代码基本相同,但在编写组件的时候遇到了几个问题,比如:
137+
1. React 版的 store 使用了 immutable 结构的数据,vuex 上的 store 如果使用了 immutable 结构,不利用监听数据变化,故把store 的数据全部使用了普通的数据,在需要这些数据的地方通过 immutable 提供的 `fromJS` 转换,在需要普通数据的地方再通过 immutable 的 `toJS` 转换成普通数据
138+
139+
2. Vue 没有 React 的`componentWillReceiveProps` 的生命周期,我的解决方法是使用 watch 配合 `deep:true` 来监听 props 的变化,如:
140+
```js
141+
watch: {
142+
$props: {
143+
deep: true,
144+
handler(nextProps) {
145+
//xxx
146+
}
147+
}
148+
}
149+
```
150+
151+
3. `matrix 组件` 的功能逻辑较复杂,使用 `template` 模版来渲染组件已经不合适了,通过自定义 render 方法再手动触发很繁琐,我的解决方法是通过 Vue 的 jsx 转换插件[babel-plugin-transform-vue-jsx](https://github.com/vuejs/babel-plugin-transform-vue-jsx)来使用 jsx 语法对页面进行渲染,当 props 或 state 变化了自动触发 render 方法,另外要注意的是 vue 的 jsx 和 React 的 jsx 书写上有一点的差异
152+
153+
## 5、架构差异
154+
Redux 的数据流向是 把 store 的状态转化为 props 注入到 根组件,根组件再把这些 props 传入不同组件,当 store 的状态变化,根组件会重新 render, 更新子组件上的 props,子组件再 根据新 props重新 render
155+
引用知乎一个答友的回答[https://www.zhihu.com/question/47686258](https://www.zhihu.com/question/47686258)来说就是:
156+
>单例store的数据在react中可以通过view组件的属性(props)不断由父模块**“单向”**传递给子模块,形成一个树状分流结构。如果我们把redux比作整个应用的“心肺” (redux的flux功能像心脏,reducer功能像肺部毛细血管),那么这个过程可以比作心脏(store)将氧分子(数据)通过动脉毛细血管(props)送到各个器官组织(view组件)末端的view组件,又可以通过flux机制,将携带交互意图信息的action反馈给store。这个过程有点像将携带代谢产物的“红细胞”(action)通过静脉毛细血管又泵回心脏(store)action流回到store以后,action以参数的形式又被分流到各个具体的reducer组件中,这些reducer同样构成一个树状的hierarchy。这个过程像静脉血中的红细胞(action)被运输到肺部毛细血管(reducer组件)接收到action后,各个child reducer以返回值的形式,将最新的state返回给parent reducer,最终确保整个单例store的所有数据是最新的。这个过程可以比作肺部毛细血管的血液充氧后,又被重新泵回了心脏回到步骤1
157+
158+
而 vuex 的思路则不同,任何组件都随时可以通过 this.$store.state.xxx 获取 store 上的数据,更自由,只要 store 上的数据变了,组件都会自动重新渲染
159+
160+
## 6、开发
161+
### 安装
162+
```
163+
npm install
164+
```
165+
### 运行
166+
```
167+
npm run dev
168+
```
169+
浏览自动打开 [localhost:8080](localhost:8080)
170+
### 多语言
171+
[i18n.json](https://github.com/Binaryify/vue-tetris/blob/master/i18n.json) 配置多语言环境,使用"lan"参数匹配语言如:`https://Binaryify.github.io/vue-tetris/?lan=en`
172+
### 打包编译
173+
```
174+
npm run build
175+
```
176+
177+
`dist` 文件夹下生成结果。

build/build.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
require('./check-versions')()
2+
3+
process.env.NODE_ENV = 'production'
4+
5+
var ora = require('ora')
6+
var rm = require('rimraf')
7+
var path = require('path')
8+
var chalk = require('chalk')
9+
var webpack = require('webpack')
10+
var config = require('../config')
11+
var webpackConfig = require('./webpack.prod.conf')
12+
13+
var spinner = ora('building for production...')
14+
spinner.start()
15+
16+
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
17+
if (err) throw err
18+
webpack(webpackConfig, function (err, stats) {
19+
spinner.stop()
20+
if (err) throw err
21+
process.stdout.write(stats.toString({
22+
colors: true,
23+
modules: false,
24+
children: false,
25+
chunks: false,
26+
chunkModules: false
27+
}) + '\n\n')
28+
29+
console.log(chalk.cyan(' Build complete.\n'))
30+
console.log(chalk.yellow(
31+
' Tip: built files are meant to be served over an HTTP server.\n' +
32+
' Opening index.html over file:// won\'t work.\n'
33+
))
34+
})
35+
})

build/check-versions.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
var chalk = require('chalk')
2+
var semver = require('semver')
3+
var packageConfig = require('../package.json')
4+
var shell = require('shelljs')
5+
function exec (cmd) {
6+
return require('child_process').execSync(cmd).toString().trim()
7+
}
8+
9+
var versionRequirements = [
10+
{
11+
name: 'node',
12+
currentVersion: semver.clean(process.version),
13+
versionRequirement: packageConfig.engines.node
14+
},
15+
]
16+
17+
if (shell.which('npm')) {
18+
versionRequirements.push({
19+
name: 'npm',
20+
currentVersion: exec('npm --version'),
21+
versionRequirement: packageConfig.engines.npm
22+
})
23+
}
24+
25+
module.exports = function () {
26+
var warnings = []
27+
for (var i = 0; i < versionRequirements.length; i++) {
28+
var mod = versionRequirements[i]
29+
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
30+
warnings.push(mod.name + ': ' +
31+
chalk.red(mod.currentVersion) + ' should be ' +
32+
chalk.green(mod.versionRequirement)
33+
)
34+
}
35+
}
36+
37+
if (warnings.length) {
38+
console.log('')
39+
console.log(chalk.yellow('To use this template, you must update following to modules:'))
40+
console.log()
41+
for (var i = 0; i < warnings.length; i++) {
42+
var warning = warnings[i]
43+
console.log(' ' + warning)
44+
}
45+
console.log()
46+
process.exit(1)
47+
}
48+
}

build/dev-client.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/* eslint-disable */
2+
require('eventsource-polyfill')
3+
var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
4+
5+
hotClient.subscribe(function (event) {
6+
if (event.action === 'reload') {
7+
window.location.reload()
8+
}
9+
})

0 commit comments

Comments
 (0)