尝鲜vue3.x+typeScript(toDolist)案例
是借鉴了大佬的demo代码,然后在其基础上进行改写了一些方法。就是感受了下ts的一些语法以及vue3.x的一些新特性。
参考博客:https://blog.csdn.net/qq_44812835/article/details/113195479
拉下来学习了下,简单尝试解读一下代码逻辑。大佬还用了数据库,node,我就是简单的想看下vue3.x新的一些特性以及使用方法,所以就直接屏蔽掉了接口调用那一块,改为localStorage+vueX存储的方式。
效果图:
note.vue:
原有的写法是,在这里调用hook中的事件,然后事件再去store中调用action中的方法,然后再去影响mutation,进而mutation去操作数据,我直接省略了接口请求,改为内存操作,很多地方直接在页面调用mutation的方法改变数据。
<template>
<div class="note-box">
<van-search
v-model="searchValue"
placeholder="搜索便签"
input-align="center"
@search="handleChange"
/>
<note-list
:notes="notes"
@ToAddNote="clickItem"
@longTouch="longTouch"
@loadMore="loadMore" ></note-list>
<van-popup v-model:show="show" position="bottom" :style="{ height: '10%' }" >
<div class="delete-box" @click="handleDel">
删除
</div>
</van-popup>
<div class="loading-box">
<van-loading size="24px" v-show="isLoading" vertical>加载中...</van-loading>
</div>
<van-button
round
icon="plus"
class="button"
type="primary"
@click="clickAdd"
></van-button>
</div>
<!-- <div v-model:a="a" v-model:b="b"></div> 可以多个v-model 相当于是 .sync 的操作-->
</template>
<script lang="ts">
// 平时开发的时候 都是插件给我们提示的 现在我们可以自带提示 通过defineComponent
import { GlobalState } from "@/store";
import { computed, defineComponent, PropType, reactive, ref, ssrContextKey, toRefs, watch } from "vue";
import { useStore, Store } from "vuex";
import { useRoute, useRouter } from "vue-router";
import { Note, Page } from "../../store/typings";
import * as Types from "../../store/action-types";
import NoteList from "../../components/NoteList.vue";
import useNoteState from '../../hooks/useNoteState';
import { Notify } from 'vant';
import {throttle} from '../../utils/uiils';
export default defineComponent({
name: "Note",
components: {
NoteList,
},
// emits: ["addnotes"], // 这样通过context.emit 就可以做提示
setup(props, context) {
//使用vuex
let store = useStore<GlobalState>();
// let { notes, getNotesByPage,deleteNote,isLoading,searchNote } = useNoteState(store);
//解构方法
let { notes,deleteNote,isLoading,searchNote } = useNoteState(store);
//使用路由
let router = useRouter();
//初始化数据
const state = reactive({
searchValue: "",
page: 1,
size: 15,
delId: ''
});
//获取初始化数据 并且缓存 如果store存在 就不重新请求
if(store.state.note.notes.length === 0) {
// getNotesByPage({
// page:state.page,
// size:state.size
// });
//直接操作vuex,mutation拿数据
store.commit(`note/${Types.PUSH_NOTES}`,{paylod:1})
}
// 路由跳转
const clickAdd = ()=>{
router.push({ path: "/addNote"});
}
const clickItem = (id:string)=>{
router.push({ path: "/addNote",query:{id}});
}
// 处理长按
const show = ref(false);
let id:string;
const longTouch =async (id:string)=>{
show.value = true;
state.delId = id;
}
// 处理删除
const handleDel= async()=>{
// store.commit(`note/${Types.DELETE_NOTES}`),state.delId //删除的一直是最后一条
await deleteNote(state.delId);
show.value = false;
}
// // 处理加载更多
// const loadMore = async () => {
// store.commit(`note/${Types.SET_LOADIBG}`,true);
// state.page++;
// await getNotesByPage({
// page:state.page,
// size:state.size
// });
// store.commit(`note/${Types.SET_LOADIBG}`,false);
// }
// 处理搜索请求逻辑
let handleChange = async ()=>{
if(!state.searchValue){
store.commit(`note/${Types.PUSH_NOTES}`,{paylod:1})
} else {
await searchNote(state.searchValue);
}
}
// 处理实时搜索
// let handleSearch = throttle(handleChange,1000);
// watch(()=>state.searchValue,handleSearch);
// 处理搜索
return {
notes,
clickAdd,
clickItem,
longTouch,
...toRefs(state),
show,
handleDel,
// loadMore,
isLoading,
handleChange
};
},
});
</script>
<style lang="scss" scoped>
.note-box {
width: 100%;
overflow: hidden;
flex: 1;
padding:0 0.1rem;
box-sizing: border-box;
.van-search {
padding: 0;
::v-deep .van-search__content {
background-color: rgb(237, 237, 237);
border-radius: 0.15rem;
}
background-color: rgb(247, 247, 247);
}
.delete-box{
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
color: #1989fa;
font-size: 0.23rem;
}
.button {
position: fixed;
bottom: 0.2rem;
right: 0.2rem;
}
.van-button {
width: 0.44rem;
height: 0.44rem;
}
.loading-box{
width: 100%;
height: 30px;
position: fixed;
bottom: 0rem;
}
}
</style>
addNote.vue:
<template>
<div class="add-note-box">
<van-nav-bar left-arrow @click-left="onClickLeft">
<template #right>
<van-icon name="success" size="18" @click="onClickLeft"/>
</template>
</van-nav-bar>
<van-field
class="field"
v-model="note.content"
rows="1"
autosize
type="textarea"
placeholder="请输入内容"
/>
</div>
</template>
<script lang="ts">
// 平时开发的时候 都是插件给我们提示的 现在我们可以自带提示 通过defineComponent
import router from "@/router";
import { GlobalState } from "@/store";
import { Note, Result } from "@/store/typings";
import { computed, defineComponent, onMounted, reactive, toRefs, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { Store, useStore } from "vuex";
import * as Types from "../../store/action-types";
import useNoteStore from "../../hooks/useNoteState"; // 引入组定义store hooks
import * as NoteAPI from "../../api/notes";
export default defineComponent({
name: "ToDo",
setup(props, context) {
let store = useStore<GlobalState>();
// let { notes, addNote ,updateNote ,deleteNote} = useNoteStore(store);
let { notes ,deleteNote} = useNoteStore(store);
const state = reactive({
note: {
content: "",
dates: "",
},
id: "",
oldContent:''
});
const routr = useRouter();
const rout = useRoute();
const date = new Date();
// 点击返回提交数据(存在id为更新,并且如果存在id,内容修改为空则为删除,不存在id为添加)
const onClickLeft = async() => {
if (!state.note.content.trim()) {
// 如果没有数据 判断是点击详情进入 还是点击添加进入
if (state.id) {
// 如果为点击详情进入,则删除
await deleteNote(state.id);
}
router.go(-1);
return;
}
// 有数据需要判断是添加 还是更新
state.note.dates = `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`;
if (state.id) {
if (state.oldContent !== state.note.content){ // 如果点击进来没有改变就不更新
// 更新
// await updateNote({
// id:state.id, note:state.note
// });
//直接操作vuex,mutation修改数据
store.commit(`note/${Types.UPDATE_NOTES}`,{id:state.id,note:state.note})
}
} else {
// await addNote(state.note);
//直接操作vuex,mutation新增数据
store.commit(`note/${Types.ADD_NOTES}`,state.note)
}
router.go(-1);
};
// 初始化数据
const initNote = () => {
const notes = store.state.note.notes;
const id = rout.query.id;
if (!id) return; // 如果没有id说明是点击添加
notes.forEach((item) => {
// 首次从store中获取
if (item._id === id) {
state.note.content = item.content;
state.oldContent = item.content; // 记录初始值
state.id = id;
}
});
if (!state.note.content) {
initNote()
//如果刷新页面从后端请求
// NoteAPI.getNoteById<Result<Note>>(id as string).then((data) => {
// state.note.content = data.data.content;
// state.oldContent = data.data.content;
// state.id = data.data._id!;
// });
}
};
onMounted(() => {
initNote();
});
return {
onClickLeft,
...toRefs(state),
};
},
});
</script>
<style lang="scss" scoped>
.add-note-box {
width: 100%;
overflow-x: hidden;
overflow-y: auto;
flex: 1;
background-color: #fff;
}
.field{
font-size: 0.22rem;
}
</style>
hooks中的事件:
之前所有的事件都是放开的,我注释掉了,因为上面部分操作是直接操作的store,没有经过这里。
这里只剩搜索和删除,还有暴露notes(便签了列表)出去
import { GlobalState } from "@/store";
import { Store } from 'vuex';
import { computed } from 'vue';
import { Page, Note } from '@/store/typings';
import * as Types from "../store/action-types";
// 封装note vuex相关操作 这样就解决了options API中 计算属性 方法等等要写在固定位置
// 这样的写法 就可以把很多功能封装到一个函数中
export default function useNote(store: Store<GlobalState>) {
let notes = computed(() => store.state.note.notes); // 使用computed可以使数据变成响应式
let isLoading = computed(()=>store.state.note.isLoading);
// function getNotesByPage(paylod:Page) {
// store.dispatch(`note/${Types.PUSH_NOTES}`, paylod);
// }
// function addNote(paylod:Note) { // 为了获取返回值 使用异步函数封装
// return store.dispatch(`note/${Types.ADD_NOTES}`, paylod);
// }
function searchNote(paylod:string) { // 为了获取返回值 使用异步函数封装
return store.dispatch(`note/${Types.SEARCH_NOTE}`, paylod);
}
// function updateNote(payload:any){
// return store.dispatch(`note/${Types.UPDATE_NOTES}`,payload);
// }
function deleteNote(payload:string){
return store.dispatch(`note/${Types.DELETE_NOTES}`,payload);
}
return {
notes,
// getNotesByPage,
// addNote,
// updateNote,
deleteNote,
isLoading,
searchNote
};
}
还有个长按的方法:
import { onMounted, onUnmounted, Ref } from 'vue';
export default function (arr:Ref<null | HTMLElement>[],callbacks: Function) {
let timer: any = null;
let isMoving = false;
const touchStart = (e: any) => {
// 类似与防抖
console.log('start')
let id = e.targetTouches[0].target.id;
timer = setTimeout(() => {
// 长按1s
if(!isMoving){
callbacks(id); // 执行长按的回调
}
}, 700);
}
const touchEnd = (e: any) => {
clearTimeout(timer);
isMoving = false;
}
const touchMove = () => {
isMoving = true;
}
onMounted(()=>{
arr.forEach((item)=>{
item.value?.addEventListener('touchstart',touchStart);
item.value?.addEventListener('touchend',touchEnd);
item.value?.addEventListener('touchmove',touchMove);
});
});
onUnmounted(()=>{
arr.forEach((item)=>{
item.value?.removeEventListener('touchstart',touchStart);
item.value?.removeEventListener('touchend',touchEnd);
item.value?.removeEventListener('touchmove',touchMove);
})
})
}
store中的方法:
此前的写法是,页面->hook->store->action->api->mutation->state,
现在部分操作直接省略,页面->store->mutation->state
import { Module } from 'vuex'
import store, { GlobalState } from '../index'
import { NoteState, Note, Page, Result } from '../typings'
// import * as NoteAPI from '../../api/notes'
import * as Types from '../action-types'
const state: NoteState = {
notes: [],
isRequestError: false,
isLoading: false
}
// 需要传入两个泛型 一个是 本身的state类型 和 全局的state类型 这样用的时候就可以提示了
const note: Module<NoteState, GlobalState> = {
namespaced: true,
state,
mutations: {
//查询
[Types.SET_NOTE](state, payload: Note[]) {
state.notes = payload
},
//增
[Types.ADD_NOTES](state, payload: Note) { // 添加一条便签
payload._id=(new Date()).toString()
payload = JSON.parse(JSON.stringify(payload))
if(localStorage.noteList.length&&JSON.parse(localStorage.noteList)){
let arr = JSON.parse(localStorage.noteList)
arr.push(payload)
localStorage.noteList = JSON.stringify(arr)
}else{
let arr =[]
arr.push(payload)
localStorage.noteList = JSON.stringify(arr)
}
state.notes = JSON.parse(localStorage.noteList)
},
//修改
[Types.UPDATE_NOTES](state, payload: any) {
state.notes.forEach((note:Note) => {
if(note._id === payload.id){
note.content = payload.note.content;
note.dates = payload.note.dates;
}
});
localStorage.noteList = JSON.stringify(state.notes)
},
//删除
[Types.DELETE_NOTES](state, payload: string) {
const index = state.notes.findIndex((note:Note) => {
return note._id === payload;
});
state.notes.splice(index,1);
},
[Types.PUSH_NOTES](state, payload: Note[]) {
state.notes=JSON.parse(localStorage.noteList)
}
// [Types.SET_ERROR](state, payload: boolean) {
// state.isRequestError = payload
// },
// [Types.SET_LOADIBG](state,payload:boolean){
// state.isLoading = payload
// }
},
actions: {
// 分页请求
[Types.PUSH_NOTES]({ commit }, payload: Page) {
// NoteAPI.getNotes<Result<Note[]>>(payload.page, payload.size).then(data => {
if(payload.page === 1) {
// commit(Types.SET_NOTE, data.data);
if( localStorage.noteList){
commit(Types.SET_NOTE, JSON.parse(localStorage.noteList));
}else{
localStorage.noteList = []
commit(Types.SET_NOTE, []);
}
// }else{
// commit(Types.PUSH_NOTES, data.data);
}
// });
},
[Types.SEARCH_NOTE]({ commit }, payload: string){
// NoteAPI.getNoteListByContent<Result<Note[]>>(payload).then(data=>{
// commit(Types.SET_NOTE, data.data);
if( localStorage.noteList){
let arr = JSON.parse(localStorage.noteList)
arr.forEach((item:any)=>{
if(item.content.search(payload)>-1){
commit(Types.SET_NOTE, [item]);
}else{
return
}
})
}else{
return
}
// })
},
// 添加note
[Types.ADD_NOTES]({ commit }, payload: Note) {
// return NoteAPI.addNote<Result<Note>>(payload).then(data => {
// commit(Types.ADD_NOTES, data.data);
// });
payload._id=(new Date()).toString()
if(localStorage.noteList.length&&JSON.parse(localStorage.noteList)){
let arr = JSON.parse(localStorage.noteList)
arr.push(payload)
localStorage.noteList = JSON.stringify(arr)
commit(Types.ADD_NOTES, payload);
}else{
let arr =[]
arr.push(payload)
localStorage.noteList = JSON.stringify(arr)
commit(Types.ADD_NOTES, payload);
}
},
// 修改note
[Types.UPDATE_NOTES]({commit,state},payload:any) {
// return NoteAPI.updateNote(payload.id,payload.note).then(data => {
// commit(Types.UPDATE_NOTES, {id:payload.id,note:payload.note}); // 更新数据
// });
if(JSON.parse(localStorage.noteList)){
let arr = JSON.parse(localStorage.noteList)
arr.forEach((item:any)=>{
if(item._id==payload.id){
item.content = payload.note
}
})
localStorage.noteList = JSON.stringify(arr)
commit(Types.UPDATE_NOTES, {id:payload.id,note:payload.note}); // 更新数据
}
},
// 删除note
[Types.DELETE_NOTES]({ commit,state }, payload: string) {
// return NoteAPI.deleteNote<Result<number>>(payload).then(data => {
// commit(Types.DELETE_NOTES, payload); // 更新数据
// })
if(JSON.parse(localStorage.noteList)){
let arr = JSON.parse(localStorage.noteList)
let index = arr.findIndex((item:any)=>item==payload)
arr.splice(index,1)
localStorage.noteList = JSON.stringify(arr)
commit(Types.DELETE_NOTES, payload); // 更新数据
}
},
}
}
export default note
另外的store中的文件,我的理解是,定义类型还有命名字典。
定义类型:
命名字典:
虽然这样子看起来还是很奇怪,在store中的使用方式,和vue2.0不一样。可能这就是ts的魅力吧
store的index.ts:
这个就和常规的没啥很大区别了
import { createStore } from 'vuex'
import { NoteState } from './typings'
import note from './modules/note'
export interface GlobalState {
note: NoteState
}
// 同样也变成了函数的写法
const store = createStore<GlobalState>({
mutations: {
},
actions: {
},
modules: {
note
}
})
export default store
数据处理逻辑:
以新增为例(新增和修改共用页面):
页面:
注意那个写法,其实也就是对应执行方法名称对应的方法
store.commit(`note/${Types.ADD_NOTES}`,state.note)
store:
再看这里面的方法名称就懂了:
//增
[Types.ADD_NOTES](state, payload: Note) { // 添加一条便签
payload._id=(new Date()).toString()
payload = JSON.parse(JSON.stringify(payload))
if(localStorage.noteList.length&&JSON.parse(localStorage.noteList)){
let arr = JSON.parse(localStorage.noteList)
arr.push(payload)
localStorage.noteList = JSON.stringify(arr)
}else{
let arr =[]
arr.push(payload)
localStorage.noteList = JSON.stringify(arr)
}
state.notes = JSON.parse(localStorage.noteList)
},
ts加持多了很多类型限制和规范。