活着,见天地,见众生,见自己
大家好,我是柒八九 。一个专注于前端开发技术/Rust
及AI
应用知识分享 的Coder
。
前言
在上一篇文章(环境变量:熟悉的陌生人)中我们就提到过,最近在做在gitlab
上发布私有npm
包的事情。
大家都很清楚,为了提高开发效率,我们会利用各种千奇百怪的方式将一些公共的工具方法或者API
进行封装,然后发布的团队成员可以探查到的地方。其中,最常用的方式就是将其构建成一个npm
包然后发布到npm公共仓库 (我们之前写的f_cli就是如此)。但是呢,有一些工具库可能会涉及公司内部信息,我们将其发布到公共仓库就不合适了。此时,我们就需要将npm
发布到内网环境。
今天呢,我们就来讲讲如何在gitlab上发布npm包。
好了,天不早了,干点正事哇。

我们能所学到的知识点
- 初始化项目
- 创建gitlab仓库
- 手动发布
- Semantic-release自动发布
- 本地项目使用私有包
1. 初始化项目
这里我们用一个比较简单的项目来做演示。如果想了解一个功能全备的前端项目都有啥,可以参考之前的文章前端项目里都有啥?
npm init
选择你认为合适的目录(这里我们直接使用demo
目录)。执行下面命令
shell
mkdir demo
cd tool
npm init
下面是我们习以为常的初始化项目的流程,其中有些值需要按照自己的项目而定,或者一路回车也可以。
安装依赖
我们应该安装一些必需和可选的开发依赖项,这将帮助我们轻松构建包。
-
webpack
,这是一个模块打包程序,webpack-cli
是一个使用webpack
的命令行工具。 通过使用webpack
,我们使用babel-loader
在打包之前将我们的ES6
代码转译为ES5
。(在这个项目中我们采用webpack
做为打包构建工具,当然你也可以选择使用vite
。这都是看个人喜好。)shellnpm i --save-dev webpack webpack-cli @babel/core babel-loader
-
jest
用于编写 jest 测试用例(可选)。shellnpm i --save-dev jest
-
prettier
、eslint-plugin-prettier
和eslint-config-prettier
用于规范和格式化我们的代码(可选)。shellnpm i --save-dev prettier eslint-plugin-prettier eslint-config-prettier
-
documentation
用于自动生成文档(可选)。shellnpm i --save-dev documentation
初始化git 仓库
通过git init
初始化git
仓库并且通过配置.gitignore
来忽略一下文件。像环境变量:熟悉的陌生人介绍过的环境变量的配置文件.env
就应该被忽略掉。
bash
.DS_Store
# node模块
node_modules/
# 日志文件
npm-debug.log*
# 编辑器设置
.vscode
# 生产环境/发布
/dist
# 测试覆盖率
/coverage
# 环境变量
.env.*
.DS_Store
是Mac OS
系统自动生成的隐藏文件,用于存储文件夹的自定义属性,如文件夹的图标位置或背景颜色等设置。
它是
Mac
独有的,其他系统如Windows
不会自动生成此文件。每个文件夹下都会生成一个
.DS_Store
文件,用于存储该文件夹的设置。对系统和其他程序没有影响,可以安全删除,但会丢失文件夹的自定义设置。
该文件不参与版本控制,通常会在
.gitignore
文件中忽略。在打包分发程序或共享文件夹时,应该删除
.DS_Store
文件,避免泄露隐私或造成兼容性问题。所以简单来说,
.DS_Store
就是一个Mac
系统使用的设置文件,对开发和分发代码没有实际作用,应该添加到忽略文件中去。
配置项目
正如我们在图片中看到的,我们的项目包含了很多文件和文件夹。现在让我们解释每一个的内容以及它们的用途。(更具体的可以参考之前的前端项目里都有啥?)
配置Prettier
+ ESLint
Prettier
用于自动格式化我们的代码ESLint
确保我们的代码风格保持良好的形式
我们可以通过配置.eslintigonre
/.eslintrc.json
/ .prettierogonre
/ .prettierrc.js
等文件来设置Prettier
+ ESLint
。
.eslintigonre
bash
# 忽略第三方依赖
node_modules
# 忽略配置文件
.eslintrc.js
.prettierrc.js
# 忽略构建输出
dist
build
lib
# 忽略检查单元测试的覆盖率报告
coverage
# 忽略文档输出
docs
.eslintrc.json
json
{
// 配置 ESLint 解析器的选项,指定了语法为 ES6,源代码类型为 ES module
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"plugins": [
"prettier" // 使用 eslint-plugin-prettier 插件
],
"rules": {
"prettier/prettier": [
"error",
{
"endOfLine": "off"
}
] // 调用 prettier/prettier 规则来格式化代码
}
}
.prettierogonre
bash
# 忽略:
node_modules
# 忽略构建输出
dist
build
lib
# 忽略测试覆盖率
coverage
# 忽略文档
docs
.prettierrc.js
js
module.exports = {
// 使用单引号
singleQuote: true,
// 对象和数组的末尾添加逗号
trailingComma: 'all',
// 每行最大长度
printWidth: 120,
// 使用4个空格作为缩进
tabWidth: 4,
};
配置 webpack.config.js
webpack.config.js
是Webpack
配置文件,用于定义如何打包 JavaScript 代码并指定如何输出打包后的文件。
js
const path = require("path");
module.exports = {
// 模式
mode: "production",
// 入口文件
entry: "./index.js",
// 输出配置
output: {
path: path.resolve("dist"),
filename: "index.js",
libraryTarget: "commonjs2",
},
// 模块规则
module: {
rules: [
{
// 对 .js 文件使用 babel-loader 处理
test: /\.js?$/,
exclude: /(node_modules)/,
use: "babel-loader",
},
]
},
// 解析模块请求的选项
resolve: {
extensions: [".js"],
},
};
项目主逻辑(src/methods)
包含 3 个文件,每个文件导出一个特定几何形状面积的计算公式。当然,在实际场景中我们需要放置我们的业务逻辑,下面只是做一个demo
级别的演示。
circleArea.js
js
/**
* 计算圆面的面积
* @param {*} raduis 圆Radius
* @returns {number} 面积
*/
function getCircleArea(radius) {
return Math.PI * radius * radius;
}
module.exports = {
getCircleArea
};
rectangleArea.js
js
/**
* 计算矩形的面积
* @param {*} length 长度
* @param {*} width 宽度
* @returns {number} 面积
*/
function getRectangleArea(length, width) {
return length * width;
}
module.exports = {
getRectangleArea
};
triangleArea.js
js
/**
* 计算三角形的面积
* @param {*} base 底边长度
* @param {*} perpendicularHight 高
* @returns 面积
*/
function getTriangleArea(base, perpendicularHeight) {
return base * perpendicularHeight * 0.5;
}
module.exports = {
getTriangleArea
};
项目主入口(index.js)
一般而言,我们的包都是以index.js
作为主入口。这个可以在package.json
的main
字段中指定。
js
const { getCircleArea } = require('./src/methods/circleArea');
const { getRectangleArea } = require('./src/methods/rectangleArea');
const { getTriangleArea } = require('./src/methods/triangleArea');
module.exports = {
getCircleArea,
getRectangleArea,
getTriangleArea,
};
设置单元测试
一个功能完备的项目,单元测试是必不可少的。但是呢,这个也是因人而异的,我们也可以选择不做这步,毕竟有些项目只是一个资源或者工具的封装。因为,我们在平时开发中已经对这些工具方法都做了验证了。
我们将使用 Jest
框架来编写 3 个方法的单元测试。为此,让我们创建/tests
文件夹并开始编写测试用例:
circleArea.test.js
js
const { getCircleArea } = require('../index');
test('测试 getCircleArea 是否返回一个真值', () => {
expect(getCircleArea(1)).toBeTruthy();
});
test('计算半径为 1 的圆的面积,预期结果:1*1*π = π', () => {
expect(getCircleArea(1)).toBe(Math.PI);
});
rectangleArea.test.js
js
const { getRectangleArea } = require('../index');
test('测试 getRectangleArea 是否返回一个真值', () => {
expect(getRectangleArea(1, 1)).toBeTruthy();
});
test('计算一个长 2 宽 2 的矩形的面积,预期结果:2*2 = 4', () => {
expect(getRectangleArea(2, 2)).toBe(4);
});
triangle.test.js
js
const { getTriangleArea } = require('../index');
test('测试 getTriangleArea 是否返回一个真值', () => {
expect(getTriangleArea(1, 1)).toBeTruthy();
});
test('计算底边长度为 1,高为 2 的三角形的面积,预期结果:1*2*0.5 = 1', () => {
expect(getTriangleArea(1, 2)).toBe(1);
});
我们可以是在package.json
中scripts
字段中新增一段专门用于单元测试的命令,并且在jest
中配置关于jest
的配置信息。
json
{
"scripts" : {
"test": "jest --coverage --passWithNoTests"
},
"jest": {
"verbose": true,
"testEnvironment": "node"
}
}
然后我们通过npm run test
执行单元测试

项目文档生成
通过使用文档工具,我们可以根据代码中包含的 jsDoc
注释自动生成代码文档。
json
{
"scripts": {
// 删除 /docs 文件夹
"docs:clean": "rimraf docs",
// 构建文档
"docs:build": "npm run docs:clean && documentation build src/** -f html -o docs",
// 删除 /dist 文件夹
"clean": "rimraf dist",
// 生产模式构建项目
"build": "npm run clean && webpack --mode production",
// 准备发布,构建项目和文档
"prepare": "npm run build && npm run docs:build",
// 运行测试和覆盖率
"test": "jest --coverage --passWithNoTests"
},
}
现在,我们只需运行相应的脚本,就能轻松地测试、构建和生成项目文档。例如,在构建软件包并准备将其投入生产时,我们只需运行 :
arduino
npm run prepare
这将生成两个文件夹 :
/dist
: 代码的发布版本/docs
:包含代码文档
2. 创建gitlab仓库
这一步其实很简单,就是在gitlab
中创建存放我们私有包的仓库。


随后,我们将我们本地仓库和gitlab
仓库做一下关联。
shell
git remote add origin https://gitlab.com/xxx/demo.git
git push --set-upstream origin master
然后我们将本地代码推人到远程仓库中。
shell
git add.
git ci -m 'feat: 项目初始化'
git push
这样我们本地代码就和远程代码有了联系。
这一步简单的不能简单了。
3. 手动发布
其实,针对在gitlab
中发布npm
包有两种方式,
- 一种是手动推送,这个每次在本地通过一些命令执行发布操作。
- 另外一种是利用
Semantic-release
走CI/CD
执行发布
下面我们先从简单的来。毕竟,不是所有项目都需要走CI/CD
或者有些工具包本身逻辑简单只需要做一次发布,终身不变。
生成令牌
项目创建完成之后,需要生成项目私有的认证令牌 ,我们把demo
这个库作为我们要发布的npm
包,先生成它的Deploy tokens
token作用:最后发布npm包的时候需要用来认证
我们在Demo
项目的主页面的Settings->Repository->Deploy tokens
中设置token
。
在新增界面中Name
可以随意起。然后比较重要的是,我们需要勾选read_package_registry/write_package_registry
的选项。这样我们这个token
就拥有了对package registry
的读写权限。
点击Create deploy token
后,gitlab
就会为为们生成key-value
格式的值。我们只关心value
。并且,我们需要将value
保存起来,因为离开这个页面,这个值就不会显示了。
本地项目新增.npmrc
要从私有注册表(在我们的情况下是Gitlab
)安装一个软件包,我们需要告诉npm
从哪里安装我们的软件包。为了实现这一点,我们在项目的根目录中创建一个名为.npmrc
的配置文件。
.npmrc
文件是NPM
项目中的配置文件,用于定义NPM在运行命令时的行为设置。通过.npmrc
文件,我们可以配置NPM
的各种行为,例如设置日志级别、定义包的注册表、配置代理等。这个文件可以帮助你在项目级别或全局级别上自定义NPM
的行为,使得NPM命令在执行时按照你的配置进行操作。.npmrc
文件采用INI格式,其中包含了一系列的键值对,用于配置NPM的各种选项。
我们需要两个比较重要的信息
@<your-scoop>:registry=https://gitlab.com/api/v4/npm/
//gitlab.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken=${AUTH_TOKEN}
上面有几个参数我们需要变更
<your-scoop>
:这里设置我们的想要的名称,这里我们设置为front789
<your_project_id>
:这是我们demo
项目在gitlab
的id,这个我们可以在Settings->General->Project ID
获取AUTH_TOKEN
我们使用环境变量来处理,这个在之前的文章中有过介绍。并且该值就是刚刚我们创建并单独保存的deploy tokens
less
@front789:registry=https://gitlab.com/api/v4/npm/
//gitlab.com/api/v4/projects/55073819/packages/npm/:_authToken=${AUTH_TOKEN}
package.json 新增publishConfig
json
{
"publishConfig": {
"@front789:registry": "https://gitlab.com/api/v4/projects/55073819/packages/npm/"
}
}
针对这块的解释,我们在你真的了解package.json吗?有过介绍,这里不在过多解释。
手动发布npm
其实这步和我们将一个包发布到npm
一样。都是通过npm publish
进行发布。但是呢,由于我们使用环境变量 (AUTH_TOKEN
)所以我们需要将AUTH_TOKEN
放置到命令行参数。
shell
AUTH_TOKEN=gldt-xxxx npm publish
如果有如下的结果,就表明我们这个包已经发布成功了。
随后,我们就可以在demo
项目中的Deploy->Package Registry
中看到发布成功的包
弊端
但是,采用这种方式进行发布包时,有一些弊端。
手动更新版本号
我们都知道在更新包时,我们需要更新版本信息。例如从1.0.0
更新到1.1.0
等。
但是,采用手动发布时,我们需要手动将项目的版本号进行更改。如果不更改还是用上面的命令(AUTH_TOKEN=gldt-xxxx npm publish
)发布时。就会报错。
下图的报错原因就是因为,本地版本号和远程仓库有冲突,然后发生了报错。
手动编译
由于我们这个项目不需要多次进行本地编译,但是有些包的更新,可能涉及到本地打包的流程,但是通过上述操作,我们就需要本地打包,然后再进行上传处理。这就很不智能。
针对上述的种种弊端,我们急需一种更加智能的方式。这就是我们接下来要讲的利用Semantic-release自动发布
。
4. Semantic-release自动发布
相比之前的手动发布,我们本节中的自动发布是利用了Gitlab
的CI/CD
功能,但凡和CI/CD
有关,那势必.gitlab-ci.yml
肯定是绕不过的坎。(这又是一篇可能大写特写的内容,我们下面就不过多解释)
.gitlab-ci.yml
yml
image: node:latest
stages:
- build
- test
- document
- publish
build:
stage: build
script:
- npm install
- CI=false npm run prepare
cache:
paths:
- node_modules/
- dist/
- src/
- docs/
artifacts:
expire_in: 1 days
when: on_success
paths:
- node_modules/
- dist/
- src/
test:
stage: test
script:
- npm run test
dependencies:
- build
cache:
paths:
- coverage/
artifacts:
expire_in: 1 days
when: on_success
paths:
- coverage/
pages:
stage: document
dependencies:
- build
script:
- mkdir .public
- cp -r docs/* .public
- mv .public public
artifacts:
paths:
- public
only:
- master
- tags
publish:
stage: publish
variables:
NPM_TOKEN: ${AUTH_TOKEN}
script:
- git config --global http.emptyAuth true
- npm run semantic-release
- echo "-- publish completed succesfully"
dependencies:
- build
- test
only:
- master
- tags
我们简单解释一下上面代码。它定义了一系列的阶段(stages
)和对应的任务(jobs
),以及这些任务之间的依赖关系和执行条件。
-
image: node:latest
:指定了使用的Docker镜像,这里使用了最新版本的Node.js镜像。 -
stages
:定义了多个阶段,包括构建(build
)、测试(test
)、文档生成(document
)和发布(publish
)。 -
build
:构建阶段的任务,包括安装依赖和运行构建脚本,并且定义了缓存和构件。构建成功后,将node_modules/
、dist/
和src/
目录作为构件保存,并且设置构件的过期时间为1天。 -
test
:测试阶段的任务,依赖于构建阶段。在构建成功后,运行测试脚本,并且定义了测试覆盖率的缓存和构件。 -
pages
:文档生成阶段的任务,依赖于构建阶段。在构建成功后,将docs/
目录下的文件复制到.public
目录,并将.public
目录重命名为public
,然后将public
目录作为构件保存。这个任务只在master
分支和标签上执行。 -
publish
:发布阶段的任务,依赖于构建和测试阶段。在构建和测试成功后,设置了NPM令牌,并运行语义化版本发布脚本。这个任务只在master
分支和标签上执行。
总之,这个配置文件定义了一个完整的CI/CD流程,包括构建、测试、文档生成和发布。它使用了缓存和构件来优化任务的执行效率,并且设置了任务的依赖关系和执行条件,以确保任务按照正确的顺序执行。
我们的流水线包含4个阶段,每个阶段负责执行一个任务。

此时,当我们通过
csharp
git add .
git ci -m 'feat: xx'
进行代码提交时,由于设置了.gitlab-ci.yml
所以他会自动触发gitlab
的CI/CD
。但是呢,上面的配置有问题。

我们看到publish stage
失败了,我们回头看我们的.gitlab-ci-yml
配置,发现在publish
阶段有一个环境变量 (AUTH_TOKEN
),这个AUTH_TOKEN
其实就和我们上一节讲的token是一样的。这里就不在过多说明。
其实,在publish
中script
有一个很明显的命令:
shell
npm run semantic-release
这是我们这节的主角。它可以帮助我们实现在gitlab
中自动发布包。
semantic-release相关操作
semantic-release
:帮助我们根据Git提交来管理何时发布新版本,并且还支持语义化版本。
安装相关依赖
sql
npm install semantic-release @semantic-release/git @semantic-release/gitlab @semantic-release/npm --save-dev
配置semantic-release
我们通过.releaserc.json
来配置semantic-release
的动作。
json
{
"branches": ["master"], // 定义了只有在master分支上的提交才会触发发布流程
"plugins": [ // 定义了语义化版本发布所使用的插件
"@semantic-release/commit-analyzer", // 使用commit-analyzer插件来分析提交信息
"@semantic-release/release-notes-generator", // 使用release-notes-generator插件来生成发布日志
[
"@semantic-release/gitlab", // 使用gitlab插件来发布到GitLab
{
"assets": [ // 定义了发布时需要包含的文件
{ "path": "index.js", "label": "Module" }, // 发布时包含index.js文件,并标注为Module
{ "path": "README.md", "label": "Documentation" } // 发布时包含README.md文件,并标注为Documentation
]
}
],
"@semantic-release/npm", // 使用npm插件来发布到npm
[
"@semantic-release/git", // 使用git插件来提交发布的变更
{
"assets": ["package.json"], // 定义了发布时需要提交的文件
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" // 提交的提交信息模板,包括版本号和发布日志
}
]
]
}
更新package.json
修改main
由于我们最终想要的代码是打包后的代码,所以我们需要修改main
,将其指向dist/index.js
。
这样做是为了在使用
semantic-release
发布npm包时,确保发布的包中包含了经过构建后的代码而不是源代码。通常,源代码位于项目的根目录,而经过构建后的代码(通常是位于dist/
目录下)才是用于实际部署和使用的代码。因此,通过将main
字段指向经过构建后的代码文件,可以确保发布的npm包包含了正确的可执行代码,而不是源代码文件。这样做可以提高包的可用性和可靠性,同时也符合npm包的最佳实践。
新增scripts命令
json
{
"scripts"{
//...
"semantic-release": "semantic-release"
//...
}
}
新增publishConfig
这里的操作和之前的手动发布的情况是一样的,就是设置一套发布规则。
json
{
"publishConfig": {
"@front789:registry": "https://gitlab.com/api/v4/projects/55073819/packages/npm/"
}
}
配置.npmrc
此处的配置和手动处理也是一样的。
less
@front789:registry=https://gitlab.com/api/v4/npm/
//gitlab.com/api/v4/projects/55073819/packages/npm/:_authToken=${AUTH_TOKEN}
在我们提交代码到gitlab
后,在publish
阶段还是会报错。

上面提示我们需要在CI
中配置GITLAB_TOKEN
。
配置gitlab 环境变量
GITLAB_TOKEN
作为发布软件包的一部分,
semantic-release
在package.json
中增加版本号。为了让semantic-release
能够提交这个更改并推送回GitLab
,流水线(pipeline
)需要一个名为GITLAB_TOKEN
的自定义CI/CD
变量。
下面是详细的配置过程。这里不在多聊。
NPM_TOKEN

我们可以在Settings->CI/CD->Variables
中设置相关的环境变量。
此时我们将变量的key设置为NPM_TOKEN
,值的话就是我们之前保存的Deploy Token
。
AUTH_TOKEN
和配置
NPM_TOKEN
同样的操作流程。
经过上述的操作,我们就配置了,我们发布npm
包需要的各种环境变量。

发布包
由于我们配置了semantic-release
,只要我们git push
本地代码到gitlab
,然后后续所有的流程就交由gitlab
负责。
此时,在Build->Pipelines
中可以看到部署过程。
经过短时间的等待,就会出现如下结果。

也就是说,我们CI/CD
成功了。
那么,如何验证我们的npm
包是否发布成功呢。
我们可以在Deploy->Package Registry
中进行查看。

每当我们本地push
代码到gitlab
就会触发一次发布流程。也就是说在Package Registry
中就会出现多个版本的npm包
。
5. 本地项目使用私有包
既然,我们向gitlab
发布完私包了,在对应的位置也看到了有包的信息。是不是意味我们可以通过npm/yarn
进行安装了呢。
让我们随意在一个新项目(demo_test
)中执行安装命令npm i @front789/demo

从错误中看到在执行npm i @front789/demo
命令时候,命令行提示在https://registry.npmjs.org
不存在@front789/demo
。
这下是不是恍然大悟了,我们虽然在gitlab
上发布了我们的私包,但是在npm i xx
的时候,如果额外指定,它是会像我们指定的仓库寻找对应的包。
这里多说几句,我们可以通过nrm
来切换和查看我们的npm
的源。

使用nrm ls
探测到我们项目所用的是npm
的源。

那么,我们就需要在我们项目中指定当遇到@front789/demo
时候,我们需要从哪里去寻找。这就需要用到.npmrc
了。
其实在gitlab
的Package Registry
中已经给我们提示了。

上面分了两种安装方式
- Instance-level
- Project-level
其实这两种方式都一样,我们就挑一种来解释。
我们在demo_test
项目中新增一个.npmrc
。然后配置如下代码。
less
@front789:registry=https://gitlab.com/api/v4/projects/55073819/packages/npm/
当我们再次执行npm i @front789/demo
时,我们以为我们完事大吉,但是控制台就有报错。

当我们看到401 Unauthorized
的错误是不是感觉到似曾相识。我们在利用CI/CD
发布包时也遇到过。因为我们在新建项目的时候,就是选择了私有。
相同的处理方式,我们可以利用环境变量来为我们的npm
新增权限信息。
我们新增另外一条命令,并且用AUTH_TOKEN
作为参数,要求我们在cli
中提供必要的授权信息。
less
@front789:registry=https://gitlab.com/api/v4/projects/55073819/packages/npm
//gitlab.com/api/v4/projects/55073819/packages/npm/:_authToken=${AUTH_TOKEN}
那么我们就可以在cli
中执行AUTH_TOKEN=gldt-xxx npm i @front789/demo

然后在package.json
中看到我们发布在gitlab
上的私包。
项目验证
既然,我们已经在本地安装了发布在gitlab
的私包。虽然在node_modules
中能看到包信息,但是我们还是不放心。
所以,我们在demo_test
中新增了以index.js
,内容如下。
js
import pkg from '@front789/demo';
const { getCircleArea } = pkg;
console.log(getCircleArea(10))
然后,我们在使用node index.js
进行验证。

完美🎉🎉🎉🎉🎉,这样我们就拥有了一个发布在gitlab
上的私包了。
后记
分享是一种态度。
全文完,既然看到这里了,如果觉得不错,随手点个赞和"在看"吧。
