来一份属于自己的脚手架吧!从零搭建一个自己VUE脚手架

816 阅读10分钟

自定义命令行

创建阶段

1.首先创建一个目录 my-climy-cli 中创建一个 index.js 作为入口文件

2.通过 npm init -y (yes) 初始化一个 package.json 文件

image-20210202154649340

3.在主入口文件 index.js 第一行加入 #!/usr/bin/env node 用于指明该脚本文件要使用node来执行。

/usr/bin/env 用来告诉用户到path目录下去寻找node,#!/usr/bin/env node 可以让系统动态的去查找node,目的是 解决不同电脑node存放路径不一致问题。

注意: 该命令必须放在第一行, 否者不会生效

image-20210202154800949

4.接着,在初始化好的 package.json 中创建一个 属于自己的命令,如下图所示。

image-20210202154649340

5.以上都完成过后通过一个指令 npm link,完成之后会看到自己本地npm目录下多出了三个文件(如下图所示)

image-20210202160746575

测试输出

image-20210202160847768

通过自定义的命令 mycli 会看到如约而至的输出了 尝试使用 node index.js

创建自己的CLI脚手架(scaffold)过程。

在前面的基础铺垫完成后,我们将进入正式的创建一个属于自己的脚手架(scaffold)过程。

在这里推荐一个帮助开发者快速搭建一个属于自己的命令行工具 Commander

首先安装:npm install commander

获取属于自己的版本号

一个属于自己最不能失去的就是自己的版本号,就好像自己的老婆一样(不要经常更换哦)

首先我们先获取到 package.json 中 属于自己的版本号

image-20210202163252487

我们通过 cumin --help 可以看到当前只有两种指令可选择玩弄。 image-20210202163704458

有点孤独寂寞,我们还是多自定义几个好伙伴一个玩吧!!毕竟CSGO都是 5 v 5 的。

自定义属于自己的 -h --help

看图再解释,方面又理解

image-20210202164124420

创建好后,使用 cumin --help 会发现多了两种选择

image-20210202164730259

program有个方法option是用来自定义一个属于自己的 -help

program.option('标记', '描述’)

-d--dest 是一样的 -d 只不过是简写

<dest> 表示你将要干什么,例如在 src 目录下创建一个 components 目录,可以这样写(目前还没实现这个功能,这里只是测试,在后面会实现。)(program.option(''-c --cumin '') 也是一样的)。

image-20210202165406874

目前 Options 中只有四个选项,那么将来可能有很多,代码会冗余,所以我们创建放入一个新的目录内。

首先创建一个lib目录

lib中创建一个core目录

core下创建一个core.js

image-20210202171204473

接下来分割代码 如下图所示

image-20210202171545702 image-20210202171956222

现在已经完成了 --help 自定义指令的方法了。

类似于 (vue create my-project) 该如何通过自定义命令创建?

首先我们得完成以下五个步骤

创建解析 create 命令 (在 create.js 文件中完成)

我们的项目是基于可扩展的所以我们将在lib目录下的core中接着创建一个 create.js 文件,并写入以下代码。

const program = require('commander')

const { createProjectAction } = require('./action')

const createProjects = () => {
  program
    .command('create <project> [others...]')
    .description('clone a repository into a folder') // 英译:将存储库克隆到文件夹中
    .action((project, outhers) => {
      console.log(project, outhers) // my-cuminProject,[ 'abc', 'cba' ]
  })
}

module.exports = createProjects
  • command('创建命令的名称') 在终端就可以这样使用 cumin create <项目名称> 紧跟项目名称后面写的任何内容都会被保存到[others...]中。
  • description('描述信息,对create指令的描述信息')
  • action (project, outhers) 是一个回调函数
    • project 返回创建目录的名称
    • outhers 返回一个数组,紧跟 "项目名称" 后面所有的值 (abc cba) (这个参数不常用)

导出后在index.js主入口文件中导入后,此时我们可以通过 cumin create my-cuminProject abc cba 来打印一下信息,会通过action函数返回。

Return(返回)

image-20210202185335129

此时我们得注意要到一个可扩展性的问题, 将来我们都要在 action 回调函数中操作,这样的话避免不了会产生很多代码,所以我们再拆出一个文件来,专门用于处理action的回调函数。

步骤:

  1. lib目录下的core中再创建一个 action.js
  2. 将action回调函数拆出来,放入 action.js

如图:

image-20210202191257522 image-20210202191150039 image-20210202191216253 image-20210202191242306

从此 action回调函数 我们就可以在 action.js 中处理了。

现在为止我们的目录是这样的:image-20210202191703646你也是这样的吗?

接下来我们就可以在 action.js 中开始编码

通过 download-git-repo 从代码仓库中下载代码

首先安装 npm install download-git-repo

简单理解 npm install download-git-repo:从远程代码仓库中下载代码

  // 引入 download-git-repo
const download =require('download-git-repo')

const createProjectAction = (project, outhers) => {
  // 1. clone 克隆项目
    
  // download用法:download("克隆的地址ClonePath", "类型", callback(回调函数))
  download() 

  // 2. 执行 npm install
  // 3. 执行 npm run serve
  // 4. 打开浏览器
}

module.exports = {
  createProjectAction
}

download-git-repo官网原图

image-20210203102354931

我们看到都是以回调函数作为我们的返回结果的,那么将一定会有很多回调函数,那么就会产生我们常见的 回调地狱 这个说法,然而我们的 download-git-repo 作者并没有提供 promise async await 等方式帮我们处理,回调地狱,所以我们得自己提供以下方式解决。首先通过 NodeJs内部的utilAPI 来解决。

const { promisify } = require('util') 被包裹的函数会以 promise 风格返回

现在我们可以通过这种方式返回const download = promisify(require('download-git-repo'))

此时我们的 download 会以 promise 风格返回了, 接着我们可以利用 ES7 中的 async await 来异步执行,同步加载了.

Example:

const { promisify } = require('util')
const download = promisify(require('download-git-repo'))

// callback -> promisify(callback) -> promise -> async await
const createProjectAction = async (project, outhers) => {

  // download用法:download("克隆的地址ClonePath", "克隆后存放的地址", callback(回调函数))
  await download() 

  // 2. 执行 npm install
  // 3. 执行 npm run serve
  // 4. 打开浏览器
}

module.exports = {
  createProjectAction
}

download函数中克隆的地址ClonePath地址模板我们可能不是唯一的,还是这句话我们要及提供可扩展性所以我们继续在lib目录下创建一个 config 目录,并在config目录中创建一个 repo-config.js存放地址。

如下图所示

image-20210203105140627 image-20210203105218216 image-20210203105250358

接着在 repo-config.js 导入来自githubcoderwhy 老师的 vue 模板.image-20210203153040081

direct:直接使用http从直接url下载

// clone path
let vueRepo = 'direct:https://github.com/lzw1254348304/hy-vue-temp.git'

module.exports = {
  vueRepo
}

回到 action.js 中写下

const { promisify } = require('util')
const download = promisify(require('download-git-repo'))

// clone path
const {
  vueRepo
} = require('../config/repo-config')

// callback -> promisify(callback) -> promise -> async await
const createProjectAction = async (project) => {
  console.log('In the process of cloning...')

  // 1. clone 克隆项目
  /**
   * vueRepo: 克隆地址
   * project: 项目名称
   * { clone: true }: 克隆整个项目
   * callback: 回到函数
   */
  await download(vueRepo, project, { clone: true }, (err) => {
    if (!err) {
      console.log('cloning finish')
    }
  })

  // 2. 执行 npm install
  // 3. 执行 npm run serve
  // 4. 打开浏览器
}

module.exports = {
  createProjectAction
}

现在我们在任意目录下输入 cumin create mydemo 就会生成一个 vue 模板了。

image-20210203152902316

Vue模板虽然说生成了,但还是需要我们自己手动 npm install 因为还有依赖没有安装,为了更加的方便快捷,我们接下来要完成的是。

创建Vue模板时自动帮我们 npm install 安装依赖

我们将来会在 cumin create <my project> 时候自动帮我们执行 npm install npm run serve open browser 所以代码会比较多,又说道可扩展性与可维护性了,所以我们得再创建一个util 目录。并在 util 目录下创建一个 terminal.js

先来理解一下概念:

{当前进程} 正在帮助我们执行JavaScript代码(忙碌中的),所以如果我们要执行{ npm install }时,可以使用
NodeAPI中的 { child-process(子进程模块) } 创建子进程来帮我们执行。

child-process:
	child_process (子进程模块)一旦使用 child_process 里面的某个方法时,是会为我们开启一个新进程的。
	
	通常我们在执行 { npm install } { npm run serve } 时,本质上都是会为我们开启另外一个进程,在另外一个进程中帮助我们完成相关的任务。	

好了,理解之后我们来介绍一下 child-process 中的 spawn 方法:

spawn(command, args, options) 会异步地衍生子进程,且不阻塞 Node.js 事件循环。

详情见: nodejs.cn/api/child_p…

terminal.js部分代码解析

// `sqawn()函数` 本质上会开启一个进程,并返回一个子进程(childProcess)
const childProcess = spawn()
  • spawn简单理解就是创建一个进程并返回给childProcess,里面有三个参数 spawn('使用什么命令','执行方式','当前目录')

    例如

      伪代码:spawn('npm', ['install'], { cwd: <myProject> })
    
  • childProcess进程就是帮我们自动执行 npm install 的,所以我们希望在执行chindProcess进程时,打印执行过程中的信息(在执行命令过程中,进程会打印很多的信息,在执行 npm install 的时候终端会打印很多信息(默认是不会打印的)

  • 为了让它在执行过程中能显示很多打印信息我们可以这样做:

       /**
         * `stdout` : 标准输出流
         * `stderr` : 标准错误流
         * `stdout`中返回一个 `pipe` 函数,简称`管道`
         * 这两个标准流是存在于 `childProcess` 进程中的,接下来我们可以这样使用它
         */
    
        /**
          * `process` 对象是一个全局变量,提供了有关当前 `Node.js` 进程的信息并对其进行控制。 
          *  作为全局变量,它始终可供 `Node.js` 应用程序使用,无需使用 `require()`。 它也可以使用 
          *	 `require()` 显式地访问。
          */
         
        childProcess.stdout.pipe(process.stdout)
        childProcess.stderr.pipe(process.stderr)
    
  • /**
     * `npm install` 是处于一个阻塞的状态的,执行完毕后,我们接下来要执行 `npm run serve` 
     * 为了可以让终端自动执行 npm install 完毕后,执行 npm run serve,我们该这样做:
     */
    childProcess.on('close', () => {
      resolve('npm install execution')
    })
    

terminal.js中的完整代码

// 执行终端命令相关代码
// Spawn 会衍生子进程,且不会阻塞 Node.js 事件循环
const { spawn } = require('child_process')

const execCommand = (...args) => {
  return new Promise((resolve, reject) => {
      
    // `sqawn()函数` 本质上会开启一个进程,并返回一个 子进程(childProcess)
    const childProcess = spawn(...args)
    
    // 执行过程中能显示很多打印信息
    childProcess.stdout.pipe(process.stdout)
    childProcess.stderr.pipe(process.stderr)

    childProcess.on('close', () => {
      resolve('npm install execution')
    })
  })
}

module.exports = {
  execCommand
}

action.js导入

const { promisify } = require('util')
const download = promisify(require('download-git-repo'))

// clone path
const { vueRepo } = require('../config/repo-config')

// 自动执行 npm install
const { execCommand } = require('../util/terminal')

// callback -> promisify(callback) -> promise -> async await
const createProjectAction = async (project) => {
  console.log('In the process of cloning...')

  // 1. clone 克隆项目
  await download(vueRepo, project, { clone: true })

  // 2. 执行 npm install
  let exec = process.platform === 'win32' ? 'npm.cmd' : 'npm'
  const execution = await execCommand(exec, ['install'], { cwd: `./${project}` })
  console.log(execution)

  
  // 3. 执行 npm run serve
  execCommand(exec, ['run', 'serve'], { cwd: `./${project}` })
  // 4. 打开浏览器
    `见下文`
}

module.exports = {
  createProjectAction
}
  • 其实在我们执行npm的时候,系统会默认帮我们执行npm.cmd的,注意在execCommand的第一个参数中,在Mac\linux操作系统中执行没问题,是因为Mac\linux操作系统会默认自动帮我们调用 npm.cmd但是在 window 操作系统中会报错的,这是因为 window 操作系统 默认不会帮我们调用 npm.cmd
  • 解决:
  //  兼容 window 操作系统
  let exec = process.platform === 'win32' ? 'npm.cmd' : 'npm'

自动运行 npm run serve

利用上面封装的execCommand函数我们可以加以利用

  // 3. 执行 npm run serve
  execCommand(exec, ['run', 'serve'], { cwd: `./${project}` })

自动打开浏览器

因为在下载的 vue 模板中,我们已经加上了 --open "serve": "vue-cli-service serve --open" 所以自动运行完npm run serve,后会自动打开默认浏览器。

创建属于自己脚手架的模板

创建四个模板,这是我们会使用到ejs这个模板库

vue-component.vue.ejs

vue-router.js.ejs

vue-vuex.js.ejs

vue-vuex-types.js.ejs

首先创建一个 VUE 组件模板,例:

vue-component.vue.ejs

<template>
  <div class="<%= data.lowerName %>">
    <h2>{{ message }}</h2>
  </div>
</template>

<script>
  export default {
    name: "<%= data.name %>",
    components: {
    },
    mixins: [],
    props: {
    },
    data: function() {
      return {
        message: "Hello <%= data.name %>"
      }
    },
    created: function() {
    },
    mounted: function() {
    },
    computed: {
    },
    methods: {
    }
  }
</script>

<style scoped>
  .<%= data.lowerName %> {
    
  }
</style>

接着我们来创建相对应的命令来生成组件,还是之前那句话 为了可扩展性

  program
    .command('cVue <project> [others...]')
    .description('create a VUE component template')
    .action(createVueComponentTemplateAction)

现在我们就可以通过 cumin cVue HelloWrold指令就可以创建一个HelloWorld.vue组件了,我们来看看 createVueComponentTemplateAction函数是如何实现的。

// 导入 compiler函数
const { compiler } = require('../util/utils')

// create VUE component template
 const createVueComponentTemplateAction = async (project) => {
  // 编译 ejs 模板
  const result = await compiler( // utils.js 导出
      'vue-template.vue.ejs', 
      { name: project, lowerName: project.toLowerCase() 
   })
  console.log(result)

  // 将 result 写入 .vue 文件中
TODO................
  // 将 .vue 文件放入文件夹中
}

我们在 util目录下创建了 utils.js文件,如下代码

compiler函数

const ejs = require('ejs')
const path = require('path')

const compiler = (templateName, data) => {
  // 根据用户执行的命令名称,拿到指定路径的模板,进行渲染创建
  const templateCurrentPath = `../templates/${templateName}`
  const templateAbsolutePath = path.resolve(__dirname, templateCurrentPath)
  // 读取 HTML 标签
  return new Promise((resolve, reject) => {
    ejs.renderFile(templateAbsolutePath, { data }, {}, (err, result) => {
      if (err) {
        reject(err)
        return
      }
      resolve(result)
    })
  })
}

// 导出 compiler 函数
module.exports = {
  compiler
}

接下来我们要将它写入指定的文件内,通过Node API 中的 fs 模块写入文件内。

compiler函数返回的内容是Vue模板,所以我们可以将Vue模板写入到Components目录下,为什么是写到Components目录下而不是其他目录呢?是因为 cumin cVue <my-project> 这个指令就是为的帮我们快速的在Components目录下创建.vue文件。

fs.writeFile('components目录下',result)

还有其他三个,乃至自己想出来的,都可以继续写下去了。都是一样的逻辑套路。

完成代码:github.com/cumin-coder…