组合式API
前面的 Vue 章节,是基础,也是对 Vue 2 光荣的回顾。
本章作为教程的尾声,将介绍 Vue 3 最强大的新功能之一:组合式 API 。
什么是组合式API
强烈建议大家先去读读官方文档 ,把组合式 API 的涵义和作用讲得非常清楚。总结成几句话就是:
话不多说,接下来让我们将文章列表页面 ArticleList.vue 的选项式API进化为组合式API ,用例子感受吧。
起步
本章的所有修改只涉及到 ArticleList.vue 的 Javascript 脚本部分。
先把旧代码先贴出来:
import axios from 'axios';
export default {
name: 'ArticleList',
data: function () {
return {
info: ''
}
},
mounted() {
this.get_article_data()
},
methods: {
imageIfExists(article) {
if (article.avatar) {
return article.avatar.content
}
},
gridStyle(article) {
if (article.avatar) {
return {
display: 'grid',
gridTemplateColumns: '1fr 4fr'
}
}
},
formatted_time: function (iso_date_string) {
const date = new Date(iso_date_string);
return date.toLocaleDateString()
},
is_page_exists(direction) {
if (direction === 'next') {
return this.info.next !== null
}
return this.info.previous !== null
},
get_page_param: function (direction) {
try {
let url_string;
switch (direction) {
case 'next':
url_string = this.info.next;
break;
case 'previous':
url_string = this.info.previous;
break;
default:
return this.$route.query.page
}
const url = new URL(url_string);
return url.searchParams.get('page')
}
catch (err) {
return
}
},
get_path: function (direction) {
let url = '';
try {
switch (direction) {
case 'next':
if (this.info.next !== undefined) {
url += (new URL(this.info.next)).search
}
break;
case 'previous':
if (this.info.previous !== undefined) {
url += (new URL(this.info.previous)).search
}
break;
}
}
catch {
return url
}
return url
},
get_article_data: function () {
let url = '/api/article';
let params = new URLSearchParams();
params.appendIfExists('page', this.$route.query.page);
params.appendIfExists('search', this.$route.query.search);
const paramsString = params.toString();
if (paramsString.charAt(0) !== '') {
url += '/?' + paramsString
}
axios
.get(url)
.then(response => (this.info = response.data))
}
},
watch: {
$route() {
this.get_article_data()
}
}
}
一大坨代码扑面而来,已经有点看不清了对吧。
下面开始魔改。
要使用组合式 API,首先要有个入口,也就是 Vue 3 的 setup() 函数:
export default {
// 组合式 APi 入口
setup() {
return {}
},
// 其他代码
...
}
这就是一个最简单了 setup() 了。
注意: Vue 执行 setup() 的时机非常早,此时 Vue 的实例都尚未生成,因此在 setup 中没有 this 。这意味着除了 props 之外,你将无法访问组件中的任何属性:比如数据、 计算属性或方法。
现在我们把本地数据 info 移动到 setup() 里,像下面这样做:
import { ref } from 'vue'
export default {
setup() {
const info = ref('');
return {
info,
}
},
// 旧代码的状态数据,注释掉
// data: function () {
// return {
// info: ''
// }
// },
}
刷新下页面,功能无任何变化。
获取数据
只把状态数据的位置挪动一下没什么意思,下面试试把获取数据的 get_article_data() 方法也改为组合式 API。
改动部分如下:
import { ref } from 'vue'
import { useRoute } from 'vue-router'
export default {
setup() {
const info = ref('');
// 创建路由
const route = useRoute();
// 获取文章列表数据的方法
const get_article_data = function () {
let url = '/api/article';
let params = new URLSearchParams();
params.appendIfExists('page', route.query.page);
params.appendIfExists('search', route.query.search);
const paramsString = params.toString();
if (paramsString.charAt(0) !== '') {
url += '/?' + paramsString
}
axios
.get(url)
.then(response => (info.value = response.data))
};
return {
info,
get_article_data
}
},
methods: {
// 把对应的方法注释掉
// get_article_data: function () {
// let url = '/api/article';
//
// let params = new URLSearchParams();
// params.appendIfExists('page', this.$route.query.page);
// params.appendIfExists('search', this.$route.query.search);
//
// const paramsString = params.toString();
// if (paramsString.charAt(0) !== '') {
// url += '/?' + paramsString
// }
//
// axios
// .get(url)
// .then(response => (this.info = response.data))
// }
},
}
看起来只是把方法挪了个地方而已。
但里面有一些很重要的区别:
Vue 实例中用到 get_article_data() 有两个地方,分别是 mounted() 和 watch ,我们把它两兄弟也搬到 setup() 中:
import { ref, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'
export default {
setup() {
const info = ref('');
const route = useRoute();
const get_article_data = function () {...};
onMounted(get_article_data);
watch(route, get_article_data);
return {
info
}
},
// mounted() {
// this.get_article_data()
// },
// watch: {
// $route() {
// this.get_article_data()
// }
// }
}
setup 中同样不能直接访问生命周期方法和监听方法,因此 Vue 3 提供了 onMounted、 watch 作为对应的替代。
可复用模块
到目前为止都没什么特别的,代码量似乎还更多了。接下来我们试试将与获取数据相关的功能抽离成独立的 JS 模块。
新建 frontend/src/composables/getArticleData.js 文件,将上面的 get_article_data() 函数挪进来:(注意有改动)
// frontend/src/composables/getArticleData.js
import axios from 'axios';
import {onMounted, watch} from 'vue'
export default function getArticleData(info, route) {
const getData = async () => {
let url = '/api/article';
let params = new URLSearchParams();
params.appendIfExists('page', route.query.page);
params.appendIfExists('search', route.query.search);
const paramsString = params.toString();
if (paramsString.charAt(0) !== '') {
url += '/?' + paramsString
}
const response = await axios.get(url);
info.value = response.data;
};
onMounted(getData);
watch(route, getData);
}
注意这里有些重要的改动:
函数通过参数将响应式对象 info、 route 传递进来,以便更新其中所包含的值。由此可见, ref 创建的是一个响应式引用 。你可以在整个程序中安全地传递它,而不必担心在某个地方失去它的响应性。
onMounted()、 watch() 方法都可以抽离到函数模块中,这极大方便了将关注点聚集的能力。
将此函数标记为需要等待返回值的异步函数(async/await),确保在获取到数据前不会执行后面的操作数据的逻辑。(从而导致报错)
接着再来修改 setup():
// frontend/src/components/ArticleList.vue
// 注释掉 axios
// import axios from 'axios';
import {ref} from 'vue'
import {useRoute} from 'vue-router'
import getArticleData from '@/composables/getArticleData.js'
export default {
setup() {
const info = ref('');
const route = useRoute();
getArticleData(info, route);
return {
info
}
},
...
}
刷新页面,功能正常无变化。
翻页模块
现在我们已经将获取数据功能抽离为一个独立模块了。
另一块关注点较为集中的逻辑就是is_page_exists()、 get_page_param() 和 get_path() 三个方法了,其作用都与翻页相关。让我们试着把这三兄弟也抽离出来。
新建 frontend/src/composables/pagination.js 文件,把这三个方法挪进来(有改动),并增加一个导出用的接口函数:
// frontend/src/composables/pagination.js
// 导出三个方法闭包的接口函数
export default function pagination(info, route) {
const is_page_exists = (direction) => {
return isPageExists(info, direction)
};
const get_page_param = (direction) => {
return getPageParam(info, route, direction)
};
const get_path = (direction) => {
return getPath(info, direction)
};
return {
is_page_exists,
get_page_param,
get_path,
}
}
// 判断 下一页/上一页 是否存在
function isPageExists(info, direction) {
if (direction === 'next') {
return info.value.next !== null
}
return info.value.previous !== null
}
// 获取页码
function getPageParam(info, route, direction) {
try {
let url_string;
switch (direction) {
case 'next':
url_string = info.value.next;
break;
case 'previous':
url_string = info.value.previous;
break;
default:
return route.query.page
}
const url = new URL(url_string);
return url.searchParams.get('page')
}
catch (err) {
return
}
}
// 获取下一页路径
function getPath(info, direction) {
let url = '';
try {
switch (direction) {
case 'next':
if (info.value.next !== undefined) {
url += (new URL(info.value.next)).search
}
break;
case 'previous':
if (info.value.previous !== undefined) {
url += (new URL(info.value.previous)).search
}
break;
}
}
catch {
return url
}
return url
}
三个功能函数都没什么好说的,就还是把与 this 相关的部分做了些许处理。接口函数 pagination() 用闭包将 info、 route 两个参数捕获,并随着函数实际调用时传入的 direction 参数传递到函数体内部,并返回对应的值。
接着修改 ArticleList.vue:
// frontend/src/components/ArticleList.vue
...
import pagination from '@/composables/pagination.js'
export default {
setup() {
const info = ref('');
const route = useRoute();
getArticleData(info, route);
const {
is_page_exists,
get_page_param,
get_path
} = pagination(info, route);
return {
info,
is_page_exists,
get_page_param,
get_path,
}
},
methods: {
// 这些东西全部都注释掉了
// is_page_exists(direction) {...}
// get_page_param: function (direction) {...},
// get_path: function (direction) {...},
// get_article_data: function () {...}
// 下面是其他方法
...
},
}
刷新页面,功能还是应该无变化。
收尾工作
现在大部分的逻辑都挪动到 setup() 中了,只剩几个调整页面外观的方法仍在 methods 中。由于其实现细节不是本文重点,详细过程就略过了,请读者自行尝试。
来看看 ArticleList.vue 脚本部分最终的全貌:
import {ref} from 'vue'
import {useRoute} from 'vue-router'
import getArticleData from '@/composables/getArticleData.js'
import pagination from '@/composables/pagination.js'
import articleGrid from '@/composables/articleGrid.js'
import formattedTime from '@/composables/formattedTime.js'
export default {
name: 'ArticleList',
setup() {
const info = ref('');
const route = useRoute();
// 获取文章数据
getArticleData(info, route);
// 翻页
const {
is_page_exists,
get_page_param,
get_path
} = pagination(info, route);
// 调整页面外观
const {
imageIfExists,
gridStyle
} = articleGrid();
// 日期格式化
const formatted_time = formattedTime;
// 需要注入到 Vue 实例的数据、方法等
return {
info,
is_page_exists,
get_page_param,
get_path,
imageIfExists,
gridStyle,
formatted_time,
}
},
}
重构完成了,感觉如何?
教程只讲了 methods()、 onMounted() 和 watch 的重构,而实际上 computed() 等其他部分都是可以改写为组合式 API 的。因此再一次建议阅读 官方文档 ,里面有你入门所需要的绝大部分内容。
一句话,既然你都用 Vue 3 了,那就多用组合式 API,少用选项式 API ,这是历史洪流。
Last modified: 06 January 2025