【青训营】-node.js基础入门

773 阅读6分钟

Node.js简介

node.js是一个基于Google V8引擎的、跨平台的JavaScript运行环境,不是一个语言

安装与运行

node.js可以在官网进行安装 nodejs.org/zh-cn/

image-20210903095915171.png 选择自己的运行环境进行安装,安装完成之后就可以在vscode中创建一个node.js的程序

现在我们创建一个读取文件内容的node程序

image-20210903100319585.png 首先在根目录创建package.json和index.js两个文件,index.js就是我们的node程序。上面也说到,node并不是一门语言,而是一个js的运行环境,所以我们的node程序都是js语言来写。在package.json文件中写入内容

{
  "name": "ts",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "typescript": "^4.4.2"
  }
}

内容可以随便写,这个文件就是用来给我们写的node程序读取的,不影响其他内容

然后编写node代码

const { readFile } = require('fs')
​
readFile('./package.json', { encoding: 'utf-8' }, (err, data) => {
    if (err) {
        throw err
    }
    console.log(data);
})

首先是引入一个叫‘fs’的模块,这个模块就是专门对文件的一个操作,包括创建、修改、读取、删除文件等操作;

然后使用这个方法,第一个参数是文件路径,第二个参数是编码格式,第三个就是一个回调promise,包含err和data两个参数

然后打开终端,执行下面的命令

node index.js

就可以得到我们读取的文件内容

image-20210903100934458.png

注意这里的log是输出在终端里面的,不是在浏览器的

以前我们的js文件是依附于HTML文件,HTML文件被放在浏览器中进行解析,读取到js内容才会进行解析js。node就相当于把V8引擎直接拿过来解析js文件,让js有了属于自己的运行环境。

版本管理

我们在开发中,会有node版本不同的问题,如何快速切换node.js的版本,就是借助于版本管理工具

n:一个npm全局的开源包,是依赖于npm来全局安装、使用的

fnm: 快速简单,兼容性支持.node-version 和.nvmrc文件

nvm:独立的软件包,Node Version Manager

特性

node是2009年诞生的,发展到现在也是非常健康了,大致有以下三个特点

异步I/O

I/O不仅仅是文件的读写,也包括网络请求和数据库的读写

当Node.js执行I/O操作时,会在响应返回并恢复操作,而不是阻塞线程并浪费CPU循环等待

代码写编写顺序与执行顺序无关

拿刚才的读取文件的node程序来说,在文件读取后面加一行输出

const { readFile } = require('fs')
​
readFile('./package.json', { encoding: 'utf-8' }, (err, data) => {
    if (err) {
        throw err
    }
    console.log(data);
})
console.log(123456);

image-20210903103917636.png

从输出结果来看,是先输出123456,再输出读取的文件内容,是因为node在编译到fs.readFile方法时,将它放在异步栈中,先执行其他代码,等异步栈完成之后才会通知主线程进行处理

image-20210903104229927.png

单线程

node.js保持了JavaScript在浏览器中单线程的特点

优点:

  • 不用处处在意状态同步问题,不会反生死锁
  • 没有线程上下文切换带来的性能开销

缺点:

  • 无法利用多核CPU
  • 错误会引起整个应用退出,健壮性不足
  • 大量计算占用导致CPU无法继续执行

浏览器为例,浏览器是多线程,JS引擎是单线程,所以不是我们的代码不行,是JS引擎就是按照单线程去解析

浏览器中有Browser进程、插件进程、GPU进程、渲染进程;渲染进程里又包括页面渲染、JS执行和事件处理

跨平台

兼容Windows和*nix平台,主要得益于在操作系统上与Node上层模块系统之间构建了一层平台架构。node作为js的运行环境,但是底层代码确实C和C++写的,在开发的时候做了平台的一个抹平;像fs这些模块属于应用层API,使用的是js代码,兼容性也非常好

image-20210903105218463.png

应用场景

node.js在大部分领域都占有一席之地,尤其是I/O密集型

web应用:Express/Koa

前端构建:Webpack

GUI客户端软件:VSCode/网易云音乐

其他:实时通讯、爬虫、CLI等...

但是不适合计算密集型的应用

模块化机制

  1. 什么是模块化?

    根据功能或业务将一个大程序拆分成互相依赖的小文件,再用简单的方式拼接起来

  2. 为什么模块化?无模块化的问题

    所有script标签必须保证顺序正确,否则会依赖报错

    比如我们HTML文件需要引入多个js文件,但是HTML读取js是按顺序的,如果1.js需要引用2.js文件的内容,那么当我们引入的顺序不是1->2,就出出现报错

    全局变量存在命名冲突,占用内存无法被回收

    当我们多人进行开发的时候,如果变量命名相同,就会出现内存无法销毁,一直占用的问题,如果是严格模式还会报错,导致程序无法运行

    IIFE/namespace会导致代码可读性低等诸多问题

CommonJS规范

node.js支持CommonJS模块规范,采用同步机制加载模块

//greeting.js
const prefix = 'hello'
const sayHi = function(){
    return prefix + 'world'
}
module.exports = {
    sayHi,
}
​
​
​
//index.js
const{ sayHi } = require('./greeating')
sayHi();

以上面代码为例,先创建greeting的js文件,定义好sayHi方法,使用module.export方法导出

在index.js文件中使用require方法引入,就可以调用

在导入sayHi这个方法的时候,sayHi方法本身有一个变量prefix,因为这个变量没有在module.exports中导出,所以在index.js文件中是访问不了这个变量的,它就相当于sayHi的私有变量。

这里采用的是同步机制加载模块,因为node是在服务端读取文件,所以读取文件的速度非常快

//greeting.js
const prefix = 'hello'
const sayHi = function(){
    console.log(`${prefix}world`)
}
exports.sayHi = sayHi
​
​
​
//index.js
const{ sayHi } = require('./greeating')
sayHi();

上面的代码跟最开始的又有一些不一样,这里将module.exports换成了exports. 这两种方法其实都是一样的,都指向了sayHi这个方法

image-20210903145454302.png

不同的是,module.exports只能导出一个变量,exports因为后面跟了变量名,可以多次使用,导出多个变量

CommonJS中exports、require、module、filename、 dirname变量

function (export,require,module,__filename,__dirname){
    const m = 1
    module.exports.m = m
}

加载方式

  1. 加载内置模块

    require('fs')
    
  2. 加载相对 | 绝对路径的文件模块

    require('/User/.../file.js')
    ​
    require('./file.js')
    
  3. 加载npm的包

    require('loadash')
    

npm包查找规则

  1. 当前目录node_modules
  2. 如果没有,往上父级的node_modules
  3. 如果没有,沿着路径向上递归,直到根目录下node_modules
  4. 找到之后会加载package.json main指向的文件,如果没有package.json则依次查找index.js、index.json、index.node

image-20210903151747931.png

因为实际的项目中,会有很多这种包的引用,如果每次都需要查找,会非常耗时,于是node就设立了一个缓存的机制

require.cache中缓存着加载过的模块,缓存的原因:同步加载

  1. 文件模块查找耗时,如果每次require都需要重新遍历查找,性能会比较差
  2. 在实际开发中,模块可能包含副作用代码

实际项目中可能会引入模块的新版本,这个时候就需要读取新版本,而不是缓存区的旧版本,就需要编写无缓存的方式编写代码

//有缓存
const mod1 = require('./foo')
const mod2 = require('./foo')
console.log(mod1 === mod2) //true//无缓存
function requireUncached(module){
    delete require.cache[require.resove(module)]
    return require(module)
}
const mod3 = requireUncache('./foo')
console.log(mod1 === mod3)  // false

其他模块规范

AMD是RequireJS在推广过程中规范化产出,异步加载,推崇依赖前置

CMD是SeaJS在推广过程中规范化产出,异步加载,推崇就近依赖

UMD规范,兼容AMD和CommonJS模式

ES Modules,语言层面的模块化规范,与环境无关,可借助babel编译

image-20210903153347902.png

image-20210903153359016.png

ES Modules

ESM是在ES6语言层面提出的一种模块化标准

ESM中只要有import、export两个关键词,不能console打印两个关键词

//导出
export default 1
export const name = "cola"
export { age }
export { name as nickname }
export { foo } from './foo'
export * from './foo'//导入
import Vue from 'vue'
import * as Vue from 'vue'
import { Component } from 'vue'
import {default as a} from 'vue'
import {Button as Btn} from 'Element'
import 'index.less'

CommonJS VS ESM

CommonJS模块输出的是一个值得拷贝;ESm模块输出的是值得引用

CommonJS模块是运行时加载;ESm模块是编译时输出(提前加载)

可以混用,但是不建议(import commonjs || import中require)

// CommonJS
//lib.js
let counter = 3
function addCounter(){
    counter ++
}
module.exports = {
    conunter,
    addCounter
}
​
//main.js
const { counter,addCounter } = require('./lib')
console.log(counter);  //3
addCOunter()
console.log(counter);  //3
//ES Modules
export let counter = 3
export function addCounter(){
    counter++
}
​
//main.js
import { counter,addCounter } from './lib.mjs'
console.log(counter);  //3
addCounter()
console.log(counter);  //4

包管理机制

npm介绍

NPM是Node.js中的包管理器,提供了安装、删除等其他命令来管理包

常用命令:

  • npm init 初始化 帮助我们自动生成package.json配置文件
  • npm config 配置
  • npm run 运行
  • npm install 安装包(npm i)
  • npm uninstall 删除包
  • npm updata 更新指定版本
  • npm info 查看包信息
  • npm publish 发布自己的包

package.json信息

image-20210905145042790.png

以webpack的package.json文件为例

  • name包名称
  • version 版本号
  • main入口文件
  • script执行脚本 npm run serve npm run build等命令
  • dependencies线上依赖
  • devDependencies开发依赖
  • repository代码托管地址

更多package.json配置

异步编程

在我们的实际开发中,有很多需求是需要在上一个函数完成之后再去执行的。那么这种情况我们一般想到的都是回调,即在fn1函数里面去调用fn2,如果需要多层的调用关系,就会出现fn3在fn2里面,fn4在fn3里面,代码不仅繁琐,还不利于阅读和扩展,这就是回调地狱。

Callback(回调)

const { readFile } = require('fs')
​
fs.readFile('./package.json', { encoding: 'utf-8' }, (err, data) => {
    if (err) throw err
    const {main} = JSON.parse(data)
    fs.readFile(main,{encoding:'utf-8'},(err,data)=>{
        if(err) throw err
        console.log(data)
    })
})

还是开始那个读取文件的例子,现在想读取main字段对应的文件内容,因为js是单线程的处理模式,我们就需要在读取到文件内容data之后,在执行一次读取文件的操作,第二次的读取文件的操作在第一次的函数内部。一次两次还好,如果需求比较多,一层套一层,就会出现回调地狱的情况。

Promise

Promise是一个具有四个状态的有限状态机,其中三个核心状态为Pending(挂起),Fulfilled(完成),Rejected(拒绝),以及一个未开始状态

image-20210903173041810.png

使用Promise,实现读取package.json中main字段对应的文件内容

const { readFile } = require('fs/promise')
​
readFile('./package.json',{encoding:'utf-8'}).then(res=>{
    return JSON.parse(res)
}).then(data=>{
    return readFile(data.main,{encoding:'utf-8'})
}).then(res=>{
    console.log(res)
})

上面代码我们可以看到,使用promise之后,函数变得非常简洁明了,promise通过链式调用,避免了回调地狱

function promise(fn,receiver){
    return (...args)=>{
        return new Promise((resolve,reject)=>{
            fn.apply(receiver,[...args,(err,res)=>{
                return err?reject(err):resolve(res)
            }])
        })
    }
}
​
const readFilePromise = promisify(fs.readFile,fs)

await

await函数使用try catch 捕获异常(注意并行处理),其实async await就是promise的语法糖,相比promise原函数更简洁

const { readFile } = require('fs/promise')
​
async ()=>{
    const { main } = JSON.parse(await readFile('./package.json',{ encoding:'utf-8'}))
    const data = await readFile(main,{encoding:'utf-8'})
    console.log(data)
}

Event

发布订阅模式,Node.js内置events模块

比如HTTP server on('request') 事件监听

//发布订阅模式
const EventEmitter = require('events')
​
class MyEmitter extends EventEmitter{}
const myEmitter = new MyEmitter()
​
myEmitter.on('event',()=>{
    console.log('an event occurred!')
})
myEmitter.emit('event')
​
//sever 监听 request事件
const http = require('http')
​
const server = http.createServer((req,res)=>{
    res.end('hello')
})
server.on('request',(req,res)=>{
    console.log(req.url)
})
server.listen(3000)

image-20210905092559042.png

image-20210905092618289.png

Web应用开发

http模块

搭建一个最简单的http服务,Node.js内置的http模块

const http = require('http')
​
http.createServer((req,res)=>{
    res.end('hello world\n')
}).listen(3000,()=>{
    console.log('App running at http://127.0.0.1:3000/')
})

Koa介绍

Koa--基于Node.js平台的下一代Web开发框架

Koa它仅仅提供了一个轻量优雅的函数库,使得编写Web应用变得得心应手,不在内核方法中绑定任何中间件

Koa在使用之前需要使用npm安装,因为Koa不是node的内置组件

const app = new Koa()
​
app.use(async ctx => {
    ctx.body = 'Hello World'
})
​
app.listen(3000, () => {
    console.log('App started at http://localhost:3000 ...');
})

image-20210905093831264.png

image-20210905093842596.png

执行过程

  • 服务启动

    • 实例化application
    • 注册中间件
    • 创建服务、监听端口
  • 接受/处理请求

    • 获取请求req、res对象
    • req->request、res->response封装
    • request&response->context
    • 执行中间件
    • 输出设置到ctx.body

image-20210905093633740.png

中间件

Koa应用程序是一个包含一组中间件函数的对象,它是按照洋葱模型组织和执行的

image-20210905093727721.png

中间件的执行顺序

image-20210905095018510.png

image-20210905095034731.png 上面我们可以看到中间件的实际执行顺序,相当于函数内的回调函数,但是没有回调地狱。

中间件简单代码实现

//中间件模拟
const fn1 = async (ctx,next)=>{
    console.log('before fn1')
    ctx.name = 'codecola'
    await next()
    console.log('after fn1')
}
const fn2 = async (ctx,next)=>{
    console.log('before fn2')
    ctx.age = 23
    await next
    console.log('after fn2')
}
​
const fn3 = async (ctx,next)=>{
    console.log(ctx)
    console.log('in fn3 ...')
}
​
const compose = (middlewares,ctx)=>{
    const dispatch = (i)=>{
        let fn = middlewares[i]
        return Promise.resolve(fn(ctx,()=>{
            return dispatch(i+1)
        }))
    }
    return dispatch(0)
}
​
compose([fn1,fn2,fn3],{})

基于中间件原理,获取处理函数执行时间

const Koa = require('koa')
const app = new Koa//logger中间件
app.use(async(ctx,next)=>{
    await next()
    const rt = ctx.response.get('X-Response-Time')
    if(ctx.url!=='/favicon.ico') {
        console.log(`${ctx.methed} ${ctx.url} - ${rt}`)
    }
})
​
//x-response-time 中间件
app.use(async (ctx,next)=>{
    const start = Data.now()
    await next()
    const ms = Data.now - start
    ctx.set('X-Response-Time',`${ms}ms`)
})
​
app.use(async ctx=>{
  let sum = 0
  for(let i = 0;i < le9; i++){
      sum += i
  }
    ctx.body = `sum=${sum}`
})
​
app.listen(3000,()=>{
    console.log('App started at http://localhost:3000 ...')
})

常用中间件

  • koa-router: 路由解析
  • koa-body: request body解析
  • koa-logger: 日志记录
  • koa-views:模板渲染
  • koa2-cors :跨域处理
  • koa-session:session处理
  • koa-helmet:安全防护
  • ...

koa中间件繁多,质量参差不齐,需要合理选择,高效组合

基于Koa的前端框架

开源:ThinkJS/Egg ...

内部:Turbo、Era、Gulu ...

它们做了什么?

  • Koa对象response/request/context/application等扩展
  • kuo常用中间件库
  • 公司内部服务支持
  • 进程管理
  • 脚手架
  • ...

线上部署

Node.js保持了JavaScript在浏览器中单线程的特点(一个进程只开一个线程)

Node.js虽然是单线程模式,但是基于事件驱动、异步非阻塞模式,可以应用于高并发场景,同时避免了线程创建、线程之间上下文切换所产生的资源开销。

缺点:

  • 无法利用多核CPU
  • 错误会引起整个应用退出,健壮性不足
  • 大量计算占用CPU,导致无法继续执行

利用多核CPU

执行一个最简单的HTTP Server

const http = require('http')
​
http.createServer((req,res)=>{
    for(let i=0;i<le7;i++){
        res.end(`handled by process.${process.pid}`)
    }
}).listen(8080)

那么如何利用多核CPU呢?

Node.js提供了cluster/child_process模块

const cluster = require('cluster')
const os = require('os')
​
if(cluster.isMaster){
    const cpulen = os.cpus().length
    for(let i=0;i<cpulen;i++){
        cluster.fork()
    }
}else{
    require('./server.js')
}

进程守护

const http = require('http')
const numCPUs = require('os').cpus().length
const cluster = require('cluster')
​
if(cluster.isMaster){
    console.log('Master process id is',process.pid)
    for(let i=0;i<numCPUs;i++){
        cluster.fork()
    }
    cluster.on('exit',function(worker,code,signal){
        console.log('work process died,id',worker.process.pid)
        cluster.fork()
    })
}else{
    const server = http.createServer()
    server.on('request',(req,res)=>{
        res.writeHead(200)
        console.log(process.pid)
        res.end('hello world\n')
    })
    server.listen(8080)
}