Files
rxthinkcmf-wiki/RXThinkCMF_Laravel13_NaiveVue/zh/tools/rich-text.md
T
2026-10-08 17:50:29 +08:00

8.9 KiB
Raw Blame History

7.3 富文本处理

:::tip 概述 富文本(如文章内容)中的图片/视频链接同样需要处理。项目采用 [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 给前端                                     │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

在 Service 中配置

class ArticleService extends BaseTenantService
{
    // 必须 use HasFileProcessing(否则占位实现直接透传,不处理)
    use HasFileProcessing, /* 其他 Trait */;

    /**
     * 富文本字段
     *
     * @var array
     */
    protected array $contentFields = ['content'];
}

配置后自动处理:

  • add/update:processContentFieldsOnSave() → save_content() 处理临时文件与占位符
  • detail/pageList/allList:processContentFieldsOnRead() / ...ReadList() 还原占位符

:::warning 未启用 Trait 不生效 BaseService 中的 processContentFieldsOnSave 是占位实现(直接返回原数据)。子类必须 use HasFileProcessing 真实实现才会处理——声明了 contentFields 却不 use,富文本将原样入库。 :::

为什么使用占位符?

:::tip 域名变更场景 假设初始配置 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] 占位
  • 外部 URL → 不处理(保持原样)

注意事项

:::warning 富文本安全

  • 富文本内容可能包含 XSS 攻击代码,建议在前端使用编辑器的安全模式(粘贴过滤)
  • save_content 只处理本站域名下的 URL,外部 URL 不做处理
  • 图片标签的 src 必须是完整 URL(包含域名),否则无法识别
  • 响应统一 Content-Type: application/json(ForceJsonResponse 中间件),内容不会被浏览器按 HTML 渲染 :::

:::tip 与文件字段的分工

  • 单文件/多文件字段(fileFields/multiFileFields)→ 结构化字段,见 7.1 文件上传全流程
  • 富文本字段(contentFields)→ HTML 内嵌资源,本页说明
  • 配置项的 value(按 type 分派,富文本类型为 ueditor)→ HasConfigValueProcessing Trait,见 5.12 系统配置管理

前两者由 HasFileProcessing Trait 提供,按属性声明分别启用;配置项不声明字段列表,而是由记录自身的 type 决定处理方式。 :::