强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 ModuleQuickJS 完整支持 ESM 语法
命名导出/默认导出支持所有标准导出方式
字节码模块使用 qjsc 预编译模块提高加载速度
自定义加载器使用 JS_SetModuleLoaderFunc 完全控制模块加载
动态导入使用 import() 按需加载模块
循环依赖QuickJS 正确处理,但应避免顶层互相调用

扩展阅读