乐趣区

关于javascript:使用-NodeJS-开发一个命令行工具批量管理多项目依赖

概述

大家好,本篇文章的内容次要分为两局部:

  1. 开发 multi-dependent-management 工具库,解决在业务上遇到的问题
  2. 对于开发这个工具库时的一些总结

multi-dependent-management 是一个基于 NodeJS 开发的,在命令行中应用的工具库,次要用于批量治理基于 Npm 的 package.json 我的项目依赖。它能够批量对你的我的项目进行依赖降级、移除、查看差别、执行 shell 命令等操作。

首先咱们先来介绍下为什么要开发这样一个工具。

背景

我当初的公司,前端开发只有 3 人,但外部应用的管理系统和 H5 就有 27 个了(大部分都是保护状态),而这些前端利用,都是基于一套组件库去开发的:

现有的 Npm 包会有多个,当咱们呈现 Bug 或有新性能迭代时,要同时更新多个零碎并对立公布上线(因为是对内应用的,而且技术治理比拟松,测试没问题就能够在某个时间段上线),这时候就有上面的流程:

不需更新业务代码,只更新依赖版本,就是下面的流程。

如果同时有多个零碎要更新,这里的操作就很麻烦。有一次因为用户模块出了问题,所有应用该模块的零碎(二十多个)都要更新,那时候十分苦楚。

因为我的项目保护都集中在一两个人,所以我的项目都会在咱们电脑本地。这时候我就想,有没有工具能够批量更新多个我的项目的依赖,而后间接 git commit 提交到 gitlab 上?(公共模块要迭代,比方侧边栏、导航栏、页面初始化等操作,都曾经封装好了,所以大部分时候咱们的组件库更新都是只需更新版本号)

这种操作,有点像 npkill,它会扫描的指标门路,让你抉择对应含有 node_modules 的文件夹,进行抉择删除。

还有 npm-check-updates 能够帮你进行依赖查看并更新的操作。

我本人搜寻了下,没找到能够集成下面我所说的内容的工具库,所以就打算本人搞一个,性能如下:

  1. 批量依赖降级
  2. 批量依赖移除
  3. 批量依赖变更
  4. 批量执行 shell 命令
  5. 查看我的项目依赖版本差别

这样就能够解决我下面所说的需要,通过执行命令,帮我把反复相似的工作解决掉。

应用

具体的应用教程在 github 仓库 有具体阐明了,这里次要分享下如何疾速应用,并应用该工具疾速解决下面的问题。

这个工具库是用 NodeJS 进行开发的,在命令行执行操作,和咱们平时应用的一些命令相似,比方:vue create test

整个工具库的操作流程,根本如下:

依照下面背景所说的需要,咱们须要通过命令行批量更新依赖版本,并提交到 gitlab。

首先装置工具库:

# 全局装置
npm i multi-dependent-management -g

解决形式一

假如我要批改的我的项目都在 ./demo 上面。

首先进行依赖更新:

# 全局装置完依赖后,应用 mdm 简写去应用
mdm upgrade -p ./demo
  • mdm 就是 multi-dependent-management 这个库的简写
  • upgrade 是这个库能够触发的动作
  • -p ./demo 是一个参数,通知这个工具库要从哪个门路进行查问

首先会递归查问该门路下所有的 package.json 文件,而后应用 npm-check-updates 查看每个我的项目的依赖版本是否最新,将能够更新的依赖一一展示进去,让你抉择哪个依赖须要更新:

当咱们抉择要更新的依赖后,就会通过 fs 间接批改文件的版本号,而不会装置依赖。

接着就要把批改记录提交到 gitlab,这时候用到的是 shell 命令:

mdm shell -p ./project

会依据你选中的我的项目,执行相干的脚本命令,该性能自由度比拟高,能够搭配不同的操作。

选完要解决的我的项目后,须要先输出独特执行的命令,没有的话,不输出保留就行了:

咱们这里输出了 git 提交的命令。

二次确认后,执行后果:

胜利将多个我的项目提交到 gitlab

解决形式二

形式一别离用了两个命令去操作:upgrade + shell。但其实咱们能够间接 shell 一次性实现。

同样假如咱们的我的项目在 ./demo 下,当初须要更新 vue 的版本,并提交到 gitlab

mdm shell -p ./demo

将依赖降级的命令,也放到 shell 去操作:

装置完依赖后,接着就提交代码到 gitlab:

总结

下面两种形式都能够解决我在“背景”所说的问题,具体应用哪种,看你的需要,应用 upgrade 命令,会主动帮你查找每个依赖能够降级的版本,而 shell 是纯手动模式,让你齐全管制要降级的依赖版本。

除去这两个性能,multi-dependent-management 工具库还有其余的性能,具体的应用大家能够去 github 或者 npm 查看。

对于开发

技术栈

该工具的开发,应用的技术栈:

  1. 无关命令行操作的工具

    1. commander
    2. enquirer(命令行交互)
    3. ora
    4. shelljs
    5. npm-check-updates(查看依赖版本是否须要降级)
  2. 单元测试

    1. jest
    2. memfs(应用内存模仿 fs)
  3. 工具库

    1. lodash
    2. just-diff
    3. semver
  4. 其余

    1. typescript
    2. commitlint
    3. husky
    4. lint-staged
    5. standard-version
    6. eslint

整个开发,就是下面所展现的库,像单测、工具库、husky、commitlint 这些都是很罕用的,这里就不一一开展。

开发工作流

应用 eslint 标准代码款式,jest 做单元测试,husky + lint-staged + git hook 进行相干命令操作。

下图就是我在开发这个工具库时,执行的流程:

上面咱们从零开始,实现下面的工作流程配置,如果嫌麻烦,我这里曾经依照上面的流程,配好了一个现成的模板。

筹备工作

整个配置,大略须要 10 – 15 分钟左右。

咱们首先要建一个我的项目,应用 typescript 进行开发。

mkdir test && cd test # 新建文件

npm init # npm 初始化

git init # 初始化 git

mkdir lib && mkdir tests # 增加文件夹

npm i typescript -S

# 增加疏忽文件:node_modules coverage dist
vim .gitignore

增加文件:

vim lib/a.ts

export function getName() {return 'ok'}
export function getData() {
  return {name: getName()
  }
}

vim lib/index.ts

import {getData} from './a'

console.log(getData())

因为咱们用的是 typescript,所以须要先编译能力用 node.js 执行

package.json 增加脚本命令:

{
  "scripts": {
    "tsc": "tsc",
    "start": "npm run tsc && node ./dist/index.js"
  }
}

tsc 命令是用来编译 .ts 文件,变为 .js。而后应用 node 执行相干文件。

增加 tsconfig.json,通知 typscript 要如何进行编译。

vim tsconfig.json

{
  "compilerOptions": {
    "module": "commonjs",
    "noImplicitAny": true,
    "removeComments": true,
    "preserveConstEnums": true,
    "outDir": "./dist",
    "declaration": true,
  },
}

这里次要说下 outDirdeclaration。当你执行 tsc 时,会将转译文件放到指定目录,而 declaration 会生成 .d.ts 文件。

配置实现后,咱们执行下命令:npm start,会有上面的日志显示:

npm start

> test-ddd@1.0.0 start ~/Downloads/test-ddd
> npm run tsc && node ./dist/index.js


> test-ddd@1.0.0 tsc ~/Downloads/test-ddd
> tsc

{name: 'ok'}

看到日志胜利打印,咱们的筹备工作实现了,目录构造是这样的:

.
├── lib
│   ├── a.ts
│   └── index.ts
├── package-lock.json
├── package.json
├── tests
└── tsconfig.json

上面就开始配置开发工作流。

配置 husky

husky 是按官网教程来的,这里用的版本是 7.x,要留神版本号,很多以前的教程是在 package.json 配置,那个是要用 4.x 版本才行。

# 先保障以后我的项目有 .git 文件
# 初始化并装置
npx husky-init && npm install
# 这时候,我的项目根目录会生成一个 .husky 文件,外面蕴含了一个钩子文件:pre-commit

批改 .husky/pre-commit 文件,将外面的 npm test 改为 npm run lint-staged,前面会用到。

配置 lint-staged

npm i lint-staged -D

package.json 增加相干配置:

{
  "scripts": {
    "lint-staged": "lint-staged",
    "lint": "eslint --fix lib/**",
    "test:unit": "jest"
  },
  "lint-staged": {"{lib,tests}/**/*": [
      "npm run lint",
      "npm run test:unit",
      "git add"
    ]
  },
}

这里咱们咱们配置了 lint-staged 和 3 个脚本命令。linttest:unit 是执行 eslintjest 用的,上面咱们持续配置这两个工具。

配置 eslint

装置:

npm i eslint -D
# 初始化
./node_modules/.bin/eslint --init
# 按着批示进行配置即可
# 这是我抉择的配置:✔ How would you like to use ESLint? · style
✔ What type of modules does your project use? · esm
✔ Which framework does your project use? · none
✔ Does your project use TypeScript? · No / Yes
✔ Where does your code run? · browser, node
✔ How would you like to define a style for your project? · guide
✔ Which style guide do you want to follow? · standard
✔ What format do you want your config file to be in? · JavaScript
Checking peerDependencies of eslint-config-standard@latest
The config that you've selected requires the following dependencies:

@typescript-eslint/eslint-plugin@latest eslint-config-standard@latest eslint@^7.12.1 eslint-plugin-import@^2.22.1 eslint-plugin-node@^11.1.0 eslint-plugin-promise@^4.2.1 || ^5.0.0 @typescript-eslint/parser@latest
✔ Would you like to install them now with npm? · No / Yes

增加 .eslintignore 疏忽不必要的文件,vim .eslintignore

package.json
package-lock.json

配置实现后,咱们看下相干命令: "lint": "eslint --fix lib/**”

这里是指定要 fix 的文件门路,依据你的我的项目进行相干变动即可。

最初能够试下执行 npm run lint 看看是否胜利。

配置 jest

npm i jest -D
# 初始化配置
./node_modules/.bin/jest --init
# 依照你本身的需要进行配置即可

配置 babeltypescript

npm i babel-jest @babel/core @babel/preset-env @babel/preset-typescript ts-node @types/jest @types/node -D

批改 tsconfig.json

{
  ...,
  // 增加:"compilerOptions": {
    ...,
    "types": [
      "jest",
      "node"
    ]
  }
}

增加 babel.config.js 文件:

// babel.config.js
module.exports = {
  presets: [['@babel/preset-env', {targets: {node: 'current'}}],
    '@babel/preset-typescript',
  ],
};

咱们增加一个测试文件,验证下是否胜利:

vim tests/a.spec.ts

import {getName} from '../lib/a'

describe('测试 getName', () => {test('执行 getName,返回字符串"ok" ', () => {expect(getName()).toBe('ok')
  })
})

批改在 package.jsonlint 命令:"lint": "eslint --fix lib/** tests/**”,增加对 tests 文件的查看。

咱们执行之前增加的脚本命令:npm run test:unit

npm run test:unit

> test-ddd@1.0.0 test:unit ~/Downloads/test-ddd
> jest

 PASS  tests/a.spec.ts
  测试 getName
    ✓ 执行 getName,返回字符串 "ok"  (2 ms)

----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files |      50 |      100 |      50 |      50 |
 a.ts     |      50 |      100 |      50 |      50 | 5
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.914 s
Ran all test suites.

看到执行胜利了,jest 的配置也实现了。

试验

筹备工作都筹备好,咱们来提交下代码试试:

git add ./
git commit -m 'test'

这时候会看到触发钩子,使 lint-staged 开始工作:

lint-staged

⚠ Skipping backup because there’s no initial commit yet.

⚠ Some of your tasks use `git add` command. Please remove it from the config since all modifications made by tasks will be automatically added to the git commit index.

✔ Preparing...
⚠ Running tasks...
  ❯ Running tasks for {lib,tests}/**/*
    ✖ npm run lint [FAILED]
    ◼ npm run test:unit
    ◼ git add
✔ Applying modifications...

✖ npm run lint:

...

~/Downloads/test-ddd/tests/a.spec.ts
  3:1  error  'describe' is not defined  no-undef
  4:3  error  'test' is not defined      no-undef
  5:5  error  'expect' is not defined    no-undef

有个文件的代码格局没通过,所以整个 commit 操作被拦挡下来,无奈胜利 commit。同理,如果 lint 命令通过,但 test:unit 命令没通过,也是会被拦挡下来。

咱们来修复下这个问题:

./eslintrc.js 增加上面的配置:

module.exports = {
  env: {jest: true,},
}

再次 commit 后,提交胜利,eslint 和单测都胜利。

git commit -m 'test'

> test-ddd@1.0.0 lint-staged ~/Downloads/test-ddd
> lint-staged

⚠ Skipping backup because there’s no initial commit yet.

⚠ Some of your tasks use `git add` command. Please remove it from the config since all modifications made by tasks will be automatically added to the git commit index.

✔ Preparing...
✔ Running tasks...
✔ Applying modifications...
[master (root-commit) 658c2dd] test
 12 files changed, 6339 insertions(+)
 .....

到这里,曾经实现咱们的配置。整个工程配置,能够当成一个工具库模板,前面有新的工具开发,间接应用该模板,疾速搭建根底性能。

单元测试

这个工具库我是有写单元测试,因为写得不是很多,只能依据覆盖率去写,哪里没有笼罩到,就补用例,一些重点的环节,就尽量测试不同的状况。

整个单测过程,我想总结下两个点:

  1. 应用 memfs 这个库是 mock fs
  2. 应用 JestspyOn 办法,mock 模块内的某个函数

这两个 Mock,是我在写单测中,常常要用到的。

应用 memfs mock fs

因为 fs 是属于 io 操作,而且工具办法波及到对文件的操作,如果不 mock fs,须要写一些重置办法,重置用于单测的文件。

或者还能够间接 mock 应用了 fs 的办法,但这个我感觉十分麻烦,因为有大量的测试用例须要用到,并且用到的场景有些会不同,所以这个办法我也没采纳。

最初我是看到这篇文章 Testing filesystem in Node.js: The easy way 后,晓得了 memfs 这个库,应用内存模式去模仿 fs。集体体验十分好,只需简略的配置,就能够解决 fs mock 问题,并且还能自定义文件目录和内容。

首先增加文件:

tests/__mocks__/fs.ts

文件内容:

import {fs} from 'memfs';

export default fs;

应用:

const packageJson = {...}
describe('test', () => {beforeEach(() => {
    // 每次执行用例钱,重置内容
    vol.reset();
    // 设置门路、目录和相干文件的内容
    vol.fromNestedJSON({
      p1: {'package.json': JSON.stringify(packageJson),
      },
      p2: {'package.json': JSON.stringify(packageJson),
      },
    }, '/abc');
  });

  describe('test...', () => {it('获取内容', async () => {
      // 应用 fs(曾经 mock 解决了)获取对应门路的文件内容
      const data = JSON.parse(fs.readFileSync('/abc/p1/package.json', { encoding: 'utf-8'}));
      // 判断获取的文件内容是否和开始配置的数据统一
      expect(data).toBe(packageJson); // pass
    });
  });

能够看到配置过程非常简单,而应用成果和 fs 没什么区别。

Mock 模块内的某个函数

咱们看下要测试的这个办法:

// 伪代码
import {getConfirmPrompt,} from './utils';
import * as upgradeUtils from './upgrade';

export async function upgrade(paths: string[]): Promise<void> {await upgradeUtils.getMultiSelectProject(paths);
  await.getConfirmPrompt().run();
}

这里只展现了关键点,咱们要 mock 下面的两个函数:

  • getConfirmPrompt 函数是另一个文件引入的
  • getMultiSelectProject 函数是同一个文件的

要 mock getConfirmPrompt 函数很简略,间接应用 spyOn 就行了:

// 伪代码
import * as utils from '../lib/utils';

describe('test', () => {test('upgrade.js', () => {jest.spyOn(utils, 'getConfirmPrompt').mockImplementation(() => ([{ ...}, {...}
    ]));
  })
})

另一个要 mock 的函数是 getMultiSelectProject,它是和 upgrade 办法在同一个文件,这里的解决办法有点绕。

首先在 该函数 的文件,增加这样一行代码:

import * as upgradeUtils from './upgrade';

须要 mock 的函数,要这样调用:

upgradeUtils.getMultiSelectProject()

接着在测试文件,同样也是要先引入:

// 伪代码
import * as upgradeUtils from '../lib/upgrade';

describe('test', () => {test('upgrade.js', () => {
    // 应用 jest.spyOn 去 mock 函数
    // 首先传入该函数的模块,第二个参数是你要 mock 的办法名
    // 再应用 mockImplementation 返回你要 mock 的值
    jest.spyOn(upgradeUtils, 'getMultiSelectProject').mockImplementation(() => ([{ ...}, {...}
    ]));
  })
})

再应用 spyOnmockImplementationgetMultiSelectProject 函数 mock 就行了。

这里我还没搞懂为什么要这样解决,前面再演绎下不同状况的 mock 形式。

总结

这次分享的内容,次要是如何应用 multi-dependent-management 这工具去解决在开发遇到的问题,并总结在开发这个工具时的性能。尽管平时有做一些小工具的开发,利用到工作上,但都很少进行这样的总结,所以这次尝试下,锤炼本人的总结能力和表达能力。

以上就是本文章的全部内容了,如果有不正确的中央,感激斧正~

画图工具:miro

录屏工具:kap

multi-dependent-management 工具仓库地址

参考链接

  1. Testing filesystem in Node.js: The easy way
  2. typescript 疾速开始
  3. husky 文档
  4. eslint
  5. jest
退出移动版