当前位置:   article > 正文

大型项目模块划分_Vue composition api 模块

compositon api 大型项目

42611da1beca9a2adfd9c3f64facddbd.png

如果你的项目,只是类似活动页面等开发场景,你只需要看我得第一篇文章就可以了解 Vue composition api 了:

https://zhuanlan.zhihu.com/p/181630740​zhuanlan.zhihu.com

如果你需要开发较为复杂的后台页面,那可能看我的第二篇和第三篇文章,也能让你很舒服地将 Vue composition api 用起来:

https://zhuanlan.zhihu.com/p/183744518​zhuanlan.zhihu.com
https://zhuanlan.zhihu.com/p/187325450​zhuanlan.zhihu.com

但是如果你想要充分发挥逻辑复用的威力,做出一个长期维护拳头产品,那么这篇文章将有所裨益,当然,这个层级我也在试探,希望能与大家共勉,出了差池也希望谅解~


模块

能做到服务封装,实现 “微”,已经很不错了吧?

照理来说,Vue composition api 的潜力已经被挖掘到极致了吧?

其实不然,即使已经做到了 服务分割,服务发现,也还远远不够,为什么呢?

回头看一下之前的例子,是不是一直都有一个问题?没发现?

我们所有的服务,最终注入并且处理的地方是在哪里?

视图组件

  1. const SomeComponent = ()=>{
  2. // 视图组件中注入服务
  3. const data = inject(serviceToken)
  4. cosnt data_2 = useService_2(data)
  5. return ()=> <div/>
  6. }

这合理么?

中小型应用,这样是合理的,视图的结构就是功能的结构

大型应用,这样不合理,视图以及一切部件,都是为功能和业务服务的


我们从视图的角度来看,当在组件内部注入服务时,其子组件和孙组件都可以访问到该服务。

其它组件呢?与其平级的组件和其父组件呢?是会访问不到么?

当组件出现嵌套依赖的时候,以组件结构作为功能结构的模型将会出现重大漏洞!

这个问题在后端,被称为:

循环依赖问题

不过呢,好消息是,js 原生的模块化文件加载机制已经处理了这个问题,是不是很开心?不用再大量展开了!

因此,你只需要通过改变文件划分,就可以完成模块化改造,让代码围绕着功能逻辑展开。

那么我们来试试,首先之前依赖视图逻辑的划分方式要彻底改变:

  1. 摒弃掉 pages/views 之类的文件夹
  2. 包含视图组件的每个文件夹下声明 index.tsx 作为主视图入口
  3. 常用第三方库尽可能 组合函数化 (这个需要靠第三方,比如 React 的 ahooks)

而我们按模块化划分的文件夹应该是怎样的结构呢?

直接看 Angular 的文档吧~

领域模块:

领域特性模块用来给用户提供应用程序领域中特有的用户体验,比如编辑客户信息或下订单等。
它们通常会有一个顶层组件来充当该特性的根组件,并且通常是私有的。用来支持它的各级子组件。

路由模块:

路由模块为其它模块提供路由配置,并且把路由这个关注点从它的配套模块中分离出来。

服务模块:

服务模块提供了一些工具服务,比如数据访问和消息。理论上,它们应该是完全由服务提供者组成的,不应该有可声明对象

窗口部件模块:

窗口部件模块为外部模块提供组件、指令和管道。很多第三方 UI 组件库都是窗口部件模块。
窗口部件模块应该完全由可声明对象组成,它们中的大部分都应该被导出。
窗口部件模块很少会有服务提供者。
如果任何模块的组件模板中需要用到这些窗口部件,就请导入相应的窗口部件模块。

那么,你定义出来的文件结构应该类似于这样:

  1. + routeModule
  2. - user.tsx
  3. - index.tsx
  4. + widgetModule // 部件模块不能有外部注入,不需要 index
  5. - dialog.tsx
  6. - message.tsx
  7. + serviceModule // 服务模块不需要 index
  8. - useRequest.tsx
  9. - useDrag.tsx
  10. + userModule // 领域模块
  11. - index.tsx
  12. - profile.tsx
  13. + workerModule // 领域模块
  14. - index.tsx
  15. - App.tsx //

当前适合 Vue Composition api 的 vue-router 还没发布,因此先不考虑 路由模块,需要重点说一下 部件模块。

部件模块

部件模块不应该有注入逻辑,不应该依赖于你的业务,比如很多第三方ui库,他的 button,table,和你正在写的业务是没有关系的,在这个模块中不可能出现 userInfo 之类的数据。

模块集成

模块可以嵌套模块,模块只是资源的一种划分方式而已:

  1. + widgetModule
  2. - dialog.tsx
  3. - message.tsx
  4. + bottomSheetModule // 嵌套 module
  5. - mobileBottomSheet.tsx

接下来来一个重头戏:

单一数据原则

模块划分了是不是不符合单一数据原则呢?有可能,即便你严格按照模块划分方式进行依赖的注入,也免不了会出现循环依赖的问题。

那怎么办呢?

很简单,所有全局单例服务的注入,全部放在根组件进行

  1. const App = ()=>{
  2. // 注入所有的单例服务
  3. const data = service()
  4. const data_2 = service_2(data)
  5. provide(serviceToken,data)
  6. provide(service_2Token,data_2)
  7. // ...
  8. return <div>{/* ... */}</div>
  9. }
  10. const SomeComponent = ()=>{
  11. // 组件中只会出现 inject
  12. const data = inject(serviceToken)
  13. return //...
  14. }

这样将大大减少心智负担,并且符合了单一数据源的原则。所有逻辑和数据都在跟组件完成了声明(当然,不一定保存于根组件)。

熟悉 Angular 的同学也会发现,这同时也是 Angular 推荐写法:

  1. @Injectable({
  2. providedIn: 'root',
  3. })
  4. export class Service {
  5. }

Angular 给出的解释是:

坚持指定通过应用的根注入器提供服务。 为何?注入器是层次化的。 为何?当你在根注入器上提供该服务时,该服务实例在每个需要该服务的类中是共享的。当服务要共享方法或状态时,这是最理想的选择。 为何?当不同的两个组件需要一个服务的不同的实例时,上面的方法这就不理想了。在这种情况下,对于需要崭新和单独服务实例的组件,最好在组件级提供服务。

当然,最后一句也可以看出,这种方式只适合 全局单例服务,非全局的化,还是得老老实实声明注入喔。


统一导入导出

当你用文件系统完成模块封装后,还有一个问题没有解决,如题所示,统一导入导出。

什么意思呢?假设你有一个部件模块 OverlayModule:

  1. // OverlayModule
  2. import Message from './Message.tsx'
  3. import Dialog from './Dialog.tsx'
  4. export default {
  5. Message,Dialog
  6. }

大多数情况下,我们希望模块有统一的导出结构。

这个无可厚非,但是当你开发大型超大型项目的时候,往往你不止想要的是统一的导出结构,更想要统一的导入结构

你似乎可以这么做:

  1. import OverlayModule from 'OverlayModule'
  2. cosnt SomeComponent = ()=>{
  3. return ()=>(
  4. <div> <OverlayModule.Message /> </div>
  5. )
  6. }

对了,小朋友们已经看出来了,之所以推荐使用 jsx 的原因,template 写法组件声明是会比较麻烦的,有小伙伴指出,Vue 的优势不是逻辑视图分离么?朋友,在有逻辑复用的情况下:整个组件都应该是视图。(坚持把视图以外逻辑放在服务中)

如果你需要导入的是 自定义组合函数 :

  1. import OverlayModule from 'OverlayModule'
  2. const SomeComponent = ()=>{
  3. const attachService = inject(OberlayModule.attachToken)
  4. return ()=>(<div/>)
  5. }

同理,如果我们需要更简单的统一导入导出方式,封装 module 时操作是最为方便的:

  1. // OverlayModule
  2. import Message from './Message.tsx'
  3. import Dialog from './Dialog.tsx'
  4. import useAttach,{attachToken} from './useAttach.tsx'
  5. export default {
  6. Message,Dialog,
  7. init(){
  8. provide(attachToken,useAttach())
  9. },
  10. attach(){
  11. return inject(attachToken)
  12. }
  13. }
  14. // App.tsx
  15. const App = ()=>{
  16. OverlayModule.init()
  17. // ...
  18. }
  19. // someComponent
  20. const SomeComponent = ()=>{
  21. const attach = OverlayModule.attach()
  22. // ...
  23. }

这样是不是会更加直观呢?


好了,Vue composition api 的系列介绍基本结束了,相信大家对新版 Vue 的潜能有了更多的期待,这里也可以做一个横向对比,让大家知道 Vue 在相关竞品中的位置。

  1. Vue 是最晚实现逻辑复用的主流框架,15年 Angular 率先实现,React 18年年底实现,Vue 还在开发中,但是,晚出现不一定是劣势,Angular 和 React 都存在一定的问题。
  2. Angular 现在可能只适合大型项目,对比没有逻辑复用的 React,和 Vue 版本,Angular 优势是无以复加的,相关的优点对于当时的 React 和 Vue 来说无异于降维打击,但是 事件驱动的 zone.js 难度实在是太高(最近将成为可选项),rxjs 上手难度尤为曲折(如果 Redux/vuex 只是用了一点点 函数式的概念,rxjs 则为函数式响应式大成,非科班很难用得顺手)。另外,对比当前版本的 React,在小型应用上,React 方便程度真的太高了,尤其是与其生态相配合。
  3. React 心智负担较高,新版本 React 将组件作为管道,大范围使用 monad,全面拥抱函数式开发(甚至可以看成一个有视图的 rxjs, cycle.js 类似体验),使用时限制太多,依赖数组/调用顺序/不许使用条件/非响应式可变ref,都在增加编程难度。

而 Vue 的优点也很明显 ——

简单

就目前的使用体验来说,Vue composition api 真的是把简单可用放在了第一位上的,核心 api 只有两个,直接使用之前的生命周期,除了需要理解响应式,其它基本无负担。

类型支持更让人惊艳,provide,inject 使用要比 React 的方式来的方便,并不需要自己单独封装 connect,要是加上了 language service,相信会有不少人从 React hooks 版本转向 Vue。

遗憾的是目前还没有带类型推断的 依赖查找/依赖注入 系统,当然,官方也是有解释的,decorator 还是不够稳定。

总而言之,Vue 正在利用自己的后发优势,在小型,中型,中大型应用上,向 React 发起挑战。

当然了,最后还是想聊聊关于不变性的思考:

不变性

如果把 Vue 的 reactive 看成“变量”,当然会认为这个系统天生不稳定,函数式拥趸甚至会说:“怎么能写变量呢?”,不过话又说回来,利用 proxy 封装之后,实际上还是调用的 getter,setter 方法,只是隐藏了细节而已。

带来的效果是,请求只需要声明,不需要传参了(react 如此做会违背不变性原则,比如 ahooks 的 useRequest.run(data)),看看这个例子:

fe24cd79abc35058baeffd34d7a60bf5.png

发送请求并未传参,原因仅仅是有这么个函数:

  1. export function useUserRequest<T>(
  2. method: "GET" | "POST" | "DELETE" | "PUT",
  3. path: string | Ref<string>,
  4. defaultData: T,
  5. options: { manual: boolean; default: any } = { manual: false, default: null }
  6. ) {
  7. const loading = ref(false);
  8. const error = ref<any>(null);
  9. const resData = ref(null);
  10. const total = ref(0);
  11. // 手动执行
  12. const run = async () => {
  13. loading.value = true;
  14. const data = unref(defaultData);
  15. const otherParams: any = {};
  16. if (method === "GET" || method === "DELETE") {
  17. otherParams.params = data;
  18. } else {
  19. otherParams.data = data;
  20. }
  21. return (userRequest({
  22. method,
  23. url: unref(path),
  24. ...otherParams,
  25. }) as any)
  26. .then((res: any) => {
  27. const { data: responseData, total: responseTotal } = res;
  28. loading.value = false;
  29. resData.value = responseData;
  30. if (responseTotal) {
  31. total.value = responseTotal;
  32. }
  33. return res;
  34. })
  35. .catch((err: any) => {
  36. error.value = err?.message;
  37. loading.value = false;
  38. Message.error(err.value || "未知错误");
  39. });
  40. };
  41. // 如果非手动执行,直接执行
  42. if (!options.manual) {
  43. run();
  44. }
  45. return {
  46. loading,
  47. error,
  48. data: computed(() => resData.value || options.default),
  49. run,
  50. total,
  51. };
  52. }

这个不纯,会出问题!

但是想想看,如果不死守教条可以让自己写的快一点,为什么还要按着书本不放呢?

我们本质上都是坏小孩,不是么?

有没有大佬想要搞个 Vue 版本的 ahooks ? 加我一起呀~

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/713796
推荐阅读
相关标签
  

闽ICP备14008679号