背景

在用Node.js+Webpack构建的方式进行开发时, 我们希望能实现修改代码能实时刷新页面UI的效果.

这个特性webpack本身是支持的, 而且基于koa也有现成的koa-webpack-hot-middleware 和 koa-webpack-dev-middleware 封装好的组件支持.

不过这里如果需要支持Node.js服务器端修改代码自动重启webpack自动编译功能就需要cluster来实现.

今天这里要讲的是如何在koa和egg应用实现Node.js应用重启中的webpack热更新功能. 要实现egg项目中webpack友好的开发体验, 需要解决如下三个问题.

问题

  • 如何解决Node.js服务器端代码修改应用重启避免webpack重新编译.
  • 如何访问js,css,image等静态资源.
  • 如何处理本地开发webpack热更新内存存储读取和线上应用本机文件读取逻辑分离.

基于koa的webpack编译和热更新实现

在koa项目中, 通过koa-webpack-dev-middleware和koa-webpack-hot-middleware可以实现webpack编译内存存储和热更新功能, 代码如下:

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);

如果按照上面实现, 可以满足修改修改客户端代码实现webpack自动变编译和UI界面热更新的功能, 但如果是修改Node.js服务器端代码重启后就会发现webpack会重新编译,

这不是我们要的效果.原因是因为middleware是依赖app的生命周期, 当app销毁时, 对应webpack compiler实例也就没有了, 重启时会重新执行middleware初始化工作.

针对这个我们可以通过Node.js cluster实现, 大概思路如下:

通过cluster worker 启动App应用

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

通过cluster master 启动一个新的koa应用, 并启动 webpack 编译.

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}`);
});
});
}

通过chokidar库监听文件夹的文件修改, 然后重启worker, 这样就能保证webpack compiler实例不被销毁.

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 通过process.send 向 master 发现消息, process.on 监听 master返回的消息

  • 首先我们看看本地文件读取的实现, 在context上面挂载readFile方法, 进行view render时, 调用app.context.readFile 方法.
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 app.context.readFile 方法, 这样进行本地开发时,开启该插件就可以无缝的从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 通过监听worker发过来的消息, 获取webpack编译进度和读取webpack compiler内存系统文件内容

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;
}
});

基于egg的webpack编译和热更新实现

通过上面koa的实现思路, egg实现就更简单了. 因为egg已经内置了worker和agent通信机制以及自动重启功能.

app.js (worker) 通过 检测webpack 编译进度

  • 通过app.messenger.sendToAgent 向agent发送消息

  • 通过app.messenger.on 监听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 启动koa实例和webpack编译流程

这里client和server编译单独启动koa实例, 而不是一个是因为在测试时发现编译会导致热更新冲突.

  • 启动webpack client 编译模式, 负责编译browser运行文件(js,css,image等静态资源)
'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: '',
});
}
});
};
  • 启动webpack server 编译模式, 负责编译服务器端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: '',
});
}
});
};
  • 挂载 webpack 内存读取实例到app上面, 方便业务扩展实现, 代码如下:

我们通过worker向agent发送消息, 就可以从webpack内存获取文件内容, 下面简单封装一下:

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);
});
});
}
}

在app/extend/application.js 挂载webpack实例

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

本地开发webpack热更新内存存储读取和线上应用文件读取逻辑分离

基于上面编译流程实现和webpack实例, 我们很容易实现koa方式的本地开发和线上运行代码分离. 下面我们就以vue 服务器渲染render实现为例:

在egg-view插件开发规范中,我们会在ctx上面挂载render方法, render方法会根据文件名进行文件读取, 模板与数据编译, 从而实现模板的渲染.如下就是controller的调用方式:

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

其中最关键的一步是根据文件名进行文件读取, 只要view插件设计时, 把文件读取的方法暴露出来(例如上面的koa的readFile),就可以实现本地开发webpack热更新内存存储读取.

  • vue view engine设计实现:
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 方法
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;

服务器view渲染插件实现 egg-view-vue

  • 通过webpack实例覆写app.vue.readFile 改变从webpack内存读取文件内容.
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 编译插件实现 egg-webpack-vue

egg+webpack+vue工程解决方案

koa和egg项目webpack热更新实现的更多相关文章

  1. webpack热更新问题和antd design字体图标库扩展

    标题也不知道怎么写好,真是尴尬.不过话说回来,距离上一次写文快两个月了,最近有点忙,一直在开发新项目, 今天刚刚闲下来,项目准备提测.借这个功夫写点东西,把新项目上学到的一些好的干活分享一下,以便之后 ...

  2. [转] webpack热更新配置小结

    webpack热更新配置 热更新,可以使开发的人在修改代码后,不用刷新浏览器即可以看到修改后的效果.而它的另一个好处则是可以只替换修改部分相关的代码,大大的缩短了构建的时间. 热更新一般会涉及到两种场 ...

  3. vue-vli3创建的项目配置热更新

    vue-vli3创建的项目配置热更新 问题描述:使用vue-cli3创建的项目,修改代码之后,浏览器页面不会自动刷新,然而之前使用webpack初始化的vue项目修改代码之后浏览器会重新加载一下,因为 ...

  4. 轻松理解webpack热更新原理

    一.前言 - webpack热更新 Hot Module Replacement,简称HMR,无需完全刷新整个页面的同时,更新模块.HMR的好处,在日常开发工作中体会颇深:节省宝贵的开发时间.提升开发 ...

  5. webpack热更新和常见错误处理

    时间:2016-11-03 10:50:54 地址:https://github.com/zhongxia245/blog/issues/45 webpack热更新 一.要求 局部刷新修改的地方 二. ...

  6. webpack热更新实现

    原文地址:webpack热更新实现 webpack,一代版本一代神,代代版本出大神.如果你的webpack和webpack-dev-server版本大于2小于等于3.6,请继续看下去.其它版本就必浪费 ...

  7. koa2 + webpack 热更新

    网上有很多express+webpack的热更新,但是koa2的很少,这两天研究了一下子,写一个简单的教程. 1.需要的包 webpack:用于构建项目 webpack-dev-middleware: ...

  8. webpack热更新

    文件地址:https://pan.baidu.com/s/1kUOwFkV 从昨天下午到今天上午搞了大半天终于把热更新搞好了,之前热更新有两个问题,第一个是不能保存表单状态.第二个是更新太慢,这次主要 ...

  9. webpack 热更新

    1.安装webpack npm install webpack -g  //全局安装 npm install webpack --save-dev  //开发环境 2.使用webpack 创建一个we ...

随机推荐

  1. centos 安装或更新最新版本软件包(git python etc)的方法 SCL IUS

    使用centos 经常发现官方提供的软件包版本过低,很多时候大家会选择下载源码自行编译,带来了很多麻烦. centos安装最新版本软件包,例如git,python等,可以通过红帽官方提供的softwa ...

  2. 百度词汇检索,计算PMI值

    '''词汇检索百度返回值,并且计算PMI值的类''' from bs4 import BeautifulSoup import requests import re import pandas as ...

  3. 2018.07.06 POJ1273 Drainage Ditches(最大流)

    Drainage Ditches Time Limit: 1000MS Memory Limit: 10000K Description Every time it rains on Farmer J ...

  4. hdu-1173(最短距离)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1173 思路:最短距离:就是现将x,y从小到大排序,然后去中间点就行了.(注意:本题答案不唯一) #in ...

  5. Can not issue data manipulation statements with executeQuery().

    这个错误提示是说无法发行sql语句到指定的位置 就是如图的两端代码的问题,excuteQuery是查询语句,而我要调用的是更新的语句,所以这样数据库很为难到底要干嘛,我实际的操作是要更新数据,所以把 ...

  6. QDesktopWidget

    在Qt中提供了QDesktopWidget类,提供屏幕的有关信息. 可以这么作: QDesktopWidget *d=QApplication::desktop(); int width=d-> ...

  7. (网络流) Island Transport --Hdu -- 4280

    链接: http://acm.hdu.edu.cn/showproblem.php?pid=4280 源点是West, 汇点是East, 用Dinic带入求就好了 代码:要用c++提交 #pragma ...

  8. ZOJ2405 Specialized Four-Digit Numbers 2017-04-18 20:43 44人阅读 评论(0) 收藏

    Specialized Four-Digit Numbers Time Limit: 2 Seconds      Memory Limit: 65536 KB Find and list all f ...

  9. hdu2571 命运 2016-09-11 16:54 53人阅读 评论(0) 收藏

    命运 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total Submiss ...

  10. Ansible之ansible-playbook roles

    刚开始学习运用 playbook 时,可能会把 playbook 写成一个很大的文件,到后来可能你会希望这些文件是可以方便去重用的,所以需要重新去组织这些文件. 基本上,使用 include 语句引用 ...