Skip to content

自定义 AMD 模块系统 ​

1. 为什么使用自定义的 AMD 模块系统 ​

  1. 小程序环境特殊性
    1. 隔离执行环境:小程序需要在逻辑层和渲染层间保持严格隔离,AMD 的模块化设计提供了良好的隔离机制
    2. 运行时动态加载:小程序经常需要按需加载子包内容,AMD 的异步特性天然支持这一需求
    3. 受限执行环境:小程序 WebView 环境对 ES Modules 支持有限,需要更可控的方案
  2. 兼容性
    1. 不同小程序平台对 JavaScript 模块系统支持程度不同
    2. 自定义 AMD 实现可以抹平差异,提供统一的模块化体验
  3. 性能和体积考量
    1. 轻量级实现(核心逻辑仅约 200 行)相比完整的模块加载器更适合小程序环境。
    2. 可以精确控制模块依赖解析和加载时机,优化首屏加载性能。
    3. 支持更精细的代码分割和按需加载策略。

2. AMD 模块系统实现 ​

2.1 模块定义与注册 ​

通过 define 方法将模块工厂函数注册到内部模块表中,每个模块保持未初始化状态,直到被首次请求。

ts
define(path: string | number, fun: ModuleFactory, argsLength?: number) {
  if (!this.moduleArr[path]) {
    this.moduleArr[path] = {
      status: statusDefineFlag,
      factory: fun,
      argsLength: argsLength,
    };
  }
}

2.2 懒加载与初始化机制 ​

当 require 调用时,模块才会被加载和初始化。

ts
require(path: string) {
  // ...
  if (moduleObj.status === statusDefineFlag) {
    const factoryFun = moduleObj.factory;
    // ...
    exports = factoryFun(self.getRequireFun(path), module, module.exports, ...factoryFuncArgs);
    // ...
    moduleObj.status = statusRequireFlag;
  }
  return moduleObj.exports;
}

模块仅在首次被 require 时才执行工厂函数,模块执行后将状态标记为已加载,缓存执行结果避免重复初始化。

2.3 模块依赖解析 ​

实现了完整的路径解析算法,支持相对路径、绝对路径,自动处理常见约定,如 index.js 和 .js 扩展名,提供类似 Node.js 的路径解析体验。

ts
getRequireFun(pathname: string) {
  // ...
  return function (path: string) {
    // 复杂的路径解析逻辑
    // 支持相对路径、绝对路径解析
    // 支持文件夹索引自动解析 (.js, /index.js)
    // ...
  };
}

2.4 上下文隔离 ​

通过 scope 和自定义参数实现模块上下文隔离,支持向模块注入特定的运行时变量,如 Promise 或全局 API,隔离不同模块系统,如服务层和渲染层可以有独立的模块实例

ts
constructor(id: string | any, requireParams: any[] = []) {
  if ('string' !== typeof id) throw new Error('AmdEngine `id` must be a string');
  this.moduleArr = {};
  this.scope = id;
  this.requireParams = requireParams;
  this.requireArgsFactory = undefined;
}

3. 主要作用与价值 ​

  1. 代码组织与模块化 提供统一的模块化标准,使开发者能按功能拆分代码 支持复杂的依赖关系管理,避免全局变量污染 增强代码可维护性和可复用性

  2. 性能优化 按需加载机制避免不必要的代码执行 路径缓存和模块缓存减少重复解析开销 支持子包加载与预加载策略,优化用户体验

  3. 安全增强

    • 通过模块隔离减少全局污染,防止变量冲突和命名空间污染

    • 支持注入安全沙箱环境,控制代码执行上下文

      ts
      // 创建隔离的模块执行环境
      const sandboxAmd = new AmdEngine("sandbox", [
        // 注入受限的全局对象
        {
          console: {
            log: console.log,
            error: console.error,
            // 不提供危险方法如 console.clear
          },
          // 提供受限的setTimeout,防止恶意长时间任务
          setTimeout: (callback, time) => {
            const maxTime = Math.min(time, 3000); // 限制最大延迟时间
            return setTimeout(callback, maxTime);
          },
          // 禁止使用危险API
          eval: undefined,
          Function: undefined,
        },
      ]);
      
      // 注册安全模块
      sandboxAmd.define(
        "safeModule",
        function (require, module, exports, sandbox) {
          // 模块中只能访问到注入的安全对象
          sandbox.console.log("安全执行"); // 可用
          // eval('危险代码') 会失败,因为eval被禁用
      
          exports.safeMethod = function () {
            // 安全的功能实现
          };
        }
      );
    • 错误边界限制,防止单个模块崩溃影响整个应用

      ts
      // 实现错误边界处理
      try {
        const moduleExports = amd.require("potentially-dangerous-module");
        // 使用模块导出
      } catch (error) {
        console.error("模块加载失败,应用继续运行:", error);
        // 执行降级逻辑
      }
    • 支持模块访问控制和权限管理

      ts
      // 权限控制示例
      const accessControlAmd = new AmdEngine("access-control");
      
      // 添加权限检查中间件
      accessControlAmd.use(function (modulePath, moduleFactory) {
        // 检查模块路径是否在白名单内
        if (!isAllowedModule(modulePath)) {
          throw new Error(`安全限制: 模块 ${modulePath} 不允许被加载`);
        }
        return moduleFactory;
      });

面试题: 小程序自定义 AMD 模块系统 ​

Q1: 在小程序环境中,为什么要使用自定义的 AMD 模块系统而不是 ES Modules? ​

答案:

  • 小程序 WebView 环境对 ES Modules 支持有限,尤其是在低版本设备上
  • 需要控制模块加载过程,实现按需加载和预加载策略
  • 自定义 AMD 可以实现更精细的沙箱隔离,提高安全性

Q2: 如何通过 AMD 模块系统增强小程序安全性?请举例说明。 ​

答案:

  • 通过模块隔离减少全局变量污染,每个模块拥有独立作用域
  • 实现安全沙箱注入,限制模块访问的全局 API 和对象
  • 可以禁用危险函数(如 eval、Function 构造器等)
  • 实现模块访问控制,限制敏感模块的加载权限
  • 提供错误边界处理,防止单模块崩溃影响整个应用
  • 代码示例(参考上述沙箱注入示例)

Q3: 实现一个 AMD 模块系统中的沙箱环境需要考虑哪些方面? ​

答案:

  • 全局对象控制:限制或修改模块能访问的全局对象
  • 危险 API 屏蔽:移除 eval、Function 等可执行任意代码的 API
  • 资源限制:限制 setTimeout/setInterval 最大执行时间
  • 网络请求控制:对模块内发起的网络请求进行权限验证
  • 错误捕获:实现全局错误处理,防止影响其他模块
  • 内存隔离:防止模块间的内存泄露和相互影响

Q4: 如何在 AMD 模块系统中实现模块间的权限控制? ​

答案:

  • 模块白名单/黑名单机制:预定义允许或禁止加载的模块清单
  • 中间件拦截:在模块加载前通过中间件拦截并进行权限检查
  • 路径限制:限制模块只能从特定路径加载依赖
  • 标签系统:为模块添加安全等级标签,只允许相同或更高安全级别的模块相互依赖
  • 签名验证:对关键模块进行签名,加载前验证签名确保模块未被篡改

基于 MIT 许可发布