Vite + Vue3 初體驗 —— Vue3 篇

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

Todo List 設計

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

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

我們來規(guī)劃一下它的功能清單吧。

  1. 輸入 Todo,按下回車即可添加一條新的 Todo Item。
  2. 以列表的形式顯示所有的 Todo Item。
  3. 可以將 Todo Item 標記為完成,標記完成后的 Todo Item 會置灰,并且排序處于最下面。
  4. 可以將 Todo Item 刪除,刪除后在列表中不展示。
  5. 可以將 Todo Item 置頂,高亮顯示,以提高優(yōu)先級。

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 ant-design-vue@next

然后,再配置一下按需加載,這樣的話,只有被使用到的組件才會被打包,可有效減小生產(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';

基礎布局

現(xiàn)在,我們的布局需要一個輸入框和一個列表,我們先在頁面把這兩個元素畫出來吧。

在此之前,在 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

處理業(yè)務邏輯

處理輸入

現(xiàn)在,我們來處理一下我們的輸入邏輯,在按下回車鍵時,將輸入的結(jié)果收集起來添加到 Todo 數(shù)組中,并且將輸入框清空。

這里需要用到雙向綁定,定義一個 引用 變量,與輸入框進行綁定。

<script setup lang="ts">
import { ref } from "vue";

// 創(chuàng)建一個引用變量,用于綁定 Todo List 數(shù)據(jù)
const todoList = ref<{
  title: string,
  is_completed: boolean
}[]>([]);

// 創(chuàng)建一個引用變量,用于綁定輸入框
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 的新特性,代表組件內(nèi)部進行雙向綁定是值 key 是 value -->
  <Input v-model:value="todoText" @keyup.enter="onTodoInputEnter" class="todo-input" placeholder="請輸入待辦項" />
</template>

現(xiàn)在打開本地開發(fā)界面,輸入一個值,然后按下回車,輸入框的值就被清空了 —— 將這一項添加到了 todoList 數(shù)組中!

渲染列表

在處理好了輸入之后,現(xiàn)在需要將列表渲染出來。

這里還是用經(jīng)典的 v-for 語法,同時需要加上一些狀態(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>

這個語法相信用過 vue2 的都清楚,就不做過多介紹了。

有一說一,vscode + volarvue3 + ts 的支持是真不錯,代碼提示和錯誤提示都非常完善了。在開發(fā)過程中,簡直是事半功倍。

處理刪除和完成邏輯

最后,我們來處理一下刪除和完成的邏輯吧。

<script setup lang="ts">
// 創(chuàng)建一個引用變量,用于綁定 Todo List 數(shù)據(jù)
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;
  // 重新排序,將已經(jī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>

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

image

加入置頂邏輯

我們需要先給數(shù)組元素添加一個字段 is_top,用于判斷該節(jié)點是否置頂。

然后,再加入置頂函數(shù)的邏輯處理以及樣式顯示。(如下)

<script setup lang="ts">
// 創(chuàng)建一個引用變量,用于綁定 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)完成的項目往前排列
  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>

然后,我們來看看我們的界面效果吧?。ㄈ缦聢D)

image

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

現(xiàn)在再來看看我們的代碼,主要是有兩塊邏輯關注點:

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

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

如果這兩塊的邏輯后面隨著業(yè)務發(fā)展而變得越來越復雜了,我可以選擇將其拆分成更小塊的業(yè)務邏輯來進行維護,還可以將這些邏輯都拆分到單文件中進行維護管理,這樣對于后續(xù)的維護和升級都能夠有更好的把控。

處理前后端交互邏輯

我們之前所有的邏輯都是在本地做的處理,現(xiàn)在我們來接入服務端的邏輯,將我們的所有數(shù)據(jù)及變更進行持久化。同時,我們也來看看在 Vue3 中,如何處理有前后端交互邏輯的場景。

假設我們有下面這么幾組接口(如下圖)

image

那么,基于這幾組接口的后端交互邏輯,我們還是用經(jīng)典的 axios 來做吧。

使用 yarn add axios 添加依賴。

這里,我們先在 src 目錄下新建一個 service,用于初始化我們用于網(wǎng)絡請求的 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 是一個在線網(wǎng)頁,我們希望每個用戶進來看到的都是自己的 Todo List。

我們來看看后臺的接口設計,他使用 key 來給 Todo Item 做分組,所以我們需要在進入頁面時,為每一個用戶生成一個獨一無二的 user key

我們先設計一個用來獲取 key 的函數(shù)吧。

這里使用 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';

// 創(chuàng)建一個引用變量,用于綁定 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)絡請求后,頁面也是不會有什么變化的,因為這個用戶目前是沒有數(shù)據(jù)的。

接下來,我們把剩下的幾個邏輯都補全。

注意:這里使用到了 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 查看的都是自己的數(shù)據(jù),并且該數(shù)據(jù)只有自己可操作。

所以,也是為了能有更好的用戶體驗,在我們所有的操作邏輯完成后,回顯數(shù)據(jù)還是用原有的邏輯。

當然,新增數(shù)據(jù)時,還是需要重新獲取列表數(shù)據(jù),因為我們操作數(shù)據(jù)時需要用到每一項的 id。

綜上所述,我們重構(gòu)后的四個函數(shù)長這樣。

// 刪除、完成、置頂?shù)倪壿嫸寂c 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;
  // 重新排序,將已經(jī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)完成的項目往前排列
  const todoItem = todoList.value.splice(index, 1);
  todoList.value.unshift(todoItem[0]);
}

// 新增 Todo Item 的邏輯都放在一處
// 創(chuàng)建一個引用變量,用于綁定輸入框
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

刷新頁面后,我們的數(shù)據(jù)依然是可以展示出來的,說明數(shù)據(jù)已經(jīng)成功做了服務端持久化啦!

小結(jié)

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

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

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

image

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

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

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

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

最后附上本次體驗的 Demo 地址。

最后一件事

如果您已經(jīng)看到這里了,希望您還是點個贊再走吧~

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

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容