import{_ as i,o as a,c as n,a2 as p}from"./chunks/framework.B-QBKwc1.js";const c=JSON.parse('{"title":"富文本处理","description":"富文本(如文章内容)中的图片/视频链接同样需要处理。","frontmatter":{},"headers":[],"relativePath":"zh/tools/rich-text.md","filePath":"zh/tools/rich-text.md"}'),l={name:"zh/tools/rich-text.md"};function t(h,s,e,k,d,F){return a(),n("div",null,[...s[0]||(s[0]=[p(`
概述
富文本(如文章内容)中的图片/视频链接同样需要处理。项目采用 [IMG_URL] 占位符机制:入库时迁移临时文件并把域名替换为占位符,读取时再还原为当前配置的域名——支持域名变更场景。
┌─────────────────────────────────────────────────────────────────┐
│ 写入流程(add/update) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 前端编辑器插入图片 │
│ │ │
│ ▼ │
│ 上传到临时目录(uploadFile 接口) │
│ │ │
│ ▼ │
│ 提交 HTML 内容到后端 │
│ │ │
│ ▼ │
│ Service(use HasFileProcessing) │
│ processContentFieldsOnSave() │
│ │ │
│ ├─ 解析 <img> 标签的 src │
│ ├─ 解析 <video>/<source>/<embed> 标签的 src │
│ ├─ 解析 <a> 标签的文件 href │
│ │ │
│ ▼ │
│ save_content(&$content, $saveDir) ← 引用传递,原地修改 │
│ │ │
│ ├─ 站内临时文件 → save_file() 迁移至正式目录 │
│ ├─ 正式文件 → 去掉域名 │
│ │ │
│ ▼ │
│ 替换为 [IMG_URL]/相对路径 占位符 │
│ │ │
│ ▼ │
│ 数据库存储含占位符的 HTML │
│ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 读取流程(detail/pageList) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 数据库读取含占位符的 HTML │
│ │ │
│ ▼ │
│ Service.processContentFieldsOnRead() │
│ │ │
│ ├─ 替换 [IMG_URL] 为 config/file.php 的 domain_url │
│ │ 如 https://cdn.example.com │
│ │ │
│ ▼ │
│ 返回完整 URL 的 HTML 给前端 │
│ │
└─────────────────────────────────────────────────────────────────┘class ArticleService extends BaseTenantService
{
// 必须 use HasFileProcessing(否则占位实现直接透传,不处理)
use HasFileProcessing, /* 其他 Trait */;
/**
* 富文本字段
*
* @var array
*/
protected array $contentFields = ['content'];
}配置后自动处理:
processContentFieldsOnSave() → save_content() 处理临时文件与占位符processContentFieldsOnRead() / ...ReadList() 还原占位符未启用 Trait 不生效
BaseService 中的 processContentFieldsOnSave 是占位实现(直接返回原数据)。子类必须 use HasFileProcessing 真实实现才会处理——声明了 contentFields 却不 use,富文本将原样入库。
域名变更场景
假设初始配置 FILE_DOMAIN_URL=https://cdn.example.com,文章内容中图片 URL 为:
https://cdn.example.com/uploads/article/20261002/abc.jpg存储时替换为:
[IMG_URL]/uploads/article/20261002/abc.jpg如果后续域名变更为 https://cdn2.example.com,只需修改 .env 中的 FILE_DOMAIN_URL,已存储的内容无需逐条更新,读取时自动替换为新域名。
save_content() 函数处理以下 HTML 标签:
| 标签 | 属性 | 说明 |
|---|---|---|
<img> | src | 图片 |
<video> | src | 视频 |
<source> | src | 视频源 |
<embed> | src | 嵌入内容 |
<a> | href | 文件链接(仅处理带扩展名的链接) |
// app/Helpers/helpers.php 中的 save_content(来自项目实际代码的函数签名)
/**
* 处理富文本内容:站内临时文件转正、正式文件去掉域名,最终替换为 [IMG_URL] 占位。
*
* @param string $content 富文本 HTML(引用传递,原地修改)
* @param string $saveDir 正式保存子目录
* @return bool 是否发生替换
*/
function save_content(string &$content, string $saveDir = 'article'): bool
{
$config = config('file');
$domainUrl = rtrim($config['domain_url'] ?? '', '/');
if (empty($domainUrl) || empty($content)) {
return false;
}
$urlMap = [];
// 1. 匹配 <img> 标签的 src
save_content_match($content, '/<img[^>]+src=["\\']?([^"\\'>\\s]+)/i', $domainUrl, $saveDir, $urlMap);
// 2. 匹配 <video>/<source>/<embed> 标签的 src
save_content_match($content, '/<(?:video|source|embed)[^>]+src=["\\']?([^"\\'>\\s]+)/i', $domainUrl, $saveDir, $urlMap);
// 3. 匹配 <a> 标签的 href(指向文件的情况)
save_content_match($content, '/<a[^>]+href=["\\']?([^"\\'>\\s]+\\.\\w{2,5})["\\']?/i', $domainUrl, $saveDir, $urlMap);
// 批量替换
if (! empty($urlMap)) {
$content = str_replace(array_keys($urlMap), array_values($urlMap), $content);
}
return true;
}处理规则(来自 save_content 实现注释):
[IMG_URL] 占位[IMG_URL] 占位富文本安全
save_content 只处理本站域名下的 URL,外部 URL 不做处理src 必须是完整 URL(包含域名),否则无法识别Content-Type: application/json(ForceJsonResponse 中间件),内容不会被浏览器按 HTML 渲染与文件字段的分工
fileFields/multiFileFields)→ 结构化字段,见 7.1 文件上传全流程contentFields)→ HTML 内嵌资源,本页说明type 分派,富文本类型为 ueditor)→ HasConfigValueProcessing Trait,见 5.12 系统配置管理前两者由 HasFileProcessing Trait 提供,按属性声明分别启用;配置项不声明字段列表,而是由记录自身的 type 决定处理方式。