网站首页 > 技术文章 正文
作者: Nealyang
转发链接:https://mp.weixin.qq.com/s/yNLaCVBhBmKTbB9_2m0pcA
前言
没有想到之前写的一篇「干货」一张页面引起的项目架构思考(rax+Typescript+hooks)还受到不少同学关注。的确,正如之前在群里所说,一个系统能有一个非常好的架构设计。但是仅仅对于前端项目页面,其实很难把「架构」一词搬出来聊个天花乱坠。
但是!好的代码结构的组织的确能够避免一些不必要的采坑。当然,这其中也不乏对前端工程师的工程师素养约束。
一言以蔽之,对于前端项目的架构(代码组织)而言,「好」,好不到哪里去。但是「坏」,却可以令人头皮发麻。
当然。。。我还是在尽可能的希望好~这也是这篇文章的目的所在。此处权且抛个砖,如果你有更好的见解和想法,欢迎随时交流~
拍卖详情页
详情页
?
图上的点我会在下文中挨个介绍
?
架构设计图
特点
- 「稳定性要求极高」 (这一点区分手淘和天猫,毕竟拍卖...你品)
- 需要详细的日志打点
- 模块之间的通信非常多(拍品状态、倒计时、出价等)
对于手淘和天猫的商品,一般都是多个人对多个物品。即使出了问题,也不影响购买,大不了问题修复再购买(最坏的情况)。
但是对于拍卖的拍品。对多对一、价高者得的属性。并且具有一定的法律效应。所以稳定性的要求极其之高。同时拍卖又具有非常高时效性要求,所以 apush、轮询啥的都要求实时更新拍品的状态。
综合以上因素的考虑。最终我们没有选择大黄蜂搭建页面的形式构建起详情页。就先走了源码链路的开发。「至于后续是否会推进落地,可能还有待商榷」。
整体架构
如果你阅读过上一篇文章一张页面引起的前端架构思考,那么可能会对接下来要介绍的目录组织结构比较熟悉。下面简单介绍下改动的部分以及添加的一些东西。
项目级别
目录的职责划分在之前的一篇文章中已经都介绍到了。这里就说下目前的一些改动点:
- 新增 count-dow
- 新增loop
- 移除EVENTS
Count-down 和 loop 都是详情页强相关的,但是由于项目名称为 pm-detail 所以,这里就提到 pages 以外的了。其实提不提的原则很简单。「该文件是否可(需)共用」
也是秉持着上面的原则,将 EVENTS 文件夹修改到页面容器里面了。毕竟,「跨页面的广播需求基本是不存在的。」
关于页面容器的介绍,也在之前的一篇《Decorator+TS装饰你的代码》一文中介绍到。这里也不赘述了。
count-down 的简单抽离
倒计时的“递归”交给 RAF 搞定。当然,这里是CountDown上的一个方法。
/**
* 开启倒计时
*/
start() {
let that = this;
function rafCallback() {
that.time -= new Date().getTime() - that.lastTime;
that.lastTime = new Date().getTime();
if (that.time < 0) {
that.time = 0;
}
that.updateCallback(that.time);
that.countDownRaf = window.requestAnimationFrame(rafCallback);
if (that.time <= 0) {
window.cancelAnimationFrame(that.countDownRaf);
if (that.endCallback) {
that.endCallback();
}
}
}
rafCallback();
}
?
具体的倒计时和轮询的编写会在下一篇文章中介绍(内网)
?
count-down 的内部消费
export const useInitCountDown = (
countDownData: IFormattedCountDown,
countEndCallback: () => any
) => {
let countDownRef = useRef(null) as any;
const [leftTime, setFormattedTime] = useState(countDownData.leftSwitchTime);
useEffect(() => {
if (countDownData.countDownSwitch) {
// 开启显示倒计时
countDownRef.current = startCountDown(
leftTime,
setFormattedTime,
countEndCallback
) ;
} else if (countDownData.implicitCountDownSwitch) {
// 开启隐藏倒计时
countDownRef.current = startImplicitCountDown(
leftTime,
countEndCallback,
(err) => {
console.log(err);
}
);
}
}, []);
useEffect(()=>{
countDownRef.current?.setTime(countDownData.leftSwitchTime);
},[countDownData.leftSwitchTime])
return leftTime;
};
?
具体的代码就不解释了,涉及到太多的业务。后面单独写一篇记录
?
消费端是在 pages/detial/count-down/customized-hooks/use-init-count-down.ts(强关联业务)里面。
pages/detail
detail
├─ components // 页面级别的 componets
│ ├─ bottom-action // 底部按钮模块
│ │ ├─ index.less
│ │ └─ index.tsx
│ ├─ config.ts // 模块的配置文件
│ ├─ count-down // 倒计时模块
│ │ ├─ customized-hooks // 倒计时模块的自定义 hooks
│ │ ├─ index.less
│ │ ├─ index.tsx
│ │ └─ utils // 倒计时模块
│ └─ loop // 倒计时模块
│ └─ index.tsx
├─ constants // 页面级别的常量定义
│ ├─ api.ts
│ ├─ common.ts
│ └─ spm.ts
├─ customized-hooks // 页面级别的自定义 hooks
│ └─ use-data-init.ts
├─ index.less
├─ index.tsx // 页面的入口文件
├─ reducers // reducer 目录(文件组织关联到 state 的设计)
│ ├─ count-down.reducer.ts // count-down 模块对应的 reducer
│ ├─ detail.reducer.ts // 汇总所有的组件的 reducer 到 detail 里面,并且包含一个公共的状态
│ ├─ index.ts // 整个页面的state
│ └─ loop.reducer.ts // 对应
├─ redux-middleware // redux 的中间件
│ ├─ redux-action-log // actionLog 中间件
│ │ └─ index.ts
│ └─ redux-mutli-action // 支持发送多个 action 的中间件
│ └─ index.ts
├─ types // 数据类型统一定义
│ ├─ count-down.d.ts
│ ├─ index.d.ts
│ ├─ item-dao.d.ts
│ ├─ loop.d.ts
│ └─ reducer-types.d.ts
├─ use-redux // 页面的状态管理
│ ├─ combineReducers.ts
│ ├─ compose.ts
│ ├─ redux.ts
│ ├─ types
│ │ ├─ actions.d.ts
│ │ └─ reducers.d.ts
│ └─ utils
│ ├─ actionTypes.ts
│ └─ warning.ts
└─ utils // 页面的工具函数
├─ demand-load-wrapper.tsx // 按需加载容器
└─ index.ts // 工具函数
关于文件和目录的说明都写在了上面的注释中。对于后续的开发者需要重点关注的是:
- components(包括 config)模块的组织
- reducer 状态的组织
- type 类型的约束
?
下面按个展开介绍
?
状态管理 useRedux
因为详情页的状态管理较为复杂,模块之间的通信也是非常频繁。所以这里我们需要引入 redux 作为状态管理。
虽然 hooks 里面已经提供了 useReducer ,但是却没有周边的“原生生态”:combineReducers、Middleware 等。所以我们将轮子搬一下,取名为:useRedux
关于 redux 的介绍可见:《从 redux 中搬个轮子给源码项目做状态管理》
「这里重点介绍在这个项目中的使用契约:」
基本使用
浪浪额够的时候写过一篇文章react技术栈项目结构探究 ,那时候我就非常喜欢将 redux 中的 initState、actionTypes、actions以及 reducer 定义到一个文件中,的确非常的清晰方便。所以这里 reducers 文件夹也是如此。
每一个文件,对应每一个功能区域的 reducer
而 reducer 内部的组成,基本都是如下:
reducer 内部结构
以上是模块的 reducer,对于开发者还需要知道的是模块的 reducer 需要查到 detail 里面:
export const detailReducer = combineReducers<ICombineItemDo>({
countDown,
loop,
detailCommon: globalStateReducer,
});
?
ICombineItemDo 会在下文的 Ts 状态约束里面介绍
?
所以如上的代码组成的最终页面 state 是如下结构
{
pageState:{
isLoading:boolean
},
itemDo:{
countDown:ICountDown,
detailCommon:IDetailCommon,
loop:ILoop
}
}
?
itemDo 其实应该命名为 itemDao但是由于 itemDo 我们用了五年了。。。尊重习惯的力量,避免不必要的麻烦
?
中间件的使用
虽然使用了中间件,但是跟 redux 还是有些不同的。具体的 applyMiddleware 就不说了,其实就是compose func 然后增强下 dispatch
export const useRedux = (reducer: Reducer, ...middleWares: Function[]) => {
const [state, dispatch] = useReducer(reducer, {});
let newDispatch;
if (middleWares.length > 0) {
newDispatch = compose(...middleWares)(dispatch);
}
useEffect(() => {
dispatch({
type: ActionTypes.INIT
});
}, []);
return {
state, dispatch: newDispatch
}
}
「所以这里的中间件都是根据当前 dispatch 的 action 里面的 data 来执行相关操作的。」
比如 redux-mutli-action 中间件
/**
* 支持 dispatch 多个 action dispatch([action1,action2,action3])
* @param next dispatch
*/
export const reduxMultiAction = next => action => {
if(action){
if (Array.isArray(action)) {
action.map((item) => next(item))
} else {
next(action);
}
}
}
非常的简单~
然后截止目前编写了两个中间件:
- 日志打点中间件
- dispatch 多个 action 中间件
?
上面的日志打点中间件可能后期会修改。理论上日志的打点不应该都会改变 state,所以是否需要为 ActionLog 提供单独的 reducer,以及提供后如何无缝的衔接,后面做到的时候可能还需要再思考下
?
模块数据分发
所谓的模块分发,存在的原因是:目前我们的详情页是有很多种不同的业务类型的,单纯的从大资产而言,就分为资产和司法、再分为变卖和拍卖、再有不同类的拍品之区分。也就是说,完整的详情页会有很多的模块,「也就是说打开的某一个详情页,并不需要加载所有的模块」。这也是为什么下文会有按需加载的 原因。
那么对于数据,我们当然需要根据接口返回的字段,来组织我们的 state 中我们要开发的 component
这里,我们在页面级别的自定义 hooks 文件夹的use-data-init.ts 中操刀。
useDataInit
- formatCountDownData 是由对应的模块提供的 format 方法。在接口返回的字段需要进行加工的时候需要
- 此处作为页面级别的 dataInit,「理论上应该是最全的数据处理情况」
format func return
按需加载
如上所说,不同页面需要不同的模块,目前详情页还未打算接SSR 以及由于组件频繁通信和稳定性要求不能走搭建,所以目前只能通过 codeSpliting 来进行代码分割的按需加载。
是的,通过 useImport
「由于是自定义 hooks,所以这里我们不能够通过判断来加载模块」。不能判断,我怎么知道 if 需要?
事实的确如此。所以我们需要一个容器,来让容器去走判断逻辑~
interface IWrapperProps{
/**
* 动态导入的模块 eg:()=>import('xxx')
*/
path:()=>void;
/**
* 导入的模块所对应的 itemDo 中模块的数据
*/
dataSource:{[key:string]:any};
/**
* 详情通用字段
*/
detailCommon:IDetailCommon;
[key: string]: any
}
/**
* 按需按需加载容器组件
*
* @export
* @param {*} props 按需加载的组件 props+path
* @returns 需按需加载的子组件
*/
export default function(props:IWrapperProps) {
const { path, ...otherProps } = props;
const [Com, error] = useImport(path);
if (Com) {
return <Com {...otherProps} />;
} else if (error) {
console.log(error);
return null;
} else {
return null;
}
}
可以看到,我会将 DataSource:当前模块数据、以及 detailCommon:通用字段 传递给需要加载的模块中。
然后在 index 中,通过接口是否有该模块字段去判断是否加载:
const renderCom = (componentConfigArr, itemDo, dispatch) => {
return componentConfigArr.map((item, index) => (
<StoreContext.Provider value={{ itemDo, dispatch }} key={index + 1}>
<DemandLoadWrapper
x-if={objHasKeys(itemDo[item.keyName])}
path={item.importFunc}
dataSource={itemDo[item.keyName]}
detailCommon={itemDo?.detailCommon}
/>
</StoreContext.Provider>
));
};
componentConfigArr来自我们组件 componets/config.ts
type IComConfigItem<T> = {
keyName: keyof IItemComponent;
importFunc: () => Promise<T>
}
/**
* 模块的导出配置,用于模块按需加载
*/
export const comConfig: IComConfigItem<Rax.RaxNode>[] = [
{
keyName: 'countDown',
importFunc: () => import('./count-down')
},
{
keyName: "loop",
importFunc: () => import('./loop')
}
];
keyName 是 itemDo 中对应接口模块的 key 的名字。这里我们用的 ts 来检查的。
类型约束
所以「理论上,后续的开发者,新增模块、修改模块,都不应该会修改到index.tsx 这个入口文件」
Ts 状态约束
「类型约束其实是 TS 的编码应该就塑造的类型思维的一部分」 ,毕竟不是介绍 Ts,所以这里主要说下新增模块如何做到类型约束的。
?
这一块,可能解释起来稍微有点烦
?
先说下我们的目的是什么:
如上,我们需要在模块 config的配置中读取到组件,并且state 中对应的模块数据注入给这个模块。重点我们还是要根据这个 keyName 来进行按需加载的判断。所以我需要你填写的 keyName 必须是你自己组织(combineReducers)出来 state 对应模块的 key
最终的效果就如上面的截图,编码的时候会提醒你,能够填写哪些字段。那么这个约束是如何形成的呢?
如图,首先我们需要将 combineReducers 和 state 通过 type 进行约束。当这个约束建立的时候,那么就可以通过这个 type 来进行 config 字段的约束
/**
* 标的模块数据
*/
export interface IItemComponent {
/**
* 倒计时模块
*/
countDown?: IFormattedCountDown;
/**
* 倒计时模块
*/
loop?: IGetLoopInfo
}
/**
* 详情页通用字段
*/
export interface IDetailCommon {
/**
* 标的 id
*/
itemId?: string;
/**
* 标的类型
*/
itemType?: string;
}
/**
* detailReducer 返回类型
*/
export interface ICombineItemDo extends IItemComponent{
detailCommon:IDetailCommon
}
如上的ICombineItemDo就是我们需要拿去约束每一个组件的 reducer 在detail.reducer 中汇总出来的state
export const detailReducer = combineReducers<ICombineItemDo>({
countDown,
loop,
detailCommon: globalStateReducer,
});
当我们 key 写错了以后,Ts 会帮我们检查出来:
当这个 type 已经拆分重组成我们想要的了时候,那么我们只需要将 config keyName 约束成 itemDo 中 componets 的某一个 key 即可。
type IComConfigItem<T> = {
keyName: keyof IItemComponent;
importFunc: () => Promise<T>
}
开发契约
所谓的开发契约其实就是你不要瞎 xx 搞~然后给在这个项目中开发的同学提供的一些职业道德约束。当然,程序猿的职业素养也都是不可靠的。所以后续考虑用脚本强制起来~
- 充分使用 TS 注释即文档的功能,每一个方法、属性、都需要编写对应注释
- 模块界限清晰,业务逻辑边界分明。不要将非此模块的代码写到公共场所里面。
- 编写对应 function 的单元测试(有点难)
- any 大法好,但是不安全
新增模块步骤
上面的契约其实有些泛泛而谈,不如实操来的痛快。下面我们通过举例说明在这个架构下,新增一个模块需要的步骤吧。
1、新增类型
「新增数据类型一定是第一步!!!」 避免一些低级错误的发生。同时,不是第一步的话。。。你后面的步骤编辑器都会报错的。
拿倒计时举例:
- 第一步在types/count-down.d.ts 中编写对应模块的「类型约束」
- 第二步,在 types/item-dao.d.ts 中注入
/**
* 标的模块数据
*/
export interface IItemComponent {
+ /**
+ * 倒计时模块
+ */
+ countDown?: IFormattedCountDown;
/**
* 倒计时模块
*/
loop?: IGetLoopInfo
}
?
最好呢,在 type/index.d.ts 中,统一导出。避免模块引入太多依赖而看起来吓唬人
?
2、reducer
编写 reducer 也分为两步:
- 第一步:编写对应 reducer,上文已经介绍到了。
- 第二步:在detail 的 reducer 中注入进去。
3、模块编写与配置
模块的编写与配置也分为两步:
- 第一步:在 componets 目录下新建对应模块,编码
- 在 componets/config.ts中注入
虽然新增一个步骤大致有些繁琐。但是也都中规中矩。每一步分为「本身模块的编写」以及「提供给你的注入方式」。
TODO
如上所介绍,再结合之前写的前端架构文章,基本上感觉介绍的差不多了。其实前端架构感觉应该换个名字:目录组织。
而搭建的这套组织形式造成的约束其实也是为了「提供更好的稳定性保障」和「代码的充分解耦」。
现在做的远远不够:
- 项目脚手架
- 自动化测试
- 编码规则静态检查
- 状态可视化
- 性能优化
- 代码覆盖率
- ...
最后,还是那句话,此处权且抛个砖,如果你有更好的见解和想法,欢迎随时交流
推荐Vue学习资料文章:
《「干货」一张页面引起的项目架构思考(rax+Typescript+hooks)》
《手把手教你如何用 Decorator 装饰你的 Typescript?「实践」》
《「干货」Deno TCP Echo Server 是怎么运行的?》
《Vue仿蘑菇街商城项目(vue+koa+mongodb)》
《基于 electron-vue 开发的音乐播放器「实践」》
《「实践」Vue项目中标配编辑器插件Vue-Quill-Editor》
《一篇文章,教你学会Vue-CLI 插件开发【Vue进阶篇】》
《「实践」基于Apify+node+react/vue搭建一个有点意思的爬虫平台》
《「实践」深入对比 Vue 3.0 Composition API 和 React Hooks》
《前端网红框架的插件机制全梳理(axios、koa、redux、vuex)》
《深入学习Vue的data、computed、watch来实现最精简响应式系统》
《10个实例小练习,快速入门熟练 Vue3 核心新特性(一)》
《10个实例小练习,快速入门熟练 Vue3 核心新特性(二)》
《教你部署搭建一个Vue-cli4+Webpack移动端框架「实践」》
《尤大大细品VuePress搭建技术网站与个人博客「实践」》
《是什么导致尤大大选择放弃Webpack?【vite 原理解析】》
《带你了解 vue-next(Vue 3.0)之 小试牛刀【实践】》
《带你了解 vue-next(Vue 3.0)之 初入茅庐【实践】》
《一篇文章教你并列比较React.js和Vue.js的语法【实践】》
《深入浅出通过vue-cli3构建一个SSR应用程序【实践】》
《聊聊昨晚尤雨溪现场针对Vue3.0 Beta版本新特性知识点汇总》
《【新消息】Vue 3.0 Beta 版本发布,你还学的动么?》
《Vue + Koa从零打造一个H5页面可视化编辑器——Quark-h5》
《深入浅出Vue3 跟着尤雨溪学 TypeScript 之 Ref 【实践】》
《手把手教你深入浅出vue-cli3升级vue-cli4的方法》
《Vue 3.0 Beta 和React 开发者分别杠上了》
《手把手教你用vue drag chart 实现一个可以拖动 / 缩放的图表组件》
《Vue3 尝鲜》
《手把手让你成为更好的Vue.js开发人员的12个技巧和窍门【实践】》
《2020 年,Vue 受欢迎程度是否会超过 React?》
《手把手教你Vue解析pdf(base64)转图片【实践】》
《手把手教你Vue之父子组件间通信实践讲解【props、$ref 、$emit】》
《深入浅出Vue3 的响应式和以前的区别到底在哪里?【实践】》
《干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)》
《基于Vue/VueRouter/Vuex/Axios登录路由和接口级拦截原理与实现》
《手把手教你D3.js 实现数据可视化极速上手到Vue应用》
《吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【上】》
《吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【中】》
《吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【下】》
作者: Nealyang
转发链接:https://mp.weixin.qq.com/s/yNLaCVBhBmKTbB9_2m0pcA
猜你喜欢
- 2024-10-11 JavaScript实现的转盘抽奖html页面前端源码
- 2024-10-11 前端录屏 + 定位源码,帮你快速定位线上 bug
- 2024-10-11 css+JavaScript实现的二级导航菜单html页面前端源码
- 2024-10-11 交互问卷表单设计html页面前端源码
- 2024-10-11 web前端实战项目(免费送源码+视频)
- 2024-10-11 html5+css3做的响应式企业网站前端源码
- 2024-10-11 小程序源代码:实现一个简易版QQ的前端页面,文末有代码
- 2024-10-11 Dive Into Code: VSCode 源码阅读(一)
- 2024-10-11 web前端:vue源码解析,vue-cli父子组件传递模板
- 2024-10-11 一个月学会web前端(免费送视频+源码)
你 发表评论:
欢迎- 501℃几个Oracle空值处理函数 oracle处理null值的函数
- 494℃Oracle分析函数之Lag和Lead()使用
- 493℃Oracle数据库的单、多行函数 oracle执行多个sql语句
- 482℃0497-如何将Kerberos的CDH6.1从Oracle JDK 1.8迁移至OpenJDK 1.8
- 473℃Oracle 12c PDB迁移(一) oracle迁移到oceanbase
- 468℃【数据统计分析】详解Oracle分组函数之CUBE
- 453℃Oracle有哪些常见的函数? oracle中常用的函数
- 449℃最佳实践 | 提效 47 倍,制造业生产 Oracle 迁移替换
- 最近发表
-
- Spring Boot跨域难题终结者:3种方案,从此告别CORS噩梦!
- 京东大佬问我,SpringBoot为什么会出现跨域问题?如何解决?
- 在 Spring Boot3 中轻松解决接口跨域访问问题
- 最常见五种跨域解决方案(常见跨域及其解决方案)
- Java Web开发中优雅应对跨域问题(java跨域问题解决办法)
- Spring Boot解决跨域最全指南:从入门到放弃?不,到根治!
- Spring Boot跨域问题终极解决方案:3种方案彻底告别CORS错误
- Spring Cloud 轻松解决跨域,别再乱用了
- Github 太狠了,居然把 "master" 干掉了
- IntelliJ IDEA 调试 Java 8,实在太香了
- 标签列表
-
- 前端设计模式 (75)
- 前端性能优化 (51)
- 前端模板 (66)
- 前端跨域 (52)
- 前端缓存 (63)
- 前端react (48)
- 前端aes加密 (58)
- 前端脚手架 (56)
- 前端md5加密 (54)
- 前端富文本编辑器 (47)
- 前端路由 (55)
- 前端数组 (65)
- 前端定时器 (47)
- Oracle RAC (73)
- oracle恢复 (76)
- oracle 删除表 (48)
- oracle 用户名 (74)
- oracle 工具 (55)
- oracle 内存 (50)
- oracle 导出表 (57)
- oracle 中文 (51)
- oracle链接 (47)
- oracle的函数 (57)
- 前端调试 (52)
- 前端登录页面 (48)
本文暂时没有评论,来添加一个吧(●'◡'●)