先给结论:真要在应用初始化之前就跳走,判断逻辑必须写在 createApp 之前,或者更早——直接放进 index.html 的头部内联脚本里。路由守卫的 beforeEach 看着像"进页面前",但它已经在应用挂载之后,不属于初始化之前。

需要这样做的场景通常是域名迁移、未授权跳转、活动页引流。这类需求有个共同点:一旦命中条件,当前这个单页应用根本没必要启动,多跑一毫秒都是浪费,还可能因为一闪而过的页面造成闪烁。
本文给出三个可用的跳转时机与各自的边界,重点讲怎么避免"跳出去又被跳回来"的死循环,最后附一份验证清单。示例基于 Vue 3 与 vue-router 4,Vue 2 的差异会单独标出。
先看结论
- 最早的时机:index.html 里的内联脚本,在任何 JS 包下载之前就完成判断。
- 常规做法:main.js 里在 createApp 之前判断,用 location.replace 跳走,不留历史记录。
- 路由守卫不算"初始化之前":那时应用已经挂载,适合做登录校验,不适合做域名级跳转。
路由与实例的生命周期容易混,Vue3 教程 里按启动流程画了完整顺序,对照着看更清楚。
一、最小场景与预期行为
把任务定死:站点域名从 old.example.com 迁到 new.example.com,所有访问旧域名的请求都要直接跳到新域名对应路径,且浏览器的后退按钮不能退回旧域名。
输入是一段入口代码。预期行为是:命中旧域名时不再初始化 Vue 应用,直接跳转,地址栏里只有一条新域名的历史记录。
// main.js
const OLD_HOST = 'old.example.com';
const NEW_HOST = 'new.example.com';
if (location.host === OLD_HOST) {
const target = `https://${NEW_HOST}${location.pathname}${location.search}${location.hash}`;
location.replace(target);
} else {
const { createApp } = await import('vue');
const App = (await import('./App.vue')).default;
createApp(App).mount('#app');
}
这段做的是:先比对主机名,命中旧域名就拼出新地址并用 location.replace 跳走;不命中才动态导入 Vue 并挂载。把 createApp 放进 else 分支是关键——命中跳转时整个应用包都不会被初始化。
用 location.replace 而不是 location.href 的原因在历史记录:replace 会替换当前这条记录,用户点后退不会回到旧域名再被弹一次。用 href 则会留下一条旧域名记录,形成"后退又被弹回来"的循环。
二、三种跳转时机与取舍
| 时机 | 执行位置 | 代价 |
|---|---|---|
| index.html 内联脚本 | 最早,包下载之前 | 逻辑写在 HTML 里,不好测试 |
| main.js 入口判断 | createApp 之前 | 应用包会被下载 |
| 路由守卫 beforeEach | 应用挂载之后 | 不满足"初始化之前" |
2.1 最早的一档:index.html 内联脚本
<script>
if (location.host === 'old.example.com') {
location.replace('https://new.example.com' + location.pathname + location.search);
}
</script>
这段做的是在任何外部脚本执行之前完成主机判断与跳转。它的价值在于连应用包都不用下载,对首屏影响最小,适合整站迁移这种命中率极高的场景。
代价是这段逻辑脱离了构建体系:没法引入工具函数、没法写单元测试、改动要重新部署静态页。逻辑简单时可用,复杂了就难维护。
2.2 常规做法:入口文件里判断
import { createApp } from 'vue';
import App from './App.vue';
if (location.host === 'old.example.com') {
location.replace('https://new.example.com' + location.pathname);
} else {
createApp(App).mount('#app');
}
这段做的是在入口文件里先判断再挂载。相比内联脚本,它可以用项目里的常量、环境变量和工具函数,逻辑可以抽成模块单独测试。
代价是应用包仍然会被下载解析(除非像第一节那样用动态 import 把重活放进 else 分支)。对多数项目这点开销可以接受。
2.3 路由守卫:为什么它不算"初始化之前"
router.beforeEach((to, from) => {
if (location.host === 'old.example.com') {
location.replace('https://new.example.com' + to.fullPath);
return false;
}
return true;
});
这段做的是在每次路由跳转前检查主机名。它能工作,但时机已经晚了:此时 Vue 应用实例已创建、根组件已挂载、首屏可能已经渲染过一帧。
路由守卫的适用场景是登录态校验这类"页面级拦截",而不是域名级引流。区分标准很简单:如果跳转条件与具体路由无关(比如只看域名),就该放在更早的入口;如果依赖目标路由的元信息,才用守卫。
三、怎么避免跳出去又跳回来
这是这类需求里最常见的线上事故:用户点后退回到旧域名,脚本再次触发跳转,看起来就像后退键失灵。
三个办法,按优先级排:
- 用 location.replace 而不是 location.href:替换当前记录,不留旧域名这一条。
- 跳转前写入标记:把跳转状态存进 sessionStorage,回来时不再重复跳。
- 旧域名侧直接配 301:用服务器或 CDN 的跳转规则处理,前端完全不参与。
第 3 条其实是最优解:域名级迁移本来就该在网关层做,前端脚本只是没有服务器权限时的权宜之计。能在 Nginx 或 CDN 上配 301,就不要写在代码里。
第 2 条的写法如下:
const FLAG = 'redirected';
if (location.host === 'old.example.com' && !sessionStorage.getItem(FLAG)) {
sessionStorage.setItem(FLAG, '1');
location.replace('https://new.example.com' + location.pathname);
}
这段做的是:跳转前先写一个标记,用户退回来时标记还在,就不会再被弹一次。sessionStorage 的生命周期是标签页级别,关掉重开会自动清除,符合"用户主动回退就不再拦截"的预期。

四、常见失败与验证清单
4.1 用 router.push 跳外链没反应
router.push 只处理应用内部路由,传一个完整 URL 会被当成路径解析,结果是 404 或匹配到兜底路由。修复办法是外部地址一律用 location.replace 或 location.href。
4.2 守卫里跳转造成死循环
守卫里改 location 之后没有 return false,路由继续解析,反复触发。修复办法是在跳转分支里显式 return false 终止本次导航。
4.3 本地开发时被误跳
判断条件写死了域名,本地 localhost 也可能命中反向条件。修复办法是把白名单做成配置项,用环境变量区分开发与生产。
4.4 验证清单
- 检查:确认跳转条件写在 createApp 之前
- 运行:把 hosts 或环境变量改成旧域名,访问页面
- 预期:直接跳转,页面没有闪一下再跳
- 失败:跳出去后退又被弹回 → 检查是不是用了 location.href
- 修复:换成 location.replace,或补上 sessionStorage 标记
- 验证:跳转后连点两次后退,确认能回到上一个正常页面而不是被弹住
上面这些步骤做的是确认时机、复现跳转、验证历史记录行为。预期结果(未在本机执行)是后退能正常退出;若被反复弹回,说明用的是 href 而非 replace。

总结
在初始化之前跳转外部页面,判断要放在 createApp 之前或 index.html 的内联脚本里;路由守卫属于挂载之后,只适合页面级拦截。跳外链用 location.replace,别用 router.push。
三条要点带走:
- 跳转时机分三档,越早代价越小,可维护性也越低,按条件命中率选;
- location.replace 与 location.href 的差别在历史记录,用错就会出现后退死循环;
- 域名级迁移优先在网关配 301,前端脚本只是没有服务器权限时的替代方案。
下一步建议把跳转判断抽成一个独立模块并写单元测试,条件变化时只改一处。启动流程的细节记不清时,翻 Vuejs 教程 最快。
常见问题
Q:为什么用 router.push 跳外部地址没反应?
A:router.push 只处理应用内部路由,传完整 URL 会被当路径解析。外部地址要用 location.replace 或 location.href。
Q:路由守卫里的跳转算不算初始化之前?
A:不算。守卫触发时实例已创建、根组件已挂载。它适合登录校验,不适合域名级引流。
Q:跳过去之后点后退又被弹回来,怎么解决?
A:改用 location.replace 替换当前历史记录,或者在跳转前写一个 sessionStorage 标记,回来时不再触发。
Q:Vue 2 的写法有什么不同?
A:判断位置一样,都在 new Vue 之前。两个版本的差异只在挂载语句:Vue 3 写 createApp(App).mount,Vue 2 写 new Vue({ render }).$mount,跳转部分完全相同。
Q:本地开发总是被误跳怎么处理?
A:把域名白名单做成环境变量配置,开发环境留空跳过判断。不要在入口文件里写死主机名的字面量。
延伸学习
想把入口与启动流程补完整,可以按这个顺序来:
- 读一篇相关笔记 index.htm 文件是什么,把入口页的作用看清;
- 再读一篇 vue 和 vue-cli 有什么区别和关联,理清工程与框架的分工;
- 想确认 HTML 元素的写法,翻 Glossary Html 教程。

TRAE-AI编程



