如果将 Spring Boot 的依赖注入(IOC)和面向切面编程(AOP)在前端实现,会碰撞出怎样有趣的火花?

前言

在现代开发中,依赖注入(IOC)和面向切面编程(AOP)是构建模块化、可维护代码的重要设计理念。IOC 通过将对象的依赖关系管理交由容器处理,减少了组件之间的耦合度,使代码更具扩展性;而 AOP 则以灵活的方式在不改变核心逻辑的前提下,实现如日志、事务等通用功能的横切关注点,提升了代码的可维护性和可复用性。

随着前端工程的日益复杂,将这些思想应用于前端开发,能为前端代码带来更强的模块化和解耦优势。博主尝试探索如何在前端实现这两大思想,期待为前端开发带来新的思路与启发。

在此,致谢所有为开源事业默默奉献的开发者们,感谢你们为技术社区带来的创新与推动力。

使用

创建容器

创建一个依赖注入容器 Container.js

class Container {
    constructor() {
        this.services = {};
    }

    register(name, definition, dependencies) {
        this.services[name] = { definition, dependencies, instance: null };
    }

    get(name) {
        const service = this.services[name];
        if (!service) {
            throw new Error(`Service '${name}' not found`);
        }

        // 如果实例已经存在,直接返回
        if (service.instance) {
            return service.instance;
        }

        // 解析依赖并实例化服务
        service.instance = service.definition(
            ...(service.dependencies || []).map(dep => this.get(dep))
        );
        return service.instance;
    }
}

module.exports = Container;
  • 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.
  • 26.
  • 27.
  • 28.
  • 29.
创建切面

创建一个面向切换 Aspect.js

class Aspect {
    constructor(target, aspect) {
        return new Proxy(target, {
            get(obj, prop) {
                if (typeof obj[prop] === 'function') {
                    return (...args) => {
                        aspect.before && aspect.before(prop, args);
                        const result = obj[prop](...args);
                        aspect.after && aspect.after(prop, args, result);
                        return result; // 确保返回原始方法的执行结果
                    };
                }
                return obj[prop];
            }
        });
    }
}

module.exports = Aspect;
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
实现
const Container = require('./Container');
const Aspect = require('./Aspect');

// 3. 创建容器并注册服务
const container = new Container();

// 注册一个日志服务
container.register('logger', () => {
    return {
        log: (message) => console.log(`[Logger]: ${message}`)
    };
});

// 注册一个用户服务,依赖于日志服务
container.register('userService', (logger) => {
    return {
        getUser: () => {
            logger.log('执行中。。。');
            return { name: 'Alice', age: 25 };
        }
    };
}, ['logger']);

// 4. 应用 AOP 到 userService
const userService = container.get('userService');
const userServiceWithAspect = new Aspect(userService, {
    before(method, args) {
        console.log(`执行前:`, args,method);
    },
    after(method, args, result) {
        result.age = 19; // 更改年龄
        console.log(`执行后`,args,method,result);
    }
});

let user = userServiceWithAspect.getUser();

console.log(user);
  • 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.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.

输出:

如果将 Spring Boot 的依赖注入(IOC)和面向切面编程(AOP)在前端实现,会碰撞出怎样有趣的火花?_AOP

为什么?

执行前打印的是一个空的数组[]

因为在调用userServiceWithAspect.getUser();方法的时候,未传入任何数据进去,而是在userService注册的时候,返回了该对象

同时返回的年龄为 age = 25

after 执行之后的,可以对age进行更改,返回19

从而实现了一个环绕切面实现

表单案例
const Container = require('./Container');
const Aspect = require('./Aspect');

// 3. 创建容器并注册服务
const container = new Container();


container.register('validationService', () => {
    return {
        validate: (formData) => {
            // 假设所有字段必须非空
            console.log("数据=====执行中====",formData)
            for (const key in formData) {
                if (!formData[key]) {
                    throw new Error(`Field '${key}' cannot be empty`);
                }
            }
            return true; // 添加返回值
        }
    };
});

const validationService = container.get('validationService');
const validationServiceWithAspect = new Aspect(validationService, {
    before(method, args) {
        console.log(`执行前:`, args,method);
    },
    after(method, args,result) {
        console.log(`执行后`,args,method,result);
    }
});

// 表单提交时调用验证
try {
    validationServiceWithAspect.validate({ username: 'Alice', password: '12345' });
} catch (error) {
    console.error(error.message);
}
  • 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.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.

输出:

如果将 Spring Boot 的依赖注入(IOC)和面向切面编程(AOP)在前端实现,会碰撞出怎样有趣的火花?_AOP_02

跟上面实现不同的是,为什么这里的before能打印出参数呢

因为在调用 validationServiceWithAspect.validate({ username: 'Alice', password: '12345' }); 传入了该对象,

所以执行之前能打印出来,

依次内推的思路,在before中修改我们对应想要的值

after 修改对应的值,然后最后返回

实现了一个环绕切面

api 请求示例
const Container = require('./Container');
const Aspect = require('./Aspect');
// 3. 创建容器并注册服务
const container = new Container();

// 3. 全局 API 请求管理
container.register('apiService', () => {
    return {
        request: (url, data,options) => {
            // 模拟 API 请求
            console.log(`执行中 请求url: ${url} 参数: ${JSON.stringify(data)} 方法:${JSON.stringify(options)}`);
            return { data: 'response data' };
        }
    };
});

const apiService = container.get('apiService');
const apiServiceWithAspect = new Aspect(apiService, {
    before(method, args) {
        console.log(`执行前: ${method} ${JSON.stringify(args)}`);
    },
    after(method, args, result) {
        console.log(`执行后: ${method} ${JSON.stringify(args)} ${JSON.stringify(result)}`);
    }
});

apiServiceWithAspect.request('/api/user',{ username: 'Alice', password: '12345' }, { method: 'GET' });
  • 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.
  • 26.
  • 27.

输出:

如果将 Spring Boot 的依赖注入(IOC)和面向切面编程(AOP)在前端实现,会碰撞出怎样有趣的火花?_SpringBoot_03

组件示例
const Container = require('./Container');
const Aspect = require('./Aspect');
// 3. 创建容器并注册服务
const container = new Container();

// 4. 跨组件状态共享
container.register('themeService', () => {
    let currentTheme = 'light';
    return {
        getTheme: () => currentTheme,
        setTheme: (theme) => {
            currentTheme = theme;
            console.log(`主题改为: ${theme}`);
        }
    };
});

const themeService = container.get('themeService');
const themeServiceWithAspect = new Aspect(themeService, {
    after(method, args) {
        if (method === 'setTheme') {
            console.log(`主题更新,触发UI重新渲染.  方法体:${method}  参数:${args}`);
        }
    }
});

themeServiceWithAspect.setTheme('dark');
  • 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.
  • 26.
  • 27.

输出:

如果将 Spring Boot 的依赖注入(IOC)和面向切面编程(AOP)在前端实现,会碰撞出怎样有趣的火花?_IOC_04