在深入探讨处理 React 和 Firebase 时要遵循的最佳实践之前,让我们回顾一下我们在前几章中看到的内容。
在前面的章节中,我们看到了 Firebase 帐户设置、Firebase 与 ReactJs 的集成、Firebase 认证提供者的登录认证、React 组件中的认证状态管理、基于角色和配置文件的数据安全、Firebase 与 React Redux 的集成、Firebase 云消息传递、Firebase 云函数、,使用 Firebase Admin SDK API 和 React 组件,我希望您也喜欢这段旅程。现在我们知道从哪里开始以及如何编写代码,但最重要的是我们如何通过遵循最佳实践编写标准编码。
因此,当我们使用 React 和 Firebase 创建应用程序时,我们需要确保 Firebase 数据库中的数据结构以及将数据传递到 React 组件是应用程序最重要的部分。
在开发领域,每个开发人员对遵循最佳实践都有自己的看法,但我将与您分享我迄今为止观察到的和经历;你可能会有不同的看法。
以下是我们将在本章中介绍的主题列表:
- Firebase 的最佳实践
- React 和 Redux 的最佳实践
在 Firebase 中,我们都知道数据以 JSON 树格式存储,并实时同步到每个连接的设备。因此,在使用 Firebase 构建跨平台应用程序(web、iOS 和 Android)时,我们可以将一个实例共享给您的所有应用程序,以接收来自实时数据库的新数据的最新更新。因此,当我们将数据添加到 JSON 树中时,它将成为现有 JSON 结构中具有关联键的节点,因此我们始终需要计划如何保存数据以构建结构正确的数据库。
在 Firebase 中,我们有四种方法可用于将数据写入 Firebase 数据库:
| set( )
| 将数据写入或替换到定义的路径,如messages/tickets/<uid>
。 |
| update( )
| 更新到节点的特定子节点,而不替换其他子节点。我们还可以使用 update 方法将数据更新到多个位置。 |
| push( )
| 要在数据库中添加数据列表,可以使用push()
方法;它每次调用时都会生成一个唯一的 ID,例如helpdesk/tickets/<unique-user-id>/<unique-ticket-id>
。 |
| transaction( )
| 当我们处理复杂数据时,可以使用此方法,这些数据可能会被并发更新(如增量计数器)损坏。 |
现在,让我们来看看数据是如何在我们的帮助台应用程序中结构化的:
{
"tickets": {
"-L4L1BLYiU-UQdE6lKA_": {
"comments": "Need extra 4GB RAM in my system"
"date": "Fri Feb 02 2018 15:51:10 GMT+0530 (India Standa..."
"department": "IT"
"email": "[email protected]"
"issueType": "Hardware Request"
"status": "progress"
},
"-L4K01hUSDzPXTIXY9oU": {
"comments": "Need extra 4GB RAM in my system"
"date": "Fri Feb 02 2018 15:51:10 GMT+0530 (India Standa..."
"department": "IT"
"email": "[email protected]"
"issueType": "Hardware Request"
"status": "progress"
}
}
}
现在,让我们以前面的数据结构为例,使用set()
方法存储具有自动递增整数的数据:
{
"tickets": {
"0": {
"comments": "Need extra 4GB RAM in my system"
"date": "Fri Feb 02 2018 15:51:10 GMT+0530 (India Standa..."
"department": "IT"
"email": "[email protected]"
"issueType": "Hardware Request"
"status": "progress"
},
"1": {
"comments": "Need extra 4GB RAM in my system"
"date": "Fri Feb 02 2018 15:51:10 GMT+0530 (India Standa..."
"department": "IT"
"email": "[email protected]"
"issueType": "Hardware Request"
"status": "progress"
}
}
}
现在,如果您看到前面的数据结构,新的票据将存储为/tickets/1
。如果只有一个用户添加票证,这将起作用,但在我们的应用程序中,许多用户可以同时添加票证。如果两名员工同时向/tickets/2
写信,则其中一张票将被另一张票删除。因此,这不是推荐的做法,在处理数据列表时,始终建议使用push()
方法生成唯一的 ID(参考前面的数据结构)。
在 Firebase 实时数据库中,当您从 JSON 树获取数据时,我们还将获取该特定节点的所有子节点,因为当我们将数据添加到 JSON 树中时,它将成为现有 JSON 结构中具有关联键的节点。Firebase Realtime Database 允许嵌套深度高达 32 层的数据,因此,当我们授予某人在特定节点上的读写访问权限时,我们还授予该节点下所有子节点的访问权限。因此,最佳实践始终是保持数据结构尽可能平坦。
让我告诉你为什么嵌套数据是坏的;请参阅以下示例:
{
// a poorly nested data architecture, because
// iterating over "products" to get a list of names requires
// potentially downloading hundreds of products of mobile
"products": {
"electronics": {
"name": "mobile",
"types": {
"samsung": { "name": "Samsung S7 Edge (Black Pearl, 128 GB)", "description": "foo" },
"apple": { ... },
// a very long list of mobile products
}
}
}
}
使用这种嵌套的数据结构,很难对数据进行迭代。即使是列出产品名称这样的简单操作,也需要将整个产品树(包括所有产品列表和类型)下载到客户端。
在扁平结构中,数据被分割成不同的路径;根据需要,只下载所需的节点可能很容易:
{
// products contains only meta info about each product
// stored under the product's unique ID
"products": {
"electronics": {
"name": "mobile"
},
"home_furniture": { ... },
"sports": { ... }
},
// product types are easily accessible (or restricted)
// we also store these by product Id
"types": {
"mobile":{
"name":"samsung"
},
"laptop": {...},
"computers":{...},
"television":{...}
"home_furniture": { ... },
"sports": { ... }
},
// details are separate from data we may want to iterate quickly
// but still easily paginated and queried, and organized by
product ID
"detail": {
"electronics": {
"samsung": { "name": "Samsung S7 Edge (Black Pearl, 128 GB)",
"description": "foo" },
"apple": { ... },
"mi": { ... }
},
"home_furniture": { ... },
"sports": { ... }
}
}
在前面的示例中,我们有一些简单嵌套的数据(例如,每个产品的细节本身就是带有子对象的对象),但我们也通过如何迭代和稍后读取数据来逻辑地组织数据。我们存储了重复数据来定义对象之间的关系;这对于维护冗余的双向、多对多或一对多关系是必要的。它使我们能够快速高效地获取手机,即使产品或产品类型的列表规模达到数百万,或者 Firebase 规则和安全性将阻止访问某些记录。
现在可以通过每个产品只下载几个字节来迭代产品列表,快速获取元数据以在 UI 中显示产品。
在看到前面的 flattern 结构之后,如果您认为在 Firebase 中单独查找每个记录是可以的,那么是的,因为 Firebase 在内部使用 web 套接字和客户端库来处理传入和传出的优化请求。即使我们得到数万条记录,这种方法仍然是合理的。
Always create the data structure that can scale in future when the app user grows.
Firebase 文档已经提到并清除了这个主题,以避免在 Firebase 数据库中使用数组,但我想强调一些可以使用数组存储数据的用例。
参考以下几点;如果以下所有条件均为真,我们可以使用数组将数据存储在 Firebase 中:
- 如果一个客户端一次可以写入数据
- 对于移除键,我们可以保存阵列并拼接它,而不是使用
.remove()
- 当通过数组索引(可变键)引用任何内容时,我们需要小心
当我们谈论使用created_date
对 Firebase 中的数据进行排序和过滤时,请确保您在创建的每个对象中都添加了created_date
键以及日期时间戳,例如ref.set(new Date().toString())
和ref.set(new Date().getTime())
,因为 Firebase 不支持 JavaScript 日期对象类型(ref.set(new Date());)
。
Firebase Admin SDK 提供在概要文件对象中添加自定义属性的功能;借助于此,我们可以为用户提供不同的访问控制,包括 react firebase 应用程序中的基于角色的控制,因此它们不会被设计为存储其他数据(如配置文件和其他自定义数据)。我们知道这看起来是一种非常方便的方式,但强烈建议不要这样做,因为这些声明存储在 ID 令牌中,这会影响性能问题,因为所有经过认证的请求始终包含与登录用户相对应的 Firebase ID 令牌。
- 自定义声明仅用于存储用于控制用户访问的数据
- 自定义声明的大小有限,因此传递大于 1000 字节的自定义声明将抛出错误
管理用户的会话并提示重新验证,因为每次用户登录时,都会将用户凭据发送到 Firebase 认证后端,并交换 Firebase ID 令牌(JWT)和刷新令牌。
以下是我们需要管理用户会话的常见场景:
- 用户被删除
- 用户已禁用
- 电子邮件地址和密码已更改
Firebase Admin SDK 还提供了使用revokeRefreshToken()
方法撤销特定用户会话的能力。它撤销给定用户的活动刷新令牌。如果我们重置密码,Firebase 认证后端将自动撤销用户令牌。
当任何数据需要认证才能访问时,必须配置以下规则:
{
"rules": {
"users": {
"$user_id": {
".read": "$user_id === auth.uid && auth.token.auth_time > (root.child('metadata').child(auth.uid).child('revokeTime').val() || 0)",
".write": "$user_id === auth.uid && auth.token.auth_time > (root.child('metadata').child(auth.uid).child('revokeTime').val() || 0)"
}
}
}
}
当我们使用 Firebase 创建实时应用程序时,我们还需要监控客户端与数据库连接和断开连接时的连接。Firebase 提供了一个简单的解决方案,当客户端与 Firebase 数据库服务器断开连接时,您可以使用该解决方案写入数据库。我们可以在断开连接时执行所有操作,如写入、设置、更新和删除。
参考 Firebase 的本例onDiscconnect()
方法:
var presenceRef = firebase.database().ref("disconnectmessage");
// Write the string when client loses connection
presenceRef.onDisconnect().set("I disconnected!");
我们也可以附加回调函数,以确保onDisconnect()
方法的附加正确:
presenceRef.onDisconnect().remove(function(err) {
if (err) {
console.error('onDisconnect event not attached properly', err);
}
});
要取消onDisconnect()
方法,可以调用.cancel()
方法onDisconnectRef.cancel();
。
Firebase 实时数据库为检测连接状态提供特殊位置 /.info/connected
。
每次应用程序连接状态更改时都会更新此信息;它返回布尔值以检查客户端连接状态是否已连接:
var connectedRef = firebase.database().ref(".info/connected");
connectedRef.on("value", function(snap) {
if (snap.val() === true) {
alert("connected");
} else {
alert("not connected");
}
});
我们还需要关注一些事情,例如应用程序中的 Firebase 实时数据库性能,以了解如何使用不同的实时数据库监控工具优化您的实时数据库性能。
我们可以通过几种不同的工具收集实时数据库的性能数据:
- **高级概述:**我们可以使用 Firebase profiler 工具查看未索引查询列表和读/写操作的实时概述。要使用探查器工具,请确保已安装 Firebase CLI 并运行以下命令。
- **计费使用率估算:**FIrebase 使用率指标在 FIrebase 控制台中为您提供计费使用率和高级性能指标。
- **详细的深入分析:**Stackdriver 监控工具为您提供了一个更精细的视图,可以查看数据库在一段时间内的运行情况。
For more details about profiling, visit https://firebase.google.com/docs/database/usage/profile
收集数据后,根据需要改进的性能领域,探索以下最佳实践和策略:
| 公制 | 说明 | 最佳实践 | | 负载/利用率 | 优化在任何给定时间处理请求时使用的数据库容量(反映在加载或io/database_ 加载指标中)。 | 优化您的数据结构(https://firebase.google.com/docs/database/usage/optimize#data-结构 跨数据库共享数据(https://firebase.google.com/docs/database/usage/optimize#shard-数据 提高听者效率 https://firebase.google.com/docs/database/usage/optimize#efficient-监听器 使用基于查询的规则*(*限制下载 https://firebase.google.com/docs/database/usage/optimize#query-规则 优化连接( https://firebase.google.com/docs/database/usage/optimize#open-连接件 | | 活动连接 | 平衡到数据库的同时连接数和活动连接数,使其保持在 100000 连接限制以下。 | 跨数据库的碎片数据(https://firebase.google.com/docs/database/usage/optimize#shard-数据 减少新连接(https://firebase.google.com/docs/database/usage/optimize#open-连接件) | | 输出带宽 | 如果从数据库下载的数据似乎比您希望的要高,则可以提高读取操作的效率并减少加密开销。 | 优化连接(https://firebase.google.com/docs/database/usage/optimize#open-连接 优化您的数据结构(https://firebase.google.com/docs/database/usage/optimize#data-结构 使用基于查询的规则限制下载量( https://firebase.google.com/docs/database/usage/optimize#query-规则 重用 SSL 会话(https://firebase.google.com/docs/database/usage/optimize#ssl-会话 提高听者效率https://firebase.google.com/docs/database/usage/optimize#efficient-听众 限制对数据的访问(https://firebase.google.com/docs/database/usage/optimize#secure-数据) | | 存储 | 确保您没有存储未使用的数据,或者在其他数据库和/或 Firebase 产品之间平衡存储的数据,以保持配额。 | 清理未使用的数据(https://firebase.google.com/docs/database/usage/optimize#cleanup-存储 优化您的数据结构(https://firebase.google.com/docs/database/usage/optimize#data-结构 跨数据库的碎片数据( https://firebase.google.com/docs/database/usage/optimize#shard-数据 使用 Firebase 存储(https://firebase.google.com/docs/storage |
来源:https://firebase.google.com/docs/database/usage/optimize
如果使用 Blaze 定价计划,我们可以创建多个实时数据库实例;然后,我们可以在同一个 Firebase 项目中创建多个数据库实例。
要从 Firebase CLI 编辑和部署规则,请执行以下步骤:
firebase target:apply database main my-db-1 my-db-2
firebase target:apply database other my-other-db-3
Update firebase.json with the deploy targets:
{
"database": [
{"target": "main", "rules", "foo.rules.json"},
{"target": "other", "rules": "bar.rules.json"}
]
}
firebase deploy
确保从同一位置一致地编辑和部署规则。
使用数据库引用访问存储在辅助数据库实例中的数据。您可以通过 URL 或应用程序获取特定数据库实例的引用。如果我们没有在.database()
方法中指定 URL,那么我们将获得应用程序默认数据库实例的引用:
// Get the default database instance for an app
var database = firebase.database();
// Get a secondary database instance by URL
var database = firebase.database('https://reactfirebaseapp-9897.firebaseio.com');
欲查看 Firebase 示例项目列表,请访问https://firebase.google.com/docs/samples/ 。
如需查看 Firebase 库列表,请参阅https://firebase.google.com/docs/libraries/ 。
您也可以订阅https://www.youtube.com/channel/UCP4bf6IHJJQehibu6ai__cg 频道更新。
每当我们有具有动态功能的组件时,数据就会出现在画面中;同样,在 React 中,我们必须处理动态数据,这似乎很容易,但并非每次都如此。
听起来很混乱!
这很容易,但有时很难,因为在 React 组件中,可以通过多种方式传递属性以从中构建渲染树,但更新视图的清晰度不高。
在前面的章节中,这句话已经清楚地表达了,所以如果你仍然不清楚,请参考那些。
正如我们所知,在 SPA(单页应用程序)中,当我们必须与状态和时间签订合同时,随着时间的推移很难掌握状态。在这里,Redux 帮助很大,如何?这是因为,在 JavaScript 应用程序中,Redux 处理两种状态:一种是数据状态,另一种是 UI 状态,它是 SPA(单页应用程序)的标准选项。此外,请记住,Redux 可以与 Angular、Jquery 或 React JavaScript 库或框架一起使用。
Redux 是一种工具,而 Flux 只是一种您无法使用的模式,比如即插即用或下载。我不否认 Redux 对通量模式有一定的影响,但我们不能说,它 100%看起来像通量。 让我们继续讨论一些差异。
Redux 遵循三个指导原则,如图所示,这也将涵盖 Redux 和 Flux 之间的差异:
-
**单存储方法:**我们在前面的图中看到,存储假装是应用程序和 Redux 中各种状态修改的“中介”。它通过存储控制两个组件之间的直接通信,即单点通信。这里,Redux 和 Flux 的区别在于 Flux 有多个存储方法,而 Redux 只有一个存储方法。
-
**只读状态:**在 React 应用程序中,组件不能直接更改状态,但必须通过“操作”将更改发送到存储。这里,Store 是一个对象,它有四个方法,如图所示:
- 仓库调度(行动)
- store.subscribe(侦听器)
- store.getState()
- 更换减速器(下一个减速器)
-
**Reducer 功能改变状态:**Reducer 功能将处理调度动作改变状态,因为 Redux 工具不允许两个组件之间直接通信;因此,它不仅会更改状态,而且还会为状态更改描述分派操作。这里的约化子可以看作是纯函数。以下是编写 reducer 函数的几个特征:
- 没有外部数据库或网络呼叫
- 根据其参数返回值
- 参数是“不可变的”
- 相同的参数返回相同的值
Reducer 函数被称为纯函数,因为它们除了返回基于其设置参数的值之外,什么都不做;它没有任何其他后果。建议它们具有平坦状态。在 Flux 或 Redux 体系结构中,处理来自 API 返回的嵌套资源总是很困难的,因此建议在组件中使用平面状态,例如 normalize。
**Hint for pros: **const data = normalize(response, arrayOf(schema.user)) state = _.merge(state, data.entities)
在平面状态中,我们有处理嵌套资源的好处,在不可变对象中,我们有不能修改的声明状态的好处。
不可变对象的另一个好处是,通过它们的引用级别相等性检查,我们可以极大地提高渲染性能。在 Immutable 中,我们有一个shouldComponentUpdate
的示例:
shouldComponentUpdate(nexProps) {
// instead of object deep comparsion
return this.props.immutableFoo !== nexProps.immutableFoo
}
在 JavaScript 中,使用不变性深度冻结节点将帮助您在变异之前冻结节点,然后验证结果。以下示例显示了相同的逻辑:
return {
...state,
foo
}
return arr1.concat(arr2)
我希望前面的例子能够清楚地说明 immutable JS 的用途和好处。它也有一种不复杂的方式,但其使用率非常低:
import { fromJS } from 'immutable'
const state = fromJS({ bar: 'biz' })
const newState = foo.set('bar', 'baz')
从我的角度来看,这是一个非常快速和美丽的功能使用。
我们必须在客户端应用程序中使用路由,对于 ReactJS,我们还需要一个或另一个路由库,因此我建议您使用 react router dom 而不是 react router。
优点:
- 标准化结构中的视图声明有助于我们立即了解什么是应用程序视图
- 使用 react-router-dom,我们可以轻松地处理嵌套视图及其视图的渐进解析
- 使用浏览历史记录功能,用户可以向后/向前导航并恢复视图状态
- 动态路径匹配
- 导航时视图上的 CSS 转换
- 标准化的应用程序结构和行为,在团队中工作时非常有用
注意:React 路由不提供任何处理数据获取的方法。我们需要使用异步道具或其他 React 数据获取机制。
可以看出,处理 webpack 的开发人员很少知道应用程序代码在几个 JavaScript 文件中的代码拆分:
require.ensure([], () => {
const Profile = require('./Profile.js')
this.setState({
currentComponent: Profile
})
})
这种代码拆分是必要的,因为每个代码对每个用户都不有用,而且不必在每个页面中加载代码块,这将给浏览器带来负担,因此为了避免这种情况,我们应该将应用程序拆分为多个代码块。
现在,您将遇到一个问题,比如如果我们将有更多的代码块,我们将不得不有更多的 HTTP 请求,这也将影响性能,但是在 HTTP/2multiplexed 的帮助下,您的问题将得到解决。您还可以将分块代码与分块哈希相结合,这也将在您更改代码时优化浏览器缓存比率。
JSX 什么都不是,但简单地说,它只是 JavaScript 语法的扩展。另外,如果您观察 JSX 的语法或结构,您会发现它与 XML 编码类似。JSX 正在执行预处理器足迹,它将 XML 语法添加到 JavaScript 中。虽然您当然可以在不使用 JSX 的情况下使用 React,但 JSX 使 React 更加整洁和优雅。与 XML 类似,JSX 标记具有标记名、属性和子项,如果属性值括在引号中,则该值将成为字符串。
JSX 的工作原理与 XML 类似,具有平衡的开始和结束标记,它有助于使大型树比“函数调用”或“对象文本”更易于阅读。
在 React中使用 JSX 的优点:
- JSX 比 JavaScript 函数更易于理解和思考
- 设计师和团队的其他成员更熟悉 JSX 的标记
- 您的标记变得更加语义化、结构化和更有意义
如何容易形象化?
正如我所说,结构/语法很容易可视化/注意到,与 JavaScript 相比,JSX 格式更清晰易读。
在我们的应用程序中,我们可以看到 JSX 语法是如何易于理解和可视化的;在这背后,有一个很大的原因是语义语法结构。JSX 很乐意将 JavaScript 代码转换为更具语义和意义的结构化标记。这允许您使用类似 HTML 的语法声明组件结构和信息,因为您知道它将转换为简单的 JavaScript 函数。React 概述了 React.DOM 命名空间中预期的所有 HTML 元素。好的方面是,它还允许您在标记中使用自己编写的自定义组件。
在 React 组件中,我们可以从更高级别的组件传递属性,因此必须了解属性,因为它将为您提供更大的灵活性来扩展组件并节省您的时间:
MyComponent.propTypes = {
isLoading: PropTypes.bool.isRequired,
items: ImmutablePropTypes.listOf(
ImmutablePropTypes.contains({
name: PropTypes.string.isRequired,
})
).isRequired
}
您还可以通过使用 react Immutable PropType 验证不可变 JS 属性的方式来验证属性。
高阶组件只是原始组件的扩展版本:
PassData({ foo: 'bar' })(MyComponent)
使用它们的主要好处是,我们可以在多种情况下使用它,例如,认证或登录验证:
requireAuth({ role: 'admin' })(MyComponent)
另一个好处是,使用高阶组件,您可以单独获取数据,并将逻辑设置为以简单的方式显示视图。
与其他框架相比,它有更多的优点:
- 它可能没有任何其他方式的影响。
- 正如我们所知,不需要绑定,因为组件不能直接交互。
- 国家是全球管理的,因此管理不善的可能性较小。
- 有时,对于中间件,很难管理其他方式的影响。
从上面提到的几点来看,很明显 Redux 架构非常强大,并且具有可重用性。
We can also use build React-Firebase application with the ReactFire library, with a few lines of JavaScript. We can integrate Firebase data into React apps via ReactFireMixin.
在本书的最后一章中,我们介绍了使用 React 和 Firebase 时应遵循的最佳实践。我们还了解了如何使用不同的工具来监控应用程序性能,以减少 bug 的数量。我们还讨论了 Firebase 实时数据库中数据结构的重要性,并讨论了传递到 React 组件的动态数据。我们还研究了其他关键因素,如 JSX、React 路由和 React PropTypes,它们是 React 应用程序中最有用的元素。我们还了解到,Redux 在维护单页应用程序(SPA的状态方面有很大帮助。