本文是 Vue3 第一次实践的总结,重点在面向对象上,也会引出一些函数式使用和相关切入点的讲解

复用带来的全新开发体验

Vue3 更新了逻辑复用,逻辑能够很方便地进行统一提取和使用,这其实是一切编程范式的基础,意味着在 Vue3 这一平台至上,更多的编程模型可以被应用,工程化职业化更进一步,构建复杂应用的能力也更上了一级台阶Vue3 虽然是前端三大框架中最晚实现逻辑复用的框架,Angular 在15年底更新了 service 支持,react 在18年支持了 hooks ,但是 Vue 是目前受众最广,前端应用最多的框架,同时其复用的使用成本是三者中最低的,因此其对复用的支持,才是最大的标志性事件

由于其保持向后兼容,对上一版本的写法继续提供支持,导致可能降低新写法的使用体验,,因此,实践新的思维模型和写法,需要很好的实践方法才能办到

首先,我们来看看在 zone,cycle,react 中广泛存在的前端开发模型:intent 我们称之为监听,包含输入事件,网络事件,浏览器调度,时间回调等,它是一切变更的来源,是一切逻辑的起点

model 我们称之为模型,包含需要操作的数据结构,绑定 intent 的响应接口,开发者定义的运行逻辑等

view 我们称之为视图,是一切逻辑的终点

从这一点上将,逻辑复用就是将 model 的部分完全独立,你对其有完全的编程权限

而不需要去配置选项里一个个设置,比如 Vue2 版本中,你需要:

{

data(){

return { /* ... */ }

},

computed:{

name(){

return //xxx }

},

created(){

// xxxx }

}

这种写法无任何编程可言,无法迭代,无法判定,无法封装

后果就是,一旦业务复杂,你需要做许多重复劳动,需要做很多复杂的方式绕过复用,需要去使用很多反模式的技巧(比如 mixin )

其中最广为人知的,就是 Vuex ,全局命令模式逻辑写法(类 Redux)全部数据与逻辑集成于一处,module 无初始化逻辑,数据迭代处理困难,复杂数据结构构造困难,结果是提升了一致性,牺牲了低耦合,降低了横向扩展能力,无异步支持,导致大型应用开发举步维艰

但是 Vue3 的发布(rc)改变了这一现状,响应式集中于一处,并且能形成统一结构传递,不在被局限于组件内部这一点需要与单向数据流做区分,上文提到的 intent -> model -> view 就是单向数据流的本意,响应式集中于一处进行传递,传递的是结构的引用,并非具体数据本身,而组件必然是由父到子,单向数据流能够得到保证,无需担心

因此,你可以轻易地配合依赖注入,构造出一个 model 模块,其中包含所有的 数据/逻辑/接口,使得其下所有组件都能访问到该数据,并保持一致性

模块与模块之间可以交换信息,轻松处理 迭代/结构/初始化问题,而这一问题在上一版本中即便利用 Vuex 都是非常难以解决的

另外一点,用户可以实现 model ,自然第三方库也是可以的,因此之后的生态,用户只需要编写 intent -> model , model -> view 的代码即可,大大减轻了工程开发上的负担

因此,Vue3 可以预见,必然会得到大范围推广,在有成熟生态的情况下,用户的上手难度会非常低,可以预见会在未来的大项目中得到应用

但是,完全针对 model 的开发,对于第三方开发者而言,难度会更上一级台阶,需要有更强大的思想方法工具,面向对象是个很好的方案

当然,pinia 也是很好的方案,用命令模式做出局部 Vuex,理念与 React 的 useReducer类似,用函数的方式解决问题,但是其写法与自定义 composition 无太大差别,优势是SSR,统一 model 结构在跨平台上有优势,另外在 rootStore 中做了 store 记录,并有以下代码:

devtoolHook.emit(

'vuex:mutation',

{

...mutation,

type: `[${mutation.storeName}]${mutation.type}`,

},

rootStore.state

)

利用了调试工具,各位也可以试试直接如此:

constdevtoolHook: DevtoolHook | undefined=target.__VUE_DEVTOOLS_GLOBAL_HOOK__

不过,这种需要额外库的方式不说引入负担的问题,额外api 的问题,在处理数据结构上,面向对象一直都是拿手好戏,为什么不直接用老办法呢?要知道在这个方向上,前人已经探索了好多个十年了,为啥不直接拿来主义呢?

面向对象的结构优势

如果不是用 Typescript 的,可以不看了,面向对象在 Vue3 中无用,但是如果用的是 Ts,那么很高兴告诉你,面向对象可以解决绝大部分类型问题

首先,对于嵌套的复杂对象结构,采用原型对象(直接声明对象)的方式,是这么做的:

interface NestedObject {

data: {

nestedData: string

}

}

const nestedData = {

data:{

nestedData: 'test'

}

}

声明了两遍,一遍声明类型,一遍声明数据,并且类型和数据之间是分离的,在 Vue2 中 Vuex 配合 Typescript 使用过的朋友们应该深有感触

其实前辈们早就总结出来这套用法的优势与劣势:优点: 1、性能提高。 2、逃避构造函数的约束。

缺点: 1、配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。 2、必须实现 Cloneable 接口。

通盘考虑在别的语言中表现为复杂的逻辑处理,在 Typescript 中表现为两次类型声明(接口)但是并不是说这种方式就不用,因为原型模式也是设计模式的一种,js 对于原型模式的支持是优势,如果没有复杂嵌套,直接使用原型模式也是非常好的

比如如果有嵌套:

interface NestedObject {

data: {

nestedData: string | NestedObject

}

}

如果采用 class 的声明方式:

class NestedObject {

data: NestedObject | string

constructor(value: string | NestedObject){

this.data = value

}

}

数据和类型的声明被保持在一处,可以防止很多类型问题的发生

另外一个结构的优势,是在于 Vue3 本身的特性,我们声明一个 reactive 数据:

const test = reactive({test: ''})

将其传递给子组件:

会出现这个问题:

为什么会出现这种问题呢?

深入理解了就会发现,Vue 的 props 与 React 和 Angular 的 props 不同,它会进行值绑定,将 props 中的数据映射到子组件 data 中

在上一个版本的非响应式传递方案中,这个方式实际上是一种性能优化

但是当前版本确实一种负担,虽然也能起到强制用户在 view 进行数据消费的作用

所以,你需要这么绕过它:

const test = { testData: reactive({ test: "test" }) }

需要在对象外层,再套一层引用,将值绑定转化为引用绑定

而且,Vue 的 ref 也是其一个再套一层的作用,但是却会在模板中自动解包

为了避免这一系列的恐怖方案,直接一个面向对象 new 过去解决问题:

const test = new TestData()

心智负担是不是一下子降到了0,不过注意以下几点:所有面向对象的实体都不要在 setup 返回对象中解包,不能 return { ... new TestData() }

所有响应式都需要在 constructor 里面初始化

重点说一下第二点:

class Form {

model: { name: string}

constructor(model: {name:string}){

this.model = reactive(model)

}

}

这么写很容易理解,相当于 为model附上了响应式功能

ref 的话是如此 :

class Form {

model: Ref

constructor(model: {name:string}){

this.model = ref(model)

}

}

这两者有以下问题:ref 出现在 reactive 中,会被解包,而且是属性解包,数组,map不解包

ref 出现在模板中自动解包

数组需要用 ref,或者 reactive 中通过属性声明

好了,大家觉得什么时候用 ref 什么时候 用 reactive 呢?

一目了然,为了避免负担,所有数据全部用 react 声明,你只要意识到 unref,computed 之类的处理会输出 ref 就行了,即:

reactive 是声明用的,ref 是处理用的

唯一例外是模板 ref,需要单独区分

如果可以的话,class 中的响应式数据应该在一处声明,比如:

class TestService{

state: {name:string,password:string,arr: string[]}

div: Ref

notReactiveData = 'test'

constructor(){

this.state = reactive({name:'',password:'',arr:[]})

this.div = ref(null)

}

}

怎么判断数据是否需要响应式呢?

需要在模板中用到,改变和需要视图配合改变

即 v-if v-for 组件参数等

依赖注入

需要你的服务不管何处都能访问到,即需要依赖注入

由于装饰器尚处于试验阶段(很多人说 ng 也在用,谷歌也在用,嗯,这就是 ng 放弃 js 的原因),因此没有自动依赖注入,依赖注入都是手动的

开惯了自动挡的 nger ,来试试开手动挡把~

核心就是利用 静态方法 和静态属性 :

class SomeService {

static token: InjectionKey = Symbol()

static setup(){

const service = new SomeService()

provide(SomeService.token, service)

return service

}

}

这个静态方法还可作为工厂函数,使用 setup 作为工厂方法名称有以下原因:短

与组件同名,方便使用者

可以直接 setup: SomeService.setup 做组件逻辑替代,不过返回数据中就不能直接返回对象咯~

这样,在子孙组件中就可以直接:

// 获取单一实例const someService = inject(SomeService.Token)

这实际上是单例,你也可以用静态方法强制单例,如果需要多例,甚至选择注入,可以声明一个非静态的 token

class SomeService{

token: InejectionKey = Symbol()

// 如果需要传递 props —— // token: string = uuid() // token: string = ~~(Math.random()*10000)+Date.now().toString()}

这样子组件就可以选择性获取上下文了

面向切面,作为函数式起点

配合装饰器,类可以很方便地实现调试测试,还记得上文中提到的 vue devtool 相关命令,以及建议统一 class 中包含 state 声明 reactive 么?

实际使用时,可能只需要如此:

class SomeService{

@debug

state: {

name:string,

password:string

}

}

就能在 vue devtool 中打印日志了,甚至做时间旅行了

这种技术叫做基于装饰器的面向切面技术,即明确数据静态结构和运行时结构的技术(面向XXX的意思是:多多考虑XXX)

既然可以很方便地扩展运行时

类同时也可作为极限函数式处理运行时问题的切面

比如 Rxjs:

class SomeComponentService {

state: any

nodeRef: any

click$ = Subject()

}

// 组件中

// 骚一点

注意,此时 Rxjs 仅仅处理运行时问题,数据静态结构在 composition 语境下使用不了

不过,其它视图无关的逻辑可以尝试使用

这下可以一步到位,纯度还需要其它工具来保证么?这可是函数流~

综上所述,Vue3 由于对逻辑复用的扩充,组件可以视为视图,即 React 宣称的,React 只是薄薄一层 View,之类的定义

逻辑,model 全在你的一个个类中,这样可以规避掉因为历史问题和用户体验问题导致的某系不适应的特性,同时让你的功能逻辑和视图逻辑脱欧

并且单个 model 配合依赖注入,可以很方便地 ——

领域驱动地划分前端功能模块

如果加上样式封装,就是妥妥的微前端(shadow root,web component,iframe)

我们将这种面向功能服务的架构,称为 SOA(面向服务),将按照功能范畴划分模块的做法称作 DDD(领域驱动)

配合 SOA 和 DDD,Vue 项目可以很方便地进行多人协作,做出大型超大型项目来

说一下命名,目前 SOA 还是没有想出好的命名方法,之前有拟定:组件劫持 Service(即组件setup等于类的setup):XXXComponentService

普通 Service: XXXService

全局 Service:XXXGlobalService

但是转念一想,架构叫 SOA 没错,但是名字一定要叫 SOA 么?Service 本身对中文使用者是有一定误导的,所以我建议这么改写:XXXCompo

XXX

XXXGlobal

Vue 组件不是采用类写的,因此不会存在歧义,外部类必然是 Service

但是还有个问题,组件如果采用 TSX 开发,组件名和服务名重叠怎么办?服务是按照功能划分的,强制你与组件取不一样的名字,如果是劫持,用 XXXCompo

另外运行切面采用 Rx 或者 Xstream 等响应式库处理保证纯度的话,响应式规则中的变量声明方式可能要改改了,原因如下:带$符很多人不习惯,尤其是 Vue 平台之前很少有人有纯响应式编程经验

reactive,ref 也是响应式变量,容易有歧义,reactive 通过 state 声明规避,ref 有类型规避,用上了这个架构必然是 Typescript,因此没有必要单独标识

事件处理需要标识

直接声明为:“clickStream”更好,也更直观

以上

更多推荐

vue面向切面_Vue3 面向对象开发指南