任何一个技术的出现都不是凭空产生的,一定是为了解决原有技术上的痛点。
服务端渲染和客户端渲染:
服务端渲染:
SSR:Server Side Rendering,服务端渲染。指的是服务器端生成完整的 HTML 页面后,发送给客户端直接渲染呈现。早期的页面都是通过服务器端渲染来完成的。
服务端渲染的缺点:有可能某次请求只是一些数据发生了变化,但服务器却需要重绘整个 HTML 页面,返回给浏览器重新渲染,不仅浪费了带宽,也增加了浏览器的性能消耗。
客户端渲染:
CSR:Client Side Rendering,客户端渲染。指的是客户端先解析服务器端发送过来的文件生成完整的 HTML 页面,再渲染呈现。这种模式就是前后端分离。
SPA 页面通常依赖的就是客户端渲染。
多页应用和单页应用:
多页应用:
一个 Web 应用包含很多 HTML 页面。每一次页面跳转的时候,服务器都会给返回一个新的 HTML 文档。
单页应用(single page application、SPA、单页面富应用):
整个应用只有一个完整的页面,点击页面中的链接不会刷新页面,只会做页面的局部更新。第一次进入页面的时候会请求一个 HTML 文件,跳转到其他路径,并不会请求新的 HTML 文件,但是页面内容也变化了。
单页应用的原理:
JS 会感知到 URL 的变化,通过这一点,可以利用 JS 动态的将当前的内容清除掉,然后将新的内容挂载到当前页面上。
单页应用的渲染流程:
- 首先浏览器根据域名或者 IP 地址向服务器请求一个
index.html
文件。 - 然后对其解析执行,生成完整的 HTML 页面后渲染显示;在这个过程中,如果发现 HTML 中有引入其他的文件,对其进行请求、解析、执行。
单页应用的缺点:
-
不利于 SEO (Search Engine Optimization)搜索引擎优化:例如百度的服务器集群 24 小时不间断地在网络上爬取数据,爬取单页应用的时候其实主要就是把网站的
index.html
下载下来,但是单页应用的index.html
中的 body 内其实只有一个 div 而已(使用 Webpack 打包构建),虽然 meta 等配置信息也是可以被爬取到的,但是页面的具体内容是爬取不到的。因此在百度的数据库中收录的网站的关键字很少,当用户在浏览器中通过百度的搜索引擎来搜索关键字的时候,由于匹配到的关键字很少,匹配度很低,因此排名会很靠后。谷歌在爬取数据方面做得要比百度好,也会执行 JS 文件,爬取单页应用时爬取到的关键字也会更多一些。
-
首屏渲染速度慢:页应用首次请求到的只是一个
index.html
页面,在解析的过程中发现有引入其他类型的文件,才会去请求下载,最后才会解析执行生成完整的 HTML 页面后渲染显示。
单页应用的 SEO 搜索引擎优化:
多页应用时,可以通过配置每个 HTML页面的 title 标题、meta 标签的 keywords 关键字、meta 标签的 descriptors 描述来进行 SEO。
-
预渲染:在构建时就简单地生成针对特定路由的静态 HTML 文件。以 Vue 为例,可以使用
prerender-spa-plugin
插件预渲染页面,搭配vue-meta-info
插件配置标题、关键字、描述。缺点:不适合有大量的页面需要 SEO 的情况,需要一个页面一个页面手动去配置;动态改变标题、关键字、描述是无效的。
// vue.config.js const path = require('path') const PrerenderSPAPlugin = require('prerender-spa-plugin'); module.exports = { configureWebpack: { plugins: [ new PrerenderSPAPlugin({ staticDir: path.join(__dirname, 'dist'), routes: [ '/', '/home', '/member', ], }), ], }, }
// main.js import Vue from 'vue' import MetaInfo from 'vue-meta-info' Vue.use(MetaInfo)
// 在 vue 页面中使用 export defaultes { metaInfo() { return { title: '标题', meta: [{ name: '关键字', content: '描述' }] } }, }
-
服务端渲染:服务器端生成完整的 HTML 页面后,发送给客户端直接渲染呈现。以 Vue 为例,可以使用开箱即用的
Nuxt.js
。
缺点:一套代码两套执行环境,会引起各种问题,例如 服务端没有window、document 等对象。
后端路由和前端路由:
路由:一个路由就是一个映射关系(key-value
:key 就是路径,value 就是 Function 或者 Component)。
后端路由:
早期的网站开发,整个 HTML 页面都是由服务器来生成的。每个页面都有自己对应的 URL,客户端发送 URL 给服务器,服务器通过对该 URL 进行匹配后生成对应的 HTML 页面,返回给客户端进行展示。
客户端请求不同的 URL,服务器生成好对应的整个页面后将页面返回给客户端,这就是后端路由。本质上就是 URL 请求地址与服务器资源之间的对应关系。在后端路由中一个路径对应一个 Function,用来处理客户端提交的请求。
后端路由会刷新整个页面。
以下为 Node 的后端路由:当 Node 接收到一个请求时,根据请求路径找到匹配的路由,调用路由中的函数来处理请求,返回响应函数。
前端路由:
前端路由的核心是 URL 发生变化,但是并不会向服务器发送新的请求,页面不会进行整体的刷新。本质上就是检测 URL 的变化,然后解析来匹配路由规则,渲染对应的组件,通过 JavaScript 来实现页面内容的更换。在前端路由中一个路径对应一个 Component,用于展示页面内容。
前端路由不会刷新整个页面。
主流的实现方式是通过 hash 模式和 history 模式。
hash 模式:
hash 模式利用的是 URL #
号后面的内容发生变化,浏览器不会向服务端发起新的请求。
可以通过 hashchange 事件监听 URL 的变化。
hash 是 URL 中 # 及后面的部分,常用作锚点在页面内进行导航。
<body>
<ul>
<!-- 定义路由 -->
<li><a href="#/home">home</a></li>
<li><a href="#/about">about</a></li>
<!-- 渲染路由对应的 UI -->
<div id="routeView"></div>
</ul>
</body>
// 页面加载完不会触发 hashchange,这里主动触发一次 hashchange 事件
window.addEventListener('DOMContentLoaded', onLoad)
// 监听路由变化
window.addEventListener('hashchange', onHashChange)
// 路由视图
var routerView = null
function onLoad () {
routerView = document.querySelector('#routeView')
onHashChange()
}
// 路由变化时,根据路由渲染对应 UI
function onHashChange () {
switch (location.hash) {
case '#/home':
routerView.innerHTML = 'Home'
break
case '#/about':
routerView.innerHTML = 'About'
break
default:
break
}
}
history 模式:
history 模式利用的是 HTML5 中新增的 history API。history API 用来操作浏览器的路由地址。浏览器会提供一个 history 对象,用来保存用户操作过的历史 URL,使用前进后退按钮时,URL 地址会发生变化,但不会向服务器发送请求,不会触发页面的整体刷新。因此可以使用 pushState 和 replaceState 这两个 API 将 URL 添加到 history 历史记录中就可以实现不发送请求的路由跳转。
可以通过 popstate 事件来监听 URL 的变化,通过 pushState 和 replaceState 这两个方法改变 URL 的 path 部分而不引起页面的整体刷新。
<body>
<ul>
<!-- 定义路由 -->
<li><a href="#/home">home</a></li>
<li><a href="#/about">about</a></li>
<!-- 渲染路由对应的 UI -->
<div id="routeView"></div>
</ul>
</body>
// 页面加载完不会触发 popstate,这里主动触发一次 popstate 事件
window.addEventListener('DOMContentLoaded', onLoad)
// 监听路由变化
window.addEventListener('popstate', onPopState)
// 路由视图
var routerView = null
function onLoad () {
routerView = document.querySelector('#routeView')
onPopState()
// 拦截 <a> 标签点击事件默认行为, 点击时使用 pushState 修改 URL并更新手动 UI,从而实现点击链接更新 URL 和 UI 的效果。
var linkList = document.querySelectorAll('a[href]')
linkList.forEach(el => el.addEventListener('click', function (e) {
e.preventDefault()
history.pushState(null, '', el.getAttribute('href'))
onPopState()
}))
}
// 路由变化时,根据路由渲染对应 UI
function onPopState () {
switch (location.pathname) {
case '/home':
routerView.innerHTML = 'Home'
break
case '/about':
routerView.innerHTML = 'About'
break
default:
break
}
}