共计 20560 个字符,预计需要花费 52 分钟才能阅读完成。
上一篇文章介绍了整体架构,接下来说说怎么依照上图的分层构造实现上面的增删改查的性能。
代码构造
vue
userManage
└── List
├── api.ts
├── EditModal
│ ├── index.tsx
│ ├── index.vue
│ ├── model.ts
│ ├── presenter.tsx
│ └── service.ts
├── index.module.less
├── index.tsx
├── index.vue
├── model.ts
├── presenter.tsx
└── service.ts
react
userManage
└── List
├── api.ts
├── EditModal
│ ├── index.tsx
│ ├── model.ts
│ ├── presenter.tsx
│ └── service.ts
├── index.module.less
├── index.tsx
├── model.ts
├── presenter.tsx
└── service.ts
model
申明页面数据
vue
// vue
import {reactive, ref} from "vue";
import {IFetchUserListResult} from "./api";
export const useModel = () => {const filterForm = reactive({ name: ""});
const userList = reactive<{value: IFetchUserListResult["result"]["rows"] }>({value: [],
});
const pagination = reactive({
size: 10,
page: 1,
total: 0,
});
const loading = ref(false);
const runFetch = ref(0);
const modalInfo = reactive<{
action: "create" | "edit";
title: "创立" | "编辑";
visible: boolean;
data?: IFetchUserListResult["result"]["rows"][0];
}>({
action: "create",
title: "创立",
visible: false,
data: undefined,
});
return {
filterForm,
userList,
pagination,
loading,
runFetch,
modalInfo,
};
};
export type Model = ReturnType<typeof useModel>;
react
// react
import {useImmer as useState} from 'use-immer';
import {IFetchUserListResult} from './api';
export const useModel = () => {const [filterForm, setFilterForm] = useState({name: ''});
const [userList, setUserList] = useState<
IFetchUserListResult['result']['rows']
>([]);
const [pagination, setPagination] = useState({size: 10, page: 1, total: 0});
const [loading, setLoading] = useState(false);
const [runFetch, setRunFetch] = useState(0);
const [modalInfo, setModalInfo] = useState<{
action: 'create' | 'edit';
title: '创立' | '编辑';
visible: boolean;
data?: IFetchUserListResult['result']['rows'][0];
}>({
action: 'create',
title: '创立',
visible: false,
data: undefined,
});
return {
filterForm,
setFilterForm,
userList,
setUserList,
pagination,
setPagination,
loading,
setLoading,
runFetch,
setRunFetch,
modalInfo,
setModalInfo,
};
};
export type Model = ReturnType<typeof useModel>;
看过几个前端整洁架构的我的项目,大部分都会把 model 分为 业务模型(畛域模型) 或者 视图模型。
业务模型(畛域模型) 能够指用于表白业务内容的数据。例如淘宝的业务模型是【商品】,博客的业务模型是【博文】,推特的业务模型是【推文】。能够了解为经典 MVC 中的 Model,蕴含了名称、形容、工夫、作者、价格等【真正意义上的】数据字段内容。
视图模型 则是 MVVM 昌盛后的新概念。要实现一个残缺的 Web 利用,除了数据外,还有 UI 交互中十分多的状态。例如:弹框是否关上、用户是否正在输出、申请 Loading 状态是否须要显示、图表数据分类是否须要显示追加字段、和用户输出时文本的大小和款式的动静扭转……这些和具体数据字段无关,但对前端理论业务场景十分重要的视图状态,能够认为是一种视图模型。
业务模型(畛域模型)的下限太高,站在业务的角度去深刻的开掘、演绎,有一个高大上的名词:畛域驱动开发。不论是前端还是后端,畛域驱动开发的老本太高,对开发人员的要求也高。花了大量的工夫去划分畛域模型,最终后果可能是弄出各种互相耦合的模型,还不如意大利面式的代码好保护。很多整洁构造的我的项目都是抉择商品,购物车作为例子,因为这些业务曾经被玩透了,比拟容易就把业务模型弄出来。
回到文章题目中写的 晋升前端开发体验,显然面向业务畛域去划分模型并不是一种好的开发体验。为了防止意大利面式的代码,还是抉择进行模型的划分,不过不是站在业务畛域的角度区划分,而是间接从拿到的设计稿或者原型着手(毕竟前端大部分的工作还是面向设计稿或者原型编程),间接把页面上须要用到的数据放到模型中。
比方这篇文章所用的例子,一个增删改查的页面。查问条件 filterForm
,列表数据 userList
,分页信息 pagination
,加载状态 loading
,新增批改弹框 modalInfo
。这几个字段就是这个页面的模型数据,不分什么业务模型,视图模型,全副放在一起。
runFetch
这个变量是为了把副作用依赖进行收口。从交互的角度来说,查问条件或者分页信息变了,应该触发网络申请这个副作用,刷新页面数据。如果查问条件由十几个,那么副作用的依赖就太多了,代码既不好保护也不简洁,所以查问条件或者分页数据变的时候,同时更新 runFetch
,副作用只依赖于 runFetch
即可。
看下面的 model
代码,其实就是一个自定义 hooks。也就是说咱们在 model
层间接依赖了框架 react 或者 vue,违反了整洁架构的标准。这是从 开发体验 和 技术选型 两方面思考,要在不引入 mobx,@formily/reactive 等第三方库的前提下实现批改 model 数据就能间接触发视图的更新,应用自定义 hooks 是最便捷的。
model 中没有限度更新数据形式,内部能间接读写模型数据。因为 model 只是以后页面中应用,没必要为了更新某个字段独自写一个办法给内部去调用。同时也不倡议在 model 中写办法,放弃 model 的洁净,后续保护或者需要变更,会有更好的开发体验,逻辑办法放到前面会介绍的 service
层中。
react 写的 model 不能在 vue 我的项目中复用,反之一样。然而在跨端开发中,model 还是能够复用的,比方如果技术栈是 react,web 端和 RN 端是能够复用 model 层的。如果用了 Taro 或者 uni-app 框架,model 层和 service 层不会受到不同端适配代码的净化,在 presenter 层或者 view 层做适配即可。
service
vue
// vue
import {delUser, fetchUserList} from "./api";
import {Model} from "./model";
export default class Service {
private model: Model;
constructor(model: Model) {this.model = model;}
async getUserList() {if (this.model.loading.value) {return;}
this.model.loading.value = true;
const res = await fetchUserList({
page: this.model.pagination.page,
size: this.model.pagination.size,
name: this.model.filterForm.name,
}).finally(() => {this.model.loading.value = false;});
if (res) {
this.model.userList.value = res.result.rows;
this.model.pagination.total = res.result.total;
}
}
changePage(page: number, pageSize: number) {if (pageSize !== this.model.pagination.size) {
this.model.pagination.page = 1;
this.model.pagination.size = pageSize;
this.model.runFetch.value += 1;
} else {
this.model.pagination.page = page;
this.model.runFetch.value += 1;
}
}
changeFilterForm(name: string, value: any) {(this.model.filterForm as any)[name] = value;
}
resetForm() {
this.model.filterForm.name = "";
this.model.pagination.page = 1;
this.model.runFetch.value += 1;
}
doSearch() {
this.model.pagination.page = 1;
this.model.runFetch.value += 1;
}
edit(data: Model["modalInfo"]["data"]) {
this.model.modalInfo.action = "edit";
this.model.modalInfo.data = JSON.parse(JSON.stringify(data));
this.model.modalInfo.visible = true;
this.model.modalInfo.title = "编辑";
}
async del(id: number) {
this.model.loading.value = true;
await delUser({id: id}).finally(() => {this.model.loading.value = false;});
}
}
react
// react
import {delUser, fetchUserList} from './api';
import {Model} from './model';
export default class Service {
private static _indstance: Service | null = null;
private model: Model;
static single(model: Model) {if (!Service._indstance) {Service._indstance = new Service(model);
}
return Service._indstance;
}
constructor(model: Model) {this.model = model;}
async getUserList() {if (this.model.loading) {return;}
this.model.setLoading(true);
const res = await fetchUserList({
page: this.model.pagination.page,
size: this.model.pagination.size,
name: this.model.filterForm.name,
}).catch(() => {});
if (res) {this.model.setUserList(res.result.rows);
this.model.setPagination((s) => {s.total = res.result.total;});
this.model.setLoading(false);
}
}
changePage(page: number, pageSize: number) {if (pageSize !== this.model.pagination.size) {this.model.setPagination((s) => {
s.page = 1;
s.size = pageSize;
});
this.model.setRunFetch(this.model.runFetch + 1);
} else {this.model.setPagination((s) => {s.page = page;});
this.model.setRunFetch(this.model.runFetch + 1);
}
}
changeFilterForm(name: string, value: any) {this.model.setFilterForm((s: any) => {s[name] = value;
});
}
resetForm() {this.model.setFilterForm({} as any);
this.model.setPagination((s) => {s.page = 1;});
this.model.setRunFetch(this.model.runFetch + 1);
}
doSearch() {this.model.setPagination((s) => {s.page = 1;});
this.model.setRunFetch(this.model.runFetch + 1);
}
edit(data: Model['modalInfo']['data']) {this.model.setModalInfo((s) => {
s.action = 'edit';
s.data = data;
s.visible = true;
s.title = '编辑';
});
}
async del(id: number) {this.model.setLoading(true);
await delUser({id}).finally(() => {this.model.setLoading(false);
});
}
}
service 是一个纯类,通过构造函数注入 model (如果是 react 技术栈,presenter 层调用的时候应用单例办法,防止每次 re-render 都生成新的实例),service 的办法内是相应的业务逻辑,能够间接读写 model 的状态。
service 要尽量放弃“整洁”,不要间接调用特定环境,端的 API,尽量遵循 依赖倒置准则。比方 fetch,WebSocket,cookie,localStorage 等 web 端原生 API 以及 APP 端 JSbridge,不倡议间接调用,而是形象,封装成独自的库或者工具函数,保障是可替换,容易 mock 的。还有 Taro,uni-app 等框架的 API 也不要间接调用,能够放到 presenter 层。还有组件库提供的命令式调用的组件,也不要应用,比方下面代码中的删除办法,调用 api 胜利后,不会间接调用组件库的 Toast 进行提醒,而是在 presenter 中调用。
service 保障足够的“整洁”,model 和 service 是能够间接进行单元测试的,不须要去关怀是 web 环境还是小程序环境。
presenter
presenter 调用 service 办法解决 view 层事件。
vue
// vue
import {watch} from "vue";
import {message, Modal} from "ant-design-vue";
import {IFetchUserListResult} from "./api";
import Service from "./service";
import {useModel} from "./model";
const usePresenter = () => {const model = useModel();
const service = new Service(model);
watch(() => model.runFetch.value,
() => {service.getUserList();
},
{immediate: true},
);
const handlePageChange = (page: number, pageSize: number) => {service.changePage(page, pageSize);
};
const handleFormChange = (name: string, value: any) => {service.changeFilterForm(name, value);
};
const handleSearch = () => {service.doSearch();
};
const handleReset = () => {service.resetForm();
};
const handelEdit = (data: IFetchUserListResult["result"]["rows"][0]) => {service.edit(data);
};
const handleDel = (data: IFetchUserListResult["result"]["rows"][0]) => {
Modal.confirm({
title: "确认",
content: "确认删除以后记录?",
cancelText: "勾销",
okText: "确认",
onOk: () => {service.del(data.id).then(() => {message.success("删除胜利");
service.getUserList();});
},
});
};
const handleCreate = () => {
model.modalInfo.visible = true;
model.modalInfo.title = "创立";
model.modalInfo.data = undefined;
};
const refresh = () => {service.getUserList();
};
return {
model,
handlePageChange,
handleFormChange,
handleSearch,
handleReset,
handelEdit,
handleDel,
handleCreate,
refresh,
};
};
export default usePresenter;
react
// react
import {message, Modal} from 'antd';
import {useEffect} from 'react';
import {IFetchUserListResult} from './api';
import {useModel} from './model';
import Service from './service';
const usePresenter = () => {const model = useModel();
const service = Service.single(model);
useEffect(() => {service.getUserList();
}, [model.runFetch]);
const handlePageChange = (page: number, pageSize: number) => {service.changePage(page, pageSize);
};
const handleFormChange = (name: string, value: any) => {service.changeFilterForm(name, value);
};
const handleSearch = () => {service.doSearch();
};
const handleReset = () => {service.resetForm();
};
const handelEdit = (data: IFetchUserListResult['result']['rows'][0]) => {service.edit(data);
};
const handleDel = (data: IFetchUserListResult['result']['rows'][0]) => {
Modal.confirm({
title: '确认',
content: '确认删除以后记录?',
cancelText: '勾销',
okText: '确认',
onOk: () => {service.del(data.id).then(() => {message.success('删除胜利');
service.getUserList();});
},
});
};
const refresh = () => {service.getUserList();
};
return {
model,
handlePageChange,
handleFormChange,
handleSearch,
handleReset,
handelEdit,
handleDel,
refresh,
};
};
export default usePresenter;
因为 presenter 是一个自定义 hooks,所以能够应用别的自定义的 hooks,以及其它开源的 hooks 库,比方 ahooks,vueuse 等。presenter 中不要呈现太多的逻辑代码,适当的抽离到 service 中。
view
view 层就是 UI 布局,能够是 jsx 也能够是 vue template。产生的事件由 presenter 解决,应用 model 进行数据绑定。
vue jsx
// vue jsx
import {defineComponent} from "vue";
import {
Table,
Pagination,
Row,
Col,
Button,
Form,
Input,
Tag,
} from "ant-design-vue";
import {PlusOutlined} from "@ant-design/icons-vue";
import usePresenter from "./presenter";
import styles from "./index.module.less";
import {ColumnsType} from "ant-design-vue/lib/table";
import EditModal from "./EditModal";
const Index = defineComponent({setup() {const presenter = usePresenter();
const {model} = presenter;
const culumns: ColumnsType = [
{
title: "姓名",
dataIndex: "name",
key: "name",
width: 150,
},
{
title: "年龄",
dataIndex: "age",
key: "age",
width: 150,
},
{
title: "电话",
dataIndex: "mobile",
key: "mobile",
width: 150,
},
{
title: "tags",
dataIndex: "tags",
key: "tags",
customRender(data) {return data.value.map((s: string) => {
return (<Tag color="blue" key={s}>
{s}
</Tag>
);
});
},
},
{
title: "住址",
dataIndex: "address",
key: "address",
width: 300,
},
{
title: "操作",
key: "action",
width: 200,
customRender(data) {
return (
<span>
<Button
type="link"
onClick={() => {presenter.handelEdit(data.record);
}}
>
编辑
</Button>
<Button
type="link"
danger
onClick={() => {presenter.handleDel(data.record);
}}
>
删除
</Button>
</span>
);
},
},
];
return {model, presenter, culumns};
},
render() {
return (
<div>
<div class={styles.index}>
<div class={styles.filter}>
<Row gutter={[20, 0]}>
<Col span={8}>
<Form.Item label="名称">
<Input
value={this.model.filterForm.name}
placeholder="输出名称搜寻"
onChange={(e) => {this.presenter.handleFormChange("name", e.target.value);
}}
onPressEnter={this.presenter.handleSearch}
/>
</Form.Item>
</Col>
</Row>
<Row>
<Col span={24} style={{textAlign: "right"}}>
<Button type="primary" onClick={this.presenter.handleSearch}>
查问
</Button>
<Button
style={{marginLeft: "10px"}}
onClick={this.presenter.handleReset}
>
重置
</Button>
<Button
style={{marginLeft: "10px"}}
type="primary"
onClick={() => {this.presenter.handleCreate();
}}
icon={<PlusOutlined />}
>
创立
</Button>
</Col>
</Row>
</div>
<Table
columns={this.culumns}
dataSource={this.model.userList.value}
loading={this.model.loading.value}
pagination={false}
/>
<Pagination
current={this.model.pagination.page}
total={this.model.pagination.total}
showQuickJumper
hideOnSinglePage
style={{marginTop: "20px"}}
pageSize={this.model.pagination.size}
onChange={this.presenter.handlePageChange}
/>
</div>
<EditModal
visible={this.model.modalInfo.visible}
data={this.model.modalInfo.data}
title={this.model.modalInfo.title}
onCancel={() => {this.model.modalInfo.visible = false;}}
onOk={() => {
this.model.modalInfo.visible = false;
this.presenter.refresh();}}
/>
</div>
);
},
});
export default Index;
vue template
// vue template
<template>
<div :class="styles.index">
<div :class="styles.filter">
<Row :gutter="[20, 0]">
<Col :span="8">
<FormItem label="名称">
<Input
:value="model.filterForm.name"
placeholder="输出名称搜寻"
@change="handleFormChange"
@press-enter="presenter.handleSearch"
/>
</FormItem>
</Col>
</Row>
<Row>
<Col span="24" style="text-align: right">
<Button type="primary" @click="presenter.handleSearch"> 查问 </Button>
<Button style="margin-left: 10px" @click="presenter.handleReset">
重置
</Button>
<Button
style="margin-left: 10px"
type="primary"
@click="presenter.handleCreate"
>
<template #icon>
<PlusOutlined />
</template>
创立
</Button>
</Col>
</Row>
</div>
<Table
:columns="columns"
:dataSource="model.userList.value"
:loading="model.loading.value"
:pagination="false"
>
<template #bodyCell="{column, record}">
<template v-if="column.key ==='tags'">
<Tag v-for="tag in record.tags" :key="tag" color="blue">{{tag}}</Tag>
</template>
<template v-else-if="column.key ==='action'">
<span>
<Button type="link" @click="() => presenter.handelEdit(record)">
编辑
</Button>
<Button
type="link"
danger
@click="
() => {presenter.handleDel(record);
}
"
>
删除
</Button>
</span>
</template>
</template>
</Table>
<Pagination
:current="model.pagination.page"
:total="model.pagination.total"
showQuickJumper
hideOnSinglePage
style="margin-top: 20px"
:pageSize="model.pagination.size"
@change="
(page, pageSize) => {presenter.handlePageChange(page, pageSize);
}
"
/>
<EditModal
:visible="model.modalInfo.visible"
:data="model.modalInfo.data"
:title="model.modalInfo.title"
:onCancel="
() => {model.modalInfo.visible = false;}
":onOk="
() => {
model.modalInfo.visible = false;
presenter.refresh();}
"
/>
</div>
</template>
<script setup lang="ts">
import {
Table,
Pagination,
Row,
Col,
Button,
Form,
Input,
Tag,
} from "ant-design-vue";
import {PlusOutlined} from "@ant-design/icons-vue";
import usePresenter from "./presenter";
import styles from "./index.module.less";
import {ColumnsType} from "ant-design-vue/lib/table";
import EditModal from "./EditModal/index.vue";
const FormItem = Form.Item;
const presenter = usePresenter();
const {model} = presenter;
const columns: ColumnsType = [
{
title: "姓名",
dataIndex: "name",
key: "name",
width: 150,
},
{
title: "年龄",
dataIndex: "age",
key: "age",
width: 150,
},
{
title: "电话",
dataIndex: "mobile",
key: "mobile",
width: 150,
},
{
title: "tags",
dataIndex: "tags",
key: "tags",
},
{
title: "住址",
dataIndex: "address",
key: "address",
width: 300,
},
{
title: "操作",
key: "action",
width: 200,
},
];
const handleFormChange = (e: any) => {presenter.handleFormChange("name", e.target.value);
};
</script>
react
// react
import {Table, Pagination, Row, Col, Button, Form, Input, Tag} from 'antd';
import {ColumnsType} from 'antd/lib/table';
import {PlusOutlined} from '@ant-design/icons';
import usePresenter from './presenter';
import styles from './index.module.less';
import EditModal from './EditModal';
function Index() {const presenter = usePresenter();
const {model} = presenter;
const culumns: ColumnsType = [
{
title: '姓名',
dataIndex: 'name',
key: 'name',
width: 150,
},
{
title: '年龄',
dataIndex: 'age',
key: 'age',
width: 150,
},
{
title: '电话',
dataIndex: 'mobile',
key: 'mobile',
width: 150,
},
{
title: 'tags',
dataIndex: 'tags',
key: 'tags',
render(value) {return value.map((s: string) => (<Tag color="blue" key={s}>
{s}
</Tag>
));
},
},
{
title: '住址',
dataIndex: 'address',
key: 'address',
width: 300,
},
{
title: 'Action',
key: 'action',
width: 200,
render(value, record) {
return (
<span>
<Button
type="link"
onClick={() => {presenter.handelEdit(record as any);
}}
>
编辑
</Button>
<Button
type="link"
danger
onClick={() => {presenter.handleDel(record as any);
}}
>
删除
</Button>
</span>
);
},
},
];
return (
<div>
<div className={styles.index}>
<div className={styles.filter}>
<Row gutter={[20, 0]}>
<Col span={8}>
<Form.Item label="名称">
<Input
value={model.filterForm.name}
placeholder="输出名称搜寻"
onChange={(e) => {presenter.handleFormChange('name', e.target.value);
}}
onPressEnter={presenter.handleSearch}
/>
</Form.Item>
</Col>
</Row>
<Row>
<Col span={24} style={{textAlign: 'right'}}>
<Button type="primary" onClick={presenter.handleSearch}>
查问
</Button>
<Button
style={{marginLeft: '10px'}}
onClick={presenter.handleReset}
>
重置
</Button>
<Button
style={{marginLeft: '10px'}}
type="primary"
onClick={() => {model.setModalInfo((s) => {
s.visible = true;
s.title = '创立';
s.data = undefined;
});
}}
icon={<PlusOutlined />}
>
创立
</Button>
</Col>
</Row>
</div>
<Table
columns={culumns as any}
dataSource={model.userList}
loading={model.loading}
pagination={false}
rowKey="id"
/>
<Pagination
current={model.pagination.page}
total={model.pagination.total}
showQuickJumper
hideOnSinglePage
style={{marginTop: '20px'}}
pageSize={model.pagination.size}
onChange={(page, pageSize) => {presenter.handlePageChange(page, pageSize);
}}
/>
</div>
<EditModal
visible={model.modalInfo.visible}
data={model.modalInfo.data}
title={model.modalInfo.title}
onCancel={() => {model.setModalInfo((s) => {s.visible = false;});
}}
onOk={() => {model.setModalInfo((s) => {s.visible = false;});
presenter.refresh();}}
/>
</div>
);
}
export default Index;
为何如此分层
一开始以这种形式写代码的时候,service 跟 presenter 一样也是一个自定义 hooks:
import useModel from './useModel';
const useService = () => {const model = useModel();
// 各种业务逻辑办法
const getRemoteData = () => {};
return {model, getRemoteData};
};
export default useService;
import useService from './useService';
const useController = () => {const service = useService();
const {model} = service;
// 调用 service 办法解决 view 事件
return {
model,
service,
};
};
export default useController;
useController 就是 usePresenter,这么操作下来,这里就产生了三个自定义 hooks,为了保障 service 和 presenter 里的 model 是同一份数据,model 只能在 sevice 中创立,返回给 presenter 应用。
因为偷懒,以及有的页面逻辑的确很简略,就把逻辑代码都写在了 presenter 中,整个 service 只有两行代码。删掉 service 吧,就得调整代码,在 presenter 中去引入以及创立 model,如果哪天业务变简单了,presenter 收缩了,须要把逻辑抽离到 service 中,又得调整一次。而且,如果技术栈是 react,比拟谋求性能的话,service 中的办法还得加上 useCallback。所以,最初 service 变成了原生语法的类,业务不简单时,presenter 中不调用就行。
回看整个文件及构造,如下:
userManage
└── List
├── api.ts
├── EditModal
│ ├── index.tsx
│ ├── index.vue
│ ├── model.ts
│ ├── presenter.tsx
│ └── service.ts
├── index.module.less
├── index.tsx
├── index.vue
├── model.ts
├── presenter.tsx
└── service.ts
这是从功能模块内具体的页面角度来划分,再以文件名来做分层,不思考不同页面之间进行复用,也简直不存在能复用的。如果某个页面或模块须要独立部署,很容易就能拆分进来。
看过其它整洁架构的落地计划,还有以下两种分层形式:
面向业务畛域,微服务式分层架构
src
│
├── module
│ ├── product
│ │ ├── api
│ │ │ ├── index.ts
│ │ │ └── mapper.ts
│ │ ├── model.ts
│ │ └── service.ts
│ └── user
│ ├── api
│ │ ├── index.ts
│ │ └── mapper.ts
│ ├── model.ts
│ └── service.ts
└── views
面向业务畛域,微服务式的分层架构。不同的 module 是依据业务来划分的,而不是某个具体的页面,须要十分熟悉业务才有可能划分好。能够同时调用多个模块来实现业务性能。如果业务模块须要独立部署,也很容易就能拆分进来。
单体式分层架构
src
├── api
│ ├── product.ts
│ └── user.ts
├── models
│ ├── productModel.ts
│ └── userModel.ts
├── services
│ ├── productService.ts
│ └── userService.ts
└── views
就像以前后端的经典三层架构,很难拆分。
数据共享,跨组件通信
父子组件应用 props 即可,子孙组件、兄弟组件(包含雷同模块不同页面)或者不同模块思考应用状态库。
状态库举荐:
vue:Pinia,全局 reactive 或者 ref 申明变量。
react:jotai,zustand,hox
集体吐槽:别再用 Redux 以及基于 Redux 弄出来的各种库了,开发体验极差
子孙组件、兄弟组件(包含雷同模块不同页面)状态共享
pennant
├── components
│ ├── PenantItem
│ │ ├── index.module.less
│ │ └── index.tsx
│ └── RoleWrapper
│ ├── index.module.less
│ └── index.tsx
├── Detail
│ ├── index.module.less
│ ├── index.tsx
│ ├── model.ts
│ ├── presenter.tsx
│ └── service.ts
├── MakingPennant
│ ├── index.module.less
│ ├── index.tsx
│ ├── model.ts
│ ├── presenter.tsx
│ └── service.ts
├── OptionalList
│ ├── index.module.less
│ ├── index.tsx
│ ├── model.ts
│ ├── presenter.tsx
│ └── service.ts
├── PresentedList
│ ├── index.module.less
│ ├── index.tsx
│ ├── model.ts
│ ├── presenter.tsx
│ └── service.ts
├── SelectGiving
│ ├── GivingItem
│ │ ├── index.module.less
│ │ └── index.tsx
│ ├── index.module.less
│ ├── index.tsx
│ ├── model.ts
│ ├── presenter.tsx
│ └── service.ts
├── Share
│ ├── index.module.less
│ ├── index.tsx
│ └── model.ts
└── store.ts
模块中的不同页面须要共享数据,在模块根目录新增 store.ts(子孙组件的话,store.ts 文件放到顶层父组件同级目录下即可)
// vue
import {reactive, ref} from "vue";
const userScore = ref(0); // 用户积分
const makingInfo = reactive<{
data: {
exchangeGoodId: number;
exchangeId: number;
goodId: number;
houseCode: string;
projectCode: string;
userId: string;
needScore: number;
bigImg: string; // 锦旗大图,空的
makingImg: string; // 制作后的图片
/**
* @description 赠送类型,0- 集体,1- 团队
* @type {(0 | 1)}
*/
sendType: 0 | 1;
staffId: string;
staffName: string;
staffAvatar: string;
staffRole: string;
sendName: string;
makingId: string; // 提交后后端返回 ID
};
}>({data: {} } as any); // 制作锦旗须要的信息
export const useStore = () => {const detory = () => {
userScore.value = 0;
makingInfo.data = {} as any;};
return {
userScore,
makingInfo,
detory,
};
};
export type Store = ReturnType<typeof useStore>;
应用全局 reactive 或者 ref 变量。也能够应用 Pinia
// react
import {createModel} from 'hox';
import {useState} from '@/hooks/useState';
export const useStore = createModel(() => {const [userScore, setUserScore] = useState(0); // 用户积分
const [makingInfo, setMakingInfo] = useState<{
exchangeGoodId: number;
exchangeId: number;
goodId: number;
houseCode: string;
projectCode: string;
userId: string;
needScore: number;
bigImg: string; // 锦旗大图,空的
makingImg: string; // 制作后的图片
/**
* @description 赠送类型,0- 集体,1- 团队
* @type {(0 | 1)}
*/
sendType: 0 | 1;
staffId: string;
staffName: string;
staffAvatar: string;
staffRole: string;
sendName: string;
makingId: string; // 提交后后端返回 ID
}>({} as any); // 制作锦旗须要的信息
const detory = () => {setUserScore(0);
setMakingInfo({} as any);
};
return {
userScore,
setUserScore,
makingInfo,
setMakingInfo,
detory,
};
});
export type Store = ReturnType<typeof useStore>;
应用 hox
presenter 层和 view 层能够间接引入 useStore,service 层能够像 model 一样注入应用:
import {useStore} from '../store';
import {useModel} from './model';
import Service from './service';
export const usePresenter = () => {const store = useStore();
const model = useModel();
const service = Service.single(model, store);
...
return {
model,
...
};
};
咱们能够称这种为 部分的数据共享,因为应用的中央就是单个模块内的组件,不须要特意地去限度数据的读写。
还有一种场景应用这种形式会有更好的开发体验:一个表单页面,表单填了一半须要跳转页面进行操作,返回到表单页面要维持之前填的表单还在,只须要把 model 中数据放到 store 中即可。
模块之间数据共享
其实就是 全局状态,按以前全局状态治理的形式放就行了。因为读写的中央变多了,须要限度更新数据的形式以及能不便的跟踪数据的变更操作。
vue 技术栈倡议应用 Pinia,react 还是下面举荐的库,都有相应的 dev-tools 察看数据的变更操作。
残缺代码
vue3
vue2.6
vue2.7
react
taro-vue
taro-react
mock 服务