游戏开发的Typescript(2)禁忌函数 - 类 eval()函数

简介

在 TypeScript 里,eval() 是个内置函数,其作用是对字符串形式的 JavaScript 代码进行解析并执行。setTimeout(),setInterval(),new Function()三个函数的第一个参数可以传入string类型,所以会存在同样的问题。我自己平时把这几个函数叫类eval()函数,但这是个不规范的说法。

文章后半部分沙箱使用的简介,如果纯ts手搓游戏需要注意,对cocos laya egret这类引擎的使用者话相对超纲,只是参考,看看就可以,不必在意。

类eval()用法

eval() 函数会把传入的字符串当作 JavaScript 代码来执行,并且能够访问和修改当前的作用域。

// 执行简单的表达式
const result = eval('2 + 3');
console.log(result); // 输出 5

// 访问和修改变量
let x = 10;
eval('x = x + 5');
console.log(x); // 输出 15

// 执行函数定义
eval('function greet() { return "Hello!"; }');
console.log(greet()); // 输出 "Hello!"

存在的风险

尽管 eval() 功能强大,但它也存在诸多风险,因此在实际开发中要谨慎使用。

安全隐患

如果 eval() 处理的是用户输入或者不可信来源的字符串,那么攻击者就有可能通过注入恶意代码来执行任意操作,比如窃取数据、修改系统配置等。

const userInput = 'console.log("You are hacked!");';
eval(userInput); // 会执行用户输入的代码
性能问题

eval() 在运行时需要动态解析和编译代码,这会比直接执行预编译的代码慢很多。

调试困难

由于 eval() 执行的是动态生成的代码,所以在调试时很难进行断点设置和错误追踪。

类型安全丧失

在 TypeScript 中使用 eval() 会让代码失去类型检查的优势,因为 TypeScript 无法对动态执行的代码进行类型分析。

替代方案

在大多数情况下,不建议使用 eval()。可以考虑采用以下替代方法:

1. 避免使用动态代码

若需求只是进行简单的配置或者计算,可使用 JSON 格式或者特定的数据结构来替代代码字符串。

// 使用JSON代替代码字符串
const config = JSON.parse('{ "name": "John", "age": 30 }');
console.log(config.name); // 输出 "John"
2. 使用函数封装

把动态代码封装到函数中,这样既能实现所需功能,又能避免安全风险。

// 定义一个函数来处理特定的计算
function calculate(expression: string) {
  const operations = {
    add: (a: number, b: number) => a + b,
    subtract: (a: number, b: number) => a - b,
    // 其他操作...
  };

  // 解析表达式并调用对应的函数
  // 这里可以实现更复杂的解析逻辑
  return operations.add(2, 3);
}

const result = calculate('add');
console.log(result); // 输出 5
3. 使用安全的解析器

如果确实需要解析表达式,可以使用专门的安全解析器,像 Function 构造函数或者第三方库。

// 使用 Function 构造函数
const add = new Function('a', 'b', 'return a + b');
console.log(add(2, 3)); // 输出 5

// 使用第三方库(如 mathjs)
import { evaluate } from 'mathjs';
const result = evaluate('2 + 3');
console.log(result); // 输出 5
4. 类型安全的动态执行

若一定要执行动态代码,可以结合类型断言来恢复类型安全。

function safeEval(code: string): T {
  // 这里可以添加安全检查
  return eval(code) as T;
}

// 指定返回类型
const result: number = safeEval('2 + 3');
console.log(result); // 输出 5

安全使用建议

游戏开发中,需要注意游戏本体不要使用eval函数。编辑器代码中若实在无法避免使用 eval(),为了养成好习惯,请遵循以下安全准则:

  1. 不要对用户输入或者不可信来源的字符串使用 eval()
  2. 对输入进行严格验证和过滤,只允许特定格式的表达式。
  3. 尽量缩小 eval() 的作用域,避免修改全局变量。
  4. 考虑使用 Workers 在沙箱环境中执行代码。

原生ts的沙箱隔离

如果一定要使用类似eval这种可以执行string的函数 ,使用沙箱环境执行代码(如插件、模组或用户自定义逻辑)能有效隔离风险。

游戏开发中的沙箱需求

在游戏开发中,沙箱主要用于:

  • 安全执行模组 / 插件代码:防止恶意或错误代码破坏游戏核心逻辑
  • 性能隔离:限制脚本执行时间和资源消耗
  • 简化调试:将错误限制在独立环境中

基于 Workers 的游戏沙箱实现(不推荐使用,只是用于逻辑参考)

sandbox-worker.ts

// sandbox-worker.ts
/// 

// 游戏对象接口
interface GameObject {
  id: string;
  type: string;
  position: { x: number; y: number };
  properties: Record;
}

// 消息类型
type Message = 
  | { type: 'loadScript', data: string }
  | { type: 'registerGameObject', data: GameObject }
  | { type: 'updateGameState', data: { objects: GameObject[] } }
  | { id: number, type: string, data: any, config: any };

// 沙箱上下文
interface SandboxContext {
  game: {
    objects: Record;
    triggerEvent: (event: any) => void;
  };
  console: {
    log: (...args: any[]) => void;
    error: (...args: any[]) => void;
  };
  Math: typeof Math;
  Date: typeof Date;
  setTimeout: typeof setTimeout;
  clearTimeout: typeof clearTimeout;
}

const worker: Worker = self as any;
let gameState: Record = {};
let scriptContext: any = null;

// 事件触发器
function triggerEvent(event: any) {
  worker.postMessage({ event });
}

// 创建沙箱上下文
function createContext(): SandboxContext {
  return {
    game: {
      objects: gameState,
      triggerEvent
    },
    console: {
      log: (...args: any[]) => worker.postMessage({ type: 'log', data: args }),
      error: (...args: any[]) => worker.postMessage({ type: 'error', data: args })
    },
    Math,
    Date,
    setTimeout,
    clearTimeout
  };
}

// 安全执行代码
function executeInSandbox(code: string, context: SandboxContext) {
  try {
    // 创建安全的函数包装器
    const wrapper = new Function(
      'context', 
      `with(context) { ${code} }`
    );
    
    // 执行代码
    return wrapper(context);
  } catch (error) {
    throw new Error(`沙箱执行错误: ${error instanceof Error ? error.message : String(error)}`);
  }
}

worker.onmessage = function(e: MessageEvent) {
  const { id, type, data, config } = e.data;
  
  try {
    switch (type) {
      case 'loadScript':
        // 加载并执行脚本
        const context = createContext();
        scriptContext = executeInSandbox(data, context);
        sendResponse(id, true);
        break;
      
      case 'registerGameObject':
        // 注册游戏对象
        gameState[data.id] = data;
        sendResponse(id, true);
        break;
      
      case 'updateGameState':
        // 更新游戏状态
        data.objects.forEach((obj: GameObject) => {
          gameState[obj.id] = obj;
        });
        
        // 如果有脚本,执行游戏更新逻辑
        if (scriptContext && typeof scriptContext.update === 'function') {
          const result = scriptContext.update({
            objects: gameState,
            deltaTime: 16 // 假设每帧16ms
          });
          sendResponse(id, result);
        } else {
          sendResponse(id, true);
        }
        break;
      
      default:
        sendResponse(id, new Error(`未知命令: ${type}`));
    }
  } catch (error) {
    sendResponse(id, error instanceof Error ? error.message : String(error));
  }
};

// 发送响应
function sendResponse(id: number, result: any) {
  if (id !== undefined) {
    worker.postMessage({ id, result: result instanceof Error ? null : result, error: result instanceof Error ? result.message : null });
  }
}    

game-sandbox.ts

// game-sandbox.ts
import { Subject, Observable } from 'rxjs';

// 游戏对象接口
interface GameObject {
  id: string;
  type: string;
  position: { x: number; y: number };
  properties: Record;
}

// 沙箱事件类型
type GameEvent = 
  | { type: 'move', objectId: string, position: { x: number, y: number } }
  | { type: 'collision', objectId: string, with: string }
  | { type: 'custom', name: string, data: any };

// 沙箱配置
interface SandboxConfig {
  maxExecutionTime?: number; // 最大执行时间(ms)
  memoryLimit?: number; // 内存限制(MB)
  allowedAPIs?: string[]; // 允许访问的API列表
}

export class GameSandbox {
  private worker: Worker;
  private eventSubject = new Subject();
  private messageId = 0;
  private callbacks = new Map void>();

  constructor(workerUrl: string, private config: SandboxConfig = {}) {
    this.worker = new Worker(workerUrl);
    
    this.worker.onmessage = (e: MessageEvent<{ 
      id?: number; 
      event?: GameEvent; 
      result?: any; 
      error?: string;
    }>) => {
      if (e.data.id !== undefined) {
        // 处理请求响应
        const callback = this.callbacks.get(e.data.id);
        if (callback) {
          callback(e.data.error ? new Error(e.data.error) : e.data.result);
          this.callbacks.delete(e.data.id);
        }
      } else if (e.data.event) {
        // 处理游戏事件
        this.eventSubject.next(e.data.event);
      }
    };
    
    this.worker.onerror = (error) => {
      console.error('沙箱错误:', error.message);
    };
  }

  // 加载游戏脚本
  loadScript(script: string): Promise {
    return this.sendMessage('loadScript', script);
  }

  // 注册游戏对象
  registerGameObject(object: GameObject): Promise {
    return this.sendMessage('registerGameObject', object);
  }

  // 更新游戏状态
  updateGameState(state: { objects: GameObject[] }): Promise {
    return this.sendMessage('updateGameState', state);
  }

  // 监听沙箱事件
  onEvent(): Observable {
    return this.eventSubject.asObservable();
  }

  // 发送消息到沙箱
  private sendMessage(type: string, data: any): Promise {
    return new Promise((resolve, reject) => {
      const id = this.messageId++;
      this.callbacks.set(id, (result) => {
        if (result instanceof Error) {
          reject(result);
        } else {
          resolve(result);
        }
      });
      
      this.worker.postMessage({ id, type, data, config: this.config });
    });
  }

  // 销毁沙箱
  destroy() {
    this.worker.terminate();
    this.eventSubject.complete();
  }
}    

game-engine.ts

// game-engine.ts
import { GameSandbox } from './game-sandbox';

// 游戏引擎核心
export class GameEngine {
  private sandbox: GameSandbox;
  private gameObjects: Record = {};
  private lastTime = 0;

  constructor() {
    this.sandbox = new GameSandbox(new URL('./sandbox-worker.ts', import.meta.url));
    
    // 监听沙箱事件
    this.sandbox.onEvent().subscribe(event => {
      this.handleGameEvent(event);
    });
  }

  // 加载游戏模组
  async loadModule(script: string) {
    await this.sandbox.loadScript(script);
  }

  // 添加游戏对象
  addGameObject(object: GameObject) {
    this.gameObjects[object.id] = object;
    this.sandbox.registerGameObject(object);
  }

  // 游戏主循环
  start() {
    this.lastTime = performance.now();
    requestAnimationFrame(this.gameLoop.bind(this));
  }

  // 游戏循环
  private gameLoop(timestamp: number) {
    const deltaTime = timestamp - this.lastTime;
    this.lastTime = timestamp;
    
    // 更新游戏状态
    this.sandbox.updateGameState({ objects: Object.values(this.gameObjects) });
    
    // 渲染游戏
    this.render();
    
    // 继续循环
    requestAnimationFrame(this.gameLoop.bind(this));
  }

  // 渲染游戏
  private render() {
    // 游戏渲染逻辑...
  }

  // 处理游戏事件
  private handleGameEvent(event: GameEvent) {
    switch (event.type) {
      case 'move':
        const obj = this.gameObjects[event.objectId];
        if (obj) {
          obj.position = event.position;
        }
        break;
      
      case 'collision':
        // 处理碰撞事件
        console.log(`碰撞事件: ${event.objectId} 与 ${event.with}`);
        break;
      
      case 'custom':
        // 处理自定义事件
        console.log(`自定义事件: ${event.name}`, event.data);
        break;
    }
  }

  // 销毁游戏引擎
  destroy() {
    this.sandbox.destroy();
  }
}

// 示例游戏对象接口
interface GameObject {
  id: string;
  type: string;
  position: { x: number; y: number };
  properties: Record;
}    

game-example.ts

// game-script-example.ts - 此代码在沙箱内运行
// 游戏初始化
function init() {
  console.log('游戏模组初始化完成');
  
  // 创建一个玩家对象
  game.triggerEvent({
    type: 'custom',
    name: 'createPlayer',
    data: { name: 'SandboxPlayer', x: 100, y: 100 }
  });
}

// 游戏更新
function update(state: { objects: Record, deltaTime: number }) {
  // 更新所有敌人位置
  Object.values(state.objects).forEach((obj: any) => {
    if (obj.type === 'enemy') {
      // 移动敌人
      obj.position.x += Math.sin(state.deltaTime / 1000) * 2;
      obj.position.y += Math.cos(state.deltaTime / 1000) * 2;
      
      // 触发移动事件
      game.triggerEvent({
        type: 'move',
        objectId: obj.id,
        position: obj.position
      });
    }
  });
  
  // 检测碰撞
  checkCollisions(state.objects);
}

// 碰撞检测
function checkCollisions(objects: Record) {
  const player = objects['player'];
  if (!player) return;
  
  Object.values(objects).forEach((obj: any) => {
    if (obj.type === 'enemy') {
      const dx = player.position.x - obj.position.x;
      const dy = player.position.y - obj.position.y;
      const distance = Math.sqrt(dx * dx + dy * dy);
      
      if (distance < 30) {
        // 触发碰撞事件
        game.triggerEvent({
          type: 'collision',
          objectId: player.id,
          with: obj.id
        });
      }
    }
  });
}

// 导出接口
export default {
  init,
  update
};    

游戏沙箱的关键特性

游戏对象隔离

  • 沙箱内代码只能访问授权的游戏对象
  • 通过事件系统与主游戏引擎通信

资源限制

  • 执行超时控制(防止无限循环)
  • 限制危险 API 访问(如 fetch、localStorage)
  • 内存使用监控(可扩展实现)

事件驱动架构

  • 沙箱通过触发事件影响游戏状态
  • 主引擎通过状态更新通知沙箱

类型安全

  • 使用 TypeScript 接口定义游戏对象和事件
  • 明确的消息传递协议

使用方法

初始化游戏引擎

const engine = new GameEngine();

加载模组

// 从服务器或用户上传加载脚本
const moduleCode = await fetchModuleCode();
engine.loadModule(moduleCode);

添加游戏对象

engine.addGameObject({
  id: 'player',
  type: 'player',
  position: { x: 100, y: 100 },
  properties: { health: 100, speed: 5 }
});

启动游戏循环

engine.start();

模组脚本示例

// 沙箱内执行的代码
function update(state) {
  // 移动所有敌人
  state.objects.forEach(enemy => {
    enemy.position.x += 1;
    game.triggerEvent({
      type: 'move',
      objectId: enemy.id,
      position: enemy.position
    });
  });
}

增强游戏沙箱的安全性

进一步限制 API 访问

// 在worker中禁用更多API
delete self.WebSocket;
delete self.indexedDB;
// ...其他需要禁用的API

实现内存监控

// 在worker中定期检查内存使用
setInterval(() => {
  const memory = performance.memory;
  if (memory.usedJSHeapSize > MAX_MEMORY) {
    throw new Error('内存使用超出限制');
  }
}, 1000);

更严格的代码验证

// 使用acorn等解析器验证代码语法
import { parse } from 'acorn';

function validateCode(code: string) {
  try {
    parse(code, { ecmaVersion: 2020 });
    return true;
  } catch (error) {
    return false;
  }
}

使用 WebAssembly 沙箱

// 对于高性能需求,考虑使用WebAssembly
WebAssembly.instantiateStreaming(fetch('module.wasm'), {
  // 定义沙箱环境
});

这种沙箱架构适合以下场景:(提醒,使用沙箱一定要小心)

  • 多人在线游戏的插件系统
  • 游戏编辑器中的脚本系统
  • 教育类游戏的学生代码执行环境
  • 游戏 modding 平台

根据具体需求,可以进一步扩展沙箱功能,如添加物理引擎集成、网络通信限制或更精细的权限控制。

总结

总之,eval() 是一把双刃剑,虽然功能强大,但也伴随着很高的风险。在实际开发中,应优先考虑其他更安全、更高效的替代方案。尽量减少类似的使用,其它的类eval函数使用时也一定要注意风险。

你可能感兴趣的:(#,typescript,typescript,javascript)