官网:https://nodejs.org/en/

官方文档:https://nodejs.org/en/docs/guides/

Node.js 是什么

  • Node.js is a JS runtime built on Chrome’s V8 JS engine (Node.js是一个基于Chrome V8引擎的JS运行时)
  • Node.js是一个异步的事件驱动的JS运行时

什么叫运行时,用类比的方法学习这个概念,一般语言都会提供一个运行时环境,Node.js其实就是提供js运行时的环境

  • JRE java运行时环境
  • C Runtime
  • .NET Common Language Runtime

runtime(运行时)指的是程序运行的时候,即指令加载到内存并由CPU执行的时候。运行时库就是程序运行的时候所依赖的库。

JS是解释型语言,还有一些语言执行之前是要编译的,比如C,Java等

C代码编译成可执行文件的时候,指令没有被CPU执行,这个时候算是编译时,也就是编译的时候。

Node.js历史 - 为性能而生

Ryan Dahl /‘raiən da:l/ 是nodejs的作者,他的工作是用C/C写高性能web服务,对于高性能,异步IO、事件驱动是基本原则,但用C/C写就太痛苦了。于是他设想使用另一种高级语言开发Web服务,评估了很多语言,发现很多语言虽然同是提供了异步IO和同步IO,但开发人员一旦使用了同步IO,他们就再也懒得写异步IO了,所以最终Ryan选择了JS。

因为JS是单线程执行,根本不能进行同步IO操作,所以JS的这一”缺陷”导致了它只能使用异步IO。

选定开发语言后,还要有运行时引擎。Ryan曾考虑自己写一个,不过明智的放弃了,因为V8就是开源的JS引擎,让Google投资去优化V8,而他只拿来用,还不用付钱。于是2009年,Ryan正式推出了基于JS和V8引擎的开源Web服务项目,命名为Node。Node第一次把JS带入到后端服务器开发,加上JS开发人员众多,Node一下子就火了起来。

Node.js依赖(Dependencied)

https://nodejs.org/en/docs/meta/topics/dependencies/

Node.js功能依赖一些库和工具:

Libraries

  • V8 由google维护的JS运行时引擎。V8为Node.js提供了JS引擎,Node.js通过V8 C++ API对其进行控制。
  • libuv 另一个重要的依赖是libuv, 它一个C语言库,用于将非阻塞(异步)IO操作封装为受支持平台之间一致的接口。他提供了处理file system、DNS、netwrok、child process(子进程),pipes(管道),信号处理(signal handling),polling(轮询) and streaming(流传输)的机制. 它还包括了一个线程池(thread pool),用于分担某些无法在操作系统级别异步完成的工作。

libuv is a multi-platform support library with a focus on asynchronous I/O. It was primarily developed for use by Node.js

  • llhttp Node.js HTTP parsing(解析)是通过llhttp来处理,一个轻量级的TypeScript和C库。他的设计宗旨是不进行任何系统调用(syscalls)或分配(allocations),因此每个请求的内存占用非常小。
  • c-ares 对于某些异步DNS请求,Node.js使用了c-ares来处理。它是一个C库,It is exposed through the DNS module in JavaScript as the resolve() family of functions.
  • OpenSSL tlscrypto模块中都广泛使用了OpenSSL,它提供了许多经过考验的加密功能实现,web依赖这些功能来保证安全性
  • zlib 为了进行快速压缩何解压缩,Node.js依赖于行业标准的zlib库,该库因在gzip何libpng中的使用而闻名。Node.js使用zlib创建同步、异步及流式压缩何解压缩接口。

Tools

  • npm node package manager, 是Node.js的模块管理器,Node.js is all about modularity,and with that comes the need for a quality package manager; for this purpose, npm was made. With npm comes the largest selection of community-created packages of any programming ecosystem, which makes building Node.js app quick and easy.
  • gyp build系统由gyp处理,它是从V8复制的基于python的项目生成器。可以生成用于跨多个平台的构建系统的项目文件。Node.js需要构建系统,因为它的大部分(及其依赖项)使用需要编译的语言写的。
  • gtest 可以使用来自Chromium的gtest测试native code,It allows testing C/C++ without needing an existing node executable to bootstrap from.

Node.js特性(JS特性)

并发处理

  • 多进程 LinuxC、Apache
  • 多线程 Java
  • 异步I/O JS 事件回调方式
  • 协程 - lua openresty、go、deno、TypeScript

ry(Ryan Dahl) 也就是node的作者, 2018年开启了一个新项目deno, A secure JavaScript/TypeScript runtime built with V8, Rust, and Tokio , TypeScript还是非学不可的。

与前端的不同

  • JS核心语法不变
  • 前端:BOM DOM
  • 后端:fs、http、buffer(缓冲区,二进制内容)、event、os

Node API

第一个node程序

新建一个0_run.js文件,打印hello world,然后在终端(Terminal)用node命令执行:node 0_run.js

  1. // 0_run.js, node 0_run.js终端会打印hello world
  2. console.log('hello world');

nodemon

上面的例子中,每次修改0_run.js后,想再次运行都需要再次执行node 0_run.js,调试时非常麻烦,这就需要用到nodemon了。

nodemon 监听node.js应用的所有改动,当改动发生时,自动重启服务(Monitor for any changes in your node.js application and automatically restart the server - perfect for development)

  1. # 全局安装 nodemon 命令
  2. npm install -g nodemon
  3. # 执行,当0_run.js里的文件有更改时,会自动重启执行新的文件
  4. nodemon 0_run.js

1_0_nodemon.png

常用模块

核心模块(require都不需要就能直接使用)

  • global
  • buffer 用于处理二进制数据流
    • alloc
    • from
    • write
    • concat
    • toString
  • process
  • module 模块操作CommonJS规范,require与module.exports

内置模块(不需要npm install安装,直接require就能使用)

  • os
  1. // http://nodejs.cn/api/os.html#os_os_freemem
  2. // os.freemem() 方法以整数的形式回空闲系统内存的字节数(B)。
  3. // os.totalmem() 方法以整数的形式返回所有系统内存的字节数(B)。
  4. const os = require('os')
  5. console.log('闲置内存:', os.freemem() / (1000*1000) + 'M')
  6. console.log('总内存:', os.totalmem() / (1000*1000) + 'M' )
  7. console.log(os.type()) // Darwin
  8. console.log(os.release()) // 18.7.0 操作系统发行版本
  9. console.log(os.hostname()) // kevindeMacBook-Air.local
  10. console.log(os.homedir()) // /Users/kevin
  11. console.log(os.cpus()) // 打印cpu内核信息,包括cpu具体型号
  12. console.log(os.userInfo()) // 当前用户信息
  • fs
  • path
  • http
  • event
  • util

第三方模块(需要npm install安装,需要require)

  • download-git-repo,Download and extract a git repository (GitHub, GitLab, Bitbucket) from node.
  1. // 先安装 npm install download-git-repo --save
  2. // 如果没有package.json 需要先npm init
  3. /**
  4. * @description Download a git repository to a destination folder with options, and callback.
  5. * @params { String } repository
  6. * - Github: github:owner/name 或者简写 owner/name
  7. * - GitLab: gitlab:owner/name
  8. * - Bitbucket : bitbucket:owner/name
  9. * - 默认为主分支(master),指定分支 owner/name#my-branch
  10. * @params { String } distination 下载repository后,存放的路径
  11. * @options { Object } 可选的选项对象,download时的选项
  12. * - { clone: false } 如果clone为true,将用git clone 代替http download
  13. * = proxy, headers, filter 等
  14. * @params { Function } callback 执行完成后的回调函数 function(err)
  15. */
  16. download(repository, destination, options, callback)
  17. // 实例:将github用户zuoxiaobai的todo项目,下载到test目录(可以是不存在的,会自动创建)下, 如果成功,打印OK,否则打印错误
  18. // 使用第三方模块 download-git-repo
  19. const download = require('download-git-repo')
  20. download('github:zuoxiaobai/todo', 'test', err => {
  21. console.log(err ? err : 'OK')
  22. })
  • ora Elegant terminal spinner 优雅的终端提示工具
  1. // 上面的例子中,下载过程没有提示,可以使用ora这个模块,来加入提示信息
  2. // 先来看看ora的demo 03—_ora.js
  3. const ora = require('ora');
  4. const spinner = ora('Loading unicorns').start();
  5. setTimeout(() => {
  6. spinner.color = 'yellow';
  7. spinner.text = 'Loading rainbows';
  8. }, 1000);
  9. setTimeout(()=> {
  10. spinner.warn('这是一个警告....')
  11. }, 3000)
  12. setTimeout(()=> {
  13. spinner.fail('这是一个错误....')
  14. }, 4000)
  15. setTimeout(()=> {
  16. spinner.succeed('加载成功!')
  17. }, 5000)

1_1_ora基础用法.gif

优化后的下载代码

  1. // 优化,加入下载过程中的提示
  2. const download = require('download-git-repo')
  3. const repo = 'github:zuoxiaobai/todo'
  4. const dest = 'test'
  5. const ora = require('ora')
  6. const process = ora(`正在下载 ${repo} ${dest} 目录...`).start()
  7. download(repo, dest, err => {
  8. console.log(err ? err : 'OK')
  9. if (err) {
  10. process.fail(err)
  11. } else {
  12. process.succeed('下载成功')
  13. }
  14. })

1_2_下载过程加入ora提示.gif

异步流程控制

  • callback
  1. taskone('任务1', () => {
  2. // 任务1的回调
  3. tasktwo('任务2', () => {
  4. // 任务2的回调
  5. taskThree('任务3', () => {
  6. // 任务3的回调
  7. console.log('task 3 callback')
  8. })
  9. })
  10. })

如何让异步任务串行化

  • promise then 承诺执行
  1. function promise(name, delay = 1000) {
  2. return new Promise(resolve => {
  3. setTimeout(() => {
  4. resolve()
  5. console.log('异步任务' + name)
  6. }, delay)
  7. })
  8. }
  9. promise('异步任务1')
  10. .then(() => {
  11. promise('异步任务2')
  12. })
  13. .then(() => {
  14. promise('异步任务3')
  15. })
  • ES6 generator
  1. // Generator 和 yield 、 iterator
  2. const generator = function* (name) {
  3. yield promise(name);
  4. yield promise(name)
  5. }
  6. const gen = generator('Generator')
  7. gen.next().value.then( ()=> {
  8. gen.next()
  9. })
  10. // 用next() 手动执行
  • 自己实现一个co库
  1. // 使用co模块,自动执行generator
  2. let co = function(gen, name) {
  3. var it = gen(name);
  4. var ret = it.next();
  5. ret.value.then((res) => {
  6. it.next(res)
  7. })
  8. }
  9. co(generator, 'CO')
  • ES7 async/await
  1. async () => {
  2. await promise('异步任务1')
  3. await promise('异步任务2')
  4. }

使用内置模块util的promisify

util.promisify是在node.js 8.x版本中新增的一个工具,用于将老式的Error first callback转换为Promise对象,让老项目改造变得更为轻松。

  1. // promisify 将函数转换为promise
  2. // const { promisify } = require('util');
  3. // 再次优化 内置util模块promisify
  4. async function downloadRepo(repo, dest) {
  5. const { promisify } = require('util')
  6. const download = promisify(require('download-git-repo'))
  7. const ora = require('ora')
  8. const process = ora(`正在下载 ${repo} ${dest} 目录...`).start()
  9. try {
  10. await download(repo, dest) // 下载
  11. } catch(e) {
  12. process.fail('失败')
  13. }
  14. process.succeed('下载成功!')
  15. }
  16. const repo = 'github:zuoxiaobai/todo'
  17. const dest = 'test'
  18. downloadRepo(repo, dest)

将download的代码封装为模块,再使用

  1. // 04_download.js
  2. module.exports.downloadRepo = async function (repo, dest) {
  3. const { promisify } = require('util')
  4. const download = promisify(require('download-git-repo'))
  5. const ora = require('ora')
  6. const process = ora(`正在下载 ${repo} ${dest} 目录...`).start()
  7. try {
  8. await download(repo, dest) // 下载
  9. } catch(e) {
  10. process.fail('失败')
  11. }
  12. process.succeed('下载成功!')
  13. }
  14. // 02_useModule.js
  15. // 封装为模块后,再次调用
  16. const repo = 'github:zuoxiaobai/todo'
  17. const dest = 'test'
  18. let { downloadRepo } = require('./04_download')
  19. downloadRepo(repo, dest)

fs模块

  1. const fs = require('fs')
  2. // 1. 同步调用
  3. const data = fs.readFileSync('./04_download.js')
  4. console.log(data)
  5. // data内容打印如下,为Buffer对象,读取的是二进制数据
  6. // <Buffer 0a 6d 6f 64 75 6c 65 2e 65 78 70 6f 72 74 73 2e 64 6f 77 6e 6c 6f
  7. // 61 64 52 65 70 6f 20 3d 20 61 73 79 6e 63 20 66 75
  8. // 6e 63 74 69 6f 6e 20 28 72 65 70 ... >
  9. console.log(data.toString()) // 将Buffer转换为字符串
  10. // 打印内容如下:
  11. //
  12. // module.exports.downloadRepo = async function (repo, dest) {
  13. // const { promisify } = require('util')
  14. // const download = promisify(require('download-git-repo'))
  15. // const ora = require('ora')
  16. // const process = ora(`正在下载 ${repo} 到 ${dest} 目录...`).start()
  17. // try {
  18. // await download(repo, dest) // 下载
  19. // } catch(e) {
  20. // process.fail('失败')
  21. // }
  22. // process.succeed('下载成功!')
  23. // }
  24. // 2. 异步调用
  25. fs.readFile('./04_download.js', (err, data) => {
  26. if (err) throw err
  27. console.log(data)
  28. })
  29. // 3. promisify处理异步调用
  30. async function consoleFileData(filePath) {
  31. let { promisify } = require('util')
  32. let readFile = promisify(fs.readFile)
  33. try {
  34. let data = await readFile(filePath)
  35. console.log(data)
  36. } catch(e) {
  37. throw err
  38. }
  39. }
  40. consoleFileData('./04_download.js')

Buffer对象

操作二进制值,需要使用Buffer

  1. const buf1 = Buffer.alloc(10) // 分配一个10字节空间
  2. console.log(buf1)
  3. // 10个字节
  4. // <Buffer 00 00 00 00 00 00 00 00 00 00> // 用16进制,表示二进制数
  5. // 00 16进制数,代表一个字节
  6. const buf2 = Buffer.from('a') // 将字母a,转换为二进制数据
  7. console.log(buf2, buf2.toString()) // <Buffer 61> 'a'
  8. const buf3 = Buffer.from('中文')
  9. console.log(buf3) // <Buffer e4 b8 ad e6 96 87>
  10. // 拼接两个buffer
  11. const buf4 = Buffer.concat([buf2, buf3])
  12. console.log(buf4, buf4.toString())
  13. // <Buffer 61 e4 b8 ad e6 96 87> 'a中文'

http模块

开启一个http服务

  1. // 07_http.js
  2. const http = require('http')
  3. const server = http.createServer((req, res)=> {
  4. res.end('hello....')
  5. })
  6. server.listen(3003)
  7. // node 07_http.js 然后打开浏览器,访问127.0.0.1:3003 即可看到hello...

1_3_http.png

渲染静态页面或json数据

  1. // 利用fs,渲染静态html、JSON字符串返回
  2. const http = require('http')
  3. const fs = require('fs')
  4. const server = http.createServer((req, res)=> {
  5. const { url, method } = req
  6. console.log('url, method: ', url, method)
  7. if (url === '/' && method === 'GET') {
  8. fs.readFile('index.html', (err, data) => {
  9. if (err) throw err
  10. res.statusCode = 200
  11. res.setHeader('Content-Type', 'text/html')
  12. res.end(data)
  13. })
  14. } else if (url === '/users' && method === 'GET') {
  15. res.writeHead(200, {
  16. 'Content-Type': 'application/json'
  17. })
  18. res.end(JSON.stringify({
  19. name: 'guoqzuo'
  20. }))
  21. }
  22. })
  23. server.listen(3003)

stream 流

现有图片img.png,复制一份该图片,使用流

  1. // 复制一个图片,就需要使用stream 流了。
  2. const fs = require('fs')
  3. const rs = fs.createReadStream('./img.png')
  4. const ws = fs.createWriteStream('./img2.png')
  5. rs.pipe(ws)

1_4_stream.png

使用流

  1. // 07_http.js代码片段
  2. // ...
  3. const { url, method,headers } = req
  4. // ...
  5. else if (method === 'GET' && headers.accept.includes('image/*')) {
  6. // 使用流
  7. // 如果index.html有img,或是请求 /favicon.ico 时,可以正常加载图片
  8. fs.createReadStream('./' + url).pipe(res)
  9. }
  10. // ...

工具链

写一个npm包zuo-util

创建一个目录

  1. mkdir zuo-util
  2. cd zuo-util
  3. npm init # 生成package.json

创建可执行的命令

什么是可执行的命令?

以vue这个命令为例,对于新手来说会很困惑。vue命令并不是vue的命令。而是vue-cli这个node包全局安装时注入的。当运行 npm install -g vue-cli 时,会全局安装vue-cli,然后在terminal终端就可以直接运行vue命令。而是vue -V 显示vue-cli版本是3.5.5,而不是vue的版本,毕竟现在vue还没有3.0的版本

为zuo-util 创建一个可执行命令zuo
  1. mkdir bin; cd bin;
  2. touch zuo // 在bin目录下创建文件zuo,注意不要加后缀

在zuo这个文件里,写入下面的内容, 最开始的一行是文件头,表示这是一个node程序,用node写的shell。

  1. #!/usr/bin/env node
  2. console.log('this is zuo util')

然后在package.json里加入下面的代码,指定命令的目录

  1. "bin": {
  2. "zuo": "./bin/zuo"
  3. },

运行命令 zuo

如果想要命令全局运行,需要在zuo-util目录下执行npm link命令,这样就可以在终端运行zuo这个命令了

1_5_npm_link让命令可以全局运行.png

使用commander模块显示帮助文档

commander模块是一个命令行工具,一般用于terminal提示,命令解析

修改 /bin/zuo内容如下:

  1. #!/usr/bin/env node
  2. const program = require('commander')
  3. program.version(require('../package.json').version)
  4. .command('init <name>', 'init project')
  5. .command('cpu','show os cpu info')
  6. program.parse(process.argv);

上面使用commander模块,初始化了一个提示,当直接执行zuo命令,会显示帮助信息,version指定zuo -V后打印额值,command显示具体的命令及说明。zuo init 会执行/bin/zuo-init,zuo cpu 会执行 /bin/zuo-cpu

1_6_commander模块使用.png

如果直接执行zuo cpu 会提示文件没有可执行权限

  1. kevindeMacBook-Air:zuo-util kevin$ zuo cpu
  2. error: /Users/kevin/Desktop/zuo-util/bin/zuo-cpu(1) not executable. try chmod or run with root
  3. # 需要在terminal 里处理下权限,方法文档 man chmod
  4. chmod +x bin/* # 将bin文件下的所有文件添加可执行权限

上面说了zuo init会执行/bin/zuo-init方法,为什么?我们来看看progress.argv,下面是/bin/zuo-init的代码,跑起来看看process.argv到底是什么

  1. #!/usr/bin/env node
  2. console.log('zuo init')
  3. console.log(process.argv)

1_7_process_argv.png

所有可以理解为zuo init test,实际执行的是

  1. /usr/local/bin/node /Users/kevin/Desktop/zuo-util/bin/zuo-init test
  2. # 相当于 node zuo-init test

初步实现两个命令

这里实现zuo init 和 zuo cpu两个命令

/bin/zuo-cpu

  1. #!/usr/bin/env node
  2. const os = require('os')
  3. let cpuInfoArr = os.cpus()
  4. console.log('CPU INFO: ')
  5. console.log(' - Count: ' + cpuInfoArr.length)
  6. console.log(' - Model: ' + cpuInfoArr[0].model)
  7. console.log(' - Speed: ' + cpuInfoArr[0].speed)

/bin/zuo-init

  1. #!/usr/bin/env node
  2. console.log('zuo init')
  3. console.log(process.argv)
  4. if (process.argv.length < 3) {
  5. console.log('Arguments error, Please use like: zuo init <name>')
  6. } else {
  7. console.log('TODO Init project ' + process.argv[2])
  8. }

上传npm

在 zuo-util目录创建publish.sh脚本文件,为该文件添加可执行权限 chmod +x publish.sh; 执行 ./publish.sh

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

1_8_publish_npm包.png

由于是第一次提交,需要先运行 npm adduser 否则会出现错误

1_9_第一次发布需要先运行npm_adduser命令.png

版本更新

比如添加了README.md文档,需要从 0.1.0 升级到 0.1.1

  • 修改package.json里的version为 0.1.1
  • 运行 ./publish.sh,提交新版本

其他问题