React原生实现SSR

原生实现React服务端渲染

github项目代码地址

一 如何实现ssr

1.创建node服务实例

  • express.static('public') 指定静态文件的访问路径,比如在html中访问script的src属性时 会以这个文件夹为根路径
// 创建node服务实例 方便在其它地方应用
import express from 'express'
const app = express()
app.use(express.static('public'))
// 服务运行在3000端口
app.listen(3000, ()=> console.log('app is running on 3000 port'))
export default app

2. 根据路由处理请求

  • 首先需要知道请求过来的路由地址是多少
  • 根据这个请求的地址去跟匹配当前配置的路由
  • 得到配置的路由之后去获取这个路由对应组件需要的数据
  • 得到数据之后就可以去进行渲染处理了
    • '/':服务器端应用程序的入口文件,'*':接收所有的路由地址
    • req.path:获取请求地址
    • routes:获取路由配置信息
app.get('*',(req,res)=>{
    const store = createStore()
    const promise = matchRoutes(routes,req.path).map(({route})=>{
        if(route.loadData) return route.loadData(store)
    })
    Promise.all(promise).then(()=>{
        // 这里知道数据获取完成了 返回一个html内容
        res.send(render(req,store))
    })
})
  • 如下是双端的路由配置,loadData这个属性是组件中定义的一个获取页面数据的方法
import List from "./pages/List";
export default [
		...
    {
        path:'/List',
        component:List.component,
        loadData:List.loadData
    }
]
  • 如下是双端组件List页面
import React,{useEffect} from "react";
import { connect } from "react-redux";
import { fetchUser } from "../store/actions/user.action";
// 客户端和服务端公用组件 属于同构代码
function List ({user,dispatch}) {
    useEffect(() => {
        dispatch(fetchUser())
    }, []);
    return <div>
                <ul>
                    {user.map(item=><li key={item.id}>{item.name}</li>)}
                </ul>
            </div>
}
function loadData (store) {
    return store.dispatch(fetchUser())
}
const mapStateToProps = (state) => ({user: state.user});
export default {
    component: connect(mapStateToProps)(List),
    loadData
}

3. 数据请求完成后,render做了啥?

  • 需要返回一个有内容的html页面
  • 在此页面中需要有script去接替ssr页面
  • 在此页面中需要有初始化数据,避免hydrate对比出现警告问题
  • 对初始化数据进行转换,需要防止xss攻击
    • renderToString的返回结果是:将组件转换成html后的字符串
    • renderRoutes:返回组件形式的路由规则
    • StaticRouter:把数组形式的路由规则转换成组件形式的路由规则
    • serialize:对数据进行转换 防止xss攻击
export default (req,store) => {
    const content = renderToString(
        <Provider store={store}>
            <StaticRouter location={req.path}>
                {renderRoutes(routes)}
            </StaticRouter>
        </Provider>
    )
    const initalState = serialize(store.getState());
    return `
        <html>
            <head>
                <title>React SSR</title>
            </head>
            <body>
                <div id="root">${content}</div>
                <script>window.INITIAL_STATE = ${initalState}</script>
								// 客户端接替ssr页面
                <script src="bundle.js"></script>
            </body>
        </html>
    `
}
  • 在浏览器端脚本运行时,初始化的数据就是服务端渲染产生的数据window.INITIAL_STATE
const store = createStore(reducers, window.INITIAL_STATE, applyMiddleware(thunk))

二. webpack做了哪些配置

1.公共配置webpack.base.js

// webpack公共配置 可以在需要的地方用merge跟其它配置合并
const path = require('path')
module.exports = {
    mode:'development', // 开发环境
    module: {
        rules: [{
            test:/\.js$/, // 匹配.js结尾的文件
            exclude:/node_modules/, // 排除node_modules文件
            use:{
                loader:'babel-loader', //使用babel-loader加载器去处理.js结尾的文件
                options:{// options:babel-loader的相关配置项
                    presets:[
                        ["@babel/preset-env",{
                            useBuiltIns:"usage" // 该配置让浏览器支持异步函数
                        }], // @babel/preset-env预置:转换的是高阶的js语法
                        "@babel/preset-react", // @babel/preset-react预置:转换jsx语法
                    ]
                }
            }
        }]
    }
}

2.客户端打包配置webpack.client.js

const path = require('path')
const merge = require('webpack-merge')
const baseConfig = require('./webpack.base')
const config  = {
    entry:'./src/client/index.js', //入口文件
    output: {
        // __dirname:项目的根路径,path:出口文件路径:项目根路径下的build文件夹下,
        path: path.join(__dirname,'public'),
        // filename:打包文件名称
        filename:'bundle.js',
    },
}
// 合并公共配置并导出
module.exports = merge(baseConfig,config)

3.服务端打包配置webpack.server.js

  • nodeExternals():在服务器端打包文件中,包含了Node系统模块.导致打包文件本身体积庞大,可以通过webpack配置剔除打包文件中的Node模块
const path = require('path')
const merge = require('webpack-merge')
const baseConfig = require('./webpack.base')
const nodeExternals = require('webpack-node-externals')
const config = {
    target:'node', //应用在node
    entry:'./src/server/index.js', //入口文件
    output: {
        // __dirname:项目的根路径,path:出口文件路径:项目根路径下的build文件夹下,
        path: path.join(__dirname,'build'),
        // filename:打包文件名称
        filename:'bundle.js',
    },
    externals:[nodeExternals()]
}
// 合并公共配置并导出
module.exports = merge(baseConfig,config)

4. 一个命令实现客户端和服务端打包

  • npm run dev合并项目启动命令 执行所有'dev:'开头的命令
    • dev:server-build监听服务webpack的入口配置的文件变化,并打包代码
    • dev:client-build监听客户端webpack的入口配置的文件变化,并打包代码
    • dev:server-run监听服务端打包的代码并执行打包之后的文件,端口号为3000
"scripts": {
  "dev": "npm-run-all --parallel dev:*",
  "dev:server-build": "webpack --config webpack.server.js --watch",
  "dev:client-build": "webpack --config webpack.client.js --watch",
  "dev:server-run": "nodemon --watch build --exec \"node build/bundle.js\"",
},

三. 哪些代码可以同构

  • pages组件页面是可以同构的,因为不管在服务端还是在客户端它们的渲染方式相同
  • 状态管理中的actionsreducers可以同构.因为处理数据的逻辑可以复用.但是createStore初始化的时候不方便复用因为在被客户端接管之后要保证客户端和服务端数据状态一致比较麻烦.
  • 路由可以同构,以数组的形式进行配置.
    • 在服务端和客户端中分别用matchRoutes和StaticRouter进行匹配或转换获取对应的页面组件
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值