cnpmjs.org实战

对Nodejs有一定了解的童鞋都应该知道,国内访问官方源 (https://registry.npmjs.com )需要漂洋过海,对模块的安装速度影响很大。

cnpm的诞生为国内Nodejs从业者带来了福音,向cnpm开发团队致敬~

此前,同事在公司内部搭建了一个cnpmjs.org@2.12.2, 部署方式基于源码克隆 + MySql(以下称『旧cnpm』)。

近日,由于某种原因,需要在另一台服务器(ubuntu16.04)重新搭建一个cnpmjs.org实例(以下称『新cnpm』)。

如果你只想以最快的速度知道最简单的步骤,请跳过『踩坑之旅』,直接往下看 推荐部署方案 即可,前提是你的环境与我类似。

踩坑之旅

从官方文档上看,注意到有这么两个链接:

当时我是懵逼的,后来两个链接都进去看一番才大致明白,这应该是两种部署方式,简单概括翻译一下,即:

  1. 5分钟简易式部署
  2. 克隆源码式部署

我们来一一解读文档:

5分钟简易式部署

文档地址:https://github.com/cnpm/cnpmjs.org/wiki/Deploy-a-private-npm-registry-in-5-minutes

1. 通过npm安装全局依赖

执行:

1
2
3
4
sudo npm install -g --build-from-source \
--registry=https://registry.npm.taobao.org \
--disturl=https://npm.taobao.org/mirrors/node \
cnpmjs.org cnpm sqlite3

可以看到,这条命令,为了照顾国内用户,指定了从淘宝源安装cnpmjs.org,cnpm,sqlite3三个模块,并且从源码构建。
我这台机器的node是通过apt-get安装的,所以必须通过sudo安装全局模块
build期间会调用到gcc,g++,make等命令,可能需要额外安装。
但是!!!
在ubuntu16.04下居然报这个错(Centos 6也有类似问题):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
gyp WARN EACCES user "root" does not have permission to access the dev dir "/home/fe/.node-gyp/7.10.1"
gyp WARN EACCES attempting to reinstall using temporary dev dir "/usr/lib/node_modules/sqlite3/.node-gyp"
make: Entering directory '/usr/lib/node_modules/sqlite3/build'
make: *** No rule to make target '../.node-gyp/7.10.1/include/node/common.gypi', needed by 'Makefile'. Stop.
make: Leaving directory '/usr/lib/node_modules/sqlite3/build'
gyp ERR! build error
gyp ERR! stack Error: `make` failed with exit code: 2
gyp ERR! stack at ChildProcess.onExit (/usr/lib/node_modules/node-gyp/lib/build.js:258:23)
gyp ERR! stack at emitTwo (events.js:106:13)
gyp ERR! stack at ChildProcess.emit (events.js:194:7)
gyp ERR! stack at Process.ChildProcess._handle.onexit (internal/child_process.js:215:12)
gyp ERR! System Linux 4.4.0-89-generic
gyp ERR! command "/usr/bin/nodejs" "/usr/lib/node_modules/node-gyp/bin/node-gyp.js" "build" "--fallback-to-build" "--module=/usr/lib/node_modules/sqlite3/lib/binding/node-v51-linux-x64/node_sqlite3.node" "--module_name=node_sqlite3" "--module_path=/usr/lib/node_modules/sqlite3/lib/binding/node-v51-linux-x64"
gyp ERR! cwd /usr/lib/node_modules/sqlite3
gyp ERR! node -v v7.10.1
gyp ERR! node-gyp -v v3.6.2
gyp ERR! not ok

我这台服务器安装的node是7.10.1版本,而且/home/fe/.node-gyp/7.10.1这个目录是不存在的, 很懵逼。。。
经过一番google,发现加上--unsafe-perm参数即可解决问题,即:

1
2
3
4
sudo npm install -g --build-from-source --unsafe-perm \
--registry=https://registry.npm.taobao.org \
--disturl=https://npm.taobao.org/mirrors/node \
cnpmjs.org cnpm sqlite3

想起来这个问题,跟bower有些类似,都是不建议通过root来执行,通过添加类似『允许root』的参数来解决问题。
后来做实验确认了下:
如果你是通过Linux的包管理器(比如ubuntu的apt-get,centos的yum)来安装nodejs,nodejs会被安装到/usr/bin/node, npm全局模块会被安装到/usr/lib/node_modules, 而这个这个目录是需要root权限才能写入的,所以安装全局模块需要加sudo,这就与上述的编译过程矛盾了, 加上--unsafe-perm才可解决问题。

如果你是通过诸如 nvm 这种工具安装nodejs的话,就不会有此问题,一切按照官网的步骤来就行了。

步骤小结:
此步骤安装了cnpmjs.org, cnpm, sqlite3三个全局模块,分别用于:
cnpmjs.org: 部署cnpm服务,同时安装了一个同名全局命令
sqlite3:cnpm服务的轻量级数据库
cnpm:cnpm客户端

2. 启动服务

执行:

1
2
nohup cnpmjs.org start --admins='myname,othername' \
--scopes='@my-company-name,@other-name' &

此句命令就『在后台』启动了cnpm服务,并且设置了myname,othername这两个用户为管理员,@my-company-name,@other-name这两个scope前缀。
管理员的权限见这里 https://github.com/cnpm/cnpmjs.org/wiki/User-permissions#admin-users

此时可以通过curl http://localhost:7001curl http://localhost:7002测试cnpm服务,但是别的客户端无法访问,因为cnpm内部绑定了127.0.0.1,可以通过修改配置让别的客户端访问,但还是建议通过nginx反向代理来访问,而且最好通过域名(解析到局域网)代理,方便记忆。
至于如何设置反向代理,请自行google~

关于『后台运行』这里我踩到了一个坑。我在服务器上使用的是zsh(通过oh-my-zh管理),而zshnohup命令做了一个『贴心』的功能:
当tty登出的时候,会把所有通过nohup启动的进程都杀掉,所以cnpmjs.org服务也一起退出进程了。

解决方案很简单,只需要在上述的命令后追加一个叹号即可(方案之一),即:

1
2
nohup cnpmjs.org start --admins='myname,othername' \
--scopes='@my-company-name,@other-name' &!

停服命令:

1
cnpmjs.org stop

该命令会读取『cnpmjs.org start的时候,写到~/.cnpmjs.org/pid』中的进程id,然后通过kill -15杀掉进程。
但是这个pid文件不稳定,有时候起服务并不会生成pid文件,导致无法通过cnpmjs.org stop停止服务,经排查已确认为bug,已提PR.

步骤小结:
此步骤启动了cnpmjs.org服务。

3. 设置客户端源

在『本地』机器执行:

1
cnpm set registry http://your.cnpmjs.registry.domain

然后就可以通过cnpm类似操作官方源那样使用私有源了。

5分钟简易式部署小结:
优点:

  1. 简洁优雅
  2. 无侵入式
    缺点:
  3. 通过命令行启动只能传入adminsscopesdataDir三个参数,可配置性较低。
  4. sqlite性能过低,当时整个源的包数量好像10个不到,一个/total请求,居然要20多秒(当初没有截图)才返回,简直不能忍。。。或许我哪里姿势不对?如果你的sqlite方式请求很快,请告诉我- -

克隆源码式部署

文档地址: https://github.com/cnpm/cnpmjs.org/wiki/Deploy
这里的文档又是让人懵逼:

  1. 文档首页说支持这些数据库: MySQL, MariaDB, SQLite or PostgreSQL,但是到这里的Dependencies里就只剩Mysql了,其他数据库只字未提
  2. Mysql不是必须的,可使用内置的sqlite3,只是性能比较差
  3. 云存储不是必须的,使用内置的fs-cnpm也没什么问题,速度挺快的,局域网够用了
  4. 有一个大标题是『Deploy With NPM Module [not recommend]』,点到那个example项目,似乎并不如预期中的『使用Npm部署』,倒是『5分钟简易式部署』才是正宗的『从Npm部署』。另外看文档的意思,这里的『从Npm部署』方式,必须使用Mysql?但从我踩坑的情况来看,无论哪种方式部署,Mysql都是可选的,但我个人是建议使用Mysql的。建议官方把Mysql初始化脚本拉到更高的级别去提供链接。

接下来开始:

安装源码

执行:

1
git clone git://github.com/cnpm/cnpmjs.org.git $HOME/cnpmjs.org

即从github克隆整个cnpm仓库。
这里有个注意点,在我这个时间点,官方居然把『3.0.0-alpha.15』的alpha版本推到了master分支,这个我表示不理解,既然是alpha版本,就不是稳定的,既然不是稳定的就不应该放在master分支上。
所以建议在3.x出稳定版前,切到2.x版本。即

1
git checkout 2.x

我这里安装的是2.x最新版2.19.4。

安装数据库

这一步其实是可选的,但是我强烈建议,因为默认的sqlite性能简直堪忧。
下面这段shell我继续懵逼:

1
2
3
4
# create mysql tables
$ mysql -u yourname -p
mysql> use cnpmjs;
mysql> source docs/db.sql

我服务器上的Mysql都是刚刚安装的,何来cnpmjs这个数据库实例?所以正确的脚本应该是:

1
2
3
4
5
6
7
# create mysql tables
$ mysql -u yourname -p
# input password
# 推荐使用这个数据库名
mysql> create database cnpmjs;
mysql> use cnpmjs;
mysql> source docs/db.sql

添加配置文件

使用vim编辑配置文件:

1
vim config/config.js

内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module.exports = {
debug: false,
enableCluster: true, // enable cluster mode
enablePrivate: true, // enable private mode, only admin can publish, other use just can sync package from source npm
database: {
db: 'cnpmjstest',
host: 'localhost',
port: 3306,
username: 'cnpmjs',
password: 'cnpmjs123'
},
admins: {
admin: 'admin@cnpmjs.org',
},
syncModel: 'exist'// 'none', 'all', 'exist'
};

然后保存退出(:wq或:x)
这里又是一个矛盾:
上一步,刚让人安装Mysql,这一步的配置,却是使用默认的『sqlite』数据库(通过阅读默认配置config/index.js可知)?
所以,如果要使用Mysql,与上一步对应的正确的配置应该是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module.exports = {
debug: false,
enableCluster: true, // enable cluster mode
enablePrivate: true, // enable private mode, only admin can publish, other use just can sync package from source npm
database: {
db: 'cnpmjs',
username: 'your_mysql_user',
password: 'your_mysql_password',
dialect: 'mysql', // 这句是重点
host: 'localhost',
port: 3306, // mysql默认端口
pool: { // cnpm默认配置
maxConnections: 10,
minConnections: 0,
maxIdleTime: 30000
},
logging: false // 不打印数据库log日志
},
admins: {
yourAdminName: 'yourAdminName@your.company'
},
syncModel: 'exist' // 'none', 'all', 'exist', 推荐exist模式
};

安装依赖

执行:

1
npm install --build-from-source --registry=https://registry.npm.taobao.org --disturl=https://npm.taobao.org/mirrors/node

这里可能会遇到与『5分钟简易式部署』同样的问题,这里不再赘述。

启动服务

执行:

1
npm start

同样是在『后台运行』cnpmjs.org服务,与『5分钟简易式部署』一样提供了registry与web页面。
停服命令:

1
npm run stop

这里的机制是,通过ps ax读取pid,然后kill。

克隆源码式部署小结:
优点:

  1. 有一个配置文件,可以自定义多种配置
  2. 由于克隆了整个cnpm仓库,可以切换历史版本(有需要的话),这似乎算不上优点- -
    缺点:
  3. 安装较为繁琐
  4. 需要自己为其创建一个目录,然后启动/停止服务都要进入到该目录去执行npm命令,不够优雅。

思考

以上均为根据官方文档的操作,然后我思考:有没有可能,将两种部署方式的有点结合起来呢?
经过两天的摸索,终于达到预期效果。见下一节。

推荐部署方案

让我们不看官方文档,重新来一次。
环境:
ubuntu16.04
shell: zsh(oh-my-zsh)
Nodejs(通过nvm安装, 不推荐通过包管理器安装,否则会有权限问题)
部署方式:
Npm + Mysql(性能较优)
客户端:
官方npm(cnpmjs.org兼容官方npm客户端) + .npmrc

如果你的环境与我不一致,请自行变通奥~

安装cnpmjs.org

执行:

1
npm i cnpmjs.org -g

安装Mysql 并初始化cnpmjs数据库

执行:

1
sudo apt-get install mysql-server -y

创建数据库实例并初始化表结构:

1
2
# 你可以使用别的用户登陆
mysql -u root -p -e "create database cnpmjs;use cnpmjs; $(cat $(npm config -g get prefix)/lib/node_modules/cnpmjs.org/docs/db.sql)"

稍微解释一下,这句命令。
引号里最终其实就是一连串的mysql命令,先创建数据库实例,然后use它,接着就是建表脚本,读取cnpmjs.org这个全局Npm包里的sql脚本。最后把整个mysql脚本通过-e传给mysql。
这里有一个小风险,如果将来的cnpmjs.org Npm发布包把sql脚本删掉,那这里就会运行失败,万一发生了,你可以到 https://github.com/cnpm/cnpmjs.org/blob/master/docs/db.sql 这里去复制脚本,人肉建表。

创建配置文件

对,你没看错,官方文档上没有说起这个,通过看源码知道可以在这提供配置文件!!!
执行:

1
vi ~/.cnpmjs.org/config.json

请注意是json文件,如果.cnpmjs.org目录不存在,请自行创建
内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
"debug": false,
"scopes": [
"@yourScopeName"
],
"enableCluster": true,
"enablePrivate": true,
"enableCompress": true,
"syncModel": "exist",
"registryHost": "",
"database": {
"db": "cnpmjs",
"username": "root",
"password": "root",
"dialect": "mysql",
"host": "localhost",
"port": 3306,
"pool": {
"maxConnections": 10,
"minConnections": 0,
"maxIdleTime": 30000
},
"logging": false
},
"admins": {
"yourAdmin": "yourAdmin@your.company"
}
}

这里需要强调一下registryHost这个字段,如果不设置,那么客户端安装npm包时,tarball将会从r.cnpmjs.org下载,显然这是不对的,将其设置为空字符串,npm客户端将自动从私有源的下载。这个问题在旧源里是没有的,是cnpmjs.org@2.16.2发生的变更

启动服务

执行:

1
nohup cnpmjs.org start > .cnpmjs.nohup.log 2>&1 &!

这就启动了7001端口的registry和7002端口的web页面
接下来就是设置nginx反向代理啦,请自行搞定~

停服脚本:

1
cnpmjs.org stop

设置源

通常私有源是部署在公司内网的,公网无法访问。
我个人建议是这样的:
客户端继续使用官方的npm,并且设置全局源为https://registry.npm.taobao.org, 推荐使用nrm来管理源。
在公司的项目工程根目录添加.npmrc,设置源指向私有源(即cnpmjs.org实例),比如:

1
registry = http://your.cnpmjs.registry.domain/

如此一来,在公司的项目里安装npm包,是从私有源安装(其中可能会有一些私有包),缺点是只能公司内可访问,或在外网通过vpn访问。
在另外一些开源项目中不添加.npmrc配置,或添加.npmrc配置但不指定源,即使用全局源,从淘宝源安装,这样这些开源项目在公网也可以顺利安装npm依赖,尤其是还要与公司以外的人合作的时候。

到这里,如果你还有疑问的话,可以尝试回到『踩坑之旅』章节参考一些坑点。

如何从旧源同步

如果你搭建的是全新的私有源,请忽略这一章节。

如开头所说,我刚刚搭建的是我们公司的第二个源,在旧源已经有好多个私有包了,就涉及到这个同步私有包的问题。

修改配置文件

1
vi ~/.cnpmjs.org/config.json

在根对象下添加sourceNpmRegistry字段,值为旧源。
scopes字段的值改为空数组。scopes字段表示『私有包』的前缀列表,在本源里的包,如果符合这个配置中的任何一个前缀,都将『忽略』从上游同步,这也符合『私有』这个特性,可以理解为『只有我这个源里有,其他源里没有的包』,也就无需从上游同步了,因为别人没有, 所以要暂时去掉这里的scope。

客户端发送同步请求

我们需要『在本地客户端』执行cnpm提供的sync命令,安装cnpm:

1
npm i cnpm -g

然后列出所有需要同步的私有包,执行cnpm sync,如:

1
cnpm sync @yourScope/package1 @yourScope/package2 @yourScope/package3

这个过程需要一些时间,也会在客户端打印比较多的日志,请耐心等待。
同步完毕后,再把配置改回来,全部项目都切换到新源。

关于发布包

发布包的时候,工程里的.npmrc配置registry字段,仍然有效。它指向哪个源,就发布到哪个源。

如果你希望只有少数人才能发布私有包,那么就将enablePrivate设置为true,即只有管理员才能发布包,记得把管理员们的账号名配置在admins字段下(值为对应账号的邮箱,其实无所谓),需要不定期维护这些管理员。
但是这里有一个问题:2.12.2版本强制所有用户都只能发布带scope的包,而2.19.4版本,管理员却允许发布不带scope的包,可能会带来同步操作的重名风险。

如果你希望省事一点,并且避免重名风险,那么就将enablePrivate设置为false

总结

cnpmjs.org总体上讲,对社区贡献还是很大的!!!
这个毋庸置疑,再次感谢cnpm团队,只是文档上需要进一步完善。
欢迎同样踩坑的你互相学习交流~