本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!
大家好,我是一只不守妇道的花喵。
本文你可以学到:
- 熟悉
cli
开发的全流程,可以开发出类似@babel/core
、@babel/preset-env
的npm包(是不是很装逼) - 熟悉
npm
发布流程 - 熟悉
lerna
开发cli
工具,快速入门lerna
- 了解架构师如何完整设计、开发一个脚手架
- 如何利用脚手架高效解决日常开发中的CI/CD流程
注:本文老衲在mac下测试,使用win的同学目录自己改下哈
1. 准备
为什么要开发脚手架(像个架构师一样思考)
这就需要说到CI/CD
:
工厂里的装配线以快速、自动化、可重复的方式从原材料生产出消费品。同样,软件交付管道以快速、自动化和可重复的方式从源代码生成发布版本。如何完成这项工作的总体设计称为“持续交付”(CD)。启动装配线的过程称为“持续集成”(CI)。确保质量的过程称为“持续测试”,将最终产品提供给用户的过程称为“持续部署”。一些专家让这一切简单、顺畅、高效地运行,这些人被称为运维开发(DevOps)践行者。[1]
解决的问题:
传统(事实上大一点点的公司都会考虑CI/CD自动化)手动操作低效、出错率高、依赖某些特定人员等。尤其在项目越来越大的时候,这些问题将更为严重,更不用谈“灰度发布”,这些点有一个就有足够的理由接着看本文。作为前端架构师就是要解决各种“效能”问题。
解决的场景:
- 统一CI/CD规范
- 提高CI/CD效率
- 减少CI/CD出错率
- 不依赖任何人,按流程走
- ……
前置知识
- 熟悉
js
,了解nodejs
- 了解过一种或几种命令行工具,如:
vue-cli
、vite
、create-react-app
、git
等 - 了解
npmjs
注册、登录、发布等流程(下面有最少知识教程)
需求
- 分包,一个脚手架如果较为复杂就必然涉及分包,分包可以有效降低工程复杂度、提高开发/维护效率、并且每个分包相对独立,可以单独使用,
babel
就有将近150的包,点击查看,互相依赖,想象下你怎么管理这么多包?每次更新版本、安装依赖怎么操作?分包的依赖升级又要怎么整? - 命令+子命令,日常用的
git
,vue-cli
都有子命令,脚手架如果足够复杂也会涉及子命令 - 日志
- 命令行交互
- 网络请求
- 本地文件处理
- ……
注:我们做一个项目的需求调研到技术选型,不会只考虑当下需要什么,都会再需求下“多考虑一步”,会考虑其扩展性,如上面的分包、命令+子命令。当然如果确实是一个很小的脚手架,是可以不考虑分包以降低设计的复杂度(当然熟悉了lerna
没啥复杂度),比如说只有一两个功能模块。但是如果像vue-cli
或babel
,涉及的模块/插件几十上百个,那就必须要考虑。作为预留,我们这边使用能分包(Multirepo
)的扩展方案。
Monorepo vs Multirepo
选型
- 分包,
lerna
,业界出名的Multirepo
方案解决方案 - 命令+子命令,
commander
、yargs
- 日志,
npmlog
、colors
- 命令行交互
- 网络请求
- 本地文件处理,
fs-extra
其他
- 版本比较:
semver
- 解析参数选项:
minimist
- ……
2. 开始
2.1.初始化
- 创建
mkdir ~/huamiao-cli_all
文件夹,后面有后端服务 - 创建
mkdir ~/huamiao-cli_all/huamiao-cli
- 切换目录:
cd ~/huamiao-cli_all/huamiao-cli
- 初始化为npm项目:
npm init -y
- 安装lerna:
npm i -D lerna
,为了使用方便(使用的时候不用使用npx,)全局也安装lerna:npm i -g lerna
- lerna初始化:
lerna init
(如果不想全局安装lerna,可以使用npx lerna init
),生成lerna.json
并修改版本为1.0.0
{
"packages": [
"packages/*"
],
"version": "1.0.0"
}
复制代码
lerna init
后会默认执行git init
,创建.gitignore
,忽略以下文件:
**/node_modules
.vscode
.DS_Store
lerna-debug.log
复制代码
- git暂存:
git add . && git status
git add . && git status
位于分支 master
尚无提交
要提交的变更:
(使用 "git rm --cached <文件>..." 以取消暂存)
新文件: .gitignore
新文件: lerna.json
新文件: package-lock.json
新文件: package.json
复制代码
- 提交到本地仓库:
git commit -m 'init'
注:npm官方源如果太慢,可以切换为淘宝源或使用cnpm/yarn
npm config set registry https://registry.npm.taobao.org/
复制代码
2.2.创建包&测试发布
- 创建
core
核心包,输入package name
,一路回车
特别注意:package name: (core) @huamiao-cli/core
lerna create core
package name: (core) @huamiao-cli/core
{
"name": "@huamiao-cli/core",
"version": "1.0.0",
"description": "> TODO: description",
"homepage": "",
"license": "ISC",
"main": "lib/core.js",
"directories": {
"lib": "lib",
"test": "__tests__"
},
"files": [
"lib"
],
"publishConfig": {
"registry": "https://registry.npm.taobao.org"
},
"scripts": {
"test": "echo \"Error: run tests from root\" && exit 1"
}
}
Is this OK? (yes)
lerna success create New package @huamiao-cli/core created at ./packages/core
复制代码
- 创建
utils
工具包,同上 npmjs
创建组织:@huamiao-cli
点击Create
点击Skip
现在可以在npmjs->个人头像->packages
查看到
- 发布,
lerna publish
,发布组织的包需要配置publishConfig
如果你用淘宝源,可能会出现下面的情况:
{
"name": "@huamiao-cli/core",
"version": "1.0.6",
"description": "> TODO: description",
"homepage": "",
"license": "ISC",
"main": "lib/core.js",
"directories": {
"lib": "lib",
"test": "__tests__"
},
"files": [
"lib"
],
"publishConfig": {
"registry": "https://registry.npm.taobao.org"
},
"scripts": {
"test": "echo 'run utils test'"
}
}
复制代码
看这边
"publishConfig": {
"registry": "https://registry.npm.taobao.org"
},
复制代码
packages
中两个包的package.json
都改为:
"publishConfig": {
"access": "public"
}
复制代码
发布前要git提交,并且要绑定远程仓库,否则:
lerna publish
info cli using local version of lerna
lerna notice cli v4.0.0
lerna info current version 1.0.0
lerna ERR! ENOREMOTEBRANCH Branch 'master' doesn't exist in remote 'origin'.
lerna ERR! ENOREMOTEBRANCH If this is a new branch, please make sure you push it to the remote first.
复制代码
我们在gitee创建一个公开库,这边不赘述了,有疑问留言。
配置远程仓库并推送:
git remote add origin https://gitee.com/xxx/huamiao-cli.git
git push -u origin master
复制代码
切换到npm官方源:
npm config set registry https://registry.npmjs.org
复制代码
注:如果这边不慎没切换到官方源,再提交一个git,再发布,版本累加即可发布
发布:lerna publish
选第一个先:
❯ Patch (1.0.1)
Minor (1.1.0)
Major (2.0.0)
Prepatch (1.0.1-alpha.0)
Preminor (1.1.0-alpha.0)
Premajor (2.0.0-alpha.0)
Custom Prerelease
Custom Version
复制代码
输入y
lerna publish
info cli using local version of lerna
lerna notice cli v4.0.0
lerna info current version 1.0.0
lerna info Assuming all packages changed
? Select a new version (currently 1.0.0) Patch (1.0.1)
Changes:
- @huamiao-cli/core: 1.0.0 => 1.0.1
- @huamiao-cli/utils: 1.0.0 => 1.0.1
? Are you sure you want to publish these packages? (ynH) y
>> Yes
复制代码
完整日志:
info cli using local version of lerna
lerna notice cli v4.0.0
lerna info current version 1.0.4
lerna info Looking for changed packages since v1.0.4
? Select a new version (currently 1.0.4) Patch (1.0.5)
Changes:
- @huamiao-cli-dev/core: 1.0.4 => 1.0.5
- @huamiao-cli-dev/utils: 1.0.4 => 1.0.5
? Are you sure you want to publish these packages? Yes
lerna info execute Skipping releases
lerna info git Pushing tags...
lerna info publish Publishing packages to npm...
lerna notice Skipping all user and access validation due to third-party registry
lerna notice Make sure you're authenticated properly ¯\_(ツ)_/¯
lerna WARN ENOLICENSE Packages @huamiao-cli-dev/core and @huamiao-cli-dev/utils are missing a license.
lerna WARN ENOLICENSE One way to fix this is to add a LICENSE.md file to the root of this repository.
lerna WARN ENOLICENSE See https://choosealicense.com for additional guidance.
lerna http fetch PUT 200 https://registry.npmjs.org/@huamiao-cli-dev%2futils 4431ms
lerna success published @huamiao-cli-dev/utils 1.0.5
lerna notice
lerna notice ? @huamiao-cli-dev/utils@1.0.5
lerna notice === Tarball Contents ===
lerna notice 73B lib/utils.js
lerna notice 487B package.json
lerna notice 108B README.md
lerna notice === Tarball Details ===
lerna notice name: @huamiao-cli-dev/utils
lerna notice version: 1.0.5
lerna notice filename: huamiao-cli-dev-utils-1.0.5.tgz
lerna notice package size: 565 B
lerna notice unpacked size: 668 B
lerna notice shasum: 9af0a5740f88b0a6829ea4a90c7107435d0f5489
lerna notice integrity: sha512-8kKZVoH1sF0WB[...]1aTA3UQiBl70g==
lerna notice total files: 3
lerna notice
lerna http fetch PUT 200 https://registry.npmjs.org/@huamiao-cli-dev%2fcore 7474ms
lerna success published @huamiao-cli-dev/core 1.0.5
lerna notice
lerna notice ? @huamiao-cli-dev/core@1.0.5
lerna notice === Tarball Contents ===
lerna notice 71B lib/core.js
lerna notice 485B package.json
lerna notice 105B README.md
lerna notice === Tarball Details ===
lerna notice name: @huamiao-cli-dev/core
lerna notice version: 1.0.5
lerna notice filename: huamiao-cli-dev-core-1.0.5.tgz
lerna notice package size: 566 B
lerna notice unpacked size: 661 B
lerna notice shasum: a26f686a3951de2df1ef2196a1b82c9d87e299ec
lerna notice integrity: sha512-rP/WQo7oROLD3[...]7LZKGyzZ9vHYQ==
lerna notice total files: 3
lerna notice
Successfully published:
- @huamiao-cli-dev/core@1.0.5
- @huamiao-cli-dev/utils@1.0.5
lerna success published 2 packages
复制代码
等待成功后,在npmjs packages
中的huamiao-cli
组织,查看是否发布成功。
这边发布在huamiao-cli-dev
这个组织里面
注:npmjs不稳定,并且对于国内网络互联互通问题经常访问较慢,如果失败,判断不是我们的问题,可以重新发布,就是learn发布每次都会要求加个版本,这点有点不合理
注:这边新手会遇到各种问题,如有问题,可以在留言,老衲帮掘友解决。
强烈建议新手测试发布成功后再进行开发。
3. 最少必要知识
3.1.learn
3.1.1.初始化
- lerna init,初始化项目,创建
lerna.json
存储version
,并检查package.json
中devDependency
有无lerna
,没有则添加
3.1.2.创建package
- lerna create,创建package
以下<>为必须,[]为可选
Usage:lerna create <name> [loc]
name:唯一、包含作用域名(本文为@huamiao-cli,eg:@huamiao-cli/core)
loc:包和包可以嵌套,选择包的相对地址,默认为第一个配置的包
位置
复制代码
- lerna add,安装依赖,eg:
lerna add module-1 --scope=module-2
,scope
指定包,不指定会安装到每个包里,每次只能安装一个依赖
Usage:lerna add <package>[@version] [--dev] [--exact] [--peer]
复制代码
- lerna link,链接依赖,本地调试包的互相依赖,多个包一起开发
3.1.3.开发和测试
- lerna exec,对packages中每个包执行命令,eg:
lerna exec -- rm -rf ./node_modules
- lerna run,对packages中每个包执行
npm script
,eg:lerna run test
- lerna clean,删除所有packages中的node_module,约等于
lerna exec -- rm -rf ./node_modules
- lerna bootstrap,对packages中每个包重新按安装依赖
3.2. npm
发布简易流程
3.2.1. npmjs
注册账号
3.2.2. 切换为官方源
打开终端,切换为官方源(如果之前没切换过源,默认是官方的,不需要切换)
npm config set registry https://registry.npmjs.org
复制代码
3.2.3. npm登录
npm login
Username: <username>
Password:
Email: (this IS public) <username>@qq.com
Logged in as <username> on https://registry.npmjs.org/.
复制代码
3.2.4. npm发布
npm publish
Successfully published:
- @huamiao-cli/core@1.0.0
- @huamiao-cli/utils@1.0.0
lerna success published 2 packages
复制代码
4. cli准备工作
4.1. 配置命令
packages/core
将作为cli入口
切换到packages/core
目录
新增文件bin/index.js
:
#!/usr/bin/env node
require('../lib/index')();
复制代码
package.json
添加:
"bin": {
"miao": "bin/index.js"
},
复制代码
lib/index.js
:
console.log('hello world!');
复制代码
npm link
安装到全局,全局就可以调用miao
命令了
打开终端测试下:miao
,测试成功,正常打印出:
hello world!
复制代码
4.2. 版本号、欢迎语
接着开发,获取版本号
const packageJson = require('../package');
复制代码
上面的欢迎语,比较一般,我们来个有逼格的:
const packageJson = require('../package');
console.log('欢迎使用');
console.log(` _ _ __ __ _ ____ _ _
| | | |_ _ __ _| \\/ (_) __ _ ___ / ___| (_)
| |_| | | | |/ _\` | |\\/| | |/ _\` |/ _ \\ ___ | | | | |
| _ | |_| | (_| | | | | | (_| | (_) ||___|| |___| | |
|_| |_|\\__,_|\\__,_|_| |_|_|\\__,_|\\___/ \\____|_|_| Version ${packageJson.version}
`);
复制代码
4.3. 日志,分包开发
上面的console.log('欢迎使用');
改颜色下,提升逼格:
const { log } = require('@huamiao-cli/utils');
复制代码
这边log
我们还没开发
core/package.json
添加
"dependencies": {
"@huamiao-cli/utils": "^1.0.0"
},
复制代码
lerna link
现在就可以用packages/utils
包的方法了
日志我们用的是npmlog
,切换到packages/utils
需要安装依赖:npm i npmlog
packages/utils/package.json
修改:
"main": "lib/index.js",
复制代码
lib/index.js
文件:
'use strict';
const log = require('./log');
// 统一导出,后面还有很多工具
module.exports = {
log
};
复制代码
lib/log.js
文件:
const log = require('npmlog')
log.level = 'info'
log.heading = 'huamiao-cli' // 自定义头部
log.addLevel('success', 2000, { fg: 'green', bold: true }) // 自定义success日志
log.addLevel('notice', 2000, { fg: 'blue', bg: 'black' }) // 自定义notice日志
module.exports = log
复制代码
回到packages/core/lib/index.js
:
"use strict";
module.exports = core;
const packageJson = require("../package");
const { log } = require("@huamiao-cli/utils");
function core() {
console.log(` _ _ __ __ _ ____ _ _
| | | |_ _ __ _| \\/ (_) __ _ ___ / ___| (_)
| |_| | | | |/ _\` | |\\/| | |/ _\` |/ _ \\ ___ | | | | |
| _ | |_| | (_| | | | | | (_| | (_) ||___|| |___| | |
|_| |_|\\__,_|\\__,_|_| |_|_|\\__,_|\\___/ \\____|_|_| Version ${packageJson.version}
`);
log.info("欢迎使用");
}
复制代码
这样版本号、欢迎语已经ok了
4.4. nodejs最低版本判断
我们这个cli
使用到的一些库是需要nodejs version 12+
,所以这边我们需要检查下当前nodejs
的版本,这边需要semver
包,安装下:npm i semver
packages/core/lib/index.js
增加:
const semver = require("semver");
const MINIMUM_NODEJS_VERSION = "14.0.0";
if (semver.lte(process.version, MINIMUM_NODEJS_VERSION)) {
console.log(`huamiao-cli 最低要求Node.js版本 v${MINIMUM_NODEJS_VERSION}`);
process.exit();
}
复制代码
这个提示我们想让他变成红色,可以用log:
const semver = require("semver");
const MINIMUM_NODEJS_VERSION = "14.0.0";
if (semver.lte(process.version, MINIMUM_NODEJS_VERSION)) {
log.error(`huamiao-cli 最低要求Node.js版本 v${MINIMUM_NODEJS_VERSION}`);
process.exit();
}
复制代码
最低nodejs
的版本号我们抽离常量出来,方便后面修改
packages/core/lib/index.js
现在我们完整的代码如下:
"use strict";
module.exports = core;
const packageJson = require("../package");
const { log } = require("@huamiao-cli/utils");
function core() {
console.log();
console.log(` _ _ __ __ _ ____ _ _
| | | |_ _ __ _| \\/ (_) __ _ ___ / ___| (_)
| |_| | | | |/ _\` | |\\/| | |/ _\` |/ _ \\ ___ | | | | |
| _ | |_| | (_| | | | | | (_| | (_) ||___|| |___| | |
|_| |_|\\__,_|\\__,_|_| |_|_|\\__,_|\\___/ \\____|_|_| Version ${packageJson.version}
`);
log.info("欢迎使用");
console.log();
const semver = require("semver");
const MINIMUM_NODEJS_VERSION = "14.0.0";
if (semver.lte(process.version, MINIMUM_NODEJS_VERSION)) {
log.error(`huamiao-cli 最低要求Node.js版本 v${MINIMUM_NODEJS_VERSION}`);
process.exit();
}
}
复制代码
执行下miao
:
4.5. 分析参数是否有效,是否debug
接下来分析下参数
为了调试方便packages/utils/lib/log.js
可以修改日志等级为verbose
,这个是调试用的等级:
log.level = 'verbose'
复制代码
校验入参,这边需要,minimist
包,安装下依赖:npm i minimist
const minimist = require("minimist");
log.info("校验入参:");
let args = minimist(process.argv.slice(2));
log.info("args: ", args);
if (args["_"].length === 0) {
log.warn('请输入参数')
}
复制代码
测试下
miao cmdChild cmdChildParams -a aParams -b bParams
huamiao-cli info 欢迎使用
huamiao-cli verb 校验入参:
huamiao-cli verb args: { _: [ 'cmdChild', 'cmdChildParams' ], a: 'aParams', b: 'bParams' }
复制代码
我们经常需要调试,所以我们加个debug参数,miao --debug
,minimist
默认支持
const minimist = require("minimist");
log.info("校验入参:");
let args = minimist(process.argv.slice(2));
log.info("args: ", args);
if (args["_"].length === 0 && !args.debug) {
log.warn("请输入参数");
} else {
// 可以在这配置debug相关设置,比如修改log的等级为verbose来打印调试日志
log.level = "verbose";
log.verbose("debug");
}
复制代码
添加下环境变量process.env.LOG_LEVEL
,并设置log的日志等级
const minimist = require("minimist");
log.verbose("校验入参:");
let args = minimist(process.argv.slice(2));
log.verbose("args: ", args);
if (args["_"].length === 0 && !args.debug) {
log.warn("请输入参数");
} else {
// 可以在这配置debug相关设置,比如修改log的等级为verbose来打印调试日志
if (args.debug) process.env.DEBUGGING = "verbose";
else process.env.DEBUGGING = "info";
log.level = process.env.DEBUGGING;
}
复制代码
准备工作算结束了
4.6. 注册命令
使用commander
包,安装依赖:npm i commander
const program = require("commander");
// 设置版本号、自定义用法说明
program.version(packageJson.version).usage("<command> [options] 其他说明");
// 添加命令可以在这里添加
// ……
// 注册命令
program.parse(process.argv);
复制代码
我们测试下,miao -h
添加个简单的命令:
const program = require("commander");
// 设置版本号、自定义用法说明
program.version(packageJson.version).usage("<command> [options] 其他说明");
// 添加命令可以在这里添加
program
.command("test")
.description("描述")
.option("-a, --all", "清空全部")
.action((cmd, options) => {
// cmd.all 自动由上面配置 .option("-a, --all", "清空全部") 创建
console.log("cmd: ", cmd.all);
log.success("测试", "一只花喵");
});
// 其他子命令
program.command("*").action(function (cmd, options) {
console.log("cmd: ", cmd);
console.log("options: ", options.args);
console.log("没有匹配到命令:miao", args['_']);
});
// 注册命令
program.option("--debug", "打开调试模式").parse(process.argv);
复制代码
试试miao test -a aaaa
、miao test --all bbbb
、miao diy
吧!
内容有点多,第一节先这样吧!
FAQ
- 依赖错误或未知错误,清理所有依赖
lerna clean
,重装依赖lerna bootstrap
Loadmap
- init【重磅】:选择模板、自定义模板等
- 发布【重磅】:发布、git操作、构建等
- 更多实现细节
参考
- [1] 什么是 CI/CD:linux.cn/article-992…
- lerna doc:github.com/lerna/lerna
- CI/CD是什么:www.redhat.com/zh/topics/d…