使用Vue3 composition-api重寫一個抽象可複用的增刪改查頁面

vue3.0 beta版本已經發布一段時間了,嘗試着用composition-api來重寫一個簡單的後臺管理系統中的增刪改查。javascript

對於經常使用的增刪改查的後臺管理頁面,一般的爲表格+詳情頁的模式,主要包含以下幾個功能:html

  • 表格用於展現數據內容
  • 點擊表格中的一項,可以彈出詳情頁進行編輯。
  • 保存或取消更新表格數據。

這種經常使用的模式在vue2下,此次試着用vue3的api來改寫,讓咱們拋棄被吐槽了不少的mixin,擁抱hooks。vue

使用@vue/cli建立項目

首先確認@vue/cli爲最新版本4.3.1,不然升級的時候可能會出現錯誤,執行:java

vue create vue3-admin-demo
cd vue3-admin-demo
vue add vue-next
複製代碼

安裝後檢查package.json中的vue版本爲3.0.0就成功了,注意建立的時候,若是有vuex及vue-router,須要先手動勾選後,再執行vue add vue-next,vuex及vue-router便會自動升級到4.0版本。react

寫一個簡單的管理頁面

初始化

main.js中初始化vue的方式也有了區別,vue再也不經過export default 方式暴露,而是使用對應的api,這裏使用vuex及vue-router的use引入也是用相似鏈式調用的方式,以下:webpack

import { createApp } from 'vue';
import App from './App.vue'
import router from './router'
import store from './store'
createApp(App).use(router).use(store).mount('#app')
複製代碼

引入vue-router後,App.vue即可以直接使用router-link,咱們在默認的模板裏增長一條路由信息:ios

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link> |
      <router-link to="/staff">Staff</router-link>
    </div>
    <router-view/>
  </div>
</template>

複製代碼

建立路由

views目錄建立staff.vue的頁面,而後去router/index.js更新一下路由:git

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  },
  {
    path: '/staff',
    name: 'Staff',
    component: () => import(/* webpackChunkName: "about" */ '../views/Staff.vue')
  }
]
複製代碼

在Staff.vue中隨便寫點什麼,預覽一下:github

OK,路由準備完成,下面開始編寫內部表格

編寫頁面模板

這裏咱們簡單定義數據爲一我的員列表,結構以下,這裏跟vue2沒有太大區別。web

<div class="home">
    <button>新增</button>
    <table>
      <thead>
        <th>姓名</th>
        <th>部門</th>
        <th>職位</th>
        <th>入職日期</th>
        <th>操做</th>
      </thead>
      <tbody>
        <tr>
          <td>name</td>
          <td>department</td>
          <td>position</td>
          <td>date</td>
          <td>
            <button>編輯</button>
            <button>刪除</button>
          </td>
        </tr>
      </tbody>
    </table>
    <div class="dialog-detail" v-show="isShowDetail">
      <div class="form-item">
        <span class="label">姓名:</span>
        <input type="text" v-model="state.form.name">
      </div>
      <div class="form-item">
        <span class="label">部門:</span>
        <input type="text" v-model="state.form.department">
      </div>
      <div class="form-item">
        <span class="label">職位:</span>
         <input type="text" v-model="state.form.position">
      </div>
      <div class="form-item">
        <span class="label">入職日期:</span>
         <input type="text" v-model="state.form.date">
      </div>
      <div class="btn-group">
        <button @click="confirmItem(state.form)">確認</button>
        <button @click="cancelEdit">取消</button>
      </div>
    </div>
    
</html>
複製代碼

js部分採用setupAPI進行改寫,Vue3中使用refreactive來定義響應式對象,其中ref能夠定義簡單的數字,字符串等變量,例如

import { ref } from 'vue'
const count = ref(0);
const isCancel = ref(true);
複製代碼

若是須要對複雜類型的變量如object之類的,就須要用reactive方法來進行定義,咱們先定義一個form對象,用於綁定詳情頁的數據,用於後續編輯和新增。

import { reactive } from 'vue';
import { usePageData } from '../components/PageData'; 
import { fetchStaff } from '../api';
export default {
  name: 'Staff',
  setup() {
    // form對象用於v-model綁定
    const state = reactive({
      form: {
        name: '',
        department: '',
        position: '',
        date: ''
      }
    })
    const pageData = usePageData(fetchStaff, state.form);
    return {
      state,
      ...pageData,
    }
  }
}
複製代碼

這裏看到了使用了一個usePageDatafetchStaff,是實現的關鍵,

複用邏輯抽象

能夠看到,staff.vue的完整代碼比較簡潔,咱們考慮到:將來須要拓展的話,不一樣表格,只要定義標準API返回的數據格式,對於獲取數據查看詳情, 編輯確認取消,之類的邏輯幾乎是如出一轍的,對於不一樣頁面來講,只有api地址不一樣,其餘邏輯均可以複用和抽象。

在之前,可能會mixin方式來進行混入,不一樣頁面引入這個mixin,調整一下data裏面的api地址便可。但當Mixin變的多的時候,就會存在不少問題,如配置項過於分散,變量,方法難以追蹤,不知道是哪一個mix進來的,重名的時候沒有很好的辦法處理等。

Vue3最終都是setup,若是咱們使用函數式的方法,把相關的邏輯都封裝在一個函數內,最終暴露給須要使用setup便可。在React hooks中的也是這樣相似的思想。因而咱們有了usePageData

usePageData

這個方法其實相似vue2中的mixin,可是更加靈活,函數式的編程思路也讓邏輯上更加統一。咱們先添加一些操做數據的方法:

export const usePageData = (fetchApi, form) => {
    // 表格操做方法
    const addItem = () => {};
    const editItem = index => {};
    const deleteItem = index => {};
    // 詳情操做方法
    const confirmItem = item => {};
    const cancelEdit = () => {};
    return {
        addItem,
        editItem,
        deleteItem,
        confirmItem,
        cancelEdit
    }
})
複製代碼

return出去的值,能夠直接給頁面模板中使用,對應的修改一下staff.vue中的按鈕事件:

<button @click="addItem">新增</button>
<button @click="editItem(index)">編輯</button>
<button @click="deleteItem(index)">刪除</button>
...
 <div class="btn-group">
    <button @click="confirmItem(state.form)">確認</button>
    <button @click="cancelEdit">取消</button>
</div>
複製代碼

繼續回到pagedata.js: 初始化的時候須要獲取接口數據,這裏不一樣對應頁面的api不一樣,因此usePageData增長一個fetchApi參數,在mounted中完成,vue3須要引入onMounted方法,添加代碼:

import { onMounted } from 'vue';
export const usePageData = (fetchApi, form) => {
   ...
    onMounted(async () => {
        let resList = await fetchApi();
        console.log(resList)
    });
    return {
        ...
    }
複製代碼

fetchApi:

回到staff.vue,調用的時候是這樣:

import { fetchStaff } from '../api';
const pageData = usePageData(fetchStaff, state.form);
複製代碼

先看看fetchStaff,主要是對api的封裝,用於請求接口:

api.js中fetchStaff 對底層接口封裝,這裏使用本地數據來模擬

import request from '../utils/request';

export const fetchStaff = query => {
    return request({
        url: './staff.json',
        method: 'get',
        params: query
    });
};
複製代碼

request.js 使用axios做爲ajax的簡單封裝

import axios from 'axios';

const service = axios.create({
    timeout: 5000
});

service.interceptors.request.use(
    config => {
        return config;
    },
    error => {
        console.log(error);
        return Promise.reject();
    }
);

service.interceptors.response.use(
    response => {
        if (response.status === 200) {
            return response.data;
        } else {
            Promise.reject();
        }
    },
    error => {
        console.log(error);
        return Promise.reject();
    }
);

export default service;

複製代碼

到如今能夠測試console.log(resList),能夠驗證數據是否正確返回了。

引入Store

在store/index.js中添加代碼,vue3中store經過createStore方式建立,完整代碼以下:

import { createStore } from "vuex";

export default createStore({
  state: {
    pageData: {}, // 保存當前頁面列表數據和總條目
    activeIndex: null //當前激活的項(即正在編輯的)
  },
  getters: {
    // 當前正在編輯的項,根據activeIndex尋找
    activeItem: state => {
      const {list} = state.pageData;
      if(state.activeIndex !== null
        && state.activeIndex !== undefined
        && state.activeIndex > -1
        && list
        && list.length
      ) {
        return list[state.activeIndex];
      }
      return null;
  },
  },
  mutations: {
    // 設置正在編輯的下標
    SET_ACTIVE_ITEM(state, index) {
      state.activeIndex = index;
    },
    // 獲取總體頁面數據
    GET_PAGE_DATA(state, payload) {
      state.pageData = payload;
    },
    // 詳情頁中的「肯定」操做
    // 須要判斷是否存在Item參數,用於區分是編輯仍是新增的狀況
    CONFIRM_EDIT_ITEM(state, item) {
      const {activeIndex, pageData} = state;
      const {list} = pageData;
      if (!list) {
        return;
      }
      if (activeIndex) {
        Object.assign(list[activeIndex], item);
      } else {
        list.push(Object.assign({}, item));
      }
      // 肯定完成後清空activeIndex 
      state.activeIndex = null;
    },
    // 詳情頁中的「取消」操做,清空當前正在編輯的下標
    CLEAR_ACTIVE_ITEM(state) {
      state.activeIndex = null;
    },
    // 刪除數據中的一項
    DELETE_ITEM(state, index) {
      const {list} = state.pageData;
      list.splice(index, 1);
    }
  },
  actions: {
    GET_PAGE_DATA({ commit }, payload) {
      commit('GET_PAGE_DATA', payload)
    },
    SET_ACTIVE_ITEM({ commit }, index) {
      commit('SET_ACTIVE_ITEM', index)
    },
    CONFIRM_EDIT_ITEM({ commit }, item) {
      commit('CONFIRM_EDIT_ITEM', item)
    },
    CLEAR_ACTIVE_ITEM({ commit }) {
      commit('CLEAR_ACTIVE_ITEM')
    },
    DELETE_ITEM({commit}, index) {
      commit('DELETE_ITEM', index)
    }
  }
});
複製代碼

在pageData中引入store

對按鈕操做進行修改,pageData完整代碼以下:

import { onMounted, computed, watch, ref } from 'vue';
// 經過useStore引入
import { useStore } from 'vuex';
export const usePageData = (fetchApi, form) => {
  // 對form進行拷貝一份原始值,用於保存後對數據的清空。
  const initForm = Object.assign({}, form);
  const store = useStore();
  
  // 這裏store中的頁面數據,用於顯示
  const pageData = computed(() => store.state.pageData);
  const activeIndex = computed(() => store.state.activeIndex);
  
  // activeItem即存在編輯中的變量,這裏須要把form內的值更新成當前激活的數據
  const activeItem = computed(() => store.getters.activeItem);
  watch(activeItem, () => {
    if (!activeItem.value) return;
    for (let key in form) {
      form[key] = activeItem.value[key];
    }
  });
  
  let isShowDetail = ref(false);
  const editItem = index => {
    isShowDetail.value = true;
    store.dispatch('SET_ACTIVE_ITEM', index);
  };
  const addItem = () => {
    isShowDetail.value = true;
    store.dispatch('SET_ACTIVE_ITEM', null);
  };
  const deleteItem = index => {
    store.dispatch('DELETE_ITEM', index);
  };
  const confirmItem = item => {
    isShowDetail.value = false;
    store.dispatch('CONFIRM_EDIT_ITEM', item);
    Object.assign(form, initForm);
  };
  const cancelEdit = () => {
    isShowDetail.value = false;
    store.dispatch('CLEAR_ACTIVE_ITEM');
    Object.assign(form, initForm);
  };
  onMounted(async () => {
    let resList = await fetchApi();
    store.dispatch('GET_PAGE_DATA', resList);
  });

  return {
    isShowDetail,
    pageData,
    activeIndex,
    editItem,
    addItem,
    confirmItem,
    cancelEdit,
    deleteItem
  }
}
複製代碼

相關的數據和變量都處理好了,更新一下頁面模板:

<template>
  <div class="home">
    <button @click="addItem">新增</button>
    <table>
      <thead>
        <th>姓名</th>
        <th>部門</th>
        <th>職位</th>
        <th>入職日期</th>
        <th>操做</th>
      </thead>
      <tbody>
        <tr v-for="(staff, index) in pageData.list" :key="staff.id">
          <td>{{staff.name}}</td>
          <td>{{staff.department}}</td>
          <td>{{staff.position}}</td>
          <td>{{staff.date}}</td>
          <td>
            <button @click="editItem(index)">編輯</button>
            <button @click="deleteItem(index)">刪除</button>
          </td>
        </tr>
      </tbody>
    </table>
    <div>總數:{{pageData.total}}</div>
    <div class="dialog-detail" v-show="isShowDetail">
      <div class="form-item">
        <span class="label">姓名:</span>
        <input type="text" v-model="state.form.name">
      </div>
      <div class="form-item">
        <span class="label">部門:</span>
        <input type="text" v-model="state.form.department">
      </div>
      <div class="form-item">
        <span class="label">職位:</span>
         <input type="text" v-model="state.form.position">
      </div>
      <div class="form-item">
        <span class="label">入職日期:</span>
         <input type="text" v-model="state.form.date">
      </div>
      <div class="btn-group">
        <button @click="confirmItem(state.form)">確認</button>
        <button @click="cancelEdit">取消</button>
      </div>
    </div>
    
  </div>
</template>

複製代碼

效果

最後看一下結果,頁面初始化加載默認數據:

新增:

編輯:

刪除:

總結

經過composition-api方式來組織代碼,帶來了新的編程體驗,可是也對編程者的要求變得更高,如何在可維護,可複用,可理解中找到平衡點,也是對咱們的一個挑戰。

本文案例完整代碼見: github.com/ccxryan/vue…

一些參考連接:

相關文章
相關標籤/搜索