前言:
掘金用vue,知乎、简书用react实现ssr,淘宝网用kissy,京东用nerv,对于seo需求很大的项目,ssr是必须的。
ssr流程
难点
1、开发环境的搭建,保证本地也能支持ssr功能
2、server端路由和client端一致
3、全局状态同步(context)
4、样式注入的处理
5、如何刷新页面时nodejs调用接口,页面加载之后不请求接口
对应https://github.com/fridaydream/blog-site
中的step_01到step_05分支
技术栈
react、mobx、mui、koa、ejs
搭建步骤
step_01: 搭建基础服务
前端入口文件代码(index.tsx):
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
const root = document.getElementById('root')
const renderMethod = module.hot ? ReactDOM.render : ReactDOM.hydrate
const render = (Component: React.ComponentType) => {
renderMethod(<Component />, root)
}
render(App)
给koa用的react入口文件代码(server-entry.tsx):
import React from 'react'
import App from './App'
export default <App />
server就是把组件入口暴露出去,通过webpack打包成nodejs使用的代码
开发环境:webpack-dev-server
启动csr的8888端口,koa启动server的3333端口,通过webpack处理react项目的入口文件server-entry.tsx
,打包libraryTarget设置为commonjs2,监听文件变化,生成nodejs的bundle文件。
生产环境:将react代码打包到dist文件夹中,同时把server-entry.tsx
打包成nodejs模块,就可以在nodejs中require
模块,运行时生成静态html吐给浏览器
step_02: server端路由和client端一致
如技术栈中,浏览器路由browerRouter,koa路由staticRouter,这样就可以在同构中复用一套代码
server-entry.tsx改成
import React from 'react'
import { StaticRouterContext } from 'react-router'
import { StaticRouter } from 'react-router-dom'
import App from './pages/App'
export default (routerContext: StaticRouterContext, url: string) => (
<StaticRouter context={routerContext} location={url}>
<App />
</StaticRouter>
)
import ReactDomServer from 'react-dom/server'
const routerContext: RouterContext = {}
const app = createApp(routerContext, ctx.url)
const appString = ReactDomServer.renderToString(app)
koa中通过ctx.url
拿到刷新页面的路由,传入server-entry.tsx
函数中,react-dom中提供的renderToString方法得到静态页面内容
step_03: 全局状态同步(context)
mobx的状态管理中
import { observable, action, toJS } from 'mobx'
export default class ThemeStore {
constructor({ theme } = { theme: 'light' }) {
this.theme = theme;
}
@observable theme
@action
setTheme(newTheme: string) {
this.theme = newTheme
}
toJson() {
return {
theme: this.theme
}
}
}
在koa通过调用ThemeStore
类上面的toJson方法,拿到所有初始化状态值(redux中也有类似方法),通过ejs注入window.__INITIAL__STATE__=<%%- initialState %>
,浏览器得到静态html之后,执行自己的前端代码,拿到window.__INITIAL__STATE__同步到浏览器中的mobx store中,这样就有了注水和脱水的过程,如果状态不一致,会出现页面闪动(体验不好,有可能就是bug了)。
step_04: 样式注入的处理
server-entry.tsx改成
import React from 'react'
import { StaticRouterContext } from 'react-router'
import { StaticRouter } from 'react-router-dom'
import { useStaticRendering } from 'mobx-react-lite'
import { GenerateId, Jss } from 'jss';
import { ThemeProvider, StylesProvider } from '@material-ui/core/styles';
import App from '@/pages/App'
import { storesContext } from "@/store/store";
import { IStores } from "@/store/types";
import { theme } from './theme'
// 让mobx在服务端渲染的时候不会重复数据变换
useStaticRendering(true) // 使用静态的渲染
export default (stores: IStores, sheetsRegistry: {}, sheetsManager: {}, jss: Jss, generateClassName: GenerateId, routerContext: StaticRouterContext, url: string) => (
<storesContext.Provider value={stores}>
<StaticRouter context={routerContext} location={url}>
<StylesProvider sheetsRegistry={sheetsRegistry} jss={jss} generateClassName={generateClassName} sheetsManager={sheetsManager}>
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
</StylesProvider>
</StaticRouter>
</storesContext.Provider>
)
nodejs中server-render.ts
import { create, SheetsRegistry } from 'jss';
const sheetsRegistry = new SheetsRegistry();
const app = createApp(stores, sheetsRegistry, sheetsManager, jss, generateClassName, routerContext, ctx.url)
@material-ui/core/styles
中提供了ThemeProvider
处理主题和StylesProvider
处理样式,执行createApp
之后,sheetsRegistry.toString()
就得到静态的css样式了,注入到style
标签中
step_05: 如何刷新页面时nodejs调用接口,页面加载之后不请求接口
如ssr流程图中,刷新页面,浏览器对服务器发起get请求,服务器根据path查找到具体组件,调用组件的getInitialProps
方法,数据注入组件,返回静态html字符串,用户看到界面,这时候用户是不能操作界面的,之后浏览器执行代码,同步store,给元素上面绑定事件,生成新样式,删除之前注入的样式,这个时候用户才能操作界面,之后的跳转都是走前端路由
koa拿到对应的组件执行上面的getInitialProps
方法,调用接口,浏览器执行时判断window.location.pathname === window.__SSRPATH__
是否相等,相等不执行getInitialProps
方法,之后页面前端跳转新路由时,需要执行getInitialProps
方法
const getComponent = (Routes: RouteItem[], path: string) => {
// 根据请求的path来匹配到对应的component
const activeRoute = Routes.find(route => matchPath(path, route)) || { Component: () => NotFound } // 找不到对应的组件时返回NotFound组件
const activeComponent = activeRoute.Component
return activeComponent
}
// 服务端渲染 根据ctx.path获取请求的具体组件,调用getInitialProps并渲染
const ActiveComponent = getComponent(Routes, ctx.path)()
ActiveComponent.getInitialProps ? await ActiveComponent.getInitialProps(ctx) : {}
export function requestInitialData(props: InitialStoresProps & QueryProps, component: ComponentType) {
useEffect(() => {
// 客户端运行时
if (typeof window !== 'undefined') {
console.log('window.location.pathname !== window.__SSRPATH__', window.location.pathname, window.__SSRPATH__)
// 非同构时,并且getInitialProps存在
if (window.location.pathname !== window.__SSRPATH__ && component.getInitialProps) {
console.log('post===')
component.getInitialProps(props)
}
}
}, [1]);
}
当然还有接口参数的获取,koa中参数从url中来,浏览器中接口参数从mobx中来
小总结
别人写的,特别好。
疑问
module.hot的概念 热更新还是热重载
- 热重载live reload: 就是当修改文件之后,webpack自动编译,然后浏览器自动刷新->等价于页面window.location.reload()
- 热更新HMR: 热重载live reload并不能够保存应用的状态(states),当刷新页面后,应用之前状态丢失。举个列子:页面中点击按钮出现弹窗,当浏览器刷新后,弹窗也随即消失,要恢复到之前状态,还需再次点击按钮。而webapck热更新HMR则不会刷新浏览器,而是运行时对模块进行热替换,保证了应用状态不会丢失,提升了开发效率
预渲染和服务端渲染的区别
服务端渲染,首先得有后端服务器(一般是 Node.js)才可以使用,如果我没有后端服务器,也想用在上面提到的两个场景,那么推荐使用预渲染。
预渲染与服务端渲染唯一的不同点在于渲染时机,服务端渲染的时机是在用户访问时执行渲染(即实时渲染,数据一般是最新的),预渲染的时机是在项目构建时,当用户访问时,数据不是一定是最新的(如果数据没有实时性,则可以直接考虑预渲染)。
预渲染(Pre Render)在构建时执行渲染,将渲染后的 HTML 片段生成静态 HTML 文件。无需使用 web 服务器实时动态编译 HTML,适用于静态站点生成。
服务端渲染css是否按需加载
通过2个不同路由下,搜索样式,发现A页面中样式在B页面中不存在,说明css是按需加载的
ssr页面有异步请求时,如何控制客户端不再次发送,next中的处理方式
next在server注水时给window.isServer=true
,如果isServer===true
,浏览器中不执行getInitialProps
方法,前端跳转路由时,将window.isServer设置成false
canvas在ssr中处理方式
或者通过部分ssr方式
服务端与页面状态不一致的情况下,页面会整体重新渲染还是局部渲染
页面没有重新渲染,不一致时,后面的都是执行一次客户端代码,通过在组件中和兄弟组件中console.log
只执行了一次
加入css,浏览器渲染时需要先删除服务端渲染的css样式,删除后重新加入样式是否会重绘
通过观察浏览器performance
,通过服务端样式和客户端样式一样,重排时间和重绘时间几乎可以忽略不计。大页面没有尝试过
加入css,浏览器渲染时需要先删除服务端渲染的css样式,删除后重新加入样式是否会重绘
不是css in js 的css库怎么处理样式注入
NextJS 内置了styled-jsx
方案
antd等组件库可以使用styled-components
方案
公共接口怎么处理(常量、用户信息)
没有好的解决方案
部署serverless
now.sh是 ZEIT 推出的一款支持 Docker、Nodejs、静态页面的全球化实时部署服务(Realtime Global Deployments)
可以通过github部署和now终端命令部署
在根目录下面创建now.json
{
"version": 2,
"builds": [
{
"src": "server/server.ts",
"use": "@now/node"
},
{
"src": "server/list.ts",
"use": "@now/node"
}
],
"routes": [
{
"src": "/api/video/info",
"dest": "server/list.ts"
},
{
"src": "/(.*)",
"dest": "server/server.ts"
}
]
}
server.ts添加module.exports = app.callback()
,迁移nodejs项目很方便,就是报错信息不好找。
作者:fridaydream
链接:https://juejin.im/post/6893880106100457480
看完两件小事
如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:
- 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
- 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程
本文著作权归作者所有,如若转载,请注明出处
转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com