前言

公众号:【可乐前端】,期待关注交换,分享一些有意思的前端常识

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

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

咱们同样也能够依据本人罕用的、习惯的技术栈去自定义一个属于本人的脚手架,让本人用的更难受。所以我依据本人的开发习惯,实现了一个基于vitereact的脚手架。本文次要实现的性能有:

  • 交互式命令行创立
  • 动静模板生成
  • 公布到npm仓库

GitHub地址:https://github.com/jayyliang/vite-react-cli

整体流程

大略看一下整个我的项目的构造

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

上面简略介绍一下生成过程的整体流程,次要包含以下几点

  • 获取命令行参数
  • 动静生成文件
  • 代码丑化
//bin/create.js#!/usr/bin/env nodeconst { 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。就能够如下实现:

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);

动静模版生成

在后面的命令行交互过程中,咱们曾经拿到了用户的各种输出。数据结构如下:

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

这个时候咱们须要一个我的项目模版,大抵的文件目录构造如下

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

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

动静模板生成

接下来就要依据命令行的输出去替换模版的内容,这里能够做一个约定,在模版中的js文件必须实现一个getContentgetExt办法,因为要依据不同的输出去生成不同的内容。而其余文件能够按需间接拷贝到目标目录中。

应用copyFolderSync办法去动静生成模版,它次要做了以下几件事件

  • 创立指标文件夹,在哪个目录下调用这个命令,指标文件夹就是这个目录(target
  • 递归遍历模版文件夹(source

    • 依据命令行传入的参数(params)过滤掉一些不须要拷贝的文件
    • 如果是js文件,则调用getContentgetExt动静获取到内容跟文件拓展名
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跟内容是如何交互的。

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对指标文件夹进行了一次代码丑化。

  • 递归解决文件夹和文件
  • 依据不同的文件名后缀抉择不同的解释器
  • 把丑化后的内容从新写到文件中
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文件。

{    //包名称  "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,就能够欢快的创立我的项目了~

最初

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