im系统微服务划分_Vue


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


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


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


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


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


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


模块

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

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

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

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

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

视图组件


const SomeComponent = ()=>{
   // 视图组件中注入服务
   const data = inject(serviceToken)
   cosnt data_2 = useService_2(data)
   return ()=> <div/>
}


这合理么?

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

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


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

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

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

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

循环依赖问题

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

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

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

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

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

直接看 Angular 的文档吧~

领域模块:

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

路由模块:

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

服务模块:

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

窗口部件模块:

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

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


+ routeModule
  - user.tsx
  - index.tsx
+ widgetModule  // 部件模块不能有外部注入,不需要 index
  - dialog.tsx
  - message.tsx
+ serviceModule  // 服务模块不需要 index
  - useRequest.tsx
  - useDrag.tsx
+ userModule  // 领域模块
  - index.tsx
  - profile.tsx
+ workerModule // 领域模块
  - index.tsx
- App.tsx  // 根


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

部件模块

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

模块集成

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


+ widgetModule 
  - dialog.tsx
  - message.tsx
  + bottomSheetModule // 嵌套 module
    - mobileBottomSheet.tsx



接下来来一个重头戏:

单一数据原则

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

那怎么办呢?

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


const App = ()=>{
  // 注入所有的单例服务
  const data = service()
  const data_2 = service_2(data)
  provide(serviceToken,data)
  provide(service_2Token,data_2)
  // ...
  return <div>{/* ... */}</div>
}

const SomeComponent = ()=>{
  // 组件中只会出现 inject
  const data = inject(serviceToken)
  return //...
}


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

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


@Injectable({
  providedIn: 'root',
})
export class Service {
}


Angular 给出的解释是:

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

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


统一导入导出

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

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


// OverlayModule
import Message from './Message.tsx'
import Dialog from './Dialog.tsx'

export default {
   Message,Dialog
}


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

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

你似乎可以这么做:


import OverlayModule from 'OverlayModule'

cosnt SomeComponent = ()=>{
   return ()=>(
     <div>  <OverlayModule.Message /> </div>
   )
}


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

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


import OverlayModule from 'OverlayModule'

const SomeComponent = ()=>{
    const attachService = inject(OberlayModule.attachToken)
    return ()=>(<div/>)
}


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


// OverlayModule
import Message from './Message.tsx'
import Dialog from './Dialog.tsx'
import useAttach,{attachToken} from './useAttach.tsx'

export default {
   Message,Dialog,
   init(){
      provide(attachToken,useAttach())
   },
   attach(){
      return inject(attachToken)
   }
}

// App.tsx
const App = ()=>{
   OverlayModule.init()
   // ...
}

// someComponent
const SomeComponent = ()=>{
   const attach = OverlayModule.attach()
   // ...
}


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


好了,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)),看看这个例子:


im系统微服务划分_Vue_02


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


export function useUserRequest<T>(
  method: "GET" | "POST" | "DELETE" | "PUT",
  path: string | Ref<string>,
  defaultData: T,
  options: { manual: boolean; default: any } = { manual: false, default: null }
) {
  const loading = ref(false);
  const error = ref<any>(null);
  const resData = ref(null);
  const total = ref(0);

  // 手动执行
  const run = async () => {
    loading.value = true;
    const data = unref(defaultData);
    const otherParams: any = {};
    if (method === "GET" || method === "DELETE") {
      otherParams.params = data;
    } else {
      otherParams.data = data;
    }
    return (userRequest({
      method,
      url: unref(path),
      ...otherParams,
    }) as any)
      .then((res: any) => {
        const { data: responseData, total: responseTotal } = res;
        loading.value = false;
        resData.value = responseData;
        if (responseTotal) {
          total.value = responseTotal;
        }
        return res;
      })
      .catch((err: any) => {
        error.value = err?.message;
        loading.value = false;
        Message.error(err.value || "未知错误");
      });
  };

  // 如果非手动执行,直接执行
  if (!options.manual) {
    run();
  }

  return {
    loading,
    error,
    data: computed(() => resData.value || options.default),
    run,
    total,
  };
}