开发一个简单的前端脚手架

手写前端脚手架 why-cli


前言

  • cli脚手架工具就是拉取模板,编译模板
  • 常用的一些工具函数,项目的基本配置,可以放到模板里配置好

1.必备模块 npm 包

  • commander 命令行参数解析
  • inquirie 命令行交互工具,可以实现命令行的选择功能,输入功能
  • download-git-repo 从 github 下载模板
  • chalk 颜色
  • metalSmith 读取所有文件,实现模板渲染
  • consolidate 统一模板引擎
  • ora 命令行 loading 效果
  • axios
  • ncp 拷贝文件

2.工程创建

2.1创建目录


  • |--bin
    • |--www //全局命令执行文件
  • |--src
    • |-- create.js // create 命令逻辑
    • |-- config.js // config 命令逻辑
    • |-- main.js // 入口文件
    • |-- utils // 存放工具方法

2.2初始化项目

  • 生成 pakeage.json

    npm init -y
    
  • pakeage.json 中设置命令

    "bin": {
    
        "why-cli": "./bin/www"
    
    }
    
  • www 文件中,使用node环境执行此文件,引入main.js入口文件

    #! /usr/bin/env node
    
    require('../src/main.js)
    
  • 链接包到全局下使用

    npm link
    

    cmd 命令行中就可以执行 why-cli

2.2 使用 eslint

  • 安装 eslint
    cnpm i eslint --save
    
  • 初始化 eslint 配置文件
    npx eslint  --init
    

2.3 使用 commander 获取版本号

  • 安装 commander
    cnpm i commander --save
    
  • main.js入口文件
    const { program } = require("commander");
    // process.argv 就是命令行中传入的参数
    program.version('0.0.1').parse(process.argv)
    
  • 动态获取版本号
    const { program } = require("commander");
    // 动态获取版本号
    const { version } = require("../package.json");
    // process.argv 就是命令行中传入的参数
    program.version('0.0.1').parse(process.argv)
    

2.4 配置指令命令

  • 将指令写成一个对象形式数据,通过遍历,去注册命令

    const mapCommands = {
      create: {
        alias: "c",
        description: "create a project",
        examples: ["active-cli create <project-name>"],
      },
      "*": {
        alias: "",
        description: "not found command",
        examples: [],
      },
    };
    // 循环创建命令
    Reflect.ownKeys(mapCommands).forEach((command) => {
      program
        .command(command)
        .alias(mapCommands[command].alias)
        .description(mapCommands[command].description)
        .action(() => {
          console.log(command)
        });
    });
    
  • create 命令主要就是下载gitee仓库中的模板,如果有需要填写的信息,拿到用户填写的信息,渲染模板

    main.js

    const { program } = require("commander");
    program
    .command("create") // 命令名称
    .alias("c") // 命令别名
    .description("create a project") // 该命令的描述信息
    .action(() => {
      // ...process.argv.slice(3) 将命令行传入的参数展开
        require(path.resolve(__dirname, "create"))(...process.argv.slice(3));
      }
    });
    .parse();
    

    create.js

    module.exports = async (projectName) => {
      projectName
    })
    

2.5 拉取项目

  • 模板放在gitee.com 上,使用axios获取项目信息
  • 安装 axios
    cnpm i axios --save
    
  • 获取组织下的仓库模板
    // 获取组织下的所有仓库
    const fetchRepoList = async () => {
      const url = `https://gitee.com/api/v5/orgs/${organization}/repos?access_token=${access_token}&type=all&page=1&per_page=20`;
      const { data } = await axios(url);
      return data;
    };
    // 获取组织下的全部模板
    let repos = await waitFnLoading(fetchRepoList, "fetch template...")();
    repos = repos.map((repo) => repo.name);
    

2.6 ora inquirer

  • ora 命令行 loading 效果,inquirer 命令行交互,选择模板,填写信息等

  • 安装

    cnpm i ora inquirer --save
    
  • 选择模板信息

    // 选择一个模板
    const { repo } = await inquirer.prompt({
      name: "repo",
      type: "list",
      message: "please choise a template to create project",
      choices: repos,
    });
    // loading 效果
    const ora = (await import("ora")).default;
    const spinner = ora(message);
    spinner.start();
    const data = await fetchRepoList();
    spinner.succeed();
    
  • 获取版本信息和选择版本

    // 根据选择的模板,获取该模板所有的tags
    let tags = await waitFnLoading(fetchTagsList, "fetch tags...")(repo);
    tags = tags.map((tag) => tag.name);
    console.log(tags);
    
    let tag = "";
    if (tags.length > 0) {
      // 选择tag
      const { tagname } = await inquirer.prompt({
        name: "tagname",
        type: "list",
        message: "please choise a tag to create project",
        choices: tags,
      });
      tag = tagname;
    }
    

2.7 下载模板

  • 安装 download-git-repo
    cnpm i download-git-repo --save
    
  • 下载前先找个临时目录,存放下载的目录
  • 临时目录就是当前用户的.template文件夹中
    // 模板临时存储位置
    const tempDestination = `${
      process.env[process.platform === "darwin" ? "HOME" : "USERPROFILE"]
    }/.template`;
    
  • 开始下载
    /**
     * @description 下载模板
     * @param {string} repo 模板
     * @param {string} tag  标签
     * @return {string} 临时下载文件的目录
     */
    const downloadTemplate = async (repo, tag) => {
      let url = `direct:https://gitee.com/${organization}/${repo}`;
      if (tag) {
        url += `#${tag}`;
      }
      const tempDest = `${tempDestination}/${repo}`;
      await new Promise((resolve, reject) => {
        downloadGitRepo(url, tempDest, { clone: true }, function (err) {
          console.log(err ? "Error" : "Success");
          if (err) {
            reject(err);
          } else {
            resolve();
          }
        });
      });
      // 返回临时存储目录
      return tempDest;
    };
    

2.9 编译拷贝模板

  • 简单模板,直接使用ncp拷贝文件

  • 安装 ncp

    cnpm i ncp --save
    
  • 拷贝

    await ncpPromise(tempDest, path.join(path.resolve(), projectName));
    
  • 复杂模板,metalsmith 编译,用 consolidate.ejs 渲染

  • 安装

    cnpm i metalsmith ejs consolidate --save
    
  • 遍历所有的文件目录,编译模板,只要是模板渲染都需要

    await new Promise((resolve, reject) => {
      Metalsmith(__dirname) // 如果传入路径,默认遍历当前文件夹 src 目录
        .source(tempDest) // 设置相对目录
        .destination(path.resolve(projectName)) // 目标文件夹路径
        .use(async (files, metal, done) => {
          // 取出 ask.js
          // 用 inquirer, 让用户填写信息
          // 将信息挂载到metal.metadata()上,传递给下一个中间件使用
          // 删除 ask.js 文件
          const ask = require(path.join(tempDest, `ask.js`));
          const answer = await inquirer.prompt(ask);
          const tempMeta = metal.metadata();
          Object.assign(tempMeta, answer);
          delete files[`ask.js`];
          done();
        })
        .use(async (files, metal, done) => {
          // 从 metal.metadata() 拿到用户填写的信息
          // 取到js。json文件,是否存在<%=模板符号
          // 通过渲染函数 consolidate.ejs.render渲染内容,将渲染结果赋值给对应文件
          const answer = metal.metadata();
          Reflect.ownKeys(files).forEach(async (fileKey) => {
            if (fileKey.includes(".js") || fileKey.includes(".json")) {
              const contentsString = files[fileKey].contents.toString();
              if (contentsString.includes("<%=")) {
                // 渲染模板
                const renderContent = await renderPromise(
                  contentsString,
                  answer
                );
                // 用渲染结果,替换对应文件内容
                files[fileKey].contents = Buffer.from(renderContent);
              }
            }
          });
          done();
        })
        .build(function (err) {
          if (err) {
            reject(err);
          } else {
            resolve();
          }
        });
    })
    
  • 去掉 link

    npm unlink active-cli
    

3.项目发布

  • npm login
  • npm publish

4.常见问题

  1. npm publish 报错403,可能原因以下:
    1. 用了淘宝镜像源 - 换成npm的源 nrm use npm
    2. 包名重复 - 删掉之前的包,改个名字。
    3. npm账户没有验证邮箱 - 验证邮箱。
    4. vpn冲突 - 关掉所有vpn再次尝试。
  2. npm i active-cli -g之后,使用active-cli时报错:not found modules...
    可能原因就是 开发时用 cnpm xxx --save, 不使用cnpm xxx --save-dev
posted @ 2021-12-10 11:53  清水渡白吟堤你如风  阅读(323)  评论(0编辑  收藏  举报