转载自https://github.com/Vincedream/ddd-fe-demo
目录结构
├── common
│ ├── components // 公用组件
│ ├── constants // 全局变量
│ │ ├── goods
│ │ │ └── index.js
│ │ ├── …
│ ├── data-source // 数据接口层
│ │ ├── goods
│ │ │ ├── requestApis.js
│ │ │ └── translators.js
│ │ ├── …
│ ├── domains // 领域层
│ │ ├── goods-domain
│ │ │ ├── entities // 实体
│ │ │ │ └── goods.js
│ │ │ └── goodsService.js // 领域Service服务
│ │ ├── …
│ └── util // 公用函数
│ └── http.js
└── page // 页面视图层
├── index
│ ├── App.js
│ ├── components
│ │ ├── GoodsItem.js
│ │ ├── GoodsItem.scss
│ │ ├── Nav.js
│ │ └── Nav.scss
│ ├── index.js
│ └── services // 该页面需要用到的Service
│ └── index.js
├── …
数据接口层 data-source文件夹
- requestApi:数据请求层,负责 http 请求,是项目中唯一与后端服务进行交流的一层。(存放服务接口,相当于API文件)
import axios from '@common/util/http';
src/common/data-source/interest/requestApis.js
import { pointRecordTranslator, pointGiftTranslator } from './translators'
export function getUserPointRecordList() {
return axios('/interest/pointRecord').then(data => {
return data.map(item => pointRecordTranslator(item));
})
}
export function getInterestGiftList() {
return axios('/interest/gift').then(data => {
return data.map(item => pointGiftTranslator(item))
})
}
分层作用:在这一层中集结了 interest 领域下所有的接口函数,避免了数据接口分散到各个页面,统一存放更易管理,解决了接口调用不统一问题。
- translator:数据清洗层,这层负责将后端返回的数据“清洗”,改造成更直观地字段(key)、更方便使用的数据(value)。(对返回的数据做处理)
export function goodsTranslator({
id,
goodsName,
price,
status,
activityType,
desc,
brand,
relatedModelId,
mainPic,
tag,
relatedModelImg
}) {
return {
id,
name: goodsName,
price: (price / 100).toFixed(2),
status,
activityType,
description: desc,
brandName: brand,
mainPicUrl: mainPic,
tags: tag
}
}
分层作用: 在这一层对接口字段、内容经过二次加工,避免了后端定义字段不规范、混乱对前端的影响,含义清晰、规范的字段在视图层使用时更具有表现力,解决了接口字段不可控性问题。
数据接口层是整个项目的根基,提供了结构清晰、定义规范、可直接使用的数据。
领域层 domain文件夹
领域层是整个项目的核心层,它掌管了所有领域下的行为与定义,它是整个项目中最能体现业务知识的一层。
- entity:实体,是领域服务的载体,它定义了业务中某个个体的属性与方法,比如抽奖活动中的奖品、活动,这些都可以抽象为实体,它在全局领域中是唯一的,不可能在别的领域中存在相同的实体。(其实就是一个类)
/**
* 抽奖活动实体
*/
import dayjs from 'dayjs'
import { lotteryTypeMap } from '@constants/lottery'
class Lottery {
constructor(lottery={}) {
this.id = lottery.id
this.name = lottery.name
this.type = lottery.type
this.startDate = lottery.startDate
this.endDate = lottery.endDate
}
// 获取活动时间范围
getLotteryTimeScope() {
return `${dayjs(this.startDate).format("M月D日")} - ${dayjs(this.endDate).format("M月D日")}`
}
// 获取活动类型描述
getLotteryType() {
return this.type && lotteryTypeMap[this.type].title
}
}
export default Lottery
在前端中,我们把它定义为一个 class 类,构造函数中初始化实体的属性,在类中定义了实体的方法,属性和方法的返回值主要是用于视图层中的直接展示,同一个实体的逻辑确保只在实体类中编写,在不同视图下可复用,解决了判断逻辑重复的问题。
- service:领域服务层,这一层中定义了领域的行为,供视图层直接调用。(关于领域的服务调用)
import {
getLotteryDetail,
getPrizeList,
playLottery,
savePrizeAddress
} from '@data-source/lottery/requestApis';
import Prize from './entities/prize';
import Lottery from './entities/lottery';
class LotteryService {
/**
* 获取本次抽奖活动详情
* @param {string} id 活动id
*/
static getLotteryDetail(id) {
return getLotteryDetail(id).then(lottery => new Lottery(lottery))
}
/**
* 获取本次抽奖活动的奖品列表
* @param {string} id 抽奖活动id
*/
static getPrizeList(id) {
return getPrizeList(id).then(list => {
return list.map(item => new Prize(item));
})
}
/**
* 进行抽奖
* @param {string} id 抽奖活动id
*/
static playLottery(id) {
return playLottery(id).then(result => {
const { recordId, prize } = result;
return {
recordId,
prize: new Prize(prize)
}
})
}
/**
* 填写中奖的收货地址信息
* @param {Object} param0 中奖记录id以及地址信息
*/
static savePrizeAddress({ recordId, name, phoneNumber, address }) {
const data = {
recordId,
name,
phoneNumber,
address
}
return savePrizeAddress(data)
}
}
export default LotteryService
分层作用: 我们可以看到,Service 层连接了 entity 层与 data-source 层,接收后端返回的数据将其转换成具有属性与方法的 entity 实体类,供视图层直接进行展示。不仅如此,Service 层还定义了该领域下的所有行为,比如填写收货地址。领域服务层涵盖了整个业务领域的行为,直观地体现了业务需求。解决了忽略业务整体的问题。
视图层 page文件夹
视图层也就是我们书写交互逻辑、样式的一层,可以使用纯 HTML 或者框架(React、Vue),这一层只需要调用了领域的服务,将返回值直接体现在视图层中,无需编写条件判断、数据筛选、数据转换等与视图展示无关的逻辑代码,这些“糙活”都在其他层中以已经完成,所以视图层是非常“薄”的一层,只需关注视图的展示与交互,整个 HTML 结构非常直观清晰。
import React from 'react';
import { UserService, InterestService } from './services';
import User from '@domain/user-domain/entities/user';
import { SIGN_USER_TYPE } from '@constants/user';
import "./App.scss"
class App extends React.Component {
state = {
pointCount: null,
user: new User()
}
componentDidMount() {
this.getUserInfo();
this.getUserPonitCount();
}
// 获取用户信息
getUserInfo = () => {
UserService.getUserDetail().then(user => {
this.setState({
user
})
});
}
// 获取用户积分
getUserPonitCount = () => {
InterestService.getUserPointCount().then(count => {
this.setState({
pointCount: count
})
})
}
render() {
const { pointCount, user } = this.state;
return (
<div className="user-page">
<h3>个人中心</h3>
<div className="user">
<div className="info">
<div>{user.type === SIGN_USER_TYPE ? `尊敬的${user.getUserTypeTitle()}:` : null}{user.name}</div>
<div>绑定手机号: {user.phoneNumber}</div>
<div>绑定email: {user.email}</div>
</div>
<div className="avatar">
<img className={`${user.isVip ? 'vip' : ''}`} src={user.avatarUrl} alt=""/>
{ user.isNeedRemindUserVipLack() && user.isVip
? <div>会员还有{user.getVipRemainDays()}天</div>
: ''
}
</div>
</div>
<div className="lottery-tips">
<div>剩余积分:{pointCount} 分</div>
<a href="/interest.html">前往积分权益中心 ></a>
</div>
</div>
);
}
}
export default App;
分层作用: 将 Service 中返回的数据直接使用,视图层中只编写交互与样式,不管是 HTML 纯粹的结构还是代码可读性,新的设计都更讨人喜欢。除了视图层与前端框架有关,其他层可独立应用于任何框架的,分层的结构解决了视图层过厚问题。