Node.js简介
node.js是一个基于Google V8引擎的、跨平台的JavaScript运行环境,不是一个语言
安装与运行
node.js可以在官网进行安装 nodejs.org/zh-cn/
选择自己的运行环境进行安装,安装完成之后就可以在vscode中创建一个node.js的程序
现在我们创建一个读取文件内容的node程序
首先在根目录创建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
就可以得到我们读取的文件内容
注意这里的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);
从输出结果来看,是先输出123456,再输出读取的文件内容,是因为node在编译到fs.readFile方法时,将它放在异步栈中,先执行其他代码,等异步栈完成之后才会通知主线程进行处理
单线程
node.js保持了JavaScript在浏览器中单线程的特点
优点:
- 不用处处在意状态同步问题,不会反生死锁
- 没有线程上下文切换带来的性能开销
缺点:
- 无法利用多核CPU
- 错误会引起整个应用退出,健壮性不足
- 大量计算占用导致CPU无法继续执行
浏览器为例,浏览器是多线程,JS引擎是单线程,所以不是我们的代码不行,是JS引擎就是按照单线程去解析
浏览器中有Browser进程、插件进程、GPU进程、渲染进程;渲染进程里又包括页面渲染、JS执行和事件处理
跨平台
兼容Windows和*nix平台,主要得益于在操作系统上与Node上层模块系统之间构建了一层平台架构。node作为js的运行环境,但是底层代码确实C和C++写的,在开发的时候做了平台的一个抹平;像fs这些模块属于应用层API,使用的是js代码,兼容性也非常好
应用场景
node.js在大部分领域都占有一席之地,尤其是I/O密集型
web应用:Express/Koa
前端构建:Webpack
GUI客户端软件:VSCode/网易云音乐
其他:实时通讯、爬虫、CLI等...
但是不适合计算密集型的应用
模块化机制
-
什么是模块化?
根据功能或业务将一个大程序拆分成互相依赖的小文件,再用简单的方式拼接起来
-
为什么模块化?无模块化的问题
所有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这个方法
不同的是,module.exports只能导出一个变量,exports因为后面跟了变量名,可以多次使用,导出多个变量
CommonJS中exports、require、module、filename、 dirname变量
function (export,require,module,__filename,__dirname){
const m = 1
module.exports.m = m
}
加载方式
-
加载内置模块
require('fs')
-
加载相对 | 绝对路径的文件模块
require('/User/.../file.js') require('./file.js')
-
加载npm的包
require('loadash')
npm包查找规则
- 当前目录node_modules
- 如果没有,往上父级的node_modules
- 如果没有,沿着路径向上递归,直到根目录下node_modules
- 找到之后会加载package.json main指向的文件,如果没有package.json则依次查找index.js、index.json、index.node
因为实际的项目中,会有很多这种包的引用,如果每次都需要查找,会非常耗时,于是node就设立了一个缓存的机制
require.cache中缓存着加载过的模块,缓存的原因:同步加载
- 文件模块查找耗时,如果每次require都需要重新遍历查找,性能会比较差
- 在实际开发中,模块可能包含副作用代码
实际项目中可能会引入模块的新版本,这个时候就需要读取新版本,而不是缓存区的旧版本,就需要编写无缓存的方式编写代码
//有缓存
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编译
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信息
以webpack的package.json文件为例
- name包名称
- version 版本号
- main入口文件
- script执行脚本 npm run serve npm run build等命令
- dependencies线上依赖
- devDependencies开发依赖
- repository代码托管地址
异步编程
在我们的实际开发中,有很多需求是需要在上一个函数完成之后再去执行的。那么这种情况我们一般想到的都是回调,即在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(拒绝),以及一个未开始状态
使用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)
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 ...');
})
执行过程
-
服务启动
- 实例化application
- 注册中间件
- 创建服务、监听端口
-
接受/处理请求
- 获取请求req、res对象
- req->request、res->response封装
- request&response->context
- 执行中间件
- 输出设置到ctx.body
中间件
Koa应用程序是一个包含一组中间件函数的对象,它是按照洋葱模型组织和执行的
中间件的执行顺序
上面我们可以看到中间件的实际执行顺序,相当于函数内的回调函数,但是没有回调地狱。
中间件简单代码实现
//中间件模拟
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)
}