开门见山的说,服务端渲染有两个特色:css
若是你的站点或者公司将来的站点须要用到服务端渲染,那么本文将会是很是适合你的一篇入门实战实践教学。本文采用 next
框架进行服务器渲染框架的搭建,最终将完成几个目标:html
本文的最终目标是全部人都能跟着这篇教程搭建本身的(第)一个服务端渲染项目,那么,开始吧。前端
咱们先新建一个目录,名为 jt-gmall
,而后进入目录,在目录下新建 package.json
,添加如下内容:react
{ "scripts": { "start": "next", "build": "next build", "serve": "next start" } }
而后咱们须要安装相关依赖:git
antd 是一个 UI 组件库,为了让页面更加美观一些。因为相关依赖比较多,安装过程可能会比较久,建议切个淘宝镜像,会快一点。github
npm i next react react-dom antd -S
依赖安装完成后,在目录下新建 pages
文件夹,同时在该文件夹下建立 index.jsx
ajax
const Home = () => <section>Hello Next!</section> export default Home;
在 next
中,每一个 .js
文件将变成一个路由,自动处理和渲染,固然也能够自定义,这个在后面的内容会讲到。npm
咱们运行 npm start
启动项目并打开 http://localhost:3000
,此时能够看到 Hello Next!
被显示在页面上了。json
咱们第一步已经完成,可是咱们会感受这和咱们平时的写法差别不大,那么实现上有什么差别吗?后端
在打开控制台查看差别以前,咱们先思考一个问题,SEO 的优化
是怎么作到的,咱们须要站在爬虫的角度
思考一下,爬虫爬取的是网络请求获取到的 html
,通常来讲(大部分)
的爬虫并不会去执行或者等待 Javascript 的执行,因此说网络请求拿到的 html
就是他们爬取的 html
。
咱们先打开一个普通的 React
页面(客户端渲染),打开控制台,查看 network
中,对主页的网络请求的响应结果以下:
咱们从图中能够看出,客户端渲染的 React
页面只有一个 id="app"
的 div
,它做为容器承载渲染 react
执行后的结果(虚拟 DOM 树),而普通的爬虫只能爬取到一个 id="app"
的空标签,爬取不到任何内容。
咱们再看看由服务端渲染,也就是咱们刚才的 next
页面返回的内容是什么:
这样看起来就很清楚了,爬虫从客户端渲染的页面中只能爬取到一个无信息的空标签
,而在服务端渲染的页面中却能够爬取到有价值的信息内容
,这就是服务端渲染对 SEO 的优化。那么在这里再提出两个问题:
AJAX
请求的数据也进行 SEO 优化吗?先解答第一个问题,答案是固然能够,可是须要继续往下看,因此咱们进入后面的章节,对 AJAX
数据的优化以及首屏渲染的优化逻辑。
本文的目的不止是教会你如何使用,还但愿可以给你们带来一些认知上的提高,因此会涉及到一些知识点背后的探讨。
咱们先回顾第一章的问题,服务端渲染能够对 AJAX 请求的数据也进行 SEO 优化吗?
,答案是能够的,那么如何实现,咱们先捋一捋这个思路。
首先,咱们知道要优化 SEO,就是要给爬虫爬取到有用的信息,而咱们不能控制爬虫等待咱们的 AJAX
请求完毕再进行爬取,因此咱们须要直接提供给爬虫一个完整的包含数据的 html
文件,怎么给?答案已经呼之欲出,对应咱们的主题 服务端渲染
,咱们须要在服务端完成 AJAX
请求,而且将数据填充在 html
中,最后将这个完整的 html
让爬虫爬取。
知识点补充: 能够作到执行js
文件,完成ajax
请求,而且将内容按照预设逻辑填充在html
中,须要浏览器的js 引擎
,谷歌使用的是v8
引擎,而Nodejs
内置也是v8
引擎,因此其实next
内部也是利用了Nodejs
的强大特性(可运行在服务端、可执行js
代码)完成了服务端渲染的功能。
下面开始实战部分,咱们新建文件 ./pages/vegetables/index.jsx
,对应的页面是 http://localhost:3000/vegetables
// ./pages/vegetables/index.jsx import React, { useState, useEffect } from "react"; import { Table, Avatar } from "antd"; const { Column } = Table; const Vegetables = () => { const [data, setData] = useState([{ _id: 1 }, { _id: 2 }, { _id: 3 }]); return <section style={{ padding: 20 }}> <Table dataSource={data} pagination={false} > <Column render={text => <Avatar src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" />} /> <Column key="_id" /> </Table> </section> } export default Vegetables;
进入 vegetables
页面后发现咱们的组件已经渲染,这也对应了咱们开头所说的 next
路由规则,可是咱们发现这个布局有点崩,这是由于咱们尚未引入 css 的缘由,咱们新建 ./pages/_app.jsx
import App from 'next/app' import React from 'react' import 'antd/dist/antd.css'; export default class MyApp extends App { static async getInitialProps({ Component, router, ctx }) { let pageProps = {} if (Component.getInitialProps) { pageProps = await Component.getInitialProps(ctx) } return { pageProps } } render() { const { Component, pageProps } = this.props return <> <Component {...pageProps} /> </> } }
这个文件是 next
官方指定用来初始化的一个文件,具体的内容咱们后面会提到。如今再看看页面,这个时候你的布局应该就好看多了。
固然这是静态数据,打开控制台也能够看到数据都已经被完整渲染在 html
中,那咱们如今就开始获取异步数据,看看是否还能够正常渲染。此时须要用到 next
提供的一个 API,那就是 getInitialProps
,你能够简单理解为这是一个在服务端执行生命周期函数,主要用于获取数据,在 ./pages/_app.jsx
中添加如下内容,最终修改后的结果以下:
因为咱们的代码可能运行在服务端也可能运行在客户端,可是服务端与不一样客户端环境的不一样致使一些 API 的不一致,fetch
就是其中之一,在Nodejs
中并无实现fetch
,因此咱们须要安装一个插件isomorphic-fetch
以进行自动的兼容处理。请求数据的格式为
graphql
,有兴趣的童鞋能够本身去了解一下,请求数据的地址是我本身的小站,方便你们作测试使用的。
import React, { useState } from "react"; import { Table, Avatar } from "antd"; import fetch from "isomorphic-fetch"; const { Column } = Table; const Vegetables = ({ vegetableList }) => { if (!vegetableList) return null; // 设置页码信息 const [pageInfo, setPageInfo] = useState({ current: vegetableList.page, pageSize: vegetableList.pageSize, total: vegetableList.total }); // 设置列表信息 const [data, setData] = useState(() => vegetableList.items); return <section style={{ padding: 20 }}> <Table rowKey="_id" dataSource={data} pagination={pageInfo} > <Column dataIndex="poster" render={text => <Avatar src={text} />} /> <Column dataIndex="name" /> <Column dataIndex="price" render={text => <>¥ {text}</>} /> </Table> </section> } const fetchVegetable = (page, pageSize) => { return fetch("http://dev-api.jt-gmall.com/mall", { method: 'POST', headers: { 'Content-Type': 'application/json' }, // graphql 的查询风格 body: JSON.stringify({ query: `{ vegetableList (page: ${page}, pageSize: ${pageSize}) { page, pageSize, total, items { _id, name, poster, price } } }` }) }).then(res => res.json()); } Vegetables.getInitialProps = async ctx => { const result = await fetchVegetable(1, 10); // 将查询结果返回,绑定在 props 上 return result.data; } export default Vegetables;
效果图以下,数据已经正常显示
下面咱们来好好捋一捋这一块的逻辑,若是你此时打开控制台刷新页面会发如今 network
控制台看不到这个请求的相关信息,这是由于咱们的请求是在服务端发起的,而且在下图也能够看出,全部的数据也在 html
中被渲染,因此此时的页面能够正常被爬虫抓取。
那么由此就能够解答上面提到的第二个问题,服务端渲染对首屏加载的渲染提高体如今何处?
,答案是如下两点:
html
中直接包含了数据,客户端能够直接渲染,无需等待异步 ajax
请求致使的白屏/空白时间,一次渲染完毕;ajax
在服务端发起,咱们能够在前端服务器与后端服务器之间搭建快速通道(如内网通讯),大幅度提高通讯/请求速度;咱们如今来完成第二章的最后内容,分页数据的加载
。服务端渲染的初始页面数据由服务端执行请求,然后续的请求(如交互类)都是由客户端继续完成。
咱们但愿能实现分页效果,那么只须要添加事件监听,而后处理事件便可,代码实现以下:
// ... const Vegetables = ({ vegetableList }) => { if (!vegetableList) return null; const fetchHandler = async page => { if (page !== pageInfo.current) { const result = await fetchVegetable(page, 10); const { vegetableList } = result.data; setData(() => vegetableList.items); setPageInfo(() => ({ current: vegetableList.page, pageSize: vegetableList.pageSize, total: vegetableList.total, onChange: fetchHandler })); } } // 设置页码信息 const [pageInfo, setPageInfo] = useState({ current: vegetableList.page, pageSize: vegetableList.pageSize, total: vegetableList.total, onChange: fetchHandler }); //... }
到这里,你们应该对 next
和服务端渲染已经有了一个初步的了解。服务端渲染简单点说就是在服务端执行 js
,将 html
填充完毕以后再将完整的 html
响应给客户端,因此服务端由 Nodejs
来作再合适不过,Nodejs
天生就有执行 js
的能力。
咱们下一章将讲解如何使用 next
搭建一个须要鉴权的页面以及鉴权失败后的自动跳转问题。
咱们在工做中常常会遇到路由拦截和鉴权问题的处理,在客户端渲染时,咱们通常都是将鉴权信息存储在 cookie、localStorage
进行本地持久化,而服务端中没有 window
对象,在 next
中咱们又该如何处理这个问题呢?
咱们先来规划一下咱们的目录,咱们会有三个路由,分别是:
vegetables
路由,里面包含了一些全部人均可以访问的实时菜价信息;login
路由,登陆后记录用户的登陆信息;user
路由,里面包含了登陆用户的我的信息,如头像、姓名等,若是未登陆跳转到 user
路由则触发自动跳转到 login
路由;咱们先对 ./pages/_app.jsx
进行一些改动,加上一个导航栏,用于跳转到对应的这几个页面,添加如下内容:
//... import { Menu } from 'antd'; import Link from 'next/link'; export default class MyApp extends App { //... render() { const { Component, pageProps } = this.props return <> <Menu mode="horizontal"> <Menu.Item key="vegetables"><Link href="/vegetables"><a>实时菜价</a></Link></Menu.Item> <Menu.Item key="user"><Link href="/user"><a>我的中心</a></Link></Menu.Item> </Menu> <Component {...pageProps} /> </> } }
加上导航栏之后,效果如上图。若是这时候你点击我的中心会出现 404
的状况,那是由于咱们尚未建立这个页面,咱们如今来建立 ./pages/user/index.jsx
:
// ./pages/user/index.jsx import React from "react"; import { Descriptions, Avatar } from 'antd'; import fetch from "isomorphic-fetch"; const User = ({ userInfo }) => { if (!userInfo) return null; const { nickname, avatarUrl, gender, city } = userInfo; return ( <section style={{ padding: 20 }}> <Descriptions title={`欢迎你 ${nickname}`}> <Descriptions.Item label="用户头像"><Avatar src={avatarUrl} /></Descriptions.Item> <Descriptions.Item label="用户昵称">{nickname}</Descriptions.Item> <Descriptions.Item label="用户性别">{gender ? "男" : "女"}</Descriptions.Item> <Descriptions.Item label="所在地">{city}</Descriptions.Item> </Descriptions> </section> ) } // 获取用户信息 const getUserInfo = async (ctx) => { return fetch("http://dev-api.jt-gmall.com/member", { method: 'POST', headers: { 'Content-Type': 'application/json' }, // graphql 的查询风格 body: JSON.stringify({ query: `{ getUserInfo { nickname avatarUrl city gender } }` }) }).then(res => res.json()); } User.getInitialProps = async ctx => { const result = await getUserInfo(ctx); // 将 result 打印出来,由于未登陆,因此首次进入这里确定是包含错误信息的 console.log(result); return {}; } export default User;
组件编写完毕后,咱们进入 http://localhost:3000/user
。此时发现页面是空白的,是由于进入了 if (!userInfo) return null;
这一步的逻辑。咱们须要看看控制台的输出,发现内容以下:
由于请求发生在服务端的
getInitialProps
,此时的输出是在命令行输出的,并不会在浏览器控制台输出,写服务端渲染的项目这一点要习惯。
{ errors: [ { message: '401: No Auth', locations: [Array], path: [Array] } ], data: { getUserInfo: null } }
拿到报错信息以后,咱们只须要处理报错信息,而后在出现 401
登陆未受权时跳转到登陆界面便可,因此在 getInitialProps
函数中再加入如下逻辑:
import Router from "next/router"; // 重定向函数 const redirect = ({ req, res }, path) => { // 若是包含 req 信息则表示代码运行在服务端 if (req) { res.writeHead(302, { Location: path }); res.end(); } else { // 客户端跳转方式 Router.push(path); } }; User.getInitialProps = async ctx => { const result = await getUserInfo(ctx); const { errors, data } = result; // 判断是否为鉴权失败错误 if (errors && errors.length > 0 && errors[0].message.startsWith("401")) { return redirect(ctx, '/login'); } return { userInfo: data.getUserInfo }; }
这里格外须要注意的一点就是,你的代码可能运行在服务端也可能运行在客户端,因此在不少地方须要进行判断,执行对应的函数,这样才是一个具备健壮性的服务端渲染项目。在上面的例子中,重定向函数就对环境进行了判断,从而执行对应的跳转方法,防止页面出错。
如今刷新页面,咱们应该跳转到了登陆页面,那么咱们如今就来把登陆页面实现一下,鉴于方便实现,咱们登陆界面只放一个登陆按钮,完成登陆功能,实现以下:
// ./pages/login/index.jsx import React from "react"; import { Button } from "antd"; import Router from "next/router"; import fetch from "isomorphic-fetch"; const Login = () => { const login = async () => { const result = await fetch("http://dev-api.jt-gmall.com/member", { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: `{ loginQuickly { token } }` }) }).then(res => res.json()); // 打印登陆结果 console.log(result); } return ( <section style={{ padding: 20 }}> <Button type="primary" onClick={login}>一键登陆</Button> </section> ) } export default Login;
代码写到这里就能够先停一下,思考一下问题了,打开页面,点击一键登陆按钮,这时候控制会输出响应结果以下:
{ "data": { "loginQuickly": { "token": "7cdbd84e994f7be693b6e578549777869e086b9db634363635e2f29b136df1a1" } } }
登陆拿到了一个 token
信息,如今咱们的问题就变成了,如何存储 token
,保持登陆态的持久化处理。咱们须要用到两个插件,分别是 js-cookie
和 next-cookies
,前者用于在客户端存储 cookie
,然后者用于在服务端和客户端获取 cookie
,咱们先用 npm
进行安装:
npm i js-cookie next-cookies -S
随后咱们修改 ./pages/login/index.jsx
,在登陆成功后将 token
信息存储到 cookie
之中,同时咱们也须要修改 ./pages/user/index.jsx
,将 token
做为请求头发送给 api 服务端
,代码实现以下:
// ./pages/login/index.jsx //... import cookie from 'js-cookie'; const login = async () => { //... const { token } = result.data.loginQuickly; cookie.set("token", token); // 存储 token 后跳转到我的信息界面 Router.push("/user"); }
// ./pages/login/index.jsx //... import nextCookie from 'next-cookies'; //... // 获取用户信息 const getUserInfo = async (ctx) => { // 在 cookie 中获取 token 信息 const { token } = nextCookie(ctx); return fetch("http://dev-api.jt-gmall.com/member", { method: 'POST', headers: { 'Content-Type': 'application/json', // 在首部带上身份认证信息 token 'x-auth-token': token }, // graphql 的查询风格 body: JSON.stringify({ query: `{ getUserInfo { nickname avatarUrl city gender } }` }) }).then(res => res.json()); } //...
之因此使用cookie
存储 token 是利用了cookie
会随着请求发送给服务端,服务端就有能力获取到客户端存储的cookie
,而localStorage
并无该特性。
咱们在 user
页面刷新,查看控制台(下图),会发现 html
文件的请求头中有 cookie
信息,服务端获取 cookie
的原理就是在请求头中获取客户端传输过来的 cookie
,这也是服务端渲染和客户端渲染的一大区别。
到这一步,关于登陆鉴权路由控制的问题已经解决。这里的话,再抛出一个问题,咱们的请求是在服务端发起的,若是发生了错误,html
没法正常填充,咱们应该怎么处理?带着这个问题,进入下一章吧。
咱们的代码在大部分时候都是可控可预测的,通常来讲只有网络请求是很差预测的,因此咱们从下面这段函数来思考如何处理网络请求错误:
// ./pages/vegetables/index.jsx // ... const fetchVegetable = (page, pageSize) => { return fetch("http://dev-api.jt-gmall.com/mall", { method: 'POST', headers: { 'Content-Type': 'application/json' }, // graphql 的查询风格 body: JSON.stringify({ query: `{ vegetableList (page: ${page}, pageSize: ${pageSize}) { page, pageSize, total, items { _id, name, poster, price } } }` }) }).then(res => res.json()); } Vegetables.getInitialProps = async ctx => { const result = await fetchVegetable(1, 10); // 将查询结果返回,绑定在 props 上 return result.data; }
咱们修改一行代码,把请求信息中的 ... vegetableList (page ...
修改为 ... vegetableListError (page ...
来测试一下会发现什么。
修改事后打开页面进行刷新,发现界面变成空白,这是由于 if (!vegetableList) return null;
这行代码致使的,咱们在 getInitialProps
中输出一下请求结果 result
,会发现返回的对象中包含 errors
信息。那么问题就很简单了,咱们只须要把错误信息传递给组件的 props
,而后交由组件处理下一步逻辑就行了,具体实现以下:
const Vegetables = ({ errors, vegetableList }) => { if (errors) return <section>{JSON.stringify(errors)}</section> //... } Vegetables.getInitialProps = async ctx => { const result = await fetchVegetable(1, 10); if (result.errors) { return { errors: result.errors }; } // 将查询结果返回,绑定在 props 上 return result.data; }
到这里你应该就明白了,咱们能够在开发环境将详细错误信息直接呈现,方便咱们调试,而正式环境咱们能够返回一个指定的错误页面,例如 500 服务器开小差
。
到这里就结束了吗?固然没有,这样的话咱们就须要在每一个页面加入这个错误处理,这样的操做很是繁琐并且缺少健壮性,因此咱们须要写一个高阶组件来进行错误的处理,咱们新建文件 ./components/withError.jsx
;
// ./components/withError.jsx import React from "react"; const WithError = () => WrappedComponent => { return class Error extends React.PureComponent { static async getInitialProps(ctx) { const result = WrappedComponent.getInitialProps && (await WrappedComponent.getInitialProps(ctx)); // 这里从业务上来讲与直接返回 result 并没有区别 // 这里只是强调对发生错误时的特殊处理 if (result.errors) { return { errors: result.errors }; } return result.data; } render() { const { errors } = this.props; if (errors && errors.length > 0) return <section>Error: {JSON.stringify(errors)}</section> return <WrappedComponent {...this.props} />; } } } export default WithError;
同时咱们也须要修改 ./pages/vegetables/index.jsx
中的 getInitialProps
函数,将对响应结果的处理延迟到组合类,同时删除以前添加的全部错误处理函数,修改后以下:
const Vegetables = ({ vegetableList }) => { if (!vegetableList) return null; //... } Vegetables.getInitialProps = async ctx => { const result = await fetchVegetable(1, 10); // 将查询结果返回,绑定在 props 上 return result; } export default WithError()(Vegetables);
此时刷新页面,会发现结果和刚才是同样的,只不过咱们只须要在导出组件的时候进行 WithError()(Vegetables)
操做便可。这个函数其实也能够放在根组件,这个就交由你们本身去探究了。
此时此刻,React 服务端渲染入门教程已经结束了,相信你们对服务端渲染也有了更加深入的理解。若是想要了解更多,能够看看 next
的官网教程,并跟着写一个实际的项目最好,这样的提高是最大的。
最后祝愿你们都可以掌握使用服务端渲染,前端技术日益精进!