前端脚手架初探究

1,053 阅读4分钟

先看下效果,现在脚手架还不完善只是完成了初始化功能,具体功能还得等之后慢慢完善

思想

本前端脚手架的思想,其实就是

  1. 使用inquirer.js 和用户进行交互,通过用户的输入进行定制化配置
  2. 从远端git仓库下载我们提前定义好的模版
  3. 将用户的输入作为插值插入到我们的模版中。实现定制化

好处

前端脚手架的好处主要是

  1. 规范统一,因为复制过程中可能会出现自己错误,如果是新建可以参考我之前文章开发的vscode插件,自动填充模版
  2. 后期可以自定义需要,比如项目中需要element可以通过插值进行插入,不需要也可以选择不引入,节省开发时间
  3. 启动新项目可以加入之前定义好的业务组件,比如loading toast组件等

具体看下代码

初始化 bin/init.js

#!/usr/bin/env node

const program = require('commander')
const path = require('path')
const fs = require('fs')
const glob = require('glob') // npm i glob -D
const download = require('../lib/download') //下载配置
const inquirer = require('inquirer') // 按需引入
const logSymbols = require("log-symbols");
const chalk = require('chalk')
const remove = require('../lib/remove') // 删除文件js
const generator = require('../lib/generator')// 模版插入
const CFonts = require('cfonts');

program.usage('<project-name>')
            .parse(process.argv) // 加入这个能获取到项目名称

// 根据输入,获取项目名称
// console.log(program)
let projectName = program.rawArgs[2] // 获取项目名称

if (!projectName) {  // project-name 必填  如果没有输入名称执行helphelp
  // 相当于执行命令的--help选项,显示help信息,这是commander内置的一个命令选项
  program.help()
  return
}

// 当前目录为空,如果当前目录的名称和project-name一样,则直接在当前目录下创建工程,否则,在当前目录下创建以project-name作为名称的目录作为工程的根目录
// 当前目录不为空,如果目录中不存在与project-name同名的目录,则创建以project-name作为名称的目录作为工程的根目录,否则提示项目已经存在,结束命令执行。
const list = glob.sync('*')  // 遍历当前目录
let next = undefined;

let rootName = path.basename(process.cwd());
if (list.length) {  // 如果当前目录不为空
  if (list.some(n => {
    const fileName = path.resolve(process.cwd(), n);
    const isDir = fs.statSync(fileName).isDirectory();
    return projectName === n && isDir
  })) {
    console.log(`项目${projectName}已经存在`);
    remove(path.resolve(process.cwd(), projectName)) // 删除重复名字文件,并且重复创建,(逻辑修改)--> 询问是否删除重名文件然后,用户回答是,然后删除文件,并重新覆盖
    // return;
  }
  rootName = projectName;
  next = Promise.resolve(projectName);
} else if (rootName === projectName) {
  rootName = '.';
  next = inquirer.prompt([
    {
      name: 'buildInCurrent',
      message: '当前目录为空,且目录名称和项目名称相同,是否直接在当前目录下创建新项目?',
      type: 'confirm',
      default: true
    }
  ]).then(answer => {
    console.log(answer.buildInCurrent)
    return Promise.resolve(answer.buildInCurrent ? '.' : projectName)
  })
} else {
  rootName = projectName;
  next = Promise.resolve(projectName)
}

next && go()

function go () {
  // 预留,处理子命令
  // console.log(path.resolve(process.cwd(), path.join('.', rootName))) // 打印当前项目目录
  // download(rootName)
  //   .then(target => console.log(target))
  //   .catch(err => console.log(err))
  next.then(projectRoot => { //
    if (projectRoot !== '.') {
      fs.mkdirSync(projectRoot)
    }
    CFonts.say('amazing', {
      font: 'block',              // define the font face
      align: 'left',              // define text alignment
      colors: ['#f80'],         // define all colors
      background: 'transparent',  // define the background color, you can also use `backgroundColor` here as key
      letterSpacing: 1,           // define letter spacing
      lineHeight: 1,              // define the line height
      space: true,                // define if the output text should have empty lines on top and on the bottom
      maxLength: '0',             // define how many character can be on one line
  });
    return download(projectRoot).then(target => {
      return {
        projectRoot,
        downloadTemp: target
      }
    })
  }).then(context => {
    // console.log(context)
    return inquirer.prompt([
      {
        name: 'projectName',
    	  message: '项目的名称',
        default: context.name
      }, {
        name: 'projectVersion',
        message: '项目的版本号',
        default: '1.0.0'
      }, {
        name: 'projectDescription',
        message: '项目的简介',
        default: `A project named ${context.projectRoot}`
      },{
        name: 'supportMacawAdmin',
        message: '是否使用element',
        default: "No",
      }
    ]).then(answers => { // 可选选项回调函数
      // return latestVersion('macaw-ui').then(version => {
      //   answers.supportUiVersion = version
      //   return {
      //     ...context,
      //     metadata: {
      //       ...answers
      //     }
      //   }
      // }).catch(err => {
      //   return Promise.reject(err)
      // })
      let v = answers.supportMacawAdmin.toUpperCase();
      answers.supportMacawAdmin = v === "YES" || v === "Y";
      return {
        ...context,
        metadata: {
          ...answers
        }
      }
    })
  }).then(context => {
    console.log("生成文件")
    console.log(context)
     //删除临时文件夹,将文件移动到目标目录下
     return generator(context);
  }).then(context => {
    // 成功用绿色显示,给出积极的反馈
    console.log(logSymbols.success, chalk.green('创建成功:)'))
    console.log(chalk.green('cd ' + context.projectRoot + '\nnpm install\nnpm run dev'))
  }).catch(err => {
    console.error(err)
     // 失败了用红色,增强提示
     console.log(err);
     console.error(logSymbols.error, chalk.red(`创建失败:${err.message}`))
  })
}

删除文件

lib/remove.js

// 删除文件系统
const fs =require("fs");
const path=require("path");

function removeDir(dir) {
  let files = fs.readdirSync(dir)
  for(var i=0;i<files.length;i++){
    let newPath = path.join(dir,files[i]);
    let stat = fs.statSync(newPath)
    if(stat.isDirectory()){
      //如果是文件夹就递归下去
      removeDir(newPath);
    }else {
     //删除文件
      fs.unlinkSync(newPath);
    }
  }
  fs.rmdirSync(dir)//如果文件夹是空的,就将自己删除掉
}

module.exports=removeDir;

下载模版

lib/down.js

const download = require('download-git-repo')
const path = require("path")
const ora = require('ora')

module.exports = function (target) {
  target = path.join(target || '.', '.download-temp');
  return new Promise(function (res, rej) {
    // 这里可以根据具体的模板地址设置下载的url,注意,如果是git,url后面的branch不能忽略
    // 格式是名字/地址 后面不加 .git 但是带着 #分支
    let url = '名字/模版#分支'
    const spinner = ora(`正在下载项目模板,源地址:${url}`)
    spinner.start();

    download(url, target, { clone: false }, function (err) { // clone false 设置成false 具体设置看官网设置
      if (err) {
        spinner.fail()
        rej(err)
      }
      else {
        // 下载的模板存放在一个临时路径中,下载完成后,可以向下通知这个临时路径,以便后续处理
        spinner.succeed()
        res(target)
      }
    })
  })
}

注意:其中的写模版名称的时候,格式必须是 名字/模版#分支

插值文件

lib/generator

const rm = require('rimraf').sync //以包的形式包装rm -rf命令,用来删除文件和文件夹的,不管文件夹是否为空,都可删除
const Metalsmith = require('metalsmith') // 插值
const Handlebars = require('handlebars') // 模版
const remove = require("../lib/remove") // 删除
const fs = require("fs")
const path = require("path")
/**
 * 生成文件
 * @param 文件的名称
 */
module.exports = function (context) {

  let metadata = context.metadata; // 用户自定义信息
  let src = context.downloadTemp; // 暂时存放文件目录
  let dest = './' + context.projectRoot; //项目的根目录

  if (!src) {
    return Promise.reject(new Error(`无效的source:${src}`))
  }
  return new Promise((resolve, reject) => {
    const metalsmith = Metalsmith(process.cwd())
      .metadata(metadata) // 将用户输入信息放入
      .clean(false)
      .source(src)
      .destination(dest);
   
    metalsmith.use((files, metalsmith, done) => {
      const meta = metalsmith.metadata()
      Object.keys(files).forEach(fileName => {
        const t = files[fileName].contents.toString()
        console.log("打印差值")
        // console.log(t)
        files[fileName].contents = new Buffer.from(Handlebars.compile(t)(meta),'UTF-8')
      })
      done()
    }).build(err => {
      remove(src);
      err ? reject(err) : resolve(context);
    })
  })
}

packjson配置

{
  "name": "cli",
  "version": "1.0.0",
  "description": "amz脚手架1.0",
  "bin": {
    "amaz": "./bin/init.js"
  },
  "main": "./bin/macaw-hellow.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "cli"
  ],
  "author": "amaizngli",
  "license": "ISC",
  "dependencies": {
    "cfonts": "^2.4.5",
    "commander": "^4.0.0",
    "download-git-repo": "^3.0.2",
    "glob": "^7.1.5",
    "handlebars": "^4.5.1",
    "inquirer": "^7.0.0",
    "metalsmith": "^2.3.0",
    "ora": "^4.0.2"
  }
}

本地测试可以使用npm link链接到全局进行开发测试

其中

特效文字使用的是cFonts插件,具体使用可以去npm查看

其中插值使用Metalsmith模版,胶水式代码,进行插值插入,模版使用的Handlebars模版

开发定制可以采用git 的.gitignore文件的思路进行,但是我还没有应用。之后完善后会一并将代码放出

欢迎一起踩坑