├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── .nvmrc ├── .travis.yml ├── CHANGELOG.md ├── LATESTLOG.md ├── LICENSE ├── README.md ├── devtools.js ├── examples ├── cart-create │ ├── README.md │ ├── cart │ │ ├── cart-list.jsx │ │ ├── index.js │ │ └── store.js │ ├── config.js │ ├── index.css │ ├── index.html │ ├── index.js │ ├── list │ │ ├── index.js │ │ └── store.js │ └── store.js ├── cart-inject │ ├── README.md │ ├── components │ │ ├── cart-list.jsx │ │ ├── cart.js │ │ └── list.js │ ├── config.js │ ├── index.css │ ├── index.html │ ├── index.js │ └── store.js ├── cart │ ├── README.md │ ├── components │ │ ├── cart-list.jsx │ │ ├── cart.js │ │ └── list.js │ ├── config.js │ ├── index.css │ ├── index.html │ ├── index.js │ └── store.js ├── counter │ ├── index.html │ └── index.js ├── pure │ ├── index.html │ └── index.js ├── scenes │ ├── index.html │ └── index.js └── todo-mvc │ ├── index.html │ └── index.js ├── package-lock.json ├── package.json ├── request.js ├── router.js ├── scripts ├── notice.js ├── release │ ├── changelog.js │ ├── index.js │ └── notice.js └── utils.js ├── src ├── compose.jsx ├── connect.jsx ├── data-source.js ├── events.js ├── hooks.js ├── hot-render.jsx ├── index.js ├── inject.jsx ├── meta.js ├── plugins │ ├── devtools.js │ ├── route.js │ └── set-values.js ├── provider.js ├── proxy.js ├── render.jsx ├── route.jsx ├── store.js └── utils.js ├── test ├── case.spec.js ├── hooks.spec.js ├── render.spec.js ├── roy.spec.js └── tojson.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env","es2015","react","stage-0"], 3 | "plugins": ["babel-plugin-add-module-exports", "transform-class-properties", "transform-async-to-generator", "transform-decorators-legacy"] 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | # Apply for all files 8 | [*] 9 | 10 | charset = utf-8 11 | 12 | indent_style = space 13 | indent_size = 4 14 | 15 | end_of_line = lf 16 | insert_final_newline = true 17 | trim_trailing_whitespace = true 18 | 19 | # package.json 20 | [package.json] 21 | indent_size = 2 22 | indent_style = space 23 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "env": { 4 | "browser": true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | 9 | "globals": { 10 | "document": false, 11 | "escape": false, 12 | "navigator": false, 13 | "unescape": false, 14 | "window": false, 15 | "describe": true, 16 | "before": true, 17 | "it": true, 18 | "expect": true, 19 | "sinon": true 20 | }, 21 | 22 | "parser": "babel-eslint", 23 | 24 | "plugins": [ 25 | 26 | ], 27 | 28 | "rules": { 29 | "block-scoped-var": 2, 30 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 31 | "camelcase": [2, { "properties": "always" }], 32 | "comma-dangle": [2, "never"], 33 | "comma-spacing": [2, { "before": false, "after": true }], 34 | "comma-style": [2, "last"], 35 | "complexity": 0, 36 | "consistent-return": 2, 37 | "consistent-this": 0, 38 | "curly": [2, "multi-line"], 39 | "default-case": 0, 40 | "dot-location": [2, "property"], 41 | "dot-notation": 0, 42 | "eol-last": 2, 43 | "eqeqeq": [2, "allow-null"], 44 | "func-names": 0, 45 | "func-style": 0, 46 | "generator-star-spacing": [2, "both"], 47 | "guard-for-in": 0, 48 | "handle-callback-err": [2, "^(err|error|anySpecificError)$" ], 49 | "indent": [2, 4, { "SwitchCase": 1 }], 50 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 51 | "keyword-spacing": [2, {"before": true, "after": true}], 52 | "linebreak-style": 0, 53 | "max-depth": 0, 54 | "max-len": [2, 200, 4], 55 | "max-nested-callbacks": 0, 56 | "max-params": 0, 57 | "max-statements": 0, 58 | "new-cap": [2, { "newIsCap": true, "capIsNew": false }], 59 | "newline-after-var": 0, 60 | "new-parens": 2, 61 | "no-alert": 0, 62 | "no-array-constructor": 2, 63 | "no-bitwise": 0, 64 | "no-caller": 2, 65 | "no-catch-shadow": 0, 66 | "no-cond-assign": 2, 67 | "no-console": 0, 68 | "no-constant-condition": 0, 69 | "no-continue": 0, 70 | "no-control-regex": 2, 71 | "no-debugger": 2, 72 | "no-delete-var": 2, 73 | "no-div-regex": 0, 74 | "no-dupe-args": 2, 75 | "no-dupe-keys": 2, 76 | "no-duplicate-case": 2, 77 | "no-else-return": 2, 78 | "no-empty": 0, 79 | "no-empty-character-class": 2, 80 | "no-eq-null": 0, 81 | "no-eval": 2, 82 | "no-ex-assign": 2, 83 | "no-extend-native": 2, 84 | "no-extra-bind": 2, 85 | "no-extra-boolean-cast": 2, 86 | "no-extra-parens": 0, 87 | "no-extra-semi": 0, 88 | "no-extra-strict": 0, 89 | "no-fallthrough": 2, 90 | "no-floating-decimal": 2, 91 | "no-func-assign": 2, 92 | "no-implied-eval": 2, 93 | "no-inline-comments": 0, 94 | "no-inner-declarations": [2, "functions"], 95 | "no-invalid-regexp": 2, 96 | "no-irregular-whitespace": 2, 97 | "no-iterator": 2, 98 | "no-label-var": 2, 99 | "no-labels": 2, 100 | "no-lone-blocks": 0, 101 | "no-lonely-if": 0, 102 | "no-loop-func": 0, 103 | "no-mixed-requires": 0, 104 | "no-mixed-spaces-and-tabs": [2, false], 105 | "no-multi-spaces": 2, 106 | "no-multi-str": 2, 107 | "no-multiple-empty-lines": [2, { "max": 1 }], 108 | "no-native-reassign": 2, 109 | "no-negated-in-lhs": 2, 110 | "no-nested-ternary": 0, 111 | "no-new": 2, 112 | "no-new-func": 2, 113 | "no-new-object": 2, 114 | "no-new-require": 2, 115 | "no-new-wrappers": 2, 116 | "no-obj-calls": 2, 117 | "no-octal": 2, 118 | "no-octal-escape": 2, 119 | "no-path-concat": 0, 120 | "no-plusplus": 0, 121 | "no-process-env": 0, 122 | "no-process-exit": 0, 123 | "no-proto": 2, 124 | "no-redeclare": 2, 125 | "no-regex-spaces": 2, 126 | "no-reserved-keys": 0, 127 | "no-restricted-modules": 0, 128 | "no-return-assign": 2, 129 | "no-script-url": 0, 130 | "no-self-compare": 2, 131 | "no-sequences": 2, 132 | "no-shadow": 0, 133 | "no-shadow-restricted-names": 2, 134 | "no-spaced-func": 2, 135 | "no-sparse-arrays": 2, 136 | "no-sync": 0, 137 | "no-ternary": 0, 138 | "no-throw-literal": 2, 139 | "no-trailing-spaces": 2, 140 | "no-undef": 2, 141 | "no-undef-init": 2, 142 | "no-undefined": 0, 143 | "no-underscore-dangle": 0, 144 | "no-unneeded-ternary": 2, 145 | "no-unreachable": 2, 146 | "no-unused-expressions": 0, 147 | "no-unused-vars": [2, { "vars": "all", "args": "none" }], 148 | "no-use-before-define": 2, 149 | "no-var": 0, 150 | "no-void": 0, 151 | "no-warning-comments": 0, 152 | "no-with": 2, 153 | "one-var": 0, 154 | "operator-assignment": 0, 155 | "operator-linebreak": [2, "after"], 156 | "padded-blocks": 0, 157 | "quote-props": 0, 158 | "quotes": [2, "single", "avoid-escape"], 159 | "radix": 2, 160 | "semi": [2, "always"], 161 | "semi-spacing": 0, 162 | "sort-vars": 0, 163 | "space-before-blocks": [2, "always"], 164 | "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}], 165 | "space-in-brackets": 0, 166 | "space-in-parens": [2, "never"], 167 | "space-infix-ops": 2, 168 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 169 | "spaced-comment": [2, "always"], 170 | "strict": 0, 171 | "use-isnan": 2, 172 | "valid-jsdoc": 0, 173 | "valid-typeof": 2, 174 | "vars-on-top": 2, 175 | "wrap-iife": [2, "any"], 176 | "wrap-regex": 0, 177 | "yoda": [2, "never"] 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | .nyc_output 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 29 | node_modules 30 | 31 | # Remove some common IDE working directories 32 | .idea 33 | .vscode 34 | 35 | .DS_Store 36 | 37 | lib/ 38 | 39 | .cache/ 40 | .examples/test.html 41 | .examples/index.js 42 | dist/ 43 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .cache/ 2 | .nyc_output/ 3 | coverage/ 4 | examples/ 5 | scripts/ 6 | test/ 7 | src/ 8 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v6.10 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | 4 | ## [2.0.7](https://github.com/windyGex/roy/compare/2.0.6...2.0.7) (2020-12-31) 5 | 6 | 7 | ### Bug Fixes 8 | 9 | * context is null ([9cb0165](https://github.com/windyGex/roy/commit/9cb0165)) 10 | 11 | 12 | 13 | 14 | 15 | ## [2.0.6](https://github.com/windyGex/roy/compare/2.0.5...2.0.6) (2020-12-30) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * $raw support ([f580e36](https://github.com/windyGex/roy/commit/f580e36)) 21 | 22 | 23 | 24 | 25 | 26 | ## [2.0.5](https://github.com/windyGex/roy/compare/2.0.4...2.0.5) (2020-12-24) 27 | 28 | 29 | ### Bug Fixes 30 | 31 | * Close [#13](https://github.com/windyGex/roy/issues/13) ([d033073](https://github.com/windyGex/roy/commit/d033073)) 32 | 33 | 34 | 35 | 36 | 37 | ## [2.0.1](https://github.com/windyGex/roy/compare/2.0.0...2.0.1) (2020-06-09) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * lost deps for using connect api ([66133f7](https://github.com/windyGex/roy/commit/66133f7)) 43 | 44 | 45 | 46 | 47 | # [2.0.0](https://github.com/windyGex/roy/compare/1.3.1...2.0.0) (2020-06-04) 48 | 49 | 50 | ### Features 51 | 52 | * support hooks ([ce8b20f](https://github.com/windyGex/roy/commit/ce8b20f)) 53 | 54 | 55 | ### BR 56 | 57 | * Only support React@16+ 58 | 59 | 60 | 61 | 62 | 63 | ## [1.3.1](https://github.com/windyGex/roy/compare/1.3.0...1.3.1) (2020-04-15) 64 | 65 | 66 | 67 | 68 | 69 | # [1.3.0](https://github.com/windyGex/roy/compare/1.2.0...1.3.0) (2019-11-11) 70 | 71 | 72 | 73 | 74 | 75 | # [1.2.0](https://github.com/windyGex/roy/compare/1.1.0...1.2.0) (2019-07-31) 76 | 77 | 78 | ### Bug Fixes 79 | 80 | * umd pkg ([9a4ec40](https://github.com/windyGex/roy/commit/9a4ec40)) 81 | * umd pkg error ([e7044ce](https://github.com/windyGex/roy/commit/e7044ce)) 82 | 83 | 84 | ### Features 85 | 86 | * instance for child ([3ec119e](https://github.com/windyGex/roy/commit/3ec119e)) 87 | 88 | 89 | 90 | 91 | 92 | # [1.1.0](https://github.com/windyGex/roy/compare/1.0.1...1.1.0) (2019-05-08) 93 | 94 | 95 | ### Bug Fixes 96 | 97 | * Avoid nested prototype method ([9541173](https://github.com/windyGex/roy/commit/9541173)) 98 | * avoid throw error when node is null ([e7b7113](https://github.com/windyGex/roy/commit/e7b7113)) 99 | * Collect dependency for componentDidMount ([18cea51](https://github.com/windyGex/roy/commit/18cea51)) 100 | * connect dependenecy, close [#3](https://github.com/windyGex/roy/issues/3). ([44f7864](https://github.com/windyGex/roy/commit/44f7864)) 101 | * dependency error ([f254359](https://github.com/windyGex/roy/commit/f254359)) 102 | * format code ([7e8d283](https://github.com/windyGex/roy/commit/7e8d283)) 103 | * Get object by array method error ([1260e17](https://github.com/windyGex/roy/commit/1260e17)) 104 | * Give priority to get value by key, Close [#6](https://github.com/windyGex/roy/issues/6) ([d016843](https://github.com/windyGex/roy/commit/d016843)) 105 | * Give priority to get value by key, Close [#6](https://github.com/windyGex/roy/issues/6) ([e0fb159](https://github.com/windyGex/roy/commit/e0fb159)) 106 | * proxy for array ([f21288c](https://github.com/windyGex/roy/commit/f21288c)) 107 | * revert code ([f344972](https://github.com/windyGex/roy/commit/f344972)) 108 | * More strict instance access ([0943ac4](https://github.com/windyGex/roy/commit/0943ac4)) 109 | * Trigger change when using set and any others. ([a1e9e36](https://github.com/windyGex/roy/commit/a1e9e36)) 110 | 111 | 112 | ### Features 113 | 114 | * Add pure for optimizing pefermance ([f91c971](https://github.com/windyGex/roy/commit/f91c971)) 115 | * Support shouldComponentUpdate when pure is true ([7058879](https://github.com/windyGex/roy/commit/7058879)) 116 | * Add and toJSON for proxy object ([b654959](https://github.com/windyGex/roy/commit/b654959)) 117 | * add throttle support ([7340024](https://github.com/windyGex/roy/commit/7340024)) 118 | * support connect store from inject ([afeb646](https://github.com/windyGex/roy/commit/afeb646)) 119 | * support mutiple args for connect, Close [#7](https://github.com/windyGex/roy/issues/7) ([021087d](https://github.com/windyGex/roy/commit/021087d)) 120 | * support mutiple args for connect, Close [#7](https://github.com/windyGex/roy/issues/7) ([da6be8a](https://github.com/windyGex/roy/commit/da6be8a)) 121 | * support takeLatest, Close [#1](https://github.com/windyGex/roy/issues/1) ([953733b](https://github.com/windyGex/roy/commit/953733b)) 122 | * support takeLatest, Close [#1](https://github.com/windyGex/roy/issues/1) ([220ee77](https://github.com/windyGex/roy/commit/220ee77)) 123 | 124 | 125 | 126 | 127 | 128 | ## [1.0.1](https://github.com/windyGex/roy/compare/1.0.0...1.0.1) (2019-02-22) 129 | 130 | 131 | ### Bug Fixes 132 | 133 | * Avoid nested prototype method ([c8770ee](https://github.com/windyGex/roy/commit/c8770ee)) 134 | * Collect dependency for componentDidMount ([d604107](https://github.com/windyGex/roy/commit/d604107)) 135 | 136 | 137 | 138 | 139 | 140 | # [1.0.0](https://github.com/windyGex/roy/compare/1.0.0-beta6...1.0.0) (2019-02-12) 141 | 142 | 143 | ### Features 144 | 145 | * support connect store from inject ([fc6bc75](https://github.com/windyGex/roy/commit/fc6bc75)) 146 | 147 | 148 | 149 | 150 | 151 | # [1.0.0-beta6](https://github.com/windyGex/roy/compare/1.0.0-beta5...1.0.0-beta6) (2019-01-03) 152 | 153 | 154 | 155 | 156 | 157 | # [1.0.0-beta5](https://github.com/windyGex/roy/compare/1.0.0-beta4...1.0.0-beta5) (2018-12-10) 158 | 159 | 160 | ### Bug Fixes 161 | 162 | * avoid throw error when node is null ([fb0538b](https://github.com/windyGex/roy/commit/fb0538b)) 163 | * connect dependenecy, close [#3](https://github.com/windyGex/roy/issues/3). ([2f64b50](https://github.com/windyGex/roy/commit/2f64b50)) 164 | * Get object by array method error ([0b3f5d5](https://github.com/windyGex/roy/commit/0b3f5d5)) 165 | 166 | 167 | 168 | 169 | 170 | # [1.0.0-beta4](https://github.com/windyGex/roy/compare/1.0.0-beta3...1.0.0-beta4) (2018-12-05) 171 | 172 | 173 | ### Features 174 | 175 | * Add and toJSON for proxy object ([3b9bcc1](https://github.com/windyGex/roy/commit/3b9bcc1)) 176 | 177 | 178 | 179 | 180 | 181 | # [1.0.0-beta3](https://github.com/windyGex/roy/compare/1.0.0-beta2...1.0.0-beta3) (2018-11-28) 182 | 183 | 184 | ### Bug Fixes 185 | 186 | * More strict instance access ([3c0b172](https://github.com/windyGex/roy/commit/3c0b172)) 187 | 188 | 189 | ### Features 190 | 191 | * Add pure for optimizing pefermance ([32fd168](https://github.com/windyGex/roy/commit/32fd168)) 192 | * Support shouldComponentUpdate when pure is true ([7390a17](https://github.com/windyGex/roy/commit/7390a17)) 193 | 194 | 195 | 196 | 197 | 198 | # [1.0.0-beta2](https://github.com/windyGex/roy/compare/1.0.0-beta1...1.0.0-beta2) (2018-11-23) 199 | 200 | 201 | ### Bug Fixes 202 | 203 | * dependency error ([27df8e4](https://github.com/windyGex/roy/commit/27df8e4)) 204 | 205 | 206 | 207 | 208 | # [1.0.0-beta1](https://github.com/windyGex/roy/compare/0.6.9...1.0.0-beta1) (2018-11-22) 209 | 210 | 211 | 212 | 213 | 214 | ## [0.6.9](https://github.com/windyGex/roy/compare/0.6.8...0.6.9) (2018-11-16) 215 | 216 | 217 | ### Bug Fixes 218 | 219 | * dependency for proxy ([f9674a6](https://github.com/windyGex/roy/commit/f9674a6)) 220 | * format code ([fc835fc](https://github.com/windyGex/roy/commit/fc835fc)) 221 | * multiple change. ([b1bed62](https://github.com/windyGex/roy/commit/b1bed62)) 222 | * proxy for array ([380d1e4](https://github.com/windyGex/roy/commit/380d1e4)) 223 | * Trigger change when using set and any others. ([b2b5588](https://github.com/windyGex/roy/commit/b2b5588)) 224 | * using proxy instead of defineProperty ([5e3c0c0](https://github.com/windyGex/roy/commit/5e3c0c0)) 225 | 226 | 227 | ### Features 228 | 229 | * add throttle support ([a916c2f](https://github.com/windyGex/roy/commit/a916c2f)) 230 | * using proxy instead of event change ([bf20693](https://github.com/windyGex/roy/commit/bf20693)) 231 | 232 | 233 | 234 | 235 | 236 | ## [0.6.8](https://github.com/windyGex/roy/compare/0.6.7...0.6.8) (2018-11-05) 237 | 238 | 239 | ### Bug Fixes 240 | 241 | * compile error ([f09386a](https://github.com/windyGex/roy/commit/f09386a)) 242 | 243 | 244 | ### Features 245 | 246 | * add transaction function for batch update, [#42883](https://github.com/windyGex/roy/issues/42883) ([f625028](https://github.com/windyGex/roy/commit/f625028)) 247 | 248 | 249 | 250 | 251 | 252 | ## [0.6.8](https://github.com/windyGex/roy/compare/0.6.7...0.6.8) (2018-11-05) 253 | 254 | 255 | ### Bug Fixes 256 | 257 | * compile error ([f09386a](https://github.com/windyGex/roy/commit/f09386a)) 258 | 259 | 260 | 261 | 262 | 263 | ## [0.6.8](https://github.com/windyGex/roy/compare/0.6.7...0.6.8) (2018-11-05) 264 | 265 | 266 | ### Bug Fixes 267 | 268 | * compile error ([f09386a](https://github.com/windyGex/roy/commit/f09386a)) 269 | 270 | 271 | 272 | 273 | ## 0.6.7 / 2018-10-12 274 | 275 | 276 | ### Bug Fixes 277 | 278 | * actions events when mount to global Store. ([4e47e85](https://github.com/windyGex/roy/commit/4e47e85)) 279 | * Add compose hooks. ([cae5bea](https://github.com/windyGex/roy/commit/cae5bea)) 280 | * Add more strict for value to JSON. ([f51b364](https://github.com/windyGex/roy/commit/f51b364)) 281 | * Add name for compose. ([fd947a6](https://github.com/windyGex/roy/commit/fd947a6)) 282 | * Add readme. ([b4783aa](https://github.com/windyGex/roy/commit/b4783aa)) 283 | * Add router for todo-mvc. ([c7aa5c4](https://github.com/windyGex/roy/commit/c7aa5c4)) 284 | * Add scene support. ([3b39914](https://github.com/windyGex/roy/commit/3b39914)) 285 | * devtools error. ([6d64ec1](https://github.com/windyGex/roy/commit/6d64ec1)) 286 | * eslint-error. ([ce94967](https://github.com/windyGex/roy/commit/ce94967)) 287 | * examples error. ([4ca2d7f](https://github.com/windyGex/roy/commit/4ca2d7f)) 288 | * flattern array toJSON undefined ([c23f2fd](https://github.com/windyGex/roy/commit/c23f2fd)) 289 | * Improve readability. ([429f7a2](https://github.com/windyGex/roy/commit/429f7a2)) 290 | * invoke state error when mount mode. ([3353eaa](https://github.com/windyGex/roy/commit/3353eaa)) 291 | * meta ([07185de](https://github.com/windyGex/roy/commit/07185de)) 292 | * model resolve. ([2f78b19](https://github.com/windyGex/roy/commit/2f78b19)) 293 | * modify actions for store. ([3dac4f5](https://github.com/windyGex/roy/commit/3dac4f5)) 294 | * mount store bugs. ([b7499c8](https://github.com/windyGex/roy/commit/b7499c8)) 295 | * observable array bugs. ([f73057e](https://github.com/windyGex/roy/commit/f73057e)) 296 | * package.json. ([2d1f46b](https://github.com/windyGex/roy/commit/2d1f46b)) 297 | * publish files. ([8083867](https://github.com/windyGex/roy/commit/8083867)) 298 | * replace puck using axios ([3e2a842](https://github.com/windyGex/roy/commit/3e2a842)) 299 | * route array support. ([5bfac44](https://github.com/windyGex/roy/commit/5bfac44)) 300 | * route plugin support. ([1ece19a](https://github.com/windyGex/roy/commit/1ece19a)) 301 | * roy support. ([10b3437](https://github.com/windyGex/roy/commit/10b3437)) 302 | * split request package for roy.js ([7edb327](https://github.com/windyGex/roy/commit/7edb327)) 303 | * sth ([977ded9](https://github.com/windyGex/roy/commit/977ded9)) 304 | * toJSON operation. ([c41cef5](https://github.com/windyGex/roy/commit/c41cef5)) 305 | * 问题修复 ([571a88a](https://github.com/windyGex/roy/commit/571a88a)) 306 | 307 | 308 | ### Features 309 | 310 | * Add centerlized store support. ([7b4d9d4](https://github.com/windyGex/roy/commit/7b4d9d4)) 311 | * Add compose function. ([7c6a169](https://github.com/windyGex/roy/commit/7c6a169)) 312 | * Add debug for royjs. ([ec7bd0b](https://github.com/windyGex/roy/commit/ec7bd0b)) 313 | * Add forceUpdate for set. ([fc20870](https://github.com/windyGex/roy/commit/fc20870)) 314 | * Add globalStore mount support. ([15053e8](https://github.com/windyGex/roy/commit/15053e8)) 315 | * Add lib for roy. ([b0ff0e9](https://github.com/windyGex/roy/commit/b0ff0e9)) 316 | * Add mount for submodel. ([55c42cb](https://github.com/windyGex/roy/commit/55c42cb)) 317 | * Add redux devtools support. ([4df25d5](https://github.com/windyGex/roy/commit/4df25d5)) 318 | * Add render function for route. ([d37011b](https://github.com/windyGex/roy/commit/d37011b)) 319 | * Add reset for state. ([1e5bb0c](https://github.com/windyGex/roy/commit/1e5bb0c)) 320 | * Add route decocators support. ([9cc3525](https://github.com/windyGex/roy/commit/9cc3525)) 321 | * Add route plugin support. ([ed1d04c](https://github.com/windyGex/roy/commit/ed1d04c)) 322 | * Add store dataSource support. ([46a85d7](https://github.com/windyGex/roy/commit/46a85d7)) 323 | * Add switch for route support. ([dd27e7e](https://github.com/windyGex/roy/commit/dd27e7e)) 324 | * Add todo examples. ([f5b5ba6](https://github.com/windyGex/roy/commit/f5b5ba6)) 325 | * Adjust arguments order and remove defineProperty foractions. ([e6215c5](https://github.com/windyGex/roy/commit/e6215c5)) 326 | * Provider simple usage. ([4e991e2](https://github.com/windyGex/roy/commit/4e991e2)) 327 | * Split package for react-router. ([6308af9](https://github.com/windyGex/roy/commit/6308af9)) 328 | * support pass store from context. ([a664f74](https://github.com/windyGex/roy/commit/a664f74)) 329 | * using hippo request as default. ([790dd3d](https://github.com/windyGex/roy/commit/790dd3d)) 330 | * Using Object.defineProperty avoid enumerable. ([ab4d815](https://github.com/windyGex/roy/commit/ab4d815)) 331 | * 代码格式优化 ([2d469a2](https://github.com/windyGex/roy/commit/2d469a2)) 332 | * 支持init初始化 ([6ae6ab0](https://github.com/windyGex/roy/commit/6ae6ab0)) 333 | 334 | 335 | 336 | -------------------------------------------------------------------------------- /LATESTLOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## [2.0.7](https://github.com/windyGex/roy/compare/2.0.6...2.0.7) (2020-12-31) 3 | 4 | 5 | ### Bug Fixes 6 | 7 | * context is null ([9cb0165](https://github.com/windyGex/roy/commit/9cb0165)) 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 windy ge 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Roy ![buildStatus](https://travis-ci.org/windyGex/royjs.svg?branch=master) 2 | 3 | A powerful mvvm framework for react. 4 | 5 | ## Install 6 | 7 | ```shell 8 | npm install @royjs/core --save 9 | ``` 10 | 11 | ## Motive 12 | 13 | ![image](https://img.alicdn.com/tfs/TB1rzpgGHGYBuNjy0FoXXciBFXa-627-241.png) 14 | 15 | The state management is nothing more than changing the state from partial to partial sharing, so in an application, each component can be managed corresponding to a state, and only when this part needs to be shared, it is extracted. 16 | 17 | ## Usage 18 | 19 | ### Basic Usage 20 | 21 | ```js 22 | import {Store, inject} from '@royjs/core'; 23 | 24 | const store = new Store({ 25 | state: { 26 | count: 0 27 | }, 28 | actions: { 29 | add(state, payload) { 30 | state.count++; 31 | }, 32 | reduce(state, payload) { 33 | state.count--; 34 | } 35 | } 36 | }); 37 | 38 | @inject(store) 39 | class App extends React.Component { 40 | render() { 41 | const {count} = this.props.state; 42 | return
this.props.dispatch('add')}>{count}
43 | } 44 | } 45 | 46 | ``` 47 | 48 | ### Centralized Store 49 | 50 | ```js 51 | import {Store, connect} from '@royjs/core'; 52 | 53 | const store = new Store({}, { 54 | plugins: [devtools] 55 | }); 56 | 57 | store.create('module1', { 58 | state: { 59 | name: 'module1' 60 | }, 61 | actions: { 62 | change(state, payload){ 63 | state.name = payload; 64 | } 65 | } 66 | }); 67 | 68 | store.create('module2', { 69 | state: { 70 | name: 'module2' 71 | }, 72 | actions: { 73 | change(state, payload){ 74 | state.name = payload; 75 | } 76 | } 77 | }); 78 | 79 | @connect(state => state.module1) 80 | class App extends React.Component { 81 | onClick = () => { 82 | this.props.dispatch('module2.change', 'changed name from module1'); 83 | } 84 | render() { 85 | return
{this.props.name}
86 | } 87 | } 88 | 89 | @connect(state => state.module2) 90 | class App2 extends React.Component { 91 | render() { 92 | return
{this.props.name}
93 | } 94 | } 95 | ``` 96 | 97 | ### Merge localStore to globalStore 98 | 99 | ```js 100 | import {Store, inject, connect} from '@royjs/core'; 101 | 102 | const store = new Store(); 103 | 104 | const subModuleStore = new Store({ 105 | state: { 106 | name: 'subModule' 107 | }, 108 | actions: { 109 | change(state) { 110 | state.name = 'subModuleChanged'; 111 | } 112 | } 113 | }) 114 | @inject(subModuleStore) 115 | class SubModule extends React.Component { 116 | render() { 117 | return
this.props.dispatch('change')}>{this.props.state.name}
118 | } 119 | } 120 | 121 | store.mount('subModule', subModuleStore); 122 | 123 | @connect(state => state.subModule) 124 | class App extends React.Component { 125 | render() { 126 | return
{this.props.name}
127 | } 128 | } 129 | ``` 130 | 131 | ### Async Request 132 | 133 | ```js 134 | import {Store, inject} from '@royjs/core'; 135 | 136 | const store = new Store({ 137 | state: { 138 | count: 0 139 | }, 140 | actions: { 141 | add(state, payload) { 142 | state.count++; 143 | }, 144 | reduce(state, payload) { 145 | state.count--; 146 | }, 147 | fetch(state, payload) { 148 | this.request('./url').then(ret => { 149 | state.dataSource = ret.ds; 150 | }); 151 | } 152 | } 153 | }); 154 | 155 | @inject(store) 156 | class App extends React.Component { 157 | componentDidMount() { 158 | this.props.dispatch('fetch'); 159 | } 160 | render() { 161 | const {dataSource} = this.props.state; 162 | return
this.props.dispatch('add')}>{dataSource}
163 | } 164 | } 165 | ``` 166 | 167 | ## Benchmark 168 | 169 | Test on my macbook pro (Intel Core i7 2.2GHz) 170 | 171 | ![benchmark](https://img.alicdn.com/tfs/TB1n.LgIuSSBuNjy0FlXXbBpVXa-786-140.png) 172 | 173 | ```shell 174 | tnpm run benchmark 175 | ``` 176 | -------------------------------------------------------------------------------- /devtools.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/plugins/devtools'); 2 | -------------------------------------------------------------------------------- /examples/cart-create/README.md: -------------------------------------------------------------------------------- 1 | # Shopping Cart 2 | 3 | 演示了基于create拆分store的示例 4 | -------------------------------------------------------------------------------- /examples/cart-create/cart/cart-list.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from '../../../src'; 3 | 4 | @connect(state => ({ 5 | list: state.cart.list 6 | }), true) 7 | class CartList extends React.Component { 8 | 9 | onSelect(id, e) { 10 | const { checked } = e.target; 11 | this.props.dispatch('cart.select', { 12 | id, 13 | checked 14 | }); 15 | } 16 | onAdd(item) { 17 | this.props.dispatch('cart.onAdd', item); 18 | } 19 | onReduce(item) { 20 | this.props.dispatch('cart.onReduce', item); 21 | } 22 | 23 | renderList(data) { 24 | return data.map(item => { 25 | return ( 26 |
  • 27 |
    28 |
    29 | 34 |
    35 |
    36 |
    37 | 38 |
    39 |
    40 |

    {item.name}

    41 |
    42 | 43 | ¥{item.price} 44 | 45 |
    46 | 库存{item.stock}件 47 |
    48 |
    49 | + 50 |
    51 |
    {item.quantity}
    52 |
    53 | - 54 |
    55 |
    56 |
    57 |
  • 58 | ); 59 | }); 60 | } 61 | render() { 62 | console.log('cartlist, render'); 63 | const { list } = this.props; 64 | if (list.length) { 65 | return ; 66 | } 67 | return ( 68 |
    69 | 这里是空的,快去逛逛吧 70 |
    71 | ); 72 | } 73 | } 74 | 75 | export default CartList; 76 | -------------------------------------------------------------------------------- /examples/cart-create/cart/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from '../../../src'; 3 | import CartList from './cart-list'; 4 | import './store'; 5 | 6 | @connect(state => state.cart) 7 | export default class Cart extends React.Component { 8 | state = { 9 | isEdit: false 10 | }; 11 | 12 | edit = () => { 13 | this.setState({ 14 | isEdit: true 15 | }); 16 | }; 17 | 18 | complete = () => { 19 | this.setState({ 20 | isEdit: false 21 | }); 22 | }; 23 | 24 | selectAll(e) { 25 | this.props.dispatch('cart.selectAll', { 26 | checked: e.target.checked 27 | }); 28 | } 29 | 30 | onRemove = () => { 31 | this.props.dispatch('cart.onRemove'); 32 | }; 33 | renderCart() { 34 | const { list } = this.props; 35 | if (list.length) { 36 | return ; 37 | } 38 | return ( 39 |
    40 | 这里是空的,快去逛逛吧 41 |
    42 | ); 43 | } 44 | render() { 45 | console.log('cart, render'); 46 | const selectedItems = this.props.list.filter(item => item.selected); 47 | const selectedNum = selectedItems.length; 48 | const totalPrice = selectedItems.reduce((total, item) => { 49 | total += item.quantity * item.price; 50 | return total; 51 | }, 0); 52 | const checked = selectedItems.length === this.props.list.length && selectedItems.length > 0; 53 | const { isEdit } = this.state; 54 | return ( 55 |
    56 |
    57 | 购物清单 58 | 59 | {!isEdit ? 编辑 : 完成} 60 | 61 |
    62 |
    63 |
    64 |
    65 |
    66 |
    67 | 68 |
    69 |
    70 | 全选 71 |
    72 | {!isEdit ? ( 73 |
    去结算({selectedNum})
    74 | ) : ( 75 |
    76 | 删除({selectedNum}) 77 |
    78 | )} 79 |
    80 | 合计: 81 | 82 | ¥{totalPrice} 83 | 84 |
    85 |
    86 |
    87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /examples/cart-create/cart/store.js: -------------------------------------------------------------------------------- 1 | import { Store } from '../../../src/'; 2 | import { goods } from '../config'; 3 | 4 | const store = Store.get(); 5 | 6 | store.create('cart', { 7 | state: { 8 | list: [] 9 | }, 10 | actions: { 11 | addCartItem(state, payload) { 12 | const item = state.list.filter(item => item.id === payload.id)[0]; 13 | if (!item) { 14 | state.list.push({ 15 | ...payload, 16 | quantity: 1 17 | }); 18 | } else { 19 | item.quantity++; 20 | } 21 | }, 22 | select(state, payload) { 23 | const item = state.list.filter(item => item.id === payload.id)[0]; 24 | item.selected = payload.checked; 25 | }, 26 | selectAll(state, payload) { 27 | state.list.forEach(item => { 28 | item.selected = payload.checked; 29 | }); 30 | }, 31 | onAdd(state, payload) { 32 | payload.quantity++; 33 | }, 34 | onReduce(state, payload) { 35 | payload.quantity = Math.max(0, --payload.quantity); 36 | }, 37 | onRemove(state, payload) { 38 | const list = state.list.filter(item => !item.selected); 39 | state.list = list; 40 | } 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /examples/cart-create/config.js: -------------------------------------------------------------------------------- 1 | export const category = [ 2 | { id: 0, des: '推荐' }, 3 | { id: 1, des: '母婴' }, 4 | { id: 2, des: '鞋包饰品' }, 5 | { id: 3, des: '食品' }, 6 | { id: 4, des: '数码家电' }, 7 | { id: 5, des: '居家百货' } 8 | ]; 9 | 10 | export const sortMethods = [ 11 | { name: '综合排序', value: 'id' }, 12 | { name: '销量优先', value: 'sales' }, 13 | { name: '价格', value: 'price' } 14 | ]; 15 | 16 | export const goods = [{ 17 | id: 1001, 18 | name: 'Beats EP头戴式耳机', 19 | price: 558, 20 | type: 4, 21 | stock: 128, 22 | sales: 1872, 23 | img: 'http://img11.360buyimg.com/n1/s528x528_jfs/t3109/194/2435573156/46587/e0e867ac/57e10978N87220944.jpg!q70.jpg' 24 | }, { 25 | id: 1002, 26 | name: '雀巢(Nestle)高钙成人奶粉', 27 | price: 60, 28 | type: 3, 29 | stock: 5, 30 | sales: 2374, 31 | img: 'http://m.360buyimg.com/babel/jfs/t5197/28/400249159/97561/304ce550/58ff0dbeN88884779.jpg!q50.jpg.webp' 32 | }, { 33 | id: 1003, 34 | name: '煎炒烹炸一锅多用', 35 | price: 216, 36 | type: 5, 37 | stock: 2, 38 | sales: 351, 39 | ishot: true, 40 | img: 'http://gw.alicdn.com/tps/TB19OfQRXXXXXbmXXXXL6TaGpXX_760x760q90s150.jpg_.webp' 41 | }, { 42 | id: 1004, 43 | name: 'ANNE KLEIN 潮流经典美式轻奢', 44 | price: 585, 45 | type: 2, 46 | stock: 465, 47 | sales: 8191, 48 | img: 'http://gw.alicdn.com/tps/TB1l5psQVXXXXcXaXXXL6TaGpXX_760x760q90s150.jpg_.webp' 49 | }, { 50 | id: 1005, 51 | name: '乐高EV3机器人积木玩具', 52 | price: 3099, 53 | type: 1, 54 | stock: 154, 55 | sales: 165, 56 | img: 'https://m.360buyimg.com/mobilecms/s357x357_jfs/t6490/168/1052550216/653858/9eef28d1/594922a8Nc3afa743.jpg!q50.jpg' 57 | }, { 58 | id: 1006, 59 | name: '全球购 路易威登(Louis Vuitton)新款女士LV印花手袋 M41112', 60 | price: 10967, 61 | type: 2, 62 | stock: 12, 63 | sales: 6, 64 | img: 'https://m.360buyimg.com/n1/s220x220_jfs/t1429/17/1007119837/464370/310392f4/55b5e5bfN75daf703.png!q70.jpg' 65 | }, { 66 | id: 1007, 67 | name: 'Kindle Paperwhite3 黑色经典版电纸书', 68 | price: 805, 69 | type: 4, 70 | stock: 3, 71 | sales: 395, 72 | img: 'http://img12.360buyimg.com/n1/s528x528_jfs/t4954/76/635213328/51972/ec4a3c3c/58e5f717N4031d162.jpg!q70.jpg' 73 | }, { 74 | id: 1008, 75 | name: 'DELSEY 男士双肩背包', 76 | price: 269, 77 | type: 2, 78 | stock: 18, 79 | sales: 69, 80 | ishot: true, 81 | img: 'http://gw.alicdn.com/tps/LB1HL0mQVXXXXbzXVXXXXXXXXXX.png' 82 | }, { 83 | id: 1009, 84 | name: '荷兰 天赋力 Herobaby 婴儿配方奶粉 4段 1岁以上700g', 85 | price: 89, 86 | type: 1, 87 | stock: 36, 88 | sales: 1895, 89 | img: 'http://m.360buyimg.com/babel/s330x330_jfs/t4597/175/4364374663/125149/4fbbaf21/590d4f5aN0467dc26.jpg!q50.jpg.webp' 90 | }, { 91 | id: 1010, 92 | name: '【全球购】越南acecook河粉牛肉河粉特产 速食即食方便面粉丝 牛肉河粉米粉65克*5袋', 93 | price: 19.9, 94 | type: 3, 95 | stock: 353, 96 | sales: 3041, 97 | ishot: true, 98 | img: 'https://m.360buyimg.com/mobilecms/s357x357_jfs/t3169/228/5426689121/95568/d463e211/586dbf56N37fcd503.jpg!q50.jpg' 99 | }, { 100 | id: 1011, 101 | name: '正品FENDI/芬迪女包钱包女长款 百搭真皮钱夹 女士小怪兽手拿包', 102 | price: 3580, 103 | type: 2, 104 | stock: 5, 105 | sales: 18, 106 | img: 'http://img.alicdn.com/imgextra/i3/TB16avCQXXXXXcsXpXXXXXXXXXX_!!0-item_pic.jpg_400x400q60s30.jpg_.webp' 107 | }]; 108 | -------------------------------------------------------------------------------- /examples/cart-create/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 14px; 3 | color: #363636; 4 | background-color: #333; 5 | } 6 | 7 | h1, 8 | ul, 9 | li, 10 | p { 11 | margin: 0; 12 | padding: 0; 13 | } 14 | 15 | li { 16 | list-style: none; 17 | } 18 | 19 | .g-panel { 20 | margin: 0 auto; 21 | width: 790px; 22 | } 23 | 24 | .cate, 25 | .filter-opt, 26 | .save { 27 | cursor: pointer; 28 | } 29 | 30 | .device { 31 | position: relative; 32 | margin: 10px; 33 | float: left; 34 | width: 375px; 35 | height: 667px; 36 | background-color: #eee; 37 | border-radius: 4px; 38 | overflow: hidden; 39 | } 40 | 41 | header { 42 | padding: 0 4%; 43 | position: relative; 44 | height: 44px; 45 | line-height: 44px; 46 | background-color: #fff; 47 | border-bottom: 1px solid #ddd; 48 | } 49 | 50 | .header-title { 51 | position: absolute; 52 | margin-left: 21%; 53 | width: 50%; 54 | font-size: 16px; 55 | text-align: center; 56 | } 57 | 58 | .header-edit { 59 | float: right; 60 | padding: 0 10px; 61 | cursor: pointer; 62 | } 63 | 64 | .tab-wrap { 65 | height: 60px; 66 | background: red; 67 | overflow: hidden; 68 | } 69 | 70 | .cate-tab { 71 | white-space: nowrap; 72 | overflow-x: scroll; 73 | -webkit-overflow-scrolling: touch; 74 | background-color: #5D4285; 75 | } 76 | 77 | .cate { 78 | display: inline-block; 79 | width: 80px; 80 | height: 70px; 81 | color: #fff; 82 | line-height: 60px; 83 | text-align: center; 84 | } 85 | 86 | .tab-active { 87 | background-color: #9A51FF; 88 | } 89 | 90 | .filter-bar { 91 | display: flex; 92 | height: 40px; 93 | background-color: #fff; 94 | border-bottom: 1px solid #E5E5E5; 95 | line-height: 40px; 96 | } 97 | 98 | .filter-opt { 99 | position: relative; 100 | width: 33.3%; 101 | color: #5F646E; 102 | text-align: center; 103 | } 104 | 105 | .filter-active { 106 | color: #7B57C5; 107 | } 108 | 109 | .filter-price:after { 110 | position: absolute; 111 | top: 13px; 112 | margin-left: 4px; 113 | content: ''; 114 | display: inline-block; 115 | width: 8px; 116 | height: 14px; 117 | background: url('http://ov52d8mm7.bkt.clouddn.com/arrow-default.png') no-repeat; 118 | background-size: 8px 14px; 119 | } 120 | 121 | .filter-active.price-up:after { 122 | background: url('http://ov52d8mm7.bkt.clouddn.com/arrow-down.png') no-repeat; 123 | background-size: 8px 14px; 124 | } 125 | 126 | .filter-active.price-down:after { 127 | background: url('http://ov52d8mm7.bkt.clouddn.com/arrow-up.png') no-repeat; 128 | background-size: 8px 14px; 129 | } 130 | 131 | .goods-list { 132 | padding-top: 8px; 133 | height: 513px; 134 | overflow-y: scroll; 135 | } 136 | 137 | .cart-list { 138 | height: 560px; 139 | } 140 | 141 | .goods-item { 142 | display: flex; 143 | margin-bottom: 8px; 144 | padding: 10px 6px; 145 | min-height: 62px; 146 | background: #fff; 147 | } 148 | 149 | .goods-img { 150 | position: relative; 151 | margin-right: 4%; 152 | display: block; 153 | width: 16%; 154 | } 155 | 156 | .goods-img img { 157 | position: absolute; 158 | top: 0; 159 | left: 0; 160 | width: 100%; 161 | } 162 | 163 | .goods-item .flag { 164 | position: absolute; 165 | top: 0; 166 | left: 0; 167 | width: 20px; 168 | height: 20px; 169 | font-size: 12px; 170 | color: #fff; 171 | text-align: center; 172 | line-height: 20px; 173 | background-color: #FC5951; 174 | border-radius: 50%; 175 | } 176 | 177 | .goods-info { 178 | position: relative; 179 | width: 80%; 180 | } 181 | 182 | .goods-title { 183 | width: 80%; 184 | height: 38px; 185 | color: #363636; 186 | line-height: 1.4; 187 | display: -webkit-box; 188 | -webkit-box-orient: vertical; 189 | -webkit-line-clamp: 2; 190 | overflow: hidden; 191 | } 192 | 193 | .goods-price { 194 | margin-top: 6px; 195 | line-height: 1; 196 | } 197 | 198 | .goods-price span { 199 | font-size: 15px; 200 | color: #7a45e5; 201 | /* background: linear-gradient(90deg, #03D2B3 0, #2181FB 80%, #2181FB 100%); 202 | -webkit-background-clip: text; 203 | -webkit-text-fill-color: transparent; */ 204 | } 205 | 206 | .des { 207 | font-size: 12px; 208 | color: #888; 209 | } 210 | 211 | .save { 212 | position: absolute; 213 | right: 10px; 214 | bottom: 2px; 215 | width: 32px; 216 | height: 22px; 217 | background-color: #7a45e5; 218 | font-size: 16px; 219 | line-height: 19px; 220 | text-align: center; 221 | color: #fff; 222 | border-radius: 12px; 223 | overflow: hidden; 224 | } 225 | 226 | .empty-states { 227 | padding-top: 60px; 228 | font-size: 18px; 229 | color: #AEB0B7; 230 | text-align: center; 231 | } 232 | 233 | .cart-list .goods-info { 234 | width: 68%; 235 | } 236 | 237 | .item-selector { 238 | width: 12%; 239 | } 240 | 241 | .icon-selector { 242 | position: relative; 243 | margin: 16px auto 0 auto; 244 | width: 16px; 245 | height: 16px; 246 | border-radius: 50%; 247 | cursor: pointer; 248 | } 249 | 250 | .selector-active { 251 | background-color: #7a45e5; 252 | border-color: #7a45e5; 253 | } 254 | 255 | .selector-active .icon { 256 | position: absolute; 257 | top: 2px; 258 | left: 2px; 259 | } 260 | 261 | .goods-num { 262 | position: absolute; 263 | right: 10px; 264 | top: 4px; 265 | width: 32px; 266 | color: #999; 267 | text-align: center; 268 | } 269 | 270 | .show-num { 271 | line-height: 28px; 272 | } 273 | 274 | .num-btn { 275 | width: 100%; 276 | height: 24px; 277 | font-size: 20px; 278 | line-height: 20px; 279 | cursor: pointer; 280 | } 281 | 282 | .action-bar { 283 | position: absolute; 284 | left: 0; 285 | bottom: 0; 286 | width: 100%; 287 | height: 52px; 288 | font-size: 15px; 289 | background-color: #fff; 290 | border-top: 1px solid #ddd; 291 | } 292 | 293 | .g-selector { 294 | float: left; 295 | width: 70px; 296 | margin-left: 4%; 297 | height: 52px; 298 | cursor: pointer; 299 | } 300 | 301 | .g-selector .item-selector { 302 | position: relative; 303 | display: inline-block; 304 | } 305 | 306 | .g-selector span { 307 | position: absolute; 308 | margin-left: 20px; 309 | color: #5F646E; 310 | top: 15px; 311 | } 312 | 313 | .total { 314 | float: right; 315 | color: #363636; 316 | font-size: 14px; 317 | line-height: 50px; 318 | margin-right: 20px; 319 | } 320 | 321 | .total span { 322 | color: #7A45E5; 323 | } 324 | 325 | .total b { 326 | font-size: 17px; 327 | margin-left: 4px; 328 | } 329 | 330 | .action-btn { 331 | float: right; 332 | width: 120px; 333 | height: 100%; 334 | color: #fff; 335 | text-align: center; 336 | font-weight: 300; 337 | line-height: 52px; 338 | cursor: pointer; 339 | } 340 | 341 | .buy-btn { 342 | background-color: #7A45E5; 343 | } 344 | 345 | .del-btn { 346 | background-color: #FF4069; 347 | } 348 | 349 | .del-box .total { 350 | display: none; 351 | } 352 | 353 | .del-box .buy-btn { 354 | display: none; 355 | } 356 | 357 | .del-box .del-btn { 358 | display: block; 359 | } 360 | .loading { 361 | position: absolute; 362 | top:0; 363 | left:0; 364 | bottom:0; 365 | right:0; 366 | background: rgba(0,0,0,0.12); 367 | color: #fff; 368 | font-size: 30px; 369 | display: flex; 370 | align-items: center; 371 | justify-content: center; 372 | } 373 | -------------------------------------------------------------------------------- /examples/cart-create/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 |
    5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/cart-create/index.js: -------------------------------------------------------------------------------- 1 | import {NavLink as Link, HashRouter, Route, Switch, Redirect} from 'react-router-dom'; 2 | import React from 'react'; 3 | import {render} from 'react-dom'; 4 | import store from './store'; 5 | import List from './list'; 6 | import Cart from './cart'; 7 | import './index.css'; 8 | 9 | class App extends React.Component { 10 | 11 | render() { 12 | return (
    13 | 14 | 15 |
    ); 16 | } 17 | } 18 | 19 | const routes = ( 20 | 21 | 22 | 23 | ); 24 | 25 | render(routes, document.querySelector('#root')); 26 | -------------------------------------------------------------------------------- /examples/cart-create/list/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { category, sortMethods } from '../config'; 3 | import { connect } from '../../../src'; 4 | import './store'; 5 | 6 | @connect(state => state.list) 7 | export default class List extends React.Component { 8 | add = item => { 9 | this.props.dispatch('cart.addCartItem', item); 10 | }; 11 | sort = value => { 12 | this.props.dispatch('list.sortList', value); 13 | }; 14 | filter = id => { 15 | this.props.dispatch('list.fetch', { 16 | category: id 17 | }); 18 | }; 19 | renderCategory(data) { 20 | return data.map(item => { 21 | return ( 22 |
  • 26 | {item.des} 27 |
  • 28 | ); 29 | }); 30 | } 31 | 32 | renderFilter(data) { 33 | return data.map(item => { 34 | return ( 35 |
  • 39 | {item.name} 40 |
  • 41 | ); 42 | }); 43 | } 44 | 45 | renderList(data) { 46 | return data.map(item => { 47 | return ( 48 |
  • 49 |
    50 | 51 |
    52 |
    53 |
    54 |

    {item.name}

    55 |
    56 | 57 | ¥{item.price} 58 | 59 |
    60 | {item.sales}人付款 61 | 62 | + 63 | 64 |
    65 |
  • 66 | ); 67 | }); 68 | } 69 | componentDidMount() { 70 | this.props.dispatch('list.fetch'); 71 | } 72 | render() { 73 | console.log('list, render'); 74 | return ( 75 |
    76 |
    77 | 商品列表 78 |
    79 |
    80 |
    81 |
      {this.renderCategory(category)}
    82 |
    83 | 84 | 85 |
    86 | {this.props.loading ?
    loading...
    : null} 87 |
    88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /examples/cart-create/list/store.js: -------------------------------------------------------------------------------- 1 | import { Store } from '../../../src/'; 2 | import { goods } from '../config'; 3 | 4 | const store = Store.get(); 5 | 6 | store.create('list', { 7 | state: { 8 | goods: [], 9 | currentCategory: null, 10 | currentSort: null, 11 | loading: false 12 | }, 13 | actions: { 14 | fetch(state, payload) { 15 | state.loading = true; 16 | const ret = new Promise(resolve => { 17 | setTimeout(() => { 18 | resolve(); 19 | }, 500); 20 | }); 21 | ret.then(() => { 22 | this.transaction(() => { 23 | if (!payload || !payload.category) { 24 | state.goods = goods; 25 | } else { 26 | state.goods = goods.filter(item => item.type === payload.category); 27 | } 28 | state.currentCategory = payload && payload.category; 29 | state.loading = false; 30 | }); 31 | }); 32 | }, 33 | sortList(state, payload) { 34 | state.goods.sort((a, b) => a[payload] - b[payload]); 35 | state.currentSort = payload; 36 | }, 37 | addCartItem(state, payload) { 38 | const item = state.cart.list.filter(item => item.id === payload.id)[0]; 39 | if (!item) { 40 | state.cart.list.push({ 41 | ...payload, 42 | quantity: 1 43 | }); 44 | } else { 45 | item.quantity++; 46 | } 47 | }, 48 | } 49 | }); 50 | -------------------------------------------------------------------------------- /examples/cart-create/store.js: -------------------------------------------------------------------------------- 1 | import { Store } from '../../src/'; 2 | import devtools from '../../src/plugins/devtools'; 3 | import routePlugin from '../../src/plugins/route'; 4 | 5 | const store = new Store( 6 | {}, 7 | { 8 | plugins: [devtools, routePlugin] 9 | } 10 | ); 11 | 12 | export default store; 13 | -------------------------------------------------------------------------------- /examples/cart-inject/README.md: -------------------------------------------------------------------------------- 1 | # Shopping Cart 2 | 3 | 演示了基于单一Store的inject行为 4 | -------------------------------------------------------------------------------- /examples/cart-inject/components/cart-list.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { inject } from '../../../src'; 3 | import store from '../store'; 4 | 5 | @inject(store, true) 6 | class CartList extends React.Component { 7 | 8 | onSelect(id, e) { 9 | const { checked } = e.target; 10 | this.props.dispatch('select', { 11 | id, 12 | checked 13 | }); 14 | } 15 | onAdd(item) { 16 | this.props.dispatch('onAdd', item); 17 | } 18 | onReduce(item) { 19 | this.props.dispatch('onReduce', item); 20 | } 21 | 22 | renderList(data) { 23 | return data.map(item => { 24 | return ( 25 |
  • 26 |
    27 |
    28 | 33 |
    34 |
    35 |
    36 | 37 |
    38 |
    39 |

    {item.name}

    40 |
    41 | 42 | ¥{item.price} 43 | 44 |
    45 | 库存{item.stock}件 46 |
    47 |
    48 | + 49 |
    50 |
    {item.quantity}
    51 |
    52 | - 53 |
    54 |
    55 |
    56 |
  • 57 | ); 58 | }); 59 | } 60 | render() { 61 | console.log('cartlist, render'); 62 | const { list } = this.props.state.cart; 63 | if (list.length) { 64 | return ; 65 | } 66 | return ( 67 |
    68 | 这里是空的,快去逛逛吧 69 |
    70 | ); 71 | } 72 | } 73 | 74 | export default CartList; 75 | -------------------------------------------------------------------------------- /examples/cart-inject/components/cart.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { inject } from '../../../src'; 3 | import CartList from './cart-list'; 4 | import store from '../store'; 5 | 6 | @inject(store, true) 7 | export default class Cart extends React.Component { 8 | state = { 9 | isEdit: false 10 | }; 11 | 12 | edit = () => { 13 | this.setState({ 14 | isEdit: true 15 | }); 16 | }; 17 | 18 | complete = () => { 19 | this.setState({ 20 | isEdit: false 21 | }); 22 | }; 23 | 24 | selectAll(e) { 25 | this.props.dispatch('selectAll', { 26 | checked: e.target.checked 27 | }); 28 | } 29 | 30 | onRemove = () => { 31 | this.props.dispatch('onRemove'); 32 | }; 33 | renderCart() { 34 | const { list } = this.store; 35 | if (list.length) { 36 | return ; 37 | } 38 | return ( 39 |
    40 | 这里是空的,快去逛逛吧 41 |
    42 | ); 43 | } 44 | render() { 45 | console.log('cart, render'); 46 | const selectedItems = this.props.state.cart.list.filter(item => item.selected); 47 | const selectedNum = selectedItems.length; 48 | const totalPrice = selectedItems.reduce((total, item) => { 49 | total += item.quantity * item.price; 50 | return total; 51 | }, 0); 52 | const checked = selectedItems.length === this.props.state.cart.list.length && selectedItems.length > 0; 53 | const { isEdit } = this.state; 54 | return ( 55 |
    56 |
    57 | 购物清单 58 | 59 | {!isEdit ? 编辑 : 完成} 60 | 61 |
    62 |
    63 |
    64 |
    65 |
    66 |
    67 | 68 |
    69 |
    70 | 全选 71 |
    72 | {!isEdit ? ( 73 |
    去结算({selectedNum})
    74 | ) : ( 75 |
    76 | 删除({selectedNum}) 77 |
    78 | )} 79 |
    80 | 合计: 81 | 82 | ¥{totalPrice} 83 | 84 |
    85 |
    86 |
    87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /examples/cart-inject/components/list.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { category, sortMethods } from '../config'; 3 | import { inject } from '../../../src'; 4 | import store from '../store'; 5 | 6 | @inject(store, true) 7 | export default class List extends React.Component { 8 | add = item => { 9 | this.props.dispatch('addCartItem', item); 10 | }; 11 | sort = value => { 12 | this.props.dispatch('sortList', value); 13 | }; 14 | filter = id => { 15 | this.props.dispatch('fetch', { 16 | category: id 17 | }); 18 | }; 19 | renderCategory(data) { 20 | return data.map(item => { 21 | return ( 22 |
  • 26 | {item.des} 27 |
  • 28 | ); 29 | }); 30 | } 31 | 32 | renderFilter(data) { 33 | return data.map(item => { 34 | return ( 35 |
  • 39 | {item.name} 40 |
  • 41 | ); 42 | }); 43 | } 44 | 45 | renderList(data) { 46 | return data.map(item => { 47 | return ( 48 |
  • 49 |
    50 | 51 |
    52 |
    53 |
    54 |

    {item.name}

    55 |
    56 | 57 | ¥{item.price} 58 | 59 |
    60 | {item.sales}人付款 61 | 62 | + 63 | 64 |
    65 |
  • 66 | ); 67 | }); 68 | } 69 | componentDidMount() { 70 | this.props.dispatch('fetch'); 71 | } 72 | render() { 73 | console.log('list, render'); 74 | return ( 75 |
    76 |
    77 | 商品列表 78 |
    79 |
    80 |
    81 |
      {this.renderCategory(category)}
    82 |
    83 | 84 | 85 |
    86 | {this.props.state.list.loading ?
    loading...
    : null} 87 |
    88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /examples/cart-inject/config.js: -------------------------------------------------------------------------------- 1 | export const category = [ 2 | { id: 0, des: '推荐' }, 3 | { id: 1, des: '母婴' }, 4 | { id: 2, des: '鞋包饰品' }, 5 | { id: 3, des: '食品' }, 6 | { id: 4, des: '数码家电' }, 7 | { id: 5, des: '居家百货' } 8 | ]; 9 | 10 | export const sortMethods = [ 11 | { name: '综合排序', value: 'id' }, 12 | { name: '销量优先', value: 'sales' }, 13 | { name: '价格', value: 'price' } 14 | ]; 15 | 16 | export const goods = [{ 17 | id: 1001, 18 | name: 'Beats EP头戴式耳机', 19 | price: 558, 20 | type: 4, 21 | stock: 128, 22 | sales: 1872, 23 | img: 'http://img11.360buyimg.com/n1/s528x528_jfs/t3109/194/2435573156/46587/e0e867ac/57e10978N87220944.jpg!q70.jpg' 24 | }, { 25 | id: 1002, 26 | name: '雀巢(Nestle)高钙成人奶粉', 27 | price: 60, 28 | type: 3, 29 | stock: 5, 30 | sales: 2374, 31 | img: 'http://m.360buyimg.com/babel/jfs/t5197/28/400249159/97561/304ce550/58ff0dbeN88884779.jpg!q50.jpg.webp' 32 | }, { 33 | id: 1003, 34 | name: '煎炒烹炸一锅多用', 35 | price: 216, 36 | type: 5, 37 | stock: 2, 38 | sales: 351, 39 | ishot: true, 40 | img: 'http://gw.alicdn.com/tps/TB19OfQRXXXXXbmXXXXL6TaGpXX_760x760q90s150.jpg_.webp' 41 | }, { 42 | id: 1004, 43 | name: 'ANNE KLEIN 潮流经典美式轻奢', 44 | price: 585, 45 | type: 2, 46 | stock: 465, 47 | sales: 8191, 48 | img: 'http://gw.alicdn.com/tps/TB1l5psQVXXXXcXaXXXL6TaGpXX_760x760q90s150.jpg_.webp' 49 | }, { 50 | id: 1005, 51 | name: '乐高EV3机器人积木玩具', 52 | price: 3099, 53 | type: 1, 54 | stock: 154, 55 | sales: 165, 56 | img: 'https://m.360buyimg.com/mobilecms/s357x357_jfs/t6490/168/1052550216/653858/9eef28d1/594922a8Nc3afa743.jpg!q50.jpg' 57 | }, { 58 | id: 1006, 59 | name: '全球购 路易威登(Louis Vuitton)新款女士LV印花手袋 M41112', 60 | price: 10967, 61 | type: 2, 62 | stock: 12, 63 | sales: 6, 64 | img: 'https://m.360buyimg.com/n1/s220x220_jfs/t1429/17/1007119837/464370/310392f4/55b5e5bfN75daf703.png!q70.jpg' 65 | }, { 66 | id: 1007, 67 | name: 'Kindle Paperwhite3 黑色经典版电纸书', 68 | price: 805, 69 | type: 4, 70 | stock: 3, 71 | sales: 395, 72 | img: 'http://img12.360buyimg.com/n1/s528x528_jfs/t4954/76/635213328/51972/ec4a3c3c/58e5f717N4031d162.jpg!q70.jpg' 73 | }, { 74 | id: 1008, 75 | name: 'DELSEY 男士双肩背包', 76 | price: 269, 77 | type: 2, 78 | stock: 18, 79 | sales: 69, 80 | ishot: true, 81 | img: 'http://gw.alicdn.com/tps/LB1HL0mQVXXXXbzXVXXXXXXXXXX.png' 82 | }, { 83 | id: 1009, 84 | name: '荷兰 天赋力 Herobaby 婴儿配方奶粉 4段 1岁以上700g', 85 | price: 89, 86 | type: 1, 87 | stock: 36, 88 | sales: 1895, 89 | img: 'http://m.360buyimg.com/babel/s330x330_jfs/t4597/175/4364374663/125149/4fbbaf21/590d4f5aN0467dc26.jpg!q50.jpg.webp' 90 | }, { 91 | id: 1010, 92 | name: '【全球购】越南acecook河粉牛肉河粉特产 速食即食方便面粉丝 牛肉河粉米粉65克*5袋', 93 | price: 19.9, 94 | type: 3, 95 | stock: 353, 96 | sales: 3041, 97 | ishot: true, 98 | img: 'https://m.360buyimg.com/mobilecms/s357x357_jfs/t3169/228/5426689121/95568/d463e211/586dbf56N37fcd503.jpg!q50.jpg' 99 | }, { 100 | id: 1011, 101 | name: '正品FENDI/芬迪女包钱包女长款 百搭真皮钱夹 女士小怪兽手拿包', 102 | price: 3580, 103 | type: 2, 104 | stock: 5, 105 | sales: 18, 106 | img: 'http://img.alicdn.com/imgextra/i3/TB16avCQXXXXXcsXpXXXXXXXXXX_!!0-item_pic.jpg_400x400q60s30.jpg_.webp' 107 | }]; 108 | -------------------------------------------------------------------------------- /examples/cart-inject/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 14px; 3 | color: #363636; 4 | background-color: #333; 5 | } 6 | 7 | h1, 8 | ul, 9 | li, 10 | p { 11 | margin: 0; 12 | padding: 0; 13 | } 14 | 15 | li { 16 | list-style: none; 17 | } 18 | 19 | .g-panel { 20 | margin: 0 auto; 21 | width: 790px; 22 | } 23 | 24 | .cate, 25 | .filter-opt, 26 | .save { 27 | cursor: pointer; 28 | } 29 | 30 | .device { 31 | position: relative; 32 | margin: 10px; 33 | float: left; 34 | width: 375px; 35 | height: 667px; 36 | background-color: #eee; 37 | border-radius: 4px; 38 | overflow: hidden; 39 | } 40 | 41 | header { 42 | padding: 0 4%; 43 | position: relative; 44 | height: 44px; 45 | line-height: 44px; 46 | background-color: #fff; 47 | border-bottom: 1px solid #ddd; 48 | } 49 | 50 | .header-title { 51 | position: absolute; 52 | margin-left: 21%; 53 | width: 50%; 54 | font-size: 16px; 55 | text-align: center; 56 | } 57 | 58 | .header-edit { 59 | float: right; 60 | padding: 0 10px; 61 | cursor: pointer; 62 | } 63 | 64 | .tab-wrap { 65 | height: 60px; 66 | background: red; 67 | overflow: hidden; 68 | } 69 | 70 | .cate-tab { 71 | white-space: nowrap; 72 | overflow-x: scroll; 73 | -webkit-overflow-scrolling: touch; 74 | background-color: #5D4285; 75 | } 76 | 77 | .cate { 78 | display: inline-block; 79 | width: 80px; 80 | height: 70px; 81 | color: #fff; 82 | line-height: 60px; 83 | text-align: center; 84 | } 85 | 86 | .tab-active { 87 | background-color: #9A51FF; 88 | } 89 | 90 | .filter-bar { 91 | display: flex; 92 | height: 40px; 93 | background-color: #fff; 94 | border-bottom: 1px solid #E5E5E5; 95 | line-height: 40px; 96 | } 97 | 98 | .filter-opt { 99 | position: relative; 100 | width: 33.3%; 101 | color: #5F646E; 102 | text-align: center; 103 | } 104 | 105 | .filter-active { 106 | color: #7B57C5; 107 | } 108 | 109 | .filter-price:after { 110 | position: absolute; 111 | top: 13px; 112 | margin-left: 4px; 113 | content: ''; 114 | display: inline-block; 115 | width: 8px; 116 | height: 14px; 117 | background: url('http://ov52d8mm7.bkt.clouddn.com/arrow-default.png') no-repeat; 118 | background-size: 8px 14px; 119 | } 120 | 121 | .filter-active.price-up:after { 122 | background: url('http://ov52d8mm7.bkt.clouddn.com/arrow-down.png') no-repeat; 123 | background-size: 8px 14px; 124 | } 125 | 126 | .filter-active.price-down:after { 127 | background: url('http://ov52d8mm7.bkt.clouddn.com/arrow-up.png') no-repeat; 128 | background-size: 8px 14px; 129 | } 130 | 131 | .goods-list { 132 | padding-top: 8px; 133 | height: 513px; 134 | overflow-y: scroll; 135 | } 136 | 137 | .cart-list { 138 | height: 560px; 139 | } 140 | 141 | .goods-item { 142 | display: flex; 143 | margin-bottom: 8px; 144 | padding: 10px 6px; 145 | min-height: 62px; 146 | background: #fff; 147 | } 148 | 149 | .goods-img { 150 | position: relative; 151 | margin-right: 4%; 152 | display: block; 153 | width: 16%; 154 | } 155 | 156 | .goods-img img { 157 | position: absolute; 158 | top: 0; 159 | left: 0; 160 | width: 100%; 161 | } 162 | 163 | .goods-item .flag { 164 | position: absolute; 165 | top: 0; 166 | left: 0; 167 | width: 20px; 168 | height: 20px; 169 | font-size: 12px; 170 | color: #fff; 171 | text-align: center; 172 | line-height: 20px; 173 | background-color: #FC5951; 174 | border-radius: 50%; 175 | } 176 | 177 | .goods-info { 178 | position: relative; 179 | width: 80%; 180 | } 181 | 182 | .goods-title { 183 | width: 80%; 184 | height: 38px; 185 | color: #363636; 186 | line-height: 1.4; 187 | display: -webkit-box; 188 | -webkit-box-orient: vertical; 189 | -webkit-line-clamp: 2; 190 | overflow: hidden; 191 | } 192 | 193 | .goods-price { 194 | margin-top: 6px; 195 | line-height: 1; 196 | } 197 | 198 | .goods-price span { 199 | font-size: 15px; 200 | color: #7a45e5; 201 | /* background: linear-gradient(90deg, #03D2B3 0, #2181FB 80%, #2181FB 100%); 202 | -webkit-background-clip: text; 203 | -webkit-text-fill-color: transparent; */ 204 | } 205 | 206 | .des { 207 | font-size: 12px; 208 | color: #888; 209 | } 210 | 211 | .save { 212 | position: absolute; 213 | right: 10px; 214 | bottom: 2px; 215 | width: 32px; 216 | height: 22px; 217 | background-color: #7a45e5; 218 | font-size: 16px; 219 | line-height: 19px; 220 | text-align: center; 221 | color: #fff; 222 | border-radius: 12px; 223 | overflow: hidden; 224 | } 225 | 226 | .empty-states { 227 | padding-top: 60px; 228 | font-size: 18px; 229 | color: #AEB0B7; 230 | text-align: center; 231 | } 232 | 233 | .cart-list .goods-info { 234 | width: 68%; 235 | } 236 | 237 | .item-selector { 238 | width: 12%; 239 | } 240 | 241 | .icon-selector { 242 | position: relative; 243 | margin: 16px auto 0 auto; 244 | width: 16px; 245 | height: 16px; 246 | border-radius: 50%; 247 | cursor: pointer; 248 | } 249 | 250 | .selector-active { 251 | background-color: #7a45e5; 252 | border-color: #7a45e5; 253 | } 254 | 255 | .selector-active .icon { 256 | position: absolute; 257 | top: 2px; 258 | left: 2px; 259 | } 260 | 261 | .goods-num { 262 | position: absolute; 263 | right: 10px; 264 | top: 4px; 265 | width: 32px; 266 | color: #999; 267 | text-align: center; 268 | } 269 | 270 | .show-num { 271 | line-height: 28px; 272 | } 273 | 274 | .num-btn { 275 | width: 100%; 276 | height: 24px; 277 | font-size: 20px; 278 | line-height: 20px; 279 | cursor: pointer; 280 | } 281 | 282 | .action-bar { 283 | position: absolute; 284 | left: 0; 285 | bottom: 0; 286 | width: 100%; 287 | height: 52px; 288 | font-size: 15px; 289 | background-color: #fff; 290 | border-top: 1px solid #ddd; 291 | } 292 | 293 | .g-selector { 294 | float: left; 295 | width: 70px; 296 | margin-left: 4%; 297 | height: 52px; 298 | cursor: pointer; 299 | } 300 | 301 | .g-selector .item-selector { 302 | position: relative; 303 | display: inline-block; 304 | } 305 | 306 | .g-selector span { 307 | position: absolute; 308 | margin-left: 20px; 309 | color: #5F646E; 310 | top: 15px; 311 | } 312 | 313 | .total { 314 | float: right; 315 | color: #363636; 316 | font-size: 14px; 317 | line-height: 50px; 318 | margin-right: 20px; 319 | } 320 | 321 | .total span { 322 | color: #7A45E5; 323 | } 324 | 325 | .total b { 326 | font-size: 17px; 327 | margin-left: 4px; 328 | } 329 | 330 | .action-btn { 331 | float: right; 332 | width: 120px; 333 | height: 100%; 334 | color: #fff; 335 | text-align: center; 336 | font-weight: 300; 337 | line-height: 52px; 338 | cursor: pointer; 339 | } 340 | 341 | .buy-btn { 342 | background-color: #7A45E5; 343 | } 344 | 345 | .del-btn { 346 | background-color: #FF4069; 347 | } 348 | 349 | .del-box .total { 350 | display: none; 351 | } 352 | 353 | .del-box .buy-btn { 354 | display: none; 355 | } 356 | 357 | .del-box .del-btn { 358 | display: block; 359 | } 360 | .loading { 361 | position: absolute; 362 | top:0; 363 | left:0; 364 | bottom:0; 365 | right:0; 366 | background: rgba(0,0,0,0.12); 367 | color: #fff; 368 | font-size: 30px; 369 | display: flex; 370 | align-items: center; 371 | justify-content: center; 372 | } 373 | -------------------------------------------------------------------------------- /examples/cart-inject/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 |
    5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/cart-inject/index.js: -------------------------------------------------------------------------------- 1 | import {NavLink as Link, HashRouter, Route, Switch, Redirect} from 'react-router-dom'; 2 | import React from 'react'; 3 | import {render} from 'react-dom'; 4 | import store from './store'; 5 | import List from './components/list'; 6 | import Cart from './components/cart'; 7 | import './index.css'; 8 | 9 | class App extends React.Component { 10 | 11 | render() { 12 | return (
    13 | 14 | 15 |
    ); 16 | } 17 | } 18 | 19 | const routes = ( 20 | 21 | 22 | 23 | ); 24 | 25 | render(routes, document.querySelector('#root')); 26 | -------------------------------------------------------------------------------- /examples/cart-inject/store.js: -------------------------------------------------------------------------------- 1 | import { Store } from '../../src/'; 2 | import devtools from '../../src/plugins/devtools'; 3 | import routePlugin from '../../src/plugins/route'; 4 | import { goods } from './config'; 5 | 6 | const store = new Store( 7 | { 8 | state: { 9 | list: { 10 | goods: [], 11 | currentCategory: null, 12 | currentSort: null, 13 | loading: false 14 | }, 15 | cart: { 16 | list: [] 17 | } 18 | }, 19 | actions: { 20 | fetch(state, payload) { 21 | state.list.loading = true; 22 | const ret = new Promise(resolve => { 23 | setTimeout(() => { 24 | resolve(); 25 | }, 500); 26 | }); 27 | ret.then(() => { 28 | this.transaction(() => { 29 | if (!payload || !payload.category) { 30 | state.list.goods = goods; 31 | } else { 32 | state.list.goods = goods.filter(item => item.type === payload.category); 33 | } 34 | state.list.currentCategory = payload && payload.category; 35 | state.list.loading = false; 36 | }); 37 | }); 38 | }, 39 | sortList(state, payload) { 40 | state.list.goods.sort((a, b) => a[payload] - b[payload]); 41 | state.list.currentSort = payload; 42 | }, 43 | addCartItem(state, payload) { 44 | const item = state.cart.list.filter(item => item.id === payload.id)[0]; 45 | if (!item) { 46 | state.cart.list.push({ 47 | ...payload, 48 | quantity: 1 49 | }); 50 | } else { 51 | item.quantity++; 52 | } 53 | }, 54 | select(state, payload) { 55 | const item = state.cart.list.filter(item => item.id === payload.id)[0]; 56 | item.selected = payload.checked; 57 | }, 58 | selectAll(state, payload) { 59 | state.cart.list.forEach(item => { 60 | item.selected = payload.checked; 61 | }); 62 | }, 63 | onAdd(state, payload) { 64 | payload.quantity++; 65 | }, 66 | onReduce(state, payload) { 67 | payload.quantity = Math.max(0, --payload.quantity); 68 | }, 69 | onRemove(state, payload) { 70 | const list = state.cart.list.filter(item => !item.selected); 71 | state.cart.list = list; 72 | } 73 | } 74 | }, 75 | { 76 | plugins: [devtools, routePlugin] 77 | } 78 | ); 79 | 80 | export default store; 81 | -------------------------------------------------------------------------------- /examples/cart/README.md: -------------------------------------------------------------------------------- 1 | # Shopping Cart 2 | 3 | 演示了基于单一Store的connect行为 4 | -------------------------------------------------------------------------------- /examples/cart/components/cart-list.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from '../../../src'; 3 | 4 | @connect(state => ({ 5 | list: state.cart.list 6 | }), true) 7 | class CartList extends React.Component { 8 | 9 | onSelect(id, e) { 10 | const { checked } = e.target; 11 | this.props.dispatch('select', { 12 | id, 13 | checked 14 | }); 15 | } 16 | onAdd(item) { 17 | this.props.dispatch('onAdd', item); 18 | } 19 | onReduce(item) { 20 | this.props.dispatch('onReduce', item); 21 | } 22 | 23 | renderList(data) { 24 | return data.map(item => { 25 | return ( 26 |
  • 27 |
    28 |
    29 | 34 |
    35 |
    36 |
    37 | 38 |
    39 |
    40 |

    {item.name}

    41 |
    42 | 43 | ¥{item.price} 44 | 45 |
    46 | 库存{item.stock}件 47 |
    48 |
    49 | + 50 |
    51 |
    {item.quantity}
    52 |
    53 | - 54 |
    55 |
    56 |
    57 |
  • 58 | ); 59 | }); 60 | } 61 | render() { 62 | console.log('cartlist, render'); 63 | const { list } = this.props; 64 | if (list.length) { 65 | return ; 66 | } 67 | return ( 68 |
    69 | 这里是空的,快去逛逛吧 70 |
    71 | ); 72 | } 73 | } 74 | 75 | export default CartList; 76 | -------------------------------------------------------------------------------- /examples/cart/components/cart.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from '../../../src'; 3 | import CartList from './cart-list'; 4 | 5 | @connect(state => state.cart) 6 | export default class Cart extends React.Component { 7 | state = { 8 | isEdit: false 9 | }; 10 | 11 | edit = () => { 12 | this.setState({ 13 | isEdit: true 14 | }); 15 | }; 16 | 17 | complete = () => { 18 | this.setState({ 19 | isEdit: false 20 | }); 21 | }; 22 | 23 | selectAll(e) { 24 | this.props.dispatch('selectAll', { 25 | checked: e.target.checked 26 | }); 27 | } 28 | 29 | onRemove = () => { 30 | this.props.dispatch('onRemove'); 31 | }; 32 | renderCart() { 33 | const { list } = this.props; 34 | if (list.length) { 35 | return ; 36 | } 37 | return ( 38 |
    39 | 这里是空的,快去逛逛吧 40 |
    41 | ); 42 | } 43 | render() { 44 | console.log('cart, render'); 45 | const selectedItems = this.props.list.filter(item => item.selected); 46 | const selectedNum = selectedItems.length; 47 | const totalPrice = selectedItems.reduce((total, item) => { 48 | total += item.quantity * item.price; 49 | return total; 50 | }, 0); 51 | const checked = selectedItems.length === this.props.list.length && selectedItems.length > 0; 52 | const { isEdit } = this.state; 53 | return ( 54 |
    55 |
    56 | 购物清单 57 | 58 | {!isEdit ? 编辑 : 完成} 59 | 60 |
    61 |
    62 |
    63 |
    64 |
    65 |
    66 | 67 |
    68 |
    69 | 全选 70 |
    71 | {!isEdit ? ( 72 |
    去结算({selectedNum})
    73 | ) : ( 74 |
    75 | 删除({selectedNum}) 76 |
    77 | )} 78 |
    79 | 合计: 80 | 81 | ¥{totalPrice} 82 | 83 |
    84 |
    85 |
    86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /examples/cart/components/list.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { category, sortMethods } from '../config'; 3 | import { connect } from '../../../src'; 4 | 5 | @connect(state => state.list) 6 | export default class List extends React.Component { 7 | add = item => { 8 | this.props.dispatch('addCartItem', item); 9 | }; 10 | sort = value => { 11 | this.props.dispatch('sortList', value); 12 | }; 13 | filter = id => { 14 | this.props.dispatch('fetch', { 15 | category: id 16 | }); 17 | }; 18 | renderCategory(data) { 19 | return data.map(item => { 20 | return ( 21 |
  • 25 | {item.des} 26 |
  • 27 | ); 28 | }); 29 | } 30 | 31 | renderFilter(data) { 32 | return data.map(item => { 33 | return ( 34 |
  • 38 | {item.name} 39 |
  • 40 | ); 41 | }); 42 | } 43 | 44 | renderList(data) { 45 | return data.map(item => { 46 | return ( 47 |
  • 48 |
    49 | 50 |
    51 |
    52 |
    53 |

    {item.name}

    54 |
    55 | 56 | ¥{item.price} 57 | 58 |
    59 | {item.sales}人付款 60 | 61 | + 62 | 63 |
    64 |
  • 65 | ); 66 | }); 67 | } 68 | componentDidMount() { 69 | this.props.dispatch('fetch'); 70 | } 71 | render() { 72 | console.log('list, render'); 73 | return ( 74 |
    75 |
    76 | 商品列表 77 |
    78 |
    79 |
    80 |
      {this.renderCategory(category)}
    81 |
    82 | 83 | 84 |
    85 | {this.props.loading ?
    loading...
    : null} 86 |
    87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /examples/cart/config.js: -------------------------------------------------------------------------------- 1 | export const category = [ 2 | { id: 0, des: '推荐' }, 3 | { id: 1, des: '母婴' }, 4 | { id: 2, des: '鞋包饰品' }, 5 | { id: 3, des: '食品' }, 6 | { id: 4, des: '数码家电' }, 7 | { id: 5, des: '居家百货' } 8 | ]; 9 | 10 | export const sortMethods = [ 11 | { name: '综合排序', value: 'id' }, 12 | { name: '销量优先', value: 'sales' }, 13 | { name: '价格', value: 'price' } 14 | ]; 15 | 16 | export const goods = [{ 17 | id: 1001, 18 | name: 'Beats EP头戴式耳机', 19 | price: 558, 20 | type: 4, 21 | stock: 128, 22 | sales: 1872, 23 | img: 'http://img11.360buyimg.com/n1/s528x528_jfs/t3109/194/2435573156/46587/e0e867ac/57e10978N87220944.jpg!q70.jpg' 24 | }, { 25 | id: 1002, 26 | name: '雀巢(Nestle)高钙成人奶粉', 27 | price: 60, 28 | type: 3, 29 | stock: 5, 30 | sales: 2374, 31 | img: 'http://m.360buyimg.com/babel/jfs/t5197/28/400249159/97561/304ce550/58ff0dbeN88884779.jpg!q50.jpg.webp' 32 | }, { 33 | id: 1003, 34 | name: '煎炒烹炸一锅多用', 35 | price: 216, 36 | type: 5, 37 | stock: 2, 38 | sales: 351, 39 | ishot: true, 40 | img: 'http://gw.alicdn.com/tps/TB19OfQRXXXXXbmXXXXL6TaGpXX_760x760q90s150.jpg_.webp' 41 | }, { 42 | id: 1004, 43 | name: 'ANNE KLEIN 潮流经典美式轻奢', 44 | price: 585, 45 | type: 2, 46 | stock: 465, 47 | sales: 8191, 48 | img: 'http://gw.alicdn.com/tps/TB1l5psQVXXXXcXaXXXL6TaGpXX_760x760q90s150.jpg_.webp' 49 | }, { 50 | id: 1005, 51 | name: '乐高EV3机器人积木玩具', 52 | price: 3099, 53 | type: 1, 54 | stock: 154, 55 | sales: 165, 56 | img: 'https://m.360buyimg.com/mobilecms/s357x357_jfs/t6490/168/1052550216/653858/9eef28d1/594922a8Nc3afa743.jpg!q50.jpg' 57 | }, { 58 | id: 1006, 59 | name: '全球购 路易威登(Louis Vuitton)新款女士LV印花手袋 M41112', 60 | price: 10967, 61 | type: 2, 62 | stock: 12, 63 | sales: 6, 64 | img: 'https://m.360buyimg.com/n1/s220x220_jfs/t1429/17/1007119837/464370/310392f4/55b5e5bfN75daf703.png!q70.jpg' 65 | }, { 66 | id: 1007, 67 | name: 'Kindle Paperwhite3 黑色经典版电纸书', 68 | price: 805, 69 | type: 4, 70 | stock: 3, 71 | sales: 395, 72 | img: 'http://img12.360buyimg.com/n1/s528x528_jfs/t4954/76/635213328/51972/ec4a3c3c/58e5f717N4031d162.jpg!q70.jpg' 73 | }, { 74 | id: 1008, 75 | name: 'DELSEY 男士双肩背包', 76 | price: 269, 77 | type: 2, 78 | stock: 18, 79 | sales: 69, 80 | ishot: true, 81 | img: 'http://gw.alicdn.com/tps/LB1HL0mQVXXXXbzXVXXXXXXXXXX.png' 82 | }, { 83 | id: 1009, 84 | name: '荷兰 天赋力 Herobaby 婴儿配方奶粉 4段 1岁以上700g', 85 | price: 89, 86 | type: 1, 87 | stock: 36, 88 | sales: 1895, 89 | img: 'http://m.360buyimg.com/babel/s330x330_jfs/t4597/175/4364374663/125149/4fbbaf21/590d4f5aN0467dc26.jpg!q50.jpg.webp' 90 | }, { 91 | id: 1010, 92 | name: '【全球购】越南acecook河粉牛肉河粉特产 速食即食方便面粉丝 牛肉河粉米粉65克*5袋', 93 | price: 19.9, 94 | type: 3, 95 | stock: 353, 96 | sales: 3041, 97 | ishot: true, 98 | img: 'https://m.360buyimg.com/mobilecms/s357x357_jfs/t3169/228/5426689121/95568/d463e211/586dbf56N37fcd503.jpg!q50.jpg' 99 | }, { 100 | id: 1011, 101 | name: '正品FENDI/芬迪女包钱包女长款 百搭真皮钱夹 女士小怪兽手拿包', 102 | price: 3580, 103 | type: 2, 104 | stock: 5, 105 | sales: 18, 106 | img: 'http://img.alicdn.com/imgextra/i3/TB16avCQXXXXXcsXpXXXXXXXXXX_!!0-item_pic.jpg_400x400q60s30.jpg_.webp' 107 | }]; 108 | -------------------------------------------------------------------------------- /examples/cart/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 14px; 3 | color: #363636; 4 | background-color: #333; 5 | } 6 | 7 | h1, 8 | ul, 9 | li, 10 | p { 11 | margin: 0; 12 | padding: 0; 13 | } 14 | 15 | li { 16 | list-style: none; 17 | } 18 | 19 | .g-panel { 20 | margin: 0 auto; 21 | width: 790px; 22 | } 23 | 24 | .cate, 25 | .filter-opt, 26 | .save { 27 | cursor: pointer; 28 | } 29 | 30 | .device { 31 | position: relative; 32 | margin: 10px; 33 | float: left; 34 | width: 375px; 35 | height: 667px; 36 | background-color: #eee; 37 | border-radius: 4px; 38 | overflow: hidden; 39 | } 40 | 41 | header { 42 | padding: 0 4%; 43 | position: relative; 44 | height: 44px; 45 | line-height: 44px; 46 | background-color: #fff; 47 | border-bottom: 1px solid #ddd; 48 | } 49 | 50 | .header-title { 51 | position: absolute; 52 | margin-left: 21%; 53 | width: 50%; 54 | font-size: 16px; 55 | text-align: center; 56 | } 57 | 58 | .header-edit { 59 | float: right; 60 | padding: 0 10px; 61 | cursor: pointer; 62 | } 63 | 64 | .tab-wrap { 65 | height: 60px; 66 | background: red; 67 | overflow: hidden; 68 | } 69 | 70 | .cate-tab { 71 | white-space: nowrap; 72 | overflow-x: scroll; 73 | -webkit-overflow-scrolling: touch; 74 | background-color: #5D4285; 75 | } 76 | 77 | .cate { 78 | display: inline-block; 79 | width: 80px; 80 | height: 70px; 81 | color: #fff; 82 | line-height: 60px; 83 | text-align: center; 84 | } 85 | 86 | .tab-active { 87 | background-color: #9A51FF; 88 | } 89 | 90 | .filter-bar { 91 | display: flex; 92 | height: 40px; 93 | background-color: #fff; 94 | border-bottom: 1px solid #E5E5E5; 95 | line-height: 40px; 96 | } 97 | 98 | .filter-opt { 99 | position: relative; 100 | width: 33.3%; 101 | color: #5F646E; 102 | text-align: center; 103 | } 104 | 105 | .filter-active { 106 | color: #7B57C5; 107 | } 108 | 109 | .filter-price:after { 110 | position: absolute; 111 | top: 13px; 112 | margin-left: 4px; 113 | content: ''; 114 | display: inline-block; 115 | width: 8px; 116 | height: 14px; 117 | background: url('http://ov52d8mm7.bkt.clouddn.com/arrow-default.png') no-repeat; 118 | background-size: 8px 14px; 119 | } 120 | 121 | .filter-active.price-up:after { 122 | background: url('http://ov52d8mm7.bkt.clouddn.com/arrow-down.png') no-repeat; 123 | background-size: 8px 14px; 124 | } 125 | 126 | .filter-active.price-down:after { 127 | background: url('http://ov52d8mm7.bkt.clouddn.com/arrow-up.png') no-repeat; 128 | background-size: 8px 14px; 129 | } 130 | 131 | .goods-list { 132 | padding-top: 8px; 133 | height: 513px; 134 | overflow-y: scroll; 135 | } 136 | 137 | .cart-list { 138 | height: 560px; 139 | } 140 | 141 | .goods-item { 142 | display: flex; 143 | margin-bottom: 8px; 144 | padding: 10px 6px; 145 | min-height: 62px; 146 | background: #fff; 147 | } 148 | 149 | .goods-img { 150 | position: relative; 151 | margin-right: 4%; 152 | display: block; 153 | width: 16%; 154 | } 155 | 156 | .goods-img img { 157 | position: absolute; 158 | top: 0; 159 | left: 0; 160 | width: 100%; 161 | } 162 | 163 | .goods-item .flag { 164 | position: absolute; 165 | top: 0; 166 | left: 0; 167 | width: 20px; 168 | height: 20px; 169 | font-size: 12px; 170 | color: #fff; 171 | text-align: center; 172 | line-height: 20px; 173 | background-color: #FC5951; 174 | border-radius: 50%; 175 | } 176 | 177 | .goods-info { 178 | position: relative; 179 | width: 80%; 180 | } 181 | 182 | .goods-title { 183 | width: 80%; 184 | height: 38px; 185 | color: #363636; 186 | line-height: 1.4; 187 | display: -webkit-box; 188 | -webkit-box-orient: vertical; 189 | -webkit-line-clamp: 2; 190 | overflow: hidden; 191 | } 192 | 193 | .goods-price { 194 | margin-top: 6px; 195 | line-height: 1; 196 | } 197 | 198 | .goods-price span { 199 | font-size: 15px; 200 | color: #7a45e5; 201 | /* background: linear-gradient(90deg, #03D2B3 0, #2181FB 80%, #2181FB 100%); 202 | -webkit-background-clip: text; 203 | -webkit-text-fill-color: transparent; */ 204 | } 205 | 206 | .des { 207 | font-size: 12px; 208 | color: #888; 209 | } 210 | 211 | .save { 212 | position: absolute; 213 | right: 10px; 214 | bottom: 2px; 215 | width: 32px; 216 | height: 22px; 217 | background-color: #7a45e5; 218 | font-size: 16px; 219 | line-height: 19px; 220 | text-align: center; 221 | color: #fff; 222 | border-radius: 12px; 223 | overflow: hidden; 224 | } 225 | 226 | .empty-states { 227 | padding-top: 60px; 228 | font-size: 18px; 229 | color: #AEB0B7; 230 | text-align: center; 231 | } 232 | 233 | .cart-list .goods-info { 234 | width: 68%; 235 | } 236 | 237 | .item-selector { 238 | width: 12%; 239 | } 240 | 241 | .icon-selector { 242 | position: relative; 243 | margin: 16px auto 0 auto; 244 | width: 16px; 245 | height: 16px; 246 | border-radius: 50%; 247 | cursor: pointer; 248 | } 249 | 250 | .selector-active { 251 | background-color: #7a45e5; 252 | border-color: #7a45e5; 253 | } 254 | 255 | .selector-active .icon { 256 | position: absolute; 257 | top: 2px; 258 | left: 2px; 259 | } 260 | 261 | .goods-num { 262 | position: absolute; 263 | right: 10px; 264 | top: 4px; 265 | width: 32px; 266 | color: #999; 267 | text-align: center; 268 | } 269 | 270 | .show-num { 271 | line-height: 28px; 272 | } 273 | 274 | .num-btn { 275 | width: 100%; 276 | height: 24px; 277 | font-size: 20px; 278 | line-height: 20px; 279 | cursor: pointer; 280 | } 281 | 282 | .action-bar { 283 | position: absolute; 284 | left: 0; 285 | bottom: 0; 286 | width: 100%; 287 | height: 52px; 288 | font-size: 15px; 289 | background-color: #fff; 290 | border-top: 1px solid #ddd; 291 | } 292 | 293 | .g-selector { 294 | float: left; 295 | width: 70px; 296 | margin-left: 4%; 297 | height: 52px; 298 | cursor: pointer; 299 | } 300 | 301 | .g-selector .item-selector { 302 | position: relative; 303 | display: inline-block; 304 | } 305 | 306 | .g-selector span { 307 | position: absolute; 308 | margin-left: 20px; 309 | color: #5F646E; 310 | top: 15px; 311 | } 312 | 313 | .total { 314 | float: right; 315 | color: #363636; 316 | font-size: 14px; 317 | line-height: 50px; 318 | margin-right: 20px; 319 | } 320 | 321 | .total span { 322 | color: #7A45E5; 323 | } 324 | 325 | .total b { 326 | font-size: 17px; 327 | margin-left: 4px; 328 | } 329 | 330 | .action-btn { 331 | float: right; 332 | width: 120px; 333 | height: 100%; 334 | color: #fff; 335 | text-align: center; 336 | font-weight: 300; 337 | line-height: 52px; 338 | cursor: pointer; 339 | } 340 | 341 | .buy-btn { 342 | background-color: #7A45E5; 343 | } 344 | 345 | .del-btn { 346 | background-color: #FF4069; 347 | } 348 | 349 | .del-box .total { 350 | display: none; 351 | } 352 | 353 | .del-box .buy-btn { 354 | display: none; 355 | } 356 | 357 | .del-box .del-btn { 358 | display: block; 359 | } 360 | .loading { 361 | position: absolute; 362 | top:0; 363 | left:0; 364 | bottom:0; 365 | right:0; 366 | background: rgba(0,0,0,0.12); 367 | color: #fff; 368 | font-size: 30px; 369 | display: flex; 370 | align-items: center; 371 | justify-content: center; 372 | } 373 | -------------------------------------------------------------------------------- /examples/cart/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 |
    5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/cart/index.js: -------------------------------------------------------------------------------- 1 | import {NavLink as Link, HashRouter, Route, Switch, Redirect} from 'react-router-dom'; 2 | import React from 'react'; 3 | import {render} from 'react-dom'; 4 | import store from './store'; 5 | import List from './components/list'; 6 | import Cart from './components/cart'; 7 | import './index.css'; 8 | 9 | class App extends React.Component { 10 | 11 | render() { 12 | return (
    13 | 14 | 15 |
    ); 16 | } 17 | } 18 | 19 | const routes = ( 20 | 21 | 22 | 23 | ); 24 | 25 | render(routes, document.querySelector('#root')); 26 | -------------------------------------------------------------------------------- /examples/cart/store.js: -------------------------------------------------------------------------------- 1 | import { Store } from '../../src/'; 2 | import devtools from '../../src/plugins/devtools'; 3 | import routePlugin from '../../src/plugins/route'; 4 | import { goods } from './config'; 5 | 6 | const store = new Store( 7 | { 8 | state: { 9 | list: { 10 | goods: [], 11 | currentCategory: null, 12 | currentSort: null, 13 | loading: false 14 | }, 15 | cart: { 16 | list: [] 17 | } 18 | }, 19 | actions: { 20 | fetch(state, payload) { 21 | state.list.loading = true; 22 | const ret = new Promise(resolve => { 23 | setTimeout(() => { 24 | resolve(); 25 | }, 500); 26 | }); 27 | ret.then(() => { 28 | this.transaction(() => { 29 | if (!payload || !payload.category) { 30 | state.list.goods = goods; 31 | } else { 32 | state.list.goods = goods.filter(item => item.type === payload.category); 33 | } 34 | state.list.currentCategory = payload && payload.category; 35 | state.list.loading = false; 36 | }); 37 | }); 38 | }, 39 | sortList(state, payload) { 40 | state.list.goods.sort((a, b) => a[payload] - b[payload]); 41 | state.list.currentSort = payload; 42 | }, 43 | addCartItem(state, payload) { 44 | const item = state.cart.list.filter(item => item.id === payload.id)[0]; 45 | if (!item) { 46 | state.cart.list.push({ 47 | ...payload, 48 | quantity: 1 49 | }); 50 | } else { 51 | item.quantity++; 52 | } 53 | }, 54 | select(state, payload) { 55 | const item = state.cart.list.filter(item => item.id === payload.id)[0]; 56 | item.selected = payload.checked; 57 | }, 58 | selectAll(state, payload) { 59 | state.cart.list.forEach(item => { 60 | item.selected = payload.checked; 61 | }); 62 | }, 63 | onAdd(state, payload) { 64 | payload.quantity++; 65 | }, 66 | onReduce(state, payload) { 67 | payload.quantity = Math.max(0, --payload.quantity); 68 | }, 69 | onRemove(state, payload) { 70 | const list = state.cart.list.filter(item => !item.selected); 71 | state.cart.list = list; 72 | } 73 | } 74 | }, 75 | { 76 | plugins: [devtools, routePlugin] 77 | } 78 | ); 79 | 80 | export default store; 81 | -------------------------------------------------------------------------------- /examples/counter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 |
    5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/counter/index.js: -------------------------------------------------------------------------------- 1 | import {Store, inject} from '../../src/'; 2 | import devtools from '../../src/plugins/devtools'; 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | 6 | const logger = function (store) { 7 | store.subscribe(obj => { 8 | console.log(obj.type, obj.payload, obj.state.toJSON()); 9 | }); 10 | }; 11 | 12 | const store = new Store({ 13 | state: { 14 | count: 0, 15 | list: [] 16 | }, 17 | actions: { 18 | add(state, payload) { 19 | state.count++; 20 | }, 21 | reduce(state, payload) { 22 | state.count--; 23 | }, 24 | async asyncAdd(state, payload) { 25 | await new Promise((resolve) => { 26 | setTimeout(() =>{ 27 | resolve(); 28 | }, 400); 29 | }); 30 | this.dispatch('add'); 31 | } 32 | } 33 | }, { 34 | plugins: [logger, devtools] 35 | }); 36 | 37 | @inject(store) 38 | class App extends React.Component { 39 | render() { 40 | const {state, dispatch} = this.props; 41 | const {count} = state; 42 | return (
    43 | {count} 44 | 45 | 46 | 47 |
    ); 48 | } 49 | } 50 | 51 | ReactDOM.render(, document.getElementById('root')); 52 | -------------------------------------------------------------------------------- /examples/pure/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 |
    5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/pure/index.js: -------------------------------------------------------------------------------- 1 | import {Store, inject} from '../../src/'; 2 | import devtools from '../../src/plugins/devtools'; 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | 6 | const logger = function (store) { 7 | store.subscribe(obj => { 8 | console.log(obj.type, obj.payload, obj.state.toJSON()); 9 | }); 10 | }; 11 | /** 12 | * 这个示例演示了pure属性的使用 13 | * 一般用在子组件也被inject的情况,这个时候父组件的刷新不会影响到子组件 14 | */ 15 | const store = new Store({ 16 | state: { 17 | count: 0, 18 | list: [] 19 | }, 20 | actions: { 21 | add(state, payload) { 22 | state.count++; 23 | }, 24 | reduce(state, payload) { 25 | state.count--; 26 | }, 27 | addList(state) { 28 | state.list.push(''); 29 | }, 30 | async asyncAdd(state, payload) { 31 | await new Promise((resolve) => { 32 | setTimeout(() =>{ 33 | resolve(); 34 | }, 400); 35 | }); 36 | this.dispatch('add'); 37 | } 38 | } 39 | }, { 40 | plugins: [logger, devtools] 41 | }); 42 | 43 | @inject(store, true) 44 | class Child extends React.Component { 45 | render() { 46 | console.log('child render!'); 47 | const {list} = this.props.state; 48 | return {list.length}; 49 | } 50 | } 51 | 52 | @inject(store) 53 | class App extends React.Component { 54 | render() { 55 | const {count} = this.props.state; 56 | const {dispatch} = this.props; 57 | return (
    58 | {count} 59 | 60 | 61 | 62 | 63 | 64 |
    ); 65 | } 66 | } 67 | 68 | ReactDOM.render(, document.getElementById('root')); 69 | -------------------------------------------------------------------------------- /examples/scenes/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/scenes/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import {Store, inject, Provider, connect} from '../../src/'; 4 | import devtools from '../../src/plugins/devtools'; 5 | 6 | const store = new Store({ 7 | name: 'list', 8 | state: { 9 | dataSource: [], 10 | complex: { 11 | a: 1 12 | } 13 | }, 14 | actions: { 15 | add(state, payload) { 16 | const {dataSource} = state; 17 | dataSource.push({ 18 | item: 'add' 19 | }); 20 | }, 21 | complex(state) { 22 | state.set('complex.a', 2); 23 | } 24 | } 25 | }); 26 | 27 | @inject(store) 28 | class List extends React.Component { 29 | render() { 30 | const {dataSource, complex} = this.props.state; 31 | return
    {dataSource.length}, {complex.a}
    ; 32 | } 33 | } 34 | 35 | const globalStore = new Store({}, { 36 | plugins: [devtools] 37 | }); 38 | 39 | globalStore.subscribe(function (state) { 40 | console.log(globalStore.state.toJSON()); 41 | }); 42 | 43 | @connect() 44 | class Button extends React.Component { 45 | onClick = () => { 46 | this.props.dispatch('list.add', 'add'); 47 | } 48 | onChange = () => { 49 | this.props.dispatch('list.complex'); 50 | } 51 | render() { 52 | return
    53 | 54 |
    ; 55 | } 56 | } 57 | 58 | ReactDOM.render( 59 | 60 |
    61 | 62 |
    64 |
    , 65 | document.getElementById('root') 66 | ); 67 | -------------------------------------------------------------------------------- /examples/todo-mvc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
    5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/todo-mvc/index.js: -------------------------------------------------------------------------------- 1 | import {NavLink as Link, HashRouter, Route, Switch, Redirect} from 'react-router-dom'; 2 | import React from 'react'; 3 | import {render} from 'react-dom'; 4 | import {Store, inject} from '../../src/'; 5 | import devtools from '../../src/plugins/devtools'; 6 | import routePlugin from '../../src/plugins/route'; 7 | 8 | const logger = function (store) { 9 | store.subscribe(obj => { 10 | console.log(obj.type, obj.state.toJSON()); 11 | }); 12 | }; 13 | 14 | const store = new Store({ 15 | state: { 16 | newTodo: '', 17 | todoList: [] 18 | }, 19 | actions: { 20 | add(state, payload) { 21 | const {todoList} = state; 22 | todoList.push({ 23 | title: payload, 24 | completed: false 25 | }); 26 | }, 27 | complete(state, payload) { 28 | payload.completed = !payload.completed; 29 | }, 30 | asyncAdd(state, payload) { 31 | setTimeout(() => { 32 | this.dispatch('add'); 33 | }, 500); 34 | } 35 | } 36 | }, { 37 | plugins: [logger, devtools, routePlugin] 38 | }); 39 | 40 | @inject(store) 41 | class App extends React.Component { 42 | onAdd = (e) => { 43 | if (e.keyCode === 13) { 44 | this.store.dispatch('add', e.target.value); 45 | this.store.dispatch('setValues', { 46 | newTodo: '' 47 | }); 48 | } 49 | } 50 | back = () => { 51 | this.store.dispatch('router.goBack'); 52 | } 53 | onChange = (e) => { 54 | this.store.dispatch('setValues', { 55 | newTodo: e.target.value 56 | }); 57 | } 58 | renderList() { 59 | const todoList = this.store.get('todoList'); 60 | const {params} = this.props.match; 61 | const filters = { 62 | 'all': todo => todo, 63 | 'active': todo => !todo.completed, 64 | 'complete': todo => todo.completed 65 | }; 66 | return todoList.filter(filters[params.filter]).map((todo, index)=> { 67 | return
  • 68 |
    69 | this.store.dispatch('complete', todo)}/> 75 | 76 |
    77 |
  • ; 78 | }); 79 | } 80 | render() { 81 | const {todoList, newTodo} = this.store.state; 82 | const todoCount = todoList.filter(todo => !todo.completed).length; 83 | return (
    84 |
    85 |

    TODO

    86 | 94 |
    95 | 96 |
    97 | 98 |
      99 | {this.renderList()} 100 |
    101 |
    102 | 103 |
    104 | 105 | {todoCount > 0 ? 剩余任务数量{todoCount}个 : null} 106 | {todoCount <= 0 ? '没有需要完成的任务' : null} 107 | 108 |
      109 |
    • All
    • 110 |
    • Active
    • 111 |
    • Complete
    • 112 |
    113 | 114 |
    115 |
    ); 116 | } 117 | } 118 | 119 | const btnStyle = { 120 | position: 'absolute', 121 | right: 5 122 | }; 123 | 124 | const routes = ( 125 | 126 | 127 | 128 | 129 | ); 130 | 131 | render(routes, document.querySelector('#root')); 132 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@royjs/core", 3 | "version": "2.0.7", 4 | "description": "A tiny mvvm library for react", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "rimraf lib && babel src --out-dir lib", 8 | "build:umd": "webpack --env dev && webpack --env build", 9 | "dev": "webpack --progress --colors --watch --env dev", 10 | "test": "nyc --reporter=html --reporter=text mocha --require babel-core/register --colors ./test/*.spec.js", 11 | "test:watch": "nyc --reporter=html --reporter=text mocha --require babel-core/register --colors -w ./test/*.spec.js", 12 | "prepublish": "npm run build && npm run build:umd", 13 | "example:todo": "parcel examples/todo-mvc/index.html", 14 | "example:counter": "parcel examples/counter/index.html", 15 | "example:scene": "parcel examples/scenes/index.html", 16 | "example:cart": "parcel examples/cart/index.html", 17 | "example:cart-create": "parcel examples/cart-create/index.html", 18 | "example:cart-inject": "parcel examples/cart-inject/index.html", 19 | "example:pure": "parcel examples/pure/index.html", 20 | "release": "npm run test && node ./scripts/release", 21 | "notice": "node ./scripts/notice" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/windyGex/roy.git" 26 | }, 27 | "files": [ 28 | "dist", 29 | "docs", 30 | "lib", 31 | "test", 32 | "HISTORY.md", 33 | "README.md", 34 | "index.js", 35 | "router.js", 36 | "request.js", 37 | "devtools.js" 38 | ], 39 | "keywords": [ 40 | "mvvm", 41 | "react", 42 | "tiny", 43 | "library", 44 | "universal", 45 | "umd", 46 | "commonjs" 47 | ], 48 | "author": "xing.gex", 49 | "license": "MIT", 50 | "bugs": { 51 | "url": "https://github.com/windyGex/roy/issues" 52 | }, 53 | "homepage": "https://github.com/windyGex/roy", 54 | "devDependencies": { 55 | "babel-cli": "^6.26.0", 56 | "babel-core": "^6.26.0", 57 | "babel-eslint": "^8.0.3", 58 | "babel-loader": "^7.1.2", 59 | "babel-plugin-add-module-exports": "^0.2.1", 60 | "babel-plugin-transform-async-generator-functions": "^6.24.1", 61 | "babel-plugin-transform-async-to-generator": "^6.24.1", 62 | "babel-plugin-transform-class-properties": "^6.24.1", 63 | "babel-plugin-transform-decorators": "^6.24.1", 64 | "babel-plugin-transform-decorators-legacy": "^1.3.5", 65 | "babel-preset-env": "^1.6.1", 66 | "babel-preset-es2015": "^6.24.1", 67 | "babel-preset-react": "^6.24.1", 68 | "babel-preset-stage-0": "^6.24.1", 69 | "babel-register": "^6.26.0", 70 | "benchmark": "^2.1.4", 71 | "chai": "^4.1.2", 72 | "chalk": "^2.4.1", 73 | "conventional-changelog": "^2.0.3", 74 | "dva": "^2.3.1", 75 | "enzyme": "^3.3.0", 76 | "enzyme-adapter-react-16": "^1.15.2", 77 | "eslint": "^4.13.1", 78 | "eslint-loader": "^1.9.0", 79 | "fs-extra": "^7.0.0", 80 | "inquirer": "^6.2.0", 81 | "jsdom": "^11.12.0", 82 | "mocha": "^4.0.1", 83 | "nyc": "^13.1.0", 84 | "parcel": "^1.9.7", 85 | "react": "16.x", 86 | "react-dom": "16.x", 87 | "react-test-renderer": "^15.6.2", 88 | "request": "^2.88.0", 89 | "rimraf": "^2.6.2", 90 | "semver": "^5.6.0", 91 | "sinon": "^6.1.5", 92 | "webpack": "^3.10.0", 93 | "yargs": "^10.0.3" 94 | }, 95 | "dependencies": { 96 | "axios": "^0.18.0", 97 | "react-router-dom": "^4.3.1", 98 | "shallowequal": "^1.1.0" 99 | }, 100 | "peerDependencies": { 101 | "react": "15.x || 16.x", 102 | "react-dom": "15.x || 16.x" 103 | }, 104 | "directories": { 105 | "example": "examples", 106 | "lib": "lib", 107 | "test": "test" 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /request.js: -------------------------------------------------------------------------------- 1 | var request = require('axios'); 2 | var DataSource = require('./lib/data-source'); 3 | 4 | DataSource.prototype.request = request; 5 | module.exports = request; 6 | -------------------------------------------------------------------------------- /router.js: -------------------------------------------------------------------------------- 1 | module.exports = require('react-router-dom'); 2 | module.exports.route = require('./lib/route'); 3 | module.exports.render = require('./lib/render'); 4 | module.exports.routePlugin = require('./lib/plugins/route'); 5 | -------------------------------------------------------------------------------- /scripts/notice.js: -------------------------------------------------------------------------------- 1 | const notice = require('./release/notice'); 2 | const co = require('co'); 3 | 4 | co(function * () { 5 | yield notice(); 6 | }).catch(err => { 7 | console.error('Notice failed', err.stack); 8 | });; 9 | -------------------------------------------------------------------------------- /scripts/release/changelog.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs-extra'); 3 | const semver = require('semver'); 4 | const request = require('request'); 5 | const inquirer = require('inquirer'); 6 | const conventionalChangelog = require('conventional-changelog'); 7 | 8 | const changelogPath = 'CHANGELOG.md'; 9 | const latestedLogPath = 'LATESTLOG.md'; 10 | const cwd = process.cwd(); 11 | 12 | module.exports = function * changelog() { 13 | const packagePath = path.resolve(__dirname, '../../package.json'); 14 | let packageInfo = require(packagePath); 15 | 16 | const tnpmInfo = yield getRemotePkgInfo(true); 17 | const tnpmVersion = tnpmInfo['dist-tags'].latest; 18 | 19 | if (tnpmInfo && !semver.gt(packageInfo.version, tnpmVersion)) { 20 | console.log(`[提示] [local:${packageInfo.version}] [tnpm:${tnpmVersion}] 请为本次提交指定新的版本号:`); 21 | 22 | let uptype = yield inquirer.prompt([ 23 | { 24 | type: 'list', 25 | name: 'type', 26 | message: '请选择版本升级的类型', 27 | choices: [ 28 | { 29 | name: 'z 位升级', 30 | value: 'z' 31 | }, { 32 | name: 'y 位升级', 33 | value: 'y' 34 | }, { 35 | name: 'x 位升级', 36 | value: 'x' 37 | }, { 38 | name: '不升级', 39 | value: 'null' 40 | } 41 | ] 42 | } 43 | ]); 44 | 45 | packageInfo.version = uptype.type === 'null' ? tnpmVersion : updateVersion(tnpmVersion, uptype.type); 46 | 47 | yield fs.writeJson(packagePath, packageInfo, { spaces: 2 }); 48 | 49 | console.log(`[提示] 回写版本号 ${packageInfo.version} 到 package.json success`); 50 | } else { 51 | console.log(`[提示] [本地 package.json 版本:${packageInfo.version}] > [tnpm 版本:${tnpmVersion}] `); 52 | } 53 | 54 | console.log(`正在生成 ${changelogPath} 文件,请稍等几秒钟...`); 55 | 56 | conventionalChangelog({ 57 | preset: 'angular' 58 | }) 59 | .on('data', (chunk) => { 60 | const log = chunk.toString().replace(/(\n## [.\d\w]+ )\(([\d-]+)\)\n/g, (all, s1, s2) => { 61 | return `${s1}/ ${s2}\n`; 62 | }); 63 | 64 | // TODO: 通过 ast 的方式插入到文件中,参考 build/generate-api.json 65 | 66 | let changelog = fs.readFileSync(changelogPath, 'utf8'); 67 | changelog = changelog.replace(/# Change Log\s\s/, '# Change Log \n\n' + log); 68 | fs.writeFileSync(changelogPath, changelog); 69 | 70 | const lines = log.split(/\n/g); 71 | let firstIndex = -1; 72 | for (let i = 0; i < lines.length; i++) { 73 | const line = lines[i]; 74 | if (/^#{1,3}/.test(line)) { 75 | firstIndex = i; 76 | break; 77 | } 78 | } 79 | 80 | if (firstIndex > -1) { 81 | fs.writeFileSync(latestedLogPath, log); 82 | } 83 | }); 84 | 85 | console.log(`成功将 ${changelogPath} 文件生成到 ${cwd} 目录下`); 86 | }; 87 | 88 | function getRemotePkgInfo(ignoreError = false) { 89 | return new Promise(function (resolve, reject) { 90 | var requestUrl = 'http://registry.npmjs.com/@royjs/core'; 91 | 92 | try { 93 | request({ 94 | url: requestUrl, 95 | timeout: 5000, 96 | json: true 97 | }, function (error, response, body) { 98 | if (error && !ignoreError) { 99 | reject(error); 100 | } 101 | resolve(body); 102 | }); 103 | } catch (err) { 104 | if (!ignoreError) { 105 | reject(err); 106 | } 107 | resolve(); 108 | } 109 | }); 110 | } 111 | 112 | function updateVersion(version, type = 'z', addend = 1) { 113 | if (!semver.valid(version)) { 114 | return version; 115 | } 116 | 117 | const versionArr = version.split('.'); 118 | 119 | switch (type) { 120 | case 'x': 121 | versionArr[2] = 0; 122 | versionArr[1] = 0; 123 | versionArr[0] = parseInt(versionArr[0]) + 1; 124 | break; 125 | case 'y': 126 | versionArr[2] = 0; 127 | versionArr[1] = parseInt(versionArr[1]) + 1; 128 | break; 129 | default: 130 | versionArr[2] = parseInt(versionArr[2]) + addend; 131 | } 132 | 133 | return versionArr.join('.'); 134 | } 135 | -------------------------------------------------------------------------------- /scripts/release/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const co = require('co'); 3 | const changelog = require('./changelog'); 4 | const notice = require('./notice'); 5 | const utils = require('../utils'); 6 | 7 | const cwd = process.cwd(); 8 | const runCmd = utils.runCmd; 9 | 10 | let packageInfo; 11 | 12 | co(function * () { 13 | yield changelog(); 14 | packageInfo = require('../../package.json'); 15 | yield pushMaster(); 16 | }).catch(err => { 17 | console.error('Release failed', err.stack); 18 | }); 19 | 20 | function * pushMaster() { 21 | yield runCmd('git checkout master'); 22 | yield runCmd('git add .'); 23 | yield runCmd(`git commit -m 'chore: Release-${packageInfo.version}'`); 24 | yield runCmd(`git tag ${packageInfo.version}`); 25 | yield runCmd(`git push origin ${packageInfo.version}`); 26 | yield runCmd('git push origin master'); 27 | } 28 | -------------------------------------------------------------------------------- /scripts/release/notice.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs-extra'); 3 | const inquirer = require('inquirer'); 4 | const chalk = require('chalk'); 5 | const { runCmd, dingGroups, ajaxPost } = require('../utils'); 6 | 7 | module.exports = function * () { 8 | const result = yield inquirer.prompt([{ 9 | name: 'sync', 10 | type: 'confirm', 11 | default: true, 12 | message: chalk.green.bold('是否同步发布信息到钉钉群') 13 | }]); 14 | if (!result.sync) { 15 | return; 16 | } 17 | 18 | const packageInfo = require(path.resolve('package.json')); 19 | const username = yield runCmd('git config --get user.name'); 20 | let latestLog = yield fs.readFile(path.resolve('LATESTLOG.md'), 'utf8'); 21 | latestLog = latestLog 22 | .replace(/\n+/g, '\n') 23 | .replace(/\(\[[\d\w]+\]\(https:\/\/[^\)]+\)\)/g, ''); 24 | 25 | const dingContent = `[公告] Royjs新版本发布通知 26 | - 版本号: ${packageInfo.version} 27 | - 发布人: ${username} 28 | 29 | 更新详情如下: 30 | 31 | ${latestLog}`; 32 | 33 | for (let i = 0; i < dingGroups.length; i++) { 34 | const url = dingGroups[i]; 35 | yield ajaxPost(url, { 36 | msgtype: 'markdown', 37 | markdown: { 38 | title: '[公告] Royjs新版本发布通知', 39 | text: dingContent 40 | } 41 | }); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /scripts/utils.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('child_process'); 2 | const request = require('request'); 3 | 4 | exports.runCmd = function (cmd, opts = { maxBuffer: 1024 * 5000 }) { 5 | return new Promise((resolve, reject) => { 6 | exec(cmd, opts, (err, stdout) => { 7 | if (err) { 8 | reject(err); 9 | } else { 10 | resolve(stdout); 11 | } 12 | }); 13 | }); 14 | }; 15 | 16 | exports.ajaxPost = function (url, data) { 17 | return new Promise((resolve, reject) => { 18 | request.post({ 19 | url, 20 | headers: { 'Content-Type': 'application/json' }, 21 | body: JSON.stringify(data) 22 | }, (error, response, body) => { 23 | error ? reject(error) : resolve(body); 24 | }); 25 | }); 26 | }; 27 | 28 | // 钉钉机器人 hook 29 | exports.dingGroups = [ 30 | // Hippo 超级战队 31 | 'https://oapi.dingtalk.com/robot/send?access_token=5d3c6985e73016080ebb2d6e3ac69603d239eed6cff3090b8c259537214ffd3f', 32 | // // Hippo 使用讨论 33 | 'https://oapi.dingtalk.com/robot/send?access_token=6122acc67589c1bf9ee362817245d12506005f27d01741a43a4fefeec5f99c7c' 34 | ]; 35 | -------------------------------------------------------------------------------- /src/compose.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Store from './store'; 4 | import inject from './inject'; 5 | 6 | const noop = function () {}; 7 | 8 | export default function compose({ 9 | name, 10 | view, 11 | components, 12 | state, 13 | actions, 14 | container, 15 | init = noop, 16 | mounted = noop, 17 | beforeUpdate = noop, 18 | updated = noop, 19 | beforeDestroy = noop 20 | }) { 21 | const store = new Store({ 22 | name, 23 | state, 24 | actions 25 | }); 26 | class ComposeComponent extends React.Component { 27 | constructor(...args) { 28 | super(...args); 29 | init.apply(this, args); 30 | } 31 | componentDidMount(...args) { 32 | mounted.apply(this, args); 33 | } 34 | componentWillUpdate(...args) { 35 | beforeUpdate.apply(this, args); 36 | } 37 | componentDidUpdate(...args) { 38 | updated.apply(this, args); 39 | } 40 | componentWillUnmount(...args) { 41 | beforeDestroy.apply(this, args); 42 | } 43 | render() { 44 | return view.call(this, { 45 | createElement: React.createElement, 46 | components 47 | }); 48 | } 49 | } 50 | const StoreComponent = inject(store)(ComposeComponent); 51 | if (container) { 52 | return ReactDOM.render(, document.querySelector(container)); 53 | } 54 | return StoreComponent; 55 | } 56 | -------------------------------------------------------------------------------- /src/connect.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Store from './store'; 4 | import eql from 'shallowequal'; 5 | import { isArray, isPlainObject, warning, get, change } from './utils'; 6 | import { StoreContext } from './provider'; 7 | 8 | const normalizer = (mapStateToProps, context, dispatch) => { 9 | let ret = {}; 10 | if (isArray(mapStateToProps)) { 11 | mapStateToProps.forEach(key => { 12 | if (typeof key === 'string') { 13 | ret[key] = context.get(key); 14 | } else { 15 | Object.keys(key).forEach(k => { 16 | ret[k] = context.get(key[k]); 17 | }); 18 | } 19 | }); 20 | } else if (typeof mapStateToProps === 'function') { 21 | ret = mapStateToProps(context); 22 | } else if (isPlainObject(mapStateToProps)) { 23 | const { state = [], actions = [] } = mapStateToProps; 24 | ret = normalizer(state, context); 25 | actions.forEach(action => { 26 | if (typeof action === 'string') { 27 | ret[action] = payload => { 28 | dispatch(action, payload); 29 | }; 30 | } else { 31 | Object.keys(action).forEach(k => { 32 | ret[k] = payload => { 33 | dispatch(action[k], payload); 34 | }; 35 | }); 36 | } 37 | }); 38 | } 39 | return ret; 40 | }; 41 | 42 | // connect([], config) -> state 43 | // connect({}, config) -> state, action 44 | // connect(() => {}, config) -> state 45 | const connect = function (mapStateToProps = state => state, config = {}) { 46 | return function withStore(Component) { 47 | const isFunctionComponent = !Component.prototype.render; 48 | class StoreWrapper extends React.Component { 49 | static contextType = StoreContext; 50 | constructor(props, context) { 51 | super(props, context); 52 | this._deps = {}; 53 | this._change = change.bind(this); 54 | this._get = get.bind(this); 55 | this.store = context && context.store || Store.get(); 56 | if (config.inject && context) { 57 | if (context.injectStore) { 58 | this.store = context.injectStore; 59 | } else { 60 | if (this.store === context.store) { 61 | warning('Royjs is using Provider store to connect because the inject store is undefined'); 62 | } else { 63 | warning('Royjs is using the first initialized store to connect because the inject store is undefined'); 64 | } 65 | } 66 | } 67 | this.store.on('change', this._change); 68 | this.store.history = this.store.history || this.props.history; 69 | 70 | if (config === true || config.pure) { 71 | this.shouldComponentUpdate = function (nextProps, nextState) { 72 | if (this.state !== nextState) { 73 | return true; 74 | } 75 | return !eql(this.props, nextProps); 76 | }; 77 | } 78 | } 79 | componentWillUnmount() { 80 | this.store.off('change', this._change); 81 | } 82 | componentDidMount() { 83 | const node = ReactDOM.findDOMNode(this); 84 | if (node) { 85 | node._instance = this; 86 | } 87 | } 88 | beforeRender() { 89 | this.store.on('get', this._get); 90 | } 91 | afterRender() { 92 | this.store.off('get', this._get); 93 | } 94 | setInstance = inc => { 95 | this._instance = inc; 96 | }; 97 | get instance() { 98 | return this._instance; 99 | } 100 | render() { 101 | this.beforeRender(); 102 | const { dispatch, state } = this.store; 103 | const props = normalizer(mapStateToProps, state, dispatch); 104 | let attrs = { 105 | ...this.props, 106 | ...props, 107 | dispatch 108 | }; 109 | if (!isFunctionComponent) { 110 | attrs.ref = this.setInstance; 111 | } 112 | const ret = ; 113 | this.afterRender(); 114 | return ret; 115 | } 116 | } 117 | return StoreWrapper; 118 | }; 119 | }; 120 | 121 | export default connect; 122 | -------------------------------------------------------------------------------- /src/data-source.js: -------------------------------------------------------------------------------- 1 | function DataSource(props) { 2 | Object.keys(props).forEach(key => { 3 | this[key] = props[key]; 4 | }); 5 | } 6 | DataSource.prototype = { 7 | url: '', 8 | request() { 9 | console.error('需要首先引入[@royjs/core/request]才能正常工作'); 10 | }, 11 | get(id, params) { 12 | return this.request.get(`${this.url}/${id}`, params); 13 | }, 14 | patch(id, params) { 15 | return this.request.patch(`${this.url}/${id}`, params); 16 | }, 17 | put(id, params) { 18 | return this.request.put(`${this.url}/${id}`, params); 19 | }, 20 | post(params) { 21 | return this.request.post(this.url, params); 22 | }, 23 | find(params) { 24 | return this.request.get(this.url, params); 25 | }, 26 | remove(id) { 27 | return this.request.delete(`${this.url}/${id}`); 28 | } 29 | }; 30 | export default DataSource; 31 | -------------------------------------------------------------------------------- /src/events.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-cond-assign */ 2 | 3 | function Events() {} 4 | 5 | Events.prototype = { 6 | on(type, callback) { 7 | let cache; 8 | if (!callback) return this; 9 | if (!this.__events) { 10 | Object.defineProperty(this, '__events', { 11 | value: {} 12 | }); 13 | } 14 | cache = this.__events; 15 | (cache[type] || (cache[type] = [])).push(callback); 16 | return this; 17 | }, 18 | off(type, callback) { 19 | const cache = this.__events; 20 | if (cache && cache[type]) { 21 | const index = cache[type].indexOf(callback); 22 | if (index !== -1) { 23 | cache[type].splice(index, 1); 24 | } 25 | } 26 | return this; 27 | }, 28 | trigger(type, evt) { 29 | const cache = this.__events; 30 | if (cache && cache[type]) { 31 | cache[type].forEach(callback => callback(evt)); 32 | } 33 | } 34 | }; 35 | 36 | // Mix `Events` to object instance or Class function. 37 | Events.mixTo = function (receiver) { 38 | receiver = typeof receiver === 'function' ? receiver.prototype : receiver; 39 | const proto = Events.prototype; 40 | for (let p in proto) { 41 | if (proto.hasOwnProperty(p)) { 42 | Object.defineProperty(receiver, p, { 43 | value: proto[p] 44 | }); 45 | } 46 | } 47 | }; 48 | 49 | export default Events; 50 | -------------------------------------------------------------------------------- /src/hooks.js: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useState, useRef } from 'react'; 2 | import Store from './store'; 3 | import { StoreContext } from './provider'; 4 | import { isPlainObject } from './utils'; 5 | 6 | export function useStore(mapStateToProps = state => state) { 7 | const ctx = useContext(StoreContext); 8 | const store = (ctx && ctx.store) || Store.get(); 9 | const deps = {}; 10 | const get = useRef(); 11 | const change = useRef(); 12 | const set = useRef(); 13 | const isUnmounted = useRef(); 14 | isUnmounted.current = false; 15 | get.current = (data) => { 16 | deps[data.key] = true; 17 | }; 18 | store.on('get', get.current); 19 | let [state, setState] = useState(() => mapStateToProps(store.state)); 20 | if (state === store.state) { 21 | state = {...state}; // for deps 22 | } 23 | store.off('get', get.current); 24 | set.current = (newState) => { 25 | if (Array.isArray(newState)) { 26 | newState = [...newState]; 27 | } else if (isPlainObject(newState)) { 28 | newState = { ...newState }; 29 | } 30 | if (!isUnmounted.current) { 31 | setState(newState); 32 | } 33 | }; 34 | change.current = (obj) => { 35 | obj = Array.isArray(obj) ? obj : [obj]; 36 | let matched; 37 | for (let index = 0; index < obj.length; index++) { 38 | const item = obj[index]; 39 | const match = Object.keys(deps).some((dep) => item.key.indexOf(dep) === 0); 40 | if (match) { 41 | matched = true; 42 | } 43 | } 44 | if (matched) { 45 | const newState = mapStateToProps(store.state); 46 | set.current(newState); 47 | } 48 | }; 49 | 50 | useEffect(() => { 51 | store.on('change', change.current); 52 | return () => { 53 | isUnmounted.current = true; 54 | store.off('change', change.current); 55 | }; 56 | }, [store]); 57 | return state; 58 | } 59 | 60 | export function useDispatch() { 61 | const ctx = useContext(StoreContext); 62 | const store = (ctx && ctx.store) || Store.get(); 63 | return store.dispatch; 64 | } 65 | -------------------------------------------------------------------------------- /src/hot-render.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | // eslint-disable-next-line no-unused-vars 4 | import Provider from './provider'; 5 | import Store from './store'; 6 | 7 | export default function hotRender(element, container, options = {}) { 8 | const root = typeof container === 'string' ? document.querySelector(container) : container; 9 | if (options.storeConfig) { 10 | const { storeConfig } = options; 11 | if (window.hotStore) { 12 | window.hotStore.hot(storeConfig.state, storeConfig.actions, '', storeConfig.plugins || []); 13 | } else { 14 | window.hotStore = new Store(storeConfig, { 15 | plugins: storeConfig.plugins || [] 16 | }); 17 | } 18 | const oldCreateElement = React.createElement; 19 | if (!oldCreateElement._patched) { 20 | React.createElement = (tag, props, ...args) => { 21 | let newProps = props; 22 | if (typeof tag !== 'string') { 23 | newProps = { 24 | dispatch: window.hotStore.dispatch, 25 | ...props 26 | }; 27 | } 28 | return oldCreateElement(tag, newProps, ...args); 29 | }; 30 | React.createElement._patched = true; 31 | } 32 | return ReactDOM.render({element}, root); 33 | } 34 | return ReactDOM.render(element, root); 35 | } 36 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import inject from './inject'; 2 | import connect from './connect'; 3 | import Provider from './provider'; 4 | import DataSource from './data-source'; 5 | import Store from './store'; 6 | import compose from './compose'; 7 | import hotRender from './hot-render'; 8 | import {throttle} from './utils'; 9 | import { useStore, useDispatch } from './hooks'; 10 | 11 | export default { 12 | DataSource, 13 | inject, 14 | connect, 15 | Store, 16 | Provider, 17 | compose, 18 | throttle, 19 | hotRender, 20 | useStore, 21 | useDispatch 22 | }; 23 | -------------------------------------------------------------------------------- /src/inject.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import eql from 'shallowequal'; 4 | import { warning, change, get } from './utils'; 5 | import { StoreContext } from './provider'; 6 | 7 | // inject(listStore) 8 | // inject(listStore, true) 9 | // inject('listStore', listStore) 10 | // inject({ 11 | // listStore, 12 | // noticeStore 13 | // }) 14 | const inject = function (key, value) { 15 | const length = arguments.length; 16 | let defaultProps = {}, 17 | pure = false; 18 | if (length === 1) { 19 | if (key.primaryKey) { 20 | defaultProps = { 21 | store: key 22 | }; 23 | } else { 24 | warning('inject multiple store will be removed at next version, using connect and Provider instead of it.'); 25 | defaultProps = key; 26 | } 27 | } else if (length === 2) { 28 | if (value === true || value === false) { 29 | pure = value; 30 | defaultProps = { 31 | store: key 32 | }; 33 | } else { 34 | defaultProps[key] = value; 35 | } 36 | } 37 | return function withStore(Component) { 38 | const { render, componentDidMount } = Component.prototype; 39 | class StoreWrapper extends React.Component { 40 | static contextType = StoreContext; 41 | 42 | constructor(props, context) { 43 | super(props, context); 44 | this._deps = {}; 45 | this._change = change.bind(this); 46 | this._get = get.bind(this); 47 | Object.keys(defaultProps).forEach(key => { 48 | const store = defaultProps[key]; 49 | this[key] = store; 50 | this[key].on('change', this._change); 51 | this[key].history = this[key].history || this.props.history; 52 | if (this[key].name) { 53 | this.context && this.context.store && this.context.store.mount(this[key].name, this[key]); 54 | } 55 | if (!Component.prototype._hasSet) { 56 | Object.defineProperty(Component.prototype, key, { 57 | get() { 58 | warning(`Using this.props.state instead of this.store.state 59 | and using this.props.dispatch instead of this.store.dispatch`); 60 | return store; 61 | } 62 | }); 63 | } 64 | }); 65 | Component.prototype._hasSet = true; 66 | 67 | // 劫持组件原型,收集依赖信息 68 | const that = this; 69 | Component.prototype.render = function (...args) { 70 | that.beforeRender(); 71 | const ret = render.apply(this, args); 72 | that.afterRender(); 73 | return ret; 74 | }; 75 | 76 | if (typeof componentDidMount === 'function') { 77 | Component.prototype.componentDidMount = function () { 78 | that.beforeRender(); 79 | componentDidMount.apply(this); 80 | that.afterRender(); 81 | }; 82 | } 83 | 84 | if (pure) { 85 | this.shouldComponentUpdate = function (nextProps, nextState) { 86 | if (this.state !== nextState) { 87 | return true; 88 | } 89 | return !eql(this.props, nextProps); 90 | }; 91 | } 92 | } 93 | 94 | beforeRender() { 95 | Object.keys(defaultProps).forEach(key => { 96 | this[key].on('get', this._get); 97 | }); 98 | } 99 | 100 | afterRender() { 101 | Object.keys(defaultProps).forEach(key => { 102 | this[key].off('get', this._get); 103 | }); 104 | } 105 | 106 | componentWillUnmount() { 107 | Object.keys(defaultProps).forEach(key => { 108 | this[key].off('change', this._change); 109 | this[key].off('get', this._get); 110 | }); 111 | // 还原组件原型,避免多次实例化导致的嵌套 112 | Component.prototype.render = render; 113 | if (componentDidMount) { 114 | Component.prototype.componentDidMount = componentDidMount; 115 | } 116 | } 117 | componentDidMount() { 118 | const node = ReactDOM.findDOMNode(this); 119 | if (node) { 120 | node._instance = this; 121 | } 122 | } 123 | setInstance = inc => { 124 | this._instance = inc; 125 | }; 126 | get instance() { 127 | return this._instance; 128 | } 129 | render() { 130 | let ret = {}; 131 | Object.keys(defaultProps).forEach(key => { 132 | const store = defaultProps[key]; 133 | if (key === 'store') { 134 | ret = { 135 | dispatch: store.dispatch, 136 | state: store.state 137 | }; 138 | } else { 139 | ret = { 140 | [`${key}Dispatch`]: store.dispatch, 141 | [`${key}State`]: store.state 142 | }; 143 | } 144 | }); 145 | return ; 146 | } 147 | } 148 | return StoreWrapper; 149 | }; 150 | }; 151 | 152 | export default inject; 153 | -------------------------------------------------------------------------------- /src/meta.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /src/plugins/devtools.js: -------------------------------------------------------------------------------- 1 | const devtools = function (store) { 2 | let tool; 3 | store.subscribe(obj => { 4 | if (window.hasOwnProperty('__REDUX_DEVTOOLS_EXTENSION__') && !tool) { 5 | tool = window.__REDUX_DEVTOOLS_EXTENSION__.connect(); 6 | tool.subscribe(message => { 7 | if (message.type === 'DISPATCH' && message.state) { 8 | store.set(JSON.parse(message.state)); 9 | } 10 | }); 11 | } 12 | tool && tool.send(obj.type, obj.state.toJSON()); 13 | }); 14 | }; 15 | 16 | export default devtools; 17 | -------------------------------------------------------------------------------- /src/plugins/route.js: -------------------------------------------------------------------------------- 1 | const route = function (store, actions) { 2 | ['push', 'replace', 'go', 'goBack', 'goForward'].forEach(method => { 3 | actions[`router.${method}`] = function (state, payload) { 4 | const { history } = store; 5 | history && history[method](payload); 6 | }; 7 | }); 8 | }; 9 | 10 | export default route; 11 | -------------------------------------------------------------------------------- /src/plugins/set-values.js: -------------------------------------------------------------------------------- 1 | const setValues = function (store, actions) { 2 | actions.setValues = function (state, payload, options) { 3 | store.transaction(() => { 4 | state.set(payload, options); 5 | }); 6 | }; 7 | }; 8 | 9 | export default setValues; 10 | -------------------------------------------------------------------------------- /src/provider.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const StoreContext = React.createContext(null); 4 | export default function Provider(props) { 5 | return ( 6 | 10 | {props.children} 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/proxy.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | import Events from './events'; 3 | import { isPlainObject } from './utils'; 4 | 5 | function wrap(key, value, ret) { 6 | if (!(value && value.$proxy)) { 7 | if (isPlainObject(value)) { 8 | value = observable(value, ret); 9 | value.on('get', function (args) { 10 | const currentKey = `${key}.${args.key}`; 11 | ret.trigger('get', { 12 | key: currentKey 13 | }); 14 | }); 15 | value.on('change', function (args) { 16 | const currentKey = `${key}.${args.key}`; 17 | ret.trigger('change', { 18 | ...args, 19 | ...{ 20 | key: currentKey 21 | } 22 | }); 23 | }); 24 | } else if (Array.isArray(value)) { 25 | value = observable(value, ret); 26 | value.on('get', function (args) { 27 | const currentKey = `${key}.${args.key}`; 28 | ret.trigger('get', { 29 | key: currentKey 30 | }); 31 | }); 32 | value.on('change', function (args) { 33 | const mixArgs = { ...args }; 34 | if (!args.key) { 35 | mixArgs.key = key; 36 | } else { 37 | mixArgs.key = `${key}.${args.key}`; 38 | } 39 | ret.trigger('change', mixArgs); 40 | }); 41 | } 42 | } 43 | return value; 44 | } 45 | 46 | function rawJSON(target) { 47 | if (Array.isArray(target)) { 48 | return target.map(item => { 49 | if (item && item.toJSON) { 50 | return item.toJSON(); 51 | } 52 | return item; 53 | }); 54 | } 55 | const ret = {}; 56 | Object.keys(target).forEach(key => { 57 | const value = target[key]; 58 | if (value && value.toJSON) { 59 | ret[key] = value.toJSON(); 60 | } else { 61 | ret[key] = value; 62 | } 63 | }); 64 | return ret; 65 | } 66 | 67 | const objectProcess = { 68 | get(options) { 69 | const { target, events } = options; 70 | return function getValue(path, slient = false) { 71 | if (!path) { 72 | return null; 73 | } 74 | 75 | if (typeof path !== 'string') { 76 | return target[path]; 77 | } 78 | // 避免使用.作为key的尴尬, 优先直接获取值 79 | let val = target[path]; 80 | if (val == null) { 81 | let key; 82 | const field = path.split('.'); 83 | if (field.length) { 84 | key = field[0]; 85 | // lists[1].name 86 | if (key.indexOf('[') >= 0) { 87 | key = key.match(/(.*)\[(.*)\]/); 88 | if (key) { 89 | try { 90 | val = target[key[1]][key[2]]; 91 | } catch (e) { 92 | throw new Error(`state ${key[1]} is undefined!`); 93 | } 94 | } 95 | } else { 96 | val = target[field[0]]; 97 | } 98 | if (val) { 99 | for (let i = 1; i < field.length; i++) { 100 | val = val[field[i]]; 101 | /* eslint-disable */ 102 | if (val == null) { 103 | break; 104 | } 105 | } 106 | } 107 | } 108 | } 109 | 110 | if (!slient) { 111 | events.trigger('get', { 112 | key: path 113 | }); 114 | } 115 | return val; 116 | }; 117 | }, 118 | set(options) { 119 | const { events, target } = options; 120 | const _set = function(object, path, value) { 121 | let keyNames = path.split('.'), 122 | keyName = keyNames[0], 123 | oldObject = object; 124 | 125 | object = object.get(keyName); 126 | if (typeof object == 'undefined') { 127 | object = wrap(keyName, {}, target.$proxy); 128 | oldObject[keyName] = object; 129 | } 130 | if (isPlainObject(object)) { 131 | keyNames.splice(0, 1); 132 | return object.set(keyNames.join('.'), value); 133 | } 134 | }; 135 | return function setValue(path, value, config = {}) { 136 | if (isPlainObject(path)) { 137 | Object.keys(path).forEach(key => { 138 | let val = path[key]; 139 | setValue(key, val, value); 140 | }); 141 | return; 142 | } 143 | let nested, 144 | getValue = objectProcess.get(options), 145 | currentValue = getValue(path, true); 146 | 147 | value = wrap(path, value, target.$proxy); 148 | if (path.indexOf('.') > 0) { 149 | nested = true; 150 | } 151 | if (nested) { 152 | _set(target.$proxy, path, value); 153 | } else if (path.indexOf('[') >= 0) { 154 | let key = path.match(/(.*)\[(.*)\]/); 155 | if (key) { 156 | target[key[1]].splice(key[2], 1, value); 157 | return; 158 | } else { 159 | throw new Error('Not right key' + path); 160 | } 161 | } else { 162 | target[path] = value; 163 | } 164 | if ((currentValue !== value || config.forceUpdate) && !nested) { 165 | events.trigger('change', { 166 | key: path 167 | }); 168 | } 169 | }; 170 | }, 171 | on(options) { 172 | return function on(...args) { 173 | const { events } = options; 174 | return events.on.apply(events, args); 175 | }; 176 | }, 177 | off(options) { 178 | return function off(...args) { 179 | const { events } = options; 180 | return events.off.apply(events, args); 181 | }; 182 | }, 183 | trigger(options) { 184 | return function trigger(...args) { 185 | const { events } = options; 186 | return events.trigger.apply(events, args); 187 | }; 188 | }, 189 | toJSON(options) { 190 | return function toJSON() { 191 | const target = options.target; 192 | return rawJSON(target); 193 | }; 194 | }, 195 | reset(options) { 196 | const { target } = options; 197 | return function() { 198 | Object.keys(target).forEach(key => { 199 | target.$proxy.set(key, undefined); 200 | }); 201 | }; 202 | } 203 | }; 204 | 205 | const arrayProcess = {}; 206 | 207 | ['on', 'off', 'trigger'].forEach(method => { 208 | arrayProcess[method] = objectProcess[method]; 209 | }); 210 | 211 | ['pop', 'shift', 'push', 'unshift', 'sort', 'reverse', 'splice'].forEach(method => { 212 | arrayProcess[method] = options => { 213 | const { target, events } = options; 214 | return function(...args) { 215 | // todo: 这里利用了新增项会调用set方法的特性,没有对新增项进行observable包裹 216 | const ret = Array.prototype[method].apply(target.$proxy, args); 217 | target.$proxy.trigger('change', {}); 218 | return ret; 219 | }; 220 | }; 221 | }); 222 | 223 | const whiteList = ['_reactFragment', 'constructor']; 224 | 225 | const observable = function observable(object) { 226 | if (object.$proxy) { 227 | return object; 228 | } 229 | 230 | const proxy = function proxy(object, parent) { 231 | const events = new Events(); 232 | let returnProxy; 233 | const handler = { 234 | get(target, key) { 235 | if (key === '$raw') { 236 | return rawJSON(target); 237 | } 238 | if (Array.isArray(target) && arrayProcess.hasOwnProperty(key)) { 239 | return arrayProcess[key]({ 240 | target, 241 | key, 242 | events 243 | }); 244 | } 245 | if (objectProcess.hasOwnProperty(key)) { 246 | return objectProcess[key]({ 247 | target, 248 | key, 249 | events 250 | }); 251 | } 252 | if (Array.isArray(target) || whiteList.indexOf(key) > -1 || (typeof key === 'string' && key.charAt(0) === '_')) { 253 | return Reflect.get(target, key); 254 | } 255 | const getValue = objectProcess.get({ 256 | target, 257 | key, 258 | events 259 | }); 260 | return getValue(key); 261 | }, 262 | set(target, key, value) { 263 | if (Array.isArray(target)) { 264 | if (isPlainObject(value)) { 265 | value = observable(value); 266 | value.on('change', args => { 267 | // todo: 待优化,现在任何item的更新都会触发针对list的更新 268 | target.$proxy.trigger('change', {}); 269 | }); 270 | } 271 | const ret = Reflect.set(target, key, value); 272 | return true; 273 | } 274 | objectProcess.set({ 275 | target, 276 | events 277 | })(key, value); 278 | return true; 279 | } 280 | }; 281 | returnProxy = new Proxy(object, handler); 282 | if (!object.$proxy) { 283 | Object.defineProperties(object, { 284 | $proxy: { 285 | get() { 286 | return returnProxy 287 | } 288 | }, 289 | $raw: { 290 | get() { 291 | return rawJSON(object) 292 | } 293 | }, 294 | toJSON: { 295 | get() { 296 | return function toJSON() { 297 | return rawJSON(object) 298 | } 299 | } 300 | } 301 | }); 302 | } 303 | return returnProxy; 304 | }; 305 | const ret = proxy(object); 306 | if (isPlainObject(object)) { 307 | for (let key in object) { 308 | if (object.hasOwnProperty(key)) { 309 | object[key] = wrap(key, object[key], ret); 310 | } 311 | } 312 | } else if (Array.isArray(object)) { 313 | object.forEach((item, index) => { 314 | object[index] = wrap(index, object[index], ret); 315 | }); 316 | } 317 | return ret; 318 | }; 319 | 320 | export default observable; 321 | -------------------------------------------------------------------------------- /src/render.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import { HashRouter, BrowserRouter, Switch } from 'react-router-dom'; 5 | import meta from './meta'; 6 | import {warning} from './utils'; 7 | 8 | export default function render(element, container, options = {}) { 9 | warning('[ render ] method is deprecated and will be removed at next version.'); 10 | const root = typeof container === 'string' ? document.querySelector(container) : container; 11 | if (meta.route) { 12 | const Router = options.browser ? BrowserRouter : HashRouter; 13 | return ReactDOM.render( 14 | 15 | {element} 16 | , 17 | root 18 | ); 19 | } 20 | return ReactDOM.render(element, container); 21 | } 22 | -------------------------------------------------------------------------------- /src/route.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | /*eslint-disable*/ 3 | import { Route } from 'react-router-dom'; 4 | import meta from './meta'; 5 | import {warning} from './utils'; 6 | 7 | const route = function(path, options) { 8 | meta.route = true; 9 | return function withRoute(Component) { 10 | warning('[ route ] method is deprecated and will be removed at next version.'); 11 | return class RouterWrapper extends React.Component { 12 | renderPath(path) { 13 | return path.map(item => { 14 | return ; 15 | }); 16 | } 17 | render() { 18 | if (Array.isArray(path)) { 19 | return
    {this.renderPath(path)}
    ; 20 | } 21 | return ; 22 | } 23 | }; 24 | }; 25 | }; 26 | 27 | export default route; 28 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import Events from './events'; 2 | import observable from './proxy'; 3 | import DataSource from './data-source'; 4 | import setValues from './plugins/set-values'; 5 | import { isArray, deepCopy, diff } from './utils'; 6 | 7 | let globalStore; 8 | 9 | class Store extends Events { 10 | // create({}) 11 | // create(name, {}) 12 | // create(store, name, params); 13 | static create = function (store, name, params) { 14 | if (arguments.length === 1) { 15 | params = store; 16 | store = globalStore; 17 | name = params.name; 18 | } else if (arguments.length === 2) { 19 | params = name; 20 | name = store; 21 | store = globalStore; 22 | } 23 | const { state, actions } = params; 24 | if (!globalStore) { 25 | console.warn('The store has not been initialized yet!'); 26 | } 27 | const stateKeys = Object.keys(state); 28 | if (stateKeys.length === 0) { 29 | store.set(name, {}); 30 | } else { 31 | stateKeys.forEach(key => { 32 | store.set(`${name}.${key}`, state[key]); 33 | }); 34 | } 35 | store._wrapActions(actions, store.get(name), name); 36 | return store.get(name); 37 | }; 38 | // mount({}) 39 | // mount(name, {}) 40 | // mount(target, name, store) 41 | static mount = function (target, name, store) { 42 | if (arguments.length === 1) { 43 | store = target; 44 | target = globalStore; 45 | name = store.name; 46 | } else if (arguments.length === 2) { 47 | store = name; 48 | name = target; 49 | target = globalStore; 50 | } 51 | let { state, actions } = store; 52 | store.on('change', args => { 53 | args = isArray(args) ? args : [args]; 54 | target.transaction(() => { 55 | for (let i = 0; i < args.length; i++) { 56 | const item = args[i]; 57 | const value = store.get(item.key); 58 | target.set(`${name}.${item.key}`, value); 59 | } 60 | }); 61 | }); 62 | store.on('get', args => { 63 | const obj = { ...args }; 64 | obj.key = `${name}.${obj.key}`; 65 | target.trigger('get', obj); 66 | }); 67 | store.on('actions', args => { 68 | target.trigger('actions', args); 69 | }); 70 | return Store.create(target, name, { 71 | state: state.toJSON(), 72 | actions 73 | }); 74 | }; 75 | static get = function () { 76 | return globalStore; 77 | }; 78 | // state 79 | // actions 80 | constructor(params = {}, options = {}) { 81 | super(params, options); 82 | let { name, state, actions = {} } = params; 83 | const { strict = false, plugins = [] } = options; 84 | state = { 85 | ...this.state, 86 | ...state 87 | }; 88 | this.originState = deepCopy(state); 89 | this.model = observable(state); 90 | this.model.on('get', args => { 91 | this.trigger('get', args); 92 | }); 93 | this.model.on('change', (args = {}) => { 94 | try { 95 | this._startBatch(); 96 | if (this.inBatch > 1) { 97 | this.pendingUnobservations.push(args); 98 | } else { 99 | this.trigger('change', args); 100 | } 101 | } finally { 102 | this._endBatch(); 103 | } 104 | }); 105 | this.inBatch = 0; 106 | this.pendingUnobservations = []; 107 | this.actions = {}; 108 | this.strict = strict; 109 | this.allowModelSet = !strict; 110 | this.state = this.model; 111 | this.url = options.url; 112 | this.name = name; 113 | this.primaryKey = options.primaryKey || 'id'; 114 | this._initPlugins(plugins, actions); 115 | this._wrapActions(actions, this.model); 116 | if (!globalStore) { 117 | globalStore = this; 118 | } 119 | } 120 | get dataSource() { 121 | return new DataSource({ 122 | url: this.url, 123 | primaryKey: this.primaryKey 124 | }); 125 | } 126 | get request() { 127 | return this.dataSource.request; 128 | } 129 | _initPlugins(plugins, actions) { 130 | const p = [...plugins]; 131 | p.unshift(setValues); 132 | p.forEach(plugin => { 133 | if (typeof plugin === 'function') { 134 | plugin(this, actions); 135 | } 136 | }); 137 | } 138 | get(key) { 139 | return this.model.get(key); 140 | } 141 | set(key, value, options = {}) { 142 | return this.model.set(key, value, options); 143 | } 144 | hot(state = {}, actions = {}, prefix, plugins) { 145 | this.transaction(() => { 146 | const keyMap = {}; 147 | const diffKeys = diff(this.originState, state) 148 | .concat(diff(state, this.originState)) 149 | .filter(key => keyMap[key] ? false : (keyMap[key] = true)); 150 | 151 | const setValue = (key, value) => { 152 | key = prefix ? `${prefix}.${key}` : key; 153 | this.set(key, value); 154 | }; 155 | 156 | diffKeys.forEach(key => { 157 | const value = key.split('.').reduce((obj, curKey) => obj && obj[curKey], state); 158 | setValue(key, value); 159 | }); 160 | this.originState = deepCopy(state); 161 | }); 162 | this._initPlugins(plugins, actions); 163 | this._wrapActions(actions, this.model, prefix); 164 | } 165 | _startBatch() { 166 | this.inBatch++; 167 | } 168 | _endBatch() { 169 | // 最外层事务结束时,才开始执行 170 | if (--this.inBatch === 0) { 171 | // 发布所有state待定的改变 172 | this._runPendingObservations(); 173 | } 174 | } 175 | _runPendingObservations() { 176 | if (this.pendingUnobservations.length) { 177 | this.trigger('change', this.pendingUnobservations.slice()); 178 | this.pendingUnobservations = []; 179 | } 180 | } 181 | _wrapActions(actions = {}, state, prefix) { 182 | Object.keys(actions).forEach(type => { 183 | const actionType = prefix ? `${prefix}.${type}` : type; 184 | const that = this; 185 | const action = actions[type]; 186 | function actionPayload(payload, options) { 187 | const ret = action.call(that, state, payload, { 188 | state: that.state, 189 | dispatch: that.dispatch, 190 | ...options 191 | }); 192 | that.trigger('actions', { 193 | type: actionType, 194 | payload, 195 | state: that.model 196 | }); 197 | return ret; 198 | } 199 | if (!action._set) { 200 | this.actions[actionType] = actionPayload; 201 | actionPayload._set = true; 202 | } else { 203 | this.actions[actionType] = action; 204 | } 205 | }); 206 | } 207 | transaction = fn => { 208 | this._startBatch(); 209 | try { 210 | return fn.apply(this); 211 | } finally { 212 | this._endBatch(); 213 | } 214 | }; 215 | dispatch = (type, payload, options) => { 216 | const action = this.actions[type]; 217 | if (!action || typeof action !== 'function') { 218 | throw new Error(`Cant find ${type} action`); 219 | } 220 | this.allowModelSet = true; 221 | const ret = action(payload, options); 222 | if (this.strict) { 223 | this.allowModelSet = false; 224 | } 225 | return ret; 226 | }; 227 | subscribe(callback) { 228 | this.on('actions', function ({ type, payload, state }) { 229 | callback({ 230 | type, 231 | payload, 232 | state 233 | }); 234 | }); 235 | } 236 | create(name, params) { 237 | return Store.create(this, name, params); 238 | } 239 | mount(name, store) { 240 | return Store.mount(this, name, store); 241 | } 242 | } 243 | 244 | export default Store; 245 | 246 | export const create = Store.create; 247 | 248 | export const get = Store.get; 249 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const checkType = function (item) { 2 | return Object.prototype.toString.call(item).replace(/\[object\s(.*)\]/, (all, matched) => matched); 3 | }; 4 | 5 | export const isPlainObject = function (item) { 6 | return checkType(item) === 'Object'; 7 | }; 8 | export const isArray = function (item) { 9 | return checkType(item) === 'Array'; 10 | }; 11 | 12 | export const throttle = function (target, key, descriptor) { 13 | const fn = target[key]; 14 | const limit = 300; 15 | let wait = false; 16 | descriptor.value = function (...args) { 17 | if (!wait) { 18 | fn.apply(this, args); 19 | wait = true; 20 | setTimeout(function () { 21 | wait = false; 22 | }, limit); 23 | } 24 | }; 25 | }; 26 | 27 | export const warning = function warning(msg) { 28 | console.error(msg); 29 | }; 30 | 31 | export const deepCopy = function deepCopy(params) { 32 | return JSON.parse(JSON.stringify(params)); 33 | }; 34 | 35 | export const jsonEqual = function equal(x, y) { 36 | if (checkType(x) === checkType(y)) { 37 | return JSON.stringify(x) === JSON.stringify(y); 38 | } 39 | return false; 40 | 41 | }; 42 | 43 | export const diff = function diff(left, right, previousPath = '', keys = []) { 44 | Object.entries(left).forEach(([k, v]) => { 45 | const currentPath = previousPath ? `${previousPath}.${k}` : k; 46 | if (isPlainObject(v) && isPlainObject(right[k])) { 47 | diff(v, right[k], currentPath, keys); 48 | } else if (!jsonEqual(right[k], v)) { 49 | keys.push(currentPath); 50 | } 51 | }); 52 | return keys; 53 | }; 54 | 55 | export const change = function change(obj) { 56 | let matched; 57 | obj = isArray(obj) ? obj : [obj]; 58 | for (let index = 0; index < obj.length; index++) { 59 | const item = obj[index]; 60 | const match = Object.keys(this._deps).some(dep => item.key.indexOf(dep) === 0); 61 | if (match) { 62 | matched = match; 63 | } 64 | } 65 | if (matched) { 66 | this.forceUpdate(); 67 | } 68 | }; 69 | 70 | export const get = function get(data) { 71 | this._deps[data.key] = true; 72 | }; 73 | -------------------------------------------------------------------------------- /test/case.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import chai from 'chai'; 3 | import React from 'react'; 4 | import Enzyme, { mount } from 'enzyme'; 5 | import Adapter from 'enzyme-adapter-react-16'; 6 | import { Store, inject, connect, Provider, compose } from '../src/index'; 7 | import { JSDOM } from 'jsdom'; 8 | import sinon from 'sinon'; 9 | 10 | const doc = new JSDOM(''); 11 | global.document = doc.window.document; 12 | global.window = doc.window; 13 | 14 | Enzyme.configure({ adapter: new Adapter() }); 15 | 16 | chai.expect(); 17 | 18 | const expect = chai.expect; 19 | 20 | describe('support inject store to React Component', () => { 21 | let store, App, wrapper; 22 | class Demo extends React.Component { 23 | render() { 24 | const [item] = this.props.state.a; 25 | return {item && item.status ? 'true' : 'false'}; 26 | } 27 | } 28 | beforeEach(() => { 29 | store = new Store({ 30 | state: { 31 | a: [] 32 | } 33 | }); 34 | App = inject(store)(Demo); 35 | wrapper = mount(); 36 | }); 37 | 38 | afterEach(() => { 39 | store = null; 40 | App = null; 41 | wrapper = null; 42 | }); 43 | 44 | // set; 45 | it('should support item change for array', () => { 46 | store.state.set('a', [ 47 | { 48 | status: true 49 | } 50 | ]); 51 | expect(wrapper.find('span').text()).eql('true'); 52 | store.state.a[0].set('status', false); 53 | expect(wrapper.find('span').text()).eql('false'); 54 | store.state.set('a', [ 55 | { 56 | status: true 57 | } 58 | ]); 59 | expect(wrapper.find('span').text()).eql('true'); 60 | }); 61 | 62 | it('should support array push method', () => { 63 | expect(wrapper.find('span').text()).eql('false'); 64 | store.state.a.push({ 65 | status: true 66 | }); 67 | expect(wrapper.find('span').text()).eql('true'); 68 | store.state.a[0].set('status', false); 69 | expect(wrapper.find('span').text()).eql('false'); 70 | }); 71 | 72 | it('should support array splice method', () => { 73 | expect(wrapper.find('span').text()).eql('false'); 74 | store.state.a.splice(0, 0, { 75 | status: true 76 | }); 77 | expect(wrapper.find('span').text()).eql('true'); 78 | store.state.a[0].set('status', false); 79 | expect(wrapper.find('span').text()).eql('false'); 80 | }); 81 | 82 | it('should support array pop method', () => { 83 | expect(wrapper.find('span').text()).eql('false'); 84 | store.state.a.splice(0, 0, { 85 | status: true 86 | }); 87 | expect(wrapper.find('span').text()).eql('true'); 88 | store.state.a.pop(); 89 | expect(wrapper.find('span').text()).eql('false'); 90 | }); 91 | 92 | it('should support set key method', () => { 93 | expect(wrapper.find('span').text()).eql('false'); 94 | store.state.set('a[0].status', true); 95 | expect(wrapper.find('span').text()).eql('true'); 96 | store.state.a[0].set('status', false); 97 | expect(wrapper.find('span').text()).eql('false'); 98 | store.state.set('a[0].status', true); 99 | expect(wrapper.find('span').text()).eql('true'); 100 | }); 101 | 102 | it('should support proxy', () => { 103 | store.state.a = [ 104 | { 105 | status: true 106 | } 107 | ]; 108 | expect(wrapper.find('span').text()).eql('true'); 109 | store.state.a[0].status = false; 110 | expect(wrapper.find('span').text()).eql('false'); 111 | }); 112 | 113 | it('should support push proxy', () => { 114 | store.state.a.push({ 115 | status: true 116 | }); 117 | expect(wrapper.find('span').text()).eql('true'); 118 | store.state.a[0].status = false; 119 | expect(wrapper.find('span').text()).eql('false'); 120 | }); 121 | 122 | it('avoid object sort', () => { 123 | store.state.sort = 1; 124 | expect(store.state.sort).eql(1); 125 | }); 126 | }); 127 | 128 | describe('support render', () => { 129 | it('should render', () => { 130 | const store = new Store({ 131 | state: { 132 | data: {} 133 | }, 134 | actions: { 135 | updateTime (state, time) { 136 | state.set('data.time', time) 137 | } 138 | } 139 | }); 140 | @connect() 141 | class Todo2 extends React.Component { 142 | render () { 143 | const { data, dispatch } = this.props; 144 | return ( 145 |
    {data.time}
    146 | ) 147 | } 148 | } 149 | class App extends React.Component { 150 | render () { 151 | return ( 152 | 153 | 154 | 155 | ) 156 | } 157 | } 158 | const app = mount(); 159 | store.state.set('data.time', 1) 160 | expect(app.find('div').text()).eql('1') 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /test/hooks.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import chai from 'chai'; 3 | import React, { useEffect } from 'react'; 4 | import Enzyme, { mount } from 'enzyme'; 5 | import Adapter from 'enzyme-adapter-react-16'; 6 | import { Store, inject, connect, Provider, useStore, useDispatch } from '../src/index'; 7 | import { JSDOM } from 'jsdom'; 8 | import sinon from 'sinon'; 9 | 10 | const doc = new JSDOM(''); 11 | global.document = doc.window.document; 12 | global.window = doc.window; 13 | 14 | Enzyme.configure({ adapter: new Adapter() }); 15 | 16 | chai.expect(); 17 | 18 | const expect = chai.expect; 19 | 20 | describe('support for function component', () => { 21 | it('should support hooks for function component', () => { 22 | const store = new Store({ 23 | state: { 24 | value: 1, 25 | }, 26 | actions: { 27 | add(state) { 28 | state.value++; 29 | }, 30 | }, 31 | }); 32 | const App = (props) => { 33 | const value = useStore((state) => state.value); 34 | const dispatch = useDispatch(); 35 | return dispatch('add')}>{value}; 36 | }; 37 | const app = mount( 38 | 39 | 40 | 41 | ); 42 | expect(app.find('span').text()).equal('1'); 43 | app.find('span').simulate('click'); 44 | expect(app.find('span').text()).equal('2'); 45 | expect(store.state.value).equal(2); 46 | }); 47 | 48 | it('should support connect for function component', () => { 49 | const store = new Store({ 50 | state: { 51 | value: 1, 52 | }, 53 | actions: { 54 | add(state) { 55 | state.value++; 56 | }, 57 | }, 58 | }); 59 | const App = (props) => { 60 | return props.dispatch('add')}>{props.value}; 61 | }; 62 | const Root = connect((state) => state)(App); 63 | const app = mount( 64 | 65 | 66 | 67 | ); 68 | expect(app.find('span').text()).equal('1'); 69 | app.find('span').simulate('click'); 70 | expect(app.find('span').text()).equal('2'); 71 | expect(store.state.value).equal(2); 72 | }); 73 | 74 | it('should support connect state for function component', () => { 75 | const store = new Store({ 76 | state: { 77 | value: 1, 78 | }, 79 | actions: { 80 | add(state) { 81 | state.value++; 82 | }, 83 | }, 84 | }); 85 | const App = (props) => { 86 | const state = useStore((state) => state); 87 | const dispatch = useDispatch(); 88 | return dispatch('add')}>{state.value}; 89 | }; 90 | const app = mount( 91 | 92 | 93 | 94 | ); 95 | expect(app.find('span').text()).equal('1'); 96 | app.find('span').simulate('click'); 97 | expect(app.find('span').text()).equal('2'); 98 | expect(store.state.value).equal(2); 99 | }); 100 | 101 | it('should not change when no necessary deps', (done) => { 102 | const store = new Store({ 103 | state: { 104 | a: 1, 105 | b: 1, 106 | }, 107 | actions: { 108 | a(state) { 109 | state.a++; 110 | }, 111 | b(state) { 112 | state.b++; 113 | }, 114 | }, 115 | }); 116 | const App = (props) => { 117 | const state = useStore((state) => state); 118 | const dispatch = useDispatch(); 119 | return dispatch('a')}>{state.a}; 120 | }; 121 | 122 | let a = 0; 123 | const Child = (props) => { 124 | const state = useStore((state) => state.b); 125 | useEffect(() => { 126 | a++; 127 | }); 128 | return {state}; 129 | }; 130 | const app = mount( 131 | 132 | 133 | 134 | 135 | ); 136 | expect(app.find('span').text()).equal('1'); 137 | app.find('span').simulate('click'); 138 | expect(app.find('span').text()).equal('2'); 139 | expect(store.state.a).equal(2); 140 | expect(a).equal(1); 141 | store.dispatch('b'); 142 | setTimeout(() => { 143 | expect(a).equal(2); 144 | done(); 145 | }, 0); 146 | expect(app.find('em').text()).equal('2'); 147 | }); 148 | 149 | it('should not render for multiple', () => { 150 | const store = new Store({ 151 | state: { 152 | a: 1, 153 | b: 2, 154 | c: 3, 155 | }, 156 | }); 157 | let a = 0, 158 | b = 0, 159 | c = 0, 160 | d = 0; 161 | const A = (props) => { 162 | const v = useStore((state) => state.a); 163 | a++; 164 | return
    {v}
    ; 165 | }; 166 | const B = (props) => { 167 | const v = useStore((state) => state.b); 168 | b++; 169 | return {v}; 170 | }; 171 | const C = (props) => { 172 | const v = useStore((state) => state.c); 173 | c++; 174 | return {v}; 175 | }; 176 | 177 | const D = (props) => { 178 | const s = useStore((state) => state); 179 | d++; 180 | return {s.b}; 181 | }; 182 | const app = mount( 183 | 184 | 185 | 186 | 187 | 188 | 189 | ); 190 | store.state.c = 4; 191 | expect(a).eql(1); 192 | expect(b).eql(1); 193 | expect(c).eql(2); 194 | expect(d).eql(2); 195 | expect(app.find('em').text()).eql('4'); 196 | store.state.b = 3; 197 | expect(b).eql(2); 198 | expect(d).eql(3); 199 | expect(app.find('span').text()).eql('3'); 200 | }); 201 | 202 | it('should support $raw', () => { 203 | const store = new Store({ 204 | }); 205 | const d = { 206 | b: 1 207 | }; 208 | store.create('test', { 209 | state: { 210 | a: d 211 | } 212 | }); 213 | expect(store.state.test.a.$raw.b).eq(1); 214 | expect(d.$raw.b).eq(1); 215 | expect(store.state.test.a.toJSON().b).eq(1); 216 | expect(d.toJSON().b).eq(1); 217 | }) 218 | }); 219 | -------------------------------------------------------------------------------- /test/render.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import chai from 'chai'; 3 | import React, { useEffect } from 'react'; 4 | import Enzyme, { mount } from 'enzyme'; 5 | import Adapter from 'enzyme-adapter-react-16'; 6 | import { Store, inject, connect, Provider, useStore, useDispatch } from '../src/index'; 7 | import { JSDOM } from 'jsdom'; 8 | import sinon from 'sinon'; 9 | 10 | const doc = new JSDOM(''); 11 | global.document = doc.window.document; 12 | global.window = doc.window; 13 | 14 | Enzyme.configure({ adapter: new Adapter() }); 15 | 16 | chai.expect(); 17 | 18 | const expect = chai.expect; 19 | 20 | describe('render count', () => { 21 | it('should support hooks for function component', (done) => { 22 | const store = new Store(); 23 | store.create('m1', { 24 | state: { 25 | v: 1, 26 | f: 2, 27 | }, 28 | }); 29 | let a = 0; 30 | let b = 0; 31 | const A = (props) => { 32 | const value = useStore(state => state.m1.v); 33 | const dispatch = useDispatch(); 34 | useEffect(() => { 35 | a++; 36 | }); 37 | return {value}; 38 | }; 39 | const B = (props) => { 40 | const value = useStore(state => state.m1.f); 41 | const dispatch = useDispatch(); 42 | useEffect(() => { 43 | b++; 44 | }); 45 | return {value}; 46 | }; 47 | const app = mount( 48 | 49 | 50 | 51 | 52 | ); 53 | store.state.m1.v = 2; 54 | setTimeout(() => { 55 | expect(b).equal(1); 56 | expect(a).equal(2); 57 | done(); 58 | }, 10); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/roy.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, before */ 2 | /* eslint-disable */ 3 | 4 | import chai from 'chai'; 5 | import React from 'react'; 6 | import Enzyme, { mount } from 'enzyme'; 7 | import Adapter from 'enzyme-adapter-react-16'; 8 | import { Store, inject, connect, Provider, compose } from '../src/index'; 9 | import { JSDOM } from 'jsdom'; 10 | import sinon from 'sinon'; 11 | 12 | const doc = new JSDOM(''); 13 | global.document = doc.window.document; 14 | global.window = doc.window; 15 | 16 | Enzyme.configure({ adapter: new Adapter() }); 17 | 18 | chai.expect(); 19 | 20 | const expect = chai.expect; 21 | 22 | describe('support inject store to React Component', () => { 23 | it('inject store using object', () => { 24 | const store = new Store({ 25 | state: { 26 | name: 'a', 27 | dataSource: [], 28 | obj: { 29 | b: 1 30 | } 31 | }, 32 | actions: { 33 | add(state, payload) { 34 | state.set('name', payload); 35 | }, 36 | push(state, payload) { 37 | state.dataSource.push({ 38 | title: 'title' 39 | }); 40 | }, 41 | change(state) { 42 | state.set('obj.b', 2); 43 | } 44 | } 45 | }); 46 | @inject(store) 47 | class App extends React.Component { 48 | render() { 49 | const { name, dataSource } = this.props.state; 50 | const b = this.props.state.get('obj.b'); 51 | return ( 52 |
    53 | {name} 54 |
    {dataSource.length}
    55 | {b} 56 |
    57 | ); 58 | } 59 | } 60 | const wrapper = mount(); 61 | expect(wrapper.find('span').text()).eq('a'); 62 | store.dispatch('add', 'b'); 63 | expect(wrapper.find('span').text()).eq('b'); 64 | store.dispatch('push'); 65 | expect(wrapper.find('.a').text()).eq('1'); 66 | expect(wrapper.find('em').text()).eq('1'); 67 | store.dispatch('change'); 68 | expect(wrapper.find('em').text()).eq('2'); 69 | }); 70 | 71 | it('support auto mount store to provider global store', done => { 72 | const store = new Store({ 73 | name: 'module', 74 | state: { 75 | name: 'a' 76 | }, 77 | actions: { 78 | change(state) { 79 | state.set('name', 'b'); 80 | } 81 | } 82 | }); 83 | @inject(store) 84 | class Module extends React.Component { 85 | render() { 86 | return {this.props.state.name}; 87 | } 88 | } 89 | @connect() 90 | class Button extends React.Component { 91 | render() { 92 | return ; 93 | } 94 | } 95 | const globalStore = new Store(); 96 | const wrapper = mount( 97 | 98 |
    99 | 100 |
    102 |
    103 | ); 104 | expect(wrapper.find('span').text()).eq('a'); 105 | wrapper.find('button').simulate('click'); 106 | setTimeout(() => { 107 | expect(wrapper.find('span').text()).eq('b'); 108 | done(); 109 | }, 10); 110 | }); 111 | 112 | it('should support compose', () => { 113 | const object = { 114 | view: function({ createElement }) { 115 | return createElement( 116 | 'div', 117 | { 118 | onClick: () => { 119 | this.props.dispatch('change'); 120 | } 121 | }, 122 | this.props.state.name 123 | ); 124 | }, 125 | state: { 126 | name: 123 127 | }, 128 | actions: { 129 | change(state, payload) { 130 | state.set('name', 456); 131 | } 132 | } 133 | }; 134 | const Component = compose(object); 135 | const wrapper = mount(); 136 | expect(wrapper.find('div').text()).eq('123'); 137 | wrapper.find('div').simulate('click'); 138 | expect(wrapper.find('div').text()).eq('456'); 139 | }); 140 | }); 141 | 142 | describe('it should support observable store', () => { 143 | it('should support store set operation', () => { 144 | const store = new Store({ 145 | state: {} 146 | }); 147 | store.set('a', 1); 148 | expect(store.state.a).eq(1); 149 | store.set('c.d', 1); 150 | expect(store.state.c.d).eq(1); 151 | store.set('d', []); 152 | expect(store.state.d.length).eq(0); 153 | const cb = sinon.spy(); 154 | store.state.on('change', cb); 155 | store.state.d.push({ 156 | a: 1 157 | }); 158 | expect(cb.called).eq(true); 159 | store.state.set('d[0].a', 2); 160 | expect(cb.called).eq(true); 161 | expect(store.state.d[0].a).eq(2); 162 | expect(store.state.get('d[0].a')).eq(2); 163 | const item = store.state.d[0]; 164 | item.set('a', 3); 165 | expect(cb.callCount).eq(3); 166 | expect(store.state.get('d[0].a')).eq(3); 167 | store.state.reset(); 168 | expect(store.state.a).eq(undefined); 169 | expect(store.state.c).eq(undefined); 170 | store.state.set('d', [ 171 | { 172 | children: [ 173 | { 174 | b: false 175 | } 176 | ] 177 | } 178 | ]); 179 | store.state.set('d[0].children[0].b', true); 180 | expect(store.state.d[0].children[0].b, true); 181 | }); 182 | }); 183 | 184 | describe('it should support array operation', () => { 185 | let store; 186 | beforeEach(() => { 187 | store = new Store({ 188 | state: {} 189 | }); 190 | }); 191 | 192 | afterEach(() => { 193 | store = null; 194 | }); 195 | 196 | it('should support array operation', () => { 197 | const callback = sinon.spy(); 198 | store.on('change', callback); 199 | store.state.set('a', []); 200 | expect(callback.called).eql(true); 201 | store.state.a.push(1); 202 | expect(callback.callCount).eql(2); 203 | store.state.a.splice(0, 1); 204 | expect(callback.callCount).eql(3); 205 | expect(store.state.a.length).eql(0); 206 | store.state.a.push(1, 3, 2); 207 | expect(callback.callCount).eql(4); 208 | store.state.a.sort(); 209 | expect(callback.callCount).eql(5); 210 | expect(store.state.a.toString()).eql('1,2,3'); 211 | store.state.a.reverse(); 212 | expect(callback.callCount).eql(6); 213 | expect(store.state.a.toString()).eql('3,2,1'); 214 | store.state.a.pop(); 215 | expect(callback.callCount).eql(7); 216 | expect(store.state.a.toString()).eql('3,2'); 217 | store.state.a.shift(); 218 | expect(callback.callCount).eql(8); 219 | expect(store.state.a.toString()).eql('2'); 220 | store.state.a.unshift(4); 221 | expect(callback.callCount).eql(9); 222 | expect(store.state.a.toString()).eql('4,2'); 223 | }); 224 | }); 225 | 226 | describe('it should support plugin', () => { 227 | it('should support inject plugin for store', () => { 228 | const cb = sinon.spy(); 229 | const plugin = (store, actions) => { 230 | store.subscribe(cb); 231 | actions.setValue = (state, payload) => { 232 | state.set(payload); 233 | }; 234 | }; 235 | const store = new Store( 236 | { 237 | actions: { 238 | change(state) { 239 | state.set('a', 1); 240 | } 241 | } 242 | }, 243 | { 244 | plugins: [plugin] 245 | } 246 | ); 247 | store.dispatch('change'); 248 | expect(cb.called).eq(true); 249 | store.dispatch('setValue', { 250 | b: 1 251 | }); 252 | expect(store.state.b).eq(1); 253 | }); 254 | }); 255 | 256 | describe('bugfix', () => { 257 | it('should fix array toJSON', () => { 258 | const store = new Store({ 259 | state: {} 260 | }); 261 | store.set('data', [1, 2, 3]); 262 | expect(store.state.data.toJSON().toString()).eq('1,2,3'); 263 | }); 264 | }); 265 | 266 | describe('it should support batch update when multiple set store', () => { 267 | it('render method should be called once when multiple sets are wrapped by the transaction method', done => { 268 | const cb = sinon.spy(); 269 | const store = new Store({ 270 | state: { 271 | count1: 0, 272 | count2: 0, 273 | count3: 0 274 | }, 275 | actions: { 276 | add(state, payload) { 277 | window.setTimeout(() => { 278 | this.transaction(() => { 279 | this.dispatch('setValues', { 280 | count1: state.count1 + 1, 281 | count2: state.count2 + 1, 282 | count3: state.count3 + 1 283 | }); 284 | }); 285 | }, 10); 286 | } 287 | } 288 | }); 289 | 290 | @inject(store) 291 | class App extends React.Component { 292 | render() { 293 | cb(); 294 | const { count1, count2, count3 } = this.props.state; 295 | const { dispatch } = this.props; 296 | return ( 297 |
    298 | 299 | {count1} 300 | {count2} 301 | {count3} 302 | 303 | 304 |
    305 | ); 306 | } 307 | } 308 | const wrapper = mount(); 309 | wrapper.find('button').simulate('click'); 310 | window.setTimeout(() => { 311 | expect(cb.callCount).eq(2); 312 | expect(wrapper.find('span').text()).eq('111'); 313 | done(); 314 | }, 10); 315 | }); 316 | 317 | it('render method should be called once when multiple sets are wrapped by the nest transaction method', done => { 318 | const cb = sinon.spy(); 319 | const store = new Store({ 320 | state: { 321 | count1: 0, 322 | count2: 0, 323 | count3: 0 324 | }, 325 | actions: { 326 | add(state, payload) { 327 | window.setTimeout(() => { 328 | this.transaction(() => { 329 | this.transaction(() => { 330 | this.dispatch('setValues', { 331 | count1: state.count1 + 1, 332 | count2: state.count2 + 1, 333 | count3: state.count3 + 1 334 | }); 335 | }); 336 | this.dispatch('setValues', { 337 | count3: state.count3 + 1 338 | }); 339 | }); 340 | }, 10); 341 | } 342 | } 343 | }); 344 | 345 | @inject(store) 346 | class App extends React.Component { 347 | render() { 348 | cb(); 349 | const { count1, count2, count3 } = this.props.state; 350 | const { dispatch } = this.props; 351 | return ( 352 |
    353 | 354 | {count1} 355 | {count2} 356 | {count3} 357 | 358 | 359 |
    360 | ); 361 | } 362 | } 363 | const wrapper = mount(); 364 | wrapper.find('button').simulate('click'); 365 | window.setTimeout(() => { 366 | expect(cb.callCount).eq(2); 367 | expect(wrapper.find('span').text()).eq('112'); 368 | done(); 369 | }, 10); 370 | }); 371 | 372 | it('Component injected with global store render method should be called once when set local store ', done => { 373 | const cb = sinon.spy(); 374 | const globalStore = new Store(); 375 | const store = new Store({ 376 | name: 'app', 377 | state: { 378 | count1: 0, 379 | count2: 0, 380 | count3: 0 381 | }, 382 | actions: { 383 | add(state, payload) { 384 | window.setTimeout(() => { 385 | this.transaction(() => { 386 | this.dispatch('setValues', { 387 | count1: state.count1 + 1, 388 | count2: state.count2 + 1, 389 | count3: state.count3 + 1 390 | }); 391 | }); 392 | }, 10); 393 | } 394 | } 395 | }); 396 | 397 | @inject(store) 398 | class Component1 extends React.Component { 399 | render() { 400 | const { count1, count2, count3 } = this.props.state; 401 | const { dispatch } = this.props; 402 | return ( 403 |
    404 | {count1} 405 | {count2} 406 | {count3} 407 | 408 |
    409 | ); 410 | } 411 | } 412 | 413 | @inject(globalStore) 414 | class Component2 extends React.Component { 415 | render() { 416 | cb(); 417 | if (this.props.state.app) { 418 | const { count1, count2, count3 } = this.props.state.app; 419 | return ( 420 | 421 | {count1} 422 | {count2} 423 | {count3} 424 | 425 | ); 426 | } 427 | return
    ; 428 | } 429 | } 430 | 431 | const wrapper = mount( 432 | 433 |
    434 | 435 | 436 |
    437 |
    438 | ); 439 | wrapper.find('button').simulate('click'); 440 | window.setTimeout(() => { 441 | expect(cb.callCount).eq(2); 442 | expect( 443 | wrapper 444 | .find('Component2') 445 | .find('span') 446 | .text() 447 | ).eq('111'); 448 | done(); 449 | }, 10); 450 | }); 451 | 452 | 453 | it('should support collect deps for didMount', () => { 454 | const store = new Store({ 455 | state: { 456 | a: 1 457 | } 458 | }); 459 | const cb = sinon.spy(); 460 | @inject(store) 461 | class App extends React.Component { 462 | componentDidMount() { 463 | const { a } = this.props.state; 464 | } 465 | componentWillReceiveProps = cb; 466 | render() { 467 | return
    ; 468 | } 469 | } 470 | mount(); 471 | expect(cb.called).eq(false); 472 | store.dispatch('setValues', { 473 | a: 2 474 | }); 475 | expect(cb.called).eq(true); 476 | }); 477 | 478 | it('should support multiple args type since 1.2.0', () => { 479 | const injectStore = new Store({ 480 | state: { 481 | a: 1 482 | }, 483 | actions: { 484 | add(state) { 485 | state.a++; 486 | } 487 | } 488 | }); 489 | @connect( 490 | { 491 | state: ['a'], 492 | actions: [ 493 | { 494 | onAdd: 'add' 495 | } 496 | ] 497 | }, 498 | ) 499 | class Child extends React.Component { 500 | render() { 501 | return this.props.onAdd()}>{this.props.a}; 502 | } 503 | } 504 | class App extends React.Component { 505 | render() { 506 | return ; 507 | } 508 | } 509 | const wrapper = mount(); 510 | expect(wrapper.find('span').text()).eq('1'); 511 | wrapper.find('span').simulate('click'); 512 | expect(wrapper.find('span').text()).eq('2'); 513 | }); 514 | }); 515 | -------------------------------------------------------------------------------- /test/tojson.js: -------------------------------------------------------------------------------- 1 | import { Store } from '../src'; 2 | const store = new Store( 3 | {}, 4 | { 5 | plugins: [] 6 | } 7 | ); 8 | const largeData = []; 9 | for (let i = 0; i < 50; i++) { 10 | const object = {}; 11 | for (let j = 0; j < 20; j++) { 12 | object[j] = 'test'; 13 | } 14 | largeData.push(object); 15 | } 16 | 17 | const nested = function (data, level) { 18 | if (level > 9) { 19 | return; 20 | } 21 | data.a = { 22 | largeData 23 | }; 24 | nested(data.a, ++level); 25 | return data; 26 | }; 27 | 28 | const data = nested({}, 0); 29 | 30 | window.toJSON = function () { 31 | const t = Date.now(); 32 | console.log(store.state.toJSON()); 33 | console.log(Date.now() - t); 34 | }; 35 | 36 | store.create('module1', { 37 | state: { 38 | name: data 39 | }, 40 | actions: { 41 | change(state, payload) { 42 | state.set('name', payload); 43 | } 44 | } 45 | }); 46 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* global __dirname, require, module*/ 2 | 3 | const webpack = require('webpack'); 4 | const UglifyJsPlugin = webpack.optimize.UglifyJsPlugin; 5 | const path = require('path'); 6 | const env = require('yargs').argv.env; // use --env with webpack 2 7 | 8 | let libraryName = 'roy'; 9 | 10 | let plugins = [], 11 | outputFile; 12 | 13 | if (env === 'build') { 14 | plugins.push( 15 | new UglifyJsPlugin({ 16 | minimize: true 17 | }) 18 | ); 19 | outputFile = libraryName + '.min.js'; 20 | } else { 21 | outputFile = libraryName + '.js'; 22 | } 23 | 24 | const config = { 25 | entry: __dirname + '/src/index.js', 26 | devtool: 'source-map', 27 | output: { 28 | path: __dirname + '/dist', 29 | filename: outputFile, 30 | library: libraryName, 31 | libraryTarget: 'umd', 32 | umdNamedDefine: true 33 | }, 34 | module: { 35 | rules: [ 36 | { 37 | test: /(\.jsx|\.js)$/, 38 | loader: 'babel-loader', 39 | exclude: /(node_modules|bower_components)/ 40 | }, 41 | { 42 | test: /(\.jsx|\.js)$/, 43 | loader: 'eslint-loader', 44 | exclude: /node_modules/ 45 | } 46 | ] 47 | }, 48 | resolve: { 49 | modules: [path.resolve('./node_modules'), path.resolve('./src')], 50 | extensions: ['.json', '.js', '.jsx'] 51 | }, 52 | plugins: plugins, 53 | externals: { 54 | react: { 55 | root: 'React', 56 | commonjs: 'react', 57 | commonjs2: 'react', 58 | amd: 'react' 59 | }, 60 | 'react-dom': { 61 | root: 'ReactDOM', 62 | commonjs: 'react-dom', 63 | commonjs2: 'react-dom', 64 | amd: 'react-dom' 65 | }, 66 | 67 | 'prop-types': { 68 | root: 'PropTypes', 69 | commonjs: 'prop-types', 70 | commonjs2: 'prop-types', 71 | amd: 'prop-types' 72 | } 73 | } 74 | }; 75 | 76 | module.exports = config; 77 | --------------------------------------------------------------------------------