同构应用是指写一份代码但可同时在浏览器和服务器中运行的应用。css
如今大多数单页应用的视图都是经过 JavaScript 代码在浏览器端渲染出来的,但在浏览器端渲染的坏处有:html
为了解决以上问题,有人提出可否将本来只运行在浏览器中的 JavaScript 渲染代码也在服务器端运行,在服务器端渲染出带内容的 HTML 后再返回。
这样就能让搜索引擎爬虫直接抓取到带数据的 HTML,同时也能下降首屏渲染时间。
因为 Node.js 的流行和成熟,以及虚拟 DOM 提出与实现,使这个假设成为可能。前端
实际上如今主流的前端框架都支持同构,包括 React、Vue二、Angular2,其中最早支持也是最成熟的同构方案是 React。
因为 React 使用者更多,它们之间又很类似,本节只介绍如何用 Webpack 构建 React 同构应用。node
同构应用运行原理的核心在于虚拟 DOM,虚拟 DOM 的意思是不直接操做 DOM 而是经过 JavaScript Object 去描述本来的 DOM 结构。
在须要更新 DOM 时不直接操做 DOM 树,而是经过更新 JavaScript Object 后再映射成 DOM 操做。react
虚拟 DOM 的优势在于:webpack
以 React 为例,核心模块 react
负责管理 React 组件的生命周期,而具体的渲染工做能够交给 react-dom
模块来负责。git
react-dom
在渲染虚拟 DOM 树时有2中方式可选:github
render()
函数去操做浏览器 DOM 树来展现出结果。renderToString()
计算出表示虚拟 DOM 的 HTML 形式的字符串。构建同构应用的最终目的是从一份项目源码中构建出2份 JavaScript 代码,一份用于在浏览器端运行,一份用于在 Node.js 环境中运行渲染出 HTML。
其中用于在 Node.js 环境中运行的 JavaScript 代码须要注意如下几点:web
document
进行 DOM 操做, 由于 Node.js 不支持这些 API;node_modules
里的第三方模块和 Node.js 原生模块(例如 fs
模块)打包进去,而是须要经过 CommonJS 规范去引入这些模块。接下来改造在3-6使用 React 框架中介绍的 React 项目,为它增长构建同构应用的功能。算法
因为要从一份源码构建出2份不一样的代码,须要有2份 Webpack 配置文件分别与之对应。
构建用于浏览器环境的配置和前面讲的没有差异,本节侧重于讲如何构建用于服务端渲染的代码。
用于构建浏览器环境代码的 webpack.config.js
配置文件保留不变,新建一个专门用于构建服务端渲染代码的配置文件 webpack_server.config.js
,内容以下:
const path = require('path'); const nodeExternals = require('webpack-node-externals'); module.exports = { // JS 执行入口文件 entry: './main_server.js', // 为了避免把 Node.js 内置的模块打包进输出文件中,例如 fs net 模块等 target: 'node', // 为了避免把 node_modules 目录下的第三方模块打包进输出文件中 externals: [nodeExternals()], output: { // 为了以 CommonJS2 规范导出渲染函数,以给采用 Node.js 编写的 HTTP 服务调用 libraryTarget: 'commonjs2', // 把最终可在 Node.js 中运行的代码输出到一个 bundle_server.js 文件 filename: 'bundle_server.js', // 输出文件都放到 dist 目录下 path: path.resolve(__dirname, './dist'), }, module: { rules: [ { test: /\.js$/, use: ['babel-loader'], exclude: path.resolve(__dirname, 'node_modules'), }, { // CSS 代码不能被打包进用于服务端的代码中去,忽略掉 CSS 文件 test: /\.css/, use: ['ignore-loader'], }, ] }, devtool: 'source-map' // 输出 source-map 方便直接调试 ES6 源码 };
以上代码有几个关键的地方,分别是:
target: 'node'
因为输出代码的运行环境是 Node.js,源码中依赖的 Node.js 原生模块不必打包进去;externals: [nodeExternals()]
webpack-node-externals 的目的是为了防止 node_modules 目录下的第三方模块被打包进去,由于 Node.js 默认会去 node_modules 目录下寻找和使用第三方模块;{test: /\.css/, use: ['ignore-loader']}
忽略掉依赖的 CSS 文件,CSS 会影响服务端渲染性能,又是作服务端渲不重要的部分;libraryTarget: 'commonjs2'
以 CommonJS2 规范导出渲染函数,以供给采用 Node.js 编写的 HTTP 服务器代码调用。为了最大限度的复用代码,须要调整下目录结构:
把页面的根组件放到一个单独的文件 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
文件内容以下:
import React from 'react'; import { renderToString } from 'react-dom/server'; import { AppComponent } from './AppComponent'; // 导出渲染函数,以给采用 Node.js 编写的 HTTP 服务器代码调用 export function render() { // 把根组件渲染成 HTML 字符串 return renderToString(<AppComponent/>) }
为了能把渲染的完整 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
以上全部准备工做已经完成,接下来执行构建,编译出目标文件:
webpack --config webpack_server.config.js
构建出用于服务端渲染的 ./dist/bundle_server.js
文件。webpack
构建出用于浏览器环境运行的 ./dist/bundle_browser.js
文件,默认的配置文件为 webpack.config.js
。构建执行完成后,执行 node ./http_server.js
启动 HTTP 服务器后,再用浏览器去访问 http://localhost:3000
就能看到 Hello,Webpack
了。
可是为了验证服务端渲染的结果,你须要打开浏览器的开发工具中的网络抓包一栏,再从新刷新浏览器后,就能抓到请求 HTML 的包了,抓包效果图以下:
能够看到服务器返回的是渲染出内容后的 HTML 而不是 HTML 模版,这说明同构应用的改造完成。
本实例 提供项目完整代码