Vite + Vue3 初體驗 —— Vue3 篇

曬兜斯 發表於 2022-01-29
Vue

在上一篇 Vite + Vue3 初體驗 —— Vite 篇 部落格中,我感受到了 Vite 帶來的執行時效率提升,這一期再來感受感受 Vue3 帶來的新變化 —— 關注點分離。

Todo List 設計

這次體驗 Vue3,我想做一個能體驗(部分) Vue3 新特性的功能模組。

想了想,用一個 Todo List 應該是比較合適的。

我們來規劃一下它的功能清單吧。

  1. 輸入 Todo,按下回車即可新增一條新的 Todo Item
  2. 以列表的形式顯示所有的 Todo Item
  3. 可以將 Todo Item 標記為完成,標記完成後的 Todo Item 會置灰,並且排序處於最下面。
  4. 可以將 Todo Item 刪除,刪除後在列表中不展示。
  5. 可以將 Todo Item 置頂,高亮顯示,以提高優先順序。

OK,接下來,我們先把基礎頁面搭建出來吧。

搭建基礎 UI 介面

配置 UI 庫

目前支援 Vue3 的 UI 框架有下面幾種:

  1. Ant Design Vue
  2. Element Plus
  3. Ionic
  4. Native UI

其中 ant-designelementui 是從 Vue2 一路走來的老 UI 庫了,我在體驗 Vue3 的時候決定還是使用輕風格的 ant-design

先安裝支援 Vue3ant-design-vue 吧。

yarn add [email protected]

然後,再配置一下按需載入,這樣的話,只有被使用到的元件才會被打包,可有效減小生產包的體積。

// 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>

這次我選了一套黃橙配色,我們來看看介面的效果吧。

image

處理業務邏輯

處理輸入

現在,我們來處理一下我們的輸入邏輯,在按下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 + volarvue3 + 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>

最後,來看看我們介面的效果吧。(如下圖)

image

加入置頂邏輯

我們需要先給陣列元素新增一個欄位 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>

然後,我們來看看我們的介面效果吧!(如下圖)

image

這樣一來,我們的 Todo List 就完成了!

現在再來看看我們的程式碼,主要是有兩塊邏輯關注點:

  1. todoList 相關邏輯,負責列表的渲染以及列表的相關操作(刪除、置頂、完成)。
  2. todoText 相關邏輯,負責處理輸入框的輸入。

在分離了邏輯關注點後帶來的好處時,如果我想要修改列表相關的處理邏輯,我只需要關注和調整 todoList 相關的程式碼即可;如果我想要調整輸入相關的邏輯,我只需要關注和調整 todoText 相關的邏輯即可。

如果這兩塊的邏輯後面隨著業務發展而變得越來越複雜了,我可以選擇將其拆分成更小塊的業務邏輯來進行維護,還可以將這些邏輯都拆分到單檔案中進行維護管理,這樣對於後續的維護和升級都能夠有更好的把控。

處理前後端互動邏輯

我們之前所有的邏輯都是在本地做的處理,現在我們來接入服務端的邏輯,將我們的所有資料及變更進行持久化。同時,我們也來看看在 Vue3 中,如何處理有前後端互動邏輯的場景。

假設我們有下面這麼幾組介面(如下圖)

image

那麼,基於這幾組介面的後端互動邏輯,我們還是用經典的 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.tstsconfig.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 = '';
}

邏輯修改完成後,我們回到頁面檢視一下效果吧!我們做一些操作後,重新整理頁面檢視一下。(如下圖)

image

重新整理頁面後,我們的資料依然是可以展示出來的,說明資料已經成功做了服務端持久化啦!

小結

這次,我們用 Vue3 來完成了一個簡單的 Todo List 系統。

可以看出,Vue3ts 的支援變得更友好了,而新的 vue 單檔案語法和 組合式 API 給我的體驗也有點接近 React + JSX。 —— 我的意思是,給開發者的體驗更好了。

我們再來看看我們用 組合式 API 實現的邏輯部分(如下圖)。

image

從上圖可以看出,我們的邏輯關注點被分成了兩大塊,分別是列表相關邏輯(渲染、操作)和新增 Todo Item。

這種清晰的職責劃分使得我們需要維護某一部分的功能時,與之相關的內容都被圈在了一個比較小的範圍,能夠讓人更加聚焦到需要調整的功能上。

如果現在讓我給 Vue3Vue2 的(開發)體驗打個分的話,我會分別給出 8分6分

好啦,我們這次的 Vue3 體驗就到此為止了,Vue3 給我的體驗還是非常不錯的!

最後附上本次體驗的 Demo 地址

最後一件事

如果您已經看到這裡了,希望您還是點個贊再走吧~

您的點贊是對作者的最大鼓勵,也可以讓更多人看到本篇文章!

如果覺得本文對您有幫助,請幫忙在 github 上點亮 star 鼓勵一下吧!