关于脚手架
提到脚手架,想必大家都不陌生,在我们的日常开发当中,不管你是用vue、react还是angular,他们都为开发者提供了一套开箱即用的脚手架工具,就像vue-cli,create-react-app,angular-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/
,我们可以使用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')
命令行解析
脚手架第一个功能就是处理用户的命令,这里需要使用 commander.js(opens new window)。这个库的功能就是解析用户的命令,根据用户的命令执行相应的逻辑操作。代码示例:
#! /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 -V 或 coo —version会显示出当前版本号信息,输入coo -h 或 coo —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(opens new window),可以参考官方文档了解。
注册create 命令
coo create <项目名>命令实现的主要功能
- 拉取远程模板到项目文件夹
- 安装依赖模块
- 运行项目
在终端输入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(opens new window),支持从 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
}
安装依赖模块
命令行交互
在安装依赖包之前,会有一个弹出交互选项,对用户选择使用哪一个包管理工具进行向导,接收用户的选择并作出相应的处理,这个交互要用到inquirer(opens new window),位置:/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
通常我们安装默认包,都是在终端通过npm i 或 yarn的方式来操作,现在我们想要通过代码来实现这个步骤,就要用到node内置模块child_process(opens new window)的spawn方法来实现。位置:/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
}
运行项目
运行项目通常是在终端输入npm run dev 或 yarn dev,那么通过编写代码实现跑在终端里的命令都可以通过node内置模块child_process的spawn或exec方法实现,也可以安装第三方依赖包execa来实现,本质都是调用子进程执行命令。主要逻辑完整代码,位置:/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, 'download packages')(command, commandObj[package].install, { cwd: `./${project}` })
// 3. 启动项目
await commandSpawn(command, commandObj[package].run, { cwd: `./${project}` })
}
module.exports = createProject
项目中需要安装的依赖包
- 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
项目地址
cli-learn(opens new window),当前项目分支 v1
总结 🎉
脚手架工具入门版,逻辑其实很简单,可能一些概念性的东西大家之前不是很清楚,但是我相信你看完一定会有所收获,也相信你也可以自己动手搭一个自己的min脚手架,感兴趣的话也可以继续扩充更多的功能。后续还会推出脚手架工具进阶版和脚手架工具高级版。
参考资料:vue-cli: https://github.com/vuejs/vue-cli
- commander.js: https://github.com/tj/commander.js/blob/master/Readme_zh-CN.md
- npm link: https://docs.npmjs.com/cli/v7/commands/npm-link
- Inquirer.js: https://github.com/SBoudrias/Inquirer.js/