构建同构应用

同构应用是指写一份代码但可同时在浏览器和服务器中运行的应用。

认识同构应用

现在大多数单页应用的视图都是通过 JavaScript 代码在浏览器端渲染出来的,但在浏览器端渲染的坏处有:

为了解决以上问题,有人提出能否将原本只运行在浏览器中的 JavaScript 渲染代码也在服务器端运行,在服务器端渲染出带内容的 HTML 后再返回。 这样就能让搜索引擎爬虫直接抓取到带数据的 HTML,同时也能降低首屏渲染时间。 由于 Node.js 的流行和成熟,以及虚拟 DOM 提出与实现,使这个假设成为可能。

实际上现在主流的前端框架都支持同构,包括 React、Vue2、Angular2,其中最先支持也是最成熟的同构方案是 React。 由于 React 使用者更多,它们之间又很相似,本节只介绍如何用 Webpack 构建 React 同构应用。

同构应用运行原理的核心在于虚拟 DOM,虚拟 DOM 的意思是不直接操作 DOM 而是通过 JavaScript Object 去描述原本的 DOM 结构。 在需要更新 DOM 时不直接操作 DOM 树,而是通过更新 JavaScript Object 后再映射成 DOM 操作。

虚拟 DOM 的优点在于:

以 React 为例,核心模块 react 负责管理 React 组件的生命周期,而具体的渲染工作可以交给 react-dom 模块来负责。

react-dom 在渲染虚拟 DOM 树时有2中方式可选:

构建同构应用的最终目的是从一份项目源码中构建出2份 JavaScript 代码,一份用于在浏览器端运行,一份用于在 Node.js 环境中运行渲染出 HTML。 其中用于在 Node.js 环境中运行的 JavaScript 代码需要注意以下几点:

解决方案

用于构建浏览器环境代码的 webpack.config.js 配置文件保留不变,新建一个专门用于构建服务端渲染代码的配置文件 webpack_server.config.js,内容如下:

See the Pen webpack_server.config.js by whjin (@whjin) on CodePen.

以上代码有几个关键的地方,分别是:

为了最大限度的复用代码,需要调整下目录结构:

把页面的根组件放到一个单独的文件 AppComponent.js,该文件只能包含根组件的代码,不能包含渲染入口的代码,而且需要导出根组件以供给渲染入口调用,AppComponent.js 内容如下:

import React, { Component } from 'react';
import './main.css';

export class AppComponent extends Component {
  render() {
    return <h1>Hello,Webpack</h1>
  }
}

分别为不同环境的渲染入口写两份不同的文件,分别是用于浏览器端渲染 DOM 的 main_browser.js 文件,和用于服务端渲染 HTML 字符串的 main_server.js 文件。

main_browser.js 文件内容如下:

import React from 'react';
import { render } from 'react-dom';
import { AppComponent } from './AppComponent';

// 把根组件渲染到 DOM 树上
render(<AppComponent/>, window.document.getElementById('app'));

main_server.js 文件内容如下:

为了能把渲染的完整 HTML 文件通过 HTTP 服务返回给请求端,还需要通过用 Node.js 编写一个 HTTP 服务器。 由于本节不专注于将 HTTP 服务器的实现,就采用了 ExpressJS 来实现,http_server.js 文件内容如下:

const express = require('express');
const { render } = require('./dist/bundle_server');
const app = express();

// 调用构建出的 bundle_server.js 中暴露出的渲染函数,再拼接下 HTML 模版,形成完整的 HTML 文件
app.get('/', function (req, res) {
  res.send(`
<html>
<head>
  <meta charset="UTF-8">
</head>
<body>
<div id="app">${render()}</div>
<!--导入 Webpack 输出的用于浏览器端渲染的 JS 文件-->
<script src="./dist/bundle_browser.js"></script>
</body>
</html>
  `);
});

// 其它请求路径返回对应的本地文件
app.use(express.static('.'));

app.listen(3000, function () {
  console.log('app listening on port 3000!')
});

再安装新引入的第三方依赖:

# 安装 Webpack 构建依赖
npm i -D css-loader style-loader ignore-loader webpack-node-externals
# 安装 HTTP 服务器依赖
npm i -S express

以上所有准备工作已经完成,接下来执行构建,编译出目标文件:

构建执行完成后,执行 node ./http_server.js 启动 HTTP 服务器后,再用浏览器去访问 http://localhost:3000 就能看到 Hello,Webpack 了。 但是为了验证服务端渲染的结果,你需要打开浏览器的开发工具中的网络抓包一栏,再重新刷新浏览器后,就能抓到请求 HTML 的包了,抓包效果图如下:

可以看到服务器返回的是渲染出内容后的 HTML 而不是 HTML 模版,这说明同构应用的改造完成。

本实例提供项目完整代码