easywebpack
首发于easywebpack

Egg 项目集成 Webpack 热更新

Egg + React 工程化解决方案Egg + Vue 工程化解决方案 方案使用了 egg-webpack 实现了 Egg + easywebpack 热更新统一处理。其实 egg-webpack 是可以单独使用的,不耦合 easywebpack 构建配置。下面以 Egg + React + Webpack 为例。

egg-wepback 介绍

集成服务

特性支持

  • 在 Agent 里面单独启动 Webpack 服务解决 Node 重启 Webpack 实例丢失导致重新编译问题
  • 提供 app.webpack 钩子从内存读取文件内容,解决本地开发 Server Side Render 文件渲染内容读取问题
  • 支持多个 Webpack 实例独立同时编译服务(支持多进程编译),自动获取可用端口号,自动注入热更新配置
  • Egg 主应用静态资源访问自动代理到独立的 Webpack 编译 HTTP 服务

egg-wepback 插件使用

  • 安装插件
npm install egg-webpack --save
  • 开启插件
// config/plugin.local.js

exports.webpack = {
  enable: true,
  package: 'egg-webpack'
};
  • 插件配置
// config/config.local.js

exports.webpack = {
  // 这里的 webpack.config.js 为原生 Webpack 配置即可
  webpackConfigList: [require('../webpack.config.js')]
};

编写 Webpack 配置

  • 安装 react-hot-loader 插件
npm install react-hot-loader --save-dev
npm install webpack progress-bar-webpack-plugin webpack-manifest-plugin --save-dev
npm install @babel/core @babel/preset-env @babel/preset-react babel-loader --save-dev
  • 编写 webpack.config.js 配置
// ${root}/webpack.config.js
const path = require('path');
const webpack = require('webpack');
const ProgressBar = require('progress-bar-webpack-plugin');
const ManifestPlugin = require('webpack-manifest-plugin');
const isDev = process.env.NODE_ENV !== 'production';
module.exports = {
  mode: process.env.NODE_ENV,
  entry: isDev ? { app: './app/web/index.js' } : {
    app:[
      'react-hot-loader/babel', // egg-webpack 默认端口为 9000
      'webpack-hot-middleware/client?path=http://127.0.0.1:9000/__webpack_hmr',
      './app/web/index.js'
    ]
  },
  resolve: {
    extensions: [ '.jsx', '.js' ],
  },
  output: {
    path: path.join(__dirname, 'app/public'),
    filename: isDev ? '[name].[hash].js' : '[name].js',
    publicPath: '/public/'
  },
  module: {
    rules: [
      {
        test: /\.js?$/,
        use: 'babel-loader',
        exclude: /node_modules/,
      },
      {
        test: /\.css$/,
        use: [ 'style-loader', 'css-loader'],
      },
      {
        test: /\.(png|jpg|gif|svg)$/,
        use: [
          {
            loader: 'file-loader',
          },
        ],
      },
    ],
  },
  plugins:[
    new webpack.HotModuleReplacementPlugin(),
    new ProgressBar(),
  ]
};

前端页面开启热更新

'use strict';
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import { AppContainer } from 'react-hot-loader';
import './app.css';

class App extends Component {
  render() {
    return <div className="title"><h1>React App</h1></div>
  }
}

ReactDOM.render(module.hot ? <AppContainer><App /></AppContainer> : <App />, document.getElementById('app'));

if (module.hot) {
  module.hot.accept();
}

页面渲染

通过 egg-view-nunjucks 模板引擎进行 layout 模板渲染,同时根据 webpack 生成 manifest.json 获取静态资源的实际路径。

  • 配置 nunjucks 引擎
// ${root}/config/plugin.js
exports.nunjucks = {
  enable: true,
  package: 'egg-view-nunjucks',
};

// {app_root}/config/config.default.js
exports.view = {
  defaultViewEngine: 'nunjucks',
  mapping: {
    '.tpl': 'nunjucks',
  },
};
  • 定义 {app_root}/app/view/layout.tpl 文件
<!DOCTYPE html>
<html lang='en'>
<head>
  <title>{{title}}</title>
  <meta name='keywords'>
  <meta name='description'>
  <meta http-equiv='content-type' content='text/html;charset=utf-8'>
  <meta name='viewport' content='initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui'>
  <link rel='shortcut icon' href='/favicon.ico' type='image/x-icon' />
  {% for item in asset.css %}
    <link rel="stylesheet" href='{{item}}' />
  {% endfor %}
</head>
<body>
  <div id='app'></div>
  <script type="text/javascript">
    window.__INITIAL_STATE__ = {{ asset.state | safe }};
  </script>
  {% for item in asset.js %}
    <script type="text/javascript" src="{{item}}"></script>
  {% endfor %}
</body>
</html>
  • 页面渲染逻辑实现
const egg = require('egg');
const manifest = require('../public/manifest.json');
module.exports = class AppController extends egg.Controller {
  async home(ctx) {
    const js = [manifest['app.js']];
    const css = [manifest['app.css']];
    await ctx.render('layout.tpl', { 
      title: 'Egg Webpack Hot Reload',
      state: {},
      asset: { js, css }
    });
  }
}

最后就可以 npm run dev (egg-bin dev) 一键启动开发,不用分别单独启动前端应用和 Node 应用,同时解决 Node 重启 Webpack 实例丢失导致重新编译问题。

Server Side Render 文件内容读取

'usestrict';
const path = require('path');
const egg = require('egg');
const vueServerRenderer = require('vue-server-renderer');
module.exports = class IndexController extends egg.Controller {
  async index(ctx) {
    const { app } = ctx;
    const filepath = path.join(app.config.view.root[0], 'app.js');
    // server render mode, the webpack config target:node
    const strJSBundle = await app.webpack.fileSystem.readWebpackMemoryFile(filepath);
    ctx.body = await vueServerRenderer.createBundleRenderer(strJSBundle).renderToString({});
  }
};

更多实践

构建流程 · 语雀www.yuque.com图标自定义前端渲染 · 语雀www.yuque.com图标sky:Webpack 热更新实现原理分析zhuanlan.zhihu.com图标

发布于 2019-07-29

文章被以下专栏收录

    基于 Webpack,为 Vue, React, Angular, Weex 提供统一构建,支持前端渲染和后端渲染,并提供统一的构建方案和骨架。GitHub: https://github.com/hubcarlBlog: http://hubcarl.github.io/easywebpack