1. 先把账算清楚IIS 跑 Vue 到底在跑什么上周帮一个朋友把他那套 Vue3 Vite 的后台管理端从 Linux 迁到一台 Windows Server 2019 上他原话是我在 Windows 下用 IIS 部署 Vue 项目搜了一堆教程全是互相抄的照着做要么白屏要么刷新 404。这其实是很多人第一次在 Windows 上折腾 IIS 部署 Vue 时的共同体验——不是教程错是教程普遍跳过了为什么这一步只告诉你往 web.config 里粘一段规则就完事出了问题完全不知道怎么查。先把最核心的认知立住Vue 打完包之后就是一堆静态文件HTML、CSS、JS、图片、字体仅此而已。它不像 Java 或 .NET 后端那样需要常驻进程、需要运行时、需要端口监听。IIS 在这里扮演的角色本质上和 Nginx、Apache 是同一类东西——一个能对外提供静态文件、并且能按规则改写 URL 的 Web 服务器。理解这一点后面所有配置就都有了解释你不需要为 Vue 装任何运行时不需要配任何特殊模块你要做的只有两件半事情——把文件放对位置、让服务器知道怎么找文件、让 URL 能正确回退。1.1 什么情况下用 IIS 部署前端是合理的不是所有场景都值得上 IIS。如果你手上是一台干净的新机器只是要跑一个纯前端项目我更建议直接用 Nginx 或者干脆用 Node 起个静态服务配置量小一个数量级。但现实里经常遇到这几种情况IIS 就成了唯一或最省事的选择公司已有 Windows Server运维团队只维护 IIS加一台 Linux 机器的流程要审批两周后端是 ASP.NET Core / .NET 那套已经跑在 IIS 上前端想跟它在同一个站点下用同一个域名和端口避免跨域和证书重复配置内网环境服务器上已经装了 IIS 用来跑其他内部系统想复用现有的 80/443 绑定和证书需要 Windows 身份验证域账号免登录这个能力是 IIS 开箱即用的Nginx 做起来要绕一大圈。提示如果你的项目后端是 ASP.NET Core 且必须在 IIS 上跑记得装对应版本的 Hosting Bundle并且把承载前端的那个应用程序池的 .NET CLR 版本设为无托管代码。纯静态前端站点完全不需要 .NET 运行时别被iis 里没有 .NET 8这类报错带着去乱装东西。1.2 路由模式决定了你后面要不要写 URL 规则这是整篇文章里最重要的一条分水岭。打开你的router/index.js看一眼createRouter的配置// 模式一hash 模式URL 里带 # const router createRouter({ history: createWebHashHistory(), routes }) // 模式二history 模式URL 干净但需要服务端配合 const router createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes })hash 模式下浏览器访问的是http://host/#/user/list#后面的内容浏览器自己处理根本不会发给服务器。所以 IIS 这一侧你什么都不用配文件扔进去就能跑。缺点也明显URL 不好看分享出去的链接带个井号某些统计工具和 SEO 场景会受影响。history 模式下URL 是http://host/user/list。用户在首页点导航没问题因为那是前端路由在拦截但一旦用户在/user/list这个地址上按了 F5浏览器就会真的向服务器请求/user/list这个路径。IIS 在物理目录下找不到叫user的文件夹直接给你一个 404。这就是点进去正常、一刷新就 404的根本原因。解决办法就是让它找不到文件时把index.html吐回去剩下的路由判断交给 Vue Router。这个回退动作在 IIS 上靠URL Rewrite模块完成。所以记住这条判断链hash 模式 → 只放文件history 模式 → 文件 一条回退规则。后面第三章会给出完整写法。1.3 资源路径前缀白屏问题的八成来自这里另一个必须在打包前想清楚的问题你的站点是部署在根路径还是子路径。根路径http://host/访问首页就是http://host/子路径http://host/admin/作为一个子应用挂在已有站点下面。Vue 打包出来的index.html里引用资源的路径是打包时写死的。如果你按根路径打包base: /却把文件放到了子目录/admin下那么index.html会去请求/assets/index-abc123.js而这个文件实际在/admin/assets/index-abc123.js。结果是页面能打开、标题正常、但一片空白F12 里一堆 404。这就是最常见的白屏。解决方式是在打包配置里把 base 设对。第三章会给出 Vite 和 vue-cli 两种写法。这里先建立一个直觉子路径部署时base 的结尾必须有斜杠而且要和 IIS 里的目录名严格一致包括大小写。我见过太多次因为 IIS 里建的是Admin、base 写的是/admin/而导致的诡异问题——Windows 文件系统本身不区分大小写但浏览器拼接 URL 和某些前端库的内部匹配是区分的。2. 打包这一步Vite 与 vue-cli 的配置差异与取舍很多教程直接从IIS 里新建网站开始讲把打包当成一句话带过结果读者卡在上传什么、传到哪儿、为什么路径不对。我把打包这一段单拎出来讲透因为部署期的绝大多数问题都是打包期埋下的。2.1 base 与 publicPath一个参数决定整条路径链Vite 项目在vite.config.js里配import { defineConfig } from vite import vue from vitejs/plugin-vue import path from node:path export default defineConfig({ // 根路径部署写 /子路径部署写 /admin/ base: /admin/, plugins: [vue()], resolve: { alias: { : path.resolve(__dirname, src) } }, build: { outDir: dist, assetsDir: assets, sourcemap: false, // 生产环境务必关掉 chunkSizeWarningLimit: 1500, rollupOptions: { output: { // 把大依赖拆开减少单文件体积 manualChunks: { vue: [vue, vue-router, pinia], ui: [element-plus] } } } } })vue-cliwebpack项目在vue.config.js里配const { defineConfig } require(vue/cli-service) module.exports defineConfig({ // 注意这里叫 publicPath5.0 之前叫 baseUrl publicPath: process.env.NODE_ENV production ? /admin/ : /, outputDir: dist, assetsDir: static, productionSourceMap: false, devServer: { proxy: { /api: { target: http://127.0.0.1:8080, changeOrigin: true } } } })注意publicPath用来解决的是静态资源从哪儿加载它管不了前端路由的跳转。history 模式下如果你部署在子路径还要给路由指定 base也就是上面代码里的createWebHistory(/admin/)或者用import.meta.env.BASE_URL让它自动跟随。这两处不一致就会出现首页正常、点导航跳到了根路径的现象。2.2 sourcemap 与拆包不只是体积问题sourcemap: false这个开关新手容易忽略。它不是纯粹为了省几 MB 空间——sourcemap 文件会把你的源码结构、注释甚至部分业务逻辑暴露在公网可访问的.map文件里。内网环境风险低一些但公网站点关掉它应该是默认动作。如果确实需要线上排查也建议把 map 文件单独放、只对特定 IP 开放。拆包manualChunks对 IIS 部署有额外的意义IIS 静态压缩默认对超过一定大小的文件不做压缩或者压缩开销很大。把 2MB 的vendor.js拆成几个 300~500KB 的块压缩效果和首屏加载体验都会明显好一些。我实测过一个后台项目拆包后 gzip 前的总体积没变但 gzip 后的传输量少了将近 20%因为小文件里有更多重复字符串模式能被字典命中。2.3 打包产物长什么样你该上传哪些东西执行npm run build之后dist目录内容大致是这样文件/目录说明是否上传index.html入口页引用所有哈希化资源必须assets/打包后的 JS/CSS/字体/图片必须favicon.ico站点图标通常放public/下必须robots.txt若public/下存在按需*.mapsourcemap不上传只上传dist里的内容不要把dist这个文件夹本身再套一层。这是另一个高频翻车点物理路径配的是D:\sites\admin结果文件实际在D:\sites\admin\dist\index.htmlIIS 找不到默认文档直接 403.14。我一般的做法是本地打完包把dist里的文件压缩后拷到服务器上一个临时目录解压后核对一眼index.html是否在物理路径的第一层然后再覆盖正式目录。2.4 打包前值得顺手检查的几项在打包命令敲下去之前我会习惯性过一遍这几项能省掉后面不少来回package.json里的scripts.build是否带了--mode production确保走的是生产环境变量文件.env.production生产环境的 API 地址是否已经指向真实域名或相对路径/api相对路径更适合 IIS 反向代理或同站点后端路由 base 与base/publicPath是否一致是否有硬编码在代码里的localhost:8080是否残留了console.log大对象的调试代码大数据量打印会拖慢生产环境。3. IIS 侧配置从建站到 URL Rewrite 的完整落地前面都是准备工作这一章开始动服务器。我按实际操作的顺序写每一步都会说明为什么这么做而不是只给点击路径。3.1 站点、应用程序与应用程序池的关系打开 IIS 管理器左侧树形结构里Sites下面默认会有一个Default Web Site。我通常不直接在它上面部署而是新建一个站点或应用程序理由有两个一是默认站点绑定着 80 端口容易和已有系统冲突二是独立站点/应用程序有独立的应用池重启一个不影响另一个。这里有个容易被忽略的决策子路径部署时用添加应用程序还是添加虚拟目录。虚拟目录共享父站点的应用程序池和大部分配置它更像是父站点下的一个文件夹映射应用程序有自己独立的应用程序池、独立的配置边界可以有自己的 web.config 而不继承父站点的某些设置。对于一个 SPA 前端我强烈建议用应用程序。原因很实在SPA 需要自己的 URL Rewrite 回退规则而这条规则如果写在虚拟目录里会继承父站点的规则链可能出现父站点规则先命中、导致回退失效的情况另外应用池独立之后将来要重启它也不会影响同服务器上的其他站点。关键配置对照表配置项建议值说明应用程序池 .NET CLR 版本无托管代码纯静态前端不需要 .NET 运行时托管管道模式集成配合 URL Rewrite 使用更自然物理路径D:\sites\admin示例里面直接是index.html和assets默认文档index.html必须加否则访问目录得到 403.14绑定端口 80/443 主机名443 需绑定证书预加载已启用True可选减少首次访问冷启动提示默认文档里如果index.html排在Default.htm、iisstart.htm之后而目录里恰好有同名文件IIS 会优先返回那个。稳妥做法是把index.html移到列表最上面或者干脆删掉用不上的默认项。3.2 URL Rewrite 模块与 web.config 的完整写法URL Rewrite 不是 IIS 自带模块需要单独安装URL Rewrite Module 2.1微软官方提供。判断装没装的方法很简单IIS 管理器里选中站点中间功能区如果没有URL 重写图标就是没装。没有这个模块的情况下你粘一个含rewrite节点的 web.config 进站点根目录整个站点会直接返回 HTTP 500.19错误代码 0x8007000d页面完全打不开。这个现象的诡异之处在于你明明只是加了个配置文件结果原本能访问的静态页也挂了很多人会误以为是权限问题。模块装好之后在站点根目录放一个web.config?xml version1.0 encodingUTF-8? configuration system.webServer !-- 1. 前端路由回退找不到文件的请求统统交给 index.html -- rewrite rules rule nameVue History Fallback stopProcessingtrue match url.* / conditions logicalGroupingMatchAll add input{REQUEST_FILENAME} matchTypeIsFile negatetrue / add input{REQUEST_FILENAME} matchTypeIsDirectory negatetrue / !-- 排除接口路径避免把 API 请求也回退成页面 -- add input{REQUEST_URI} pattern^/api/ negatetrue / /conditions action typeRewrite url/admin/index.html / /rule /rules /rewrite !-- 2. 补一些旧系统缺失的 MIME 映射 -- staticContent remove fileExtension.woff2 / mimeMap fileExtension.woff2 mimeTypefont/woff2 / remove fileExtension.json / mimeMap fileExtension.json mimeTypeapplication/json / /staticContent !-- 3. 一些基础安全响应头 -- httpProtocol customHeaders add nameX-Content-Type-Options valuenosniff / remove nameX-Powered-By / /customHeaders /httpProtocol /system.webServer /configuration几个要点值得展开说。stopProcessingtrue表示这条规则命中后就不再往下匹配后续规则避免和其他规则打架。negatetrue是取反意思是当请求的路径不是真实存在的文件、也不是真实存在的目录时才回退——真实存在的assets/index-abc.js会正常返回不会被回退干扰。action里的 URL 要写相对于站点根的路径如果你用的是独立站点部署到根路径就写/index.html如果挂在应用/admin下写/admin/index.html或者用相对写法index.html也可以但绝对路径更不容易出错。3.3 应用程序池权限与那类 0x80005000 报错站点建好、文件放好、规则配好访问还是 500 或 403大概率是权限。IIS 的应用程序池默认以IIS AppPool\池名这个虚拟账号身份读取文件。如果你把文件放在D:\这种非系统盘或者手动改过目录 ACL很容易出现应用池标识没有读取权限。标准的解决方式有两种。一种是图形界面右键站点 → 管理网站 → 高级设置 → 记下应用程序池名然后在文件资源管理器里给物理目录添加IIS AppPool\你的池名的读取和执行权限。另一种我更常用——命令行批量且可脚本化icacls D:\sites\admin /grant IIS AppPool\AdminPool:(OI)(CI)(RX) /T /C(OI)(CI)表示对象继承和容器继承RX是读取和执行/T递归子目录/C是遇到错误继续。这条命令在批量部署多套环境时特别省事。至于应用程序池权限设置失败请手动为其设置 LocalSystem 权限未知错误 0x80005000这类报错我在几台服务器上都遇到过根因基本集中在三种情况物理路径根本不存在或不完整IIS 想给一个不存在的目录设 ACL直接失败服务器在域环境里IIS 试图解析账号的 SID 时和域控通信异常报出来的就是 0x80005000IIS 管理器不是以管理员权限运行改 ACL 的动作被系统拒绝但错误信息被包装成了权限设置失败。我的处理顺序是先在命令行里确认路径存在dir D:\sites\admin再用上面那条icacls手动授权最后重启应用池。不要照搬错误提示去给目录加 LocalSystem 权限——那等于把整个目录敞开给系统最高权限Windows 上的提权风险会明显上升而且它根本不是正确的修法。3.4 压缩与缓存让传输量真正降下来IIS 的静态压缩默认是关闭的需要先在服务器角色 → Web 服务器 → 性能里勾选静态内容压缩装完之后在站点或服务器级别的压缩功能里启用。启用后IIS 会对text/html、text/css、application/javascript等 MIME 类型做 gzip。这里有个特别容易踩的坑Vite 打包可以生成.brBrotli和.gz预压缩文件但 IIS 原生不认这些预压缩产物它只会自己实时压缩。所以你带着一堆.js.br上传上去IIS 依然会传原始.js白白占了磁盘。想要 Brotli要么给 IIS 装第三方压缩模块要么干脆放弃预压缩交给 IIS 的 gzip 处理——对绝大多数后台系统来说gzip 已经完全够用。另一个坑是静态压缩的 MIME 白名单。IIS 的applicationHost.config里维护着staticCompression的 MIME 列表如果某个类型不在里面就不会被压缩。旧的 IIS 版本里.js的映射有时是text/javascript有时是application/javascript两边对不上就不压缩。排查方法很直接F12 看响应头有没有Content-Encoding: gzip。4. 上线后高频故障的排查链路这一章我按现象 → 排查动作 → 根因的方式来写你可以直接照着走一遍。这也是我觉得最有价值的部分因为这些现象在教程里很少被系统整理但实际部署时几乎必然会撞上至少一个。4.1 页面标题正常但一片空白路径前缀错位这是最经典的。判断方法按 F12 打开 Network 面板刷新页面看index.html本身是不是 200。如果它是 200但下面一堆 JS/CSS 全是 404那基本可以确定是资源路径错了。这时看 404 的请求 URL。如果请求的是/assets/xxx.js但你实际部署在/admin/assets/xxx.js说明打包时base/publicPath没设成/admin/。反过来请求的是/admin/assets/xxx.js但你的文件在/assets/说明 base 设多了。改配置、重新打包、重新上传即可。还有一种更隐蔽的情况index.html是 200资源也全是 200但页面还是白的控制台报Uncaught SyntaxError: Unexpected token 。这通常意味着某个 JS 请求实际返回的是 HTML 内容——也就是你的回退规则把 JS 请求也回退了。回头检查matchTypeIsFile那个条件有没有写对或者物理路径下这个文件是否真的存在。4.2 首页正常、刷新就 404history 模式缺少回退前面说过原理了这里补充排查动作。先确认路由模式createWebHashHistory还是createWebHistory。hash 模式出现这个问题说明你在某处错误地改成了 historyhistory 模式出现这个问题就去检查站点根目录有没有web.config、URL Rewrite 模块装没装、规则里的url路径对不对。有一个容易忽略的变体根路径访问正常带参数或带多级路径就 404。比如/user/detail/123能打开/user/detail打不开。这种情况下多半是规则里加了精确匹配或者正则限制把部分路径排除在外了。用match url.* /全匹配、只靠条件过滤是最省心的写法。4.3 接口报 404/405前后端边界没划清前端页面跑起来了登录接口报 404这种问题的排查思路和纯静态问题完全不同得分清是请求根本没到后端还是到了后端被拒。先在 F12 里看请求的实际 URL 和状态码。如果是 404 且响应体是一段 HTML说明请求被回退规则吞掉、返回了index.html。解决办法是在回退规则里显式排除接口前缀也就是前面web.config里那条pattern^/api/ negatetrue。这里的/api/必须和你前端里 axios 的baseURL完全一致。如果是 405通常是方法不被允许。IIS 的 WebDAV 模块有时候会拦截PUT、DELETE、PROPFIND这类方法返回 405。处理方式是在web.config里把 WebDAV 模块移除system.webServer modules remove nameWebDAVModule / /modules handlers remove nameWebDAV / /handlers /system.webServer如果是跨域报错那说明前后端不在同一个源上。最稳的方案是把后端也挂到同一个 IIS 站点下比如/api走反向代理这样根本没有跨域问题。次选是在后端或者 IIS 层加 CORS 响应头但要注意Access-Control-Allow-Origin和Allow-Credentials不能同时用通配符。4.4 400/500 类报错从 IIS 日志反推有些问题前端看不出来得回到服务器上看日志。IIS 日志默认在%SystemDrive%\inetpub\logs\LogFiles\W3SVC站点ID下按天切分。日志里能直接看到请求的 URL、状态码、子状态码和耗时。几个常见状态码的含义状态码子状态码常见含义40314目录列表被拒通常是没配默认文档4043MIME 类型未映射比如.woff2缺失4044处理程序未映射通常出现在动态请求上50019web.config 配置错误常见是模块未安装50030URL Rewrite 规则内部错误5023反向代理时后端不可达我一般会先用appcmd确认站点状态和配置有没有语法问题%windir%\system32\inetsrv\appcmd list site %windir%\system32\inetsrv\appcmd list apppool改动多了之后配置容易越改越乱这时候上线前做一次配置备份能救命%windir%\system32\inetsrv\appcmd list site /config /xml d:\backup\sites-20240101.xml %windir%\system32\inetsrv\appcmd list apppool /config /xml d:\backup\apppools-20240101.xml出问题时对照备份文件比对比盲改快得多尤其是接手别人维护的服务器时。5. 缓存策略与上线流程让部署这件事可重复文件放上去能跑只是第一次成功。真正让部署这件事变得轻松的是可重复、可回滚、不会因为缓存导致用户看到旧页面。5.1 index.html 绝不能被强缓存Vue 打包的 JS/CSS 文件名带内容哈希文件内容一变文件名就变所以它们可以设很长的缓存时间——一年都没问题这是行业惯例。但index.html是个例外它引用的哈希文件名每次发版都会变如果它被用户浏览器缓存住了用户就会拿着旧 HTML 去请求已经删掉的旧 JS结果就是白屏。IIS 的clientCache是针对静态内容的统一设置如果粗暴地设成一年.html也会被一起缓存。所以需要给index.html单独指定策略location pathindex.html system.webServer httpProtocol customHeaders add nameCache-Control valueno-cache, no-store, must-revalidate / add namePragma valueno-cache / /customHeaders /httpProtocol /system.webServer /location要对assets目录设长缓存则在web.config根级别加staticContent clientCache cacheControlModeUseMaxAge cacheControlMaxAge365.00:00:00 / /staticContent注意location节点如果在站点 web.config 里报此配置节不能在此路径中使用说明该节被锁定了需要在服务器级别用appcmd unlock config解锁或者把 index.html 换一种方式处理。改之前先备份配置别在没备份的情况下动 applicationHost.config。5.2 到底该让 IIS 做反向代理还是交给别的组件不少项目是前端挂在 IIS、后端跑在另一台机器上。这时候有两种做法让 IIS 用 URL Rewrite 的Rewrite动作把/api转发到后端需要装 Application Request Routing 模块或者让前端直接请求后端的完整域名。我的经验是如果后端就在同一台机器上、同一个内网里用 IIS 反向代理最省事因为前端代码里写相对路径/api就行开发、测试、生产三套环境不用改代码也不用处理跨域和证书问题。如果后端在别的网络区域或者性能要求高那还是让专业的网关组件去做IIS 在这个位置更多是过渡方案。用 ARR 做反向代理时记得开启代理功能在 ARR 的 Server Proxy Settings 里勾选 Enable proxy否则规则写了也不生效——这个开关藏在服务器级别而不是站点级别很多人找不到。5.3 一份可以照着走的部署自检清单最后给一份我常用的检查清单发版前照着过一遍能挡掉九成以上的问题打包配置里的base/publicPath与部署路径一致路由 base 与打包 base 一致dist里的内容直接位于物理路径第一层没有多套一层目录默认文档里有index.html且排在第一URL Rewrite 模块已安装web.config无语法错误回退规则排除了/api前缀应用程序池标识对物理目录有读取和执行权限index.html已设置不缓存assets已设置长缓存静态压缩已启用响应头里能看到Content-Encoding关闭 sourcemap删除调试代码和测试环境地址部署前已用appcmd导出配置备份手动在浏览器里对 3~5 个深层路由做一次刷新测试。5.4 子路径部署时容易忽略的两个细节子路径部署除了 base还有两个地方会出问题我踩过不止一次。一是Cookie 的 path。如果登录态是存在 Cookie 里的而 Cookie 的 path 设成了/那它会在整个域名下可见正常但如果后端设置时带了具体路径或者你在子路径下发现登录态丢失就得去检查这条 Cookie 的 path 是不是覆盖了/admin。二是在CSS 里通过url()引用的背景图或字体。这类资源在构建时会被 Vite 处理成带 base 的绝对路径一般不会有问题但如果代码里写了url(/images/bg.png)这种以/开头的硬编码路径构建工具不会帮你加 base浏览器会去根路径找自然 404。这种问题在本地开发时因为根路径恰好是对的完全看不出来一上生产就暴露。排查方式很简单全局搜一遍url(/和src/改成相对路径或者用import引入。我自己从 Nginx 转到 IIS 部署前端的过程中最深的体会是Nginx 那套try_files $uri $uri/ /index.html一行搞定的事情在 IIS 上被拆成了装模块 写 XML 处理权限 处理配置锁定四步学习曲线确实更陡。但反过来IIS 的图形化界面在排查权限和绑定问题时也更直观日志和配置导出机制做得相当规范。如果你后续还要在这台机器上部署其他前端项目建议把调好的web.config做成模板存下来改一下回退路径就能复用第二次部署的时间能压缩到十分钟以内。
阅读完成 · 觉得有帮助?