设计模式—关于如何更好的封装与创建对象

news/2024/4/27 21:52:58/文章来源:https://www.cnblogs.com/mr-mikey/p/16812346.html

上一节我们主要学习了使用设计模式来写代码的指导思想以及设计模式的分门别类,本节主要学习创建型的三种设计模式是怎么使用的。如何利用创建型设计模式来指导我们更好的封装代码更好的创建对象。

为什么要封装?封装能带给我们什么好处?

  • 定义变量不会污染外部:封装的首要目的是保护我们的变量不会被外部所污染,也不会污染外部的变量,在大型项目中很难保证变量名不和别人的变量名起冲突,所以说这点很重要。
  • 能够作为一个模块调用:除了上面那点之外,良好的封装才能保证我们的代码作为独立的模块与外部沟通,把程序变成模块架构中的基础就是良好的封装成模块。
  • 遵循开闭原则:对修改进行关闭,对扩展进行开放,我们的代码要允许使用者进行扩展而不允许使用者进行修改,开闭原则建立在封装的基础上,只有在良好的封装的情况下才能保证代码没法被进行直接修改。

什么是好的封装?

  • 首先必须保证我们的内部变量外部不可见,也不能修改,也不能直接拿到,
  • 因为模块始终要和外部沟通,所以只是要让模块的内部变量不能被外部直接调用,因此我们要留出两种接口,第一种是留出给外部调用功能的接口,
  • 第二种是留出一定的扩展接口,方便别人去扩展这个模块的代码和功能。

企业微信截图_16641613726917.png

总结起来一共六个字:不可见、留接口

一、封装对象时的设计模式

我们要介绍三种帮助我们封装对象的设计模式,这三种设计模式能非常有效的帮助我们产出对象。为什么要在这里把对象和封装放在一起?原因是应用设计模式在产出对象的时候,其实已经达成了一种比较好的封装。

1.1. 创建对象时两种常用的设计模式

工厂模式:既然叫做工厂,其实就是创造一个专门用来创建对象的工厂,我们通过这个工厂来获取对象,从而代替我们使用new操作符或者是代替我们通过手写来生成对象。

目的:方便我们大量创建对象。

应用场景:工厂肯定是要大批量的产出某样东西,所以说当你需要创建的对象需要大批量生成时,应该首先想到工厂模式。比如我们项目中有很多的弹窗,我们应该把我们弹窗组件封装成工厂模式提供给别人使用,当别人需要调用弹窗的时候只需要调用工厂就能拿到弹窗对象了。

建造者模式:工厂模式用来大量产出类似对象,这些对象可能只是内容不同,其它部分大致相同,而建造者模式截然相反,建造者我们首先想到造房子,造房子肯定是精细化建造,不可能大批量产出房子,所以说建造者模式其实是一种精细化构建的思想。

目的:指导我们通过组合构建一个复杂的全局对象

应用场景:当需要创建单个、庞大的可能并不需要大量产出的组合对象时,就可以考虑使用建造者模式,它与工厂模式分别对应于编程中产出对象的两个方面。例如:写一个复杂的轮播图,轮播图一般一个页面只有一个,而轮播图有各种各样的动画效果以及各种复杂的交互,此时采用建造者模式封装代码是非常好的。

以上是两个帮助我们创建对象的设计模式,在创建完对象之后往往需要一些特殊性的需求,例如:需要保障对象在全局有且仅有一个

1.2. 创建全局仅有一个对象的设计模式

单例模式:为什么要确保全局有且只有一个对象呢?因为在很多情况下,我们为了避免出现多个对个对象之间发生干扰,此时我们往往需要保证全局只有一个对象。

目的:确保全局有且仅有一个对象

应用场景:为了避免重复新建,避免多个对象存在互相干扰。例如:当在项目中重复的去new同一个类,会导致多个对象之间存在干扰,这个时候可以考虑使用单例模式。

二、基本结构

2.1. 工厂模式的基本结构

工厂模式就是写一个方法,只需要调用这个方法,就能拿到你要的对象,例如:

企业微信截图_16641617711995.png

Factory方法就是工厂模式要写的工厂,工厂要做的事情非常简单,通过参数(本例中为type)告诉工厂我要什么样的对象,在工厂内部根据参数判断要什么对象,然后在内部创建好对象并返回。

2.2. 建造者模式的基本结构

建造者模式是把一个复杂的类各个部分拆分成独立的类,然后再在最终类里组合到一块,例如:

企业微信截图_16641619824839.png

建造者模式突出建造,像我们造房子一样,我们会用预制好的板、梁这些预制好的东西来造成我们的房子,因为建造者模式的内部结构比较复杂,所以在写建造者模式的时候它的内部会有很多别的类组成,就像上面代码所写的Model1、Model2,可以把它们看做建房子的板和梁,最终拿出去给别人使用的类会由Model1、Model2在内部组合而成,也就是我们说的把一个复杂的类拆分成独立的类,然后再组合到一起形成最终使用的类,Final为最终给出去的类。

2.3. 单例模式的基本结构

单例模式是通过定义一个方法,使用时只允许通过此方法拿到存在内部的同一实例化对象,例如:

企业微信截图_16641620408085.png

单例模式的做法并不是很固定,更重要的是要记住它全局只有一个对象的思想,例如代码示例中的Singleton就是一个作为单例来实例化的一个对象,Singleton对象下挂载一个getInstance方法,只能通过这个方法来获取这个类的实例化对象。这个方法里面先判断this上有没有instance属性,如果有直接返回这个属性,如果没有就把这个属性赋值为实例化的Singleton并返回。这样我们通过调用getInstance方法来拿到实例对象,如果已经实例化过了就会拿到之前实例化的对象,如果没有实例化过,就会把这个类实例化。

要实现一个标准的单例模式并不复杂,无非是用一个变量来标志当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象。

三、应用代码示例

3.1. 工厂模式的示例

3.1.1. 多彩的弹窗

需求:项目中有一个弹窗需求,弹窗有多种,它们之间存在内容和颜色上的差异。

假如我们有一个info弹窗、一个confirm弹窗、一个cancel弹窗

企业微信截图_16641621682658.png

如果我们需要创建3个info弹窗、三个confirm弹窗、三个cancel弹窗,分别有不同的内容和颜色,在没有工厂模式的情况下,我们很有可能会这样做:创建三个Info弹窗,new infoPop然后传入内容和颜色,然后复制粘贴一大堆,这样写就很麻烦。如:

企业微信截图_16641621905480.png

我们用工厂模式改造一下

企业微信截图_1664162211581.png

创建一个pop工厂,然后传入弹窗类型,内容和颜色参数,在工厂里面判断要什么类型的弹窗就返回什么类型的弹窗。这样创建工厂之后我们再去创建弹窗对象只需要告诉工厂需要什么类型的弹窗即可。

企业微信截图_1664162241913.png

代码写成这样之后就可以把这部分代码作为插件的代码封装起来,封装很简单,把代码放入匿名自执行函数中,然后指向外部暴露工厂即可(挂载在全局对象下),外部的使用者就可以直接调用工厂而不用关心具体要new哪个弹窗。

企业微信截图_16641622838530.png

如果有很多个弹窗就可以把弹窗配置成数组,数组中定好要什么弹窗,弹窗的内容和颜色,然后循环数组调用工厂即可。

企业微信截图_16641623088655.png

上面的弹窗工厂还存在一个问题,如果使用者在不知道的情况下去new了这个工厂,这样可能就不太好使了,所以我们改造一下工厂,判断this是不是弹窗工厂pop,如果是就代表使用者使用了new操作符,此时就给它new一个实例,实例为this[type](content, color),如果不是就代表使用者是直接调用的,就去new一下弹窗工厂pop,让它再走到流程中去。

企业微信截图_16641623709512.png

此时就不用switch去进行判断了,可以把switch代码都删掉,把infoPop、confirmPop、cancelPop这些方法都挂载到pop工厂的prototype上面去。

企业微信截图_16641623903235.png

这样无论以后再要扩展不同类型的弹窗也好,还是需要减少弹窗也好都会方便很多,因为我们只需要修改一下原型链即可,而对于之前这种switch的写法,我们需要修改一下switch还要加一层判断,换成这种写法我们只需要修改prototype就可以了。这种就是加了健壮性判断和可扩展性的工厂代码。

3.1.2. 源码示例-jQuery

需求:jQuery需要操作dom,每一个dom都是一个jQuery对象。

jQuery把dom包装成jQuery对象来方便我们操作dom,我们要操作这么多dom,如果每一个jQuery对象都需要new的话,这样的操作就十分麻烦,所以说jQuery本身的封装采用的是工厂模式,我们只需要调用jQuery的$方法就能拿到jQuery对象,jQuery的源码内部是怎么构造这个工厂的?

企业微信截图_16641624864541.png

首先在外层以一个匿名自执行函数将代码封装起来,然后在自执行函数内部定义一个暴露出去的jQuery方法,它接收一个选择器以及上下文,再把jQuery方法作为一个$符号和jQuery挂载到window对象上,也就是把工厂挂载到window上。

jQuery工厂内部返回了一个jQuery.fn下的init方法,也就是说其实拿到的对象是通过jQuery.fn.init创建的实例对象,这里之所以不去new jQuery类本身,是因为会形成无限循环的递归。

企业微信截图_16641625045517.png

然后让jQuery.fn等于jQuery.prototype,因为所有的方法都会挂载到prototype下,这里利用引用的特点让fn等于prototype,引用了prototype上所有的方法。然后在prototype下创建init方法。

企业微信截图_16641625305181.png

既然我们最终拿到的实例化对象是init的实例化对象,所以说init类的原型链要和jQuery本身的原型链等价,所以再把jQuery.fn.init.prototype等于jQuery.fn,也就相当于等于jQuery的prototype

企业微信截图_16641625517599.png

相对于jQuery的整个架构而言,它的各种各样的方法和模块是怎么扩展的呢?

它有一个extend方法,这个方法的作用是拷贝,如果只传一个对象它会把这个对象拷贝到jQuery上面

企业微信截图_16641625711975.png

代码示例中拷贝到jQuery的fn上面也相当于拷贝到它的原型链上面,也相当于拷贝到了init的原型链上面,然后各种各样的模块就可以通过extend拷贝进去。比如css方法、animate方法等等都会通过这个方法拷贝进去。

以上就是jQuery架构上的实现,其实就是一个工厂模式,只不过它去构造工厂的方式和我们前面演示的代码有点差别,但它始终逃不出一点就是用调用方法的方式自动给我们想要的对象来代替我们去new想要的对象。这样带来的好处第一个就是方便我们操作,像jQuery中大量操作dom的情况下没必要一个个去new它,第二个就是我们可以没必要去具体了解要new哪个,只需要告诉工厂需要哪个就可以了。

通过以上两个案例,不难发现工厂模式就是把真正需要暴露的对象先封装起来,然后只暴露一个工厂方法,让使用者通过这个工厂方法来获取对象,它的优势在于方便我们大量创建对象。

3.2. 建造者模式的示例

3.2.1. 编写一个编辑器插件

需求:有一个编辑器插件,初始化的时候需要配置大量参数,而且内部功能很多。(搭建架子,细节不实现)

编辑器插件的功能很多,比如我们可以前进后退、编辑字体颜色等等功能,面对这样一个复杂的编辑器插件,我们使用建造者模式是非常合适的,因为我们要写的编辑器插件:

  1. 只需要少量的编辑器对象,通常来说一个编辑页面只需要一个编辑器;
  2. 它初始化的时候需要大量参数,这样大量的参数让工厂去解析的话会花费大量的时间;
  3. 它的内部功能比较复杂,可能由多模块组成。这时候我们就需要一个精细化构建—建造者模式。

编辑器会有一个最终的类,也就是说使用的时候需要new这个类(本例为Editor类),建造者模式是把它的模块拆分成独立的类然后再组合起来,要完成我们的功能,需要拆分出哪些类?

  • 首先需要一个初始化的类,因为我们的编辑器插件要有html结构,所以我们创建initHTML类;
  • 然后创建控制字体大小、颜色的类fontControll;
  • 我们还有前进后退功能,这个时候就需要一个状态管理类来管理当前内容是什么状态,所以创建状态管理类stateControll

企业微信截图_16641627721578.png

模块都拆出来之后,再去给这些类定义方法

  • 比如initHTML类上会有initStyle方法初始化编辑器的样式,会有最终将编辑器渲染成dom的renderDom方法
  • 比如fontControll类上会有改变字体颜色的方法changeColor,改变字体大小的方法changeFontSize
  • 比如状态管理类stateControll上要有保存状态的方法saveState、回滚状态的方法stateBack、前进状态的方法stateGo

企业微信截图_16641628037483.png

image.png

企业微信截图_1664162826201.png

我们把最终给别人使用的类Editor挂载到window对象上

image.png

然后将拆分出来的类在Editor类中组合起来

image.png

当我们把这些独立的模块都构建进Editor类之后,这些模块就都可以互相调用了。比如我们现在增加一个状态回滚的功能,我们只需要拿出当前状态。

首先在stateControll类中定义一个state数组专门用来存储前进以及后退的状态,然后创建状态指针,指向当前的状态

image.png

在状态回滚方法stateBack中,只需要从状态数组里面取出当前状态的上一个状态,然后调用字体控制模块fontControll改变字体

image.png

在这样的一种组织之下,各种模块之间的沟通会变得很明晰,一个复杂功能的插件被我们解析成了几个独立的小插件,最后再组合起来,这样既方便我们编写代码,也方便我们去组织模块之间的沟通。保持了模块间的低耦合,从而形成高效沟通和高效编程,这就是建造者模式的目的。

3.2.2. Vue的初始化

需求:vue内部模块众多,而且过程复杂,有需要可以自己去阅读源码。

Vue内部的建造方式也可以看成是建造者模式,它的建造过程如下:

首先创建Vue类,为了防止使用者不通过new操作符调用它,需要在它的内部使用instance进行判断this是不是Vue,如果不是代表没有使用new操作符,此时需要抛出警告告诉使用者没有使用new操作符,如果是则调用init方法,将用户在初始化的时候后传入的配置参数options传进去完成一个配置的初始化。

image.png

vue相关的很多功能比如生命周期、事件系统、渲染函数、都是怎么注入到这个极其简单的Vue类中的呢?

vue源码中调用了一系列的初始化方法进行混入,例如:

image.png

通过调用这些方法混入到Vue类,和上例中将模块独立为一个个类最后放到Editor类中是一样的道理,只不过Vue中将写法改成了方法调用,而上例中直接在构造函数中写入。

Vue类本身非常简单,它所有的功能都是独立开发然后通过一系列的混入完成的,由此可见Vue使用的也是建造者模式来构建对象的。

3.3. 单例模式的示例

3.3.1. 写一个数据储存对象

需求:项目中有一个全局的数据存储者,这个存储者只能有一个,不然会需要进行同步,增加复杂度。

在一个多方工作的情况下如果有使用者不小心又new了这个对象,就会导致数据可能会产生两边的不一致,就需要同步这些数据,无形之中增加了复杂度,这样一个全局储存对象必然要变成单例模式。

假设我们的全局储存对象为store,首先创建store类,然后在内部创建store变量用于存储数据,我们需要保证无论怎么new store类只能返回同一个对象,可以通过store下的install属性来判断,如果有这个属性就直接返回该属性,如果没有就把store.install属性赋值为this,this在使用new操作符的时候就指向store本身。然后我们在store类的外部将install属性初始化为null,代码示例如下:

image.png

通过以上代码可以发现,无论怎么new store类拿到的都是同样的对象,例如:

image.png

代码示例中创建了两个store,因为它们都指向同一个对象,当修改s1的属性时,s2的属性也改变了

image.png

此时如果去掉单例模式验证一下,将store类的代码修改为:

image.png

运行结果如下:

image.png

此时因为每次new都会创建新的对象,所以s1和s2不指向同一个对象,导致数据要维护两份,单例模式的目的就是减少多个对象的干扰,使我们编程更加简单。

3.3.2. vue-router源码解析

需求:vue-router必须保障全局有且仅有一个,否则会错乱。

vue-router源码里怎样保证全局只有一个的?代码如下:

image.png

上面代码就是防止vue-router重复注册的代码,我们在使用vue-router的时候会调用vue.use方法去注册vue-router,其实在调用vue.use的时候就会去执行install方法,所以只要保证install方法每次在调用的时候判断一下是否已经被use过了,如果已经use过了就不执行后面的代码。

它的实现方式也很简单,在外层定义一个_Vue变量,install方法每次都会接收到一个参数Vue,这个参数就是Vue的类,然后内部判断了install方法下面有没有installed属性并且_Vue等于Vue类,就直接return不执行后面的内容。如果没有installed属性就说明没有被赋值过,将installed属性设为true,再将外面定义的_Vue变量赋值为Vue类,这样在下次调用install方法的时候就直接进入判断条件中断执行了。这跟我们前面演示的单例模式非常类似,都是通过方法的一个属性来判断,vue-router中额外加了一个判断也就是外部的_Vue变量是否等于传进来的Vue类,通过多重判断保障代码安全性。

下一篇:设计模式—关于如何提高代码复用

本文由博客一文多发平台 OpenWrite 发布!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_404188.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

神经网络图像识别技术,神经网络指纹识别

1、声纹识别技术未来的发展趋势如何? 近几年来,我国生物识别技术行业市场主体数量呈迅速增长的趋势,截至目前,行业企业数量超4000家。据统计,2013-2018年,我国生物识别技术行业新增企业数量呈逐年增长的趋…

【编程题】【Scratch四级】2022.06 成绩查询

成绩查询 期末考试结束了,小朋友想知道自己考试的成绩和班级排名,让我们一起来实现这个功能吧! 1. 准备工作 (1)保留默认白色背景和小猫角色; (2)创建名为“姓名”和“成绩”的列表,按照图1输入相关内容。 2. 功能实现 (1)点击小绿旗,小猫询问“你要查询谁的成…

JS(第十课)JS中的对象

Com.Java.Basis第九课 《类与对象》_星辰镜的博客-CSDN博客 有兴趣的可以去看一下 本文内容核心:对象 在百度翻译中对象称为object 安装插件 第一部分去找找在JS中对象的语法是什么 如何去创建对象的,调用对象! var name"李四"; var people…

图形学-(视图变换,投影变换)

1.视图变换 在 3 维物体变到二维平面的过程中,我们需要规定好相机的位置。对于相机所做的变换就是视图变换 (Viewing/Camera transformation)。 我们需要对相机位置进行定义,对于一个相机我们要规定下面三个属性: 相…

Transformer3

又是 一篇关于Transformer的~~ 太多了 本文结合Transformer和ConvNets的优点,构造了一种新的即插即用运算符ParC,可以高效地部署在不同的平台上。一个Trick 搞定 CNN与Transformer,即插即涨点即提速! 论文链接:http…

Ansible自动化运维工具介绍与部署

ansible自动化运维工具介绍与部署 文章目录一、什么是自动化运维?二、常用的自动化运维工具2.1 Ansible2.2 SaltStack2.3 Puppet2.4 三种自动化工具特点对比三、介绍Ansible3.1 什么是 Ansible?3.2 Ansible 无需代理3.3 Ansible 方式四、部署Ansible4.1 控制节点4.…

U-BOOT小全(三):SPL框架

1、什么是SPL? 为了可以使已有的所有SPL的设计统一,也为了简化添加适用于新板子的设计,专门设计一个通用的SPL框架。在SPL框架下,一个板子的所有代码都能够被重用。代码复制和链接不再是必要的。 在uboot-2011的/doc/README.spl…

跨境电商必读:什么是社交媒体营销?

关键词:跨境电商,社交媒体营销 社交媒体渠道已迅速成为业务发展战略不可分割的一部分,关于社交媒体如何重塑消费者行为,以及组织如何利用它获得收益,网络上已经有大量相关信息,但仍有许多跨境电商企业难以…

H3C VLAN简单配置

将两台电脑都设为自动获取IP&#xff0c;然后左边的加入vlan10,右边的加入vlan20 先配置左边交换机 <H3C>system-view [H3C]user-interface console 0 [H3C-line-console0]idle-timeout 0 0 [H3C-line-console0]exit [H3C]vlan 10 [H3C-vlan10]port GigabitEthernet 1/…

04-Jenkins构建Maven项目

1&#xff09;Jenkins项目构建类型 a&#xff09;Jenkins构建的项目类型介绍 Jenkins中自动构建项目的类型有很多&#xff0c;常用的有以下三种&#xff1a;自由风格软件项目&#xff08;FreeStyle Project&#xff09;Maven项目&#xff08;Maven Project&#xff09;流水线项…

沉睡者IT - 如何识别NFT“洗盘交易”?

推荐阅读1&#xff1a;【创业粉引流变现项目】 推荐阅读2&#xff1a;【抖音网上如何赚钱变现】 推荐阅读3&#xff1a;【中视频横版16:9视频制作教程】 对金融人士来说&#xff0c;“洗盘交易”&#xff08;wash trading&#xff09;并不是一个新词。加密货币也以相同的买入…

grid管理的数据库实例修改时区问题

问题描述 由于安装创建数据库之前没将操作系统的时区设置为本地正确的时区&#xff08;Pacific/Apia&#xff09;&#xff0c;使用的是GMT时间&#xff08;UTC时区&#xff09;&#xff0c;后续将操作系统和数据库修改成正确时区之后&#xff0c;发现sysdate显示有误。 服务端…

计算机网络基础 ---- 动态路由---OSPF协议----详解

OSPF协议&#xff1a; ospf&#xff1a;开放式最短路径优先协议&#xff08;Open Shortest Path First&#xff09; 一、基本概念&#xff1a; 是携带真实掩码&#xff08;无类别链路状态IGP协议&#xff09;&#xff1b;周期更新&#xff08;30min&#xff09; 触发更新 -…

Taichi 加速 Python 中图像处理

Taichi 咱来说一说哈 在计算机视觉&#xff08;特别是深度学习图片预处理、模型训练&#xff09;等复杂度较高&#xff0c;技术迭代速度快的领域&#xff0c;用 Python 快速开发出算法原型、验证效果是许多研发人员的首选方案。著名图像处理库 OpenCV 就提供了完整的 Python 封…

Ubuntu Linux 操作系统-清华大学开源软件镜像站下载

Ubuntu 是一个由全球化的专业开发团队建造的操作系统。它包含了所有您需要的应用程序&#xff1a;浏览器、Office 套件、多媒体程序、即时消息等。Ubuntu 是一个 Windows 和 Office 的开源替代品。 Ubuntu 的名称来自非洲南部祖鲁语或豪萨语的 “ubuntu” 一词&#xff08;译为…

总结超参数最优化

1.超参数是什么&#xff1f; 超参数是指的是比如SGD的一个学习率&#xff0c;或者是抑制过拟合时候的一个权重衰减率参数等等&#xff0c;这些参数需要一个合适的值才能很好的提升神经网络的学习效率 2.我们在进行超参数最优化的时候如何去做到超参数最优化&#xff1f; 2.1.设…

盘点十大国内外热门的SaaS点评平台

在B端市场里&#xff0c;企业如何调研、选择企服软件是困扰已久的问题。使用者需要选择合适的企服软件去提升企业内部效率&#xff0c;因此&#xff0c;选择一个合适的SaaS点评平台便非常重要。在SaaS点评平台&#xff0c;使用者可以了解、选择产品&#xff0c;而服务商可以收获…

撰写SCI论文好用的免费工具(下) - 易智编译EaseEditing

书接上文。易智编译再推荐几种好用的写作工具&#xff1a; 7.Grammarly Grammarly是谷歌浏览器的一个扩展程序&#xff0c;可在浏览器任一窗口中检查你文字的拼写和语法。无论是使用Google文档编写&#xff0c;撰写电子邮件还是发布文章&#xff0c;Grammarly都会指出英语错误…

CNN的识别机制

搬来这个给自己学学~~ 所有基础都要写一个 大佬勿怪 只为自己学习 近年来&#xff0c;CNN 因其优异的性能&#xff0c;在计算机视觉、自然语言处理等各个领域受到了研究者们的青睐。但是&#xff0c;CNN 是一个 「黑盒」 模型&#xff0c;即模型的学习内容和决策过程很难用人类…

Blazor组件自做十 : 光学字符识别 OCR 组件

光学字符识别 OCR 组件 演示地址 https://blazor.app1.es/ocr使用方法手机或者电脑点击拍照OCR可启动相机拍照,或者点击文件OCR选择文件,稍等片刻即可获得OCR结果.直接输入Url可识别在线图片AI表格识别 AI Form 演示地址 https://blazor.app1.es/aiform