一行代码统一规范 包管理器

560 阅读5分钟

前言

在前端工程化里面我认为比较重要,也是不太起眼的事就是如何统筹好团队开发习惯。当项目中各个成员对项目开发认知程度不一时。

三流的技术管理者会甩锅成员技术不到位,对技术栈理解不深刻等,一个项目都 run 不起来,导致项目拖后腿。而一流的技术 Leader 往往会站在团队约束视角,寻找统一化、规范化的方向上去找问题,从而今后避免类似错误。

将开发规则喜好等借助工具定死,往往是解决类似问题的良药。今天首次参加 若川源码共读活动 所选的一个主题,川哥建议我的任务,就是跟此有关。那就来说说今天的主角:only-allow

什么是only-allow?

Force a specific package manager to be used on a project(强制在项目上使用特定的包管理器 )

既然它是工程化里面的内容,那必然大概率是个 npm 包,所以我们先去 npm 官网搜一下(凡事先看项目首页是个好习惯)www.npmjs.com/package/onl…

第一句话结束里已经讲的很明白了。强制在项目里使用某个特定的包管理器?为什么要这么做呢?我们都知道,现在的包管理器,可以说是五花八门,随口一说可能就好几个:npm/cnpm/yarn/pnpm等,按照喜欢先创造问题、解决问题的死循环习惯来看,以后可能会出现更多其他的管理器(习惯就好,无奈摊手)。

对于我们维护工程的同学来说,显然个人无所谓。但是团队而言就是灾难性的。如果不同人使用的管理器不一样,那么就可能出现

A 同学:为啥你安装成功,我依赖没成功?

B 同学:这个项目别用npm,你试试 yarn add (心里:烦死了,每个人都来问一遍,xxx 都讲了八百遍了)。

显然这种基础问题问多了,容易极大伤害了团队成员之间的感情。

使用和了解

{
  "scripts": {
    "preinstall": "npx only-allow npm"
  }
}

只需要这样,就可以限制这个项目只能使用 npm 来安装依赖了。使用起来非常简单,不需要多讲啥。我们重点看原理!

根据 若川源码共读 引文里所提供的指引,我们克隆下来项目:git clone https://github.com/lxchuan12/only-allow-analysis.git

调试

这里的收获是从 若川源码共读 引文中里了解到 vscode 居然还提供了一个 auto debugger 的功能:只要在 vscode 里面跑的 scripts 都可以打完断点自动断下来,而无需再手动配置 .vscode 里面的内容了。可以说是一个非常方便的功能了,关于调试不多讲,因为有讲的比我更好的文章:《新手向:前端程序员必学基本技能——调试JS代码》

源码

在看源码前,完全没有想到实现起来如此简单。这也暴露了我很少关注 npm 文档以及一些 node 工程化中常见功能的短板。我们先来介绍些前置知识

preinstall

官网介绍地址:docs.npmjs.com/cli/v8/usin…

当执行 scripts 脚本的时候,并不是只执行了 install 操作,而是有一个完整生命周期,会调用钩子脚本。我们可以看到文档里说道:npm install 的时候 就会执行以下声明周期钩子。

prexxx 和 postxxx 是前置钩子和后置钩子,当执行某个命令的时候。这两个命令会在之前和之后分别执行。

{
  "scripts": {
    "precompress": "{{ executes BEFORE the `compress` script }}",
    "compress": "{{ run command to compress files }}",
    "postcompress": "{{ executes AFTER `compress` script }}"
  }
}

process

process.argv可以获得npm运行时 scripts 里面的完整参数,也可以理解为命令行启动这个脚本时候的代码。例如 node bin.js pnpm那么就会形成一个数组: ['node', 'bin.js', 'pnpm'] 那么我们就知道了,此时用户希望限制使用哪个工具包?当然是数组里的第 2 项。

依赖包介绍

which-pm-runs

Detects what package manager executes the process(检测哪个包管理器执行进程 )

npm包地址:www.npmjs.com/package/whi…

boxen

Create boxes in the terminal(没必要翻译了吧.......)

npm包地址:www.npmjs.com/package/box…

源码解读

先来画一下大概的流程:

直接上完整源码:

#!/usr/bin/env node
const whichPMRuns = require('which-pm-runs')
const boxen = require('boxen')
const argv = process.argv.slice(2)// package.json 里面的 "scripts": {"preinstall": "node bin.js pnpm"} 的第3个参数就是 这里就是pnpm
if (argv.length === 0) {
  console.log('Please specify the wanted package manager: only-allow <npm|pnpm|yarn>')
  process.exit(1)
}
const wantedPM = argv[0]//根据前面获取到的参数,这里取出我们希望的工具包
if (wantedPM !== 'npm' && wantedPM !== 'pnpm' && wantedPM !== 'yarn') {//判断支持的是哪些?
  console.log(`"${wantedPM}" is not a valid package manager. Available package managers are: npm, pnpm, or yarn.`)
  process.exit(1)
}
const usedPM = whichPMRuns()//获取当前命令行使用的工具名称和版本
if (usedPM && usedPM.name !== wantedPM) {//好了,这里就很好理解了,一旦不一样,咱们就得来搞点事情嘛,告诉用户这个行不通。也是我们主要的提示逻辑了。
  const boxenOpts = { borderColor: 'red', borderStyle: 'double', padding: 1 }
  switch (wantedPM) {
    case 'npm':
      console.log(boxen('Use "npm install" for installation in this project', boxenOpts))
      break
    case 'pnpm':
      console.log(boxen(`Use "pnpm install" for installation in this project.

If you don't have pnpm, install it via "npm i -g pnpm".
For more details, go to https://pnpm.js.org/`, boxenOpts))
      break
    case 'yarn':
      console.log(boxen(`Use "yarn" for installation in this project.

If you don't have Yarn, install it via "npm i -g yarn".
For more details, go to https://yarnpkg.com/`, boxenOpts))
      break
  }
  process.exit(1)
}

思考与改进

这段代码并不难,但是当中的 swicth case 看着却让人有点难受(并不是说这样不好,在这样 mini 的工程里没有关系,我并不希望把精力花在跟大家辩论上)我试着想去做个优化,对代码未来的扩展性能够提升一下,也算是锻炼下自己能力吧。

策略模式

建立映射

当看到一堆判断冗余的时候,我脑海当中第一想到的就是《设计模式》当中的“策略模式”。于是对代码进行提炼,所有的判断都归到了一个对象里,而对象由函数包裹,今后大家就在这里按照 key,value 的方式取值即可。

let registryObj = {
  npm: 'Use "npm install" for installation in this project',
  pnpm: `Use "pnpm install" for installation in this project.

        If you don't have pnpm, install it via "npm i -g pnpm".
        For more details, go to https://pnpm.js.org/`,
  yarn: `Use "yarn" for installation in this project.

        If you don't have Yarn, install it via "npm i -g yarn".
        For more details, go to https://yarnpkg.com/`
};

瞧,通过这种方式,我们很轻松的可以取自己想要的。但是它不够灵活和安全,容易被肆意的修改(有经验的同学从这句话里,大概就能听出“闭包”两个词了~).

解决方法(有问题)

接下来,我又“想当然”的做了一个优化,通过高阶函数,扩展了一下方法。但是这里我犯了一个低级错误,大家帮我来找找问题:

function createRegistry(fn) {
  let registryObj = {
    npm: 'Use "npm install" for installation in this project',
    pnpm: `Use "pnpm install" for installation in this project.

        If you don't have pnpm, install it via "npm i -g pnpm".
        For more details, go to https://pnpm.js.org/`,
    yarn: `Use "yarn" for installation in this project.

        If you don't have Yarn, install it via "npm i -g yarn".
        For more details, go to https://yarnpkg.com/`
  };
  return (...args) => {
    return fn(registryObj, ...args)
  };
}

let getRegistry = createRegistry((registryObj, type) => registryObj[type]);
let hasRegistry = createRegistry((registryObj, PM) => Object.keys(registryObj).includes(PM));
let addRegistry = createRegistry((registryObj, key, value) => registryObj[key] = value);
let deleteRegistry = create((registryObj, key, value) => delete registryObj[key]);
addRegistry('pop','value');
console.log(getRegistry('pop'))

扩展方法

好了,揭晓答案:

由于createRegistry 每次都在生成一个新的 registryObj,赋给闭包,导致了 getRegistry、hasRegistry、addRegistry、deleteRegistry四个方法都“各自为政”,每个人身上都有一份自己的 registryObj,所以当我们要对它们进行增删改查,会发现不起作用。因为它们各改各的,谁也不搭理谁。

解决这个问题的办法也很简单,那就是只让 createRegistry 函数创建一次,然后所有的新函数都基于它去执行。代码如下:

function memoRegistry() {
  let registryObj = {
    npm: 'Use "npm install" for installation in this project',
    pnpm: `Use "pnpm install" for installation in this project.

        If you don't have pnpm, install it via "npm i -g pnpm".
        For more details, go to https://pnpm.js.org/`,
    yarn: `Use "yarn" for installation in this project.

        If you don't have Yarn, install it via "npm i -g yarn".
        For more details, go to https://yarnpkg.com/`
  };
  return (fn) => {
    // debugger
    return fn.bind(null,registryObj)//所有进来的函数都绑定一下闭包内记忆的 registryObj。
  };
}
let createRegistry = memoRegistry()//在这里只创建一次,记住 registryObj 一个就好。
let getRegistry = createRegistry((registryObj, type) => registryObj[type]);
let hasRegistry = createRegistry((registryObj, PM) => Object.keys(registryObj).includes(PM));
let addRegistry = createRegistry((registryObj, key, value) => registryObj[key] = value);
let deleteRegistry = createRegistry((registryObj, key, value) => delete registryObj[key]);
addRegistry('pop','value');
console.log(getRegistry('pop'))

源码改进

最终,我们将代码加入到源码里,改写如下:

#!/usr/bin/env node
const whichPMRuns = require('which-pm-runs');
const boxen = require('boxen');

const argv = process.argv.slice(2)// package.json 里面的 "scripts": {"preinstall": "node bin.js pnpm"} 的第3个参数就是 这里就是pnpm
if (argv.length === 0) {
  console.log('Please specify the wanted package manager: only-allow <npm|pnpm|yarn>')
  process.exit(1)
}
const wantedPM = argv[0]//根据前面获取到的参数,这里取出我们希望的工具包
if (hasRegistry(wantedPM)) {
  console.log(`"${wantedPM}" is not a valid package manager. Available package managers are: npm, pnpm, or yarn.`)
  process.exit(1)
}
const usedPM = whichPMRuns()//获取当前命令行使用的工具名称和版本

if (usedPM && usedPM.name !== wantedPM) {//好了,这里就很好理解了,一旦不一样,咱们就得来搞点事情嘛,告诉用户这个行不通。也是我们主要的提示逻辑了。
  const boxenOpts = { borderColor: 'red', borderStyle: 'double', padding: 1 }
  console.log(boxen(getRegistry(wantedPM), boxenOpts))
  process.exit(1)
}

可以看到,代码清爽了很多!

遗留与总结

问题

在调试源码的时候,发现 npm 有时候不会被 preinstall 拦截,查了最新的 npm 文档以后,发现里面确实是支持的,也就是说这个很有可能是个 npm 新版的 bug!

在翻看文档的时候:docs.npmjs.com/cli/v8/comm…

这里面提到了一个 npm 的配置,当这个配置打开以后,可以发现我们的:npm install 是生效的。可以触发preinstall 钩子。但是对于 npm install xxxx目前还是没有办法。期待有同学研究完可以有更好的办法!

参与感想

我觉得看源码不难,一方面 若川 都会有自己的一篇文章给大家铺路,很容易就上手了。即使不懂的也可以直接问。所以我认为参与这样的活动,最主要在于如何写笔记总结好自己的所见、所得、所想。才是最关键的!这种学习方式可以加深自己的表达方式和知识学习,对于文章的解读和理解是否消化到位了。最后感谢 若川 发起的活动,学习氛围很好,值得推广!

若有收获,就点个赞吧