翻译 | 《JavaScript Everywhere》第9章 详细信息(^_^)

翻译 | 《JavaScript Everywhere》第9章 详细信息(^_^)

写在最前面

大家好呀,我是毛小悠,是一位前端开发工程师。正在翻译一本英文技术书籍。

为了提高大家的阅读体验,对语句的结构和内容略有调整。如果发现本文中有存在瑕疵的地方,或者你有任何意见或者建议,可以在评论区留言,或者加我的微信:code\_maomao,欢迎相互沟通交流学习。

(σ゚∀゚)σ..:\*☆哎哟不错哦

第9章 详细信息

当现在普遍存在的空气清新剂Febreze首次发布时,它就像是一个哑巴。

原始的广告只显示人们使用该产品可以去除特定的难闻气味,例如香烟烟雾,导致销售不佳。面对令人失望的结果,营销团队将重点转移到使用Febreze作为完美的细节。现在,这些广告描绘了有人打扫房间,松软枕头并以Febreze兴奋地完成新鲜房间的过程。产品的这种重新设计导致销量猛增。

这是细节很重要的一个很好的例子。

现在我们有一个可以正常使用的API,但是它缺少使我们能够投入生产的画龙点睛的功能。

在本章中,我们将实现一些WebGraphQL应用程序安全性以及用户体验的最佳实践。

这些细节远远超出了空气清新剂的喷洒范围,对于我们应用程序的安全性,安全性和可用性至关重要。

Web应用程序和Express.js最佳实践

Express.js是支持我们API的底层Web应用程序框架。我们可以对Express.js代码进行一些小调整,为我们的应用程序提供坚实的基础。

Express Helmet

the ExpressHelmet中间件是小型安全性中间件功能的集合。这些将调整我们应用程序的HTTP头,以提高安全性。尽管其中许多基于浏览器的应用程序,但是启用Helmet是保护我们的应用程序不受常见Web漏洞影响的简单步骤。

要启用Helmet,我们需要在应用程序中使用中间件,并指示Express在尽早我们的中间件堆栈中使用它。在./src/index.js文件中,添加以下内容:

// first require the package at the top of the file
const helmet = require('helmet')

// add the middleware at the top of the stack, after const app = express()
app.use(helmet()); 

通过添加Helmet中间件,我们迅速为我们的应用程序启用了常见的Web安全最佳实践。

跨域资源共享

跨域资源共享(CORS)是我们允许从另一个域请求资源的方法。由于我们的APIUI代码将分别存在,因此我们希望启用其他来源的使用。如果你有兴趣了解CORS的来龙去脉,我强烈建议你Mozilla CORS指南。

要启用CORS,我们将在.src/index.js文件中使用Express.jsCORS中间件软件包:

// first require the package at the top of the file
const cors = require('cors');

// add the middleware after app.use(helmet());
app.use(cors()); 

通过以这种方式添加中间件,我们启用了来自所有域的跨域请求。目前,这对我们来说很好,因为我们处于开发模式,并且很可能会使用托管服务提供商生成的域,但是通过使用中间件,我们也可以将请求限制为特定来源。

Pagination分页

当前,我们的笔记查询和用户查询都返回了数据库中全部列表的笔记和用户。

这对于本地开发而言很好用,但是随着我们应用程序的增长,变得难以为继,因为查询可能返回多个(或数千个)笔记的查询非常昂贵,并且会降低数据库、服务器和网络的速度。相反,我们可以对这些查询进行分页,仅返回一定数量的结果。我们可以实现两种常见的分页类型。

第一种偏移分页,由客户端传递偏移号并返回有限数量的数据来工作。

例如,如果每页数据限制为10条记录,而我们想请求第三页数据,则可以传递20的偏移量。虽然从概念上讲这是最直接的方法,但它可能会遇到扩展和性能问题。

第二种分页是基于游标的分页,其中将基于时间的游标或唯一标识符作为起点传递。然后,我们要求遵循此记录的特定数量的数据。这种方法使我们可以最大程度地控制分页。另外,由于Mongo的对象ID是有序的(它们以4字节的时间值开头),因此我们可以轻松地将其用作光标。要了解有关Mongo对象ID的更多信息,建议阅读相应的MongoDB文档。

如果听不懂这个概念,那没关系。让我们逐步实现将笔记的分页作为一个GraphQL查询。首先,让我们定义将要创建的内容,然后是更新模式,最后是解析器代码。对于我们的需求,我们要查询我们的API,同时可以选择将游标作为参数传递。然后,API应该返回有限数量的数据,表示数据集中最后一个的光标点以及如果要查询的另一页数据的布尔值。

通过此描述,我们可以更新src/schema.js文件来定义此新查询。首先,我们需要在文件中添加一个NoteFeed类型:

type NoteFeed {
  notes: [Note]!
  cursor: String!
  hasNextPage: Boolean!
} 

接下来,我们将添加我们的noteFeed查询:

type Query {
  # add noteFeed to our existing queries
  noteFeed(cursor: String): NoteFeed
} 

更新结构后,我们可以编写我们查询的解析器代码。在./src/resolvers/query.js中,将以下内容添加到导出的对象中:

noteFeed: async (parent, { cursor }, { models }) => {
  // hardcode the limit to 10 items
  const limit = 10;
  // set the default hasNextPage value to false
  let hasNextPage = false;
  // if no cursor is passed the default query will be empty
  // this will pull the newest notes from the db
  let cursorQuery = {};

  // if there is a cursor
  // our query will look for notes with an ObjectId less than that of the cursor
  if (cursor) {
   cursorQuery = { _id: { $lt: cursor } };
  }

  // find the limit + 1 of notes in our db, sorted newest to oldest
  let notes = await models.Note.find(cursorQuery)
   .sort({ _id: -1 })
   .limit(limit + 1);

  // if the number of notes we find exceeds our limit
  // set hasNextPage to true and trim the notes to the limit
  if (notes.length > limit) {
   hasNextPage = true;
   notes = notes.slice(0, -1);
  }

  // the new cursor will be the Mongo object ID of the last item in the feed array
  const newCursor = notes[notes.length - 1]._id;

  return {
   notes,
   cursor: newCursor,
   hasNextPage
  };
} 

使用此解析器后,我们可以查询我们的noteFeed,最多返回10个结果。在GraphQL Playground中,我们可以编写以下查询来接收笔记,它们的对象ID,它们的“创建于”时间戳,光标和下一页布尔值的列表:

query {
  noteFeed {
   notes {
    id
    createdAt
   }
   cursor
   hasNextPage
  }
} 

由于我们的数据库中有10个以上的笔记,因此它返回一个游标以及hasNextPagetrue。使用该光标,我们可以查询提要的第二页:

query {
  noteFeed(cursor: "<YOUR OBJECT ID>") {
   notes {
    id
    createdAt
   }
   cursor
   hasNextPage
  }
} 

我们可以继续对hasNextPage值为true的每个游标执行此操作。有了这个实现,我们就创建了一个分页的笔记块。这不仅使我们的UI可以请求特定的数据块,还可以减轻服务器和数据库的负担。

数据限制

除了建立分页之外,我们还要限制可以通过我们的API请求的数据量。这样可以防止查询使我们的服务器或数据库超载。

此过程中的第一步很简单,就是限制查询可以返回的数据量。我们的两个查询,usernotes,从数据库返回所有匹配的数据。我们可以通过设置一个我们的数据库查询的limit()方法。例如,在我们的.src/resolvers/query.js文件中,我们可以按以下方式更新笔记查询:

notes: async (parent, args, { models }) => {
  return await models.Note.find().limit(100);
}

虽然限制数据是一个很好的开端,但目前我们的查询可以无限深度地编写。这意味着可以编写单个查询来检索笔记列表、每个笔记的作者信息、每个作者的收藏夹列表、每个收藏夹的作者信息,等等。一个查询中有很多数据,我们继续编写!为了防止这些类型的过度查询,我们可以根据API限制查询的深度。

此外,我们可能有一些复杂的查询,这些查询没有过多嵌套,但仍然需要大量计算才能返回数据。我们可以通过限制查询的复杂性来防止这类请求。

我们可以通过使用./src/index.js文件中的graphql-depth-limitgraphql-validation-complexity包来实现这些限制:

// import the modules at the top of the file
const depthLimit = require('graphql-depth-limit');
const { createComplexityLimitRule } = require('graphql-validation-complexity');

// update our ApolloServer code to include validationRules
const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [depthLimit(5), createComplexityLimitRule(1000)],
  context: async ({ req }) => {
    // get the user token from the headers
    const token = req.headers.authorization;
    // try to retrieve a user with the token
    const user = await getUser(token);
    // add the db models and the user to the context
    return { models, user };
  }
}); 

通过添加这些软件包,我们为API添加了额外的查询保护。有关保护GraphQL API免受恶意查询的更多信息,请查看Spectrum首席技术官Max Stoiber的精彩文章。

其他注意事项

构建我们的API后,你应该对GraphQL开发的基础知识有扎实的了解。如果你想深入了解这些主题,那么接下来可以去测试,GraphQL订阅和Apollo Engine就是一些不错的选择。

测验

好吧,我承认:我没有为本书写测试而感到内疚。测试我们的代码很重要,因为它使我们能够轻松进行更改并改善与其他开发人员的协作。GraphQL设置的一大优点是,解析器只是简单的函数,需要一些参数并返回数据。这使我们的GraphQL逻辑易于测试。

订阅内容

订阅是GraphQL强大的功能,它提供了一种直接的方式来将发布-订阅模式集成到我们的应用程序中。这意味着在服务器上发布数据时,UI可以订阅通知或更新。这使GraphQL服务器成为处理实时数据的应用程序的理想解决方案。有关GraphQL订阅的更多信息,请查看Apollo服务器文档。

Apollo GraphQL平台

在开发API的整个过程中,我们一直在使用Apollo GraphQL库。在以后的章节中,我们还将使用Apollo客户端库与API进行接口。我之所以选择这些库是因为它们是行业标准,并且为使用GraphQL提供了出色的开发人员经验。如果你将应用程序投入生产,则维护这些库的公司Apollo还将提供一个平台,该平台提供对GraphQL API的监视和工具。你可以在以下处了解更多信息ApolloApollo)的网站。

结论

在本章中,我们为我们的应用程序添加了一些收尾工作。虽然我们可以实现许多其他的选择,但在这一点上,我们已经开发了一个可靠的MVP(最小可行产品)。在这种状态下,我们准备好启动我们的API

在下一章中,我们将把我们的API部署到一个公共web服务器上。

如果有理解不到位的地方,欢迎大家纠错。如果觉得还可以,麻烦您点赞收藏或者分享一下,希望可以帮到更多人。