☰
IIS 部署 Vue:解决 history 模式刷新 404 与重写配置
2026/10/1 12:03:23 网站建设 项目流程

周五晚上八点,打包好的dist目录整个丢进 IIS 的站点根目录,浏览器打开首页——正常,点导航跳转——也正常,手贱按了一下 F5,页面直接 404。这个场景我见过太多次了,多到一看标题「IIS 部署 vue 项目」就知道对方卡在哪一步。问题不在 Vue,也不在 IIS 有 bug,而在于 Vue Router 的history模式把「路径」当成了前端状态,而 IIS 从头到尾都认为「路径」对应磁盘上一个真实存在的文件或者目录。两边对同一个 URL 的理解完全不一致,冲突就必然发生。

这篇东西想解决的就是这一类问题:把一个已经打包好的 Vue 项目稳稳当当放到 IIS 上跑起来,刷新不 404、子目录部署不白屏、静态资源不缓存错、接口能转发、出报错能自己查出来。适合两类人看:一类是前端出身、第一次碰 IIS 的同学,另一类是常年写后端、被临时抓来发版的全栈。全文不依赖任何特定框架版本,Vue 2、Vue 3、Vite、Vue CLI 打包出来的产物都适用,因为到了 IIS 这一层,它只认识 HTML、JS、CSS 和一堆静态文件。

1. 刷新就 404 的根因:Vue 的路由和 IIS 的文件查找不是一回事

1.1 从一次真实的 404 现象说起

假设站点根目录是D:\web\mysite,里面躺着index.html、assets目录。你在浏览器里输https://example.com/user/list,IIS 收到请求后做的事非常朴素:把user/list拼到根目录后面,去磁盘上找D:\web\mysite\user\list这个文件。找不到,回 404。它根本不知道也不关心你前端里定义过一条/user/list的路由。

而你的开发环境为什么没事?因为npm run dev起的是 Vite 或 webpack-dev-server,它们默认带了一个historyApiFallback的兜底:任何找不到物理文件的请求,统统返回index.html。浏览器拿到index.html,Vue 跑起来,路由匹配到/user/list,页面渲染。所以本地开发从来没暴露过这个问题,它被 dev server 悄悄盖住了。

部署到 IIS 上来,本质就是把这个兜底逻辑在服务端再实现一遍。实现手段就是 URL Rewrite:把「找不到物理文件」的请求,重写到index.html,让 Vue 自己去解析。逻辑就这么简单,剩下的全是细节。

1.2 hash 模式为什么「天生免疫」这类问题

如果你的路由用的是createWebHashHistory(),URL 长这样:https://example.com/#/user/list。注意井号后面的部分,浏览器在发 HTTP 请求时根本不会带上它,发给 IIS 的永远是https://example.com/。IIS 老老实实返回index.html,Vue 拿到 hash 自己做匹配。整个过程 IIS 只服务了一个静态文件,完全不需要重写规则。

这就是为什么很多人简单粗暴地把模式改成 hash 就「修好了」。但代价也很明显:URL 里带个井号,分享出去难看,SEO 不友好,部分第三方回调(比如登录回跳)遇到井号会有额外处理成本。所以我的建议是——能用 history 就用 history,把重写规则配对,一次配好后面几十年都不用管。除非你的项目部署在完全没法改服务端配置的环境里(比如某些托管平台只给一个纯静态目录),那才退回 hash。

1.3 先决定模式,再决定要不要装 URL Rewrite

顺序不要搞反。正确流程是:

  1. 打开src/router/index.js(或router.js),确认是createWebHistory()还是createWebHashHistory()。
  2. 如果是 hash,直接扔文件到 IIS,配好默认文档index.html就能跑,不用装 URL Rewrite。
  3. 如果是 history,先装 URL Rewrite 模块,再写规则。

我在实际项目里踩过的一个坑是:接手别人的项目,路由文件里写的 history 模式,但打包配置里又加了publicPath: './',同时 IIS 上还配了个「虚拟目录」而不是「应用程序」,三层因素叠在一起,导致重写规则怎么写都对不上。所以后面第 3 章会专门讲子目录部署时的路径对齐,那是最容易翻车的地方。

2. 装 IIS 之前先想清楚:哪些功能必须开,哪些能省

2.1 URL Rewrite 模块的版本与安装来源

IIS 本体在 Windows 里是「功能」,但 URL Rewrite 是独立组件,Windows 功能列表里没有它。这点是新手最容易卡住的地方:他们打开服务器管理器翻了个底朝天,也找不到「重写」这两个字。

获取方式是从 IIS 官方站点下载独立安装包,选rewrite_amd64_zh-CN.msi这个版本(现在常见的是 2.1 版)。安装过程没什么可说的,一路下一步。装完之后不会立刻在管理器里出现,需要关掉 IIS 管理器重新打开,或者干脆执行一次iisreset。你会看到站点功能区多了一个「URL 重写」的图标,双击进去能可视化配规则。

顺便说一个实用判断技巧:如果你在站点根目录放了带<rewrite>节点的web.config,但服务器上没装这个模块,IIS 不会忽略它,而是直接给你一个 500.19,错误信息里带着0x8007000d和「无法识别的配置节 rewrite」。看到这个组合,基本可以断定是模块没装,而不是配置文件写错。

2.2 Windows 功能里必须勾选的那几项

Windows Server 2019 上,路径是「服务器管理器 → 添加角色和功能 → Web 服务器(IIS)」。有一堆子项容易漏,我列一下对 Vue 项目真正有用的:

功能项位置是否必须
静态内容常见 HTTP 功能必须,漏了连 HTML 都返回不了
默认文档常见 HTTP 功能必须,决定访问目录时返回哪个文件
HTTP 错误常见 HTTP 功能建议开,方便看到真实状态码
目录浏览常见 HTTP 功能建议关,避免目录结构外泄
静态内容压缩性能建议开,JS/CSS 体积能小七八成
动态内容压缩性能代理转发场景才需要
URL 授权规则安全性一般不用,除非要做访问控制
WebSocket 协议应用程序开发项目里有实时推送才需要

Win11 家用版是没有 IIS 的,在「启用或关闭 Windows 功能」里翻不出这一项,必须是专业版或企业版。这点很多人在自己笔记本上折腾半天,最后发现是系统版本问题。另外 Win11 上启用之后,inetmgr命令不一定能直接调起来,可以在开始菜单搜「IIS 管理器」,或者去「管理工具」里找。

注意:Windows Server 上装完 IIS 后,默认站点会占用 80 端口。如果你要部署的站点也用 80,记得先把默认站点停掉或者删掉,否则两个站点抢端口,表现是「访问到的是另一个页面」,很容易误判成缓存问题。

2.3 应用程序池按「无托管代码」配,不是随便选

纯静态的 Vue 产物,没有任何 .NET 代码在跑,所以应用程序池的.NET CLR 版本应该设成**「无托管代码」**。选成v4.0虽然也能跑,但会白白加载一个 CLR 运行时,占内存、拖慢首字节响应,而且一旦这台机器上还跑着别的 .NET 应用,容易在权限和临时目录上打架。

托管管道模式选「集成」,这个默认就是,不用改。

再就是身份标识。默认是ApplicationPoolIdentity,这是最稳妥的选择。它对应的实际账户是IIS AppPool\你的池名。很多教程让人改成LocalSystem或者Administrator,我不建议——那是为了绕过权限报错,代价是把整个 Web 目录暴露在最高权限下。正确的做法是留在ApplicationPoolIdentity,然后去目录权限里给这个虚拟账户单独授权。

另外热词里出现的「iis 中没有 .net8」,这个和 Vue 部署其实没关系。如果你这台 IIS 上同时要跑一个 .NET 8 的后端 API,那需要单独安装 ASP.NET Core Hosting Bundle,装完iisreset才会在模块列表里出现AspNetCoreModuleV2。前后端是两个独立进程,配置上也应该拆成两个应用程序池,互不干扰。

3. web.config 逐行拆解:一条能用的重写规则长什么样

3.1 最小可用版本与每个节点的职责

先看一个能跑起来的最小版本,位置是站点根目录下的web.config:

<?xml version="1.0" encoding="utf-8"?> <configuration> <system.webServer> <rewrite> <rules> <rule name="Vue History Fallback" stopProcessing="true"> <match url="(.*)" /> <conditions logicalGrouping="MatchAll"> <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" /> <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" /> </conditions> <action type="Rewrite" url="/index.html" /> </rule> </rules> </rewrite> </system.webServer> </configuration>

逐行说清楚它干了什么:

<match url="(.*)" />里的url不是完整的 URL,而是站点根目录之后的相对路径,并且不带开头的斜杠。比如请求/user/list?a=1,这里的url是user/list,查询字符串不包含在内。(.*)就是全部捕获。

stopProcessing="true"的意思是:这条规则命中之后,后面的规则不再执行。这个属性非常关键,如果漏了,会和其他规则叠加,产生难以预测的结果。

<action type="Rewrite" url="/index.html" />用的是 Rewrite 而不是 Redirect。区别很重要:Rewrite 是服务端内部改写,浏览器地址栏 URL 不变,用户仍然看到/user/list;Redirect 是返回 301/302,浏览器地址栏会变成/index.html,Vue 拿不到原始路径,路由直接失效。所以这里必须用 Rewrite。

3.2 两个条件判断为什么必须加 negate

如果没有那两个条件,会发生什么?所有请求——包括/assets/index-abc123.js、/logo.png——都会被重写到index.html。浏览器请求一个 JS 文件,收到的却是 HTML,控制台立刻报Unexpected token '<',页面白屏。这就是热词里「vue 打包后布局异常」最典型的成因之一:不是样式写错了,而是资源文件被重写规则吃掉了。

条件里的{REQUEST_FILENAME}是 IIS 提供的服务器变量,值是请求映射到磁盘后的完整物理路径。matchType="IsFile"配合negate="true",翻译成人话就是「当这个路径不是一个已存在的文件时,条件成立」。IsDirectory同理,判断是不是一个真实目录。

两个条件用logicalGrouping="MatchAll"组合,意思是必须同时满足「不是文件」且「不是目录」,才会触发重写。这样真实存在的 JS、CSS、图片、字体文件都能正常返回,只有那些不存在的路径(也就是 Vue 的路由地址)才会交给index.html。

这里有个细节值得多说一句:IsFile和IsDirectory会真的去访问文件系统。请求量大的时候,这确实有一点 IO 开销,但 IIS 本身对这些元数据有缓存,实测在几万 QPS 级别的静态站点上没有成为瓶颈。真到了需要优化的程度,说明你的流量已经该上 CDN 了,而不是继续在 IIS 上抠这点开销。

3.3 部署到子目录时 base 与重写目标的对应关系

这是翻车率最高的一块。假设你的站点不是部署在根目录,而是https://example.com/app/下面。这时候有两处必须同时改,改一处都不行。

第一处是打包配置。Vite 项目改vite.config.js:

export default defineConfig({ base: '/app/', // 其他配置 })

Vue CLI 项目改vue.config.js:

module.exports = { publicPath: '/app/', }

这一处的效果是:index.html里引用资源的路径会从/assets/xxx.js变成/app/assets/xxx.js。漏了这一步,浏览器会去根目录找/assets/xxx.js,找不到就 404,表现就是白屏加一屏红色报错。

第二处是重写规则的目标路径:

<action type="Rewrite" url="/app/index.html" />

如果你用的是相对写法url="index.html",在根目录部署时没问题,在子目录部署时行为取决于 IIS 版本和上下文,容易出现解析到上一级的情况。我的习惯是始终写绝对路径,把站点前缀写死,这样不管将来挪到哪里,逻辑都是清晰的。

还有一个更隐蔽的坑:子目录如果在 IIS 里被配成了「虚拟目录」而不是「应用程序」,那么父站点的web.config会向下继承。父站点的重写规则如果写得比较宽(比如匹配(.*)且没排除子目录),会把子站点的请求也重写到父站点的index.html,于是子应用永远打不开。解决办法有两个:要么把子目录右键 → 转换为应用程序,给它独立的应用程序池和配置边界;要么在父规则里加一条条件把子路径排除掉。我推荐前者,配置边界清晰,排错时不会互相干扰。

3.4 完整可抄的 web.config(含优先级顺序)

把上面几件事合到一份文件里,同时把规则顺序也排好。规则是从上往下依次匹配的,所以特殊规则必须排在通用规则前面:

<?xml version="1.0" encoding="utf-8"?> <configuration> <system.webServer> <rewrite> <rules> <!-- 1. 不存在的 assets 资源直接 404,避免被兜底成 index.html 掩盖问题 --> <rule name="Missing Asset 404" stopProcessing="true"> <match url="^assets/.*" /> <conditions> <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" /> </conditions> <action type="CustomResponse" statusCode="404" statusReason="Not Found" statusDescription="Static asset not found" /> </rule> <!-- 2. 接口请求交给后端(如果有的话),必须排在兜底规则之前 --> <!-- 3. 其余全部交给 index.html --> <rule name="Vue History Fallback" stopProcessing="true"> <match url="(.*)" /> <conditions logicalGrouping="MatchAll"> <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" /> <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" /> <add input="{REQUEST_URI}" pattern="^/api/" negate="true" /> </conditions> <action type="Rewrite" url="/index.html" /> </rule> </rules> </rewrite> <defaultDocument> <files> <clear /> <add value="index.html" /> </files> </defaultDocument> <httpErrors errorMode="DetailedLocalOnly" existingResponse="PassThrough" /> </system.webServer> </configuration>

两条容易被忽略的配置值得说。defaultDocument里加index.html并clear,是为了确保访问https://example.com/时返回的是你的 SPA 入口,而不是 IIS 默认的iisstart.htm之类。httpErrors的existingResponse="PassThrough"是让应用本身产生的响应状态码原样透传,避免 IIS 用自己的错误页覆盖掉前端返回的 404 或者 401,调试接口的时候这个非常有用。

提示:改完web.config不需要重启站点,IIS 会自动监听文件变更。但如果你改的是applicationHost.config层面的东西(比如解锁配置节),那就得iisreset或者重启对应站点。

4. 那些刷新 404 之外的问题:缓存、压缩、MIME

4.1 index.html 被缓存住导致发布不生效

重写配好之后,另一个高频问题是「明明发了新版本,用户看到的还是旧页面」。原因通常是index.html被浏览器缓存了。index.html是所有资源的入口,它一变,引用的哈希文件名就变了,整个版本才会更新。它被缓存住,等于整个发布链路断在了第一环。

解决办法是在站点配置里对index.html明确禁用缓存:

<location path="index.html"> <system.webServer> <staticContent> <clientCache cacheControlMode="DisableCache" /> </staticContent> <httpProtocol> <customHeaders> <add name="Cache-Control" value="no-cache, no-store, must-revalidate" /> <add name="Pragma" value="no-cache" /> <add name="Expires" value="0" /> </customHeaders> </httpProtocol> </system.webServer> </location>

<location>节点必须放在<configuration>下面,和<system.webServer>平级,不能塞到system.webServer里面去。这点写错了会直接 500.19。三个头部是有历史原因的:Cache-Control是现代浏览器认的,Pragma和Expires是给老代理和 IE 时代的老客户端兜底的。现在只写第一个也基本够用,但既然成本为零,一起带上更省心。

4.2 带哈希的静态资源为什么可以放心长缓存

Vite 和 Vue CLI 打包出来的assets目录,文件名里都带内容哈希,比如index-a1b2c3d4.js。文件内容一变,文件名就变,所以这类资源可以放心大胆地设超长缓存期:

<location path="assets"> <system.webServer> <staticContent> <clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="365.00:00:00" /> </staticContent> </system.webServer> </location>

cacheControlMaxAge的格式是天.时:分:秒,所以365.00:00:00就是 365 天。这个组合的效果是:用户第一次访问下载全部资源,之后每次访问只需要拉一个几 KB 的index.html,其余全部走本地缓存。实测下来首屏加载能从一两秒降到一两百毫秒。

有个前提要注意:这个策略成立的唯一条件是文件名带哈希。如果你的项目因为某些历史原因关掉了哈希(比如build.assetsDir定制成了固定名),那就千万别设长缓存,否则用户永远拿不到新文件,只能靠强刷。判断方法很简单,打开dist/assets目录看一眼文件名有没有那一串随机字符。

4.3 m3u8、ts、woff2、json 这些类型要手动补

IIS 内置的 MIME 类型表是比较老的,很多现代前端用到的扩展名它不认识。不认识的表现是:文件明明在那儿,请求却返回 404(准确说是 404.3,因为被 MIME 规则拦掉了)。热词里出现的vue 播放 m3u8就属于这一类。

需要在system.webServer下补一张表:

<staticContent> <remove fileExtension=".json" /> <mimeMap fileExtension=".json" mimeType="application/json" /> <mimeMap fileExtension=".m3u8" mimeType="application/vnd.apple.mpegurl" /> <mimeMap fileExtension=".ts" mimeType="video/mp2t" /> <mimeMap fileExtension=".woff" mimeType="font/woff" /> <mimeMap fileExtension=".woff2" mimeType="font/woff2" /> <mimeMap fileExtension=".wasm" mimeType="application/wasm" /> <mimeMap fileExtension=".webmanifest" mimeType="application/manifest+json" /> <mimeMap fileExtension=".svg" mimeType="image/svg+xml" /> </staticContent>

.json之所以要先remove再add,是因为部分 IIS 版本内置的application/json定义和实际需求有出入,直接add会因为重复定义报错。remove之后再add,无论原来有没有都能保证最终只有一条。

.m3u8配成application/vnd.apple.mpegurl、.ts配成video/mp2t,这两个是 HLS 播放的通行约定。配错了会怎样?某些播放器会因为 MIME 类型不符拒绝解析,控制台报一个看着毫不相关的错误。这个我在一个监控回放项目里踩过,排查了两个小时才发现是 MIME 的问题。

注意:staticContent这个配置节在部分 IIS 环境下是被锁定的,直接写在web.config里会 500.19。遇到这种情况,用管理员命令行执行解锁:%windir%\system32\inetsrv\appcmd unlock config /section:system.webServer/staticContent,然后再iisreset。

4.4 压缩开不开,取决于你的服务器预算

静态内容压缩能把 JS 和 CSS 的体积压掉七成左右,效果非常直观:

<urlCompression doStaticCompression="true" doDynamicCompression="true" />

但这里有两个前提必须同时满足,否则开了也不生效。第一,Windows 功能里得装了「静态内容压缩」;第二,applicationHost.config里的压缩配置允许压缩对应的 MIME 类型,默认只压text/*和application/javascript,像application/json、image/svg+xml这些要手动加进去。

CPU 开销方面,静态压缩是在首次请求时压一次然后缓存到磁盘,后续请求直接读压缩好的产物,所以持续开销几乎为零,内存和磁盘占用也不大。动态压缩是每个请求现压,CPU 开销明显,只有在做反向代理转发生成内容时才建议打开。我一般的原则是:静态压缩默认开,动态压缩看后端压力再定。

5. 前后端分离场景:让 IIS 顺便当一次反向代理

5.1 ARR 装上之后还要把代理开关打开

前后端分离的项目,前端要调/api/xxx的接口。开发时靠 Vite 的server.proxy或者 Vue CLI 的devServer.proxy转发,上线之后这个代理就没了,需要 IIS 接手。

IIS 做反向代理要装两个东西:URL Rewrite(前面已经装了)和ARR(Application Request Routing)。ARR 装完之后,默认的代理功能是关闭的,很多人卡在这里——规则写得完全正确,请求就是不通,返回 404 或者 502。

开启方式有两种。图形界面是在 IIS 管理器根节点上双击「Application Request Routing Cache」→ 右侧「Server Proxy Settings」→ 勾上「Enable proxy」。命令行更利索:

%windir%\system32\inetsrv\appcmd set config -section:system.webServer/proxy /enabled:"true" /commit:apphost

注意结尾的/commit:apphost,意思是写到applicationHost.config这一层。不加的话只影响当前站点,换个站点又得重配一遍。

5.2 代理规则为什么必须排在 history 规则前面

代理规则长这样,放在第 3.4 节那份配置里的位置2处:

<rule name="Api Proxy" stopProcessing="true"> <match url="^api/(.*)" /> <action type="Rewrite" url="http://127.0.0.1:8080/{R:1}" /> <serverVariables> <set name="HTTP_X_FORWARDED_FOR" value="{REMOTE_ADDR}" /> <set name="HTTP_X_FORWARDED_PROTO" value="https" /> <set name="HTTP_X_FORWARDED_HOST" value="{HTTP_HOST}" /> </serverVariables> </rule>

{R:1}是match里第一个捕获组的内容,也就是api/后面的部分。比如请求/api/user/list,转发到后端就是http://127.0.0.1:8080/user/list。后端如果本身就是以/api开头的路由,那url就写成http://127.0.0.1:8080/api/{R:1},这点取决于后端的实际定义,写反了会 404。

为什么必须排在 history 兜底规则前面?因为兜底规则匹配的是(.*),几乎是全匹配。如果它排在前面,/api/user/list会被重写成index.html,浏览器拿到一段 HTML 去当 JSON 解析,报一个Unexpected token '<' in JSON at position 0的错误。这个报错信息特别有辨识度,看到它基本可以直接怀疑重写规则顺序错了。

serverVariables那几行是把原始请求信息透传给后端。如果后端需要记录真实客户端 IP,没有X-Forwarded-For就只能拿到127.0.0.1。这里有个必须先做的动作:服务器变量默认不允许在web.config里设置,要先在applicationHost.config里加白名单:

<system.webServer> <rewrite> <allowedServerVariables> <add name="HTTP_X_FORWARDED_FOR" /> <add name="HTTP_X_FORWARDED_PROTO" /> <add name="HTTP_X_FORWARDED_HOST" /> </allowedServerVariables> </rewrite> </system.webServer>

漏了这一步,站点会直接 500,错误信息里提示某个 server variable 不允许被修改。这个报错挺费解的,第一次遇到大概率会往规则语法上想,其实是白名单的问题。

5.3 502、跨域与真实 IP 丢失的排查方向

代理配好之后常见的三个问题,我按出现频率排一下。

502 Bad Gateway,九成是后端没起来或者端口不对。先用curl http://127.0.0.1:8080/在服务器本机测一下,通了再回头查 IIS 配置。另外要注意 IIS 站点和127.0.0.1的关系——如果后端监听的是localhost,在某些环境下解析到 IPv6 的::1,而 IIS 转发到127.0.0.1(IPv4),就会出现本机 curl 通但 IIS 转发不通的诡异现象。稳妥做法是让后端直接监听0.0.0.0或者明确的 IP,别用localhost。

跨域报错,如果 IIS 已经做了反向代理,前端请求的就是同源的/api/xxx,理论上不该有跨域。还报跨域的话,多半是前端代码里写死了后端的完整地址(比如baseURL: 'http://api.example.com'),绕过了代理。检查一下 axios 或 fetch 的baseURL配置,改成/api这样的相对路径。

真实 IP 丢失,看第 5.2 节的serverVariables有没有配、白名单有没有加。两层都对了,后端才能从请求头里读到X-Forwarded-For。这里有个小细节:如果前端还套了一层 CDN,X-Forwarded-For会是一个逗号分隔的 IP 链,第一个才是真实客户端 IP,后端解析时别直接取整个字符串。

6. 报错排查链路:500.19、0x80005000 与 administration.config

6.1 500.19 三种不同成因要分开看

500.19 是 IIS 部署里出现频率最高的错误码,但它是一类错误,不是一种。看到它先看后面的子错误码:

子错误码含义处理方向
0x8007000d配置节无法识别对应模块没装,或标签拼写错误
0x80070021配置节被锁定用 appcmd unlock config 解锁
0x80070005拒绝访问配置文件权限不足
0x800700b7重复定义同一节点出现两次,比如两次 mimeMap

0x8007000d最常见的两个触发场景:一是web.config里有<rewrite>但 URL Rewrite 模块没装;二是标签名拼错,比如把<staticContent>写成<staticContents>。IIS 报错信息里通常会给出行号,直接跳到那一行看。

0x80070021就是配置节被锁。前面提过staticContent可能被锁,实际上httpCompression、customHeaders、handlers这几个也经常被锁。解锁命令的通用写法是appcmd unlock config /section:system.webServer/节点名。要确认某个节当前是锁还是解锁状态,可以用appcmd list config /section:system.webServer/节点名查一下,输出里会有overrideMode这一项。

提示:解锁是写到applicationHost.config的,属于服务器级改动,会影响这台机器上的所有站点。在多人共用的服务器上做这个操作前,最好知会一下同事,或者至少先做个配置备份(见 7.1)。

6.2 权限报错分层定位:目录、应用程序池、配置文件

权限问题有个好用的判断方法:看错误码里出现的是路径还是配置文件名。

错误信息里带具体目录路径(比如D:\web\mysite\index.html),那是站点目录权限问题,解决方案是给目录加上IIS_IUSRS的读取和执行权限:

icacls "D:\web\mysite" /grant "IIS_IUSRS:(OI)(CI)(RX)" /T

(OI)(CI)表示权限继承到子文件和子目录,/T表示递归应用到现有内容。如果站点需要写文件(比如上传目录、日志目录),那就单独给那个子目录授权给应用程序池身份:

icacls "D:\web\mysite\uploads" /grant "IIS AppPool\mysite_pool:(OI)(CI)(M)" /T

注意这里的账户名格式是IIS AppPool\加上应用程序池名称,中间有个空格。写成IISAppPool\或者用池的显示名都会失败。另外这条命令在 PowerShell 里可能需要加引号处理,命令行工具icacls的参数用双引号包住比较稳。

如果错误信息里出现的是配置文件路径(applicationHost.config或administration.config),那是另一类问题,下一节说。

还有一个容易被忽略的点:站点目录绝对不要放在用户目录下,比如C:\Users\Administrator\Desktop\dist。应用程序池身份对这个路径天生没有权限,而且用户目录的继承权限比较复杂,加权限也经常加不对。正确做法是把站点放到独立盘符或独立目录,比如D:\web\下面,路径里不要有中文和空格。

6.3 administration.config 报错时的止损与恢复

热词里那个「iis报错:执行此操作时出错, 文件名:c:\windows\system32\inetsrv\config\administration.config」是让很多人心态崩掉的一个错误。它的典型表现是:打开 IIS 管理器某个功能时弹窗,说操作失败,指向administration.config这个文件。

这个文件保存的是 IIS 管理器的界面配置,不是站点的运行配置。所以第一件要放心的事是:它坏了不会导致已经跑起来的站点挂掉。它是管理工具的配置,影响的是你能不能通过图形界面操作。

常见成因有这么几个:文件被设成了只读;文件的 ACL 权限被改动过(比如之前有人为了「解决权限问题」把这整个 config 目录的权限改了);文件内容被损坏,比如非正常关机导致写入中断;还有一种情况是用了非管理员账户登录系统,IIS 管理器连不上 ADSI 提供程序,报错里带0x80005000。

处理顺序我一般是这样:

  1. 先确认是不是权限问题——右键administration.config看属性,只读勾要去掉;再在安全选项卡里确认SYSTEM和Administrators有完全控制权限。
  2. 确认是用管理员身份运行 IIS 管理器。错误码0x80005000基本都指向这一类身份验证问题。
  3. 前两步都没问题,就考虑文件损坏。这时候不要手改这个文件,用配置备份恢复(第 7.1 节的命令),或者从同版本的机器上拷一份原始的administration.config覆盖。

注意:c:\windows\system32\inetsrv\config\这个目录下的文件是整个 IIS 的配置中枢,动它之前一定先备份。我见过有人为了「修好权限」,直接给整个 config 目录加了Everyone 完全控制,结果是配置文件被意外改动,站点配置全乱了,比原来的问题严重十倍。

6.4 上线前必须做的 IIS 配置备份与还原

备份这件事,最好的时间点是改配置之前,而不是出问题之后。IIS 自带备份机制,命令很简单:

:: 创建一个名为 before-vue-deploy 的备份 %windir%\system32\inetsrv\appcmd add backup "before-vue-deploy" :: 列出所有已有备份 %windir%\system32\inetsrv\appcmd list backup :: 需要回滚时恢复 %windir%\system32\inetsrv\appcmd restore backup "before-vue-deploy"

备份的产物默认落在%windir%\system32\inetsrv\backup下面,每个备份是一个以时间戳命名的文件夹,里面是配置文件的副本。这个操作耗时通常在一秒以内,成本极低,但关键时刻能救命。养成习惯:每次动站点绑定、应用程序池、applicationHost.config之前,先add backup一句。

还有一种更细粒度的做法是直接备份整个 config 目录:

robocopy "C:\Windows\System32\inetsrv\config" "D:\iis-config-backup" /MIR /R:1 /W:1

/MIR是镜像模式,保持两边一致。适合在做大批量变更前留一份完整快照。还原的时候反向执行一次即可,但注意还原时最好先把 IIS 服务停掉(net stop w3svc),避免文件被占用。

7. 发布脚本里顺手加的两条命令

自动化发布这块,只说两个我每次都会加进去的步骤,成本很低但省心。

第一条是发布完成后清掉dist目录里不该带的东西。本地打包产物里有时候会夹着.map文件(source map)、.DS_Store、编辑器临时文件。.map文件尤其要注意,它包含完整源码,暴露出去等于把代码送人。在发布脚本里加一句清理:

find ./dist -name "*.map" -type f -delete

或者在打包配置里直接关掉 source map 生成,Vite 里是build.sourcemap: false,Vue CLI 里是productionSourceMap: false。后者更彻底,因为文件根本不会生成。

第二条是发布后做一次健康检查,别等到用户反馈才发现问题。用curl打几个关键路径,看状态码:

curl -o /dev/null -s -w "%{http_code}\n" https://example.com/ curl -o /dev/null -s -w "%{http_code}\n" https://example.com/user/list curl -o /dev/null -s -w "%{http_code}\n" https://example.com/api/health

三个都返回 200 才算发布成功。第一条验证默认文档,第二条验证重写规则,第三条验证反向代理。这三个点覆盖了绝大多数部署故障,加起来跑不到两秒。

最后再补一个我自己踩过的坑。有一次站点在测试环境好好的,上到生产就白屏,控制台报一堆资源 404。查了半天发现是生产环境走了一层 CDN,CDN 的缓存规则把index.html也缓存了,导致新版本发上去用户拿到的还是老的index.html,引用的还是老哈希的资源文件,而那些老文件已经被新版本覆盖掉了。解决方案是在 CDN 那层也把index.html设成不缓存,和 IIS 这边的策略保持一致。这个坑的教训是:缓存策略要在整条链路上统一,从 IIS 到 CDN 再到浏览器,任何一层漏了都可能出问题。排查这类问题的思路也很简单,看index.html的响应头里Cache-Control是什么,一层层往上找,哪一层说「可以缓存」,问题就在哪一层。

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

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

立即咨询