文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

教你用NodeJs构建属于自己的前端脚手工具

2024-04-02 19:55

关注

一.前言

在日常开发中,我们经常会用到各种脚手架工具,如常用的vue和react脚手架工具:vue-cli、Create React App。只需要执行内置命令和选择内置条件就可以生成对应的项目模板。极大的提高了我们开发开发效率,所以我们能不能根据自己的日常业务构建属于自己的一套脚手架工具呢,答案是可以的。接下来步入本文章的主题

二.技术栈

三.特别说明

在开始之前,需要了解nodejs基础知识,常用shell执行命令,和javascript基础知识!!! 本文将从零到一完整讲解脚手架开发过程,以Vite + React18.0为模板案例,不会完整开发类似vue-cli 的所有功能!你可按照此文优化完善自己的脚手架代码。

四.构建项目

现在正式开始构建脚手架项目,可根据自己熟悉方式创建,直接创建项目文件夹

# 创建项目文件夹 名称为 my-cli 可自定义
mkdir my-cli
# 初始化 npm 根据自己的需求填写对应信息,也可以直接默认
npm init

五.安装依赖

yarn add commander execa inquirer chalk mkdirp
# or npm 安装 
npm install commander execa inquirer chalk mkdirp

六.目录说明

完整目录,如下所示,可根据目录设计添加对应文件。

my-cli # 项目名称
└── bin # 主目录
    ├── commands # 命令文件
    │   ├── options # 命令所有选项
    │   │   ├── create.js # 创建命令
    │   │   ├── help.js # 帮助命令
    │   │   ├── help.js # 导出所有命令文件
    │   ├── index.js # 导出command命令方法
    ├── inquirers # 交互式命令文件
    │   ├── options # 交互式命令所有选项
    │   │   ├── common.js # 公共交互式命令
    │   │   ├── react.js # react相关命令
    │   │   ├── index.js # 导出所有交互式命令文件
    │   ├── index.js # 导出command命令方法
    ├── templates # 所有模板文件
    │   ├── react # react相关模板
    │   ├── vue # vue相关模板
    ├── utils # 工具类
    │   ├── index.js # 工具类处理函数
    ├── index.js # 入口文件

完整代码:my-cli

七.实战

在开始前,我们需要修改package.json文件,增加"bin": "./bin/index.js":发布到npm设置执行入口文件; "type": "module":表示允许执行export、import操作;"start": "node ./bin/index.js":做本地测试使用,表示执行脚手架入口文件

{
  + "bin": "./bin/index.js",
  + "type": "module",
  "scripts": {
    - "test": "echo \"Error: no test specified\" && exit 1",
    + "start": "node ./bin/index.js"
  }
}

(一).入口文件

首先从入口文件开始分析,需要的模块逐步完善。目录路径:bin/index.js

#! /usr/bin/env node
// 引入 fs模 块
import fs, { mkdirSync } from "fs";
// 引入 path 模块
import path from "path";

// 引入 commands 相关命令方法
import commands from "./commands/index.js";
// 获取用户输入、选择项
let config = await command();

需要注意的是#! /usr/bin/env node 表示执行环境为node,引入fs文件系统模块,创建文件项目会涉及到文件相关读取写能力;引入path读取文件路径。commands执行命令;通过let config = await command();获取用户输入、选择项;这一步涉及到commands所以接下来开始分析:bin/commands/index.js用户输入命令模块

(二).命令文件

目录路径:bin/commands/index.js

// 引入 commander 命令
import { program } from "commander";
import chalk from "chalk";
// 导入 create创建命令 help帮助命令
import { create, help } from "./options/index.js";

export default async (call) => {
  return new Promise((resolve, reject) => {
    program
      .name(chalk.blue("my-cli"))
      .usage("[global options] command");
    // 版本信息
    program.version("1.0.0");
    // 帮助信息
    program.option("-F, --framework", "surport vue,react");
    // 创建项目命令
    create(program, (item) => {
      resolve(item);
    });
    // 帮助信息
    help(program);
    // 解析指令
    program.parse(process.argv);
  });
};

我们需要关注的是create、help分别是创建命令、帮助命令

(三).创建命令

create创建命令 ,文件路径:bin/commands/options/create.js



 import inquirer from "inquirer";
 import { changeTemplate, changeVariant, inputProjectName } from "../../inquirers/options/index.js";
 import  {InvalidArgumentError} from "commander";
 import chalk from "chalk";
 import { hasTemplate, getSupportTs } from '../../utils/index.js'
 export default (program, call) => {
   program
     .command("create")
     .argument("<build-method>", "build tools", validatBuildMethod)
     .argument("[app-name]", "app name", validatAppName)
     .description("create a new project powered by my-cli")
     .option("-t, --template <value>", "create a new project by template", validatTemplate)
     .action(async (method, projectName, option) => {
       let item = {};
        // 判断用户是否输入 projectName 
        if (!projectName) {
          item = await inquirer.prompt([inputProjectName(), changeTemplate(), changeVariant()]);
          // string转为boolean
          item.supportTs = item.supportTs === 'true' ? true : false
          return call && call({ method, ...item });
        }
       // 如果用户没有输入 模板参数,则提供项目类型选择 react/vue
       if (!option.template) {
         item = await getFramework(option);
				 item.supportTs = item.supportTs === 'true' ? true : false
       } else {
         item = option;
         item.supportTs = getSupportTs(item.template)
       }
       call && call({ method, projectName, ...item });
     });
 };
 
 
 function validatBuildMethod(val) {
   if (val === "vite") return val;
   else
     throw new InvalidArgumentError(
       chalk.red(
         `
         "<build-method>构建方式,只支持值为:${chalk.green(
           "vite"
         )}!请重新输入`
       )
     );
 }
 
 
 function validatAppName(appName) {
   var reg = /^[a-zA-Z][-_a-zA-Z0-9]/;
   if (!reg.test(appName)) {
     throw new InvalidArgumentError(
       chalk.red(`
       <app-name>项目名称必须以字母开头且长度大于2,请重新输入!`)
     );
   }
   return appName;
 }


 function validatTemplate(template) {
   if (hasTemplate(template)) return template
   else {
    console.log(chalk.white(`error: option '-t, --template <value>' argument '${template}' is invalid`))
   }
 }
 
 async function getFramework() {
   let answer = await inquirer.prompt([changeTemplate(), changeVariant()]);
   return answer;
 }

思路分析:

上图所示,主要分两种情况,当用户完整输入和部分输入参数判。

关于program详细分析

.command("create"):表示创建一个名为create的命令;

.argument("<build-method>", "build tools", validatBuildMethod):其中argument:表示参数、<build-method>:表示构建方式,必填参数、build tools:表示参数描述、validatBuildMethod:表示自定义校验方法;

[app-name]:表示可选项,项目名称。<>:表示必填参数,[]:表示可选项

.description("create a new project powered by my-project-cli"):表示命令描述

.option("-t, --template <value>", "create a new project by template"):表示命令参数、 "-t, --template <value>":表示用户可输入-t--template:表示template参数、"create a new project by template":表示参数描述;

.action(async (method, projectName, option) => {}):表示命令行为事件,其中method, projectName:分别代表<build-method>构建方式、<app-name>项目名称,和顺序关联,可根据自己需求增删参数、option:表示参数选项、也就是"-t, --template <value>"可根据自己需求定义。

hasTemplategetSupportTs:分别表示是否存在模板,获取是否支持ts,在bin/utils/index.js可查看其用法

当用户没有完整输入项目框架时,我们会提供changeTemplatechangeVariantinputProjectName这个时候,就需要用到inquirer交互式命令提供选择项。

(四).公共处理交互式命令

文件路径:bin/inquirers/options/common.js

import chalk from "chalk";
export const changeVariant = () => {
    return {
      type: "list",
      name: "supportTs",
      choices: [
        {
          name: 'true',
        },
        {
          name: 'false',
        }
      ],
      message:
        "Support TS(default by javascript)",
    };
  };

  export const inputProjectName = () => {
    return {
      type: "input",
      name: "projectName",
      default: "vite-app-project",
      validate: function (appName) {
        var done = this.async();
        var reg = /^[a-zA-Z][-_a-zA-Z0-9]/;
        if (!reg.test(appName)) {
          done(chalk.red(`<app-name>项目名称必须以字母开头且长度大于2,请重新输入!`));
        }
        done(null, true);
      },
      message:
        "Project name",
    };
  };

参数解释,完整参数文档inpuirer文档

其中changeVariant:选择是否支持ts;inputProjectName:输入项目名称

(五).选择项目框架交互式命令

文件路径:bin/inquirers/options/template.js



 export const changeTemplate = () => {
    return {
      type: "list",
      name: "template",
      choices: [
        {
          name: "react",
        },
        {
          name: "vue",
        },
      ],
      message: "Select a framework",
    };
  };

创建命令,到此结束,接下来分析help命令

文件路径:bin/commands/options/help.js


 import chalk from "chalk";

 export default (program) => {
   program.addHelpText(
     "after",
     `
       Run ${chalk.green(
         "my-cli <command> --help"
       )} for detailed usage of given command.`
   );
 };

addHelpText:表示添加帮助文字,"after":表示在之后,可选项“before”,效果图如下

(六).导出command命令

文件目录:bin/commands/option/index.js


import create from "./create.js";
import help from "./help.js";
export { create, help };

(七).导出inquirer交互式命令

文件目录:bin/inquirers/option/index.js


 import { changeTemplate } from "./template.js";
 import { changeVariant, inputProjectName } from './common.js'
 export { changeTemplate, changeVariant, inputProjectName };

(八).工具类代码

export function hasTemplate(template) {
    return ['vue', 'vue-ts', 'react', 'react-ts'].includes(template)
}

export function getSupportTs(template) {
  return ['vue-ts', 'react-ts'].includes(template)
}

这两个方法在react命令中使用到,补充!!!

八.本地测试

准备完以上代码后,我们开始本地测试,测试目的是检测是否能够正确的返回我们预设的结果,可根据自己的需求定义和完善脚手架。本文只是讲解demo流程!!!

(一).完整输入

完成输入测试包含正确输入、构建方式有误输入、项目名称不合规输入

js版本完成输入、默认为js版本

# react 版本
yarn start create vite app-test --template react
# or npm
npm run start create vite app-test--template react

效果图:

ts版本正确输入

# react-ts 版本
yarn start create vite app-test --template react-ts
# or npm
npm run start create vite app-test --template react-ts

效果图:

以上结果也就是我们入口文件:bin/index.jslet config = await command();获取的值

构建方法输入有误vite1:为错误输入❌  只支持vite方式构建,可根据自己的需求适配多种构建方式

yarn start create vite1 app-test
# or npm
npm run start create vite1 app-test

效果图:

项目名称不合规输入 1my-test-project:为不符合规范名称

yarn start create vite 1my-test-project --template react
# or npm
npm run start create vite 1my-test-project --template react

效果图:

(二).不完整输入

包含构建方式不输入、名称不输入、项目框架方式不输入

构建方式不输入会直接导致报错,重点是名称不输入、项目框架方式不输入,这时会给用户提供选择

项目名称不输入

yarn start create vite 
# or npm
npm run start create vite 

效果图

项目名称不输入会提供默认值和输入给用户,同时完成项目框架选择、和是否支持ts选择。完整截图如下:

这个结果也就是我们入口文件:bin/index.jslet config = await command();获取的值

项目框架方式不输入

yarn start create vite app-test 
# or npm
npm run start create vite app-test 

效果图:

不输入项目框架会提供选择,同时选择是否支持ts,完整截图如下:

到此结束测试,你可以根据这个思路完善自己的脚手架,接下来开始模板生成

九.导入Vite + react18模板

当我们能获取到用户输入选择的值后,就可根据用户输入的参数生成对应的项目模板,本文以vite+reat18为例

bin/templates目录下创建React模板文件,完整目录如下:

bin
├── templates # 所有模板文件
│   ├── react # react相关模板
│   │   ├── src # react主要文件
│   │   │   ├── App.css # App 样式文件
│   │   │   ├── App.jsx.ejs # App 模板文件
│   │   │   ├── index.css # 首页样式文件
│   │   │   ├── main.js.ejs # main 模板文件
│   │   │   ├── logo.svg # react logo文件
│   │   ├── .gitignore # git忽略配置文件
│   │   ├── index.html.ejs # react首页ejs模板
│   │   ├── package.json.ejs # package.json ejs模板
│   │   ├── tsconfig.json # ts 配置文件
│   │   ├── tsconfig.node.json # tsconfig node配置文件
│   │   ├── vite.config.js.ejs # vite.config ejs模板

具体代码,查看完整代码:github.com/hu-snail/my…

如果实现vite + vue3可参考此方法生成对应模板

模板技巧:不需要动态改变文件,直接使用原后缀名,例如:静态资源(图、视频、音等资源)、需要动态改变的文件,保留原来的后缀名,在其基础上添加.ejs:例如(package.jsonpackage.json.ejs)这样的好处是,知道原来的文件类型,方便更快的处理

十.创建生成模板代码

完成模板之后,开始重点环节,根据用户输入选择参数动态生成项目模板,跟着我的步骤完成生成模板语法

第一步:创建build文件夹

bin/创建build文件,用于存放生成模板文件和生成配置文件,目录如下:

bin
├── build # 生成模板文件
    ├── config.js # 模板配置文件
    ├── react.js # 生成react模板文件

第二步: config配置

因为脚手架内置了支持tsjs,所有存在差异化,具体配置如下:


export const jsignoreFile = [
    'tsconfig.json',
    'tsconfig.node.json'
]

其中模板中的tsconfig.jsontsconfig.node.jsonts才存在,所以在js需要忽略。可按照此方式区分文件之间的差异化。

第三步:react生成模板

准备完以上内容后,接下正式开始生成模板,文件路径:bin/build/react.js



import fs from 'fs'
import mkdirp from "mkdirp";
import { getFiles, copyFile, getCode } from '../utils/index.js'
import { jsignoreFile } from './config.js'

代码分析:

代码中导入了getFilescopyFilegetCode三个方法,分别代表获取文件、拷贝文件、获取代码。首先从这三个方法开始分析,文件路径:bin/utils/index.js

1).getFiles 获取文件

import fs from "fs";
let files = []
let dirs = []
export function getFiles(template, dir) {
const templatePath = `./bin/templates/${template}/`
const rootFiles = fs.readdirSync(templatePath, 'utf-8')
rootFiles.map(item => {
  const stat = fs.lstatSync(templatePath + item)
  const isDir = stat.isDirectory()
  if (isDir) {
    const itemDir = `${template}/${item}/`.replace(/react\//g, '')
    dirs.push(itemDir)
    getFiles(`${template}/${item}`, itemDir)
  } else files.push((dir ? dir : '') + item)
})
return {files, dirs}
}

代码分析:

该函数提供两个参数,分别是templatedir,表示模板、目录,同时在方法前定义了两个变量filesdirs分别表示文件、目录,主要作用区分文件和目录,这样方便用于生成目录和创建文件。

2).copyFile拷贝文件

import fs from "fs"; 
import { fileURLToPath } from "url";
const __dirname = fileURLToPath(import.meta.url);

 export function copyFile(rootPath, template, item) {
   const fromFileName = path.resolve(
     __dirname,
     `../../templates/${template}/${item}`
   );
   const toFileName = `${rootPath}/${item}`;
   const rs = fs.createReadStream(fromFileName, {
     autoClose: true,
     encoding: "utf-8",
     highWaterMark: 64 * 1024 * 1024,
     flags: "r",
   });
   const ws = fs.createWriteStream(toFileName, {
     encoding: "utf-8",
     flags: "a",
     highWaterMark: 16 * 1024 * 1024,
     autoClose: true,
   });
   rs.on("data", (chunk) => {
     const wsFlag = ws.write(chunk, "utf-8");
     if (!wsFlag) {
       rs.pause();
     }
   });
   ws.on("drain", () => {
     // 继续读取
     rs.resume();
   });
 
   rs.on("end", () => {
     ws.end();
   });
 }

代码分析:

该函数提供三个参数rootPathtemplateitem,分别表示根目录,模板名称、静态模板路径。

3).getCode获取代码

import ejs from "ejs";
import fs from "fs";

 export function getCode(config, templateName, templatePath) {
   const template = fs.readFileSync(
     path.resolve(__dirname, `../../templates/${templateName}/${templatePath}`)
   );
   const code = ejs.render(template.toString(), {
     ...config,
   });
   return code;
 }

代码分析:

该函数接受三个参数configtemplateNametemplatePath:分别表示配置信息、模板名称、模板地址

第四步 分析createReact函数

分析完工具函数代码之后,我们正式分析主函数createReact



import fs from 'fs'
import mkdirp from "mkdirp";
import { getFiles, copyFile, getCode } from '../utils/index.js'
import { jsignoreFile } from './config.js'
export const createReact = (config, rootPath) => {
   // 创建项目
   mkdirp.sync(rootPath)
   const { template, supportTs } = config
   const reactTemplate = (template === 'react' || template === 'react-ts') ? 'react' : ''
   const { files, dirs } = getFiles(reactTemplate)
   // 创建文件夹
   dirs.map(item => {
    mkdirp.sync(`${rootPath}/${item}`)
   })

   files.map(item => {
    const isEjs = item.indexOf('.ejs') !== -1
    // 对模板文件进行操作
    if (isEjs) {
       const fileTyep = supportTs ? 'ts' : 'js'
       // 去掉ejs后缀
       const newItem = item.replace(/.ejs/g, '')
       // json后缀名不需要处理
       const isJson = newItem.indexOf('.json') !== -1
       // 替换后缀名
       const newFilePath = isJson ? newItem : newItem.replace(/js/g, fileTyep)
       // 写入相关模板文件
      fs.writeFileSync(
         `${rootPath}/${newFilePath}`,
         getCode(config, reactTemplate, item)
       )
    } else {
       // 如果不是ejs模板,直接复制文件 
       if (supportTs) copyFile(rootPath, reactTemplate, item)
       else {
         // 判断是否存在js需要忽略的文件,即存在ts文件
         const hasTsFile = jsignoreFile.includes(item)
         if (!hasTsFile) copyFile(rootPath, reactTemplate, item)
       }
    }
   })
    return ''
}

代码分析:

该函数接受两个参数configrootPath:分别表示用户输入选择参数值配置、项目根目录。

其他项有代码注释,不一一赘述

第五步 模板动态配置

作为测试,我们对package.json.ejsindex.html.ejs进行动态配置,分别对应的目录bin/templates/react/package.json.ejsbin/templates/react/index.html.ejs

package.json.ejs:代码

{
  "name": "<%= projectName %>",
  "private": true,
  "version": "0.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
  },
  "devDependencies": {
    "@types/react": "^18.0.0",
    "@types/react-dom": "^18.0.0",
    "@vitejs/plugin-react": "^1.3.0",
    <%_ if (supportTs) { -%>
    "typescript": "^4.6.3",
    <%_ } -%>
    "vite": "^2.9.9"
  }
}

根据supportTs:判断是否需要"typescript",需要注意的<%_ -%>:语法会删除多余空行,如果使用<% %>:会导致留白,可以自行测试,"name": "<%= projectName %>":根据创建的项目名称生成

index.html.ejs:代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/src/favicon.svg" rel="external nofollow"  />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title><%= projectName %></title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

动态生成 <title><%= projectName %></title>

以上模板只是测试,可根据ejs模板语法,动态生成你需要的值

第五步 入口文件执行模板生成

接下来将在入口文件,执行我们的createReact函数

#! /usr/bin/env node

 import fs, { mkdirSync } from "fs";
 import path from "path";

 import command from "./commands/index.js";
 import { createReact } from './build/react.js'

 let config = await command();

 var currentPath = path.resolve("./");

 createReact(config, getRootPath())

 function getRootPath() {
    return `${currentPath}/${config.projectName}`;
 }

生成模板演示

现在可以看看具体效果

(一).js版本

yarn start create vite app-test -t react

效果图

生成目录

package.json文件

index.hml文件

(二).ts版本

yarn start create vite app-test -t react-ts

效果图

生成目录

package.json文件

index.hml文件相同

总结

到此这篇关于用NodeJs构建属于自己的前端脚手工具的文章就介绍到这了,更多相关NodeJs构建前端脚手工具内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     807人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     351人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     314人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     433人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     221人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-前端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯