koa and egg project realization webpack hot update

background

When developing with Node.js + Webpack way to build, we hope to achieve the effect of modifying the code can refresh the page UI in real time.
This feature is supported webpack itself, but also ready-based koa koa-webpack-hot-middleware and koa-webpack-dev-middleware component supports encapsulated.
but here, if desired to modify the code to support server-side Node.js automatic restart function automatically compiled WebPACK cluster need to achieve.

Here to talk about today is how to achieve webpack hot update feature in the Node.js application restart koa and egg applications. To achieve egg development project webpack friendly experience, the need to address the following three questions.

problem

  • How to solve the Node.js server-side code to modify the application to restart avoid webpack recompiled.
  • How to access static resources js, css, image and so on.
  • How to deal with local heat development webpack memory stores read and update online application document reading machine of the present logic.

Based webpack compiled and updated heat of koa

In koa project may be implemented WebPACK compile and store memory update function by heat koa-webpack-dev-middleware, and koa-webpack-hot-middleware, code is as follows:

const compiler = webpack(webpackConfig);
const devMiddleware = require('koa-webpack-dev-middleware')(compiler, options);
const hotMiddleware = require('koa-webpack-hot-middleware')(compiler, options);
app.use(devMiddleware);
app.use(hotMiddleware);

If you follow to achieve the above, modify modify to meet client code to compile and implement webpack automatically become hot update UI interface features, but if it is modified Node.js server-side code will find webpack restart will re-compile,
this is not what we want effect. the reason is because the middleware is dependent on the app life cycle, when the app destroyed, there will be no instance of the corresponding webpack compiler, and re-executes the middleware initialization restart.
for this we can achieve through Node.js cluster, probably thinking follows :

Start the application via App cluster worker

if (cluster.isWorker) {
  const koa = require('koa');
    app.listen(8888, () =>{
        app.logger.info('The server is running on port: 9999');
    });
}

Start a new koa application by cluster master, and start webpack compilation.

const cluster = require('cluster');
const chokidar = require('chokidar');

if (cluster.isMaster) {
  const koa = require('koa');
  const app = koa();
  const compiler = webpack([clientWebpackConfig,serverWebpackConfig]);
  const devMiddleware = require('koa-webpack-dev-middleware')(compiler);
  const hotMiddleware = require('koa-webpack-hot-middleware')(compiler);
  app.use(devMiddleware);
  app.use(hotMiddleware);

  let worker = cluster.fork();
  chokidar.watch(config.dir, config.options).on('change', path =>{
    console.log(`${path} changed`);
    worker.kill();
    worker = cluster.fork().on('listening', (address) =>{
      console.log(`[master] listening: worker ${worker.id}, pid:${worker.process.pid} ,Address:${address.address } :${address.port}`);
    });
  });
}

By chokidar library file monitor folder changes, and then restart the worker, so we can guarantee webpack compiler instances are not destroyed.

const watchConfig = {
        dir: [ 'controller', 'middleware', 'lib', 'model', 'app.js', 'index.js' ],
        options: {}
    };
    let worker = cluster.fork();
    chokidar.watch(watchConfig.dir, watchConfig.options).on('change', path =>{
        console.log(`${path} changed`);
        worker && worker.kill();
        worker = cluster.fork().on('listening', (address) =>{
            console.log(`[master] listening: worker ${worker.id}, pid:${worker.process.pid} ,Address:${address.address } :${address.port}`);
        });
});

worker through the process.senddiscovery message to the master, process.onlisten for messages returned by the master

  • First we look for local file read in context readFile mounted above method, view render, call the app.context.readFilemethod.
app.context.readFile = function(fileName){
  const filePath = path.join(config.baseDir, config.staticDir, fileName);
  return new Promise((resolve, reject) =>{
    fs.readFile(filePath, CHARSET, function(err, data){
      if (err) {
        reject(err);
      } else {
        resolve(data);
      }
    });
  });
};
  • Worker by overwriting app.context.readFilemethod, when carried out by local development, turn the plug-in can read files from a seamless memory compiler system which webpack
app.context.readFile = (fileName) =>{
  return new Promise((resolve, reject) =>{
    process.send({ action: Constant.EVENT_FILE_READ, fileName });
    process.on(Constant.EVENT_MESSAGE, (msg) =>{
      resolve(msg.content);
    });
  });
};

master by listening to the worker sent me a message, get webpack progress of the compilation and webpack compiler reads the memory contents of the file system

cluster.on(Constant.EVENT_MESSAGE, (worker, msg) =>{
        switch (msg.action) {
            case Constant.EVENT_WEBPACK_BUILD_STATE: {
                const data = {
                    action: Constant.EVENT_WEBPACK_BUILD_STATE,
                    state: app.webpack_client_build_success && app.webpack_server_build_success
                };
                worker.send(data);
                break;
            }
            case Constant.EVENT_FILE_READ: {
                const fileName = msg.fileName;
                try {
                    const compiler = app.compiler;
                    const filePath = path.join(compiler.outputPath, fileName);
                    const content = app.compiler.outputFileSystem.readFileSync(filePath).toString(Constant.CHARSET);
                    worker.send({ fileName, content });
                } catch (e) {
                    console.log(`read file ${fileName} error`, e.toString());
                }
                break;
            }
            default:
                break;
        }
});

The webpack compiled based on egg and hot update realization

Koa achieved by the above ideas, egg achieve even simpler because the egg has built-worker and agent communication mechanism and an automatic restart function.

app.js (worker) by detecting the progress of the compilation webpack

  • By app.messenger.sendToAgentsending a message to the agent

  • By app.messenger.onsending messages over the monitor agent

app.use(function* (next) {
  if (app.webpack_server_build_success && app.webpack_client_build_success) {
    yield* next;
  } else {
    const serverData = yield new Promise(resolve => {
      this.app.messenger.sendToAgent(Constant.EVENT_WEBPACK_SERVER_BUILD_STATE, {
        webpackBuildCheck: true,
      });
      this.app.messenger.on(Constant.EVENT_WEBPACK_SERVER_BUILD_STATE, data => {
        resolve(data);
      });
    });
    app.webpack_server_build_success = serverData.state;

    const clientData = yield new Promise(resolve => {
      this.app.messenger.sendToAgent(Constant.EVENT_WEBPACK_CLIENT_BUILD_STATE, {
        webpackBuildCheck: true,
      });
      this.app.messenger.on(Constant.EVENT_WEBPACK_CLIENT_BUILD_STATE, data => {
        resolve(data);
      });
    });

    app.webpack_client_build_success = clientData.state;

    if (!(app.webpack_server_build_success && app.webpack_client_build_success)) {
      if (app.webpack_loading_text) {
        this.body = app.webpack_loading_text;
      } else {
        const filePath = path.resolve(__dirname, './lib/template/loading.html');
        this.body = app.webpack_loading_text = fs.readFileSync(filePath, 'utf8');
      }
    } else {
      yield* next;
    }
  }
});

app.messenger.on(Constant.EVENT_WEBPACK_SERVER_BUILD_STATE, data => {
  app.webpack_server_build_success = data.state;
});

app.messenger.on(Constant.EVENT_WEBPACK_CLIENT_BUILD_STATE, data => {
  app.webpack_client_build_success = data.state;
});

agent.js start koa instance and webpack compilation flow

Here client and server start compiling separate koa instance, instead of one is because it was found in the test compilation will lead to hot update conflict.

  • Start webpack client compilation mode, is responsible for compiling run the file browser (js, css, image and other static resources)
'use strict';

const webpack = require('webpack');
const koa = require('koa');
const cors = require('kcors');
const app = koa();
app.use(cors());
const Constant = require('./constant');
const Utils = require('./utils');

module.exports = agent => {

  const config = agent.config.webpack;
  const webpackConfig = config.clientConfig;
  const compiler = webpack([webpackConfig]);

  compiler.plugin('done', compilation => {
    // Child extract-text-webpack-plugin:
    compilation.stats.forEach(stat => {
      stat.compilation.children = stat.compilation.children.filter(child => {
        return child.name !== 'extract-text-webpack-plugin';
      });
    });
    agent.messenger.sendToApp(Constant.EVENT_WEBPACK_CLIENT_BUILD_STATE, { state: true });
    agent.webpack_client_build_success = true;
  });

  const devMiddleware = require('koa-webpack-dev-middleware')(compiler, {
    publicPath: webpackConfig.output.publicPath,
    stats: {
      colors: true,
      children: true,
      modules: false,
      chunks: false,
      chunkModules: false,
    },
    watchOptions: {
      ignored: /node_modules/,
    },
  });

  const hotMiddleware = require('koa-webpack-hot-middleware')(compiler, {
    log: false,
    reload: true,
  });

  app.use(devMiddleware);
  app.use(hotMiddleware);

  app.listen(config.port, err => {
    if (!err) {
      agent.logger.info(`start webpack client build service: http://127.0.0.1:${config.port}`);
    }
  });

  agent.messenger.on(Constant.EVENT_WEBPACK_CLIENT_BUILD_STATE, () => {
    agent.messenger.sendToApp(Constant.EVENT_WEBPACK_CLIENT_BUILD_STATE, { state: agent.webpack_client_build_success });
  });

  agent.messenger.on(Constant.EVENT_WEBPACK_READ_CLIENT_FILE_MEMORY, data => {
    const fileContent = Utils.readWebpackMemoryFile(compiler, data.filePath);
    if (fileContent) {
      agent.messenger.sendToApp(Constant.EVENT_WEBPACK_READ_CLIENT_FILE_MEMORY_CONTENT, {
        fileContent,
      });
    } else {
      agent.logger.error(`webpack client memory file[${data.filePath}] not exist!`);
      agent.messenger.sendToApp(Constant.EVENT_WEBPACK_READ_CLIENT_FILE_MEMORY_CONTENT, {
        fileContent: '',
      });
    }
  });
};
  • Start webpack server compilation mode, is responsible for compiling a file server running Node
'use strict';

const webpack = require('webpack');
const koa = require('koa');
const cors = require('kcors');
const app = koa();
app.use(cors());
const Constant = require('./constant');
const Utils = require('./utils');

module.exports = agent => {
  const config = agent.config.webpack;
  const serverWebpackConfig = config.serverConfig;
  const compiler = webpack([serverWebpackConfig]);

  compiler.plugin('done', () => {
    agent.messenger.sendToApp(Constant.EVENT_WEBPACK_SERVER_BUILD_STATE, { state: true });
    agent.webpack_server_build_success = true;
  });

  const devMiddleware = require('koa-webpack-dev-middleware')(compiler, {
    publicPath: serverWebpackConfig.output.publicPath,
    stats: {
      colors: true,
      children: true,
      modules: false,
      chunks: false,
      chunkModules: false,
    },
    watchOptions: {
      ignored: /node_modules/,
    },
  });

  app.use(devMiddleware);

  app.listen(config.port + 1, err => {
    if (!err) {
      agent.logger.info(`start webpack server build service: http://127.0.0.1:${config.port + 1}`);
    }
  });

  agent.messenger.on(Constant.EVENT_WEBPACK_SERVER_BUILD_STATE, () => {
    agent.messenger.sendToApp(Constant.EVENT_WEBPACK_SERVER_BUILD_STATE, { state: agent.webpack_server_build_success });
  });

  agent.messenger.on(Constant.EVENT_WEBPACK_READ_SERVER_FILE_MEMORY, data => {
    const fileContent = Utils.readWebpackMemoryFile(compiler, data.filePath);
    if (fileContent) {
      agent.messenger.sendToApp(Constant.EVENT_WEBPACK_READ_SERVER_FILE_MEMORY_CONTENT, {
        fileContent,
      });
    } else {
      // agent.logger.error(`webpack server memory file[${data.filePath}] not exist!`);
      agent.messenger.sendToApp(Constant.EVENT_WEBPACK_READ_SERVER_FILE_MEMORY_CONTENT, {
        fileContent: '',
      });
    }
  });
};
  • Memory read instance to mount webpack appabove, to facilitate business expansion achieved, as follows:

We send a message through the worker to the agent, you can get the file contents from webpack memory, look at the following simple package:

class FileSystem {

  constructor(app) {
    this.app = app;
  }

  readClientFile(filePath, fileName) {
    return new Promise(resolve => {
      this.app.messenger.sendToAgent(Constant.EVENT_WEBPACK_READ_CLIENT_FILE_MEMORY, {
        filePath,
        fileName,
      });
      this.app.messenger.on(Constant.EVENT_WEBPACK_READ_CLIENT_FILE_MEMORY_CONTENT, data => {
        resolve(data.fileContent);
      });
    });
  }

  readServerFile(filePath, fileName) {
    return new Promise(resolve => {
      this.app.messenger.sendToAgent(Constant.EVENT_WEBPACK_READ_SERVER_FILE_MEMORY, {
        filePath,
        fileName,
      });
      this.app.messenger.on(Constant.EVENT_WEBPACK_READ_SERVER_FILE_MEMORY_CONTENT, data => {
        resolve(data.fileContent);
      });
    });
  }
}

In app / extend / application.js instance mount webpack

const WEBPACK = Symbol('Application#webpack');
module.exports = {
  get webpack() {
    if (!this[WEBPACK]) {
      this[WEBPACK] = new FileSystem(this);
    }
    return this[WEBPACK];
  },
};

Local development of heat webpack update the memory and storing the read line separate application file read logic

Based on the above compilation process implementation and webpack instance, we can easily achieve koa way of local development and online operation code-Here we have to achieve, for example vue render rendering server:

In the egg-view plug-in development specification, we will mount ctx render the above method, the method would render file read by file name, template and data compilation to achieve the following template rendering is called by the controller:

exports.index = function* (ctx) {
  yield ctx.render('index/index.js', Model.getPage(1, 10));
};

One of the most crucial step is to read the file based on the file name, as long as the plug-in design view when the file read method is exposed (for example, koa top of readFile), you can achieve local development webpack hot update memory storage read.

  • vue view engine design and implementation:
const Engine = require('../../lib/engine');
const VUE_ENGINE = Symbol('Application#vue');

module.exports = {

  get vue() {
    if (!this[VUE_ENGINE]) {
      this[VUE_ENGINE] = new Engine(this);
    }
    return this[VUE_ENGINE];
  },
};
class Engine {
  constructor(app) {
    this.app = app;
    this.config = app.config.vue;
    this.cache = LRU(this.config.cache);
    this.fileLoader = new FileLoader(app, this.cache);
    this.renderer = vueServerRenderer.createRenderer();
    this.renderOptions = Object.assign({
      cache: this.cache,
    }, this.config.renderOptions);
  }

  createBundleRenderer(code, renderOptions) {
    return vueServerRenderer.createBundleRenderer(code, Object.assign({}, this.renderOptions, renderOptions));
  }

  * readFile(name) {
    return yield this.fileLoader.load(name);
  }

  render(code, data = {}, options = {}) {
    return new Promise((resolve, reject) => {
      this.createBundleRenderer(code, options.renderOptions).renderToString(data, (err, html) => {
        if (err) {
          reject(err);
        } else {
          resolve(html);
        }
      });
    });
  }
}
  • ctx.render method
class View {
  constructor(ctx) {
    this.app = ctx.app;
  }

  * render(name, locals, options = {}) {
    // 我们通过覆写app.vue.readFile即可改变文件读取逻辑
    const code = yield this.app.vue.readFile(name);
    return this.app.vue.render(code, { state: locals }, options);
  }

  renderString(tpl, locals) {
    return this.app.vue.renderString(tpl, locals);
  }
}

module.exports = View;

Server view rendering plug-in implementation egg-view-vue

  • Webpack example by overwriting app.vue.readFile change the contents of memory from webpack read the file.
if (app.vue) {
  app.vue.readFile = fileName => {
    const filePath = path.isAbsolute(fileName) ? fileName : path.join(app.config.view.root[0], fileName);
    if (/\.js$/.test(fileName)) {
      return app.webpack.fileSystem.readServerFile(filePath, fileName);
    }
    return app.webpack.fileSystem.readClientFile(filePath, fileName);
  };
}

app.messenger.on(app.webpack.Constant.EVENT_WEBPACK_CLIENT_BUILD_STATE, data => {
  if (data.state) {
    const filepath = app.config.webpackvue.build.manifest;
    const promise = app.webpack.fileSystem.readClientFile(filepath);
    promise.then(content => {
      fs.writeFileSync(filepath, content, 'utf8');
    });
  }
});

webpack + vue compiler plug-in implementation egg-webpack-vue

egg + webpack + vue engineering solutions

Reproduced in: https: //www.cnblogs.com/hubcarl/p/6847818.html

Guess you like

Origin blog.csdn.net/weixin_33976072/article/details/93817360