webpack-从零开始定制一个vue-cli(第二步)

382 阅读7分钟

Cli(脚手架)工具现在在我们的日常开发中经常用到。比如:vue-cli、create-react-app、webpack-cli等。使用这些脚手架能快速的帮我们搭建对应项目。但有时我们也想为自己的项目定制适合的脚手架,避免大量的copy工作。所以接下来我们就来实现一个属于自己的简洁版的cli。

完整的项目地址:github: uc-cli

开始前准备

你可能会疑惑:如何让定制的cli工具全局安装后能以指令的形式启动脚手架进行构建?

在package.json文件中,提供了个bin字段,通过设置这个字段就能让我们的脚手架安装后能以指令的形式构建内容。

首先,我们要新建一个项目,并初始化package.json文件

mkdir uc-cli && cd uc-cli

npm init

然后bin字段中设置你命名的指令:

"bin": {
   "uc": "bin/index.js"
 }

在这里我设置的指令是uc指令,你也可以设置自己的指令。启动的是bin目录下的index.js。所以我们要在项目下新建一个bin目录,在再index.js文件里写入一点代码测试一下

console.log('test uc-cli')

之后就可以通过node bin/index.js测试打印结果。如果你要通过指令的形式测试可以在项目下运行npm install -g。如果项目文件更新了可以运行npm link进行更新

搭建前的准备

既然是开发自己的cli工具,那么开发前当然要先准备好模板。模板就是你要自动生成的项目文件,比如一个html文件或者一个js文件,或者是一个项目的模板。就像vue-cli生成的项目文件。

首先我们在项目下新建一个lib文件夹用于存放核心逻辑代码,再新建个template文件夹用于存放模板文件,我们把不需要根据用户配置改动的文件放在base文件夹下,需要改动的放在template文件夹下。

笔者一年前写过一个搭建cli的文章:webpack-从零开始定制一个vue-cli(第一小步)。当时仅仅是搭建起了Vue的开发环境,并没有cli工具类似的功能,所有仅仅是个模板。

模板有了,还需考虑cli需要哪些功能。就我这个cli工具而言,设计给用户自定义选项有:

  • 创建项目的类型:
    • vue
    • react(暂不支持)
  • 创建项目的方式:(1:手动配置,2:默认配置)
  • 选择手动配置时
    • 是否需要添加css预处理器
    • 是否需要添加eslint
    • 是否需要添加vue-router
    • 是否需要添加vuex
    • 是否需要添加axios(已配置好拦截器,封装好的axios)
    • 选择安装包的包管理器(auto:自动选择,yarn,npm,cnpm)

你也可以在开发前构思一下你的cli工具需要有哪些功能。

如果你的cli工具需要发布到npm,那在开始前最好去npm查一查,你起的包名是否有人已经在npm上发布过了,如果发布过了你是不能再发布的哦~

编写核心代码

cli工具核心逻辑就是通过获取用户自定义的选项,然后根据配置选项把准备好的模板或则远程仓库的模板的文件复制到本地。

在编写代码前你需要了解一些工具包的功能以及用法:

inquirer

要和用户交互,获取用户自定义的选项就需要使用到地址:inquirer这个包,用法如下:

// 和用户交互,获取用户配置
function getUserOption () {
  let qustions = []
  qustions.push(cssPreName: {
    name: 'cssPreName',
    type: 'list',
    message: '请选择你要添加的CSS预处理器: ',
    choices: ['none', 'sass', 'less', 'stylus']
  })
  qustions.push({...})

  return inquirer.prompt(qustions)
}
// inquirer.prompt 会返回一个Promise对象。
inquirer.prompt([...]).then(optins => {
  // 
})

运行效果如下:

chalk:能在控制台输出不同颜色的字体

cross-spawn:跨终端的命令包,使用spawn能帮我们执行一些命令;如 npm i。用法和child_process.spawn类似

which:能帮我们检测出哪些指令可用。比图:检测npm或yarn指令是否可用

memFs 和 memEditor:当我们有些模板文件需要根据用户的配置项动态改变时,可以用这两个包。通过ejs模板语法动态改变模板内容,为模板注入数据

this.store = memFs.create()
this.fs = memEditor.create(this.store)

this.fs.copyTpl('template/vue/App.vue', 'App.vue', {
  addRouter: true
})

例如App.vue文件根据用户配置是否使用vue-router而导出不同的模板

<template>
  <div>
  <%_ if (addRouter) { _%>
    <router-view/>
  <%_ } else { _%>
    <Home/>
  <%_ } _%>
  </div>
</template>

<script>
<%_ if (!addRouter) { _%>
import Home from '@/views/Home.vue'
<%_ } _%>

export default {
  name: 'App',
  <%_ if (!addRouter) { _%>
  components: { Home },
  <%_ } _%>
  data () {
    return {
    }
  }
}
</script>

commander:完整的 node.js 命令行解决方案,灵感来自 Ruby 的 commander。

使用这个包能帮我输出一些配置说明,输出cli工具的版本号,也能让我们运行一些命令。 如:uc -V uc --help等。地址:commander.js

现在我们就来开始开发cli工具吧~

在lib/index.js新建一个init方法,并在bin/index.js引用:

// lib/index.js
function init () {
  
}
module.exports = init

// bin/index.js
#!/usr/bin/env node

'use strict';

const init = require('../lib/index')

init()

init就是初始化cli的逻辑。在init中打印创建项目的位置,cli版本并且编写用户配置项等。这里需要用到chalkinquirer这两个包。

function init () {
  console.log()
  console.log(chalk.hex('#f3ec00')(`欢迎使用uc-cli version: ${packageJson.version}`))
  console.log()
  console.log(chalk.white(`项目创建在: ${process.cwd()}`))
  console.log()
  getUserOption().then(option => {
    console.log(option)
  })
}

function getUserOption () {
  let qustions = []
  qustions.push({
    name: 'cssPreName',
    type: 'list',
    message: '请选择你要添加的CSS预处理器: ',
    choices: ['none', 'sass', 'less', 'stylus']
  })
  qustions.push({
    name: 'eslint',
    type: 'list',
    message: 'eslint配置:',
    choices: ['none', 'standard', 'prettier', 'airbnb']
  })
  qustions.push({
    name: 'router',
    type: 'input',
    message: '是否添加router (y/n)?',
    validate: (val) => {
      val = val && val.toLowerCase()
      let v = ['y', 'yes', 'n', 'no']
      if (v.indexOf(val) === -1) {
        console.log(chalk.red(' 请输入 y 或 n'))
        return false
      }
      return true
    }
  })

  return inquirer.prompt(qustions)
}

打印结果如下:

和用户交互完毕后,就需要根据用户的配置项把我们的模板文件拷贝到本地了。对于文件的拷贝我们当然要使用命令的方式让它能自动执行。

首先,建个构建项目的Create类,在里面创建一个copyTemplate方法负责文件的拷贝。在拷贝文件前需要获取项目名称以及根据项目名称创建文件夹。在创建前还要判断文件是否已经存在。通过process.cwd()可以获取指令运行的路径。通过process.argv可以获取指令后面的参数,可以设计成指令后面的第一个参数就是项目名称。在创建好文件后就可以执行拷贝的指令了,这里执行指令用的是cross-spawn这个包。对于有些要根据用户配置而进行拷贝的文件需要用到前面提到的mem-fs 和 mem-fs-editor。最后执行安装,构建完成。

const chalk = require('chalk')
const spawn = require('cross-spawn')
const fs = require('fs')
const path = require('path')
const memFs = require('mem-fs')
const memEditor = require('mem-fs-editor')

class Create {
  constructor (options) {
  	// 用户配置项
    this.options = options

    this.store = memFs.create()
    this.fs = memEditor.create(this.store)
  }

  copyTemplate () {
    // 通过process.argv可以获得指令后面输入的参数。
    const projectPath = path.join(process.cwd(), process.argv[2])
    if (fs.existsSync(process.argv[2])) {
      console.log(chalk.red('项目已存在,请换个项目名称'))
      process.exit(1)
    }
    // 创建项目文件,并拷贝
    fs.mkdirSync(projectPath)
    // 这里是路径使用是自己写的,对于不同平台会有问题,
    // 如果要发布包需要使用node的path来处理
    spawn('cp', ['-r', 'template/base/.', projectPath], { stdio: 'inherit' })

    if (this.options.router === 'y') {
      this.fs.copyTpl('template/App.vue', path.join(projectPath, 'App.vue'), {
        addRouter: true
      })
    }

    this.fs.commit(() => {
      console.log('构建中,请稍等。。。')
      // 自动安装包,这里可以使用which包来获取安装指令
      let sp = spawn(
        'cnpm', ['install', 'vue-router', '-D'],
        { stdio: 'inherit', cwd: projectPath }
      )
      sp.on('close', (code) => {
        console.log()
        console.log(chalk.green('构建完成 √'))
      })
    })
  }
}

然后在init方法使用

getUserOption().then(option => {
  let create = new Create(option)
  create.copyTemplate()
})

执行命令创建项目 如果发布到npm后,或者本地安装好,可以直接使用uc指令构建项目。例如:uc my_project

这样一个简单的cli工具就制作完成了。虽然看起来是很简单,但如果想要做好,做丰富还是有很难度的,也有很多要了解的知识点。如果你没有搭建过自己的cli工具就去搭建一个自己的cli工具试试吧~相信你会有收获的。搭建过程中可以参考vue-clicreate-react-app这两个优秀的cli工具

参考链接:

手把手带你撸一个cli工具