这篇是某个晚上试玩 Strapi 这套 headless CMS 的心得,主要是谈 Strapi 和 headless CMS 带来的变革,不太会谈到具体的操作过程。

先谈谈 headless CMS。

无头 CMS

Headless CMS 是前后端分离概念下的产物,headless CMS 可以简单的理解为剥去前端的 CMS,headless CMS 以 API 的方式(通常是 RESTful API 或 GraphQL) 供应前端内容,前端(通常是 Aurelia、Svelte、Vue、React、Angular)也透过 API 与 headless CMS 沟通,取得内容呈现,或发送内容回 headless CMS。

在上面的前后端分离的架构下,headless CMS 必须具备几项特性:

  1. 管理内容的能力,包括内容的栏位、资料型态、栏位关联性、以及内容本身,以开发的角度讲,就是 model 的制定与管理。另外一种内容是媒体管理,图片、音档、影片、PDF 等的媒体资产管理。

  2. 管理资料库的能力,上面的内容都必须对应到资料库,以开发的角度讲,就是 ORM。

  3. 管理 API 的能力,上面的内容(model)除了向下对应到资料库外,向外也要有对应的 API,并且 model、table、API 的连动是自动化的。

  4. 除了主要的内容外,还必须有权限、身份认证等系统必备的 API。

  5. 上面的每个特性都是有一个后台界面(Admin Panel)可以让一般人操作,而不是只能透过程式码的方式操作。

从上面几点可以看出 headless CMS 相较于典型的 MCV web 框架(如 Masonite、Laravel),多了几项特性:

  • Model 是可以由用户在 Admin Panel 自行定义的,不用由开发人员施工。

  • Controller 是自动化建构的,只要在 Admin Panel 定义好 model,API 就会自动产生,不用开发人员施工。

在这样的特性下,配合大前端时代的降临,大部分的业务逻辑都往前端实做,开发人员的精力完全可以投注在前端工程上,headless CMS 的角色就专注于当个称职的网站后端或应用后端,是不是很棒?

肩带

Strapi 是个开源的 headless CMS 系统,底层则是 Node.js 的 web 框架 Koa。

依照 Strapi 的文件把范例建起来之后,在 Strapi Admin Panel 内建了一个 Restaurant 的 model(Strapi 称为 Content Type):

[! Strapi] (https://res.cloudinary.com/practicaldev/image/fetch/s--Zy3yjWwD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads .s3.amazonaws.com/上传/文章/rxeodkk7nxl58xxwabna.png)

Strapi 会自动帮我们产生 API 与文件:

Strapi OpenAPI 文件

而在专案目录内,Strapi 会自动帮我们配置出 Restaurant 的路由、model 和 API:

my-project/
┣ api/
┃ ┗ restaurant/
┃   ┣ config/
┃   ┃ ┗ routes.json
┃   ┣ controllers/
┃   ┃ ┗ restaurant.js
┃   ┣ documentation/
┃   ┃ ┗ 1.0.0/
┃   ┃   ┣ overrides/
┃   ┃   ┗ restaurant.json
┃   ┣ models/
┃   ┃ ┣ restaurant.js
┃   ┃ ┗ restaurant.settings.json
┃   ┗ services/
┃     ┗ restaurant.js
┣ config/
┃ ┣ functions/
┃ ┃ ┣ responses/
┃ ┃ ┃ ┗ 404.js
┃ ┃ ┣ bootstrap.js
┃ ┃ ┗ cron.js
┃ ┣ database.js
┃ ┗ server.js
┣ extensions/
┃ ┣ documentation/
┃ ┣ email/
┃ ┣ upload/
┃ ┗ users-permissions/
┗ public/
  ┣ uploads/
  ┗ robots.txt

进入全屏模式 退出全屏模式

可以看到,如果有需要的话,可以再对 controller、model、service 做开发,下面分别看看这些原始码的内容与架构。

路由

{
    "routes": [
        {
            "method": "GET",
            "path": "/restaurants",
            "handler": "restaurant.find",
            "config": {
                "policies": []
            }
        },
        {
            "method": "GET",
            "path": "/restaurants/count",
            "handler": "restaurant.count",
            "config": {
                "policies": []
            }
        },
        {
            "method": "GET",
            "path": "/restaurants/:id",
            "handler": "restaurant.findOne",
            "config": {
                "policies": []
            }
        },
        {
            "method": "POST",
            "path": "/restaurants",
            "handler": "restaurant.create",
            "config": {
                "policies": []
            }
        },
        {
            "method": "PUT",
            "path": "/restaurants/:id",
            "handler": "restaurant.update",
            "config": {
                "policies": []
            }
        },
        {
            "method": "DELETE",
            "path": "/restaurants/:id",
            "handler": "restaurant.delete",
            "config": {
                "policies": []
            }
        }
    ]
}

进入全屏模式 退出全屏模式

型号

栏位定义在 api/models/restaurant.settings.json:

{
    "kind": "collectionType",
    "collectionName": "restaurants",
    "info": {
        "name": "restaurant",
        "description": ""
    },
    "options": {
        "increments": true,
        "timestamps": true,
        "draftAndPublish": true
    },
    "attributes": {
        "name": {
            "type": "string",
            "required": true,
            "unique": true
        },
        "description": {
            "type": "richtext"
        },
        "BGM": {
            "collection": "file",
            "via": "related",
            "allowedTypes": [
                "images",
                "files",
                "videos"
            ],
            "plugin": "upload",
            "required": false
        }
    }
}

进入全屏模式 退出全屏模式

在 Admin Panel 定义的 model(Content Type)以及栏位都会有相对的 JSON 定义档产生,这样的好处是可以让栏位定义档本身也被 Git 管理,这也才有办法让其他的程式逻辑(如 controller)和 model 一同接受版控的管理。

另外一个是 model 的程式逻辑,在 api/restaurant/models/restaurant.js:

'use strict';

/**
 * Read the documentation (https://strapi.io/documentation/developer-docs/latest/concepts/models.html#lifecycle-hooks)
 * to customize this model
 */

module.exports = {};

进入全屏模式 退出全屏模式

内容相当简单,只有一段引导我们去看 model 开发文件的注解。

后面的 controller、service 也都是类似的内容。

控制器

档案在 api/controllers/restaurant.js:

'use strict';

/**
 * Read the documentation (https://strapi.io/documentation/developer-docs/latest/concepts/controllers.html#core-controllers)
 * to customize this controller
 */

module.exports = {};

进入全屏模式 退出全屏模式

服务

档案在 api/services/restaurant.js:

'use strict';

/**
 * Read the documentation (https://strapi.io/documentation/developer-docs/latest/concepts/services.html#core-services)
 * to customize this service
 */

module.exports = {};

进入全屏模式 退出全屏模式

Strapi 的扩充机制

实际在 Strapi Admin Panel 定义好 Restaurant 以及看过专案目录内的档案后,可以归纳一下 Strapi 的设计及它的扩充机制,前面提过,在商业逻辑往前端移动的大前端时代的背景下,像 Strapi 这样傻瓜型的 headless CMS 可以很快速让我们定义出 model 的栏位以及产出相对应的 API 及文件,但因为 Strapi 依然是基于传统的 web 框架 Koa,它还是保留了所有后端开发的架构,这样的设计兼顾了速度与弹性。

在 Admin Panel 方面,除了 model 的定义与内容的管理外,看起来略显阳春,但根据 Straip 的文件,Admin Panel 也是可以被客制的,另外 Strapi本身也有设计 plugin 的机制,包括 Strapi 自己的 GraphQL 也是以一支独立的 plugin 的方式被使用。

总结

归纳一下 Strapi 的特点:

  • 有 Admin Panel 用于定义资料与管理资料。

  • 定义的资料会自动产出 API 与 API 文件给前端使用。

  • 在 Admin Panel 定义的资料型态都会以 JSON 的格式储存,因此可以被版控系统管理。

  • 还是可以自行做后端开发与客制。

  • 开源,可以自架,资料库也放在自己家。

好处很明显,API 的制定变得简单又快速,time to market 时间可以省掉一半(写后端的那一半)。

同场加映几个也颇具特色的 headless CMS 及其它相关专案:

  • Slicknode:headless CMS「服务」,无开源,资料放在 Slicknode 家,特色是跑在 AWS serverless 平台上,感觉比 Strapi 能应付更大的存取需求。

  • Directus:和 Strapi 特色类似,也是开源专案,目前底层是 PHP 和 Zend,下一版 Directus 9 会改用 Node.js。

  • FastAPI:把 headless CMS 的前台界面(如 Strapi 的 Admin Panel)再剥离的 web 框架,FastAPI 顾名思义是专门为 API 设计的框架,在程式码内定义好 route、model、function 后 FastAPI 就会自动产出 API 文件,FastAPI 还有其它专为 API 设计的特性,可以访问 FastAPI 网站了解。

补充

Strapi 有提供 rich text 型态的栏位,它在编辑区是以 Markdown 的方式做编辑,如下图:

[! Strapi] (https://res.cloudinary.com/practicaldev/image/fetch/s--YHjWYepW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads .s3.amazonaws.com/上传/文章/kbieh2ud4xjltanm02c8.png)

不过大家都知道 Markdown 本身的格式是受限的,例如不能指定idclass,也不能改文字颜色,虽然 Markdown 允许在内文中直接插入 HTML,不过这样就失去了这个 Admin Panel 存在的重要特性之一:让非开发人员可以在此管理内容,残念です。

Logo

更多推荐