很久以前,很认真地学习了html、css、es6,上手了颇为超前的MVVM框架React,
那时候很喜欢React中纯组件化地概念,尤其是React Router的设计,并且,Redux所推崇的
函数式也是吹牛逼的资本。

最近又重回前端,写了两个小网站,试了试Vue和全新的Typescript,以及api管理工具rap和yapi。稍作整理。

vue启动

现在已经很少有人,从零搭建项目了,也就是自己配置webpack,安装一些开发插件。
这些配置主要涵盖以下几点:

  1. 开发时的lint,语言特性支持。
  2. 编译流程,尤其是使用到js新特性,都会用babel翻译一遍,然后像less、sass等样式配置,
    也需要对应的loader进行翻译。
  3. 开发时用的本地server,可以在本地运行一个web server,供开发者查看网页效果。
  4. 热加载插件,每次修改代码,就马上重新编译,刷新页面。有时候甚至只是局部刷新,并不重新加载页面。
  5. 打包发布,这里还得包括用到的图标、图片,一些文本。通常会配置多个版本,不同之处在于是否压缩,是否携带debug信息。

使用vue cli可以很轻松地构建一个具备这些功能的开发环境。

1
2
3
npm i -g @vue/cli

vue create your-app-name

选择手动控制可以细粒度地选择一些特性,
我一般会打开typescript、eslint。

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
├── .browserslistrc
├── .editorconfig
├── .eslintrc.js
├── Dockerfile
├── LICENSE
├── README.md
├── babel.config.js
├── package.json
├── public
│   ├── favicon.ico
│   └── index.html
├── src
│   ├── App.vue
│   ├── api
│   │   ├── index.ts
│   │   └── request.ts
│   ├── assets
│   │   └── logo.png
│   ├── components
│   ├── main.ts
│   ├── router
│   │   └── index.ts
│   ├── shims-tsx.d.ts
│   ├── shims-vue.d.ts
│   ├── store
│   │   └── index.ts
│   └── views
│   └── Home.vue
├── tsconfig.json
├── yarn.lock
└── ytt.config.ts

几个配置文件都相当短,vue的插件封装了许多功能,
有点类似java里的springboot-compiler-plugin。

DOM树

Vue给用户看到的也就是一个树状结构的组件视图,一般叫做虚拟DOM树。
从代码来看,index.html里放了一个带id的标签,然后js里,
vue的mount函数,使用了这个id。整个由vue构建的视图就插入到了这个标签中。

初始化vue的时候,router和全局状态管理vuex就被传递了进去。

1
2
3
4
5
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
1
2
3
4
5
6
7
8
9
10
<template>
<div>
<a-layout id="layout">
<a-layout-header style="padding: 0;"><div style="font-size: 20px; color: #ffffff">HCloud Web</div></a-layout-header>
<a-layout-content :style="{ margin: '24px 16px', padding: '24px', background: '#fff', minHeight: '600px' }">
<router-view/>
</a-layout-content>
</a-layout>
</div>
</template>

因此,App.vue就相当于是Root。
这里就可以按照html的写法,组织标签了。

组件

自定义组件

不同的是,vue的template里可以使用各种自定义组件。
如上文中的<a-xxx>标签,都是引用自antd的组件。不过实际上引入antd用的是Vue.use(),
按官方说法,这是引入的plugin,会调用目标的intall方法,相当于一个hook。全局组件的注册使用Vue.component( id, [definition] )

我们自己写的组件,一般不会注册到全局,那样比较混乱。只有基础、常用的组件会注册到全局,
而这些组件都会有各种库提供。

引入自己写的组件可以用import语句,直接引入那个vue文件。然后放到components属性下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import CustomComponent from '@/src/components/custom.vue'
export default {
components: {
'component-a': ComponentA,
'component-b': ComponentB
}
}

// vue class
// other options should be placed in component function
@Component({
components: {
AceEditor
}
})
export default class Editor extends Vue {

}

组件属性绑定

正如在代码中看到的,每个标签都或多或少有一些属性值。
属性的传入直接在标签上填写即可,代表的是一个常量。
如果要传递变量,就需要使用vue提供的特殊方法。
常用的有v-bind、v-once、v-on。具体含义参考手册。

1
<blog-post v-bind:title="post.title"></blog-post>

对于组件自身,props可以直接访问,就如同它自己的属性一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
const AceEditorProps = Vue.extend({
props: {
name: String
}
})

@Component
export default class AceEditor extends AceEditorProps {
get message (): string {
// this.name will be typed
return 'Hello, ' + this.name
}
}

全局中心存储

在我看来,只有props需求的组件,复用性很好,即插即用。
但真正做一个网页,或者说一个项目的时候。很多页面是单例模式:
组件或许可以复用,但在这个项目里,它绝对只会出现一次,数据也只有一份。
比如说,购物车,网页主页,个人中心。

这些页面显然也是由组件堆叠起来的,但它们整体是不可复用的,也不需要复用。
当你想要制作两个购物车页面,那就得有两份不同的购物车数据,两份路由,
这是不可复用的根源,同时也没有必要做这种事情。

因此,这些数据是唯一的情景下,很适合将数据抽离出来,存放到全局存储中。
某种程度上,这也是无奈之举,谈不上多好的设计,是一个非常自然的想法:
数据是唯一的,那就必然要交给一个唯一的对象去管理。
如果类似组件的属性那样管理,这个数据传递起来会非常麻烦,
子组件都必须通过props层层传递,而修改的入口也没有限定。
正因为有种种不便之处,才开发出一个中心存储,再通过一些奇技淫巧,
使得所有组件都能访问到这个中心存储。

Vue的中心存储叫做Vuex。
提供Store、Mutation、Action、Module四个接口。

Store非常好理解,就是刚才说的中心存储。

1
this.$store.state.p

像这样访问属性的方式,就可以访问store的数据。
如果想把这些数据使用到组件中,那么需要再套一层。

1
2
3
4
5
6
7
8
9
10
computed: {
p() {
return this.$store.state.p
}
}

// class
get name() {
return this.$store.state.p
}

根据vuex的要求,修改属性一定要通过mutation,不能直接给state赋值。
从原理上说,修改属性这个操作,是必须要被监控到的,因为它可能触发DOM的改变。
通过mutation来修改属性,就有了一个明显的切面去监控数据变化。
但是实际上实现这个监控,手法有很多,也是有可能实现成,直接赋值,也被监控到的。
不过,使用mutation还有一个好处是,数据的改变方式是固定的,甚至可以加上类型限定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
// mutate state
state.count++
}
}
})

// component method
this.$store.commit('increment')

Mutation都是同步方法,如果想要实现异步接口,就得用Action。
Promise就不展开讲了。
经过我尝试,async也是可以用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
actions: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
},
// async, this is not a good example
// because setTimeout is not 'promisefy'
async actionB ({ commit }) {
await new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
},
}

Module就是提供树状层次,让状态不必全部集中到一个入口上,而是分散在不同模块中。

路由

Vue的路由和React差异很大。React自Router 4.0之后,Router就和普通组件一模一样,
直接嵌入到页面中即可。

Vue的路由采取的是slot的方式。先填一些占位符,再在Router初始化时,统一定义每个占位符。

1
2
3
4
5
<template>
<div id="app">
<router-view/>
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import Info from '@/views/FunctionInfo.vue'
import Home from '@/views/Home.vue'
import Editor from '@/views/Editor.vue'

const routes = [
{
path: '/function/:id',
component: Info
},
{
path: '/newfunction',
component: Editor
},
{
path: '*',
component: Home
}
]

嵌套路由就是在子组件里面也放上<router-view>,在routes定义中使用children
详情看手册把,不赘述了。

路由的改变也提供了一个封装的link组件。
但实际上就是通过this.$router所携带的各种方法操作路由。
详情见手册。