关于脚手架

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

脚手架的好处

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

    实现的功能(入门版)

  • coo -V 查看当前版本号

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

    初始化项目

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

    项目目录搭建

  1. coo-cli
  2. ├─README.md
  3. ├─index.js // 入口文件
  4. ├─package.json
  5. ├─yarn.lock
  6. ├─lib // 主文件
  7. | ├─utils // 一系列工具函数
  8. | | ├─executeCommand.js // 执行进程脚本
  9. | | waitFnLoading.js // loading加载
  10. | ├─linkConfig // 链接配置文件
  11. | | vue-repo.js
  12. | ├─core // 源码
  13. | | ├─create.js // 命令创建入口
  14. | | ├─help.js // 配置信息(用法)
  15. | | ├─actions // 一系列命令脚本
  16. | | | ├─createProject.js // 创建模板脚本
  17. | | | index.js // 统一导出文件

自定义命令

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

  1. {
  2. "name": "coo-cli",
  3. "version": "1.0.0",
  4. "description": "",
  5. "main": "index.js",
  6. "bin": {
  7. "coo": "index.js"
  8. },
  9. "scripts": {
  10. "test": "echo \"Error: no test specified\" && exit 1"
  11. },
  12. "keywords": [],
  13. "author": "",
  14. "license": "ISC"
  15. }

链接到全局环境

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进行解析

  1. #! /usr/bin/env node
  2. console.log('hello coo-cli')

入门 - 图1

命令行解析

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

  1. #! /usr/bin/env node
  2. const program = require('commander')
  3. program.option('-s, --coo', 'this is a coo cli')
  4. program.version(require('./package.json').version).parse(process.argv)

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

  1. const program = require('commander')
  2. const createCommands = () => {
  3. program
  4. .command('create <project> [others...]')
  5. .description('create a new project from remote repository')
  6. .action(function(project) {
  7. console.log(project)
  8. })
  9. }

在终端输入coo create my-project(实际上执行的是 node index.js create my-project),commander 解析到命令 create 和参数 my-project。然后在 action 的回调里取到参数 project(值为 my-project)。
关于process.argv(opens new window),可以参考官方文档了解。 入门 - 图2

注册create 命令

coo create <项目名>命令实现的主要功能

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

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

  1. const program = require('commander')
  2. const { createProjectAction } = require('./actions')
  3. const createCommands = () => {
  4. program
  5. .command('create <object> [others...]')
  6. .description('create a new project from remote repository')
  7. .action(createProjectAction)
  8. }
  9. module.exports = createCommands

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

  1. // 创建项目方法
  2. const createProjectAction = async (project) => {
  3. console.log(project) // my-project
  4. }
  5. module.exports = {
  6. createProjectAction
  7. }

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

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

  1. const { promisify } = require('util')
  2. const download = promisify(require('download-git-repo')) // promise化
  3. const { vuepressRepo } = require('../linkConfig/vue-repo')
  4. const waitFnLoading = require('../../utils/waitFnLoading')
  5. // 创建项目方法
  6. const createProjectAction = async (project) => {
  7. // 1. 拉取远程项目
  8. await waitFnLoading(download, 'initing project...')(vuepressRepo, project, { clone: true })
  9. }
  10. module.exports = {
  11. createProjectAction
  12. }

入门 - 图3

安装依赖模块

命令行交互

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

  1. const { promisify } = require('util')
  2. const download = promisify(require('download-git-repo'))
  3. const inquirer = require('inquirer')
  4. const { vuepressRepo, vueTempRepo } = require('../linkConfig/vue-repo')
  5. const { commandSpawn } = require('../../utils/executeCommand')
  6. const waitFnLoading = require('../../utils/waitFnLoading')
  7. // 创建项目方法
  8. const createProject = async (project) => {
  9. // 1. 拉取远程项目
  10. await waitFnLoading(download, 'init project')(vuepressRepo, project, { clone: true })
  11. // 2. 安装npm包
  12. // 进行向导
  13. const commandObj = {
  14. yarn: {
  15. install: [],
  16. run: ['docs:dev']
  17. },
  18. npm: {
  19. install: ['install'],
  20. run: ['run', 'docs:dev']
  21. }
  22. }
  23. const { package } = await inquirer.prompt({
  24. name: 'package',
  25. type: 'list',
  26. message: 'please select a package manage command',
  27. choices: [
  28. 'yarn',
  29. 'npm'
  30. ]
  31. })
  32. // 系统执行命令兼容
  33. const command = process.platform === 'win32' ? `${package}.cmd` : package
  34. // 下载依赖
  35. await waitFnLoading(commandSpawn, 'installing packages...')(command, commandObj[package].install, { cwd: `./${project}` })
  36. }
  37. module.exports = createProject

入门 - 图4
通常我们安装默认包,都是在终端通过npm i 或 yarn的方式来操作,现在我们想要通过代码来实现这个步骤,就要用到node内置模块child_process(opens new window)的spawn方法来实现。位置:/utils/executeCommand.js

  1. const { spawn } = require('child_process')
  2. const commandSpawn = (...args) => {
  3. return new Promise((resolve, reject) => {
  4. const childProcess = spawn(...args)
  5. // 输出子进程文件流信息
  6. childProcess.stdout.pipe(process.stdout)
  7. // 输出子进程报错信息
  8. childProcess.stderr.pipe(process.stderr)
  9. // 监听close方法(npm模块全部下载完成触发)
  10. childProcess.on('close', () => {
  11. resolve()
  12. })
  13. })
  14. }
  15. module.exports = {
  16. commandSpawn
  17. }

入门 - 图5

运行项目

运行项目通常是在终端输入npm run dev 或 yarn dev,那么通过编写代码实现跑在终端里的命令都可以通过node内置模块child_process的spawn或exec方法实现,也可以安装第三方依赖包execa来实现,本质都是调用子进程执行命令。主要逻辑完整代码,位置:/core/actions/createProject.js

  1. const { promisify } = require('util')
  2. const download = promisify(require('download-git-repo'))
  3. const inquirer = require('inquirer')
  4. const { vuepressRepo, vueTempRepo } = require('../linkConfig/vue-repo')
  5. const { commandSpawn } = require('../../utils/executeCommand')
  6. const waitFnLoading = require('../../utils/waitFnLoading')
  7. // 创建项目方法
  8. const createProject = async (project) => {
  9. // 1. 拉取远程项目
  10. await waitFnLoading(download, 'init project')(vuepressRepo, project, { clone: true })
  11. // 2. 安装npm包
  12. // 进行向导
  13. const commandObj = {
  14. yarn: {
  15. install: [],
  16. run: ['docs:dev']
  17. },
  18. npm: {
  19. install: ['install'],
  20. run: ['run', 'docs:dev']
  21. }
  22. }
  23. const { package } = await inquirer.prompt({
  24. name: 'package',
  25. type: 'list',
  26. message: 'please select a package manage command',
  27. choices: [
  28. 'yarn',
  29. 'npm'
  30. ]
  31. })
  32. // 系统执行命令兼容
  33. const command = process.platform === 'win32' ? `${package}.cmd` : package
  34. await waitFnLoading(commandSpawn, 'download packages')(command, commandObj[package].install, { cwd: `./${project}` })
  35. // 3. 启动项目
  36. await commandSpawn(command, commandObj[package].run, { cwd: `./${project}` })
  37. }
  38. module.exports = createProject

入门 - 图6
项目中需要安装的依赖包