从零开发一个脚手架(入门)

本文作者:岳永

这个系列文章,设计为3篇文章讲解,从入门、进阶到高级,希望可以帮助大家掌握并且学会一个脚手架开发的整体流程和思想,能在自己的团队中用起来,帮助团队实现一些可以提升开发效率的脚手架工具。

关于脚手架

提到脚手架,想必大家都不陌生,在我们的日常开发当中,不管你是用vue、react还是angular,他们都为开发者提供了一套开箱即用的脚手架工具,就像vue-clicreate-react-appangular-cli 等都是非常优秀的脚手架,脚手架的概念,通俗理解的讲就是可以帮助我们快速初始化一个项目架子,包括项目依赖、模板、构建工具等等,不需要我们从零配置就可以“一键启动”的这么一个工具,让我们可以快速进入业务开发,提升效率。

脚手架的好处

  • 减少重复性的工作,无需拷贝现有项目再删除无关代码,或者从零创建一个项目和文件
  • 可以内置交互动态生成用户需要的项目结构和配置文件,灵活性强
  • 多人开发模式高效,摒弃传统低效的复制粘贴

实现的功能(入门版)

  • coo -V 查看当前版本号
  • coo -h 查看帮助信息(用法)
  • coo create my-project 拉取远程模版到当前项目文件夹中,并弹出交互向导,选择安装依赖包工具,进行依赖安装,最后运行项目

初始化项目

创建一个空项目(coo-cli),使用npm init -y进行初始化。

项目目录搭建

coo-cli
├─README.md
├─index.js // 入口文件
├─package.json
├─yarn.lock
├─lib // 主文件
|  ├─utils // 一系列工具函数
|  |   ├─executeCommand.js // 执行进程脚本
|  |   └waitFnLoading.js // loading加载
|  ├─linkConfig // 链接配置文件
|  |     └vue-repo.js
|  ├─core // 源码
|  |  ├─create.js // 命令创建入口
|  |  ├─help.js // 配置信息(用法)
|  |  ├─actions // 一系列命令脚本
|  |  |    ├─createProject.js // 创建模板脚本
|  |  |    └index.js // 统一导出文件
复制代码

自定义命令

node.js 内置了对命令行操作的支持,package.json中的 bin 字段可以定义命令和关联的执行文件。在 package.json 中添加 bin 字段

{
  "name": "coo-cli",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "bin": {
    "coo": "index.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
复制代码

链接到全局环境

npm link

  • 介绍:在本地开发npm模块的时候,我们可以使用npm link命令,将npm 模块链接到对应的运行项目中去,方便对模块进行调试,该模块会根据package.json上的配置,被链接到全局,路径是{prefix}/lib/node_modules/<package>,我们可以使用npm config get prefix命令获取到prefix的值。
  • 使用:进到当前开发的模块项目中,在和package.json同一级目录下执行npm link,这个时候在package.json中配置的这个bin对象属性值生效,全局环境中就会有一个可执行的coo命令,实际上,就是用 coo 命令来代替执行 node index.js

index.js入口文件中,行首加入#!/usr/bin/env node指定当前脚本由node.js进行解析

#! /usr/bin/env node

console.log('hello coo-cli')
复制代码

WX20210423-105635@2x.png

命令行解析

脚手架第一个功能就是处理用户的命令,这里需要使用 commander.js。这个库的功能就是解析用户的命令,根据用户的命令执行相应的逻辑操作。代码示例:

#! /usr/bin/env node

const program = require('commander')

program.option('-s, --coo', 'this is a coo cli')

program.version(require('./package.json').version).parse(process.argv)
复制代码

在终端输入coo -Vcoo --version会显示出当前版本号信息,输入coo -hcoo --help会显示出当前用法和描述信息。

const program = require('commander')

const createCommands = () => {
  program
    .command('create <project> [others...]')
    .description('create a new project from remote repository')
    .action(function(project) {
      console.log(project)
    })
}
复制代码

在终端输入coo create my-project(实际上执行的是 node index.js create my-project),commander 解析到命令 create 和参数 my-project。然后在 action 的回调里取到参数 project(值为 my-project)。

关于process.argv,可以参考官方文档了解。

WX20210423-105651@2x.png

注册create 命令

coo create my-project命令实现的主要功能

  • 拉取远程模板到项目文件夹
  • 安装依赖模块
  • 运行项目

在终端输入coo create my-project命令后,会通过commander解析到create命令和参数my-project,然后脚手架就可以在action回调函数里取到project参数(值为my-project),这个注册命令的代码我们写在/core/create.js中,代码示例:

const program = require('commander')
const { createProjectAction } = require('./actions')

const createCommands = () => {
  program
    .command('create <object> [others...]')
    .description('create a new project from remote repository')
    .action(createProjectAction)
}

module.exports = createCommands
复制代码

可以看到createProjectAction函数是我们对该命令对应操作的主要逻辑的一个封装,在/core/actions/createProject.js中,代码如下:

// 创建项目方法
const createProjectAction = async (project) => {
  console.log(project) // my-project
}

module.exports = {
  createProjectAction
}
复制代码

拉取远程模板到项目文件夹

从远程拉取项目到本地,这个时候我们要用到download-git-repo,支持从 Github、Gitlab 下载远程仓库到本地。 位置:/core/actions/createProject.js

const { promisify } = require('util')
const download = promisify(require('download-git-repo')) // promise化
const { vuepressRepo } = require('../linkConfig/vue-repo')
const waitFnLoading = require('../../utils/waitFnLoading')

// 创建项目方法
const createProjectAction = async (project) => {
  // 1. 拉取远程项目
  await waitFnLoading(download, 'initing project...')(vuepressRepo, project, { clone: true })
}

module.exports = {
  createProjectAction
}
复制代码

WX20210423-105708@2x.png

安装依赖模块

命令行交互

在安装依赖包之前,会有一个弹出交互选项,对用户选择使用哪一个包管理工具进行向导,接收用户的选择并作出相应的处理,这个交互要用到inquirer,位置:/core/actions/createProject.js

const { promisify } = require('util')
const download = promisify(require('download-git-repo'))
const inquirer = require('inquirer')
const { vuepressRepo, vueTempRepo } = require('../linkConfig/vue-repo')
const { commandSpawn } = require('../../utils/executeCommand')
const waitFnLoading = require('../../utils/waitFnLoading')

// 创建项目方法
const createProject = async (project) => {
  // 1. 拉取远程项目
  await waitFnLoading(download, 'init project')(vuepressRepo, project, { clone: true })
  // 2. 安装npm包
  // 进行向导
  const commandObj = {
    yarn: {
      install: [],
      run: ['docs:dev']
    },
    npm: {
      install: ['install'],
      run: ['run', 'docs:dev']
    }
  }
  const { package } = await inquirer.prompt({
    name: 'package',
    type: 'list',
    message: 'please select a package manage command',
    choices: [
      'yarn',
      'npm'
    ]
  })
  // 系统执行命令兼容
  const command = process.platform === 'win32' ? `${package}.cmd` : package

  // 下载依赖
  await waitFnLoading(commandSpawn, 'installing packages...')(command, commandObj[package].install, { cwd: `./${project}` })
}

module.exports = createProject
复制代码

WX20210423-105724@2x.png

通常我们安装默认包,都是在终端通过npm iyarn的方式来操作,现在我们想要通过代码来实现这个步骤,就要用到node内置模块child_processspawn方法来实现。位置:/utils/executeCommand.js

const { spawn } = require('child_process')

const commandSpawn = (...args) => {
  return new Promise((resolve, reject) => {
    const childProcess = spawn(...args)
    // 输出子进程文件流信息
    childProcess.stdout.pipe(process.stdout)
    // 输出子进程报错信息
    childProcess.stderr.pipe(process.stderr)
    // 监听close方法(npm模块全部下载完成触发)
    childProcess.on('close', () => {
      resolve()
    })
  })
}

module.exports = {
  commandSpawn
}

![WX20210423-105737@2x.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a023a81be82f4681bdcd28d1d3469c54~tplv-k3u1fbpfcp-watermark.image)

## 运行项目
运行项目通常是在终端输入```npm run dev``````yarn dev```,那么通过编写代码实现跑在终端里的命令都可以通过node内置模块```child_process``````spawn``````exec```方法实现,也可以安装第三方依赖包```execa```来实现,本质都是调用子进程执行命令。主要逻辑完整代码,位置:```/core/actions/createProject.js```
```js {38-39}
const { promisify } = require('util')
const download = promisify(require('download-git-repo'))
const inquirer = require('inquirer')
const { vuepressRepo, vueTempRepo } = require('../linkConfig/vue-repo')
const { commandSpawn } = require('../../utils/executeCommand')
const waitFnLoading = require('../../utils/waitFnLoading')

// 创建项目方法
const createProject = async (project) => {
  // 1. 拉取远程项目
  await waitFnLoading(download, 'init project')(vuepressRepo, project, { clone: true })
  // 2. 安装npm包
  // 进行向导
  const commandObj = {
    yarn: {
      install: [],
      run: ['docs:dev']
    },
    npm: {
      install: ['install'],
      run: ['run', 'docs:dev']
    }
  }
  const { package } = await inquirer.prompt({
    name: 'package',
    type: 'list',
    message: 'please select a package manage command',
    choices: [
      'yarn',
      'npm'
    ]
  })
  // 系统执行命令兼容
  const command = process.platform === 'win32' ? `${package}.cmd` : package

  await waitFnLoading(commandSpawn, 'download packages')(command, commandObj[package].install, { cwd: `./${project}` })

  // 3. 启动项目
  await commandSpawn(command, commandObj[package].run, { cwd: `./${project}` })
}

module.exports = createProject
复制代码

WX20210423-105750@2x.png

项目中需要安装的依赖包

  • commander: 命令行解析工具
  • download-git-repo: 下载远程模板
  • inquirer: 交互式命令行工具
  • ora: 显示loading动画
yarn add commander download-git-repo inquirer ora 
or
npm install commander download-git-repo inquirer ora -S
复制代码

项目地址

WX20210423-105825@2x.png

总结

脚手架工具入门版,逻辑其实很简单,可能一些概念性的东西大家之前不是很清楚,但是我相信你看完一定会有所收获,也相信你也可以自己动手搭一个自己的min脚手架,感兴趣的话也可以继续扩充更多的功能。后续还会推出脚手架工具进阶版脚手架工具高级版

参考资料:

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享