vue3.0 beta版本已经发布一段时间了,尝试着用composition-api来重写一个简单的后台管理系统中的增删改查。javascript
对于经常使用的增删改查的后台管理页面,一般的为表格+详情页的模式,主要包含以下几个功能:html
这种经常使用的模式在vue2下,此次试着用vue3的api来改写,让咱们抛弃被吐槽了不少的mixin,拥抱hooks。vue
首先确认@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
这里咱们简单定义数据为一我的员列表,结构以下,这里跟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中使用ref
和reactive
来定义响应式对象,其中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,
}
}
}
复制代码
这里看到了使用了一个usePageData
和fetchStaff
,是实现的关键,
能够看到,staff.vue的完整代码比较简洁,咱们考虑到:将来须要拓展的话,不一样表格,只要定义标准API返回的数据格式,对于获取数据,查看详情, 编辑,确认,取消,之类的逻辑几乎是如出一辙的,对于不一样页面来讲,只有api地址不一样,其余逻辑均可以复用和抽象。
在之前,可能会mixin方式来进行混入,不一样页面引入这个mixin,调整一下data里面的api地址便可。但当Mixin变的多的时候,就会存在不少问题,如配置项过于分散,变量,方法难以追踪,不知道是哪一个mix进来的,重名的时候没有很好的办法处理等。
Vue3最终都是setup,若是咱们使用函数式的方法,把相关的逻辑都封装在一个函数内,最终暴露给须要使用setup便可。在React hooks中的也是这样相似的思想。因而咱们有了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 {
...
}
复制代码
回到staff.vue,调用的时候是这样:
import { fetchStaff } from '../api';
const pageData = usePageData(fetchStaff, state.form);
复制代码
先看看fetchStaff
,主要是对api的封装,用于请求接口:
import request from '../utils/request';
export const fetchStaff = query => {
return request({
url: './staff.json',
method: 'get',
params: query
});
};
复制代码
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/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完整代码以下:
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…
一些参考连接: