告诉你为何以及如何搭建一个私有的npm仓库

1,130 阅读14分钟
原文链接: zhuanlan.zhihu.com
作者:百度外卖-王丝雨@阿不思 百度外卖-郑傲@慢城小ZA 百度外卖-汪梦@daisy
转载请标明出处

为何需要搭建私有npm仓库?


npm——我们大家都知道是NodeJS的包管理工具,用于Node插件的管理包括安装、卸载、管理依赖等。


基于npm命令行我们可以快速的安装项目中所依赖的代码模块,甚至可以自己发布一些自己写的插件等。使得我们的项目开发效率得到大大的提升。


那么基于npm我们可以做哪些事情呢?

简单来说就是:

  • 一行命令,(批量)安装别人写好的模块
  • 一行命令,卸载安装好的模块
  • 一行命令,更新到最新(或指定版本)的模块

具体在项目里面常用的就是通过npm install ——快速安装项目里package.json里devDependencies和dependencies所依赖的包文件。从而快速启动一个新的项目。

那么问题来了。。。


发布到npm的模块是开源的,我们只能共享大家都能用的开源模块。


然而,现如今随着业务越来越复杂,项目迭代速度也越来越快,那么项目间的常用业务代码共享变得非常之有必要。而对于公司的业务代码我们当然是不能开源的。这时候就需要搭建一个类似于 http://npmjs.org 平台私有的npm仓库,用于企业里面常用的业务模块,如业务组件的存放和快速安装。


通俗一点来说,我们就是要搭建一个属于企业内部的npm仓库,自己管理包的同时借助npm的命令行工具快速复用业务代码模块或者业务组件。


在没有npm私有仓库之前,本着"避免重复造轮子"的原则,我们可能是人工的手动去从其他各个项目里面将可以复用的业务代码或业务组件copy过来。

图1 没有NMP私有仓库——人工拷贝业务组件


试想一下我们可能需要做哪些工作?

  • 了解哪些已有项目有我们需要的可复用业务组件
  • 找到项目模块代码并定位组件
  • 详细查看组件代码,考量功能是否契合
  • copy组件到自己的项目


一个组件尚且需要如此大费周折,跟别说多个业务组件。况且由于没有组件的说明文档,每一个组件有可能和自己的需求不是那么契合,而这些关键信息都需要我们花费大量时间去重新阅读代码。

同学们,在现如今这个时间就是金钱的时代,我们花费了多少宝贵的时间在这些这些繁琐、无趣并且不具有可靠性的工作上面在我们还在浪费时间去做这些繁琐的杂事的时候,真正的大牛早已快速做完这些去做其他更有意义的事情了。

那么你需要的可能就只是——一个的私有npm仓库,有了私有npm仓库之后我们只需输入几行命令那些早已上传到私有仓库的成熟组件就会自动安装到自己的项目里面。

如果人工拷贝需要1-2个小时的话,那么npm私有仓库命令行安装可能只需要1-2分钟。并且私有仓库一般在自己的服务器搭建,更快速稳定。总结来说私有npm仓库有如下好处:

  • 便于管理企业内的业务组件或者模块
  • 私密性
  • 确保npm服务快速、稳定
  • 控制npm模块质量和安全(防止恶意代码植入)



npm及私有npm的工作原理?


在具体了解搭建过程之前,我们先简单了解下npm以及私有npm基本的工作原理。


我们使用npm安装、共享和分发代码,npm帮助我们管理项目中的依赖关系,那么它是如何做到的呢?

具体过程如下图:

图2 npm工作流程示意图

当我们使用npm install去安装一个模块时,会先检查node_modules目录中是否已经存在这个模块,如果没有便会向远程仓库查询。

npm提供了一个模块信息查询服务,通过访问

registry.npmjs.org/packaename/version

就可以查到某个发布在npm模块上的具体信息以及下载地址,下载并解压到本地完成安装。


那如果我们搭建了私有的npm,上述这个过程将如何实现呢?


目前主流的实现方案大致是这样的:

图3 private npm工作流程示意图

用户install后向私有npm发起请求,服务器会先查询所请求的这个模块是否是我们自己的私有模块或已经缓存过的公共模块,如果是则直接返回给用户;如果请求的是一个还没有被缓存的公共模块,那么则会向上游源请求模块并进行缓存后返回给用户。

上游的源可以是npm仓库,也可以是淘宝镜像。


如何搭建一个私有npm仓库?

业界主流的私有npm仓库搭建的主流方案有如下几种:

  1. 付费购买
  2. 使用 git+ssh 这种方式直接引用到 GitHub 项目地址
  3. 使用Sinopia
  4. 使用cnpmjs.org

第一种,一是考虑到公司可能不会提供经费,二npm在国内访问很慢,就是花钱也买不到好的体验。

第二种,不能更新即 npm update,不能使用 semver(语义化版本规范)。


那么较好的选择就只剩下第三种和第四种。


下面将分别使用基于Sinopia和基于cnpmjs.org这两种方案来搭建私有npm仓库并进行总结。

1. Sinopia方案篇

1.1 服务端部署

安装

前置工作:配置nodejs及npm环境

npm install -g sinopia

启动

sinopia
warn  --- config file - /home/map/.config/sinopia/config.yaml
warn  --- http address - http://localhost:4873/


此时访问localhost:4873,可获取html文件并且服务端响应正常,表示安装成功。


$ curl localhost:4873
<!doctype html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<title>Sinopia</title>
<link rel="icon" type="image/png" href="http://localhost:4873/-/static/favicon.png"/>
...


服务端响应

$ sinopia
...
http  <-- 200, user: undefined, req: 'GET /', bytes: 0/10896


配置

运行sinopia,自动生成的工作目录如下(通过第一个warn可以看到具体路径):

$ tree /home/map/.config/sinopia/
/home/map/.config/sinopia/
|-- config.yaml //存放所有配置信息
|-- htpasswd        //存放所有账户信息
`-- storage         //存放私有npm包及缓存公有包
|-- npm_test
|   |-- npm_test-1.0.0.tgz
|   |-- npm_test-1.0.1.tgz
|   `-- package.json
`-- sinopia
       `-- package.json

3 directories, 6 files


config.yaml默认配置

# This is the default config file. It allows all users to do anything,
# so don't use it on production systems.
#
# Look here for more config file examples:
# https://github.com/rlidwka/sinopia/tree/master/conf

# path to a directory with all packages
storage: ./storage      //npm包存放的路径

auth:
 htpasswd:
file: ./htpasswd    //保存用户的账号密码等信息
# Maximum amount of users allowed to register, defaults to "+inf".
# You can set this to -1 to disable registration.
#max_users: 1000 //默认为1000,改为-1,禁止注册

# a list of other known repositories we can talk to
uplinks:
 npmjs:
url: https://registry.npmjs.org/    
//拉取公共包的地址源,默认为npm的官网,可以使用淘宝的npm镜像地址

packages: //配置权限管理
'@*/*':
# scoped packages
   access: $all
publish: $authenticated
'*':

# allow all users (including non-authenticated users) to read and
# publish all packages
#
# you can specify usernames/groupnames (depending on your auth plugin)
# and three keywords: "$all", "$anonymous", "$authenticated"
   access: $all

# allow all known users to publish packages
# (anyone can register by default, remember?)
publish: $authenticated

# if package is not available locally, proxy requests to 'npmjs' registry
proxy: npmjs

# log settings
logs:
- {type: stdout, format: pretty, level: http}
#- {type: file, path: sinopia.log, level: info}


外网访问配置

通过在config.yaml中修改服务默认的监听端口,从而可以通过外网访问 sinopia 仓库。

listen: 0.0.0.0:4873

外网通过http://[IP | 域名]:[端口]的形式来访问。

浏览器外网访问如图:

图4 外网访问示例


账号配置

config.yaml 中auth部分对应账号的管理,默认可以通过客户端npm adduser添加账号。可以通过max_users:-1禁止客户端创建,而通过我们修改htpasswd文件来管理用户。

htpasswd文件示例:

lisi:{SHA}????????????????=:autocreated 2016-02-05T15:39:19.960Z
wangwu:{SHA}????????????????=:autocreated 2016-02-05T17:59:05.041Z


密码是被加密过的,是简单的SHA1哈稀之后再转换成 Base64 。

1.2 客户端配置

配置npm registry

建议客户端使用nrm 进行npm registry地址管理和切换

安装

npm install -g nrm

添加sinopia仓库地址

nrm add sinopia http://192.168.xx.xx:4873

切换私有仓库

nrm use sinopia

查看所有仓库地址(星标为当前仓库源)

nrm ls
npm ---- https://registry.npmjs.org/
cnpm --- http://r.cnpmjs.org/
taobao - https://registry.npm.taobao.org/
nj ----- https://registry.nodejitsu.com/
rednpm - http://registry.mirror.cqupt.edu.cn/
npmMirror https://skimdb.npmjs.com/registry/
edunpm - http://registry.enpmjs.org/
* sinopia http://192.168.xx.xx:4873/


1.3 发包

切换到私有仓库之后,发包的操作跟npm发包基本无差别。
登录账号之后:

npm publish
+ npm_test@1.0.1
ps: 版本号重复的情况再次发布的包不会主动更新,并且发布不会有错误提示,更新包务必更新版本号。

发布成功后私有仓库站点会显示包情况,README.md文件会作为详情描述展开。

图5 包发布成功示例图

2. cnpmjs.org方案篇


2.1 服务端部署


官方依赖如下图:

图6 官方依赖配置

我这边的配置:

  • 服务器Linux version 3.10.0_1-0-0-8
  • node v8.9.0
  • npm v5.5.1
  • mysql 5.1.73


安装 cnpmjs.org


npm i -g cnpmjs.org

我安装的是3.0.0-beta.1。


修改cnpmjs.org 配置文件


cnpmjs.org 默认安装路径:/usr/local/lib/node_modules/cnpmjs.org

打开配置文件:cnpmjs.org/config/inde…

部分配置项说明

/*
 * server configure //服务器配置
 */
 
registryPort: 7001,         //仓库访问端口(执行发布安装)
webPort: 7002,              //展示查询站点访问端口
bindingHost: '',   //监听绑定的 Host,默认127.0.0.1,外网访问注释掉此项即可


/**
* database config //数据库相关设置
*/

database: {
    db: 'cnpmjs',      //数据库名称
    username: 'root',       //数据库访问账号
    password: '123456',           //数据库访问密码
    
    // the sql dialect of the database
    // - currently supported: 'mysql', 'sqlite', 'postgres', 'mariadb'
    dialect: 'mysql',       //使用数据库,默认sqlite,这里我们改成mysql
    
    // custom host; default: 127.0.0.1
    host: '127.0.0.1',      //数据库访问IP,通常127.0.0.1
    
    // custom port; default: 3306
    port: 3306,             //数据库访问端口,通常3306
    
    
// 模块文件存储,默认将发布的私有模块跟缓存公共模块存储在本地文件系统中,路径~/.cnpmjs.org/nfs ,也就是模块文件都存储在这个目录下;或者可以选择三方储存方式比如七牛等,着这里配置插件;也支持接口开发扩展储存;

nfs: require('fs-cnpm')({
    dir: path.join(dataDir, 'nfs')
}),
    
// registry url name //模块注册列表访问域名,默认r.cnpmjs.org,安装模块时会到这个域名下查找,这个默认设置略坑,建议没有外网域名的先清空回头再配
registryHost: '',


// default system admins    //默认管理员账号
  admins: {
    // name: email
    //fengmk2: 'fengmk2@gmail.com',
    admin: 'admin@cnpmjs.org',
    //dead_horse: 'dead_horse@qq.com',
  },
  
 
/*
 * registry mode config  私有模块发布相关配置
*/

  //是否开启私有模式,默认为 false;
  //私有模式下只有管理员能发布模块,其他账号只有同步权限
  //非私有模式,注册用户都可以发布模块
  enablePrivate: false, 

  // registry scopes
  //若为非私有模式发布则此项必填,非管理员发布模块式命名必须以scopes字段开头,模块命名示例“@cnpm/packagename”
  //更多了解npm-scope请查阅https://docs.npmjs.com/misc/scope
  scopes: [ '@cnpm', '@cnpmtest', '@cnpm-test' ],

  // 私有模块非scopes白名单,各种非以scope方式发布的老模块的白名单管理,数组形式维护
  privatePackages: [],


/**
* sync configs 同步源仓库相关设置
*/

//npm官方registry地址,不会直接从这个地址同步模块,但有时会从这里获取模块信息,除非必要请勿更改
officialNpmRegistry: 'https://registry.npmjs.com',
officialNpmReplicate: 'https://replicate.npmjs.com',

//同步模块上游registry地址
sourceNpmRegistry: 'https://registry.npm.taobao.org',

//上游registry是否是cnpm,默认true,若要使用npm官方地址作为同步上游,请设置为false
sourceNpmRegistryIsCNpm: true,

//若安装时模块不存在,是否向源registry进行同步,默认true
syncByInstall: true,

// 同步模式选项
// none: 不进行同步,只管理用户上传的私有模块,公共模块直接从上游获取
// exist: 只同步已经存在于数据库的模块
// all: 定时同步所有源registry的模块
syncModel: 'exist', // 'none', 'all', 'exist'

// 同步时间间隔,默认10分钟
syncInterval: '10m',


// 是否同步模块中devDependencies,默认false
syncDevDependencies: false,

//用户账号系统接入,可以扩展接入公司的账号系统
//本文暂不涉及,详见https://github.com/cnpm/cnpmjs.org/wiki/Use-Your-Own-User-Authorization
userService: null,

//另外一个比较坑的默认设置,默认false,踩坑记录里详细说
enableAbbreviatedMetadata: true,


新建数据库并导表


用户名、密码进入数据库

mysql -uroot -p123456
ps:-u用户名 -p密码,初次使用的mysql还没创建系统账号时默认为空, mysql命令后直接回车后即可进入命令行。创建mysql账号,注意名称跟cnpm配置中相对应。


创建cnpm所需的数据库

create database cnpmjs;

切换到cnpm数据库

use cnpmjs;

导入cnpm数据库配置文件

文件位于cpm安装目录docs/db.sql下

source docs/db.sql;

查看导入的表

我这个版本共导入16个表

show tables;
tip:数据库配置这块如果觉得命令行太麻烦,可以使用可视化数据库管理工具,例如phpAdmin等进行管理。


跑起来


cnpmjs.org start

访问对应的IP加访问端口即可看到,展示站点如下图

图7 start之后示意图

我们可以搜索我们想要的包,如果没有可以选择sync进行同步。

图8 选择同步

选择同步之后,后有如下同步过程

图9 同步过程

2.2 客户端配置


客户端我们需要安装cnpm工具进行模块管理

npm i -g cnpm

把cnpm的registry指向我们的私有npm服务ip,端口使用registry端口;

cnpm config set registry 192.xxx.x.x:7001


2.3 发布私有模块


私有仓库用户登录,填写用户名密码邮箱

cnpm login

命名行发布

cnpm publish

2.4 爬坑总结


cnpm的方案配置上要比sinopia复杂,所以踩的坑也多一点,在此记录一下。


(1) 数据库前期准备


在准备数据库的阶段mysql-V检查到系统已经预装了mysql,但运行mysql登录命令的时候却报错如下:

$ mysql
ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/lib/mysql/mysql.sock' (2)

于是我启动了服务,然鹅依旧报错

$ service mysqld start
mysqld: unrecognized service

查解决方法改配置文件依然没有效果,最终发现mysql-server没有安装😥,这个报错完全看不出来是这个问题😂。。。

yum install mysql-server

安装之后重新运行server就好了。


(2) 展示站点数据库访问失败


创建数据库,导表,改配置之后运行起来,访问站点页面也没啥问题,but为啥没有数据??打开请求一看,获取数据的接口500了。

查了log发现报少表,打开数据库确认确实少表,果然删了重新导。过程中有三个表报‘Unknown character set: 'utf8mb4’导入失败,第一次没注意。。。

查后发现mysql在mysql5.6+的版本中才支持utf8mb4的编码格式,而我们的版本只支持utf8。

那就只有两个选择,第一改编码格式,第二升级数据库。

改过三张表的编码后,顺利导入,站点获取数据正常。


(3) 模块安装一直失败


命令行报错

图10 命令行报错

其实这个请求地址一看就很可疑,这个地址正式config中registryHost的默认值,所以要么给这个域名绑IP访问,要么就把这设置项先空着,等有了外网域名的时候再改。


(4) 私有模块发布失败


报错反应模块命名不对,发布私有模块需要遵守scope的命名规范,带上相应的前缀名,具体情况参见npm-scope


(5) 同步模块报错


sync error: TypeError: Cannot read property 'findAll' of null
... ...

这个问题最终是查issues解决的,貌似在1149加入了功能之后出现的,需要把enableAbbreviatedMetadata:设置为true解决issues#1236,#1289。


3. 番外篇——进程管理

由于使用命令行起的服务需要通过使用进程管理来守护。

为了提升私有库服务稳定性,我们选择pm2守护进程。

安装pm2:

npm install -g pm2

使用pm2 启动 sinopia:

pm2 start sinopia

常用pm2操作

pm2 list //查看所有进程
pm2 logs                        //查看日志
pm2 start app_name|app_id       //启动某个进程
pm2 stop app_name|app_id        //停止某个进程
pm2 restart app_name|app_id     //重启某个进程
pm2 delete app_name|app_id      //删除某个进程

总结

通过两种主流方案——基于Sinopia和基于cnpm搭建npm私有仓库——的尝试,我们发现:


  • Sinopia比较偏向于一个零配置、轻量型的私有npm模块管理工具,不需要额外的数据库配置,它内部自带小型数据库,支持私有模块管理的同时也支持缓存使用过的公共模块,发布及缓存的模块以静态资源形式本地存储。支持静态配置型用户管理机制,以及分层模块权限设置。
  • cnpm可以实现公共模块镜像更新以及私有模块管理,支持拓展多种存储形式,相对的数据库的配置较多,部署过程略复杂。是淘宝及多家大型公司搭建内部私有npm仓库选择的方案。
  • cnpm的方案在部署过程以及整体设计上要比Sinopia复杂的多,维护成本也比较高;但相对的也提供更高的扩展性,可以支持多种业务场景;

俗话说:脱离业务场景谈解决方案,都是耍流氓。

如果你是一个想要拥有私人npm仓库的个人开发者或者小团队,Sinopia完全可以满足需求;如果想要对私人npm服务做更多个性化定制方案,那么cnpm的扩展性支持更加友好。