Vue工程化开发

基于Vue官方提供的一个脚手架Vue-cli,生成一个工程化的 Vue项目模板,学习Vue工程化、规范化的开发模式

1.项目结构

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
├── build ----------------------------------------------- webpack相关配置文件
│ ├── build.js ---------------- webpack打包配置文件
│ ├── check-versions.js ------- 检查npm,nodejs版本
│ ├── dev-client.js ----------- 设置环境
│ ├── dev-server.js ----------- 创建express服务器,配置中间件,启动可热重载的服务器
│ ├── utils.js ---------------- 配置资源路径,配置css加载器
│ ├── vue-loader.conf.js ------ 配置css加载器等
│ ├── webpack.base.conf.js ---- webpack基本配置
│ ├── webpack.dev.conf.js ----- 用于开发的webpack设置
│ ├── webpack.prod.conf.js ---- 用于打包的webpack设置
├── config ----------------------------------------------- 配置文件
├── node_modules ----------------------------------------- 存放依赖的目录
├── src -------------------------------------------------- 源码
│ ├── api --------------------------------- 自己封装的api接口
│ ├── assets ------------------------------ 静态文件
│ ├── components -------------------------- 组件
│ ├── main.js ----------------------------- 主js
│ ├── App.vue ----------------------------- 项目入口组件
│ ├── router ------------------------------ 路由
│ ├── store ------------------------------- Vuex插件:仓库模块
│ ├── utils ------------------------------- 自己封装的工具方法模块
├── package.json ----------------------------------------- node配置文件
├── .babelrc---------------------------------------------- babel配置文件
├── .editorconfig----------------------------------------- 编辑器配置
├── .gitignore-------------------------------------------- 配置git可忽略的文件

2.主要文件

2.1 index.html 主页

index.html如其他html一样,但一般只定义一个空的根节点,在main.js里面定义的实例将挂载在根节点下,内容都通过vue组件来填充

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>vuedemo</title>
</head>
<body>
<!-- 定义的vue实例将挂载在#app节点下 -->
<div id="app"></div>
</body>
</html>

2.2 main.js 入口文件

main.js主要是引入vue框架,根组件及路由设置,并且定义vue实例,下面的 components:{App}就是引入的根组件App.vue

后期还可以引入插件,当然首先得安装插件。

Vue2写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*引入vue框架*/
import Vue from 'vue'
/*引入根组件*/
import App from './App'
/*引入路由设置*/
import router from './router'

/*定义Vue实例*/
new Vue({
el:'#app',
//向 Vue 实例导入了一个 router 实例
router,
//基于App创建元素结构
render: h => h(App)
}).$mount('#app') //将Vue实例挂载到index.html中id为app的div中
//.$mount('#app') 相当于 el:'#app',两者取一即可

Vue3写法:

1
2
3
4
5
6
7
8
9
10
11
/*引入vue框架*/
import { createApp } from 'vue'
/*引入根组件*/
import App from './App.vue'
/*引入路由设置*/
import router from './router'

/*定义Vue实例*/
const app = createApp(App)
app.use(router)
app.mount('#app') //将Vue实例挂载到index.html中id为app的div中

2.3 App.vue 根组件

在组件化的开发模式中,App.vue作为根组件,可以在这里写入其他组件

App.vue是整个应用程序的入口点,它负责管理路由、状态和其他全局资源

三部分组成:

  1. template: 结构 (有且只能一个根元素)
  2. script: js逻辑
  3. style: 样式(可支持less,需要装包)

2.4 package.json 配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"name": "demo", //项目名称,必需字段
"version": "0.1.0",//项目版本,必需字段
"private": true,
"scripts": { //可执行的脚本命令
"serve": "vue-cli-service serve", //启动项目
"build": "vue-cli-service build" //打包
},
"dependencies": { //npm包声明-生产环境中所必需的包
"core-js": "^3.8.3",
"register-service-worker": "^1.7.2",
"vue": "^3.2.13",
"vue-router": "^4.0.3"
},
"devDependencies": {//npm包声明-开发阶段需要的包
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-pwa": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-service": "~5.0.0"
}
}

3.router模块

router使用方法:

yarn add vue-router@3

yarn add vue-router@4

  1. 下载安装vue-router,vue2用vue-router3.x,vue3用vue-router4.x
  2. 引入vue-router
  3. vue-router注册
  4. 创建router对象
  5. 注入router对象
  6. views目录中存放页面组件
  7. index.js中管理路由路径
  8. App.vue中使用<router-view></router-view>

Vue2

index.js

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
import Vue from "vue"
//引入vue-router
import VueRouter from 'vue-router'
//引入页面组件
import ViewOne from "@/views/ViewOne.vue"
import ViewTwo from "@/views/ViewTwo.vue"
import ViewThree from "@/views/ViewThree.vue"

//注册
Vue.use(VueRouter)
//创建router对象
const router = new VueRouter({
//管理路由
routes:[
{path:'/',redirect:'/one'},
{path:'/one',component:ViewOne},
{path:'/two',component:ViewTwo},
//路由传参
{path:'/three/:id',component:ViewThree},
{path:'*',component:NotFind}
]
});

//导出router对象
export default router

mian.js

1
2
3
4
5
6
7
8
9
10
11
12
import Vue from 'vue'
import App from './App.vue'
//引入router目录中的router对象
import router from "@/router"

Vue.config.productionTip = false

new Vue({
render: h => h(App),
//注入router对象到Vue实例中
router
}).$mount('#app')

Vue3

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//引入 createRouter, createWebHistory
import { createRouter, createWebHistory } from 'vue-router'
//引入页面组件
import ViewOne from "@/views/ViewOne.vue"
import ViewTwo from "@/views/ViewTwo.vue"
import ViewThree from "@/views/ViewThree.vue"
//定义路由规则
const routes = [
{path:'/',redirect:'/one'},
{path:'/one',component:ViewOne},
{path:'/two',component:ViewTwo},
{path:'/three',component:ViewThree},
{path:'*',component:NotFind}
]
//创建router对象
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
//导出router对象
export default router

main.js

1
2
3
4
5
6
7
8
9
import { createApp } from 'vue'
import App from './App.vue'
//引入router目录中的router对象
import router from "@/router"

const app = createApp(App);
//注入router对象
app.use(router);
app.mount('#app');

App.vue

1
2
3
4
5
6
7
8
<template>
<div id="app">
<router-link to="/one">1 </router-link>
<router-link to="/two"> 2 </router-link>
<router-link to="/three"> 3</router-link>
<router-view></router-view>
</div>
</template>

路由导航守卫

将下面内容写入index.js 。这里是Vue2的写法,具体参考官网:导航守卫 | Vue Router (vuejs.org)

1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义需要权限的页面
const authorUrls = ['/two','/three']
// 路由守卫
router.beforeEach((to, from, next) => {
if (authorUrls.includes(to.path) && !localStorage.getItem('token')) {
// 如果访问的页面需要登录权限,并且没有登录,则跳转到登录页面
alert('需要登录才能访问')
next({ path: '/login' })
} else {
// 无需登录权限的页面直接放行
next()
}
})

Vue3的写法有所不同:导航守卫 | Vue Router (vuejs.org)

4.store模块(Vuex)

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import Vue from 'vue'
//引入vuex插件
import Vuex from 'vuex'
//注册安装
Vue.use(Vuex)
//创建仓库
const store = new Vuex.Store({
// state 状态,即数据,类似于vue组件中的data
// 区别:
// 1.data 是组件自己的数据
// 2.state 是所有组件共享的数据
state: {
count: 114514,
title: 'abcd'
}
})
//导出仓库对象
export default store

main.js

1
2
3
4
5
6
7
8
9
10
import Vue from 'vue'
import App from './App.vue'
//引入上面导出的仓库
import store from '@/store'

new Vue({
//注入到Vue对象中
store,
render: h => h(App)
}).$mount('#app')

数据引用-state

①通过store直接访问

1
2
3
4
5
6
7
<template>
<div>
{{ $store.state.count }}
-
{{ $store.state.title }}
</div>
</template>

②通过辅助函数mapState(简化)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div>
{{ count }} - {{ title }}
</div>
</template>

<script>
import { mapState } from 'vuex'
export default {
computed: {
...mapState(['count', 'title'])
}
}
</script>

修改数据-mutations

Vuex同样遵循单向数据流,组件中不能直接修改仓库中的数据

this.$store.state.count++

像这样的写法虽然能运行,但不符合规范,在组件中如果需要修改仓库中的数据,应该通过mutations来实现

使用步骤如下:

①mutations中定义方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const store = new Vuex.Store({
state: {
count: 1,
title: 'abcd'
},
mutations: {
//定义两个方法
addCount (state, n) {
state.count += n
},
updateTitle (state, newTitle) {
state.title = newTitle
}
}
})

②通过this.$store.commit( ‘方法名’,参数 )调用

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
<template>
<div>
{{ $store.state.count }} - {{ $store.state.title }}
<button @click="handleAdd(1)">Count+1</button>
<button @click="handleAdd(2)">Count+2</button>
<button @click="handleAdd(3)">Count+3</button>
<input v-model="newTitle">
<button @click="handleTitle(newTitle)">updateTitle</button>
</div>
</template>

<script>

export default {
data () {
return {
newTitle: ''
}
},
methods: {
//调用mutations中的方法
handleAdd (n) {
this.$store.commit('addCount', n)
},
handleTitle (newTitle) {
this.$store.commit('updateTitle', newTitle)
}
}
}
</script>

简化②-辅助函数mapMutations

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
<template>
<div>
{{ $store.state.count }} - {{ $store.state.title }}
<button @click="addCount(1)">Count+1</button>
<button @click="addCount(2)">Count+2</button>
<button @click="addCount(3)">Count+3</button>
<input v-model="newTitle">
<button @click="updateTitle(newTitle)">updateTitle</button>
</div>
</template>

<script>
//引入mapMutations
import { mapMutations } from 'vuex'

export default {
data () {
return {
newTitle: ''
}
},
//简化后只需要通过mapMutations映射方法进来,即可直接使用
methods: {
...mapMutations(['addCount', 'updateTitle'])
}
}
</script>

异步修改数据-actions

①在actions中定义异步方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
addCount (state, n) {
state.count += n
}
},
actions: {
//异步方法
addCountAsync (content, n) {
setTimeout(() => {
content.commit('addCount', n)
}, 5000)
}
}
})

②通过this.$store.dispatch( ‘方法名’,参数 )调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div>
{{ $store.state.count }}
<button @click="HandleAddCountAsync(2)">Count+2</button>
</div>
</template>

<script>
export default {
methods: {
HandleAddCountAsync (n) {
this.$store.dispatch('addCountAsync', n)
}
}
}
</script>

简化②-辅助函数mapActions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div>
{{ $store.state.count }}
<button @click="addCountAsync(2)">Count+2</button>
</div>
</template>

<script>
import { mapMutations, mapActions } from 'vuex'

export default {
//类似mutations
methods: {
...mapMutations(['addCount']),
...mapActions(['addCountAsync'])
}
}
</script>

类似计算属性-getters

①在getters中定义计算属性

1
2
3
4
5
6
7
8
9
10
11
12
const store = new Vuex.Store({
state: {
list: [1, 2, 3, 4, 5, 6, 7, 8, 9]
},
//类似于computed
getters: {
//计算属性
filterList (state) {
return state.list.filter(item => item > 5)
}
}
})

②通过调用

1
2
3
4
5
6
<template>
<div>
{{ $store.state.list }}
{{ $store.getters.filterList }}
</div>
</template>

简化②-辅助函数mapGetters

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div>
{{ $store.state.list }}
{{ $store.getters.filterList }}
</div>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
methods: {
...mapGetters(['filterList'])
}
}
</script>

分模块-modules

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割。

在模块中使用:namespaced: true, 命名空间,添加之后,当前模块下的标识符可以和其它模块相同,用于解决不同模块的命名冲突问题

划分模块后的目录结构

划分模块步骤

①建立模块

setting.js - setting模块

1
2
3
4
5
6
7
8
9
10
11
// 配置模块内容信息
export default {
namespaced: true,
state: {
theme: 'light',
desc: '描述信息'
},
mutations: {},
actions: {},
getters: {}
}

user.js - user模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 配置模块内容信息
export default {
namespaced: true,
state: {
userInfo: {
name: 'zgl',
age: 18
},
score: 100
},
mutations: {},
actions: {},
getters: {}
}

②index.js导入并挂载模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Vue from 'vue'
import Vuex from 'vuex'
// 导入模块
import user from '@/store/modules/user'
import setting from '@/store/modules/setting'

Vue.use(Vuex)

const store = new Vuex.Store({
state: {},
// 挂载模块
modules: {
user,
setting
}
})
export default store

挂载完成后即可正常使用模块中的数据

5.utils工具模块

utils工具模块是自己封装的一些工具方法模块

下面以一个名为request、封装了axios实例的模块为例:

①创建utils包,创建request.js

②在axios官网查看创建实例以及配置其他内容的方式

③封装axios,导出request实例

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
import axios from 'axios'

const instance = axios.create({
// 基地址
baseURL: 'https://api.vvhan.com/api',
// 超时时间
timeout: 5000,
// 请求头
headers: { 'Content-type': 'text/json' }
})

// 添加请求拦截器
instance.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
return config
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error)
})

// 添加响应拦截器
instance.interceptors.response.use(function (response) {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
// 只保留 response.data
return response.data
}, function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error)
})
// 导出该实例对象
export default instance

④使用request

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
<template>
<div>
{{ someWord }}
<button @click="getSomething">获取</button>
</div>
</template>

<script>
// 使用原生的axios
// import axios from 'axios'

// 使用封装的工具-request
import request from '@/utils/request'

export default {
data () {
return {
someWord: '默认'
}
},
methods: {
async getSomething () {
// 原写法:
// await axios({
// method: 'get',
// url: 'https://api.vvhan.com/api/ian'
// }).then((response) => {
// this.someWord = response.data
// })
// 现在的写法:
const res = await request.get('/ian')
this.someWord = res

}
}
}
</script>

6.api接口模块

将所有api封装为一个模块来进行管理,是一种规范化的开发方式。当项目越发复杂时,如果某个多次使用的api需要更改,那么通过一个单独的api模块来管理会减少许多不必要的修改

①创建api目录,不同功能的api放入不同的js文件

②封装api

1
2
3
4
5
6
7
8
9
10
import request from '@/utils/request'
//导出方法
export function loginByPhone (phone, code) {
return request.post('/login', {
form: {
phone: phone,
code: code
}
})
}

③调用api

1
2
3
4
5
6
7
8
9
10
11
12
<script>
import { loginByPhone } from '@/api/login'
export default {
methods: {
async login () {
//调用
const res = await loginByPhone('15012345678', '1234')
console.log(res)
}
}
}
</script>

7.打包

package.js中有一条build命令,点击运行即可完成打包,或者通过命令行:

yarn build

打包完成后会生成一个dist目录,将该目录放到服务器中即可

默认情况下,需要放到服务器根目录打开,如果希望双击运行,需要配置publicPath 配成相对路径