Django-Vue搭建个人博客:组合式API
7410 views, 2023/10/17 updated Go to Comments
前面的 Vue 章节,是基础,也是对 Vue 2 光荣的回顾。
本章作为教程的尾声,将介绍 Vue 3 最强大的新功能之一:组合式 API 。
什么是组合式API
强烈建议大家先去读读官方文档,把组合式 API 的涵义和作用讲得非常清楚。总结成几句话就是:
- Vue 2 能够很好的胜任任何中小大型项目,但是对于超大型项目(几百个以上的组件)有天生的缺陷,最显著的矛盾是逻辑关注点分离:你可能很难短时间分清哪些方法在操作哪些数据、哪些变量又被哪些组件所更改了。
- 组合式 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: ''
// }
// },
}
- 你不能普通的如
let info = ''
这样声明状态,这不是响应式的,而只是个普通的字符串。因此用 Vue 3 提供的ref
将其包装成一个响应式的对象,和旧的data
中的数据一样。 - 用
return
将状态数据返回,Vue 就会将其注入到 Vue 实例的this
中。
刷新下页面,功能无任何变化。
获取数据
只把状态数据的位置挪动一下没什么意思,下面试试把获取数据的 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))
// }
},
}
看起来只是把方法挪了个地方而已。
但里面有一些很重要的区别:
- 由于
setup()
里没有this
,自然this.$route
也不能用,必须用 vue-router 的useRoute()
方法手动创建路由对象。(类似的还有useRouter()
)。所有用到this
的地方都进行了对应的调整。 - 用
ref.value
可访问到响应式对象中实际保存的数据,比如info.value
。 - 用
return
将方法返回。
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
作为对应的替代。
由于 setup 外不再关注 get_article_data() 方法,因此可以不用返回它了。
此外
watch(route, ...)
可能导致潜在的性能问题(控制台警告提示),不过教程为保持简单就不深究了。有关此问题的讨论见vue-next issues。
可复用模块
到目前为止都没什么特别的,代码量似乎还更多了。接下来我们试试将与获取数据相关的功能抽离成独立的 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),确保在获取到数据前不会执行后面的操作数据的逻辑。(从而导致报错)
你不能在函数体内部重新创建一个 info,那和 setup 中的 info 是两个不同的对象。
接着再来修改 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
}
},
...
}
- 仅需要调用
get_article_data
即可。 - 不再需要 axios ,将其注释掉。
刷新页面,功能正常无变化。
翻页模块
现在我们已经将获取数据功能抽离为一个独立模块了。
另一块关注点较为集中的逻辑就是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
参数传递到函数体内部,并返回对应的值。
如何理解捕获这个词?看看
getPageParam()
里用到的三个参数:info、route 和 direction。其中 direction 是函数每次调用时通过参数动态传递进来的,但是 info 和 route 却不是这样,它们是创建get_page_param
闭包时被强行”抓取“进函数体内的对象的引用。这就是所谓的被闭包捕获了。闭包的详细用法超出本文的讲解范围,搞不懂的读者朋友可自行找文档补充对应语法知识。
接着修改 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,这是历史洪流。
那为什么前面花了那么多力气讲选项式 API?首先它相对容易理解,对新手入门比较友好;其次 Vue 3 对其完全支持,学了不亏;再次将其移植到组合式 API 不困难,无非就是多点函数异步和闭包的理解;最后,不是所有公司都能够马上用上 Vue 3 的,掌握 Vue 2 的核心基础在当下是非常必要的。