用 Cloudflare 搭一个低成本企业知识库:Pages + Workers + D1 实战源码
Cloudflare 企业知识库搭建|附源码
在企业数字化协作中,知识库几乎是每个团队都绕不开的基础设施。无论是产品文档、研发规范、运维手册、客户支持 FAQ,还是内部流程制度,如果缺少一个统一、可搜索、易维护的知识沉淀平台,信息就会分散在聊天记录、个人文档、网盘文件和邮件中,最终导致新人学习成本高、重复沟通多、经验难以复用。
传统知识库系统通常需要购买服务器、配置数据库、部署后端服务、维护搜索引擎和权限系统。对于中小团队来说,这套成本并不低。而 Cloudflare 提供的一系列边缘计算能力,例如 Cloudflare Pages、Workers、D1、KV、R2、Access,可以帮助我们以较低成本搭建一个安全、快速、可扩展的企业知识库。
本文将介绍一种基于 Cloudflare 的企业知识库搭建方案,并提供一套可运行的示例源码。你可以基于它继续扩展,例如增加全文搜索、SSO 登录、Markdown 编辑器、附件管理、AI 问答等功能。
一、为什么选择 Cloudflare 搭建知识库?
Cloudflare 最初以 CDN、安全防护和 DNS 服务闻名,但近几年它的开发者平台能力越来越完整,已经可以支撑很多轻量级甚至中型应用。
对于企业知识库来说,Cloudflare 有几个明显优势。
1. 全球访问速度快
Cloudflare 的网络节点遍布全球,使用 Pages 或 Workers 部署后,用户访问会被路由到较近的边缘节点。对于跨城市、跨区域甚至跨国团队来说,访问速度通常比单一云服务器更稳定。
2. 运维成本低
传统部署需要自己维护服务器、Nginx、HTTPS、系统补丁、应用进程、数据库备份等。Cloudflare Pages 和 Workers 属于 Serverless 架构,大部分底层运维工作由平台完成,开发者只需要关注业务逻辑。
3. 安全能力完善
企业知识库往往包含内部资料,不适合完全公开。Cloudflare Access 可以基于邮箱、身份提供商、组织规则进行访问控制,不必自行实现复杂的登录系统。结合 WAF、Bot 防护、DDoS 防护,可以提升整体安全性。
4. 成本友好
对于访问量不大的内部系统,Cloudflare 的免费额度已经能覆盖不少场景。即使进入付费阶段,也可以按需扩展,不需要一开始就购买固定规格服务器。
二、知识库功能设计
本文示例实现一个基础版企业知识库,主要包含以下功能:
- 文档列表展示;
- 文档详情查看;
- 文档支持 Markdown 格式;
- 支持按分类查看;
- 支持前端搜索标题和摘要;
- 使用 Cloudflare Pages 部署前端;
- 使用 Cloudflare Workers 提供 API;
- 使用 Cloudflare D1 存储文档数据;
- 可扩展 Cloudflare Access 做企业登录保护。
为了保持源码清晰,本文暂不实现复杂的后台管理系统,而是通过 SQL 初始化文档数据。实际生产环境中,可以继续扩展后台管理、权限分组、版本历史和编辑审核等功能。
三、整体架构
推荐架构如下:
用户浏览器
│
▼
Cloudflare Pages
│ 静态前端页面
▼
Cloudflare Workers API
│ 查询文档数据
▼
Cloudflare D1
存储知识库文章、分类等数据
其中:
- Pages:负责托管前端静态页面;
- Workers:负责处理 API 请求;
- D1:Cloudflare 提供的 Serverless SQLite 数据库;
- Access:可选,用于保护整个知识库访问入口;
- R2:可选,用于存储图片、PDF、附件等文件;
- KV:可选,用于缓存热门文档、站点配置等数据。
这种架构非常适合轻量级企业知识库:部署简单、扩展方便、边缘访问速度快。
四、项目目录结构
示例项目结构如下:
cloudflare-kb/
├── frontend/
│ ├── index.html
│ ├── style.css
│ └── app.js
├── worker/
│ ├── src/
│ │ └── index.js
│ └── wrangler.toml
└── schema.sql
说明:
frontend:前端静态文件;worker:Cloudflare Worker API 服务;schema.sql:D1 数据库建表和初始化数据脚本。
五、数据库设计
为了演示方便,我们只设计两张表:分类表和文档表。
-- schema.sql
DROP TABLE IF EXISTS categories;
DROP TABLE IF EXISTS docs;
CREATE TABLE categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
description TEXT DEFAULT '',
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE docs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category_id INTEGER NOT NULL,
title TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
summary TEXT DEFAULT '',
content TEXT NOT NULL,
author TEXT DEFAULT '',
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES categories(id)
);
INSERT INTO categories (name, slug, description) VALUES
('研发规范', 'dev', '研发流程、代码规范、发布规范'),
('运维手册', 'ops', '服务器、监控、故障处理相关文档'),
('产品文档', 'product', '产品说明、需求设计和用户指南');
INSERT INTO docs (category_id, title, slug, summary, content, author) VALUES
(
1,
'Git 分支管理规范',
'git-branch-guide',
'介绍企业研发团队常用的 Git 分支管理策略。',
'# Git 分支管理规范
## 分支类型
- main:生产环境稳定分支
- develop:日常开发集成分支
- feature/*:功能开发分支
- hotfix/*:线上紧急修复分支
## 提交流程
1. 从 develop 创建 feature 分支;
2. 开发完成后提交 Pull Request;
3. Code Review 通过后合并;
4. 测试通过后发布到生产环境。
## 注意事项
不要直接向 main 分支提交代码,所有变更都必须经过审核。',
'技术团队'
),
(
2,
'线上故障处理流程',
'incident-response',
'定义线上故障发现、响应、定位、恢复和复盘流程。',
'# 线上故障处理流程
## 处理原则
线上故障处理应遵循快速止血、明确分工、及时同步、事后复盘的原则。
## 流程
1. 发现告警;
2. 确认影响范围;
3. 指定故障负责人;
4. 采取回滚、限流、扩容等止血措施;
5. 故障恢复后输出复盘报告。
## 复盘内容
- 故障时间线
- 根因分析
- 影响范围
- 改进措施
- 负责人和完成时间',
'运维团队'
),
(
3,
'产品需求评审模板',
'prd-review-template',
'用于产品需求评审会议的通用模板。',
'# 产品需求评审模板
## 背景
说明需求来源、业务目标和用户痛点。
## 目标
明确本次需求希望达成的结果。
## 范围
说明本期做什么、不做什么。
## 交互说明
补充页面流程、状态变化、异常场景。
## 验收标准
列出研发、测试、产品共同认可的验收条件。',
'产品团队'
);
六、Cloudflare Worker API 源码
进入 worker/src/index.js,编写如下代码:
export default {
async fetch(request, env) {
const url = new URL(request.url);
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type"
};
if (request.method === "OPTIONS") {
return new Response(null, {
headers: corsHeaders
});
}
try {
if (url.pathname === "/api/categories") {
return json(await getCategories(env), corsHeaders);
}
if (url.pathname === "/api/docs") {
const category = url.searchParams.get("category");
const keyword = url.searchParams.get("keyword");
return json(await getDocs(env, category, keyword), corsHeaders);
}
if (url.pathname.startsWith("/api/docs/")) {
const slug = url.pathname.replace("/api/docs/", "");
return json(await getDocDetail(env, slug), corsHeaders);
}
return json({ error: "Not Found" }, corsHeaders, 404);
} catch (err) {
return json({
error: "Internal Server Error",
message: err.message
}, corsHeaders, 500);
}
}
};
function json(data, headers = {}, status = 200) {
return new Response(JSON.stringify(data, null, 2), {
status,
headers: {
"Content-Type": "application/json; charset=utf-8",
...headers
}
});
}
async function getCategories(env) {
const result = await env.DB.prepare(
"SELECT id, name, slug, description FROM categories ORDER BY id ASC"
).all();
return result.results;
}
async function getDocs(env, category, keyword) {
let sql = `
SELECT
docs.id,
docs.title,
docs.slug,
docs.summary,
docs.author,
docs.created_at,
docs.updated_at,
categories.name AS category_name,
categories.slug AS category_slug
FROM docs
LEFT JOIN categories ON docs.category_id = categories.id
WHERE 1 = 1
`;
const params = [];
if (category) {
sql += " AND categories.slug = ?";
params.push(category);
}
if (keyword) {
sql += " AND (docs.title LIKE ? OR docs.summary LIKE ?)";
params.push(`%${keyword}%`, `%${keyword}%`);
}
sql += " ORDER BY docs.updated_at DESC";
const stmt = env.DB.prepare(sql).bind(...params);
const result = await stmt.all();
return result.results;
}
async function getDocDetail(env, slug) {
const result = await env.DB.prepare(`
SELECT
docs.id,
docs.title,
docs.slug,
docs.summary,
docs.content,
docs.author,
docs.created_at,
docs.updated_at,
categories.name AS category_name,
categories.slug AS category_slug
FROM docs
LEFT JOIN categories ON docs.category_id = categories.id
WHERE docs.slug = ?
LIMIT 1
`).bind(slug).first();
if (!result) {
return {
error: "Document Not Found"
};
}
return result;
}
这段 Worker 代码提供了三个核心接口:
GET /api/categories 获取分类列表
GET /api/docs 获取文档列表
GET /api/docs/:slug 获取文档详情
列表接口支持两个查询参数:
/api/docs?category=dev
/api/docs?keyword=git
实际项目中,你可以继续增加:
POST /api/docs新增文档;PUT /api/docs/:id更新文档;DELETE /api/docs/:id删除文档;- 权限校验;
- 操作审计日志;
- 文档版本历史。
七、Worker 配置文件
在 worker/wrangler.toml 中写入:
name = "cloudflare-kb-api"
main = "src/index.js"
compatibility_date = "2024-06-01"
[[d1_databases]]
binding = "DB"
database_name = "cloudflare_kb"
database_id = "替换为你的 D1 database_id"
binding = "DB" 要与代码中的 env.DB 保持一致。
八、创建并初始化 D1 数据库
首先安装 Wrangler:
npm install -g wrangler
登录 Cloudflare:
wrangler login
创建 D1 数据库:
wrangler d1 create cloudflare_kb
执行后会输出类似内容:
[[d1_databases]]
binding = "DB"
database_name = "cloudflare_kb"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
将其中的 database_id 填入 wrangler.toml。
初始化数据库:
wrangler d1 execute cloudflare_kb --file=../schema.sql
如果你的终端当前目录在项目根目录,可以执行:
wrangler d1 execute cloudflare_kb --file=schema.sql
部署 Worker:
cd worker
wrangler deploy
部署成功后,你会得到一个 Worker 地址,例如:
https://cloudflare-kb-api.yourname.workers.dev
九、前端页面源码
下面是一个不依赖框架的前端示例,适合快速部署到 Cloudflare Pages。如果后续需要更复杂的交互,可以改造成 Vue、React 或 Next.js 项目。
1. frontend/index.html
企业知识库
2. frontend/style.css
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif;
background: #f5f7fb;
color: #1f2937;
}
.app {
display: flex;
min-height: 100vh;
}
.sidebar {
width: 300px;
padding: 28px;
background: #111827;
color: #fff;
}
.sidebar h1 {
margin: 0;
font-size: 26px;
}
.desc {
color: #cbd5e1;
line-height: 1.7;
margin-bottom: 24px;
}
.search-box {
display: flex;
gap: 8px;
margin-bottom: 24px;
}
.search-box input {
flex: 1;
padding: 10px;
border-radius: 8px;
border: none;
}
.search-box button,
.category {
cursor: pointer;
}
.search-box button {
border: none;
border-radius: 8px;
padding: 0 14px;
background: #2563eb;
color: #fff;
}
.category {
display: block;
width: 100%;
margin-bottom: 10px;
padding: 12px;
border: none;
border-radius: 10px;
background: #1f2937;
color: #e5e7eb;
text-align: left;
}
.category.active,
.category:hover {
background: #2563eb;
color: #fff;
}
.content {
flex: 1;
padding: 40px;
}
.doc-list {
display: grid;
gap: 18px;
}
.doc-card {
padding: 22px;
background: #fff;
border-radius: 14px;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
cursor: pointer;
transition: all 0.2s ease;
}
.doc-card:hover {
transform: translateY(-2px);
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.1);
}
.doc-card h2 {
margin: 0 0 10px;
font-size: 22px;
}
.doc-card p {
margin: 0 0 14px;
color: #4b5563;
line-height: 1.7;
}
.meta {
font-size: 13px;
color: #6b7280;
}
.doc-detail {
max-width: 860px;
padding: 36px;
background: #fff;
border-radius: 16px;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
}
.doc-detail h1 {
margin-top: 0;
}
.doc-detail .back {
display: inline-block;
margin-bottom: 24px;
color: #2563eb;
cursor: pointer;
}
.doc-body {
line-height: 1.9;
}
.doc-body h1,
.doc-body h2,
.doc-body h3 {
margin-top: 28px;
}
.doc-body code {
background: #f3f4f6;
padding: 2px 5px;
border-radius: 4px;
}
.hidden {
display: none;
}
@media (max-width: 800px) {
.app {
flex-direction: column;
}
.sidebar {
width: 100%;
}
.content {
padding: 20px;
}
}
3. frontend/app.js
const API_BASE = "https://cloudflare-kb-api.yourname.workers.dev";
const categoryList = document.getElementById("categoryList");
const docList = document.getElementById("docList");
const docDetail = document.getElementById("docDetail");
const keywordInput = document.getElementById("keywordInput");
const searchBtn = document.getElementById("searchBtn");
let currentCategory = "";
async function request(path) {
const res = await fetch(`${API_BASE}${path}`);
if (!res.ok) {
throw new Error("请求失败");
}
return res.json();
}
async function loadCategories() {
const categories = await request("/api/categories");
categoryList.innerHTML = categories.map(item => `
`).join("");
document.querySelectorAll(".category").forEach(btn => {
btn.addEventListener("click", () => {
document.querySelectorAll(".category").forEach(i => i.classList.remove("active"));
btn.classList.add("active");
currentCategory = btn.dataset.slug || "";
loadDocs();
});
});
}
async function loadDocs() {
const keyword = keywordInput.value.trim();
const params = new URLSearchParams();
if (currentCategory) {
params.set("category", currentCategory);
}
if (keyword) {
params.set("keyword", keyword);
}
const docs = await request(`/api/docs?${params.toString()}`);
docDetail.classList.add("hidden");
docList.classList.remove("hidden");
if (!docs.length) {
docList.innerHTML = `暂无文档`;
return;
}
docList.innerHTML = docs.map(doc => `
${escapeHtml(doc.title)}
${escapeHtml(doc.summary || "")}
`).join("");
document.querySelectorAll(".doc-card").forEach(card => {
card.addEventListener("click", () => {
loadDocDetail(card.dataset.slug);
});
});
}
async function loadDocDetail(slug) {
const doc = await request(`/api/docs/${slug}`);
if (doc.error) {
docDetail.innerHTML = `文档不存在
`;
return;
}
docList.classList.add("hidden");
docDetail.classList.remove("hidden");
docDetail.innerHTML = `
← 返回列表
${escapeHtml(doc.title)}
${markdownToHtml(doc.content || "")}
`;
document.getElementById("backBtn").addEventListener("click", () => {
docDetail.classList.add("hidden");
docList.classList.remove("hidden");
});
}
function markdownToHtml(markdown) {
return escapeHtml(markdown)
.replace(/^### (.*$)/gim, "$1
")
.replace(/^## (.*$)/gim, "$1
")
.replace(/^# (.*$)/gim, "$1
")
.replace(/^\- (.*$)/gim, "$1 ")
.replace(/\n\n/g, "")
.replace(/\n/g, "
")
.replace(/^(.+)$/gim, "
$1
")
.replace(/<\/p>/g, " ");
}
function escapeHtml(str) {
return String(str).replace(/[&<>"']/g, char => ({
"&": "&",
"<": "<",
">": ">",
"\"": """,
"'": "'"
}[char]));
}
searchBtn.addEventListener("click", loadDocs);
keywordInput.addEventListener("keydown", event => {
if (event.key === "Enter") {
loadDocs();
}
});
loadCategories();
loadDocs();
需要注意的是,这里的 markdownToHtml 只是一个非常简化的 Markdown 渲染函数,适合演示使用。生产环境建议接入成熟库,例如 marked、markdown-it,并配合 HTML 清洗库,例如 DOMPurify,避免 XSS 风险。
十、部署前端到 Cloudflare Pages
将 frontend 目录作为静态站点部署即可。
如果使用 Cloudflare 控制台:
- 进入 Cloudflare Dashboard;
- 打开 Workers & Pages;
- 选择 Create application;
- 选择 Pages;
- 连接 GitHub 或直接上传静态文件;
- 构建命令留空;
- 输出目录设置为
frontend; - 部署完成后访问 Pages 域名。
如果使用 Wrangler 部署 Pages:
wrangler pages deploy frontend --project-name cloudflare-kb
部署完成后,你会得到类似地址:
https://cloudflare-kb.pages.dev
打开后即可访问企业知识库页面。
十一、使用 Cloudflare Access 保护知识库
企业知识库通常不应该裸露在公网。最简单的保护方式是使用 Cloudflare Access。
配置思路如下:
- 在 Cloudflare Zero Trust 控制台中创建 Access 应用;
- 应用类型选择 Self-hosted;
- 域名填写你的 Pages 域名或自定义域名,例如:
kb.example.com
- 配置访问策略,例如只允许公司邮箱后缀访问:
Emails ending in: @example.com
- 用户访问知识库时,会先进入 Cloudflare Access 登录页面;
- 验证通过后才能访问知识库内容。
这种方式的好处是,你可以不在应用代码里实现登录系统,也能快速获得企业级访问控制能力。如果企业已经使用 Google Workspace、Azure AD、Okta 等身份提供商,也可以与 Cloudflare Access 集成,实现统一登录。
十二、生产环境优化建议
上面的源码是一个最小可运行版本。如果要用于真实企业内部,可以从以下几个方向继续优化。
1. 后台管理系统
目前示例通过 SQL 写入文档,适合初始化和演示。生产环境建议增加后台功能:
- 新建文档;
- 编辑文档;
- 删除文档;
- 草稿和发布状态;
- 文档标签;
- 文档排序;
- 上传封面或附件;
- 操作日志。
后台接口建议增加身份校验,避免未授权用户修改资料。
2. 文档权限控制
不同部门可能需要不同的可见范围。例如:
- 全员可见;
- 仅研发可见;
- 仅运维可见;
- 仅管理层可见。
可以在 docs 表中增加字段:
visibility TEXT DEFAULT 'public'
或建立文档权限表:
CREATE TABLE doc_permissions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
doc_id INTEGER NOT NULL,
role TEXT NOT NULL
);
然后在 Worker 中读取 Cloudflare Access 传递的用户身份信息,根据用户邮箱、部门或角色过滤文档。
3. 全文搜索
当前示例只搜索标题和摘要。如果文档数量较多,需要全文搜索。可以考虑几种方案:
- 使用 D1 的简单 LIKE 查询;
- 将文档同步到 Meilisearch、Typesense、Algolia;
- 使用 Cloudflare Workers AI 做语义搜索;
- 将文档向量化后存入 Vectorize,实现 AI 知识库问答。
如果只是几百篇内部文档,标题和摘要搜索已经能满足基本需求。
4. 附件和图片管理
知识库经常需要插入图片、流程图、PDF、Excel 等附件。建议使用 Cloudflare R2 存储文件,然后在文档中引用 R2 文件地址。
文件上传流程可以设计为:
用户选择文件
│
前端请求上传签名
│
Worker 生成预签名 URL
│
前端直传 R2
│
文档内容保存附件地址
这种方式可以减少 Worker 中转文件的压力。
5. 缓存热门文档
如果某些文档访问量较高,可以使用 Cache API 或 KV 缓存文档详情,减少 D1 查询次数。例如:
const cacheKey = `doc:${slug}`;
首次读取 D1 后写入 KV,后续直接从 KV 返回。更新文档时再删除缓存。
6. 审计和合规
企业内部系统需要关注数据安全。建议记录以下行为:
- 谁查看了敏感文档;
- 谁修改了文档;
- 谁删除了文档;
- 谁上传了附件;
- 权限何时发生变化。
这些日志可以写入 D1、Logpush 或第三方日志平台。
十三、常见问题
1. Cloudflare D1 适合大型知识库吗?
D1 基于 SQLite,适合轻量级和中等规模应用。如果企业知识库文档数量在几百到几万篇范围内,通常可以满足需求。但如果需要复杂全文搜索、强事务、高并发写入,建议引入专业数据库或搜索系统。
2. 知识库内容能否完全私有?
可以。你可以将 Pages 站点绑定到自定义域名,并使用 Cloudflare Access 限制访问。同时,Worker API 也应该配置 Access 或校验身份,避免用户绕过前端直接访问接口。
3. 是否必须使用前后端分离?
不是。也可以用 Worker 直接返回 HTML 页面,做成单体应用。但前后端分离更利于后续扩展,例如移动端、小程序、桌面端或 AI 助手都可以复用同一套 API。
4. Markdown 是否安全?
Markdown 本身并不等于安全。用户输入的内容经过渲染后可能包含 HTML 或脚本,因此生产环境必须做 XSS 防护。建议在前端或服务端使用成熟的 Markdown 渲染器,并对 HTML 结果进行清洗。
十四、总结
本文介绍了一套基于 Cloudflare 的企业知识库搭建方案,核心组件包括:
- 使用 Cloudflare Pages 托管前端;
- 使用 Cloudflare Workers 提供接口;
- 使用 Cloudflare D1 存储分类和文档;
- 使用 Cloudflare Access 实现企业级访问控制;
- 后续可结合 R2、KV、Vectorize、Workers AI 扩展附件、缓存和智能问答能力。
这套方案最大的优点是部署简单、成本较低、访问速度快,而且天然具备较好的安全和扩展基础。对于希望快速建设内部文档中心、研发知识库、运维手册或产品资料中心的团队来说,是一个非常值得尝试的方向。
如果你只是需要一个轻量级内部知识库,可以直接使用本文源码运行;如果你需要更完整的企业级系统,可以在此基础上继续扩展后台管理、权限体系、全文搜索、附件管理和 AI 问答。Cloudflare 的开发者平台已经足够成熟,完全可以支撑这一类轻量到中型的企业内部应用。