目前,我们的应用能够添加、编辑和删除文章,但只有在 Redux Reducer 的帮助下才能在前端进行。我们需要添加一些完整的堆栈机制,使其能够对数据库执行 CRUD 操作。我们还需要在后端添加一些安全功能,以便未经身份验证的用户无法在 MongoDB 集合上执行 CRUD 操作。
让我们暂停一下编码。在开始开发全堆栈 Falcor 机制之前,让我们更详细地讨论 React、Node 和 Falcor 设置。
重要的是要理解为什么我们在技术堆栈中选择了 Falcor。一般来说,在我工作的定制软件开发公司(您可以在www.ReactPoland.com上找到更多信息),我们使用 Falcor,因为它在开发全堆栈移动/web 应用的生产率方面对我们的客户具有许多巨大的优势。其中包括:
- 概念的简单性
- 与 RESTful 方法相比,开发速度提高了 30%以上
- 学习曲线很浅,因此学习 Falcor 的开发人员可以很快变得有效
- 一种有效的获取数据的方法(从后端到客户端),非常令人震惊
现在,我将保持这四点简短而甜蜜。在本章后面,您将了解更多有关使用 Falcor 和 Node 时可能遇到的问题。
目前,我们已经组装了一种带有 React、Redux、Falcor、Node、Express 和 MongoDB 的全栈初学者工具包。它还不完美。本章的重点将包括以下主题:
- 更好地理解无休止的数据获取解决方案的大局,以及 Falcor 和 Relay/GraphQL 之间的异同
- 如何保护路由以在后端对用户进行身份验证
- 如何在错误选择器的帮助下处理后端的错误并将其无缝发送到前端
- 详细了解 Falcor 的哨兵以及
$ref
、$atom
和$error
在 Falcor 中的具体工作方式 - 什么是 JSON 图及其工作原理
- Falcor 中的虚拟 JSON 概念是什么
在单页应用时代之前,在客户机上获取数据没有问题,因为所有数据都是在服务器上获取的,即使这样,服务器也会将 HTML 标记发送给客户机。每次有人点击 URL(href
,我们的浏览器都会从服务器请求全新的 HTML 标记。
基于上述非 SPA 应用的原则,RubyonRails 成为 web 开发技术堆栈之王,但后来情况发生了变化。自 2009-2010 年以来,我们一直在创建越来越多的 JavaScript 客户端应用,这些应用很可能从后端获取一次,例如,bundle.js
文件。他们叫温泉。
由于 SP 应用的这种趋势,出现了一些非 SP 应用开发人员不知道的新问题,例如从后端的 API 端点获取数据,以便在客户端使用 JSON 数据。
通常,RESTful 应用的老式工作流如下所示:
- 在后端创建端点。
- 在前端创建抓取机制。
- 通过在前端根据 API 规范对 POST/GET 请求进行编码,从后端获取数据。
- 当您从后端获取 JSON 到前端时,您可以使用数据并使用它,以便根据特定用例创建 UI 视图。
如果客户或老板这样的人改变了主意,那么这个过程有点令人沮丧,因为您在后端和前端实现了整个代码。后来,后端 API 端点变得无关紧要,因此您需要根据更改的需求从头开始处理它们。
对于 Falcor 来说,随处可见的一个模型是这个伟大图书馆的主要口号。通常,使用它的主要目的是创建一个在前端和后端完全相同的 JSON 模型。这对我们意味着什么?这意味着,如果有任何变化,我们需要改变模型,这在后端和前端是完全相同的——因此,如果有任何变化,我们需要调整我们的模型,而不必担心数据是如何在后端提供和在前端获取的。
Falcor 的创新之处在于引入了一个称为虚拟 JSON 的新概念(类似于 React 的虚拟 DOM)。这使您可以将所有远程数据源(例如,我们的示例中的 MongoDB)表示为一个域模型。整个想法是以相同的方式编写代码,而不关心数据的位置:数据是在客户端内存缓存上还是在服务器上?您无需在意,因为 Falcor 以其创新的方法为您做了很多工作(例如,查询xhr
请求)。
数据获取对于开发人员来说是一个问题。Falcor 是来帮助简化的。您可以将数据从后端提取到前端,编写的代码行比以往更少!
2016 年 5 月,我看到的唯一可行的竞争对手是名为 Relay(客户端)和 GraphQL(后端)的 Facebook 库。
让我们试着比较两者。
与任何工具一样,总是有优点和缺点。
可以肯定的是,在中小型项目中,Falcor 总是比 Relay/GraphQL 好,至少除非您拥有精通 Relay/GraphQL 的开发大师(或者您自己也是大师)。为什么呢?
通常,Relay(用于前端)和 GrapQL(用于后端)是两种不同的工具,您必须高效才能正确使用。
通常在商业环境中,您没有太多时间从头开始学习。这也是 React 成功的原因之一。
为什么反应成功了?React 更容易掌握,以便成为一名高效的前端开发人员。一个 CTO 或技术总监雇佣一个了解 jQuery 的新手开发人员(例如),然后 CTO 可以很容易地预测这个初级开发人员将在 7 到 14 天内有效地做出反应;我教初级前端开发人员掌握 JavaScript/jQuery 的基本知识,我发现他们很快就能用 React 高效地创建客户端应用。
我们可以在 Falcor 身上找到同样的情况。Falcor 与 Relay+GraphQL 相比,就像 React 与 Angular 的整体框架相比简单。
前几段中描述的这一单一因素意味着 Falcor 更适合预算有限的中小型项目。
当你有 6 个月的时间掌握一项技术时,你可能会在预算大得多的大公司(如 Facebook)中找到一些学习 Relay/GraphQL 的机会。
FalcorJS 可以在两周内有效掌握,但 GraphQL+Relay 无法。
这两种工具都试图解决同一个问题。通过对开发人员和网络的设计,它们是高效的(与 RESTful 方法相比,尝试优化查询的数量)。
它们能够查询后端服务器以获取数据,还具有批处理能力(因此您可以通过一个网络请求获取两组以上的数据)。两者都有一些缓存功能。
通过技术概述,我们可以发现,一般来说,Relay 允许您从 GraphQL 服务器查询未定义数量的项。在 Falcor 中,为了进行比较,您需要首先询问后端它有多少项,然后才能查询集合对象的详细信息(如本书中的文章)。
一般来说,这里最大的区别在于 GraphQL/Relay 是一种查询语言工具,而 Falcor 不是。什么是查询语言?您可以使用它从前端进行类似于 SQL 的查询,如下所示:
post: () => Relay.QL
fragment on Articles {
title,
content
}
前面的代码可以通过Relay.QL
从前端进行查询,然后 GraphQL 以与 SQL 相同的方式处理查询,如下所示:
SELECT title, content FROM Articles
例如,如果数据库中有一百万篇文章,而你没想到前端会有这么多文章,事情可能会变得更加困难。
在 Falcor 中,您的做法有所不同,正如您已经了解到的:
const articlesLength = await falcorModel.
getValue('articles.length').
then((length) => length);
const articles = await falcorModel.
get(['articles', {from: 0, to: articlesLength-1},
['_id','articleTitle', 'articleContent']]).
then((articlesResponse) => articlesResponse.json.articles);
在前面的 Falcor 示例中,您必须首先知道 MongoDB 实例中有多少条记录。
这是最重要的分歧之一,给双方带来了一些挑战。
对于 GraphQL 和 Relay,问题在于这些查询语言的能力是否值得学习曲线中产生的复杂性,因为对于中小型项目来说,这种复杂性可能不值得。
既然已经讨论了基本的区别,让我们关注 Falcor 并改进我们当前的发布应用。
我们需要改进以下方面:
- 登录后,我们将在每个请求中发送用户详细信息(令牌、用户名和角色;您可以在后面的部分中找到一个屏幕截图,以改进前端上的 Falcor 代码)
- 需要保护后端,以便在后端上运行添加/编辑/删除操作之前检查授权
- 我们需要提供在后端捕获错误的能力,并向前端的用户发出关于某些工作不正常的通知
目前,我们的应用能够添加/编辑/删除路线。我们当前实现的问题是,我们没有检查执行 CRUD 操作的客户端是否具有执行 CRUD 操作的权限。
保护 Falcor 路由的解决方案需要对我们当前的实现进行一些更改,因此对于每个请求,在执行操作之前,我们将检查是否从客户端获得了正确的令牌,以及进行呼叫的用户是否能够编辑令牌(在我们的例子中,这意味着,如果任何人拥有编辑角色,并且通过用户名和密码进行了正确的身份验证,那么他就可以添加/编辑/删除文章)。
正如 Falcor 文档所述,“JSON 图形是将图形信息建模为 JSON 对象的约定。使用 Falcor 的应用将其所有域数据表示为单个 JSON 图形对象。”
一般来说,Falcor 中的 JSON 图是有效的 JSON,具有一些新特性。更准确地说,JSON 图形除了字符串、数字和布尔值之外,还引入了一种新的数据类型。Falcor 中的新数据类型称为哨兵。我将在本章后面部分解释。
一般来说,在 Falcor 中需要了解的第二件最重要的事情是 JSON 信封。最棒的是它们是开箱即用的,所以你不必太担心它们。但是,如果您想知道简短而甜蜜的答案是什么,JSON 信封可以帮助您通过 HTTP 协议发送 JSON 的模型。这是一种从前端到后端传输数据的方式(使用.call
、.set
和.get
方法)。同样,在后端之前(处理请求的详细信息之后),在将改进模型的详细信息发送到客户端之前,Falcor 将其放入信封中,以便通过网络轻松传输。
JSON 信封的一个很好的(但不是完美的)类比是,您将一个书面列表放入信封中,因为您不想将一些有价值的信息从点A发送到点B;网络不在乎你在信封里寄什么。最重要的是发送方和接收方都知道应用模型的上下文。
您可以在找到关于 JSON 图和信封的更多信息 http://netflix.github.io/falcor/documentation/jsongraph.html 。
目前,在用户自行授权后,所有数据都保存到本地存储器中。我们需要通过将数据(令牌、用户名和角色)与每个请求一起发送回后端来结束循环,这样我们就可以再次检查用户的身份验证是否正确。如果没有,那么我们需要在请求中发送一个身份验证错误,并在前端显示它。
出于安全原因,以下屏幕截图中的安排特别重要,以便未经授权的用户无法在我们的数据库中添加/编辑/删除文章:
在屏幕截图中,您可以找到在哪里可以获得有关localStorage
数据的信息。
以下是我们在src/falcorModel.js
中的当前代码:
// this code is already in the codebase
const falcor = require('falcor');
const FalcorDataSource = require('falcor-http-datasource');
const model = new falcor.Model({
source: new FalcorDataSource('/model.json')
});
export default model;
我们需要将其更改为新的改进版本:
import falcor from 'falcor';
import FalcorDataSource from 'falcor-http-datasource';
class PublishingAppDataSource extends FalcorDataSource {
onBeforeRequest ( config ) {
const token = localStorage.token;
const username = localStorage.username;
const role = localStorage.role;
if (token && username && role) {
config.headers['token'] = token;
config.headers['username'] = username;
config.headers['role'] = role;
}
}
}
const model = new falcor.Model({
source: new PublishingAppDataSource('/model.json')
});
export default model;
在前面的代码片段中我们做了什么?ECMAScript 6 中的extends
关键字显示了类语法的简单性的一个例子。扩展FalcorDataSource
意味着PublishingAppDataSource
继承FalcorDataSource
所拥有的一切,并使onBeforeRequest
方法具有我们的定制行为(通过变异config.headers
。onBeforeRequest
方法是在xhr
实例创建之前检查我们修改的配置。这有助于我们使用令牌、用户名和角色修改XMLHttpRequest
——如果我们的应用的用户在此期间注销,我们可以将该信息发送到后端。
在falcorModel.js
中实现上一个代码并记录用户后,这些变量将添加到每个请求中:
通常,我们当前从server/routes.js
文件导出一个对象数组。我们需要改进它,因此我们将返回一个函数,该函数将修改我们的对象数组,以便我们可以控制将哪个路由返回给哪个用户,如果用户没有有效的令牌或足够的权限,我们将返回一个错误。这将提高我们整个应用的安全性。
在server/server.js
文件中,找到这个旧代码:
// this shall be already in your codebase
app.use('/model.json', falcorExpress.dataSourceRoute((req, res)
=> {
return new falcorRouter(routes);
}));
用这个改进的替换它:
app.use('/model.json', falcorExpress.dataSourceRoute((req, res)
=> {
return new falcorRouter(
[]
.concat(routes(req, res))
);
}));
在我们的新版本中,我们假设routes
变量是包含req
和res
变量的函数。
让我们改进路由本身,这样我们就不再返回数组,而是返回一个数组的函数(这样我们就有了更大的灵活性)。
下一步是改进server/routes.js
文件,以便生成一个接收currentSession
对象的函数,该对象存储关于请求的所有信息。我们需要在routes.js
中对此进行更改:
// this code is already in your codebase:
const PublishingAppRoutes = [
...sessionRoutes,
{
route: 'articles.length',
get: () => {
return Article.count({}, function(err, count) {
return count;
}).then ((articlesCountInDB) => {
return {
path: ['articles', 'length'],
value: articlesCountInDB
}
})
}
},
//
// ...... There is more code between, it has been truncated in
//order to save space
//
export default PublishingAppRoutes;
我们不需要导出路由数组,而是需要导出一个函数,该函数将根据当前请求的头详细信息返回路由。
server/routes.js
文件的顶部(带导入)如下:
import configMongoose from './configMongoose';
import sessionRoutes from './routesSession';
import jsonGraph from 'falcor-json-graph';
import jwt from 'jsonwebtoken';
import jwtSecret from './configSecret';
let $atom = jsonGraph.atom; // this will be explained later
//in the chapter
const Article = configMongoose.Article;
然后导出一个新函数:
export default ( req, res ) => {
let { token, role, username } = req.headers;
let userDetailsToHash = username+role;
let authSignToken = jwt.sign(userDetailsToHash,
jwtSecret.secret);
let isAuthorized = authSignToken === token;
let sessionObject = {isAuthorized, role, username};
console.info(`The ${username} is authorized === `,
isAuthorized);
const PublishingAppRoutes = [
...sessionRoutes,
{
route: 'articles.length',
get: () => {
return Article.count({}, function(err, count) {
return count;
}).then ((articlesCountInDB) => {
return {
path: ['articles', 'length'],
value: articlesCountInDB
}
})
}
}];
return PublishingAppRoutes;
}
首先,我们将req
(请求详细信息)和res
(表示 HTTP 响应的对象)变量接收到 arrow 函数中。根据req
提供的信息,我们得到了标题详细信息(let { token, role, username } = req.headers;
。接下来,我们有userDetailsToHash
,然后我们用let authSignToken = jwt.sign(userDetailsToHash, jwtSecret.secret)
检查什么是正确的authToken
。之后,我们用let isAuthorized = authSign === token
检查用户是否被授权。然后我们创建一个sessionObject
,它将在以后的所有 Falcor 路线(let sessionObject = {isAuthorized, role, username};
中重复使用。
目前,我们有一条路径(articles.length
),在第 2 章、中对我们的发布应用进行了全栈登录和注册(所以目前没有新的内容)。
正如您在前面的代码中看到的,我们不是直接导出PublishingAppRoutes
,而是使用箭头函数export default (req, res)
导出。
我们需要重新添加(在articles.length
下)第二条路线,称为articles[{integers}]["_id","articleTitle","articleContent"]
,在server/routes
中有以下代码:
{
route:
'articles[{integers}]["_id","articleTitle","articleContent"]',
get: (pathSet) => {
const articlesIndex = pathSet[1];
return Article.find({}, function(err, articlesDocs) {
return articlesDocs;
}).then ((articlesArrayFromDB) => {
let results = [];
articlesIndex.forEach((index) => {
const singleArticleObject =
articlesArrayFromDB[index].toObject();
const falcorSingleArticleResult = {
path: ['articles', index],
value: singleArticleObject
};
results.push(falcorSingleArticleResult);
});
return results;
})
}
}
这是从数据库获取文章并为其返回falcor-route
的路径。与之前介绍的完全相同;唯一不同的是,现在它是函数的一部分(export default ( req, res ) => { ... }
。
在我们开始使用falcor-router
在后端实现添加/编辑/删除之前,我们需要先介绍 Sentinel 的概念,因为它对我们的全栈应用的健康非常重要,稍后将解释其原因。
让我们了解什么是哨兵。他们需要使 Fullstack 的 Falcor 应用正常工作。这是一套你必须学习的工具。
它们是新的原语值类型,专门用于使后端和客户端之间的数据传输更加容易和现成(新的 Falcor 原语值的示例有$error
和$ref
。这里有一个类比:在常规 JSON 中有类型,如字符串、数字、对象和。另一方面,在 Falcor 的虚拟 JSON 中,您还可以在前面列出的标准 JSON 类型旁边使用哨兵,如$error
、$ref
或$atom
。
Additional information about sentinels is available at https://netflix.github.io/falcor/documentation/model.html#sentinel-metadata.
在这个阶段,了解 Falcor 的哨兵是如何工作的很重要。Falcor 中不同类型的哨兵将在以下章节中解释。
根据文档,“引用是一个 JSON 对象,其$type
键的值为ref
,而value
键的值为Path
数组。”
“引用就像 UNIX 文件系统中的符号链接,”正如文档所述,这种比较非常好。
$ref
的示例如下:
{ $type: 'ref', value: ['articlesById', 'STRING_ARTICLE_ID_HERE'] }
If you use $ref(['articlesById','STRING_ARTCILE_ID_HERE'])
, it's equal to the preceding example. The $ref
sentinel is a function that changes the array's details into that $type
and value's notation object.
为了在任何 Falcor 相关项目中部署/使用$ref
,您可以找到这两种方法,但在我们的项目中,我们将坚持$ref(['articlesById','STRING_ARTCILE_ID_HERE'])
约定。
为了明确起见,以下是如何在我们的代码库中导入$ref
哨兵:
// wait, this is just an example, don't code this here:
import jsonGraph from 'falcor-json-graph';
let $ref = jsonGraph.ref;
// now you can use $ref([x, y]) function
导入falcor-json-graph
后,可以使用$ref
哨兵。您将已经安装了falcor-json-graph
库,如前一章所述;如果没有,请使用以下选项:
npm i --save falcor-json-graph@1.1.7
但是articlesById
在整个$ref
演出中意味着什么?在前面的例子中,STRING_ARTICLE_ID_HERE
是什么意思?让我们看一下我们项目中的一个例子,它可能会让您更清楚。
假设 MongoDB 实例中有两篇文章:
// this is just explanation example, don't write this here
// we assume that _id comes from MongoDB
[
{
_id: '987654',
articleTitle: 'Lorem ipsum - article one',
articleContent: 'Here goes the content of the article'
},
{
_id: '123456',
articleTitle: 'Lorem ipsum - article two',
articleContent: 'Sky is the limit, the content goes here.'
}
]
因此,根据我们的数组中模拟文章的示例(id987654
和123456
),$ref
将如下所示:
// JSON envelope is an array of two $refs
// The following is an example, don't write it
[
$ref([ articlesById,'987654' ]),
$ref([ articlesById,'123456' ])
]
更详细的答案是:
// JSON envelope is an array of two $refs (other notation than
//above, but the same end effect)
[
{ $type: 'ref', value: ['articlesById', '987654'] },
{ $type: 'ref', value: ['articlesById', '123456'] }
]
An important thing to note is that articlesById
is a new route that hasn't been created yet (we will do so in a moment).
但为什么我们的文章中需要这些?
通常,可以在多个位置保留对数据库中一个对象的引用(如 Unix 中的符号链接)。在我们的例子中,它是一篇文章集合中带有某个_id
的文章。
哨兵什么时候派上用场?想象一下,在我们的发布应用的模型中,我们添加了一个最近访问的文章功能,并提供了喜欢一篇文章的功能(比如在 Facebook 上)。
基于这两个新特性,我们的新模型将如下所示(这只是一个示例;不要编写代码):
// this is just explanatory example code:
let cache = {
articles: [
{
id: 987654,
articleTitle: 'Lorem ipsum - article one',
articleContent: 'Here goes the content of the article'
numberOfLikes: 0
},
{
id: 123456,
articleTitle: 'Lorem ipsum - article two from backend',
articleContent: 'Sky is the limit, the content goes
here.',
numberOfLikes: 0
}
],
recentlyVisitedArticles: [
{
id: 123456,
articleTitle: 'Lorem ipsum - article two from backend',
articleContent: 'Sky is the limit, the content goes
here.',
numberOfLikes: 0
}
]
};
根据前面示例的模型,如果有人喜欢 ID 为123456
的文章,我们需要在两个地方更新模型。这正是$ref
派上用场的地方。
让我们将示例改进为以下内容:
let cache = {
articlesById: {
987654: {
_id: 987654,
articleTitle: 'Lorem ipsum - article one',
articleContent: 'Here goes the content of the article'
numberOfLikes: 0
},
123456: {
_id: 123456,
articleTitle: 'Lorem ipsum - article two from backend',
articleContent: 'Sky is the limit, the content goes
here.',
numberOfLikes: 0
}
},
articles: [
{ $type: 'ref', value: ['articlesById', '987654'] },
{ $type: 'ref', value: ['articlesById', '123456'] }
],
recentlyVisitedArticles: [
{ $type: 'ref', value: ['articlesById', '123456'] }
]
};
在我们新改进的$ref
示例中,您可以在articles
或recentlyVisitedArticles
中找到需要告诉 Falcor 文章 ID 的符号。Falcor 自己将跟随$ref
哨兵,知道我们正在寻找的对象的路由名称(本例中为articlesById
路由)和 ID(在我们的示例中为123456
或987654
。稍后我们将在实践中使用它。
要知道这是它工作原理的简化版本,但要理解$ref
最好的类比是 UNIX 的符号链接。
好吧,这是很多理论——让我们开始编码吧!我们将改进猫鼬模型。
然后我们将前面描述的$ref
哨兵添加到server/routes.js
文件中:
// example of ref, don't write it yet:
let articleRef = $ref(['articlesById', currentMongoID]);
我们还将增加两条 Falcor 航线,articlesById
和articles.add
。在前端,我们将对src/layouts/PublishingApp.js
和src/views/articles/AddArticleView.js
进行一些改进。
让我们开始玩吧。
我们要做的第一件事是在server/configMongoose.js
打开猫鼬模型:
// this is old codebase, you already shall have it:
import mongoose from 'mongoose';
const conf = {
hostname: process.env.MONGO_HOSTNAME || 'localhost',
port: process.env.MONGO_PORT || 27017,
env: process.env.MONGO_ENV || 'local',
};
mongoose.connect(`mongodb://${conf.hostname}:${conf.port}/
${conf.env}`);
const articleSchema = {
articleTitle:String,
articleContent:String
}
我们将对此版本进行改进:
import mongoose from 'mongoose';
const Schema = mongoose.Schema;
const conf = {
hostname: process.env.MONGO_HOSTNAME || 'localhost',
port: process.env.MONGO_PORT || 27017,
env: process.env.MONGO_ENV || 'local',
};
mongoose.connect(`mongodb://${conf.hostname}:${conf.port}/
${conf.env}`);
const articleSchema = new Schema({
articleTitle:String,
articleContent:String,
articleContentJSON: Object
},
{
minimize: false
}
);
在前面的代码中,您会发现我们导入了new const Schema = mongoose.Schema
。后来,我们用articleContentJSON: Object
改进了我们的articleSchema
。这是必需的,因为草稿 js 的状态将保存在 JSON 对象中。如果用户创建一篇文章,将其保存到数据库中,然后希望编辑该文章,这将非常有用。在这种情况下,我们将使用articleContentJSON
来恢复草稿 js 编辑器的内容状态。
第二件事是为{ minimize: false }
提供选项。这是必需的,因为默认情况下 Mongoose 会删除所有空对象,例如{ emptyObject: {}, nonEmptyObject: { test: true } }
,因此如果没有设置minimize: false
,那么我们将在数据库中获得不完整的对象(在这里设置此标志是非常重要的一步)。有些草稿 js 对象是必需的,但默认为空(特别是草稿 js 对象的entityMap
属性)。
在server/routes.js
文件中,我们需要开始使用$ref
哨兵。您在该文件中的导入应如下所示:
import configMongoose from './configMongoose';
import sessionRoutes from './routesSession';
import jsonGraph from 'falcor-json-graph'; // this is new
import jwt from 'jsonwebtoken';
import jwtSecret from './configSecret';
let $ref = jsonGraph.ref; // this is new
let $atom = jsonGraph.atom; // this is new
const Article = configMongoose.Article;
在前面的代码片段中,唯一的新东西是我们从'falcor-json-graph';
导入jsonGraph
,然后添加let $ref = jsonGraph.ref;
和let``$atom = jsonGraph.atom
。
我们在routes.js
范围内增加了$ref
哨兵。我们需要准备一条新路线articlesById[{keys}]["_id","articleTitle","articleContent","articleContentJSON"]
,如下所示:
{
route: 'articlesById[{keys}]["_id","articleTitle",
"articleContent","articleContentJSON"]',
get: function(pathSet) {
let articlesIDs = pathSet[1];
return Article.find({
'_id': { $in: articlesIDs}
}, function(err, articlesDocs) {
return articlesDocs;
}).then ((articlesArrayFromDB) => {
let results = [];
articlesArrayFromDB.map((articleObject) => {
let articleResObj = articleObject.toObject();
let currentIdString = String(articleResObj['_id']);
if (typeof articleResObj.articleContentJSON !==
'undefined') {
articleResObj.articleContentJSON =
$atom(articleResObj.articleContentJSON);
}
results.push({
path: ['articlesById', currentIdString],
value: articleResObj
});
});
return results;
});
}
},
定义了articlesById[{keys}]
路由,键是我们需要在请求中返回的请求 URL 的 ID,正如您在const articlesIDs = pathSet[1];
中看到的。
为了更具体地了解pathSet
,请查看以下示例:
// just an example:
[
{ $type: 'ref', value: ['articlesById', '123456'] },
{ $type: 'ref', value: ['articlesById', '987654'] }
]
在这种情况下,falcor-router
将跟随articlesById
,在pathSet
中,您将得到这个(您可以看到pathSet
的确切值):
['articlesById', ['123456', '987654']]
const articlesIDs = pathSet[1]``;
中articlesIDs
的值可以在这里找到:
['123456', '987654']
正如您稍后将发现的,我们接下来使用这个articlesIDs
值:
// this is already in your codebase:
return Article.find({
'_id': { $in: articlesIDs}
}, function(err, articlesDocs) {
正如您在'_id': { $in: articlesIDs}
中看到的,我们正在传递一个articlesIDs
数组。基于这些 ID,我们将收到 IDs 找到的特定文章的数组(SQLWHERE
等价物)。下一步是迭代收到的文章:
// this already is in your codebase:
articlesArrayFromDB.map((articleObject) => {
将对象推入results
数组:
// this already is in your codebase:
let articleResObj = articleObject.toObject();
let currentIdString = String(articleResObj['_id']);
if (typeof articleResObj.articleContentJSON !== 'undefined') {
articleResObj.articleContentJSON =
$atom(articleResObj.articleContentJSON);
}
results.push({
path: ['articlesById', currentIdString],
value: articleResObj
});
在前面的代码段中几乎没有什么新内容。唯一的新事物是这样的陈述:
// this already is in your codebase:
if (typeof articleResObj.articleContentJSON !== 'undefined') {
articleResObj.articleContentJSON =
$atom(articleResObj.articleContentJSON);
}
我们在这里明确使用来自 Falcor 的$atom
哨兵:$atom(articleResObj.articleContentJSON);
。
$atom
sentinel 是附加到值的元数据,模型必须对其进行不同的处理。使用 Falcor 可以非常简单地返回数字类型的值或字符串类型的值。对 Falcor 来说,返回一个对象更为棘手。为什么?
Falcor 大量使用 JavaScript 的对象和数组,当我们知道一个对象/数组被一个$atom
哨兵(比如我们的例子中的$atom(articleResObj.articleContentJSON
)包裹时,Falcor 知道它不应该深入到该数组/对象中。出于性能方面的考虑,它是通过设计实现的。
性能原因是什么?例如,如果返回一个包含 10000 个非常深的对象的数组而不包装该数组,则可能需要非常、非常长的时间来构建和区分模型。一般来说,出于性能原因,您要通过falcor-router
返回前端的任何对象和数组在返回之前都必须使用$atom
进行包装;否则,您将得到如下错误(如果您不使用$atom
此对象进行包装):
Uncaught MaxRetryExceededError: The allowed number of retries
have been exceeded.
当 Falcor 试图获取那些更深层次的对象时,客户端会显示此错误,而不会事先在后端被$atom
哨兵包裹。
我们现在需要将$ref
哨兵返回到articlesById
,而不是所有文章的详细信息,因此我们需要更改此旧代码:
// this already shall be in your codebase:
{
route:
'articles[{integers}]["_id","articleTitle","articleContent"]',
get: (pathSet) => {
const articlesIndex = pathSet[1];
return Article.find({}, function(err, articlesDocs) {
return articlesDocs;
}).then ((articlesArrayFromDB) => {
let results = [];
articlesIndex.forEach((index) => {
const singleArticleObject =
articlesArrayFromDB[index].toObject();
const falcorSingleArticleResult = {
path: ['articles', index],
value: singleArticleObject
};
results.push(falcorSingleArticleResult);
});
return results;
})
}
}
我们将对此新代码进行改进:
{
route: 'articles[{integers}]',
get: (pathSet) => {
const articlesIndex = pathSet[1];
return Article.find({}, '_id', function(err, articlesDocs) {
return articlesDocs;
}).then ((articlesArrayFromDB) => {
let results = [];
articlesIndex.forEach((index) => {
let currentMongoID =
String(articlesArrayFromDB[index]['_id']);
let articleRef = $ref(['articlesById', currentMongoID]);
const falcorSingleArticleResult = {
path: ['articles', index],
value: articleRef
};
results.push(falcorSingleArticleResult);
});
return results;
})
}
},
发生了什么变化?查看旧代码库中的路由:articles[{integers}]["_id","articleTitle","articleContent"]
。目前,我们的articles[{integers}]
路线没有直接返回for["_id","articleTitle","articleContent"]
数据(在新版本中),因此我们不得不将其删除,以便让 Falcor 了解这一事实(目前articlesById
正在返回详细信息)。
下一件已经改变的事情是,我们创建了一个新的$ref
哨兵,包括以下内容:
// this is already in your codebase:
let currentMongoID = String(articlesArrayFromDB[index]['_id']);
let articleRef = $ref(['articlesById', currentMongoID]);
如您所见,通过这样做,我们通知(使用$ref``falcor-router
,如果前端请求关于article[{integers}]
的任何更多信息,那么falcor-router
应该遵循articlesById
路由,以便从数据库中检索该数据。
在此之后,查看此旧路径的值:
// old version
const singleArticleObject = articlesArrayFromDB[index].toObject();
const falcorSingleArticleResult = {
path: ['articles', index],
value: singleArticleObject
};
您会发现它已被articleRef
的值替换:
// new improved version
let articleRef = $ref(['articlesById', currentMongoID]);
const falcorSingleArticleResult = {
path: ['articles', index],
value: articleRef
};
正如您可能发现的,在旧版本中,我们返回了关于一篇文章的所有信息(变量singleArticleObject
),但在新版本中,我们只返回了$ref
哨兵(articleRef)
。
The $ref
sentinels make falcor-router
automatically follow on the backend, so if there are any refs in the first route, Falcor resolves all the $ref
sentinels until it gets all the pending data; after that, it returns the data in a single request, which saves a lot of latency (instead of performing several HTTP requests, everything followed with $refs
is fetched in one browser-to-backend call).
我们只需要在路由中添加一条新的articles.add
路由:
{
route: 'articles.add',
call: (callPath, args) => {
const newArticleObj = args[0];
var article = new Article(newArticleObj);
return article.save(function (err, data) {
if (err) {
console.info('ERROR', err);
return err;
}
else {
return data;
}
}).then ((data) => {
return Article.count({}, function(err, count) {
}).then((count) => {
return { count, data };
});
}).then ((res) => {
//
// we will add more stuff here in a moment, below
//
return results;
});
}
}
正如您在这里看到的,我们从前端收到了一篇新文章的详细信息,其中有const newArticleObj = args[0];
,随后我们用var article = new Article(newArticleObj);
创建了一个新的Article
模型。之后,article
变量有一个.save
方法,在下面的查询中调用。我们执行两个查询,返回 Mongoose 的承诺。以下是第一点:
return article.save(function (err, data) {
这个.save
方法只是帮助我们将文档插入数据库。保存文章后,我们需要计算数据库中有多少,因此我们运行第二个查询:
return Article.count({}, function(err, count) {
保存文章并计数后,我们返回该信息(return { count, data };
。最后一件事是在falcor-router
的帮助下,将新文章 ID 和计数号从后端返回到前端,因此我们替换此注释:
//
// we will add more stuff here in a moment, below
//
取而代之的是,我们将使用以下新代码来帮助我们实现目标:
let newArticleDetail = res.data.toObject();
let newArticleID = String(newArticleDetail['_id']);
let NewArticleRef = $ref(['articlesById', newArticleID]);
let results = [
{
path: ['articles', res.count-1],
value: NewArticleRef
},
{
path: ['articles', 'newArticleID'],
value: newArticleID
},
{
path: ['articles', 'length'],
value: res.count
}
];
return results;
正如您在前面的代码片段中所看到的,我们在这里获得了newArticleDetail
详细信息。接下来,我们用newArticleID
获取新 ID,并确保它是一个字符串。在所有这些之后,我们用let NewArticleRef = $ref(['articlesById', newArticleID]);
定义了一个新的$ref
哨兵。
在results
变量中,您可以找到三条新路径:
path: ['articles', res.count-1]
:此路径建立了模型,因此我们可以在客户端收到响应后获得 Falcor 模型中的所有信息path: ['articles', 'newArticleID']
:这有助于我们在前端快速获取新 IDpath: ['articles', 'length']
:当然,这会更新我们文章集的长度,因此在我们添加新文章后,前端的 Falcor 模型可以获得最新信息
我们刚刚做了一个添加文章的后端路由。现在让我们开始开发前端,这样我们就可以将所有新文章推送到数据库中。
在src/layouts/PublishingApp.js
文件中,找到以下代码:
get(['articles', {from: 0, to: articlesLength-1}, ['_id','articleTitle', 'articleContent']]).
使用articleContentJSON
将其更改为改进版本:
get(['articles', {from: 0, to: articlesLength-1}, ['_id','articleTitle', 'articleContent', 'articleContentJSON']]).
下一步是改进src/views/articles/AddArticleView.js
中的_submitArticle
功能,增加falcorModel
导入:
// this is old function to replace:
_articleSubmit() {
let newArticle = {
articleTitle: this.state.title,
articleContent: this.state.htmlContent,
articleContentJSON: this.state.contentJSON
}
let newArticleID = 'MOCKEDRandomid' + Math.floor(Math.random() *
10000);
newArticle['_id'] = newArticleID;
this.props.articleActions.pushNewArticle(newArticle);
this.setState({ newArticleID: newArticleID});
}
将此代码替换为以下改进版本:
async _articleSubmit() {
let newArticle = {
articleTitle: this.state.title,
articleContent: this.state.htmlContent,
articleContentJSON: this.state.contentJSON
}
let newArticleID = await falcorModel
.call(
'articles.add',
[newArticle]
).
then((result) => {
return falcorModel.getValue(
['articles', 'newArticleID']
).then((articleID) => {
return articleID;
});
});
newArticle['_id'] = newArticleID;
this.props.articleActions.pushNewArticle(newArticle);
this.setState({ newArticleID: newArticleID});
}
另外,在AddArticleView.js
文件的顶部添加此导入;否则,async_articleSumbit
将不起作用:
import falcorModel from '../../falcorModel.js';
如您所见,我们在函数名(async _articleSubmit()
之前添加了async
关键字。新的要求是:
// this already is in your codebase:
let newArticleID = await falcorModel
.call(
'articles.add',
[newArticle]
).
then((result) => {
return falcorModel.getValue(
['articles', 'newArticleID']
).then((articleID) => {
return articleID;
});
});
在这里,我们等待falcorModel.call
。在.call
参数中,我们添加了newArticle
。然后,在承诺解决后,我们检查newArticleID
与以下内容的关系:
// this already is in your codebase:
return falcorModel.getValue(
['articles', 'newArticleID']
).then((articleID) => {
return articleID;
});
之后,我们只使用与旧版本完全相同的内容:
newArticle['_id'] = newArticleID;
this.props.articleActions.pushNewArticle(newArticle);
this.setState({ newArticleID: newArticleID});
这只是将更新后的带有真实 ID 的newArticle
通过articleActions
从 MongoDB 推送到文章的缩减器中。我们还将setState
与newArticleID
一起使用,这样您就可以看到新文章是使用真实的 Mongo ID 正确创建的。
您应该知道,在每个路由中,我们都返回一个对象或一个对象数组;即使只有一条路线返回,两种方法都可以。举个例子:
// this already is in your codebase (just an example)
{
route: 'articles.length',
get: () => {
return Article.count({}, function(err, count) {
return count;
}).then ((articlesCountInDB) => {
return {
path: ['articles', 'length'],
value: articlesCountInDB
}
})
}
},
这还可以返回包含一个对象的数组,如下所示:
get: () => {
return Article.count({}, function(err, count) {
return count;
}).then ((articlesCountInDB) => {
return [
{
path: ['articles', 'length'],
value: articlesCountInDB
}
]
})
}
如您所见,即使使用一个articles.length
,我们也会返回一个数组(而不是单个对象),这也会起作用。
出于与前面所述相同的原因,这就是为什么在articlesById
中,我们将多条路由推送到阵列中:
// this is already in your codebase
let results = [];
articlesArrayFromDB.map((articleObject) => {
let articleResObj = articleObject.toObject();
let currentIdString = String(articleResObj['_id']);
if (typeof articleResObj.articleContentJSON !== 'undefined') {
articleResObj.articleContentJSON =
$atom(articleResObj.articleContentJSON);
}
// pushing multiple routes
results.push({
path: ['articlesById', currentIdString],
value: articleResObj
});
});
return results; // returning array of routes' objects
这是在《Falcor》一章中值得一提的一件事。
让我们在server/routes.js
文件中创建一个路由,用于更新现有文档(编辑功能):
{
route: 'articles.update',
call: async (callPath, args) =>
{
let updatedArticle = args[0];
let articleID = String(updatedArticle._id);
let article = new Article(updatedArticle);
article.isNew = false;
return article.save(function (err, data) {
if (err) {
console.info('ERROR', err);
return err;
}
}).then ((res) => {
return [
{
path: ['articlesById', articleID],
value: updatedArticle
},
{
path: ['articlesById', articleID],
invalidate: true
}
];
});
}
},
正如您在这里看到的,我们仍然使用类似于articles.add
路线的article.save
方法。需要注意的重要一点是,猫鼬要求isNew
标志为false
(article.isNew = false;
。如果不提供此标志,则会出现类似以下内容的 Mongoose 错误:
{"error":{"name":"MongoError","code":11000,"err":"insertDocument
:: caused by :: 11000 E11000 duplicate key error index:
staging.articles.$_id _ dup key: { :
ObjectId('1515b34ed65022ec234b5c5f') }"}}
代码的其余部分相当简单;我们保存文章的模型,然后通过falcor-router
返回更新后的模型,包括以下内容:
// this is already in your code base:
return [
{
path: ['articlesById', articleID],
value: updatedArticle
},
{
path: ['articlesById', articleID],
invalidate: true
}
];
新事物是invalidate
旗。正如文档中所述,“invalidate 方法同步地从模型缓存中删除多个路径或路径集。”换句话说,您需要告诉前端的 Falcor 模型,["articlesById", articleID]
路径中的某些内容已更改,这样您就可以同步后端和前端的数据。
For more stuff about invalidate
in Falcor, you can go to https://netflix.github.io/falcor/doc/Model.html#invalidate.
为了实现delete
功能,我们需要创建一条新路线:
{
route: 'articles.delete',
call: (callPath, args) =>
{
const toDeleteArticleId = args[0];
return Article.find({ _id: toDeleteArticleId }).
remove((err) => {
if (err) {
console.info('ERROR', err);
return err;
}
}).then((res) => {
return [
{
path: ['articlesById', toDeleteArticleId],
invalidate: true
}
]
});
}
}
这也使用了invalidate
,但这一次,这是我们在这里返回的唯一内容,因为文档已被删除,所以我们需要做的唯一事情是通知浏览器缓存旧文章已无效,并且没有任何内容可以替换,如更新示例中所示。
我们已经在后端实现了update
和delete
路由。接下来,在src/views/articles/EditArticleView.js
文件中,您需要找到以下代码:
// this is old already in your codebase:
_articleEditSubmit() {
let currentArticleID = this.state.editedArticleID;
let editedArticle = {
_id: currentArticleID,
articleTitle: this.state.title,
articleContent: this.state.htmlContent,
articleContentJSON: this.state.contentJSON
}
this.props.articleActions.editArticle(editedArticle);
this.setState({ articleEditSuccess: true });
}
将其替换为此async _articleEditSubmit
功能:
async _articleEditSubmit() {
let currentArticleID = this.state.editedArticleID;
let editedArticle = {
_id: currentArticleID,
articleTitle: this.state.title,
articleContent: this.state.htmlContent,
articleContentJSON: this.state.contentJSON
}
let editResults = await falcorModel
.call(
['articles', 'update'],
[editedArticle]
).
then((result) => {
return result;
});
this.props.articleActions.editArticle(editedArticle);
this.setState({ articleEditSuccess: true });
}
正如您在这里看到的,最重要的是我们在_articleEditSubmit
函数中实现了.call
函数,该函数使用editedArticle
变量发送已编辑对象的详细信息。
在同一文件中,找到_handleDeletion
方法:
// old version
_handleDeletion() {
let articleID = this.state.editedArticleID;
this.props.articleActions.deleteArticle(articleID);
this.setState({
openDelete: false
});
this.props.history.pushState(null, '/dashboard');
}
将其更改为新的改进版本:
async _handleDeletion() {
let articleID = this.state.editedArticleID;
let deletetionResults = await falcorModel
.call(
['articles', 'delete'],
[articleID]
).
then((result) => {
return result;
});
this.props.articleActions.deleteArticle(articleID);
this.setState({
openDelete: false
});
this.props.history.pushState(null, '/dashboard');
}
与删除类似,唯一的区别是我们只发送带有.call
的已删除文章的articleID
。
我们需要实现一种保护所有添加/编辑/删除路由的方法,并且还需要一种通用的干式(不要重复您自己)方法来通知用户后端发生的错误。例如,前端可能出现错误,我们需要在 React 实例的客户端应用中向用户发送错误消息:
- 授权错误:您无权执行该操作
- 超时错误:例如使用外部 API 的服务;我们需要通知用户任何潜在的错误
- 数据不存在:可能有用户会调用我们数据库中不存在的文章 ID 的情况,所以让我们通知他
一般来说,我们现在的目标是创建一种通用方法,将后端的所有潜在错误消息移动到客户端,这样我们就可以改善使用应用的一般体验。
还有$error
sentinel(与 Falcor 相关的变量类型),它通常是返回错误的一种方法。
通常,正如您已经知道的,Falcor 会对请求进行批处理。多亏了它们,您可以在一个 HTTP 请求中从不同的 falcor 路由获取数据。以下示例是您可以一次性获取的内容:
- 一个数据集:完成并准备好检索
- 第二个数据集:第二个数据集,可能包含错误
当第二个数据集中出现错误时,我们不希望影响一个数据集的获取过程(您需要记住,我们示例中的两个数据集是在一个请求中获取的)。
Useful parts from the documentation that may help you understand error handling in Falcor are available here:
https://netflix.github.io/falcor/doc/Model.html#~errorSelector
https://netflix.github.io/falcor/documentation/model.html#error-handling
http://netflix.github.io/falcor/documentation/router.html (search for $error
on this page to find more examples from the documentation)
让我们从 CoreLayout(src/layouts/CoreLayout.js
的改进开始。在AppBar
下,使用以下内容导入新的snackbar
组件:
import AppBar from 'material-ui/lib/app-bar';
import Snackbar from 'material-ui/lib/snackbar';
然后,在导入下的 CoreLayout 外部,创建一个新函数并将其导出:
let errorFuncUtil = (errMsg, errPath) => {
}
export { errorFuncUtil as errorFunc };
然后找到CoreLayout
构造函数,将其更改为在 Falcor$error
哨兵返回错误时,使用名为errorFuncUtil
的导出函数作为基础中的回调:
// old constructor
constructor(props) {
super(props);
}
这是新的:
constructor(props) {
super(props);
this.state = {
errorValue: null
}
if (typeof window !== 'undefined') {
errorFuncUtil = this.handleFalcorErrors.bind(this);
}
}
您可以在这里找到,我们引入了一个新的errorValue
状态(默认状态为null
。然后,仅在前端(由于if(typeof window !== 'undefined')
,我们将this.handleErrors.bind(this)
分配给我们的errorFuncUtil
。
稍后您会发现,这是因为导出的errorFuncUtil
将在我们的falcorModel.js
中导入,在我们的falcorModel.js
中,我们将使用尽可能好的干燥方式将 Falcor 后端发生的任何错误通知我们的 CoreLayout。这一点的好处在于,我们只需实现一次,但这将是一种通用的方式,用于通知客户端应用用户任何错误(这也将节省我们未来的开发工作,因为任何错误都将由我们现在实现的方法来处理)。
我们需要在 CoreLayout 中添加一个名为handleFalcorErrors
的新功能:
handleFalcorErrors(errMsg, errPath) {
let errorValue = `Error: ${errMsg} (path ${JSON.stringify(errPath)})`
this.setState({errorValue});
}
handleFalcorErrors
功能设置错误的新状态。我们将使用errMsg
(我们在后端创建此错误,稍后您将了解到)和errPath
(可选,但这是发生错误的falcor-route
路径)为用户合成错误。
好的,我们已经准备好了一切;CoreLayout
函数中唯一缺少的是改进的渲染。CoreLayout 的新渲染如下所示:
render () {
let errorSnackbarJSX = null;
if (this.state.errorValue) {
errorSnackbarJSX = <Snackbar
open={true}
message={this.state.errorValue}
autoHideDuration={8000}
onRequestClose={ () => console.log('You can add custom
onClose code') } />;
}
const buttonStyle = {
margin: 5
};
const homeIconStyle = {
margin: 5,
paddingTop: 5
};
let menuLinksJSX;
let userIsLoggedIn = typeof localStorage !== 'undefined' &&
localStorage.token && this.props.routes[1].name !== 'logout';
if (userIsLoggedIn) {
menuLinksJSX = (
<span>
<Link to='/dashboard'>
<RaisedButton label='Dashboard' style={buttonStyle} />
</Link>
<Link to='/logout'>
<RaisedButton label='Logout' style={buttonStyle} />
</Link>
</span>);
} else {
menuLinksJSX = (
<span>
<Link to='/register'>
<RaisedButton label='Register' style={buttonStyle} />
</Link>
<Link to='/login'>
<RaisedButton label='Login' style={buttonStyle} />
</Link>
</span>);
}
let homePageButtonJSX = (
<Link to='/'>
<RaisedButton label={<ActionHome />}
style={homeIconStyle} />
</Link>);
return (
<div>
{errorSnackbarJSX}
<AppBar
title='Publishing App'
iconElementLeft={homePageButtonJSX}
iconElementRight={menuLinksJSX} />
<br/>
{this.props.children}
</div>
);
}
如您所见,新部件与物料 UIsnackbar
组件相关。看看这个:
let errorSnackbarJSX = null;
if (this.state.errorValue) {
errorSnackbarJSX = <Snackbar
open={true}
message={this.state.errorValue}
autoHideDuration={8000} />;
}
此代码片段正在准备我们的erroSnackbarJSX
和以下内容:
<div>
{errorSnackbarJSX}
<AppBar
title='Publishing App'
iconElementLeft={homePageButtonJSX}
iconElementRight={menuLinksJSX} />
<br/>
{this.props.children}
</div>
确保{errorSnackbarJSX}
的放置方式与本例完全相同。否则,您可能会在应用的测试运行期间发现一些问题。您现在已经完成了与 CoreLayout 改进相关的所有工作。
在src/falcorModel.js
文件中,识别以下代码:
// already in your codebase, old code:
import falcor from 'falcor';
import FalcorDataSource from 'falcor-http-datasource';
class PublishingAppDataSource extends FalcorDataSource {
onBeforeRequest ( config ) {
const token = localStorage.token;
const username = localStorage.username;
const role = localStorage.role;
if (token && username && role) {
config.headers['token'] = token;
config.headers['username'] = username;
config.headers['role'] = role;
}
}
}
const model = new falcor.Model({
source: new PublishingAppDataSource('/model.json')
});
export default model;
必须通过在falcor.Model
中添加新选项来改进此代码:
import falcor from 'falcor';
import FalcorDataSource from 'falcor-http-datasource';
import {errorFunc} from './layouts/CoreLayout';
class PublishingAppDataSource extends FalcorDataSource {
onBeforeRequest ( config ) {
const token = localStorage.token;
const username = localStorage.username;
const role = localStorage.role;
if (token && username && role) {
config.headers['token'] = token;
config.headers['username'] = username;
config.headers['role'] = role;
}
}
}
let falcorOptions = {
source: new PublishingAppDataSource('/model.json'),
errorSelector: function(path, error) {
errorFunc(error.value, path);
error.$expires = -1000 * 60 * 2;
return error;
}
};
const model = new falcor.Model(falcorOptions);
export default model;
我们添加的第一件事是将errorFunc
导入到该文件的顶部:
import {errorFunc} from './layouts/CoreLayout';
除了errorFunc
之外,我们还引入了falcorOptions
变量。源代码与上一版本中的相同。我们增加了errorSelector
,每次客户端调用后端时都会运行errorSelector
,后端的falcor-router
返回$error
哨兵。
有关错误选择器的更多详细信息,请参见https://netflix.github.io/falcor/documentation/model.html#the-错误选择器值。
我们将分两步执行后端实现:
- 一个错误示例,仅用于测试客户端代码。
- 在确定错误处理工作正常后,我们将正确保护端点。
让我们从server/routes.js
文件中的导入开始:
import configMongoose from './configMongoose';
import sessionRoutes from './routesSession';
import jsonGraph from 'falcor-json-graph';
import jwt from 'jsonwebtoken';
import jwtSecret from './configSecret';
let $ref = jsonGraph.ref;
let $atom = jsonGraph.atom;
let $error = jsonGraph.error;
const Article = configMongoose.Article;
唯一的新功能是您需要从falcor-json-graph
导入$error
哨兵。
The goal of our $error
test is to replace a working route that is responsible for fetching articles (articles[{integers}])
. After we break this route, we will be able to test whether our frontend and backend setup is working. After we test the errors (refer to the next screenshot), we will delete this breaking $error
code from articles[{integers}]
. Read on for details.
用article
路线进行测试:
{
route: 'articles[{integers}]',
get: (pathSet) => {
const articlesIndex = pathSet[1];
return {
path: ['articles'],
value: $error('auth error')
}
return Article.find({}, '_id', function(err, articlesDocs) {
return articlesDocs;
}).then ((articlesArrayFromDB) => {
let results = [];
articlesIndex.forEach((index) => {
let currentMongoID =
String(articlesArrayFromDB[index]['_id']);
let articleRef = $ref(['articlesById', currentMongoID]);
const falcorSingleArticleResult = {
path: ['articles', index],
value: articleRef
};
results.push(falcorSingleArticleResult);
});
return results;
})
}
},
正如你所看到的,这只是一个测试。我们稍后将改进这段代码,但让我们测试一下$error('auth error')
sentinel 中的文本是否会显示给用户。
运行 MongoDB:
$ mongod
然后,在另一个终端中运行服务器:
$ npm start
运行这两个命令后,将浏览器指向http://localhost:3000
,您将在 8 秒钟内看到此错误:
如您所见,窗口底部的黑色背景上有白色文本:
如果你运行应用,并且在主页上看到屏幕截图上的错误消息,那么它会告诉你你很好!
确定错误处理对您有效后,可以替换旧代码:
{
route: 'articles[{integers}]',
get: (pathSet) => {
const articlesIndex = pathSet[1];
return {
path: ['articles'],
value: $error('auth error')
}
return Article.find({}, '_id', function(err, articlesDocs) {
将其更改为以下内容,但不返回错误:
{
route: 'articles[{integers}]',
get: (pathSet) => {
const articlesIndex = pathSet[1];
return Article.find({}, '_id', function(err, articlesDocs) {
现在,当您尝试从后端获取文章时,应用将开始正常工作,而不会抛出错误。
我们已经在server/routes.js
中实现了一些检查用户是否被授权的逻辑,如下所示:
// this already is in your codebase:
export default ( req, res ) => {
let { token, role, username } = req.headers;
let userDetailsToHash = username+role;
let authSignToken = jwt.sign(userDetailsToHash, jwtSecret.secret);
let isAuthorized = authSignToken === token;
let sessionObject = {isAuthorized, role, username};
console.info(`The ${username} is authorized === `, isAuthorized);
在这段代码中,您会发现我们可以在每个需要授权的角色和编辑器角色的开头创建以下逻辑:
// this is example of falcor-router $errors, don't write it:
if (isAuthorized === false) {
return {
path: ['HERE_GOES_THE_REAL_FALCOR_PATH'],
value: $error('auth error')
}
} elseif(role !== 'editor') {
return {
path: ['HERE_GOES_THE_REAL_FALCOR_PATH'],
value: $error('you must be an editor in order
to perform this action')
}
}
正如您在这里看到的,这只是一个示例(暂时不要更改它;我们将在稍后实现它),带有path['HERE_GOES_THE_REAL_FALCOR_PATH']
。
首先,我们使用isAuthorized === false
检查用户是否被授权;如果未经授权,他将看到一个错误(使用我们刚刚实现的通用错误机制):
将来,我们的发布应用中可能会有更多角色,因此如果有人不是编辑,他将在错误中看到以下内容:
对于我们的申请条款中要求授权的路线(server/routes.js
,添加以下内容:
route: 'articles.add',
以下是旧代码:
// this is already in your codebase, old code:
{
route: 'articles.add',
call: (callPath, args) => {
const newArticleObj = args[0];
var article = new Article(newArticleObj);
return article.save(function (err, data) {
if (err) {
console.info('ERROR', err);
return err;
}
else {
return data;
}
}).then ((data) => {
// code has been striped out from here for the sake of brevity,
nothing changes below
带auth
检查的新代码如下:
{
route: 'articles.add',
call: (callPath, args) => {
if (sessionObject.isAuthorized === false) {
return {
path: ['articles'],
value: $error('auth error')
}
} else if(sessionObject.role !== 'editor' &&
sessionObject.role !== 'admin') {
return {
path: ['articles'],
value: $error('you must be an editor
in order to perform this action')
}
}
const newArticleObj = args[0];
var article = new Article(newArticleObj);
return article.save(function (err, data) {
if (err) {
console.info('ERROR', err);
return err;
}
else {
return data;
}
}).then ((data) => {
// code has been striped out from here for
//the sake of brevity, nothing changes below
您可以在这里找到,我们添加了两个带有isAuthorized === false
和!== 'editor'
角色的检查。以下路由内容将几乎相同(只是路径略有更改)。
以下是articles
更新:
route: 'articles.update',
这是旧代码:
// this is already in your codebase, old code:
{
route: 'articles.update',
call: async (callPath, args) =>
{
const updatedArticle = args[0];
let articleID = String(updatedArticle._id);
let article = new Article(updatedArticle);
article.isNew = false;
return article.save(function (err, data) {
if (err) {
console.info('ERROR', err);
return err;
}
}).then ((res) => {
// code has been striped out from here for the
//sake of brevity, nothing changes below
带有auth
检查的新代码如下:
{
route: 'articles.update',
call: async (callPath, args) =>
{
if (sessionObject.isAuthorized === false) {
return {
path: ['articles'],
value: $error('auth error')
}
} else if(sessionObject.role !== 'editor' &&
sessionObject.role !== 'admin') {
return {
path: ['articles'],
value: $error('you must be an editor
in order to perform this action')
}
}
const updatedArticle = args[0];
let articleID = String(updatedArticle._id);
let article = new Article(updatedArticle);
article.isNew = false;
return article.save(function (err, data) {
if (err) {
console.info('ERROR', err);
return err;
}
}).then ((res) => {
// code has been striped out from here
//for the sake of brevity, nothing changes below
articles delete:
route: 'articles.delete',
查找此旧代码:
// this is already in your codebase, old code:
{
route: 'articles.delete',
call: (callPath, args) =>
{
let toDeleteArticleId = args[0];
return Article.find({ _id: toDeleteArticleId }).remove((err) => {
if (err) {
console.info('ERROR', err);
return err;
}
}).then((res) => {
// code has been striped out from here
//for the sake of brevity, nothing changes below
用新代码替换为auth
检查:
{
route: 'articles.delete',
call: (callPath, args) =>
{
if (sessionObject.isAuthorized === false) {
return {
path: ['articles'],
value: $error('auth error')
}
} else if(sessionObject.role !== 'editor' &&
sessionObject.role !== 'admin') {
return {
path: ['articles'],
value: $error('you must be an
editor in order to perform this action')
}
}
let toDeleteArticleId = args[0];
return Article.find({ _id: toDeleteArticleId }).remove((err) => {
if (err) {
console.info('ERROR', err);
return err;
}
}).then((res) => {
// code has been striped out from here
//for the sake of brevity, nothing below changes
正如您所看到的,返回几乎相同——我们可以降低代码重复。我们可以为它们创建一个 helper 函数,这样代码就更少了,但是您需要记住,您需要设置一个与返回错误时请求的路径类似的路径。例如,如果您在articles.update
上,则需要在文章路径中返回一个错误(或者如果您在XYZ.update
上,则错误转到XYZ
路径)。
在下一章中,我们将实现 AWSS3,以便能够上传文章的封面照片。除此之外,我们还将使用新功能改进发布应用。