同构应用是指写一份代码但可同时在浏览器和服务器中运行的应用。css
大多数单页应用的视图都是经过JavaScript代码在浏览器端渲染出来,可是在浏览器端渲染的坏处有:html
搜索引发没法收录你的网页,由于展现的数据都是在浏览器端异步渲染出来的,大多数爬虫没法获取到这些数据。对于复杂的单页应用,渲染过程计算量大,对低端移动设备来讲可能有性能问题,用户能明显感知首屏的渲染延迟。node
同构应用运行原理的核心在于虚拟DOM, 虚拟DOM的优势在于:react
- 由于操做 DOM 树是高耗时的操做,尽可能减小 DOM 树操做能优化网页性能。而 DOM Diff 算法能找出2个不一样 Object 的最小差别,得出最小 DOM 操做;
- 虚拟DOM的在渲染的时候不单单能够经过操做DOM树来表示结果,也能有其余的表示方法。例如虚拟DOM渲染成字符串(服务器渲染)等。
以react为例子,核心模块react负责管理react组件的生命周期,而具体的渲染工做能够交给react-dom模块来负责。
react-dom在渲染虚拟dom树时有2种方式可选:webpack
- 经过render()函数去操做浏览器DOM树来展现出结果;
- 经过renderToString()计算出表示虚拟DOM的HTML形式的字符串;
构建同构应用的最终目的是从一份项目源码中构建出2份JavaScript代码。一份用于在node环境中运行渲染出HTML。其中用于在node环境中运行的JavaScript代码须要注意:web
- 不能包含浏览器环境提供的API;
- 不能包含css代码,由于服务端渲染的目的是渲染html内容, 渲染出css代码会增长额外的计算量,影响服务端渲染;
- 不能像用于浏览器环境的输出代码那样把node_modules里的第三方模块和nodejs原生模块打包进去,而是须要经过commonjs规范去引入这些模块。
- 须要经过commonjs规范导出一个渲染函数,以用于在HTTP服务器中执行这个渲染函数,渲染出HTML内容返回。
因为要从一份源码构建出2份不一样的代码,须要2份webpack配置文件分别与之对应。构建用于浏览器环境的配置和前面讲的没有差异,主要侧重讲如何构建用于服务端渲染的代码。算法
建立一个用于构建服务端渲染代码的配置文件webpack_server.config.js内容以下:express
const path = require("path"); const nodeExternals = require("webpack-node-externals"); module.exports = { //js执行入口文件 entry: "./main_server.js", //为了避免把nodejs内置模块打包进输出文件中,例如: fs net模块等; target: "node", //为了避免把node_modeuls目录下的第三方模块打包进输出文件中 externals: [nodeExternals()], output: { //为了以commonjs2规范导出渲染函数,以给采用nodejs编写的HTTP服务调用 libraryTarget: "commonjs2", //把最终可在nodejs运行的代码输出到一个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' }
以上代码有几个关键的地方,分别是:浏览器
1. target: 'node' 因为输出代码的运行环境是node,源码中依赖的node原生模块不必打包进去; 2. externals: [nodeExternals()] webpack-node-externals的目的是为了防止node_modules目录下的第三方模块被打包进去,由于nodejs默认会去node_modules目录下去寻找和使用第三方模块。 3. {{test: /\.css/, use: ['ignore-loader']}忽略掉依赖的css文件,css会影响服务端渲染性能,又是作服务端渲染不重要的部分; 4. libraryTarget: 'commonjs2'以commonjs2规范导出渲染函数,以供给采用nodejs编写的http服务器代码调用。
为了最大限度的服用代码,须要调整目下目录结构:
把页面的根组件放到一个单独的文件AppComponent.js,该文件只能包含根组件的代码,不能包含渲染入口的代码,并且须要导出根组件以供给渲染入口调用。服务器
import React, { Component } from 'react'; import "./main.css" export class AppComponent extends Component { render() { return <h1>hello webpack</h1> } }
分别为不一样环境的渲染入口写两份不一样的文件,分别是用于浏览器端渲染DOM的main_brwser.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' //导出渲染函数, 以采用nodejs编写http服务器代码调用 export function render() { // 把根组件渲染成 HTML 字符串 return renderToString(<AppComponent/>) }
为了能把渲染的完整html文件经过http服务返回给请求端,还须要经过node启动一个http服务器,用express来实现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!') });