引言

前進重新整理,後退不重新整理,是一個類似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配置檔案中:

  1. 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;

主頁:

  1. <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>

子頁面:

  1. <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;
      left: 0;
      top: 0;
      width: 100%;
      height: 100%;
      background-color: #fff;
    }
    </style>

react中的實現

在routes中:

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

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

    export default Routes;

主頁:

  1. 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;

子頁面:

  1. 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有一定要求。