├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierrc ├── .travis.yml ├── .vscode └── settings.json ├── API.md ├── API_zh-CN.md ├── LICENSE ├── README.md ├── README_zh-CN.md ├── build ├── dist │ ├── Application.d.ts │ ├── Application.js │ ├── actions.d.ts │ ├── actions.js │ ├── global.d.ts │ ├── global.js │ ├── index.d.ts │ ├── index.js │ ├── loading.d.ts │ ├── loading.js │ ├── model.d.ts │ ├── model.js │ ├── module.d.ts │ ├── module.js │ ├── sprite.d.ts │ ├── sprite.js │ ├── store.d.ts │ └── store.js ├── es5 │ ├── Application.d.ts │ ├── Application.js │ ├── actions.d.ts │ ├── actions.js │ ├── global.d.ts │ ├── global.js │ ├── index.d.ts │ ├── index.js │ ├── loading.d.ts │ ├── loading.js │ ├── model.d.ts │ ├── model.js │ ├── module.d.ts │ ├── module.js │ ├── sprite.d.ts │ ├── sprite.js │ ├── store.d.ts │ └── store.js └── es6 │ ├── Application.d.ts │ ├── Application.js │ ├── actions.d.ts │ ├── actions.js │ ├── global.d.ts │ ├── global.js │ ├── index.d.ts │ ├── index.js │ ├── loading.d.ts │ ├── loading.js │ ├── model.d.ts │ ├── model.js │ ├── module.d.ts │ ├── module.js │ ├── sprite.d.ts │ ├── sprite.js │ ├── store.d.ts │ └── store.js ├── docs ├── en.md ├── imgs │ ├── 1.png │ ├── 2.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── a.jpg │ ├── b.gif │ ├── c.gif │ ├── d.jpg │ ├── e.gif │ └── qr.jpg ├── recommend.md └── vs-dva.md ├── jest.config.js ├── package-lock.json ├── package.json ├── scripts └── build.js ├── src ├── Application.tsx ├── actions.ts ├── global.ts ├── index.ts ├── loading.ts ├── model.ts ├── module.tsx ├── sprite.ts └── store.ts ├── test ├── __snapshots__ │ ├── csr_render.test.ts.snap │ └── ssr_render.test.ts.snap ├── api.ts ├── client.ts ├── csr_render.test.ts ├── modules │ ├── app │ │ ├── index.ts │ │ ├── model.ts │ │ └── views │ │ │ ├── Main.tsx │ │ │ └── index.ts │ ├── index.ts │ ├── names.ts │ ├── photos │ │ ├── index.ts │ │ ├── model.ts │ │ └── views │ │ │ ├── Main.tsx │ │ │ └── index.ts │ └── videos │ │ ├── index.ts │ │ ├── model.ts │ │ └── views │ │ ├── Main.tsx │ │ └── index.ts ├── server.ts ├── setup.js ├── ssr_render.test.ts ├── type.ts └── utils.ts ├── tsconfig.build.json ├── tsconfig.jest.json ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | [**.bat] 12 | end_of_line = crlf -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/* -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | }, 5 | "extends": ["airbnb-base", "prettier"], 6 | "plugins": ["prettier"], 7 | "rules": { 8 | "prettier/prettier": "error", 9 | "import/no-extraneous-dependencies": ["error", {"devDependencies": true}], 10 | "import/no-dynamic-require": "off", 11 | "no-console": "off", 12 | "no-underscore-dangle": "off", 13 | "no-useless-concat": "off", 14 | "max-len": ["error", {"code": 300}] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | npm-debug.log 3 | npm-error.log 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "printWidth": 300, 4 | "trailingComma": "all", 5 | "bracketSpacing": false 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "9" 4 | cache: 5 | directories: 6 | - node_modules 7 | script: 8 | - npm run build 9 | branches: 10 | only: 11 | - master 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.eol": "\n", 3 | "typescript.tsdk": "node_modules/typescript/lib", 4 | "editor.formatOnSave": true, 5 | "prettier.stylelintIntegration": true, 6 | "typescript.updateImportsOnFileMove.enabled": "never" 7 | } 8 | -------------------------------------------------------------------------------- /API_zh-CN.md: -------------------------------------------------------------------------------- 1 | [English](./API.md) | **简体中文** 2 | 3 | 4 | 5 | - [BaseModuleHandlers](#basemodulehandlers) 6 | - [BaseModuleState](#basemodulestate) 7 | - [buildApp](#buildapp) 8 | - [delayPromise](#delaypromise) 9 | - [effect](#effect) 10 | - [ERROR](#error) 11 | - [exportModel](#exportmodel) 12 | - [exportModule](#exportmodule) 13 | - [exportView](#exportview) 14 | - [INIT](#init) 15 | - [LoadingState](#loadingstate) 16 | - [loadModel](#loadmodel) 17 | - [loadView](#loadview) 18 | - [LOCATION_CHANGE](#location_change) 19 | - [logger](#logger) 20 | - [reducer](#reducer) 21 | - [renderApp](#renderapp) 22 | - [RootState](#rootstate) 23 | - [RouterParser](#routerparser) 24 | - [setLoading](#setloading) 25 | - [setLoadingDepthTime](#setloadingdepthtime) 26 | - [VIEW_INVALID](#view_invalid) 27 | 28 | 29 | 30 | ### BaseModuleHandlers 31 | 32 | ModuleHandlers 的基类,所有 ModuleHandlers 必须继承此类 33 | 34 | ```JS 35 | declare class BaseModuleHandlers { 36 | protected readonly initState: S; // 初始值 37 | protected readonly namespace: N; // moduleName 38 | protected readonly store: ModelStore; // 引用 store 39 | protected readonly actions: Actions; // 引用本模块的 actions 40 | protected readonly routerActions: typeof routerActions; // 引用 connected-react-router 41 | constructor(initState: S, presetData?: any); 42 | protected readonly state: S; // 本模块的 ModuleState 43 | protected readonly rootState: R; // 全部的 State 44 | protected readonly currentState: S; // 当前本模块的 ModuleState 45 | protected readonly currentRootState: R; // 当前全部的 State 46 | protected dispatch(action: Action): Action | Promise; // 引用store.dispatch 47 | // 如果某个 actionHandler 不希望被外界触发,请设为 protected 或 private, 48 | // 此时不能使用 this.actions.someAction() 触发,而要使用 this.callThisAction(this.someAction) 49 | protected callThisAction(handler: (...args: T) => any, ...rest: T): { 50 | type: string; 51 | playload?: any; 52 | }; 53 | protected INIT(payload: S): S; // 一个 Reducer Handler,兼听action: moduleName/INIT 54 | protected UPDATE(payload: S): S; // 一个 Reducer Handler,兼听action: moduleName/UPDATE 55 | protected LOADING(payload: { // 一个 Reducer Handler,兼听action: moduleName/LOADING 56 | [group: string]: string; 57 | }): S; 58 | // 一个快捷方法,相当于this.dispatch(this.callThisAction(this.UPDATE, {...this.state, ...payload})); 59 | protected updateState(payload: Partial): void; 60 | } 61 | ``` 62 | 63 | - state / currentState 以及 rootState / currentRootState 区别: 64 | 由于使用 action handler 观察者兼听模式,一个 action 可能被多个 actionHandler 共同兼听,它们的执行是顺序依次执行的,当所有 reducer 执行完成了,才会 combine 成一个总的 rootState 来更新 store。如果在更新过程中,一个 actionHandler 中需要及时得到其它 handler update 之后的最新 state,请使用 currentState 或 currentRootState 65 | 66 | ### BaseModuleState 67 | 68 | ModuleState 的抽象接口,所有 ModuleState 必须实现此接口 69 | 70 | ```JS 71 | interface BaseModuleState { 72 | // 指明该节点是不是一个ModuleState 73 | // 该属性数据由框架自动赋值,请不要人为赋值 74 | isModule?: boolean; 75 | // 记录本模块的 loading 状态,可以是多个不同的 loading 状态 76 | loading?: { 77 | [key: string]: LoadingState; 78 | }; 79 | } 80 | ``` 81 | 82 | ### buildApp 83 | 84 | 浏览器端总的入口方法,使用此方法创建应用。 85 | 86 | ```JS 87 | declare function buildApp>( 88 | moduleGetter: M, // 模块的获取方式,同步或是异步 89 | appName: A, // 入口模块的 moduleName 90 | storeOptions?: StoreOptions, // store配置参数 91 | container?: string | Element | Function, // 容器 dom id 92 | ssrInitStoreKey?: string // 如果使用ssr,服务端的脱水数据 key 93 | ): Store; 94 | 95 | // store配置参数 96 | interface StoreOptions { 97 | reducers?: ReducersMapObject; // 第三方的 reducers,注意不要与 moduleName 冲突 98 | middlewares?: Middleware[]; // 第三方的 中间件 99 | enhancers?: StoreEnhancer[]; // 第三方的 enhancers 100 | routerParser?: RouterParser; // 自定义路由解析器 101 | initData?: any; // 初始化 store 状态,如果使用 ssr,初始化 store 状态为两者 merge 102 | } 103 | ``` 104 | 105 | ### delayPromise 106 | 107 | 方法的装饰器,装饰返回 promise 的某方法,模拟延迟 second 秒后才返回,如:@delayPromise(5) 108 | 109 | ```JS 110 | declare function delayPromise( 111 | second: number //延迟多少秒返回 112 | ):(target: any, propertyKey: string, descriptor: PropertyDescriptor) => void; 113 | ``` 114 | 115 | ### effect 116 | 117 | 方法的装饰器,指明该方法为一个 effect actionHandler,并可注入 loading 跟踪状态 118 | 119 | ```JS 120 | declare function effect( 121 | // 注入 loading key,如果不写默认 key 为 global,如果不想跟踪 loading 状态,必须设置为 null 122 | loadingForGroupName?: string | null, 123 | // 注入 loading key 的模块,默认为本模块 124 | loadingForModuleName?: string 125 | ): (target: any, key: string, descriptor: PropertyDescriptor) => PropertyDescriptor; 126 | ``` 127 | 128 | ### ERROR 129 | 130 | 一个常量,当框架捕获到未处理的错误时,会派发此 Error action,可以在模块中兼听此 action,来集中处理某些错误,比如上报给服务器 131 | 132 | ```JS 133 | declare const ERROR = "@@framework/ERROR"; 134 | ``` 135 | 136 | ### exportModel 137 | 138 | 创建并导出模块的 Model,一般写在 module/model.ts 中 139 | 140 | ```JS 141 | declare function exportModel( 142 | namespace: N, // 模块名称 143 | HandlersClass: { // ModuleActionHandlers 144 | new (initState: S, presetData?: any): BaseModuleHandlers, N>; 145 | }, 146 | initState: S // 初始 ModuleState 147 | ): Model; 148 | ``` 149 | 150 | ### exportModule 151 | 152 | 创建并导出模块的对外接口,一般写在 module/facade.ts 中 153 | 154 | ```JS 155 | declare function exportModule( 156 | namespace: string // 模块名称 157 | ): { 158 | namespace: string; 159 | actions: T; 160 | }; 161 | ``` 162 | 163 | ### exportView 164 | 165 | 创建并导出模块的 view,一般写在 module/views/index.ts 中 166 | 167 | ```JS 168 | declare function exportView>( 169 | ComponentView: C, // 要导出的 React Component 170 | model: Model // 本模块的 Model 171 | viewName?: string // view名称 172 | ): C; 173 | ``` 174 | 175 | ### INIT 176 | 177 | 一个常量,当模块初始化时,会派发一个 module/INIT 的 action,可以兼听本模块或其它模块的此 action,并对此作出反应 178 | 179 | ```JS 180 | declare const INIT = "INIT"; 181 | ``` 182 | 183 | ### LoadingState 184 | 185 | 一个可追踪的 loading 状态,使用 @effect() 或 setLoading() 可创建一个追踪的 loading 状态。 186 | 187 | - 同一个 key 名的多个 loading 状态会自动合并追踪。比如同时发出多个 ajax,但只想作为一个 loading 状态追踪。 188 | - 对于超过一定时间(默认 2 秒)的 loading 状态,会转为 Depth 状态。比如对于未超过 2 秒就返回的 ajax 请求,不需要显示黑色遮罩,防止闪烁。 189 | 190 | ```JS 191 | declare enum LoadingState { 192 | Start = "Start", 193 | Stop = "Stop", 194 | Depth = "Depth" 195 | } 196 | ``` 197 | 198 | ### loadModel 199 | 200 | 加载指定模块的 Model,一般适用于 ssr 项目,非 ssr 项目,展示 view 的时候会自动加载其 Model,无需特别调用。在 ssr 项目中由于单向数据流的限定,加载 view 前必须先加载 model,不能等待 view 来自动加载。注意,此方法会返回一个 promise,所以可以使用 await 来等待。 201 | 202 | ```JS 203 | declare function loadModel( 204 | getModule: GetModule 205 | ): Promise; 206 | ``` 207 | 208 | ### loadView 209 | 210 | 异步加载某个 view,如果是同步加载,直接用 es6 import 即可。 211 | 212 | ```JS 213 | declare function loadView, V extends ReturnViews, N extends Extract>( 214 | moduleGetter: MG, 215 | moduleName: M, 216 | viewName: N, 217 | loadingComponent?: React.ReactNode 218 | ): V[N]; 219 | ``` 220 | 221 | ### LOCATION_CHANGE 222 | 223 | 一个常量,当 URL 发生变化时时,会派发一个 @@router/LOCATION_CHANGE 的 action,可以兼听此 action,并对此作出反应 224 | 225 | ```JS 226 | declare const LOCATION_CHANGE = "@@router/LOCATION_CHANGE"; 227 | ``` 228 | 229 | ### logger 230 | 231 | 方法装饰器,用来装饰某个 effect ActionHandler,可以注入一个执行前和执行后的钩子,用来记录某些信息,比如执行时间等。 232 | 233 | ```JS 234 | declare function logger( 235 | before: (action: Action, moduleName: string, promiseResult: Promise) => void, 236 | after: null | ((status: "Rejected" | "Resolved", beforeResult: any, effectResult: any) => void) 237 | ): (target: any, key: string, descriptor: PropertyDescriptor) => void; 238 | ``` 239 | 240 | ### reducer 241 | 242 | 方法装饰器,用来指明该方法为一个 reducer actionHandler 243 | 244 | ```JS 245 | declare function reducer(target: any, key: string, descriptor: PropertyDescriptor): PropertyDescriptor; 246 | ``` 247 | 248 | ### renderApp 249 | 250 | ssr 服务器渲染的入口方法,仅用于 server 端,与 buildApp 呼应。 251 | 252 | ```JS 253 | declare function renderApp>( 254 | moduleGetter: M, // 模块的获取方式,同步或是异步 255 | appName: A, // 入口模块的 moduleName 256 | initialEntries: string[], // http请求时在 server 端虚拟 history 历史记录,一般包含当前 url 即可 257 | storeOptions?: StoreOptions, // 与 buildApp 的 storeOptions 相同 258 | ssrInitStoreKey?: string, // 服务端的脱水数据 key 259 | renderToStream?: boolean // 是否采用 renderToStream 来渲染 react 260 | ): Promise<{ 261 | html: string | NodeJS.ReadableStream; // 服务器端渲染生成的 html 262 | data: any; // 服务器端生成的脱水数据 263 | ssrInitStoreKey: string; // 服务端的脱水数据 key 264 | }>; 265 | ``` 266 | 267 | ### RootState 268 | 269 | 总的 Store 数据结构类型接口。 270 | 包含名为 router 的路由数据,该节点默认由 connected-react-routert 生成,可以使用自定义的 RouterParser 271 | 包含名为 views 的当前展示视图数据,该节点由框架根据当前 view 自动生成 272 | 273 | ```JS 274 | declare declare type RootState = { 275 | router: R; // 路由节点 276 | views: { // 当前视图展示节点 277 | [moduleName:string]?: {[viewName:string]:number}; 278 | }; 279 | } 280 | ``` 281 | 282 | ### RouterParser 283 | 284 | 自定义路由解析器,框架集成 connected-react-router,会作简单的 URL 解析,如果需要自定义解析,可以在 StoreOptions 中传入该解析器 285 | 286 | ```JS 287 | declare type RouterParser = (nextRouter: T, prevRouter?: T) => T; 288 | ``` 289 | 290 | ### setLoading 291 | 292 | 主动设置一个加载项,与 @effect("loadingKey") 注入 loading 状态一样。 293 | 294 | ```JS 295 | declare function setLoading>( 296 | item: T, // 异步加载项,必须是一个Promise 297 | // namespace/group 一起创建一个跟踪此加载项的 key,所有 key 相同的加载项将合并成一个 298 | namespace?: string, 299 | group?: string 300 | ): T; 301 | ``` 302 | 303 | ### setLoadingDepthTime 304 | 305 | 参见 LoadingState,设置 loading 状态变为 Depth 的等待时间,默认为 2 秒 306 | 307 | ```JS 308 | declare function setLoadingDepthTime(second: number): void; 309 | ``` 310 | 311 | ### VIEW_INVALID 312 | 313 | 一个常量,当视图需要更新时,会派发一个 @@framework/VIEW_INVALID 的 action,可以兼听此 action,并对此作出反应 314 | 315 | ```JS 316 | export declare const VIEW_INVALID = "@@framework/VIEW_INVALID"; 317 | ``` 318 | 319 | 当需要监听视图是否需要更新时,兼听此 action 比 @@router/LOCATION_CHANGE 更合理,该 action 的派发时机: 320 | 321 | - 当@@router/LOCATION_CHANGE 被派发后的一个任务周期内 322 | - 当任何一个 view 被 Mount 或 Unmount 后的一个任务周期内 323 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 wooline 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 | -------------------------------------------------------------------------------- /build/dist/Application.d.ts: -------------------------------------------------------------------------------- 1 | import { ReactElement } from "react"; 2 | import { Middleware, ReducersMapObject, StoreEnhancer, Store } from "redux"; 3 | import { ModuleGetter } from "./global"; 4 | import { RouterParser } from "./store"; 5 | export interface StoreOptions { 6 | reducers?: ReducersMapObject; 7 | middlewares?: Middleware[]; 8 | enhancers?: StoreEnhancer[]; 9 | routerParser?: RouterParser; 10 | initData?: any; 11 | } 12 | export declare function buildApp>(moduleGetter: M, appName: A, storeOptions?: StoreOptions, container?: string | Element | ((component: ReactElement) => void), ssrInitStoreKey?: string): Store; 13 | export declare function renderApp>(moduleGetter: M, appName: A, initialEntries: string[], storeOptions?: StoreOptions, ssrInitStoreKey?: string, renderToStream?: boolean): Promise<{ 14 | html: string | NodeJS.ReadableStream; 15 | data: any; 16 | ssrInitStoreKey: string; 17 | }>; 18 | -------------------------------------------------------------------------------- /build/dist/Application.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var tslib_1 = require("tslib"); 4 | var connected_react_router_1 = require("connected-react-router"); 5 | var createBrowserHistory_1 = require("history/createBrowserHistory"); 6 | var createMemoryHistory_1 = require("history/createMemoryHistory"); 7 | var React = require("react"); 8 | var ReactDOM = require("react-dom"); 9 | var server_1 = require("react-dom/server"); 10 | var react_redux_1 = require("react-redux"); 11 | var react_router_dom_1 = require("react-router-dom"); 12 | var global_1 = require("./global"); 13 | var store_1 = require("./store"); 14 | function isPromiseModule(module) { 15 | return typeof module["then"] === "function"; 16 | } 17 | function getModuleByName(moduleName, moduleGetter) { 18 | if (moduleName === "router") { 19 | throw new Error("router is a system module"); 20 | } 21 | var result = moduleGetter[moduleName](); 22 | if (isPromiseModule(result)) { 23 | return result.then(function (module) { 24 | moduleGetter[moduleName] = function () { return module; }; 25 | return module; 26 | }); 27 | } 28 | else { 29 | return result; 30 | } 31 | } 32 | function getModuleListByNames(moduleNames, moduleGetter) { 33 | var preModules = moduleNames.map(function (moduleName) { 34 | var module = getModuleByName(moduleName, moduleGetter); 35 | if (isPromiseModule(module)) { 36 | return module; 37 | } 38 | else { 39 | return Promise.resolve(module); 40 | } 41 | }); 42 | return Promise.all(preModules); 43 | } 44 | function buildApp(moduleGetter, appName, storeOptions, container, ssrInitStoreKey) { 45 | if (storeOptions === void 0) { storeOptions = {}; } 46 | if (container === void 0) { container = "root"; } 47 | if (ssrInitStoreKey === void 0) { ssrInitStoreKey = "reactCoatInitStore"; } 48 | global_1.MetaData.appModuleName = appName; 49 | var history = createBrowserHistory_1.default(); 50 | var initData = {}; 51 | if (storeOptions.initData || window[ssrInitStoreKey]) { 52 | initData = tslib_1.__assign({}, window[ssrInitStoreKey], storeOptions.initData); 53 | } 54 | var store = store_1.buildStore(history, storeOptions.reducers, storeOptions.middlewares, storeOptions.enhancers, initData, storeOptions.routerParser); 55 | var preModuleNames = [appName]; 56 | if (initData) { 57 | preModuleNames.push.apply(preModuleNames, Object.keys(initData).filter(function (key) { return key !== appName && initData[key].isModule; })); 58 | } 59 | getModuleListByNames(preModuleNames, moduleGetter).then(function (_a) { 60 | var appModel = _a[0]; 61 | appModel.model(store); 62 | var WithRouter = react_router_dom_1.withRouter(appModel.views.Main); 63 | var app = (React.createElement(react_redux_1.Provider, { store: store }, 64 | React.createElement(connected_react_router_1.ConnectedRouter, { history: history }, 65 | React.createElement(WithRouter, null)))); 66 | if (typeof container === "function") { 67 | container(app); 68 | } 69 | else { 70 | var render = window[ssrInitStoreKey] ? ReactDOM.hydrate : ReactDOM.render; 71 | render(app, typeof container === "string" ? document.getElementById(container) : container); 72 | } 73 | }); 74 | return store; 75 | } 76 | exports.buildApp = buildApp; 77 | function renderApp(moduleGetter, appName, initialEntries, storeOptions, ssrInitStoreKey, renderToStream) { 78 | if (storeOptions === void 0) { storeOptions = {}; } 79 | if (ssrInitStoreKey === void 0) { ssrInitStoreKey = "reactCoatInitStore"; } 80 | if (renderToStream === void 0) { renderToStream = false; } 81 | global_1.MetaData.appModuleName = appName; 82 | var history = createMemoryHistory_1.default({ initialEntries: initialEntries }); 83 | var store = store_1.buildStore(history, storeOptions.reducers, storeOptions.middlewares, storeOptions.enhancers, storeOptions.initData, storeOptions.routerParser); 84 | var appModule = moduleGetter[appName](); 85 | var render = renderToStream ? server_1.renderToNodeStream : server_1.renderToString; 86 | return appModule 87 | .model(store) 88 | .catch(function (err) { 89 | return store.dispatch(global_1.errorAction(err)); 90 | }) 91 | .then(function () { 92 | var data = store.getState(); 93 | return { 94 | ssrInitStoreKey: ssrInitStoreKey, 95 | data: data, 96 | html: render(React.createElement(react_redux_1.Provider, { store: store }, 97 | React.createElement(connected_react_router_1.ConnectedRouter, { history: history }, 98 | React.createElement(appModule.views.Main, null)))), 99 | }; 100 | }); 101 | } 102 | exports.renderApp = renderApp; 103 | -------------------------------------------------------------------------------- /build/dist/actions.d.ts: -------------------------------------------------------------------------------- 1 | import { routerActions } from "connected-react-router"; 2 | import { Action, BaseModuleState, ModelStore, RootState } from "./global"; 3 | export declare class BaseModuleHandlers, N extends string> { 4 | protected readonly initState: S; 5 | protected readonly namespace: N; 6 | protected readonly store: ModelStore; 7 | protected readonly actions: Actions; 8 | protected readonly routerActions: typeof routerActions; 9 | constructor(initState: S, presetData?: any); 10 | protected readonly state: S; 11 | protected readonly rootState: R; 12 | protected readonly currentState: S; 13 | protected readonly currentRootState: R; 14 | protected dispatch(action: Action): Action | Promise; 15 | protected callThisAction(handler: (...args: T) => any, ...rest: T): { 16 | type: string; 17 | playload?: any; 18 | }; 19 | protected INIT(payload: S): S; 20 | protected UPDATE(payload: S): S; 21 | protected LOADING(payload: { 22 | [group: string]: string; 23 | }): S; 24 | protected updateState(payload: Partial): void; 25 | } 26 | export declare function logger(before: (action: Action, moduleName: string, promiseResult: Promise) => void, after: null | ((status: "Rejected" | "Resolved", beforeResult: any, effectResult: any) => void)): (target: any, key: string, descriptor: PropertyDescriptor) => void; 27 | export declare function effect(loadingForGroupName?: string | null, loadingForModuleName?: string): (target: any, key: string, descriptor: PropertyDescriptor) => PropertyDescriptor; 28 | export declare function reducer(target: any, key: string, descriptor: PropertyDescriptor): PropertyDescriptor; 29 | declare type Handler = F extends (...args: infer P) => any ? (...args: P) => { 30 | type: string; 31 | } : never; 32 | export declare type Actions = { 33 | [K in keyof Ins]: Ins[K] extends (...args: any[]) => any ? Handler : never; 34 | }; 35 | export {}; 36 | -------------------------------------------------------------------------------- /build/dist/actions.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var tslib_1 = require("tslib"); 4 | var connected_react_router_1 = require("connected-react-router"); 5 | var global_1 = require("./global"); 6 | var loading_1 = require("./loading"); 7 | var BaseModuleHandlers = (function () { 8 | function BaseModuleHandlers(initState, presetData) { 9 | this.namespace = ""; 10 | this.store = null; 11 | this.actions = null; 12 | this.routerActions = connected_react_router_1.routerActions; 13 | initState.isModule = true; 14 | this.initState = initState; 15 | } 16 | Object.defineProperty(BaseModuleHandlers.prototype, "state", { 17 | get: function () { 18 | return this.store.reactCoat.prevState[this.namespace]; 19 | }, 20 | enumerable: true, 21 | configurable: true 22 | }); 23 | Object.defineProperty(BaseModuleHandlers.prototype, "rootState", { 24 | get: function () { 25 | return this.store.reactCoat.prevState; 26 | }, 27 | enumerable: true, 28 | configurable: true 29 | }); 30 | Object.defineProperty(BaseModuleHandlers.prototype, "currentState", { 31 | get: function () { 32 | return this.store.reactCoat.currentState[this.namespace]; 33 | }, 34 | enumerable: true, 35 | configurable: true 36 | }); 37 | Object.defineProperty(BaseModuleHandlers.prototype, "currentRootState", { 38 | get: function () { 39 | return this.store.reactCoat.currentState; 40 | }, 41 | enumerable: true, 42 | configurable: true 43 | }); 44 | BaseModuleHandlers.prototype.dispatch = function (action) { 45 | return this.store.dispatch(action); 46 | }; 47 | BaseModuleHandlers.prototype.callThisAction = function (handler) { 48 | var rest = []; 49 | for (var _i = 1; _i < arguments.length; _i++) { 50 | rest[_i - 1] = arguments[_i]; 51 | } 52 | var actions = global_1.getModuleActionCreatorList(this.namespace); 53 | return actions[handler.__actionName__](rest[0]); 54 | }; 55 | BaseModuleHandlers.prototype.INIT = function (payload) { 56 | return payload; 57 | }; 58 | BaseModuleHandlers.prototype.UPDATE = function (payload) { 59 | return payload; 60 | }; 61 | BaseModuleHandlers.prototype.LOADING = function (payload) { 62 | var state = this.state; 63 | if (!state) { 64 | return state; 65 | } 66 | return tslib_1.__assign({}, state, { loading: tslib_1.__assign({}, state.loading, payload) }); 67 | }; 68 | BaseModuleHandlers.prototype.updateState = function (payload) { 69 | this.dispatch(this.callThisAction(this.UPDATE, tslib_1.__assign({}, this.state, payload))); 70 | }; 71 | tslib_1.__decorate([ 72 | reducer 73 | ], BaseModuleHandlers.prototype, "INIT", null); 74 | tslib_1.__decorate([ 75 | reducer 76 | ], BaseModuleHandlers.prototype, "UPDATE", null); 77 | tslib_1.__decorate([ 78 | reducer 79 | ], BaseModuleHandlers.prototype, "LOADING", null); 80 | return BaseModuleHandlers; 81 | }()); 82 | exports.BaseModuleHandlers = BaseModuleHandlers; 83 | function logger(before, after) { 84 | return function (target, key, descriptor) { 85 | var fun = descriptor.value; 86 | if (!fun.__decorators__) { 87 | fun.__decorators__ = []; 88 | } 89 | fun.__decorators__.push([before, after]); 90 | }; 91 | } 92 | exports.logger = logger; 93 | function effect(loadingForGroupName, loadingForModuleName) { 94 | if (loadingForGroupName === undefined) { 95 | loadingForGroupName = "global"; 96 | loadingForModuleName = global_1.MetaData.appModuleName; 97 | } 98 | return function (target, key, descriptor) { 99 | var fun = descriptor.value; 100 | fun.__actionName__ = key; 101 | fun.__isEffect__ = true; 102 | descriptor.enumerable = true; 103 | if (loadingForGroupName) { 104 | var before = function (curAction, moduleName, promiseResult) { 105 | if (global_1.MetaData.isBrowser) { 106 | if (!loadingForModuleName) { 107 | loadingForModuleName = moduleName; 108 | } 109 | loading_1.setLoading(promiseResult, loadingForModuleName, loadingForGroupName); 110 | } 111 | }; 112 | if (!fun.__decorators__) { 113 | fun.__decorators__ = []; 114 | } 115 | fun.__decorators__.push([before, null]); 116 | } 117 | return descriptor; 118 | }; 119 | } 120 | exports.effect = effect; 121 | function reducer(target, key, descriptor) { 122 | var fun = descriptor.value; 123 | fun.__actionName__ = key; 124 | fun.__isReducer__ = true; 125 | descriptor.enumerable = true; 126 | return descriptor; 127 | } 128 | exports.reducer = reducer; 129 | -------------------------------------------------------------------------------- /build/dist/global.d.ts: -------------------------------------------------------------------------------- 1 | import { RouterState } from "connected-react-router"; 2 | import { ComponentType } from "react"; 3 | import { History } from "history"; 4 | import { Store } from "redux"; 5 | import { LoadingState } from "./loading"; 6 | export interface ModelStore extends Store { 7 | reactCoat: { 8 | history: History; 9 | prevState: { 10 | [key: string]: any; 11 | }; 12 | currentState: { 13 | [key: string]: any; 14 | }; 15 | reducerMap: ReducerMap; 16 | effectMap: EffectMap; 17 | injectedModules: { 18 | [namespace: string]: boolean; 19 | }; 20 | routerInited: boolean; 21 | currentViews: CurrentViews; 22 | }; 23 | } 24 | export interface CurrentViews { 25 | [moduleName: string]: { 26 | [viewName: string]: number; 27 | }; 28 | } 29 | export interface BaseModuleState { 30 | isModule?: boolean; 31 | loading?: { 32 | [key: string]: LoadingState; 33 | }; 34 | } 35 | export declare function isModuleState(module: any): module is BaseModuleState; 36 | export declare type GetModule = () => M | Promise; 37 | export interface ModuleGetter { 38 | [moduleName: string]: GetModule; 39 | } 40 | export declare type ReturnModule any> = T extends () => Promise ? R : T extends () => infer R ? R : Module; 41 | declare type ModuleStates = M["model"]["initState"]; 42 | declare type ModuleViews = { 43 | [key in keyof M["views"]]?: number; 44 | }; 45 | export declare type RootState = { 46 | router: R; 47 | views: { 48 | [key in keyof G]?: ModuleViews>; 49 | }; 50 | } & { 51 | [key in keyof G]?: ModuleStates>; 52 | }; 53 | export interface Action { 54 | type: string; 55 | priority?: string[]; 56 | payload?: any; 57 | } 58 | export interface ActionCreatorMap { 59 | [moduleName: string]: ActionCreatorList; 60 | } 61 | export interface ActionCreatorList { 62 | [actionName: string]: ActionCreator; 63 | } 64 | export declare type ActionCreator = (payload?: any) => Action; 65 | export interface ActionHandler { 66 | __actionName__: string; 67 | __isReducer__?: boolean; 68 | __isEffect__?: boolean; 69 | __isHandler__?: boolean; 70 | __decorators__?: Array<[(action: Action, moduleName: string, effectResult: Promise) => any, null | ((status: "Rejected" | "Resolved", beforeResult: any, effectResult: any) => void)]>; 71 | __decoratorResults__?: any[]; 72 | (payload?: any): any; 73 | } 74 | export interface ReducerHandler extends ActionHandler { 75 | (payload?: any): BaseModuleState; 76 | } 77 | export interface EffectHandler extends ActionHandler { 78 | (payload?: any): Promise; 79 | } 80 | export interface ActionHandlerList { 81 | [actionName: string]: ActionHandler; 82 | } 83 | export interface ActionHandlerMap { 84 | [actionName: string]: { 85 | [moduleName: string]: ActionHandler; 86 | }; 87 | } 88 | export interface ReducerMap extends ActionHandlerMap { 89 | [actionName: string]: { 90 | [moduleName: string]: ReducerHandler; 91 | }; 92 | } 93 | export interface EffectMap extends ActionHandlerMap { 94 | [actionName: string]: { 95 | [moduleName: string]: EffectHandler; 96 | }; 97 | } 98 | export declare const LOADING = "LOADING"; 99 | export declare const ERROR = "@@framework/ERROR"; 100 | export declare const INIT = "INIT"; 101 | export declare const LOCATION_CHANGE = "@@router/LOCATION_CHANGE"; 102 | export declare const VIEW_INVALID = "@@framework/VIEW_INVALID"; 103 | export declare const NSP = "/"; 104 | export declare const MetaData: { 105 | isBrowser: boolean; 106 | isDev: boolean; 107 | actionCreatorMap: ActionCreatorMap; 108 | clientStore: ModelStore; 109 | appModuleName: string; 110 | }; 111 | export declare function isPromise(data: any): data is Promise; 112 | export interface Module; 114 | } = { 115 | [key: string]: ComponentType; 116 | }> { 117 | model: M; 118 | views: VS; 119 | } 120 | export declare function errorAction(error: any): { 121 | type: string; 122 | error: any; 123 | }; 124 | export declare function viewInvalidAction(currentViews: CurrentViews): { 125 | type: string; 126 | currentViews: CurrentViews; 127 | }; 128 | export declare function setAppModuleName(moduleName: string): void; 129 | export declare function delayPromise(second: number): (target: any, propertyKey: string, descriptor: PropertyDescriptor) => void; 130 | export declare function getModuleActionCreatorList(namespace: string): ActionCreatorList; 131 | export interface Model { 132 | namespace: string; 133 | initState: ModuleState; 134 | (store: ModelStore): Promise; 135 | } 136 | export declare function exportModule(namespace: string): { 137 | namespace: string; 138 | actions: T; 139 | }; 140 | export declare function warning(...args: any[]): void; 141 | export {}; 142 | -------------------------------------------------------------------------------- /build/dist/global.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | function isModuleState(module) { 4 | return module.isModule; 5 | } 6 | exports.isModuleState = isModuleState; 7 | exports.LOADING = "LOADING"; 8 | exports.ERROR = "@@framework/ERROR"; 9 | exports.INIT = "INIT"; 10 | exports.LOCATION_CHANGE = "@@router/LOCATION_CHANGE"; 11 | exports.VIEW_INVALID = "@@framework/VIEW_INVALID"; 12 | exports.NSP = "/"; 13 | exports.MetaData = { 14 | isBrowser: typeof window === "object", 15 | isDev: process.env.NODE_ENV !== "production", 16 | actionCreatorMap: {}, 17 | clientStore: null, 18 | appModuleName: null, 19 | }; 20 | function isPromise(data) { 21 | return typeof data["then"] === "function"; 22 | } 23 | exports.isPromise = isPromise; 24 | function errorAction(error) { 25 | return { 26 | type: exports.ERROR, 27 | error: error, 28 | }; 29 | } 30 | exports.errorAction = errorAction; 31 | function viewInvalidAction(currentViews) { 32 | return { 33 | type: exports.VIEW_INVALID, 34 | currentViews: currentViews, 35 | }; 36 | } 37 | exports.viewInvalidAction = viewInvalidAction; 38 | function setAppModuleName(moduleName) { 39 | exports.MetaData.appModuleName = moduleName; 40 | } 41 | exports.setAppModuleName = setAppModuleName; 42 | function delayPromise(second) { 43 | return function (target, propertyKey, descriptor) { 44 | var fun = descriptor.value; 45 | descriptor.value = function () { 46 | var args = []; 47 | for (var _i = 0; _i < arguments.length; _i++) { 48 | args[_i] = arguments[_i]; 49 | } 50 | var delay = new Promise(function (resolve) { 51 | setTimeout(function () { 52 | resolve(true); 53 | }, second * 1000); 54 | }); 55 | return Promise.all([delay, fun.apply(target, args)]).then(function (items) { 56 | return items[1]; 57 | }); 58 | }; 59 | }; 60 | } 61 | exports.delayPromise = delayPromise; 62 | function getModuleActionCreatorList(namespace) { 63 | if (exports.MetaData.actionCreatorMap[namespace]) { 64 | return exports.MetaData.actionCreatorMap[namespace]; 65 | } 66 | else { 67 | var obj = {}; 68 | exports.MetaData.actionCreatorMap[namespace] = obj; 69 | return obj; 70 | } 71 | } 72 | exports.getModuleActionCreatorList = getModuleActionCreatorList; 73 | function exportModule(namespace) { 74 | var actions = getModuleActionCreatorList(namespace); 75 | return { 76 | namespace: namespace, 77 | actions: actions, 78 | }; 79 | } 80 | exports.exportModule = exportModule; 81 | function warning() { 82 | var args = []; 83 | for (var _i = 0; _i < arguments.length; _i++) { 84 | args[_i] = arguments[_i]; 85 | } 86 | if (exports.MetaData.isDev) { 87 | console.warn.apply(console, args); 88 | } 89 | } 90 | exports.warning = warning; 91 | -------------------------------------------------------------------------------- /build/dist/index.d.ts: -------------------------------------------------------------------------------- 1 | export { RouterState } from "connected-react-router"; 2 | export { Actions, BaseModuleHandlers, effect, logger, reducer } from "./actions"; 3 | export { buildApp, renderApp } from "./Application"; 4 | export { BaseModuleState, CurrentViews, delayPromise, ERROR, errorAction, exportModule, GetModule, INIT, LOCATION_CHANGE, ModelStore, Module, ModuleGetter, ReturnModule, RootState, VIEW_INVALID } from "./global"; 5 | export { LoadingState, setLoading, setLoadingDepthTime } from "./loading"; 6 | export { exportModel } from "./model"; 7 | export { exportView, loadModel, loadView } from "./module"; 8 | export { RouterParser } from "./store"; 9 | -------------------------------------------------------------------------------- /build/dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var actions_1 = require("./actions"); 4 | exports.BaseModuleHandlers = actions_1.BaseModuleHandlers; 5 | exports.effect = actions_1.effect; 6 | exports.logger = actions_1.logger; 7 | exports.reducer = actions_1.reducer; 8 | var Application_1 = require("./Application"); 9 | exports.buildApp = Application_1.buildApp; 10 | exports.renderApp = Application_1.renderApp; 11 | var global_1 = require("./global"); 12 | exports.delayPromise = global_1.delayPromise; 13 | exports.ERROR = global_1.ERROR; 14 | exports.errorAction = global_1.errorAction; 15 | exports.exportModule = global_1.exportModule; 16 | exports.INIT = global_1.INIT; 17 | exports.LOCATION_CHANGE = global_1.LOCATION_CHANGE; 18 | exports.VIEW_INVALID = global_1.VIEW_INVALID; 19 | var loading_1 = require("./loading"); 20 | exports.LoadingState = loading_1.LoadingState; 21 | exports.setLoading = loading_1.setLoading; 22 | exports.setLoadingDepthTime = loading_1.setLoadingDepthTime; 23 | var model_1 = require("./model"); 24 | exports.exportModel = model_1.exportModel; 25 | var module_1 = require("./module"); 26 | exports.exportView = module_1.exportView; 27 | exports.loadModel = module_1.loadModel; 28 | exports.loadView = module_1.loadView; 29 | -------------------------------------------------------------------------------- /build/dist/loading.d.ts: -------------------------------------------------------------------------------- 1 | export declare function setLoadingDepthTime(second: number): void; 2 | export declare function setLoading>(item: T, namespace?: string, group?: string): T; 3 | export declare enum LoadingState { 4 | Start = "Start", 5 | Stop = "Stop", 6 | Depth = "Depth" 7 | } 8 | -------------------------------------------------------------------------------- /build/dist/loading.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var global_1 = require("./global"); 4 | var sprite_1 = require("./sprite"); 5 | var loadings = {}; 6 | var depthTime = 2; 7 | function setLoadingDepthTime(second) { 8 | depthTime = second; 9 | } 10 | exports.setLoadingDepthTime = setLoadingDepthTime; 11 | function setLoading(item, namespace, group) { 12 | if (namespace === void 0) { namespace = global_1.MetaData.appModuleName; } 13 | if (group === void 0) { group = "global"; } 14 | if (!global_1.MetaData.isBrowser) { 15 | return item; 16 | } 17 | var key = namespace + "/" + group; 18 | if (!loadings[key]) { 19 | loadings[key] = new sprite_1.TaskCounter(depthTime); 20 | loadings[key].addListener(sprite_1.TaskCountEvent, function (e) { 21 | var _a; 22 | var store = global_1.MetaData.clientStore; 23 | if (store) { 24 | var actions = global_1.getModuleActionCreatorList(namespace)[global_1.LOADING]; 25 | var action = actions((_a = {}, _a[group] = e.data, _a)); 26 | store.dispatch(action); 27 | } 28 | }); 29 | } 30 | loadings[key].addItem(item); 31 | return item; 32 | } 33 | exports.setLoading = setLoading; 34 | var LoadingState; 35 | (function (LoadingState) { 36 | LoadingState["Start"] = "Start"; 37 | LoadingState["Stop"] = "Stop"; 38 | LoadingState["Depth"] = "Depth"; 39 | })(LoadingState = exports.LoadingState || (exports.LoadingState = {})); 40 | -------------------------------------------------------------------------------- /build/dist/model.d.ts: -------------------------------------------------------------------------------- 1 | import { BaseModuleState, RootState, Model } from "./global"; 2 | import { BaseModuleHandlers } from "./actions"; 3 | export declare function exportModel(namespace: N, HandlersClass: { 4 | new (initState: S, presetData?: any): BaseModuleHandlers, N>; 5 | }, initState: S): Model; 6 | -------------------------------------------------------------------------------- /build/dist/model.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var global_1 = require("./global"); 4 | function exportModel(namespace, HandlersClass, initState) { 5 | var fun = function (store) { 6 | var hasInjected = store.reactCoat.injectedModules[namespace]; 7 | if (!hasInjected) { 8 | store.reactCoat.injectedModules[namespace] = true; 9 | var moduleState = store.getState()[namespace]; 10 | var handlers = new HandlersClass(initState, moduleState); 11 | handlers.namespace = namespace; 12 | handlers.store = store; 13 | var actions = injectActions(store, namespace, handlers); 14 | handlers.actions = actions; 15 | if (!moduleState) { 16 | var initAction = actions.INIT(handlers.initState); 17 | var action = store.dispatch(initAction); 18 | if (global_1.isPromise(action)) { 19 | return action; 20 | } 21 | else { 22 | return Promise.resolve(void 0); 23 | } 24 | } 25 | else { 26 | return Promise.resolve(void 0); 27 | } 28 | } 29 | else { 30 | return Promise.resolve(void 0); 31 | } 32 | }; 33 | fun.namespace = namespace; 34 | fun.initState = initState; 35 | return fun; 36 | } 37 | exports.exportModel = exportModel; 38 | function bindThis(fun, thisObj) { 39 | var newFun = fun.bind(thisObj); 40 | Object.keys(fun).forEach(function (key) { 41 | newFun[key] = fun[key]; 42 | }); 43 | return newFun; 44 | } 45 | function injectActions(store, namespace, handlers) { 46 | for (var actionName in handlers) { 47 | if (typeof handlers[actionName] === "function") { 48 | var handler = handlers[actionName]; 49 | if (handler.__isReducer__ || handler.__isEffect__) { 50 | handler = bindThis(handler, handlers); 51 | var arr = actionName.split(global_1.NSP); 52 | if (arr[1]) { 53 | handler.__isHandler__ = true; 54 | transformAction(actionName, handler, namespace, handler.__isEffect__ ? store.reactCoat.effectMap : store.reactCoat.reducerMap); 55 | } 56 | else { 57 | handler.__isHandler__ = false; 58 | transformAction(namespace + global_1.NSP + actionName, handler, namespace, handler.__isEffect__ ? store.reactCoat.effectMap : store.reactCoat.reducerMap); 59 | addModuleActionCreatorList(namespace, actionName); 60 | } 61 | } 62 | } 63 | } 64 | return global_1.getModuleActionCreatorList(namespace); 65 | } 66 | function transformAction(actionName, action, listenerModule, actionHandlerMap) { 67 | if (!actionHandlerMap[actionName]) { 68 | actionHandlerMap[actionName] = {}; 69 | } 70 | if (actionHandlerMap[actionName][listenerModule]) { 71 | throw new Error("Action duplicate or conflict : " + actionName + "."); 72 | } 73 | actionHandlerMap[actionName][listenerModule] = action; 74 | } 75 | function addModuleActionCreatorList(namespace, actionName) { 76 | var actions = global_1.getModuleActionCreatorList(namespace); 77 | if (!actions[actionName]) { 78 | actions[actionName] = function (payload) { return ({ type: namespace + global_1.NSP + actionName, payload: payload }); }; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /build/dist/module.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { ComponentType } from "react"; 3 | import { Model, Module, GetModule, ModuleGetter } from "./global"; 4 | export declare function loadModel(getModule: GetModule): Promise; 5 | export declare type ReturnViews any> = T extends () => Promise> ? R : never; 6 | export declare function loadView, V extends ReturnViews, N extends Extract>(moduleGetter: MG, moduleName: M, viewName: N, loadingComponent?: React.ReactNode): V[N]; 7 | export declare function exportView>(ComponentView: C, model: Model, viewName: string): C; 8 | -------------------------------------------------------------------------------- /build/dist/module.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var tslib_1 = require("tslib"); 4 | var React = require("react"); 5 | var global_1 = require("./global"); 6 | var store_1 = require("./store"); 7 | function isPromiseModule(module) { 8 | return typeof module["then"] === "function"; 9 | } 10 | function isPromiseView(moduleView) { 11 | return typeof moduleView["then"] === "function"; 12 | } 13 | function getView(getModule, viewName) { 14 | var result = getModule(); 15 | if (isPromiseModule(result)) { 16 | return result.then(function (module) { return module.views[viewName]; }); 17 | } 18 | else { 19 | return result.views[viewName]; 20 | } 21 | } 22 | function loadModel(getModule) { 23 | var result = getModule(); 24 | if (isPromiseModule(result)) { 25 | return result.then(function (module) { return module.model; }); 26 | } 27 | else { 28 | return Promise.resolve(result.model); 29 | } 30 | } 31 | exports.loadModel = loadModel; 32 | function loadView(moduleGetter, moduleName, viewName, loadingComponent) { 33 | if (loadingComponent === void 0) { loadingComponent = null; } 34 | return (function (_super) { 35 | tslib_1.__extends(Loader, _super); 36 | function Loader() { 37 | var _this = _super !== null && _super.apply(this, arguments) || this; 38 | _this.state = { 39 | Component: null, 40 | }; 41 | return _this; 42 | } 43 | Loader.prototype.shouldComponentUpdate = function (nextProps, nextState) { 44 | return nextState.Component !== this.state.Component; 45 | }; 46 | Loader.prototype.componentWillMount = function () { 47 | var _this = this; 48 | var moduleViewResult = getView(moduleGetter[moduleName], viewName); 49 | if (isPromiseView(moduleViewResult)) { 50 | moduleViewResult.then(function (Component) { 51 | _this.setState({ 52 | Component: Component, 53 | }); 54 | }); 55 | } 56 | else { 57 | this.setState({ 58 | Component: moduleViewResult, 59 | }); 60 | } 61 | }; 62 | Loader.prototype.render = function () { 63 | var Component = this.state.Component; 64 | return Component ? React.createElement(Component, tslib_1.__assign({}, this.props)) : loadingComponent; 65 | }; 66 | return Loader; 67 | }(React.Component)); 68 | } 69 | exports.loadView = loadView; 70 | function exportView(ComponentView, model, viewName) { 71 | var Comp = ComponentView; 72 | if (global_1.MetaData.isBrowser) { 73 | return (function (_super) { 74 | tslib_1.__extends(Component, _super); 75 | function Component(props, context) { 76 | var _this = _super.call(this, props, context) || this; 77 | var state = global_1.MetaData.clientStore.getState(); 78 | var namespace = model.namespace; 79 | _this.state = { 80 | modelReady: !!state[namespace], 81 | }; 82 | model(global_1.MetaData.clientStore).then(function () { 83 | if (!_this.state.modelReady) { 84 | _this.setState({ modelReady: true }); 85 | } 86 | }); 87 | return _this; 88 | } 89 | Component.prototype.componentWillMount = function () { 90 | var _a; 91 | var currentViews = global_1.MetaData.clientStore.reactCoat.currentViews; 92 | if (!currentViews[model.namespace]) { 93 | currentViews[model.namespace] = (_a = {}, _a[viewName] = 1, _a); 94 | } 95 | else { 96 | var views = currentViews[model.namespace]; 97 | if (!views[viewName]) { 98 | views[viewName] = 1; 99 | } 100 | else { 101 | views[viewName]++; 102 | } 103 | } 104 | store_1.invalidview(); 105 | }; 106 | Component.prototype.componentWillUnmount = function () { 107 | var currentViews = global_1.MetaData.clientStore.reactCoat.currentViews; 108 | if (currentViews[model.namespace] && currentViews[model.namespace][viewName]) { 109 | currentViews[model.namespace][viewName]--; 110 | } 111 | store_1.invalidview(); 112 | }; 113 | Component.prototype.render = function () { 114 | return this.state.modelReady ? React.createElement(Comp, tslib_1.__assign({}, this.props)) : null; 115 | }; 116 | return Component; 117 | }(React.PureComponent)); 118 | } 119 | else { 120 | return Comp; 121 | } 122 | } 123 | exports.exportView = exportView; 124 | -------------------------------------------------------------------------------- /build/dist/sprite.d.ts: -------------------------------------------------------------------------------- 1 | export declare const TaskCountEvent = "TaskCountEvent"; 2 | export declare type TaskCounterState = "Start" | "Stop" | "Depth"; 3 | export declare class PEvent { 4 | readonly name: string; 5 | readonly data?: any; 6 | bubbling: boolean; 7 | readonly target: PDispatcher; 8 | readonly currentTarget: PDispatcher; 9 | constructor(name: string, data?: any, bubbling?: boolean); 10 | setTarget(target: PDispatcher): void; 11 | setCurrentTarget(target: PDispatcher): void; 12 | } 13 | export declare class PDispatcher { 14 | readonly parent?: PDispatcher | undefined; 15 | protected readonly storeHandlers: { 16 | [key: string]: Array<(e: PEvent) => void>; 17 | }; 18 | constructor(parent?: PDispatcher | undefined); 19 | addListener(ename: string, handler: (e: PEvent) => void): this; 20 | removeListener(ename?: string, handler?: (e: PEvent) => void): this; 21 | dispatch(evt: PEvent): this; 22 | setParent(parent?: PDispatcher): this; 23 | } 24 | export declare class TaskCounter extends PDispatcher { 25 | deferSecond: number; 26 | readonly list: Array<{ 27 | promise: Promise; 28 | note: string; 29 | }>; 30 | private ctimer; 31 | constructor(deferSecond: number); 32 | addItem(promise: Promise, note?: string): Promise; 33 | private completeItem; 34 | } 35 | -------------------------------------------------------------------------------- /build/dist/sprite.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var tslib_1 = require("tslib"); 4 | function emptyObject(obj) { 5 | var arr = []; 6 | for (var key in obj) { 7 | if (obj.hasOwnProperty(key)) { 8 | arr.push(key); 9 | } 10 | } 11 | arr.forEach(function (key) { 12 | delete obj[key]; 13 | }); 14 | return obj; 15 | } 16 | function findIndexInArray(arr, fun) { 17 | for (var i = 0, k = arr.length; i < k; i++) { 18 | if (fun(arr[i])) { 19 | return i; 20 | } 21 | } 22 | return -1; 23 | } 24 | exports.TaskCountEvent = "TaskCountEvent"; 25 | var PEvent = (function () { 26 | function PEvent(name, data, bubbling) { 27 | if (bubbling === void 0) { bubbling = false; } 28 | this.name = name; 29 | this.data = data; 30 | this.bubbling = bubbling; 31 | this.target = null; 32 | this.currentTarget = null; 33 | } 34 | PEvent.prototype.setTarget = function (target) { 35 | this.target = target; 36 | }; 37 | PEvent.prototype.setCurrentTarget = function (target) { 38 | this.currentTarget = target; 39 | }; 40 | return PEvent; 41 | }()); 42 | exports.PEvent = PEvent; 43 | var PDispatcher = (function () { 44 | function PDispatcher(parent) { 45 | this.parent = parent; 46 | this.storeHandlers = {}; 47 | } 48 | PDispatcher.prototype.addListener = function (ename, handler) { 49 | var dictionary = this.storeHandlers[ename]; 50 | if (!dictionary) { 51 | this.storeHandlers[ename] = dictionary = []; 52 | } 53 | dictionary.push(handler); 54 | return this; 55 | }; 56 | PDispatcher.prototype.removeListener = function (ename, handler) { 57 | if (!ename) { 58 | emptyObject(this.storeHandlers); 59 | } 60 | else { 61 | var handlers = this.storeHandlers; 62 | if (handlers.propertyIsEnumerable(ename)) { 63 | var dictionary = handlers[ename]; 64 | if (!handler) { 65 | delete handlers[ename]; 66 | } 67 | else { 68 | var n = dictionary.indexOf(handler); 69 | if (n > -1) { 70 | dictionary.splice(n, 1); 71 | } 72 | if (dictionary.length === 0) { 73 | delete handlers[ename]; 74 | } 75 | } 76 | } 77 | } 78 | return this; 79 | }; 80 | PDispatcher.prototype.dispatch = function (evt) { 81 | if (!evt.target) { 82 | evt.setTarget(this); 83 | } 84 | evt.setCurrentTarget(this); 85 | var dictionary = this.storeHandlers[evt.name]; 86 | if (dictionary) { 87 | for (var i = 0, k = dictionary.length; i < k; i++) { 88 | dictionary[i](evt); 89 | } 90 | } 91 | if (this.parent && evt.bubbling) { 92 | this.parent.dispatch(evt); 93 | } 94 | return this; 95 | }; 96 | PDispatcher.prototype.setParent = function (parent) { 97 | this.parent = parent; 98 | return this; 99 | }; 100 | return PDispatcher; 101 | }()); 102 | exports.PDispatcher = PDispatcher; 103 | var TaskCounter = (function (_super) { 104 | tslib_1.__extends(TaskCounter, _super); 105 | function TaskCounter(deferSecond) { 106 | var _this = _super.call(this) || this; 107 | _this.deferSecond = deferSecond; 108 | _this.list = []; 109 | _this.ctimer = 0; 110 | return _this; 111 | } 112 | TaskCounter.prototype.addItem = function (promise, note) { 113 | var _this = this; 114 | if (note === void 0) { note = ""; } 115 | if (!this.list.some(function (item) { return item.promise === promise; })) { 116 | this.list.push({ promise: promise, note: note }); 117 | promise.then(function (resolve) { return _this.completeItem(promise); }, function (reject) { return _this.completeItem(promise); }); 118 | if (this.list.length === 1) { 119 | this.dispatch(new PEvent(exports.TaskCountEvent, "Start")); 120 | this.ctimer = window.setTimeout(function () { 121 | _this.ctimer = 0; 122 | if (_this.list.length > 0) { 123 | _this.dispatch(new PEvent(exports.TaskCountEvent, "Depth")); 124 | } 125 | }, this.deferSecond * 1000); 126 | } 127 | } 128 | return promise; 129 | }; 130 | TaskCounter.prototype.completeItem = function (promise) { 131 | var i = findIndexInArray(this.list, function (item) { return item.promise === promise; }); 132 | if (i > -1) { 133 | this.list.splice(i, 1); 134 | if (this.list.length === 0) { 135 | if (this.ctimer) { 136 | clearTimeout(this.ctimer); 137 | this.ctimer = 0; 138 | } 139 | this.dispatch(new PEvent(exports.TaskCountEvent, "Stop")); 140 | } 141 | } 142 | return this; 143 | }; 144 | return TaskCounter; 145 | }(PDispatcher)); 146 | exports.TaskCounter = TaskCounter; 147 | -------------------------------------------------------------------------------- /build/dist/store.d.ts: -------------------------------------------------------------------------------- 1 | import { History } from "history"; 2 | import { Middleware, ReducersMapObject, StoreEnhancer } from "redux"; 3 | import { ModelStore } from "./global"; 4 | export declare function invalidview(): void; 5 | export declare type RouterParser = (nextRouter: T, prevRouter?: T) => T; 6 | export declare function buildStore(storeHistory: History, reducersMapObject?: ReducersMapObject, storeMiddlewares?: Middleware[], storeEnhancers?: StoreEnhancer[], initData?: any, routerParser?: RouterParser): ModelStore; 7 | -------------------------------------------------------------------------------- /build/es5/Application.d.ts: -------------------------------------------------------------------------------- 1 | import { ReactElement } from "react"; 2 | import { Middleware, ReducersMapObject, StoreEnhancer, Store } from "redux"; 3 | import { ModuleGetter } from "./global"; 4 | import { RouterParser } from "./store"; 5 | export interface StoreOptions { 6 | reducers?: ReducersMapObject; 7 | middlewares?: Middleware[]; 8 | enhancers?: StoreEnhancer[]; 9 | routerParser?: RouterParser; 10 | initData?: any; 11 | } 12 | export declare function buildApp>(moduleGetter: M, appName: A, storeOptions?: StoreOptions, container?: string | Element | ((component: ReactElement) => void), ssrInitStoreKey?: string): Store; 13 | export declare function renderApp>(moduleGetter: M, appName: A, initialEntries: string[], storeOptions?: StoreOptions, ssrInitStoreKey?: string, renderToStream?: boolean): Promise<{ 14 | html: string | NodeJS.ReadableStream; 15 | data: any; 16 | ssrInitStoreKey: string; 17 | }>; 18 | -------------------------------------------------------------------------------- /build/es5/Application.js: -------------------------------------------------------------------------------- 1 | import * as tslib_1 from "tslib"; 2 | import { ConnectedRouter } from "connected-react-router"; 3 | import createBrowserHistory from "history/createBrowserHistory"; 4 | import createMemoryHistory from "history/createMemoryHistory"; 5 | import * as React from "react"; 6 | import * as ReactDOM from "react-dom"; 7 | import { renderToNodeStream, renderToString } from "react-dom/server"; 8 | import { Provider } from "react-redux"; 9 | import { withRouter } from "react-router-dom"; 10 | import { errorAction, MetaData } from "./global"; 11 | import { buildStore } from "./store"; 12 | function isPromiseModule(module) { 13 | return typeof module["then"] === "function"; 14 | } 15 | function getModuleByName(moduleName, moduleGetter) { 16 | if (moduleName === "router") { 17 | throw new Error("router is a system module"); 18 | } 19 | var result = moduleGetter[moduleName](); 20 | if (isPromiseModule(result)) { 21 | return result.then(function (module) { 22 | moduleGetter[moduleName] = function () { return module; }; 23 | return module; 24 | }); 25 | } 26 | else { 27 | return result; 28 | } 29 | } 30 | function getModuleListByNames(moduleNames, moduleGetter) { 31 | var preModules = moduleNames.map(function (moduleName) { 32 | var module = getModuleByName(moduleName, moduleGetter); 33 | if (isPromiseModule(module)) { 34 | return module; 35 | } 36 | else { 37 | return Promise.resolve(module); 38 | } 39 | }); 40 | return Promise.all(preModules); 41 | } 42 | export function buildApp(moduleGetter, appName, storeOptions, container, ssrInitStoreKey) { 43 | if (storeOptions === void 0) { storeOptions = {}; } 44 | if (container === void 0) { container = "root"; } 45 | if (ssrInitStoreKey === void 0) { ssrInitStoreKey = "reactCoatInitStore"; } 46 | MetaData.appModuleName = appName; 47 | var history = createBrowserHistory(); 48 | var initData = {}; 49 | if (storeOptions.initData || window[ssrInitStoreKey]) { 50 | initData = tslib_1.__assign({}, window[ssrInitStoreKey], storeOptions.initData); 51 | } 52 | var store = buildStore(history, storeOptions.reducers, storeOptions.middlewares, storeOptions.enhancers, initData, storeOptions.routerParser); 53 | var preModuleNames = [appName]; 54 | if (initData) { 55 | preModuleNames.push.apply(preModuleNames, Object.keys(initData).filter(function (key) { return key !== appName && initData[key].isModule; })); 56 | } 57 | getModuleListByNames(preModuleNames, moduleGetter).then(function (_a) { 58 | var appModel = _a[0]; 59 | appModel.model(store); 60 | var WithRouter = withRouter(appModel.views.Main); 61 | var app = (React.createElement(Provider, { store: store }, 62 | React.createElement(ConnectedRouter, { history: history }, 63 | React.createElement(WithRouter, null)))); 64 | if (typeof container === "function") { 65 | container(app); 66 | } 67 | else { 68 | var render = window[ssrInitStoreKey] ? ReactDOM.hydrate : ReactDOM.render; 69 | render(app, typeof container === "string" ? document.getElementById(container) : container); 70 | } 71 | }); 72 | return store; 73 | } 74 | export function renderApp(moduleGetter, appName, initialEntries, storeOptions, ssrInitStoreKey, renderToStream) { 75 | if (storeOptions === void 0) { storeOptions = {}; } 76 | if (ssrInitStoreKey === void 0) { ssrInitStoreKey = "reactCoatInitStore"; } 77 | if (renderToStream === void 0) { renderToStream = false; } 78 | MetaData.appModuleName = appName; 79 | var history = createMemoryHistory({ initialEntries: initialEntries }); 80 | var store = buildStore(history, storeOptions.reducers, storeOptions.middlewares, storeOptions.enhancers, storeOptions.initData, storeOptions.routerParser); 81 | var appModule = moduleGetter[appName](); 82 | var render = renderToStream ? renderToNodeStream : renderToString; 83 | return appModule 84 | .model(store) 85 | .catch(function (err) { 86 | return store.dispatch(errorAction(err)); 87 | }) 88 | .then(function () { 89 | var data = store.getState(); 90 | return { 91 | ssrInitStoreKey: ssrInitStoreKey, 92 | data: data, 93 | html: render(React.createElement(Provider, { store: store }, 94 | React.createElement(ConnectedRouter, { history: history }, 95 | React.createElement(appModule.views.Main, null)))), 96 | }; 97 | }); 98 | } 99 | -------------------------------------------------------------------------------- /build/es5/actions.d.ts: -------------------------------------------------------------------------------- 1 | import { routerActions } from "connected-react-router"; 2 | import { Action, BaseModuleState, ModelStore, RootState } from "./global"; 3 | export declare class BaseModuleHandlers, N extends string> { 4 | protected readonly initState: S; 5 | protected readonly namespace: N; 6 | protected readonly store: ModelStore; 7 | protected readonly actions: Actions; 8 | protected readonly routerActions: typeof routerActions; 9 | constructor(initState: S, presetData?: any); 10 | protected readonly state: S; 11 | protected readonly rootState: R; 12 | protected readonly currentState: S; 13 | protected readonly currentRootState: R; 14 | protected dispatch(action: Action): Action | Promise; 15 | protected callThisAction(handler: (...args: T) => any, ...rest: T): { 16 | type: string; 17 | playload?: any; 18 | }; 19 | protected INIT(payload: S): S; 20 | protected UPDATE(payload: S): S; 21 | protected LOADING(payload: { 22 | [group: string]: string; 23 | }): S; 24 | protected updateState(payload: Partial): void; 25 | } 26 | export declare function logger(before: (action: Action, moduleName: string, promiseResult: Promise) => void, after: null | ((status: "Rejected" | "Resolved", beforeResult: any, effectResult: any) => void)): (target: any, key: string, descriptor: PropertyDescriptor) => void; 27 | export declare function effect(loadingForGroupName?: string | null, loadingForModuleName?: string): (target: any, key: string, descriptor: PropertyDescriptor) => PropertyDescriptor; 28 | export declare function reducer(target: any, key: string, descriptor: PropertyDescriptor): PropertyDescriptor; 29 | declare type Handler = F extends (...args: infer P) => any ? (...args: P) => { 30 | type: string; 31 | } : never; 32 | export declare type Actions = { 33 | [K in keyof Ins]: Ins[K] extends (...args: any[]) => any ? Handler : never; 34 | }; 35 | export {}; 36 | -------------------------------------------------------------------------------- /build/es5/actions.js: -------------------------------------------------------------------------------- 1 | import * as tslib_1 from "tslib"; 2 | import { routerActions } from "connected-react-router"; 3 | import { getModuleActionCreatorList, MetaData } from "./global"; 4 | import { setLoading } from "./loading"; 5 | var BaseModuleHandlers = (function () { 6 | function BaseModuleHandlers(initState, presetData) { 7 | this.namespace = ""; 8 | this.store = null; 9 | this.actions = null; 10 | this.routerActions = routerActions; 11 | initState.isModule = true; 12 | this.initState = initState; 13 | } 14 | Object.defineProperty(BaseModuleHandlers.prototype, "state", { 15 | get: function () { 16 | return this.store.reactCoat.prevState[this.namespace]; 17 | }, 18 | enumerable: true, 19 | configurable: true 20 | }); 21 | Object.defineProperty(BaseModuleHandlers.prototype, "rootState", { 22 | get: function () { 23 | return this.store.reactCoat.prevState; 24 | }, 25 | enumerable: true, 26 | configurable: true 27 | }); 28 | Object.defineProperty(BaseModuleHandlers.prototype, "currentState", { 29 | get: function () { 30 | return this.store.reactCoat.currentState[this.namespace]; 31 | }, 32 | enumerable: true, 33 | configurable: true 34 | }); 35 | Object.defineProperty(BaseModuleHandlers.prototype, "currentRootState", { 36 | get: function () { 37 | return this.store.reactCoat.currentState; 38 | }, 39 | enumerable: true, 40 | configurable: true 41 | }); 42 | BaseModuleHandlers.prototype.dispatch = function (action) { 43 | return this.store.dispatch(action); 44 | }; 45 | BaseModuleHandlers.prototype.callThisAction = function (handler) { 46 | var rest = []; 47 | for (var _i = 1; _i < arguments.length; _i++) { 48 | rest[_i - 1] = arguments[_i]; 49 | } 50 | var actions = getModuleActionCreatorList(this.namespace); 51 | return actions[handler.__actionName__](rest[0]); 52 | }; 53 | BaseModuleHandlers.prototype.INIT = function (payload) { 54 | return payload; 55 | }; 56 | BaseModuleHandlers.prototype.UPDATE = function (payload) { 57 | return payload; 58 | }; 59 | BaseModuleHandlers.prototype.LOADING = function (payload) { 60 | var state = this.state; 61 | if (!state) { 62 | return state; 63 | } 64 | return tslib_1.__assign({}, state, { loading: tslib_1.__assign({}, state.loading, payload) }); 65 | }; 66 | BaseModuleHandlers.prototype.updateState = function (payload) { 67 | this.dispatch(this.callThisAction(this.UPDATE, tslib_1.__assign({}, this.state, payload))); 68 | }; 69 | tslib_1.__decorate([ 70 | reducer 71 | ], BaseModuleHandlers.prototype, "INIT", null); 72 | tslib_1.__decorate([ 73 | reducer 74 | ], BaseModuleHandlers.prototype, "UPDATE", null); 75 | tslib_1.__decorate([ 76 | reducer 77 | ], BaseModuleHandlers.prototype, "LOADING", null); 78 | return BaseModuleHandlers; 79 | }()); 80 | export { BaseModuleHandlers }; 81 | export function logger(before, after) { 82 | return function (target, key, descriptor) { 83 | var fun = descriptor.value; 84 | if (!fun.__decorators__) { 85 | fun.__decorators__ = []; 86 | } 87 | fun.__decorators__.push([before, after]); 88 | }; 89 | } 90 | export function effect(loadingForGroupName, loadingForModuleName) { 91 | if (loadingForGroupName === undefined) { 92 | loadingForGroupName = "global"; 93 | loadingForModuleName = MetaData.appModuleName; 94 | } 95 | return function (target, key, descriptor) { 96 | var fun = descriptor.value; 97 | fun.__actionName__ = key; 98 | fun.__isEffect__ = true; 99 | descriptor.enumerable = true; 100 | if (loadingForGroupName) { 101 | var before = function (curAction, moduleName, promiseResult) { 102 | if (MetaData.isBrowser) { 103 | if (!loadingForModuleName) { 104 | loadingForModuleName = moduleName; 105 | } 106 | setLoading(promiseResult, loadingForModuleName, loadingForGroupName); 107 | } 108 | }; 109 | if (!fun.__decorators__) { 110 | fun.__decorators__ = []; 111 | } 112 | fun.__decorators__.push([before, null]); 113 | } 114 | return descriptor; 115 | }; 116 | } 117 | export function reducer(target, key, descriptor) { 118 | var fun = descriptor.value; 119 | fun.__actionName__ = key; 120 | fun.__isReducer__ = true; 121 | descriptor.enumerable = true; 122 | return descriptor; 123 | } 124 | -------------------------------------------------------------------------------- /build/es5/global.d.ts: -------------------------------------------------------------------------------- 1 | import { RouterState } from "connected-react-router"; 2 | import { ComponentType } from "react"; 3 | import { History } from "history"; 4 | import { Store } from "redux"; 5 | import { LoadingState } from "./loading"; 6 | export interface ModelStore extends Store { 7 | reactCoat: { 8 | history: History; 9 | prevState: { 10 | [key: string]: any; 11 | }; 12 | currentState: { 13 | [key: string]: any; 14 | }; 15 | reducerMap: ReducerMap; 16 | effectMap: EffectMap; 17 | injectedModules: { 18 | [namespace: string]: boolean; 19 | }; 20 | routerInited: boolean; 21 | currentViews: CurrentViews; 22 | }; 23 | } 24 | export interface CurrentViews { 25 | [moduleName: string]: { 26 | [viewName: string]: number; 27 | }; 28 | } 29 | export interface BaseModuleState { 30 | isModule?: boolean; 31 | loading?: { 32 | [key: string]: LoadingState; 33 | }; 34 | } 35 | export declare function isModuleState(module: any): module is BaseModuleState; 36 | export declare type GetModule = () => M | Promise; 37 | export interface ModuleGetter { 38 | [moduleName: string]: GetModule; 39 | } 40 | export declare type ReturnModule any> = T extends () => Promise ? R : T extends () => infer R ? R : Module; 41 | declare type ModuleStates = M["model"]["initState"]; 42 | declare type ModuleViews = { 43 | [key in keyof M["views"]]?: number; 44 | }; 45 | export declare type RootState = { 46 | router: R; 47 | views: { 48 | [key in keyof G]?: ModuleViews>; 49 | }; 50 | } & { 51 | [key in keyof G]?: ModuleStates>; 52 | }; 53 | export interface Action { 54 | type: string; 55 | priority?: string[]; 56 | payload?: any; 57 | } 58 | export interface ActionCreatorMap { 59 | [moduleName: string]: ActionCreatorList; 60 | } 61 | export interface ActionCreatorList { 62 | [actionName: string]: ActionCreator; 63 | } 64 | export declare type ActionCreator = (payload?: any) => Action; 65 | export interface ActionHandler { 66 | __actionName__: string; 67 | __isReducer__?: boolean; 68 | __isEffect__?: boolean; 69 | __isHandler__?: boolean; 70 | __decorators__?: Array<[(action: Action, moduleName: string, effectResult: Promise) => any, null | ((status: "Rejected" | "Resolved", beforeResult: any, effectResult: any) => void)]>; 71 | __decoratorResults__?: any[]; 72 | (payload?: any): any; 73 | } 74 | export interface ReducerHandler extends ActionHandler { 75 | (payload?: any): BaseModuleState; 76 | } 77 | export interface EffectHandler extends ActionHandler { 78 | (payload?: any): Promise; 79 | } 80 | export interface ActionHandlerList { 81 | [actionName: string]: ActionHandler; 82 | } 83 | export interface ActionHandlerMap { 84 | [actionName: string]: { 85 | [moduleName: string]: ActionHandler; 86 | }; 87 | } 88 | export interface ReducerMap extends ActionHandlerMap { 89 | [actionName: string]: { 90 | [moduleName: string]: ReducerHandler; 91 | }; 92 | } 93 | export interface EffectMap extends ActionHandlerMap { 94 | [actionName: string]: { 95 | [moduleName: string]: EffectHandler; 96 | }; 97 | } 98 | export declare const LOADING = "LOADING"; 99 | export declare const ERROR = "@@framework/ERROR"; 100 | export declare const INIT = "INIT"; 101 | export declare const LOCATION_CHANGE = "@@router/LOCATION_CHANGE"; 102 | export declare const VIEW_INVALID = "@@framework/VIEW_INVALID"; 103 | export declare const NSP = "/"; 104 | export declare const MetaData: { 105 | isBrowser: boolean; 106 | isDev: boolean; 107 | actionCreatorMap: ActionCreatorMap; 108 | clientStore: ModelStore; 109 | appModuleName: string; 110 | }; 111 | export declare function isPromise(data: any): data is Promise; 112 | export interface Module; 114 | } = { 115 | [key: string]: ComponentType; 116 | }> { 117 | model: M; 118 | views: VS; 119 | } 120 | export declare function errorAction(error: any): { 121 | type: string; 122 | error: any; 123 | }; 124 | export declare function viewInvalidAction(currentViews: CurrentViews): { 125 | type: string; 126 | currentViews: CurrentViews; 127 | }; 128 | export declare function setAppModuleName(moduleName: string): void; 129 | export declare function delayPromise(second: number): (target: any, propertyKey: string, descriptor: PropertyDescriptor) => void; 130 | export declare function getModuleActionCreatorList(namespace: string): ActionCreatorList; 131 | export interface Model { 132 | namespace: string; 133 | initState: ModuleState; 134 | (store: ModelStore): Promise; 135 | } 136 | export declare function exportModule(namespace: string): { 137 | namespace: string; 138 | actions: T; 139 | }; 140 | export declare function warning(...args: any[]): void; 141 | export {}; 142 | -------------------------------------------------------------------------------- /build/es5/global.js: -------------------------------------------------------------------------------- 1 | export function isModuleState(module) { 2 | return module.isModule; 3 | } 4 | export var LOADING = "LOADING"; 5 | export var ERROR = "@@framework/ERROR"; 6 | export var INIT = "INIT"; 7 | export var LOCATION_CHANGE = "@@router/LOCATION_CHANGE"; 8 | export var VIEW_INVALID = "@@framework/VIEW_INVALID"; 9 | export var NSP = "/"; 10 | export var MetaData = { 11 | isBrowser: typeof window === "object", 12 | isDev: process.env.NODE_ENV !== "production", 13 | actionCreatorMap: {}, 14 | clientStore: null, 15 | appModuleName: null, 16 | }; 17 | export function isPromise(data) { 18 | return typeof data["then"] === "function"; 19 | } 20 | export function errorAction(error) { 21 | return { 22 | type: ERROR, 23 | error: error, 24 | }; 25 | } 26 | export function viewInvalidAction(currentViews) { 27 | return { 28 | type: VIEW_INVALID, 29 | currentViews: currentViews, 30 | }; 31 | } 32 | export function setAppModuleName(moduleName) { 33 | MetaData.appModuleName = moduleName; 34 | } 35 | export function delayPromise(second) { 36 | return function (target, propertyKey, descriptor) { 37 | var fun = descriptor.value; 38 | descriptor.value = function () { 39 | var args = []; 40 | for (var _i = 0; _i < arguments.length; _i++) { 41 | args[_i] = arguments[_i]; 42 | } 43 | var delay = new Promise(function (resolve) { 44 | setTimeout(function () { 45 | resolve(true); 46 | }, second * 1000); 47 | }); 48 | return Promise.all([delay, fun.apply(target, args)]).then(function (items) { 49 | return items[1]; 50 | }); 51 | }; 52 | }; 53 | } 54 | export function getModuleActionCreatorList(namespace) { 55 | if (MetaData.actionCreatorMap[namespace]) { 56 | return MetaData.actionCreatorMap[namespace]; 57 | } 58 | else { 59 | var obj = {}; 60 | MetaData.actionCreatorMap[namespace] = obj; 61 | return obj; 62 | } 63 | } 64 | export function exportModule(namespace) { 65 | var actions = getModuleActionCreatorList(namespace); 66 | return { 67 | namespace: namespace, 68 | actions: actions, 69 | }; 70 | } 71 | export function warning() { 72 | var args = []; 73 | for (var _i = 0; _i < arguments.length; _i++) { 74 | args[_i] = arguments[_i]; 75 | } 76 | if (MetaData.isDev) { 77 | console.warn.apply(console, args); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /build/es5/index.d.ts: -------------------------------------------------------------------------------- 1 | export { RouterState } from "connected-react-router"; 2 | export { Actions, BaseModuleHandlers, effect, logger, reducer } from "./actions"; 3 | export { buildApp, renderApp } from "./Application"; 4 | export { BaseModuleState, CurrentViews, delayPromise, ERROR, errorAction, exportModule, GetModule, INIT, LOCATION_CHANGE, ModelStore, Module, ModuleGetter, ReturnModule, RootState, VIEW_INVALID } from "./global"; 5 | export { LoadingState, setLoading, setLoadingDepthTime } from "./loading"; 6 | export { exportModel } from "./model"; 7 | export { exportView, loadModel, loadView } from "./module"; 8 | export { RouterParser } from "./store"; 9 | -------------------------------------------------------------------------------- /build/es5/index.js: -------------------------------------------------------------------------------- 1 | export { BaseModuleHandlers, effect, logger, reducer } from "./actions"; 2 | export { buildApp, renderApp } from "./Application"; 3 | export { delayPromise, ERROR, errorAction, exportModule, INIT, LOCATION_CHANGE, VIEW_INVALID } from "./global"; 4 | export { LoadingState, setLoading, setLoadingDepthTime } from "./loading"; 5 | export { exportModel } from "./model"; 6 | export { exportView, loadModel, loadView } from "./module"; 7 | -------------------------------------------------------------------------------- /build/es5/loading.d.ts: -------------------------------------------------------------------------------- 1 | export declare function setLoadingDepthTime(second: number): void; 2 | export declare function setLoading>(item: T, namespace?: string, group?: string): T; 3 | export declare enum LoadingState { 4 | Start = "Start", 5 | Stop = "Stop", 6 | Depth = "Depth" 7 | } 8 | -------------------------------------------------------------------------------- /build/es5/loading.js: -------------------------------------------------------------------------------- 1 | import { MetaData, LOADING, getModuleActionCreatorList } from "./global"; 2 | import { TaskCounter, TaskCountEvent } from "./sprite"; 3 | var loadings = {}; 4 | var depthTime = 2; 5 | export function setLoadingDepthTime(second) { 6 | depthTime = second; 7 | } 8 | export function setLoading(item, namespace, group) { 9 | if (namespace === void 0) { namespace = MetaData.appModuleName; } 10 | if (group === void 0) { group = "global"; } 11 | if (!MetaData.isBrowser) { 12 | return item; 13 | } 14 | var key = namespace + "/" + group; 15 | if (!loadings[key]) { 16 | loadings[key] = new TaskCounter(depthTime); 17 | loadings[key].addListener(TaskCountEvent, function (e) { 18 | var _a; 19 | var store = MetaData.clientStore; 20 | if (store) { 21 | var actions = getModuleActionCreatorList(namespace)[LOADING]; 22 | var action = actions((_a = {}, _a[group] = e.data, _a)); 23 | store.dispatch(action); 24 | } 25 | }); 26 | } 27 | loadings[key].addItem(item); 28 | return item; 29 | } 30 | export var LoadingState; 31 | (function (LoadingState) { 32 | LoadingState["Start"] = "Start"; 33 | LoadingState["Stop"] = "Stop"; 34 | LoadingState["Depth"] = "Depth"; 35 | })(LoadingState || (LoadingState = {})); 36 | -------------------------------------------------------------------------------- /build/es5/model.d.ts: -------------------------------------------------------------------------------- 1 | import { BaseModuleState, RootState, Model } from "./global"; 2 | import { BaseModuleHandlers } from "./actions"; 3 | export declare function exportModel(namespace: N, HandlersClass: { 4 | new (initState: S, presetData?: any): BaseModuleHandlers, N>; 5 | }, initState: S): Model; 6 | -------------------------------------------------------------------------------- /build/es5/model.js: -------------------------------------------------------------------------------- 1 | import { isPromise, getModuleActionCreatorList, NSP } from "./global"; 2 | export function exportModel(namespace, HandlersClass, initState) { 3 | var fun = function (store) { 4 | var hasInjected = store.reactCoat.injectedModules[namespace]; 5 | if (!hasInjected) { 6 | store.reactCoat.injectedModules[namespace] = true; 7 | var moduleState = store.getState()[namespace]; 8 | var handlers = new HandlersClass(initState, moduleState); 9 | handlers.namespace = namespace; 10 | handlers.store = store; 11 | var actions = injectActions(store, namespace, handlers); 12 | handlers.actions = actions; 13 | if (!moduleState) { 14 | var initAction = actions.INIT(handlers.initState); 15 | var action = store.dispatch(initAction); 16 | if (isPromise(action)) { 17 | return action; 18 | } 19 | else { 20 | return Promise.resolve(void 0); 21 | } 22 | } 23 | else { 24 | return Promise.resolve(void 0); 25 | } 26 | } 27 | else { 28 | return Promise.resolve(void 0); 29 | } 30 | }; 31 | fun.namespace = namespace; 32 | fun.initState = initState; 33 | return fun; 34 | } 35 | function bindThis(fun, thisObj) { 36 | var newFun = fun.bind(thisObj); 37 | Object.keys(fun).forEach(function (key) { 38 | newFun[key] = fun[key]; 39 | }); 40 | return newFun; 41 | } 42 | function injectActions(store, namespace, handlers) { 43 | for (var actionName in handlers) { 44 | if (typeof handlers[actionName] === "function") { 45 | var handler = handlers[actionName]; 46 | if (handler.__isReducer__ || handler.__isEffect__) { 47 | handler = bindThis(handler, handlers); 48 | var arr = actionName.split(NSP); 49 | if (arr[1]) { 50 | handler.__isHandler__ = true; 51 | transformAction(actionName, handler, namespace, handler.__isEffect__ ? store.reactCoat.effectMap : store.reactCoat.reducerMap); 52 | } 53 | else { 54 | handler.__isHandler__ = false; 55 | transformAction(namespace + NSP + actionName, handler, namespace, handler.__isEffect__ ? store.reactCoat.effectMap : store.reactCoat.reducerMap); 56 | addModuleActionCreatorList(namespace, actionName); 57 | } 58 | } 59 | } 60 | } 61 | return getModuleActionCreatorList(namespace); 62 | } 63 | function transformAction(actionName, action, listenerModule, actionHandlerMap) { 64 | if (!actionHandlerMap[actionName]) { 65 | actionHandlerMap[actionName] = {}; 66 | } 67 | if (actionHandlerMap[actionName][listenerModule]) { 68 | throw new Error("Action duplicate or conflict : " + actionName + "."); 69 | } 70 | actionHandlerMap[actionName][listenerModule] = action; 71 | } 72 | function addModuleActionCreatorList(namespace, actionName) { 73 | var actions = getModuleActionCreatorList(namespace); 74 | if (!actions[actionName]) { 75 | actions[actionName] = function (payload) { return ({ type: namespace + NSP + actionName, payload: payload }); }; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /build/es5/module.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { ComponentType } from "react"; 3 | import { Model, Module, GetModule, ModuleGetter } from "./global"; 4 | export declare function loadModel(getModule: GetModule): Promise; 5 | export declare type ReturnViews any> = T extends () => Promise> ? R : never; 6 | export declare function loadView, V extends ReturnViews, N extends Extract>(moduleGetter: MG, moduleName: M, viewName: N, loadingComponent?: React.ReactNode): V[N]; 7 | export declare function exportView>(ComponentView: C, model: Model, viewName: string): C; 8 | -------------------------------------------------------------------------------- /build/es5/module.js: -------------------------------------------------------------------------------- 1 | import * as tslib_1 from "tslib"; 2 | import * as React from "react"; 3 | import { MetaData } from "./global"; 4 | import { invalidview } from "./store"; 5 | function isPromiseModule(module) { 6 | return typeof module["then"] === "function"; 7 | } 8 | function isPromiseView(moduleView) { 9 | return typeof moduleView["then"] === "function"; 10 | } 11 | function getView(getModule, viewName) { 12 | var result = getModule(); 13 | if (isPromiseModule(result)) { 14 | return result.then(function (module) { return module.views[viewName]; }); 15 | } 16 | else { 17 | return result.views[viewName]; 18 | } 19 | } 20 | export function loadModel(getModule) { 21 | var result = getModule(); 22 | if (isPromiseModule(result)) { 23 | return result.then(function (module) { return module.model; }); 24 | } 25 | else { 26 | return Promise.resolve(result.model); 27 | } 28 | } 29 | export function loadView(moduleGetter, moduleName, viewName, loadingComponent) { 30 | if (loadingComponent === void 0) { loadingComponent = null; } 31 | return (function (_super) { 32 | tslib_1.__extends(Loader, _super); 33 | function Loader() { 34 | var _this = _super !== null && _super.apply(this, arguments) || this; 35 | _this.state = { 36 | Component: null, 37 | }; 38 | return _this; 39 | } 40 | Loader.prototype.shouldComponentUpdate = function (nextProps, nextState) { 41 | return nextState.Component !== this.state.Component; 42 | }; 43 | Loader.prototype.componentWillMount = function () { 44 | var _this = this; 45 | var moduleViewResult = getView(moduleGetter[moduleName], viewName); 46 | if (isPromiseView(moduleViewResult)) { 47 | moduleViewResult.then(function (Component) { 48 | _this.setState({ 49 | Component: Component, 50 | }); 51 | }); 52 | } 53 | else { 54 | this.setState({ 55 | Component: moduleViewResult, 56 | }); 57 | } 58 | }; 59 | Loader.prototype.render = function () { 60 | var Component = this.state.Component; 61 | return Component ? React.createElement(Component, tslib_1.__assign({}, this.props)) : loadingComponent; 62 | }; 63 | return Loader; 64 | }(React.Component)); 65 | } 66 | export function exportView(ComponentView, model, viewName) { 67 | var Comp = ComponentView; 68 | if (MetaData.isBrowser) { 69 | return (function (_super) { 70 | tslib_1.__extends(Component, _super); 71 | function Component(props, context) { 72 | var _this = _super.call(this, props, context) || this; 73 | var state = MetaData.clientStore.getState(); 74 | var namespace = model.namespace; 75 | _this.state = { 76 | modelReady: !!state[namespace], 77 | }; 78 | model(MetaData.clientStore).then(function () { 79 | if (!_this.state.modelReady) { 80 | _this.setState({ modelReady: true }); 81 | } 82 | }); 83 | return _this; 84 | } 85 | Component.prototype.componentWillMount = function () { 86 | var _a; 87 | var currentViews = MetaData.clientStore.reactCoat.currentViews; 88 | if (!currentViews[model.namespace]) { 89 | currentViews[model.namespace] = (_a = {}, _a[viewName] = 1, _a); 90 | } 91 | else { 92 | var views = currentViews[model.namespace]; 93 | if (!views[viewName]) { 94 | views[viewName] = 1; 95 | } 96 | else { 97 | views[viewName]++; 98 | } 99 | } 100 | invalidview(); 101 | }; 102 | Component.prototype.componentWillUnmount = function () { 103 | var currentViews = MetaData.clientStore.reactCoat.currentViews; 104 | if (currentViews[model.namespace] && currentViews[model.namespace][viewName]) { 105 | currentViews[model.namespace][viewName]--; 106 | } 107 | invalidview(); 108 | }; 109 | Component.prototype.render = function () { 110 | return this.state.modelReady ? React.createElement(Comp, tslib_1.__assign({}, this.props)) : null; 111 | }; 112 | return Component; 113 | }(React.PureComponent)); 114 | } 115 | else { 116 | return Comp; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /build/es5/sprite.d.ts: -------------------------------------------------------------------------------- 1 | export declare const TaskCountEvent = "TaskCountEvent"; 2 | export declare type TaskCounterState = "Start" | "Stop" | "Depth"; 3 | export declare class PEvent { 4 | readonly name: string; 5 | readonly data?: any; 6 | bubbling: boolean; 7 | readonly target: PDispatcher; 8 | readonly currentTarget: PDispatcher; 9 | constructor(name: string, data?: any, bubbling?: boolean); 10 | setTarget(target: PDispatcher): void; 11 | setCurrentTarget(target: PDispatcher): void; 12 | } 13 | export declare class PDispatcher { 14 | readonly parent?: PDispatcher | undefined; 15 | protected readonly storeHandlers: { 16 | [key: string]: Array<(e: PEvent) => void>; 17 | }; 18 | constructor(parent?: PDispatcher | undefined); 19 | addListener(ename: string, handler: (e: PEvent) => void): this; 20 | removeListener(ename?: string, handler?: (e: PEvent) => void): this; 21 | dispatch(evt: PEvent): this; 22 | setParent(parent?: PDispatcher): this; 23 | } 24 | export declare class TaskCounter extends PDispatcher { 25 | deferSecond: number; 26 | readonly list: Array<{ 27 | promise: Promise; 28 | note: string; 29 | }>; 30 | private ctimer; 31 | constructor(deferSecond: number); 32 | addItem(promise: Promise, note?: string): Promise; 33 | private completeItem; 34 | } 35 | -------------------------------------------------------------------------------- /build/es5/sprite.js: -------------------------------------------------------------------------------- 1 | import * as tslib_1 from "tslib"; 2 | function emptyObject(obj) { 3 | var arr = []; 4 | for (var key in obj) { 5 | if (obj.hasOwnProperty(key)) { 6 | arr.push(key); 7 | } 8 | } 9 | arr.forEach(function (key) { 10 | delete obj[key]; 11 | }); 12 | return obj; 13 | } 14 | function findIndexInArray(arr, fun) { 15 | for (var i = 0, k = arr.length; i < k; i++) { 16 | if (fun(arr[i])) { 17 | return i; 18 | } 19 | } 20 | return -1; 21 | } 22 | export var TaskCountEvent = "TaskCountEvent"; 23 | var PEvent = (function () { 24 | function PEvent(name, data, bubbling) { 25 | if (bubbling === void 0) { bubbling = false; } 26 | this.name = name; 27 | this.data = data; 28 | this.bubbling = bubbling; 29 | this.target = null; 30 | this.currentTarget = null; 31 | } 32 | PEvent.prototype.setTarget = function (target) { 33 | this.target = target; 34 | }; 35 | PEvent.prototype.setCurrentTarget = function (target) { 36 | this.currentTarget = target; 37 | }; 38 | return PEvent; 39 | }()); 40 | export { PEvent }; 41 | var PDispatcher = (function () { 42 | function PDispatcher(parent) { 43 | this.parent = parent; 44 | this.storeHandlers = {}; 45 | } 46 | PDispatcher.prototype.addListener = function (ename, handler) { 47 | var dictionary = this.storeHandlers[ename]; 48 | if (!dictionary) { 49 | this.storeHandlers[ename] = dictionary = []; 50 | } 51 | dictionary.push(handler); 52 | return this; 53 | }; 54 | PDispatcher.prototype.removeListener = function (ename, handler) { 55 | if (!ename) { 56 | emptyObject(this.storeHandlers); 57 | } 58 | else { 59 | var handlers = this.storeHandlers; 60 | if (handlers.propertyIsEnumerable(ename)) { 61 | var dictionary = handlers[ename]; 62 | if (!handler) { 63 | delete handlers[ename]; 64 | } 65 | else { 66 | var n = dictionary.indexOf(handler); 67 | if (n > -1) { 68 | dictionary.splice(n, 1); 69 | } 70 | if (dictionary.length === 0) { 71 | delete handlers[ename]; 72 | } 73 | } 74 | } 75 | } 76 | return this; 77 | }; 78 | PDispatcher.prototype.dispatch = function (evt) { 79 | if (!evt.target) { 80 | evt.setTarget(this); 81 | } 82 | evt.setCurrentTarget(this); 83 | var dictionary = this.storeHandlers[evt.name]; 84 | if (dictionary) { 85 | for (var i = 0, k = dictionary.length; i < k; i++) { 86 | dictionary[i](evt); 87 | } 88 | } 89 | if (this.parent && evt.bubbling) { 90 | this.parent.dispatch(evt); 91 | } 92 | return this; 93 | }; 94 | PDispatcher.prototype.setParent = function (parent) { 95 | this.parent = parent; 96 | return this; 97 | }; 98 | return PDispatcher; 99 | }()); 100 | export { PDispatcher }; 101 | var TaskCounter = (function (_super) { 102 | tslib_1.__extends(TaskCounter, _super); 103 | function TaskCounter(deferSecond) { 104 | var _this = _super.call(this) || this; 105 | _this.deferSecond = deferSecond; 106 | _this.list = []; 107 | _this.ctimer = 0; 108 | return _this; 109 | } 110 | TaskCounter.prototype.addItem = function (promise, note) { 111 | var _this = this; 112 | if (note === void 0) { note = ""; } 113 | if (!this.list.some(function (item) { return item.promise === promise; })) { 114 | this.list.push({ promise: promise, note: note }); 115 | promise.then(function (resolve) { return _this.completeItem(promise); }, function (reject) { return _this.completeItem(promise); }); 116 | if (this.list.length === 1) { 117 | this.dispatch(new PEvent(TaskCountEvent, "Start")); 118 | this.ctimer = window.setTimeout(function () { 119 | _this.ctimer = 0; 120 | if (_this.list.length > 0) { 121 | _this.dispatch(new PEvent(TaskCountEvent, "Depth")); 122 | } 123 | }, this.deferSecond * 1000); 124 | } 125 | } 126 | return promise; 127 | }; 128 | TaskCounter.prototype.completeItem = function (promise) { 129 | var i = findIndexInArray(this.list, function (item) { return item.promise === promise; }); 130 | if (i > -1) { 131 | this.list.splice(i, 1); 132 | if (this.list.length === 0) { 133 | if (this.ctimer) { 134 | clearTimeout(this.ctimer); 135 | this.ctimer = 0; 136 | } 137 | this.dispatch(new PEvent(TaskCountEvent, "Stop")); 138 | } 139 | } 140 | return this; 141 | }; 142 | return TaskCounter; 143 | }(PDispatcher)); 144 | export { TaskCounter }; 145 | -------------------------------------------------------------------------------- /build/es5/store.d.ts: -------------------------------------------------------------------------------- 1 | import { History } from "history"; 2 | import { Middleware, ReducersMapObject, StoreEnhancer } from "redux"; 3 | import { ModelStore } from "./global"; 4 | export declare function invalidview(): void; 5 | export declare type RouterParser = (nextRouter: T, prevRouter?: T) => T; 6 | export declare function buildStore(storeHistory: History, reducersMapObject?: ReducersMapObject, storeMiddlewares?: Middleware[], storeEnhancers?: StoreEnhancer[], initData?: any, routerParser?: RouterParser): ModelStore; 7 | -------------------------------------------------------------------------------- /build/es6/Application.d.ts: -------------------------------------------------------------------------------- 1 | import { ReactElement } from "react"; 2 | import { Middleware, ReducersMapObject, StoreEnhancer, Store } from "redux"; 3 | import { ModuleGetter } from "./global"; 4 | import { RouterParser } from "./store"; 5 | export interface StoreOptions { 6 | reducers?: ReducersMapObject; 7 | middlewares?: Middleware[]; 8 | enhancers?: StoreEnhancer[]; 9 | routerParser?: RouterParser; 10 | initData?: any; 11 | } 12 | export declare function buildApp>(moduleGetter: M, appName: A, storeOptions?: StoreOptions, container?: string | Element | ((component: ReactElement) => void), ssrInitStoreKey?: string): Store; 13 | export declare function renderApp>(moduleGetter: M, appName: A, initialEntries: string[], storeOptions?: StoreOptions, ssrInitStoreKey?: string, renderToStream?: boolean): Promise<{ 14 | html: string | NodeJS.ReadableStream; 15 | data: any; 16 | ssrInitStoreKey: string; 17 | }>; 18 | -------------------------------------------------------------------------------- /build/es6/Application.js: -------------------------------------------------------------------------------- 1 | import { ConnectedRouter } from "connected-react-router"; 2 | import createBrowserHistory from "history/createBrowserHistory"; 3 | import createMemoryHistory from "history/createMemoryHistory"; 4 | import * as React from "react"; 5 | import * as ReactDOM from "react-dom"; 6 | import { renderToNodeStream, renderToString } from "react-dom/server"; 7 | import { Provider } from "react-redux"; 8 | import { withRouter } from "react-router-dom"; 9 | import { errorAction, MetaData } from "./global"; 10 | import { buildStore } from "./store"; 11 | function isPromiseModule(module) { 12 | return typeof module["then"] === "function"; 13 | } 14 | function getModuleByName(moduleName, moduleGetter) { 15 | if (moduleName === "router") { 16 | throw new Error("router is a system module"); 17 | } 18 | const result = moduleGetter[moduleName](); 19 | if (isPromiseModule(result)) { 20 | return result.then(module => { 21 | moduleGetter[moduleName] = () => module; 22 | return module; 23 | }); 24 | } 25 | else { 26 | return result; 27 | } 28 | } 29 | function getModuleListByNames(moduleNames, moduleGetter) { 30 | const preModules = moduleNames.map(moduleName => { 31 | const module = getModuleByName(moduleName, moduleGetter); 32 | if (isPromiseModule(module)) { 33 | return module; 34 | } 35 | else { 36 | return Promise.resolve(module); 37 | } 38 | }); 39 | return Promise.all(preModules); 40 | } 41 | export function buildApp(moduleGetter, appName, storeOptions = {}, container = "root", ssrInitStoreKey = "reactCoatInitStore") { 42 | MetaData.appModuleName = appName; 43 | const history = createBrowserHistory(); 44 | let initData = {}; 45 | if (storeOptions.initData || window[ssrInitStoreKey]) { 46 | initData = Object.assign({}, window[ssrInitStoreKey], storeOptions.initData); 47 | } 48 | const store = buildStore(history, storeOptions.reducers, storeOptions.middlewares, storeOptions.enhancers, initData, storeOptions.routerParser); 49 | const preModuleNames = [appName]; 50 | if (initData) { 51 | preModuleNames.push(...Object.keys(initData).filter(key => key !== appName && initData[key].isModule)); 52 | } 53 | getModuleListByNames(preModuleNames, moduleGetter).then(([appModel]) => { 54 | appModel.model(store); 55 | const WithRouter = withRouter(appModel.views.Main); 56 | const app = (React.createElement(Provider, { store: store }, 57 | React.createElement(ConnectedRouter, { history: history }, 58 | React.createElement(WithRouter, null)))); 59 | if (typeof container === "function") { 60 | container(app); 61 | } 62 | else { 63 | const render = window[ssrInitStoreKey] ? ReactDOM.hydrate : ReactDOM.render; 64 | render(app, typeof container === "string" ? document.getElementById(container) : container); 65 | } 66 | }); 67 | return store; 68 | } 69 | export function renderApp(moduleGetter, appName, initialEntries, storeOptions = {}, ssrInitStoreKey = "reactCoatInitStore", renderToStream = false) { 70 | MetaData.appModuleName = appName; 71 | const history = createMemoryHistory({ initialEntries }); 72 | const store = buildStore(history, storeOptions.reducers, storeOptions.middlewares, storeOptions.enhancers, storeOptions.initData, storeOptions.routerParser); 73 | const appModule = moduleGetter[appName](); 74 | const render = renderToStream ? renderToNodeStream : renderToString; 75 | return appModule 76 | .model(store) 77 | .catch(err => { 78 | return store.dispatch(errorAction(err)); 79 | }) 80 | .then(() => { 81 | const data = store.getState(); 82 | return { 83 | ssrInitStoreKey, 84 | data, 85 | html: render(React.createElement(Provider, { store: store }, 86 | React.createElement(ConnectedRouter, { history: history }, 87 | React.createElement(appModule.views.Main, null)))), 88 | }; 89 | }); 90 | } 91 | -------------------------------------------------------------------------------- /build/es6/actions.d.ts: -------------------------------------------------------------------------------- 1 | import { routerActions } from "connected-react-router"; 2 | import { Action, BaseModuleState, ModelStore, RootState } from "./global"; 3 | export declare class BaseModuleHandlers, N extends string> { 4 | protected readonly initState: S; 5 | protected readonly namespace: N; 6 | protected readonly store: ModelStore; 7 | protected readonly actions: Actions; 8 | protected readonly routerActions: typeof routerActions; 9 | constructor(initState: S, presetData?: any); 10 | protected readonly state: S; 11 | protected readonly rootState: R; 12 | protected readonly currentState: S; 13 | protected readonly currentRootState: R; 14 | protected dispatch(action: Action): Action | Promise; 15 | protected callThisAction(handler: (...args: T) => any, ...rest: T): { 16 | type: string; 17 | playload?: any; 18 | }; 19 | protected INIT(payload: S): S; 20 | protected UPDATE(payload: S): S; 21 | protected LOADING(payload: { 22 | [group: string]: string; 23 | }): S; 24 | protected updateState(payload: Partial): void; 25 | } 26 | export declare function logger(before: (action: Action, moduleName: string, promiseResult: Promise) => void, after: null | ((status: "Rejected" | "Resolved", beforeResult: any, effectResult: any) => void)): (target: any, key: string, descriptor: PropertyDescriptor) => void; 27 | export declare function effect(loadingForGroupName?: string | null, loadingForModuleName?: string): (target: any, key: string, descriptor: PropertyDescriptor) => PropertyDescriptor; 28 | export declare function reducer(target: any, key: string, descriptor: PropertyDescriptor): PropertyDescriptor; 29 | declare type Handler = F extends (...args: infer P) => any ? (...args: P) => { 30 | type: string; 31 | } : never; 32 | export declare type Actions = { 33 | [K in keyof Ins]: Ins[K] extends (...args: any[]) => any ? Handler : never; 34 | }; 35 | export {}; 36 | -------------------------------------------------------------------------------- /build/es6/actions.js: -------------------------------------------------------------------------------- 1 | import * as tslib_1 from "tslib"; 2 | import { routerActions } from "connected-react-router"; 3 | import { getModuleActionCreatorList, MetaData } from "./global"; 4 | import { setLoading } from "./loading"; 5 | export class BaseModuleHandlers { 6 | constructor(initState, presetData) { 7 | this.namespace = ""; 8 | this.store = null; 9 | this.actions = null; 10 | this.routerActions = routerActions; 11 | initState.isModule = true; 12 | this.initState = initState; 13 | } 14 | get state() { 15 | return this.store.reactCoat.prevState[this.namespace]; 16 | } 17 | get rootState() { 18 | return this.store.reactCoat.prevState; 19 | } 20 | get currentState() { 21 | return this.store.reactCoat.currentState[this.namespace]; 22 | } 23 | get currentRootState() { 24 | return this.store.reactCoat.currentState; 25 | } 26 | dispatch(action) { 27 | return this.store.dispatch(action); 28 | } 29 | callThisAction(handler, ...rest) { 30 | const actions = getModuleActionCreatorList(this.namespace); 31 | return actions[handler.__actionName__](rest[0]); 32 | } 33 | INIT(payload) { 34 | return payload; 35 | } 36 | UPDATE(payload) { 37 | return payload; 38 | } 39 | LOADING(payload) { 40 | const state = this.state; 41 | if (!state) { 42 | return state; 43 | } 44 | return Object.assign({}, state, { loading: Object.assign({}, state.loading, payload) }); 45 | } 46 | updateState(payload) { 47 | this.dispatch(this.callThisAction(this.UPDATE, Object.assign({}, this.state, payload))); 48 | } 49 | } 50 | tslib_1.__decorate([ 51 | reducer 52 | ], BaseModuleHandlers.prototype, "INIT", null); 53 | tslib_1.__decorate([ 54 | reducer 55 | ], BaseModuleHandlers.prototype, "UPDATE", null); 56 | tslib_1.__decorate([ 57 | reducer 58 | ], BaseModuleHandlers.prototype, "LOADING", null); 59 | export function logger(before, after) { 60 | return (target, key, descriptor) => { 61 | const fun = descriptor.value; 62 | if (!fun.__decorators__) { 63 | fun.__decorators__ = []; 64 | } 65 | fun.__decorators__.push([before, after]); 66 | }; 67 | } 68 | export function effect(loadingForGroupName, loadingForModuleName) { 69 | if (loadingForGroupName === undefined) { 70 | loadingForGroupName = "global"; 71 | loadingForModuleName = MetaData.appModuleName; 72 | } 73 | return (target, key, descriptor) => { 74 | const fun = descriptor.value; 75 | fun.__actionName__ = key; 76 | fun.__isEffect__ = true; 77 | descriptor.enumerable = true; 78 | if (loadingForGroupName) { 79 | const before = (curAction, moduleName, promiseResult) => { 80 | if (MetaData.isBrowser) { 81 | if (!loadingForModuleName) { 82 | loadingForModuleName = moduleName; 83 | } 84 | setLoading(promiseResult, loadingForModuleName, loadingForGroupName); 85 | } 86 | }; 87 | if (!fun.__decorators__) { 88 | fun.__decorators__ = []; 89 | } 90 | fun.__decorators__.push([before, null]); 91 | } 92 | return descriptor; 93 | }; 94 | } 95 | export function reducer(target, key, descriptor) { 96 | const fun = descriptor.value; 97 | fun.__actionName__ = key; 98 | fun.__isReducer__ = true; 99 | descriptor.enumerable = true; 100 | return descriptor; 101 | } 102 | -------------------------------------------------------------------------------- /build/es6/global.d.ts: -------------------------------------------------------------------------------- 1 | import { RouterState } from "connected-react-router"; 2 | import { ComponentType } from "react"; 3 | import { History } from "history"; 4 | import { Store } from "redux"; 5 | import { LoadingState } from "./loading"; 6 | export interface ModelStore extends Store { 7 | reactCoat: { 8 | history: History; 9 | prevState: { 10 | [key: string]: any; 11 | }; 12 | currentState: { 13 | [key: string]: any; 14 | }; 15 | reducerMap: ReducerMap; 16 | effectMap: EffectMap; 17 | injectedModules: { 18 | [namespace: string]: boolean; 19 | }; 20 | routerInited: boolean; 21 | currentViews: CurrentViews; 22 | }; 23 | } 24 | export interface CurrentViews { 25 | [moduleName: string]: { 26 | [viewName: string]: number; 27 | }; 28 | } 29 | export interface BaseModuleState { 30 | isModule?: boolean; 31 | loading?: { 32 | [key: string]: LoadingState; 33 | }; 34 | } 35 | export declare function isModuleState(module: any): module is BaseModuleState; 36 | export declare type GetModule = () => M | Promise; 37 | export interface ModuleGetter { 38 | [moduleName: string]: GetModule; 39 | } 40 | export declare type ReturnModule any> = T extends () => Promise ? R : T extends () => infer R ? R : Module; 41 | declare type ModuleStates = M["model"]["initState"]; 42 | declare type ModuleViews = { 43 | [key in keyof M["views"]]?: number; 44 | }; 45 | export declare type RootState = { 46 | router: R; 47 | views: { 48 | [key in keyof G]?: ModuleViews>; 49 | }; 50 | } & { 51 | [key in keyof G]?: ModuleStates>; 52 | }; 53 | export interface Action { 54 | type: string; 55 | priority?: string[]; 56 | payload?: any; 57 | } 58 | export interface ActionCreatorMap { 59 | [moduleName: string]: ActionCreatorList; 60 | } 61 | export interface ActionCreatorList { 62 | [actionName: string]: ActionCreator; 63 | } 64 | export declare type ActionCreator = (payload?: any) => Action; 65 | export interface ActionHandler { 66 | __actionName__: string; 67 | __isReducer__?: boolean; 68 | __isEffect__?: boolean; 69 | __isHandler__?: boolean; 70 | __decorators__?: Array<[(action: Action, moduleName: string, effectResult: Promise) => any, null | ((status: "Rejected" | "Resolved", beforeResult: any, effectResult: any) => void)]>; 71 | __decoratorResults__?: any[]; 72 | (payload?: any): any; 73 | } 74 | export interface ReducerHandler extends ActionHandler { 75 | (payload?: any): BaseModuleState; 76 | } 77 | export interface EffectHandler extends ActionHandler { 78 | (payload?: any): Promise; 79 | } 80 | export interface ActionHandlerList { 81 | [actionName: string]: ActionHandler; 82 | } 83 | export interface ActionHandlerMap { 84 | [actionName: string]: { 85 | [moduleName: string]: ActionHandler; 86 | }; 87 | } 88 | export interface ReducerMap extends ActionHandlerMap { 89 | [actionName: string]: { 90 | [moduleName: string]: ReducerHandler; 91 | }; 92 | } 93 | export interface EffectMap extends ActionHandlerMap { 94 | [actionName: string]: { 95 | [moduleName: string]: EffectHandler; 96 | }; 97 | } 98 | export declare const LOADING = "LOADING"; 99 | export declare const ERROR = "@@framework/ERROR"; 100 | export declare const INIT = "INIT"; 101 | export declare const LOCATION_CHANGE = "@@router/LOCATION_CHANGE"; 102 | export declare const VIEW_INVALID = "@@framework/VIEW_INVALID"; 103 | export declare const NSP = "/"; 104 | export declare const MetaData: { 105 | isBrowser: boolean; 106 | isDev: boolean; 107 | actionCreatorMap: ActionCreatorMap; 108 | clientStore: ModelStore; 109 | appModuleName: string; 110 | }; 111 | export declare function isPromise(data: any): data is Promise; 112 | export interface Module; 114 | } = { 115 | [key: string]: ComponentType; 116 | }> { 117 | model: M; 118 | views: VS; 119 | } 120 | export declare function errorAction(error: any): { 121 | type: string; 122 | error: any; 123 | }; 124 | export declare function viewInvalidAction(currentViews: CurrentViews): { 125 | type: string; 126 | currentViews: CurrentViews; 127 | }; 128 | export declare function setAppModuleName(moduleName: string): void; 129 | export declare function delayPromise(second: number): (target: any, propertyKey: string, descriptor: PropertyDescriptor) => void; 130 | export declare function getModuleActionCreatorList(namespace: string): ActionCreatorList; 131 | export interface Model { 132 | namespace: string; 133 | initState: ModuleState; 134 | (store: ModelStore): Promise; 135 | } 136 | export declare function exportModule(namespace: string): { 137 | namespace: string; 138 | actions: T; 139 | }; 140 | export declare function warning(...args: any[]): void; 141 | export {}; 142 | -------------------------------------------------------------------------------- /build/es6/global.js: -------------------------------------------------------------------------------- 1 | export function isModuleState(module) { 2 | return module.isModule; 3 | } 4 | export const LOADING = "LOADING"; 5 | export const ERROR = "@@framework/ERROR"; 6 | export const INIT = "INIT"; 7 | export const LOCATION_CHANGE = "@@router/LOCATION_CHANGE"; 8 | export const VIEW_INVALID = "@@framework/VIEW_INVALID"; 9 | export const NSP = "/"; 10 | export const MetaData = { 11 | isBrowser: typeof window === "object", 12 | isDev: process.env.NODE_ENV !== "production", 13 | actionCreatorMap: {}, 14 | clientStore: null, 15 | appModuleName: null, 16 | }; 17 | export function isPromise(data) { 18 | return typeof data["then"] === "function"; 19 | } 20 | export function errorAction(error) { 21 | return { 22 | type: ERROR, 23 | error, 24 | }; 25 | } 26 | export function viewInvalidAction(currentViews) { 27 | return { 28 | type: VIEW_INVALID, 29 | currentViews, 30 | }; 31 | } 32 | export function setAppModuleName(moduleName) { 33 | MetaData.appModuleName = moduleName; 34 | } 35 | export function delayPromise(second) { 36 | return (target, propertyKey, descriptor) => { 37 | const fun = descriptor.value; 38 | descriptor.value = (...args) => { 39 | const delay = new Promise(resolve => { 40 | setTimeout(() => { 41 | resolve(true); 42 | }, second * 1000); 43 | }); 44 | return Promise.all([delay, fun.apply(target, args)]).then(items => { 45 | return items[1]; 46 | }); 47 | }; 48 | }; 49 | } 50 | export function getModuleActionCreatorList(namespace) { 51 | if (MetaData.actionCreatorMap[namespace]) { 52 | return MetaData.actionCreatorMap[namespace]; 53 | } 54 | else { 55 | const obj = {}; 56 | MetaData.actionCreatorMap[namespace] = obj; 57 | return obj; 58 | } 59 | } 60 | export function exportModule(namespace) { 61 | const actions = getModuleActionCreatorList(namespace); 62 | return { 63 | namespace, 64 | actions, 65 | }; 66 | } 67 | export function warning(...args) { 68 | if (MetaData.isDev) { 69 | console.warn(...args); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /build/es6/index.d.ts: -------------------------------------------------------------------------------- 1 | export { RouterState } from "connected-react-router"; 2 | export { Actions, BaseModuleHandlers, effect, logger, reducer } from "./actions"; 3 | export { buildApp, renderApp } from "./Application"; 4 | export { BaseModuleState, CurrentViews, delayPromise, ERROR, errorAction, exportModule, GetModule, INIT, LOCATION_CHANGE, ModelStore, Module, ModuleGetter, ReturnModule, RootState, VIEW_INVALID } from "./global"; 5 | export { LoadingState, setLoading, setLoadingDepthTime } from "./loading"; 6 | export { exportModel } from "./model"; 7 | export { exportView, loadModel, loadView } from "./module"; 8 | export { RouterParser } from "./store"; 9 | -------------------------------------------------------------------------------- /build/es6/index.js: -------------------------------------------------------------------------------- 1 | export { BaseModuleHandlers, effect, logger, reducer } from "./actions"; 2 | export { buildApp, renderApp } from "./Application"; 3 | export { delayPromise, ERROR, errorAction, exportModule, INIT, LOCATION_CHANGE, VIEW_INVALID } from "./global"; 4 | export { LoadingState, setLoading, setLoadingDepthTime } from "./loading"; 5 | export { exportModel } from "./model"; 6 | export { exportView, loadModel, loadView } from "./module"; 7 | -------------------------------------------------------------------------------- /build/es6/loading.d.ts: -------------------------------------------------------------------------------- 1 | export declare function setLoadingDepthTime(second: number): void; 2 | export declare function setLoading>(item: T, namespace?: string, group?: string): T; 3 | export declare enum LoadingState { 4 | Start = "Start", 5 | Stop = "Stop", 6 | Depth = "Depth" 7 | } 8 | -------------------------------------------------------------------------------- /build/es6/loading.js: -------------------------------------------------------------------------------- 1 | import { MetaData, LOADING, getModuleActionCreatorList } from "./global"; 2 | import { TaskCounter, TaskCountEvent } from "./sprite"; 3 | const loadings = {}; 4 | let depthTime = 2; 5 | export function setLoadingDepthTime(second) { 6 | depthTime = second; 7 | } 8 | export function setLoading(item, namespace = MetaData.appModuleName, group = "global") { 9 | if (!MetaData.isBrowser) { 10 | return item; 11 | } 12 | const key = namespace + "/" + group; 13 | if (!loadings[key]) { 14 | loadings[key] = new TaskCounter(depthTime); 15 | loadings[key].addListener(TaskCountEvent, e => { 16 | const store = MetaData.clientStore; 17 | if (store) { 18 | const actions = getModuleActionCreatorList(namespace)[LOADING]; 19 | const action = actions({ [group]: e.data }); 20 | store.dispatch(action); 21 | } 22 | }); 23 | } 24 | loadings[key].addItem(item); 25 | return item; 26 | } 27 | export var LoadingState; 28 | (function (LoadingState) { 29 | LoadingState["Start"] = "Start"; 30 | LoadingState["Stop"] = "Stop"; 31 | LoadingState["Depth"] = "Depth"; 32 | })(LoadingState || (LoadingState = {})); 33 | -------------------------------------------------------------------------------- /build/es6/model.d.ts: -------------------------------------------------------------------------------- 1 | import { BaseModuleState, RootState, Model } from "./global"; 2 | import { BaseModuleHandlers } from "./actions"; 3 | export declare function exportModel(namespace: N, HandlersClass: { 4 | new (initState: S, presetData?: any): BaseModuleHandlers, N>; 5 | }, initState: S): Model; 6 | -------------------------------------------------------------------------------- /build/es6/model.js: -------------------------------------------------------------------------------- 1 | import { isPromise, getModuleActionCreatorList, NSP } from "./global"; 2 | export function exportModel(namespace, HandlersClass, initState) { 3 | const fun = (store) => { 4 | const hasInjected = store.reactCoat.injectedModules[namespace]; 5 | if (!hasInjected) { 6 | store.reactCoat.injectedModules[namespace] = true; 7 | const moduleState = store.getState()[namespace]; 8 | const handlers = new HandlersClass(initState, moduleState); 9 | handlers.namespace = namespace; 10 | handlers.store = store; 11 | const actions = injectActions(store, namespace, handlers); 12 | handlers.actions = actions; 13 | if (!moduleState) { 14 | const initAction = actions.INIT(handlers.initState); 15 | const action = store.dispatch(initAction); 16 | if (isPromise(action)) { 17 | return action; 18 | } 19 | else { 20 | return Promise.resolve(void 0); 21 | } 22 | } 23 | else { 24 | return Promise.resolve(void 0); 25 | } 26 | } 27 | else { 28 | return Promise.resolve(void 0); 29 | } 30 | }; 31 | fun.namespace = namespace; 32 | fun.initState = initState; 33 | return fun; 34 | } 35 | function bindThis(fun, thisObj) { 36 | const newFun = fun.bind(thisObj); 37 | Object.keys(fun).forEach(key => { 38 | newFun[key] = fun[key]; 39 | }); 40 | return newFun; 41 | } 42 | function injectActions(store, namespace, handlers) { 43 | for (const actionName in handlers) { 44 | if (typeof handlers[actionName] === "function") { 45 | let handler = handlers[actionName]; 46 | if (handler.__isReducer__ || handler.__isEffect__) { 47 | handler = bindThis(handler, handlers); 48 | const arr = actionName.split(NSP); 49 | if (arr[1]) { 50 | handler.__isHandler__ = true; 51 | transformAction(actionName, handler, namespace, handler.__isEffect__ ? store.reactCoat.effectMap : store.reactCoat.reducerMap); 52 | } 53 | else { 54 | handler.__isHandler__ = false; 55 | transformAction(namespace + NSP + actionName, handler, namespace, handler.__isEffect__ ? store.reactCoat.effectMap : store.reactCoat.reducerMap); 56 | addModuleActionCreatorList(namespace, actionName); 57 | } 58 | } 59 | } 60 | } 61 | return getModuleActionCreatorList(namespace); 62 | } 63 | function transformAction(actionName, action, listenerModule, actionHandlerMap) { 64 | if (!actionHandlerMap[actionName]) { 65 | actionHandlerMap[actionName] = {}; 66 | } 67 | if (actionHandlerMap[actionName][listenerModule]) { 68 | throw new Error(`Action duplicate or conflict : ${actionName}.`); 69 | } 70 | actionHandlerMap[actionName][listenerModule] = action; 71 | } 72 | function addModuleActionCreatorList(namespace, actionName) { 73 | const actions = getModuleActionCreatorList(namespace); 74 | if (!actions[actionName]) { 75 | actions[actionName] = payload => ({ type: namespace + NSP + actionName, payload }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /build/es6/module.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { ComponentType } from "react"; 3 | import { Model, Module, GetModule, ModuleGetter } from "./global"; 4 | export declare function loadModel(getModule: GetModule): Promise; 5 | export declare type ReturnViews any> = T extends () => Promise> ? R : never; 6 | export declare function loadView, V extends ReturnViews, N extends Extract>(moduleGetter: MG, moduleName: M, viewName: N, loadingComponent?: React.ReactNode): V[N]; 7 | export declare function exportView>(ComponentView: C, model: Model, viewName: string): C; 8 | -------------------------------------------------------------------------------- /build/es6/module.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { MetaData } from "./global"; 3 | import { invalidview } from "./store"; 4 | function isPromiseModule(module) { 5 | return typeof module["then"] === "function"; 6 | } 7 | function isPromiseView(moduleView) { 8 | return typeof moduleView["then"] === "function"; 9 | } 10 | function getView(getModule, viewName) { 11 | const result = getModule(); 12 | if (isPromiseModule(result)) { 13 | return result.then(module => module.views[viewName]); 14 | } 15 | else { 16 | return result.views[viewName]; 17 | } 18 | } 19 | export function loadModel(getModule) { 20 | const result = getModule(); 21 | if (isPromiseModule(result)) { 22 | return result.then(module => module.model); 23 | } 24 | else { 25 | return Promise.resolve(result.model); 26 | } 27 | } 28 | export function loadView(moduleGetter, moduleName, viewName, loadingComponent = null) { 29 | return class Loader extends React.Component { 30 | constructor() { 31 | super(...arguments); 32 | this.state = { 33 | Component: null, 34 | }; 35 | } 36 | shouldComponentUpdate(nextProps, nextState) { 37 | return nextState.Component !== this.state.Component; 38 | } 39 | componentWillMount() { 40 | const moduleViewResult = getView(moduleGetter[moduleName], viewName); 41 | if (isPromiseView(moduleViewResult)) { 42 | moduleViewResult.then(Component => { 43 | this.setState({ 44 | Component, 45 | }); 46 | }); 47 | } 48 | else { 49 | this.setState({ 50 | Component: moduleViewResult, 51 | }); 52 | } 53 | } 54 | render() { 55 | const { Component } = this.state; 56 | return Component ? React.createElement(Component, Object.assign({}, this.props)) : loadingComponent; 57 | } 58 | }; 59 | } 60 | export function exportView(ComponentView, model, viewName) { 61 | const Comp = ComponentView; 62 | if (MetaData.isBrowser) { 63 | return class Component extends React.PureComponent { 64 | constructor(props, context) { 65 | super(props, context); 66 | const state = MetaData.clientStore.getState(); 67 | const namespace = model.namespace; 68 | this.state = { 69 | modelReady: !!state[namespace], 70 | }; 71 | model(MetaData.clientStore).then(() => { 72 | if (!this.state.modelReady) { 73 | this.setState({ modelReady: true }); 74 | } 75 | }); 76 | } 77 | componentWillMount() { 78 | const currentViews = MetaData.clientStore.reactCoat.currentViews; 79 | if (!currentViews[model.namespace]) { 80 | currentViews[model.namespace] = { [viewName]: 1 }; 81 | } 82 | else { 83 | const views = currentViews[model.namespace]; 84 | if (!views[viewName]) { 85 | views[viewName] = 1; 86 | } 87 | else { 88 | views[viewName]++; 89 | } 90 | } 91 | invalidview(); 92 | } 93 | componentWillUnmount() { 94 | const currentViews = MetaData.clientStore.reactCoat.currentViews; 95 | if (currentViews[model.namespace] && currentViews[model.namespace][viewName]) { 96 | currentViews[model.namespace][viewName]--; 97 | } 98 | invalidview(); 99 | } 100 | render() { 101 | return this.state.modelReady ? React.createElement(Comp, Object.assign({}, this.props)) : null; 102 | } 103 | }; 104 | } 105 | else { 106 | return Comp; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /build/es6/sprite.d.ts: -------------------------------------------------------------------------------- 1 | export declare const TaskCountEvent = "TaskCountEvent"; 2 | export declare type TaskCounterState = "Start" | "Stop" | "Depth"; 3 | export declare class PEvent { 4 | readonly name: string; 5 | readonly data?: any; 6 | bubbling: boolean; 7 | readonly target: PDispatcher; 8 | readonly currentTarget: PDispatcher; 9 | constructor(name: string, data?: any, bubbling?: boolean); 10 | setTarget(target: PDispatcher): void; 11 | setCurrentTarget(target: PDispatcher): void; 12 | } 13 | export declare class PDispatcher { 14 | readonly parent?: PDispatcher | undefined; 15 | protected readonly storeHandlers: { 16 | [key: string]: Array<(e: PEvent) => void>; 17 | }; 18 | constructor(parent?: PDispatcher | undefined); 19 | addListener(ename: string, handler: (e: PEvent) => void): this; 20 | removeListener(ename?: string, handler?: (e: PEvent) => void): this; 21 | dispatch(evt: PEvent): this; 22 | setParent(parent?: PDispatcher): this; 23 | } 24 | export declare class TaskCounter extends PDispatcher { 25 | deferSecond: number; 26 | readonly list: Array<{ 27 | promise: Promise; 28 | note: string; 29 | }>; 30 | private ctimer; 31 | constructor(deferSecond: number); 32 | addItem(promise: Promise, note?: string): Promise; 33 | private completeItem; 34 | } 35 | -------------------------------------------------------------------------------- /build/es6/sprite.js: -------------------------------------------------------------------------------- 1 | function emptyObject(obj) { 2 | const arr = []; 3 | for (const key in obj) { 4 | if (obj.hasOwnProperty(key)) { 5 | arr.push(key); 6 | } 7 | } 8 | arr.forEach((key) => { 9 | delete obj[key]; 10 | }); 11 | return obj; 12 | } 13 | function findIndexInArray(arr, fun) { 14 | for (let i = 0, k = arr.length; i < k; i++) { 15 | if (fun(arr[i])) { 16 | return i; 17 | } 18 | } 19 | return -1; 20 | } 21 | export const TaskCountEvent = "TaskCountEvent"; 22 | export class PEvent { 23 | constructor(name, data, bubbling = false) { 24 | this.name = name; 25 | this.data = data; 26 | this.bubbling = bubbling; 27 | this.target = null; 28 | this.currentTarget = null; 29 | } 30 | setTarget(target) { 31 | this.target = target; 32 | } 33 | setCurrentTarget(target) { 34 | this.currentTarget = target; 35 | } 36 | } 37 | export class PDispatcher { 38 | constructor(parent) { 39 | this.parent = parent; 40 | this.storeHandlers = {}; 41 | } 42 | addListener(ename, handler) { 43 | let dictionary = this.storeHandlers[ename]; 44 | if (!dictionary) { 45 | this.storeHandlers[ename] = dictionary = []; 46 | } 47 | dictionary.push(handler); 48 | return this; 49 | } 50 | removeListener(ename, handler) { 51 | if (!ename) { 52 | emptyObject(this.storeHandlers); 53 | } 54 | else { 55 | const handlers = this.storeHandlers; 56 | if (handlers.propertyIsEnumerable(ename)) { 57 | const dictionary = handlers[ename]; 58 | if (!handler) { 59 | delete handlers[ename]; 60 | } 61 | else { 62 | const n = dictionary.indexOf(handler); 63 | if (n > -1) { 64 | dictionary.splice(n, 1); 65 | } 66 | if (dictionary.length === 0) { 67 | delete handlers[ename]; 68 | } 69 | } 70 | } 71 | } 72 | return this; 73 | } 74 | dispatch(evt) { 75 | if (!evt.target) { 76 | evt.setTarget(this); 77 | } 78 | evt.setCurrentTarget(this); 79 | const dictionary = this.storeHandlers[evt.name]; 80 | if (dictionary) { 81 | for (let i = 0, k = dictionary.length; i < k; i++) { 82 | dictionary[i](evt); 83 | } 84 | } 85 | if (this.parent && evt.bubbling) { 86 | this.parent.dispatch(evt); 87 | } 88 | return this; 89 | } 90 | setParent(parent) { 91 | this.parent = parent; 92 | return this; 93 | } 94 | } 95 | export class TaskCounter extends PDispatcher { 96 | constructor(deferSecond) { 97 | super(); 98 | this.deferSecond = deferSecond; 99 | this.list = []; 100 | this.ctimer = 0; 101 | } 102 | addItem(promise, note = "") { 103 | if (!this.list.some(item => item.promise === promise)) { 104 | this.list.push({ promise, note }); 105 | promise.then(resolve => this.completeItem(promise), reject => this.completeItem(promise)); 106 | if (this.list.length === 1) { 107 | this.dispatch(new PEvent(TaskCountEvent, "Start")); 108 | this.ctimer = window.setTimeout(() => { 109 | this.ctimer = 0; 110 | if (this.list.length > 0) { 111 | this.dispatch(new PEvent(TaskCountEvent, "Depth")); 112 | } 113 | }, this.deferSecond * 1000); 114 | } 115 | } 116 | return promise; 117 | } 118 | completeItem(promise) { 119 | const i = findIndexInArray(this.list, item => item.promise === promise); 120 | if (i > -1) { 121 | this.list.splice(i, 1); 122 | if (this.list.length === 0) { 123 | if (this.ctimer) { 124 | clearTimeout(this.ctimer); 125 | this.ctimer = 0; 126 | } 127 | this.dispatch(new PEvent(TaskCountEvent, "Stop")); 128 | } 129 | } 130 | return this; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /build/es6/store.d.ts: -------------------------------------------------------------------------------- 1 | import { History } from "history"; 2 | import { Middleware, ReducersMapObject, StoreEnhancer } from "redux"; 3 | import { ModelStore } from "./global"; 4 | export declare function invalidview(): void; 5 | export declare type RouterParser = (nextRouter: T, prevRouter?: T) => T; 6 | export declare function buildStore(storeHistory: History, reducersMapObject?: ReducersMapObject, storeMiddlewares?: Middleware[], storeEnhancers?: StoreEnhancer[], initData?: any, routerParser?: RouterParser): ModelStore; 7 | -------------------------------------------------------------------------------- /docs/imgs/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/react-coat/9470cdd6afb9e8251466872833ab0521625f1502/docs/imgs/1.png -------------------------------------------------------------------------------- /docs/imgs/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/react-coat/9470cdd6afb9e8251466872833ab0521625f1502/docs/imgs/2.png -------------------------------------------------------------------------------- /docs/imgs/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/react-coat/9470cdd6afb9e8251466872833ab0521625f1502/docs/imgs/4.png -------------------------------------------------------------------------------- /docs/imgs/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/react-coat/9470cdd6afb9e8251466872833ab0521625f1502/docs/imgs/5.png -------------------------------------------------------------------------------- /docs/imgs/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/react-coat/9470cdd6afb9e8251466872833ab0521625f1502/docs/imgs/6.png -------------------------------------------------------------------------------- /docs/imgs/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/react-coat/9470cdd6afb9e8251466872833ab0521625f1502/docs/imgs/7.png -------------------------------------------------------------------------------- /docs/imgs/a.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/react-coat/9470cdd6afb9e8251466872833ab0521625f1502/docs/imgs/a.jpg -------------------------------------------------------------------------------- /docs/imgs/b.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/react-coat/9470cdd6afb9e8251466872833ab0521625f1502/docs/imgs/b.gif -------------------------------------------------------------------------------- /docs/imgs/c.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/react-coat/9470cdd6afb9e8251466872833ab0521625f1502/docs/imgs/c.gif -------------------------------------------------------------------------------- /docs/imgs/d.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/react-coat/9470cdd6afb9e8251466872833ab0521625f1502/docs/imgs/d.jpg -------------------------------------------------------------------------------- /docs/imgs/e.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/react-coat/9470cdd6afb9e8251466872833ab0521625f1502/docs/imgs/e.gif -------------------------------------------------------------------------------- /docs/imgs/qr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/react-coat/9470cdd6afb9e8251466872833ab0521625f1502/docs/imgs/qr.jpg -------------------------------------------------------------------------------- /docs/recommend.md: -------------------------------------------------------------------------------- 1 | 这年头,Redux 状态管理框架满天飞,前几天在网上闲逛偶然又发现 Rematch、Mirror、Smox、Xredux,都用了一下,发现都是套瓷娃娃,大同小异,拿几个比较历害的来说: 2 | 3 | - [DvaJS](https://github.com/dvajs/dva) Github Stars `12000+` 4 | - [Rematch](https://github.com/rematch/rematch) Github Stars `5000+` 5 | - [Mirror](https://github.com/mirrorjs/mirror) Github Stars `1200+` 6 | 7 | 无非就是类似这样的: 8 | 9 | ```JS 10 | model({ 11 | state: ..., 12 | reducers: { 13 | aaa(playload) 14 | bbb(playload) 15 | }, 16 | effects: { 17 | ccc(playload) 18 | ddd(playload) 19 | } 20 | }) 21 | ``` 22 | 23 | ![无聊](https://github.com/wooline/react-coat/blob/master/docs/imgs/c.gif) 24 | 25 | 审美疲劳了?H 起来,给大家推荐一款小鲜肉`React-coat`: 26 | 27 | [https://github.com/wooline/react-coat](https://github.com/wooline/react-coat) 28 | 29 | ```JS 30 | class ModuleHandlers extends BaseModuleHandlers { 31 | @reducer 32 | public aaa(playload): State {...} 33 | 34 | @reducer 35 | private bbb(playload): State {...} 36 | 37 | @effect("ajaxLoading") 38 | public async ccc(playload) {...} 39 | 40 | @effect("loginLoading") 41 | private async ddd(playload) {...} 42 | } 43 | ``` 44 | 45 | spring 风格?ng 风格? 46 | 47 | ![好](https://github.com/wooline/react-coat/blob/master/docs/imgs/e.gif) 就问你骚气不骚气?😂 48 | 49 | 可能你会说,用 Class 呀,不喜欢,我喜欢 FP 风格。我想说,这是状态管理框架非 React UI 框架,不要为了流行 FP 就皆 FP,就象当年 JS 流行面向对象编程,把面向过程说成洪水猛兽。 50 | 51 | ## 武装到牙齿的 TS 类型反射 52 | 53 | React-coat 全面拥抱 Typescript,直接上图: 54 | 55 | action 调用时的类型反射: 56 | 57 | ![好](https://github.com/wooline/react-coat/blob/master/docs/imgs/4.png) 58 | 59 | 动态加载模块时的类型反射: 60 | 61 | ![好](https://github.com/wooline/react-coat/blob/master/docs/imgs/5.png) 62 | 63 | Store State 结构的类型反射: 64 | 65 | ![好](https://github.com/wooline/react-coat/blob/master/docs/imgs/6.png) 66 | 67 | 连路由参数也有类型反射: 68 | 69 | ![好](https://github.com/wooline/react-coat/blob/master/docs/imgs/7.png) 70 | 71 | ![好](https://github.com/wooline/react-coat/blob/master/docs/imgs/e.gif) 就问你骚气不骚气?😂 72 | 73 | ## 支持单页 SPA 和服务器渲染 SSR 同构 74 | 75 | - 而且**SSR 在开发时也可以享受:“热更新”** 76 | - 还支持 SPA(单页) + SSR(服务器渲染)一键切换。 77 | 78 | > 打开项目根下的./package.json,在"devServer"项中,将 ssr 设为 true 将启用服务器渲染,设为 false 仅使用浏览器渲染 79 | 80 | ![好](https://github.com/wooline/react-coat/blob/master/docs/imgs/e.gif) 就问你骚气不骚气?😂 81 | 82 | ## 强大而便捷的 Dispatch Action 83 | 84 | 对比一下各大框架 Dispatch Action 的语法: 85 | 86 | ```JS 87 | // Dva中 88 | yield put({type: 'moduleA/increment', payload: 2}); 89 | 90 | // Rematch中 91 | dispatch.moduleA.increment(2); 92 | 93 | // Mirror中 94 | actions.moduleA.increment(2); 95 | 96 | // React-coat中 97 | import moduleA from "modules/moduleA/facade"; 98 | ... 99 | await this.dispatch(moduleA.actions.increment(2)); 100 | ``` 101 | 102 | - 语法简洁性上,Dva 用的 saga 中的 yield put,还要写 type 和 payload,最繁琐。其它三款都直接用方法调用,更简洁。 103 | - Rematch 和 Mirror 等于把所有 action 都放到一个全局变量中去了,而 React-coat **去中心化**,按需引入 moduleA,`更利于系统保持松散结构`。 104 | - 从语义上来说 React-coat 依然显示的保留 dispatch 关键字,`moduleA.actions.increment(2)` 返回的是依然是 Action,`dispatch(action)` 作为 Redux 的基本理念得到完整的保持,Rematch 和 Mirror 已经变成传统的 MVC 了。 105 | - 从功能上,只有 Dva 和 React 支持`同步 effect`。其它两款都不支持,或者是我没发现?什么是同步 effect?例如: 106 | 107 | - query 会触发一个 effect,updateState 会触发一个 reducer 108 | - updateState 需要等待 query 执行完后再 dispatch 109 | 110 | ```JS 111 | // Dva中使用 saga 的 put.resolve 来支持同步 effect 112 | yield put.resolve({type: 'query',payload:1}); 113 | yield put({type: 'updateState', payload: 2}); 114 | ``` 115 | 116 | ```JS 117 | // React-coat 中可以直接 awiat dispatch 118 | await this.dispatch(thisModule.actions.query(1)); 119 | this.dispatch(thisModule.actions.updateState(2)); 120 | ``` 121 | 122 | - React-coat 的独有的杀手锏:action 名称和参数的类型反射和智能提示、public private 权限的控制,让你感受什么才叫真正的封装。试想下如果多人同时并行开发多个模块,你还需要为你的模块写一大篇 API 说明文档么? 123 | 124 | ![好](https://github.com/wooline/react-coat/blob/master/docs/imgs/4.png) 125 | 126 | ![好](https://github.com/wooline/react-coat/blob/master/docs/imgs/e.gif) 就问你骚气不骚气?😂 127 | 128 | ## 彻底的模块化 129 | 130 | 既然是企业级应用,那模块化自然是少不了的,包括模块封装、代码分割、按需加载。模块化的目的主要是拆分复杂系统、解耦与重用。 131 | 132 | 以上框架中,Rematch 和 Mirror 的模块化功能比较弱,且不优雅,略过。Dva 和 React-coat 都有强大的模块化功能,其中 Dva 可以搭配 UMI 来自动配置。 133 | 134 | 在 dva 中动态加载 model 和 component,要靠路由配置: 135 | 136 | ```JS 137 | { 138 | path: '/user', 139 | models: () => [import(/* webpackChunkName: 'userModel' */'./pages/users/model.js')], 140 | component: () => import(/* webpackChunkName: 'userPage' */'./pages/users/page.js'), 141 | } 142 | ``` 143 | 144 | React-coat 中代码分割和路由分层而治: 145 | 146 | - 代码分割只做代码分割,不参和路由的事,因为模块也不一定是非得用路由的方式来加载。 147 | - 路由只做路由的事情,不参和代码分割的事,因为模块也不一定非得做代码分割。 148 | - 一个 Module 整体打包成一个 bundle,包括 model 和 views,不至于太碎片。 149 | 150 | ```JS 151 | // 定义代码分割 152 | export const moduleGetter = { 153 | app: () => { 154 | return import(/* webpackChunkName: "app" */ "modules/app"); 155 | }, 156 | photos: () => { 157 | return import(/* webpackChunkName: "photos" */ "modules/photos"); 158 | }, 159 | } 160 | ``` 161 | 162 | React-coat 中支持路由动态加载,也支持非路由动态加载 163 | 164 | ```JS 165 | // 使用路由加载: 166 | const PhotosView = loadView(moduleGetter, ModuleNames.photos, "Main"); 167 | ... 168 | 169 | ``` 170 | 171 | ```JS 172 | // 直接加载: 173 | const PhotosView = loadView(moduleGetter, ModuleNames.photos, "Main"); 174 | ... 175 | render() { 176 | const {showDetails} = this.props; 177 | return showDetails ? : ; 178 | } 179 | ``` 180 | 181 | - Dva 以 Page UI 主线来划分模块;React-coat 以业务功能**高内聚、低偶合**来划分模块。后者更适合解耦与重用。 182 | - Dva 使用集中配置、将 Page、路由、model、代码分割全部都集中写在一个中心文件中;**React-coat 去中心化**,将各自的逻辑封装在各自模块中,并且 model、代码分割、路由分层而治,互不干涉。后者更干净整洁。 183 | - Dva 将每个 model 和 component 都做成一个代码分割包;React-coat 将一个 Module 整体做成一个代码分割包,前者太碎,后者更符合 bundle 概念。 184 | - React-coat 支持路由动态加载 View,也支持非路由动态加载 View,二条腿走路步子迈得更大。 185 | - React-coat 动态加载 View 时会自动导入 Model,无需手工配置加载 Model,是真正的路由组件化。 186 | 187 | 更多差异还是请看:[与 DvaJS 风云对话,是 DvaJS 挑战者?还是又一轮子?](https://juejin.im/post/5c7c84a951882546c54c1910) 188 | 189 | ![好](https://github.com/wooline/react-coat/blob/master/docs/imgs/e.gif) 就问你骚气不骚气?😂 190 | 191 | ## 跨模块的调用与协作 192 | 193 | 在复杂的长业务流程中,跨模块调用与协作是少不了的,Dva、Rematch、Mirror、React-coat 都支持跨模块派发 action,跨模块读取 State。比如: 194 | 195 | ```JS 196 | // Mirror中 197 | if(resphonse.success){ 198 | actions.moduleA.doSomeEffect(); 199 | actions.moduleB.doSomeEffect(); 200 | } 201 | ``` 202 | 203 | 这是一种串联调用的模式,适应于一些耦合紧密的业务流。 但对于一些松散耦合的业务流程,最佳的方式应当是观察者模式,或叫事件广播模式。 204 | 205 | > 场景:当 moduleA 执行了一个 action,moduleB、moduleC、moduleD...都需要执行一些各自的动作 206 | 207 | 这就是 React-coat 独有的杀手锏:ActionHandler 概念。 208 | 209 | ```JS 210 | class ModuleB { 211 | //在ModuleB中监听"ModuleA/update" action 212 | async ["ModuleA/update"] (){ 213 | await this.dispatch(this.action.featchData()) 214 | } 215 | } 216 | 217 | class ModuleC { 218 | //在ModuleC中监听"ModuleA/update" action 219 | async ["ModuleA/update"] (){ 220 | await this.dispatch(this.action.featchData()) 221 | } 222 | } 223 | ``` 224 | 225 | React-coat 主动调用、事件广播两种模式都支持,二手都要抓,二手都要硬。就问你骚气不骚气?😂 226 | 227 | ![完毕](https://github.com/wooline/react-coat/blob/master/docs/imgs/a.jpg) 228 | ![完毕](https://github.com/wooline/react-coat/blob/master/docs/imgs/b.gif) 229 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ["/test"], 3 | testRegex: "\\.test\\.tsx?$", 4 | transform: { 5 | "^.+\\.tsx?$": "ts-jest", 6 | }, 7 | globals: { 8 | "ts-jest": { 9 | tsConfig: "./tsconfig.jest.json", 10 | diagnostics: true, 11 | }, 12 | }, 13 | moduleFileExtensions: ["ts", "tsx", "js", "jsx"], 14 | moduleDirectories: ["node_modules", "src"], 15 | setupFiles: ["./test/setup.js"], 16 | testURL: "http://localhost/", 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-coat", 3 | "version": "4.1.6", 4 | "description": "Structured React + Redux with Typescript and support for isomorphic rendering beautifully(SSR)", 5 | "main": "build/dist/index.js", 6 | "module": "build/es5/index.js", 7 | "types": "build/es6/index.d.ts", 8 | "scripts": { 9 | "build": "node scripts/build.js", 10 | "test": "jest" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/wooline/react-coat.git" 15 | }, 16 | "author": "wooline ", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/wooline/react-coat/issues" 20 | }, 21 | "publishConfig": { 22 | "access": "public" 23 | }, 24 | "homepage": "https://github.com/wooline/react-coat#readme", 25 | "engines": { 26 | "node": ">=8.0.0" 27 | }, 28 | "peerDependencies": { 29 | "@types/node": "^9.0.0 || ^10.0.0 || ^11.0.0", 30 | "@types/history": "^4.0.0", 31 | "@types/react": "^16.0.0", 32 | "@types/react-dom": "^16.0.0", 33 | "@types/react-redux": "^5.0.0 || ^6.0.0 || ^7.0.0", 34 | "@types/react-router-dom": "^4.0.0", 35 | "connected-react-router": "^5.0.0 || ^6.0.0", 36 | "history": "^4.0.0", 37 | "react": "^16.3.0", 38 | "react-dom": "^16.3.0", 39 | "react-redux": "^5.0.0 || ^6.0.0", 40 | "react-router-dom": "^4.0.0", 41 | "redux": "^3.0.0 || ^4.0.0" 42 | }, 43 | "dependencies": {}, 44 | "devDependencies": { 45 | "@types/enzyme": "~3.1.18", 46 | "@types/enzyme-adapter-react-16": "~1.0.4", 47 | "@types/jest": "~24.0.6", 48 | "@types/node": "~11.9.4", 49 | "@types/react": "~16.8.4", 50 | "@types/react-dom": "~16.8.2", 51 | "@types/react-redux": "~7.0.1", 52 | "@types/react-router-dom": "~4.3.1", 53 | "babel-eslint": "~10.0.1", 54 | "chalk": "~2.4.1", 55 | "connected-react-router": "^6.3.1", 56 | "enzyme": "~3.9.0", 57 | "enzyme-adapter-react-16": "~1.9.1", 58 | "eslint": "~5.8.0", 59 | "eslint-config-airbnb-base": "~13.1.0", 60 | "eslint-config-prettier": "~3.3.0", 61 | "eslint-plugin-import": "~2.14.0", 62 | "eslint-plugin-prettier": "~3.0.0", 63 | "fs-extra": "~7.0.0", 64 | "history": "~4.7.2", 65 | "jest": "~24.1.0", 66 | "prettier": "~1.15.2", 67 | "react": "~16.8.2", 68 | "react-dom": "~16.8.2", 69 | "react-redux": "^6.0.1", 70 | "react-router-dom": "~4.3.1", 71 | "redux": "~4.0.1", 72 | "ts-jest": "~24.0.0", 73 | "tslint": "~5.11.0", 74 | "tslint-config-prettier": "~1.15.0", 75 | "tslint-plugin-prettier": "~2.0.1", 76 | "tslint-react": "~3.6.0", 77 | "typescript": "~3.2.1" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const chalk = require("chalk"); 2 | const childProcess = require("child_process"); 3 | const fs = require("fs-extra"); 4 | 5 | function spawn(command, args, errorMessage) { 6 | const isWindows = process.platform === "win32"; // spawn with {shell: true} can solve .cmd resolving, but prettier doesn't run correctly on mac/linux 7 | const result = childProcess.spawnSync(isWindows ? `${command}.cmd` : command, args, {stdio: "inherit"}); 8 | if (result.error) { 9 | console.error(result.error); 10 | process.exit(1); 11 | } 12 | if (result.status !== 0) { 13 | console.error(chalk`{red.bold ${errorMessage}}`); 14 | console.error(`non-zero exit code returned, code=${result.status}, command=${command} ${args.join(" ")}`); 15 | process.exit(1); 16 | } 17 | } 18 | 19 | // function checkCodeStyle() { 20 | // console.info(chalk`{green.bold [task]} {white.bold check code style}`); 21 | // return spawn("prettier", ["--config", "webpack/prettier.json", "--list-different", "src/**/*.{ts,tsx,less}"], "check code style failed, please format above files"); 22 | // } 23 | 24 | function lint() { 25 | console.info(chalk`{green.bold [task]} {white.bold lint}`); 26 | return spawn("tslint", ["-c", "tslint.json", "src/**/*.{ts,tsx}"], "lint failed, please fix"); 27 | } 28 | 29 | function cleanup() { 30 | console.info(chalk`{green.bold [task]} {white.bold cleanup}`); 31 | fs.emptyDirSync("build"); 32 | } 33 | 34 | function test() { 35 | console.info(chalk`{green.bold [task]} {white.bold test}`); 36 | return spawn("jest", [], "test failed, please fix"); 37 | } 38 | 39 | function compile() { 40 | console.info(chalk`{green.bold [task]} {white.bold compile}`); 41 | spawn("tsc", ["-p", "tsconfig.build.json", "-target", "ES5", "-outDir", "build/dist", "-module", "CommonJs"], "compile failed, please fix"); 42 | console.info(chalk`{green.bold [task]} {white.bold compile es5}`); 43 | spawn("tsc", ["-p", "tsconfig.build.json", "-target", "ES5", "-outDir", "build/es5"], "compile failed, please fix"); 44 | console.info(chalk`{green.bold [task]} {white.bold compile es6}`); 45 | spawn("tsc", ["-p", "tsconfig.build.json", "-target", "ES6", "-outDir", "build/es6"], "compile failed, please fix"); 46 | } 47 | 48 | function build() { 49 | cleanup(); 50 | test(); 51 | lint(); 52 | compile(); 53 | } 54 | 55 | build(); 56 | -------------------------------------------------------------------------------- /src/Application.tsx: -------------------------------------------------------------------------------- 1 | import {ConnectedRouter} from "connected-react-router"; 2 | import createBrowserHistory from "history/createBrowserHistory"; 3 | import createMemoryHistory from "history/createMemoryHistory"; 4 | import * as React from "react"; 5 | import {ReactElement} from "react"; 6 | import * as ReactDOM from "react-dom"; 7 | import {renderToNodeStream, renderToString} from "react-dom/server"; 8 | import {Provider} from "react-redux"; 9 | import {withRouter} from "react-router-dom"; 10 | import {Middleware, ReducersMapObject, StoreEnhancer, Store} from "redux"; 11 | import {errorAction, MetaData, Module, ModuleGetter} from "./global"; 12 | import {buildStore, RouterParser} from "./store"; 13 | 14 | export interface StoreOptions { 15 | reducers?: ReducersMapObject; 16 | middlewares?: Middleware[]; 17 | enhancers?: StoreEnhancer[]; 18 | routerParser?: RouterParser; 19 | initData?: any; 20 | } 21 | function isPromiseModule(module: Module | Promise): module is Promise { 22 | return typeof module["then"] === "function"; 23 | } 24 | function getModuleByName(moduleName: string, moduleGetter: ModuleGetter): Promise | Module { 25 | if (moduleName === "router") { 26 | throw new Error("router is a system module"); 27 | } 28 | const result = moduleGetter[moduleName](); 29 | if (isPromiseModule(result)) { 30 | return result.then(module => { 31 | moduleGetter[moduleName] = () => module; 32 | return module; 33 | }); 34 | } else { 35 | return result; 36 | } 37 | } 38 | function getModuleListByNames(moduleNames: string[], moduleGetter: ModuleGetter): Promise { 39 | const preModules = moduleNames.map(moduleName => { 40 | const module = getModuleByName(moduleName, moduleGetter); 41 | if (isPromiseModule(module)) { 42 | return module; 43 | } else { 44 | return Promise.resolve(module); 45 | } 46 | }); 47 | return Promise.all(preModules); 48 | } 49 | export function buildApp>(moduleGetter: M, appName: A, storeOptions: StoreOptions = {}, container: string | Element | ((component: ReactElement) => void) = "root", ssrInitStoreKey: string = "reactCoatInitStore") { 50 | MetaData.appModuleName = appName; 51 | const history = createBrowserHistory(); 52 | let initData = {}; 53 | if (storeOptions.initData || window[ssrInitStoreKey]) { 54 | initData = {...window[ssrInitStoreKey], ...storeOptions.initData}; 55 | } 56 | const store = buildStore(history, storeOptions.reducers, storeOptions.middlewares, storeOptions.enhancers, initData, storeOptions.routerParser); 57 | const preModuleNames: string[] = [appName]; 58 | if (initData) { 59 | preModuleNames.push(...Object.keys(initData).filter(key => key !== appName && initData[key].isModule)); 60 | } 61 | getModuleListByNames(preModuleNames, moduleGetter).then(([appModel]) => { 62 | appModel.model(store); 63 | /* 此处没有使用.then,可以让view不必等init初始化完就及时展示出来,不过会导致和server端渲染不一样,以及可能会出现某些问题,待观察 */ 64 | const WithRouter = withRouter(appModel.views.Main); 65 | const app = ( 66 | 67 | 68 | 69 | 70 | 71 | ); 72 | if (typeof container === "function") { 73 | container(app); 74 | } else { 75 | const render = window[ssrInitStoreKey] ? ReactDOM.hydrate : ReactDOM.render; 76 | render(app, typeof container === "string" ? document.getElementById(container) : container); 77 | } 78 | }); 79 | return store as Store; 80 | } 81 | 82 | export function renderApp>( 83 | moduleGetter: M, 84 | appName: A, 85 | initialEntries: string[], 86 | storeOptions: StoreOptions = {}, 87 | ssrInitStoreKey: string = "reactCoatInitStore", 88 | renderToStream: boolean = false, 89 | ): Promise<{html: string | NodeJS.ReadableStream; data: any; ssrInitStoreKey: string}> { 90 | MetaData.appModuleName = appName; 91 | const history = createMemoryHistory({initialEntries}); 92 | const store = buildStore(history, storeOptions.reducers, storeOptions.middlewares, storeOptions.enhancers, storeOptions.initData, storeOptions.routerParser); 93 | const appModule = moduleGetter[appName]() as Module; 94 | const render = renderToStream ? renderToNodeStream : renderToString; 95 | return appModule 96 | .model(store) 97 | .catch(err => { 98 | return store.dispatch(errorAction(err)); 99 | }) 100 | .then(() => { 101 | const data = store.getState(); 102 | return { 103 | ssrInitStoreKey, 104 | data, 105 | html: render( 106 | 107 | 108 | 109 | 110 | , 111 | ), 112 | }; 113 | }); 114 | } 115 | -------------------------------------------------------------------------------- /src/actions.ts: -------------------------------------------------------------------------------- 1 | import {routerActions} from "connected-react-router"; 2 | import {Action, ActionHandler, BaseModuleState, getModuleActionCreatorList, MetaData, ModelStore, RootState} from "./global"; 3 | import {setLoading} from "./loading"; 4 | 5 | export class BaseModuleHandlers, N extends string> { 6 | protected readonly initState: S; 7 | protected readonly namespace: N = "" as any; 8 | protected readonly store: ModelStore = null as any; 9 | protected readonly actions: Actions = null as any; 10 | protected readonly routerActions: typeof routerActions = routerActions; 11 | 12 | constructor(initState: S, presetData?: any) { 13 | initState.isModule = true; 14 | this.initState = initState; 15 | } 16 | 17 | protected get state(): S { 18 | return this.store.reactCoat.prevState[this.namespace as string]; 19 | } 20 | 21 | protected get rootState(): R { 22 | return this.store.reactCoat.prevState as any; 23 | } 24 | 25 | protected get currentState(): S { 26 | return this.store.reactCoat.currentState[this.namespace as string]; 27 | } 28 | 29 | protected get currentRootState(): R { 30 | return this.store.reactCoat.currentState as any; 31 | } 32 | 33 | protected dispatch(action: Action): Action | Promise { 34 | return this.store.dispatch(action) as any; 35 | } 36 | 37 | protected callThisAction(handler: (...args: T) => any, ...rest: T): {type: string; playload?: any} { 38 | const actions = getModuleActionCreatorList(this.namespace); 39 | return actions[(handler as ActionHandler).__actionName__](rest[0]); 40 | } 41 | 42 | @reducer 43 | protected INIT(payload: S): S { 44 | return payload; 45 | } 46 | @reducer 47 | protected UPDATE(payload: S): S { 48 | return payload; 49 | } 50 | 51 | @reducer 52 | protected LOADING(payload: {[group: string]: string}): S { 53 | const state = this.state; 54 | if (!state) { 55 | return state; 56 | } 57 | return { 58 | ...state, 59 | loading: {...state.loading, ...payload}, 60 | }; 61 | } 62 | protected updateState(payload: Partial) { 63 | this.dispatch(this.callThisAction(this.UPDATE, {...this.state, ...payload})); 64 | } 65 | } 66 | 67 | export function logger(before: (action: Action, moduleName: string, promiseResult: Promise) => void, after: null | ((status: "Rejected" | "Resolved", beforeResult: any, effectResult: any) => void)) { 68 | return (target: any, key: string, descriptor: PropertyDescriptor) => { 69 | const fun: ActionHandler = descriptor.value; 70 | if (!fun.__decorators__) { 71 | fun.__decorators__ = []; 72 | } 73 | fun.__decorators__.push([before, after]); 74 | }; 75 | } 76 | 77 | export function effect(loadingForGroupName?: string | null, loadingForModuleName?: string) { 78 | if (loadingForGroupName === undefined) { 79 | loadingForGroupName = "global"; 80 | loadingForModuleName = MetaData.appModuleName; 81 | } 82 | return (target: any, key: string, descriptor: PropertyDescriptor) => { 83 | const fun = descriptor.value as ActionHandler; 84 | fun.__actionName__ = key; 85 | fun.__isEffect__ = true; 86 | descriptor.enumerable = true; 87 | if (loadingForGroupName) { 88 | const before = (curAction: Action, moduleName: string, promiseResult: Promise) => { 89 | if (MetaData.isBrowser) { 90 | if (!loadingForModuleName) { 91 | loadingForModuleName = moduleName; 92 | } 93 | setLoading(promiseResult, loadingForModuleName, loadingForGroupName as string); 94 | } 95 | }; 96 | if (!fun.__decorators__) { 97 | fun.__decorators__ = []; 98 | } 99 | fun.__decorators__.push([before, null]); 100 | } 101 | return descriptor; 102 | }; 103 | } 104 | 105 | export function reducer(target: any, key: string, descriptor: PropertyDescriptor) { 106 | const fun = descriptor.value as ActionHandler; 107 | fun.__actionName__ = key; 108 | fun.__isReducer__ = true; 109 | descriptor.enumerable = true; 110 | return descriptor; 111 | } 112 | 113 | type Handler = F extends (...args: infer P) => any 114 | ? ( 115 | ...args: P 116 | ) => { 117 | type: string; 118 | } 119 | : never; 120 | 121 | export type Actions = {[K in keyof Ins]: Ins[K] extends (...args: any[]) => any ? Handler : never}; 122 | -------------------------------------------------------------------------------- /src/global.ts: -------------------------------------------------------------------------------- 1 | import {RouterState} from "connected-react-router"; 2 | import {ComponentType} from "react"; 3 | import {History} from "history"; 4 | import {Store} from "redux"; 5 | import {LoadingState} from "./loading"; 6 | export interface ModelStore extends Store { 7 | reactCoat: { 8 | history: History; 9 | prevState: {[key: string]: any}; 10 | currentState: {[key: string]: any}; 11 | reducerMap: ReducerMap; 12 | effectMap: EffectMap; 13 | injectedModules: {[namespace: string]: boolean}; 14 | routerInited: boolean; 15 | currentViews: CurrentViews; 16 | }; 17 | } 18 | export interface CurrentViews { 19 | [moduleName: string]: {[viewName: string]: number}; 20 | } 21 | export interface BaseModuleState { 22 | isModule?: boolean; 23 | loading?: {[key: string]: LoadingState}; 24 | } 25 | 26 | export function isModuleState(module: any): module is BaseModuleState { 27 | return module.isModule; 28 | } 29 | 30 | export type GetModule = () => M | Promise; 31 | 32 | export interface ModuleGetter { 33 | [moduleName: string]: GetModule; 34 | } 35 | 36 | export type ReturnModule any> = T extends () => Promise ? R : T extends () => infer R ? R : Module; 37 | 38 | type ModuleStates = M["model"]["initState"]; 39 | type ModuleViews = {[key in keyof M["views"]]?: number}; 40 | 41 | export type RootState = { 42 | router: R; 43 | views: {[key in keyof G]?: ModuleViews>}; 44 | } & {[key in keyof G]?: ModuleStates>}; 45 | 46 | export interface Action { 47 | type: string; 48 | priority?: string[]; 49 | payload?: any; 50 | } 51 | export interface ActionCreatorMap { 52 | [moduleName: string]: ActionCreatorList; 53 | } 54 | export interface ActionCreatorList { 55 | [actionName: string]: ActionCreator; 56 | } 57 | export type ActionCreator = (payload?: any) => Action; 58 | 59 | export interface ActionHandler { 60 | __actionName__: string; 61 | __isReducer__?: boolean; 62 | __isEffect__?: boolean; 63 | __isHandler__?: boolean; 64 | __decorators__?: Array<[(action: Action, moduleName: string, effectResult: Promise) => any, null | ((status: "Rejected" | "Resolved", beforeResult: any, effectResult: any) => void)]>; 65 | __decoratorResults__?: any[]; 66 | (payload?: any): any; 67 | } 68 | 69 | export interface ReducerHandler extends ActionHandler { 70 | (payload?: any): BaseModuleState; 71 | } 72 | export interface EffectHandler extends ActionHandler { 73 | (payload?: any): Promise; 74 | } 75 | export interface ActionHandlerList { 76 | [actionName: string]: ActionHandler; 77 | } 78 | export interface ActionHandlerMap { 79 | [actionName: string]: {[moduleName: string]: ActionHandler}; 80 | } 81 | export interface ReducerMap extends ActionHandlerMap { 82 | [actionName: string]: {[moduleName: string]: ReducerHandler}; 83 | } 84 | export interface EffectMap extends ActionHandlerMap { 85 | [actionName: string]: {[moduleName: string]: EffectHandler}; 86 | } 87 | export const LOADING = "LOADING"; 88 | export const ERROR = "@@framework/ERROR"; 89 | export const INIT = "INIT"; 90 | export const LOCATION_CHANGE = "@@router/LOCATION_CHANGE"; 91 | export const VIEW_INVALID = "@@framework/VIEW_INVALID"; 92 | export const NSP = "/"; 93 | 94 | export const MetaData: { 95 | isBrowser: boolean; 96 | isDev: boolean; 97 | actionCreatorMap: ActionCreatorMap; 98 | clientStore: ModelStore; 99 | appModuleName: string; 100 | } = { 101 | isBrowser: typeof window === "object", 102 | isDev: process.env.NODE_ENV !== "production", 103 | actionCreatorMap: {}, 104 | clientStore: null as any, 105 | appModuleName: null as any, 106 | }; 107 | 108 | export function isPromise(data: any): data is Promise { 109 | return typeof data["then"] === "function"; 110 | } 111 | export interface Module} = {[key: string]: ComponentType}> { 112 | model: M; 113 | views: VS; 114 | } 115 | 116 | export function errorAction(error: any) { 117 | return { 118 | type: ERROR, 119 | error, 120 | }; 121 | } 122 | 123 | export function viewInvalidAction(currentViews: CurrentViews) { 124 | return { 125 | type: VIEW_INVALID, 126 | currentViews, 127 | }; 128 | } 129 | export function setAppModuleName(moduleName: string) { 130 | MetaData.appModuleName = moduleName; 131 | } 132 | 133 | export function delayPromise(second: number) { 134 | return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { 135 | const fun = descriptor.value; 136 | descriptor.value = (...args: any[]) => { 137 | const delay = new Promise(resolve => { 138 | setTimeout(() => { 139 | resolve(true); 140 | }, second * 1000); 141 | }); 142 | return Promise.all([delay, fun.apply(target, args)]).then(items => { 143 | return items[1]; 144 | }); 145 | }; 146 | }; 147 | } 148 | 149 | export function getModuleActionCreatorList(namespace: string) { 150 | // if (window["Proxy"]) { 151 | // actions = new window["Proxy"]( 152 | // {}, 153 | // { 154 | // get: (target: {}, key: string) => { 155 | // return (data: any) => ({ type: namespace + "/" + key, data }); 156 | // } 157 | // } 158 | // ); 159 | // } else { 160 | // actions = getModuleActions(namespace) as any; 161 | // } 162 | if (MetaData.actionCreatorMap[namespace]) { 163 | return MetaData.actionCreatorMap[namespace]; 164 | } else { 165 | const obj = {}; 166 | MetaData.actionCreatorMap[namespace] = obj; 167 | return obj; 168 | } 169 | } 170 | 171 | export interface Model { 172 | namespace: string; 173 | initState: ModuleState; 174 | (store: ModelStore): Promise; 175 | } 176 | 177 | export function exportModule(namespace: string) { 178 | const actions: T = getModuleActionCreatorList(namespace) as T; 179 | return { 180 | namespace, 181 | actions, 182 | }; 183 | } 184 | export function warning(...args: any[]) { 185 | if (MetaData.isDev) { 186 | console.warn(...args); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export {RouterState} from "connected-react-router"; 2 | export {Actions, BaseModuleHandlers, effect, logger, reducer} from "./actions"; 3 | export {buildApp, renderApp} from "./Application"; 4 | export {BaseModuleState, CurrentViews, delayPromise, ERROR, errorAction, exportModule, GetModule, INIT, LOCATION_CHANGE, ModelStore, Module, ModuleGetter, ReturnModule, RootState, VIEW_INVALID} from "./global"; 5 | export {LoadingState, setLoading, setLoadingDepthTime} from "./loading"; 6 | export {exportModel} from "./model"; 7 | export {exportView, loadModel, loadView} from "./module"; 8 | export {RouterParser} from "./store"; 9 | -------------------------------------------------------------------------------- /src/loading.ts: -------------------------------------------------------------------------------- 1 | import {MetaData, LOADING, getModuleActionCreatorList} from "./global"; 2 | import {TaskCounter, TaskCountEvent} from "./sprite"; 3 | 4 | const loadings: {[namespace: string]: TaskCounter} = {}; 5 | 6 | let depthTime: number = 2; 7 | 8 | export function setLoadingDepthTime(second: number) { 9 | depthTime = second; 10 | } 11 | export function setLoading>(item: T, namespace: string = MetaData.appModuleName, group: string = "global"): T { 12 | if (!MetaData.isBrowser) { 13 | return item; 14 | } 15 | const key = namespace + "/" + group; 16 | if (!loadings[key]) { 17 | loadings[key] = new TaskCounter(depthTime); 18 | loadings[key].addListener(TaskCountEvent, e => { 19 | const store = MetaData.clientStore; 20 | if (store) { 21 | const actions = getModuleActionCreatorList(namespace)[LOADING]; 22 | const action = actions({[group]: e.data}); 23 | store.dispatch(action); 24 | } 25 | }); 26 | } 27 | loadings[key].addItem(item); 28 | return item; 29 | } 30 | export enum LoadingState { 31 | Start = "Start", 32 | Stop = "Stop", 33 | Depth = "Depth", 34 | } 35 | -------------------------------------------------------------------------------- /src/model.ts: -------------------------------------------------------------------------------- 1 | import {ActionHandler, ActionHandlerList, ActionHandlerMap, isPromise, getModuleActionCreatorList, NSP, BaseModuleState, RootState, ModelStore, Model} from "./global"; 2 | import {BaseModuleHandlers} from "./actions"; 3 | 4 | export function exportModel(namespace: N, HandlersClass: {new (initState: S, presetData?: any): BaseModuleHandlers, N>}, initState: S): Model { 5 | const fun = (store: ModelStore) => { 6 | const hasInjected = store.reactCoat.injectedModules[namespace]; 7 | if (!hasInjected) { 8 | store.reactCoat.injectedModules[namespace] = true; 9 | const moduleState: BaseModuleState = store.getState()[namespace]; 10 | const handlers = new HandlersClass(initState, moduleState); 11 | (handlers as any).namespace = namespace; 12 | (handlers as any).store = store; 13 | const actions = injectActions(store, namespace, handlers as any); 14 | (handlers as any).actions = actions; 15 | if (!moduleState) { 16 | const initAction = actions.INIT((handlers as any).initState); 17 | const action = store.dispatch(initAction); 18 | if (isPromise(action)) { 19 | return action; 20 | } else { 21 | return Promise.resolve(void 0); 22 | } 23 | } else { 24 | return Promise.resolve(void 0); 25 | } 26 | } else { 27 | return Promise.resolve(void 0); 28 | } 29 | }; 30 | fun.namespace = namespace; 31 | fun.initState = initState; 32 | return fun; 33 | } 34 | function bindThis(fun: ActionHandler, thisObj: any) { 35 | const newFun = fun.bind(thisObj); 36 | Object.keys(fun).forEach(key => { 37 | newFun[key] = fun[key]; 38 | }); 39 | 40 | return newFun as ActionHandler; 41 | } 42 | function injectActions(store: ModelStore, namespace: string, handlers: ActionHandlerList) { 43 | for (const actionName in handlers) { 44 | if (typeof handlers[actionName] === "function") { 45 | let handler = handlers[actionName]; 46 | if (handler.__isReducer__ || handler.__isEffect__) { 47 | handler = bindThis(handler, handlers); 48 | const arr = actionName.split(NSP); 49 | if (arr[1]) { 50 | handler.__isHandler__ = true; 51 | transformAction(actionName, handler, namespace, handler.__isEffect__ ? store.reactCoat.effectMap : store.reactCoat.reducerMap); 52 | } else { 53 | handler.__isHandler__ = false; 54 | transformAction(namespace + NSP + actionName, handler, namespace, handler.__isEffect__ ? store.reactCoat.effectMap : store.reactCoat.reducerMap); 55 | addModuleActionCreatorList(namespace, actionName); 56 | } 57 | } 58 | } 59 | } 60 | return getModuleActionCreatorList(namespace); 61 | } 62 | 63 | function transformAction(actionName: string, action: ActionHandler, listenerModule: string, actionHandlerMap: ActionHandlerMap) { 64 | if (!actionHandlerMap[actionName]) { 65 | actionHandlerMap[actionName] = {}; 66 | } 67 | if (actionHandlerMap[actionName][listenerModule]) { 68 | throw new Error(`Action duplicate or conflict : ${actionName}.`); 69 | } 70 | actionHandlerMap[actionName][listenerModule] = action; 71 | } 72 | 73 | function addModuleActionCreatorList(namespace: string, actionName: string) { 74 | const actions = getModuleActionCreatorList(namespace); 75 | if (!actions[actionName]) { 76 | actions[actionName] = payload => ({type: namespace + NSP + actionName, payload}); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/module.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {ComponentType} from "react"; 3 | import {Model, Module, MetaData, GetModule, ModuleGetter} from "./global"; 4 | import {invalidview} from "./store"; 5 | 6 | function isPromiseModule(module: Module | Promise): module is Promise { 7 | return typeof module["then"] === "function"; 8 | } 9 | function isPromiseView(moduleView: ComponentType | Promise>): moduleView is Promise> { 10 | return typeof moduleView["then"] === "function"; 11 | } 12 | function getView>(getModule: GetModule, viewName: N): M["views"][N] | Promise { 13 | const result = getModule(); 14 | if (isPromiseModule(result)) { 15 | return result.then(module => module.views[viewName]); 16 | } else { 17 | return result.views[viewName]; 18 | } 19 | } 20 | 21 | export function loadModel(getModule: GetModule): Promise { 22 | const result = getModule(); 23 | if (isPromiseModule(result)) { 24 | return result.then(module => module.model); 25 | } else { 26 | return Promise.resolve(result.model); 27 | } 28 | } 29 | interface LoadViewState { 30 | Component: ComponentType | null; 31 | } 32 | interface ViewState { 33 | modelReady: boolean; 34 | } 35 | export type ReturnViews any> = T extends () => Promise> ? R : never; 36 | 37 | export function loadView, V extends ReturnViews, N extends Extract>(moduleGetter: MG, moduleName: M, viewName: N, loadingComponent: React.ReactNode = null): V[N] { 38 | return class Loader extends React.Component { 39 | public state: LoadViewState = { 40 | Component: null, 41 | }; 42 | public shouldComponentUpdate(nextProps: any, nextState: LoadViewState) { 43 | return nextState.Component !== this.state.Component; 44 | } 45 | public componentWillMount() { 46 | const moduleViewResult = getView(moduleGetter[moduleName], viewName); 47 | if (isPromiseView(moduleViewResult)) { 48 | moduleViewResult.then(Component => { 49 | this.setState({ 50 | Component, 51 | }); 52 | }); 53 | } else { 54 | this.setState({ 55 | Component: moduleViewResult, 56 | }); 57 | } 58 | } 59 | 60 | public render() { 61 | const {Component} = this.state; 62 | return Component ? : loadingComponent; 63 | } 64 | } as any; 65 | } 66 | 67 | export function exportView>(ComponentView: C, model: Model, viewName: string): C { 68 | const Comp = ComponentView as any; 69 | if (MetaData.isBrowser) { 70 | return class Component extends React.PureComponent { 71 | public state: ViewState; 72 | constructor(props: any, context?: any) { 73 | super(props, context); 74 | const state = MetaData.clientStore.getState(); 75 | const namespace = model.namespace; 76 | this.state = { 77 | modelReady: !!state[namespace], 78 | }; 79 | model(MetaData.clientStore).then(() => { 80 | if (!this.state.modelReady) { 81 | this.setState({modelReady: true}); 82 | } 83 | }); 84 | } 85 | public componentWillMount() { 86 | const currentViews = MetaData.clientStore.reactCoat.currentViews; 87 | if (!currentViews[model.namespace]) { 88 | currentViews[model.namespace] = {[viewName]: 1}; 89 | } else { 90 | const views = currentViews[model.namespace]; 91 | if (!views[viewName]) { 92 | views[viewName] = 1; 93 | } else { 94 | views[viewName]++; 95 | } 96 | } 97 | invalidview(); 98 | } 99 | public componentWillUnmount() { 100 | const currentViews = MetaData.clientStore.reactCoat.currentViews; 101 | if (currentViews[model.namespace] && currentViews[model.namespace][viewName]) { 102 | currentViews[model.namespace][viewName]--; 103 | } 104 | invalidview(); 105 | } 106 | public render() { 107 | return this.state.modelReady ? : null; 108 | } 109 | } as any; 110 | } else { 111 | // ssr时不存在invalidview,也不需要 model初始化,因为必须提前将 model初始化(单向数据流) 112 | return Comp; 113 | } 114 | } 115 | 116 | /* 117 | let autoId: number = 0; 118 | export function exportView>(ComponentView: C, model: Model): C { 119 | const Comp = ComponentView as any; 120 | return class PureComponent extends React.PureComponent { 121 | private uid: string = (autoId++).toString(); 122 | public static contextTypes = { 123 | store: PropTypes.object, 124 | }; 125 | public render() { 126 | return ; 127 | } 128 | public componentWillMount() { 129 | const {store} = this.context; 130 | (model as InternalModel)(store, this.uid); 131 | } 132 | public componentWillUnmount() { 133 | const {store} = this.context; 134 | (model as InternalModel)(store, this.uid, true); 135 | } 136 | } as any; 137 | } 138 | 139 | export function exportModel(namespace: string, HandlersClass: {new (): BaseModuleHandlers}): Model { 140 | return async (store: ModelStore, viewName?: string, unmount?: boolean) => { 141 | const hasInjected = store.reactCoat.injectedModules[namespace]; 142 | if (!hasInjected) { 143 | store.reactCoat.injectedModules[namespace] = viewName ? {[viewName]: true} : {}; 144 | const handlers = new HandlersClass(); 145 | (handlers as any).namespace = namespace; 146 | (handlers as any).store = store; 147 | const actions = injectActions(store, namespace, handlers as any); 148 | const hasInited = Boolean(store.getState()[namespace]); 149 | if (!hasInited) { 150 | const initAction = actions.INIT((handlers as any).initState); 151 | await store.dispatch(initAction); 152 | } 153 | if (viewName) { 154 | console.log("mount", namespace, "first Inject", hasInited); 155 | return store.dispatch(actions.MOUNT()); 156 | } 157 | } else { 158 | const actions = getModuleActionCreatorList(namespace); 159 | if (unmount && viewName) { 160 | delete hasInjected[viewName]; 161 | if (Object.keys(hasInjected).length === 0) { 162 | return store.dispatch(actions.UNMOUNT()); 163 | } 164 | } else if (viewName) { 165 | if (Object.keys(hasInjected).length === 0) { 166 | console.log("mount", namespace, "hasInjected"); 167 | hasInjected[viewName] = true; 168 | return store.dispatch(actions.MOUNT()); 169 | } else { 170 | hasInjected[viewName] = true; 171 | } 172 | } 173 | } 174 | return void 0; 175 | }; 176 | */ 177 | -------------------------------------------------------------------------------- /src/sprite.ts: -------------------------------------------------------------------------------- 1 | function emptyObject(obj: any): T { 2 | const arr: string[] = []; 3 | for (const key in obj) { 4 | if (obj.hasOwnProperty(key)) { 5 | arr.push(key); 6 | } 7 | } 8 | arr.forEach((key: string) => { 9 | delete obj[key]; 10 | }); 11 | return obj; 12 | } 13 | function findIndexInArray(arr: T[], fun: (item: T) => boolean): number { 14 | for (let i = 0, k = arr.length; i < k; i++) { 15 | if (fun(arr[i])) { 16 | return i; 17 | } 18 | } 19 | return -1; 20 | } 21 | export const TaskCountEvent = "TaskCountEvent"; 22 | 23 | export type TaskCounterState = "Start" | "Stop" | "Depth"; 24 | 25 | export class PEvent { 26 | public readonly target: PDispatcher = null as any; 27 | public readonly currentTarget: PDispatcher = null as any; 28 | 29 | constructor(public readonly name: string, public readonly data?: any, public bubbling: boolean = false) {} 30 | 31 | public setTarget(target: PDispatcher) { 32 | (this as any).target = target; 33 | } 34 | 35 | public setCurrentTarget(target: PDispatcher) { 36 | (this as any).currentTarget = target; 37 | } 38 | } 39 | 40 | export class PDispatcher { 41 | protected readonly storeHandlers: { 42 | [key: string]: Array<(e: PEvent) => void>; 43 | } = {}; 44 | 45 | constructor(public readonly parent?: PDispatcher | undefined) {} 46 | 47 | public addListener(ename: string, handler: (e: PEvent) => void): this { 48 | let dictionary = this.storeHandlers[ename]; 49 | if (!dictionary) { 50 | this.storeHandlers[ename] = dictionary = []; 51 | } 52 | dictionary.push(handler); 53 | return this; 54 | } 55 | 56 | public removeListener(ename?: string, handler?: (e: PEvent) => void): this { 57 | if (!ename) { 58 | emptyObject(this.storeHandlers); 59 | } else { 60 | const handlers = this.storeHandlers; 61 | if (handlers.propertyIsEnumerable(ename)) { 62 | const dictionary = handlers[ename]; 63 | if (!handler) { 64 | delete handlers[ename]; 65 | } else { 66 | const n = dictionary.indexOf(handler); 67 | if (n > -1) { 68 | dictionary.splice(n, 1); 69 | } 70 | if (dictionary.length === 0) { 71 | delete handlers[ename]; 72 | } 73 | } 74 | } 75 | } 76 | return this; 77 | } 78 | 79 | public dispatch(evt: PEvent): this { 80 | if (!evt.target) { 81 | evt.setTarget(this); 82 | } 83 | evt.setCurrentTarget(this); 84 | const dictionary = this.storeHandlers[evt.name]; 85 | if (dictionary) { 86 | for (let i = 0, k = dictionary.length; i < k; i++) { 87 | dictionary[i](evt); 88 | } 89 | } 90 | if (this.parent && evt.bubbling) { 91 | this.parent.dispatch(evt); 92 | } 93 | return this; 94 | } 95 | public setParent(parent?: PDispatcher): this { 96 | (this as any).parent = parent; 97 | return this; 98 | } 99 | } 100 | 101 | export class TaskCounter extends PDispatcher { 102 | public readonly list: Array<{promise: Promise; note: string}> = []; 103 | private ctimer: number = 0; 104 | constructor(public deferSecond: number) { 105 | super(); 106 | } 107 | public addItem(promise: Promise, note: string = ""): Promise { 108 | if (!this.list.some(item => item.promise === promise)) { 109 | this.list.push({promise, note}); 110 | promise.then(resolve => this.completeItem(promise), reject => this.completeItem(promise)); 111 | 112 | if (this.list.length === 1) { 113 | this.dispatch(new PEvent(TaskCountEvent, "Start")); 114 | this.ctimer = window.setTimeout(() => { 115 | this.ctimer = 0; 116 | if (this.list.length > 0) { 117 | this.dispatch(new PEvent(TaskCountEvent, "Depth")); 118 | } 119 | }, this.deferSecond * 1000); 120 | } 121 | } 122 | return promise; 123 | } 124 | private completeItem(promise: Promise): this { 125 | const i = findIndexInArray(this.list, item => item.promise === promise); 126 | if (i > -1) { 127 | this.list.splice(i, 1); 128 | if (this.list.length === 0) { 129 | if (this.ctimer) { 130 | clearTimeout(this.ctimer); 131 | this.ctimer = 0; 132 | } 133 | 134 | this.dispatch(new PEvent(TaskCountEvent, "Stop")); 135 | } 136 | } 137 | return this; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | import {connectRouter, routerMiddleware} from "connected-react-router"; 2 | import {History} from "history"; 3 | import {applyMiddleware, compose, createStore, Middleware, ReducersMapObject, StoreEnhancer} from "redux"; 4 | import {Action, CurrentViews, LOADING, LOCATION_CHANGE, MetaData, ModelStore, NSP, errorAction, viewInvalidAction, VIEW_INVALID} from "./global"; 5 | 6 | let invalidViewTimer: NodeJS.Timer | null; 7 | 8 | function checkInvalidview() { 9 | invalidViewTimer = null; 10 | const currentViews = MetaData.clientStore.reactCoat.currentViews; 11 | const views: CurrentViews = {}; 12 | for (const moduleName in currentViews) { 13 | if (currentViews.hasOwnProperty(moduleName)) { 14 | const element = currentViews[moduleName]; 15 | for (const viewname in element) { 16 | if (element[viewname]) { 17 | if (!views[moduleName]) { 18 | views[moduleName] = {}; 19 | } 20 | views[moduleName][viewname] = element[viewname]; 21 | } 22 | } 23 | } 24 | } 25 | MetaData.clientStore.dispatch(viewInvalidAction(views)); 26 | } 27 | 28 | export function invalidview() { 29 | if (!invalidViewTimer) { 30 | invalidViewTimer = setTimeout(checkInvalidview, 4); 31 | } 32 | } 33 | 34 | function getActionData(action: Action) { 35 | const arr = Object.keys(action).filter(key => key !== "type" && key !== "priority" && key !== "time"); 36 | if (arr.length === 0) { 37 | return undefined; 38 | } else if (arr.length === 1) { 39 | return action[arr[0]]; 40 | } else { 41 | const data = {...action}; 42 | delete data["type"]; 43 | delete data["priority"]; 44 | delete data["time"]; 45 | return data; 46 | } 47 | } 48 | 49 | export type RouterParser = (nextRouter: T, prevRouter?: T) => T; 50 | 51 | function simpleEqual(obj1: any, obj2: any): boolean { 52 | if (obj1 === obj2) { 53 | return true; 54 | } else if (typeof obj1 !== typeof obj2 || typeof obj1 !== "object") { 55 | return false; 56 | } else { 57 | const keys1 = Object.keys(obj1); 58 | const keys2 = Object.keys(obj2); 59 | if (keys1.length !== keys2.length) { 60 | return false; 61 | } else { 62 | for (const key of keys1) { 63 | if (!simpleEqual(obj1[key], obj2[key])) { 64 | return false; 65 | } 66 | } 67 | return true; 68 | } 69 | } 70 | } 71 | export function buildStore(storeHistory: History, reducersMapObject: ReducersMapObject = {}, storeMiddlewares: Middleware[] = [], storeEnhancers: StoreEnhancer[] = [], initData: any = {}, routerParser?: RouterParser): ModelStore { 72 | let store: ModelStore; 73 | const combineReducers = (rootState: {[key: string]: any}, action: Action) => { 74 | if (!store) { 75 | return rootState; 76 | } 77 | const reactCoat = store.reactCoat; 78 | reactCoat.prevState = rootState; 79 | const currentState = {...rootState}; 80 | reactCoat.currentState = currentState; 81 | 82 | if (!currentState.views) { 83 | currentState.views = {}; 84 | } 85 | Object.keys(reducersMapObject).forEach(namespace => { 86 | currentState[namespace] = reducersMapObject[namespace](currentState[namespace], action); 87 | if (namespace === "router" && routerParser && rootState.router !== currentState.router) { 88 | currentState.router = routerParser(currentState.router, rootState.router); 89 | } 90 | }); 91 | // 内置 action handler 92 | if (action.type === VIEW_INVALID) { 93 | const views: CurrentViews = getActionData(action); 94 | if (!simpleEqual(currentState.views, views)) { 95 | currentState.views = views; 96 | } 97 | } 98 | const handlersCommon = reactCoat.reducerMap[action.type] || {}; 99 | // 支持泛监听,形如 */loading 100 | const handlersEvery = reactCoat.reducerMap[action.type.replace(new RegExp(`[^${NSP}]+`), "*")] || {}; 101 | const handlers = {...handlersCommon, ...handlersEvery}; 102 | const handlerModules = Object.keys(handlers); 103 | 104 | if (handlerModules.length > 0) { 105 | const orderList: string[] = action.priority ? [...action.priority] : []; 106 | handlerModules.forEach(namespace => { 107 | const fun = handlers[namespace]; 108 | if (fun.__isHandler__) { 109 | orderList.push(namespace); 110 | } else { 111 | orderList.unshift(namespace); 112 | } 113 | }); 114 | const moduleNameMap: {[key: string]: boolean} = {}; 115 | orderList.forEach(namespace => { 116 | if (!moduleNameMap[namespace]) { 117 | moduleNameMap[namespace] = true; 118 | const fun = handlers[namespace]; 119 | currentState[namespace] = fun(getActionData(action)); 120 | } 121 | }); 122 | } 123 | const changed = Object.keys(rootState).length !== Object.keys(currentState).length || Object.keys(rootState).some(namespace => rootState[namespace] !== currentState[namespace]); 124 | reactCoat.prevState = changed ? currentState : rootState; 125 | return reactCoat.prevState; 126 | }; 127 | const effectMiddleware = ({dispatch}: {dispatch: Function}) => (next: Function) => (originalAction: Action) => { 128 | if (!MetaData.isBrowser) { 129 | if (originalAction.type.split(NSP)[1] === LOADING) { 130 | return originalAction; 131 | } 132 | } 133 | // SSR需要数据是单向的,store->view,不能store->view->store->view,而view:ConnectedRouter初始化时会触发一次LOCATION_CHANGE 134 | if (originalAction.type === LOCATION_CHANGE) { 135 | if (!store.reactCoat.routerInited) { 136 | store.reactCoat.routerInited = true; 137 | return originalAction; 138 | } else { 139 | invalidview(); 140 | } 141 | } 142 | const action: Action = next(originalAction); 143 | if (!action) { 144 | return null; 145 | } 146 | const handlersCommon = store.reactCoat.effectMap[action.type] || {}; 147 | // 支持泛监听,形如 */loading 148 | const handlersEvery = store.reactCoat.effectMap[action.type.replace(new RegExp(`[^${NSP}]+`), "*")] || {}; 149 | const handlers = {...handlersCommon, ...handlersEvery}; 150 | const handlerModules = Object.keys(handlers); 151 | 152 | if (handlerModules.length > 0) { 153 | const orderList: string[] = action.priority ? [...action.priority] : []; 154 | handlerModules.forEach(namespace => { 155 | const fun = handlers[namespace]; 156 | if (fun.__isHandler__) { 157 | orderList.push(namespace); 158 | } else { 159 | orderList.unshift(namespace); 160 | } 161 | }); 162 | const moduleNameMap: {[key: string]: boolean} = {}; 163 | const promiseResults: Array> = []; 164 | orderList.forEach(namespace => { 165 | if (!moduleNameMap[namespace]) { 166 | moduleNameMap[namespace] = true; 167 | const fun = handlers[namespace]; 168 | const effectResult = fun(getActionData(action)); 169 | const decorators = fun.__decorators__; 170 | if (decorators) { 171 | const results: any[] = []; 172 | decorators.forEach((decorator, index) => { 173 | results[index] = decorator[0](action, namespace, effectResult); 174 | }); 175 | fun.__decoratorResults__ = results; 176 | } 177 | 178 | effectResult.then( 179 | (reslove: any) => { 180 | if (decorators) { 181 | const results = fun.__decoratorResults__ || []; 182 | decorators.forEach((decorator, index) => { 183 | if (decorator[1]) { 184 | decorator[1]("Resolved", results[index], reslove); 185 | } 186 | }); 187 | fun.__decoratorResults__ = undefined; 188 | } 189 | return reslove; 190 | }, 191 | (reject: any) => { 192 | if (decorators) { 193 | const results = fun.__decoratorResults__ || []; 194 | decorators.forEach((decorator, index) => { 195 | if (decorator[1]) { 196 | decorator[1]("Rejected", results[index], reject); 197 | } 198 | }); 199 | fun.__decoratorResults__ = undefined; 200 | } 201 | }, 202 | ); 203 | promiseResults.push(effectResult); 204 | } 205 | }); 206 | if (promiseResults.length) { 207 | return Promise.all(promiseResults); 208 | } 209 | } 210 | return action; 211 | }; 212 | 213 | if (reducersMapObject.router) { 214 | throw new Error("the reducer name 'router' is not allowed"); 215 | } 216 | reducersMapObject.router = connectRouter(storeHistory); 217 | const enhancers = [applyMiddleware(...[effectMiddleware, routerMiddleware(storeHistory), ...storeMiddlewares]), ...storeEnhancers]; 218 | if (MetaData.isBrowser && MetaData.isDev && window["__REDUX_DEVTOOLS_EXTENSION__"]) { 219 | // 220 | // __REDUX_DEVTOOLS_EXTENSION_COMPOSE__ 221 | enhancers.push(window["__REDUX_DEVTOOLS_EXTENSION__"](window["__REDUX_DEVTOOLS_EXTENSION__OPTIONS"])); 222 | } 223 | store = createStore(combineReducers as any, initData, compose(...enhancers)); 224 | if (store.reactCoat) { 225 | throw new Error("store enhancers has 'reactCoat' property"); 226 | } else { 227 | store.reactCoat = { 228 | history: storeHistory, 229 | prevState: {router: null as any}, 230 | currentState: {router: null as any}, 231 | reducerMap: {}, 232 | effectMap: {}, 233 | injectedModules: {}, 234 | routerInited: false, 235 | currentViews: {}, 236 | }; 237 | } 238 | MetaData.clientStore = store; 239 | if (MetaData.isBrowser) { 240 | window.onerror = (evt: Event | string, source?: string, fileno?: number, columnNumber?: number, error?: Error) => { 241 | store.dispatch(errorAction(error || evt)); 242 | }; 243 | if ("onunhandledrejection" in window) { 244 | window.onunhandledrejection = error => { 245 | store.dispatch(errorAction(error.reason)); 246 | }; 247 | } 248 | } 249 | return store; 250 | } 251 | -------------------------------------------------------------------------------- /test/__snapshots__/csr_render.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`spa success:/videos?search={title:success} 1`] = ` 4 | Object { 5 | "data": Object { 6 | "app": Object { 7 | "curUser": Object { 8 | "avatarUrl": "/imgs/1.jpg", 9 | "hasLogin": true, 10 | "uid": "1", 11 | "username": "test", 12 | }, 13 | "isModule": true, 14 | "loading": Object { 15 | "global": "Stop", 16 | "login": "Stop", 17 | }, 18 | }, 19 | "router": Object { 20 | "action": "POP", 21 | "location": Object { 22 | "hash": "", 23 | "pathname": "/videos", 24 | "search": "?search={%22title%22:%22success%22}", 25 | "state": undefined, 26 | }, 27 | }, 28 | "videos": Object { 29 | "isModule": true, 30 | "listItems": Array [ 31 | Object { 32 | "id": "1", 33 | "title": "item1", 34 | }, 35 | Object { 36 | "id": "2", 37 | "title": "item2", 38 | }, 39 | ], 40 | "listSearch": Object { 41 | "page": 1, 42 | "pageSize": 10, 43 | "title": "success", 44 | }, 45 | "listSummary": Object { 46 | "page": 1, 47 | "pageSize": 10, 48 | "totalItems": 10, 49 | "totalPages": 1, 50 | }, 51 | "loading": Object { 52 | "global": "Stop", 53 | }, 54 | }, 55 | "views": Object { 56 | "app": Object { 57 | "Main": 1, 58 | }, 59 | "videos": Object { 60 | "Main": 1, 61 | }, 62 | }, 63 | }, 64 | "html": "
  • video-item1
  • video-item2
共10条,第1/1页
", 65 | } 66 | `; 67 | -------------------------------------------------------------------------------- /test/__snapshots__/ssr_render.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ssr failed:/photos?search={title:error} 1`] = ` 4 | Object { 5 | "data": Object { 6 | "app": Object { 7 | "curUser": Object { 8 | "avatarUrl": "/imgs/1.jpg", 9 | "hasLogin": true, 10 | "uid": "1", 11 | "username": "test", 12 | }, 13 | "isModule": true, 14 | "loading": Object { 15 | "global": "Stop", 16 | "login": "Stop", 17 | }, 18 | "ssrError": "获取列表失败!", 19 | }, 20 | "photos": Object { 21 | "isModule": true, 22 | }, 23 | "router": Object { 24 | "action": "POP", 25 | "location": Object { 26 | "hash": "", 27 | "pathname": "/photos", 28 | "search": "?search={\\"title\\":\\"error\\"}", 29 | "state": undefined, 30 | }, 31 | }, 32 | "views": Object {}, 33 | }, 34 | "html": "
获取列表失败!
", 35 | "ssrInitStoreKey": "reactCoatInitStore", 36 | } 37 | `; 38 | 39 | exports[`ssr success:/photos?search={title:success} 1`] = ` 40 | Object { 41 | "data": Object { 42 | "app": Object { 43 | "curUser": Object { 44 | "avatarUrl": "/imgs/1.jpg", 45 | "hasLogin": true, 46 | "uid": "1", 47 | "username": "test", 48 | }, 49 | "isModule": true, 50 | "loading": Object { 51 | "global": "Stop", 52 | "login": "Stop", 53 | }, 54 | }, 55 | "photos": Object { 56 | "isModule": true, 57 | "listItems": Array [ 58 | Object { 59 | "id": "1", 60 | "title": "item1", 61 | }, 62 | Object { 63 | "id": "2", 64 | "title": "item2", 65 | }, 66 | ], 67 | "listSearch": Object { 68 | "page": 1, 69 | "pageSize": 10, 70 | "title": "success", 71 | }, 72 | "listSummary": Object { 73 | "page": 1, 74 | "pageSize": 10, 75 | "totalItems": 10, 76 | "totalPages": 1, 77 | }, 78 | }, 79 | "router": Object { 80 | "action": "POP", 81 | "location": Object { 82 | "hash": "", 83 | "pathname": "/photos", 84 | "search": "?search={\\"title\\":\\"success\\"}", 85 | "state": undefined, 86 | }, 87 | }, 88 | "views": Object {}, 89 | }, 90 | "html": "
  • photo-item1
  • photo-item2
共10条,第1/1页
", 91 | "ssrInitStoreKey": "reactCoatInitStore", 92 | } 93 | `; 94 | -------------------------------------------------------------------------------- /test/api.ts: -------------------------------------------------------------------------------- 1 | import {CurUser, LoginRequest, LoginResponse, ListSearch, ListItem, ListSummary} from "./type"; 2 | 3 | export class API { 4 | public getCurUser(): Promise { 5 | return Promise.resolve({uid: "1", username: "test", hasLogin: true, avatarUrl: "/imgs/1.jpg"}); 6 | } 7 | public login(req: LoginRequest): Promise { 8 | return Promise.resolve({ 9 | data: { 10 | uid: "1", 11 | username: "Jimmy", 12 | hasLogin: true, 13 | avatarUrl: "imgs/u1.jpg", 14 | }, 15 | error: null, 16 | }); 17 | } 18 | public searchList(listSearch: ListSearch): Promise<{listItems: ListItem[]; listSummary: ListSummary}> { 19 | if (listSearch.title === "error") { 20 | return Promise.reject(new Error("获取列表失败!")); 21 | } else if (listSearch.title === "exception") { 22 | return Promise.reject(new Error("服务器内部错误!")); 23 | } else { 24 | return Promise.resolve({ 25 | listItems: [{id: "1", title: "item1"}, {id: "2", title: "item2"}], 26 | listSummary: { 27 | page: 1, 28 | pageSize: 10, 29 | totalItems: 10, 30 | totalPages: 1, 31 | }, 32 | }); 33 | } 34 | } 35 | } 36 | 37 | export const api = new API(); 38 | -------------------------------------------------------------------------------- /test/client.ts: -------------------------------------------------------------------------------- 1 | import {Middleware} from "redux"; 2 | import {ReactElement} from "react"; 3 | import {buildApp} from "index"; 4 | import {ModuleNames} from "./modules/names"; 5 | import {moduleGetter} from "./modules"; 6 | 7 | export default function render(path: string, middlewares: Middleware[], container: (comp: ReactElement) => void) { 8 | history.replaceState({}, "Test", `http://localhost${path}`); 9 | return buildApp( 10 | moduleGetter, 11 | ModuleNames.app, 12 | { 13 | middlewares, 14 | }, 15 | container, 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /test/csr_render.test.ts: -------------------------------------------------------------------------------- 1 | import {mount} from "enzyme"; 2 | import client from "./client"; 3 | 4 | test("spa success:/videos?search={title:success}", done => { 5 | expect.assertions(2); 6 | const actions: string[] = []; 7 | const logerMiddleware = ({dispatch}: {dispatch: Function}) => (next: Function) => (originalAction: {type: string}) => { 8 | actions.push(originalAction.type); 9 | return next(originalAction); 10 | }; 11 | const store = client('/videos?search={"title":"success"}', [logerMiddleware], app => { 12 | const wrapper = mount(app); 13 | setTimeout(() => { 14 | expect({html: wrapper.html(), data: store.getState()}).toMatchSnapshot(); 15 | expect(actions).toEqual(["app/INIT", "app/LOADING", "videos/INIT", "app/UPDATE", "app/LOADING", "@@framework/VIEW_INVALID", "videos/searchList", "videos/LOADING", "videos/UPDATE", "videos/LOADING"]); 16 | done(); 17 | }, 1000); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/modules/app/index.ts: -------------------------------------------------------------------------------- 1 | import model from "./model"; 2 | import * as views from "./views"; 3 | export {views, model}; 4 | -------------------------------------------------------------------------------- /test/modules/app/model.ts: -------------------------------------------------------------------------------- 1 | import {api} from "../../api"; 2 | import {CurUser} from "../../type"; 3 | import {ModuleNames} from "../names"; 4 | import {RootState, moduleGetter} from "../index"; 5 | import {Actions, BaseModuleHandlers, BaseModuleState, effect, exportModel, ERROR, loadModel, LoadingState, reducer} from "index"; 6 | 7 | // 定义本模块的State类型 8 | export interface State extends BaseModuleState { 9 | ssrError?: string; 10 | curUser: CurUser | null; 11 | loading: { 12 | global: LoadingState; 13 | login: LoadingState; 14 | }; 15 | } 16 | 17 | // 定义本模块State的初始值 18 | const initState: State = { 19 | curUser: null, 20 | loading: { 21 | global: LoadingState.Stop, 22 | login: LoadingState.Stop, 23 | }, 24 | }; 25 | 26 | // 定义本模块的Handlers 27 | class ModuleHandlers extends BaseModuleHandlers { 28 | @reducer 29 | protected putCurUser(curUser: CurUser): State { 30 | return {...this.state, curUser}; 31 | } 32 | @reducer 33 | protected putSsrError(ssrError: string): State { 34 | return {...this.state, ssrError}; 35 | } 36 | @effect(null) 37 | protected async [ERROR](error: Error) { 38 | if (error.message === "获取列表失败!") { 39 | // 消化error为正常状态 40 | this.dispatch(this.callThisAction(this.putSsrError, error.message)); 41 | } else { 42 | // 不消化error,继续向上抛出 43 | throw error; 44 | } 45 | } 46 | @effect() 47 | protected async [ModuleNames.app + "/INIT"]() { 48 | const curUser = await api.getCurUser(); 49 | this.updateState({ 50 | curUser, 51 | }); 52 | // photos模块使用ssr渲染,需要手动loadModel 53 | if (this.rootState.router.location.pathname === "/photos") { 54 | await loadModel(moduleGetter.photos).then(subModel => { 55 | return subModel(this.store); 56 | }); 57 | } 58 | } 59 | } 60 | 61 | // 导出本模块的Actions 62 | export type ModuleActions = Actions; 63 | 64 | export default exportModel(ModuleNames.app, ModuleHandlers, initState); 65 | -------------------------------------------------------------------------------- /test/modules/app/views/Main.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {connect} from "react-redux"; 3 | import {Route, Switch} from "react-router-dom"; 4 | import {RootState} from "../../index"; 5 | import {Main as PhotosView} from "../../photos/views"; 6 | import {Main as VideosView} from "../../videos/views"; 7 | 8 | export interface Props { 9 | ssrError?: string; 10 | } 11 | 12 | export class Component extends React.PureComponent { 13 | public render() { 14 | const {ssrError} = this.props; 15 | 16 | if (ssrError) { 17 | return
{ssrError}
; 18 | } else { 19 | return ( 20 |
21 | 22 | 23 | 24 | 25 |
26 | ); 27 | } 28 | } 29 | } 30 | 31 | const mapStateToProps = (state: RootState) => { 32 | const model = state.app!; 33 | return { 34 | ssrError: model.ssrError, 35 | }; 36 | }; 37 | 38 | export default connect(mapStateToProps)(Component); 39 | -------------------------------------------------------------------------------- /test/modules/app/views/index.ts: -------------------------------------------------------------------------------- 1 | import {exportView} from "index"; 2 | import model from "../model"; 3 | import MainComponent from "./Main"; 4 | 5 | export const Main = exportView(MainComponent, model, "Main"); 6 | -------------------------------------------------------------------------------- /test/modules/index.ts: -------------------------------------------------------------------------------- 1 | import {RootState as BaseState} from "index"; 2 | import {ModuleNames} from "./names"; 3 | import * as appModule from "./app"; 4 | import * as photosModule from "./photos"; 5 | import * as videosModule from "./videos"; 6 | 7 | export const moduleGetter = ( any}>(getter: T) => { 8 | return getter as {[key in ModuleNames]: T[key]}; 9 | })({ 10 | app: () => { 11 | return appModule; 12 | }, 13 | photos: () => { 14 | return photosModule; 15 | }, 16 | videos: () => { 17 | return videosModule; 18 | }, 19 | }); 20 | 21 | export type ModuleGetter = typeof moduleGetter; 22 | 23 | export type RootState = BaseState; 24 | -------------------------------------------------------------------------------- /test/modules/names.ts: -------------------------------------------------------------------------------- 1 | export enum ModuleNames { 2 | app = "app", 3 | photos = "photos", 4 | videos = "videos", 5 | } 6 | -------------------------------------------------------------------------------- /test/modules/photos/index.ts: -------------------------------------------------------------------------------- 1 | import model from "./model"; 2 | import * as views from "./views"; 3 | export {views, model}; 4 | -------------------------------------------------------------------------------- /test/modules/photos/model.ts: -------------------------------------------------------------------------------- 1 | import {api} from "../../api"; 2 | import {ListSearch, ListItem, ListSummary} from "../../type"; 3 | import {parseQuery} from "../../utils"; 4 | import {ModuleNames} from "../names"; 5 | import {RootState} from "../index"; 6 | import {Actions, BaseModuleHandlers, BaseModuleState, effect, exportModel, VIEW_INVALID} from "index"; 7 | 8 | // 定义本模块的State类型 9 | export interface State extends BaseModuleState { 10 | listSearch?: ListSearch; 11 | listItems?: ListItem[]; 12 | listSummary?: ListSummary; 13 | } 14 | 15 | export const defaultListSearch: ListSearch = { 16 | title: "", 17 | page: 1, 18 | pageSize: 10, 19 | }; 20 | 21 | // 定义本模块State的初始值 22 | const initState: State = {}; 23 | 24 | // 定义本模块的Handlers 25 | class ModuleHandlers extends BaseModuleHandlers { 26 | @effect() 27 | public async searchList(options: Partial = {}) { 28 | const listSearch: ListSearch = {...(this.state.listSearch || defaultListSearch), ...options}; 29 | const {listItems, listSummary} = await api.searchList(listSearch); 30 | this.updateState({listSearch, listItems, listSummary}); 31 | } 32 | protected async parseRouter() { 33 | const {search} = this.rootState.router.location; 34 | const listSearch = parseQuery("search", search, defaultListSearch); 35 | await this.dispatch(this.actions.searchList(listSearch)); 36 | } 37 | @effect(null) 38 | protected async [VIEW_INVALID]() { 39 | const views = this.rootState.views; 40 | if (views.photos) { 41 | await this.parseRouter(); 42 | } 43 | } 44 | @effect() 45 | protected async [ModuleNames.photos + "/INIT"]() { 46 | await this.parseRouter(); 47 | } 48 | } 49 | 50 | // 导出本模块的Actions 51 | export type ModuleActions = Actions; 52 | 53 | export default exportModel(ModuleNames.photos, ModuleHandlers, initState); 54 | -------------------------------------------------------------------------------- /test/modules/photos/views/Main.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {connect} from "react-redux"; 3 | import {RootState} from "../../index"; 4 | import {ListSearch, ListItem, ListSummary} from "../../../type"; 5 | 6 | export interface Props { 7 | listSearch: ListSearch | undefined; 8 | listItems: ListItem[] | undefined; 9 | listSummary: ListSummary | undefined; 10 | } 11 | 12 | export class Component extends React.PureComponent { 13 | public render() { 14 | const {listSearch, listItems, listSummary} = this.props; 15 | 16 | if (listItems && listSearch) { 17 | return ( 18 |
19 |
    20 | {listItems.map(item => ( 21 |
  • photo-{item.title}
  • 22 | ))} 23 |
24 | {listSummary &&
{`共${listSummary.totalItems}条,第${listSummary.page}/${listSummary.totalPages}页`}
} 25 |
26 | ); 27 | } else { 28 | return
loading...
; 29 | } 30 | } 31 | } 32 | 33 | const mapStateToProps = (state: RootState) => { 34 | const model = state.photos!; 35 | return { 36 | listSearch: model.listSearch, 37 | listItems: model.listItems, 38 | listSummary: model.listSummary, 39 | }; 40 | }; 41 | 42 | export default connect(mapStateToProps)(Component); 43 | -------------------------------------------------------------------------------- /test/modules/photos/views/index.ts: -------------------------------------------------------------------------------- 1 | import {exportView} from "index"; 2 | import model from "../model"; 3 | import MainComponent from "./Main"; 4 | 5 | export const Main = exportView(MainComponent, model, "Main"); 6 | -------------------------------------------------------------------------------- /test/modules/videos/index.ts: -------------------------------------------------------------------------------- 1 | import model from "./model"; 2 | import * as views from "./views"; 3 | export {views, model}; 4 | -------------------------------------------------------------------------------- /test/modules/videos/model.ts: -------------------------------------------------------------------------------- 1 | import {api} from "../../api"; 2 | import {ListSearch, ListItem, ListSummary} from "../../type"; 3 | import {parseQuery} from "../../utils"; 4 | import {ModuleNames} from "../names"; 5 | import {RootState} from "../index"; 6 | import {Actions, BaseModuleHandlers, BaseModuleState, effect, exportModel, VIEW_INVALID} from "index"; 7 | 8 | // 定义本模块的State类型 9 | export interface State extends BaseModuleState { 10 | listSearch?: ListSearch; 11 | listItems?: ListItem[]; 12 | listSummary?: ListSummary; 13 | } 14 | 15 | export const defaultListSearch: ListSearch = { 16 | title: "", 17 | page: 1, 18 | pageSize: 10, 19 | }; 20 | 21 | // 定义本模块State的初始值 22 | const initState: State = {}; 23 | 24 | // 定义本模块的Handlers 25 | class ModuleHandlers extends BaseModuleHandlers { 26 | @effect() 27 | public async searchList(options: Partial = {}) { 28 | const listSearch: ListSearch = {...(this.state.listSearch || defaultListSearch), ...options}; 29 | const {listItems, listSummary} = await api.searchList(listSearch); 30 | this.updateState({listSearch, listItems, listSummary}); 31 | } 32 | protected async parseRouter() { 33 | const {search} = this.rootState.router.location; 34 | const listSearch = parseQuery("search", search, defaultListSearch); 35 | await this.dispatch(this.actions.searchList(listSearch)); 36 | } 37 | @effect(null) 38 | protected async [VIEW_INVALID]() { 39 | const views = this.rootState.views; 40 | if (views.videos) { 41 | await this.parseRouter(); 42 | } 43 | } 44 | } 45 | 46 | // 导出本模块的Actions 47 | export type ModuleActions = Actions; 48 | 49 | export default exportModel(ModuleNames.videos, ModuleHandlers, initState); 50 | -------------------------------------------------------------------------------- /test/modules/videos/views/Main.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {connect} from "react-redux"; 3 | import {RootState} from "../../index"; 4 | import {ListSearch, ListItem, ListSummary} from "../../../type"; 5 | 6 | export interface Props { 7 | listSearch: ListSearch | undefined; 8 | listItems: ListItem[] | undefined; 9 | listSummary: ListSummary | undefined; 10 | } 11 | 12 | export class Component extends React.PureComponent { 13 | public render() { 14 | const {listSearch, listItems, listSummary} = this.props; 15 | 16 | if (listItems && listSearch) { 17 | return ( 18 |
19 |
    20 | {listItems.map(item => ( 21 |
  • video-{item.title}
  • 22 | ))} 23 |
24 | {listSummary &&
{`共${listSummary.totalItems}条,第${listSummary.page}/${listSummary.totalPages}页`}
} 25 |
26 | ); 27 | } else { 28 | return
loading...
; 29 | } 30 | } 31 | } 32 | 33 | const mapStateToProps = (state: RootState) => { 34 | const model = state.videos!; 35 | return { 36 | listSearch: model.listSearch, 37 | listItems: model.listItems, 38 | listSummary: model.listSummary, 39 | }; 40 | }; 41 | 42 | export default connect(mapStateToProps)(Component); 43 | -------------------------------------------------------------------------------- /test/modules/videos/views/index.ts: -------------------------------------------------------------------------------- 1 | import {exportView} from "index"; 2 | import model from "../model"; 3 | import MainComponent from "./Main"; 4 | 5 | export const Main = exportView(MainComponent, model, "Main"); 6 | -------------------------------------------------------------------------------- /test/server.ts: -------------------------------------------------------------------------------- 1 | import {Middleware} from "redux"; 2 | import {renderApp} from "index"; 3 | import {ModuleNames} from "./modules/names"; 4 | import {moduleGetter} from "./modules"; 5 | 6 | export default function render(path: string, middlewares: Middleware[]): Promise { 7 | return renderApp(moduleGetter, ModuleNames.app, [path], {middlewares}); 8 | } 9 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | const Enzyme = require("enzyme"); 2 | const Adapter = require("enzyme-adapter-react-16"); 3 | 4 | Enzyme.configure({adapter: new Adapter()}); 5 | -------------------------------------------------------------------------------- /test/ssr_render.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | import server from "./server"; 6 | 7 | test("ssr success:/photos?search={title:success}", () => { 8 | const actions: string[] = []; 9 | const logerMiddleware = ({dispatch}: {dispatch: Function}) => (next: Function) => (originalAction: {type: string}) => { 10 | actions.push(originalAction.type); 11 | return next(originalAction); 12 | }; 13 | expect.assertions(2); 14 | return server('/photos?search={"title":"success"}', [logerMiddleware]).then(result => { 15 | // 不验证 connected-react-router 自动加上的一个随机数 key 16 | delete result.data.router.location.key; 17 | expect(result).toMatchSnapshot(); 18 | expect(actions).toEqual(["app/INIT", "app/UPDATE", "photos/INIT", "photos/searchList", "photos/UPDATE"]); 19 | }); 20 | }); 21 | 22 | test("ssr failed:/photos?search={title:error}", () => { 23 | const actions: string[] = []; 24 | const logerMiddleware = ({dispatch}: {dispatch: Function}) => (next: Function) => (originalAction: {type: string}) => { 25 | actions.push(originalAction.type); 26 | return next(originalAction); 27 | }; 28 | expect.assertions(2); 29 | return server('/photos?search={"title":"error"}', [logerMiddleware]).then(result => { 30 | // 不验证 connected-react-router 自动加上的一个随机数 key 31 | delete result.data.router.location.key; 32 | expect(result).toMatchSnapshot(); 33 | expect(actions).toEqual(["app/INIT", "app/UPDATE", "photos/INIT", "photos/searchList", "@@framework/ERROR", "app/putSsrError"]); 34 | }); 35 | }); 36 | 37 | test("ssr exception:/photos?search={title:exception}", () => { 38 | const actions: string[] = []; 39 | const logerMiddleware = ({dispatch}: {dispatch: Function}) => (next: Function) => (originalAction: {type: string}) => { 40 | actions.push(originalAction.type); 41 | return next(originalAction); 42 | }; 43 | expect.assertions(2); 44 | return server('/photos?search={"title":"exception"}', [logerMiddleware]).catch(e => { 45 | expect(e.message).toBe("服务器内部错误!"); 46 | expect(actions).toEqual(["app/INIT", "app/UPDATE", "photos/INIT", "photos/searchList", "@@framework/ERROR"]); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/type.ts: -------------------------------------------------------------------------------- 1 | export interface ErrorType { 2 | code: Code; 3 | message: string; 4 | detail?: Detail; 5 | } 6 | export interface DefaultResult { 7 | data: Data; 8 | error: Error | null; 9 | } 10 | 11 | export interface CurUser { 12 | uid: string; 13 | username: string; 14 | hasLogin: boolean; 15 | avatarUrl: string; 16 | } 17 | export interface LoginRequest { 18 | username: string; 19 | password: string; 20 | } 21 | export type LoginErrorCode = "passwordWrong" | "usernameHasExisted"; 22 | export const loginErrorCode = { 23 | passwordWrong: "passwordWrong", 24 | usernameHasExisted: "usernameHasExisted", 25 | }; 26 | export type LoginResponse = DefaultResult; 27 | 28 | export interface ListSearch { 29 | page: number; 30 | pageSize: number; 31 | title: string; 32 | } 33 | export interface ListItem { 34 | id: string; 35 | title: string; 36 | } 37 | export interface ListSummary { 38 | page: number; 39 | pageSize: number; 40 | totalItems: number; 41 | totalPages: number; 42 | } 43 | -------------------------------------------------------------------------------- /test/utils.ts: -------------------------------------------------------------------------------- 1 | export function parseQuery(key: string, search: string, defArgs: A): A { 2 | const str = key + "="; 3 | let [, query] = search.split(str); 4 | if (query) { 5 | query = query.split("&")[0]; 6 | } 7 | if (query) { 8 | const args = JSON.parse(unescape(query)); 9 | if (defArgs) { 10 | return {...defArgs, ...args}; 11 | } else { 12 | return args; 13 | } 14 | } else { 15 | return defArgs; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["./src"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.jest.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./src", 4 | "outDir": "./libs", 5 | "module": "esnext", 6 | "target": "es5", 7 | "moduleResolution": "node", 8 | "sourceMap": false, 9 | "inlineSources": false, 10 | "declaration": true, 11 | "strict": true, 12 | "allowSyntheticDefaultImports": false, 13 | "esModuleInterop": false, 14 | "importHelpers": true, 15 | "newLine": "LF", 16 | "jsx": "react", 17 | "lib": ["dom", "es2017"], 18 | "removeComments": true, 19 | "experimentalDecorators": true, 20 | "forceConsistentCasingInFileNames": true, 21 | "suppressImplicitAnyIndexErrors": true, 22 | "noImplicitReturns": true, 23 | "noUnusedLocals": true 24 | }, 25 | "include": ["./src", "./test"] 26 | } 27 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"], 3 | "rulesDirectory": ["tslint-plugin-prettier"], 4 | "rules": { 5 | "prettier": true, 6 | "interface-over-type-literal": false, 7 | "interface-name": [true, "never-prefix"], 8 | "object-literal-sort-keys": false, 9 | "ordered-imports": false, 10 | "max-line-length": [true, 300], 11 | "member-access": false, 12 | "only-arrow-functions": false, 13 | "max-classes-per-file": false, 14 | "no-empty-interface": false, 15 | "jsx-no-multiline-js": false, 16 | "arrow-parens": [true, "ban-single-arg-parens"], 17 | "no-var-requires": false, 18 | "jsx-boolean-value": false, 19 | "no-console": false, 20 | "no-namespace": false, 21 | "member-ordering": false, 22 | "ban-types": false, 23 | "no-string-literal": false, 24 | "semicolon": [true, "always", "ignore-bound-class-methods"] 25 | } 26 | } 27 | --------------------------------------------------------------------------------