TypeScript如何在MVVM模式下实现仅对View公开的类字段?
方案1:TypeScript 编译时校验(零运行时开销)
这个方案通过类型拆分实现权限控制,完全符合你期望的语法效果,开发阶段就能发现非法调用:
首先定义类型工具和view高阶函数的类型:
// 定义装饰器,标记仅View可访问的属性 const publicForView = (_target: any, _propertyKey: string) => {}; // 拆分ViewModel的两种对外接口 type PublicVM<T> = Omit<T, keyof { [K in keyof T as T[K] extends `${string}doSomethingInViewModel` ? K : never]: any }>; type ViewAccessibleVM<T> = PublicVM<T> & Pick<T, { [K in keyof T as T[K] extends `${string}doSomethingInView` ? K : never][number] }>; // view高阶函数的类型定义,仅这里会传入ViewAccessibleVM类型的实例 declare function view<T extends new (...args: any[]) => any>( VM: T, render: (props: { viewModel: ViewAccessibleVM<InstanceType<T>> }) => JSX.Element ): () => JSX.Element;
业务代码可以直接写成你期望的格式:
class ViewModel1 { // 公开方法,所有位置可调用 doSomething = () => { console.log('doSomething') }; // 仅View可访问的方法 @publicForView doSomethingInView = () => { console.log('doSomethingInView') }; // 类私有方法,仅内部可调用 private doSomethingInViewModel = () => { console.log('doSomethingInViewModel') }; } const View1 = view(ViewModel1, ({ viewModel }) => { viewModel.doSomethingInView(); // 正常调用 viewModel.doSomething(); // 正常调用 viewModel.doSomethingInViewModel(); // TS直接报错,无法调用 return <div/>; }); class ViewModel2 { private viewModel1: PublicVM<ViewModel1> = new ViewModel1(); constructor() { this.viewModel1.doSomethingInView(); // TS直接报错,无法调用 this.viewModel1.doSomething(); // 正常调用 // @ts-expect-error 私有方法不可访问 this.viewModel1.doSomethingInViewModel(); // 报错 } }
这个方案的优势是没有额外运行时代码,所有校验在编译阶段完成,符合常规前端开发习惯。
方案2:运行时权限拦截
如果需要防止编译绕过后的非法调用,可以在view高阶函数中用Proxy做运行时拦截:
// 存储每个VM实例对应的仅View可访问的属性名 const viewAccessibleProps = new WeakMap(); const publicForView = (target, propertyKey) => { if (!viewAccessibleProps.has(target.constructor)) { viewAccessibleProps.set(target.constructor, new Set()); } viewAccessibleProps.get(target.constructor).add(propertyKey); }; function view(VM, render) { return () => { const vm = new VM(); // 只有在view高阶函数内部才返回带访问权限的Proxy const vmForView = new Proxy(vm, { get(target, prop) { // 私有方法直接不可访问 if (prop.startsWith('#') || prop === 'doSomethingInViewModel') { throw new Error(`私有属性${String(prop)}不可访问`); } return target[prop]; } }); return render({ viewModel: vmForView }); }; } // 其他位置获取VM实例时,返回仅暴露公共方法的Proxy function createPublicVM(VM) { const vm = new VM(); const viewProps = viewAccessibleProps.get(VM) || new Set(); return new Proxy(vm, { get(target, prop) { if (viewProps.has(prop) || prop.startsWith('#') || prop === 'doSomethingInViewModel') { throw new Error(`属性${String(prop)}不可访问`); } return target[prop]; } }); }
使用时其他ViewModel获取的是公共VM实例,仅View组件能拿到带view专属权限的实例,运行时非法调用会直接抛出错误。
内容的提问来源于stack exchange,提问作者Yoskutik
相关产品推荐
相关产品推荐

