在上一篇 Vite + Vue3 初體驗 —— Vite 篇 部落格中,我感受到了 Vite 帶來的執行時效率提升,這一期再來感受感受 Vue3
帶來的新變化 —— 關注點分離。
Todo List 設計
這次體驗 Vue3
,我想做一個能體驗(部分) Vue3
新特性的功能模組。
想了想,用一個 Todo List
應該是比較合適的。
我們來規劃一下它的功能清單吧。
- 輸入
Todo
,按下回車即可新增一條新的Todo Item
。 - 以列表的形式顯示所有的
Todo Item
。 - 可以將
Todo Item
標記為完成,標記完成後的Todo Item
會置灰,並且排序處於最下面。 - 可以將
Todo Item
刪除,刪除後在列表中不展示。 - 可以將
Todo Item
置頂,高亮顯示,以提高優先順序。
OK,接下來,我們先把基礎頁面搭建出來吧。
搭建基礎 UI 介面
配置 UI 庫
目前支援 Vue3
的 UI 框架有下面幾種:
其中 ant-design
和 elementui
是從 Vue2
一路走來的老 UI 庫了,我在體驗 Vue3
的時候決定還是使用輕風格的 ant-design
。
先安裝支援 Vue3
的 ant-design-vue
吧。
yarn add ant-design-vue@next
然後,再配置一下按需載入,這樣的話,只有被使用到的元件才會被打包,可有效減小生產包的體積。
// 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';
基礎佈局
現在,我們的佈局需要一個輸入框和一個列表,我們先在頁面把這兩個元素畫出來吧。
在此之前,在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="請輸入待辦項" />
<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>
這次我選了一套黃橙配色,我們來看看介面的效果吧。
處理業務邏輯
處理輸入
現在,我們來處理一下我們的輸入邏輯,在按下Enter鍵時,將輸入的結果收集起來新增到 Todo
陣列中,並且將輸入框清空。
這裡需要用到雙向繫結,定義一個 引用
變數,與輸入框進行繫結。
<script setup lang="ts">
import { ref } from "vue";
// 建立一個引用變數,用於繫結 Todo List 資料
const todoList = ref<{
title: string,
is_completed: boolean
}[]>([]);
// 建立一個引用變數,用於繫結輸入框
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 語法是 vue3 的新特性,代表元件內部進行雙向繫結是值 key 是 value -->
<Input v-model:value="todoText" @keyup.enter="onTodoInputEnter" class="todo-input" placeholder="請輸入待辦項" />
</template>
現在開啟本地開發介面,輸入一個值,然後按下回車,輸入框的值就被清空了 —— 將這一項新增到了 todoList
陣列中!
渲染列表
在處理好了輸入之後,現在需要將列表渲染出來。
這裡還是用經典的 v-for
語法,同時需要加上一些狀態的判斷。
<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>
這個語法相信用過 vue2
的都清楚,就不做過多介紹了。
有一說一,vscode
+volar
對vue3 + ts
的支援是真不錯,程式碼提示和錯誤提示都非常完善了。在開發過程中,簡直是事半功倍。
處理刪除和完成邏輯
最後,我們來處理一下刪除和完成的邏輯吧。
<script setup lang="ts">
// 建立一個引用變數,用於繫結 Todo List 資料
const todoList = ref<{
title: string,
is_completed: boolean
}[]>([]);
// 刪除和完成的邏輯都與 todoList 放在同一個地方,這樣對於邏輯關注點就更加聚焦了
const onDeleteItem = (index: number) => {
todoList.value.splice(index, 1);
}
const onCompleteItem = (index: number) => {
todoList.value[index].is_completed = true;
// 重新排序,將已經完成的專案往後排列
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>
最後,來看看我們介面的效果吧。(如下圖)
加入置頂邏輯
我們需要先給陣列元素新增一個欄位 is_top
,用於判斷該節點是否置頂。
然後,再加入置頂函式的邏輯處理以及樣式顯示。(如下)
<script setup lang="ts">
// 建立一個引用變數,用於繫結 Todo List 資料
const todoList = ref<{
title: string,
is_completed: boolean,
is_top: boolean
}[]>([]);
const onTopItem = (index: number) => {
todoList.value[index].is_top = true;
// 重新排序,將已經完成的專案往前排列
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>
然後,我們來看看我們的介面效果吧!(如下圖)
這樣一來,我們的 Todo List
就完成了!
現在再來看看我們的程式碼,主要是有兩塊邏輯關注點:
todoList
相關邏輯,負責列表的渲染以及列表的相關操作(刪除、置頂、完成)。todoText
相關邏輯,負責處理輸入框的輸入。
在分離了邏輯關注點後帶來的好處時,如果我想要修改列表相關的處理邏輯,我只需要關注和調整 todoList
相關的程式碼即可;如果我想要調整輸入相關的邏輯,我只需要關注和調整 todoText
相關的邏輯即可。
如果這兩塊的邏輯後面隨著業務發展而變得越來越複雜了,我可以選擇將其拆分成更小塊的業務邏輯來進行維護,還可以將這些邏輯都拆分到單檔案中進行維護管理,這樣對於後續的維護和升級都能夠有更好的把控。
處理前後端互動邏輯
我們之前所有的邏輯都是在本地做的處理,現在我們來接入服務端的邏輯,將我們的所有資料及變更進行持久化。同時,我們也來看看在 Vue3
中,如何處理有前後端互動邏輯的場景。
假設我們有下面這麼幾組介面(如下圖)
那麼,基於這幾組介面的後端互動邏輯,我們還是用經典的 axios
來做吧。
使用 yarn add axios
新增依賴。
這裡,我們先在 src
目錄下新建一個 service
,用於初始化我們用於網路請求的 service
。(如下)
// src/service/index.ts
import axios from "axios";
const service = axios.create({
// 設定 baseURL,這個地址是我部署的後端服務
baseURL: "https://hacker.jt-gmall.com"
});
export default service;
使用者身份資訊
我們設計的 Todo List
是一個線上網頁,我們希望每個使用者進來看到的都是自己的 Todo List
。
我們來看看後臺的介面設計,他使用 key
來給 Todo Item
做分組,所以我們需要在進入頁面時,為每一個使用者生成一個獨一無二的 user key
。
我們先設計一個用來獲取 key
的函式吧。
這裡使用uuid
來生成唯一的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
檔案,我們先寫一個獲取遠端 Todo
列表的邏輯。(如下)
// TodoList.vue
import service from "@/service";
import { getUserKey } from '@/service/auth';
// 建立一個引用變數,用於繫結 Todo List 資料
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();
這裡加上網路請求後,頁面也是不會有什麼變化的,因為這個使用者目前是沒有資料的。
接下來,我們把剩下的幾個邏輯都補全。
注意:這裡使用到了alias
別名功能,需要在vite.config.ts
和tsconfig.json
中進行配置。
import path from 'path';
// vite.config.ts
export default defineConfig({
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
}
},
// ...
})
// tsconfig.json
{
"compilerOptions": {
// ...
"baseUrl": "./",
"paths": {
"@/*": ["./src/*"]
}
}
}
新增、置頂、完成、刪除 Todo
由於使用者進入 Todo List
檢視的都是自己的資料,並且該資料只有自己可操作。
所以,也是為了能有更好的使用者體驗,在我們所有的操作邏輯完成後,回顯資料還是用原有的邏輯。
當然,新增資料時,還是需要重新獲取列表資料,因為我們運算元據時需要用到每一項的 id
。
綜上所述,我們重構後的四個函式長這樣。
// 刪除、完成、置頂的邏輯都與 todoList 放在同一個地方,這樣對於邏輯關注點就更加聚焦了
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;
// 重新排序,將已經完成的專案往後排列
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;
// 重新排序,將已經完成的專案往前排列
const todoItem = todoList.value.splice(index, 1);
todoList.value.unshift(todoItem[0]);
}
// 新增 Todo Item 的邏輯都放在一處
// 建立一個引用變數,用於繫結輸入框
const todoText = ref('');
const addTodoItem = () => {
// 新增一個 TodoItem,請求新增介面
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 = '';
}
邏輯修改完成後,我們回到頁面檢視一下效果吧!我們做一些操作後,重新整理頁面檢視一下。(如下圖)
重新整理頁面後,我們的資料依然是可以展示出來的,說明資料已經成功做了服務端持久化啦!
小結
這次,我們用 Vue3
來完成了一個簡單的 Todo List
系統。
可以看出,Vue3
對 ts
的支援變得更友好了,而新的 vue
單檔案語法和 組合式 API
給我的體驗也有點接近 React
+ JSX
。 —— 我的意思是,給開發者的體驗更好了。
我們再來看看我們用 組合式 API
實現的邏輯部分(如下圖)。
從上圖可以看出,我們的邏輯關注點被分成了兩大塊,分別是列表相關邏輯(渲染、操作)和新增 Todo Item。
這種清晰的職責劃分使得我們需要維護某一部分的功能時,與之相關的內容都被圈在了一個比較小的範圍,能夠讓人更加聚焦到需要調整的功能上。
如果現在讓我給 Vue3
和 Vue2
的(開發)體驗打個分的話,我會分別給出 8分
和 6分
。
好啦,我們這次的 Vue3
體驗就到此為止了,Vue3
給我的體驗還是非常不錯的!
最後附上本次體驗的 Demo 地址。
最後一件事
如果您已經看到這裡了,希望您還是點個贊再走吧~
您的點贊是對作者的最大鼓勵,也可以讓更多人看到本篇文章!
如果覺得本文對您有幫助,請幫忙在 github 上點亮 star
鼓勵一下吧!