先附上github地址github.com/hzfvictory/…,方便你们更容易理解,否则后面不少地方会一头雾水。javascript
欢迎你们点star,提issue,一块儿进步!😄php
CSR:css
页面渲染是JS负责进行的
复制代码
浏览器发送请求–>服务器返回HTML–>浏览器发送bundle.js请求–>服务器返回bundle.js–>浏览器执行bundle.js中的react代码完成渲染html
SSR:前端
服务器端直接返回HTML让浏览器直接渲染
复制代码
浏览器发送请求–>服务器运行React代码生成页面–>服务器返回页面vue
传统CSR的弊端:java
因为页面显示过程要进行JS文件拉取和React代码执行,在这个渲染过程当中至少涉及到两个 HTTP请求周期(html+js),因此会有必定的耗时,首屏加载时间会比较慢。node
对于SEO(Search Engine Optimazition,即搜索引擎优化),彻底无能为力,由于搜索引擎爬虫只认识html结构的内容,而不能识别JS代码内容。react
SSR的弊端:webpack
ssr的出现,就是为了解决这些传统CSR的弊端
复制代码
在 React 中使用 ssr 技术,咱们让 React 代码在服务器端先执行一次,使得用户下载的 HTML 已经包含了全部的页面展现内容,这样,页面展现的过程只须要经历一个 HTTP 请求周期,TTFP(Time To First Page) 时间获得一倍以上的缩减
可是使用 ssr 这种技术,将使本来简单的 React 项目变得很是复杂
相对于仅仅须要提供静态文件的服务器,ssr中使用的渲染程序天然会占用更多的CPU和内存资源
在服务器生成的页面因此,一些经常使用的浏览器API可能没法正常使用,好比window、docment和alert等,若是使用的话须要对运行的环境加以判断
开发调试会有一些麻烦,由于涉及了浏览器及服务器,对于SPA的一些组件的生命周期的管理会变得复杂
可能会因为某些因素致使服务器端渲染的结果与浏览器端的结果不一致,项目的可维护性会下降,代码问题的追溯也会变得困难
因此,使用 ssr 在解决问题的同时,也会带来很是多的反作用,有的时候,这些反作用的伤害比起 ssr 技术带来的优点要大的多。通常建议ssr,除非你的项目特别依赖搜索引擎流量,或者对首屏时间有特殊的要求,不然不建议使用 ssr,若是只对seo有要求可以使用 prerender预渲染。
这里介绍的是ssr,是基于React 的SPA项目,不是像 thinkphp、jsp、nodeJs+ejs 这种纯后端直出渲染方式,因此这种大多数只是针对首屏的ssr,由于浏览器的路由跳转方式是用的H5的history API
的window.history.pushState()
,使得咱们便可以修改 url
也能够不刷新页面,因此是不会走服务端的【能够经过预加载获取须要的数据】。
ssr 之因此可以实现,本质上是由于虚拟 DOM 的存在
ssr 的工程中,React 代码会在客户端和服务器端各执行一次,由于代码在 Node 环境下是没有DOM这个概念的,因此在React 框架中引入了一个概念叫作虚拟 DOM,React 在作页面操做时,实际上不是直接操做 DOM,而是操做虚拟 DOM,也就是操做普通的 JavaScript 对象,这就使得 ssr 成为了可能。在服务器,我能够操做 JavaScript 对象,判断环境是服务器环境,咱们把虚拟 DOM 映射成字符串输出;在客户端,我也能够操做 JavaScript 对象,判断环境是客户端环境,我就直接将虚拟 DOM 映射成真实 DOM,完成页面挂载。
const Koa = require('koa');
const app = new Koa();
app.use((ctx) => {
if (ctx.path === '/') {
ctx.body =
` <html> <head> <title>禾口和ssr</title> </head> <body> <h1>hello</h1> <h2>world</h2> </body> </html> `;
}
})
const server = app.listen('9999', () => {
const {port} = server.address();
console.log(`http://localhost:${port}`)
})
复制代码
const Koa = require('koa');
const app = new Koa();
const route = require("koa-router")() // 这里也可使用构造函数
route.get("/", (ctx) => {
ctx.body =
` <html> <head> <title>禾口和ssr</title> </head> <body> <h1>hello</h1> <h2>world</h2> </body> </html> `
})
app.use(route.routes());
app.use(route.allowedMethods()); //自动设置响应头ctx.status完善response响应头
const server = app.listen('9999', () => {
const {port} = server.address();
console.log(`http://localhost:${port}`)
})
复制代码
这样一个简单的服务端渲染就搞定了,服务器端直接返回HTML让浏览器直接渲染,并且网页源代码中是有这些dom信息的对seo很是友好,咱们react、vue这些都是经过webpack引入了js,全部的功能页面展现通通由js完成。
到这一步已经不能直接用node启动服务了,由于没有babel
, React不会转化成createElement
的形式,并且使用node也不能直接使用import导入方式。
随便编写一个React的组件
// src/pages/home
import React from 'react';
const Home = () => {
return (
<div> <div>Home组件</div> </div>
)
}
export default Home
复制代码
而后咱们把当前组件,使用服务员渲染出来,看下面配置:
// config/webpack.base.js
const path = require('path')
module.exports = {
module: {
rules: [{
test: /\.js|jsx$/,
loader: 'babel-loader',
exclude: /node_modules/,
options: {
presets: ['@babel/preset-react', ['@babel/preset-env', {
targets: {
browsers: ['last 2 versions']
}
}]]
}
}]
}
}
复制代码
服务端运行的代码若是须要依赖 Node 核心模块或者第三方模块,就再也不须要把客户端的一些模块代码打包到最终代码中了
。由于环境已经安装这些依赖,能够直接引用。这样一来,就须要咱们在 webpack 中配置:target:node
,并借助 webpack-node-externals 插件,解决第三方依赖打包的问题。
// config/webpack.server.js
const path = require('path')
const nodeExternals = require('webpack-node-externals')
const merge = require('webpack-merge')
const config = require('./webpack.base')
const serverConfig = {
target: 'node', // 编译出能让node识别的代码 https://webpack.docschina.org/concepts/targets/
mode: 'development', // 这里的mode要特别注意
entry: './src/server/index.js', // 对应服务端的代码
// https://webpack.docschina.org/configuration/externals/
externals: [nodeExternals()], // 为了忽略node_modules文件夹中的全部模块
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, '../bundle')
}
}
module.exports = merge(config, serverConfig)
复制代码
target: 'node' 和 target: 'web' 的大体区别
// target: 'node'
exports.ids = [0];
exports.modules = {};
// target: 'web'
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
}])
复制代码
// server/index.js
import Koa from 'koa';
import Router from "koa-router"
import React from "react"; // 必须引入
import {renderToString} from 'react-dom/server'; // react-dom提供的方法
import Home from "../src/pages/home"
const app = new Koa();
const route = new Router()
const content = renderToString(<Home/>);
route.get("/", (ctx) => {
ctx.body =
` <html> <head> <title>禾口和ssr</title> </head> <body> <div id="root">${content}</div> </body> </html> `
})
app.use(route.routes());
app.use(route.allowedMethods());
const server = app.listen('9999', () => {
const {port} = server.address();
console.log(`http://localhost:${port}`)
})
复制代码
以上使用了renderToString, 咱们都知道react-dom提供了四种服务端渲染函数,以下:
对于服务端渲染而言
componentDidMout
绑定浏览器事件等 这些在服务端没完成也不可能执行任务。data-react-id
的纯 html 在前端 react 加载完成后, 以前服务端渲染的页面会抹掉以前服务端的从新渲染(可能页面会闪一下). 换句话说 前端react就根本就不认识以前服务端渲染的内容, render 方法会使用 innerHTML 的方法重写 #react-target 里的内容在package添加启动配置
// package.json
"scripts": {
"dev": "npm-run-all --parallel dev:build:server dev:start",
"dev:build:server": "webpack --config config/webpack.server.js --watch",
"dev:start": "nodemon ./bundle/bundle.js"
}
复制代码
执行 yarn dev ,打开 http://localhost:9999/ 页面直接在浏览上显示, 到此,就初步实现了一个React组件是服务端渲染,加入你在组件Home里面添加一些方法或者调取接口,你会发现这些都没有执行,因此咱们还须要接下来进一步完善。
要解决上面上面的问题,就须要同构了,所谓同构,通俗的讲,就是一套React代码在服务器上运行一遍,到浏览器渲染时在运行一遍,服务端渲染完成页面结构,浏览器端渲染完成事件绑定接口调取(重复加载的js或者css客户端协调阶段时候会进行比对,若是同样则不渲染了)。
把打包后的js,注入到html中,这样到浏览器就会再次请求,就能够完成事件绑定等行为操做。
咱们要用到react-dom的hydrate
// client/index.js
import React, {Component} from "react"
import ReactDom from 'react-dom';
import {BrowserRouter as Router, Switch} from 'react-router-dom';
import {renderRoutes} from 'react-router-config';
import Loadable from 'react-loadable'; // 这里是个人一个路由拆分,大家能够不用
import routes from '../router';
class App extends Component {
render() {
return (
<Router> <Switch> {renderRoutes(routes.routes)} </Switch> </Router>
)
}
}
Loadable.preloadReady().then(() => {
ReactDom.hydrate(
<App/>, document.getElementById('root'));
})
复制代码
hydrate() 描述的是 ReactDOM 复用 ReactDOMServer 服务端渲染的内容时尽量保留结构,并补充事件绑定等 Client 特有内容的过程
说白了render()
标签上没有惟一的属性,可是要尽量复用 ssr 的 HTML 结构,因此就出现了hydrate()
,可是目前二者都是能够用的,17版本render()
就不在支持ssr
而后配置客户端的webpack将其编译打包成js,在服务端html里面引入。
客户端和服务端打包后的输出目录
// config/outputPath
module.exports = {
OUTPUTCLIENT: 'static',
OUTPUTSERVER: 'bundle'
}
复制代码
// config/webpack.client.js
const path = require('path')
const merge = require('webpack-merge')
const config = require('./webpack.base')
const {OUTPUTCLIENT} = require("./outputPath")
const outputPath = `../${OUTPUTCLIENT}`
const clientConfig = {
mode: 'development',
entry: path.resolve(__dirname, '../client/index.js'),
output: {
filename: 'index.[chunkhash:8].js', // 这里我用的hash,目的是防止缓存
path: path.resolve(__dirname, outputPath),
publicPath: '/'
},
module: {
rules: [
{
test: /\.css?$/,
use: ['style-loader', {// 这里建议使用style-loader,少许的css直接采用客户端渲染了
loader: 'css-loader',
options: {
modules: true, // 这要跟服务端保持一致,否则head里面有样式,客户端没有对应的class
}
}]
},
{
test: /\.(png|jpeg|jpg|gif|svg)?$/,
loader: 'url-loader',
options: {
limit: 8000,
outputPath: outputPath, // 输入路径
publicPath: '/'
}
}
]
}
}
module.exports = merge(config, clientConfig)
复制代码
而后在上面的package.json
,里面添加 "dev:build:client": "webpack --config webpack.client.js --watch"
,就能对浏览器用到的一些js完成打包。
服务器端路由代码相对要复杂一点,须要你把 location
(当前请求路径)传递给 StaticRouter
组件,这样 StaticRouter
才能根据路径分析出当前所须要的组件是谁。(PS:StaticRouter
是 React-Router
针对服务器端渲染专门提供的一个路由组件。)
// server/index.js
import Koa from 'koa';
import React from "react";
import Router from "koa-router"
import {renderToString} from 'react-dom/server';
import {StaticRouter} from 'react-router-dom';
import Loadable from 'react-loadable';
import routes from '@/router';
import {renderRoutes, matchRoutes} from "react-router-config";
import {renderHTML} from "./tem"
const app = new Koa();
const route = new Router()
route.get(["/:route?", /\/([\w|\d]+)\/.*/], (ctx) => {
const content = renderToString(
// 重点是这
<StaticRouter location={ctx.path}> {renderRoutes(routes.routes)} </StaticRouter>
);
ctx.body = renderHTML(content, {})
})
// 这里要注意下中间件的前后顺序
app.use(require('koa-static')(process.cwd() + '/static'));
app.use(route.routes());
app.use(route.allowedMethods());
Loadable.preloadAll().then(() => {
const server = app.listen('9999', () => {
const {port} = server.address();
console.log(`\x1B[33m\x1B[4mhttp://localhost:${port}\x1B[0m`)
})
});
复制代码
// server/tem.js
const glob = require('glob');
let project = glob.sync(process.cwd() + '/static/index.*.js');
let path = project[0].split('/')
export const renderHTML = (content, store) => ` <!DOCTYPE html> <html lang="zh"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" /> <meta name="theme-color" content="#000000"> <title>禾口和ssr</title> </head> <body> <div id="root">${content}</div> <script src=/${path[path.length - 1]}></script> // 这个 '/' 必定要添加,坑了很久 </body> </html> `
复制代码
正常的服务端渲染只是返回了 HTML 字符串,样式须要浏览器加载完 CSS 后才会加上,这个样式添加的过程就会形成页面的闪动
,因此在服务端里面直接添加须要引用的CSS。
咱们不能再使用 style-loader 了,由于这个 webpack loader 会在编译时将样式模块载入到 HTML header 中。可是在服务端渲染环境下,没有 window 对象,style-loader 进而会报错。通常咱们换用 isomorphic-style-loader
,同时 isomorphic-style-loader
也会解决页面样式闪动的问题,它的原理也不难理解:isomorphic-style-loader
利用 context API,在渲染页面组件时获取全部 React 组件的样式信息,在服务器端输出 html 字符串的同时,也将样式插入到 html 字符串当中,将结果一同传送到客户端。
由于咱们已经开启了cssmodules,因此直接导入到head里面是不会存在样式冲突的问题。isomorphic-style-loader
已经给咱们提供了一些导入css 的 高阶函数 withsSyles
和 hooks useStyles
,用的时候比较省事。
看下代码配置
// config/webpack.client.js
{
test: [/\.css|less$/],
use: [
'style-loader', // 也能够跟服务端的同样,就是麻烦点,每次使用css,都须要额外的手续
{
loader: 'css-loader',
options: {
modules: true,
}
},
'less-loader',
]
}
复制代码
// config/webpack.server.js
{
test: [/\.css|less$/],
use: [
'isomorphic-style-loader',
{
loader: 'css-loader',
options: {
modules: true,
}
},
'less-loader' // 必须配置 否则会当成css,视觉可能看不出来,由于客户端配置了less
]
}
复制代码
服务端首页
// server/index.js
// ...
const css = new Set() // 这个必须在路由函数里面,在外面的话,就会累加出现以前的css
const insertCss = (...styles) => styles.forEach(style => css.add(style._getCss()));
const content = renderToString(
<StaticRouter location={ctx.path}> <StyleContext.Provider value={{insertCss}}> {renderRoutes(routes.routes)} </StyleContext.Provider> </StaticRouter>
)
ctx.body = renderHTML(content, {}, css)
// ....
复制代码
客户端也须要配置
// client/index.js
import React from "react"
import ReactDom from 'react-dom';
import {BrowserRouter as Router} from 'react-router-dom';
import {renderRoutes} from 'react-router-config';
import Loadable from 'react-loadable';
import StyleContext from 'isomorphic-style-loader/StyleContext'
import routes from '../router';
const insertCss = (...styles) => {
const removeCss = styles.map(style => style._insertCss && style._insertCss());
return () => removeCss.forEach(dispose => dispose && dispose())
}
const App = () => {
return (
<Router> {renderRoutes(routes.routes)} </Router>
)
};
Loadable.preloadReady().then(() => {
ReactDom.hydrate(
<StyleContext.Provider value={{insertCss}}> <App/> </StyleContext.Provider>, document.getElementById('root'));
})
复制代码
这样服务端和客户端均可以直接使用isomorphic-style-loader
的一些API, 可是有些不重要
的页面,或者不重要的css能够直接采用客户端渲染由于客户端用的是style-loader
,就不须要引入高阶函数或者useStyles。
在页面内具体使用
// 函数式组件
import useStyles from 'isomorphic-style-loader/useStyles'
import styles from "./index.css"
const Index = (props) => {
useStyles(styles)
}
复制代码
// 类组件使用
import withStyles from 'isomorphic-style-loader/withStyles'
import styles from "./index.css"
@withStyles(styles) // 须要在webpack.base.js里面额外配置
class Index extends React.Component {}
复制代码
// 使用客户端渲染
import styles from "./index.css"
const Index = () => {
// 这里也可使用useStyles 部分使用客户端渲染
return (
<div> <h1 className={styles['title-center']}>message</h1> <h1 className={'title-center'}>message</h1> </div>
)
}
复制代码
而后打开网页的源代码就能够看见head里面已经有咱们须要的css了。
以前项目一直用的dva,这里直接使用的dva-core代替的redux,不会配置的自行查下文档。
建立 Store
:这一部分有坑,要注意避免,你们知道,在客户端渲染中,用户的浏览器中永远只存在一个 Store
,因此代码上你能够这么写
const dvaApp = createApp({
initialState: {},
models: models,
});
const store = dvaApp.getStore();
export default store;
复制代码
然而在服务器端,这么写就有问题了,由于服务器端的 Store
是全部用户都要用的,若是像上面这样构建 Store
,Store
变成了一个单例,全部用户共享 Store
,显然就有问题了。因此在服务器端渲染中,Store
的建立应该i像下面这样,返回一个函数,每一个用户访问的时候,这个函数从新执行,为每一个用户提供一个独立的 Store
const dvaApp = createApp({
initialState: {},
models: models,
});
export const getStore = () => {
return dvaApp.getStore();
}
复制代码
别慌,你若是这样的作的话,redux的数据仍是全部客户同步公用,由于你的model是一个对象,是静态导入,这个时候你应该把model写成函数的形式,这样后台每次都能获取到最新的数据
const menuTree = () => {
return {
namespace: 'menuTree',
state: {
routes: []
},
effects: {
* reset(payload, {call, put, select, update}) {
// .........
},
},
reducers: {
save(state, {payload}) {
return {...state, ...payload};
},
},
}
};
export default menuTree
复制代码
换成函数的导出形式就OK了,而后在createApp的方法,原有的models.forEach(model => app.model(model);
转换成 models.forEach(model => app.model(model()));
就OK了。
数据获取的解决方案是配置路由 route-router-config,结合 matchRoutes
,找到页面上相关组件所需的请求接口的方法并执行请求,这就要求开发者经过路由配置信息,显式地告知服务端请求内容。
客户端路由改造
// router/index.js
{
path: '/login',
exact: true,
component: Login,
loadData: Login.loadData, // 这里就是请求数据的方法
title: '登陆页'
}
复制代码
// 客户端组件使用
class Index extends Component {}
Index.loadData = async (store) => {
store.dispatch({
type: "menuTree/reset",
});
console.log('我试试这个到底加载不');
}
export default Index
复制代码
服务端代码
// server/index.js
// 获取请求的方法
const promises = [];
matchedRoutes.forEach(item => {
if (item.route.loadData) {
const promise = new Promise((resolve, reject) => {
// 这里用了.then 因此组件里面必须使用async或者promise
item.route.loadData(store).then(resolve).catch(reject)
})
promises.push(promise);
}
});
// 这里要注意的一个问题,你的方法多是异步的,会出现 ctx.body 没有执行的问题,因此要把这个中间件设置为异步的
// 为了确保组件的loadData的方法执行完毕
await Promise.all(promises).then(() => {
const css = new Set(); // 防止钩子函数执行两次
const insertCss = (...styles) => styles.forEach(style => css.add(style._getCss()));
const helmet = Helmet.renderStatic();
const content = renderToString(
<Provider store={store}> <StaticRouter location={ctx.path}> <StyleContext.Provider value={{insertCss}}> {renderRoutes(routes.routes)} </StyleContext.Provider> </StaticRouter> </Provider>
)
ctx.body = renderHTML(content, store, css, helmet)
})
复制代码
涉及到数据的预获取,也是服务端渲染的真正意义。
上面的代码正常运行是没问题了,可是发现客户端和服务端的store,存在不一样步的问题。
其实也很好理解。当服务端拿到store并获取数据后,客户端的js代码又执行一遍,在客户端代码执行的时候又建立了一个空的store,两个store的数据不能同步。
因此 在服务器端渲染时,首先服务端请求接口拿到数据,并处理准备好数据状态(若是使用 Redux,就是进行 store 的更新),为了减小客户端的请求,咱们须要保留住这个状态。通常作法是在服务器端返回 HTML 字符串的时候,将数据 JSON.stringify 一并返回,这个过程,叫作注水;在客户端,就再也不须要进行数据的请求了,能够直接使用服务端下发下来的数据,这个过程叫脱水。
<script>
window.context = {
// 这里是注水
state: ${serialize(store.getState())} // serialize 是为了防止xss的攻击
}
</script>
复制代码
import {create} from 'dva-core';
function createApp(opt) {
// .....
return app;
}
// 服务端的redux
const dvaApp = createApp({
initialState: {},
models: models,
});
export const getStore = () => {
return dvaApp.getStore();
}
// 客户端的redux
export const getClientStore = () => {
// 须要先拿到服务端的数据, 脱水
const initialState = window.context ? window.context.state : {};
const dvaClientApp = createApp({
initialState,
models: models,
});
return dvaClientApp.getStore();
}
复制代码
服务端是没有域的存在,因此不会存在跨域的问题,可是在客户端调取接口还存在跨域的问题,因此还须要配置下代理,代码以下:
import httpProxy from 'http-proxy-middleware';
import k2c from "koa2-connect"
// 转发代理
app.use(async (ctx, next) => {
if (ctx.url.startsWith('/api')) { //匹配有api字段的请求url
ctx.respond = false // 绕过koa内置对象response ,写入原始res对象,而不是koa处理过的response
await k2c(httpProxy({
target: 'https://api.xxxxx.xxx',
changeOrigin: true,
secure: false,
pathRewrite: {
'^/api': ''
}
}
))(ctx, next);
}
await next()
})
复制代码
还能够安装koa的代理模块 koa2-proxy-middleware
,用法以下:
const proxy = require('koa2-proxy-middleware');
const options = {
targets: {
'/user': {
// this is option of http-proxy-middleware
target: 'http://localhost:3001', // target host
changeOrigin: true, // needed for virtual hosted sites
},
'/user/:id': {
target: 'http://localhost:3001',
changeOrigin: true,
},
'/api/*': {
target: 'http://localhost:3001',
changeOrigin: true,
pathRewrite: {
'/passager/xx': '/mPassenger/ee', // rewrite path
}
},
}
}
app.use(proxy(options));
复制代码
源码也没几行,有兴趣能够看下 koa2-proxy-middleware
作更完整的SEO
App 组件嵌入到 document.getElementById('root')
节点当中,通常是不包含 head 标签的,可是单页应用在切换路由时,可能也会须要动态修改 head 标签信息,好比 title 内容。也就是说:在单页面应用切换页面,不会通过服务端渲染,可是咱们仍然须要更改 document 的 title 内容。
若是直接改客户端的title,直接就可使用document.title
,可是咱们如今要把SEO作好,而后咱们要更改服务端head里面的meta title等内容,这里咱们要用到 react-helmet。
具体代码很是简单
// 客户端实现方式
import React, {Component, Fragment} from "react"
import {Helmet} from "react-helmet";
class Index extends Component {
render() {
return (
<Fragment> <Helmet> <title>这是login页</title> <meta name="description" content="这里是禾口和react-ssr的调研"/> </Helmet> </Fragment>
)
}
}
复制代码
// 服务端实现
import Koa from 'koa';
import React from "react";
import Router from "koa-router"
import {renderToString} from 'react-dom/server';
import {StaticRouter} from 'react-router-dom';
import {Helmet} from 'react-helmet'; // 这里引入
// ....
const app = new Koa();
const route = new Router()
route.get(["/:route?", /\/([\w|\d]+)\/.*/], (ctx) => {
// ....
const helmet = Helmet.renderStatic(); // 这里获取下当前的head信息
const content = renderToString(
<StaticRouter location={ctx.path}> <StyleContext.Provider value={{insertCss}}> {renderRoutes(routes.routes)} </StyleContext.Provider> </StaticRouter>
)
ctx.body = ` <!DOCTYPE html> <html lang="zh-Hans-CN"> <head> <meta charset="utf-8"> ${helmet.title.toString()} ${helmet.meta.toString()} <link rel="shortcut icon" href="/favicon.ico"> <style>${[...css].join('')}</style> </head> <body> <div id="root">${content}</div> <script src=/index.js></script> </body> </html> `
})
// ... ...
复制代码
客户端登陆的时候,把登陆的token,放到浏览器的cookie中而且存到redux一份,cookie在服务端能够经过请求的页面直接获取到;因此当用户刷新页面的时候,能够经过页面请求获取到token,而后向redux里面存放一份,这样客户端想要获取token就能够直接在redux里面拿了,loadDate函数能够经过第二个参数传进获取。
用react-router-config
的matchRoutes
方法,当捕获为空数组的时候,说明没有当前路由,跳转到404 页面,这里面有一个注意的点是,如说有二级或二级以上的路由,这个方法能捕获第一个路由的方法,因此要判断当前获取到的是否是一级路由,并且当前数据还不能为空。
// server/index.js
// 判断404
let hasRoute = matchedRoutes.length === 1 && !!matchedRoutes[0].route.routes
if (hasRoute || !matchedRoutes.length) {
ctx.response.redirect('/404');
return;
}
// 添加 ‘/’ 重定向是同样的套路
复制代码
安全问题很是关键,尤为是涉及到服务端渲染,开发者要格外当心。这里提出一个点:咱们前面提到了注水和脱水过程,其中的代码:
<script>
window.context = {
initialState: ${JSON.stringify(store.getState())}
}
</script>
复制代码
很是容易遭受 XSS 攻击,JSON.stringify 可能会形成 script 注入,使用 serialize-javascript 库进行处理,这也是同构应用中最容易被忽视的细节。
另外一个规避这种 XSS 风险的作法是:将数据传递个页面中一个隐藏的 textarea 的 value 中,textarea 的 value 天然就不怕 XSS 风险了。
上面的问题,均已解决,可能文章介绍的不具体,具体以源码为准。
喜欢的mark👍