node modules

node_modules 目录实在太复杂了,动不动就是几万个小文件。每次安装完,Spotlight 就开始扫描、iCloud 就开始上传,电脑就比较难受了。

解决方法一

在你的 bash 中加入以下函数

1
2
3
4
5
6
# $1 is "find path"
# $2 is "folder name"
# example "nosync ~ node_modules"
nosync() {
find $1 -type d \( -name "$2.nosync" -a -prune \) -o -name "$2" -exec bash -c "cd {}/.. && mv $2 $2.nosync && ln -s $2.nosync $2" \; -a -prune;
}

命令有点长,但是我觉得我可以解释😆

懒人版概要

iCloud 会忽略名称以 .nosync 结尾的文件/文件夹,以达到阻止同步的效果。
该脚本通过对目标文件夹末尾添加 .nosync,并添加软链接到之前的文件夹名,来实现阻止同步文件夹的效果。

使用方法

你可以将该脚本加入你的 .bashrc 或者 .zshrc 中,然后 source 一下或者重新打开命令行即可使用该函数

该函数接收两个参数

  1. 第一个参数是待查找的目录,会递归查找,如 ~ 表示从用户目录开始递归查找
  2. 第二个参数是需要取消同步的文件夹名,注意,此版本我没有兼容文件的取消同步,只能使用在文件夹上,如 node_modules

例如我想递归查找 Document 目录下的 node_modules 文件夹,并且不让他们同步,则我需要执行的命令是

nosync ~/Documents node_modules

脚本解释

脚本使用了 find 命令,

find $1 表示我们指定查找的起始位置为函数的第一个参数。-type d 表示我们查找的目标是文件夹类型的。

find 中有两种逻辑命令 -a 表示 and/且,-o 表示 or/或。还有一种有用的命令 -prune,相当于大多数编程语言中的 continue,表示取消当前的递归。

\( -name "$2.nosync" -a -prune \) 表示如果发现该文件夹已经被我们修改过了,那我们就直接跳过这个文件夹的递归了,大大减少了计算量。

-o -name "$2" -exec 表示如果名称是我们的修改目标,如 node_modules,那就执行一个脚本。-exec 表示执行脚本,后面紧跟的就是脚本内容,脚本内容应该以字符串 ;结尾,所以我们还加入了一个转义符号\,因为这些内容都是 find 的字命令的内容,不转义则会被 find 命令给捕获到。

bash -c "cd {}/.. && mv $2 $2.nosync && ln -s $2.nosync $2" 这就是我们的脚本内容了,使用 bash -c 来执行另一段以字符串形式提供的 bash 命令。在 -exec 命令中,{} 代表当前匹配到的文件目录,"cd {}/.. && mv $2 $2.nosync && ln -s $2.nosync $2" 则很简单了,我们进入匹配到的文件的父目录,然后重命名目标文件夹,最后生成一个软连接。

上面这个脚本我多提一些,因为 {} 的替换只能在 -exec 的一级命令中,不能通过 dirname 去获取父目录,因为他是二级命令了,获得到的永远只能是.,则无法获得到真实的父目录,所以采用了 cd {}/..这种传统手艺。

那回过头来,为什么要获得父目录呢,直接 mv 和 ln 不就行了?其实 mv 是没问题的,关键是 ln 命令,它会使用相对目录,如果在创建链接的时候,使用的是更高一级的相对目录,在目标文件夹中访问时就会出错,并且考虑到父级目录随时可能改变,也会造成链接的失效。所以放弃了绝对目录和远距离相对目录的实现方案,采用了最短距离的相对目录,这样就能把改动的风险降到最低,可靠性提到最高。

那最后的 -a -prune; 则表示,如果当前目录是目标目录,我们在前一部分的 exec 刚刚将其处理完,所以也将跳过此目录,不再递归它的子目录,达到提高遍历效率的目的。

OK,就这么多,iCloud 的同步也太简陋了,期待有更好的同步控制。

解决方法二

解决方法一有一个致命弱点:git 会自动索引 node_modules.sync,当然有更好的程序可以自动将该文件夹加入 .gitignore 中,但总觉得还是不是特别舒服,两个 node_modules 看起来就不舒服,而且和团队协作的时候会造成困扰。

这两天看到了一篇远古文章node_modules 困境,然后开始了解 pnpm 这个包管理工具。

发现它对于项目内 node_modules 文件夹的处理方式正是我期待的系统链接方式,这样整个 node_modules 内文件数量的大小瞬间变小了好几个数量级。

但软件不是黑魔法,前端更不是。他是将所有的模块都安装到了 node_modules/.pnpm 这个文件夹中了。但是好消息是,pnpm 提供了一个配置项 virtual-store-dir,可以自定义 .pnpm 文件夹的位置!

这就好办了,我们把 .pnpm 目录统一存放到单独的缓存目录中,如 ~/Library/Caches/PnpmCache/ 这样。

  • iCloud 不会去同步这个目录
  • git 不会去扫描这个目录
  • Spotlight 不会去索引这个目录

简直完美!

但是这里有个大问题: Cache 文件夹是系统的缓存文件夹,在每次系统重启后就会被清空(由于我极少重启电脑,就算被删除也能在几秒内安装回来,反而能让系统帮我自动清理长久不用的包,这个位置比较适合我。如果你是经常重启设备的话,可以尝试换其他位置存放)

但是注意:不同项目的依赖包不应放到同一个 node_modules 文件夹下,所以,我想了一个临时折中的办法 - 加 hash,

就是每个项目会有对应的一个以时间戳生成的 md5 hashecho $(date) | md5 或者 echo $(pwd) | md5,然后将此 hash 作为缓存目录的文件夹名称,如这样

~/Library/Caches/PnpmCache/a9564ebc3289b7a14551baf8ad5ec60a/

然后将这个地址设置为当前项目的 virtual-store-dir,具体方法就是,在项目根目录的 .npmrc 文件中,添加一个记录

1
2
# .npmrc
virtual-store-dir=~/Library/Caches/PnpmCache/a9564ebc3289b7a14551baf8ad5ec60a/

最后,使用 pnpm install 安装依赖就可以了,node_modules 文件夹从之前的几万个文件变为现在的一百多个;从几百 MB 变为现在的 几十k,简直神了!(Ryan 当时哪儿能想到 node_modules 会成这么个无底洞啊😂)

步骤总结

  1. 生成一个完全不与之前重复的 hash,如 a9564ebc3289b7a14551baf8ad5ec60a (通过 echo $(date) | md5echo $(pwd) | md5 得到)
  2. 添加一个记录 virtual-store-dir=~/Library/Caches/PnpmCache/这里写你刚生成的hash/ 到你的 .npmrc 文件中
  3. pnpm install

我后面再看看这能不能更自动一点😆

更新

我写了一个 bash 脚本:

1
2
3
4
5
6
7
8
9
10
11
cache-virtual-store-dir() {
if [ -f ".npmrc" ]; then
if [ `grep "^virtual-store-dir=" .npmrc | wc -l` -gt 0 ]; then
:;
else
echo "virtual-store-dir=~/Library/Caches/PnpmCache/"`echo $(pwd) | md5`"/" >> .npmrc;
fi;
else
echo "virtual-store-dir=~/Library/Caches/PnpmCache/"`echo $(pwd) | md5`"/" > .npmrc;
fi;
}

每次执行 pnpm install 的时候,先会自动执行上面的函数,自动管理 .npmrc 文件。