使用 Nexus3 搭建 npm 私库,超详细的图文教程

3,903 阅读2分钟

使用 Nexus3 搭建 npm 私库,超详细的图文教程

安装运行 Nexus3

1. 下载
2. 安装
  • nexus-3.46.0-01-win64.zip 文件解压到 D:\Program Files\nexus3 目录下,如图所示:

image-20230614164910393

其中:

nexus-3.46.0-01 为 nexus 服务器相关文件目录 sonatype-work 为 nexus 数据存储目录

  • 新增全局变量。在 path 变量中新增 D:\Program Files\nexus3\nexus-3.46.0-01\bin

  • 注册服务。以管理员身份打开命令行窗口。运行nexus /install nexus3。 即可在系统同注册一个名为 nexus3 的服务。

  • windows + r 打开运行窗口,输入services.msc 找到 nexus3 服务,点击启动,即可启动服务,如图所示:

image-20230614165739006

  • 更多命令。
# 启动服务
$ nexus /run nexus3
# 停止服务
$ nexus /stop nexus3
# 删除服务
$ nexus /uninstall nexus3
  • 稍等片刻后,你可以通过在浏览器中输入http://localhost:8081/,进入服务,如图所示:

image-20230614170149239

  • 点击 Sign in 进行登录。其中用户名为 admin,密码在文件 D:\Program Files\nexus3\sonatype-work\nexus3\admin.password 中查看,第一次登录需要修改密码,如修改成 admin123, 修改密码后 admin.password ⽂件会⾃动删除。

  • 至此,nexus 安装运行已全部完成。

创建新仓库

  • 创建一个名为 vue-cms 的仓库,如图所示:

image-20230614171036475

image-20230614171235654

image-20230614171409432

  • 完成后,点击 Create repository 完成。

  • 以上操作完成后,现在我们就可以在浏览列表看到 vue-cms 仓库了,如图所示:

image-20230614171710784

上传 npm 依赖到自定义仓库

接下来我们测试一下,上传项目中的 npm 依赖到我们的自定义仓库

1. 创建项目
  • 创建目录 test-project
  • 执行 npm init -y

image-20230614172553873

  • 安装依赖 npm install chart.js

image-20230614172716826

2. 下载 tgz 包

接下来我们要做的就是,将package.json 中的依赖 chart.js 进行下载。

方式1:

  • 打开 package-lock.json 文件,找到 packages 属性,将其下的所有依赖包,进行下载。

image-20230614173255675

  • 复制每个插件 resolved 属性后的链接到浏览器进行下载。

  • 将下载的 .tgz 包放到与 package.json 同级目录的 node(需创建) 文件夹下。

方式2(推荐!!!自己写的,如果有bug,请及时联系我):

通过 node.js 下载依赖包,需要本地有 node 环境

# 完整目录结构
.
└── npm-publish 根目录    
        ├── node // 依赖包存储目录
        ├── index.js // 入口文件
        ├── node_modules
        ├── package.json
        └── package-lock.json

步骤如下:

# 1. 创建并进入一个新目录
mkdir npm-publish && cd npm-publish

# 2. 初始化项目,生成 package.json 文件
$ npm init -y

# 3. 创建 index.js,并复制代码(见下)

# 4. 安装 axios,因为我是通过axios下载文件的,所以该插件必备
$ npm i axios

# 3. 安装自己所需依赖包,如 chart.js
$ npm i chart.js 

# 4. 打开控制台,执行index.js文件
$ node index.js

# 5. 我想此时已经下载完成了,所有下载依赖放置在同级目录 node 文件夹下
  • index.js 内容
const fs = require("fs");
const path = require("path");

// 读取 package-lock.json 文件
const filePath = path.join(__dirname, "package-lock.json");

let data;
try {
  data = JSON.parse(fs.readFileSync(filePath, "utf8"));
} catch (error) {
  console.log("Error reading or parsing JSON file:", filePath);
}

if (!data) {
  return;
}

const packages = data.packages;
const packageList = Object.keys(packages);
console.log(`======= 共计 ${packageList.length - 1} 个依赖包 =======`);

// 创建 axios 请求
const axios = require("axios");
const targetDir = path.join(__dirname, "node");

if (!fs.existsSync(targetDir)) {
  fs.mkdirSync(targetDir);
}

async function downloadFile(url, outputPath) {
  const writer = fs.createWriteStream(outputPath);
  const response = await axios({
    url,
    method: "GET",
    responseType: "stream",
  });

  response.data.pipe(writer);

  return new Promise((resolve, reject) => {
    writer.on("finish", resolve);
    writer.on("error", reject);
  });
}

// 下载文件
let successCount = 0;
let failCount = 0;
packageList.forEach(async (key, index) => {
  if (key.indexOf("node_modules") > -1) {
    const filePath = packages[key].resolved;
    const fileName = path.basename(filePath);
    const outputPath = path.join(targetDir, fileName);

    try {
      if (fs.existsSync(outputPath)) {
        successCount++;
        console.log(`${fileName}: 文件已存在,已下载 ${successCount} 个文件`);
        return;
      }

      console.log(`正在下载: ${filePath}`);
      await downloadFile(filePath, outputPath);
      successCount++;
      console.log(`${filePath}: 下载完成,已下载 ${successCount} 个文件`);
    } catch (error) {
      failCount++;
      console.log(`${filePath}: 下载失败,已失败 ${failCount} 个文件`);
      fs.unlinkSync(outputPath); // 删除未完成的文件
    }
  }
});

方式3:

通过 python 脚本进行下载,需要有 python 环境

  • package.json 同级目录下创建 download.py 文件, 脚本如下:
# -*-coding:utf-8-*-
import json
import os
import urllib.request
from pathlib import Path


def node_modules(file_dir):
    # 通过递归遍历 node_modules 每个子包的 package.json 解析下载链接
    links = []
    for root, dirs, files in os.walk(file_dir):
        if 'package.json' in files:
            package_json_file = os.path.join(root, 'package.json')
            try:
                with open(package_json_file, 'r', encoding='UTF-8') as load_f:
                    load_dict = json.load(load_f)
                    if '_resolved' in load_dict.keys():
                        links.append(load_dict['_resolved'])
            except Exception as e:
                print(package_json_file)
                print('Error:', e)
    return links


def package_lock(package_lock_path):
    # 通过递归遍历 package-lock.json 解析下载链接
    links = []
    with open(package_lock_path, 'r', encoding='UTF-8') as load_f:
        load_dict = json.load(load_f)
        search(load_dict, "resolved", links)
    return links


def yarn_lock(package_lock_path):
    # 通过递归遍历 xxx-yarn.lock 解析下载链接
    links = []
    with open(package_lock_path, 'r', encoding='UTF-8') as load_f:
        for line in load_f:
            if line.find('resolved') >= 0:
                line = line.replace('resolved', '')
                url = line.strip().strip('"')
                links.append(url)
    return links


def search(json_object, key, links):
    # 遍历查找指定的key
    for k in json_object:
        if k == key:
            links.append(json_object[k])
        if isinstance(json_object[k], dict):
            search(json_object[k], key, links)
        if isinstance(json_object[k], list):
            for item in json_object[k]:
                if isinstance(item, dict):
                    search(item, key, links)


def download_file(path, store_path, flag):
    # 根据下载链接下载
    if not Path(store_path).exists():
        os.makedirs(store_path, int('0755'))

    links = []
    if path.endswith("package-lock.json"):
        links = package_lock(path)
    elif path.endswith("yarn.lock"):
        links = yarn_lock(path)
    else:
        links = node_modules(path)
    print("link resolved number:" + str(len(links)))

    for url in links:
        try:
            filename = url.split('/')[-1]
            index = filename.find('?')
            # 去掉 ? 参数和 # 哈希
            if index > 0:
                filename = filename[:index]
            index = filename.find('#')
            if index > 0:
                filename = filename[:index]
            filepath = os.path.join(store_path, filename)
            if not Path(filepath).exists():
                print("download:" + url)
                # 以防以后对请求头做限制
                opener = urllib.request.build_opener()
                opener.addheaders = [('User-agent', 'Mozilla/5.0')]
                urllib.request.install_opener(opener)
                if flag:
                    new_path = os.path.join(os.getcwd(), 'tgz')
                    if not Path(new_path).exists():
                        os.makedirs(new_path, int('0755'))
                    filepath = os.path.join(new_path, filename)
                urllib.request.urlretrieve(url, filepath)
            # else:
                # print("file already exists:", filename)
        except Exception as e:
            print('Error Url:' + url)
            print('Error:', e)


if __name__ == '__main__':
    # 通过 xxx 文件解析对应依赖树
    download_link = os.path.join(os.getcwd(), 'package-lock.json')
    # 下载文件存放的路径
    download_path = os.path.join(os.getcwd(), 'node')
    # 下载文件是否存放到一个新的路径里,默认存放到 node 目录下, download_flag 为 True 时 存放到 tgz 目录下
    download_flag = False
    download_file(download_link, download_path, download_flag)
    print("ok")
  • 运行脚本,执行 python download.py,也可以修改 scripts 命令来执行。 执行后我们可以看到在 node 目录下有了两个 .tgz 包。

image-20230614174518201

  • 如有报错,请将当前目录下的 nodetgz 文件夹删除后重新执行脚本。
3. 上传依赖
  • package.json 同级目录下创建 publish.sh
#!/bin/bash
PACKAGE_PATH=./node
REPOSITORY=http://localhost:8081/repository/vue-cms/
npm login --registry=$REPOSITORY
for file in $PACKAGE_PATH/*.tgz; do
 npm publish --registry=$REPOSITORY $file
done

如图所示:

image-20230614175634242

  • 在当前项目下,打开 Git Bash,运行 ./publish.sh。如图所示:

image-20230614175813158

  • 输入用户名 admin 和 密码 admin123 以及邮箱 123@qq.com(随意),回车。

image-20230614180059071

  • 靠,报错了401!,如图:

image-20230614180307080

  • 在 nexus 平台上添加 npm Bearer Token Realm

image-20230614180512129

  • 再次执行 ./publish.sh, 成功!

  • 我们可以去 nexus 平台上找到 vue-cms 仓库点进去看到我们已经上传的依赖。如图所示:

image-20230614180747868

测试 npm install

既然 npm 依赖已经上传到了私库上了接下来我们测试一下是否可以成功下载。

  • 删除 vue-cms 项目的 node_modules
  • 创建 .npmrc 文件,指定下载路径。
registry=http://localhost:8081/repository/vue-cms/

如图所示:

image-20230614181219095

  • 打开终端,执行 npm install,成功!

如图所示:

image-20230614181344230

常遇问题

1. 内网上传的 tgz 包无法下载成功

原因:

个别包(这里以 nw@0.36.4-sdk 为例),下载时会自动去它的官网下载安装,内网无法访问导致下载失败。

解决方案:

将下载的 nw-0.36.4-sdk-tgz 解压,其内是 package 文件夹。 阅读其 package.json 文件,发现 "scripts.postinstall": "node scripts/install.js" 走的是 scripts 下的install.js 文件。 阅读 install.js 文件, 发现其 urlBase 为 官网源,将其修改为 我们 nginx 文件下载的服务器,如 http://IP:Port/nwjs/nwjs-sdk-v0.36.4-win-x64.zip, 这里的压缩包是 nw-0.36.4-sdk 的可执行包,点击下载,并将 url = urlBase,注释其平台类型判断代码。 随后在 package 文件夹下新增 .npmrc 文件, 设置 registry 源为内网的 nexus 库, 设置完毕后执行 npm pulish进行推送。 推送成功后即可测试下载。