前言
在前端工程化里面我认为比较重要,也是不太起眼的事就是如何统筹好团队开发习惯。当项目中各个成员对项目开发认知程度不一时。
三流的技术管理者会甩锅成员技术不到位,对技术栈理解不深刻等,一个项目都 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
目前还是没有办法。期待有同学研究完可以有更好的办法!
参与感想
我觉得看源码不难,一方面 若川 都会有自己的一篇文章给大家铺路,很容易就上手了。即使不懂的也可以直接问。所以我认为参与这样的活动,最主要在于如何写笔记总结好自己的所见、所得、所想。才是最关键的!这种学习方式可以加深自己的表达方式和知识学习,对于文章的解读和理解是否消化到位了。最后感谢 若川 发起的活动,学习氛围很好,值得推广!
若有收获,就点个赞吧