写点什么

有赞 Flutter 插件开发与发布

  • 2019-08-01
  • 本文字数:8480 字

    阅读完需:约 28 分钟

有赞Flutter插件开发与发布

一、Flutter 插件简介

一种专用的 Dart 包,其中包含用 Dart 代码编写的 API,以及针对 Android(使用 Java 或 Kotlin)和针对 iOS(使用 OC 或 Swift)平台的特定实现(另外也可以包含 Native 的组件代码),也就是说插件包括原生代码与 Dart 代码。插件开发完成后,将上传到 dart 插件管理服务仓库,类似于 maven、pod 库,然后在 flutter 开发过程中可以通过 pubspec.yaml(dart 包管理配置文件)来获取插件服务。

二、为什么要开发 Flutter 插件

随着 Flutter 生态越来越完善,以及 Flutter 在性能上的高光表现,越来越多的模块将会通过 Flutter 来进行实现。为了更方便的与原生工程进行对接以及降低整体工程的耦合,Flutter 的开发模式也需要做成组件化的模式,拥有独立调试以及可拆卸的特性。原生工程在接入 Flutter 模块时,只需要在 gradle(pod) 中添加依赖,即可与 Flutter 模块进行交互。


在 Flutter 不同的模块开发过程中,我们不想重复的去搭建一些基础的 flutter 组件,比如埋点组件、网络通信组件、图片处理组件等,同时我们也希望在不同的 Flutter 模块开发过程中,保持 Flutter 整体的视觉风格一致,所以我们需要抽离出一些 Flutter 通用插件,来保证风格的统一以及整体工程的简洁、清晰。


总结一下,Flutter 插件化开发的好处:


  • 组件独立维护,降低工程耦合

  • 降低开发 Flutter 新模块的成本

  • 保持整体风格统一


上面讲了 Flutter 插件包括原生模块与 Dart 模块,Dart 模块很好理解,就是用 dart 写一些通用 UI、通用 IO 等。那原生模块应该怎么理解?


首先,虽然 Flutter 的生态现在已经越来越完善了,但是相比于 Android 跟 iOS 原生的生态体系,还是远远不够。很多在 Android 跟 iOS 原生上有的很酷炫的库,在 Flutter 中还没有或者是并没有那么的完善。其次,想必大家在原生工程里都有一套用了多年的稳定基础组件,包括网络组件、数据组件等,要重新在 Flutter 中用 dart 来搭建一套,时间成本、风险成本、组件兼容性等都是不可控的。所以,最理想的方式就是 Flutter 的基础组件可以对我们现有原生的组件做一层包装,然后提供接口给 Flutter 模块进行调用,这样一来什么时间、风险、兼容性都不是问题。我们只要维护一套原生组件就好,Flutter 组件只是一层包装,并不在意内部如何去实现。那么 Flutter 跟原生怎么进行交互呢?

三、Flutter 如何与原生交互

Flutter 与原生的交互模型,类似于一种 C-S 模型。其中 Flutter 为 Client 层,原生为 Server 层,两者通过 MethodChannel 进行消息通信,原生端向 Flutter 提供已有的 Native 组件功能。


在客户端, MethodChannel允许发送与方法调用相对应的消息。在平台方面,Android 上的 MethodChannel和 iOS 上的 FlutterMethodChannel启用接收方法调用并返回结果。这些类允许你使用非常少的“样板”代码开发平台插件。


Flutter 与原生的消息传递采用标准信息编解码器,是一种相对高效的二进制序列化与反序列化。当接收跟发送消息时,这些值在消息中会自动进行序列化与反序列化。详细的请参阅 StandardMessageCodec


3.1 什么是 MethodChannel

Flutter 定义了 3 种 Channel 模型,分别是:


  • BasicMessageChannel:用于传递字符串和半结构化的信息

  • MethodChannel:用于传递方法调用(method invocation)

  • EventChannel: 用于数据流(event streams)的通信


3 种 channel 之间既有共性,也有各自的特性,下面我们就 MethodChannel 进行展开 MethodChannel 有 3 个重要的成员变量:


- String name
复制代码


在 Flutter 中会存在多个 Channel,一个 Channel 对象通过 name 来进行唯一的标识,所以在 Channel 的命名上一定要独一无二,推荐采用组件名 _Channel 名 组合来进行命名


- BinaryMessenger messenge
复制代码


BinaryMessenger 是 Platform 端与 Flutter 端通信的工具,其通信使用的消息格式为二进制格式数据。当我们初始化一个 Channel,并向该 Channel 注册处理消息的 Handler 时,实际上会生成一个与之对应的 BinaryMessageHandler,并以 channel name 为 key,注册到 BinaryMessenger 中。当 Flutter 端发送消息到 BinaryMessenger 时,BinaryMessenger 会根据其入参 channel 找到对应的 BinaryMessageHandler,并交由其处理。

Binarymessenger 在 Android 端是一个接口,其具体实现为 FlutterNativeView。而其在 iOS 端是一个协议,名称为 FlutterBinaryMessenger,FlutterViewController 遵循了它。

Binarymessenger 并不知道 Channel 的存在,它只和 BinaryMessageHandler 打交道。而 Channel 和 BinaryMessageHandler 则是一一对应的。由于 Channel 从 BinaryMessageHandler 接收到的消息是二进制格式数据,无法直接使用,故 Channel 会将该二进制消息通过 Codec(消息编解码器)解码为能识别的消息并传递给 Handler 进行处理。

当 Handler 处理完消息之后,会通过回调函数返回 result,并将 result 通过编解码器编码为二进制格式数据,通过 BinaryMessenger 返回。


- MethodCodec codec
复制代码


消息编解码器 Codec 主要用于将二进制格式的数据转化为 Handler 能够识别的数据

MethodCodec 主要是对 MethodCall 中这个对象进行序列化与反序列化

MethodCall 是 Flutter 向 Native 发起调用产生的对象,其中包含了方法名以及一个参数集合(map 或者是 Json)


介绍完 3 个重要的变量,我们把整个流程连起来,看一下完成的交互流程是怎么样的

3.2 Flutter 与原生通信整体流程

  • 首先从 dart 层调用 _channel.invokeMethod(“方法名”,参数),invoke 方法会将传入的方法名与参数封装成 MethodCall 对象,然后通过 MethodCodec 对 MethodCall 对象进行编码,形成二进制格式。然后通过 BinaryMessenger 的 send 方法,将二进制格式的数据进行发送,我们继续看一下 send 方法是如何实现的:


Future<dynamic> invokeMethod(String method, [dynamic arguments]) async {        assert(method != null);              ///send messenge        final dynamic result = await BinaryMessages.send(          name,          codec.encodeMethodCall(MethodCall(method, arguments)),        );        if (result == null)          throw MissingPluginException('No implementation found for method $method on channel $name');        return codec.decodeEnvelope(result);    }
复制代码


  • 这里截取了 send 方法里关键代码, dart 层最终通过调用了 native 方法 Window_sendPlatformMessage,将序列化后的对象通过 c 层进行发送:


{    final _MessageHandler handler = _mockHandlers[channel];    if (handler != null)      return handler(message);    return _sendPlatformMessage(channel, message);}String _sendPlatformMessage(String name,                              PlatformMessageResponseCallback callback,                              ByteData data) native 'Window_sendPlatformMessage';
复制代码


  • 我们在 Flutter engine 的 native 代码中可以找到上述 native 方法的对应实现,这里截取关键部分,可以看到最后是交给了 WindowClient 的 handlePlatformMessage 方法进行实现,我们继续往下跟:


...dart_state->window()->client()->HandlePlatformMessage(        fml::MakeRefCounted<PlatformMessage>(name, response));...
复制代码


  • (这里以 Android 举例,iOS 同理)可以看到,在 Android 平台 HandlePlatformMessage 方法中,调用到了 JNI 方法,将 c 层收到的信息向 java 层抛:


void PlatformViewAndroid::HandlePlatformMessage(    fml::RefPtr<blink::PlatformMessage> message) {  JNIEnv* env = fml::jni::AttachCurrentThread();  fml::jni::ScopedJavaLocalRef<jobject> view = java_object_.get(env);  auto java_channel = fml::jni::StringToJavaString(env, message->channel());  if (message->hasData()) {    fml::jni::ScopedJavaLocalRef<jbyteArray> message_array(env, env->NewByteArray(message->data().size()));    env->SetByteArrayRegion(        message_array.obj(), 0, message->data().size(),        reinterpret_cast<const jbyte*>(message->data().data()));    message = nullptr;    // This call can re-enter in InvokePlatformMessageXxxResponseCallback.    FlutterViewHandlePlatformMessage(env, view.obj(), java_channel.obj(),                                     message_array.obj(), response_id);  } else {    message = nullptr;    // This call can re-enter in InvokePlatformMessageXxxResponseCallback.    FlutterViewHandlePlatformMessage(env, view.obj(), java_channel.obj(),                                     nullptr, response_id);  }}
复制代码


  • 看一下 JNI 对应的 java 方法,最终通过 handler.onMessage(),完成了本次 dart 信息的传递。方法中的 handler,就是我们前面提到的 MethodHandler,也是我们插件的 Native 模块注册的 MethodHandler


  private void handlePlatformMessage(final String channel, byte[] message, final int replyId) {        this.assertAttached();        BinaryMessageHandler handler = (BinaryMessageHandler)this.mMessageHandlers.get(channel);        if (handler != null) {            try {                ByteBuffer buffer = message == null ? null : ByteBuffer.wrap(message);                handler.onMessage(buffer, new BinaryReply() {                    // ...                });            } catch (Exception var6) {                // ...            }        } else {            Log.e("FlutterNativeView", "Uncaught exception in binary message listener", var6);            nativeInvokePlatformMessageEmptyResponseCallback(this.mNativePlatformView, replyId);        }    }
复制代码


MethodHandler 接口有 2 个回调参数 MethodCall 、Result


public interface MethodCallHandler {        void onMethodCall(MethodCall var1, MethodChannel.Result var2);    }
复制代码


其中 MethodCall 就是我们前面说的,由 dart 端传递过来通过序列化、反序列化的对象。


Platform 端可以从 MethodCall 中取出方法名以及参数,然后进行实现。


Result 是一个回调接口,最终的结果会通过另一个序列化、反序列化的过程返回给 dart,过程就跟上述的一致,如果无需任何返回的,可以不用这个参数。


public interface Result {        void success(@Nullable Object var1);        void error(String var1, @Nullable String var2, @Nullable Object var3);        void notImplemented();    }
复制代码


  • MethodHandler 是在什么时候注册的?


在插件运行的时候,我们会调用插件的 registerWith 方法,在生成 MethodChannel 对象时,同时向 MethodChannel 注册了一个 MethodHandler,MethodHandler 对象跟 MethodChannel 对象是一一对应的。


以上就是整个 Flutter 与 Native 的交互流程,消息的传递是通过跨平台的 c 来实现。以下是 Flutter 到原生的消息传递流程图,Native 到 Flutter 也是类似的。



讲完了通信流程,下面开始正式进入插件开发。

四、创建插件工程

推荐通过命令行来创建,因为通过 IDE 来创建有时候会卡住,而且会比较慢


flutter create --org com.qima.kdt --template=plugin -i swift -a kotlin flutter_plugin
复制代码


  • 创建好以后的目录结构如下

  • rootProject

    • lib dart 模块

    • android android 模块

    • ios ios 模块

    • example 示例测试工程可用于插件的调试

    • pubspec.yaml flutter 项目的配置文件

    • ….

4.1 什么是 pubspec.yaml

dart 生态下的包管理配置文件类似 Android 中的 gradle、iOS 中的 Podfile,在这里可以统一管理整个 flutter 工程的 dart 依赖包,以及管理整个插件的发布属性。

4.2 创建过程可能会遇到的问题

  • IDE 一直卡在 creating Flutter Project……


原因:Flutter 工程在创建过程中需要下载需要的插件,因为网络原因导致需要的插件无法下载成功会导致该问题

解决:

  • 切换网络,或者搭一个梯子

  • 通过命令行来创建插件


  • 编译 Android 模块遇到 Invoke-customs are only supported starting with Android O (–min-api 26)


在 app.gradle 中增加

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}


创建完插件工程后,分别对原生端与 Flutter 端进行开发

4.3 原生端开发

  • 实现 MethodCallHandler 接口,注册 MethodChannel 对象,MethodChannel 在创建时一定要保证 name 唯一

  • 将 MethodHandler 接口注册到 MethodChannel 中

  • 包装原生端组件,包括一些二方库、三方库,将包好的方法通过 MethodCallHandler 暴露给 Flutter 端

4.4 Flutter 端开发

  • 找到 MethodChannel 对象,通过唯一标识 name,注意(name 一定要与原生端注册的一致)

  • 定义 dart 方法,因为要保证方法的执行不产生阻塞,所以推荐用 Future async await .相关的语法见 dart 语法

  • 调用 methodChannel.invokeMothed() 与原生进行通信


以上就完成了整个插件部分的开发,开发完成后,先不急着将插件发布。可以先在本地的 example 中对所开发的插件进行验证,验证无误后,再进行发布

五、插件测试

在 example/lib/main.dart 下调用插件中的方法,然后直接通过命令将工程跑起来查看输出:


flutter run
复制代码

插件都还没有发布,为什么 example 工程可以直接引用?

看一下 example 目录下的 pubspec.yaml 文件,里面有一句:


xxxxx(插件名):path: ../
复制代码


pubspec.yaml 不但可以引用服务器上的插件,也可以引用本地路径下的插件。如此我们可以在插件未发布的情况下,直接在本地的测试工程里对插件进行测试。


后续的所有 flutter 模块的单独调试,也是同样的模式。开发完 flutter 模块后,直接在 example 工程中引入调试,不必与 host 工程进行耦合,可以提供整体的开发效率。测试没有问题后,在进行插件发布,集成开发。

六、插件发布

6.1 私有 Flutter 服务器环境搭建

Flutter 插件默认是上传到 Flutter 社区的公共仓库中,实际开发中,我们会有很多暂时不想要开源,只供团队内部使用的插件。因此将这些插件发布到 Flutter 社区中明显是不合适的,所以需要搭建一个团队内私有的 flutter 插件管理环境。官方提供了接入文档,这里不展开了。


  • dart 环境配置

  • 服务器搭建

6.1.1 官方代码结构简要说明


  • example.dart 程序入口,负责各种数据配置,及服务启动

  • shelf_pubserver.dart 定义了当前 dart 服务支持的所有接口

  • 获取某个插件的信息 /api/packages/

  • 获取某个插件特定版本的信息 /api/packages//versions/

  • 下载插件 /api/packages//versions/.tar.gz

  • 上传插件 /api/packages/versions/new

  • 删除插件 /api/packages//uploaders/


因为上传的插件文件都是存储在 Linux 服务器上的,并且已经提供以上这些接口,因此后期也可以简单搭建个 flutter web 网站,查看私有服务器上的插件包信息,方便开发使用。


  • 启动服务


dart example/example.dart-s 是否fetch官方仓库-h ${ip / domain}-p 端口-d 上传上来的插件包在服务器上的存储地址
复制代码


完成了私有 flutter 插件管理服务环境后,准备开始插件的上传,首先需要检查本地插件的发布配置信息。

6.2 完善 pubspec.yaml 文件

插件名称description: 插件描述version: 0.0.1 版本号author: xxxx<xx@xxx.com>homepage: 项目主页地址publish_to: 填写私有服务器的地址(如果是发布到flutter pub则不用填写,插件默认是上传到flutter pub)
复制代码

6.3 检验是否满足上传条件

flutter packages pub publish --dry-run
复制代码


–dry-run 参数表示本次执行会检查插件的配置信息是否有效,插件是否满足上传条件。如果成功的话并不会真正的将插件上传,而是会显示本次要发布插件的信息,并提示成功。一般在插件的正式发布前,建议先执行该命令,避免在上传过程中出现错误


当插件符合上传条件后,可以开始进行正式发布

6.4 正式发布

  • 发布至 pub 平台


flutter packages pub publish
复制代码


  • 发布至私有服务器


flutter packages pub publish --server $服务器地址
复制代码


pubspec.yaml 文件中列出的包作者与授权发布该包的人员列表不同。发布某个软件包的第一个版本的人自动成为第一个也是唯一一个有权上传其他版本软件包的人。要允许或禁止其他人上载版本,请使用 pub uploader 命令。


最终出现如下内容,代表上传成功


....|-- local.properties|-- pubspec.yaml|-- test|   '-- xxxx.dart'-- xxxx.iml
Looks great! Are you ready to upload your package (y/n)? yUploading...Successfully uploaded package.
复制代码

七、插件引用

开发上传完成后,就可以在后续的任何 Flutter 模块中,在 pubspec.yaml 中添加依赖进行引用


pubspec.yaml 更多用法见 pubspec.yaml 官方文档


  • pub 仓库插件


#插件名:版本号flutter_boost: ^0.0.411
复制代码


  • 私有仓库引用


${library name}:     hosted:       name: ${library name}        url:  xxxxx    version: ^1.0.0
复制代码


ok,以上就是完整的 Flutter 插件开发、发布、引用的流程。

八、有赞路由插件开发实践

有赞路由插件第一版的开发思路是对开源项目 flutter-boost 做一层包装,然后接入到 flutter 业务中。后期用有赞自己的 flutter 路由组件替换 flutter-boost。


我们按照上述流程,在 pubspec.yaml 中引入了 flutter-boost 插件,然后进行二次包装。在包装 dart 接口时很顺利,没有遇到什么阻碍。然而在 Native 模块,却一直不能引用到 flutter-boost 中的 native code。不仅仅是 android 如此,iOS 的同学也遇到同样的问题。


是不是插件引用插件,宿主插件就无法引用接入插件的 native 代码呢?我们又试了试,创建了一个 flutter module 以及一个一个 flutter application 来接入 flutter-boost 插件,看看能否引用到 flutter-boost 中的原生代码,最后发现都可以引用,唯独 flutter plugin 无法引用。


看来应该是插件工程的特殊性导致。于是,我们开始对比插件工程与其他工程的区别,最终发现,module 工程以及 application 工程比插件工程多了一个 include_flutter.groovy 文件


rootProject.name = 'android_generated'setBinding(new Binding([gradle: this]))evaluate(new File('include_flutter.groovy'))
复制代码


iOS 多了一个 podhelper.rb


flutter_application_path = '../my_flutter/'eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)end
复制代码


看一看这个文件到底做了什么,以 android 举例:


def scriptFile = getClass().protectionDomain.codeSource.location.toURI()def flutterProjectRoot = new File(scriptFile).parentFile.parentFile
gradle.include ':flutter'//获取项目的根目录gradle.project(':flutter').projectDir = new File(flutterProjectRoot, '.android/Flutter')
def plugins = new Properties()//在根目录下找到一个叫 .flutter-plugins的文件,然后逐行读入;def pluginsFile = new File(flutterProjectRoot, '.flutter-plugins')if (pluginsFile.exists()) { pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }}//.flutter-plugins的内容如下,存放了对应原生模块的名字以及路径flutter_boost=/Users/xxx/Downloads/flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_boost-0.0.415/xservice_kit=/Users/xxx/Downloads/flutter/.pub-cache/hosted/pub.flutter-io.cn/xservice_kit-0.0.29/
//如果是android工程的,则通过gradle引用到工程中,完成对插件原生lib的引用plugins.each { name, path -> def pluginDirectory = flutterProjectRoot.toPath().resolve(path).resolve('android').toFile() gradle.include ":$name" gradle.project(":$name").projectDir = pluginDirectory}...
复制代码


ok,到这里就很清楚了。一个 dart 插件不仅仅提供的是 dart 层的功能,其原生层的功能也可以直接给宿主的原生层去引用。dart 插件在完成打包后,其原生部分的代码也会被打成一个依赖包。插件工程默认是不能够引用三方插件的原生依赖包,只能引用到 dart 部分。当然如果想要引用到三方插件的 native 功能,需要自己写一个类似于 flutter module 工程自动创建的依赖包收集脚本。

九、总结

目前 Flutter 生态越来越完善,后续不可避免的会越来越多的与 Flutter 进行交互。为了更好的与 Native 项目的兼容,减少原生工程与 Flutter 业务的耦合,Flutter 插件化是一个不错的选择。目前有赞 Flutter 插件化项目已经封装了网络、埋点、路由等基础插件,后续将在线上应用进行接入尝试,希望能给正在探索 Flutter 的同学一些灵感。


相关阅读:


StandardMessageCodec:https://api.flutter.dev/flutter/services/StandardMessageCodec-class.html


本文转载自公众号有赞 coder(ID:youzan_coder)


原文链接


https://mp.weixin.qq.com/s?__biz=MzAxOTY5MDMxNA==&mid=2455759918&idx=1&sn=63bbbd98d002dc30ad21b70239a7313e&chksm=8c686a0bbb1fe31da09d7ed32283beb2f173d8f4434030c794f2ca4143785290d235900731a5&scene=27#wechat_redirect


2019-08-01 08:0013875

评论

发布
暂无评论
发现更多内容

鸿蒙生态汇聚200万+开发者,金山办公、京东分享高效开发新体验

叶落便知秋

用技术为内容注入生命力,华为视频持续升级影音体验

科技汇

DDL 毫秒级同步,Light Schema Change 的设计与实现|新版本揭秘

SelectDB

数据库 大数据 Doris schema 企业号十月 PK 榜

TDSQL-C真·秒级启停:连接断了,又没断

腾讯云数据库

数据库 腾讯云 TDSQL-C 腾讯云数据库

科普|渲染农场与超级计算机有什么不同?

Finovy Cloud

人工智能 深度学习 图像处理 云渲染 渲染农场

EMR with TiSpark(on EKS )

TiDB 社区干货传送门

集合管道模式(上)

冰心的小屋

集成管道模式 pipline

双机热备软件哪家好?有哪些功能?咨询电话多少?

行云管家

高可用 热备 双机热备

探讨:2023 年 WEB 3 的 5 大趋势

开发微hkkf5566

实践一年之久,vivo 如何基于 APISIX 进行业务基础架构的演进

API7.ai 技术团队

开源 云原生 API网关 APISIX 客户案例

Web3.0 中的去中心化身份

开发微hkkf5566

一文了解 DataLeap 中的 Notebook

字节跳动数据平台

大数据 火山引擎 DataLeap

案例故事丨MySQL or TiDB?HTAP 数据库在中国 SaaS 行业头部服务商的应用实践

TiDB 社区干货传送门

Java Web(三)HTML和CSS

浅辄

Java web HTML5, CSS3 11月月更

API 网关 Apache APISIX 3.0 版本正式发布!

API7.ai 技术团队

Apache 开源 APISIX 新版本/特性发布

还不会日志异常检测?看完这篇文章就够了!

云智慧AIOps社区

人工智能 机器学习 大数据 日志分析 异常日志

如何通过Java 在 Word 中更改字体颜色

Geek_249eec

word java;

华为全联接大会2022丨华为云打造可信认证体系,加速开发者成长

华为云开发者联盟

云计算 后端 华为云 企业号十月 PK 榜

SpringCloudAlibaba 微服务组件 Nacos 之配置中心源码深度解析

程序员小毕

微服务 后端 nacos 架构师 java面试

TiKV 源码阅读三部曲(二)读流程

TiDB 社区干货传送门

Linux中gcc4.8.5升级到gcc5.4.0用已经编译好的安装包升级(重点是不用编译安装,可以更省时)

A-刘晨阳

Linux 运维 GCC 11月月更 gcc5.4

TiDB 2022 Hackathon 产品组决赛“不负责任”点评

TiDB 社区干货传送门

如何使用清源 CleanSource SCA 管理开源风险

安势信息

SCA SBOM 清源CleanSource SCA 开源风险

保定有几家等保测评机构?咨询电话多少?

行云管家

等保测评 等级测评 等保测评机构 保定

久等了,青年技术沙龙北京发车!

小红书技术REDtech

TiDB 的 graceful shutdown

TiDB 社区干货传送门

PingCAP 携手阿里云,探索 TiDB 云原生的进阶之路

TiDB 社区干货传送门

峰会实录 | 镜舟科技CEO孙文现:基于StarRocks打造企业级极速统一数据分析产品

StarRocks

数据库·

Centos7 gcc4.8.5升级到版本gcc5.4.0

A-刘晨阳

Linux 运维 11月月更 gcc5.4

大数据中,LED显示屏行业的两大服务和四大功能

Dylan

LED显示屏 户外LED显示屏 led显示屏厂家

【杭州专场】蚂蚁单测自动生成产品体验活动招募开启

TRaaS

有赞Flutter插件开发与发布_语言 & 开发_菲克_InfoQ精选文章