深入浅出webpack学习(16)--认识同构应用

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

认识同构应用

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

搜索引起无法收录你的网页,因为展示的数据都是在浏览器端异步渲染出来的,大多数爬虫无法获取到这些数据。

对于复杂的单页应用,渲染过程计算量大,对低端移动设备来说可能有性能问题,用户能明显感知首屏的渲染延迟。

同构应用运行原理的核心在于虚拟DOM, 虚拟DOM的优点在于:

  1. 因为操作 DOM 树是高耗时的操作,尽量减少 DOM 树操作能优化网页性能。而 DOM Diff 算法能找出2个不同 Object 的最小差异,得出最小 DOM 操作;
  2. 虚拟DOM的在渲染的时候不仅仅可以通过操作DOM树来表示结果,也能有其他的表示方法。例如虚拟DOM渲染成字符串(服务器渲染)等。

以react为例子,核心模块react负责管理react组件的生命周期,而具体的渲染工作可以交给react-dom模块来负责。
react-dom在渲染虚拟dom树时有2种方式可选:

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

构建同构应用的最终目的是从一份项目源码中构建出2份JavaScript代码。一份用于在node环境中运行渲染出HTML。其中用于在node环境中运行的JavaScript代码需要注意:

  1. 不能包含浏览器环境提供的API;
  2. 不能包含css代码,因为服务端渲染的目的是渲染html内容, 渲染出css代码会增加额外的计算量,影响服务端渲染;
  3. 不能像用于浏览器环境的输出代码那样把node_modules里的第三方模块和nodejs原生模块打包进去,而是需要通过commonjs规范去引入这些模块。
  4. 需要通过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!')
});

相关推荐