2024年1月

apply和call

apply和call非常类似,都是用于改变函数中this的指向,只是传入的参数不同,等于间接调用一个函数,也等于将这个函数绑定到一个指定的对象上:

let name = 'window'

function getName(param1, param2) {
  console.log(this.name)
  console.log(param1, param2)
}
let obj = {
  name: 'easylee',
}
getName.call(obj, 123, 23)
getName.apply(obj, [123, 23])

如上面的例子,如果直接调用
getName
那么返回的是
window
,但是通过
call
方法,将函数绑定到了
obj
上,成为obj的一个函数,同时里面的
this
也指向了
obj

两者主要的区别在于,当函数有多个参数时,
call
是直接传入多个参数,而
apply
将多个参数组合成一个数组传输参数

手写call

原理:

  1. 首先,通过
    Function.prototype.myCall
    将自定义的
    myCall
    方法添加到所有函数的原型对象上,使得所有函数实例都可以调用该方法。

  2. myCall
    方法内部,首先通过
    typeof this !== "function"
    判断调用
    myCall
    的对象是否为函数。如果不是函数,则抛出一个类型错误。
  3. 然后,判断是否传入了上下文对象
    context
    。如果没有传入,则将
    context
    赋值为全局对象。这里使用了一种判断全局对象的方法,先判断是否存在
    global
    对象,如果存在则使用
    global
    ,否则判断是否存在
    window
    对象,如果存在则使用
    window
    ,如果都不存在则将
    context
    赋值为
    undefined
  4. 接下来,使用
    Symbol
    创建一个唯一的键
    fn
    ,用于将调用
    myCall
    的函数绑定到上下文对象的新属性上。
  5. 将调用
    myCall
    的函数赋值给上下文对象的
    fn
    属性,实现了将函数绑定到上下文对象上的效果。
  6. 调用绑定在上下文对象上的函数,并传入
    myCall
    方法的其他参数
    args
  7. 将绑定在上下文对象上的函数删除,以避免对上下文对象造成影响。
  8. 返回函数调用的结果。
Function.prototype.myCall = function (context, ...args) {
  // 判断调用myCall的是否为函数
  if (typeof this !== 'function') {
    throw new TypeError('Function.prototype.myCall - 被调用的对象必须是函数')
  }
  // 判断是否传入上下文对象,不传入则指定默认全局对象
  context = context || (typeof global !== 'undefined' ? gloabl : typeof window !== 'undefined' ? window : undefined)

  // 在上下文对象上绑定当前调用的函数,作为属性方法
  // 不能直接调用this方法函数,原因在于如果不将这个方法绑定到上下文对象上
  // 直接执行this函数,this函数里面的this上下文对象无法识别为绑定的对象
  let fn = Symbol('key')
  context[fn] = this
  const result = context[fn](...args)
  // 删除这个函数,避免对上下文对象造成影响
  delete context[fn]
  return result
}
const test = {
  name: 'xxx',
  hello: function () {
    console.log(`hello,${this.name}!`)
  },
  add: function (a, b) {
    return a + b
  },
}
const obj = { name: 'world' }
test.hello.myCall(obj) //hello,world!
test.hello.call(obj) //hello,world!
console.log(test.add.myCall(null, 1, 2)) //3
console.log(test.add.call(null, 1, 2)) //3

手写apply

Function.prototype.myApply = function (context, argsArr) {
  // 判断调用myApply的是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Function.prototype.myApply - 被调用的对象必须是函数");
  }

  // 判断传入的参数是否为数组
  if (argsArr && !Array.isArray(argsArr)) {
    throw new TypeError("Function.prototype.myApply - 第二个参数必须是数组");
  }

  // 如果没有传入上下文对象,则默认为全局对象
  //global:nodejs的全局对象
  //window:浏览器的全局对象
  context =
    context ||
    (typeof global !== "undefined"
      ? global
      : typeof window !== "undefined"
      ? window
      : undefined);

  // 用Symbol来创建唯一的fn,防止名字冲突
  let fn = Symbol("key");

  // this是调用myApply的函数,将函数绑定到上下文对象的新属性上
  context[fn] = this;

  // 传入myApply的多个参数
  const result = Array.isArray(argsArr)
    ? context[fn](...argsArr)
    : context[fn]();

  // 将增加的fn方法删除
  delete context[fn];

  return result;
};

// 测试一下
const test = {
  name: "xxx",
  hello: function () {
    console.log(`hello,${this.name}!`);
  },
};
const obj = { name: "world" };
test.hello.myApply(obj); //hello,world!
test.hello.apply(obj); //hello,world!
const arr = [2,3,6,5,1,7,9,5,0]
console.log(Math.max.myApply(null,arr));//9
console.log(Math.max.apply(null,arr));//9

bind

最后来看看
bind
,和前面两者主要的区别是,通过 bind 绑定的不会立即调用,而是返回一个新函数,然后需要手动调用这个新函数,来实现函数内部
this
的绑定

let name = 'window'
function getName(param1, param2) {
  console.log(this.name)
  console.log(param1)
  console.log(param2)
}
let obj = {
  name: 'easylee',
}

let fn = getName.bind(obj, 123, 234)	// 通过绑定创建一个新函数,然后再调用新函数
fn()

除此之外,
bind
还支持柯里化,也就是绑定时传入的参数将保留到调用时直接使用

let sum = (x, y) => x + y
let succ = sum.bind(null, 1) // 绑定时没有指定对象,但是给函数的第一个参数指定为1
succ(2) // 3, 调用时只传递了一个参数2,会直接对应到y,因为前面的1已经绑定到x上了

手写bind

原理:

  1. 首先,通过
    Function.prototype.myBind
    将自定义的
    myBind
    方法添加到所有函数的原型对象上,使得所有函数实例都可以调用该方法。

  2. myBind
    方法内部,首先通过
    typeof this !== "function"
    判断调用
    myBind
    的对象是否为函数。如果不是函数,则抛出一个类型错误。
  3. 然后,判断是否传入了上下文对象
    context
    。如果没有传入,则将
    context
    赋值为全局对象。这里使用了一种判断全局对象的方法,先判断是否存在
    global
    对象,如果存在则使用
    global
    ,否则判断是否存在
    window
    对象,如果存在则使用
    window
    ,如果都不存在则将
    context
    赋值为
    undefined
  4. 保存原始函数的引用,使用
    _this
    变量来表示。
  5. 返回一个新的闭包函数
    fn
    作为绑定函数。这个函数接受任意数量的参数
    innerArgs
    。(关于闭包的介绍可以看这篇文章->
    闭包的应用场景
  6. 在返回的函数
    fn
    中,首先判断是否通过
    new
    关键字调用了函数。这里需要注意一点,如果返回出去的函数被当作构造函数使用,即使用
    new
    关键字调用时,
    this
    的值会指向新创建的实例对象。通过检查
    this instanceof fn
    ,可以判断返回出去的函数是否被作为构造函数调用。这里使用
    new _this(...args, ...innerArgs)
    来创建新对象。
  7. 如果不是通过
    new
    调用的,就使用
    apply
    方法将原始函数
    _this
    绑定到指定的上下文对象
    context
    上。这里使用
    apply
    方法的目的是将参数数组
    args.concat(innerArgs)
    作为参数传递给原始函数。
Function.prototype.myBind = function (context, ...args) {
  // 判断调用myBind的是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Function.prototype.myBind - 被调用的对象必须是函数");
  }

  // 如果没有传入上下文对象,则默认为全局对象
  //global:nodejs的全局对象
  //window:浏览器的全局对象
 context =
    context || (typeof global !== "undefined"
      ? global
      : typeof window !== "undefined"
      ? window
      : undefined);

  // 保存原始函数的引用,this就是要绑定的函数
  const _this = this;

  // 返回一个新的函数作为绑定函数
  return function fn(...innerArgs) {
    // 判断返回出去的函数有没有被new
    if (this instanceof fn) {
      return new _this(...args, ...innerArgs);
    }
    // 使用apply方法将原函数绑定到指定的上下文对象上
    return _this.apply(context,args.concat(innerArgs));
  };
};

// 测试
const test = {
  name: "xxx",
  hello: function (a,b,c) {
    console.log(`hello,${this.name}!`,a+b+c);
  },
};
const obj = { name: "world" };
let hello1 = test.hello.myBind(obj,1);
let hello2 = test.hello.bind(obj,1); 
hello1(2,3)//hello,world! 6
hello2(2,3)//hello,world! 6
console.log(new hello1(2,3));
//hello,undefined! 6
// hello {}
console.log(new hello2(2,3));
//hello,undefined! 6
// hello {}

总结一下,这三个函数都是用于改变函数内
this
对象的指向,只是使用方式有不同,其中 apply 传递多个参数使用数组的形式,call 则直接传递多个参数,而 bind 则可以将绑定时传递的参数保留到调用时直接使用,支持柯里化,同时 bind 不会直接调用,绑定之后返回一个新函数,然后通过调用新函数再执行。

TypeChat
用一句话概括,就是用了它你可以让大语言模型(比如 ChatGPT)将自然语言转换成特定类型的 JSON 数据。

我们在使用 ChatGPT 的时候,大致流程如下:

假如我们需要 ChatGPT 按照我们输入的 prompt,输出指定格式的 JSON 数据,我们在 prompt 里将我们的要求描述清楚就行,比如

好像没什么问题,很听话。

我们再看一个例子

{
    "filters": [
        {
            "component": "select",
            "key": "店铺名称",
            "label": "店铺名称",
            "placeholder": "请输入"
        },
        {
            "component": "select",
            "key": "店铺编码",
            "label": "店铺编码",
            "placeholder": "请输入"
        },
        {
            "component": "select",
            "key": "店铺门头编码",
            "label": "店铺门头编码",
            "placeholder": "请输入"
        },
        {
            "component": "select",
            "key": "所在区域",
            "label": "所在区域",
            "placeholder": "全部"
        }
    ],
    "columns": [
        {
            "slot": false,
            "title": "店铺编码",
            "dataIndex": "店铺编码",
            "key": "店铺编码"
        },
        {
            "slot": false,
            "title": "店铺名称",
            "dataIndex": "店铺名称",
            "key": "店铺名称"
        },
        {
            "slot": false,
            "title": "店铺业务范围",
            "dataIndex": "店铺业务范围",
            "key": "店铺业务范围"
        },
        {
            "slot": false,
            "title": "店铺类型",
            "dataIndex": "店铺类型",
            "key": "店铺类型"
        },
        {
            "slot": false,
            "title": "所在区域",
            "dataIndex": "所在区域",
            "key": "所在区域"
        },
        {
            "slot": false,
            "title": "详细地址",
            "dataIndex": "详细地址",
            "key": "详细地址"
        }
    ],
    "pagination": {
        "show": true,
        "page": "page",
        "size": "size",
        "total": "result.total"
    },
    "includeModifyModal": false,
    "fetchName": "fetchTableList",
    "result": "[\"result\"][\"records\"]",
    "serviceName": "getTableList"
}

我们需要 ChatGPT 把上面 JSON 数据中 filters 字段中的 key 字段的值翻译为英文,使用驼峰语法。columns 字段中的 key、dataIndex 字段的值翻译为英文,使用驼峰语法。

我们的 prompt 可以这么写:

{
    "filters": [
        {
            "component": "select",
            "key": "店铺名称",
            "label": "店铺名称",
            "placeholder": "请输入"
        },
        {
            "component": "select",
            "key": "店铺编码",
            "label": "店铺编码",
            "placeholder": "请输入"
        },
        {
            "component": "select",
            "key": "店铺门头编码",
            "label": "店铺门头编码",
            "placeholder": "请输入"
        },
        {
            "component": "select",
            "key": "所在区域",
            "label": "所在区域",
            "placeholder": "全部"
        }
    ],
    "columns": [
        {
            "slot": false,
            "title": "店铺编码",
            "dataIndex": "店铺编码",
            "key": "店铺编码"
        },
        {
            "slot": false,
            "title": "店铺名称",
            "dataIndex": "店铺名称",
            "key": "店铺名称"
        },
        {
            "slot": false,
            "title": "店铺业务范围",
            "dataIndex": "店铺业务范围",
            "key": "店铺业务范围"
        },
        {
            "slot": false,
            "title": "店铺类型",
            "dataIndex": "店铺类型",
            "key": "店铺类型"
        },
        {
            "slot": false,
            "title": "所在区域",
            "dataIndex": "所在区域",
            "key": "所在区域"
        },
        {
            "slot": false,
            "title": "详细地址",
            "dataIndex": "详细地址",
            "key": "详细地址"
        }
    ],
    "pagination": {
        "show": true,
        "page": "page",
        "size": "size",
        "total": "result.total"
    },
    "includeModifyModal": false,
    "fetchName": "fetchTableList",
    "result": "[\"result\"][\"records\"]",
    "serviceName": "getTableList"
}
将这段 json 中,filters 字段中的 key 字段的值翻译为英文,使用驼峰语法,label、placeholder 字段的值保留中文。
columns 字段中的 key、dataIndex 字段的值翻译为英文,使用驼峰语法,title 字段的值保留中文。 
返回翻译后的 markdown 语法的代码块

最后面的那段话已经经过了几次修改,我们看下 ChatGPT 返回的内容:

{
    "filters": [
        {
            "component": "select",
            "storeName": "店铺名称",
            "label": "店铺名称",
            "placeholder": "请输入"
        },
        {
            "component": "select",
            "storeCode": "店铺编码",
            "label": "店铺编码",
            "placeholder": "请输入"
        },
        {
            "component": "select",
            "storeDoorCode": "店铺门头编码",
            "label": "店铺门头编码",
            "placeholder": "请输入"
        },
        {
            "component": "select",
            "area": "所在区域",
            "label": "所在区域",
            "placeholder": "全部"
        }
    ],
    "columns": [
        {
            "slot": false,
            "title": "店铺编码",
            "storeCode": "店铺编码",
            "key": "storeCode"
        },
        {
            "slot": false,
            "title": "店铺名称",
            "storeName": "店铺名称",
            "key": "storeName"
        },
        {
            "slot": false,
            "title": "店铺业务范围",
            "storeBusinessScope": "店铺业务范围",
            "key": "storeBusinessScope"
        },
        {
            "slot": false,
            "title": "店铺类型",
            "storeType": "店铺类型",
            "key": "storeType"
        },
        {
            "slot": false,
            "title": "所在区域",
            "area": "所在区域",
            "key": "area"
        },
        {
            "slot": false,
            "title": "详细地址",
            "detailedAddress": "详细地址",
            "key": "detailedAddress"
        }
    ],
    "pagination": {
        "show": true,
        "page": "page",
        "size": "size",
        "total": "result.total"
    },
    "includeModifyModal": false,
    "fetchName": "fetchTableList",
    "result": "[\"result\"][\"records\"]",
    "serviceName": "getTableList"
}

filters 里的 key 字段哪去了?columns 里 dataIndex 字段哪去了?

这只是我遇到的各种情况里的一种,当然也有成功的,反正就是很玄学,可能是我半年 prompt 练习生的水平不够吧。

如果我们用
TypeChat
会怎么样呢?

直接看效果 :

typechat.gif

完美。

TypeChat
的大致原理如下:

其实就是在 prompt 拼上了一个 TS 类型,要求 ChatGPT 按照 TS 类型声明输出 JSON。背后还会构建一个 TS 程序去进行校验,如果校验失败就把错误信息也加到 prompt 里,让 ChatGPT 再处理一次。

上面生成的 prompt 跟 TypeChat 生成的有些区别,我并没有直接使用 TypeChat,为了方便在我的项目里使用就抄了一份进行修改,在生成 prompt 的里加上了 “按照字段的注释进行处理” 这一要求。修改后的代码:
https://github.com/lowcode-scaffold/lowcode-materials/blob/master/share/TypeChatSlim/index.ts

在 TS 类型的字段注释里还可以加入更离谱的东西来要求 ChatGPT 去处理,比如之前弄的一个根据聊天记录生成日程安排的小 demo,在字段注释里加上当前时间,让 ChatGPT 根据会话内容推算出具体的日期。TS 类型定义如下:

const res = await translate({
    schema: `export type SchedulType = {
              /**
               * 时间,格式:YYYY-MM-DD HH:mm:ss,当前时间为 ${new Date().toLocaleString()},请推算出正确的时间
               */
              time: string;
              /**
               * 活动主题
               */
              todo: string;
              /**
               * 地点
               */
              addr: string;
              /**
               * 活动的全体参与人
               */
              participant: string[];
            };`,
    typeName: 'SchedulType',
    request: `根据下面的聊天记录,生成日程安排:${message.data}`,
    createChatCompletion: createChatCompletionForScript,
    showWebview: true,
  });

a9cf046b-05ce-405c-9ebc-f7b8f4c373f0.png

聊天记录中只说到了周六下午两点,ChatGPT 根据字段注释里的当前时间正确推算出了日程的具体日期。

受到 TypeChat 的启发,借助 JSON Schema 实现了一个
JSONSchemaChat
,原理差不多,效果如下:

Azure应用服务用YARP取代了Nginx,获得了80%以上的吞吐量。他们每天处理160B多个请求(1.9 m RPS)。这是微软的一项了不起的技术创新。

首先我们来介绍一下什么是Yarp

Yarp是什么?

YARP(Yet Another Reverse Proxy)是一个开源的、高性能的反向代理库,由Microsoft开发,使用C#语言编写。它旨在作为.NET平台上构建反向代理服务器的基础。YARP主要针对.NET 5及以上版本,允许开发者在.NET应用程序中轻松地实现反向代理的功能。

YARP的主要特点和功能:

  1. 模块化和可扩展性:
    YARP设计成高度模块化的,这意味着可以根据需要替换或扩展内部组件,如HTTP请求路由、负载均衡、健康检查等。
  2. 性能:
    YARP针对高性能进行了优化,利用了.NET的异步编程模型和高效的IO操作,以处理大量并发连接。
  3. 配置驱动:
    YARP的行为可以通过配置来控制,支持从文件、数据库或其他来源动态加载配置。
  4. 路由:
    可以基于各种参数(如路径、头部、查询参数)配置请求路由规则。
  5. 负载均衡:
    内置多种负载均衡策略,如轮询、最少连接、随机选择等,并且可以自定义负载均衡策略。
  6. 健康检查:
    支持后端服务的健康检查,以确保请求只会被转发到健康的后端服务实例。
  7. 转换器:
    允许对请求和响应进行转换,如修改头部、路径或查询参数。
  8. 会话亲和性:
    支持会话亲和性(Session Affinity),确保来自同一客户端的请求被发送到相同的后端服务实例。

使用YARP的一些场景:

  • 反向代理:
    在客户端和后端服务之间提供一个中间层,用于请求转发和负载均衡。
  • API网关:
    作为微服务架构中的API网关,提供路由、鉴权、监控等功能。
  • 边缘服务:
    在应用程序和外部世界之间提供安全层,处理SSL终止、请求限制等任务。

Yarp简单的使用

创建一个WebApi的项目

安装
Nuget

<ItemGroup>
	<PackageReference Include="Yarp.ReverseProxy" Version="2.0.0" />
</ItemGroup>

打开
appsettings.json

{
 "Logging": {
   "LogLevel": {
     "Default": "Information",
     "Microsoft": "Warning",
     "Microsoft.Hosting.Lifetime": "Information"
   }
 },
 "AllowedHosts": "*",
 "ReverseProxy": {
   "Routes": {
     "route1" : {
       "ClusterId": "cluster1",
       "Match": {
         "Path": "{**catch-all}"
       }
     }
   },
   "Clusters": {
     "cluster1": {
       "Destinations": {
         "destination1": {
           "Address": "https://cn.bing.com/"
         }
       }
     }
   }
 }
}

打开
Program.cs

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
app.MapReverseProxy();
app.Run();

然后启动项目,访问我们的api就会被代理转发到
bing

Yarp工具代理使用

下面我们在提供一个在中间件使用yarp的方式

我们需要用到
IHttpForwarder

先修改
Program.cs
在这里我们注入了
HttpForwarder
,然后提供一个Run中间件,在中间件中手动指定了端点的地址
https://cn.bing.com/
然后我们启动一下项目。

using Yarp.ReverseProxy.Forwarder;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpForwarder(); // 注入IHttpForwarder 
var app = builder.Build();

var httpMessage = new HttpMessageInvoker(new HttpClientHandler());

app.Run((async context =>
{
    var httpForwarder = context.RequestServices.GetRequiredService<IHttpForwarder>();
    var destinationPrefix = "https://cn.bing.com/";

    await httpForwarder.SendAsync(context, destinationPrefix, httpMessage);
}));

app.Run();

也是一样会被代理过去,但是于简单使用不一样的是我们是在代码层面控制代理的。

使用yarp修改Bing的响应内容

我们继续基于上面的代理使用进行修改bing的相应内容!

打开
Program.cs

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpForwarder(); // 注入IHttpForwarder 
var app = builder.Build();

var httpMessage = new HttpMessageInvoker(new HttpClientHandler()
{
    // 忽略https错误
    ServerCertificateCustomValidationCallback = (_, _, _, _) => true,
    AllowAutoRedirect = false,
    AutomaticDecompression = DecompressionMethods.GZip,
    UseCookies = false,
    UseProxy = false,
    UseDefaultCredentials = true,
});
var destinationPrefix = "https://cn.bing.com/";

var bingTransformer = new BingTransformer();

app.Run((async context =>
{
    var httpForwarder = context.RequestServices.GetRequiredService<IHttpForwarder>();
    await httpForwarder.SendAsync(context, destinationPrefix, httpMessage, new ForwarderRequestConfig(),
        bingTransformer);
}));

app.Run();

创建
BingTransformer.cs

public class BingTransformer : HttpTransformer
{
    public override async ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest,
        string destinationPrefix,
        CancellationToken cancellationToken)
    {
        var uri = RequestUtilities.MakeDestinationAddress(destinationPrefix, httpContext.Request.Path,
            httpContext.Request.QueryString);
        proxyRequest.RequestUri = uri;
        proxyRequest.Headers.Host = uri.Host;
        await base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix, cancellationToken);
    }

    public override async ValueTask<bool> TransformResponseAsync(HttpContext httpContext,
        HttpResponseMessage? proxyResponse,
        CancellationToken cancellationToken)
    {
        await base.TransformResponseAsync(httpContext, proxyResponse, cancellationToken);

        if (httpContext.Request.Method == "GET" &&
            httpContext.Response.Headers["Content-Type"].Any(x => x.StartsWith("text/html")))
        {
            var encoding = proxyResponse.Content.Headers.FirstOrDefault(x => x.Key == "Content-Encoding").Value;
            if (encoding?.FirstOrDefault() == "gzip")
            {
                var content = proxyResponse?.Content.ReadAsByteArrayAsync(cancellationToken).Result;
                if (content != null)
                {
                    var result = Encoding.UTF8.GetString(GZipDecompressByte(content));
                    result = result.Replace("国内版", "Token Bing 搜索 - 国内版");
                    proxyResponse.Content = new StringContent(GZipDecompressString(result));
                }
            }
            else if (encoding.FirstOrDefault() == "br")
            {
                var content = proxyResponse?.Content.ReadAsByteArrayAsync(cancellationToken).Result;
                if (content != null)
                {
                    var result = Encoding.UTF8.GetString(BrDecompress(content));
                    result = result.Replace("国内版", "Token Bing 搜索 - 国内版");
                    proxyResponse.Content = new ByteArrayContent(BrCompress(result));
                }
            }
            else
            {
                var content = proxyResponse?.Content.ReadAsStringAsync(cancellationToken).Result;
                if (content != null)
                {
                    content = content.Replace("国内版", "Token Bing 搜索 - 国内版");
                    proxyResponse.Content = new StringContent(content);
                }
            }
        }

        return true;
    }

    /// <summary>
    /// 解压GZip
    /// </summary>
    /// <param name="bytes"></param>
    /// <returns></returns>
    public static byte[] GZipDecompressByte(byte[] bytes)
    {
        using var targetStream = new MemoryStream();
        using var compressStream = new MemoryStream(bytes);
        using var zipStream = new GZipStream(compressStream, CompressionMode.Decompress);
        using (var decompressionStream = new GZipStream(compressStream, CompressionMode.Decompress))
        {
            decompressionStream.CopyTo(targetStream);
        }

        return targetStream.ToArray();
    }

    /// <summary>
    /// 解压GZip
    /// </summary>
    /// <param name="str"></param>
    /// <returns></returns>
    public static string GZipDecompressString(string str)
    {
        using var compressStream = new MemoryStream(Encoding.UTF8.GetBytes(str));
        using var zipStream = new GZipStream(compressStream, CompressionMode.Decompress);
        using var resultStream = new StreamReader(new MemoryStream(compressStream.ToArray()));
        return resultStream.ReadToEnd();
    }

    /// <summary>
    /// Br压缩
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    public static byte[] BrCompress(string str)
    {
        using var outputStream = new MemoryStream();
        using (var compressionStream = new BrotliStream(outputStream, CompressionMode.Compress))
        {
            compressionStream.Write(Encoding.UTF8.GetBytes(str));
        }

        return outputStream.ToArray();
    }

    /// <summary>
    /// Br解压
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    public static byte[] BrDecompress(byte[] input)
    {
        using (var inputStream = new MemoryStream(input))
        using (var outputStream = new MemoryStream())
        using (var decompressionStream = new BrotliStream(inputStream, CompressionMode.Decompress))
        {
            decompressionStream.CopyTo(outputStream);
            return outputStream.ToArray();
        }
    }
}

得到的效果我们将
国内版
修改成了
Token Bing 搜索 - 国内版

Yarp AOT尝试

下面我们将Yarp进行AOT尝试,首先打开我们的项目,添加以下参数

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <InvariantGlobalization>true</InvariantGlobalization>
    <PublishAot>true</PublishAot>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Yarp.ReverseProxy" Version="2.0.0" />
  </ItemGroup>
</Project>

AOT以后的大小
15MB

测试转发并无问题

优化AOT大小,添加以下参数

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <InvariantGlobalization>true</InvariantGlobalization>
    <PublishAot>true</PublishAot>

    <StackTraceSupport>false</StackTraceSupport>
    <OptimizationPreference>Size</OptimizationPreference>
    <PublishTrimmed>true</PublishTrimmed>
    <BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
    <EventSourceSupport>false</EventSourceSupport>
    <HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
    <EnableUnsafeBinaryFormatterSerialization>false</EnableUnsafeBinaryFormatterSerialization>
    <MetadataUpdaterSupport>false</MetadataUpdaterSupport>
    <UseNativeHttpHandler>true</UseNativeHttpHandler>
    <TrimMode>link</TrimMode>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Yarp.ReverseProxy" Version="2.0.0" />
  </ItemGroup>
</Project>

减少2MB左右,并且正常运行代理

Yarp相关资料

技术交流群:737776595

官方文档:
https://microsoft.github.io/reverse-proxy/articles/getting-started.html

来着token的分享

我们在odoo中构建业务系统模块的时候,通常会使用one2many、many2one或many2many将模型进行关联,由此产生的数据也会通过外键发生关联。那么在odoo中删除数据的时候,如何关联删除呢?接下来我们以初学者的视角来观察下这一过程。

刚开始接触odoo,可能时不时的就会感叹odoo的低代码魅力:“
这么少的代码,这么强悍的功能
”。即便是一个主从模型的功能,很快也就完事了。正沉浸在这种喜悦中的时候,突然一个弹框提示:

目前构建的代码涉及到两个菜单功能,运单管理和承运管理,业务场景是这样的:

  • 运单管理用于创建要运送的货物的基本信息和明细信息,包括主表[waybill]和明细表[waybill_detail]
  • 承运管理也是一个主附表关系,主表[shipping_document]用来记录车辆、司机、运费等基本信息,附表[shiping_document_detail]用来记录要承运的运单信息
  • 现在要删除运单表的信息,但是提示需要把承运明细表[shiping_document_detail]中的数据删除;问题是承运管理中的数据已经删除过了,用动态图来说明下:

既然提示说需要删除承运明细表[shiping_document_detail]中的数据,在管理界面中已经删除过看不到数据了,我们不妨到数据表中看下:

原来在数据库中并没有把数据删除掉,只是把承运主表[shipping_document]的关联id删除了,如上图标记1所示。如果此时我删除运单表[waybill]中的数据,在承运明细表[shipping_document_detail]中依然有和运单相关的残留数据,如上图标记2所示。这在odoo看来是不可接受的,所以就抛出了验证错误提示。要避免这样的验证错误提示发生,我们就需要在删除承运主表[shipping_document]数据的时候,将承运明细表[shipping_document_detail]中的关联数据也删除掉。接下来我们就来说下odoo中的级联删除操作:

ondelete=’cascade’

ondelete是odoo在模型中引入的一种触发机制,在odoo16源码中,目前只有Selection和Many2one这两种字段类型支持传参调用,我们看下ondelete在Many2one类型中的定义:

通过上图我们了解到,ondelete的功能就是在参考模型中的记录被删除的时候,做一些操作。可以有’set null’、’restrict’、’cascade’这些操作,现在我们主要介绍cascade。我们在承运明细表[shipping_document_detail]的关联字段shipping_document_id上添加ondelete=’cascade’:

1 classShippingDocumentDetail(models.Model):2     _name = 'shipping.document.detail'
3     _description = 'Shipping Document Detail'
4 
5     waybill_id = fields.Many2one('waybill', required=True, domain="[('destination', '=', destination_id)]")6     receiver_id = fields.Many2one(related='waybill_id.receiver_id', readonly=True)7     receiver_tel = fields.Char(string="Consignee Phone", store=False, readonly=True, compute="_compute_customerInfo")8     detail_ids = fields.One2many(related='waybill_id.detail_ids')9     total_fees = fields.Float(related='waybill_id.total_fees')10     shipping_document_id = fields.Many2one('shipping.document', string='Shipping Document',ondelete='cascade')11     destination_id=fields.Integer(string='Destination Id', compute="_compute_destination_id")

模块升级后,我们先手动删除承运明细表[shipping_document_detail]中的残留数据,然后添加新的测试数据,我们添加的承运单截图如下:

数据库中对应截图如下:

接下来我们执行承运单的删除操作,再查询数据库的时候,发现刚才创建的数据已经没有了。

写这篇博客的时候,我已经整理好了文章大纲,边写边测试验证,最后发现我原来的理解是错的,原来的大纲中还有在One2many中使用ondelete=’cascade’,以及ondelete=’cascade’无法解决问题时如何通过unlink()来解决。唉,半路借来的内容真是害人不轻。还好写了博客,又参考odoo官方文档和源码走了一遍流程;不仅深入的理解了这个知识点,也优化了一知半解的时候写的代码。

源码地址【源码更新会有3-5天的时间出入】:
github

点击链接阅读原文:
菜园工程师