我有一个模式,用来尝试在 vanilla js 中保留尽可能多的逻辑(单元测试、数据获取等),并使用 Vue 来处理渲染或仅在框架中可能的任何其他功能。这样我的应用程序就尽可能与框架无关。
我有一个项目,在过去几年中从 Vue 2 到 Vue 2.6 以及组合 API 都取得了进展。本质上:我有一个普通的 js 对象,然后通过另一个“useMyObject”样式函数公开它们并将状态设置为反应式。然后,我将所有操作绑定到我的“vanilla”js 控制器,并将模板绑定到反应状态。在 vue 2.6 + compost 中,这一切顺利。
我已经轻视了以下内容,但作为一个例子:
// Vanilla JS file nick-controller.ts
export interface Person {
id: number
firstName: string
lastName: string
age: number
address: {
line1: string
line2: string
city: string
state: string
zip: string
}
}
export interface NickControllerState {
people: Person[]
personEditing: Person
isoTimeStamp: string
}
export function factoryNickController() {
const state: NickControllerState = {
people: [],
personEditing: {
id: 0,
firstName: '',
lastName: '',
age: 0,
address: {
line1: '',
line2: '',
city: '',
state: '',
zip: ''
}
},
isoTimeStamp: ''
}
function addPerson(person: Person) {
state.people.push(person)
state.isoTimeStamp = new Date().toISOString()
}
return {
state,
addPerson
}
}
然后是要反应的文件(我添加了“addTestPerson”只是为了我的头发拉扯测试,但通常这个文件只有几行长)
export function useNickController() {
const controller = factoryNickController()
// * * * * * * * * * * * * * * * * * * * * * * * *
// * * * * * * * * * * * * * * * * * * * * * * * *
// * * * Make the vanilla js state reactive. * * *
// * * * * * * * * * * * * * * * * * * * * * * * *
// * * * * * * * * * * * * * * * * * * * * * * * *
const state = reactive(controller.state)
function addTestPerson() {
addPersonCounter++
// create random 5 digit number
const random5DigitNumber = Math.floor(10000 + Math.random() * 90000)
const person: Person = {
id: addPersonCounter,
firstName: 'Nick_' + addPersonCounter.toString(),
lastName: 'Benz_' + addPersonCounter.toString(),
age: 50,
address: {
line1: '123 Main St',
line2: '',
city: 'Denver',
state: 'CO',
zip: random5DigitNumber.toString()
}
}
controller.addPerson(person)
}
return {
state,
controller,
addTestPerson,
}
}
然后创建一个单文件组件,在“useFunction”中添加...
<template>
<div>
<button @click="nickController.addTestPerson()">Add</button>
<ul v-for="person in nickController.state.people" :key="person.id">
<li>id: {{ person.id + person.firstName + '-' + person.lastName }}</li>
</ul>
</div>
</template>
<script lang="ts">
import { defineComponent, type SetupContext } from 'vue'
import { useNickController } from '@/modules/nick/useNickController'
export default defineComponent({
name: 'NickTestControllerPage',
components: {},
props: {},
setup(props: any, context: SetupContext) {
let nickController = useNickController()
return { nickController }
}
})
</script>
按下添加按钮或以任何方式更改状态,模板重新渲染...完美。
我现在参与了一个Vue 3项目,如果我在“defineComponent”SFC中使用上述方法或者使用Vue首选的“setup”方法...
<script setup lang="ts">
import { useNickController } from '@/modules/nick/useNickController'
import {
factoryNickController,
type NickControllerState,
type Person
} from '@/modules/nick/nick-controller'
import { reactive} from 'vue'
const nickController = useNickController()
function addTestPerson() {
// create random 5 digit number
const random5DigitNumber = Math.floor(10000 + Math.random() * 90000)
...etc...etc...
nickController.state.people.push(person)
}
</script>
...它不会更新,直到“下一个刻度”渲染。例如。在页面上粘贴一个绑定到某个局部变量的输入字段...
const myField = ref("")
...更改字段值,触发重新渲染...然后渲染我的反应式香草状态下的 v-for。
我已经尝试了我能想到的所有不同方法:计算、观察者、组件中的普通对象......我无法让它工作。
重申一下:在 vue 2.6 + 组合 api 中,这非常有效。
我们将非常欢迎任何帮助或指点。预先感谢。
Vue 2 修改对象以便做出反应,并为现有属性添加 getter/setter 对,如 documentation 中所述,
reactive
必须在 2.7 组合 API 中使用此机制。 Vue 3 用 Proxy
包裹一个对象,并保持原始对象完好无损。
这是 Vue 中反应式类的常见陷阱。尽管该示例没有使用类,但问题是相同的;对非反应性原始对象的引用被保留和修改,并且无法跟踪更改。解决方案也是一样的,反应状态应该一致引用,最直接的方法是使用
this
代替非反应 state
变量:
...
return {
state,
addPerson(person: Person) {
// this.state !== state in Vue 3
this.state.people.push(person)
this.state.isoTimeStamp = new Date().toISOString()
}
}
...
以这种方式定义方法时,
this
可以正确推断 TS 类型。
使用方式如下:
const controller = reactive(factoryNickController());
不同之处在于,用
addPerson(...)
代替 controller.addPerson(...)
会导致上下文错误,需要重新绑定方法才能允许这种用法。