在上一篇 Vite + Vue3 初體驗(yàn) —— Vite 篇 博客中恃泪,我感受到了 Vite 帶來(lái)的運(yùn)行時(shí)效率提升贝乎,這一期再來(lái)感受感受 Vue3
帶來(lái)的新變化 —— 關(guān)注點(diǎn)分離。
Todo List 設(shè)計(jì)
這次體驗(yàn) Vue3
朽肥,我想做一個(gè)能體驗(yàn)(部分) Vue3
新特性的功能模塊持钉。
想了想每强,用一個(gè) Todo List
應(yīng)該是比較合適的空执。
我們來(lái)規(guī)劃一下它的功能清單吧辨绊。
- 輸入
Todo
门坷,按下回車(chē)即可添加一條新的Todo Item
默蚌。 - 以列表的形式顯示所有的
Todo Item
鼻弧。 - 可以將
Todo Item
標(biāo)記為完成攘轩,標(biāo)記完成后的Todo Item
會(huì)置灰度帮,并且排序處于最下面。 - 可以將
Todo Item
刪除挠铲,刪除后在列表中不展示安聘。 - 可以將
Todo Item
置頂浴韭,高亮顯示念颈,以提高優(yōu)先級(jí)。
OK窟感,接下來(lái)柿祈,我們先把基礎(chǔ)頁(yè)面搭建出來(lái)吧蜜自。
搭建基礎(chǔ) UI 界面
配置 UI 庫(kù)
目前支持 Vue3
的 UI 框架有下面幾種:
其中 ant-design
和 elementui
是從 Vue2
一路走來(lái)的老 UI 庫(kù)了袁辈,我在體驗(yàn) Vue3
的時(shí)候決定還是使用輕風(fēng)格的 ant-design
。
先安裝支持 Vue3
的 ant-design-vue
吧荞彼。
yarn add ant-design-vue@next
然后鸣皂,再配置一下按需加載寞缝,這樣的話(huà)荆陆,只有被使用到的組件才會(huì)被打包被啼,可有效減小生產(chǎn)包的體積。
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
Components({
resolvers: [
AntDesignVueResolver(),
],
}),
]
});
最后命浴,在 main.ts
中引入樣式文件。
// main.ts
import 'ant-design-vue/dist/antd.css';
基礎(chǔ)布局
現(xiàn)在,我們的布局需要一個(gè)輸入框和一個(gè)列表飞醉,我們先在頁(yè)面把這兩個(gè)元素畫(huà)出來(lái)吧轴术。
在此之前,在
App.vue
中引入了我們的TodoList
組件失暂。
// TodoList.vue
<script setup lang="ts">
import { DeleteOutlined, CheckOutlined, CheckCircleFilled } from '@ant-design/icons-vue';
import { Input } from "ant-design-vue";
</script>
<template>
<section class="todo-list-container">
<section class="todo-wrapper">
<Input class="todo-input" placeholder="請(qǐng)輸入待辦項(xiàng)" />
<section class="todo-list">
<section class="todo-item">
<span>Todo Item</span>
<div class="operator-list">
<DeleteOutlined />
<CheckOutlined />
</div>
</section>
<section class="todo-item">
<span>Todo Item</span>
<div class="operator-list">
<DeleteOutlined />
<CheckOutlined />
</div>
</section>
<section class="todo-item todo-checked">
<span>Todo Item</span>
<div class="operator-list">
<CheckCircleFilled />
</div>
</section>
</section>
</section>
</section>
</template>
<style scoped lang="less">
.todo-list-container {
display: flex;
justify-content: center;
width: 100vw;
height: 100vh;
box-sizing: border-box;
padding-top: 100px;
background: linear-gradient(rgba(93, 190, 129, .02), rgba(125, 185, 222, .02));
.todo-wrapper {
width: 60vw;
.todo-input {
width: 100%;
height: 50px;
font-size: 18px;
color: #F05E1C;
border: 2px solid rgba(255, 177, 27, 0.5);
border-radius: 5px;
}
.todo-input::placeholder {
color: #F05E1C;
opacity: .4;
}
.ant-input:hover, .ant-input:focus {
border-color: #FFB11B;
box-shadow: 0 0 0 2px rgb(255 177 27 / 20%);
}
.todo-list {
margin-top: 20px;
.todo-item {
box-sizing: border-box;
padding: 15px 10px;
cursor: pointer;
border-bottom: 2px solid rgba(255, 177, 27, 0.3);
color: #F05E1C;
margin-bottom: 5px;
font-size: 16px;
transition: all .5s;
display: flex;
justify-content: space-between;
align-items: center;
padding-right: 10px;
.operator-list {
display: flex;
justify-content: flex-start;
align-items: center;
:first-child {
margin-right: 10px;
}
}
}
.todo-checked {
color: rgba(199, 199, 199, 1);
border-bottom-color: rgba(199, 199, 199, .4);
transition: all .5s;
}
.todo-item:hover {
box-shadow: 0 0 5px 8px rgb(255 177 27 / 20%);
border-bottom: 2px solid transparent;
}
.todo-checked:hover {
box-shadow: none;
border-bottom-color: rgba(199, 199, 199, .4);
}
}
}
}
</style>
這次我選了一套黃橙配色摧冀,我們來(lái)看看界面的效果吧索昂。
處理業(yè)務(wù)邏輯
處理輸入
現(xiàn)在,我們來(lái)處理一下我們的輸入邏輯框产,在按下回車(chē)鍵時(shí)秉宿,將輸入的結(jié)果收集起來(lái)添加到 Todo
數(shù)組中描睦,并且將輸入框清空。
這里需要用到雙向綁定,定義一個(gè) 引用
變量撵彻,與輸入框進(jìn)行綁定。
<script setup lang="ts">
import { ref } from "vue";
// 創(chuàng)建一個(gè)引用變量陌僵,用于綁定 Todo List 數(shù)據(jù)
const todoList = ref<{
title: string,
is_completed: boolean
}[]>([]);
// 創(chuàng)建一個(gè)引用變量轴合,用于綁定輸入框
const todoText = ref('');
const onTodoInputEnter = () => {
// 將 todo item 添加到 todoList 中
todoList.value.unshift({
title: todoText.value,
is_completed: false
});
// 添加到 todoList 后,清空 todoText 的值
todoText.value = '';
}
</script>
<template>
//...
<!-- v-model:value 語(yǔ)法是 vue3 的新特性碗短,代表組件內(nèi)部進(jìn)行雙向綁定是值 key 是 value -->
<Input v-model:value="todoText" @keyup.enter="onTodoInputEnter" class="todo-input" placeholder="請(qǐng)輸入待辦項(xiàng)" />
</template>
現(xiàn)在打開(kāi)本地開(kāi)發(fā)界面受葛,輸入一個(gè)值,然后按下回車(chē)偎谁,輸入框的值就被清空了 —— 將這一項(xiàng)添加到了 todoList
數(shù)組中总滩!
渲染列表
在處理好了輸入之后搭盾,現(xiàn)在需要將列表渲染出來(lái)蝌以。
這里還是用經(jīng)典的 v-for
語(yǔ)法奸攻,同時(shí)需要加上一些狀態(tài)的判斷硝训。
<section class="todo-list">
<section v-for="item in todoList" class="todo-item" :class="{'todo-completed': item.is_completed}">
<span>{{item.title}}</span>
<div class="operator-list">
<CheckCircleFilled v-show="item.is_completed" />
<DeleteOutlined v-show="!item.is_completed" />
<CheckOutlined v-show="!item.is_completed" />
</div>
</section>
</section>
這個(gè)語(yǔ)法相信用過(guò) vue2
的都清楚荸哟,就不做過(guò)多介紹了掏父。
有一說(shuō)一陶缺,
vscode
+volar
對(duì)vue3 + ts
的支持是真不錯(cuò),代碼提示和錯(cuò)誤提示都非常完善了。在開(kāi)發(fā)過(guò)程中慎菲,簡(jiǎn)直是事半功倍有决。
處理刪除和完成邏輯
最后篱瞎,我們來(lái)處理一下刪除和完成的邏輯吧。
<script setup lang="ts">
// 創(chuàng)建一個(gè)引用變量,用于綁定 Todo List 數(shù)據(jù)
const todoList = ref<{
title: string,
is_completed: boolean
}[]>([]);
// 刪除和完成的邏輯都與 todoList 放在同一個(gè)地方嫌套,這樣對(duì)于邏輯關(guān)注點(diǎn)就更加聚焦了
const onDeleteItem = (index: number) => {
todoList.value.splice(index, 1);
}
const onCompleteItem = (index: number) => {
todoList.value[index].is_completed = true;
// 重新排序挨约,將已經(jīng)完成的項(xiàng)目往后排列
todoList.value = todoList.value.sort(item => item.is_completed ? 0 : -1);
}
</script>
<template>
//...
<DeleteOutlined v-show="!item.is_completed" @click="onDeleteItem(index)" />
<CheckOutlined v-show="!item.is_completed" @click="onCompleteItem(index)" />
</template>
最后,來(lái)看看我們界面的效果吧篮撑。(如下圖)
加入置頂邏輯
我們需要先給數(shù)組元素添加一個(gè)字段 is_top
又碌,用于判斷該節(jié)點(diǎn)是否置頂圾笨。
然后俭令,再加入置頂函數(shù)的邏輯處理以及樣式顯示。(如下)
<script setup lang="ts">
// 創(chuàng)建一個(gè)引用變量奏寨,用于綁定 Todo List 數(shù)據(jù)
const todoList = ref<{
title: string,
is_completed: boolean,
is_top: boolean
}[]>([]);
const onTopItem = (index: number) => {
todoList.value[index].is_top = true;
// 重新排序,將已經(jīng)完成的項(xiàng)目往前排列
const todoItem = todoList.value.splice(index, 1);
todoList.value.unshift(todoItem[0]);
}
</script>
<template>
//...
<section class="todo-list">
<section v-for="(item, index) in todoList"
class="todo-item"
:class="{'todo-completed': item.is_completed, 'todo-top': item.is_top}">
<span>{{item.title}}</span>
<div class="operator-list">
<CheckCircleFilled v-show="item.is_completed" />
<DeleteOutlined v-show="!item.is_completed" @click="onDeleteItem(index)" />
<ToTopOutlined v-show="!item.is_completed" @click="onTopItem(index)" />
<CheckOutlined v-show="!item.is_completed" @click="onCompleteItem(index)" />
</div>
</section>
</section>
</template>
然后,我們來(lái)看看我們的界面效果吧1枳纭(如下圖)
這樣一來(lái)邢滑,我們的 Todo List
就完成了愿汰!
現(xiàn)在再來(lái)看看我們的代碼,主要是有兩塊邏輯關(guān)注點(diǎn):
-
todoList
相關(guān)邏輯酗宋,負(fù)責(zé)列表的渲染以及列表的相關(guān)操作(刪除芬失、置頂、完成)抛蚤。 -
todoText
相關(guān)邏輯骑脱,負(fù)責(zé)處理輸入框的輸入菜枷。
在分離了邏輯關(guān)注點(diǎn)后帶來(lái)的好處時(shí),如果我想要修改列表相關(guān)的處理邏輯叁丧,我只需要關(guān)注和調(diào)整 todoList
相關(guān)的代碼即可啤誊;如果我想要調(diào)整輸入相關(guān)的邏輯,我只需要關(guān)注和調(diào)整 todoText
相關(guān)的邏輯即可拥娄。
如果這兩塊的邏輯后面隨著業(yè)務(wù)發(fā)展而變得越來(lái)越復(fù)雜了蚊锹,我可以選擇將其拆分成更小塊的業(yè)務(wù)邏輯來(lái)進(jìn)行維護(hù),還可以將這些邏輯都拆分到單文件中進(jìn)行維護(hù)管理稚瘾,這樣對(duì)于后續(xù)的維護(hù)和升級(jí)都能夠有更好的把控牡昆。
處理前后端交互邏輯
我們之前所有的邏輯都是在本地做的處理,現(xiàn)在我們來(lái)接入服務(wù)端的邏輯摊欠,將我們的所有數(shù)據(jù)及變更進(jìn)行持久化丢烘。同時(shí),我們也來(lái)看看在 Vue3
中些椒,如何處理有前后端交互邏輯的場(chǎng)景播瞳。
假設(shè)我們有下面這么幾組接口(如下圖)
那么,基于這幾組接口的后端交互邏輯免糕,我們還是用經(jīng)典的 axios
來(lái)做吧赢乓。
使用
yarn add axios
添加依賴(lài)。
這里石窑,我們先在 src
目錄下新建一個(gè) service
牌芋,用于初始化我們用于網(wǎng)絡(luò)請(qǐng)求的 service
。(如下)
// src/service/index.ts
import axios from "axios";
const service = axios.create({
// 設(shè)置 baseURL松逊,這個(gè)地址是我部署的后端服務(wù)
baseURL: "https://hacker.jt-gmall.com"
});
export default service;
用戶(hù)身份信息
我們?cè)O(shè)計(jì)的 Todo List
是一個(gè)在線(xiàn)網(wǎng)頁(yè)躺屁,我們希望每個(gè)用戶(hù)進(jìn)來(lái)看到的都是自己的 Todo List
。
我們來(lái)看看后臺(tái)的接口設(shè)計(jì)经宏,他使用 key
來(lái)給 Todo Item
做分組楼咳,所以我們需要在進(jìn)入頁(yè)面時(shí),為每一個(gè)用戶(hù)生成一個(gè)獨(dú)一無(wú)二的 user key
烛恤。
我們先設(shè)計(jì)一個(gè)用來(lái)獲取 key
的函數(shù)吧母怜。
這里使用
uuid
來(lái)生成唯一的user key
。
// service/auth.ts
import { v4 as uuid } from "uuid";
const getUserKey = () => {
if (localStorage.getItem('user_key')) return localStorage.getItem('user_key');
const userKey = uuid();
localStorage.setItem('user_key', userKey);
return userKey;
}
export {
getUserKey
}
獲取 Todo List
然后缚柏,我們回到我們的 TodoList.vue
文件苹熏,我們先寫(xiě)一個(gè)獲取遠(yuǎn)端 Todo
列表的邏輯。(如下)
// TodoList.vue
import service from "@/service";
import { getUserKey } from '@/service/auth';
// 創(chuàng)建一個(gè)引用變量,用于綁定 Todo List 數(shù)據(jù)
const todoList = ref<{
title: string,
is_completed: boolean,
is_top: boolean
}[]>([]);
// 初始化 todo list
const getTodoList = async () => {
const reply = await service.get('/todo/get-todo-list', { params: { key: getUserKey() } });
todoList.value = reply.data.data;
}
getTodoList();
這里加上網(wǎng)絡(luò)請(qǐng)求后轨域,頁(yè)面也是不會(huì)有什么變化的袱耽,因?yàn)檫@個(gè)用戶(hù)目前是沒(méi)有數(shù)據(jù)的。
接下來(lái)干发,我們把剩下的幾個(gè)邏輯都補(bǔ)全朱巨。
注意:這里使用到了
alias
別名功能,需要在vite.config.ts
和tsconfig.json
中進(jìn)行配置枉长。
import path from 'path';
// vite.config.ts
export default defineConfig({
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
}
},
// ...
})
// tsconfig.json
{
"compilerOptions": {
// ...
"baseUrl": "./",
"paths": {
"@/*": ["./src/*"]
}
}
}
新增冀续、置頂、完成必峰、刪除 Todo
由于用戶(hù)進(jìn)入 Todo List
查看的都是自己的數(shù)據(jù)洪唐,并且該數(shù)據(jù)只有自己可操作。
所以吼蚁,也是為了能有更好的用戶(hù)體驗(yàn)凭需,在我們所有的操作邏輯完成后,回顯數(shù)據(jù)還是用原有的邏輯肝匆。
當(dāng)然粒蜈,新增數(shù)據(jù)時(shí),還是需要重新獲取列表數(shù)據(jù)旗国,因?yàn)槲覀儾僮鲾?shù)據(jù)時(shí)需要用到每一項(xiàng)的 id
枯怖。
綜上所述,我們重構(gòu)后的四個(gè)函數(shù)長(zhǎng)這樣粗仓。
// 刪除、完成设捐、置頂?shù)倪壿嫸寂c todoList 放在同一個(gè)地方借浊,這樣對(duì)于邏輯關(guān)注點(diǎn)就更加聚焦了
const onDeleteItem = async (index: number) => {
const id = todoList.value[index].id;
await service.post('/todo/delete', { id });
todoList.value.splice(index, 1);
}
const onCompleteItem = async (index: number) => {
const id = todoList.value[index].id;
await service.post('/todo/complete', { id });
todoList.value[index].is_completed = true;
// 重新排序,將已經(jīng)完成的項(xiàng)目往后排列
const todoItem = todoList.value.splice(index, 1);
todoList.value.push(todoItem[0]);
}
const onTopItem = async (index: number) => {
const id = todoList.value[index].id;
await service.post('/todo/top', { id });
todoList.value[index].is_top = true;
// 重新排序萝招,將已經(jīng)完成的項(xiàng)目往前排列
const todoItem = todoList.value.splice(index, 1);
todoList.value.unshift(todoItem[0]);
}
// 新增 Todo Item 的邏輯都放在一處
// 創(chuàng)建一個(gè)引用變量蚂斤,用于綁定輸入框
const todoText = ref('');
const addTodoItem = () => {
// 新增一個(gè) TodoItem,請(qǐng)求新增接口
const todoItem = {
key: getUserKey(),
title: todoText.value
}
return service.post('/todo/add', todoItem);
}
const onTodoInputEnter = async () => {
if (todoText.value === '') return;
await addTodoItem();
await getTodoList();
// 添加成功后槐沼,清空 todoText 的值
todoText.value = '';
}
邏輯修改完成后曙蒸,我們回到頁(yè)面查看一下效果吧!我們做一些操作后岗钩,刷新頁(yè)面查看一下纽窟。(如下圖)
刷新頁(yè)面后,我們的數(shù)據(jù)依然是可以展示出來(lái)的兼吓,說(shuō)明數(shù)據(jù)已經(jīng)成功做了服務(wù)端持久化啦臂港!
小結(jié)
這次,我們用 Vue3
來(lái)完成了一個(gè)簡(jiǎn)單的 Todo List
系統(tǒng)。
可以看出审孽,Vue3
對(duì) ts
的支持變得更友好了县袱,而新的 vue
單文件語(yǔ)法和 組合式 API
給我的體驗(yàn)也有點(diǎn)接近 React
+ JSX
。 —— 我的意思是佑力,給開(kāi)發(fā)者的體驗(yàn)更好了式散。
我們?cè)賮?lái)看看我們用 組合式 API
實(shí)現(xiàn)的邏輯部分(如下圖)。
從上圖可以看出打颤,我們的邏輯關(guān)注點(diǎn)被分成了兩大塊暴拄,分別是列表相關(guān)邏輯(渲染、操作)和新增 Todo Item瘸洛。
這種清晰的職責(zé)劃分使得我們需要維護(hù)某一部分的功能時(shí)揍移,與之相關(guān)的內(nèi)容都被圈在了一個(gè)比較小的范圍,能夠讓人更加聚焦到需要調(diào)整的功能上反肋。
如果現(xiàn)在讓我給 Vue3
和 Vue2
的(開(kāi)發(fā))體驗(yàn)打個(gè)分的話(huà)那伐,我會(huì)分別給出 8分
和 6分
。
好啦石蔗,我們這次的 Vue3
體驗(yàn)就到此為止了罕邀,Vue3
給我的體驗(yàn)還是非常不錯(cuò)的!
最后附上本次體驗(yàn)的 Demo 地址养距。
最后一件事
如果您已經(jīng)看到這里了诉探,希望您還是點(diǎn)個(gè)贊再走吧~
您的點(diǎn)贊是對(duì)作者的最大鼓勵(lì),也可以讓更多人看到本篇文章棍厌!
如果覺(jué)得本文對(duì)您有幫助肾胯,請(qǐng)幫忙在 github 上點(diǎn)亮 star
鼓勵(lì)一下吧!