单页面与多页面应用, 路由

多页面应用(MultiPage Application,MPA)

MPA (Multi-page Application) 多页面应用指的就是最传统的 HTML 网页设计,早期的网站都是这样的设计,所之称为「网页设计」。使用 MPA 在使用者浏览 Web 时会依据点击需求切换页面,浏览器会不停的重载页面 (Reload),整个操作也常感觉卡卡。如果使用这样的设计在 Web App 中,使用者体验比较差,整体流畅度扣分。但进入门槛低,简单套个 jQuery 就可以完成。

单页面应用(SinglePage Web Application,SPA)

SPA (Single-page Application) 顾名思义在 Web 设计上使用单一页面,利用 JavaScript 操作 Dom 的技术实现各种应用,现今在介面上算是非常受欢迎的设计,搭配 AJAX 使得整体页面反应速度相当迅速,配合上路由懒加载等手段可以达到Native应用的体验。

具体对比分析

单页面应用(SPA) 多页面应用(MPA)
应用组成 一个外壳页面和多个页面片段组成 多个完整页面组成
资源共用 共用,只需在外壳部分加载 不共用,每个页面都需要加载
刷新方式 页面片段刷新(局部刷新) 整页刷新
url 模式 a.com/#/pageone
a.com/#/pagetwo
a.com/pageone.html
a.com/pagetwo.html
用户体验 页面片段间的切换快,用户体验良好 页面切换加载缓慢,流畅度不够,用户体验比较差
转场动画 容易实现 无法实现
数据传递 容易 依赖 url传参、或者cookie 、localStorage等
搜索引擎优化(SEO) 需要单独方案、实现较为困难、不利于SEO检索 可利用服务器端渲染(SSR)优化 实现方法简易
试用范围 高要求的体验度、追求界面流畅的应用 适用于追求高度支持搜索引擎的应用
开发成本 较高,常需借助专业的框架 较低 ,但页面重复代码多
维护成本 相对容易 相对复杂
结论 单页面应用模式有很多好处,已经是Web开发的潮流,特别是移动应用开发

实现(流程图)

cmd-markdown-logo

代码实现

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>原生实现hash和browser两种路由模式</title>
</head>

<body>
  <div class="router_box">
    <a href="/home" class="router" replace="true">主页</a>
    <a href="/news" class="router">新闻</a>
    <a href="/team" class="router">团队</a>
    <a href="/about100" class="router">关于</a>
  </div>
  <div id="router-view"></div>

  <script>
    function Router(params) {
      // 记录routes配置
      this.routes = params.routes || [];
      // 记录路由模式
      this.mode = params.mode || 'hash';

      // 初始化
      this.init = function () {
        // 绑定路由响应事件
        var that = this;
        document.querySelectorAll(".router").forEach((item, index) => {
          item.addEventListener("click", function (e) {
            // 阻止a标签的默认行为
            if (e && e.preventDefault) {
              e.preventDefault();
            } else {
              window.event.returnValue = false;
            }

            if (that.mode == 'hash') {
              // 判断是replace方法还是push方法
              if (this.getAttribute("replace")) {
                var i = window.location.href.indexOf('#')
                // 通过replace方法直接替换url
                window.location.replace(
                  window.location.href.slice(0, i >= 0 ? i : 0) + '#' + this.getAttribute("href")
                )
              } else {
                // 通过赋值追加
                window.location.hash = this.getAttribute("href");
              }
            } else {
              if (this.getAttribute("replace")) {
                window.history.replaceState({}, '', window.location.origin + this.getAttribute("href"))
              } else {
                window.history.pushState({}, '', window.location.origin + this.getAttribute("href"))
              }
              // 手动调用更新内容方法
              that.routerChange();
            }

          }, false);
        });
        // 监听路由改变
        if (this.mode == 'hash') {
          window.addEventListener("hashchange", () => {
            this.routerChange();
          });
        } else {
          window.addEventListener('popstate', e => {
            console.log(123);
            this.routerChange();
          })
        }
        // 第一次进入的时候更新视图
        this.routerChange();
      },
        // 路由改变监听事件
        this.routerChange = function () {
          if (this.mode == 'hash') {
            let nowHash = window.location.hash;
            let index = this.routes.findIndex((item, index) => {
              return nowHash == ('#' + item.path);
            });

            if (index >= 0) {
              // 查找到对应路由
              document.querySelector("#router-view").innerHTML = this.routes[index].component;
            } else {
              // 没找到对应路由,查找有没有*
              let defaultIndex = this.routes.findIndex((item, index) => {
                return item.path == '*';
              });
              // 查找到*,执行重定向
              if (defaultIndex >= 0) {
                const i = window.location.href.indexOf('#')
                window.location.replace(
                  window.location.href.slice(0, i >= 0 ? i : 0) + '#' + this.routes[defaultIndex].redirect
                )
              }
            }
          } else {
            let path = window.location.href.replace(window.location.origin, '');
            let index = this.routes.findIndex((item, index) => {
              console.log('path...', path, 'item.path...', item.path);
              return path == item.path;
            });
            if (index >= 0) {
              // 查找到对应路由
              document.querySelector("#router-view").innerHTML = this.routes[index].component;
            } else {
              // 没找到对应路由,查找有没有*
              let defaultIndex = this.routes.findIndex((item, index) => {
                return item.path == '*';
              });
              // 查找到*,执行重定向
              if (defaultIndex >= 0) {
                window.history.replaceState({}, '', window.location.origin + this.routes[defaultIndex].redirect)
                this.routerChange();
              }
            }
          }
        }
      // 调用初始化
      this.init();
    }
    new Router({
      mode: 'history',
      routes: [
        { path: '/home', component: '<h1>主页</h1><h4>新一代前端工程师:我们啥都会</h4>' },
        { path: '/news', component: '<h1>新闻</h1><h4>今天2018-11-5,上课还得穿工装</h4>' },
        { path: '/team', component: '<h1>团队</h1><h4>WEB前端工程师</h4>' },
        { path: '/about', component: '<h1>关于</h1><h4>一面而高薪就业</h4>' },
        { path: '*', redirect: '/home' }
      ]
    });
  </script>
</body>

</html>

兼容问题

ie9及以下不支持html5 history新特性,可引入history库

上次更新: 2019-5-8 11:06:53