├── .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 
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 | 
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 | 
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 |
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 |
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 |
79 |
80 |
81 |
{this.renderCategory(category)}
82 |
83 |
{this.renderFilter(sortMethods)}
84 |
{this.renderList(this.props.goods)}
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 |
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 |
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 |
79 |
80 |
81 |
{this.renderCategory(category)}
82 |
83 |
{this.renderFilter(sortMethods)}
84 |
{this.renderList(this.props.state.list.goods)}
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 |
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 |
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 |
78 |
79 |
80 |
{this.renderCategory(category)}
81 |
82 |
{this.renderFilter(sortMethods)}
83 |
{this.renderList(this.props.goods)}
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 |
63 |
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 |
95 |
96 |
102 |
103 |
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 |
101 |
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 |
--------------------------------------------------------------------------------