前言
公众号:【可乐前端】,期待关注交换,分享一些有意思的前端常识
咱们在开发的过程中经常会应用到一些脚手架来帮咱们疾速构建我的项目模版,罕用的脚手架命令有如下这些:
create-react-app
vue-cli
create-vite
咱们同样也能够依据本人罕用的、习惯的技术栈去自定义一个属于本人的脚手架,让本人用的更难受。所以我依据本人的开发习惯,实现了一个基于vite
和react
的脚手架。本文次要实现的性能有:
- 交互式命令行创立
- 动静模板生成
- 公布到
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
文件必须实现一个getContent
和getExt
办法,因为要依据不同的输出去生成不同的内容。而其余文件能够按需间接拷贝到目标目录中。
应用copyFolderSync
办法去动静生成模版,它次要做了以下几件事件
- 创立指标文件夹,在哪个目录下调用这个命令,指标文件夹就是这个目录(
target
) 递归遍历模版文件夹(
source
)- 依据命令行传入的参数(
params
)过滤掉一些不须要拷贝的文件 - 如果是
js
文件,则调用getContent
和getExt
动静获取到内容跟文件拓展名
- 依据命令行传入的参数(
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
,就能够欢快的创立我的项目了~
最初
本文纯属抛砖引玉,提供一个自定义脚手架的思路。如果你也有这样的需要,能够参考本文的思路去实现。欢送评论区交换~