深入浅出webpack学习(16)--认识同构应用
同构应用是指写一份代码但可同时在浏览器和服务器中运行的应用。
认识同构应用
大多数单页应用的视图都是通过JavaScript代码在浏览器端渲染出来,但是在浏览器端渲染的坏处有:
搜索引起无法收录你的网页,因为展示的数据都是在浏览器端异步渲染出来的,大多数爬虫无法获取到这些数据。对于复杂的单页应用,渲染过程计算量大,对低端移动设备来说可能有性能问题,用户能明显感知首屏的渲染延迟。
同构应用运行原理的核心在于虚拟DOM, 虚拟DOM的优点在于:
- 因为操作 DOM 树是高耗时的操作,尽量减少 DOM 树操作能优化网页性能。而 DOM Diff 算法能找出2个不同 Object 的最小差异,得出最小 DOM 操作;
- 虚拟DOM的在渲染的时候不仅仅可以通过操作DOM树来表示结果,也能有其他的表示方法。例如虚拟DOM渲染成字符串(服务器渲染)等。
以react为例子,核心模块react负责管理react组件的生命周期,而具体的渲染工作可以交给react-dom模块来负责。
react-dom在渲染虚拟dom树时有2种方式可选:
- 通过render()函数去操作浏览器DOM树来展示出结果;
- 通过renderToString()计算出表示虚拟DOM的HTML形式的字符串;
构建同构应用的最终目的是从一份项目源码中构建出2份JavaScript代码。一份用于在node环境中运行渲染出HTML。其中用于在node环境中运行的JavaScript代码需要注意:
- 不能包含浏览器环境提供的API;
- 不能包含css代码,因为服务端渲染的目的是渲染html内容, 渲染出css代码会增加额外的计算量,影响服务端渲染;
- 不能像用于浏览器环境的输出代码那样把node_modules里的第三方模块和nodejs原生模块打包进去,而是需要通过commonjs规范去引入这些模块。
- 需要通过commonjs规范导出一个渲染函数,以用于在HTTP服务器中执行这个渲染函数,渲染出HTML内容返回。
解决方案
由于要从一份源码构建出2份不同的代码,需要2份webpack配置文件分别与之对应。构建用于浏览器环境的配置和前面讲的没有差别,主要侧重讲如何构建用于服务端渲染的代码。
创建一个用于构建服务端渲染代码的配置文件webpack_server.config.js内容如下:
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!') });
相关推荐
Vue和React是数据驱动视图,如何有效控制DOM操作?能不能把计算,更多的转移为js计算?因为js执行速度很快。patch函数-->patch,对比tag,对比tag与key,对比children