从 package.json 来聊聊如何管理一款优秀的 Npm 包

2022-09-19 17:08:33 浏览数 (1)

写在前边

其实原本只是想写一些有关于 Package.json 相关的内容,但是最近在关于业务频繁迭代的 Npm 包版本管理方面做了一些尝试,积累了一部分心得,所以刚好也拿出来在文章的后半部分和大伙分享下。

整篇文章大概会涉及到以下二个方面内容:

  • Package.json 中一些重要字段用途讲解,比如 browserexportsmodulemainversion等。
  • 频繁业务迭代背景下,如何尽量语义化的迭代 NPM 包版本。

也许,你并不了解 Package.json

开始之前大家可以思考一个在平常不过的小问题:

Axios 大家或多或少都会使用过。它的一大特性即使支持双端(NodeJs 和 Web)端同时良好运行。不过你有想过在我们日常 build web 项目时,它是如何抹平环境差异呢?

换句话说,Axios 中的 Node 请求处理依赖的 http/https 模块。如果将它打包进入 web 代码一定是会发生问题的,但当日常我们使用 Axios 进行 web 项目构建时并不存在任何问题,不知道大家有没有思考过这个问题。

经常使用 Axios 的小伙伴可以稍微思考一下上面的问题,稍后文章中会为你解开这个迷惑。

首先,我们从 Package.json 作为文章切入点来聊聊 NPM 包中的声明文件。

mainmodule

关于 main 以及 module 字段对于大家来说应该是非常常见了。

每当我们通过 npm install xxx 安装某个包时,之后在项目中引入该包。

绝大多数情况下,针对于引入的包入口文件都是取决于这两个字段的设置。

比如,我们以 vue 为例,在 vuepackage.json 中存在这样的声明:

代码语言:javascript复制
// ...
"main": "dist/vue.runtime.common.js", 
"module": "dist/vue.runtime.esm.js",
复制代码

它即表示当我们引入 vue 时,会根据导入模块时不同的模块规范语句查找不同的入口文件

简单来说,当你在项目中使用 const vue = require('vue') 时和使用 import vue from 'vue' 进行引入时实际上引入的是完全不同的两个文件。

前者由于是 CJS 的引入方式,所以会自动寻找对应 main 字段中的 node_modules/vue/dist/vue.runtime.common.js

而后者由于 ESM的方式则会自动寻找对应的 module 字段中的路径 node_modules/vue/dist/vue.runtime.esm.js

当然,如果 vue 版本不同或者你使用的是 pnpm 上边示例代码中的 node_modules 中的目录都不尽相同。

其次,偶尔有些情况下我们引入的包并不存在这两个字段。

  • 如果你是以 ESM 的方式引入该包时,首先会去寻找对应的 module 字段。如果 module 不存在的情况它会接下来去寻找对应的 main 字段。
  • 当然,如果 main 字段也不存在的话,默认是会寻找当前包中的 index.js 作为入口文件。

上述是一些这两个字段的基本使用情况,现在我们假设这样一种场景。

通常我们在 Web 项目中引入包的方式基本都是通过 ESM 的方式进行 import xxx from 'xxx' 的方式作为引入。

毫无疑问,通常它会按照上述的查找方式去寻找对应的 module -> main -> index.js 进行查找。

但如果此时存在另外一种特殊场景,虽然我们使用 ESM 的方式来引入了这个包。但是我们希望在引入时寻找该包的入口文件是根据 main 字段进行查找而非 module

这种情况下,我们可以利用构建工具来帮我我们来实现对应的效果。

比如假使我们使用 webpack 对于我们的 FE 项目进行构建,此时我们希望所有的包引入默认按照 main 字段的路径作为入口文件,那么此时我们可以通过 resolve.mainFields 来进行处理。

当然如果你使用 Rollup 进行构建你的项目,你也可以通过 @rollup/plugin-node-resolve 插件中的 mainFields 来实现这个功能。

当然 resolve.mainFields 默认会根据不同的构建环境来设置默认值。感兴趣的朋友可以点击这里查看对应的默认预设。

比如我们以为 webpack 举例:

webpack 在 target: web 的情况下 mainFields 字段默认为 ['browser', 'module', 'main']

这样就意味着假如你使用 webpack 构建你的 Web 项目,无论你使用 ESM 还是 CJS 语法引入第三方包,本质上都是会优先查找 module 字段是否存在,之后才会去寻找 main 字段。

关于 browser 字段我们会在稍后详细讲述到。

当然,如果你的项目不依赖于任何构建工具作为纯 NodeJs 项目。那么其实仍然是会按照我们刚才所述的查找规则进行查找的。

browser

上述我们描述了关于 modulemain 字段的含义,本质上它们两个都是针对于导入 Npm 包时规定按照哪个字段的路径去查找入口文件的字段。

其实还有一个额外重要的 browser 字段,它是一个社区沉淀的字段。同样也是针对于特定环境(浏览器环境)下的入口文件定义字段。

关于 browserpackage.json 中有两种配置方式。

通常我们使用最多的也就是第一种。配置 browser 为一个 String 类型的路径。

第一种含义

通常,通过配置 browser 为一个单一的字符串时它会替换 main 字段作为浏览器环境下的包入口文件。

比如:

代码语言:javascript复制
"browser": "./lib/browser/main.js"
复制代码

通常,我们在使用 webpack 构建我们的项目时候默认构建环境 target 配置会是web

上述我们提到过的 resolve.mainFields 字段其实在浏览器环境下 target:web 下默认值会是:

代码语言:javascript复制
mainFields: ['browser', 'module', 'main'],
复制代码

这也就意味着,假使某个 NPM 包中同时存在 mainmodulebrowser 三个字段。

当构建环境为 web 时(浏览器环境下),那么该包的入口文件会变为 browser 字段而而非其他两个字段。(前提是你没有修改resolve.mainFields配置)

第二种含义

当然,browser 字段还有一种不是很常用,但是在特殊情况下会存在意想不到效果的神奇用法。

将它配置为一个 Map 对象表示声明需要替换的路径或者文件。

单独来听描述也许会感觉稍微有点生涩,没关系,接下来我们来稍微解释一下这句话的含义:

假如我们在项目中存在这样一个 NPM 包 qingfeng

  • package.json
代码语言:javascript复制
{
  "name": "qingfeng",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "browser": {
    "./src/server.js": "./src/client.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
复制代码

可以看到 package.json 中,我们设置了 browser 字段为一个路径映射的对象。

之后,我们来看看 index.js 中的内容:

代码语言:javascript复制
// 项目入口文件
import server from './src/server'
import client from './src/client'

if (typeof window === 'undefined') {
  console.log(server)
} else {
  console.log(client)
}
复制代码

在入口文件中,我们根据运行环境做了简单的判断。

如果 window 不存在那么就 log server 模块的内容,如果存在则 log client 模块。

此时,我们来看看各个模块中的内容:

代码语言:javascript复制
// src/server.js
import path from 'path'

const server = path.resolve()

export default server
复制代码
代码语言:javascript复制
// src/client.js
const isClient = 'client'

export default isClient
复制代码

可以看到 Server.js 中的代码依赖了 path 模块,引用 node 的 path 模块在我们日常的项目中(浏览器环境下)一定是会报错的。

此时 browser 字段的作用就会体现出来了:当我们在构建工具(webpack)中声明的代码环境为浏览器环境web下,并且browser字段为对象时。 此时寻找 Npm 包的入口文件仍然会按照原有的入口文件字段去查找,比如 main 或者 module

不过,在编译该包时会根据 browser 字段中的 Map 将包内的模块进行映射。

比如我们 qingfeng 包中引入了:

import server from './src/server'

在 browser 环境中进行构建时会将它的路径替换为

import server from './src/client'进行打包。

上述是 Webpack development 模式打包后该模块的代码,可以看到最终在 web 环境下两个条件 case 中的 log 是一模一样的模块。

当然,你也可以将对应模块的值设置为 false,表示禁止将该 module 加入到构建的产物中。比如

代码语言:javascript复制
  "browser": {
    // 构建时,如果是浏览器环境下不会将该包的 /src/server.js 模块内容打包进去
    "./src/server.js": false
  },
复制代码

不过,如果你的 NPM 可以做到在 client 和 server 环境下可以无差异化的实现的话,那么其实这个字段是完全可以被忽略的。

exports

Node 在 v12.7.0 版本中引入了 exports 字段作为 package.json 中对于 main 字段的强大替代品。

在各个开源库的 package.json 中你也许会经常见到这字段,接下来我们来聊聊 exports 字段是如何被处理的。

首先我们需要明确的是,exports 在 node v12.7.0 后提供了对于入口文件的替代品字段,它的优先级是高于任何入口字段的(modulemainbrowser

简单来说,加入我们的 package.json 中同时定义了 exports 字段和 modulemainbrowser 字段。

那么,在 Node v12.7.0 以上版本相当于 modulemainbrowser 等字段都不会生效,仅仅 exports 字段会生效。

接下来,我们来看看 exports 字段究竟应该如何使用:

路径封装

首先 exports 字段可以对于包中导出的路径进行封装。

比如下面的代码:

代码语言:javascript复制
{
  // 表示该包仅存在默认导出,默认导出为 ./index.js
  "exports": "./index.js"
}

// 上述的写法相当于
{
  "exports": {
    ".": "./index.js"
  }
}
复制代码

当我们定义了该字段后,该 Npm 包仅支持引入包自身,禁止引入其他子路径,相当于对于子路径的封装。

换句话说,我们仅仅只能引入 index.js。比如我们引入了未在 exports 中定义的模块。

代码语言:javascript复制
// Error
// 此时控制台会报错,找不到该模块(无法引入在 exports 未定义的子模块路径)
import qingfeng from 'qingfeng/src/server.js'

// correct
import qingfeng from 'qingfeng'
复制代码

同时在使用 exports 关键字时,可以通过 . 的方式来定义主入口文件:

代码语言:javascript复制
{
  "exports": {
    // . 表示引入包默认的导出文件路径, 比如 import qingfeng from 'qingfeng'
    // 这里的 . 即表示未携带任何路径的 qingfeng,相当于默认导出 ./index.js 文件的内容
    ".": "./index.js",
    // 同时额外定义一个可以被引入的子路径
    // 可以通过 import qingfengSub from 'qingfeng/submodule.js' 进行引入 /src/submodule.js 的文件
    "./submodule.js": "./src/submodule.js"
  }
}
复制代码

条件导出

同样, exports 字段的强大不仅仅在于它对于包中子模块的封装。这个字段同时提供了一种根据特定条件映射到不同路径的方法。

比如,通常我们编写的 NPM 包支持被 ESM 和 CJS 两种方式同时引入,根据不同的引入方式来寻找不同的入口文件。

在不使用 exports 字段时,我们可以通过 modulemain 来进行区分,比如:

代码语言:javascript复制
"module": "./index-module.js",
"main": "./index-require.cjs"
复制代码

exports 字段中同时为我们提供了该条件判断:

代码语言:javascript复制
// package.json
{
  "exports": {
    // ESM 引入时的入口文件
    "import": "./index-module.js",
    // CJS 方式引入时寻找的路径
    "require": "./index-require.cjs"
  },
}

// 相当于
{
  "exports": {
    "import": {
        ".":  "./index-module.js"
    },
    "require": {
        ".": "./index-require.cjs"
    }
  },
}
复制代码

可以看到 exports 关键字中定义的 key 为 importrequire 分别表示两种不同的模块引入方式使用该包时引入的不同文件路径。

关于条件判断的 Key 值,除了上述的 importrequire 分别代表的 ESM 引入和 CJS 引入的方式,NodeJS 同样提供了以下的条件匹配:

  • "import"- 当包通过 ESM 或加载时匹配 import(),或者通过 ECMAScript 模块加载器的任何顶级导入或解析操作。
  • "require"- 当包通过 CJS 加载时,匹配require()
  • "default"- 始终匹配的默认选项。可以是 CommonJS 或 ES 模块文件。这种情况应始终排在最后。(他会匹配任意模块引入方式)

需要注意的是 exports 中的 key/value 顺序很重要,在发生条件匹配时较早的条目具有更高的优先级并优先于后面的条目。

当然上边我们提到的条件导出不仅仅适用于包的默认导出路径,同样也适用于子路径。比如:

代码语言:javascript复制
{
  "exports": {
    ".": "./index.js",
    "./feature.js": {
      "import": "./feature-node.js",
      "default": "./feature.js"
    }
  }
}
复制代码

嵌套条件

上边我们说到过 exports 的条件匹配,它支持不同的引入方式从而进行不同的条件导出。

同样 exports 还支持多层嵌套,支持在运行环境中嵌套不同的引入方式从而进行有条件的导出。

比如:

代码语言:javascript复制
{
  "exports": {
    "node": {
      "import": "./feature-node.mjs",
      "require": "./feature-node.cjs"
    },
    "default": "./feature.mjs"
  }
}
复制代码

上述的匹配条件就类似于 js 中的 if 语句,首先检查是否是 Node 环境下去运行。如果是则进入模块判断是 ESM 引入方式还是 CJS 方式。

如果不是,则进行往下匹配进入 default 默认匹配,default 会匹配任何方式。

同样 NodeJs 中默认支持以下运行环境的:

  • "node"- **匹配任何 Node.js 环境。**可以是 CommonJS 或 ES 模块文件。在大多数情况下,不需要显式调用 Node.js 平台。
  • "node-addons"- 类似于"node"并匹配任何 Node.js 环境。此条件可用于提供使用本机 C 插件的入口点,而不是更通用且不依赖本机插件的入口点。

更多的 exports key

当然,除了上述 Node 中支持的 exports key 的条件。比如上述我们提到的 importrequirenodedefault 等。

同样,exports 的 Key 也支持许多社区中的成熟关键字条件,比如:

  • "types"- typescipt 可以使用它来解析给定导出的类型定义文件
  • "deno"- 表示 Deno 平台的关键 key。
  • "browser"- 任何 Web 浏览器环境。
  • "development"- 可用于定义仅开发环境入口点,例如提供额外的调试上下文。
  • "production"- 可用于定义生产环境入口点。必须始终与 互斥"development"

最后,让我们以 Vue/Core 中的 exports 来为大家看看开源项目中的 exports 关键字用法:

代码语言:javascript复制
  // ...
  "exports": {
  	".": {
          "import": {
                "node": "./index.mjs",
                "default": "./dist/vue.runtime.esm-bundler.js"
             },
            "require": "./index.js",
            "types": "./dist/vue.d.ts"
  	},
  	"./server-renderer": {
            "import": "./server-renderer/index.mjs",
            "require": "./server-renderer/index.js"
  	},
  	"./compiler-sfc": {
            "import": "./compiler-sfc/index.mjs",
            "require": "./compiler-sfc/index.js"
  	},
  	"./dist/*": "./dist/*",
  	"./package.json": "./package.json",
  	"./macros": "./macros.d.ts",
  	"./macros-global": "./macros-global.d.ts",
  	"./ref-macros": "./ref-macros.d.ts"
    }
    // ...
复制代码

相信看懂上面这份 exports 配置对于大家来说已经小菜一碟了。

如果你仍然不是很明白上述的配置,那么一定请你翻回去认真读一读上边的内容~

自定义运行环境

上述针对于 exports 的字段的解释其实已经基本结束了,但是通过上边的描述我们清楚 Node 在查找模块时会根据 exports 中的不同环境来进行匹配对应包的导出路径。

不知道有没有好奇心重的小伙伴,上述所谓的 browserdevelopment 等运行环境究竟是如何被设置的呢

或者换一个问题,如果我们在 exports 中希望额外添加一个环境的引入路径,应该如何做呢?

比如,此时我们希望定义一个名为 qingfeng 的加载环境:

代码语言:javascript复制
// qingfeng 所在的 NPM 包 package.json
{
  "name": "qingfeng",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "exports": {
    // 匹配 qingfeng 的运行环境,当匹配当前运行环境为 qingfeng 时并且引入方式为 ESM 加载时
    // 该包运行 ./hello.js
    "qingfeng": {
      "import": "./hello.js"
    }
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

// hello.js
const qingfeng = 'wanghaoyu'

export default qingfeng
复制代码
代码语言:javascript复制
// 项目入口文件 (注意是引入 qingfeng 这个包的项目)
import qingfeng from 'qingfeng';

console.log(qingfeng, 'qingfeng');
复制代码

其实答案也非常简单,在运行 NodeJs 脚本时可以通过 --conditions 标志添加自定义用户条件。

比如此时我们通过

node --conditions=hello src/index.js

来运行项目入口文件,随后 terminal 中打印出 wanghaoyu

这样也就达成了我们自定义的 qingfeng 运行环境。

构建工具补充

写到这里,因为牵连了一些构建工具的内容。稍微和大家总结和解释一下有关于构建工具的配置选项,以免大家混淆。

我们以 webpack 为例,所谓的 target 仅仅是表示构建环境的区别。比如传入 web 表示 browser 构建环境,传入 node 表示 node 构建环境。

对应过来也就是设置 --conditions 参数。

而最终入口文件是使用 module 还是 main 字段最终还是决定于 mainFields 字段。

只不过 webpack 中的 mainFields 会根据 target 存在一些默认的预设值,当然你也可以手动修改配置:

代码语言:javascript复制
const path = require('path');
// 强行告诉 webapck 运行环境为 browser,但是入口文件仍然使用 main 字段。
module.exports = {
  entry: './src/index.js',
  devtool: false,
  target: 'web',
  mode: 'development',
  resolve: {
    unsafeCache: false,
    mainFields: ['main'],
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js',
  },
};
复制代码

简单来说也就是 target 处理的是构建环境相关,而 mainFields 处理的是入口文件相关。之所以 target 不同导致引入包的入口字段不同,是因为根据 target 的不同 mainFields 会有不同的预设默认值。

当然,上述的条件都是建立在包中不使用 exports 关键字的前提下。

如果引入的 Npm 包中定义了 exports 关键字来定义对应的入口文件导出,上文我们提到过对应的 modulemain 字段都是无效。

此时自然 webpack 中的 resolve.mainFields 字段也会失去它的效果,我们需要通过 resolve.conditionNames 字段来定义对应的环境。

也就是说,在引入的 Npm 包的 pck 中如果存在 exports 关键字时,构建配置的 resolve.mainFields 是无效的。

如果未设置 resolve.conditionNames 字段,那么默认 webpack 会按照你当前的运行环境以及引入方式从而去 npm 包中的 exports 字段查找对应匹配的文件。

比如:

代码语言:javascript复制
// webpack.config.js
const path = require('path');

module.exports = {
  entry: './src/index.js',
  devtool: false,
  target: 'web',
  mode: 'development',
  resolve: {
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js',
  },
};


// 项目文件
// 会优先在 exports 字段中的 browser 环境下寻找 import 字段
import qingfeng from 'qingfeng'

// 会优先在 exports 字段中的 browser 环境下寻找 require 字段
const qingfeng = require('qingfeng')
复制代码

当设置 resolve.conditionNames 时,会按照 resolve.conditionNames 匹配的字段在 exports 字段中查找入口文件。

设置 resolve.conditionNames 时,会忽略你所设置的 target ,仅仅会按照 conditionNamesexports 字段中寻找对应的导出文件。

需要注意的是,所谓的忽略 target 并不代表是 target 字段无效了,而是说在寻找引入的 Npm 包 exports 入口文件时并不会按照 target 相关的环境来寻找,而是会完全按照 resolve.conditionNames 设置的值来寻找。(当前构建环境仍然为 target

target 的作用不单单是在未设置 resolve.conditionNames 时决定项目内引入的 Npm 包关于 exports 的相关路径匹配的,它更多的作用是会根据不同的构建环境来设置一些默认的构建预设。

比如:

代码语言:javascript复制
const path = require('path');

module.exports = {
  entry: './src/index.js',
  devtool: false,
  target: 'node',
  mode: 'development',
  resolve: {
    // mainFields 在 Npm 包中存在 exports 的情况下完全无用
    mainFields: ['main'],
    // 即使设置了 target: node 环境,但是由于设置了 conditionNames: ['browser', 'import']
    // 仍然会去 browser 的 import 中去寻找对应的入口文件
    conditionNames: ['browser', 'import'],
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js',
  },
};
复制代码

当然,你所使用的构建工具如果是 Rollup 也可以使用 @rollup/plugin-node-resolvemainFields 以及 exportConditions 配合 browser 选项来实现 webpack 的上述功能。

频繁业务迭代下的 Npm 包版本应该如何管理

关于 Npm Version 相关的信息,不太清楚的同学可以查阅春哥的这篇semver:语义版本号标准 npm的版本控制器。

这个章节其实主要想和大家交流一些关于频繁业务迭代下 Npm 包版本仅可能语义化自己的看法。

设想目前的业务场景下,多人在同步开发一款 NPM 包。换句话说,假设我们有一款 NPM 包 latest 版本为 1.0.0。

此时,产品需要迭代需求 A,此时小王同学进行负责本次需求的迭代。开发完毕后发布 1.0.0-alpha.0 进行测试。

同时,产品又需要同步迭代需求 B,同一个项目也许有多个需求在同步迭代(我相信这在日常业务中绝不少见)。

OK,此时小张同学同时基于 latest 版本的包进行开发新功能,开发完毕后发布 1.0.0-alpha.1 。

此时,由于两位同学发布的 alpha 版本的包存在的一些 Bug ,所以就会造成以下现象:

  • 1.0.0-alpha.0: A 功能相关。
  • 1.0.0-alpha.1: B 功能相关。
  • 1.0.0-alpha.2: A 功能相关。
  • 1.0.0-alpha.3: B 功能相关。
  • ...

最终,我们在 Npm 上的版本号虽然是遵从了 semver 规范,但是通过频繁迭代的版本号完全无法关联相应版本的单一功能。

当然规避这个问题最佳途径是通过合理的产品规划进度以及相关关联版本生成 CHANGELOG 从而进行固定周期的包版本正常迭代。

但是在频繁业务迭代的背景下,这个也许对于团队来说是一种趋于理想化的状态。

那么面对上述这种情况,我们希望尽可能的在发布 NPM 包时,对于相应每个独立的功能可以拥有单独的版本迭代

为了解决上述的问题,并且达到相对独立的需求。我们设计了这样一种思路:

在我们进行开发一个新的独立功能时,往往都是会基于一个新的 feat/xxx-x 的独立分支来进行开发。

比如,此时我需要为我的 NPM 包新增一个多账号相关需求,那么我会基于现有的稳定分支派生出一个名为 feat/multi-account 的分支。

只要分支命名合理的话,通过分支名我们可以对于该分支实现的迭代功能一目了然对吧。

那么,我们能否将分支名和对应的 NPM 包版本进行关联呢,如果将分支和 NPM 包版本进行了关联,其实就很容易实现我们上述的需求:确定单一功能迭代下的包版本号语义化。

简单来说,它的步骤是这样的:

  • 当开发人员接到该包需要添加一个新的需求时,建立分支 `feat/multi-account 分支。
  • 开发人员在功能分支完成开发后,首次发布本次功能相关 NPM 包提交测试。
    • 发布时,首先会根据脚本读取当前分支名称:feat/multi-account
    • 将读取到的分支名 feat/multi-account 替换为 multi.account
    • 去远程 NPM 地址查找是否已经存在该包关联的 dist-tag,假设发布的包名为 vue。那么就相当于执行 npm view vue@multi.account version
      • 如果存在该 tag 相关版本,那么即表示已经进行过发布该功能相关的 tag 版本,此时仅仅需要根据远程版本号进行叠加即可。比如远程为 1.0.0-multi.account.0 此时本地即会生成 1.0.0-multi.account.1
      • 如果不存在该 tag 相关版本,那么表示该分支相关的功能是首次进行发布。那么首先会拉取远程最新的 latest 稳定版版本(假如稳定版为 1.0.0),之后根据稳定版版本会新建相关 dist-tag 进行发布,相当于会发布 1.0.0-multi.account.0

关于 Npm dist-tag 的相关内容,不太了解的同学可以查阅这里。

本质上 dist-tag 你可以将它理解成为 git tag 类似,通常我们来用它来组织和标记和正式版不同版本的包。比如 vue 中

可以看到 vue 中除了 latest 正式版本,同样也存在 beta、legacy、csp 等等自定义的 dist-tag 相关版本包。

我们回归正文,通过上述的步骤其实已经可以将相关版本迭代和 dist-tag 进行强关联了。

比如我们开发的多账号功能,关于多账号功能的迭代版本:

代码语言:javascript复制
1.0.0-multi.account.0
1.0.0-multi.account.1
1.0.0-multi.account.2
// ...
复制代码

同时,如果有别的同学在同步开发一款收集用户行为的需求的话,其实也可以做到完全独立的语义化版本:

代码语言:javascript复制
1.0.0-collect.user.0
1.0.0-collect.user.1
1.0.0-collect.user.2
// ...
复制代码

上述随着业务迭代需求的增加的话不可避免的会带来存在很多个 tag 的包。这个问题其实解决起来也比较简单。

无论是我们通过 CI 关联分支删除即调用命令删除远程 tag 还是手动的方式,只要在相关 tag 测试稳定后,合并进入正式代码时删除掉对应的 dist-tag 即可解决 tag 数繁多的问题。

当然,本质上通过合理的产品迭代流程和计划完全是不存在上述的问题。上边的思路也只是针对于频繁业务迭代背景下的一个临时 Hack 方案。

这个方案中也许仍存在不少值得深思的优化点,也欢迎大家在评论区互相交流各自的看法。

写在结尾

文章的内容到这里就要画上句话了,感谢每一位可以看到结尾的小伙伴。

希望大家可以从文章中的内容有所收获,当然也欢迎每一位小伙伴在评论区留下自己的见解我们互相讨论。

0 人点赞