从0开发一个属于自己的前端脚手架

227 阅读4分钟

基本使用方式:

mycli create projectName

mycli add test

mycli add test -u aa/bb/cc

1.首先了解什么是脚手架?

网上一搜基本上很多讲解应该是都能看明白的,我就说一下我的理解吧! 前端脚手架就是一个用前端代码写的一个工具,其目的就是减少重复性的工作,提高开发者的开发效率。比如我们常用的vue-cli,create-react-app等都是脚手架,能够在项目前期快速搭建一个项目的模板,不懂的童鞋,自己理解一下吧~

2.搭建脚手架雏形

  • 新建一个空文件夹
  • npm init生成package.json文件
  • 根目录新建一个bin/index.js作为入口文件
  • 根目录新建lib文件夹存放脚手架相关代码
  • npm link
  • 测试命令是否正确link 以上为基本步骤
  1. 编辑bin/index.js

#! /usr/bin/env node是默认要求的,用系统中的node环境去执行这个文件

#! /usr/bin/env node

console.log('hello')
  1. package.json新增属性bin

mycli这个是你的脚手架命令,可更改

  "bin": {
    "mycli": "./bin/index.js"
  },
  1. 控制台执行npm link 命令,将mycli命令绑定到全局
  2. 测试命令是否正确link,出现hello证明没有问题啦

WX20211222-172430@2x.png

3.分步实现脚手架

3.1 配置create命令

yarn add commander,需要用到commander库解析用户传递的参数,配置脚手架命令 lib/index.js

const _package = require('../package.json');

// 解析用户的参数
const program = require('commander');

// 用法 mycli create projectName
program
  .command("create <project>") //配置命令
  .alias("C") //设置别名
  .description('create a project') //命令的描述
  .action((project,moreParams) => { //命令的动作
    console.log('这里是这个命令具体需要执行的事情')
    console.log('命令上的项目名称project',project)
    console.log('命令上的其他参数',moreParams)
    // .....
  })

// 解析用户传递过来的参数
program
  .version(_package.version)  //脚手架的版本
  .parse(process.argv);

到这里,一个简易命令已经生成啦~,后续只需要关注action中的执行事件 WX20211222-182147@2x.png

3.2 完成cerate命令执行事件

效果是:用户在终端输入 mycli create test命令创建项目后,会出现交互式命令行选择需要生成的模板,选择完成之后从gitlab或者github上拉取对应的资源,并且在当前命令行所执行的目录上生成一个test项目。

这里我们需要安装的依赖为以下三个:

"download-git-repo": "^3.0.2",
"inquirer,": "^8.2.0",
"ora": "3.4.0"
  • inquirer为交互式命令在终端选择,
  • ora可以在终端的输出内容并且有loading效果,
  • chalk是ora插件附带安装的,可以实现终端输出的内容颜色更改等,
  • download-git-repo可以在gitlab或者github上拉取代码 不会用的可以下去自己看看官方使用方式哟~

lib/index.js

修改之前的内容

program
  .command('create <project>') // 配置命令
  .alias('C') // 设置别名
  .description('create a folder') // 命令的描述
  .action((project,moreParams) => { // 命令的动作
+    const actionFn = require(path.resolve(__dirname, 'actions/createAction'));
+    actionFn(project,moreParams)
  });

lib/actions/createAction.js

// mycli create projectName
const ora = require('ora');
const chalk = require('chalk');
const Inquirer = require('inquirer');
// node库提供的可以将一个函数封装成promise形式
const { promisify } = require('util')
const downloadGitRepo = promisify(require('download-git-repo'))

const questionList = [
  {
    type: 'list',
    message: '请选择想生成的模板:',
    name: 'template',
    choices: ['create-react-app', 'Micro application', 'umi'],
  },
];

const downloadGitUrlMap = {
  "create-react-app":"direct:http://git.xxx.com/xxx.git",
  "Micro application": "direct:http://git.xxx.com/xxx.git",
  "umi": "direct:http://git.xxx.com/xxx.git",
}
const createActions = async (projectName,moreParams) => {
  // 获取当前命令所在目录的地址
  const projectPath = process.cwd()+`/${projectName}`
 
  // 设置控制台的交互式命令
  const answer = await Inquirer.prompt(questionList);

  // 设置等待提示语
  const spinner = ora('正在拉取模版中...\n').start();

  // 拉取模版代码
  /*
      // node中的fs文件系统
      const  { rm,stat }  = require('fs/promises');
      处理重复目录拉取模板的兼容问题,我就不写啦
      根据你自己的需求通过stat先判断projectPath是否存在
      存在就给删除 await rm(projectPath,{ recursive: true, force: true }),再拉取模板
      但是存在就rm有点隐患就是可能删除有意义的同名文件,建议可以通过inquirer询问一下是否删除
  */
  try {
    await downloadGitRepo(downloadGitUrlMap[answer.template],projectPath,{ clone: true })
    // 结束提示
    spinner.succeed(`${chalk.green(`${projectName}项目已创建成功`)}`);
  } catch (error) {
    console.log(error)
    spinner.fail(`${chalk.red(`${projectName}项目创建失败,建议检查是否已存在${projectName}项目`)}`);
  }
};

module.exports = createActions;

附上效果图:

WX20211223-165926@2x.png

WX20211223-170027@2x.png

3.3 完成add命令

效果是:mycli add test 或者 mycli add test -u aa/bb/cc 命令,在对应的目录下生成test文件,且里面包含了相对于的内容。比如index.tsx和index.less

yarn add ejs ,解析模板内容

lib/index.js新增命令

// 用法 mycli add folderName   或者 mycli add test -u aa/bb/cc
program
  .command('add <folderName>')
  .alias('A')
  .option('-u <url>','where do you want to add a folder')
  .description('add a folder') 
  .action((project,moreParams) => {
    const actionFn = require(path.resolve(__dirname, 'actions/addAction'));
    actionFn(project,moreParams)
  });

新增 lib/template/index.tsx.ejs模板

import React from 'react'
import './index.less'

const <%= name %> = () => {
  return (
    <div className="<%= name %>box">
      
    </div>
  )
}

export default <%= name %>

新增 lib/template/index.less.ejs

.<%= name %>box{

}

增加 lib/actions/addAction.js

const { promisify } = require('util')
const path = require('path')
const fs = require('fs')
const ejs = require('ejs')
const renderFile = promisify(ejs.renderFile)

// 递归创建目录
const mkdirFn = async (_path) => {
  // 是否存在该目录
  try {
    fs.statSync(_path).isDirectory()
    return true
  } catch (error) {}

  //不存在就递归创建,请熟悉path.dirname的用法
  if (mkdirFn(path.dirname(_path))) {
    fs.mkdirSync(_path)
    return true
  }
}

const addAction = async (project,moreParams) => {

  // 获取当前命令所在地址
  let currentPath = process.cwd()

  // 如果设置了创建路径,修改currentPath
  if(moreParams.u){
    currentPath = path.join(currentPath,moreParams.u)
  }

  //创建目录
  await mkdirFn(`${currentPath}/${project}`)

  //读取ejs模板
  const templateTsx = await renderFile(process.cwd()+'/lib/template/index.tsx.ejs',{name:project})
  const templateLess = await renderFile(process.cwd()+'/lib/template/index.less.ejs',{name:project})

  // 写入文件
  fs.writeFileSync(`${currentPath}/${project}/index.tsx`,templateTsx)
  fs.writeFileSync(`${currentPath}/${project}/index.less`,templateLess)
}

module.exports = addAction

以上可以根据实际的业务情况完善相应的内容,有时间可以了解以下插件的使用方式,内容很多