后端说,单页面SPA和前端路由是怎么回事

没有请求的路由

在传统开发中,浏览器点击一个超链接,就会像后端web服务器发送一个html文档请求,然后页面刷新。但开始单页面开发后,就完全不同了。

单页面?这个概念难以理解。我用一个js作为整个web应用,然后再这个js中操作dom变化,以此来实现页面变化。这不叫单页面吗?这叫!但不完善,因为这种方法破坏了浏览器自带的导航功能。比如前进,后退。所以单页面前端应用要解决两件事内容变化导航变化。这是现代前端成立的基础。

想必初次接触vue-routernuxt的人很多对前端路由困惑。明明浏览器地址栏的链接变了,为什么浏览器却没有发送请求出去?至少我是很疑惑的。

这要归功于一个浏览器API,History API。学过wpf的人可能对这玩意不陌生,因为wpf也可以借助导航开发浏览器式应用程序。

导航

导航这种理念包括如下几种功能

  • history.back()

    后退到上一个页面

  • history.forward()

    前进到下一个页面

  • history.go(-2)

    跳转到前两个页面

但下面这三个方法才是实现单页面的关键。因为调用这三个api设置location不会引起浏览器向服务器发送页面请求

  • history.pushState(data, title, url)
  • history.popState()
  • history.replaceState(data, title, url)

history导航是一个栈,这是用来操作栈的方法,入栈、出栈、更新栈顶元素。只不过这个栈里面存放的是页面相关信息。最重要的是pushState,其他的可以暂时不管,也不影响使用。

还有一个关键的,当点击导航浏览器前进和后退按钮,会触发事件window.onpopstate。在这里,我们用js读取导航栈中的信息,并操作dom。这解决了和浏览器导航集成的问题。

这些 API 的主要目的是支持像单页应用这样的网站,它们使用 JavaScript API(如 fetch())来更新页面的新内容,而不是加载整个新页面。

其实到这里,单页面基实现的本原理已经清楚了。

实现一个简单的单页面应用

容器

单页面应用需要一个容器,这里我使用一个div作为页面的容器。

html 复制代码
<div id="app"></div>

实现页面

页面由模板js代码组成。为了方便书写,各自放在一个script标签中。在模板中声明页面结构,用type="text/html"属性,使得我们可以获得语法感知的提示。然后再定义脚本,其实就是一个函数。在其中读取模板,请求数据,然后渲染页面到容器中

html 复制代码
<!-- 模板 -->
	<script type="text/html" id="page1_html">
		<h1>首页</h1>
		<h2>这是第一个页面</h2>
		<div id="content"></div>
		<button style="background-color: lightcoral;" onclick="route.routeTo('/page2')">跳转到关于页面</button>
	</script>
<!-- 脚本 -->
	<script id="page1_js">
		function loadPage1(){
			//把模板页面替换进容器
			document.getElementById("app").innerHTML=document.getElementById("page1_html").innerHTML;
			//取数据然后生成内容,实际可能有ajax和fetch请求
			var data={
				text:"这是使用js生成的内容",
				id:1
			};
			document.getElementById("content").innerText=JSON.stringify(data);
		}
	</script>

前端路由调度

光有模板和脚本不行,我们还需要一个调度算法,用来调用页面渲染函数、更新导航。就是所谓的前端路由功能了。为了便于观察,我把声明的页面放在一个对象中,这就形成了路由表,方便搜索。当然,不要这个也是可以的,可以手动调用脚本渲染函数loadPage1

javascript 复制代码
const route={
    page:[
        {
            url:"/page1",
            module:loadPage1
        },
        {
            url:"/page2",
            module:loadPage2
        }
    ]
}

有了路由表之后,就可以添加调度算法,根据传入的url,寻找到对应的对象,添加到导航栈中,然后调用脚本渲染函数loadPageXXX

javascript 复制代码
const route={
    routeTo:(url)=>{
        var page=route.page.find(r=>r.url==url);
        if(page==null){
            alert("文件不存在");
        }
        else{
            if(window.location.pathname==url)return;
            history.pushState(page.url,"",page.url);
            page.module("这里可以传参数");
        }
    }
}
//默认跳转到首页
route.routeTo("/page1");

到了这一个,已经实现了单页面。点击跳转按钮,地址栏会变,页面也会变。但有一个问题,点击浏览器导航按钮不管用。这是因为我们还没监听popstate事件处理这个操作。

因为点击导航按钮,地址栏会变,但页面渲染什么内容?这需要我们去处理。所以在这个事件中,我们根据url,从路由表中找到页面,然后渲染出来。

javascript 复制代码
// 处理前进与后退
        window.addEventListener("popstate",(e)=>{
            var page=route.page.find(r=>r.url==e.state);
            if (page) {
                page.module("这里可以传参数");
            }
        })

效果

可以看到,切换页面时,地址栏变了,但并没有网络请求发出

完整代码

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SPA页面</title>
</head>
<body>
    <div id="app"></div>
    <!-- 首页 -->
    <script type="text/html" id="page1_html">
        <h1>首页</h1>
        <h2>这是第一个页面</h2>
        <div id="content"></div>
        <button style="background-color: lightcoral;" onclick="route.routeTo('/page2')">跳转到关于页面</button>
    </script>
    <script id="page1_js">
        function loadPage1(){
            //把模板页面替换进容器
            document.getElementById("app").innerHTML=document.getElementById("page1_html").innerHTML;
            //取数据然后生成内容,实际可能有ajax和fetch请求
            var data={
                text:"这是使用js生成的内容",
                id:1
            };
            document.getElementById("content").innerText=JSON.stringify(data);
        }
    </script>

    <!-- 关于页 -->
    <script type="text/html" id="page2_html">
        <h1>关于</h1>
        <h2>这是第二个页面</h2>
        <div id="content"></div>
        <button style="background-color: lightgreen;" onclick="route.routeTo('/page1')">跳转到首页</button>
    </script>
    <script id="page2_js">
        function loadPage2(){
            //把模板页面替换进容器
            document.getElementById("app").innerHTML=document.getElementById("page2_html").innerHTML;
            //取数据然后生成内容,实际可能有ajax和fetch请求
            var data={
                text:"假设这里是网站信息",
                id:2
            };
            document.getElementById("content").innerText=JSON.stringify(data);
        }
    </script>
    
    <!-- 调度 -->
    <script>
        const route={
            page:[
                {
                    url:"/page1",
                    module:loadPage1
                },
                {
                    url:"/page2",
                    module:loadPage2
                }
            ],
            routeTo:(url)=>{
                var page=route.page.find(r=>r.url==url);
                if(page==null){
                    alert("文件不存在");
                }
                else{
                    if(window.location.pathname==url)return;
                    history.pushState(page.url,"",page.url);
                    page.module("这里可以传参数");
                }
            }
        }
        // 处理前进与后退
        window.addEventListener("popstate",(e)=>{
            var page=route.page.find(r=>r.url==e.state);
            if (page) {
                page.module("这里可以传参数");
            }
        })
        //默认跳转到首页
        route.routeTo("/page1");
    </script>
</body>
</html>

结语

从上面的实现中,你们也能看到,单页面应用就是把整个应用程序发送到浏览器,在浏览器里面去运行这个程序。所以相比与一个html文件,一旦应用上规模,体积也相应的会很大。这就牵扯出chunk,把应用分块的概念。第一次请求的时候只把调度部分、首页以及相关几个页面传入到浏览器,后续请求到了没有传输的页面时,再把后续文件传输过来。

由于我们的页面都是在一个网页中,本质上是传输了一个web服务器到浏览器中,在导航时,由js控制渲染。所以过渡效果、过滤器、请求管道、中间件、web服务器具有的东西在网页中实现也就有了可能。

但单页面应用SPA,这个web服务器一切都要归功于浏览器提供的那个关键的功能,History API

当然,这里还没有处理复制一个url到新标签页打开的功能。但这其实很简单,就是首次跳转根据location来调用route.routeTo("地址栏的url")

相关推荐
a_ran1 天前
一些 Go Web 开发笔记
后端·golang·go·编程·web·网站
柏箱2 天前
使用html写一个能发起请求的登录界面
前端·javascript·html·web
OEC小胖胖3 天前
Spring MVC系统学习(二)——Spring MVC的核心类和注解
java·后端·学习·spring·mvc·web
cyt涛6 天前
WEB服务器——Tomcat
运维·服务器·http·servlet·tomcat·web·jsp
OEC小胖胖6 天前
js中正则表达式中【exec】用法深度解读
开发语言·前端·javascript·正则表达式·web
Dovir多多7 天前
web服务器运维常用技巧总结
运维·服务器·ubuntu·docker·centos·云计算·web
余生H7 天前
前端大模型入门:使用Transformers.js实现纯网页版RAG(一)
前端·人工智能·transformer·embedding·web·word2vec·rag
安红豆.9 天前
[NewStarCTF 2023 公开赛道]Begin of PHP1
web安全·php·哈希算法·web·ctf
HinsCoder9 天前
【测试】——Selenium API (万字详解)
自动化测试·笔记·学习·selenium·测试工具·web·测试
OEC小胖胖9 天前
js进阶——作用域闭包
开发语言·前端·javascript·ecmascript·web