Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

从 0 开始实现 react 版本的 hackernews (基于 dva) #9

Open
sorrycc opened this issue Aug 19, 2016 · 8 comments
Open

从 0 开始实现 react 版本的 hackernews (基于 dva) #9

sorrycc opened this issue Aug 19, 2016 · 8 comments
Labels

Comments

@sorrycc
Copy link
Owner

sorrycc commented Aug 19, 2016


Live Demo

说一说基于 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

@blankzust
Copy link

blankzust commented Sep 9, 2016

我觉得这个做法非常适合websocket,但是如果没有websocket,是不是没有必要事先存好所有id?因为最新数据都是需要直接得到的而不是通过websocket返回最新的数据的id然后通过id找到相应的值。现在非常纠结dva项目架构该怎么写比较好

@sorrycc
Copy link
Owner Author

sorrycc commented Sep 9, 2016

你指的是 state 怎么设计?

@phpsmarter
Copy link

model设计费脑子,体现在什么地方? 初学者不太懂,但是感觉到state要弄好是不太容易的,这里有什么教程,或者经验吗?

@jiaozhouzhou
Copy link

假如我要删除一条数据的话 怎么删

@TheWaWaR
Copy link

@jiaozhouzhou 删掉之后刷新列表?

@truhangle
Copy link

云歉,model里面的selectors是干嘛用的?是因为跳转页面以后,保持状态有关系吗?

@caozihao
Copy link

我感觉这个说明比user-dashboard那个项目清晰的多,另外似乎还要学redux-saga,真是头大啊

@sorrycc
Copy link
Owner Author

sorrycc commented Dec 29, 2016

@helloskull 不用学习完整的 redux-saga,掌握这里的知识点就可以用 dva 了,https://github.com/dvajs/dva-knowledgemap

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

7 participants