單頁應用後退不刷新方案(vue & react)

引言

前進刷新,後退不刷新,是一個類似app頁面的特點,要在單頁web應用中做後退不刷新,卻並非一件易事。

為什麼麻煩

spa的渲染原理(以vue為例):url的更改觸發onHashChange/pushState/popState/replaceState,通過url中的pathName去匹配路由中定義的組件,載入進來並實例化渲染在項目的出口router-view中。

換言之,一個實例的解析渲染意味著另外一個實例的銷毀,因為渲染出口只有一個。

keep-alive為什麼不行?因為keep-alive的原理是將實例化後的組件存儲起來,當下次url匹配到了改組件時,優先從存儲裡面取。

但是vue只提供了入存儲的方式,沒提供刪存儲的方式,所以沒法實現「前進刷新」。

有一種方案是手動根據to和from去做前進後退判斷,這種判斷不能應對複雜的跳轉邏輯可維護性也很差

有坑的社區方案(以vue為例)

vue-page-stackvue-navigation

這兩個方案都有明顯缺點:前者不支援嵌套路由,在一些場景下會出現url變化,頁面完全無反應的情況,後者存在類似的bug。並且這兩種方案侵入性都很強,因為他們都是基於vue-router的魔改。並且會在url中增加無意義的多餘欄位(stackID)

目前不錯的方案

現在有一個可行且簡單的方案:嵌套子路由 + 疊頁面

疊頁面的靈感:原生應用中的webview in webview,多頁應用中的window in window

要在spa中實現後退不刷新,本質是要實現多實例共存

這個方案的核心在於:通過嵌套子路由實現多實例共存,通過css的absolute實現視覺上的頁面堆疊

vue中的實現

在routes配置文件中:

import Home from "../views/Home.vue";

const routes = [
  {
    path"/home",
    name"Home",
    component: Home,
    children: [
      {
        path"sub",
        component() =>
          import(/* webpackChunkName: "sub" */ "../views/Sub.vue"),
      },
    ],
  },
];

export default routes;

主頁:

<template>
  <div class="home">
    <input v-model="inputValue" />
    <h3>{{ inputValue }}</h3>
    <button @click="handleToSub">to sub</button>
    <router-view @reload="handleReload" />
  </div>
</template>

<script>
export default {
  name"Home",
  data() {
    return {
      inputValue"",
    };
  },
  methods: {
    handleToSub() {
      // 注意路由格式,是基於上一個路由/home下面的sub,不是獨立的/sub
      this.$router.push("/home/sub");
    },

    handleReload(val) {
      // 這裡可以做一些重新獲取數據的操作,比如在詳情頁修改數據,返回後重新拉取列表
      console.log("reload", val);
    },
  },
  mounted() {
    // 子頁面返回,不會重新跑生命周期
    console.log("mounted");
  },
};
</script>

<style scoped>
.home {
  position: relative;
}
</style>

子頁面:

<template>
  <div class="sub">
    <h1>This is Sub page</h1>
  </div>

</template>

<script>
export default {
  beforeDestroy() {
    /
/ 可以傳自定義參數,如果沒需要,也可以不做
    this.$emit("reload", 123);
  },
};
</
script>

<style scoped>
.sub {
  position: absolute;
  left0;
  top0;
  width100%;
  height100%;
  background-color#fff;
}
</style>

react中的實現

在routes中:

import { Route } from "react-router-dom";

const Routes = () => {
  return (
    <>
      {/* 這裡不能加exact,因為要先匹配父頁面再匹配子頁面 */}
      <Route path="/home" component={lazy(() => import("../views/Home"))} />
    </>
  );
};

export default Routes;

主頁:

import React, { useEffect, useState } from "react";
import { Route, useHistory } from "react-router-dom";
import styled from "styled-components";
import Sub from "./Sub";

const HomeContainer = styled.div`
  position: relative;
`
;

const Home: React.FC = () => {
  const [inputValue, setInputValue] = useState("");
  const history = useHistory();

  const handleToSub = () => {
    history.push("/home/sub");
  };

  const handleReload = (val: number) => {
    console.log("reload", val);
  };

  useEffect(() => {
    console.log("mounted");
  }, []);

  return (
    <HomeContainer>
      <input
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
      <h3>{inputValue}</h3>
      <button onClick={handleToSub}>to sub</button>
      <Route
        path="/home/sub"
        component={() => <Sub handleReload={handleReload} />}
      />
    </HomeContainer>
  );
};

export default Home;

子頁面:

import React from "react";
import styled from "styled-components";

const SubContainer = styled.div`
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  background-color: #fff;
`
;

type SubProps = {
  handleReload(val: number) => void;
};

const Sub: React.FC<SubProps> = ({ handleReload }) => {
  useEffect(() => {
      return () => handleReload(123);
  }, []);

  return (
    <SubContainer>
      <h1>This is Sub page</h1>
    </SubContainer>

  );
};

export default Sub;

該方案的優點

  • 實現簡單,無侵入式修改,幾乎0邏輯;
  • 子頁面可以單獨提供出去,供三方接入;
  • 完全的多實例共存,後退不刷新;
  • 可以像父子組件一樣通訊,監聽子頁面離開;

缺點

路由格式需要做改造,必須做成嵌套關係,對url有一定要求。

Tags: