Angular/Vue/React 联合教程(一)抛弃 MVVM

抛开框架细枝末节的差异,以及开玩笑式的语法区别,本系列文章会将 Angular/Vue/React 联合在一起进行阐述,只重点讲述 三大框架都通用 的核心开发理念

MVVM 模式,中介模式的一种,构建原系统到目标系统的一个中介模型,建立起两系统交互的桥梁

时代在变化,对于编程来说,一个很明显的趋势就是:

无论什么模式实现,都有向适配器发展的趋势

因为工具的发展会越来越完善,封装会越来越彻底,由于应用开发越来越复杂,因此工具的心智负担也需要相应地降低,以便提升开发效率

前端广泛使用的中介模式 MVVM 也是被替代者中的一员

以 vue 为例,不论框架以什么模式实现,当用户使用时,将框架视为适配器是最优解:

在 Vue2 中,当我们进行组件开发时,我们需要复写以下结构:

// MVVM
<script>
export default {
     data(){
        return {}
     },
     computed:{
        // 数据映射
     },
     watch:{
        // io 源
     },
     methods:{
        // 方法
     },
     // 生命周期
     updated(){
     },
     created(){
     },
     mounted(){
     },
     beforeDestroy(){
     }
     // ...
}
</script>
 

很多人在这里遇到了学习上的困难,比如:

  1. computed/watch 什么意思?有什么区别?
  2. 组件中生命周期的调用顺序是什么?组件间生命周期调用顺序又是什么?
  3. 为什么部分情况下需要 nextTick 进行变更?
  4. 如果数据需要跨组件进行访问,该怎么实现?怎么弥合不同组件间的生命周期?
  5. 如何保持组件状态?为何采用 keep-alive 需要替换生命周期?
  6. 如何变更嵌套的 data?

这些问题,放置在 React/ng 中,就是类似”如何实现 keep-alive, 为何有 changed after checked 错误?多组件共享服务为何有意外变更?等等等……”

如果一个东西使用起来困难或复杂,优先考虑工具的问题

因为程序员最伟大的特质,就是 ——

请你想想,遇到这些问题的根源是什么?

没错,根源就是 MVVM,或者说 ——

MVVM 被暴露给了用户

  • 生命周期是什么? 是 M,V ,VM 之间不断同步的回调
  • mount 子到父,update 父到子?因为变检和patch顺序不同
  • 为什么跨组件的数据访问,会是个问题? 因为逻辑被 MVVM 打断,不同组件间调度彼此隔离,仅通过 props 绑定
  • 为什么组件状态保持是个问题? 因为没有方便的方案,只能将 M,VM 都缓存下来

归根到底,生命周期,跨组件逻辑复用,组件调度等等问题,都理应是框架处理的问题!

框架将本该他处理的问题交给你,你当然会觉得难受,当然复用出现困难

而现在,你只需要写你的代码逻辑,框架,只是一个帮你响应视图的工具 ——

只有 M -> V , 不必考虑 VM

<script setup>
// 跨组件 状态逻辑 (注意,是状态逻辑,不单单是状态)
// 又称 依赖注入
const someOtherLogic = inject(someToken)

export const someMethod = ()=>{
  someOtherLogic.callSomeFunc()
}
export const {xxx} = someOtherLogic
export const local = ref({xxx:'xxx'})
// 响应式io
watch([local], (res)=>{
 // in
})
const newLocal = computed(()=>local)
</script>
 

我们来看看消失了什么?

  1. props
  2. 生命周期
  3. 跨组件相关工具的必要性

这些东西的消失,带来的后果就是 ——

组件(前端意义上的组件),只是一个处理视图和相关数据绑定的地方,并不承担具体的逻辑(setup 只是个绑定函数的地方)

props 不再变得必要,因为有个更好的处理组件间状态逻辑共享的方案 —— 依赖注入

换言之,你只需要写你的代码逻辑,不必担心框架会对你的代码进行怎样的解析

父组件得告诉子组件,你只是我的一部分 —— Key绑定

props 的作用是什么?父子组件交互?

不对,因为单纯只处理父子组件交互的工具,不具备完善的复用性(中间再加一层组建封装,就没得玩了)

所以,只采用依赖注入,可以实现所有的跨组件逻辑么?

先别急着下结论,我们来看看依赖注入经常会遇到的问题 ——

迭代分形

// 父组件
<script setup>
export const data = ref([1,2,3,4,5])
provide('parent',data)
</script>
<template>
  <ChildCompo v-for="(item,key) in data" :key="key"/>
</template>

// 子组件
<script setup>
const parent = inject('parent')
</script>
 

子组件拿到的,只是父组件的全部数据,如何知道自己是 v-for 生成的实例中的哪一个?

很简单,只需要:

// 子组件
<script setup>
const key = getCurrentInstance()?.vnode?.key
const parent = inject('parent')
const currentData = parent.value[key]
</script>
 

这样,就能将当前组件实例,与父组件中声明的列表实例项匹配起来

然而,getCurrentInstance 是一个限制颇多,且不符合使用习惯的 api,因此,这个时候可以捡起 props,用更优雅的方式定义:

// 父组件
<script setup>
export const data = ref([1,2,3,4,5])
provide('parent',data)
</script>
<template>
  <ChildCompo v-for="(item,key) in data" :key="key" :index="key"/>
</template>

// 子组件
<script setup>
const props = defineProps(['index'])
const parent = inject('parent')
const currentData = parent.value[props.index]
</script>
 

这里需要注意的是:

  1. :key=”xxx” 是给框架使用的迭代标识, :index 是组件使用的迭代标识
  2. 注意:从祖先组件获取的 ref/reactive ,只能通过其进行赋值

const parentData = inject('parent')

parentData.xxx.xxx.xxx = xxx 生效

const xxx = parent.xxx.xxx; xxx.xxx = xxx 不生效或无法监听

永远记住,你操作的是 proxy

迭代分形是一个非常重要的概念,甚至代码结构都需要按照是否出现分形进行划分,后面的文章会从 DDD 的角度,专门讨论 aggregation划分 的问题

但是,如果我们把 parentData[key] 的取值,也看成逻辑,显然,这个逻辑应该放置在父组件更好一些(父组件取值更自然)

那我们封装一个很巧的组件,避开这种硬邦邦的分形绑定

// Mapping.vue
<script setup name="Mapping">
const props = defineProps(['data','prividerKey'])
provide(props.providerKey, props.data)
</script>
<template>
  <slot></slot>
</template>

// 直接在父组件:
<script setup>
import Mapping from './Mapping.vue'
export const data = ref([1,2,3,4,5])
provide('parent',data)
</script>
<template>
  <Mapping v-for="item in data" :key="key" :providerKey="dataItem" :data="item">
    <ChildCompo />
  </div>
</template>
 

这种方式,暂且称之为 “Key绑定注入组件”

眼尖的朋友们肯定会发现,这种做法其实已经在 UI 库中广泛出现,比如 Antd React:

 <Form
{...layout}
name="basic"
initialValues={{ remember: true }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
>
<Form.Item
label="Username"
name="username"
rules={[{ required: true, message: 'Please input your username!' }]}
>
<Input />
</Form.Item>
<Form.Item
label="Password"
name="password"
rules={[{ required: true, message: 'Please input your password!' }]}
>
<Input.Password />
</Form.Item>
<Form.Item {...tailLayout} name="remember" valuePropName="checked">
<Checkbox>Remember me</Checkbox>
</Form.Item>
<Form.Item {...tailLayout}>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>

用一个 key 绑定并 privide (或者拥有相同职能)的组件,就能将集合数据的一部分,投射给子组件,看并且绕过生硬的 props 数据传递

有了这些技巧,你的代码,就可以 ——

完全脱离组件编程

// someSetup.js
function someSetup(){
return {
// ...
}
}
// SomeComponent.vue
<script>
import someSetup from './someSetup.js'
export default defineComponent({setup:someSetup})
</script>
<template>
</template>

也就是说,状态逻辑真正地写在 js 文件中

这一点 React 和 Angular 也是一样的(逻辑不在组件中):

// react
// useSomeService.ts
useSomeService(){
return {/* ... */}
}
function Component(){
const someService = useSomeService()
return /* ... */
}
// Angular
// some.service.ts
@injectable()
export default class SomeService{
// ...
}
// 组件内
constructor(public someService: SomeService){}

关于 Ng 特殊的全类型依赖注入会在 SOA 主题下专门讨论

甚至在 Angular 的代码风格要求中,白纸黑字地写着:

坚持在组件中只包含与视图相关的逻辑。所有其它逻辑都应该放到服务中。

逻辑代码,是你的逻辑代码

没有生命周期,没有拆分复用的负担,你就好好写你的代码就行了

框架重要么?框架不重要!

作为一个适配器,已经不具备切换的学习成本了

原创文章,作者:我心飞翔,如若转载,请注明出处:https://www.pipipi.net/12590.html

发表评论

登录后才能评论