十分钟快速搭建自己的npm私有仓库

5,206 阅读6分钟

如何搭建 NPM 仓库

方案主要有两种:cnpm、verdaccio
下面演示采用 cnpm 方案的具体过程

1. 准备工作

  • Linux 服务器 及 Node 环境
  • Mysql 数据库

2. 安装 cnpmjs.org

  • 在服务器上通过 git 安装 cnpmjs.org
git clone https://github.com/cnpm/cnpmjs.org.git
  • 安装项目依赖
cd ./cnpmjs.org
npm i // cnpm i

3. 初始化数据库

cnpmjs.org 项目 docs 目录下已经给我们备好了创建数据库的脚本 db.sql,复制后在 mysql 中执行即可

cat ./docs/db.sql

4. 初始化配置

vim ./config/index.js

cnpm 的配置项非常多, 一些比较重要的放在了下面

{
    enableCluster:是否启用 cluster-worker 模式启动服务,默认 false,生产环节推荐为 true;
    registryPort:API 专用的 registry 服务端口,默认 7001;
    webPort:Web 服务端口,默认 7002;
    bindingHost:监听绑定的 Host ,默认为 127.0.0.1,如果采用 Nginx 反向代理的话推荐不用改,否则改为0.0.0.0;
    sessionSecret:session 用的盐;
    logdir:日志目录;
    uploadDir:临时上传文件目录;
    viewCache:视图模板缓存是否开启,默认为 false;
    enableCompress:是否开启 gzip 压缩,默认为 false;
    admins:管理员,这是一个 JSON Object,对应各键名为各管理员的用户名,键值为其邮箱,默认为 { fengmk2: 'fengmk2@gmail.com', admin: 'admin@cnpmjs.org', dead_horse: 'dead_horse@qq.com' };
    logoURL:Logo 地址;
    adBanner:广告 Banner 的地址;
    customReadmeFile:实际上我们看到的 cnpmjs.org 首页中间一大堆冗长的介绍是一个 Markdown 文件转化而成的,你可以设置该项来自行替换这个文件;
    customFooter:自定义页脚模板;
    npmClientName:默认为 cnpm,如果你有自己开发或者 fork 的 npm 客户端的话请改成自己的 CLI 命令,这个应该会在一些页面的说明处替换成你所写的;
    backupFilePrefix:备份目录;
    database:{
        db:数据的库名;
        username:数据库用户名;
        password:数据库密码;
        dialect:数据库适配器,可选 "mysql""sqlite""postgres""mariadb",默认为 "sqlite";
        hsot:数据库地址;
        port:数据库端口;
        pool:{
            //数据库连接池相关配置,为一个对象;
            maxConnections:最大连接数,默认为 10;
            minConnections:最小连接数,默认为 0;
            maxIdleTime:单条链接最大空闲时间,默认为 30000 毫秒;
        }
    },
    nfs:包文件系统处理对象,为一个 Node.js 对象,默认是 fs-cnpm 这个包,并且配置在 ~/.cnpmjs/nfs 目录下,也就是说默认所有同步的包都会被放在这个目录下;开发者可以使用别的一些文件系统插件(如上传到又拍云等),又或者自己去按接口开发一个逻辑层,这些都是后话了;
    registryHost:需设置为自己服务器地址,npm install时优先从私有仓库中拉取,默认为 r.cnpmjs.org;
    enablePrivate:是否开启私有模式,默认为 false//如果是私有模式则只有管理员能发布包,其它人只能从源站同步包;
    //如果是非私有模式则所有登录用户都能发布包;
    scopes:非管理员发布包的时候只能用以 scopes 里面列举的命名空间为前缀来发布,如果没设置则无法发布,也就是说这是一个必填项,默认为 [ '@cnpm', '@cnpmtest', '@cnpm-test' ],据苏千大大解释是为了便于管理以及让公司的员工自觉按需发布;更多关于 NPM scope 的说明请参见 npm-scope;
    privatePackages:就如该配置项的注释所述,出于历史包袱的原因,有些已经存在的私有包(可能之前是用 Git 的方式安装的)并没有以命名空间的形式来命名,而这种包本来是无法上传到 CNPM 的,这个配置项数组就是用来加这些例外白名单的,默认为一个空数组;
    sourceNpmRegistry:更新源 NPM 的 registry 地址,默认为 https://registry.npm.taobao.org;
    sourceNpmRegistryIsCNpm:源 registry 是否为 CNPM ,默认为 true,如果你使用的源是官方 NPM 源,请将其设为 false;
    syncByInstall:如果安装包的时候发现包不存在,则尝试从更新源同步,默认为 true;
    syncModel:更新模式(不过我觉得是个 typo),有下面几种模式可以选择,默认为 "none";
    // "none":永不同步,只管理私有用户上传的包,其它源包会直接从源站获取;
    // "exist":定时同步已经存在于数据库的包;
    // "all":定时同步所有源站的包;
    syncInterval:同步间隔,默认为 "10m" 即十分钟;
    syncDevDependencies:是否同步每个包里面的 devDependencies 包们,默认为 false;
    badgeSubject:包的 badge 显示的名字,默认为 cnpm;
    userService:用户验证接口,默认为 null,即无用户相关功能也就是无法有用户去上传包,该部分需要自己实现接口功能并配置,如与公司的 Gitlab 相对接,这也是后话了;
    alwaysAuth:是否始终需要用户验证,即便是 $ cnpm install 等命令;
    httpProxy:代理地址设置,用于你在墙内源站在墙外的情况。
}

对于这些配置,我们着重更改以下几个

  • 服务访问端口
registryPort: 7001, //仓库服务访问端口
webPort: 7002,     //web站点访问端口
bindingHost: '',  //监听绑定的 Host,默认127.0.0.1,外网访问注释掉此项即可,一般我们不会把我们内部端口暴露出去,可以在nginx层做一个转发,所以这个配置可以注释掉。如果直接外网访问,配置为 0.0.0.0
registryHost: xxx // 需设置为自己服务器地址,npm install时优先从私有仓库中拉取,否则将拉取不到私有包
  • 数据库配置
database: {
  db: 'npm', // 数据库名称
  username: 'admin',//用户
  password: 'admin123', //密码
  dialect: 'mysql',//默认是sqlite,我们选择的mysql
  host: '127.0.0.1', //数据库服务地址
  port: 3306,   // 端口
  ...//其他的暂时不用关注
},
  • 是否启用私有模式
//私有模式下,只有管理员才能发布模块。非管理员发布模块式命名必须以 scopes 字段开头例如:@lxy/package
scopes: ['@lxy'], // 发布前缀
enablePrivate: true, //开启私有模式
  • 管理员账号配置
admins: {
    // username: email
    liuxueyong: 'liuxueyong@agora.io'
}
  • 同步模式
// 同步模式选项
// none: 不进行同步,只管理用户上传的私有模块,公共模块直接从上游获取
// exist: 只同步已经存在于数据库的模块
// all: 定时同步所有源 registry 的模块
syncModel: 'exist'

5. 启动项目

// 安装 pm2
npm i pm2 -g
// 用 pm2 启动项目
pm2 start ./dispatch.js  //dispatch.js在cnpmjs.org项目的根目录下

//启动后还需要在服务器中将上述设置的两个端口暴露出去才能在外网访问

6. 使用

1 初始化

  • 安装 nrm 来对 npm 源进行管理 ( yarn 为 yrm )
npm i nrm -g
// npm i yrm -g
  • 新增自己的私有源到 nrm 源列表
// nrm add [name] [host]
nrm add lxy http://106.15.170.172:9010/

// yrm add [name] [host]
// yrm add lxy http://106.15.170.172:9010/
  • 切换到私有源
nrm use lxy
// yrm use lxy
  • 注册私有源用户
// username 和 email 对应之前设置过的 admin 对象里的 key、value, 在 admin 对象中的即为管理员
npm adduser

image.png

  • 登录私有仓库
npm login

2 日常使用

  • 发布私有模块
npm publish
  • 安装私有模块
npm i xxx
  • 包管理前端页面
// 服务器地址 + 设置的 web 站点访问端口
http://106.15.170.172:9011/

7. 一些奇怪的坑

1. 配置项 syncModel

问题描述:上传私有库后, 如 syncModel: 'none', 当尝试 install 时会报 404;syncModel: 'exist'可以正确install,但是会同步大量的包到本地
问题根源:执行npm install xxx时,完成鉴权后,进入 proxyToNpm 中间件,当syncModel: 'none' 且前缀不匹配 scopedUrls 的规则时将被视为非私有包而导向 npm,但 npm 无此包于是报 404
解决方案:私有包名称应与 scopedUrls 相匹配

// ./servers/registry.js
...
app.use(auth());
app.use(proxyToNpm());
...
// ./middleware/proxy_to_npm.js
  var redirectUrl = config.sourceNpmRegistry;
  var proxyUrls = [
    // /:pkg, dont contains scoped package
    // /:pkg/:versionOrTag
    /^\/[\w\-\.]+(?:\/[\w\-\.]+)?$/,
    // /-/package/:pkg/dist-tags
    /^\/\-\/package\/[\w\-\.]+\/dist-tags/,
  ];
  var scopedUrls = [
    // scoped package
    /^\/(@[\w\-\.]+)\/[\w\-\.]+(?:\/[\w\-\.]+)?$/,
    /^\/\-\/package\/(@[\w\-\.]+)\/[\w\-\.]+\/dist\-tags/,
  ];
  
  return function* proxyToNpm(next) {
    if (config.syncModel !== 'none') {
      return yield next;
    }

    // syncModel === none
    // only proxy read requests
    if (this.method !== 'GET' && this.method !== 'HEAD') {
      return yield next;
    }

    var pathname =  decodeURIComponent(this.path);

    var isScoped = false;
    var isPublichScoped = false;
    // check scoped name
    if (config.scopes && config.scopes.length > 0) {
      for (var i = 0; i < scopedUrls.length; i++) {
        const m = scopedUrls[i].exec(pathname);
        if (m) {
          isScoped = true;
          if (config.scopes.indexOf(m[1]) !== -1) {
            // internal scoped
            isPublichScoped = false;
          } else {
            isPublichScoped = true;
          }
          break;
        }
      }
    }

    var isPublich = false;
    if (!isScoped) {
      for (var i = 0; i < proxyUrls.length; i++) {
        isPublich = proxyUrls[i].test(pathname);
        if (isPublich) {
          break;
        }
      }
    }

    if (isPublich || isPublichScoped) {
      var url = redirectUrl + this.url;
      debug('proxy isPublich: %s, isPublichScoped: %s, package to %s',
        isPublich, isPublichScoped, url);
      this.redirect(url);
      return;
    }

    yield next;
  };

8. 其他事项

1. 实现权限管理系统

现状:只有一个管理员账户,需要发布不带前缀的私有包切换到该账户进行发布(或者多个管理员?),加前缀私有包所有人都可以发布(可通过权限系统配置账号只能发布到的前缀列表);install 权限裸奔,只要将源切换到服务器的私源就可以 install
理想状态:对接 OA 系统,只有账号密码都正确且状态正常的用户才可以 install 私有包
参考资料: github.com/cnpm/cnpmjs…

2. 配置 nginx

作为一个企业级的应用,IP + 端口的方式就如同裸奔一样,采用 nginx 做反向代理到某个域名

3. 私有包存储上云

私有仓库在同步和上传的时候,会交给 NFS 对象相应的函数去处理,NFS 对象返回处理结束之后再返回下载链接,所以通过自定义 NFS 模块可以实现 npm 包的各种定制存储。目前官方默认使用 fs-cnpm,该模块会将上传或者同步的包保存在服务器本地的 root/.cnpmjs.org/doenloads/ 目录下, 存储资源会是一个瓶颈。这个时候将私有包或者同步的资源放到云上就是一个非常好的方案。官方给出了下面几种 NFS 模块:

upyun-cnpm:又拍云存储插件
fs-cnpm:本地存储的插件
sfs-client: SFS(Simple FIle Store)存储插件
qn-cnpm:七牛云存储插件
oss-cnpm:阿里云 OSS 存储插件
  • 首先在 cnpmjs.org 项目目录下安装oss-cnpm模块
npm i oss-cnpm
  • 然后在云服务控制台 oss 管理中新增了一个 bucket 来存储 npm 包,然后修改项目配置文件,将默认的 fs-cnpm 模块替换成 oss-cnpm
var oss = require("oss-cnpm");
var nfs = oss.create({
  accessKeyId: 'xxxx',
  accessKeySecret: 'xxx',
  endpoint: 'oss-cn-beijing.aliyuncs.com',
  bucket: 'catfly-xxx',
  mode: 'private',
})
var config = {
  ...,
  nfs:nfs,
  ...
}
  • 最后重启项目,这个时候再发布或者同步资源的时候,服务器本地目录不会有新发布或同步的包了,在 oss 对应的 bucket 里面能找到刚刚发布或者同步的资源。