最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Vue2路由地址欄變化API(pushState和replaceState)的避坑指南

 更新時(shí)間:2026年02月24日 08:50:58   作者:DTcode7  
你是不是也遇到過這種尷尬場景:明明URL變了,頁面卻沒刷新?或者一刷新直接404教做人?別慌,這都是history.pushState和replaceState在搞事情,因此本文給大家介紹了Vue2路由地址欄變化API的避坑指南,需要的朋友可以參考下

開篇先嘮兩句,兄弟們,今天不整那些虛的,就聊聊Vue2里路由切換時(shí)地址欄咋變的這事兒。你是不是也遇到過這種尷尬場景:明明URL變了,頁面卻沒刷新?或者一刷新直接404教做人?別慌,這都是history.pushState和replaceState在搞事情。我當(dāng)年也是踩了一堆坑才搞明白的,今天就把這些血淚經(jīng)驗(yàn)掏心窩子分享給你們。

這倆API到底是個(gè)啥來頭

先說清楚啊,pushState和replaceState可不是Vue發(fā)明的,人家是HTML5原生就帶的能力。Vue Router的history模式就是站在巨人肩膀上玩出來的。簡單理解就是:這倆兄弟能讓地址欄URL變來變?nèi)?,但頁面就是不刷新,是不是很神奇?/p>

pushState像是往歷史記錄里加一條新記錄,replaceState則是把當(dāng)前這條記錄給替換掉。聽起來差不多?實(shí)際用起來差別可大了去了。

原生API的基本用法

在深入Vue之前,咱們先看看這倆API裸奔時(shí)長啥樣:

// pushState - 往歷史記錄里塞一條新的
history.pushState({page: 1}, "標(biāo)題", "/page1");

// replaceState - 把當(dāng)前這條記錄給替換了
history.replaceState({page: 2}, "標(biāo)題", "/page2");

看到?jīng)]?參數(shù)結(jié)構(gòu)一模一樣,都是三個(gè)參數(shù)。第一個(gè)參數(shù)是個(gè)對象,可以存點(diǎn)數(shù)據(jù);第二個(gè)是標(biāo)題,現(xiàn)在基本沒用;第三個(gè)是URL。但注意了,這URL必須跟當(dāng)前頁面同源,跨域?瀏覽器直接給你報(bào)錯(cuò)沒商量。

// 跨域操作?門兒都沒有!
try {
  history.pushState({}, "", "https://baidu.com/some-page");
} catch (e) {
  console.error("報(bào)錯(cuò)了吧!SecurityError: The operation is insecure.");
}

瀏覽器這個(gè)安全限制是鐵律,想鉆空子?沒戲。所以那些想偷偷改域名跳轉(zhuǎn)到釣魚網(wǎng)站的想法,趁早打消。

為什么需要這倆API

以前咱們做SPA(單頁應(yīng)用),地址欄不動(dòng),用戶點(diǎn)前進(jìn)后退直接懵逼,因?yàn)闉g覽器不知道你內(nèi)部路由變了。有了這倆API,地址欄能跟著變,用戶的前進(jìn)后退按鈕也好使了,體驗(yàn)瞬間絲滑。

但問題來了——Vue Router為啥不讓我們直接用這倆API?往下看你就知道了。

扒開源碼看看Vue2咋玩的

Vue Router在history模式下,內(nèi)部其實(shí)就是調(diào)用了這兩個(gè)API。但注意了,Vue可不只是簡單調(diào)用一下就完事了,它還干了不少活兒。

Vue Router的history模式初始化

咱們看看Vue Router初始化時(shí)都干了啥:

// router/index.js 里常見的配置
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('../views/About.vue') // 懶加載,省流量
  }
]

const router = new VueRouter({
  mode: 'history',  // 關(guān)鍵配置!開啟history模式
  base: process.env.BASE_URL,
  routes
})

export default router

看到那個(gè)mode: 'history'沒?這就是開關(guān)。一旦開啟,Vue Router就會(huì)開始操作window.history。

底層到底怎么調(diào)用的

Vue Router的源碼里,history模式的實(shí)現(xiàn)主要在src/history/html5.js(如果你去翻源碼的話)。核心邏輯大概長這樣:

// 這是Vue Router內(nèi)部的大致實(shí)現(xiàn)思路,不是完整源碼
class HTML5History extends History {
  constructor(router, base) {
    super(router, base)
    
    // 初始化時(shí)先處理一下當(dāng)前URL
    const initLocation = getLocation(this.base)
    // 監(jiān)聽popstate事件,處理瀏覽器前進(jìn)后退
    window.addEventListener('popstate', e => {
      const current = this.current
      // 處理路由變化...
      this.transitionTo(location, route => {
        if (e.state) {
          // 處理state數(shù)據(jù)
        } else {
          // 兼容處理,有些瀏覽器popstate不觸發(fā)state
        }
      })
    })
  }

  push(location, onComplete, onAbort) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
      // 關(guān)鍵代碼!調(diào)用原生pushState
      pushState(cleanPath(this.base + route.fullPath))
      // 觸發(fā)afterEach鉤子
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    }, onAbort)
  }

  replace(location, onComplete, onAbort) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
      // 關(guān)鍵代碼!調(diào)用原生replaceState
      replaceState(cleanPath(this.base + route.fullPath))
      handleScroll(this.router, route, fromRoute, true)
      onComplete && onComplete(route)
    }, onAbort)
  }
}

// 封裝的pushState函數(shù)
function pushState(url, replace) {
  // 保存滾動(dòng)位置
  saveScrollPosition()
  // 調(diào)用原生API
  try {
    if (replace) {
      history.replaceState({ key: getStateKey() }, '', url)
    } else {
      history.pushState({ key: getStateKey() }, '', url)
    }
  } catch (e) {
    // 降級處理,萬一不支持就強(qiáng)制跳轉(zhuǎn)
    window.location[replace ? 'replace' : 'assign'](url)
  }
}

看到?jīng)]?Vue Router在調(diào)用原生API之前和之后,干了這么多事兒:

  1. 觸發(fā)路由守衛(wèi)函數(shù) - beforeEach、beforeResolve、afterEach這一套流程走下來
  2. 更新currentRoute狀態(tài) - 全局的$route對象要更新,所有依賴它的組件都要重新渲染
  3. 處理滾動(dòng)行為 - 記住頁面滾動(dòng)位置,返回時(shí)恢復(fù)
  4. 監(jiān)聽popstate事件 - 用戶點(diǎn)瀏覽器前進(jìn)后退按鈕時(shí),Vue要能感知到

你要是直接拿原生API去搞,這些功能一個(gè)都沒有,到時(shí)候別怪組件不更新、守衛(wèi)不觸發(fā)。

直接調(diào)用原生API的后果

不信邪?咱們試試直接調(diào)用原生API會(huì)發(fā)生什么:

// 在某個(gè)Vue組件里,你腦子一抽寫了這行代碼
history.pushState({}, "", "/new-page");

// 結(jié)果:
// 1. 地址欄確實(shí)變成 /new-page 了 ?
// 2. 但是!Vue Router根本不知道這事 ?
// 3. $route.path 還是舊的 ?
// 4. 路由守衛(wèi)沒觸發(fā) ?
// 5. 組件沒切換 ?
// 6. 用戶刷新頁面,直接404或者顯示/new-page對應(yīng)的內(nèi)容(如果有的話)

// 更慘的是,這時(shí)候用戶點(diǎn)瀏覽器后退按鈕
// Vue Router會(huì)一臉懵逼:這是哪?我沒記錄過這個(gè)路由?。?
// 然后各種異常行為就出現(xiàn)了

這就是為什么我一直強(qiáng)調(diào):能用Vue Router封裝好的方法就別自己調(diào)用原生API。

兩個(gè)API的參數(shù)都長啥樣

這倆兄弟的參數(shù)結(jié)構(gòu)是一模一樣的,都是三個(gè)參數(shù)。但每個(gè)參數(shù)都有講究,咱們掰開揉碎了說。

第一個(gè)參數(shù):state對象

這個(gè)對象可以存一些跟這個(gè)路由狀態(tài)相關(guān)的數(shù)據(jù),比如用戶信息、頁面狀態(tài)啥的。傳null也行,但傳了的話以后可以通過history.state取回來。

// 存點(diǎn)有用的數(shù)據(jù)
history.pushState(
  { 
    userId: 12345, 
    fromPage: 'home',
    scrollPosition: 500,
    timestamp: Date.now()
  }, 
  "", 
  "/user/profile"
);

// 以后可以通過event.state取到
window.onpopstate = function(event) {
  console.log("之前存的數(shù)據(jù):", event.state);
  // 輸出:{ userId: 12345, fromPage: 'home', ... }
  
  // 可以恢復(fù)滾動(dòng)位置
  if (event.state && event.state.scrollPosition) {
    window.scrollTo(0, event.state.scrollPosition);
  }
};

Vue Router自己也用了這個(gè)特性,你看它存的key就是用來識別路由狀態(tài)的。

第二個(gè)參數(shù):title

新頁面的標(biāo)題。不過說實(shí)話現(xiàn)在大部分瀏覽器都不鳥這個(gè)參數(shù),傳null就完事兒了。以前Safari還支持一下,現(xiàn)在基本統(tǒng)一無視。

// 你寫了
history.pushState({}, "這是新標(biāo)題", "/new-page");

// 瀏覽器:嗯,知道了,但我不改title
// 所以還得手動(dòng)改
document.title = "這是新標(biāo)題";

Vue Router的title管理是通過路由配置的meta或者afterEach鉤子來做的,不依賴這個(gè)參數(shù)。

第三個(gè)參數(shù):url

新的網(wǎng)址,這個(gè)必須跟當(dāng)前頁面在同一個(gè)域??梢允窍鄬β窂?,也可以是絕對路徑,但域名、協(xié)議、端口必須一致。

// 這些都可以
history.pushState({}, "", "/new-page");           // 相對根路徑
history.pushState({}, "", "new-page");            // 相對當(dāng)前路徑
history.pushState({}, "", "/user/123/edit");     // 帶參數(shù)
history.pushState({}, "", "?tab=2");              // 只改query
history.pushState({}, "", "#section3");           // 只改hash

// 這些不行,直接報(bào)錯(cuò)
history.pushState({}, "", "https://other.com/page");  // 不同域名
history.pushState({}, "", "http://other.com/page");        // 協(xié)議相對URL也不行
history.pushState({}, "", "http://當(dāng)前域名/page");     // 協(xié)議不同(http vs https)

pushState和replaceState到底有啥區(qū)別

很多人到這還是一臉懵,這倆到底啥區(qū)別?我打個(gè)比方你就懂了。

歷史記錄的行為差異

pushState就像是你逛淘寶,每點(diǎn)一個(gè)商品頁面,瀏覽器歷史記錄就多一條,你點(diǎn)后退能一層層往回退。replaceState就像是你在同一個(gè)商品頁面切換不同規(guī)格(比如紅色變藍(lán)色),歷史記錄不會(huì)增加,點(diǎn)后退直接跳到上一個(gè)完全不同的頁面(比如從商品頁跳回搜索頁)。

畫個(gè)圖更清楚:

初始狀態(tài):頁面A(當(dāng)前)
         ↓
pushState到頁面B后:頁面A → 頁面B(當(dāng)前)
                   ↑
                   后退能回到A

初始狀態(tài):頁面A(當(dāng)前)
         ↓
replaceState到頁面B后:頁面B(當(dāng)前)【頁面A被替換了】
                      ↑
                      后退直接跳到A之前的歷史記錄(比如頁面0)

實(shí)際代碼對比

// 場景:用戶從商品列表點(diǎn)進(jìn)詳情頁,應(yīng)該能后退回列表
// 用pushState
this.$router.push('/product/123');
// 或者原生
history.pushState({}, "", "/product/123");

// 歷史記錄:列表頁 → 詳情頁(當(dāng)前)
// 用戶點(diǎn)后退:回到列表頁 ?

// 場景:用戶在詳情頁切換SKU(規(guī)格),不應(yīng)該增加歷史記錄
// 用replaceState
this.$router.replace('/product/456');
// 或者原生
history.replaceState({}, "", "/product/456");

// 歷史記錄:列表頁 → 詳情頁-新SKU(當(dāng)前,替換了原來的詳情頁)
// 用戶點(diǎn)后退:直接回到列表頁,跳過SKU切換的過程 ?

實(shí)際開發(fā)中的經(jīng)典場景

場景一:登錄后的重定向

// 登錄成功后,不想讓用戶后退回到登錄頁
this.$router.replace('/dashboard');
// 而不是
this.$router.push('/dashboard');

// 這樣用戶點(diǎn)后退,直接跳到登錄前的頁面(比如首頁),而不是登錄頁
// 體驗(yàn)好很多,不然用戶后退看到登錄頁會(huì)懵逼:我不是剛登過嗎?

場景二:表單提交后的跳轉(zhuǎn)

// 表單提交成功,跳轉(zhuǎn)到結(jié)果頁
submitForm() {
  api.submit(this.formData).then(res => {
    // 用replace,避免用戶后退回到表單頁又提交一次
    this.$router.replace(`/order/success?orderId=${res.id}`);
  });
}

場景三:帶臨時(shí)參數(shù)的頁面

// 比如支付頁面,帶個(gè)臨時(shí)token,不想留在歷史記錄里
this.$router.replace({
  path: '/payment',
  query: { token: 'temp_token_123' }  // 這個(gè)token用完即焚
});

// 支付完成后
this.$router.replace('/payment/success');  // 又把token清掉了

// 整個(gè)過程歷史記錄很干凈

實(shí)際項(xiàng)目里咋用才不翻車

來點(diǎn)干貨,說說實(shí)際開發(fā)中咋用。我分幾個(gè)常見場景給你們掰扯掰扯。

場景一:表單防重復(fù)提交

這是最經(jīng)典的replace使用場景。用戶填了半天表單,提交成功后你給他跳到成功頁。這時(shí)候必須用replace,不然用戶點(diǎn)后退,回到表單頁,看著滿屏的數(shù)據(jù),手一抖又點(diǎn)了一次提交,后端就收到重復(fù)數(shù)據(jù)了。

// 表單組件
export default {
  data() {
    return {
      form: {
        name: '',
        email: '',
        content: ''
      },
      submitting: false
    }
  },
  
  methods: {
    async handleSubmit() {
      if (this.submitting) return;  // 防連點(diǎn)
      
      this.submitting = true;
      
      try {
        const res = await this.$http.post('/api/feedback', this.form);
        
        // 關(guān)鍵!用replace跳轉(zhuǎn),不留歷史記錄
        this.$router.replace({
          name: 'FeedbackSuccess',
          params: { id: res.data.id }
        });
        
      } catch (error) {
        this.$message.error('提交失?。? + error.message);
      } finally {
        this.submitting = false;
      }
    }
  }
}

// 成功頁組件
export default {
  beforeRouteEnter(to, from, next) {
    // 甚至可以加個(gè)守衛(wèi),確保只能從表單頁進(jìn)來
    if (from.name !== 'FeedbackForm') {
      next({ name: 'FeedbackForm' });  // 直接進(jìn)成功頁?打回表單頁
    } else {
      next();
    }
  }
}

場景二:帶狀態(tài)保持的頁面切換

有時(shí)候你想讓用戶后退時(shí)恢復(fù)之前的狀態(tài),比如滾動(dòng)位置、篩選條件等。這時(shí)候可以配合state參數(shù):

// 列表頁組件
export default {
  data() {
    return {
      list: [],
      filters: {
        category: 'all',
        sort: 'newest'
      },
      scrollTop: 0
    }
  },
  
  methods: {
    handleFilterChange(newFilters) {
      this.filters = newFilters;
      this.fetchData();
      
      // 把篩選條件塞進(jìn)URL,但用replace不增加歷史記錄
      // 用戶刷新頁面時(shí)篩選條件還在,但后退不會(huì)回到上一次的篩選
      this.$router.replace({
        query: { ...this.filters }
      });
    },
    
    goToDetail(item) {
      // 去詳情頁之前,保存當(dāng)前狀態(tài)
      const state = {
        filters: this.filters,
        scrollPosition: document.documentElement.scrollTop,
        timestamp: Date.now()
      };
      
      // 這里用pushState,但要手動(dòng)調(diào)用,因?yàn)閂ue Router的push不支持自定義state
      // 這是個(gè)騷操作,慎用!
      history.pushState(state, "", `/detail/${item.id}`);
      
      // 然后告訴Vue Router去這個(gè)路由,但不觸發(fā)它的pushState
      this.$router.push(`/detail/${item.id}`).catch(() => {});
    }
  },
  
  // 從詳情頁后退回來時(shí)恢復(fù)狀態(tài)
  activated() {  // 用了keep-alive的話
    const state = history.state;
    if (state && state.filters) {
      this.filters = state.filters;
      this.$nextTick(() => {
        window.scrollTo(0, state.scrollPosition || 0);
      });
    }
  }
}

場景三:權(quán)限攔截后的處理

做后臺(tái)管理系統(tǒng)時(shí),經(jīng)常要判斷權(quán)限。沒權(quán)限的時(shí)候,用replace跳回登錄頁或者403頁,別用push,不然用戶點(diǎn)后退又在權(quán)限判斷里死循環(huán)了。

// router.js 里的全局守衛(wèi)
router.beforeEach((to, from, next) => {
  const token = localStorage.getItem('token');
  const userRole = store.state.user.role;
  
  // 需要登錄但沒token
  if (to.matched.some(record => record.meta.requiresAuth) && !token) {
    // 用replace,不留登錄頁的歷史記錄
    next({ 
      name: 'Login', 
      replace: true,
      query: { redirect: to.fullPath }  // 記住想去的頁面,登錄后跳轉(zhuǎn)
    });
    return;
  }
  
  // 需要特定角色
  if (to.meta.requiredRole && to.meta.requiredRole !== userRole) {
    // 沒權(quán)限,replace到403頁
    next({ 
      name: 'Forbidden', 
      replace: true 
    });
    return;
  }
  
  next();
});

場景四:URL參數(shù)清理

有時(shí)候頁面有一些臨時(shí)的query參數(shù),比如從其他網(wǎng)站帶過來的utm_source跟蹤參數(shù),或者一次性的通知標(biāo)記。這些參數(shù)用完就該清理掉,讓URL干凈點(diǎn)。

// App.vue 或者某個(gè)布局組件的created里
created() {
  const query = { ...this.$route.query };
  let hasChange = false;
  
  // 清理一次性參數(shù)
  if (query.notificationRead) {
    delete query.notificationRead;
    // 標(biāo)記通知已讀的邏輯...
    hasChange = true;
  }
  
  // 清理空值參數(shù)
  Object.keys(query).forEach(key => {
    if (query[key] === '' || query[key] === null || query[key] === undefined) {
      delete query[key];
      hasChange = true;
    }
  });
  
  if (hasChange) {
    // 清理完用replace更新URL,不增加歷史記錄
    this.$router.replace({ query });
  }
}

場景五:服務(wù)器配置(這個(gè)坑太深了)

history模式最大的坑就是刷新404。因?yàn)榍岸寺酚墒翘摂M的,服務(wù)器上并沒有對應(yīng)的物理文件。

Nginx配置:

server {
    listen 80;
    server_name myapp.com;
    root /var/www/myapp/dist;  # 打包后的dist目錄
    
    location / {
        # 關(guān)鍵配置!所有路由都指向index.html
        try_files $uri $uri/ /index.html;
    }
    
    # API代理
    location /api {
        proxy_pass http://backend_server;
    }
}

Apache配置:

<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /index.html [L]
</IfModule>

Node.js/Express:

const express = require('express');
const path = require('path');
const app = express();

// 靜態(tài)資源
app.use(express.static(path.join(__dirname, 'dist')));

// 所有路由返回index.html,讓Vue Router處理
app.get('*', (req, res) => {
  res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});

app.listen(3000);

開發(fā)環(huán)境配置(webpack-dev-server):

// vue.config.js
module.exports = {
  devServer: {
    historyApiFallback: true,  // 開發(fā)時(shí)自動(dòng)處理
    // 或者更精細(xì)的配置
    historyApiFallback: {
      rewrites: [
        { from: /^\/api/, to: '/api' },  // API請求不轉(zhuǎn)發(fā)
        { from: /./, to: '/index.html' }  // 其他都轉(zhuǎn)發(fā)
      ]
    }
  }
}

踩坑實(shí)錄和排查思路

坑來了啊,兄弟們坐穩(wěn)。這些都是我血與淚的教訓(xùn)。

坑一:直接調(diào)用原生API,Vue不同步

這個(gè)前面說過,但值得再強(qiáng)調(diào)。你直接調(diào)用history.pushState,Vue Router根本不知道,然后各種詭異行為就出現(xiàn)了。

癥狀:

  • URL變了,頁面沒切換
  • 組件沒重新渲染
  • 路由守衛(wèi)沒觸發(fā)
  • 瀏覽器后退時(shí),Vue Router狀態(tài)混亂

排查:

// 在main.js或者某個(gè)全局文件里,加個(gè)監(jiān)聽看看是不是有人瞎搞
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;

history.pushState = function(...args) {
  console.warn('有人直接調(diào)用了pushState!', new Error().stack);
  return originalPushState.apply(this, args);
};

history.replaceState = function(...args) {
  console.warn('有人直接調(diào)用了replaceState!', new Error().stack);
  return originalReplaceState.apply(this, args);
};

解決方案: 全局搜索history.pushStatehistory.replaceState,全部換成this.$router.pushthis.$router.replace

坑二:刷新頁面404

這是history模式的老大難問題。開發(fā)時(shí)好好的,一部署到生產(chǎn)環(huán)境,刷新就404。

癥狀:

  • 首頁能打開
  • 點(diǎn)擊鏈接正常跳轉(zhuǎn)
  • 直接訪問/user/profile或者刷新這個(gè)頁面,404

排查步驟:

  1. 先看Network面板,404的請求是HTML還是其他資源
  2. 看服務(wù)器日志,確認(rèn)請求到了哪里
  3. 檢查服務(wù)器配置有沒有try_files或者等效配置

臨時(shí)解決方案(應(yīng)急用):

如果暫時(shí)改不了服務(wù)器配置,可以改成hash模式:

const router = new VueRouter({
  mode: 'hash',  // 改成hash模式,URL會(huì)帶#,但不會(huì)404
  routes
});

但這不是長久之計(jì),history模式的URL更美觀,SEO也更好(配合SSR)。

坑三:popstate事件監(jiān)聽不到

注意啊,pushState和replaceState本身不會(huì)觸發(fā)popstate事件!只有用戶點(diǎn)瀏覽器前進(jìn)后退按鈕、或者調(diào)用history.back()/history.forward()/history.go()時(shí)才會(huì)觸發(fā)。

// 錯(cuò)誤的期待
window.addEventListener('popstate', (e) => {
  console.log('popstate觸發(fā)!', e.state);
});

history.pushState({page: 1}, "", "/page1");
// 你以為會(huì)打印日志?并不會(huì)!

// 正確的理解
history.pushState({page: 1}, "", "/page1");  // 不觸發(fā)popstate
history.back();  // 這才觸發(fā)popstate!

實(shí)際應(yīng)用:表單離開提示

// 在表單組件里
export default {
  data() {
    return {
      formDirty: false,  // 表單是否被修改過
      confirmed: false   // 用戶是否確認(rèn)離開
    }
  },
  
  mounted() {
    // 監(jiān)聽瀏覽器后退/前進(jìn)
    window.addEventListener('popstate', this.handlePopState);
    
    // 監(jiān)聽頁面關(guān)閉/刷新
    window.addEventListener('beforeunload', this.handleBeforeUnload);
  },
  
  beforeDestroy() {
    window.removeEventListener('popstate', this.handlePopState);
    window.removeEventListener('beforeunload', this.handleBeforeUnload);
  },
  
  methods: {
    handlePopState(e) {
      // 注意:這時(shí)候路由已經(jīng)變了,但Vue可能還沒反應(yīng)過來
      if (this.formDirty && !this.confirmed) {
        // 阻止默認(rèn)行為是不可能的,popstate沒法阻止
        // 只能把用戶推回去,或者給個(gè)提示
        
        // 推回去(體驗(yàn)不太好,但有效)
        history.forward();
        
        // 或者顯示個(gè)對話框
        this.showConfirmDialog().then(confirmed => {
          if (confirmed) {
            this.confirmed = true;
            history.back();  // 再次后退
          }
        });
      }
    },
    
    handleBeforeUnload(e) {
      if (this.formDirty) {
        e.preventDefault();
        e.returnValue = '';  // Chrome需要這個(gè)
      }
    },
    
    // 更好的做法:用Vue Router的導(dǎo)航守衛(wèi)
    beforeRouteLeave(to, from, next) {
      if (this.formDirty && !this.confirmed) {
        this.$confirm('有未保存的更改,確定離開嗎?', '提示', {
          confirmButtonText: '確定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          next();
        }).catch(() => {
          next(false);  // 取消導(dǎo)航
        });
      } else {
        next();
      }
    }
  }
}

坑四:跨域URL報(bào)錯(cuò)

這個(gè)前面說過,但 worth repeating。這倆API有個(gè)鐵律:只能修改同源URL。

癥狀:

  • 代碼報(bào)錯(cuò):SecurityError: The operation is insecure.
  • 或者靜默失敗(某些瀏覽器)

常見觸發(fā)場景:

// 場景1:協(xié)議不同
// 當(dāng)前是 https://example.com
history.pushState({}, "", "http://example.com/page");  // http vs https,不行

// 場景2:端口不同
// 當(dāng)前是 http://localhost:8080
history.pushState({}, "", "http://localhost:3000/page");  // 8080 vs 3000,不行

// 場景3:子域名不同(某些瀏覽器嚴(yán)格模式)
// 當(dāng)前是 https://www.example.com
history.pushState({}, "", "https://api.example.com/page");  // 可能不行

解決方案: 跨域跳轉(zhuǎn)只能用window.location.href或者window.open,別指望history API。

坑五:state對象存太大

雖然能存數(shù)據(jù),但別啥都往里塞。有大小限制的,不同瀏覽器不一樣,一般幾MB到幾十MB。

癥狀:

  • 某些瀏覽器報(bào)錯(cuò):QuotaExceededError
  • 頁面卡頓(序列化大對象)
  • 后退時(shí)state丟失

錯(cuò)誤示范:

// 別這么干!
history.pushState({
  hugeData: Array(1000000).fill('x'),  // 100萬個(gè)字符,瘋了吧
  imageBase64: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...',  // 大圖轉(zhuǎn)base64
  fullPageHTML: document.documentElement.outerHTML  // 整個(gè)頁面HTML,瘋了吧
}, "", "/page");

正確做法:

// 只存關(guān)鍵標(biāo)識
history.pushState({
  pageId: 'product-123',
  scrollPosition: window.scrollY,
  timestamp: Date.now()
}, "", "/product/123");

// 大數(shù)據(jù)存 IndexedDB 或者 sessionStorage
sessionStorage.setItem('product-123-data', JSON.stringify(hugeData));

坑六:移動(dòng)端兼容性問題

iOS Safari和Android的各種瀏覽器,對history API的實(shí)現(xiàn)有些細(xì)微差別。

已知問題:

  1. iOS Safari的title問題:雖然第二個(gè)參數(shù)沒用,但不傳或者傳空字符串,有時(shí)候會(huì)有奇怪的行為。建議傳個(gè)空字符串或者當(dāng)前title。
  2. 微信內(nèi)置瀏覽器:微信的X5內(nèi)核有時(shí)候會(huì)有延遲,pushState后立即獲取location可能還是舊的。
  3. 快速點(diǎn)擊后退:某些低端Android機(jī),快速點(diǎn)擊后退按鈕,popstate事件可能丟失。

防御性編程:

// 封裝一個(gè)可靠的push方法
function safePushState(data, title, url) {
  try {
    // iOS Safari兼容性處理
    const safeTitle = title || document.title || '';
    history.pushState(data, safeTitle, url);
    
    // 微信瀏覽器延遲處理
    if (/MicroMessenger/i.test(navigator.userAgent)) {
      return new Promise(resolve => setTimeout(resolve, 50));
    }
  } catch (e) {
    console.error('pushState失敗:', e);
    // 降級處理
    window.location.href = url;
  }
}

幾個(gè)讓代碼更騷的操作技巧

技巧這東西,知道的人覺得簡單,不知道的人能卡半天。

技巧一:配合beforeEach做動(dòng)態(tài)權(quán)限

URL變了但沒權(quán)限?直接replace回登錄頁,別讓用戶看到一閃而過的無權(quán)限頁面。

// router.js
router.beforeEach(async (to, from, next) => {
  // 顯示loading
  store.commit('SHOW_LOADING');
  
  // 獲取用戶權(quán)限(可能從接口拿,可能從store拿)
  const userPerms = await store.dispatch('getUserPermissions');
  
  if (to.meta.permission && !userPerms.includes(to.meta.permission)) {
    // 沒權(quán)限,直接replace,不留下當(dāng)前路由的歷史記錄
    next({ 
      name: 'Login', 
      replace: true,
      query: { 
        redirect: to.fullPath,
        reason: 'no-permission'  // 可以加個(gè)標(biāo)記,登錄頁顯示特殊提示
      }
    });
    return;
  }
  
  // 有權(quán)限,正常走
  next();
});

router.afterEach(() => {
  // 隱藏loading
  store.commit('HIDE_LOADING');
});

技巧二:用state傳遞敏感數(shù)據(jù)

有些數(shù)據(jù)不想顯示在URL里,但又需要在路由間傳遞。比如支付時(shí)的臨時(shí)token,或者一些隱私信息。

// 支付頁面
methods: {
  initPayment() {
    // 獲取支付token(這個(gè)token很敏感,不想放URL)
    api.getPaymentToken().then(token => {
      // 用replaceState把token存進(jìn)history state
      // 注意:這里要結(jié)合Vue Router的replace使用
      const currentState = history.state || {};
      history.replaceState(
        { ...currentState, paymentToken: token },
        '',
        this.$route.fullPath
      );
      
      // 然后繼續(xù)支付流程
      this.startPayment(token);
    });
  }
},

// 支付結(jié)果頁(同一路由下,或者后退回來)
mounted() {
  // 從state里取token
  const state = history.state || {};
  if (state.paymentToken) {
    this.verifyPayment(state.paymentToken);
  } else {
    // token沒了?可能是用戶刷新了,去查接口或者報(bào)錯(cuò)
    this.handleMissingToken();
  }
}

安全提示: state里的數(shù)據(jù)雖然不在URL里,但還是存在客戶端,別存密碼之類的超級敏感信息。

技巧三:監(jiān)聽popstate做自定義邏輯

有時(shí)候你想在瀏覽器后退時(shí)做點(diǎn)特殊處理,比如恢復(fù)頁面狀態(tài)、或者阻止某些操作。

// 在一個(gè)復(fù)雜的表單頁面
export default {
  data() {
    return {
      step: 1,
      maxStepReached: 1,
      formData: {}
    }
  },
  
  created() {
    // 初始化時(shí)根據(jù)當(dāng)前step設(shè)置state
    this.syncHistoryState();
  },
  
  methods: {
    goToStep(step) {
      this.step = step;
      this.maxStepReached = Math.max(this.maxStepReached, step);
      this.syncHistoryState();
    },
    
    syncHistoryState() {
      // 每步都push一個(gè)新歷史記錄
      const state = { step: this.step, t: Date.now() };
      const url = `${this.$route.path}?step=${this.step}`;
      
      // 只有步驟前進(jìn)時(shí)才push,后退時(shí)不push(避免死循環(huán))
      const currentState = history.state || {};
      if (currentState.step < this.step) {
        history.pushState(state, '', url);
      } else {
        history.replaceState(state, '', url);
      }
    },
    
    handlePopState(e) {
      const state = e.state || {};
      if (state.step) {
        // 用戶點(diǎn)了后退/前進(jìn),同步步驟
        this.step = state.step;
        // 可以在這里做步驟切換的動(dòng)畫
        this.animateStepChange();
      }
    }
  },
  
  mounted() {
    window.addEventListener('popstate', this.handlePopState);
  },
  
  beforeDestroy() {
    window.removeEventListener('popstate', this.handlePopState);
  }
}

這樣用戶就能用瀏覽器后退按鈕在表單的各個(gè)步驟間切換,體驗(yàn)很原生。

技巧四:hash模式和history模式共存

老項(xiàng)目遷移的時(shí)候特別有用,部分路由走h(yuǎn)ash,部分走h(yuǎn)istory。雖然有點(diǎn)hack,但確實(shí)能解決問題。

// router.js
const router = new VueRouter({
  mode: 'history',  // 默認(rèn)history
  routes: [
    // 新頁面,走h(yuǎn)istory
    { path: '/new-feature', component: NewFeature },
    
    // 老頁面,重定向到hash模式
    { 
      path: '/legacy-page', 
      beforeEnter(to, from, next) {
        // 強(qiáng)制跳轉(zhuǎn)到hash模式
        window.location.href = '/#/legacy-page';
      }
    }
  ]
});

// 或者反過來,默認(rèn)hash,特殊路由用history
// 這個(gè)更復(fù)雜,需要手動(dòng)管理

技巧五:URL參數(shù)清洗和美化

讓URL更干凈,去掉那些沒用的默認(rèn)參數(shù)。

// 一個(gè)帶很多篩選條件的列表頁
methods: {
  updateFilters(newFilters) {
    // 清理默認(rèn)值
    const cleanFilters = {};
    Object.keys(newFilters).forEach(key => {
      const value = newFilters[key];
      // 去掉空值、undefined、和默認(rèn)值一樣的值
      if (value !== '' && value !== undefined && value !== null && value !== this.defaultFilters[key]) {
        cleanFilters[key] = value;
      }
    });
    
    // 用replace更新URL,不增加歷史記錄
    // 但用query記錄,這樣刷新頁面篩選條件還在
    this.$router.replace({
      query: Object.keys(cleanFilters).length > 0 ? cleanFilters : undefined
    }).catch(() => {});
    
    // 實(shí)際發(fā)請求
    this.fetchList(cleanFilters);
  }
}

這樣URL就不會(huì)出現(xiàn)?category=all&sort=default&page=1這種全是默認(rèn)值的丑陋情況了。

最后嘮點(diǎn)實(shí)在的

說到這,估計(jì)有人要問了:都2026年了還學(xué)Vue2干啥?問得好!但現(xiàn)實(shí)就是很多老項(xiàng)目還在跑,你總得會(huì)維護(hù)吧?再說了,這倆API的原理搞懂了,Vue3、React Router、甚至自己寫路由框架都不在話下。

記住啊,能用Vue Router封裝好的方法就別自己調(diào)用原生API,除非你有特殊需求。封裝好的方法幫你把該干的活兒都干了,省心省力。原生API留著理解原理和解決特殊場景就行。

還有幾個(gè)忠告:

性能方面: 別在循環(huán)里調(diào)用pushState,那玩意兒有開銷的。也別存太多數(shù)據(jù)在state里,序列化反序列化都要時(shí)間。

用戶體驗(yàn): replace用多了,用戶的歷史記錄就斷了,點(diǎn)后退直接跳出你的應(yīng)用,這體驗(yàn)好不好得看場景。登錄后replace掉登錄頁是好的,但正常瀏覽流程都用replace就過分了。

調(diào)試技巧: 在控制臺(tái)輸入history能看到當(dāng)前的歷史記錄棧,雖然看不到具體內(nèi)容(隱私原因),但能看到長度。配合console.log(history.state)能看到當(dāng)前state。

未來趨勢: Vue3的Router其實(shí)原理差不多,只是Composition API寫法不同。React Router v6也類似。甚至瀏覽器新出的Navigation API(還在實(shí)驗(yàn)階段)可能會(huì)取代history API,但那是后話了。

行了,今天就嘮到這。這些代碼片段你拿去直接用或者改改都行,有問題自己多console.log,別光看不練。要是這文章幫到你了,下次碰到前端面試題問路由原理,你能多吹十分鐘,這就值了。

以上就是Vue2路由地址欄變化API(pushState和replaceState)的避坑指南的詳細(xì)內(nèi)容,更多關(guān)于Vue2路由地址欄變化API的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • webpack中的optimization配置示例詳解

    webpack中的optimization配置示例詳解

    這篇文章主要介紹了webpack中的optimization配置詳解,主要就是根據(jù)不同的策略來分割打包出來的bundle,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2023-02-02
  • vue+element實(shí)現(xiàn)動(dòng)態(tài)加載表單

    vue+element實(shí)現(xiàn)動(dòng)態(tài)加載表單

    這篇文章主要為大家詳細(xì)介紹了vue+element實(shí)現(xiàn)動(dòng)態(tài)加載表單,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2020-12-12
  • Vue項(xiàng)目獲取url中的參數(shù)(親測可用)

    Vue項(xiàng)目獲取url中的參數(shù)(親測可用)

    這篇文章主要介紹了Vue項(xiàng)目獲取url中的參數(shù),本文通過兩種情況分析給大家詳細(xì)介紹,感興趣的朋友一起看看吧
    2022-08-08
  • Vue 3開發(fā)中VueUse強(qiáng)大Hooks庫

    Vue 3開發(fā)中VueUse強(qiáng)大Hooks庫

    VueUse提供了一個(gè)豐富且強(qiáng)大的Hooks庫,可以幫助開發(fā)者快速實(shí)現(xiàn)各種功能,提高開發(fā)效率,本文來詳細(xì)的介紹一下,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2024-08-08
  • Vue filter 過濾當(dāng)前時(shí)間 實(shí)現(xiàn)實(shí)時(shí)更新效果

    Vue filter 過濾當(dāng)前時(shí)間 實(shí)現(xiàn)實(shí)時(shí)更新效果

    這篇文章主要介紹了Vue filter 過濾當(dāng)前時(shí)間 實(shí)現(xiàn)實(shí)時(shí)更新效果,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2019-12-12
  • vue時(shí)間格式總結(jié)以及轉(zhuǎn)換方法詳解

    vue時(shí)間格式總結(jié)以及轉(zhuǎn)換方法詳解

    項(xiàng)目中后臺(tái)返回的時(shí)間有多種形式,時(shí)間戳、ISO標(biāo)準(zhǔn)時(shí)間格式等,我們需要轉(zhuǎn)化展示成能看的懂得時(shí)間格式,下面這篇文章主要給大家介紹了關(guān)于vue時(shí)間格式總結(jié)以及轉(zhuǎn)換方法的相關(guān)資料,需要的朋友可以參考下
    2022-12-12
  • Vue父組件如何獲取子組件中的變量

    Vue父組件如何獲取子組件中的變量

    這篇文章主要為大家詳細(xì)介紹了Vue父組件如何獲取子組件中的變量,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2019-07-07
  • VUE利用vuex模擬實(shí)現(xiàn)新聞點(diǎn)贊功能實(shí)例

    VUE利用vuex模擬實(shí)現(xiàn)新聞點(diǎn)贊功能實(shí)例

    本篇文章主要介紹了VUE利用vuex模擬實(shí)現(xiàn)新聞點(diǎn)贊功能實(shí)例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2017-06-06
  • vue父組件值變化但子組件不刷新的三種解決方案

    vue父組件值變化但子組件不刷新的三種解決方案

    父組件傳遞給子組件的數(shù)據(jù),如果是一個(gè)復(fù)雜對象(例如一個(gè)數(shù)組或?qū)ο螅?那么子組件只會(huì)監(jiān)聽對象的引用而不是對象的內(nèi)容,這意味著當(dāng)對象的內(nèi)容發(fā)生變化時(shí),子組件不會(huì)更新,本文給大家介紹了vue子組件不刷新的三種解決方案,需要的朋友可以參考下
    2024-03-03
  • vue+freemarker中遇到的坑及解決

    vue+freemarker中遇到的坑及解決

    這篇文章主要介紹了vue+freemarker中遇到的坑及解決,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-09-09

最新評論

昆明市| 慈溪市| 来宾市| 新泰市| 锡林浩特市| 凭祥市| 称多县| 张家港市| 明星| 宁国市| 平塘县| 昭苏县| 桃园县| 五河县| 庆元县| 苏尼特右旗| 海安县| 庆安县| 石台县| 化德县| 卓尼县| 台东县| 三亚市| 红安县| 调兵山市| 鄂州市| 宁德市| 扎兰屯市| 托克逊县| 麟游县| 津南区| 呈贡县| 济阳县| 章丘市| 横山县| 西昌市| 松江区| 当雄县| 闽侯县| 丽江市| 登封市|