赞
踩
如果你的项目,只是类似活动页面等开发场景,你只需要看我得第一篇文章就可以了解 Vue composition api 了:
https://zhuanlan.zhihu.com/p/181630740zhuanlan.zhihu.com如果你需要开发较为复杂的后台页面,那可能看我的第二篇和第三篇文章,也能让你很舒服地将 Vue composition api 用起来:
https://zhuanlan.zhihu.com/p/183744518zhuanlan.zhihu.com但是如果你想要充分发挥逻辑复用的威力,做出一个长期维护拳头产品,那么这篇文章将有所裨益,当然,这个层级我也在试探,希望能与大家共勉,出了差池也希望谅解~
能做到服务封装,实现 “微”,已经很不错了吧?
照理来说,Vue composition api 的潜力已经被挖掘到极致了吧?
其实不然,即使已经做到了 服务分割,服务发现,也还远远不够,为什么呢?
回头看一下之前的例子,是不是一直都有一个问题?没发现?
我们所有的服务,最终注入并且处理的地方是在哪里?
视图组件!
- const SomeComponent = ()=>{
- // 视图组件中注入服务
- const data = inject(serviceToken)
- cosnt data_2 = useService_2(data)
- return ()=> <div/>
- }
这合理么?
中小型应用,这样是合理的,视图的结构就是功能的结构。
大型应用,这样不合理,视图以及一切部件,都是为功能和业务服务的!
我们从视图的角度来看,当在组件内部注入服务时,其子组件和孙组件都可以访问到该服务。
其它组件呢?与其平级的组件和其父组件呢?是会访问不到么?
当组件出现嵌套依赖的时候,以组件结构作为功能结构的模型将会出现重大漏洞!
这个问题在后端,被称为:
不过呢,好消息是,js 原生的模块化文件加载机制已经处理了这个问题,是不是很开心?不用再大量展开了!
因此,你只需要通过改变文件划分,就可以完成模块化改造,让代码围绕着功能逻辑展开。
那么我们来试试,首先之前依赖视图逻辑的划分方式要彻底改变:
而我们按模块化划分的文件夹应该是怎样的结构呢?
直接看 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 在相关竞品中的位置。
而 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)),看看这个例子:
发送请求并未传参,原因仅仅是有这么个函数:
- 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,
- };
- }
这个不纯,会出问题!
但是想想看,如果不死守教条可以让自己写的快一点,为什么还要按着书本不放呢?
我们本质上都是坏小孩,不是么?
有没有大佬想要搞个 Vue 版本的 ahooks ? 加我一起呀~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。