@jsaop/jsaop

1.0.6 • Public • Published

jsaop

jsaop 是一个前端 AOP 工具。用于在面向对象编程开发模式中,对目标方法织入通知(Advice)。从而实现业务代码和功能性代码分离解耦。

版本日志 2021-09-29

  1. 解决循环依赖问题

版本日志 2021-07-13

  1. 调整属性检查会触发目标类的 get 执行问题

适用场景

  1. 面向对象开发模式,基于 typescript 或者 es6 均可
  2. 埋点、日志、异常收集等需要跟业务逻辑分离的逻辑代码

准备工作

安装

npm i --save @jsaop/jsaop

或者

yarn add --save @jsaop/jsaop

应用

开启 decorator 支持

  1. ts 文件配置 tsconfig.json,js 文件配置 jsconfig.json
{
    "compilerOptions": {
        // ...
        // 启用装饰器
        "experimentalDecorators": true
        // ...
    }
}

  1. 配置 babel

babel 需要配置 decorator 和 class 语法支持,需要用到下面两个 plugin

yarn 安装 plugin

yarn add @babel/plugin-proposal-decorators -D

npm 安装 plugin

npm i @babel/plugin-proposal-decorators -D

配置信息(.babelrc / babel.config.js / babel.config.js)

@babel/plugin-proposal-decorators 需要开启 legacy(值为 true),同时启用 setPublicClassFields、setClassMethods,以支持和使用 stage1 的 decorator 语法。

具体可以参考babel 官网相关信息

{
    "assumptions": {
        "setClassMethods": true,
        "setComputedProperties": true,
        "setPublicClassFields": true
    },
    "presets": [
        [
            "@babel/preset-env",
            {
                "modules": false
            }
        ]
    ],
    "plugins": [["@babel/plugin-proposal-decorators", { "legacy": true }]]
}

1. 基本用法

import { Aspect, Before, After, Around, Pointcut, Weaving } from '@jsaop/jsaop'

@Aspect()
class TestAspect {
    @Pointcut()
    get pointcut() {
        return 'app.tsa.pages:TestPage.do*'
    }

    @Before({ value: 'pointcut' })
    beforeAction(jp) {
        console.log('before action:', jp)
    }

    @After({ value: 'pointcut' })
    afterAction(jp, rst, err) {
        console.log('after action:', jp, rst, err)
    }
}

@Weaving({ namespace: 'app.tsa.pages' })
class TestPage {
    doSomeThing() {
        console.log('doSomeThing')
        return 'success'
    }
}

new TestPage().doSomeThing()

2. 支持异步 async/await 和 Promise

import {Aspect, Before, After, Around, Pointcut, Weaving} from '@jsaop/jsaop'

@Aspect()
class TestAspect{
    //...
}

@Weaving()
class TestPage{
    async doSomeThing(){
        console.log('doSomeThing')
        await return Promise.resolve('success')
    }
}

new TestPage().doSomeThing()

API 文档说明


切面(Aspect)

切面由切点(PointCut)和增强(Advice)组成,它既包括了横切逻辑的定义,也包括了连接点(JoinPoint)的定义,AOP 就是将切面所定义的横切逻辑织入到切面所制定的连接点中。

这是一个完整的 Aspect 定义实例,包括切点 pointcut,增强 beforeAction 和 afterAction,以及连接点匹配逻辑'app.tsa.pages:TestPage.do*'

@Aspect()
class TestAspect {
    @Pointcut()
    get pointcut() {
        return 'app.tsa.pages:TestPage.do*'
    }

    @Before({ value: 'pointcut' })
    beforeAction(jp) {
        console.log('before action:', jp)
    }

    @After({ value: 'pointcut' })
    afterAction(jp, rst, err) {
        console.log('after action:', jp, rst, err)
    }
}

Weaving(织入)

把切面织入目标位置,语法@Weaving(opts:WeavingOpts)

interface WeavingOpts {
    blackList?: Array<string> // 排除掉方法名,跳执行效率
    namespace?: string // 命名空间
}

实例

@Weaving({namespace:'app.fe.pages'})
class TestPage{
    async doSomeThing(){
        await return Promise.resolve('success')
    }
}

连接点(JoinPoint)

程序执行的某个特定位置,如某个方法调用前,调用后,方法抛出异常后,这些代码中的特定点称为连接点。通常作为通知(Advice)的参数出现

interface JoinPoint {
    target: any // 目标类
    args: any[] // 目标方法参数
    thisArg: any // this指向,目标类的实例,目标方法的上下文context
    value: any // 目标方法
    // ...
}

切入点(Pointcut)

每个程序的连接点有多个,如何定位到某个感兴趣的连接点,就需要通过切点来定位。

定义 Pointcut 语法格式

@Pointcut([type])
get [pointcut name](){
    return [pointcut rules]
}
  1. type:'prototype'|'static'指方法类型,原型方法(prototype)和静态方法(static);
  2. pointcut name 切点名称;
  3. pointcut rules 匹配规则: 命名空间?:类名.方法名称(namespace?:className.methodName)

例如

@Pointcut('prototype')
get pointcut(){
    return 'app.tsa.pages:TestPage.do*'
}

rules:PointcutRules 匹配说明

  1. 匹配类型
type PointcutRuleType = {
    namespace?: RegExp | string
    className: RegExp | string
    methodName: RegExp | string
}

type PointcutRules = string | RegExp | PointcutRuleType | Array<PointcutRuleType | RegExp | string>
  1. string 类型匹配

匹配命名空间 app.tsa.pages,TestPage 类,以 do 开头的任意方法

'app.tsa.pages:TestPage.do*'

多个匹配规则可以用 && 分割

'app.tsa.pages:TestPage.do* && app.tsa.pages:ArticlePage.submit*'
  1. 正则匹配

任意命名空间(可省),SomeClass 类,以 do 开头的任意方法

;/^([\d\w][_./-\w\d]*[:]?)?SomeClass.do[\w\d]+$/
  1. PointcutRuleType 匹配
@Pointcut()
get pointcut() {
    return {
        className: 'SomeClass',
        methodName: 'submit*'
    }
}
  1. 支持数组放置上述单个或者多个匹配规则
@Pointcut()
get pointcut() {
    return [
        'app.tsa.pages:TestPage.do*',
        'app.tsa.pages:ArticlePage.submit*',
        /^([\d\w][_./-\w\d]*[:]?)?SomeClass.do[\w\d]+$/
    ]
}
  1. "*"匹配多个字符,"?"匹配单个字符

  2. namespace 可以省略,但不建议这么做,因为匹配基于 className 和 methodName,极易发生冲突


通知(Advice)~

  1. 前置通知(Before Advice) 语法@Before({value:[pointcut name]}),目标动作执行之前织入通知。 前置通知只有一个参数,即:连接点 jp:JoinPoint
    @Before({value:'pointcut'})
    beforeAction(jp){
        console.log('Before action:',jp)
    }

  1. 后置通知(After Advice) 语法@After({value:[pointcut name]}),目标动作执行之后织入通知,无论成功,还是发生异常都会执行。 后置通知有三个参数,分别是:连接点 jp:JoinPoint, 返回值 rst:any, 异常 err: Error。
    @After({value:'pointcut'})
    afterAction(jp, rst, err){
        console.log('After action:',jp, rst, err)
    }

  1. 返回结果通知(AfterReturning Advice) 语法@AfterReturning({value:[pointcut name]}),目标动作执行成功之后执行 后置通知有两个参数,分别是:连接点 jp:JoinPoint, 结果 rst:any
    @AfterReturning({value:'pointcut'})
    afterReturningAction(jp, rst){
        console.log('AfterReturning action:',jp, rst)
    }

  1. 异常通知(AfterThrowing Advice) 语法@AfterThrowing({value:[pointcut name]}),目标动作执行发生异常后执行 后置通知有两个参数,分别是:连接点 jp:JoinPoint, 结果 err:Error
    @AfterThrowing({value:'pointcut'})
    afterThrowingAction(jp, err){
        console.log('AfterThrowing action:',jp, err)
    }
  1. 环绕通知(Around Advice) 语法@Around({value:[pointcut name]}),目标动作执行发生异常后执行 后置通知有一个参数,即:连接点 jp:ProceedJoinPoint。ProceedJoinPoint 继承 JoinPoint,含有一个 procced 方法,缓存了目标动作的执行,以及其他通知的执行
    interface ProceedJoinPoint extends JoinPoint {
        new(jp: ProceedJoinPointType)
        procced(): any
    }

执行 jp.procced(),才会触发执行动作

    @Around({value:'pointcut'})
    aroundAction(jp){
        console.log('Before Around action:',jp)
        let rst = jp.procced()
        console.log('After Around action:',jp)

        return rst
    }
  1. 通知执行顺序
  • 正常执行顺序:
    Around => Before => target method => AfterReturning => After => Around
  • 发生异常执行顺序:
    Around => Before => target method => AfterThrowing => After => Around

Tree Shaking 问题

Aspect 类文件和目标类文件,属于隐式的依赖关系,很容易被 Tree Shaking 清理掉。有几种办法解决这个问题

  1. package.json 添加 sideEffects 清单,使文件不受 tree shaking 影响
{
    "sideEffects": ["./src/aspects/**/*.ts", "./src/assets/**/*.js", "./src/assets/**/*.scss", "./src/assets/**/*.css"]
}
  1. babel-loader 添加 sideEffects 清单, 使文件不受 tree shaking 影响
module: {
    rules: [
        {
            test: /\.jsx?$/,
            exclude: /(node_modules|bower_components)/,
            use: {
                loader: 'babel-loader'
            },
            sideEffects: ['"./src/aspects/**/*.ts"']
        }
    ]
}

代码压缩开启混淆,造成 Pointcut 的 rules 匹配失效问题

  1. 排除掉 className 和 methodName 混淆,以 Terser 为例
const TerserPlugin = require('terser-webpack-plugin')

new TerserPlugin({
    cache: true, // 开启缓存,提升编译速度
    parallel: true, // 开启多进程,提升编译速度
    terserOptions: {
        mangle: true, // 混淆代码
        keep_classnames: true, // 保持classname不混淆(解决AOP动态匹配)
        keep_fnames: true // 函数、方法名称不混淆(解决AOP动态匹配)
    }
})

Readme

Keywords

Package Sidebar

Install

npm i @jsaop/jsaop

Weekly Downloads

0

Version

1.0.6

License

MIT

Unpacked Size

80.5 kB

Total Files

8

Last publish

Collaborators

  • muyi0327