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…

相关文章
相关标签/搜索