自制前端项目脚手架

2023-05-16

准备工作(一些常用库)

ora
可以用于表示当前模板的状态
在这里插入图片描述

const oraIcon = ora().start(`正在下载模板\n`);

oraIcon.fail(`模板下载失败\n`);
oraIcon.succeed("模板下载成功");

figlet
粉笔字体库
在这里插入图片描述

const font = figlet.textSync('CLI-TOOL', {
  font: 'bulbhead'
})

commander
用于在终端创建脚手架的执行指令

const program = require('commander')

program
  .command('init template')
  .description('初始化项目模板')
  .action(async() => {
	// 此处添加指令需要执行的逻辑
   }

inquirer
添加选择列表
在这里插入图片描述

inquirer.prompt({
  type: "list",
  message: "请选择创建项目类型",
  name: "select",
  choices: [{
      name: 'Electron项目模板',
      value: 'electron'
    },
    {
      name: 'PC项目模板',
      value: 'pc'
  }]
}).then((data) => {
	// 此处执行条件选择后的逻辑
}

log-symbols
为信息输出添加图标
![在这里插入图片描述](https://img-blog.csdnimg.cn/993d522d9192410da636c3d6fc3ffd9e.png

console.log(logSymbols.error, chalk.green("模板下载成功"))

chalk
添加文字颜色和样式
在这里插入图片描述

download-git-repo
下载远程仓库模板,因为我的脚手架中使用的是本地模板,所以没有用这个库,通常需要下载一些github上的公共模板的时候,需要通过这个库来调用

const download = require('download-git-repo')

downloa(repository, ProjectName, options, callback)

repository:远程仓库地址

http://github.com:hyf940760301/cli-template#main (地址需要按照这种格式来定义,代码存放的网站地址:用户名/项目名#分支名)

projectName:存放下载的文件路径,可以直接是文件名,默认是当前目录

options:配置项,如{clone: boolean}表示用http download还是git clone下载

callback:回调函数,可以在里面对操作过程进行一些设置,比如添加文本颜色,文案内容或增加一些图标等


实现脚手架的搭建

1. 初始化项目
本地创建文件夹后
进入文件夹根目录,npm初始化配置,生成package.json文件

cd ./cli-tool
npm init

2. 创建脚手架入口文件
一般在根目录创建bin文件夹
在bin文件夹中创建index.js
对不不是很复杂的脚手架,index.js文件中就是我们脚手架实现的全部核心代码了

// ./bin/index.js

#!/usr/bin/env node  // 注意:这一行一定要添加到你的入口文件顶部,否则在关联本地或者上传仓库后,执行指令会出现指令报错的情况。
console.log('this is cli-tool')

3. 修改入口指向,创建本地符号链接后测试指令
修改package.json中的内容

// package.json
{
  "name": "cli-tool",
  "version": "0.0.1",
  "description": "前端脚手架",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "watch": "npm run compile -- --watch"
  },
  "author": "xxx",
  "license": "ISC",
  // 注:这里需要添加的是脚手架运行的指令以及指向的入口文件
  "bin": {
    "cli-tool": "./bin/index.js"
  },
  "dependencies": {
    "babel-cli": "^6.26.0",
    "babel-env": "^2.4.1",
    "chalk": "^3.0.0",
    "commander": "^5.0.0",
    "figlet": "^1.5.2",
    "glob": "^8.0.3",
    "inquirer": "^7.1.0",
    "log-symbols": "^3.0.0",
    "ora": "^4.0.3"
  },
  "devDependencies": {
    "babel-cli": "^6.26.0",
    "babel-core": "^6.26.3",
    "babel-env": "^2.4.1"
  }
}


添加关联本地的符号链接,测试指令运行
(在项目的根目录执行, 然后就可以在我们本地C盘中node文件夹下找到我们新添加的指令了)
在这里插入图片描述

本地全局运行指令,查看打印结果
在这里插入图片描述

4. 走通整个流程后,我们开始改造入口文件,丰富脚手架的内容

// ./bin/index.js

const fs = require('fs')
const ora = require('ora')
const path = require('path')
const glob = require('glob')
const chalk = require('chalk')
const figlet = require('figlet')
const process = require('process')
const program = require('commander')
const inquirer = require('inquirer')
const logSymbols = require('log-symbols')

program
  .version('0.1.0') // -v 或者 --version 的时候会输出该版本号

// 添加脚手架执行指令:cli-tool init template
program
  .command('init template')
  .description('初始化项目模板')
  .action(async() => {

    // 打印粉笔字
    const font = figlet.textSync('CLI-TOOL', {
      font: 'bulbhead'
    })

	// 第一次选择列表,选择Electron或者PC端前端项目模板
    inquirer.prompt({
      type: "list",
      message: "请选择创建项目类型",
      name: "select",
      choices: [
		{
	      name: 'Electron项目模板',
	      value: 'electron'
	    },
	    {
	      name: 'PC项目模板',
	      value: 'pc'
	    }
	  ]
    }).then((data) => {
      console.log(`\n`)
      if (data.select === 'electron') {     
        // 等待第一次选择结束后,执行第二次选择列表,这里因为只有两个列表选择,所以就直接在then()方法里执行了下一个promise
        // 如果存在多个选择列表,建议使用Promise.all方法来实现
        inquirer.prompt(defaultData.project).then((answers) => {
          // 拷贝文件夹,将本地目标文件夹模板直接复制到当前新建的文件夹下
          // 关于模板拷贝的细节,下一部分会把这段代码单独拎出来,这里先大致浏览一下就行
          function copyDir(src, dist, callback) {
            fs.access(dist, function(err){
              if(err){
                // 目录不存在时创建目录
                fs.mkdirSync(dist);
              }
              _copy(null, src, dist);
            });
          
            function _copy(err, src, dist) {
              if(err){
                callback(err);
              } else {
                try {
                  const paths = fs.readdirSync(src)
                  paths.forEach(function(_path) {
                    const _src = path.join(src, _path);
                    const _dist = path.join(dist, _path);
                    fs.stat(_src, function(err, stat) {
                      if(err){
                        callback(err);
                      } else {
                        // 判断是文件还是目录
                        if(stat.isFile()) {
                          fs.writeFileSync(_dist, fs.readFileSync(_src));
                        } else if(stat.isDirectory()) {
                          // 当是目录是,递归复制
                          copyDir(_src, _dist, callback)
                        }
                      }
                    })
                  })
                } catch (err) {
                  callback(err)
                }
              }
            }
          }


          // 1. 获取当前命令所在目录
          const dirPath = process.cwd();
          console.log(dirPath)

          // 2. 扫描当前目录, 判断是否有同名文件夹存在
          const fileList = glob.sync('*')
          console.log(fs.readdirSync(dirPath))

          if (fileList.some((element) => element === answers.name)) {
            console.log(logSymbols.error, `当前目录下存在同名文件夹\n`)
            console.log(logSymbols.error, `创建失败\n`)
          } else {
            // 3. 如果没有,创建文件夹
            try {
              fs.mkdirSync(`${dirPath}${answers.name}`)
              console.log(logSymbols.success, `目录创建成功\n`)

              // 开始下载模板
              const oraIcon = ora().start(`正在下载模板\n`);

              // 4. 创建好文件夹后,复制本地模板到创建的目标文件夹
              // __dirname:返回运行文件所在的目录 E:\dbt\cli-tool\bin
              const dirName = __dirname.split('')
              // 目的是为了删除目录中的\bin部分
              dirName.splice(-4, 4)
              const resultDirName = dirName.join('')

              // 5. 复制目标文件夹内容
              copyDir(path.join(resultDirName, 'packages'), path.join(dirPath, answers.name), (err) => {
                if (err) {
                  oraIcon.fail(`模板下载失败\n`);
                } 
              })

              setTimeout(() => {
                oraIcon.succeed("模板下载成功");
                
                console.log(chalk.green(`
======================================================\n
    进入项目根目录:  cd ./${answers.name}\n
    安装项目依赖:  yarn install\n
    启动项目:  yarn run serve 或 npm run serve\n
======================================================
              `))}, 2000)
              
            } catch (err) {
              console.log(logSymbols.error, chalk.green("目录创建失败"))
            }
          }
        })
      }
    })
  })


// 没有任何命令的时候输出使用帮助
if (!process.argv.slice(2).length) {
  program.outputHelp()
}

program.parse(process.argv)

5. 我的脚手架实现思考
在实现脚手架的过程中,我个人觉得基本上可以分为两种方式:

  1. 先下载一个通用的公共模板,在公共模板的基础上,将结合业务的配置以及新增文件添加到公共模板当中,我有大致看过vue-cli的代码结构,很复杂,但是基本的结构也是这种模式,通过在一个基础模板上增加各种配置项来实现定制化的项目结构。
  2. 第二种方式其实是我自己想到的一种方式,将需要的项目模板整体添加在脚手架工具中(删除不必要的文件项,比如node-modules),通过node的文件系统来实现将项目模板整体复制到本地目标的路径当中。相比于第一种来说,实现成本比较低,我认为比较适合需要快速成型一个直接可用的内部项目结构。
// 第二种实现方式的核心代码逻辑
// ./bin/index.js

// 实现将指定项目模板文件夹复制到目标路径当中
// src:需要复制的项目模板根路径(node中通常使用绝对路径更保险一点),dist:本地新建文件夹的根路径,callback:产生错误时的回调
function copyDir(src, dist, callback) {
  // 获取指定目录的权限,不指定第二个参数则为默认的F_OK,判断新建文件夹是否存在,不对rwx(读写)权限做任何说明
  fs.access(dist, function(err){
    if(err){
      // 目录不存在时创建目录
      fs.mkdirSync(dist);
    }
    _copy(null, src, dist);
  });

  function _copy(err, src, dist) {
    if(err){
      callback(err);
    } else {
      try {
      	// 获取当前项目模板目录下一层的所有子目录和子文件数组
        const paths = fs.readdirSync(src)
        paths.forEach(function(_path) {
         // 实现路径拼接
          const _src = path.join(src, _path);
          const _dist = path.join(dist, _path);
          // 获取当前目标文件或文件夹信息
          fs.stat(_src, function(err, stat) {
            if(err){
              callback(err);
            } else {
              // 判断是文件还是目录
              if(stat.isFile()) {
              	// 实现文件读写,将项目模板中的对应文件读出后,写入对应路径下对应的文件中,实现对应文件创建和内容复制
                fs.writeFileSync(_dist, fs.readFileSync(_src));
              } else if(stat.isDirectory()) {
                // 当是目录是,递归复制
                copyDir(_src, _dist, callback)
              }
            }
          })
        })
      } catch (err) {
        callback(err)
      }
    }
  }
}


 // 1. 获取当前命令所在目录,即本地打开控制台时的目标路径
const dirPath = process.cwd();

// 2. 扫描当前目录, 判断是否有同名文件夹存在
// glob.sync("*")会获取当前命令所在路径下的子目录数组,也可以用fs.readdir(dirPath)替代
const fileList = glob.sync('*')

// 这里的answer.name就是我们在执行脚手架选项时输入的内容
if (fileList.some((element) => element === answers.name)) {
  console.log(logSymbols.error, `当前目录下存在同名文件夹\n`)
  console.log(logSymbols.error, `创建失败\n`)
} else {
  // 3. 如果没有,创建文件夹
  try {
    fs.mkdirSync(`${dirPath}${answers.name}`)
    console.log(logSymbols.success, `目录创建成功\n`)

    // 开始下载模板
    const oraIcon = ora().start(`正在下载模板\n`);

    // 4. 创建好文件夹后,复制本地模板到创建的目标文件夹
    // __dirname:返回运行文件所在的目录 E:\dbt\cli-tool\bin,所以为了方便也可以直接把入口文件放在根路径,这样就少了一层/bin
    const dirName = __dirname.split('')
    // 目的是为了删除目录中的\bin部分
    dirName.splice(-4, 4)
    const resultDirName = dirName.join('')

    // 5. 复制目标文件夹内容
    copyDir(path.join(resultDirName, 'packages'), path.join(dirPath, answers.name), (err) => {
      if (err) {
        oraIcon.fail(`模板下载失败\n`);
      } 
    })

    setTimeout(() => {
      oraIcon.succeed("模板下载成功");
      
      console.log(chalk.green(`
======================================================\n
进入项目根目录:  cd ./${answers.name}\n
安装项目依赖:  yarn install\n
启动项目:  yarn run serve 或 npm run serve\n
======================================================
    `))}, 2000)
    
  } catch (err) {
    console.log(logSymbols.error, chalk.green("目录创建失败"))
  }
}

最后: 其实我的这个方法比较简单粗暴,在没有那么复杂的需求前提下,实现成本更低一些,但是势必性能以及脚手架体积会随着项目模板不断地添加变得更大,而且没有办法优化,这一点比较致命,因为实现的本质就是文件夹的整体复制,所以比较适合小型团体,如果是对于十几种项目类型甚至更多的话,就不太适合了,还是得老老实实的按照第一种通用的方式来进行定制化。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

自制前端项目脚手架 的相关文章

  • 模式识别—判别函数分类法(几何分类法)

    目录 统计模式识别之判别分析判别函数定义解释样例 判断函数正负值的确定确定判别函数的两个因素 线性判别函数一般形式性质两类情况多类情况 小结 广义线性判别函数目的 线性判别函数的几何性质模式空间与超平面概念讨论小结 权空间与权向量解概念线性
  • 【脚本】echo 输出赋值给变量

    链接 xff1a http zhidao baidu com link url 61 FMhso6Hf4eeRQN7p2qqzLOBAYPwh6yMJCWOvgmBFTDYWAEZ9ceuREtWhggxtcYG1iBhaJgqrcU7ad
  • 联邦学习 - 基础知识+白皮书+杨强教授讲座总结+同态加密+ 差分隐私

    联邦学习 兴起原因概念分类横向联邦学习纵向联邦学习联邦迁移学习 优势系统架构联邦学习与现有研究的区别联邦学习与差分隐私理论的区别联邦学习与分布式机器学习的区别联邦学习与联邦数据库的关系 联邦学习的最新发展及应用 xff08 2019第四届全
  • boomlab 实验 炸弹实验 系统级程序设计 CMU

    MENU boomlab还有30s到达实验1Step1 反汇编vim大法检查boom原因gdb调试出结果examinequit 实验二分析汇编语言ENDING 实验三答案 实验四func4 实验五实验六gdb调试 答案汇总ENDING问题解
  • CSAPP Lab:attacklab

    大小尾端 首先关于这个 xff0c 我一直没记清楚 xff0c 所以做个总结 xff1a 在裘宗燕翻译的 程序设计实践 里 xff0c 这对术语并没有翻译为 大端 和小端 xff0c 而是 高尾端 和 低尾端 xff0c 这就好理解了 xf
  • Advances and Open Problems in Federated Learning 总结翻译

    摘要 联邦学习 FL 是一种机器学习设置 xff0c 在这种设置中 xff0c 许多客户 例如移动设备或整个组织 在中央服务 器 例如服务提供商 的协调下协作地训练模型 xff0c 同时保持训练数据分散 FL体现了集中数据收集和最 小化的原
  • Multi-Center Federated Learning

    Multi Center Federated Learning Motivation 现有的联合学习方法通常采用单个全局模型来通过汇总其梯度来捕获所有用户的共享知识 xff0c 而不管其数据分布之间的差异如何 但是 xff0c 由于用户行为
  • No Fear of Heterogeneity: Classifier Calibration for Federated Learning with Non-IID Data

    No Fear of Heterogeneity Classifier Calibration for Federated Learning with Non IID Data Existing Methods for non IID da
  • Three scenarios for continual learning

    Three scenarios for continual learning Standard artificial neural networks suffer from the well known issue of catastrop
  • MQ2烟雾传感器

    1 MQ 2气体传感器所使用的气敏材料是在清洁空气中电导率较低的二氧化锡 SnO2 当传感器所处环境中存在可燃气体时 xff0c 传感器的电导率随空气中可燃气体浓度的增加而增大 使用简单的电路即可将电导率的变化转换为与该气体浓度相对应的输出
  • alembic

    alembic是sqlalchemy的作者开发的 用来做ORM模型与数据库的迁移与映射 alembic使用方式跟git有点了类似 xff0c 表现在两个方面 xff0c 第一个 xff0c alembic的所有命令都是以alembic开头
  • VScode远程免密登录

    安装配置python环境 xff1a 用VScode配置Python开发环境 xiaoj wang 博客园 cnblogs com VScode免密登录远程服务器 VS code ssh免密登陆 1 xff09 windows 下 xff0
  • linux虚拟机和主机能相互ping通,linux却不能访问外网

    linux虚拟机和主机能相互ping通 xff0c linux却不能访问外网 下面是试错过程 修改ifcfg eth0 xff08 名字可能不一样 xff09 vi etc sysconfig network scripts ifcfg e
  • 树莓派:树莓派的各个引脚

    由于第一次接触树莓派 xff0c xff0c xff0c emmmm xff0c 仔细写 xff0c 奥里给 3 3V 5V xff08 VCC xff09 xff1a 显然是电源正极啦 GND xff1a 接地用 xff0c 负极负极负极
  • 不分类工具:sd卡格式化工具安装教程

    下载地址 xff1a https www sdcard org downloads formatter 4 eula windows 进入上面这个链接 xff0c 你会看到满上面都是字 xff0c 有一个download xff0c 点完还
  • 不分类工具:Win32 DiskImager安装教程

    下载地址 xff1a http sourceforge net projects win32diskimager 这个也是很普普通通的下载安装 1 直接 download 2 双击安装文件 xff0c 弹出如下框 xff0c 选择我同意 x
  • Meta-Learning: Learning to Learn Fast

    Meta Learning Learning to Learn Fast 元学习 学习如何学习 译 原文 本文与原文基本没有区别 xff0c 仅供个人学习记录 电子笔记本 前言 xff1a 元学习解决 xff1a 遇到没有见过的任务 xff
  • 解决 Docker 容器时间与本地时间不一致的问题

    Linux 通过 Date 命令查看系统时间 xff0c 得到如下结果 xff1a root 64 iZ8vbg6m7f5ntzibw3t4huZ date Mon Aug 26 12 24 58 CST 2019 但是在 Docker 容
  • 记录ssh 和vnc命令

    ssh windows是客户端 linux是服务端 在windows powershell 输入 ssh rikirobot 64 192 168 x xxx xff08 ip地址 xff09 VNC Viewer 参考文章 xff1a 1

随机推荐

  • Redux源码解析(部分)

    相信用过React的小伙伴对于Redux一定不陌生 xff0c A Predictable State Container for JS Apps xff0c 这是官方文档对于Redux的定义 xff0c 即一款适用于JS Apps的可预测
  • Axios源码解析(部分)

    从 Github 上把 Axios 项目的 master 分支拷贝到本地 xff0c 用编辑器打开项目目录 首先我们先解析一下整个 Axios 项目的一些关键的文件结构 对照项目的文件目录 xff0c 梳理一下其中的一些关键文件夹以及它的作
  • 解析Javacript的事件循环机制

    前言 作为最受欢迎的Web编程语言 xff0c Javascript的单线程执行是其一大特点 xff0c 也就是说在同一时间只能有一个任务处于执行状态 xff0c 而后续的任务需要等待当前任务处理完毕后才能继续处理 xff0c 而在当前编程
  • 关于项目打包相关的优化问题

    本文主要是针对项目在打包过程中使用的一些Webpack相关的打包手段 1 首先在真正处理我们的打包之前 xff0c 我们可以通过安装插件来具体查看一下我们目前项目中的打包模块以及打包时间 打包后各个模块的可视化工具 webpack bund
  • 英语词汇、短语、语句积累

    rules of thumb 经验法则
  • 关于Webpack plugins插件的两种写法

    类写法以及require方式引入 span class token keyword class span span class token class name HelloWorld span span class token punctu
  • 关于mini-css-extract-plugin在使用过程中出现冲突的问题

    今天在优化打包的过程中 xff0c 运行npm run build后 xff0c 总是会出现冲突的警告信息 xff0c 而且对于我的这个项目而言出现了几十条的冲突提示 如下 问题排查 首先我以为可能是由于我新引入的优化方面的插件导致了原先的
  • 关于使用Antd中的DatePicker出现的日期格式转化问题(Dayjs和Momentjs)

    在测试过程中发现了一个比较有意思的bug问题 xff0c 我们使用的是antd中的DatePicker组件 xff0c 当时间选择框存在已经设定的初始值后 xff0c 点击时间选择框直接报错 xff0c 但是当清除内容或者处于新建没有默认值
  • Javascript基础知识整理—1

    1 JS数据类型 原始数据类型 xff1a boolean xff0c string xff0c null xff0c undefined xff0c symbol xff0c bigint xff0c number 引用类型 xff1a
  • React基础

    1 Context全局值 链接地址 链接地址 目的是为了避免一些外部的传参向下传递时需要通过一层层的组件 span class token comment defaultValue只有在找不到附近的Provider时才会起作用 span s
  • git基础知识

    1 对当前commit的内容进行修正 如果发现commit的内容有问题想要修改 xff0c 正常做法可以重新再commit一次 通过amend可以直接将commit和暂存区的内容进行合并 xff0c 就不需要再重新commit一次了 ame
  • Typescript基础

    1 Pick和Omit 源码地址 相似点 xff1a 都是对接口进行剪裁 keyof 操作符常和接口结构一起使用 xff0c 得到一组对象键值的字面量类型组成的联合类型 xff0c 如 a b c 我们也常用 keyof any 表示成员未
  • Javascript基础知识整理—2

    1 节流和防抖的实现 https blog csdn net weixin 45709829 article details 123910592 防抖 Debounce 在设定的n秒内只会执行最新的函数 防抖实现 立即执行和非立即执行 sp
  • node文件系统 常用文件处理方法

    打开文件 获取文件描述符 java件描述符 xff1a 被打开的文件分配的一个简单的数字作为标识符 span class token keyword const span fs span class token operator 61 sp
  • Electron基础

    安装 span class token comment 基于Vue span span class token number 1 span 全局安装Vue脚手架 npm install span class token operator s
  • node、npm 、package.json、Angular Cli、webpack之间的关系(Windows环境下)

    IDE xff1a webstorm xff0c 已安装angular插件 Angular Cli 依赖webpack xff0c 简化创建项目流程 xff1b npm属于node一部分 xff0c npm 从package json找对应
  • node文件系统—将目标文件夹中的所有文件复制到指定目录

    span class token keyword const span fs span class token operator 61 span span class token function require span span cla
  • Great Habits of Programmer(程序员的好习惯)

    原文出处 xff1a https github com benjycui benjycui github io issues 1 Most of you heard about Refactoring Improving the Desig
  • You -- Yes, You -- Can Speak at a Conference(你 -- 是的,你 -- 可以在会议上发言)

    原文出处 xff1a https engineering appfolio com appfolio engineering 2017 1 9 you yes you can speak at a conference I ve been
  • 自制前端项目脚手架

    准备工作 xff08 一些常用库 xff09 ora 可以用于表示当前模板的状态 span class token keyword const span oraIcon span class token operator 61 span s