FreshRSS

🔒
❌ 关于 FreshRSS
发现新文章,点击刷新页面。
今天 — 2022年1月28日SegmentFault 最新的文章

建一座国际连锁“商场”:openEuler的雄心与蓝图 | 开源访谈《源创者说》首播

2022年1月28日 08:21

​访谈者:马玮,SegmentFault 思否技术编辑

受访者:熊伟

熊伟,南开大学毕业,工学博士,曾在TurboLinux、WindRiver等公司参与操作系统研发。2014年加入华为,现为2012实验室中央软件院服务器操作系统首席架构师,openEuler技术委员会委员,openEuler社区主要创建人之一,当选“2020中国开源杰出贡献人物”。


2010年左右,华为开始研发EulerOS操作系统。经过长达十年的打磨,EulerOS在华为内部已经成熟并大规模应用。操作系统作为IT基础设施的底座,是数字世界的重要基础设施,发挥着承上启下的重要作用。为了和产业界的伙伴们共建这一基础设施,华为在2019年决定将EulerOS开源,命名为openEuler,与行业伙伴共同构筑共建,共享,共治的全面数字基础设施。

https://www.bilibili.com/vide...

从操作系统到数字基础设施,openEuler打造多元开放生态

相对于传统意义上的“软件”而言,操作系统更类似于一个平台系统,向用户提供各种满足多样性需求的各种软件。正因如此,熊伟认为“构建openEuler的过程很类似建设一座大型连锁商场,商场的价值不在于商场本身,而在于内部丰富多彩的服务,比如餐饮、娱乐、电影等等,这些异彩纷呈的服务才使得商场成为大家都喜欢去的一个场所。类比一下,openEuler这样的操作系统平台的重要性也在于其上各种丰富多彩的软硬件系统,openEuler的目标就是不断吸纳各种厂商入驻,不断推出各种服务,不断丰富操作系统的应用,同时不断吸纳各种用户,在用户的持续反馈中不断改进自己,从而最终建立起一个完整的产业生态系统。”

如今,数字基础设施正在向万物互联的方向发展,云、边缘、IoT等场景成为了openEuler的新战场。与以往不同的是,这些新的场景需要面对更加开放、多种硬件平台和架构、更注重端云协作的生态需求。熊伟认为未来openEuler需要对社区的组织形式,整体的技术架构进行全面升级,以满足社区规模不断扩大所带来的挑战,并将操作系统架构从注重服务器,云等场景扩展到IoT、嵌入式,边缘计算等新的场景。

“openEuler作为数字基础设施,形式将会更加开放,吸纳更多厂商、用户与开发者参与其中,共同将openEuler真正构建成为数字世界的底座”,熊伟对于openEuler的未来充满信心。

面对不同的应用场景,openEuler不光提供Linux内核,也针对边缘计算,嵌入式,IoT等场景提供实时性内核等新的选择,为客户提供多种选项,覆盖更为广泛的应用场景,这是openEuler区别于传统操作系统的重要特征。

通过多内核支持,不但使得openEuler能覆盖更多的业务场景,也有利于不同的场景具有统一的开发视图,归一的开发接口,让软件更易于在不同的系统上进行迁移。做到更好的端侧,边缘侧和云侧的协同,大大提升开发效率。基于openEuler平台,同一份应用程序可以在云、边缘、IoT、车联网、工业园区等众多场景中无缝迁移。这种能力会极大降低开发门槛,为繁荣的应用生态创造基础条件。

熊伟认为,除了openEuler社区中的一些硬核技术,openEuler的核心竞争力还体现在社区的“软”实力上。在熊伟看来,openEuler社区的核心目标并不只是做出一两件软件成品,而是打造一个能够良性循环与可持续发展的组织架构,建立一套完善的流程规范,并持续为行业提供人才输出。当组织,流程,人才这些要素都构建起来以后,并辅之以优秀的社区文化,openEuler一定能持续不断的产出符合市场需求的高水平软件,甚至孵化出世界级的软件。

和一个大型购物中心会带动一个区域的消费繁荣与经济成长类似,openEuler建设起一个良好的运行框架体系后,随之崛起的将不仅是一个操作系统社区,更会是包含软件包、基础库、开发框架,工具链、前端交互系统、后端服务系统、用户反馈与交流系统,多语言支持等一系列功能的庞大集合体。社区将形成一个巨大的公共资源池,在统一的资源池上,结合构建工具,可以做到按照用户的不同需求,定制出不同类型操作系统的能力,推动高水平核心软件技术的探索,成为在全球软件产业举足轻重的力量。

以开源开放赢得合作伙伴认同支持,openEuler快速成长的核心动力

从数字世界的基础设施来讲,软硬件需要更坚实的底座;从全球计算产业迭代升级来讲,需要更符合未来计算需求的新操作系统,操作系统在这个特殊的历史机遇节点上,将肩负着转型的重大使命。事实上,对于openEuler而言,通过代码开源加强横向协作,通过基础软硬件适配、应用适配、整机集成适配等工作加强产业链上下游协作,是逐渐形成体系化的技术和产业生态的关键。

这一过程中,离不开社区参与者的参与。根据公开资料显示,自2019年12月31日开放源代码以来,openEuler已经成为国内最具活力的开源社区,下载超过40万次,2021年商业应用超过100万套。短短两年时间里,openEuler 社区吸引了近万名贡献者、300+家合作伙伴,建立近百个SIG组,放眼全球都是极快的速度。

熊伟深刻意识到,中国IT产业其实并不缺乏开源协作的能量和动力,只是之前缺乏一个有足够号召力的平台。当openEuler这样的平台建立起来后,产业蕴藏的能量就能迅速得以聚集、释放,创造出令人惊叹的成果。

在社区发布的第二个版本openEuler 20.09中,有超过30%的工作来自华为以外企业的贡献。20.09版是中国第一个由不同企业共同协作开发完成的大型操作系统平台,这在整个中国操作系统发展史上都一件具有里程碑意义的事件。感谢行业伙伴对openEuler的巨大热情与贡献,没有这样的共同努力,也就没有openEuler今天的优异成绩与无限潜力。

作为开放的社区,openEuler汇聚了企业,科研院所,高校,兄弟社区,个人爱好者,甚至中学生等各种力量。有些企业在商业上是存在竞争关系的,但在社区里,这些商业上竞争的公司能够坐在一起,共同制定技术标准,共同协作,共同完成产品研发,这为中国企业树立了产业合作最好的样板。众多科研院所,高校能够基于openEuler进行科研,教学,这不光为产学研架起了合作的桥梁,打通了科研和产业的通道,也为产业未来储备了更多的人才。这些伙伴在openEuler中所作的一切,都是在为中国的基础产业添砖加瓦。

在最新的openEuler 20.09版本中,华为的整体贡献量已经降到了百分之五十以下。openEuler已经真正成为了产业伙伴共同打造的公共基础平台,未来这一平台还将汇聚越来越多的伙伴,实现共建,共享,共治的社区践言。

从商场到连锁巨头:openEuler的未来图景

“如果把openEuler比作一家大型连锁商场,那么我最大的期望是openEuler能够实现国际化,成长为覆盖全球的连锁店”。谈到openEuler的未来发展,熊伟如此说到。

有些观点认为,未来的操作系统将演变成一个操作系统,也就是一个操作系统能覆盖所有场景。但熊伟的预测恰恰相反:“随着GPU、TPU,DPU等更多芯片种类,多种架构形式的出现,以及云计算、边缘计算、IoT等更多计算场景的差异化需求,操作系统产业可能会更具多样性,迎来更加丰富、更加多样的新时代。”

在技术演进方面,近年,多样化算力成为了芯片产业的热点, openEuler希望进一步加大多样化算力的技术探索,从系统和工具链底层为多样化算力提供优秀的支持。这种支持反过来还可以激励和支撑芯片产业的发展。

随着社区的进一步发展, openEuler社区需要始终秉持“共建、共享、共治”的理念,聚力打造多样性计算场景操作系统。因此,openEuler社区需要持续优化组织架构,保持良好、健康的生态体系运转,并在这一过程中构建良好的开源流程,建设懂开源、懂基础设施、懂芯片,懂体系架构的人才梯队。

openEuler从建立的开始阶段,就将目光投向了全球,openEuler不光在中国,更希望在全球赢得认可。早在2019年,openEuler就开始尝试在全球的一些地区进行拓展,这也为中国开源社区的国际化做出了自己的贡献。据熊伟介绍,openEuler在22年会加大海外拓展的力度,openEuler也希望和全球的合作伙伴,全球的各种开源社区形成国际化协作,开放共赢,力求在全球范围内逐渐构筑起自身影响力。希望在不远的将来,openEuler能够成为被全球企业所广泛使用的操作系统平台,并成为全球开发者和使用者喜欢的开源社区。欢迎大家加入openEuler社区,去参与,去建设,去分享,发出你们的光和热。


关于华为开源

作为可信赖的开源公民,华为通过持续贡献,携手伙伴,提倡包容、公平、开放和更团结的协作,共建世界级基础软件开源社区,加速行业数字化进程。

· 主流开源组织的积极参与者和支持者。目前华为已是数十个国际开源基金会的顶级/初创会员。

· 规模贡献开源基础软件,夯实数字基础设施生态底座,携手伙伴、开发者共建开源生态。近两年来,面向云原生、自动化和智能化,华为先后开源了KubeEdge、Mindspore、openEuler、openGauss、OpenHarmony等多个平台级基础软件开源项目,成为被全球开发者所接受的开源社区,并在各行业商用落地。

· 积极建设可持续发展、有生命力的可信开源社区。华为致力于完善社区生态治理架构,确保社区持续演进。

关注华为开源公众号,了解更多!

点击【此处】,进入华为开源官网了解更多

Flutter启动流程分析之插件化升级探索

2022年1月26日 03:39
作者 得物技术

Flutter是Google推出的一款跨平台框架。与Weex等其他跨端框架不同的是,Flutter的界面布局绘制是由自己完成的,而不是转换成对应平台的原生组件。那么各个平台是如何启动它的呢?从Flutter官方提供的架构图上看,Flutter Embedder层提供了底层操作系统到Flutter的程序入口,平台采用适合当前系统特性的方式去各自实现。本文基于flutter 2.0.6版本源码,来探索Android平台上flutter Embedder层对应的启动流程,看看这个过程中做了些什么事情,有什么问题是需要我们在项目中注意的。

这部分源码位于engine源码中的/engine/shell/platform/android/ 目录下。

1.主流程

先来看看整体的流程:

Android以FlutterActivity/FlutterFragment/FlutterView的形式承载flutter界面。当我们使用AndroidStudio创建一个新的flutter工程时,生成的MainActivity是直接继承了FlutterActivity,那么很明显,主要的逻辑都在这个FlutterActivity里面了。从流程图看到,flutter的启动流程也是从FlutterActivity的onCreate方法开始的:

1.FlutterActivity将onCreate主要的操作委托给delegate对象去实现。

2.delegate中调用setupFlutterEngine创建FlutterEngine。

3.FlutterEngine初始化各种channel之后,再创建FlutterLoader去加载资源文件和apk里的打包产物,之后初始化JNI的几个线程和DartVM。

4.delegate之后再通过FlutterEngine注册各个插件。

5.FlutterActivity调用delegate的onCreateView创建FlutterView。

6.最后,onStart生命周期中通过delegate的onStart方法执行DartExecutor.executeDartEntrypoint,这个方法会在jni层执行Dart代码的入口函数。至此启动完成。

1.1.FlutterActivity

FlutterActivity也是继承的Activity,但是它把主要的功能都委托给了FlutterActivityAndFragmentDelegate类去实现,实现的Host接口主要是支持在delegate中获取FlutterActivity的一些参数,比如configureFlutterEngine,这些方法可以由子类去重写,实现自定义配置。

接下来,我们看看FlutterActivity的onCreate(),主要的两个步骤是:

1.delegate.onAttach(this): 初始化FlutterEngine、注册各个插件。(注意,这里传的this即是delegate中的host对象)

2.setContentView(createFlutterView() ): 创建FlutterView并绑定到FlutterEngine。

这两个步骤都是委托给 FlutterActivityAndFragmentDelegate 去实现的。

1.2.FlutterActivityAndFragmentDelegate

1.2.1.onAttach

总结一下,onAttach中主要做了一下几件事情:

1.设置flutterEngine:

1.1.判断是否从缓存中获取;

1.2.判断是否有自定义flutterEngine;

1.3.new 一个新的flutterEngine对象;

  1. 将插件attach到host activity,最终会调用各个插件的onAttachedToActivity方法。

3.创建PlatformPlugin

4.注册插件。

1.2.2.configureFlutterEngine

这里说一下configureFlutterEngine(flutterEngine)主要是干什么的,这个方法是在FlutterActivity中实现的,代码如下:

它通过反射找到了GeneratedPluginRegistrant类,并调用了其registerWith方法。这个类我们可以在工程中的 /android/java/目录下找到,是flutter tool自动生成的,当我们在pubspec.yaml中添加一个插件,并执行pub get命令后即会生成。

系统默认使用反射实现,我们也可以在MainActivity中重写这个方法,直接调用registerWith方法。

1.3.FlutterEngine

再来看看FlutterEngine的构造函数。FlutterEngine是一个独立的flutter运行环境,通过它能使用DartExecutor执行Dart代码。

DartExecutor可以跟FlutterRenderer配合渲染UI,也可以在只在后台运行Dart代码,不渲染UI。

当初始化第一个FlutterEngine时,DartVM会被创建,之后可以继续创建多个FlutterEngine, 每个FlutterEngine对应的DartExecutor执行在不同的DartIsolate中,但同一个Native进程只有一个DartVM。

可以看到,这里面做的事情还是很多的:

1.初始化AssetsManager。

2.创建DartExecutor并设置对应PlatformMessageHandler

3.初始化一系列的系统channel。

4.初始化FlutterLoader,加载Resource资源和libflutter.so、libapp.so等apk产物。

5.创建FlutterRenderer、FlutterEngineConnectionRegistry。

6.如果需要,自动注册pubspec.yaml中声明的插件。

接下来看一下FlutterLoader相关的内容。

1.4.FlutterLoader

FlutterLoader以单例的形式存在,一个进程只用初始化一次。用来加载apk安装包中的资源文件和代码产物,必须在主线程中进行。

startInitialization()方法中主要做了以下几件事情:

1.加载传给activity的meta配置信息;

2.提取apk安装包中的assets资源,主要是在DEBUG和JIT_RELEASE模式下的产物 ,比如vmSnapshotData、isolateSnapshotData等;

3.加载flutter engine C++部分源码,即在flutterJNI执行System.loadLibrary("flutter")

public void ensureInitializationComplete(
    @NonNull Context applicationContext, @Nullable String[] args) {
  //多次调用无效
  if (initialized) {
    return;
  }
  ...
  try {
    //startInitializatioz中得到的几个资源文件目录
    InitResult result = initResultFuture.get();
    //这个列表中动态配置了flutter启动需要加载的一些资源的路径
    List<String> shellArgs = new ArrayList<>();
    shellArgs.add("--icu-symbol-prefix=_binary_icudtl_dat");
    //libflutter.so的路径
    shellArgs.add(
        "--icu-native-lib-path="
            + flutterApplicationInfo.nativeLibraryDir
            + File.separator
            + DEFAULT_LIBRARY);
    if (args != null) {
      //方法参数中传来的,可以在重写FltterActivity::getFlutterShellArgs()来自定义参数
      Collections.addAll(shellArgs, args);
    }
    String kernelPath = null;
    //DEBUG和JIT_RELEASE模式下只加载snapshot数据
    if (BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) {
      ...
      shellArgs.add("--" + SNAPSHOT_ASSET_PATH_KEY + "=" + snapshotAssetPath);
      shellArgs.add("--" + VM_SNAPSHOT_DATA_KEY + "=" + flutterApplicationInfo.vmSnapshotData);
      shellArgs.add(
          "--" + ISOLATE_SNAPSHOT_DATA_KEY + "=" + flutterApplicationInfo.isolateSnapshotData);
    } else {
    //RELEASE模式下加载libapp.so文件,这是Dart代码编译后的产物
    //默认是相对路径
      shellArgs.add(
          "--" + AOT_SHARED_LIBRARY_NAME + "=" + flutterApplicationInfo.aotSharedLibraryName);
      //同一个key可以存多个值,当根据前面的相对路径找不到文件时,再尝试用绝对路径加载
      shellArgs.add(
          "--"
              + AOT_SHARED_LIBRARY_NAME
              + "="
              + flutterApplicationInfo.nativeLibraryDir
              + File.separator
              + flutterApplicationInfo.aotSharedLibraryName);
    }
    ...
    //到jni层去初始化Dart VM和Flutter engine,该方法只可以被调用一次
    flutterJNI.init(
        applicationContext,
        shellArgs.toArray(new String[0]),
        kernelPath,
        result.appStoragePath,
        result.engineCachesPath,
        initTimeMillis);

    initialized = true;
  } catch (Exception e) {
    throw new RuntimeException(e);
  }
}

这个方法的作用是动态配置flutter引擎启动前的各种资源路径和其他配置,以 --key=value 的方式统一添加到shellArgs中,然后调用flutterJNI.init到C++层去处理,C++层会将传入的配置保存到一个setting对象中,之后根据setting创建FlutterMain对象,保存为一个全局静态变量g_flutter_main。之后初始化DartVM等步骤就可以用到这里保存的配置信息了。

1.5.onStart

根据Android中Activity的生命周期,onCreate执行完之后就是onStart了。同样的,FlutterView还是将onStart中的操作委托给了delegate对象去完成。

可以看到,onStart生命周期就做了一件事情:执行Dart代码的入口函数。这里有一些需要注意的地方:

  1. DartExecutor只会执行一次,这意味着一个FlutterEngine对应的DartExecutor不支持重启或者重载

2.Dart Navigator的初始路由默认是"/"。我们可以重写getInitialRoute来自定义。

3.Dart 入口函数 默认是main(),重写 getDartEntrypointFunctionName 方法可以自定义。

  1. executeDartEntrypoint最终会通过FlutterJNI的方法来调用JNI方法来执行。在UI Thread中执行DartIsolate.Run(config),根据entrypoint_name找到Dart入口的句柄后运行_startIsolate执行入口函数,之后执行main函数的runApp()。

至此,Flutter项目成功在Android平台上启动完成。

2.应用-热更新

其实我这次探索Flutter启动流程的一个主要目的是寻找Flutter在Android侧的热更新方案。那么看完了整个流程之后,我们要如何做到热更新呢?

flutter app的apk安装包的几个主要产物是,flutter_assets、libflutter.so和libapp.so:

flutter_assets:包含flutter应用项目中的资源文件,font、images、audio等;

libflutter.so:flutter embedder层相关的C++代码。

libapp.so:我们写的Dart代码编译后的产物

只要可以在加载之前动态替换掉libapp.so这个文件,即可实现flutter代码的热更新。

2.1.方法一:反射修改FlutterLoader

那么libapp.so是在哪里加载的呢?其实上面 1.4.FlutterLoader 已经提到了,在ensureInitializationComplete()方法中,有一个shellArgs列表存储了资源路径配置信息。libapp.so对应的key是 "aot-shared-library-name"

那么,只要替换掉这一块代码,将路径设置成自定义的路径即可让框架去加载新的libapp_fix.so文件。具体步骤是:

1.继承FlutterLoader,重写ensureInitializationComplete(),将 "aot-shared-library-name" 对应的路径设置成自定义的路径。

2.我们看看flutterEngine中是怎么创建的FlutterLoader实例的:

flutterLoader = FlutterInjector.instance().flutterLoader();

那么,我们只要实例化自定义的FlutterLoader类,并通过反射的方式将FlutterInjector中的flutterLoader实例替换成新的实例即可。

2.2.方法二:重写getFlutterShellArgs()

我们注意到ensureInitializationComplete()方法中往AOT_SHARED_LIBRARY_NAME这个key里面添加了2个值,只有当相对路径下找不到文件的情况下才回去寻找绝对路径下的文件。那么我们只要将自定义的so文件路径设置成 "aot-shared-library-name" 第一条value就可以让框架只加载最新的安装包了。

由于ensureInitializationComplete()方法会将参数String[] args中的内容全部加入shellArgs列表,那么我们只要在args中加上 "aot-shared-library-name=自定义路径" 这一条配置就行了,我们看看这个args参数怎么来的:

host.getFlutterShellArgs().toArray()即使args参数的来源了。从之前的分析,我们已经知道了,delegate中的host对象是FlutterActivity的引用,我们再来看看FlutterActivity是怎么实现的:

这是一个public方法,那么我们只要在MainActivity中重写这个方法,并在获取到FlutterShellArgs之后将需要的配置添加进去即可:

很明显,这个方法更加简单有效。需要注意的是,这个配置只会在RELEASE模式下加载,所以DEBUG和JIT_RELEASE模式模式下调试是不起作用的。

3.总结

最后,大致进行一下总结:

1.纯flutter项目中,Android默认以FlutterActivity的形式承载flutter界面。Native-Flutter混合工程中还可以使用FlutterFragment/FlutterView2种方式,具体看使用场景。

2.FlutterActivity将绝大部分工作委托给FlutterActivityAndFragmentDelegate实现。

3.启动过程主要是FlutterActivity的onCreate()和onStart()方法。

onCreate() 会初始化FlutterEngine、注册各个插件,之后创建FlutterView并绑定到FlutterEngine。

onStart() 主要是通过DartExecutor去执行Dart代码的入口函数。

4.初始化第一个FlutterEngine时会创建和初始化DartVM。可以创建多个FlutterEngine,一个FlutterEngine对应一个DartExecutor,每个DartExecutor在自己的DartIsolate中执行。

5.DartExecutor可以和FlutterRender配合渲染UI,也可以只执行Dart代码不渲染UI。

6.FlutterView有两种模式:FlutterSurfaceView和FlutterTextureView。顾名思义,即分别使用surfaceView和textureView来承载flutter视图。FlutterSurfaceView渲染性能更好,但是视图在Native-Flutter混合工程中不支持灵活的z-index设置。

文/KECHANGZHAO

关注得物技术,做最潮技术人!

清除 useEffect 副作用

2022年1月27日 15:06
作者 公子

在 React 组件中,我们会在 useEffect() 中执行方法,并返回一个函数用于清除它带来的副作用影响。以下是我们业务中的一个场景,该自定义 Hooks 用于每隔 2s 调用接口更新数据。

import { useState, useEffect } from 'react';

export function useFetchDataInterval(fetchData) {
  const [list, setList] = useState([]);
  useEffect(() => {
    const id = setInterval(async () => {
      const data = await fetchData();
      setList(list => list.concat(data));
    }, 2000);
    return () => clearInterval(id);
  }, [fetchData]);

  return list;
}

🐚 问题

该方法的问题在于没有考虑到 fetchData() 方法的执行时间,如果它的执行时间超过 2s 的话,那就会造成轮询任务的堆积。而且后续也有需求把这个定时时间动态化,由服务端下发间隔时间,降低服务端压力。

所以这里我们可以考虑使用 setTimeout 来替换 setInterval。由于每次都是上一次请求完成之后再设置延迟时间,确保了他们不会堆积。以下是修改后的代码。

import { useState, useEffect } from 'react';

export function useFetchDataInterval(fetchData) {
  const [list, setList] = useState([]);
  useEffect(() => {
    let id;
    async function getList() {
      const data = await fetchData();
      setList(list => list.concat(data));
      id = setTimeout(getList, 2000);
    }
    getList();
    return () => clearTimeout(id);
  }, [fetchData]);

  return list;
}

不过改成 setTimeout 之后会引来新的问题。由于下一次的 setTimeout 执行需要等待 fetchData() 完成之后才会执行。如果在 fetchData() 还没有结束的时候我们就卸载组件的话,此时 clearTimeout() 只能无意义的清除当前执行时的回调,fetchData() 后调用 getList() 创建的新的延迟回调还是会继续执行。

在线示例:CodeSandbox

可以看到在点击按钮隐藏组件之后,接口请求次数还是在继续增加着。那么要如何解决这个问题?以下提供了几种解决方案。

🌟如何解决

🐋 Promise Effect

该问题的原因是 Promise 执行过程中,无法取消后续还没有定义的 setTimeout() 导致的。所以最开始想到的就是我们不应该直接对 timeoutID 进行记录,而是应该向上记录整个逻辑的 Promise 对象。当 Promise 执行完成之后我们再清除 timeout,保证我们每次都能确切的清除掉任务。

在线示例:CodeSandbox

import { useState, useEffect } from 'react';

export function useFetchDataInterval(fetchData) {
  const [list, setList] = useState([]);
  useEffect(() => {
    let getListPromise;
    async function getList() {
      const data = await fetchData();
      setList((list) => list.concat(data));
      return setTimeout(() => {
        getListPromise = getList();
      }, 2000);
    }

    getListPromise = getList();
    return () => {
      getListPromise.then((id) => clearTimeout(id));
    };
  }, [fetchData]);
  return list;
}

🐳 AbortController

上面的方案能比较好的解决问题,但是在组件卸载的时候 Promise 任务还在执行,会造成资源的浪费。其实我们换个思路想一下,Promise 异步请求对于组件来说应该也是副作用,也是需要”清除“的。只要清除了 Promise 任务,后续的流程自然不会执行,就不会有这个问题了。

清除 Promise 目前可以利用 AbortController 来实现,我们通过在卸载回调中执行 controller.abort() 方法,最终让代码走到 Reject 逻辑中,阻止了后续的代码执行。

在线示例:CodeSandbox

import { useState, useEffect } from 'react';

function fetchDataWithAbort({ fetchData, signal }) {
  if (signal.aborted) {
    return Promise.reject("aborted");
  }
  return new Promise((resolve, reject) => {
    fetchData().then(resolve, reject);
    signal.addEventListener("aborted", () => {
      reject("aborted");
    });
  });
}
function useFetchDataInterval(fetchData) {
  const [list, setList] = useState([]);
  useEffect(() => {
    let id;
    const controller = new AbortController();
    async function getList() {
      try {
        const data = await fetchDataWithAbort({ fetchData, signal: controller.signal });
        setList(list => list.concat(data));
        id = setTimeout(getList, 2000);
      } catch(e) {
        console.error(e);
      }
    }
    getList();
    return () => {
      clearTimeout(id);
      controller.abort();
    };
  }, [fetchData]);

  return list;
}

🐬 状态标记

上面一种方案,我们的本质是让异步请求抛错,中断了后续代码的执行。那是不是我设置一个标记变量,标记是非卸载状态才执行后续的逻辑也可以呢?所以该方案应运而生。

定义了一个 unmounted 变量,如果在卸载回调中标记其为 true。在异步任务后判断如果 unmounted === true 的话就不走后续的逻辑来实现类似的效果。

在线示例:CodeSandbox

import { useState, useEffect } from 'react';

export function useFetchDataInterval(fetchData) {
  const [list, setList] = useState([]);
  useEffect(() => {
    let id;
    let unmounted;
    async function getList() {
      const data = await fetchData();
      if(unmounted) {
        return;
      }

      setList(list => list.concat(data));
      id = setTimeout(getList, 2000);
    }
    getList();
    return () => {
      unmounted = true;
      clearTimeout(id);
    }
  }, [fetchData]);

  return list;
}

🎃 后记

问题的本质是一个长时间的异步任务在过程中的时候组件卸载后如何清除后续的副作用。

这个其实不仅仅局限在本文的 Case 中,我们大家平常经常写的在 useEffect 中请求接口,返回后更新 State 的逻辑也会存在类似的问题。

只是由于在一个已卸载组件中 setState 并没有什么效果,在用户层面无感知。而且 React 会帮助我们识别该场景,如果已卸载组件再做 setState 操作的话,会有 Warning 提示。

再加上一般异步请求都比较快,所以大家也不会注意到这个问题。

所以大家还有什么其他的解决方法解决这个问题吗?欢迎评论留言~

注: 题图来自《How To Call Web APIs with the useEffect Hook in React》
昨天 — 2022年1月27日SegmentFault 最新的文章

Apache 首位华人董事吴晟:开源里的“偷懒”学

2022年1月17日 06:55
作者 腾源会

2019 年 4 月 ,Apache 软件基金会宣布 Apache SkyWalking (以下简称“ SkyWalking ”)毕业,成为 Apache 顶级项目。2021年 3 月,Apache SkyWalking 创始人吴晟成功当选 Apache 软件基金会董事,成为 Apache 董事团队中的首位中国人。目前,已有超过 100 家公司公开宣布在使用 SkyWalking,来自于数百家公司的超过 550 位源码贡献者活跃于 SkyWalking 社区。

12月,腾源会荣幸地邀请了吴晟加入成为腾源会导师,试图聊聊他的开源理念和最佳实践。在这篇访谈内容中,我们虽然讲述的是他在开源中的“偷懒”学,但实则传递的是他这几年基于 SkyWalking 项目和社区运营所沉淀的最宝贵的实践方法论,以及他对开源社区管理最深刻的理念和认知。

image.png

PART ONE
前言:“懒”是创造力的源泉

我很喜欢用「懒」这个词。因为在我看来「懒」其实是很多创造力的源泉,因为“懒”得走路,所以有人发明了车;因为“懒”得写字,所以有人发明了语音;正因为很多东西「不方便」,所以才会促使我们去解决问题。

我会认为我一天应该只工作8个小时,甚至更少的时间,但最好能挣一样甚至更多的钱,这是我做所有事情给自己预设的条件。所以我希望我能以最少的时间去创造更大的价值。因为第一身体是自己的,第二只有休息的时候才有创造力,而工作的时候是很难有的。

PART TWO
“偷懒”学招式一:让项目实现高度模块化

很多开源团队在做社区运营的时候都会遇到一个问题,项目使用者虽然很多,但最终真正愿意反哺贡献给社区的人却十分的稀少。

这个问题需要我们从两方面看待和反思。首先我们要明白,人的意识是需要培养的。任何一个使用你的开源项目的人,并不会在刚开始就存在“我想要向你贡献代码”的意识,因为他本身基于这个项目的二次开发可能仅仅只是为了公司的服务,所以这并不能促使他产生要去反哺社区的想法。

但是作为项目核心人员来说,有两件事却是我们必须需要做的:第一个是在技术层面,你需要去审视你的项目是否适合被贡献。现在很多项目从设计上看都比较“内聚”,当改动项目中的一行代码,就有可能会影响到整个项目的代码和逻辑。这就意味着使用者在更改项目的任何一处地方前都要去考虑社区所有的用户的所有的需求。所以除非他是一名职业开发者,否则根本就不可能考虑完全,这就会从某种程度上变相地提高了项目贡献的门槛。

所以可以看到,不管是 Linux,Istio 还是 SkyWalking ,都有一个很大的特点:当项目被「高度模块化」之后,贡献者就会开始急剧的提高。

绝大部分好的开源软件,都经过了十分精良的设计,有着大量模块化的设计思路,以确保这个项目在修改某个区域的实现的时候,不会影响到其他区域。这对于项目最初的创造者和项目人员来说,有着非常高的设计和技术要求。

核心共建者有多长时间,项目就可以发展多少,这不是我们想看到的事情。尽管我们知道项目将来可以运用到非常多不同的场景中,但我们只会做好一个场景,并开放接口,预留给其他开发者去创新和拓展的空间。这需要核心共建者在不停的去审视整个行业,保持项目的前瞻性。而有些项目创造者会更执着于实现项目的功能,而没有提供给其他开发者创新和拓展的空间,这就会导致最终也只能由自己去做进一步维护和修改。

目前 SkyWalking 已经进入了不需要我去监管,放心让它自运行的阶段。我们为它建立了一整套从代码模块规范、自动化检查和各种规模的自动化测试,来保证它不会将任何意外代码合并起来,几乎可以保证每一个提交后的代码都可以被用于生产服务。所以这也是为什么 SkyWalking 只需要 2-3 个人 Review 代码,就能将这个庞大且复杂的社区运营起来的原因。

image.png
图注:Apache SkyWalking 项目基础介绍

PART THREE
“偷懒”学招式二:
被“逼”出来的项目贡献者

总有人说中国的开源环境不好,在我来看,是因为中国很多开源项目都把大家服务的太好了。

目前 SkyWalking 几乎没有给开源社区用户提供太多的技术支持。我们认为 SkyWalking 已经为开发者提供了足够多的项目文档,去支持他们了解项目,解决问题以及对它进行进一步的拓展和开发。除非问题已经影响到了项目原本的功能,我们可能会提供比较积极的帮助。

如果大家在 SkyWalking 社区里,不管是QQ群还是其他渠道,我们的维护者们(也包括我自己)都很少回答来自社区的各种提问。我们认为,文档已经给到用户了,他要 Report 就走正常的渠道,但即使 Report 的是一些真真实实存在的问题,也不代表社区会帮助去修复。

如果我们认为用户反馈的这个问题确实影响到了项目本身功能,我可能会相对比较积极的帮助,但如果是一个弱关联或无关紧要的问题,可能我根本就不会在意这是不是一个bug。

这是我们在北美公司一贯惯行的原则,我们花精力在开源项目里面,是为了更好地为商业公司服务。而花在开源社区里面的时间,更多是在下班之后我的业余时间。

当我以这样的心态看待时,我并不会在意其他人是否会喜欢这个开源软件,因为我并没有责任和必要去花自己的时间解决遇到的所有问题,这本身就不是商业服务的范畴。

事实上,中国在享受全世界最好的开源项目的服务,甚至比商业项目还多,但 SkyWalking 社区并不会这么去做。长久来看,实际上这会对开源项目“共建性”造成极大的伤害。因为从某种程度上来说,社区贡献者是需要被“逼”出来的,当项目不提供其他技术支持时,你唯一的选择就是要么贡献,要么就忍着。

PART FOUR
“偷懒”学招式三:
“The Best Thing as the Perfect
Founder is Disappear
and Enjoy My Vacation”

我认为好的管理者一定要学会放权,尤其在开源社区中。做开源项目非常重要的一点就是,你能不能做到一个Ownership的转移,以及Share。

每一个大型开源项目都不是一个个人的开源项目,作为项目创始人,也许你可以永久享受这个项目“ Founder ”的头衔,为这个称号感到荣幸,也很高兴曾经创造了一个项目,能为大家来用,但是也仅仅如此。你的开源项目今天、明天能做的一切,以及十年后是不是能活下来,这一切仰仗于社区其他人。

所以我作为项目的 Founder ,我可以去休假,可以短暂地从项目里消失,至少社区中每天日常的 Review 并不一定要由我去处理。大家可能会发现我可能很积极会去回,那是因为我有时间,但并不代表一定要我去回。SkyWalking 中有 30 多个子项目,都有不同的人在Review,不同的 PMC 在管理着这些子项目,以及这些子项目对外的集成。

中国有很多的项目,嘴上说着希望大家来贡献,但是心里面却想着我不想把这个程序让你来干,这其中的原因大多来自于“我并不想把代码的控制权交出去”。
本质上,这确实可能是一个很难的心理过程,因为我们的成长环境都让我们形成了一种无形“潜意识”,如果你是领导就是你说了算,下面的人没有决策权。但如果在开源社区中,不构建人人平等的理念,不把自己的所属权放出去,把代码的所属权放出去,是很难调动大家的主动性的。

另外,对于早期的项目来说,你需要思考的第一个问题是谁需要你,而不是你想做什么。

这个边界是其实很清晰,但是对于很多人来说可能很纠结的一个事情。你想做什么,也许你心里有一份技术方案。但如果你不仅仅是以娱乐为目的,而是真的想让你的开源项目有更多人使用,就需要你在项目初期就想清楚你的项目的定位,它要解决什么样的问题,去看看别人需要你是什么,需要你去考虑当前这个行业共性有什么。而你所做的项目是否有共性,将决定你以后是否能走向一定的高度。

在想清楚这些问题后,你需要给项目设立一个明确的目标。每一个版本的迭代都是在不同的时期围绕项目的不同目标去做不同的事情,所以每一个小的版本号都有着明确的目标。通过不停的去试错、更新版本才能将它进一步的完善。这需要你对时间有着心里准备,因为大家所熟知的好的项目,都是经历了很多年的迭代,慢慢熬出来的。

另外,其实 SkyWalking 出海容易的一个原因,就是因为通过朋友的介绍。关于“走出去”这件事情,对于我或者项目来说,与其让我去雇很多人来做运营,我更愿意让项目的几位核心的贡献者去保持非常强的社交的能力,能与更多人去交流和沟通,才能项目真正地将项目带出去,而不仅仅是完全凭借自己的一个人的力量去推动。

PART FIVE
精彩 QA 回放集锦

Q:2017年前,SkyWalking 可能都不是一个能引爆流行度的开源项目,但它今天获得了规模化应用。这其中的突破临界点是什么?是什么原因?

在2017年之前, SkyWalking 都只是一个玩具。所以 2017 年是它突破的临界点的时间。

我们正式决定做这个事情是在 2017 年的 GOPS 大会上,当时的版本号是3.2.6,跟今天依然差距很远。但是在今天看来,3.2.6是我们第一次决定让 SkyWalking 在生产中运行,而在3.2.6之后,我们决定采用很多以前只在商业环境里面采用的设计,把它变成一个真正适合运维的 APM产品。

因此,SkyWalking 其实从 2017 年- 2019 年才开始流行起来和得到比较广泛的应用,所以从第一行代码到这个项目真的被运用,其实花了将近4~5年的时间,而大家看到的是一个指数级分布,反而从2019年到现在,是一个最不重要的时间点,因为那是一个正常的传播效应。

当达到一个临界点的时候,那是一个正常的传播效应,只是说还是快还是慢,仅此而已。

Q:SkyWalking 从进入 Apache 到毕业成为顶级项目仅用了一年半的时间。Apache 顶级项目的标准是什么?那一年半 SkyWalking 经历了什么?

其实这个问题的根本是,你对自己的项目团队的认知,或者说你得知道什么叫做健康的开源项目。

Apache 基金会里其实有很多项目并不一定社区很大,或者有很多参与,现在大概有300多个顶级项目,可能绝大多数中国人连听都没听过,然后有一些项目里面可能就只有三五个人,这都很正常。

那么在我看来,顶级开源项目并不等同于有多火,Star数有多高。而是:

第一,像上面提到的需要营造一个公平的环境,不应该是某家公司或者某个人在控制着这个项目,如果你要控制一个开源项目,你在公司开源就好了,没有必要放到基金会来。

第二,你是不是能够非常好的认可你的开发者们,当他们对项目做出了大的贡献的时候,你是不是能够吸纳他们进来,给他们权力,让他们提交和审核别人提交代码。

第三,你是不是能够建立一个基本良性的社区,有人贡献,有人提代码,有人提议,有人用。

最后,你的项目知识产权是不是清晰,这可能是最难的一件事情,因为很多开源项目不完全都是自己写的,会有大量的组件层面的依赖,关键就是你是不是合理合规地使用了这些依赖,基金会会对此会有非常严格的审查。
采访嘉宾介绍:吴晟,Apache 软件基金会首位华人董事,开源 APM 项目 SkyWalking 创始人,分布式追踪与诊断技术专家,骨灰级开源社区爱好者。先后在大唐软件、亚信中国、华为从事技术工作;2018 年 5 月,加入 To B 级混合云外企 Tetrate,成为公司创始工程师。

腾源会(WeOpen)是腾讯云成立的汇聚开源项目、开源爱好者、开源领导者的开放社区,致力于帮助开源项目健康成长、开源爱好者能交流协助、开源领导者能发挥领袖价值,让全球开源生态变得更加繁荣。

image.png

从服务端生成Excel电子表格(Node.js+SpreadJS)

Node.js是一个基于Chrome V8引擎的JavaScript运行环境,通常用于创建网络应用程序。它可以同时处理多个连接,并且不像其他大多数模型那样依赖线程。

对于 Web 开发者来说,从数据库或Web服务器获取数据,然后输出到Excel文件以进行进一步分析的场景时有发生。我们的技术团队在跟国内外各行各业用户交流的过程中,就曾发现有很多的用户尝试在Node.js的环境下运行SpreadJS 纯前端表格控件,借助该控件,可以在服务器不预装任何Excel依赖项的情况下,收集用户输入的信息,并将其自动导出到Excel文件中。

为了满足广大技术爱好者的需要,同时减少大家在未来技术选型方面所走的弯路,本文将就SpreadJS 与 Node.js之间的技术性方案进行探讨!

一、安装 SpreadJS 和 Node .js

首先,我们需要安装Node.js以及Mock-Browser,BufferJS和FileReader,大家可以前往以下链接进行下载,同步操作:

我们将使用Visual Studio创建应用程序。打开Visual Studio后,使用JavaScript\&gt; Node.js\&gt;Blank Node.js控制台应用程序模板创建一个新应用程序。这将自动创建所需的文件并打开" app.js"文件,也是我们将要更改的唯一文件。

对于BufferJS库,您需要下载该软件包,然后通过导航到项目文件夹(一旦创建)并运行以下命令,将其手动安装到项目中:

npm install

安装完成后,您可能需要打开项目的package.json文件并将其添加到" dependencies"部分。文件内容应如下所示:

{
"name": "spread-sheets-node-jsapp",
"version": "0.0.0",
"description": "SpreadSheetsNodeJSApp",
"main": "app.js",
"author": {
   "name": "admin"
},
"dependencies": {
   "FileReader": "^0.10.2",
   "bufferjs": "1.0.0",
   "mock-browser": "^0.92.14"
  }
}

在此示例中,我们将使用Node.js的文件系统模块。我们可以将其加载到:

var fs = require('fs')

为了将SpreadJS与Node.js结合使用,我们还需要加载已安装的Mock-Browser:

var mockBrowser =require('mock-browser').mocks.MockBrowser

在加载SpreadJS脚本之前,我们需要初始化模拟浏览器。初始化我们稍后在应用程序中可能需要使用的变量,尤其是" window"变量:

global.window =mockBrowser.createWindow()
global.document = window.document
global.navigator = window.navigator
global.HTMLCollection =window.HTMLCollection
global.getComputedStyle =window.getComputedStyle

初始化FileReader库:

var fileReader = require('filereader');
global.FileReader = fileReader;

二、使用SpreadJS npm 包

将SpreadJS安装文件中的SpreadJS Sheets和ExcelIO包添加到项目中。

您可以通过右键单击解决方案资源管理器的" npm"部分并将它们添加到您的项目中,然后选择"安装新的NPM软件包"。您应该能够搜索" GrapeCity"并安装以下2个软件包:

@grapecity/spread-sheets
@grapectiy/spread-excelio

将SpreadJS npm软件包添加到项目后,正确的依赖关系将被写入package.json:

1.    {
2.    "name": "spread-sheets-node-jsapp",
3.    "version": "0.0.0",
4.    "description": "SpreadSheetsNodeJSApp",
5.    "main": "app.js",
6.    "author": {
7.       "name": "admin"
8.    },
9.      "dependencies":{
10.       "@grapecity/spread-excelio": "^11.2.1",
11.       "@grapecity/spread-sheets": "^11.2.1",
12.       "FileReader": "^0.10.2",
13.       "bufferjs": "1.0.0",
14.       "mock-browser": "^0.92.14"
15.      }
16.    }

现在我们需要在app.js文件中引入它:

var GC =require('@grapecity/spread-sheets')
var GCExcel =require('@grapecity/spread-excelio');

使用npm软件包时,还需要设置许可证密钥(点击此处,免费申请许可证密钥):

GC.Spread.Sheets.LicenseKey ="<YOUR KEY HERE>"

在这个特定的应用程序中,我们将向用户显示他们正在使用哪个版本的SpreadJS。为此,我们可以引入package.json文件,然后引用依赖项以获取版本号:

var packageJson =require('./package.json')
console.log('\n** Using Spreadjs Version"' + packageJson.dependencies["@grapecity/spread-sheets"] +'" **')

三、将 Excel 文件加载到您的 Node.js 应用程序中

点击此处,下载现成的Excel模板文件,该文件包含了从用户那里获取数据。接下来,将数据放入文件中并导出。在这种情况下,文件是用户可以编辑的状态。

初始化工作簿和ExcelIO变量:

var wb = new GC.Spread.Sheets.Workbook();
var excelIO = new GCExcel.IO();

我们在读取文件时将代码包装在try / catch块中。然后,初始化变量" readline",让您读取用户输入到控制台的数据。接下来,我们将其存储到一个JavaScript数组中,以便轻松填写Excel文件:

// Instantiate the spreadsheet and modifyit
console.log('\nManipulatingSpreadsheet\n---');
try {
   var file = fs.readFileSync('./content/billingInvoiceTemplate.xlsx');
   excelIO.open(file.buffer, (data) => {
       wb.fromJSON(data);
       const readline = require('readline');
       var invoice = {
            generalInfo: [],
            invoiceItems: [],
            companyDetails: []
       };
   });
} catch (e) {
   console.error("** Error manipulating spreadsheet **");
   console.error(e);
}

四、收集用户输入信息

上图显示了我们正在使用的Excel文件。我们可以在excelio.open调用中创建一个单独的函数,以在控制台中提示用户需要的每一项内容。我们也可以创建一个单独的数组,将数据保存到每个输入后,然后将其推送到我们创建的invoice.generalInfo数组中:

fillGeneralInformation();
function fillGeneralInformation() {
   console.log("-----------------------\nFill in InvoiceDetails\n-----------------------")
   const rl = readline.createInterface({
       input: process.stdin,
       output: process.stdout
   });
   var generalInfoArray = [];
   rl.question('Invoice Number: ', (answer) => {
       generalInfoArray.push(answer);
       rl.question('Invoice Date (dd Month Year): ', (answer) => {
           generalInfoArray.push(answer);
            rl.question('Payment Due Date (ddMonth Year): ', (answer) => {
                generalInfoArray.push(answer);
                rl.question('Customer Name: ',(answer) => {
                   generalInfoArray.push(answer);
                    rl.question('CustomerCompany Name: ', (answer) => {
                       generalInfoArray.push(answer);
                        rl.question('Customer Street Address:', (answer) => {
                           generalInfoArray.push(answer);
                           rl.question('Customer City, State, Zip (<City>, <State Abbr><Zip>): ', (answer) => {
                                generalInfoArray.push(answer);
                               rl.question('Invoice Company Name: ', (answer) => {
                                   generalInfoArray.push(answer);
                                   rl.question('Invoice Street Address: ', (answer) => {
                                       generalInfoArray.push(answer);
                                       rl.question('Invoice City, State, Zip (<City>, <State Abbr><Zip>): ', (answer) => {
                                            generalInfoArray.push(answer);
                                           rl.close();
                                           invoice.generalInfo.push({
                                               "invoiceNumber": generalInfoArray[0],
                                               "invoiceDate": generalInfoArray[1],
                                               "paymentDueDate": generalInfoArray[2],
                                               "customerName": generalInfoArray[3],
                                               "customerCompanyName": generalInfoArray[4],
                                               "customerStreetAddress": generalInfoArray[5],
                                               "customerCityStateZip": generalInfoArray[6],
                                               "invoiceCompanyName": generalInfoArray[7],
                                               "invoiceStreetAddress": generalInfoArray[8],
                                               "invoiceCityStateZip": generalInfoArray[9],
                                            });
                                           console.log("General Invoice Information Stored");
                                           fillCompanyDetails();
                                        });
                                    });
                               });
                            });
                        });
                    });
                });
            });
       });
   });
}

该函数被称为" fillCompanyDetails",目的是收集有关公司的信息以填充到工作簿的第二张表中:

function fillCompanyDetails() {
   console.log("-----------------------\nFill in CompanyDetails\n-----------------------")
   const rl = readline.createInterface({
       input: process.stdin,
       output: process.stdout
   });
   var companyDetailsArray = []
   rl.question('Your Name: ', (answer) => {
       companyDetailsArray.push(answer);
       rl.question('Company Name: ', (answer) => {
            companyDetailsArray.push(answer);
            rl.question('Address Line 1: ',(answer) => {
               companyDetailsArray.push(answer);
                rl.question('Address Line 2: ',(answer) => {
                   companyDetailsArray.push(answer);
                    rl.question('Address Line3: ', (answer) => {
                       companyDetailsArray.push(answer);
                        rl.question('AddressLine 4: ', (answer) => {
                           companyDetailsArray.push(answer);
                           rl.question('Address Line 5: ', (answer) => {
                               companyDetailsArray.push(answer);
                               rl.question('Phone: ', (answer) => {
                                   companyDetailsArray.push(answer);
                                   rl.question('Facsimile: ', (answer) => {
                                       companyDetailsArray.push(answer);
                                        rl.question('Website: ', (answer)=> {
                                           companyDetailsArray.push(answer);
                                           rl.question('Email: ', (answer) => {
                                                companyDetailsArray.push(answer);
                                               rl.question('Currency Abbreviation: ', (answer) => {
                                                   companyDetailsArray.push(answer);
                                                    rl.question('Beneficiary: ',(answer) => {
                                                       companyDetailsArray.push(answer);
                                                       rl.question('Bank: ', (answer) => {
                                                            companyDetailsArray.push(answer);
                                                           rl.question('Bank Address: ', (answer) => {
                                                               companyDetailsArray.push(answer);
                                                               rl.question('Account Number: ', (answer) => {
                                                                   companyDetailsArray.push(answer);
                                                                    rl.question('RoutingNumber: ', (answer) => {
                                                                       companyDetailsArray.push(answer);
                                                                       rl.question('Make Checks Payable To: ', (answer) => {
                                                                           companyDetailsArray.push(answer);
                                                                            rl.close();
                                                                           invoice.companyDetails.push({
                                                                               "yourName": companyDetailsArray[0],
                                                                               "companyName": companyDetailsArray[1],
                                                                               "addressLine1": companyDetailsArray[2],
                                                                               "addressLine2": companyDetailsArray[3],
                                                                               "addressLine3": companyDetailsArray[4],
                                                                               "addressLine4": companyDetailsArray[5],
                                                                               "addressLine5": companyDetailsArray[6],
                                                                                "phone":companyDetailsArray[7],
                                                                               "facsimile": companyDetailsArray[8],
                                                                                "website":companyDetailsArray[9],
                                                                               "email": companyDetailsArray[10],
                                                                               "currencyAbbreviation":companyDetailsArray[11],
                                                                               "beneficiary": companyDetailsArray[12],
                                                                               "bank":companyDetailsArray[13],
                                                                               "bankAddress": companyDetailsArray[14],
                                                                               "accountNumber": companyDetailsArray[15],
                                                                               "routingNumber": companyDetailsArray[16],
                                                                               "payableTo": companyDetailsArray[17]
                                                                           });
                                                                           console.log("Invoice Company Information Stored");
                                                                            console.log("-----------------------\nFillin Invoice Items\n-----------------------")
                                                                           fillInvoiceItemsInformation();
                                                                        });
                                                                   });
                                                               });
                                                           });
                                                       });
                                                   });
                                               });
                                            });
                                        });
                                    });
                                });
                            });
                        });
                    });
                });
            });
       });
   });
}




现在我们已经有了用户的基本信息,我们可以集中精力收集单个项目,并另命名为" fillInvoiceItemsInformation"函数。在每个项目执行之前,我们会询问用户是否要添加一个项目。如果他们继续输入" y",那么我们将收集该项目的信息,然后再次询问直到他们键入" n":

function fillInvoiceItemsInformation() {
   const rl = readline.createInterface({
       input: process.stdin,
       output: process.stdout
   });
   var invoiceItemArray = [];
   rl.question('Add item?(y/n): ', (answer) => {
       switch (answer) {
            case "y":
               console.log("-----------------------\nEnter ItemInformation\n-----------------------");
                rl.question('Quantity: ',(answer) => {
                   invoiceItemArray.push(answer);
                    rl.question('Details: ',(answer) => {
                       invoiceItemArray.push(answer);
                        rl.question('UnitPrice: ', (answer) => {
                           invoiceItemArray.push(answer);
                           invoice.invoiceItems.push({
                               "quantity":invoiceItemArray[0],
                               "details": invoiceItemArray[1],
                               "unitPrice": invoiceItemArray[2]
                            });
                            console.log("ItemInformation Added");
                            rl.close();
                           fillInvoiceItemsInformation();
                        });
                    });
                });
                break;
            case "n":
               rl.close();
                return fillExcelFile();
                break;
            default:
                console.log("Incorrectoption, Please enter 'y' or 'n'.");
       }
   });
}

五、填入您的Excel 文件

在收集所有必需的用户信息后,我们可以将其填入到Excel文件中:

function fillExcelFile() {
   console.log("-----------------------\nFilling in Excelfile\n-----------------------");
   fillBillingInfo();
   fillCompanySetup();
}
function fillBillingInfo() {
   var sheet = wb.getSheet(0);
   sheet.getCell(0, 2).value(invoice.generalInfo[0].invoiceNumber);
   sheet.getCell(1, 1).value(invoice.generalInfo[0].invoiceDate);
   sheet.getCell(2, 2).value(invoice.generalInfo[0].paymentDueDate);
   sheet.getCell(3, 1).value(invoice.generalInfo[0].customerName);
   sheet.getCell(4, 1).value(invoice.generalInfo[0].customerCompanyName);
   sheet.getCell(5, 1).value(invoice.generalInfo[0].customerStreetAddress);
   sheet.getCell(6, 1).value(invoice.generalInfo[0].customerCityStateZip);
   sheet.getCell(3, 3).value(invoice.generalInfo[0].invoiceCompanyName);
   sheet.getCell(4, 3).value(invoice.generalInfo[0].invoiceStreetAddress);
   sheet.getCell(5, 3).value(invoice.generalInfo[0].invoiceCityStateZip);
}
function fillCompanySetup() {
   var sheet = wb.getSheet(1);
   sheet.getCell(2, 2).value(invoice.companyDetails[0].yourName);
   sheet.getCell(3, 2).value(invoice.companyDetails[0].companyName);
   sheet.getCell(4, 2).value(invoice.companyDetails[0].addressLine1);
   sheet.getCell(5, 2).value(invoice.companyDetails[0].addressLine2);
   sheet.getCell(6, 2).value(invoice.companyDetails[0].addressLine3);
   sheet.getCell(7, 2).value(invoice.companyDetails[0].addressLine4);
   sheet.getCell(8, 2).value(invoice.companyDetails[0].addressLine5);
   sheet.getCell(9, 2).value(invoice.companyDetails[0].phone);
   sheet.getCell(10, 2).value(invoice.companyDetails[0].facsimile);
   sheet.getCell(11, 2).value(invoice.companyDetails[0].website);
   sheet.getCell(12, 2).value(invoice.companyDetails[0].email);
   sheet.getCell(13, 2).value(invoice.companyDetails[0].currencyAbbreviation);
   sheet.getCell(14, 2).value(invoice.companyDetails[0].beneficiary);
   sheet.getCell(15, 2).value(invoice.companyDetails[0].bank);
   sheet.getCell(16, 2).value(invoice.companyDetails[0].bankAddress);
   sheet.getCell(17, 2).value(invoice.companyDetails[0].accountNumber);
   sheet.getCell(18, 2).value(invoice.companyDetails[0].routingNumber);
   sheet.getCell(19, 2).value(invoice.companyDetails[0].payableTo);
}

为了防止用户添加的数量超过工作表最大行数,我们可以在工作表中自动添加更多行。在设置数组中表单中的项目之前,默认添加行:

function fillInvoiceItems() {
   var sheet = wb.getSheet(0);
   var rowsToAdd = 0;
   if (invoice.invoiceItems.length > 15) {
       rowsToAdd = invoice.invoiceItems.length - 15;
       sheet.addRows(22, rowsToAdd);
   }
   var rowIndex = 8;
   if (invoice.invoiceItems.length >= 1) {
       for (var i = 0; i < invoice.invoiceItems.length; i++) {
            sheet.getCell(rowIndex,1).value(invoice.invoiceItems.quantity);
            sheet.getCell(rowIndex,2).value(invoice.invoiceItems.details);
            sheet.getCell(rowIndex,3).value(invoice.invoiceItems.unitPrice);
            rowIndex++;
       }
   }
}

六、将文档内容从 Node.js 导出到 Excel 文件

在工作簿中填写完信息后,我们可以将工作簿导出到Excel文件中。为此,我们将使用excelio打开功能。在这种情况下,只需将日期输入文件名即可:

function exportExcelFile() {
   excelIO.save(wb.toJSON(), (data) => {
       fs.appendFileSync('Invoice' + new Date().valueOf() + '.xlsx', newBuffer(data), function (err) {
            console.log(err);
       });
       console.log("Export success");
   }, (err) => {
       console.log(err);
   }, { useArrayBuffer: true });
}

完成的文件将如下所示:

以上就是第一篇《从服务端生成Excel电子表格(Node.js+SpreadJS)》的全部内容。为了能够解决批量绑定数据源并导出Excel、批量修改大量Excel内容及样式、服务端批量打印以及生成PDF文档等需求,我们提供了更为成熟的官方手段:SpreadJS + GcExcel,该方案提供了比Node.js+SpreadJS更加优秀的性能和稳定性,这就是我们下一篇《从服务端生成Excel电子表格(GcExcel + SpreadJS)》的主要内容,敬请期待。

昨天以前SegmentFault 最新的文章

Vercel 与 Next.js:开源全明星团队背后的商业逻辑

2022年1月26日 02:13
作者 aryu
视频版本推荐同步观看,喜欢请一键三连~

https://www.bilibili.com/vide...

引子

Vercel 是由 Guillermo Rauch 创立的云服务公司,以拥有数个知名开源项目为大众所知,随着 2021 年在融资、招聘、产品等方面的高调亮相,Vercel 的商业逻辑也愈发清晰。

背景故事:后端渲染时代

现代前端框架的代表之一 React 于 2013 年开源,与同期其他前端框架一起推动前后端分离的开发模式直至现在。

而在现代前端框架被广泛采纳之前,绝大部分网站采用的是后端渲染的方式输出前端页面。

以一个典型的新闻网站为例,当新闻编辑在后台(例如一个 CMS)提交一篇文章,文章对应的内容首先会保存在数据库中。让读者访问这篇文章的 URL 时,后端程序会读取数据库中的内容,并传入前端模板中,渲染得到最终的 HTML 结果后返回给浏览器。

不过这样的方式在性能上较为浪费的,因为文章内容发布之后极少需要修改,但数以万计的读者每次打开文章页面都会产生数据库查询与后端渲染的重复开销。一个非常自然的解决方法就是将后端渲染的 HTML 缓存,而这份缓存作为静态内容也更容易推送至 CDN,实现全国甚至全球加速。即使文章内容需要修改,这套方案也可以通过重新生成再更新 CDN 的方式轻松应对。

由于实现简单、效果出众,这样的架构被广泛应用于各类内容相对静态的网站中。

背景故事:Guillermo Rauch

Vercel 的当家开源项目 Next.js 并不是 Guillermo 的第一个开源项目,Guillermo 另外两个被广泛使用的早期开源项目是 Node.js 的 websocket 框架 socket.io 和 MongoDB 客户端 mongoose,由此可见他在创立 Vercel 之前就已经有相当深的 Node.js 后端开发功力。

而他同时也是传奇开源项目 Mootools 的开发者之一。以现如今的角度看 Mootools 的代码没有太多惊奇之处,并且在同时期的竞争中也没能战胜 jQuery。但在这个项目的开发者列表中却聚集了一众知名开发者,其中不少人后来都加入 Facebook 参与 React 等前端项目的开发,包括 React 团队的灵魂人物 Sebastian Markbåge 和测试工具 jest 的创造者 Christoph Nakazawa 等。不难看出 Guillermo 当时对于前端框架也有深入的研究,并且周边有一群未来 React 社区的核心玩家。

Zeit now:极简主义的部署体验

2015 年 Guillermo 创立了名为 Zeit 的公司,并在 2020 年改名为现在更为大家所熟知的 Vercel。2016 年 Zeit 推出了核心产品 now,帮助开发者轻松将应用部署到云端,拉开了 Vercel 商业化进程的帷幕。

与我们在背景故事中提到的后端渲染架构相比,Guillermo 当时找到了两个明显的痛点:

  1. 这套架构虽然设计并不复杂,但在实际实施时却困难重重,往往需要云上部署经验丰富的 devops 帮助落地,而这也被认为是中大型公司的专利,小公司与个人开发者缺少对应的人力。
  2. 公有云服务众多且逻辑复杂,普通开发者想学习并达成最佳实践,所耗费的时间甚至高于开发应用本身。

以此为出发点,now 诞生了。now 本身并没有开发复杂的云上基础设施,而是帮用户将应用部署到 AWS 等公有云上。结合 Guillermo 过往丰富的 web 应用开发经验,他将域名配置、DNS 解析、SSL 证书、CDN 缓存等一系列工作封装到了产品内部,让用户真正做到一键部署应用,并享受最佳实践带来的性能优势。

对极简主义的追求贯穿了 Zeit 的产品定义与视觉设计,黑白相间的优雅界面成为了他们最早的名片。

这一阶段 Zeit 基于 now 的商业逻辑已经清晰:通过简化公有云部署节省用户人力,通过生产环境最佳实践提升应用表现。前者不难理解,用户节省的人力成本可以转换为付费的意愿,后者则是 Zeit/Vercel 持续至今的思路:用户最愿意为生产环境付费以提升稳定性与性能。

而这一模式的难点则是用户如何产生粘性而不是转向成本更低的公有云或者其他定位相同的部署平台。Guillermo 的答案是从自己精通的 Node.js 与前端技术出发,以开源的方式切入市场。

Next.js:前后端分离后的产物

随着现代前端框架的流行,越来越多 web 开发从后端渲染转向前后端分离的开发模式。后端服务不再负责渲染视图,而是以 API 的方式提供数据供不同形态的客户端使用,而原本渲染视图的工作则转移到了前端。

这一方式降低了前后端之间的耦合度、将渲染负载转移到客户端、更容易同时支持桌面端和移动端网站、前端能够开发更复杂的客户端交互,因此迅速地流行开来。

但是架构的改变也带来了新的问题。分离之后,后端返回的 HTML 很可能只包含了极少数视图,而绝大部分的视图都是在前端通过框架动态渲染,数据也是通过 API 异步加载。这一变化带来的最典型问题就是 SEO 失效,绝大多数搜索引擎都是通过后端返回的 HTML 内容进行索引,但在前后端分离之后搜索引擎读取到的信息非常有限,也就影响了网站的 SEO 效果。

这一问题使得大量依赖 SEO 的网站没法使用现代前端框架,框架开发者们也将这一问题作为头等大事开始解决。但是以 SSR 为代表的框架后端渲染方案在设计初期通常都存在不易上手、性能差的问题,时至今日也没能形成标准化的方案。

2016 年,Guillermo 看到了这一问题背后的机会,创造了 Next.js,一个开源的 React 上层框架。在之后的几年中,Next.js 持续发力 SSR、SSG、JAM stack 等方向,但万变不离其宗,解决的都是现代前端框架如何后端渲染的问题。同时凭借着对开源社区的深度理解与持续运营,Next.js 也成为了 React 和 Node.js 领域极受欢迎的项目。

Next.js 在开源社区的成果给 Zeit 带来了新的商业机会。大量开发者开始因为 Next.js 与 Zeit 产生粘性,而使用 Next.JS 之后部署会比普通的静态页面更为复杂,为此 Zeit now 适时地深度集成 Next.js,让使用 Next.js 的开发者与项目成为 Zeit now 平台中的一等公民。

同样地,背靠 Zeit now 可获得的极简部署体验与生产级别保障,也让 Next.js 与同类开源产品相比有了独特的优势,进一步壮大了 Next.js。

Vercel:开源全明星团队之上的产品矩阵

随着 Next.js 与 Zeit now 的相辅相成,2020 年 4 月 Zeit 改名为 Vercel 并完成 2100 万美元的 A 轮融资,正式启航。

自那之后,Vercel 陆续完成了 B、C、D 轮融资,总融资额达到 3.13 亿美元,与接连不断地融资同样引入瞩目的是 Vercel 在招聘上的成果。

目前 Next.js 的 lead developer Tim Neutkens 就是从社区中走入 Vercel 的。最初 Tim 以开源的方式参与 Next.js,并开发了 Next.js 生态中的重要项目 mdx。2017 年,他作为第一个 Next.js 全职研发正式加入 Vercel。

Tim 并不是 Vercel 内部培养的唯一开源明星。国人工程师 Shu Ding 在 Vercel 身兼研发和设计的工作,同时他也主导开发了 React 社区中热门的开源数据请求工具 SWR,在功能上 SWR 也进一步提升了 Next.js 中的开发体验。

从他们二人身上可以看到 Vercel 内部的工程师与开源文化之深,以及运营推广能力之强。

除了内部培养,Vercel 更让大家津津乐道的是他们从知名开源项目中挖掘人才的能力。

2019 年,Vercel 从 React 的官方脚手架项目 create-react-app 中招聘了核心维护中 Joe Haddad,自那之后的几年时间里 Next.js 的脚手架易用性显著提升。这次引援中 Vercel 的招聘策略显得十分清晰,作为面向开发者提供服务的商业公司,从开发者重度依赖的开源项目中挖掘人才可以有效提升产品竞争力,还能增加自己在开源社区中的话语权。

2021 年起的一系列招聘动作更是如此。

Next.js 为了实现后端渲染,重度使用了 JS 生态中的打包构建工具 webpack。为了增强对 webpack 的理解与话语权,Vercel 招聘了 webpack 的创造者 Tobias Koppers。

由于 JS 需要处理复杂的浏览器兼容性问题,绝大部分项目都使用了 babel 作为编译工具解决这一问题。Vercel 没有选择从 babel 社区中招聘,而是将橄榄枝抛给了 SWC 的作者 Donny。SWC 是一个使用 rust 开发的 babel 替代品,并且有高达数十倍的性能优势。Next.js 的最新版本中也全面使用 SWC 替代 babel。

之后,Vercel 直接收购了 monorepo 构建工具 Turborepo 并将其开源,项目创始人 Jared Palmer 也被招入麾下,Jared 同时也是知名开源项目 Formik、TSDX 的作者。Vercel 平台上每天都在进行不计其数的构建与编译,SWC 与 Turborepo 这样的高性能工具如果能够被广泛使用,将为 Vercel 自身节省大量公有云支出。

仅仅使用 Next.js 吸引开发者使用 Vercel 平台还不足够,Vercel 将目光投向了其他前端框架。在支持了市面上绝大多数框架的构建与部署之后,Vercel 宣布招聘 Svelte 框架的作者 Rich Harris,想必之后 Svelte 也会像 Next.js 一样在 Vercel 平台上获得最精致的集成体验。

在大家被 Vercel 接连不断的重磅招聘吸引之时,2021 年底他们又宣布了全年最后一个大新闻,React 核心团队的灵魂人物 Sebastian Markbåge 也将加入 Vercel。作为 构建在 React 之上的框架,Next.js 的未来与 React 的演进息息相关。尤其是在 React 团队提出 server component 的概念之后,Next.js 的后端渲染与官方方案是否能够有机结合,都离不开 Sebastian 的技术视野以及在 React 项目中的话语权。

随着开源全明星团队的建成,Vercel 从构建工具、编译器到开发框架的产品矩阵也更加完整。与通常认知里开源会带来商业风险不同的是,Vercel通过在一众核心开源项目中招聘核心成员、获得话语权,反而成为了开发者无法绕开的开源依赖,开发者们对开源项目的粘性帮助 Vercel 进一步构建了商业上的护城河。

预测:Vercel 的未来与挑战

梳理清楚 Vercel 的团队组建与产品演进思路之后,我们不妨大胆预测它未来的行动以及需要面对的挑战。

首先延续招聘 Svelte 作者的方式,每与一个前端框架建立深度合作,就能够获取一批新的潜在用户。未来 Vercel 极有可能从 VueJS core team 中进行招聘,也有可能从 Vue.js 生态中的 Next.js 替代品 Nuxt 着手,甚至整体收购 Nuxt 团队。

其次深度集成 webpack 是否是正确的选择,也是 Next.js 需要面对的问题。在切换至 swc 提升性能之时,Vercel 团队曾表示会继续使用 webpack,不考虑 vite 等新的构建方案。但从社区问答中我们还是看到了大量 webpack 相关的问题,也有开发者抱怨 webpack 的复杂配置让 Next.js 的使用体验不及预期。

最后则是与其余厂商之间的竞争。同类厂商如 Netlify 不必多说,一定会在产品与技术上持续对 Vercel 产生压力,而 Vercel 背后所使用的 AWS、Cloudflare 等巨头也将带来巨大的挑战。AWS 拥有功能强大的 Amplify,Cloudflare 的 Pages 与 Workers 服务也在获得更多的客户,而他们在价格与集成度上的优势也许是 Vercel 无法逾越的屏障。

不过无论如何,Guillermo 与 Vercel 已经足够神奇,也在开源与商业化的结合方式上给大家带来了新的思路。

APICloud AVM框架列表组件list-view的使用、flex布局教程

2022年1月25日 12:52
作者 海的尽头

avm.js 是APICloud 推出的多端开发框架。使用 avm.js 一个技术栈可同时开发 Android & iOS 原生 App、小程序和 iOS 轻 App,且多端渲染效果统一;全新的 App 引擎 3.0 不依赖 webView,提供百分百的原生渲染,保障 App 性能和体验与原生 App 一致。

list-view 定义可复用内容的竖向滚动视图,可以优化内存占用和渲染性能,支持下拉刷新和上拉加载。可使用 scroll-view 的基本属性。
list-view 里面可放置 cell、list-header、list-footer、refresh 等组件,使用 cell 组件作为每项显示内容。

下面看一个list-view的示例:

<template>
    <list-view id="listView" class="main" enable-back-to-top onscrolltolower={this.onscrolltolower}>
        <cell class="cell c-lc-rl_ac c-sy-border_bottom c-lc-ptb" onclick={this.itemClick}>
            <img class="img" src={item.url} alt="">
            <text class="title c-filling-w c-pl">{item.title}</text>
        </cell>
        <list-footer class="footer">
            <text>加载中...</text>
        </list-footer>
    </list-view>
</template>
<style src='../../css/c-layout-mini.css' scoped>
</style>
<style>
.main {
    width: 100%;
    height: 100%;
}
 
.cell {
    width: 100%;
    height: 70px;
}
 
.img {
    height: 50px;
    width: 50px;
    margin-left: 10px;
}
.title {
    height: 50px;
    font-size: 20px;
    line-height: 50px;
}
 
.footer {
    justify-content: center;
    align-items: center;
}
</style>
 
<script>
export default {
    name: 'test',
    methods: {
        apiready() {
            this.initData(false);
        },
        initData(loadMore) {
            var that = this;
            var skip = that.dataList ? that.dataList.length : 0;
            var dataList = [];
            for (var i = 0; i < 20; i++) {
                dataList[i] = {
                    title: '项目' + (i + skip),
                    url: '../../image/nav_tab_2.png'
                }
            }
            var listView = document.getElementById('listView');
            if (loadMore) {
                that.dataList = that.dataList.concat(dataList);
                listView.insert({
                    data: dataList
                });
            } else {
                that.dataList = dataList;
                listView.load({
                    data: dataList
                });
            }
        },
        onscrolltolower() {
            this.initData(true);
        },
        itemClick(e) {
            api.alert({
                msg: '当前项索引:' + e.currentTarget.index
            });
        }
    }
}
</script>

效果如下图:
image.png

list-view 只支持APP,需要用自定义loader或APPloader 调试。调试教程可查看文档APICloud Studio3 WiFi真机同步和WiFi真机预览使用说明

list-view 自带内存回收功能,可以滚动加载更多。

给list-view 添加下拉刷新组件refresh

根据refresh 组件文档,把 refresh 标签添加到 list-view 标签中,如下:

<template>
    <list-view id="listView" class="main" enable-back-to-top onscrolltolower={this.onscrolltolower}>
        <refresh class="refresh" state={refreshState} onstatechange={this.onstatechange}>
            <image class={refreshIconClass} src="../../image/refresh.png"></image>
            <image class={refreshLoadingClass} src="../../image/loading_more.gif"></image>
            <text class="refreshStateDesc">{refreshStateDesc}</text>
        </refresh>
        
        <cell class="cell c-lc-rl_ac c-sy-border_bottom c-lc-ptb" onclick={this.itemClick}>
            <img class="img" src={item.url} alt="">
            <text class="title c-filling-w c-pl">{item.title}</text>
        </cell>
        <list-footer class="footer">
            <text>加载中...</text>
        </list-footer>
    </list-view>
</template>

把refresh 组件的css ,js 代码也复制到相应的style 和 script 标签中,并在项目目录image 标签下添加用到的两张下拉刷新图片。完整代码如下:

<template>
    <list-view id="listView" class="main" enable-back-to-top onscrolltolower={this.onscrolltolower}>
        <refresh class="refresh" state={refreshState} onstatechange={this.onstatechange}>
            <image class={refreshIconClass} src="../../image/refresh.png"></image>
            <image class={refreshLoadingClass} src="../../image/loading_more.gif"></image>
            <text class="refreshStateDesc">{refreshStateDesc}</text>
        </refresh>
        
        <cell class="cell c-lc-rl_ac c-sy-border_bottom c-lc-ptb" onclick={this.itemClick}>
            <img class="img" src={item.url} alt="">
            <text class="title c-filling-w c-pl">{item.title}</text>
        </cell>
        <list-footer class="footer">
            <text>加载中...</text>
        </list-footer>
    </list-view>
</template>
<style  src='../../css/c-layout-mini.css' scoped>
</style>
<style>
.main {
    width: 100%;
    height: 100%;
}
 
.cell{
    width: 100%;
    height: 70px;
}
 
.img{
    height: 50px;
    width: 50px;
    margin-left: 10px;    
}
.title {
    height: 50px;
    font-size: 20px;
    line-height: 50px;
}
 
  .footer {
        justify-content: center;
        align-items: center;
    }
 
    .refresh {
        align-items: center;
        justify-content: center;
        background-color: #eee;
    }
    .refreshStateDesc {
        color: #e3007f;
        font-size: 13px;
    }
    .refreshIcon {
        position: absolute;
        width: 25px;
        height: 22px;
        bottom: 21px;
        left: 70px;
        transition-property: transform;
        transition-duration: 100ms;
    }
    .refreshIcon-normal {
        transform: rotate(0);
        visibility: visible;
    }
    .refreshIcon-dragging {
        transform: rotate(180deg);
        visibility: visible;
    }
    .refreshIcon-refreshing {
        visibility: hidden;
    }
    .refreshLoading {
        position: absolute;
        width: 22px;
        height: 22px;
        bottom: 21px;
        left: 70px;
        visibility: hidden;
    }
    .refreshLoading-refreshing {
        visibility: visible;
    }
 
</style>
 
<script>
    export default {
        name: 'test',
         data(){
            return {
                refreshState: 'normal'
            }
        },
        computed: {
            refreshIconClass(){
                if (this.data.refreshState == 'normal') {
                    return 'refreshIcon refreshIcon-normal';
                } else if (this.data.refreshState == 'dragging') {
                    return 'refreshIcon refreshIcon-dragging';
                } else if (this.data.refreshState == 'refreshing') {
                    return 'refreshIcon refreshIcon-refreshing';
                }
            },
            refreshLoadingClass() {
                if (this.data.refreshState == 'refreshing') {
                    return 'refreshLoading refreshLoading-refreshing';
                } else {
                    return 'refreshLoading';
                }
            },
            refreshStateDesc() {
                if (this.data.refreshState == 'normal') {
                    return '下拉可以刷新';
                } else if (this.data.refreshState == 'dragging') {
                    return '松开可以刷新';
                } else if (this.data.refreshState == 'refreshing') {
                    return '刷新中...';
                }
            }
        },
        methods:{
            apiready() {
                this.initData(false);
            },
            initData(loadMore) {
                var that = this;
                var skip = that.dataList?that.dataList.length:0;
                var dataList = [];
                for (var i=0;i<20;i++) {
                    dataList[i] = {
                        title: '项目' + (i + skip),
                        url: '../../image/nav_tab_2.png'
                    }
                }
                var listView = document.getElementById('listView');
                if (loadMore) {
                    that.dataList = that.dataList.concat(dataList);
                    listView.insert({
                        data: dataList
                    });
                } else {
                    that.dataList = dataList;
                    listView.load({
                        data: dataList
                    });
                }
            },
            onscrolltolower() {
                this.initData(true);
            },
            itemClick(e) {
                api.alert({
                    msg: '当前项索引:' + e.currentTarget.index
                });
            },
            onstatechange(e) {
                var state = e.detail.state;
                if (state == 'normal') {
                    this.data.refreshState = 'normal';
                } else if (state == 'dragging') {
                    this.data.refreshState = 'dragging';
                } else if (state == 'refreshing') {
                    this.data.refreshState = 'refreshing';
                    var that = this;
                    setTimeout(function(){
                        that.data.refreshState = 'normal';
                    }, 2000);
                }
            }
        }
    }
</script>

wi-fi 同步到手机 loader,下拉页面,运行效果如下:

image.png

Flex 布局介绍:
Flex 布局意思是弹性盒子布局,比较适合移动端场景,适配不同屏幕大小。

<div>
   <div>item</div>
   <div>item</div>
   <div>item</div>
</div>

通常可以把父元素定义为弹性盒子或称为容器,其子元素为弹性盒子的项目。flex布局的主要功能是在主轴或交叉轴按预期排列分布项目,定义每个项目占用空间比例,并可跟随容器大小伸缩。

image.png

上图引自下面这篇博客,推荐阅读:
http://www.ruanyifeng.com/blo...

推荐一个flex git:
https://gitee.com/jiang_xinch...

应云而生,一文看懂端到端的可观测体系构建

2021年初,可观测性的概念在国内市场还鲜少有人提到,但到了2021年下半年,有关可观测性的研讨和实践却开始如雨后春笋般层出不穷,知名公司 Grafana 甚至直接将原来的监控工具改成了可观测性技术栈并推了一系列服务。可观测性真的能够解决传统监控体系面临的诸多问题吗?又该如何构建可观测体系?本期,亚马逊云科技 Tech Talk 特别邀请到观测云 CEO 蒋烁淼带来分享《构建端到端的可观测体系最佳实践》。

可观测性为何突然“火出圈”

可观测性看似是个新鲜词,但其实它的起源远比我们的认知要早得多。可观测性最早是匈牙利裔工程师鲁道夫·卡尔曼针对线性动态系统提出的概念。若以信号流图来看,若所有的内部状态都可以输出到输出信号,此系统即有可观测性。1948年伯特·维纳发表的著作《控制论-关于动物和机器中控制和通讯的科学》同样提到了可观测性。控制理论中的可观测性是指系统可以由其外部输出推断其内部状态的程度。

随着云计算的发展,可观测性的概念逐渐走入计算机软件领域。为什么近期可观测性的热度显著提升了呢?

蒋烁淼认为,这很大程度是由于系统复杂性的增强。IT 系统的本质是一个数字化的系统,过去,系统本身结构简单,多为单体式架构,且基础设施相对固定,可以通过监控去查看系统。但随着云原生时代的到了,管理对象从单一主机逐渐变成云,后来又变成云原生的分布式复杂系统,传统的面向基础设施的监控、简单的日志和简单的 APM 没有办法解决问题,因此,需要构建系统完整的可观测性。

可观测性中使用的主要数据类是指标、日志、链路。它们通常被称为“可观测性的三大支柱”。

  • 指标(Metric):指标是连续时间下的系统的值的记录,基础指标通常用于描述两种数据类型,一种是计数(Count),一种是计量(Gauge)。
  • 日志(Log):系统/应用输出的时间相关的记录,通常由系统/软件开发人员输出,方便定位系统的错误和状态。
  • 链路(Tracing):基于有向无环图构建的软件各个模块直接地调用关系。

三大支柱至关重要,开发者正是通过这三个维度的数据来判定应用系统的状况。和传统监控相比,可观测体系拥有诸多优势。

传统监控面向已知的问题,只能去发现和通知那些已知可能会发生的故障,如:CPU>90%。主要监控对象是 IT 对象,仅面向服务端的组件,解决基础的运维问题。

而可观测性则能够协助发现并定位未知的问题。其核心是不断收集系统产生的各种核心指标与数据,通过数据分析的方式来保障和优化业务,如:发现小程序客户端在某个城市的支付失败率非常高,从而判断是否是代码层面上导致这样一个异常。可观测性主要监测的对象不仅仅是IT对象,还有应用和业务,面向云、分布式系统、APP/小程序。

在分享中蒋烁淼谈到,随着基础设施的发展,传统监控将逐步被可观测性所取代。

他将构建可观测性的价值总结为以下五点:

  • 让 SLO 可视化,清晰的目标和现状
  • 发现与定位未知问题
  • 减少团队间的澄清成本
  • 降低业务异常造成的无法预知的经济损失
  • 提升最终用户体验和满意度

开源 or SaaS,可观测性构建正确的打开方式是?

相比于传统监控体系,构建可观测性既然有诸多优势和价值。那么该如何构建可观测性呢?

首先,需要尽可能地收集所有组件系统的所有相关⾯的基础数据,包括云、主机、容器、Kubernetes 集群,应⽤和各种终端。实时收集这些数据的成本并不⾼,但如果没有收集,⼀旦系统故障需要排查分析的时候,就⽆法有效评估当时的状态。

其次,要明确系统可观测性构建的责任。谁是这个组件的构建者,谁负责定义这个组件的 SLI,谁负责收集所有的相关基础数据并构建相应的仪表盘以及谁为相关的组件的 SLO 负责,需要责任到人。

第三,开发者需要为可观测性负责。开发者要将⾃⼰开发系统的可观测性数据暴露作为软件质量⼯程的⼀部分,如果说单元测试是为了保证最⼩单元代码的可⽤性,那么开发者标准化暴露可观测性基础数据也将作为⽣产系统可靠性的必要条件。

第四,需要建⽴统⼀的指标、⽇志、链路规范,统⼀团队的⼯具链。即采取相同的指标命名规范,相同的⽇志格式,相同的链路系统。如果在遵循 OpenTelemetry 标准后,仍有不同,则可定义串联整个系统的统⼀TAG 规范,如:所有错误都是 state:error。

第五,要持续优化改进整体可观测性。针对整个系统的可观测,包括数据收集,视图构建,TAG 体系建⽴,这些步骤均需要时间,不能因为覆盖度或者构建的仪表盘未能在某次事故中发挥作⽤而继续⽤过去的⽅式处理问题。每次未被观测的故障都是进⼀步提升可观测范围的绝佳机会。

从可观测性构建的路径不难看出,其过程是非常复杂的。那么,主流的构建方式有哪些?蒋烁淼介绍了两种最为常见的可观测性构建方式,分别是通过开源的方式构建和采用 SaaS 产品进行构建。

得益于开源生态的蓬勃发展,为可观测性的构建提供了诸多选择。采用开源的方式构建,需要构建者从前端的数据抓取到后端的数据处理,包括数据展示、告警等周边功能的相关知识有非常详尽的了解掌握。因此,这种方式适合于那些有足够实力或者学习成本及时间成本相对充足的团队。

相比于开源的方式,采用成熟的 SaaS 产品构建可观测性是一种更加高效的方式。蒋烁淼以观测云的产品为例,介绍了这种方式的四点优势。

  • 不做缝合怪:在服务器内仅安装一个 agent 就可以收集这台主机所有相关的系统数据,避免成堆的 agent 和配置项。
  • 不做小白鼠:能提供端到端的完整覆盖,并能做到开箱即用,避免良莠不齐的集成,如:观测云就能够支持超过200种技术栈,实现端到端的覆盖。
  • 不封闭、高度可编程:可实现轻松构建任意的可观测场景,甚至将业务数据参数引入到整体的观测中,灵活性强。此外,还能够避免死板的集成,拥有强大的二次开发能力。
  • 不留隐患:观察云对用户侧代码永久开源,单向通讯,不会也不能向客户环境下发指令。所有的数据收集默认脱敏且用户可对整个过程进行控制。

前面提到,可观测性的构建是应“云”而生的,不仅如此,观测云本身也是完完全全的云原生产品。观测云中整套产品包括数据平台,都是部署在亚马逊云科技的 EKS 之上的,并基于容器进行编排。观测云的整体架构非常简单,即通过一个 agent 将海量数据进行统一,进入数据平台,然后通过平台的能力提供完整的可观测性。整个系统分为核心平台层、Web 层和数据接入层,核心平台层是完全由观测云进行自研的,没有进行开源。上层的 Web 层,在核心数据处理平台上有一套与平台对接的 API。蒋烁淼说:“对于客户来说,更推荐直接选择观测云的 SaaS 产品,如果愿意,客户也可以在亚马逊上完全孤立地进行部署,也是非常方便的,只不过整体费用要比直接采用 SaaS 产品高得多。

为什么会选择亚马逊云科技?主要是基于以下考量:

  • 观测系统本身要有高一个数量级的可靠性和更高的 SLA:观测云是帮助客户构建可观测性系统的平台,因此需要自身拥有很高的可靠性,如果不能提供足够高的可靠性,一旦观测系统出现故障,便无法及时提醒客户,提供详细的分析更无从谈起。此外,选择云服务本身也能够让一部分观测云平台的 SLA 由亚马逊来提供。
  • 更成熟的 Marketplace:用户可通过中国的团队直接在亚马逊上进行产品购买,亚马逊云科技会把产品消费直接在 Marketplace 上记账。需要说明的是,观测云的产品是根据数据规模来付费的,当用户没有数据量的时候几乎是免费的。
  • 全球性:亚马逊云科技能够提供比海外产品更好的兼容性,尤其对于中国的技术栈整体成本更低。蒋烁淼在分享中透露:“在春节过后,观测云将会在海外亚马逊云科技节点部署我们的观测平台。观测云希望用中国力量为中国的出海客户提供比海外产品更好的、成本更低的选择。”
  • 借力 APN 融入亚马逊云科技全球网络:观测云希望借助亚马逊云科技强大的生态,将可观测性作为最终对客户提供服务的手段,并希望能够借力APN,帮助更多用户了解可观测性的效果,这个也是观测云选择亚马逊科技非常重要的原因之一。

除了是完完整整的云原生产品,在观测云的系统中,还包含几个非常有趣的设计。首先,在采集侧:

  • 观测云把第三方指标,日志,链路采集协议统一转为观测云协议
  • 插件式采集栈设计, 各插件之间采用 go 协程隔离,互不影响
  • 主动式资源消耗控制防止 agent 端资源压力过大(cgroup 控制采集资源占用)
  • 被动式资源消耗控制防止 server 端压力过大(背压机制)
  • 潮汐机制的分布式日志解析 (pipeline)

其次,在存储查询侧,观测云统一了查询语法,用户无需关心底层数据存储,简单易上手。

第三,在分析侧,观测云实现了全部数据串联,并构建了统一的查看器,将原始数据以类似多维分析和列表的方式进行分析,用户可以去构建自己的查看器。此外,由于数据量大,为避免前端造成用户浏览器压力过大,观测云可以按照指定百分比来采集数据,并提供 SLO/SLI 的面板,帮助客户构建自己应用系统整体可靠性的度量方式。

构建端到端的可观测体系实践案例

在对概念层和技术层面进行详细的介绍后,蒋烁淼以某电商客户作为案例,就具体该如何构建端到端的可观测体系进行了讲解。

案例中电商客户面临的问题是:交易流程从客户下单到仓库到最后财务记账,一个订单需要将近10次接口调用,其中任何环节都有可能出现问题,例如程序问题,网络异常,库存卡住等。目前没有有效的监控工具能够把对订单流程进行监控,出问题一般都是门店员工反馈过来,然后运维人员根据订单去参照流程去查询问题出在哪里,非常被动,且工作量较大,每天需要运维人员去查询业务接口是否走完。

针对该客户构建端到端的可观测体系的过程大致分为四步:第一步,梳理观测对象集成接入。采用观测云的产品,整个接入过程仅需要30分钟左右就可以完成。

第二步,统一查看与分析。具体步骤为,首先,对用户体验进行监测,然后查看该行为下的和后端打通的链路,并点击具体的链路进入链路查看器,最后查看相应链路的日志。

第三,通过查看器实现业务的可观测。

第四,通过 SLO 监控器预警。

通过观测云完成端到端的可观测性构建后,该电商客户将订单流程节点状态可视化,可实现以订单号检索订单流程节点状态,流程卡在哪个环节,报错信息是怎样一目了然。从用户操作界面、网络、后端服务到依赖的中间件、操作系统,任意故障都能够提供清晰的溯源与分析。不仅如此,观测云还提供实时异常监控告警,确保问题能够被及时发现并及时处理。

除电商领域的应用外,观测云的 SaaS 产品还适用于非常多应用场景。在观测云的官网有完整的系统可观测性构建的最佳实践,感兴趣的小伙伴可直接去观测云官网查看相应文档。

戳阅读原文直达官网
https://www.guance.com/#home

巧用 CSS 实现动态线条 Loading 动画

2022年1月19日 02:17
作者 chokcoco

有群友问我,使用 CSS 如何实现如下 Loading 效果:

这是一个非常有意思的问题。

我们知道,使用 CSS,我们可以非常轻松的实现这样一个动画效果:

<div></div>
div {
    width: 100px;
    height: 100px;
    border-radius: 50%;
    border: 2px solid transparent;
    border-top: 2px solid #000;
    border-left: 2px solid #000;
    animation: rotate 3s infinite linear;
}
@keyframes rotate {
    100% {
        transform: rotate(360deg);
    }
}

动画如下:

与要求的线条 loading 动画相比,上述动画缺少了比较核心的一点在于:

  • 线条在旋转运动的过程中,长短是会发生变化的

所以,这里的的难点也就转变为了,如何动态的实现弧形线段的长短变化?解决了这个问题,也就基本上解决了上述的线条变换 Loading 动画。

本文将介绍 CSS 当中,几种有意思的,可能可以动态改变弧形线条长短的方式:

方法一:使用遮罩实现

第一种方法,也是比较容易想到的方式,使用遮罩的方式实现。

我们实现两个半圆线条,一个是实际能看到的颜色,另外一个则是和背景色相同的,相对更为粗一点的半圆线条,当两条线条运动的速率不一致时,我们从视觉上,也就能看到动态变化的弧形线条。

看看示意图,一看就懂:

我们把上述红色线条,替换成背景白色,整体的动画效果就非常的相似了,伪代码如下:

<div></div>

div {
    width: 200px;
    height: 200px;
}
div::before {
    position: absolute;
    content: "";
    top: 0px; left: 0px; right: 0px; bottom: 0px;
    border-radius: 50%;
    border: 3px solid transparent;
    border-top: 3px solid #000;
    border-left: 3px solid #000;
    animation: rotate 3s infinite ease-out;
}
div::after {
    position: absolute;
    content: "";
    top: -2px; left: -2px; right: -2px; bottom: -2px;
    border-radius: 50%;
    border: 7px solid transparent;
    border-bottom: 7px solid #fff;
    border-right: 7px solid #fff;
    animation: rotate 4s infinite ease-in-out;
}
@keyframes rotate {
    100% {
        transform: rotate(0deg);
    }
}

核心就是实现两条半圆线条,一条黑色,一条背景色,两段线条以不同的速率运动(通过动画时间及缓动控制),效果如下:

完整的代码你可以猛击 -- CodePen Demo - Linear Loading

上述方案最大的 2 个问题在于:

  1. 如果背景色不是纯色,会露馅
  2. 如果要求能展现的线段长度大于半个圆,无法完成

基于此,我们只能另辟蹊径。

方法二:借助 SVG 的 stroke-* 能力

在之前非常多的篇文章中,都有讲到过在 CSS 配合 SVG,我们可以实现各种简单或复杂的线条动画,像是简单的:

或者自定义复杂路径的复杂的线条动画:

> 对 CSS/SVG 实现线条动画感兴趣的,但是还不太了解的,可以看看我的这篇文章 -- 【Web动画】SVG 线条动画入门

在这里,我们只需要一个简单的 SVG 标签 <circle>,配合其 CSS 样式 stroke-dasharraystroke-dashoffset 即可轻松完成上述效果:

<svg class="circular" viewbox="25 25 50 50">
  <circle class="path" cx="50" cy="50" r="20" fill="none" />
</svg>
.circular {
  width: 100px;
  height: 100px;
  animation: rotate 2s linear infinite;
}
.path {
  stroke-dasharray: 1, 200;
  stroke-dashoffset: 0;
  stroke: #000;
  animation: dash 1.5s ease-in-out infinite
}
@keyframes rotate {
  100% {
    transform: rotate(360deg);
  }
}
@keyframes dash {
  0% {
    stroke-dasharray: 1, 200;
    stroke-dashoffset: 0;
  }
  50% {
    stroke-dasharray: 89, 200;
    stroke-dashoffset: -35px;
  }
  100% {
    stroke-dasharray: 89, 200;
    stroke-dashoffset: -124px;
  }
}

简单解释下:

  • stroke:类比 css 中的 border-color,给 svg 图形设定边框颜色;
  • stroke-dasharray:值是一组数组,没数量上限,每个数字交替表示划线与间隔的宽度;
  • stroke-dashoffset:dash 模式到路径开始的距离。

我们利用 stroke-dasharray 将原本完整的线条切割成多段,假设是 stroke-dasharray: 10, 10 表示这样一个图形:

第一个 10 表示线段的长度,第二个 10 表示两条可见的线段中间的空隙。

而实际代码中的 stroke-dasharray: 1, 200,表示在两条 1px 的线段中间,间隔 200px,由于直径 40px 的圆的周长为 40 * π ≈ 125.6px,小于 200,所以实际如图下,只有一个点:

同理,stroke-dasharray: 89, 200 表示:

通过 animation,让线段在这两种状态之间补间变换。而 stroke-dashoffset 的作用则是将线段向前推移,配合父容器的 transform: rotate() 旋转动画,使得视觉效果,线段是在一直在向一个方向旋转。结果如下:

完整的代码你可以戳这里:CodePen Demo -- Linear loading

OK,还会有同学说了,我不想引入 SVG 标签,我只想使用纯 CSS 方案。这里,还有一种利用 CSS @property 的纯 CSS 方案。

方法三:使用 CSS @property 让 conic-gradient 动起来

这里我们需要借助 CSS @property 的能力,使得本来无法实现动画效果的角向渐变,动起来。

这个方法,我在介绍 CSS @property 的文章中也有提及 -- CSS @property,让不可能变可能

正常来说,渐变是无法进行动画效果的,如下所示:

<div></div>
.normal {
    width: 200px;
    height: 200px;
    border-radius: 50%;
    background: conic-gradient(yellowgreen, yellowgreen 25%, transparent 25%, transparent 100%); 
    transition: background 300ms;
    
    &:hover {
        background: conic-gradient(yellowgreen, yellowgreen 60%, transparent 60.1%, transparent 100%); 
    }
}

将会得到这样一种效果,由于 conic-gradient 是不支持过渡动画的,得到的是一帧向另外一帧的直接变化:

好,使用 CSS @property 自定义变量改造一下:

@property --per {
  syntax: '<percentage>';
  inherits: false;
  initial-value: 25%;
}

div {
    background: conic-gradient(yellowgreen, yellowgreen var(--per), transparent var(--per), transparent 100%); 
    transition: --per 300ms linear;
    
    &:hover {
        --per: 60%;
    }
}

看看改造后的效果:

在这里,我们可以让渐变动态的动起来,赋予了动画的能力。

我们只需要再引入 mask,将中间部分裁切掉,即可实现上述线条 Loading 动画,伪代码如下:

<div></div>

@property --per {
    syntax: "<percentage>";
    inherits: false;
    initial-value: 10%;
}

div {
    position: relative;
    width: 100px;
    height: 100px;
    border-radius: 50%;
    animation: rotate 11s infinite ease-in-out;

    &::before {
        content: "";
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        border-radius: 50%;
        background: conic-gradient(transparent, transparent var(--per), #fa7 var(--per), #fa7);
        mask: radial-gradient(transparent, transparent 47.5px, #000 48px, #000);
        animation: change 3s infinite cubic-bezier(0.57, 0.29, 0.49, 0.76);
    }
}

@keyframes change {
    50% {
        transform: rotate(270deg);
        --per: 98%;
    }
    100% {
        transform: rotate(720deg);
    }
}

@keyframes rotate {
    100% {
        transform: rotate(360deg);
        filter: hue-rotate(360deg);
    }
}

这里,我顺便加上了 filter: hue-rotate(),让线条在旋转的同时,颜色也跟着变化,最终效果如下,这是一个纯 CSS 解决方案:

完整的代码你可以猛击这里:Linear Loading Animation

本方案的唯一问题在于,当前 CSS @property 的兼容性稍微不是那么乐观。当然,未来可期。

最后

简单总结一下,本文介绍了 3 种实现动态弧形线条长短变化的 Loading 动画,当然,它们各有优劣,实际使用的时候根据实际情况具体取舍。有的时候,切图也许也是更省时间的一种方式:)

好了,本文到此结束,希望本文对你有所帮助 :)

想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄

更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

微软公布 VS Code Java 2022 年路线图!

2022年1月24日 03:56
作者 MissD

近日,微软公布了 Visual Studio Code Java 在 2022 年的开发路线图。

在 1 月 20 日发布的新年第一篇博客文章中,微软对 2021 年的亮点做了回顾,并正式发布了 2022 年路线图,其中就包括一些重点关注的方面和令人兴奋的功能更新。

基础发展经验改进

微软方面表示,由于基本的内环体验会影响开发人员的日常生产力,因此该领域将继续成为微软首要的关注点。其中包括改进代码完成建议的智能性,提供更相关的代码段生成,并根据用户偏好提供各种快捷方式(如“syso”和“sout”)。

此外,微软还将进一步改进调试经验。其中包括:允许对反编译类进行调试、在变量视图中更快地求值以及lambda表达式求值;探讨启用由新的 ProjectLoom 支持的虚拟线程的可能性,以获得更好的调试性能。另外,显示测试覆盖率是微软希望支持的另一个特性,以进一步增强其 Java 测试体验。最后,将始终尝试支持最新的Java 技术,因此 Java 18 支持是该公司的路线图。

性能和可靠性

性能和可靠性是微软在 2022 年关注的另一个领域。在该领域,微软希望提高Java 语言服务器的可靠性,减少 Java 语言服务器无响应的情况。据社区信息显示,如果有很多依赖项需要下载,那么第一次导入项目可能需要很多时间,微软对此还将进行下一步计划研究。

构建工具

构建和依赖关系管理一直是 Java 开发的关键部分,特别是对于大型和多模块项目。对此,微软方面也将继续为 Gradle for Java 扩展添加新特性,改进现有的 Maven 扩展。

Spring Boot 端到端支持

作为最流行的 Java 框架之一,Spring Boot 框架允许开发人员轻松构建微服务或web应用程序。当前的 Spring 启动扩展包包含许多优化 Visual Studio 代码上 Spring 开发体验的功能,在了解到 Spring 开发人员社区的要求后,微软认为可以做得更多 —— 后续将与 VMWare 合作对核心 Java 扩展和 Spring 扩展进行改进,以下是改进领域的总体列表:

  • 更轻松地创建 Spring 项目、控制器和 bean 的工作流
  • 更好地可视化核心 Spring 概念(如 bean 和 API 映射)
  • Spring 控制器和类的样板代码生成
  • 在 Spring Boot dashboard 中改进 Spring 应用程序生命周期管理
  • 管理依赖项时添加 Spring 库的更直观体验

有了这些 Spring 改进,希望 Visual Studio 代码上的 Spring 开发体验将比此前更加轻松。

用户体验

作为微软投资的下一个领域,用户体验(UX)将会成为重点关注的方面,如开发者对于更易用功能的需求等。在意识到 Java 开发人员可能来自不同的 Java IDE 背景后,微软将更容易地从其他 IDE 迁移设置和配置。

此外,由于许多学生开发人员在Visual Studio 代码中使用 Java,因此微软计划将对此进行一些改进,如为没有构建工具的项目提供更好的 JUnit 测试端到端体验,为 JavaFX/Swing 项目优化项目创建工作流,Java AWT 包的导入体验更加流畅,同时也希望在 Java 扩展中支持 Live Share。

云原生开发

云计算是软件开发行业最近最热门的话题之一。使用云本地开发方法,开发人员需要处理微服务、云平台、Kubernetes 等。为了满足开发人员在这一领域的需求,微软计划探索与 Kubernetes 的深度集成,以及与不同云服务(如 Azure Spring 云)的交互。借助 Visual Studio 代码的远程开发扩展和 GitHub 代码空间,以此让云本机开发成为 Java 开发人员的绝佳体验。

功能更新

Java 扩展中的嵌入式 JRE

有了 Visual Studio 代码中特定于平台的扩展的支持,微软现也已将 JRE 嵌入到 Java 扩展中。同时,微软还对入门体验和配置 Java 运行时页面(Ctrl+Shift+P:“配置 Java 运行时”)进行了更新。新用户只需安装 Java 扩展包下载项目的 Java 开发工具包(支持 Java 1.5 或更高版本)创建 Java文件,然后就可以愉快地编码。

配置 Java 格式化程序设置

为了解决程序员对于 Java 代码配置格式化程序设置的问题,微软引入了一个视图,开发人员可以在其中更改格式化程序设置并预览效果,以下两种方式可访问此功能:

选项一:只需打开命令面板(Ctrl+Shift+P),然后运行“Java:Open Formatter Settings with Preview”命令

选项二:使用“Java:Help Center”(Ctrl+Shift+P,然后运行“Java:Help Center”命令)。这将打开 Java 帮助中心页面,其中包含非常有用的功能和快捷方式列表。

在此页面上,只需单击“配置格式化程序设置”。

帮助中心

执行上述任一选项都将引导用户进入“格式化程序设置”视图,在该视图中,用户可以在 Visual Studio 代码中轻松更改和预览格式化程序设置。

反馈和建议

2022 年,Visual Studio 代码也将针对 Java 开发做出不断更新,并根据用户的反馈和建议来塑造未来的产品。

原文链接:https://devblogs.microsoft.co...

如何学习RxJava3?有这个项目就够了!

2022年1月18日 16:40
作者 xuexiangjys

前言

最近跳槽到了一家新的公司, 居然发现这家公司的项目大量使用了RxJava3相关的技术, 这让我这个Rx系列轻度使用者有些无所适从.

俗话说, 千学不如一看, 千看不如一练. 为了能够快速地熟悉和掌握RxJava3相关的内容, 我参照了官方文档并结合了之前使用的经验, 写了一个小的学习项目RxJava3Sample, 里面包含了文档简介、例子、日志展示和源码等内容。

就这样, 我看着文档一遍遍地敲着代码学习, 掌握的速度非常的迅速. 有对RxJava3感兴趣的不妨把项目下下来, 本地跑一跑试一试, 效果绝对出乎你想象!

好了话不多说, 下面我来简单介绍一下这个项目.

项目地址

https://github.com/xuexiangjys/RxJava3Sample

演示效果

项目演示

发射器类型

类型描述
Observable<T>能够发射0或n个数据,并以成功或错误事件终止。
Flowable<T>能够发射0或n个数据,并以成功或错误事件终止。支持Backpressure,可以控制数据源发射的速度。可由Observable转化而来。
Single<T>只发射单个数据或错误事件。只处理 onNext 和 onError 事件,没有onComplete。
Completable它从来不发射数据,只处理 onComplete 和 onError 事件。可以看成是Rx的Runnable。
Maybe<T>能够发射0或者1个数据,要么成功,要么失败。类似Single和Completable的结合。如果处理了onNext 和 onError,那么就不处理onComplete。

Subject

Subject 既是 Observable 又是 Observer(Subscriber)
类型描述
PublishSubject只能接收到订阅之后的所有数据
BehaviorSubject接收到订阅前的最后一条数据和订阅后的所有数据
AsyncSubject只接收到最后一条数据
ReplaySubject接收订阅前和订阅后的所有数据
SerializedSubject线程安全的Subject,可由其他Subject调用toSerialized转换而来

常用操作符

类型操作符
创建类型createjusttimerintervalfromerror
转换类型mapflatMapconcatMapswitchMapbuffer
过滤类型filterofTypedebouncethrottleWithTimeoutdistinctdistinctUntilChangedfirstlasttakeskip
结合类型mergezipcombineLatest
错误处理类型retryonErrorResumeNext
辅助操作类型delaysubscribeOndototimeout
条件和布尔类型ambcontainstakeUntilskipUntil
算术和聚合类型reducemaxminsum

Scheduler

类型描述
Schedulers.io缓存线程池,线程数量无穷大,用于I/O操作
Schedulers.computation固定线程池,大小为CPU核数,用于CPU密集型计算(无阻塞)
Schedulers.single单线程池
Schedulers.newThread为指定任务启动一个新的线程
Schedulers.trampoline当其它排队的任务完成后,在当前线程排队开始执行
Schedulers.from使用指定的Executor作为调度器
AndroidSchedulers.mainThreadAndroid的主线程,即UI线程

Plugins

插件,又可称Hook, 可以修改Rxjava的默认行为。

使用RxJavaPlugins可以进行以下三类功能的Hook和自定义:

  • 发射器Observable、Single、Completable和Maybe的生命周期(装载和被订阅)。
  • Rxjava的各类线程调度器Scheduler。
  • Rxjava全局未处理的错误。

特别感谢

最后

如果你觉得这个项目对你学习RxJava3有所帮助, 你可以点击star进行收藏或者将其分享出去, 让更多的人了解和掌握RxJava3!

我是xuexiangjys,一枚热爱学习,爱好编程,致力于Android架构研究以及开源项目经验分享的技术up主。获取更多资讯,欢迎微信搜索公众号:【我的Android开源之旅】

什么样的问题应该使用动态规划

2022年1月18日 02:14
作者 xiangzhihong

说起动态规划,我不知道你有没有这样的困扰,在掌握了一些基础算法和数据结构之后,碰到一些较为复杂的问题还是无从下手,面试时自然也是胆战心惊。如果我说动态规划是个玄幻的问题其实也不为过。究其原因,我觉得可以归因于这样两点:

  • 你对动态规划相关问题的套路和思想还没有完全掌握;
  • 你没有系统地总结过究竟有哪些问题可以用动态规划解决。

知己知彼,你想把动态规划作为你的面试武器之一,就得足够了解它;而应对面试,总结、归类问题其实是个不错的选择,这在我们刷题的时候其实也能感觉得到。那么,我们就针对以上两点,系统地谈一谈究竟什么样的问题可以用动态规划来解。

一、动态规划是一种思想

动态规划算法,这种叫法我想你应该经常听说。嗯,从道理上讲这么叫我觉得也没错,首先动态规划它不是数据结构,这一点毋庸置疑,并且严格意义上来说它就是一种算法。但更加准确或者更加贴切的提法应该是说动态规划是一种思想。那算法和思想又有什么区别呢?

一般来说,我们都会把算法和数据结构放一起来讲,这是因为它们之间密切相关,而算法也往往是在特定数据结构的基础之上对解题方案的一种严谨的总结。

比如说,在一个乱序数组的基础上进行排序,这里的数据结构指的是什么呢?很显然是数组,而算法则是所谓的排序。至于排序算法,你可以考虑使用简单的冒泡排序或效率更高的快速排序方法等等来解决问题。

没错,你应该也感觉到了,算法是一种简单的经验总结和套路。那什么是思想呢?相较于算法,思想更多的是指导你我来解决问题。

比如说,在解决一个复杂问题的时候,我们可以先将问题简化,先解决简单的问题,再解决难的问题,那么这就是一种指导解决问题的思想。另外,我们常说的分治也是一种简单的思想,当然它在诸如归并排序或递归算法当中会常常被提及。

而动态规划就是这样一个指导我们解决问题的思想:你需要利用已经计算好的结果来推导你的计算,即大规模问题的结果是由小规模问题的结果运算得来的

总结一下:算法是一种经验总结,而思想则是用来指导我们解决问题的。既然动态规划是一种思想,那它实际上就是一个比较抽象的概念了,也很难和实际的问题关联起来。所以说,弄清楚什么样的问题可以使用动态规划来解就显得十分重要了。

二、动态规划问题的特点

动态规划作为运筹学上的一种最优化解题方法,在算法问题上已经得到广泛应用。接下来我们就来看一下动归问题所具备的一些特点。

2.1 最优解问题

除非你碰到的问题是简单到找出一个数组中最大的值这样,对这种问题来说,你可以对数组进行排序,然后取数组头或尾部的元素,如果觉得麻烦,你也可以直接遍历得到最值。不然的话,你就得考虑使用动态规划来解决这个问题了。这样的问题一般都会让你求最大子数组、求最长递增子数组、求最长递增子序列或求最长公共子串、子序列等等。

如果碰到求最值问题,我们可以使用下面的套路来解决问题:

  • 优先考虑使用贪心算法的可能性;
  • 然后是暴力递归进行穷举,针对数据规模不大的情况;
  • 如果上面两种都不适合,那么再选择动态规划。

可以看到,求解动态规划的核心问题其实就是穷举。当然了,动态规划问题也不会这么简单了事,我们还需要考虑待解决的问题是否存在重叠子问题、最优子结构等特性。

清楚了动态规划算法的特点,接下来我们就来看一下哪些问题适合用动态规划思想来解题。

1. 乘积最大子数组

给你一个整数数组 numbers,找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),返回该子数组的乘积。


示例1:
输入: [2,7,-2,4]
输出: 14
解释: 子数组 [2,7] 有最大乘积 14。


示例2:
输入: [-5,0,3,-1]
输出: 3
解释: 结果不能为 15, 因为 [-5,3,-1] 不是子数组,是子序列。

首先,很明显这个题目当中包含一个“最”字,使用动态规划求解的概率就很大。这个问题的目的就是从数组中寻找一个最大的连续区间,确保这个区间的乘积最大。由于每个连续区间可以划分成两个更小的连续区间,而且大的连续区间的结果是两个小连续区间的乘积,因此这个问题还是求解满足条件的最大值,同样可以进行问题分解,而且属于求最值问题。同时,这个问题与求最大连续子序列和比较相似,唯一的区别就是你需要在这个问题里考虑正负号的问题,其它就相同了。

对应实现代码:

class Solution {
public:
    int maxProduct(vector<int>& nums) {
        if(nums.empty()) return 0;

        int curMax = nums[0];
        int curMin = nums[0];
        int maxPro = nums[0];
        for(int i=1; i<nums.size(); i++){
            int temp = curMax;    // 因为curMax在下一行可能会被更新,所以保存下来
            curMax = max(max(curMax*nums[i], nums[i]), curMin*nums[i]);
            curMin = min(min(curMin*nums[i], nums[i]), temp*nums[i]);
            maxPro = max(curMax, maxPro);
        }
        return maxPro;
    }
};

2. 最长回文子串

问题:给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

示例1:
输入: "babad"
输出: "bab"


示例2:
输入: "cbbd"
输出: "bb"

【回文串】是一个正读和反读都一样的字符串,比如“level”或者“noon”等等就是回文串。这个问题依然包含一个“最”字,同样由于求解的最长回文子串肯定包含一个更短的回文子串,因此我们依然可以使用动态规划来求解这个问题。

对应实现代码:

class Solution {
        public boolean isPalindrome(String s, int b, int e){//判断s[b...e]是否为回文字符串
        int i = b, j = e;
        while(i <= j){
            if(s.charAt(i) != s.charAt(j)) return false;
            ++i;
            --j;
        }
        return true;
    }
    public String longestPalindrome(String s) {
        if(s.length() <=1){
            return s;
        }
        int l = 1, j = 0, ll = 1;
        for(int i = 1; i < s.length(); ++i){
             //下面这个if语句就是用来维持循环不变式,即ll恒表示:以第i个字符为尾的最长回文子串的长度
             if(i - 1 - ll >= 0 && s.charAt(i) == s.charAt(i-1-ll)) ll += 2;
             else{
                 while(true){//重新确定以i为边界,最长的回文字串长度。确认范围为从ll+1到1
                     if(ll == 0||isPalindrome(s,i-ll,i)){
                         ++ll;
                         break;
                     }
                     --ll;
                 }
             }
             if(ll > l){//更新最长回文子串信息
                l = ll;
                j = i;
            }
        }
        return s.substring(j-l+1, j+1);//返回从j-l+1到j长度为l的子串
    }
}

3. 最长上升子序列

问题:给定一个无序的整数数组,找到其中最长上升子序列的长度。可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。

示例:
输入: [10,9,2,5,3,7,66,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,66],它的长度是 4。

这个问题依然是一个最优解问题,假设我们要求一个长度为 5 的字符串中的上升自序列,我们只需要知道长度为 4 的字符串最长上升子序列是多长,就可以根据剩下的数字确定最后的结果。
对应实现代码:

class Solution {
    public int lengthOfLIS(int[] nums) {
        if(nums.length == 0) return 0;
        int[] dp = new int[nums.length];
        int res = 0;
        Arrays.fill(dp, 1);
        for(int i = 0; i < nums.length; i++) {
            for(int j = 0; j < i; j++) {
                if(nums[j] < nums[i]) dp[i] = Math.max(dp[i], dp[j] + 1);
            }
            res = Math.max(res, dp[i]);
        }
        return res;
    }
}

2.2 求可行性

如果有这样一个问题,让你判断是否存在一条总和为 x 的路径(如果找到了,就是 True;如果找不到,自然就是 False),或者让你判断能否找到一条符合某种条件的路径,那么这类问题都可以归纳为求可行性问题,并且可以使用动态规划来解。

1. 凑零兑换问题

问题:给你 k 种面值的硬币,面值分别为 c1, c2 … ck,每种硬币的数量无限,再给一个总金额 amount,问你最少需要几枚硬币凑出这个金额,如果不可能凑出,算法返回 -1 。

示例1:
输入: c1=1, c2=2, c3=5, c4=7, amount = 15
输出: 3
解释: 11 = 7 + 7 + 1。


示例2:
输入: c1=3, amount =7
输出: -1
解释: 3怎么也凑不到7这个值。

这个问题显而易见,如果不可能凑出我们需要的金额(即 amount),最后算法需要返回 -1,否则输出可能的硬币数量。这是一个典型的求可行性的动态规划问题。

对于示例代码:

class Solution {
    public int coinChange(int[] coins, int amount) {
        if(coins.length == 0)
            return -1;
        //声明一个amount+1长度的数组dp,代表各个价值的钱包,第0个钱包可以容纳的总价值为0,其它全部初始化为无穷大
        //dp[j]代表当钱包的总价值为j时,所需要的最少硬币的个数
        int[] dp = new int[amount+1];
        Arrays.fill(dp,1,dp.length,Integer.MAX_VALUE);
        for (int coin : coins) {
            for (int j = coin; j <= amount; j++) {
                if(dp[j-coin] != Integer.MAX_VALUE) {
                    dp[j] = Math.min(dp[j], dp[j-coin]+1);
                }
            }
        }
        if(dp[amount] != Integer.MAX_VALUE)
            return dp[amount];
        return -1;
    }
}

2. 字符串交错组成问题

问题:给定三个字符串 s1, s2, s3, 验证 s3 是否是由 s1 和 s2 交错组成的。

示例1:
输入: s1="aabcc",s2 ="dbbca",s3="aadbbcbcac"
输出: true
解释: 可以交错组成。


示例2:
输入: s1="aabcc",s2="dbbca",s3="aadbbbaccc"
输出: false
解释:无法交错组成。

这个问题稍微有点复杂,但是我们依然可以通过子问题的视角,首先求解 s1 中某个长度的子字符串是否由 s2 和 s3 的子字符串交错组成,直到求解整个 s1 的长度为止,也可以看成一个包含子问题的最值问题。
对应示例代码:

class Solution {
    public boolean isInterleave(String s1, String s2, String s3) {
        int length = s3.length();
        // 特殊情况处理
        if(s1.isEmpty() && s2.isEmpty() && s3.isEmpty()) return true;
        if(s1.isEmpty()) return s2.equals(s3);
        if(s2.isEmpty()) return s1.equals(s3);
        if(s1.length() + s2.length() != length) return false;
 
        int[][] dp = new int[s2.length()+1][s1.length()+1];
        // 边界赋值
        for(int i = 1;i < s1.length()+1;i++){
            if(s1.substring(0,i).equals(s3.substring(0,i))){
                dp[0][i] = 1;
            }
        }
        for(int i = 1;i < s2.length()+1;i++){
            if(s2.substring(0,i).equals(s3.substring(0,i))){
                dp[i][0] = 1;
            }
        }
        
        for(int i = 2;i <= length;i++){
            // 遍历 i 的所有组成(边界除外)
            for(int j = 1;j < i;j++){
                // 防止越界
                if(s1.length() >= j && i-j <= s2.length()){
                    if(s1.charAt(j-1) == s3.charAt(i-1) && dp[i-j][j-1] == 1){
                        dp[i-j][j] = 1;
                    }
                }
                // 防止越界
                if(s2.length() >= j && i-j <= s1.length()){
                    if(s2.charAt(j-1) == s3.charAt(i-1) && dp[j-1][i-j] == 1){
                        dp[j][i-j] = 1;
                    }
                }
            }
        }
        return dp[s2.length()][s1.length()]==1;
    }
}

2.3 求总数

除了求最值与可行性之外,求方案总数也是比较常见的一类动态规划问题。比如说给定一个数据结构和限定条件,让你计算出一个方案的所有可能的路径,那么这种问题就属于求方案总数的问题。

1. 硬币组合问题

问题:英国的英镑硬币有 1p, 2p, 5p, 10p, 20p, 50p, £1 (100p), 和 £2 (200p)。比如我们可以用以下方式来组成 2 英镑:1×£1 + 1×50p + 2×20p + 1×5p + 1×2p + 3×1p。问题是一共有多少种方式可以组成 n 英镑? 注意不能有重复,比如 1 英镑 +2 个 50P 和 50P+50P+1 英镑是一样的。

示例1:
输入: 2
输出: 73682 

这个问题本质还是求满足条件的组合,只不过这里不需要求出具体的值或者说组合,只需要计算出组合的数量即可。

public class Main {
    public static void main(String[] args) throws Exception {
        
        Scanner sc = new Scanner(System.in);
        while (sc.hasNext()) {
            
            int n = sc.nextInt();
            int coin[] = { 1, 5, 10, 20, 50, 100 };
            
            // dp[i][j]表示用前i种硬币凑成j元的组合数
            long[][] dp = new long[7][n + 1];
            
            for (int i = 1; i <= n; i++) {
                dp[0][i] = 0; // 用0种硬币凑成i元的组合数为0
            }
            
            for (int i = 0; i <= 6; i++) {
                dp[i][0] = 1; // 用i种硬币凑成0元的组合数为1,所有硬币均为0个即可
            }
            
            for (int i = 1; i <= 6; i++) {
                
                for (int j = 1; j <= n; j++) {
                    
                    dp[i][j] = 0;
                    for (int k = 0; k <= j / coin[i - 1]; k++) {
                        
                        dp[i][j] += dp[i - 1][j - k * coin[i - 1]];
                    }
                }
            }
            
            System.out.print(dp[6][n]);
        }
        sc.close();
    }
}

2. 路径规划问题

问题:一个机器人位于一个 m x n 网格的左上角。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角,共有多少路径?

示例1:
输入: 2 2
输出: 2


示例1:
输入: 3 3
输出: 6

这个问题还是一个求满足条件的组合数量的问题,只不过这里的组合变成了路径的组合。我们可以先求出长宽更小的网格中的所有路径,然后再在一个更大的网格内求解更多的组合。这和硬币组合的问题相比没有什么本质区别。

这里有一个规律或者说现象需要强调,那就是求方案总数的动态规划问题一般都指的是求“一个”方案的所有具体形式。如果是求“所有”方案的具体形式,那这种肯定不是动态规划问题,而是使用传统递归来遍历出所有方案的具体形式。

为什么这么说呢?因为你需要把所有情况枚举出来,大多情况下根本就没有重叠子问题给你优化。即便有,你也只能使用备忘录对遍历进行一个简单加速。但本质上,这类问题不是动态规划问题。

对应示例代码:

package com.qst.Tesst;

import java.util.Scanner;

public class Test12 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            int x = scanner.nextInt();
            int y = scanner.nextInt();

            //设置路径
            long[][] path = new long[x + 1][y + 1];
            //设置领导数量
            int n = scanner.nextInt();

            //领导位置
            for (int i = 0; i < n; i++) {
                int a = scanner.nextInt();
                int b = scanner.nextInt();
                path[a][b] = -1;
            }

            for (int i = 0; i <= x; i++) {
                path[i][0] = 1;
            }
            for (int j = 0; j <= y; j++) {
                path[0][j] = 1;
            }

            for (int i = 1; i <= x; i++) {
                for (int j = 1; j <= y; j++) {
                    if (path[i][j] == -1) {
                        path[i][j] = 0;
                    } else {
                        path[i][j] = path[i - 1][j] + path[i][j - 1];
                    }

                }

            }
            System.out.println(path[x][y]);
        }
    }
}

三、 如何确认动态规划问题

从前面我所说来看,如果你碰到了求最值、求可行性或者是求方案总数的问题的话,那么这个问题就八九不离十了,你基本可以确定它就需要使用动态规划来解。但是,也有一些个别情况需要注意:

3.1 数据不可排序

假设我们有一个无序数列,希望求出这个数列中最大的两个数字之和。很多初学者刚刚学完动态规划会走火入魔到看到最优化问题就想用动态规划来求解,事实上,这个问题不是简单做一个排序或者做一个遍历就可以求解出来的。对于这种问题,我们应该先考虑一下能不能通过排序来简化问题,如果不能,才极有可能是动态规划问题。

最小的 k 个数

问题:输入整数数组 arr ,找出其中最小的 k 个数。例如,输入 4、5、1、6、2、7、3、8 这 8 个数字,则最小的 4 个数字是 1、2、3、4。

示例1:
输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]


示例2:
输入:arr = [0,1,2,1], k = 1
输出:[0]

我们发现虽然这个问题也是求“最”值,但其实只要通过排序就能解决,所以我们应该用排序、堆等算法或者数据结构就可以解决,而不应该用动态规划。

对应的示例代码:

public class Solution {
    public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
                int t;
        boolean flag;
        ArrayList result = new ArrayList();
        if(k>input.length){
            return result;
        }
        for(int i =0;i<input.length;i++){
            flag = true;
            for(int j = 0; j < input.length-i;j++)
                if(j<input.length-i-1){
                    if(input[j] > input[j+1]) {
                        t = input[j];
                        input[j] = input[j+1];
                        input[j+1] = t;
                        flag = false;
                    }
                }
            if(flag)break;
        }
        for(int i = 0; i < k;i++){
            result.add(input[i]);
        }
        return  result;
    }
}

3.2 数据不可交换

还有一类问题,可以归类到我们总结的几类问题里去,但是不存在动态规划要求的重叠子问题(比如经典的八皇后问题),那么这类问题就无法通过动态规划求解。

全排列

问题:给定一个没有重复数字的序列,返回其所有可能的全排列。


示例:
输入: [1,2,3]
输出:
[
  [1,2,3],
  [1,3,2],
  [2,1,3],
  [2,3,1],
  [3,1,2],
  [3,2,1]
]

这个问题虽然是求组合,但没有重叠子问题,更不存在最优化的要求,因此可以使用回溯方法处理。

对应的示例代码:

public class Main {
    public static void main(String[] args) {
        perm(new int[]{1,2,3},new Stack<>());
    }
    public static void perm(int[] array, Stack<Integer> stack) {
        if(array.length <= 0) {
            //进入了叶子节点,输出栈中内容
            System.out.println(stack);
        } else {
            for (int i = 0; i < array.length; i++) {
                //tmepArray是一个临时数组,用于就是Ri
                //eg:1,2,3的全排列,先取出1,那么这时tempArray中就是2,3
                int[] tempArray = new int[array.length-1];
                System.arraycopy(array,0,tempArray,0,i);
                System.arraycopy(array,i+1,tempArray,i,array.length-i-1);
                stack.push(array[i]);
                perm(tempArray,stack);
                stack.pop();
            }
        }
    }
}

总结一下,哪些问题可以使用动态规划呢,通常含有下面情况的一般都可以使用动态规划来解决:

  • 求最优解问题(最大值和最小值);
  • 求可行性(True 或 False);
  • 求方案总数;
  • 数据结构不可排序(Unsortable);
  • 算法不可使用交换(Non-swappable)。

如果面试题目出现这些特征,那么在 90% 的情况下你都能断言它就是一个动归问题。除此之外,还需要考虑这个问题是否包含重叠子问题与最优子结构,在这个基础之上你就可以 99% 断言它是否为动归问题,并且也顺势找到了大致的解题思路。

[译]Vue.js + Astro 比 Vue SPA 更好吗?

最近有很多开发者宣布他们已经将网站迁移到Astro。这通常伴随着一张接近完美的Lighthouse分数的截图和一系列火箭表情符号。

像大多数人一样,我发现无休止的新框架会让人感到厌倦。但我对Astro做了一些研究,认为它真的值得一试。

在本文中,我将向您展示如何使用 Astro 构建基于 Vue 的应用程序,我们将了解其独特的架构如何带来比单页应用程序 (SPA) 更好的性能。

SPA 架构回顾

在我们看到 Astro 的实际应用之前,我们需要了解它的架构。为此,让我们首先提醒自己单页应用架构的优缺点。

SPA 将网站的所有功能和内容抽象为 JavaScript 组件。这很棒,因为它使网站的开发变得容易。

这种方法的缺点是当网站投入生产时。所有这些JavaScript组件被捆绑在一起成为一个大的应用程序。由于体积太大,浏览器下载和运行的速度可能会很慢。

当然,你可以通过代码拆分来优化这个捆绑。但是,浏览器仍然会有一些前期成本必须支付,只是为了启动网站。

<!-- 典型的 SPA 页面 -->
<head>
  <script src="/app.js"></script>
</head>
<body>
<!-- 在 app.js 加载之前,此页面没有有意义的内容 -->
<div id="app"></div>
</body>

Islands architecture

Islands架构,Astro使用的架构,也使用了组件。然而,与单页应用程序不同的是,这些组件并没有捆绑到一个 JavaScript 包中。

相反,每个组件都被视为一个独立的迷你应用程序,与所有其他组件隔离存在。

例如,如果您的页面有一个基于 JavaScript 的导航栏,那将是一个迷你应用程序。如果它还具有 JavaScript 驱动的图像轮播,那就是另一个迷你应用程序。等等。

但是,如果这些组件没有被捆绑,它们如何被包含在项目中?我将在下一节解释这个问题。

<!-- Islands architecture -->
<body>
<MyNavBar /> <!-- navbar mini app -->
<main>
  <MyCarousel /> <!-- carousel mini app -->
  <div class="content">
    <!-- more page content... -->
  </div>
</main>
</body>

服务器渲染的组件

Astro 主要是一个静态站点生成器。它适用于大多数支持服务器渲染组件的 UI 库,包括 Vue、Svelte、Preact、React 和 Lit。

因此,当Astro构建你的应用程序时,每个JavaScript组件都在服务器端加载,内容是”快照“。这个快照被添加到静态页面上。

服务器渲染不是 Astro 特有的功能,但在 SPA 中这是一个可选功能,而在 Astro 中,这是一个至关重要的功能,我们将在下面看到。

<!-- 开发内容 -->
<body>
<MyForm /> <!-- JS component -->
</body>

<!-- 快照内容 -->
<body>
<form> <!-- Server rendered JS component -->
  <input type="text" >
  <!-- ... -->
</form>
</body>

Progressive hydration

这就是Astro的神奇之处——通过islands架构、服务器渲染组件和渐进式水合的组合。

由于我们的页面被划分为服务器渲染的迷你应用程序,互动层(JS)可以独立加载,并且只在需要的时候加载。

例如,您可能有一个交互式表单。此表单位于页面下方,位于视口之外。

表单是服务器渲染的,所以我们在页面上看到它。但是,在用户将其滚动到视图中之前,不需要加载昂贵的 JavaScript。

这就是 Astro 中“渐进式水合作用”的含义——我们只在需要时加载 JavaScript。

建立一个 Vue + Astro 项目

现在理论已经讲完了,让我们来看看它的实际效果吧!

要开始创建 Astro 项目,我们将首先创建一个目录:

$ mkdir vue-astro

然后运行 ​​Astro 安装向导:

$ npm init astro

安装向导将允许我们选择“Vue”作为我们选择的框架。这将创建一个包含 Vue 组件的样板项目。

Astro组件

Astro 页面保存在 src/pages 目录中。在默认安装中,我们看到一个文件 index.astro,如下所示。

src/pages/index.astro

---
import VueCounter from '../components/VueCounter.vue';
let title = 'My Astro Site';
---
<html lang="en">
<head>
  <!-- ... -->
  <title>{title}</title> 
</head>
<body>
  <main>
    <!-- ... -->
    <VueCounter client:visible />
  </main>
</body>
</html>

Astro 具有单文件组件样式,与 Vue 类似,但有一些重要区别。

首先,在文件的顶部,我们看到似乎是前端内容,即用 --- 划定的内容。这是在服务器端运行的JavaScript。这不会被发送到客户端。

在这里我们可以看到两件重要的事情:首先,我们正在导入一个Vue组件(你可以从任何支持的框架中导入组件)。另外,我们正在设置一个值:title

这里声明的所有变量在模板中都是可用的。你会注意到 title 在模板中以类似JSX的语法进行插值。

src/pages/index.astro

---
...
let title = 'My Astro Site';
---
<html lang="en">
<head>
  <!-- ... -->
  <title>{title}</title> 
</head>
<!-- ... -->

接下来,注意模板中声明的组件。

默认情况下,组件在客户端是不交互的,只是由Astro进行服务器渲染。

如果我们想让组件交互,即加载 JavaScript,我们需要给它一个指令告诉客户端何时加载它。

在这种情况下,client:visible 指令告诉 Astro 当组件在页面中可见时使 VueCounter 交互。

如果发生这种情况,Astro 将从服务器请求该组件的 JS 并对其进行水合。

---
import VueCounter from '../components/VueCounter.vue';
...
---
<html lang="en">
<head><!-- ... --></head>
<body>
  <main>
    <!-- ... -->
    <VueCounter client:visible />
  </main>
</body>
</html>

加载Astro

现在让我们运行 Astro 的开发服务器来查看我们的项目。

npm run dev

在页面的源代码中,你会看到在文档中没有任何的JavaScript捆绑! 不过,我们确实看到了服务器渲染的Vue组件。

我们还看到 Astro 在文档正文的底部添加了一个脚本。在这里,它加载了一个模块来水合 Vue 组件。

该模块将下载 Vue 组件和依赖项(Vue 框架),而不会阻塞渲染。

index.html

<!-- Page source -->
<body>
<!-- server rendered component -->
<div id="vue" class="counter">
  <button>-</button>
  <pre>0</pre>
  <button>+</button>
</div>

<!-- 添加的代码片段以水合 Vue 组件 --> 
<script type="module">
import setup from '/_astro_frontend/hydrate/visible.js';
// ...
</script>

为什么 Vue + Astro 可能比 Vue SPA 更好

要了解为什么 Astro 在 UX 方面可以击败单页应用程序,让我们对网站加载时发生的情况进行简化分解。

  1. index.html已经加载。它没有JS捆绑,但它包括你的服务器渲染的组件,所以用户已经可以看到你的网站内容--只是还没有互动。
  2. 组件所需的任何JS现在将以一系列独立脚本的形式异步下载。
  3. 下载这些脚本后,它们将被解析并运行。现在可以进行交互了。

现在让我们想象一下,我们把这个网站重新建成一个单页的应用程序。它现在会如何加载?

  1. index.html被加载。由于该页面不包含任何内容,用户无法看到任何东西。浏览器将开始下载捆绑程序。
  2. 下载 JS 包后,浏览器就会对其进行解析。用户仍然看不到任何东西。
  3. 一旦 JS 包被解析并运行,页面内容就生成了。用户现在可以查看应用程序并与之交互。

简单的说:Astro 网站几乎可以立即提供可见的内容,不像 SPA 需要先下载并运行 JS 包。

(Astro应用也会稍早提供互动性,因为它可能不需要下载那么多JS,因为没有SPA外壳、路由器等)

最后的想法

Astro 的架构可能是比单页应用程序更好的选择,因为它无需 JavaScript 即可使内容可见,并且仅在需要时加载 JS。

从理论上讲,单页应用程序可以通过预渲染和代码拆分的组合来实现类似的效果。不同之处在于,Astro 网站默认以这种方式进行优化,因为您需要选择加入交互性和 JS。

当然,并不是每个应用程序都会从​​这种架构中受益,因为 SPA 更适合某些类型的应用程序,例如高度动态和交互式的应用程序。所以我们不会期望 SPA 架构会消失。

绝对是讲的最清楚的-NodeJS模块系统

2022年1月16日 14:08
作者 云中歌

highlight: a11y-dark

theme: smartblue

NodeJS目前有两个系统:一套是CommonJS(简称CJS),另一套是ECMAScript modules(简称ESM); 本篇内容主要三个话题:

  1. CommonJS的内部原理
  2. NodeJS平台的ESM模块系统
  3. CommonJS与ESM的区别;如何在两套系统进行转换
    首先讲讲为什么要有模块系统

为什么要有模块系统

一门好的语言一定要有模块系统,因为它能为我们解决工程中遇到的基本需求

  • 把功能进行模块拆分,能够让代码更具有条理,更容易理解,能够让我们单独开发并测试各个子模块的功能
  • 能够对功能进行封装,然后再其他模块能够直接引入使用,提高复用性
  • 实现封装:只需要对外提供简单的输入输出文档,内部实现能够对外屏蔽,减少理解成本
  • 管理依赖关系:好的模块系统能够让开发者根据现有的第三方模块,轻松的构建其他模块。另外模块系统能够让用户简单引入自己想要的模块,并且把依赖链上的模块进行引入
    刚开始的时候,JavaScript并没有好的模块系统,页面主要是通过多个script标签引入不同的资源。但是随着系统的逐渐复杂化,传统的script标签模式不能满足业务需求,所以才开始计划定义一套模块系统,有AMD,UMD等等
    NodeJS是运行在后台的一门服务端语言,相对于浏览器的html,缺乏script标签来引入文件,完全依赖本地文件系统的js文件。于是NodeJS按照CommonJS规范实现了一套模块系统
    2015年ES2015规范发布,到了这个时候,JS才对模块系统有了正式标准,按照这种标准打造的模块系统叫作ESM系统,他让浏览器和服务端在模块的管理方式上更加一致

CommonJS模块

CommonJS规划中有两个基本理念:

  • 用户可以通过requeire函数,引入本地文件系统中的某个模块
  • 通过exports和module.exports两个特殊变量,对外发布能力

    模块加载器

    下面来简单实现一个简单的模块加载器
    首先是加载模块内容的函数,我们把这个函数放在私有作用域里边避免污染全局环境,然后eval运行该函数

    function loadModule(filname, module, require) {
    const wrappedSrc = `
      (function (module, exports, require) {
        ${fs.readFileSync(filename, 'utf-8')}
      })(module, module.exports, require)
    `
    eval(wrappedSrc)
    }

    在代码中我们通过同步方法readFileSync来读取了模块内容。一般来说,在调用文件系统API时,不应该使用同步版本,但是此处确实是使用了这个方式,Commonjs通过同步操作,来保证多个模块能够安装正常的依赖顺序得到引入
    现在在实现require函数

    function require(moduleName) {
    const id = require.resolve(moduleName);
    if (require.cache[id]) {
      return require.cache[id].exports
    }
    
    // 模块的元数据
    
    const module = {
      exports: {},
      id,
    }
    
    require.cache[id] = module;
    
    loadModule(id, module, require);
    
    // 返回导出的变量
    return module.exports
    }
    
    require.cache = {};
    require.resolve = (moduleName) => {
    // 根据ModuleName解析完整的模块ID
    }

    上面实现了一个简单的require函数,这个自制的模块系统有几个不走需要解释

  • 输入模块的ModuleName以后,首先要解析出模块的完整路径(如何解析后面会讲到),然后把这个结果保存在id的变量之中
  • 如果该模块已经被加载过了,会立刻返回缓存中的结果
  • 如果该模板没有被加载过,那么就配置一套环境。具体来说,先创建一个module变量,让他包含一个exports的属性。这个对象的内容,将由模块在导出API时所使用的的那些代码来填充
  • 将module对象缓存起来
  • 执行loadModule函数,传入刚建立的module对象,通过函数将另外一个模块的内容进行挂载
  • 返回另外模块的导出内容

    模块解析算法

    在前面提到解析模块的完整路径,我们通过传入模块名,模块解析函数能够返回模块的对应的完整路径,接下来通过路径来加载对应模块的代码,并用这个路径来标识模块的身份。resolve函数所用的解析函数主要是处理以下三种情况

  • 要加载的是不是文件模块? 如果moduleName以/开头,那就视为一条绝对路径,加载时只需要安装该路径原样返回即可。如果moduleName以./开头,那么就当成一条相对路径,这样相对路径是从请求载入该模块的这个目录算起的
  • 要加载的是不是核心模块 如果moduleName不是以/或者./开头,那么算法会首先尝试在NodeJS的核心模块去寻找
  • 要加载的是不是包模块 如果没有找到moduleName匹配的核心模块,那就从发出加载请求的这个模块开始,逐层向上搜寻名为node_modules的陌路,看看里边有没有能够与moduleName匹配的模块,如果有就载入该模块。如果还没有,就沿着目录继续线上走,并在相应的node_modules目录中搜寻,一直到文件系统的根目录
    通过这种方式就能实现两个模块依赖不同版本的包,但是仍然能够正常加载
    例如以下目录结构:

    myApp
      - index.js
      - node_modules
          - depA
              - index.js
          - depB
              - index.js
              - node_modules
                  - depA
          - depC
              - index.js
              - node_modules
                  - depA

    在上述例子中虽然myAppdepBdepC都依赖了depA但是加载进来的确实不同的模块。比如:

  • /myApp/index.js中,加载的来源是/myApp/node_modules/depA
  • /myApp/node_modules/depB/index.js, 加载的是/myApp/node_modules/depB/node_modules/depA
  • /myApp/node_modules/depC/index.js, 加载的是/myApp/node_modules/depC/node_modules/depA
    NodeJs之所以能够把依赖关系管理好,就因为它背后有模块解析算法这样一个核心的部分,能够管理上千个包,而不会发生冲突或出现版本不兼容的问题

    循环依赖

    很多人觉得循环依赖是理论上的设计问题,但是这种问题很可能出现在实际项目中,所以应该知道CommonJS如何处理这种情况的。是看之前实现的require函数就能够意识到其中的风险。下面通过一个例子来讲解
    UML 图.jpg
    有个mian.js的模块,需要依赖了a.js和b.js两个模块,同时a.js需要依赖b.js,但是b.js又反过来依赖了a.js,这就造成了循环依赖,下面是源代码:

    // a.js
    exports.loaded = false;
    const b = require('./b');
    module.exports = {
    b,
    loaded: true
    }
    // b.js
    exports.loaded = false;
    const a = require('./a')
    module.exports = {
    a,
    loaded: false
    }
    // main.js
    const a = require('./a');
    const b = require('./b');
    console.log('A ->', JSON.stringify(a))
    console.log('B ->', JSON.stringify(b))

    运行main.js会得到以下结果

image.png
从结果可以看到,CommonJS在循环依赖所引发的风险。b模块导入a模块的时候,内容并不是完整的,具体来说他只是反应了b.js模块请求a.js模块时,该模块所处的状态,而无法反应a.js模块最终加载完毕的一个状态
下面用一个示例图来表示这个过程
UML 图 (1).jpg
下面是具体的流程解释

  1. 整个流程从main.js开始,这个模块一开始开始导入a.js模块
  2. a.js首先要做的,是导出一个名为loaded的值,并把该值设为false
  3. a.js模块要求导入b.js模块
  4. 与a.js类似,b.js首先也是导出loaded为false的变量
  5. b.js继续执行,需要导入a.js
  6. 由于系统已经开始处理a.js模块了,所以b.js会把a.js已经导出的内容,立即复制到本模块中
  7. b.js会把自己导出的loaded值改为false
  8. 由于b已经执行完成,控制权会回到a.js,他会把b.js模块的状态拷贝一份
  9. a.js继续执行,修改导出值loaded为true
  10. 最后就执行main.js
    上面可以看到由于是同步执行,导致b.js导入的a.js模块并不是完整的,无法反应b.js的最终应有的状态。
    在上面例子中可以看到,循环依赖所产生的的结果,这对大型项目来说,更加严重。

使用方法就比较简单了,篇幅有限就不在这篇文章中进行讲解了

ESM

ESM是ECMAScript 2015规范的一部分,这份规范给Javascript制定了统一的模块系统,以适应各种执行环境。ESM和CommonJS的一项重要区别,在于在ES模块是静态的,也就是说引入模块的语句必须要写在最顶层。另外受引用的模块只能使用常量字符串,不能依赖需要运行期动态求值的表达式。
比如我们不能通过下面方式来引入ES模块

if (condition) {
  import module1 from 'module1'
} else {
  import module2 from 'module2'
}

而CommonJS能够根据条件导入不同的模块

let module = null
if (condition) {
  module = require("module1")
} else {
  module = require("module2")
}

看起来相对CommonJS更严格了一些,但是正是因为这种静态引入机制,我们能够对依赖关系进行静态分析,去除不会执行的逻辑,这个就叫tree-shaking

模块加载过程

要想理解ESM系统的运作原理,以及它处理循环依赖的关系,我们需要明白系统是如何解析并执行Javascript代码

载入模块的各个阶段

解释器的目标是构建一张图来描述所要载入的模块之间的依赖关系,这种图也叫做依赖图。
解释器正是通过这种依赖图,来判断模块的依赖关系,并决定自己应该按照什么顺序去执行代码。例如我们需要执行某个js文件,那么解释器会从入口开始,寻找所有的import语句,如果在寻找过程中又遇到了import语句,那就会以深度优先的方式递归,直到所有的代码都解析完毕。
这个过程可细分为三个过程:

  1. 剖析: 找到所有的引入语句,并递归从相关文件中加载每个模块的内容
  2. 实例化: 针对某个导出的实体,在内存中保留一个带名称的引入,但暂且不给他赋值。此时还要根据import和export关键字建立依赖关系,此时不执行js代码
  3. 执行:到了这个阶段,NodeJS开始执行代码,这能够让实际导出的实体,能够获得实际的取值
    在CommonJS中,是边解析依赖,一边执行文件。所以当看到require的时候,就代表前面的代码已经执行完成。因为require操作不一定要在文件开头,而是可以出现在任务地方
    但是ESM系统不同,这三个阶段是分开的,它必须先把依赖图完整的构造出来,然后才开始执行代码

    循环依赖

    在之前提到的CommonJS循环依赖的例子,使用ESM的方式进行改造

    // a.js
    import * as bModule from './b.js';
    export let loaded = false;
    export const b = bModule;
    loaded = true;
    // b.js
    import * as aModule from './b.js';
    export let loaded = false;
    export const a = aModule;
    loaded = true;
    // main.js
    import * as a from './a.js';
    import * as b from './b.js';
    console.log("A =>", a)
    console.log("B =>", b)

    需要注意的是这里不能是用JSON.strinfy方法,因为这里使用了循环依赖
    image.png
    在上面执行结果中可以看到a.js和b.js都能够完整的观察到对方,不同与CommonJS,有模块拿到的状态是不完整的状态。

剖析

下面来解析一下其中的过程:
UML 图 (2).jpg

已上图为例:

  1. 从main.js开始剖析,首先发现了一条import语句,然后进入a.js
  2. 从a.js开始执行,发现了另外一条import语句,执行b.js
  3. 在b.js开始执行,发现了一条import语句,引入a.js,因为之前a.js已经被依赖过,我们不会再去执行这条路径
  4. b.js继续往下执行,发现没有别的import语句。回到a.js之后,也发现没有其他的import语句,然后直接回到main.js入口文件。继续往下执行,发现要求引入b.js,但是这个模块之前被访问过了,因此这条路径不会执行
    经过深度优先的方式,模块依赖关系图已经形成一个树状图,然后解释器在通过这个依赖图执行代码
    在这个阶段,解释器要从入口点开始,开始分析各模块之间的依赖关系。这个阶段解释器只关心系统的import语句,并把这些语句想要引入的模块给加载进来,并以深度优先的方式探索依赖图。按照这种方法遍历依赖关系,得到一种树状的结构

    实例化

    在这一阶段,解释器会从树状结构的底部开始,逐渐向顶部走。没走到一个模块,它就会寻找该模块所要导出的所有属性,并在内存中构建一张隐射表,以存放此模块所要导出的属性名称与该属性即将拥有的取值
    如下图所示:

流程图.jpg
从上图可以看到,模块是按照什么顺序来实例化的

  1. 解释器首先从b.js模块开始,它发现这个模块要导出loaded和a
  2. 然后解释器又分析a.js模块,他发现这个模块要导出loaded和b
  3. 最后分析main.js模块,他发现这个模块不导出任何功能
  4. 实例化阶段所构造的这套exports隐射图,只记录导出的名称与该名称即将拥有的值之间关系,至于这个值本身,既不在本阶段初始化。
    走完上述流程后,解析器还需要在执行一遍,这次他会把各模块所导出的名称与引入这些的那些模块关联起来,如下图所示:

流程图 (1).jpg
这次的步骤为:

  1. 模块b.js要与模块b.js所导出的内容相连接,这条链接叫作aModule
  2. 模块a.js要与模块a.js所导出的内容相连接,这条链接叫作bModule
  3. 最后模块main.js要与模块b.js所导出的内容相连接
  4. 在这个阶段,所有的值并没有初始化,我们只是建立相应的链接,能够让这些链接指向相应的值,至于值本身,需要等到下一阶段才能确定

    执行

    这这个阶段,系统终于要执行每份文件里边的代码。他按照后序的深度优先顺序,由下而上的访问最初那张依赖图,并逐个执行访问到的文件。在本例中,main.js会放在最后执行。这种执行结果保证了,程序在运行主逻辑的时候,各模块所导出的那些值,全部得到了初始化

UML 图.jpg
以上图具体步骤为:

  1. 从b.js开始执行。首先要执行的这行代码,会把该模块所导出的loaded初始化为false
  2. 接下来往下执行,会把aModule复制给a,这个时候a拿到的是一个引用值,这个值就是a.js模块
  3. 然后设置loaded的值为true。这个时候b模块所有的值都全部确定了下来
  4. 现在执行a.js。首先初始化导出值loaded为false
  5. 接下来将该模块导出的b属性值得到初始值,这个值是bModule的引用
  6. 最后把loaded的值改为true。到了这里,我们就把a.js模块系统导出的这些属性所对应的值,最终确定了下来
    走完这些步骤后,系统就可以正式执行main.js文件,这个时候,各模块所导出的属性全都已经求值完毕,由于系统是通过引用而不是复制来引入模块,所以就算模块之间有循环依赖关系,每个模块还是能够完整看到对方的最终状态

    CommonJS与ESM的区别与交互使用

    这里讲CommonJS和ESM之间几个重要的区别,以及如何在必要的时候搭配使用这两种模块

    ESM不支持CommonJS提供的某些引用

    CommonJS提供一些关键引用,不受ESM支持,这包括requireexportsmodule.exports__filename__diranme。如果在ES模块中使用这些,会到程序发生引用错误的问题。
    在ESM系统中,我们可以通过import.meta这个特殊对象来获取一个引用,这个引用指的是当前文件的URL。具体来说,就是通过import.meta.url这种写法,来获取当前模块的文件路径,这个路径类似于file: ///path/to/current_module.js。我们可以根据这条路径,构造出__filename__dirname所表示的那两条绝对路径:

    import { fileURLToPath } from 'url';
    import { dirname } from 'path';
    const __dirname = fileURLToPath(import.meta.url);
    const __dirname = dirname(__filename);

    CommonJS的require函数,也可以通过用下面这种方法,在ESM模块里边进行实现:

    import { createRequire } from 'module';
    const require = createRequire(import.meta.url)

    现在,就可以在ES模块系统的环境下,用这个require()函数来加载Commonjs模块

    在其中一个模块系统中使用另外一个模块

    在上面提到,在ESM模块中使用module.createRequire函数来加载commonJS模块。除了这个方法,其实还可以通过import语言引入CommonJS模块。不过这种方式只会导出默认导出的内容;

    import pkg from 'commonJS-module'
    import { method1 } from 'commonJS-module' // 会报错

    但是反过来没办法,我们没办法在commonJS中引入ESM模块
    此外ESM不支持把json文件当成模块进行引入,这在commonjs却可以轻松实现
    下面这种import语句,就会报错

    import json from 'data.json'

    如果需要引入json文件,还需要借助createRequire函数:

    import { createRequire } from 'module';
    const require = createRequire(import.meta.url);
    const data = require("./data.json");
    console.log(data)

总结

本文主要讲解了NodeJS中两种模块系统是如何工作的,通过了解这些原因能够帮忙我们编写避免一些难以排查的问题的bug

1000粉!使用Three.js制作一个专属3D奖牌🥇

2022年1月20日 00:35
作者 dragonir

背景

破防了 😭!突然发现 SegmentFault 平台的粉丝数量已经突破 1000 了,它是我的三个博客平台掘金、博客园、SegmentFault中首个粉丝突破 1000 的,于是设计开发这个页面,特此纪念一下。非常感谢大家的关注 🙏,后续我会更加专注前端知识的整理分享,写出更多高质量的文章。(希望其他平台也早日破千 😂

本文使用 React + Three.js 技术栈,实现粉丝突破 10003D 纪念页面,包含的主要知识点包括:Three.js 提供的光源、DirectionLight 平行光、HemisphereLight 半球光源、AmbientLight 环境光、奖牌素材生成、贴图知识、MeshPhysicalMaterial 物理材质、TWEEN 镜头补间动画、CSS 礼花动画等。

效果

实现效果图如文章 👆 Banner图 所示,页面由包含我的个人信息的奖牌 🥇1000+ Followers 模型构成,通过以下链接可以实时预览哦 🤣

👀 在线预览:https://dragonir.github.io/3d...

实现

引入资源

首先引入开发功能所需的库,其中 FBXLoader 用于加在 1000+ 字体模型、OrbitControls 镜头轨道控制、TWEEN 用于生成补间动画、Stats 用于开发时性能查看。

import * as THREE from "three";
import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { TWEEN } from "three/examples/jsm/libs/tween.module.min.js";
import Stats from "three/examples/jsm/libs/stats.module";

场景初始化

这部分内容主要用于初始化场景和参数,详细讲解可点击文章末尾链接阅读我之前的文章,本文不再赘述。

container = document.getElementById('container');
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.needsUpdate = true;
container.appendChild(renderer.domElement);
// 场景
scene = new THREE.Scene();
// 给场景设置好看的背景
scene.background = new THREE.TextureLoader().load(backgroundTexture);
// 摄像机
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 0, 0);
camera.lookAt(new THREE.Vector3(0, 0, 0));
// 控制器
controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0, 0);
controls.enableDamping = true;
controls.enableZoom = false;
controls.enablePan = false;
controls.rotateSpeed = .2;
📌 为了达到更好的视觉效果,为 OrbitControls 设置了缩放禁用、平移禁用和减小默认旋转速度

光照效果

为了模拟真实的物理场景,本示例中使用了 3种 光源。

// 直射光
const cubeGeometry = new THREE.BoxGeometry(0.001, 0.001, 0.001);
const cubeMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff });
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
cube.position.set(0, 0, 0);
light = new THREE.DirectionalLight(0xffffff, 1);
light.intensity = 1;
light.position.set(18, 20, 60);
light.castShadow = true;
light.target = cube;
light.shadow.mapSize.width = 512 * 12;
light.shadow.mapSize.height = 512 * 12;
light.shadow.camera.top = 80;
light.shadow.camera.bottom = -80;
light.shadow.camera.left = -80;
light.shadow.camera.right = 80;
scene.add(light);
// 半球光
const ambientLight = new THREE.AmbientLight(0xffffff);
ambientLight.intensity = .8;
scene.add(ambientLight);
// 环境光
const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0xfffc00);
hemisphereLight.intensity = .3;
scene.add(hemisphereLight);

💡 Three.js 提供的光源

Three.js 库提供了一些列光源,而且没种光源都有特定的行为和用途。这些光源包括:

光源名称描述
AmbientLight 环境光这是一种基础光源,它的颜色会添加到整个场景和所有对象的当前颜色上
PointLight 点光源空间中的一点,朝所有的方向发射光线
SpotLight 聚光灯光源这种光源有聚光的效果,类似台灯、天花板上的吊灯,或者手电筒
DirectionLight 平行光也称为无限光。从这种光源发出的光线可以看着平行的。例如,太阳光
HemishpereLight 半球光这是一种特殊光源,可以用来创建更加自然的室外光线,模拟放光面和光线微弱的天空
AreaLight 面光源使用这种光源可以指定散发光线的平面,而不是空间中的一个点
LensFlare 镜头眩光这不是一种光源,但是通过 LensFlare 可以为场景中的光源添加眩光效果

💡 THREE.DirectionLight 平行光

THREE.DirectionLight 可以看作是距离很远的光,它发出的所有光线都是相互平行的。平行光的一个范例就是太阳光。被平行光照亮的整个区域接受到的光强是一样的。

构造函数

new THREE.DirectionLight(color);

属性说明

  • position:光源在场景中的位置。
  • target:目标。它的指向很重要。使用 target 属性,你可以将光源指向场景中的特定对象或位置。此属性需要一个 THREE.Object3D 对象。
  • intensity:光源照射的强度,默认值:1
  • castShadow:投影,如果设置为 true,这个光源就会生成阴影。
  • onlyShadow:仅阴影,如果此属性设置为 true,则该光源只生成阴影,而不会在场景中添加任何光照。
  • shadow.camera.near:投影近点,表示距离光源的哪一个位置开始生成阴影。
  • shadow.camera.far:投影远点,表示到距离光源的哪一个位置可以生成阴影。
  • shadow.camera.left:投影左边界。
  • shadow.camera.right:投影右边界。
  • shadow.camera.top:投影上边界。
  • shadow.camera.bottom:投影下边界。
  • shadow.map.widthshadow.map.height:阴影映射宽度和阴影映射高度。决定了有多少像素用来生成阴影。当阴影具有锯齿状边缘或看起来不光滑时,可以增加这个值。在场景渲染之后无法更改。两者的默认值均为:512

💡 THREE.HemisphereLight 半球光光源

使用半球光光源,可以创建出更加贴近自然的光照效果

构造函数

new THREE.HeimsphereLight(groundColor, color, intensity);

属性说明

  • groundColor:从地面发出的光线颜色。
  • Color:从天空发出的光线颜色。
  • intensity:光线照射的强度。

💡 THREE.AmbientLight 环境光

在创建 THREE.AmbientLight 时,颜色会应用到全局。该光源并没有特别的来源方向,并且不会产生阴影

构造函数

new THREE.AmbientLight(color);

使用建议

  • 通常不能将 THREE.AmbientLight 作为场景中唯一的光源,因为它会将场景中的所有物体渲染为相同的颜色。
  • 使用其他光源,如 THREE.SpotLightTHREE.DirectionLight的同时使用它,目的是弱化阴影或给场景添加一些额外颜色。
  • 由于 THREE.AmbientLight 光源不需要指定位置并且会应用到全局,所以只需要指定个颜色,然后将它添加到场景中即可。

添加网格和地面

添加网格是为了方便开发,可以调整模型的合适的相对位置,本例中保留网格的目的是为了页面更有 3D景深效果。透明材质的地面是为了显示模型的阴影。

// 网格
const grid = new THREE.GridHelper(200, 200, 0xffffff, 0xffffff);
grid.position.set(0, -30, -50);
grid.material.transparent = true;
grid.material.opacity = 0.1;
scene.add(grid);
// 创建地面,透明材质显示阴影
var planeGeometry = new THREE.PlaneGeometry(200, 200);
var planeMaterial = new THREE.ShadowMaterial({ opacity: .5 });
var plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.rotation.x = -0.5 * Math.PI;
plane.position.set(0, -30, -50);
plane.receiveShadow = true;
scene.add(plane);

创建奖牌

由于时间关系,本示例奖牌模型直接使用 Three.js 自带的基础立方体模型 THREE.BoxGeometry 来实现,你也可以使用其他立方体如球体、圆珠等,甚至可以使用 Blender 等专业建模软件创建自己喜欢的奖牌形状。(ps:个人觉得立方体也挺好看的 😂)

💡 奖牌UI素材生成

🥇 奖牌上下面和侧面贴图制作

为了生成的奖牌有黄金质感,本例中使用 👇 该材质贴图,来生成亮瞎眼的24K纯金效果 🤑

🥇 奖牌正面和背面贴图制作

奖牌的正面和背面使用的贴图是 SegmentFault 个人中心页的截图,为了更具有金属效果,我用 👆 上面金属材质贴图给它添加了一个带有圆角的边框

Photoshop 生成圆角金属边框具体方法:截图上面添加金属图层 -> 使用框选工具框选需要删除的内容 -> 点击选择 -> 点击修改 -> 点击平滑 -> 输入合适的圆角大小 -> 删除选区 -> 合并图层 -> 完成并导出图片。

最终的正反面的材质贴图如 👇 下图所示,为了显示更清晰,我在 Photoshop 中同时修改了图片的对比度饱和度,并加了 SegmentFaultLogo 在上面。

🥇 奖牌正面和背面的法相贴图制作

为了生成凹凸质感,就需要为模型添加法相贴图。使用 👆 上面已经生成的正面和背面的材质贴图,就可以使用在线工具自动生成法相贴图。生成时可以根据需要,通过调整 StrengthLevelBlur 等参数进行样式微调,并且能够实时预览。调整好后点击 Download 下载即可。

🚪 法相贴图在线制作工具传送门:NormalMap-Online

通过多次调节优化,最终使用的法相贴图如 👇 下图所示。

使用上面生成的素材,现在进行奖牌模型的构建。正面和背面使用个人信息材质,其他面使用金属材质。然后遍历对所有面调整金属度粗糙度样式。

let segmentMap = new THREE.MeshPhysicalMaterial({map: new THREE.TextureLoader().load(segmentTexture), normalMap: new THREE.TextureLoader().load(normalMapTexture) });
let metalMap = new THREE.MeshPhysicalMaterial({map: new THREE.TextureLoader().load(metalTexture)});
// 创建纹理数组
const boxMaps = [metalMap, metalMap, metalMap, metalMap, segmentMap, segmentMap];
// 💡 立方体长宽高比例需要和贴图的大小比例一致,厚度可以随便定
box = new THREE.Mesh(new THREE.BoxGeometry(297, 456, 12), boxMaps);
box.material.map(item => {
  // 材质样式调整
  item.metalness = .5;
  item.roughness = .4;
  item.refractionRatio = 1;
  return item;
});
box.scale.set(0.085, 0.085, 0.085);
box.position.set(-22, 2, 0);
box.castShadow = true;
meshes.push(box);
scene.add(box);

👆 上面 4 张效果图依次对应的是:

  • 图1:创建没有贴图的 BoxGeometry,只是一个白色的立方体。
  • 图2:立方体添加 材质贴图,此时没有凹凸效果
  • 图3:立方体添加 法相贴图,此时产生凹凸效果
  • 图4:调节立方体材质的 金属度粗糙程度反射率,更具有真实感。

💡 Three.js 中的贴图

贴图类型
  • map:材质贴图
  • normalMap:法线贴图
  • bumpMap:凹凸贴图
  • envMap:环境贴图
  • specularMap:高光贴图
  • lightMap:光照贴图
贴图原理

通过纹理贴图加载器 TextureLoader() 去新创建一个贴图对象出来,然后再去调用里面的 load() 方法去加载一张图片,这样就会返回一个纹理对象,纹理对象可以作为模型材质颜色贴图 map 属性的值,材质的颜色贴图属性 map 设置后,模型会从纹理贴图上采集像素值。

💡 MeshPhysicalMaterial 物理材质

MeshPhysicalMaterial 类是 PBR 物理材质,可以更好的模拟光照计算,相比较高光网格材质MeshPhongMaterial 渲染效果更逼真。

如果你想展示一个产品,为了更逼真的渲染效果最好选择该材质,如果游戏为了更好的显示效果可以选择 PBR 材质 MeshPhysicalMaterial,而不是高光材质 MeshPhongMaterial
特殊属性
  • .metalness 金属度属性:表示材质像金属的程度。非金属材料,如木材或石材,使用 0.0,金属使用 1.0,中间没有(通常). 默认 0.5. 0.01.0 之间的值可用于生锈的金属外观。如果还提供了粗糙度贴图 .metalnessMap,则两个值都相乘。
  • .roughness 粗糙度属性:表示材质的粗糙程度. 0.0 表示平滑的镜面反射,1.0 表示完全漫反射. 默认 0.5. 如果还提供粗糙度贴图 .roughnessMap,则两个值相乘.
  • .metalnessMap 金属度贴图:纹理的蓝色通道用于改变材料的金属度.
  • .roughnessMap 粗糙度贴图:纹理的绿色通道用于改变材料的粗糙度。
📌 注意使用物理材质的时候,一般需要设置环境贴图 .envMap

加载1000+文字模型

1000+ 字样的模型使用 THREE.LoadingManagerFBXLoader 加载。详细使用方法也不再本文中赘述,可参考文章末尾链接查看我的其他文章,里面有详细描述。😁

const manager = new THREE.LoadingManager();
manager.onProgress = async(url, loaded, total) => {
  if (Math.floor(loaded / total * 100) === 100) {
    // 设置加载进度
    _this.setState({ loadingProcess: Math.floor(loaded / total * 100) });
    // 加载镜头移动补间动画
    Animations.animateCamera(camera, controls, { x: 0, y: 4, z: 60 }, { x: 0, y: 0, z: 0 }, 3600, () => {});
  } else {
    _this.setState({ loadingProcess: Math.floor(loaded / total * 100) });
  }
};
const fbxLoader = new FBXLoader(manager);
fbxLoader.load(textModel, mesh => {
  mesh.traverse(child => {
    if (child.isMesh) {
      // 生成阴影
      child.castShadow = true;
      // 样式调整
      child.material.metalness = 1;
      child.material.roughness = .2;
      meshes.push(mesh);
    }
  });
  mesh.position.set(16, -4, 0);
  mesh.rotation.x = Math.PI / 2
  mesh.scale.set(.08, .08, .08);
  scene.add(mesh);
});

补间动画

相机移动实现漫游等动画,页面打开时,模型加载完毕从大变小的动画就是通过 TWEEN 实现的。

animateCamera: (camera, controls, newP, newT, time = 2000, callBack) => {
  var tween = new TWEEN.Tween({
    x1: camera.position.x, // 相机x
    y1: camera.position.y, // 相机y
    z1: camera.position.z, // 相机z
    x2: controls.target.x, // 控制点的中心点x
    y2: controls.target.y, // 控制点的中心点y
    z2: controls.target.z, // 控制点的中心点z
  });
  tween.to({
    x1: newP.x,
    y1: newP.y,
    z1: newP.z,
    x2: newT.x,
    y2: newT.y,
    z2: newT.z,
  }, time);
  tween.onUpdate(function (object) {
    camera.position.x = object.x1;
    camera.position.y = object.y1;
    camera.position.z = object.z1;
    controls.target.x = object.x2;
    controls.target.y = object.y2;
    controls.target.z = object.z2;
    controls.update();
  });
  tween.onComplete(function () {
    controls.enabled = true;
    callBack();
  });
  tween.easing(TWEEN.Easing.Cubic.InOut);
  tween.start();
}

动画更新

最后不要忘了要在 requestAnimationFrame 中更新场景、轨道控制器、TWEEN、以及模型的自转 🌍 等。

// 监听页面缩放,更新相机和渲染
function onWindowResize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
  stats && stats.update();
  controls && controls.update();
  TWEEN && TWEEN.update();
  // 奖牌模型自转
  box && (box.rotation.y += .04);
}

礼花动画

最后,通过 box-shadow 和简单的 CSS 动画,给页面添加 🎉 绽放效果,营造 🎅 欢庆氛围!

<div className="firework_1"></div>
<div className="firework_2"></div>
<!-- ... -->
<div className="firework_10"></div>

样式动画:

[class^=firework_] {
  position: absolute;
  width: 0.1rem;
  height: 0.1rem;
  border-radius: 50%;
  transform: scale(8)
}
.firework_1 {
  animation: firework_lg 2s both infinite;
  animation-delay: 0.3s;
  top: 5%;
  left: 5%;
}
@keyframes firework_lg {
  0%, 100% {
    opacity: 0;
  }
  10%, 70% {
    opacity: 1;
  }
  100% {
    box-shadow: -0.9rem 0rem 0 #fff, 0.9rem 0rem 0 #fff, 0rem -0.9rem 0 #fff, 0rem 0.9rem 0 #fff, 0.63rem -0.63rem 0 #fff, 0.63rem 0.63rem 0 #fff, -0.63rem -0.63rem 0 #fff, -0.63rem 0.63rem 0 #fff;
  }
}

实现效果:

🔗 完整代码 https://github.com/dragonir/3...

总结

本文中主要涉及到的知识点包括:

  • Three.js 提供的光源
  • THREE.DirectionLight 平行光
  • THREE.HemisphereLight 半球光光源
  • THREE.AmbientLight 环境光
  • 奖牌 UI 素材生成
  • Three.js 中的贴图
  • MeshPhysicalMaterial 物理材质
  • TWEEN 镜头补间动画
  • CSS 礼花动画

想了解场景初始化、光照、阴影及其他 Three.js 的相关知识,可阅读我的其他文章。如果觉得文章对你有帮助,不要忘了 一键三连 👍

附录

PWA 技术落地!让你的站点(Web)秒变APP(应用程序)

Web应用方兴未艾,我们已经十分习惯习惯了在电脑上进行以自己的工作,而随着众多功能强大的在线网站,我们的Windows的桌面也不再拥挤着各种快捷方式;不光是PC端,在移动端我们也不再在浩如烟海的应用市场安装各种软件,轻量级的各种小程序取代了他们的位置,无需安装点开即用的方式为大家的工作生活带来了很大便利。

我们很明白这种改变对我们生活工作带来的便利,但是偶尔在网上冲浪我们也会怀念那个满桌面都是本地应用的时代,桌面双击即可运行,不用在开启的网页中搜寻我们需要的功能网页,哪怕断网了依旧可以正常使用,使用速度上它们似乎是比网页更快。

看到这里你可能想说,你说这么多,跟你讲的PWA有什么关系啊?

什么是 渐进式 Web 应用( PWA )?

                --- 渐进式 Web 应用(Progressive Web App简称PWA)介绍

PWA 指的是使用指定技术和标准模式来开发的 Web 应用,让Web应用具有原生应用的特性和体验。比如我们觉得本地应用使用便捷,响应速度更加快等。

通过PWA技术,有两个好处。一方面应用开发还是采用Web开发的方式,我们只需要简单的配置就可以使用,无需为各种操作系统制作安装包,应用的入口依旧是网页,在浏览器中一键安装,没有繁琐的访问应用商店下载过程。

另一方面应用安装后,用户可以通过桌面图标快速访问,应用所需资源在第一次安装后离线缓存在本地离线也可使用,可以实时使用系统推送,应用自动升级无需重新安装。

例如在Chrome中支持PWA技术的站点,可以直接在地址栏中点击安装,或者浏览器选项中点击install安装。

PWA 现状

PWA 由 Google 于 2016 年提出,于 2017 年正式技术落地,并在 2018 年迎来重大突破,全球顶级的浏览器厂商,Google、Microsoft、Apple 已经全数宣布支持 PWA 技术。PWA的关键技术是Service Worker,目前桌面和移动设备上的所有主流浏览器都已支持。目前除了Safari,其他主流浏览器都已支持添加主屏幕、推送通知消息。

在这里我们简单为大家介绍一下Service Worker。

Service Worker充当代理服务器的位置,位于 Web 应用程序、浏览器和网络(如果可用)之间。这个API旨在创造更好的离线体验,拦截网络请求并根据网络是否可用采取适当的行动,并更新驻留在服务器上的内容,它还允许访问推送通知和并和后台API同步。

PWA 的使用场景和未来在何处?

根据PWA的介绍,你可能会问,这玩意儿的价值在哪里呢?

目前的数据统计显示移动端之下,PWA并没有太多市场,在我们移动端上3G、4G到现在5G一个百兆的APP可以被很快的下,除了坐飞机,我们的手机基本不会有离线的时候。

而在PC端,我们开始工作,只要你还在使用Office等办公软件,你就会意识到WPA带来的便捷不可估量。在全球信息化的过程中,我们的公司也在不断信息化的过程。各种常用的工具软件会变成必备的一环,从而被集成到Web应用中。比如在线Excel在线报表设计,在线word等。

这一切,都逐渐和"在线""web前端"连在一起。

想要将这些应用顺利挪入Web应用中,却不是那么简单的事。这些工具功能复杂,资源较重,同时对于一些需要实时反馈的工作流项目,也会经常出现忘记操作的情况。

仅拿在线Excel来说,协同编辑这一方面的难点包括不仅限于:多人冲突处理、版本数据更新、房间管理、富文本处理、复制黏贴处理等等。

下图使用PWA技术集成了Excel表格编辑器,对于最终用户,完全保留了Excel的操作体验,多任务工作时,使用alt(cmd)——tab快速切换应用,系统级别推送实时关注工作状态。而这一切都可以在我们的Web应用中出现,不再需要本地应用。

介绍了PWA的相关知识点,下面我们就一起来通过实例看看PWA如何让一个站点变成APP吧。

实例使用

准备工作,下载表格编辑器示例, https://www.grapecity.com.cn/...
让SpreadJS在线表格编辑器支持PWA只需要实现App Manifest 和 Service Worker

  1. 添加 manifest.json 文件
    新建manifest.json,并在index.html中引用

    {
      "name": "SpreadJSDesigner",
      "short_name": "SJSD",
      "descriptions": "SpreadJS在线表格编辑器",
      "start_url": "./",
      "background_color": "#fff",
      "display": "minimal-ui",
      "scope": "./",
      "theme_color": "#fff",
      "icons": [
     {
       "src": "./welcome.png",
       "type": "image/png",
       "sizes": "200x200",
       "purpose": "any"
     }
      ]
    }
1.    <link rel="manifest" href="./manifest.json">
  1. 实现Service Worker
    新建sw.js, 通过Service Worker缓存设计器所需要的spreadjs资源

    var cacheName = 'v14.2.2';
    var cacheFiles = [
        '/',
        './index.html',
        './lib/css/gc.spread.sheets.excel2013white.14.2.2.css',
      './lib/css/gc.spread.sheets.designer.14.2.2.css',
      './custom.css',
        './lib/scripts/gc.spread.sheets.all.14.2.2.js',
        './lib/scripts/plugins/gc.spread.sheets.charts.14.2.2.js',
        './lib/scripts/plugins/gc.spread.sheets.shapes.14.2.2.js',
        './lib/scripts/plugins/gc.spread.sheets.print.14.2.2.js',
        './lib/scripts/plugins/gc.spread.sheets.barcode.14.2.2.js',
        './lib/scripts/plugins/gc.spread.sheets.pdf.14.2.2.js',
        './lib/scripts/plugins/gc.spread.pivot.pivottables.14.2.2.js',
        './lib/scripts/interop/gc.spread.excelio.14.2.2.js',
        './lib/scripts/resources/zh/gc.spread.sheets.resources.zh.14.2.2.js',
        './lib/scripts/gc.spread.sheets.designer.resource.cn.14.2.2.js',
        './lib/scripts/gc.spread.sheets.designer.all.14.2.2.js',
    ];
    // 监听 install 事件,安装完成后,进行文件缓存
    self.addEventListener('install', function (e) {
        console.log('Service Worker 状态: install');
        var cacheOpenPromise = caches.open(cacheName).then(function (cache) {
      // 把要缓存的 cacheFiles 列表传入
      return cache.addAll(cacheFiles);
        });
        e.waitUntil(cacheOpenPromise);
    });
    // 监听 fetch 事件,安装完成后,进行文件缓存
    self.addEventListener('fetch', function (e) {
        console.log('Service Worker 状态: fetch');
        var cacheMatchPromise = caches.match(e.request).then(function (cache) {
          // 如果有cache则直接返回,否则通过fetch请求
          return cache || fetch(e.request);
      }).catch(function (err) {
          console.log(err);
          return fetch(e.request);
      })
        e.respondWith(cacheMatchPromise);
    });
    // 监听 activate 事件,清除缓存
    self.addEventListener('activate', function (e) {
        console.log('Service Worker 状态: activate');
        var cachePromise = caches.keys().then(function (keys) {
      return Promise.all(keys.map(function (key) {
          if (key !== cacheName) {
              return caches.delete(key);
          }
      }));
        })
        e.waitUntil(cachePromise);
        return self.clients.claim();
    });

    index.html页面组册sw.js

    <script>
              if ('serviceWorker' in navigator) {
                      window.addEventListener('load', function () {
                              navigator.serviceWorker.register('./sw.js')
                              .then(function (registration) {
                                      // 注册成功
                                      console.log('ServiceWorker registration successful with scope: ', registration.scope);
                              })
                              .catch(function (err) {
                                      // 注册失败:
                                      console.log('ServiceWorker registration failed: ', err);
                              });
                      });
              }
      </script>

    通过以上两个步骤的操作,spreadjs在线表格编辑器页面就支持PWA了。注意PWA需要https的支持,本地通过localhost测试不受影响。
    通过localhost访问页面,可以在Chrome地址栏看到安装选项

安装后,就可以通过应用程序按钮双击访问了

对于Chrome 的PWA应用,同样可以通过快捷键开启开发者工具,在Network中可以看到,资源都是通过ServiceWorker缓存获取

以上便是借助PWA技术让SpreadJS在线表格编辑器变成桌面编辑器的操作步骤,大家在熟练掌握并使用 PWA 架构及其相关技术后,便可以试着用它来构建更具高可用的现代化 Web 应用,快去试试吧!

智能运营:有效触达,驱动用户生命周期价值升级

2022年1月17日 07:06
作者 HMSCore

用户运营的本质在于同用户进行有效的沟通。用户处于不同生命周期的阶段,相应的运营重点也不同,因此需要通过针对性的运营,帮助提升各生命周期的用户价值。

华为分析服务的特性智能运营可以帮助运营人员实现“在恰当的时刻,给恰当的人,推送恰当的消息”,对用户进行有效触达。根据用户的生命周期和运营目标,可以分为5个阶段进行触达:

1、新手期激活阶段

新手期一般是指用户完成下载注册,但并未真正使用过产品的核心价值。用户激活就是让潜在的用户及流量实现从访客到用户的转变,引导用户去使用产品核心功能。
在智能运营平台,先通过用户标签和事件圈选出新手期用户,然后设置周期性任务,对新手期用户进行产品消息的推送,让用户更早、更容易到达核心功能点。

  • 智能运营-创建受众群组

2、成长期转化阶段

促进转化是促使用户生命周期向成熟期迈进的重要一步。在此阶段,需要通过运营,促使用户多次完成预期目标并产生价值,如购买、发布内容等。

如何促进用户产生更多的转化动作?良好的交互和有重点的引导,会让用户更容易进入产品规划好的核心路径。以促进首单购买为例,通过智能运营精准圈选出多次浏览某一类型商品的人群,发送品类优惠券并进行推送通知,以提升用户的购买意愿。

3、成熟期留存阶段

在提高用户留存阶段,我们需要找到用户热衷于产品的关键功能,在现有的使用粘度上,让用户在产品中不断探索新的价值。

比如内容型App通过留存分析挖掘用户的内容类型偏好,借助智能运营的个性化推荐,持续给用户提供喜欢的内容,从而提高用户留存率。

4、沉默期唤醒阶段

沉睡用户的唤醒是用户运营的重要环节,首先要对沉睡用户进行识别,然后将合适的内容发送给恰当的用户,提升推送内容的打开率。

A/B测试选择合适文案,提高打开率

选择智能运营的“Push文案测试”模板,将目标用户随机分成两个部分,推送不同的文案,根据最终测试结果选择效果较好的文案用于运营活动创建。

  • Push文案测试模板

多渠道智能触达,提高到达率

合适的内容已经准备好,借助智能运营的多波次营销功能,让内容可以更安全快速到达用户。当消息首次通过Push发送失败后,自动采取短信补发,同时对未到达用户进行防打扰设置,避免重复发送。

  • 多波次营销功能

5、流失期召回阶段

已经流失的用户需要召回,才能继续使用产品。针对长期不活跃用户,召回策略可与唤醒一致;但对于已经卸载的用户,需要进一步卸载分析,针对性的给予召回。

  • 卸载召回流程

    用户的生命周期管理是长期的,智能运营支持多渠道、多场景精准触达目标受众,活动效果实时监控,让运营人员可以轻松构建和管理活动流程中的每一步,实现各个阶段的有效触达。

了解更多详情>>

访问华为开发者联盟官网
获取开发指导文档
华为移动服务开源仓库地址:GitHubGitee

关注我们,第一时间了解 HMS Core 最新技术资讯~

SegmentFault D-Day 2021 年度总结报告

2022年1月17日 09:31
关于 D-Day:
这里的 D-Day 不是军事术语,而是开发者(Developer)的抢滩登陆之日!SegmentFault D-Day 全称 SegmentFault Developer Day,是由 SegmentFault 思否发起主办的系列技术沙龙。自 2014 年启动,D-Day 技术沙龙已在北、上、广、深、杭等 10 个城市巡回举办了数十场,涉及大前端、技术架构、云计算、大数据、移动开发等众多主题,覆盖数万名开发者。

2021 SegmentFault D-Day 强势回归,于 7 月开始相继举行 5 场技术分享,聚焦云原生、大前端、技术架构、开源等领域。一直以来我们在技术传播的道路上从未放慢步伐,在技术分享的质量上也从未放低要求,因此 SegmentFault D-Day 也受到了越来越多开发者的热爱与肯定。

我们希望为更多的开发者,在 SegmentFault 思否社区上有更多的收获~

2021 SegmentFault D-Day 已经结束,我们重新整理了 D-Day 的精彩内容,再次与大家分享。以下是 2021 年全年 D-Day 的精彩回顾:

SegmentFault D-Day 大前端技术沙龙

嘉宾议题介绍:

  • 夏温武,阿里巴巴前端技术专家《淘系 web 前端架构开发实践》
  • 陆宏鸣,驻云科技技术总监 《如何进行前端性能的观测》
  • 郑仁杰,字节跳动前端工程师 《面向未来与浏览器规范的前端 DDD 架构设计》
  • 姚尧,葡萄城技术布道师 《前端电子表格技术分享》
  • 卡颂,《React技术揭秘》作者 《React 对全球前端框架发展的影响》

分享回顾:https://ke.segmentfault.com/c...

SegmentFault D-Day 后端架构演进之路

嘉宾议题介绍:

  • 方杨,阿里巴巴技术专家《NOAH-面向业务价值的自适应高可用架构》
  • 蒋烁淼,观测云创始人 《SRE工程的落地指南》
  • 李能,每日互动股份有限公司(个推)服务端资深技术专家 《个推API网关演进之路》
  • 毛雯蓓,网易有道资深开发工程师《自建私有云or公有云 —— 有道少儿团队的云原生实践与思考》
  • 周斌杰,蚂蚁集团技术专家《service mesh 落地之后:为 sidecar 注入灵魂》

分享回顾:https://ke.segmentfault.com/c...

SegmentFault D-Day Online 开源开放与新技术创新

嘉宾议题介绍:

  • 潘杨,观测云产品方案架构师《开源可观测与观测云查询语言对比》
  • 马嘉伦,移动端开发工程师 《浅谈 Flutter 复杂状态管理 — 封装一个可扩展的 IM 页面库》
  • 张晋涛,支流科技云原生技术专家 《容器镜像构建的原理和最佳实践》

分享回顾:https://ke.segmentfault.com/c...

SegmentFault D-Day Online 论道云原生与微服务

嘉宾议题介绍:

  • 马若飞,FreeWheel 首席工程师《FreeWheel的云原生应用实践》
  • 刘玉杰,观测云产品方案架构师《第三代微服务技术Istio实操》
  • 王英伟,解决方案架构师《云原生全家桶,企业真的需要吗?》
  • 方阗,青云科技顾问研发工程师《镜像构建技术 Buildpacks 的原理及在函数计算平台中的落地》

分享回顾:https://ke.segmentfault.com/c...

SegmentFault D-Day Online 前端前沿技术实践

嘉宾议题介绍:

  • 范济颖(边城),四川凯路威科技有限公司 软件总工程师《前端技术迁移实践 - 从 jQuery 到 Vue3》
  • 邓昌亮,观测云 资深前端开发工程师《如何优雅的捕获 web 应用的各种指标数据》
  • 陈俊文,腾讯科技有限公司 前端高级工程师《从小众到大众-看Web端如何玩转音视频》
  • 姚尧,西安葡萄城软件有限公司 技术布道师《前端电子表格技术应用实践》

分享回顾:https://ke.segmentfault.com/c...

特别鸣谢

最后,特别感谢在 SegmentFault D-Day 中,给我们分享技术内容的老师们,也感谢一直支持我们的合作伙伴!

SegmentFault 将继续致力于维护最纯粹的技术分享平台,为大家带来更干货的实践内容分享,也为开发者们提供更优质的交流空间。

期待 2022 我们与各位开发者小伙伴们一起成长~


在社区中有什么问题也可以反馈给小姐姐哦~

image.png

思否小姐姐在这里提前祝大家新年快乐,新的一年头发浓密,没有 BUG ~

线程池如何观测?这个方案让你对线程池的运行情况了如指掌!

2022年1月17日 08:04
作者 铂赛东

今天我们来聊一个比较实用的话题,动态可监控可观测的线程池实践。

这是个全新的开源项目,作者提供了一种非常好的思路解决了线程池的可观测问题。

这个开源项目叫:DynamicTp

地址在文章末尾。


写在前面

稍微有些Java编程经验的小伙伴都知道,Java的精髓在juc包,这是大名鼎鼎的Doug Lea老爷子的杰作,评价一个程序员Java水平怎么样,一定程度上看他对juc包下的一些技术掌握的怎么样,这也是面试中的基本上必问的一些技术点之一。

juc包主要包括:

1.原子类(AtomicXXX)

2.锁类(XXXLock)

3.线程同步类(AQS、CountDownLatch、CyclicBarrier、Semaphore、Exchanger)

4.任务执行器类(Executor体系类,包括今天的主角ThreadPoolExecutor)

5.并发集合类(ConcurrentXXX、CopyOnWriteXXX)相关集合类

6.阻塞队列类(BlockingQueue继承体系类)

7.Future相关类

8.其他一些辅助工具类

多线程编程场景下,这些类都是必备技能,会这些可以帮助我们写出高质量、高性能、少bug的代码,同时这些也是Java中比较难啃的一些技术,需要持之以恒,学以致用,在使用中感受他们带来的奥妙。

上边简单罗列了下juc包下功能分类,这篇文章我们主要来介绍动态可监控线程池的,所以具体内容也就不展开讲了,以后有时间单独来聊吧。看这篇文章前,希望读者最好有一定的线程池ThreadPoolExecutor使用经验,不然看起来会有点懵。

如果你对ThreadPoolExecutor不是很熟悉,推荐阅读下面两篇文章

javadoop: https://www.javadoop.com/post/java-thread-pool

美团技术博客: https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html


背景

使用ThreadPoolExecutor过程中你是否有以下痛点呢?

1.代码中创建了一个ThreadPoolExecutor,但是不知道那几个核心参数设置多少比较合适

2.凭经验设置参数值,上线后发现需要调整,改代码重启服务,非常麻烦

3.线程池相对开发人员来说是个黑盒,运行情况不能感知到,直到出现问题

如果你有以上痛点,这篇文章要介绍的动态可监控线程池(DynamicTp)或许能帮助到你。

如果看过ThreadPoolExecutor的源码,大概可以知道其实它有提供一些set方法,可以在运行时动态去修改相应的值,这些方法有:

public void setCorePoolSize(int corePoolSize);
public void setMaximumPoolSize(int maximumPoolSize);
public void setKeepAliveTime(long time, TimeUnit unit);
public void setThreadFactory(ThreadFactory threadFactory);
public void setRejectedExecutionHandler(RejectedExecutionHandler handler);

现在大多数的互联网项目其实都会微服务化部署,有一套自己的服务治理体系,微服务组件中的分布式配置中心扮演的就是动态修改配置,实时生效的角色。那么我们是否可以结合配置中心来做运行时线程池参数的动态调整呢?答案是肯定的,而且配置中心相对都是高可用的,使用它也不用过于担心配置推送出现问题这类事儿,而且也能减少研发动态线程池组件的难度和工作量。

综上,我们总结出以下的背景

  • 广泛性:在Java开发中,想要提高系统性能,线程池已经是一个90%以上的人都会选择使用的基础工具
  • 不确定性:项目中可能会创建很多线程池,既有IO密集型的,也有CPU密集型的,但线程池的参数并不好确定;需要有套机制在运行过程中动态去调整参数
  • 无感知性,线程池运行过程中的各项指标一般感知不到;需要有套监控报警机制在事前、事中就能让开发人员感知到线程池的运行状况,及时处理
  • 高可用性,配置变更需要及时推送到客户端;需要有高可用的配置管理推送服务,配置中心是现在大多数互联网系统都会使用的组件,与之结合可以大幅度减少开发量及接入难度

简介

我们基于配置中心对线程池ThreadPoolExecutor做一些扩展,实现对运行中线程池参数的动态修改,实时生效;以及实时监控线程池的运行状态,触发设置的报警策略时报警,报警信息会推送办公平台(钉钉、企微等)。报警维度包括(队列容量、线程池活性、拒绝触发等);同时也会定时采集线程池指标数据供监控平台可视化使用。使我们能时刻感知到线程池的负载,根据情况及时调整,避免出现问题影响线上业务。

    |  __ \                            (_) |__   __|
    | |  | |_   _ _ __   __ _ _ __ ___  _  ___| |_ __  
    | |  | | | | | '_ \ / _` | '_ ` _ | |/ __| | '_ \ 
    | |__| | |_| | | | | (_| | | | | | | | (__| | |_) |
    |_____/ __, |_| |_|__,_|_| |_| |_|_|___|_| .__/ 
             __/ |                              | |    
            |___/                               |_|    
     :: Dynamic Thread Pool :: 

特性

  • 参考美团线程池实践 ,对线程池参数动态化管理,增加监控、报警功能
  • 基于Spring框架,现只支持SpringBoot项目使用,轻量级,引入starter即可食用
  • 基于配置中心实现线程池参数动态调整,实时生效;集成主流配置中心,默认支持Nacos、Apollo,同时也提供SPI接口可自定义扩展实现
  • 内置通知报警功能,提供多种报警维度(配置变更通知、活性报警、容量阈值报警、拒绝策略触发报警),默认支持企业微信、钉钉报警,同时提供SPI接口可自定义扩展实现
  • 内置线程池指标采集功能,支持通过MicroMeter、JsonLog日志输出、Endpoint三种方式,可通过SPI接口自定义扩展实现

架构设计

主要分四大模块

  • 配置变更监听模块:

    1.监听特定配置中心的指定配置文件(默认实现Nacos、Apollo),可通过内部提供的SPI接口扩展其他实现

    2.解析配置文件内容,内置实现yml、properties配置文件的解析,可通过内部提供的SPI接口扩展其他实现

    3.通知线程池管理模块实现刷新

  • 线程池管理模块:

    1.服务启动时从配置中心拉取配置信息,生成线程池实例注册到内部线程池注册中心中

    2.监听模块监听到配置变更时,将变更信息传递给管理模块,实现线程池参数的刷新

    3.代码中通过getExecutor()方法根据线程池名称来获取线程池对象实例

  • 监控模块:

    实现监控指标采集以及输出,默认提供以下三种方式,也可通过内部提供的SPI接口扩展其他实现

    1.默认实现Json log输出到磁盘

    2.MicroMeter采集,引入MicroMeter相关依赖

    3.暴雷Endpoint端点,可通过http方式访问

  • 通知告警模块:

    对接办公平台,实现通告告警功能,默认实现钉钉、企微,可通过内部提供的SPI接口扩展其他实现,通知告警类型如下

    1.线程池参数变更通知

    2.阻塞队列容量达到设置阈值告警

    3.线程池活性达到设置阈值告警

    4.触发拒绝策略告警


使用

  • maven依赖

    <dependency>
         <groupId>io.github.lyh200</groupId>
         <artifactId>dynamic-tp-spring-cloud-starter</artifactId>
         <version>1.0.2-RELEASE</version>
    </dependency>
  • 线程池配置

    spring:
      dynamic:
        tp:
          enabled: true
          enabledBanner: true        # 是否开启banner打印,默认true
          enabledCollect: false      # 是否开启监控指标采集,默认false
          collectorType: logging     # 监控数据采集器类型(JsonLog | MicroMeter),默认logging
          logPath: /home/logs        # 监控日志数据路径,默认${user.home}/logs
          monitorInterval: 5         # 监控时间间隔(报警判断、指标采集),默认5s
          nacos:                     # nacos配置,不配置有默认值(规则name-dev.yml这样)
            dataId: dynamic-tp-demo-dev.yml
            group: DEFAULT_GROUP
          apollo:                    # apollo配置,不配置默认拿apollo配置第一个namespace
            namespace: dynamic-tp-demo-dev.yml
          configType: yml            # 配置文件类型
          platforms:                 # 通知报警平台配置
            - platform: wechat
              urlKey: 3a7500-1287-4bd-a798-c5c3d8b69c  # 替换
              receivers: test1,test2                   # 接受人企微名称
            - platform: ding
              urlKey: f80dad441fcd655438f4a08dcd6a     # 替换
              secret: SECb5441fa6f375d5b9d21           # 替换,非sign模式可以没有此值
              receivers: 15810119805                   # 钉钉账号手机号          
          executors:                                   # 动态线程池配置
            - threadPoolName: dynamic-tp-test-1
              corePoolSize: 6
              maximumPoolSize: 8
              queueCapacity: 200
              queueType: VariableLinkedBlockingQueue   # 任务队列,查看源码QueueTypeEnum枚举类
              rejectedHandlerType: CallerRunsPolicy    # 拒绝策略,查看RejectedTypeEnum枚举类
              keepAliveTime: 50
              allowCoreThreadTimeOut: false
              threadNamePrefix: test           # 线程名前缀
              notifyItems:                     # 报警项,不配置自动会配置(变更通知、容量报警、活性报警、拒绝报警)
                - type: capacity               # 报警项类型,查看源码 NotifyTypeEnum枚举类
                  enabled: true
                  threshold: 80                # 报警阈值
                  platforms: [ding,wechat]     # 可选配置,不配置默认拿上层platforms配置的所以平台
                  interval: 120                # 报警间隔(单位:s)
                - type: change
                  enabled: true
                - type: liveness
                  enabled: true
                  threshold: 80
                - type: reject
                  enabled: true
                  threshold: 1
  • 代码方式生成,服务启动会自动注册

    @Configuration
    public class DtpConfig {
    
       @Bean
       public DtpExecutor demo1Executor() {
           return DtpCreator.createDynamicFast("demo1-executor");
      }
    
       @Bean
       public ThreadPoolExecutor demo2Executor() {
           return ThreadPoolBuilder.newBuilder()
                  .threadPoolName("demo2-executor")
                  .corePoolSize(8)
                  .maximumPoolSize(16)
                  .keepAliveTime(50)
                  .allowCoreThreadTimeOut(true)
                  .workQueue(QueueTypeEnum.SYNCHRONOUS_QUEUE.getName(), null, false)
                  .rejectedExecutionHandler(RejectedTypeEnum.CALLER_RUNS_POLICY.getName())
                  .buildDynamic();
      }
    }
  • 代码调用,根据线程池名称获取

    public static void main(String[] args) {
           DtpExecutor dtpExecutor = DtpRegistry.getExecutor("dynamic-tp-test-1");
           dtpExecutor.execute(() -> System.out.println("test"));
    }

注意事项

  1. 配置文件配置的参数会覆盖通过代码生成方式配置的参数
  2. 阻塞队列只有VariableLinkedBlockingQueue类型可以修改capacity,该类型功能和LinkedBlockingQueue相似,只是capacity不是final类型,可以修改,
    VariableLinkedBlockingQueue参考RabbitMq的实现
  3. 启动看到如下日志输出证明接入成功

    
    |  __ \                            (_) |__   __|   
    | |  | |_   _ _ __   __ _ _ __ ___  _  ___| |_ __  
    | |  | | | | | '_ \ / _` | '_ ` _ | |/ __| | '_ \ 
    | |__| | |_| | | | | (_| | | | | | | | (__| | |_) |
    |_____/ __, |_| |_|__,_|_| |_| |_|_|___|_| .__/ 
             __/ |                              | |    
            |___/                               |_|    
     :: Dynamic Thread Pool :: 
    
    DynamicTp register, executor: DtpMainPropWrapper(dtpName=dynamic-tp-test-1, corePoolSize=6, maxPoolSize=8, keepAliveTime=50, queueType=VariableLinkedBlockingQueue, queueCapacity=200, rejectType=RejectedCountableCallerRunsPolicy, allowCoreThreadTimeOut=false)
  4. 配置变更会推送通知消息,且会高亮变更的字段

    
    DynamicTp [dynamic-tp-test-1] refresh end, changed keys: [corePoolSize, queueCapacity], corePoolSize: [6 => 4], maxPoolSize: [8 => 8], queueType: [VariableLinkedBlockingQueue => VariableLinkedBlockingQueue], queueCapacity: [200 => 2000], keepAliveTime: [50s => 50s], rejectedType: [CallerRunsPolicy => CallerRunsPolicy], allowsCoreThreadTimeOut: [false => false]

通知报警

触发报警阈值会推送相应报警消息(活性、容量、拒绝),且会高亮显示相应字段

配置变更会推送通知消息,且会高亮变更的字段


监控日志

通过collectType属性配置监控指标采集类型,默认 logging

  • MicroMeter:通过引入相关MicroMeter依赖采集到相应的平台
    (如Prometheus,InfluxDb...)
  • Logging:定时采集指标数据以Json日志格式输出磁盘,地址${logPath}/dy
    namictp/${appName}.monitor.log

    2022-01-11 00:25:20.599 INFO [dtp-monitor-thread-1:d.m.log] {"activeCount":0,"queueSize":0,"largestPoolSize":0,"poolSize":0,"rejectHandlerName":"RejectedCountableCallerRunsPolicy","queueCapacity":1024,"fair":false,"rejectCount":0,"waitTaskCount":0,"taskCount":0,"queueRemainingCapacity":1024,"corePoolSize":6,"queueType":"VariableLinkedBlockingQueue","completedTaskCount":0,"dtpName":"remoting-call","maximumPoolSize":8}
    2022-01-11 00:25:25.603 INFO [dtp-monitor-thread-1:d.m.log] {"activeCount":0,"queueSize":0,"largestPoolSize":0,"poolSize":0,"rejectHandlerName":"RejectedCountableCallerRunsPolicy","queueCapacity":1024,"fair":false,"rejectCount":0,"waitTaskCount":0,"taskCount":0,"queueRemainingCapacity":1024,"corePoolSize":6,"queueType":"VariableLinkedBlockingQueue","completedTaskCount":0,"dtpName":"remoting-call","maximumPoolSize":8}
    2022-01-11 00:25:30.609 INFO [dtp-monitor-thread-1:d.m.log] {"activeCount":0,"queueSize":0,"largestPoolSize":0,"poolSize":0,"rejectHandlerName":"RejectedCountableCallerRunsPolicy","queueCapacity":1024,"fair":false,"rejectCount":0,"waitTaskCount":0,"taskCount":0,"queueRemainingCapacity":1024,"corePoolSize":6,"queueType":"VariableLinkedBlockingQueue","completedTaskCount":0,"dtpName":"remoting-call","maximumPoolSize":8}
    2022-01-11 00:25:35.613 INFO [dtp-monitor-thread-1:d.m.log] {"activeCount":0,"queueSize":0,"largestPoolSize":0,"poolSize":0,"rejectHandlerName":"RejectedCountableCallerRunsPolicy","queueCapacity":1024,"fair":false,"rejectCount":0,"waitTaskCount":0,"taskCount":0,"queueRemainingCapacity":1024,"corePoolSize":6,"queueType":"VariableLinkedBlockingQueue","completedTaskCount":0,"dtpName":"remoting-call","maximumPoolSize":8}
    2022-01-11 00:25:40.616 INFO [dtp-monitor-thread-1:d.m.log] {"activeCount":0,"queueSize":0,"largestPoolSize":0,"poolSize":0,"rejectHandlerName":"RejectedCountableCallerRunsPolicy","queueCapacity":1024,"fair":false,"rejectCount":0,"waitTaskCount":0,"taskCount":0,"queueRemainingCapacity":1024,"corePoolSize":6,"queueType":"VariableLinkedBlockingQueue","completedTaskCount":0,"dtpName":"remoting-call","maximumPoolSize":8}
  • 暴露EndPoint端点(dynamic-tp),可以通过http方式请求

    [
        {
            "dtp_name": "remoting-call",
            "core_pool_size": 6,
            "maximum_pool_size": 12,
            "queue_type": "SynchronousQueue",
            "queue_capacity": 0,
            "queue_size": 0,
            "fair": false,
            "queue_remaining_capacity": 0,
            "active_count": 0,
            "task_count": 21760,
            "completed_task_count": 21760,
            "largest_pool_size": 12,
            "pool_size": 6,
            "wait_task_count": 0,
            "reject_count": 124662,
            "reject_handler_name": "CallerRunsPolicy"
        },
        {
            "max_memory": "228 MB",
            "total_memory": "147 MB",
            "free_memory": "44.07 MB",
            "usable_memory": "125.07 MB"
        }
    ]

项目地址

gitee地址: https://gitee.com/yanhom/dynamic-tp

github地址https://github.com/lyh200/dynamic-tp-spring-cloud-starter


联系作者

对项目有什么想法或者建议,可以在上述地址中加到作者微信进行交流,或者创建issues,一起完善项目!

最后,支持的话还望大家去点个star哦。

❌