LinkCat:PathMatcher路径匹配器

这里记录PathMatcher的设计以备忘。

首先我们需要知道的是这个PathMatcher的目标是什么。

目标

PathMatcher是LinkCat的注解模块中比较重要的一个匹配器,他是对资源来源注解 resource.origin 功能和结构进行特化的匹配器。所有对于resource.origin注解的匹配都会在这个匹配器里进行。

而这里定义一下resource.origin的值形式:

resource.origin=https/com/github/www/Exfluex/LinkCat

上面是一个非常简单的例子,这里其实可以看到是一个 https://www.github.com/Exfluex/LinkCat 的变体。这里没有用常规的URL形式,而是使用了一种将域名以“.”分段reverse后再构成路径的方式组成。这样的变形有很多好处,第一个好处是便于路径组织,第二个好处是便于正则匹配同时可以用KoaRouter内部使用的path-regex直接操作(舒服)。

这里我们引入一下域名方面的知识,也就是DNS服务相关的一些知识。

首先一个域名是由一些“.”Dot分割开来的域组合而成,比如 blog.oyxf.top 实际上就是三个域构成,而在长期的的网络使用环境下不难发现域名实际从右到左是一个narrow的一个过程。如果用地址来比喻,就是 市.省份.国家 这样的构成。而这里关于DNS解析方面的知识我们引用一下《网络是怎样连接的》一书对于DNS解析的图文(我的实体书忘了放在哪里了,只有先用电纸书截图代替):

下图中是对于DNS解析过程的一个模拟,这里可以明显看到是在执行对 lab.glasscom.com的解析。我们只考虑图片展示的内容的可以得出计算机解析时会找到最近的DNS服务器,而DNS服务器实际先会读缓存,如果命中记录就返回,没有命中就请求上级DNS服务器。而接下来的流程请参考往下第二张图。

DNS解析大接力

这里可以看到实际上DNS解析的过程也是从顶级域名往前进行解析的,我们这里只是将符合人类阅读的格式变为了计算机的实际执行格式。

所以我们将resource.origin注解的值组织形式总结一下就是:

resource.origin = 协议/[顶级域名/主域名/次级域名/..........]/[资源路径]

有了以上的知识,下面我们就开始正式介绍PathMatcher的实现。

可行性

我其实一开始就打算把resource.origin的组织形式改成上面这种格式,因为在我之前使用Koajs时,我就发现Koa-Router内部使用的是path-to-regex来生成对于路径的正则表达式。因此我后面在写linkcat resource.origin相关的matcher时第一时间就想到了上面这种办法。

这里就解决了PathMatcher最大的一个问题,其他的都是一些小细节了。

API层面的使用

这里我们介绍一下怎么在LinkCat插件编写时使用这个PathMatcher的特性。下面以@linkcat/plugin-linkcat-github为例:

GithubPlugin插件

这里可以看到onPag中的ScopeString内就有一个关于resource.origin的限定:

resource.origin=https/com/github/www/:AccountOrProject/:Repository

首先,我们将这个路径匹配字符串成为匹配路径,而这里的 :AccountOrProject 和 :Repository 实际是两个具名参数,因此我们这个匹配路径实际是一个带参匹配路径。

这里其实很好理解,就是把常规的 https://www.github.com/Exfluex/LinkCat URL匹配上,同时把参数 AccountOrProject=Exfluex而Repository=LinkCat。

由于我们这里有两个具名参数,而LinkCat会在匹配时自动将具名参数存储到payload上插件域下的变量列表中,而在插件域下的preparer、filler、finalizer都可以通过payload上指向当前插件数据的current属性拿到这个参数列表。因此这里payload.current[“resource.orign”][“Respository”](注意这里是CaseSensitive的)实际拿到了我们对应的 Repository=LinkCat的值。

MetaRetriever插件

然后我们再来看看metaretriever插件中的带参匹配路径:

resource.origin=:protocol(http|https)/:rest(.*)

这里可以看到有一个具名参数叫protocol,而他的值只有http或者https两种,剩下的被填入rest参数中。

具体一些更加复杂的用法可以参考path-to-regex的文档,但常规情况下这种简单的带参匹配机制就已经比较实用了。

PathMatcher实现

这里先贴一下PathMatcher

export class PathMatcher implements DefaultMatcherProto<PathMatcherConfig>{
  matchFn:MatchFunction;
  match(target:string){
    const res = this.matchFn(target);//很简单的一个内部逻辑,直接匹配字符串
    if(res == false)
      return null;
    return res.params;//将匹配成功时匹配出来的参数传递回去
  };
  constructor(public id:number,public config:PathMatcherConfig){
    this.matchFn = match(this.config.goal);//这里的match函数是path-to-regex提供的用于转换path为正则匹配函数的api函数。
  }
}

在开始看PathMatcherFactory代码之前,我们先思考一个问题,上面这种匹配路径的格式能够给我们带来一些什么样的优化呢?首先是可以分段,将前面几段路径做成一个树形结构,比如根节点是 根域(DNS专业名称实际是个Dot)即为空,第二层是https/http/file/smb等协议的域,第三层是顶级域名比如com、org、top的域。然后第四层就是主域名例如github/google/baidu的域,而第五层是叶子节点也是所有被注册的PathMatcher所存在的地方。

但在实际代码实现中我们没有对协议进行匹配,而是直接从顶级域名开始匹配(后面考虑增加IP匹配方法):

路径分段机制
分段结果

简单画个图:

resource.origin中路径
快查树

这里就比较明显了首先根节点是根域(第一层),第二层是顶级域名,第三层是主域名,第四层是次级域名的首字母,然后第五层下挂载这叶子节点,也就是我们实际的Matcher在的位置。

我们先看看树结构的实现,是一个比较简单的树节点定义,其中需要注意的是这里面我使用了Symbol定义了一个flag位,判断是否是叶子节点(emm为了后面的拓展设计的,后面准备改成bits flag)。

export namespace PathMatcherFactory{
  export type Config = DefaultMatcherProto.Config
  export const leaf = Symbol("leaf");
  export const index = Symbol("index");
  export const pathMatcher = /\/([^\/]+)\/([^\/]+)\/([^\/]{1})/;
  export class Node{
    [leaf]=false;
  }
  export class QuickSearchBranch extends Node{
    [key:string]:Node;
  }
  export class QuickSearchLeaf extends Node{
    [leaf] = true;
    constructor(public matchers:PathMatcher[]=[]){
      super();
    }

  }
}

下面是整个PathMatcherFactory的实现

export class PathMatcherFactory implements DefaultMatcherFactory<PathMatcher,PathMatcherConfig>{
  root:PathMatcherFactory.QuickSearchBranch=new PathMatcherFactory.QuickSearchBranch();//快查树根节点
  prior:PathMatcher[]=[];//高优先级或不是path-to-regex类型的的PathMatcher
  constructor(public id:number,public config:DefaultMatcherProto.Config){
  }
  gen(config: PathMatcherConfig): PathMatcher {//生成PathMatcher
    const matcher = new PathMatcher(this.generateId(),config);
    this.register(matcher);//注册到树上
    return matcher;
  }
  private maxId=0;
  private  generateId():number{
    return this.maxId++;
  }
  _callback:MatcherCallback=defaultCallback;//细节看之前RenderModule的文章
  async match(target: string, env:MatcherEnv): Promise<number> {//匹配入口
    let num  =0;
    this.prior.forEach(async matcher=>{//先把高优先级的matcher执行一遍
      const res = matcher.match(target);
      if(res != null){
        num++;
        env.data = res;
        const cb = matcher.config.callback??this._callback;
        cb(matcher,env);//回调函数
      }
    });
    const res = PathMatcherFactory.pathMatcher.exec(target);//分段path
    if(res == null){
      //TODO Throw Warnning
      return -1;
    };
    let branch:PathMatcherFactory.QuickSearchBranch;
    branch =this.root[res[1]] as PathMatcherFactory.QuickSearchBranch;//拿到顶级域名 com\org\top节点
    if(!branch){
      return num;
    }
    branch = branch[res[2]] as PathMatcherFactory.QuickSearchBranch;//拿到主域名 github/google/oyxf节点
    if(!branch){
      return num;
    }
    const leaf = branch[res[3]] as PathMatcherFactory.QuickSearchLeaf;//拿到次级域名首字母索引节点
    if(!leaf){
      return num;
    }
    leaf.matchers.forEach(matcher =>{//执行挂载的Matcher
      const res = matcher.match(target);
      if(res != null){
        num++;
        env.data = res;
        const cb = matcher.config.callback??this._callback;
        cb(matcher,env);
      }
    });
    return num;
  }
  register(matcher: PathMatcher): this {
    if(matcher.config.priority > 0){
      this.prior.push(matcher);//如果优先级比较高,就push到prior
      return this;
    }
    const res = PathMatcherFactory.pathMatcher.exec(matcher.config.goal);
    if(res == null){
      //TODO Throw Warnning
      this.prior.push(matcher);//如果无法分段,就push到prior,这里还有bug需要Fix
      return this
    };
    let branch:PathMatcherFactory.QuickSearchBranch;
    let leaf:PathMatcherFactory.QuickSearchLeaf;
    this.root[res[1]] = branch = (this.root[res[1]]??new PathMatcherFactory.QuickSearchBranch()) as PathMatcherFactory.QuickSearchBranch;//构建顶级域名节点
    branch[res[2]] = branch  =  (branch[res[2]]??new PathMatcherFactory.QuickSearchBranch()) as PathMatcherFactory.QuickSearchBranch;//构建主域名节点
    branch[res[3]] = leaf = (branch[res[3]]??new PathMatcherFactory.QuickSearchLeaf()) as PathMatcherFactory.QuickSearchLeaf;//构建次级域名索引节点
    leaf.matchers.push(matcher);//把Matcher存入叶子节点
    return this;
  }
  unregister(id: number): this {//nope
    return this;
  }
  traverse(traverse: Registry.traverseFn<PathMatcher>): this {
    return this;//nope
  }
  find(id: number): Registry.Item<number>[] {
    return [];//nope
  }
}

这里就是介绍完了PathMatcher的设计和实现中比较核心的内容。


v0.1 wep 完成文章内容编写 2022/03/06

发表评论

您的电子邮箱地址不会被公开。 必填项已用 * 标注