圖片來自 Lars Kienle

初探 Strapi Headless CMS

這篇是某個晚上試玩 Strapi 這套 headless CMS 的心得,主要是談 Strapi 和 headless CMS 帶來的變革,不太會談到具體的操作過程。

先談談 headless CMS。

Headless 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

Strapi 是個開源的 headless CMS 系統,底層則是 Node.js 的 web 框架 Koa。

依照 Strapi 的文件把範例建起來之後,在 Strapi Admin Panel 內建了一個 Restaurant 的 model(Strapi 稱為 Content Type):

Strapi

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 做開發,下面分別看看這些原始碼的內容與架構。

Routing

{
    "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": []
            }
        }
    ]
}

Model

欄位定義在 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 也都是類似的內容。

Controller

檔案在 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 = {};

Service

檔案在 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

不過大家都知道 Markdown 本身的格式是受限的,例如不能指定 idclass,也不能改文字顏色,雖然 Markdown 允許在內文中直接插入 HTML,不過這樣就失去了這個 Admin Panel 存在的重要特性之一:讓非開發人員可以在此管理內容,殘念です。