有时候我们需要在项目添加几个需要SEO的页面,但是项目写完了,不想用next,也不想搞很麻烦,于是就想简单搭一个环境。把ssr页面和spa的分开写,但是项目不分开。
效果截图
SPA的截图


SSR大家都很熟悉,就是服务端渲染,无非就是输出html,但是node不识别jsx、tsx,于是我们需要babel。下面复习下babel。
(下面是babel-register实现,不是webpack实现, 更灵活一点)
项目结构
// 开发项目结构(原来的脚手架添加一个ssr用的目录而已)
SPA项目结构
----dist
----react-ssr/src
--------pages/
------------home/index.tsx
-----------其他页面
----src/
--------main.tsx
--------page/
--------SPA页面1
--------SPA页面2
... 其他
// SSR启动目录(部署项目在这里启动)
SSR项目结构
run.js // 执行babel注册等,然后再执行main.tsx
main.tsx // 业务代码入口
就两个文件就可以了哈。
但是为啥SSR项目里面都没有业务代码呢,因为我们可以通过node把SPA里面的ssr目录copy过来,然后自动执行,还有路由直接根据目录定就好了。
开发流程就是,正常写代码,然后需要ssr的在自定义目录里面新建就好了, 部署的时候正常打包,然后启动nodeJs SSR项目既可。这就是我们比较舒服的开发流程。下面说说怎么实现这个吧。首写node需要识别tsx、识别es2015的引入模块,过滤不需要的img等文件,这个就需要babel实现。
babel
node使用babel转换数据
基本文档
www.babeljs.cn/docs/config…
识别ts、tsx
babeljs.io/docs/en/bab…
babel使用插件
babeljs.io/docs/en/pre…
注册,使node可以连接babel
www.babeljs.cn/docs/babel-…
node识别es2015
transform-es2015-modules-commonjs
使用例子
const fse = require('fs-extra')
const originDir = 'F:/pc/react-ssr/src/'
const tarDir = './src/'
fse.copy(originDir, tarDir, {overwrite: true} , (err) => {
if(err){
console.log(err)
console.log('同步页面失败')
return
}
console.log('同步页面成功')
var option = {
ignore: [
function(filepath) {
return filepath === /.+\.css/.exec(filepath);
},
],
extensions: [".jsx", '.ts', '.tsx'],
cache: true,
}
require('./ignore.js')()
require("@babel/register")(option)
require("./main.tsx")
})
packpage.json添加依赖例子
"babel": {
"presets": [
[
"@babel/preset-typescript",
{
"isTSX": true,
"allExtensions": true
}
],
"@babel/preset-react"
],
"plugins": [
"css-modules-transform",
"transform-es2015-modules-commonjs"
]
}
react
既然可以识别jsx了,那么再转html就完成了一半了,转html我们用renderToString
具体文档:
服务端渲染
reactjs.org/docs/react-…
客户端渲染,使用hydrate更好
reactjs.org/docs/react-…
同构
同构需要做如下内容:文件可以通用、服务端注入数据、客户端渲染绑定事件等处理
文件通用
文件通用, class实现比较简单,直接获取方法。hooks方式实现
function Face2Face({data = ''}) {
const [val, $val] = useState(0)
let [res, $res] = useState(data)
let onClickAdd = () => {
$val(val + 1)
}
useEffect(() => {
Face2Face.init().then(data => {
$res(data)
})
return () => {
console.log('离开')
}
}, [])
return <div>
<div>value: {val}</div>
<div onClick={onClickAdd}>
<Button text="add"></Button>
</div>
<div>{res}</div>
<a href="/#/">离开</a>
</div>
}
Face2Face.init = async (queryMap?: any) => {
const res = await axios('http://baidu.com')
await new Promise((ok) => {
setTimeout(() => {
ok()
}, 1e3)
})
return res.data
}
export default Face2Face
上面代码比较有代表性,先抛出初始化函数,这个可以给服务端使用,服务端执行后继续执行后续,数据同构props传递过去。
数据注入
let data: any
if(Page.init){
data = await Page.init(queryMap)
}
ctx.response.body = html.toString()
.replace('{{APP}}', renderToString(<Page data={data} />))
绑定客户端
说到客户端,这时候需要先清楚代码怎么跑的问题了。
- 项目容易分离,可脱离node可绑定node
- 容易融合项目,之前项目庞大,不容易迁移,那么尊重历史,继续按之前的方式执行,打包的时候打包一份可以给ssr使用的包就可以。
- 方便新项目,上手操作方便,调试方便(既要容易和老项目融合也方便新项目玩)
- 保留灵活性,可跨框架,跨语言(ts、js),体积小,性能好,体验好
这样项目方式就定下来了
根据第二条,那么就注定ssr位置不是在spa项目里面了,因为在SPA里面使用hooks的话,会因为指向的react位置不一样,除非公用一个package, 但是服务端不应该执行那么重的东西。
既然项目不在同一个位置,但是又想文件同步,我们需要先绑定项目,使文件同步。(可使用外部软件实现)
配置webpack打包出SSR服务需要的包
由SPA项目打包,打包后的问题拷贝到当前项目 同样开发也是由SPA项目页面过来
路由通用
路由和文件保持一致即可 服务端代码
let path = ctx.path === '/' ? '/index' : ctx.path
const Page = require(basePath + path).default
完整main.tsx代码
import Koa from 'koa'
import React from 'react'
const app = new Koa()
import fs from 'fs'
import { renderToString } from 'react-dom/server'
import _static from 'koa-static'
const staticPath = 'F:/pc/dist/'
const basePath = './src/pages'
const PORT = 4000
const html = fs.readFileSync(`${staticPath}index.html`)
app.use(async (ctx, next) => {
let queryMap = {}
ctx.querystring.split('&').map(e => {
let arr = e.split('=')
queryMap[arr[0]] = arr[1]
})
try {
// 异常行为
if(ctx.path.includes('../')){
return ctx.response.body = '404'
}
// 静态文件访问
if(ctx.path.split('.').length > 1){
return next()
}
// 获取页面
let path = ctx.path === '/' ? '/index' : ctx.path
const Page = require(basePath + path).default
let data: any
if(Page.init){
data = await Page.init(queryMap)
}
ctx.set('Server', 'xiexiuyue-react-ssr')
ctx.set('Content-Type', 'text/html; charset=utf-8')
ctx.response.body = html.toString()
.replace('<div id=root></div>', `<div id="root">
<!-- {{inner}} -->
${renderToString(<Page data={data} />)}
</div>`)
} catch (error) {
console.log('onload error')
console.log(error)
ctx.response.body = '404'
}
})
app.use(_static(staticPath))
app.listen(PORT)
console.log(`http://localhost:${PORT}`)
参考博文
www.ruanyifeng.com/blog/2016/0…
libin1991.github.io/2019/12/04/…
juejin.cn/post/684490…