创建自己定义的脚手架,能够加速开发流程,并且对项目开发更加规范化。
目标: 通过quick-cli快速创建标准项目,集成公司自定义的一些规范。

第三方包的使用

commander

命令行插件,npmjs地址
使用方法:

<> 和[]的区别

  • <> 表示必选参数,不加参数的话就报错
  • []表示可选参数,如果后边不加参数的话,就是布尔值

    command和option的区别

  • command执行的命令,传递参数不用再次标识

  • option执行命名时的参数设置,传递参数需要用【-简称】或者【—全称】标识
  • 使用command要放option上边 ```javascript const program = require(‘commander’); program .command(“create-app “) .option(“-s —size [size]”, “Pizza size”, /^(large|medium|small)$/i, “medium”) .option(“-d —drink “, “Drink”, /^(coke|pepsi|izze)$/i) .action((name, options) => { console.log(“name done =>”, name, options); }) .parse(process.argv); // 执行:node bin/demo1.js setupo -s large -d coke // 输出:name done => setupo { size: ‘large’, drink: ‘coke’ }

// 执行:node bin/demo1.js -s large -d coke // 报错:error: missing required argument ‘app-name’

// 执行:node bin/demo1.js hello -s -d coke // 输出:name done => hello { size: true, drink: ‘coke’ }

// 执行:node bin/demo1.js hello -s // 输出:name done => hello { size: true }

  1. <a name="x0J3S"></a>
  2. #### program.parse与action连接书写和分开的区别
  3. ```javascript
  4. /*
  5. program.parse与action连接书写和分开的区别
  6. 连接书写时:不用再次输入 create 指令,如果输入了create,会把create作为app-name的参数
  7. 分开写时:必须输入 create 指令,否则会报错
  8. */
  9. program
  10. .command("create <app-name>")
  11. .description("创建项目")
  12. .option("-n --name <type>", "output name")
  13. .action((name, options) => {
  14. console.log("done", name, options);
  15. })
  16. .parse(process.argv);
  17. /* program
  18. .command("create <app-name>")
  19. .description("创建项目")
  20. .option("-n --name <type>", "output name")
  21. .action((name, options) => {
  22. console.log("done", name, options);
  23. })
  24. program.parse(process.argv); */

figlet插件,大字符显示

在终端打印大型文字,npmjs地址

  1. const figlet = require("figlet");
  2. const { promisify } = require("util");
  3. const asyncFiglet = promisify(figlet);
  4. // 传统的callback调用方法
  5. /* figlet("Hello World!!", function (err, data) {
  6. if (err) {
  7. console.log("Something went wrong...");
  8. console.dir(err);
  9. return;
  10. }
  11. console.log(data);
  12. }); */
  13. // 使用aysnc 同步调用的方式
  14. async function run(text) {
  15. try {
  16. let data = await asyncFiglet(text);
  17. console.log(data);
  18. } catch (err) {
  19. console.log(err);
  20. }
  21. }
  22. run(process.argv[2]);

image.png
image.png

chalk美化日志输出

npmjs地址
使用案例

  1. import chalk from 'chalk';
  2. const error = chalk.bold.red;
  3. const warning = chalk.hex('#FFA500'); // Orange color
  4. console.log(error('Error!'));
  5. console.log(warning('Warning!'));

inquirer命令行参数输入交互

npmjs地址
使用案例

参数类型

  • type:表示提问的类型,包括:input、confirm、 list、rawlist、expand、checkbox、password、editor。
  • name: 存储当前输入的值。
  • message:问题的描述。
  • default:默认值。
  • choices:列表选项,在某些type下可用,并且包含一个分隔符(separator);
  • validate:对用户的回答进行校验。
  • filter:对用户的回答进行过滤处理,返回处理后的值。
  • when:根据前面问题的回答,判断当前问题是否需要被回答。
  • pageSize:修改某些type类型下的渲染行数。
  • prefix:修改message默认前缀。
  • suffix:修改message默认后缀。
    1. var inquirer = require('inquirer');
    2. inquirer
    3. .prompt([
    4. /* Pass your questions in here */
    5. {
    6. name: "author",
    7. type: "input",
    8. message: "开发人员姓名",
    9. }
    10. ])
    11. .then((answers) => {
    12. // Use user feedback for... whatever!!
    13. console.log("answer: ", answers);
    14. })
    15. .catch((error) => {
    16. if (error.isTtyError) {
    17. // Prompt couldn't be rendered in the current environment
    18. } else {
    19. // Something else went wrong
    20. }
    21. });

    shelljs

    在js中可以执行shell命令
    npmjs地址 ```javascript var shell = require(‘shelljs’);

if (!shell.which(‘git’)) { shell.echo(‘Sorry, this script requires git’); shell.exit(1); }

// Copy files to release dir shell.rm(‘-rf’, ‘out/Release’); shell.cp(‘-R’, ‘stuff/‘, ‘out/Release’);

// Replace macros in each .js file shell.cd(‘lib’); shell.ls(‘.js’).forEach(function (file) { shell.sed(‘-i’, ‘BUILD_VERSION’, ‘v0.1.2’, file); shell.sed(‘-i’, /^.REMOVE_THIS_LINE.$/, ‘’, file); shell.sed(‘-i’, /.REPLACE_LINE_WITH_MACRO.*\n/, shell.cat(‘macro.js’), file); }); shell.cd(‘..’);

// Run external tool synchronously if (shell.exec(‘git commit -am “Auto-commit”‘).code !== 0) { shell.echo(‘Error: Git commit failed’); shell.exit(1); }

  1. <a name="lPagd"></a>
  2. ### ora
  3. 下载代码时的loading效果<br />[npmjs地址](https://www.npmjs.com/package/ora)
  4. ```javascript
  5. import ora from 'ora';
  6. const spinner = ora('Loading unicorns').start();
  7. setTimeout(() => {
  8. spinner.color = 'yellow';
  9. spinner.text = 'Loading rainbows';
  10. setTimeout(() => {
  11. spinner.stop();
  12. }, 500);
  13. }, 1000);

download-git-repo

npmjs地址
插件作用,下载git仓库的代码。

  1. // 拉取仓库代码,如果是分支,则在仓库地址后 [#dev]
  2. download('direct:https://github.com/shenshuai89/logtheme', 'test/tmp', { clone: true }, function (err) {
  3. console.log(err ? 'Error' : 'Success')
  4. })

开发一个cli工具

平常我们创建项目的时候会发现这个项目创建的目录结构或者组织架构还或者一部分代码配置要多次初始化。使用频率高,重复性工作多,浪费大量时间。可以开发一个cli工具,通过简单命令就可以方便创建项目模版。

希望实现一个工具可以做到,可以重复使用之前搭建的项目配置,这样就有更多的时间专注业务逻辑层的东西;
实现一个quick-create-library工具,主要使用上面介绍的各种插件工具。

🔔注意:把依赖项设置dependencies,这样在全局安装cli时会把依赖包一起安装

创建命令入口文件

新建bin目录,创建index.js文件。
第一行需要标注 :#! /usr/bin/env node,标识该文件用node运行

  1. #! /usr/bin/env node
  2. const program = require("commander");
  3. const { promisify } = require("util");
  4. // 打印大字体logo
  5. const asyncFiglet = promisify(require("figlet"));
  6. // 美化输出日志
  7. const chalk = require("chalk");
  8. const log = (content) => console.log(chalk.yellow(content));
  9. // 增加用户交互
  10. const inquirer = require("inquirer");
  11. const init = require("./init");
  12. // 读取package中的version信息,version会随着 major、minor、patch更新
  13. const { version } = require("../package.json");
  14. program.version(version);
  15. program.option("-n --name <type>", "set project name");
  16. async function printLogo(name) {
  17. let data = await asyncFiglet(name);
  18. console.log(data);
  19. }
  20. program
  21. // <> 参数为比填
  22. .command("create <app-name>")
  23. .description("Create a project")
  24. .action(async (name) => {
  25. await printLogo(name);
  26. log("准备创建项目");
  27. let answer = await inquirer.prompt([
  28. {
  29. name: "language",
  30. type: "list",
  31. message: "请选择语言版本",
  32. choices: ["javascript", "typescript"],
  33. },
  34. ]);
  35. if (answer.language) {
  36. log("javascript: ");
  37. // 执行init方法
  38. init(name);
  39. } else {
  40. log("typescript: ");
  41. }
  42. });
  43. program.parse(process.argv);
  1. const { promisify } = require("util");
  2. const ora = require("ora");
  3. const download = promisify(require("download-git-repo"));
  4. const shell = require("shelljs");
  5. const chalk = require("chalk");
  6. const log = (content) => console.log(chalk.yellow(content));
  7. module.exports = async (name) => {
  8. log("creat project");
  9. const spinner = ora("loading...").start();
  10. shell.rm("-rf", name);
  11. try {
  12. await download(
  13. "direct:https://github.com/shenshuai89/librarytemp.git#main",
  14. name,
  15. { clone: true }
  16. );
  17. spinner.succeed("🚀🚀🚀 下载成功");
  18. log(`
  19. =================================================
  20. cd ${name}
  21. yarn or npm install
  22. yarn run dev
  23. `)
  24. } catch (error) {
  25. console.error(error);
  26. log("⚡️⚡️⚡️下载失败");
  27. spinner.stop();
  28. }
  29. };

设置package

  1. "bin": {
  2. "quick-create-library": "./bin/index.js",
  3. "qcli": "./bin/index.js"
  4. },
  5. "scripts": {
  6. "major": "npm version major -m 'build: update major'",
  7. "minor": "npm version minor -m 'build: update minor'",
  8. "patch": "npm version patch -m 'build: update patch'",
  9. "pub:major": "npm run major && npm publish --access=public",
  10. "pub:minor": "npm run minor && npm publish --access=public",
  11. "pub:patch": "npm run patch && npm publish --access=public"
  12. },

scripts脚本可以用于设置action动作时使用。

通过ejs,给模板动态设置参数

修改init.js文件,下载模版成功后,添加对模板文件的获取。

  1. /* 动态设置模版信息 */
  2. const packagePath = path.join(targetPath, "package.json");
  3. // 设置examples中的文件引入
  4. const exampleIndexPath = path.join(targetPath, "examples/index.html");
  5. const exampleNodePath = path.join(targetPath, "examples/useNodeTest.js");

通过inquirer插件,设置用户交互,让用户的输入信息可以直接写入模版中。

  1. let pkgData = await inquirer.prompt([
  2. {
  3. type: "input",
  4. name: "author",
  5. message: "author?",
  6. default: "",
  7. },
  8. {
  9. type: "input",
  10. name: "description",
  11. message: "description?",
  12. default: "create npmjs package.",
  13. },
  14. {
  15. type: "list",
  16. name: "license",
  17. message: "license?",
  18. choices: ["MIT", "GPL", "BSD", "Mozilla", "Apache", "LGPL"],
  19. default: "MIT",
  20. },
  21. ]);
  22. if (fs.existsSync(packagePath)) {
  23. const content = fs.readFileSync(packagePath).toString();
  24. //编译package.json文件
  25. const result = ejs.compile(content)({ projectName: name, ...pkgData });
  26. fs.writeFileSync(packagePath, result);
  27. }
  28. if (fs.existsSync(exampleIndexPath)) {
  29. const content = fs.readFileSync(exampleIndexPath).toString();
  30. //编译package.json文件
  31. const result = ejs.compile(content)({ projectName: name });
  32. fs.writeFileSync(exampleIndexPath, result);
  33. }
  34. if (fs.existsSync(exampleNodePath)) {
  35. const content = fs.readFileSync(exampleNodePath).toString();
  36. //编译package.json文件
  37. const result = ejs.compile(content)({ projectName: name });
  38. fs.writeFileSync(exampleNodePath, result);
  39. }

主要使用ejs的compile API。

设置action自动发包

由于发布包是一个固定的流程化操作的过程,该过程中会涉及到npm源的切换,以及npmjs账号的登录【可以使用actions中的npm token替代】,为了避免这些重复的操作,也可以直接进行发布包,可以通过创建actions来自动构建包并发布包。

进入到仓库的Action页面

点击仓库上方的Actions按钮
image.png

点击new workflow按钮,可以创建一个新的actions。
image.png
然后会自动创建yml并进入修改页面
image.png

编辑yml文件

创建major.yml、minor.yml、patch.yml三个action配置文件,只用修改一下17行和22行对应的脚步

  1. name: pub_patch
  2. on:
  3. workflow_dispatch:
  4. jobs:
  5. publish-npm:
  6. runs-on: ubuntu-latest
  7. steps:
  8. - uses: actions/checkout@v2
  9. - name: install node
  10. uses: actions/setup-node@v3.0.0
  11. with:
  12. node-version: "14.x"
  13. registry-url: https://registry.npmjs.org
  14. - name: publish patch
  15. run: |
  16. git config --global user.name 'xxx123'
  17. git config --global user.email '123xxx.com'
  18. npm config set //registry.npmjs.org/:_authToken=$NPM_TOKEN
  19. npm run pub:major
  20. git push -f
  21. env:
  22. NPM_TOKEN: ${{secrets.NPM_TOKEN}}
  • name:自定义actions脚本的名称,以后在workflows中展示的名称。
  • on:workflows 通过 on 关键字定义触发条件,主要有三类触发事件

    • 人工触发

      1. on: workflow_dispatch
    • 定时触发,每隔 15 分钟触发一次 workflows

      1. on:
      2. schedule:
      3. - cron: '*/15 * * * *'
    • Webhook 触发,在 GitHub 上的操作,比如创建 Issues、新增 Deployment ,已经代码的pull/push等,都能够通过 API 获取到相关的事件。通过这些事件,我们可以精准地定制 workflow 的行为。通常我们都是基于 push 或者 pull requests 触发,下面列举几个不常见的示例:

      1. on:
      2. fork
      1. on:
      2. watch:
      3. types: [started]
      1. on:
      2. issues:
      3. types: [opened]
  • job: 创建的任务名称

    • build和publish-npm,表示创建的不同任务流,当然也可以创建到一个job中。如果创建的一个job中,无法使用job的任务编排控制
    • steps:表示任务中的执行过程步骤
      • uses:表示在Marketplace中已经存在的插件,可以方便使用。
      • name:表示执行过程中steps的标识

image.png

  1. - run:表示要执行的命令,如果是多条命令,可以使用 | 作为表示,之后可以写多条命令。
  1. ## 给npm包发布patch版本
  2. ## 过程中设置npmjs的登录,使用env的NPM_TOKEN
  3. ## github的配置。
  4. name: pub_patch
  5. on:
  6. workflow_dispatch:
  7. jobs:
  8. publish-npm:
  9. runs-on: ubuntu-latest
  10. steps:
  11. - uses: actions/checkout@v2
  12. - name: install node
  13. uses: actions/setup-node@v3.0.0
  14. with:
  15. node-version: "14.x"
  16. registry-url: https://registry.npmjs.org
  17. - name: install deps
  18. run: npm install
  19. - name: prepublish
  20. run: npm run prepublish
  21. - name: publish patch
  22. # run可以创建多个任务命令,用 | 做标识。
  23. # 完成git登录
  24. # 完成npmjs验证
  25. # 完成patch补丁发布
  26. # 将最新代码推送到仓库
  27. # env是用于设置变量,这些变量可以在仓库的setting中设置,避免一些明文数据泄露
  28. run: |
  29. git config --global user.name 'xxx'
  30. git config --global user.email 'sss'
  31. npm config set //registry.npmjs.org/:_authToken=$NPM_TOKEN
  32. npm run pub:patch
  33. git push -f
  34. env:
  35. NPM_TOKEN: ${{secrets.NPM_TOKEN}}

Job 编排控制执行顺序

workflow 由很多个 job 组成,借助于 needs 参数,我们可以管理这些 job 之间的依赖,控制其执行流程。

  1. on: push
  2. jobs:
  3. job1:
  4. runs-on: ubuntu-latest
  5. steps:
  6. - run: echo "job1"
  7. job2:
  8. runs-on: ubuntu-latest
  9. steps:
  10. - run: sleep 5
  11. needs: job1
  12. job3:
  13. runs-on: ubuntu-latest
  14. steps:
  15. - run: sleep 10
  16. needs: job1
  17. job4:
  18. runs-on: ubuntu-latest
  19. steps:
  20. - run: echo "job4"
  21. needs: [job2, job3]

上面的 workflows 执行时,job2 和 job3 会等 job1 执行成功时才执行,job4 会等 job2 和 job3 执行成功时才执行。
image.png

身份认证,npm token

用github actions将文件上传到npm库时,则需要在github配置一下npm access tokens
登录你的账号,然后点击Access Tokens
image.png
image.png
接下来一定要选择中间的 Automation,这样才能免登录,并且保证一定的安全行。
image.png
image.png
打开github的仓库,找到setting,创建NPM_TOKEN变量
image.png
配置完成后,就可以用action发布新的脚手架内容。