QuickJS 嵌入式 JavaScript 引擎完全教程 / 05 - 模块系统
模块系统
5.1 ES Module 基础
QuickJS 完整实现了 ECMAScript Module(ESM)规范,支持 import / export 语法。
模块与脚本的区别
| 特性 | 脚本模式 | 模块模式 |
|---|---|---|
| 作用域 | 全局 | 模块私有 |
this | 全局对象 | undefined |
import/export | ❌ | ✅ |
顶层 await | ❌ | ✅ |
| 严格模式 | 需手动声明 | 默认 |
| 声明提升 | 函数提升 | 无提升 |
| 重复声明 | 允许(var) | 报错 |
5.2 导入导出语法
命名导出 (Named Export)
// utils.js — 多种命名导出方式
// 方式 1:声明时导出
export function formatDate(date) {
return date.toISOString().split('T')[0];
}
export const MAX_SIZE = 1024 * 1024;
export class EventEmitter {
#listeners = {};
on(event, fn) {
(this.#listeners[event] ??= []).push(fn);
}
emit(event, ...args) {
(this.#listeners[event] ?? []).forEach(fn => fn(...args));
}
}
// 方式 2:末尾统一导出
function helperA() { return 'a'; }
function helperB() { return 'b'; }
const SECRET = 'abc123';
export { helperA, helperB, SECRET };
// 方式 3:重命名导出
export { helperA as a, helperB as b };
默认导出 (Default Export)
// logger.js — 默认导出
export default class Logger {
#prefix;
constructor(prefix = 'LOG') {
this.#prefix = prefix;
}
info(msg) { console.log(`[${this.#prefix}] INFO: ${msg}`); }
warn(msg) { console.warn(`[${this.#prefix}] WARN: ${msg}`); }
error(msg) { console.error(`[${this.#prefix}] ERROR: ${msg}`); }
}
导入方式
// app.js — 各种导入方式
import Logger from "./logger.js"; // 默认导入
import { formatDate, MAX_SIZE } from "./utils.js"; // 命名导入
import { helperA as a } from "./utils.js"; // 别名导入
import * as utils from "./utils.js"; // 命名空间导入
import Logger2, { EventEmitter } from "./utils.js"; // 混合导入
// 动态导入(第 5.6 节详述)
const mod = await import("./dynamic.js");
重导出 (Re-export)
// index.js — 模块聚合
export { default as Logger } from "./logger.js";
export { formatDate, MAX_SIZE } from "./utils.js";
export * from "./utils.js"; // 重导出所有(不含 default)
export * as utils from "./utils.js"; // 作为命名空间重导出
5.3 循环依赖处理
ES Module 规范要求支持循环依赖。QuickJS 通过模块状态机正确处理。
模块加载状态
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Unlinked │ ──→ │ Linking │ ──→ │ Linked │ ──→ │ Evaluated│
└──────────┘ └──────────┘ └──────────┘ └──────────┘
│ ↑
│ ┌──────────┐ │
└────────→ │ Evaluating│ ──────────┘
└──────────┘
| 状态 | 说明 |
|---|---|
| Unlinked | 未链接,模块代码未加载 |
| Linking | 正在解析导入导出绑定 |
| Linked | 绑定已建立,但代码未执行 |
| Evaluating | 正在执行模块代码 |
| Evaluated | 执行完成,可以使用 |
循环依赖示例
// a.js
import { bFunc } from "./b.js";
export function aFunc() {
return "a:" + bFunc();
}
console.log("Module a loaded");
// b.js
import { aFunc } from "./a.js";
export function bFunc() {
return "b";
}
console.log("Module b loaded, aFunc():", aFunc()); // 可能报错或得到未完成的绑定
注意: QuickJS 使用惰性绑定解析,在循环依赖中,如果在模块顶层访问尚未执行完毕的导入绑定,可能得到
undefined或触发错误。最佳实践是避免在模块顶层执行相互调用。
5.4 字节码模块
QuickJS 支持将模块预编译为字节码(bytecode),提高加载速度。
使用 qjsc 编译模块
# 编译单个模块为字节码
./qjsc -o mymodule.qjsc -m mymodule.js
# 编译整个应用(包含所有依赖模块)
./qjsc -o app.qjsc -m app.js
# 生成 C 嵌入文件
./qjsc -c -o bytecode.h \
-m module_a.js \
-m module_b.js \
-m app.js
# 指定模块名
./qjsc -c -o bytecode.h -M mylib -m ./lib/mylib.js
在 C 中加载字节码模块
// bytecode_loader.c
#include "quickjs-libc.h"
#include <stdio.h>
// 假设 qjsc 已生成如下字节数组(在 bytecode.h 中)
// extern const uint8_t mymodule_bytecode[];
// extern const uint32_t mymodule_bytecode_size;
static JSValue js_load_bytecode_module(JSContext *ctx,
const char *module_name) {
// 根据模块名查找对应的字节码
const uint8_t *buf;
uint32_t buf_len;
if (strcmp(module_name, "mymodule") == 0) {
buf = mymodule_bytecode;
buf_len = mymodule_bytecode_size;
} else {
return JS_ThrowTypeError(ctx, "Unknown module: %s", module_name);
}
// 加载字节码
JSValue obj = JS_ReadObject(ctx, buf, buf_len, JS_READ_OBJ_BYTECODE);
if (JS_IsException(obj)) return obj;
// 编译为模块
JSValue result = JS_EvalFunction(ctx, obj);
return result;
}
5.5 自定义模块加载器
QuickJS 允许你完全控制模块的加载过程,这对嵌入式环境特别有用。
模块加载函数
// custom_loader.c — 自定义模块加载器
#include "quickjs-libc.h"
#include <stdio.h>
#include <string.h>
// 模块源码存储结构
typedef struct {
const char *name;
const char *source;
} ModuleEntry;
// 预定义的模块表(编译时嵌入)
static ModuleEntry builtin_modules[] = {
{ "math", "export function add(a,b){return a+b} export const PI=3.14;" },
{ "greet", "export default function(name){return `Hello, ${name}!`}" },
{ NULL, NULL }
};
// 查找模块源码
static const char* find_module_source(const char *name) {
for (ModuleEntry *e = builtin_modules; e->name; e++) {
if (strcmp(e->name, name) == 0) return e->source;
}
return NULL;
}
// 模块查找回调
static JSModuleDef* custom_module_loader(JSContext *ctx,
const char *module_name,
void *opaque) {
// 先尝试从内存表查找
const char *source = find_module_source(module_name);
if (source) {
JSValue func_val = JS_Eval(ctx, source, strlen(source),
module_name, JS_EVAL_TYPE_MODULE);
if (JS_IsException(func_val)) {
// 返回 NULL 表示加载失败
return NULL;
}
JSModuleDef *m = JS_VALUE_GET_PTR(func_val);
JS_FreeValue(ctx, func_val);
return m;
}
// 回退到默认文件加载
return NULL; // 让 QuickJS 尝试默认加载
}
// 注册自定义加载器
void setup_custom_loader(JSContext *ctx) {
JS_SetModuleLoaderFunc(JS_GetRuntime(ctx),
NULL, // normalize 回调
custom_module_loader,
NULL); // opaque 数据
}
int main() {
JSRuntime *rt = JS_NewRuntime();
JSContext *ctx = JS_NewContext(rt);
setup_custom_loader(ctx);
const char *code = R"(
import { add, PI } from "math";
import greet from "greet";
console.log("2 + 3 =", add(2, 3));
console.log("PI =", PI);
console.log(greet("QuickJS"));
)";
JSValue result = JS_Eval(ctx, code, strlen(code), "<main>",
JS_EVAL_TYPE_MODULE);
if (JS_IsException(result)) {
JSValue ex = JS_GetException(ctx);
const char *msg = JS_ToCString(ctx, ex);
fprintf(stderr, "Error: %s\n", msg);
JS_FreeCString(ctx, msg);
JS_FreeValue(ctx, ex);
}
JS_FreeValue(ctx, result);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
return 0;
}
模块名称规范化
// module_normalize.c — 模块路径规范化
#include "quickjs-libc.h"
#include <string.h>
#include <stdlib.h>
#include <libgen.h>
static char* normalize_path(const char *base, const char *name) {
// 简化实现:相对于基础模块的路径解析
if (name[0] == '.') {
char *base_copy = strdup(base);
char *dir = dirname(base_copy);
char *result = malloc(strlen(dir) + strlen(name) + 2);
sprintf(result, "%s/%s", dir, name);
free(base_copy);
return result;
}
return strdup(name);
}
static JSValue module_normalize(JSContext *ctx,
const char *module_name,
const char *base_name,
void *opaque) {
char *normalized = normalize_path(base_name, module_name);
JSValue result = JS_NewString(ctx, normalized);
free(normalized);
return result;
}
// 注册时传入 normalize 回调
JS_SetModuleLoaderFunc(JS_GetRuntime(ctx),
module_normalize, // normalize
custom_loader, // load
NULL);
5.6 动态导入 (Dynamic Import)
动态导入允许在运行时按需加载模块,返回 Promise。
// dynamic_import.js
async function loadModule(name) {
try {
const module = await import(name);
console.log("Module loaded:", Object.keys(module));
return module;
} catch (e) {
console.error("Failed to load:", name, e.message);
return null;
}
}
// 条件加载
const module = await loadModule("./optional_feature.js");
if (module) {
module.doSomething();
} else {
console.log("Feature not available");
}
注意: QuickJS 的动态
import()在qjs命令行工具中工作正常,但在 C 嵌入环境中需要确保模块加载器回调已正确注册。
5.7 模块与字节码混合工作流
开发环境:源码模块
project/
├── src/
│ ├── app.js ← 主入口
│ ├── utils.js ← 工具模块
│ └── config.js ← 配置模块
├── lib/
│ ├── db.js ← 第三方库
│ └── cache.js
└── Makefile
生产环境:字节码部署
# Makefile — 编译为字节码
QJS=qjs
QJSC=qjsc
all: app.qjsc
# 编译所有模块为字节码
app.qjsc: src/app.js src/utils.js src/config.js
$(QJSC) -o $@ -m $^
# 生成 C 嵌入头文件
bytecode.h: src/app.js src/utils.js src/config.js
$(QJSC) -c -o $@ -m $^
clean:
rm -f app.qjsc bytecode.h
5.8 模块系统常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
SyntaxError: import not allowed | 以脚本模式运行 | 使用 --module 标志 |
Cannot find module | 路径不正确 | 检查相对路径,使用 ./ 前缀 |
TypeError: not a function | 模块未正确导出 | 检查 export default vs 命名导出 |
循环依赖中的 undefined | 模块执行顺序问题 | 避免顶层互相调用 |
| 字节码版本不匹配 | QuickJS 版本不同 | 使用相同版本编译和执行 |
5.9 本章小结
| 要点 | 说明 |
|---|---|
| ES Module | QuickJS 完整支持 ESM 语法 |
| 命名导出/默认导出 | 支持所有标准导出方式 |
| 字节码模块 | 使用 qjsc 预编译模块提高加载速度 |
| 自定义加载器 | 使用 JS_SetModuleLoaderFunc 完全控制模块加载 |
| 动态导入 | 使用 import() 按需加载模块 |
| 循环依赖 | QuickJS 正确处理,但应避免顶层互相调用 |