在用Node.js+Webpack構建的方式進行開發時, 咱們但願能實現修改代碼能實時刷新頁面UI的效果.
這個特性webpack自己是支持的, 並且基於koa也有現成的koa-webpack-hot-middleware 和 koa-webpack-dev-middleware 封裝好的組件支持.
不過這裏若是須要支持Node.js服務器端修改代碼自動重啓webpack自動編譯功能就須要cluster來實現.css
今天這裏要講的是如何在koa和egg應用實現Node.js應用重啓中的webpack熱更新功能. 要實現egg項目中webpack友好的開發體驗, 須要解決以下三個問題.html
在koa項目中, 經過koa-webpack-dev-middleware和koa-webpack-hot-middleware能夠實現webpack編譯內存存儲和熱更新功能, 代碼以下:vue
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實現, 大概思路以下:node
if (cluster.isWorker) { const koa = require('koa'); app.listen(8888, () =>{ app.logger.info('The server is running on port: 9999'); }); }
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}`); }); }); }
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}`); }); });
process.send
向 master 發現消息, process.on
監聽 master返回的消息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); } }); }); };
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); }); }); };
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; } });
經過上面koa的實現思路, egg實現就更簡單了. 由於egg已經內置了worker和agent通訊機制以及自動重啓功能.webpack
經過app.messenger.sendToAgent
向agent發送消息git
經過app.messenger.on
監聽agent發送過來的消息github
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; });
這裏client和server編譯單獨啓動koa實例, 而不是一個是由於在測試時發現編譯會致使熱更新衝突.web
'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: '', }); } }); };
'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: '', }); } }); };
app
上面, 方便業務擴展實現, 代碼以下:咱們經過worker向agent發送消息, 就能夠從webpack內存獲取文件內容, 下面簡單封裝一下:promise
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實例, 咱們很容易實現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熱更新內存存儲讀取.
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); } }); }); } }
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
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