使用nodejs编写cli(命令行)实现公共框架一键安装

在前端开发工作中,我们经常会使用到webpack-cli、Vue-cli、create-react-app等cli工具去搭建项目框架,但有时候这些工具并不能完全满足我们的需求。
公司可能会在不同时期启动多个项目,而这些项目极大可能具有相同的部分(如登录页面、公共框架等),如果只依赖已有的这些cli工具去搭建项目框架,那么每次启动一个新项目我们就要把这些相同的部分书写一遍。如果我们拥有自己的cli工具,里面包含一些自定义的模块和功能,这样每次就能使用自己的cli工具搭建项目了。
下面我们就来介绍如何使用nodejs编写cli,来实现一键创建基于create-react-app/vue-cli搭建的包含我们需要的自定义模块的公共框架,本文中的例子是使用create-react-app的。

思路

先来介绍一下思路,也就是实现步骤。

  1. 搭建符合需求的自定义框架,作为模板文件存放到远程仓库。
  2. 初始化cli项目。
  3. 创建交互式命令。
  4. 创建项目目录(即项目文件夹)和模板文件临时目录。
  5. 获取远程仓库模板文件代码,存放到临时目录。
  6. 把临时目录里的模板文件代码copy到项目中。
  7. 删除模板文件中包含定制信息。
  8. 重新写入部分文件。
  9. 将cli项目发布至 npm。

下面就一步一步来实现吧。

搭建符合需求的自定义框架,作为模板文件存放到远程仓库

项目的开始我们需要搭建一套符合需求的自定义框架,也就是我们所要开发的绝大部分项目都要使用到的功能和模块(我们称之为项目的公共部分)。这里我们选用create-react-app作为脚手架,自定义框架中包含登录模块,业务模块的左侧菜单、顶部用户信息、面包屑等功能,这些不是本文的重点,所以不在这里多做阐述。远程仓库我选用的是github,感兴趣的可以去查看具体代码front-react-frame。

模板目录

初始化cli项目

创建一个新项目create-react-frame并上传到github,在这个项目中使用 Node.js 的 esm 模块,注意需要把 Node.js 升至 12.0.0 以上版本以支持 esm 模块。
esm 模块的配置方式为在package.json文件中添加"type": "module"。cli项目目录如下:

cli项目目录

node官方对于 package.jsontype字段含义的解释:

  • 如果最近package.json文件包含字段“type”,其值为“module”,则以.js结尾或没有任何扩展名的文件将作为ES模块进行加载。
  • 其中最近package.json文件的意思是在当前文件夹下、该文件夹的父文件夹等中查找package.json,直到根目录。
  • 如果最近package.json文件没有type字段,或者包含"type": "commonjs",则无扩展名和.js结尾的文件将被视为commonjs模块进行加载。
  • 无论package.json中的type字段为何值,.mjs结尾的文件都按照ES模块来处理,.cjs结尾的文件都按照commonjs模块来处理。
    这里的ES模块和上面所说的esm模块同义,叫法不同而已。

创建交互式命令

我们之所以使用命令行工具,目的之一就是想让用户在创建项目框架时输入一些值,用于修改模板文件中的配置,自动生成完全符合我们需求的个性化项目框架,所以要在cli项目中创建交互式命令。
这里我们使用 inquirer 文档,安装 inquirer。

npm i inquirer

需要哪些交互式命令根据自己情况而定,我这里定义了五种:

  • 创建的目录名称(必填)
  • 开发环境接口服务代理(选填),不填则为默认内容
  • 项目名称(选填),不填则为默认内容
  • 用户信息接口地址(选填),不填则为默认内容
  • 菜单接口地址(选填),不填则为默认内容

以创建的目录名称为例在这里简单介绍下,其他的都比较类似,就不再做重复阐述了。

  1. 在初始化cli项目章节提到的目录结构 bin/questions 中创建 createDir.js 文件,内容如下:
// 要创建的目录名称
export default () => ({
  type: 'input',
  name: 'createDir',
  message: '请输入要创建的目录名称:',
  validate(val) {
    if (val) return true;
    return '请输入要创建的目录名称';
  },
});
  1. 创建交互问答入口文件 bin/questions/index.js
/**
 * 交互式命令行
 * 交互问答入口文件
 */
import inquirer from 'inquirer';
import createDir from './createDir.js';
import developUrl from './developUrl.js';
import projectName from './projectName.js';
import currentUser from './currentUser.js';
import userMenu from './userMenu.js';

export default () =>
  inquirer.prompt([
    createDir(), // 要创建的目录名称
    developUrl(),
    projectName(),
    currentUser(),
    userMenu(),
  ]);
  1. 这样就完成了交互式命令的创建,直接在脚手架入口文件
    bin/index.js 中引用即可。在 bin/index.js 文件顶部声明执行环境,添加#!/usr/bin/env node或者#!/usr/bin/node,这是告诉系统,下面这个脚本,使用nodejs来执行。
    #!/usr/bin/env node的意思是让系统自己去找node的执行程序。
    #!/usr/bin/node的意思是,明确告诉系统,node的执行程序在路径为/usr/bin/node
#!/usr/bin/env node
// CLI执行入口文件
import questions from './questions/index.js';

// 交互命令行输入的值
const config = await questions();

创建项目目录(即项目文件夹)和模板文件临时目录

上面的章节中用户已经输入了要创建的目录名称(createDir),那么接下来就是要在当前位置创建一个项目目录(即项目文件夹)和模板文件临时目录。

import fs from 'fs';
import chalk from 'chalk'; // chalk 美化输出

// 创建的项目路径
const projectPath = `./${config.createDir}`;
// 存放模板文件的目录路径
const templatesDirRootPath = `${projectPath}/templatesModulesDir`;

// 1. 先创建目标目录和模板文件临时目录
console.log(chalk.green(`根据createDir创建文件夹 -> ${projectPath}`));
fs.mkdirSync(projectPath);
fs.mkdirSync(templatesDirRootPath);
  • 这里为了输出效果的美观引入了chalk ,它同样需要安装
npm install chalk
  • fs是nodejs的核心模块之一,感兴趣的可以自己去学习一下。

获取远程仓库模板文件代码,存放到临时目录

接下来就是把存放在远程仓库中的自定义框架作为模板文件,使用git拉取下来,存放到临时目录。
这里我们使用 execa 子进程管理工具,依然是需要先安装。

npm i execa
import { execaSync, execa } from 'execa';
import ora from 'ora';

// 2. 获取远程仓库模板文件代码,存放到临时目录
let getGitRemoteResult = {}; // 拉取远程仓库结果
const getGitRemote = () => {
  const spinners = [ora('读取中...')];
  spinners[0].start();

  try {
    getGitRemoteResult = execaSync(
      `git`,
      [
        'clone',
        '-b',
        'master',
        'https://github.com/1045757307/front-react-frame.git',
      ],
      {
        cwd: templatesDirRootPath,
      }
    );
  } catch (err) {
    fs.rmdirSync(projectPath);
    console.error(err);
  }

  // console.log(chalk.blue('getGitRemoteResult:'), getGitRemoteResult);
  if (
    getGitRemoteResult.failed === true ||
    getGitRemoteResult.failed === undefined ||
    getGitRemoteResult.failed === null
  ) {
    spinners[0].fail('读取远程仓库失败!');
  } else {
    spinners[0].succeed('读取远程仓库成功!');
  }
};

getGitRemote();

上面拉取远程仓库代码子进程使用了 ora 来实现 loading 效果。

把临时目录里的模板文件代码copy到项目中

这里使用 fs-extra 复制文件,fs-extra需要单独安装

npm i fs-extra
import fse from 'fs-extra';

// 3. 把临时目录里的模板文件代码copy到项目中
const fsCopy = async () => {
  const spinners = [ora('创建模块中...')];
  spinners[0].start();
  try {
    console.log(chalk.blue('copy模板文件代码'));
    await fse.copy(
      `${templatesDirRootPath}/front-react-frame`,
      `${projectPath}`,
      (err) => {
        if (err) {
          console.error(err);
        } else {
          delDir();
          spinners[0].succeed('创建模块成功!');
          rewrite();
        }
      }
    );
  } catch (err) {
    fs.rmdirSync(projectPath);
    console.error(err);
  }
};

fsCopy();

删除模板文件中包含定制信息

projectPath这个路径下(也就是我们的项目目录)可能存在一些我们在开发中不需要的文件,至少现在就知道临时目录是不需要的,所以我们接下来的工作就是删除他们。

// 4. 删除模板文件中包含定制信息
const delDir = async () => {
  console.log(chalk.red('删除模板文件中包含定制信息'));

  // 删除模板文件临时目录
  await execa(`rm`, ['-rf', `${templatesDirRootPath}`], { cwd: './' });
  // 删除.git文件夹
  await execa(`rm`, ['-rf', `${projectPath}/.git`], { cwd: './' });
  // 删除package-lock.json
  await execa(`rm`, ['-rf', `${projectPath}/package-lock.json`], { cwd: './' });
};

重新写入部分文件

我们从远程仓库拉取的模板文件中肯定有某些地方是需要单独配置的,而在前面的章节中,用户通过交互式命令输入了很多配置项,那么下面就是这些配置项真正发挥作用的时候了。
我们要根据这些配置项重写部分文件,在这个项目中我重写了package.json.envindex.htmlBasicLayout.jsxMain/index.jsx等文件。
想要重写这些文件需要将对应的代码文件写入到我们的 CLI 项目的 ejs 模板中,在这里以package.json为例,其他重写的文件于此类似,详细可参考源码。
bin/templates目录下创建package文件夹以及package/index.jspackage.ejs文件。
bin/template/package/package.ejs

{
    "name": "<%= createDir %>",
    "version": "0.1.0",
    "private": true,
    "scripts": {
            ...
    },
    "devDependencies": {
            ...
        }
        ...
}

bin/template/package/index.js

import ejs from 'ejs';
import fs from 'fs';
import prettier from 'prettier';
import { getRootPath } from '../../utils/index.js';

export default ({ createDir }) => {
  const file = fs.readFileSync(getRootPath('template/package/package.ejs'));
  const code = ejs.render(file.toString(), { createDir });
  // 格式化
  return prettier.format(code, { parser: 'json' });
};
  • 这里通过ejs 文档来实现模板化文件内容。
  • 为了使代码更好阅读,我们可以用很多方式来格式化,比如手动使用IDE格式化、或者 通过 ESLint、stylelint格式化,又或者使用 Prettier,这里我们使用prettier来实现格式化代码。
    所以需要安装ejsprettier
npm install ejs prettier

我们定义了一个通用方法获取绝对路径,通用方法统一存放到bin/utils文件夹下。
bin/utils/index.js

// 主要是获取脚手架项目的物理路径函数
import path from 'path';
import { fileURLToPath } from 'url';
import process from 'process';

// 获取绝对路径
export const getRootPath = (pathUrl) => {
  const __dirname = fileURLToPath(import.meta.url);
  return path.resolve(__dirname, `../../${pathUrl}`);
};

以上准备工作完成以后,就可以在bin/index.js引入这些 ejs 模板,来修改远程仓库自定义项目中的某些配置了。

import packageJson from './template/package/index.js';
import envFile from './template/env/index.js';
import indexHtml from './template/indexHtml/index.js';
import BasicLayout from './template/BasicLayout/index.js';
import mainCompontent from './template/mainCompontent/index.js';

// 5. 重新写入部分文件
const rewrite = async () => {
  console.log(chalk.green('重新写入部分文件'));

  await execa(`rm`, ['-rf', `${projectPath}/package.json`], { cwd: './' });
  fs.writeFileSync(`${projectPath}/package.json`, packageJson(config));

  await execa(`rm`, ['-rf', `${projectPath}/.env`], { cwd: './' });
  fs.writeFileSync(`${projectPath}/.env`, envFile(config));

  await execa(`rm`, ['-rf', `${projectPath}/public/index.html`], { cwd: './' });
  fs.writeFileSync(`${projectPath}/public/index.html`, indexHtml(config));

  await execa(
    `rm`,
    ['-rf', `${projectPath}/src/common/components/Layout/BasicLayout.jsx`],
    { cwd: './' }
  );
  fs.writeFileSync(
    `${projectPath}/src/common/components/Layout/BasicLayout.jsx`,
    BasicLayout(config)
  );

  await execa(
    `rm`,
    ['-rf', `${projectPath}/src/common/components/Main/index.jsx`],
    { cwd: './' }
  );
  fs.writeFileSync(
    `${projectPath}/src/common/components/Main/index.jsx`,
    mainCompontent(config)
  );
};

将cli项目发布至 npm

到这里已经基本上完成了用nodejs编写cli工具的全部工作,接下来我们只需要在package.json文件里加上bin字段,就可以把它发布出去,以达到一键安装的目的。

"bin": {
    "create-react-frame-z": "bin/index.js"
  },

bin字段里面写上这个命令行的名字,也就是create-react-frame-z,它告诉npm里面的js脚本可以通过命令行的方式执行,以create-react-frame-z的命令调用,当然命令行的名字你想写什么都是你的自由。

  • 如果在测试阶段,可以在 cli 根目录执行 npm link 将 cli 模块链接到全局npm模块中,与 npm i -g 类似,但是他可以调试代码,因为 npm link 的形式可以理解为模块链接或者快捷方式。
  • 在正式阶段就要通过 npm publish 将 cli 项目发布到 npm , 通过 npm i -g 的形式安装到全局,然后执行 create-react-frame-z 即可执行我们的cli工具,这里我们展开来说下。
  1. 在 cli 项目根目录执行 npm init 来补充完整 package.json 的字段,根据提示填写即可。

    npm init

  2. 写好README.md,这是一个给大家描述你的包的markdown文件,会展示在你的npm包介绍首页。

  3. 如果你已经有npm账号,那么就用npm命令登陆一下,如果还没有npm 账号,需要去npm官网注册一个。


    npm login
  4. 登录完成以后就是执行npm publish把我们的cli项目发布出去。

    npm publish

    如果发布出现错误,请先去检查npm上是否有跟你同名的包,所以这里建议npm publish之前先去npm 官网搜索一下你要发布的包名,如果能搜到你就要换一个名字了,如果搜索结果为空,那么你很幸运,暂时还没有跟你同名的包。
    如果不是首次发布,version版本没有改变也会导致发布失败。一般情况下,一旦你要修改你已经发布后的代码,然后又要执行发布操作,务必到package.json里面,把version改一下,比如从1.0.0改为1.0.1,然后在执行npm publish,这样就可以成功发布了。


以上就是这篇文章的所有内容了,相信按照以上的步骤依次执行,你也可以编写自己的命令行工具,实现自定义框架的一键安装。

你可能感兴趣的:(使用nodejs编写cli(命令行)实现公共框架一键安装)