阅读 213

由 SPA 和 MPA 引发关于页面刷新的思考

1 引言

我发现一件事情,就是如果写一篇记录学习的博客,有时候是关于一些误入牛角尖的问题,真的只有有过相同经历的能够切身体会理解,所以能点赞收藏的小伙伴,真的给我一种知音的感觉

小菜鸡如果说错了,烦请指点。问题的起源偶然发现,在我学习的项目中,多页面应用之间的跳转都是通过 <form> 表单或者 <a/> 标签改变 URL,进行页面之间的跳转。而在单页面应用中都是通过前端路由的,明显使用路由跳转更为便捷。

然后问题就来了,请仔细看一下 SPA (Single Page Application) 和 MPA(Mulpile Page Application) 项目的 URL

MPA:

image-20210610092424034.png

SPA:

image-20210610092451952.png

从上可以看出一些差别, MPA 端口后面跟着的就是要请求的 html 文件,? 后面跟着请求的参数。 而 SAP 端口后面跟着的就"路径",也不知道请求的是啥资源,html 文件? json 文件?或者其他?这就很让人疑惑。其实这里就可以引入一个概念——渲染方式

2 渲染

2.1 CSR 和 SSR

SPA 全称 Single Page Application,即单页面应用。一般也称为 CSR(Client Side Render),即客户端渲染。它所需的资源,如 HTML、CSS 和 JS 等,在一次请求中就加载完成,也就是不需刷新地动态加载。浏览器(Client)渲染顾名思义就是所有的页面渲染、逻辑处理、页面路由、接口请求均是在浏览器中发生。对于 SPA 来说,页面的切换就是视图之间的切换。

MPA(Mulpile Page Application),即多页面应用。一般都是 SSR 全称 Server Side Render,服务器端渲染。服务端渲染则是在服务端完成页面的渲染,在服务端完成页面模板、数据填充、页面渲染,然后将完整的HTML内容返回给到浏览器。由于所有的渲染工作都在服务端完成,因此网站的首屏时间,白屏时间也会比较长,同时,对于服务端的负载要求也会比较高。

2.2 前端框架的 SPA

如此,引言里边的疑惑就可以窥探到一点真相了,应该可以这样去理解这件事——例如利用 React 脚手架 create-react-app 创建一个单页面应用 (SPA),在 vcCode 终端运行 npm start,就会启动项目,一般是占用3000端口,在浏览器地址栏输入http://localhost:3000就会可以看到初始页面,原因应该是 vcCode,会在本地开启一个服务器,在客户端也就是浏览器访问本地的3000端口,就会把所需的资源一次性全给客户端,这样例如访问localhost:3000/ldp.json,就会去项目下的public文件夹下寻找 ldp.json文件 没有指定要请求什么资源的时候,会默认为项目的 public 文件夹下的index.html,至于展示index.html 需要啥资源都能清楚,这也就依赖于 webpack 的打包功能了。,然后在 index.html文件里面一般会有一个 div标签作为容器,src 目录下会有一个 index.js 文件,作为入口文件,里面可以写 JSX 语法,然后渲染成真实 DOM 到容器里面。再细说就是 React 的组件化思想了。

3 路由

3.1 后端路由

通过用户请求的 URL 导航到具体的 HTML 页面;每跳转到不同的 URL,都是重新访问服务端,如果是静态资源的获取,那么这个函数就是一个文件读取操作,如果是较为复杂的动态资源的获取,那么这个函数就是一个链接数据库,增删改查数据并进行处理的数据操作。下面自个手写了一个简单的获取静态页面的后端路由。

// 1. 引入 http,fs 模块
const http = require("http");
const fs = require('fs')
//创建服务
http.createServer(function (req, res) {
  // 设置 HTTP 头部,状态码是 200,文件类型是 html,字符集是 utf8
  res.writeHead(200, {
    "Content-Type": "text/html;charset=UTF-8"
  });

    if(req.url == '/test2') {
        let buf = fs.readFileSync("./test2.html");
        res.write(buf);  
    } else if (req.url == '/test1') {
        let buf = fs.readFileSync("./test1.html");
        res.write(buf);
    } else {
        res.write(req.headers.host+req.url)
    }
  // 结束响应
  res.end();

}).listen(3000); // 监听3000端口
复制代码

3.2 前端路由

3.2.1 前端路由的由来

SPA 的出现离不开 Ajax,SPA 的一大特征就是在与用户的交互过程中,不再需要重新刷新页面,而通过 Ajax 向服务端发送请求,不改变 URL,能够实现局部刷新,使得页面显示更加流畅

但是SPA 中用户的交互是通过 JS 改变 HTML 内容来实现的,页面本身的 URL 并没有变化,这就导致一些不方便:

  • SPA 无法记住用户的操作记录,假如你想分享一个详情页面的文章链接给你的好朋友,但是由于你的操作没有导致 URL 的变化,你的朋友在浏览器复制粘贴你的链接,打开的是首页而不是详情页。
  • SPA 中虽然由于业务的不同会有多种页面展示形式,但只有一个 URL,对 SEO 不友好,不方便搜索引擎进行收录

不改变 URL,可以避免页面的全局重新渲染,但是又会导致上面的问题,难搞哦,前端路由就是为了解决这两者冲突的问题

3.2.2 hsah

hsah路由是一种比较早的路由,熟悉 URL 构成的小伙伴一定知道 # 后的内容 Web 服务都会自动忽略,可以通过监听onhashchange事件获取到hash的改变,通过 window.location.hash 读取到的,读取到路径加以解析之后就可以响应不同路径的逻辑处理,好像还有一种说法叫锚点,用来做定位使用的

<!DOCTYPE html>
 <html lang="en">
 <head>
     <meta charset="UTF-8">
     <meta http-equiv="X-UA-Compatible" content="IE=edge">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>Document</title>
 </head>
 <body>
     <ul>
        <li><a href="#/">/</a></li>
        <li><a href="#/page1">page1</a></li>
        <li><a href="#/page2">page2</a></li>
     </ul>
     <div id="inner">1</div>
    <script>
        let sum=1
        window.addEventListener("hashchange", () => {
            let result = document.getElementById('inner')
            sum++
            result.innerHTML = sum
        });
    </script>
 </body>
 </html>
复制代码

上面的代码就简单的实现了根据不同的 hash 路由在页面展现不同的内容

3.2.3 html5

在HTML5之前,浏览器的history可以通过以下几个方法,在多个页面之间跳转

history.go(n);        // 前进n页,可为负数
history.forward();     // 前进一页
history.back();      // 后退一
复制代码

在 HTML5 的规范中,history 新增了以下几个 API:

history.pushState();         // 添加新的状态到历史状态栈
history.replaceState();      // 用新的状态代替当前状态
history.state                // 返回当前状态对象
复制代码

pushStatereplaceState 的参数都有三个

  1. 状态对象,通过pushState () 创建新的历史记录条目。状态对象发生变化,popstate事件就会被触发
  2. 标题,大部分浏览器会忽略这个参数
  3. URL ,该参数定义了新的历史URL记录,pushState() 后浏览器并不会立即加载这个URL

我监听了两个按钮的点击事件,具体代码如下:

        button[3].addEventListener("click",() => {//按钮1
            var stateObj = { foo: 'bar' };
            var stateObj2 = {bar:'foo'}

            history.pushState(stateObj, 'page 2', '2.html');
            history.pushState(stateObj2, 'page 2', '3.html');
            console.log(history.state)
        })
        button[4].addEventListener("click",() => {//按钮2
            history.pushState({page: 1}, 'title 1', '?page=1');
            history.pushState({page: 2}, 'title 2', '?page=2');
            history.replaceState({page: 3}, 'title 3', '?page=3');
            console.log(history.state)

        })
复制代码

一开始的 URL 是这样的 image-20210610194857314.png

点击按钮一:image-20210610194953222.png

此时 history,state 输出是 {bar: "foo"},我在点击浏览器回退按钮,URL为: image-20210610195252943.png

在浏览器打印此时的 history,state,为 {foo: "bar"}

点击按钮二:image-20210610195511531.png

回退后为: image-20210610195601071.png

值得注意的是,虽然点击按钮 URL 发生了变化,但是页面并不会刷新变化,这就是路由的雏形了。

3.2.4 前端框架路由

框架的路由里面水太深,我暂时把握不住,后面等自己将 Vue 和 React 的学习进行总结的时候会考虑记录,但是能够知道,前端框架的路由都是对前端路由的封装,因为最近学 React 比较多,还是拿它来举例。

react-router 的工作方式,是在组件树顶层放一个 Router 组件,然后在组件树中散落着很多 Route 组件(注意比 Router 少一个“r”),顶层的 Router 组件负责分析监听 URL 的变化,在它保护伞之下的 Route 组件可以直接读取这些信息。

在 index.js 文件里面就渲染 <App/> 组件,<App>组件如图:

image-20210608194144845.png

页面跳转就要使用 Link 标签 <Link to='/'>Home</Link>,更改 URL,而不做页面跳转,其实 <Link>,是封装后的 <a>

关于 BrowserRouter 和 HashRouter

  1. 留下历史记录的底层原理不一样
  • BrowserRouter使用的是H5的history API,不兼容IE9及一下版本
  • HashRouter使用的URL的哈希值(/#)#后面的内容是不会传给服务器的
  1. 刷新后对路由state参数的影响
    • BrowserRouter没有影响,因为state保存在history中,浏览器不关闭,不清理缓存,history就能用
    • HashRouter刷新后会导致state参数的丢失,因为state再地址上面不放参数,然后HashRouter又不能调用history

4 多页面路由

其实我最初的疑惑还有一个就是,在多页面应用中能不能使用路由呢?

谷歌了一下,发现好像有这种解决方案呢?感兴趣的同学可以搜索多页面多路由,进行了解,我大概看了一下,都是基于 Vue-cli,Vue 我都有段时间没碰了,那我走?哈哈哈哈!待以后拾起来的时候再去看看,立个 flag 搁这。

参考:

前端:SPA、CSR、MPA 与 SSR

MDN History API

文章分类
前端
文章标签