react-手把手教你構建一個簡單的ssr環境

有時候咱們須要在項目添加幾個須要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

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-…typescript

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} />))
複製代碼

綁定客戶端

說到客戶端,這時候須要先清楚代碼怎麼跑的問題了。

  1. 項目容易分離,可脫離node可綁定node
  2. 容易融合項目,以前項目龐大,不容易遷移,那麼尊重歷史,繼續按以前的方式執行,打包的時候打包一份能夠給ssr使用的包就能夠。
  3. 方便新項目,上手操做方便,調試方便(既要容易和老項目融合也方便新項目玩)
  4. 保留靈活性,可跨框架,跨語言(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.im/post/5c8eed…

相關文章
相關標籤/搜索