前端项目结构的最佳实践
前端项目结构的最佳实践
前言
本文章仅根据个人经验总结与学习视频参考而写成,不代表这就是最终标准答案,实际项目中更应该思考的是项目模块化编写,功能接口设计的是否合理,而不是一味照抄别人的项目,这样反而会缺少灵活性
本文章参照项目为 SPA 架构,采用 vuex 或 redux 等全局数据方案,使用 vue-router / react-router 等全局路由方案,仅一个根 app 实现的前端项目
全局数据方案
基本上是顶层一个 Provider,然后解构 store 提供给全局使用,这种方式的优点是使用简单,不需要过多思考操作。
缺点也很明显,数据与组件耦合性关联低了,有时你可能需要变动 store 结构,但是不知道哪个组件用了 store 内的数据,这时维护起来就很麻烦了。
其次可能是有性能问题,redux 每次变动时通知的是全局监听该 store 的组件,这就意味着使用全局状态的组件必须更新,当然用 useMemo 也可以优化,但同理仍然少不了内存占用。
要减少这种处理,可以考虑 Zustand 或者 nanostores 原子化方案。
全局路由方案
就 vue-router 作为项目路由管理库,举一个 router 目录下的结构:
+ src
+ router
+ modules
- Home
- ...
- index.ts
- routes.ts // 路由表
在 modules 中采用 export { default as Home } from '[...path]/Home'
的方式导出页面,这样可以明确展示模块关系,特别是页面特别多的项目时可以使用这种模块的方式区分。
然后你可以在 index.ts
编写权限与登录控制状态的相关代码实现。
项目目录架构
页面目录结构
这个相对比较重要,以下是一个pages目录下的较好页面目录总结:
+ src
+ pages
+ ...
+ Home // 页面名字
+ assets // 静态资源
+ components // 页面使用的组件
- api.ts // 接口
- config.ts // 页面配置
- constants.ts // 页面常量
- index.tsx // 页面入口文件
- interface.d.ts // 类型声明
- store // 同页面跨组件状态处理
- moduleA.ts // 私有 store,用于局部共享
- export.ts // 暴露到外部的公共 store
+ ...
上面的目录可以根据自己实际情况动态调整
这种目录的好处是实现了局部化,能很清晰的告诉开发者这些是属于哪一块区域的
API 结构
这个结构的复杂性取决于项目规模,有的项目可能只需要一个 host 即可,有的则可能分了 114 个 host
store 结构
前文已经提到了 store 使用的是 redux,所以我们可以在各个子级页面的 store 文件夹下控制哪些是可以暴露到全局,哪些是私用的
+ src
+ store
+ Home.ts
- `export * from 'pages/Home/store/export.ts'` // 引入暴露的 store
+ About
+ ...
- index.ts // 全局 store 入口,这里聚合了所有来自页面主动暴露的 store
router 结构
跟 store 类似,也是有两层聚合
这里就不再多写了
数据请求封装
普通项目内每个页面无非都是:请求数据,渲染数据,错误 / 刷新处理。大部分页面只需要把这几件事做好了就可以得到一个完整页面
我们可以根据此规范来封装一个 hook:
const {
refreshing,
errMsg,
data,
setRefreshing,
loading
} = useRequest</* data type */ T, /* error type */ U>(
/* api fn */userInfoApi,
/* default value?*/ null,
/* params? */ params,
/* disabled? */ false
)
内部实现还是基于原有 hooks 处理
高度封装带来的好处是极致的开发效率与生产速度,同时也考验了封装的健壮性。
其次是关注点分离,维护更方便。
内部细节
其实这个 hooks 编写难度不大,唯一要注意的可能就是 refresh
比如用户触发某个动作如点击,正常可能会想着需要去做什么事情,比如请求或者改变数据
但是在这个 hooks 封装中,我们则是通过 hooks 暴露的方法来触发刷新
这样做的目的仍然是为了实现关注点分离(解耦)
副作用分离
又称 代数效应
,其目的是为了让函数目的保持纯粹。
在 react 中的最佳实践就是 hooks 了。
比如 state,我们只需要设置他的值,并通过 setState 更新,更新时在 dom 上的副作用被 react 给透明化了,因此我们无需担心数据改变带来的副作用。
同理,上文也实现了一个副作用分离处理,因为这个 hooks 不是异步的,然后保持存粹的状态,组件做的就是根据状态修改 dom,至此实现了数据请求与 UI 分离,同时实现了请求带来的网络副作用隐藏。
这种思想在 vue3 setup 语法糖加持下也可以实现。
代码/组件复用规定
代码复用是经常能在某些项目看到的操作,但是个人认为不一定是能复用的代码就尽量复用,虽然复用可以减少代码量,但相对的会提升维护成本,比如某天复用了同一段代码的地方在不同页面有新的逻辑要加入,这个时候就又需要拆分和重新编写,而且假如你的复用代码不是纯函数时会变得更麻烦,因为有外部状态在影响
决定一段代码是否复用可以参考以下几点:
- 是否为纯函数
- 方法是否简单,简单的话建议再复制一份,而不是复用
- 复杂方法还是要进行复用的,且可能要保持外部灵活性
同理,组件复用也可以参考以下几点:
- 组件是否简单,简单的话建议再复制一份,而不是复用
- 组件适用范围,假如组件只是在两三个页面之间使用,那么也可以再多复制几份放到他们各自的 components 文件夹去
- 复杂组件仍然建议在全局 components 下建立,并保持高度灵活性
以上只是参考,不代表这就是标准答案
像某些方法如全局请求或者全局 hooks 还是可以复用的
分离逻辑到极致的组件页面
如下:
/** 请求本体,可以对数据进行二次加工 */
function api() {
return fetch(`...`).then((data) => { /* handle data code */; return data })
}
/** 请求 hook, 把副作用都封装到此处,消除组件内使用异步代码的影响 */
function useRequest(api) {
const [state, setState] = useState()
useEffect(() => {
api().then((data) => {
setState(data)
})
}, []);
return {
state
}
}
/** 组件,只受状态变化影响并改变 DOM 视图,确保 UI 层简洁 */
function Foo() {
const {state} = useRequest()
return (
<div>
{state}
</div>
)
}
以上例子实现了数据状态分离,是一个代数效应的最佳实践。
总结
- 逻辑与 UI 分离,这样可以实现关注点分开,虽然在某种程度上有性能的牺牲,但是实现了高效率开发
- 影响局部化处理,页面组件模块应放置在各自的区域下,而不是实现了一个组件就立刻封装到全局
- 合理封装复用代码
- 代数效应实践,隐藏副作用,实现关注点分离