前言
最近项目里开始用Vue.js 2.4,感觉这个框架确实不错,上手比较容易。整理一下这段时间的开发经验,主要是组件开发方面的一些实践。
Vue.js简介
Vue.js是一个渐进式JavaScript框架,目前用的是2.4版本。相比Angular那套复杂的体系,Vue学习成本低很多,而且文档写得很清楚。
核心特性
- 响应式数据绑定 - 数据变了视图自动更新
- 组件化开发 - 把页面拆成一个个组件
- 虚拟DOM - 性能优化,不用直接操作DOM
- 指令系统 - v-if、v-for这些很好用
开发环境搭建
使用vue-cli脚手架
1
2
3
4
5
6
7
8
| # 安装vue-cli
npm install -g vue-cli
# 创建项目
vue init webpack my-project
cd my-project
npm install
npm run dev
|
项目结构
1
2
3
4
5
6
7
| src/
├── components/ # 组件目录
├── views/ # 页面组件
├── router/ # 路由配置
├── store/ # Vuex状态管理
├── assets/ # 静态资源
└── App.vue # 根组件
|
组件开发实践
单文件组件
Vue的单文件组件(.vue)把模板、脚本、样式写在一个文件里,很方便。
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
32
33
34
35
36
37
38
39
| <template>
<div class="user-card">
<img :src="user.avatar" :alt="user.name">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
<button @click="sendMessage">发消息</button>
</div>
</template>
<script>
export default {
name: 'UserCard',
props: {
user: {
type: Object,
required: true
}
},
methods: {
sendMessage() {
this.$emit('send-message', this.user.id)
}
}
}
</script>
<style scoped>
.user-card {
border: 1px solid #ddd;
padding: 20px;
border-radius: 4px;
}
.user-card img {
width: 60px;
height: 60px;
border-radius: 50%;
}
</style>
|
组件通信
父子组件通信
1
2
3
4
5
| // 父组件传数据给子组件 - props
<user-card :user="currentUser"></user-card>
// 子组件传数据给父组件 - $emit
this.$emit('user-selected', userId)
|
兄弟组件通信
1
2
3
4
5
6
7
8
9
10
11
| // 使用事件总线
// main.js
Vue.prototype.$bus = new Vue()
// 组件A发送事件
this.$bus.$emit('data-changed', data)
// 组件B监听事件
this.$bus.$on('data-changed', (data) => {
// 处理数据
})
|
生命周期钩子
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
| export default {
data() {
return {
users: []
}
},
created() {
// 组件创建后,DOM还没渲染
console.log('组件创建了')
},
mounted() {
// DOM渲染完成
this.loadUsers()
},
beforeDestroy() {
// 组件销毁前清理工作
this.$bus.$off('data-changed')
},
methods: {
loadUsers() {
// 加载用户数据
this.$http.get('/api/users').then(response => {
this.users = response.data
})
}
}
}
|
状态管理 - Vuex
项目复杂了就需要状态管理,Vuex是Vue官方的状态管理库。
基本概念
- State - 存储数据的地方
- Getters - 从state派生出来的数据
- Mutations - 修改state的唯一方式
- Actions - 异步操作,提交mutations
简单示例
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
32
33
34
35
36
37
| // store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
users: [],
loading: false
},
getters: {
userCount: state => state.users.length
},
mutations: {
SET_USERS(state, users) {
state.users = users
},
SET_LOADING(state, loading) {
state.loading = loading
}
},
actions: {
async loadUsers({ commit }) {
commit('SET_LOADING', true)
try {
const response = await this.$http.get('/api/users')
commit('SET_USERS', response.data)
} finally {
commit('SET_LOADING', false)
}
}
}
})
|
在组件中使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| import { mapState, mapActions } from 'vuex'
export default {
computed: {
...mapState(['users', 'loading'])
},
methods: {
...mapActions(['loadUsers'])
},
mounted() {
this.loadUsers()
}
}
|
路由管理 - Vue Router
单页应用需要前端路由,Vue Router是官方路由库。
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
| // router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/views/Home'
import UserList from '@/views/UserList'
import UserDetail from '@/views/UserDetail'
Vue.use(Router)
export default new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/users',
name: 'UserList',
component: UserList
},
{
path: '/users/:id',
name: 'UserDetail',
component: UserDetail,
props: true
}
]
})
|
路由跳转
1
2
3
4
5
6
7
8
9
| // 编程式导航
this.$router.push('/users')
this.$router.push({ name: 'UserDetail', params: { id: 123 } })
// 模板中使用
<router-link to="/users">用户列表</router-link>
<router-link :to="{ name: 'UserDetail', params: { id: user.id } }">
{{ user.name }}
</router-link>
|
HTTP请求处理
使用axios
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
| // main.js
import axios from 'axios'
// 配置基础URL
axios.defaults.baseURL = 'http://localhost:3000/api'
// 请求拦截器
axios.interceptors.request.use(config => {
// 添加token
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// 响应拦截器
axios.interceptors.response.use(
response => response,
error => {
if (error.response.status === 401) {
// 跳转到登录页
this.$router.push('/login')
}
return Promise.reject(error)
}
)
Vue.prototype.$http = axios
|
在组件中使用
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
32
| export default {
data() {
return {
users: [],
loading: false
}
},
methods: {
async loadUsers() {
this.loading = true
try {
const response = await this.$http.get('/users')
this.users = response.data
} catch (error) {
console.error('加载用户失败:', error)
} finally {
this.loading = false
}
},
async createUser(userData) {
try {
await this.$http.post('/users', userData)
this.$message.success('创建成功')
this.loadUsers()
} catch (error) {
this.$message.error('创建失败')
}
}
}
}
|
常用UI组件库
Element UI
1
2
3
4
5
| // main.js
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)
|
使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| <template>
<div>
<el-table :data="users" v-loading="loading">
<el-table-column prop="name" label="姓名"></el-table-column>
<el-table-column prop="email" label="邮箱"></el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button size="mini" @click="editUser(scope.row)">编辑</el-button>
<el-button size="mini" type="danger" @click="deleteUser(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
@current-change="handlePageChange"
:current-page="currentPage"
:page-size="pageSize"
:total="total">
</el-pagination>
</div>
</template>
|
开发技巧
计算属性 vs 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| export default {
data() {
return {
users: []
}
},
computed: {
// 计算属性有缓存,依赖不变就不会重新计算
activeUsers() {
return this.users.filter(user => user.active)
}
},
methods: {
// 方法每次调用都会执行
getActiveUsers() {
return this.users.filter(user => user.active)
}
}
}
|
侦听器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| export default {
data() {
return {
searchText: '',
users: []
}
},
watch: {
// 简单侦听
searchText(newVal, oldVal) {
this.searchUsers(newVal)
},
// 深度侦听对象
user: {
handler(newVal, oldVal) {
this.saveUser(newVal)
},
deep: true
}
}
}
|
自定义指令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // 全局指令
Vue.directive('focus', {
inserted: function (el) {
el.focus()
}
})
// 局部指令
export default {
directives: {
focus: {
inserted: function (el) {
el.focus()
}
}
}
}
// 使用
<input v-focus>
|
性能优化
懒加载路由
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| const UserList = () => import('@/views/UserList')
const UserDetail = () => import('@/views/UserDetail')
export default new Router({
routes: [
{
path: '/users',
component: UserList
},
{
path: '/users/:id',
component: UserDetail
}
]
})
|
使用key优化列表渲染
1
2
3
4
5
6
7
8
9
10
11
12
13
| <template>
<div>
<!-- 好的做法 -->
<div v-for="user in users" :key="user.id">
{{ user.name }}
</div>
<!-- 不好的做法 -->
<div v-for="(user, index) in users" :key="index">
{{ user.name }}
</div>
</div>
</template>
|
避免不必要的重新渲染
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| export default {
data() {
return {
expensiveData: null
}
},
computed: {
// 使用计算属性缓存复杂计算
processedData() {
if (!this.expensiveData) return []
return this.expensiveData.map(item => {
// 复杂的数据处理
return processItem(item)
})
}
}
}
|
常见问题
数组更新检测
1
2
3
4
5
6
7
| // Vue不能检测这些数组变化
this.users[0] = newUser // 不会触发更新
this.users.length = 0 // 不会触发更新
// 正确的做法
this.$set(this.users, 0, newUser)
this.users.splice(0, this.users.length)
|
对象属性添加
1
2
3
4
5
6
7
| // 不会触发更新
this.user.newProperty = 'value'
// 正确的做法
this.$set(this.user, 'newProperty', 'value')
// 或者
this.user = Object.assign({}, this.user, { newProperty: 'value' })
|
异步组件加载失败
1
2
3
4
5
6
7
| const AsyncComponent = () => ({
component: import('./AsyncComponent.vue'),
loading: LoadingComponent,
error: ErrorComponent,
delay: 200,
timeout: 3000
})
|
总结
Vue.js确实是个不错的框架,学习成本低,文档清楚,生态也比较完善。这段时间用下来感觉开发效率提升不少。
几个要点:
- 组件化思维很重要,把页面拆分成可复用的组件
- 合理使用Vuex管理状态,不要什么都往里面放
- 路由懒加载能提升首屏加载速度
- 多用计算属性,少用方法
- 注意数组和对象的更新检测问题
目前Vue 2.x版本已经比较稳定了,可以放心在项目中使用。后面准备研究一下服务端渲染(SSR),听说对SEO比较友好。
参考资料