注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

元宇宙讨论

元宇宙讨论

元宇宙到底是什么?来畅所欲言
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

开源框架 Egg.js 文档未经授权被转载,原作者反成“恶人”在 v2ex 上被讨伐

5 月 26 日,Egg.js(阿里开源的企业级 Node.js 框架)核心开发者 @“天猪”在知乎发了一篇题为《关于我个人“恶意投诉”别人未授权转载事件的说明》的声明,对近期自己反成“恶人”在 v2ex 上被“讨伐”的事情表示困惑。开发者原文转载 MIT L...
继续阅读 »

5 月 26 日,Egg.js(阿里开源的企业级 Node.js 框架)核心开发者 @“天猪”在知乎发了一篇题为《关于我个人“恶意投诉”别人未授权转载事件的说明》的声明,对近期自己反成“恶人”在 v2ex 上被“讨伐”的事情表示困惑。


开发者原文转载 MIT License 协议文档

被知乎告侵权


原来在很多年前,@天猪 写了一篇关于 Egg.js 某个开源项目的某个特性的使用文档,并于 2018 年将该文档发布到了 2 个地方 —— Egg.js 知乎专栏(文档 A)和 Egg.js 的 GitHub repo 文档库(文档 B)。

其中,文档 A 的版权已授权给知乎,而发布在 GitHub 上的文档 B 则采用了 MIT License 协议。

文档地址:https://zhuanlan.zhihu.com/p/35334932

值得注意的是,发布到这两个地方的内容(文档 A 和文档 B )大部分是重合的。

2019 年,开发者 @an168bang521 在未告知原作者@“天猪”的前提下,从 GitHub 将 Egg.js(文档 B)原文转载到了其个人网站上。(现文章已删除)

地址:https://www.axihe.com/edu/egg/tutorials/typescript.html


但由于 Egg.js 文档(文档 B)使用的是 MIT License 协议,即“允许任何人在 MIT 协议下进行使用和操作”,因此开发者 @an168bang521 原封不动转载 该文档就引发了争议。

Eggjs 使用的 MIT LICENS 链接: https://github.com/eggjs/egg/blob/master/LICENSE

随后,开发者 @an168bang521 搬运自文档 B(采用了 MIT License)的个人网页收到来自知乎的 “侵权告知函” 。

因此,这位开发者 @an168bang521 才终于想起了 Egg.js 文档(文档 B)的原作者,并在知乎平台上发私信给@“天猪”。
 

未经授权被转载

Egg.js 文档原作者反成恶人

在 v2ex 上被“讨伐”


也就在发布这篇声明的前一天晚上,@“天猪” 刚刚收到了这位开发者 @an168bang521 的私信邮件。

在邮件中,该开发者称自己因在 2019 年摘抄了原作者 @“天猪” 的一篇“开源软件 Egg.js 在 GitHub 的技术文档”而被知乎告知侵权,且收到了知乎委托的公司发送的 “侵权告知函”。

开发者 @an168bang521 表示,因为文档 B 使用的是 MIT License 协议,因此自己“大段使用该仓库内的文档,是属于 MIT 里的使用、复制、修改、合并、发布、分发、再许可或出售”。

对此,Egg.js 核心维护者@“天猪”回应称,这因为他们在知乎的专栏(文档 A)已授权给平台的版权服务,(但由于文档 A 和 文档 B 的内容大部分重合)因此当知乎平台检测到对应的文章被未授权转载时,就会自动发送侵权通知。

让人意外的是,在收到该邮件的第二天,就在@“天猪”莫名其妙且感到困惑的时候,该开发者 @an168bang521 已经将该事件的帖子发布在了 v2ex 上,且遭到了来自评论区一堆回复者的“讨伐”。

事情发展到这里,作者@“天猪”才发现:自己的开源 Egg.js 技术文档未经授权被转载,现在自己反而被迫变成了“恶人和小丑”?

随后,@“天猪”开始重视该事件,且正式着手研究关于“基于MIT 协议的开源框架文档未授权被转载”的法律相关事宜。

目前,@“天猪”在该声明中已经附上了自己的“诉求”——“唯一的要求就是:事先跟我打招呼获取授权,注明原文出处,不要破坏文章结构以及加太多广告。”

@“天猪”表示,关于文档站的三方发布问题,自己的观点跟去年 Vue @尤雨溪的一样 —— 关于文档,协议和版权,主要期望的是:及时同步 + 注明非官方 + 出处 + 不要破坏文章结构以及加太多广告。

最后,@“天猪”也强调,正“因为我们都热爱开源,所以基本上都默认 MIT ,真的要用,我们也似乎没有太多的办法,如果三方有过度的行为,也只能倒逼我后续的开源项目都会重新考虑开源协议。”

关于该事件的后续发展,本站 Segmentfault 编辑部也将持续关注,如果您对该事件有相关看法,也欢迎在评论区留言互动。

参考链接:https://zhuanlan.zhihu.com/p/520119900
收起阅读 »

<版本>Android统一依赖管理

总结: 在多module的项目中,对版本的统一管理很重要,可以避免多个版本库的冲突问题,也方便日后的统一升级等等 Android的版本依赖的统一管理,有三种方式: 传统apply from的方式 buildsrc方式 composing builds方式 ...
继续阅读 »

总结:


在多module的项目中,对版本的统一管理很重要,可以避免多个版本库的冲突问题,也方便日后的统一升级等等


Android的版本依赖的统一管理,有三种方式:



  • 传统apply from的方式

  • buildsrc方式

  • composing builds方式


一、传统apply from的方式


在根目录新建一个config.gradle(或其他随意的xxx.gradle)文件
或在根目录的build.gradle定义一些变量
如:


ext {
android = [
compileSdkVersion: 30,
buildToolsVersion: "30",
minSdkVersion : 16,
targetSdkVersion : 28,
versionCode : 100,
versionName : "1.0.0"
]

versions = [
appcompatVersion : "1.1.0",
coreKtxVersion : "1.2.0",
supportLibraryVersion : "28.0.0",
glideVersion : "4.11.0",
okhttpVersion : "3.11.0",
retrofitVersion : "2.3.0",
constraintLayoutVersion: "1.1.3",
gsonVersion : "2.8",
//等等······
]

dependencies = [
//base
"constraintLayout" : "androidx.constraintlayout:constraintlayout:${version["constraintLayoutVersion"]}",
"appcompat" : "androidx.appcompat:appcompat:${version["appcompatVersion"]}",
"coreKtx" : "androidx.core:core-ktx:${version["coreKtxVersion"]}",
//等等······
]
}

在工程的根目录build.gradle添加:


apply from"config.gradle"

在需要依赖的modulebuild.gradle中,依赖的方式如下:


dependencies {
...
// 添加appcompatVersion依赖
api rootProject.ext.dependencies["appcompatVersion"]
...
}

【缺点】



  • 无法跟踪代码,需要手动搜索相关的依赖

  • 可读性很差


二、buildsrc方式


什么是buildsrc


当运行 gradle 时会检查项目中是否存在一个名为 buildsrc 的目录。然后 gradle 会自动编译并测试这段代码,并将其放入构建脚本的类路径中。


对于多项目构建,只能有一个 buildsrc 目录,该目录必须位于根项目目录中, buildsrc 是 gradle 项目根目录下的一个目录,它可以包含我们的构建逻辑。


与脚本插件相比,buildsrc 应该是首选,因为它更易于维护、重构和测试代码。


优缺点:


1】优点:



  • buildSrc是Android默认插件,共享 buildsrc 库工件的引用,全局只有这一个地方可以修改

  • 支持自动补全,支持跳转。


2】缺点:



  • 依赖更新将重新构建整个项目,项目越大,重新构建的时间就越长,造成不必要的时间浪费。


Gradle 文档



A change in buildSrc causes the whole project to become out-of-date. Thus, when making small incremental changes, the --no-rebuild command-line option is often helpful to get faster feedback. Remember to run a full build regularly or at least when you’re done, though.


buildSrc的更改会导致整个项目过时,因此,在进行小的增量更改时,-- --no-rebuild命令行选项通常有助于获得更快的反馈。不过,请记住要定期或至少在完成后运行完整版本。



使用方式:


参考:Kotlin + buildSrc for Better Gradle Dependency Management




  • 在项目根目录下新建一个名为 buildSrc 的文件夹(名字必须是 buildSrc,因为运行 Gradle 时会检查项目中是否存在一个名为 buildSrc 的目录)




  • 在 buildSrc 文件夹里创建名为 build.gradle.kts 的文件,添加以下内容




plugins {
`kotlin-dsl`
}
repositories{
jcenter()
}


  • 在 buildSrc/src/main/java/包名/ 目录下新建 Deps.kt 文件,添加以下内容


object Versions {
......

val appcompat = "1.1.0"

......
}

object Deps {
......

val appcompat = "androidx.appcompat:appcompat:${Versions.appcompat}"

......
}


  • 重启 Android Studio,项目里就会多出一个名为 buildSrc 的 module,实现效果


示意图


三、composing builds方式


摘自 Gradle 文档:复合构建只是包含其他构建的构建. 在许多方面,复合构建类似于 Gradle 多项目构建,不同之处在于,它包括完整的 builds ,而不是包含单个 projects



  • 组合通常独立开发的构建,例如,在应用程序使用的库中尝试错误修复时

  • 将大型的多项目构建分解为更小,更孤立的块,可以根据需要独立或一起工作


优缺点:


1】优点:



  • 支持单向跟踪

  • 自动补全

  • 依赖更新时,不会重新构建整个项目


2】缺点:



  • 需要在每一个module中都添加相应的插件引用


使用方式:


参考Gradle文档



  • 新建的 module 名称 VersionPlugin(名字随意)

  • 在 versionPlugin 文件夹下的 build.gradle 文件内,添加以下内容


buildscript {
repositories {
jcenter()
}
dependencies {
// 因为使用的 Kotlin 需要需要添加 Kotlin 插件
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10"
}
}

apply plugin: 'kotlin'
apply plugin: 'java-gradle-plugin'
repositories {
// 需要添加 jcenter 否则会提示找不到 gradlePlugin
jcenter()
}

gradlePlugin {
plugins {
version {
// 在 app 模块需要通过 id 引用这个插件
id = 'com.yu.plugin'
// 实现这个插件的类的路径
implementationClass = 'com.yu.versionplugin.VersionPlugin'
}
}
}


  • 在 VersionPlugin/src/main/java/包名/ 目录下新建 DependencyManager.kt 文件,添加相关的依赖配置,如:


package com.yu.versionplugin

/**
* 配置和 build相关的
*/
object BuildVersion {
const val compileSdkVersion = 29
const val buildToolsVersion = "29.0.2"
const val minSdkVersion = 17
const val targetSdkVersion = 26
const val versionCode = 102
const val versionName = "1.0.2"
}

/**
* 项目相关配置
*/
object BuildConfig {
//AndroidX
const val appcompat = "androidx.appcompat:appcompat:1.2.0"
const val constraintLayout = "androidx.constraintlayout:constraintlayout:2.0.4"
const val coreKtx = "androidx.core:core-ktx:1.3.2"
const val material = "com.google.android.material:material:1.2.1"
const val junittest = "androidx.test.ext:junit:1.1.2"
const val swiperefreshlayout = "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
const val recyclerview = "androidx.recyclerview:recyclerview:1.1.0"
const val cardview = "androidx.cardview:cardview:1.0.0"

//Depend
const val junit = "junit:junit:4.12"
const val espresso_core = "com.android.support.test.espresso:espresso-core:3.0.2"
const val guava = "com.google.guava:guava:24.1-jre"
const val commons = "org.apache.commons:commons-lang3:3.6"
const val zxing = "com.google.zxing:core:3.3.2"

//leakcanary
const val leakcanary = "com.squareup.leakcanary:leakcanary-android:2.4"

//jetPack
const val room_runtime = "androidx.room:room-runtime:2.2.5"
const val room_compiler = "androidx.room:room-compiler:2.2.5"
const val room_rxjava2 = "androidx.room:room-rxjava2:2.2.5"
const val lifecycle_extensions = "android.arch.lifecycle:extensions:1.1.1"
const val lifecycle_compiler = "android.arch.lifecycle:compiler:1.1.1"
const val rxlifecycle = "com.trello.rxlifecycle3:rxlifecycle:3.1.0"
const val rxlifecycle_components = "com.trello.rxlifecycle3:rxlifecycle-components:3.1.0"

//Kotlin
const val kotlinx_coroutines_core = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7"
//...
}


  • 在 VersionPlugin/src/main/java/包名/ 目录下新建 VersionPlugin.kt,实现Plugin接口,如下:


package com.yu.versionplugin

import org.gradle.api.Plugin
import org.gradle.api.Project

class VersionPlugin : Plugin<Project>{
override fun apply(p0: Project) {

}

companion object{

}
}

项目目录结构



  • settings.gradle 文件内添加如下代码,并重启 Android Studio


//注意是 includeBuild
includeBuild 'VersionPlugin'


  • app 模块 build.gradle 文件内 首行 添加以下内容


plugins{
// 这个 id 就是在 VersionPlugin 文件夹下 build.gradle 文件内定义的 id
id "com.yu.plugin"
}
// 定义的依赖地址
import com.yu.versionplugin.*


  • 使用如下:


import com.yu.versionplugin.*

plugins {
id 'com.android.application'
id 'kotlin-android'
id 'com.yu.plugin'
}

android {
compileSdk 32

defaultConfig {
applicationId "com.yu.versiontest"
minSdk BuildVersion.minSdkVersion
targetSdk BuildVersion.targetSdkVersion
versionCode BuildVersion.versionCode
versionName BuildVersion.versionName
}
//.....
}

dependencies {

implementation BuildConfig.coreKtx
implementation BuildConfig.appcompat
implementation BuildConfig.material
//......
}

作者:玉圣
链接:https://juejin.cn/post/7097431328441761800
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

如何解决Flutter的WebView白屏和视频自动播放

前言 众所周知,Flutter 的 WebView 不太友好,用起来不顺手。 我们 Flutter 开发常用的 WebView 库有2个,一个是 Flutter 官方自己出的 webview_flutter ,另一个是比较流行的 flutter_inappwe...
继续阅读 »

前言


众所周知,Flutter 的 WebView 不太友好,用起来不顺手。
我们 Flutter 开发常用的 WebView 库有2个,一个是 Flutter 官方自己出的 webview_flutter ,另一个是比较流行的 flutter_inappwebview 。这两个库其实差不多,flutter_inappwebview 功能比较丰富,封装了很多事件、方法等,但是很多问题这两个库都会遇到。本文以 webview_flutter 为基础库展开讲解相关问题以及解决方案。


问题


白屏、UI错乱




如上图所示



  • 测试的时候发现部分手机(如OPPO)会出现白屏现象(左图)

  • 原生与 Flutter 混编,打开页面会发现页面布局变了,顶部banner变小了(右图)


查阅网上的一些解决方案,千篇一律都是:


if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView();

但是,这样设置其实是不对的,还会出现以上问题,真正的解决方案是:


if (Platform.isAndroid) WebView.platform = AndroidWebView();

视频自动播放


由于需求需要,打开页面的时候,列表的第一个视频(YouTube/Facebook 视频)需要自动播放。
但是发现没法自动播放,如下图,会出现播放之后马上暂停的现象。



查阅资料得知,是谷歌浏览器的隐私政策导致的。



所以要想视频自动播放,有两种方案:



  • 静音播放。

    • 在 Web 端调用视频播放器的静音即可自动播放。



  • 模拟点击。

    • 给 WebView 设置一个 GlobalKey 。
      WebView(
      key: logic.state.videoGlobalKey,
      ......
      );
      }


    • 然后在 WebView 的 onPageFinished 方法里,通过 GlobalKey 获取 WebView 的位置,从而进行模拟点击,就可以自动播放视频了。
      var currentContext = state.videoGlobalKey.currentContext;
      var offset = (currentContext?.findRenderObject() as RenderBox)
      .localToGlobal(Offset.zero);
      //模拟点击
      var addPointer = PointerAddedEvent(
      pointer: 0,
      position: Offset(
      offset.dx + 92.w,
      offset.dy + 92.w),
      );
      var downPointer = PointerDownEvent(
      pointer: 0,
      position: Offset(
      offset.dx + 92.w,
      offset.dy + 92.w),
      );
      var upPointer = PointerUpEvent(
      pointer: 0,
      position: Offset(
      offset.dx + 92.w,
      offset.dy + 92.w),
      );
      GestureBinding.instance!.handlePointerEvent(addPointer);
      GestureBinding.instance!.handlePointerEvent(downPointer);
      GestureBinding.instance!.handlePointerEvent(upPointer);





这两种方案各有利弊,方案一无法播放声音(需要用户手动点击开启声音),方案二偶尔会有误触的操作。我们 APP 通过与产品商量最终选取的是方案一的解决方案。


另外 iOS 端自动播放会自动全屏,需要设置以下属性:


WebView(
key: logic.state.videoGlobalKey,
// 允许在线播放(解决iOS播放视频自动全屏)
allowsInlineMediaPlayback: true,
......
);
}

作者:未央歌
链接:https://juejin.cn/post/7102256787117572132
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

台积电多人离职:老婆受不了

作为全球晶圆代工的一哥,台积电这几年成了香饽饽,营收及盈利也大涨,员工待遇也是业内最高的,今年4月份还要全员加薪8%,然而高薪诱惑下还有大量员工离职,有员工爆料称这样的工作方式已经影响家庭和谐。有网友在论坛上发帖请教台积电内部员工,称自己的同学之前离开了台积电...
继续阅读 »

作为全球晶圆代工的一哥,台积电这几年成了香饽饽,营收及盈利也大涨,员工待遇也是业内最高的,今年4月份还要全员加薪8%,然而高薪诱惑下还有大量员工离职,有员工爆料称这样的工作方式已经影响家庭和谐。

有网友在论坛上发帖请教台积电内部员工,称自己的同学之前离开了台积电,现在看到加薪8%之后又有些后悔,想知道大家为什么在台积电有这么丰厚的收入还要离职呢?


很快这个帖子就变成了吐槽大会,台积电内部员工抱怨起台积电的工作制度了,直言钱已经不重要了。

11、12点下班隔天6点跟国外开会,我朋友姐夫是这样,老婆受不了。

生活品质很差,不然为何老外做不起来?

有钱没生活,当过半导体制造厂工程师就懂

新同事硕士毕业去2年肝指数上升很多

完全无法理解只有工作跟睡觉的生活

用命换的你是能待多久

里面的想出来,外面的想进去,离职时到七厂人资办理,真的是要排队

要不要去台积电就是钱/生活的选择而已

因为台积电实际上跟外商比起来给的不算高薪又血汗。

3月初有报道称,今年预计招募超过8000名新员工,其中,硕士毕业工程师平均年薪上看200万新台币,约合人民币45万元。


今年2月份,台积电董事会批准了2021年薪酬奖励,去年员工业绩奖金与酬劳(分红)合计712亿290万元,其中员工业绩奖金约新台币356亿145万元已于每季季后发放,而酬劳(分红)约新台币356亿145万元将于今年七月发放。另外,搜索公众号Linux就该这样学后台回复“git书籍”,获取一份惊喜礼包。

台积电去年的总奖励相比2020年增长了2.4%,按照5.7万员工总数来看,人均奖励约为124万新台币,约合28.2万元人民币。

网友表示:“用钱换命,可以啊!多少人命在消耗,钱没进口袋。”

你还有什么想要补充的吗?

版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!

收起阅读 »

恶意技术时代下的负责任技术

从数百万美元的勒索软件赔款,到数亿用户私人信息的数据泄露,围绕恶意技术的头条新闻一直很吸引眼球,但它们并不能说明全部问题。真实情况是,恶意技术远不止蓄意的、有针对性的破坏系统或窃取用户数据。尽管黑客攻击、勒索软件攻击、数据泄露和 DDoS 攻击在这类新闻叙述中...
继续阅读 »

从数百万美元的勒索软件赔款,到数亿用户私人信息的数据泄露,围绕恶意技术的头条新闻一直很吸引眼球,但它们并不能说明全部问题。

真实情况是,恶意技术远不止蓄意的、有针对性的破坏系统或窃取用户数据。尽管黑客攻击、勒索软件攻击、数据泄露和 DDoS 攻击在这类新闻叙述中占据着主导地位,也的确造成了严重损害,但实际上它们只是沧海一粟。

认识所有形式的恶意技术行为

首先要摆正心态,不要对 "恶意技术 "一词感到过于恐慌。这里谈论的不仅是技术攻击本身,我们需要更广泛地思考这一问题。

恶意技术并不单纯指非法行为,我们甚至不是在谈论那些必然是恶意的事情。例如,有些人完全乐于接受在线监控,因为这可以帮助他们获得更精准、更具个性化的推荐。与此同时也有另外一些人会不遗余力地躲避任何数字监控的追踪,因为这在他们看来是不道德的。

有些技术看似恶意,其出发点却并不如此。例如,图像识别软件的开发者不会刻意让软件在识别到黑人妇女面孔时提供不一致的结果,只是由于软件现阶段有一个糟糕的数据集,才会呈现出看似有偏见的结果,而这些并非开发者的本意。这些都不是恶意的项目,当然也不归属于恶意的品牌。在许多情况下,它们甚至不是设计或规划上的失败,而是团队没有充分考虑到一项技术决定会对所有潜在的利益相关者群体产生怎样不同的影响。

这是一个关键点。在设计之初,软件通常有一个特定的利益相关者群体,设计者试图直接服务或满足这一群体的需求,却常常忽视了该产品对其他利益相关者的影响。比如训练一个自然语言处理模型所产生的二氧化碳排放足迹,与纽约和北京之间往返125次航班相同——很少有人考虑到这种环境上的影响。除此之外还有很多人忽视的公平、公正问题。例如,疫情期间很多教育转入线上,一些家庭没有良好的网络情况,根本无法同时支撑几个孩子共同参与线上课程,更不用说父母也要同时在家办公。许多人看到的是数字教育的精彩革命,但他们没有看到那些因数字不平等而被落下的人。

为什么现在是拥抱道德技术的正确时机?

疫情所暴露的数字不平等和正在发生的气候危机只是原因之二。现如今,技术几乎深深扎根于我们生活的各个方面——医疗决定、信贷决定、缓刑或判刑决定,所有这些对个人生活有巨大影响的事情,都受到技术选择的巨大影响。这其中的利害关系是非常真实的,而技术决策者在做这些决定时却很少做到全面考虑。这就是为什么对各种组织来说,树立负责任的技术思维是如此重要。

负责任技术的定义

负责任技术——有时也被称为道德技术或公平技术——是一个总括性的术语,包含了多种概念,总之就是用技术做正确的事情——这可能意味着任何事情,从采取措施使更容易获得一个应用程序,到实施政策以帮助持续提供公平的技术体验。

从字面上看,这是一个相对简单的概念,但仍然被误解所笼罩。我最近读到一些信息,说“责任”在土木工程等领域很容易定义,你的责任是确保建筑物稳定,不倒塌,不对相关居民的生活产生负面影响。其隐藏的含义是,在软件或技术领域,这一点很难定义。

是的,我们并不像医疗行业那样受到任何形式的“希波克拉底誓言”的约束。但是,当涉及到用技术做出道德以及负责任的决定时,我们常常给自己留了过多的余地。

我们可以采取哪些措施来变得更加负责?

为了减少技术所附带的无意伤害并做出负责任的决定,今天的企业需要着重关注的是,识别可能受到特定技术决定影响的潜在利益相关者。这意味着需要关注:

  • 受测群体——他们是否真正代表了预期中将使用该产品的终端用户群体?这个过程是否涵盖了所有利益相关群体,他们是否有发声渠道?

  • 服务所需数据集的质量和准确性——是否没有偏见,是否能够提供真实、全面的反馈和包容性体验?

  • 设计中是否考虑到了平等和易用性——以及复杂的特征或功能是否以整体可用性和可及性为代价?

  • 这一决策是否会对人类社会产生更广义的负面影响?例如,它是否与我们的可持续发展目标相一致,是否有可能破坏环境?

我希望鼓励组织做的另一件事是,明确声明组织关心什么,以及希望所采用的技术能帮助实现什么。正如《数学毁灭武器》的作者Cathy O'Neil所说,有些时候,你必须在公平和利润之间进行交易。

这取决于你想在这个光谱上处于什么位置,但重要的是要明确目标和意图。我曾与一个组织合作,该组织制定了一个框架,表达了他们在使用客户数据方面的价值观和原则,清楚地阐述了他们打算如何操作这些数据,以及为什么会做出这个决定。这花了几个月的时间,但这使他们的意图和道德立场十分清晰,并且容易保持一致。

减轻技术的无意识伤害

技术所产生的无意识伤害有多种形式,可能潜伏在任何技术决策中。作为技术管理者,我们有责任提出正确的问题,并考虑这些技术将如何被每个人使用,以及可能对使用者的生活和经历产生怎样的影响。

转向这种负责任的方法和心态在理论上相对简单,但在看到有意义的结果之前,整个行业的组织和专业人士需要作出真正的奉献。正如我们有责任保护客户数据免受恶意威胁一样,我们也有道德责任尽我们所能减少技术所产生的负面影响,并为所有人建立一个平等、可访问的数字世界。

来源: Thoughtworks洞见

收起阅读 »

IE浏览器6月退役 微软力推Edge:渲染时间减少85%、CPU内存占用大降

前不见微软宣布IE浏览器6月退役,这个始于Win95时代的浏览器在27年后还是要被放弃了,微软现在的重点放在了Edge浏览器上,不仅技术更新,而且性能更好,CPU及内存占用大幅下降。在日前的微软Build 2022大会上,微软介绍了Edge浏览器的进展,很快就...
继续阅读 »

前不见微软宣布IE浏览器6月退役,这个始于Win95时代的浏览器在27年后还是要被放弃了,微软现在的重点放在了Edge浏览器上,不仅技术更新,而且性能更好,CPU及内存占用大幅下降。

在日前的微软Build 2022大会上,微软介绍了Edge浏览器的进展,很快就会升级WebView2,届时Edge浏览器性能会得到更好的提升。

IE浏览器6月退役 微软力推Edge:渲染时间减少85%、CPU内存占用大降

微软也公布了对比结果,与使用Internet Explorer运行他们的解决方案相比,使用 Edge WebView2,可以将渲染时间减少了85%,CPU占用率降低33%,内存占用率也会降低32%,改善明显。

4月初,根据分析机构StatCounter的数据,Edge成功实现了对Safari的反超。

截止2022年3月,Edge获得了9.65%的市场份额,超过Safari的9.56%成功夺得第二名,但与Chrome堪称恐怖的67.29%相比,依旧存在明显的差距。

IE浏览器6月退役 微软力推Edge:渲染时间减少85%、CPU内存占用大降

来源:tech.ifeng.com/c/8GKXHf1ZEWO

收起阅读 »

微前端框架 qiankun 技术分析

如何加载子应用single-spa 通过 js entry 的形式来加载子应用。而 qiankun 采用了 html entry 的形式。这两种方式的优缺点我们在理解微前端技术原理中已经做过分析,这里不再赘述,我们看看 qiankun 是如何实现 html e...
继续阅读 »

如何加载子应用

single-spa 通过 js entry 的形式来加载子应用。而 qiankun 采用了 html entry 的形式。这两种方式的优缺点我们在理解微前端技术原理中已经做过分析,这里不再赘述,我们看看 qiankun 是如何实现 html entry 的。

qiankun 提供了一个 API registerMicroApps 来注册子应用,其内部调用 single-spa 提供的 registerApplication 方法。在调用 registerApplication 之前,会调用内部的 loadApp 方法来加载子应用的资源,初始化子应用的配置。

通过阅读 loadApp 的代码,我们发现,qiankun 通过 import-html-entry 这个包来加载子应用。import-html-entry 的作用就是通过解析子应用的入口 html 文件,来获取子应用的 html 模板、css 样式和入口 JS 导出的生命周期函数。

import-html-entry

import-html-entry 是这样工作的,假设我们有如下 html entry 文件:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>test</title>
</head>
<body>

<!-- mark the entry script with entry attribute -->
<script src="https://unpkg.com/mobx@5.0.3/lib/mobx.umd.js" entry></script>
<script src="https://unpkg.com/react@16.4.2/umd/react.production.min.js"></script>
</body>
</html>

我们使用 import-html-entry 来解析这个 html 文件:

import importHTML from 'import-html-entry';

importHTML('./subApp/index.html')
.then(res => {
console.log(res.template);

res.execScripts().then(exports => {
const mobx = exports;
const { observable } = mobx;
observable({
name: 'kuitos'
})
})
});

importHTML 的返回值有如下几个属性:

  • template 处理后的 HTML 模板
  • assetPublicPath 静态资源的公共路径
  • getExternalScripts 获取所有外部脚本的函数,返回脚本路径
  • getExternalStyleSheets 获取所有外部样式的函数,返回样式文件的路径
  • execScripts 执行脚本的函数

在 importHTML 的返回值中,除了几个工具类的方法,最重要的就是 template 和 execScripts 了。

importHTML('./subApp/index.html') 的整个执行过程代码比较长,我们只讲一下大概的执行原理,感兴趣的同学可以自行查看importHTML 的源码

importHTML 首先会通过 fetch 函数请求具体的 html 内容,然后在 processTpl 函数 中通过一系列复杂的正则匹配,解析出 html 中的样式文件和 js 文件。

importHTML 函数返回值为 { template, scripts, entry, styles },分别是 html 模板,html 中的 js 文件(包含内嵌的代码和通过链接加载的代码),子应用的入口文件,html 中的样式文件(同样是包含内嵌的代码和通过链接加载的代码)。

之后通过 getEmbedHTML 函数 将所有使用外部链接加载的样式全部转化成内嵌到 html 中的样式。getEmbedHTML 返回的 html 就是 importHTML 函数最终返回的 template 内容。

现在,我们看看 execScripts 是怎么实现的。

execScripts 内部会调用 getExternalScripts 加载所有 js 代码的文本内容,然后通过 eval("code") 的形式执行加载的代码。

注意,execScripts 的函数签名是这样的 (sandbox?: object, strictGlobal?: boolean, execScriptsHooks?: ExecScriptsHooks): Promise<unknown>。允许我们传入一个沙箱对象,如果子应用按照微前端的规范打包,那么会在全局对象上设置 mountunmount 这几个生命周期函数属性。execScripts 在执行 eval("code") 的时候,会巧妙的把我们指定的沙箱最为全局对象包装到 "code" 中,子应用能够运行在沙盒环境中。

在执行完 eval("code") 以后,就可以从沙盒对象上获取子应用导出的生命周期函数了。

loadApp

现在我们把视线拉回 loadApp 中,loadApp 在获取到 templateexecScripts 这些信息以后,会基于 template 生成 render 函数用于渲染子应用的页面。之后会根据需要生成沙盒,并将沙盒对象传给 execScripts 来获取子应用导出的声明周期函数。

之后,在子应用生命周期函数的基础上,构建新的生命周期函数,再调用 single-spa 的 API 启动子应用。

在这些新的生命周期函数中,会在不同时机负责启动沙盒、渲染子应用、清理沙盒等事务。

隔离

在完成子应用的加载以后,作为一个微前端框架,要解决好子应用的隔离问题,主要要解决 JS 隔离和样式隔离这两方面的问题。

JS 隔离

qiankun 为根据浏览器的能力创建两种沙箱,在老旧浏览器中会创建快照模式 的浏览器中创建 VM 模式的沙箱 ProxySandbox

篇幅限制,我们只看 ProxySandbox 的实现,在其构造函数中,我们可以看到具体的逻辑:首先会根据用户指定的全局对象(默认是 window)创建一个 fakeWindow,之后在这个 fakeWindow 上创建一个 proxy 对象,在子应用中,这个 proxy 对象就是全局变量 window

constructor(name: string, globalContext = window) {
const { fakeWindow, propertiesWithGetter } = createFakeWindow(globalContext);
const proxy = new Proxy(fakeWindow, {
set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {},
get: (target: FakeWindow, p: PropertyKey): any => {},
has(target: FakeWindow, p: string | number | symbol): boolean {},

getOwnPropertyDescriptor(target: FakeWindow, p: string | number | symbol): PropertyDescriptor | undefined {},

ownKeys(target: FakeWindow): ArrayLike<string | symbol> {},

defineProperty(target: Window, p: PropertyKey, attributes: PropertyDescriptor): boolean {},

deleteProperty: (target: FakeWindow, p: string | number | symbol): boolean => {},

getPrototypeOf() {
return Reflect.getPrototypeOf(globalContext);
},
});
this.proxy = proxy;
}

其实 qiankun 中的沙箱分两个类型:

  • app 环境沙箱
    app 环境沙箱是指应用初始化过之后,应用会在什么样的上下文环境运行。每个应用的环境沙箱只会初始化一次,因为子应用只会触发一次 bootstrap 。子应用在切换时,实际上切换的是 app 环境沙箱。
  • render 沙箱
    子应用在 app mount 开始前生成好的的沙箱。每次子应用切换过后,render 沙箱都会重现初始化。

上面说的 ProxySandbox 其实是 render 沙箱。至于 app 环境沙箱,qiankun 目前只针对在应用 bootstrap 时动态创建样式链接、脚本链接等副作用打了补丁,保证子应用切换时这些副作用互不干扰。

之所以设计两层沙箱,是为了保证每个子应用切换回来之后,还能运行在应用 bootstrap 之后的环境下。

样式隔离

qiankun 提供了多种样式隔离方式,隔离效果最好的是 shadow dom,但是由于其存在诸多限制,qiankun 官方在将来的版本中将会弃用,转而推行 experimentalStyleIsolation 方案。

我们可以通过下面这段代码看到 experimentalStyleIsolation 方案的基本原理。

const styleNodes = appElement.querySelectorAll('style') || [];
forEach(styleNodes, (stylesheetElement: HTMLStyleElement) => {
css.process(appElement!, stylesheetElement, appInstanceId);
});

css.process 的核心逻辑,就是给读取到的子应用的样式添加带有子应用信息的前缀。效果如下:

/* 假设应用名是 react16 */
.app-main {
font-size: 14px;
}

div[data-qiankun-react16] .app-main {
font-size: 14px;
}

通过上面的隔离方法,基本可以保证子应用间的样式互不影响。

小结

qiankun 在 single-spa 的基础上根据实际的生产实践开发了很多有用的功能,大大降低了微前端的使用成本。

本文仅仅针对如何加载子应用和如何做好子应用间的隔离这两个问题,介绍了 qiankun 的实现。其实,在隔离这个问题上,qiankun 也仅仅是根据实际中会遇到的情况做了必要的隔离措施,并没有像 iframe 那样实现完全的隔离。我们可以说 qiankun 实现的隔离有缺陷,也可以说是 qiankun 在实际的业务需求和完全隔离的实现成本之间做的取舍。

原文:https://segmentfault.com/a/1190000041151414

收起阅读 »

pc端微信授权登录两种实现方式的总结

在开发pc端项目中,使用微信授权登录是种很常用的功能,目前在功能实现上有两种不同的方式,现根据两种方式做如下总结。一、跳转微信授权登录页面进行扫码授权这种方法实现非常简单只用跳转链接就可以实现微信授权登录window.location = https://op...
继续阅读 »

在开发pc端项目中,使用微信授权登录是种很常用的功能,目前在功能实现上有两种不同的方式,现根据两种方式做如下总结。

一、跳转微信授权登录页面进行扫码授权

这种方法实现非常简单只用跳转链接就可以实现微信授权登录

window.location = https://open.weixin.qq.com/connect/qrconnect?appid=${appid}&redirect_uri=${回调域名}/login&response_type=code&scope=snsapi_login&state=${自定义配置}#wechat_redirect

跳转之后进行微信扫码,之后微信会带着code,回调回你设置的回调域名,这之后拿到code再和后台进行交互,即可实现微信登陆。
这种方法相对来说实现起来非常简单,但是因为需要先跳转微信授权登录页面,在体验上来说可能不是太好。

二、在当前页面生成微信授权登录二维码

这种方法是需要引入wxLogin.js,动态生成微信登陆二维码,具体实现方法如下:

const s = document.createElement('script')
s.type = 'text/javascript'
s.src = 'https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js'
const wxElement = document.body.appendChild(s)
wxElement.onload = function () {
var obj = new WxLogin({
id: 'wx_login_id', // 需要显示的容器id
appid: '', // 公众号appid
scope: 'snsapi_login', // 网页默认即可
redirect_uri:'', // 授权成功后回调的url
state: '', // 可设置为简单的随机数加session用来校验
style: 'black', // 提供"black"、"white"可选。二维码的样式
href: '' // 外部css(查看二维码的dom结构,根据类名进行样式覆盖)文件url,需要https
})
}

其中href参数项还可以通过node将css文件转换为data-url,实现方式如下:

var fs = require('fs');
function base64_encode(file) {
var bitmap = fs.readFileSync(file);
return 'data:text/css;base64,'+new Buffer(bitmap).toString('base64');
}
console.log(base64_encode('./qrcode.css'))

在终端对该js文件执行命令:

node qr.js

把打印出来的url粘贴到href即可。
这种实现方法避免了需要跳转新页面进行扫码,二维码的样式也可以进行更多的自定义设置,可能在体验上是更好的选择。

原文:https://segmentfault.com/a/1190000024492932


收起阅读 »

苹果:App自6月30日起支持删除账号,开发者相关问题都在这里了

今晨,苹果正式宣布自 2022 年 6 月 30 日起,提交至 App Store 且支持账号创建的应用,必须允许用户在应用内删除账号。6 月 30 日起,App 必须允许用户删除账号从 2022 年 6 月 30 日开始,App Store 内支持账号创建的...
继续阅读 »

今晨,苹果正式宣布自 2022 年 6 月 30 日起,提交至 App Store 且支持账号创建的应用,必须允许用户在应用内删除账号。

6 月 30 日起,App 必须允许用户删除账号

从 2022 年 6 月 30 日开始,App Store 内支持账号创建的应用,必须提供删除账号的功能。

1653473320(1).jpg

出海痛点很多?点击这里解决



开发者如需更新应用程序以完善删除账号功能,需要注意以下几点:

1)用户能在应用中快速找到删除账号的入口,一般可在账户设置中找到;

2)如果用户是通过 Apple ID 登录,需要在删除账号时使用 Sign in with Apple REST API 来撤销用户令牌;

发.png

3)用户删除账号不仅是暂时停用或禁用账号,苹果要求在应用内,所有与该账号相关的个人数据都可以被删除,以帮助用户更好地管理隐私数据;

4)受高度监管的应用可能需要提供额外的客户服务流程,以跟进账号删除过程;

5)遵守有关存储和保留用户账号信息以及处理账号删除的适用法律要求,包括遵守不同国家或地区的当地法律。

此外,如果用户需要访问网站以指引如何删除账号,开发者也需提供相关链接。

若删除账号需要额外的时间,或删除时应用购买问题需要另外解决,开发者也应告知用户。

App 删除账号功能相关问题

Q:开发者可以将用户引导到客户服务流程以完成账号删除吗?

A:受高度监管的应用,如中应用商店审查指南 5.1.1(ix)所述,可能会使用额外的客户服务流程来确认和促进账号删除过程。

不在高度监管的行业中运行的应用程序不应要求用户拨打电话、发送电子邮件或通过其他支持流程完成账号删除。

Q:开发者是否可以要求重新认证,或添加确认步骤以确保账号不会被意外删除或被账号持有人以外的人删除?

A:可以,确保删除动作是用户期望进行的。

开发者可以添加步骤来验证用户身份,并确认他们想要删除该账号(如通过输入已与该账号关联的电子邮件或电话号码)。

但是,给用户删除账号增加不必要的困难将不会通过审核。

Q:如应用使用 Sign in with Apple 为用户提供账号创建和身份验证,需要进行哪些更改?

A:支持 Sign in with Apple 的应用需要使用 Sign in with Apple REST API 来撤销用户令牌。更多信息,请查看苹果官方文档和设计建议。

Q:如果开发者的应用链接到默认网络浏览器以创建账号,是否仍需要在应用内提供账号删除功能?

A:是的。但请注意链接到默认 Web 浏览器进行登录或注册账号,会影响用户体验,具体可查看应用商店审查指南 4。

Q:应用会自动为用户创建一个账号,是否需要提供进行账号删除的选项?

A:是的。用户应该可以选择删除自动生成的账号(包括访客账号)以及与这些账号关联的数据。

同时,开发者需要确保应用中的任何账号创建都符合当地法律。

Q:账号删除是否必须立即自动完成?

A:不是,可以接受手动删除账号,并花费一些时间。

开发者需要通知用户删除账号需要多长时间,并在删除完成后提供确认,并确保删除账号所用的时间。

Q:删除账号后,用户产生的内容是否需要在共享的应用中删除?

A:是的。用户删除账号时,将删除与其账号关联的所有数据,包括与他人一起生成的内容,如照片、视频、文字帖子和评论等。

如果当地法律要求开发者维护某些数据,请另外告知用户。

Q:是否允许应用只在某些地方根据 CCPA、GDPR 或其他当地法律删除账号?

A:不可以。应该允许所有用户删除他们的账号,无论他们身在何处,开发者的账号删除流程也需要提供给所有用户。

Q:如何管理自动续订的用户,以免在用户删除账号后意外收费?

A:告知用户管理订阅,后续计费将通过 Apple 继续,并提醒用户在下一次收费前取消订阅。

开发者使用 App Store 自动续订的 Server Notifications,可以实时查看用户的订阅状态,或者使用订阅状态 API 进行识别。

同时,开发者可以提供 Apple 支持链接(https: //support.apple.com/en-us/HT204084),帮助用户提交退款请求。

此外,开发者还可以提供一个选项,即设置账号删除日期与订阅到期时间一致,但仍需提供可立即删除账号的选项。

应用更新过程中的更多常见问题,可访问以下网站了解:

https://developer.apple.com/support/offering-account-deletion-in-your-app

据悉,苹果去年就已宣布调整 App Store 的指导方针,要求应用允许用户删除自己的账户,但由于功能实现较复杂,苹果两度推迟实行。如今正式推行,预计未来一段时间内或将有大量应用进行更新。

收起阅读 »

西电女生毕设找代笔,事后玩起“仙人跳”被举报

这是一场无比荒谬的闹剧:5 月 22 日,一名网友在西安电子科技大学贴吧发文控诉,声称自己在为该校两名学生代写论文后,反遭对方利用平台漏洞,以投诉为名进行敲诈,自己忍无可忍将其曝光。这一带有 " 黑吃黑 " 元素的离奇事件,瞬间一石激起千层浪。次日,西安电子科...
继续阅读 »

这是一场无比荒谬的闹剧:5 月 22 日,一名网友在西安电子科技大学贴吧发文控诉,声称自己在为该校两名学生代写论文后,反遭对方利用平台漏洞,以投诉为名进行敲诈,自己忍无可忍将其曝光。

这一带有 " 黑吃黑 " 元素的离奇事件,瞬间一石激起千层浪。次日,西安电子科技大学计算机科学与技术学院紧急发表声明,表示已开始调查学生雷某某、卢某某涉嫌找人有偿代做毕业设计的相关问题,并决定暂停了两人的毕业设计答辩工作。

当下正值毕业季,无数学生仍在忙于毕业论文(设计)的定稿和答辩。这一突如其来的学术丑闻,或许会使各大高校再度提高论文的审核标准,让毕业难上加难。因此不少人开玩笑称," 天临四年 " 还没结束,又迎来了 " 雷卢元年 "。

事实上,代写论文等学术不端行为,早已是老生常谈的问题。所以为什么这一事件曝光后,还会在全网引发轩然大波呢?

首先是动机问题。根据这位爆料的 " 枪手 " 所言,雷某某和卢某某两名学生先是通过闲鱼平台联系自己代做项目文件,而等到自己做完后,对方才说这是她们的毕业设计。

一般来说,普通项目文件(论文)的代写不违反法律关于著作权的规定,而毕业论文(设计)这类学位论文的代写,则毋庸置疑属于学术不端。

根据教育部 2013 年开始施行的《学位论文作假处理办法》,代写学位论文不仅会处理购买方,取消其学位申请资格,3 年内不得再次申请,甚至直接开除学籍;而且出售方也会被追究责任,属于在读学生的要开除学籍,属于教师或其他工作人员的要开除或解聘。

因此,这位 " 枪手 " 认为西电学生欺骗自己的行为是故意构陷,并且无端带来了极高的法律风险。

其次是行为问题。从曝光的聊天记录中可以看出,雷某某在早期沟通需求时,先是转发给了 " 枪手 " 几篇用以参考的论文,又要求增加在线学习的 Qlearning 算法。在 " 枪手 " 完成后,又要改回普通的 Qlearning,根据导师的要求增加新的内容,最后则又是要求通过 Dqn 算法实现。

经过了数次修改后," 枪手 " 认为多次修改需要加钱,而雷某某却认为 " 枪手 " 没能满足自己需求,要求退款。遭到拒绝后,甚至将 " 枪手 " 举报至平台客服,并且威胁他 "(客服)接了(投诉)要处理不能拖 "。

不仅如此,卢某某同 " 枪手 " 也经历了类似的交涉,最终双方没能达成交易。其中到底经历了什么,发帖爆料人并未详细多说。

根据爆料人描述,卢某某先是以修改项目骗他代写毕业设计,完了之后又以向闲鱼举报为由要求他退款。她因为 " 枪手 " 没能秒回自己信息而发火,直言 " 我不想举报你,你赶紧转账过来 "、" 我钱不要了,和你死磕到底 "、" 我要去举报正经维权 " 等,甚至用两分钟倒计时威胁,不转账就直接举报。

" 枪手 " 在西安电子科技大学贴吧发文控诉后不久,就有评论区的网友人肉出了雷某某和卢某某两人的身份信息,包括姓名、专业、班级,并且向她们的指导老师发邮件举报其学术不端行为。然而卢某某似乎依然没能认识到自己的错误,她多次联系 " 枪手 ",要求删帖减小影响。

当她意识到事态无法挽回之时,只是悻悻表示:" 对你来说是经济损失,可是对我们来说是真的没办法毕业的事情。" ——堪比琼瑶剧里那句 " 你失去的只是一条腿,她失去的可是爱情啊 ",都有着令人无比迷惑的逻辑和三观。

雷某某卢某某清楚自己找人代写论文的行为将会导致多么严重的后果吗?

根据《西安电子科技大学本科生考试纪律与学术规范条例》和《西安电子科技大学学位论文作假行为处理实施细则》的规定,一旦两人行为属实,至少需要延迟毕业 1 年,如若从严处理,还会被开除学籍,并取消学位申请资格。

像雷某某和卢某某这样找 " 枪手 " 代写论文的行为,在如今的大学生中并不少见。2018 年,微信公众号「大学声」联合全国学业发展联盟,进行了一次大学生毕业论文(设计)情况调查发现,38.4% 的大学生听说过一两个同学找代写,33% 大学生身边有一些找代写的同学,仅 28.6% 大学生周围同学完全没有找代写的现象。

■ 图源公众号「大学声」

虽然学位论文的查重率要求越来越严格,代写论文却依然屡禁不止。不同于抄袭,随时会被数据库筛查出来,代写则完全属于一个 " 自己不说就没人知道 " 的灰色地带。

正规的学位论文从选题、开题、撰写乃至修改的过程中,学生都需要与指导老师有充分沟通,一篇论文往往是几个月时间长期打磨的结果。然而代写论文大多数是临时 " 抱佛脚 ",正如雷某某和卢某某这样,在答辩前一个月才找 " 枪手 " 匆忙代写。

这样质量低下的代写论文最终能够瞒天过海,除了学生失职,指导老师的工作也有疏忽。他们不仅没有给予学生相应的关注,也没能发现学生在撰写论文中的问题和猫腻。这样 " 重结果、轻过程 " 的评价机制,给了学生代写论文的机会和胆量。

需求催生了市场,校园厕所的小广告、网络群组,甚至闲鱼这类二手交易平台中隐藏着巨大的论文代写生意。即使部分网络平台屏蔽了 " 论文代写 " 这一关键词,转去搜索 " 文章指导 " 或 " 文章创作 " 等词条,仍然会出现大量的论文代写 " 枪手 "。

在论文代写这一 " 灰色产业 " 中,由于缺少监管,许多学生在购买了代写服务后还会遇到各式各样的问题。根据《法治日报》调查发现,许多论文代写商家质量低下,内容大多为简单复制;坐地起价,修改或查看都需要额外支付费用;虚假包装,甚至将大一新生包装成名校硕士。

如果购买服务的学生不满意,去找客服申诉,得到的结果大多数是不欢而散,要么被对方拉黑,要么反被对方威胁要将文章上传至网络、告诉学校老师等。最后,找代写的学生往往选择暗自吃下闷亏。

这或许就是此次西电学生代写论文事件爆火的原因——从前都是 " 枪手 " 举报学生,如今是学生举报 " 枪手 " ——让灰色产业中的权力关系直接颠倒过来了,上演了一出意想不到的 " 黑吃黑 "。

" 枪手 " 将雷某某和卢某某两人曝光后,据称为了防止舆论升级,已经将帖子删除。诚然,我们无法用他的单方面信源去还原事件的全貌。但雷某某和卢某某二人代写论文、投诉敲诈,无论如何都是极为恶劣的行为,是自身诚信和德行的全方面破产。

寻求有偿代写损害了自己在学术上的诚信,这样唯结果论的价值取向更是令人忧心。《中国科学报》调查发现,涉事的两位学生中,卢某某疑似已于 2021 年 10 月获得计算机科学与技术学院的 2022 年推免生录取名额。

在学校的经验分享活动中,卢某某甚至作为先锋模范,建议同校的大二同学要从保研政策、综合素质能力加分和第二课堂学分认证方面全面成长。且不谈风波过后,她是否还有机会保研。人前一套背后一套的行径,使得她早已透支了自己在人际关系中的诚信。

在成立调查工作组后,西安电子科技大学都需要尽快公布一份详尽的调查报告和处理方案。因为这不仅关乎她们个人的未来,更关乎整个教育系统的公平性和公正性。

这枚学术不端的震撼弹带来了极高的网络声量,无论是在贴吧还是微博,各个社交平台以及媒体报道中都能看到相关讨论。可是许多评论也走向了难以预料的方向,许多人开始认为这两位学生是生活中做作的 " 小仙女 ",为她们如今面临失学的人生窘境而弹冠相庆。

更有甚者,通过人肉搜索等方式找到了这两名学生的照片,发出来让人打分评价她们的外貌。无端揣测她们因为受到网暴而假装自己得抑郁症(玉玉症),以换取保研机会。甚至将她们看作是平时对男性喊打喊杀的 " 极端女拳 ",认为对她们的举报是一场正义的性别战争。

在这样一种集体狂躁的情绪中,隐私权甚至道德准则都不再重要,暴力成了解决事情的唯一手段。

但暴力真的能解决学术不端的问题吗?很显然不能。

学术不端一直都是教育和管理中难以铲除的顽疾,有人认为应该加强论文评审和答辩,用 " 宽进严出 " 来治理乱象;也有人认为根本上是要加强学生的基础研究能力,放宽本科论文要求,甚至取消本科学位论文 ……

罗翔认为,论文抄袭问题实质上和我们应该以何种信念作为我们人生以及学术的动力相关,这是每一个以学术为志业的人都应该认真思考的问题。

高校毕业季当前,这两名西电学生的经历再次敲响了学术诚信的警钟。越来越多的学生需要意识到,自己在学位论文的独创性声明里签下的不只是自己的名字,还是迈向更广阔世界前的一份承诺。

而对学术不端事件处理得越公开、越清晰,人们去触犯这些道德标准的可能性就越小。除了松一阵紧一阵的学风建设,很显然,我们的大学还需要做得更多。

作者 | 佳星
收起阅读 »

GitHub 上又一款开源的像素风字体,和元宇宙社交最搭啦

GitHub 上又一款开源的像素风字体:泛中日韩像素字体。支持 10、12 和 16 像素。目标是为开发「像素风游戏」,提供一套开箱即用的字体解决方案。项目不仅提供了全部的字形设计源文件,也提供了构建字体所需要的完整程序。GitHub:github.com/T...
继续阅读 »

GitHub 上又一款开源的像素风字体:泛中日韩像素字体。支持 10、12 和 16 像素。目标是为开发「像素风游戏」,提供一套开箱即用的字体解决方案。

项目不仅提供了全部的字形设计源文件,也提供了构建字体所需要的完整程序。



GitHub:github.com/TakWolf/ark-pixel-font   

收起阅读 »

搜狐全员收到“工资补助”诈骗邮件 大量员工余额被划走

据说,搜狐全体员工收到一封内部域名发来的诈骗邮件,说是工资补贴,基本上所有员工都点了,被骗人数和金额巨大……在上周,搜狐全体员工收到了一封来自“搜狐财务部”名为《5月份员工工资补助通知》的邮件,大量员工按照附件要求扫码,并填写了银行账号等信息,最终不但没有等到...
继续阅读 »

据说,搜狐全体员工收到一封内部域名发来的诈骗邮件,说是工资补贴,基本上所有员工都点了,被骗人数和金额巨大……


在上周,搜狐全体员工收到了一封来自“搜狐财务部”名为《5月份员工工资补助通知》的邮件,大量员工按照附件要求扫码,并填写了银行账号等信息,最终不但没有等到所谓的补助,工资卡内的余额也被划走。

图片中的聊天记录中有人表示,“点击后扫码,工资卡里的钱就被划光了”。


邮件发信地址为sohutv-legal@sohu-inc.com,确实为搜狐内部域名,能够通过内部邮箱发邮件,这样来看,要弄就是被黑了,要么就有“内鬼”。


这要是真的就很尴尬了,两种可能,要么是钓鱼拿到了内网邮箱,要么利用漏洞直接进入了邮件服务器,然后再全员批量发送邮件进行诱导盗刷。


有网友直呼:打工人真的好惨。也有网友表示不理解:不是每年都护网吗?一看就是平时没有做这方面的演练。还有网友表示同情:卡上的余额都没了,得有多绝望。

简而言之,搜狐公司员工遭遇了网络上最常见的诈骗方式。但因为邮件来源显示为搜狐公司内部域名,公司平时报销也存在需要员工银行账号的惯例,加上员工之间本身就有薪资保密的义务,搜狐几乎所有员工都没有对邮件内容产生怀疑,这才导致被骗人数和涉案金额巨大。


聊天记录显示,事后搜狐迅速采取了行动,包括立刻删除了相关邮件,并由ES部门出面汇总遭遇诈骗员工的信息到派出所报案。

事实上,类似的“工资补助”诈骗从去年开始就在全国发生过多起,搜狐新闻也进行过相关报道。

来源:mp.weixin.qq.com/s/AY1sisbn0MfO9NM1bhqpQQ

收起阅读 »

CAS以及Atomic原子操作详解

CAS以及Atomic原子操作详解 CAS 什么是CAS 针对一个变量,首先比较它在内存中的值与某个期望的值是否相同,如果相同就给它赋予新值 其原子性是直接在硬件层面得到保障的 CAS是一种无锁算法,在不使用锁的情况下实现多线程之间的变量同步 底层...
继续阅读 »

CAS以及Atomic原子操作详解


CAS




  • 什么是CAS



    • 针对一个变量,首先比较它在内存中的值与某个期望的值是否相同,如果相同就给它赋予新值

    • 其原子性是直接在硬件层面得到保障

    • CAS是一种无锁算法,在不使用锁的情况下实现多线程之间的变量同步




  • 底层: CAS的底层实现



    • 从JVM源码层面的看CAS

      • 原子性

        • 在单核处理器是通过cmpxchgl指令来保证原子性的,在多核处理器下无法保证了,通过lock前缀的加持变为lock cmpxchgl保证了原子性,这里lock前缀指令拥有保证后续指令的原子性的作用



      • 有序性

        • 通过C++关键字volatile禁止指令重排序保证有序性,对于C++关键字volatile有两个作用一个是禁止重排序,一个是防止代码被优化



      • 其中可见性在JVM源码层面是保证的了,因为多核处理器下会加lock前缀指令,但是Java代码层面实现的CAS不能保证get加锁标记和set加锁标记的可见性,比如Atomic类中需要通过volatile修饰state保证可见性






  • 缺陷: CAS的缺陷



    • 一般CAS都是配合自旋,自旋时间过长,可能会导致CPU满载,所以一般会选择自旋到一定次数去park

    • 每次只能保证一个共享变量进行原子操作

    • ABA问题

      • 问题: 什么是ABA问题

        • 当有多个线程对一个原子类进行操作时,某个线程在这段时间内将A修改到B,又马上将其修改为A,其他线程并不感知,还是会被修改成功



      • 问题: ABA问题的解决方案

        • 数据库有个锁是乐观锁,是一种通过版本号方式来进行数据同步,也就是每次更新的时候都会匹配这个版本号,只有符号才能更新成功,同样的ABA问题也是基于这种去解决的,相应的Java也提供了对应的原子类AtomicStampedRefrence,其内部reference就是我们实际存储的变量,stamp就是版本号,每次修改可以通过加1来保证版本的唯一性








  • 问题: CAS失败自旋的操作存在什么问题



    • CAS自旋时间过长不成功,会给CPU带来较大的开销




  • CAS的应用




    • CAS操作的是由Unsafe类提供支持,该类定义了三种针对不同类型变量的CAS操作


      public final native boolean compareAndSwapObject(Object o, long offset,Object expected,Object x);
      public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
      public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);





Atomic原子类



  • 在并发编程中很容易出现并发安全的问题,比如自增操作,有可能不能获取正确的值,一般情况想到的是synchronized来保证线程安全,但是由于它是悲观锁,并不是最高效的解决方案,所以Juc提供了乐观锁的方式去提升性能

    • 基本类型: AtomicInteger、AtomicLong、AtomicBoolean

    • 引用类型: AtomicReference、AtomicStampedRerence、AtomicMarkableReference

    • 数组类型: AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

    • 对象属性原子修改器: AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater

    • 原子类型累加器(JDK8增加的类): DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder、Striped64




LongAdder和DoubleAdder


瓶颈详解



  • 对于高并发场景下,多个线程同时进行自旋操作,会出现大量失败并不断自旋的情况,此时AtomicLong自旋会成为瓶颈,LongAdder引入解决了高并发场景,AtomicInteger、AtomicLong的自旋瓶颈问题


LongAdder原理


image.png

  • AtomicLong中有个内部变量value保存着实际的值,所有的操作都是针对该变量进行,在高并发场景下,value变量其实就是一个热点,多个线程同时竞争这个热点,而这样冲突的概率就比较大了

  • 重点: LongAdder的基本思路就是分散热点,将value的值分散到一个数组中,不同线程会命中到这个数组的不同槽位中,各个线程只对自己槽位中的那个值进行CAS操作,这样就分散了热点,冲突的概率就小很多,如果要获取真正的值,只需要将各个槽位的值累加返回

  • LongAdder设计的精妙之处: 尽量减少热点冲突,不到最后万不得已,尽量将CAS操作延迟

  • 注意: LongAdder的sum方法会有线程安全的问题

    • 高并发场景下除非全局加锁,否则得不到程序运行中某个时刻绝对准确的值,由于计算总和时没有对Cell数组进行加锁,所以在累加过程中可能有其他线程对于Cell数组中的值因为线程安全无法保障进行了修改,也有可能对数组进行了扩容,所以sum返回的值并不是非常精确的,其返回值并不是一个调用sum方法的原子快照值




LongAdder逻辑


image.png

LongAccumulator



  • LongAccumulator是LongAdder的增强版本,LongAdder只针对数组值进行加减运算,而LongAccumulator提供了自定义的函数操作

  • LongAccumulator内部原理和LongAdder几乎完全一样,都是利用了父类Striped64的longAccumulate方法

作者:枫度柚子
链接:https://juejin.cn/post/7101216131397976071
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

从单例谈double-check必要性,多种单例各取所需

前言 前面铺掉了那么多都是在讲原则,讲图例。很多同学可能都觉得和设计模式不是很搭边。虽说设计模式也是理论的东西,但是设计原则可能对我们理解而言更加的抽象。不过好在原则东西不是很多,后面我们就可以开始转讲设计模式了。 我的思路是按照设计模式进行分类整理。期间穿...
继续阅读 »

前言



  • 前面铺掉了那么多都是在讲原则,讲图例。很多同学可能都觉得和设计模式不是很搭边。虽说设计模式也是理论的东西,但是设计原则可能对我们理解而言更加的抽象。不过好在原则东西不是很多,后面我们就可以开始转讲设计模式了。

  • 我的思路是按照设计模式进行分类整理。期间穿插相关的知识进行扩展从而保证我们学习的更加的全面。在正式开始前我现在这里立个Flag。争取在20周内完成我们设计模式章节的内容。期间可能会有别的学习,20周争取吧

  • 相信单例模式是大家第一个使用到的设计模式吧。不管你怎么样,我第一个使用的就是单例模式。其实单例模式也是分很多种的【饿汉式】、【懒汉式】。如果在细分还有线程安全和线程不安全版本的。


饿汉式



  • 顾名思义饿汉式就是对类需求很迫切。从Java角度看就是类随着JVM启动就开始创建,不管你是否使用到只要JVM启动就会创建。


 public class SingleFactory
 {
     private static Person person = new Person();
 
     private SingleFactory()
    {
    }
 
     public static Person getInstance()
    {
         return person;
    }
 }


  • 上面这段代码就是饿汉式单例模式。通过这单代码我们也能够总结出单例模式的几个特点






















  • 特点
    隐藏类的创建即外部无法进行创建
    内部初始化好一个完整的类
    提供一个可以访问到内部实例的方法,这里指的是getInstance



image-20220509183514066.png



  • 单例模式特点还是很容易区分的。饿汉式感觉挺好的,那为什么后面还会出现懒汉式及其相关的变形呢?下面我们就来看看饿汉式有啥缺点吧。

  • 首先上面我们提到饿汉式的标志性特点就是随着JVM 的启动开始生成实例对象。这是优点同时也是缺点。大家应该都用过Mybatis等框架,这些框架为了加快我们程序的启动速度纷纷推出各种懒加载机制。

  • 何为懒加载呢?就是用到的时候再去初始化相关业务,将和启动不相关的部分抽离出去,这样启动速度自然就快了起来了。在回到饿汉式,你不管三七二十一就把我给创建了这无疑影响了我的程序启动速度。如果这个单例模式你使用了倒还好,假如启动之后压根就没用到这个单例模式的类,那我岂不是吃力不讨好。不仅浪费了时间还浪费了我的空间。

  • 所以说,处于对性能的考虑呢?还是建议大家不要使用饿汉式单例。但是,存在即是合理的,我们不能一棒子打死一堆人。具体场景具体对待吧XDM。


🐶变形1


 public class SingleFactory
 {
     private static Person person ;
 
     static {
         person = new Person();
    }
 
     private SingleFactory()
    {
    }
 
     public static Person getInstance()
    {
         return person;
    }
 }


  • 咋一看好像和上面的没啥区别哦。仔细对比你就会发现我们这里并没有立刻创建Person这个类,而是放在静态代码块中初始化实例了。

  • 放在静态代码块和直接创建其实是一样的。都是通过类加载的方式来进行实例化的。基本同根同源没啥可说的 。

  • 关于Static关键字我们之前也有说过,他涉及到的是类加载的顺序。我们在类加载的最后阶段就是执行我们的静态代码块


懒汉式


 public class SingleFactory
 {
     private static Person person = null;
 
     private SingleFactory()
    {
    }
 
     public static Person getInstance()
    {
         try
        {
             Thread.sleep(30);
        }
         catch (InterruptedException e)
        {
            e.printStackTrace();
        }
         if(person==null){
             person=new Person();
        }
         return person;
    }
 }


  • 懒汉式就是将我们的对象创建放在最后一刻进行创建。并不是跟随类加载的时候生成对象的,这样会造成一定程度的内存浪费。懒汉式更加的提高了内存的有效利用。在getInstance方法中我们在获取对象前判断是否已经生成过对象。如果没有在生成对象。这种行为俗称懒,所以叫做懒汉式单例模式


🐱变形1



  • 上面懒汉式单例中我加入了睡眠操作。这是因为我想模拟出他的缺点。上面这种方式在高并发的场景下并不能保证系统中仅有一个实例对象。


 public class SingleFactory
 {
     private static Person person = null;
 
     private SingleFactory()
    {
    }
 
     public static Person getIstance()
    {
         try
        {
             Thread.sleep(30);
        }
         catch (InterruptedException e)
        {
             e.printStackTrace();
        }
         synchronized (SingleFactory.class)
        {
             if (person == null)
            {
                 person = new Person();
            }
        }
         return person;
    }
 }


  • 只需要加一把锁,就能保证线性操作了。但是仔细想想难道这样就真的安全了吗。


double-check



  • 在多线程下安全的单例模式应该非double-check莫属了吧。


 public class OnFactory {
     private static volatile OnFactory onFactory;
 
     public static OnFactory getInstance() {
         if (null == onFactory) {
             synchronized (OnFactory.class) {
                 if (null == onFactory) {
                     onFactory = new OnFactory();
                }
            }
        }
         return onFactory;
    }
 }


  • 这段代码是之前咱们学习double-check和volatile的时候写过的一段代码。在这里我们不仅在锁前后都判断了而且还加上了volatile进行内存刷新。关于volatile需要的在主页中搜索关键词即可找到。这里仅需要知道一点volatile必须存在否则线程不安全。

作者:zxhtom
链接:https://juejin.cn/post/7101496650610245646
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Kotlin - 改良责任链模式

一、前言 责任链模式 作用:避免请求的发送者和接收者之间的耦合关系,将这个对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。 举例:OKHttp 的拦截器、Servlet 中的 FilterChain 二、使用责任链模式 例子:学生...
继续阅读 »

一、前言



  • 责任链模式

    • 作用:避免请求的发送者和接收者之间的耦合关系,将这个对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。

    • 举例:OKHttp 的拦截器、Servlet 中的 FilterChain




二、使用责任链模式



  • 例子:学生会经费申请

  • 重点:1 个请求会在 n 个处理器组成的处理器链上传递


以学生会经费申请会例,学生会会有一些日常开销以及活动开支,需要向学院的学生会基金申请经费,如果金额在 100 元之内,由分部长审批;如果金额在 100 到 500 元之间,由会长审批;如果金额在 500 到 1000 元之间,由学院辅导员审批;而如果金额超过 1000 元,则默认打回申请。像这种需要一层层往后传递请求的情况,非常适合采用责任链模式来设计程序:


/**
* 经费申请事件
*
* @author GitLqr
*/
data class ApplyEvent(val money: Int, val title: String)

/**
* 经费审批处理器
*
* @author GitLqr
*/
interface ApplyHandler {
val successor: ApplyHandler?
fun handleEvent(event: ApplyEvent)
}


注意:责任链模式需要将处理器对象连成一条链,最简单粗暴的方式就是让前驱处理器持有后继处理器 successor



接着,根据案例需要,编写各个角色对应的处理器类:


/**
* 部长
*
* @author GitLqr
*/
class GroupLeader(override val successor: ApplyHandler?) : ApplyHandler {
override fun handleEvent(event: ApplyEvent) {
when {
event.money <= 100 -> println("Group Leader handled application: ${event.title}.")
successor != null -> successor.handleEvent(event)
else -> println("Group Leader: This application cannot be handled.")
}
}
}

/**
* 会长
*
* @author GitLqr
*/
class President(override val successor: ApplyHandler?) : ApplyHandler {
override fun handleEvent(event: ApplyEvent) {
when {
event.money <= 500 -> println("President handled application: ${event.title}.")
successor != null -> successor.handleEvent(event)
else -> println("President: This application cannot be handled.")
}
}
}

/**
* 学院
*
* @author GitLqr
*/
class College(override val successor: ApplyHandler?) : ApplyHandler {
override fun handleEvent(event: ApplyEvent) {
when {
event.money <= 1000 -> println("College handled application: ${event.title}.")
successor != null -> successor.handleEvent(event)
else -> println("College: This application cannot be handled.")
}
}
}

最后,创建各个角色处理器实例,并按顺序组成一条链,由链头开始接收、转发需要被处理的经费申请事件:


// 使用
// val college = College(null)
// val president = President(college)
// val groupLeader = GroupLeader(president)
val groupLeader = GroupLeader(President(College(null)))
groupLeader.handleEvent(ApplyEvent(10, "buy a pen")) // 买只钢笔
groupLeader.handleEvent(ApplyEvent(200, "team building")) // 团建
groupLeader.handleEvent(ApplyEvent(600, "hold a debate match")) // 举行辩论赛
groupLeader.handleEvent(ApplyEvent(1200, "annual meeting of the college")) // 学院年会

// 输出
Group Leader handled application: buy a pen.
President handled application: team building.
College handled application: hold a debate match.
College: This application cannot be handled.

从输出结果可以看到,经费申请事件会在处理器链上传递,直到被一个合适的处理器处理并终止。



注意:这话是针对当前案例说的,责任链模式没有硬性要求一个请求只能被一个处理器处理,你可以在前面的处理器中对请求进行加工,提取数据等等操作,并且可以选择是否放行,交由后面的处理器继续处理,这需要根据实际情况,灵活应变。



三、改良责任链模式



  • 例子:学生会经费申请

  • 重点:偏函数 Partial Function


在对上述案例进行改良之前,我们先来了解一下偏函数是什么,在不同的编程语言中,对偏函数的理解还不一样,在 Python 中,偏函数是使用 functools.partial 把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。而在 Scala 中,偏函数是使用 PartialFunction 构建一个仅仅处理输入参数的部分分支的函数,换句话说,就是带有判断条件的函数,只有满足条件的参数,才会被函数处理。


以上结论来自以下两篇文章:




题外话:对 Scala 偏函数有兴趣的可以看一下上面的文章,写的很通透。



回过头来,责任链模式的核心机理是,整个链条上的每个处理环节都有对其输入的校验标准,当输入的参数处于某个责任链节的有效接收范围之内,该环节才能对其做出正常的处理操作。那么,我们是不是可以把链条上的每个处理环节看做是一个个的偏函数呢?是的,不过 Kotlin 中并没有内置偏函数 API,好在有一个第三方 Kotlin 函数库【funKTionale】,其中的 partialfunctions.kt 就有 Scala 中偏函数的类似实现:


// https://github.com/MarioAriasC/funKTionale/blob/master/funktionale-utils/src/main/kotlin/org/funktionale/utils/partialfunctions.kt

class PartialFunction<in P1, out R>(private val definetAt: (P1) -> Boolean, private val f: (P1) -> R) : (P1) -> R {
override fun invoke(p1: P1): R {
if (definetAt(p1)) {
return f(p1)
} else {
throw IllegalArgumentException("Value: ($p1) isn't supported by this function")
}
}

fun isDefinedAt(p1: P1) = definetAt(p1)
}

这个 PartialFunction 类第一眼看上去感觉好复杂,分成如下几步,方便理解:



  • PartialFunction 继承自一个函数类型 (P1) -> R,编译器会强制要求实现 invoke() 方法,这意味着 PartialFunction 实例对象可以像调用函数那样使用。

  • 构造参数 1 definetAt: (P1) -> Boolean 用于判断 P1 参数是否满足被处理的条件。

  • 构造参数 2 f: (P1) -> R 用于处理 P1 参数并返回 R 类型值。

  • 成员方法 invoke 中,当 P1 满足条件时,则将 P1 交给 构造参数 2 f: (P1) -> R 处理;否则抛出异常。

  • 成员方法 isDefinedAt 只是构造参数 1 definetAt 的拷贝。


所以,用一句话概括 PartialFunction 实例对象,就是一个带有判断条件的"函数",只有满足条件的参数,才会被"函数"处理。现在我们用一个个 PartialFunction 实例来代替处理器是完全没问题的,问题是怎么把它们链接起来呢?【funKTionale】中还为 PartialFunction 扩展了一个 orElse 函数,这就是把偏函数组合起来的关键:


// https://github.com/MarioAriasC/funKTionale/blob/master/funktionale-utils/src/main/kotlin/org/funktionale/utils/partialfunctions.kt

infix fun <P1, R> PartialFunction<P1, R>.orElse(that: PartialFunction<P1, R>): PartialFunction<P1, R> {
return PartialFunction({ this.isDefinedAt(it) || that.isDefinedAt(it) }) {
when {
this.isDefinedAt(it) -> this(it)
that.isDefinedAt(it) -> that(it)
else -> throw IllegalArgumentException("function not definet for parameter ($it)")
}
}
}

同样,也分成如下几步,方便理解:



  • orElsePartialFunction 的扩展函数,故内部可以使用 this 获取原本的 PartialFunction 实例(也就是 receiver)。

  • orElse 只接收一个 PartialFunction 类型参数 that,并且返回一个 PartialFunction 类型实例,故 orElse 可以嵌套调用。

  • orElse 返回值是一个使用了两个 PartialFunction 实例对象 (即 thisthat)组合出来的一个新的 PartialFunction 实例对象,

  • orElse 返回值的意图是,只要原本的 thisthat 中有一个条件成立,那么就让条件成立的那个来处理参数 P1 ,否则抛出异常。其实,这个 that 就相当于是责任链模式中的 successor

  • orElse 使用 infix 修饰,故支持中缀表达式写法。



注意:你可能一时看不懂 PartialFunction({ xxx }){ yyy } 这个奇怪的语法,其实很简单,在创建一个 PartialFunction 实例时,可以传入两个 Lambda 表达式,所以正常写法应该是这样的 PartialFunction({ xxx }, { yyy }) ,不过,在 Kotlin 中,当 Lambda 表达式作为最后一个参数传入时,可以写到函数外部,所以就出现了 PartialFunction({ xxx }){ yyy } 这种写法。



好了,现在用 PartialFunction 来改良原本的责任链模式代码:


/**
* 使用自运行Lambda来构建一个个 PartialFunction 实例:部长、会长、学院
*
* @author GitLqr
*/
val groupLeader = {
val definetAt: (ApplyEvent) -> Boolean = { it.money <= 200 }
val handler: (ApplyEvent) -> Unit = { println("Group Leader handled application: ${it.title}.") }
PartialFunction(definetAt, handler)
}()
val president = {
val definetAt: (ApplyEvent) -> Boolean = { it.money <= 500 }
val handler: (ApplyEvent) -> Unit = { println("President handled application: ${it.title}.") }
PartialFunction(definetAt, handler)
}()
val college = {
val definetAt: (ApplyEvent) -> Boolean = { true }
val handler: (ApplyEvent) -> Unit = {
when {
it.money <= 1000 -> println("College handled application: ${it.title}.")
else -> println("College: This application is refused.")
}
}
PartialFunction(definetAt, handler)
}()


注意:自运行 Lambda 相当于是 js 中的立即执行函数。



接下来就是用 orElse 将一个个 PartialFunction 实例链接起来:


// 使用
// val applyChain = groupLeader.orElse(president.orElse(college))
val applyChain = groupLeader orElse president orElse college // 中缀表达式
applyChain(ApplyEvent(10, "buy a pen")) // 买只钢笔
applyChain(ApplyEvent(200, "team building")) // 团建
applyChain(ApplyEvent(600, "hold a debate match")) // 举行辩论赛
applyChain(ApplyEvent(1200, "annual meeting of the college")) // 学院年会

// 输出
Group Leader handled application: buy a pen.
Group Leader handled application: team building.
College handled application: hold a debate match.
College: This application is refused.

使用 PartialFunction 之后,不仅可以不幅度减少代码量,结合 orElse 能获得更好的语法表达。以上,就是使用偏函数改良责任链模式的全部内容了。为了加深对偏函数的理解,这里引用数据工匠记的 Scala 《偏函数(Partial Function)》原文中的话:



为什么要用偏函数呢?以我个人愚见,还是一个重用粒度的问题。函数式的编程思想是以一种“演绎法”而非“归纳法”去寻求解决空间。也就是说,它并不是要去归纳问题然后分解问题并解决问题,而是看透问题本质,定义最原初的操作和组合规则,面对问题时,可以通过组合各种函数去解决问题,这也正是“组合子(combinator)”的含义。偏函数则更进一步,将函数求解空间中各个分支也分离出来,形成可以被组合的偏函数。


作者:GitLqr
链接:https://juejin.cn/post/7100562422376693797
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Flutter自绘组件:扇形图

简介 在开发过程中通常会遇到一些不规则的UI,比如不规则的线条,多边形,统计图表等等,用那些通用组件通过组合的方式无法进行实现,这就需要我们自己进行绘制。可以通过使用CuntomPaint组件并结合画笔CustomPainter去进行手动绘制各种图形。 Cus...
继续阅读 »

简介


在开发过程中通常会遇到一些不规则的UI,比如不规则的线条,多边形,统计图表等等,用那些通用组件通过组合的方式无法进行实现,这就需要我们自己进行绘制。可以通过使用CuntomPaint组件并结合画笔CustomPainter去进行手动绘制各种图形。


CustomPaint介绍


CustomPaint是一个继承SingleChildRenderObjectWidgetWidget,这里主要介绍几个重要参数:

childCustomPaint的子组件。
painter: 画笔,绘制的图形会显示在child后面。

foregroundPainter:前景画笔,绘制的图形会显示在child前面。

size:绘制区域大小。


CustomPainter介绍


CustomPainter是一个抽象类,通过自定义一个类继承自CustomPainter,重写paintshouldRepaint方法,具体绘制主要在paint方法里。


paint介绍


主要两个参数:
Canvas:画布,可以用于绘制各种图形。
Size:绘制区域的大小。


void paint(Canvas canvas, Size size)

shouldRepaint介绍


在Widget重绘前会调用该方法确定时候需要重绘,shouldRepaint返回ture表示需要重绘,返回false表示不需要重绘。


bool shouldRepaint(CustomPainter oldDelegate)

示例


这里我们通过绘制一个饼状图来演示绘制的整体流程。


pie_chart_view.gif


使用CustomPaint


首先,使用CustomPaint,绘制大小为父组件最大值,传入自定义painter


@override
Widget build(BuildContext context) {
return CustomPaint(
size: Size.infinite,
painter: PieChartPainter(),
);
}

自定义Painter


自定义PieChartPainter继承CustomPainter


class PieChartPainters extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {

}

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return oldDelegate != this;
}
}

绘制


接着我们来实现paint方法进行绘制


@override
void paint(Canvas canvas, Size size) {
//移动到中心点
canvas.translate(size.width / 2, size.height / 2);
//绘制饼状图
_drawPie(canvas, size);
//绘制扇形分割线
_drawSpaceLine(canvas);
//绘制中心圆
_drawHole(canvas, size);
}

绘制饼状图

我们以整个画布的中点为圆点,然后计算出每个扇形的角度区域,通过canvas.drawArc绘制扇形。


pie_chart_view1.png


void _drawPie(Canvas canvas, Size size) {
var startAngle = 0.0;
var sumValue = models.fold<double>(0.0, (sum, model) => sum + model.value);
for (var model in models) {
Paint paint = Paint()
..style = PaintingStyle.fill
..color = model.color;
var sweepAngle = model.value / sumValue * 360;
canvas.drawArc(Rect.fromCircle(radius: model.radius, center: Offset.zero),
startAngle * pi / 180, sweepAngle * pi / 180, true, paint);

//为每一个区域绘制延长线和文字
_drawLineAndText(
canvas, size, model.radius, startAngle, sweepAngle, model);

startAngle += sweepAngle;
}
}

绘制延长线以及文本

延长线的起点为扇形区域边缘中点位置,长度为一个固定的长度,转折点坐标通过半径加这个固定长度和三角函数进行计算,然后通过转折点的位置决定横线终点的方向,而横线的长度则根据文字的宽度决定,然后通过canvas.drawLine进行绘制直线。

文本绘制使用TextPainter.paint进行绘制,paint方法里面最终是通过canvas.drawParagraph进行绘制的。

最后再在文字的前面通过canvas.drawCircle绘制一个小圆点。


pie_chart_view2.png


 void _drawLineAndText(Canvas canvas, Size size, double radius,
double startAngle, double sweepAngle, PieChartModel model) {
var ratio = (sweepAngle / 360.0 * 100).toStringAsFixed(2);

var top = Text(model.name);
var topTextPainter = getTextPainter(top);

var bottom = Text("$ratio%");
var bottomTextPainter = getTextPainter(bottom);

// 绘制横线
// 计算开始坐标以及转折点的坐标
var startX = radius * (cos((startAngle + (sweepAngle / 2)) * (pi / 180)));
var startY = radius * (sin((startAngle + (sweepAngle / 2)) * (pi / 180)));

var firstLine = radius / 5;
var secondLine =
max(bottomTextPainter.width, topTextPainter.width) + radius / 4;
var pointX = (radius + firstLine) *
(cos((startAngle + (sweepAngle / 2)) * (pi / 180)));
var pointY = (radius + firstLine) *
(sin((startAngle + (sweepAngle / 2)) * (pi / 180)));

// 计算坐标在左边还是在右边
// 并计算横线结束坐标
// 如果结束坐标超过了绘制区域,则改变结束坐标的值
var marginOffset = 20.0; // 距离绘制边界的偏移量
var endX = 0.0;
if (pointX - startX > 0) {
endX = min(pointX + secondLine, size.width / 2 - marginOffset);
secondLine = endX - pointX;
} else {
endX = max(pointX - secondLine, -size.width / 2 + marginOffset);
secondLine = pointX - endX;
}

Paint paint = Paint()
..style = PaintingStyle.fill
..strokeWidth = 1
..color = Colors.grey;

// 绘制延长线
canvas.drawLine(Offset(startX, startY), Offset(pointX, pointY), paint);
canvas.drawLine(Offset(pointX, pointY), Offset(endX, pointY), paint);

// 文字距离中间横线上下间距偏移量
var offset = 4;
var textWidth = bottomTextPainter.width;
var textStartX = 0.0;
textStartX =
_calculateTextStartX(pointX, startX, textWidth, secondLine, textStartX, offset);
bottomTextPainter.paint(canvas, Offset(textStartX, pointY + offset));

textWidth = topTextPainter.width;
var textHeight = topTextPainter.height;
textStartX =
_calculateTextStartX(pointX, startX, textWidth, secondLine, textStartX, offset);
topTextPainter.paint(canvas, Offset(textStartX, pointY - offset - textHeight));

// 绘制文字前面的小圆点
paint.color = model.color;
canvas.drawCircle(
Offset(textStartX - 8, pointY - 4 - topTextPainter.height / 2),
4,
paint);
}

绘制扇形分割线

在绘制完扇形之后,然后在扇形的开始的那条边上绘制一条直线,起点为圆点,长度为扇形半径,终点的位置根据半径和扇形开始的那条边的角度用三角函数进行计算,然后通过canvas.drawLine进行绘制。


pie_chart_view3.png


void _drawSpaceLine(Canvas canvas) {
var sumValue = models.fold<double>(0.0, (sum, model) => sum + model.value);
var startAngle = 0.0;
for (var model in models) {
_drawLine(canvas, startAngle, model.radius);
startAngle += model.value / sumValue * 360;
}
}

void _drawLine(Canvas canvas, double angle, double radius) {
var endX = cos(angle * pi / 180) * radius;
var endY = sin(angle * pi / 180) * radius;
Paint paint = Paint()
..style = PaintingStyle.fill
..color = Colors.white
..strokeWidth = spaceWidth;
canvas.drawLine(Offset.zero, Offset(endX, endY), paint);
}

绘制内部中心圆

这里可以通过传入的参数判断是否需要绘制这个圆,使用canvas.drawCircle进行绘制一个与背景色一致的圆。


pie_chart_view4.png


void _drawHole(Canvas canvas, Size size) {
if (isShowHole) {
holePath.reset();
Paint paint = Paint()
..style = PaintingStyle.fill
..color = Colors.white;
canvas.drawCircle(Offset.zero, holeRadius, paint);
}
}

触摸事件处理


接下来我们来处理点击事件,当我们点击某一个扇形区域时,此扇形需要突出显示,如下图:


pie_chart_view5.png


重写hitTest方法

注意

这个方法的返回值决定是否响应事件。

默认情况下返回null,事件不会向下传递,也不会进行处理;
如果返回true则当前组件进行处理事件;
如果返回false则当前组件不会响应点击事件,会向下一层传递;


我直接在这里处理点击事件,通过该方法传入的offset确定点击的位置,如果点击位置是在圆形区域内并且不在中心圆内则处理事件同时判断所点击的具体是哪个扇形,反之则恢复默认状态。


@override
bool? hitTest(Offset offset) {
if (oldTapOffset.dx==offset.dx && oldTapOffset.dy==offset.dy) {
return false;
}
oldTapOffset = offset;
for (int i = 0; i < paths.length; i++) {
if (paths[i].contains(offset) &&
!holePath.contains(offset)) {
onTap?.call(i);
oldTapOffset = offset;
return true;
}
}
onTap?.call(-1);
return false;
}

至此,我们通过onTap向上传递出点击的是第几个扇形,然后进行处理,更新UI就可以了。


动画实现


pie_chart_view.gif


这里通过Widget继承ImplicitlyAnimatedWidget来实现,ImplicitlyAnimatedWidget是一个抽象类,继承自StatefulWidget,既然是StatefulWidget那肯定还有一个StateState继承AnimatedWidgetBaseState(此类继承自ImplicitlyAnimatedWidgetState),感兴趣的小伙伴可以直接去看源码


实现AnimatedWidgetBaseState里面的forEachTween方法,主要是用于来更新Tween的初始值。


@override
void forEachTween(TweenVisitor<dynamic>visitor) {
customPieTween = visitor(customPieTween, end, (dynamic value) {
return CustomPieTween(begin: value, end: end);
}) as CustomPieTween;
}

自定义CustomPieTween继承自Tween,重写lerp方法,对需要做动画的参数进行处理


class CustomPieTween extends Tween<List<PieChartModel>> {
CustomPieTween({List<PieChartModel>? begin, List<PieChartModel>? end})
: super(begin: begin, end: end);

@override
List<PieChartModel> lerp(double t) {
List<PieChartModel> list = [];
begin?.asMap().forEach((index, model) {
list.add(model
..radius = lerpDouble(model.radius, end?[index].radius ?? 100.0, t));
});
return list;
}

double lerpDouble(double radius, double radius2, double t) {
if (radius == radius2) {
return radius;
}
var d = (radius2 - radius) * t;
var value = radius + d;
return value;
}
}

完整代码


感兴趣的小伙伴可以直接看源码
GitHub:chart_view


作者:Eau_z
链接:https://juejin.cn/post/7098140878945927175
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

商业软件选型之困

以往,商业软件的选择是非常简单粗暴的。但是近些年,如果你为企业做过商业软件选型,那么就会明白,在科技飞速发展的现在,为企业挑选商业软件是非常困难且繁琐的。为什么会发生这样的变化呢?我们应当如何简化商业软件的挑选过程呢?PART 01 软件选择的复杂性不可否认,...
继续阅读 »

以往,商业软件的选择是非常简单粗暴的。但是近些年,如果你为企业做过商业软件选型,那么就会明白,在科技飞速发展的现在,为企业挑选商业软件是非常困难且繁琐的。为什么会发生这样的变化呢?我们应当如何简化商业软件的挑选过程呢?

PART 01 软件选择的复杂性

不可否认,为企业挑选商业软件是一个困难且耗时的过程。下面是当今社会出现这种变化的一些原因:

1、利基解决方案的出现

在过去的数十年中,我们见证了不同种类利基软件的爆发式增长。无论企业的需求是什么,大概率都会存在为此需求专门设计和构建的解决方案。甚至有可能专门为此类特定需求的软件创造冗长的名称和缩写。

例如,你知道CMMS(Computer Maintenance Management System,计算机维护管理系统)和EAM(Enterprise Asset Management,企业资产管理系统)软件在资产管理领域的差异吗?在大多数情况下,这其实是好现象。无论你的业务需求是什么,都可能有现成的软件方案能够解决这些问题。

更好的方面是,这些解决方案之间可能存在竞争,因此你可以不同的平台之间选择,因为此类平台都提供相似的功能。但是,这也为软件的选型决策带来困扰。

首先,你会为不同的业务需求选择不同的利基平台吗?你也可以选择能够提供多种功能的综合性平台。其次,你真的需要软件平台来处理小问题吗?还是说这些问题可以自己搞定?

2、海量的选择

软件开发是个利润丰厚的领域。如果你创建的软件平台满足了目标群体的实际需求,就会在经济层面得到丰厚的回报。这吸引了数百万企业家和开发者进入这个商业领域。反过来,这又为其他企业家创造了海量的商业机会。

即使你在寻找特定类型的平台,在开始搜寻的时候也会发现有大量的同类产品可供挑选。尽管我们倾向于更多的选择余地是个好事情,但这最终会使我们的选择过程陷入困境,并使我们对最终决定并不十分满意。

3、苹果和橙子的比较

如果有两个平台以完全迥异的方式实现了相同的目标,我们将如何比较他们呢?如果两个平台在功能上一致,但是其中一个的售价低20%,那么从两者中选择价格优惠的平台,是显而易见的英明决策。然而,如果它们有完全不同的使用体验、功能和报价标准呢?就如同将苹果与橙子进行比较,我们无法确认哪个是更好的选择。

4、官僚机构和委员会的决定

由于根深蒂固的官僚主义会影响委员会做出的决定,因此企业有时会将自己陷入异常困难和煎熬的决策过程。从某种程度来看,这是可以理解的。

为大型企业购买软件是一个重要的决策过程,因此避免过多的人员参与是很有必要的。当然,这也是一个能够影响很多部门和人员的决定。因此,这个决定不应当由领导者独自决策。然而,没有十全十美的决定。大部分时候,集体决策会耗费更长的时间,且决策仍然会导致意料之外的结果,这是无法完全避免的。

5、安全性和潜在漏洞

企业需要评估引入新软件带来的安全风险和潜在漏洞,并制定应对措施。购买软件带来的这部分风险非常复杂,企业无法忽视它。许多企业现在都有专门的风险评估团队,他们的唯一工作就是评估与软件相关的潜在安全风险。

6、合约和法律问题

签订软件服务合同是让人非常伤脑筋的事情,尤其是当你将自己限定在一份为期三年的协议中时。尽管许多软件平台非常乐意用户选择订阅制协议,但是对于重要项目和特殊平台,制定严谨有效的合同仍然是非常必要的。进行合同审查,意味着项目流程需要消耗更多的时间。此外,合同的条款将会让你更加头疼。

7、未知因素:业务需求

或许你已经明确了未来几年内的业务需求。但是,从现在起的十年内,业务需求会发生哪些变化呢?你认为这个平台能够与你的业务同步成长吗?你能够预知自己的业务是否会发生根本性的改变,最终不再需要该平台吗?

8、未知因素:软件开发

当今的软件产品不断更新迭代。软件开发人员根据需要添加和移除功能,并升级UI以改进用户体验。你能确定这个软件朝着正确的方向发展吗?当然不能,这根本是无法预知的。

PART 02 如何让软件选择变得简单呢?

如果你十分努力地为企业挑选软件,可以参考以下步骤来让这个过程变得轻松一些。

1、从需求评估开始

在为企业采购之前,先进行需求评估是最好的办法。有太多的企业家和采购人员在需求很模糊的时候就冒险进入市场。他们认为四处逛逛就可以解决问题、识别痛点和聚焦解决方案类型。

然而,这最终可能会使事情复杂化,造成误会并引入之前未曾考虑到的新需求,这些新需求很可能并非真实需求。相反,在团队的真实需求中投入精力,并记录需求,然后寻找能够满足这些需求的解决方案,才是最快速有效的办法。

2、缩小决策范围

尽可能缩小你的决策范围。如果你为一个没有约束条件的特殊需求选择软件平台,那么你将被各种可能性所淹没。相反,尝试立即消除一些选项;例如,为自己设定一个严格的预算,就可以排除超出预算价格的软件。你是否只考虑具备特定功能的软件平台?

3、优化灵活性和适应性

尽可能优化灵活性和适应性。如果你在仅剩的两个软件之间犹豫不决,就选择灵活性和适应性更强的那个。因为未来充满不确定性,所以良好的灵活性和适应性能够最大程度地适应不确定性带来的变化。

4、尽可能在同类产品中进行挑选

基于区块链的平台并不完全一样,尽管它们是依托于相同类型的基础架构设计开发的。因此,不要假定所有给定的利基软件都拥有同等水平的性能表现,无论它们的页面和功能是什么样的。以公平和直观的方式对比不同的平台,这通常并不容易,也不太现实。我们需要做的是尽可能在同类产品之间进行比较和挑选。

5、寻找可信的开发者

与其只评估产品,不如评估开发团队及其产品理念。通常来说,为值得信赖和称职的开发人员投出一票,是非常明智的选择,软件平台的表现在此时反而是次要的。

观察项目的领导力以及团队成员的经验和技术水平。是否为产品做好了长远的规划?开发人员是否为自己的工作成果感到自豪?

为企业挑选软件很繁琐,但它不应该是流程上的噩梦。如果你运用这些策略并且愿意保持适应能力且持续学习软件领域的相关技能,你会得到更大的收获——对自己的选择自信起来。

翻译:仇凯

原文:https://readwrite.com/why-choosing-software-is-such-a-tough-decision-in-the-modern-era/

收起阅读 »

零侵入性:一个注解,优雅的实现循环重试功能

前言在实际工作中,重处理是一个非常常见的场景,比如:发送消息失败。调用远程服务失败。争抢锁失败。这些错误可能是因为网络波动造成的,等待过后重处理就能成功.通常来说,会用try/catch,while循环之类的语法来进行重处理,但是这样的做法缺乏统一性,并且不是...
继续阅读 »

前言

在实际工作中,重处理是一个非常常见的场景,比如:

  1. 发送消息失败。

  2. 调用远程服务失败。

  3. 争抢锁失败。

这些错误可能是因为网络波动造成的,等待过后重处理就能成功.通常来说,会用try/catch,while循环之类的语法来进行重处理,但是这样的做法缺乏统一性,并且不是很方便,要多写很多代码.然而spring-retry却可以通过注解,在不入侵原有业务逻辑代码的方式下,优雅的实现重处理功能.

一、@Retryable是什么?

spring系列的spring-retry是另一个实用程序模块,可以帮助我们以标准方式处理任何特定操作的重试。在spring-retry中,所有配置都是基于简单注释的。

二、使用步骤

1.POM依赖

<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>

2.启用@Retryable

@EnableRetry
@SpringBootApplication
public class HelloApplication {
  public static void main(String[] args) {
      SpringApplication.run(HelloApplication.class, args);
  }
}

3.在方法上添加@Retryable

import com.mail.elegant.service.TestRetryService;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import java.time.LocalTime;

@Service
public class TestRetryServiceImpl implements TestRetryService {
  @Override
  @Retryable(value = Exception.class,maxAttempts = 3,backoff = @Backoff(delay = 2000,multiplier = 1.5))
  public int test(int code) throws Exception{
      System.out.println("test被调用,时间:"+LocalTime.now());
        if (code==0){
            throw new Exception("情况不对头!");
        }
      System.out.println("test被调用,情况对头了!");
      return 200;
  }
}

来简单解释一下注解中几个参数的含义:

  1. value:抛出指定异常才会重试

  2. include:和value一样,默认为空,当exclude也为空时,默认所有异常

  3. exclude:指定不处理的异常

  4. maxAttempts:最大重试次数,默认3次

  5. backoff:重试等待策略,默认使用@Backoff,@Backoff的value默认为1000L,我们设置为2000L;multiplier(指定延迟倍数)默认为0,表示固定暂停1秒后进行重试,如果把multiplier设置为1.5,则第一次重试为2秒,第二次为3秒,第三次为4.5秒。

当重试耗尽时还是失败,会出现什么情况呢?

当重试耗尽时,RetryOperations可以将控制传递给另一个回调,即RecoveryCallback。Spring-Retry还提供了@Recover注解,用于@Retryable重试失败后处理方法。如果不需要回调方法,可以直接不写回调方法,那么实现的效果是,重试次数完了后,如果还是没成功没符合业务判断,就抛出异常。

4.@Recover

@Recover
public int recover(Exception e, int code){
System.out.println("回调方法执行!!!!");
//记日志到数据库 或者调用其余的方法
  return 400;
}

可以看到传参里面写的是 Exception e,这个是作为回调的接头暗号(重试次数用完了,还是失败,我们抛出这个Exception e通知触发这个回调方法)。对于@Recover注解的方法,需要特别注意的是:

  1. 方法的返回值必须与@Retryable方法一致

  2. 方法的第一个参数,必须是Throwable类型的,建议是与@Retryable配置的异常一致,其他的参数,需要哪个参数,写进去就可以了(@Recover方法中有的)

  3. 该回调方法与重试方法写在同一个实现类里面

5. 注意事项

  1. 由于是基于AOP实现,所以不支持类里自调用方法

  2. 如果重试失败需要给@Recover注解的方法做后续处理,那这个重试的方法不能有返回值,只能是void

  3. 方法内不能使用try catch,只能往外抛异常

  4. @Recover注解来开启重试失败后调用的方法(注意,需跟重处理方法在同一个类中),此注解注释的方法参数一定要是@Retryable抛出的异常,否则无法识别,可以在该方法中进行日志处理。

总结

本篇主要简单介绍了Springboot中的Retryable的使用,主要的适用场景和注意事项,当需要重试的时候还是很有用的。

作者:Memory小峰

来源:blog.csdn.net/h254931252/article/details/109257998

收起阅读 »

与10倍开发者共事两年:他永远是对的,而我倍受煎熬

与 10 倍开发者共事,足以改变自己的职业生涯。  最近,我在网上看到不少关于 10 倍开发者的讨论。有些人想要成为这样的人,也有些人想远离这样的人。但在此之前,我们可能先要弄清楚这样一个问题:10 倍开发者真的存在、只是传说,或者仅仅是人们由于相对认知而感受...
继续阅读 »

与 10 倍开发者共事,足以改变自己的职业生涯。

  最近,我在网上看到不少关于 10 倍开发者的讨论。有些人想要成为这样的人,也有些人想远离这样的人。但在此之前,我们可能先要弄清楚这样一个问题:10 倍开发者真的存在、只是传说,或者仅仅是人们由于相对认知而感受到的概念?

  在得出结论之前,我想先给大家讲讲自己的经历。

与 10 倍开发者共事

  大约十年之前,公司的软件开发总监雇佣了一名三级软件工程师,我们都叫他 Gary。大概在同一时期,我们还雇用了一位名叫 Mitch 的二级软件工程师。最初几个月里,Gary 非常安静,总是一个人待着,努力解决一个个纯技术性的问题。当时我们的任务,是为实时 3D 机械训练软件制作空气等流体的流动动画。公司里的每个人都一直希望实现这个效果,但由于种种挑战,始终未能达成。而 Gary,成为帮助我们冲击难关的英雄。

  在他准备把功能提交给 QA 进行审查时,整个功能的观感至少比我想象中还要好,性能也超出预期,并拥有数千项单元测试断言作为支持。相比之下,我们的主体代码库根本就没经受过这么全面的测试。不用说了,各级管理人员都对这项睽违已久的功能感到非常满意。

我们的代码中有很多庞大,而且复杂得让人害怕的部分。

  不久之后,Gary 又组织了一次工程展示。展示内容主要集中在架构层面,即围绕对象生命周期、依赖倒置、ad-hoc 生命周期 / 明确限定范围的对象、某些分配反模式的危害、有碍单元测试覆盖的代码耦合,以及这些因素与很多内部工程问题之间的关联等等。这次展示让与会者们感到困惑,甚至感到颇为尴尬。毕竟这一切赤裸裸的批评,指向的正是那些最早加入公司、并一路构建起知识产权体系的老员工。

  我们的技术债务问题确实严重,但……也没有那么严重。虽然不会影响到生产力,但我们的代码中确实有很多庞大、而且复杂得让人害怕的部分。Gary 要做的正是揭露出这一切。但我们压力很大,因为我们每个人都是他提出的问题中的一部分。

他对现代软件设计的理解领先我们好几年。

  这个人的特点是,他永远是对的。不只是在争论当中,也包括在各种判断当中,他更像是个全知全能的神。虽然我一直以先弄清事实再发言的好习惯著称,但我也得承认,在整个共事期间我一共只揪出过他一到两次不太准确的表达。和这样的人共事压力很大,因为同事们总会发现一些自己本该了解、但却一无所知的重要知识。考虑到他们往往与 Gary 有着同样的职称和头衔,这就更让人感到无地自容。

  人性总有阴暗面,大家不喜欢那些特别聪明的人。特别是在对方提出的真知灼见既正确、又缺乏善意时,就更是让人不爽。所以同事们的普遍共识是,这家伙是个刻薄鬼。我个人并不觉得他是故意要让人难堪,但 Gary 在让人难堪这事上真的很有天赋。与此同时,他对现代软件设计的理解领先我们好几年,而这些心得还得在我们公司逐步实践,也许他觉得身边的同事真的让他很失望。

  公平地讲,我们沿用陈旧技术与方法是有原因的,而且也靠这些旧办法开发出了强大的产品。任何公司都可能存在类似的问题。

  Gary 强悍的技术实力加上对于敏捷流程的坚定拥护,最终挤走了雇用他的老领导,并由他自己上位。同事们震惊了一段时间,但很快就发现 Gary 主管带来了一系列令人兴奋的新变化。公司调整了自身产品各类,Mitch、我和另一位新任软件开发测试工程师(SDET)并纳入新团队中,尝试公司之前从未做过的工作。

  根据交流感受,Gary 一直以为我是二级软件工程师。但在发现我实际上只是一级时,他相当愤怒,并很快去找公司高层理论。几周之后,我就升职了。同样的,Mitch 虽然只是二级软件工程师,但他却拥有不逊于三级工程师的知识与技能。但没办法,他只能等……不知道在等什么,总之需要一段时间才能得到与自己水平相符的职称。

  有时候,Mitch 与 Gary 形影不离。我记得我们曾经花无数个小时在办公室里对未来新产品的架构设计组织头脑风暴与思维实验。到这个时候,我才意识到这两位的水平高到不知道哪里去了。有很长一段时间,他们两个人似乎开始用一种独特的语言交流。虽然他们之前从来没有协作过,但他们都认为公司内部缺少现代编程的基本概念。刚开始,人们不喜欢这两个人在那里说东说西;但事实证明,在他们碰头之后,两个人的编码效率确实高、质量也是真的稳定。

  我这个人比较擅长处理技术上的困难任务,Mitch 特别聪明,而 Gary 则拥有最强的编码质量。更让人稀奇的是,虽然 Gary 总是在全体大会和管理层会议中占用很长的时间,包括设计并记录新的标准流程、为各个开发者提供帮助与指导,但我到现在也不太确定他究竟是怎么在短时间内为公司带来这么显著的生产力提升的。总之在他的带领下,整个团队都不需要加班了,包括他自己。

让所有开发者拥有共同的价值观,是建立和谐团队与强大代码库的关键。

  尽管我已经有了几年的编程经验,但在 Gary 团队中度过的两年,绝对为我后续的高级开发者头衔奠定了良好的基础。他帮助我改掉了不少多年来养成的习惯——就是那种特别普遍,但并没什么用处,有时候甚至令人讨厌的习惯。相反,我们开始建立起更有前瞻性的视角,并积极使用先进工具与更高效的解决办法。而我从他身上学到的最重要一点,在于让所有开发者拥有共同的价值观,是建立和谐团队与强大代码库的关键。

  我们开发出的应用程序几乎没有缺陷,性能非常好、易于扩展,而且能够在之后的项目中重复使用。从各个方面来看,这都是我在入职以来见证到的最令人振奋的技术成功。

如果这样的状况都不能给公司敲响警钟,那管理层就太失败了。

  如果各位读者朋友也是那种重视工作、热爱工作的人,应该也曾被企业内的政治问题折磨得发狂。我怀疑 Gary 也是因为这个才决定离职,因为当时他并没有跳槽的打算。Mitch 在之后不到一年也选择离开,同样没有什么跳槽计划。两位最具才华的员工选择裸辞,这绝对是个强烈的信号。如果这样的状况都不能给公司敲响警钟,那管理层就太失败了——或者说,他们已经陷入了更大的问题当中。

  Gary 给我的临别忠告是,“你需要多多表达自己。”回顾我们一起奋斗的那段时间,Gary 和 Mitch 都特别善于表达自己,他们有时候甚至不给我说话的余地。但只要把话筒交给我,我说出来的就一定会是有意义的东西。在他们的引导下,我意识到这确实非常重要。

  我必须快速成长,帮助填补他们离去后留下的空白。虽然我的工作绩效同样非常出色,但最终我也离开了这家公司。我在这里度过了一段黄金岁月,也感激这家公司帮助我开启了职业生涯。但离别终有时,大家没必要总是强绑在一起。

  几年之后,我仍然在把自己从 Gary 身上学到的价值观带到其他岗位上,也努力让自己成为一个善于表达的人。事实证明,这种价值观确实让我在其他公司里也获得了尊重与广阔的发展空间。

要点汇总

  不知道大家在这个故事里有什么心得,下面我来谈谈自己的切身感受……

我们很难量化什么才是真正的 10 倍程序员,但这个问题其实没那么重要

真正重要的,是帮助你身边的人获得提升。

  有些人可能会争论某个同事到底是不是真正的 10 倍程序员。这样的 10 倍到底是在跟谁比?10 倍又具体体现在哪些方面?

  不少朋友都有过在一半的要求时间内完成了 4 倍工作量的经历,在项目中实现了更高的单元测试覆盖率以及更出色的代码质量,总体产出可以达到其他初级开发者的 10 倍以上等等。有时候,与具有一定经验的同行竞争时,您可能也凭借着更少的技术债务或者更强的特定领域专业知识达成了类似的优势。

  但这一切终究会被慢慢抹平,大家会凭借类似的从业经验、使用相同的工具、基于同一套代码库、以相同的理念 / 流程 / 设计模式处理同样的技术债务。在这样的前提下,开发者之间的效率仍有区别,但恐怕绝不可能有 10 倍那么夸张。

  问题的关键并不在于比其他人更强,而是帮助你身边的人获得提升。出色的开发者没有理由用自己的优势来打击其他同事,最重要的是为他人提供指导、发现阻碍生产力进步的因素、解决问题并防止其再次发生。这样,我们就能拥有一支真正有战斗力的队伍,而不只是围绕着一位开发明星原地打转。

成为专家,还是培养自己的专业性

自满实际是在沉默当中寻找安全感。

  我们不该因为某人出于长久以来的习惯、使用得到广泛证明的标准与既定技术,并由此以毫无追求的安全方法完成功能实现就对其横加指责。结合自己的经历,Gary 当初眼中的我们就像是这样一群业余爱好者。他不太注意自己的态度,只是他希望整个团队成长为软件开发专家的心情完全可以理解。

  但请千万不要忘记,其他人也是人,人总是有着种种缺陷。Gary 也是这样,他在第 100 次看到同样的错误时肯定要发脾气;只是这样的错误对其他人来讲属于“正常现象”。失去耐心的同时,你也失去了对同事们应有的尊重,这本身就是对专业性的践踏。

  软件领域的专业性像是一条微妙的线,我们不能随时越界,但在看到需要纠正的系统性问题时也不应视而不见。在此期间,你可能会引发混乱、可能会树敌,甚至威胁到自己的这只饭碗……但自满实际上是在沉默中寻找安全感。

  如果希望改变,请在社交层面找到完美的平衡点。要用心挑选提出建议的契机,更要用心挑选提出建议时的用语。

重视实践、技术与理念

如果能够做到,这一切将改变你的职业生涯。

  • 这些东西并不能保证把工作效率提升 10 倍。但我可以保证,只要培养起这样的能力,您会对软件开发拥有更加深刻的理解。

  • 严格遵循 SOLID 设计原则

  • 使用 MVC 模式进一步分离关注点

  • 命令查询职责分离

  • 通过实时代码覆盖工具完成单元测试覆盖

  • 使用行为驱动型开发发现需求细节,同时实现 UI 测试自动化

  • 明确定义并强制实施“已确认的定义”

  • 代码质量与分支策略,借此保证源代码控制系统拥有良好的清洁度与性能

  • 拥抱敏捷理念,但不必被动接受 SCRUM 中强调的一切流程

  在职业生涯中亲身实践这些目标并不容易,毕竟每个人都已经在成长过程中积累起了自己的一套工作方式。但如果能够做到,这一切将改变你的职业生涯。

10 倍程序员的背后,可能代表着 10 倍错误

这类开发者的根本问题,在于他们的顶头上司。

  公司里还有一位与众不同的开发者,我们叫他 James。从某种意义上说,他在公司已经拥有相当丰富的资历,非常擅长处理一部分编程任务。但他不愿意为自己的错误负责,经理多次批评还是无济于事。

  最要命的是,其他人的大部分工作都处于 James 团队开发成果的下游。所以如果他弄错了,每个人都能感觉到;而如果别人弄错了,对他几乎没有影响。这就是上下游依赖关系的基本特征,要求上游一方必须拥有强大的责任心。

  那么,为什么会陷入这么糟糕的状况呢?因为这位牛仔不相信单元测试,觉得这纯粹是在“浪费时间”,但其他人需要为他的武断买单。此外,他会反复把有问题的代码(包括无法编译或者存在严重阻塞问题的代码)添加到其他人正在使用的分支中,搞得公司内部民怨沸腾。

  这类开发者的根本问题,在于他们的顶头上司。这帮管理者没有建立良好的实践,甚至把这种独行侠式的坏习惯视为理所当然。

写在最后

  我觉得这个世界上的 10 倍开发者也分好几种,有自私型的、有乐于助人型的、有平易近人型的,也有令人生畏型的。如果大家有天分能够加入 10 倍开发者阵营,希望各位能认真选择自己想成为哪一种类型。

来源:http://k.sina.com.cn/article_1746173800_68147f6802700xlnz.html

收起阅读 »

我是一位10倍速开发者,但却感到很孤独

近日,有位网友(@100011_100001,下称11)在HN上倾诉了自己的烦恼。据11的描述,他是一个相当优秀的开发者,但他感觉很孤独。以下是11的自述内容:我是一个10x developer(10倍效率的开发者,简称10倍速开发者),但我讨厌这种称呼。如今...
继续阅读 »

当一个人在从事的领域能力越出众、站得越高,那“高处不胜寒”的感觉就会越强烈。就像搜狐创始人张朝阳在一次采访中说的:“我是真的什么都有,想有什么我都可以买,但是我居然这么痛苦。”

本以为商界大佬的这种苦恼无人能懂,但没想到在技术界,也有程序员在经历着能力出众带来的孤独感。

近日,有位网友(@100011_100001,下称11)在HN上倾诉了自己的烦恼。据11的描述,他是一个相当优秀的开发者,但他感觉很孤独。以下是11的自述内容:

1 “从事开发十年,我在团队中的地位越来越高”

我是一个10x developer(10倍效率的开发者,简称10倍速开发者),但我讨厌这种称呼。

故事开始并不是这样的。我在33岁的时候成为一名小开发者,人们一直认为我比看起来更有经验。我不确定是我的生活经历还是对自我完善的不懈追求,我一直在不断提高自己的能力。


大约在我38岁的时候,我觉得自己有了一定的能力。因为我的代码质量越来越高,只是速度相对会慢一点。这时,我受到了产品负责人的夸赞:“虽然你要多花些时间来完成任务,但我对你的完成质量完全放心。”

如今,我已经42岁了。现在我已能实现在代码只有极少Bug的情况下快速完成任务,而且工作时间也是标准的朝八晚五。当然,我现在参与了更多的会议、架构讨论、前沿的概念验证等等,但这并没有让我慢下来,反而使我的速度更快。

我从未打算成为一个超强的开发者,但在过去几年,我明显注意到了周围人对我的需求。比如,技术负责人邀请我参加会议,并让我发表看法;从未和我合作过的程序员也给我打电话,只因为“你可能知道答案”;我还被要求参与其他部门的代码审查。最近我在参加会议时发现,就连之前从未和我有过交流的人都知道我,甚至我的经理在介绍我时也只说 :“这是X,你可能听说过他。”

2 “10人团队,我一人完成71%的工作”

有人做了一个调查,看看我所在的应用程序组(大约350个开发者)按用户划分的git提交数量。结果发现,我写的一个执行各种自动化任务的脚本是第一名,我是第二名。这让我很吃惊,也让我无法压制住一些想法和感受,因此有了这篇长文。

虽然我经常提交、审查和合并代码,但我并不认为git提交的数量能证明什么。为了找到更具体的例子来证明我的“疏离感”,我查看了一下Jira故事和故事点。事实证明,在我的10人团队中(包括我自己),我在2022年完成了所有故事点的71%,其他9人负责另外29%,这与我的git提交数量相比也是一致的。

当然,我说这个话题的重点并不是为了吹牛(如果我有这种意思,我道歉)。我想表达的是,这种处境让我很孤独,很有压力。因为在这之前,我的一些决定还会受到质疑,这让我觉得很感激,因为质疑会让我创造出更好的解决方案。而现在,人们只是接受我说的任何东西,并认为我的方法是最好的。

在这种处境下,我感觉自己没有同伴,我只是在拖着我的整个团队和我周围的人一起走。这让我压力很大,感觉有些事情如果我不做,它就不会被完成,同时我也担心自己会因为受不到挑战而变得自满。


可怕的是,我发现自己有很大的控制权,因为团队负责的应用程序中,大约80%的代码库都是我编写的代码。我认为这明显不是一件好事,但他们好像一点也不以为意。最糟糕的是,我内心深处产生了一种挫败感,因为团队其他人的行动似乎都很缓慢。

总结一下,我是一个非常优秀的开发者,我喜欢写代码。然而我觉得自己很孤独,也害怕自己会对自身的能力而感到自负。我是唯一有这种感觉的人吗?我可以做些什么来改变现状?

3 另寻出路还是帮助团队进步?

对于11的烦恼,网友@ctvo的分析获得了最多的点赞:“我认为你是一个高于平均水平的开发者,在一家低于平均水平的公司工作。这可能是你所有问题的根源。你明知道同事没有负重前行,但还是要和他们一起工作,这实在是让人很沮丧。无论如何,我认为你不适合你目前的公司及其文化。”

网友@elviejo称:“如果你是房间里最聪明的人,那么你就进错了房间。但说真的,如果你喜欢编码,那你或许应该换一家公司,寻找更难的挑战。”


还有网友认为,11已经在多年的编码工作上取得了太多的成就,听完他的自述,明显是到了需要过渡的地步。完全可以减少编码工作,开始更多的教学

网友@symby:“我不认为找一份新工作可以解决问题,我建议你不要再自己做那么多贡献,而是开始帮助别人让他们做出贡献。‘大树底下长不出大树’,或许正是你远超他人的能力投射成了阴影,使你的同事难以成长?10倍速开发者很了不起,但很难被复刻,最好是让自己同事的能力也变强,扩大团队的产出。”

最后,你有过因为能力太强导致自己在团队里被过分“依赖”的经历吗?对这种因为“过于优秀”而产生的孤独感,你又有什么好的建议?欢迎在评论区留言~

参考链接:

  • news.ycombinator.com/item?id=31438426

来源:mp.weixin.qq.com/s/vljoD9c6-m7Jx9toGI5wog

收起阅读 »

Three.js控制物体显示与隐藏的方法

本文会讲解一下Three.js控制物体显示与隐藏的方法,主要包括以下几种方式:visible属性;layers属性。下面会分别通过简单的例子介绍下上述几个方式的简单使用方法和一些它们之间的区别。如果没有特殊说明,下面的源码以 r105 版本...
继续阅读 »

本文会讲解一下Three.js控制物体显示与隐藏的方法,主要包括以下几种方式:

  1. visible属性;
  2. layers属性。

下面会分别通过简单的例子介绍下上述几个方式的简单使用方法和一些它们之间的区别。如果没有特殊说明,下面的源码以 r105 版本为例:

visible属性

visible 是Object3D的属性。只有当 visible 是 true 的时候,该物体才会被渲染。任何继承 Object3D 的对象都可以通过该属性去控制它的显示与否,比如:MeshGroupSpriteLight等。

举个简单的例子:

// 控制单个物体的显示和隐藏
const geometry = new THREE.PlaneGeometry(1, 1) // 1*1的一个平面
const planeMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 }) // 红色平面
const plane = new THREE.Mesh(geometry, planeMaterial)
plane.visible = false // 不显示单个物体
scene.add(plane)
// 控制一组物体的显示和隐藏
const geometry = new THREE.PlaneGeometry(1, 1)
const planeMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
const plane = new THREE.Mesh(geometry, planeMaterial)
const group = new THREE.Group()
group.add(plane)
group.visible = false // 不显示一组物体
scene.add(group)

通过后面的例子可以看出,当我们想要控制一组物体的显示与隐藏,可以把这些物体放入一个 Group 中,只通过控制 Group 的显示与隐藏即可。

这块的代码逻辑是在WebGLRenderer.js的 projectObject 方法中实现的。

首先,在 render 方法中调用了 projectObject 方法:

this.render = function ( scene, camera ) {
// ...
projectObject( scene, camera, 0, _this.sortObjects );
// ...
}

projectObject 方法的定义如下:

function projectObject( object, camera, groupOrder, sortObjects ) {
if ( object.visible === false ) return; // 注释1:visible属性是false直接返回
// ...
var children = object.children; // 注释2:递归应用在children上

for ( var i = 0, l = children.length; i < l; i ++ ) {

projectObject( children[ i ], camera, groupOrder, sortObjects ); // 注释2:递归应用在children上

}
}

从注释1可以看出,如果 Group 的 visible 是 false,那么就不会在 children 上递归调用,所以就能达到通过 Group 控制一组对象的显示与隐藏的效果。

当 visible 是 false 的时候,Raycaster 的 intersectObject 或者 intersectObjects 也不会把该物体考虑在内。这块的代码逻辑是在 Raycaster.js

intersectObject: function ( object, recursive, optionalTarget ) {
// ...
intersectObject( object, this, intersects, recursive ); // 注释1:调用了公共方法intersectObject
// ...
},

intersectObjects: function ( objects, recursive, optionalTarget ) {
// ...

for ( var i = 0, l = objects.length; i < l; i ++ ) {

intersectObject( objects[ i ], this, intersects, recursive ); // 注释1:循环调用了公共方法intersectObject

}
// ...
}

// 注释1:公共方法intersectObject
function intersectObject( object, raycaster, intersects, recursive ) {

if ( object.visible === false ) return; // 注释1:如果visible是false,直接return

// ...
}

从注释1可以看出,如果 Group 或者单个物体的 visible 是 false ,就不做检测了。

layers属性

Object3D的layers属性 是一个 Layers 对象。任何继承 Object3D 的对象都有这个属性,比如 Camera 。Raycaster 虽然不是继承自 Object3D ,但它同样有 layers 属性(r113版本以上)。

和上面的 visible 属性一样,layers 属性同样可以控制物体的显示与隐藏、Raycaster 的行为。当物体和相机至少有一个同样的层的时候,物体就可见,否则不可见。同样,当物体和 Raycaster 至少有一个同样的层的时候,才会进行是否相交的测试。这里,强调了是至少有一个,是因为 Layers 可以设置多个层。

Layers 一共可以表示 32 个层,0 到 31 层。内部表示为:




Layers 可以设置同时拥有多个层:

  1. 可以通过 Layers 的 enable 和 disable 方法开启和关闭当前层,参数是上面表格中的 0 到 31 。
  2. 可以通过 Layers 的 set 方法 只开启 当前层,参数是上述表格中的 0 到 31
  3. 可以通过 Layers 的 test 的方法判断两个 Layers 对象是否存在 至少一个公共层 。

当开启多个层的时候,其实就是上述表格中的二进制进行 按位或 操作。比如 同时 开启 0231 层,那么内部存储的值就是 10000000000000000000000000000101

layers 属性默认只开启 0 层。

还是上面那个例子,我们看下怎么控制物体的显示和隐藏:

// 控制单个物体的显示和隐藏
const geometry = new THREE.PlaneGeometry(1, 1)
const planeMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
const plane = new THREE.Mesh(geometry, planeMaterial)
plane.layers.set(1) // 设置平面只有第1层,相机默认是在第0层,所以该物体不会显示出来
scene.add(plane)
// 控制一组物体的显示和隐藏
const geometry = new THREE.PlaneGeometry(1, 1)
const planeMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
const plane = new THREE.Mesh(geometry, planeMaterial)
const group = new THREE.Group()
group.layers.set(1) // 注释1: 设置group只有第一层,相机默认是在第0层,但是此时平面物体还是显示出来了?
group.add(plane)
scene.add(group)

设置单个物体的 layer 可以看到物体成功的没有显示出来。但是,当我们给 group 设置 layer 之后,发现 group 的 children(平面物体)还是显示了出来。那么,这是什么原因呢?让我们看下源码,同样还是上面的 projectObject 方法:

function projectObject( object, camera, groupOrder, sortObjects ) {

if ( object.visible === false ) return;

var visible = object.layers.test( camera.layers ); // 注释1:判断物体和相机是否存在一个公共层

if ( visible ) { // 注释1:如果存在,对物体进行下面的处理
// ...
}

var children = object.children; // 注释1:不管该物体是否和相机存在一个公共层,都会对children进行递归

for ( var i = 0, l = children.length; i < l; i ++ ) {

projectObject( children[ i ], camera, groupOrder, sortObjects );

}
}

从上述注释1可以看出,即使该物体和相机不存在公共层,也不影响该物体的 children 显示。这也就解释了上述为什么给 group 设置 layers ,但是平面物体还是能显示出来。从这一点上来看,layers 和 visible 属性在控制物体显示和隐藏的方面是不一样的。

和 visible 属性一样,接下来我们看下 Layers 对 Raycaster 的影响。同样我还是看了 Raycaster.js 文件,但是发现根本就没有 layers 字段。后来,我看了下最新版本 r140 的 Raycaster.js

function intersectObject( object, raycaster, intersects, recursive ) {

if ( object.layers.test( raycaster.layers ) ) { // 注释1:判断物体和Raycaster是否有公共层

object.raycast( raycaster, intersects );

}

if ( recursive === true ) { // 注释1:不管该物体和Raycaster是否有公共层,都不影响children

const children = object.children;

for ( let i = 0, l = children.length; i < l; i ++ ) {

intersectObject( children[ i ], raycaster, intersects, true );

}
}
}

不同于前面,visible 和 layers 都可以用来控制物体的显示与隐藏,visible 和 layers 只有一个可以用来控制 Raycaster 的行为,具体是哪一个生效,可以看下 Three.js的迁移指南

可以看到,从 r114 版本,废除了 visible ,开始使用 layers 控制 Raycaster 的行为:

r113 → r114
Raycaster honors now invisible 3D objects in intersection tests. Use the new property Raycaster.layers for selectively ignoring 3D objects during raycasting.

总结

从上面可以看出,visible 和 layers 在控制物体显示与隐藏、Raycaster 是否进行等方面是存在差异的。

当该物体的 visible 属性为 false 并且 layers 属性测试失败的时候,行为总结如下:


原文链接:https://segmentfault.com/a/1190000041881241

收起阅读 »

强制20天内开发APP后集体被裁,技术负责人怒用公司官微发文:祝“早日倒闭!”

身为一名程序员,你一定经历过为了准时上线项目而通宵敲代码,也一定经历过为了完善项目而连夜找BUG。需求、Deadline是代码之外,伴随程序员终身的两件大事。但当你因BUG被领导破口大骂、当你通宵达旦完成项目上线后却还被公司解雇,这时你会怎么做?近日,一家科技...
继续阅读 »

身为一名程序员,你一定经历过为了准时上线项目而通宵敲代码,也一定经历过为了完善项目而连夜找BUG。需求、Deadline是代码之外,伴随程序员终身的两件大事。

但当你因BUG被领导破口大骂、当你通宵达旦完成项目上线后却还被公司解雇,这时你会怎么做?

近日,一家科技公司的微信公众号发布了一篇祝公司“早日倒闭”的文章,疑似遭原技术团队的负责人盗号。以下为具体内容:

通宵加班,最终换来团队被集体解雇

文章中称,3月末,该团队被公司要求在20天内就开发出一款APP。但该负责人表示,稍微有点常识的人都知道,一款APP从设计到开发,最快也需要40天左右的时间。无奈之下,团队全员只能在居家办公、沟通不便的情况下天天通宵加班,最终在4月中旬开发出了新的APP。

在这么短时间内完成开发的产品,出现一部分BUG也是很正常的。“但是某李姓领导,在出现BUG后冲进我们的办公室,一通大骂,语言粗鄙不堪……”,“在修复BUG的过程中,李某不断施压,强制要求大家加班,有一次凌晨3点多给我打电话,一通指责……”

此外,该负责人还补充道,4月末,(李某)又要求增加一大堆狗屁不通的所谓新功能,比如im即时通讯功能要求在一周内完成。但负责人表示,这种自己开发的功能至少要两个月。

最后,团队负责人透露,他们努力了、付出了、熬夜了,最终换来的却是集体解雇


图源:微博

这篇文章发出后,经过不断地传播,阅读迅速达到了10万+。而后晚间,该文章才被删除。

公司回应:与事实严重不符,严重抹黑公司形象

舆论不断发酵,随后该公司连夜发布了回应声明:

  1. 该文章内容与事实严重不符,严重抹黑公司形象;

  2. 微信公众号文章发布后,已要求公司法务处理相关事宜,并与原技术取得沟通,现已妥善解决该问题;

  1. 因处理该问题需要时间,所以公众号文章未能及时删除,并非公司恶意炒作;

  1. 对各大平台及个人针对该文章进行断章取义的宣传,我公司保留追究其法律责任的权利;

  2. 我司对占用了大家的时间和公共资源表示歉意,请不要恶意转载。


图源:官方公众号

网友:“希望不要是什么营销手段”

目前,该公司回应“已妥善解决问题”,事实如何也无从得知。对此,网友表示:

  • “专业的事情一定要专业的人来做,如果让一个不懂行的人来弄,那肯定是乱套了。”

  • “记得我之前的领导经常这样说:‘不要告诉我过程,我要的是结果。’”

  • “人家辛辛苦苦研究出来了APP!然后遭到了解雇,有时候真的是不可想象。”

  • “如果是真的的话,公司也太卸磨杀驴了吧,打工人真的敢去吗?”

  • “如果真是被解雇了,是不是应该用法律武器来维护利益?”

最后,你对该事件有什么看法?

来源:程序人生(ID:coder_life)

收起阅读 »

源码阅读原则

不是绝对的,只是提供一种大致的思路大致的了解一个类、方法、字段所代表的含义明确你需要了解某个功能A的实现,越具体越好,列出切入点,然后从上至下的分析对于行数庞大、逻辑复杂的源码,我们在追踪时遇到非相关源码是必定的,可以简单追踪几个层级,给自己定一个界限,否则容...
继续阅读 »

源码阅读原则

不是绝对的,只是提供一种大致的思路

见名之意

大致的了解一个类、方法、字段所代表的含义

切入点

明确你需要了解某个功能A的实现,越具体越好,列出切入点,然后从上至下的分析

分支

对于行数庞大、逻辑复杂的源码,我们在追踪时遇到非相关源码是必定的,可以简单追踪几个层级,给自己定一个界限,否则容易丢失目标,淹没在源码的海洋中

分支字段

追踪有没有直接返回该字段的方法,通过方法注释,直接快速了解该字段的作用。

对于没有向外暴露的字段,我们追踪它的usage

  • 数量较少:可以通过各usage处的方法名大致了解,又或者是直接阅读源码

  • 数量较多:建议另辟蹊径,实在没办法再逐一攻破

分支方法

首先是阅读方法注释,有几种情况:

  • 涉及新术语:在类中搜索关键字找到相关方法或类

  • 涉及新的类:看分支类

  • 功能A相关:略过

分支类

先阅读理解类注释,有以下几种情况:

  • 涉及到新的领域:通过查看继承树的方式,大致了解它规模体系和作用

  • 不确定和功能A是否有关联:可查阅官方文档或者搜索引擎做确定

断点调试

动态分析的数据能够帮助我们去验证我们的理解是否正确,实践是检验真理的唯一标准

usage截止点

当你从某个方法出发,寻找它是在何处调用时,请记住你的目的,我们应该在脱离了强相关功能方法处截止,继续usage的意义不大。

比如RecyclerViewscrapOrRecycleView,我们的目的是:寻找什么时候触发了回收View

应该在onLayoutChildren处停止,再继续usage时,你的目的就变成了:寻找什么时候布置Adapter所有相关的子View


作者:土猫少侠
来源:juejin.cn/post/7100806273460863006

收起阅读 »

封装Kotlin协程请求,这一篇就够了

协程(coroutines)的封装在默认的Kotlin协程环境中,我们需要自定义协程的作用域CoroutineScope,还有负责维护协程的调度等等,有没有方法可以让协程的使用者屏蔽对底层协程的认识,简单就能使用呢?这里带来了一个封装思路。封装前例子假如我们有...
继续阅读 »

协程(coroutines)的封装

在默认的Kotlin协程环境中,我们需要自定义协程的作用域CoroutineScope,还有负责维护协程的调度等等,有没有方法可以让协程的使用者屏蔽对底层协程的认识,简单就能使用呢?这里带来了一个封装思路。

封装前例子

假如我们有个一个suspend函数

suspend fun getTest():String{
  delay(5000)
  return "11"
}

我们要实现的封装是:

1.执行suspend函数,并且使用者对底层无感知,无需了解协程就可以使用,这就要求我们屏蔽CoroutineScope细节

2.自动类型转换,比如返回String我们就应该可以在成功的回调中自动转型为String

3.成功的回调,失败的回调等

4.要不,来个DSL风格

//
async{
  // 请求
  getTest()

}.await(onSuccess = {
  //成功时回调,并且具有返回的类型
  Log.i("print",it)
},onError = {

},onComplete = {

})

可以看到,编译时就已经将it变为我们想要的类型了! 我们最终想要实现上面这种方式

封装开始

思路:我们自动对请求进行线程切换,使用Dispatchers即可,还有就是我们需要有监听的回调和DSL写法,所以就可以考虑用协程的async方式发起一个请求,返回值是Deferred类型,我们就可以使用扩展函数实现.await的形式!如果熟悉flutter的同学看的话,是不是很像我们的dio请求方式呢!下面是代码,可以根据更细节的需求进行补充噢:

fun <T> async(loader:suspend () ->T): Deferred<T> {
  val deferred = CoroutineScope(Dispatchers.IO).async {
      loader.invoke()
  }
  return deferred
}

fun <T> Deferred<T>.await(onSuccess:(T)->Unit,onError:(e:Exception)->Unit,onComplete:(()->Unit)?=null){
  CoroutineScope(Dispatchers.Main).launch {

      try{          
          val result = this@await.await()    
          onSuccess(result)
           
      }catch (e:Exception){
          onError(e)
      }
      finally {
          onComplete?.invoke()
      }
  }
}

总结

是不是非常好玩呢!我们实现了一个dio风格的请求,对于开发者来说,只需定义suspend修饰的函数,就可以无缝使用我们的请求框架!


作者:Pika
来源:https://juejin.cn/post/7100856445905666079

收起阅读 »

qiankun微前端

本文参考: 官网 你可能并不需要微前端什么是微前端?Techniques, strategies and recipes for building a modern web app with multiple teams that can ship fea...
继续阅读 »

本文参考
官网
你可能并不需要微前端

什么是微前端?

Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. -- Micro Frontends 微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。

qiankun是怎么来的?

所有的技术都是为了解决当前的现实问题,然后通过思考和实践创造出来的。微前端本质上是为了解决组织和团队间协作带来的沟通和管理的问题

引用微前端作者的思想:

微前端是康威定律在前端架构上的映射。 康威定律指导思想:既然沟通是大问题,那么就不要沟通就好了

作者认为大型系统都逃不过熵增定律,宇宙的本质,所有的东西都会从有序走向无序。一个东西如果你不去管理,他就会变成一坨垃圾,所以你想要维持一个东西的有序性,就要付出努力去维护他。所以从中找到平衡,qiankun就诞生了。通过分治的手段,让上帝的归上帝,凯撒的归凯撒

什么情况下使用qiankun?

我们在开发中可能会碰到下面的问题

  • 旧的系统不能下,新的需求还在来

  • 公司内部有很多的系统,不同系统间可能需要展示同一个页面

  • 一个系统过于庞大,每个人分别管理一个模块,git分支比较混乱。想要把系统拆分开来

微前端首先解决的,是如何解构巨石应用

核心价值:技术栈无关,应用之间不应该有任何直接或间接的技术栈、依赖、以及实现上的耦合。

作者认为正确的微前端方案的目标应该是

方案上跟使用 iframe 做微前端一样简单,同时又解决了 iframe 带来的各种体验上的问题

qiankun的原理

qiankun 是一个基于 single-spa微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。

qiankun框架内部fetch请求资源,解析出js、css文件和HTML document,插入到主应用指定的容器中(使用HTML Entry接入方式)

  1. 调用import-html-entry模块的importEntry函数,获取到对应子应用的html文件、可执行脚本文件以及publicpath

  2. 调用getDefaultTplWrapper将子应用的html内容用div标签包裹起来

  3. 调用createElement函数生成剔除html、body、head标签后的子应用html内容(通过innerHTML达到过滤效果)

  4. 调用getRender函数得到render函数(所以子应用一定要有render函数)

  5. 调用第4步得到的render,将container内部清空,并将子应用的dom元素渲染到指定的contanter元素上

  6. 调用getAppWrapperGetter函数,生成一个可以获取处理过的子应用dom元素的函数initialAppWrapperGetter,以备后续使用子应用dom元素

  7. 如果sandbox为true,则调用createSandboxContainer函数

  8. 执行execScripts函数,执行子应用脚本

  9. 执行getMicroAppStateActions函数,获取onGlobalStateChange、setGlobalState、offGlobalStateChange,用于主子应用传递信息

  10. 执行parcelConfigGetter函数,包装mount和unmount

上述步骤的源码

qiankun如何实现隔离?

沙箱隔离

qiankun的沙箱有2种 JS沙箱 和 CSS沙箱

JS沙箱

JS沙箱又分为2种,快照沙箱(为了兼容IE)和 代理沙箱

快照沙箱 snapshotSandbox

基于diff实现,用来兼容不支持Proxy的浏览器,只适用单个子应用。会污染全局window

  1. 激活沙箱:将主应用window的信息存到windowSnapshot

  2. 根据

    modifyPropMap

    ,恢复为子应用的window信息

    读取和修改的是window中的数据,windowSnapshot是缓存的数据

  3. 退出沙箱:根据windowSnapshot把window恢复为主应用数据,将windowSnapshot和window进行diff,将变更的值存到modifyPropMap中,然后把window恢复为主应用数据

总结

  • windowSnapshot主应用的window信息

  • modifyPropMap子应用修改的window信息

相对应的源码

代理沙箱

代理沙箱也分为2种,单例和多例,都是由Proxy实现

单例沙箱 legacySandbox

为了兼容性 singular 模式下依旧使用该沙箱,等新沙箱稳定之后再切换。 创建 addedPropsMapInSandbox(沙箱期间新增的全局变量)、modifiedPropsOriginalValueMapInSandbox(沙箱期间更新的全局变量)、currentUpdatedPropsValueMap(持续记录更新的(新增和修改的)全局变量的 map 用于在任意时刻做 snapshot) 三个变量,前两个用来恢复主应用window,最后一个用来恢复子应用window。同样会污染window,但性能比快照沙箱稍好,不用遍历window

  1. 激活沙箱:根据currentUpdatedPropsValueMap还原子应用的window数据

  2. window只要变动,在

    currentUpdatedPropsValueMap

    中进行记录

    1. 判断addedPropsMapInSandbox中是否有对应 key 的记录,没有新增一条,有的话往下执行

    2. 判断modifiedPropsOriginalValueMapInSandbox中是否有对应 key 的记录,没有的话,记录从window中对应key/value,有的话继续往下执行

    3. 修改window对应的key/value

  3. 退出沙箱:根据addedPropsMapInSandboxmodifiedPropsOriginalValueMapInSandbox还原主应用的window信息

相对性的源码

多例沙箱 proxySandbox

主应用和子应用的window独立,不再共同维护一份window,终于JS沙箱也和qiankun微前端的思想统一了…实行了分治。不会污染全局window,支持多个子应用。

  1. 激活沙箱

  2. 取值,先从自己命名空间下的fakeWindow找key,没找到,找window

  3. 赋值,直接给自己命名空间下的fakeWindow赋值

  4. 退出沙箱

相对应的源码

CSS沙箱

严格沙箱 和 实验性沙箱

严格沙箱

在加载子应用时,添加strictStyleIsolation: true属性,会将整个子应用放到Shadow DOM内进行嵌入,完全隔离了主子应用


缺点:子应用中应用的一些弹框组件会因为找不到body而丢失

实验性沙箱

在加载子应用时,添加experimentalStyleIsolation: true属性,实现形式类似于vue中style标签中的scoped属性,qiankun会自动为子应用所有的样式增加后缀标签,如:div[data-qiankun=“xxx”],这里的XXX为注册子应用时name的值


缺点:子应用中应用的一些弹框组件会因为插入到了主应用到body而丢失样式

相对应的源码


作者:丙乙
来源:https://juejin.cn/post/7100825726424711204

收起阅读 »

v-for中diff算法

当没有key时获取新旧数组长度,取最短的数组(Math.min())进行比较,如果用长的数组进行比较,会发生越界错误以短数组进行for循环,从新旧数组各组一个值进行patch,如果内容一样就不进行更新,如果内容不一样,Vue源码会进行更深层次的比较,如果类型都...
继续阅读 »

当没有key时


获取新旧数组长度,取最短的数组(Math.min())进行比较,如果用长的数组进行比较,会发生越界错误


以短数组进行for循环,从新旧数组各组一个值进行patch,如果内容一样就不进行更新,如果内容不一样,Vue源码会进行更深层次的比较,如果类型都不一样的话,直接创建一个新类型,如果类型一样,值不同,就只更新值,效率会更高,当for循环完毕,新旧数组长度会进行比较,如果旧的长度大有新的长度,就会执行unmountChildren,删除多余的节点,如果新的长度大于旧的长度,就会执行mountChildren,创建新的节点

当有key时


第一步,从头部开始遍历


通过isSameVNodeType进行比较


如果type 和 key 都一样,继续遍历,如果不同,跳出循环,进入第二步

第二步,从尾部开始遍历


和第一步操作一致

如果不同,跳出循环进入第三步

第三步,果旧节点遍历完,依然有新的节点,就是添加节点操作,用一个null和新节点进行patch,n1为空值时,是添加



如果新节点遍历完了,旧节点还有就进入第四步

第四步,新节点遍历完毕,旧节点还有,就进行删除操作


第五步,如果是一个无序的节点,vue会从旧的节点里找到新的节点里相同的值并创建一个新的数组,根据key建立一个索引,找到了就放入新数组里,比较完之后,有多余的旧节点就删除,有没有比较过的新节点就添加


作者:啊哈呀呀呀呀
来源:juejin.cn/post/7100858461520560135

收起阅读 »

IP属地获取,前端获取用户位置信息

尝试获取用户的位置信息写在前面想要像一些平台那样显示用户的位置信息,例如某省市那样。那么这是如何做到的, 据说这个位置信息的准确性在通信网络运营商那里?先不管,先实践尝试下能不能获取。尝试一:navigator.geolocation尝试了使用 navigat...
继续阅读 »


尝试获取用户的位置信息

写在前面

想要像一些平台那样显示用户的位置信息,例如某省市那样。那么这是如何做到的, 据说这个位置信息的准确性在通信网络运营商那里?先不管,先实践尝试下能不能获取。

尝试一:navigator.geolocation

尝试了使用 navigator.geolocation,但未能成功拿到信息。

getGeolocation(){
 if ('geolocation' in navigator) {
   /* 地理位置服务可用 */
   console.log('地理位置服务可用')
   navigator.geolocation.getCurrentPosition(function (position) {
     console.dir('回调成功')
     console.dir(position) // 没有输出
     console.dir(position.coords.latitude, position.coords.longitude)
  }, function (error) {
     console.error(error)
  })
} else {
   /* 地理位置服务不可用 */
   console.error('地理位置服务可用')
}
}

尝试二:sohu 的接口

尝试使用 pv.sohu.com/cityjson?ie… 获取用户位置信息, 成功获取到信息,信息样本如下:

{"cip": "14.11.11.11", "cid": "440000", "cname": "广东省"}
// 需要做跨域处理
getIpAndAddressSohu(){
 // config 是配置对象,可按需设置,例如 responseType,headers 中设置 token 等
 const config = {
   headers: {
     Accept: 'application/json',
     'Content-Type': 'application/json;charset=UTF-8',
  },
}
 axios.get('/apiSohu/cityjson?ie=utf-8', config).then(res => {
   console.log(res.data) // var returnCitySN = {"cip": "14.23.44.50", "cid": "440000", "cname": "广东省"};
   const info = res.data.substring(19, res.data.length - 1)
   console.log(info) // {"cip": "14.23.44.50", "cid": "440000", "cname": "广东省"}
   this.ip = JSON.parse(info).cip
   this.address = JSON.parse(info).cname
})
}

调试的时候,做了跨域处理。

proxy: {
 '/apiSohu': {
   target: 'http://pv.sohu.com/', // localhost=>target
   changeOrigin: true,
   pathRewrite: {
   '/apiSohu': '/'
  }
},
}

下面是一张获取到位置信息的效果图:


尝试三:百度地图的接口

需要先引入百度地图依赖,有一个参数 ak 需要注意,这需要像管理方申请。例如下方这样

<script src="https://api.map.baidu.com/api?v=2.0&ak=3ufnnh6aD5CST"></script>
getLocation() { /*获取当前位置(浏览器定位)*/
const $this = this;
var geolocation = new BMap.Geolocation();//返回用户当前的位置
geolocation.getCurrentPosition(function (r) {
  if (this.getStatus() == BMAP_STATUS_SUCCESS) {
    $this.city = r.address.city;
    console.log(r.address) // {city: '广州市', city_code: 0, district: '', province: '广东省', street: '', …}
  }
});
}
function getLocationBaiduIp(){/*获取用户当前位置(ip定位)*/
function myFun(result){
  const cityName = result.name;
  console.log(result) // {center: O, level: 12, name: '广州市', code: 257}
}
var myCity = new BMap.LocalCity();
myCity.get(myFun);
}

成功用户的省市位置,以及经纬度坐标,但会先弹窗征求用户意见。



写在后面

尝试结果不太理想,sohu 的接口内部是咋实现的,这似乎没有弹起像下面那样的征询用户意见的提示。


而在 navigator.geolocation 和 BMap.Geolocation() 中是弹起了的。

用别人的接口总归是没多大意思,也不知道不用征求用户意见是咋实现的。

经实测 sohu 的接口和 new BMap.Geolocation() 都可以拿到用户的位置信息(省市、经纬度等)。

作者:灵扁扁

来源:https://juejin.cn/post/7100916925504421918

收起阅读 »

一种兼容、更小、易用的WEB字体API

如何使用 Google Fonts CSS API 有效地使用WEB字体?多年来,WEB字体技术发生了很多变化,过去在WEB中使用特殊字体的常用做法是图片或者Flash,这种借助图片或者Flash的实现方式不够灵活。随着 WEB 字体的出现,特别是 Googl...
继续阅读 »

如何使用 Google Fonts CSS API 有效地使用WEB字体?

多年来,WEB字体技术发生了很多变化,过去在WEB中使用特殊字体的常用做法是图片或者Flash,这种借助图片或者Flash的实现方式不够灵活。随着 WEB 字体的出现,特别是 Google Fonts CSS API 的普及,让在WEB中使用特殊字体变得简单、快速、灵活,当然更多的还是面向英文字体,对于做外贸或者英文网站的开发者来说是福音。

Google Fonts CSS API 在不断发展,以跟上WEB字体技术的变化。它从最初的价值主张——允许浏览器在所有使用API的网站上缓存常用字体,从而使网页加载更快,到现在已经有了很大的进步。现在不再是这样了,但API仍然提供了额外的优化方案,使网站加载迅速,字体工作性能更佳。

使用Google Fonts CSS API ,网站可以请求它需要的字体数据来保持它的CSS加载时间到最少,确保网站访问者可以尽可能快地加载内容。该API将以最佳的字体响应每个请求的web浏览器。

所有这一切都是通过在代码中包含一行 HTML 来实现的。

如何使用 Google Fonts CSS API

Google Fonts CSS API 文档很好地总结了它:

你不需要做任何编程;所要做的就是在 HTML 文档中添加一个特殊的样式表链接,然后在 CSS 样式中引用该字体。

需要做的最低限度是在 HTML 中包含一行,如下所示:

<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap" rel="stylesheet" />

复制代码

当从 API 请求字体时,可以指定想要的一个或多个系列,以及(可选)它们的权重、样式、子集和其他选项。然后 API 将通过以下两种方式之一处理请求:

  1. 如果请求使用 API 已有文件的通用参数,它会立即将 CSS 返回给用户,将定向到这些文件。

  2. 如果请求的字体带有 API 当前未缓存的参数,它将即时对字体进行子集化,使用 HarfBuzz 快速完成,并返回指向它们的 CSS。

字体文件可以很大,但不一定要很大

WEB 字体可以很大,在 WOFF2 中,仅一个 Noto Sans Japanese 的大小就几乎是 3.4MB ,将其下载给每一位用户将拖累页面加载时间。当每一毫秒都很重要并且每个字节都很宝贵时,需要确保只加载用户需要的数据。

Google Fonts CSS API 可以创建非常小的字体文件(称为子集),实时生成,只为用户提供网站所需的文本和样式。可以使用 text 参数请求特定字符,而不是提供整个字体。

<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap&text=RobtMn" rel="stylesheet" />

复制代码


CSS API 还自动为用户提供额外的WEB字体优化,无需设置任何 API 参数。该 API 将为用户提供已启用 unicode-range 的 CSS 文件(如果 Web 浏览器支持),因此只为网站需要的特定字符加载字体。

unicode-range CSS 描述符是一种现在可用于应对大字体下载的工具,这个 CSS 属性设置 @font-face 声明包含的 Unicode 字符范围。如果在页面上呈现这些字符之一,则下载该字体。这适用于所有类型的语言,因此可以采用包含拉丁文、希腊文或西里尔文字符的字体并制作更小的子集。在前面的图表中,可以看到如果必须加载所有这三个字符集,则将超过 600 个字形。


这也为 Web 启用了中文、日文和韩文 (CJK) 字体提供支持。在上图中,可以看到 CJK 字体覆盖的字符数是拉丁字符字体的 15-20 倍。 CJK 字体通常非常大,并且这些语言中的许多字符不像其他字体那样频繁使用。

使用 CSS API 和 unicode-range 可以减少大约 90% 的文件传输。使用 unicode-range 描述符,可以单独定义每个部分,并且只有在内容包含这些字符范围中的一个字符时才会下载每个切片。

例如只想在 Noto Sans JP 中设置单词 こんにちは ,则可以按照如下方式使用:

  • 自托管自己的 WOFF2 文件

  • 使用 CSS API 检索 WOFF2

  • 使用 CSS API 并将 text= 参数设置为 こんにちは


在此示例中,可以看到通过使用 CSS API,已经比自托管 WOFF2 字体节省了 97.5%,这要归功于 API 内置支持将大字体分隔到 unicode-range 中功能。通过更进一步并准确指定要显示的文本,可以进一步将字体大小减小到仅 CSS API 字体的 95.3% ,相当于比自托管字体小 99.9%

Google Fonts CSS API 将自动以用户浏览器支持的最小和最兼容格式提供字体。如果用户使用的是支持 WOFF2 的浏览器,API 将提供 WOFF2 中的字体,但如果他们使用的是旧版浏览器,API 将以该浏览器支持的格式提供字体。为了减少每个用户的文件大小,API 还会在不需要时从字体中删除数据。例如,将为浏览器不需要的用户删除提示数据。

使用 Google Fonts CSS API 让WEB字体面向未来

Google 字体团队还为新的 W3C 标准做出了贡献,这些标准继续创新网络字体技术,例如 WOFF2。当前的一个项目是增量字体传输,它允许用户在屏幕上使用字体文件时加载非常小的部分,并按需流式传输其余部分,超过了 unicode-range 的性能。当使用 WEB 字体API时,当用户在浏览器中可用时,就可以获得这些底层字体传输技术的优化改进。

这就是字体 API 的美妙之处:用户可以从每项新技术改进中受益,而无需对网站进行任何更改。新的WEB字体格式?没问题,新的浏览器或操作系统支持?它已经处理好了。因此,可以自由地专注于用户和内容,而不是陷入WEB字体维护的困境。

可变字体支持内置

可变字体是可以在多个轴之间存储一系列设计变化的字体文件,新版本的 Google Fonts CSS API 包括对它们的支持。添加一个额外的变化轴可以使字体具有新的灵活性,但它几乎可以使字体文件的大小增加一倍。

当 CSS API 请求更具体时,Google Fonts CSS API 可以仅提供网站所需的可变字体部分,以减少用户的下载大小。这使得可以为 WEB 使用可变字体,而不会导致页面加载时间过长。可以通过在轴上指定单个值或指定范围来执行此操作,甚至可以在一个请求中指定多个轴和多个字体系列, API 可以灵活地满足需求。

总结

Google Fonts CSS API 可帮助WEB提供以下字体:

  • 更兼容

  • 体积更小

  • 加载快速

  • 易于使用

有关 Google 字体的更多信息,请访问 fonts.google.com


作者:天行无忌
来源:juejin.cn/post/7100927964224700424

收起阅读 »

跟我学flutter:细细品Widget(五)Element

前言跟我学flutter系列:跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制跟我...
继续阅读 »

前言

跟我学flutter系列:
跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin
跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate
跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制

跟我学flutter:在国内如何发布自己的Plugin 或者 Package

跟我学flutter:Flutter雷达图表(一)如何使用kg_charts

跟我学flutter:细细品Widget(一)Widget&Element初识

跟我学flutter:细细品Widget(二)StatelessWidget&StatefulWidget

跟我学flutter:细细品Widget(三)ProxyWidget,InheritedWidget

跟我学flutter:细细品Widget(四)Widget 渲染过程 与 RenderObjectWidget

跟我学flutter:细细品Widget(五)Element

企业级篇目:
跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统
跟我学企业级flutter项目:dio网络框架增加公共请求参数&header
跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层
跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview
跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget


之前的文章都有简述Element,这篇将着重去讲Element
Widget是描述一个UI元素的配置数据,Element才真正代表屏幕显示元素


分类


在这里插入图片描述
如上图所示Element分为两类




  • ComponentElement : 组合类Element。这类Element主要用来组合其他更基础的Element,得到功能更加复杂的Element。开发时常用到的StatelessWidget和StatefulWidget相对应的Element:StatelessElement和StatefulElement,即属于ComponentElement。




  • RenderObjectElement : 渲染类Element,对应Renderer Widget,是框架最核心的Element。RenderObjectElement主要包括LeafRenderObjectElement(叶子无节点),SingleChildRenderObjectElement(单child),和MultiChildRenderObjectElement(多child)。




Element生命周期


Element有4种状态:initial,active,inactive,defunct。其对应的意义如下:



  • initial:初始状态,Element刚创建时就是该状态。

  • active:激活状态。此时Element的Parent已经通过mount将该Element插入Element Tree的指定的插槽处(Slot),Element此时随时可能显示在屏幕上。

  • inactive:未激活状态。当Widget Tree发生变化,Element对应的Widget发生变化,同时由于新旧Widget的Key或者的RunTimeType不匹配等原因导致该Element也被移除,因此该Element的状态变为未激活状态,被从屏幕上移除。并将该Element从Element Tree中移除,如果该Element有对应的RenderObject,还会将对应的RenderObject从Render Tree移除。但是,此Element还是有被复用的机会,例如通过GlobalKey进行复用。

  • defunct:失效状态。如果一个处于未激活状态的Element在当前帧动画结束时还是未被复用,此时会调用该Element的unmount函数,将Element的状态改为defunct,并对其中的资源进行清理。


Element4种状态间的转换关系如下图所示:


在这里插入图片描述


ComponentElement


在这里插入图片描述



State和StatefulElement是一一对应的,只有在初始化StatefulElement时,才会初始化对应的State并将其绑定到StatefulElement上



核心流程


一个Element的核心操作流程有,创建、更新、销毁三种,下面将分别介绍这三个流程。


创建


在这里插入图片描述
ComponentElement的创建起源与父Widget调用inflateWidget,然后通过mount将该Element挂载至Element Tree,并递归创建子节点。


更新


在这里插入图片描述
由父Element执行更新子节点的操作(updateChild),由于新旧Widget的类型和Key均未发生变化,因此触发了Element的更新操作,并通过performRebuild将更新操作传递下去。其核心函数updateChild之后会详细介绍。


销毁


在这里插入图片描述
由父Element或更上级的节点执行更新子节点的操作(updateChild),由于新旧Widget的类型或者Key发生变化,或者新Widget被移除,因此导致该Element被转为未激活状态,并被加入未激活列表,并在下一帧被失效。


核心函数



  • inflateWidget


Element inflateWidget(Widget newWidget, dynamic newSlot) {
final Key key = newWidget.key;
//复用GlobalKey对应的Element
if (key is GlobalKey) {
final Element newChild = _retakeInactiveElement(key, newWidget);
if (newChild != null) {
newChild._activateWithParent(this, newSlot);
final Element updatedChild = updateChild(newChild, newWidget, newSlot);
return updatedChild;
}
}
//创建Element,并挂载至Element Tree
final Element newChild = newWidget.createElement();
newChild.mount(this, newSlot);
return newChild;
}
复制代码


  1. 判断新Widget是否有GlobalKey,如果有GlobalKey,则从Inactive Elements列表中找到对应的Element并进行复用。(可能从树的另一个位置嫁接或重新激活)

  2. 无可复用Element,则根据新Widget创建对应的Element,并将其挂载至Element Tree。



  • mount


void mount(Element parent, dynamic newSlot) {
//更新_parent等属性,将元素加入Element Tree
_parent = parent;
_slot = newSlot;
_depth = _parent != null ? _parent.depth + 1 : 1;
_active = true;
if (parent != null) // Only assign ownership if the parent is non-null
_owner = parent.owner;
//注册GlobalKey
final Key key = widget.key;
if (key is GlobalKey) {
key._register(this);
}
_updateInheritance();
}
复制代码


  1. 将给Element加入Element Tree,更新_parent,_slot等树相关的属性。

  2. 如果新Widget有GlobalKey,将该Element注册进GlobalKey中,其作用下文会详细分析。

  3. ComponentElement的mount函数会调用_firstBuild函数,触发子Widget的创建和更新。



  • performRebuild


@override
void performRebuild()
{
//调用build函数,生成子Widget
Widget built;
built = build();
//根据新的子Widget更新子Element
_child = updateChild(_child, built, slot);
}
复制代码


  1. 调用build函数,生成子Widget。

  2. 根据新的子Widget更新子Element。



  • update


@mustCallSuper
void update(covariant Widget newWidget) {
_widget = newWidget;
}
复制代码


  1. 将对应的Widget更新为新的Widget。

  2. 在ComponentElement的各种子类中,还会调用rebuild函数触发对子Widget的重建。



  • updateChild


@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
if (newWidget == null) {
//新的Child Widget为null,则返回null;如果旧Child Widget,使其未激活
if (child != null)
deactivateChild(child);
return null;
}
Element newChild;
if (child != null) {
//新的Child Widget不为null,旧的Child Widget也不为null
bool hasSameSuperclass = true;
if (hasSameSuperclass && child.widget == newWidget) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
newChild = child;
} else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)){
//Key和RuntimeType相同,使用update更新
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
child.update(newWidget);
newChild = child;
} else {
//Key或RuntimeType不相同,使旧的Child Widget未激活,并对新的Child Widget使用inflateWidget
deactivateChild(child);
newChild = inflateWidget(newWidget, newSlot);
}
} else {
//新的Child Widget不为null,旧的Child Widget为null,对新的Child Widget使用inflateWidget
newChild = inflateWidget(newWidget, newSlot);
}

return newChild;
}
复制代码

根据新的子Widget,更新旧的子Element,或者得到新的子Element。
逻辑如下(伪代码):


if(newWidget == null){
if(Child == null){
return null;
}else{
移除旧的子Element,返回null
}
}else{
if(Child == null){
返回新Element
}else{
如果Widget能更新,更新旧的子Element,并返回之;否则创建新的子Element并返回。
}
}

复制代码

该逻辑概括如下:



  1. 如果newWidget为null,则返回null,同时如果有旧的子Element则移除之。

  2. 如果newWidget不为null,旧Child为null,则创建新的子Element,并返回之。

  3. 如果newWidget不为null,旧Child不为null,新旧子Widget的Key和RuntimeType等都相同,则调用update方法更新子Element并返回之。

  4. 如果newWidget不为null,旧Child不为null,新旧子Widget的Key和RuntimeType等不完全相同,则说明Widget Tree有变动,此时移除旧的子Element,并创建新的子Element,并返回之。


RenderObjectElement


RenderObjectElement同核心元素Widget及RenderObject之间的关系如下图所示:
在这里插入图片描述
如图:


RenderObjectElement持有Parent Element,但是不一定持有Child Element,有可能无Child Element,有可能持有一个Child Element(Child),有可能持有多个Child Element(Children)。
RenderObjectElement持有对应的Widget和RenderObject,将Widget、RenderObject串联起来,实现了Widget、Element、RenderObject之间的绑定。


核心流程


如ComponentElement一样,RenderObjectElement的核心操作流程有,创建、更新、销毁三种,接下来会详细介绍这三种流程。



  • 创建


-在这里插入图片描述


RenderObjectElement的创建流程和ComponentElement的创建流程基本一致,其最大的区别是ComponentElement在mount后,会调用build来创建子Widget,而RenderObjectElement则是create和attach其RenderObject。



  • 更新


在这里插入图片描述
RenderObjectElement的更新流程和ComponentElement的更新流程也基本一致,其最大的区别是ComponentElement的update函数会调用build函数,重新触发子Widget的构建,而RenderObjectElement则是调用updateRenderObject对绑定的RenderObject进行更新。



  • 销毁


在这里插入图片描述
RenderObjectElement的销毁流程和ComponentElement的销毁流程也基本一致。也是由父Element或更上级的节点执行更新子节点的操作(updateChild),导致该Element被停用,并被加入未激活列表,并在下一帧被失效。其不一样的地方是在unmount Element的时候,会调用didUnmountRenderObject失效对应的RenderObject。


核心函数



  • inflateWidget


该函数和ComponentElement的inflateWidget函数完全一致,此处不再复述。



  • mount


void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_renderObject = widget.createRenderObject(this);
attachRenderObject(newSlot);
_dirty = false;
}
复制代码

该函数的调用时机和ComponentElement的一致,当Element第一次被插入Element Tree的时候,该方法被调用。其主要职责也和ComponentElement的一致,此处只列举不一样的职责,职责如下:



  1. 调用createRenderObject创建RenderObject,并使用attachRenderObject将RenderObject关联到Element上。

  2. SingleChildRenderObjectElement会调用updateChild更新子节点,MultiChildRenderObjectElement会调用每个子节点的inflateWidget重建所有子Widget。



  • performRebuild


@override
void performRebuild()
{
//更新renderObject
widget.updateRenderObject(this, renderObject);
_dirty = false;
}
复制代码

performRebuild的主要职责如下:


调用updateRenderObject更新对应的RenderObject。



  • update


@override
void update(covariant RenderObjectWidget newWidget) {
super.update(newWidget);
widget.updateRenderObject(this, renderObject);
_dirty = false;
}
复制代码

update的主要职责如下:



  1. 将对应的Widget更新为新的Widget。

  2. 调用updateRenderObject更新对应的RenderObject。



  • updateChild


@protected
List updateChildren(List oldChildren, List newWidgets, { Set forgottenChildren }) {
int newChildrenTop = 0;
int oldChildrenTop = 0;
int newChildrenBottom = newWidgets.length - 1;
int oldChildrenBottom = oldChildren.length - 1;

final List newChildren = oldChildren.length == newWidgets.length ?
oldChildren : List(newWidgets.length);

Element previousChild;

// 从顶部向下更新子Element
// Update the top of the list.
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
final Widget newWidget = newWidgets[newChildrenTop];
if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
break;
final Element newChild = updateChild(oldChild, newWidget, IndexedSlot(newChildrenTop, previousChild));
newChildren[newChildrenTop] = newChild;
previousChild = newChild;
newChildrenTop += 1;
oldChildrenTop += 1;
}

// 从底部向上扫描子Element
// Scan the bottom of the list.
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenBottom]);
final Widget newWidget = newWidgets[newChildrenBottom];
if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
break;
oldChildrenBottom -= 1;
newChildrenBottom -= 1;
}

// 扫描旧的子Element列表里面中间的子Element,保存Widget有Key的Element到oldKeyChildren,其他的失效
// Scan the old children in the middle of the list.
final bool haveOldChildren = oldChildrenTop <= oldChildrenBottom;
Map oldKeyedChildren;
if (haveOldChildren) {
oldKeyedChildren = {};
while (oldChildrenTop <= oldChildrenBottom) {
final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
if (oldChild != null) {
if (oldChild.widget.key != null)
oldKeyedChildren[oldChild.widget.key] = oldChild;
else
deactivateChild(oldChild);
}
oldChildrenTop += 1;
}
}

// 根据Widget的Key更新oldKeyChildren中的Element。
// Update the middle of the list.
while (newChildrenTop <= newChildrenBottom) {
Element oldChild;
final Widget newWidget = newWidgets[newChildrenTop];
if (haveOldChildren) {
final Key key = newWidget.key;
if (key != null) {
oldChild = oldKeyedChildren[key];
if (oldChild != null) {
if (Widget.canUpdate(oldChild.widget, newWidget)) {
// we found a match!
// remove it from oldKeyedChildren so we don't unsync it later
oldKeyedChildren.remove(key);
} else {
// Not a match, let's pretend we didn't see it for now.
oldChild = null;
}
}
}
}

final Element newChild = updateChild(oldChild, newWidget, IndexedSlot(newChildrenTop, previousChild));
newChildren[newChildrenTop] = newChild;
previousChild = newChild;
newChildrenTop += 1;
}

newChildrenBottom = newWidgets.length - 1;
oldChildrenBottom = oldChildren.length - 1;

// 从下到上更新底部的Element。.
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
final Element oldChild = oldChildren[oldChildrenTop];
final Widget newWidget = newWidgets[newChildrenTop];
final Element newChild = updateChild(oldChild, newWidget, IndexedSlot(newChildrenTop, previousChild));
newChildren[newChildrenTop] = newChild;
previousChild = newChild;
newChildrenTop += 1;
oldChildrenTop += 1;
}

// 清除旧子Element列表中其他所有剩余Element
// Clean up any of the remaining middle nodes from the old list.
if (haveOldChildren && oldKeyedChildren.isNotEmpty) {
for (final Element oldChild in oldKeyedChildren.values) {
if (forgottenChildren == null || !forgottenChildren.contains(oldChild))
deactivateChild(oldChild);
}
}

return newChildren;
}
复制代码

该函数的主要职责如下:



  1. 复用能复用的子节点,并调用updateChild对子节点进行更新。

  2. 对不能更新的子节点,调用deactivateChild对该子节点进行失效。


其步骤如下:



  1. 从顶部向下更新子Element。

  2. 从底部向上扫描子Element。

  3. 扫描旧的子Element列表里面中间的子Element,保存Widget有Key的Element到oldKeyChildren,其他的失效。

  4. 对于新的子Element列表,如果其对应的Widget的Key和oldKeyChildren中的Key相同,更新oldKeyChildren中的Element。

  5. 从下到上更新底部的Element。

  6. 清除旧子Element列表中其他所有剩余Element。


文章参考于:zhuanlan.zhihu.com/p/369286610


收起阅读 »

跟我学flutter:细细品Widget(四)Widget 渲染过程 与 RenderObjectWidget

前言跟我学flutter系列:跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制跟我...
继续阅读 »

前言

跟我学flutter系列:
跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin
跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate
跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制

跟我学flutter:在国内如何发布自己的Plugin 或者 Package

跟我学flutter:Flutter雷达图表(一)如何使用kg_charts

跟我学flutter:细细品Widget(一)Widget&Element初识

跟我学flutter:细细品Widget(二)StatelessWidget&StatefulWidget

跟我学flutter:细细品Widget(三)ProxyWidget,InheritedWidget

跟我学flutter:细细品Widget(四)Widget 渲染过程 与 RenderObjectWidget

跟我学flutter:细细品Widget(五)Element

企业级篇目:
跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统
跟我学企业级flutter项目:dio网络框架增加公共请求参数&header
跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层
跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview
跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget


StatelessWidget 和 StatefulWidget 只是用来组装控件的容器,并不负责组件最后的布局和绘制。在 Flutter 中,布局和绘制工作实际上是在 Widget 的另一个子类 RenderObjectWidget 内完成的。
RenderObjectWidget为RenderObjectElement提供配置信息。
RenderObjectElement包装了RenderObject,RenderObject为应用程序提供真正的渲染。


源码


abstract class RenderObjectWidget extends Widget {

const RenderObjectWidget({ Key? key }) : super(key: key);

@override
@factory
RenderObjectElement createElement();

@protected
@factory
RenderObject createRenderObject(BuildContext context);

@protected
void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }

@protected
void didUnmountRenderObject(covariant RenderObject renderObject) { }
}


  • createElement 需要返回一个继承RenderObjectElement的类

  • createRenderObject 创建 Render Widget 对应的 Render Object,同样子类需要重写该方法。该方法在对应的 Element 被挂载到树上时调用(Element.mount),即在 Element 挂载过程中同步构建了「Render Tree」

  • updateRenderObject 在 Widget 更新后,修改对应的 Render Object。该方法在首次 build 以及需要更新 Widget 时都会调用;

  • didUnmountRenderObject 「Render Object」从「Render Tree」上移除时调用该方法。


RenderObjectElement 源码


abstract class RenderObjectElement extends Element {
RenderObject _renderObject;

@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_renderObject = widget.createRenderObject(this);
attachRenderObject(newSlot);
_dirty = false;
}

@override
void update(covariant RenderObjectWidget newWidget) {
super.update(newWidget);
widget.updateRenderObject(this, renderObject);
_dirty = false;
}
...
}


  • mount: RenderObject 对象的创建,以及与渲染树的插入工作,插入到渲染树后的 Element 就可以显示到屏幕中了。

  • update: 如果 Widget 的配置数据发生了改变,那么持有该 Widget 的 Element 节点也会被标记为 dirty。在下一个周期的绘制时,Flutter 就会触发 Element 树的更新,并使用最新的 Widget 数据更新自身以及关联的 RenderObject 对象,接下来便会进入 Layout 和 Paint 的流程。而真正的绘制和布局过程,则完全交由 RenderObject 完成。



RenderObject 主要处理一些固定的操作,如:布局、绘制和 Hit testing。 与ComponentElement一样RenderObjectElement也是抽象类,不同的是ComponentElement不会直接创建RenderObject,而是间接通过创建其他Element创建RenderObject。



RenderObjectElement主要有三个系统的子类,分别处理renderObject作为child时的不同情况。



  1. LeafRenderObjectElement:叶子渲染对象对应的元素,处理没有children的renderObject。

  2. SingleChildRenderObjectElement:处理只有单个child的renderObject。

  3. MultiChildRenderObjectElement: 处理有多个children的渲染对象



有时RenderObject的child模型更复杂一些,比如多维数组的形式,则可能需要基于RenderObjectElement实现一个新的子类。



RenderObjectElement 充当widget与renderObject之间的中介者。需要进行方法覆盖,以便它们返回元素期望的特定类型,例如:


class FooElement extends RenderObjectElement {                                       

@override
Foo get widget => super.widget;

@override
RenderFoo get renderObject => super.renderObject;

}

widget返回Foo,renderObject 返回RenderFoo


系统常用组件与RenderObjectElement:



























常用组件Widget(父级)Element
Flex/Wrap/Flow/StackMultiChildRenderObjectWidgetMultiChildRenderObjectElement
RawImage(Imaget)/ErrorWidgetLeafRenderObjectWidgetLeafRenderObjectElement
Offstage/SizedBox/Align/PaddingSingleChildRenderObjectWidgetSingleChildRenderObjectElement

RenderObject源码


abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {
...
void layout(Constraints constraints, { bool parentUsesSize = false }) {...}

void paint(PaintingContext context, Offset offset) { }
}

布局和绘制完成后,接下来的事情就交给 Skia 了。在 VSync 信号同步时直接从渲染树合成 Bitmap,然后提交给 GPU。


文章参考:http://www.jianshu.com/p/c3de443a7…

收起阅读 »

跟我学flutter:细细品Widget(三)ProxyWidget,InheritedWidget

前言跟我学flutter系列:跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制跟我...
继续阅读 »

前言

跟我学flutter系列:
跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin
跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate
跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制

跟我学flutter:在国内如何发布自己的Plugin 或者 Package

跟我学flutter:Flutter雷达图表(一)如何使用kg_charts

跟我学flutter:细细品Widget(一)Widget&Element初识

跟我学flutter:细细品Widget(二)StatelessWidget&StatefulWidget

跟我学flutter:细细品Widget(三)ProxyWidget,InheritedWidget

跟我学flutter:细细品Widget(四)Widget 渲染过程 与 RenderObjectWidget

跟我学flutter:细细品Widget(五)Element

企业级篇目:
跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统
跟我学企业级flutter项目:dio网络框架增加公共请求参数&header
跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层
跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview
跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget


ProxyWidget作为抽象基类本身没有任何功能,但他有两个实现类ParentDataWidget & InheritedElement


源码


abstract class ProxyWidget extends Widget {

const ProxyWidget({ Key? key, required this.child }) : super(key: key);

final Widget child;
}

InheritedWidget


InheritedWidget 用于在树上向下传递数据。


通过BuildContext.dependOnInheritedWidgetOfExactType可以获取最近的「Inherited Widget」,需要注意的是通过这种方式获取「Inherited Widget」时,当「Inherited Widget」状态有变化时,会导致该引用方 rebuild。


通常,为了使用方便会「Inherited Widget」会提供静态方法of,在该方法中调用BuildContext.dependOnInheritedWidgetOfExactType。of方法可以直接返回「Inherited Widget」,也可以是具体的数据。


有时,「Inherited Widget」是作为另一个类的实现细节而存在的,其本身是私有的(外部不可见),此时of方法就会放到对外公开的类上。最典型的例子就是Theme,其本身是StatelessWidget类型,但其内部创建了一个「Inherited Widget」:_InheritedTheme,of方法就定义在上Theme上:


  static ThemeData of(BuildContext context) {
final _InheritedTheme? inheritedTheme = context.dependOnInheritedWidgetOfExactType<_InheritedTheme>();
final MaterialLocalizations? localizations = Localizations.of(context, MaterialLocalizations);
final ScriptCategory category = localizations?.scriptCategory ?? ScriptCategory.englishLike;
final ThemeData theme = inheritedTheme?.theme.data ?? _kFallbackTheme;
return ThemeData.localize(theme, theme.typography.geometryThemeFor(category));
}

该of方法返回的是ThemeData类型的具体数据,并在其内部首先调用了BuildContext.dependOnInheritedWidgetOfExactType。


我们经常使用的「Inherited Widget」莫过于MediaQuery,同样提供了of方法:


  static MediaQueryData of(BuildContext context) {
assert(context != null);
assert(debugCheckHasMediaQuery(context));
return context.dependOnInheritedWidgetOfExactType()!.data;
}

在这里插入图片描述


源码


abstract class InheritedWidget extends ProxyWidget {

const InheritedWidget({ Key? key, required Widget child })
:
super(key: key, child: child)
;

@override
InheritedElement createElement() => InheritedElement(this);

@protected
bool updateShouldNotify(covariant InheritedWidget oldWidget);
}

createElement


「Inherited Widget」对应的 Element 为InheritedElement,一般情况下InheritedElement子类不用重写该方法;


updateShouldNotify


「Inherited Widget」rebuilt 时判断是否需要 rebuilt 那些依赖它的 Widget;


如下是MediaQuery.updateShouldNotify的实现,在新老Widget.data 不相等时才 rebuilt 那依赖的 Widget。


  @override
bool updateShouldNotify(MediaQuery oldWidget)
=> data != oldWidget.data;


依赖了 InheritedWidget 在数据变动的情况下 didChangeDependencies 会被调用,
依赖的意思是 使用 return context.dependOnInheritedWidgetOfExactType()
如果使用context.getElementForInheritedWidgetOfExactType().widget的话,只会用其中的数据,而不会重新rebuild



@override
InheritedElement getElementForInheritedWidgetOfExactType() {
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
return ancestor;
}
@override
InheritedWidget dependOnInheritedWidgetOfExactType({ Object aspect }) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
//多出的部分
if (ancestor != null) {
return dependOnInheritedElement(ancestor, aspect: aspect) as T;
}
_hadUnsatisfiedDependencies = true;
return null;
}

我们可以看到,dependOnInheritedWidgetOfExactType() 比 getElementForInheritedWidgetOfExactType()多调了dependOnInheritedElement方法,dependOnInheritedElement源码如下:


  @override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
assert(ancestor != null);
_dependencies ??= HashSet();
_dependencies.add(ancestor);
ancestor.updateDependencies(this, aspect);
return ancestor.widget;
}

可以看到dependOnInheritedElement方法中主要是注册了依赖关系!看到这里也就清晰了,调用dependOnInheritedWidgetOfExactType() 和 getElementForInheritedWidgetOfExactType()的区别就是前者会注册依赖关系,而后者不会,所以在调用dependOnInheritedWidgetOfExactType()时,InheritedWidget和依赖它的子孙组件关系便完成了注册,之后当InheritedWidget发生变化时,就会更新依赖它的子孙组件,也就是会调这些子孙组件的didChangeDependencies()方法和build()方法。而当调用的是 getElementForInheritedWidgetOfExactType()时,由于没有注册依赖关系,所以之后当InheritedWidget发生变化时,就不会更新相应的子孙Widget。


文章内容参考:http://www.jb51.net/article/221…



收起阅读 »

什么是请求参数、表单参数、url参数、header参数、Cookie参数?一文讲懂

最近在工作中对 http 的请求参数解析有了进一步的认识,写个小短文记录一下。回顾下自己的情况,大概就是:有点点网络及编程基础,只需要加深一点点对 HTTP 协议的理解就能弄明白了。先分享一个小故事:我至今仍清晰地记得大三实习时的第一个工作任务,我需要调用其他...
继续阅读 »

最近在工作中对 http 的请求参数解析有了进一步的认识,写个小短文记录一下。

回顾下自己的情况,大概就是:有点点网络及编程基础,只需要加深一点点对 HTTP 协议的理解就能弄明白了。

先分享一个小故事:我至今仍清晰地记得大三实习时的第一个工作任务,我需要调用其他部门提供的 api 去完成某项业务。

那个 api 文档只告诉了我请求参数需要传什么,没有提及用什么方式传,比如这样:


其实如果有经验的话,直接在请求体或 url 里填参数试一下就知道了;另一个是新人有时候不太敢问问题,其实只要向同事确认一下就好的。

然而由于当时我掌握的编程知识有限,只会用表单提交数据。所以当我下载完同事安利的 api 调用调试工具 postman 后,我就在网上查怎么用 postman 发送表单数据,结果折腾了好久 api 还是没能调通。

当天晚上我向老同学求助,他问我上课是不是又睡过去了?

我说你怎么知道?

他说当然咯,你上课睡觉不学习又不是一天两天的事情......

后来他告诉我得好好学一下 http 协议,看看可以在协议的哪些位置放请求参数。

一个简单的 http 服务器还原

那么,在正式讲解之前,我们先简单搭建一个 http 服务器,阿菌沿用经典的 python 版云你好服务器进行讲解。

云你好服务器的代码很简单,服务器首先会获取 name 用户名这个参数,如果用户传了这个参数,就返回 Hello xxx,xxx 指的是 name 用户名;如果用户没有传这个参数则返回 Hello World

# 云你好服务源码
from flask import Flask
from flask import request

app = Flask(__name__)

# 云你好服务 API 接口
@app.get("/api/hello")
def hello():
   # 看用户是否传递了参数 name
   name = request.args.get("name", "")
   # 如果传了参数就向目标对象打招呼,输出 Hello XXX,否则输出 Hello World
   return f"Hello {name}" if name else "Hello World"

# 启动云你好服务
if __name__ == '__main__':
   app.run()

为了快速开发(大伙可以下载一个 python 把这个代码跑一下,用自己的语言实现一个类似的服务器也是可以的),阿菌这里使用了 flask 框架构建后端服务。

在具体获取参数的时候,我选择了在 request.args 中获取参数。这里提前剧透一下:在 flask 框架中,request.args 指的是从 url 中获取参数(不过这是我们后面讲解的内容,大家有个印象就好)

抓包查看 http 报文

有了 http 服务器后,我们开始深入讲解 http 协议,em...个人觉得只在学校上课看教材学计算机网络好像还欠缺了点啥,比较推荐大家下载一个像 Wireshark 这样的网络抓包软件,动手拆解网络包,深入学习各种网络协议。抓取网络包的示例视频

为了搞清楚什么是请求参数、表单参数、url 参数、Header 参数、Cookie 参数,我们先发一个 http 请求,然后抓取这个请求的网络包,看看一份 http 报文会携带哪些信息。

呼应开头,用户阿菌是个只会发表单数据的萌新,他使用 postman 向云你好 api 发送了一个 post 请求:


剧情发展正常,我们没能得到 Hello 阿菌(服务器会到 url 中获取参数,咱们用表单形式提交,所以获取不到)

由于咱们对请求体这个概念比较模糊,接下来我们重新发一个一模一样的请求,并且通过 Wireshark 抓包看一下:


可以看到强大的 Wireshark 帮助我们把请求抓取了下来,并把整个网络包的链路层协议,IP层协议,传输层协议,应用层协议全都解析好了。

由于咱们小码农一般都忙于解决应用层问题,所以我们把目光聚焦于高亮的 Hypertext Transfer Protocol 超文本传输协议,也就是大名鼎鼎的 HTTP 协议。

首先我们查看一下 HTTP 报文的完整内容:


可以看到,http 协议大概是这么组成的:

  • 第一行是请求的方式,比如 GET / POST / DELETE / PUT

  • 请求方式后面跟的是请求的路径,一般把这个叫 URI(统一资源标识符)

补充:URL 是统一资源定位符,见名知义,因为要定位,所以要指定协议甚至是位置,比如这样:http://localhost:5000/api/hello

  • 请求路径后面跟的是 HTTP 的版本,比如这里是 HTTP/1.1

完整的第一行如下:

POST /api/hello HTTP/1.1

第二行的 User-Agent 则用于告诉对方发起请求的客户端是啥,比如咱们用 Postman 发起的请求,Postman 就会自动把这个参数设置为它自己:

User-Agent: PostmanRuntime/7.28.4

第三行的 Accept 用于告诉对方我们希望收到什么类型的数据,这里默认是能接受所有类型的数据:

Accept: */*

第四行就非常值得留意,Postman-Token 是 Postman 自己传的参数,这个我们放到下面讲!

Postman-Token: ddd72e1a-0d63-4bad-a18e-22e38a5de3fc

第五行是请求的主机,网络上的一个服务一般用 ip 加端口作为唯一标识:

Host: 127.0.0.1:5000

第六行指定的是咱们请求发起方可以理解的压缩方式:

Accept-Encoding: gzip, deflate, br

第七行告诉对方处理完当前请求后不要关闭连接:

Connection: keep-alive

第八行告诉对方咱们请求体的内容格式,这个是本文的侧重点啦!比如我们这里指定的是一般浏览器的原生表单格式:

Content-Type: application/x-www-form-urlencoded

好了,下面大家要留意了,第九行的 Content-Length 给出的是请求体的大小。

而请求体,会放在紧跟着的一个空行之后。比如本请求的请求体内容是以 key=value 形式填充的,也就是我们表单参数的内容了:

Content-Length: 23

name=%E9%98%BF%E8%8F%8C

看到这里我们先简单小结一下,想要告诉服务器我们发送的是表单数据,一共需要两步:

  1. Content-Type 设置为 application/x-www-form-urlencoded

  2. 在请求体中按照 key=value 的形式填写请求参数

什么是协议?进一步了解 http

好了,接下来我们进一步讲解,大家试想一下,网络应用,其实就是端到端的交互,最常见的就是服务端和客户端交互模型:客户端发一些参数数据给服务端,通过这些参数数据告诉服务端它想得到什么或想干什么,服务端根据客户端传递的参数数据作出处理。

传输层协议通过 ip 和端口号帮我们定位到了具体的服务应用,具体怎么交互是由我们程序员自己定义的。

大概在 30 年前,英国计算机科学家蒂姆·伯纳斯-李定义了原始超级文本传输协议(HTTP),后续我们的 web 应用大都延续采用了他定义的这套标准,当然这套标准也在不断地进行迭代。

许多文献资料会把 http 协议描述得比较晦涩,加上协议这个词听起来有点高大上,初学者入门学习的时候往往感觉不太友好。

其实协议说白了就是一种格式,就好比我们写书信,约定要先顶格写个敬爱的 xxx,然后写个你好,然后换一个段落再写正文,可能最后还得加上日期署名等等。

我们只要按照格式写信,老师就能一眼看出来我们在写信;只要我们按协议格式发请求数据,服务器就能一眼看出来我们想要得到什么或想干什么。

当然,老师是因为老早就学过书信格式,所以他才能看懂书信格式;服务端程序也一样,我们要预先编写好 http 协议的解析逻辑,然后我们的服务器才能根据解析逻辑去获取一个 http 请求中的各种东西。

当然这个解析 http 协议的逻辑不是谁都能写出来的,就算能写出来,也未必写得好,所以我们会使用厉害的人封装好的脚手架,比如 java 里的 spring 全套、Go 语言里的 Gin 等等。

回到我们开头给出的示例:

from flask import Flask
from flask import request

app = Flask(__name__)

# 云你好服务 API 接口
@app.get("/api/hello")
def hello():
   # 看用户是否传递了参数 name
   name = request.args.get("name", "")
   # 如果传了参数就向目标对象打招呼,输出 Hello XXX,否则输出 Hello World
   return f"Hello {name}" if name else "Hello World"

# 启动云你好服务
if __name__ == '__main__':
   app.run()

阿菌的示例使用了 python 里的 flask 框架,在处理逻辑中使用了 request.args 获取请求参数,而 args 封装的就是框架从 url 中获取参数的逻辑。比如我们发送请求的 url 为:

http://127.0.0.1:5000/api/hello?name=ajun

框架会帮助我们从 url 中的 ? 后面开始截取,然后把 name=ajun 这些参数存放到 args 里。

切换一下,假设我们是云你好服务提供者,我们希望用户通过表单参数的形式使用云你好服务,我们只要把获取 name 参数的方式改成从表单参数里获取就可以了,flask 在 request.form 里封装了表单参数(关于框架是怎么在数行 http 请求中封装参数的,大家可以看自己使用的框架的具体逻辑,估计区别不大,只是存在一些语言特性上的差异):

@app.post("/api/hello")
def hello():
   # 看用户是否传递了参数 name
   name = request.form.get("name", "")
   # 如果传了参数就向目标对象打招呼,输出 Hello XXX,否则输出 Hello World
   return f"Hello {name}" if name else "Hello World"

思考:我们可以在 http 协议中传递什么参数?

最后,我们解释本文的标题,其实想要明白各种参数之间的区别,我们可以换一个角度思考:

咱们可以在一份 http 报文的哪些位置传递参数?

接下来回顾一下一个 http 请求的内容:

POST /api/hello HTTP/1.1
User-Agent: PostmanRuntime/7.28.4
Accept: */*
Postman-Token: fbf75035-a647-46dc-adc0-333751a9399e
Host: 127.0.0.1:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 23

name=%E9%98%BF%E8%8F%8C

大家看,咱们的 http 报文,也就是基于传输层之上的应用层报文,大概就长上面这样。

我们考虑两种情况,第一种情况,我们基于别人已经开发好的脚手架开发 http 服务器。

由于框架会基于 http 协议进行解析,所以框架会帮助我们解析好请求 url,各种 Header 头(比如:Cookie 等),以及具体的响应内容都帮我们封装解析好了(比如按照 key=value 的方式去读取请求体)。

那当我们开发服务端的时候,就可以指定从 url、header、响应体中获取参数了,比如:

  • url 参数:指的就是 url 中 ? 后面携带的 key value 形式参数

  • header 参数:指的就是各个 header 头,我们甚至可以自定义 header,比如 Postman-Token 就是 postman 这个软件自己携带的,我们服务端如果需要的话是可以指定获取这个参数的

  • Cookie 参数:其实就是名字为 Cookie 的请求头

  • 表单参数:指的就是 Content-Type 为 application/x-www-form-urlencoded 下请求体的内容,如果我们的表单需要传文件,还会有其他的 Content-Type

  • json 参数:指的就是 Content-Type 为 application/json 下请求体的内容(当然服务端可以不根据 Content-Type 直接解析请求体,但按照协议的规范工程项目或许会更好维护)

综上所述,请求参数就是对上面各种类型的参数的一个总称了。

大家会发现,不管什么 url 参数、header 参数、Cookie 参数、表单参数,其实就是换着法儿,按照一定的格式把数据放到应用层报文中。关键在于我们的服务端程序和客户端程序按照一种什么样的约定去传递和获取这些参数。这就是协议吧~

还有另一种情况,当然这只是开玩笑了,比如以后哪位大佬或者哪家企业定义了一种新的数据传输标准,推广至全球,比如叫 hppt 协议,这样是完全可以自己给各种形式参数下定义取名字的。这可能就是为啥我们说一流的企业、大佬制定标准,接下来的围绕标准研发技术,进而是基于技术卖产品,最后是围绕产品提供服务了。

一旦标准制定了,整个行业都围绕这个标准转了,而且感觉影响会越来越深远......

讲解参考链接

作者:胡涂阿菌
来源:juejin.cn/post/7100400494081736711

收起阅读 »

Base64编码解码原理

Base64编码与解码原理涉及的算法1、短除法短除法运算方法是先用一个除数除以能被它除尽的一个质数,以此类推,除到商是质数为止。通过短除法,十进制数可以不断除以2得到多个余数。最后,将余数从下到上进行排列组合,得到二进制数。实例:以字符n对应的ascII编码1...
继续阅读 »

Base64编码与解码

原理涉及的算法

1、短除法

短除法运算方法是先用一个除数除以能被它除尽的一个质数,以此类推,除到商是质数为止。通过短除法,十进制数可以不断除以2得到多个余数。最后,将余数从下到上进行排列组合,得到二进制数。

实例:以字符n对应的ascII编码110为例。

110 / 2  = 55...0
55 / 2 = 27...1
27 / 2 = 13...1
13 / 2 = 6...1
6   / 2 = 3...0
3   / 2 = 1...1
1   / 2 = 0...1

将余数从下到上进行排列组合,得到字符n对应的ascII编码110转二进制为1101110,因为一字节对应8位(bit), 所以需要向前补0补足8位,得到01101110。其余字符同理可得。

2、按权展开求和

按权展开求和, 8位二进制数从右到左,次数是0到7依次递增, 基数*底数次数,从左到右依次累加,相加结果为对应十进制数。我们已二进制数01101110转10进制为例:

(01101110)2=0∗20+1∗21+1∗22+1∗23+0∗24+1∗25+1∗26+0∗27(01101110)_2 = 0 * 2^0 + 1 * 2 ^ 1 + 1 * 2^2 + 1 * 2^3 + 0 * 2^4 + 1 * 2^5 + 1 * 2^6 + 0 * 2^7(01101110)2=0∗20+1∗21+1∗22+1∗23+0∗24+1∗25+1∗26+0∗27

3、位概念

二进制数系统中,每个0或1就是一个位(bit,比特),也叫存储单元,位是数据存储的最小单位。其中 8bit 就称为一个字节(Byte)。

4、移位运算符

移位运算符在程序设计中,是位操作运算符的一种。移位运算符可以在二进制的基础上对数字进行平移。按照平移的方向和填充数字的规则分为三种:<<(左移)、>>(带符号右移)和>>>(无符号右移)。在base64的编码和解码过程中操作的是正数,所以仅使用<<(左移)、>>(带符号右移)两种运算符。

  1. 左移运算:是将一个二进制位的操作数按指定移动的位数向左移动,移出位被丢弃,右边移出的空位一律补0。【左移相当于一个数乘以2的次方】

  2. 右移运算:是将一个二进制位的操作数按指定移动的位数向右移动,移出位被丢弃,左边移出的空位一律补0,或者补符号位,这由不同的机器而定。在使用补码作为机器数的机器中,正数的符号位为0,负数的符号位为1。【右移相当于一个数除以2的次方】

// 左移
01101000 << 2 -> 101000(左侧移出位被丢弃) -> 10100000(右侧空位一律补0)
// 右移
01101000 >> 2 -> 011010(右侧移出位被丢弃) -> 00011010(左侧空位一律补0)

5、与运算、或运算

与运算、或运算都是计算机中一种基本的逻辑运算方式。

  1. 与运算:符号表示为&。运算规则:两位同时为“1”,结果才为“1”,否则为0

  2. 或运算:符号表示为|。运算规则:两位只要有一位为“1”,结果就为“1”,否则为0

什么是base64编码

2^6=64\

\

Base64编码是将字符串以每3个8比特(bit)的字节子序列拆分成4个6比特(bit)的字节(6比特有效字节,最左边两个永远为0,其实也是8比特的字节)子序列,再将得到的子序列查找Base64的编码索引表,得到对应的字符拼接成新的字符串的一种编码方式。

每3个8比特(bit)的字节子序列拆分成4个6比特(bit)的字节的拆分过程如下图所示:


为什么base64编码后的大小是原来的4/3倍

因为6和8的最大公倍数是24,所以3个8比特的字节刚好可以拆分成4个6比特的字节,3 x 8 = 6 x 4。计算机中,因为一个字节需要8个存储单元存储,所以我们要把6个比特往前面补两位0,补足8个比特。如下图所示:


补足后所需的存储单元为32个,是原来所需的24个的4/3倍。这也就是base64编码后的大小是原来的4/3倍的原因。

为什么命名为base64呢?

因为6位(bit)的二进制数有2的6次方个,也就是二进制数(00000000-00111111)之间的代表0-63的64个二进制数。

不是说一个字节是用8位二进制表示的吗,为什么不是2的8次方?

因为我们得到的8位二进制数的前两位永远是0,真正的有效位只有6位,所以我们所能够得到的二进制数只有2的6次方个。

Base64字符是哪64个?

Base64的编码索引表,字符选用了"A-Z、a-z、0-9、+、/" 64个可打印字符来代表(00000000-00111111)这64个二进制数。即

let base64EncodeChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

编码原理

要把3个字节拆分成4个字节可以怎么做?

流程图


思路

分析映射关系:abc → xyzi。我们从高位到低位添加索引来分析这个过程

  • x: (前面补两个0)a的前六位 => 00a7a6a5a4a3a2

  • y: (前面补两个0)a的后两位 + b的前四位 => 00a1a0b7b6b5b4

  • z: (前面补两个0)b的后四位 + c的前两位 => 00b3b2b1b0c7c6

  • i: (前面补两个0)c的后六位 => 00c5c4c3c2c1c0

通过上述的映射关系,得到实现思路:

  1. 将字符对应的AscII编码转为8位二进制数

  2. 将每三个8位二进制数进行以下操作

    • 将第一个数右移位2位,得到第一个6位有效位二进制数

    • 将第一个数 & 0x3之后左移位4位,得到第二个6位有效位二进制数的第一个和第二个有效位,将第二个数 & 0xf0之后右移位4位,得到第二个6位有效位二进制数的后四位有效位,两者取且得到第二个6位有效位二进制

    • 将第二个数 & 0xf之后左移位2位,得到第三个6位有效位二进制数的前四位有效位,将第三个数 & 0xC0之后右移位6位,得到第三个6位有效位二进制数的后两位有效位,两者取且得到第三个6位有效位二进制

    • 将第三个数 & 0x3f,得到第四个6位有效位二进制数

  3. 将获得的6位有效位二进制数转十进制,查找对呀base64字符

代码实现

以hao字符串为例,观察base64编码的过程,将上面转换通过代码逻辑分析实现

// 输入字符串
let str = 'hao'
// base64字符串
let base64EncodeChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
// 定义输入、输出字节的二进制数
let char1, char2, char3, out1, out2, out3, out4, out
// 将字符对应的ascII编码转为8位二进制数
char1 = str.charCodeAt(0) & 0xff // 104 01101000
char2 = str.charCodeAt(1) & 0xff // 97 01100001
char3 = str.charCodeAt(2) & 0xff // 111 01101111
// 输出6位有效字节二进制数
out1 = char1 >> 2 // 26 011010
out2 = (char1 & 0x3) << 4 | (char2 & 0xf0) >> 4 // 6 000110
out3 = (char2 & 0xf) << 2 | (char3 & 0xc0) >> 6 // 5 000101
out4 = char3 & 0x3f // 47 101111

out = base64EncodeChars[out1] + base64EncodeChars[out2] + base64EncodeChars[out3] + base64EncodeChars[out4] // aGFv

算法剖析

  1. out1: char1 >> 2

    01101000 -> 00011010
    复制代码
  2. out2 = (char1 & 0x3) << 4 | (char2 & 0xf0) >> 4

    // 且运算
    01101000       01100001
    00000011       11110000
    --------       --------
    00000000       01100000

    // 移位运算后得
    00000000       00000110

    // 或运算
    00000000
    00000110
    --------
    00000110
    复制代码

第三个字符第四个字符同理

整理上述代码,扩展至多字符字符串

// 输入字符串
let str = 'haohaohao'
// base64字符串
let base64EncodeChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

// 获取字符串长度
let len = str.length
// 当前字符索引
let index = 0
// 输出字符串
let out = ''
while(index < len) {
  // 定义输入、输出字节的二进制数
  let char1, char2, char3, out1, out2, out3, out4
  // 将字符对应的ascII编码转为8位二进制数
  char1 = str.charCodeAt(index++) & 0xff // 104 01101000
  char2 = str.charCodeAt(index++) & 0xff // 97 01100001
  char3 = str.charCodeAt(index++) & 0xff // 111 01101111
  // 输出6位有效字节二进制数
  out1 = char1 >> 2 // 26 011010
  out2 = (char1 & 0x3) << 4 | (char2 & 0xf0) >> 4 // 6 000110
  out3 = (char2 & 0xf) << 2 | (char3 & 0xc0) >> 6 // 5 000101
  out4 = char3 & 0x3f // 47 101111

  out = out + base64EncodeChars[out1] + base64EncodeChars[out2] + base64EncodeChars[out3] + base64EncodeChars[out4] // aGFv
}

原字符串长度不是3的整倍数的情况,需要特殊处理

    ...
  char1 = str.charCodeAt(index++) & 0xff // 104 01101000
  if (index == len) {
      out2 = (char1 & 0x3) << 4
      out = out + base64EncodeChars[out1] + base64EncodeChars[out2] + '=='
      return out
  }
  char2 = str.charCodeAt(index++) & 0xff // 97 01100001
  if (index == len) {
      out1 = char1 >> 2 // 26 011010
      out2 = (char1 & 0x3) << 4 | (char2 & 0xf0) >> 4 // 6 000110
      out3 = (char2 & 0xf) << 2
      out = out + base64EncodeChars[out1] + base64EncodeChars[out2] + base64EncodeChars[out3] + '='
      return out
  }
  ...

全部代码

function base64Encode(str) {
// base64字符串
let base64EncodeChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

// 获取字符串长度
let len = str.length
// 当前字符索引
let index = 0
// 输出字符串
let out = ''
while(index < len) {
// 定义输入、输出字节的二进制数
let char1, char2, char3, out1, out2, out3, out4
// 将字符对应的ascII编码转为8位二进制数
char1 = str.charCodeAt(index++) & 0xff
out1 = char1 >> 2
if (index == len) {
out2 = (char1 & 0x3) << 4
out = out + base64EncodeChars[out1] + base64EncodeChars[out2] + '=='
return out
}
char2 = str.charCodeAt(index++) & 0xff
out2 = (char1 & 0x3) << 4 | (char2 & 0xf0) >> 4
if (index == len) {
out3 = (char2 & 0xf) << 2
out = out + base64EncodeChars[out1] + base64EncodeChars[out2] + base64EncodeChars[out3] + '='
return out
}
char3 = str.charCodeAt(index++) & 0xff
// 输出6位有效字节二进制数
out3 = (char2 & 0xf) << 2 | (char3 & 0xc0) >> 6
out4 = char3 & 0x3f

out = out + base64EncodeChars[out1] + base64EncodeChars[out2] + base64EncodeChars[out3] + base64EncodeChars[out4]
}
return out
}
base64Encode('haohao') // aGFvaGFv
base64Encode('haoha') // aGFvaGE=
base64Encode('haoh') // aGFvaA==

解码原理

逆向推导,由每4个6位有效位的二进制数合并成3个8位二进制数,根据ascII编码映射到对应字符后拼接字符串

思路

分析映射关系 xyzi -> abc

  • a: x后六位 + y第三、四位 => x5x4x3x2x1x0y5y4

  • b: y后四位 + z第三、四、五、六位 => y3y2y1y0z5z4z3z2

  • c: z后两位 + i后六位 => z1z0i5i4i3i2i1i0

  1. 将字符对应的base64字符集的索引转为6位有效位二进制数

  2. 将每四个6位有效位二进制数进行以下操作

    1. 第一个二进制数左移位2位,得到新二进制数的前6位,第二个二进制数 & 0x30之后右移位4位,取或集得到第一个新二进制数

    2. 第二个二进制数 & 0xf之后左移位4位,第三个二进制数 & 0x3c之后右移位2位,取或集得到第二个新二进制数

    3. 第二个二进制数 & 0x3之后左移位6位,与第四个二进制数取或集得到第二个新二进制数

  3. 根据ascII编码映射到对应字符后拼接字符串

代码实现

// base64字符串
let str = 'aGFv'
// base64字符集
let base64CharsArr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('')
// 获取索引值
let char1 = base64CharsArr.findIndex(char => char==str[0]) & 0xff // 26 011010
let char2 = base64CharsArr.findIndex(char => char==str[1]) & 0xff // 6 000110
let char3 = base64CharsArr.findIndex(char => char==str[2]) & 0xff // 5 000101
let char4 = base64CharsArr.findIndex(char => char==str[3]) & 0xff // 47 101111
let out1, out2, out3, out
// 位运算
out1 = char1 << 2 | (char2 & 0x30) >> 4
out2 = (char2 & 0xf) << 4 | (char3 & 0x3c) >> 2
out3 = (char3 & 0x3) << 6 | char4
console.log(out1, out2, out3)
out = String.fromCharCode(out1) + String.fromCharCode(out2) + String.fromCharCode(out3)

遇到有用'='补过位的情况时

function base64decode(str) {
// base64字符集
let base64CharsArr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('')
let char1 = base64CharsArr.findIndex(char => char==str[0])
let char2 = base64CharsArr.findIndex(char => char==str[1])
let out1, out2, out3, out
if (char1 == -1 || char2 == -1) return out
char1 = char1 & 0xff
char2 = char2 & 0xff
let char3 = base64CharsArr.findIndex(char => char==str[2])
// 第三位不在base64对照表中时,只拼接第一个字符串
if (char3 == -1) {
out1 = char1 << 2 | (char2 & 0x30) >> 4
out = String.fromCharCode(out1)
return out
}
let char4 = base64CharsArr.findIndex(char => char==str[3])
// 第三位不在base64对照表中时,只拼接第一个和第二个字符串
if (char4 == -1) {
out1 = char1 << 2 | (char2 & 0x30) >> 4
out2 = (char2 & 0xf) << 4 | (char3 & 0x3c) >> 2
out = String.fromCharCode(out1) + String.fromCharCode(out2)
return out
}
// 位运算
out1 = char1 << 2 | (char2 & 0x30) >> 4
out2 = (char2 & 0xf) << 4 | (char3 & 0x3c) >> 2
out3 = (char3 & 0x3) << 6 | char4
console.log(out1, out2, out3)
out = String.fromCharCode(out1) + String.fromCharCode(out2) + String.fromCharCode(out3)
return out
}

解码整个字符串,整理代码后

function base64decode(str) {
// base64字符集
let base64CharsArr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('')
let i = 0
let len = str.length
let out = ''
while(i < len) {
let char1 = base64CharsArr.findIndex(char => char==str[i])
i++
let char2 = base64CharsArr.findIndex(char => char==str[i])
i++
let out1, out2, out3
if (char1 == -1 || char2 == -1) return out
char1 = char1 & 0xff
char2 = char2 & 0xff
let char3 = base64CharsArr.findIndex(char => char==str[i])
i++
// 第三位不在base64对照表中时,只拼接第一个字符串
out1 = char1 << 2 | (char2 & 0x30) >> 4
if (char3 == -1) {
out = out + String.fromCharCode(out1)
return out
}
let char4 = base64CharsArr.findIndex(char => char==str[i])
i++
// 第三位不在base64对照表中时,只拼接第一个和第二个字符串
out2 = (char2 & 0xf) << 4 | (char3 & 0x3c) >> 2
if (char4 == -1) {
out = out + String.fromCharCode(out1) + String.fromCharCode(out2)
return out
}
// 位运算
out3 = (char3 & 0x3) << 6 | char4
console.log(out1, out2, out3)
out = out + String.fromCharCode(out1) + String.fromCharCode(out2) + String.fromCharCode(out3)
}
return out
}
base64decode('aGFvaGFv') // haohao
base64decode('aGFvaGE=') // haoha
base64decode('aGFvaA==') // haoh

上述解码核心是字符与base64字符集索引的映射,网上看到过使用AscII编码索引映射base64字符索引的方法

let base64DecodeChars = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1]
//
let char1 = 'hao'.charCodeAt(0) // h -> 104
base64DecodeChars[char1] // 33 -> base64编码表中的h

由此可见,base64DecodeChars对照accII编码表的索引存放的是base64编码表的对应字符的索引。

jdk1.8之前的方式

Base64编码与解码时,会使用到JDK里sun.misc包套件下的BASE64Encoder类和BASE64Decoder类

sun.misc包所提供的Base64编码解码功能效率不高,因此在1.8之后的jdk版本已经被删除了

// 编码器
final BASE64Encoder encoder = new BASE64Encoder();
// 解码器
final BASE64Decoder decoder = new BASE64Decoder();
final String text = "字串文字";
final byte[] textByte = text.getBytes("UTF-8");
//编码
final String encodedText = encoder.encode(textByte);
System.out.println(encodedText);
//解码
System.out.println(new String(decoder.decodeBuffer(encodedText), "UTF-8"));

Apache Commons Codec包的方式

Apache Commons Codec 有提供Base64的编码与解码功能,会使用到 org.apache.commons.codec.binary 套件下的Base64类别,用法如下

1、引入依赖

<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-compress</artifactId>
  <version>1.21</version>
</dependency>

2、代码实现

final Base64 base64 = new Base64();
final String text = "字串文字";
final byte[] textByte = text.getBytes("UTF-8");
//编码
final String encodedText = base64.encodeToString(textByte);
System.out.println(encodedText);
//解码
System.out.println(new String(base64.decode(encodedText), "UTF-8"));

jdk1.8之后的方式

与sun.misc包和Apache Commons Codec所提供的Base64编解码器方式来比较,Java 8提供的Base64拥有更好的效能。实际测试编码与解码速度,Java 8提供的Base64,要比 sun.misc 套件提供的还要快至少11倍,比 Apache Commons Codec 提供的还要快至少3倍。

// 解码器
final Base64.Decoder decoder = Base64.getDecoder();
// 编码器
final Base64.Encoder encoder = Base64.getEncoder();
final String text = "字串文字";
final byte[] textByte = text.getBytes(StandardCharsets.UTF_8);
//编码
final String encodedText = encoder.encodeToString(textByte);
System.out.println(encodedText);
//解码
System.out.println(new String(decoder.decode(encodedText), StandardCharsets.UTF_8));

总结

Base64 是一种数据编码方式,可做简单加密使用,可以t通过改变base64编码映射顺序来形成自己独特的加密算法进行加密解密。

编码表

Base64编码表


AscII码编码表


作者:loginfo
来源:juejin.cn/post/7100421228644532255

收起阅读 »

Python-可变和不可变类型

1. 不可变类型不可变类型,内存中的数据不允许被修改(一旦被定义,内存中分配了小格子,就不能再修改内容了):数字类型int,bool,float,complex,long(2,x)字符串str元组tuple2. 可变类型可变类型,内存中的数据可以被修改(可以通...
继续阅读 »

1. 不可变类型

不可变类型,内存中的数据不允许被修改(一旦被定义,内存中分配了小格子,就不能再修改内容了):

  • 数字类型intboolfloatcomplexlong(2,x)

  • 字符串str

  • 元组tuple

2. 可变类型

可变类型,内存中的数据可以被修改(可以通过变量名调用方法来修改列表和字典内部的内容,而内存地址不发生变化):

  • 列表list

  • 字典dict(注:字典中的key只能使用不可变类型的数据)

注:给变量赋新值的时候,只是改变了变量的引用地址,不是修改之前的内容

  1. 可变类型的数据变化,是通过方法来实现的

  2. 如果给一个可变类型的变量,复制了一个新的数据,引用会修改(变量从之前的数据上撕下来,贴到新赋值的数据上)

3. 代码演示

# 新建列表
a = [1, 2, 3]
print("列表a:", a)
print("列表a的地址:", id(a))
print("*"*50)
# 追加元素
a.append(999)
print("列表a:", a)
print("列表a的地址:", id(a))
print("*"*50)
# 移除元素
a.remove(2)
print("列表a:", a)
print("列表a的地址:", id(a))
print("*"*50)
# 清空列表
a.clear()
print("列表a:", a)
print("列表a的地址:", id(a))
print("*"*50)
# 将空列表赋值给变量a
a = []
print("列表a的地址:", id(a))   # 通过输出可以看出地址发生了变化
print("*"*50)
# 新建字典
d = {"name": "xiaoming"}
print("字典d为:", d)
print("字典d的地址:", id(d))
print("*"*50)
# 追加键值对
d["age"] = 18
print("字典d为:", d)
print("字典d的地址:", id(d))
print("*"*50)
# 删除键值对
d.pop("age")
print("字典d为:", d)
print("字典d的地址:", id(d))
print("*"*50)
# 清空所有键值对
d.clear()
print("字典d为:", d)
print("字典d的地址:", id(d))
print("*"*50)
# 对d赋值空字典
d = {}
print("字典d为:", d)
print("字典d的地址:", id(d))
print("*"*50)

4. 运行结果

可变类型(列表和字典)的数据变化,是通过方法(比如append,remove,pop等)来实现的,不会改变地址。而重新赋值后地址会改变。具体运行结果如下图所示:




作者:ZacheryZHANG
来源:juejin.cn/post/7100423532655411213

收起阅读 »

推荐一款超棒的SpringCloud 脚手架项目

之前接个私活,在网上找了好久没有找到合适的框架,不是版本低没人维护了,在不就是组件相互依赖较高。所以我自己搭建一个全新spingCloud框架,里面所有组件可插拔的,集成多个组件供大家选择,喜欢哪个用哪个一、系统架构图二、快速启动1.本地启动nacos: ht...
继续阅读 »

之前接个私活,在网上找了好久没有找到合适的框架,不是版本低没人维护了,在不就是组件相互依赖较高。所以我自己搭建一个全新spingCloud框架,里面所有组件可插拔的,集成多个组件供大家选择,喜欢哪个用哪个

一、系统架构图


二、快速启动

1.本地启动nacos: http://127.0.0.1:8848

sh startup.sh -m standalone

2.本地启动sentinel: http://127.0.0.1:9000

nohup java -Dauth.enabled=false -Dserver.port=9000 -jar sentinel-dashboard-1.8.1.jar &

3.本地启动zipkin: http://127.0.0.1:9411/

nohup java -jar zipkin-server-2.23.2-exec.jar &

三、项目概述

  • springboot+springcloud

  • 注册中心:nacos

  • 网关:gateway

  • RPC:feign

以下是可插拔功能组件

  • 流控熔断降级:sentinel

  • 全链路跟踪:sleth+zipkin

  • 分布式事务:seata

  • 封装功能模块:全局异常处理、日志输出打印持久化、多数据源、鉴权授权模块、zk(分布式锁和订阅者模式)

  • maven:实现多环境打包、直推镜像到docker私服。

这个项目整合了springcloud体系中的各种组件。以及集成配置说明。同时将自己平时使用的功能性的封装以及工具包都最为模块整合进来。可以避免某些技术点长时间不使用后的遗忘。

另一方面现在springboot springcloud 已经springcloud-alibaba的版本迭代速度越来越快。

为了保证我们的封装和集成方式在新版本中依然正常运行,需要用该项目进行最新版本的适配实验。这样可以更快的在项目中集合工程中的功能模块。

四、项目预览






五、新建业务工程模块说明

由于springboot遵循 约定大于配置的原则。所以本工程中所有的额类都在的包路径都在com.cloud.base下。

如果新建的业务项目有规定使用指定的基础包路径则需要在启动类增加包扫描注解将com.cloud.base下的所有类加入到扫描范围下。

@ComponentScan(basePackages = "com.cloud.base")

如果可以继续使用com.cloud.base 则约定将启动类放在该路径下即可。

六、模块划分

父工程:

cloud-base - 版本依赖管理 <groupId>com.cloud</groupId>
|
|--common - 通用工具类和包 <groupId>com.cloud.common</groupId>
|   |
|   |--core-common 通用包 该包包含了SpringMVC的依赖,会与WebFlux的服务有冲突
|   |
|   |--core-exception 自定义异常和请求统一返回类
|
|--dependency - 三方功能依赖集合 无任何实现 <groupId>com.cloud.dependency</groupId>
|   |
|   |--dependency-alibaba-cloud 关于alibaba-cloud的依赖集合
|   |
|   |--dependency-mybatis-tk 关于ORM mybatis+tk.mybatis+pagehelper的依赖集合
|   |
|   |--dependency-mybatis-plus 关于ORM mybatis+mybatis—plus+pagehelper的依赖集合
|   |
|   |--dependency-seata 关于分布式事务seata的依赖集合
|   |
|   |--dependency-sentinel 关于流控组件sentinel的依赖集合
|   |
|   |--dependency-sentinel-gateway 关于网关集成流控组件sentinel的依赖集合(仅仅gateway网关使用该依赖)
|   |
|   |--dependency-sleuth-zipkin 关于链路跟踪sleuth-zipkin的依赖集合
|
|--modules - 自定义自实现的功能组件模块 <groupId>com.cloud.modules</groupId>
|   |
|   |--modules-logger 日志功能封装
|   |
|   |--modules-multi-datasource 多数据功能封装
|   |
|   |--modules-lh-security 分布式安全授权鉴权框架封装
|   |
|   |--modules-youji-task 酉鸡-分布式定时任务管理模块
|   |
|
|  
|  
| 以下是独立部署的应用 以下服务启动后配合前端工程使用 (cloud-base-angular-admin)
|
|--cloud-gateway 应用网关
|
|--authorize-center 集成了modules-lh-security 的授权中心,提供统一授权和鉴权
|  
|--code-generator 代码生成工具
|
|--user-center 用户中心 提供用户管理和权限管理的相关服务
|
|--youji-manage-server 集成了modules-youji-task 的定时任务管理服务端

七、版本使用说明

<springboot.version>2.4.2</springboot.version>
<springcloud.version>2020.0.3</springcloud.version>
<springcloud-alibaba.version>2021.1</springcloud-alibaba.version>

八、多环境打包说明

在需要独立打包的模块resources资源目录下增加不同环境的配置文件

application-dev.yml
application-test.yml
application-prod.yml

修改application.yml

spring:
profiles:
  active: @profileActive@

在需要独立打包的模块下的pom文件中添加一下打包配置。

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${springboot.version}</version>
<configuration>
<fork>true</fork>
<addResources>true</addResources>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<delimiters>
<delimiter>@</delimiter>
</delimiters>
<useDefaultDelimiters>false</useDefaultDelimiters>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>

<profiles>
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<profileActive>dev</profileActive>
</properties>
</profile>
<profile>
<id>test</id>
<properties>
<profileActive>test</profileActive>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<profileActive>prod</profileActive>
</properties>
</profile>
</profiles>

mvn打包命令

# 打开发环境
mvn clean package -P dev -Dmaven.test.skip=ture
# 打测试环境
mvn clean package -P test -Dmaven.test.skip=ture
# 打生产环境
mvn clean package -P prod -Dmaven.test.skip=ture

九、构建Docker镜像

整合dockerfile插件,可直接将jar包构建为docker image 并推送到远程仓库

增加插件依赖

<!-- docker image build -->
<plugin>
  <groupId>com.spotify</groupId>
  <artifactId>dockerfile-maven-plugin</artifactId>
  <version>1.4.10</version>
  <executions>
      <execution>
          <id>default</id>
          <goals>
              <!--如果package时不想用docker打包,就注释掉这个goal-->
              <!--                       <goal>build</goal>-->
              <goal>push</goal>
          </goals>
      </execution>
  </executions>
  <configuration>
      <repository>49.232.166.94:8099/example/${project.artifactId}</repository>
      <tag>${profileActive}-${project.version}</tag>
      <username>admin</username>
      <password>Harbor12345</password>
      <buildArgs>
          <JAR_FILE>target/${project.build.finalName}.jar</JAR_FILE>
      </buildArgs>
  </configuration>
</plugin>

在pom.xml同级目录下增加Dockerfile

FROM registry.cn-hangzhou.aliyuncs.com/lh0811/lh0811-docer:lh-jdk1.8-0.0.1
MAINTAINER lh0811
ADD ./target/${JAR_FILE} /opt/app.jar
RUN chmod +x /opt/app.jar
CMD java -jar /opt/app.jar

十、源码获取

源码和开发笔记

作者:我先失陪了
来源:https://juejin.cn/post/7100457917115007013

收起阅读 »

JavaScript中的事件委托

事件委托基本概念事件委托,就是一个元素的响应事件的函数委托给另一个元素一般我们都是把函数绑定给当前元素的父元素或更外层元素,当事件响应到需要绑定的元素的时候,会通过事件冒泡机制(或事件捕获)去触发外层元素的绑定事件,在外层元素上去执行函数在了解事件委托之前,我...
继续阅读 »

事件委托基本概念

事件委托,就是一个元素的响应事件的函数委托给另一个元素

一般我们都是把函数绑定给当前元素的父元素或更外层元素,当事件响应到需要绑定的元素的时候,会通过事件冒泡机制(或事件捕获)去触发外层元素的绑定事件,在外层元素上去执行函数

在了解事件委托之前,我们可以先了解事件流,事件冒泡以及事件捕获

事件流:捕获阶段,目标阶段,冒泡阶段

DOM事件流有3个阶段:捕获阶段,目标阶段,冒泡阶段;

三个阶段的顺序为:捕获阶段——目标阶段——冒泡阶段

事件冒泡

事件的触发响应会从最底层目标一层层地向外到最外层(根节点)

比如说我现在有一个盒子f,里面有个子元素s

  <div class="f">
       <div class="s"></div>
 </div>

添加事件

var f = document.querySelector('.f')
var s = document.querySelector('.s')
f.addEventListener('click',()=>{
   console.log('fffff');
})
s.addEventListener('click',()=>{
   console.log('sssss');
})

当我点击子元素的时候

冒泡顺序 s -> f

事件捕获

事件响应从最外层的Window开始,逐级向内层前进,直到具体事件目标元素。在捕获阶段,不会处理响应元素注册的冒泡事件

继续使用上一个例子,只需要将addEventListener第三个参数改为true即可

添加事件

var f = document.querySelector('.f')
var s = document.querySelector('.s')
f.addEventListener('click',()=>{
   console.log('fffff');
},true)
s.addEventListener('click',()=>{
   console.log('sssss');
},true)

点击子元素

捕获顺序 f -> s

这里我们可以思考一下,如果同时绑定了冒泡和捕获事件的话,会有怎样的执行顺序呢?

例子不变,稍微改一下js代码

var f = document.querySelector('.f')
var s = document.querySelector('.s')
f.addEventListener('click',()=>{
   console.log('f捕获');
},true)
s.addEventListener('click',()=>{
   console.log('s捕获');
},true)
f.addEventListener('click',()=>{
   console.log('f冒泡');
})
s.addEventListener('click',()=>{
   console.log('s冒泡');
})

此时点击子元素

执行顺序: f捕获->s捕获->s冒泡—>f冒泡

得出结论:当我们同时绑定捕获和冒泡事件的时候,会先从外层开始捕获到目标元素,然后由目标元素冒泡到外层

回到事件委托

了解了事件捕获和事件冒泡,再来看事件委托就很好理解了

强调一遍,事件委托把函数绑定给当前元素的父元素或更外层元素,当事件响应到需要绑定的元素的时候,会通过事件冒泡机制(或事件捕获)去触发外层元素的绑定事件,在外层元素上去执行函数

新开一个例子

<ul class="list">
       <li class="item"></li>
       <li class="item"></li>
       <li class="item"></li>
       <li class="item"></li>
       <li class="item"></li>
</ul>

现在我们有一个列表,当我们点击列表中的某一项时可以触发对应事件,如果我们给列表的每一项都添加事件,对于内存消耗是非常大的,效率上需要消耗很多性能

这个时候我们就可以把这个点击事件绑定到他的父层,也就是 ul 上,然后在执行事件的时候再去匹配判断目标元素;

var list = document.querySelector('.list')
// 利用冒泡机制实现
list.addEventListener('click',(e)=>{
  e.target.style.backgroundColor='blue'
})
// 利用捕获机制实现
list.addEventListener('click',(e)=>{
  e.target.style.backgroundColor='red'
},true)

当我点击其中一个子元素的时候


总结

  • 事件委托就是根据事件冒泡或事件捕获的机制来实现的

  • 事件冒泡就是事件的触发响应会从最底层目标一层层地向外到最外层(根节点)

  • 事件捕获就是事件响应从最外层的Window开始,逐级向内层前进,直到具体事件目标元素。在捕获阶段,不会处理响应元素注册的冒泡事件

补充:

对于目标元素,捕获和冒泡的执行顺序是由绑定事件的执行顺序决定的

作者:张宏都
来源:https://juejin.cn/post/7100468737647575048

收起阅读 »

axios 请求拦截器&响应拦截器

一、 拦截器介绍一般在使用axios时,会用到拦截器的功能,一般分为两种:请求拦截器、响应拦截器。请求拦截器 在请求发送前进行必要操作处理,例如添加统一cookie、请求体加验证、设置请求头等,相当于是对每个接口里相同操作的一个封装;响应拦截器 同理,响应拦截...
继续阅读 »

一、 拦截器介绍

一般在使用axios时,会用到拦截器的功能,一般分为两种:请求拦截器、响应拦截器。

  1. 请求拦截器
    在请求发送前进行必要操作处理,例如添加统一cookie、请求体加验证、设置请求头等,相当于是对每个接口里相同操作的一个封装;

  2. 响应拦截器
    同理,响应拦截器也是如此功能,只是在请求得到响应之后,对响应体的一些处理,通常是数据统一处理等,也常来判断登录失效等。

二、 Axios实例

  1. 创建axios实例

// 引入axios
import axios from 'axios'

// 创建实例
let instance = axios.create({
   baseURL: 'xxxxxxxxxx',
   timeout: 15000  // 毫秒
})
  1. baseURL设置:

let baseURL;
if(process.env.NODE_ENV === 'development') {
   baseURL = 'xxx本地环境xxx';
} else if(process.env.NODE_ENV === 'production') {
   baseURL = 'xxx生产环境xxx';
}

// 实例
let instance = axios.create({
   baseURL: baseURL,
   ...
})
  1. 修改实例配置的三种方式

// 第一种:局限性比较大
axios.defaults.timeout = 1000;
axios.defaults.baseURL = 'xxxxx';

// 第二种:实例配置
let instance = axios.create({
   baseURL: 'xxxxx',
   timeout: 1000,  // 超时,401
})
// 创建完后修改
instance.defaults.timeout = 3000

// 第三种:发起请求时修改配置、
instance.get('/xxx',{
   timeout: 5000
})

这三种修改配置方法的优先级如下:请求配置 > 实例配置 > 全局配置

三、 配置拦截器

// 请求拦截器
instance.interceptors.request.use(req=>{}, err=>{});
// 响应拦截器
instance.interceptors.reponse.use(req=>{}, err=>{});
  1. 请求拦截器

// use(两个参数)
axios.interceptors.request.use(req => {
   // 在发送请求前要做的事儿
   ...
   return req
}, err => {
   // 在请求错误时要做的事儿
   ...
   // 该返回的数据则是axios.catch(err)中接收的数据
   return Promise.reject(err)
})
  1. 响应拦截器

// use(两个参数)
axios.interceptors.reponse.use(res => {
   // 请求成功对响应数据做处理
   ...
   // 该返回的数据则是axios.then(res)中接收的数据
   return res
}, err => {
   // 在请求错误时要做的事儿
   ...
   // 该返回的数据则是axios.catch(err)中接收的数据
   return Promise.reject(err)
})
  1. 常见错误码处理(error)
    axios请求错误时,可在catch里进行错误处理。

axios.get().then().catch(err => {
   // 错误处理
})

四、 axios请求拦截器的案例

// 设置请求拦截器
axios.interceptors.request.use(
 config => {
   // console.log(config) // 该处可以将config打印出来看一下,该部分将发送给后端(server端)
   config.headers.Authorization<