引言
前進重繪,后退不重繪,是一個類似app頁面的特點,要在單頁web應用中做后退不重繪,卻并非一件易事,
為什么麻煩
spa的渲染原理(以vue為例):url的更改觸發onHashChange/pushState/popState/replaceState,通過url中的pathName去匹配路由中定義的組件,加載進來并實體化渲染在專案的出口router-view中,
換言之,一個實體的決議渲染意味著另外一個實體的銷毀,因為渲染出口只有一個,
keep-alive為什么不行?因為keep-alive的原理是將實體化后的組件存盤起來,當下次url匹配到了改組件時,優先從存盤里面取,
但是vue只提供了入存盤的方式,沒提供刪存盤的方式,所以沒法實作“前進重繪”,
有一種方案是手動根據to和from去做前進后退判斷,這種判斷不能應對復雜的跳轉邏輯,可維護性也很差,
有坑的社區方案(以vue為例)
vue-page-stack,vue-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;
left: 0;
top: 0;
width: 100%;
height: 100%;
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=https://www.cnblogs.com/zhangnan35/archive/2021/09/27/{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;
題外
在前司的核心專案“平安好車主”中,我就在部分h5新專案用了該方案,在線上經受住了170w+訪問量的考驗,目前在Shopee也在推行這種h5方案,由于邏輯簡單,得到了不少同事的認可和使用,比如常見的:串列頁存在搜索條件,進入詳情頁再回傳, 大家可以試用一下,會有驚喜的,
該方案的優點
- 實作簡單,無侵入式修改,幾乎0邏輯;
- 子頁面可以單獨提供出去,供三方接入;
- 完全的多實體共存,后退不重繪;
- 可以像父子組件一樣通信,監聽子頁面離開;
缺點
路由格式需要做改造,必須做成嵌套關系,對url有一定要求,
github地址
https://github.com/zhangnan24/no-refresh-back-vue
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/303495.html
標籤:其他
