前端项目写完之后,最后一步往往就卡在“怎么让它在服务器上跑起来”。这两年我经手过不少后台管理系统、数据看板、内部工具,打包产物是 Vue 的 dist 目录,服务器却是客户机房里那台装着 Windows Server 的物理机,运维同事只会 IIS,不想也不会给你装 Docker、装 Nginx。这种时候,把 Vue 项目部署到 IIS 上就是最现实的选择。这篇内容就是把这套流程完整拆一遍,从 IIS 功能勾选、应用程序池配置、web.config 重写规则,到刷新页面 404、500.19 配置锁定、应用程序池权限报错 0x80005000 这些坑,全部讲透。不管你是刚把第一个 Vue 项目打包完的新手,还是被 history 路由折腾过一下午的老手,都能从里面找到能直接抄的配置。
1. 先把部署链路想清楚:Vue 产物和 IIS 的配合逻辑
很多人部署失败,不是命令敲错了,而是压根没搞明白自己在部署什么。把 Vue 项目丢到 IIS 上,跟把 PHP 项目丢上去完全是两回事,理解这一点能省掉后面一半的排查时间。
1.1 Vue 打包后到底生成了哪些东西
执行npm run build之后,你拿到的 dist 目录,本质上就是一包纯静态文件:一个index.html、一个带哈希值的assets目录(里面是 js、css、字体、图片),可能还有 favicon、manifest.json、robots.txt 这些零碎文件。Vue CLI 默认输出到dist,Vite 也是dist,可以在配置里改。
关键在于,这些文件之间是靠相对或绝对路径互相引用的。index.html里会写<script src="/assets/index-a1b2c3.js">,这个/开头代表站点根目录。如果你的站点没建在根路径,而是挂在http://server.com/admin/下面,这个/assets/...就会指向http://server.com/assets/...,直接 404,页面白屏。这是新手最容易踩的第一个坑,后面第二章节会专门讲怎么改。
还有一点,Vue 是单页应用(SPA)。整个站点物理上只有一个真实的 HTML 文件,就是index.html。用户在页面上点到/user/list、/order/detail/123,这些路径都是前端路由(vue-router)用 JavaScript 在浏览器里动态渲染出来的,服务器上并不存在user/list这个目录,也没有order/detail/123这个文件。用户在页面内点击跳转没问题,因为路由被 JS 拦截了;但一旦用户按 F5 刷新,或者直接把链接复制给别人打开,浏览器就会发一个真实请求给 IIS,IIS 去磁盘上找D:\site\user\list,找不到,返回 404。
所以 IIS 部署 Vue 的核心矛盾,就一句话:服务器只认识 index.html,但用户会请求任意路由路径。解决它的手段只有一个字:重写(Rewrite)。把“文件不存在的请求”统统交给index.html,剩下的交给前端路由自己处理。
1.2 为什么有人建议干脆用 Nginx 或者 Node 托管
我见过不少人说“Vue 就该用 Nginx 部署”,这话有一定道理。Nginx 的try_files $uri $uri/ /index.html;一行配置就解决了重写问题,而 IIS 需要额外装 URL Rewrite 模块、还要写 XML 规则,确实更绕。Node 方案则是直接跑一个 Express 服务读 dist 目录,也一样能解决。
但现实情况是,很多单位的服务器是 Windows Server 2019/2022,上面已经跑着 IIS,可能还有 .NET 的老站点、别的部门的系统,端口 80/443 已经被 IIS 占了。再装一个 Nginx 监听 8080,然后做端口转发,反而更乱。更不用说加上 IIS 的 Windows 身份验证、IP 限制、日志审计能力,这些东西在内网环境下往往比“配置简单”重要得多。
所以我的判断是:服务器是 Windows 且已经跑了 IIS,那就老老实实配 IIS,别为了绕开一个 web.config 去引入新的技术栈。真正要做的,是把重写规则、缓存策略、权限这几件事配明白。
1.3 部署前你需要确认的几件事
动手之前,把下面这张清单过一遍,能省下大量来回折腾的时间。
| 检查项 | 说明 | 不满足的后果 |
|---|---|---|
| IIS 已安装 | 服务器管理器 → 添加角色和功能 → Web 服务器(IIS) | 没有 IIS 管理器可用 |
| URL Rewrite 模块 | 微软官网下载独立安装包,装完需重启 IIS 管理器 | 500.19 配置错误,直接打不开站点 |
| 静态内容功能 | IIS 功能里勾选“静态内容” | 403.14 或 404.3,静态文件读不了 |
| 默认文档配置 | 添加 index.html 到默认文档列表 | 访问根路径显示目录列表或 403 |
| 应用程序池模式 | 建议“无托管代码” | 无谓地加载 .NET 运行时 |
| 目录读权限 | IIS_IUSRS 对站点目录有读取权限 | 401.3 拒绝访问 |
这张表看着简单,但我可以负责任地说,至少七成的部署失败案例,问题都出在这六行里的某一行。尤其是 URL Rewrite 模块,很多人以为装了 IIS 就自带,实际上它是需要单独下载安装的扩展组件,装完之后 IIS 管理器里才会多出“URL 重写”这个图标。
2. 环境准备:功能组件、模块和打包参数
环境这一步做扎实,后面基本就是复制粘贴。我习惯把 IIS 配置和前端打包配置分成两条线来看,因为它们解决的是不同层面的问题:IIS 负责“请求怎么被处理”,打包配置负责“资源路径长什么样”。
2.1 安装 IIS 并勾选关键功能
在 Windows Server 上,路径是:服务器管理器 → 管理 → 添加角色和功能 → 一路下一步到“服务器角色” → 勾选“Web 服务器(IIS)”。在 Windows 10/11 专业版上,则是:控制面板 → 程序和功能 → 启用或关闭 Windows 功能 → Internet Information Services。
勾选的时候,很多人图快直接全勾,我不建议这么干。全勾会把 FTP 服务器、WebDAV、各种开发诊断组件都装上去,攻击面变大,而且某些组件(比如 WebDAV)还会干扰正常的静态文件请求——WebDAV 模块会拦截 PUT/DELETE 之类的方法,某些情况下甚至影响 GET 的处理顺序。
我一般只勾这些:
- Web 服务器 → 常见 HTTP 功能:静态内容、默认文档、目录浏览(可选)、HTTP 错误、HTTP 重定向
- Web 服务器 → 性能:静态内容压缩、动态内容压缩
- Web 服务器 → 安全性:请求筛选、Windows 身份验证(内网需要域账号登录时勾)
- Web 服务器 → 应用程序开发:一般不需要勾 ASP.NET,除非同一台机器还有 .NET 站点
- 管理工具 → IIS 管理控制台:必须勾,否则没有图形界面
装完之后,浏览器打开http://localhost,能看到 IIS 的默认欢迎页,说明基础环境没问题。
2.2 URL Rewrite 模块:一个必须单独装的组件
这一步是重灾区。IIS 装好之后,IIS 管理器里是没有“URL 重写”图标的,必须去微软官网下载URL Rewrite Module 2.1(注意是 2.1 版本,2.0 在 Server 2022 上有兼容问题),安装包很小,装完重启 IIS 管理器就能看到。
怎么验证装成功了?在 IIS 管理器里点开任意一个站点,功能视图里能看到“URL 重写”图标就行。或者直接用命令行验证:
%windir%\system32\inetsrv\appcmd.exe list config -section:system.webServer/rewrite如果提示“找不到请求的节”,说明模块没装好;如果正常返回配置内容(哪怕是空的),说明模块已就位。
注意:URL Rewrite 模块是服务器级别的,装一次所有站点都能用。但如果你在 web.config 里写了 rewrite 规则,而目标服务器没装这个模块,站点会直接抛 500.19,连静态文件都打不开,报错信息里会写“无法读取配置节 rewrite,因为它缺少节声明”。
2.3 打包配置:publicPath 和 base 到底该填什么
这是前端侧唯一需要改的地方,但改错了就是白屏。分两种情况:
情况一:站点部署在根路径,比如http://192.168.1.10/,那资源路径保持默认就行。
- Vue CLI 3+:
vue.config.js里publicPath: '/'(默认值,可不写) - Vite:
vite.config.js里base: '/'(默认值,可不写)
情况二:站点部署在子目录,比如http://192.168.1.10/admin/,必须显式指定。
- Vue CLI:
publicPath: '/admin/' - Vite:
base: '/admin/'
注意必须以斜杠开头,也必须以斜杠结尾。写成admin/会变成相对路径,在嵌套路由下解析错乱;写成/admin不带尾部斜杠,拼接出来的资源地址会变成/adminassets/xxx.js,一样 404。这个细节我踩过,排查了快一个小时才发现少了个斜杠。
配置改完,重新npm run build,然后打开 dist/index.html 看一眼源码,确认里面的引用是/admin/assets/index-xxxx.js这种形式,就对了。
另外提一句,如果你的项目用了 hash 路由模式(地址栏带#,比如http://site/#/user),那 IIS 这边不需要任何重写规则,因为#后面的内容根本不会发给服务器。但 hash 模式地址难看,SEO 也不友好,大部分后台系统还是用 history 模式。下面讲的内容默认都是 history 模式。
2.4 关闭 source map 和拆分策略的小建议
上线前顺手检查两件事。一是生产构建别带 source map,默认productionSourceMap: false或者 Vite 里build.sourcemap: false,否则你的源码会原样暴露在生产环境的浏览器里,内网项目也建议关掉。
二是如果项目比较大,dist 里可能出现几十个 chunk 文件。这本身没问题,但要留意 IIS 的 MIME 类型:.js、.css、.woff2、.svg这些 IIS 默认都认识,但有几类扩展名在部分系统上会被漏掉,比如.mjs、.wasm、.json在极老版本的 IIS 上可能缺失,遇到的时候在 web.config 里补静态内容映射就行,第四章节会给出具体写法。
3. 建站实操:从应用程序池到 web.config 的完整配置
这一章是整套流程的核心,我把每一步的操作顺序和背后的原因都写清楚,你可以照着做。
3.1 应用程序池:为什么一定要选“无托管代码”
打开 IIS 管理器,左侧展开服务器节点,右键“应用程序池” → 添加应用程序池。
- 名称:
vue-admin-pool(自己起,别用中文和空格) - .NET CLR 版本:无托管代码(No Managed Code)
- 托管管道模式:集成(Integrated)
- 启动模式:AlwaysRunning(可选,减少首次访问的冷启动)
为什么选“无托管代码”?因为 Vue 的 dist 里全是静态文件,IIS 只需要把它们原样读出来发给浏览器,根本不需要加载 .NET 运行时。如果选了 v4.0,IIS 会为每个请求初始化 CLR,白白吃掉几十兆内存,进程启动也更慢。这个选项不影响任何静态文件服务能力,放心选。
“托管管道模式”选集成,是 IIS 7 之后的推荐模式,重写规则和请求筛选都在这个模式下工作得最好。
这里还有几个默认参数值得改一下:
| 参数 | 默认值 | 建议值 | 原因 |
|---|---|---|---|
| 闲置超时 | 20 分钟 | 0(不超时) | 内网系统访问间隔长,避免频繁回收 |
| 固定时间间隔回收 | 1740 分钟 | 保持或按需 | 定期回收可释放内存碎片 |
| 最大工作进程数 | 1 | 1 | 静态站不需要 Web Garden |
| 队列长度 | 1000 | 保持 | 静态文件响应快,很少堆积 |
改闲置超时这一步,我强烈建议做。有一次部署完后用户反馈“每天早上第一次打开特别慢”,就是这个 20 分钟闲置回收导致的,进程被回收后第一次请求要重新启动工作进程,加上磁盘冷读,体验很差。
3.2 网站绑定与物理路径
右键“网站” → 添加网站:
- 网站名称:
vue-admin - 应用程序池:选刚才建的
vue-admin-pool - 物理路径:指向你放 dist 内容的目录,比如
D:\wwwroot\vue-admin - 绑定:类型 http,IP 地址“全部未分配”,端口填一个没被占用的(比如 8080),主机名可以留空
关于物理路径的一个重要提醒:不要直接把路径指向D:\project\vue-admin\dist,而是先把 dist 里的内容复制到D:\wwwroot\vue-admin,或者单独建立发布目录。原因有两个:一是开发目录里有 node_modules、src、.git,权限混乱且体积巨大;二是有时候开发机就是服务器本身,直接指向 dist 的话,下次构建会覆盖,但如果构建失败,站点就成了半成品状态。
如果想让流程更规范,可以用符号链接或者直接用 CI 复制,但手工部署的场景下,一个干净的发布目录是最省心的。
绑定端口这里,如果 80 端口没被占用可以直接用 80。如果是内网多站点,建议用不同端口或者主机名区分,别去动默认站点的绑定,容易把别人的站点搞挂。
3.3 目录权限:只给读权限就够了吗
网站的物理目录默认继承上级目录的 NTFS 权限,大多数情况下 IIS_IUSRS 已经有了读取权限。但如果你是从别的地方 copy 过来的目录,或者自己新建的目录结构,权限可能没继承好。
右键目录 → 属性 → 安全 → 编辑 → 添加 → 输入IIS_IUSRS→ 确定,然后只勾读取和执行、列出文件夹内容、读取这三项。
注意:千万别为了让程序“跑起来”就把 Everyone 加进去给完全控制,这是内网安全审计最喜欢抓的问题。Vue 站点是纯静态的,读取权限完全够用。只有当日志写入应用目录或者有上传功能时才需要额外给写权限,那也应该精确到具体子目录。
这里顺便说一下热词里提到的那个经典报错——“应用程序池权限设置失败,请手动为其设置 LocalSystem 权限,未知错误(0x80005000)”。这个报错通常出现在两种场景:一是你尝试在 IIS 管理器里修改应用程序池标识,但服务器不在域里,或者输入的用户名格式不对(比如该写.\username却写了username);二是应用程序池标识指向了一个已经被删除或禁用的账户。
0x80005000 本质上是 ADSI 层的错误码,代表“找不到对象”,跟权限本身关系不大,是账户解析失败。正确的处理方式是:
- 检查应用程序池 → 高级设置 → 进程模型 → 标识,如果是自定义账户,确认用户名格式为
计算机名\用户名或.\用户名 - 如果不需要模拟特定账户,直接改回
ApplicationPoolIdentity即可 - 如果确实需要 LocalSystem(极少见,通常是为了访问网络共享),注意这是高权限账户,等同于服务器管理员,能不用就不用
我见过有人为了解决一个读文件失败,直接把应用程序池标识改成 LocalSystem,问题确实没了,但这个池如果被攻破,攻击者直接拿到系统权限。正确做法是把需要的目录权限授予IIS AppPool\vue-admin-pool这个虚拟账户,而不是给整个池提权。
3.4 默认文档:把 index.html 排到第一位
站点 → 双击“默认文档”,你会看到Default.htm、Default.asp、index.htm、iisstart.htm等。Vue 的入口是index.html,如果列表里没有,需要手动添加;如果有,建议把它移到最上面。
为什么强调顺序?因为默认文档是按列表顺序逐个尝试的,如果目录下同时存在index.htm和index.html(某些模板会带),IIS 会先命中原生的那个,导致你看到的是旧内容或者 404。把index.html置顶最保险。
3.5 web.config:整套方案里最关键的一个文件
在站点根目录(也就是放 index.html 的那个目录)新建web.config,内容如下。这是我在多个项目里验证过的最小可用版本:
<?xml version="1.0" encoding="UTF-8"?> <configuration> <system.webServer> <!-- 1. 默认文档 --> <defaultDocument> <files> <clear /> <add value="index.html" /> </files> </defaultDocument> <!-- 2. history 路由重写:文件/目录不存在时交给 index.html --> <rewrite> <rules> <rule name="Vue Router History Mode" 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" /> <add input="{REQUEST_URI}" pattern="^/admin/api/" negate="true" /> </conditions> <action type="Rewrite" url="/index.html" /> </rule> </rules> </rewrite> <!-- 3. 缓存策略:带哈希的静态资源长缓存,index.html 不缓存 --> <staticContent> <clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="365.00:00:00" /> <remove fileExtension=".json" /> <mimeMap fileExtension=".json" mimeType="application/json" /> <remove fileExtension=".woff" /> <mimeMap fileExtension=".woff" mimeType="font/woff" /> <remove fileExtension=".woff2" /> <mimeMap fileExtension=".woff2" mimeType="font/woff2" /> </staticContent> <!-- 4. 压缩 --> <httpCompression> <staticTypes> <add mimeType="application/javascript" enabled="true" /> <add mimeType="text/css" enabled="true" /> <add mimeType="application/json" enabled="true" /> <add mimeType="image/svg+xml" enabled="true" /> </staticTypes> </httpCompression> <!-- 5. index.html 单独禁止缓存 --> <httpProtocol> <customHeaders> <add name="X-Content-Type-Options" value="nosniff" /> </customHeaders> </httpProtocol> </system.webServer> </configuration>如果你部署在子目录/admin/,把规则里的url="/index.html"改成url="/admin/index.html",同时在conditions里加一条排除/admin/api/的规则,避免接口请求被吞掉。
这段配置里有几个点需要展开说。
关于{REQUEST_FILENAME}的两个否定条件。IsFile和IsDirectory都要negate="true",意思是“这个路径既不是真实文件,也不是真实目录”的时候才重写。为什么不只判断文件?因为如果请求的是一个真实存在的目录(比如 assets 目录本身),重写成 index.html 反而是错的。两个都判断最稳妥。
关于排除 API 路径。前后端分离的项目里,前端站点经常要代理/api到后端服务。如果不排除,后端接口返回 404 的时候,请求会被重写成 index.html,前端拿到一堆 HTML 内容去 JSON.parse,报出莫名其妙的语法错误。这个坑我见过太多次。排除规则用^/api/正则即可,注意别写成^/api,否则/apixxx也会被排除。
关于clientCache。这是个站点级的全局缓存设置,配合 Vite 或 Vue CLI 生成的哈希文件名(index-a1b2c3.js),可以实现“内容变了文件名就变,浏览器自动重新下载”。但index.html绝对不能长缓存,否则用户永远拿到旧版本。所以更精细的做法是把全局 clientCache 设短一点(比如 7 天),然后在 assets 子目录里单独放一个 web.config 设长缓存。
3.6 assets 子目录的独立缓存配置
在dist/assets/目录下再放一个 web.config:
<?xml version="1.0" encoding="UTF-8"?> <configuration> <system.webServer> <staticContent> <clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="365.00:00:00" /> </staticContent> </system.webServer> </configuration>这样index.html走站点根目录的短缓存,assets 里的哈希文件走一年长缓存。每次发版只要 index.html 更新了,浏览器就会拿到新的资源引用,命名带哈希的新文件会重新下载,老文件在缓存里也无所谓。
4. 常见报错与排查实录
这一章是我最想写的部分,因为前面三章的内容文档里都能查到,但真正让人崩溃的是那些“看起来一样、原因完全不同”的报错。
4.1 页面白屏、刷新 404、资源加载失败
这三个现象经常被混为一谈,但成因完全不同,先看这张速查表:
| 现象 | 根本原因 | 定位方法 | 解决方式 |
|---|---|---|---|
| 打开首页就白屏,控制台 404 | 资源路径不对 | 看 Network 里 js 请求的 URL | 修正 publicPath / base |
| 首页正常,点进子路由刷新后 404 | 缺少重写规则 | 确认 web.config 是否生效 | 加 rewrite 规则 |
| 首页正常,子路由刷新后显示首页内容 | 重写规则写错了(把 assets 也重写了) | 看 Network 里 js 返回的 Content-Type 是 text/html | 加 IsFile/IsDirectory 条件 |
| 打开是 IIS 欢迎页 | 默认文档没配或顺序不对 | 检查默认文档列表 | 把 index.html 置顶 |
| 打开显示目录列表 | 目录浏览被启用且没有默认文档 | 站点功能里看目录浏览状态 | 关闭目录浏览,配好默认文档 |
第二行和第三行的区别特别关键。刷新后 404说明请求根本没被重写,规则没生效;刷新后显示首页内容但路由不对说明重写发生了,但把不该重写的也重写了。后者更隐蔽,因为页面“看起来是好的”,但用户一刷新就回到首页,体验很差。
如果重写规则没生效,排查顺序是:一,确认 URL Rewrite 模块装了;二,确认 web.config 在站点根目录而不是子目录;三,确认 web.config 的编码是 UTF-8 无 BOM(有 BOM 会导致解析失败);四,浏览器强刷清缓存,IIS 管理器里点“重新启动”站点。我遇到过一次是文件保存成了 UTF-8 with BOM,IIS 直接报 500.19,报错信息里明确写了“配置错误”,但很容易被忽略。
4.2 500.19 配置错误:三个高频成因
500.19 是 IIS 部署里最典型的报错,它的含义是“配置文件有非法内容”。三种情况最常见:
第一种:rewrite 节未声明。服务器没装 URL Rewrite 模块,而 web.config 里写了<rewrite>。解决办法就是装模块,或者临时把 rewrite 段注释掉验证。
第二种:节被锁定(overrideModeDefault)。有些节(比如httpCompression、staticContent的部分子节点)在 applicationHost.config 里被设置为不允许在站点级别覆盖,写进 web.config 就会报 500.19,报错信息里会指明具体是哪个节。解决办法有两个,一是用 IIS 管理器的“配置编辑器”在服务器级别修改,二是在%windir%\system32\inetsrv\config\applicationHost.config里把对应节的overrideModeDefault改成Allow:
<section name="httpCompression" overrideModeDefault="Allow" />注意:改 applicationHost.config 之前一定要备份,这个文件是 IIS 的全局配置,改坏了所有站点都起不来。改完执行
iisreset或者net stop was /y && net start w3svc让它生效。
第三种:编码问题。web.config 必须是 UTF-8 无 BOM。用记事本另存为的时候注意编码选项,推荐用 VS Code 或 Notepad++ 保存并确认编码格式。
4.3 401.3、403.14、404.3 这三个经典错误
- 401.3 未经授权:IIS 进程账户没有读取该目录的权限。检查 IIS_IUSRS 权限,或者检查目录是否在一个拒绝继承的路径下。还有一种可能是目录在用户桌面上或者 C 盘某些受保护位置,建议统一放到
D:\wwwroot这类独立盘符下。 - 403.14 目录列表被拒绝:说明请求命中了目录,但默认文档没找到。检查 index.html 是否在根目录、默认文档列表里是否有它。
- 404.3 MIME 类型被拒绝:这是静态文件的扩展名没有对应的 MIME 映射。常见于
.woff2、.wasm、.mjs、.json(老版本 IIS)。在 web.config 的staticContent里加 mimeMap 就行,前面给的模板里已经包含了常见项。
补一个排查技巧:这三个错误如果在浏览器里看到的信息很简略,可以打开 IIS 的“错误页”功能,把详细错误信息设置为“本地请求显示详细错误、远程请求显示自定义错误”,这样在服务器本机用浏览器访问就能看到具体的错误模块和错误码,定位效率高很多。
4.4 接口跨域和反向代理
前后端分离项目部署到同一个 IIS 下,最省事的方案就是让 IIS 做反向代理,把/api转发到后端服务(比如本机的 8080 端口 Node 服务,或者 9000 端口的后端)。
这需要两个东西:Application Request Routing(ARR)模块,以及 URL Rewrite。ARR 装完之后,在 IIS 管理器根节点上双击“Application Request Routing Cache” → 右侧“Server Proxy Settings” → 勾选 “Enable proxy”,然后应用。
web.config 里加一条代理规则:
<rule name="API Proxy" stopProcessing="true"> <match url="^api/(.*)" /> <action type="Rewrite" url="http://127.0.0.1:9000/{R:1}" /> </rule>这条规则要放在 history 重写规则之前,因为 rewrite 规则是按顺序匹配的,前面的命中后如果stopProcessing="true"就不再往下走。
如果不想折腾 ARR,另一个方案是后端自己开 CORS,前端请求直接打后端地址。内网环境下两者都行,但反向代理的好处是前端代码里写/api就行,不用区分开发环境和生产环境的域名。
注意:启用 ARR 代理后,如果规则写得过于宽泛(比如
^.*),可能把静态资源的请求也代理出去,导致页面彻底打不开。规则的正则一定要写精确。
4.5 发版后用户还看到旧页面
这是上线之后最常被投诉的问题。原因通常是浏览器缓存了旧的index.html,或者中间有 CDN/代理缓存。
排查思路:先在服务器上用curl -I http://localhost/index.html看响应头,确认Cache-Control是不是no-cache或者很短的 max-age。如果是长缓存,说明站点级的 clientCache 设置把 index.html 也覆盖了。
前面给的方案是站点级设短缓存、assets 设长缓存。更严格的做法是给 index.html 单独加 outboundRules 或者 URL Rewrite 设置响应头,但在实际项目里,站点级设cacheControlMaxAge="7.00:00:00"配合 assets 子目录的独立配置,实践中已经够用了。
另外提醒一句,如果前端项目里用了 Service Worker(PWA 插件),缓存策略会更复杂,SW 会在客户端拦截请求。遇到这类项目,发版的时候要在构建配置里处理好 SW 的版本更新,否则用户可能要清好几次缓存。这类项目我一般会在发版后主动告诉用户"按 Ctrl+F5 强刷一次",比反复解释原理管用。
5. 上线之后的优化与运维小技巧
站点能打开只是及格线,真正体现水平的是后续的稳定性、可维护性和出问题时的恢复速度。
5.1 开启静态压缩,体积能省一半以上
Vue 打包出来的 js 文件动辄几百 KB 到一两兆,开启压缩后传输体积通常能降到三分之一。IIS 的静态压缩在“服务器 → 压缩”功能里配置,勾选“启用静态内容压缩”,然后设置staticCompressionLevel(默认 7,范围 0-9,越高越省带宽但更耗 CPU)。
<httpCompression directory="%SystemDrive%\inetpub\temp\IIS Temporary Compressed Files"> <scheme name="gzip" dll="%Windir%\system32\inetsrv\gzip.dll" staticCompressionLevel="9" /> <staticTypes> <add mimeType="text/*" enabled="true" /> <add mimeType="application/javascript" enabled="true" /> <add mimeType="application/json" enabled="true" /> <add mimeType="image/svg+xml" enabled="true" /> </staticTypes> </httpCompression>要注意的是,压缩和缓存配合使用时有个坑:IIS 会把压缩后的版本缓存到临时目录,如果磁盘满了或者临时目录权限不对,压缩会静默失败甚至报 500。所以启用之后,务必用浏览器的 Network 面板确认响应头里有Content-Encoding: gzip,而不是只看配置文件。
5.2 日志、备份与快速回滚
日志:IIS 默认日志在%SystemDrive%\inetpub\logs\LogFiles\W3SVC{站点ID},是按天分文件的。查问题时用记事本打开太痛苦,推荐装一个 Log Parser 或者直接用 PowerShell 过滤:
Get-Content .\u_ex250315.log | Select-String " 404 "日志里重点关注sc-status(状态码)、cs-uri-stem(请求路径)、time-taken(耗时)。如果发现大量 404 集中在某个路径模式上,基本就能定位到是哪类资源没配好。
备份:IIS 的配置备份是个容易被忽略但极其重要的环节。有两种粒度:
一是配置级备份,用 appcmd 导出整个 IIS 配置:
%windir%\system32\inetsrv\appcmd.exe list config /xml > D:\backup\iis-config-backup.xml恢复的时候用appcmd.exe add config /in <file>或者直接替换 applicationHost.config。IIS 管理器里也有图形化的“共享配置”导出功能,可以导出为加密文件,迁移到另一台服务器时特别方便。
二是站点级备份,其实就是把发布目录整个打包。我的习惯是在发布目录旁边建一个releases文件夹,每次发版把当前版本压缩成vue-admin-20250315.zip,保留最近五个版本。线上出问题时,解压上一个版本覆盖回去,一分钟内完成回滚,比重头排查快得多。
提示:备份文件不要放在站点目录里面,否则会被人通过 URL 直接下载。放到站点物理路径之外的目录,比如
D:\backup\。
5.3 一些零碎但实用的经验
关于汉字目录名和空格。站点物理路径里不要出现中文、空格、特殊符号。IIS 在某些情况下对含空格路径的处理会有问题,特别是配合 URL Rewrite 的时候,反代路径拼接容易出错。统一用英文小写加短横线的命名。
关于端口和防火墙。建站后如果只能本机访问,外部访问不了,先检查 Windows 防火墙的入站规则有没有放行对应端口。快速验证方式是netstat -ano | findstr :8080看端口是否在监听,如果监听正常但外部不通,那基本就是防火墙问题。
关于并发和连接数。静态文件站点的性能瓶颈通常不在 IIS,而在磁盘 IO 和带宽。如果站点访问量大,可以考虑把 dist 目录放到 SSD 上,或者前面挂一层缓存。IIS 本身的并发处理能力对内网几百人的规模来说是绰绰有余的。
关于部署到子目录时的一个细节。如果你在同一个 IIS 站点下挂多个 Vue 应用,比如/admin/和/portal/,记得每个子目录都要有独立的 web.config,并且重写规则的url要指向各自的index.html。同时,publicPath也要分别配置。这三个地方(打包配置、目录结构、web.config)必须一致,缺一个就会出问题。
关于 CI 自动化。手工部署一两次还行,次数多了必然出错。如果团队有条件,可以用 Jenkins 或者 GitLab Runner 在 Windows 上跑构建脚本:拉代码、npm install、npm run build、robocopy 到发布目录、调用appcmd recycle apppool回收应用程序池。这套流程跑通之后,手工部署的错漏基本就绝迹了。
最后说一下我自己的体会。IIS 部署 Vue 这件事,难点从来不在技术本身,而在于信息不对称——前端开发者不熟悉 IIS 的配置层级,运维同学不理解 history 路由为什么需要重写。把这层窗户纸捅破之后,整套流程其实非常稳定,一台 Server 2019 的机器,配好之后连续跑一年多不出问题是很正常的。真正需要花心思的,是第一次配的时候把 web.config 写扎实,把权限收干净,把缓存分层理清楚。这三件事做完了,后面就只剩下发版和回滚两个动作,轻松得很。