陌小路的个人博客 陌小路的个人博客
首页
  • 技术专区

    • 面试
    • Vue
    • Electron
    • TypeScript
    • Serverless
    • GraphQL
  • 我的秋招之旅
  • 2019年终总结
Todo
收藏夹
关于作者
GitHub

陌小路

前端切图仔
首页
  • 技术专区

    • 面试
    • Vue
    • Electron
    • TypeScript
    • Serverless
    • GraphQL
  • 我的秋招之旅
  • 2019年终总结
Todo
收藏夹
关于作者
GitHub
  • Vue

    • Vue3-beta-初体验
    • Vue-nextTick源码分析
    • Vuex-源码分析01
    • Vuex-源码分析02
    • Vite源码分析
    • Vue服务端渲染

      • 介绍
      • 快速上手
      • Vue项目添加SSR
      • 服务端搭建
      • 一键式启动项目与热更新
        • 总结
      • Petite-Vue
    • React

    • 面试

    • Electron

    • Serverless

    • GraphQL

    • TypeScript

    • RxJS

    • 工程化

    • Webpack

    • Nestjs

    • WebRTC & P2P

    • Docker

    • Linux

    • Git

    • Svelte

    • 踩坑日记 & 小Tips

    • 其他

    • technology
    • Vue
    陌小路
    2021-01-30

    一键式启动项目与热更新

    # 痛点

    在经受完前面一堆代码的洗礼之后,相信读者或许感受到启动项目的不方便了,首先先得将业务代码按照两份配置构建一遍,接着再来启动服务,这让我们在调试过程中十分不友好。

    不仅如此,我们在修改了前端页面代码之后,我们还得重新构建一遍,然后再启动项目,即便是只添加了一个字,我们都需要重复走一遍这个逻辑,是不是十分的难受。

    # 需求

    针对上述问题,这里提出几点优化目标:

    1. 首先我们能否一键自动构建并启动项目,解决繁琐的启动流程。
    2. 其次,是否能实现热更新能力,页面修改内容之后能无刷新更新页面。
    3. 在具备了前端代码热更新能力之后,我们修改了webpack相关配置文件是否也能自动重新构建并重启服务。

    # 优化

    针对上述优化目标,这里也将逐一进行解决。

    为了实现一键启动项目并实时打包构建,这里需要修改一下服务端代码,集成webpack打包能力,话不多说,先上码:

    # 完整代码

    const { createBundleRenderer } = require('vue-server-renderer');
    const express = require('express');
    const app = express();
    const router = express.Router();
    const chalk = require('chalk');
    const fs = require('fs');
    const template = fs.readFileSync('./index.html', 'utf-8')
    const path = require('path');
    let clientManifest = require(path.resolve(__dirname, 'dist', 'vue-ssr-client-manifest.json'));
    let serverBundle = require(path.resolve(__dirname, 'dist', 'vue-ssr-server-bundle.json'));
    
    // ------可以从这开始看
    const webpack = require('webpack');
    const WebpackDevMiddleware = require('webpack-dev-middleware');
    const WebpackHotMiddleware = require('webpack-hot-middleware');
    const clientWebpackConfig = require('./build/webpack.client.config');
    const serverWebpackConfig = require('./build/webpack.server.config')
    const clientCompiler = webpack(clientWebpackConfig);
    const serverCompiler = webpack(serverWebpackConfig);
    let renderer = {};
    let buildCount = 0;
    
    // 客户端构建
    const clientMiddleware = WebpackDevMiddleware(clientCompiler)
    app.use(clientMiddleware);
    
    clientCompiler.hooks.done.tap('compilerDone', () => {
        console.log('客户端构建完成')
        clientManifest = JSON.parse(clientMiddleware.context.outputFileSystem.readFileSync(path.join(clientWebpackConfig.output.path, 'vue-ssr-client-manifest.json')).toString())
        runBuildRenderer();
    })
    
    // 服务端构建
    const serverMiddleware = WebpackDevMiddleware(serverCompiler)
    app.use(serverMiddleware);
    serverCompiler.hooks.done.tap('compilerDone', () => {
        console.log('服务端构建完成')
        serverBundle = JSON.parse(serverMiddleware.context.outputFileSystem.readFileSync(path.join(clientWebpackConfig.output.path, 'vue-ssr-server-bundle.json')).toString())
        runBuildRenderer();
    })
    
    // 判断并重新创建新的renderer
    function runBuildRenderer(init = false) {
        buildCount++;
        if (!init && buildCount < 2) return;
        if (clientManifest && serverBundle) {
            buildCount = 0;
            console.log('新的renderer已产生')
            renderer = createBundleRenderer(serverBundle, {
                template,
                clientManifest,
                runInNewContext: false
            })
        }
    }
    
    // 热更新中间件
    app.use(WebpackHotMiddleware(clientCompiler, { log: false }));
    runBuildRenderer(true);
    
    // ------到这
    
    app.use(express.static(path.resolve(__dirname, './dist')))
    
    router.get('*', (req, res) => {
        const context = { url: req.url };
        renderer.renderToString(context, (err, html) => {
            if (err) res.send(err);
            res.send(html);
        })
    })
    
    app.use(router);
    
    app.listen(3000, function() {
        console.log(
            'App runing at:',
            `Local: ${ chalk.blueBright.underline('http://localhost:3000') }`
        )
    });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80

    大家看到这么老长一段代码不要慌张,且听笔者为你细细道来。

    # 逻辑分析

    这里关键部分就在上述笔者标记的代码段中间,首先这里会涉及到三个库:

    • webpack:用于构建。
    • webpack-dev-middleware:用于配合webpack将构建好的文件保存在内存中,而不是写入到文件。
    • webpack-hot-middleware:用于配合实现热更新。

    # webpack构建部分

    const webpack = require('webpack');
    const WebpackDevMiddleware = require('webpack-dev-middleware');
    const clientWebpackConfig = require('./build/webpack.client.config');
    const serverWebpackConfig = require('./build/webpack.server.config')
    const clientCompiler = webpack(clientWebpackConfig);
    const serverCompiler = webpack(serverWebpackConfig);
    const clientMiddleware = WebpackDevMiddleware(clientCompiler)
    const serverMiddleware = WebpackDevMiddleware(serverCompiler)
    
    1
    2
    3
    4
    5
    6
    7
    8

    这里主要是服务端进行webpack打包,配合webpack-dev-middleware,实现对构建完毕资源的访问。

    从代码上看,首先引入webpack与webpack-dev-middleware包,接着导入客户端构建与服务端构建这两份webpack配置文件用于生成compiler,然后使用webpack-dev-middleware,实现将构建代码输出到内存中便于访问。

    这里由于构建之后的内容都输出到了内存中,而在生成renderer的部分需要客户端构建manifest文件和服务端构建的bundle文件,所以我们得想办法在它们构建完之后触发重新生成renderer,以便于实现修改webpack配置文件也能顺利生成新的renderer。

    所以呢,鉴于以上问题,我们需要去监听一下webpack构建完成的事件,也就是当它构建完之后就重新执行生成renderer逻辑。

    # 客户端构建部分
    // 客户端构建
    clientCompiler.hooks.done.tap('compilerDone', () => {
        console.log('客户端构建完成')
        clientManifest = JSON.parse(clientMiddleware.context.outputFileSystem.readFileSync(path.join(clientWebpackConfig.output.path, 'vue-ssr-client-manifest.json')).toString())
        runBuildRenderer();
    })
    
    1
    2
    3
    4
    5
    6

    这里我们定义了一个全局变量clientManifest,用于每次构建都能被实时访问到。

    这里我们就通过生成的客户端构建实例来注册一个编译完成的事件,注册完之后,webpack内部就会帮我们调用这个回调函数,同时我们这个时候在回调函数中就能访问到构建好的文件了,具体实现如上。在读取完对应文件之后,我们重新执行创建renderer函数,就能更新renderer了。

    这个runBuildRenderer函数放在后面分析,不要慌。

    # 服务端构建部分

    同理,跟客户端构建类似。

    // 服务端构建
    serverCompiler.hooks.done.tap('compilerDone', () => {
        console.log('服务端构建完成')
        serverBundle = JSON.parse(serverMiddleware.context.outputFileSystem.readFileSync(path.join(clientWebpackConfig.output.path, 'vue-ssr-server-bundle.json')).toString())
        runBuildRenderer();
    })
    
    1
    2
    3
    4
    5
    6

    我们通过观察可以发现,这里的客户端构建与服务端构建部分主要做的事情就是等webpack构建好了之后更新相关全局变量的值,同时执行这个runBuildRenderer函数,先从名字来看,就是用来重新生成renderer的,那么接下来让我们看看它内部的实现吧。

    # runBuildRenderer

    为什么客户端部分和服务端部分都会调用这个函数,这是因为生成renderer需要客户端与服务端构建的结果,而他们的构建又没法保证顺序,所以这里会都调用一次,然后维护一个buildCount变量,执行一次则加一,等到被调用了两次也就是客户端构建和服务端构建都完成了再更新renderer,并重置buildCount为下一次做准备。

    function runBuildRenderer(init = false) {
        buildCount++;
        if (!init && buildCount < 2) return;
        if (clientManifest && serverBundle) {
            buildCount = 0;
            console.log('新的renderer已产生')
            renderer = createBundleRenderer(serverBundle, {
                template,
                clientManifest,
                runInNewContext: false
            })
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    从功能上来看也是比较简单的,主要就是用来生成新的renderer,同时区分了初始化和热更新的状态,初始化的时候我们默认直接取打包到dist目录中的结果;如果是热更新阶段,那么就需要等clientManifest、serverBundle都构建好了再重新rebuild。

    当然了,初始化的时候笔者会调用一次,并给init参数传为true,生成renderer,以用于保证初始渲染能力。

    # 浏览器热更新

    正如我们使用vue脚手架启动项目一样,我们也希望能够在业务代码改变之后,页面自动热更新,而不需要我手动刷新页面,这个时候就需要我们webpack-hot-middleware来配合了。

    // server.js
    const WebpackHotMiddleware = require('webpack-hot-middleware');
    app.use(WebpackHotMiddleware(clientCompiler, { log: false }));
    
    1
    2
    3

    我们在server.js中也就是服务端添加一个中间件即可。

    注意!还没完,我们还需要做一件事才能生效。

    # 修改客户端构建webpack配置
    const webpack = require('webpack')
    const hotModuleScript = 'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=10000&reload=true'
    
    {
        entry: [hotModuleScript, path.resolve(__dirname, '../entry-client.js')],
        plugins: [new webpack.HotModuleReplacementPlugin()]
    }
    
    1
    2
    3
    4
    5
    6
    7

    我们得把我们客户端构建的入口配置改一下,以数组形式,在原有的入口文件前面添加这么一段。

    同时,添加一个新插件(plugin),它是我们webpack内置的,可以直接通过webpack.来获取。

    编辑
    上次更新: 2023/11/25, 4:11:00
    服务端搭建
    总结

    ← 服务端搭建 总结→

    最近更新
    01
    github加速
    01-01
    02
    在线小工具
    01-01
    03
    Lora-Embeddings
    11-27
    更多文章>
    Theme by Vdoing | Copyright © 2020-2024 STDSuperman | MIT License
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式