Loading...
墨滴

hauk0101

2021/04/14  阅读:122  主题:嫩青

轻量级 npm 私有仓库 Verdaccio 框架应用指南

轻量级 npm 私有仓库 Verdaccio 框架应用指南

本文不涉及 Verdaccio 的原理解析,也不会把官方文档中的教程全部誊抄一遍,如果有对应需求的读者,建议忽略本文,直接去 Verdaccio 的 Github 查看源码,或是直接去 Verdaccio 文档官网翻阅一手资料。

前言

因为团队在使用现在的 npm 私有库的过程中遇到了一些问题,所以想试试通过 Verdaccio 替换原有的 cnpmjs 来解决问题,但在我调研之后,发现可能 Verdaccio 并不是我要的答案。具体的问题我会在后面的内容中再描述。

本文主要分享一些我个人在尝试本地部署 Verdaccio 过程中的一些想法,如果能帮助到同样有困惑的同行们,那自然是再好不过了。

目录

  • Verdaccio 简介
  • Verdaccio 使用
  • Verdaccio 进阶
  • 杂谈
  • 参考资料
Verdaccio 简介
  • 什么是 Verdaccio

Verdaccio 是一个 Node.js创建的轻量的私有npm proxy registry 。

上面是官网网站的描述,我理解起来就是在你本地或者是团队内部网络中搭建一套内部的 npm 仓库。npm 仓库可以做什么, Verdaccio 提供的私有仓库就能做什么。

Verdaccio 其实也是由另一个 npm 私有仓库框架 sinopia 的一个 fork 延续而来,由于 sinopia 太久没有更新了,所以 Verdaccio 作为一个新的开源项目继续延续了下来。现在也更推荐大家选择更为活跃的 Verdaccio。

  • Verdaccio 能做什么
    • 发布私有包,不被团队外部的人使用
    • 通过 proxy 下载私有仓库没有的包
    • 缓存私有仓库没有的包,方便下次安装更快
    • 设置私有仓库的访问权限
    • 使用 Docker 、AWS 等部署方式

其实对于大部分需要搭建私有仓库的团队来讲,只需要有一个能够发布私有包、安装私有包、下载私有包的功能基本就满足了实际需要。很多自己硬想出来的定制化需求,在实际应用中很少能用得到。所以更大的需求是简单、安全的搭建一个私有库就满足需要了,这也是 Verdaccio 大受欢迎的根本原因之一。

如果还有特殊需要,比如针对私有库没有的 npm 官网中存在的第三方开源包,可以通过修改配置文件的方式,来设置 proxy 实现。同时 Verdaccio 也提供了下载第三方开源包并缓存至私有仓库的配置项。

对于私有库的权限管理,Verdaccio 提供了配置文件的方式和插件接入的方式,方便不同用户的傻瓜式配置或深度定制的实现。

Verdaccio 使用

如何使用 Verdaccio ,主要涉及“如何安装”、“如何发包”、“如何下载包” 基本就够用了。

  • 安装 Verdaccio
npm install --global verdaccio
  • 启动 Verdaccio
    • 普通启动
    // 命令行中,输入 verdaccio 即可
    verdaccio
    • 建议通过 pm2 启动
    // 先安装 pm2
    npm install -g pm2
    // 通过 pm2 启动 verdaccio
    pm2 start verdaccio
    • 访问 verdaccio 的 web 页面
    // 默认访问地址
    http://localhost:4873/
  • 将本地私有包,发布到私有仓库
// 进入带有 package.json 的私有包项目目录
// 执行发版命令
npm publish --registry http://localhost:4873/
// 如果提示 login,则正常执行 npm login 即可
  • 下载私有仓库中的私有包
// 下载私有仓库的包 your-package
npm install your-package --registry  http://localhost:4873/

至此,基本一个私有仓库就算部署好了,也基本可以提供服务了。可以直接在团队中特定私有仓库服务器中进行上述命令的操作。

Verdaccio 进阶

所谓进阶,其实更多是一些我个人体会的经验分享,希望可以给想更深入研究 verdaccio 的同行们一些小小的思路

  • 配置文件

现在如果在全网搜索 Verdaccio ,其实基本都是上述“如何安装”+ Verdaccio 的 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/verdaccio/verdaccio/tree/master/conf
#

# path to a directory with all packages
storage: /Users/allin0943/.local/share/verdaccio/storage
# path to a directory with plugins to include
plugins: ./plugins

web:
  title: Verdaccio
  # comment out to disable gravatar support
  # gravatar: false
  # by default packages are ordercer ascendant (asc|desc)
  # sort_packages: asc
  # convert your UI to the dark side
  # darkMode: true

# translate your registry, api i18n not available yet
# i18n:
# list of the available translations https://github.com/verdaccio/ui/tree/master/i18n/translations
#   web: en-US

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

# a list of other known repositories we can talk to
uplinks:
  npmjs:
    url: https://registry.npmjs.org/

packages:
  '@*/*':
    # scoped packages
    access: $all
    publish: $authenticated
    unpublish: $authenticated
    proxy: npmjs

  '**':
    # 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/publish packages
    # (anyone can register by default, remember?)
    publish: $authenticated
    unpublish: $authenticated

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

# You can specify HTTP/1.1 server keep alive timeout in seconds for incoming connections.
# A value of 0 makes the http server behave similarly to Node.js versions prior to 8.0.0, which did not have a keep-alive timeout.
# WORKAROUND: Through given configuration you can workaround following issue https://github.com/verdaccio/verdaccio/issues/301. Set to 0 in case 60 is not enough.
server:
  keepAliveTimeout: 60

middlewares:
  audit:
    enabled: true

# log settings
logs:
  - { type: stdout, format: pretty, level: http }
  #- {type: file, path: verdaccio.log, level: info}
#experiments:
#  # support for npm token command
#  token: false
#  # support for the new v1 search endpoint, functional by incomplete read more on ticket 1732
#  search: false
#  # disable writing body size to logs, read more on ticket 1912
#  bytesin_off: false

# This affect the web and api (not developed yet)
#i18n:
#web: en-US

  • 插件 关于插件,我发现可以使用的大多数都跟 auth 权限有关,如果有兴趣折腾权限相关的,可以看一看官网文档提供的一些插件。只需要做两步:

    • 安装 auth 插件
      // 选择自己需要的 auth 插件,可以参考
      https://verdaccio.org/docs/zh-CN/plugin-auth
    • 配置 auth 插件
      // 在配置文件 config.yaml 中设置
      auth:
       you_select_auth_plugin 
       
  • 插件拓展 Verdaccio 本身支持 5 种插件的开发和使用:https://verdaccio.org/docs/zh-CN/dev-plugins

    • Authentication Plugin
    • Middleware Plugin
    • Storage Plugin
    • Theme
    • Filter plugins
  • 定制化

与其说定制化,不如说我想看看我们能按照我们的意愿做哪些改动。然后绕了一圈,发现基于 Verdaccio 给的引导,能改动的并不算多,如果要改基本是需要对 Verdaccio 源码进行改动,同时如果要改 UI,还需要修改 Verdaccio-UI 项目,并且再进行打包编译。

这样有一个弊端就是,深层次的定制化之后,不能够再很方便的享受到 Verdaccio 版本更新之后的新功能或是优化。我还是说说,我在这里的一些小发现吧。

整个 Verdaccio 项目的组成:

// 后端服务是基于 Express 框架开发
// 前端页面是基于 React 框架开发。
// 数据管理,没有引入额外的数据库对包文件进行管理,用到的是基于本地文件读写的 `@verdaccio/local-storage` 自研库进行数据管理。
// 前后端项目均为 TypeScript 实现

当我想修改主页的某个元素时,我发现需要对基于 React 开发的 verdaccion-ui 库进行开发并打包,同时再引入并配置 Verdaccio 的 templatePath 。

后端项目中,渲染主页的部分源码(这里只是想说明要改动一个 UI 是需要在专门的 UI 项目中去修改并编译):

Search.configureStorage(storage);
  /* eslint new-cap:off */
  const router = express.Router();

  router.use(auth.webUIJWTmiddleware());
  router.use(setSecurityWebHeaders);
  // 获取前端页面渲染的 HTML 文件
  const themePath = loadTheme(config) || require('@verdaccio/ui-theme')();
  const indexTemplate = path.join(themePath, 'index.html');
  const template = fs.readFileSync(indexTemplate).toString();

function renderHTML(req, res) {
    const protocol = getWebProtocol(req.get(HEADERS.FORWARDED_PROTO), req.protocol);
    const host = req.get('host');
    const { url_prefix } = config;
    const uri = `${protocol}://${host}`;
    const base = combineBaseUrl(protocol, host, url_prefix);
    const language = config?.i18n?.web ?? DEFAULT_LANGUAGE;
    const darkMode = config?.web?.darkMode ?? false;
    const primaryColor = validatePrimaryColor(config?.web?.primary_color);
    const title = _.get(config, 'web.title') ? config.web.title : WEB_TITLE;
    const scope = _.get(config, 'web.scope') ? config.web.scope : '';
    const options = {
      uri,
      darkMode,
      protocol,
      host,
      url_prefix,
      base,
      primaryColor,
      title,
      scope,
      language
    };

    const webPage = template
      .replace(/ToReplaceByVerdaccioUI/g, JSON.stringify(options))
      .replace(/ToReplaceByVerdaccio/g, base)
      .replace(/ToReplaceByPrefix/g, url_prefix)
      .replace(/ToReplaceByVersion/g, pkgJSON.version)
      .replace(/ToReplaceByTitle/g, title)
      .replace(/ToReplaceByLogo/g, logoURI)
      .replace(/ToReplaceByPrimaryColor/g, primaryColor)
      .replace(/ToReplaceByScope/g, scope);

    res.setHeader('Content-Type', HEADERS.TEXT_HTML);
    // 这里可以看到,只是简单的将 webPage 页面渲染,更多逻辑基本都依赖于 ui 页面的项目,也就是说,这里可以算是一种前后端分离的实现
    res.send(webPage);
  }

前端项目中路由相关的部分源码(这里只是为了说明我们看到的 http://localhost:4873/ 页面的跳转,其实是前端 SPA 的实现):

// AppRoute.tsx
import { createBrowserHistory } from 'history';
import React, { useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { Route as ReactRouterDomRoute, Switch, Router } from 'react-router-dom';

import AppContext from './AppContext';
import loadable from './utils/loadable';

const NotFound = loadable(() => import(/* webpackChunkName: "NotFound" */ 'verdaccio-ui/components/NotFound'));
const VersionContextProvider = loadable(() =>
  import(/* webpackChunkName: "Provider" */ '../pages/Version/VersionContextProvider')
);
const VersionPage = loadable(() => import(/* webpackChunkName: "Version" */ '../pages/Version'));
const HomePage = loadable(() => import(/* webpackChunkName: "Home" */ '../pages/home'));

// 路由枚举,涵盖了 Verdaccio 支持的所有页面,及其对应的组件
enum Route {
  ROOT = '/',
  SCOPE_PACKAGE = '/-/web/detail/@:scope/:package',
  SCOPE_PACKAGE_VERSION = '/-/web/detail/@:scope/:package/v/:version',
  PACKAGE = '/-/web/detail/:package',
  PACKAGE_VERSION = '/-/web/detail/:package/v/:version',
}

export const history = createBrowserHistory({
  basename: window?.__VERDACCIO_BASENAME_UI_OPTIONS?.url_prefix,
});

const AppRoute: React.FC = () => {
  const appContext = useContext(AppContext);
  const { t } = useTranslation();

  if (!appContext) {
    throw Error(t('app-context-not-correct-used'));
  }

  const { user } = appContext;

  const isUserLoggedIn = user?.username;

  return (
    <Router history={history}>
      <Switch>
        <ReactRouterDomRoute exact={true} path={Route.ROOT}>
          <HomePage isUserLoggedIn={!!isUserLoggedIn} />
        </ReactRouterDomRoute>
        <ReactRouterDomRoute exact={true} path={Route.PACKAGE}>
          <VersionContextProvider>
            <VersionPage />
          </VersionContextProvider>
        </ReactRouterDomRoute>
        <ReactRouterDomRoute exact={true} path={Route.PACKAGE_VERSION}>
          <VersionContextProvider>
            <VersionPage />
          </VersionContextProvider>
        </ReactRouterDomRoute>
        <ReactRouterDomRoute exact={true} path={Route.SCOPE_PACKAGE_VERSION}>
          <VersionContextProvider>
            <VersionPage />
          </VersionContextProvider>
        </ReactRouterDomRoute>
        <ReactRouterDomRoute exact={true} path={Route.SCOPE_PACKAGE}>
          <VersionContextProvider>
            <VersionPage />
          </VersionContextProvider>
        </ReactRouterDomRoute>
        <ReactRouterDomRoute>
          <NotFound />
        </ReactRouterDomRoute>
      </Switch>
    </Router>
  );
};

export default AppRoute;

再往后便是插件的开发了,没有看的很细,基本上是基于 Express 做的中间件开发。一来我没有想清楚需要针对插件这一块做哪些功能开发,二来我也还没有细研究 Verdaccio 官网推荐的那些 Plugins 的实现和用途,所以这里就不赘述。

杂谈

团队里很早的时候就用 cnpmjs 搭建了自己的 npm 私有仓库,并且一直运行到现在。但是随着项目越变越多,团队内部的同学对现有的 Npm 私有仓库或多或少有一些抱怨,比如:

  • 现在的私有仓库发包过程比较麻烦
  • 项目中安装私包总会出现失败的情况
  • 私有包耦合度越来越高,维护困难

最开始的时候,我们面对这个问题,以为是当初选择的私有仓库的技术方案有问题,所以为了解决团队中大家吐槽的点,我花了一点时间调研了一下行业内其他团队的技术方案,发现有很多朋友推荐试试 Verdaccio ,我也以为这个能解决我们的问题。

但是在我一顿折腾之后,我把我的结论跟团队老大反馈之后,发现我们走错路了。其实 Verdaccio 本身能解决的问题就是“几乎零配置的方式帮助你和你的团队搭建一个私有 npm 仓库”,仅此而已。而我们回过头来再看,发现我们团队遇到的问题,更多的是如何解决私有包发版、如何对私有包做解耦的问题,当然这是后话,我们会继续解决这个问题,但不是本文需要深究的方向。

至此,经过一顿折腾,我个人对 Verdaccio 的感受,有了不一样的认识,也在这里简单总结一下吧,方便如果有其他需要的同学,再考虑使用它时,能有一个参考。

  • Verdaccio 仅仅解决的是如何傻瓜式的部署一套 npm 私有仓库的问题,它不是银弹
  • 如果对 npm 私有仓库有很高的定制化需求,建议进行二次开发,如果有数据统计的需求,可以考虑在 express 中接入对应的数据库,但如果都进行到这一步了,为何不想想 cnpmjs 是否更符合你的需求呢,当然后续我也会回头再看看 cnpmjs 的情况。
  • 每个团队有每个团队的需求,每个项目也有每个项目存在的意义,不要贸然看其他团队都在用 Verdaccio ,就无脑的切过来,尤其是本身已经用 cnpmjs 搭建过私有库的情况,因为这里还涉及到一个数据迁移的问题,目前来看, cnpmjs 的私有包迁移到 Verdaccio ,没有一个比较好的解决方案(我能想到就是下面2种)
    • 抛弃以往版本,用最新版本在新的 npm 私有库中再发布一次
    • 找到对应的版本,手动或是写脚本的方式,批量迁移一遍
  • 要理解 Verdaccio 的“轻量级”定位,即使它也提供了插件的方式,但是也要考虑它的局限性
  • 通过这次一般深度的实践,有一个体会就是,有问题直接撸官方的文档就好了,有时候搜太多“教程”不一定能够满足你的需求,甚至还会受文章时间和项目版本的影响,导致一些莫名其妙的问题。还有就是很多文章都是简单的把官网撸过来,这就是为什么我第一句就提醒了大家,直接看官网或源码,更快更准。

最后,推荐一套TS全系列的教程吧。 近期在提升TS,收藏了一套很不错的教程,无偿分享给xdm https://www.yidengxuetang.com/pub-page/index.html

参考资料

浏览知识共享许可协议

知识共享许可协议
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。

hauk0101

2021/04/14  阅读:122  主题:嫩青

作者介绍

hauk0101