利用Docker容器搭建高效的Node.js开发环境

阅读时间:15分钟

使用Node.js遇到的问题

Node.js无疑是js开发者的福音,因为它既可用于web端开发,当作构建工具,也可以用于服务端,搭建web服务器。

但使用Node.js(尤其是npm)时也会碰到一些麻烦的事情,比如:

  • Node.js版本问题。Node.js版本更新速度是相当激进的,而且一些时候还是主版本更新,这意味着放弃之前版本的兼容,作为Node.js的御用模块管理工具npm也紧随其步伐进行升级。不同项目可能需要在不同nodejs版本(npm版本)环境下正常(安装模块)运行,所以需要对Node.js(npm)进行版本切换。
  • 模块的跨平台性。一些模块不具备跨平台性,某些重要模块在某些系统环境下会安装失败(我不会告诉你安装node-sass是个多么曲折、容易出错的过程)。
  • 模块目录结构。依赖模块之间目录层级非常复杂,文件数量多,安装非常消耗时间。

针对以上问题,聪明的开发者们想出了各种解决方案

  • 使用n模块或nvm工具对Node.js进行版本切换。
  • 修改环境变量或者使用虚拟机、服务器切换开发的系统环境。
  • 直接拷贝node_modules目录让模块重复利用。

只可惜这些解决方案都是有缺陷的,对于windows系统的开发者来说更是如此:

  • n模块和nvm都宣称不支持Windows,对win10就更不必说。
  • 利用虚拟机开发搭建环境是个麻烦事,服务器开发需要远程调试也不方便。
  • 频繁的拷贝会导致磁盘上有不少重复的node模块,而且随着项目增多,以哪个node_modules文件夹为准来进行拷贝将很难管理。

Docker带来的曙光

既然这些问题无法使用传统的工具解决,那么不妨切换一下角度使用新的工具:Docker。
Docker的使用场景通常是多应用、多服务器的生产环境或持续集成中的测试环境,用于开发环境的案例比较少,能看到的案例也仅仅是启动容器、构建镜像,实际应用、深入探索者寥寥。
出现这种情抛开学习成本不考虑,一方面可能就现在的开发方式而言,分工越来越细,主张各种“分离”,开发者通常只需要在某种单一环境下,而人们了解Docker大多有以上先入为主的观念,所以未曾想到用于开发环境;另一方面可能是实际操作中碰到了一些暂时无法解决的问题。
瑕不掩瑜,将Docker用于Node.js的开发环境不仅能解决开头提出的几个问题,而且对于开发完成之后与测试人员、运维人员进行快速对接和实施部署也是非常有帮助的。

用Docker解决使用Node.js中遇到的问题

Docker利用虚拟化技术,利用镜像创建称之为容器的虚拟运行环境来运行我们的程序。
所以对于切换nodejs版本的问题,只需要利用不同版本的nodejs镜像来创建容器即可轻松实现。

Docker本身在Linux、Mac、Windows三大操作系统都能安装(win10以下版本需要使用docker toolbox),所以Docker容器是跨平台的,镜像一致即容器一致。
调试起来也非常方便,利用Node.js自带的inspector功能,配合visual studio code的remote debuger可轻松实现断点、重启等操作。

要减少npm install的时间,在不修改npm本身的情况下,路径只有一条:缓存node_modules下的模块。

一种缓存方式是创建一个容器做代理服务器(或私有仓库),将npm的registry地址指向该代理服务器进行模块安装。
代理服务器从远程仓库下载模块并返回给请求安装的容器,同时该缓存模块。下次请求该模块中直接从缓存中读取。
这种方式的麻烦之处不是搭建代理服务器(或私有仓库),而是效率不高,一方面还是需要发送http(s)请求来下载解压模块;另一方面对于开发者来说还是每个代码仓库一个node_modules,其中可能相当一部分是重复的;更为严重的是,这些模块和当前系统还不一致(容器中安装的是linux系统使用的模块,而开发者安装的系统可能是Windows或Mac)。

所以比较好的方式应该是在开发者本地进行缓存,建立一个模块池,不同的项目都可以从这个模块池中找到自己想要的模块,如果找不到就往池里面新增模块。这样一方面可以增加模块的最大利用率,避免因不同项目存储多份相同的模块,从而节省了大量的空间。更重要的是,对于已安装的模块,npm不会再发送请求下载模块,从而节省了大量的时间。这些节省的空间和时间优势,会随着项目数量和规模的增长而变得更加明显。

然而这要实现起来却并非上述那般容易,就连node.js官方的文档也只有关于如何将应用部署到Docker容器中的介绍。

《Dockerizing a Node.js web app》

《Docker and Node.js Best Practices》

下面就来详细介绍具体操作

用Docker搭建Node.js开发环境实例

虽然Node.js在前后端开发使用场景作用差别很大,前端通常用来运行构建工具,如gulp、webpack等,后端则可以直接执行js代码启动服务器。
不过目录结构大体相同,所以可以放在一起讨论。下面是个简单的项目结构示例,代表了项目种的几类文件和目录。
我们现在有个Node.js项目,在 C:\Repo\project 路径中

1
2
3
4
5
6
|
- src //源文件目录
- node_modules // node模块
- package.json // node模块管理工具
- dist // 被压缩、合并、编译生成的目标代码目录
...

在这个项目结构中,srcpackage.json是开发中可能需要修改的目录和文件,node_modules是需要被缓存的目录,dist是需要用来部署、执行的目录。

用Docker容器管理项目在此便会出现分歧。

一部分开发者可能会如前面提到的文章一样,通过编写Dockerfile来构建镜像,将node_modules放入镜像中,然后挂载源码路径,这样以后搭建开发环境可以直接通过镜像创建容器来实现,不过实现的问题是:

每次配置文件改动、模块依赖变化都需要重新build,即使不build直接在容器中用npm安装,但是由于package.json是文件,无法挂载,所以导致又需要手动同步配置文件,这些都是相当麻烦而且容易出错的。

另一部分开发者可能会想到直接挂载整个项目,如

docker run -itd -v C:\Repo\project:/project --name project node:7-alpine

但是这样并不能缓存node_modules目录,只是在容器中执行node进程而已。

那把node_modules再单独挂载一次不就行了?很遗憾,Docker并不支持这种嵌套挂载宿主机目录的方式~

但这个思路已经很接近我们想要的结果了,所以我们要用另外的方式挂载node_modules来进行缓存,比如说把它和另一个容器进行挂载。

先创建一个容器用来缓存node_modules,并暴露 /project/node_modules 作为挂载路径

docker run -it -v /project/node_modules --name node_modules alpine

然后创建容器时通过 –volumes-from 实现与缓存容器共享 /project/node_modules。

docker run -itd -v C:\Repo\project:/project --volumes-from node_modules --name project node:7-alpine

再project容器中安装一个lodash 模块试试是否成功。

docker exec -it project npm i lodash

创建一个临时容器看看是否安装成功。

docker exec -it --rm --volumes-from node_modules -w /project node:7-alpine ls node_modules

成功显示lodash,打开宿主机上的文件夹,node_modules下空空如也,证明模块已安装到容器中,且可以重复使用。

开发环境优化

把数据保存在容器中并不是一种值得推荐的做法,抛开Docker守护进程和容器本身的稳定性不说,容器也存在一定被误删的可能性。
而这种共享卷的方式有个更麻烦的问题是所有想利用这个缓存卷的容器目录结构都必须是 /project/node_modules,这样的限制就显得很不友好了。
另外用来缓存卷的容器基本上算是浪费了,起不到什么实质性的作用。

顺着挂载卷这条线索继续往下找,便可以发现更好的解决方法:创建一个Docker volume用来共享容器间的数据。

docker volume create node_modules7

首先的好处便是这个volume可以叫任意名字,也可以挂载到容器不同的路径下。这里之所以加上“7”是因为不同npm版本组织模块的方式会有些不同,这里通过对node版本号来进行标注,表示这些模块可用于Node.js 7版本。

这时候创建容器我们便可以用node_modules7这个volume进行挂载了

docker run -itd -v C:\Repo\project:/project -v node_modules7:/project/node_modules --name project node:7-alpine

这次我们安装underscore测试一下

docker exec -it project npm i underscore

依旧创建一个临时容器看看是否安装成功,不过我们挂载到容器的/app/node_modules目录下试试

docker exec -it --rm -v node_modules7:/app/node_modules -w /project node:7-alpine ls node_modules

显示underscore目录,表示模块共享成功。

最大问题——自动编译/刷新

利用Docker容器搭建开发环境可能碰到最麻烦的问题便是无法实现自动刷新、编译,尤其是对于系统环境为Windows的用户,由于NFS存储卷底层的事件消息与Docker容器通信有些问题,导致Node.js模块的监听文件变化的功能失效。(据部分开发者反映Mac系统下可能也会出现这种问题)
所以主流常用的Node.js模块都会支持设置参数来开启以轮询模式,检测文件修。

其中包括前端目前常用的构建工具gulp就支持开启poll模式

1
2
3
4
5
6
gulp.watch('xxx', {
mode: 'poll', // 开启轮询模式
interval: 500 // 轮询间隔
}, function() {
...
})

前端模块构建工具webpack也是提供了poll选项。

1
2
3
watchOptions: {
poll: 1000 // 开启轮询模式并设置轮询间隔
}

虽然Node.js作为服务端不需要构建工具,但是在实际开发中,我们希望修改代码的时候服务器能自动重启,所以会使用nodemon这样的模块来帮我们运行代码。而nodemon本身也支持“-L”参数来进行轮询检查代码。例如:

nodemon -L app.js

总结

对于其他语言环境的开发者,相信对Docker容器的卷共享、网络通信等基本概念有所熟悉之后,结合实际开发情况,参照本文所述思路,应该也可以搭建理想的开发环境。


一部由众多技术专家推荐, 帮你成为具有全面能力和全局视野工程师的进阶利器—— 《了不起的JavaScript工程师》出版了! 点击下方链接即刻踏上进阶之路!


亚里士朱德 wechat
更多WEB技术分享请订阅微信公众号“WEB学习社”