Node Server零基础: 开发环境文件自动重载

前言

在 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 为例,supervisorforever 等类似的进程管理工具异曲同工,这里不再赘述。

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 足以解决所有问题。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

廖泽恺的专栏

1 篇文章1 人订阅

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Spring相关

Springboot用官方建议访问Html页面并接传值

我们以前通常习惯用webapp来放置jsp页面,但是到了Springboot中,官方建议用Static文件夹来存放及静态的资源,

924
来自专栏云飞学编程

用Python抓取百度翻译内容并打造自己的翻译脚本!

英文不好一直是我的一个短板,尤其是在学习代码的阶段,经常需要查询各种错误,很是苦逼,一直就想自己做个翻译的脚本,省去打开网页的时间,但是查询之后发现网上的教程都...

1721
来自专栏吴伟祥

Linux下的shell简介(三) 原

        shell的本意是“壳”的意思,其实已经很形象地说明了shell在Linux系统中的作用。shell就是围绕在Linux内核之外的一个“壳”程序...

603
来自专栏张尧博客

10个有用的”ls”命令面试问题(2)

3218
来自专栏王磊的博客

聊聊excel生成图片的几种方式

目录     I:需求。    II:实现思路。     III:实现方式。     IV:优缺点分析。     V:结论。     VI:wps安装与配置。 ...

33911
来自专栏Android相关

All com.android.support libraries must use the exact same version

看info是因为一些lib中使用了examples等,而那些example中使用了27.0.2版本的support library,和我们本身使用的suppor...

2402
来自专栏地方网络工作室的专栏

nodejs 升级后, vue+webpack 项目 node-sass 报错的解决方法

关于 node 环境升级到 v8^ 以上,node-sass 报错的解决方法 今天给同事电脑升级了一下系统,顺便升级了所有的软件,发现原来好好的项目报错了。报错...

3088
来自专栏宋凯伦的技术小栈

【从业余项目中学习2】C# 实现调用Matlab函数(Visual Studio:2008, Matlab:R2009a)

  最近正在给客户做的个人项目,要求实现C#与Matlab之间的调用,即C# winform界面收集用户输入的参数,将参数传递给Matlab的算法计算,Matl...

2687
来自专栏熊训德的专栏

Hbase replication源码分析

本文主要通过Replication的核心原理和failover 过程,对Hbase replication源码分析,希望对大家了解Hbase源码有所帮助。

8230
来自专栏SpringBoot 核心技术

第一章:Maven环境下如何配置QueryDSL环境

1103

扫码关注云+社区