如何自己动手撸一个前端脚手架

46 阅读3分钟

前言

身为一个前端开发,平时肯定没少用create-react-app,vue-cli脚手架之类的工具。但这类工具基本都是提供了比较基础功能,后续在实际的项目中大多都会自己根据项目的实际情况增加不少额外配置。在一个公司的前端团队,技术栈基本是统一的,在新启动项目后,大多都要复制脚手架脚本到该项目目录,然后剔除一些不必要的部分配置代码。那么假如在公司内部有这么一个脚手架,大家需要的直接只需要通过几个简单node命令即可生成,那岂不美哉,kpi又有了,哈哈(开个玩笑)。


WX20230313-165801@2x.png

期望有如下功能

  1. 可以供用户选择Vue的模版,还是react的模版
  2. 提供是否支持less还有scss的选项
  3. 提供是否需要ts的选项
  4. 提供是否支持eslint的选项

文件目录划分

bin
  -bin.js // 配置脚本
lib // 资源文件目录
  - ejs // 所需要的ejs模版目录
  - env // 放置.env文件目录
  - template // 模版文件目录
    - vue // vue相关配置的文件目录
    - react // react 相关配置文件目录
  - utils //工具函数目录    
-src // 逻辑代码目录

仓库地址


核心逻辑

  1. 解析命令行命令,转化为对应的参数
  2. 根据相关参数,然后生成相关配置文件即可

但是在生成文件的时候,需要根据不同参数,进行ejs(或其他模版引擎),进行编译最后一些文件内容由node.js动态编译生成即可。

依赖项

  • chalk
  • commander
  • ejs
  • inquirer
  • log-symbols

以上依赖项目,除了ejs,其他的均为解析命令,增加命令行交互体验插件

直接上干货

第一步:解析命令

根目录的package.json文件

{
  "name": "vite-cli-plus",
  "version": "0.0.1",
  "description": "",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "bin": {
    "vite-cli-plus": "./bin/bin.js"
  },
  "keywords": [
    "View",
    "Vue",
    "Cli",
    "Scss",
    "Less"
  ],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "chalk": "^5.2.0",
    "commander": "^10.0.0",
    "ejs": "^3.1.8",
    "inquirer": "^9.1.4",
    "log-symbols": "^5.1.0"
  }
}

其实这边就需要注意bin这个配置,输入了命令之后就等于是直接执行了bin/bin.js文件,然后bin.js会将命令解析位参数,我们拿到对应的参数就可以去执行不同的方法

// bin.js文件内容

#!/usr/bin/env node
import fs from 'fs';
import { program } from 'commander';
import logSymbols from 'log-symbols';
import chalk from 'chalk';
import { created } from '../src/index.js'; // 核心方法,后续会讲到

program.version(JSON.parse(fs.readFileSync('./package.json', 'utf8')).version, '-v, --version', '检查vite-cli-plus版本号')
program.option('-c, --create <arg1>', '创建的项目名称')

program.parse()
const options = program.opts();
if (options.create) {
    created(options.create);
} else {
    console.log(logSymbols.error, chalk.hex('#d60b46').bold('请输入项目名称')); // 控制台打日志
}

第二步:提供模版供用户筛选

这里就需要用到 inquirer这个库,可以提供给我们在控制台的选项供用户选择,最后返回给我们对应的参数

import logSymbols from 'log-symbols';
import chalk from 'chalk';
import inquirer from 'inquirer';
import fs from 'fs';
import path from 'path';
import { vueCreateFile } from './vue.js'; // 创建vue模版的方法,后面会讲到

export const created = async (projectName) => {
    if (fs.existsSync(`./${projectName}`)) { // 这边主要是校验文件目录是否已经存在
      	const dir = await inquirer.prompt([
			{
				type: 'confirm',
				name: 'dir',
				message: '文件目录已经存在,是否删除该文件夹?',
				default: true
			}
      	]);
      	if (!dir.dir) {
        	console.log(logSymbols.error, chalk.hex('#d60b46').bold('请先手动删除该文件目录,再进行初始化'));
        	return
      	}
      	fs.rmSync(`./${projectName}`, { recursive: true })
    }
    fs.mkdirSync(`./${projectName}`) // 创建文件目录
    console.log(logSymbols.success, chalk.blue('项目文件创建成功'));
    const template = await inquirer.prompt([
        {
			type: 'list',
			name: 'template',
			message: '请选择框架',
			choices: [
				'Vue3'
			]
        }
    ]);
    const styles = await inquirer.prompt([
        {
			type: 'list',
			name: 'style',
			message: '请选择CSS语言',
			choices: [
				'SCSS',
				'LESS',
				'都需要'
			]
        }
    ]);
    const lang = await inquirer.prompt([
        {
			type: 'confirm',
			name: 'lang',
			message: '是否需要TypeScript',
			default: true
        }
    ]);
	const esLint = await inquirer.prompt([
        {
			type: 'confirm',
			name: 'esLint',
			message: '是否需要ESlint',
			default: true
        }
    ]);
	if (template.template === 'Vue3') {
		await vueCreateFile(projectName, styles, lang, esLint) // 此处将用户选择的一些模版参数交给vueCreateFile方法去创建对应的文件
	}
}
 

第三步:编译文件生成vue模版的一些基础配置

此处用到了ejs模版,主要是为了方便根据一些参数,去编译生成不同的文件内容,也有很多的工具,是通过下载一些官方项目的github仓库的代码,直接套用官方维护仓库的一些模版配置项。当然这样的方式也各有利弊


利: 1.自己维护版本号,最起码可以保证生成的脚本是稳定可用的,不会存在官方仓库更新了之后,导致你的脚手架版本出现不可控的问题。 2.在特定的业务场景,比如公司内部固定使用某一个版本的框架等,那么自己固定模版内容,不会存在版本不兼容的问题

弊: 1.想使用新版本的一些特性不及时 2.需要自己花费一定的时间精力去维护一些模版配置和相关依赖的版本

第四步:编译文件生成脚本的的逻辑

  1. 准备好一些基础的vue脚手架的模版,比如vite-config的配置

2.通过ejs模版编译,注入一些变量

  1. 最后通过node.js操作一些文件,编译相关模版最后生成我们需要的配置脚本
import path from 'path';
import logSymbols from 'log-symbols';
import chalk from 'chalk';
import fs from 'fs';
import { compile } from '../lib/utils/utils.js';
/**
 * vue项目初始化文件方法
 * @param {*} projectName 项目名称 
 * @param {*} styles 选择的style语言
 * @param {*} lang 选择的开发语言
 * @param {*} esLint 是否需要EsLint
 */
export const vueCreateFile = async (projectName, styles, lang, esLint) => {
    const targetPath = path.resolve(path.resolve(), `./${projectName}`);
    const sourcePath = path.resolve(path.resolve(), './lib/template/vue');
    const packagePath = path.resolve(path.resolve(), './lib/ejs/vue-package.ejs');
    const envPath = path.resolve(path.resolve(), './lib/env');
    fs.cpSync(sourcePath, targetPath, { recursive: true, filter: (file) => {
        if (file.indexOf('.ejs') !== -1) {
            return false
        }
        return true
    }});
    fs.cpSync(envPath, targetPath, { recursive: true, filter: (file) => {
        return true
    }});
    try {
        const packageStr = await compile(packagePath, { 
          projectName: projectName,
          styles: styles.style,
          lang: lang.lang,
          esLint: esLint.esLint
        });
        const indexStr = await compile(`${sourcePath}/index.ejs`, { 
            lang: lang.lang
        });
        const mainStr = await compile(`${sourcePath}/src/main.ejs`, {});
        const viteStr = await compile(`${sourcePath}/vite.config.ejs`, {
            esLint: esLint.esLint
        });
        fs.writeFileSync(`${targetPath}/package.json`, packageStr, 'utf8');
        fs.writeFileSync(`${targetPath}/index.html`, indexStr, 'utf8');
        fs.writeFileSync(`${targetPath}/vite.config.js`, viteStr, 'utf8');
        if (lang.lang) {
            fs.writeFileSync(`${targetPath}/src/main.ts`, mainStr, 'utf8');
            const tsConfig = await compile(`${sourcePath}/tsconfig.ejs`, {});
            const tsNodeConfig = await compile(`${sourcePath}/tsconfig.node.ejs`, {});
            fs.writeFileSync(`${targetPath}/tsconfig.json`, tsConfig, 'utf8');
            fs.writeFileSync(`${targetPath}/tsconfig.node.json`, tsNodeConfig, 'utf8');
        } else {
            fs.writeFileSync(`${targetPath}/src/main.js`, mainStr, 'utf8');
        }
        if (esLint.esLint) {
            const esLintStr = await compile(`${sourcePath}/eslintrc.cjs.ejs`, {
                lang: lang.lang
            });
            fs.writeFileSync(`${targetPath}/.eslintrc.cjs`, esLintStr, 'utf8');
        }
    } catch (err) {
        console.log(logSymbols.error, chalk.hex('#d60b46').bold('文件编译错误'), err);
    }
}

完整代码的仓库地址

其实脚手架是在公司的项目的特定场景还可以进行不少的扩展,比如一个命令直接推送到对应的环境等(虽然jenkins也可以),统一公司内部前端工具,尽量减少大家代码的差异性。

请注意在本地开发过程中,执行的文件都是./bin/bnin.js文件,如执行代码 ./bin/bin.js -c demo 就是在当前目录下创建一个文件名为demo的项目目录

文章不易,走过路过,点个赞再走,哈哈