Portal CMS在 VSCode 中运行的本地 Headless CMS 插件 Portal CMS 是一个完全运行在 VSCode 内部的内容管理系统插件。它不依赖服务器或数据库,所有内容以 JSON 文件形式存储在工作区的 它解决什么问题构建博客、官网、文档站等前端项目时,开发者常常面临一个矛盾:需要结构化内容管理,但部署一套完整的 Headless CMS 又过于繁重。 Portal CMS 提供了第三条路:
功能特性
系统要求
安装方式一:应用市场安装
方式二:手动安装
|
| 类型 | 说明 | 示例用途 |
|---|---|---|
string |
单行文本,可设最大/最小长度 | 标题、名称、URL |
text |
多行长文本 | 正文、描述、Markdown 内容 |
number |
整数或浮点数,可设范围 | 价格、排序权重、数量 |
boolean |
开关(true/false) | 是否置顶、是否启用 |
date |
日期时间,ISO 8601 格式 | 发布日期、截止时间 |
media |
图片或文件引用 | 封面图、附件、头像 |
component |
嵌套结构,可包含上述所有类型 | 作者信息、SEO 配置、地址 |
component 类型支持任意层级嵌套,例如:
字段:author(component)
└── name (string)
└── email (string)
└── avatar (media)
编辑已有内容类型: 在左侧导航悬停内容类型名称,点击出现的编辑图标即可修改字段,但 Key 不可变更。
3. 录入条目
内容类型创建完成后,在左侧导航点击对应类型名称进入条目列表。
新建条目:
- 点击右上角的 "+ 新建" 按钮
- 在编辑器中依次填写各字段内容
- 点击右下角 "保存" 按钮
编辑条目: 在列表中点击任意条目即可重新打开编辑器。
删除条目: 打开编辑器后点击左下角 "删除" 按钮,确认后条目文件将从磁盘删除,关联的媒体文件同步清理。
4. 草稿与发布状态
每条条目都有两种状态,可在编辑器底部切换:
- 草稿(Draft):
publishedAt为空,不会出现在导出文件中,适合未完成或待审核的内容 - 已发布(Published):
publishedAt记录发布时间,正常出现在导出文件中
条目列表顶部会显示当前内容类型的草稿数量,方便快速了解发布状态。
5. 媒体文件管理
media 类型字段支持从本地选择图片或文件:
- 在条目编辑器中点击媒体字段的 "选择文件" 按钮
- 在弹出的系统文件选择框中选择文件
- 文件自动复制到
.cms/content/{slug}/media/目录,同名文件会直接覆盖
保存成功后,媒体文件在列表中可直接预览;点击文件名可在 VSCode 编辑器中打开原文件。删除条目时,不再被任何条目引用的媒体文件会被自动清理。
导出 JSON 中的媒体字段保存的是工作区相对路径(如 .cms/content/article/media/cover.png),前端可根据项目的静态资源处理方式进行引用。
6. 导出 JSON
点击条目列表右上角的导出按钮:
- 绿色"发布并导出":存在未发布草稿时显示,点击后将所有草稿标记为已发布并导出
- 黄色"重新导出":有条目变更尚未导出时显示
- 灰色"导出":内容已是最新状态
导出文件位于 .cms/export/ 目录,文件名为内容类型 Key 的复数形式(如 articles.json、products.json)。
7. 在前端项目中使用
导出的 JSON 文件可直接在前端项目中使用,字段位于顶层,无嵌套 attributes。
通用用法(ES Module import):
import articlesData from '../.cms/export/articles.json'
// 过滤已发布条目
const articles = articlesData.data.filter(a => a.publishedAt !== null)
articles.forEach(a => {
console.log(a.title) // 自定义字段直接访问
console.log(a.documentId) // 唯一 ID(nanoid)
console.log(a.publishedAt) // 发布时间
})
Vue 3 示例:
<script setup>
import { computed } from 'vue'
import articlesData from '../.cms/export/articles.json'
const articles = computed(() =>
articlesData.data.filter(a => a.publishedAt !== null)
)
</script>
<template>
<article v-for="a in articles" :key="a.documentId">
<h2>{{ a.title }}</h2>
<img v-if="a.cover" :src="https://github.com/opensourceways/vscode-portal-cms/raw/HEAD/a.cover.url" :alt="a.cover.name" />
<p>{{ a.body }}</p>
</article>
</template>
Next.js / Nuxt 静态生成示例:
// next.js - getStaticProps
import articlesData from '../../.cms/export/articles.json'
export async function getStaticProps() {
const articles = articlesData.data.filter(a => a.publishedAt !== null)
return { props: { articles } }
}
.cms/ 目录结构
.cms/
├── manifest.json # 元数据(版本号 + slug 列表 + 脏标记)
├── schemas/
│ └── article.json # 内容类型定义(字段结构)
├── content/
│ └── article/
│ ├── v7l2kpx3n8eq.json # 单条条目(文件名 = 条目 ID)
│ ├── m4rqzw9y1jce.json
│ └── media/ # 该内容类型的媒体文件
│ └── cover.png
└── export/
└── articles.json # 导出的扁平化 JSON
建议将 .cms/ 整体纳入 Git 版本控制,内容与代码统一管理。若只需版本控制结构定义而不提交内容数据,可在 .gitignore 中排除 .cms/content/。
开发指南
环境准备
git clone <repo>
cd vscode-portal-cms
yarn install # 安装所有 workspace 依赖
yarn compile # 初次构建(extension + webview)
工具要求:Node.js >= 18.x,yarn >= 1.22。
开发模式
推荐在两个终端分别运行:
# 终端 1:监听 extension 变更
yarn watch:ext
# 终端 2:监听 webview 变更
yarn watch:ui
或直接按 F5,launch.json 配置的 watch:all 任务会同时启动两个 watcher。
调试流程
- 按
F5(选择 Run Extension (Watch))启动 Extension Development Host - 在 Development Host 窗口中打开一个文件夹
- 点击活动栏的 Portal CMS 图标
修改 Extension 代码后: esbuild 自动重新打包,Ctrl+R 重载 Development Host。
修改 Webview 代码后: Vite 自动重新打包,关闭面板后重新打开即可。
调试 Webview JS:
Development Host → Ctrl+Shift+P → Developer: Open Webview Developer Tools
构建与发布
yarn vscode:prepublish # 生产模式构建(minify)
yarn package # 打包为 .vsix(需要 @vscode/vsce)
代码结构与核心设计请参阅 docs/design.md。
常见问题
Q:.cms/ 目录应该提交到 Git 吗?
建议提交。内容与代码同步管理,团队成员克隆后立即拥有完整数据。若只是本地草稿,可在 .gitignore 中排除 .cms/content/。
Q:插件在 Remote SSH / Dev Container 中可以用吗?
可以。所有文件操作使用 vscode.workspace.fs,I/O 自动路由到远程文件系统。
Q:为什么打开面板时显示空白?
请先运行 yarn compile,然后按 Ctrl+R 重载 Development Host 窗口。
Q:导出的媒体路径是绝对路径还是相对路径?
相对路径(以工作区根目录为基准),例如 .cms/content/article/media/cover.png。前端通过 Vite 的静态资源处理或 CDN 上传均可使用。
Q:如何重置 CMS 数据?
直接删除工作区中的 .cms/ 目录,重新打开面板后自动重新初始化。