前端项目结构的最佳实践 - Shiina's Blog

前端项目结构的最佳实践

2024 年 5 月 10 日 - 04:37:51 发布
3.9K 字 14分钟
本作品采用 CC BY-NC-SA 4.0 进行许可。

前端项目结构的最佳实践

前言

本文章仅根据个人经验总结与学习视频参考而写成,不代表这就是最终标准答案,实际项目中更应该思考的是项目模块化编写,功能接口设计的是否合理,而不是一味照抄别人的项目,这样反而会缺少灵活性

本文章参照项目为 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 分离,这样可以实现关注点分开,虽然在某种程度上有性能的牺牲,但是实现了高效率开发
  • 影响局部化处理,页面组件模块应放置在各自的区域下,而不是实现了一个组件就立刻封装到全局
  • 合理封装复用代码
  • 代数效应实践,隐藏副作用,实现关注点分离
个人信息
avatar
Shiinafan
文章
39
标签
52
归档
4
导航