🚀自定义属于你的脚手架并发布到NPM仓库

前言

我们在开发的过程中常常会使用到一些脚手架来帮我们快速构建项目模版,常用的脚手架命令有如下这些:

  • create-react-app
  • vue-cli
  • create-vite

我们同样也可以根据自己常用的、习惯的技术栈去自定义一个属于自己的脚手架,让自己用的更舒服。所以我根据自己的开发习惯,实现了一个基于vitereact的脚手架。本文主要实现的功能有:

  • 交互式命令行创建
  • 动态模板生成
  • 发布到npm仓库

GitHub地址:github.com/jayyliang/v...

整体流程

大概看一下整个项目的结构

  • bin 我们要实现的命令行命令
  • src 具体的代码实现
  • template 模版文件

下面简单介绍一下生成过程的整体流程,主要包括以下几点

  • 获取命令行参数
  • 动态生成文件
  • 代码美化
js 复制代码
//bin/create.js
#!/usr/bin/env node
const { PROMPT } = require("../src/constants");
const { copyFolderSync,format } = require("../src/exec");
const { getPrompt } = require("../src/interactive")
const path = require('path');
const run = async () => {
  const prompts = await getPrompt()
  //获取目录
  const projectName = prompts[PROMPT.NAME]
  const projectPath = path.join(process.cwd(), projectName)
  //生成模版
  copyFolderSync(path.join(__dirname, "../template"), projectPath, prompts)
  // 代码美化
  await format(projectPath)
  console.log(`🚀 项目地址:${projectPath}`)
}

run()

交互式命令行

这里主要用到的是inquirer这个库,它十分强大,可以很容易的帮我们创建一个交互式的命令行。比如我们希望创建的时候输入项目名称,选择Javascript/Typescript。就可以如下实现:

js 复制代码
const inquirer = require("inquirer");
const prompts = [
  {
    type: "input",
    name: PROMPT.NAME,
    message: "项目名称",
  },
  {
    type: "list",
    name: PROMPT.LANG,
    message: "JS/TS",
    choices: [ENUMS[PROMPT.LANG].JavaScript, ENUMS[PROMPT.LANG].TypeScript],
  },
];

const commandRes = await inquirer.prompt(prompts);

动态模版生成

在前面的命令行交互过程中,我们已经拿到了用户的各种输入。数据结构如下:

js 复制代码
{
  NAME: 'project-name',
  LANG: 'TypeScript',
  axios: 'y',
  mobx: 'y',
  LIB: [ 'antd', 'lodash', 'dayjs' ]
}

这个时候我们需要一个项目模版,大致的文件目录结构如下

整个脚手架的创建流程如下

  • 入口文件为/bin/create.js
  • 解析命令行输入
  • 根据输入递归解析模版文件夹,生成模版
  • 代码美化

动态模板生成

接下来就要根据命令行的输入去替换模版的内容,这里可以做一个约定,在模版中的js文件必须实现一个getContentgetExt方法,因为要根据不同的输入去生成不同的内容。而其他文件可以按需直接拷贝到目标目录中。

使用copyFolderSync方法去动态生成模版,它主要做了以下几件事情

  • 创建目标文件夹,在哪个目录下调用这个命令,目标文件夹就是这个目录(target
  • 递归遍历模版文件夹(source
    • 根据命令行传入的参数(params)过滤掉一些不需要拷贝的文件
    • 如果是js文件,则调用getContentgetExt动态获取到内容跟文件拓展名
js 复制代码
const copyFolderSync = (source, target, params) => {
  // 创建目标文件夹
  if (!fs.existsSync(target)) {
    fs.mkdirSync(target);
  }

  // 读取源文件夹
  const files = fs.readdirSync(source);

  // 遍历文件并逐一拷贝
  files.forEach(file => {
    const sourcePath = path.join(source, file);
    const targetPath = path.join(target, file);
    if (sourcePath.includes("api") && !params[DEPS.AXIOS.key]) {
      return;
    }
    if (sourcePath.includes("store") && !params[DEPS.MOBX.key]) {
      return;
    }
    if (
      (sourcePath.includes("tsconfig") || sourcePath.includes("vite-env")) &&
      params[PROMPT.LANG] !== ENUMS[PROMPT.LANG].TypeScript
    ) {
      return;
    }
    // 如果是目录,则递归拷贝
    if (fs.statSync(sourcePath).isDirectory()) {
      copyFolderSync(sourcePath, targetPath, params);
    } else {
      // 如果是文件,则直接拷贝
      const ext = path.extname(sourcePath);
      if (ext.substring(1) === "js") {
        const file = require(sourcePath);
        const { getContent, getExt } = file;
        const content = getContent(params);
        const ext = getExt(params);
        const fileInfo = path.parse(sourcePath);
        const name = `${fileInfo.name}.${ext}`;
        fs.writeFileSync(path.join(target, name), content, {
          encoding: "utf8",
        });
      } else {
        fs.copyFileSync(sourcePath, targetPath);
      }
    }
  });
};

以下是一个js文件的例子,旨在介绍params跟内容是如何交互的。

js 复制代码
const { PROMPT, ENUMS } = require("../../src/constants");

const getContent = params => {
  return `
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.${
    params[PROMPT.LANG] === ENUMS[PROMPT.LANG].JavaScript ? "jsx" : "tsx"
  }";
import "./global.less";
ReactDOM.createRoot(document.getElementById("root")${
    params[PROMPT.LANG] === ENUMS[PROMPT.LANG].JavaScript ? "" : "!"
  }).render(<App />);
  `;
};
const getExt = params => {
  return params[PROMPT.LANG] === ENUMS[PROMPT.LANG].JavaScript ? "jsx" : "tsx";
};
module.exports = {
  getContent,
  getExt,
};

这里的实现方式基本上是根据参数拼接模版字符串,返回给调用方,动态生成文件。

代码美化

由于模版文件的内容是字符串拼接的,所以生成目标文件后不太好看。这里在生成完之后调用了prettier对目标文件夹进行了一次代码美化。

  • 递归处理文件夹和文件
  • 根据不同的文件名后缀选择不同的解释器
  • 把美化后的内容重新写到文件中
js 复制代码
const getParser = filePath => {
  const ext = path.extname(filePath);

  switch (ext) {
    case ".js":
      return "babel";
    case ".ts":
      return "typescript";
    case ".jsx":
      return "babel";
    case ".tsx":
      return "typescript";
    case ".html":
      return "html";
    case ".json":
      return "json";
    default:
      return null; // 如果无法确定解析器,则返回 null
  }
};

const format = async folderPath => {
  const files = fs.readdirSync(folderPath);
  for (const file of files) {
    const filePath = path.join(folderPath, file);
    const isDirectory = fs.statSync(filePath).isDirectory();
    if (isDirectory) {
      await format(filePath);
    } else {
      const fileContent = fs.readFileSync(filePath, "utf-8");
      const parser = getParser(filePath);
      if (parser) {
        const formattedContent = await prettier.format(fileContent, {
          parser,
        });
        fs.writeFileSync(filePath, formattedContent, "utf-8");
      } else {
      }
    }
  }
};

发布到npm仓库

这个时候我们已经实现了这个脚手架工具,下面我们可以把它发布到npm仓库中,以便使用起来更加方便。如果你没有npm账号,可以去https://npmjs.com/ 注册一个账号。

然后命令行输入

  • npm login
  • npm publish

这样就可以发布到npm仓库中。这里需要关注的是你的package.json文件。

json 复制代码
{
    //包名称
  "name": "@jayliang/vite-react-cli",
  //包的版本号
  "version": "1.0.0",
  "description": "基于vite跟react的脚手架工具",
  // 这里表示你的包是否是公开的,以及发布的地址是什么
  "publishConfig": {
    "access": "public",
    "registry": "https://registry.npmjs.org/"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  //我们发布的是命令行命令,所以这里需要定义一个bin对象
  "bin": {
    "create": "bin/create.js"
  },
  "keywords": [
    "cli",
    "vite",
    "react"
  ],
  "author": "jayliang",
  "license": "MIT",
  "dependencies": {
    "inquirer": "^8.2.2",
    "prettier": "^3.1.1"
  }
}

成功发布到npm仓库之后,可以使用npm i -g @jayliang/vite-react-cli去安装这个包,然后执行命令npx @jayliang/vite-react-cli,就可以愉快的创建项目了~

最后

本文纯属抛砖引玉,提供一个自定义脚手架的思路。如果你也有这样的需求,可以参考本文的思路去实现。欢迎评论区交流~

相关推荐
outstanding木槿5 分钟前
JS中for循环里的ajax请求不数据
前端·javascript·react.js·ajax
酥饼~12 分钟前
html固定头和第一列简单例子
前端·javascript·html
所以经济危机就是没有新技术拉动增长了16 分钟前
二、javascript的进阶知识
开发语言·javascript·ecmascript
Bubluu27 分钟前
浏览器点击视频裁剪当前帧,然后粘贴到页面
开发语言·javascript·音视频
鎈卟誃筅甡39 分钟前
Vuex 的使用和原理详解
前端·javascript
呆呆小雅44 分钟前
二、创建第一个VUE项目
前端·javascript·vue.js
m0_748239331 小时前
前端(Ajax)
前端·javascript·ajax
Fighting_p1 小时前
【记录】列表自动滚动轮播功能实现
前端·javascript·vue.js
前端Hardy1 小时前
HTML&CSS:超炫丝滑的卡片水波纹效果
前端·javascript·css·3d·html
Domain-zhuo1 小时前
Git和SVN有什么区别?
前端·javascript·vue.js·git·svn·webpack·node.js