服务端渲染(Server Side Render)
什么是SSR
这就要从前端发展开始讲起了。
早期受制于浏览器以及技术、兼容性等问题,网页的显示效果非常单一,几乎都是静态页,前端工作非常简单,其实就是编写页面模板,然后让后端负责渲染。在互联网早期,前端通常由后端或者是美工来兼任。当时的网站开发采用后端MVC模式,当用户访问网站时,会向后台发送一个请求,后台接收到请求,生成静态HTML文件,发送到浏览器(这也算一种服务端渲染?)。web1.0时代的诸多服务端框架最基础的组件之一就是文档模版,比如asp, jsp等,核心设计理念就是HTML文件里放占位符然后由服务端替换成实际数据后返回。
Model(模型层):提供/保存数据 Controller(控制层):数据处理,实现业务逻辑 View(视图层):展示数据,提供用户界面 前端只是后端 MVC 的 V
随着现代技术发展,到了web2.0时代,网页成为了一种可以在浏览器上单独运行的由HTML和JS编写的应用程序,前端负责编写这个应用程序,后端则负责提供数据接口和编写后台业务逻辑,前后端通过API交互,前端页面利用ajax进行数据更新。
在web开发中,开发者可以在MPA和SPA中进行选择,由于SPA 很快,用户体验好,并且实现了前后端的明确划分,大多数现代web应用程序都采用SPA。
但SPA在某些方面不如MPA,例如不好进行拓展,也不利于搜索引擎优化SEO。(MPA虽然慢,难维护,但是SEO好啊!所以电子商务喜欢用)
SPA(Single-Page Application)
包含在一个页面中的应用程序称之为SPA。当你访问SPA时,浏览器会下载整个应用程序的数据。因此,SPA是在浏览器而不是在服务器上执行逻辑,你浏览应用程序的不同部分时页面不会重新加载。
MPA(Multi-Page Application)
MPA是一个具有多个页面的应用程序。它的工作方式和传统的静态页面一样:每当用户与应用程序交互都需要重新加载页面。MPA通常包含大量数据和复杂的结构,它的页面大多包含静态内容和指向其他内部页面的链接。这就是为什么MPA的用户界面更复杂和多层。大多数大型电子商务应用程序都是使用MPA构建的。
在web2.0时代,SPA一般使用的是客户端渲染(CSR),当然也有时候会使用服务端渲染(SSR)。
CSR
客户端渲染模式下,前端只负责写视图和交互,后端提供数据接口,客户端请求后,服务端将前端渲染文件发给客户端,客户端再跑一遍JS,利用ajax请求数据,生成DOM插入HTML文件,最终渲染呈现。
SSR
服务端渲染模式下,数据的初始请求被放在了服务端,服务端收到请求后,把数据填充到模板形成完整的页面,由服务端把渲染的完整的页面返回给客户端。客户端拿到手的,是可以直接渲染然后呈现给用户的 HTML 内容,不需要为了生成 DOM 内容自己再去跑一遍 JS 代码,只需要初始化加载JS,进行事件的绑定即可进行交互。
对于同一个请求,csr在客户端进行请求,ssr在服务端进行请求。
更多SSR和CSR的比对参考文章
详细的SSR流程图:
为什么要使用SSR
使用CSR使得服务器压力减轻,也做到了前后端分离,但是在网络环境差的情况下,多次请求就使得首屏渲染慢,白屏时间长,在没完成渲染之前用户无法看到内容,很影响体验。
还有一点则是CSR模式下搜索引擎爬虫看不到完整的HTML源码,例如在爬取用React写的网站中,爬虫能够看到的就是这样的代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>React App</title>
</head>
<body>
<div id="root"></div>
<script src="/app.js"></script>
</body>
</html>
这样的结果显然不利于非常不利于SEO(Search Engine Optimization)。
相对CSR来讲,SSR就有首屏渲染快,利于SEO,节省客户端的资源消耗的优点。
如何使用SSR(NEXT.JS)
说简单点,就是将是将单页应用(SPA)在服务器端渲染成 HTML 片段,发送到浏览器,然后交由浏览器为其绑定状态与事件,使其成为完全可交互页面。
NEXT.JS是一个React框架,适用于SEO友好的网站,支持静态化和服务端渲染。该框架与SSR不相关的内容例如文件路由、API路由等暂不介绍,有兴趣的可以去看NEXT.JS官方文档
这里仅介绍SSR使用的相关内容。(Vue请使用NUXT.JS)
getServerSideProps
NEXT.JS提供一个在服务端运行的函数getServerSideProps,具体可以看官方描述。
export async function getServerSideProps(context) {
return {
props: {}, // will be passed to the page component as props
}
}
服务端在每次用户请求页面时,先调用这个函数,并用函数返回的信息在服务端渲染生成完整的HTML文件,最后发送给用户。
getServerSideProps只在服务器端运行,从不在浏览器上运行。NEXT.JS会向API自动请求数据,自动渲染完整HTML。
Simple Example
function Page({ data }) {
// Render data...
}
// This gets called on every request
export async function getServerSideProps() {
// Fetch data from external API
const res = await fetch(`https://.../data`)
const data = await res.json()
// Pass data to the page via props
return { props: { data } }
}
export default Page
Example 1
npx create-next-app
./pages/index.js
import Head from 'next/head'
import styles from '../styles/Home.module.css'
export default function Home({data}) {
return (
<div className={styles.container}>
<Head>
<title>Create Next App</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<div className={styles.card}>
<p>{data.str}</p>
</div>
</div>
)
}
export async function getServerSideProps(){
// ./pages/api/hello.js 服务器本地API
// data = { str : 'Hello World!'}
const res=await fetch('http://localhost:3000/api/hello')
// 可以是外部API 都会在服务端完成请求和渲染
// const res=await fetch('https://v2.jinrishici.com/token')
const data = await res.json()
return { props: { data } }
}
可以看到发送过来的已经是完整的html文件了。
这个例子和web1.0时代的后台渲染静态html类似。
Example 2
更复杂一点的页面(也没有,就是请求的数据在服务器初始化以后,仍然可以在页面上编辑,进入web2.0
同例子1,也是在index.js文件中
import Head from "next/head";
import styles from "../styles/Home.module.css";
import Item from "./component"
export default function Home({ data }) {
return (
<div className={styles.container}>
<Head>
<title>Create Next App</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Item data={data.data} />
</div>
);
}
export async function getServerSideProps() {
// const res=await fetch('http://localhost:3000/api/hello')
const res = await fetch("https://v2.jinrishici.com/token");
const data = await res.json();
return { props: { data } };
}
在新建的component.js中定义Item
import styles from '../styles/Home.module.css'
class Item extends React.Component {
constructor(props) {
super(props);
this.state = {
data: this.props.data,
};
this.change = this.change.bind(this);
}
change(event) {
let value = event.target.value;
this.setState({
data: value,
});
}
render() {
return (
<div className={styles.card}>
<input
autoFocus
type="text"
class="edit"
value={this.state.data}
onChange={this.change}
/>
;
</div>
);
}
}
export default Item;
在主页面index.js中进行SSR请求,将获得的data通过props传入Item组件进行初始化,Item中的值仍可以通过this.setState进行修改。
在控制台中可以看到请求不是在客户端(浏览器)中发起的,而是在服务端渲染完整的html文件后才发送给客户端的。
拓展:除了SSR还有什么——预渲染(Prerender)
如果页面没有数据、纯静态页面,或需要在客户端更新的数据较少,推荐使用Prerender来减轻负担。
利用NEXT.JS的Static Generation可以实现。
概念
在Prerender模式下,用户向服务端请求数据,服务端返回预先保存的静态HTML和JS文件,客户端接受HTML文件并进行渲染,同时根据收到的JS文件进行数据请求,获得数据后再将其填充进之前渲染出的框架中。页面只渲染一次,并且在打包时就决定了页面的样子。如果页面有数据,则渲染时数据与打包时一致,直到请求数据返回后才进行更新。
原理
利用Headless Chrome(在非Chrome环境中运行Chrome)Puppeteer(Node.js库)提供的API在无UI情况下调用Chrome,在Webpack构建的最后阶段访问配置路由,将其渲染的页面输出到HTML文件中,并建立相应文件目录打包。这样每次用户请求时就先发送这个打包页面,渲染后再请求数据填充。
区别
SSR和Prerender最大的区别就是Prerender是静态的,在打包时就准备好了HTML文件;而SSR是动态的,在每次用户请求时都生成HTML文件。
用户请求前的服务器渲染即为「预渲染」。
用户请求后的服务器渲染即为「服务端渲染」。
使用(NEXT.JS)
If a page uses Static Generation, the page HTML is generated at build time. That means in production, the page HTML is generated when you run
next build. This HTML will then be reused on each request. It can be cached by a CDN.
无数据
静态页面,不需要请求数据,NEXT.JS在构建时生成HTML文件。
function About() {
return <div>About</div>
}
export default About
有数据
请注意,这里的有数据指的是生成页面时需要的数据,生成HTML后就固定HTML文件的数布局了(没有虚拟DOM,生成了真实的DOM树,返回也返回完整文件)。
比如说生成博客首页时,需要根据博客文章标题列出列表,有数据指的是生成博客首页需要的文章标题数据而不是其他实时更新的数据。一旦根据提供的数据生成了HTML文件以后,HTML的布局就不能更改,想要修改生成后的HTML文件就需要额外在客户端使用AJAX请求等来更新数据
利用getStaticProps和getStaticPaths来进行预渲染。
Example 1
单页,不需要根据数据不同生成不同页面
function Blog({ posts }) {
return (
<ul>
{posts.map((post) => (
<li>{post.title}</li>
))}
</ul>
)
}
// This function gets called at build time on server-side.
// It may be called again, on a serverless function, if
// revalidation is enabled and a new request comes in
export async function getStaticProps() {
const res = await fetch('https://.../posts')
const posts = await res.json()
return {
props: {
posts,
},
// Next.js will attempt to re-generate the page:
// - When a request comes in
// - At most once every second
revalidate: 1, // In seconds
}
}
export default Blog
Example 2
使用动态路由,页面根据数据不同有相应更改,但总体框架相同,如根据博客内容(某目录下markdown文件)生成博客文章页面,框架相同,但是根据路由不同改变内容。下列以建立博客为例。
// pages/posts/[id].js
function Post({ post }) {
// Render post...
}
// This function gets called at build time
export async function getStaticPaths() {
// Call an external API endpoint to get posts
const res = await fetch('https://.../posts')
const posts = await res.json()
// Get the paths we want to pre-render based on posts
const paths = posts.map((post) => ({
params: { id: post.id },
}))
// We'll pre-render only these paths at build time.
// { fallback: false } means other routes should 404.
return { paths, fallback: false }
}
// This also gets called at build time
export async function getStaticProps({ params }) {
// params contains the post `id`.
// If the route is like /posts/1, then params.id is 1
const res = await fetch(`https://.../posts/${params.id}`)
const post = await res.json()
// Pass post data to the page via props
return { props: { post } }
}
export default Post
getStaticPaths获取一个包含所有需要预渲染的页面id的数组,用map方法生成必要的返回参数paths,当然也可以直接传入paths,例如
paths: [
{ params: { id: '1' } },
{ params: { id: '2' } }
]
该函数中返回的paths中的多组params会依次传给getStaticProps,getStaticProps根据param获取数据,并通过props将数据传给模板框架进行渲染,生成由多个HTML页面,用户可以通过post/[id]访问对应id的HTML文件。
getStaticPaths只会被调用一次,而getStaticProps和Post函数则是有几组params就调用几次,用于生成对应params的页面。
除了paths是getStaticPaths的必要返回参数,fallback也是必须的。当fallback为false时,不是getStaticPaths返回的/posts/[id]路径都将显示404;当fallback为true时,请求非getStaticPaths返回的的路径会得到NEXT.JS会单独提供一个fallback版本的页面,并在后台重新运行getStaticPaths、getStaticProps和Post尝试获取这个id所对应的数据并附加生成对应的HTML文件,并在生成成功以后返回给用户,也会将其添加到缓存中,这样下次再次请求相同id也会有对应的HTML。
简单说就是fallback:true时可以自动额外添加渲染并缓存没有在path中给出,但可以请求到该id对应数据的页面。除非有意限制用户能访问到的页面,不然强烈建议fallback设为true。
结语
由于请求数据和渲染都在服务端完成,会导致服务端负载过重,开发和维护的成本也有点高(服务端和客户端运行同一套代码,需要同构)。而且SEO除了SSR还有其他解决方案(Prerensder,虽然某些场景(用户发布信息实时更新等)下并不适用)。个人觉得,如果害怕SPA用户体验差,在加载的时候加个loading页面,加载好了自然过渡就挺好。
再说了,不能跑JS的搜索引擎不是好搜索引擎,Google和Bing都能跑了,Maybe以后百度也能跑了,所以其实不太需要关心SEO问题(应该升级搜索引擎而不是让前端优化!)
简而言之,不推荐使用SSR。
以及,NEXT.JS是真的很适合用来搭建博客!