同构

Bing 的一项研究表明:页面的加载时间每增加 10ms,站点年度总收入就会减少 $250K。

认识同构应用

image

现在大多数单页应用的视图都是通过 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 的优点在于:

  • 因为操作 DOM 树是高耗时的操作,尽量减少 DOM 树操作能优化网页性能。而 DOM Diff 算法能找出2个不同 Object 的最小差异,得出最小 DOM 操作;
  • 虚拟 DOM 的在渲染的时候不仅仅可以通过操作 DOM 树来表示出结果,也能有其它的表示方式,例如把虚拟 DOM 渲染成字符串(服务器端渲染),或者渲染成手机 App 原生的 UI 组件( React Native)。

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

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

  • 通过 render() 函数去操作浏览器 DOM 树来展示出结果。
  • 通过 renderToString() 计算出表示虚拟 DOM 的 HTML 形式的字符串。

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

  • 不能包含浏览器环境提供的 API,例如使用 document 进行 DOM 操作,  因为 Node.js 不支持这些 API;
  • 不能包含 CSS 代码,因为服务端渲染的目的是渲染出 HTML 内容,渲染出 CSS 代码会增加额外的计算量,影响服务端渲染性能;
  • 不能像用于浏览器环境的输出代码那样把 node_modules 里的第三方模块和 Node.js 原生模块(例如 fs 模块)打包进去,而是需要通过 CommonJS 规范去引入这些模块。
  • 需要通过 CommonJS 规范导出一个渲染函数,以用于在 HTTP 服务器中去执行这个渲染函数,渲染出 HTML 内容返回。

解决方案

接下来改造在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](http://localhost:3000/) 就能看到 Hello,Webpack 了。 但是为了验证服务端渲染的结果,你需要打开浏览器的开发工具中的网络抓包一栏,再重新刷新浏览器后,就能抓到请求 HTML 的包了,抓包效果图如下:

[图片上传失败...(image-6ffefa-1571725007894)]

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

相关链接:
https://juejin.im/entry/5b1631085188257d492adc9e http://webpack.wuhaolin.cn/3%E5%AE%9E%E6%88%98/3-11%E6%9E%84%E5%BB%BA%E5%90%8C%E6%9E%84%E5%BA%94%E7%94%A8.html

http://www.aliued.com/?p=3077

https://www.jianshu.com/p/5ce23647e7e3

https://github.com/camsong/blog/issues/8

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容