2 React前端工程架构 - 开发环境实时更新

 

dev server & hot load相关commit

相关commit

解决hot load缓慢的commit

服务端实时渲染相关commit

服务端实时渲染

0x0 问题 & 解决方案概述

问题一:每次client代码改动,都需要手动重新编译打包,导致开发效率低下

工具:webpack dev server

问题二:每次代码变动,都需要刷新浏览器才能看到效果,开发效率低下

工具:hot module replacement

0x1 动态编译

1 功能启动

安装webpack-dev-server

npm i webpack-dev-server -D

修改文件

webpack.config.client.js

...
const isDev = process.env.NODE_ENV === "development";
...
const config = {
    ...
    mode: 'production'
};
if (isDev) {
    config.devServer = {
        host: '0.0.0.0',
        port: '8787',
        contentBase: path.join(__dirname, '../dist'),
        hot: false,
        overlay: {
            error: true
        },
        publicPath: '/public',
        historyApiFallback: {
            index: '/public/index.html'
        },
    }
}

module.exports = config;

说明:

  1. 通过启动webpack的时,传入的参数,判断当前环境
  2. 若为dev,则配置devserver
    1. contentBase应该和楼上的output的path一致
    2. hot:是否启动hot module replacement
      • 因为还没做相关配置,暂时关掉
    3. overlay:发生哪些情况,需要在浏览器弹窗提示
    4. publicPath:把静态资源自动挂载到publicPath路径下
      • 应该和config中的publicPath对应
    5. historyApiFallback:history中可以配置很多对应关系
      1. 配置之后,我们所有无法返回的404的内容,都会返回给浏览器这个index.html文件
  3. 【注意】跑dev服务器之前,需要干掉之前本地编译生成的/dist目录
    • dev server会检测硬盘上有木有dist这个目录,有的话直接访问这个目录下的js文件,没办法动态编译了

 配置服务器启动方法

{
  ...
  "scripts": {
    ...
    "dev:client": "cross-env NODE_ENV=development  webpack-dev-server --config build/webpack.config.client.js",
    ...
},

说明:

效果:

0x2 热更新

配置好dev server之后,可以实现「自动编译」功能

热更新希望达成的效果,是「不用刷新页面」展示修改后的代码

1 配置

.babelrc

{
  ...
  "plugins": [
    "react-hot-loader/babel"
  ]
}

安装

@next表示是测试版,还没发布到主包

npm i react-hot-loader@next -D

重新组织app.js

...
import { AppContainer } from 'react-hot-loader'

const root = document.getElementById("root")
const render = (Component) => {
    ReactDOM.render(
        <AppContainer>
            <Component />
        </AppContainer>
        ,
        root
    )
};

render(App);

if (module.hot) {
     module.hot.accept('./App.jsx', () => {
         const NextApp = require('./App.jsx').default;
         render(NextApp)
     })
}

说明:

  1. 自定义了一个render方法,用来实现,把组件挂载到id为root的标签中
    • AppContainer:实现hot load必须要用这个标签包裹整个App才行
  2. if语句部分
    • 逻辑:当需要热更新的时候,需要重新渲染整个App


基本功能完成,但是有很多warning,而且刷新很慢,下面为优化方法 - 设置mode


webpack.config.client.js

...
if (isDev) {
    config.entry = {
        app: [
            'react-hot-loader/patch',
            path.join(__dirname, '../client/app.js')
        ]
    };

    config.devServer = {
        ...
        hot: true,
        ...
    };

    config.mode = 'development';
    
    config.plugins.push(new webpack.HotModuleReplacementPlugin())
}
...

说明:

  1. webpack的entry可以是一个数组,代表这个entry里面包含了很多引用文件,需要打包到同一个文件中去
    • 这里是要把hot-loader的patch打到app.js中
    • 以实现hot load功能
  2. 需要打开hot选项
  3. 需要把模式设置为development,否则webpack会默认当前为生产环境,导致hot load的时候异常缓慢
  4. 需要把hotmodule插件,配置到plugins里

0x3 实时服务端渲染

本节Commit

开发过程中,也有「实时服务端渲染」需求

服务端渲染:将实时编译好 & 存在dev server中的「服务端js-bundle」,和dev server中的index.html结合起来,返回最终内容

server.js:渲染服务器,需要区分是否为开发环境

const express = require('express');
const ReactSSR = require('react-dom/server');
const fs = require('fs');
const path = require('path');

const isDev = process.env.NODE_ENV === "development";
const app = express();

if (!isDev) {
    const template = fs.readFileSync(path.join(__dirname, '../dist/index.html'), 'utf-8');
    const serverEntry = require('../dist/server-entry.js').default;
    app.use('/public', express.static(path.join(__dirname, '../dist')));
    app.get('*', function (req, res) {
        const appString = ReactSSR.renderToString(serverEntry);
        res.send(template.replace('<!--app-->', appString))
    });
} else {
    const devStatic = require('./util/dev-static');
    devStatic(app);
}

app.listen(2333, function () {
   console.log('🚀 服务起来了,正在监听2333端口(✌゚∀゚)☞')
});


server/util/dev-static.js:用来实现server的dev环境下相关逻辑

part I:获取index.html

const axios = require('axios');

const getTemplate = () => {
    return new Promise((resolve, reject) => {
        axios.get('http://localhost:8787/public/index.html')
            .then((res) => {
                resolve(res.data)
            })
            .catch(reject)
    })
};

需要返回渲染完成之后的内容。我们知道,服务端渲染需要两个东东

  1. 编译好的server端jsbundle
  2. index.html(注入了client端jsbundle的)

part II:获取编译好的server-jsbundle

... part I ...


const webpack = require('webpack');
const path = require('path');
const serverConfig = require('../../build/webpack.config.server'); // #1

const serverCompiler = webpack(serverConfig);   // #2

const MemoryFs = require('memory-fs');
const mfs = new MemoryFs;
serverCompiler.outputFileSystem = mfs;  //  #4

const Module = module.constructor;
let serverBundle;   //  #5

serverCompiler.watch({}, (err, stats) => {
    if (err) throw err;
    stats = stats.toJson();
    stats.errors.forEach(err => console.error(err));
    stats.warnings.forEach(warn => console.warn(warn));

    const bundlePath = path.join(serverConfig.output.path, serverConfig.output.filename)    // #3
    const serverBundleString = mfs.readFileSync(bundlePath, 'utf-8');   //  #4
    
    const m = new Module();
    m._compile(serverBundleString, 'server-entry.js');
    serverBundle = m.exports.default;   //  #5
});

const ReactDomServer = require('react-dom/server'); //  #6

module.exports = function (app) {
     app.get('*', function (req, res) {
         getTemplate().then((index) => {
             const appString = ReactDomServer.renderToString(serverBundle);
             res.send(index.replace('<!--app-->', appString));
         })
     })
};

说明:

  1. 拿到webpack的server配置文件
  2. 通过配置,启动一个webpack的编译器
    • 可以监听entry下的文件是否有变化,一旦变化会重新打包
    • 这一部分,为啥能这样做呢?
      • 因为webpack本身就提供了一个在nodejs中作为模块调用的方式
      • 它不仅仅是一个命令行打包工具
      • 还让我们在服务端渲染的时候,能拿到它打包出来的内容,用于服务端渲染
  3. 拿到打出来的jsbundle的Path
  4. 让文件的读写都在内存中执行,而非Disk,以提高速度
    • 工具:memory-fs
    • 安装:`npm i memory-fs -D`
    • 注意:读取出来的是编译好的string,而非可执行的module,需要把string构建成module才行
    • 提示:MemoryFs和nodejs中disk FS的api是一致的,只不过实现做了替换
      • 输出到内存而不是硬盘
  5. string构建module
    • 拿到当前module的构造器
    • 用构造器把string构造成真正的module
      • 使用这个api的时候,需要指定它的名字
      • 指定文件名,不然无法在缓存中存储这部分内容
      • 指定文件名后,下次读这部分内容,才能拿到它
    • 获得编译好的bundle
      • 注意:需要先exports出来
      • 这个模块是通过exports挂载从模块里扔出来的东西的,所以想要获取东西,一定需要先exports
  6. 使用react-dom/server,把拿到的index.html和server-js-bundle渲染成最终html


配置npm启动脚本

package.json

{
    ...
    "dev:web:start": "cross-env NODE_ENV=development node server/server.js"
    ...
}


问题:生成的html文件中,引入的js文件不能正确请求 - 请求返回值是index.html,而不是我们需要的js文件真正内容

解决方案:

server.js 修饰

...
module.exports = function (app) {
    const proxy = require('http-proxy-middleware');
    app.use('/public', proxy({target: 'http://localhost:8787'}));
    ...
};

说明:

启动


warning相关参考文档:

  1. Warning: render(): Target node has markup rendered by React...