单页面应用程序

几个比较典型的单页面应用程序网站:

单页应用(英语:single-page application,缩写SPA)是一种网络应用程序网站的模型,它通过动态重写当前页面来与用户交互,而非传统的从服务器重新加载整个新页面。这种方法避免了页面之间切换打断用户体验,使应用程序更像一个桌面应用程序

单页应用的出现不可考。从 Wikipedia 上可以看到,起源很早,甚至可以追溯到 2002 年。你可能会说,这不是什么新概念,Google 的 Gmail 就是一个单页应用,可追溯到 2004 年到 2005 年。但这个概念火热起来在 2010 年,Backbone 问世之后。随着后来的 Knockout、Ember.js、AngularJS 的兴起,单页应用开始家喻户晓起来。2015 年 React 的出现,Virtual DOM 横扫了各种 MV* 框架,单页应用框架甚至进入到了一个新的时代。目前主流的前端开发框架都可以让你轻松的构建单页面应用程序。

SPA 技术将逻辑从服务器转移到了客户端。这导致 Web 服务器发展为一个纯数据 API 或 Web 服务。这种架构的转变在一些圈子中被称为“瘦服务器架构”,以强调复杂性已从服务端转移到客户端,并认为这最终降低了系统的整体复杂性。

下面是单页面应用和传统的多页面应用各自的交互方式:

多页面应用,多页面跳转刷新所有资源:
image-20190617223054564.png

在传统的网页应用中,浏览器更多的是充当一个展示层,路由处理、服务调用、页面跳转流程都由服务端来处理。即 MVC 都放在服务器端,而 V 作为用户界面则通过网络发送到浏览器端,作为 UI 与用户交互。

这样的范式有以下特点:

  • 重服务端,由于 MVC 都存在于服务器上,因此这类应用在开发资源和开发的重心都偏向后端,往往是后端工程师来主导整个项目开发;
  • 页面频繁刷新,由于浏览器端只是一个展现层,当页面功能有所变化的时,页面就刷新,这会导致资源的浪费,用户需要花费额外的时间等待页面刷新,用户体验不佳。

单页面应用,只有一张 Web 页面的应用,是一种从 Web 服务器加载的富客户端,单页面跳转仅刷新局部资源 ,公共资源(js、css 等)仅需加载一次:
image-20190617223143378.png

优缺点

优点:

  • 增强用户体验,这个应该是最显著的有点,由于路由分发直接在浏览器端完成,页面是不刷新,对用户的响应非常及时,因此提升了用户体验
  • 减轻服务器压力,服务器只用出数据就可以,不用管展示逻辑和页面合成,吞吐能力会提高几倍
  • 完全的前端组件化,前端开发不再以页面为单位,更多地采用组件化的思想,代码结构和组织方式更加规范化,便于修改和调整
  • 良好的前后端分离开发,前端负责界面显示,后端负责数据存储和计算,各司其职,不会把前后端的逻辑混杂在一起

缺点:

  • 不利于 SEO,单页页面,数据在前端渲染,就意味着没有 SEO,或者需要使用变通的方案
    • 不过目前可以配置服务端渲染来解决这个问题,但还不够足够成熟
  • 初次加载耗时相对增多,要在一个页面上为用户提供产品的所有功能,在这个页面加载的时候,首先要加载大量的静态资源,这个加载时间相对比较长
  • 较高的前端开发门槛,对开发人员技能水平要求较高,不再是『切切图,画画页面这么简单』
  • 低版本浏览器兼容差

单页面导航路径

  • 概念
  • 路径
    • 一般使用锚点,也就是 hash 作为页面导航的路径标识
    • 为什么?因为正常的 url 地址会发请求,而 hash 锚点不会发请求刷新页面
  • VueRouter 内部监视了 hash 的改变
    • window.onhashchange
  • 然后根据 hash 的改变去展示路由规则中配置的组件
  • Vue Router 默认要求 hash 导航路径都以 #/ 开头
    • 为什么?
    • 主要是为了和正常的 hash 锚点(网页内部定位,id)作区别
    • 例如我们使用锚点内部定位的时候,需要给元素起 id,我们几乎不会给这个 id 起名为 /xxx
    • 如果 VueRouter 没有 #/ 的规则,例如直接 #foo 就可能会和你锚点的那个 id foo 冲突。

有人会说,能不能让 url 漂亮点儿,不要使用 hash 呢?

可以。

我们可以利用 HTML5 history 的的方式使用传统的 url。(我们可以用 JavaScript 代码控制 url)。

工作原理

  • HTML5 History API
    • 优雅
    • 同构应用友好
  • Hash
    • 兼容性好
  • hash 模式
    • 兼容性更好,不需要后端处理任何配置
    • file 协议或是 http 协议都可以运行
    • 比较丑,不能用于服务端渲染同构开发
  • history 模式
    • 相比 hash 浏览器兼容不太好,需要后端特殊配置
    • 必须运行在 http|https 服务中
    • url 简洁美观,如果需要做服务端渲染同构开发,则必须使用 history 模式
    • 注意:使用了 history 模式之后,不要在模板中直接使用普通的 a 链接去跳转,一定要使用 router-link 或者 router.push 进行导航。

hash 模式

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <title>Document</title>
  6. </head>
  7. <body>
  8. <ul>
  9. <li><a href="#/page1">页面1</a></li>
  10. <li><a href="#/page2">页面2</a></li>
  11. </ul>
  12. <div id="router-view"></div>
  13. <script>
  14. const page1 = `
  15. <div>
  16. <h2>页面1</h2>
  17. </div>
  18. `;
  19. const page2 = `
  20. <div>
  21. <h2>页面2</h2>
  22. </div>
  23. `;
  24. const page404 = `
  25. <div>
  26. <h2>页面2</h2>
  27. </div>
  28. `;
  29. var routerView = document.querySelector("#router-view");
  30. window.onhashchange = function() {
  31. const hash = window.location.hash.substr(1);
  32. if (hash === "/page1") {
  33. routerView.innerHTML = page1;
  34. } else if (hash === "/page2") {
  35. routerView.innerHTML = page2;
  36. } else {
  37. routerView.innerHTML = page404;
  38. }
  39. };
  40. window.onhashchange();
  41. </script>
  42. </body>
  43. </html>

history 模式

History API

  • onpopstate 响应历史记录改变的一个事件
  • pushState 创建历史记录
  • 前进,后退,控制

一共两个需求:

  • 执行打开页面的操作
  • 历史操作记录

Hash

  • hashchange
  • location.hash

安装

直接下载 / CDN

https://unpkg.com/vue-router/dist/vue-router.js

在 Vue 后面加载 vue-router,它会自动安装的:

<script src="/path/to/vue.js"></script>
<script src="/path/to/vue-router.js"></script>

NPM

npm install vue-router

如果在一个模块化工程中使用它,必须要通过 Vue.use() 明确地安装路由功能:

import Vue from "vue";
import VueRouter from "vue-router";

Vue.use(VueRouter);

如果使用全局的 script 标签,则无须如此 (手动安装)。

Hello World

用 Vue.js + Vue Router 创建单页应用,是非常简单的。使用 Vue.js ,我们已经可以通过组合组件来组成应用程序,当你要把 Vue Router 添加进来,我们需要做的是,将组件 (components) 映射到路由 (routes),然后告诉 Vue Router 在哪里渲染它们。下面是个基本例子:

<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>

<div id="app">
  <h1>Hello App!</h1>
  <p>
    <!-- 使用 router-link 组件来导航. -->
    <!-- 通过传入 `to` 属性指定链接. -->
    <!-- <router-link> 默认会被渲染成一个 `<a>` 标签 -->
    <router-link to="/foo">Go to Foo</router-link>
    <router-link to="/bar">Go to Bar</router-link>
  </p>
  <!-- 路由出口 -->
  <!-- 路由匹配到的组件将渲染在这里 -->
  <router-view></router-view>
</div>
// 0. 如果使用模块化机制编程,导入Vue和VueRouter,要调用 Vue.use(VueRouter)

// 1. 定义 (路由) 组件。
// 可以从其他文件 import 进来
const Foo = { template: "<div>foo</div>" };
const Bar = { template: "<div>bar</div>" };

// 2. 定义路由
// 每个路由应该映射一个组件。 其中"component" 可以是
// 通过 Vue.extend() 创建的组件构造器,
// 或者,只是一个组件配置对象。
// 我们晚点再讨论嵌套路由。
const routes = [
  { path: "/foo", component: Foo },
  { path: "/bar", component: Bar }
];

// 3. 创建 router 实例,然后传 `routes` 配置
// 你还可以传别的配置参数, 不过先这么简单着吧。
const router = new VueRouter({
  routes // (缩写) 相当于 routes: routes
});

// 4. 创建和挂载根实例。
// 记得要通过 router 配置参数注入路由,
// 从而让整个应用都有路由功能
const app = new Vue({
  router
}).$mount("#app");

// 现在,应用已经启动了!

动态路由匹配

https://router.vuejs.org/zh/guide/essentials/dynamic-matching.html


嵌套路由

https://router.vuejs.org/zh/guide/essentials/nested-routes.html


编程式导航

https://router.vuejs.org/zh/guide/essentials/navigation.html


命名路由


命名视图


重定向和别名


相关 API

路由对象 this.$route

一个路由对象 (route object) 表示当前激活的路由的状态信息,包含了当前 URL 解析得到的信息,还有 URL 匹配到的路由记录 (route records)

路由对象是不可变 (immutable) 的,每次成功的导航后都会产生一个新的对象。

路由对象出现在多个地方:

  • 在组件内,即 this.$route
  • $route 观察者回调内
  • router.match(location) 的返回值
  • $route.path
    • 类型: string
      字符串,对应当前路由的路径,总是解析为绝对路径,如 "/foo/bar"
  • $route.params
    • 类型: Object
      一个 key/value 对象,包含了动态片段和全匹配片段,如果没有路由参数,就是一个空对象。
  • $route.query
    • 类型: Object
      一个 key/value 对象,表示 URL 查询参数。例如,对于路径 /foo?user=1,则有 $route.query.user == 1,如果没有查询参数,则是个空对象。
  • $route.hash
    • 类型: string
      当前路由的 hash 值 (带 #) ,如果没有 hash 值,则为空字符串。
  • $route.fullPath
    • 类型: string
      完成解析后的 URL,包含查询参数和 hash 的完整路径。
  • $route.matched
    • 类型: Array<RouteRecord>

一个数组,包含当前路由的所有嵌套路径片段的路由记录 。路由记录就是 routes 配置数组中的对象副本 (还有在 children 数组)。

const router = new VueRouter({
  routes: [
    // 下面的对象就是路由记录
    {
      path: "/foo",
      component: Foo,
      children: [
        // 这也是个路由记录
        { path: "bar", component: Bar }
      ]
    }
  ]
});

当 URL 为 /foo/bar$route.matched 将会是一个包含从上到下的所有对象 (副本)。

  • $route.name
    当前路由的名称,如果有的话。(查看命名路由)
  • $route.redirectedFrom
    如果存在重定向,即为重定向来源的路由的名字。(参阅重定向和别名)

Router 实例

我们在组件中访问的 this.$router 就是路由文件中创建的路由实例

API 作用 备注
push 跳转,会形成历史记录 想要导航到不同的 URL,则使用 router.push 方法。这个方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,则回到之前的 URL。
replace 路径替换 跳过去,别回来了
go 跳转到指定步骤的路由
back 后退一步
forward 前进一步