有時候咱們須要在項目添加幾個須要SEO的頁面,可是項目寫完了,不想用next,也不想搞很麻煩,因而就想簡單搭一個環境。把ssr頁面和spa的分開寫,可是項目不分開。css
效果截圖html
SPA的截圖 node
SSR的截圖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…