17、服务器端渲染(SSR)的实现与应用

服务器端渲染(SSR)的实现与应用

在现代Web开发中,服务器端渲染(Server-Side Rendering,简称SSR)是一项非常重要的技术,它可以显著提升网站的SEO性能,使网站更容易被主流搜索引擎(如Google、Yahoo和Bing)索引。本文将详细介绍如何实现SSR,并结合实际案例展示如何使用Promise来连接组件与Redux,通过API获取数据并进行服务器端渲染。

1. 服务器端渲染的重要性

服务器端渲染对于提升网站的SEO至关重要。虽然Googlebot目前支持客户端渲染(CSR),可以在Google上对网站进行索引,但对于其他搜索引擎(如Yahoo、Bing或DuckDuckGo),使用SSR可以更好地提高网站的SEO效果。如果您不太关心SEO,那么可能不需要担心SSR;但如果您希望网站在多个搜索引擎上都有良好的表现,那么SSR是一个不错的选择。

2. 实现服务器端渲染的准备工作

在开始实现SSR之前,需要安装一些必要的依赖项。可以使用以下命令进行安装:

npm install --save-dev webpack-node-externals webpack-dev-middleware webpack-hot-middleware webpack-hot-server-middleware webpack-merge babel-cli babel-preset-es2015
3. 具体实现步骤

以下是实现服务器端渲染的详细步骤:
1. 添加npm脚本 :在 package.json 文件中添加以下脚本:

"scripts": {
  "clean": "rm -rf dist/ && rm -rf public/app",
  "start": "npm run clean & NODE_ENV=development BABEL_ENV=development nodemon src/server --watch src/server --watch src/shared --exec babel-node --presets es2015",
  "start-analyzer": "npm run clean && NODE_ENV=development BABEL_ENV=development ANALYZER=true babel-node src/server"
}
  1. 修改 webpack.config.js 文件 :由于要实现SSR,需要将Webpack配置分为客户端配置和服务器配置,并以数组形式返回。文件内容如下:
// Webpack Configuration (Client & Server)
import clientConfig from './webpack/webpack.config.client';
import serverConfig from './webpack/webpack.config.server';
export default [
  clientConfig,
  serverConfig
];
  1. 创建客户端配置文件 webpack.config.client.js
// Dependencies
import webpackMerge from 'webpack-merge';
// Webpack Configuration
import commonConfig from './webpack.config.common';
import {
  context,
  devtool,
  entry,
  name,
  output,
  optimization,
  plugins,
  target
} from './configuration';
// Type of Configuration
const type = 'client';
export default webpackMerge(commonConfig(type), {
  context: context(type),
  devtool,
  entry: entry(type),
  name: name(type),
  output: output(type),
  optimization,
  plugins: plugins(type),
  target: target(type)
});
  1. 创建服务器配置文件 webpack.config.server.js
// Dependencies
import webpackMerge from 'webpack-merge';
// Webpack Configuration
import commonConfig from './webpack.config.common';
// Configuration
import {
  context,
  entry,
  externals,
  name,
  output,
  plugins,
  target
} from './configuration';
// Type of Configuration
const type = 'server';
export default webpackMerge(commonConfig(type), {
  context: context(type),
  entry: entry(type),
  externals: externals(type),
  name: name(type),
  output: output(type),
  plugins: plugins(type),
  target: target(type)
});
  1. 创建通用配置文件 webpack.config.common.js
// Configuration
import { module, resolve, mode } from './configuration';
export default type => ({
  module: module(type),
  resolve,
  mode
});
  1. 创建 context.js 文件
// Dependencies
import path from 'path';
export default type => type === 'server'
  ? path.resolve(__dirname, '../../src/server')
  : path.resolve(__dirname, '../../src/client');
  1. 创建 entry.js 文件
// Environment
const isDevelopment = process.env.NODE_ENV !== 'production';
export default type => {
  if (type === 'server') {
    return './render/serverRender.js';
  }
  const entry = [];
  if (isDevelopment) {
    entry.push(
      'webpack-hot-middleware/client',
      'react-hot-loader/patch'
    );
  }
  entry.push('./index.jsx');
  return entry;
};
  1. 创建 externals.js 文件
// Dependencies
import nodeExternals from 'webpack-node-externals';
export default () => [
  nodeExternals({
    whitelist: [/^redux\/(store|modules)/]
  })
];
  1. 修改 module.js 文件
// Dependencies
import ExtractTextPlugin from 'extract-text-webpack-plugin';
// Environment
const isDevelopment = process.env.NODE_ENV !== 'production';
export default type => {
  const rules = [
    {
      test: /\.(js|jsx)$/,
      use: 'babel-loader',
      exclude: /node_modules/
    }
  ];
  if (!isDevelopment || type === 'server') {
    rules.push({
      test: /\.scss$/,
      use: ExtractTextPlugin.extract({
        fallback: 'style-loader',
        use: [
          'css-loader?minimize=true&modules=true&localIdentName=[name]__[local]_[hash:base64]',
          'sass-loader'
        ]
      })
    });
  } else {
    rules.push({
      test: /\.scss$/,
      use: [
        {
          loader: 'style-loader'
        },
        {
          loader: 'css-loader',
          options: {
            modules: true,
            importLoaders: 1,
            localIdentName: '[name]__[local]_[hash:base64]',
            sourceMap: true,
            minimize: true
          }
        },
        {
          loader: 'sass-loader'
        }
      ]
    });
  }
  return {
    rules
  };
};
  1. 创建 name.js 文件
export default type => type;
  1. 创建 output.js 文件
// Dependencies
import path from 'path';
export default type => {
  if (type === 'server') {
    return {
      filename: 'server.js',
      path: path.resolve(__dirname, '../../dist'),
      libraryTarget: 'commonjs2'
    };
  }
  return {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, '../../public/app'),
    publicPath: '/'
  };
};
  1. 创建 plugins.js 文件
// Dependencies
import CompressionPlugin from 'compression-webpack-plugin';
import ExtractTextPlugin from 'extract-text-webpack-plugin';
import webpack from 'webpack';
import WebpackNotifierPlugin from 'webpack-notifier';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
// Environment
const isDevelopment = process.env.NODE_ENV !== 'production';
// Analyzer
const isAnalyzer = process.env.ANALYZER === 'true';
export default type => {
  const plugins = [
    new ExtractTextPlugin({
      filename: '../../public/css/style.css'
    })
  ];
  if (isAnalyzer) {
    plugins.push(
      new BundleAnalyzerPlugin()
    );
  }
  if (isDevelopment) {
    plugins.push(
      new webpack.HotModuleReplacementPlugin(),
      new webpack.NoEmitOnErrorsPlugin(),
      new WebpackNotifierPlugin({
        title: 'CodeJobs'
      })
    );
  } else {
    plugins.push(
      new CompressionPlugin({
        asset: '[path].gz[query]',
        algorithm: 'gzip',
        test: /\.js$|\.css$|\.html$/,
        threshold: 10240,
        minRatio: 0.8
      })
    );
  }
  return plugins;
};
  1. 创建 resolve.js 文件
// Dependencies
import path from 'path';
export default {
  extensions: ['.js', '.jsx'],
  modules: [
    'node_modules',
    path.resolve(__dirname, '../../src/client'),
    path.resolve(__dirname, '../../src/server')
  ]
};
  1. 创建 target.js 文件
export default type => type === 'server' ? 'node' : 'web';
  1. 修改 App.jsx 文件
// Dependencies
import React from 'react';
import {
  BrowserRouter,
  StaticRouter,
  Switch,
  Route
} from 'react-router-dom';
// Components
import About from '@components/About';
import Home from '@components/Home';
export default ({ server, location, context = {} }) => {
  const routes = (
    <Switch>
      <Route exact path="/" component={Home} />
      <Route exact path="/about" component={About} />
    </Switch>
  );
  // Client Router
  let router = (
    <BrowserRouter>
      {routes}
    </BrowserRouter>
  );
  // Server Router
  if (server) {
    router = (
      <StaticRouter location={location} context={context}>
        {routes}
      </StaticRouter>
    );
  }
  return router;
};
  1. 修改 server/index.js 文件
// Dependencies
import express from 'express';
import path from 'path';
import webpackDevMiddleware from 'webpack-dev-middleware';
import webpackHotMiddleware from 'webpack-hot-middleware';
import webpackHotServerMiddleware from 'webpack-hot-server-middleware';
import webpack from 'webpack';
// Utils
import { isMobile, isBot } from '@utils/device';
// Client Render
import clientRender from './render/clientRender';
// Webpack Configuration
import webpackConfig from '@webpack';
// Environment
const isProduction = process.env.NODE_ENV === 'production';
// Express Application
const app = express();
// Webpack Compiler
const compiler = webpack(webpackConfig);
// Public directory
app.use(express.static(path.join(__dirname, '../../public')));
// Device Detection
app.use((req, res, next) => {
  req.isMobile = isMobile(req.headers['user-agent']);
  // We detect if a search bot is accessing...
  req.isBot = isBot(req.headers['user-agent']);
  next();
});
// Webpack Middleware
if (!isProduction) {
  // Hot Module Replacement
  app.use(webpackDevMiddleware(compiler));
  app.use(webpackHotMiddleware(
    compiler.compilers.find(compiler => compiler.name === 'client')
  ));
} else {
  // GZip Compression just for Production
  app.get('*.js', (req, res, next) => {
    req.url = `${req.url}.gz`;
    res.set('Content-Encoding', 'gzip');
    next();
  });
}
// Client Side Rendering
app.use(clientRender());
if (isProduction) {
  try {
    // eslint-disable-next-line
    const serverRender = require('../../dist/server.js').default;
    app.use(serverRender());
  } catch (e) {
    throw e;
  }
}
// For Server Side Rendering on Development Mode
app.use(webpackHotServerMiddleware(compiler));
// Disabling x-powered-by
app.disable('x-powered-by');
// Listen Port...
app.listen(3000);
  1. 修改 clientRender.js 文件
// HTML
import html from './html';
// Initial State
import initialState from './initialState';
export default function clientRender() {
  return (req, res, next) => {
    if (req.isBot) {
      return next();
    }
    res.send(html({
      title: 'Codejobs',
      initialState: initialState(req)
    }));
  };
}
  1. 创建 serverRender.js 文件
// Dependencies
import React from 'react';
import { renderToString } from 'react-dom/server';
import { Provider } from 'react-redux';
// Redux Store
import configureStore from '@configureStore';
// Components
import App from '../../client/App';
import html from './html';
// Initial State
import initialState from './initialState';
export default function serverRender() {
  return (req, res, next) => {
    // Configuring Redux Store
    const store = configureStore(initialState(req));
    const markup = renderToString(
      <Provider store={store}>
        <App
          server
          location={req.url}
        />
      </Provider>
    );
    res.send(html({
      title: 'Codejobs',
      markup,
      initialState: initialState(req)
    }));
  };
}
4. 服务器端渲染的工作原理

可以使用 npm start 命令启动应用程序。在浏览器(如Chrome)中打开 http://localhost:3000 ,右键单击并选择“查看页面源代码”,可能会发现没有使用SSR。这是因为我们只对搜索机器人使用SSR。 isBot 函数可以检测所有搜索机器人,为了测试SSR,将 curl 作为机器人添加到测试中,其代码如下:

export function isBot(ua) {
  const b = /curl|bot|googlebot|google|baidu|bing|msn|duckduckgo|teoma|slurp|yandex|crawler|spider|robot|crawling/i;
  return b.test(ua);
}

在应用程序运行时,打开一个新的终端,执行以下命令:

curl http://localhost:3000

可以看到 #root div内的HTML代码是使用SSR渲染的。如果想测试 /about 路由,也可以使用 curl 命令:

curl http://localhost:3000/about

此外,Chrome有一个名为 User-Agent Switcher for Chrome 的扩展程序,可以在浏览器中指定要使用的用户代理。例如,可以为Googlebot添加一个特殊的用户代理,然后在 User-Agent Switcher 中选择 Chrome | Bot ,当查看页面源代码时,可以看到HTML代码以SSR方式渲染。

5. 处理 window 对象的问题

在使用SSR时,直接使用 window 对象会导致 ReferenceError ,例如:

ReferenceError: window is not defined

为了解决这个问题,可以创建一个函数来验证是否在浏览器环境中使用 window 对象:

export function isBrowser() {
  return typeof window !== 'undefined';
}

每次需要使用 window 对象时,可以这样做:

const store = isBrowser() ? configureStore(window.initialState) : {};
6. 使用Promise实现服务器端渲染

前面的示例展示了如何使用简单组件进行SSR,但在实际应用中,可能需要使用Promise来连接组件与Redux,通过API获取数据并进行服务器端渲染。以下是具体步骤:

6.1 准备工作

需要安装以下包:

npm install axios babel-preset-stage-0 react-router-dom redux-devtools-extension redux-thunk
6.2 具体实现步骤
  1. 添加简单API :创建一个简单的API来显示待办事项列表,在 src/server/controllers/api.js 文件中添加以下代码:
import express from 'express';
const router = express.Router();
// Mock data, this should come from a database....
const todo = [
  {
    id: 1,
    title: 'Go to the Gym'
  },
  {
    id: 2,
    title: 'Dentist Appointment'
  },
  {
    id: 3,
    title: 'Finish homework'
  }
];
router.get('/todo/list', (req, res, next) => {
  res.json({
    response: todo
  });
});
export default router;
  1. 导入API控制器 :在 src/server/index.js 文件中导入API控制器,并将其作为中间件添加到 /api 路由中:
// Controllers
import apiController from './controllers/api';
// Express Application
const app = express();
// Webpack Compiler
const compiler = webpack(webpackConfig);
// Routes
app.use('/api', apiController);
  1. 修改 serverRender.js 文件 :之前在 serverRender.js 文件中直接渲染 App 组件,现在需要从具有 initialAction 静态方法的组件中获取Promise,将它们保存到 promises 数组中,解析这些Promise,然后渲染 App 组件:
// Dependencies
import React from 'react';
import { renderToString } from 'react-dom/server';
import { Provider } from 'react-redux';
import { matchPath } from 'react-router-dom';
// Redux Store
import configureStore from '@configureStore';
// Components
import App from '../../client/App';
// HTML
import html from './html';
// Initial State
import initialState from './initialState';
// Routes
import routes from '@shared/routes';
export default function serverRender() {
  return (req, res, next) => {
    // Configuring Redux Store
    const store = configureStore(initialState(req));
    // Getting the promises from the components which has initialAction.
    const promises = routes.paths.reduce((promises, route) => {
      if (matchPath(req.url, route) && route.component && route.component.initialAction) {
        promises.push(Promise.resolve(store.dispatch(route.component.initialAction())));
      }
      return promises;
    }, []);
    // Resolving our promises
    Promise.all(promises)
      .then(() => {
        // Getting Redux Initial State
        const initialState = store.getState();
        // Rendering with SSR
        const markup = renderToString(
          <Provider store={store}>
            <App
              server
              location={req.url}
            />
          </Provider>
        );
        // Sending our HTML code.
        res.send(html({
          title: 'Codejobs',
          markup,
          initialState
        }));
      })
      .catch(e => {
        // eslint-disable-line no-console
        console.log('Promise Error: ', e);
      });
  };
}
  1. 修改客户端目录结构 :将组件封装为小型应用程序,在其中创建 actions API components containers reducers 文件夹。新的结构如下:
client
└── todo
    ├── actions
    │   ├── actionTypes.js
    │   └── index.js
    ├── api
    │   ├── constants.js
    │   └── index.js
    ├── components
    │   ├── Layout.jsx
    │   └── Todo.jsx
    ├── containers
    │   └── index.js
    └── reducer
        └── index.js
  1. 创建 actionTypes.js 文件 :在 src/client/todo/actions/actionTypes.js 文件中添加 FETCH_TODO 动作:
// Actions
export const FETCH_TODO = {
  request: () => 'FETCH_TODO_REQUEST',
  success: () => 'FETCH_TODO_SUCCESS'
};
  1. 创建 index.js 文件 :在 src/client/todo/actions/index.js 文件中创建 fetchTodo 动作,用于从API中获取待办事项列表:
// Base Actions
import { request, received } from '@baseActions';
// Api
import api from '../api';
// Action Types
import { FETCH_TODO } from './actionTypes';
export const fetchTodo = () => dispatch => {
  const action = FETCH_TODO;
  const { fetchTodo } = api;
  dispatch(request(action));
  return fetchTodo()
    .then(response => dispatch(received(action, response.data)));
};
  1. 创建 baseActions.js 文件 :在 src/shared/redux/baseActions.js 文件中创建 request received 函数,用于简化动作的分发:
// Base Actions
export const request = ACTION => ({
  type: ACTION.request()
});
export const received = (ACTION, data) => ({
  type: ACTION.success(),
  payload: data
});
  1. 创建 constants.js 文件 :在 src/client/todo/api/constants.js 文件中定义API常量:
export const API = Object.freeze({
  TODO: 'api/todo/list'
});
  1. 创建 index.js 文件 :在 src/client/todo/api/index.js 文件中创建 Api 类,并添加一个静态方法 fetchTodo
// Dependencies
import axios from 'axios';
// Configuration
import config from '@configuration';
// Utils
import { isBrowser } from '@utils/frontend';
// Constants
import { API } from './constants';
class Api {
  static fetchTodo() {
    // For Node (SSR) we have to specify our base domain (http://localhost:3000/api/todo/list)
    // For Client Side Render just /api/todo/list.
    const url = isBrowser()
      ? API.TODO
      : `${config.baseUrl}/${API.TODO}`;
    return axios(url);
  }
}
export default Api;
  1. 创建 Todo 容器 :在 src/client/todo/container/index.js 文件中,将待办事项列表映射到Redux,并将 fetchTodo 动作添加到Redux中:
// Dependencies
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
// Components
import Layout from '../components/Layout';
// Actions
import { fetchTodo } from '../actions';
export default connect(({ todo }) => ({
  todo: todo.list
}), dispatch => bindActionCreators(
  {
    fetchTodo
  },
  dispatch
))(Layout);
  1. 创建 Layout 组件 :在 src/client/todo/components/Layout.jsx 文件中创建 Layout 组件:
// Dependencies
import React from 'react';
// Shared Components
import Header from '@layout/Header';
import Content from '@layout/Content';
import Footer from '@layout/Footer';
// Componenets
import Todo from '../components/Todo';
const Layout = props => (
  <main>
    <Header {...props} />
    <Content>
      <Todo {...props} />
    </Content>
    <Footer {...props} />
  </main>
);
export default Layout;
  1. 创建 reducer 文件 :在 src/client/todo/reducer/index.js 文件中创建 todoReducer
// Utils
import { getNewState } from '@utils/frontend';
// Action Types
import { FETCH_TODO } from '../actions/actionTypes';
// Initial State
const initialState = {
  list: []
};
export default function todoReducer(state = initialState, action) {
  switch (action.type) {
    case FETCH_TODO.success(): {
      const { payload: { response = [] } } = action;
      return getNewState(state, {
        list: response
      });
    }
    default:
      return state;
  }
}
  1. 创建 Todo 组件 :在 src/client/todo/components/Todo.jsx 文件中创建 Todo 组件,在 componentDidMount 方法中执行 fetchTodo 动作,并将待办事项列表渲染为HTML列表:
// Dependencies
import React, { Component } from 'react';
// Utils
import { isFirstRender } from '@utils/frontend';
// Styles
import styles from './Todo.scss';
class Todo extends Component {
  componentDidMount() {
    const { fetchTodo } = this.props;
    fetchTodo();
  }
  render() {
    const {
      todo
    } = this.props;
    if (isFirstRender(todo)) {
      return null;
    }
    return (
      <div>
        <div className={styles.Todo}>
          <ol>
            {todo.map((item, key) =>
              <li key={key}>{item.title}</li>)}
          </ol>
        </div>
      </div>
    );
  }
}
export default Todo;

总结

通过以上步骤,我们详细介绍了如何实现服务器端渲染,并结合实际案例展示了如何使用Promise来连接组件与Redux,通过API获取数据并进行服务器端渲染。服务器端渲染可以显著提升网站的SEO性能,同时解决了在SSR中使用 window 对象的问题。希望本文对您理解和实现服务器端渲染有所帮助。

流程图

graph LR
    A[开始] --> B[安装依赖]
    B --> C[添加npm脚本]
    C --> D[修改webpack配置文件]
    D --> E[创建客户端和服务器配置文件]
    E --> F[创建通用配置文件]
    F --> G[创建其他配置文件]
    G --> H[修改App.jsx文件]
    H --> I[修改server/index.js文件]
    I --> J[修改clientRender.js文件]
    J --> K[创建serverRender.js文件]
    K --> L[启动应用程序]
    L --> M[测试SSR]
    M --> N[处理window对象问题]
    N --> O[使用Promise实现SSR]
    O --> P[结束]

表格

步骤 描述
1 安装必要的依赖项
2 添加npm脚本到 package.json 文件
3 修改 webpack.config.js 文件
4 创建客户端和服务器配置文件
5 创建通用配置文件
6 创建其他配置文件(如 context.js entry.js 等)
7 修改 App.jsx 文件
8 修改 server/index.js 文件
9 修改 clientRender.js 文件
10 创建 serverRender.js 文件
11 启动应用程序并测试SSR
12 处理 window 对象的问题
13 使用Promise实现服务器端渲染

6.3 使用Promise实现服务器端渲染的工作原理

在使用Promise实现服务器端渲染时,核心在于处理组件的异步数据获取。通过 serverRender.js 文件中的逻辑,我们可以看到整个过程的详细步骤:
1. 配置Redux Store :首先,根据请求的初始状态配置Redux Store。

const store = configureStore(initialState(req));
  1. 获取组件的Promise :遍历路由配置,找出具有 initialAction 静态方法的组件,并将其返回的Promise添加到 promises 数组中。
const promises = routes.paths.reduce((promises, route) => {
  if (matchPath(req.url, route) && route.component && route.component.initialAction) {
    promises.push(Promise.resolve(store.dispatch(route.component.initialAction())));
  }
  return promises;
}, []);
  1. 解析Promise :使用 Promise.all 方法解析所有Promise,确保所有异步数据都已获取。
Promise.all(promises)
  .then(() => {
    // 获取Redux初始状态
    const initialState = store.getState();
    // 使用SSR渲染组件
    const markup = renderToString(
      <Provider store={store}>
        <App
          server
          location={req.url}
        />
      </Provider>
    );
    // 发送HTML代码
    res.send(html({
      title: 'Codejobs',
      markup,
      initialState
    }));
  })
  .catch(e => {
    console.log('Promise Error: ', e);
  });

7. 常见问题及解决方案

7.1 window 对象问题

在服务器端渲染中,直接使用 window 对象会导致 ReferenceError 。为了解决这个问题,我们创建了 isBrowser 函数来验证是否在浏览器环境中使用 window 对象。

export function isBrowser() {
  return typeof window !== 'undefined';
}

在需要使用 window 对象的地方,可以这样使用:

const store = isBrowser() ? configureStore(window.initialState) : {};
7.2 异步数据获取问题

在使用Promise实现服务器端渲染时,确保所有异步数据都已获取是关键。通过在 serverRender.js 文件中使用 Promise.all 方法,我们可以等待所有Promise解析完成后再进行渲染。

7.3 性能问题

服务器端渲染可能会对服务器性能产生一定影响。为了优化性能,可以考虑以下几点:
- 缓存数据 :对于一些不经常变化的数据,可以使用缓存来减少重复的API请求。
- 代码分割 :使用Webpack的代码分割功能,将应用程序拆分为多个小块,减少初始加载时间。
- 压缩代码 :在生产环境中,使用压缩插件(如 CompressionPlugin )对代码进行压缩,减少传输数据量。

8. 最佳实践

8.1 代码结构

将组件封装为小型应用程序,每个应用程序包含 actions API components containers reducers 文件夹,这样可以提高代码的可维护性和可扩展性。

8.2 错误处理

serverRender.js 文件中,使用 try...catch 块来捕获Promise解析过程中的错误,并进行适当的处理。

Promise.all(promises)
  .then(() => {
    // 渲染逻辑
  })
  .catch(e => {
    console.log('Promise Error: ', e);
  });
8.3 性能优化

除了前面提到的缓存数据、代码分割和压缩代码外,还可以使用CDN来加速静态资源的加载。

9. 总结

通过本文的介绍,我们详细了解了服务器端渲染(SSR)的实现方法和使用Promise实现服务器端渲染的具体步骤。服务器端渲染可以显著提升网站的SEO性能,同时解决了在SSR中使用 window 对象的问题。在实际应用中,我们需要注意处理异步数据获取、性能优化和错误处理等问题,遵循最佳实践来提高代码的可维护性和可扩展性。

流程图

graph LR
    A[开始使用Promise实现SSR] --> B[配置Redux Store]
    B --> C[获取组件的Promise]
    C --> D[解析Promise]
    D --> E[获取Redux初始状态]
    E --> F[使用SSR渲染组件]
    F --> G[发送HTML代码]
    G --> H[结束]

表格

步骤 描述
1 配置Redux Store
2 获取组件的Promise
3 解析Promise
4 获取Redux初始状态
5 使用SSR渲染组件
6 发送HTML代码

希望本文对您理解和实现服务器端渲染有所帮助,让您能够在实际项目中更好地应用这一技术。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值