1】浏览器引擎
浏览器内核
就是指的:排版引擎(页面渲染引擎)。
- Gecko:早期网景、火狐使用。
- Trident:微软开发,IE4~11浏览器,但是Edge转向Blink。
- Webkit:苹果基于KHTML开发、开源。用于Safari、谷歌之前也使用。
- Blink:是Webkit的一个分支,Google开发,目前用于谷歌、Edge、Opera等。
JavaScript引擎
职责:JS是高级语言------>汇编语言------->机器语言----->cpu执行
- SpiderMonkey
- Chakra
- JavaScriptCore:webkit
- V8:google
2】node版本管理
- nvm:官方提供
- n:TJ大神提,供交互式的版本管理(推荐)
n
#安装最新的lts版本
n lts
#安装最新的current版本(支持更多语法)
n latest
#安装指定版本
n 12.18.1
3】REPL
REPL(Read-Eval-Print Loop)译为:“读取-求值-输出” 循环;
REPL是一个简单的、交互式的编程环境。(相当于浏览器的console控制台)
直接在终端输入:node
回车进入。
全局对象区别:
浏览器中是有window、document等全局对象的,而node中则没有,而是有global、process等全局对象
4】node命令行传参
获取参数:process.argv
- argc:argument counter的缩写(传入的具体参数)
- argv:argument vector的缩写(传递参数的个数)
- vector 翻译过来是矢量的意思,在程序中表示一直数据结构。
- JS中是一种数组结构,里面存储一些参数信息。
//index.js
console.log(process.argv)
1、当我们直接 node index.js
回车执行
//打印出内容:
[
'/usr/local/bin/node',//node的全局绝对路径
'/Users/mac/Desktop/react_project/NodeRestudy/index.js'//index.js的绝对路径
]
2、当我们传递参数 node index.js env=production platform=ios author=rayhomie
回车执行
//打印出内容:
[
'/usr/local/bin/node',//node的全局绝对路径
'/Users/mac/Desktop/react_project/NodeRestudy/node命令行传参.js',//index.js的绝对路径
'env=production',//接收的参数
'platform=ios',//接收的参数
'author=rayhomie'//接收的参数
]
5】常见的全局对象
1、特殊全局对象
- 这些特殊全局对象实际上是模块中的变量,只是每个模块有,看起来像是全局变量
- 在命令行交互中不可以使用
- 包括:
__dirname
、__filename
、exports
、module
、require()
//index.js
console.log(__dirname)
console.log(__filename)
/*
/Users/mac/Desktop/react_project/NodeRestudy
/Users/mac/Desktop/react_project/NodeRestudy/特殊全局对象.js
*/
2、常见全局对象
- process对象:提供Node进程相关的信息
- 比如Node的运行环境、参数信息等
- 还可以将环境变量读取到process的env中
- console对象
- 定时器函数
setTimeout
:在n毫秒后执行一次setInterval
:每n毫秒重复执行一次setImmediate
:I/O事件后的回调“立即”执行process.nextTick
:添加到下一次tick队列中;
- global对象:所有东西都放在了global对象上(类似浏览器的window对象)
6】模块化
JavaScript被称之为披着C语言外衣的Lisp。(函数式编程)
但是早期JS还是存在很多缺陷:
- 比如var定义的变量作用域问题
- 比如JS的面向对象并不能像常规面向对象语言一样使用class
- 比如JS没有模块化
模块化规范:AMD、CMD、CommonJS(CJS)、ES Module(ESM)
- 规范都该包含的功能:模块本身可以导出暴露的属性,模块又可以导入自己需要的属性。
CommonJS
- Node是CommonJS在服务器端一个具有代表性的实现
- Browserify是CommonJS在浏览器中的一种实现
- webpack打包工具内部对CommonJS的实现(还有各种模块规范实现)
Node中对CommonJS进行了实现和支持:
- 在Node中每一个js文件是都是一个单独的模块
- 在模块中CJS规范的核心变量:
exports
、module.exports
、require
exports
和module.exports
负责对模块中的内容进行导出。require
函数可以导入其他模块(自定义模块,系统模块,第三方库模块)中的内容。
浅拷贝
//------------------- CJS/module1.js -------------
let name = 'rayhomie'
let age = 20
const person = {
name: 'rayhomie',
age: 20
}
function modify(newName, newAge) {
name = newName
age = newAge
person.name = newName
person.age = newAge
}
setTimeout(() => {
console.log(name, age);//leihao 30
}, 1000)
/*
//使用exports导出
exports.name = name
exports.age = age
exports.person = person
exports.modify = modify
*/
//使用module.exports导出
module.exports = { name, age, person, modify }
//----------------- CJS/main.js -----------------
const module1 = require('./module1.js')
//实际上是浅拷贝拷贝了一份module1.js模块对象
console.log(module1);
/*
{
name: 'rayhomie',
age: 20,
person: { name: 'rayhomie', age: 20 },
modify: [Function: modify]
}
*/
module1.modify('leihao', 30)
console.log(module1);
/*
{
name: 'rayhomie',
age: 20,
person: { name: 'leihao', age: 30 },
modify: [Function: modify]
}
*/
/*
//module1.js中的setTimeout打印:
leihao 30
*/
理解:默认情况下模块的exports变量指向一个空对象,require()
执行返回的是exports变量指向的对象。我们导出的操作是exports.key=value
或module.exports={...}
,这样对对象设置属性也就是:将要暴露的属性浅拷贝到exports指向的对象上。所以CJS是导出其实是浅拷贝模块。
module.exports和exports有什么区别?
通过维基百科中对CommonJS的规范的解析:
- CJS中是没有module.exports的概念的
- 但是为了实现模块的导出,Node中使用的是Module的类,每一个模块都是Module的一个实例(
new Module()
),也就是每一个文件都是一个module实例。 - 所以在Node中真正用于导出的其实不是exports,而是module.exports
- 其实直接使用exports导出,node内部也是通过module.exports来实现的。
因为源码里面做了一件事情:在当前模块最顶层执行module.exports=exports
//------------ CJS/module2.js -------------------
let name = 'rayhomie'
exports.name = name
console.log(exports, module.exports, exports === module.exports);
//{ name: 'rayhomie' } { name: 'rayhomie' } true
//------------- CJS/module3.js --------------------
let name = 'rayhomie'
module.exports ={ name }
console.log(exports, module.exports);
//{} { name: 'rayhomie' }
require的查找规则
常见:require(X)
- 情况一:X是一个核心模块,比如path、http
- 直接返回核心模块,并停止查找
- 情况二:X是以
./
或../
或/
(根目录)开头的- 将X当做一个文件在对应的本地目录下查找
- 如果有后缀名,按照后缀名的格式查找对应文件
- 如果没有后缀名,会按照以下顺序:
X
、X.js
、X.json
、X.node
的顺序查找
- 如果没有找到对应文件,则将X作为一个目录
- 查找该目录下的index文件:
X/index.js
、X/index.json
、X/index.node
- 查找该目录下的index文件:
- 如果还没有找到,那么就报错:not found
- 将X当做一个文件在对应的本地目录下查找
- 情况三:直接是一个X(没有路径),并且X不是一个核心模块
- 在当前文件夹下的node_modules文件夹下面查找
- 如果没有,则递归依次向上层查找,直到
/node_modules
根目录下(全局依赖) - 如果还没有找到,那么报错:not found
模块加载过程
- 模块在被第一次引入时,模块中的js代码会被运行一次
- 模块多次引入时,会缓存,最终只加载(运行)一次
- Module实例上面有一个loaded属性,用于标记是否被加载过。
- 加载过程是深度优先算法DFS。
- 加载过程是同步的,会阻塞代码执行
AMD
- Asynchronous Module Definition(异步模块定义)
- 它采用的是异步异步加载模块(加载的时候不影响后面代码的运行)
规范这是定义代码应该如何去编写,只有有了具体的实现才能被应用:
- AMD的实现比较常用的库是 require.js 和 curl.js
require.js的使用:
<!-- index.html -->
<script src="./lib/require.js" data-main="./index.js" ></script>
<!-- data-main属性定义项目脚本入口(等到src中的require.js加载完,立即去获取data-main中的文件并执行) -->
//------------ index.js --------------
//入口用于声明配置管理模块
(function () {
//require是外部require.js加载的全局变量
require.config({//需要配置
baseUrl: '',
paths: {//模块名与路径的映射(不加后缀名)
"module1": "./modules/module1",
"module2": "./modules/module2"
}
})
//加载module2.js中的代码
require(['module2'], function (module2) {
console.log(module2);
})
})()
//------------ ./modules/module1.js --------------
//每个模块需要使用define来定义
define(function () {
const name = 'rayhomie'
const age = 20
const sayHello = (name) => {
console.log('你好' + name)
}
return {//暴露给外部
name,
age,
sayHello
}
})
//------------ ./modules/module2.js --------------
//每个模块需要使用define来定义
define([
'module1'//依赖的模块
], function (module1) {//使用参数来接收导入的变量
console.log(module1.name)
console.log(module1.age)
module1.sayHello('leihao')
return {}
})
CMD
- Common Module Definition(通用模块定义)
- 它采用了异步加载模块,吸取了Commonjs的写法简便的优点。
- (加载的时候不影响后面代码的运行)
- CMD规范的实现,比较常用的库是:Sea.js
<!-- index.html --> <script src="./lib/sea.js"></script><script> //直接使用脚本的主入口(区别于AMD,主入口中不需要配置管理各个模块)。 seajs.use('./index.js')</script>
//------------ index.js --------------define(function (require, exports, module) { //和Commonjs规范差不多的导出导入 const { name, age, sayHello } = require('./modules/module1.js') console.log(name); console.log(age); sayHello('leihao')})//------------ ./modules/module1.js --------------define(function (require, exports, module) { const name = 'rayhomie' const age = 20 const sayHello = function (name) { console.log('你好', name); } //和Commonjs规范差不多的导出导入 module.exports = { name, age, sayHello }})
ES Module
- 它使用了 import 和 export 关键字(并不是函数和对象,需要js引擎解析)
- 它采用了编译期的静态分析,并且也加入了动态引用的方式
- ESM由js引擎解析,在解析时已经确定了模块的依赖关系(在转换成AST之前),所以导入必须写在模块最前面。但ESM还提供了
import()
的运行时环境。parse->ast->bytecode->runtime
- ESM将自动采用严格模式:
use strict
浏览器上使用
<!-- index.html --> <!-- 设置type="module"让js引擎识别 --><script src="./index.js" type="module"></script>
//------------ index.js --------------console.log(111)
报错问题:直接在浏览器打开index.html本地文件,会报跨域错误,由于浏览器的安全限制,不支持module以file的协议引入。解决办法是把index.html放在服务器上面,然后浏览器去请求。(VScode中可以安装Live Server插件,然后打开)
导出方式
- 方式一:导出变量
export const x
//------------ ./modules/module1.js --------------export const name = 'rayhomie'export const age = 20export const sayHello = function (name) { console.log('你好', name);}//------------ index.js --------------//分别导入变量import { name, age, sayHello } from './modules/module1.js';//分别导入变量并起别名import { name as Name, age as Age, sayHello as SayHello } from './modules/module1.js';//分别导入全部变量放在一个对象中并起别名import * as module1 from './modules/module1.js';
- 方式二:导出变量列表
export { x, y, z }
//------------ ./modules/module1.js --------------const name = 'rayhomie'const age = 20const sayHello = function (name) { console.log('你好', name);}export { name, age, sayHello }//导出的不是对象,只是语法(放置要导出的变量的引用列表)//------------ index.js --------------import { name, age, sayHello } from './modules/module1.js';console.log(name, age);sayHello('leihao')
- 方式三:给变量起别名
export { x as X, y as Y, z as Z }
//------------ ./modules/module1.js --------------const name = 'rayhomie'const age = 20const sayHello = function (name) { console.log('你好', name);}export { name as Name, age as Age, sayHello as SayHello}//------------ index.js --------------import { Name, Age, SayHello } from './modules/module1.js';console.log(Name, Age);SayHello('leihao')
- 方式四:默认导出(不需要变量名字)
export default { xxx, yyy, zzz }
//------------ ./modules/module1.js --------------const name = 'rayhomie'const age = 20const sayHello = function (name) { console.log('你好', name);}export default { name, age, sayHello }//------------ index.js --------------import module1 from './modules/module1.js';const { name, age, sayHello } = module1console.log(name, age);sayHello('leihao')
- 方式五:导入+导出的语法
export { * as module1 } from './modules/module1.js'
一般用于封装库的时候,将所有接口放在一个文件中暴露
//------------ ./modules/module1.js --------------const name = 'rayhomie'const age = 20const sayHello = function (name) { console.log('你好', name);}export { name, age, sayHello }//------------ index.js --------------//先导入全部变量,再导出export { * as module1 } from './modules/module1.js'
import函数
parse->ast->bytecode->runtime
ESM由js引擎解析,在解析时已经确定了模块的依赖关系(在转换成AST之前),所以导入必须写在模块最前面。但ESM还提供了import()
的运行时环境(异步加载)。(浏览器环境没有require()
来加载模块,因为不支持CommonJS)
//------------ index.js --------------let flag = true;if (flag) {//运行时环境 const promise = import('./modules/module1.js') //返回promise promise.then((res) => { console.log(res); })}
在webpack中使用ESM规范的import()
会将该文件进行单独打包,用于动态加载。
打包工具和浏览器环境的ESM区别
webpack、rollup等模块化打包工具,只是实现了ESM规范,然后帮我们把代码解析转化模块化打包,打包出来的代码是常规浏览器都支持运行的代码(不需要浏览器再去解析一次,直接到浏览器运行代码即可)。需要跟浏览器环境的ESM做区别,浏览器环境的ESM代码是由JS引擎去解析并执行。
浏览器ESM加载过程
-
浏览器ESM加载js文件的过程是编译(解析)时加载的,并且是异步的。
-
import不能和运行时相关的内容放在一起使用:from后面的路径不能动态获取;不能建import放在条件语句中;
-
异步:体现在在script标签中设置了
type='module'
,那么这个srcipt脚本就是异步的就不会阻塞其他的脚本内容加载。相当于默认加了一个async属性。 -
<!-- 设置type="module"让js引擎识别 --><script src="./index.js" type="module"></script><!-- 由于浏览器中ES module异步加载,所以不会阻塞后面的脚本执行(相当于默认加了一个async属性) --> <script src="./react.js"></script><script>console.log('hello')</script>
-
-
ESM通过export导出的是变量本身的引用(无论 基本 还是 引用 类型变量,都可以实时的获取到修改后的值,区别于CommonJS)
-
//------------ ./modules/module1.js --------------let name = 'rayhomie'setTimeout(() => { name = 'leihao'}, 500)//CommonJS导出的是对象,所以基本类型就无法获取到变化之后的值。export { name }//记住导出的不是对象,而是一种语法!!!//------------ index.js --------------import { name } from './modules/module1.js';console.log(name);setTimeout(() => { console.log(name);}, 1000)//结果:立即输出rayhomie,一秒后输出leihao。
-
ESM做了基本数据的实时绑定:(导入的基本数据类型值不能修改,但是引用数据类型可以修改它的属性)
在node中使用ESM
node的JS引擎也是本身有ES module的实现,但是默认是使用的CommonJS模块,我们需要以下操作,才可以让nodeJS引擎去解析ES module的模块代码。
- 在package.json中设置
type:module
- 或者使用.mjs作为拓展名,node才会把他当成ES的模块。
CJS和ESM交互
- 通常情况下,CommonJS不能加载ES module
- CJS是同步执行加载,但ESM必须经过静态分析,才可以执行。
- Node中不支持
- webpack等打包工具,可以支持。
- 多数情况下,ES module可以加载CommonJS
- ES module在加载CommonJS时,会将其
module.exports
导出的内容作为export default
导出方式来使用 - 这个依然要看具体的规范实现,比如webpack中是支持的,Node最新的Current版本也是支持的
- ES module在加载CommonJS时,会将其
//Node v16.1.0//------------ ./modules/module.js --------------const name = 'rayhomie'const age = 20module.exports = { name, age }//CommonJS导出//------------ index.mjs --------------import module2 from './modules/module2.js';//ES module导入console.log(module2);//{ name: 'rayhomie', age: 20 }
7】常见内置模块
path模块
①路径拼接
path.resolve
path.join
区别:resolve会判断我们拼接的路径字符串第一个参数中,是否有以/
或./
或../
开头的路径
/
:如果以/
开头则直接拼接无/
或./
:如果是以./
开头,则是从电脑根路径开始cd拼接(没有/
也会被当成./
,比如:'user/mac'等价于'./user/mac'
)../
:如果以../
开头也是从电脑根路径开始cd拼接,但是需要cd到上一层目录
//记住下面的这三个拼接是一样的:
path.resolve(__dirname,'index.js')// /Users/mac/index.js
path.join(__dirname,'index.js')// /Users/mac/index.js
path.join(__dirname,'/index.js')// /Users/mac/index.js
//但是下面这个不一样:
path.resolve(__dirname,'/index.js')// /index.js
写一个resolve方法:
const resolve = dir => path.resolve(__dirname, dir)
console.log(resolve('src'));///Users/mac/src
//webpack中就使用这样的方式起别名:
{
alias:{
"@":resolve("src"),
"components":resolve("src/components")
}
}
②获取路径信息
path.dirname
:文件夹路径path.basename
:文件名path.extname
:后缀名
fs模块
①同步操作②异步回调③异步promises
const fs = require('fs')const filePath = './text.txt'//同步操作const syncFile = fs.readFileSync(filePath)console.log(syncFile);//异步回调fs.readFile(filePath, (err, data) => { if (err) { console.log(err); return; } console.log('读取的内容是:', data)//默认是读取到十六进制的buffer类型 console.log(data.toString())//Buffer转成String类型})//使用promisesconst promisesFile = fs.promises.readFile(filePath)promisesFile.then(data => console.log(data))
events模块
基本使用
const EventEmitter = require('events')//1.创建发射器实例const emitter = new EventEmitter()//2.监听某个事件//on是addListener的简写emitter.on('eventname', (...args) => { console.log('监听到了eventname事件第1次', ...args);})emitter.addListener('eventname', (...args) => { console.log('监听到了eventname事件第2次', ...args);})//3.触发事件emitter.emit('eventname','rayhomie','leihao')/*打印结果:监听到了eventname事件第1次 rayhomie leihao监听到了eventname事件第2次 rayhomie leihao*/
不常用
const EventEmitter = require('events')const emitter = new EventEmitter()emitter.on('tap', (...args) => { console.log('监听到了tap', ...args);})emitter.on('click', (...args) => { console.log('监听到了click', ...args);})console.log(emitter.eventNames());// [ 'tap', 'click' ]console.log(emitter.listenerCount('tap'));// 1console.log(emitter.listeners('click'));// [ [Function (anonymous)] ]
8】Npm包管理
package.json
必填写的属性:name、version
-
name是项目的名称
-
version是的当前项目的版本号
-
private记录当前的项目是否私有
-
description是描述信息
-
author是作者相关信息
-
license是开源协议
-
main属性(在webpack中这个mian字段没有用,因为入口交给webpack去打包了,而且这个main定义的入口只遵循CommonJS规范,不支持tree-shaking)
//别人使用我们的包时,require时就会到node_modules下的包里面找main入口文件去加载。const rayhomieui=require('rayhomieui')
-
module属性不是npm官方的字段(webpack和rollup联合推出),定义了ES module规范的入口(方便tree-shaking)
-
types属性是定义类型声明文件入口
-
browserslist属性:用于配置打包后的JavaScript浏览器的兼容情况,否则我们需要配置polyfills来支持语法。
依赖版本号
semver版本规范X.Y.Z:
- X主版本号(major):兼容不了以前的版本。
- Y次版本号(minor):向下兼容,加了新功能特性。
- Z修订号(patch):没有新功能,只是修复之前版本的bug。
^x.y.z
:表示x保持不变的,y和z永远安装最新版本。
~x.y.z
:表示x和y保持不变的,z永远安装最新版本。
npm install原理
查看npm缓存
#获取当前的缓存文件夹npm config get cache
yarn工具
弥补npm早期的缺陷,npm5改进升级了很多。
npm i yarn -g
全局安装yarn。
npx
npx是npm5.2之后自带的一个工具
作用:常用用它来调用项目中的某个模块的命令。
举例:比如我们全局安装webpack@5.37.0,项目中安装webpack@3.6.0。此时我们在项目中使用命令webpack --version
,使用的是全局的5.37.0。此时我们想要使用项目中的局部命令,①只能通过./node_modules/.bin/webpack --version
来使用局部webpack,②在package.json中设置脚本也是使用的局部的命令,③也可通过npx webpack --version
来调用局部命令。
9】手写脚手架
命令行编写
需要使用到Linux中的**shebang(hashbang)**符号#!
,作用是根据环境来执行脚本。
下面这个就是在index.js文件中写了一句shebang代码,表示的是别人在运行这个脚本的时候,需要根据环境去找node可执行文件,然后找到node之后去执行index.js后面的代码内容。
#!/usr/bin/env nodeconsole.log('hello world')
配置bin字段
在package.json中有一个bin字段,可以对当前包的终端命令行进行配置:
{ "name": "lh", "version": "1.0.0", "description": "", "main": "index.js", "bin": {//配置命令行 "lh": "index.js"//当别人安装我们的包之后,可以指向lh命令。然后就去运行index.js }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "rayhomie <1572801584@qq.com>", "license": "MIT"}
流程:但别人安装了我们的包之后可以使用我们在bin中定义的lh
命令,去执行index.js
文件,而且会根据文件中的shebang
指令去当前环境找node,器执行文件中的脚本。
开发时link测试
我们在开发时,可以在当前包下使用npm link
命令。将包链接到当前环境,然后就可以模拟用户安装了我们的包的操作,再使用我们的包了。软链接后的包可以在全局包中目录下查看npm root -g
,正式发包后可以把它删除了。
此时在终端命令行使用lh
命令,即可看到终端输出hello world
。
命令参数传递
例子,写一个lh --version
查看版本号的命令。
#!/usr/bin/env node//参数-v,--version获取版本号process.argv.find(item => ['--version', '-V'].includes(item)) && console.log(require('./package.json').version);
这样通过process.argv来获取参数判断显然很麻烦,所以我们可以使用一个commander库进行命令行开发更方便。
#!/usr/bin/env nodeconst program = require('commander')program.version(require('./package.json').version)program.parse(process.argv)//解析参数,--help
此时在终端输入lh --help
可以解析出所有的参数。lh --version
就可以显示当前版本号。
commander
#!/usr/bin/env nodeconst program = require('commander')//查看版本号program.version(require('./package.json').version)//添加自己的optionsprogram.option('-h --lh', 'a lh cli')//手动设置路径,<>为可选参数program.option('-d --dest <dest>', 'a destination folder, example: -d /src/components')//监听参数传递program.on('--help', function () { console.log('监听到--help!!!');})//解析一定要写在参数配置的后面program.parse(process.argv)/*//在program.parse后面可以获取到参数信息const opts = program.opts()//lh -h,打印:trueconsole.log(opts.lh);//lh -d /src/components,打印:/src/componentsconsole.log(opts.dest);*///命令program .command('create <project> [others...]')//创建命令 .description('clone repository into a folder')//描述 .action((project, others) => {//可以获取参数 console.log(project, others);})//触发
download-git-repo
使用这个库可以用node克隆git的仓库到本地。
promisify
在node中可以使用node提供的工具,将回调形式转成promise
const { promisify } = require('util')
const download = promisify(require('download-git-repo'))
download(...).then(res=>{ }).catch(err=>{ })
node执行终端命令
需要用到child_process模块(封装terminal.js)
//进程通信
const { exec, spawn } = require('child_process')
const commandSpawn = (...args) => {
return new Promise((resolve, reject) => {
//创建子进程,执行终端命令,并返回子进程,获取子进程信息
const childProcess = spawn(...args)//返回值是该子进程(进行进程间通信)
childProcess.stdout.pipe(process.stdout)//将子进程的输出流导入到当前进程的输出流中
childProcess.stderr.pipe(process.stderr)//错误信息导入
childProcess.on('close', () => {
resolve()
})//监听关闭
})
}
const commandExec = (...args) => {
return new Promise((resolve, reject) => {
const childProcess = exec(...args)
childProcess.stdout.pipe(process.stdout)
childProcess.stderr.pipe(process.stderr)
childProcess.on('close', () => {
resolve()
})
})
}
module.exports = {
commandSpawn,
commandExec
}
进行调用执行终端命令。具体exec和spawn的参数可以在官网查看。
commandSpawn(npm, ['install', '--force'], { cwd: `./${project}` })//cwd选择执行路径
使用node修改文件内容
这里就举个栗子,修改packge.json中的name字段
const path = require('path')//使用promisify包裹const { promisify } = require('util')const readFile = promisify(require('fs').readFile)const writeFile = promisify(require('fs').writeFile)async function writeProjectName(Path, name) {//修改package.json的name(传入的参数是项目根路径,name字段需要更改的名字) //读取文件 const data = await readFile(path.join(Path, 'package.json'), 'utf8') const newList = {}//暂存新对象 const list = JSON.parse(data) for (let key in list) { if (key === 'name') { newList[key] = name continue } newList[key] = list[key]; } let newContent = JSON.stringify(newList, null, 2); //重写文件 await writeFile(path.join(Path, 'package.json'), newContent, 'utf8')}module.exports = writeProjectName
10】Buffer
计算机中所有的内容最终都会使用二进制来表示,如文字、数字、图片、音频、视频等。
js中0x开头的都是16进制数。
使用常用场景:
- 在node使用buffer对二进制数据进行处理。Node中可以使用sharp库,就是读取图片或者传入图片的Buffer对其再进行处理。
- 一个保存文本的文件并不是使用utf-8进行编码的,而是使用GBK,那么我们必须读取到他们的二进制数据,再通过GBK转换成对应的文字。
- Node中通过TCP建立长连接,TCP传输的是字节流,我们需要将数据转成字节再进行传入,并且需要知道传输字节的大小(客户端需要根据大小来判断读取多少内容)
//推荐使用Buffer.from,而不是new Bufferconst message = '你好啊'//1.对中文使用buffer进行utf8的编码 let utf8buffer = Buffer.from(message, 'utf8')console.log(utf8buffer);// <Buffer e4 bd a0 e5 a5 bd e5 95 8a>//2.toString默认对utf8解码console.log(utf8buffer.toString());// 你好啊console.log(utf8buffer.toString('utf16le'))// 뷤붥闥//1.对中文使用buffer进行utf16le的编码 let utf16lebuffer = Buffer.from(message, 'utf16le')console.log(utf16lebuffer);// <Buffer 60 4f 7d 59 4a 55>//2.toString对utf16le解码console.log(utf16lebuffer.toString('utf16le'));// 你好啊
buffer的创建方式
new Buffer
Deprecated: Use Buffer.from(string[, encoding])
instead.
被重声明,请使用Buffer.from
Buffer.from
//Buffer.from(string[, encoding])const buf1 = Buffer.from('buffer');const buf2 = Buffer.from(buf1);buf1[0] = 0x61;console.log(buf1.toString());// Prints: aufferconsole.log(buf2.toString());// Prints: buffer
Buffer.alloc
//Buffer.alloc(size[,fill[,encoding]])let buffer = Buffer.alloc(8)//给当前buffer分配8个字节内存console.log(buffer);// <Buffer 00 00 00 00 00 00 00 00>//直接修改buffer内容buffer[0] = 88// 十进制的88buffer[1] = 0x88// 十六进制的88console.log(buffer);// <Buffer 58 88 00 00 00 00 00 00>let fillbuffer= Buffer.alloc(8,'12')// 分配8字节并填充满字符串console.log(fillbuffer);// <Buffer 31 32 31 32 31 32 31 32>console.log(fillbuffer.toString());// 12121212
Buffer和文件操作
读取文件的本质都是读取二进制数据
//文本文件处理
const fs = require('fs')
fs.readFile('./text.txt', { encoding: 'utf8' }, (err, data) => {
console.log(data);// 雷浩
})
fs.readFile('./text.txt', (err, data) => {
console.log(data);// <Buffer e9 9b b7 e6 b5 a9>
console.log(data.toString());// 雷浩
})
使用sharp处理图片文件
(它内部也是需要拿到buffer数据来处理图片)
//处理图片文件
const fs = require('fs')
//使用sharp库来处理图片数据https://github.com/lovell/sharp#documentation
const sharp = require('sharp');
//方式一:传入buffer
fs.readFile('./picture.jpg', (err, data) => {
sharp(data)
.resize(20, 20)
.toFile('./20x20.jpg')
})
//方式二:传入路径
sharp('./picture.jpg')
.resize(100, 100)
.toFile('./100x100.jpg')
11】事件循环和异步IO
事件循环
- 事实上我们把事件循环理解成我们编写的JavaScript和浏览器或者Node之间的一个桥梁。
- 浏览器事件循环是一个我们编写的JavaScript代码和浏览器Api调用(setTimeout/AJAX/监听事件等)的一个桥梁,桥梁之间他们通过回调函数进行沟通。
- Node的事件循环是一个我们编写的JavaScript代码和系统调用(fs、network)之间的一个桥梁,桥梁之间他们通过回调函数进行沟通。
进程和线程
- 进程(process):计算机已经运行的应用程序;
- 线程(thread):操作系统中能够运行运算调度的最小单位;
- 进程可以看成线程的容器。
浏览器事件循环
宏任务:同步代码、timer回调、浏览器事件回调、ajax回调、DOM监听、UI Rendering
微任务:Promise的then回调、Mutation Observer API、queueMicrotask()等
宏任务执行结束完成后立即去清空微任务队列,直到微任务被清空才去执行下一轮宏任务。(只维护两个队列:消息队列和微任务队列)
- 在执行任何一个宏任务之前(不是列队,是一个宏任务),都会先去查看微任务队列中是否有任务需要执行
- 也就是宏任务执行之前,必须保证微任务队列是空的
- 如果不为空,那么就优先执行微任务队列中的任务(回调)
<script>
console.log('开始')
setTimeout(() => {
console.log('结束')
}, 0)
new Promise((resolve, reject) => {
resolve('then')
}).then(res => console.log(res))
for (let i = 0; i < 1000000000; i++) {
}
//顺序:开始----------->then-->结束
//等待若干秒后才执行结束。因为timer的回调需要等待宏任务结束才执行
//浏览器事件、ajax等和timer一样都是异步宏任务,会被加到宏任务队列
</script>
题目一:
setTimeout(() => {
console.log('set1');
new Promise((resolve) => {
resolve()
}).then(() => {
new Promise((resolve) => {
resolve()
}).then(() => {
console.log('then4');
})
console.log('then2');
})
})
new Promise((resolve) => {
console.log('pr1');
resolve()
}).then(() => {
console.log('then1');
})
setTimeout(() => {
console.log('set2');
})
console.log(2);
queueMicrotask(() => {
console.log('queueMicrotask1');
})
new Promise((resolve) => {
resolve()
}).then(() => {
console.log('then3');
})
//答案:
/*
pr1
2
then1
queueMicrotask1
then3
set1
then2
then4
set2
*/
题目二:
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(() => {
console.log('setTimeout')
}, 0)
async1()
new Promise((resolve) => {
console.log('promise1');
resolve()
}).then(() => {
console.log('promise2');
})
console.log('script end');
//答案:
/*
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
*/
await async2()
可以看做成new Promise(resolve=>{ async2() .. })
await
下面的代码就是认为是then
回调函数中的代码
题目三:
被resolve之后才会调用一次.then。(连续的.then调用是一次一次加入微任务中的)
Promise.resolve().then(()=>{
console.log(1)
Promise.resolve().then(()=>{
console.log(3)
})
}).then(()=>{
console.log(2)
})
/*
1
3
2
*/
题目四:
Promise.resolve().then(()=>{
console.log(0)
setTimeout(()=>{
console.log('宏任务')
},0)
return Promise.resolve(4)
}).then((res)=>{
console.log(res)
})
Promise.resolve().then(()=>{
console.log(1)
}).then(()=>{
console.log(2)
}).then(()=>{
console.log(3)
}).then(()=>{
console.log(5)
}).then(()=>{
console.log(6)
})
/*
0
1
2
3
4
5
6
宏任务
*/
讲道理,按照我们对事件循环的知识体系去解答,得到的答案应该是这样的:0,1,4,2,3,5,6,宏任务
。但是在node环境和浏览器环境运行却答案不一致。
这道题答不对没关系,这题属于是顶级牛角尖的题目(与Promise实现有关)
顶级解释在这里:求前端大佬解析这道Promise题,为啥resolved 是在 promise2之后输出?
Node事件循环
node架构分析
浏览器中的EventLoop是根据HTML5规范来实现的,不同的浏览器可能会有不同的实现,而Node中是由libuv实现的。
- libuv中主要维护了一个EventLoop和worker threads(线程池),libuv是一个多平台的专注于异步IO的库。libuv采用的是非阻塞异步IO的调用方式
- Event Loop负责调用系统的一些其他操作:文件的IO、Network、child-process等
阻塞IO和非阻塞IO
操作系统通常提供了两种调用方式:阻塞式调用和非阻塞式调用
- 阻塞式调用:调用结果返回之前,当前线程处于阻塞态(阻塞态CPU是不会分配时间片的)调用线程只有在得到调用结果之后才会继续执行
- 非阻塞式调用:调用执行之后,当前线程不会停止执行,只需要过一段时间来检查一下有没有结果返回即可(轮询操作:需要知道是否系统调用完成)
开发中的很多耗时操作都是基于非阻塞式调用:
- 网络请求本身使用了Socket通信,而Socket本身提供了select模型,可以进行非阻塞式的工作
- 文件读写的IO操作,我们可以使用操作系统提供的基于事件的回调机制
如果主线程频繁的去进行轮询的工作,那么必然会大大降低性能,并且开发中我们不只是一个文件的读写,可能是多个文件,而且可能是多个功能的系统非阻塞式调用,如:网络IO、数据库IO、子进程调用等。所以libuv提供了一个线程池(thread pool):
- 线程池会负责所有相关的操作,并且会通过轮询或者其他的方式等待结果
- 当获取到结果时,就可以将对应的回调函数放到事件循环中(某一个事件队列)
- 事件循环就可以负责接管后续的回调工作,告知JavaScript应用程序执行对应的回调函数
阻塞和非阻塞,同步和异步的区别?
阻塞和非阻塞是对于被调用者来说的
- 这里就是系统调用,操作系统为我们提供了阻塞调用和非阻塞调用
同步和异步是对于调用者来说的
- 我们在程序中发起调用之后,不会进行其他任何的操作,只是等待结果,这个过程就称之为同步调用
- 发起调用之后,并不会等待结果,继续完成其他的工作,等到有回调时再去执行,这个过程就是异步调用
node事件循环阶段
无论是我们的文件IO、数据库、网络IO、定时器、子进程,在完成对应的操作后,都会将对应的结果和回调函数放到事件循环(任务队列)中,事件循环会不断地从任务队列中取出对应的事件(回调函数)来执行。
Node中一次完整的事件循环Tick分成很多个阶段:(也就是我们注册调度任务(交给Node完成一些IO事件),Node处理完成之后,将结果以回调函数的形式返回给我们,这些回调函数按照事件循环的顺序来执行)
- 定时器(Timers):本阶段执行已经被
setTimeout()
和setInterval()
的调度回调函数 - 待定回调(Pending Callback):对某些系统操作(如TCP错误类型)执行回调,比如TCP连接时接收到ECONNREFUSED
- Idle、prepare:仅系统内部使用
- 轮询(Poll):检索新的I/O事件;执行与I/O相关的回调;(停留时间最长,希望IO的回调尽早响应)
- 检测:
setImmediate()
的回调函数在这里执行 - 关闭的回调函数:一些关闭的回调函数,如:
socket.on('close',...)
宏任务微任务
Node宏任务:setTimeout、setInterval、IO事件、setImmediate、close事件;
Node微任务:Promise的then回调、queueMicrotask、process.nextTick
注意:process.nextTick是放在单独的一个队列里面存放(不和Promise.then、queueMicrotask放在一起)
Node事件循环维护的队列
微任务队列:next ticks队列、其他微任务队列(promise.then、queueMicrotask)
宏任务队列:timers队列(setTimeout、setInterval)、轮询队列(io事件)、check队列(setImmediate)、close队列(close回调)
题目一:
async function async1() { console.log('async1 start'); await async2() console.log('async1 end');}async function async2() { console.log('async2');}console.log('script start');setTimeout(() => { console.log('setTimeout0');}, 0)setTimeout(() => { console.log('setTimeout2');}, 300)setImmediate(() => console.log('setImmediate'))process.nextTick(() => console.log('nextTick1'))async1()process.nextTick(() => console.log('nextTick2'))new Promise((resolve) => { console.log('promise1'); resolve(); console.log('promise2');//会被同步执行}).then(() => { console.log('promise3');})console.log('script end');//答案:/*script startasync1 startasync2promise1promise2script endnextTick1nextTick2async1 endpromise3setTimeout0setImmediatesetTimeout2*/
题目二:
setTimeout(() => { console.log('setTimeout');}, 0)setImmediate(()=>{ console.log('setImmediate');})//答案:/*setTimeoutsetImmediate或者setImmediatesetTimeout*/
原因:初始化事件循环队列需要时间,timers的回调先保存到红黑树再放到timers队列中也需要时间,setImmediate的回调不需要保存直接加入到check队列几乎不耗时。
12】Stream
Node中很多对象是基于流实现的:
- http模块的Request和Response对象
- process.stdout对象
另外:官网说所有的流都是EventEmitter的实例
Nodejs中有四种基本流类型:
- Writable:可以向其写入数据的流(
fs.createReadStream
) - Readable:可以从中读取数据的流(
fs.createWriteStream
) - Duplex:同时为Readable和Writable的流
- Transform:Duplex可以在写入和读取数据时修改或转换数据的流
Readable流的使用
const fs = require('fs')//流的方式读取const reader = fs.createReadStream('./read.txt', { code: 'utf8', highWaterMark: 2//每次最多读两个字节})//监听data回调获取读取的数据reader.on('data', (data) => { console.log(data.toString()); reader.pause()//暂停读取 setTimeout(() => { reader.resume()//恢复读取 }, 1000)})//监听文件被打开reader.on('open', () => { console.log('文件被打开');})//监听文件关闭reader.on('close', () => { console.log('文件被关闭');})
Writable流的使用
const fs = require('fs')const writer = fs.createWriteStream('./write.txt')writer.write('你好啊', (err) => { if (err) { console.log(err); return } console.log('写入成功一次');})writer.write('雷浩',(err)=>{ console.log('写入成功二次');})// writer.close()//可以使用closewriter.end('End')//也可以使用end(传入参数可以写入到文件中)//监听关闭writer.on('close', () => { console.log('文件已关闭');})
pipe方法的使用
const fs = require('fs')const writer = fs.createWriteStream('./write.txt')const reader = fs.createReadStream('./read.txt', { code: 'utf8', highWaterMark: 2//每次最多读两个字节})reader.pipe(writer)//将读到的值,导入输出流writer.close()
13】Http
web服务器:当客户端需要某一个资源时,可以向一台服务器,通过Http请求获取到这个资源;提供资源的这个服务器,就是web服务器。
创建web服务器
const http = require("http");//创建一个web服务器const server = http.createServer((req, res) => { res.end("hello server");});//启动服务器server.listen(3000, () => { console.log(`~服务器已经启动在3000端口~`);});
由于原生http较为复杂:url判断、method判断、参数处理、逻辑代码处理等,所以我们需要框架进行处理。
14】Express
Express整个框架的核心就是中间件,理解了中间件其他都很简单,其实中间件就是一个回调函数。
使用方式:
方式一:通过express-generator
脚手架创建
#安装npm i -g express-generator#创建脚手架模板express demo_name
方式二:从零自己搭建
const express = require("express");//express其实是一个函数:createApplication => 返回app对象const app = express();//监听默认路径app.get("/", (req, res, next) => { res.end("hello get express");});app.post("/", (req, res, next) => { res.end("hello post express");});//开启监听app.listen(8888, () => { console.log("express服务器启动成功");});
express是一个路由和中间件的web框架,它本身的功能非常少,Express应用程序的本质上是一系列中间件函数的调用。
中间件:本质上是传递给express的一个回调函数。
- 这个回调函数接收三个参数:
- 请求对象(request对象)
- 响应对象(response对象)
- next函数(在express中定义的用于执行下一个中间件的函数)
- 如果当前中间件功能没有结束请求-响应周期,则必须调用
next()
,将控制权传递给下一个中间件功能,否则请求将被挂起。
以路由中间件为例: