摘要
Redux 是 其他 flux 框架 推荐使用的 React 框架。当我开始写这篇文章时,它还是 1.0.0 版本,当这篇文章发布时,它已经是 3.0.0 了。
它的作者,Dan Abramov 已经发布一些很棒的 文档,但是他依然没有完全指明如何在大规模项目中使用 Redux,所以人们开始问了 “有哪些大型项目使用了 Redux”. 好吧,希望这篇文章可以解决这些疑惑。
我们将会讨论:
Redux 的所有技术栈
Redux 的各个模块都做了什么
如何划分 Redux 项目结构
如何处理 WebSocket 的异步数据
正文
我应该有哪些知识储备?
阅读 Redux 官方文档。
阅读了 Dan 的文章 Smart & Dumb Components。
开发 Redux 项目需要使用哪些工具?
Redux 不仅仅是 Redux, 它是一堆相关东西的集合,其中有些已经发布到 v1.0.0,有些还在酝酿中。
你的工具包可能包含下面的绝大多数:
Webpack
使用它打包你的文件,而不是使用 Browserify、Require 或者任何你挣扎使用的工具。为什么?因为一部分 Redux 初始化示例 展示了它的热加载能力,而且这些示例是使用 Webpack 构建的。
点击保存就可以直接看到样式的更新是如此方便,以至于我都不想再使用保存->刷新页面->跳转到指定页面这样繁琐重复的方式了。
Babel
一部分原因是它让你可以使用 ES6/7 的语法糖, 另一部份原因是热加载现在是作为一个 Babel plugin 实现的。
React
虽然在这篇文章发布时 0.13 版是稳定版本,但我们依然期待 0.14 版, 因为它能修复一些上下文相关的问题。
因为 ES6 提供了 classes 机制,所以 React 也 弃用 Mixins了。现在应该使用 高阶组件 来代替--把你的 React 组件包裹在一个提供 上下文 的父元素中。Redux 充分利用了这一点。
Redux
这个没什么好说的。
React-Redux
严格来说,它与 Redux 无关,它是为 React 编写的,提供了把 React 组件和 Redux Store 连接在一起的高阶组件。
Middleware
你有两个选择:thunks 或者别的 promise 库。无论哪个选择,它都能让你在 action creatores 中运行异步代码成为可能。
Request Library
这正是上述异步代码的原因。我使用 Axios,它基于 promise,因此可以和 promise 中间件很好的兼容。
React-Router(-redux)
表面上看,使用路由就是更新导航栏并显示对应的应用页面。然而更底层的原因是它提供了一逻辑机制去拆分你的代码。
路由带来的问题是,它给了你更多的 state,而这些 state 却不属于你的 store。Redux-Router 可以确保你的 state 被 Redux 管理。
如何使用 Redux 的不同部分?
我们都知道 Flux 是一个单向数据流框架,但即使这样,我们如何使用它?
在应用中你需要:
在一个不太规范的框架中,你可以随意放置内容,可能在一个活着两个不同的地方做了上述所有事情。
我按照以下的标准组织我的代码:
使用路由来确保你的组件拥有正确的数据
这是一个很好的方式,因为它划分了数据集合。使用 Route 中的 onEnter 方法 来指定需要渲染的东西。你不必让这个方法等到数据集合加载完毕,因为。。。
使用智能组件来确保你的木偶组件可以渲染
你的智能组件应该是配置在 Route 中的组件,你的智能组件的 render 方法控制子组件的渲染数据:
render () { if (this.hasData()) { return this.renderComponents(); } else { return this.renderLoadingScreen(); } }
智能组件尽可能的做数据预处理,以使你的木偶组件足够 “木偶”
比如说,当你传递一个处理句柄给木偶组件时,带上它需要的 id,这样
木偶组件就不需要自己获取 id 了:
renderComponents () { return <DumbComponent onSelect={this.itemSelected.bind(this,this.props.item.id)} >; }
使用木偶组件去渲染所有东西
不要放哪怕一个 <div> 到你的智能组件中,任何时候,智能组件都应该仅仅是木偶组件的组合。拆分你的关注点,不要在这里写一点东西。
使用智能组件调用 actions creators
当一个木偶组件和用户有交互时,它自己不应该处理任何逻辑--它应该仅仅调用从智能组件中传过来的处理函数,然后由这个函数去处理。
然后智能组件采集必要的数据传递给 action creator。
在 ActionCreators 中转换应用数据结构到 API 数据结构
你的 ActionCreators 负责在应用数据结构和 API 数据结构间转换。这个操作是双向的--发起请求,处理返回值。
因为 action 的输出会被 reducer 处理,而 reducer 并不知道自己是被怎样调用的,你可能发现有时候你不能仅仅返回 API 的调用结果--你需要补充它的附加字段,比如:如果你的 action 是 PROJECT_UPDATE,你需要返回新的项目名和 id,而 API 仅仅返回 {savedAt: "<some date>"},你就需要这样传递参数:
function updateProject(projectId,projectName) { request.put(`/project/${projectId}`,{projectName}).then( response => Object.assign( {projectId,projectName},response.data ) ); }
使用 reducers 同步你的 state
有趣的是,一个 reducer 可以处理任何的 action。一个数据清理的场景是,当用户注销时,清理 store 中的所有数据:
switch (action.type) { ... case USER_logoUT: return {} }
文件结构
如何组织文件结构是件复杂的事,因为它比处理一成不变的东西多了很多艺术性和个人风格。
我找到了 Redux 应用中的两个分离点,然后我围绕这两个分离点组织文件结构。
一个分离点是 数据。你的 actions 可以在任何地方被调用(虽然通常都是被智能组件调用)。你的 reducers 和 actions 是绑定的。actions 可以组合在一起,根据模块构建你的应用:可能一部分是处理用户登录和权限,另一部分是用户管理的项目。所有这些都有创建、查询、更新和删除,而这些都应该放在一起。
另一个分离点是 视图。根据视图你就可以布局你的应用--不同页面的路由,聚合数据和交互的智能组件,渲染数据的木偶组件。
多个视图可以调用同一个 action。比如,项目列表页面可以让你简单的编辑项目名,而项目详情页面可以提供一个编辑项目名的表单。而这两者都有不同分离的路由,不同的智能组件,不同的木偶组件和不同的数据集。
所以,我这样组织我的项目文件:
public/ index.html client/ index.js modules/ reducers.js users/ constants.js actions/ user_fetch.js user_login.js permissions_fetch.js reducers/ index.js user.js permission.js projects/ routes/ login/ index.js containers/ login.js components/ login.js logged_in/ project_list/ project_view/
modules 目录负责处理和数据相关的文件,不同模块的数据处理通过子目录的方式划分。这使得您未来可以把这些模块单独打包到你的 npm 仓库,它们之间没有依赖。
每个 action 和 reducer 都有自己单独的文件。有的项目 试图把一个模块中的所有内容都放倒一个文件中。我个人反对在中大型项目中采用这种做法,当项目越来越大时,应该把东西拆成尽可能小的块。
为了使不同的模块的 reducers 保持相似的结构,增加了 index.js 文件,它导出了该目录中的所有 reducer,然后顶层的 reducers.js 引入所有模块的 reducers。这些单独的 reducers 文件都会用于生成 Redux store。
routes 目录负责管理所有视图相关的文件,按不同的路由划分子目录。每个 route 目录包涵三个部分:
同样的,随着路径层级变深,会分解成更多的小组件。我推荐这种方式,因为它允许你仅仅在需要的时候实例化这些路由。而且意味着你的路由仅仅包含子其子目录中的文件,这样感觉很好并且解偶了。
通过使用 onEnter 和 onLeave 方法,你的路由文件同样可以作为数据的关卡。在这里你可以触发 fetch action 来获取组件需要的数据。这在你使用深层路由嵌套的时候很有用,比如,给定路由 /app/project/10/permission,你可以:
当切换到另外一个路由 /app/project/11,你仅仅需要获取更改的数据(/11 对应的数据),这时你就只需要一次对项目 11 的请求了:
import Projects from "./containers/projects"; import ProjectDetailRoute from "routes/project_detail"; export default class ProjectList { constructor () { this.path = "project"; this.projectDetailRoute = new ProjectDetailRoute(); } getChildRoutes (state,cb) { cb(null,[this.projectDetailRoute]); } getComponents (cb) { cb(null,ProjectTasks); } onEnter () { this.fetchProjects(); } fetchProjects () { ... } }
如何命名
Actions: <名词>-<动词>,比如 Project-Create,User-Login。依据是按照对象类型而不是动作类型分组。
Reducers: <名词>。
如何处理第三方异步数据
很明显的这里有条正确的流程(Action->Reducer->SmartContainer->DumbComponent)。但如何让你的更改符合这个流程?
第三方异步数据通常来自于 WebSocket。你可能仅仅想在应用的某些部分监听它,比如登录时,或者某些页面。而且,从 UI 到 actions 的处理流程是,用户触发了一个事件,木偶组件把事件传播到智能组件,然后触发一个 action。
但在这种情况下,没有木偶组件渲染内容,而由路由决定你何时接收数据,action 把数据注入到 redux。这个智能组件不需要任何木偶组件,也应该独立于其他智能组件。
React-Route 很好的处理了这个问题:
Route 可以有多个组件:
getComponents () { cb(null,{view: ViewContainer,data: DataContainer}; }
该智能组件可以这样渲染:
render () { return <div>{this.props.view}{this.props.data}</div> }
DataContainer 可以通过 componentDidUpdate 对 props 的更改作出响应,或者根据 componentWillUnmount 关闭连接。
总结
我已经连续两周在写这篇文章了,因为我总觉得还有些东西需要加进去。故事没有结束,但我把它发布出来以使 Redux 新手可以看到我对 Reactiflux 的探索。请评论和注释这篇文章,我将在接下来的几周内持续关注它。
作者信息
原文作者: Will Becker
原文链接: https://medium.com/lexical-labs-engineering/redux-best-practices-64d59775802e#.1b8hgoju1
翻译自MaxLeap团队_UX成员:Henry Bai
力谱宿云团队首发:https://blog.maxleap.cn/archives/930
欢迎关注微信订阅号:从移动到云端欢迎加入我们的MaxLeap活动QQ群:555973817,我们将不定期做技术分享活动。若有转载需要,请转发时注意自带作者信息一栏并本自媒体公号:力谱宿云,尊重原创作者及译者的劳动成果~ 谢谢配合~