从 0 开始实现 react 版本的 hackernews (基于 dva)
sorrycc opened this issue · 8 comments
说一说基于 dva 实现 dva-hackernews 的过程。
基本思路是按照 service -> model -> component 的顺序来实现的,好处是可以用真实数据,不用额外写 mock 方法。
脚手架
通过 dva-cli 生成项目初始文件,然后 npm start
启动。
Service
hackernews 数据接口来自 firebase,所以可以直接用 firebase 这个 package 。firebase 基于 websocket 连接实现,除了初次请求慢些,后面的数据加载很快。相比 http 来说,省去不少请求。
为了方便在 effects 里调用,service 方法需要返回 promise 。watchList
除外,这个不在 effects 里调,而是在 subscriptions 里,用于实时更新列表数据。
Model
写 model 层是脑力劳动,而写 component 层是体力劳动。
数据结构
先设计数据结构,为了让 reducer 里写得比较容易,所以选择扁平化的方式。即把 item 拎出来,以 id 为 key 统一存放,然后其他地方即可引用 id 。
{
list: {
top: [123, 456],
new: [123, 456],
},
itemsById: {
123: { title: 'foo' },
456: { title: 'bar' },
789: { title: 'wow' },
},
}
这样更新 item 就比较简单,反之如果要更新 list.top['123'] 的数据,想想都麻烦。(没用 immutable.js)
state 更新
然后是完成处理 action 的部分,reducers 和 effects,分别负责 state 更新和异步逻辑。
state 更新的部分写在 reducers 里,没什么特别的,灵活掌握 array 和 object 的各种方法就可以了,注意 array 到 object 的转换可以用 reduce 简化。
saveItems(state, { payload: itemsArr }) {
const items = itemsArr.reduce((memo, item) => {
memo[item.id] = item;
return memo;
}, {});
return { ...state, itemsById: { ...state.itemsById, ...items }};
},
异步逻辑
异步逻辑部分,写在 effects 里。通过 generator 组织,所以基本上都是一层缩进下来就完了。
*fetchList({ payload }) {
const { type, page } = payload;
yield put({ type: 'app/showLoading' });
const ids = yield call(fetchIdsByType, type);
const itemsPerPage = yield select(state => state.item.itemsPerPage);
const items = yield call(
fetchItems,
ids.slice(itemsPerPage * (page - 1), itemsPerPage * page)
);
yield put({ type: 'saveList', payload: { ids, type } });
yield put({ type: 'saveItems', payload: items });
yield put({ type: 'app/hideLoading' });
},
为了实时性,切换页面不管 item 是否有缓存,都会重新请求一遍。
评论数据是递归获取的,因为不知道有几层。还好是 websocket,如果换成 http 的实现应该会很慢。虽然是比较快,但在评论页面也能明显感觉到是一层层更新出来的。
定义完所有 action 的处理,接下来要看如何调用他们。基本上就两个地方,subscriptions 和 component 。
初始数据请求
subscription 意为订阅,用于数据源的订阅。
而初始数据加载实际上是订阅了 history 的变更,待满足 url 匹配时,触发 action 加载远程数据。这些逻辑不放 route component 还有好处是可以更好地配合 hmr,同时让 route component 保持 stateless component 的写法。
由于 react-router 的限制,这里需使用 path-to-regexp 库来解决 url 匹配的问题。
history.listen(({ pathname }, { params }) => {
if (pathToRegexp(`/item/:itemId`).test(pathname)) {
dispatch({
type: 'item/fetchComments',
payload: params.itemId,
});
}
});
当用户进入 item 页面时,通过 action item/fetchComments
获取评论数据。
实时更新
同上,实时更新也写在 subscriptions 里,等于是订阅了 list 的数据源。有更新时,保存新的 id,然后重新加载本页数据。
watchList(type, ids => {
dispatch({
type: 'saveList',
payload: {
type, ids
},
});
dispatch({
type: 'fetchList',
payload: {
type,
page,
},
});
});
selector
由于我们的数据是扁平化的,不能直接交由 component 渲染,需要一层 selector 。比如我想要 top 下第 1 页的列表。
export function listSelector(state, ownProps) {
const page = parseInt(ownProps.params.page || 1, 10);
const { itemsPerPage, activeType, lists, itemsById } = state.item;
const ids = lists[activeType].slice(itemsPerPage * (page - 1), itemsPerPage * page);
const items = ids.reduce((memo, id) => {
if (itemsById[id]) memo.push(itemsById[id]);
return memo;
}, []);
const maxPage = Math.ceil(lists[activeType].length / itemsPerPage);
return {
items,
page,
maxPage,
activeType,
};
}
Component
写完 model 层,感到一阵轻松,剩下的基本不费脑了。
动画
动画没有用上 react-motion,而是基于 ReactCSSTransitionGroup 实现,方法和 vue 以及 angular 都类似。动效可以上 nganimate 找一个喜欢的样式过来用。
<ReactCSSTransitionGroup
transitionName="item"
transitionEnterTimeout={500}
transitionLeaveTimeout={500}
>
{
items.map(item => <Item key={item.id} item={item} />)
}
</ReactCSSTransitionGroup>
总结
以上是实现 hackernews 一些经验。先写什么并不重要,主要是要有分层的概念,可以先写 model,也可以先写 component 。dva 借鉴 elm 的概念整合了 reducers, effects 和 subscriptions 到 model,让分层更清晰,并让各种觉得的代码有所归属。希望大家能动手实践一把,会发现相比现有 redux 方法的优势。
More
我觉得这个做法非常适合websocket,但是如果没有websocket,是不是没有必要事先存好所有id?因为最新数据都是需要直接得到的而不是通过websocket返回最新的数据的id然后通过id找到相应的值。现在非常纠结dva项目架构该怎么写比较好
你指的是 state 怎么设计?
model设计费脑子,体现在什么地方? 初学者不太懂,但是感觉到state要弄好是不太容易的,这里有什么教程,或者经验吗?
假如我要删除一条数据的话 怎么删
@jiaozhouzhou 删掉之后刷新列表?
云歉,model里面的selectors是干嘛用的?是因为跳转页面以后,保持状态有关系吗?
我感觉这个说明比user-dashboard那个项目清晰的多,另外似乎还要学redux-saga,真是头大啊
@helloskull 不用学习完整的 redux-saga,掌握这里的知识点就可以用 dva 了,https://github.com/dvajs/dva-knowledgemap