控制台里蹦出那行红色的Refused to apply style from '...' because its MIME type ('text/html') is not a supported stylesheet MIME type, and strict MIME checking is enabled.,我第一反应从来不是去看 CSS 文件里写错了什么,而是直接切到 Network 面板。这个拒绝应用样式,因为他的 MIME type ('text/html') 不是受支持的 style MIME type的报错,本质上是浏览器在替你把关:它向你要一份样式表,服务器却塞给它一段 HTML,于是它把这段内容直接扔了。这意味着页面样式大面积失效、布局塌成一列、字体和间距全没了,但往往只有部分样式丢失,所以很多人会在 CSS 语法里绕半天弯子。
这篇内容面向所有跟 Web 页面打交道的人:写前端但还不太熟调试面板的、做后端接口顺带管静态资源的、负责部署上线的运维同学,以及被这个报错卡过一次、想彻底搞清楚原理的人。我会从浏览器判定规则讲起,把这条链路上每一环可能出错的地方拆开,给出可以直接复制的配置片段和一套三分钟定位问题的排查流程,最后附上我在实际项目里踩过的坑和上线自检清单。不需要你精通 HTTP 协议,但看完之后你会知道该往哪儿看第一眼。
1. 这个报错到底在说什么:浏览器拒绝应用样式的判定逻辑
1.1 一条报错信息里藏着三层含义
先把这行报错拆开读。Refused to apply style from '某个地址'——主语是"拒绝应用样式",动作发生在浏览器解析<link rel="stylesheet">或@import之后;because its MIME type ('text/html')——原因是你拿回来的东西被标记成了 HTML;is not a supported stylesheet MIME type, and strict MIME checking is enabled——浏览器开了严格的 MIME 校验,只认text/css这一种类型(少数浏览器也接受text/x-css之类的变体)。
三层含义其实是递进的。第一层,浏览器确实发出了请求并且拿到了响应,不是网络断了;第二层,响应体的类型是text/html,说明服务器返回的是一段网页而不是样式表;第三层,浏览器出于安全考虑不会"猜"你的内容,不会因为内容看起来像 CSS 就拿来用。这就把问题范围锁死了:绝对不是 CSS 文件里少了分号或者选择器写错,而是这个 URL 根本没有指向一个真实的 CSS 文件。
我特别想强调第三层,因为很多人调这个 bug 时会走偏。早期浏览器有 MIME sniffing 机制,拿回来的内容会被"猜"类型,所以当年即使路径错了但服务器仍然吐回一段 CSS 文本,页面也可能正常显示。现在主流浏览器在样式表、脚本这类资源上都启用了严格校验,猜错就拒,这正是这个报错近几年变得高频的原因。
1.2 为什么偏偏是 text/html 而不是别的类型
text/html出现频率最高,原因很朴素:绝大多数 Web 服务器、应用框架、反向代理在"找不到你要的东西"或者"不知道该给什么"时,默认返回的就是一个 HTML 页面。404 错误页是 HTML,单页应用的路由兜底是 HTML,登录拦截的重定向目标是 HTML,后端路由未匹配时的 500 页面也是 HTML。
所以这个报错实际上是两层问题的叠加:根因是请求没落到正确的静态文件上,表象才是 MIME 类型不对。你要是只盯着"MIME 配置"去改服务器,很可能改半天没效果,因为服务器压根就没打算处理这个路径。我一般把这类问题叫做"症状在 A 处,病灶在 B 处",看到text/html就想一句话:我请求的 CSS 被谁截胡了。
当然也有更少见的情况,返回类型是application/octet-stream(服务器不认识扩展名,当成二进制流返回)、text/plain(纯文本服务器没配 MIME 映射)、application/json(请求被 API 网关接管了)。这几种的排查思路与text/html一致,只是"截胡者"不同。
1.3 哪些场景最容易撞上这个报错
按我处理过的次数排个序,高频场景大致有这么几类。路径问题排第一,尤其是相对路径在多层路由下被解析成奇怪位置;单页应用的路由兜底排第二,服务器把.css请求也丢给index.html;权限拦截排第三,静态资源请求被登录校验挡下来重定向;构建发布路径配置错误排第四,打包出来的引用地址和实际部署目录对不上;大小写敏感排第五,本地开发在 Windows 上跑得好好的,一上 Linux 服务器全崩。
这几类有一个共同点:它们都不是"HTML 或 CSS 写错了",而是路由分发和资源寻址层面的问题。理解了这一点,你再看这个报错,心态会完全不一样,从"我代码是不是写错了"变成"这条请求走的是哪条规则"。后面的章节我会把每一类拆开讲,给出具体的排查命令和修复配置。
2. 从请求链路看问题:样式请求到底在哪一步走岔了
2.1 一次 CSS 请求的完整生命周期
想要高效定位,脑子里得有一张链路图。浏览器解析 HTML 遇到<link rel="stylesheet" href="X">,先拿X和当前页面的基准地址做拼接,得到一个绝对 URL;然后发起 GET 请求,可能命中的环节依次是:浏览器缓存、CDN 或边缘节点缓存、反向代理、Web 服务器静态文件规则、后端应用路由、框架中间件。任何一环决定"这个路径不该我管但我也不放行",响应就可能变成一个 HTML 页面。
我在排查时习惯从后往前倒推:先在 Network 面板确认浏览器最终请求的绝对地址,再用 curl 直连服务器验证同一个地址返回什么,最后看服务器配置文件里哪些规则可能命中这个路径。这样三步就能把范围从"整个系统"缩到"某一条 location 或某一段中间件"。比起漫无目的地翻代码,倒推法的命中率高得多,因为链路末端的信息量最大。
还有一个容易被忽略的前提:浏览器缓存。如果之前某个时刻服务器返回的是 HTML,浏览器可能把这个 HTML 按 URL 缓存了,你修好配置后刷新页面依然报错。所以排查第一步永远是勾上 Network 面板的 "Disable cache" 或者用无痕窗口,这个动作能帮你省掉至少三成的无效排查时间。
2.2 Content-Type 是怎么被决定的
服务器决定响应头里的Content-Type时,依据通常有两个:扩展名映射表和框架的默认行为。静态文件服务器(比如 Nginx)会读 mime 配置文件,.css映射到text/css,.js映射到application/javascript;如果映射表里没有这个扩展名,就走默认类型,有的配置默认是application/octet-stream,有的是text/plain。
而应用层的框架,情况就复杂了。模板引擎渲染返回的内容类型是text/html;接口返回 JSON 是application/json;但如果是通配路由兜底把index.html的内容吐出来,响应头一般也是text/html。所以当你看到text/html时,基本可以断定:请求最终被应用层接住并且渲染了一个页面,而不是被静态文件服务处理。
这也解释了一个常见困惑:为什么我用 curl 看静态资源地址返回 200?因为返回 200 的是那个兜底页面,状态码漂亮但内容是错的。这就引出了一个非常重要的排查概念,下面单独讲。
2.3 软 404:状态码骗人的那种情况
正常的 404 会告诉你文件不存在,但"软 404"(soft 404)会返回 200 状态码加一个"页面不存在"的 HTML 页面。这种设计在某些场景下是为了体验(用户看到友好的错误页),但对排查静态资源问题是灾难性的,因为你的工具全都告诉你"请求成功了"。
我判断软 404 的方法很直接:看响应大小和内容。一个真实的 CSS 文件通常几 KB 到几百 KB,而错误页或首页的 HTML 结构内容大小不一样,更重要的是 Preview 里一眼就能看到<html>、<div id="app">这些东西。所以别只看状态码,一定要看响应体。我自己总结的一句话是:看头看状态,看体看真相。
顺带提醒,如果服务器把 404 也返回成 200,还有一个副作用:搜索引擎可能收录大量无效页面,前端监控也统计不出真实错误率。这不是本篇的重点,但如果你们团队正好在做工程化治理,建议一并把 true 404 加回来。
2.4 五类根因的归类框架
为了让后面的排查不至于散乱,我先把根因归成五类,你可以对着自己的场景先归类再下钻。第一类是寻址错误,URL 拼错、相对路径解析错、扩展名丢了、大小写不一致;第二类是路由劫持,服务器或框架把所有请求都交给了 HTML 兜底;第三类是权限拦截,静态资源请求被鉴权逻辑重定向;第四类是部署错位,构建产物的引用路径和实际目录不匹配;第五类是服务配置缺陷,MIME 映射缺失、代理规则覆盖、CDN 回源异常。
这五类里,前三类占了绝大多数。而且它们有一个共同的验证手段:直接请求一个确定存在的 CSS 文件。如果这个肯定存在的文件也返回 HTML,那就是路由或权限层面的事;如果它正常返回text/css,而你的文件不行,那就是寻址问题。这个对照实验我几乎每次都用,一秒钟就能把范围砍一半。
3. 三分钟定位:用浏览器工具和命令行把问题钉死
3.1 Network 面板的正确看法
打开开发者工具的 Network 面板,刷新页面,按类型筛选 CSS 或者直接按关键字搜文件名。重点看四列:Name(请求的文件名)、Status(状态码)、Type(浏览器判定的资源类型)、Size(响应大小)。当你看到某个 CSS 请求的 Type 显示为html而不是css,问题就已经被点名了,就是它。
点开这条请求,看右侧的 Headers,重点核对三项:Request URL 是不是你以为的地址、Status Code 是多少、Response Headers 里的Content-Type是什么。如果 Request URL 跟你 HTML 里写的不一样,那多半是相对路径解析的问题;如果 Status 是 200 但Content-Type是text/html,那基本是兜底页面;如果 Status 是 302 或者 301,看 Location 指向哪里,多半是重定向到了登录页或首页。
我个人习惯多看一眼 Size 那一列,因为它能给出很快的直觉判断:一个 200 多字节的"CSS 文件"绝对不可能是真的样式表,八九不离十是重定向响应或者极简错误页。这种量级上的异常往往是最高效的线索。
3.2 从 Response 内容一眼断案
切到 Response 或者 Preview 标签,直接看返回的文本。如果看到的是<!DOCTYPE html>、<html lang="zh-CN">、<title>登录</title>这类内容,答案已经写在那儿了:你请求的是样式表,拿回来的是一个网页。这时候就该去找"谁把这个请求转成了网页渲染"。
有时候 Response 里会看到框架的报错堆栈、模板变量没渲染的占位符、或者干脆是空白页。空白页的情况多见于后端在渲染错误页时又出了错,这时候去翻服务端日志往往更快。还有一种情况是返回了一段看起来像首页导航的东西,那就不用猜了,是路由兜底或者重定向。
我建议养成一个习惯:凡是样式、脚本类资源报 MIME 问题的,永远先看响应体第一行。第一行是<!DOCTYPE就去找路由,第一行是空的就去查服务器日志,这比读十遍报错信息都管用。
3.3 用命令行做无干扰验证
浏览器有缓存、有 Service Worker、有各种插件,有时候验证结果不干净。我通常会用命令行再确认一遍,命令很简单:
# 只看响应头,不下载内容 curl -I "https://你的域名/static/css/app.css" # 同时看状态码、Content-Type 和响应体前 200 字节 curl -s -D - "https://你的域名/static/css/app.css" -o /tmp/out.bin | head -20 head -c 200 /tmp/out.bin第一行看状态码和Content-Type,第二行直接看内容开头是不是 HTML。加上-L可以跟随重定向,这样你能看到最终落地页是什么。如果你们有本地和线上两套环境,同一条命令各跑一次,对比结果往往立刻就能看出是环境配置差异还是代码问题。
提示:如果站点有登录态要求,curl 直连可能被拦到登录页,这时候要用带上 Cookie 的方式验证,或者临时在服务器本机用
curl 127.0.0.1访问,绕开网关和鉴权层。
3.4 排查速查表
把上面这些判断固化成一张表,遇到问题逐行对照,效率会比凭感觉高很多。
| 现象 | 最可能的原因 | 优先验证动作 |
|---|---|---|
| 请求 URL 与预期不符,多了一层路径 | 相对路径在多层路由下解析错误 | 改为以/开头的绝对路径 |
| 状态 200,Type 为 html,内容是首页 | 路由兜底把 CSS 请求交给了 index.html | 检查服务器 try_files 或框架通配路由 |
| 状态 302,跳转到登录或首页 | 静态资源被鉴权中间件拦截 | 检查拦截白名单是否包含静态目录 |
| 状态 404,但页面显示"页面不存在"HTML | 文件确实不存在或路径大小写不一致 | 在服务器上ls确认文件真实路径 |
| 本地正常,线上报错 | 部署路径或 publicPath 配置不一致 | 对比构建产物中的引用前缀 |
| 只有部分 CSS 报错,其他正常 | 单个文件引用地址写错或该文件未打包 | 对比报错文件和正常文件的引用来源 |
| 浏览器控制台报错,curl 正常 | 浏览器缓存或 Service Worker 干扰 | 无痕窗口加禁用缓存重试 |
这张表我在多个项目里复用,遇到没见过的现象就补一行进去。它最大的价值不是覆盖所有情况,而是强迫你在动手改代码之前先把现象归类,避免"看到报错就改 CSS"的直觉反应。
4. 分场景修复:五类根因的具体改法
4.1 寻址错误:相对路径和大小写的连环坑
先说最常见的相对路径。HTML 里写<link rel="stylesheet" href="css/app.css">,如果当前页面地址是https://站点/user/profile,浏览器会把它解析成https://站点/user/css/app.css,而不是你以为的站点根目录下。这类问题在单页应用或多级路由的站点上极其常见,因为首页能正常加载,一进详情页样式就崩。
修复方式很干脆:静态资源引用一律用根路径开头,href="/css/app.css",或者更规范地由构建工具注入带哈希的绝对路径。如果项目必须部署在子目录下(比如https://站点/app/),那就得配好构建工具的 base 或 publicPath,让它自动加上/app/前缀,而不是手工在模板里拼。
大小写问题是第二号杀手。Linux 文件系统区分大小写,App.css和app.css是两个文件。本地在大小写不敏感的平台上开发,引用写错了也能跑通,一部署到线上就 404。我的处理习惯是:文件名统一小写加中划线,构建产物里禁止出现大写字母,从源头掐掉这个可能。遇上历史项目改不动命名,就在 CI 里加一条检查,扫描 HTML 和样式里的引用路径是否与实际文件名完全一致。
还有一类寻址错误更难发现:扩展名缺失或者被问号参数污染。比如模板拼接时写成了href="/css/app",或者拼了个?v=1.0但服务器把带查询串的路径当成了另一个资源。这类问题的特征是 Network 里看到的 URL 就差那么一点点,改成正确形式后立刻恢复。
4.2 路由兜底:单页应用最容易踩的那一脚
单页应用为了支持前端路由,服务器常常配置成"任何找不到的路径都返回 index.html"。这个配置一旦写得过于宽泛,.css、.js、.png这些静态资源请求也会被兜进去,于是浏览器拿到的就是 HTML。这就是这个报错最经典的成因。
正确的写法是让静态资源优先匹配,兜底只处理"看起来像页面路由"的请求。以常见的反向代理配置为例,思路是把静态目录的匹配规则放在兜底规则之前,并且在兜底里排除带扩展名的路径:
server { root /var/www/site; # 静态资源:真实存在就返回,不存在就是 404,绝不兜底 location ^~ /static/ { try_files $uri =404; } # 带扩展名的请求统一按静态文件处理 location ~* \.(css|js|map|png|jpg|jpeg|gif|svg|woff2?|ttf|ico)$ { try_files $uri =404; expires 7d; add_header Cache-Control "public"; } # 其余请求交给单页应用兜底 location / { try_files $uri $uri/ /index.html; } }关键在=404这个写法。它表示"找不到就老老实实报 404",而不是继续往下匹配到兜底规则。很多配置写成try_files $uri /index.html;,问题就出在这里:CSS 文件不存在时,它把 index.html 交出去了,状态码还是 200,浏览器就只能报 MIME 类型错误。
如果你用的是 Node 生态的静态服务,道理一样:静态中间件必须在通配路由之前注册。
const express = require('express'); const path = require('path'); const app = express(); // 静态目录必须放在所有通配路由前面 app.use('/static', express.static(path.join(__dirname, 'public'), { fallthrough: false // 找不到直接 404,不要交给后面的路由 })); app.get('/api/*', apiHandler); // 前端路由兜底放在最后 app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'public/index.html')); });fallthrough: false这个参数值得记住,默认值true会让"找不到文件"的情况落到下一个中间件,最后被通配路由接走,产生一模一样的 MIME 错误。我在两个项目里就是因为漏了这个参数,排查了一下午。
4.3 权限拦截:登录重定向把 CSS 带走了
有些站点的鉴权逻辑放在最外层,未登录用户访问任何路径都会被 302 到登录页。这个逻辑如果没给静态资源开白名单,浏览器请求 CSS 时也会被重定向,最终拿到的是登录页的 HTML,于是报 MIME 错误。这种现象在"登录态过期"时尤其容易发生:用户停留久了,刷新页面,CSS 全丢,只剩登录框。
修复要点是把静态资源路径、公开资源路径从鉴权中间件里排除。以常见的中间件写法为例:
const PUBLIC_PREFIX = ['/static/', '/assets/', '/favicon.ico', '/public/']; app.use((req, res, next) => { if (PUBLIC_PREFIX.some(p => req.path.startsWith(p))) { return next(); } if (!req.user) { // 接口请求返回 401,页面请求才重定向 if (req.path.startsWith('/api/')) { return res.status(401).json({ error: 'unauthorized' }); } return res.redirect('/login'); } next(); });注意那段区分 API 和页面的判断。如果对接口请求也返回 302,前端拿到的就是一段登录页 HTML,解析 JSON 时又会报另一类错误。我在实际项目里定过一条规矩:静态资源和接口请求永远不能返回 HTML 重定向,静态资源返回 404,接口返回 401,只有页面导航才重定向。这条规矩落实之后,这一整类问题几乎绝迹。
4.4 部署错位:构建前缀和实际目录对不上
构建工具生成的资源引用前缀,必须和实际部署目录一致。前端项目里这个配置项通常叫base(Vite)或者publicPath(webpack)。如果站点部署在https://example.com/admin/下,但构建时用了默认的/,产出的 HTML 会去请求https://example.com/assets/app.css,而实际文件在/admin/assets/app.css,结果就是 404 或者被兜底成 HTML。
配置方式很直接:
// vite.config.js export default { base: '/admin/' };// webpack.config.js module.exports = { output: { publicPath: '/admin/' } };这里有个判断技巧:打开构建产物里的index.html,看里面引用的资源路径是不是以你期望的前缀开头。这是最直接的自检,比猜测配置项有效。另外,改完配置一定要重新构建,我见过不止一次"改了配置但部署的是旧产物"的情况,排查半天最后发现是发布流程没跑构建。
还有一个隐蔽的错位:发布脚本只上传了 HTML,没上传静态资源。这在分离部署的项目里时有发生,页面能打开但样式全无。上线后我会第一时间在浏览器里直接访问一个资源 URL 确认文件存在,这已经是肌肉记忆了。
4.5 服务配置与 CDN:那些不常改但一改就出事的地方
服务器层面还有两个细节。一是 mime 映射文件没加载。有些精简镜像里,主配置缺少引入 mime 类型的指令,导致服务器不认识.css,按默认类型返回。表现为所有静态资源类型都不对,不只是样式。这种情况在自建服务器上偶有发生,检查方式是直接请求一个静态文件看Content-Type。
二是 CDN 回源行为。CDN 在源站返回错误时,有些配置会拿自定义错误页替换,于是边缘节点返回给浏览器的是 HTML 错误页,但状态码可能是 200。表现就是本地直连源站正常,走 CDN 就报 MIME 错误。这类问题排查的关键是绕过 CDN 直连源站对比,如果直连正常,问题就在 CDN 的错误页配置或者缓存上,清理缓存或者调整错误页策略即可。
我一般会在 CDN 侧单独配置一条规则:静态资源类路径的 4xx 不要替换成自定义页面,原样透传。这不是什么高深配置,但能省下未来无数次误会。
5. 把问题挡在上线前:工程化预防手段
5.1 构建产物自检脚本
与其等用户反馈样式崩了才发现,不如在发布流程里加一道自动检查。思路很简单:构建完成后,扫描产物里的 HTML,提取所有link[rel=stylesheet]和script的地址,逐个做 HEAD 请求,检查状态码和Content-Type。任何一项不符合预期就中断发布。
#!/usr/bin/env bash set -e BASE="https://你的域名" FILES=$(grep -oE 'href="[^"]+\.css"' dist/index.html | sed 's/href="//;s/"//') for f in $FILES; do url="$BASE$f" ct=$(curl -s -o /dev/null -w '%{content_type}' -I "$url") code=$(curl -s -o /dev/null -w '%{http_code}' -I "$url") if [ "$code" != "200" ] || [[ "$ct" != text/css* ]]; then echo "资源异常: $url 状态=$code 类型=$ct" exit 1 fi done echo "静态资源自检通过"这个脚本不超过二十行,但价值很高。它把"人眼检查"变成了"流水线卡口",尤其适合发布频繁的项目。我在一个有几十个静态资源的项目里跑过,第一次执行就抓出两个引用路径写错的文件,那两个问题在测试环境一直没被发现,因为测试环境的兜底页面恰好长得不难看。
提示:脚本里的地址建议用发布后的预览环境,不要用生产域名,避免自检请求污染线上监控数据。
5.2 缓存策略与缓存破坏
缓存本身不会导致 MIME 错误,但会延长错误的影响时间。如果某个 CSS 地址曾经返回过 HTML,浏览器或 CDN 缓存了这个响应,即使你修好了配置,用户可能还在用缓存里的错误版本。所以修完问题之后,一定要考虑清缓存。
彻底的方案是内容哈希命名,构建产物变成app.3f9a2c.css。文件名变了,旧缓存自然失效。这也是我强烈建议所有项目都开启的配置,它同时解决了缓存更新不及时和错误响应残留两个问题。如果项目暂时没法上哈希,退一步的做法是给资源地址加版本参数,或者修复后主动刷新 CDN 缓存。
另一个细节是 HTML 本身的缓存策略。HTML 应该设置为不缓存或短缓存,静态资源设置长缓存,这是标准做法。反过来的配置会导致用户一直拿到旧的 HTML 引用旧的资源,问题排查起来非常绕。
5.3 上线检查清单
把易错点固化成一个清单,每次上线前扫一遍,成本几分钟,收益很大。
| 检查项 | 怎么查 | 通过标准 |
|---|---|---|
| 构建前缀是否正确 | 打开产物 index.html 看引用路径 | 前缀与实际部署目录一致 |
| 静态资源是否完整上传 | 直接访问一个资源 URL | 200 且 Content-Type 为 text/css |
| 路由兜底是否会截胡资源 | 请求一个不存在的 .css | 返回 404,不是 200 HTML |
| 鉴权白名单是否包含静态目录 | 未登录状态访问资源 URL | 直接返回文件,不跳转登录 |
| CDN 错误页策略 | 请求不存在的资源 | 原样透传 404 |
| 缓存策略 | 查看响应头 Cache-Control | HTML 短缓存,资源长缓存 |
这张表我建议直接放进团队的上线文档里。清单的意义不是每条都必须走一遍,而是在出过问题的地方留个记号,让下一个人不用重新踩。
6. 踩坑实录:几个反直觉的问题和排查经验
6.1 明明文件存在,为什么还是报错
有一次排查一个很怪的案例:服务器上文件确实存在,ls能看到,权限也没问题,但浏览器就是报 MIME 错误。最后发现是配置里有一条更靠前的正则规则把这个路径拦走了,交给了后端应用处理,后端应用不认识这个路径,返回了首页。这类问题的特点是"直连文件系统没问题,但请求链路不对"。
排查方法是逐层剥离:先用服务器本机curl 127.0.0.1/路径看返回,再走域名看返回,两者不一致就说明中间有代理或网关改写;再看配置文件里所有可能命中这个路径的规则,注意规则的匹配顺序,很多服务器是"先匹配先赢"。这个案例让我养成一个习惯:改配置前先确认哪条规则在生效,而不是想当然地往文件末尾追加。
6.2 Service Worker 造成的假象
还有一个坑值得单独说。有些项目注册了 Service Worker 做离线缓存,如果缓存策略写得不严谨,它可能把某个 HTML 响应缓存到了 CSS 的地址上,然后所有请求都从这个缓存返回。表现是无论你怎么改服务器配置,浏览器始终报同一个错。
判断方法很简单:无痕窗口打开,或者在开发者工具里注销 Service Worker 并清空缓存,如果问题消失,那就是它。修复方式是调整缓存策略,明确只缓存预期的资源类型,同时在发布新版本时正确更新缓存。这类问题排查成本高是因为它绕过了网络层,你以为在看服务器,其实在看本地存储。
6.3 常见问题速查
把高频问题集中列一下,方便对照。
| 问题 | 排查思路 | 处理方式 |
|---|---|---|
| 只有某个页面样式丢失 | 对比该页面与其他页面的资源引用路径 | 修正相对路径为绝对路径 |
| 刷新后正常,直接进入异常 | 缓存或路由兜底 | 检查缓存头与兜底规则 |
| 本地正常线上异常 | 对比构建前缀与部署目录 | 统一 base/publicPath |
| 登录后恢复正常 | 鉴权拦截了静态资源 | 静态路径加入白名单 |
| 偶发出现,刷新就好 | CDN 节点缓存不一致 | 刷新缓存并统一错误页策略 |
| 所有资源类型都错 | MIME 映射未加载 | 检查服务器 mime 配置 |
6.4 我个人的几条经验
第一条:先看响应体,再改代码。这个报错百分之九十以上跟 CSS 内容无关,改 CSS 是白费力气。养成第一眼打开 Network 看响应的习惯,能省下大量时间。
第二条:用对照实验缩小范围。请求一个确定存在的资源、请求一个确定不存在的资源、直连源站与走 CDN 各试一次,三个对照做完,问题基本就定位到某一层了。这个方法比读文档快得多。
第三条:把兜底规则写窄。无论是服务器配置还是应用路由,兜底都应该只处理页面导航,明确排掉带扩展名的路径。这是一劳永逸的做法,能消灭这一整类问题。
第四条:静态资源的错误必须返回真 404。宁可让用户看到朴素的 404 页面,也不要用 200 加友好页面把问题藏起来。藏起来的问题,最终都会以更难查的形式回来。
第五条:上线自检不要靠人。人眼检查会疲劳,脚本不会。二十行的自检脚本,长期收益远超投入。
这几条听着朴素,但都是被问题追着跑了几轮之后总结出来的。真要说的话,这个报错本身并不复杂,复杂的是它背后那张由路由、代理、鉴权、缓存交织而成的网。搞清楚网的结构,比记住某一条配置怎么写更有用。