初探 Strapi Headless CMS
Tags: strapi, cms, headless
Published: 2021-11-14 11:19:42

這篇是某個晚上試玩 [Strapi](https://strapi.io/) 這套 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 的文件](https://strapi.io/documentation/developer-docs/latest/getting-started/quick-start.html)把範例建起來之後，在 Strapi Admin Panel 內建了一個 Restaurant 的 model（Strapi 稱為 Content Type）：

![Strapi](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rxeodkk7nxl58xxwabna.png)

Strapi 會自動幫我們產生 API 與文件：

![Strapi OpenAPI 文件](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h3vqgvpi5yjc5j7f0slr.png)

而在專案目錄內，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

```JSON
{
    "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：

```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：

```JavaScript
'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：

```JavaScript
'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：

```JavaScript
'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 也是可以被客製的](https://strapi.io/documentation/developer-docs/latest/admin-panel/customization.html)，另外 [Strapi本身也有設計 plugin 的機制](https://strapi.io/documentation/developer-docs/latest/plugins/documentation.html)，包括 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](https://editor.leonh.space/2021/pydantic/)：把 headless CMS 的前台界面（如 Strapi 的 Admin Panel）再剝離的 web 框架，FastAPI 顧名思義是專門為 API 設計的框架，在程式碼內定義好 route、model、function 後 FastAPI 就會自動產出 API 文件，FastAPI 還有其它專為 API 設計的特性，可以訪問 FastAPI 網站了解。

## 補充

Strapi 有提供 rich text 型態的欄位，它在編輯區是以 Markdown 的方式做編輯，如下圖：

![Strapi](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kbieh2ud4xjltanm02c8.png)

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