react+express实现服务端客户端同构渲染

最近开始研究服务端渲染,之前对这块不是很熟悉,现在做的项目本身是用react+express实现的,刚好熟悉项目架构的同时也把这块搞清楚。

1.客户端项目构建,

npx创建react项目,就按照之前创建正常的项目一样先把客户端的项目创建完成。实现一个简单的路由和页面,分别实现Home组件和About组件, 这一块对前端的同学很熟悉,就不着重讲了,这时候我们npm run start就可以看到项目跑在3000端口,这时候是一个纯粹的客户端项目。

yaml 复制代码
const routes = [
  {
    path: '/',
    extract: true,
    component: <Home />
  },
  {
    path: '/about',
    extract: true,
    component: <About />
  },
  {
    path: '/home',
    extract: true,
    component: <Home />
  },
];

这时候我们如果禁用js, 则会显示pulic文件夹下的index.html模版,写着你需要打开js才能正常运行项目。

所以服务端渲染就是需要渲染一个html模版。

2.服务端项目构建

我们选择express框架实现服务端渲染。首先在项目下建立server目录,用于存放我们的服务端代码。为了方便代码分割,我们建立app/routes/views三个文件夹,分别对应执行/路由/模版。

服务端代码的撰写原理,就是提前将客户端的代码渲染成为HTML模版然后直接直接展示,由客户端加载的js来执行其余需要js执行的操作。在这当中,express和react都为我们提供了非常方便的API我们直接使用即可。

首先编写入口文件index.js 主要负责启动项目,我们通过node server/index.js可以启动服务端项目,前面的依赖文件都是为了能够让react代码在服务端运行。

js 复制代码
// ignore `.scss` imports
require('ignore-styles');

// transpile imports on the fly
require('@babel/register')({
  presets: [
    ['@babel/preset-env'],
    '@babel/preset-react',
    '@babel/preset-typescript',
  ],
  plugins: [],
});
const Loadable = require('react-loadable');
const app = require('./app/development');

const PROT = 9000;
// import express server
Loadable.preloadAll()
  .then(() => {
    app.listen(PROT, (err) => {
      console.log('the app is now in http://localhost:9000')
      if (err) {
        console.error(err);
      }
    });
  })
  .catch((err) => {
    console.log('server fail', err);
    console.error(err);
  });

app/development.js 在这个项目里我们使用了webpackDevMiddleware中间件,主要是打包我们的react生成bundle.js后注入模版,让服务端正确拿到对应的js文件,以便到客户端的时候正确执行。

js 复制代码
const path = require('path');
const webpack = require('webpack');
const routes = require('../routes/development');
const webpackConfig = require('../../config/webpack.config.ssr.js');
const webpackDevMiddleware = require('webpack-dev-middleware');

// webpack init dev && hot middleware add webpack打包
const config = webpackConfig(process.env.NODE_ENV);;
const compiler = webpack(config);
const devMiddleware = webpackDevMiddleware(compiler, {
  serverSideRender: true,
  publicPath: config.output.publicPath,
});

const express = require('express');

// 创建应用程序
const createApplication = (inject) => {
  const orinalApp = express();
  // 注入并初始化一些路由或者webpack热更新中间件
  inject(orinalApp);
  return orinalApp;
};


const app = createApplication((orinalApp) => {
  // 模版设置文件夹
  orinalApp.set('view engine', 'ejs');
  orinalApp.set('views', path.resolve(__dirname, '../views/'));
  orinalApp.enable('view cache');

  // 静态资源访问
  orinalApp.use(devMiddleware);
  orinalApp.use('/', routes);
});

module.exports = app;

views/index.ejs 这个文件夹里存放的就是我们的服务端模版,使用ejs语法,在渲染的时候通过变量注入获取正确的appHtml和js/css的地址。

js 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta http-equiv="X-UA-Compatible" content="ie=edge" />
  <meta name="viewport"
    content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no" />
  <meta name="referrer" content="origin" />
  <link href="<%= PUBLIC_URL + 'static/css/main.css' %>" rel="stylesheet" />
</head>
  <body>
    <div id="root"><%- appHtml %></div>
   <script src="<%= PUBLIC_URL + 'static/js/bundle.js' %>"></script>
  </body>
</html>

routes/development.js 这里是对服务端的路由做了配置,在服务端因为路由是持久化的,所以使用StaticRouter来包裹路由,提前将客户端的路由下的components renderToString再注入到服务端模版中。

js 复制代码
import { Router as originalRouter } from 'express';
import { renderToString } from 'react-dom/server';
import React from 'react';
import { Route, Routes } from 'react-router-dom';
import { StaticRouter } from "react-router-dom/server";
import routes from '../../src/routes';

const webpackConfig = require('../../config/webpack.config.ssr.js');
const config = webpackConfig(process.env.NODE_ENV);;

const router = originalRouter();
router.get('*', async (req, res) => {

  let appHtml = renderToString(
    <StaticRouter location={req.url}>
      <Routes>
        {routes.map(({ path, exact, component }) => (
          <Route path={path} exact={exact} key={path} element={component} />
        ))}
      </Routes>
    </StaticRouter>);
  let tplName = 'index';
  res.render(tplName, {
    title: '腾讯新闻',
    appHtml: appHtml,
    PUBLIC_URL: config.output.publicPath,
  });
});

module.exports = router;

以上就实现了简单的服务端渲染,这个时候我们启动服务端项目,就可以看见项目运行,这时候如果禁用js,也可以看到模版被正确渲染出来。

TIPS:

在webpack打包ssr的时候有几点需要注意, 基本配置和webpack.config.js相同,但需要修改如下几点: 1.css的解析需要使用MiniCssExtractPlugin,将css单独提取出来成css文件后通过通过link标签注入,并且需要改为css-loader,。cra模版项目在本地默认是将css和js打包在一起,使用style-loader来讲样式通过标签写入html中的,这样的话在服务端就不能正确加载css。 2.入口文件默认是index.js里面的渲染方式是使用reactDOM.render(),在服务端我们更推荐使用hydrateRoot来渲染,这样在客户端渲染的时候就不是清空所有服务端代码重绘,而是在服务端模版基础上绘制,对于性能和体验都更好,因此可以另外创建一个root.js,并且修改打包的入口文件。

项目git地址 github.com/sissi144/ss...

相关推荐
程序员凡尘24 分钟前
完美解决 Array 方法 (map/filter/reduce) 不按预期工作 的正确解决方法,亲测有效!!!
前端·javascript·vue.js
编程零零七4 小时前
Python数据分析工具(三):pymssql的用法
开发语言·前端·数据库·python·oracle·数据分析·pymssql
(⊙o⊙)~哦6 小时前
JavaScript substring() 方法
前端
无心使然云中漫步6 小时前
GIS OGC之WMTS地图服务,通过Capabilities XML描述文档,获取matrixIds,origin,计算resolutions
前端·javascript
Bug缔造者6 小时前
Element-ui el-table 全局表格排序
前端·javascript·vue.js
xnian_7 小时前
解决ruoyi-vue-pro-master框架引入报错,启动报错问题
前端·javascript·vue.js
麒麟而非淇淋8 小时前
AJAX 入门 day1
前端·javascript·ajax
2401_858120538 小时前
深入理解MATLAB中的事件处理机制
前端·javascript·matlab
阿树梢8 小时前
【Vue】VueRouter路由
前端·javascript·vue.js
随笔写9 小时前
vue使用关于speak-tss插件的详细介绍
前端·javascript·vue.js