从一个简单的初始化远程git仓库工具开始学习前端脚手架的建设

2,037 阅读8分钟

1. 开篇

在开始之前我们得知道我们要做的东西是什么样的 -> 一个能根据git地址初始化远程仓库的工具

说到这里你就会说:“git不是提供了一个clone命令给你用吗?”

ok,那我们用clone来看一下他和我们的预期有什么不同

使用clone来下载远程仓库

git clone https://gitee.com/xiao-koa/xiao-koa-demo.git

我们来看看有什么缺陷

  1. 项目文件夹的名字不是我们自己定义的
  2. 项目的.git文件存在
  3. package.json内容不是初始化的 例如:name、version、description...

上诉就是git克隆存在的问题,可能到这你会说动手改一下就好了,何必多此一举去做一个工具。但你点开这篇文章不就是为了学习吗?我们只是举个简单的例子让你体会前端开发工具的流程,在这个基础上你可以随心所欲做你想做的工具,成为别人口中的黑客小子(不是)

ok那么废话不多说直接开始!!!

2. 前提

在学习之前,我们得去看一下世面上的初始化类npm工具有哪些,并且他们的使用方法是什么样的

  1. vue-cli

    # vue脚手架,用于初始化vue项目# 1. 先全局安装
    npm install -g @vue/cli
    ​
    # 2. 再使用
    vue create vue01
    
  2. vite

    # 新型前端构建工具# 1. 直接使用
    npm init vite
    

可以看到 vite 比 vue-cli 简单便捷,我们来看下npm init xxx做了什么

首先我们使用 init 随便输入一个不存在的包名

npm init woshizhendeshuai

随后就报错了

C:\Users\Administrator>npm init woshizhendeshuai
npm ERR! code E404
npm ERR! 404 Not Found - GET https://registry.npmjs.org/create-woshizhendeshuai - Not found
npm ERR! 404
npm ERR! 404  'create-woshizhendeshuai@latest' is not in this registry.
npm ERR! 404
npm ERR! 404 Note that you can also install from a
npm ERR! 404 tarball, folder, http url, or git url.
​
npm ERR! A complete log of this run can be found in:
npm ERR!     C:\Users\Administrator\AppData\Local\npm-cache_logs\2022-10-15T02_59_27_512Z-debug-0.log

从上面就可以看出来是没有找到这个包名create-woshizhendeshuai@latest,原来使用 init 的时候npm会帮我们自动在名字前面加上create-,所以我们想要使用这个功能就需要将库的名字前面加上create-,至于那个@latest是指下载版本最新包。

关于npm init xxx我们就暂时讲到这里,后面就够用了,有兴趣的请自行查阅相关文章。

3. 初始化

从上面可以看出使用npm init xxx还是很方便的,所以我们这个项目就用这个功能。

  1. 新建文件夹,名称为create-git-store,注意:必须使用create开头

  2. 初始化项目

    npm init -y && npm i typescript --save-dev && npx tsc --init && npm i --save-dev @types/node
    
  1. 在项目里面新建src文件夹,在文件夹下面创建index.ts

    #!/usr/bin/env node
    console.log('hello utils');
    
  2. package.json的scripts里面添加

      "scripts": {
        "dev": "npx ts-node ./src/index.ts"
      },
    
  3. 运行npm run dev,如果你在控制台看到输出字符就代表你大功告成了!!!

    PS E:\轩小浅\create-git-store> npm run dev
    
    > create-git-store@1.0.0 dev
    > npx ts-node ./src/index.ts
    
    hello utils
    

4. 解析命令

我们的命令是设计成这样的

npm init git-store project https://gitee.com/xuanxiaoqian/qian-cli.git mater

npm init git-store <项目名称> <远程地址> [分支名]

其中项目名称和远程地址为必填项,分支名不填默认master。那我们怎么样才能获取到这些参数呢?社区有一个开源库commander为我们解决了这个问题

下载commander

npm install commander 

使用

#!/usr/bin/env node
import { program } from "commander"; // 导入

program
  .command("<app-name> [gitConfig...]")
  .description("初始化远程模板")
  .action((name: string, gitConfig: string[]) => {
    console.log(name);
    console.log(gitConfig);
  });

program.parse(process.argv);	// 必须解析

上面代码很简单,我们一步一步看 先导入commander的program

然后在program身上链式调用添加属性

  1. program.command("<app-name> [gitConfig...]") //解析的命令
    
    // 尖括号为必填参数,方括号为选填参数,方括号参数里面的 “...” 表示可以有多个参数
    
    // 当我们使用npm init git-store project xxx mater的时候,其中project参数就给到了app-name,后面的参数就当成一个数组给到了gitConfig
    
  2. program.description("初始化远程模板")	// 命令的描述
    
    // 当我们使用npm init git-store --help就可以在里面看到
    
  3. program.action((name: string, gitConfig: string[]) => {	// 当用户输入了命令后的回调函数
        console.log(name);
        console.log(gitConfig);
      });
    
  4. program.parse(process.argv);	// 解析命令
    

然后我们去调试代码,首先在package.json的scripts里面添加

"bin": {
    "git-store": "./src/index.js"
},
"scripts": {
    "build": "npx tsc ./src/index.ts"
},

其中bin代表工具性质的npm包,一定有bin字段,对外暴露脚本命令。

然后运行下面命令

npm run build && npm link

其中link表示将当前包链接到全局

做完上述之后我们来到控制台运行git-store project xxx

然后你就会得到一个错误 ......

C:\Users\Administrator>git-store project xxx
error: unknown command 'project'

说没有找到project命令,我猜可能是commander需要一个解析开头,我们将index.ts修改一下

#!/usr/bin/env node
import { program } from "commander"; // 导入

program
  .command("init <app-name> [gitConfig...]")
  .description("初始化远程模板")
  .action((name: string, gitConfig: string[]) => {
    console.log(name);
    console.log(gitConfig);
  });

program.parse(process.argv);	// 必须解析

再来到控制台运行git-store init project xxx就可以看到没有报错并且正确的打印了参数

可这不是我们想要的,然后我就去看了vite官方源码,其中它是用minimist实现的,那我们也只好换工具库(别打我)

一系列操作

# 卸载commander
npm uninstall commander

# 安装minimist
npm install minimist

# 安装minimist的ts声明文件
npm i --save-dev @types/minimist

修改src/index.ts

#!/usr/bin/env node

import minimist from "minimist";

const argv = minimist(process.argv.slice(2), { string: ["_"] });

console.log(argv);

运行npm run build,然后报错

src/index.ts:3:8 - error TS1259: Module '"E:/\u8F69\u5C0F\u6D45/create-git-store/node_modules/@types/minimist/index"' can only be default-imported using the 'esModuleInterop' flag

经过搜寻,发现是tsc编译的问题,问题详细 最终我们选择esbuild来打包,并且esbuild还能减少打包等待时间

tips:不要嫌BUG多,现在把坑踩了以后遇到了就不会再头疼

下载esbuild

npm install esbuild --save-dev

在项目根目录添加一个build.js文件

const esbuild = require("esbuild");

const build = async function () {
  await esbuild.build({
    entryPoints: ["src/index.ts"],
    bundle: true,
    splitting: false,
    outfile: "src/index.js",
    format: "cjs",
    platform: "node",
    target: "node14",
  });
  console.log("打包成功!");
};

build();

修改package.json的scripts为

"scripts": {
    "build": "node build.js"
},

运行npm run build就可以看到打包成功了,在控制台运行git-store project xxx master就可以看到输出的内容

C:\Users\Administrator>git-store project xxx master
{ _: [ 'project', 'xxx', 'master' ] }

到此为止解析命令就完成了

5. 执行命令

上一步已经拿到了参数,首先我们需要做一些前置过滤,只有用户输入的是正确的才去运行脚本

  1. 解析的参数数组长度必须 >=2
  2. 数组第二个的地址必须是git地址

ok我们来到代码解决这些

#!/usr/bin/env node

import minimist from "minimist";
import { exec } from "child_process";
import path from "path";
import fs from "fs";

const argv = minimist(process.argv.slice(2), { string: ["_"] });

console.log(argv._);

if (argv._.length < 2) {
    console.log("输入命令有误");
    process.exit(0);
}

let projectName = argv._[0];
let gitRemote = argv._[1];
let gitBranch = argv._[3] ?? “master”;

let regex = new RegExp(/^http(s)?://.*.git$/);
if (!regex.test(gitRemote)) {
    console.log(`无效git地址${gitRemote},必须以.git结尾`);
    process.exit(1);
}

console.log("可以执行命令了");

然后就是根据用户的参数去下载远程模板,我现在知道的有三种方法下载

  1. 使用download-git-repo库下载
  2. 使用axios下载(gitee不可用)
  3. 通过clone下载

我来说下我用这些分别遇到的问题。使用download-git-repo库下载的时候报错不能捕捉。使用axios下载局限性太大。所以我最终选择了通过git提供的clone下载

node原生提供了一个库可以模拟用户执行命令

import { exec } from 'child_process'

exec('echo "hello"',(err, stdout, stderr) => {
    if (err) {
        console.log(red(`报错原因:${err}`))
        process.exit(1)
    }

    process.exit(1)
},

我们拿着这个库来运行clone命令

#!/usr/bin/env node

import minimist from "minimist";
import { exec } from "child_process";
import path from "path";
import fs from "fs";

const argv = minimist(process.argv.slice(2), { string: ["_"] });

if (argv._.length < 2) {
  console.log("输入命令有误");
  process.exit(0);
}

let projectName = argv._[0];
let gitRemote = argv._[1];
let gitBranch = argv._[3] ?? "master";

let regex = new RegExp(/^http(s)?://.*.git$/);
if (!regex.test(gitRemote)) {
  console.log(`无效git地址${gitRemote},必须以.git结尾`);
  process.exit(1);
}

const removeDir = (filePath: string) => {
  let statObj = fs.statSync(filePath); // fs.statSync同步读取文件状态,判断是文件目录还是文件。
  if (statObj.isDirectory()) {
    //如果是目录
    let dirs = fs.readdirSync(filePath); //fs.readdirSync()同步的读取目标下的文件 返回一个不包括 '.' 和 '..' 的文件名的数组['b','a']
    dirs = dirs.map((dir) => path.join(filePath, dir)); //拼上完整的路径
    for (let i = 0; i < dirs.length; i++) {
      // 深度 先将儿子移除掉 再删除掉自己
      removeDir(dirs[i]);
    }
    fs.rmdirSync(filePath); //删除目录
  } else {
    fs.unlinkSync(filePath); //删除文件
  }
};

exec(`git clone -b ${gitBranch} ${gitRemote} ${projectName}`, (err) => {
  if (err) {
    console.log(`报错原因:${err}`);

    process.exit(1);
  }

  // 删除.git
  removeDir(path.resolve(projectName, ".git"));

  // 前提是存在package.json
  if (fs.existsSync(path.resolve(projectName, "package.json"))) {
    let _data: any = JSON.parse(
      fs.readFileSync(path.resolve(projectName, "package.json"), {
        encoding: "utf-8",
        flag: "r",
      })
    );
    _data.name = projectName;
    _data.version = "0.0.0";
    _data.description = "";

    let str = JSON.stringify(_data, null, 4);
    fs.writeFileSync(`${path.resolve(projectName, "package.json")}`, str);
  }

  console.log();
  console.log("现在运行:");
  console.log();
  console.log("  cd " + path.basename(projectName));
});

注意,我们尽量不要用外部工具,这样就可以让我们库的体积特别小,用户运行下载才会更快

测试下功能完好,至此我们的功能基本做完了,短短70几行代码就打开了你做前端工具的思路,是不是觉得特别值呢?

现在就到了最期待的任务线 —— 发布到npm上让其他小朋友能够使用并成为他们眼中的黑客小子

修改package.json,全部示例:

{
  "name": "create-git-store",
  "version": "1.0.0",
  "description": "帮助你快速初始化git远程模板",
  "main": "index.js",
  "bin": {
    "git-store": "./src/index.js"
  },
  "file": [
    "src/index.js"
  ],
  "scripts": {
    "dev": "npx ts-node ./src/index.ts",
    "build": "node build.js"
  },
  "keywords": [
    "create-git-store",
    "git-store",
    "xuanxiaoqian"
  ],
  "author": "xuanxiaoqian",
  "license": "MIT",
  "devDependencies": {
    "@types/minimist": "^1.2.2",
    "@types/node": "^18.11.0",
    "esbuild": "^0.15.11",
    "typescript": "^4.8.4"
  },
  "dependencies": {
    "minimist": "^1.2.7"
  }
}

然后执行npm publish,如果你本地还没登录需要输入用户名密码登录。至于怎么注册npm和登录发布包这里就不多说了。

然后本地就可以去使用你的包了

npm init git-store project-git https://gitee.com/xuanxiaoqian/qian-cli.git master

6. 流水化

你以为上面那些就完了?不不不,真正的干货还在后面。

上面的仅仅是最基本的功能实现,如果你后续要修改一点东西重新发布怎么办?

  1. 打包代码
  2. 修改package.json的version(npm不能发布低于现有版本)
  3. 发布到npm

你可能又说:“就这就这?我不偷懒我手动做,咋啦!” 那万一你还将代码放到github上呢?万一你代码在gitee和github上都有呢?还要git add、git commit、git push ......

我就问你麻不麻烦?ok,我们接下来解决这些问题

理想中的状态是运行npm publish就将上面所有的事情全部做完,好巧不巧,npm有一个钩子函数prepublishOnly,执行npm publish的时候会先运行prepublishOnly,那这个东西在哪呢?就在package.json的scripts里面:

"scripts": {
    "prepublishOnly": "node test.js"
},

去执行一下npm publish就会先运行test.js文件,再帮你push到npm上

但是我们不用node去做,而是用一个库zx,有些小伙伴可能听说过这个,之前很有名气,能让你在 Node 中编写 Shell 脚本库

我们来下载

npm install zx --save-dev

在根目录添加prepublish.mjs

#!/usr/bin/env zx
import 'zx/globals'

await $`echo 123`

运行npx zx prepublish.mjs

就可以看到控制台输出信息了

那么技巧交给你了,你知道接下来怎么做了吧。这里给到我写的

#!/usr/bin/env zx
import "zx/globals";

await $`npm run build`;

let { version } = JSON.parse(fs.readFileSync("./package.json"));
let _data = JSON.parse(fs.readFileSync("./package.json"));

let v = _data.version.split(".").map(Number);

v[v.length - 1] += 1;

_data.version = v.join(".");

fs.writeFileSync("./package.json", JSON.stringify(_data, null, 2));

console.log(`版本号: ${version} -> ${_data.version}`);

await $`git add .`;

await $`git commit -m "版本号: ${_data.version}"`;

await $`git push gitee master`;

try {
    await $`git push github master`
  } catch (error) {}

console.log(`版本号: ${version} -> ${_data.version}`);

上面就是本篇文章的全部内容啦,如果你从上面写下来遇到的错误解决不了。可以去看一下我写好的 仓库(gitee)对比一下

最后就是该篇文章在哔哩哔哩后续有讲解 传送门 欢迎关注一下呀~

哔哩哔哩:地址

gitee:地址