Deno/Bun学习笔记

Deno/Bun学习笔记

小叶子

封面作者:れむ

请先阅读Node.js学习笔记

⭐Deno

Deno 是一个基于 V8 引擎的 JavaScriptTypeScript 运行时, 由 Node.js 的创始人 Ryan Dahl 开发, 目的是解决 Node.js 的一些问题

  • Deno 是一个安全的运行时环境, 它默认不允许访问文件系统、网络和环境变量, 除非显式授权
  • Deno 内置了 TypeScript 编译器, 无需安装额外的工具
  • Deno 使用 ES Modules, 不再支持 CommonJS 模块
  • Deno 所有的异步操作都返回 Promise, 不再使用传统回调函数
  • Deno 通过 URL 导入模块, 不再使用 node_modules 目录
  • Deno 可以简单地将 JSTS 文件编译为可执行文件
  • Deno 旨在兼容 WebAPI, 如 fetchprompt
  • Deno 的内置 API 都在全局对象 Deno

安装运行

类似于 NVM, Deno 也有一个 Deno Version Manager, DVM, 可以用来安装和管理 Deno 版本

1
2
3
4
5
6
7
8
# 用 PowerShell 安装
irm https://deno.land/x/dvm/install.ps1 | iex
# 验证
dvm info
deno --version
# 运行
deno run xxx.js/xxx.ts
# 可以在 VSCode 中安装 Deno 插件
命令说明
dvm install <version>安装指定版本的 Deno
dvm uninstall <version>卸载指定版本的 Deno
dvm use <version>使用指定版本的 Deno
dvm list列出已安装的 Deno 版本
dvm info显示当前使用的 DVMDeno 版本
dvm activate激活 DVM
dvm deactivate取消激活 DVM

要卸载 DVM, 只需删除 DVM 安装的文件夹和环境变量即可

基本命令

命令说明
deno --version显示 Deno 版本
deno upgrade更新 Deno 版本
deno run xxx运行 JSTS 文件, 支持本地和远程文件
deno fmt xxx格式化 JSTS 文件
deno compile [--output xxx] xxx编译 JSTS 文件为可执行文件
  • 安全选项和 --watch 选项应放在 run 等命令之后, xxx 之前
  • --watch 选项可以监视文件变化, 自动重新执行命令, 支持 deno run/test/compiler/fmt 命令
  • xxx 之后的所有选项都会被传递给 xxx 内的 Deno.args 数组

deno.json

deno.jsonDeno 的配置文件, 不是必须的, 但可以用来配置 Deno 运行时的一些选项, 详见官方文档

1
2
3
4
5
6
{
"tasks": {
// 设置任务, 相当于 package.json 中的 scripts
"start": "deno run --allow-net server.js"
}
}
1
deno task start

安全选项

选项说明
--allow-net[=IP/HOSTNAME]允许网络访问
--allow-read[=PATH]允许读取文件
--allow-write[=PATH]允许写入文件
--allow-env[=KEY]允许访问环境变量
--allow-run[=PROGRAM]允许运行子进程
--allow-all / -A允许所有权限
--unstable-xxx允许不稳定的 API, 如 --unstable-kv

从 Node.js 迁移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Node.js
const fs = require('fs')
const express = require('express')
const config = require('./config,js')
module.exports = { name: 'xiaoyezi' }

// Deno
import fs from 'node:fs'
import express from 'npm:express[@x.x.x]'
import config from './config.js'
export default { name: 'xiaoyezi' }

// __dirname
import.meta.dirname
// __filename
import.meta.filename
1
2
3
4
5
6
7
8
9
10
11
12
13
// package.json
{
"scripts": {
"start": "node --watch server.js"
}
}

// deno.json
{
"tasks": {
"start": "deno run --watch --allow-net --allow-read server.js"
}
}
1
2
3
4
# Node.js
npm start
# Deno
deno task start

兼容列表

API兼容性
node:url完全
node:path完全
node:os完全
node:buffer完全
node:Blob完全
node:process部分
不支持 multipleResolves 事件
不支持 worker 事件
node:http部分
不支持 http.Agent.prototype.createConnection 方法
node:fs部分
不支持 utf16leucs2latin-1 编码写入文件
不支持 Dirent 的一些方法
不支持 FSWatcher.refFSWatcher.unref
node:fs/promises部分
不支持 lchmodlchownlutimes

Standard Library

path

1
import { resolve, dirname } from "https://deno.land/[email protected]/path/mod.ts"
方法说明
resolve(...paths)解析路径
dirname(path)获取路径的目录名

async

1
import * as async from 'https://deno.land/std/async/mod.ts'
方法说明
async.delay(ms[, options])延迟执行(在指定时间后解决 Promise
options 可以设置 persistent(计时结束后继续进程, 默认 true
async.retry(fn, options)重试执行异步函数(直到成功或达到最大次数)
options.maxAttempts(最大次数, 默认 5
options.maxTimeout(单次尝试的超时时间, 默认 60000

cli

1
import { promptSecret } from 'https://deno.land/std/cli/mod.ts'
方法说明
prompt(message)提示用户输入信息(无需引入)
promptSecret(message)提示用户输入密码

yaml

1
import * as yaml from 'https://deno.land/std/yaml/mod.ts'
方法说明
yaml.parse(content)解析 YAML 字符串为对象
yaml.stringify(object)将对象序列化为 YAML 字符串

File System

详见官方文档

读取

方法说明
Deno.readFile(path)
Deno.readFileSync(path)
读取文件, 返回 Uint8Array
Deno.readTextFile(path)
Deno.readTextFileSync(path)
读取文本文件(如 .json.txt), 返回字符串或对象
Deno.open(path[, options])
Deno.openSync(path[, options])
打开文件, 返回 FsFile
options.read: 允许读取, 默认 true
options.write: 允许写入, 默认 false
options.create: 不存在时创建, 默认 false
options.append: 追加写入, 默认 false
options.truncate: 打开后清空文件, 默认 false
options.mode: 文件权限, 默认 0o666, 对 Windows 无效

FsFile

打开的文件是 FsFile 类的实例对象

方法说明
file.close()关闭文件(避免内存泄漏)
file.read(buffer)
file.readSync(buffer)
读取文件到缓冲区
file.write(buffer)
file.writeSync(buffer)
将缓冲区写入文件

buffer 指的是 Uint8Array 类型的类数组对象

写入

方法说明
Deno.writeFile(path, data[, options])
Deno.writeFileSync(path, data[, options])
写入文件(数据为 Uint8Array
Deno.writeTextFile(path, data[, options])
Deno.writeTextFileSync(path, data[, options])
写入文本文件(数据为字符串)

WriteFileOptions

属性说明默认值
append追加写入false
create不存在时创建true
createNew强制创建新文件false
mode文件权限0o666

其他

方法说明
Deno.copyFile(from, to)
Deno.copyFileSync(from, to)
复制文件
Deno.create(path)
Deno.createSync(path)
创建文件
Deno.rename(from, to)
Deno.renameSync(from, to)
重命名或移动文件
Deno.mkdir(path[, options])
Deno.mkdirSync(path[, options])
创建目录
options.recursive: 允许递归创建, 默认 false
Deno.remove(path[, options])
Deno.removeSync(path[, options])
删除文件或目录
options.recursive: 允许递归删除, 默认 false
Deno.stat(path)
Deno.statSync(path)
获取文件信息, 最终返回 Deno.FileInfo 对象

FileInfo

属性说明
isFile是否为文件
isDirectory是否为目录
isSymlink是否为符号链接
size文件大小
mtime修改时间
atime访问时间
birthtime创建时间

HTTP

方法说明
Deno.serve([options, ]callback)创建 HTTP 服务器
callback: 形参为 Request 对象, 返回 Response 对象
options.port: 端口号, 默认 8000
options.hostname: 主机名, 默认 0.0.0.0

RequestResponse 对象详见JavaScript学习笔记

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
// 创建 HTTP 服务器
Deno.serve(async req => {
// 请求方法
console.log(req.method)
// URL
const url = new URL(req.url)
// 路径
console.log(url.pathname)
// 查询参数
console.log(url.searchParams)
// 请求头
console.log(req.headers)
// 请求体
if (req.body) {
const body = await req.json()
console.log(body)
}
// 返回响应
req.respond(JSON.stringify({ name: 'xiaoyezi' }), {
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
})
})

Deno 也支持 HTTPS, 但需要额外的配置, 且实际开发中一般会通过 Nginx 等反向代理服务器来实现 HTTPS

import.meta

import.meta 对象包含了当前模块的元数据;其中 import.meta.filenameimport.meta.dirname 只在本地文件中有效, 可以用来替代 __filename__dirname

属性说明
import.meta.url当前模块的绝对路径, 如 file:///xxx/xxx.js
import.meta.main是否为主模块
import.meta.filename当前模块的绝对路径, 如 D:\xxx\xxx.js
import.meta.dirname当前模块的目录路径, 如 D:\xxx

这个不是 Deno 的独有 API,在设置了 "type": "module"Node.js 环境中也可以使用

全局对象

Deno 的全局对象是 globalThis, 它包含了 JavaScript 中的所有全局对象和函数; 在一些方面相当于 Node.js 中的 process

方法说明
globalThis.close()退出进程(WebAPI 中的方法)
Deno.exit()退出进程, 与 globalThis.close() 没有区别

globalThisselfwindow(不推荐使用)都指向全局对象, 也可以直接使用全局对象下的方法和属性(如 close()

全局事件

Deno 支持一些常用于浏览器的全局事件, 如 loadunloadbeforeunload

1
2
3
4
5
6
7
8
9
10
11
// 定义事件处理函数
const handler = event => {
console.log(`触发了 ${event.type} 事件`)
}
// 这几种添加事件的方式都是有效的
addEventListener('load', handler)
// onload = handler
self.addEventListener('unload', handler)
// self.onunload = handler
globalThis.addEventListener('beforeunload', handler)
// globalThis.onbeforeunload = handler

先执行 beforeunload 事件, 再执行 unload 事件; 其中 unload 事件无法阻止页面关闭, 而 beforeunload 事件可以设置 event.preventDefault() 来阻止页面关闭

WebAPI

Deno 致力于兼容 WebAPI, 如 fetchprompt 等, 以实现更统一的开发体验; 详见官方文档

location

location 对象包含了当前页面的地址信息; 由于 Deno 不是真的浏览器环境, 所以要通过 --location https://xxx 选项来模拟 location 对象

1
2
3
4
5
// --location https://leafyee.xyz
// 获取当前页面的地址信息
console.log(location.href) // https://leafyee.xyz
// 网络访问的相对路径会以 location 为基准
fetch('./api') // https://leafyee.xyz/api

常见可用 API

用法见JavaScript学习笔记

API说明
Worker创建子线程
fetch发送网络请求
prompt从命令行获取用户输入
sessionStorage会话存储
localStorage本地存储

sessionStoragelocalStorage 是绑定到 --location 的, 不同的 --location 不共享

KV

Deno 提供了内置的 KV 存储驱动, 用于存储键值对数据, 可以在本地或 Deno Deploy 上直接使用

本地访问远程 KV 存储时, 需要将 Deno DeployAccess Token 设置为环境变量 DENO_KV_ACCESS_TOKEN, 并在调用 Deno.openKv 时传入数据库的 URL 作为参数

key 是一个数组, 元素按照重要性先后排列, 如 ['user', 'xiaoyezi']; 元素可以是 Uint8Arraystringnumberbigintboolean; value 是一个对象

方法说明
Deno.openKv(['URL'])打开 KV 数据库
kv.close()关闭 KV 数据库
kv.delete(key)删除键值对
kv.get(key)获取键值对
kv.getMany(keysArray)获取多个键值对
kv.list(selector)列出键值对, 返回迭代器而不是 Promise
但遍历迭代器时会返回 Promise
kv.set(key, value[, { expireIn }])设置键值对
expireIn: 过期时间(毫秒)
  • 获取到的多个键值对是一个 KvListIterator 类的可迭代对象, 也可以用 [] 访问其元素
  • 获取到的键值对是一个 Deno.KvEntry 类的实例对象: { key: KvKey; value: T; versionstamp: string; }
  • selector 是一个 Deno.KvListSelector 类的实例对象: { prefix: KvKey; } (前缀选择器); 详见官方文档
1
2
3
4
5
6
7
8
9
10
11
// 打开 KV 数据库
const kv = await Deno.openKv()
// 设置键值对
await kv.set(['user', 'xiaoyezi'], { name: 'xiaoyezi', age: 18 })
// 获取键值对
const data = await kv.get(['user', 'xiaoyezi'])
console.log(data.value)
// 删除键值对
await kv.delete(['user', 'xiaoyezi'])
// 关闭 KV 数据库
kv.close()

常见问题

要这么写才对, 而不是在 kv.list 前加 await

1
2
3
4
const test = kv.list({ prefix: [params.hostname, 'visitors'] })
for await (const key of test) {
console.log(key)
}

环境变量

Deno.env

方法说明
Deno.env.set(key, value)设置环境变量
Deno.env.get(key)获取环境变量
Deno.env.has(key)判断环境变量是否存在
Deno.env.delete(key)删除环境变量
Deno.env.toObject()获取所有环境变量

Deno Deploy 上的环境变量可以在 Deno.env 中获取

Deno.args

Deno.args 是一个数组, 包含了所有传递给 Deno 运行时的命令行参数

1
2
// deno run xxx.js arg1 arg2
console.log(Deno.args) // ['arg1', 'arg2']

.env 文件

Deno 支持读取 .env 文件, 但不支持直接设置环境变量, 需要手动设置

1
2
# .env
KEY=VALUE
1
2
3
4
// 读取 .env 文件
import { load } from "https://deno.land/[email protected]/dotenv/mod.ts"
const env = await load()
const value = env.KEY

🚧FFI

Foreing Function Interface

Deno 提供了 FFI, 可以调用 C/C++Rust 等语言的函数, 详见官方文档

⭐Bun

Bun 也是一个 JavaScriptTypeScript 运行时, 基于 JavaScriptCore 而不是 V8; 相比于 Deno, Bun 更注重于 Node.js 的兼容性, 可以无痛迁移, 也可以作为类似 npm 的包管理器使用

Bun 的内置 API 都挂载于全局对象 Bun 上, 与 DenoDeno 对象类似; 并且也支持很多 WebAPI

1
2
3
4
5
6
7
8
# 安装
powershell -c "irm bun.sh/install.ps1|iex"
# 验证
bun --version
# 升级
bun upgrade
# 卸载
powershell -c ~\.bun\uninstall.ps1

如果是中文用户名, 安装全局包可能会出现 Access Denied, 这是因为中文用户名处理时出现了乱码, 这里有个邪门的解决办法:

  1. 管理员权限打开 PowerShell
  2. 运行全局安装命令, 如 bun add -g cowsay
  3. 复制出现的乱码路径, 如 C:\Users\锟斤拷(乱码)
  4. 删除乱码路径
  5. 管理员权限打开 cmd
  6. 运行 mklink /j C:\Users\锟斤拷(乱码) C:\Users\正确用户名 创建乱码用户名到正确用户名的链接

此方法也适用于其他出现中文乱码的情况

Runtime

bunnode
bun [--watch] [run] x.js/ts/jsx/tsxnode xxx.js
bun [run] xxxnpm run xxx
bun run --bun xxx强制使用 Bun 运行脚本
忽略 #!/usr/bin/env node 声明
bun initpnpm init
bun create xxxpnpm create xxx
  • 内置命令和 package.json 冲突时, 优先使用内置命令; 此时应使用 bun run xxx 而不是 bun xxx
  • 通过 bun add -d @types/bun 可以安装 Bun 的类型声明文件

.env

Bun 会自动按顺序读取以下环境变量文件 (无需 dotenv), 并挂载到 process.env 上 (但也可以通过 Bun.envimport.meta.env 获取)

  1. 通过命令行传递: KEY=VALUE bun run dev
  2. 手动指定文件: bun --env-file=xxx run dev
  3. .env.local
  4. .env.production / .env.development / .env.test (根据 NODE_ENV 环境变量)
  5. .env
1
2
3
# .env
NAME=xiaoyezi
AGE=18
1
2
3
// 读取环境变量
console.log(process.env.NAME) // xiaoyezi
console.log(typeof process.env.AGE) // string

🚧Bun.serve

import.meta

属性说明
import.meta.dirname
import.meta.dir
当前模块目录的绝对路径
import.meta.filename
import.meta.path
当前模块的绝对路径
import.meta.file当前模块的文件名, 如 xxx.ts
import.meta.main是否为主模块, 类似于 Python__name__ == '__main__'
import.meta.urlfile:// 开头的当前模块的绝对路径

Shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 引入 Shell 模块
import { $ } from 'bun'

// 执行命令
const { stdout: Buffer, stderr: Buffer } = await $`echo "Hello, World!"`

// 阻止输出
await $`echo "Hello, World!"`.quiet()

// 获取输出
const output: string = await $`echo "Hello, World!"`.text()
const output: string = await $`echo "Hello, World!"`.json()
for await (const line of $`echo "Hello, World!"`.lines()) {
console.log(line)
}
const output: Blob = await $`echo "Hello, World!"`.blob()

// 改变工作目录
await $.cwd('/xxx')
await $`echo "Hello, World!"`.cwd('../')

// 覆盖环境变量
await $`echo $NAME`.env({ ...import.meta.env, NAME: 'xiaoyezi' }) // xiaoyezi

🚧Redirect

1
2
3
4
5
6
7
8
9
10
import { $ } from 'bun'

// 输出到 TypedArray
const u8a = new Uint8Array()
await $`echo "Hello, World!" > ${u8a}`
// 输出到文件
const file = Bun.file('xxx.txt')
await $`echo "Hello, World!" > ${file}`

// 其他 ...

Package Manager

bunpnpm
bun install/ipnpm i
bun updatepnpm up
bun add/a xxx
bun add/a --dev/-d/-D xxx
bun add/a --global/-g xxx
pnpm add xxx
pnpm add -D xxx
pnpm add -g xxx
bun remove/rm xxxpnpm rm xxx
bun pm ls [-g]pnpm list [-g]
bunx [--bun] xxx
bun x [--bun] xxx
pnpx xxx
  • --bun 会强制使用 Bun 运行脚本, 忽略 #!/usr/bin/env node 声明
  • 如果没有 node_modules 目录, Bun 会像 Deno 一样自动安装依赖 (到全局缓存)

🚧Bundler

  • 标题: Deno/Bun学习笔记
  • 作者: 小叶子
  • 创建于 : 2024-03-16 21:03:49
  • 更新于 : 2024-05-20 15:13:42
  • 链接: https://blog.leafyee.xyz/2024/03/16/Deno和Bun学习笔记/
  • 版权声明: 版权所有 © 小叶子,禁止转载。
评论