基于 Vue 、React 的应用多数被称为 单页应用,就是打开一个页面,点击页面上的链接,浏览器地址栏的 URI 改变了,页面局部内容跟着改变,但整个页面并没有刷新,没有跳转。
# 例如
开始 http://h2o.xin/home
变成 http://h2o.xin/about
变成 http://h2o.xin/help
# 地址变,内容变,整个页面不刷新
下面基于我个人理解,分享一下大致原理。
我们要处理的内容有:
- URL地址: 改变浏览器 url 地址,通常是 /xxxx 部分,即路由地址
- 历史记录: 把改变后的 url 地址保存为历史记录,以便之后来回切换
- 渲染组件: 新的路由地址,渲染相应新的组件
切换内容的场景包括:
- 超链接: 点超链接切换显示不同的组件内容
- 前进/后退按钮: 点击按钮切换显示不同的组件内容
- 浏览器刷新: 浏览器刷新后决定显示哪个组件
接下来我就以上面3个场景为主线来进行说明。
一. <a> 链接
框架中 <router-link> 之类的自定义标签,其最终实质都是基于 <a> 标签,然后绑定单击事件
在单击事件处理函数中,对 3 个内容操作处理:
1-URL 地址 & 2-历史记录
history.pushState(state, title[, url])
功能:
- 改变浏览器地址栏中地址,只能修改 URI,相当于 window.location.pathname
- 添加到浏览器历史记录
- 不会实际请求. 就是说没有实际跳转刷新动作
参数:
- state 状态对象,将来浏览器切换到相应的 url 地址时,可以拿到这个自定义的对象。它有640k个字符的限制
- title 大多数浏览器忽略此参数。一般给空字符串
- url 改变后的 URI 地址. 可以是当前网站根目录开始, 也可以是相对路径, 但不能是其它域名.
const state = { 'page_id': 1, 'user_id': 5 }
const title = ''
const url = 'abcd.html'
history.pushState(state, title, url)
history.replaceState(state, title[, url])
和 pushState() 几乎一样,只是 replace 是替换当前url,而非添加
3-渲染组件
在 <a> 事件处理函数中:
- 通过 window.location.pathname 获取当前路由地址
- Vue、React 等框架根据路由地址加载不同的组件内容,框架中封装好的方法,本文暂不讨论。
二.前进/后退 按钮
1-URL 地址 & 2-历史记录
浏览器的前进/后退按钮,就是借助“历史记录”切换 url 地址,比如从 /home , 变成 /about , 变成 /help ,但浏览器并没有缓存相应内容,因为那些内容不是浏览器向服务器请求而来的,而是 JS 代码加载组件得来。
3-渲染组件
通过 JS 代码监听 「popstate 事件」,来获知 前进/后退 按钮的按下。
在对应的事件处理函数中:
- 通过 window.location.pathname 获取当前路由地址
- Vue、React 等框架根据路由地址加载不同的组件内容,框架中封装好的方法,本文暂不讨论。
三.浏览器刷新
原理
假设当前浏览器路由地址为 http://h2o.xin/about ,此时刷新浏览器会向远程服务器发送请求。对于单页应用,路由地址是由客户端负责解析,远程服务器没有相应的处理。为了防止报错,服务器端把不能正确处理的请求响应为网站首页,例如 http://h2o.xin/index.html ,此时,浏览器上面地址还会保持为 http://h2o.xin/about。浏览器接收影响后,重新获取控制权,页面中的 JS 代码根据当前路由地址再次渲染相应的组件内容。
实现
不同的服务端有不同的实现方式。假设打包后的项目代码在 web 目录中
node.js + express 的方式
# 创建空项目, 安装 express
npm init -y
npm i express -D
创建 app.js
const path = require('path')
// 导入处理 history 模式的模块
const history = require('connect-history-api-fallback')
// 导入 express
const express = require('express')
const app = express()
// 注册处理 history 模式的中间件
app.use( history() )
// 处理静态资源的中间件,网站根目录 ./web
app.use(express.static(path.join(__dirname, './web')))
// 开启服务器,端口是 3000
app.listen(3000, () => {
console.log('服务器开启,端口:3000')
})
执行
node app.js
nginx 方式
将 web 目录中内容复制到 nginx 的 html 目录中 (网站根目录, 不同版本名称不同)
编辑 nginx.conf
server {
listen 80;
server_name localhost;
location / {
root html;
index index.html index.htm;
# 添加下面这一行就好
try_files $uri $uri/ /index.html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
对前端感兴趣的朋友可以加Q群沟通(929330787)