npm 私有仓库

淘宝 NPM 镜像站切换新域名时,放了一张知乎博主·天猪·的图片,如下:

图片来源:zhuanlan.zhihu.com/p/432578145

看着逐年增长的访问量,不禁让人感慨,npm 的出现,对 JavaScript 生态产生了深远影响。npm 包下载量的变迁,也是 JavaScript 生态体系发展的一个缩影,同时也是国内公司前端领域发展的一个缩影。

npm 的出现,让我们可以轻松使用第三方模块,也让我们可以轻松发布自己的模块。

随着企业的发展,公司内部对私有的 npm 源的需求越来越多,下面就来介绍一下,企业级 npm 私有仓库部署方案。

一、 为什么需要 npm 私有仓库

我们从基本要求和扩展需求两个方面来分析。

  1. 基本要求
    • 安装速度快
    • 内网访问
    • 对接公司内部登录权限系统
    • 支持私有包
    • 避免公网 npm 服务不稳定
  2. 扩展需求
    • 安全审计,追踪依赖,及时发现安全问题
    • 使用 AI 相关技术,自动产出项目老旧依赖升级建议
    • 支持内部 npm cli 工具扩展更多功能
    • 前端工程数字化建设,定制化数据统计分析功能

基本要求,是搭建企业级 npm 私有仓库的核心诉求;扩展需求,则是企业数字安全和团队工程化的诉求。当然,不同的团队有不同的需求,本质上就是为了安全和稳定,能依托 npm 服务做定制化服务。

二、开源项目方案

既然要搭建企业级 npm 私有仓库,那么首先就要考虑开源项目。

有 nexus-public、verdaccio(sinopia已停止更新)、cnpmcore(cnpmjs已废弃)等,其中 nexus-public 是 java 项目,其他都是 nodejs 项目。另外有收费的有 JFrog Artifactory,是通用通用存储库管理器,支持许多流行仓库包括 npm。

基本上,能做到私有化部署npm服务 + 对接公司内部权限系统,就能满足基本要求。

  • nexus-public 是 java 项目,能满足基本要求,如果公司有同步管理 maven、docker等需求,可以考虑。
  • verdaccio 界面简洁,功能齐全,社区活跃,拥有 15k 的 star数,是一个不错的选择。verdaccio 非常轻量,安装和配置都非常简单,可以通过插件,定制化包括身份验证、存储和通知等功能。
  • cnpmcore 是淘宝 NPM 镜像站服务 npmmirror.com 背后的核心,是基于 eggjs 开发的。

三、选择 cnpmcore

企业服务,要求数据和服务分离,既要保证数据的安全和可靠性,又要保证服务的持续升级迭代。

既然是内网私有化部署,不用面对公网的流量,更多的关注点在于,除了满足内网 npm 基本服务要求,更多的是企业定制化需求。

综合考虑各方需求后,我认为 cnpmcore 对于企业级 npm 私有仓库部署方案来说,是一个不错的解决方案。

图片来源: juejin.cn/post/713272...

相比于前身 cnpmjs,cnpmcore 进行了重大的重构,cnpmcore 对我来说,看中的就是它的二次研发能力。

cnpmcore 部署比较简单,文档介绍的已经很详细了,主要步骤简单描述:

  1. 依赖服务准备,MySQL 数据服务、Redis 缓存服务,包存储默认是本地文件系统,推荐使用对象存储服务
  2. 源码下载,安装依赖,修改配置文件,启动服务

比起直接源码部署,我更推荐创建 tegg 项目,集成 cnpmcore 使用, cnpmcore 官方有提供详细的 tegg 集成示例。当前有个前提,你对于 eggjstegg 有一定的了解。

了解 cnpmcore 项目架构

首先放一下 cnpmcore 文档里的架构分层依赖图

简单总结一下文档内容:

  • 项目结构:项目按照功能进行了分层,包括 common(通用工具和服务调用)、core(核心业务逻辑)、repository(数据存储和查询)、port(HTTP 控制器)、infra(基于 PaaS 的基础设置实现)等。
  • Controller:主要处理 HTTP 请求,继承自 AbstractController 和 MiddlewareController。AbstractController 封装了一些基础的数据 Entity 访问方法,MiddlewareController 主要负责编排中间件的加载顺序。
  • 请求合法性校验:请求合法性校验包括请求参数校验、用户认证和资源操作权限校验。请求参数校验使用 egg-typebox-validate,用户认证和资源操作权限校验通过 UserRoleManager 进行。
  • Service:依赖 Repository,然后被 Controller 依赖。PackageManagerService 管理所有包以及版本信息。
  • Repository:依赖 Model,然后被 Service 和 Controller 依赖。Repository 类方法命名规则包括 findSomething(查询一个模型数据)、saveSomething(保存一个模型数据)、removeSomething(移除一个模型数据)和 listSomethings(查询一批模型数据)。

由于 tegg 的存在,后续在定制化开发中,我们可以按需导出所需要的模块,然后进行二次开发。

四、定制化开发

如何在 tegg 中集成 cnpmcore 是官方提供的集成示例,完整代码可以参考 这里。

准备工作

依赖的外部服务这里不多做赘述,既然是企业级,可以自行对接公司内部的 MySQL、Redis 服务。另外,准备对象存储服务 s3, 用于存储包文件。

注意 : cnpmcore 是渐进式开发,mysql 数据结构会随着版本发生变动,升级 cnpmcore 时,一定要注意这里 github.com/cnpm/cnpmco..., 提前做好数据库表结构变更准备。

项目初始化

shell 复制代码
$ mkdir tegg-cnpm
$ cd tegg-cnpm
$ npm init egg --type=ts

初始化后的项目,是一个 tegg 项目,集成了 eggjs 和 tegg,这里先更新一下依赖。

然后,我们需要安装 cnpmcore 依赖,以及需要的一些依赖,如 @eggjs/tegg-orm-plugin、egg-typebox-validate、s3-cnpmcore 等。

配置文件

  1. 修改 config/config.default.ts 配置文件,添加 cnpmcore 配置。
arduino 复制代码
  config.cnpmcore = {
    name: 'mynpm',
    sourceRegistry: 'https://registry.npmmirror.com',
    ...
    // sync mode
    //  - none: don't sync npm package, just redirect it to sourceRegistry
    //  - all: sync all npm packages
    //  - exist: only sync exist packages, effected when `enableCheckRecentlyUpdated` or `enableChangesStream` is enabled
    syncMode: SyncMode.admin,
    syncDeleteMode: SyncDeleteMode.delete,
    ...
    registry: process.env.CNPMCORE_CONFIG_REGISTRY || 'http://localhost:7001', // 填写自己的域名
    // white scope list
    allowScopes: [
      '@myscope', // 这里添加自己的 scope
    ],
    ...
    // 默认的系统管理员
    admins: { 
      // name: email
      xxx: 'xxx@my.cn',
    },
    ...
  };
  1. 添加 nfs 配置,用于存储包文件
php 复制代码
import S3Client from 's3-cnpmcore';
...
// 默认是本地文件系统
config.nfs = {
  client: null,
  dir: join(config.dataDir, 'nfs'),
};

// 这里使用 s3 对象存储, s3-cnpmcore 是 cnpmcore 官方提供的 s3 对象存储实现, 当然可以不用这个,自己实现也可以
config.nfs.client = new S3Client({
    region: 'xx',
    endpoint: 'http://xxx.com',
    credentials: {
      accessKeyId: '',
      secretAccessKey: '',
    },
    bucket: 'cnpmcore',
    forcePathStyle: false,
    disableURL: false,
  });
  1. 修改 config/plugin.ts 配置文件,开启我们需要的插件,如下:
bash 复制代码
  teggOrm: {
    enable: true,
    package: '@eggjs/tegg-orm-plugin',
  },
  typeboxValidate: {
    enable: true,
    package: 'egg-typebox-validate',
  },
  1. 添加 module.json
css 复制代码
[  {    "path": "../app/service"  },  {    "path": "../app/infra"  },  {    "package": "cnpmcore/common"  },  {    "package": "cnpmcore/core"  },  {    "package": "cnpmcore/port"  },  {    "package": "cnpmcore/repository"  }]

其中 package 可以导出 cnpmcore 的模块,也可以用 path 导出自己的本地模块,具体参考 tegg 文档。

  1. 写业务代码

文件结构

如图所示,在 infra 中,我们可以实现自己的 adapter,如 nfs 为例,这里实现了自己的 nfs adapter,用于存储包文件。如果在默认配置里,没有配置 nfs.client,那么就会使用默认的本地文件系统存储包文件。

以 AuthAdapter 为例,这里实现了自己的 AuthAdapter,用于对接公司内部的登录权限系统。

typescript 复制代码
import { AccessLevel, EggContext, SingletonProto } from '@eggjs/tegg';
import { AuthAdapter } from 'cnpmcore/infra/AuthAdapter';
import { randomUUID } from 'crypto';
import { AuthUrlResult, userResult } from 'node_modules/cnpmcore/dist/app/common/typing';

const ONE_DAY = 3600 * 24;

@SingletonProto({
  name: 'authAdapter',
  accessLevel: AccessLevel.PUBLIC,
})
export class MyAuthAdapter extends AuthAdapter {
  async getAuthUrl(ctx: EggContext): Promise<AuthUrlResult> {
    const sessionId = randomUUID();
    await this.redis.setex(sessionId, ONE_DAY, '');

    const registry = ctx.app.config.cnpmcore.registry;
    const ssoLoginUrl = ctx.app.config.ssoLoginUrl;
    const ref = encodeURIComponent(`${registry}/cli/login/sso/${sessionId}`);
    return {
      loginUrl: `${ssoLoginUrl}?ref=${ref}`,
      doneUrl: `${registry}/-/v1/login/done/session/${sessionId}`,
    };
  }

  async ensureCurrentUser(): Promise<userResult | null> {
    if (this.user) {
      return this.user;
    }
    return null;
  }
}

添加登录验证Controller

kotlin 复制代码
import {
  HTTPController,
  HTTPMethod,
  HTTPMethodEnum,
  Context,
  EggContext,
  HTTPParam,
  HTTPQuery,
  HTTPBody,
  Middleware,
  Inject,
} from '@eggjs/tegg';
import { EggLogger, EggAppConfig } from 'egg';
import { traceMethod } from '../middleware/trace_method';
import { LoginService } from '../service/LoginService';
import { CacheAdapter } from 'cnpmcore/common/adapter/CacheAdapter';
import { UserService } from 'cnpmcore/core/service/UserService';

@HTTPController()
@Middleware(traceMethod)
export class LoginController {
  @Inject()
  private readonly loginService: LoginService;
  @Inject()
  protected userService: UserService;
  @Inject()
  private cacheAdapter: CacheAdapter;
  @Inject()
  protected config: EggAppConfig;
  @Inject()
  private readonly logger: EggLogger;

  @HTTPMethod({
    method: HTTPMethodEnum.GET,
    path: '/cli/login/sso/:sessionId',
  })
  async cliLogin(@Context() ctx: EggContext, @HTTPParam() sessionId: string, @HTTPQuery() sid: string) {
    
    if (!sessionId) {
      return { success: false, data: { message: 'need sessionId' } };
    }
  
    // 验证 sessionId 是否有效
    const sessionData = await this.cacheAdapter.get(sessionId);
    if (sessionData !== '') {
      return { success: false, data: { message: 'invalid sessionId' } };
    }

    // 通过自己实现的 loginService 获取用户信息
    const user = await this.loginService.getUser(ctx, sid);
    if (!user?.name || !user?.email) {
      return { success: false, data: { message: 'invalid user info' } };
    }

    // 通过 cnpmcore UserService 保存 token
    const { token } = await this.userService.ensureTokenByUser({ name: user.name, email: user.email, ip: ctx.ip });
    await this.cacheAdapter.set(sessionId, token!.token!);

    // return { success: true, data: { message: 'login success' } };
    // 跳转到登录成功页
    ctx.redirect(`${this.config.cnpmcore.registry}/-/v1/login/request/success`);
    return;
  }
}

前端部分

上面讲了服务端定制化开发,对于前端部分,cnpm 提供了 github.com/cnpm/cnpmwe...

五、总结

cnpmcore 对于企业级 npm 私有仓库部署方案来说,是一个不错的选择。通过在 tegg 中集成 cnpmcore,我们可以很方便的进行 npm 服务的二次开发,扩展自己的业务代码,满足企业定制化需求。

相关推荐
熊的猫34 分钟前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
瑶琴AI前端1 小时前
uniapp组件实现省市区三级联动选择
java·前端·uni-app
会发光的猪。1 小时前
如何在vscode中安装git详细新手教程
前端·ide·git·vscode
我要洋人死2 小时前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人2 小时前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人2 小时前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR3 小时前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香3 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q2498596933 小时前
前端预览word、excel、ppt
前端·word·excel
小华同学ai3 小时前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书