一步一步搭建脚手架

咱们在使用 vue-clicreate-react-app 的时候,只要执行一个简单的命令 vue init app 或是 create-react-app app 就是快速建立出一个可直接使用的项目模板,极大地提升了开发效率。javascript

本文提供了一个开发简易脚手架的过程。vue

准备工做

第三方工具

  • comandertj 大神出品的nodejs命令行解决方案,用于捕获控制台输入的命令;
  • chalk:命令行文字配色工具;
  • cross-spawn:跨平台的 node spawn/spawnSync 解决方案;
  • fs-extranodejs fs 的增强版,新增了API的同时,也包含了原fsAPI
  • handlebars:一个字符串模板工具,能够将信息填充到模板的指定位置;
  • inquirer:交互式命令行用户界面集合,用于使用者补充信息或是选择操做;
  • log-symbols:不一样日志级别的彩色符号标志,包含了 infosuccesswarningerror 四级;
  • ora:动态加载操做符号;

初始化项目

首先,这仍然是一个 nodejs 的工程项目,因此咱们新建一个名为 scaffold-demo 的文件夹,并使用 npm init 来初始化项目。此时,项目中只有一个 package.json 文件,内容以下:java

{
  "name": "scaffold-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}
复制代码

而后咱们删除 "main": "index.js",加入 "private": falsenode

main:是程序的主要入口点,就是说若是有其余用户 installrequrie 这个包,那么将返回该文件 export 出来的对象。react

private:是为了保护私有库的手段,当你的库是私有库的时候,加入 "private": true,那么npm将会拒绝发布这个库。git

咱们在使用其余脚手架时,在控制台中输入一段简短的命令就能快速建立一个项目模板,那么他们是如何使用命令行来操做运行项目的呢,答案就在 npmpackage.jsonbin 字段值中。github

bin 字段接受一个 k-v 的Map,其中 key 表示命令名称,value表示命令执行的入口文件。当设置了bin字段后,一旦安装了你的 packagenpm将会这个命令注册到全局中,并连接对应的文件,而后用户就能够直接使用该命令了。vue-cli

详见:npm bin 官方文档npm

咱们须要在 package.json 文件中加入如下内容,其中这个 sd 就是咱们命令:json

"bin": {
  "sd": "./main.js"
},
复制代码

而后在项目中新建 main.js 文件,内容以下:

#!/usr/bin/env node

console.log('Hello Bin')
复制代码

其中 #!/usr/bin/env node 的做用就是这行代码是当系统运行到这一行的时候,去 env 中查找 node 配置,而且调用对应的解释器来运行以后的 node 程序。

而后咱们执行命令 npm link 或是 npm install -g,这样将本项目的命令注册到了全局中,而后在命令行中执行 sd 就能看到结果 Hello Bin

first-npm

npm link :将当前 package 连接到全局执行环境。

npm install -g:将当前 package 全局安装到本地。

对应的解除命令为: npm unlink 或是 npm uninstall -g

正式开始

如今咱们已经可以完成最基础的命令行操做了,继续构建咱们简易脚手架。

1. 捕获命令信息

在上文,咱们设置了bin信息,可是只有一个命令名称信息,可是在其余脚手架中,咱们能够输入多个字段,如 create-react-app appcreate-reate-app 表示命令,app表示建立的项目的名称。而这种捕获命令行的操做咱们能够借助 comander 来完成。

实际上,vuereact 的脚手架也是借助 comander 完成的。

咱们将 main.js 作以下修改:

#!/usr/bin/env node
const program = require('commander')

program
  .command('init <name>')
  .description('初始化模板')
  .action(name => {
    console.log('Hello ' + name)
  })

program.parse(process.argv)
复制代码

而后在命令行输入 sd init firstApp,就能看到返回 Hello firstApp了。

npm-firstApp

在上述代码中,command 函数表示当前命令的一个子命令,能够设置多个,紧随的 description 用于描述该命令,action 表示输入命令后须要执行的操做。其中 command 中的尖括号(<>)表示该参数为必须输入的,中括号([])表示为可选的。 program.parse(process.argv) 必需要,若是没有则不会起做用。

更详细例子参考官网的例子:github.com/tj/commande…

2. 复制项目模板至指定目录

在本文中咱们采用的本地项目模板复制的方式,即本脚手架中包含了所须要初始项目的模板文件,位于Template文件夹下(这个目录开发者能够随意修改)。

若是想使用在线模板的方式,能够借助工具 download-git-repo,将 copy 换成下载便可。

本文的 template 内容见文末的代码仓库。

而后咱们将 action 中的逻辑替换成以下内容:

action(async name => {
	// 判断用户是否输入应用名称,若是没有设置为 myApp
  const projectName = name || 'myApp'
  // 获取 template 文件夹路径
  const sourceProjectPath = __dirname + '/template'
  // 获取命令所在文件夹路径
  // path.resolve(name) == process.cwd() + '/' + name
  const targetProjectPath = path.resolve(projectName)

  // 建立一个空的文件夹
  fs.emptyDirSync(targetProjectPath)

  try {
    // 将模板文件夹中的内容复制到目标文件夹(目标文件夹为命令输入所在文件夹)
    fs.copySync(sourceProjectPath, targetProjectPath)
    console.log('已经成功拷贝 Template 文件夹下全部文件!')
  } catch (err) {
    console.error('项目初始化失败,已退出!')
    return
  }
}
复制代码

3. 确认目标文件夹是否存在(命令行交互)

咱们已经完成了最基础简单的目标文件复制的过程,可是在实际过程当中,颇有可能存在用户输入的文件夹已经存在了的状况,因此咱们须要询问用户是要覆盖原文件夹内容仍是退出从新操做。这一块的操做咱们使用 inquirer 来完成,inquirer 能够提供命令行的用户交互功能。

咱们在建立空文件夹以前加入一下判断文件是否存在的代码。

// 判断文件夹是否存在
if (fs.existsSync(targetProjectPath)) {
  console.log(`文件夹 ${projectName} 已经存在!`)
  try {
    // 若存在,则询问用户是否覆盖当前文件夹的内容,yes 则覆盖,no 则退出。
    const { isCover } = await inquirer.prompt([
      { name: 'isCover', message: '是否要覆盖当前文件夹的内容', type: 'confirm' }
    ])
    if (!isCover) {
      return
    }
  } catch (error) {
    console.log('项目初始化失败,已退出!')
    return
  }
}
复制代码

请注意这里使用了 async - await

app-exist

4. 美化命令行 console

如今的命令行都是单调的白色字,咱们使用 chalklog-symbols 来实现命令行的美化。主要代码以下:

主要改了 console 部分的代码,使用 log-symbols 添加输出标识, chalk 改变文字颜色。

action(async name => {
  // 判断用户是否输入应用名称,若是没有设置为 myApp
  const projectName = name || 'myApp'
  // 获取 template 文件夹路径
  const sourceProjectPath = __dirname + '/template'
  // 获取命令所在文件夹路径
  // path.resolve(name) == process.cwd() + '/' + name
  const targetProjectPath = path.resolve(projectName)

  // 判断文件夹是否存在及其后续逻辑
  if (fs.existsSync(targetProjectPath)) {
    console.log(symbols.info, chalk.blue(`文件夹 ${projectName} 已经存在!`))
    try {
      const { isCover } = await inquirer.prompt([
        { name: 'isCover', message: '是否要覆盖当前文件夹的内容', type: 'confirm' }
      ])
      if (!isCover) {
        return
      }
    } catch (error) {
      console.log(symbols.fail, chalk.red('项目初始化失败,已退出!'))

      return
    }
  }
  // 建立一个空的文件夹
  fs.emptyDirSync(targetProjectPath)

  try {
    // 将模板文件夹中的内容复制到目标文件夹(目标文件夹为命令输入所在文件夹)
    fs.copySync(sourceProjectPath, targetProjectPath)
    console.log(symbols.success, chalk.green('已经成功拷贝 Template 文件夹下全部文件!'))
  } catch (err) {
    console.error(symbols.fail, chalk.red('项目初始化失败,已退出!'))
    return
  }
})
复制代码

美化前:

console-normal

美化后:

console-beautify

5. 修改 package.json

有些时候,咱们须要根据用户输入来修改填充 package.json,就像 npm init 的时候输入的信息。在这里咱们使用 inquirer 获取用户输入,使用 handlebars 来将用户输入填充到 package.json 中去。

在拷贝文件夹后加入如下代码:

// 获取项目的描述及做者名称等信息
const { projectDescription, projectAuthor } = await inquirer.prompt([
  { name: 'projectDescription', message: '请输入项目描述' },
  { name: 'projectAuthor', message: '请输入做者名字' }
])

const meta = {
  projectAuthor,
  projectDescription,
  projectName
}

// 获取拷贝后的模板项目中的 `package.json`
const targetPackageFile = targetProjectPath + '/package.json'
if (fs.pathExistsSync(targetPackageFile)) {
  // 读取文件,并转换成字符串模板
  const content = fs.readFileSync(targetPackageFile).toString()
  // 利用 handlebars 将须要的内容写入到模板中
  const result = handlebars.compile(content)(meta)
  fs.writeFileSync(targetPackageFile, result)
} else {
  console.log('package.json 文件不存在:' + targetPackageFile)
}
复制代码

至此,咱们的简易脚手架已经基本搭建完成了,可以在指定文件夹生成项目模板文件。可是,咱们若是使用 create-react-app 的话,就会发现只要你一执行命令就会它帮你自动安装依赖,并且也会自动初始化 Git

如今咱们就来完成这两个功能。

6. 安装依赖

// 经过执行命令 yarn --version 的方式,来判断本机是否已经安装了 yarn
// 若是安装了,后续就使用yarn,不然就使用 npm;
function canUseYarn() {
  try {
    spawn('yarnpkg', ['--version'])
    return true
  } catch (error) {
    return false
  }
}

function tryYarn(root) {
  return new Promise((resolve, reject) => {
    let child
    const isUseYarn = canUseYarn()
    if (isUseYarn) {
      // 这里就至关于命令行中执行 `yarn`
      child = spawn('yarnpkg', ['--cwd', root], { stdio: 'inherit' })
    } else {
      // 这里就至关于命令行中执行 `npm install`
      child = spawn('npm', ['install'], { cwd: root, stdio: 'inherit' })
    }
		// 当命令执行完成的时候,判断是否执行成功,并输出相应的输出。
    child.on('close', code => {
      if (code !== 0) {
        reject(console.log(symbols.error, chalk.red(isUseYarn ? 'yarn' : 'npm' + ' 依赖安装失败...')))
        return
      }
      resolve(console.log(symbols.success, chalk.green(isUseYarn ? 'yarn' : 'npm' + ' 依赖安装完成!')))
    })
  })
}
复制代码

这里须要注意的是执行命令语句 spawn('yarnpkg', ['--cwd', root], { stdio: 'inherit' })

上述语句至关于命令行中执行 yarn,可是咱们必须加上 '--cwd' 来将其执行路径修改成命令所在的目录,由于 spawn 默认执行目录是脚手架目录。同时又由于 spawn 是开了一个子线程,因此若是你不使用 { stdio: 'inherit' },那么你将看不到 yarn 安装的过程。

参考博客:Node.js child_process模块解读

stdio 选项用于配置父进程和子进程之间创建的管道,因为 stdio 管道有三个(stdin, stdout, stderr)所以 stdio 的三个可能的值实际上是数组的一种简写

  • pipe 至关于 ['pipe', 'pipe', 'pipe'](默认值)
  • ignore 至关于 ['ignore', 'ignore', 'ignore']
  • inherit 至关于 [process.stdin, process.stdout, process.stderr]

而后在修改 package.json 代码后面添加如下代码便可。

// 安装依赖
await tryYarn(targetProjectPath)
复制代码

7. 初始化 Git

而后咱们进行git的初始化,即执行 git init

function tryInitGit(root) {
  // 本来模板中,咱们就存放了 gitignore 模板文件,须要将其内容复制到新建的 .gitignore 文件中
  try {
    // 若是项目中存在了 .gitignore 文件,那么这个 API 会执行失败,跳入 catch 分支进行合并操做
    fs.moveSync(path.join(root, 'gitignore'), path.join(root, '.gitignore'))
  } catch (error) {
    const content = fs.readFileSync(path.join(root, 'gitignore'))
    fs.appendFileSync(path.join(root, '.gitignore'), content)
  } finally {
    // 移除 gitignore 模板文件
    fs.removeSync(path.join(root, 'gitignore'))
  }

  try {
    spawn('git', ['init'], { cwd: root })
    spawn('git', ['add .'], { cwd: root })
    spawn('git', ['commit', '-m', 'Initial commit from New App'], { cwd: root })
    console.log(symbols.success, chalk.green('Git 初始化完成!'))
  } catch (error) {
    console.log(symbols.error, chalk.red('Git 初始化失败...'))
  }
}
复制代码

而后在安装依赖以后加入如下代码:

// 初始化 git
tryInitGit(targetProjectPath)
复制代码

completed

总结

本文代码仓库:github.com/Huanqiang/s…

本文总结了我的在搭建简易脚手架的过程,功能过于简单,算是一个小小的开端吧。

最后不禁感叹 nodejs 仍是很是之强悍的!

相关文章
相关标签/搜索