收錄待用,修改轉載已取得 "騰訊雲" 授權 前言 在 web 前端開發中,我們會藉助 Grunt、Gulp 和 Webpack 等工具的 Watch 模塊去監聽文件變化,那服務端應該怎麼做?其實文件變化的監聽依然可以藉助構建工具,但我們還需要自動重啟服務或者熱重載。本文將介紹三種常見的方法。 方案一 ...
收錄待用,修改轉載已取得騰訊雲授權
前言
在 web 前端開發中,我們會藉助 Grunt、Gulp 和 Webpack 等工具的 Watch 模塊去監聽文件變化,那服務端應該怎麼做?其實文件變化的監聽依然可以藉助構建工具,但我們還需要自動重啟服務或者熱重載。本文將介紹三種常見的方法。
方案一:fs.watch
使用 node 原生的 fs.watch 方法監聽文件改動,所謂的“熱重載”也不過是及時清除記憶體中的文件緩存。示例如下:
const fs = require('fs'),
path = require('path'),
projectRootPath = path.resolve(__dirname, './src');
const watch = project => {
require('./src'); // 啟動 APP,自動檢索到 src/index.js
try { // 監聽文件夾
fs.watch(project, { recursive: true }, cacheClean)
} catch(e) {
console.error('watch file error');
}
}
// 清除緩存
const cacheClean = () => {
Object.keys(require.cache).forEach(function (id) {
if (/[\/\\](src)[\/\\]/.test(id)) {
delete require.cache[id]
}
})
}
// 啟動開發模式
watch(projectRootPath);
註意:在伺服器入口文件 src/index.js 中引用中間件時需要套一層函數,並使用 require 的方式引入模塊才能清除緩存。比如:
// 引入中間件或控制器
app.use(async (ctx, next) => {
await require('./controllers/main.js')(ctx);
});
// 或引入路由
app.use(async (ctx, next) => {
await require('./router').routes()(ctx, next)
})
方案二:應用進程管理器
此處以 PM2 為例,supervisor、forever 等類似的進程管理工具異曲同工,這裡不再贅述。
PM2 是一款帶有負載均衡功能的 Node 應用進程管理器,具有 —watch 配置項,用來監聽應用目錄的變化,一旦發生變化,立即重啟。詳見:Auto restart apps on file change。他是真正意義上的重啟,不是熱替換。
缺點:PM2 並不提供優雅的方式告知用戶何時重啟或者殺掉進程。
以下是一個簡單的 PM2 配置 (開發環境) start.js,啟動進程 node start.js
。
const pm2 = require('pm2');
pm2.connect(function(err) {
if (err) {
console.error(err);
process.exit(2);
}
pm2.start({
"watch": ["./app"], // 開啟 watch 模式,並監聽 app 文件夾下的改動
"ignore_watch": ["node_modules", "assets"], // 忽略監聽的文件
"watch_options": {
"followSymlinks": false // 不允許符號鏈接
},
name: 'httpServer',
script: './server/index.js', // APP 入口
exec_mode: 'fockMode', // 開發模式下建議使用 fockModel
instances: 1, // 僅啟用 1 個 CPU
max_memory_restart: '100M' // 當占用 100M 記憶體時重啟 APP
}, function(err, apps) {
pm2.disconnect(); // Disconnects from PM2
if (err) throw err
});
});
每次修改文件之後保存(Ctrl+S),會有個黑框閃一下,說明應用已經成功重啟了。
方案三:chokidar + babel
chokidar 是對 fs.watch / fs.watchFile / fsevents 的一層封裝。它的優勢包括解決(出自 chokidar 文檔):
1、在 OS X 下不能獲取文件名;
2、在 OS X 下 Sublime 修改文件後不能獲取到修改事件;
3、修改文件會觸發兩次事件;
4、不提供文件遞歸監聽;
5、高 CPU 使用率;
6、…
這裡使用 babel 的原因是想要支持最新的 js 語法,包括 ES2017、Stage-x,以及 import / export default 等模塊語法。
下麵提供一個完整的監聽重載配置文件,並通過註釋說明功能和意義。
const projectRootPath = path.resolve(__dirname, '..'),
srcPath = path.join(projectRootPath, 'src'), // 源文件
appPath = path.join(projectRootPath, 'app'), // 編譯後輸出文件夾
devDebug = debug('dev'),
watcher = chokidar.watch(path.join(__dirname, '../src'))
// 啟動 chokidar 監聽文件改動
watcher.on('ready', function () {
// babel 編譯文件夾目錄
babelCliDir({
outDir: 'app/',
retainLines: true,
sourceMaps: true
}, [ 'src/' ]) // compile all when start
require('../app') // 啟動 APP(編譯後的文件)
// 添加監聽方法
watcher
// 文件新增
.on('add', function (absPath) {
compileFile('src/', 'app/', path.relative(srcPath, absPath), cacheClean)
})
// 文件修改
.on('change', function (absPath) {
compileFile('src/', 'app/', path.relative(srcPath, absPath), cacheClean)
})
// 文件刪除
.on('unlink', function (absPath) {
var rmfileRelative = path.relative(srcPath, absPath)
var rmfile = path.join(appPath, rmfileRelative)
try {
fs.unlinkSync(rmfile)
fs.unlinkSync(rmfile + '.map')
} catch (e) {
devDebug('fail to unlink', rmfile)
return
}
console.log('Deleted', rmfileRelative)
cacheClean(); //清除緩存
})
})
// 動態編譯文件
function compileFile (srcDir, outDir, filename, cb) {
const outFile = path.join(outDir, filename),
srcFile = path.join(srcDir, filename);
try {
babelCliFile({
outFile: outFile,
retainLines: true,
highlightCode: true,
comments: true,
babelrc: true,
sourceMaps: true
}, [ srcFile ], {
highlightCode: true,
comments: true,
babelrc: true,
ignore: [],
sourceMaps: true
})
} catch (e) {
console.error('Error while compiling file %s', filename, e)
return
}
console.log(srcFile + ' -> ' + outFile)
cb && cb() // 通常為清除緩存
}
// 清除緩存
function cacheClean () {
Object.keys(require.cache).forEach(function (id) {
if (/[\/\\](app)[\/\\]/.test(id)) {
delete require.cache[id]
}
})
console.log('App Cache Cleaned...')
}
// 監聽程式退出
process.on('exit', function (e) {
console.log('App Quit')
})
註意:為了能讓緩存失效,我們同樣需要在 use 里包裹一層函數,並以 require 的方式引入路由
app.use(async (ctx, next) => {
await require('./router').routes()(ctx, next)
})
方案四:開發插件
nodemon 和 node-dev 都是可用於 node.js 開發版插件,提供簡單易用的開發環境。以 nodemon 為例,全局安裝或本地安裝都可
npm install nodemon -g
然後通過 nodemon ./server.js localhost 8080 啟動開發進程。獨立、簡單,好用!
詳見:remy/nodemon
綜上
每個方法都有不同的適用場景。如果想要嘗試最新語法,推薦試用方案三;如果追求簡單快捷,方案二是不錯的選擇。
這就結束了嗎?
如果我既想用最新的語法特性,又需要像 PM2 那樣簡單,怎麼辦?babel 構建工具(如 webpack)對於每個前端開發並不陌生,再加一款 PM2 足以解決所有問題。
原文鏈接:https://www.qcloud.com/community/article/476280