2023年4月

在前面介绍的随笔《
基于SqlSugar的开发框架循序渐进介绍(7)-- 在文件上传模块中采用选项模式【Options】处理常规上传和FTP文件上传
》中介绍过在文件上传处理的过程中,整合了本地文件上传和基于FTP方式的上传文件的处理整合。本篇随笔继续介绍文件上传的处理,基于选项模式【Options】方式整合基于阿里云OSS对象存储的处理方式。

1、选项模式【Options】的处理

文件上传处理应该由程序进行配置,决定使用那种方式,那么这里面我们为了弹性化处理, 在文件上传模块中采用选项模式【Options】处理常规上传和FTP文件上传的配置参数信息。

微软引入
选项模式
,它是用于配置框架服务使用的设置. 选项模式由
Microsoft.Extensions.Options
NuGet包实现,除了ASP.NET Core应用,它还适用于任何类型的应用程序,如果需要了解,
微软的文档
详细解释了选项模式。

选项模式的限制之一是你只能解析(注入)
IOptions <MyOptions>
并在依赖注入配置完成(即所有模块的
ConfigureServices
方法完成)后获取选项值。如果你正在开发一个模块,可能需要让开发者能够设置一些选项,并在依赖注入注册阶段使用这些选项. 你可能需要根据选项值配置其他服务或更改依赖注入的注册代码。IOptions<>是单例,因此一旦生成了,除非通过代码的方式更改,它的值是不会更新的。

之前介绍过的文件上传处理,

一个是本地文件处理,一个是FTP文件处理,它们选择那种方式,依赖于配置参数的信息,如下示意图所示。

当我们增加了对象存储OSS方式后,可以扩展下这个图示如下所示。

在本地文件处理过程中,如果是Web API方式调用服务层,那么就在Web API所在的文件系统中,如果是Winform界面直接调用服务层,那么就是在当前系统中处理文件,这种方式可以有效的管理我们的文件信息。

在FTP文件处理过程中,则是根据选项参数的信息,调用FluentFTP类库进行文件的上传操作。

在OSS对象存储处理过程中,我们一般基于阿里云、腾讯云等这些云服务OSS的处理方式,一般它们会提供相应开发语言的SDK,我们引用并进行整合即可。

基于选项模式,根据我们的处理方式,我们定义一个对象,用于承载上传参数的信息,如下代码所示。

    /// <summary>
    ///文件上传处理的选项信息/// </summary>
    public classUploadSettingOptions
{
/// <summary> ///可指定的存储物理地址,如C:\\Attachment,如果没有配置项AttachmentBasePath,则默认一个相对目录。/// </summary> public string AttachmentBasePath { get; set; }/// <summary> ///指定附件上传的方式,如ftp为FTP方式,为空则为普通方式/// </summary> public string AttachmentUploadType { get; set; }/// <summary> ///FTP上传配置/// </summary> public FtpProviderOptions FtpProvider { get; set; }/// <summary> ///OSS存储的配置/// </summary> public OSSProviderOptions OSSProvider { get; set; }
}

其中本地介绍OSS存储处理的配置选项对象如下所示。

    /// <summary>
    ///OSS对象存储配置选项/// </summary>
    public sealed classOSSProviderOptions
{
/// <summary> ///指定提供商/// </summary> public OSSProvider Provider { get; set; }/// <summary> ///终结点/// </summary> public string Endpoint { get; set; }public string AccessKey { get; set; }public string SecretKey { get; set; }public string Region { get; set; }public bool IsEnableHttps { get; set; } = true;public bool IsEnableCache { get; set; }public string Bucket { get; set; }
}

对应WebAPI的appSettngs.json文件配置项内容如下:

2、阿里云的OSS存储设置处理

对于阿里云的OSS来说,对应的AccesKey和SecretKey自己可以通过查看账户的信息可以获取到。

我们开始需要添加一个Bucket节点,用来存放相关的文件,并且需要开启对应的权限,如下所示。

一旦正式允许可以上传文件,那么文件存储在对应的Bucket项目中,如下所示。

准备好这些前期工作后,我们如果需要在WebAPI端进行OSS存储的处理,可以利用阿里云的OSS 的SDK进行处理即可,如下所示。

SDK的常规使用很简单,我们可以参考它的
aliyun-oss-csharp-sdk
案例来上传处理即可。

例如它的上传文件的处理代码如下所示:

    OssClient client = newOssClient(endpoint, accessKeyId, accessKeySecret); 
client.PutObject(bucketName, key, filePathToUpload);

在文件处理的通用上传处理函数里面,我们根据配置内容选择不同的上传模式。

        /// <summary>
        ///上传文件(根据配置文件选择合适的上传方式)/// </summary>
        /// <param name="info">文件信息(包含流数据)</param>
        /// <returns></returns>
        public async Task<CommonResult>Upload(FileUploadInfo info)
{
var uploadType = this._options.AttachmentUploadType;if (string.IsNullOrEmpty(uploadType))
{
return await this.UploadByNormal(info);
}
else if (uploadType.Equals("ftp", StringComparison.OrdinalIgnoreCase))
{
return await this.UploadByFTP(info);
}
else if(uploadType.Equals("oss", StringComparison.OrdinalIgnoreCase))
{
return await this.UploadByOSS(info);
}
else{throw new ArgumentException("AttachmentUploadType配置指定了无效的值, 请置空, 填写ftp或者填写oss。");
}
}

由于本篇主要介绍OSS的对象存储方式,因此我们来看看对应的OSS上传处理的操作。

和其他上传(如FTP上传)方式类似,我们也是构建对应的信息后调用OSS的SDK处理即可,如下代码所示。

/// <summary>
///使用阿里云的OSS服务上传文件/// </summary>
/// <param name="info"></param>
/// <returns></returns>
public async Task<CommonResult>UploadByOSS(FileUploadInfo info)
{
var result = newCommonResult();var ossOptions = this._options.OSSProvider;if(ossOptions != null)
{
//上传获得的相对路径 string category =info.Category;if (string.IsNullOrEmpty(category))
{
category
= "Photo";
}
//OSS的文件路径不能以正斜线(/)或者反斜线(\)字符开头。//确定日期时间目录(格式:yyyy-MM),不存在则创建 string savePath = string.Format("{0}-{1:D2}/{2}", DateTime.Now.Year, DateTime.Now.Month, category);var ext =FileUtil.GetExtension(info.FileName);var finalName = string.Format("{0}{1}", Guid.NewGuid().ToString(), ext);//FileUtil.GetFileName(file); var filePath = string.Concat(savePath, "/", finalName);var stream =FileUtil.BytesToStream(info.FileData);//使用阿里云的OSS服务上传文件 var client = new OssClient(ossOptions.Endpoint, ossOptions.AccessKey, ossOptions.SecretKey);
client.PutObject(ossOptions.Bucket, filePath, stream);
//基础路径和部分路径 info.BasePath = $"{(ossOptions.IsEnableHttps ?"https":"http")}://{ossOptions.Bucket}.{ossOptions.Endpoint}";
info.SavePath
=filePath;
info.AddTime
=DateTime.Now;


result.Success
= await base.InsertAsync(info);if(result.Success)
{
//记录返回值, Data1为具体的URL路径//生成外链地址 方便前端预览 result.Data1 =info.BasePath.UriCombine(savePath);
result.Data2
=savePath;
}
else{
result.ErrorMessage
= "数据写入数据库出错。";
}
}
else{
result.ErrorMessage
= "OSS配置信息不正确或没有启用";
}
returnresult;
}

最终我们上传的文件列表如上图所示。

3、在Vue3+ElementPlus+Vite的前端项目上测试前端上传文件的处理

我们在一个前端的编辑器
@wangeditor
上整合图片上传的处理来展示这个OSS对象保存的操作。案例需要前端项目安装对应的插件,如下所示。

前端页面引用插件对象的代码如下所示

    <div class="wangeditor">
      <Toolbar style="border-bottom: 1px solid #ccc" :editor="editorRef" :defaultConfig="toolbarConfig" :mode="mode" />
      <Editor style="height: 500px; overflow-y: hidden" v-model="valueHtml" :defaultConfig="editorConfig" :mode="mode" @onCreated="handleCreated" />
    </div>

在脚本代码中引入对象的样式和对象。

<scriptsetup lang="ts">import'@wangeditor/editor/dist/css/style.css';//引入 css
import { Editor, Toolbar } from'@wangeditor/editor-for-vue';


defineOptions({
name:
'Editor'});

const mode
= 'default';//编辑器实例,必须用 shallowRef const editorRef=shallowRef();

为了配置上传图片文件的处理操作,我们需要根据编辑器的介绍,以及我们WebAPI端的处理函数定义,在脚本部分中增加对应的配置信息,如下代码所示

const editorConfig ={
placeholder:
'请输入内容...',
MENU_CONF: {
//配置默认字号 //配置上传图片 uploadImage: {//上传图片请求接口路径 server: '/api/FileUpload/PostUpload',//后端接收的文件名称 fieldName: 'multipartFile',
maxFileSize:
10 * 1024 * 1024, //上传图片10M //上传的图片类型 allowedFileTypes: ['image/*'],//自定义增加 http header headers: {
Authorization:
'Bearer ' +getAccessToken()
},
//自定义上传参数,例如传递验证的 token 等。参数会被添加到 formData 中,一起上传到服务端。 meta: {
guid:
'',
folder:
'文档图片'},//将 meta 拼接到 url 参数中,默认 false metaWithUrl: false,//小于该值就插入 base64 格式(而不上传),默认为 0 base64LimitSize: 10 * 1024, //10MB //自定义上传图片返回格式【后端返回的格式】 customInsert(res: any, insertFn: InsertFnType) {
console.log(res);
if (!res.success) {
ElMessage.error(
'上传文件失败,' +res.error);return;
}
//从 res 中找到 url alt href ,然后插入图片 ,根据后端实际返回的字段来 var result = res.result as Array<ResponseFileInfo>;if (result.length > 0) {
console.log(result[
0]);
insertFn(result[
0].url, result[0].name, result[0].url);
}
},
//单个文件上传成功之后 onSuccess(file: File, res: any) {if(res.success) {
ElMessage.success(`${file.name} 上传成功`);
return;
}
else{
ElMessage.warning(`${file.name} 上传出了点异常`);
return;
}
},//单个文件上传失败 onFailed(file: File, res: any) {
console.log(res);
ElMessage.error(`${file.name} 上传失败`);
},
//上传错误,或者触发 timeout 超时 onError(file: File, err: any, res: any) {
console.log(err, res);
ElMessage.error(`${file.name} 上传出错`);
}
}
}
};

其中主要的就是需要增加授权的令牌头部信息,以及对应上传需要用到的参数,还有就是返回的接口对象里面,需要根据Web API的定义,然后转换为本地识别的类别进行展示即可,如定义的
customInsert 函数

文件上传成功后,可以看到在编辑器中插入了对应的图片文件,图片地址指向的是阿里云OSS的路径。

在数据库的文件记录中,我们可以看到对应的路径信息,如下所示。

如果我们需要在项目中实现业务的图片上传功能,如下界面所示的效果。

可以参考随笔《
基于SqlSugar的开发框架循序渐进介绍(13)-- 基于ElementPlus的上传组件进行封装,便于项目使用
》介绍内容进行自定义上传文件的处理即可。

<el-form-itemlabel="资料文档">
     <my-uploadv-model="viewForm.attachGUID":data="{ guid: viewForm.attachGUID, folder: '用户图片' }"disabled/>
 </el-form-item>

以上就是我们针对不同的上传方式处理的操作,对于OSS的文件上传,我们通过整合阿里云的OSS的SDK,直接调用进行处理即可,非常方便,对于其他类似的平台,也可以针对它们提供的SDK进行扩展即可。

SqlSugar开发框架介绍:
https://www.iqidi.com/Framework/sugarIndex.htm

系列文章:


基于SqlSugar的开发框架的循序渐进介绍(1)--框架基础类的设计和使用


基于SqlSugar的开发框架循序渐进介绍(2)-- 基于中间表的查询处理


基于SqlSugar的开发框架循序渐进介绍(3)-- 实现代码生成工具Database2Sharp的整合开发


基于SqlSugar的开发框架循序渐进介绍(4)-- 在数据访问基类中对GUID主键进行自动赋值处理


基于SqlSugar的开发框架循序渐进介绍(5)-- 在服务层使用接口注入方式实现IOC控制反转


基于SqlSugar的开发框架循序渐进介绍(6)-- 在基类接口中注入用户身份信息接口


基于SqlSugar的开发框架循序渐进介绍(7)-- 在文件上传模块中采用选项模式【Options】处理常规上传和FTP文件上传


基于SqlSugar的开发框架循序渐进介绍(8)-- 在基类函数封装实现用户操作日志记录


基于SqlSugar的开发框架循序渐进介绍(9)-- 结合Winform控件实现字段的权限控制


基于SqlSugar的开发框架循序渐进介绍(10)-- 利用axios组件的封装,实现对后端API数据的访问和基类的统一封装处理


基于SqlSugar的开发框架循序渐进介绍(11)-- 使用TypeScript和Vue3的Setup语法糖编写页面和组件的总结


基于SqlSugar的开发框架循序渐进介绍(12)-- 拆分页面模块内容为组件,实现分而治之的处理


基于SqlSugar的开发框架循序渐进介绍(13)-- 基于ElementPlus的上传组件进行封装,便于项目使用


基于SqlSugar的开发框架循序渐进介绍(14)-- 基于Vue3+TypeScript的全局对象的注入和使用


基于SqlSugar的开发框架循序渐进介绍(15)-- 整合代码生成工具进行前端界面的生成


基于SqlSugar的开发框架循序渐进介绍(16)-- 工作流模块的功能介绍


基于SqlSugar的开发框架循序渐进介绍(17)-- 基于CSRedis实现缓存的处理


基于SqlSugar的开发框架循序渐进介绍(18)-- 基于代码生成工具Database2Sharp,快速生成Vue3+TypeScript的前端界面和Winform端界面


基于SqlSugar的开发框架循序渐进介绍(19)-- 基于UniApp+Vue的移动前端的功能介绍


基于SqlSugar的开发框架循序渐进介绍(20)-- 在基于UniApp+Vue的移动端实现多条件查询的处理


基于SqlSugar的开发框架循序渐进介绍(21)-- 在工作流列表页面中增加一些转义信息的输出,在后端进行内容转换


基于SqlSugar的开发框架循序渐进介绍(22)-- Vue3+TypeScript的前端工作流模块中实现统一的表单编辑和表单详情查看处理


基于SqlSugar的开发框架循序渐进介绍(23)-- Winform端管理系统中平滑增加对Web API对接的需求


基于SqlSugar的开发框架循序渐进介绍(24)-- 使用Serialize.Linq对Lambda表达式进行序列化和反序列化

基于SqlSugar的开发框架循序渐进介绍(25)-- 基于SignalR实现多端的消息通讯

基于SqlSugar的开发框架循序渐进介绍(26)-- 实现本地上传、FTP上传、阿里云OSS上传三者合一处理

当下,酝酿能量的超级边缘。

最近,我们在谈视频化狂飙、谈AIGC颠覆、谈算力动能不足,很少谈及边缘。但“边缘”恰恰与这一切相关,且越发密不可分,它是未来技术发展的极大影响因子。

“到2025年,超过70%的组织将为其⾄少⼀个边缘计算系统,部署超⼤规模云边缘解决⽅案,并会结合其云部署。这比例远⾼于2022年的不到15%。”

这是Gartner在最近发布的Competitive Landscape : Hyperscale Edge Solution Providers 报告中,给出的大胆又直接的判断。

Gartner视角中的“边缘”价值

“边缘是物和⼈与⽹络数字世界连接的物理位置,它是数字化转型,以及⼈、物和企业之间新交互的关键推动因素。”Gartner的描述,极度契合当下的边缘之力。

由边缘发展而来的边缘计算,正在呈现快速发展之势,它满⾜了对靠近边缘处理数据的⽇益增⻓的需求。它还通过解决延迟、带宽、⾃主性和隐私要求,来补充数字业务⽤例的云计算,这就顺势揭开了“边缘”的意义和能力。

提到云计算,固有印象一定是中心云。而边缘计算和中心云计算是相辅相成的,非相互竞争或排斥。下图显示,两种计算可以通过不同的⽅式解决不同的需求,从⽽创造新的价值。

中心云和边缘的关系

从中心到边缘,与Gartner所观察的一致,我们发现:

l “边缘”是趋于简单的解决路径

超⼤规模边缘正在成为在当今IT现代化和数字计划中,实现边缘计算以及解决数据主权需求的越来越简单的⽅法,因为用户在当今组织中的分布和普及程度越来越⾼。

l “边缘”是公共云的最强助力

公共云功能得到当今超⼤规模边缘的补充,许多情况下,它在利⽤云到边缘模型的同时,也提供与公共云区域相同的功能。

在此之上,涉及一个概念,分布式云。

分布式云是⼀种新兴技术趋势,推动云外⽅法。分布式云满⾜了客户让云计算资源更接近数据和业务活动发⽣的物理位置的需求,同时保留了公共云的集中管理能⼒。

分布式云连续体

从中心云到分布式云,从云计算到边缘计算,成就了边缘云的存在,可以说是一种最具亲和力的云计算存在形态,而其酝酿的价值一定在超大规模之上来发挥。

正如Gartner所述,“5G为通信服务提供商提供了⽀持新的边缘计算⽤例的机会,而超⼤规模云提供商正在竞相成为电信边缘基础设施的领先者。”

6家“全球超大规模边缘供应商”

Gartner提到,“随着超级边缘(也称为超⼤规模边缘)变得⼴泛可⽤,它们将在短期内与单个超⼤规模企业及其特许经营权相关联。这将产⽣对‘多重超⼤规模’边缘架构和采用模型的需求,类似于在云世界中的多云采⽤模型。”

在5G代开的连接通道之下,更打开了一种可以称之为“超级边缘”的边缘解决方案和产品新世界。

与传统边缘不同,超级边缘构建在超⼤规模云提供商提供的平台上,作为其分布式云解决⽅案的⼀部分。5G的⾼速、低延迟连接将许多功能卸载到这些超级边缘,⽽不是依赖集中式云数据中⼼来处理某些超级计算和分布式⼯作负载,这便解决了传统边缘一直面临的延时挑战,尤其可以应对云游戏和高性能计算的应用场景。

供应商使⽤不同的⽅法来开发新产品以满⾜这些新的边缘计算要求。然⽽,所有超⼤规模云提供商都基于相同的原则开发他们的产品,Gartner称之为“从云到边缘”的⽅法。

通过从云到边缘的⽅法,超⼤规模云提供商将公共云架构分发到边缘,从⽽实现中心和边缘之间的⼀致性。

从云端到边缘的方法

此次最新报告中,从全球范围内,Gartner指出了6家最具代表性的“全球超大规模边缘供应商”,分别为:阿里云、AWS、谷歌云、IBM、微软天青、甲骨文,这6家各具声色,为边缘云赛道带来独特价值。

报告中指出,阿里云的云内容分发⽹络 (CDN) 业务,提供在阿⾥云的云边缘节点服务 (ENS) 上运⾏的标准边缘计算服务。带有CDN的ENS,提供了⽐以前的解决⽅案更⾼的收益,以及横向服务扩展、运营和维护以及为客户提供的按需付费选项。

正如Gartner所描述,阿里云基于CDN节点构建的全球化边缘基础设施,通过500+节点结合算网融合技术,实现全球覆盖,聚焦在音视频、云游戏、云渲染、边缘组网、云网融合五类场景化解决方案。未来,更多的全球节点还会转变为边缘计算(ENS)模式,带来更大的覆盖与价值。

与此同时,Gartner也指出,AWS的边缘产品组合通过多种选项部署到边缘,为客户提供⼀种混合体验;Google分布式云(GDC)可以跨多个位置运⾏,包括⾕歌的⽹络边缘、运营商边缘、客户边缘和客户数据中⼼;IBM Cloud Satellite是IBM的出云解决⽅案,满⾜⾏业对以软件为中⼼的未来需求,以越来越灵活的⽅式将云服务扩展到不同的环境;微软⽀持多种边缘集成技术,包括连接、管理、安全和云原⽣开发,并给予边缘高度多样化的需求,且特别关注垂直合作伙伴关系的建立;Oracle构建了OCI分布式云,并提供⼀套包含五个分布式云产品的套件,向全球客户提供其分布式云服务。

从产品、功能的成熟度、市场的吸引力、市场的独特主张,Gartner提出的这6大全球超大规模边缘供应商,着力不同,呈现强劲的发展势头。

在全球释放“超级边缘”的能量

在全球构建超级边缘之力,绝非一件简单的事。

阿里云实现了国内31个省的全面覆盖,也就是说,用户在中国大陆的这些区域内都能获得充沛的算力支撑。在云计算基础设施服务方面,阿里云边缘云基于统一的飞天底座,提供一云多芯、一云多态的云计算架构,从中心向边缘辐射,让算力无限延伸。

作为中国最大、技术领先、全球覆盖的边缘云计算平台,阿里云边缘云在海外部署了60多个节点,覆盖海外30多个国家,以亚太区域为核心,在欧美、中东非等区域均能提供算力支持。

在边缘节点算力建设上,阿里云在全球提供500多个强有力的全节点,具备边缘云多态计算、融合存储、融合网络等所有云能力,通过合计170万核的通用算力CPU核,和400P FLOPS的异构算力核,以满足更多边缘创新场景的算力刚需

伴随全球业务发展,阿里云先后在韩国、泰国、德国、印尼、菲律宾和沙特新增了七座数据中心,面向广大的海外用户提供云服务。同时,边缘云小型轻量和快速交付的特性,使得其相较中心云而言,部署更为灵活、分布更为广泛。

在网络传输层,就近用户侧部署边缘云,可有利于优化异地运营商的网络接入;通过专属网络连接边缘云与中心云,则有利于为上层业务开展屏蔽边缘云与中心云之间的复杂网络环境。

在业务应用层,将中心化的云服务延展为云边协同的服务,不仅可以通过边缘算力和存储满足数据本地贮存的需求,还可以通过应用本地下沉、边缘就近访问等技术手段提升业务体验,对传统中心云的服务形成恰到好处的补充。

实践数据显示,在全球互联网用户增长最快、网络等基建水平相对落后的新兴市场国家,采用边缘云就近用户提供服务,可以将异地用户的业务访问时延缩短50%以上,从而有力应对“长距离传输”和“最后一公里接入”为业务开展带来的挑战。

随着互联网出海进度的加快,中国企业席卷全球的脚步由国内延伸至欧美、东南亚、印度,进而扩展至更广阔的拉美、中东和非洲市场。在视频直播、游戏、在线教育等领域,国内出海企业已日渐占据了海外市场的半壁江山。

得益于国内边缘云的商业化积累和与出海企业的长期深入合作,阿里云率先将边缘云的场景化服务能力由国内延伸至海外,致力于在更广阔的地域为出海企业提供与国内一致的技术支持和服务体验,助力企业业务全球版图的扩张。

写在最后

在全球,阿里云边缘云构建了泛在融合的算力网络,打造“一点接入、即取即用”的算力服务,为新兴企业、新兴场景注入真正落地的动力之源。

如今,全球都沸腾在AIGC的风潮里,而“边缘”也许会成为新内容形态的动能中心,它利用算网融合的能力,在边缘端和云边协同机制下,解决更复杂的渲染和AI计算,以更自如的算力,让AIGC更自如地普惠。

不仅如此,边缘算力所拥有的低时延、安全、隐私等优势,更符合对AI创作所有权和隐私权之需。而“边缘”对AIGC时代的助攻,也只是我们能预见的冰山一角。

可以确定的是,用户群日益分散,需求日新月异,欲望的极限不断被打破,在每个边缘端不断被满足,这一切都为超⼤规模边缘提供商创造⼀系列的机会。

最后,把视线抬高,边缘云更像是新兴技术下的影响雷达,众多颠覆性技术的蔓延之势,总离不开边缘的存在。

本文介绍在
Anaconda
环境下,安装
Python

栅格、矢量等地理数据处理库
GDAL
的方法。

需要注意的是,本文介绍基于
conda install
命令直接联网安装
GDAL
库的方法;这一方法有时不太稳定,且速度较慢。因此,如果有需要,大家可以参考
Anaconda环境GDAL库基于whl文件的配置方法
这篇文章中的方法,可以更快速地配置
GDAL
库。

首先,我们打开“
Anaconda Prompt (Anaconda)
”软件。

随后,将弹出如下所示的命令输入窗口。

在上述弹出的命令输入窗口中,输入以下代码:

conda install -c conda-forge gdal

随后,系统将自动搜索
GDAL
这一模块,并准备安装。

在这里有一点需要注意——也是我们在之前很多
Python
模块安装教程文章中提到的:如果我们开启了网络代理软件,则可能会导致系统找不到
GDAL
这一模块的元数据的下载地址,出现如下所示的错误提示。

针对这种情况,我们将网络代理软件关闭后,重新输入前述代码,即可解决问题。

另一方面,在我实际操作的过程中,发现在这一步骤里,配置环境环节进行得会稍微有些慢;但是稍等片刻还是可以正常配置完毕的。

待系统找到
GDAL
这一模块的元数据后,我们输入
y
即可开始下载、安装的过程。

成功完成下载与安装后,会出现如下所示的界面。

为了验证我们
GDAL
模块的安装是否成功,我们可以在编译器中尝试加载这一模块;若发现可以成功加载,则说明
GDAL
模块安装无误。

至此,大功告成。

其他章节请看:

低代码 系列

中后台集成低代码预研

背景

笔者目前维护一个 react 中后台系统(以
spug
为例),每次来了新的需求都需要前端人员重新开发。

前面我们已经对
低代码
有了一定的认识,如果能通过一个
可视化的配置页面
就能完成前端开发,将极大的提高前端(或后端)的效率。甚至能加快企业内部数字化(信息化)建设。

低代码介绍

低代码
这一概念由 Forrester 在 2014 年正式提出。低代码,顾名思义,就是指开发者写很少的代码,通过低代码平台提供的界面、逻辑、对象、流程等
可视化编排工具
来完成大量的开发工作,
降低
软件开发中的不确定性和复杂性。实现软件的高效构建,无需重复传统的手动编程,同时兼顾业务人员和专业开发人员的更多参与。

零代码
属于低代码平台的一种,不提供或者仅支持有限的编程扩展能力,技术门槛低,应用场景有限。

目标

最优 稍次
通过低代码平台配置系统
所有
前端页面
通过低代码平台配置系统
大部分
前端页面

预研产品

amis

amis
是百度的一个低代码前端框架,它使用
JSON
配置来生成页面,可以减少页面开发工作量,极大提升效率。开源
免费
,github Star 13.4k。

包含两个项目:amis 和 amis-editor(前些天
已开源
)。amis-editor 通过可视化形式生成页面,画原型的功夫就将前端页面给开发好了,最后生成该页面的配置(一个json),该配置放入 amis 解析出来就是一个前端页面。

amis 在线
编辑器
如下:

Tip
: amis 更过介绍请看
这里

amis api

爱速搭

爱速搭
是百度智能云推出的低代码开发平台,它灵活性强,对开发者友好,在百度内部大规模使用,有超过 4w 内部页面是基于它制作的,是百度内部中台系统的核心基础设施。支持私有部署,
收费

Tip
: amis 是爱速搭团队开源的前端低代码框架,爱速搭应用中的页面都是基于 amis 渲染的,同时爱速搭平台自身的绝大部分页面也是基于 amis 开发 ——
爱速搭与amis

lowcode-engine

阿里低代码引擎(
lowcode-engine
)是一款为低代码平台开发者提供的,具备强大定制扩展能力的低代码设计器研发框架。
免费

Tip
: 前面笔者也稍微实现了一个简单的
可视化编辑器
,有些麻烦,也有很多不足。真实场景倾向使用
成熟的编辑器

钉钉宜搭

钉钉宜搭
是阿里巴巴自研的低代码应用开发平台,基于阿里云的云基础设施和钉钉的企业数字化操作系统,为每个组织提供低门槛、高效率的数字化业务应用生产新模式。在宜搭上生产的每个应用天然具备互联互通、数据驱动、安全可控的特点。
收费

在宜搭模版市场提供了一些免费应用模版,只需选择一个模版修改个别文案,一分钟就能搭建一款专属应用

方案概要

市面上确实存在帮助企业加快数字化建设的低代码平台,通过该平台能较快的搭建各种系统,但通常是
收费
的。

通过各类低代码产品的预研,也结合笔者当前工作需求:
免费

内网部署

灵活
)。目标求其次:通过低代码平台配置系统
大部分
前端页面。

方案筛选:

  • amis vs 爱速搭:amis 免费
  • amis vs lowcode-engine: amis 更全,包含编辑器和渲染器(解析 json 成前端页面),而 lowcode-engine 只是一个编辑器。倘若需要自建一个低代码平台, lowcode-engine 是一个不错的选择

最终方案:
中后台系统(spug) + amis + amis-editor
(开源、免费)。就像这样:

graph LR
A[[amis editor]]-- 配置页面生成 --> B{{json}}-- 传输 --> C[[amis]] -- 解析JSON渲染 --> 页面
  • amis-editor 配置页面,生成 json
  • amis 通过 json 渲染出页面
  • spug 集成 amis

方案可行性

通过 amis-editor 可视化操作快速创建页面,然后将配置放入 amis 中解析,实现大部分前端页面的可视化生成。

接口数据

本地启动一个 node 服务,用于
模拟
接口数据。

使用其他服务也可以,只要发送请求能返回数据(数据参考 amis 官网,直接使用 amis 官网的接口报跨域失败)。就像这样:

node 服务

初始化项目 local-mock:

$ mkdir local-mock
$ cd local-mock
// 初始化项目
$ npm init -y

修改如下 package.json:

{
  "name": "local-mock",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.20.2",
    "cookie-parser": "^1.4.6",
    "cors": "^2.8.5",
    "express": "^4.18.2"
  },
  "devDependencies": {
    "nodemon": "^2.0.22"
  }
}

新建 node.js(注:允许来自
http://localhost
的跨域请求):

// node.js

var express = require('express');
var app = express();

// 跨域参考:https://blog.csdn.net/gdutRex/article/details/103636581
var allowCors = function (req, res, next) {
    // 注:笔者使用 `*` 仍报跨域问题,修改为请求地址(`http://localhost`)即可。
    res.header('Access-Control-Allow-Origin', 'http://localhost');
    res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
    res.header('Access-Control-Allow-Headers', 'Content-Type,lang,sfopenreferer ');
    res.header('Access-Control-Allow-Credentials', 'true');
    next();
};

//使用跨域中间件
app.use(allowCors);

// mock数据来自 amis 官网示例:https://aisuda.bce.baidu.com/amis/zh-CN/components/crud
const data1 = {"status":0,"msg":"ok","data":{"count":171,"rows":[{"engine":"Gecko - tuhzbk","browser":"Camino 1.0","platform":"OSX.2+","version":"1.8","grade":"A","id":11},{"engine":"Gecko - aias9l","browser":"Camino 1.5","platform":"OSX.3+","version":"1.8","grade":"A","id":12},{"engine":"Gecko - s72lo","browser":"Netscape 7.2","platform":"Win 95+ / Mac OS 8.6-9.2","version":"1.7","grade":"A","id":13},{"engine":"Gecko - 1uegwbc","browser":"Netscape Browser 8","platform":"Win 98SE+","version":"1.7","grade":"A","id":14},{"engine":"Gecko - tjtajk","browser":"Netscape Navigator 9","platform":"Win 98+ / OSX.2+","version":"1.8","grade":"A","id":15},{"engine":"Gecko - ux0rsf","browser":"Mozilla 1.0","platform":"Win 95+ / OSX.1+","version":"1","grade":"A","id":16},{"engine":"Gecko - a3ae5r","browser":"Mozilla 1.1","platform":"Win 95+ / OSX.1+","version":"1.1","grade":"A","id":17},{"engine":"Gecko - 55daeh","browser":"Mozilla 1.2","platform":"Win 95+ / OSX.1+","version":"1.2","grade":"A","id":18},{"engine":"Gecko - eh2p99","browser":"Mozilla 1.3","platform":"Win 95+ / OSX.1+","version":"1.3","grade":"A","id":19},{"engine":"Gecko - f6yo9k","browser":"Mozilla 1.4","platform":"Win 95+ / OSX.1+","version":"1.4","grade":"A","id":20}]}}
const data2 = { "status": 0, "msg": "ok" }

// 查询
app.get('/amis/api/mock2/', function (req, res) {
    res.end(JSON.stringify(data1));
})

// 新增
app.post('/amis/api/mock2/', function (req, res) {
    res.end(JSON.stringify(data2));
})

// 监听3000端口
var server = app.listen(3020, function () {
    console.log('listening at =====> http://127.0.0.1:3020...');
});

编辑器

下载
amis-editor-demo
,通过
npm i
安装依赖,然后
npm run dev
本地启动编辑器:

Administrator@3L-WK-10 MINGW64 /e/amis-editor-demo-master (master)
$ npm run dev

> amis-editor-demo@1.0.0 dev E:\amis-editor-demo-master
> amis dev


                             _/
    _/_/_/  _/_/_/  _/_/          _/_/_/
 _/    _/  _/    _/    _/  _/  _/_/
_/    _/  _/    _/    _/  _/      _/_/
 _/_/_/  _/    _/    _/  _/  _/_/_/


当前版本:v3.1.8.

- [amis]开启调试模式...
<i> [webpack-dev-middleware] wait until bundle finished
assets by path *.js 66.8 MiB
  assets by chunk 28.6 MiB (id hint: vendors) 53 assets
  + 61 assets
assets by info 4.13 MiB [immutable]
  assets by chunk 2.92 MiB (auxiliary name: editor) 17 assets
  + 5 assets
assets by path *.css 5.07 MiB
  assets by chunk 224 KiB (id hint: vendors) 2 assets
  asset index.css 2.98 MiB [emitted] (name: index)
  asset editor.css 1.87 MiB [emitted] (name: editor)
assets by path *.html 1.31 KiB
  asset editor.html 674 bytes [emitted]
  asset index.html 672 bytes [emitted]
Entrypoint index 13.7 MiB (4.06 MiB) = index.css 2.98 MiB index.js 10.7 MiB 21 auxiliary assets
Entrypoint editor 12.6 MiB (2.92 MiB) = editor.css 1.87 MiB editor.js 10.8 MiB 17 auxiliary assets
runtime modules 1.06 MiB 652 modules
orphan modules 3.34 MiB (javascript) 4.13 MiB (asset) [orphan] 106 modules
javascript modules 31.1 MiB
  modules by path ./node_modules/ 31.1 MiB 3639 modules
  modules by path ./src/ 38.4 KiB 22 modules
css modules 3.15 MiB
  modules by path ./node_modules/monaco-editor/esm/vs/ 138 KiB 68 modules
  modules by path ./node_modules/amis/ 2.65 MiB 3 modules
  modules by path ./node_modules/@fortawesome/fontawesome-free/css/*.css 113 KiB 2 modules
  + 3 modules
json modules 1.93 MiB
  ./node_modules/amis/schema.json 1.9 MiB [built] [code generated]
  ./node_modules/entities/lib/maps/entities.json 28.4 KiB [built] [code generated]
webpack 5.76.3 compiled successfully in 37081 ms
√ [amis]调试模式已开启!
> Listening at http://localhost:80

当前运行脚本:
 http://localhost:80/index.js
当前运行样式[可能不存在]:
http://localhost:80/index.css
(node:10788) UnhandledPromiseRejectionWarning: Error: Exited with code 4294967295
...

输出有点错误,不管它,浏览器访问
http://localhost:80
即可进入编辑器页面。

下面笔者快速
演示
配置一个有
增加

删除

编辑

查询
的页面。就像这样:

配置过程如下:

最后生成的配置在这里(也可直接修改):

Tip
:除了
编辑
的url需要修改 json,其他的都可以在编辑器右侧中配置。


:目前笔者将 amis-editor 作为一个单独的项目运行,通过
npm run build
打包,打包后的 html 中出现以
https://aisuda.github.io/amis-editor-demo/demo/
开头的资源,引入内网是不行的,于是更改
assetsPublicPath: './'
即可指向引入打包出来的资源。目前还是有点
小图标
没有出来,可能需要更改某些地方。

// amis.config.js
build: {
    entry: { 
      index: './src/index.tsx',
      editor:  './src/mobile.tsx',
    },
    NODE_ENV: 'production',
    assetsRoot: resolve('./demo'), // 打包后的文件绝对路径(物理路径)
    // assetsPublicPath: 'https://aisuda.github.io/amis-editor-demo/demo/', // 设置静态资源的引用路径(根域名+路径)
    assetsPublicPath: './', // 设置静态资源的引用路径(根域名+路径)
    ...
}

中后台集成 amis

引入 amis

下载
sdk.tar.gz
解压后把 skd 文件夹放入 spug 的 public 中,然后在单页面中引入。就像这样:

// spug/public.index.html

<!-- 引入 amis 的包 >
<link rel="stylesheet" href="sdk/sdk.css" />
<link rel="stylesheet" href="sdk/helper.css" />
<link rel="stylesheet" href="sdk/iconfont.css" />

<script src="sdk/sdk.js"></script>

...
<title>Spug</title>

Tip
:amis 还提供了 react 的方式,也就是通过 npm 来使用,但公司分配的机器不太好,构建项目报内存不够(
JavaScript heap out of memory
),多次尝试(比如:在 linux(node18)中运行、调整内存大小 --max-old-space-size、win7 中安装 node14/16)也未解决,只能选用 sdk 的方式。

新建页面

新建页面
amis A
,用于将 amis-editor 的配置 json 渲染出前端页面。相关代码有:

  • store.js - 取得 amis-editor 的配置。这里使用 mockjs 模拟,数据就是上文编辑器创建的页面。
// spug\src\pages\amis\store.js
import { observable,} from 'mobx';
import http from 'libs/http';

class Store {
    // 表格数据
    @observable config = {};
    @observable isFetching = false;

    fetchRecords = (path) => {
        http.get(`/api${path}/`)
            .then(res => {
                // {type: "page", title: "Hello world", body: Array(2), id: "u:7b55b5793e16", asideResizor: false, …}
                console.log('res', res)
                this.config = res;
            })
            .finally(() => this.isFetching = false)
    };
}

export default new Store()
  • index.js - 根据配置渲染页面,也就是 amis 解析 json:
// spug\src\pages\amis\index.js
import React from 'react';
import { observer } from 'mobx-react';
import axios from 'axios';

import store from './store';
import _ from 'lodash';

window.enableAMISDebug = true
// amis 环境配置
const env = {
    theme: 'cxd',
    // 下面三个接口必须实现
    fetcher: ({
        url, // 接口地址
        method, // 请求方法 get、post、put、delete
        data, // 请求数据
        responseType,
        config, // 其他配置
        headers // 请求头
    }) => {
        config = config || {};
        config.withCredentials = true;
        responseType && (config.responseType = responseType);

        if (config.cancelExecutor) {
            config.cancelToken = new (axios).CancelToken(
                config.cancelExecutor
            );
        }

        config.headers = headers || {};

        if (method !== 'post' && method !== 'put' && method !== 'patch') {
            if (data) {
                config.params = data;
            }
            return (axios)[method](url, config);
        } else if (data && data instanceof FormData) {
            config.headers = config.headers || {};
            config.headers['Content-Type'] = 'multipart/form-data';
        } else if (
            data &&
            typeof data !== 'string' &&
            !(data instanceof Blob) &&
            !(data instanceof ArrayBuffer)
        ) {
            data = JSON.stringify(data);
            config.headers = config.headers || {};
            config.headers['Content-Type'] = 'application/json';
        }

        return (axios)[method](url, data, config);
    },
    isCancel: (value) => {
        console.log('isCancel')
    },
    copy: (content) => {
        console.log('copy')
    }
};

function updateAmis(...options) {
    if (!document.getElementById('amisbox')) {
        return
    }
    let amis = window.amisRequire('amis/embed');
    let amisScoped = amis.embed('#amisbox', ...options);
}
@observer
class AMISComponent extends React.Component {
    componentDidMount() {
        const path = this.props.page
        // 请求对应页面的配置
        store.fetchRecords(path)
    }

    render() {
        // 根据配置文件重新渲染页面
        // 注:使用 lodash 的深拷贝不起作用
        updateAmis(JSON.parse(JSON.stringify(store.config)), {}, env)
        return <div id="amisbox"></div>

    }
}

class APP extends React.Component {
    componentWillUnmount() {
        // 卸载时需要清空,否则切换页面还会显示上个页面
        store.config = {};
    }
    render() {
        return (
            <>
                <AMISComponent page={this.props.location.pathname} />
            </>
        );
    }
}
export default APP;


:为了让配置变更时 amis 能重新渲染页面,笔者使用了一个 hack 的方式:
JSON.parse(JSON.stringify(store.config))
。其他方法都不行(curd 存在显示问题):Object.assign(store.config)、_.cloneDeep(store.config)、{...store.config}

Tip
: 这段代码参考 react 引入方式的
https://github.com/aisuda/amis-react-starter/blob/main/src/App.tsx
。例子中是 typescript 写法,对于暂时不支持 ts 的项目,直接将类型去除即可使用。就像这样:

config.cancelToken = new (axios as any).CancelToken(
    config.cancelExecutor
);
// 去除 ts 的类型
config.cancelToken = new (axios).CancelToken(
    config.cancelExecutor
);
import React from 'react';
import { observer } from 'mobx-react';
import axios from 'axios';

import { render as renderAmis, ToastComponent, AlertComponent } from 'amis';
import store from './store';
import _ from 'lodash';

// amis 环境配置
const env = {
  theme: 'cxd',
  // 下面三个接口必须实现
  fetcher: ({
    url, // 接口地址
    method, // 请求方法 get、post、put、delete
    data, // 请求数据
    responseType,
    config, // 其他配置
    headers // 请求头
  }) => {
    console.log('fetcher', method)
    config = config || {};
    config.withCredentials = true;
    responseType && (config.responseType = responseType);

    if (config.cancelExecutor) {
      config.cancelToken = new (axios).CancelToken(
        config.cancelExecutor
      );
    }

    config.headers = headers || {};

    if (method !== 'post' && method !== 'put' && method !== 'patch') {
      if (data) {
        config.params = data;
      }
      return (axios)[method](url, config);
    } else if (data && data instanceof FormData) {
      config.headers = config.headers || {};
      config.headers['Content-Type'] = 'multipart/form-data';
    } else if (
      data &&
      typeof data !== 'string' &&
      !(data instanceof Blob) &&
      !(data instanceof ArrayBuffer)
    ) {
      data = JSON.stringify(data);
      config.headers = config.headers || {};
      config.headers['Content-Type'] = 'application/json';
    }

    return (axios)[method](url, data, config);
  },
  isCancel: (value) => {
    console.log('isCancel')
  },
  copy: (content) => {
    console.log('copy')
  }
};

@observer
class AMISComponent extends React.Component {
  componentDidMount() {
    const path = this.props.page
    // 请求对应页面的配置
    store.fetchRecords(path)
  }

  render() {
    return renderAmis(
      // store.config,
      // 使用 _.cloneDeep() 报错
      JSON.parse(JSON.stringify(store.config)),
      {
        // props...
      },
      env
    );
  }
}

class APP extends React.Component {
  componentWillUnmount() {
    // 卸载时需要清空,否则切换页面还会显示上个页面
    store.config = {};
  }
  render() {
    return (
      <>
        <ToastComponent key="toast" position={'top-right'} />
        <AlertComponent key="alert" />
        <AMISComponent page={this.props.location.pathname} />
      </>
    );
  }
}

export default APP;
放行 amis 接口


:spug 中 axios 被封装到 http.js 中,虽然在 amis 中通过 axios 发送请求,还是会走 http.js 中的拦截器(
handleResponse
)

由于 amis 的数据格式和 spug 的不同,这里暂时约定有
flag:amis
的是 amis 的接口,数据不做处理,直接放行。

// http.js

import http from 'axios'
...
// response处理
function handleResponse(response) {
  // 由于 amis 的数据格式和 spug 的不同,这里暂时约定有 flag:amis(例如:{"flag": "amis", "status": 0, "msg": "ok" }) 的是amis 的接口,数据不做处理,直接放行
  const isAmis = response.data.flag === 'amis'
  if(isAmis){
    return Promise.resolve(response.data)
  }

Tip
:amis 所需的数据格式和 spug 的不同,一种方法是前端来调整。建议让后端返回 amis 所需的数据格式,因为这是 amis 的页面,是新功能。

node 服务

现在spug的服务接口是 3010,修改 node 服务允许其跨域:

var allowCors = function (req, res, next) {
    // 注:笔者使用 `*` 仍报跨域问题,修改为请求地址(`http://localhost`)即可。
    res.header('Access-Control-Allow-Origin', 'http://localhost:3010');
    ...
    next();
};

Tip
:使用 yapi(docker 方式安装即可) 非常方便模拟数据,也无需处理跨域。

效果

amis-editor 集成到 spug 的效果如下:

自定义组件

amis-editor 也提供了自定义组件,笔者参考
自定义组件-a
新建一个
自定义组件-b
:

实现很简单,就是复制一份以下文件:

-   ./renderer/MyRenderer.tsx
-   ./editor/MyRenderer.tsx

spug 中的 amis 集成该自定义组件。可以这样做:

// 自定义组件,props 中可以拿到配置中的所有参数,比如 props.label 是 'Name'
function CustomComponent(props) {
    const { target } = props;
    let dom = React.useRef(null);
    React.useEffect(function () {
        // 从这里开始写自定义代码,dom.current 就是新创建的 dom 节点
        // 可以基于这个 dom 节点对接任意 JavaScript 框架,比如 jQuery/Vue 等
        dom.current.innerHTML = `<p>Hello {target}! @amis-editor</p>`
        // 而 props 中能拿到这个
    });
    return React.createElement('div', {
        ref: dom
    });
}

方案总结

中台系统 + amis + amis-editor
此方案能通过
可视化的配置
实现前端页面的开发,实现所见即所得的效果。

graph LR
A[[amis editor]]-- 配置页面生成 --> B{{json}}-- 传输 --> C[[amis]] -- 解析JSON渲染 --> 页面

对于常用的页面只需要通过可视化配置界面就能完成前端开发。

其他章节请看:

低代码 系列