10分钟极速搭建——前端脚手架(多模板)

791 阅读4分钟

背景

你是否在工作中找个模板,要反复去代码仓库去拉取代码,但是现在只要简单几分钟,就可以快速实现像vue cli一样的功能,在终端选择对应的模板,生成对应的模板文件,同时支持任意拓展不同的模板。对于公司的前端基建的建设还是很有必要的

logo (3).png

github源码地址 github.com/ccj-007/web… 觉得不错可以点个star ⭐

开始

首先我们要知道脚手架需要对应的哪些包?

npmdescription
chalk在终端的文本样式修改
commander命令行交互界面
cross-spawn用于执行node的命令
ejs模板渲染可以更加细化的配合commander控制代码逻辑生成
figlet终端显示的logo
shelljs执行shell命令
ora转圈圈
inquirer跟终端交互,选择对应的模板类型
fs-extra封装node的文件api,更强大

了解了我们的工具包,下面我们的思路是输入我们的包名,能想vue一样生成对应的交互模板 ,命令行输入类似 vue create my-app生成对应的项目名的文件,然后生成多个模板选择,同时考虑文件夹是否已经存在,如果存在那么就覆盖,同时也像vue-cli一样可以获取版本信息等。当然也需要考虑加一些logo、chalk文本样式效果,还有进度条。然后最后按需要安装依赖,最后给个cd <文件夹> npm run start 的提示。

开启一个交互界面

QQ截图20220717164953.png

#! /usr/bin/env node  //意思用node来执行这个脚本文件, 意味着你可以通过create <project> 来创建文件
const program = require('commander')
const log = require('../lib/log')

//commander配置node命令交互界面
program
  .on('--help', () => {
    log.outLOGO()
    log.outCyanLog(`Run  roc <command> --help show details`)
  })
  .version('0.1.0')
  .command('create <name>')
  .description('create a new project')
  .option('-f, --force', 'overwrite target directory if it exist')
  .action((name, options) => {
    log.outLOGO()
    //在这个../lib/create中获取对应的输入的信息,项目名和选项,后面我们根据这个选项生成
    require('../lib/create.js')(name, options)
  })


program
  // 配置版本号信息
  .version(`v${require('../package.json').version}`)
  .usage('<command> [option]')

program.parse(process.argv)  // 解析命令

对终端的文本样式封装下吧

//.lib/log.js  样式的封装,logo,chalk
const figlet = require('figlet');
const chalk = require('chalk')

const outLOGO = () => {
  console.log('\r\n' + figlet.textSync('WEB-CORE-CLI', {
    font: 'Standard',
    horizontalLayout: 'fitted',
    width: 120,
    whitespaceBreak: true
  }));
}

const outCyanLog = (info) => {
  console.log(`\n ${chalk.cyan.bold(info)} \n`);
}

const outRedLog = (info) => {
  console.log(`\n ${chalk.red.bold(info)} \n`);
}

module.exports = {
  outLOGO,
  outCyanLog,
  outRedLog
}

根据用户交互生成模板

QQ截图20220717165109.png

QQ截图20220717165640.png

/**
 * @description cli
*/
const path = require('path')
const fs = require('fs-extra')
const inquirer = require('inquirer')
const ejs = require('ejs')
const spawn = require('cross-spawn');
const ora = require('ora')
const process = require('process')
const shell = require('shelljs');
const log = require('./log');

let tplList = ['h5', 'node-mock', 'vue3-ts', 'h5-vue2']  //对应的模板文件名
let excludeFileList = ['node_modules', 'dist']  //需要排除的文件
let noNpmList = ['h5', 'node-mock']  //不需要安装的依赖

module.exports = async function (name, options) {
  const cwd = process.cwd() //node 命令执行路径 
  const projUrl = path.join(cwd, name)
  //目录下存在此项目文件夹
  if (fs.existsSync(projUrl)) {
    //是否带有-f强制创建指令
    if (options.force) {
      await fs.remove(projUrl)
    } else {
      //todo: 询问用户是否确定要覆盖
      inquirer.prompt([
        {
          name: 'action',   //boolean
          type: 'list',
          message: 'Target directory already exists Pick an action:',
          choices: [
            {
              name: 'Overwrite',
              value: 'overwrite'
            }, {
              name: 'Cancel',
              value: false
            }
          ]
        }
      ]).then(async answer => {
        let { action } = answer
        if (!action) {
          return;
        } else if (action === 'overwrite') {
          // 移除已存在的目录
          await fs.remove(projUrl)
          createProject(name, projUrl)
        }
      })
    }
  } else {
    createProject(name, projUrl)
  }
}

/**
 * 创建项目文件
 *
 * @param {string} name
 * @param {string} projUrl
 */
const createProject = (name, projUrl) => {
  let choices = []
  tplList.forEach(item => {
    choices.push({ name: item })
  })
  //询问需要的模板
  inquirer.prompt([
    {
      name: 'tplname',
      type: 'list',
      message: '请选择一个模板使用: ',
      choices: choices
    }
  ]).then(async answer => {
    let { tplname } = answer
    if (tplname) {
      const destUrl = path.join(__dirname, '../', 'templates/', tplname);

      deepCopyFiles(destUrl, projUrl, name)  //复制文件

      log.outCyanLog(`目录: ${projUrl} 项目名:${name} 创建成功!`)

      startShell(name, tplname)  //安装依赖
    }
  })
}

/**
 *拷贝文件夹下所有文件
 *
 * @param {string} destUrl
 * @param {string} projUrl
 * @param {string} name
 */
const deepCopyFiles = (destUrl, projUrl, name) => {
  fs.mkdir(projUrl, { recursive: true }, (err) => {
    if (err) return

    fs.readdir(destUrl, (err, files) => {
      if (err) throw err;
      files.forEach((file) => {
        //判断是否是文件夹,排除不需要的文件
        if (!(excludeFileList.findIndex((f) => f === file) > -1)) {
          fs.stat(path.join(destUrl, file), (err, stats) => {
            if (err) return
            if (stats.isDirectory()) {
              deepCopyFiles(path.join(destUrl, file), path.join(projUrl, file), name)
            } else {
              // 使用 ejs 渲染对应的模版文件
              ejs.renderFile(path.join(destUrl, file), name).then(data => {
                // 生成 ejs 处理后的模版文件
                fs.writeFileSync(path.join(projUrl, file), data)
              })
            }
          })
        }
      })
    })
  })
}

/**
 * shell命令
 */
const startShell = async (name, tplname) => {
  //如果是不需要安裝依賴的
  if (noNpmList.findIndex((item) => item === tplname) > -1) {
    log.outCyanLog(`创建${tplname}成功!`)
    return
  }
  const spinner = ora('安装依赖中......').start();

  shell.cd(name);

  // 安装依赖
  const pnpmCommend = spawn('pnpm install', [], {
    stdio: 'inherit'
  });

  // 监听执行结果
  pnpmCommend.on('close', function (code) {
    // 执行失败
    if (code !== 0) {
      log.outRedLog('安装依赖失败')
      process.exit(1);
    }
    // 执行成功
    else {
      log.outCyanLog(`安装依赖成功!!\n cd ${name} \n npm run dev`)
    }
    spinner.stop()
  })
}

写入模板

在tempaltes文件中定义不同的模板,主要文件名要对应上面的tplList字段,根据需求定义

QQ截图20220717164359.png

调试看下!发个包

调试首先要修改package内的bin(全局包命令的入口)、file(需要打包的文件)、mian字段 (项目执行主入口), 在调试的时候其他我们可以直接用npm link直接软连接到node的npm文件夹中。通过 npm config get prefix 快速获取文件夹路径。同时也可以看下file是否成功过滤了文件。如果想在其他包中测试使用,建议执行npx link <你的路径>,当然你也可以取消链接npm unlink <你的路径>。

如果调试没问题,我们要确保name字段是唯一,版本递增,然后nrm切换npm官方源,登录后npm publish就大功告成 !

//package.json
{
  "name": "web-core-cli",
  "version": "1.0.3",
  "description": "All-in-one scaffolding, cli integrated with Vue3 ecological chain, cli of H5 page, mock template cli of Node",
  "main": "./bin/cli.js",
  "bin": {
    "web-core-cli": "bin/cli.js"
  },
  "keywords": [
    "cli",
    "allin",
    "vue",
    "template",
    "h5",
    "node"
  ],
  "scripts": {
    "cli": "node ./bin/cli.js"
  },
  "author": "chen",
  "license": "ISC",
  "dependencies": {
    "chalk": "^4.1.2",
    "commander": "^9.3.0",
    "cross-spawn": "^7.0.3",
    "ejs": "^3.1.8",
    "figlet": "^1.5.2",
    "fs-extra": "^10.1.0",
    "inquirer": "^7.3.3",
    "ora": "^5.4.1",
    "shelljs": "^0.8.5"
  },
}

如何部署到私人仓库 ?

mmexport1658048658754.png

😅 我们做个给公司的前端基建用的可不能直接发布到npm官网,那么其实我们可以使用这个包 Verdaccio, 配合docker安装下,然后注意在config.yaml写入配置,注意下路径,然后创建对应的用户名和账号,他也可以统一管理那你的账号数据在某个文件中。那么后面无非nrm add 你的ip地址,然后npm publish, 那么你的私有仓库就搭建上去了,不明白的小伙伴可以网上看看教程。

还有哪些优化的点?

1.其实可以跟vue一样有个默认配置和自定义配置的,当然目前这个还是个雏形,通过ejs模板的变量可以控制很多东西,这里可以多挖一些东西。

logo (3).png