前端学习笔记 3:Vue 工程
上一篇文章介绍了如何在单一 Html 页面中使用 Vue,本文介绍如何从头开始用 Vue 构建一个前端工程项目。
环境准备
Vue 框架代码的创建依赖于 Node.js,因此需要先安装 Node.js。
创建和启动
创建
通过以下命令可以创建 Vue 的框架代码:
npm create vue@latest
- 该命令执行后会先检查是否安装 create-vue 工具,如果没有,就安装。然后再使用 create-vue 创建框架代码。
- npm(Node Package Manager)是 NodeJS 的包管理工具,类似于 Linux 的 RPM 或 YUM。
在执行过程中会询问是否启用一些功能模块:
Vue.js - The Progressive JavaScript Framework√ 请输入项目名称: ... vue-project
√ 是否使用 TypeScript 语法? ... 否 / 是
√ 是否启用 JSX 支持? ... 否 / 是
√ 是否引入 Vue Router 进行单页面应用开发? ... 否 / 是
√ 是否引入 Pinia 用于状态管理? ... 否 / 是
√ 是否引入 Vitest 用于单元测试? ... 否 / 是
√ 是否要引入一款端到端(End to End)测试工具? » 不需要
√ 是否引入 ESLint 用于代码质量检测? ... 否 / 是
默认是否
。
安装好框架代码后,还需要进入工程目录安装相关依赖:
cd .\vue-project\
npm install
启动
执行npm run dev
命令可以启动 Vue 项目:
VITE v5.0.10 ready in 2711 ms➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h + enter to show help
就像控制台提示信息中显示的,运行 Vue 项目的 Nodejs 服务端地址默认是 http://localhost:5173/,前往该地址就能看到一个默认的欢迎页面。
除了命令行启动外,还可以用 VSCode 启动:
如果界面上没有 NPM 脚本 这一栏,可以通过这里开启:
快速开始
下面简单分析一下工程默认的欢迎页的显示逻辑。
实际上浏览器访问时加载的是index.html
文件:
该文件通过<script type="module" src="/src/main.js"></script>
这行代码以模块化的方式加载了 JS 文件/src/main.js
:
在main.js
中,通过import { createApp } from 'vue'
导入了 Vue 的createApp
函数,与之前不同的是,因为已经用 npm 工具安装了本地依赖(npm install
),所以这里是通过本地导入,而非从在线的 JS 文件导入 Vue 函数。
npm 安装的本地依赖模块位于
node_modules
目录下。
这里最重要的是import App from './App.vue'
,从App.vue
文件导入了一个 App 对象,之后的代码createApp(App).mount('#app')
使用该对象作为参数创建了 Vue 实例。
App.vue
文件包含三部分:
事实上,在开发基于 Vue 的前端项目时,我们主要工作是创建和修改 Vue 文件。
作为示例,这里可以创建一个Hello.vue
:
<template><h1>Hello World!</h1>
</template>
<style>
h1 {color: aqua;
}
</style>
要在页面中显示,还需要在main.js
中替换导入代码:
import App from './Hello.vue'
在 Vue 文件中同样可以像前文那样为 Vue 实例提供数据和方法:
<script>
export default {data() {return {msg: 'Hello World!'}}
}
</script>
<template><h1>{{ msg }}</h1>
</template>
这里通过export default {...}
指定了Hello.vue
文件默认导出的对象内容,该对象会在main.js
中导入为App
对象,也就是用于创建 Vue 实例的参数对象,因此我们可以在export default {...}
定义的默认导出对象中定义data
或methods
等 Vue 需要的方法或属性。
除了上面的方式外,还可以借助ref
函数定义数据或方法:
<script setup>
import {ref} from 'vue'
const msg = ref('Hello World!')
</script>
注意,这里的
script
有setup
属性。
API 风格
Vue 的 API 有两种风格,这里分别用两种风格编写同样的功能页面进行说明。
选项式
定义一个 Count.vue
文件:
<script>
export default {data() { //定义响应式数据return {num: 0}},methods: { // 定义 Vue 方法count() {this.num++;}},mounted(){ // 定义钩子函数console.log("Vue 实例已加载...")}
}
</script>
<template><button @click="count">count:{{ num }}</button>
</template>
要让该文件生效,还要在Hello.vue
中导入:
<script setup>
import {ref} from 'vue'
const msg = ref('Hello World!')
import Count from './Count.vue'
</script>
<template><h1>{{ msg }}</h1><br/><Count/>
</template>
注意
>Count/>
标签,该标签的位置代表Count.vue
文件中的模版插入的位置。
选项式的优点在于结构简单,便于理解,缺点是代码结构过于死板,不够灵活。
组合式
<script setup>
import { ref, onMounted } from 'vue'
// 定义响应式数据
const num = ref(0)
// 定义 Vue 方法
function count() {num.value++;
}
// 定义钩子函数
onMounted(() => {console.log("Vue 实例已加载...")
})
</script>
<template><button @click="count">count:{{ num }}</button>
</template>
在组合式 API 中,需要用ref
函数定义响应式数据,用特定的函数(比如 onMounted
)定义 Vue 生命周期的钩子方法。特别需要注意的是,组合式 API 中,ref
定义的响应式数据有一个value
属性,表示响应式数据的值,因此这里在count
函数中,自增使用的是num.value++
而非num++
。
案例
下面是一个简单案例,用 Vue 工程的方式创建一个ArticleList.vue
,用于展示文章列表和搜索框。
在编写这个 Vue 文件之前,需要先在本地安装 axios
的依赖:
npm install axios
下面是ArticleList.vue
的完整内容:
<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'
// 创建表格对应的响应式数据
const articles = ref([])
// Vue 实例初始化后加载表格数据
onMounted(() => {axios.get("http://localhost:8080/article/getAll").then(result => {articles.value = result.data}).catch(error => {console.log(error)})
})
// 创建搜索条件对应的响应式数据
const conditions = ref({category: '',state: ''
})
// 定义搜索绑定事件
const search = () => {axios.get("http://localhost:8080/article/search", { params: {...conditions.value} }).then(result => {articles.value = result.data}).catch(error => {console.log(error)})
}
</script>
<template><div>文章分类:<input type="text" v-model="conditions.category"/>发布状态:<input type="text" v-model="conditions.state"/><button @click="search">搜索</button><br /><br /><table border="1"><tr><td>文章标题</td><td>分类</td><td>发表时间</td><td>状态</td><td>操作</td></tr><tr v-for="article in articles"><td>{{ article.title }}</td><td>{{ article.category }}</td><td>{{ article.time }}</td><td>{{ article.state }}</td><td><button>编辑</button><button>删除</button></td></tr></table></div>
</template>
注意,这里search
函数内调用axios.get
方法传参时使用了 ES6 的解构赋值:
{ params: {...conditions.value} }
这样可以让代码更简洁。
当然你也可以使用传统匿名函数分别给属性赋值的方式。
封装函数
上面的案例有一个缺陷——通过 Axios
调用接口获取数据的部分没有单独封装成函数,这样不利于其它部分的代码进行复用。更好的做法是将这些会被复用的逻辑抽取成单独的函数保存在单独的 JS 文件中,在需要使用的地方导入所需的函数并进行调用。
首先在src
目录下创建一个/api/article.js
文件:
import axios from 'axios'export async function getAllArticlesService(){return await axios.get("http://localhost:8080/article/getAll").then(result => {return result.data}).catch(error => {console.log(error)})
}
对 ArticleList.vue
进行重构:
import { getAllArticlesService } from '@/api/article.js'
// ...
// Vue 实例初始化后加载表格数据
onMounted(async () => {articles.value = await getAllArticlesService()
})
需要注意的是,这里使用了await
与async
关键字,这是因为抽取后的函数getAllArticlesService
中的axios.get
本质上是异步调用,因此没办法同步地获取其返回值,所以需要在调用时添加await
关键字将其变成同步调用,而此时进行调用的函数(getAllArticlesService
)本身变成了异步,所以要添加async
关键字。同理,在调用onMounted
钩子方法时,同样需要给作为参数的匿名函数加上async
,并且在其中的getAllArticlesService
调用加上await
。
- 如果不使用
await
和async
,就不会有任何数据加载。因为异步调用的关系,响应式数据的赋值语句实际上还没有等到异步调用执行并返回就已经被主线程执行完毕,所以不会有任何实际数据被赋值。- 在导入时,如果导入的是本地
src
目录下的资源,可以使用@/
代表src
目录。
搜索文章相关调用同样可以进行封装,这里不再赘述。
axios 实例
上面的案例还存在一个瑕疵,单个 JS 文件中的多次 Axios 调用实际上使用的是相同的服务端域名(HOST),只是具体的接口路径不同。针对这个问题,可以使用 Axios 实例进行简化和统一设置:
import axios from 'axios'
const instance = axios.create({baseURL: 'http://localhost:8080'
});export async function getAllArticlesService() {return await instance.get("/article/getAll").then(result => {return result.data}).catch(error => {console.log(error)})
}
axios 拦截器
使用axios
进行异步请求时,往往需要对响应结果进行相似的处理,比如:
instance.get("/article/getAll").then(result => {return result.data}).catch(error => {console.log(error)})
对此,可以创建一个单独的 axios 实例进行复用,并且在这个实例上定义请求/响应拦截器对请求或响应进行统一处理。
添加/util/request.js
import axios from 'axios'
const instance = axios.create({baseURL: 'http://localhost:8080'
});
instance.interceptors.response.use(result => {return result.data},error => {console.log(error)return Promise.reject(error);}
)
export default instance
interceptors.response.use
用于设置响应拦截器,接收两个参数,分别为调用成功(HTTP 状态码 2XX)和调用失败(HTTP 状态码不是 2XX)时的回调函数。
在article.js
中导入:
import request from '@/util/request.js'export async function getAllArticlesService() {return await request.get("/article/getAll")
}export async function searchService(conditions) {return await request.get("/article/search", { params: conditions })
}
因为实例request
设置了响应拦截器对结果进行统一处理,所以这里不需要再使用.then
或.catch
进行处理。
实际上内层的await
和async
关键字是可以省略的,只要最外层调用有即可:
import request from '@/util/request.js'export function getAllArticlesService() {return request.get("/article/getAll")
}export function searchService(conditions) {return request.get("/article/search", { params: conditions })
}
谢谢阅读,本文的完整示例代码见这里。
参考资料
- 1.1 ES6 教程 | 菜鸟教程 (runoob.com)
- 拦截器 | Axios中文文档 | Axios中文网 (axios-http.cn)