☰
Vue 2 后台富文本编辑器实战:vue-quill-editor 集成与图片上传、粘贴清洗避坑指南
2026/10/13 1:04:13 网站建设 项目流程

1. 为什么后台项目总绕不开富文本编辑器

做过管理后台的朋友大概率都碰到过这个需求:运营同学跑过来说,商品详情页要能加粗、要能插入图片、要能排版,最好还能贴表格。你打开需求文档一看,好家伙,一个详情页描述字段,硬生生被产品经理写成了"支持图文混排、支持标题层级、支持列表、支持链接"。

这时候你面临的选择其实不多。要么自己拿contenteditable手搓一个,要么找个现成的轮子。手搓的结局我见过太多次了——第一周勉强能用,第二周开始处理光标位置,第三周处理粘贴 Word 文档的样式污染,第四周直接放弃重写。所以绝大多数 Vue 项目最后都会落到一个方案上:vue-quill-editor。

这篇文章要聊的就是这个组件在真实 Vue 项目里怎么用、怎么配、怎么避坑。它基于 Quill 这个老牌富文本内核封装,Vue 2 时代几乎是后台项目的标配。我会从选型逻辑讲到完整代码,再到图片上传、粘贴清洗、内容回显这些实际开发中一定会遇到的环节。适合正在做后台系统、CMS、内容管理模块的前端同学,也适合刚接手一个老项目、发现里面用了这个组件想搞明白它脾气的人。

先说清楚它的定位:轻量、够用、上手快。它不是那种功能堆到天花板的编辑器,但胜在体积小、API 直白、和 Vue 的绑定方式自然。你要是指望它开箱即用支持协同编辑、版本对比,那确实想多了;但你要的只是"让运营能排版一段富文本存进数据库",它完全胜任。

2. 选型之前先想清楚:富文本编辑器到底在选什么

2.1 富文本编辑器的三个核心维度

很多人选编辑器只看"功能多不多",这是个误区。我踩过坑之后总结下来,真正决定你后期维护成本的是这三个维度。

第一个维度是数据格式。编辑器吐出来的到底是 HTML 字符串,还是它自己的一套 Delta 结构,还是 Markdown?这直接决定了你后端怎么存、前端怎么回显、以后换编辑器要不要做数据迁移。Quill 默认用的是 Delta(一种 JSON 描述文档结构的格式),但它也支持直接拿 HTML 初始化,所以灵活性还行。

第二个维度是扩展方式。你迟早会遇到"默认工具栏不够用"的情况,比如要加一个自定义按钮、要限制某些格式。这时候编辑器有没有清晰的扩展接口就很关键。Quill 的模块化设计在这点上做得不错,工具栏、快捷键、粘贴处理都能单独配置。

第三个维度是粘贴处理能力。这是最容易被低估、又最容易出问题的地方。运营从 Word 或者别的网页复制一段内容进来,带进来一堆style、class、font标签,你的详情页样式瞬间崩掉。编辑器能不能在粘贴时做清洗,直接决定了你的页面会不会变成一锅粥。

2.2 为什么 vue-quill-editor 在 Vue 2 项目里这么常见

vue-quill-editor 本质上是把 Quill 包装成了一个 Vue 组件,通过v-model双向绑定内容。它的优势在于和 Vue 的响应式体系贴合得比较自然,你不需要手动去监听text-change事件再同步数据,组件帮你做了。

对比几个常见方案:wangEditor 更偏国产化、中文文档友好,但早期版本和 Vue 的集成需要自己包一层;TinyMCE 功能强但体积大、商用还有授权问题;CKEditor 5 架构现代但学习曲线陡。vue-quill-editor 卡在一个很舒服的位置——功能够用、体积可控、集成成本低。

注意:vue-quill-editor 主要面向 Vue 2。如果你在做 Vue 3 项目,官方推荐的是@vueup/vue-quill这类社区维护的版本,API 有差异,别直接照搬本文代码。

2.3 一个容易被忽略的决策点:内容存储格式

我见过不少项目在这里翻车。有人图省事,直接把编辑器输出的 HTML 原样存进数据库,结果后来要做内容检索、要做多端渲染(比如小程序端),发现 HTML 里全是内联样式,解析起来痛苦不堪。

我的建议是:如果只是后台展示,存 HTML 没问题;如果内容要跨端复用,优先考虑存 Delta 或者做一层结构化处理。Quill 的 Delta 是纯 JSON,结构清晰,转成其他格式也方便。当然这取决于你的后端设计,前端这边要做的就是确认好v-model绑定的到底是 HTML 还是 Delta。

3. 环境搭建与基础集成:从零跑起来

3.1 依赖安装与版本选择

安装本身没什么难度,但版本这块要留个心眼。

npm install vue-quill-editor --save npm install quill --save

这里有个细节:vue-quill-editor 依赖 Quill,但它的package.json里对 Quill 的版本约束可能比较宽松。我遇到过装完之后样式错乱的情况,排查半天发现是 Quill 被提升到了不兼容的大版本。稳妥的做法是显式指定 Quill 的版本,比如锁在 1.x 系列,和 vue-quill-editor 匹配。

npm install quill@1.3.7 --save

如果你用的是 Vue CLI 创建的项目,装完直接就能用。如果是自己搭的 webpack 环境,注意 Quill 的样式文件要单独引入,不然工具栏会变成一堆没有样式的按钮。

3.2 全局注册还是局部注册

两种方式我都用过,各有场景。

全局注册适合"整个后台到处都要用编辑器"的情况,在main.js里一次性搞定:

import Vue from 'vue' import VueQuillEditor from 'vue-quill-editor' import 'quill/dist/quill.core.css' import 'quill/dist/quill.snow.css' import 'quill/dist/quill.bubble.css' Vue.use(VueQuillEditor)

局部注册适合"只有某几个页面用"的情况,在组件里按需引入,能减小打包体积:

import { quillEditor } from 'vue-quill-editor' import 'quill/dist/quill.snow.css' export default { components: { quillEditor } }

提示:quill.snow.css是工具栏主题,quill.bubble.css是气泡主题,quill.core.css是核心样式。三个都引不会报错,但按需引入更干净。用 snow 主题就只引 snow 那个。

3.3 最小可用示例

先跑一个最简版本,确认环境没问题,再往上加功能。

<template> <div class="editor-wrapper"> <quill-editor v-model="content" :options="editorOptions" @change="onEditorChange" /> </div> </template> <script> export default { data() { return { content: '<p>在这里输入内容...</p>', editorOptions: { theme: 'snow', placeholder: '请输入内容', modules: { toolbar: [ ['bold', 'italic', 'underline', 'strike'], ['blockquote', 'code-block'], [{ header: 1 }, { header: 2 }], [{ list: 'ordered' }, { list: 'bullet' }], [{ color: [] }, { background: [] }], ['link', 'image'], ['clean'] ] } } } }, methods: { onEditorChange({ html, text }) { // html 是富文本内容,text 是纯文本 console.log('当前 HTML:', html) } } } </script> <style scoped> .editor-wrapper { min-height: 300px; } </style>

这段代码跑起来,你就能看到一个带工具栏的编辑器了。v-model绑定的content会随着输入实时更新,@change事件能拿到 HTML 和纯文本两个版本。

3.4 工具栏配置的取舍逻辑

上面那段toolbar数组不是随便写的,每一项都有考虑。

['bold', 'italic', 'underline', 'strike']是基础行内格式,几乎必留。['blockquote', 'code-block']看场景,如果是技术文档类内容就留着,纯商品描述可以砍掉。[{ header: 1 }, { header: 2 }]只给了两级标题,因为后台内容通常不需要 H3 以下的层级,给多了反而让运营乱用。

['link', 'image']这两个是重点,也是坑最多的。link相对简单,image就复杂了——默认行为是把图片转成 base64 塞进内容里,内容一长,数据库字段直接爆掉。这个后面单独讲。

['clean']是清除格式按钮,强烈建议保留。运营粘贴内容后格式乱了,有个一键清除的入口能省很多事。

4. 图片上传:默认行为为什么不能用

4.1 base64 方案的致命问题

Quill 默认的图片处理是把图片读成 base64 字符串,直接内联到 HTML 里。小图标还行,一张几百 KB 的图转成 base64 之后体积膨胀约三分之一,再存进数据库,一个字段几 MB 是常事。

我见过一个项目,商品详情页字段用的是TEXT类型,结果运营传了几张高清图,直接超出字段长度限制,保存报错。后来改成LONGTEXT勉强撑住,但查询性能肉眼可见地下降。

所以生产环境必须把图片改成上传到服务器、返回 URL 的方式。核心思路是重写 Quill 的图片处理逻辑,拦截默认的 base64 行为。

4.2 自定义图片处理器的实现

Quill 的工具栏按钮可以绑定自定义 handler。我们接管image按钮,弹出一个文件选择框,选完上传,拿到 URL 再插入编辑器。

editorOptions: { theme: 'snow', modules: { toolbar: { container: [ ['bold', 'italic', 'underline'], [{ list: 'ordered' }, { list: 'bullet' }], ['link', 'image'], ['clean'] ], handlers: { image: function () { // this 指向 Quill 实例 const quill = this.quill const input = document.createElement('input') input.setAttribute('type', 'file') input.setAttribute('accept', 'image/*') input.click() input.onchange = async () => { const file = input.files[0] if (!file) return // 这里调用你的上传接口 const imageUrl = await uploadImage(file) // 拿到 URL 后插入到光标位置 const range = quill.getSelection(true) quill.insertEmbed(range.index, 'image', imageUrl) quill.setSelection(range.index + 1) } } } } } }

uploadImage是你自己的上传函数,用FormData把文件发给后端,返回图片地址。这里要注意quill.getSelection(true)的参数true,它表示如果当前没有选区,就把焦点放到编辑器末尾,避免插入位置出错。

4.3 上传函数的完整写法

function uploadImage(file) { return new Promise((resolve, reject) => { const formData = new FormData() formData.append('file', file) // 假设你用的是 axios axios.post('/api/upload/image', formData, { headers: { 'Content-Type': 'multipart/form-data' } }).then(res => { if (res.data && res.data.url) { resolve(res.data.url) } else { reject(new Error('上传返回格式异常')) } }).catch(err => { reject(err) }) }) }

注意:上传接口的返回结构一定要和后端约定清楚。我遇到过返回{ code: 0, data: { path: '...' } }的,也遇到过直接返回字符串的,前端解析逻辑要跟着调整,别想当然。

4.4 图片大小与格式的前置校验

别等上传完了才发现图片太大。在input.onchange里先做校验,体验会好很多。

const MAX_SIZE = 5 * 1024 * 1024 // 5MB const ALLOW_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'] if (!ALLOW_TYPES.includes(file.type)) { alert('只支持 JPG、PNG、GIF、WebP 格式') return } if (file.size > MAX_SIZE) { alert('图片不能超过 5MB') return }

这两个阈值不是拍脑袋定的。5MB 是大多数后端上传接口的默认上限,格式限制则是为了避开一些浏览器兼容性差的格式。如果你的业务需要支持更大的图,记得同步调整后端配置。

5. 粘贴清洗:让运营的复制粘贴不再毁页面

5.1 粘贴污染的典型表现

运营从某个网页复制一段带样式的文字,粘进编辑器,你会发现:字体变了、行距乱了、还带了一堆莫名其妙的背景色。更离谱的是从 Word 复制,会带进来mso-开头的私有样式,页面上看着还行,一存进数据库再渲染出来就完全不是那个样子。

Quill 本身对粘贴有一定处理,但默认策略比较宽松,很多内联样式会被保留。我们需要主动配置剪贴板模块来做清洗。

5.2 配置 clipboard 模块做格式过滤

editorOptions: { theme: 'snow', modules: { clipboard: { // 只允许这些格式的标签通过 allowed: { tags: ['p', 'br', 'strong', 'em', 'u', 's', 'blockquote', 'ol', 'ul', 'li', 'h1', 'h2', 'a', 'img'], attributes: ['href', 'src', 'alt', 'width', 'height'] }, // 匹配到这些规则时做转换 matchers: [ ['SPAN', (node, delta) => { // 把 span 拆掉,只保留里面的文字 return delta }] ] } } }

allowed里的tags是白名单,不在列表里的标签会被剥离,只保留文字内容。attributes同理,只保留这几个属性,style、class这些统统丢掉。

matchers更灵活,可以针对特定标签做自定义处理。上面这个例子是把span标签拆掉——因为很多网页的样式都挂在span上,拆掉它基本就干净了。

5.3 处理粘贴图片的特殊情况

有个场景要单独说:用户直接截图然后粘贴。这时候剪贴板里是图片数据,不是 URL。默认情况下 Quill 会把它转成 base64,又回到老问题。

解决办法是监听粘贴事件,检测到图片数据就走上传流程。

mounted() { this.$refs.myQuillEditor.quill.root.addEventListener('paste', (e) => { const clipboardData = e.clipboardData if (!clipboardData || !clipboardData.items) return for (let i = 0; i < clipboardData.items.length; i++) { const item = clipboardData.items[i] if (item.type.indexOf('image') !== -1) { e.preventDefault() const file = item.getAsFile() // 走上传逻辑,拿到 URL 后插入 uploadImage(file).then(url => { const quill = this.$refs.myQuillEditor.quill const range = quill.getSelection(true) quill.insertEmbed(range.index, 'image', url) }) } } }) }

这段代码要放在mounted里,因为需要拿到真实的 DOM 节点。this.$refs.myQuillEditor.quill是 Quill 实例,.root是编辑区域的 DOM。

提示:e.preventDefault()一定要加,否则默认的 base64 插入行为还会执行,你会看到图片被插了两次。

6. 内容回显与数据同步的那些坑

6.1 编辑场景下的内容初始化

新增和编辑是两个不同的场景。新增时content是空字符串或者一段占位内容;编辑时要把后端返回的 HTML 塞进去。

看起来很简单,但有个时序问题:如果content是在异步请求返回后才赋值的,编辑器可能已经初始化完了,这时候直接改content不一定能正确渲染。

// 推荐做法:等数据回来再渲染编辑器 <quill-editor v-if="contentLoaded" v-model="content" :options="editorOptions" /> // 请求数据 fetchDetail().then(res => { this.content = res.data.content this.contentLoaded = true })

用v-if控制编辑器在数据准备好之后再渲染,能避开大部分初始化时序问题。代价是页面会有一个短暂的空白,但比内容显示错乱要好。

6.2 v-model 的更新时机

v-model在 vue-quill-editor 里绑定的是@input事件,但 Quill 的内容变化触发频率很高,每次输入都会触发。如果你在@change里做了比较重的操作(比如实时保存),要注意做防抖。

import { debounce } from 'lodash' methods: { onEditorChange: debounce(function ({ html }) { this.content = html // 这里可以做自动保存 }, 500) }

500ms 是个经验值,既能保证内容及时同步,又不会因为频繁触发拖慢输入体验。具体数值可以根据你的保存接口耗时调整。

6.3 纯文本与 HTML 的取舍

@change事件同时给了html和text。什么时候用哪个?

如果后端存的是 HTML,那v-model绑定的就是 HTML,直接用。如果后端需要纯文本做摘要或者搜索,就用text。我一般会在提交表单时同时带上两个字段,HTML 用于渲染,纯文本用于列表页的简介展示。

submitForm() { const payload = { contentHtml: this.content, contentText: this.content.replace(/<[^>]+>/g, '').slice(0, 100) } // 提交... }

那个正则replace(/<[^>]+>/g, '')是粗暴地去掉所有标签,对于简单场景够用。如果内容里有<这种转义字符,可能会误伤,更严谨的做法是用 DOM 解析,但一般后台场景没必要上那么重。

7. 常见问题排查速查表

实际开发中遇到的问题五花八门,我把高频的整理成一张表,方便对照排查。

问题现象可能原因解决方向
工具栏样式丢失没引入 quill 的 css引入quill.snow.css
编辑器高度不可控默认高度由内容撑开用外层容器设min-height,或自定义.ql-editor样式
图片插入后是 base64用了默认 image handler重写 handler 走上传接口
粘贴内容样式混乱剪贴板未做过滤配置clipboard.allowed白名单
编辑时内容不显示初始化时序问题用v-if等数据回来再渲染
输入卡顿change 事件里做了重操作加防抖,或把重操作移到提交时
内容超出字段长度图片存了 base64改上传方案,或扩大字段类型
光标位置错乱插入内容后没重设选区插入后调用setSelection

7.1 编辑器高度控制的正确姿势

Quill 默认高度是跟着内容走的,内容少的时候编辑器就一小条,体验很差。常见的做法是给编辑区域设一个最小高度。

.editor-wrapper ::v-deep .ql-editor { min-height: 300px; }

注意::v-deep,因为 Quill 的 DOM 是动态生成的,scoped 样式默认作用不到。Vue 2 里用::v-deep或者/deep/,Vue 3 里用:deep()。

7.2 内容为空时的判断

提交表单前要校验内容是否为空。但编辑器里如果只有<p><br></p>这种,content不是空字符串,直接判断!content会失效。

function isContentEmpty(html) { if (!html) return true const text = html.replace(/<[^>]+>/g, '').replace(/&nbsp;/g, '').trim() return text.length === 0 }

先把标签去掉,再把&nbsp;替换掉,最后 trim,这样判断才准。

7.3 一个我踩过的坑:SSR 环境下的报错

如果你的项目用了服务端渲染,直接在组件里引入 vue-quill-editor 会报document is not defined,因为 Quill 依赖浏览器环境。

解决办法是用动态导入,把编辑器组件放到客户端才加载。

components: { quillEditor: () => import('vue-quill-editor').then(m => m.quillEditor) }

或者用<client-only>包裹(Nuxt 场景)。这个坑不常遇到,但一旦遇到,报错信息很迷惑,容易卡很久。

8. 一些让代码更稳的工程化建议

8.1 把编辑器封装成业务组件

不要在每个页面里重复写一遍editorOptions和上传逻辑。抽一个RichEditor.vue出来,把配置、上传、校验都收进去,对外只暴露v-model和几个必要的 props。

<!-- RichEditor.vue --> <template> <quill-editor v-model="innerContent" :options="mergedOptions" @change="handleChange" /> </template> <script> export default { name: 'RichEditor', props: { value: { type: String, default: '' }, placeholder: { type: String, default: '请输入内容' }, minHeight: { type: Number, default: 300 } }, // ... } </script>

这样业务页面用起来就是一行<rich-editor v-model="form.content" />,清爽很多。上传接口地址、图片大小限制这些也可以做成 props 或者全局配置,方便统一调整。

8.2 上传接口的容错处理

上传失败是常态,网络抖动、文件太大、后端限流都可能。上传函数里一定要有失败回调,给用户明确提示,并且把编辑器状态恢复干净。

try { const url = await uploadImage(file) quill.insertEmbed(range.index, 'image', url) } catch (err) { this.$message.error('图片上传失败,请重试') // 不要插入任何内容,保持编辑器原状 }

我见过上传失败后还往编辑器里插了个空img标签的写法,结果用户保存时带了个坏图,页面渲染出个裂图图标,很尴尬。

8.3 内容安全:XSS 防护不能省

富文本内容最终是要渲染到页面上的,如果后端不做过滤,前端直接v-html渲染,就存在 XSS 风险。用户(或者恶意攻击者)可以在内容里塞<script>或者onerror事件。

前端这边能做的是在渲染时做一层过滤,用 DOMPurify 这类库清洗一遍。

import DOMPurify from 'dompurify' const safeHtml = DOMPurify.sanitize(rawHtml)

当然,根本的防护要在后端做,前端过滤只是补充。但作为前端,知道这个风险点、在渲染环节加一道防线,是基本素养。

8.4 关于性能的一点观察

Quill 在内容量不大的时候性能很好,但如果一篇文章有几万字、几十张图,编辑器的输入延迟会明显上升。这是富文本编辑器的通病,不是 vue-quill-editor 独有的问题。

如果你的业务确实有超长内容的需求,可以考虑分块编辑,或者用 Markdown 编辑器替代。但对于绝大多数后台场景,几千字的内容量,Quill 完全扛得住,不用过度优化。

9. 完整前端代码整合

把前面讲的东西串起来,给一份可以直接参考的完整组件代码。

<template> <div class="rich-editor"> <quill-editor v-if="ready" ref="myQuillEditor" v-model="innerContent" :options="editorOptions" @change="onChange" /> </div> </template> <script> import { quillEditor } from 'vue-quill-editor' import 'quill/dist/quill.snow.css' import axios from 'axios' const MAX_IMAGE_SIZE = 5 * 1024 * 1024 const ALLOW_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'] export default { name: 'RichEditor', components: { quillEditor }, props: { value: { type: String, default: '' }, placeholder: { type: String, default: '请输入内容' }, uploadUrl: { type: String, default: '/api/upload/image' } }, data() { return { ready: false, innerContent: this.value, editorOptions: { theme: 'snow', placeholder: this.placeholder, modules: { toolbar: { container: [ ['bold', 'italic', 'underline', 'strike'], [{ header: 1 }, { header: 2 }], [{ list: 'ordered' }, { list: 'bullet' }], ['blockquote', 'link', 'image'], ['clean'] ], handlers: { image: this.handleImageClick } }, clipboard: { allowed: { tags: ['p', 'br', 'strong', 'em', 'u', 's', 'blockquote', 'ol', 'ul', 'li', 'h1', 'h2', 'a', 'img'], attributes: ['href', 'src', 'alt'] } } } } } }, watch: { value(val) { if (val !== this.innerContent) { this.innerContent = val } } }, mounted() { this.ready = true this.$nextTick(() => { this.bindPasteEvent() }) }, methods: { onChange({ html }) { this.$emit('input', html) this.$emit('change', html) }, handleImageClick() { const quill = this.$refs.myQuillEditor.quill const input = document.createElement('input') input.type = 'file' input.accept = 'image/*' input.click() input.onchange = async () => { const file = input.files[0] if (!file) return if (!this.validateImage(file)) return try { const url = await this.uploadImage(file) const range = quill.getSelection(true) quill.insertEmbed(range.index, 'image', url) quill.setSelection(range.index + 1) } catch (e) { this.$emit('upload-error', e) } } }, validateImage(file) { if (!ALLOW_IMAGE_TYPES.includes(file.type)) { this.$emit('upload-error', new Error('图片格式不支持')) return false } if (file.size > MAX_IMAGE_SIZE) { this.$emit('upload-error', new Error('图片超过大小限制')) return false } return true }, uploadImage(file) { return new Promise((resolve, reject) => { const formData = new FormData() formData.append('file', file) axios.post(this.uploadUrl, formData, { headers: { 'Content-Type': 'multipart/form-data' } }).then(res => { const url = res.data && (res.data.url || res.data.path) url ? resolve(url) : reject(new Error('返回格式异常')) }).catch(reject) }) }, bindPasteEvent() { const quill = this.$refs.myQuillEditor.quill quill.root.addEventListener('paste', (e) => { const items = e.clipboardData && e.clipboardData.items if (!items) return for (let i = 0; i < items.length; i++) { if (items[i].type.indexOf('image') !== -1) { e.preventDefault() const file = items[i].getAsFile() if (!this.validateImage(file)) return this.uploadImage(file).then(url => { const range = quill.getSelection(true) quill.insertEmbed(range.index, 'image', url) quill.setSelection(range.index + 1) }).catch(err => { this.$emit('upload-error', err) }) } } }) } } } </script> <style scoped> .rich-editor ::v-deep .ql-editor { min-height: 300px; font-size: 14px; line-height: 1.6; } .rich-editor ::v-deep .ql-toolbar { border-top-left-radius: 4px; border-top-right-radius: 4px; } .rich-editor ::v-deep .ql-container { border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; } </style>

这份代码把上传、粘贴、校验、样式都收进去了,业务页面只需要<rich-editor v-model="form.content" @upload-error="handleError" />就能用。上传失败的提示交给父组件处理,组件本身保持纯粹。

10. 关于这个组件我个人的几点体会

用了几年下来,vue-quill-editor 给我的感觉是"够用但不惊艳"。它不会让你在集成阶段花太多时间,但真正的内容处理逻辑——上传、清洗、校验——还是得自己写。指望它开箱即用解决所有问题,最后一定会失望。

我现在的习惯是,只要项目里出现富文本需求,先花半小时把上传和粘贴清洗这两块搭好,再开始写业务。这两块是后期最容易出问题的地方,前期做扎实,后面能省很多返工。

另外提醒一句,如果你的项目已经在往 Vue 3 迁移,别在这个组件上投入太多定制化改造,迁移成本会很高。早点评估替代方案,把定制逻辑抽成独立的工具函数,迁移时能少改不少代码。

至于内容存储格式,我还是那句话:能存结构化数据就别存 HTML。HTML 看着方便,但一旦要做跨端渲染或者内容分析,你会发现它是个负担。Delta 或者自定义的 JSON 结构,前期多花点设计时间,后期省心得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询