有时候咱们须要在项目添加几个须要SEO的页面,可是项目写完了,不想用next,也不想搞很麻烦,因而就想简单搭一个环境。把ssr页面和spa的分开写,可是项目不分开。css
效果截图html
SPA的截图 node
SSR你们都很熟悉,就是服务端渲染,无非就是输出html,可是node不识别jsx、tsx,因而咱们须要babel。下面复习下babel。react
(下面是babel-register实现,不是webpack实现, 更灵活一点)webpack
项目结构ios
// 开发项目结构(原来的脚手架添加一个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 // 业务代码入口
复制代码
就两个文件就能够了哈。git
可是为啥SSR项目里面都没有业务代码呢,由于咱们能够经过node把SPA里面的ssr目录copy过来,而后自动执行,还有路由直接根据目录定就行了。github
开发流程就是,正常写代码,而后须要ssr的在自定义目录里面新建就行了, 部署的时候正常打包,而后启动nodeJs SSR项目既可。这就是咱们比较舒服的开发流程。下面说说怎么实现这个吧。首写node须要识别tsx、识别es2015的引入模块,过滤不须要的img等文件,这个就须要babel实现。web
基本文档
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-…typescript
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"
]
}
复制代码
既然能够识别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} />))
复制代码
说到客户端,这时候须要先清楚代码怎么跑的问题了。
这样项目方式就定下来了
根据第二条,那么就注定ssr位置不是在spa项目里面了,由于在SPA里面使用hooks的话,会由于指向的react位置不同,除非公用一个package, 可是服务端不该该执行那么重的东西。
既然项目不在同一个位置,可是又想文件同步,咱们须要先绑定项目,使文件同步。(可以使用外部软件实现)
由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.im/post/5c8eed…