npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

tsaop

v1.0.3

Published

typescript aspect aop

Downloads

1

Readme

jsaop

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

适用场景

  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()
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动态匹配)
        }
    })