为什么应该优先选择 Pinia/Vuex,而不是经典的服务类方法?

huangapple go评论61阅读模式
英文:

Why should Pinia/Vuex be preferred over classic approach with service classes?

问题

<!-- language-all: lang-js -->

Pinia/Vuex

Pinia/Vuex,以及Redux,旨在成为“单一数据源”,即您可以有一个或多个存储库来保存应用程序数据,可以从任何地方访问。

Pinia存储库看起来像这样:

export let useProductsStore = defineStore('products', () => {
    let data = ref(products);

    function getList(params) {
        return someSearchStuffForProducts(params);
    }

    return { data, getList };
});

然后可以这样使用:

let productsStore = useProductsStore();
console.log(data, data.value);
productsStore.getList(params);

我们可以创建多个存储库:

let usersStore = useUsersStore();
let productsStore = useProductsStore();
let basketStore = useBasketStore();
let favoritesStore = useFavoritesStore();

存储库可以相互引用:

export let useUsersStore = defineStore('users', () => {
    let productsStore = useProductsStore();
});

export let useBasketsStore = defineStore('basket', () => {
    let productsStore = useProductsStore();
});

//等等

最终,Pinia/Vuex是提供检索和操作存储在状态中的数据的工具。

管理器/服务类

但还有另一种方法,这是一种成熟的方法:管理器/服务类。

之前的例子可以重写为:

//定义“单一数据源”
let store = {
    products: { /* ... */ },
    currentUser: { /* ... */ },
    userBasket: { /* ... */ },
    userFavorites: { /* ... */ },
};

//这里是管理器类
class ProductsManager {
    constructor(params) {
        this.state = params.state;
        //...
    }

    getList(params) {
        return someSearchStuffForProducts(params);
    }
}

class UsersManager {
    constructor(params) {
        this.state = params.state;
        //Products manager作为依赖项注入
        this.productsManager = params.productsManager;
        //...
    }
}

class BasketManager {
    constructor(params) {
        this.state = params.state;
        //Products manager作为依赖项注入
        this.productsManager = params.productsManager;
        //...
    }
}

//一些配置/初始化脚本
export let DIC = {}; //管理器实例的容器
DIC.productsManager = new ProductsManager({ state: store.products });
DIC.usersManager = new UsersManager({
    state: store.currentUser,
    productsManager: DIC.productsManager,
});
DIC.basketManager = new BasketManager({
    state: store.userBasket,
    productsManager: DIC.productsManager,
});

//用法
import { DIC } from './config';
DIC.productsManager.getList();
DIC.basketManager.add(someProductId);
DIC.basketManager.changeCount(someProductId, 3);

所有这些都可以在TypeScript中轻松输入,无需额外的包装、ref()等。

讨论

就我所见,Pinia看起来像是“重复造轮子”:相同的功能用笨拙的方式编写。

而且,它不提供依赖注入:您无法在配置中初始化存储库并准确地将一个存储库注入到另一个存储库中,您必须通过useProductsStore()等方式硬编码依赖关系。

继承或任何其他面向对象编程(OOP)的东西也不可能实现。

Pinia甚至推崇循环依赖,这会导致代码混乱,难以维护。

那么,为什么有人会更喜欢Pinia/Vuex而不是经过时间考验的、清晰的OOP方法与管理器类?我已经花费了几十个小时编写自己构思的教程项目,使用Pinia作为“Vue的下一个推荐状态管理工具”,现在我感到很想将一切都重写为管理器类,因为我觉得Pinia笨重而多余。我刚刚想起几年前,我曾经在写另一个测试项目——使用Vue2——那时我使用了管理器类——一切都运行得非常顺利。我是不是忽略了什么?如果我放弃Pinia,会不会遇到问题?

英文:

<!-- language-all: lang-js -->

Pinia/Vuex

Pinia/Vuex, as well as Redux, are designed to be a "single source of truth", where you have a store or multiple stores that hold the application data, available from everywhere.

A Pinia store looks like this:

export let useProductsStore = defineStore(&#39;products&#39;, () =&gt; {
let data = ref(products);
function getList (params) {
return someSearchStuffForProducts(params);
}
return {data, getList};
});

Then can be used as:

let productsStore = useProductsStore();
console.log(data, data.value);
productsStore.getList(params);

We can create multiple stores:

let usersStore     = useUsersStore();
let productsStore  = useProductsStore();
let basketStore    = useBasketStore();
let favoritesStore = useFavoritesStore();

Stores can refer to each other:

export let useUsersStore = defineStore(&#39;users&#39;, () =&gt; {
let productsStore = useProductsStore();
}
export let useBasketsStore = defineStore(&#39;basket&#39;, () =&gt; {
let productsStore = useProductsStore();
}
//Et cetera

In the end, Pinia/Vuex are tools that provide abilities to retrieve and manipulate data stored in states.

Manager/service classes

But there's another approach, well-established one: manager/service classes.

Previous examples can be rewritten as:

//Define the &quot;single source of truth&quot;
let store = {
products:      { /* ... */},
currentUser:   { /* ... */},
userBasket:    { /* ... */},
userFavorites: { /* ... */},
};
//Here goes manager classes
class ProductsManager {
constructor (params) {
this.state = params.state;
//...
}
getList (params) {
return someSearchStuffForProducts(params);
}
}
class UsersManager {
constructor (params) {
this.state = params.state;
//Products manager is injected as a dependency
this.productsManager = params.productsManager;
//...
}
}
class BasketManager {
constructor (params) {
this.state = params.state;
//Products manager is injected as a dependency
this.productsManager = params.productsManager;
//...
}
}
//Some config/initialization script
export let DIC = {}; //Container for manager instances
DIC.productsManager = new ProductsManager({state: store.products});
DIC.usersManager = new usersManager({
state:           store.currentUser,
productsManager: DIC.productsManager,
});
DIC.basketManager = new BasketManager({
state:           store.userBasket,
productsManager: DIC.productsManager,
});
//Usage
import {DIC} from &#39;./config&#39;;
DIC.productsManager.getList();
DIC.basketManager.add(someProductId);
DIC.basketManager.changeCount(someProductId, 3);

All of this can be easily typed in TypeScript without additional wrappers, ref(), etc.

Discussion

As far as I can see, Pinia looks like "reinventing the wheel": same functionality written in a clumsy way.

Moreover, it doesn't provide dependency injection: you can't init stores in the config and accurately inject one store to another, you have to hardcode dependencies right into a store, by useProductsStore() and such.

Inheritance or any other OOP stuff aren't possible too.

Pinia even promotes circular dependencies, which leads to spaghetti code with poor maintainability.

So, after all, why should one prefer Pinia/Vuex over battle-tested, clean OOP approach with manager classes? I've been writing my self-invented tutorial project for dozens of hours, using Pinia as "next recommended state management for Vue", and now I feel tempted to rewrite everything into manager classes, as I find Pinia unwieldy and abundant. I just recalled that several years ago I was writing another test project - with Vue2 - I used manager classes then - and everything was working smoothly. Do I overlook something? Will I have problems if I abandon Pinia?

答案1

得分: 7

在Vue的响应性中,类是二等公民,并且存在一些潜在问题。它们无法在构造函数中绑定this,这会导致使用非响应式的类实例而不是响应式代理。它们无法高效地使用refs,因为这些refs在文档中以一种已记录但异常的方式解包。它们也不能使用计算refsget/set访问器。这些问题要求要么以一种奇怪的方式编写类,明确使用Vue响应性API,要么以一种受限的方式设计类,以使reactive(new MyClass)不会妨碍其正确工作。

与存储相比,类没有像Vue Devtools、插件系统等那样的功能支持。

在JavaScript中,类也无法序列化,因此保存和恢复状态需要自定义逻辑,而不是像存储持久性插件中所做的简单JSON(反)序列化。

依赖注入并不限于类,可以以适当的方式执行,例如对于Pinia存储:

const basketManagerStore = defineStore({
  state: () => ({ _getFoo: null }),
  getters: { 
    foo: state => state._getFoo()
  },
  actions: {
    setFoo(getFoo) {
      this._getFoo = getFoo;
    }
  }
});
    
basketManagerStore.setFoo(useSomeFooStore);

在许多情况下,与存储实例相比,更推荐处理Pinia存储组合,因为这可以解决在调用组合太早时可能出现的循环依赖问题。与类相同的问题可能会要求使用DI容器而不是直接使用类实例。

继承方面没有问题,因为可以使用函数式编程(FP)而不是面向对象编程(OOP)来处理可重用代码。Vue虽然不明确推广它,但使前者更加成为惯用且舒适的使用方式。

简而言之:因为Vue响应性的主要用例是处理普通对象和FP,所以最好坚持使用普通对象和FP。

英文:

Classes are second-class citizens in Vue reactivity and have some pitfalls. They cannot bind this in a constructor, this will result in using non-reactive class instance instead of reactive proxy. They cannot efficiently use refs because these ones are unwrapped in a documented but abnormal way. They cannot make use of get/set accessors for computed refs. These concerns require to either write a class in an odd way with the explicit use of Vue reactivity API, or design a class in a restricted way, so reactive(new MyClass) won't prevent it from working correctly.

Classes don't have the features that stores have, like the extensive support for Vue devtools, plugin system, etc.

Classes are also not serializable in JavaScript, so saving and restoring a state requires custom logic instead of simple JSON (un)serialization like it's done in store persistence plugins.

Dependency injection isn't unique to classes and can be performed in a suitable way, e.g. for Pinia stores:

const basketManagerStore = defineStore({
state: () =&gt; ({ _getFoo: null }),
getters: { 
foo: state =&gt; state._getFoo()
},
actions: {
setFoo(getFoo) {
this._getFoo = getFoo;
}
}
});
basketManagerStore.setFoo(useSomeFooStore);

It's preferable to deal with Pinia store composables instead of store instances in many cases because this resolves circular dependencies that could be a problem if a composable is called too early. The same problem could appear with classes and require to use DI container instead of using class instances directly.

There are no problems with inheritance because reusable code can be treated with FP instead of OOP. Vue doesn't explicitly promote it but makes the former more idiomatic and comfortable to use.

TL;DR: stick to plain objects and FP because this is the primarily case that Vue reactivity was designed for.

huangapple
  • 本文由 发表于 2023年5月14日 20:20:04
  • 转载请务必保留本文链接:https://go.coder-hub.com/76247444.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定