直接就是干,手写一个属于你的前端脚手架工具

470 阅读5分钟

写在前面

你可能用到过很多前端脚手架工具,有没有试想过到底如何写一个属于你的脚手架呢?本人之前写了一个简单版的脚手架,如果你刚好也是想了解这块,本文可能对你有帮助,以下代码逻辑的梳理,你也可以去体验一下

npm install duffy-cli -g

流程图

技术栈

  • 开发环境: win7
  • 开发工具: VScode
  • 所需依赖包:

Node.js:整个脚手架的运行环境。本脚手架的 Node.js 版本: v8.11.1 。

es6: JavaScript 的新语法。

commander:TJ大神开发的工具,能够更好地组织和处理命令行的输入。

chalk:色彩丰富的终端工具。

ora:优雅的终端微调器,可以控制终端输出,这里主要用于命令行上的加载效果。

download-git-repo:用于下载远程仓库至本地 支持GitHub、GitLab、Bitbucket

handlebars: 知名的模板引擎

ini: 节点的ini格式解析器和序列化器

inquirer: 用于命令行与开发者交互

metalsmith: 静态网站生成器

request: 发送http请求的工具。

rimraf: 相当于UNIX的“rm -rf”命令

semver: 版本号处理工具

user-home: 用于获取用户的根目录

babel-preset-env: 会根据目标环境选择不支持的新特性来转译

项目搭建

初始化项目

创建项目目录后执行npm init按照提示完成初始化项目。

配置全局使用

为了可以全局使用,我们需要在 package.json 里面设置一下:

"bin": {
   "dfcli": "./bin/www"
 },

本地调试的时候,在项目根目录下执行: npm link 。 即可把 duffy-cli 命令绑定到全局,以后就可以直接以 dfcli 作为命令开头

安装依赖

依赖为上面的依赖包,

npm install semver rimraf .....

入口文件的设置

当前项目文件夹新建bin文件夹创建www.js文件

#! /usr/bin/env node
require('../dist/main.js')

命令管理 (src/main.js)

通过 commander 来设置不同的命令。

  • command 方法是设置命令的名字。
  • description 方法是设置描述。
  • alias 方法是设置简写。
  • action 方法是设置回调。
  Object.keys(configMap).forEach((action) => {
    program
    .command(action)
    .alias(configMap[action].alias)
    .description(configMap[action].description)
    .action(function () {
        if (action == 'new') {
          handleType()
        }
        //检测版本并执行main函数
        checkVersion(()=>{
          main(action, ...process.argv.slice(3))
        })
      });// 要分段
  })

每次执行主文件,需检测版本是否最新并提示

处理用户输入(\src\config.js)

在根目录下建立 .bgrc 文件并写入如下内容,用来存放用户托管平台名字、仓库地址、模版信息等,模版信息尚在开发中,后期会把模板信息提出来专门处理。

处理用户输入命令(\src\utils\rc.js)

新增/修改

dfcli config set <key> <value>

set 功能有两个,第一实现用户输入的新增和修改,第二实现初始化rc文件。

实现代码:

export let set = async (k,v) => {
   let has = await exist(rcUrl)
    if (k && v) {
      let opts
      if (has) {
        opts = await readFile(rcUrl, 'utf-8')
        opts = decode(opts)
        opts = Object.assign(opts,{[k]: v})
      } else {
        opts = Object.assign(DEFAULT,{[k]: v}) 
      }
      await writeFile(rcUrl, encode(opts), 'utf-8')
    } else { // rc初始化
      if (has) return
      await writeFile(rcUrl, encode(DEFAULT), 'utf-8')
    }
}

查看/获取

dfcli config get <key>

当没有get 后面没有key时,默认走getAll()表示获取全部bgrc文件内容

eg: dfcli config get 实现代码:

export let get = async (k) => {
  let has = await exist(rcUrl)
  let opts
  if (has) {
    opts = await readFile(rcUrl, 'utf-8')
    opts = decode(opts)
    console.log(opts[k])
  } else {
    return ''
  }
}
export let getAll = async () => {
  let has = await exist(rcUrl)
  let opts
  if (has) {
    opts = await readFile(rcUrl, 'utf-8')
    return decode(opts)
  } else {
    return ''
  }
}

删除

dfcli config remove <key> 

代码实现:

export let remove = async (k) => {
  let has = await exist(rcUrl)
  let opts
  if (has) {
    opts = await readFile(rcUrl, 'utf-8')
    opts = decode(opts)
    if (opts.hasOwnProperty(k)) {
      delete opts[k]
    }
    await writeFile(rcUrl, encode(opts), 'utf-8')
  } else {
    return ''
  }
}

更换模板地址

dfcli config set repertroy github:owner
dfcli config set username yourname

查看模板列表(\src\list.js)

dfcli list

初始化项目 (\src\install.js)

命令:

dfcli init

根据.bgrc文件的配置,获取托管在github上面的模板名字:

// 模板名字列表
let getTplNameList = async () => {
  let loading = ora('Loading template list .......')
  loading.start()
  let list = await getTplList()
  loading.succeed('template list complete.')
  let name = list.map((list) => list.name)
  return name
}

获取到模板名字列表后,让用户选择模板

const promptList = [{
      type: 'list',
      message: 'Please select a template: ',
      name: 'tpl_name',
      choices: tplnameList
  }]
  let ans = await inquirer.prompt(promptList)
  console.log(ans.tpl_name)

1.先判断当前模板本地是否以缓存,有缓存的话询问用户是否覆盖,没有缓存直接下载模板

2.选择模板后,让用户输入项目名字和项目目标文件夹

3.生成渲染模板到指定的位置

// 判断是本地是否有模板
let localCheckTpl = (tmpName) => {
  //远程模板地址
  const tmpRepo=path.resolve(userHome,'.tpl')
  //本地模板存放仓库
  const tmpDest=path.join(tmpRepo,tmpName)
  return {
    isExist: exists(tmpDest),
    tmpDest
  }
}
// 下载模板
let downloadTplAndGenrate = async (proName) => {
  //远程模板地址
  const tmpRepo=path.resolve(userHome,'.tpl')
  //本地模板存放仓库
  const tmpDest=path.join(tmpRepo,proName)
  let all = await getAll()
  let loading = ora(`download template start...`)
  loading.start()
  await download( `${all.repertroy}/${proName}`, home + '/.tpl/' + proName)
  loading.succeed(`template download complete.`)
  await generate(tmpDest)
}

// generate.js
export default async(tmpPath)=>{
  //初始化Metalsmith对象
  const metalsmith=Metalsmith(tmpPath)
  // 用户输入项目名字和目标文件夹
  let answer = await inquirer.prompt([{
    type:'input',
    name:'name',
    message:'Please enter your project name:',
    default:'dfcli-project'
  },{
    type:'input',
    name:'destination',
    message:'Please enter the path where your project will be stored:',
    default:process.cwd()
  }])
  //项目生成路径
  const destination=path.join(absolutePathFormat(answer.destination),answer.name)
  const loading = ora('generating...')
  //加入新的全局变量
  Object.assign(metalsmith.metadata(),answer)
  // console.log(metalsmith.metadata())
  loading.start()

  metalsmith
  .source('.')
  .destination(destination)
  .clean(false)
  .build(function(err) {      
    loading.stop()
    if (err) throw err
    console.log()
    console.log(chalk.green('Build Successfully'))
    console.log()
    console.log((`${chalk.green('Please cd')} ${destination} ${chalk.green('to start your coding')}`))
    console.log()
  })
}

项目结构说明

|-- bin
|   `-- www // 主文件入口,启动跑main.js
|-- package.json
`-- src 
    |-- config.js 配置rc文件的增删改查
    |-- create.js new 创建单个页面或者模板
    |-- generate.js 构建生成项目内容
    |-- index.js 根据用户输入动作命令,执行不同命令(dfcli init、dfcli config、dfcli list、dfcli new),具体任务从这里开始分开
    |-- install.js 模板初始化(dfcli init)
    |-- list.js 本地模板列表(dfcli list)
    |-- main.js 通过commander初始化并设置不同的命令
    |-- new.js 创建页面或者模板的具体实现
    `-- utils  
        |-- checkVersion.js 包版本检查
        |-- constant.js  一些需要的常量
        |-- gitHandle.js git相关操作
        |-- localPath.js 路径校验
        `-- rc.js rc文件的增删改查具体操作方法