news 2026/9/28 7:51:30

Univer 在线表格协同编辑引擎:Canvas 渲染与 Facade API 实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Univer 在线表格协同编辑引擎:Canvas 渲染与 Facade API 实战

1. 从“univer”这个标题说起:它到底是什么,能解决什么问题

第一次看到“univer”这个词,很多人会以为是“universe”的缩写,或者某个开源社区的新玩具。实际上,Univer 是一套面向在线表格、文档、幻灯片场景的前端协同编辑解决方案,核心定位是“可嵌入的办公套件引擎”。它把电子表格、富文本文档、演示文稿这三类最常见的办公形态,做成了一套可以按需引入的 SDK,让开发者不用从零去写一个 Excel 或者 Word 的网页版,而是直接在自己的系统里嵌入一个功能完整的编辑器。

这件事的价值在哪里?做过企业级管理系统的人都知道,业务里最难缠的需求往往不是增删改查,而是“用户想要一个像 Excel 一样的表格”。财务要做预算表、运营要做数据看板、HR 要做花名册,这些需求背后都指向同一个东西:一个支持公式、支持多 Sheet、支持格式、支持协同的表格组件。市面上能选的方案不多,要么是商业授权费用高得离谱,要么是开源方案功能残缺、维护停滞。Univer 切入的正是这个空档。

它适合谁来用?我梳理了一下,大致是三类人。第一类是前端工程师,尤其是做 B 端 SaaS、低代码平台、数据中台的那批人,他们需要在产品里嵌入表格或文档能力;第二类是技术负责人或架构师,在选型阶段评估“自研还是接入现成方案”;第三类是独立开发者,想快速做一个在线协作工具的原型。这三类人的共同诉求是:不想重复造轮子,但又要对轮子有足够的控制权。

Univer 的技术底座值得单独说一句。它用 Canvas 做渲染层,而不是传统的 DOM 表格。这个选择直接决定了它的性能上限和交互体验。DOM 表格在几百行数据时就开始卡,而 Canvas 渲染可以轻松扛住几万行甚至更多。同时它提供了 Facade API,把底层复杂的渲染逻辑、数据模型、命令系统封装成一套相对友好的接口,让业务层不用关心 Canvas 的绘制细节。再加上 Node.js 生态的配合,服务端可以做协同计算、文件导入导出、公式求值等重活。

提示:Univer 不是“开箱即用的在线 Excel 网站”,它是一套 SDK。你要把它嵌到自己的项目里,需要写代码、做集成、处理数据流。如果你的诉求是“今天就要一个能用的在线表格”,那它可能不是最省事的选择;但如果你的诉求是“我要一个能深度定制、能和我现有系统打通的表格引擎”,那它值得认真看。

2. 整体架构拆解:为什么是 Canvas + Facade API + Node.js 这套组合

2.1 Canvas 渲染引擎:性能与交互的底层逻辑

先说 Canvas 这条路。网页上做表格,传统做法是用<table>或者<div>拼,每个单元格是一个 DOM 节点。这种方案的好处是天然支持文本选择、无障碍访问、CSS 样式,但坏处也很明显:DOM 节点数量一上去,浏览器的布局和重绘开销就爆炸。一个 1000 行 × 20 列的表格就是 2 万个节点,滚动的时候浏览器要不停计算每个节点的位置,帧率直接掉到个位数。

Canvas 的思路完全不同。整个表格就是一张画布,所有单元格、边框、文字、选中高亮都画在这张画布上。浏览器只需要维护一个 Canvas 元素,节点数量恒定。滚动的时候,Univer 根据可视区域计算出需要绘制的单元格范围,只画看得见的部分,这就是所谓的“虚拟化渲染”。我实测过,在同样的机器上,DOM 方案在 5000 行左右开始明显卡顿,而 Canvas 方案在 5 万行时滚动依然跟手。

但 Canvas 不是没有代价。最大的问题是“一切都要自己画”。文本换行、光标定位、选区高亮、滚动条、右键菜单,这些在 DOM 里免费的东西,在 Canvas 里都要手动实现。Univer 为此构建了一套完整的渲染管线:从数据模型到布局计算,再到绘制指令,最后落到 Canvas 上下文。这套管线的好处是可控性极强,你可以精确控制每一个像素的绘制;坏处是学习曲线陡峭,想深度定制渲染效果,得先理解它的渲染分层。

Univer 的渲染大致分三层。最底层是 Canvas 绘制层,负责实际的像素输出;中间是视图层,管理滚动、缩放、选区这些交互状态;最上层是渲染调度层,决定什么时候重绘、重绘哪些区域。这种分层设计的好处是,当你只需要改一个单元格的样式时,不需要整表重绘,只触发局部刷新。对于协同编辑场景,这一点尤其重要,因为别人的光标移动、单元格修改都会触发重绘,如果每次都全量刷新,性能根本扛不住。

2.2 Facade API:把复杂留给自己,把简单留给业务

Facade API 是 Univer 对外暴露的核心接口层。Facade 这个词在软件工程里是“门面模式”的意思,说白了就是给一套复杂的子系统套一个简单的壳。Univer 底层有数据模型、命令系统、渲染引擎、插件体系,如果让业务代码直接操作这些,学习成本极高,而且一旦底层升级,业务代码全得改。Facade API 的作用就是隔离这层复杂度。

举个例子。你想在表格里插入一行,底层可能涉及:修改数据模型的行列索引、触发布局重算、通知渲染层重绘、更新选区状态、记录撤销栈。这一串操作如果让业务自己写,很容易漏掉某一步导致状态不一致。Facade API 把它封装成一个insertRow方法,你调用就行,内部帮你把该做的都做了。这就是门面模式的价值:降低认知负担,减少出错概率。

Facade API 的设计还有一个考量是“面向场景”。它不是把底层 API 简单包一层,而是按照业务场景重新组织。比如“获取当前选区”“设置单元格值”“合并单元格”“冻结行列”这些操作,都是业务开发者高频使用的。这种设计思路让 API 更贴近实际需求,而不是让开发者去猜底层怎么用。

注意:Facade API 虽然简化了操作,但不意味着你可以完全不懂底层。当遇到性能问题或者需要深度定制时,还是得往下钻。我的建议是,先用 Facade API 把功能跑通,等遇到瓶颈了再去研究底层实现。

2.3 Node.js 在协同场景中的角色

很多人以为 Univer 是纯前端方案,其实 Node.js 在协同编辑场景里扮演了关键角色。在线表格的协同,核心难点不是“把数据同步给所有人”,而是“如何处理并发冲突”。两个人同时改同一个单元格,谁赢?A 在 B 的基础上改了公式,B 又改了数值,最后应该是什么结果?

Univer 的协同方案通常配合服务端来做。Node.js 在这里的职责包括:维护文档的权威状态、处理操作变换、做冲突消解、持久化数据、管理用户权限。为什么选 Node.js 而不是 Java 或 Go?一个现实原因是前端团队更容易上手,前后端可以共享一部分逻辑代码,比如公式解析、数据校验。另一个原因是 Node.js 的异步 IO 模型适合处理大量并发的 WebSocket 连接,而协同编辑本质上就是长连接 + 消息广播。

Node.js 还负责文件导入导出。用户上传一个 Excel 文件,服务端需要解析它、转换成 Univer 的数据格式、再推送给前端。导出的时候反过来,把 Univer 的数据模型序列化成 Excel 文件。这些操作涉及大量的二进制处理和格式转换,Node.js 生态里有成熟的库可以用。

2.4 插件化架构:按需引入,不背多余包袱

Univer 的插件化设计是我比较欣赏的一点。它把功能拆成一个个插件:公式插件、协同插件、导入导出插件、条件格式插件、图表插件等等。你用什么就装什么,不用为不需要的功能买单。这对打包体积和运行时性能都有好处。

插件化的另一个好处是扩展性。如果你的业务有特殊需求,比如自定义一个函数、自定义一种单元格类型,可以写自己的插件挂上去。这种开放性让 Univer 不只是一个“现成的表格”,而是一个“可以长大的表格平台”。

3. 核心实操:从零搭建一个 Univer 表格应用

3.1 环境准备与依赖安装

先把环境搭起来。Univer 是 TypeScript 写的,所以你的项目最好也是 TS 环境。Node.js 版本建议 18 LTS 以上,我用的 18.20.4,实测稳定。包管理用 npm、pnpm、yarn 都行,我个人习惯 pnpm,速度快、磁盘占用小。

创建一个新项目,初始化之后安装核心依赖:

pnpm init pnpm add @univerjs/core @univerjs/ui @univerjs/sheets @univerjs/sheets-ui pnpm add -D vite typescript

这里解释一下这几个包的分工。@univerjs/core是核心运行时,提供数据模型、命令系统、插件机制;@univerjs/ui是通用 UI 组件,比如工具栏、菜单、弹窗;@univerjs/sheets是表格的数据逻辑;@univerjs/sheets-ui是表格的界面渲染。如果你还需要公式,加@univerjs/sheets-formula;需要协同,加@univerjs/sheets-collaboration。

提示:Univer 的包版本更新比较快,安装时注意各包版本要对齐,否则容易出现 API 不匹配的问题。我一般会在 package.json 里锁定版本号,避免自动升级带来的意外。

3.2 初始化一个最小可用的表格

环境好了,写一个最小的表格实例。核心步骤是:创建 Univer 实例、注册插件、挂载到 DOM。

import { Univer, LocaleType, merge } from '@univerjs/core'; import { UniverSheetsPlugin } from '@univerjs/sheets'; import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui'; import { UniverUIPlugin } from '@univerjs/ui'; const univer = new Univer({ locale: LocaleType.ZH_CN, theme: 'default', }); univer.registerPlugin(UniverUIPlugin, { container: 'app', }); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverSheetsUIPlugin); univer.createUnit('workbook', { id: 'my-workbook', sheetOrder: ['sheet-01'], sheets: { 'sheet-01': { id: 'sheet-01', name: 'Sheet1', rowCount: 1000, columnCount: 26, cellData: { 0: { 0: { v: 'Hello Univer' }, 1: { v: 123 }, }, }, }, }, });

这段代码跑起来,页面上就会出现一个表格,第一行第一列显示“Hello Univer”,第二列显示 123。看起来简单,但背后做了很多事:创建了数据模型、初始化了渲染引擎、绑定了键盘和鼠标事件、建立了命令系统。

这里有个细节值得说:createUnit的第二个参数就是工作簿的初始数据。sheetOrder定义了 Sheet 的排列顺序,sheets里是每个 Sheet 的具体内容。cellData用行列索引定位单元格,v是值。这种数据结构比二维数组更灵活,因为不是每个单元格都有内容,用对象存储可以节省内存。

3.3 用 Facade API 操作表格数据

表格跑起来了,接下来是业务操作。Facade API 是日常开发用得最多的接口。假设我们要实现一个功能:读取当前选中的单元格,把它的值改成大写。

const facade = univer.getActiveUnit(); const sheet = facade.getActiveSheet(); // 获取当前选区 const selection = sheet.getSelection(); const range = selection.getActiveRange(); // 遍历选区内的单元格 for (let row = range.startRow; row <= range.endRow; row++) { for (let col = range.startColumn; col <= range.endColumn; col++) { const cell = sheet.getCell(row, col); if (cell && typeof cell.v === 'string') { sheet.setCellValue(row, col, cell.v.toUpperCase()); } } }

这段代码展示了 Facade API 的基本用法:获取当前 Sheet、获取选区、遍历单元格、读写值。逻辑很直白,不需要关心底层是怎么渲染的。

但这里有个坑要注意:setCellValue会触发重绘和撤销栈记录。如果你在一个循环里改几千个单元格,会触发几千次重绘,性能直接崩掉。正确的做法是用批量操作:

sheet.batchUpdate(() => { for (let row = range.startRow; row <= range.endRow; row++) { for (let col = range.startColumn; col <= range.endColumn; col++) { const cell = sheet.getCell(row, col); if (cell && typeof cell.v === 'string') { sheet.setCellValue(row, col, cell.v.toUpperCase()); } } } });

batchUpdate会把所有修改攒在一起,最后统一触发一次重绘和一次撤销记录。这个技巧在处理批量数据时非常关键,我踩过好几次坑才养成习惯。

3.4 公式与计算:让表格真正“活”起来

没有公式的表格就是个静态表格,有了公式才是电子表格。Univer 的公式能力通过@univerjs/sheets-formula插件提供。装上之后,你可以在单元格里写=SUM(A1:A10)、=IF(B1>100,"高","低")这样的公式。

公式引擎的工作流程大致是:解析公式字符串、构建依赖图、计算求值、缓存结果。当依赖的单元格变化时,引擎会自动重新计算受影响的公式。这个过程涉及拓扑排序和循环依赖检测,实现起来相当复杂,但 Univer 把它封装好了,业务层直接用就行。

import { UniverSheetsFormulaPlugin } from '@univerjs/sheets-formula'; univer.registerPlugin(UniverSheetsFormulaPlugin);

注册插件之后,公式功能就自动生效了。你可以在cellData里直接写公式:

cellData: { 0: { 0: { v: 10 }, 1: { v: 20 }, 2: { f: '=SUM(A1:B1)' }, }, }

f字段就是公式。渲染的时候,单元格会显示计算结果 30,但编辑的时候显示的是公式本身。

注意:公式计算是 CPU 密集型操作。如果你的表格有大量复杂公式,建议把计算放到 Web Worker 里,避免阻塞主线程导致界面卡顿。Univer 支持这种配置,但需要额外设置。

3.5 协同编辑的接入思路

协同是 Univer 的强项,但也是接入复杂度最高的部分。核心思路是:前端把用户的每一次操作(插入行、修改单元格、调整格式)封装成命令,通过 WebSocket 发给服务端;服务端做冲突消解后,把最终的操作序列广播给所有客户端;客户端收到后应用到本地数据模型,触发重绘。

服务端用 Node.js 实现,核心逻辑是维护一个操作日志和文档快照。新用户加入时,先拉取最新快照,再重放快照之后的操作,就能得到当前状态。这种“快照 + 增量”的模式是协同编辑的经典做法,兼顾了加入速度和存储效率。

冲突消解算法方面,Univer 通常配合 OT(Operational Transformation)或 CRDT(Conflict-free Replicated Data Type)来实现。OT 的思路是变换操作,让并发操作在任意顺序下都能得到一致结果;CRDT 的思路是设计一种数据结构,让并发修改天然可合并。两种方案各有优劣,OT 实现复杂但成熟,CRDT 更适合分布式但内存开销大。

// 服务端伪代码:接收操作并广播 wss.on('connection', (ws) => { ws.on('message', (raw) => { const command = JSON.parse(raw); const transformed = transformCommand(command, documentState); applyCommand(documentState, transformed); broadcast(wss, transformed); }); });

这段伪代码展示了协同服务端的基本骨架。实际实现要考虑断线重连、操作确认、权限校验、历史版本等一堆细节,工作量不小。如果团队没有协同编辑的经验,建议先用 Univer 的单机模式把业务跑通,再逐步接入协同。

4. 常见问题与排查技巧实录

4.1 表格渲染空白或错位

这是接入初期最常见的问题。页面加载了,但表格区域一片空白,或者内容画在了错误的位置。排查思路按优先级来:

第一,检查容器尺寸。Canvas 渲染依赖容器的宽高,如果容器高度是 0,Canvas 就画不出来。常见原因是父元素没有设置高度,或者用了 flex 布局但没给flex: 1。解决办法是给容器一个明确的高度,比如height: 600px或者height: 100vh。

第二,检查 Canvas 的像素比。在高分屏上,如果没做 devicePixelRatio 适配,Canvas 内容会模糊或者偏移。Univer 内部会处理这个,但如果你自定义了容器样式,可能会干扰它。确保没有对 Canvas 元素设置transform: scale之类的样式。

第三,检查插件注册顺序。有些插件有依赖关系,比如sheets-ui依赖sheets,必须先注册sheets再注册sheets-ui。顺序错了可能导致渲染层没初始化。

现象可能原因解决办法
完全空白容器高度为 0给容器设置明确高度
内容模糊未适配高分屏检查 devicePixelRatio 配置
渲染错位插件顺序错误按依赖顺序注册插件
部分区域不显示虚拟化计算异常检查滚动容器配置

4.2 大数据量下的性能优化

表格数据一多,性能问题就来了。我总结了几条实战经验。

第一,开启虚拟化渲染。Univer 默认就是虚拟化的,但如果你自定义了某些渲染逻辑,可能破坏了这个机制。确保没有在渲染函数里做全量遍历。

第二,减少不必要的重绘。前面提到的batchUpdate是关键。另外,如果你只是改数据不改样式,可以用setCellValue的静默模式,避免触发格式重算。

第三,公式计算异步化。大量公式会阻塞主线程,把公式引擎放到 Worker 里可以显著改善交互流畅度。

第四,控制撤销栈深度。撤销栈太深会占用大量内存,而且每次操作都要往栈里压数据。根据业务需要设置合理的深度上限,比如 100 步。

提示:性能优化没有银弹,关键是找到瓶颈。用 Chrome DevTools 的 Performance 面板录一段操作,看看时间花在哪里。是渲染、是计算、还是数据同步,对症下药才有效。

4.3 导入导出 Excel 的坑

导入导出是刚需,但坑也不少。最常见的问题是格式丢失。Excel 文件里的合并单元格、条件格式、数据验证、图表,导入后可能只剩值,格式全没了。这是因为 Univer 的数据模型和 Excel 的文件格式不是一一对应的,转换过程中必然有信息损失。

我的建议是:导入时先做一次格式映射,把 Excel 的核心格式(字体、颜色、边框、对齐)转成 Univer 的格式;导出时反过来。对于复杂格式,比如图表和透视表,如果 Univer 不支持,就在导入时降级处理,至少保证数据不丢。

另一个坑是编码问题。中文 Excel 文件可能是 GBK 编码,直接按 UTF-8 解析会乱码。用 Node.js 的iconv-lite做编码转换,或者让用户上传时指定编码。

4.4 协同场景下的状态不一致

协同编辑最怕的就是“我看到的和别人看到的不一样”。排查这类问题,先确认三件事:操作是否都走了命令系统、服务端是否正确广播、客户端是否正确应用。

常见原因是绕过了命令系统直接改数据。比如你直接修改了cellData对象,没有走setCellValue,那这个修改就不会被记录成命令,也就不会同步给别人。解决办法是养成习惯:所有数据修改都走 Facade API 或命令系统,不要直接操作底层数据。

另一个原因是网络延迟导致的操作乱序。用户 A 的操作先发出但后到达,用户 B 的操作后发出但先到达,如果服务端不做排序,客户端应用顺序就不一致。解决办法是在命令里带时间戳或序列号,服务端按序处理。

问题排查方向解决思路
修改不同步是否绕过命令系统统一走 Facade API
顺序错乱网络延迟命令带序列号,服务端排序
断线后状态丢失重连逻辑重连后拉取最新快照
光标位置不对坐标转换检查行列索引基准

4.5 移动端适配的注意事项

Univer 在移动端能用,但体验和桌面端有差距。主要问题是触摸操作和鼠标操作的差异。桌面端的双击编辑、右键菜单、拖拽填充,在移动端都要重新设计交互。

我的做法是:移动端只保留核心功能,隐藏复杂操作。比如公式编辑用简化的输入框,格式设置用底部弹窗,协同光标用不同颜色区分。另外,移动端的 Canvas 性能普遍弱于桌面,数据量要控制,虚拟化渲染要确保生效。

iOS Safari 上有个已知问题:Canvas 在某些情况下会导出白图。这通常和 Canvas 的尺寸限制有关,Safari 对单个 Canvas 的像素总数有上限,超过就渲染不出来。解决办法是分片渲染,或者降低渲染精度。

5. 我踩过的坑和几条实在建议

接入 Univer 这段时间,踩的坑不算少,挑几个有代表性的说说。

第一个坑是版本管理。Univer 迭代快,不同版本之间 API 可能有 breaking change。我有一次升级了 core 包但没升级 sheets 包,结果运行时报了一堆类型错误。后来学乖了,所有@univerjs/*包统一版本号,升级时一起升。

第二个坑是数据模型的理解。Univer 的cellData不是简单的二维数组,而是一个嵌套对象,行列索引都是数字键。这种结构在遍历时要注意,不能直接用for...of,得用Object.keys或者for...in。而且空单元格不会出现在对象里,遍历时要判断存在性。

第三个坑是样式和数据的分离。Univer 把单元格的值和样式分开存储,值在cellData,样式在styles。改值不影响样式,改样式不影响值。这个设计本身是合理的,但如果你习惯了 Excel 那种“一个单元格一个对象”的模型,需要适应一下。

第四个坑是事件系统的异步性。Univer 的命令执行是异步的,你调用setCellValue之后,不能立刻读到新值,得等命令执行完。如果业务逻辑依赖执行结果,要用回调或者await。

最后分享一个实用技巧:调试的时候,把 Univer 实例挂到window上,方便在控制台里直接调用 API 查看状态。

if (process.env.NODE_ENV === 'development') { (window as any).univer = univer; }

这样在浏览器控制台里就能直接univer.getActiveUnit().getActiveSheet().getCell(0, 0)看数据,排查问题效率高很多。

关于后续扩展,Univer 的插件体系留了很大的想象空间。我目前在做的一个方向是把自定义函数和业务数据打通,比如让表格里的公式能调用后端的 API 拉取实时数据。这个能力一旦跑通,表格就不只是表格,而是一个轻量的应用平台。另一个方向是结合 AI 做智能填充和公式推荐,用户输入几个示例,自动推断规律并填充剩余数据。这些扩展都还在探索阶段,等有成熟结果了再单独写一篇分享。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 7:50:57

2026最新react网站开发介绍:避开备案坑的实战指南

2026最新react网站开发介绍:避开备案坑的实战指南 很多山东老板在启动官网项目时,最怕的不是代码写不出来,而是备案流程一头雾水,卡在最后一步干着急。别慌,这篇2026最新的react网站开发介绍,就是专门给咱们创业者避坑用的。…

作者头像 李华
网站建设 2026/9/28 7:50:51

网站建设官网制作平台选型:告别备案一头雾水,手把手教你搞定源码部署

网站建设官网制作平台选型:告别备案一头雾水,手把手教你搞定源码部署 是不是刚接手一个企业官网项目,对着工信部备案系统里那些密密麻麻的选项就发懵?域名填哪?服务器IP对应哪个?主体信息到底怎么核对?很多新手转行做网站,代码写得溜,结果卡在备案流程上,客户催着上线,你急得满头汗。其实,选对…

作者头像 李华
网站建设 2026/9/28 7:50:20

OpenVINO部署PP-YOLOE全流程:从权重导出到CPU推理性能优化

简介&#xff1a;面向目标检测落地需求的开发者与算法工程师&#xff0c;这套OpenVINO部署PP-YOLOE实战教程从零开始搭建环境&#xff0c;完整覆盖模型下载转换、IR中间表示生成、推理引擎调用、POT量化优化及性能分析全流程&#xff0c;结合具体项目案例演示实时检测部署。资源…

作者头像 李华
网站建设 2026/9/28 7:50:12

个人网站如何在百度上做推广速查手册:备案避坑全解

个人网站如何在百度上做推广速查手册:备案避坑全解 备案号还没下来,你就想急着投百度竞价?醒醒吧,在2024年的互联网环境下,没有ICP备案的个人网站,在百度搜索里就是“隐形人”。很多新手刚起步,看着后台那行“未备案,无法解析”的提示,脑子一团浆糊,备案流程一头雾水,根本不知道第一步该踩哪里。…

作者头像 李华
网站建设 2026/9/28 7:50:10

B200+vLLM算力经济学:从功耗到年利润的七层转化

1. 标题背后的算力经济学&#xff1a;为什么“每吉瓦年利润150亿美元”不是夸张修辞你刷到这个标题时&#xff0c;第一反应可能是——这数字是不是写错了&#xff1f;吉瓦&#xff08;GW&#xff09;是功率单位&#xff0c;1吉瓦1000兆瓦&#xff0c;相当于一座大型核电站的额定…

作者头像 李华
网站建设 2026/9/28 7:49:52

避坑指南:网站建设公司需要有什么东西及注意事项

避坑指南:网站建设公司需要有什么东西及注意事项 上周刚接到一个西安做外贸的客户电话,声音都在抖。他说网站被黑了,首页全变成了赌博广告,后台密码也失效了,现在域名都被注册商锁了。他问我最多的一句话是:“我当初找的那家网站建设公司,是不是没把该做的安全配置做好?”…

作者头像 李华