Keystone 简介

Keystone 是一个可扩展的无头 CMS,用于在 Node.js 中构建 GraphQL API。它是开源的,并配备了一个非常不错的管理 UI,你可以在其中管理你的内容。就像 WordPress 一样,你可以在 Keystone 中创建称为列表的自定义内容类型,然后通过 GraphQL API 查询你的内容。你从源代码创建列表,就像你创建 REST API 一样。你添加 API 所需的内容,使其具有高度的可扩展性和可扩展性。

要使用 Keystone,首先,你需要准备一个用于存储内容的数据库。Keystone 支持 MongoDB 和 PostgreSQL。你需要安装和配置其中一个,然后找到 Keystone 的连接字符串。你在第 9 章“添加服务器端数据库”中学习了 MongoDB,因此再次将其用作 Keystone 的数据库对你来说应该不是问题。但是 PostgreSQL 呢?让我们来看看。

有关 Keystone 的更多信息,请访问 https://www.keystonejs.com/。

安装和保护 PostgreSQL (Ubuntu)

PostgreSQL,也称为 Postgres,是一个对象关系数据库系统,常与 MySQL 进行比较,后者是一个(纯粹的)关系数据库管理系统 (RDBMS)。两者都是开源的并且使用表,但它们之间存在差异。

例如,Postgres 在很大程度上符合 SQL 标准,而 MySQL 只是部分符合;MySQL 在读取速度方面性能更快,而 PostgreSQL 在处理复杂的查询方面更快。有关 Postgres 的更多信息,请访问 https://www.postgresql.org/。

你可以在许多不同的操作系统上安装 Postgres,包括 Linux、macOS 和 Windows。根据你的操作系统,你可以按照 https://www.postgresql.org/download/ 上的官方指南在你的机器上安装它。以下步骤将向你展示如何在 Linux(特别是 Ubuntu)上安装和保护它:

  1. 更新你的本地软件包索引,并使用 Ubuntu 的 apt 软件包管理系统从 Ubuntu 的默认存储库安装 Postgres:

    $ sudo apt update
    $ sudo apt install postgresql postgresql-contrib
  2. 通过检查其版本来验证 Postgres:

    $ psql -v

    如果你得到以下输出,则表示你已成功安装:

    /usr/lib/postgresql/12/bin/psql: option requires an argument -- 'v'
    Try "psql --help" for more information.

    路径中的数字 12 表示你的机器上安装的是 Postgres 版本 12。

  3. 从你的终端进入 Postgres shell:

    $ sudo -u postgres psql

    你应该在终端上看到类似于以下的输出:

    postgres@lau-desktop:~$ psql
    psql (12.2 (Ubuntu 12.2-2.pgdg19.10+1))
    Type "help" for help.
    postgres=
  4. 使用 Postgres 的 \du 命令列出默认用户:

    postgres= \du

    你应该看到两个默认用户,如下所示:

    Role name
    -----------
     postgres
     root

    我们将使用终端上的交互式提示符向列表中添加一个新的管理用户(或角色)。但是,我们需要先退出 Postgres shell:

    postgres= \q
  5. 输入以下带有 --interactive 标志的命令:

    $ sudo -u postgres createuser --interactive

    你应该看到以下两个关于新角色名称以及该角色是否应具有超级用户权限的问题:

    Enter name of role to add: user1
    Shall the new role be a superuser? (y/n) y

    在这里,我们将新用户命名为 user1。它具有与默认用户相同的超级用户权限。

  6. 使用 sudo -u postgres psql 登录 Postgres shell,并通过 \du 命令验证新用户。你应该看到它已添加到列表中。

  7. 使用以下 SQL 查询为新用户添加密码:

    ALTER USER user1 PASSWORD 'password';

    如果你得到以下输出,则表示你已成功为此用户添加密码:

    ALTER ROLE
  8. 退出 Postgres shell。现在,你可以使用 PHP 的 Adminer (https://www.adminer.org/) 使用此用户登录 Postgres,并从那里添加一个新数据库,该数据库将在你稍后安装 Keystone 时需要。然后,你可以为刚刚创建的数据库使用以下格式的 Postgres 连接字符串:

    postgres://<username>:<password>@localhost/<dbname>

请注意,出于安全原因,任何用户从 Adminer 登录数据库始终需要密码。因此,无论它是 MySQL、Postgres 还是 MongoDB 数据库,为你的数据库添加安全性(尤其是在生产环境中)都是一个很好的做法。那么 MongoDB 呢?你在前面的章节中学习了如何安装和使用它,但它尚未受到保护。我们将在下一节中了解如何做到这一点。

安装和保护 MongoDB (Ubuntu)

到目前为止,你应该知道如何安装 MongoDB。因此,在本节中,我们将重点介绍如何保护 MongoDB 中的数据库。要保护 MongoDB,我们将首先向 MongoDB 添加一个管理用户,如下所示:

  1. 从终端连接到 Mongo shell:

    $ mongo
  2. 选择 admin 数据库,并向该数据库添加一个具有用户名和密码(例如,root 和 password)的新用户,如下所示:

    > use admin
    > db.createUser(
    {
      user: "root",
      pwd: "password",
      roles: [ { role: "userAdminAnyDatabase", db: "admin" },
               "readWriteAnyDatabase" ]
    }
    )
  3. 退出 shell,并从终端打开 MongoDB 配置文件:

    $ sudo nano /etc/mongod.conf
  4. 找到 security 部分,删除注释符号 #,并添加 authorization 设置,如下所示:

    # mongodb.conf
    security:
      authorization: "enabled"
  5. 保存并退出文件,然后重启 MongoDB:

    $ sudo systemctl restart mongod
  6. 通过检查 MongoDB 的状态来验证配置:

    $ sudo systemctl status mongod

    如果看到 “active” 状态,则表示你已正确配置。

  7. 使用用户名 “root”、密码以及 --authenticationDatabase 选项登录。此外,提供存储用户的数据库名称,在本例中为 “admin”:

    $ mongo --port 27017 -u "root" -p "password" --authenticationDatabase "admin"
  8. 创建一个新数据库(例如,test),并向其附加一个新用户:

    > use test
    db.createUser(
    {
      user: "user1",
      pwd: "password",
      roles: [ { role: "readWrite", db: "test" } ]
    }
    )
  9. 退出并以 user1 身份登录来测试数据库:

    $ mongo --port 27017 -u "user1" -p "password" --authenticationDatabase "test"
  10. 测试你是否可以访问此 test 数据库但不能访问其他数据库:

    > show dbs

    如果没有任何输出,则表示在身份验证后你仅被授权访问此数据库。你可以使用以下格式为 Keystone 或任何其他应用程序(例如,Express、Koa 等)提供 MongoDB 连接字符串:

    mogodb://<username>:<password>@localhost:27017/<dbname>

同样,为数据库添加安全性(尤其是在生产环境中)是一个很好的做法,但是对于本地开发而言,禁用身份验证可以更轻松、更快速地开发应用程序。你始终可以在本地开发中禁用它,并在生产服务器中启用它。

现在,两个数据库系统(PostgresMongoDB)都已准备就绪,你可以选择其中任何一个来构建你的 Keystone 应用程序。那么,开始吧!

安装和创建 Keystone 应用

有两种启动 Keystone 项目的方法——从头开始或使用称为 keystone-app 的 Keystone 脚手架工具。如果你打算从头开始,你需要手动安装任何与 Keystone 相关的软件包。这包括最低要求的 Keystone 软件包和你构建应用程序所需的其他 Keystone 软件包。让我们看一下这个手动安装:

  1. 创建一个项目目录并安装最低要求的软件包——Keystone 软件包本身、Keystone GraphQL 软件包(在 Keystone 中被视为一个应用程序)和一个数据库适配器:

    $ npm i @keystonejs/keystone
    $ npm i @keystonejs/app-graphql
    $ npm i @keystonejs/adapter-mongoose
  2. 安装你需要的其他 Keystone 软件包,例如 Keystone Admin UI 软件包(在 Keystone 中被视为一个应用程序)和用于注册列表的 Keystone 字段软件包:

    $ npm i @keystonejs/app-admin-ui
    $ npm i @keystonejs/fields
  3. 在你的根目录中创建一个空的 index.js 文件,并导入你刚刚安装的软件包:

    // index.js
    const { Keystone } = require('@keystonejs/keystone')
    const { GraphQLApp } = require('@keystonejs/app-graphql')
    const { AdminUIApp } = require('@keystonejs/app-admin-ui')
    const { MongooseAdapter } = require('@keystonejs/adapter-mongoose')
    const { Text } = require('@keystonejs/fields')
  4. 创建一个新的 Keystone 实例,并将数据库适配器的新实例传递给它,如下所示:

    const keystone = new Keystone({
      name: 'My Keystone Project',
      adapter: new MongooseAdapter({ mongoUri:
        'mongodb://localhost/yourdb-name' }),
    })

    查看以下指南以了解如何配置 Mongoose 适配器:https://www.keystonejs.com/keystonejs/adapter-mongoose/。当我们使用脚手架工具安装 Keystone 时,我们将再次介绍这一点。

  5. 创建一个简单的列表——例如 Page 列表——并定义你需要用来存储此列表中每个项目数据的字段:

    keystone.createList('Page', {
      fields: {
        name: { type: Text },
      },
    })

    按照约定,GraphQL 的列表名称首字母大写。我们很快将介绍这一点。

  6. 导出 keystone 实例和应用程序,以便它们可以执行:

    module.exports = {
      keystone,
      apps: [new GraphQLApp(), new AdminUIApp()]
    }
  7. 创建一个 package.json 文件(如果尚未创建),并将以下 keystone 命令添加到 scripts 中,如下所示:

    "scripts": {
      "dev": "keystone"
    }
  8. 通过在终端上运行 dev 脚本来启动应用程序:

    $ npm run dev

    你应该在终端上看到以下输出。这意味着你已成功启动应用程序:

    Command: keystone dev
    ✓ Validated project entry file ./index.js
    ✓ Keystone server listening on port 3000
    ✓ Initialised Keystone instance
    ✓ Connected to database
    ✓ Keystone instance is ready at http://localhost:3000
    ∞ Keystone Admin UI: http://localhost:3000/admin
    ∞ GraphQL Playground: http://localhost:3000/admin/graphiql
    ∞ GraphQL API: http://localhost:3000/admin/api

做得好!你的第一个也是最简单的 Keystone 应用程序已启动并运行。在这个应用程序中,你在 localhost:3000/admin/api 有一个 GraphQL API,在 localhost:3000/admin/graphiql 有一个 GraphQL Playground,在 localhost:3000/admin 有一个 Keystone Admin UI。但是我们如何使用 GraphQL API 和 GraphQL Playground 呢?请放心,我们将在接下来的部分中介绍这一点。

启动一个新的 Keystone 应用程序一点也不难,不是吗?你只需要安装 Keystone 所需的和你需要的东西。然而,启动 Keystone 应用程序最简单的方法是使用脚手架工具。使用脚手架工具的好处是,它在安装过程中附带了一些可选的 Keystone 应用程序示例,这些示例可以作为非常有用的指南和模板。这些可选示例如下:

  • Starter: 此示例演示了使用 Keystone 的基本用户身份验证。

  • Todo: 此示例演示了一个简单的应用程序,用于向 Todo 列表添加项目,以及一些前端集成(HTML、CSS 和 JavaScript)。

  • Blank: 此示例提供了一个基本的起点,以及 Keystone Admin UI、GraphQL API 和 GraphQL Playground。这些与手动安装中的类似,但没有 Keystone 字段软件包。

  • Nuxt: 此示例演示了与 Nuxt.js 的简单集成。

我们将选择空白选项,因为它提供了我们需要的基本软件包,以便我们可以在其基础上构建我们的列表。让我们看一下:

  1. 在你的终端上使用任何名称创建一个新的 Keystone 应用程序:

    $ npm init keystone-app <app-name>
  2. 回答 Keystone 提出的问题,如下所示:

    ✓ What is your project name?
    ✓ Select a starter project: Starter / Blank / Todo / Nuxt
    ✓ Select a database type: MongoDB / Postgre
  3. 安装完成后,进入你的项目目录:

    $ cd <app-name>
  4. 如果你使用的是受保护的 Postgres,只需提供连接字符串以及 Keystone 的用户名、密码和数据库:

    // index.js
    const adapterConfig = { knexOptions: { connection: 'postgres://
    <username>:<password>@localhost/<dbname>' } }

    请注意,如果没有启用身份验证,你只需从字符串中删除 <username>:<password>@。然后,运行以下命令来安装数据库表:

    $ npm run create-tables

    有关 Knex 数据库适配器的更多信息,请访问 https://www.keystonejs.com/quick-start/adapters 或访问 http://knexjs.org/ 上的 knex.js。它是一个用于 PostgreSQL、MySQL 和 SQLite3 的查询构建器。

  5. 如果你使用的是受保护的 MongoDB,只需提供连接字符串以及 Keystone 的用户名、密码和数据库:

    // index.js
    const adapterConfig = { mongoUri:
      'mogodb://<username>:<password>@localhost:27017/<dbname>' }

    请注意,如果没有启用身份验证,你只需从字符串中删除 <username>:<password>@。

    有关 Mongoose 数据库适配器的更多信息,请访问 https://www.keystonejs.com/keystonejs/adapter-mongoose/ 或访问 https://mongoosejs.com/ 上的 Mongoose。MongoDB 本质上是一个无模式数据库系统,因此此适配器用作模式解决方案来建模我们应用程序中的数据。

  6. 将服务器默认端口从 3000 更改为 4000,以服务 Keystone 应用程序。你可以通过简单地将 PORT=4000 添加到 dev 脚本中来实现,如下所示:

    // package.json
    "scripts": {
      "dev": "cross-env NODE_ENV=development PORT=4000 ..."
    }

    我们将 Keystone 的端口更改为 4000,是因为我们为 Nuxt 应用程序保留了 3000 端口。

  7. 在我们的项目中安装 nodemon。这将允许我们监视 Keystone 应用程序中的更改,以便它可以为我们重新加载服务器:

    $ npm i nodemon --save-dev
  8. 安装此软件包后,将 nodemon --exec 命令添加到 dev 脚本中,如下所示:

    // package.json
    "scripts": {
      "dev": "... nodemon --exec keystone dev",
    }

    有关 nodemon 的更多信息,请访问 https://nodemon.io/。

  9. 使用以下命令启动我们的 Keystone 应用程序的开发服务器:

    $ npm run dev

    你应该在终端上看到以下输出。这意味着你已成功安装 Keystone 应用程序:

    ✓ Keystone instance is ready at http://localhost:4000
    ∞ Keystone Admin UI: http://localhost:4000/admin
    ∞ GraphQL Playground: http://localhost:4000/admin/graphiql
    ∞ GraphQL API: http://localhost:4000/admin/api

这与执行手动安装相同,但端口不同。在这个应用程序中,你在 localhost:4000/admin/api 有一个 GraphQL API,在 localhost:4000/admin/graphiql 有一个 GraphQL Playground,在 localhost:4000/admin 有一个 Keystone Admin UI。在我们使用 GraphQL API 和 GraphQL Playground 做任何事情之前,我们必须向我们的 Keystone 应用程序添加列表,并开始从 Keystone Admin UI 注入数据。我们将在下一节中开始向应用程序添加列表和字段。

你可以在本书 GitHub 存储库的 /chapter-18/keystone/ 中找到我们使用这两种安装技术创建的应用程序。

创建列表和字段

在 Keystone 中,列表是模式(schemas)。模式是一种数据模型,它具有描述我们数据的数据类型。Keystone 中也是如此:列表模式由具有数据类型的字段组成,这些数据类型描述了它们接受的数据,就像我们在手动安装中所做的那样,我们有一个由单个 Text 类型的 name 字段组成的 Page 列表。

Keystone 中有许多不同的字段类型,例如 File、Float、Checkbox、Content、DateTime、Slug 和 Relationships。你可以在其文档 https://www.keystonejs.com/ 中找到你需要了解的其余 Keystone 字段类型。

要向列表添加字段及其类型,你只需在你的项目目录中安装包含这些字段类型的 Keystone 软件包。例如,@keystonejs/fields 软件包包含 Checkbox、Text、Float 和 DateTime 等字段类型。你可以在 https://www.google.com/search?q=https://www.keystonejs.com/keystonejs/fields/fields 找到其余字段类型的信息。安装所需的字段类型软件包后,你只需导入它们,并使用 JavaScript 解构赋值来提取创建列表所需的字段类型。

然而,列表会随着时间的推移而增长,这意味着它们可能会变得混乱且难以维护。因此,为了更好地维护,最好在 /list/ 目录中的单独文件中创建列表,如下所示:

// lists/Page.js
const { Text } = require('@keystonejs/fields')
module.exports = {
  fields: {...},
}

然后,你只需将其导入到 index.js 文件中。因此,让我们了解一下构建我们的 Keystone 应用程序需要哪些模式/列表和其他 Keystone 软件包。我们将创建的列表如下:

  • 一个 Page 模式/列表,用于存储主页面,如首页、关于、联系方式和项目

  • 一个 Project 模式/列表,用于存储项目页面

  • 一个 Image 模式/列表,用于存储主页面和项目页面的图像

  • 一个 Slide Image 模式/列表,仅用于存储主页面的图像

  • 一个 Nav Link 模式/列表,用于存储站点链接

我们将使用以下 Keystone 软件包来创建这些列表:

现在,让我们安装并使用它们来创建我们的列表:

  1. 通过 npm 安装我们前面提到的 Keystone 软件包:

    $ npm i @keystonejs/app-static
    $ npm i @keystonejs/file-adapters
    $ npm i @keystonejs/fields-wysiwyg-tinymce
  2. 将 @keystonejs/app-static 导入到 index.js 中,并定义要保存静态文件的路径和文件夹名称:

    // index.js
    const { StaticApp } = require('@keystonejs/app-static');
    module.exports = {
      apps: [
        new StaticApp({
          path: '/public',
          src: 'public'
        }),
      ],
    }
  3. 在 /lists/ 目录中创建一个 File.js 文件。然后,使用 @keystonejs/fields 中的 File、Text 和 Slug 字段类型以及 @keystonejs/file-adapters 中的 LocalFileAdapter 定义 Image 列表的字段。这将允许你将文件上传到本地位置,即 /public/files/:

    // lists/File.js
    const { File, Text, Slug } = require('@keystonejs/fields')
    const { LocalFileAdapter } = require('@keystonejs/file-adapters')
    const fileAdapter = new LocalFileAdapter({
      src: './public/files',
      path: '/public/files',
    })
    module.exports = {
      fields: {
        title: { type: Text, isRequired: true },
        alt: { type: Text },
        caption: { type: Text, isMultiline: true },
        name: { type: Slug },
        file: { type: File, adapter: fileAdapter, isRequired: true },
      }
    }

    在上面的代码中,我们定义了一个字段列表(title、alt、caption、name 和 file),以便我们可以存储每个上传文件的元信息。在每个列表模式中都有一个 name 字段是一个很好的做法,这样我们就可以在此字段中存储一个唯一的名称,该名称可以用作 Keystone Admin UI 中的标签。我们可以使用它来轻松识别每个注入的列表项。要为此字段生成唯一的名称,我们可以使用 Slug 类型,该类型默认情况下从 title 字段生成唯一的名称。

    有关我们在上面的代码中使用的字段类型的更多信息,请访问以下链接:

    有关 LocalFileAdapter 的更多信息,请访问 https://www.keystonejs.com/keystonejs/file-adapters/localfileadapter。

    我们的应用程序文件可以使用 CloudinaryFileAdapter 上传到 Cloudinary。

    有关如何设置帐户以便在 Cloudinary 上托管文件的更多信息,请访问 https://cloudinary.com/。

  4. 在 /lists/ 目录中创建一个 SlideImage.js 文件,并定义与 File.js 文件中相同的字段,并添加一个额外的字段类型 Relationship,以便你可以将幻灯片图像链接到项目页面:

    // lists/SlideImage.js
    const { Relationship } = require('@keystonejs/fields')
    module.exports = {
      fields: {
        // ...
        link: { type: Relationship, ref: 'Project' },
      },
    }

    有关 Relationship 字段的更多信息,请访问 https://www.keystonejs.com/keystonejs/fields/src/types/relationship/。

  5. 在 /lists/ 目录中创建一个 Page.js 文件,并使用 @keystonejs/fields 和 @keystonejs/fields-wysiwyg-tinymce 中的 Text、Relationship、Slug 和 Wysiwyg 字段类型定义 Page 列表的字段,如下所示:

    // lists/Page.js
    const { Text, Relationship, Slug } = require('@keystonejs/fields')
    const { Wysiwyg } = require('@keystonejs/fields-wysiwyg-tinymce')
    module.exports = {
      fields: {
        title: { type: Text, isRequired: true },
        excerpt: { type: Text, isMultiline: true },
        content: { type: Wysiwyg },
        name: { type: Slug },
        featuredImage: { type: Relationship, ref: 'Image' },
        slideImages: { type: Relationship, ref: 'SlideImage', many:
          true },
      },
    }

    在上面的代码中,我们定义了一个字段列表(title、excerpt、content、name、featuredImage 和 slideImages),以便我们可以存储将注入到此内容类型中的每个主要页面的数据。请注意,我们将 featuredImage 链接到 Image 列表,并将 slideImages 链接到 SlideImage 列表。我们希望允许在 slideImages 字段中放置多个图像,因此我们将 many 选项设置为 true。

    有关这些一对多和多对多关系的更多信息,请访问 https://www.keystonejs.com/guides/newschema-cheatsheet。

  6. 在 /lists/ 目录中创建一个 Project.js 文件,并为 Project 列表定义与 File.js 文件中相同的字段,并添加两个额外的字段 (fullscreenImage 和 projectImages):

    // lists/Project.js
    const { Text, Relationship, Slug } = require('@keystonejs/fields')
    const { Wysiwyg } = require('@keystonejs/fields-wysiwyg-tinymce')
    module.exports = {
      fields: {
        //...
        fullscreenImage: { type: Relationship, ref: 'Image' },
        projectImages: { type: Relationship, ref: 'Image', many:
          true },
      },
    }
  7. 在 /lists/ 目录中创建一个 NavLink.js 文件,并使用 @keystonejs/fields 中的 Text、Relationship、Slug 和 Integer 字段类型定义 NavLink 列表的字段(title、order、name、link、subLinks),如下所示:

    // lists/NavLink.js
    const { Text, Relationship, Slug, Integer } =
      require('@keystonejs/fields')
    module.exports = {
      fields: {
        title: { type: Text, isRequired: true },
        order: { type: Integer, isRequired: true },
        name: { type: Slug },
        link: { type: Relationship, ref: 'Page' },
        subLinks: { type: Relationship, ref: 'Project', many: true },
      },
    }

    在这里,我们使用 order 字段根据它们在 GraphQL 查询中的数字位置对链接项进行排序。你很快就会了解这一点。subLinks 字段是一个示例,演示如何在 Keystone 中创建简单的子链接。因此,我们可以通过将项目页面附加到此字段来向主链接添加多个子链接,该字段使用 Relationship 字段类型链接到 Project 列表。

    有关 Integer 字段类型的更多信息,请访问 https://www.keystonejs.com/keystonejs/fields/src/types/integer/。

  8. 从 /lists/ 目录导入文件,并开始从中创建列表模式,如下所示:

    // index.js
    const PageSchema = require('./lists/Page.js')
    const ProjectSchema = require('./lists/Project.js')
    const FileSchema = require('./lists/File.js')
    const SlideImageSchema = require('./lists/SlideImage.js')
    const NavLinkSchema = require('./lists/NavLink.js')
    const keystone = new Keystone({ ... })
    keystone.createList('Page', PageSchema)
    keystone.createList('Project', ProjectSchema)
    keystone.createList('Image', FileSchema)
    keystone.createList('SlideImage', SlideImageSchema)
    keystone.createList('NavLink', NavLinkSchema)
  9. 通过在终端上运行 dev 脚本来启动应用程序:

    $ npm run dev

你应该在终端上看到与上一节中显示的相同的 URL 列表。这意味着你已在 localhost:4000 上成功启动应用程序。现在,你可以将浏览器指向 localhost:4000/admin 并开始从 Keystone Admin UI 注入内容和上传文件。一旦你准备好内容和数据,你就可以使用 GraphQL API 和 GraphQL Playground 查询它们。但在你这样做之前,你应该了解什么是 GraphQL 以及如何独立于 Keystone 创建和使用它。因此,让我们来看看!

你可以在本书 GitHub 存储库的 /chapter-18/crossdomain/backend/keystone/ 中找到此应用程序的源代码。