Skip to content
导航栏

前言

作者:winter wang
更新于:11 天前
字数统计:4.1k 字
阅读时长:16 分钟
阅读量:

今天我们来复习&总结青训营实战班第一天的课程,也就是全栈然叔带来的Node基础Api与CLI实战课程。

今天主要学习如何创建一个自己的脚手架工具 🛠️,这样可以避免每次写小demo的时候要进行各种重复配置 🕹️。最后我们发布到npm仓库,让周围的小伙伴也可以使用你写的工具~ 其实我们今天主要要学习的是一种自动化的思想,这也是前端工程化重要的一环。

本文从 ① Node基础 到 ② 脚手架搭建 到 ③ npm仓库上传 ,完全从0到1造轮子,没有基础的小伙伴也能看懂~ PS:这是第一篇,后面还有一篇 我们用happy-path小步走的编码方式再写一个koa的脚手架工具,你会收获更多~~ 点赞 👍 越多更新越快 ✈️

相关的代码在这里https://gitee.com/ykang2020/youth_camp_ykjun

PS: 我废话是真的多.......

一、Node基础API

在字节跳动青训营基础班,我们学习了一些Node.js的基础概念,今天我们先来学习下Node.js的一些基础API,为我们后面实践部分做好铺垫 🏃‍♂️ 🏃‍ ~

PS: 基础班的课程笔记博文在这里:

node.jsAPI 官方文档 中文:http://nodejs.cn/api/ 英文:https://nodejs.org/dist/latest-v14.x/docs/api/

核心API - 无需 require

  • buffer
  • module
  • process

内置API - 需要 require 无需 install

  • os
  • fs
  • path
  • http
  • event

1. fs与异步IO

参考:http://nodejs.cn/api/fs.html 以及 https://nodejs.org/dist/latest-v14.x/docs/api/fs.html

首先引入 fs 文件模块

js
const fs = require('node:fs')

① 同步读取文件

js
// 同步读取
const data = fs.readFileSync('./04_buffer.js')
console.log('data', data.toString())

这里如果不调用 datatoString() 方法,结果则会返回一个 buffer ,因为对于计算机来说,它所存储的所有文件都是二进制文件

使用 datatoString() 方法是默认将二进制文件按 utf-8 转换成文本,也就是默认调用 toString('utf-8')

② 异步读取文件

错误优先的回调函数,在回调函数中进行文件处理操作

js
fs.readFile('./04_buffer.js', (err, data) => {
  if (err)
    throw err
  console.log(data.toString())
})

我们知道使用回调形式进行异步操作不好,我们现在都是用 promise 了,所以自然的想到可以将异步文件读取有没有 promise 风格的API可以使用,然后就可以使用 async/await 来使用异步文件操作

Node.js提供了Promise风格的fs文件操作API,可以看文档

image.png

③ promisify

在Node.js的 util 中提供了一个方法 promisify ,可以将回调的方法修饰成 promise 风格的API,就可以使用 async/await 语法了

js
(async () => {
  const fs = require('node:fs')
  const { promisify } = require('node:util')
  const readFile = promisify(fs.readFile)

  const data = await readFile('./04_buffer.js')
  console.log(data.toString())
})()

2. buffer与字符集

buffer 翻译过来就是缓冲区

分配一个10个字节的 buffer

js
const buf1 = Buffer.alloc(10)
console.log(buf1) // <Buffer 00 00 00 00 00 00 00 00 00 00>

打印输出看看,方便显示阅读,转换成了十六进制,底层当然是二进制存储的

下面使用 buffer 存储一个英文文字,一个字母一个字节

js
const buf2 = Buffer.from('yk')
console.log(buf2) // <Buffer 79 6b>

使用 buffer 存储一个中文文字,一个文字三个字节(utf-8)

js
const buf3 = Buffer.from('')
console.log(buf3) // <Buffer e8 8f 8c>   utf-8/16/32

可以连接两个 buffer

js
const buf4 = Buffer.concat([buf2, buf3])
console.log(buf4, buf4.toString()) // <Buffer 79 6b e8 8f 8c> yk菌

之前也说过,使用 toString 方法就可以显示文字

3. http

使用 http 模块快速写一个 http 服务器

js
const http = require('node:http')

http
  .createServer((request, response) => {
    console.log('a request')
    response.end('Hi YK菌') // 这个end不太好,不好理解
  })
  .listen(3000, () => {
    console.log('Server at 3000')
  })

image.png

我们来看看原型链,先来定义一个函数,来获取对象的原型链

js
function getPrototypeChain(obj) {
  const protoChain = []
  while ((obj = Object.getPrototypeOf(obj)))
    protoChain.push(obj)

  return protoChain
}

来看看 requestresponse 的原型链

js
const http = require('node:http')

http
  .createServer((request, response) => {
    console.log(
      'a request',
      getPrototypeChain(request),
      getPrototypeChain(response)
    )
    response.end('Hi YK node')
  })
  .listen(3000, () => {
    console.log('Server at 3000')
  })

request的原型链

image.png

response的原型链

image.png

可以发现 requestresponse 都是继承自 Stream

这个问题我们后面再讨论,我们继续使用我们的 http 模块来搭建服务器

我们返回一个index.html文件,这就用到我们上面说到的 fs 文件读取API了

js
const fs = require('node:fs')
const http = require('node:http')

http
  .createServer((request, response) => {
    const { url, method } = request
    console.log('url:', url)
    if (url === '/' && method === 'GET') {
      fs.readFile('index.html', (err, data) => {
        if (err) {
          response.writeHead(500, {
            'Content-Type': 'text/plain;charset=utf-8',
          })
          response.end('500 服务器挂了,兄弟!!!')
        }
        response.statusCode = 200
        response.setHeader('Content-Typr', 'text/html')
        response.end(data)
      })
    }
    else {
      response.statusCode = 400
      response.setHeader('Content-Type', 'text/plain;charset=utf-8')
      response.end('404 找不到了呀!')
    }
  })
  .listen(3000, () => {
    console.log('Server at 3000')
  })

练习1 node基础 fs\buffer\http 搭建一个http服务

写一个http服务器,返回index.html

微信截图_20210920104152.png

如果我们的index.html文件中添加了一个img标签,引入了一张图片,我们应该怎么处理?直接用 readFile 读取可以吗?图片一般都比较大,这样会大量消耗内存资源,不要直接使用 readFile 读取, 因为它要把全部图片内容加载到服务器。

那应该用什么? 我们引入下面 Stream 流的概念

4. stream

在本地复制一个图片

js
const fs = require('node:fs')
// 图片复制  使用 fs read+write也可以,但是需要读到内存再从内存中写出去

// 我们换一种方式
const rs = fs.createReadStream('./YK菌.jpg')
const ws = fs.createWriteStream('./YK菌分身.jpg')

// 管道,文件流,从一个文件流到另一个文件
rs.pipe(ws)

文件流,从一个文件流到另一个文件

image.png

所以我们就可以这样编写代码

js
const fs = require('node:fs')
const http = require('node:http')

http
  .createServer((request, response) => {
    const { url, method } = request
    console.log('url:', url)
    if (url === '/' && method === 'GET') {
      fs.readFile('index.html', (err, data) => {
        if (err) {
          response.writeHead(500, {
            'Content-Type': 'text/plain;charset=utf-8',
          })
          response.end('500 服务器挂了,兄弟!!!')
        }
        response.statusCode = 200
        response.setHeader('Content-Typr', 'text/html')
        response.end(data)
      })
    }
    else if (url === '/users' && method === 'GET') {
      response.writeHead(200, { 'Content-Type': 'application/json' })
      response.end(JSON.stringify({ name: 'yk菌' }))
    }
    else if (method === 'GET' && headers.accept.indexOf('image/*' !== -1)) {
      // 拿到所有图片
      // 不要直接使用readFile读取, 因为它要把全部图片内容加载到服务器
      // 使用 流 stream !!!!
      fs.createReadStream(`.${url}`).pipe(response)
    }
    else {
      response.statusCode = 400
      response.setHeader('Content-Type', 'text/plain;charset=utf-8')
      response.end('404 找不到了呀!')
    }
  })
  .listen(3000, () => {
    console.log('Server at 3000')
  })

在index.html添加图片,然后在服务器返回图片的时候使用流的形式返回,这样可以节省内存

image.png

5. 子进程

child_process模块给予Node可以随意创建子进程(child_process)的能力。

它提供了4个方法用于创建子进程。

  • spawn():启动一个子进程来执行命令。
  • exec():启动一个子进程来执行命令,与spawn()不同的是其接口不同,它有一个回调函数获知子进程的状况。
  • execFile():启动一个子进程来执行可执行文件。
  • fork():与spawn()类似,不同点在于它创建Node的子进程只需指定要执行的JavaScript文件模块即可。

二、实战:实现 cli 工具

目标:利用我们学过的Node.js的知识,实现一个自己的脚手架工具,脚手架用来快速创建一个项目,一个会自动生成前端路由的模板项目。我们叫它约定路由

1. 初始化

新建一个项目目录,并初始化

powershell
mkdir vue-auto-router-cli
cd vue-auto-router-cli
npm init -y

创建一个文件夹bin,然后创建一个yk.js文件,作为入口文件

要让全局可以使用 yk 指令,在 package.json 中加入这个配置项

json
"bin": {
  "yk": "./bin/yk.js"
},

在 yk.js 头部加上,用来指定解释器类型为node

js
#!/usr/bin/env node
console.log('欢迎使用YK菌的 cli 工具')

最后别忘了 link 一下

powershell
npm link

此时在任意目录下的控制台中执行 yk 指令 后台就都会用 node 执行yk.js文件

image.png

2. 定制命令行页面

接下来我们来定制一个我们自己的命令行页面

js
#!/usr/bin/env node
console.log('欢迎使用YK菌的 cli 工具')
console.log(process.argv)

image.png

可以看到,用户输入的信息都保存在process.argv

我们先安装一下接下来要用到的所有的第三方库

powershell
npm i commander download-git-repo ora@5 handlebars figlet clear chalk open -s

这里我们使用一个第三方库 commander 来定制我们的命令行

我们的yk.js这样编写

js
#!/usr/bin/env node
// console.log("欢迎使用YK菌的 cli 工具");
// console.log(process.argv);
const program = require('commander')

program.version(require('../package.json').version)

program
  .command('init <name>')
  .description('init project')
  .action((name) => {
    console.log(`init ${name}`)
  })
program.parse(process.argv)

现在再使用我们的 yk 指令,就像点样子了~

image.png

3. 定制初始化项目的欢迎界面

我们将program.action中不可能只打印一句话,我们把初始化项目的逻辑抽离出来

js
program.action(require('../lib/init'))

我们就创建文件 lib/init.js

js
// 打印欢迎界面
const { promisify } = require('node:util')
const figlet = promisify(require('figlet'))
const clear = require('clear')
const chalk = require('chalk')
// 封装一个输出绿色文字的API
const log = content => console.log(chalk.green(content))

module.exports = async (name) => {
  // 打印欢迎界面
  clear()

  const data = await figlet.textSync('YK!Welcome')
  log(data)
}

此时使用 yk 初始化一个项目的时候,就会出现欢迎页面了~

image.png

当然这里的文字的样式可以自己定制 可以看看文档 https://www.npmjs.com/package/figlet

这里我就随便配置一个字体样式了~

js
const data = await figlet.textSync('YK!Welcome', {
  font: 'Ghost',
  horizontalLayout: 'default',
  verticalLayout: 'default',
  width: 200,
  whitespaceBreak: true,
})

image.png

4. 下载代码模板

我们新建一个文件用来处理下载代码模板的逻辑

新建 lib/download.js

js
const { promisify } = require('node:util')

module.exports.clone = async function (repo, desc) {
  const download = promisify(require('download-git-repo'))

  const ora = require('ora') // 进度条
  const process = ora(`✈下载.......${repo}`)

  await process.start()
  await download(repo, desc)
  process.succeed()
}

这里用到两个库 download-git-repo 库用来下载代码模板 和 ora 库用来显示下载进度条(这里我们安装的是5版本,最新的是6版本,为什么不用最新版呢,因为最新版只支持ESM模块化引入)

在GitHub上面准备一个代码模板

image.png

在init.js中引入并使用下载模块即可

js
const { clone } = require('./download')

module.exports = async (name) => {

  log(`创建项目${name}`)
  // 这里的git仓库可以自己指定
  await clone('github:yk2012/vue-template', name)
}

image.png

可以看到文件就已经被下载到当前目录下了

image.png

5. 自动安装项目依赖

在Node.js中使用子进程 child_process 安装依赖

创建一个子进程安装的 promise 风格的接口,然后还要合并子进程的流到主进程中

js
const spawn = async (...args) => {
  // 同步Promise api
  const { spawn } = require('node:child_process')
  return new Promise((resolve) => {

    // windows系统兼容性处理
    const options = args[args.length - 1]
    if (process.platform === 'win32')
      options.shell = true

    const proc = spawn(...args)
    // 输出流 子进程 合并到 主进程
    proc.stdout.pipe(process.stdout)
    proc.stderr.pipe(process.stderr)
    proc.on('close', () => {
      resolve()
    })
  })
}

注意中间有一段处理Windows兼容性问题的代码。

调用子进程安装依赖 (就是在项目目录下执行npm install

js
// 下载依赖 npm i
// 子进程
// spawn("npm", ["install"]);
log('安装依赖....')
await spawn('npm', ['install'], { cwd: `./${name}` })

6. 配置自动打开服务

js
log(
  chalk.green(
      `
    安装完成:
    To get Starat :
    ================================
      cd ${name}
      npm run serve
    ================================
    `
  )
)
open('http://localhost:8080/')
await spawn('npm', ['run', 'serve'], { cwd: `./${name}` })

完整的演示

image.png

自动跳转链接,打开前端项目页面

image.png

7. 实现约定路由

根据views目录中的页面,自动生成路由,也就是说,我们在views目录下面添加一个新的组件,然后执行一下命令,就会自动配置路由在页面,这也是开发中比较常用的功能了~ 不用手动配置,程序自动配置这不是很香嘛~~

在lib文件夹中创建refresh.js文件,这里编写我们的代码逻辑

js
// 读文件列表

// 拼代码。模板渲染的方式

const fs = require('node:fs')
const handlebars = require('handlebars')
const chalk = require('chalk')

module.exports = async () => {
  // 获取列表
  const list = fs
    .readdirSync('./src/views')
    .filter(v => v !== 'Home.vue')
    .map(v => ({
      name: v.replace('.vue', '').toLowerCase(),
      file: v,
    }))

  // 生成路由定义
  compile({ list }, './src/router.js', './template/router.js.hbs')
  // 生成菜单
  compile({ list }, './src/App.vue', './template/App.vue.hbs')

  /**
   *
   * @param {*} meta 数据定义
   * @param {*} filePath  目标文件
   * @param {*} templatePath 模板
   */
  function compile(meta, filePath, templatePath) {
    if (fs.existsSync(templatePath)) {
      const content = fs.readFileSync(templatePath).toString()
      const result = handlebars.compile(content)(meta)
      fs.writeFileSync(filePath, result)
      console.log(chalk.green(`${filePath}创建成功`))
    }
  }
}

在yk.js文件中加入这个指令

js
program
  .command('refresh')
  .description('refresh routers...')
  .action(require('../lib/refresh'))

我们新建在views文件夹下面新建一个组件Yk.vue

然后执行 yk refresh

image.png

可以看到效果

image.png

三、发布到npm中央仓库

详细操作可以看我之前的博文 【npm】发布 自定义JS工具函数库 到npm中央仓库

上次我们是一行一行的敲的,这次我们也采用自动化的方式,写一个.sh文件

publish.sh

sh
#!/usr/bin/env bash
npm config get registry # 检查仓库镜像库
npm config set registry=https://registry.npmjs.org
echo '请进行登录相关操作:'
npm login # 登陆
echo "-------publishing-------"
npm publish # 发布
npm config set registry=https://registry.npm.taobao.org # 设置为淘宝镜像
echo "发布完成"
exit

我来修改下我这个包名为 vue-auto-router-cli-yk 版本为 1.0.0

js
{
  "name": "vue-auto-router-cli-yk",
  "version": "1.0.0",
  "description": "这是YK菌写的vue脚手架工具",
  "main": "index.js",
  "bin": {
    "yk": "./bin/yk.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "chalk": "^4.1.2",
    "clear": "^0.1.0",
    "commander": "^8.2.0",
    "download-git-repo": "^3.0.2",
    "figlet": "^1.5.2",
    "handlebars": "^4.7.7",
    "open": "^8.2.1",
    "ora": "^5.4.1"
  }
}

最后我们执行sh脚本文件

Windows的电脑还不能直接执行,但是我装了git,git bash可以打开,然后进去文件夹发现可以直接双击打开,哈哈,那我就双击打开了~

image.png

如果你看过我之前的博文【npm】发布 自定义JS工具函数库 到npm中央仓库,那其实你也不用那么麻烦,直接命令行换个镜像然后npm publish 就行了

也就是这三步

powershell
npm config set registry=https://registry.npmjs.org
npm publish
npm config set registry=https://registry.npm.taobao.org

image.png

最后去npm官网看看 https://www.npmjs.com/

image.png

这时候就可以让周围的小伙伴使用你刚刚编写的脚手架工具啦~

powershell
npm install vue-auto-router-cli-yk -g
yk init vue_demo

四、总结

今天我们主要学习了一下Node.js的基础API,然后我们写了一个简单的脚手架,并发布到了npm仓库。下一篇,我们来写一个koa框架的脚手架,更加复杂一点,而且我们会使用happy-path小步走的思路来编写代码。有了今天 子进程 的基础,下一篇的内容也会很好理解~

最后,可以留个言点个赞吗? 点个关注那就再好不过了,下面还有专栏链接,可以关注一波,及时收到青训营笔记系列的最新博文消息。

Contributors

winter wang