标签归档:npm

让docker构建nodejs应用时使用npm缓存加速安装

我的node应用在原本的部署脚本下,每次部署都要十几分钟,而我的项目又不适合多阶段构建,于是想了各种办法让它使用镜像layer缓存一部分安装过程,但是多多少少都有点问题。

看来想要不出问题的话还是要在部署的时候让安装过程完整跑一次,这样要缩短部署时间就只能尽量利用npm缓存。

第一步:挂个外部缓存

我本来以为dockerfile构建过程中是没法像容器那样绑定一个volume来让过程文件持久化的,但是昨天我才发现其实是有的,就是要更新一下docker,这是Buildkit特性的一部分,这个特性在18.09版本之后的docker才有,总之尽量把docker升级到最新版就可以用了。

使用方法很简单,如下Dockerfile:

FROM node:18.18.1-buster-slim
RUN apt update &&\
	apt install -y git openssh-client python3 curl
COPY ./deploy /deploy
COPY ./app/ /app
RUN --mount=type=cache,target=/root/.npm \   #在RUN命令的开头这样写来挂一个缓存目录,这个mount参数需要在每个要使用此缓存的命令里都写一遍
	sh /deploy/install.sh   #然后执行你的安装脚本
WORKDIR "/deploy"
ENTRYPOINT ["/bin/sh"]
CMD [ "./start.sh" ]
EXPOSE 80

这样在这个dockerfile构建过程中就会把构建容器的`/root/.npm`映射到通用的docker缓存里,而且我测试下来如果在其它镜像里也挂载同样的缓存,那么其它镜像构建的时候也可以使用该缓存,但我其它镜像的FROM镜像都是相同的,不知道如果来源镜像变了是否会影响缓存挂载。

注意:~/.npm 是linux下npm默认的缓存目录,但默认缓存目录是可以更改的,如果你的项目里修改了npm的默认缓存目录地址,那么这里也要一起改。或者如果你的平台比较特殊,npm的默认目录本来就不在这里,那么也要进行对应的修改。

除了挂载缓存(type=cache)以外,还可以绑定外部文件或目录(type=bind),相关资料在这里https://docs.docker.com/build/guide/mounts/,不过这个文档有点迷惑,没写source和target哪个是里面哪个是外面的,我在source里面写外部路径它给我报文件不存在,然后我就直接用cache了,没再继续尝试。

如果你觉得构建过程缓存的文件有问题,或者单纯想清除这些缓存,可以根据这里的方法,执行以下命令:

docker builder prune --filter type=exec.cachemount

如果没有效果,可以尝试去掉–filter及其参数。

第二步:让npm优先使用缓存(可选)

npm在安装依赖时,即使本地有缓存,也会向服务器发起请求检查每个本地的缓存有没有过期,这个过程也很漫长。其实一般即使缓存过期了也问题不大,因为正常来说同一个版本号的包其内容是不会变的,所以可以让npm优先使用本地缓存,跳过检查其在线状态,这样可以大幅减少安装时间。

方法很简单,只要给安装命令加个`–prefer-offline`参数:

npm i --prefer-offline
#也可以再加个 --verbose 参数确认是否真的使用了缓存
npm i --prefer-offline --verbose

这样折腾完了之后,应用的后续部署时间在依赖没有改变的情况下从原来的十几分钟缩短到了一分多钟,堪称火箭级加速,总算解决了一个困扰我一年多的问题。

[nodejs]同时使用devDependencies和dependencies以处理本地依赖链接

我做了一个自用的工具包,但我想把其中一部分独立出来放到其它项目里用,所以我把它们放在一个命名空间里发布了出来。

于是就出现了一个问题,本来这些包是属于一整个项目,其中的模块存在一些相互依赖,既然要分开发布那么package.json里依赖模块的目标就不能再写原来的本地路径了,需要改成发布的版本号或者项目地址。

本地开发时依赖路径填写本地路径,执行npm i会帮你直接创建一个目录链接到目标目录上,这样你在目标依赖里的改动就不需要发布或者复制就可以直接在当前项目里测试,但如果需要把依赖目标改成模块的发布版本号的话,即使手动把node_module中安装的模块删除手动创建目录链接,执行npm命令在有些情况下也会把已经创建的链接删除并重新从npm下载发布版本,这样会导致开发时不知不觉使用错误的依赖。

刚刚我测试了一下,发现一个模块可以同时出现在package.jsondevDependenciesdependencies里,而且默认情况下npm会优先找devDependencies里模块的目标是否有效,有的话就会选用这个安装,因此要解决我上面遇到的问题,其实只要把需要本地开发的发布模块同时写在这两个字段里,在devDependencies里的目标写本地路径,在dependencies里的目标写发布版本即可,这样在本地存在目标路径时npm就会优先帮你创建路径,而不是从npm下载发布版。参考如下:

{
	"devDependencies": {
		"@jialibs/utils": "file:../Utils"
	},
	"dependencies": {
		"@jialibs/utils": "^1"
	}
}

就是有两点要注意,如果被依赖的模块版本变了,要注意同步修改这个里面依赖的版本,否则其它地方从npm下载就会安装老版本的依赖,或者另一个解决方案就是像我这样直接一个大版本号或者写到次版本,这样只要大版本或次版本没变,那么别处就始终会下载最新版(已经安装过的需要清除package-lock.json,否则npm还是会按照lock里的版本安装)。

另一点要注意的是如果在生产环境为npm i命令显式添加了--omit=dev这样的参数的话,会导致在devDependencies里出现过的包都不会被安装,这种情况可以尝试把本地路径的包放到`peerDependencies`中。

[expressjs]捕获路由和中间件的异步错误

express本身可以写async函数作为路由或者中间件(以下简称路由)使用,但如果在其中直接抛出异步错误的话则不会被捕获并传递给错误处理路由,需要在每一个async路由中手动写try/catch来捕获并处理错误,这显然对于拥有统一错误处理逻辑的框架是不友好的。

我不知道为什么express始终没有在4.x版本添加这个支持,看起来这应该并不会导致什么兼容性问题,即使不默认全局开启这个功能,也可以添加一个选项或开关函数来允许使用者开启异步错误捕获,但它始终没有这么做。

在这之前npm上也已经有一些可以实现这个功能的补丁包,不过它们几乎都是给创建出的app实例的所有相关路由方法套了一层壳在里面try/catch错误,导致如果是自己用一些特殊方法创建的路由对象,就失效了。

我原本用了一个直接修改express源码中相关原型方法的包,叫做express-async-errors,不过这个包没考虑到项目引用其它位置的express包的情况,它直接在代码中对相关源文件进行了require,导致无法作用于项目外依赖库的express,所以我重写了一个,而且源码比它还要简单,因为我直接把`Layer`类的handle方法改成了`async`函数,然后在里面`await`我们的路由函数,目前还没发现什么副作用,在我的项目中直接使用没有出现问题。

包地址:express-async-error-patch

`asyncPatch`函数需要传入一个创建出来的app实例, 为的是能获取到app所属的express包里的构造函数,不是为了在实例上包装方法。该函数在任何阶段调用都可以,只要在处理请求前调用即可。

[Node.js]解决‘gbk’ codec can’t decode byte 0x80 in position

每次换一个环境执行npm i都会碰到这样的问题,记录一下解决方法,

方法1:手动去报错的文件里指定编码为UTF-8,我自己第一次解决这个问题就是用的这个办法,刚刚想找找看有没有别的解决方法的时候发现别人也有去手动改源码的,放个参考链接

简单地说就是找到报错的那行,给open函数加个`encoding=’utf-8’` 参数就可以解决问题,但这样属于改了人家文件,更新后还是会变回去的,所以现在我不这样做了。

方法2:去控制面板的区域设置里到“管理”标签页下,“更改系统区域设置”,把“Beta版:使用Unicode UTF-8提供全球语言支持(U)”勾上,然后重启一下,python就会默认用utf-8来读文件了。

这个方法会改变整个系统的默认代码页,最主要的影响是会导致使用非Unicode的程序乱码甚至崩溃,还有在gbk设置时以ansi保存的含有中文的bat脚本会乱码。如果影响不大的话倒是可以让系统一直保持在这个区域选项上,影响某些软件的正常使用的话在执行完需要的编译任务后还得改回去。在这种模式下可以使用“locale emulator”这样的软件以Chinese模式启动程序,以解决程序的乱码和崩溃问题。

方法3:我没试过,理论上应该可行,从python本体入手,把它的默认编码设置成utf-8,参考这里的方案2。这个没用

npm多用户

npm的用户登录信息写在用户目录的`.npmrc` 文件里,所以同一时间只能登录一个账号,要切换账号的话就要重新用`npm login` 登录,比较麻烦。

于是写了个切换.npmrc文件来直接切换用户的shell脚本

分别login到对应的用户,然后查看用户目录里的.npmrc文件内容,复制出来,填到下面对应的地方

#!/bin/bash
if [ $1 = "user1" ]
then
	echo "user : $1";
	echo -e "registry=http://registry.npmjs.org/\n//registry.npmjs.org/:_authToken=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" > ~/.npmrc;
elif [ $1 = "user2" ]
then
	echo "user : $1";
	echo -e "registry=http://registry.npmjs.org/\n//registry.npmjs.org/:_authToken=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" > ~/.npmrc;
fi

保存成比如`npmuser` 文件放在某个PATH目录里,然后用`npmuser user1` 这样的命令就可以直接切换用户而不用再登录了。

Error: Cannot find module ‘npmlog’

手动安装node的时候一不注意就会出现这个错误,装好了之后使用命令`npm` 测试一下出来了这个错误:`Error: Cannot find module ‘npmlog’` 。

 

这一般是放在/bin里的链接不对导致的。

可能情况1

链接指向的不是`node_modules/npm/bin/npm-cli.js` ,初次手动安装难免会搞不清要链接哪个文件,把链接指向正确的位置即可。

情况2

使用的不是软链接,由于npm-cli.js中使用的是相对路径,要是你一不小心链了个硬链接,那么npm就会从/bin解析相对路径,所以就找不到npmlog模块了。要使用软链接,在ln命令后加上`-s` 选项:`ln -s node_modules/npm/bin/npm-cli.js /bin/npm`

 

解决node无法调用全局模块的问题

刚刚我把SPDY装到全局,`npm -g install spdy` ,

spdy@2.0.4 /usr/local/lib/node_modules/spdy
├── http-deceiver@1.2.4
├── handle-thing@1.2.4
├── select-hose@2.0.0
├── debug@2.2.0 (ms@0.7.1)
└── spdy-transport@1.1.8 (obuf@1.1.1, wbuf@1.7.0, hpack.js@2.1.4, readable-stream@2.0.2)

可以看到这个模块被装到/usr/local/lib/node_modules里了

 

 

 

然后开个环境测试一下,结果

> require('spdy')
Error: Cannot find module 'spdy'
    at Function.Module._resolveFilename (module.js:336:15)
    at Function.Module._load (module.js:286:25)
    at Module.require (module.js:365:17)
    at require (module.js:384:17)
    at repl:1:1
    at REPLServer.defaultEval (repl.js:154:27)
    at bound (domain.js:254:14)
    at REPLServer.runBound [as eval] (domain.js:267:12)
    at REPLServer.<anonymous> (repl.js:308:12)
    at emitOne (events.js:77:13)

吓得我满脑子都是doge!!!这是为什么?

继续阅读解决node无法调用全局模块的问题