Skip to content

Latest commit

 

History

History
242 lines (176 loc) · 6.47 KB

2018-12-16.md

File metadata and controls

242 lines (176 loc) · 6.47 KB

Next 服务端同构实践

背景

新博客是使用 React 进行客户端渲染的,考虑到 SEO 的问题,决定改用服务端渲染来进行 SEO 优化

选型

React 发展至今,其服务端渲染已经有很成熟的方案,比如 Next ,但是对于那些比较简单的 React 应用,其实不用框架反而会更简单一些。

服务端渲染的原理其实就是在 Node 层调用 React 提供的 renderToString 生成 html 作为响应体返回客户端

// server.js
import express from 'express';
const app = express();

import { renderToString } from 'react-dom/server';

app.get('/', (req, res) => {
  const content = renderToString(
        <div>
            Hello World!
        </div>
    );

  res.send(content);
});

对于需要从服务端异步获取数据的组件,只要对服务端暴露一个获取数据的方法即可

// UserList.js
import { fetchUsers } from "../actions/index";

class UsersList extends Component {
    componentDidMount() {
        this.props.fetchUsers();
    }

    renderUsers() {
        return this.props.users.map(user => {
            return <li key={user.id}>{user.name}</li>
        })
    }

    render() {

        return (
            <div>
                <ul>
                    {this.renderUsers()}
                </ul>
            </div>
        )
    }
}

function mapStateToProps(state) {
    return {
        users: state.users
    }
}

function loadData(store) {
    return store.dispatch(fetchUsers());
}
export default {
    component: connect(mapStateToProps, {
        fetchUsers
    })(UsersList),
    loadData
};

// server.js
app.get('/', (req, res) => {
  const promises = matchRoutes(Routes, req.path).map(({ route }) => {
    const {
        loadData
    } = route; // 检测是否需要 loadData 

        return loadData ? loadData(store) : null;
    }).map(promise => {
        if(promise) {
            return new Promise((resolve) => {
                promise.then(resolve).catch(resolve);
            })
        }
    });

    Promise.all(promises).then(() => {
        const content = renderToString(
            <Provider store={store}>
                <StaticRouter location={req.path} context={context}>
                    <div>
                        {renderRoutes(Routes)}
                    </div>
                </StaticRouter>
            </Provider>
        );

        res.send(content);
    });
});

之前学习服务端渲染的时候写过一个 demo 实现了 Express + React + Redux + React-Router 的 服务端渲染,如果想自己实现 ssr 可以作为参考 React-SSR

最后本着要顺便学习 Next 的目的,还是决定使用 Next 来实现服务端渲染。

环境搭建

第一件事当然是安装 Next

$ npm next @zeit/next-css @zeit/next-less next-images --save

除了 Next 还顺便安装了用于处理样式和图片的插件,基本上做一个应用离不开这些依赖。

对于全新的项目,Next 的安装会非常顺利,然而在我的实践中,遇到了一个坑。

因为项目之前是使用 webpack 作为打包工具,所以已经安装了 webpackwebpack-cli 还有各种各样 loader,但是 Next 本地也内置了 webpack 「全家桶」,所以在安装完 Next 之后,项目中就存在两个版本的 webpack 编译的时候将会有一个又一个错等着你,所以对于已有项目的升级,建议安装 next 之前,遵循以下步骤

  1. 删掉 package.json 里跟 webpack 有关的依赖,
  2. 删掉 package-lock.json
  3. 删掉 node_modules
  4. npm install 一次
  5. 安装 Next 相关依赖

基本使用

Next 的 文档 已经提供了非常详细的使用说明,开发上和 React 基本没什么区别,只是在 Next 并不需要声明 React

// import React from 'react';
export default () => <div>Welcome to next.js!</div>

当在命令行运行 Next 的时候

$ next

Next 会根据默认目录 pages 中的 js 文件生成对应页面路由服务

pages/index.js 对应 {domain}/

pages/about.js 对应 {domain}/about

对于需要从服务端异步获取数据的组件,只要为组件实现一个 getInitialProps 的静态方法即可

class PostListPage extends Component {
  static async getInitialProps () {
    const posts = await getPostsList();

    return {
      posts
    }
  }

  constructor(props) {
    super(props);
  }

  render() {
    const {
      posts
    } = this.props;

    return posts.map(post => {
      // ...
    })
    )
  }
}

同时 Next 提供了客户端路由支持,因此你的应用不再需要 React-Router

import Link from 'next/link';

const Navigator = () =>
  <nav>
    <Link href="/"><a>index</a></Link>
    <Link href="/about"><a>about</a></Link>
  </nav>;

export default Navigator;

自定义 Next 配置

在根目录创建 next.config.js 修改 Next 的默认配置,或者进行插件增强,比如 Next 默认只支持 CSS in JS,如果要支持 import css 文件或者使用 less 等预编译器,就需要用到插件

const withLess = require('@zeit/next-less');

module.exports = withLess();

再比如 Next 默认不支持通过 import 引入图片,这时候就需要 next-images

const withLess = require('@zeit/next-less');
const withImages = require('next-images');

module.exports = withImages(withLess());

你也可以自己修改 Next 内部的 webpack 配置

const withLess = require('@zeit/next-less');
const withImages = require('next-images');

module.exports = withImages(withLess({
  webpack (config) {
    // 对 config 进行修改
    return config;
}));

参考我在开发过程遇到的一个 场景

融合 Egg

Next 只提供简单的客户端路由,为了程序扩展性更强,应该搭配专业的 Node 服务端框架使用,目前项目是使用 Egg 作为服务端,需要将 Next 和 Egg 进行融合。

方案参考 这个 即可

上线

运行 next build 即可

总结

博客升级到 Next 一共用了不到一天的时间,可见 Next 还是挺强大的,当你看到这篇文章的时候,看到的已经是一个服务端渲染的 JavaScript 同构应用了。