安装node.js
安装
验证
1 2 3 4 |
$ node -v v16.14.2 $ npm -v 8.7.0 |
其他设置1
镜像
- 找到
npm
配置文件:Windows
:C:\Users\<用户名>\.npmrc
Mac/Linux
:~/.npmrc
- 用文本编辑器打开
.npmrc
,添加以下内容:
1 2 |
electron_mirror="https://npmmirror.com/mirrors/electron/" registry=https://registry.npmmirror.com/ |
- 保存文件并重启终端
- 清理缓存并重试
- 可能需要
1 2 3 4 5 6 7 8 |
# 强制清理 npm 缓存 npm cache clean --force # 删除 node_modules 和锁定文件 rm -rf node_modules package-lock.json # 重新安装 Electron npm install --save-dev electron |
- 通过以下命令检查配置是否生效:
electron_mirror=https://npmmirror.com/mirrors/electron/
表示配置成功
1 |
npm config list | grep electron_mirror |
代理
- 若使用
VPN
或企业代理,需在.npmrc
中添加代理设置:
1 2 |
proxy=http://your-proxy:port https-proxy=http://your-proxy:port |
创建应用程序
初始化 npm
项目
1 2 |
mkdir my-electron-app && cd my-electron-app npm init |
将 Electron
安装为项目的 devDependencies
- 仅在开发环境需要的额外依赖
- 输出如下:
1 2 3 4 5 6 7 |
d:\codes\electron\my-electron-app>npm install --save-dev electron npm warn deprecated boolean@3.2.0: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. added 70 packages in 4m 17 packages are looking for funding run `npm fund` for details |
运行 Electron
应用
main.js
1 |
console.log('Hello from Electron 👋') |
package.json
- 要执行这个脚本,需要在
package.json
的scripts
字段中添加一个start
命令,内容为electron .
- 这个命令会告诉
Electron
在当前目录下寻找主脚本,并以开发模式运行它
- 要执行这个脚本,需要在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
{ "name": "my-electron-app", "version": "1.0.0", "description": "Hello World!", "main": "main.js", "scripts": { "start": "electron .", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Jane Doe", "license": "MIT", "devDependencies": { "electron": "23.1.3" } } |
- 终端执行:
1 |
npm run start |
1 2 3 4 5 6 |
d:\codes\electron\my-electron-app>npm run start > my-electron-app@1.0.0 start > electron . Hello from Electron 馃憢 |
将网页装载到 BrowserWindow
- 在
Electron
中,每个窗口展示一个页面,后者可以来自本地的HTML
,也可以来自远程URL
- 在项目的根目录中创建一个
index.html
文件,并写入下面的内容:
- 在项目的根目录中创建一个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'" /> <meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'" /> <title>Hello from Electron renderer!</title> </head> <body> <h1>Hello from Electron renderer!</h1> <p>👋</p> </body> </html> |
修改main.js
- 第一行中,我们使用
CommonJS
语法导入了两个Electron
模块:app
,这个模块控制着您应用程序的事件生命周期BrowserWindow
,这个模块创建和管理app
的窗口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
console.log('Hello from Electron 👋') const {app, BrowserWindow} = require('electron') const createWindow = () => { const win = new BrowserWindow({ width: 800, height: 600 }) win.loadFile('index.html') } app.whenReady().then(() => { createWindow() }) |
Electron
的许多核心模块都是Node.js
的事件触发器,遵循Node.js
的异步事件驱动架构app
模块就是其中一个- 在
Electron
中,只有在app
模块的ready
事件(event
)触发后才能创建BrowserWindows
实例 - 可以通过使用
app.whenReady()
API
来监听此事件,并在其成功后调用createWindow()
方法
1 2 3 |
app.whenReady().then(() => { createWindow() }) |
关于页面
- 应用中的每个页面都在一个单独的进程中运行,我们称这些进程为 渲染器 (
renderer
) - 渲染进程使用与常规
Web
开发相同的JavaScript API
和工具
管理应用的窗口生命周期
概述
- 应用窗口在不同操作系统中的行为也不同
Electron
允许您自行实现这些行为来遵循操作系统的规范,而不是采用默认的强制执行- 可以通过监听
app
和BrowserWindow
模组的事件,自行实现基础的应用窗口规范
针对特定平台
- 通过检查
Node.js
的process.platform
变量,您可以针对特定平台运行特定代码
关闭所有窗口时退出应用 (Windows & Linux
)
- 在
Windows
和Linux
上,我们通常希望在关闭一个应用的所有窗口后让它退出 - 可以监听
app
模块的window-all-closed
事件,并在判断用户不使用macOS
后调用app.quit()
来退出您的应用程序
1 2 3 |
app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit() }) |
没有窗口打开则打开一个窗口 (macOS
)
- 与前二者相比,即使没有打开任何窗口,
macOS
应用通常也会继续运行- 在没有窗口可用时调用
app
会打开一个新窗口 - 可以监听
app
模组的activate
事件,如果没有任何打开(open
)的BrowserWindow
,调用您已有的createWindow()
方法新建一个
- 在没有窗口可用时调用
1 2 3 4 5 6 7 |
app.whenReady().then(() => { createWindow() app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow() }) }) |
VSCode
里面调试
- 在根目录新建一个
.vscode
文件夹,然后在其中新建一个launch.json
配置文件并填写如下内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
{ "version": "0.2.0", "compounds": [ { "name": "Main + renderer", "configurations": ["Main", "Renderer"], "stopAll": true } ], "configurations": [ { "name": "Renderer", "port": 9222, "request": "attach", "type": "chrome", "webRoot": "${workspaceFolder}" }, { "name": "Main", "type": "node", "request": "launch", "cwd": "${workspaceFolder}", "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", "windows": { "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" }, "args": [".", "--remote-debugging-port=9222"], "outputCapture": "std", "console": "integratedTerminal" } ] } |
DevTools
调试
Node.js
的异步事件驱动架构
概述
Node.js
的异步事件驱动架构是其高效处理高并发的核心机制,结合了非阻塞I/O
、事件循环(Event Loop
)和事件驱动的编程模型
V8
- 负责解析和执行
JavaScript
代码,将代码编译为机器指令 - 与底层系统直接交互,提升性能
libuv
库
- 跨平台异步
I/O
库,提供事件循环、线程池和文件系统操作的非阻塞实现 - 管理事件循环的
6
个阶段(timers
、poll
、check
等),协调异步任务调度
事件循环(Event Loop
)
- 单线程模型:
- 主线程仅负责事件调度,避免多线程上下文切换的开销
- 非阻塞
I/O
:- 通过回调函数或
Promise
处理耗时操作(如文件读写、网络请求),主线程无需等待I/O
完成
- 通过回调函数或
事件循环的详细阶段
- 概述
- 事件循环按顺序执行以下阶段,每个阶段处理特定类型的任务
Timers
阶段- 执行
setTimeout
和setInterval
的回调
- 执行
Pending I/O Callbacks
- 处理上一轮未完成的
I/O
回调(如TCP
错误)
- 处理上一轮未完成的
Idle/Prepare
- 系统内部使用
Poll
阶段- 核心阶段:检索新的
I/O
事件(如文件读取完成、HTTP
请求到达),执行关联的回调 - 若队列为空,则阻塞等待新事件,或跳转到
Check
阶段
- 核心阶段:检索新的
Check
阶段- 执行
setImmediate
的回调
- 执行
Close Callbacks
- 处理关闭事件(如
socket.on('close')
)
- 处理关闭事件(如
异步任务的分类与优先级
- 宏任务(
Macrotask
)- 包括
setTimeout
、setInterval
、I/O
操作、setImmediate
- 每个事件循环阶段执行一个宏任务
- 包括
- 微任务(
Microtask
)- 包括
Promise.then
、process.nextTick
process.nextTick
在事件循环各阶段之间立即执行(优先级最高)Promise
回调在宏任务结束后执行
- 包括
事件驱动模型的核心机制
EventEmitter
类- 实现发布-订阅模式,通过
.on()
注册事件监听器,.emit()
触发事件
- 实现发布-订阅模式,通过
1 2 3 4 5 |
const EventEmitter = require('events'); class MyEmitter extends EventEmitter {} const emitter = new MyEmitter(); emitter.on('event', () => console.log('Event triggered!')); emitter.emit('event'); // 触发事件 |
非阻塞 I/O
的工作流程
- 主线程发起
I/O
请求后,立即继续执行后续代码 I/O
完成后,libuv
将回调推入事件队列,事件循环在Poll
阶段执行回调
单线程与事件循环
Node.js
主线程运行V8
引擎解析JavaScript
,同步代码直接执行,异步任务(如I/O
、定时器)通过libuv
提交到事件循环队列- 主线程通过 事件循环(
Event Loop
) 调度任务,避免阻塞
- 主线程通过 事件循环(
- 与
libuv
的协作- 任务分发:
Node.js
主线程将异步任务(如fs.readFile
)委托给libuv
,后者通过线程池或系统内核的异步接口(如epoll
)处理 - 回调触发:任务完成后,
libuv
将回调推入事件队列,主线程在轮询到对应阶段时执行回调
- 任务分发:
问题
libuv
是否维护自己的事件队列?- 是的,
libuv
维护独立的事件队列,但它的队列结构是分阶段的 libuv
的事件循环分为6
个阶段(如Timers
阶段、Poll
阶段、Check
阶段等),每个阶段对应一个特定类型的事件队列
Timers
队列:存放setTimeout
/setInterval
的回调
I/O
队列:存放已完成的I/O
操作(如文件读写、网络请求)的回调
Check
队列:存放setImmediate
的回调- 当异步任务(如文件
I/O
)完成时,libuv
的线程池会将回调推入对应阶段的队列,等待事件循环轮询到该阶段时执行
- 是的,
- 主线程是否有自己的事件队列?
- 主线程没有独立的事件队列,但
Node.js
主线程的事件循环与libuv
的事件队列紧密绑定
具体来说:
主线程的事件循环直接管理libuv
的事件队列
当主线程空闲时,会通过事件循环(uv_run
)依次轮询libuv
的6
个阶段队列,并执行其中的回调 - 微任务队列:主线程还有两个特殊的中间队列(不属于
libuv
):
nextTick
队列:存放process.nextTick
的回调,优先级最高
Microtask
队列:存放Promise
的回调,优先级次之
这两个队列会在每个libuv
阶段切换时优先清空
- 主线程没有独立的事件队列,但
- 主线程有没有任务队列?
nextTick 队列
和Microtask 队列
属于Node.js
主线程自身的任务队列
- 回调的执行者是
libuv
还是主线程?- 回调由主线程执行,但
libuv
负责调度: libuv
线程池:处理耗时任务(如文件I/O
、DNS
解析),完成后将回调推入对应阶段的队列- 主线程的事件循环:在轮询到对应阶段时,从
libuv
队列中取出回调并执行
- 回调由主线程执行,但
- 主程序处理任务的优先级?
- 同步代码
nextTick
队列Microtask
队列libuv
阶段队列(如Timers
、I/O
)
总结(记忆)
node.js
主线程运行着一个事件循环,还有两个自己的任务队列nextTick
和Microtask
v8
引擎负责解析执行JavaScript
代码,同步代码直接在主线程执行,异步代码交给libuv
处理libuv
维护了6
个阶段队列,这6
个队列存放相应任务的对应的回调node.js
主线程在空闲时候,按优先级取出这些队列中的回调(nextTick
,Microtask
,libuv
阶段队列)并执行
事件触发器
EventEmitter
类
Node.js
的events
模块提供了EventEmitter
类,所有能触发事件的对象均是该类的实例- 通过
on()
方法注册监听器(订阅事件),emit()
方法触发事件(发布事件)
1 2 3 4 5 6 7 |
const EventEmitter = require('events'); const emitter = new EventEmitter(); // 订阅事件 emitter.on('fire', () => console.log('点火')); // 触发事件 emitter.emit('fire'); // 输出:点火 |
事件驱动的异步模型
- 异步模型见上述
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!