转载自:慕课网课件/ 大前端试听课 /

步骤一 · 前端工程化和进阶调试技巧

前言

什么是脚手架?

脚手架是为了保证各施工过程顺利进行而搭设的工作平台。——百度百科

下面看一段vue-cli的官方解释:

Vue CLI 是一个基于 Vue.js 进行快速开发的完整系统,Vue CLI 致力于将 Vue 生态中的工具基础标准化。它确保了各种构建工具能够基于智能的默认配置即可平稳衔接,这样你可以专注在撰写应用上,而不必花好几天去纠结配置的问题。与此同时,它也为每个工具提供了调整配置的灵活性,无需 eject。

简单来说,脚手架就是「为了减少重复性工作而做的工具」
通常我们使用vue-cli创建项目会如下图所示,用交互的方式,选择用户需要的功能,最后创建一个项目,那么它内部是怎么实现的呢?今天我们这篇文章就简单介绍一下vue-cli工具的核心原理,以及实现一个简单的cli工具。
CLI脚手架工作原理 - 图1

基本组成

通常来说,cli工具都必须用到下面的一些插件。

  • commander
    • 作用:解析参数
  • inquirer
    • 作用:交互式命令作用
  • download-git-report
    • 作用:在官网上下载模板
  • chalk
    • 作用:在命令行增加色彩
  • metalsmith
    • 作用:读取文件,实现模板渲染
  • consoledate

    • 作用:统一的模板引擎(比如:对ejs的解析)

      核心代码的实现

      下面,会通过代码的形式,一步一步的来做一个简单的CLI的项目。
      首先,我新建了一个项目,并且进行npm init的初始化工作。
      新建一个bin的目录,然后在该目录下新建www的文件。
      package.json中,把bin指向bin目录下的www.js文件。
      如下图
      CLI脚手架工作原理 - 图2
      bin/www.js 的文件内容
      1. #! /usr/bin/env node
      2. console.log('111')
      这个时候,执行npm link就可以把package.js中配置的bin命令生效了。试一下:
      CLI脚手架工作原理 - 图3

      利用Commander来解析用户的参数

      这里需要安装Commander的插件了。
      npm i commander<br />如何查看用户传递过来的参数呢?<br />先对bin/www.js`文件进行如下修改
      1. require('../src/main');
      新建src目录,新建main.js 和 constance.js
      1. // constance.js
      2. const { version } = require('../package.json');
      3. module.exports = {
      4. version
      5. }
      6. // main.js
      7. const program = require('commander')
      8. console.log(process.argv)
      9. const { version } = require('./constants.js')
      10. program.version(version).parse(process.argv)
      上述代码做了这几件事情:
  • 从package里拿到当前版本

  • 解析用户传递的参数

当我们运行toimooc --version的时候,会输出以下内容。
CLI脚手架工作原理 - 图4

创建一个create的命令

  1. // main.js
  2. const program = require('commander')
  3. const { version } = require('./constants.js')
  4. program
  5. .command('create')
  6. .alias('c')
  7. .description(' create a project')
  8. .action(() => {
  9. console.log('done')
  10. })
  11. // 解析用户传递的参数
  12. program.version(version).parse(process.argv)

上述命令通过command创建了一个create的命令,别名是c. 操作的内容是打印done
当我们运行toimooc create的时候,会打印如下内容:
CLI脚手架工作原理 - 图5
通常情况下,需要的命令一定不止create一个,这样的话,就需要对命令进行封装了。
下面对该部分代码进行封装

  1. // main.js
  2. // 定义映射对象
  3. const mapActions = {
  4. create: {
  5. alias: 'c',
  6. description: 'create a project',
  7. examples: [
  8. 'toimooc-cli create <project-name>'
  9. ]
  10. },
  11. config: {
  12. alias: 'c',
  13. description: 'config project variable',
  14. examples: [
  15. 'toimooc-cli config set <k> <v>',
  16. 'toimooc-cli config get <k>',
  17. ]
  18. },
  19. '*': {
  20. alias: 'c',
  21. description: 'command not found',
  22. examples: []
  23. }
  24. }
  25. // 循环映射对象
  26. Reflect.ownKeys(mapActions).forEach(action => {
  27. program
  28. .command(action)
  29. .alias(mapActions[action].alias)
  30. .description(mapActions[action].description)
  31. .action(() => {
  32. // console.log('done')
  33. if(action === '*') {
  34. console.log(mapActions[action].description)
  35. } else {
  36. console.log(action)
  37. }
  38. })
  39. })

为了代码的层次化结构更加清晰,action里面的所有操作, 需要拆分到独立的js文件中,使每个函数文件独立完成自己需要完成的事情。
新建src/create.js文件:

  1. module.exports = (projectName) => {
  2. console.log('create', projectName)
  3. }

修改Main.js

  1. Reflect.ownKeys(mapActions).forEach(action => {
  2. program
  3. .command(action)
  4. .alias(mapActions[action].alias)
  5. .description(mapActions[action].description)
  6. .action(() => {
  7. // console.log('done')
  8. if(action === '*') {
  9. console.log(mapActions[action].description)
  10. } else {
  11. require(path.resolve(__dirname, action))(...process.argv.slice(3))
  12. }
  13. })
  14. })

此时执行create projectName就会打印出我们需要的内容:
CLI脚手架工作原理 - 图6

监听help事件

通常用户会使用help来查看帮助命令,这个是如何实现的呢?

  1. // 监听用户的help事件
  2. program.on('--help', () => {
  3. Reflect.ownKeys(mapActions).forEach(action => {
  4. mapActions[action].examples.forEach(example => {
  5. console.log(' ' + example)
  6. })
  7. })
  8. })

CLI脚手架工作原理 - 图7

从GitHub 拉取代码

github官网提供了拉取代码的Api接口
通过这个地址可以获取我在github上面所有的项目https://api.github.com/users/haimingyue/repos](https://api.github.com/users/haimingyue/repos)
user/后面的haimingyue是我的github的名称。可以替换成你个人的。

完善create文件代码

create的功能是创建项目,拉取所有的项目列表,让用户选择安装哪个项目。

  1. const axios = require('axios');
  2. // 获取项目列表
  3. const fetchRepoList = async () => {
  4. const { data } = await axios.get('https://api.github.com/users/haimingyue/repos');
  5. return data;
  6. }
  7. module.exports = async (projectName) => {
  8. // console.log('create', projectName)
  9. let repos = await fetchRepoList();
  10. repos = repos.map((item) => item.name)
  11. console.log(repos)
  12. }

CLI脚手架工作原理 - 图8
接下来需要用到oraInquirer插件。

  • ora的作用是Loading效果
  • Inquirer的作用是和用户进行交互式命令
    1. // 封装loading效果
    2. const waitFnloading = (fn, message) => async(...args) => {
    3. const spinner = ora(message)
    4. spinner.start()
    5. const result = await fn(...args)
    6. spinner.succeed()
    7. return result;
    8. }
    9. //根据github接口,获取项目的tag信息
    10. const fetchTagList = async () => {
    11. const { data } = await axios.get('https://api.github.com/repos/haimingyue/mmall-fe/tags');
    12. return data;
    13. }
    14. // 使用Inquirer实现交互式命令
    15. module.exports = async (projectName) => {
    16. let repos = await waitFnloading(fetchRepoList, 'fetching template')()
    17. repos = repos.map((item) => item.name)
    18. const { repo } = await Inquirer.prompt({
    19. name: 'repo',
    20. type: 'list',
    21. message: 'please choise a template to create project',
    22. choices: repos
    23. })
    24. let tags = await waitFnloading(fetchTagList, 'fetching tags')(repo)
    25. const { tag } = await Inquirer.prompt({
    26. name: 'tag',
    27. type: 'list',
    28. message: 'please choise a tag',
    29. choices: tags
    30. })
    31. tags = tags.map(item => item.name)
    32. console.log('tags', tags)
    33. }
    完成效果如下:
    CLI脚手架工作原理 - 图9

    使用download-git-repo下载

    这一步的主要目的是使用download-git-repo插件下载仓库的文件,并保存到本地的临时文件夹内。
    1. // constants.js
    2. // 选择下载模板的目录
    3. const downloadDirectory = `${process.env[process.platform === 'darwin' ? 'HOME' : 'USERPROFILE']}/.template`
    4. module.exports = {
    5. downloadDirectory
    6. }
    7. // create.js
    8. const { promisify } = require('util');
    9. let downloadGitRepo = require('download-git-repo');
    10. downloadGitRepo = promisify(downloadGitRepo);
    11. const downloadDirectory = require('./constants');
    12. // 封装拉取项目的代码
    13. const download = async (repo, tag) => {
    14. let api = `haimingyue/${repo}`
    15. if(tag) {
    16. api+=`#${tag}`
    17. }
    18. const dest = `${downloadDirectory}/${repo}`;
    19. await downloadGitRepo(api, dest)
    20. return dest;
    21. }
    22. module.exports = async (projectName) => {
    23. let repos = await waitFnloading(fetchRepoList, 'fetching template')()
    24. repos = repos.map((item) => item.name)
    25. const { repo } = await Inquirer.prompt({
    26. name: 'repo',
    27. type: 'list',
    28. message: 'please choise a template to create project',
    29. choices: repos
    30. })
    31. let tags = await waitFnloading(fetchTagList, 'fetching tags')(repo)
    32. const { tag } = await Inquirer.prompt({
    33. name: 'tag',
    34. type: 'list',
    35. message: 'please choise a tag',
    36. choices: tags
    37. })
    38. tags = tags.map(item => item.name)
    39. const result = await download(repo, tag)
    40. console.log('tags', tags)
    41. }

    把下载好的文件赋值到当前文件夹

    ncp Asynchronous recursive file & directory copying

下载复制文件夹是需要使用插件ncp的。
npm install ncp

  1. const path = require('path');
  2. let ncp = require('ncp');
  3. ncp = promisify(ncp);
  4. ...
  5. module.exports = async (projectName) => {
  6. ... // 其他代码
  7. const result = await waitFnloading(download, 'do') (repo, tag)
  8. ncp(result, path.resolve(projectName))
  9. }

这里把临时文件复制到当前的文件夹。
CLI脚手架工作原理 - 图10
到这里,CLI工作原理的介绍就基本完成了。
这里只是简单的实现了模板的下载过程,复杂的cli工具还需要处理文件是否存在,ejs等模板引擎的解析等等。完成CLI的代码之后,发布到npm上面,就可以被用户使用了。

检测到您还没有关注慕课网服务号,无法接收课程更新通知。请扫描二维码即可绑定

下一节
1-4 CLI脚手架工作原理
播放下一节
重新观看

章节问答笔记资料

+ 10 MP看完一节视频

步骤一 大前端试听课

· 走进大前端世界,拒绝迷茫

· 前端工程化和进阶调试技巧

第1章 场景二:前端工程化如何入门到进阶?

· 全栈开发通用Web后台框架

· 自动化&缺陷控制+多端展望

· 大前端