十一、实现服务器端渲染

本章将介绍以下配方:

  • 实现服务器端渲染
  • 使用服务器端呈现实现承诺
  • 实现 Next.js

介绍

React 通常使用客户端渲染(CSR)。这意味着它会在目标div中动态注入 HTML 代码(它通常使用#app#rootID),这就是为什么如果您尝试直接查看页面代码(右键单击查看页面代码),您会看到如下内容:

查看实际代码的唯一方法是使用 Chrome Dev 工具或其他工具检查站点,以下是 React 使用 CSR 生成的代码:

通过查看页面,您可以看到注入我们的#rootdiv 的代码。服务器端呈现(SSR)对于提高我们网站的SEO非常有用,并被主要搜索引擎索引,如谷歌雅虎Bing。如果你不太关心 SEO,你可能不需要担心SSR。目前,谷歌机器人支持CSR,它可以在谷歌上为我们的网站建立索引,但是如果您关心 SEO,并且担心在其他搜索引擎上提高 SEO,例如雅虎必应DuckDuckGo,那么,使用 SSR 就是一条出路。

实现服务器端渲染

在这个配方中,我们将在我们的项目中实现 SSR。

准备

我们将使用第 10 章中最后一个配方的代码(使用 React/Redux 实现 Node.js 和 Webpack 4】,掌握 Webpack 4.x,并安装一些其他依赖项:

npm install --save-dev webpack-node-externals webpack-dev-middleware webpack-hot-middleware webpack-hot-server-middleware webpack-merge babel-cli babel-preset-es2015

怎么做。。。

现在让我们看一下渲染的步骤:

  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"
    }

File: package.json

  1. 现在我们必须更改我们的webpack.config.js文件。因为我们将要实现 SSR,所以我们需要将 Web 包配置分为客户端配置和服务器配置,并将它们作为数组返回。该文件应如下所示:
  // Webpack Configuration (Client & Server)
  import clientConfig from './webpack/webpack.config.client';
  import serverConfig from './webpack/webpack.config.server';

 export default [
    clientConfig,
    serverConfig
  ];

File: webpack.config.js

  1. 现在我们需要在webpack文件夹中为我们的客户机配置创建一个文件。我们需要称之为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)
  });

File: webpack/webpack.config.client.js

  1. 现在,服务器配置应如下所示:
 // 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)
  });

File: webpack/webpack.config.server.js

  1. 如您所见,在这两个文件中,我们都导入了一个公共配置文件,其中包含需要添加到客户端和服务器的配置:
  // Configuration
  import { module, resolve, mode } from './configuration';
  export default type => ({
    module: module(type),
    resolve,
    mode
  });

File: webpack/webpack.config.common.js

  1. 我们需要为 Webpack 节点添加新的配置文件,还需要修改一些我们已经拥有的文件。我们需要创建的第一个是context.js。在这个文件(以及其他一些文件)中,我们将导出一个带有类型参数的函数,它可以是客户端服务器,根据该值,我们将返回不同的配置:
  // Dependencies
  import path from 'path';
 export default type => type === 'server'
    ? path.resolve(__dirname, '../../src/server')
    : path.resolve(__dirname, '../../src/client');

File: webpack/configuration/context.js

  1. 条目文件是我们将要添加到捆绑包中的所有文件的添加位置。我们的条目文件现在应该如下所示:
  // 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;
  };

File: webpack/configuration/entry.js

  1. 我们需要创建一个名为 externals.js 的文件,其中包含我们不会捆绑的模块(除非它们在白名单上):
  // Dependencies
  import nodeExternals from 'webpack-node-externals';

  export default () => [
    nodeExternals({
      whitelist: [/^redux\/(store|modules)/]
    })
  ];

File: webpack/configuration/externals.js

  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
    };
  };

File: webpack/configuration/module.js

  1. 现在我们需要为名称创建一个节点:
  export default type => type;

File: webpack/configuration/name.js

  1. 对于输出配置,我们需要根据配置类型(客户端或服务器)返回一个对象:
  // 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: '/'
    };
  };

File: webpack/configuration/output.js

  1. 在我们的plugins.js文件中,我们正在验证用户是否发送了ANALYZER变量来显示BundleAnalyzerPlugin,就在这种情况下,而不是每次我们在开发模式下运行我们的应用时:
  // 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;
  };

File: webpack/configuration/plugins.js

  1. 我们需要在解析文件中指定我们的模块;文件应如下所示:
  // Dependencies
  import path from 'path';

  export default {
    extensions: ['.js', '.jsx'],
    modules: [
      'node_modules',
      path.resolve(__dirname, '../../src/client'),
      path.resolve(__dirname, '../../src/server')
    ]
  };

File: webpack/configuration/resolve.js

  1. 我们需要创建的最后一个配置是target.js文件:
 export default type => type === 'server' ? 'node' : 'web';

File: webpack/configuration/target.js

  1. 配置完我们的 Webpack 后,我们需要修改我们的App.jsx文件,其中我们需要使用<BrowserRouter>组件为客户端创建路由,使用<StaticRouter>组件为服务器创建路由:
  // 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;
  };

File: src/client/App.jsx

  1. 现在我们需要修改我们的服务器文件(index.js,以使用我们的clientRenderserverRender中间件:
  // 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);

File: src/server/index.js

  1. 我们需要修改我们的clientRender.js文件。如果我们检测到具有isBot功能的搜索机器人,我们将返回next()中间件。否则,我们呈现 HTML 并使用 CSR 执行应用:
  // 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)
      }));
    };
  }

File: src/server/render/clientRender.js

  1. 现在让我们创建serverRender.js文件。这里,我们需要使用react-dom/server库中的renderToString方法呈现App组件:
  // 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)
      }));
    };
  }

File: src/server/render/serverRender.js

它是如何工作的。。。

您可以通过运行npm start命令来启动应用。

如果您在浏览器中的http://localhost:3000处打开应用(例如 Chrome),然后右键单击并查看页面源代码,您可能会注意到我们没有使用 SSR:

这是因为我们将只对搜索机器人使用 SSR。isBot功能将检测所有搜索机器人,为了测试,我添加了curl作为机器人来测试我们的 SSR;这是该功能的代码:

  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);
  }

File: src/shared/utils/device.js

当应用在另一个终端中运行时,打开一个新终端,然后执行以下命令:

    curl http://localhost:3000

如您所见,#root div 中的 HTML 代码使用 SSR 进行渲染。

此外,如果您想尝试在 curl 中运行/about,您将看到它也将使用 SSR 进行渲染:

Chrome 有一个扩展名为 CChrome的用户代理切换器,您可以在其中指定要在浏览器中使用的用户代理。通过这种方式,您可以为 Googlebot 添加一个特殊的用户代理,例如:

然后,如果在 User Agent Switcher 中选择 Chrome | Bot,则在查看页面源代码时,可以看到 HTML 代码将其呈现为 SSR:

还有更多。。。

当我们使用 SSR 时,在尝试为客户机使用对象窗口时,我们必须非常小心。如果直接使用 SSR 使用,则会出现如下引用错误:

    ReferenceError: window is not defined

要解决此问题,可以验证窗口对象是否存在,但这可能非常重复。我更喜欢创建一个函数来验证我们使用的是浏览器(客户端)还是服务器。您可以这样做:

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

然后,每次需要使用窗口对象时,都可以执行以下操作:

 const store = isBrowser() ? configureStore(window.initialState) : {};

使用服务器端呈现实现承诺

在上一个配方中,我们看到了 SSR 是如何工作的,但该配方仅限于用简单的组件显示 SSR。在本教程中,我们将学习如何实现将组件连接到 Redux 的承诺,使用 API 获取数据并使用 SSR 呈现组件。

准备

我们将使用上一个配方中的相同代码,但我们将进行一些更改。在此配方中,我们需要安装以下软件包:

    npm install axios babel-preset-stage-0 react-router-dom redux-devtools-extension redux-thunk

怎么做。。。

对于这个方法,我们将实现一个从 API 中提取的基本 todo 列表,以展示如何使用 SSR 将 Redux 连接到我们的应用:

  1. 我们需要做的第一件事是添加一个简单的 API 来显示待办事项列表:
  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;

File: src/server/controllers/api.js

  1. 第二步是将此 API 控制器导入我们的src/server/index.js文件,并将其作为中间件添加到/api路由上:
  ...
  // Controllers
  import apiController from './controllers/api';
  ...
  // Express Application
  const app = express();

  // Webpack Compiler
  const compiler = webpack(webpackConfig);

  // Routes
  app.use('/api', apiController);
  ...

File: src/server/index.js

  1. 之前,在我们的serverRender.js文件中,我们直接呈现了App组件。现在我们需要从具有静态方法initialAction的组件获取承诺,将其保存到承诺数组中,解析它们,然后呈现我们的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); 
        });
    };
  }

File: src/server/render/serverRender.js

  1. 在这个方法中,我们需要在客户端目录中稍微更改一下文件夹结构。以前,我们有一个components目录,我们的组件在里面。现在我们将把我们的组件封装成小应用,在里面我们可以创建我们的动作、API、组件、容器和还原器。我们的新结构应如下所示:

  1. 我们将创建一个 todo 应用。要做到这一点,首先我们需要添加 actions 文件夹,并在其中创建第一个actionTypes.js文件。在这个文件中,我们需要添加我们的FETCH_TODO操作。我更喜欢创建一个具有两个功能的对象,一个用于请求,另一个用于成功;当我们在减速器上使用此功能时,以及当我们采取行动时,您将看到此功能的优势:
  // Actions
  export const FETCH_TODO = {
    request: () => 'FETCH_TODO_REQUEST',
    success: () => 'FETCH_TODO_SUCCESS'
  };

File: src/client/todo/actions/actionTypes.js

  1. 在我们的index.js文件中,我们将创建一个 fetchTodo 操作,以从 API 检索我们的 todo 列表项:
  // 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)));
  };

File: src/client/todo/actions/index.js

  1. 如您所见,我们正在使用来自基本操作的两个特定方法(请求和接收)。这些函数将帮助我们轻松地分派操作(您还记得我们在操作中使用了请求和成功方法吗?):
  // Base Actions
  export const request = ACTION => ({
    type: ACTION.request()
  });

  export const received = (ACTION, data) => ({
    type: ACTION.success(),
    payload: data
  });

File: src/shared/redux/baseActions.js

  1. 现在让我们创建我们的api文件夹,我们需要在其中添加constants.js文件和index.js文件:
 export const API = Object.freeze({
    TODO: 'api/todo/list'
  });

File: src/client/todo/api/constants.js

  1. 在我们的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;

File: src/client/todo/api/index.js

  1. 在我们的 Todo 容器中,我们需要映射 Todo 列表,并将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);

File: src/client/todo/container/index.js

  1. 我们的布局组件应如下所示:
  // 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;

File: src/client/todo/components/Layout.jsx

  1. 在这个配方中,我们不会看到布局组件(页眉、内容和页脚),因为它们非常通用,我们在过去的配方中使用过它们。现在,让我们创建 reducer 文件:
  // 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;
    }
  }

File: src/client/todo/reducer/index.js

  1. 我们的 Todo 组件将在 componentDidMount 方法中执行 fetchTodo 操作,然后将 Todo 列表呈现为 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;

File: src/client/todo/components/Todo.jsx

  1. 最后,我们需要为 todo 应用创建一个index.jsx文件,在该文件中,我们将添加 initialAction(这将返回一个承诺),以执行 fetchTodo 操作并使用 SSR 呈现该 todo 列表:
  // Dependencies
  import React from 'react';

  // Actions
  import { fetchTodo } from './actions';

  // Main Container
  import Container from './container';

 // Main Component
  const Main = props => <Container {...props} />;

  // Initial Action
  Main.initialAction = () => fetchTodo();

 export default Main;

File: src/client/todo/index.jsx

它是如何工作的。。。

正如您在我们的serverRender.js文件中所看到的,我们得到承诺并解决它们,然后使用 SSR 呈现我们的应用。

如果要测试应用,需要转到 http://localhost:3000/todo 在浏览器中。

请记住,在我们的应用中,搜索机器人和 curl 只使用 SSR,否则将使用 CSR。这是因为我们必须使用 SSR 的唯一原因是为了改进我们在谷歌、雅虎和必应的 SEO。

如果我们使用 CSR,我们执行操作的方式是 Todo 组件中的componentDidMount()方法;如果我们使用 SSR,我们将使用initialAction方法,它返回一个承诺,将在serverRender.js中解决。

如果打开该页面,您将看到以下内容:

如果您想查看 SSR 是否工作,可以使用curl命令在您的终端中执行相同的 URL:

如您所见,todo 列表缩减器已添加到initialState中,从那里,我们可以使用 SSR 呈现列表。

实现 Next.js

js 是一个用于服务器应用的最低限度的框架

在本教程中,我们将学习如何使用 Sass 实现 Next.js,我们还将使用 axios 从服务获取数据。

准备

首先,我们创建一个名为nextjs的新目录,初始化package.json,最后在其中创建一个新目录:

 mkdir nextjs
 cd nextjs
 npm init -y
 mkdir src

然后我们需要安装一些依赖项:

 npm install next react react-dom axios node-sass @zeit/next-sass

怎么做。。。

现在我们已经安装了依赖项,让我们创建第一个 Next.js 应用:

  1. 我们需要做的第一件事是在 package.json 中创建一些脚本。在每个脚本中,我们需要指定src目录。否则,它将尝试从根目录而不是从src路径开始下一步:
  "scripts": {
    "start": "next start src",
    "dev": "next src",
    "build": "next build src"
  }

File: package.json

  1. Next 中的主目录名为pages。在这里,我们将包括我们希望使用 Next 渲染的所有pages
 cd src && mkdir pages
  1. 我们需要创建的第一个页面是index.jsx
 const Index = () => <h1>Home</h1>;

  export default Index;

File: src/pages/index.jsx

  1. 现在,让我们使用 dev 脚本运行应用:
 npm run dev
  1. 如果一切正常,您应该在终端中看到:

  1. 打开http://localhost:3000

Next.js has its own Webpack configuration and hot reloading enabled. That means if you edit the index.js file you will see the changes reflected without refreshing the page.

  1. 现在,让我们创建一个关于页面,看看路由是如何工作的:
 const About = () => <h1>About</h1>;

  export default About;

File: src/pages/about.jsx

  1. 现在,如果转到,您将看到关于页面 http://localhost:3000/about. 如您所见,Next.js 会为我们创建的每个页面自动创建一个新路由。这意味着我们不需要安装 React 路由来处理路由。

In Next pages, it is not necessary to import React because it is automatically handled by Next as well. 

  1. 现在,我们需要创建一个next.config.js文件,并导入 WITHASS 方法,以便在我们的项目中使用 Sass。不幸的是,该文件需要用 ES5 语法编写,因为目前不支持使用 ES6 的 babel 扩展名(https://github.com/zeit/next.js/issues/2916
 const withSass = require('@zeit/next-sass');

  module.exports = withSass();

File: src/next.config.js In this file, we can also add custom Webpack configuration if we need it.

  1. 然后我们需要在pages目录中创建一个名为_document.js的特殊文件。此文件由 Next.js 自动处理,在这里我们可以定义文档的头部和正文:
  import Document, { Head, Main, NextScript } from 'next/document';

  export default class MyDocument extends Document {
    render() {
      return (
        <html>
          <Head>
            <title>Codejobs with Next</title>
            <link 
 rel="stylesheet" 
 href="/_next/static/style.css" />
          </Head>

          <body>
            <Main />
            <NextScript />
          </body>
        </html>
      );
    }
  }

File: src/pages/_document.js The path to the CSS file (/_next/static/style.css) is by default; we should use that one to use styles in our project.

  1. 现在我们可以创建一些组件来包装页面。我们需要创建的第一个选项是菜单选项的导航栏:
  import Link from 'next/link';
  import './Navbar.scss';

  const Navbar = () => (
    <div className="navbar">
      <ul>
        <li>Codejobs</li>
        <li><Link href="/">Home</Link></li>
        <li><Link href="/about">About</Link></li>
      </ul>
    </div>
  )
 export default Navbar;

File: src/components/Navbar.jsx The Link component is not the same as the React Router Link. There are a few differences; for example, the React Router Link uses the "to" prop and the Next Link uses "href" to specify the URL.

  1. 现在我们可以为我们的navbar添加 Sass 样式:
 .navbar {
    background: black;
    color: white;
    height: 60px;

    ul {
      padding: 0;
      margin: 0;
      list-style: none;

      li {
        display: inline-block;
        margin-left: 30px;
        text-align: center;

        a {
          display: block;
          color: white;
          line-height: 60px;
          width: 150px;

          &:hover {
            background: white;
            color: black;
          }
        }
      }
    }
  }

File: src/components/Navbar.scss

  1. 然后我们需要创建布局组件:
  import Navbar from './Navbar';
  import './Layout.scss';

  const Layout = ({ children }) => (
    <div className="layout">
      <Navbar />

      <div className="wrapper">
        {children}
      </div>
    </div>
  )

  export default Layout;

File: src/components/Layout.jsx

  1. 我们的布局样式如下所示:
  body {
    font-family: verdana;
    padding: 0;
    margin: 0;
  }

  .layout {
    a {
      text-decoration: none;
    }

    .wrapper {
      margin: 0 auto;
      width: 96%;
    }
  }

File: src/components/Layout.scss

  1. 您还记得第 5 章精通 Redux中关于从 CoinMarketCap(Repository: Chapter05/Recipe2/coinmarketcap中列出前 100 种加密货币的配方吗?在这个配方中,我们将使用 Next.js 执行相同的操作。我们需要做的第一件事是修改页面的index.js文件,并在getInitialProps方法中执行异步axios请求:
  import axios from 'axios';
  import Layout from '../components/Layout';
  import Coins from '../components/Coins';

  const Index = ({ coins }) => (
    <Layout>
      <div className="index">
        <Coins coins={coins} />
      </div>
    </Layout>
  );

  Index.getInitialProps = async () => {
    const url = 'https://api.coinmarketcap.com/v1/ticker/';
    const res = await axios.get(url);

    return {
      coins: res.data
    };
  };

 export default Index;

File: src/pages/index.js

  1. 现在让我们创建Coins组件:
  // Dependencies
  import React, { Component } from 'react';
  import { array } from 'prop-types';

  // Styles
  import './Coins.scss';

  const Coins = ({ coins }) => (
    <div className="Coins">
      <h1>Top 100 Coins</h1>

      <ul>
        {coins.map((coin, key) => (
          <li key={key}>
            <span className="left">{coin.rank} {coin.name} <strong>
            {coin.symbol}</strong></span>
            <span className="right">${coin.price_usd}</span>
          </li>
        ))}
      </ul>
    </div>
  );

  Coins.propTypes = {
    coins: array
  };

 export default Coins;

File: src/components/Coins.jsx

  1. Coins组件的样式如下:
 .Coins {
    h1 {
      text-align: center;
    }

    ul {
      margin: 0 auto;
      margin-bottom: 20px;
      padding: 0;
      list-style: none;
      width: 400px;

      li {
        border-bottom: 1px solid black;
        text-align: left;
        padding: 10px;
        display: flex;
        justify-content: space-between;

        a {
          display: block;
          color: #333;
          text-decoration: none;
          background: #5ed4ff;

          &:hover {
            color: #333;
            text-decoration: none;
            background: #baecff;
          }
        }
      }
    }
  }

File: src/components/Coins.scss

它是如何工作的。。。

现在我们已经创建了所有页面和组件,让我们通过运行npm run dev来测试下一个应用:

现在让我们看看它是如何在 HTML 视图中呈现的:

万岁!HTML 是用 SSR 呈现的,非常适合改进 SEO。正如您所看到的,使用 Next 创建应用非常快,并且我们在启用 SSR 时避免了大量配置。*