关于「领域驱动设计」在前端的应用,断断续续都能看到一些博客,但大部分看完后不明觉厉,转眼就忘记了,最重要的原因是无法应用在自己项目中。
其实有一个大部分前端同学每天都接触的领域模型 - 「列表领域」。
领域模型是公共认知的体现
我们说到列表时,都知道它在 PC
端一般是表格形式,移动端是列表形式。
- 表格形式下它有页码,可以点击切换页码,表格显示当前页数据
- 列表形式它是无限滚动加载或点击加载更多,列表显示当前页及之前所有页数据
-
PC
端移动端都有搜索功能,搜索时需要将页码重置到第一页
不仅仅是上面的描述,还有更多对于「列表」这个概念的「公共认知」就不赘述了。这个公共认知不仅对于研发,对于产品也是一样的。
产品经理在 PRD
中对于列表功能从不会详细描述,不会写出这样的 PRD
... 该页面展示客户表格,默认请求第一页数据,下方展示可以点击的页码 1 - n,点击页码时,将当前表格数据更新为点击页码对应的数据;根据关键字对客户进行搜索时,不使用当前已点击的页码而是第一页;点击重置按钮时,清空所有搜索条件,页码和表格数据回到第一页。
只需要一句「该页面展示客户列表,支持分页和搜索」,研发就知道怎么做了。对于某个业务逻辑大家有一致的认知,它就是领域模型。
领域模型在代码上的表现
下面假设存在 fetchCustomers
函数,它是一个支持分页、搜索的客户列表请求方法。
// 声明领域模型
class List {
constructor(fn) {
this.fn = fn;
}
async fetch(params = { page: 1, pageSize: 10 }) {
const response = await this.fn(params);
this.response = response;
return response;
}
}
const customerList = new List(fetchCustomers);
// 初始化时这样调用
customerList.fetch().then(({ list, total }) => {
// 获取列表数据成功
console.log(list, total);
});
// 点击页码时这样调用(后面均省略 .then)
customerList.fetch({ page: 2 });
// 搜索时这样调用
customerList.fetch({ page: 1, name: "ltaoo" });
List
虽然有方法有属性,但它没有表达「业务逻辑」所以不能算领域模型。
class List {
constructor(fn) {
this.fn = fn;
}
async fetch(params = { page: 1, pageSize: 10 }) {
const response = await this.fn(params);
this.response = response;
return response;
}
init() {
return this.fetch({ page: 1, pageSize: 10 });
}
goto(page) {
return this.fetch({ page, pageSize: 10 });
}
search(params = {}) {
return this.fetch({ page: 1, pageSize: 10, ...params });
}
}
const customerList = new List(fetchCustomers);
customerList.init().then(({ list, total }) => {
// ...
});
customerList.goto(2);
customerList.search({ name: "ltaoo" });
将业务逻辑以合适的方法名封装在 List
中,调用时无需关心逻辑内部实现,只需要调用对应方法,这才是领域模型。
除了这几个方法,我们还可以有 loadMore
实现移动端加载更多、reset
重置列表等。
有生命力的领域模型
其实上面的逻辑写成一个 react hook
是完全没有问题的,类似这样
function useList(fn) {
const [response, setResponse] = useState({ list: [], total: 0 });
async function fetch(params = { page: 1, pageSize: 10 }) {
const newResponse = await fn(params);
setResponse(newResponse);
}
return [
response,
{
init() {
fetch({ page: 1 });
},
goto(page) {
fetch({ page });
},
},
];
}
// 使用时
function CustomerMangePage() {
const [response, { init, goto }] = useList(fetchCustomers);
useEffect(() => {
init();
}, []);
// 渲染客户列表
}
写法上仍然简洁清晰,不过写成类的好处是不和框架耦合,无论框架怎么变更,只要仍使用 js
开发,这个 List
类是可以一直使用的。
当然,不和框架耦合,不意味着写法只能固定,我们可以创建一层框架与领域的粘合层,仍以 react hook
为例
function useList(fn) {
const list = useRef(new List(fn));
const [response, setResponse] = useState({ list: [], total: 0 });
useEffect(() => {
// 这个需要 List 内实现每次调用 fetch 后调用 onChange
list.onChange = (nextResponse) => {
setResponse(nextResponse);
};
}, []);
return [
response,
{
init: list.init,
goto: list.goto,
},
];
}
和直接实现为 react hook
用法是一样的,但它还可以在 vue
等框架中使用。
当然,说了这么多,这里是一个可运行的示例,实际上手体验下吧
点击体验
网友评论