Featured image of post Vue.js组件开发实践 - 从入门到放弃

Vue.js组件开发实践 - 从入门到放弃

前言

最近项目里开始用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
npm install 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确实是个不错的框架,学习成本低,文档清楚,生态也比较完善。这段时间用下来感觉开发效率提升不少。

几个要点:

  1. 组件化思维很重要,把页面拆分成可复用的组件
  2. 合理使用Vuex管理状态,不要什么都往里面放
  3. 路由懒加载能提升首屏加载速度
  4. 多用计算属性,少用方法
  5. 注意数组和对象的更新检测问题

目前Vue 2.x版本已经比较稳定了,可以放心在项目中使用。后面准备研究一下服务端渲染(SSR),听说对SEO比较友好。

参考资料

使用 Hugo 构建
主题 StackJimmy 设计