├── .gitignore
├── README.md
├── config
├── env.js
├── fsExistsSync.js
├── paths.js
├── polyfills.js
├── webpack.config.dev.js
└── webpack.config.prod.js
├── package.json
├── public
├── favicon.ico
└── index.html
├── scripts
├── build.js
└── start.js
└── src
├── appconfig.js
├── components
├── AppBottomNav
│ ├── index.css
│ └── index.js
├── CardListCreator.js
├── Loading
│ ├── index.css
│ └── index.js
├── MessageCard
│ ├── MessageCard.css
│ └── index.js
├── MessageList.js
├── PrivateRoute.js
├── PullView.js
├── PullViewWrap.js
├── RecentCard
│ ├── index.css
│ └── index.js
├── RecentList.js
├── ReplyBox
│ ├── index.css
│ └── index.js
├── ReplyCard
│ ├── ReplyCard.css
│ └── index.js
├── ReplyList.js
├── TopicCard
│ ├── index.css
│ └── index.js
└── TopicList.js
├── containers
├── App
│ ├── index.css
│ └── index.js
├── CollectionPage
│ ├── index.css
│ └── index.js
├── LoginPage
│ ├── index.css
│ └── index.js
├── MessagePage
│ ├── index.css
│ └── index.js
├── NewTopicPage
│ ├── index.css
│ └── index.js
├── SettingPage
│ ├── index.css
│ └── index.js
├── TopicPage
│ ├── TopicContent.js
│ ├── index.css
│ └── index.js
├── TopicsPage
│ ├── TopicsHeader.css
│ ├── TopicsHeader.js
│ ├── TopicsPageCreator.js
│ ├── index.css
│ └── index.js
└── UserPage
│ ├── index.css
│ └── index.js
├── core
├── api
│ ├── api-service.js
│ └── index.js
├── app
│ ├── actions.js
│ ├── epics.js
│ ├── index.js
│ ├── reducers.js
│ └── selectors.js
├── auth
│ ├── actions.js
│ ├── epics.js
│ ├── index.js
│ ├── reducers.js
│ └── selectors.js
├── collection
│ ├── actions.js
│ ├── epics.js
│ ├── index.js
│ ├── reducers.js
│ └── selectors.js
├── constants.js
├── db
│ ├── actions.js
│ ├── index.js
│ ├── reducers.js
│ └── selectors.js
├── epics.js
├── localstore
│ ├── actions.js
│ ├── epics.js
│ └── index.js
├── message
│ ├── actions.js
│ ├── epics.js
│ ├── index.js
│ ├── reducers.js
│ ├── schemas.js
│ └── selectors.js
├── reducers.js
├── reply
│ ├── actions.js
│ ├── epics.js
│ ├── index.js
│ ├── reducers.js
│ ├── schemas.js
│ └── selectors.js
├── store
│ ├── configureStore.dev.js
│ ├── configureStore.prod.js
│ └── index.js
├── topic
│ ├── actions.js
│ ├── epics.js
│ ├── index.js
│ ├── reducers.js
│ ├── schemas.js
│ └── selector.js
├── user
│ ├── actions.js
│ ├── epics.js
│ ├── index.js
│ ├── reducers.js
│ ├── schemas.js
│ └── selectors.js
└── utils
│ └── index.js
└── index.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 |
6 | # testing
7 | coverage
8 |
9 | # production
10 | build
11 |
12 | # misc
13 | .DS_Store
14 | .env
15 | npm-debug.log
16 |
17 | # IDE
18 | .idea
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | > 本项目是由[CNode社区](https://cnodejs.org)提供的API,使用React全家桶所开发的数据持久化的单页应用。
3 |
4 | 这里所说数据持久化的单页应用,指的是利用`immutable + normalizr + reselect`这一套组合,在应用中简单实现了类似数据库的数据存储,使数据保持持久且同步,在渲染时优先使用存储的数据,以减少请求,提升应用响应。
5 |
6 | #### 为什么这么做?
7 | 可以参考文章[State 范式化](http://cn.redux.js.org/docs/recipes/reducers/NormalizingStateShape.html)的描述。
8 |
9 | 用本项目举例,在首次拉取主题列表页数据,经过范式化后得到数据如下:
10 | 
11 | 这里我把范式化后的数据都存放于`db`之下,之内包含`topics`、`users`、`replies`等“*数据表*”,`topics`表内的主题以其`id`作为索引,`users`表内的用户以其`loginname`作为索引。主题内的`author`通过索引可以关联到`users`表内对应用户。
12 |
13 |
14 | 这里可以看到,主题内已经包含了主题内容但不包含评论,所以可以直接打开主题详情页而不用去拉取数据,可以留一个加载评论的按钮,在用户需要时加载评论,由于接口限制,不能单独加载主题评论,所以这里实际拉取的是主题完整数据。在用户主动拉取这一篇主题之后的`db`如下:
15 | 
16 | 这里的`replies`表,用来存放评论,评论以其`id`作为索引。而主题内也多了`replies`属性,展开后是主题的`id`列表,可以通过索引关联到`replies`表内对应评论。评论内的`author`通过索引可以关联到`users`表内对应用户。
17 |
18 | 那好处到底是是什么呢?引用文章[State 范式化](http://cn.redux.js.org/docs/recipes/reducers/NormalizingStateShape.html)的话来说:
19 | * 每个数据项只在一个地方定义,如果数据项需要更新的话不用在多处改变
20 | * reducer 逻辑不用处理深层次的嵌套,因此看上去可能会更加简单
21 | * 检索或者更新给定数据项的逻辑变得简单与一致。给定一个数据项的 type 和 ID,不必挖掘其他对象而是通过几个简单的步骤就能查找到它。
22 |
23 | #### 接下来该这么做?
24 | 上面只讲到了范式化,接下来我们需要从`db`中提取出渲染所需数据,这时候就该`reselect`出场了,引用[官方文档的描述](https://github.com/reactjs/reselect):
25 | * Selectors can compute derived data, allowing Redux to store the minimal possible state.
26 |
27 | selectors可以计算衍生数据,使得Redux最小化存储的state。
28 |
29 | * Selectors are efficient. A selector is not recomputed unless one of its arguments change.
30 |
31 | selectors是高效的。一个selector只在arguments改变时进行重计算。
32 |
33 | * Selectors are composable. They can be used as input to other selectors.
34 |
35 | selectors是可组合的,它可作为其他selectors的输入。
36 |
37 | 另外可以参考文章[计算衍生数据](http://cn.redux.js.org/docs/recipes/ComputingDerivedData.html)。
38 |
39 | 由于项目全面使用了`immutable.js`,对于数据的存取还是比较方便。
40 |
41 | ## 涉及技术
42 | * react
43 | * react router 4
44 | * redux
45 | * redux-observable
46 | * rxjs
47 | * immutable.js
48 | * normalizr
49 | * reselect
50 |
51 | ## 开发记录
52 | [用create-react-app定制自己的react项目模板](https://github.com/JoV5/blog/blob/master/前端/React/用create-react-app定制自己的react项目模板.md)
53 |
54 | [用react写一个下拉刷新上滑加载组件](https://github.com/JoV5/blog/blob/master/%E5%89%8D%E7%AB%AF/React/%E7%94%A8react%E5%86%99%E4%B8%80%E4%B8%AA%E4%B8%8B%E6%8B%89%E5%88%B7%E6%96%B0%E4%B8%8A%E6%BB%91%E5%8A%A0%E8%BD%BD%E7%BB%84%E4%BB%B6.md)
55 |
56 | ## TODOS
57 | - [ ] 回到顶部
58 | - [ ] 回复尾巴
59 | - [ ] 已发布主题更新
60 | - [ ] 友好提示
61 | - [ ] 消息页面支持直接回复点赞
62 | - [ ] 收藏页面支持直接取消收藏
63 | - [ ] 优化css
64 | - [ ] 优化reducers
65 | - [ ] 各种动画
66 | - [ ] chunk的重命名
67 |
68 |
69 |
--------------------------------------------------------------------------------
/config/env.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
4 | // injected into the application via DefinePlugin in Webpack configuration.
5 |
6 | var REACT_APP = /^REACT_APP_/i;
7 |
8 | function getClientEnvironment(publicUrl) {
9 | var raw = Object
10 | .keys(process.env)
11 | .filter(key => REACT_APP.test(key))
12 | .reduce((env, key) => {
13 | env[key] = process.env[key];
14 | return env;
15 | }, {
16 | // Useful for determining whether we’re running in production mode.
17 | // Most importantly, it switches React into the correct mode.
18 | 'NODE_ENV': process.env.NODE_ENV || 'development',
19 | // Useful for resolving the correct path to static assets in `public`.
20 | // For example,
.
21 | // This should only be used as an escape hatch. Normally you would put
22 | // images into the `src` and `import` them in code to get their paths.
23 | 'PUBLIC_URL': publicUrl
24 | });
25 | // Stringify all values so we can feed into Webpack DefinePlugin
26 | var stringified = {
27 | 'process.env': Object
28 | .keys(raw)
29 | .reduce((env, key) => {
30 | env[key] = JSON.stringify(raw[key]);
31 | return env;
32 | }, {})
33 | };
34 |
35 | return { raw, stringified };
36 | }
37 |
38 | module.exports = getClientEnvironment;
39 |
--------------------------------------------------------------------------------
/config/fsExistsSync.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 |
3 | module.exports = function fsExistsSync(path) {
4 | try{
5 | fs.accessSync(path, fs.F_OK);
6 | }catch(e){
7 | return false;
8 | }
9 | return true;
10 | };
--------------------------------------------------------------------------------
/config/paths.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var path = require('path');
4 | var fs = require('fs');
5 | var url = require('url');
6 |
7 | // Make sure any symlinks in the project folder are resolved:
8 | // https://github.com/facebookincubator/create-react-app/issues/637
9 | var appDirectory = fs.realpathSync(process.cwd());
10 | function resolveApp(relativePath) {
11 | return path.resolve(appDirectory, relativePath);
12 | }
13 |
14 | // We support resolving modules according to `NODE_PATH`.
15 | // This lets you use absolute paths in imports inside large monorepos:
16 | // https://github.com/facebookincubator/create-react-app/issues/253.
17 |
18 | // It works similar to `NODE_PATH` in Node itself:
19 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders
20 |
21 | // We will export `nodePaths` as an array of absolute paths.
22 | // It will then be used by Webpack configs.
23 | // Jest doesn’t need this because it already handles `NODE_PATH` out of the box.
24 |
25 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored.
26 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims.
27 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421
28 |
29 | var nodePaths = (process.env.NODE_PATH || '')
30 | .split(process.platform === 'win32' ? ';' : ':')
31 | .filter(Boolean)
32 | .filter(folder => !path.isAbsolute(folder))
33 | .map(resolveApp);
34 |
35 | let appname = process.argv.find(arg => arg.indexOf('app=') > -1);
36 | appname = appname ? appname.split('=')[1] : '';
37 |
38 | var envPublicUrl = process.env.PUBLIC_URL;
39 |
40 | function ensureSlash(path, needsSlash) {
41 | var hasSlash = path.endsWith('/');
42 | if (hasSlash && !needsSlash) {
43 | return path.substr(path, path.length - 1);
44 | } else if (!hasSlash && needsSlash) {
45 | return path + '/';
46 | } else {
47 | return path;
48 | }
49 | }
50 |
51 | function getPublicUrl(appPackageJson) {
52 | return envPublicUrl || require(appPackageJson).homepage;
53 | }
54 |
55 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer
56 | // "public path" at which the app is served.
57 | // Webpack needs to know it to put the right
193 |
203 |