单文件组件的组件传值_移动端组件化架构(下)

我的组件化方案

    对于项目架构来说,一定要建立于业务之上来设计架构。不同的项目业务不同,组件化方案的设计也会不同,应该设计最适合公司业务的架构。

架构设计

    以我之前公司项目为例,项目是一个地图导航应用,业务层之下的核心模块和基础模块占比较大,涉及到地图SDK、算路、语音等模块。且基础模块相对比较独立,对外提供了很多调用接口。由此可以看出,公司项目是一个重逻辑的项目,不像电商等App偏展示。

    项目整体的架构设计是:层级架构+组件化架构,对于具体的实现细节会在下面详细讲解。采取这种结构混合的方式进行整体架构,对于组件的管理和层级划分比较有利,符合公司业务需求。

a96e5e4eac2631586e71611522616610.png

    在设计架构时,我们将整个项目都拆分为组件,组件化程度相当高。用到哪个组件就在工程中通过Podfile进行集成,并通过URLRouter统一所有组件间的通信。

   组件化架构是项目的整体框架,而对于框架中每个业务模块的实现,可以是任意方式的架构,MVVM、MVC、MVCS等都是可以的,只要通过MGJRouter将组件间的通信方式统一即可。

分层架构

    组件化架构在物理结构上来说是不分层次的,只有组件与组件之间的划分关系。但是在组件化架构的基础上,应该根据项目和业务设计自己的层次架构,这套层次架构可以用来区分组件所处的层次及职责,所以我们设计了层级架构+组件化架构的整体架构。

    我公司项目最开始设计的是三层架构:业务层 -> 核心层 (high + low) -> 基础层,其中核心层又分为high和low两部分。但是这种架构会造成核心层过重,基础层过轻的问题,这种并不适合组件化架构。

    在三层架构中会发现,low层并没有耦合业务逻辑,在同层级中是比较独立的,职责较为单一和基础。我们对low层下沉到基础层中,并和基础层进行合并。所以架构被重新分为三层架构:业务层 -> 核心层 -> 基础层。之前基础层大多是资源文件和配置文件,在项目中存在感并不高。

    在分层架构中,需要注意只能上层对下层依赖,下层对上层不能有依赖,下层中不要包含上层业务逻辑。对于项目中存在的公共资源和代码,应该将其下沉到下层中。

职责划分

    在三层架构中,业务层负责处理上层业务,将不同业务划分到相应组件中,例如IM组件、导航组件、用户组件等。业务层的组件间关系比较复杂,会涉及到组件间业务的通信,以及业务层组件对下层组件的引用。

    核心层位于业务层下方,为业务层提供业务支持,如网络、语音识别等组件应该划分到核心层。核心层应该尽量减少组件间的依赖,将依赖降到最小。核心层有时相互之间也需要支持,例如经纬度组件需要网络组件提供网络请求的支持,这种是不可避免的。

    其他比较基础的模块,都放在基础层当做基础组件。例如AFN、地图SDK、加密算法等,这些组件都比较独立且不掺杂任何业务逻辑,职责更加单一,相对于核心层更底层。可以包含第三方库、资源文件、配置文件、基础库等几大类,基础层组件相互之间不应该产生任何依赖。

    在设计各个组件时,应该遵循“高内聚,低耦合”的设计规范,组件的调用应该简单且直接,减少调用方的其他处理。对于核心层和基础层的划分,可以以是否涉及业务、是否涉及同级组件间通信、是否经常改动为参照点。如果符合这几点则放在核心层,如果不符合则放在基础层。

集成方式

    新建一个项目后,首先将配置文件、URLRouter、App容器等集成到主工程中,做一些基础的项目配置,随后集成需要的组件即可。项目被整体拆分为组件化架构后,应用对所有组件的集成方式都是一样的,通过Podfile将需要的组件集成到项目中。通过组件化的方式,使得开发新项目速度变得非常快。

    在集成业务层和核心层组件后,组件间的通信都是由URLRouter进行通信,项目中不允许直接依赖组件源码。而基础层组件则在集成后直接依赖,例如资源文件和配置文件,这些都是直接在主工程或组件中使用的。第三方库则是通过核心层的业务封装,封装后由URLRouter进行通信,但核心层也是直接依赖第三方库源码的。

    组件的集成方式有两种,源码和framework的形式,我们使用framework的方式集成。因为一般都是项目比较大才用组件化的,但大型项目都会存在编译时间的问题,如果通过framework则会大大减少编译时间,可以节省开发人员的时间。

组件间通信

    对于组件间通信,我们采用的MGJRouter方案。因为MGJRouter现在已经很稳定了,而且可以满足蘑菇街这样量级的App需求,证明是很好的,没必要自己写一套再慢慢踩坑。

    MGJRouter的好处在于,其调用方式很灵活,通过MGJRouter注册并在block中处理回调,通过URL直接调用或者URL+Params字典的方式进行调用。由于通过URL拼接参数或Params字典传值,所以其参数类型没有数量限定,传递比较灵活。在通过openURL:调用后,可以在completionBlock中处理完成逻辑。

    MGJRouter有个问题在于,在编写组件间通信的代码时,会涉及到大量的Hardcood。对于Hardcode的问题,蘑菇街开发了一套后台系统,将所有的Router需要的URL和参数名,都定义到这套系统中。我们维护了一个Plist表,内部按不同组件进行划分,包含URL和传参名以及回调参数。

85d6d817946c61c7b8a43a88373950bd.png

小思考

    MGJRouter可以在openURL:时传入一个NSDictionary参数,在接触RAC之后,我在想是不是可以把NSDictionary参数变为RACSignal参数,直接传一个信号过去。

    注册MGJRouter:

objc
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id subscriber) {
    [subscriber sendNext:@"刘小壮"];
    return [RACDisposable disposableWithBlock:^{
        NSLog(@"disposable");
    }];
}];
[MGJRouter registerURLPattern:@"CTB://UserCenter/getUserInfo" withSignal:signal];

    调用MGJRouter:

objc
RACSignal *signal = [MGJRouter openURL:@"CTB://UserCenter/getUserInfo"];
[signal subscribeNext:^(NSString *userName) {
    NSLog(@"userName %@", userName);
}];

    这种方式是可行的。使用RACSignal方式优点在于,相对于直接传字典过去更加灵活,并且具备RAC的诸多特性。但缺点也不少,信号控制不好乱用的话也很容易挖坑,是否使用还是看团队情况了。

H5和Native通信

    在项目中经常会用到H5页面,如果能通过点击H5页面调起原生页面,这样的话Native和H5的融合会更好。所以我们设计了一套H5和Native交互的方案,这套方案可以使用URLRouter的方式调起原生页面,实现方式也很简单,并且这套方案和H5原本的跳转逻辑并不冲突。

    通过iOS自带UIWebView创建一个H5页面后,H5可以通过调用下面的JS函数和Native通信。调用时可以传入新的URL,这个URL可以设置为URLRouter的URL。

objc
window.location.href = 'CTB://UserCenter/UserLogin?userName=lxz&WeChatID=lz2046703959';

    通过JS刷新H5页面时,会调用下面的代理方法。如果方法返回YES,则会根据URL协议进行跳转。

objc
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;

    跳转时系统会判断通信协议,如果是HTTP等标准协议,则会在当前页面进行刷新。如果跳转协议在URL Schame中注册,则会通过系统openURL:的方式调用到AppDelegate的系统代理方法中,在代理方法中调用URLRouter,则可以通过H5页面唤起原生页面。

APP Service

    在应用启动过程中,通常会做一些初始化操作。有些初始化操作是运行程序所需要的,例如崩溃统计、建立服务器的长连接等。或有的组件会对初始化操作有依赖关系,例如网络组件依赖requestToken等。

    对于应用启动时的初始化操作,应该创建一个AppService来统一管理启动操作,将初始化操作都放在里面,包含创建根控制器等。其中有的初始化操作需要尽快执行,有的并不需要立即执行,可以根据不同操作设定优先级,来管理所有初始化操作。

objc
#import
typedef NS_ENUM(NSUInteger, CTBAppServicePriority) {
    CTBAppServicePriorityLow,
    CTBAppServicePriorityDefault,
    CTBAppServicePriorityHigh,
};
@interface CTBAppService : NSObject
+ (instancetype)appService;
- (void)registerService:(dispatch_block_t)serviceBlock 
               priority:(CTBAppServicePriority)priority;
@end

参数传递

项目中存在很多的模型定义,那组件化后这些模型应该定义在哪呢?

    casatwy对模型类的观点是去Model化,简单来说就是用字典代替Model存储数据。这对于组件化架构来说,是解决组件之间数据传递的一个很好的方法。但是去Model的方式,会存在大量的字段读取代码,使用起来远没有模型类方便。

    因为模型类是关乎业务的,理论上必须放在业务层也就是业务组件这一层。但是要把模型对象从一个组件中当做参数传递到另一个组件中,模型类放在调用方和被调方的哪个组件都不太合适,而且有可能不只两个组件使用到这个模型对象。这样的话在其他组件使用模型对象,必然会造成引用和耦合。

    如果在用到这个模型对象的所有组件中,都分别维护一份相同的模型类,或者各自维护不同结构的模型类,这样之后业务发生改变模型类就会很麻烦,这是不可取的。

设计方案

如果将所有模型类单独拉出来,定义一个模型组件呢?

    这个看起来比较可行,将这个定义模型的组件下沉到基础层,模型组件不包含业务,只声明模型对象的类。如果将原来各个组件的模型类定义都拉出来,单独放在一个组件中,可以将原有各组件的Model层变得很轻量,这样对整个项目架构来说也是有好处的。

    在通过Router进行组件间调用时,通过字典进行传值,这种方式比较灵活。在组件内部使用Model层时,还是用模型组件中定义的Model类。Model层建议还是用Model对象的形式比较方便,不建议整体使用去Model化的设计。在接收到其他组件传递过来的字典参数时,可以通过Model类提供的初始化方法,或其他转Model框架将字典转为Model对象。

objc
@interface CTBStoreWelfareListModel : NSObject
/**
 * 自定义初始化方法
 */
- (instancetype)initWithDict:(NSDictionary *)dict;
@end

    我公司持久化方案用的是CoreData,所有模型的定义都在CoreData组件中,则不需要再单独创建一个模型组件。

动态化构想

    我公司项目是一个常规的地图类项目,首页和百度、高德等主流地图导航App一样,有很多添加在地图上的控件。有的版本会添加控件上去,而有的版本会删除控件,与之对应的功能也会被隐藏。

    所以,有次和组里小伙伴们开会的时候就在考虑,能不能在服务器下发代码对首页进行布局!这样就可以对首页进行动态布局,例如有活动的时候在指定时间显示某个控件,这样可以避免App Store审核慢的问题。又或者线上某个模块出现问题,可以紧急下架出问题的模块。

   对于这个问题,我们设计了一套动态配置方案,这套方案可以对整个App进行配置。

配置表设计

    对于动态配置的问题,我们简单设计了一个配置表,初期打算在首页上先进行试水,以后可能会布置到更多的页面上。这样应用程序各模块的入口,都可以通过配置表来控制,并且通过Router控制页面间跳转,灵活性非常大。

    在第一次安装程序时使用内置的配置表,之后每次都用服务器来替换本地的配置表,这样就可以实现动态配置应用。下面是一个简单设计的配置数据,JSON中配置的是首页的配置信息,用来模拟服务器下发的数据,真正服务器下发的字段会比这个多很多。

objc
{
    "status": 200,
    "viewList": [
        {
            "className": "UIButton",
            "frame": {
                "originX": 10,
                "originY": 10,
                "sizeWidth": 50,
                "sizeHeight": 30
            },
            "normalImageURL": "http://image/normal.com",
            "highlightedImageURL": "http://image/highlighted.com",
            "normalText": "text",
            "textColor": "#FFFFFF",
            "routerURL": "CTB://search/***"
        }
    ]
}

    对于服务器返回的数据,我们会创建一套解析器,这个解析器用来将JSON解析并“转换”为标准的UIKit控件。点击后的事件都通过Router进行跳转,所以首页的灵活性和Router的使用程度成正比。

    这套方案类似于React Native的方案,从服务器下发页面展示效果,但没有React Native功能那么全。相对而言是一个轻量级的配置方案,主要用于页面配置。

资源动态配置

    除了页面的配置之外,我们发现地图类App一般都存在ipa过大的问题,这样在下载时很消耗流量以及时间。所以我们就在想能不能把资源也做到动态配置,在用户运行程序的时候再加载资源文件包。

    我们想通过配置表的方式,将图片资源文件都放到服务器上,图片的URL也随配置表一起从服务器获取。在使用时请求图片并缓存到本地,成为真正的网络APP。在此基础上设计缓存机制,定期清理本地的图片缓存,减少用户磁盘占用。

淘宝组件化架构

本章节源自于宗心在阿里技术沙龙上的一次技术分享。

(https://yq.aliyun.com/articles/129)

架构发展

    淘宝iOS客户端初期是单工程的普通项目,但随着业务的飞速发展,现有架构并不能承载越来越多的业务需求,导致代码间耦合很严重。后期开发团队对其不断进行重构,将项目重构为组件化架构,淘宝iOS和Android两个平台,除了某个平台特有的一些特性或某些方案不便实施之外,大体架构都是差不多的。

发展历程

  1. 刚开始是普通的单工程项目,以传统的MVC架构进行开发。随着业务不断的增加,导致项目非常臃肿、耦合严重。

  2. 2013年淘宝开启"all in 无线"计划,计划将淘宝变为一个大的平台,将阿里系大多数业务都集成到这个平台上,造成了业务的大爆发。 淘宝开始实行插件化架构,将每个业务模块划分为一个子工程,将组件以framework二方库的形式集成到主工程。但这种方式并没有做到真正的拆分,还是在一个工程中使用git进行merge,这样还会造成合并冲突、不好回退等问题。

  3. 迎来淘宝移动端有史以来最大的重构,将其重构为组件化架构。将每个模块当做一个组件,每个组件都是一个单独的项目,并且将组件打包成framework。主工程通过podfile集成所有组件的framework,实现业务之间真正的隔离,通过CocoaPods实现组件化架构。

架构优势

    淘宝是使用git来做源码管理的,在插件化架构时需要尽可能避免merge操作,否则在大团队中协作成本是很大的。而使用CocoaPods进行组件化开发,则避免了这个问题。

    在CocoaPods中可以通过podfile很好的配置各个组件,包括组件的增加和删除,以及控制某个组件的版本。使用CocoaPods的原因,很大程度是为了解决大型项目中,代码管理工具merge代码导致的冲突。并且可以通过配置podfile文件,轻松配置项目。

    每个组件工程有两个target,一个负责编译当前组件和运行调试,另一个负责打包framework。先在组件工程做测试,测试完成后再集成到主工程中集成测试。

    每个组件都是一个独立app,可以独立开发、测试,使得业务组件更加独立,所有组件可以并行开发。下层为上层提供能满足需求的底层库,保证上层业务层可以正常开发,并将底层库封装成framework集成到主工程中。

    使用CocoaPods进行组件集成的好处在于,在集成测试自己组件时,可以直接在本地主工程中,通过podfile使用当前组件源码,可以直接进行集成测试,不需要提交到服务器仓库。

淘宝四层架构

c44c86f52d84ac4738f8a19d63411c6b.png

    淘宝架构的核心思想是一切皆组件,将工程中所有代码都抽象为组件。

    淘宝架构主要分为四层,最上层是组件Bundle(业务组件),依次往下是容器(核心层),中间件Bundle(功能封装),基础库Bundle(底层库)。容器层为整个架构的核心,负责组件间的调度和消息派发。

总线设计

    总线设计:URL路由+服务+消息。统一所有组件的通信标准,各个业务间通过总线进行通信。

75af21bbaa1cd7c73e9f6842dff57526.png

URL总线

    通过URL总线对三端进行了统一,一个URL可以调起iOS、Android、前端三个平台,产品运营和服务器只需要下发一套URL即可调用对应的组件。

    URL路由可以发起请求也可以接受返回值,和MGJRouter差不多。URL路由请求可以被解析就直接拿来使用,如果不能被解析就跳转H5页面。这样就完成了一个对不存在组件调用的兼容,使用户手中比较老的版本依然可以显示新的组件。

    服务提供一些公共服务,由服务方组件负责实现,通过Protocol进行调用。

消息总线

    应用通过消息总线进行事件的中心分发,类似于iOS的通知机制。例如客户端前后台切换,则可以通过消息总线分发到接收消息的组件。因为通过URLRouter只是一对一的进行消息派发和调度,如果多次注册同一个URL,则会被覆盖掉。

Bundle App

c5b236869fe19c79076f6ef6e2b05b47.png

    在组件化架构的基础上,淘宝提出Bundle App的概念,可以通过已有组件,进行简单配置后就可以组成一个新的app出来。解决了多个应用业务复用的问题,防止重复开发同一业务或功能。

     Bundle即App,容器即OS,所有Bundle App被集成到OS上,使每个组件的开发就像app开发一样简单。这样就做到了从巨型app回归普通app的轻盈,使大型项目的开发问题彻底得到了解决。

总   结

    好多朋友在看完这篇文章后,都问有没有Demo。其实架构是思想上的东西,重点还是理解架构思想。文章中对思想的概述已经很全面了,用多个项目的例子来描述组件化架构。就算提供了Demo,也没法把Demo套在其他工程上用,因为并不一定适合所在的工程。

    后来想了一下,我把组件化架构的集成方式,简单写了个Demo,这样可以解决很多人在架构集成上的问题。我把Demo放在我Github上了,用Coding的服务器来模拟我公司私有服务器,直接拿MGJRouter来当Demo工程中的Router。下面是Demo地址。

https://github.com/DeveloperErenLiu/ComponentArchitecture

783a549e07c39a9e507dbc15d36339be.png

推荐阅读

iOS汇编快速入门

如何评价 SwiftUI?

从 SwiftUI 谈声明式 UI 与类型系统

在看就点点吧 e77422f5fbcf6a466c07e9756e86c5e5.png

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

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

相关文章

为什么圆是360度?超颠覆的解释

圆为什么有360度?为什么不是300度呢?古文明时期人类把很多不能解释的自然现象归结为“天意”真的有天意吗?我们把圆分成等份,奇迹出现了.....依次等分下去,结果一样...任何被分成等分的角度的所有数字之和为9现在我们来…

我获得“微软MVP”奖项,后续将会贡献更多技术内容

昨天晚上,我收到了微软总部发来的“恭喜获得MVP”的邮件。请点击【阅读原文】查看我的MVP Profile页面。有的朋友说“一直以为你早就是MVP了”。其实这么多年我做的技术贡献主要是录编程视频教程,而这些视频教程都是通过BT下载等方式传播,没有…

[Spring MVC] - InitBinder验证

Spring MVC使用InitBinder验证: 使用InitBinder做验证的情况一般会在此Controller中提交的数据需要有一些是业务性质的,也即比较复杂的验证情况下才会使用。大部份简单的表单验证,使用annotation验证即可以解决。 Annotation验证使用方法可参…

linux6.5进入救援模式,rhel6.5救援模式修复系统

如果系统中很多重要的部分被删除了例如/boot下的所有东西,则可以通过救援模式[rootdazzle1 桌面]# mkdir /backup[rootdazzle1 桌面]# cp /etc/fstab /backup/fstab  //先备份以下fstab文件,也可以不备份自己写[rootdazzle1 桌面]# rm -rf /boot/*  …

一名毕业生的自述:我知道我必须写论文,但没聪明到可以写出来......

全世界只有3.14 % 的人关注了爆炸吧知识2020年转眼就到了4月。在即将毕业的学子之间,每天的狂野问候语是这样的:“你论文改完了吗?”“你论文查重率是多少?”“你什么时候答辩?”在微博上实时搜索“翟天临”三个字&…

不是架构的架构之四:业务层的实现与自动代理

我们在开篇中提到,希望能有一种办法,能自动适应系统的环境配置,在局域网小型应用中将直接访问数据库以获得最高的性能,在分布式环境中自动使用WCF来获得较好的安全性和连通性。 但是,我们不希望这样的特性使我们的开发…

python程序设计实践教程陈东_Python

“我们正步入一个数据或许比软件更重要的新时代。——Tim OReilly” 运用数据是精准刻画事物、呈现发展规律的主要手段,分析数据展示规律,把思想变得更精细! 本课程面向各类编程学习者,讲解利用Python语言表达N维数据并结合数据特…

Silverlight中开发和设计人员的合作文档信息

-----------------------------------------------------------------------------------> copyright:http://www.docin.com/p-34191215.html转载于:https://www.cnblogs.com/molin/archive/2009/12/08/silverlight_manager.html

和男朋友一块儿吃VS单独一人在家吃饭

1 和男朋友一块儿吃VS单独一人在家吃饭2 忍不住要为这位跳高选手鼓掌了3 我们家的蔬菜就没有这种觉悟4 这螳螂拳算是练到家了5 现实中的你胖的一批 6 这套户型咋样?7 你能看出几个字你点的每个赞,我都认真当成了喜欢

指针04 - 零基础入门学习C语言44

第八章:指针04 让编程改变世界 Change the world by program 小结 归纳起来, 如果有一个实参数组, 想在函数中改变此数组中的元素的值, 实参与形参的对应关系有以下4种情况: (1) 形参和实参都用数组名, 如: [codesyntax lang&…

填坑 | .NET 5在Docker中访问MSSQL报错

【.NET Core】| 作者 / Edison Zhou不知道你有没有在.NET Core/.NET 5的Docker访问MS SQL Server数据库,如果有,那么很有可能会遇到这个错误。1SSL版本错误最近在公司用.NET 5重构部分业务服务,由于之前老系统使用了MS SQL Server数据库&…

六个机械原理,动图形象直观、解读通俗易懂

全世界只有3.14 % 的人关注了爆炸吧知识01间歇运动机构▼间歇运动机构能够将原动件的连续转动转变为从动件周期性运动和停歇的机构,称为间歇运动机构。例如牛头刨床工作台的横向进给运动,电影放映机的送片运动等都用有间歇运动机构。常见的间歇运动机构有…

linux读写usb host,LINUX下USB1.1设备学习小记(3)_host与device

各位还记得”任何传输都是由host发起的”这句话么~在usb设备插入pc中到拔出usb设备,都是由host进行询问的一个usb鼠标的工作流程可以表达如下:usb鼠标插入pc中:主机询问设备:给我你的设备信息(控制传输)主机根据usb鼠标的设备信息进行驱动配置,配置结束后主机询问设备:给我你的…

java判断时间区间 隔天_Java初中级程序员面试题宝典

Java基础部分&与&&区别?&和&&都是逻辑运算符,都是判断两边同时真则为真,否则为假;但是&&当第一个条件不成之后,后面的条件都不执行了,而&则还是继续执行,直…

什么是sns

SNS 目录[隐藏] 第一章 什么是SNS? 第二章 SNS带给顾客的特殊价值描绘 第三章 观察SNS的技术与服务 第四章 SNS市场与竞争 第五章 SNS的主要营销策略简介 第六章 中国SNS的发展与主要网站及产品SNSSNS,全称Social Networking Services,即社会…

elsa-core——1.Hello World:Console

github上有一个开源的工作流项目elsa,elsa-core是core语言的版本,其文档因为是英文,因此会让很多人不想看,或者是看不下去,从这篇文章开始我将开始对elsa-core的文档翻译成中文并分享出来,方便大家查看与学…

docker 查看容器_Docker介绍

docker介绍什么是docker?我们先看一下官方文档对docker的定义。翻译一下就是:Docker是一个集开发,发布和运行应用程序的开放平台。Docker能够分离应用和基础架构,从而可以使得用户可以快速交付软件。借助于Docker,用户…

ASF经验谈(上)

http://software.intel.com/zh-cn/blogs/2009/12/01/asf/?cidsw:51cto【前言】写这篇文章之前先声明一下,笔者这里说到的ASF不是指Microsoft的多媒体文件的ASF格式(可能这个很多人更熟悉一些),而是用于远程管理的ASF(Alert Stand…

ubuntu linux 批量部署,使用Cobbler批量部署Linux和Windows:CentOS/Ubuntu批量安装(二)...

通过前面服务端的部署,已经配置好了 Cobbler Server 端,接下来开始进行 CentOS/Ubuntu 的批量安装,在进行 CentOS/Ubuntu 批量安装时,也需要通过Cobbler来做相应的发行版导入配置。流程如下:上传ISO镜像到 Cobbler Ser…

盘点这些年被黑的最惨的语言

全世界只有3.14 % 的人关注了爆炸吧知识这些年,被黑过的语言数不胜数,最惨的要数HTML,但CSS、Python等也惨遭黑手比惨?这就来一波。HTMLHTMLC语言C语言JavaJavaJavaJavaJavaScript JavaScript JavaScriptJavaPHPPHPPHPPHPPythonPy…