├── 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 |
--------------------------------------------------------------------------------