
上周帮人看一个网页打不开的活儿对方描述得特别笃定代码是在系统自带的文本编辑里写的另存为.html双击能打开就是Safari里一片花屏。我让他把文件发过来head -c 48看了一眼前几个字节是{\rtf1\ansi\ansicpg936\cocoartf...。到这一步基本就不用往下问了——这不是浏览器兼容问题也不是 HTML 语法问题是这个文件的本质压根就不是 HTML。这类故障我前后遇到过十几次九成以上不是 Safari 挑食而是文本编辑这个工具默认在帮你写富文本它把 RTF 容器连同字体表、颜色表、编码转义一股脑写进了那个叫index.html的文件里。下面把整条链路拆开讲怎么三分钟确诊、怎么把文件从富文本救回纯代码、编码和 DOCTYPE 上 Safari 比 Chrome 更认死理的地方、file://协议下脚本为什么集体罢工以及弹窗和表单控件那些容易被误判成 Safari 坏了的行为。1. 打开就是一屏花括号先确认写出来的到底是不是 HTML1.1 三条命令一分钟看清文件真面目浏览器渲染一个本地文件之前只会做两件事看扩展名决定用哪套解析器看内容开头决定按什么编码和模式解析。扩展名是我们自己写的所以第一件要排除的就是扩展名说它是 HTML内容说它不是。我固定用这三个命令做体检比任何编辑器都可靠# 1. 看系统认定的文件类型 file index.html # 2. 看前 64 个字节注意有没有 BOM 和奇怪的头部 xxd -l 64 index.html # 3. 看编码判定结果 file --mime-encoding index.html把结果对照下面这张表基本一眼定性文件开头的字节实际格式Safari 里的表现{\rtf1RTF 富文本满屏花括号和\xx转义串或整段乱码!DOCTYPE html标准 HTML5正常渲染ef bb bf !DOCTYPEUTF-8 带 BOM多数情况正常但可能在 body 前多出一个文本节点ff fe/fe ffUTF-16白屏或整页问号d0 cf 11 e0Word/WPS 二进制Safari 直接下载或显示空白纯英文且无任何标签纯文本Safari 把整段内容当源码显示有个很坑的中间态如果文件里只有英文和基本符号RTF 包装后的内容反而不太花Safari 会把它当纯文本渲染出来你会看到一段长得像代码但排版诡异的文字。这时候人容易判断成能打开只是样式没生效然后往 CSS 方向排查越排越远。所以别靠肉眼靠xxd。1.2 文本编辑的富文本默认值是怎么把代码包进去的macOS 的文本编辑有两个身份富文本编辑器和纯文本编辑器。新建文稿时它默认走富文本这时候你敲进去的每一个字符都活在一层 RTF 壳里。壳长这样简化{\rtf1\ansi\ansicpg936\cocoartf2761 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \f0\fs24 !DOCTYPE html ... }关键点在于另存为.html不会触发格式转换。文本编辑不会因为你在文件名里写了.html就自动丢掉 RTF 结构按纯文本写盘。它做的事只是把当前文稿按它现在的格式写进你给的这个名字里格式还是富文本。更隐蔽的是非 ASCII 字符的处理。RTF 里中文不会以 UTF-8 字节存在而是被转成\c4\e3\ba\c3这样的 ANSI 十六进制转义\ansicpg936那行就是声明代码页。所以即便 Safari 勉强大胆地把它当文本显示中文也是碎的。中文 HTML 文件靠文本编辑的富文本模式保存等于把编码问题叠加在格式问题上故障现象翻倍。这里有个我自己踩过的判断误区一开始我以为是 Safari 对本地文件的编码猜测策略太保守还专门去改meta charset。改了十几遍没用因为问题在字节层meta是给解析器看的建议管不到文件本身长什么样。1.3 扩展名骗局你以为打开的是 Safari其实不是另外两个高频误判得单独拎出来。第一个是隐藏扩展名。文本编辑的偏好设置里有个选项是给纯文本文件自动追加.txtWindows 记事本保存时如果保存类型停留在文本文档结果一样。Finder 默认隐藏已知扩展名于是你看到的是index.html磁盘上躺的是index.html.txt。双击它系统按.txt关联去处理Safari 里什么都不显示。验证方法Finder 里右键选显示简介看名称与扩展名那一栏或者终端ls -l一秒看穿。第二个是默认打开方式被劫持。装了 WPS 或某些办公套件之后.html的关联可能被它们抢走。你双击文件打开的是编辑器界面里显示的是带格式的文本于是得出文件坏了的结论——其实 Safari 从头到尾没被调用过。检查路径右键 → 显示简介 → 打开方式改成 Safari再点全部更改。提醒排查这类问题第一步永远是确认我看到的渲染结果真的是 Safari 产出的吗。这一步花十秒能省掉半小时。2. 把文件从富文本救回纯代码一次把保存流程改对2.1 格式菜单里的制作纯文本和保存弹窗那个.txt询问已经写好的文件有救不用重打。在文本编辑里打开它菜单栏格式 → 制作纯文本快捷键 ShiftCmdT。这一步会把文稿从 RTF 降级成纯文本字体颜色之类的富文本属性直接丢弃——我们写代码本来就不需要它们丢了正好。转换完存盘会弹一个询问要将.txt附加到文件名吗这时候一定要选不使用。这个弹窗是很多人修复失败的最后一根稻草前面的转换全做对了顺手点了使用 .txt磁盘上于是多出一个index.html.txt回到 1.3 那个坑里。保存面板底部还有一个纯文本编码下拉框选UTF-8。这一步别跳过。文本编辑在纯文本模式下默认一般也是 UTF-8但如果这个文件是从别人那里拿来的、或者你在系统里做过语言相关的调整它可能停在 GB18030 上。选完保存meta charset和实际字节对上了中文才不会瞎。2.2 偏好设置里那两个开关决定了以后还会不会犯光修好一个文件不够得把工具改到下次不会再犯。打开文本编辑 → 设置偏好设置→ 打开和存储这个面板里有两项跟 HTML 直接相关打开文件时的一项控制 HTML 文件是按 HTML 代码显示还是按格式化文本显示存储文件时的一项控制保存 HTML 文件时是写 HTML 代码还是写格式化文本。把这两项都切到按代码那一侧之后文本编辑处理.html就不再自作聪明。同时在新建文稿面板里把默认格式从富文本改成纯文本。改完这一组设置文本编辑才算勉强能当代码编辑器用。不过说实话我自己的做法是文本编辑只用来临时救急和看别人的文件写页面一律换工具。原因很简单它的设计目标是处理富文本纯文本模式只是它的一种降级状态随时可能因为一次粘贴、一次另存而回到富文本风险不可控。2.3 修复成功的三个验证信号改完之后别靠看起来对了来判断按下面三个信号依次确认head -n 1 index.html输出的第一行是!DOCTYPE html或html前面没有任何花括号xxd -l 3 index.html不是ef bb bf除非你明确需要 BOM一般不需要Safari 打开后能正常看到页面结构而不是一整块等宽字体堆叠的文本。第三条有个进阶验证法先把开发者菜单打开后面 4.3 会讲然后右键 → 检查元素。能展开出正常的 DOM 树说明浏览器真的按 HTML 解析了如果检查器里只有一个pre或者整段文本节点那就是还在被当纯文本处理。顺带说一个彻底绕开富文本问题的土办法直接用终端写文件。把代码贴进一个带引号的heredoc写盘出来的必然是纯文本 UTF-8一个字节都不会被包装cat ~/Desktop/demo/index.html EOF !DOCTYPE html html langzh-cn head meta charsetutf-8 title测试页/title /head body h1能正常渲染/h1 /body /html EOFEOF里的单引号很重要它阻止 shell 对内容做变量替换和转义处理标签里的$、反引号都能原样落地。3. 编码、DOCTYPE 与 metaSafari 在解析上比 Chrome 更认死理的地方3.1 charset 对不上中文就变成一串问号文件格式修对了之后下一个高频问题是编码。Safari 在本地file://场景下没有 HTTP 响应头可以依赖判定编码的顺序大致是BOM →meta charset→ 系统语言区猜测。这三条任何一条给错中文就崩。典型症状对照实际编码meta 声明Safari 表现UTF-8utf-8正常GB18030utf-8中文全是乱码方块UTF-8gb2312中文乱码或问号UTF-8没写桌面端多半能猜对移动端偶发乱码UTF-16utf-8白屏或整页异常排查方式file --mime-encoding index.html看实际编码grep -i charset index.html看声明两边一致才行。如果内容里中文不多也可以直接iconv -f GB18030 -t UTF-8 src.html dst.html转一遍。这里有个经验值写中文页面就别省meta charset。桌面 Safari 猜对的概率确实不低但一旦文件进了邮件附件、被别人用别的工具重新保存、或者被某个构建流程处理过猜测就会失效。显式声明是唯一稳的做法而且它成本只有一行。3.2 BOM 这个看不见的字符会在布局上咬你一口UTF-8 BOM 是三个字节ef bb bf它出现在文件最开头本身不可见。Safari 通常能识别并跳过所以很多人觉得它无害。但在两类场景下它会变成真实的麻烦一是 BOM 出现在!DOCTYPE html前面时个别解析路径会把这三个字节当成内容结果 DOM 里在html之前多出一个文本节点。表现是页面顶部莫名多一条空白或者用display: flex布局body的时候这个多出来的文本节点会被当成一个 flex item布局整体偏一段。这类问题最难查因为你看不到它。二是文件被其他工具二次处理后BOM 和声明可能互相打架。去掉 BOM 很简单# macOS 的 sed 需要给 -i 传一个空字符串参数 sed -i 1s/^\xEF\xBB\xBF// index.html至于 UTF-16我的建议是本地写页面完全别用。它的字节结构和 UTF-8 完全不通即使meta charset写着 utf-8 也救不回来因为浏览器在解码阶段就已经走岔了。文本编辑的纯文本模式下默认是 UTF-8只要你没在编码下拉框里手动选过 UTF-16一般不会遇到。3.3 DOCTYPE 缺失或写歪怪异模式会让布局全面反常DOCTYPE 这个东西看着像仪式其实它决定浏览器用哪套渲染规则。少了它WebKit 会进入怪异模式quirks mode一批历史兼容规则被激活最直接的影响是盒模型行为标准模式怪异模式width的含义内容区宽度内容 padding border百分比高度继承正常向上寻找更容易失效行内元素间距处理按规范历史兼容逻辑表格字号继承继承不继承你在 Chrome 里量好宽度是 300px到 Safari 里变成 340px别急着怀疑 Safari 的兼容性先看第一行有没有 DOCTYPE。我见过最典型的例子是一个卡片容器设了width: 300px; padding: 20px;标准模式下总宽 340px怪异模式下总宽还是 300px视觉上就缩水了然后有人去改 padding越改越偏。几个容易写歪的地方!DOCTYPE html必须是文件最开头的有效内容。别在它前面放注释、空行说明、或者别的标签。稳妥原则就是第一行永远是它字符集声明放在head里。大小写不敏感!doctype html和!DOCTYPE HTML都行但别把!漏了也别写成! DOCTYPE html这种中间带空格的形式。老式的!DOCTYPE HTML PUBLIC -//W3C//DTD HTML 4.01//EN ...会走严格模式但没必要用了直接上 HTML5 的短写法。3.4 meta 标签里的拼写陷阱Safari 不报错但会忽略这一块是典型的没有报错但功能失效因为 HTML 属性解析是宽容的写歪了浏览器不吭声只是当它不存在。我列几个实际遇到过的写法后果charsetutf 8属性值非法charset 声明失效退回猜测charsetutf8不标准的写法建议统一成 utf-8langzh cn语言标记无效影响朗读、断词、拼写检查contentwidthdevice-width, initial-scale1里逗号写成中文逗号移动端 viewport 失效页面按桌面宽度缩放缺title不影响渲染但标签页显示文件名体验差好用的习惯是meta里的引号一个都别省。虽然 HTML5 允许无引号属性值但nameviewport contentwidthdevice-width,initial-scale1这种写法只要中间多个空格就散架了而有的空格是看不见的比如全角空格。写成nameviewport这种全引号形式出错概率最小。4. 页面能显示了但脚本不跑file:// 协议下 Safari 的额外门槛4.1 模块脚本和 fetch 为什么在本地文件里被拦文件格式、编码、DOCTYPE 都对了页面能正常渲染但一按按钮控制台就报错这种也很多见。最常见的一条是Cross origin requests are only supported for protocol schemes: http, data, ...或者Origin null is not allowed by Access-Control-Allow-Origin原因是file://协议下页面被视为不透明来源同源策略会拦掉几乎所有跨来源请求。具体被拦的东西包括script typemodule里的import以及任何模块脚本加载fetch()和XMLHttpRequest读取本地文件Service Worker 注册file://下完全不支持用canvas读取图片像素会被判定为跨来源污染getImageData抛安全错误。这几条在 Chrome 里也差不多但 Safari 拦得更早、报错更隐晦。有人写了个纯静态页面用模块化的方式拆了几个.js文件双击打开在 Chrome 里能跑、Safari 里一片空白就是撞在这上面了。4.2 一行命令起个本地服务器一次性绕开所有限制解决办法不是改代码是给文件一个真正的来源。:file://换成http://之后上面那些限制全部消失。最省事的方式是用 Python 自带的服务器不需要装任何东西cd ~/Desktop/demo python3 -m http.server 8000然后在 Safari 里访问http://localhost:8000/。如果要测试的目录里有index.html它会被自动当作首页加载。其他等效方案挑顺手的用# Node 生态不想全局装可以用 npx npx serve -l 8000 # 有 PHP 环境的话 php -S localhost:8000从file://切到http://localhost之后有几个变化值得注意模块脚本能加载了fetch(./data.json)能拿到东西了Service Worker 能注册了浏览器缓存行为也更接近线上环境。唯一要留意的是端口占用8000 被别的服务占了就换 8001报Address already in use就是这个问题。提示如果你在本地服务器里改文件不生效先确认是不是缓存。Safari 对本地地址的缓存策略也挺积极开发阶段可以按 CmdOptionE 清空缓存再刷新。4.3 把 Safari 的开发者菜单打开看报错而不是猜Safari 默认把开发者工具藏起来所以很多人遇到本地页面问题只能靠猜。打开方式Safari → 设置 → 高级 → 勾选显示网页开发者功能。之后菜单栏会出现开发菜单页面里右键也有了检查元素。这个工具打开之后排查效率是量级提升的控制台面板看红色报错编码问题、模块加载失败、CORS 拦截都会在这里留下痕迹网络面板看每个资源的加载状态file://下的请求被拦会明确标出来元素面板确认 DOM 结构前面说的被当纯文本渲染一眼可辨。有个细节Safari 的检查器对本地文件也有效不需要启动服务器。所以哪怕你暂时不想起服务也能先用它确认页面到底有没有被正确解析。5. Safari 自己的脾气弹窗、表单控件与移动端视口5.1 弹窗被阻止通常不是代码坏了弹窗被阻止是本地测试里非常容易被误判成文件问题的一类现象。Safari 对window.open的判定标准是用户手势只有在一个真实的点击、触摸事件处理函数的同步调用栈里发起的弹窗才被允许。这意味着下面这些写法会被拦在setTimeout、setInterval回调里调window.open在fetch().then()的异步回调里调window.open页面加载完成DOMContentLoaded、load时自动调window.open需要先请求接口、拿到结果后再打开新窗口的逻辑。正确做法是把打开新窗口这个动作尽量前置到点击事件里document.querySelector(#openBtn).addEventListener(click, () { // 同步执行保留用户手势上下文 const win window.open(about:blank, _blank); // 异步拿到数据后再决定跳哪 fetch(/api/target) .then(r r.text()) .then(url { if (win) win.location.href url; }); });先开一个空白窗口占位拿到数据后再改它的地址用户手势的判定就不会丢。另外 Safari 设置里有个网站 → 弹出式窗口的分站设置可以针对某个站点放行。但那只能解决你自己机器上的问题不能当成通用方案代码层面还是应该保证用户手势。还有一个相关行为Safari 会阻止没有用户交互时的自动播放音视频、阻止非用户手势触发的alert之外的某些模态行为。如果你写的是个自测页面弹窗没出来先用是否由点击触发这一条过滤一遍再去怀疑别的地方。5.2 表单控件和 -webkit- 前缀那些事Safari 的表单控件外观长期依赖-webkit-前缀。input typedate、input typerange、select、滚动条样式这些你写appearance: none在部分版本上不生效得写input[typedate] { -webkit-appearance: none; appearance: none; }另一个必踩的坑是移动端 Safari 的输入框缩放如果input或select的font-size小于 16px获得焦点时页面会自动放大用户得手动缩回去。很多人在桌面端调试得好好的手机上一点输入框页面就飘。解决方式是把表单控件的字号设到 16px 以上或者配合 viewport 的maximum-scale但后者会影响无障碍慎用。文本相关的样式里-webkit-text-size-adjust: 100%值得加一句它能阻止某些场景下浏览器自动调整字号导致布局错位。滚动容器上的-webkit-overflow-scrolling: touch现在基本可以不加了新版本默认行为已经够顺滑。5.3 移动端 Safari 的 100vh和桌面窗口缩放的关系height: 100vh在移动端 Safari 里是出了名的不可靠vh按视口的最大高度计算不随地址栏收放变化于是全屏容器要么被底部地址栏压掉一截要么内容被推到屏幕外。替代方案.hero { min-height: 100vh; /* 兜底 */ min-height: 100dvh; /* 动态视口单位新版本 Safari 支持 */ }dvh会跟随地址栏状态动态变化是现在比较推荐的写法。桌面端 Safari 也有个类似的陷阱100vh是相对窗口高度算的用户拖动缩放窗口时它跟着变如果你在一个overflow: hidden的容器里用它内容可能被裁掉。这种时候改用100%配合html, body { height: 100% }更稳。6. 一份能照着走的排查清单以及我踩过的几个坑6.1 从打不开到跑起来的顺序表把前面所有情形收成一张按顺序排查的表。顺序很重要因为它按改动成本从低到高排先看格式再看编码再看解析模式最后才动代码。症状最可能的原因一步验证满屏花括号和转义串富文本伪装成 .htmlhead -c 48 index.html完全空白什么都没渲染扩展名不对.html.txt或关联被抢显示简介看扩展名中文全是乱码方块编码与 charset 声明不一致file --mime-encoding index.html布局整体偏几像素到几十像素DOCTYPE 缺失进怪异模式看第一行页面正常但按钮无反应模块脚本 / fetch 被 file:// 拦控制台看 CORS 报错点击后新窗口不出来没有用户手势上下文看 window.open 的调用位置手机上一点输入框就放大控件字号小于 16px查 input 的 font-size按这个顺序走绝大多数情况在前两步就能定位不会绕到改代码那一步。6.2 我建议的编辑器与工作流用文本编辑写网页只能算应急。它的核心设计目标是富文本纯文本是降级状态任何时候一次粘贴都可能把格式切回去。我现在的工作流是这样写代码用 VS Code 或 Nova 这类专门的编辑器它们不会在保存时偷偷加包装层在 Finder 设置 → 高级里勾上显示所有文件扩展名。这一个开关能干掉至少三成的文件打不开误报因为你能直接看到.html.txtVS Code 里装个本地服务器插件比如 Live Server保存即刷新全程走http://天然避开file://的所有限制只在需要快速看一眼别人发来的 HTML 文件时才用文本编辑打开而且开之前先确认偏好设置里那两项 HTML 相关的开关是按代码显示。还有一点值得说尽量避免在富文本模式里粘贴代码。富文本编译器会把尖括号、引号按显示逻辑处理有时候你看到的h1和磁盘上存的h1不是同一串字符可能已经被转成了lt;h1gt;。这种文件在浏览器里能打开但显示的是源码文字而不是渲染结果第一眼很难和格式问题区分开。6.3 几个反直觉的细节最后补几个我自己遇到过、但很少被文档提到的点。文件权限也可能是原因。如果文件的权限位变成了000在某些同步工具、压缩包解压、或者手动chmod之后会发生Safari 打开时会静默失败不报错也不显示。ls -l看到权限是----------就中招了chmod 644 index.html修回来。HTML 邮件的套路不要用到本地页面上。有人为了写个能在邮件客户端正常显示的页面用了大量内联样式、表格布局、老式属性。这套东西在 Safari 里渲染本地文件也没问题但它会把结构性错误掩盖掉。如果是本地开发还是按现代方式写。中文路径和空格在做本地服务器时会咬人。python3 -m http.server启动的目录如果路径里有中文一般没事但如果文件名里带空格写 URL 时要转义成%20浏览器地址栏有时会自动处理命令行里curl就必须手动处理。文件名我一般建议全用英文小写加连字符省心。别用浏览器的查看源代码来判断文件本身。Safari 的查看源代码显示的是浏览器已经解析过的内容如果它先按文本渲染了一遍你看到的源码其实是渲染后的文本。要判断原始字节还是回到xxd和file。上面这些流程我前后用了几年从最早被 RTF 包装坑到怀疑人生到后来固定成先看字节、再看扩展名、再看编码、最后看协议的四步平均修复时间从半小时压缩到两三分钟。真正让我印象最深的还是最开始那次一个四十多行的静态页面打不开我花了二十分钟调 CSS最后发现是文本编辑在文件开头写了一段{\rtf1\ansi...}。从那之后我给自己定了一条规矩——排查任何浏览器打不开 HTML的问题第一条命令永远是head -c 48。这十秒钟如果省了后面可能要赔上半小时。