├── examples ├── sfc.d.ts ├── tsconfig.json ├── cache │ ├── index.html │ └── app.ts ├── di │ ├── index.html │ ├── app.ts │ └── store.ts ├── extends │ ├── index.html │ └── app.ts ├── start │ ├── index.html │ └── app.ts ├── index.html ├── server.js └── webpack.config.js ├── .gitignore ├── test └── unit │ ├── api.test.ts │ ├── tsconfig.json │ ├── state │ ├── plugin.test.ts │ ├── middleward.test.ts │ └── state.test.ts │ └── di │ └── inject.test.ts ├── src ├── state │ ├── scope.ts │ ├── helper.ts │ ├── compose.ts │ ├── watcher.ts │ ├── state.ts │ ├── computed.ts │ └── mutation.ts ├── di │ ├── di_meta.ts │ ├── map.ts │ ├── class_meta.ts │ ├── binding.ts │ ├── inject.ts │ ├── container.ts │ ├── injector.ts │ └── provider.ts ├── vue-class-state.ts ├── dev │ ├── strict.ts │ └── devtool.ts └── util.ts ├── release.sh ├── typings └── vue.d.ts ├── tsconfig.json ├── rollup.config.js ├── LICENSE ├── package.json ├── tslint.json └── README.md /examples/sfc.d.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | declare module '*.vue' { 4 | import Vue from 'vue'; 5 | export default Vue; 6 | } 7 | 8 | -------------------------------------------------------------------------------- /examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext" 4 | }, 5 | "extends": "../tsconfig.json" 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | node_modules 4 | docs/_book 5 | examples/**/build.js 6 | explorations 7 | *.log 8 | .rpt2_cache 9 | test/unit-build 10 | lib -------------------------------------------------------------------------------- /test/unit/api.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import * as VueClassState from '../../lib/vue-class-state.common'; 3 | 4 | test('apis', (t) => { 5 | const apis = Object.keys(VueClassState).sort(); 6 | t.deepEqual(apis, ['Container', 'Getter', 'Inject', 'Mutation', 'State', 'bind']); 7 | }); 8 | -------------------------------------------------------------------------------- /examples/cache/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | cache 7 | 8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/di/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | vue-class-state di 7 | 8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/extends/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | vue-class-state start 7 | 8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/start/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | vue-class-state start 7 | 8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/state/scope.ts: -------------------------------------------------------------------------------- 1 | import { hideProperty } from '../util'; 2 | 3 | export const scopeKey = '__scope__'; 4 | 5 | export class ScopeData { 6 | 7 | public $state: any = {}; 8 | 9 | public $getters: any = {}; 10 | 11 | public static get(ctx: any): ScopeData { 12 | return ctx[scopeKey] || (function () { 13 | const scope = new ScopeData(); 14 | hideProperty(ctx, scopeKey, scope); 15 | return scope; 16 | })(); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /examples/di/app.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Component from 'vue-class-component'; 3 | import { Inject } from 'vue-class-state'; 4 | import { AppContainer, StateKeys, Store } from './store'; 5 | 6 | @Component({ 7 | template: '
{{store.text}}
' 8 | }) 9 | class App extends Vue { 10 | 11 | // 根据注入标识在子组件中注入实例 12 | @Inject(StateKeys.STORE) store!: Store; 13 | 14 | } 15 | 16 | new Vue({ 17 | el: '#app', 18 | // 在根组件实例化一个容器,传入到provide选项 19 | provide: new AppContainer(), 20 | render: (h) => h(App) 21 | }); 22 | -------------------------------------------------------------------------------- /src/di/di_meta.ts: -------------------------------------------------------------------------------- 1 | import { IIdentifier } from '../state/helper'; 2 | import { hideProperty } from '../util'; 3 | import { Provider } from './provider'; 4 | 5 | export const meta_key = '__meta__'; 6 | 7 | export class DIMetaData { 8 | public static get(ctx: any): DIMetaData { 9 | if (!ctx[meta_key]) { 10 | hideProperty(ctx, meta_key, new DIMetaData()); 11 | } 12 | return ctx[meta_key]; 13 | } 14 | 15 | public identifier!: IIdentifier; 16 | public hasBeenInjected: boolean = false; 17 | public provider!: Provider; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | vue-class-state Examples 6 | 7 | 8 | 9 |

vue-class-state Examples

10 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/unit/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "lib": [ 6 | "dom", 7 | "es2015" 8 | ], 9 | "noImplicitAny": false, 10 | "sourceMap": false, 11 | "experimentalDecorators": true, 12 | "allowSyntheticDefaultImports": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "outDir": "../unit-build/" 16 | }, 17 | "include": [ 18 | "../../lib/**/*.ts", 19 | "./**/*.test.ts", 20 | ] 21 | } -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | echo "Enter release version: " 3 | read VERSION 4 | 5 | read -p "Releasing $VERSION - are you sure? (y/n)" -n 1 -r 6 | echo # (optional) move to a new line 7 | if [[ $REPLY =~ ^[Yy]$ ]] 8 | then 9 | echo "Releasing $VERSION ..." 10 | 11 | # run tests 12 | npm run pre-publish 13 | 14 | # build 15 | VERSION=$VERSION npm run build 16 | 17 | # commit 18 | git add -A 19 | git commit -m "[build] $VERSION" 20 | npm version $VERSION --message "[release] $VERSION" 21 | 22 | # publish 23 | git push origin refs/tags/v$VERSION 24 | git push 25 | npm publish 26 | fi -------------------------------------------------------------------------------- /typings/vue.d.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | declare module 'vue/types/options' { 4 | // tslint:disable-next-line:interface-name 5 | interface WatchOptions { 6 | sync?: boolean; 7 | } 8 | } 9 | 10 | declare module 'vue/types/vue' { 11 | // tslint:disable-next-line:interface-name 12 | interface VueConstructor { 13 | util: { 14 | defineReactive( 15 | obj: any, 16 | key: string, 17 | val: any, 18 | customSetter?: (value: any) => void 19 | ): void 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/vue-class-state.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * vue-class-state interface 3 | */ 4 | 5 | /* state */ 6 | export { IClass, IIdentifier, IPlugin } from './state/helper'; 7 | export { IMutation } from './state/mutation'; 8 | 9 | /* di */ 10 | export { IInjector, IInstanceFactory } from './di/injector'; 11 | 12 | /* module */ 13 | export { IContainerOption, IContainer } from './di/container'; 14 | 15 | /** 16 | * vue-class-state api 17 | */ 18 | export { bind } from './di/binding'; 19 | export { State } from './state/state'; 20 | export { Computed as Getter } from './state/computed'; 21 | export { Inject } from './di/inject'; 22 | export { Mutation } from './state/mutation'; 23 | export { Container } from './di/container'; 24 | -------------------------------------------------------------------------------- /test/unit/state/plugin.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { bind, Container, State } from '../../../lib/vue-class-state.common'; 3 | 4 | test('global plugin', t => { 5 | const testKey = 'test'; 6 | class Test { 7 | @State public count = 0; 8 | 9 | constructor() { 10 | t.is(this.count, 0); 11 | } 12 | } 13 | 14 | @Container({ 15 | providers: [ 16 | bind(testKey).toClass(Test) 17 | ], 18 | globalPlugins: [ 19 | (data: Test) => data.count = 100 20 | ] 21 | }) 22 | class TestContainer { } 23 | 24 | const state = new TestContainer()[testKey] as Test; 25 | t.is(state.count, 100); 26 | }); 27 | -------------------------------------------------------------------------------- /examples/extends/app.ts: -------------------------------------------------------------------------------- 1 | import { bind, Container, Getter, State } from 'vue-class-state'; 2 | 3 | class Super { 4 | @State public super = 'Super'; 5 | 6 | @Getter get superGetter() { 7 | return this.super; 8 | } 9 | } 10 | 11 | class Base extends Super { 12 | @State public base = 'Base'; 13 | 14 | @Getter get baseGetter() { 15 | return this.base; 16 | } 17 | } 18 | 19 | class Child extends Base { 20 | @State public child = 'Child'; 21 | 22 | @Getter get childGetter() { 23 | return this.child; 24 | } 25 | } 26 | 27 | @Container({ 28 | providers: [bind('child').toClass(Child)], 29 | devtool: ['child'] 30 | }) 31 | class AppContainer { } 32 | 33 | new AppContainer(); 34 | -------------------------------------------------------------------------------- /src/di/map.ts: -------------------------------------------------------------------------------- 1 | import { hasOwn } from '../util'; 2 | 3 | export interface IMap { 4 | get(key: K): V | undefined; 5 | has(key: K): boolean; 6 | set(key: K, value: V): this; 7 | } 8 | 9 | class SimpleMap implements IMap { 10 | 11 | private dictionary: object = Object.create(null); 12 | 13 | public get(key: K): V | undefined { 14 | return this.dictionary[key as any] || undefined; 15 | } 16 | 17 | public set(key: K, value: V): this { 18 | this.dictionary[key as any] = value; 19 | return this; 20 | } 21 | 22 | public has(key: K): boolean { 23 | return hasOwn(this.dictionary, key as any); 24 | } 25 | } 26 | 27 | export const UseMap = typeof Map === 'function' ? Map : SimpleMap as any; 28 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const webpack = require('webpack') 3 | const webpackDevMiddleware = require('webpack-dev-middleware') 4 | const webpackHotMiddleware = require('webpack-hot-middleware') 5 | const WebpackConfig = require('./webpack.config') 6 | 7 | const app = express() 8 | const compiler = webpack(WebpackConfig) 9 | 10 | app.use(webpackDevMiddleware(compiler, { 11 | publicPath: '/__build__/', 12 | stats: { 13 | colors: true, 14 | chunks: false 15 | } 16 | })) 17 | 18 | app.use(webpackHotMiddleware(compiler)) 19 | 20 | app.use(express.static(__dirname)) 21 | 22 | const port = process.env.PORT || 3000 23 | module.exports = app.listen(port, () => { 24 | console.log(`Server listening on http://localhost:${port}, Ctrl+C to stop`) 25 | }) -------------------------------------------------------------------------------- /src/state/helper.ts: -------------------------------------------------------------------------------- 1 | 2 | import Vue from 'vue'; 3 | import { devtoolHook } from '../dev/devtool'; 4 | import { IMiddleware } from './compose'; 5 | 6 | export interface IClass { new(...args: any[]): T; } 7 | 8 | export type IIdentifier = string; 9 | 10 | export type IPlugin = (state: any) => void; 11 | 12 | // tslint:disable-next-line:no-empty 13 | export const noop = function () { }; 14 | 15 | export const isSSR = Vue.prototype.$isServer; 16 | 17 | export const globalState = { 18 | middlewares: [] as IMiddleware[], 19 | isCommitting: false 20 | }; 21 | 22 | if (process.env.NODE_ENV !== 'production' && devtoolHook) { 23 | globalState.middlewares.push((next: any, mutation: any, state: any) => { 24 | const result = next(); 25 | devtoolHook.emit('vuex:mutation', mutation, state); 26 | return result; 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /src/di/class_meta.ts: -------------------------------------------------------------------------------- 1 | import { IIdentifier } from '../state/helper'; 2 | import { hasOwn, hideProperty } from '../util'; 3 | 4 | export interface IGetters { [key: string]: () => any; } 5 | 6 | const classMetaKey = '__meta__'; 7 | 8 | export class ClassMetaData { 9 | 10 | public static get(target: any): ClassMetaData { 11 | if (hasOwn(target.constructor, classMetaKey)) { 12 | return target.constructor[classMetaKey]; 13 | } 14 | const meta = new ClassMetaData(); 15 | hideProperty(target.constructor, classMetaKey, meta); 16 | return meta; 17 | } 18 | 19 | public injectParameterMeta: IIdentifier[] = []; 20 | 21 | public getterKeys: string[] = []; 22 | 23 | public addGetterKey(key: string) { 24 | 25 | if (this.getterKeys.indexOf(key) === -1) { 26 | this.getterKeys.push(key); 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "es2015", 7 | ], 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "experimentalDecorators": true, 11 | "strict": true, 12 | "strictFunctionTypes": false, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "suppressImplicitAnyIndexErrors": true, 16 | "allowSyntheticDefaultImports": true, 17 | "declaration": true, 18 | "declarationDir": "./lib", 19 | "outDir": "lib", 20 | "baseUrl": ".", 21 | "paths": { 22 | "vue-class-state": [ 23 | "src/vue-class-state.ts" 24 | ] 25 | } 26 | }, 27 | "include": [ 28 | "src/**/*.ts", 29 | "examples/**/*.ts", 30 | "typings/*.ts" 31 | // "test/**/*.ts" 32 | ], 33 | "exclude": [ 34 | "lib/*", 35 | "node_modules/*" 36 | ], 37 | "compileOnSave": false 38 | } -------------------------------------------------------------------------------- /src/dev/strict.ts: -------------------------------------------------------------------------------- 1 | import { DIMetaData } from '../di/di_meta'; 2 | import { watcherKey } from '../state/computed'; 3 | import { globalState } from '../state/helper'; 4 | import { ScopeData, scopeKey } from '../state/scope'; 5 | import { Watcher } from '../state/watcher'; 6 | import { assert, hideProperty } from '../util'; 7 | 8 | export function useStrict(state: any) { 9 | const identifier = DIMetaData.get(state).identifier, 10 | scope = state[scopeKey] as ScopeData || undefined; 11 | if (scope && Watcher) { 12 | if (!state[watcherKey]) { 13 | hideProperty(state, watcherKey, []); 14 | } 15 | new Watcher(state, () => { 16 | return scope.$state; 17 | }, () => { 18 | assert(globalState.isCommitting, 19 | `Do not mutate state[${identifier}] data outside mutation handlers.`); 20 | }, { deep: true, sync: true } as any 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/start/app.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { Getter, Mutation, State } from 'vue-class-state'; 3 | 4 | class Addition { 5 | 6 | // 类中的数据在初始化后会被Vue观察到 7 | @State public a = 0; 8 | @State public b = 1; 9 | 10 | // 本类中的getter 都会代理为Vue的计算属性 11 | @Getter get sum() { 12 | return this.a + this.b; 13 | } 14 | 15 | // 突变方法,与vuex一致必须为同步函数 16 | @Mutation public change() { 17 | const temp = this.sum; 18 | this.a = this.b; 19 | this.b = temp; 20 | } 21 | 22 | } 23 | 24 | const addition = new Addition(); 25 | // tslint:disable-next-line:no-console 26 | console.log(addition); 27 | new Vue({ 28 | el: '#app', 29 | template: `
{{addition.sum}}
`, 30 | computed: { 31 | addition() { 32 | return addition; 33 | } 34 | }, 35 | mounted() { 36 | setInterval(() => { 37 | this.addition.change(); 38 | }, 2000); 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | 2 | import typescript from 'rollup-plugin-typescript2'; 3 | import filesize from 'rollup-plugin-filesize'; 4 | const version = process.env.VERSION || require('./package.json').version; 5 | const banner = 6 | `/** 7 | * vue-class-state v${version} 8 | * (c) ${new Date().getFullYear()} zetaplus006 9 | * @license MIT 10 | */` 11 | const input = 'src/vue-class-state.ts'; 12 | const name = 'vue-class-state'; 13 | 14 | const options = [{ 15 | file: 'lib/vue-class-state.esm.js', 16 | format: 'es' 17 | }, { 18 | file: 'lib/vue-class-state.common.js', 19 | format: 'cjs' 20 | }] 21 | 22 | export default options.map(({ file, format, env, isMin }) => { 23 | const config = { 24 | input, 25 | output: { 26 | name, 27 | file, 28 | format, 29 | banner 30 | }, 31 | plugins: [ 32 | typescript(), 33 | filesize() 34 | ], 35 | external: ['vue'] 36 | } 37 | return config; 38 | }) 39 | 40 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | 2 | export function assert(condition: any, msg: string) { 3 | if (!condition) throw new Error(`[vue-class-state warn] ${msg}`); 4 | } 5 | 6 | const _hasOwn = Object.prototype.hasOwnProperty; 7 | export function hasOwn(obj: any, key: string) { 8 | return _hasOwn.call(obj, key); 9 | } 10 | 11 | export const def = Object.defineProperty; 12 | 13 | export function hideProperty(obj: any, key: string, value: any) { 14 | def(obj, key, { 15 | value, 16 | enumerable: false, 17 | configurable: true 18 | }); 19 | } 20 | 21 | export function defGet(obj: any, key: string, get: () => any) { 22 | def(obj, key, { 23 | get, 24 | enumerable: true, 25 | configurable: true 26 | }); 27 | } 28 | 29 | export function assign(target: T, source: U): T & U { 30 | let key; 31 | for (key in source) { 32 | if (hasOwn(source, key)) { 33 | (target as any)[key] = source[key]; 34 | } 35 | } 36 | return target as any; 37 | } 38 | 39 | export const isDev = process.env.NODE_ENV !== 'production'; 40 | -------------------------------------------------------------------------------- /src/state/compose.ts: -------------------------------------------------------------------------------- 1 | import { IMutation } from './mutation'; 2 | export type IMiddleware = (next: () => void, mutation: IMutation, state: T) => void; 3 | 4 | /** 5 | * change from https://github.com/koajs/compose/blob/master/index.js 6 | */ 7 | export function compose(middlewares: IMiddleware[]): IMiddleware { 8 | return function (next: () => void, mutation: IMutation, state: any) { 9 | let index: number = -1; 10 | return dispatch(0); 11 | function dispatch(i: number): void { 12 | if (i <= index) throw new Error('next() called multiple times'); 13 | index = i; 14 | let fn = middlewares[i]; 15 | if (i === middlewares.length) fn = next; 16 | if (!fn) return; 17 | try { 18 | // tslint:disable-next-line:no-shadowed-variable 19 | return fn(function next() { 20 | return dispatch(i + 1); 21 | }, mutation, state); 22 | } catch (err) { 23 | throw err; 24 | } 25 | } 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/di/binding.ts: -------------------------------------------------------------------------------- 1 | import { IClass, IIdentifier } from '../state/helper'; 2 | import { 3 | ClassInjector, FactoryInjector, 4 | IInjector, IInstanceFactory, ValueInjector 5 | } from './injector'; 6 | 7 | export class Binding { 8 | 9 | public injectorFactory!: () => IInjector; 10 | 11 | constructor( 12 | public identifier: IIdentifier 13 | ) { } 14 | 15 | public toClass(stateClass: IClass) { 16 | this.injectorFactory = () => 17 | new ClassInjector(this.identifier, stateClass); 18 | return this; 19 | } 20 | 21 | public toValue(state: T) { 22 | this.injectorFactory = () => 23 | new ValueInjector(this.identifier, state); 24 | return this; 25 | } 26 | 27 | public toFactory(factory: IInstanceFactory, deps: IIdentifier[] = []) { 28 | this.injectorFactory = () => 29 | new FactoryInjector(this.identifier, factory, deps); 30 | return this; 31 | } 32 | 33 | } 34 | 35 | export function bind(identifier: IIdentifier) { 36 | return new Binding(identifier); 37 | } 38 | -------------------------------------------------------------------------------- /test/unit/state/middleward.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { IMutation, Mutation, State } from '../../../lib/vue-class-state.common'; 3 | 4 | test('instance middleware', t => { 5 | 6 | const list = []; 7 | 8 | const TestMutation = Mutation.create((next: () => void, m: IMutation) => { 9 | m.payload[0].a = 10; 10 | list.push(1); 11 | next(); 12 | list.push(5); 13 | }, (next: () => void) => { 14 | list.push(2); 15 | next(); 16 | list.push(4); 17 | }); 18 | 19 | class Test { 20 | 21 | @State public data = { 22 | a: 1, 23 | b: 2 24 | }; 25 | 26 | @State public count = 0; 27 | 28 | @TestMutation public change(data: any, count: number) { 29 | Object.assign(this.data, data); 30 | list.push(3); 31 | this.count = count; 32 | } 33 | 34 | } 35 | 36 | const state = new Test(); 37 | state.change({ 38 | a: 5, 39 | b: 6 40 | }, 1); 41 | t.deepEqual(state.data, { a: 10, b: 6 }); 42 | t.deepEqual(list, [1, 2, 3, 4, 5]); 43 | }); 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 zetaplus006 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/state/watcher.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { isSSR } from './helper'; 3 | 4 | export interface IWatcherOption { 5 | lazy: boolean; 6 | // computed for next vue version(>2.5.15) 7 | computed: boolean; 8 | } 9 | 10 | export declare class IWatcher { 11 | constructor( 12 | vm: any, 13 | expOrFn: () => any, 14 | cb: () => any, 15 | options?: IWatcherOption 16 | ); 17 | public value: any; 18 | public dirty: boolean; 19 | public evaluate(): void; 20 | public depend(): void; 21 | } 22 | 23 | export interface IDep { 24 | target: any; 25 | } 26 | 27 | let _watcher, _dep, Watcher: typeof IWatcher, Dep: IDep; 28 | if (!isSSR) { 29 | const computedKey = 'c'; 30 | const vm = new Vue({ 31 | data: { 32 | a: 1 33 | }, 34 | computed: { 35 | [computedKey]() { 36 | return 1; 37 | } 38 | } 39 | }); 40 | vm.$destroy(); 41 | _watcher = (vm as any)._computedWatchers[computedKey]; 42 | _dep = (vm as any)._data.__ob__.dep; 43 | Watcher = Object.getPrototypeOf(_watcher).constructor; 44 | Dep = Object.getPrototypeOf(_dep).constructor; 45 | } 46 | export { 47 | Watcher, 48 | Dep 49 | }; 50 | -------------------------------------------------------------------------------- /examples/di/store.ts: -------------------------------------------------------------------------------- 1 | import { bind, Container, Getter, Inject, State } from 'vue-class-state'; 2 | 3 | // 定义注入标识 4 | export const StateKeys = { 5 | A: 'stateA', 6 | B: 'stateB', 7 | STORE: 'store' 8 | }; 9 | 10 | export class StateA { 11 | // 定义可观察数据 12 | @State text = 'A'; 13 | } 14 | 15 | export class StateB { 16 | @State text = 'B'; 17 | } 18 | 19 | export class Store { 20 | 21 | // 根据注入标识在将实例注入到类实例属性中 22 | // 并且在第一次读取该属性时才进行初始化 23 | // @Inject(StateKeys.A) stateA!: StateA 24 | 25 | constructor( 26 | // 根据注入标识在将实例注入到构造器参数中 27 | @Inject(StateKeys.A) public stateA: StateA, 28 | @Inject(StateKeys.B) public stateB: StateB 29 | ) { 30 | } 31 | 32 | // 定义计算属性, 33 | // 并且在第一次读取该属性时才进行该计算属性的初始化 34 | @Getter get text() { 35 | return this.stateA.text + this.stateB.text; 36 | } 37 | 38 | } 39 | 40 | // 定义容器 41 | @Container({ 42 | providers: [ 43 | // 绑定注入规则,一个标识对应一个类实例(容器范围内单例注入) 44 | bind(StateKeys.A).toClass(StateA), 45 | bind(StateKeys.B).toClass(StateB), 46 | bind(StateKeys.STORE).toClass(Store) 47 | ], 48 | // 开启严格模式 49 | strict: true, 50 | devtool: true 51 | }) 52 | export class AppContainer { } 53 | -------------------------------------------------------------------------------- /examples/cache/app.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { bind, Container, IMutation, Mutation, State } from 'vue-class-state'; 3 | // tslint:disable:no-console 4 | 5 | // 如果想拦截某些Mutation的执行,可以创建一个新的装饰器,执行顺序和 koa(直接抄它的)一样,洋葱模型,但不支持异步 6 | const CacheMutation = Mutation.create((next: () => void, mutation: IMutation, state: Counter) => { 7 | // mutation 执行前打印相关信息 8 | console.log(` 9 | mutation类型,供devtool使用: ${mutation.type} 10 | 传入mutation方法的参数数组: ${JSON.stringify(mutation.payload)} 11 | 调用的模块注入标识: ${mutation.identifier} 12 | 调用的方法名: ${mutation.mutationType} 13 | `); 14 | next(); 15 | // mutation 执行后保存缓存 16 | localStorage.setItem(state.cacheKey, JSON.stringify(state)); 17 | }); 18 | 19 | class Counter { 20 | 21 | cacheKey = 'cache-key'; 22 | 23 | @State public obj = { test: 1 }; 24 | 25 | @State public num = 0; 26 | 27 | // @CacheMutation 28 | @CacheMutation 29 | public add() { 30 | this.num++; 31 | } 32 | 33 | constructor() { 34 | const cacheStr = localStorage.getItem(this.cacheKey); 35 | if (cacheStr) { 36 | // tslint:disable-next-line:no-shadowed-variable 37 | const cache = JSON.parse(cacheStr); 38 | State.replaceState(this, cache); 39 | } 40 | setInterval(() => { 41 | // 等同于 CacheMutation.commit(this, () => this.num++, 'add'); 42 | this.add(); 43 | }, 1000); 44 | } 45 | } 46 | 47 | const COUNTER = 'counter'; 48 | 49 | @Container({ 50 | providers: [bind(COUNTER).toClass(Counter)], 51 | strict: [COUNTER] 52 | }) 53 | class AppContainer { } 54 | 55 | const container = new AppContainer(); 56 | 57 | new Vue({ 58 | el: '#app', 59 | template: `
{{counter.num}}
`, 60 | computed: { 61 | counter() { 62 | return container[COUNTER]; 63 | } 64 | } 65 | }); 66 | -------------------------------------------------------------------------------- /examples/webpack.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const webpack = require('webpack') 4 | 5 | module.exports = { 6 | 7 | devtool: '#source-map', 8 | 9 | entry: fs.readdirSync(__dirname).reduce((entries, dir) => { 10 | const fullDir = path.join(__dirname, dir) 11 | const entry = path.join(fullDir, 'app.ts') 12 | if (fs.statSync(fullDir).isDirectory() && fs.existsSync(entry)) { 13 | entries[dir] = ['webpack-hot-middleware/client', entry] 14 | } 15 | return entries 16 | }, {}), 17 | 18 | output: { 19 | path: path.join(__dirname, '__build__'), 20 | filename: '[name].js', 21 | chunkFilename: '[id].chunk.js', 22 | publicPath: '/__build__/' 23 | }, 24 | 25 | module: { 26 | rules: [ 27 | // { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' }, 28 | { 29 | test: /\.vue$/, 30 | loader: 'vue-loader', 31 | options: { 32 | loader: { 33 | css: 'css-loader' 34 | } 35 | } 36 | }, 37 | { 38 | test: /\.ts$/, 39 | enforce: 'pre', 40 | loader: 'tslint-loader' 41 | }, 42 | { 43 | test: /\.ts$/, 44 | exclude: /node_modules|vue\/src/, 45 | loader: 'ts-loader', 46 | options: { 47 | appendTsSuffixTo: [/\.vue$/] 48 | } 49 | }, 50 | ] 51 | }, 52 | 53 | resolve: { 54 | extensions: ['.ts', '.js'], 55 | alias: { 56 | 'vue-class-state': path.resolve(__dirname, '../src/vue-class-state.ts'), 57 | vue: 'vue/dist/vue.common.js' 58 | } 59 | }, 60 | 61 | plugins: [ 62 | new webpack.optimize.CommonsChunkPlugin({ 63 | name: 'shared', 64 | filename: 'shared.js' 65 | }), 66 | new webpack.DefinePlugin({ 67 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development') 68 | }), 69 | new webpack.HotModuleReplacementPlugin(), 70 | new webpack.NoEmitOnErrorsPlugin() 71 | 72 | ] 73 | 74 | } -------------------------------------------------------------------------------- /src/state/state.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { assert, assign, def, hasOwn, isDev } from '../util'; 3 | import { isSSR, noop } from './helper'; 4 | import { allowChange } from './mutation'; 5 | import { ScopeData, scopeKey } from './scope'; 6 | 7 | export const StateDecorator = isSSR 8 | // tslint:disable-next-line:no-empty 9 | ? noop as PropertyDecorator 10 | : function (target: object, propertyKey: string) { 11 | def(target, propertyKey, { 12 | get() { 13 | if (process.env.NODE_ENV !== 'production') { 14 | assert(false, `This property [${propertyKey}] must be initialized`); 15 | } 16 | }, 17 | set(value) { 18 | Vue.util.defineReactive(this, propertyKey, value); 19 | const scopeData = ScopeData.get(this); 20 | if (isDev) { 21 | def(scopeData.$state, propertyKey, { 22 | get: () => this[propertyKey], 23 | set: (val: any) => { 24 | this[propertyKey] = val; 25 | }, 26 | enumerable: true, 27 | configurable: true 28 | }); 29 | } 30 | }, 31 | enumerable: true, 32 | configurable: true 33 | }); 34 | }; 35 | 36 | export function replaceState(targetState: any, state: any): void { 37 | const scope = targetState[scopeKey] as ScopeData || undefined; 38 | if (scope === undefined) return; 39 | allowChange(() => { 40 | for (const key in state) { 41 | if (hasOwn(targetState, key)) { 42 | targetState[key] = state[key]; 43 | } 44 | } 45 | }); 46 | } 47 | 48 | export function getAllState(state: any) { 49 | return ScopeData.get(state).$state; 50 | } 51 | 52 | export const State = assign( 53 | StateDecorator, { 54 | replaceState, 55 | getAllState 56 | }); 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-class-state", 3 | "version": "0.4.1", 4 | "description": "A object-oriented style State Management for Vue", 5 | "main": "lib/vue-class-state.common.js", 6 | "module": "lib/vue-class-state.esm.js", 7 | "typings": "lib/vue-class-state.d.ts", 8 | "files": [ 9 | "lib", 10 | "typings", 11 | "README", 12 | "LICENSE" 13 | ], 14 | "scripts": { 15 | "dev": "node examples/server.js", 16 | "build": "rimraf lib && rollup -c && npm run move-dts", 17 | "move-dts": "rimraf lib/examples && ncp lib/src/ lib/ && rimraf lib/src", 18 | "test": "npm run build-unit && npm run test-unit", 19 | "test-unit": " ava --verbose --tap-nyan test/unit-build", 20 | "test-unit-ts": "ava-ts --verbose --tap-nyan test/unit", 21 | "build-unit": "rimraf test/unit-build && tsc -p test/unit", 22 | "pre-publish": "npm run build && npm run test", 23 | "release": "bash release.sh" 24 | }, 25 | "keywords": [ 26 | "vue", 27 | "typescript", 28 | "ioc", 29 | "di" 30 | ], 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/zetaplus006/vue-class-state.git" 34 | }, 35 | "author": "zetaplus006", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/zetaplus006/vue-class-state/issues" 39 | }, 40 | "homepage": "https://github.com/zetaplus006/vue-class-state#readme", 41 | "devDependencies": { 42 | "@types/node": "^8.0.17", 43 | "ava": "^0.25.0", 44 | "chai": "^4.1.1", 45 | "css-loader": "^0.28.4", 46 | "express": "^4.15.3", 47 | "mocha": "^3.5.0", 48 | "ncp": "^2.0.0", 49 | "rimraf": "^2.6.1", 50 | "rollup": "^0.56.5", 51 | "rollup-plugin-filesize": "^1.4.2", 52 | "rollup-plugin-typescript2": "^0.11.1", 53 | "ts-loader": "^2.3.1", 54 | "tslint": "^5.9.1", 55 | "tslint-loader": "^3.5.3", 56 | "typescript": "^2.7.2", 57 | "vue": "^2.5.15", 58 | "vue-class-component": "^6.1.0", 59 | "vue-loader": "^13.0.2", 60 | "vue-property-decorator": "^6.0.0", 61 | "vue-template-compiler": "^2.5.6", 62 | "webpack": "^3.4.0", 63 | "webpack-dev-middleware": "^1.11.0", 64 | "webpack-hot-middleware": "^2.18.2" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/di/inject.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { IClass, IIdentifier } from '../state/helper'; 3 | import { hideProperty } from '../util'; 4 | import { ClassMetaData } from './class_meta'; 5 | import { DIMetaData } from './di_meta'; 6 | export function Inject(identifier: IIdentifier): any { 7 | return function (target: IClass | object, propertyKey?: string, parameterIndex?: number) { 8 | if (typeof parameterIndex === 'number') { 9 | setParamsMeta(target as IClass, parameterIndex!, identifier); 10 | } else { 11 | if (target instanceof Vue) { 12 | injectIntoComponent(target, propertyKey!, identifier); 13 | } else { 14 | return lazyDecorator(propertyKey!, identifier); 15 | } 16 | } 17 | }; 18 | } 19 | 20 | export function setParamsMeta(target: IClass, index: number, identifier: IIdentifier): void { 21 | const classMeta = ClassMetaData.get(target.prototype); 22 | classMeta.injectParameterMeta[index] = identifier; 23 | } 24 | 25 | export function lazyDecorator(propertyKey: string, identifier?: IIdentifier) { 26 | const stateKey: IIdentifier = identifier || propertyKey; 27 | return { 28 | get(this: any) { 29 | const state = DIMetaData.get(this).provider.get(stateKey); 30 | hideProperty(this, propertyKey, state); 31 | return state; 32 | }, 33 | enumerable: false, 34 | configuriable: true 35 | }; 36 | } 37 | 38 | /** 39 | * change from https://github.com/vuejs/vue-class-component/blob/master/src/util.ts#L19 40 | * and https://github.com/kaorun343/vue-property-decorator/blob/master/src/vue-property-decorator.ts#L19 41 | */ 42 | export function injectIntoComponent(proto: any, propertyKey: string, identifier: IIdentifier) { 43 | const Ctor = proto.constructor; 44 | if (!Ctor.__decorators__) { 45 | Ctor.__decorators__ = []; 46 | } 47 | const factory = (option: any, key: string) => { 48 | if (option.inject === undefined) { 49 | option.inject = {}; 50 | } 51 | if (!Array.isArray(option.inject)) { 52 | option.inject[key] = identifier; 53 | } 54 | }; 55 | Ctor.__decorators__.push((options: any) => factory(options, propertyKey)); 56 | } 57 | -------------------------------------------------------------------------------- /src/di/container.ts: -------------------------------------------------------------------------------- 1 | // import { devtool } from '../dev/devtool'; 2 | import { devtool } from '../dev/devtool'; 3 | import { useStrict } from '../dev/strict'; 4 | import { Binding } from '../di/binding'; 5 | import { DIMetaData } from '../di/di_meta'; 6 | import { Provider } from '../di/provider'; 7 | import { IClass, IIdentifier, IPlugin, isSSR } from '../state/helper'; 8 | import { scopeKey } from '../state/scope'; 9 | import { hideProperty } from '../util'; 10 | 11 | export interface IContainerOption { 12 | providers: Array>; 13 | globalPlugins?: IPlugin[]; 14 | strict?: boolean | IIdentifier[]; 15 | devtool?: boolean | IIdentifier[]; 16 | } 17 | 18 | export interface IContainer { 19 | _provider: Provider; 20 | _globalPlugins: IPlugin[]; 21 | _option: IContainerOption; 22 | } 23 | 24 | export function Container(option: IContainerOption) { 25 | return function (_target: IClass) { 26 | return createContainerClass(option); 27 | }; 28 | } 29 | 30 | function createContainerClass(option: IContainerOption) { 31 | return class $StateModule implements IContainer { 32 | 33 | public _provider!: Provider; 34 | 35 | public _globalPlugins!: IPlugin[]; 36 | 37 | public _option!: IContainerOption; 38 | 39 | constructor() { 40 | hideProperty(this, '_provider', new Provider(this)); 41 | hideProperty(this, '_globalPlugins', option.globalPlugins || []); 42 | hideProperty(this, '_option', option); 43 | const storeIdentifiers = option.providers.map((binding) => { 44 | this._provider.register(binding.injectorFactory()); 45 | return binding.identifier; 46 | }); 47 | const strictList = option.strict === true ? storeIdentifiers : (option.strict || []); 48 | const devtoolList = option.devtool === false 49 | ? [] : Array.isArray(option.devtool) 50 | ? option.devtool : storeIdentifiers; 51 | 52 | this._provider.registerInjectedHook((instance: any, diMetaData: DIMetaData) => { 53 | if (process.env.NODE_ENV !== 'production' && !isSSR) { 54 | if (instance[scopeKey] && !diMetaData.hasBeenInjected 55 | && strictList.indexOf(diMetaData.identifier) > -1) { 56 | useStrict(instance); 57 | } 58 | } 59 | this._globalPlugins.forEach((action) => action(instance)); 60 | }); 61 | if (process.env.NODE_ENV !== 'production' && devtoolList.length) { 62 | devtool(this, devtoolList); 63 | } 64 | } 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /src/di/injector.ts: -------------------------------------------------------------------------------- 1 | import { IClass, IIdentifier } from '../state/helper'; 2 | import { ClassMetaData } from './class_meta'; 3 | import { DIMetaData } from './di_meta'; 4 | import { Provider } from './provider'; 5 | 6 | export interface IInjector { 7 | identifier: IIdentifier; 8 | provider: Provider; 9 | resolve(): T; 10 | } 11 | 12 | export type IInstanceFactory = (...arg: any[]) => T; 13 | 14 | export abstract class BaseInjector { 15 | 16 | public instance!: T; 17 | public provider!: Provider; 18 | 19 | public resolve(): T { 20 | return this.instance || (this.instance = this.getInstance()); 21 | } 22 | 23 | public addDIMeta(instance: T, identifier: IIdentifier) { 24 | const meta = DIMetaData.get(instance); 25 | if (meta.hasBeenInjected) { 26 | return; 27 | } 28 | meta.identifier = identifier; 29 | meta.provider = this.provider; 30 | this.provider.hooks.forEach((fn) => fn(instance, meta)); 31 | meta.hasBeenInjected = true; 32 | } 33 | 34 | protected abstract getInstance(): T; 35 | } 36 | 37 | export class ClassInjector extends BaseInjector implements IInjector { 38 | 39 | constructor( 40 | public identifier: IIdentifier, 41 | public stateClass: IClass 42 | ) { 43 | super(); 44 | } 45 | 46 | public getInstance() { 47 | const instance = resolveClassInstance(this.provider, this); 48 | this.addDIMeta(instance, this.identifier); 49 | return instance; 50 | } 51 | } 52 | 53 | export class ValueInjector extends BaseInjector implements IInjector { 54 | 55 | constructor( 56 | public identifier: IIdentifier, 57 | private state: T 58 | ) { 59 | super(); 60 | } 61 | 62 | public getInstance() { 63 | this.addDIMeta(this.state, this.identifier); 64 | return this.state; 65 | } 66 | 67 | } 68 | 69 | export class FactoryInjector extends BaseInjector implements IInjector { 70 | 71 | constructor( 72 | public identifier: IIdentifier, 73 | private stateFactory: IInstanceFactory, 74 | private deps: IIdentifier[] 75 | ) { 76 | super(); 77 | } 78 | 79 | public getInstance() { 80 | const args = this.provider.getAll(this.deps); 81 | const instance = this.stateFactory.apply(null, args) as T; 82 | this.addDIMeta(instance, this.identifier); 83 | return instance; 84 | } 85 | 86 | } 87 | 88 | export function resolveClassInstance(provider: Provider, injector: ClassInjector) { 89 | const classMeta = ClassMetaData.get(injector.stateClass.prototype); 90 | const parameterMeta = classMeta.injectParameterMeta; 91 | const args = provider.getAll(parameterMeta); 92 | return new injector.stateClass(...args); 93 | } 94 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:recommended" 4 | ], 5 | "rules": { 6 | "class-name": true, 7 | "comment-format": [ 8 | true, 9 | "check-space" 10 | ], 11 | "curly": false, 12 | "indent": [ 13 | true, 14 | "spaces", 15 | 4 16 | ], 17 | "jsdoc-format": true, 18 | "no-consecutive-blank-lines": true, 19 | "no-debugger": true, 20 | "no-duplicate-key": true, 21 | "no-duplicate-variable": true, 22 | "no-eval": true, 23 | "no-internal-module": true, 24 | "no-trailing-whitespace": true, 25 | "no-shadowed-variable": true, 26 | "no-switch-case-fall-through": true, 27 | "no-unreachable": true, 28 | "no-unused-expression": false, 29 | "no_unused-variable": [ 30 | true 31 | ], 32 | "no-use-before-declare": false, 33 | "no-var-keyword": true, 34 | "one-line": [ 35 | true, 36 | "check-open-brace", 37 | "check-whitespace", 38 | "check-catch" 39 | ], 40 | "quotemark": [ 41 | true, 42 | "single" 43 | ], 44 | "semicolon": [ 45 | true, 46 | "always" 47 | ], 48 | "trailing-comma": [ 49 | true, 50 | { 51 | "multiline": "never", 52 | "singleline": "never" 53 | } 54 | ], 55 | "triple-equals": [ 56 | true, 57 | "allow-null-check" 58 | ], 59 | "typedef-whitespace": [ 60 | true, 61 | { 62 | "call-signature": "nospace", 63 | "index-signature": "nospace", 64 | "parameter": "nospace", 65 | "property-declaration": "nospace", 66 | "variable-declaration": "nospace" 67 | } 68 | ], 69 | "variable-name": [ 70 | true, 71 | "ban-keywords" 72 | ], 73 | "whitespace": [ 74 | true, 75 | "check-branch", 76 | "check-decl", 77 | "check-operator", 78 | "check-separator", 79 | "check-type" 80 | ], 81 | "space-before-function-paren": false, 82 | "linebreak-style": false, 83 | "no-unused-variable": true, 84 | "object-literal-sort-keys": false, 85 | "only-arrow-functions": false, 86 | "no-string-literal": false, 87 | "max-classes-per-file": false, 88 | "member-ordering": false, 89 | "forin": false, 90 | "one-variable-per-declaration": false, 91 | "no-var-requires": false, 92 | "member-access": false, 93 | "arrow-parens": false 94 | } 95 | } -------------------------------------------------------------------------------- /src/di/provider.ts: -------------------------------------------------------------------------------- 1 | import { IMap, UseMap } from '../di/map'; 2 | import { IIdentifier } from '../state/helper'; 3 | import { replaceState } from '../state/state'; 4 | import { assert, defGet } from '../util'; 5 | import { DIMetaData } from './di_meta'; 6 | import { IInjector } from './injector'; 7 | 8 | export interface IProxyState { 9 | [key: string]: any; 10 | } 11 | 12 | export class Provider { 13 | 14 | /** 15 | * for vue provide option 16 | */ 17 | public proxy: any; 18 | 19 | public hooks: Array<(instance: any, meta: DIMetaData) => void> = []; 20 | 21 | constructor(proxyObj: any) { 22 | this.proxy = proxyObj; 23 | } 24 | 25 | private injectorMap: IMap> = new UseMap(); 26 | 27 | /** 28 | * get state instance 29 | * @param identifier 30 | */ 31 | public get(identifier: IIdentifier): any { 32 | const injector = this.injectorMap.get(identifier); 33 | if (process.env.NODE_ENV !== 'production') { 34 | assert(injector, 35 | `${String(identifier)} not find in provider`); 36 | } 37 | return (injector as IInjector).resolve(); 38 | } 39 | 40 | /** 41 | * get state instance array 42 | * @param deps 43 | */ 44 | public getAll(deps: IIdentifier[]): any[] { 45 | return deps.map((identifier) => this.get(identifier)); 46 | } 47 | 48 | /** 49 | * register a injector in the provider 50 | * @param injector 51 | */ 52 | public register(injector: IInjector) { 53 | this.checkIdentifier(injector.identifier); 54 | injector.provider = this; 55 | this.injectorMap.set(injector.identifier, injector); 56 | this.defProxy(injector); 57 | } 58 | 59 | public checkIdentifier(identifier: IIdentifier) { 60 | if (process.env.NODE_ENV !== 'production') { 61 | assert(!this.injectorMap.has(identifier), 62 | `The identifier ${String(identifier)} has been repeated`); 63 | } 64 | } 65 | 66 | /** 67 | * replaceState for SSR and devtool 68 | * @param proxyState 69 | */ 70 | public replaceAllState(proxyState: IProxyState) { 71 | for (const key in proxyState) { 72 | const instance = this.proxy[key]; 73 | replaceState(instance, proxyState[key]); 74 | } 75 | } 76 | 77 | public registerInjectedHook(injected: (instance: any, meta: DIMetaData) => void) { 78 | if (this.hooks.indexOf(injected) > -1) { 79 | return; 80 | } 81 | this.hooks.push(injected); 82 | } 83 | 84 | /** 85 | * for vue provide option 86 | * @param injector 87 | */ 88 | private defProxy(injector: IInjector) { 89 | defGet(this.proxy, injector.identifier, () => { 90 | return injector.resolve(); 91 | }); 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/state/computed.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * change from https://github.com/vuejs/vue/blob/dev/src/core/instance/state.js#L163 3 | */ 4 | import { ClassMetaData } from '../di/class_meta'; 5 | import { assert, def, defGet, hideProperty, isDev } from '../util'; 6 | import { isSSR, noop } from './helper'; 7 | import { ScopeData } from './scope'; 8 | import { Dep, IWatcher, IWatcherOption, Watcher } from './watcher'; 9 | 10 | export interface IComputedOption { 11 | enumerable: boolean; 12 | } 13 | const defaultComputedOption: IComputedOption = { enumerable: false }; 14 | 15 | const computedWatcherOptions: IWatcherOption = { 16 | lazy: true, 17 | computed: true 18 | }; 19 | 20 | export function Computed(option: IComputedOption): any; 21 | export function Computed(target: object, propertyKey: string): any; 22 | export function Computed(targetOrOption: any, propertyKey?: string): any { 23 | if (propertyKey) { 24 | return createComputed(defaultComputedOption, targetOrOption, propertyKey); 25 | } else { 26 | return function (target: object, key: string) { 27 | return createComputed(targetOrOption, target, key); 28 | }; 29 | } 30 | } 31 | 32 | export const watcherKey = '_watchers'; 33 | 34 | export const createComputed = isSSR 35 | // tslint:disable-next-line:no-empty 36 | ? noop as any 37 | : _createComputed; 38 | 39 | export function _createComputed(option: IComputedOption, target: any, propertyKey: string): PropertyDescriptor { 40 | const desc = Object.getOwnPropertyDescriptor(target, propertyKey); 41 | if (process.env.NODE_ENV !== 'production') { 42 | assert(desc && desc.get, '[@Getter] must be used for getter property'); 43 | } 44 | const get = desc!.get!; 45 | ClassMetaData.get(target).addGetterKey(propertyKey); 46 | return { 47 | get() { 48 | const scope = ScopeData.get(this); 49 | if (!this[watcherKey]) { 50 | hideProperty(this, watcherKey, []); 51 | } 52 | const watcher = new Watcher( 53 | this, 54 | get, 55 | noop, 56 | computedWatcherOptions 57 | ); 58 | const getter = createComputedGetter(watcher); 59 | def(this, propertyKey, { 60 | get: getter, 61 | enumerable: option.enumerable, 62 | configurable: true 63 | }); 64 | if (isDev) { 65 | defGet(scope.$getters, propertyKey, () => this[propertyKey]); 66 | } 67 | return getter(); 68 | }, 69 | enumerable: option.enumerable, 70 | configurable: true 71 | }; 72 | } 73 | 74 | function createComputedGetter(watcher: IWatcher) { 75 | return function computedGetter() { 76 | if (watcher.dirty) { 77 | watcher.evaluate(); 78 | } 79 | if (Dep.target) { 80 | watcher.depend(); 81 | } 82 | return watcher.value; 83 | }; 84 | } 85 | -------------------------------------------------------------------------------- /src/dev/devtool.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { ClassMetaData } from '../di/class_meta'; 3 | import { IContainer } from '../di/container'; 4 | import { IProxyState, Provider } from '../di/provider'; 5 | import { IIdentifier } from '../state/helper'; 6 | import { ScopeData } from '../state/scope'; 7 | import { def } from '../util'; 8 | 9 | export const devtoolHook = 10 | typeof window !== 'undefined' && 11 | window['__VUE_DEVTOOLS_GLOBAL_HOOK__']; 12 | export function devtool(container: IContainer, identifiers: IIdentifier[]) { 13 | 14 | const provider = container._provider; 15 | 16 | if (!devtoolHook) return; 17 | 18 | const store: IStore = simulationStore(provider, identifiers); 19 | 20 | devtoolHook.emit('vuex:init', store); 21 | 22 | devtoolHook.on('vuex:travel-to-state', (targetState: any) => { 23 | provider.replaceAllState(targetState); 24 | }); 25 | 26 | } 27 | 28 | interface IStore { 29 | state: any; 30 | getters: any; 31 | _devtoolHook: any; 32 | } 33 | 34 | function simulationStore(provider: Provider, identifiers: IIdentifier[]): IStore { 35 | const { state, getters } = getStateAndGetters(provider.proxy, identifiers); 36 | const store = { 37 | state, 38 | getters, 39 | _devtoolHook: devtoolHook, 40 | // tslint:disable-next-line:no-empty 41 | registerModule() { }, 42 | // tslint:disable-next-line:no-empty 43 | unregisterModule() { }, 44 | replaceState(targetState: IProxyState) { 45 | provider.replaceAllState(targetState); 46 | }, 47 | _vm: new Vue({}), 48 | _mutations: {} 49 | }; 50 | return store; 51 | } 52 | 53 | function getStateAndGetters(proxy: any, identifiers: IIdentifier[]) { 54 | const getters = {}; 55 | const state = {}; 56 | const keys: IIdentifier[] = identifiers; 57 | keys.forEach((key) => { 58 | const instance = proxy[key]; 59 | const scope = ScopeData.get(instance); 60 | tryReadGetters(instance); 61 | def(getters, String(key), { 62 | value: scope.$getters, 63 | enumerable: true, 64 | configurable: true 65 | }); 66 | def(state, String(key), { 67 | value: scope.$state, 68 | enumerable: true, 69 | configurable: true 70 | }); 71 | }); 72 | return { 73 | state, 74 | getters 75 | }; 76 | } 77 | 78 | /** 79 | * try to read the first getter 80 | * @param instance 81 | */ 82 | function tryReadGetters(instance: any, proto?: any) { 83 | if (proto && proto === Object.prototype) { 84 | return; 85 | } 86 | const _proto = Object.getPrototypeOf(proto || instance); 87 | const getterKeys = ClassMetaData.get(_proto).getterKeys; 88 | let len = getterKeys.length; 89 | try { 90 | while (len--) { 91 | instance[getterKeys[len]]; 92 | } 93 | // tslint:disable-next-line:no-empty 94 | } finally { } 95 | tryReadGetters(instance, _proto); 96 | } 97 | -------------------------------------------------------------------------------- /src/state/mutation.ts: -------------------------------------------------------------------------------- 1 | import { DIMetaData, meta_key } from '../di/di_meta'; 2 | import { assign } from '../util'; 3 | import { compose, IMiddleware } from './compose'; 4 | import { globalState, IIdentifier } from './helper'; 5 | export interface IMutation { 6 | type: string; 7 | payload: any[]; 8 | mutationType: string; 9 | identifier: IIdentifier; 10 | } 11 | 12 | export const Mutation = assign(createMutation(), { create: createMutation }); 13 | 14 | export function createMutation(...middleware: IMiddleware[]) { 15 | 16 | let cb: (...args: any[]) => any; 17 | let args: any[] = []; 18 | 19 | const commitFn = compose(middleware.concat(globalState.middlewares) 20 | .concat((_next: () => void, _mutation: IMutation, state: any) => { 21 | const result = allowChange(() => cb && cb.apply(state, args)); 22 | return result; 23 | })); 24 | 25 | // const commit = (state: any, fn: () => void, mutationType?: string, arg?: any[]) => { 26 | // cb = fn; 27 | // args = arg || []; 28 | // return commitFn(null as any, createMuationData(state, mutationType, arg), state); 29 | // }; 30 | 31 | function commit(fn: () => any, mutationType?: string, arg?: any[]): any; 32 | function commit(state: any, fn: () => any, mutationType?: string, arg?: any[]): any; 33 | function commit(...commitArgs: any[]) { 34 | let state: any; 35 | let mutationType: string; 36 | if (typeof commitArgs[0] === 'function') { 37 | state = undefined; 38 | cb = commitArgs[0]; 39 | mutationType = commitArgs[1]; 40 | args = commitArgs[2] || []; 41 | } else { 42 | state = commitArgs[0]; 43 | cb = commitArgs[1]; 44 | mutationType = commitArgs[2]; 45 | args = commitArgs[3] || []; 46 | } 47 | return commitFn(null as any, createMuationData(state, mutationType, args), state); 48 | } 49 | 50 | function decorator(_target: any, methodName: string, descriptor: PropertyDescriptor) { 51 | const mutationFn = descriptor.value; 52 | descriptor.value = function (...arg: any[]) { 53 | return commit(this, mutationFn, methodName, arg); 54 | }; 55 | return descriptor; 56 | } 57 | 58 | return assign(decorator, { commit }); 59 | } 60 | 61 | const unnamedName = ''; 62 | const unknownIdentifier = 'unknown'; 63 | 64 | function createMuationData(ctx: any | undefined, mutationType: string | undefined, payload: any) { 65 | const meta = ctx && ctx[meta_key] as DIMetaData | undefined, 66 | identifier = meta && meta.identifier || unknownIdentifier, 67 | mType = mutationType || unnamedName, 68 | type = identifier + ': ' + mType; 69 | 70 | const mutation: IMutation = { 71 | type, 72 | payload, 73 | mutationType: mType, 74 | identifier 75 | }; 76 | return mutation; 77 | } 78 | 79 | export const allowChange = process.env.NODE_ENV !== 'production' 80 | ? (cb: () => void) => { 81 | const temp = globalState.isCommitting; 82 | globalState.isCommitting = true; 83 | const result = cb(); 84 | globalState.isCommitting = temp; 85 | return result; 86 | } 87 | : (cb: () => void) => cb(); 88 | -------------------------------------------------------------------------------- /test/unit/state/state.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { Getter, Mutation, State } from '../../../lib/vue-class-state.common'; 3 | 4 | test('$state $getters', (t) => { 5 | class Test { 6 | @State public a = 1; 7 | @State public b = 2; 8 | @Getter get sum() { 9 | return this.a + this.b; 10 | } 11 | @Getter get diff() { 12 | return this.b === this.a; 13 | } 14 | } 15 | const state = new Test() as any; 16 | const scope = state.__scope__ as any; 17 | t.deepEqual(state.__scope__.$state, { a: 1, b: 2 }); 18 | t.true(state.sum === scope.$getters.sum); 19 | t.true(scope.$getters.sum === 3); 20 | t.true(state.diff === scope.$getters.diff); 21 | t.false(scope.$getters.diff); 22 | }); 23 | 24 | test('computed', t => { 25 | let num = 0; 26 | class Test { 27 | @State public a = 1; 28 | @Getter get t() { 29 | num++; 30 | return this.a; 31 | } 32 | } 33 | const state = new Test(); 34 | 35 | state.t; 36 | state.t; 37 | state.t; 38 | state.a = 2; 39 | state.t; 40 | state.t; 41 | state.t; 42 | state.a = 5; 43 | state.t; 44 | state.t; 45 | state.t; 46 | 47 | t.is(num, 3); 48 | }); 49 | 50 | test('Mutation.commit', t => { 51 | class Test { 52 | @State public a = 1; 53 | @State public b = 2; 54 | 55 | public change() { 56 | Mutation.commit(this, () => { 57 | this.a = 2; 58 | this.b = 4; 59 | }); 60 | } 61 | 62 | public change2() { 63 | Mutation.commit(() => { 64 | this.a = 6; 65 | this.b = 8; 66 | }); 67 | } 68 | } 69 | const state = new Test(); 70 | state.change(); 71 | t.is(state.a, 2); 72 | t.is(state.b, 4); 73 | state.change2(); 74 | t.is(state.a, 6); 75 | t.is(state.b, 8); 76 | }); 77 | 78 | test('State.replaceState', t => { 79 | class Test { 80 | @State public a = 1; 81 | @State public b = 2; 82 | @State public obj = {}; 83 | get jsonString() { 84 | return Object.assign({}, this, { 85 | a: 3, 86 | obj: { 87 | c: 4 88 | } 89 | }); 90 | } 91 | } 92 | const state = new Test(); 93 | State.replaceState(state, state.jsonString); 94 | t.deepEqual(State.getAllState(state), { 95 | a: 3, 96 | b: 2, 97 | obj: { 98 | c: 4 99 | } 100 | }); 101 | }); 102 | 103 | test('extends', t => { 104 | class Super { 105 | @State public super = 'Super'; 106 | 107 | @Getter get SuperGetter() { 108 | return this.super; 109 | } 110 | } 111 | 112 | class Base extends Super { 113 | @State public base = 'Base'; 114 | 115 | @Getter get baseGetter() { 116 | return this.base; 117 | } 118 | } 119 | 120 | class Child extends Base { 121 | @State public child = 'Child'; 122 | 123 | @Getter get childGetter() { 124 | return this.child; 125 | } 126 | } 127 | 128 | const c = new Child(); 129 | t.true(c.child === 'Child' && c.child === c.childGetter); 130 | t.true(c.base === 'Base' && c.base === c.baseGetter); 131 | t.true(c.super === 'Super' && c.super === c.SuperGetter); 132 | 133 | }); 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## vue-class-state 2 | > Vue状态管理,灵感来自[mobx](https://github.com/mobxjs/mobx) 3 | 4 | `vue-class-state`提供以下功能: 5 | 6 | 1.`state`、`getters`、`mutation`,其概念与`vuex`基本相通,区别是vue-class-state是以class(类)和decorator(装饰器)的形式来实现的。 7 | 8 | 2.简单的依赖注入,用于解决子模块之间共享数据的问题,也可与Vue的[provide/inject](https://cn.vuejs.org/v2/api/#provide-inject)配合使用。 9 | 10 | 3.支持严格模式,开启后`state`只能在`mutation`中被修改,支持拦截mutation。 11 | 12 | 4.支持`vue`官方devtool,可以在devtool的vuex标签下查看`state`、`getters`、`mutation`。 13 | 14 | ## 安装 15 | 16 | ```bash 17 | npm install vue vue-class-state --save 18 | ``` 19 | 20 | 注意: 21 | 22 | 1.TypeScript用户需要开启tsconfig.json中的`experimentalDecorators`和`allowSyntheticDefaultImports`的编译选项 23 | 24 | 2.javaScript+Babel用户需要[babel-plugin-transform-decorators-legacy](babel-plugin-transform-decorators-legacy)和[babel-plugin-transform-class-properties](https://babeljs.io/docs/plugins/transform-class-properties/)插件。 25 | 26 | 27 | 28 | ## 基本使用 29 | 30 | ``` typescript 31 | // store.ts 32 | 33 | import { bind, Container, Getter, Inject, State } from 'vue-class-state'; 34 | 35 | // 定义注入标识 36 | export const StateKeys = { 37 | A: 'stateA', 38 | B: 'stateB', 39 | STORE: 'store' 40 | }; 41 | 42 | export class StateA { 43 | // 定义响应式数据 44 | @State text = 'A'; 45 | } 46 | 47 | export class StateB { 48 | @State text = 'B'; 49 | } 50 | 51 | export class Store { 52 | 53 | // 根据注入标识在将实例注入到类实例属性中 54 | // 并且在第一次读取该属性时才进行初始化 55 | // @Inject(StateKeys.A) stateA!: StateA 56 | 57 | constructor( 58 | // 根据注入标识在将实例注入到构造器参数中 59 | @Inject(StateKeys.A) public stateA: StateA, 60 | @Inject(StateKeys.B) public stateB: StateB 61 | ) { 62 | } 63 | 64 | // 定义计算属性, 65 | // 并且在第一次读取该属性时才进行该计算属性的初始化 66 | @Getter get text() { 67 | return this.stateA.text + this.stateB.text; 68 | } 69 | 70 | } 71 | 72 | // 定义容器 73 | @Container({ 74 | providers: [ 75 | // 绑定注入规则,一个标识对应一个类实例(容器范围内单例注入) 76 | bind(StateKeys.A).toClass(StateA), 77 | bind(StateKeys.B).toClass(StateB), 78 | bind(StateKeys.STORE).toClass(Store) 79 | ], 80 | // 开启严格模式 81 | strict: true 82 | }) 83 | export class AppContainer { } 84 | ``` 85 | 86 | ``` typescript 87 | // app.ts 88 | 89 | import Vue from 'vue'; 90 | import Component from 'vue-class-component'; 91 | import { Inject } from 'vue-class-state'; 92 | import { AppContainer, StateKeys, Store } from './store'; 93 | 94 | // 推荐使用vue官方的vue-class-component库 95 | @Component({ 96 | template: '
{{store.text}}
' 97 | }) 98 | class App extends Vue { 99 | 100 | // 根据注入标识在子组件中注入实例 101 | @Inject(StateKeys.STORE) store!: Store; 102 | 103 | } 104 | 105 | new Vue({ 106 | el: '#app', 107 | // 在根组件实例化一个容器,传入到provide选项 108 | provide: new AppContainer(), 109 | render: (h) => h(App) 110 | }); 111 | ``` 112 | 113 | ### 注册类 114 | 115 | ```typescript 116 | bind(moduleKeys.A).toClass(ModuleA) 117 | ``` 118 | 119 | ### 注册值 120 | 121 | ```typescript 122 | bind(moduleKeys.A).toValue(new ModuleA()) 123 | ``` 124 | 125 | ### 注册工厂 126 | 127 | ```typescript 128 | bind(moduleKeys.A).toFactory(() => new ModuleA()) 129 | 130 | // 传入的第二个参数类型为注入标识数组,表明该工厂依赖的其他模块,会依次注入到工厂参数中 131 | bind(moduleKeys.B).toFactory((moduleA: IModule, moduleB: IModule) => { 132 | return new ModuleC(moduleA, moduleB) 133 | }, [moduleKeys.A, moduleKeys.B]) 134 | 135 | 136 | bind(moduleKeys.B).toFactory((moduleA: IModule, moduleB: IModule) => { 137 | if (isSSR) { 138 | return moduleA 139 | } else { 140 | return moduleB 141 | } 142 | }, [moduleKeys.A, moduleKeys.B]) 143 | ``` 144 | 145 | ### 拦截`mutation` 146 | 147 | 以下是简单的缓存例子 148 | 149 | ```typescript 150 | import Vue from 'vue'; 151 | import { bind, Container, IMutation, Mutation, State } from 'vue-class-state'; 152 | 153 | // 如果想拦截某些Mutation的执行,可以创建一个新的装饰器,执行顺序和 koa(直接抄它的)一样,洋葱模型,但不支持异步 154 | const CacheMutation = Mutation.create((next: () => void, mutation: IMutation, state: Counter) => { 155 | // mutation 执行前打印相关信息 156 | console.log(` 157 | mutation类型,供devtool使用: ${mutation.type} 158 | 传入mutation方法的参数数组: ${JSON.stringify(mutation.payload)} 159 | 调用的模块注入标识: ${mutation.identifier} 160 | 调用的方法名: ${mutation.mutationType} 161 | `); 162 | const res = next(); 163 | // mutation 执行后保存缓存 164 | localStorage.setItem(state.cacheKey, JSON.stringify(state)); 165 | return res; 166 | }); 167 | 168 | class Counter { 169 | 170 | cacheKey = 'cache-key'; 171 | 172 | @State public num = 0; 173 | 174 | // 严格模式下,修改实例的state值必须调用该实例的Mutation方法 175 | // 和vuex一致,必须为同步函数 176 | @CacheMutation 177 | public add() { 178 | this.num++; 179 | } 180 | 181 | // 默认的Mutation不会被拦截 182 | @Mutation 183 | public add2() { 184 | this.num++; 185 | } 186 | 187 | constructor() { 188 | const cacheStr = localStorage.getItem(this.cacheKey); 189 | if (cacheStr) { 190 | const cache = JSON.parse(cacheStr); 191 | State.replaceState(this, cache); 192 | } 193 | setInterval(() => { 194 | // 等同于 CacheMutation.commit(this, () => this.num++, 'add'); 195 | // 最简化写法 CacheMutation.commit(() => this.num++) ,注意由于没有传入this,这种写法中间件是拿不到state的,看情况使用 196 | this.add(); 197 | }, 1000); 198 | } 199 | } 200 | 201 | const COUNTER = 'counter'; 202 | 203 | @Container({ 204 | providers: [bind(COUNTER).toClass(Counter)], 205 | strict: [COUNTER] 206 | }) 207 | class AppContainer { } 208 | 209 | const container = new AppContainer(); 210 | 211 | new Vue({ 212 | el: '#app', 213 | template: `
{{counter.num}}
`, 214 | computed: { 215 | counter() { 216 | return container[COUNTER]; 217 | } 218 | } 219 | }); 220 | ``` -------------------------------------------------------------------------------- /test/unit/di/inject.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { bind, Container, Inject } from '../../../lib/vue-class-state.common'; 3 | 4 | test('class inject', t => { 5 | const KEYS = { 6 | A: 'A', 7 | B: 'B', 8 | ROOT: 'ROOT' 9 | }; 10 | 11 | interface IModule { 12 | text: string; 13 | } 14 | 15 | class StateA implements IModule { 16 | public text = 'A'; 17 | } 18 | 19 | class StateB implements IModule { 20 | public text = 'B'; 21 | } 22 | 23 | class Root { 24 | @Inject(KEYS.A) public stateA: IModule; 25 | @Inject(KEYS.B) public stateB: IModule; 26 | 27 | constructor( 28 | @Inject(KEYS.A) public paramA: IModule, 29 | @Inject(KEYS.B) public paramB: IModule 30 | ) { 31 | 32 | } 33 | } 34 | 35 | @Container({ 36 | providers: [ 37 | bind(KEYS.A).toClass(StateA), 38 | bind(KEYS.B).toClass(StateB), 39 | bind(KEYS.ROOT).toClass(Root) 40 | ] 41 | }) 42 | class Store { } 43 | 44 | const root = new Store()[KEYS.ROOT] as Root; 45 | 46 | t.true(root instanceof Root); 47 | t.is(root.paramA, root.stateA); 48 | t.is(root.paramB, root.stateB); 49 | t.is(root.paramA.text, 'A'); 50 | t.is(root.paramB.text, 'B'); 51 | }); 52 | 53 | test('value inject', t => { 54 | const KEYS = { 55 | A: 'A', 56 | B: 'B', 57 | ROOT: 'ROOT' 58 | }; 59 | 60 | interface IModule { 61 | text: string; 62 | } 63 | 64 | class StateA implements IModule { 65 | public text = 'A'; 66 | } 67 | 68 | class StateB implements IModule { 69 | public text = 'B'; 70 | } 71 | 72 | class Root { 73 | @Inject(KEYS.A) public stateA: IModule; 74 | @Inject(KEYS.B) public stateB: IModule; 75 | 76 | constructor( 77 | @Inject(KEYS.A) public paramA: IModule, 78 | @Inject(KEYS.B) public paramB: IModule 79 | ) { 80 | 81 | } 82 | } 83 | 84 | @Container({ 85 | providers: [ 86 | bind(KEYS.A).toValue(new StateA()), 87 | bind(KEYS.B).toValue(new StateB()), 88 | bind(KEYS.ROOT).toClass(Root) 89 | ] 90 | }) 91 | class Store { } 92 | 93 | const root = new Store()[KEYS.ROOT] as Root; 94 | 95 | t.true(root instanceof Root); 96 | t.is(root.paramA, root.stateA); 97 | t.is(root.paramB, root.stateB); 98 | t.is(root.paramA.text, 'A'); 99 | t.is(root.paramB.text, 'B'); 100 | }); 101 | 102 | test('factory inject', t => { 103 | const KEYS = { 104 | A: 'A', 105 | B: 'B', 106 | ROOT: 'ROOT' 107 | }; 108 | 109 | interface IModule { 110 | text: string; 111 | } 112 | 113 | class StateA implements IModule { 114 | public text = 'A'; 115 | } 116 | 117 | class StateB implements IModule { 118 | public text = 'B'; 119 | } 120 | 121 | class Root { 122 | @Inject(KEYS.A) public stateA: IModule; 123 | @Inject(KEYS.B) public stateB: IModule; 124 | 125 | constructor( 126 | @Inject(KEYS.A) public paramA: IModule, 127 | @Inject(KEYS.B) public paramB: IModule 128 | ) { 129 | 130 | } 131 | } 132 | const valueA = new StateA(), valueB = new StateB(); 133 | @Container({ 134 | providers: [ 135 | bind(KEYS.A).toFactory(() => valueA), 136 | bind(KEYS.B).toFactory(() => valueB), 137 | bind(KEYS.ROOT).toClass(Root) 138 | ] 139 | }) 140 | class Store { } 141 | 142 | const root = new Store()[KEYS.ROOT] as Root; 143 | 144 | t.true(root instanceof Root); 145 | t.is(root.paramA, root.stateA); 146 | t.is(root.paramB, root.stateB); 147 | t.is(root.paramA, valueA); 148 | t.is(root.paramB, valueB); 149 | t.is(root.paramA.text, 'A'); 150 | t.is(root.paramB.text, 'B'); 151 | }); 152 | 153 | test('factory inject with deps', t => { 154 | const KEYS = { 155 | A: 'A', 156 | B: 'B', 157 | ROOT: 'ROOT' 158 | }; 159 | 160 | interface IModule { 161 | text: string; 162 | } 163 | 164 | class StateA implements IModule { 165 | public text = 'A'; 166 | } 167 | 168 | class Root { 169 | @Inject(KEYS.A) public stateA: IModule; 170 | @Inject(KEYS.B) public stateB: IModule; 171 | 172 | constructor( 173 | @Inject(KEYS.A) public paramA: IModule, 174 | @Inject(KEYS.B) public paramB: IModule 175 | ) { 176 | 177 | } 178 | } 179 | @Container({ 180 | providers: [ 181 | bind(KEYS.A).toFactory(() => new StateA()), 182 | bind(KEYS.B).toFactory((depA: IModule) => depA, [KEYS.A]), 183 | bind(KEYS.ROOT).toClass(Root) 184 | ] 185 | }) 186 | class Store { } 187 | 188 | const root = new Store()[KEYS.ROOT] as Root; 189 | 190 | t.true(root instanceof Root); 191 | t.is(root.paramA, root.paramB); 192 | t.is(root.paramA, root.stateA); 193 | t.is(root.paramB, root.stateB); 194 | t.is(root.paramA.text, 'A'); 195 | t.is(root.paramB.text, root.paramB.text); 196 | }); 197 | 198 | test('deep inject', t => { 199 | const KEYS = { 200 | A: 'A', 201 | B: 'B', 202 | C: 'C', 203 | ROOT: 'ROOT' 204 | }; 205 | 206 | interface IState { 207 | text: string; 208 | } 209 | 210 | class StateA implements IState { 211 | public text = 'A'; 212 | } 213 | 214 | class StateB implements IState { 215 | public text = 'B'; 216 | } 217 | 218 | class StateC { 219 | 220 | constructor( 221 | @Inject(KEYS.A) public stateA: IState, 222 | @Inject(KEYS.B) public stateB: IState 223 | ) { 224 | 225 | } 226 | 227 | } 228 | 229 | class Root { 230 | @Inject(KEYS.A) public stateA: IState; 231 | @Inject(KEYS.B) public stateB: IState; 232 | @Inject(KEYS.C) public stateC: IState; 233 | 234 | constructor( 235 | @Inject(KEYS.A) public paramA: IState, 236 | @Inject(KEYS.B) public paramB: IState, 237 | @Inject(KEYS.C) public paramC: StateC 238 | ) { 239 | 240 | } 241 | } 242 | 243 | @Container({ 244 | providers: [ 245 | bind(KEYS.A).toClass(StateA), 246 | bind(KEYS.B).toClass(StateB), 247 | bind(KEYS.C).toClass(StateC), 248 | bind(KEYS.ROOT).toClass(Root) 249 | ] 250 | }) 251 | class Store { } 252 | 253 | const root = new Store()[KEYS.ROOT] as Root; 254 | 255 | t.true(root instanceof Root); 256 | t.is(root.paramA, root.stateA); 257 | t.is(root.paramB, root.stateB); 258 | t.is(root.paramC.stateA, root.paramA); 259 | t.is(root.paramC.stateB, root.paramB); 260 | }); 261 | --------------------------------------------------------------------------------