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

@jsaop/jsaop

v1.0.6

Published

typescript aspect aop

Downloads

3

Readme

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