注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Kotlin flow实践总结

背景 最近学了下Kotlin Flow,顺便在项目中进行了实践,做一下总结。 Flow是什么 按顺序发出多个值的数据流。 本质就是一个生产者消费者模型,生产者发送数据给消费者进行消费。 冷流:当执行collect的时候(也就是有消费者的时候),生产者才开...
继续阅读 »

背景


最近学了下Kotlin Flow,顺便在项目中进行了实践,做一下总结。


image.png


Flow是什么


按顺序发出多个值的数据流。

本质就是一个生产者消费者模型,生产者发送数据给消费者进行消费。


Image.png



  • 冷流:当执行collect的时候(也就是有消费者的时候),生产者才开始发射数据流。

    生产者与消费者是一对一的关系。当生产者发送数据的时候,对应的消费者才可以收到数据。

  • 热流:不管有没有执行collect(也就是不管有没有消费者),生产者都会发射数据流到内存中。

    生产者与消费者是一对多的关系。当生产者发送数据的时候,多个消费者都可以收到数据


实践场景


场景一:简单列表数据的加载状态


简单的列表显示场景,可以使用onStart,onEmpty,catch,onCompletion等回调操作符,监听数据流的状态,显示相应的加载状态UI。



  • onStart:在数据发射之前触发,onStart所在的线程,是数据产生的线程

  • onCompletion:在数据流结束时触发,onCompletion所在的线程,是数据产生的线程

  • onEmpty:当数据流结束了,缺没有发出任何元素的时候触发。

  • catch:数据流发生错误的时候触发

  • flowOn:指定上游数据流的CoroutineContext,下游数据流不会受到影响


private fun coldFlowDemo() {
//创建一个冷流,在3秒后发射一个数据
val coldFlow = flow<Int> {
delay(3000)
emit(1)
}
lifecycleScope.launch(Dispatchers.IO) {
coldFlow.onStart {
Log.d(TAG, "coldFlow onStart, thread:${Thread.currentThread().name}")
mBinding.progressBar.isVisible = true
mBinding.tvLoadingStatus.text = "加载中"
}.onEmpty {
Log.d(TAG, "coldFlow onEmpty, thread:${Thread.currentThread().name}")
mBinding.progressBar.isVisible = false
mBinding.tvLoadingStatus.text = "数据加载为空"
}.catch {
Log.d(TAG, "coldFlow catch, thread:${Thread.currentThread().name}")
mBinding.progressBar.isVisible = false
mBinding.tvLoadingStatus.text = "数据加载错误:$it"
}.onCompletion {
Log.d(TAG, "coldFlow onCompletion, thread:${Thread.currentThread().name}")
mBinding.progressBar.isVisible = false
mBinding.tvLoadingStatus.text = "加载完成"
}
//指定上游数据流的CoroutineContext,下游数据流不会受到影响
.flowOn(Dispatchers.Main)
.collect {
Log.d(TAG, "coldFlow collect:$it, thread:${Thread.currentThread().name}")
}
}
}

比如上面的例子。
使用flow构建起函数,创建一个冷流,3秒后发送一个值到数据流中。
使用onStart,onEmpty,catch,onCompletion操作符,监听数据流的状态。


日志输出:


coldFlow onStart, thread:main
coldFlow onCompletion, thread:main
coldFlow collect:1, thread:DefaultDispatcher-worker-1

场景二:同一种数据,需要加载本地数据和网络数据


在实际的开发场景中,经常会将一些网络数据保存到本地,下次加载数据的时候,优先使用本地数据,再使用网络数据。

但是本地数据和网络数据的加载完成时机不一样,所以可能会有下面几种场景。



  1. 本地数据比网络数据先加载完成:那先使用本地数据,再使用网络数据

  2. 网络数据比本地数据先加载完成:



  • 网络数据加载成功,那只使用网络数据即可,不需要再使用本地数据了。

  • 网络数据加载失败,可以继续尝试使用本地数据进行兜底。



  1. 本地数据和网络数据都加载失败:通知上层数据加载失败


实现CacheRepositity


将上面的逻辑进行简单封装成一个基类,CacheRepositity。

相应的子类,只需要实现两个方法即可。



  • CResult:代表加载结果,Success 或者 Error。

  • fetchDataFromLocal(),实现本地数据读取的逻辑

  • fetchDataFromNetWork(),实现网络数据获取的逻辑


abstract class CacheRepositity<T> {
private val TAG = "CacheRepositity"

fun getData() = channelFlow<CResult<T>> {
supervisorScope {
val dataFromLocalDeffer = async {
fetchDataFromLocal().also {
Log.d(TAG,"fetchDataFromLocal result:$it , thread:${Thread.currentThread().name}")
//本地数据加载成功
if (it is CResult.Success) {
send(it)
}
}
}

val dataFromNetDeffer = async {
fetchDataFromNetWork().also {
Log.d(TAG,"fetchDataFromNetWork result:$it , thread:${Thread.currentThread().name}")
//网络数据加载成功
if (it is CResult.Success) {
send(it)
//如果网络数据已加载,可以直接取消任务,就不需要处理本地数据了
dataFromLocalDeffer.cancel()
}
}
}

//本地数据和网络数据,都加载失败的情况
val localData = dataFromLocalDeffer.await()
val networkData = dataFromNetDeffer.await()
if (localData is CResult.Error && networkData is CResult.Error) {
send(CResult.Error(Throwable("load data error")))
}
}
}

protected abstract suspend fun fetchDataFromLocal(): CResult<T>

protected abstract suspend fun fetchDataFromNetWork(): CResult<T>

}

sealed class CResult<out R> {
data class Success<out T>(val data: T) : CResult<T>()
data class Error(val throwable: Throwable) : CResult<Nothing>()
}

测试验证


写个TestRepositity,实现CacheRepositity的抽象方法。

通过delay延迟耗时来模拟各种场景,观察日志的输出顺序。


private fun cacheRepositityDemo(){
val repositity=TestRepositity()
lifecycleScope.launch {
repositity.getData().onStart {
Log.d(TAG, "TestRepositity: onStart")
}.onCompletion {
Log.d(TAG, "TestRepositity: onCompletion")
}.collect {
Log.d(TAG, "collect: $it")
}
}
}

本地数据比网络数据加载快


class TestRepositity : CacheRepositity<String>() {
override suspend fun fetchDataFromLocal(): CResult<String> {
delay(1000)
return CResult.Success("data from fetchDataFromLocal")
}

override suspend fun fetchDataFromNetWork(): CResult<String> {
delay(2000)
return CResult.Success("data from fetchDataFromNetWork")
}
}

模拟数据:本地加载delay1秒,网络加载delay2秒

日志输出:collect 执行两次,先收到本地数据,再收到网络数据。


onStart
fetchDataFromLocal result:Success(data=data from fetchDataFromLocal) , thread:main
collect: Success(data=data from fetchDataFromLocal)
fetchDataFromNetWork result:Success(data=data from fetchDataFromNetWork) , thread:main
collect: Success(data=data from fetchDataFromNetWork)
onCompletion

网络数据比本地数据加载快


class TestRepositity : CacheRepositity<String>() {
override suspend fun fetchDataFromLocal(): CResult<String> {
delay(2000)
return CResult.Success("data from fetchDataFromLocal")
}

override suspend fun fetchDataFromNetWork(): CResult<String> {
delay(1000)
return CResult.Success("data from fetchDataFromNetWork")
}
}

模拟数据:本地加载delay 2秒,网络加载delay 1秒

日志输出:collect 只执行1次,只收到网络数据。


onStart
fetchDataFromNetWork result:Success(data=data from fetchDataFromNetWork) , thread:main
collect: Success(data=data from fetchDataFromNetWork)
onCompletion

网络数据加载失败,使用本地数据


class TestRepositity : CacheRepositity<String>() {
override suspend fun fetchDataFromLocal(): CResult<String> {
delay(2000)
return CResult.Success("data from fetchDataFromLocal")
}

override suspend fun fetchDataFromNetWork(): CResult<String> {
delay(1000)
return CResult.Error(Throwable("fetchDataFromNetWork Error"))
}
}

模拟数据:本地加载delay 2秒,网络数据加载失败

日志输出:collect 只执行1次,只收到本地数据。


onStart
fetchDataFromNetWork result:Error(throwable=java.lang.Throwable: fetchDataFromNetWork Error) , thread:main
fetchDataFromLocal result:Success(data=data from fetchDataFromLocal) , thread:main
collect: Success(data=data from fetchDataFromLocal)
onCompletion

网络数据和本地数据都加载失败


class TestRepositity : CacheRepositity<String>() {
override suspend fun fetchDataFromLocal(): CResult<String> {
delay(2000)
return CResult.Error(Throwable("fetchDataFromLocal Error"))
}

override suspend fun fetchDataFromNetWork(): CResult<String> {
delay(1000)
return CResult.Error(Throwable("fetchDataFromNetWork Error"))
}
}

模拟数据:本地数据加载失败,网络数据加载失败

日志输出: collect 只执行1次,结果是CResult.Error,代表加载数据失败。


onStart
fetchDataFromNetWork result:Error(throwable=java.lang.Throwable: fetchDataFromNetWork Error) , thread:main
fetchDataFromLocal result:Error(throwable=java.lang.Throwable: fetchDataFromLocal Error) , thread:main
collect: Error(throwable=java.lang.Throwable: load data error)
onCompletion

场景三:多种数据源,按照顺序合并进行展示


Image.png


在实际的开发场景中,经常一个页面的数据,是需要发起多个网络请求之后,组合数据之后再进行显示。
比如类似这种页面,3种数据,需要由3个网络请求获取得到,然后再进行相应的显示。


实现目标:



  1. 接口间不需要互相等待,哪些数据先回来,就先展示哪部分

  2. 控制数据的显示顺序


flow combine操作符


可以合并多个不同的 Flow 数据流,生成一个新的流。
只要其中某个子 Flow 数据流有产生新数据的时候,就会触发 combine 操作,进行重新计算,生成一个新的数据。


例子


class HomeViewModel : ViewModel() {

//暴露给View层的列表数据
val list = MutableLiveData<List<String?>>()

//多个子Flow,这里简单都返回String,实际场景根据需要,返回相应的数据类型即可
private val bannerFlow = MutableStateFlow<String?>(null)
private val channelFlow = MutableStateFlow<String?>(null)
private val listFlow = MutableStateFlow<String?>(null)


init {
//使用combine操作符
viewModelScope.launch {
combine(bannerFlow, channelFlow, listFlow) { bannerData, channelData, listData ->
Log.d("HomeViewModel", "combine bannerData:$bannerData,channelData:$channelData,listData:$listData")
//只要子flow里面的数据不为空,就放到resultList里面
val resultList = mutableListOf<String?>()
if (bannerData != null) {
resultList.add(bannerData)
}
if (channelData != null) {
resultList.add(channelData)
}
if (listData != null) {
resultList.add(listData)
}
resultList
}.collect {
//收集combine之后的数据,修改liveData的值,通知UI层刷新列表
Log.d("HomeViewModel", "collect: ${it.size}")
list.postValue(it)
}
}
}

fun loadData() {
viewModelScope.launch(Dispatchers.IO) {
//模拟耗时操作
async {
delay(1000)
Log.d("HomeViewModel", "getBannerData success")
bannerFlow.emit("Banner")
}
async {
delay(2000)
Log.d("HomeViewModel", "getChannelData success")
channelFlow.emit("Channel")
}
async {
delay(3000)
Log.d("HomeViewModel", "getListData success")
listFlow.emit("List")
}
}
}
}

HomeViewModel



  1. 提供一个 LiveData 的列表数据给View层使用

  2. 内部有3个子 flow ,分别负责相应数据的生产。(这里简单都返回String,实际场景根据需要,返回相应的数据类型即可)。

  3. 通过 combine 操作符,组合这3个子flow的数据。

  4. collect 接收生成的新数据,并修改liveData的数据,通知刷新UI


View层使用


private fun flowCombineDemo() {
val homeViewModel by viewModels<HomeViewModel>()
homeViewModel.list.observe(this) {
Log.d("HomeViewModel", "observe size:${it.size}")
}
homeViewModel.loadData()
}

简单的创建一个 ViewModel ,observe 列表数据对应的 LiveData。

通过输出的日志发现,触发数据加载之后,每次子 Flow 流生产数据的时候,都会触发一次 combine 操作,生成新的数据。


日志输出:
combine bannerData:null,channelData:null,listData:null
collect: 0
observe size:0

getBannerData success
combine bannerData:Banner,channelData:null,listData:null
collect: 1
observe size:1

getChannelData success
combine bannerData:Banner,channelData:Channel,listData:null
collect: 2
observe size:2

getListData success
combine bannerData:Banner,channelData:Channel,listData:List
collect: 3
observe size:3

总结


具体场景,具体分析。刚好这几个场景,配合Flow进行使用,整体实现也相对简单了一些。


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

Kotlin的 :: 符号是个啥?

前言 在阅读Kotlin的代码时,经常有看到 :: 这个符号,这个符号专业术语叫做成员引用,在代码中使用可以简化代码,那到底怎么使用呢以及使用的范围,这篇文章就来好好捋一下。 正文 这里虽然很熟悉,但是我们还是从简单说起,需要了解为什么这样设计。 传递函数优化...
继续阅读 »

前言


在阅读Kotlin的代码时,经常有看到 :: 这个符号,这个符号专业术语叫做成员引用,在代码中使用可以简化代码,那到底怎么使用呢以及使用的范围,这篇文章就来好好捋一下。


正文


这里虽然很熟悉,但是我们还是从简单说起,需要了解为什么这样设计。


传递函数优化


这里我们举个栗子,就看这个熟悉的sortBy排序函数,先定义People类:


//测试代码
data class People(val name: String,val age: Int){
//自定义的排序条件
fun getMax() : Int{
return age * 10 + name.length
}
}

然后我们来进行排序:


val people = People("zyh",10)
val people1 = People("zyh1",100)
val peopleList = arrayListOf(people,people1)
//给sortBy传入lambda
peopleList.sortBy { people -> people.getMax() }

这里我们给sortBy函数传递一个lambda,由于sortBy函数是内联的,所以传递给它的lambda会被内联,但是假如现在有个问题,就是这些lambda已经被定义成了函数变量,比如我定义了一个顶层函数:


//定义了一个顶层函数
fun getMaxSort(people: People): Int{
return (people.age) * 10 + people.name.length
}

或者排序条件已经定义成了一个变量值:


//排序条件
val condition = { people: People -> people.getMax() }

那这时如果我想再进行排序必须要这么写了:


//调用一遍函数
peopleList.sortBy { getMaxSort(it) }
//传递参数
peopleList.sortBy(condition)

然后这里我们可以利用成员引用 :: 符号来优化一下:


//直接就会调用顶层函数getMaxSort
peopleList.sortBy(::getMaxSort)
//直接就会调用People类的getMax函数
peopleList.sortBy(People::getMax)

这里看起来就是语法糖,可以简化代码。


成员引用 ::


你有没有想过这里是为什么,这里使用了 :: 符号其实就是把函数转换成了一个值,首先我们使用


val condition = { people: People -> people.getMax() }

这种时,其实condition就是一个函数类型的变量,这个我们之前文章说过,Kotlin支持完整的函数类型,而使用高阶函数可以用lambda,但是getMaxSort()函数它就是一个函数了,它不是一个值,除非你再外面给它包裹一层构成lambda,所以这里调用condition传递进的是sortBy()中,而getMaxSort(it)是以lambda的形式又包裹了一层。


但是使用 :: 符号后,也就是把函数转换成了一个值,比如 People::getMax 这就是一个值,它代表的就是People内的getMax函数。


而 ::getMaxSort 也是一个值,它表示getMaxSort函数。


使用范围


前面2个例子其实也就表明了这种成员引用的使用范围,一个是类的函数或者属性,还有就是顶层函数,它没有类名,可以省略。


绑定引用


这里再额外说一个知识点,前面说成员引用都是 类名:属性名 这种格式,比如 People::getMax ,但是它在后面KT版本变化后进行了优化,可以看下面代码:


//定义一个people实例
val people = People("zyh",10)
//利用成员引用,把函数转换成值
val ageFun = People::age
val age = ageFun(people)
//直接在对象实例上使用 ::
val ageValue = people::age

从上面我们发现,ageValue的值可以从实例上通过成员引用调用得到,不过这里区别就大了,ageFun是一个函数类型,而ageValue则是一个int值。


总结


总结一下,其实成员引用 :: 很简单,它就是把函数给转成了值,而这个值可以看成是函数类型,这样说就十分好理解了。


不过这个真实原理可不是这么简单,并不是利用lambda又把函数包裹了一层,这里应该是反射的相关知识,我们后续再具体来说其原理,刚好后续有反射相关的文章,大家可以点赞、关注一波。


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

一文扫清DDD核心概念理解障碍

引言 在前面的几篇文章中分别从DDD概念、核心思想以及代码落地等层面阐述了DDD的落地实践的过程,但是很多同学表示对于DDD的某些概念还是觉得不太好理解,影响了对于DDD的学习以及实际应用。因此本文针对大家反馈的问题进行详细的说明,希望可以用大白话的方式把这些...
继续阅读 »

引言


在前面的几篇文章中分别从DDD概念、核心思想以及代码落地等层面阐述了DDD的落地实践的过程,但是很多同学表示对于DDD的某些概念还是觉得不太好理解,影响了对于DDD的学习以及实际应用。因此本文针对大家反馈的问题进行详细的说明,希望可以用大白话的方式把这些看似复杂的概念形象化、简单化。


领域、子域、核心域等这么多域到底怎么理解?


在DDD的众多概念中,首先需要搞清楚的就是到底什么是领域。因为DDD是领域驱动设计,所以领域是DDD的核心基础概念。那么到底什么是领域呢?领域是指某个业务范围以及在范围内进行的活动。根据这个定义,我们知道领域最重要的一个核心点就是范围,只有限定了问题研究的范围,才能针对具体范围内的问题进行研究分析,在后期 进行微服务拆分的的时候也是根据范围来进行的。


我们开发的软件平台是为了解决用户问题,既然我们需要研究问题并解决问题,那就得先确定问题的范围到底是什么。如果我们做的是电商平台,那么我们研究的是电商这个领域或者说电商这个范围的问题,实体店内的销售情况就不是我们研究的问题范围了。因此我们可以把领域理解为我们需要解决指定业务范围内的问题域。再举个生活中的例子,派出所实际都是有自己的片区的也就是业务范围,户籍管理、治安等都是归片区的派出所负责的。这里的片区实际就是领域,派出所专注解决自己片区内的各种事项。


既然我们研究的领域确定了,或者说研究的问题域以及范围确定了,那么接下来就需要对领域进行进一步的划分和切割。实际上这和我们研究事物的一般方法手段是一致的,一旦某个问题太大无从下手的时候,都会将问题进行一步步的拆解,再逐个进行分析和解决。那么放到DDD中,我们在进行分析领域的时候,如果领域对应的业务范围过大,那么就需要对领域进行拆解划分,形成对应的子域或者说更小的问题域,所以说子域对应的是相对于领域来说,更小的业务范围以及问题域。


回到我们刚才所说的电商领域,它就是一个非常大的领域,因为电商实际还包含了商品、用户、营销、支付、订单、物流等等各种复杂的业务。因此支付域、物流域等就是相对于电商来说更小的业务范围或者更小的问题域,那么这部分领域就是对于电商这个领域的子域,相当于对电商这个业务范围的进一步的划分。


image.png


搞清楚了领域和子域的区别之后,那么怎么理解核心域、通用域以及支撑域这么多其他的域呢(域太多了,我脑袋开始嗡嗡响了)?领域和子域是按照范围大小进行区分的,那么核心域、通用域等实际就是按照功能属性进行划分的。


核心域:平台的核心竞争力、最重要的业务,比如对于阿里来说,电商就是核心域。


通用域:其他子域沉淀的通用能力,没有定制化的能力,比如公司的数据中台。


支撑域:不包含核心业务能力也不是各个子域的通用能力沉淀。


那么为什么划分了子域之后,还要分什么核心域、通用域呢?实际上这样划分的目的是根据子域的属性,确定公司对于不同域的资源投入。将优势的资源投入到具备核心竞争力的域上,也是为了让产品更加具备竞争力,就是所谓的钱要花到刀刃上。


限界上下文?限界是什么?上下文又是什么?


限界上下文我觉得是DDD中一个不太好理解的概念,光看这个不明觉厉的名字,甚至有点不知道它到底想表达什么样的意思。我们先来看下限界上下文的原文---Bounded Context,通过原文我们可以看得出来,实际上限界上下文这个翻译增加了我们的理解成本。而反观Bounded Context这个原文实际更好理解一点,即为有边界的上下文。这里给大家一个小建议,如果技术上某个概念不好理解,那么不妨去看看它的原文是什么,大部分情况下原文会比翻译过来的更好理解,更能反映设计者想要表达的真实含义。


image.png


大家都知道我们的语言是有上下文环境的,有的时候同样一句话在不同的语言环境或者说语言上下文中,所代表的意思是不一样的。打个比方假如你有一个女朋友,你们约好晚上一起去吃饭,你在去晚上吃饭地方的路上,这个时候你收到一条来自女朋友的语音:“我已经到龙翔桥了,你出来后往苹果店走。如果你到了,我还没到,你就等着吧。如果我到了,你还没到,你就等着吧。”这里的你就等着吧,在不同的语境下包含的意思是不同的,一个是陈述事实,一个让你瑟瑟发抖。


因此,既然语言本身就有上下文,那么用通用语言描述的业务肯定也是有边界的。DDD中的限界上下文就是用来圈定业务范围的,目的是为了确保业务语言在限界上下文内的表达唯一,不会产生额外的歧义。这个时候大家会不会有另外一个问题,那么这个限界上下文到底是一个逻辑概念还是代码层面会有一个实实在在的边界呢?


按照我自己的理解,限界上下文既是概念上的业务边界,也是代码层面的逻辑逻辑边界。为什么这么说呢?我们在进行业务划分的时候,领域划分为子域集合,子域再划分为子子域集合,那么子子域的业务边界有时候就会和限界上下文的边界重合,也就是说子子域本身就是限界上下文,那么此时限界上下文就是业务边界。在代码落地的过程中,用户服务涉及到用户的创建、用户信息的修改等操作。肯定不会到订单服务中去做这些事情。因为他们属于不同的业务域,也就是说订单相关的操作已经超越了用户的边界上下文,因此它应该在订单的边界上下文中进行。


域和边界上下文的关系是一对一或者一对多的关系,实际上我认为域和限界上下文本质上一致的,应该是为什么这么说呢,比如我们做的微服务当中用户服务,比如,肯定不会到订单服务中去做这些事情。因为他们属于不同的业务域,也就是说订单相关的操作已经超越了用户的边界上下文,因此它应该在订单的限界上下文中进行。限界上下文最主要的作用就是限定哪些业务面描述以及业务动作在这个限界当中。


image.png


总结
DDD在实际落地实践过程中会遇到各种各样的问题,首当其冲的就是一些核心概念晦涩难懂,阻碍了技术人员对DDD的理解和掌握。本文对DDD比较难理解的核心概念进行了详细的描述,相信通过本文大家对于这些核心概念的理解能够更加深入。


创作不易,如果各位同学觉得文章还不错的话,麻烦点赞+收藏+评论交流哦。老样子,文末和大家分享一首诗词。


西江月▪中秋和子由


世事一场大梦,人生几度秋凉?夜来风叶已鸣廊,看取眉头鬓上。


酒贱常愁客少,月明多被云妨。中秋谁与共孤光,把盏凄然北望。


作者:慕枫技术笔记
来源:https://juejin.cn/post/7084074668147081230 收起阅读 »

Redis 缓存穿透与缓存击穿

一、🐕缓存穿透(查不到数据) 比如 用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是先持久层数据库查询,发现也没有,于是本次查询失败,当用户很多的时候,缓存都没有命中时(一般为秒杀活动),于是都会去请求持久层数据库,这就会导致 给持...
继续阅读 »

一、🐕缓存穿透(查不到数据)


比如 用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是先持久层数据库查询,发现也没有,于是本次查询失败,当用户很多的时候,缓存都没有命中时(一般为秒杀活动),于是都会去请求持久层数据库,这就会导致 给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透



解决方案




布隆过滤器


布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力
在这里插入图片描述




缓存空对象


当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了持久层数据库
在这里插入图片描述


但是这种方法会存在两个问题


1、如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,并且当中还有还能多空值的键


2、即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响



二、🐂缓存击穿(量太大,缓存过期)


这里需要注意和缓存击穿的区别,缓存击穿,是指一个key非常热点,在不停的杠大并发,大并发集中对这个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一堵墙上戳穿了一个洞
当某个key 在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且写回缓存,会导致数据库瞬间压力过大



解决方案



设置热点数据永不过期



从缓存层面上看,没有设置过期时间,所以不会出现热点key过期后产生的问题



加互斥锁



分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获取分布式锁的权限,因此只需要等待即可,这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大



三、🐅缓存雪崩


缓存雪崩,是指在某一个时间段,缓存集中过期失效,Redis宕机


产生雪崩的原因之一,比如在双十一零点,有一波商品比较热门,所以这波商品会放在缓存,假设缓存一个小时,那么到了1点钟时,缓存即将过期,此时如果再对此波商品进行访问查询,压力最终就会落到数据库上,对于数据库而言,就会产生周期性的压力波峰,于是所有的请求都会到达存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。


在这里插入图片描述


其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,此时,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。



解决方案


1、Redis 高可用


既然 redis 有可能挂掉,那我就设多几台 redis ,其实就是搭建集群


2、限流降级


在缓存失效后,通过加锁或者 队列来控制读数据库写缓存的线程数量,比如对某个key 只允许一个线程查询数据和写缓存,其他线程等待


3、数据预热


数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中,在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀



作者:CSDNCoder
来源:https://juejin.cn/post/7084820115505545247
收起阅读 »

Python 中的万能之王 Lambda 函数

Python 提供了非常多的库和内置函数。有不同的方法可以执行相同的任务,而在 Python 中,有个万能之王函数:lambda 函数,它可以以不同的方式在任何地方使用。今天将和大家一起研究下这个万能之王!Lambda 函数简介Lambda函数也被称为匿名(没...
继续阅读 »

Python 提供了非常多的库和内置函数。有不同的方法可以执行相同的任务,而在 Python 中,有个万能之王函数:lambda 函数,它可以以不同的方式在任何地方使用。今天将和大家一起研究下这个万能之王!

Lambda 函数简介

Lambda函数也被称为匿名(没有名称)函数,它直接接受参数的数量以及使用该参数执行的条件或操作,该参数以冒号分隔,并返回最终结果。为了在大型代码库上编写代码时执行一项小任务,或者在函数中执行一项小任务,便在正常过程中使用lambda函数。
lambda argument_list:expersion
argument_list是参数列表,它的结构与Python中函数(function)的参数列表是一样的

a,b
a=1,b=2
*args
**kwargs
a,b=1,*args

....

expression是一个关于参数的表达式,表达式中出现的参数需要在argument_list中有定义,并且表达式只能是单行的。

1
None
a+b
sum(a)
1 if a >10 else 0
[i for i in range(10)]
...

普通函数和Lambda函数的区别

  1. 没有名称
    Lambda函数没有名称,而普通操作有一个合适的名称。

  2. Lambda函数没有返回值
    使用def关键字构建的普通函数返回值或序列数据类型,但在Lambda函数中返回一个完整的过程。假设我们想要检查数字是偶数还是奇数,使用lambda函数语法类似于下面的代码片段。

    b = lambda x: "Even" if x%2==0 else "Odd" b(9)

  3. 函数只在一行中
    Lambda函数只在一行中编写和创建,而在普通函数的中使用缩进

  4. 不用于代码重用
    Lambda函数不能用于代码重用,或者不能在任何其他文件中导入这个函数。相反,普通函数用于代码重用,可以在外部文件中使用。

为什么要使用Lambda函数?
一般情况下,我们不使用Lambda函数,而是将其与高阶函数一起使用。高阶函数是一种需要多个函数来完成任务的函数,或者当一个函数返回任何另一个函数时,可以选择使用Lambda函数。

什么是高阶函数?
通过一个例子来理解高阶函数。假设有一个整数列表,必须返回三个输出。

  • 一个列表中所有偶数的和

  • 一个列表中所有奇数的和

  • 一个所有能被三整除的数的和
    首先假设用普通函数来处理这个问题。在这种情况下,将声明三个不同的变量来存储各个任务,并使用一个for循环处理并返回结果三个变量。该方法常规可正常运行。

现在使用Lambda函数来解决这个问题,那么可以用三个不同的Lambda函数来检查一个待检验数是否是偶数,奇数,还是能被三整除,然后在结果中加上一个数。

def return_sum(func, lst):
result = 0
for i in lst:
  #if val satisfies func
  if func(i):
    result = result + i
return result
lst = [11,14,21,56,78,45,29,28]
x = lambda a: a%2 == 0
y = lambda a: a%2 != 0
z = lambda a: a%3 == 0
print(return_sum(x, lst))
print(return_sum(y, lst))
print(return_sum(z, lst))

这里创建了一个高阶函数,其中将Lambda函数作为一个部分传递给普通函数。其实这种类型的代码在互联网上随处可见。然而很多人在使用Python时都会忽略这个函数,或者只是偶尔使用它,但其实这些函数真的非常方便,同时也可以节省更多的代码行。接下来我们一起看看这些高阶函数。

Python内置高阶函数

Map函数

map() 会根据提供的函数对指定序列做映射。

Map函数是一个接受两个参数的函数。第一个参数 function 以参数序列中的每一个元素调用 function 函数,第二个是任何可迭代的序列数据类型。返回包含每次 function 函数返回值的新列表。

map(function, iterable, ...)
Map函数将定义在迭代器对象中的某种类型的操作。假设我们要将数组元素进行平方运算,即将一个数组的每个元素的平方映射到另一个产生所需结果的数组。

arr = [2,4,6,8] 
arr = list(map(lambda x: x*x, arr))
print(arr)

我们可以以不同的方式使用Map函数。假设有一个包含名称、地址等详细信息的字典列表,目标是生成一个包含所有名称的新列表。

students = [
          {"name": "John Doe",
            "father name": "Robert Doe",
            "Address": "123 Hall street"
            },
          {
            "name": "Rahul Garg",
            "father name": "Kamal Garg",
            "Address": "3-Upper-Street corner"
          },
          {
            "name": "Angela Steven",
            "father name": "Jabob steven",
            "Address": "Unknown"
          }
]
print(list(map(lambda student: student['name'], students)))
>>> ['John Doe', 'Rahul Garg', 'Angela Steven']

上述操作通常出现在从数据库或网络抓取获取数据等场景中。

Filter函数

Filter函数根据给定的特定条件过滤掉数据。即在函数中设定过滤条件,迭代元素,保留返回值为True 的元素。Map 函数对每个元素进行操作,而 filter 函数仅输出满足特定要求的元素。

假设有一个水果名称列表,任务是只输出那些名称中包含字符“g”的名称。

fruits = ['mango', 'apple', 'orange', 'cherry', 'grapes'] 
print(list(filter(lambda fruit: 'g' in fruit, fruits)))

filter(function or None, iterable) --> filter object

返回一个迭代器,为那些函数或项为真的可迭代项。如果函数为None,则返回为真的项。

Reduce函数

这个函数比较特别,不是 Python 的内置函数,需要通过from functools import reduce 导入。Reduce 从序列数据结构返回单个输出值,它通过应用一个给定的函数来减少元素。

reduce(function, sequence[, initial]) -> value

将包含两个参数的函数(function)累计应用于序列(sequence)的项,从左到右,从而将序列reduce至单个值。

如果存在initial,则将其放在项目之前的序列,并作为默认值时序列是空的。

假设有一个整数列表,并求得所有元素的总和。且使用reduce函数而不是使用for循环来处理此问题。

from functools import reduce
lst = [2,4,6,8,10]
print(reduce(lambda x, y: x+y, lst))
>>> 30

还可以使用 reduce 函数而不是for循环从列表中找到最大或最小的元素。

lst = [2,4,6,8]
# 找到最大元素
print(reduce(lambda x, y: x if x>y else y, lst))
# 找到最小元素
print(reduce(lambda x, y: x if x<y else y, lst))

高阶函数的替代方法

列表推导式

其实列表推导式只是一个for循环,用于添加新列表中的每一项,以从现有索引或一组元素创建一个新列表。之前使用map、filter和reduce完成的工作也可以使用列表推导式完成。然而,相比于使用Map和filter函数,很多人更喜欢使用列表推导式,也许是因为它更容易应用和记忆。

同样使用列表推导式将数组中每个元素进行平方运算,水果的例子也可以使用列表推导式来解决。

arr = [2,4,6,8]
arr = [i**2 for i in arr]
print(arr)
fruit_result = [fruit for fruit in fruits if 'g' in fruit]
print(fruit_result)

字典推导式

与列表推导式一样,使用字典推导式从现有的字典创建一个新字典。还可以从列表创建字典。

假设有一个整数列表,需要创建一个字典,其中键是列表中的每个元素,值是列表中的每个元素的平方。

lst = [2,4,6,8]
D1 = {item:item**2 for item in lst}
print(D1)
>>> {2: 4, 4: 16, 6: 36, 8: 64}
# 创建一个只包含奇数元素的字典
arr = [1,2,3,4,5,6,7,8]
D2 = {item: item**2 for item in arr if item %2 != 0}
print(D2)
>>> {1: 1, 3: 9, 5: 25, 7: 49}

一个简单应用

如何快速找到多个字典的公共键

方法一

dl = [d1, d2, d3] # d1, d2, d3为字典,目标找到所有字典的公共键
[k for k in dl[0] if all(map(lambda d: k in d, dl[1:]))]

dl = [{1:'life', 2: 'is'}, 
{1:'short', 3: 'i'},
{1: 'use', 4: 'python'}]
[k for k in dl[0] if all(map(lambda d: k in d, dl[1:]))]
# 1

解析

# 列表表达式遍历dl中第一个字典中的键
[k for k in dl[0]]
# [1, 2]

# lambda 匿名函数判断字典中的键,即k值是否在其余字典中
list(map(lambda d: 1 in d, dl[1:]))
# [True, True]
list(map(lambda d: 2 in d, dl[1:]))
#[False, False]

# 列表表达式条件为上述结果([True, True])全为True,则输出对应的k值
#1

方法二

# 利用集合(set)的交集操作
from functools import reduce
# reduce(lambda a, b: a*b, range(1,11)) # 10!
reduce(lambda a, b: a & b, map(dict.keys, dl))

写在最后
目前已经学习了Lambda函数是什么,以及Lambda函数的一些使用方法。随后又一起学习了Python中的高阶函数,以及如何在高阶函数中使用lambda函数。除此之外,还学习了高阶函数的替代方法:在列表推导式和字典推导式中执行之前操作。虽然这些方法看似简单,或者说你之前已经见到过这类方法,但你很可能很少使用它们。你可以尝试在其他更加复杂的函数中使用它们,以便使代码更加简洁。

作者:编程学习网
来源:https://juejin.cn/post/7084062324981497870

收起阅读 »

Flutter bottomSheet 高度自适应及溢出处理

最近在创建 bottomSheet的时候遇到一个问题:弹窗的高度无法根据其内容自适应 先放上显示弹窗的代码,如下: Future<T?> showSheet<T>( BuildContext context, Widg...
继续阅读 »

最近在创建 bottomSheet的时候遇到一个问题:弹窗的高度无法根据其内容自适应



先放上显示弹窗的代码,如下:


Future<T?> showSheet<T>(
BuildContext context,
Widget body, {
bool scrollControlled = false,
Color bodyColor = Colors.white,
EdgeInsets? bodyPadding,
BorderRadius? borderRadius,
}) {
const radius = Radius.circular(16);
borderRadius ??= const BorderRadius.only(topLeft: radius, topRight: radius);
bodyPadding ??= const EdgeInsets.all(20);
return showModalBottomSheet(
context: context,
elevation: 0,
backgroundColor: bodyColor,
shape: RoundedRectangleBorder(borderRadius: borderRadius),
barrierColor: Colors.black.withOpacity(0.25),
// A处
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height - MediaQuery.of(context).viewPadding.top),
isScrollControlled: scrollControlled,
builder: (ctx) => Padding(
padding: EdgeInsets.only(
left: bodyPadding!.left,
top: bodyPadding.top,
right: bodyPadding.right,
// B处
bottom: bodyPadding.bottom + MediaQuery.of(ctx).viewPadding.bottom,
),
child: body,
));
}


其中,A处、B处的作用就是,让弹窗的内容始终显示在安全区域内



高度自适应问题


首先,我们在弹窗中显示点内容:


showSheet(context, Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: const [
Text('这是第一行'),
],
));

效果如下图所示:




此时,我们只需要将显示内容的代码改为如下:


showSheet(context, Column(
mainAxisSize: MainAxisSize.min, // 这一行是关键所在
crossAxisAlignment: CrossAxisAlignment.stretch,
children: const [
Text('这是第一行'),
],
));

现在的效果图如下:




现在我们可以看到,弹窗的高度已经根据内容自适应了。


内容溢出问题


前面的解决方式,仅在内容高度小于默认高度时有效。当内容过多,高度大于默认高度时,就会出现溢出警告,如下图所示:




此时,我们该怎么办呢?


答案是:运用 showModalBottomSheet 的 isScrollControlled 参数,将其设置为true即可,代码如下:


showSheet(context, Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: const [
Text('这是第一行'),
Text('这是很长长..(此处省略若干字)..的一段话,')
],
), scrollControlled: true); // 这一行用于告诉系统,弹窗的内容完全由我们自己管理

此时,效果图如下:



showSheet 补充说明


对前面showSheet代码中,A处、B处的进一步说明:


A处:如果不对内容的高度进行限制,则内容会显示在状态栏之后,而引起用户交互问题。如下图所示:



B处:如果不加 MediaQuery.of(ctx).viewPadding.bottom 这一句,则内容有可能会显示在底部横条的下方,此时也不利于交互


最终版本图样


内容较少(高度跟随内容自适应):



内容很多(顶部、底部均显示在安全区域内):



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

2022了,来体验下 flutter web

前言 flutter从 17年 推出,18年12月 开始发布 1.0 版本,2021年3月 发布 2.0 增加了对桌面和 web 应用的支持。 最大特点是基于skia实现自绘引擎,使用dart语言开发,既支持JIT(just in time: 即时编译)又支持...
继续阅读 »

前言


flutter从 17年 推出,18年12月 开始发布 1.0 版本,2021年3月 发布 2.0 增加了对桌面和 web 应用的支持。
最大特点是基于skia实现自绘引擎,使用dart语言开发,既支持JIT(just in time: 即时编译)又支持AOT(ahead of time: 提前编译),开发阶段使用JIT模式提高时效性,同时在发布阶段使用AOT模式提高编译性能。
作为前端的话,还是更关注flutter web的支持情况。为了体验flutter web,特意用flutter写了个小游戏来看编译后的代码在web上运行如何。


开始之前


早在3年前的 19年初 1.0 出来没多久的时候就尝试用flutter来写一些常见的菜单注册登录等页面demo来,那时候flutter的生态还在发展中,除了官方提供的一些解决方案,三方的一些包很多都不成体系,应用范围较小,由于当时是抱着前端的固有思路来尝鲜flutter,flutter 刚发展起来,轮子远没有那么多,发现写起来远没有Vue、React 这类生态成熟的框架写起来舒服,除了 widget 组件多,写起UI来可以直接看文档写完很方便外,网络请求,路由管理、状态管理(这些像vue有axios/vue-router/vuex)用官方的方法写起来相当麻烦(也可能是我不会用,对新手不友好),维护起来就更麻烦了。


过去3年了,再看flutter,2.0版本发布也快一年了,当再次想用flutter写个demo的时候,发现了社区已经出现了一些经过几年发展的provider、getx之类的状态管理框架,能帮助新手快速入门,用了 getx 感觉是个脚手架,又不仅仅是脚手架,简直是大而全的轮子,状态管理、路由管理一应俱全,生成的目录结构清晰,你只需要去填充 UI 和处理数据。用法也很简单,对新手很友好。


flutter + getx 写一个小游戏


既然选好了那就用 getx 生成项目目录,开始开发,选用了一个很常见的小游戏:数字华容道,功能也简单。 项目地址


项目可以打包成原生应用,也可以打包成 web 应用


数字华容道web版


flutter web 渲染模式


不同的渲染器在不同场景下各有优势,因此 Flutter 同时支持以下两种渲染模式:


HTML 渲染器: 结合了 HTML 元素、CSS、Canvas 和 SVG。该渲染模式的下载文件体积较小。
CanvasKit 渲染器: 渲染效果与 Flutter 移动和桌面端完全一致,性能更好,widget 密度更高,但增加了约 2MB 的下载文件体积。
为了针对每个设备的特性优化您的 Flutter web 应用,渲染模式默认设置为自动。这意味着您的应用将在移动浏览器上使用 HTML 渲染器运行,在桌面浏览器上使用 CanvasKit 渲染器运行。官方文档




使用 HTML 渲染


flutter run -d chrome --web-renderer html
复制代码

使用 HTML,CSS,Canvas 和 SVG 元素来渲染,应用的大小相对较小,元素数量多,请求都是http2


元素如下



请求如下



使用 CanvasKit 渲染


CanvasKit 是以 WASM 为编译目标的Web平台图形绘制接口,其目标是将 Skia 的图形 API 导出到 Web 平台。


flutter run -d chrome --web-renderer canvaskit
复制代码

默认 CanvasKit 渲染,元素数量比html少很多,就是需要请求 canvaskit.wasm,该文件大小7MB左右、默认在 unpkg.com 国内加载速度慢,可以将文件放到国内 cdn 以提升请求效率



元素如下



请求如下,部分还使用了http3



小结


flutter web 通过编译成浏览器可运行的代码,经实践来看,性能还是有些问题,不过如果是单单想要写SPA,那恐怕还是js首选。目前来说flutter的生态经过几年的发展已经有了很多开源轮子,但要说稳定性还无法击败js,要不要用 flutter web 就要根据实际需求来决定了。


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

Flutter 项目复盘 纯纯的实战经验

一. 项目开始 1. 新建flutter项目时首先明确native端用的语言 java还是kotlin , objectC 还是swift ,否则选错了后期换挺麻烦的 2. 选择自己的路由管理和状态管理包,决定项目架构 以我而言 第一个项目用的 fluro 和...
继续阅读 »

一. 项目开始


1. 新建flutter项目时首先明确native端用的语言 java还是kotlin , objectC 还是swift ,否则选错了后期换挺麻烦的


2. 选择自己的路由管理和状态管理包,决定项目架构
以我而言 第一个项目用的 fluro 和 provider 一个路由管理一个状态管理,项目目录新建store和route文件夹,分别存放provider的model文件和fluro的配置文件,到了第二个项目,发现了Getx,一个集合了依赖注入,路由管理,状态管理的包,用起来! 项目目录结构有了很大的变化,整体条理整洁


第一个 image.png


第二个 image.png


3. 常用包配置,比如 Getx 需要把外层MaterialApp换成GetMaterialApp, flutter_screenutil 需要初始化设计图比例,provider全局导入,Dio 封装,拦截器,网络提示等等


二. 全局配置


1. 复用样式


1. 由于flutter 某些小widget复用性很高,而App 需要统一样式 ,样式颜色之类的预设文件放在command文件夹内


colours.dart ,可以预设静态class,存储常用主题色
image.png


styles.dart,可以预设 字体样式 分割线样式 各种固定值间隔


image.png


2. 建议全局管理后端接口,整洁还便于维护,舒服


3. models 文件夹


models 文件夹,可能在web端并不常用,但是在dart里我觉得很需要,后端返回的Json 字符串,一定要通过model类 格式化为一个类,可以极大地减少拼写错误或者类型错误, . 语法也比 [''] 用起来舒服的多推荐一个网站 quickType 输入json对象,一键输出model类!


4. 是否强制横竖屏?


需要在main.dart里配置好


// 强制横屏
SystemChrome.setPreferredOrientations(
[DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);

5. 是否需要修改顶部 底部状态栏布局以及样式?


SystemUiOverlayStyleSystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle); 来配置


6. 设置字体不跟随系统


参考地址


class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}

class _MyAppState extends State with WidgetsBindingObserver {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Container(color: Colors.white),
builder: (context, widget) {
return MediaQuery(
//设置文字大小不随系统设置改变
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
child: widget,
);
},
);
}
}

7. 国际化配置


使用部分widget会显示英文,比如IOS风格的dialog,显示中文这需要设置一下了,
首先需要一个包支持,


  flutter_localizations:
sdk: flutter

引入包,然后在main.dart MetrialApp 的配置项中加入


   // 设置本地化,部分原生内容显示中文,例如长按选中文本显示复制、剪切
localizationsDelegates: [
GlobalMaterialLocalizations.delegate, //国际化
GlobalWidgetsLocalizations.delegate,//国际化
const FallbackCupertinoLocalisationsDelegate() // 这里是为了解决一个报错 看第 8 条
],
//国际化
supportedLocales: [
const Locale('zh', 'CH'),
const Locale('en', 'US'),
],

8. 使用CupertinoAlertDialog报错:The getter 'alertDialogLabel' was called on null


解决方法:
在main.dart中 加入如下类,然后在MetrialApp 的 localizationsDelegates 中实例化 见第 7 条


class FallbackCupertinoLocalisationsDelegate
extends LocalizationsDelegate
{
const FallbackCupertinoLocalisationsDelegate();

@override
bool isSupported(Locale locale)
=> true;

@override
Future
load(Locale locale) =>
DefaultCupertinoLocalizations.load(locale);

@override
bool shouldReload(FallbackCupertinoLocalisationsDelegate old)
=> false;
}

9. ImageCache


最近版本的flutter更新,限制了catchedImage的上限, 100张 1000mb ,而业务需求却需要缓存更多,设置一下了这需要


    class ChangeCatchImage extends WidgetsFlutterBinding {
@override
createImageCache() *{*
Global.myImageCatche = ImageCache()
..maximumSize = 1000
..maximumSizeBytes = 1000 << 20; // 1000MB
return Global.myImageCatche;
}
}

然后在main.dart runApp那里实例化一下ChangeCatchImage() 就可以了


三. 业务模块


常见的业务模块代码分析,比如登录页,闪屏页,首页,退出登录等

1. 首先安利一下Getx


一个文件夹就是一个业务模块,独自管理数据,通过依赖注入数据共享,
舒服


image.png


包括 logic 逻辑控制层 state 数据管理层 view 视图组件层 ,当前业务的复用widget写在文件夹下


2. 登录模块


作为app的入口门户,炫酷美观是少不了的,这就需要关注性能优化,而输入的地方,验证的逻辑要有安全设计



  • 首先关于动画性能优化,最关键的一点是精准的更新需要变化的组件,我们可以通过devtool的工具查看更新范围


image.png



  • 其次时安全设计,简单的来看,限制登录次数,禁止简易密码,加密传输,验证token等,进阶版的比如,防止参数注入,过滤敏感字符等

  • 登录之前的账户验证,密码验证,必填项等,然后登录请求,需要加loading,按钮禁用,就不需要防抖了

  • 登录之后保存到本地用户基本信息(可能存在安全问题,暂未深究),然后下次登陆默认检测是否存在基本信息,并验证过期时间,和token,之后隐式登录到首页


3. splash闪屏模块


app登陆首页的准备页面,可以嵌入广告,或者定制软件宣传动画,提示三秒后跳过
如何优雅的加入app闪屏页?
其实就是在main.dart里把初始化页面设置为splash页面,之后通过跳转逻辑
判断去首页还是登录注册页面
比如这里我用了Getx 就简单配置一下


image.png


4. 操作引导模块


第一次使用app,或者重大更新之后往往会有操作引导
我的项目里用到了两种类型的操作引导


成果图
第一种


1.jpg


第二种


image.png
image.png


二者都是基于overlayEntry()Overlay.of(context).insert(overlayEntry)实现的
第二种用了一个包 操作引导 flutter_intro: ^2.2.1,绑定Widget的GlobalKey,来获取Element信息,拿到位置大小,确保框选的位置正确,外层遮罩与第一种一样都是用overlayEntry()创建的


k.png


创建之后,展示出来
Overlay.of(context).insert(your_overlayEntry)
在某个按钮处切换下一个 比如点击我知道了,下一页之类的


     onPressed: () {
// 执行 remove 方法销毁 第一个overlayEntry 实例
overlayEntryTap.remove();
// 第二个
Overlay.of(context).insert(overlayEntryScale);
},

关于第二个实现涉及的flutter_intro包,粘一下我的代码,详细的可以参照pub食用


final intro = Intro(
// 一共有几步,这里就会创建2个GlobalKey,一会用到
stepCount: 2,
// 点击遮罩下一个
maskClosable: true,
// 高亮区域与 widget 的内边距
padding: EdgeInsets.all(0),
// 高亮区域的圆角半径
borderRadius: BorderRadius.all(Radius.circular(4)),
// use defaultTheme
widgetBuilder: StepWidgetBuilder.useDefaultTheme(
texts: ["点击添加收藏", "下拉添加书签"],
buttonTextBuilder: (currPage, totalPage) {
return currPage < totalPage - 1
? '我知道了 ${currPage + 3}/${totalPage + 2}'
: '完成 ${currPage + 3}/${totalPage + 2}';
},
),
);

......
// 这里用到key来绑定任意Widget
Positioned(
key: intro.keys[1],
top: 0,
right: 20,
...
)
......

5. CustomPaint 绘图画板模块


成果图


image.png


当初选择flutter就是因为,有大量的绘制需求,看中了自带skia,绘制效率高且流畅而且具备平台一致性
结果坑也不少


首先来讲一下 猪脚 CustomPaint


顾名思义,这是一个个性化绘制组件,他的工作就是给你创建一个画布,你想怎么画怎么画,我们直接看怎么用
首先格式化写法



  • 首先 需要写在widget 树里吧
    Container( 
    child: CustomPaint(
    painter: myPainter(),
    ),

    看一下参数列表,发现painter 接收一个CustomePainter对象,这里可以注意一下child参数,很奇怪明明绘制界面都放在painter里了,留一个child干啥用??? 其实有大用,这里面放是他的子widget,但是不参与绘制更新的,通俗一点就是我绘制一片流动的云彩,但是有个静止的太阳,云彩的位置是实时repaint的,这时就可以把太阳widget放在child中,优化性能


image.png



  • 接下来我们创建myPainter()


    class myPainter extends CustomPainter { 
@override
void paint(Canvas canvas, Size size) {
// 创建画笔
final Paint paint = Paint();
// 绘制一个圆
canvas.drawCircle(Offset(50, 50), 5, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;



  • 到这里 我们需要实现两个重要的函数(如上代码)


第一个paint()函数,自带了画布对象 canvas,和画布尺寸 size,这样我们就可以使用Canvas的内置绘制函数了!
而绘制函数,都需要接收一个Paint 画笔对象


image.png


这个画笔对象,就是用来设置画笔颜色,粗细,样式,接头样式等等


Paint paint = Paint(); 
//设置画笔
paint ..style = PaintingStyle.stroke
..color = Colors.red
..strokeWidth = 10;


第二个函数shouldRepaint() 顾名思义判断是否需要重绘,如果返回false就是不需要重绘,只执行一次paine(),返回true就是总是重绘,依据实际需求设置
如果需要绘制类似于 根据数值不断变高的柱状图动画


代码如下(搬走就能用哦)


class BarChartPainter extends CustomPainter {
final List datas;
final List datasrc;
final List xAxis;
final double max;
final Animation animation;

BarChartPainter(
{@required this.xAxis,
@required this.datas,
this.max,
this.datasrc,
this.animation})
: super(repaint: animation);

@override
void paint(Canvas canvas, Size size) {
_darwBars(canvas, size);
_drawAxis(canvas, size);
}

@override
bool shouldRepaint(BarChartPainter oldDelegate) => true;

// 绘制坐标轴
void _drawAxis(Canvas canvas, Size size) {
final double sw = size.width;
final double sh = size.height;

// 使用 Paint 定义路径的样式
final Paint paint = Paint()
..color = Colors.grey
..style = PaintingStyle.stroke
..strokeWidth = 1
..strokeCap = StrokeCap.round;

// 使用 Path 定义绘制的路径,从画布的左上角到左下角在到右下角
final Path path = Path()
..moveTo(40, sh)
..lineTo(sw - 20, sh);

// 使用 drawPath 方法绘制路径
canvas.drawPath(path, paint);
}

// 绘制柱形
void _darwBars(Canvas canvas, Size size) {
final sh = size.height;
final paint = Paint()..style = PaintingStyle.fill;
final double _barWidth = size.width / 20;
final double _barGap = size.width / 25 * 2 + 18;
final double textFontSize = 14.0;

for (int i = 0; i < datas.length; i++) {
final double data = datas[i] * ((size.height - 15) / max);
final top = sh - data;
// 矩形的左边缘为当前索引值乘以矩形宽度加上矩形之间的间距
final double left = i * _barWidth + (i * _barGap) + _barGap;
// 使用 Rect.fromLTWH 方法创建要绘制的矩形
final rect = RRect.fromLTRBAndCorners(
left, top, left + _barWidth, top + data,
topLeft: Radius.circular(5), topRight: Radius.circular(3));
// 使用 drawRect 方法绘制矩形

final offset = Offset(
left + _barWidth / 2 - textFontSize / 2 - 8,
top - textFontSize - 5,
);
paint.color = Color(0xFF59C8FD);

//绘制bar
canvas.drawRRect(rect, paint);

// 使用 TextPainter 绘制矩形上放的数值
TextPainter(
text: TextSpan(
text: datas[i] == 0.0 ? '' : datas[i].toStringAsFixed(0) + " %",
style: TextStyle(
fontSize: textFontSize,
color: paint.color,
// color: Colours.gray_33,
),
),
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
)
..layout(
minWidth: 0,
maxWidth: textFontSize * data.toString().length,
)
..paint(canvas, offset);

final xData = xAxis[i];
final xOffset = Offset(left, sh + 6);
// 绘制横轴标识
TextPainter(
textAlign: TextAlign.center,
text: TextSpan(
text: '$xData' != ''
? '$xData'.substring(0, 4) + '-' + '$xData'.substring(4, 6)
: '',
style: TextStyle(
fontSize: 12,
color: Colors.black,
),
),
textDirection: TextDirection.ltr,
)
..layout(
minWidth: 0,
maxWidth: size.width,
)
..paint(canvas, xOffset);
}
}
}

好了,customPainter,大体就这么用,下面回归话题,绘制画板
其实整体任务相当复杂,这里刨析一处,其他的融会贯通


拿最经典的铅笔画图来说
其实单纯的实现铅笔画图,甚至带笔锋,类似于签名,都很简单,网上教程一堆


大体思路就是 加一个GestureDetector ,主要用 onPanUpdate事件实时触发绘制动作,用canvas绘制出来
绘制简单,但是性能优化复杂


这里直接给出我测试的最优解
先把新的坐标点与之前的点连成线,可以一次多连接几个,也就是类似于节流的处理手法,
比如等panUpate触发了五次回调,先都把这五个点连接成线,第六次再统一绘制一条线(要是还有啥好办法,希望不吝赐教!)
详细的以后单独整理出来一个项目


6. websocket 即时通讯模块


成果图


Inked2_LI.jpg
只做了最基本的文字 图片 文件功能


简单把各项功能实现说一下,以后会详细整理,并加入音视频



  • 关于websocket


    首先肯定是连接websocket,用到一个包 web_socket_channel


    然后初始化websocket


    // 初始化websocket
    initWebsocket(){
    Global.channel = IOWebSocketChannel.connect(
    WebsocketUrl, // websocket地址
    //这个参数注意一下, 这里是每隔10000毫秒发送ping,如果间隔10000ms没收到pong,就默认断开连接
    //所以收网速等影响,这个参数如果太小,比如100ms就会,出现过一阵子自己断开连接的问题,参考实际设置
    pingInterval: Duration(milliseconds: 10000),
    );
    // 监听服务端消息
    Global.channel.stream.listen(
    (mes) => onMessage(mes),// 处理消息
    onError: (error) => {onError(error)}, // 连接错误
    onDone: () => {onDone()}, // 断开连接
    cancelOnError: true //设置错误时取消订阅
    );
    }



  • 处理消息


    进入页面加载聊天消息,长列表还是得用ListView.build(),消息多的时候体验好很多


    每次监听到新消息,加入到数组中,并更新视图,这一步不同的状态管理方法不同.


    加入消息这里就有难点了


    首先分四种情况 a. 自己发的并且在ListView底部,b. 自己发的但是不在ListView底部, c. 别人发的消息并且在底部,d. 别人发的不在底部.


    a 和 b,c: 只要是自己发得就滚动到底部,在底部时就滚动的慢点,有种消息上拉的感觉


    // 这里要确保在LIstView中已经加入并渲染完成新消息
    // 我的处理就是加了一个延迟,再滚动
    // 直接滚动到ListView底部
    scrollController.jumpTo(scrollController.position.maxScrollExtent);
    // 滚动到某个确定的元素
    Scrollable.ensureVisible(
    // 给每一条消息对象加GlobalKey,获取到当前上下文
    state.messageList[index].key.currentContext,
    duration: Duration(milliseconds: 100),
    curve: Curves.easeInOut,
    // 控制对齐方式
    alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd);

    d : 这种情况,做了个类似于微信的提示




image.png


但是点击定位到消息有坑了,因为用的listView.build,当你在翻阅上边的消息,下面的消息并没有加载,因此获取不到currentContext,因为元素并没有渲染,也就定位错乱了,目前最理想的解决办法就是,往上翻的时候,之下的记录全部渲染,往下滑时再依次清.




  • 文件和图片


    用到了几个包 file_picker, open_file, path_provider


    file_picker ,用来选择文件和图片,可以配置单选多选,需要在安卓的配置文件里加权限


    open_file , 类似于微信点击文件,先下载,然后调用本地默认程序打开文件


    path_provider,提供系统可用路径,用于创建文件目录


    具体使用如下


     // 访问不到app私有目录 导致我卡了很久...
    // Directory dirloc = await getTemporaryDirectory();
    // 访问外置存储目录
    final dirPath = await getExternalStorageDirectory();
    Directory file = Directory(dirPath.path + "/" + "temFile");
    // 不存在就创建目录
    try {
    bool exists = await file.exists();
    if (!exists) {
    await file.create(); // 创建了temFile 目录 用于缓存文件
    }
    } catch (e) {
    print(e);
    }
    // 下边就很关键了 可能不同的后端数据不同实现
    // 请求存储权限 需要一个包 permission_handler: ^6.1.1
    Permission.storage.request().then((value) async {
    //如果许可
    if (value.isGranted) {
    // 判断文件是否存在 wjmc 就是一个变量存储着文件名
    File _tempFile = File(file.path + '/' + wjmc);
    if (!await _tempFile.exists()) {
    try {
    //1、创建文件地址 带扩展 我用了getx cstate
    // final ChatState cState = Get.find().state;
    // 这是一个通用组件 不管理数据 从chatState里注入
    cState.path = file.path + '/' + wjmc;
    //2、下载文件到本地
    cState.downloading.value = nbbh;
    var response = await dio.get(fileUrl);
    Stream resp = response.data.stream;
    //4. 转为uint8类型
    final Uint8List bytes =
    await consolidateHttpClientResponseBytes(resp);
    //5. 转为List 并写入文件
    final List _filelist = List.from(bytes);
    final filePath = File(cState.path);
    await filePath.writeAsBytes(_filelist,
    mode: FileMode.append, flush: true);
    } catch (e) {
    print(e);
    }
    }
    cState.downloading.value = '';
    // 6.这里可以记录位置,保存path到一个数组里,退出软件之后清除缓存 我没做
    open(cState.path);
    }
    });
    // 读取Stream 文件流 处理为Uint8List
    Future consolidateHttpClientResponseBytes(Stream response) {
    final Completer completer = Completer.sync();
    final List> chunks = >[];
    int contentLength = 0;
    response.listen((chunk) {
    chunks.add(chunk);
    contentLength += chunk.length;
    }, onDone: () {
    final Uint8List bytes = Uint8List(contentLength);
    int offset = 0;
    for (List chunk in chunks) {
    bytes.setRange(offset, offset + chunk.length, chunk);
    offset += chunk.length;
    }
    completer.complete(bytes);
    }, onError: completer.completeError, cancelOnError: true);

    return completer.future;
    }
    void open(path) {
    // 下载完成 准备打开文件
    showCupertinoDialog(
    context: Get.context,// 舒服
    builder: (context) {
    return Material(
    color: Colors.transparent,
    child: CupertinoAlertDialog(
    title: Padding(
    padding: EdgeInsets.only(bottom: 10),
    child: Text("提示"),
    ),
    content: Padding(
    padding: EdgeInsets.only(left: 5),
    child: Text("是否打开文件?"),
    ),
    actions: [
    CupertinoButton(
    child: Text(
    "取消",
    style: TextStyle(color: Colours.gray_88),
    ),
    onPressed: () {
    Get.back();
    },
    ),
    CupertinoButton(
    child: Text("确定"),
    onPressed: () async {
    Get.back();
    // 直接调用就能打开,会通过系统默认程序打开 比如.doc 默认用office等.
    await OpenFile.open(
    cState.path,
    );
    }),
    ]),
    );
    },
    );
    }

    音视频用的小鱼易连,但是木有Flutter SDK ,只能基于安卓的去封装,以后有机会再讲讲.


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

系统模块划分设计的思考

系统模块划分设计的思考前言首先明确一下,这里所说的系统模块划分,是针对client,service,common这样的技术划分,而不是针对具体业务的模块划分。避免由于歧义,造成你的时间浪费。直接原因公司内部某技术团队,在引用我们系统的client包时,启动失败...
继续阅读 »

系统模块划分设计的思考

前言

首先明确一下,这里所说的系统模块划分,是针对client,service,common这样的技术划分,而不是针对具体业务的模块划分。避免由于歧义,造成你的时间浪费。

直接原因

公司内部某技术团队,在引用我们系统的client包时,启动失败。
失败原因是由于client下有一个cache相关的依赖,其注入失败导致的。

然后,就发出了这样一个疑问:我只是希望使用一个hsf接口,为什么还要引入诸如缓存,web处理工具等不相关的东西。

这也就自然地引出了前辈对我的一句教导:对外的client需要尽可能地轻便。

很明显,我们原有的client太重了,包含了对外的RPC接口,相关模型(如xxxDTO),工具包等等。

可能有人就要提出,直接RPC+模型一个包,其它内容一个包不就OK了嘛?

问题真的就这么简单嘛?

根本原因

其实出现上述问题,是因为在系统设计之初,并没有深入思考client包的定位,以及日后可能遇到的情况。

这也就导致了今天这样的局面,所幸目前的外部引用并不多,更多是内部引用。及时调整,推广新的依赖,与相关规范为时不晚。

常见模块拆分

先说说我以前的模块拆分。最早的拆分是每个业务模块主要拆分为:

  • xxx-service:具体业务实现模块。

  • xxx-client:对外提供的RPC接口模块。

  • xxx-common:对外的工具,以及模型。

这种拆分方式,是我早期从一份微服务教程中看到的。优点是简单明了,调用方可选择性地选择需要的模块引入。

至于一些通用的组件,如统一返回格式(如ServerResponse,RtObject),则放在了最早的核心(功能核心,但内容很少)模块上。

后来,认为这样并不合适,不应该将通用组件放在一个业务模块上。所以建立了一个base模块,用来将通用的组件,如工具,统一返回格式等都放入其中。

另外,将每个服务都有的xxx-common模块给取消了。将其中的模型,放入了xxx-client,毕竟是外部调用需要的。将其中的工具,根据需要进行拆分:

  • base:多个服务都会使用的。

  • xxx-service:只有这个服务本身使用

  • xxx-client:有限的服务使用,并且往往是服务提供方和服务调用方都要使用。但是往往这种情况,大多是由于接口设计存在问题导致的。所以多为过渡方案。

上述这个方案,也就是我在负责某物联网项目时采用的最终模块划分方式。

在当时的业务下,该方案的优点是模块清晰,较为简洁,并且尽可能满足了迪米特原则(可以参考《阿里Java开发手册相关实践》)。缺点则是需要一定的技术水平,对组件的功能域认识清晰。并且需要有一定的设计思考与能力(如上述工具拆分的第三点-xxx-client,明白为什么这是设计缺陷导致,并能够解决)。

新的问题

那么,既然上述的方案挺不错的,为什么不复用到现在的项目呢?

因为业务变了,导致应用场景变了。而这也带来了新的问题,新的考虑角度。

原先的物联网业务规模并不大,所以依赖也较为简单,也并不需要进行依赖的封装等,所以针对主要是client的内/外这一维度考虑的。

但是现有的业务场景,由于规模较大,模块依赖层级较多,导致上层模块引入过多的依赖。如有一个缓存模块,依赖tair-starter(一个封装的key-value的存储),然后日志模块依赖该缓存模块(进行性能优化),紧接着日志模块作为一个通用模块,被放入了common模块中。依赖链路如下:

调用方 -> common模块 -> 日志模块 -> 缓存模块 -> tair-starter依赖

但是有的调用方表示,根本就不需要日志模块,却引入了tair-starter这一重依赖(starter作为封装,都比较重),甚至由于tair-starter的内部依赖与自身原有依赖冲突,得去排查依赖,进行exclude。
但是同时,也有的调用方,系统通过rich客户端,达到性能优化等目标。

所以,现有的业务场景除了需要考虑client的内/外这一维度,还需要考虑client的pool/rich这一维度。

可能有的小伙伴,看到这里有点晕乎乎的,这两个维度考量的核心在哪里?

内/外,考虑的是按照内外这条线,尽量将client设计得简洁,避免给调用方引入无用依赖。

而pool/rich,考虑的是性能,用户的使用成本(是否开箱即用)等。

最终解决方案

最终的解决方案是对外提供3+n

  • xxx-client(1个):所有外部系统引用都需要的内容,如统一返回格式等。

  • xxx-yyy-client(n个):对具体业务依赖的引用,进行了二次拆分。如xxx-order-client(这里是用订单提花那你一下,大家理解意思就OK)。

  • xxx-pool-client(1个):系统引用所需要的基本依赖,如Lindorm的依赖等。

  • xxx-rich-client(1个):系统引用所需要的依赖与对应starter,如一些自定义的自动装载starter(不需要用户进行配置)。

这个方案,换个思路,理解也简单。
我们提供相关的能力,具体如何选择,交给调用方决定。

其实,讨论中还提到了BOM方案(通过DependentManagement进行jar包版本管理)。不过分析后,我们认为BOM方案更适合那些依赖集比较稳定的client,如一些中间件。而我们目前的业务系统,还在快速发展,所以并不适用。

总结

简单来说,直接从用户需求考虑(这里的用户就是调用方):

  • 外部依赖:

    • 额外引入的依赖尽可能地少,最好只引入二方依赖(我们提供的jar),不引入第三方依赖。

    • 引入的二方依赖不“夹带”私货(如二方jar引入了一堆大第三方依赖)。

  • 自动配置:

    • 可以傻瓜式使用。如引入对应的starter依赖,就可以自动装配对应默认配置。

    • 也可以自定义配置。用户可以在自定义配置,并不用引入无效的配置(因为starter经常引入不需要的依赖)。

  • 性能:

    • 可以通过starter,提供一定的封装,保证一定的性能(如接口缓存,请求合并等)。

    • 可以自定义实现基础功能。因为有些人并不放心功能封装(虽然只是少数,但是稳定性前辈提出的)。

补充

这里补充一点,我对讨论中一个问题的回答,这里提一下。

有人提到工具类,应该如何划分。因为有的工具类,是不依赖于服务状态的,如CookieUtil进行Cookie处理。有的工具类,是依赖于服务状态的,如RedisUtil包含RedisPool状态,直连Redis,处理Redis请求与响应。

其实这里有两个细节:

  • 工具应该按照上面的方式进行划分为两种。单一模块系统,不依赖服务状态的工具往往置于util包,依赖服务状态的工具往往置于common包中。这里还有一个明显的区分点:前者的方法可以设为static,而后者并不能(因为依赖于new出来的状态)。

  • 依赖于状态的工具类,其实是一种拆分不完全的体现。如RedisUtil,可以拆分为连接状态管理的RedisPool与请求响应处理的RedisUitl。两者的组合方式有很多,取决于使用者的需要,以后有机会写一篇相关的博客。不过,我希望大家记住 面向接口编程的原则。

愿与诸君共进步。

原文链接:https://blog.csdn.net/cureking/article/details/105663951

收起阅读 »

从零到阿里的三年

一、背景三年的时间,可以做些什么呢?又或者说,可以做成什么呢?每个人都有各自的机遇、背负、努力。所以这永远没有一个标准答案,有的只是每个人自己的答案。而我能做的,就说出自己的故事,给大家一份参考。如果可以帮助到大家,那就是极好的了。关键词:真实、履历、思考、效...
继续阅读 »

一、背景

三年的时间,可以做些什么呢?又或者说,可以做成什么呢?
每个人都有各自的机遇、背负、努力。所以这永远没有一个标准答案,有的只是每个人自己的答案。
而我能做的,就说出自己的故事,给大家一份参考。如果可以帮助到大家,那就是极好的了。

关键词:真实、履历、思考、效率、执行、不足

履历

先说一下履历吧。毕竟没有履历,可能被人认为是贩卖焦虑的软文。

  • 18.5~18.12:杭州某在线教育公司

    • 岗位:产品+项目+管理+前端+运营
    • 职责:属于啥都干,哪里需要,填哪里。技术方面只做过H5课件,未涉及后端。
    • 薪资:6k x 12
    • 成长:职场
    • 学习:书、证书考试驱动的学习(培训)、慕课网与网易云课堂的各小课程、各类技术白皮书&实时讯息(区块链项目需要)。
    • 证书:计算机技术与软件专业技术资格考试(即”软考“)-系统架构设计师(高级)
  • 19.2~19.12:杭州某物联网公司

    • 岗位:架构师&技术负责
    • 职责:带领团队,0-1搭建工业物联网。真正后端技术的起点。
    • 薪资:11k x 12
    • 成长:技术
    • 学习:书、证书考试驱动的学习(培训)、慕课网各小课、网易云课堂微专业大课_高级开发工程师
    • 证书:计算机技术与软件专业技术资格考试(即”软考“)-系统分析师(高级)
  • 19.12~20.3:某三方外包公司(参与阿里某核心中间件团队开发)

    • 岗位:高级开发工程师~技术专家(外包公司评级)
    • 职责:参与中间件平台非核心系统开发
    • 薪资:20k x 12
    • 成长:三个月试用期都没有,成长有限。
  • 20.3~21.10:某二方外包公司(参与阿里巴巴-盒马开发)

    • 岗位:高级开发工程师
    • 职责:主要负责盒马门店域数字化作业系统开发(主要是智能决策&终端作业)
    • 薪资:(25+2)k x (13 + 3)
    • 成长:业务、产研认识
    • 学习:书、证书考试驱动的学习(培训)、慕课网各小课、慕课网体系大课_Java架构师-十项全能、网易云课堂微专业大课_大数据工程师、九章算法大课_算法班&系统设计课
    • 证书:PMP
  • 21.12~现在:阿里巴巴集团

    • 岗位:保密
    • 职责:参与核心团队开发
    • 薪资:保密

PS:这里简单介绍一下我经历的所谓大小课、培训。

小结

就职业生涯而言,我的开局并不糟糕,起码也算是本科毕业了。这已经超过了不少人了。
但相对很多大佬而言,真的很一般的开局。既不是985、211,也不是研究生。相对团队里一个个清华研究生、东南大学研究生,那这学历属实有点寒碜了。
至于真材实料方面,大学平均成绩也很一般,偏科严重。
不过,大学生涯还是给我留了点东东的。一方面感兴趣的专业课真的学得好,学院前列。另一方面读的书挺多的,整个大学生涯借了图书馆几百本书吧。不敢说每本书都深入看过,但起码每本书都翻过,不少书还做过笔记啥的。

毕业季的时候,想去阿里(当时的杭州大学生都比较向往)。然而学校不怎么出名,阿里根本就没宣讲,当时迷迷糊糊就错过了。现在想来,当时就算有机会,就我那学历和成绩,也够呛。所以,没有赶上校招的快车道(但塞翁失马焉知非福)。

三年多时间(只算后端技术,还不到三年)的学习后,经过“超乎常人”的选拔(足足九轮面试,还不是因为自身原因加餐。。。放心,后面会有面经滴),加入到了阿里的核心部门。

总的来说,打分7/10吧。就结果而言,虽然略低于预期计划,但结果可以接受。就过程而言,由于缺乏经验&指导,以及自身由于各种原因,浪费了不少时间,导致整个过程的效率没有达到预期。这造成,我现在还得补前面三年欠的沉淀债务。

二、剖析

简单剖析一下这三年,离不开三个东西:驱动、思考、执行。
首先得有上层驱动力,才可以推动整个事物前进。其次,要把握好方向盘,确定正确方向。最后,将方向落地到执行层面
简单来说,念头->方向->执行

1.驱动

从某种意义上来说,我也算是职场经验丰富了。毕业三年多,已经经历了五家公司(算上那个三个月不到的三方外包的话)。
从小公司,到大公司,接触了许多人。他们给我最大的印象区别,就是自我驱动力。这份自我驱动力,我有时称之为野心。自我驱动力不同的人,最终表现出来的差十分之大。
近些年比较火的舒适圈理论、还有公司画饼,其实都与自我驱动力相关。

这时候,我就要举例子了。我曾经有一位同事的技术态度就是:可以用(他)现有技术栈解决的,坚决不学新技术。不可以用现有技术栈解决的,就反馈这个问题无解。
另外,我刚进一家公司的时候,那位老板给我画了个饼:希望公司的小伙伴,以后都可以在杭州买房。。。只能说,真是“好大”一个饼。当时,我就泄了一半的气。

简单来说,做人要有梦想,万一真的实现了呢。
如果真的很佛系,可以去看一下励志演讲,喝喝鸡汤。如果嫌鸡汤不好喝,去B站看看美国那边千万刀的豪宅(国内的房子就算了,那鸡汤就不香了),欧洲那边的广袤牧场。
不要老想着鸡汤有毒。鸡汤有毒也许只是一个扭曲的骗局呢?只能说按照博弈的角度来说,创业有成者一定表示,创业者是困难无比,而且一分靠努力,九分靠天命。看懂,掌声。

有人说,当所有人忘记你,便是你的死亡。我和我朋友开玩笑说,当我彻底不蹦哒了,就是我的第一次死亡。

2.思考

思考的三大输出体现:

  • 对过去的总结(复盘等)。
  • 对现状的辨识(厘清组织结构、人际关系等)。
  • 对未来的规划(新年梦想、执行清单等)。

技术上有这么一个准则:
一个需求下来了,如果某人立马开写,要么他做过一样需求,要么他是个憨批。
无论需求多小、多简单,只要么做过,我们就需要进行分析,再进行设计,最后编码、测试、上线。
从架构来说,编码之前需要分析&设计(概要设计&详细设计)。

**人生没有彩排,但可以提前规划。**每个人对自己的职业都应该有短期、中期、长期的规划。短期规划精准,满足Smart原则,中期规划富有弹性,可按照需要进行有效转变。长期规划,则必须follow自己的长期目标(目标质量很重要)。
短期目标,可以借助清单进行执行。中期目标可以和亲友进行探讨,多倾听不同意见(比如不同行业的)。长期目标,就得问自己了,自我价值实现在哪里。
很多人可能并没有自己为之奋斗终生的自我价值,甚至可以说大部分人进了棺材都没有。那可以选择和我一样,令自己拥有更多的选择权,直到找到自我价值的那天。

思考是一种习惯,当习惯了它,就经常会有文思泉涌的感觉。

3.执行

执行方面,我比较认同PDCA,但不需要过于形式化。对自己而言,更多是形成这样的处理模式。

a.计划

前面有提到,通过在脑海里的推演,找到一个切实可行的高效路线。
事前就多思考,执行就蒙头干。

b.执行

执行没啥说的。照做就多了。
中途坚持不下来了,就喝喝鸡汤,听听激昂的音乐(个人方法),就又有干劲了。
当然,如果真的感觉身体很累,那就去锻炼健身吧。我曾经有过半年多的专业健身,效果还是很不错的。

如果真的有一件事无法执行,或者说需要调整计划,那就问问自己,究竟有没有偷懒的成分,如果没有,那就去改变计划吧。

c.复盘&调整

对完成的事情,进行复盘总结,找出优点与不足。
比如,我每年都会写年度总结。平日里,也经常在白板/平板上,对之前做的事情进行分析。
但不可否认,过去的三年里,相对于大量的输入,复盘还是不足。准确说,其实是知识的内化&沉淀不足。

4.小结

总结一下,就一句话。
驱动决定可能,思考决定上限,执行决定下限。

三、方法

前面提到的方向到执行,跨度过大。比如我想学技术、学XXX技术。具体怎么学呢?
所以,这就涉及到方案层面了,或者说流程方法层面了。
举个不恰当的例子,一位p8表示这个财年要做平台商业化,下面一群p6嗷嗷叫咋做。这就需要几位p7,配合p8拆分目标。最后几位p7有了交互逻辑解耦、领域模型抽象等工作,它们就可以将它们细化成具体系统,交给下面的p6。
(准确来说,中间都是在做拆解,并不是一定只有7才有方案/流程方法的考量,粒度不同而已)

俗语说得好,磨刀不误砍柴工。

1.工具

这里的工具只针对狭义工具。更多是集中于软件。广义的工具,是包涵达成目标的多种手段。后面会有一片文章与之相关。

这个时代,不会还有死读书的吧。
合理利用工具,可以大幅提升学习、工作等效率。就像Mybatis取代JDBC、Spring取代EJB、SpringBoot取代Spring一般。

我这里简单提及一些我使用的工具。如果大家感兴趣,我后续会写一篇详细文档,进行阐述的。
Idea Ultimate、Sublime、MarginNote3、XMind、ProcessOn、GoodNotes5、Notability、印象笔记、印象笔记-剪切插件等app
设备方面,随着今年MacBook max的到手,苹果全家桶齐全了。

这里,我提一下我对工具花费的看法。我这边所有软件都是正版的,设备则是按需购买。这些加起来花费不少。但是效率的提升,真的完全赚回来了。

虽然我有marginNote3看pdf文档,但是很多时候还是喜欢看实体书,所以我买了很多实体书。后来甚至为了能够方便看实体ppt等,我直接买了个支持airprint的打印机。。。不过,确实方便了很多。

按照工具的必要程度、效率提升、价格,再根据腰包进行购买。但,你得先知道有这么个东东。囧

我认为,思考与工具(广义上的工具),就是最大的效率提升工具。后面,我会有一篇专门的文档进行这方面的阐述。毕竟也有多个小伙伴问到我效率方面的问题。诸如你这么多书,看得完嘛?你学那么多课,学得过来嘛?项目比较大时,你怎么同时进行项目管理、方案设计,以及核心开发。简而言之,如何实现有效提升效率。

PS:有关破解的问题,个人认为如果可以,还是付费支持一下的说。大家都经历过从破解到付费的时代。开发最常见的,就是jetbrains产品了。早两年,我也是破解,不过后面买得起了,就买了全家桶支持。

2.流程

如果只是单一的工具,那么整体效率终究有限。尤其单一工具带来的局限性,真的令强迫症发狂。
所以,你需要流程,甚至多个流程组成的生态(比如学习生态)。
就学习而言,我目前有两个纵向闭环流程。一个是依据marginNote3的阅读-学习-复习模式构建,这是marginNote官方流程,大家可以了解一下。另一个是基于印象笔记-剪切插件、印象笔记构建,但是复习效果还存在不足。

后续也有想过利用Notion建立流程,或者利用对开发很友好的语雀建立流程。具体整个流程体系,我还在优化迭代中,欢迎大家给出意见。

PS:其实,之前出于定制化的需求,我都想自己写一个知识库工具了。

3.内化

内化,就是把知识变成自己的。
知识看了、收藏了、下载了,都不一定属于你的。甚至做了笔记,这个知识可能还是不属于你的。所以,我们需要有意识地进行知识的内化。

内化的方式有很多。前面说的看、做笔记都是,只是效率低而已。更高一级,是去做实践。比如手写AQS,再比对比对源码,做做思考笔记,你就可以吊打大部分面试官(实测)。再高一级,就是去教别人。比如写博客、线上/线下教课等。效果还是很不错的(实测)。

我自己,就会做笔记(印象笔记),写博客(博客园、CSDN),技术分享(群组、团队、公司等)

5.小结

总结一句话,方案决定整体效率。
需要时常反思自己的流程&工具构建的方案效率,是否可以再提高。平时多留意一些效率方案的文章等。

四、警醒

1.不足之处

那么回首这三年多,是否有不足呢?那当然是肯定的。
自身的不足,体现在三个方面:收集有余,沉淀不足;时间浪费;缺乏锻炼,身体素质下降

a.收集有余,沉淀不足

最近三年虽说没有成为收集癖,大多数信息也是个人相关的。
但是从课程(大课五个、培训三个、小课几十),到文章、再到各类书籍,信息收集得太多了。好在大课整理进度85+%,部分小课被大课覆盖内容,就只是随便看看了。但是后续内化程度不足,水平也是参差不齐。
最近趁着有时间,推进了进度,后续还需要持续推进内化进度。

b.时间浪费

时常回去啥也不想做,就想发呆&看视频/直播,晃荡一两个小时,才回过神,开始做事情。个人觉得一个很重要的原因是白天注意力比较集中,刚回来的时候心思比较活跃,所以注意力难以集中。再就是整体精力不足。

解决无非开源节流:

  • 开源:增强身体锻炼,提高个人精力上线。
  • 节流:需要合理安排时间&精力,提高时间“质量”(详见精英控系列,后续详述)。

PS:我属于白天一干活,就可以开心坐一天的那种,除非特殊情况,否则就真的感觉像心流那样度过一天。但晚上就真的不怎么想加班,尤其实际没啥事情。

c.健身时间变少导致身体素质下降

虽然整体来看,还不错。但是相较于大学时候的身体素质,那确实有所下降。后面需要安排上的说。

2.客观认识

过去三年多,有很多运气的成分,这不得不承认。
比如被第一家公司老板挖到,是因为我那段时间对区块链技术比较关注,被他发现了。。。
比如被第二家公司老板挖到,是因为我在第一家公司考虑软考-系统架构设计师,所以在我对后端技术只学习了一两个月的情况下,被挖过去当技术负责。。。
比如被第三家公司上司挖到,是因为我在第二家公司时,虽然通过了阿里一面,但被我拒绝了后续流程。然后,我的简历就一直在阿里人才库,并且评价还不错。。。
第四家公司原因同上。。。

再比如我遇到上司与同事,大多都挺不错的,简直是职场最大lucky。

如果刨除这些运气,今天的我又会是什么样呢?

五、总结

如果工作的信念只剩一个,我希望是自我驱动。
如果人生的核心只剩一个,我希望是思考。

至于为什么要做这样的分享呢?大学的时候,我学到一句话,我很有感触。这个社会让你知道的,是它想让你知道的。
博弈使得每一位成功者成功后,都会选择包装自己,使得自己的成功更为顺理成章。而我能做的,只有从一开始就展示自己的一点一滴,没有包装的真实。那么如果最终我可以获得一些成就,说明我的道路是正确可行、可持续的。
过去中国几十年的阶级越迁已经越来越难见,那么我们看到的道路真的还是正确的吗?结果与过程真的相匹配嘛?这里无法如数学那样可以明确推理的答案,所以需要我们这些人去探寻。而我能做的,就是用自己的经历去验证自己的想法。再将这份经历真实地展现出来,供大家参考

欢迎大家就文章中的一些问题,如职场生存、职业规划、效率提升、面试经验等,与我进行交流。

最后,愿与诸君共进步。

这次总结更像是对过去三年多的一种粗粒度总结。不过,后续会有面经、工具、工作经验等方面的详细文章。

原文链接:https://blog.csdn.net/cureking/article/details/122179291

收起阅读 »

请不要“妖魔化”外包岗位!——一文带你正确认识外包

一、背景一转眼,又到了金三银四的跳槽&求职季。在IT行业,跳槽就离不开一个词,那就是外包。可以说,每一位IT人都接触过外包,甚至参与其中。而多数IT职场萌新,都面临着大厂外包,还是小公司的绝望抉择。虽然很多人虽然抵制外包,但他们往往对外包只有比较直观、...
继续阅读 »

一、背景

一转眼,又到了金三银四的跳槽&求职季。
在IT行业,跳槽就离不开一个词,那就是外包。可以说,每一位IT人都接触过外包,甚至参与其中。而多数IT职场萌新,都面临着大厂外包,还是小公司的绝望抉择。虽然很多人虽然抵制外包,但他们往往对外包只有比较直观、碎片的认识。
网上针对IT外包的资料,很少很少,而且大多比较零碎。我恰巧对外包算比较有经验(经历详见我之前的 从零到阿里的三年)。
所以我想谈一谈外包。希望能给需要的小伙伴,一些参考与帮助。

二、分析

1.什么是外包

为了更好地分析,我们需要了解什么是外包。
外包是一种将目标,委托给其他组织的管理模型。
外包有很多种,如项目外包、产品外包、工程外包等等。而我们最为关心的,则是人力资源外包。
这样说比较抽象,我来举个例子。

  • 项目外包:为了完成某个项目,出于进度、成本,甚至是风险转移的考量,将项目拆分一部分(如非核心部分)交给其他个人/组织。比如猪八戒网上的一些项目,就是这样的项目。
  • 产品外包:多数出于成本考量,将部分产品外包给其他个人/组织。比如战神5将部分场景、人物模型外包给外部团队完成。
  • 工程外包:多数出于成本、风险、进度等考量,将工程交给其他组织。比如包工头承诺完成大楼的墙壁粉刷等。
  • 人力外包:多数出于成本(也有是对上层政策的对策)的考量,将员工合同签署到其他人力资源公司等。比如国内IT行业的中软国际,员工与中软国际签合同,但却在阿里、大搜车等公司驻场工作(也很多有与目标公司分开的情况)。

2.二方外包VS三方外包

我们有时候会听到招聘人员说自己是二方外包,或者直接说自己不是外包,只是合同签署公司有所差别,和正式员工没有什么区别。抛开那种没底线的欺骗行为,到底什么是二方外包呢?它与三方外包的差别是什么?

最直接的区别,三方外包的合同都是与独立的第三方人力资源公司签署,二方外包的合同是与目标公司的关联公司(多为控股子公司)签署。
这里直接举个例子吧。我是一个即将成为盒马外包的开发人员。

  • 三方外包:我和一家与盒马不入股的中软国际签署合同。
  • 二方外包:我和一家由盒马控股的上嘉签署合同。

透过现象看本质。这两种合同的签署方式,直接决定了你和目标公司(如盒马)的关系。
盒马无法直接管理三方外包,甚至说两者解除合作关系后,你就无法在盒马工作了,所以盒马对三方外包员工的信任会比较低。体现到实际表现中,就是三方外包员工的权限总是很有限。另外,三方外包员工即使无法在盒马工作,也可以被三方外包公司派遣到支付宝等其他公司。所以,盒马与三方外包公司基本都是把三方外包员工视为商品,人力资源商品。
但是盒马有权管理二方外包,所以在工作上会更加信任二方外包员工。而且这个二方外包只会服务于盒马,所以在盒马会在一定程度上把二方外包员工当自己的正式员工看待。

搞清楚了外包员工与目标公司的关系(合同关系),自然就清楚了同样是外包,为什么二方比三方有着更好的待遇&机会。

3.外包的优点

虽然很多开发人员都抵制外包,但实际情况则是依旧有大量开发,选择加入到外包这个圈子。
这说明,外包一定是有好处的。所以,我简单归类了三点好处。

a.面试门槛

外包的面试门槛,相对大厂要低很多。尤其一些初级岗位,真的是有手就行。
原因很简单,有三点:

  • 对于三方外包公司而言,外包员工都是商品,商品越多,公司越赚钱。所以三方外包公司一定会极力帮助你通过面试,包括但不限于给面试资料、透露考题等。
  • 目标公司的面试官大多也不会太重视。而且面试内容,相较于正式员工,更多集中于实用技能,不会出于潜力考虑,询问诸如项目管理、业务思考深度等问题。
  • 即使一家公司翻车了,三方外包公司还会推荐别的公司。概率学上来说,总会通过的。囧

所以,在穷途末路时可以考虑先去外包混一混,别把自己饿死。

b.薪资水平

可能很多人并不知道目标公司给外包公司的合同价。一般来说,你和三方外包公司谈到的最高价,再提升30%~50%,便是目标公司给外包公司的合同价。之所以这么高,是因为正式员工的福利待遇比外包好太多了,比如十六薪、旅游、商业保险等。另外,目标公司政策上会卡住正式员工HC的。
三方外包员工的薪资上限是由级别确定的。而这个级别是面试过程中,目标公司面试官确定的。
你看懂了嘛?看出来什么了嘛?机智的小伙伴已经看到薪资大幅提升的方法了。
是的。只要你确定了你的级别,那么无论你之前薪资多低,你都可以和三方外包公司要这个级别的最高薪资。
因为对于三方外包公司而言,当你级别确定后,目标公司就会给出一个确定的合同价格,为你付钱给三方外包公司。所以,只要你的要价没超出三方外包公司对这个级别设定的薪资上限,他们就一定会和你签合同。毕竟多一个合同,就多赚一份钱。
而在正式员工中,多数情况下,HRG都会以应聘者上一份工作涨薪30%左右为上限。因为再高的价格就得走审批了,流程会比较麻烦。所以除非你非常优秀,否则薪资上限就在那里摆着。

这里说一下我当初的情况,我当初的薪资就是从11k x 12,直接跳到20k x 12。

其实从从零到阿里的三年 的相关经历就可以看出这三个月不到的三方外包经历,帮我实现了短短三个月从11k x 12,到20k x 12,再到24.5k x 16近三倍多工资的巨大跳跃。

当时入职时的级别是资深开发工程师。但是入职一个月后,又被调整为技术专家。所以,我也不确定20k x 12是针对资深开发工程师级别,还是技术专家级别。另外,外包公司的技术专家,大家看着开心就行了。

所以,外包是可以实现薪资的大幅提升的一种方式。

c.学习机会

很多人知道外包的种种不好,但还是选择去外包,这是为什么呢?因为很多人,包括我在内,都相信外包有接触大佬,接触复杂系统、接触大型项目的学习机会。
有一说一,外包虽然没有招聘人员提的那么好学习机会,但却是有一定的学习机会。你可以接触到大佬的代码、架构图,甚至负责项目。
不过,所谓的学习机会完全取决于目标团队。目标团队给你多少文档权限、给你多少代码权限,以及你与目标团队的协作方式,都极大影响了这个学习机会。不得不说,二方和三方的学习机会相差是非常大的。

说一下我在二方,也就是在盒马的情况。首先,我要感谢我所在的团队,尤其是我的一二级主管对我非常照顾,给了我很多机会。非常感谢。由于团队与二级主管(P8)的开明,作为二方的我几乎享有正式员工的所有权限。只要能开的权限,都对我放开审批通过权限。而由于一级主管(P7)的信任与支持,我甚至拥有超出一般员工的项目机会、业务沟通、管理提升。不过,随着二方员工的权限抵达边界、上升渠道卡死,以及最重要的一二级主管离去,我也在近两年的工作后后选择离开。

所以,外包是可以有学习机会的,但取决于所在的团队

4.外包的缺点

说完优点,接下来说说缺点。
虽然大部分人都抵制外包,但是很多人,尤其是萌新,并不清楚外包的主要缺点。
我这里简单归纳一下。

a.工作碎片化

外包的工作内容,大多十分碎片化,甚至是机械化。
因为如果这个工作内容真的很完整、成块儿,那正式工就做掉了。正式工做掉的理由有两个:

  • 完整工作内容有利于他,去构建业务认知。
  • 完整内容拆分出来外包,需要进行进行大量的沟通与团队协作,不利于整体效率。

那么有没有办法避免碎片化呢?答案是有的。一方面可以表现出自身能力,获取正式团队信任,从而获取更完整工作内容。另一方面,从碎片内容中找到联系,构建自身认知体系,从而让碎片化的内容,不再碎片。这个是职业通用技能,实操是有一定困难的,有机会可以聊聊。

工作内容的碎片化,就带来了两个最直观的后果:

  • 提升困难:工作内容的碎片化,导致自身在技术和业务上难以提升,进而影响晋升、转正等,乃至后续可能的面试。
  • 缺乏重量级面试项目:由于工作内容很零碎,面试时无法进行整合,从而获得一个较为完整&复杂的项目。进而导致无法在面试中很好地表现自身实力,影响后续面试结果。

b.缺乏上升通道

这里辟谣一波,许多外包都说有转正机会。实际情况是几乎等于零。 其中,三方外包更是可以直接和零划等号。
三方外包的转正,往往就是给个内推机会,然后和面试官会熟悉一些。然而这些都没什么价值。内推的机会,简直不要太好找的说。现在的大厂,大部分人才招聘,都是技术部员工直接内推的。至于面试官的熟悉,只能说大多数情况下那只会让你的面试更加困难。
至于二方外包的转正,大多也会对绩效、贡献等诸多方面有要求,还需要一二级主管进行推荐。另外,还需要经历审批,以及转正答辩。

靠谱的晋升通道,可能也只有外包公司自己的晋升通道了。具体,我也不太清楚。毕竟我在两家外包公司都是技术最高级了。不过,外包公司的外包员工技术级别,我见过最高的是技术专家,公司宣称对标阿里p6+。

那么到底有没有外包转正式?答案是有的。我见过盒马测试团队的二方外包晋升正式成功了。然而,人家根本没接受,直接跑到支付宝某部门了。

所以,现实是能从外包晋升正式员工的人,水平早超出那条线了,完全可以走正常社招流程。当时作为外包的我,当然也很关注这方面啦。我当时咨询了很多人,得出的结论是,同一个人,晋升的得到的薪资待遇,会比社招得到的待遇低很多。结论就是,能外包晋升正式工,也别走晋升途径,直接去走社招途径。 当然,如果和这个团队离不开什么,与团队有了较深的羁绊,那就没办法了。

所以,外包终究不是归宿晋升通道窄,且晋升性价比很低。

c.温水煮青蛙

很多人都知道外包工作不是最终归宿,为什么还有那么多人一条路走到黑,最后黯然离开?答案很简单,就是 温水煮青蛙 。
一方面,外包的工作往往两极分化,要么一堆碎片化事情,要么无所事事。这对于有一定能力的小伙伴,摸鱼不要太容易。外包的工作考核很是简单,尤其数量最多的三方外包。另一方面,外包的薪资还是说得过去的。属于那种虽然买房买车会有点吃力,但是日常生活还是可以过得比较潇洒的。总结一下,活少钱多压力小,就像温水一般,将外包员工们慢慢麻醉,最终死去等到公司抛弃了自己,才发现自己已经失去了市场竞争力了。

d.心理压力

多数外包,都会承受着低人一等的压力。
这个压力,往往不是来自周边的小伙伴,而是来自周边的环境,甚至是来自外包员工自己。

压力来源于:

  • 外部:
    • 狗牌:狗牌是心理压力的最大来源。阿里的狗牌是可以很容易区分正式员工和外包员工的,每次进门刷卡,就会感到压力,那种与众不同的压力。我就见过很多外包的朋友,从正式员工那里获得正式狗牌壳,替换自己的外包员工狗牌壳。说实话,我当时也感觉一些压力。但我一直都没有这样做,因为我坚信我并不比那些正式员工差,我只是欠缺一个自己给自己的机会。不过平时开会,我也只是把狗牌放兜里。囧
    • 福利待遇:大厂的福利待遇不错,但其中许多福利待遇是外包享受不到的。比如旅游,三方外包基本无法享受免费跟团旅游的。结果就是大家在谈论旅游,你只能默默敲电脑。还有文化衫,很多时候公司的文化衫,是不会算着外包的文化衫的。看着周边都穿上了统一样式的文化衫,只有你格格不入地穿着自己的衣服,心里压力陡增。
    • 权限:正式员工的权限与外包员工的权限存在很大差异。当主管在群里发了一篇内部技术论坛的帖子,表示需要大家观摩学习。结果你发现自己没有权限访问&申请,心态简直裂开。
  • 自我:人或多或少,都会有自我中心的倾向。比如有一天你平地摔了一跤,你周边的小伙伴一周不到就忘了。结果你为此纠结了好几个月,认为很是丢人。很多敏感的小伙伴,甚至会把一些正常的行为动作,解读出别的含义。比如主管看到你来了,切换了一下屏幕,你就认为主管在向你隐瞒什么。

这种心理压力,虽然有可能使当事人寻求突破。但更多人是被这种压力,压得喘不过来气。
一方面需要团队与主管的关心&照顾(这里再次感谢我的一二级主管),另一方面需要靠自我。靠自我,主要依据自己对现状的清醒认识(自己在外包的目的等,详见下),以及自己的职业规划(明确的职业规划,可以大幅减少自我焦虑)。

5.如何选择外包

虽然外包听着不好听,但是依然有大量的人进入外包,那么该如何选择外包岗位呢?
多数人选择外包,无非三类:

  • 作为临时的工作
  • 作为跳薪的踏板
  • 作为提升的一步

这里就这三类主要目的,谈一下该如何选择外包岗位。

a.临时工作

如果是临时工作,建议直接在三个月内离开
一方面脱离试用期,离开手续会很麻烦。更重要的是心理上也会有感情,存在“要不就这里”的心理。另一方面一旦超过三个月,在大厂背调环节,则会被列入考察。对公司&团队而言,早点离开,总比半年后离开,更容易接受(无论是培养、情感,抑或是损失)。
临时工作,那就选择试用期在三个月及以上。在此基础上,还需要考虑:

  • 试用期薪资高:不仅仅是工资,而且是试用期工资。因为一些外包offer,试用期薪资打八折,而有的外包offer则不然。
  • 试用期个人时间多:这点就比较油滑了。首先临时工作,意味着后面需要跳槽,那么自然需要不少时间去准备面试。其次,有的工作试用期工作量会比较少。最后,有的工作虽然工作量不少,但是缺乏对外包员工的管控,完全可以上班准备面试。囧

b.跳薪踏板

如果是跳薪踏板,建议早日落实,并且选择月薪高的。
绝大多数情况下,外包薪资涨薪高,仅适用于月薪25k以下。因为正常开发外包,是有月薪25k的一条线的。阿里这边几个BU的二方外包,封顶25k月薪,但一般可以13 + 3。所以,如果想拿外包做跳薪踏板,那就要趁早。
之所以选择月薪高的offer,是因为二方外包,以及大厂关注的是月薪,默认给16个月薪资。这时候,你的月薪很高,很容易就在第二次跳槽时直接月薪&年薪月份数双成长。操作得当,年薪可以快速上升数倍。不过薪资高,有时候也是一种负担,有机会会谈谈这个问题。话说后面,要不写一个用来快速涨薪的文章?囧
薪资的话题,比较敏感,我就先说这么多。剩下的,自行领悟哈。只能告诉你们,现在知道&执行这个操作的人,不多。/doge

c.自我提升

如果是自我提升,建议和正式员工在一起办公,并且积极主动
如果只是为了见识见识复杂系统,那完全可以考虑项目外包,不需要考虑人力资源外包。所以所谓的自我提升,更多是为了学习大佬们在面对复杂系统、技术难题、项目管理等问题时的处理手段、方法论、思想等。而当你的工作地点并没有和正式员工在一起,这一切都是泡影。也许可以摸鱼摸得很开心,但是对自我提升,毫无作用。

分开这种事儿并不是我们能决定的。那么有两种选择,一个是转换主次目的,随时准备跳槽。另一个是让主管想办法,把你捞过去。之前我们这边主管为了让同团队的三方外包可以和大团队一起工作,直接每周申请外部访客。最后是打了一个到P10的申请,将这位三方外包员工,留在了园区工作。

在一起工作,只是提供了学习&成长的可能,更多的是需要你积极主动地参与其中。前面说过外包的温水煮青蛙,重要的一点是外包很容易摸鱼,表现出来的,就是不积极主动。很多人成为外包员工后,就想少做一些工作,感觉做多了就亏了。。。其实主动争取工作,一方面有机会获取到更多更有价值的工作内容。另一方面可以展现自身主观能动性,进而让主管给你更多机会。另外,面对外包员工,多数主管并不会太关心其成长。所以这就需要外包员工自己去努力获取更多资源、更多信息、更多成长。其实无论是否为外包员工,都需要积极主动,才可以获得更多成长。只是外包员工更需要这个,因为几乎没有人去推你
具体如何在工作中更好地成长,我后面会写一些相关的文章,就不在这里展开了。

三、总结

这篇文章,先介绍了外包的概念,包括二方外包与三方外包的区别。进而分析了外包工作的优缺点。最后进一步分析外包工作的选择,如何选择,如何面对等。
希望这一篇文章,能够帮助到那些接触到外包工作机会,甚至已经是外包员工的小伙伴。

来源:cnblogs.com/Tiancheng-Duan/p/16002433.html

收起阅读 »

2021年 IT 圈吃瓜指南

来源:InfoQ













来源:InfoQ

再谈如何写好技术文档

— 1 —搞清楚主谓宾— 2 —不滥用代词、过渡词和标点符号— 3 —多用强势动词,少用形容词和副词— 5 —正确使用段落— 6 —适当使用列表和表格—&nbs...
继续阅读 »

参加工作时间久一点的工程师应该有这样一个体会:自己平时代码写得再多再好,可一旦要用文档去描述或者表达某一个事情或者问题时,都感觉非常困难,无从下手,不知道自己该写什么不该写什么;或者费了九牛二虎之力写出来的东西没法满足要求,需要再三去修改调整。这其中的主要原因我归纳有两点:

  1. 思维方式固化。大部分人平时代码写得太多,文字类型的表述又写得太少。而代码和文字明显是两种不同的思维方式,在代码里陷得太深,不容易跳出来;

  2. 本身文字表达能力有限。这个跟写代码一样,有人代码质量高、bug少;有人水平低、bug自然就多。

以上两点其实都可以通过平时多练、多写、多梳理的方式去弥补,比如周期性的博客总结和记录。但是,如果你能刻意系统性地去补充一些关于“技术型写作”的理论知识,一定能够事半功倍。这就像我们刚学编程时,一顿学、一顿模仿,但是总感觉缺了点什么,自己再努力发现深度还是不够。

这时候,我们需要做的是看一本高质量的经典书籍,书籍能帮我们梳理知识点、总结各种碰到过的问题,从理论上解答我们心中各种疑惑,将之前的野路子“正规化”。

下面我根据平时的一些积累,将技术型写作的理论知识归纳成10个要点。

  1. 搞清楚主谓宾

  2. 不滥用代词、过渡词和标点符号

  3. 多用强势动词,少用形容词和副词

  4. 正确使用术语

  5. 正确使用段落

  6. 适当使用列表和表格

  7. 一图胜千言

  8. 统一样式和风格

  9. 把握好整体文档结构

  10. 明确文档的目标群体


 
1 
搞清楚主谓宾


文档主要由段落组成,段落由句子组成,而大部分句子又由“主谓宾”组成(可能有些场合省略了,但是读者可以通过上下文轻松get到)。主谓宾是主干骨架,其他内容可以看作是句子的修饰,主干骨架是决定句子是否垮掉的主要原因。

现在很多人可能已经忘记了句子的基本构成,毕竟以汉语为母语的人,大概率是不太会关心这些“细节”,就像说英语的国家可能不太关心am、is、are一样,你说哪个人家都理解。

但是,文档中的一句话读起来是否别扭,大多数时候是由句子构成决定的。在不考虑文档上下文的情况下,如果一个句子能包含正确的主语、谓语和宾语(可选),那么它读起来至少是很顺口的。下面举一个明显搞不清主谓宾的例子:

传统图像处理算法,通过计算烟火颜色特征,极易受烟火周围环境相近颜色干扰而造成误检。

尽管你能读懂作者想要表达的意思,但是这句话读起来还是太别扭。“传统图像处理算法”应该算是主语,后面的“通过……”这句不完整,“极易受……干扰”这句还可以,“……造成误检”算是谓语宾语,但是这里用错了动词,为什么是“算法造成误检”,难道不是“周围环境相近颜色干扰造成误检”吗?

这句话的主干内容是:算法极易受……影响而……。正确的表述应该类似下面这样:

因为传统图像处理算法通过计算烟火颜色特征去识别烟火,所以它极易受烟火周围环境相近颜色干扰而出现误检。

我们用过渡词(因为……所以……)将原来的句子拆成了前后两个部分,前面部分的主语是“传统图像处理算法”,谓宾是“识别烟火”;后半部分的主语是“它”,谓宾是“出现误检”。经过调整后,前后两个部分的主语是同一个:传统图像处理算法。下面再直观看一下修改之后的句子主干骨架:

<因为><传统图像处理算法>通过计算烟火颜色特征去<识别烟火>, <所以><它>极易受烟火周围环境相近颜色干扰而<出现误检>。

如果你觉得用“因为……所以……”不太好,那么可以再换一种表述:

传统图像处理算法通过计算烟火颜色特征去识别烟火,烟火周围环境相近颜色的干扰极易造成误检。

第一句还是跟之前一样,主语是“传统图像处理算法”,第二句主语变成了“干扰”,谓宾是“造成误检”。下面我们直观地看一下修改之后的句子主干骨架:

<传统图像处理算法>通过计算烟火颜色特征去<识别烟火>, 烟火周围环境相近颜色的<干扰>极易<造成误检>。

最后再举一个错误的例子:

由于误报率与漏报率很高,因此不管是否有真实事件发生都会去留意,也会有规定的日程定点巡查视频任务。

上面这个句子的作者完全没搞懂谁是主语,谁是谓语。感兴趣的童鞋可以试着修改一下,改成你认为正确的表述。


 
2 
不滥用代词、过渡词和标点符号


不滥用代词和过渡词

中文文档中的代词主要有:你、我、他、她、它、其、前者、后者、这样、那样、如此等等,过渡词主要有:因为/所以、不但/而且、首先/然后等等。下面这张表格列举了一些常见的代词和过渡词及其常用场合:

序号
类型
名称
常用场合举例
1
代词

C语言中引入了“指针”的概念,作用是为了能够提升内存访问速度。
2代词
后者
C语言发明于1970年代,C++语言发明于1980年代,后者主要引入了面向对象思想。
3代词

指针能够提升程序访问内存的速度,但特点仍存在一些缺陷。
4代词

C语言的一大特性是指针,这就像C++语言和的面向对象思想一样。
5
过渡词
因为/所以
因为神经网络可以自动提取数据特征,所以基于神经网络的深度学习技术中不再有传统意义上的“特征工程”这一概念。
6
过渡词
首先/然后
首先我们要保证有足够多的训练数据,然后我们再选择一个适合该问题的神经网络模型。

表2-1 代词和过渡词举例

代词和过渡词就像标点符号一样,容易被滥用。代词滥用主要体现在作者在使用它们的时候并没有搞清楚它们代表的究竟是谁,是前一句的主语、还是前一句的宾语或者干脆是前一整句话?

过渡词滥用主要体现在作者在使用它们的时候并没有搞清楚前后两句话的逻辑关系,是递进还是转折或者是因果?(过渡词滥用频率要低很多,毕竟搞清楚前后句子逻辑的难度要小)接下来举几个滥用代词和过渡词的例子:

C++语言发明于1980年代,它支持“指针”和“面向对象(Object-Oriented)”两个特性,其价值在计算机编程语言历史上数一数二。

上面这个句子中出现了两个代词“它”和“其”,抛开句子内容本身对错不论,第二个代词指向的对象其实并不明确,“其”指的是“指针”、“面向对象”还是“C++语言”?或者是指“C++语言同时支持……两个特性”这个陈述?像这种有歧义的场合,我们应该少用代词,尽量用具体的主语去代替:

C++语言发明于1980年代,它支持“指针”和“面向对象(Object-Oriented)”两个特性,C++的价值在计算机编程语言历史上数一数二。

如果你一定要用代词,那么调整一下可能更好:

C++语言发明于1980年代,它同时支持“指针”和“面向对象(Object-Oriented)”两个特性,这个价值在计算机编程语言历史上数一数二。

再读一读,你是不是没有感觉到歧义了?我们在“支持”前面增加了一个“同时”,然后将代词换成了“这个”,现在这个代词指的是“C++语言同时支持...两个特性”这个陈述,修改后整个句子的意思更明确。

我们再来看另外一个滥用代词的例子:

该模块主要负责对视频进行解码,输出单张YUV格式的图片,并对输出的图片进行压缩和裁剪,前者基于Resize方法来完成,后者基于Crop()方法完成。

对于大部分人来讲,上面这段没什么问题。代词“前者”指的是压缩、“后者”指的是裁剪,原因很简单,因为单词Resize对应的是压缩、单词Crop对应的是裁剪。

但是这段话如果拿给没有任何知识背景的人去读(大概率可能是找不到这种人),恐怕会存在歧义,主要原因是代词前面提到了很多东西,“前者”和“后者”指向不明确,到底是指“解码”、“输出单张图片”还是后面的“压缩”和“裁剪”?下面这样调整后,整段话的意思更加明确:

该模块主要负责对视频进行解码,输出单张YUV格式的图片,并对输出的图片进行压缩和裁剪,压缩基于Resize方法来完成,裁剪基于Crop()方法完成。

我们去掉了代词,直接用具体的主语来代替,句子意思非常明确。如果你一定要使用代词,那么也可以这样调整:

该模块主要负责对视频进行解码,输出单张YUV格式的图片。同时,它还对输出的图片进行压缩和裁剪,前者基于Resize()方法完成,后者基于Crop()方法完成。

上面这段话还是使用了代词“前者”/“后者”,但是我们修改了标点符号,并且增加了一个过渡词“同时……”,这样做的目的是让读者知道虽然整段话说的是同一个东西,但是前后的句子已经分开了,为我们后面使用代词做好准备。

好的,现在我们来总结一下在技术型文档编写过程中使用代词时的一些有价值经验:

  1. 代词可以指它前面出现过的名词、短语甚至整个句子,但是一定是前面出现过的;

  2. 代词的位置和它要指向的目标最好不要隔得太远,1~3句话之内,超过就不要用了;

  3. 代词的作用是减少小范围内某些词汇或句子重复出现的频率,要用到恰到好处;

  4. 代词前面出现的混淆目标如果太多,一定要重新调整句子,确保代词指向无歧义。

不滥用标点符号

接下来我们再看另一个,标点符号的滥用要普遍很多,其主要原因是:标点符号的使用并没有非常明确的对错之分。至少对大部分人而言,使用句号还是逗号其实并没有什么严格的评判标准,只要不出现“一逗到底”的极端情况,其余大概率都OK。下面这张表格是我根据以往经验,总结出来的应用于技术型写作时中文标点符号使用规则:

序号
符号
写法
使用场合
1逗号

前后两句话关联性比较大,阅读时停顿时间短。
2句号

前后两句话关联性比较小,阅读时停顿时间稍长。
3
分号

前后两句话地位相对平等,句子的内容和格式基本保持一致。比如列表中,如果每项是一个句子或者短语,那么第1至第N-1项结尾使用分号,第N项结尾使用句号。
4
冒号

技术型文档中,冒号一般用在需要引入重要内容的场合。比如当你需要插入一张表格或者一张图片时,需要提前做一个提醒(下表列举了常见的代词和过渡词:),提醒结束时补充一个冒号。
5
括号
()、【】
()一般用于解释性的场合,负责对名词或者句子的补充解释。【】用得比较少,我一般用于需要增加醒目标记的名词或短语中。
6
顿号

一般可以用在枚举名词或者短语的场合。
7
问号
不用多解释。
8
引号
“”、‘’
一般用于标记特殊名词、专用名词、短语,或需要重点突出的名词或短语。
9
分隔号
/
一般用于成对出现的名词(举例:因为/所以、首先/然后等等都是过渡词),或者根据文档上下文来判断地位差不多的相近词(举例:算法的好坏直接影响最终报表中误报/误报率那一栏)。
10
破折号
——
用得不多。
11
省略号
……
不用多解释。
12
感叹号

技术型文档不是写小说,用得不多。
13
书名号
《》、<>
不用多解释。

表2-2 常用标点符号

上面这张表格基本涵盖了常用的中文标点符号,其中有一小部分在技术型文档中不太常见,比如感叹号、破折号,这些符号多多少少带有某种感情色彩,不太适合用于技术型文档编写。前面已经简单概括了一下各个符号的使用场合,下面挑几个容易出错的再一一详细说明:

C++语言发明于1980年代,它衍生自C语言,主要引入了“面向对象(Object-Oriented)”思想,面向对象思想强调对数据的封装和对功能的复用,此特性有利于开发者对代码的维护和扩展,目前,大部分计算机编程语言已经支持了面向对象特性。

上面这段话属于典型的“一逗到底”的例子。作者从C++语言说到了面向对象思想,最后总结大部分计算机编程语言都支持面向对象。我们如果将整段话拆开来看,其实它想表述的是3个内容,每个内容之间最好使用句号,停顿时间稍长一些。我们调整之后的效果是:

C++语言发明于1980年代,它衍生自C语言,主要引入了“面向对象(Object-Oriented)”思想。面向对象思想强调对数据的封装和对功能的复用,此特性有利于开发者对代码的维护和扩展。目前,大部分计算机编程语言已经支持了面向对象特性。

接下来我们再看看分号的使用。根据我个人经验,分号常用在列表场合,下面举一个例子说明:

下面是“将大象装入冰箱”的具体步骤:

  1. 打开冰箱门;

  2. 将大象装进冰箱;

  3. 关上冰箱门。

上面是一个有序列表,列表中的各项内容是一个短语。当列表中各项内容是短语或者句子的时候,除最后一项之外其余项目结尾一般都使用分号(注意,同一个列表中各项的格式最好都保持一致,要么都是短语,要么都是单个的名词,这个后面专门讲列表的时候会提到)。如果列表中各项内容只是一个名词时,那么结尾就可以不用标点符号:

下面是“可以被装进冰箱”的动物:

  • 狗子

  • 大象

  • 猴子

  • 鹦鹉

上面是一个无序列表,列表中的各项内容是一个名词,这时候名词结尾处不需要添加任何标点符号。

我们最后再来看一下小括号的使用场合。在技术型文档中,小括号主要用于对前面的名词、短语或者句子进行补充说明,比如当文档中出现缩写词汇时,我们会在它的后面增加一个小括号,在括号里面注明该缩写词汇的全称。下面举一个使用小括号对缩写词汇解释说明的例子:

API(Application Program Interface)是系统对外提供的访问接口,使用者可以按照API文档中的接口定义去访问系统中的数据,并与它做一些交互。

上面这段话主要讲API是什么、可以干什么。它是Application Program Interface三个单词的简称,为了让读者更清楚该术语的定义,作者可以选择在第一个“API”出现的位置增加一个小括号,并将术语全称补充进来,之后的整个文档无需再重复该操作(后面会单独提到术语全称和简称的运用规则)。

除了能对缩写词汇进行解释说明之外,小括号还可以用于对前面整个句子进行补充说明,再看下面这个例子:

它是Application Program Interface三个单词的简称,为了让读者更清楚该术语的定义,作者可以选择在第一个“API”出现的位置增加一个小括号,并将术语全称补充进来,之后的整个文档无需再重复该操作(后面会单独提到术语全称和简称的运用规则)。

上面这段话其实前面已经出现过,最后小括号里面的内容主要是为了对它前面一句话进行补充。如果补充性说明内容太长,比如要好几句话才能起到补充的作用,那么这个时候我们就不应该再使用小括号了,可以考虑调整句子结构,然后将补充性的内容当作段落主体的一部分。

关于代词、过渡词以及标点符号滥用的内容就讲到这里,其中有一些内容是我个人的写作喜好,其实并没有非常明确的对错之分,比如前面讲到列表中分号的使用,很多人这时候可能选择使用句号。

大家可以根据自己的判断去处理这种模棱两可的场景,当然一些比较确定的规则,比如当列表项只有名词的时候,列表项结尾不要使用任何标点符号,这一点还是比较确定的。


 
3 
多用强势动词,少用形容词和副词


强势动词和主动语句

很多人可能第一次听到“强势动词”这个说法,陌生还难以理解。如果将它翻译成英文,对应的单词应该是“Strong Verbs”,意思是强有力的动词,你可以理解为:听起来动作幅度大、冲击力强的那一类动词。打个比方,假如“走”是弱势动词,那么“跳”就是强势动词;假如拿刀“切”是弱势动词,那么拿刀“砍”就是强势动词。下面这张表格列举了一些强势/弱势动词的例子:

序号
弱势动词
(可考虑)强势动词
1
走过去
跳过去
2
切肉
砍肉
3
出现异常
抛出异常
4
程序退出
程序崩溃
5内存增长
内存泄漏
6找不到日志文件
日志文件丢失
7客户提出质疑
客户投诉
8
任务未完成
任务延期
9角色权限是由管理员设置的
管理员控制角色权限
10
系统无法正常使用API返回的结果
系统无法正常解析API返回的结果

表3-1 强势/弱势动词对比

上面列出了10对强势/弱势动词,我们观察可以发现:弱势动词一般无法正确表达问题/事情的真实情况。在技术型文档编写过程中,虽然我们不能借助词汇使用、句子构成以及标点符号等手段去传递感情倾向,但是也不能掩盖真实准确的内容表达。

在提到强势动词时,我们还要注意“主动语句”和“被动语句”的区别。在技术型文档编写过程中,应该尽量少使用被动语句。下面这张表格列举了一些主动/被动语句的例子:

序号
被动语句
(可考虑)主动语句
1
角色权限是由管理员控制的
管理员控制角色权限
2
API结果无法被系统正常解析
系统无法正常解析API结果
3
图像特征是通过CNN逐步降维的方式提取的
CNN通过逐步降维的方式提取图像特征
4
这种检测效果无法被客户接受
客户无法接受这种检测效果
5
经过研发排查发现,这个现象是正常的(*)
经过研发排查发现,这个属于正常现象

表3-2 主动/被动语句对比

上面表中第5项(带*号)严格来讲不算被动语句,但是在技术型写作过程中,我们应该避免使用“……是……的。”这种句式,该句式太过口语化。尽量少用被动语句的原因有以下三个:

  1. 读起来麻烦。读者读到被动语句时,需要先在脑子里将其转换一下再去理解;

  2. 难以理解。读者有时候很难分清被动语句中的真实主语(甚至可能省略了主语);

  3. 字数多。被动语句一般更长、字数更多。

那么被动语句是不是完全不让用了呢?当然不是。仔细的读者可能已经观察到了前面在举例的时候我们有这样一段话:

C++语言<发明于>1980年代,它支持“指针”和“面向对象(Object-Oriented)”两个特性,C++的价值在计算机编程语言历史上数一数二。

上面第一句中的“……于”其实就是被动语句,像“C++语言发明于……”、“该文档编辑于……”这些都算被动语句,由于宾语(这里是C++语言)更重要,所以默认省略了真实主语(某某发明C++语言,可是某某在这里不太重要)。这类句子结构有一个特点就是:宾语比真实主语重要,所以放到句子的开头位置。

少用形容词和副词

技术型文档讲究的是一个“准”字,它不像小说、散文之类的文学作品带有很强的感情色彩,也不同于网络博客可以掺杂一些非正式词汇,更不能跟Marketing Speech(营销话语)一样常常夸大其词。为了做好前面说的“准”,技术型文档应该尽量少用形容词和副词,因为这些词语大部分都属于“主观”表达。下面举几个使用形容词和副词的例子:

为了保证系统运行更高效,他们尝试尽可能压缩图片尺寸,事实证明这个尝试非常成功。这样的工作看似简单,却蕴含着高技术含量。

上面这段话使用了好几个副词和形容词,比如“尽可能”、“非常”、“高”。如果是技术型文档,这段话建议调整为:

为了提高系统运行效率,他们将图片尺寸压缩到原来的1/3,系统响应速度提升2倍。

我们用具体的数值替换了原来的形容词和副词,并且直接删掉了最后一句话,最后一句话在技术型文档中起不到任何作用。下面这张表格列举了部分形容词和副词使用不恰当的场合:

序号
形容词/副词
(可考虑)调整为
1
经过优化,接口响应速度提升明显
经过优化,接口响应速度提升2倍
2
很多人反应现场误报很多
数据统计发现,现场误报率为11%
3
大部分客户投诉说系统很不好用
最近一个月有超过50个客户投诉说系统不好用
4
升级依赖库后,该函数运行很快
将依赖库升级到2.3.1版本后,该函数执行时间缩短到100ms以内
5
研发同事很辛苦,每天加班很晚
研发同事很辛苦,每天23:00之后才下班

表3-3 形容词/副词使用不恰当举例

最后,我们来总结一下:

  1. 优先使用方便读者阅读理解的动词和句式(强势动词和主动语句);

  2. 尽量少用形容词和副词,用具体数值代替、或者调整句子表述。


 
4 
正确使用术语


这里提到的术语分两种:一种是计算机领域通用的专业术语,像SDK、面向对象、TCP/IP、微服务等等这些名词,它们基本已经被大众接受和理解,我们在编写文档的时候不能随意再重新去命名、调整或者改变拼写(将“TCP/IP”写成“Tcp/ip”)。

另外一种是当前文档需要定义的术语,这种术语只有在当前文档上下文中才有效。我们在编写技术型文档时,通过自己的判断,如果认为文档读者缺乏对相关术语(不管是前面哪一种)的理解,我们都应该在文档靠前位置给出对术语的解释说明,也就是我们平时文档中常见的“名词解释”。

序号名词说明
1SDKSoftware Development Kit,软件开发包,开发者基于该工具包开发更丰富的高层应用。
2内存泄漏通过new/malloc等方法申请的内存在使用完后未被及时释放,程序运行内存占用越来越高。
3面向对象强调对数据和功能的封装,提升代码的可复用性、可扩展性以及灵活性。
4FVM(*)Front Video Manager,前端视频管理服务,负责视频接入、分发等业务。
5视频大数据标签服务(*)对视频进行结构化处理,生成结构化标签,并对外提供标签检索等功能。

表4-1 名词解释举例(*为自定义术语)

有些文档可能篇幅比较短,并不是传统意义上的需求设计类文档,比如对某个线上问题分析的结果汇报、对某个模型检测效果的验证报告、或者研发阶段性的工作总结。这些文档由于本身内容就不多,大部分可能直接进入主题,这时候如果还要在文档中专门增加一块名词解释的版块(并且总共也就一两个术语),就显得比较突兀。

另外一种对术语进行解释说明的方式是用我们前面提到的小括号,我们可以在术语后面增加一个小括号,然后在括号里添加补充说明。这种方式很便捷,但是只适合简单的场景,比如在小括号里面补充术语的全称或者简称,或者只做简单的解释说明。如果对一个术语的解释内容很长,就不太适合用这个方法,下面举一个错误的例子:

当视频离线时,FVM(Front Video Manager,前端视频管理服务,负责视频接入、分发等业务。)会产生一条告警记录,并存入节点数据库。

上面这个术语解释内容太长,不太适合使用小括号的方式,这种情况要么在文档正文中专门对FVM进行解释,要么在小括号中只给出FVM的英文全称即可:

当视频离线时,FVM(Front Video Manager)会产生一条告警记录,并存入节点数据库。

使用小括号去做术语解释还需要注意一点的是:只需要在术语第一次出现的时候做一次解释即可,不需要重复多次。下面举一个重复的错误例子:

当视频离线时,FVM(Front Video Manager)会产生一条告警记录,并存入节点数据库。之后节点数据库会将该条告警记录同步到平台数据库,平台FVM(Front Video Manager)检测到有新的告警记录时,会通过消息中间件通知业务系统,业务系统随后将告警信息以短信(或钉钉)的方式通知到用户。

上面对术语FVM的解释重复了两次,这种做法是错误的,第二次我们可以直接去掉。

有些术语存在全称和简称,我们熟悉的SDK全称是“Software Development Kit”,但是现在基本没有人再去使用它的全称。像这种简称已经被大众熟知的术语,我们就不能再标新立异的去用它的全称。

另外一些在文档中自定义的术语,文档作者为了便于阅读可能也会提供一个简写的版本,在这种情况下,文档前后应该保持一致,即:要么整篇文档都用全称,要么都用简称,尽量做到一致。下面举一个全称简称使用不一致的例子:

IVA(Intelligent Video Analytics,智能视频分析)服务主要负责视频解码、模型推理、目标跟踪以及目标行为分析,该服务是整个系统中最复杂的一个模块。智能视频分析服务由张三团队开发完成,一共耗时6个月,人力成本开销120万。

上面这段话中,前半部分作者使用“IVA”简称(小括号中做了全称说明),但是在后面一句话中作者又使用了全称“智能视频分析”,这种做法没有遵循统一原则。不仅同一段落应该保持统一,整篇文档也应该做到统一,术语在文档中第一次出现时是简称,那么整篇文档都应该用简称,反之亦然。

最后我们来总结一下,在技术型文档中使用术语时需要注意的一些事项:

  1. 文档读者不熟悉的术语(包括通用术语和文档自定义术语)都应该有解释说明;

  2. 小括号只适合简短的术语解释场合,括号里的内容不能太长(一两句短语之内);

  3. 任何方式的术语解释只需要有一次即可(术语第一次出现时),不要解释多次;

  4. 术语的全称和简称要保持使用一致,要么整篇文档都用全称、要么都用简称;

  5. 对于计算机领域的通用专业术语,需要沿用主流用法,不要随意再去调整。


 
5 
正确使用段落


单一职责

与面向对象编程中“类的单一职责原则”一样,文档中的句子(特指以句号结尾的一句话)、段落也应该遵循“单一职责原则”。前面讲标点符号的时候已经提到过,同一段话中前后关联性不大的两句话之间用句号,这样可以保证每句话想要表达的是相对独立的内容。

段落也一样,一个段落只陈述一个主题,可以保证段落的句子不会太多、内容不会太长,便于读者阅读和理解。下面举一个段落使用错误的例子:

Excel提供一个组织数据的高效方法。我们可以将Excel想象成一个有行和列的二维表格,每一行代表一个独立的实体,每一列代表该实体的不同属性。Excel还具备数学功能,比如计算平均值和方差等数学操作。如果你想使用Excel来记录图书信息,那么每一行代表不同的书本,每一列代表书本的属性,比如书的名称、价格以及出版社等等信息。

上面这段话的第一句已经明确了段落主题:Excel能高效地组织数据。可是,这段话中间却穿插了一个不相干的句子,说Excel具备数学功能,能够做一些数学操作,这句话显然跟本段主题不一致,我们需要将其去掉:

Excel提供一个组织数据的高效方法。我们可以将Excel想象成一个有行和列的二维表格,每一行代表一个独立的实体,每一列代表该实体的不同属性。如果你想使用Excel来记录图书信息,那么每一行代表不同的书本,每一列代表书本的属性,比如书的名称、价格以及出版社等等信息。

好的开头语

除了要保证段落的“单一职责”之外,我们还需要给每个段落一句“好的”开头语。那么什么是好的开头语呢?好的开头语要能让读者读完之后就能猜到文档作者在本段中想要陈述的主题,其实就是概括性的句子。

还是以上面那段话为例子,它的第一句话“Excel提供一个组织数据的高效方法”其实就是很好的开头语,它提示本段内容主要讲Excel如何高效地组织数据。如果我们将上面那段话的开头调整一下,那么效果明显就差了很多:

Excel由许许多多的单元格组成,每个单元格可以包含不同的内容。我们可以将Excel想象成一个有行和列的二维表格,每一行代表一个独立的实体,每一列代表该实体的不同属性。如果你想使用Excel来记录图书信息,那么每一行代表不同的书本,每一列代表书本的属性,比如书的名称、价格以及出版社等等信息。

读者读完上面第一句话后,可能还是很懵,需要读完整段话才能明白文档作者在本段中想要表达的意思。段落的开头语可以通过提炼段落内容得到,我们可以在段落写完之后回过头提炼一句话作为本段的开头语,下面这段话描述代码中循环语句的作用:

目前几乎所有的计算机编程语言都支持循环语句,例如,我们可以编写代码来判断一个用户命令行输入是否等于“quit”(退出命令),如果需要判断100万次,那就创建一个循环,让判断逻辑代码运行100万次。

上面的这段话本身没什么问题,主要介绍循环语句的功能和应用场合。但是如果我们提炼一下,在段落开头增加一个更好的开头语,效果可能会提升很多:

循环语句会多次运行同一个代码块,直到不再满足循环条件为止。目前几乎所有的计算机编程语言都支持循环语句,例如,我们可以编写代码来判断一个用户命令行输入是否等于“quit”(退出命令),如果需要判断100万次,那就创建一个循环,让判断逻辑代码运行100万次。

上面开头第一句话就说清楚了循环结构的特点,读者读完第一句话基本就知道整段内容要讲什么。一个好的开头语能够节省读者的时间,因为并不是每个读者都有兴趣去阅读整段的内容,开头语可以给读者“是否继续读下去”一个参考。

控制段落长度

控制段落长度并没有一个明确的标准,它只是一个非常主观的说法。如果文档中某个段落内容太长(比如那种一段话就占半页Word),作者自己应该反复阅读几次再对段落做一些精简,这样既可以节省读者的时间,大概率也能提升意思表达的准确性。

同样,也不太建议文档频繁出现小段落,比如整段内容只有一两句话那种,这个时候可以考虑段落合并或者稍微扩充一下内容。

最后我们来总结一下,在技术型文档中如何正确使用段落:

  1. 一个段落只负责讲一个内容,两个不同的主题应该拆分成两个段落去陈述;

  2. 尽量为每个段落增加一个“好的”开头语,能够清晰表达(或暗示)本段的主题;

  3. 要控制好段落内容长短,“不长不短”根据自己经验(比如不超5~7个句子)。


 
6 
适当使用列表和表格


文字相对来讲其实是一种效率比较低的表达方式。如果你想让人快速地去理解你要表达的意思,图片应该是最好的一种方式,但是图片有一个缺点就是:有时候它只能从宏观上去表达,无法体现其中细节。

当我们想要尽可能直观地去陈述内容,又想尽可能多的包含细节时,我们可以考虑使用列表或者表格。有些读者非常抵触大段大段的文字(尤其在技术型文档中),一种改进方法是前面提到的“控制段落长度”,尽量让段落内容精简、单一;再一个就是看看段落内容是否能以列表或者表格的方式去呈现,这种方式可以给人“严谨、清晰”的感觉。

使用列表

列表简单来讲就是将你原来用段落方式呈现的内容改用项目(Item)的方式去呈现,一般它主要用于枚举、过程描述或者要点归纳等场合。列表中的各项可以是名词、短语,甚至是句子,各项目之间有严格顺序要求的列表叫“有序列表”,相反并没有严格顺序要求的列表叫“无序列表”。下面是以段落的方式陈述小张今天所做的事情:

白天在公司上班期间,小张一共修复了7个bug,做了3个代码合并(评审),并和项目经理讨论了前天提的新需求。晚上回到家后,小张先做饭,然后给儿子洗澡,23:30上床睡觉。

上面这段话本身没什么问题,用了合理的标点符号和过渡词,读起来清晰明了。但是,如果在技术型文档编写中,能将这段话改用列表的方式呈现,起到的效果会更好:

张白天在公司:

  • 修复了7个bug;

  • 做了3个代码合并(评审);

  • 和项目经理讨论前天提的新需求。

晚上回到家后:

  1. 做晚饭;

  2. 给儿子洗澡;

  3. 23:30上床睡觉。

我们将原来的一段话拆成了两个列表,并在每个列表前面做了一个“引入说明”(以冒号结束),介绍了接下来列表的背景上下文。第一个列表是无序列表,因为原文并没有突出强调小张白天在公司每项工作之间的前后关系(无顺序要求),只是一个归纳统计;第二个列表是一个有序列表,原文很明显强调了小张晚上回家之后做事的先后顺序(最后一项还给出了具体时间)。

在技术型文档中,合理地运用列表这种方式去呈现内容可以给人一种“逻辑严谨、思路清晰”的感觉,让读者更相信你讲的内容。

在使用列表时,我们应该确保列表中各项内容结构一致,即:要么都是名词,要么都是短语,要么都是句子。这个原则既能保证你使用列表的初衷(逻辑严谨、思路清晰),也能让读者读起来更舒服。下面是一个错误使用列表的示范:

影响系统检测准确性的因素有:

  • 模型;

  • 产品开通过程中,工程师对算法参数校准程度;

  • 应用现场是否有灯光照明。

上面列表一共包含3项,每项的内容结构各不相同,第一项是一个名词,第二项是一个句子,第三项是一个短语。我们将结构统一后,可以调整为下面这样:

影响系统检测准确性的因素有:

  • 模型的复杂性;

  • 部署时对算法参数校准的程度;

  • 应用现场是否有灯光照明。

上面是将列表中各项内容修改为短语,我们还可以换另外一种方式:

影响系统检测准确性的因素有:

  • 模型类型

  • 校准程度

  • 环境亮度

上面是将列表中各项内容修改为名词,由于是名词,每项结尾处不使用任何标点符号(参见前面专门讲标点符号的章节)。下面是对列表运用的总结:

  1. 列表一般用于枚举、过程描述、要点归纳等场合;

  2. 需要强调顺序的时候应该使用有序列表,其余视情况而定;

  3. 列表中各项内容结构应保持一致,都是名词、短语或者句子;

  4. 每个列表前面尽量添加一个明确的“引入说明”,以冒号结束。

使用表格

表格其实跟面向对象有一定联系,大部分时候表格中的一行相当于一个对象,表格中的列相当于对象的属性(字段),表格和面向对象组织数据的方式本质上是一致的。技术型文档中表格一般用来组织与数字有关的内容,当然也有例外,就像前面章节中用到的表格,纯粹是为了组织文本内容。

下面是在技术型文档中,使用表格时可以参考的一些经验:

  1. 组织数字相关内容时,一定要用表格(大部分人可能已经有这个意识);

  2. 组织结构化类型的文本内容时,尽量用表格;

  3. 每个表格都应该配一个表格标题,简要说明表格内容;

  4. 文档中的表格应具备一致的样式和风格,比如标题字体、背景填充等。

在技术型文档中使用表格组织文本内容时,需要控制每个单元格的文本长度。一般情况下建议单元格中只使用短语,如果必须要用段落,也应该控制段落中句子数量(一般建议不超过2~3句)。下面是错误使用表格来组织文本内容的示范:

序号
语言
介绍
1
C
C语言由贝尔实验室发明于1969至1973年,是一种编译型计算机编程语言。它运行速度快、效率高、使用灵活,常被用于计算机底层驱动以及各种语言编译器的开发。C语言是一种面向过程的编程语言,同时它的语法相对来讲较复杂,新人入门门槛比较高。
2
C++
C++语言发明于1979年,是一种编译型计算机编程语言。它衍生自C语言,继承了C语言的一些特性,比如使用指针直接访问内存,同时它也支持面向对象编程,提升了代码的可复用性、可扩展性以及灵活性。由于C++继承了C的大部分语法,再加上本身具备复杂的类型系统以及泛型编程等语言特性,新人入门门槛也比较高。
3
Python
Python语言发明于1991年,是一种解释型计算机编程语言,因此运行速度相对要慢。Python除了支持面向对象编程之外,还支持函数式编程,它语法简单,更贴近人类自然语言,新人入门门槛较低。Python是目前人工智能领域最热门的语言,对应的工具库非常丰富。

表6-1 三种编程语言介绍

上面是以表格的形式来介绍C、C++以及Python三种编程语言,但是在“介绍”那一列中的文本内容太长,我们可以换一种表达方式:

C
C++
Python
由AT&T 贝尔实验室发明于1969至1973年
由BJarne Struistrup发明于1979年
由Guido van Rossum发明于1991年
语法比较复杂,新人入门门槛高
语法比较复杂,新人入门门槛较高
语法简单,贴近人类自然语言,新人入门门槛低
编译型语言
编译型语言
解释型语言
支持面向过程编程
支持面向过程、面向对象编程
支持面向过程、面向对象、函数式编程
偏底层、运行速度快、使用灵活
继承了C语言的一些特性,在其基础之上还支持面向对象等特性
语法简单,学习难度低
一般用于驱动、编译器、嵌入式或者其他偏向硬件层面的开发
一般用于游戏前后端、PC客户端的开发
一般用于数据科学、人工智能相关开发

表6-2 C vs C++ vs Python

上面表格一共还是3列,但是现在每列代表一种编程语言,列中的每个单元格是对该语言的描述,描述内容都比较精简。如果你想继续补充内容,可以对应地增加行即可。

表格的组织方式有多种多样,行可以变成列、列可以变成行,并没有严格的限制。我们只需要找一个适合自己的方式,比如上面这种每列代表一种语言,是因为该场景需要介绍的编程语言只有三种,如果数量再多点(或者数量不确定,后期会继续增加),那么表格宽度就不太够、这种组织方式就不再合适。


 
7 
一图胜千言


人类在发明文字媒介之前,用的是图形符号。图像(或图形、图片)是所有内容表达方式中最直观的一种,同时也能提升读者的阅读兴趣。有人专门做过研究:在文档中增加图像能提升读者对文档的喜爱程度,不管这个图像跟文档内容本身是否有关系(https://reurl.cc/RjkrK6)。

也就是说,哪怕在文档中插入无关紧要的图像,读者也更愿意去尝试阅读文档中其他的内容。我们平时看别人演示PPT时,如果发现整页都是文字描述,大概率就不会有认真去听的欲望。下面是一段对双向链表的文字描述:

双向链表也叫双链表,是链表的一种。它的每个数据节点中都有两个指针,分别指向直接后继节点和直接前驱节点。所以,从双向链表中的任意一个节点开始,我们都可以很方便地访问它的前驱节点和后继节点。在应用双向链表时,我们一般构造双向循环链表,链表首尾相连。

上面这段描述双向链表的文字本身已经非常清晰,对数据结构有一定基础的人看完文字基本就能理解双向链表的结构和应用场合(基于它的特点)。但是,如果是一个零基础的小白来看这段话,可能效果就不会太好(尤其如果这段话是作为PPT中的内容,大概不会再有更多的内容补充)。如果我们在这段话后面增加一个插图,来直观告诉读者双向链表长什么样:

双向链表也叫双链表,是链表的一种。它的每个数据节点中都有两个指针,分别指向直接后继节点和直接前驱节点。所以,从双向链表中的任意一个节点开始,我们都可以很方便地访问它的前驱节点和后继节点。在应用双向链表时,我们一般构造双向循环链表,链表首尾相连。下图是双向链表结构示意图:


图1 双向链表结构

上面的文本配合图片,能让读者更加直观的理解双向链表的结构特点。当文档中的文本和图片同时出现时,读者大概率会先看图片,然后再结合文字去理解,加快文档阅读速度。

可抽象也可具体

技术型文档中的插图不一定都得是流程图、架构图、或者结构设计图这种非常具体的技术相关图片,还可以是抽象的、能形象表达文档主题的图片。下面是在技术型文档中使用卡通和漫画图片的示例:

示例1:

Gitlab中有Label和Tag两个概念。


为了便于区分,这里将Label翻译成“标签”,将Tag翻译成“标记”(在有些地方这两个单词翻译并没有严格的差异)。Gitlab中标签的作用是为了分类、快速地检索和过滤,用户能通过标签来直观的管理Issues,比如to-do、bug等等。

标记的主要作用是为了归档,给Commit取一个形象的别名,后期快速定位和查找。GitLab中创建标记可以理解为“做记号”,建立索引。一般推荐为标记定义一个有意义的名称,比如以版本号为名,当我们要发布1.0版本,对应的标记名称可以是“v1.0”,如果我们要发布2.0预览版,那么对应的标记名称可以是“2.0-pre”。

示例2:

源码版本控制系统(Source Code Version Control System)主要负责对源代码版本进行管理,涉及到代码提交、撤销、比对、分支管理、代码合并等功能。源码管理是软件开发过程中非常重要的一个环节,它能有效保证软件代码质量。


图1 团队协作

源码管理并不是软件开发周期的全部,整个软件开发周期涉及到多个流程、多个团队(多人)协作完成,包括立项/结项、进度/任务管理、需求/设计、bug管理、测试、集成上线等环节。

突出图中重点

当我们想为文档添加图片时,单张图片包含的内容不宜太过复杂,图片应该能准确地表达意思。如果一张图太过复杂、或者包含了一些可能引起歧义的部分,我们可以尝试以下两种改进方式:

  1. 将复杂的图拆开,一张图对应一个局部细节;

  2. 在图片中将重点区域标记出来,让读者可以一眼就发现重点。

在技术型文档中插入复杂的系统架构图很常见,这种时候建议遵循“先宏观,再具体”的原则,循序渐进。我们不要一上来就放一张大图,还想将所有的细节都包含进去,这种想法不太现实,这不仅对你画图的技能要求很高,读者看完也容易一脸懵。下面这张图太过复杂:

整个视频分析系统由3大服务组成,分别是Intelligent Video Analytics、Front Video Service以及Distribute Load Balance,这3大服务一共包含15个子模块。下面是视频分析系统结构:


图1 视频分析系统结构

上面这个例子中插入的这张图既想描述3大服务之间的交互关系、又想描述各个服务内部子模块之间的交互关系(上面只是示意图,实际情况可能比这个更复杂)。文档读者碰到这种情况可能会产生两个感觉:一是图太复杂了,很难看懂,有些地方迫于空间原因字号还小;二是我需要重点关注的点在哪里?如果遵循前面提到的“先宏观,再具体”的原则,上面这个例子可以调整为:

整个视频分析系统由3大服务组成,分别是Intelligent Video Analytics、Front Video Service以及Distribute Load Balance。下面是视频分析系统中各服务之间的关系:


图1 视频分析系统服务交互

其中,Intelligent Video Analytics服务主要负责对视频解码、推理以及行为分析等结构化操作。该服务内部一共包含9个子模块,模块之间的关系见下图:


图2 Intelligent Video Analytics服务子模块交互

Front Video Service服务主要负责视频接入、分发、配置管理等功能。该服务内部一共包含3个子模块……

另外一种情况,插入的图片中包含了不相干内容,文档作者又没有给出醒目的标记,读者看完不清楚关注重点在哪里。下面是错误的示例:

GitLab中的Release功能主要用来对仓库中的代码以及其他一些相关资料文件进行归档,通常用于版本发布。当有新版本发布时,用户可以基于对应的Commit创建一个Tag标记,给它一个合理的名字,比如“v1.0-pre”(代表发布1.0预览版),然后再基于该Tag发布版本。

后期,其他人可以通过Release菜单快速浏览、检索项目版本发布记录以及对应时间点的相关代码和资料。用户可以在GitLab主界面的左侧菜单中找到Release功能入口:


图1 Gitlab中Release菜单

上面图片在介绍Release功能时给出的图片中包含的菜单项太多,为了让读者更直观看懂图片关注点,可以将图片调整如下(左右两种都可以):

GitLab中的Release功能主要用来对仓库中的代码以及其他一些相关资料文件进行归档,通常用于版本发布。当有新版本发布时,用户可以基于对应的Commit创建一个Tag标记,给它一个合理的名字,比如“v1.0-pre”(代表发布1.0预览版),然后再基于该Tag发布版本。

后期,其他人可以通过Release菜单快速浏览、检索项目版本发布记录以及对应时间点的相关代码和资料。用户可以在Gitlab主界面的左侧菜单中找到Release功能入口:


图1 Gitlab中Release菜单

有准确的图标题

图片是为了读者能够更直观地理解文档内容,但是图片毕竟不是文字,不同的人对同一张图片理解可能存在差异,尤其对于那种不包含任何文字的图片。因此,在文档中插入任何图片时,我们应该为它定义一个合适、贴切的标题。图标题一般是一个名词或者短语,作用跟前面讲到的表格标题一样,协助读者理解图片所要表达的含义。


 
8 
统一样式和风格


文档的样式和风格其实跟我们写代码一样,写代码要遵守统一的代码风格(变量命名、换行规则等等),写文档也应该遵守统一的文档风格。公司或者组织一般都有自己的文档风格规范,规范会定义好正文/标题字体字号、页眉页脚、页边距、行间距、段前段后间距等等,按照规范写出来的文档风格基本就能保持一致。

对于没有规范可用的场合,文档作者可以根据自己的偏好执行即可,保证整篇文档的内容遵守相同的风格,比如文档开头和文档结尾的段落间距、列表样式、对齐方式都应该保持一致。本篇文档的主要规范定义如下:

  1. 页边距上下左右2cm;

  2. 标题18号华文仿宋,正文12号宋体,正文中表格/图标题12号华文仿宋;

  3. 段前/段后间距0.5,段落行间距1.5倍,段落首行对齐不空格;

  4. 表格、图片居中对齐,图标题在图片下方、表格标题在表格上方。

还有另外一些比较重要的样式定义,比如列表样式(本篇文档中每个列表外面套了一个表格,表格无左右边框),还比如本篇文档涉及到了很多举例和示范,所有的举例示范都在表格中,并且表格有自己的样式(字体字号、背景颜色等等)。


 
9 
把握好整体文档结构


把握好整体文档结构是一件非常困难的事情,这个其实跟前面讲到的文档内容本身没什么关系。文档作者在动笔之前需要有一个宏观的构思,需要在脑子里先将文档大纲梳理一遍,一级标题可以是什么、二级标题又可以是什么,然后考虑将合适的内容放到对应的章节中去。

优秀的作者在正式动手之前,可能已经有了很长一段时间的思考准备,尤其对于那种非常复杂的文档。但是这种方式对一些人来讲可能不太现实,难度太大。那么这时候就只能考虑另外一种方式,动手之前先在白纸上打草稿,列出来文档大纲,然后不断修改和调整,直到满意为止。

其实不管上面哪种方式,文档结构考验的是作者组织内容的思维能力。对于一些需求、设计类型的“主流”技术型文档,考验的是作者对软件需求、系统架构的理解深度,该写什么不该写什么,写到什么程度,这些都需要作者考虑清楚,这类型的文档一般有标准的模板可以参考,大家平时写得/见得也比较多。

对于另外一些“非主流”类型的技术型文档,比如对某个线上问题的分析报告、技术/原型调研类文档,这些文档一般规模比较小、也没什么参考标准,全靠作者自己去组织。

下面就以“对某个用户需求做技术性反馈”为例,抛砖引玉,简单描述一下技术型文档结构应该如何去组织:

场景说明:

视频分析系统中,客户要求在事件录像文件中对涉事车辆目标(或区域)进行高亮标框显示,视频录像在播放时会有一个醒目的多边形提醒用户具体事件发生位置。客户懂一些技术相关知识,要求公司技术研发团队针对该需求给出合理的需求反馈,如果需求可实现,评估工作难度;如果需求不可实现,说明具体原因。

根据上面场景说明,该需求并非硬性要求。甲方提出了一个想法,并且非常贴心地考虑到了乙方是否具备条件实现,希望给出一个实质性的答复。公司技术团队在写反馈说明文档之前,应该考虑以下两个问题:

  1. 如果正常响应该需求,具体的方案是什么、难点/风险点各是什么;

  2. 如果不能正常响应该需求,具体原因是什么,是否有可替代方案、替代方案是什么。

也就是说,不管最终团队是否响应该需求,我们在文档中都要有非常实质性的内容,不应该是空话、套话。下面就以“不响应”为例,描述文档应该包含哪些内容:

序号
节标题名称
主要内容
1
背景说明
用自己的话将客户的需求完整描述一遍,不要有任何偏差,表明我方已认真理解过原始需求。
2
已有录像逻辑
详细描述系统中目前已有的事件录像逻辑。因为我们本次是不响应该需求,所以对后面不响应有利的内容一定要着重强调(要突出已有录像逻辑的优势)。
3
录像标框逻辑
详细描述在事件录像文件中对涉事目标(或区域)进行高亮标框的逻辑。注意这里按照理想逻辑去描述,不用考虑任何外在限制。
4
录像标框难点
结合第3点,重点归纳、整理出在录像文件中标框的难点,比如需要对每一路进行解码再去叠加图形、视频画面不能压缩否则影响分辨率等等,这些对设备性能要求非常高,会增加硬件成本。
5
解决方案一 (不计代价去响应)
按照理想逻辑去响应,但是要提出前提条件或者代价,比如单台设备分析路数降低到原来的一半,硬件成本是原来的2本。(其实就是要排除这个方案)
6
解决方案二 (可替代方案)
提出一种可替代的方案,可以满足客户最开始提出的“有醒目标记提醒用户”。比如当视频录像播放时,可以在播放器上面叠加一个高亮方框,能够大概标记涉事车辆目标(或区域)。同时,强调该方案的优势(比如工作周期短、对成本无影响)。
7
结论
其实根据前面的描述,只要认真读完文档的人基本都能知道结论是什么、应该选哪个方案。但是这里还是要书面写上,根据前面的描述,解决方案二有更大的优势,建议采用方案二。

需要注意的是,“响应”或者“不响应”的决定很多时候不在技术团队或者写这个文档的人手里。虽然文档中的内容应该为最终的结论服务,但是总体上不应该有偏差。


 
10 
明确文档的目标群体


文档的目标群体是谁?这个其实应该是写文档最开始就需要明确的东西,面对不同的群体,我们文档的内容、结构包括内容描述程度都会不同。尽早确定读者有助于在构思阶段就明确文档内容边界,哪些该写、哪些不该写,该写的又应该如何去写,这些都是编写文档的大方向。

作者:周智,前微软(中国)Windows工程院员工,目前从事于深度学习计算机视觉相关工作,交通安防领域的视频目标检测、跟踪和行为分析。

收起阅读 »

不幸言中,“核酸码”打不开.....那就聊聊为什么我觉得要挂的原因吧!

周四晚上的时候,看到消息说4月9日起要采用新的核酸检查系统,要推出一个新的码,叫:核酸码。当晚就有很多网友发现随申办上已经有入口了,但点进去是报错的:但是因为还没投入真正使用,所以也没啥大的反馈,大家就瞎讨论了技术栈和这个错误可能的原因啥的。我也顺带瞎扯了一句...
继续阅读 »

周四晚上的时候,看到消息说4月9日起要采用新的核酸检查系统,要推出一个新的码,叫:核酸码

当晚就有很多网友发现随申办上已经有入口了,但点进去是报错的:

但是因为还没投入真正使用,所以也没啥大的反馈,大家就瞎讨论了技术栈和这个错误可能的原因啥的。

我也顺带瞎扯了一句:可能会出性能问题(因为我一直觉得国内擅长Hibernate的开发者比较少)


谁想到,今天在获取核酸码的时候真的碰到各种困难,在获取核酸码的时候,就一直刷不出来,有时候显示人多,有时候504错误:

上面我是12点尝试的,后来16、17点还看到很多朋友圈反应各种卡住,刷不出来。


可能这个系统确实太赶了,所以没做好?不过这个谁知道呢?作为一名技术博主就不瞎猜了。

顺手分享一下为什么我觉得用spring data jpa,很可能会挂?

先说说常规国内用的比较多的技术MyBatis,因为大家都是用直接写SQL的方式来实现数据读写的,这个时候团队里DBA、数据库专家、或者实力强点的开发,往往自己已经能够把SQL执行优化到比较好的地步了。这个是否能做好,与我们对SQL、Java这些知识的掌握程度有关

而当我们用Spring Data JPA这样的框架时候,开发者在框架的帮助下,好多SQL都被隐藏了,喜欢些Java代码来替代SQL的开发过程是挺爽的,但也因为这个原因,他可能并不知道最终自己写的代码真正会执行的SQL具体是怎么样的。

这的时候对于优化就带了很大的难度,对于专业DBA来说,他一般都是不具备Spring Data JPA代码到SQL转化的认识,他是很难帮你做静态分析的。而开发者一侧也有这个问题,如果不是很熟悉Hibernate的话,就很容易写出低性能的代码(不代表框架实现的低性能,核心还是使用姿势的问题)。

所以,我一直建议在高并发系统中对数据访问框架的选型一定要慎重,不是说Spring Data JPA不行,而是需要有熟悉的人来把握(特别提这点的原因是国好多是半调子)。不然就比较容易出现性能问题,但是MyBatis的话,对于国内开发者来说,因为直接写SQL,所以还是相对还是更容易理解和把控一些。

好了,借今天核酸码的现象,跟大家聊聊这两个框架的想法,不知道你是否认同?欢迎留言区说说你的观点。

来源:https://mp.weixin.qq.com/s/43bE8juIRQbQLO3vBTUKWA

收起阅读 »

面试官:知道 Flutter 生命周期?下周来入职!

作为一名移动端开发工程师,刚接触 Flutter 的时候,一定会有这样的疑问:Flutter 的生命周期是怎么样的?是如何处理生命周期的?我的 onCreate()[Android] 在哪里?viewDidLoad()[iOS] 呢? 我的业务逻辑应该放在哪里...
继续阅读 »

作为一名移动端开发工程师,刚接触 Flutter 的时候,一定会有这样的疑问:Flutter 的生命周期是怎么样的?是如何处理生命周期的?我的 onCreate()[Android] 在哪里?viewDidLoad()[iOS] 呢? 我的业务逻辑应该放在哪里处理?初始化数据呢?希望看了这篇文章后,可以对你有一点小小的帮助。


安卓


如果你是一名安卓开发工程师,那么对于 Activity 生命周期肯定不陌生



  • onCreate

  • onStart

  • onResume

  • onPause

  • onStop

  • onDestroy


android_life_cycle


iOS


如果你是一名 iOS 开发工程师,那么 UIViewController 的生命周期肯定也已经很了解了。



  • viewDidLoad

  • viewWillAppear

  • viewDidAppear

  • viewWillDisappear

  • viewDidDisappear

  • viewDidUnload


ios_life_cycle


Flutter


知道了 Android 和 iOS 的生命周期,那么 Flutter 呢?有和移动端对应的生命周期函数么?如果之前你对 Flutter 有一点点了解的话,你会发现 Flutter 中有两个主要的 Widget:StatelessWidget(无状态)StatefulWidget(有状态)。本篇文章我们主要来介绍下 StatefulWidget,因为它有着和 Android 和 iOS 相似的生命周期。


StatelessWidget


无状态组件是不可变的,这意味着它们的属性不能变化,所有的值都是最终的。可以理解为将外部传入的数据转化为界面展示的内容,只会渲染一次。
对于无状态组件生命周期只有 build 这个过程。无状态组件的构建方法通常只在三种情况下会被调用:小组件第一次被插入树中,小组件的父组件改变其配置,以及它所依赖的 InheritedWidget 发生变化时。


StatefulWidget


有状态组件持有的状态可能在 Widget 生命周期中发生变化,是定义交互逻辑和业务逻辑。可以理解为具有动态可交互的内容界面,会根据数据的变化进行多次渲染。实现一个 StatefulWidget 至少需要两个类:



  • 一个是 StatefulWidget 类。

  • 另一个是 Sate 类。StatefulWidget 类本身是不可变的,但是 State 类在 Widget 生命周期中始终存在。StatefulWidget 将其可变的状态存储在由 createState 方法创建的 State 对象中,或者存储在该 State 订阅的对象中。


StatefulWidget 生命周期



  • createState:该函数为 StatefulWidget 中创建 State 的方法,当 StatefulWidget 被创建时会立即执行 createState。createState 函数执行完毕后表示当前组件已经在 Widget 树中,此时有一个非常重要的属性 mounted 被置为 true。

  • initState:该函数为 State 初始化调用,只会被调用一次,因此,通常会在该回调中做一些一次性的操作,如执行 State 各变量的初始赋值、订阅子树的事件通知、与服务端交互,获取服务端数据后调用 setState 来设置 State。

  • didChangeDependencies:该函数是在该组件依赖的 State 发生变化时会被调用。这里说的 State 为全局 State,例如系统语言 Locale 或者应用主题等,Flutter 框架会通知 widget 调用此回调。类似于前端 Redux 存储的 State。该方法调用后,组件的状态变为 dirty,立即调用 build 方法。

  • build:主要是返回需要渲染的 Widget,由于 build 会被调用多次,因此在该函数中只能做返回 Widget 相关逻辑,避免因为执行多次而导致状态异常。

  • reassemble:主要在开发阶段使用,在 debug 模式下,每次热重载都会调用该函数,因此在 debug 阶段可以在此期间增加一些 debug 代码,来检查代码问题。此回调在 release 模式下永远不会被调用。

  • didUpdateWidget:该函数主要是在组件重新构建,比如说热重载,父组件发生 build 的情况下,子组件该方法才会被调用,其次该方法调用之后一定会再调用本组件中的 build 方法。

  • deactivate:在组件被移除节点后会被调用,如果该组件被移除节点,然后未被插入到其他节点时,则会继续调用 dispose 永久移除。

  • dispose:永久移除组件,并释放组件资源。调用完 dispose 后,mounted 属性被设置为 false,也代表组件生命周期的结束。


不是生命周期但是却非常重要的几个概念


下面这些并不是生命周期的一部分,但是在生命周期中起到了很重要的作用。



  • mounted:是 State 中的一个重要属性,相当于一个标识,用来表示当前组件是否在树中。在 createState 后 initState 前,mounted 会被置为 true,表示当前组件已经在树中。调用 dispose 时,mounted 被置为 false,表示当前组件不在树中。

  • dirty:表示当前组件为脏状态,下一帧时将会执行 build 函数,调用 setState 方法或者执行 didUpdateWidget 方法后,组件的状态为 dirty。

  • clean:与 dirty 相对应,clean 表示组件当前的状态为干净状态,clean 状态下组件不会执行 build 函数。


stateful_widget_lifecycle 生命周期流程图


上图为 flutter 生命周期流程图


大致分为四个阶段



  1. 初始化阶段,包括两个生命周期函数 createState 和 initState;

  2. 组件创建阶段,包括 didChangeDependencies 和 build;

  3. 触发组件多次 build ,这个阶段有可能是因为 didChangeDependencies、 setState 或者 didUpdateWidget 而引发的组件重新 build ,在组件运行过程中会多次触发,这也是优化过程中需要着重注意的点;

  4. 最后是组件销毁阶段,deactivate 和 dispose。


组件首次加载执行过程


首先我们来实现下面这段代码(类似于 flutter 自己的计数器项目),康康组件首次创建是否按照上述流程图中的顺序来执行的。



  1. 创建一个 flutter 项目;

  2. 创建 count_widget.dart 中添加以下代码;


import 'package:flutter/material.dart';

class CountWidget extends StatefulWidget {
CountWidget({Key key}) : super(key: key);

@override
_CountWidgetState createState() {
print('count createState');
return _CountWidgetState();
}
}

class _CountWidgetState extends State<CountWidget> {
int _count = 0;
void _incrementCounter() {
setState(() {
print('count setState');
_count++;
});
}

@override
void initState() {
print('count initState');
super.initState();
}

@override
void didChangeDependencies() {
print('count didChangeDependencies');
super.didChangeDependencies();
}

@override
void didUpdateWidget(CountWidget oldWidget) {
print('count didUpdateWidget');
super.didUpdateWidget(oldWidget);
}

@override
void deactivate() {
print('count deactivate');
super.deactivate();
}

@override
void dispose() {
print('count dispose');
super.dispose();
}

@override
void reassemble() {
print('count reassemble');
super.reassemble();
}

@override
Widget build(BuildContext context) {
print('count build');
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'$_count',
style: Theme.of(context).textTheme.headline4,
),
Padding(
padding: EdgeInsets.only(top: 100),
child: IconButton(
icon: Icon(
Icons.add,
size: 30,
),
onPressed: _incrementCounter,
),
),
],
),
);
}
}

上述代码把 StatefulWidget 的一些生命周期都进行了重写,并且在执行中都打印了标识,方便看到函数的执行顺序。



  1. 在 main.dart 中加载该组件。代码如下:


import 'package:flutter/material.dart';

import './pages/count_widget.dart';

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);

final String title;

@override
_MyHomePageState createState() {
return _MyHomePageState();
}
}

class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: CountWidget(),
);
}
}

这时 CountWidget 作为 MyHomePage 的子组件。我们打开模拟器,开始运行。在控制台可以看到如下日志,可以看出 StatefulWidget 在第一次被创建的时候是调用下面四个函数。


flutter: count createState
flutter: count initState
flutter: count didChangeDependencies
flutter: count build

点击屏幕上的 ➕ 按钮,_count 增加 1,模拟器上的数字由 0 变为 1,日志如下。也就是说在状态发生变化的时候,会调用 setStatebuild 两个函数。


flutter: count setState
flutter: count build

command + s 热重载后,日志如下:


flutter: count reassemble
flutter: count didUpdateWidget
flutter: count build

注释掉 main.dart 中的 CountWidget,command + s 热重载后,这时 CountWidget 消失在模拟器上,日志如下:


class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
// body: CountWidget(),
);
}
}

flutter: count reassemble
flutter: count deactivate
flutter: count dispose

经过上述一系列操作之后,通过日志打印并结合生命周期流程图,我们可以很清晰的看出各生命周期函数的作用以及理解生命周期的几个阶段。
相信很多细心的同学已经发现了一个细节,那就是 build 方法在不同的操作中都被调用了,下面我们来介绍什么情况下会触发组件再次 build。


触发组件再次 build


触发组件再次 build 的方式有三种,分别是 setStatedidChangeDependenciesdidUpdateWidget


1.setState 很好理解,只要组件状态发生变化时,就会触发组件 build。在上述的操作过程中,点击 ➕ 按钮,_count 会加 1,结果如下图:


set_state


2.didChangeDependencies,组件依赖的全局 state 发生了变化时,也会调用 build。例如系统语言等、主题色等。


3.didUpdateWidget,我们以下方代码为例。在 main.dart 中,同样的重写生命周期函数,并打印。在 CountWidget 外包一层 Column ,并创建同级的 RaisedButton 做为父 Widget 中的计数器。


class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);

final String title;

@override
_MyHomePageState createState() {
print('main createState');
return _MyHomePageState();
}
}

class _MyHomePageState extends State<MyHomePage> {
int mainCount = 0;

void _changeMainCount() {
setState(() {
print('main setState');
mainCount++;
});
}

@override
void initState() {
print('main initState');
super.initState();
}

@override
void didChangeDependencies() {
print('main didChangeDependencies');
super.didChangeDependencies();
}

@override
void didUpdateWidget(MyHomePage oldWidget) {
print('main didUpdateWidget');
super.didUpdateWidget(oldWidget);
}

@override
void deactivate() {
print('main deactivate');
super.deactivate();
}

@override
void dispose() {
print('main dispose');
super.dispose();
}

@override
void reassemble() {
print('main reassemble');
super.reassemble();
}

@override
Widget build(BuildContext context) {
print('main build');
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
children: <Widget>[
RaisedButton(
onPressed: () => _changeMainCount(),
child: Text('mainCount = $mainCount'),
),
CountWidget(),
],
),
);
}
}

重新加载 app,可以看到打印日志如下:


father_widget_create_state


flutter: main createState
flutter: main initState
flutter: main didChangeDependencies
flutter: main build
flutter: count createState
flutter: count initState
flutter: count didChangeDependencies
flutter: count build

可以发现:



  • 父组件也经历了 createStateinitStatedidChangeDependenciesbuild 这四个过程。

  • 并且父组件要在 build 之后才会创建子组件。


点击 MyHomePage(父组件)的 mainCount 按钮 ,打印如下:


flutter: main setState
flutter: main build
flutter: count didUpdateWidget
flutter: count build

点击 CountWidget 的 ➕ 按钮,打印如下:


flutter: count setState
flutter: count build

可以说明父组件的 State 变化会引起子组件的 didUpdateWidget 和 build,子组件自己的状态变化不会引起父组件的状态改变


组件销毁


我们重复上面的操作,为 CountWidget 添加一个子组件 CountSubWidget,并用 count sub 前缀打印日志。重新加载 app。


注释掉 CountWidget 中的 CountSubWidget,打印日志如下:


flutter: main reassemble
flutter: count reassemble
flutter: count sub reassemble
flutter: main didUpdateWidget
flutter: main build
flutter: count didUpdateWidget
flutter: count build
flutter: count sub deactivate
flutter: count sub dispose

恢复到注释前,注释掉 MyHomePage 中的 CountWidget,打印如下:


flutter: main reassemble
flutter: count reassemble
flutter: count sub reassemble
flutter: main didUpdateWidget
flutter: main build
flutter: count deactivate
flutter: count sub deactivate
flutter: count sub dispose
flutter: count dispose

因为是热重载,所以会调用 reassembledidUpdateWidgetbuild,我们可以忽略带有这几个函数的打印日志。可以得出结论:
父组件移除,会先移除节点,然后子组件移除节点,子组件被永久移除,最后是父组件被永久移除。


Flutter App Lifecycle


上面我们介绍的生命周期主要是 StatefulWidget 组件的生命周期,下面我们来简单介绍一下和 app 平台相关的生命周期,比如退出到后台。


我们创建 app_lifecycle_state.dart 文件并创建 AppLifecycle,他是一个 StatefulWidget,但是他要继承 WidgetsBindingObserver。


import 'package:flutter/material.dart';

class AppLifecycle extends StatefulWidget {
AppLifecycle({Key key}) : super(key: key);

@override
_AppLifecycleState createState() {
print('sub createState');
return _AppLifecycleState();
}
}

class _AppLifecycleState extends State<AppLifecycle>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
print('sub initState');
}

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// TODO: implement didChangeAppLifecycleState
super.didChangeAppLifecycleState(state);
print('didChangeAppLifecycleState');
if (state == AppLifecycleState.resumed) {
print('resumed:');
} else if (state == AppLifecycleState.inactive) {
print('inactive');
} else if (state == AppLifecycleState.paused) {
print('paused');
} else if (state == AppLifecycleState.detached) {
print('detached');
}
}

@override
Widget build(BuildContext context) {
print('sub build');
return Container(
child: Text('data'),
);
}
}

didChangeAppLifecycleState 方法是重点,AppLifecycleState 中的状态包括:resumedinactivepauseddetached 四种。


didChangeAppLifecycleState 方法的依赖于系统的通知(notifications),正常情况下,App是可以接收到这些通知,但有个别情况下是无法接收到通知的,比如用户关机等。它的四种生命周期状态枚举源码中有详细的介绍和说明,下面附上源码以及简单的翻译说明。


app_life_cycle_state



  • resumed:该应用程序是可见的,并对用户的输入作出反应。也就是应用程序进入前台。

  • inactive:应用程序处于非活动状态,没有接收用户的输入。在 iOS 上,这种状态对应的是应用程序或 Flutter 主机视图在前台非活动状态下运行。当处于电话呼叫、响应 TouchID 请求、进入应用切换器或控制中心时,或者当 UIViewController 托管的 Flutter 应用程序正在过渡。在 Android 上,这相当于应用程序或 Flutter 主机视图在前台非活动状态下运行。当另一个活动被关注时,如分屏应用、电话呼叫、画中画应用、系统对话框或其他窗口,应用会过渡到这种状态。也就是应用进入后台。

  • pause:该应用程序目前对用户不可见,对用户的输入没有反应,并且在后台运行。当应用程序处于这种状态时,引擎将不会调用。也就是说应用进入非活动状态。

  • detached:应用程序仍然被托管在flutter引擎上,但与任何主机视图分离。处于此状态的时机:引擎首次加载到附加到一个平台 View 的过程中,或者由于执行 Navigator pop,view 被销毁。


除了 app 生命周期的方法,Flutter 还有一些其他不属于生命周期,但是也会在一些特殊时机被观察到的方法,如 didChangeAccessibilityFeatures(当前系统改变了一些访问性活动的回调)didHaveMemoryPressure(低内存回调)didChangeLocales(用户本地设置变化时调用,如系统语言改变)didChangeTextScaleFactor(文字系数变化) 等,如果有兴趣的话,可以去试一试。


总结


本篇文章主要介绍了 Widget 中的 StatefulWidget 的生命周期,以及 Flutter App 相关的生命周期。但是要切记,StatefulWidget 虽好,但也不要无脑的所有 Widget 全都用它,能使用 StatelessWidget 还是要尽量去使用 StatelessWidget(仔细想一下,这是为什么呢?)。好啦,看完本篇文章,你就是 Flutter 初级开发工程师了,可以去面试了(狗头保命)。


最后


真正坚持到最后的人,往往靠的不是短暂的激情,而是恰到好处的喜欢和投入。你还那么年轻,完全可以成为任何你想要成为的样子!


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

Flutter 蒙层控件

功能说明 新手引导高亮蒙层 图片进度条 使用说明 Import the packages: import 'package:flutter_mask_view/flutter_mask_view.dart'; show height-light mask ...
继续阅读 »

功能说明



  • 新手引导高亮蒙层

  • 图片进度条


使用说明


Import the packages:


import 'package:flutter_mask_view/flutter_mask_view.dart';

show height-light mask for newer:


 Scaffold(
body: Stack(
children: [
//only display background for demo
Image.asset(ImagesRes.BG_HOME),

//config
HeightLightMaskView(
//控件大小
maskViewSize: Size(720, 1080),
//蒙层颜色
backgroundColor: Colors.blue.withOpacity(0.6),
//高亮区域颜色
color: Colors.transparent,
//设置高亮区域形状,如果width = height = radius 为圆形,否则矩形
rRect: RRect.fromRectAndRadius(
Rect.fromLTWH(100, 100, 50, 50),
Radius.circular(50),
),
)
],
),
)

more:


          HeightLightMaskView(
maskViewSize: Size(720, 1080),
backgroundColor: Colors.blue.withOpacity(0.6),
color: Colors.transparent,
//自定义蒙层区域形状
pathBuilder: (Size size) {
return Path()
..moveTo(100, 100)
..lineTo(50, 150)
..lineTo(150, 150);
},
//在蒙层上自定义绘制内容
drawAfter: (Canvas canvas, Size size) {
Paint paint = Paint()
..color = Colors.red
..strokeWidth = 15
..style = PaintingStyle.stroke;
canvas.drawCircle(Offset(150, 150), 50, paint);
},
//是否重绘,默认return false, 如果使用动画,此返回true
rePaintDelegate: (CustomPainter oldDelegate){
return false;
},
)

Display



create image progress bar:


      ImageProgressMaskView(
size: Size(360, 840),
//进度图片
backgroundRes: 'images/bg.png',
//当前进度
progress: 0.5,
//蒙层形状,内置以下两种蒙层:
//矩形蒙层:PathProviders.sRecPathProvider
//水波蒙层(可配置水波高度和密度):PathProviders.createWaveProvider

//自定义进度蒙层
pathProvider: PathProviders.createWaveProvider(60, 100),
),
)

PathProviders.sRecPathProvider:



PathProviders.createWaveProvider:



与动画联动:


class _MaskTestAppState extends State<MaskTestApp>
with SingleTickerProviderStateMixin {
late AnimationController _controller;

@override
void initState() {
_controller =
AnimationController(duration: Duration(seconds: 5), vsync: this);
_controller.forward();
super.initState();
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Center(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Stack(
alignment: Alignment.center,
children: [
ImageProgressMaskView(
size: Size(300, 300),
backgroundRes: ImagesRes.IMG,
progress: _controller.value,
pathProvider: PathProviders.createWaveProvider(60, 40),
rePaintDelegate: (_) => true,
),
Text(
'${(_controller.value * 100).toInt()} %',
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
fontSize: 30,
),
)
],
);
},
),
),
);
}
}

Result:


case 1:



case 2: (png)



仓库地址


PUB


Github


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

你会写注释吗?

前言有一本书叫《代码整洁之道》,不知你看过没?初次听闻此书,并未激发我的阅读欲。再次听闻,不免心想:代码竟还整洁之道?我倒要瞧瞧,怎么个整洁法。我是怀着试探地心看了这本书,结果收获了满脑子糟糕的代码。天呐!这代码我貌似一句也看不懂,幸好还有文字,尚可宽慰我这颗...
继续阅读 »

前言

有一本书叫《代码整洁之道》,不知你看过没?

初次听闻此书,并未激发我的阅读欲。再次听闻,不免心想:代码竟还整洁之道?我倒要瞧瞧,怎么个整洁法。

我是怀着试探地心看了这本书,结果收获了满脑子糟糕的代码。天呐!这代码我貌似一句也看不懂,幸好还有文字,尚可宽慰我这颗被代码撞乱的心,于是咬咬牙读了下去。

这本书里面讲了很多代码整洁之道,关于有意义的命名、函数、注释、格式、错误处理、边界等共十七大篇章。如果你感兴趣,可以去看看。我只是粗略地看了一下,因为有些我也看不大明白。特别是当某些代码脱离了计算机而存在的时候,我好像不认识它们了,它们变得异常陌生。恕我我孤陋寡闻了,哎。

尽管如此,此书第四章中,关于“注释”的代码整洁之道,却给我留下了异常深刻的印象。Why? 因为里面关于注释的观点刷新了我的认知,与我的思想产生了一点点灵魂的碰撞,并且说服了我,还驱动我写下了这篇文章。

一、被注释吸引

下面是“注释”篇章的开头两段,特意贴了上来,因为我就是被这样的开头吸引了。希望它能带给你一点点启发。


不知你读完以上两段,作何感想?

我的感想是:如果你的代码写得足够优秀,是不需要过多注释的。注释最多可以证明糟糕的代码。

额,此刻我很想找一个捂脸的表情。与此同时,我在脑海里迅速地回忆了一遍注释之于我的心历路程:从最初知道“注释”这么个神奇玩意儿时的欣喜,到步步沦陷“注释”的魔爪,以致如今看着满屏的代码,不写点儿注释都感觉空落落的......

收回来,继续品。 作者开篇的观点约莫如下:

  • 注释的恰当用法是弥补我们在用代码表达意图时遭遇的失败

  • 如果你发现自己需要写注释,再想想是否有办法翻盘,用代码来表达

  • 注释会撒谎,代码在变动演化,但注释不能总是跟着走

  • 只有代码是唯一准确的信息来源

注意,作者用来了“失败”一词。你无法找到表达自我的恰当方法,所以就要用注释,这并不值得庆祝。当然,这并不意味作者就完全否定了注释的价值,程序员应当负责将代码保持在可维护、有关联、精确的高度。只不过作者更倾向于把力气用在写清楚代码上,直接保证无须编写注释,或者花心思减少注释量。

二、好的注释

有些注释是必须的,作者列举了一些值得写的注释。

  • 公司代码规范要求编写与法律有关的注释

  • 提供基本信息的注释

  • 对意图的解释

  • 阐释:把晦涩难明的参数或返回值的意义翻译为某种可读形式

  • 警示:用于警告其他程序员会出现某种后果的注释

  • TODO 注释:是一种程序员认为应该做,但由于某些原因目前还没做的工作

  • 放大:放大某种看起来不合理之物的重要性的注释

  • 公共 API 中的 Javadoc

尽管如此,作者一再强调:唯一真正好的注释是你想办法不去写的注释。足见作者对注释之深恶痛疾,对糟糕代码之嫌弃,对代码整洁要求之高。你可以细品。

三、坏的注释

果然是有代码洁癖的人,作者用了更多的篇幅来描述坏的注释。

  • 喃喃自语:因为过程需要就添加注释,就是无谓之举

  • 多余的注释:并不比代码本身提供更多的信息,甚至比读代码所花时间长

  • 误导性注释:写出不够精确的注释误导读者

  • 循规式注释:每个函数都要有 Javadoc 或每个变量都要有注释的规则愚不可及

  • 日志式注释:每次编辑代码时,在模块开始处添加一条注释,应当全部删除

  • 废话注释:喋喋不休,废话连篇的注释,一旦代码修改,将变成一堆谎言

  • 能用函数或变量时就别用注释:建议重构代码,删掉注释

  • 位置标记:在源代码中标记某个特别位置,多数实属无理又鸡零狗碎

  • 括号后面的注释:如果你发现自己想标记右括号,其实应该做的是缩短函数

  • 归属与署名:源代码控制系统是这类信息最好的归属地

  • 注释掉的代码:注释掉的代码堆积在一起,就像酒瓶底的渣滓一般

  • HTML 注释:源代码注释中的 HTML 标记是一种厌物

  • 非本地信息:假如你一定要写注释,请确保它描述了离它最近的代码

  • 信息过多:别在注释中添加有趣的历史性话题或无关的细节描述

  • 不明显的关系:注释及其描述的代码之间的联系应该显而易见

  • 函数头:短函数不需要太多的描述,选个好的函数名胜于写函数头注释

一言以蔽之:用整理代码的决心替代创造废话的冲动吧。 你会发现自己成为更优秀、更快乐的程序员。

小结

作者把“注释”拎出来,说了这么多,最终还是回归到了代码本身。

那如何才能写出整洁的代码呢?如果你不明白整洁对代码的意义,尝试去写整洁代码就毫无意义。如果你明白糟糕的代码给你带来的代价,你就会明白,花时间保持代码整洁不但有关效率,还有关生存。争取让营地比你来时更干净吧!

最后,贴上书中震撼我的一隅,希望它能指引你逐渐走向代码整洁之道,与君共勉!



作者:linwanxia
来源:https://juejin.cn/post/7083029096615116837

收起阅读 »

你确定(a == 1 && a == 2 && a == 3)不能为true?

前言最近遇到一个非常有意思的面试题: JavaScript中有没有可能让(a== 1 && a ==2 && a==3)返回true?讲真刚看到这题的时候,我是用这种眼神看面试官的:你TM逗我呢? 尊重一下我可行?没10年脑血栓...
继续阅读 »

前言

最近遇到一个非常有意思的面试题: JavaScript中有没有可能让(a== 1 && a ==2 && a==3)返回true?

讲真刚看到这题的时候,我是用这种眼神看面试官的:你TM逗我呢? 尊重一下我可行?没10年脑血栓问不出这玩意,


但看他一脸"贱笑",一副你一定答不出来的感觉,我觉得此事定不简单...


障眼法我TM给跪了

咱们先不管面试官的意图是什么,具体考察的是什么知识,先来看看几种奇特的解法。

解法1:隐藏字符 + if

const if = () => !0
const a = 9

if(a == 1 && a == 2 && a == 3)
{
 console.log('前端胖头鱼') // 前端胖头鱼
}

眼见为虚


我觉得此时你和我一样,在严重怀疑自己怕是个假前端if也能被改写?a明明是9却可以等于1、2、3


别急,这其实是一个障眼法,只是取巧蒙蔽了我们的双眼,请看下图


真相大白if的后面有个隐藏字符,本质上是声明了一个无论输入啥都返回true函数,而下面的代码块,更是和这个函数没半毛钱关系,怎么样都会执行!!!

{
 console.log('前端胖头鱼') // 前端胖头鱼
}

所以通过构造一个看似重写了if的代码块,仿佛真的实现了题目,实在是太骚了!!!

解法2:隐藏字符 + a变量

有了上面的经验,接下来的解法,你也不会感到奇怪了。

const aᅠ = 1
const a = 2
const ᅠa = 3

if (aᅠ == 1 && a == 2 && ᅠa == 3) {
 console.log('前端胖头鱼') // 前端胖头鱼
}


解法3:隐藏字符 + 数字变量

既然可以伪造三个a变量,那也可以伪造三个123变量嘛

const a = 1
const ᅠ1 = a
const ᅠ2 = a
const ᅠ3 = a

if (a == ᅠ1 && a == ᅠ2 && a == ᅠ3) {
 console.log('前端胖头鱼') // 前端胖头鱼
}

大千世界,果然眼见为虚啊!!!


再来一种奇特的解法

上面几种解法本质上都没有使 a == 1 && a == 2 && a == 3true,不过是障眼法,大家笑笑就好啦!接下来我要认真起来了...

解法4:“with”

MDN上映入眼帘的是一个警告,仿佛他的存在就是个错误,我也从来没有在实际工作中用过他,但他却可以用来解决这个题目。


let i = 1

with ({
 get a() {
   return i++
}
}) {
 if (a == 1 && a == 2 && a == 3) {
   console.log('前端胖头鱼')
}
}

聪明的你甚至都不用我解释代码啥意思了。

隐式转换成解题的关键

上面给出的4种解法多少有点歪门邪道的意思,为了让面试官死心,接下来的才是正解之道,而JS中的隐式转换规则大概也是出这道题的初衷。

隐式转换部分规则

JS中使用==对两个值进行比较时,会进行如下操作:

  1. 将两个被比较的值转换为相同的类型。

  2. 转换后(等式的一边或两边都可能被转换)再进行值的比较。

比较的规则如下表(mdn


从表中可以得到几点信息为了让(a == 1),a只有这几种:

  1. a类型为String,并且可转换为数字1('1' == 1 => true

  2. a类型为Boolean,并且可转换为数字1 (true == 1 => true)

  3. a类型为Object,通过转换机制后,可转换为数字1 (请看下文

对象转原始类型的"转换机制"

规则1和2没有什么特殊的地方,我们来看看3:

对象转原始类型,会调用内置的[ToPrimitive]函数,逻辑大致如下:

  1. 如果有Symbol.toPrimitive方法,优先调用再返回,否则进行2。

  2. 调用valueOf,如果可以转换为原始类型,则返回,否则进行3。

  3. 调用toString,如果可以转换为原始类型,则返回,否则进行4。

  4. 如果都没有返回原始类型,会报错。

const obj = {
 value: 1,
 valueOf() {
   return 2
},
 toString() {
   return '3'
},
[Symbol.toPrimitive]() {
   return 4
}
}

obj == 4 // true
// 您可以将Symbol.toPrimitive、toString、valueOf分别注释掉验证转换规则

解法5: Symbol.toPrimitive

我们可以利用隐式转换规则3完成题目(看完答案你就知道为什么啦!

const a = {
 i: 1,
[Symbol.toPrimitive]() {
   return this.i++
}
}
// 每次进行a == xxx时都会先经过Symbol.toPrimitive函数,自然也就可以实现a依次递增的效果
if (a == 1 && a == 2 && a == 3) {
 console.log('前端胖头鱼') // 前端胖头鱼
}

解法6: valueOf vs toString

当然也可以利用valueOftoString

let a = {
 i: 1,
 // valueOf替换成toString效果是一样的
 // toString
 valueOf() {
   return this.i++
}
}

if (a == 1 && a == 2 && a == 3) {
 console.log('前端胖头鱼') // 前端胖头鱼
}

解法7:Array && join

数组对象在进行隐式转换时,同样符合规则3,只是在toString时还会调用join方法。所以也可以从这里下手

let a = [1, 2, 3]

a.join = a.shift

if (a == 1 && a == 2 && a == 3) {
 console.log('前端胖头鱼') // 前端胖头鱼
}

数据劫持亦是一条出路

通过隐式转换我们做出了3种让a == 1 && a == 2 && a == 3返回true的方案,聪明的你一定想到另一种思路,数据劫持,伟大的Vue就曾使用数据劫持赢得了千万开发者的芳心,我们也试试用它来解决这道面试题

解法8:Object.defineProperty

通过劫持window对象,每次读取a属性时,都给_a 增加1

let _a = 1
Object.defineProperty(window, 'a', {
 get() {
   return _a++
}
})

if (a == 1 && a == 2 && a == 3) {
 console.log('前端胖头鱼') // 前端胖头鱼
}

解法9:Proxy

当然还有另一种劫持数据的方式,Vue3也是将响应式原理中的数据劫持Object.defineProperty换成了Proxy

let a = new Proxy({ i: 1 }, {
get(target) {
return () => target.i++
}
})

if (a == 1 && a == 2 && a == 3) {
console.log('前端胖头鱼') // 前端胖头鱼
}

最后

希望能一直给大家分享实用、基础、进阶的知识点,一起早早下班,快乐摸鱼。

作者:前端胖头鱼
来源:https://juejin.cn/post/7079936779914051615

收起阅读 »

作为一名前端,该如何理解Nginx?

大家好,我叫小杜杜,作为一名小前端,只需要好好写代码,至于部署相关的操作,我们通常接触不到,正所谓专业的人干专业的事,我们在工作中并不需要去配置,但这并不代表不需要了解,相信大家都多多少少听过nginx,所以今天就聊聊,还请大家多多支持~Nginx是什么?Ng...
继续阅读 »

大家好,我叫小杜杜,作为一名小前端,只需要好好写代码,至于部署相关的操作,我们通常接触不到,正所谓专业的人干专业的事,我们在工作中并不需要去配置,但这并不代表不需要了解,相信大家都多多少少听过nginx,所以今天就聊聊,还请大家多多支持~


Nginx是什么?

Nginx (engine x) 是一个轻量级、高性能的HTTP反向代理服务器,同时也是一个通用代理服务器(TCP/UDP/IMAP/POP3/SMTP),最初由俄罗斯人Igor Sysoev编写。

简单的说:

  • Nginx是一个拥有高性能HTTP和反向代理服务器,其特点是占用内存少并发能力强,并且在现实中,nginx的并发能力要比在同类型的网页服务器中表现要好

  • Nginx专为性能优化而开发,最重要的要求便是性能,且十分注重效率,有报告nginx能支持高达50000个并发连接数

正向代理和反向代理

Nginx 是一个反向代理服务器,那么反向代理是什么呢?我们先看看什么叫做正向代理

正向代理:局域网中的电脑用户想要直接访问网络是不可行的,只能通过代理服务器(Server)来访问,这种代理服务就被称为正向代理。

就好比我们俩在一块,直接对话即可,但如果我和你分隔两地,我们要想对话,必须借助一个通讯设备(如:电话)来沟通,那么这个通讯设备就是"代理服务器",这种行为称为“正向代理”

那么反向代理是什么呢?

反向代理:客户端无法感知代理,因为客户端访问网络不需要配置,只要把请求发送到反向代理服务器,由反向代理服务器去选择目标服务器获取数据,然后再返回到客户端,此时反向代理服务器和目标服务器对外就是一个服务器,暴露的是代理服务器地址,隐藏了真实服务器IP地址。

在正向代理中,我向你打电话,你能看到向你打电话的电话号码,由电话号码知道是我给你打的,那么此时我用虚拟电话给你打过去,你看到的不再是我的手机号,而是虚拟号码,你便不知道是我给你打的,这种行为变叫做"反向代理"。

在以上述的例子简单的说下:

  • 正向代理:我通过我的手机(proxy Server)去给你打电话,相当于我和我的手机是一个整体,与你的手机(Server)是分开的

  • 反向代理:我通过我的手机(proxy Server)通过软件转化为虚拟号码去给你打电话,此时相当于我的手机和你的手机是一个整体,和我是分开的

负载均衡

负载均衡:是高可用网络基础架构的关键组件,通常用于将工作负载分布到多个服务器来提高网站、应用、数据库或其他服务的性能和可靠性。

如果没有负载均衡,客户端与服务端的操作通常是:客户端请求服务端,然后服务端去数据库查询数据,将返回的数据带给客户端


但随着客户端越来越多,数据,访问量飞速增长,这种情况显然无法满足,我们从上图发现,客户端的请求和相应都是通过服务端的,那么我们加大服务端的量,让多个服务端分担,是不是就能解决这个问题了呢?

但此时对于客户端而言,他去访问这个地址就是固定的,才不会去管那个服务端有时间,你只要给我返回出数据就OK了,所以我们就需要一个“管理者“,将这些服务端找个老大过来,客户端直接找老大,再由老大分配谁处理谁的数据,从而减轻服务端的压力,而这个”老大“就是反向代理服务器,而端口号就是这些服务端的工号。


向这样,当有15个请求时,反向代理服务器会平均分配给服务端,也就是各处理5个,这个过程就称之为:负载均衡

动静分离

当客户端发起请求时,正常的情况是这样的:


就好比你去找客服,一般先是先说一大堆官方的话,你问什么,他都会这么说,那么这个就叫静态资源(可以理解为是html,css)

而回答具体的问题时,每个回答都是不同的,而这些不同的就叫做动态资源(会改变,可以理解为是变量)

在未分离的时候,可以理解为每个客服都要先说出官方的话,在打出具体的回答,这无异加大了客服的工作量,所以为了更好的有效利用客服的时间,我们把这些官方的话分离出来,找个机器人,让他代替客服去说,这样就减轻了客服的工作量。

也就是说,我们将动态资源和静态资源分离出来,交给不同的服务器去解析,这样就加快了解析的速度,从而降低由单个服务器的压力


安装 Nginx

关于 nginx 如何安装,这里就不做过多的介绍了,感兴趣的小伙伴看看这篇文章:【Linux】中如何安装nginx

这里让我们看看一些常用的命令:

  • 查看版本:./nginx -v

  • 启动:./nginx

  • 关闭:./nginx -s stop(推荐) 或 ./nginx -s quit

  • 重新加载nginx配置:./nginx -s reload

Nginx 的配置文件

配置文件分为三个模块:

  • 全局块:从配置文件开始到events块之间,主要是设置一些影响nginx服务器整体运行的配置指令。(按道理说:并发处理服务的配置时,值越大,可支持的并发处理量越多,但此时会受到硬件、软件等设备等的制约)

  • events块:影响nginx服务器与用户的网络连接,常用的设置包括是否开启对多workprocess下的网络连接进行序列化,是否允许同时接收多个网络连接等等

  • http块:如反向代理和负载均衡都在此配置

location 的匹配规则

共有四种方式:

    location[ = | ~ | ~* | ^~ ] url {
   
  }
复制代码
  • =精确匹配,用于不含正则表达式的url前,要求字符串与url严格匹配,完全相等时,才能停止向下搜索并处理请求

  • ^~:用于不含正则表达式的url前,要求ngin服务器找到表示url和字符串匹配度最高的location后,立即使用此location处理请求,而不再匹配

  • ~最佳匹配,用于表示url包含正则表达式,并且区分大小写。

  • ~*:与~一样,只是不区分大小写

注意:

  • 如果 url 包含正则表达式,则不需要~ 作为开头表示

  • nginx的匹配具有优先顺序,一旦匹配上就会立马退出,不再进行向下匹配

End

关于具体的配置可以参考:写给前端的nginx教程

致此,有关Nginx相关的知识就已经完成了,相信对于前段而言已经足够了,喜欢的点个赞👍🏻支持下吧(● ̄(エ) ̄●)


作者:小杜杜
来源:https://juejin.cn/post/7082655545491980301

收起阅读 »

我用 nodejs 爬了一万多张小姐姐壁纸

前言哈喽,大家好,我是小马,为什么要下载这么多图片呢? 前几天使用 uni-app + uniCloud 免费部署了一个壁纸小程序,那么接下来就需要一些资源,给小程序填充内容。爬取图片首先初始化项目,并且安装 axios 和 ch...
继续阅读 »

前言

哈喽,大家好,我是小马,为什么要下载这么多图片呢? 前几天使用 uni-app + uniCloud 免费部署了一个壁纸小程序,那么接下来就需要一些资源,给小程序填充内容。

爬取图片

首先初始化项目,并且安装 axios 和 cheerio

npm init -y && npm i axios cheerio

axios 用于爬取网页内容,cheerio 是服务端的 jquery api, 我们用它来获取 dom 中的图片地址;const axios = require('axios')

const cheerio = require('cheerio')

function getImageUrl(target_url, containerEelment) {
let result_list = []
const res = await axios.get(target_url)
const html = res.data
const $ = cheerio.load(html)
const result_list = []
$(containerEelment).each((element) => {
result_list.push($(element).find('img').attr('src'))
})
return result_list
}

这样就可以获取到页面中的图片 url 了。接下来需要根据 url 下载图片。

如何使用 nodejs 下载文件

方式一:使用内置模块 ‘https’ 和 ‘fs’

使用 node js 下载文件可以使用内置包或第三方库完成。

GET 方法用于 HTTPS 来获取要下载的文件。 createWriteStream() 是一个用于创建可写流的方法,它只接收一个参数,即文件保存的位置。Pipe()是从可读流中读取数据并将其写入可写流的方法。const fs = require('fs')

const https = require('https')

// URL of the image
const url = 'GFG.jpeg'

https.get(url, (res) => {
// Image will be stored at this path
const path = `${__dirname}/files/img.jpeg`
const filePath = fs.createWriteStream(path)
res.pipe(filePath)
filePath.on('finish', () => {
filePath.close()
console.log('Download Completed')
})
})

方式二:DownloadHelper
npm install node-downloader-helper

下面是从网站下载图片的代码。一个对象 dl 是由类 DownloadHelper 创建的,它接收两个参数:

  1. 将要下载的图像。
  2. 下载后必须保存图像的路径。

File 变量包含将要下载的图像的 URL,filePath 变量包含将要保存文件的路径。const { DownloaderHelper } = require('node-downloader-helper')


// URL of the image
const file = 'GFG.jpeg'
// Path at which image will be downloaded
const filePath = `${__dirname}/files`

const dl = new DownloaderHelper(file, filePath)

dl.on('end', () => console.log('Download Completed'))
dl.start()

方法三: 使用 download

是 npm 大神 sindresorhus 写的,非常好用

npm install download

下面是从网站下载图片的代码。下载函数接收文件和文件路径。const download = require('download')


// Url of the image
const file = 'GFG.jpeg'
// Path at which image will get downloaded
const filePath = `${__dirname}/files`

download(file, filePath).then(() => {
console.log('Download Completed')
})

最终代码

本来想去爬百度壁纸,但是清晰度不太够,而且还有水印等,后来, 群里有个小伙伴找到了一个 api,估计是某个手机 APP 上的高清壁纸,可以直接获得下载的 url,我就直接用了。

下面是完整代码

const download = require('download')
const axios = require('axios')

let headers = {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36',
}

function sleep(time) {
return new Promise((reslove) => setTimeout(reslove, time))
}

async function load(skip = 0) {
const data = await axios
.get(
'http://service.picasso.adesk.com/v1/vertical/category/4e4d610cdf714d2966000000/vertical',
{
headers,
params: {
limit: 30, // 每页固定返回30条
skip: skip,
first: 0,
order: 'hot',
},
}
)
.then((res) => {
return res.data.res.vertical
})
.catch((err) => {
console.log(err)
})
await downloadFile(data)
await sleep(3000)
if (skip < 1000) {
load(skip + 30)
} else {
console.log('下载完成')
}
}

async function downloadFile(data) {
for (let index = 0; index < data.length; index++) {
const item = data[index]

// Path at which image will get downloaded
const filePath = `${__dirname}/美女`

await download(item.wp, filePath, {
filename: item.id + '.jpeg',
headers,
}).then(() => {
console.log(`Download ${item.id} Completed`)
return
})
}
}

load()

上面代码中先要设置 User-Agent 并且设置 3s 延迟, 这样可以防止服务端阻止爬虫,直接返回 403。

直接 node index.js 就会自动下载图片了。

爬取运行中

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

收起阅读 »

React18正式版发布,未来发展趋势是?

2022年3月29号,React18正式版发布。从v16开始,React团队就在普及并发的概念。在v18的迭代过程中(alpha、Beta、RC),也一直在科普并发特性,所以正式版发布时,已经没有什么新鲜特性。本文主要讲解v18发布日志中透露的一些未来发展趋势...
继续阅读 »

2022年3月29号,React18正式版发布。

v16开始,React团队就在普及并发的概念。在v18的迭代过程中(alpha、Beta、RC),也一直在科普并发特性,所以正式版发布时,已经没有什么新鲜特性。

本文主要讲解v18发布日志中透露的一些未来发展趋势。

欢迎加入人类高质量前端框架研究群,带飞

开发者可能并不会接触到并发特性

React对增加API是很慎重的。从13年诞生至今,触发更新的方式都是this.setState

而引入并发概念后,光是与并发相关的API就有好几个,比如:

  • useTransition

  • useDeferredValue

甚至出现了为并发兜底的API(即并发情况下,不使用这些API可能会出bug),比如:

  • useSyncExternalStore

  • useInsertionEffect

一下多出这么多API,还不是像useState这种不使用不行的API,况且,并发这一特性对于多数前端开发者都有些陌生。

你可以代入自己的业务想想,让开发者上手使用并发特性有多难。

所以,在未来用v18开发的应用,开发者可能并不会接触到并发特性。这些特性更可能是由各种库封装好的。

比如:startTransition可以让用户在不同视图间切换的同时,不阻塞用户输入。

这一API很可能会由各种Router实现,再作为一个配置项开放给开发者。

万物皆可Suspense

对于React来说,有两类瓶颈需要解决:

  • CPU的瓶颈,如大计算量的操作导致页面卡顿

  • IO的瓶颈,如请求服务端数据时的等待时间

其中CPU的瓶颈通过并发特性的优先级中断机制解决。

IO的瓶颈则交给Suspense解决。

所以,未来一切与IO相关的操作,都会收敛到Suspense这一解决方案内。

从最初的React.lazy到如今仍在开发中的Server Components,最终万物皆可Suspense

这其中有些逻辑是很复杂的,比如:

  • Server Components

  • 新的服务端渲染方案

所以,这些操作不大可能是直接面向开发者的。

这又回到了上一条,这些操作会交由各种库实现。如果复杂度更高,则会交由基于React封装的框架实现,比如Next.jsRemix

这也是为什么React团队核心人物Sebastian会加入Next.js

可以说,React未来的定位是:一个前端底层操作系统,足够复杂,一般开发者慎用。

而开发者使用的是基于该操作系统实现的各种上层应用

总结

如果说v16之前各种React Like库还能靠体积、性能优势分走React部分蛋糕,那未来两者走的完全是两条赛道,因为两者的生态不再兼容。

未来不再会有React全家桶的概念,桶里的各个部件最终会沦为更大的框架中的一个小模块。

当前你们业务里是直接使用React呢,还是使用各种框架(比如Next.js)?

作者:魔术师卡颂
来源:https://juejin.cn/post/7080719159645962271 收起阅读 »

高并发之伪共享和缓存行填充(缓存行对齐)(@Contended)

1.使用缓存行(Cache Line)填充前后对比伪共享和缓存行填充,我们先看一个例子,让大家感受一下了解底层知识后,你的代码可以快到起飞的感jio: 在类中定义看似无用的成员属性,速度有质的提升。 如下是未使用缓存行(Cache Line)填充方法运行的结果...
继续阅读 »

1.使用缓存行(Cache Line)填充前后对比

伪共享和缓存行填充,我们先看一个例子,让大家感受一下了解底层知识后,你的代码可以快到起飞的感jio: 在类中定义看似无用的成员属性,速度有质的提升。 如下是未使用缓存行(Cache Line)填充方法运行的结果,可以看到耗时是3579毫秒:

而在其变量x的前后加上7个long类型到变量(在变量x前56Byte,后面也是56Byte,这就是缓存行填充,下面章节会详细介绍),当然这个14个变量是不会在代码中被用到的,但是为什么速度会提升将近2倍呢,如下图所示,可以看到耗时为1280毫秒:


ps:上面两个截图中的完整代码见
章节5,大家也可以直接跳转到章节去看下完整的代码。

为什么会这么神奇,这里为先提前说下结论,具体的大家可以往后看。

  • 缓存一致性是根据缓存行(Cache line)为单元来进行同步的,即缓存中的传输单元为缓存行,一个缓存行大小通常为64Byte;

  • 缓存行的内容一发生变化,就需要进行缓存同步;

  • 所以虽然用到的不是同一个数据,但是他们(数据X和数据Y)在同一个缓存行中,缓存行的内容一发生变化,就需要进行缓存同步,这个同步是需要时间的。

2.内存、缓存与寄存器之间如何传输数据

为什么会这样呢?前面我们提到过缓存一致性的问题,见笔者该篇博文:“了解高并发底层原理”,面试官:讲一下MESI(缓存一致性协议)吧,点击文字即可跳转。 其中内存、缓存与寄存器之间的关系图大致如下:


硬盘中的可执行文件加载到寄存器中进行运算的过程如下:

  1. 硬盘中的可执行文件(底层存储还是二进制的)加载到内存中,操作系统为其分配资源,变成了一个进程A,此时还没有跑起来;

  2. 过了一段时间之后,CPU0的时间片分配给了进程A,此时CPU0进行线程的装载,然后把需要用到的数据先从内存中读取到缓存中,读取的单元为一个缓存行,其大小现在通常为64字节(记住这个缓存行大小为64字节,这个非常重要,在后面会多次用到这个数值)。

  3. 然后数据再从缓存中读取到寄存器中,目前缓存一般为三级缓存,这里不具体画出。

  4. 寄存器得到了数据之后送去ALU(arithmetic and logic unit)做计算。

这里说一下为什么要设计三级缓存:

  • 电脑通过使用时钟来同步指令的执行。时钟脉冲在一个固定的频率(称为时钟频率)。当你买了一台1.5GHz的电脑,1.5GHz就是时钟频率,即每秒15亿次的时钟脉冲,一次完整的时钟脉冲称为一个周期(cycle),时钟并不记录分和秒。它以不变的速率简单跳动。

  • 其主要原因还是因为CPU方法内存消耗的时间太长了,CPU从各级缓存和内存中读取数据所需时间如下:

CPU访问大约需要的周期(cycle)大约需要的时间
寄存器1 cycle0ns
L1 Cache3—4 cycle1ns
L2 Cache10—20 cycle3ns
L3 Cache40—45 cycle15ns
内存60—90ns

3.缓存中数据共享问题(真实共享和伪共享)

3.1 真实共享(不同CPU的寄存器中都到了同一个变量X)

首先我们先说数据的真实共享,如下图,我们在CPU0和CPU1中都用到了数据X,现在不考虑数据Y。


如果不考虑缓存一致性,会出现如下问题: 在多线程情况下,此时由两个cpu同时开始读取了long X =0,然后同时执行如下语句,会出现如下情况:

int X = 0;
X++;

刚开始,X初始化为0,假设有两个线程A,B,

  1. A线程在CPU0上进行执行,从主存加载X变量的数值到缓存,然后从缓存中加载到寄存器中,在寄存器中执行X+1操作,得到X的值为1,此时得到X等于1的值还存放在CPU0的缓存中;

  2. 由于线程A计算X等于1的值还存放在缓存中,还没有刷新会内存,此时线程B执行在CPU1上,从内存中加载i的值,此时X的值还是0,然后进行X+1操作,得到X的值为1,存到CPU1的缓存中,

  3. A,B线程得到的值都是1,在一定的时间周期之后刷新回内存

  4. 写回内存后,两次X++操作之后,其值还是1;

可以看到虽然我们做了两次++X操作,但是只进行了一次加1操作,这就是缓存不一致带来的后果。

如何解决该问题:

  • 具体的我们可以通过MESI协议(详情见笔者该篇博文:blog.csdn.net/MrYushiwen/…)来保证缓存的一致性,如上图最中间的红字所示,在不同寄存器的缓存中,需要考虑数据的一致性问题,这个需要花费一定的时间来同步数据,从而达到缓存一致性的作用。

3.2伪共享(不同CPU的寄存器中用到了不同的变量,一个用到的是X,一个用到的是Y,并且XY在同一个缓存行中)

  • 缓存一致性是根据缓存行(Cache line)为单元来进行同步的,即缓存中的传输单元为缓存行,一个缓存行大小通常为64Byte;

  • 缓存行的内容一发生变化,就需要进行缓存同步;

  • 在3.1中,我们在寄存器用到的数据是同一个X,他们肯定是在同一个缓存行中的,这个是真实的共享数据的,共享的数据为X。

  • 而在3.2中,不同CPU的寄存器中用到了不同的变量,一个用到的是X,一个用到的是Y,但是变量X、Y在同一个缓存行中(一次读取64Byte,见3.1中的图),缓存一致性是根据缓存行为单元来进行同步的,所以虽然用到的不是同一个数据,但是他们(数据X和数据Y)在同一个缓存行中,他们的缓存同步也需要时间。


4.伪共享解决办法(缓存行填充或者使用@Contended注解)

4.1.缓存行填充

如章节一所示,我们可以在x变量前后进行缓存行的填充,:

public volatile long A,B,C,D,E,F,G;
public volatile long x = 1L;
public volatile long a,b,c,d,e,f,g;

添加后,3.2章节中的截图将会变成如下样子:


不论如何进行缓存行的划分,包括x在内的连续64Byte,也就是一个缓存行不可能存在变量Y,同样变量Y所在的缓存行不可能存在x,这样就不存在伪共享的情况,他们之间就不需要考虑缓存一致性问题了,也就节省了这一部分时间。

4.2.Contended注解

在Java 8中,提供了@sun.misc.Contended注解来避免伪共享,原理是在使用此注解的对象或字段的前后各增加128字节大小的padding,使用2倍于大多数硬件缓存行的大小来避免相邻扇区预取导致的伪共享冲突。我们目前的缓存行大小一般为64Byte,这里Contended注解为我们前后加上了128字节绰绰有余。 注意:如果想要@Contended注解起作用,需要在启动时添加JVM参数-XX:-RestrictContended 参数后 @sun.misc.Contended 注解才有。

然而在java11中@Contended注解被归类到模块java.base中的包jdk.internal.vm.annotation中,其中定义了Contended注解类型。笔者用的是java12,其注解如下:


加上该注解,如下,也能达到缓存行填充的效果


5.完整代码(利用缓存行填充和没用缓存行填充)

大家自己也可以跑一下如下代码,看利用缓存行填充后的神奇效果。

5.1没用缓存行填充代码如下:

package mesi;

import java.util.concurrent.CountDownLatch;

/**
* @Author: YuShiwen
* @Date: 2022/2/27 2:52 PM
* @Version: 1.0
*/

public class NoCacheLineFill {

   public volatile long x = 1L;
}

class MainDemo {

   public static void main(String[] args) throws InterruptedException {
       // CountDownLatch是在java1.5被引入的,它是通过一个计数器来实现的,计数器的初始值为线程的数量。
       // 每当一个线程完成了自己的任务后,调用countDown方法,计数器的值就会减1。
       // 当计数器值到达0时,它表示所有的线程已经完成了任务,然后调用await的线程就可以恢复执行任务了。
       CountDownLatch countDownLatch = new CountDownLatch(2);

       NoCacheLineFill[] arr = new NoCacheLineFill[2];
       arr[0] = new NoCacheLineFill();
       arr[1] = new NoCacheLineFill();

       Thread threadA = new Thread(() -> {
           for (long i = 0; i < 1_000_000_000L; i++) {
               arr[0].x = i;
          }
           countDownLatch.countDown();
      }, "ThreadA");

       Thread threadB = new Thread(() -> {
           for (long i = 0; i < 100_000_000L; i++) {
               arr[1].x = i;
          }
           countDownLatch.countDown();
      }, "ThreadB");

       final long start = System.nanoTime();
       threadA.start();
       threadB.start();
       //等待线程A、B执行完毕
       countDownLatch.await();
       final long end = System.nanoTime();
       System.out.println("耗时:" + (end - start) / 1_000_000 + "毫秒");

  }
}

5.2利用缓存行填充代码如下:

package mesi;

import java.util.concurrent.CountDownLatch;

/**
* @Author: YuShiwen
* @Date: 2022/2/27 3:45 PM
* @Version: 1.0
*/

public class UseCacheLineFill {

   public volatile long A, B, C, D, E, F, G;
   public volatile long x = 1L;
   public volatile long a, b, c, d, e, f, g;
}

class MainDemo01 {

   public static void main(String[] args) throws InterruptedException {
       // CountDownLatch是在java1.5被引入的,它是通过一个计数器来实现的,计数器的初始值为线程的数量。
       // 每当一个线程完成了自己的任务后,调用countDown方法,计数器的值就会减1。
       // 当计数器值到达0时,它表示所有的线程已经完成了任务,然后调用await的线程就可以恢复执行任务了。
       CountDownLatch countDownLatch = new CountDownLatch(2);

       UseCacheLineFill[] arr = new UseCacheLineFill[2];
       arr[0] = new UseCacheLineFill();
       arr[1] = new UseCacheLineFill();

       Thread threadA = new Thread(() -> {
           for (long i = 0; i < 1_000_000_000L; i++) {
               arr[0].x = i;
          }
           countDownLatch.countDown();
      }, "ThreadA");

       Thread threadB = new Thread(() -> {
           for (long i = 0; i < 1_000_000_000L; i++) {
               arr[1].x = i;
          }
           countDownLatch.countDown();
      }, "ThreadB");

       final long start = System.nanoTime();
       threadA.start();
       threadB.start();
       //等待线程A、B执行完毕
       countDownLatch.await();
       final long end = System.nanoTime();
       System.out.println("耗时:" + (end - start) / 1_000_000 + "毫秒");

  }
}

作者:YuShiwen
来源:https://juejin.cn/post/7083030159304949767

收起阅读 »

人人为我,我为人人——环信开发者“𠈌”计划邀你加入!

各位亲爱的环友们~环信技术社区及官方支持群自组建以来涌现了不少不分昼夜,互帮互助,无私帮助他人解决问题的热心网友,看似不经意的“顺手答一下”“刚好遇到过”,于被帮助的人都是雪中炭,暗室灯,绝渡舟的存在~~为鼓励这些默默发光的环友同时壮大帮帮团队伍,环信推出“𠈌...
继续阅读 »

各位亲爱的环友们~

环信技术社区及官方支持群自组建以来涌现了不少不分昼夜,互帮互助,无私帮助他人解决问题的热心网友,看似不经意的“顺手答一下”“刚好遇到过”,于被帮助的人都是雪中炭,暗室灯,绝渡舟的存在~~为鼓励这些默默发光的环友同时壮大帮帮团队伍,环信推出
“𠈌”计划。

“𠈌”计划以传递人人为我,我为人人的开发者互助精神为目标,将程序员自由开放和共享精神发扬光大。赠人玫瑰,手有余香,帮助他人沉淀自己的技术力量,现诚邀广大开发者积极加入!

包括但不限于在技术社区和官方支持群里解答IM集成及以外的所有开发问题。


从四月开始,每月底在本社区技术支持群(环信官方技术支持群1、2、3)分别选出3~5名当月积极帮助他人解决问题的网友(非环信员工)给予一定的福利奖励。
同时把Ta送上环信月度优秀群友墙~!颁发优秀环友徽章一枚,自本月起,累计上墙次数超过3次的环友们将拥有神秘的年度大奖~



 福利奖励标准 

IMGeek论坛:积极回复每月新发问题(集成问题,开发问题,bug解决等),帮助坛友解决问题——回帖总条数top5


技术支持群:受企业微信群统计功能限制,每月由以下各群内的环信支持小哥哥小姐姐们提名。



*以上暂为每月评选标准,评选方式后期会慢慢优化,以更客观数据为依据,贯彻公平公正的原则,坚持获选者0争议0质疑的宗旨,众望所归。
*环信员工不参与以上排名或提名。


 福利大礼包 

环信礼包:含环信定制周边、月优秀环友徽章、京东卡、其他随机盲盒




“每月优秀环友”在次月10日前揭晓并发放奖励。


如果您特别了解环信IM集成及相关问题解答,欢迎加入答疑方队。

想加入官方技术支持群的朋友,请联系环信冬冬通过审核后进群。



收起阅读 »

跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制

前言跟我学flutter系列:跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制企业...
继续阅读 »
前言
跟我学flutter系列:
跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin
跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate
跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制
企业级篇目:
跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统
跟我学企业级flutter项目:dio网络框架增加公共请求参数&header
跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层
跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview
跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget

我们在开发flutter应用的时候编写代码,要么是同步代码,要么是异步代码。那么什么是同步什么是异步呢?

  • 同步代码就是正常编写的代码块
  • 异步代码就是Future,async等关键字修饰的代码块

一、时机不同

他们区别于运行时机不同,同步代码先执行,异步代码后执行,即使你的同步代码写在最后,那也是你的同步代码执行,之后运行你的异步代码。

二、机制不同

异步代码运行在 event loop中,类似于Android里的Looper机制,是一个死循环,event loop不断的从事件队列里取事件然后运行。

event loop循环机制

如图所示,事件存放于队列中,loop循环执行 运行图 Dart的事件循环如下图所示。循环中有两个队列。一个是微任务队列(MicroTask queue),一个是事件队列(Event queue)。 在这里插入图片描述 事件队列包含外部事件,例如I/O, Timer,绘制事件等等。 微任务队列则包含有Dart内部的微任务,主要是通过scheduleMicrotask来调度。

  1. 首先处理所有微任务队列里的微任务。
  2. 处理完所有微任务以后。从事件队列里取1个事件进行处理。
  3. 回到微任务队列继续循环。

Dart要先把所有的微任务处理完,再处理一个事件,处理完之后再看看微任务队列。如此循环。

例子:

8个微任务
2个事件

Dart-->执行完8个微任务
Dart-->执行完1个事件
Dart-->查看微任务队列
Dart-->再执行完1个事件
done

异步执行

那么在Dart中如何让你的代码异步执行呢?很简单,把要异步执行的代码放在微任务队列或者事件队列里就行了。

可以调用scheduleMicrotask来让代码以微任务的方式异步执行

    scheduleMicrotask((){
print('a microtask');
});

可以调用Timer.run来让代码以Event的方式异步执行

   Timer.run((){
print('a event');
});

Future异步执行

创建一个立刻在事件队列里运行的Future:

Future(() => print('立刻在Event queue中运行的Future'));

创建一个延时1秒在事件队列里运行的Future:

Future.delayed(const Duration(seconds:1), () => print('1秒后在Event queue中运行的Future'));

创建一个在微任务队列里运行的Future:

Future.microtask(() => print('在Microtask queue里运行的Future'));

创建一个同步运行的Future:

Future.sync(() => print('同步运行的Future'));

这里要注意一下,这个同步运行指的是构造Future的时候传入的函数是同步运行的,这个Future通过then串进来的回调函数是调度到微任务队列异步执行的。

有了Future之后, 通过调用then来把回调函数串起来,这样就解决了"回调地狱"的问题。

Future(()=> print('task'))
.then((_)=> print('callback1'))
.then((_)=> print('callback2'));

在task打印完毕以后,通过then串起来的回调函数会按照链接的顺序依次执行。 如果task执行出错怎么办?你可以通过catchError来链上一个错误处理函数:

 Future(()=> throw 'we have a problem')
.then((_)=> print('callback1'))
.then((_)=> print('callback2'))
.catchError((error)=>print('$error'));

上面这个Future执行时直接抛出一个异常,这个异常会被catchError捕捉到。类似于Java中的try/catch机制的catch代码块。运行后只会执行catchError里的代码。两个then中的代码都不会被执行。

既然有了类似Java的try/catch,那么Java中的finally也应该有吧。有的,那就是whenComplete:


Future(()=> throw 'we have a problem')
.then((_)=> print('callback1'))
.then((_)=> print('callback2'))
.catchError((error)=>print('$error'))
.whenComplete(()=> print('whenComplete'));

无论这个Future是正常执行完毕还是抛出异常,whenComplete都一定会被执行。

结果执行

把如上的代码在dart中运行看看输出

 print('1');
var fu1 = Future(() => print('立刻在Event queue中运行的Future'));
Future future2 = new Future((){
print("future2 初始化任务");
});
print('2');
Future.delayed(const Duration(seconds:1), () => print('1秒后在Event queue中运行的Future'));
print('3');
var fu2 = Future.microtask(() => print('在Microtask queue里运行的Future'));
print('4');
Future.sync(() => print('同步运行的Future')).then((value) => print('then同步运行的Future'));
print('5');
fu1.then((value) => print('then 立刻在Event queue中运行的Future'));
print('6');
fu2.then((value) => print('then 在Microtask queue里运行的Future'));
print('7');
Future(()=> throw 'we have a problem')
.then((_)=> print('callback1'))
.then((_)=> print('callback2'))
.catchError((error)=>print('$error'));
print('8');
Future(()=> throw 'we have a problem')
.then((_)=> print('callback1'))
.then((_)=> print('callback2'))
.catchError((error)=>print('$error'))
.whenComplete(()=> print('whenComplete'));
print('9');
Future future4 = Future.value("立即执行").then((value){
print("future4 执行then");
}).whenComplete((){
print("future4 执行whenComplete");
});
print('10');


future2.then((_) {
print("future2 执行then");
future4.then((_){
print("future4 执行then2");
});

});

输出

I/flutter (29040): 1
I/flutter (29040): 2
I/flutter (29040): 3
I/flutter (29040): 4
I/flutter (29040): 同步运行的Future
I/flutter (29040): 5
I/flutter (29040): 6
I/flutter (29040): 7
I/flutter (29040): 8
I/flutter (29040): 9
I/flutter (29040): 10
I/flutter (29040): 在Microtask queue里运行的Future
I/flutter (29040): thenMicrotask queue里运行的Future
I/flutter (29040): then同步运行的Future
I/flutter (29040): future4 执行then
I/flutter (29040): future4 执行whenComplete
I/flutter (29040): 立刻在Event queue中运行的Future
I/flutter (29040): then 立刻在Event queue中运行的Future
I/flutter (29040): future2 初始化任务
I/flutter (29040): future2 执行then
I/flutter (29040): future4 执行then2
I/flutter (29040): we have a problem
I/flutter (29040): we have a problem
I/flutter (29040): whenComplete
I/flutter (29040): 1秒后在Event queue中运行的Future

输出说明:

  • 先输出同步代码,再输出异步代码
  • 通过then串联起的任务会在主要任务执行完立即执行
  • Future.sync是同步执行,then执行在微任务队列中
  • 通过Future.value()函数创建的任务是立即执行的
  • 如果是在whenComplete之后注册的then,那么这个then的任务将放在microtask执行

Completer

Completer允许你做某个异步事情的时候,调用c.complete(value)方法来传入最后要返回的值。最后通过c.future的返回值来得到结果,(注意:宣告完成的complete和completeError方法只能调用一次,不然会报错)。 例子:

test() async {
Completer c = new Completer();
for (var i = 0; i < 1000; i++) {
if (i == 900 && c.isCompleted == false) {
c.completeError('error in $i');
}
if (i == 800 && c.isCompleted == false) {
c.complete('complete in $i');
}
}

try {
String res = await c.future;
print(res); //得到complete传入的返回值 'complete in 800'
} catch (e) {
print(e);//捕获completeError返回的错误
}
}


收起阅读 »

跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate

前言 跟我学flutter系列:跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制企...
继续阅读 »

前言


跟我学flutter系列:
跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin
跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate
跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制
企业级篇目:
跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统
跟我学企业级flutter项目:dio网络框架增加公共请求参数&header
跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层
跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview
跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget

Dart是单线程的,Dart提供了Isolate,isolate提供了多线程的能力。但作为多线程能力的,却内存不能共享。但同样的内存不能共享,那么就不存在锁竞争问题。


举个例子来展示作用


如果一段代码执行事件很长,flutter如何开发。
基本页面代码(一段代码)


ElevatedButton(
child: Text("登录"),
onPressed: () {
执行运行代码();
}

延时代码块


String work(int value){
print("work start");
sleep(Duration(seconds:value));
print("work end");
return "work complete:$value";
}

第一种:直接执行运行代码(延时5秒)


  执行运行代码() {
work(5);
}

结果:
5秒卡的死死的


第二种:async执行运行代码(延时5秒)


  执行运行代码() async{
work(5);
}

结果:
5秒依旧卡的死死的


------------------------------------------------我是分割线--------------------------------------------------



why?在dart中,async不是异步计算么?(循环机制下篇讲)因为我们仍旧是在同一个UI线程中做运算,异步只是说我可以先运行其他的,等我这边有结果再返回,但是,我们的计算仍旧是在这个UI线程,仍会阻塞UI的刷新,异步只是在同一个线程的并发操作。



第三种:ioslate执行运行代码(延时5秒)



但是由于dart中的Isolate比较重量级,UI线程和Isolate中的数据的传输比较复杂,因此flutter为了简化用户代码,在foundation库中封装了一个轻量级compute操作。



  执行运行代码() async{
var result = await compute(work, 5);
print(result);
}

结果:
居然不卡顿了


使用说明



compute的使用还是有些限制,它没有办法多次返回结果,也没有办法持续性的传值计算,每次调用,相当于新建一个隔离,如果调用过多的话反而会适得其反。我们需要根据不同的业务选择用compute和isolate




Future work(int value) async{
//接收消息管道
ReceivePort rp = new ReceivePort();
//发送消息管道
SendPort port = rp.sendPort;
Isolate isolate = await Isolate.spawn(workEvent, port);
//发送消息管道2
final sendPort2 = await rp.first;
//返回应答数据
final answer = ReceivePort();
sendPort2.send([answer.sendPort, value]);
return answer.first;
}

void workEvent(SendPort port) {
//接收消息管道2
final rPort = ReceivePort();
SendPort port2 = rPort.sendPort;
// 将新isolate中创建的SendPort发送到主isolate中用于通信
port.send(port2);

rPort.listen((message) {
final send = message[0] as SendPort;
send.send(work(5));
});
}

基本方法


    //恢复 isolate 的使用
isolate.resume(isolate.pauseCapability);

//暂停 isolate 的使用
isolate.pause(isolate.pauseCapability);

//结束 isolate 的使用
isolate.kill(priority: Isolate.immediate);

//赋值为空 便于内存及时回收
isolate = null;


两个进程都双向绑定了消息通信的通道,即使新的Isolate中的任务完成了,它的进程也不会立刻退出,因此,当使用完自己创建的Isolate后,最好调用isolate.kill(priority: Isolate.immediate);将Isolate立即杀死。



用Future还是isolate?


future使用场景:



  • 代码段可以独立运行而不会影响应用程序的流畅性


isolate使用场景:



  • 繁重的处理可能要花一些时间才能完成

  • 网络加载大图

  • 图片处理
收起阅读 »

跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin

前言跟我学flutter系列:跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制企业...
继续阅读 »

前言

跟我学flutter系列:
跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin
跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate
跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制
企业级篇目:
跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统
跟我学企业级flutter项目:dio网络框架增加公共请求参数&header
跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层
跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview
跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget

与java&kotlin不同的是,dart中有一个特殊的关键字mixin(mix-in),用这个关键字的类被其他类(包含)的时候,其他类就拥有了该类的方法。这样代码不通过继承(extend)就可以重用。


场景来展示mixin如何使用


由于在java&kotlin中经常性的用extent & implements 并不知道mixin是如何使用,那么我举几个特殊的例子来帮助大家理解


场景用例


在这里插入图片描述
如上uml图所示
鸟作为父类,鸟必备的技能为(下蛋和走路),而作为其子类的大雁和麻雀可以飞行,企鹅却不能飞行。
那么飞行却成为个别鸟类的技能,如果在父类中定义实现飞,那在企鹅中就多了个空实现。如果定义一个接口实现飞,那么在能飞的鸟类中就必须都要重新编写飞的代码。如何让这一切变得容易呢。
那么我们用混入(with)来实现如下代码:


abstract class Bird{

void walk() { print('我会走路'); }
void xiadan() { print('我会下蛋'); }
}

abstract class Fly{
void fly() { print('我会飞'); }
}

//大雁

class Dayan extends Bird with Fly {}

//企鹅

class Qier extends Bird {}

如果 Fly 类 不希望作为常规类被使用,使用关键字 mixin 替换 class 。


mixin Fly{
void fly() { print('我会飞'); }
}

如果 Fly 类 只希望限定于鸟类去使用,那么需要加入如下关键字


mixin Fly on Bird{
void fly() { print('我会飞'); }
}

mixin特点



  1. mixin 没有构造函数,不能被实例化

  2. 可以当做接口使用,class 混入之后需要实现

  3. 可以使用on 指定混入的类类型,如果不是报错。

  4. 如果with后的多个类中有相同的方法,如果当前使用类重写了该方法,就会调用当前类中的方法。如果当前使用类没有重写了该方法,则会调用距离with关键字最远类中的方法。


调用顺序展示


简单顺序调用


如果with后的多个类中有相同的方法,如果当前使用类重写了该方法,就会调用当前类中的方法。如果当前使用类没有重写了该方法,则会调用距离with关键字最远类中的方法。


abstract class First {
void doPrint() {
print('First');
}
}

abstract class Second {
void doPrint() {
print('Second');
}
}

class Father {
void doPrint() {
print('Father');
}
}

class Son extends Father with First,Second {

}

调用:


	Son son = Son();
son.doPrint();

打印:


Second

重写后调用


class Son extends Father with First,Second {
void doPrint() {
print('Son');
}
}

调用:


	Son son = Son();
son.doPrint();

打印:


Son

带有父类方法调用


class Father {
void init() {
print('Father init');
}
}
mixin FirstMixin on Father {
void init() {
print('FirstMixin init start');
super.init();
print('FirstMixin init end');
}
}

mixin SecondMixin on Father {
void init() {
print('SecondMixin init start');
super.init();
print('SecondMixin init end');
}
}


class Son extends Father with FirstMixin, SecondMixin {

@override
void init() {
print('Son init start');
super.init();
print('Son init end');
}
}

调用:


  Son().init();

打印:


flutter: Son init start
flutter: SecondMixin init start
flutter: FirstMixin init start
flutter: Father init
flutter: FirstMixin init end
flutter: SecondMixin init end
flutter: Son init end

说明






















方式类型说明
withmixin混入该类内容
with onmixin混入该类内容,但必须是特点的类型

特别注意


mixin 可以on多个类,但with时候之前的类必须已经有相关的实现


mixin Mix on Mix1,Mix2{ }
收起阅读 »

Java好用的时间类,别在用Date了

前言假设你想获取当前时间,那么你肯定看过这样的代码public static void main(String[] args) { Date date = new Date(System.currentTimeMillis()); Syste...
继续阅读 »

前言

假设你想获取当前时间,那么你肯定看过这样的代码

public static void main(String[] args) {

Date date = new Date(System.currentTimeMillis());
System.out.println(date.getYear());
System.out.println(date.getMonth());
System.out.println(date.getDate());
}

获取年份,获取月份,获取..日期?
运行一下

121
9
27

怎么回事?获取年份,日期怎么都不对,点开源码发现

/**
* Returns a value that is the result of subtracting 1900 from the
* year that contains or begins with the instant in time represented
* by this <code>Date</code> object, as interpreted in the local
* time zone.
*
* @return the year represented by this date, minus 1900.
* @see java.util.Calendar
* @deprecated As of JDK version 1.1,
* replaced by <code>Calendar.get(Calendar.YEAR) - 1900</code>.
*/
@Deprecated
public int getYear() {
return normalize().getYear() - 1900;
}

原来是某个对象值 减去了 1900,注释也表示,返回值减去了1900,难道我们每次获取年份需要在 加上1900?注释也说明了让我们 用Calendar.get()替换,并且该方法已经被废弃了。点开getMonth()也是一样,返回了一个0到11的值。getDate()获取日期?不应该是getDay()吗?老外的day都是sunday、monday,getDate()才是获取日期。再注意到这些api都是在1.1的时候被废弃了,私以为是为了消除getYear减去1900等这些歧义。收~

Calendar 日历类

public static void main(String[] args) {

Calendar calendar = Calendar.getInstance();
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH);
int dom = calendar.get(Calendar.DAY_OF_MONTH);
int doy = calendar.get(Calendar.DAY_OF_YEAR);
int dow = calendar.get(Calendar.DAY_OF_WEEK);
int dowim = calendar.get(Calendar.DAY_OF_WEEK_IN_MONTH);
System.out.println(year+"年"+ month+"月");
System.out.println(dom+"日");
System.out.println(doy+"日");
System.out.println(dow+"日");
System.out.println(dowim);
}

打印(运行时间2021年10月27日 星期三 晴)

2021年9月
27日
300日
4日
4

问:月份怎么是上个月的?
答:是为了计算方便,月是0到11之间的值。
问:计算方便?
答:比如月份从1月开始,增加一个月,12月+1=13,没有13月。假设取余,(12+1)=1 正好为1月,那11月增加一个月,(11+1)=0,这就有问题了。所以为了计算方便1月,返回了0值。date.getMonth()也是一个道理。 问:那下面的DAY_OF_XXX 又是什么意思?
答:猜!根据结果猜。
Calendar.DAY_OF_MONTH 在这个月 的这一天
Calendar.DAY_OF_YEAR 在这一年 的这一天
Calendar.DAY_OF_WEEK 在这一周 的这一天
Calendar.DAY_OF_WEEK_IN_MONTH 在这一个月 这一天在 第几周
到这里 Calendar.DAY_OF_WEEK 为什么是 4 ,你肯定也猜到了
Calendar.HOUR
Calendar.HOUR_OF_DAY
Calendar.SECOND
...其他的 你肯定也会用了

LocalDate 本地日期类

LocalDate localDate = LocalDate.now();
System.out.println("当前日期:"+localDate.getYear()+" 年 "+localDate.getMonthValue()+" 月 "+localDate.getDayOfMonth()+"日" );

//结果
当前日期:2021 年 10 月 27日

也可以通过 LocalDate.of(年,月,日)去构造

LocalDate pluslocalDate = localDate.plusDays(1);//增加一天
LocalDate pluslocalDate = localDate.plusYears(1);//增加一年

其他api

LocalDate.isBefore(LocalDate);
LocalDate.isAfter();
LocalDate.isEqual();

也就是对两个日期的判断,是在前、在后、或者相等。

LocalTime 本地时间类

LocalTime localTime = LocalTime.now();
System.out.println("当前时间:"+localTime.getHour()+"h "+localTime.getSecond()+"m "+localTime.getMinute()+"s" );

LocalDate和LocalTime 都有类似作用的api
LocalDate.plusDays(1) 增加一天
LocalTime.plusHours(1) 增加一小时 等等~
其他api

LocalTime.isBefore(LocalTime);
LocalTime.isAfter();

对两个时间的判断。肯定碰到过一个需求,今天离活动开始时间还剩多少天。

LocalDateTime 本地日期时间类

public final class LocalDateTime ...{

private final LocalDate date;

private final LocalTime time;
}

LocalDateTime = LocalDate + LocalTime 懂的都懂

Instant 类

Instant 是瞬间,某一时刻的意思

Instant.ofEpochMilli(System.currentTimeMillis())
Instant.now()

通过Instant可以创建一个 “瞬间” 对象,ofEpochMilli()可以接受某一个“瞬间”,比如当前时间,或者是过去、将来的一个时间。
比如,通过一个“瞬间”创建一个LocalDateTime对象

LocalDateTime now = LocalDateTime.ofInstant(
Instant.ofEpochMilli(System.currentTimeMillis()),ZoneId.systemDefault());

System.out.println("当前日期:"+now.getYear()+" 年 "+now.getMonthValue()+" 月 "+now.getDayOfMonth()+"日" )

Period 类

Period 是 时期,一段时间 的意思
Period有个between方法专门比较两个 日期 的

LocalDate startDate = LocalDateTime.ofInstant(
Instant.ofEpochMilli(1601175465000L), ZoneId.systemDefault()).toLocalDate();//1601175465000是2020-9-27 10:57:45
Period p = Period.between(startDate, LocalDate.now());

System.out.println("目标日期距离今天的时间差:"+p.getYears()+" 年 "+p.getMonths()+" 个月 "+p.getDays()+" 天" );

//目标日期距离今天的时间差:1 年 1 个月 1 天

看一眼源码

public static Period between(LocalDate startDateInclusive, LocalDate endDateExclusive) {
return startDateInclusive.until(endDateExclusive);
}

public Period until(ChronoLocalDate endDateExclusive) {
LocalDate end = LocalDate.from(endDateExclusive);
long totalMonths = end.getProlepticMonth() - this.getProlepticMonth(); // safe
int days = end.day - this.day;
if (totalMonths > 0 && days < 0) {
totalMonths--;
LocalDate calcDate = this.plusMonths(totalMonths);
days = (int) (end.toEpochDay() - calcDate.toEpochDay()); // safe
} else if (totalMonths < 0 && days > 0) {
totalMonths++;
days -= end.lengthOfMonth();
}
long years = totalMonths / 12; // safe
int months = (int) (totalMonths % 12); // safe
return Period.of(Math.toIntExact(years), months, days);
}

他只接受两个LocalDate对象,对时间的计算,算好之后返回Period对象

Duration 类

Duration 是 期间 持续时间 的意思 上代码

LocalDateTime end = LocalDateTime.ofInstant(Instant.ofEpochMilli(System.currentTimeMillis()), ZoneId.systemDefault());
LocalDateTime start = LocalDateTime.ofInstant(Instant.ofEpochMilli(1601175465000L), ZoneId.systemDefault());
Duration duration = Duration.between(start, end);

System.out.println("开始时间到结束时间,持续了"+duration.toDays()+"天");
System.out.println("开始时间到结束时间,持续了"+duration.toHours()+"小时");
System.out.println("开始时间到结束时间,持续了"+duration.toMillis()/1000+"秒");

可以看到between也接受两个参数,LocalDateTime对象,源码是对两个时间的计算,并返回对象。

对象转换

再贴点api

//long -> LocalDateTime
LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault())

//String -> LocalDateTime
DateTimeFormatter dateTimeFormatter1 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime.parse("2021-10-28 00:00:00", dateTimeFormatter1);

//LocalDateTime -> long
LocalDateTime对象.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();

//LocalDateTime -> String
DateTimeFormatter dateTimeFormatter1 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime对象.format(dateTimeFormatter1)

对象转换几乎都涵盖了,里面有个时区对象,这个一般用默认时区。

总结

用LocalDate、LocalTime、LocalDateTime代替了Date类。Date管日期,Time管时间
LocalDateTime = LocalDate + LocalTime
Period 只能用LocalDate
Duration 持续时间,所以LocalDate、LocalTime、LocalDateTime 都能处理
至于Calendar 日历类,这里面的api,都是针对日历的,比如这个月的第一天是星期几。
总体来说,都是api的使用,非常清晰,废弃date.getMonth()等,使用localDate.getMonthValue()来获取几月,更易理解,更易贴合使用。代码都贴在了github上了


作者:回眸婉约
链接:https://juejin.cn/post/7024389549652443172
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

飞机上一般是什么操作系统?安全不 ?

首先,请大家为“3·21”东航MU5735坠机事故,默哀1分钟,再接着看本文 !来正文 。。。科普文 !航空软件其实并不神秘,从计算机架构上来说,同普通的计算机系统并无太大区别,都是由处理器、总线、I/O设备、存储设备、网络设备、通讯设备、操作系统和应用软件所...
继续阅读 »

首先,请大家为“3·21”东航MU5735坠机事故,默哀1分钟,再接着看本文 !


来正文 。。。科普文 !

航空软件其实并不神秘,从计算机架构上来说,同普通的计算机系统并无太大区别,都是由处理器、总线、I/O设备、存储设备、网络设备、通讯设备、操作系统和应用软件所构成的。仅仅是为了满足很高指标的可靠性、健壮性和实时性,而采用了另一套东西而已。

1、波音-787、AH-64用的操作系统是VxWorks

VxWorks官网http://www.windriver.com/products/vxworks/

2、B-2、F-16、F-22、F-35、空客-380使用的操作系统均是Integrity-178B


Integrity-178B官网https://www.ghs.com/products/safety_critical/integrity-do-178b.html

类似波音-787、空客-380、空客-350内部设备之间是使用以太网的一种变体来互联的,叫AFDX,在应用软件这一层,同普通的以太网程序没有任何区别。扩展:10个关键词,了解MU5735搜寻最新进展

3、过去这些设备经常使用ADA语言来编写,现在为了降低成本,在F-35项目上已经改为使用C++了


F-35项目的C++编程规范http://www.stroustrup.com/JSF-AV-rules.pdf

F-35的微处理器是PowerPC指令集的,为了保证可靠性,采用的编译器也是同普通的编译器不太一样。编译器也是有可能出现bug的,为了保障源代码同编译出来的目标代码完全一致,避免编译器的bug造成问题,在JSF项目内部的软件开发中,经常使用CompCert编译器。这个编译器只能编译C99,但是可靠性极高。扩展:远程控制系统

要知道,近几年全球范围内飞机失事发生的次数不少。据不完全统计,每年全球大约有4000万次的飞机起落,而我国的飞机失事率一直处于非常低的水平。此前中国已经连续12年没有发生过重大民航事故了,而上一次坠机事故还是发生在2010年8月24日,河南航空的伊春空难,当时坠毁的机型为ERJ-190。另外,搜索公众号Java架构师技术后台回复“Spring”,获取一份惊喜礼包。

截至目前,东航坠机已经过去24小时了。总体来说,无论大家讨论什么因素导致的,都不具有肯定性的说法,包括为什么急速骤降,最后垂直坠落,飞机本身有没有问题,是不是操作系统出了故障,有没有遭遇极端天气影响等等,这一切都是属于未知数。

任何空难发生都是悲剧的,事故真实原因还需要等待官方调查结论、依靠黑匣子等来解开谜团。


参考来源:

1. VxWorks官方网站

http://www.windriver.com/products/vxworks/

2. Integrity-178B的官方网站

https://www.ghs.com/products/safety_critical/integrity-do-178b.html

3. 《F-35项目的C++编程规范》PDF

http://www.stroustrup.com/JSF-AV-rules.pdf

来源:科技曼

收起阅读 »

Flutter 与原生通信的三种方式

Flutter 与原生之间的通信依赖灵活的消息传递方式 应用的Flutter部分通过平台通道(platform channel)将消息发送到其应用程序的所在的宿主(iOS或Android)应用(原生应用) 宿主监听平台通道,并接收该消息。然后它会调用该...
继续阅读 »

Flutter 与原生之间的通信依赖灵活的消息传递方式




  • 应用的Flutter部分通过平台通道(platform channel)将消息发送到其应用程序的所在的宿主(iOS或Android)应用(原生应用)




  • 宿主监听平台通道,并接收该消息。然后它会调用该平台的 API,并将响应发送回客户端,即应用程序的 Flutter 部分




Flutter 与原生存在三种交互方式




  • MethodChannel:用于传递方法调用(method invocation)通常用来调用 native 中某个方法




  • BasicMessageChannel:用于传递字符串和半结构化的信息,这个用的比较少




  • EventChannel:用于数据流(event streams)的通信。有监听功能,比如电量变化之后直接推送数据给flutter端




三种 Channel 之间互相独立,各有用途,但它们在设计上却非常相近。每种 Channel 均有三个重要成员变量:




  • name: String类型,代表 Channel 的名字,也是其唯一标识符




  • messager:BinaryMessenger 类型,代表消息信使,是消息的发送与接收的工具




  • codec: MessageCodec 类型或 MethodCodec 类型,代表消息的编解码器




具体使用



  • 首先分别创建 Native 工程和 Flutter Module。我这里是以 iOS 端和 Flutter 通信为例,创建完 iOS 工程后,需要通过 CocoaPods 管理 Flutter Module。


截屏2021-11-27 下午3.09.28.png



  • 然后在 iOS 工程里面创建 Podfile ,然后引入 Flutter Module ,具体代码如下:


platform :ios,'11.0'
inhibit_all_warnings!

#flutter module 文件路径
flutter_application_path = '../flutter_module'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

target 'Native_iOS' do

install_all_flutter_pods(flutter_application_path)

end

注意: flutter_application_path 这个是 Flutter 工程的路径,我是原生项目和 Flutter在一个目录下



  • 最后在终端 pod install 一下,看是否能正常引入 Flutter Module。这样就可以在iOS工程里面导入#import <Flutter/Flutter.h>

一、MethodChannel的使用


这里写的代码实现了以下功能


1.实现了点击原生页面的按钮跳转到 Flutter 页面,在 Flutter 点击返回按钮能正常返回原生页面


2.实现在Flutter页面点击当前电量,从原生界面传值到 Flutter 页面


原生端代码


@property (nonatomic, strong)FlutterEngine *flutterEngine;

@property (nonatomic, strong)FlutterViewController *flutterVC;

@property (nonatomic, strong)FlutterMethodChannel *methodChannel;

- (void)viewDidLoad {
    [super viewDidLoad];

   //隐藏了原生的导航栏
    self.navigationController.navigationBarHidden = YES;

    UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 80, 80)];
    btn.backgroundColor = [UIColor redColor];
    [btn addTarget:self action: @selector(onBtnClick) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:btn];

    self.flutterVC = [[FlutterViewController alloc] initWithEngine:self.flutterEngine nibName:nil bundle:nil];
//创建channel
    self.methodChannel = [FlutterMethodChannel methodChannelWithName:@"methodChannel" binaryMessenger:self.flutterVC.binaryMessenger];

}

- (void)onBtnClick {

    //告诉Flutter对应的页面
//Method--方法名称,arguments--参数
    [self.methodChannel invokeMethod:@"EnterFlutter" arguments:@""];

//push进入Flutter页面
    [self.navigationController pushViewController:self.flutterVC animated:YES];

    __weak __typeof(self) weakSelf = self;
//监听Flutter发来的事件
    [self.methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult  _Nonnull result) {
//响应从Flutter页面发送来的方法
        if ([call.method isEqualToString:@"exit"]) {
            [weakSelf.flutterVC.navigationController popViewControllerAnimated:YES];
        } else if ([call.method isEqualToString:@"getBatteryLevel"]) {
//传值回Flutter页面
            [weakSelf.methodChannel invokeMethod:@"BatteryLevel" arguments:@"60%"];
        }
    }];
}

//创建引擎,真正在项目中,引擎可以定义为一个单例。这样处理防止在原生里面存在多引擎,是非常占有内存的
- (FlutterEngine *)flutterEngine {
    if (!_flutterEngine) {
        FlutterEngine * engine = [[FlutterEngine alloc] initWithName:@"flutterEngin"];
        if (engine.run) {
            _flutterEngine = engine;
        }
    }
    return _flutterEngine;
}

Flutter 端代码


class _MyHomePageState extends State<MyHomePage> {

String batteryLevel = '0%';
//定义通道
final MethodChannel _methodhannel =
const MethodChannel('com.pages.your/native_get');

@override
void initState() {
super.initState();

//Flutter端监听发送过来的数据
_methodhannel.setMethodCallHandler((call) {
if (call.method == 'EnterFlutter') {
print(call.arguments);
} else if (call.method == 'BatteryLevel') {
batteryLevel = call.arguments;
}
setState(() {});
return Future(() {});
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
children: [
ElevatedButton(
onPressed: () {
//发送消息给原生
_methodhannel.invokeListMethod('exit');
},
child: Text('返回'),
),
ElevatedButton(
onPressed: () {
//发送消息给原生
_oneChannel.invokeListMethod('getBatteryLevel');
},
child: Text('当前电量${batteryLevel}'),
),
],
),
),
);
}
}

二、BasicMessageChannel的使用


它是可以双端通信的,Flutter 端可以给 iOS 发送消息,iOS 也可以给 Flutter 发送消息。这段代码实现了在 Flutter 中的 TextField 输入文字,在 iOS 端能及时输出。


原生端代码


需要在上面代码的基础上增加 MessageChannel ,并接收消息和发送消息


@property (nonatomic, strong) FlutterBasicMessageChannel *messageChannel;

self.messageChannel = [FlutterBasicMessageChannel messageChannelWithName:@"messgaeChannel" binaryMessenger:self.flutterVC.binaryMessenger];

[self.messageChannel setMessageHandler:^(id _Nullable message, FlutterReply  _Nonnull callback) {

        NSLog(@"收到Flutter的:%@",message);
    }];

Flutter 端代码


//需要创建和iOS端相同名称的通道
final messageChannel =
const BasicMessageChannel("messgaeChannel", StandardMessageCodec());

监听消息


messageChannel.setMessageHandler((message) {
print('收到来自iOS的$message');
return Future(() {});
});

发送消息


messageChannel.send(str);

三、EventChannel的使用


只能是原生发送消息给 Flutter 端,例如监听手机电量变化,网络变化,传感器等。


我这里在原生端实现了一个定时器,每隔一秒发送一个消息给 Flutter 端,模仿这个功能。


原生端代码


记得所在的类要实现这个协议 FlutterStreamHandler


//定义属性
//通道
@property (nonatomic, strong) FlutterEventChannel *eventChannel;
//事件回调
@property (nonatomic, copy) FlutterEventSink events;
//用于计数
@property (nonatomic, assign) NSInteger count;

//初始化通道
self.eventChannel = [FlutterEventChannel eventChannelWithName:@"eventChannel" binaryMessenger:self.flutterVC.binaryMessenger];

[self.eventChannel setStreamHandler:self];

//调用创建定时器
[self createTimer];

//创建定时器
- (void)createTimer {

    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector: @selector(timeStart) userInfo:nil repeats:YES];
}

//发送消息
- (void)timeStart{

    self.count += 1;
    NSDictionary *dic = [NSDictionary dictionaryWithObject:@(self.count) forKey:@"count"];
    if (self.events != nil) {
        self.events(dic);
    }
}

//代表通道已经建好,原生端可以发送数据了
- (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments eventSink:(FlutterEventSink)eventSink {

    self.events = eventSink;
    return nil;
}

//代表Flutter端不再接收
- (FlutterError* _Nullable)onCancelWithArguments:(id _Nullable)arguments {

    self.events = nil;
    return nil;
}

Flutter 端代码


//创建通道
final EventChannel eventChannel = const EventChannel('eventChannel');

//开始监听数据
eventChannel.receiveBroadcastStream().listen((event) {
print(event.toString());
});

以上就是iOS原生和Flutter通信的三种方式,消息传递是异步的,这确保了用户界面在消息传递时不会被挂起。


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

在外企工作真的爽吗?

从参加工作至今,我觉得,我做过最幸运的事情包括下面三个:在IT圈混的人都知道,做程序员,在一家公司能干够5年,绝对算的上老员工。如果还不跳槽,很多人会觉得你要么缺乏实力,要么没有奋斗精神。因此,在我加入外企的第5个年头,我也动了换工作的心思。而在这之后,照顾家...
继续阅读 »
最近互联网行业裁员的消息,把大家搞的忧心忡忡。对于在换工作或找工作的小伙伴,犹豫是否要到互联网公司了。剩下就是外企、国企和传统行业了,最近外企因为work life balance的口碑比较好,很多小伙伴都挺憧憬的,那么外企到底怎么样呢?下面给大家精选了一下网友在外企的体验文章,给大家了解一下过来人的体会。
我是一名程序员,从30岁加入外企,至今已经11年。
从参加工作至今,我觉得,我做过最幸运的事情包括下面三个:
  1. 娶了现在的老婆。
  2. 房价暴涨之前买了房。
  3. 30岁之后进入外企。
在IT圈混的人都知道,做程序员,在一家公司能干够5年,绝对算的上老员工。如果还不跳槽,很多人会觉得你要么缺乏实力,要么没有奋斗精神。因此,在我加入外企的第5个年头,我也动了换工作的心思。
面试一圈下来,也收到了几个不错的offer。于是也下定了离开的念头,连离职报告都写好了,就差推开领导的门递上去了。而这时,家里突然有人生病,需要我照顾,也打断了我换工作的计划。
而在这之后,照顾家人的这段时间。它彻底改变我对工作,对人生的看法。
因为家人生病的急,没有在第一时间来的及请假。事情处理后,才在短信里给领导请了假,但心里是忐忑的。因为,觉得领导可能会为难,毕竟公司也不是他家开的。但没想到他很快就回复了我:
“照顾好家人,工作的事你就不要担心了。另外,注意休息,保重身体!”
当时读了,我一个大男人,心里暖暖的,鼻子甚至有点酸楚。
通过这件事,使我真切的感受到这是一个充满温情的公司。这种温情,在人生的特殊时刻,至关重要。
如果我在一家制度严苛,领导不近人情的公司。家人生病的这段时间,估计我很可能要被迫离职了。这样,在我人生最灰暗,经济压力最大的时间点上。我反而丢了工作,没了收入。也许,生活一下子就把我彻底击倒了。
而从那之后的5年。随着时间的推移,我身体的变化,我愈加的觉出,我当时没离开这家公司是多么正确的一个决定。
人生特别荒谬的一点是,当你30岁精力旺盛的时候。你既无法想象,也无法理解一个40岁油腻中年男,对家庭和健康的感受。于是,你一边鄙视着别人稳定的生活,一边高呼着奋斗的口号要去外边闯荡,去赢取别人眼中的财务自由。
但眨眼间,等你真到40岁,期待的财务自由并没有到来。但精力已大不如前,健康问题此起彼伏的时候,就会刻骨的体会到什么叫做人生的无力感。但是,你却已经没了退路。
人到40,当你再也加不动班,却也找不到一个温情公司收留的时候,你怎么办?这个时候,你终于认识到了公务员、国企、外企的好处,但你却没有了机会。
于是,在人生最艰难的节骨点,你被吊在了哪里,没有了出路。
但这能怪谁呢?人总不能在所有的时候,把所有的好处都占了吧,这并不公平!
今年我41岁,按部就班的在公司上班, 也常被人嘲笑没出息,缺乏“奋斗”的激情。但这世界上,是谁规定“奋斗”的人生就更高尚呢?
对我来说,理想生活就是现在这样:
  1. 没有996。
  2. 一年20天年假。
  3. 家庭和生活完美平衡。
  4. 工资虽然没那么高,但也够有尊严的生活。
  5. 同事之间和和气气,互相尊重。
  6. ...
至于好不好,不同的年龄,不同的经历,自会有不同的感受。
“如人饮水,冷暖自知”。
作者:铅刀一割、沈世钧
来源:https://www.zhihu.com/question/299766610
该死的知乎,不知道为啥非要给我推荐这个帖子“在外企工作爽吗?”,下面这个回答看起来更刺激。

爽。
上周刚刚从前东家离职。
离职原因不是我跳槽,而是集团我们的业务条线退出中国,被迫离职。
这是我进的第一家外企,也坚定了我之后(大概率)只在外企找活的信念。
首先上一个我的工作台位镇楼。
200多平的办公室,只有5个员工。

我司隶属一家欧洲集团,我所在的子公司从事非常小众的环境权益交易。
这在欧美已经是成熟产业,但在国内还处于起步阶段。
加之持有这些资产的多为能源国企(涉及国有资产流失等口径问题),所以总部也是保持“试水”的态度设立了我司。
公司地处使馆区,与集团另外一个规模较大的兄弟公司在同一层。
楼下一众网红咖啡馆、餐厅,交通十分便利。
每天上下班不打卡,大家基本是10点左右陆续到,6点左右陆续走。
但你有事,三点走也没人管你。
进公司就是15天年假(社会工龄十年以上则是20天),每多呆一年,增加一天年假,封顶25天。
如果生病了,和老板说一下就好,所以我从来没有关心过自己有几天病假……
我有个同事妈妈得了癌症,有一阵子天天3点左右就走了,大家也都十分理解(当然他不久之后陪家里人出国治病了,也就直接离职了)。
另外一个同事自己生病,整整一年时间没有来上班(名义上是在家办公,其实她的活都是亚太同事分摊了),领着全额工资,住的单人病房也是公司的商业保险全包的。
薪水一般。
和我bat的同学比……人活着何必给自己找不痛快?
但站在性价比角度来看还行。
来源:https://mp.weixin.qq.com/s/fAW1V9ZJNBHWX0M4vONC 4A
收起阅读 »

优秀的后端应该有哪些开发习惯?

前言毕业快三年了,前后也待过几家公司,碰到各种各样的同事。见识过各种各样的代码,优秀的、垃圾的、不堪入目的、看了想跑路的等等,所以这篇文章记录一下一个优秀的后端 Java 开发应该有哪些好的开发习惯。拆分合理的目录结构受传统的 MVC 模式影响,传统做法大多是...
继续阅读 »

前言

毕业快三年了,前后也待过几家公司,碰到各种各样的同事。见识过各种各样的代码,优秀的、垃圾的、不堪入目的、看了想跑路的等等,所以这篇文章记录一下一个优秀的后端 Java 开发应该有哪些好的开发习惯。

拆分合理的目录结构

受传统的 MVC 模式影响,传统做法大多是几个固定的文件夹 controller、service、mapper、entity,然后无限制添加,到最后你就会发现一个 service 文件夹下面有几十上百个 Service 类,根本没法分清业务模块。正确的做法是在写 service 上层新建一个 modules 文件夹,在 moudles 文件夹下根据不同业务建立不同的包,在这些包下面写具体的 service、controller、entity、enums 包或者继续拆分。



等以后开发版本迭代,如果某个包可以继续拆领域就继续往下拆,可以很清楚的一览项目业务模块。后续拆微服务也简单。

封装方法形参

当你的方法形参过多时请封装一个对象出来...... 下面是一个反面教材,谁特么教你这样写代码的!

public void updateCustomerDeviceAndInstallInfo(long customerId, String channelKey,
                  String androidId, String imei, String gaId,
                  String gcmPushToken, String instanceId) {}

写个对象出来

public class CustomerDeviceRequest {
  private Long customerId;
  //省略属性......
}

为什么要这么写?比如你这方法是用来查询的,万一以后加个查询条件是不是要修改方法?每次加每次都要改方法参数列表。封装个对象,以后无论加多少查询条件都只需要在对象里面加字段就行。而且关键是看起来代码也很舒服啊!

封装业务逻辑

如果你看过“屎山”你就会有深刻的感触,这特么一个方法能写几千行代码,还无任何规则可言......往往负责的人会说,这个业务太复杂,没有办法改善,实际上这都是懒的借口。不管业务再复杂,我们都能够用合理的设计、封装去提升代码可读性。下面贴两段高级开发(假装自己是高级开发)写的代码

@Transactional
public ChildOrder submit(Long orderId, OrderSubmitRequest.Shop shop) {
  ChildOrder childOrder = this.generateOrder(shop);
  childOrder.setOrderId(orderId);
  //订单来源 APP/微信小程序
  childOrder.setSource(userService.getOrderSource());
  // 校验优惠券
  orderAdjustmentService.validate(shop.getOrderAdjustments());
  // 订单商品
  orderProductService.add(childOrder, shop);
  // 订单附件
  orderAnnexService.add(childOrder.getId(), shop.getOrderAnnexes());
  // 处理订单地址信息
  processAddress(childOrder, shop);
  // 最后插入订单
  childOrderMapper.insert(childOrder);
  this.updateSkuInventory(shop, childOrder);
  // 发送订单创建事件
  applicationEventPublisher.publishEvent(new ChildOrderCreatedEvent(this, shop, childOrder));
  return childOrder;
}
@Transactional
public void clearBills(Long customerId) {
  // 获取清算需要的账单、deposit等信息
  ClearContext context = getClearContext(customerId);
  // 校验金额合法
  checkAmount(context);
  // 判断是否可用优惠券,返回可抵扣金额
  CouponDeductibleResponse deductibleResponse = couponDeducted(context);
  // 清算所有账单
  DepositClearResponse response = clearBills(context);
  // 更新 l_pay_deposit
  lPayDepositService.clear(context.getDeposit(), response);
  // 发送还款对账消息
  repaymentService.sendVerifyBillMessage(customerId, context.getDeposit(), EventName.DEPOSIT_SUCCEED_FLOW_REMINDER);
  // 更新账户余额
  accountService.clear(context, response);
  // 处理清算的优惠券,被用掉或者解绑
  couponService.clear(deductibleResponse);
  // 保存券抵扣记录
  clearCouponDeductService.add(context, deductibleResponse);
}

这段两代码里面其实业务很复杂,内部估计保守干了五万件事情,但是不同水平的人写出来就完全不同,不得不赞一下这个注释,这个业务的拆分和方法的封装。一个大业务里面有多个小业务,不同的业务调用不同的 service 方法即可,后续接手的人即使没有流程图等相关文档也能快速理解这里的业务,而很多初级开发写出来的业务方法就是上一行代码是 A 业务的,下一行代码是 B业务的,在下面一行代码又是 A 业务的,业务调用之间还嵌套这一堆单元逻辑,显得非常混乱,代码还多。

判断集合类型不为空的正确方式

很多人喜欢写这样的代码去判断集合

if (list == null || list.size() == 0) {
return null;
}

当然你硬要这么写也没什么问题......但是不觉得难受么,现在框架中随便一个 jar 包都有集合工具类,比如 org.springframework.util.CollectionUtilscom.baomidou.mybatisplus.core.toolkit.CollectionUtils 。 以后请这么写

if (CollectionUtils.isEmpty(list) || CollectionUtils.isNotEmpty(list)) {
return null;
}

集合类型返回值不要 return null

当你的业务方法返回值是集合类型时,请不要返回 null,正确的操作是返回一个空集合。你看 mybatis 的列表查询,如果没查询到元素返回的就是一个空集合,而不是 null。否则调用方得去做 NULL 判断,多数场景下对于对象也是如此。

映射数据库的属性尽量不要用基本类型

我们都知道 int/long 等基本数据类型作为成员变量默认值是 0。现在流行使用 mybatisplus 、mybatis 等 ORM 框架,在进行插入或者更新的时候很容易会带着默认值插入更新到数据库。我特么真想砍了之前的开发,重构的项目里面实体类里面全都是基本数据类型。当场裂开......

封装判断条件

public void method(LoanAppEntity loanAppEntity, long operatorId) {
if (LoanAppEntity.LoanAppStatus.OVERDUE != loanAppEntity.getStatus()
        && LoanAppEntity.LoanAppStatus.CURRENT != loanAppEntity.getStatus()
        && LoanAppEntity.LoanAppStatus.GRACE_PERIOD != loanAppEntity.getStatus()) {
  //...
  return;
}

这段代码的可读性很差,这 if 里面谁知道干啥的?我们用面向对象的思想去给 loanApp 这个对象里面封装个方法不就行了么?

public void method(LoanAppEntity loan, long operatorId) {
if (!loan.finished()) {
  //...
  return;
}

LoanApp 这个类中封装一个方法,简单来说就是这个逻辑判断细节不该出现在业务方法中。

/**
* 贷款单是否完成
*/
public boolean finished() {
return LoanAppEntity.LoanAppStatus.OVERDUE != this.getStatus()
        && LoanAppEntity.LoanAppStatus.CURRENT != this.getStatus()
        && LoanAppEntity.LoanAppStatus.GRACE_PERIOD != this.getStatus();
}

控制方法复杂度

推荐一款 IDEA 插件 CodeMetrics ,它能显示出方法的复杂度,它是对方法中的表达式进行计算,布尔表达式,if/else 分支,循环等。


点击可以查看哪些代码增加了方法的复杂度,可以适当进行参考,毕竟我们通常写的是业务代码,在保证正常工作的前提下最重要的是要让别人能够快速看懂。当你的方法复杂度超过 10 就要考虑是否可以优化了。

使用 @ConfigurationProperties 代替 @Value

之前居然还看到有文章推荐使用 @Value 比 @ConfigurationProperties 好用的,吐了,别误人子弟。列举一下 @ConfigurationProperties 的好处。

  • 在项目 application.yml 配置文件中按住 ctrl + 鼠标左键点击配置属性可以快速导航到配置类。写配置时也能自动补全、联想到注释。需要额外引入一个依赖 org.springframework.boot:spring-boot-configuration-processor


  • @ConfigurationProperties 支持 NACOS 配置自动刷新,使用 @Value 需要在 BEAN 上面使用 @RefreshScope 注解才能实现自动刷新

  • @ConfigurationProperties 可以结合 Validation 校验,@NotNull、@Length 等注解,如果配置校验没通过程序将启动不起来,及早的发现生产丢失配置等问题。

  • @ConfigurationProperties 可以注入多个属性,@Value 只能一个一个写

  • @ConfigurationProperties 可以支持复杂类型,无论嵌套多少层,都可以正确映射成对象

相比之下我不明白为什么那么多人不愿意接受新的东西,裂开......你可以看下所有的 springboot-starter 里面用的都是 @ConfigurationProperties 来接配置属性。

推荐使用 lombok

当然这是一个有争议的问题,我的习惯是使用它省去 getter、setter、toString 等等。

不要在 AService 调用 BMapper

我们一定要遵循从 AService -> BService -> BMapper,如果每个 Service 都能直接调用其他的 Mapper,那特么还要其他 Service 干嘛?老项目还有从 controller 调用 mapper 的,把控制器当 service 来处理了。。。

尽量少写工具类

为什么说要少写工具类,因为你写的大部分工具类,在你无形中引入的 jar 包里面就有,String 的,Assert 断言的,IO 上传文件,拷贝流的,Bigdecimal 的等等。自己写容易错还要加载多余的类。

不要包裹 OpenFeign 接口返回值

搞不懂为什么那么多人喜欢把接口的返回值用 Response 包装起来......加个 code、message、success 字段,然后每次调用方就变成这样

CouponCommonResult bindResult = couponApi.useCoupon(request.getCustomerId(), order.getLoanId(), coupon.getCode());
if (Objects.isNull(bindResult) || !bindResult.getResult()) {
throw new AppException(CouponErrorCode.ERR_REC_COUPON_USED_FAILED);
}

这样就相当于

  1. 在 coupon-api 抛出异常

  2. 在 coupon-api 拦截异常,修改 Response.code

  3. 在调用方判断 response.code 如果是 FAIELD 再把异常抛出去......

你直接在服务提供方抛异常不就行了么。。。而且这样一包装 HTTP 请求永远都是 200,没法做重试和监控。当然这个问题涉及到接口响应体该如何设计,目前网上大多是三种流派

  • 接口响应状态一律 200

  • 接口响应状态遵从HTTP真实状态

  • 佛系开发,领导怎么说就怎么做

不接受反驳,我推荐使用 HTTP 标准状态。特定场景包括参数校验失败等一律使用 400 给前端弹 toast。下篇文章会阐述一律 200 的坏处。

写有意义的方法注释

这种注释你写出来是怕后面接手的人瞎么......

/**
* 请求电话验证
*
* @param credentialNum
* @param callback
* @param param
* @return PhoneVerifyResult
*/

要么就别写,要么就在后面加上描述......写这样的注释被 IDEA 报一堆警告看着蛋疼

和前端交互的 DTO 对象命名

什么 VO、BO、DTO、PO 我倒真是觉得没有那么大必要分那么详细,至少我们在和前端交互的时候类名要起的合适,不要直接用映射数据库的类返回给前端,这会返回很多不必要的信息,如果有敏感信息还要特殊处理。

推荐的做法是接受前端请求的类定义为 XxxRequest,响应的定义为 XxxResponse。以订单为例:接受保存更新订单信息的实体类可以定义为 OrderRequest,订单查询响应定义为 OrderResponse,订单的查询条件请求定义为 OrderQueryRequest

尽量别让 IDEA 报警

我是很反感看到 IDEA 代码窗口一串警告的,非常难受。因为有警告就代表代码还可以优化,或者说存在问题。 前几天捕捉了一个团队内部的小bug,其实本来和我没有关系,但是同事都在一头雾水的看外面的业务判断为什么走的分支不对,我一眼就扫到了问题。

因为 java 中整数字面量都是 int 类型,到集合中就变成了 Integer,然后 stepId 点上去一看是 long 类型,在集合中就是 Long,那这个 contains 妥妥的返回 false,都不是一个类型。

你看如果注重到警告,鼠标移过去看一眼提示就清楚了,少了一个生产 bug。

尽可能使用新技术组件

我觉得这是一个程序员应该具备的素养......反正我是喜欢用新的技术组件,因为新的技术组件出现必定是解决旧技术组件的不足,而且作为一个技术人员我们应该要与时俱进~~ 当然前提是要做好准备工作,不能无脑升级。举个最简单的例子,Java 17 都出来了,新项目现在还有人用 Date 来处理日期时间...... 都什么年代了你还在用 Date

结语

本篇文章简单介绍我日常开发的习惯,当然仅是作者自己的见解。暂时只想到这几点,以后发现其他的会更新。

作者:暮色妖娆丶
来源:https://juejin.cn/post/7072252275002966030

收起阅读 »

纯后端如何写前端?我用了低代码平台

我是3y,一年CRUD经验用十年的markdown程序员👨🏻‍💻常年被誉为优质八股文选手花了几天搭了个后台管理页面,今天分享下我的搭建过程,全文非技术向,就当跟大家吹吹水吧。1、我的前端技术老读者可能知道我是上了大学以后,才了解什么是编程。在这之前,我对编程一...
继续阅读 »

我是3y,一年CRUD经验用十年的markdown程序员👨🏻‍💻常年被誉为优质八股文选手

花了几天搭了个后台管理页面,今天分享下我的搭建过程,全文非技术向,就当跟大家吹吹水吧。


1、我的前端技术

老读者可能知道我是上了大学以后,才了解什么是编程。在这之前,我对编程一无所知,甚至报考了计算机专业之后也未曾了解过它是做什么的。

在大一的第一个学期,我印象中只开了一门C++的编程课(其他的全是数学)。嗯,理所当然,我是听不懂的,也不知道用来干什么。


刚进大学的时候,我对一切充满了未知,在那时候顺其自然地就想要进几个社团玩玩。但在众多社团里都找不到我擅长的领域,等快到截止时间了。我又不想大学期间什么社团都没有参加,最后报了两个:乒乓球社团和计算机协会

这个计算机协会绝大多数的人员都来自于计算机专业,再后来才发现这个协会的主要工作就是给人「重装系统」,不过这是后话啦。

当时加入计算机协会还需要满足一定的条件:师兄给了一个「网站」我们这群人,让我们上去学习,等到国庆回来后看下我们的学习进度再来决定是否有资格加入。

那个网站其实就是对HTML/CSS/JavaScript入门教程,是一个国外的网站,具体的地址我肯定是忘了。不过那时候,我国庆闲着也没事干,于是就开始学起来了。我当时的进度应该是学到CSS,能简单的页面布局和展示图片啥的

刚开始的时候,觉得蛮有趣的:我改下这个代码,字体的颜色就变了,图片就能展示出来了。原来我平时上网的网站是这样弄出来的啊! (比什么C++有趣多了)

国庆后回来发现:考核啥的并不重要,只要报名了就都通过了。


有了基本的认知后,我对这个也并不太上心,没有持续地学下去。再后来,我实在是太无聊,就开始想以后毕业找工作的事了,自己也得在大学充实下自己,于是我开始在知乎搜各种答案「如何入门编程」。

在知乎搜了各种路线并浪费了大量时间以后,我终于开始看视频入门。我熬完了JavaSE基础之后,我记得我是看方立勋老师入门的JavaWeb,到前端的课程以后,我觉得前端HTML/CSS/JavaScript啥的都要补补,于是又去找资源学习(那时候信奉着技多不压身)。

印象中是看韩顺平老师的HTML/CSS/JavaScript,那时候还手打代码的阶段,把我看得一愣一愣的(IDE都不需要的)。随着学习,发现好像还得学AJAX/jQuery,于是我又去找资源了,不过我已经忘了看哪个老师的AJAXjQuery课程。

在这个学习的过程中,我曾经用纯HTML/CSS/JavaScript跟着视频仿照过某某网站,在jQuery的学习时候做过各种的轮播图动画。还理解了marginpadding的区别。临近毕业的时候,也会点BootStrap来写个简单的页面(丑就完事了)


等我进公司了以后,技术架构前后端是分离的,虽然我拉了前端的代码,但我看不懂,期间我也没学。以至于我两年多是没碰过前端的,我对前端充满着敬畏(刚毕业那段时间,前端在飞速发展

2、AUSTIN前端选型

从我筹划要写austin项目的时候,我就知道我肯定要写一个「后台管理页面」,但我迟迟没下手。一方面是我认为「后端」才是我的赛道,另一方面我「前端」确实菜,不想动手。

我有想过要不找个小伙伴帮我写,但是很快就被我自己否定了:还得给小伙伴提需求,算了


当我要面临前端的时,我第一时间就想到:肯定是有什么框架能够快速搭建出一个管理页面的。我自己不知道,但是,我的朋友圈肯定是有人知道的啊。于是,我果断求助:


我被安利了很多框架,简单列举下出场率比较高的。

:大多数我只是粗略看了下,没有仔细研究。若有错误可以在评论区留言,轻喷

2.1 renren-fast

官网文档:http://www.renren.io/guide#getdo…


它这个框架是前后端分离的,后端还可以生成对应的CRUD代码,前端基于vueelement-ui开发。

当时其实我有点想选它的,但考虑到我要再部署个后端,还得学点vue,我就搁置了

2.2 RuoYi

官方文档:doc.ruoyi.vip/ruoyi/


RuoYi给我安利的也很多,这个貌似最近非常火?感觉我被推荐了以后,到处都能看到它的身影。

我简单刷了下文档,感觉他做的事比renren-fast要多,文档也很齐全,但是没找到我想要的东西:我打开一个文档,我希望能看到它的系统架构,系统之间的交互或者架构层面上的东西,但我没快速找到。

项目齐全和复杂对我来说或许并不是一件好事,很可能意味着我的学习成本可能会更大。于是,我也搁置着。

2.3 Vue相关

vue-element-admin

官方文档:panjiachen.github.io/vue-element…


Vue Antd Admin

官方文档:iczer.gitee.io/vue-antd-ad…


Ant Design Pro

官方文档:pro.antdv.com/docs/gettin…


这几个项目被推荐率也是极高的,从第一行介绍我基本就知道需要去学Vue的语法,奈何我太懒了,搁置着。

2.4 layui

有好几小伙伴们听说我会jQuery,于是给我推荐了layui。我以前印象中好像听过这个框架,但一直没了解过他。但是,当我搜到它的时候,它已经不维护了


GitHub地址:github.com/sentsin/lay…

我简单浏览下文档,其实它也有对应的一套”语法“,需要一定的学习成本,但不高。


第一感觉有点类似我以前写过的BootStrap,我对这不太感冒,感觉如果要接入可能还是需要自己写比较多的代码。

2.5 其他

还有些小伙伴推荐或者我看到的文章推荐:x-admin/D2admin/smartchart/JEECG-BOOT/Dcat-admin/iview-admin等等等,在这里面还有些依赖着PHP/Python

总的来说,我还是觉得这些框架有一定的学习成本(我真的是懒出天际了)。可能需要我去部署后端,也可能需要我学习前端的框架语法,也可能让我学Vue

看到这里,可能你们很好奇我最后选了什么作为austin的前端,都已经被我筛了这么多了。在公布之前,我想说的是:如果想要页面好看灵活性高还是得学习Vue。从上面我被推荐的框架中,好多都是在Vue的基础上改动的,并且我敢肯定:还有很多基于Vue且好用的后台是我不知道的。

:我这里指代跟我一样不懂前端的(如果本身就已经懂前端,你说啥都对)


3、AMIS框架

我最后选择了amis作为austin的前端。这个框架在我朋友圈只有一个小伙伴推荐,我第一次打开文档的时候,确实惊艳到我了


文档地址:baidu.gitee.io/amis/zh-CN/…

它是一个低代码前端框架:amis 的渲染过程是将 json 转成对应的 React 组件

我花了半天粗略地刷了下文档,大概知道了JSON的结构(说实话,他这个文档写得挺可以的),然后我去GitHub找了一份模板,就直接开始动手了,readme十分简短。


GitHub:github.com/aisuda/amis…

这个前端低代码工具还有个好处就是可以通过可视化编辑器拖拉生成JSON代码,将生成好的代码直接往自己本地一贴,就完事了,确实挺方便的。


可视化编辑器的地址:aisuda.github.io/amis-editor…

4、使用感受

其实没什么好讲的,无非就是在页面上拖拉得到一个页面,然后调用API的时候看下文档的姿势。

在这个过程中我也去看了下这个框架的评价,发现百度内部很多系统就用的这个框架来搭建页面的,也看到Bigo也有在线上使用这个框架来搭建后台。有一线/二线公司都在线上使用该框架了,我就认为问题不大了。

总的来说,我这次搭建austin后台实际编码时间没多少,都在改JSON配置和查文档。我周六下午2点到的图书馆,新建了GitHub仓库,在6点闭馆前就已经搭出个大概页面了,然后在周日空闲时间里再完善了几下,感觉可以用了

austin-amis仓库地址:github.com/ZhongFuChen…

在搭建的过程中,amis低代码框架还是有地方可吐槽的,就是它的灵活性太低。我们的接口返回值需要迎合它的主体结构,当我们如果有嵌套JSON这种就变得异常难处理,表单无法用表达式进行回显等等。

它并不完美,很可能需要我用些奇怪的姿势妥协,不要问我接口返回的时候为啥转了一层Map


不管怎么说,这不妨碍我花了极短的时间就能搭出一个能看的后台管理页面(CRUD已齐全)


5、总结

目前搭好的前端能用,也只能用一点点,后面会逐渐完善它的配置和功能的。我后面有链路追踪的功能,肯定要在后台这把清洗后的数据提供给后台进行查询,但也不会花比较长的篇幅再来聊前端这事了。

我一直定位是在后端的代码上,至于前端我能学,但我又不想学。怎么说呢,利益最大化吧。我把学前端的时间花在学后端上,或许可能对我有更大的受益。现在基本前后端分离了,在公司我也没什么机会写前端。

下一篇很有可能是聊分布式定时任务框架上,我发现我的进度可以的,这个季度拿个4.0应该问题不大了。

都看到这里了,点个赞一点都不过分吧?我是3y,下期见。


austin项目源码Gitee链接:gitee.com/austin

austin项目源码GitHub链接:github.com/austin


作者:Java3y
来源:https://juejin.cn/post/7076231399669235725

收起阅读 »

你最少用几行代码实现深拷贝?

前言深度克隆(深拷贝)一直都是初、中级前端面试中经常被问到的题目,网上介绍的实现方式也都各有千秋,大体可以概括为三种方式:JSON.stringify+JSON.pars e, 这个很好理解;全量判断类型,根据类型做不同的处理2的变型,简化类型判断过程前两种比...
继续阅读 »

前言

深度克隆(深拷贝)一直都是初、中级前端面试中经常被问到的题目,网上介绍的实现方式也都各有千秋,大体可以概括为三种方式:

  1. JSON.stringify+JSON.pars e, 这个很好理解;

  2. 全量判断类型,根据类型做不同的处理

  3. 2的变型,简化类型判断过程

前两种比较常见也比较基础,所以我们今天主要讨论的是第三种。

阅读全文你将学习到:

  1. 更简洁的深度克隆方式

  2. Object.getOwnPropertyDescriptors()api

  3. 类型判断的通用方法

问题分析

深拷贝 自然是 相对 浅拷贝 而言的。 我们都知道 引用数据类型 变量存储的是数据的引用,就是一个指向内存空间的指针, 所以如果我们像赋值简单数据类型那样的方式赋值的话,其实只能复制一个指针引用,并没有实现真正的数据克隆。

通过这个例子很容易就能理解:

const obj1 = {
   name: 'superman'
}
const obj2 = obj1;
obj1.name = '前端切图仔';
console.log(obj2.name); // 前端切图仔

所以深度克隆就是为了解决引用数据类型不能被通过赋值的方式 复制 的问题。

引用数据类型

我们不妨来罗列一下引用数据类型都有哪些:

  • ES6之前: Object, Array, Date, RegExp, Error,

  • ES6之后: Map, Set, WeakMap, WeakSet,

所以,我们要深度克隆,就需要对数据进行遍历并根据类型采取相应的克隆方式。 当然因为数据会存在多层嵌套的情况,采用递归是不错的选择。

简单粗暴版本

function deepClone(obj) {
   let res = {};
   // 类型判断的通用方法
   function getType(obj) {
       return Object.prototype.toString.call(obj).replaceAll(new RegE xp(/\[|\]|object /g), "");
  }
   const type = getType(obj);
   const reference = ["Set", "WeakSet", "Map", "WeakMap", "RegExp", "Date", "Error"];
   if (type === "Object") {
       for (const key in obj) {
           if (Object.hasOwnProperty.call(obj, key)) {
               res[key] = deepClone(obj[key]);
          }
      }
  } else if (type === "Array") {
       console.log('array obj', obj);
       obj.forEach((e, i) => {
           res[i] = deepClone(e);
      });
  }
   else if (type === "Date") {
       res = new Date(obj);
  } else if (type === "RegExp") {
       res = new RegExp(obj);
  } else if (type === "Map") {
       res = new Map(obj);
  } else if (type === "Set") {
       res = new Set(obj);
  } else if (type === "WeakMap") {
       res = new WeakMap(obj);
  } else if (type === "WeakSet") {
       res = new WeakSet(obj);
  }else if (type === "Error") {
       res = new Error(obj);
  }
    else {
       res = obj;
  }
   return res;
}

其实这就是我们最前面提到的第二种方式,很傻对不对,明眼人一眼就能看出来有很多冗余代码可以合并。

我们先进行最基本的优化:

合并冗余代码

将一眼就能看出来冗余的代码合并下。

function deepClone(obj) {
   let res = null;
   // 类型判断的通用方法
   function getType(obj) {
       return Object.prototype.toString.call(obj).replaceAll(new RegExp(/\[|\]|object /g), "");
  }
   const type = getType(obj);
   const reference = ["Set", "WeakSet", "Map", "WeakMap", "RegExp", "Date", "Error"];
   if (type === "Object") {
       res = {};
       for (const key in obj) {
           if (Object.hasOwnProperty.call(obj, key)) {
               res[key] = deepClone(obj[key]);
          }
      }
  } else if (type === "Array") {
       console.log('array obj', obj);
       res = [];
       obj.forEach((e, i) => {
           res[i] = deepClone(e);
      });
  }
   // 优化此部分冗余判断
   // else if (type === "Date") {
   //     res = new Date(obj);
   // } else if (type === "RegExp") {
   //     res = new RegExp(obj);
   // } else if (type === "Map") {
   //     res = new Map(obj);
   // } else if (type === "Set") {
   //     res = new Set(obj);
   // } else if (type === "WeakMap") {
   //     res = new WeakMap(obj);
   // } else if (type === "WeakSet") {
   //     res = new WeakSet(obj);
   // }else if (type === "Error") {
   //   res = new Error(obj);
   //}
   else if (reference.includes(type)) {
       res = new obj.constructor(obj);
  } else {
       res = obj;
  }
   return res;
}

为了验证代码的正确性,我们用下面这个数据验证下:

const map = new Map();
map.set("key", "value");
map.set("ConardLi", "coder");

const set = new Set();
set.add("ConardLi");
set.add("coder");

const target = {
   field1: 1,
   field2: undefined,
   field3: {
       child: "child",
  },
   field4: [2, 4, 8],
   empty: null,
   map,
   set,
   bool: new Boolean(true),
   num: new Number(2),
   str: new String(2),
   symbol: Object(Symbol(1)),
   date: new Date(),
   reg: /\d+/,
   error: new Error(),
   func1: () => {
       let t = 0;
       console.log("coder", t++);
  },
   func2: function (a, b) {
       return a + b;
  },
};
//测试代码
const test1 = deepClone(target);
target.field4.push(9);
console.log('test1: ', test1);

执行结果:


还有进一步优化的空间吗?

答案当然是肯定的。

// 判断类型的方法移到外部,避免递归过程中多次执行
const judgeType = origin => {
   return Object.prototype.toString.call(origin).replaceAll(new RegExp(/\[|\]|object /g), "");
};
const reference = ["Set", "WeakSet", "Map", "WeakMap", "RegExp", "Date", "Error"];
function deepClone(obj) {
   // 定义新的对象,最后返回
    //通过 obj 的原型创建对象
   const cloneObj = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));

   // 遍历对象,克隆属性
   for (let key of Reflect.ownKeys(obj)) {
       const val = obj[key];
       const type = judgeType(val);
       if (reference.includes(type)) {
           newObj[key] = new val.constructor(val);
      } else if (typeof val === "object" && val !== null) {
           // 递归克隆
           newObj[key] = deepClone(val);
      } else {
           // 基本数据类型和function
           newObj[key] = val;
      }
  }
   return newObj;
}

执行结果如下:


  • Object.getOwnPropertyDescriptors() 方法用来获取一个对象的所有自身属性的描述符。

  • 返回所指定对象的所有自身属性的描述符,如果没有任何自身属性,则返回空对象。

具体解释和内容见MDN

这样做的好处就是能够提前定义好最后返回的数据类型。

这个实现参考了网上一位大佬的实现方式,个人觉得理解成本有点高,而且对数组类型的处理也不是特别优雅, 返回类数组。

我在我上面代码的基础上进行了改造,改造后的代码如下:

function deepClone(obj) {
   let res = null;
   const reference = [Date, RegExp, Set, WeakSet, Map, WeakMap, Error];
   if (reference.includes(obj?.constructor)) {
       res = new obj.constructor(obj);
  } else if (Array.isArray(obj)) {
       res = [];
       obj.forEach((e, i) => {
           res[i] = deepClone(e);
      });
  } else if (typeof obj === "Object" && obj !== null) {
       res = {};
       for (const key in obj) {
           if (Object.hasOwnProperty.call(obj, key)) {
               res[key] = deepClone(obj[key]);
          }
      }
  } else {
       res = obj;
  }
   return res;
}

虽然代码量上没有什么优势,但是整体的理解成本和你清晰度上我觉得会更好一点。那么你觉得呢?

最后,还有循环引用问题,避免出现无线循环的问题。

我们用hash来存储已经加载过的对象,如果已经存在的对象,就直接返回。

function deepClone(obj, hash = new WeakMap()) {
   if (hash.has(obj)) {
       return obj;
  }
   let res = null;
   const reference = [Date, RegExp, Set, WeakSet, Map, WeakMap, Error];

   if (reference.includes(obj?.constructor)) {
       res = new obj.constructor(obj);
  } else if (Array.isArray(obj)) {
       res = [];
       obj.forEach((e, i) => {
           res[i] = deepClone(e);
      });
  } else if (typeof obj === "Object" && obj !== null) {
       res = {};
       for (const key in obj) {
           if (Object.hasOwnProperty.call(obj, key)) {
               res[key] = deepClone(obj[key]);
          }
      }
  } else {
       res = obj;
  }
   hash.set(obj, res);
   return res;
}

总结

对于深拷贝的实现,可能存在很多不同的实现方式,关键在于理解其原理,并能够记住一种最容易理解和实现的方式,面对类似的问题才能做到 临危不乱,泰然自若。 上面的实现你觉得哪个更好呢?欢迎大佬们在评论区交流~


作者:前端superman
来源:https://juejin.cn/post/7075351322014253064

收起阅读 »

为了快乐的摸鱼,专门写了个网站!

这是鄙人做的网站,目的呢原本是为了摸鱼,把产品那边整的页面快速构建出来,咱们公司用的是比较老的vue2版本,组件库是ant-design-vue,做的系统是一些中规中矩的企业用的办公系统,所以页面都是千篇一律。作为卑微的996社畜,不想被肆无忌惮的压榨,于是有...
继续阅读 »

直接进入主题: demo

这是鄙人做的网站,目的呢原本是为了摸鱼,把产品那边整的页面快速构建出来,咱们公司用的是比较老的vue2版本,组件库是ant-design-vue,做的系统是一些中规中矩的企业用的办公系统,所以页面都是千篇一律。作为卑微的996社畜,不想被肆无忌惮的压榨,于是有一天,我就琢磨着通过拖拉拽的方式把组件模块组合起来,能快速的响应产品那边朝令夕改的无理要求。

经过将近一个月的鼓捣,小破站也在命运多舛中慢慢走向成熟。

先简单介绍吧,显而易见的操作界面:传统的页眉,低调却不失风采;左侧的手风琴列表,简约而不简单;中控是一个设计器,有了它你可以写出一个出色的网页,而不需要写一行代码(少量代码还是必要的)!





本小破站还做了国际化、自适应,能基本满足常规的企业系统界面需求,比如传统的ERP/HR/SDM等后台管理系统,页面的顶部有一个下拉框,里面有默认的几个示例,都是通过这种拖拽方式做出来的。

有一个地方需要特别说明,就是组件提供的事件回调函数提供w,w,w,vm这两个全局参数。w表示当前window全局对象,w表示当前window全局对象,w表示当前window全局对象,vm则代表全局vm对象,也就是this。通过这两个参数,是可以简单的写出组件间调用的方法的 (可以看看test#table这个例子)。当然,涉及更复杂一点的业务逻辑,则需要做更多的代码复用,以及watch监听等等,这部分功能的话暂时还没有想好怎么实现。

组件有基本的antd组件、echarts组件,还有vue-3d-model组件,为了更方便的编辑属性和代码,用了bin-ace-editor,有了这些大佬们的轮子,转起来确实快乐。

功能还在逐步完善中,最近也没很多时间去写,总之有时间就去补充,日积月累的完善吧。

PS:

小破站带宽是乞丐版的1Mb,鄙人已经尽力做了cdn加速,希望不卡

第一次打开会自动生成3个示例,放在localStorage里面

感谢阅读 ^_^


作者:AllenThomas
来源:https://juejin.cn/post/7077743139934437406

收起阅读 »

95年女程序员内心的感受

作为一个95年女程序员,2022也是我从事前端的第5个年头了,期间换过2个公司,想和大家分享下真实感受。坐标: 东北 故事开始 95年出生的女生,大专学历,大学专业为“电子商务”, 因为接触到网页设计与制作以及PS等课程,从而对通过代码写过网页产生了兴趣,东...
继续阅读 »

作为一个95年女程序员,2022也是我从事前端的第5个年头了,期间换过2个公司,想和大家分享下真实感受。坐标: 东北



故事开始


95年出生的女生,大专学历,大学专业为“电子商务”, 因为接触到网页设计与制作以及PS等课程,从而对通过代码写过网页产生了兴趣,东西做出来觉得很有成就感,于是实习期间抱着期待的心情投了很多关于网页设计和网页制作相关的工作,但是更多等来的是培训机构的电话,被告知不通过培训时找不到工作的,学习的东西太少不够用于工作,于是阴差阳错的去了培训机构学习了前端开发...


第一个公司


第一家公司是我2017年3月初入职的,是一个外包公司,由于是mini型公司,没有前端,所以是一个UI小姐姐面试的我,刚好我懂一点设计,聊的比较来,并没有问我太多技术问题就让我去上班了,就这样开启了我的第一份工作...


感受:公司很小,人员很nice,成长很快,初创型企业真的很锻炼人,当只有我一个前端面临问题时,真的很无助,但也因此锻炼我发现问题解决问题的能力,不会就学,不懂就百度,慢慢的也开始能够独当一面,虽然经常加班,但是我很感谢这一份经历,虽然工资低也很累但是也收获了巨大成长


离职原因:后来公司被一个大公司收购成为了一个小部门,因为拖欠工资没办法维持现在的生活就离开了...


第二个公司


第二家公司是2018年入职,是一个自研产品的公司,研发人员上百个,当然前端人员自然也很多啦,想要重点说说感受


感受:公司很大,制度很多,流程相对规范,比如git提交的规范、自动构建、内部脚手架等,不过我发现明显的问题,就是分工不均匀,有的项目组天天加班,持续半年加班,但是有的项目组,每天除了摸鱼还是摸鱼,比如我们组,很多项目都流产了,就很多时候都没有事情,最多的就是逛逛B站,偶尔自学,看看新闻时事....


离职:目前尚未离职,在纠结中。


焦虑


在这个公司快4年了,技术一直停滞,生活的很安逸,但是有点温水煮青蛙的感觉,刚开始会自学,慢慢的也就不太想自学了,好像整个人都堕落了,没有刚开始工作的冲劲了... 算一算我也27岁了,虽说未来的事情说不准,但是确实30岁以后的的程序员确实找工作相对会难一点,尤其是女程序员会考虑到结婚生子的问题,年龄大了确实也会受限,所以现在在纠结到底从事什么工作,是转行还是重新努力学习前端知识,从事前端开发工作,会发现很迷茫,换行or继续前端....


很后悔浪费了这4年的光阴,希望后面的程序员弟弟妹妹要珍惜自己现在的时光,程序员本身就业时间就相对其他行业短暂一点,对于我们来说每一天都很重要,4年了足够我们做很多很多事情了,如果对开发不是很感兴趣也可以提前根据自己的兴趣来规划职业


作者:M77星球
来源:https://juejin.cn/post/7076377003057741838
收起阅读 »

前端无痛刷新Token

前端无痛刷新Token这个需求场景很常见,几乎很多项目都会用上,之前项目也实现过,最近刚好有个项目要实现,重新梳理一番。需求对于需要前端实现无痛刷新Token,无非就两种:请求前判断Token是否过期,过期则刷新请求后根据返回状态判断是否过期,过期则刷新处理逻...
继续阅读 »

前端无痛刷新Token

这个需求场景很常见,几乎很多项目都会用上,之前项目也实现过,最近刚好有个项目要实现,重新梳理一番。

需求

对于需要前端实现无痛刷新Token,无非就两种:

  1. 请求前判断Token是否过期,过期则刷新

  2. 请求后根据返回状态判断是否过期,过期则刷新

处理逻辑

实现起来也没多大差别,只是判断的位置不一样,核心原理都一样:

  1. 判断Token是否过期

    1. 没过期则正常处理

    2. 过期则发起刷新Token的请求

      1. 拿到新的Token保存

      2. 重新发送Token过期这段时间内发起的请求

重点:

  • 保持Token过期这段时间发起请求状态(不能进入失败回调)

  • 把刷新Token后重新发送请求的响应数据返回到对应的调用者

实现

  1. 创建一个flag isRefreshing 来判断是否刷新中

  2. 创建一个数组队列retryRequests来保存需要重新发起的请求

  3. 判断到Token过期

    1. isRefreshing = false的情况下 发起刷新Token的请求

      1. 刷新Token后遍历执行队列retryRequests

    2. isRefreshing = true 表示正在刷新Token,返回一个Pending状态的Promise,并把请求信息保存到队列retryRequests

import axios from "axios";
import Store from "@/store";
import Router from "@/router";
import { Message } from "element-ui";
import UserUtil from "@/utils/user";

// 创建实例
const Instance = axios.create();
Instance.defaults.baseURL = "/api";
Instance.defaults.headers.post["Content-Type"] = "application/json";
Instance.defaults.headers.post["Accept"] = "application/json";

// 定义一个flag 判断是否刷新Token中
let isRefreshing = false;
// 保存需要重新发起请求的队列
let retryRequests = [];

// 请求拦截
Instance.interceptors.request.use(async function(config) {
 Store.commit("startLoading");
 const userInfo = UserUtil.getLocalInfo();
 if (userInfo) {
   //业务需要把Token信息放在 params 里面,一般来说都是放在 headers里面
   config.params = Object.assign(config.params ? config.params : {}, {
     appkey: userInfo.AppKey,
     token: userInfo.Token
  });
}
 return config;
});

// 响应拦截
Instance.interceptors.response.use(
 async function(response) {
   Store.commit("finishLoading");
   const res = response.data;
   if (res.errcode == 0) {
     return Promise.resolve(res);
  } else if (
     res.errcode == 30001 ||
     res.errcode == 40001 ||
     res.errcode == 42001 ||
     res.errcode == 40014
  ) {
   // 需要刷新Token 的状态 30001 40001 42001 40014
   // 拿到本次请求的配置
     let config = response.config;
   //   进入登录页面的不做刷新Token 处理
     if (Router.currentRoute.path !== "/login") {
       if (!isRefreshing) {
           // 改变flag状态,表示正在刷新Token中
         isRefreshing = true;
       //   刷新Token
         return Store.dispatch("user/relogin")
          .then(res => {
           // 设置刷新后的Token
             config.params.token = res.Token;
             config.params.appkey = res.AppKey;
           //   遍历执行需要重新发起请求的队列
             retryRequests.forEach(cb => cb(res));
           //   清空队列
             retryRequests = [];
             return Instance.request(config);
          })
          .catch(() => {
             retryRequests = [];
             Message.error("自动登录失败,请重新登录");
               const code = Store.state.user.info.CustomerCode || "";
               // 刷新Token 失败 清空缓存的用户信息 并调整到登录页面
               Store.dispatch("user/logout");
               Router.replace({
                 path: "/login",
                 query: { redirect: Router.currentRoute.fullPath, code: code }
              });
          })
          .finally(() => {
               // 请求完成后重置flag
             isRefreshing = false;
          });
      } else {
         // 正在刷新token,返回一个未执行resolve的promise
         // 把promise 的resolve 保存到队列的回调里面,等待刷新Token后调用
         // 原调用者会处于等待状态直到 队列重新发起请求,再把响应返回,以达到用户无感知的目的(无痛刷新)
         return new Promise(resolve => {
           // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
           retryRequests.push(info => {
               // 将新的Token重新赋值
             config.params.token = info.Token;
             config.params.appkey = info.AppKey;
             resolve(Instance.request(config));
          });
        });
      }
    }
     return new Promise(() => {});
  } else {
     return Promise.reject(res);
  }
},
 function(error) {
   let err = {};
   if (error.response) {
     err.errcode = error.response.status;
     err.errmsg = error.response.statusText;
  } else {
     err.errcode = -1;
     err.errmsg = error.message;
  }
   Store.commit("finishLoading");
   return Promise.reject(err);
}
);

export default Instance;


作者:沐夕花开
来源:https://juejin.cn/post/7075348765162340383

收起阅读 »

基于JDK的动态代理原理分析

基于JDK的动态代理原理分析 这篇文章解决三个问题: What 动态代理是什么 How 动态代理怎么用 Why 动态代理的原理 动态代理是什么? 动态代理是代理模式的一种具体实现,是指在程序运行期间,动态的生成目标对象的代理类(直接加载在内存中的字节码文件...
继续阅读 »

基于JDK的动态代理原理分析


这篇文章解决三个问题:



  1. What 动态代理是什么

  2. How 动态代理怎么用

  3. Why 动态代理的原理


动态代理是什么?


动态代理是代理模式的一种具体实现,是指在程序运行期间,动态的生成目标对象的代理类(直接加载在内存中的字节码文件),实现对目标对象所有方法的增强。通过这种方式,我们可以在不改变(或无法改变)目标对象源码的情况下,对目标对象的方法执行前后进行干预。


动态代理怎么用?


首先,准备好我们需要代理的类和接口,因为JDK的动态代理是基于接口实现的,所以被代理的对象必须要有接口


/**
* SaySomething接口
*/

public interface SaySomething {

   public void sayHello();

   public void sayBye();
}

/**
* SaySomething的实现类
*/

public class SaySomethingImpl implements SaySomething {
   @Override
   public void sayHello() {
       System.out.println("Hello World");
  }

   @Override
   public void sayBye() {
       System.out.println("Bye Bye");
  }
}

按照动态代理的用法,需要自定义一个处理器,用来编写自定义逻辑,实现对被代理对象的增强。


自定义的处理器需要满足以下要求:



  • 需要实现InvocationHandler,重写invoke方法,在invoke方法中通过加入自定义逻辑,实现对目标对象的增强。

  • 需要持有一个成员变量,成员变量的是被代理对象的实例,通过构造参数传入。(用来支持反射调用被代理对象的方法)

  • 需要提供一个参数为被代理对象接口类的有参构造。(用来支持反射调用被代理对象的方法)


/**
* 自定义的处理器,用来编写自定义逻辑,实现对被代理对象的增强
*/

public class CustomHandler implements InvocationHandler {

   //需要有一个成员变量,成员变量为被代理对象,通过构造参数传入,用来支持方法的反射调用。
   private SaySomething obj;
   
   //需要有一个有参构造,通过构造函数将被代理对象的实例传入,用来支持方法的反射调用
   public CustomHandler(SaySomething obj) {
       this.obj = obj;
  }

   /**
    * proxy:动态生成的代理类对象com.sun.proxy.$Proxy0
    * method:被代理对象的真实的方法的Method对象
    * args:调用方法时的入参
    */

   @Override
   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
       //目标方法执行前的自定义逻辑处理
       System.out.println("-----before------");

       //执行目标对象的方法,使用反射来执行方法,反射需要传入目标对象,此时用到了成员变量obj。
       Object result = method.invoke(obj, args);

       //目标方法执行后的自定义逻辑处理
       System.out.println("-----after------");
       return result;
  }
}

这样我们就完成了自定义处理器的编写,同时在invoke方法中实现对了代理对象方法的增强,被代理类的所有方法的执行都会执行我们自定义的逻辑。


接下来,需要通过Proxy,newProxyInstance()方法来生成代理对象的实例,并进行方法调用测试。


public class JdkProxyTest {
   public static void main(String[] args) {
       //将生成的代理对象的字节码文件 保存到硬盘
       System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

       //被代理对象的实例
       SaySomething obj = new SaySomethingImpl();
       //通过构造函数,传入被代理对象的实例,生成处理器的实例
       InvocationHandler handler = new CustomHandler(obj);
       //通过Proxy.newProxyInstance方法,传入被代理对象Class对象、处理器实例,生成代理对象实例
       SaySomething proxyInstance = (SaySomething) Proxy.newProxyInstance(obj.getClass().getClassLoader(),
                                                                          new Class[]{SaySomething.class}, handler);
       //调用生成的代理对象的sayHello方法
       proxyInstance.sayHello();
       System.out.println("===================分割线==================");
       //调用生成的代理对象的sayBye方法
       proxyInstance.sayBye();
  }
}

image.png
运行main方法,查看控制台,大功告成。至此,我们已经完整的完成了一次动态代理的使用。


动态代理的原理


生成的proxyInstance对象到底是什么,为什么调用它的sayHello方法会执行CustomerHandler的invoke方法呢?


直接贴上proxyInstance的字节码文件,我们就会恍然大悟了...


//$Proxy0是SaySomething的实现类,重写了sayHello和sayBye方法
public final class $Proxy0 extends Proxy implements SaySomething {
   private static Method m1;
   private static Method m3;
   private static Method m2;
   private static Method m4;
   private static Method m0;

   public $Proxy0(InvocationHandler var1) throws {
       super(var1);
  }

   static {
       try {
           m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
           m3 = Class.forName("com.example.demo.hanmc.proxy.jdk.SaySomething").getMethod("sayHello");
           m2 = Class.forName("java.lang.Object").getMethod("toString");
           m4 = Class.forName("com.example.demo.hanmc.proxy.jdk.SaySomething").getMethod("sayBye");
           m0 = Class.forName("java.lang.Object").getMethod("hashCode");
      } catch (NoSuchMethodException var2) {
           throw new NoSuchMethodError(var2.getMessage());
      } catch (ClassNotFoundException var3) {
           throw new NoClassDefFoundError(var3.getMessage());
      }
  }
 
   //实现了接口的sayHello方法,在方法内部调用了CustomerHandler的invoke方法,同时传入了Method对象,
   //所以在CustomerHandler对象中可以通过mathod.invovke方法调用SyaSomthing的sayHello方法
   public final void sayHello() throws {
       try {
           //h是父类Proxy中的InvocationHandler对象,其实就是我们自定义的CustomHandler对象
           super.h.invoke(this, m3, (Object[])null);
      } catch (RuntimeException | Error var2) {
           throw var2;
      } catch (Throwable var3) {
           throw new UndeclaredThrowableException(var3);
      }
  }

   public final void sayBye() throws {
       try {
           super.h.invoke(this, m4, (Object[])null);
      } catch (RuntimeException | Error var2) {
           throw var2;
      } catch (Throwable var3) {
           throw new UndeclaredThrowableException(var3);
      }
  }
   public final int hashCode() throws {
      //忽略内容
  }
   public final boolean equals(Object var1) throws {
      //忽略内容
  }
   public final String toString() throws {
      //忽略内容
  }
}

看到了生成的代理对象的字节码文件,是不是一切都明白你了,原理竟然如此简单^_^


作者:82年咖啡
来源:https://juejin.cn/post/7079720742899843080
收起阅读 »

你已经是个成熟的前端了,应该学会破解防盗链了

今天一早打开微信,就看到国产github——gitee崩了。 Issue列表里面全是反馈图片显示异常,仔细一看,原来是图床的防盗链。 场景复现 之前没用过gitee,火速去建了一个账号试验一下。 我在我的gitee中上传一张图片,在gitee本站里面显示是正...
继续阅读 »

今天一早打开微信,就看到国产github——gitee崩了。



Issue列表里面全是反馈图片显示异常,仔细一看,原来是图床的防盗链。


场景复现


之前没用过gitee,火速去建了一个账号试验一下。


我在我的gitee中上传一张图片,在gitee本站里面显示是正常的。


1-1.png


右键复制这张图片的地址,放到一个第三方的在线编辑器中,发现图片变成gitee的logo了



什么是防盗链


防盗链不是一根链条,正确的停顿应该是防·盗链——防止其他网站盗用我的链接。


我把图片上传到gitee的服务器,得到了图片的链接,然后拿着这个链接在第三方编辑器中使用,这就是在“盗用”——因为这张图片占用了gitee的服务器资源,却为第三方编辑器工作,gitee得不到好处,还得多花钱。


如何实现防盗链


要实现防盗链,就需要知道图片的请求是从哪里发出的。可以实现这一功能的有请求头中的originrefererorigin只有在XHR请求中才会带上,所以图片资源只能借助referer。其实gitee也确实是这么做的。


通过判断请求的referer,如果请求来源不是本站就返回302,重定向到gitee的logo上,最后在第三方网站引用存在gitee的资源就全变成它的logo了。


可以在开发者工具中看到第三方网站请求gitee图片的流程:



  1. 首先请求正常的图片,但是没有返回200,而是302重定向,其中响应头中的location就是要重定向去向的地址;

  2. 接着浏览器会自动请求这个location,并用这个返回结果代替第一次请求的返回内容;


最后,我们的图片在第三方网站就变成gitee的logo了。


如何破解防盗链


想让gitee不知道我在盗用,就不能让他发现请求的来源是第三方,只要把referer藏起来就好,可以在终端尝试这段代码:


curl 'https://images.gitee.com/uploads/images/2022/0326/155444_dc9923a4_10659337.jpeg' \
-o noReferer.jpg

这段👆代码的意思是请求这张jpg图片资源,把返回结果以noReferer.jpg这个名称保存在当前目录下,并且没有带上referer,测试结果是图片正常保存下来了。


就像加上了gitee本站的referer一样可以正常请求👇:


curl 'https://images.gitee.com/uploads/images/2022/0326/155444_dc9923a4_10659337.jpeg' \
-H 'referer: https://gitee.com' \
-o fromGitee.jpg

而在第三方网站请求的效果就像这段👇代码


curl 'https://images.gitee.com/uploads/images/2022/0326/155444_dc9923a4_10659337.jpeg' \
-H 'referer: https://editor.mdnice.com/' \
-o otherReferer.png

带上了第三方网站的标识https://editor.mdnice.com最终无法正常下载。


gitee做的不够完善吗


测试完上面的三段代码,不知道你会不会疑惑,gitee为什么不把“请求来源不能是第三方网站”的策略改成“请求来源必须是本站点”呢?换句话说,控制referer不能为空,只要是空就重定向。


因为在浏览器的地址栏中直接输入这个图片的url,然后回车,发起的请求是没有referer字段的,在这种场景下如果还是返回gitee的logo,就显得不太合理了。



图片的url:https://images.gitee.com/uploads/images/2022/0326/155444_dc9923a4_10659337.jpeg



图片看不到了,现在怎么办


如果你的个人搭建的博客里面用了很多存在gitee的图片,你可以在html的head部分加上这样一行


<meta name="referrer" content="no-referrer" />


或者


<img referrer="no-referrer|origin|unsafe-url" src="{item.src}"/>


来阻止请求因带上站点来源而被重定向成gitee的logo。


如果你是博客的访问者,可以借助一个chrome小插件ModHeader,把referer给“擦掉”



这样第三方站点就可以正常访问啦~


1-2.png


结语


上面提到的解决方式只是开个玩笑,临时恢复使用可以。但还是要慢慢把图片迁移到自己的服务器才最可靠。


作者:前端私教年年
来源:https://juejin.cn/post/7079705713781506079 收起阅读 »

【集成攻略】手把手教你环信对接离线推送,再搞不定把你头打掉

前提条件1.macOS系统,安装了xcode,并且配置好了cocoapods第三方管理工具2.有苹果开发者账号3.有环信开发者账号(注册地址:https://console.easemob.com/user/register)在苹果开发者中心创建项目,注册推送...
继续阅读 »

前提条件

1.macOS系统,安装了xcode,并且配置好了cocoapods第三方管理工具

2.有苹果开发者账号

3.有环信开发者账号

(注册地址:https://console.easemob.com/user/register


在苹果开发者中心创建项目,注册推送证书.

1.登录苹果开发者中心.

https://developer.apple.com/

(请用自己的苹果开发者账号)





2.苹果开发者中心创建 - Identifiers.

(name - empushdemo )

(identifier - com.yyytp.empushdemo )











3.钥匙串 - 从证书颁发机构请求证书

(本机证书)
















4.针对刚创建的bundle id开通并注册Certificates push 证书

(注册 可以在开发和生产双环境下使用的推送证书)













5.安装证书到本机,并导出 push - p12 

(这里需要格外注意操作步骤!不能展开!!!要闭合的状态导出!!!)

(因为申领的证书是双环境的,所以导出的p12文件直接复制成双份即可)

(开发证书名称 : yyytp_empush_apns_dev)

(生产证书名称 : yyytp_empush_apns_pro)

(密码 : 不告诉你)




==========
提示:解决证书不受信任的问题

如果在《钥匙串访问》中出现"证书不受信任"的警告时,可以去苹果官方网站下载G4证书,并双击打开即可

“证书不受信任”的图片样例


苹果官方网站链接:

https://www.apple.com/certificateauthority/

苹果官方网站需要下载的G4证书截图说明:


苹果官方解释:
苹果全球开发者关系中级证书的版本将于2023年2月7日到期,续订版本将于2030年2月20日到期。更新后的版本用于签署2021 1月28日之后颁发的新软件签名证书。剩余服务证书将于2022年1月27日更新。
为苹果平台开发的苹果开发者计划和苹果开发者企业计划的成员需要下载更新的证书,并遵循以下说明。
苹果开发者企业计划的成员需要在所有使用2020年9月1日之后生成的企业iOS分发证书进行代码签名的机器上安装续订的证书。
对于2021 1月28日之后生成的所有软件签名证书,由在Xcode中拥有个人帐户的开发人员和iOS大学开发人员计划成员提供的,也需要进行此更新。
新的中间证书由Xcode 11.4.1或更高版本自动下载,可在证书颁发机构页面上下载。通过验证过期日期设置为2030,确认安装了正确的中间证书。

注:本解决方案来自简书平台的博主AndyLiYL
原文链接:
https://www.jianshu.com/p/2697ed4f6e41

==========


后续补充:导出时必须使用[英文+数字+下划线]字符集内字符.不能使用中文和其他特殊符号







以上是在苹果开发者中心做了两件事

1.创建了bundleid为com.yyytp.empushdemo的app

2.创建推送证书 apns-2.cer 并导出了 (.p12) 证书,复制为2份,分别命名为 yyytp_empush_apns_dev yyytp_empush_apns_pro 密码是 123456

其中两份p12证书内容是完全一样的,只是命名不同,都适用于开发和生产环境,不过为了后期导入环信后台时方便辨识


===========分割线===========================


在环信console后台创建一个appkey,顺带创建一个测试username


1.登录环信console后台:https://console.easemob.com/user/login




2.创建appkey

(app_name : showpushdemo )





3.创建一个测试账号

(测试账号 : emtest 密码 1 )







========分割线=======================


在环信console后台中上传推送证书(.p12文件)

注意!!!是在刚才创建的appkey(1168171101115760#showpushdemo)下创建证书!!!

(这里需要注意的细节是:证书名不能有中文和其他特殊符号!!! 建议 字母 + 下划线)

(所以我会采用 yyytp_empush_apns_dev / yyytp_empush_apns_pro 这样的命名方式)





最终效果




=========分割线=======================


在代码中实现

1.创建项目

em_push_showdemo

2.集成环信SDK

pod 'HyphenateChat' , '3.9.0'




3.配置项目





4.代码部分如下:





下面代码是AppDelegate.m的所有代码,可直接复制粘贴

下面代码是AppDelegate.m的所有代码,可直接复制粘贴

下面代码是AppDelegate.m的所有代码,可直接复制粘贴


//
// AppDelegate.m
// em_push_showdemo
//
// Created by flower on 2022/3/14.
//

#import "AppDelegate.h"
#import
#import

@interface AppDelegate ()

@end

@implementation AppDelegate


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
/*
1.注册环信SDK
2.注册推送
注册成功之后 绑定至环信SDK
3.登录账号
*/

[self _registerEMCHAT];
[self _registerSysPush];
[self _loginEMCHAT];
return YES;
}

- (void)_registerEMCHAT{
EMOptions *options = [EMOptions optionsWithAppkey:@"1168171101115760#showpushdemo"];
options.apnsCertName = @"yyytp_empush_apns_dev";
options.isAutoLogin = false;
options.usingHttpsOnly = true;
[EMClient.sharedClient initializeSDKWithOptions:options];
}

- (void)_registerSysPush{
[UNUserNotificationCenter.currentNotificationCenter
requestAuthorizationWithOptions:
UNAuthorizationOptionBadge|
UNAuthorizationOptionSound|
UNAuthorizationOptionAlert
completionHandler:^(BOOL granted, NSError * _Nullable error) {
if (granted) {
dispatch_async(dispatch_get_main_queue(), ^{
[UIApplication.sharedApplication registerForRemoteNotifications];
});
}
}];
}

- (void)_loginEMCHAT{
[EMClient.sharedClient loginWithUsername:@"emtest" password:@"1" completion:^(NSString *aUsername, EMError *aError) {
if (aError) {
NSLog(@"登录失败");
}else{
NSLog(@"登录成功");
//下面这个updatePushDisplayStyle是设置显示效果,有两种显示效果可以设置.详情可查看枚举值(EMPushDisplayStyleSimpleBanner)的定义
[EMClient.sharedClient.pushManager updatePushDisplayStyle:EMPushDisplayStyleSimpleBanner completion:^(EMError * _Nonnull aError) {
}];
}
}];
}

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken{
NSLog(@"绑定成功");
dispatch_async(dispatch_get_main_queue(), ^{
[EMClient.sharedClient bindDeviceToken:deviceToken];
});
}

- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error{
}



@end


4.运行至手机,运行完成后,退出APP,发送消息测试推送.

收起阅读 »

跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget

前言跟我学flutter系列:跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制企业...
继续阅读 »

前言

跟我学flutter系列:
跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin
跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate
跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制
企业级篇目:
跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统
跟我学企业级flutter项目:dio网络框架增加公共请求参数&header
跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层
跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview
跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget

如何开发一款易用的,并且可以扩展的空页面呢?那么今天我将带领大家手把手开发一款可扩展的空页面。

开发前注意事项

1、定义好空页面状态 2、可扩展思想(用抽象或基类替代实体) 3、抽离出空页面的结构

空页面展示

在这里插入图片描述

开始搭建

一、页面分析

空页面需要元素有:

  1. 展示图片
  2. 展示文案
  3. 展示刷新按钮

页面功能点:

  1. 文案可自定义
  2. 图片可自定义
  3. 按钮可隐藏

wiget作用范围:

  1. 可包裹其他widget
  2. 不包裹其他widget

二、定义状态

2.1 几种状态

enum EmptyStatus {
fail, //失败视图
loading, //加载视图
nodata, //没有数据视图
none //没有状态
}

没有状态该空页面就隐藏掉

2.2 空页面刷新回调

abstract class IEmptyRefresh{

void pressedReload();

}

2.3 定义copy类(复用做准备)&定义空接口(抽离要扩展的方法)

abstract class Copyable {
T copy();
}
abstract class IEmpty implements Copyable{
IEmptyRefresh? iEmptyRefresh;
Widget? diyImage; // 自定义图片替换
Widget? diyText;// 自定义文案替换
Widget? image();

Widget? text();

Widget? refresh();
}

2.4 空页面实现类

默认加载中页面

class DefaultLodingPage extends IEmpty{

@override
Widget? text() {
return diyText??Text(
LibEmptyManager.instance.libEmptyPageLoding,
style: TextStyle(fontSize: LibEmptyManager.instance.textSize, color: AppTheme.instance.textColor()),
);
}

@override
Widget? image() {
return null;
}

@override
Widget? refresh() => null;

@override
IEmpty copy() {
return DefaultLodingPage()
..diyImage = diyImage
..diyText = diyText
..iEmptyRefresh=iEmptyRefresh;
}


}
默认空页面

class DefaultEmptyPage extends IEmpty{

@override
Widget? text() {
return diyText??Text(
LibEmptyManager.instance.libEmptyPageNoData,
style: TextStyle(fontSize: LibEmptyManager.instance.textSize, color: AppTheme.instance.textColor()),
);
}

@override
Widget? image() {
return Padding(
padding: const EdgeInsets.only(bottom: 20),
child: diyImage??Icon(LibEmptyManager.instance.imageNoData,color: AppTheme.instance.imageColor(),size: LibEmptyManager.instance.imageSize,),
);
}

@override
Widget? refresh() => null;

@override
IEmpty copy() {
return DefaultEmptyPage()
..diyImage = diyImage
..diyText = diyText
..iEmptyRefresh=iEmptyRefresh;;
}


}
默认网络失效页

class DefaultNetWorkError extends IEmpty {
@override
Widget? text() {
return diyText??Text(
LibEmptyManager.instance.libEmptyPageNetError,
style: TextStyle(fontSize: LibEmptyManager.instance.textSize, color: AppTheme.instance.textColor()),
);
}

@override
Widget? image() {
return Padding(
padding: const EdgeInsets.only(bottom: 20),
child: diyImage??Icon(LibEmptyManager.instance.imageNetWork,color: AppTheme.instance.imageColor(),size: LibEmptyManager.instance.imageSize,),
);
}

@override
Widget? refresh() {
return Padding(
padding: const EdgeInsets.only(top: 20),
child: Padding(
padding: const EdgeInsets.only(left: 20,right: 20),
child: ElevatedButton(onPressed: () => iEmptyRefresh?.pressedReload(),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(AppTheme.instance.btnBackColor()),
shape: MaterialStateProperty.all(const StadiumBorder()),
)
, child: Text(LibEmptyManager.instance.libRefresh,style: TextStyle(fontSize: LibEmptyManager.instance.libRefreshSize,color: AppTheme.instance.btnTextColor())),),
),
);
}

@override
IEmpty copy() {
return DefaultNetWorkError()
..diyImage = diyImage
..diyText = diyText
..iEmptyRefresh=iEmptyRefresh;;
}
}

2.5 空页面管理类

可进行外部配置


class LibEmptyManager{
IEmpty emptyPage = DefaultEmptyPage();
IEmpty loadingPage = DefaultLodingPage();
IEmpty netWorkError = DefaultNetWorkError();

late LibEmptyConfig libEmptyConfig;

LibEmptyManager._();

static final LibEmptyManager _instance = LibEmptyManager._();

static LibEmptyManager get instance {
return _instance;
}

2.6 核心逻辑

判断状态,并进行类型拷贝,并增加自定义参数

switch(widget.layoutType){
case EmptyStatus.none:
visable = true;
break;
// return widget.child;
case EmptyStatus.fail:
iEmpty = LibEmptyManager.instance.netWorkError.copy()
..diyText = widget.networkText
..diyImage = widget.networkImage
;
break;
case EmptyStatus.nodata:
iEmpty = LibEmptyManager.instance.emptyPage.copy()
..diyText = widget.emptyText
..diyImage = widget.emptyImage
;
break;
case EmptyStatus.loading:
iEmpty = LibEmptyManager.instance.loadingPage;
break;
default:
iEmpty = LibEmptyManager.instance.emptyPage.copy()
..diyText = widget.emptyText
..diyImage = widget.emptyImage
;
}

如果是包裹类型需要stack进行包装

return Stack(
children: [
Offstage(
offstage: !visable,
child: widget.child,
),
Offstage(
offstage: visable,
child: Container(
width: double.infinity,
color: AppTheme.instance.backgroundColor(),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: _listEmpty(iEmpty),
),
),),
],
);

判断是否有网,有网的话,就刷新,没网的话,就提示


@override
void pressedReload() async
{
bool isConnectNetWork = await isConnected();
if(isConnectNetWork){
widget.refresh.call();
}else{
TipToast.instance.tip(LibLocalizations.getLibString().libNetWorkNoConnect!,tipType: TipType.error);
}
}

// 是否有网
Future isConnected() async {
var connectivityResult = await (Connectivity().checkConnectivity());
return connectivityResult != ConnectivityResult.none;
}

组装empty


List _listEmpty(IEmpty? iEmpty) {
List tempEmpty = [];
if(iEmpty!=null){
Widget? image = iEmpty.image();
Widget? text = iEmpty.text();
Widget? refresh = iEmpty.refresh();
if(image!=null){
tempEmpty.add(image);
}
if(text!=null){
tempEmpty.add(text);
}
if(refresh!=null){
tempEmpty.add(refresh);
}

}
return tempEmpty;
}

三、空页面widget实现完整代码

class LibEmptyView extends StatefulWidget{
Widget? child;
EmptyStatus layoutType;
VoidCallback refresh;


Widget? networkImage;Widget? networkText;
Widget? emptyImage;Widget? emptyText;

LibEmptyView({Key? key, this.child,required this.refresh,required this.layoutType,this.networkImage,this.networkText, this.emptyImage,this.emptyText}) : super(key: key);

@override
State createState() => _LibEmptyViewState();

}

class _LibEmptyViewState extends State implements IEmptyRefresh{
//控制器

@override
Widget build(BuildContext context) {
IEmpty? iEmpty;
bool visable = false;
switch(widget.layoutType){
case EmptyStatus.none:
visable = true;
break;
case EmptyStatus.fail:
iEmpty = LibEmptyManager.instance.netWorkError.copy()
..diyText = widget.networkText
..diyImage = widget.networkImage
;
break;
case EmptyStatus.nodata:
iEmpty = LibEmptyManager.instance.emptyPage.copy()
..diyText = widget.emptyText
..diyImage = widget.emptyImage
;
break;
case EmptyStatus.loading:
iEmpty = LibEmptyManager.instance.loadingPage;
break;
default:
iEmpty = LibEmptyManager.instance.emptyPage.copy()
..diyText = widget.emptyText
..diyImage = widget.emptyImage
;
}
iEmpty?.iEmptyRefresh = this;



return Stack(
children: [
Offstage(
offstage: !visable,
child: widget.child,
),
Offstage(
offstage: visable,
child: Container(
width: double.infinity,
color: AppTheme.instance.backgroundColor(),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: _listEmpty(iEmpty),
),
),),
],
);
}

@override
void pressedReload() async{
bool isConnectNetWork = await isConnected();
if(isConnectNetWork){
widget.refresh.call();
}else{
TipToast.instance.tip(LibLocalizations.getLibString().libNetWorkNoConnect!,tipType: TipType.error);
}
}


// 是否有网
Future isConnected() async {
var connectivityResult = await (Connectivity().checkConnectivity());
return connectivityResult != ConnectivityResult.none;
}
}

List _listEmpty(IEmpty? iEmpty) {
List tempEmpty = [];
if(iEmpty!=null){
Widget? image = iEmpty.image();
Widget? text = iEmpty.text();
Widget? refresh = iEmpty.refresh();
if(image!=null){
tempEmpty.add(image);
}
if(text!=null){
tempEmpty.add(text);
}
if(refresh!=null){
tempEmpty.add(refresh);
}

}
return tempEmpty;
}

四、空页面widget使用代码

包裹使用 (代码中的webview封装参见:跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview

LibEmptyView(
layoutType: status,
refresh: () {

status = EmptyStatus.none;
_innerWebPageController.reload();

},

child: InnerWebPage(widget.url,titleCallBack: (title){
setState(() {
urlTitle = title;
});
},javascriptChannels: widget._javascriptChannels,urlIntercept: widget._urlIntercept,onInnerWebPageCreated: (innerWebPageController){
_innerWebPageController = innerWebPageController;
widget._javascriptChannels?.webPageCallBack = webPageCallBack;
widget._urlIntercept?.webPageCallBack = webPageCallBack;
},onWebResourceError: (error){
setState(() {
status = EmptyStatus.fail;
});
},),
),
));

非包裹使用

if(_status == EmptyStatus.none){
return _listViewUi.call(_allReportItems);
}else{
var empty = LibEmptyView(
layoutType: _status,
refresh: () {
_status = EmptyStatus.loading;
LibLoading.show();
_refreshCenter.refreshData();
},networkImage: networkImage,networkText: networkText,emptyImage: emptyImage,emptyText: emptyText,);
if(builder!=null){
return builder.call(context,empty);
}else{
return empty;
}
}

感谢大家阅读我的文章

收起阅读 »

Flutter桌面端开发——复制和粘贴内容

复制和粘贴这个功能,一般系统都自带,简单的按几个键就能完成。但是有时候我们想要自己在应用中集成这个功能,或者想在用户复制文字后不使用粘贴操作,就让复制的内容直接出现在我们的应用中。想要实现该功能,就可以用我今天介绍的几个插件。 screen_capturer ...
继续阅读 »

复制和粘贴这个功能,一般系统都自带,简单的按几个键就能完成。但是有时候我们想要自己在应用中集成这个功能,或者想在用户复制文字后不使用粘贴操作,就让复制的内容直接出现在我们的应用中。想要实现该功能,就可以用我今天介绍的几个插件。


screen_capturer


这个方法是用来截取屏幕的。本来想写一期介绍截屏插件的,但是找了一圈只找到这个适用于桌面端,只写这一个插件篇幅又太短,所以直接加了进来。


安装🛠


点击screen_capturer获取最新版本。以下是在编写本文章时的最新版本:


screen_capturer: ^0.1.0

使用🥟


该插件的主体是 ScreenCapturer.instance ,其下有3个方法:isAccessAllowed 、requestAccess 和 capture。


isAccessAllowed 和 requestAccess仅在 macOS中适用,分别用来检测和申请截图的权限。


await ScreenCapturer.instance.requestAccess();  // 申请权限
bool _isAllowed = await ScreenCapturer.instance.isAccessAllowed (); // 检测是否拥有权限

我们截图的目的是把图片显示出来,所以在正式截图前先定义个图片路径的参数:


String _image;

接下来介绍截图的主要方法 capture,该方法可以传递2个参数:



  • String? imagePath:该属性为必填,传递一个图片保存的路径和名称

  • bool silent:设置是否开启截屏的提示音


String _imageName = '${DateTime.now().millisecondsSinceEpoch}.png';  // 设置图片名称
String _imagePath = 'C:\\Users\\ilgnefz\\Pictures\\$_imageName'; // 设置图片保存的路径
CapturedData? _capturedData = await ScreenCapturer.instance.capture(imagePath: _imagePath);
if (_capturedData != null) {
_image = _capturedData.imagePath;
setState(() {});
} else {
BotToast.showText(text: '截图被取消了');
}

1


通过运行可以发现,这里调用的其实是系统的截图功能,然后将截取的图片进行了保存。和windows自带的截图又有些差别,自带的只会将截图保存到剪切板中。


screen_text_extractor


安装🛠


点击screen_text_extractor获取最新版本。以下是在编写本文章时的最新版本:


screen_text_extractor: ^0.1.0

使用🥟


该插件的主体是 ScreenTextExtractor.instance ,其下有7个方法:



  1. isAccessAllowed:检测是否有进行相关操作的权限,仅macOS

  2. requestAccess:申请进行相关操作的权限,仅macOS

  3. extractFromClipboard:从剪切板提取内容

  4. extractFromScreenSelection:从选择的屏幕提取

  5. simulateCtrlCKeyPress:模拟 Ctrl + c ,返回一个布尔值


前面4个仅macOS使用的方法和screen_capturer一样,这里就不多赘述。后面3个方法将会返回一个 Future 的 ExtractedData 对象。


我们先定义一个String对象用来显示获取到的内容:


String _text = '获取的内容将会在这里🤪';

获取剪切板内容


ExtractedData data = await ScreenTextExtractor.instance.extractFromClipboard();
_text = data.text;
setState((){});

我们先在 windows 中按 windows键 + v 来调出剪切板,清空


无标题


使用该方法看一下:


1


我们将会得到一个空白的内容。为了更好的用户体验,我们可以添加个条件。


if (data.text!.isEmpty) {
BotToast.showText(text: '剪切板什么都没有🤨');
} else {
_text = data.text!;
setState(() {});
}

我们现在复制一段内容:


无标题


然后看看效果:


2


获取成功😀,但是剪切板除了能存储文本,还是能存储图片的。


1


但是ExtractedData只有个text属性,我们来看下会发生什么:


3


直接为空了😶


获取选区内容


ExtractedData data = await ScreenTextExtractor.instance.extractFromScreenSelection(
useAccessibilityAPIFirst: false, // 使用辅助功能API,仅macOS
);
if (data.text!.isEmpty) {
BotToast.showText(text: '剪切板什么都没有🤨');
} else {
_text = data.text!;
setState(() {});
}

👻该方法如果是在windows端,返回的就是extractFromClipboard()方法的结果,在macOS端和Linux端暂时无法演示😪


pasteboard


安装🛠


点击pasteboard获取最新版本。以下是在编写本文章时的最新版本:


pasteboard: ^0.0.2

使用🥟


该插件中的 Pasteboard 对象一共拥有4个方法:



  • image:复制图片

  • file:复制文件/文本

  • writeImage:粘贴图片

  • writeFile:粘贴文件/文本


复制粘贴文本


当然,第一步先定义一个用来存储结果的变量:


String _text = '还没粘贴任何内容';

定义一个文本控制器,用来获取输入的内容:


final TextEditingController _controller = TextEditingController();

接下来使用 pasteboard 来实现复制和粘贴的功能:




  • 复制文本


    void _copyText() async {
    if (_controller.text.isEmpty) {
    BotToast.showText(text: '啥都没输入,你要我复制什么🥴');
    } else {
    final lines = const LineSplitter().convert(_controller.text);
    await Pasteboard.writeFiles(lines);
    }
    }



  • 粘贴文本


    void _pastText() async {
    final results = await Pasteboard.files();
    if (results.isNotEmpty) {
    _text = result.toString();
    setState(() {});
    } else {
    BotToast.showText(text: '我什么都不能给你,因为我也咩有😭');
    }
    }



我们先来试一下,不用复制直接直接粘贴会发生什么。此时我的剪切板有一条内容:


无标题


来看看效果:


1


我们可以发现,它并不能读取我们剪切板的内容。试下复制再粘贴:


2


通过测试可以知道,最终的结果是一个数组。我们再来看看剪切板有没有记录:


无标题


这里其实用的是上面同一张图,因为没有变化所以就没再截图了。


通过以上内容,我们可以发现,pasteboard 的复制粘贴是和系统隔开的。


复制粘贴文件


其实代码可以不用修改,但是为了更好的显示,我们还是修改以下:


void _pastText() async {
final results = await Pasteboard.files();
if (results.isNotEmpty) {
_text = '';
for (final result in results) {
_text += '$result\n';
}
setState(() {});
} else {
BotToast.showText(text: '我什么都不能给你,因为我也咩有😭');
}
}

在这里,我使用了 url_launcher 插件,用来打开系统的文件浏览器。代码如下:


void _openExplorer() async {
const _filePath = r'C:\Users\ilgnefz\Pictures';
final Uri _uri = Uri.file(_filePath);
await launch(_uri.toString());
}

来看看效果:


image


图片本质上也是文件,可以直接使用上面的方法进行复制粘贴。所以关于图片的方法就不讲解了


(🤫ps: 其实是我使用官方例子的方法,用Base64图片进行测试,发现无法得到想要的结果。使用了官方的例子也是一样。复制图片的方法需要传递一个Uint8List参数,虽然可以使用其他方法转换,但是就变得麻烦了。以后我会出一篇关于用 CustomPaint 绘制图片的文章,里面会用到将图片转换成Uint8List对象的方法)。


clipboard


安装🛠


点击clipboard获取最新版本。以下是在编写本文章时的最新版本:


clipboard: ^0.1.3

使用🥟


该插件拥有4个方法:



  • controlC:模仿 cttr + c 键,复制

  • controlC:模仿 cttr + v 键,粘贴

  • copy:复制

  • paste:粘贴


先来看看前面两个方法:


void _useCtrC() async {
if (_controller.text.isEmpty) {
BotToast.showText(text: '啥都没输入,你要我复制什么🥴');
} else {
await FlutterClipboard.controlC(_controller.text);
}
}

void _useCtrV() async {
ClipboardData result = await FlutterClipboard.controlV();
_text = result.text.toString();
setState(() {});
}

使用 controlV 会返回一个 ClipboardData 对象。


4


后面两个方法和前面的唯一不同,就是返回的是一个 String 对象:


void _useCopy() async {
if (_controller.text.isEmpty) {
BotToast.showText(text: '啥都没输入,你要我复制什么🥴');
} else {
await FlutterClipboard.copy(_controller.text);
}
}

void _usePaste() async {
_text = await FlutterClipboard.paste();
setState(() {});
}

5


我们打开系统的剪切板可以发现,以上复制的内容都会被记录。我们试一下不按复制看能不能直接读取剪切板的信息进行粘贴:


6


试试 paste 方法:


7


🛫OK,以上就是这篇文章的全部内容,仅针对插件的当前版本,并不能保证适用于以后插件用法的更新迭代。


最后,感谢 leanflutterMixin Network 两个团队还有 samuelezedi 对以上插件的开发和维护😁。本应用代码已上传至 githubgitee,有需要的可以下载下来查看学习。


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

Flutter好用的轮子推荐03:一套精美实用的内置动画弹窗组件

前言 Flutter 是 Google 开源的应用开发框架,仅通过一套代码就能构建支持Android、iOS、Windows、Linux等多平台的应用。Flutter的性能非常高,拥有120fps的刷新率,也是目前非常流行的跨平台UI开发框架。 本专栏为大家收...
继续阅读 »

前言


Flutter 是 Google 开源的应用开发框架,仅通过一套代码就能构建支持Android、iOS、Windows、Linux等多平台的应用。Flutter的性能非常高,拥有120fps的刷新率,也是目前非常流行的跨平台UI开发框架。


本专栏为大家收集了Github上近70个优秀开源库,后续也将持续更新。希望可以帮助大家提升搬砖效率,同时祝愿Flutter的生态越来越完善🎉🎉。


正文


一、🚀 轮子介绍



  • 名称:awesome_dialog

  • 概述:一个简单易用的内置动画弹窗

  • 出版商:marcos930807@gmail.com

  • 仓库地址:awesomeDialogs

  • 推荐指数: ⭐️⭐️⭐️⭐️

  • 常用指数: ⭐️⭐️⭐️⭐️

  • 效果预览:


gif.gif


二、⚙️ 安装及使用


dependencies:
awesome_dialog: ^2.1.2

import 'package:awesome_dialog/awesome_dialog.dart';

三、🔧 常用属性






















































































































































































属性类型描述
dialogTypeDialogType设置弹窗类型
customHeaderWidget设置自定义标题(如果设置了,DiaologType将被忽略。)
widthdouble弹窗最大宽度
titleString弹窗标题
descString弹窗描述文本
bodyWidget弹窗主体,如果设置了此属性,标题和描述将被忽略。
contextBuildContext@required
btnOkTextString确认按钮的文本
btnOkIconIconData确认按钮的图标
btnOkOnPressFunction确认按钮事件
btnOkColorColor确认按钮颜色
btnOkWidget创建自定义按钮,以上确认按钮相关属性将被忽略
btnCancelTextString取消按钮的文本
btnCancelIconIconData取消按钮的图标
btnCancelOnPressFunction取消按钮事件
btnCancelColorColor取消按钮颜色
btnCancelWidget创建自定义按钮,以上取消按钮相关属性将被忽略
buttonsBorderRadiusBorderRadiusGeometry按钮圆角
dismissOnTouchOutsidebool点击外部消失
onDissmissCallbackFunction弹窗关闭回调
animTypeAnimType动画类型
aligmentAlignmentGeometry弹出方式
useRootNavigatorbool使用根导航控制器而不是当前根导航控制器,可处理跨界面关闭弹窗。
headerAnimationLoopbool标题动画是否循环播放
paddingEdgeInsetsGeometry弹窗内边距
autoHideDuration自动隐藏时间
keyboardAwarebool键盘弹出内容被遮挡时是否跟随移动
dismissOnBackKeyPressbool控制弹窗是否可以通过关闭按钮消失
buttonsBorderRadiusBorderRadiusGeometry按钮圆角
buttonsTextStyleTextStyle按钮文字风格
showCloseIconbool是否显示关闭按钮
closeIconWidget关闭按钮图标
dialogBackgroundColorColor弹窗背景色
borderSideBorderSide整个弹窗形状

四、🗂 示例


1.带有点击动画的按钮


animatedButton-2.gif


AnimatedButton(
color: Colors.cyan,
text: '这是一个带有点击动画的按钮',
pressEvent: () {},
)

2.固定宽度并带有确认 / 取消按钮的提示框


fixedWidthAndButtons.gif


AnimatedButton(
text: '固定宽度并带有确认 / 取消按钮的提示框',
pressEvent: () {
AwesomeDialog(
context: context,
dialogType: DialogType.INFO_REVERSED,
borderSide: const BorderSide(
color: Colors.green,
width: 2,
),
width: 380,
buttonsBorderRadius: const BorderRadius.all(
Radius.circular(2),
),
btnCancelText: '不予理会',
btnOkText: '冲啊!',
headerAnimationLoop: false,
animType: AnimType.BOTTOMSLIDE,
title: '提示',
desc: '一个1级bug向你发起挑衅,是否迎战?',
showCloseIcon: true,
btnCancelOnPress: () {},
btnOkOnPress: () {},
).show();
});

3.自定义按钮样式的问题对话框


questionDialogWithCustomButtons.gif


AnimatedButton(
color: Colors.orange[700],
text: '具有自定义按钮样式的问题对话框',
pressEvent: () {
AwesomeDialog(
context: context,
dialogType: DialogType.QUESTION,
headerAnimationLoop: false,
animType: AnimType.BOTTOMSLIDE,
title: '触发额外剧情',
desc: '发现一名晕倒在草丛的路人,你会?',
buttonsTextStyle: const TextStyle(color: Colors.black),
btnCancelText: '拿走他的钱袋',
btnOkText: '救助',
showCloseIcon: true,
btnCancelOnPress: () {},
btnOkOnPress: () {},
).show();
});

4.无按钮的信息提示框


noHeaderDialog.gif


AnimatedButton(
color: Colors.grey,
text: '无按钮的信息提示框',
pressEvent: () {
AwesomeDialog(
context: context,
headerAnimationLoop: true,
animType: AnimType.BOTTOMSLIDE,
title: '提示',
desc:
'你救下路人,意外发现他是一位精通Flutter的满级大佬,大佬为了向你表示感谢,赠送你了全套Flutter的学习资料...',
).show();
});

5.警示框


warningDialog.gif


AnimatedButton(
color: Colors.orange,
text: '警示框',
pressEvent: () {
AwesomeDialog(
context: context,
dialogType: DialogType.WARNING,
headerAnimationLoop: false,
animType: AnimType.TOPSLIDE,
showCloseIcon: true,
closeIcon: const Icon(Icons.close_fullscreen_outlined),
title: '警告',
desc: '意外发现bug的窝点,你准备?',
btnCancelOnPress: () {},
onDissmissCallback: (type) {
debugPrint('Dialog Dissmiss from callback $type');
},
btnCancelText: '暂且撤退',
btnOkText: '发起战斗',
btnOkOnPress: () {},
).show();
});

6.错误提示框


errorDialog.gif


AnimatedButton(
color: Colors.red,
text: '错误提示框',
pressEvent: () {
AwesomeDialog(
context: context,
dialogType: DialogType.ERROR,
animType: AnimType.RIGHSLIDE,
headerAnimationLoop: true,
title: '挑战失败',
desc: '你寡不敌众,败下阵来,(回到出生点后,拿出大佬赠送的全套学习资料,立志学成后报仇血恨... )',
btnOkOnPress: () {},
btnOkIcon: Icons.cancel,
btnOkColor: Colors.red,
).show();
});

7.成功提示框


successDialog.gif


AnimatedButton(
color: Colors.green,
text: '成功提示框',
pressEvent: () {
AwesomeDialog(
context: context,
animType: AnimType.LEFTSLIDE,
headerAnimationLoop: false,
dialogType: DialogType.SUCCES,
showCloseIcon: true,
title: '挑战成功',
desc: '经过三天三夜的苦战,你成功消灭了所有的bug',
btnOkOnPress: () {
debugPrint('OnClcik');
},
btnOkIcon: Icons.check_circle,
onDissmissCallback: (type) {
debugPrint('Dialog Dissmiss from callback $type');
},
).show();
});

8.不带顶部动画的弹窗


noHeaderDialog.gif


AnimatedButton(
color: Colors.cyan,
text: '不带顶部动画的弹窗',
pressEvent: () {
AwesomeDialog(
context: context,
headerAnimationLoop: false,
dialogType: DialogType.NO_HEADER,
title: 'No Header',
desc:'Dialog description here...',
btnOkOnPress: () {
debugPrint('OnClcik');
},
btnOkIcon: Icons.check_circle,
).show();
});

9.自定义内容弹窗


customBodyDialog.gif


AnimatedButton(
color: Colors.purple,
text: '自定义内容弹窗',
pressEvent: () {
AwesomeDialog(
context: context,
animType: AnimType.SCALE,
dialogType: DialogType.INFO,
body: const Center(
child: Text(
'If the body is specified, then title and description will be ignored, this allows to further customize the dialogue.',
style: TextStyle(fontStyle: FontStyle.italic),
),
),
title: 'This is Ignored',
desc: 'This is also Ignored',
).show();
});

10.自动隐藏弹窗


autoHideDialog.gif


AnimatedButton(
color: Colors.grey,
text: '自动隐藏弹窗',
pressEvent: () {
AwesomeDialog(
context: context,
dialogType: DialogType.INFO,
animType: AnimType.SCALE,
title: 'Auto Hide Dialog',
desc: 'AutoHide after 2 seconds',
autoHide: const Duration(seconds: 2),
).show();
});

11.测试弹窗


testingDialog.gif


AnimatedButton(
color: Colors.blue,
text: '测试弹窗',
pressEvent: () {
AwesomeDialog(
context: context,
keyboardAware: true,
dismissOnBackKeyPress: false,
dialogType: DialogType.WARNING,
animType: AnimType.BOTTOMSLIDE,
btnCancelText: "Cancel Order",
btnOkText: "Yes, I will pay",
title: 'Continue to pay?',
desc:'Please confirm that you will pay 3000 INR within 30 mins. Creating orders without paying will create penalty charges, and your account may be disabled.',
btnCancelOnPress: () {},
btnOkOnPress: () {},
).show();
});

12.文本输入弹窗


bodyWithInput.gif


AnimatedButton(
color: Colors.blueGrey,
text: '带有文本输入框的弹窗',
pressEvent: () {
late AwesomeDialog dialog;
dialog = AwesomeDialog(
context: context,
animType: AnimType.SCALE,
dialogType: DialogType.INFO,
keyboardAware: true,
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: <Widget>[
Text('Form Data',
style: Theme.of(context).textTheme.headline6,),
const SizedBox(height: 10,),
Material(
elevation: 0,
color: Colors.blueGrey.withAlpha(40),
child: TextFormField(
autofocus: true,
minLines: 1,
decoration: const InputDecoration(
border: InputBorder.none,
labelText: 'Title',
prefixIcon: Icon(Icons.text_fields),
),
),
),
const SizedBox(height: 10,),
Material(
elevation: 0,
color: Colors.blueGrey.withAlpha(40),
child: TextFormField(
autofocus: true,
keyboardType: TextInputType.multiline,
minLines: 2,
maxLines: null,
decoration: const InputDecoration(
border: InputBorder.none,
labelText: 'Description',
prefixIcon: Icon(Icons.text_fields),
),
),
),
const SizedBox(height: 10,),
AnimatedButton(
isFixedHeight: false,
text: 'Close',
pressEvent: () {
dialog.dismiss();
},
)
],),
),
)..show();
});

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

从0-1背包问题看动态规划的几种方式

0-1 knapsack Problem Statement Given the weights and profits of ‘N’ items, we are asked to put these items in a knapsack that has ...
继续阅读 »

0-1 knapsack


Problem Statement


Given the weights and profits of ‘N’ items, we are asked to put these items in a knapsack that has a capacity ‘C’. The goal is to get the maximum profit from the items in the knapsack. Each item can only be selected once, as we don’t have multiple quantities of any item.


Items: { Apple, Orange, Banana, Melon }
Weights: { 2, 3, 1, 4 }
Profits: { 4, 5, 3, 7 }
Knapsack capacity: 5
Output: 10

暴力解法


每个 Item 都可以放或者不放,可以对每个元素一次进行递归


export function bruteforce(
profits: number[],
weights: number[],
capacity: number
) {
const recursive = (i: number, c: number, p: number): number => {
if (i >= profits.length) return p;

return Math.max(
c + weights[i] > capacity
? p
: recursive(i + 1, c + weights[i], p + profits[i]),
recursive(i + 1, c, p)
);
};

const ans = recursive(0, 0, 0);
return ans;
}

时间复杂度:O(2n)


空间复杂度:O(n)


Top-Down DP


稍微变一个 recursive 的逻辑,recursive 函数表示,把 0~i 范围内的 Item 装入 capacity 为 c 的背包中,能够获得的最大的 profit


image.png


可以看到有重复的分支,我们需要做的就是在这个基础上记忆


export function topDown(
profits: number[],
weights: number[],
capacity: number
) {
const dp: Record<string, number> = {};

const recursive = (i: number, c: number, p: number): number => {
if (i >= profits.length) return p;

const key = `${i}_${c}`;

if (dp[key]) {
return dp[key];
}

const ans = Math.max(
c - weights[i] >= 0
? recursive(i + 1, c - weights[i], p + profits[i])
: p,
recursive(i + 1, c, p)
);

return (dp[key] = ans);
};

const ans = recursive(0, capacity, 0);
return ans;
}

时间复杂度:O(n * c),因为记忆化后,最多有 n * c 个子问题


空间复杂度:O(n * c + n)


Bottom-Up DP


dp 记录对于前 i 个元素,当 capacity 为 c 时,能获得的最大 profit,那么


dp[i][c] = max (
// 不取当前元素
dp[i-1][c],
// 取当前元素
profits[i] + dp[i-1][c-weights[i]]
)

图解如下:


image.png


export function bottomUp(
profits: number[],
weights: number[],
capacity: number
) {
const N = profits.length;
const dp: number[][] = new Array(N)
.fill(0)
.map(() => new Array(capacity + 1).fill(0));

// 初始化
for (let i = 0; i < N; i++) dp[i][0] = 0;
for (let c = 0; c <= capacity; c++) {
if (weights[0] <= c) dp[0][c] = profits[0];
}

// dp
for (let i = 1; i < N; i++) {
for (let c = 1; c <= capacity; c++) {
dp[i][c] = Math.max(
dp[i - 1][c],
c < weights[i] ? 0 : profits[i] + dp[i - 1][c - weights[i]]
);
}
}

return dp[N - 1][capacity];
}

时间复杂度:O(n * c)


空间复杂度:O(n * c)


Bottom-Up DP 优化


在计算第 i 个元素的过程中,只需要用到前一次的 dp[c] and dp[c-weight[i]] ,所以,在空间复杂度上我们可以做优化,可以使用同一个数组进行前后两次的记忆


如果 capacity 从 c ~ 0 ,而不是 0 ~ c 循环,去修改 dp[i][c ~ capacity],可以确保 dp[i][0 ~ c-1] 的值是前一次的,但是,如果按照之前的反过来,计算 dp[i][c] 的时候,dp[i][0 ~ c-1] 已经变成第 i 次的值在存储,所以行不通


export function bottomUp2(
profits: number[],
weights: number[],
capacity: number
) {
const N = profits.length;
const dp: number[] = new Array(capacity + 1).fill(0);

// 初始化
for (let c = 0; c <= capacity; c++) {
if (weights[0] <= c) dp[c] = profits[0];
}

// dp
for (let i = 1; i < N; i++) {
for (let c = capacity; c >= 0; c--) {
dp[c] = Math.max(
dp[c],
c < weights[i] ? 0 : profits[i] + dp[c - weights[i]]
);
}
}

return dp[capacity];
}

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

七大跨域解决方法原理

前言 大家好,我是林三心。用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初衷。 咱们做前端的,平时跟后端对接接口那是必须的事情,但是可能很多同学忽略了一个对接过程中可能会发生的问题——跨域,那跨域到底是啥呢?为什么会跨域呢?又怎么才能解决呢...
继续阅读 »

前言


大家好,我是林三心。用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初衷。


咱们做前端的,平时跟后端对接接口那是必须的事情,但是可能很多同学忽略了一个对接过程中可能会发生的问题——跨域,那跨域到底是啥呢?为什么会跨域呢?又怎么才能解决呢?


截屏2021-10-01 上午7.16.06.png


为什么跨域?


image.png


为什么会出现跨域问题呢?那就不得不讲浏览器的同源策略了,它规定了协议号-域名-端口号这三者必须都相同才符合同源策略


截屏2021-10-01 上午8.50.11.png


如有有一个不相同,就会出现跨域问题,不符合同源策略导致的后果有



  • 1、LocalStorge、SessionStorge、Cookie等浏览器内存无法跨域访问

  • 2、DOM节点无法跨域操作

  • 3、Ajax请求无法跨域请求


注意点:一个IP是可以注册多个不同域名的,也就是多个域名可能指向同一个IP,即使是这样,他们也不符合同源策略


截屏2021-10-01 上午9.02.55.png


跨域的时机?


跨域发生在什么时候呢?我考过很多位同学,得到了两种答案



  • 1、请求一发出就被浏览器的跨域报错拦下来了(大多数人回答)

  • 2、请求发出去到后端,后端返回数据,在浏览器接收后端数据时被浏览器的跨域报错拦下来


那到底是哪种呢?我们可以验证下,咱们先npm i nodemon -g,然后创建一个index.js,然后nodemon index起一个node服务


// index.js  http://127.0.0.1:8000

const http = require('http');

const port = 8000;

http.createServer(function (req, res) {
const { query } = urllib.parse(req.url, true);
console.log(query.name)
console.log('到后端喽')
res.end(JSON.stringify('林三心'));
}).listen(port, function () {
console.log('server is listening on port ' + port);
})
复制代码

再创建一个index.html,用来写前端的请求代码,咱们就写一个简单的AJAX请求


// index.html  http://127.0.0.1:5500/index.html
<script>
//步骤一:创建异步对象
var ajax = new XMLHttpRequest();
//步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数
ajax.open('get', 'http://127.0.0.1:8000?name=前端过来的林三心');
//步骤三:发送请求
ajax.send();
//步骤四:注册事件 onreadystatechange 状态改变就会调用
ajax.onreadystatechange = function () {
if (ajax.readyState == 4 && ajax.status == 200) {
//步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的
console.log(ajax.responseText);//输入相应的内容
}
}

</script>
复制代码

截屏2021-10-01 下午1.37.01.png


最终,前端确实是跨域报错了。但这不是结果,我们要想知道是哪一个答案,关键在于看后端的node服务那里有没有输出,就一目了然了。所以,答案2才是对的。


截屏2021-10-01 下午1.38.52.png


截屏2021-10-01 下午1.41.51.png


同域情况 && 跨域情况?


前面提到了同源策略,满足协议号-域名-端口号这三者都相同就是同域,反之就是跨域,会导致跨域报错,下面通过几个例子让大家巩固一下对同域和跨域的认识把!


截屏2021-10-01 上午9.24.38.png


解决跨域的方案


跨域其实是一个很久的问题了,对应的解决方案也有很多,一起接着往下读吧!!!


JSONP


前面咱们说了,因为浏览器同源策略的存在,导致存在跨域问题,那有没有不受跨域问题所束缚的东西呢?其实是有的,以下这三个标签加载资源路径是不受束缚的



  • 1、script标签:<script src="加载资源路径"></script>

  • 2、link标签:<link herf="加载资源路径"></link>

  • 3、img标签:<img src="加载资源路径"></img>


而JSONP就是利用了scriptsrc加载不受束缚,从而可以拥有从不同的域拿到数据的能力。但是JSONP需要前端后端配合,才能实现最终的跨域获取数据


JSONP通俗点说就是:利用script的src去发送请求,将一个方法名callback传给后端,后端拿到这个方法名,将所需数据,通过字符串拼接成新的字符串callback(所需数据),并发送到前端,前端接收到这个字符串之后,就会自动执行方法callback(所需数据)。老规矩,先上图,再上代码。


截屏2021-10-01 下午1.22.08.png


后端代码


// index.js  http://127.0.0.1:8000

const http = require('http');
const urllib = require('url');

const port = 8000;

http.createServer(function (req, res) {
const { query } = urllib.parse(req.url, true);
if (query && query.callback) {
const { name, age, callback } = query
const person = `${name}今年${age}岁啦!!!`
const str = `${callback}(${JSON.stringify(person)})` // 拼成callback(data)
res.end(str);
} else {
res.end(JSON.stringify('没东西啊你'));
}
}).listen(port, function () {
console.log('server is listening on port ' + port);
})
复制代码

前端代码


// index.html  http://127.0.0.1:5500/index.html

const jsonp = (url, params, cbName) => {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
window[cbName] = (data) => {
resolve(data)
document.body.removeChild(script)
}
params = { ...params, callback: cbName }
const arr = Object.keys(params).map(key => `${key}=${params[key]}`)
script.src = `${url}?${arr.join('&')}`
document.body.appendChild(script)
})
}

jsonp('http://127.0.0.1:8000', { name: '林三心', age: 23 }, 'callback').then(data => {
console.log(data) // 林三心今年23岁啦!!!
})
复制代码

截屏2021-10-01 下午1.47.29.png



JSONP的缺点就是,需要前后端配合,并且只支持get请求方法



WebSocket


WebSocket是什么东西?其实我也不怎么懂,但是我也不会像别人一样把MDN的资料直接复制过来,因为复制过来相信大家也是看不懂的。


我理解的WebSocket是一种协议(跟http同级,都是协议),并且他可以进行跨域通信,为什么他支持跨域通信呢?我这里找到一篇文章WebSocket凭啥可以跨域?,讲的挺好


截屏2021-10-01 下午10.02.39.png


后端代码


先安装npm i ws


// index.js  http://127.0.0.1:8000
const Websocket = require('ws');

const port = 8000;
const ws = new Websocket.Server({ port })
ws.on('connection', (obj) => {
obj.on('message', (data) => {
data = JSON.parse(data.toString())
const { name, age } = data
obj.send(`${name}今年${age}岁啦!!!`)
})
})
复制代码

前端代码


// index.html  http://127.0.0.1:5500/index.html


function myWebsocket(url, params) {
return new Promise((resolve, reject) => {
const socket = new WebSocket(url)
socket.onopen = () => {
socket.send(JSON.stringify(params))
}
socket.onmessage = (e) => {
resolve(e.data)
}
})
}
myWebsocket('ws://127.0.0.1:8000', { name: '林三心', age: 23 }).then(data => {
console.log(data) // 林三心今年23岁啦!!!
})
复制代码

结果如下


截屏2021-10-01 下午1.47.29.png


Cors


Cors,全称是Cross-Origin Resource Sharing,意思是跨域资源共享,Cors一般是由后端来开启的,一旦开启,前端就可以跨域访问后端。


为什么后端开启Cors,前端就能跨域请求后端呢?我的理解是:前端跨域访问到后端,后端开启Cors,发送Access-Control-Allow-Origin: 域名 字段到前端(其实不止一个),前端浏览器判断Access-Control-Allow-Origin的域名如果跟前端域名一样,浏览器就不会实行跨域拦截,从而解决跨域问题。


截屏2021-10-01 下午6.41.11.png


后端代码


// index.js  http://127.0.0.1:8000

const http = require('http');
const urllib = require('url');

const port = 8000;

http.createServer(function (req, res) {
// 开启Cors
res.writeHead(200, {
//设置允许跨域的域名,也可设置*允许所有域名
'Access-Control-Allow-Origin': 'http://127.0.0.1:5500',
//跨域允许的请求方法,也可设置*允许所有方法
"Access-Control-Allow-Methods": "DELETE,PUT,POST,GET,OPTIONS",
//允许的header类型
'Access-Control-Allow-Headers': 'Content-Type'
})
const { query: { name, age } } = urllib.parse(req.url, true);
res.end(`${name}今年${age}岁啦!!!`);
}).listen(port, function () {
console.log('server is listening on port ' + port);
})
复制代码

前端代码


// index.html  http://127.0.0.1:5500/index.html
//步骤一:创建异步对象
var ajax = new XMLHttpRequest();
//步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数
ajax.open('get', 'http://127.0.0.1:8000?name=林三心&age=23');
//步骤三:发送请求
ajax.send();
//步骤四:注册事件 onreadystatechange 状态改变就会调用
ajax.onreadystatechange = function () {
if (ajax.readyState == 4 && ajax.status == 200) {
//步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的
console.log(ajax.responseText);//输入相应的内容
}
}
复制代码

结果如下


截屏2021-10-01 下午1.47.29.png


截屏2021-10-01 下午7.10.57.png


Node接口代理


还是回到同源策略,同源策略它只是浏览器的一个策略而已,它是限制不到后端的,也就是前端-后端会被同源策略限制,但是后端-后端则不会被限制,所以可以通过Node接口代理,先访问已设置Cors的后端1,再让后端1去访问后端2获取数据到后端1,后端1再把数据传到前端


截屏2021-10-01 下午8.46.28.png


后端2代码


// index.js  http://127.0.0.1:8000

const http = require('http');
const urllib = require('url');

const port = 8000;

http.createServer(function (req, res) {
console.log(888)
const { query: { name, age } } = urllib.parse(req.url, true);
res.end(`${name}今年${age}岁啦!!!`)
}).listen(port, function () {
console.log('server is listening on port ' + port);
})
复制代码

创建一个index2.js,并nodmeon index2.js


后端1代码


// index2.js  http://127.0.0.1:8888

const http = require('http');
const urllib = require('url');
const querystring = require('querystring');
const port = 8888;

http.createServer(function (req, res) {
// 开启Cors
res.writeHead(200, {
//设置允许跨域的域名,也可设置*允许所有域名
'Access-Control-Allow-Origin': 'http://127.0.0.1:5500',
//跨域允许的请求方法,也可设置*允许所有方法
"Access-Control-Allow-Methods": "DELETE,PUT,POST,GET,OPTIONS",
//允许的header类型
'Access-Control-Allow-Headers': 'Content-Type'
})
const { query } = urllib.parse(req.url, true);
const { methods = 'GET', headers } = req
const proxyReq = http.request({
host: '127.0.0.1',
port: '8000',
path: `/?${querystring.stringify(query)}`,
methods,
headers
}, proxyRes => {
proxyRes.on('data', chunk => {
console.log(chunk.toString())
res.end(chunk.toString())
})
}).end()
}).listen(port, function () {
console.log('server is listening on port ' + port);
})
复制代码

前端代码


// index.html  http://127.0.0.1:5500

//步骤一:创建异步对象
var ajax = new XMLHttpRequest();
//步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数,动态的传递参数starName到服务端
ajax.open('get', 'http://127.0.0.1:8888?name=林三心&age=23');
//步骤三:发送请求
ajax.send();
//步骤四:注册事件 onreadystatechange 状态改变就会调用
ajax.onreadystatechange = function () {
if (ajax.readyState == 4 && ajax.status == 200) {
//步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的
console.log(ajax.responseText);//输入相应的内容
}
}
复制代码

结果如下


截屏2021-10-01 下午1.47.29.png


Nginx


其实NginxNode接口代理是一个道理,只不过Nginx就不需要我们自己去搭建一个中间服务


截屏2021-10-01 下午8.47.40.png


先下载nginx,然后将nginx目录下的nginx.conf修改如下:


    server{
listen 8888;
server_name 127.0.0.1;

location /{
proxy_pass 127.0.0.1:8000;
}
}
复制代码

最后通过命令行nginx -s reload启动nginx


后端代码


// index.js  http://127.0.0.1:8000

const http = require('http');
const urllib = require('url');

const port = 8000;

http.createServer(function (req, res) {
const { query: { name, age } } = urllib.parse(req.url, true);
res.end(`${name}今年${age}岁啦!!!`);
}).listen(port, function () {
console.log('server is listening on port ' + port);
})
复制代码

前端代码


// index.html  http://127.0.0.1:5500

//步骤一:创建异步对象
var ajax = new XMLHttpRequest();
//步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数,动态的传递参数starName到服务端
ajax.open('get', 'http://127.0.0.1:8888?name=林三心&age=23');
//步骤三:发送请求
ajax.send();
//步骤四:注册事件 onreadystatechange 状态改变就会调用
ajax.onreadystatechange = function () {
if (ajax.readyState == 4 && ajax.status == 200) {
//步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的
console.log(ajax.responseText);//输入相应的内容
}
}
复制代码

结果如下


截屏2021-10-01 下午1.47.29.png


postMessage


场景:http://127.0.0.1:5500/index.html页面中使用了iframe标签内嵌了一个http://127.0.0.1:5555/index.html的页面


虽然这两个页面存在于一个页面中,但是需要iframe标签来嵌套才行,这两个页面之间是无法进行通信的,因为他们端口号不同,根据同源策略,他们之间存在跨域问题


那应该怎么办呢?使用postMessage可以使这两个页面进行通信


截屏2021-10-01 下午9.28.53.png


// http:127.0.0.1:5500/index.html

<body>
<iframe src="http://127.0.0.1:5555/index.html" id="frame"></iframe>
</body>
<script>
document.getElementById('frame').onload = function () {
this.contentWindow.postMessage({ name: '林三心', age: 23 }, 'http://127.0.0.1:5555')
window.onmessage = function (e) {
console.log(e.data) // 林三心今年23岁啦!!!
}
}
</script>
复制代码

// http://127.0.0.1:5555/index.html

<script>
window.onmessage = function (e) {
const { data: { name, age }, origin } = e
e.source.postMessage(`${name}今年${age}岁啦!!!`, origin)
}
</script>
复制代码

document.domain && iframe


场景:a.sanxin.com/index.htmlb.sanxin.com/index.html之间的通信


其实上面这两个正常情况下是无法通信的,因为他们的域名不相同,属于跨域通信


那怎么办呢?其实他们有一个共同点,那就是他们的二级域名都是sanxin.com,这使得他们可以通过document.domain && iframe的方式来通信


截屏2021-10-01 下午9.58.55.png


由于本菜鸟暂时没有服务器,所以暂时使用本地来模拟


// http://127.0.0.1:5500/index.html

<body>
<iframe src="http://127.0.0.1:5555/index.html" id="frame"></iframe>
</body>
<script>
document.domain = '127.0.0.1'
document.getElementById('frame').onload = function () {
console.log(this.contentWindow.data) // 林三心今年23岁啦!!!
}
</script>
复制代码

// http://127.0.0.1:5555/index.html

<script>
// window.name="林三心今年23岁啦!!!"
document.domain = '127.0.0.1'
var data = '林三心今年23岁啦!!!';
</script>

复制代码

结果如下


截屏2021-10-01 下午1.47.29.png


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

画一手好的架构图是码农进阶的开始

1.前言 你是否对大厂展示的五花八门,花花绿绿的架构设计图所深深吸引,当我们想用几张图来介绍下业务系统,是不是对着画布不知从何下手?作为技术扛把子的筒子们是不是需要一张图来描述系统,让系统各个参与方都能看的明白?如果有这样的困惑,本文将介绍一些画图的方...
继续阅读 »

1.前言

你是否对大厂展示的五花八门,花花绿绿的架构设计图所深深吸引,当我们想用几张图来介绍下业务系统,是不是对着画布不知从何下手?作为技术扛把子的筒子们是不是需要一张图来描述系统,让系统各个参与方都能看的明白?如果有这样的困惑,本文将介绍一些画图的方法论,让技术图纸更加清晰。

2. 架构的定义

  • 系统架构是概念的体现,是对物/信息的功能与形式元素之间的对应情况所做的分配,是对元素之间的关系以及元素同周边环境之间的关系所做的定义;

  • 架构就是对系统中的实体以及实体之间的关系所进行的抽象描述,是一系列的决策;

  • 架构是结构和愿景.

在TOGAF企业架构理论中, 架构是从公司战略层面,自顶向下的细化的一部分,从战略=> 业务架构=>应用/数据/技术架构,当然老板层关注的是战略与业务架构,我们搬砖的需要聚焦到应用/数据/技术架构这一层。


  • 业务架构: 由业务架构师负责,也可以称为业务领域专家、行业专家,业务架构属于顶层设计,其对业务的定义和划分会影响组织架构和技术架构;

  • 应用架构: 由应用架构师负责,需要根据业务场景需要,设计应用的层次结构,制定应用规范、定义接口和数据交互协议等。并尽量将应用的复杂度控制在一个可以接受的水平,从而在快速的支撑业务发展的同时,在保证系统的可用性和可维护性的同时,确保应用满足非功能属性的要求如性能、安全、稳定性等。

  • 技术架构: 描述了需要哪些服务;选择哪些技术组件来实现技术服务;技术服务以及组件之间的交互关系;

  • 数据架构: 描述了数据模型、分布、数据的流向、数据的生命周期、数据的管理等关系;

3.架构图的分类

系统架构图是为了抽象的表示软件系统的整体轮廓和各个组件之间的相互关系和约束边界,以及软件系统的物理部署和软件系统的演进方向的整体视图。好的架构图可以让干系人理解、遵循架构决策,就需要把架构信息传递出去。那么,画架构图是为了:解决沟通障碍/达成共识/减少歧义。比较流行的是4+1视图和C4视图。

3.1 4+1视图

3.1.1 场景视图

用于描述系统的参与者与功能用例间的关系,反映系统的最终需求和交互设计,通常由用例图表示;


3.1.2 逻辑视图

用于描述系统软件功能拆解后的组件关系,组件约束和边界,反映系统整体组成与系统如何构建的过程,通常由UML的组件图和类图来表示。


3.1.3 物理视图

用于描述系统软件到物理硬件的映射关系,反映出系统的组件是如何部署到一组可计算机器节点上,用于指导软件系统的部署实施过程。


3.1.4 处理流程视图

用于描述系统软件组件之间的通信时序,数据的输入输出,反映系统的功能流程与数据流程,通常由时序图和流程图表示。


3.1.5 开发视图

开发视图用于描述系统的模块划分和组成,以及细化到内部包的组成设计,服务于开发人员,反映系统开发实施过程。


5 种架构视图从不同角度表示一个软件系统的不同特征,组合到一起作为架构蓝图描述系统架构。

3.2 C4视图

下面的案例来自C4官网,然后加上了一些笔者的理解。


C4 模型使用容器(应用程序、数据存储、微服务等)、组件和代码来描述一个软件系统的静态结构。这几种图比较容易画,也给出了画图要点,但最关键的是,我们认为,它明确指出了每种图可能的受众以及意义。

3.2.1 语境图(System Context Diagram)

用于描述要我们要构建的系统是什么,用户是谁,需要如何融入已有的IT环境。这个图的受众可以是开发团队的内部人员、外部的技术或非技术人员。


3.2.2 容器图(Container Diagram)

容器图是把语境图里待建设的系统做了一个展开描述,主要受众是团队内部或外部的开发人员或运维人员,主要用来描述软件系统的整体形态,体现了高层次的技术决策与选型,系统中的职责是如何分布的,容器间是如何交互的。


3.2.3 组件图(Component Diagram)

组件图是把某个容器进行展开,描述其内部的模块,主要是给内部开发人员看的,怎么去做代码的组织和构建,描述了系统由哪些组件/服务组成,了组件之间的关系和依赖,为软件开发如何分解交付提供了框架。


4.怎么画好架构图

上面的分类是前人的经验总结,图也是从网上摘来的,那么这些图画的好不好呢?是不是我们要依葫芦画瓢去画这样一些图?先不去管这些图好不好,我们通过对这些图的分类以及作用,思考了一下,总结下来,我们认为,明确这两点之后,从受众角度来说,一个好的架构图是不需要解释的,它应该是自描述的,并且要具备一致性和足够的准确性,能够与代码相呼应。

4.1 视图的受众

在画出一个好的架构图之前, 首先应该要明确其受众,再想清楚要给他们传递什么信息 ,所以,不要为了画一个物理视图去画物理视图,为了画一个逻辑视图去画逻辑视图,而应该根据受众的不同,传递的信息的不同,用图准确地表达出来,最后的图可能就是在这样一些分类里。那么,画出的图好不好的一个直接标准就是:受众有没有准确接收到想传递的信息。

4.2 视图的元素区分

可以看到架构视图是由方框和线条等元素构成,要利用形状、颜色、线条变化等区分元素的含义,避免混淆。架构是一项复杂的工作,只使用单个图表来表示架构很容易造成莫名其妙的语义混乱。

让我们一起画出好的架构图!

参考资料


作者:代码的色彩
来源:https://juejin.cn/post/7062662600437268493

收起阅读 »

CSS性能优化的8个技巧

我们都知道对于网站来说,性能至关重要,CSS作为页面渲染和内容展现的重要环节,影响着用户对整个网站的第一体验。因此,与其相关的性能优化是不容忽视的。对于性能优化我们常常在项目完成时才去考虑,经常被推迟到项目的末期,甚至到暴露出严重的性能问题时才进行性能优化,相...
继续阅读 »

我们都知道对于网站来说,性能至关重要,CSS作为页面渲染和内容展现的重要环节,影响着用户对整个网站的第一体验。因此,与其相关的性能优化是不容忽视的。

对于性能优化我们常常在项目完成时才去考虑,经常被推迟到项目的末期,甚至到暴露出严重的性能问题时才进行性能优化,相信大多数人对此深有体会。

笔者认为,为了更多地避免这一情况,首先要重视起性能优化相关的工作,将其贯穿到整个产品设计与开发中。其次,就是了解性能相关的内容,在项目开发过程中,自然而然地进行性能优化。最后,也是最最重要的,那就是从现在开始实施优化。

推荐大家阅读下奇舞周刊之前推的《嗨,送你一张Web性能优化地图》1这篇文章,能够帮助大家对性能优化需要做的事以及需要考虑的问题形成一个整体的概念。

本文将会详细介绍CSS性能优化相关的技巧,笔者将它们分为实践型建议型两类,共8个小技巧。实践型技巧能够快速地应用在项目中,能够很好地提升性能,也是笔者经常使用的,建议大家尽快在项目中实践。建议型技巧中,有的可能对性能影响并不显著,有的平时大家也并不会那么用,所以笔者不会着重讲述,读者们可以根据自身情况了解一下即可。

在正式开始之前,需要大家对于浏览器的工作原理2有些一定的了解,需要的小伙伴可以先简单了解下。

下面我们开始介绍实践型的4个优化技巧,先从首屏关键CSS开始。

1. 内联首屏关键CSS(Critical CSS)

性能优化中有一个重要的指标——首次有效绘制(First Meaningful Paint,简称FMP)即指页面的首要内容(primary content)出现在屏幕上的时间。这一指标影响用户看到页面前所需等待的时间,而内联首屏关键CSS(即Critical CSS,可以称之为首屏关键CSS)能减少这一时间。

大家应该都习惯于通过link标签引用外部CSS文件。但需要知道的是,将CSS直接内联到HTML文档中能使CSS更快速地下载。而使用外部CSS文件时,需要在HTML文档下载完成后才知道所要引用的CSS文件,然后才下载它们。所以说,内联CSS能够使浏览器开始页面渲染的时间提前,因为在HTML下载完成之后就能渲染了。

既然内联CSS能够使页面渲染的开始时间提前,那么是否可以内联所有的CSS呢?答案显然是否定的,这种方式并不适用于内联较大的CSS文件。因为初始拥塞窗口3存在限制(TCP相关概念,通常是 14.6kB,压缩后大小),如果内联CSS后的文件超出了这一限制,系统就需要在服务器和浏览器之间进行更多次的往返,这样并不能提前页面渲染时间。因此,我们应当只将渲染首屏内容所需的关键CSS内联到HTML中

既然已经知道内联首屏关键CSS能够优化性能了,那下一步就是如何确定首屏关键CSS了。显然,我们不需要手动确定哪些内容是首屏关键CSS。Github上有一个项目Critical CSS4,可以将属于首屏的关键样式提取出来,大家可以看一下该项目,结合自己的构建工具进行使用。当然为了保证正确,大家最好再亲自确认下提取出的内容是否有缺失。

不过内联CSS有一个缺点,内联之后的CSS不会进行缓存,每次都会重新下载。不过如上所说,如果我们将内联后的文件大小控制在了14.6kb以内,这似乎并不是什么大问题。

如上,我们已经介绍了为什么要内联关键CSS以及如何内联,那么剩下的CSS我们怎么处理好呢?建议使用外部CSS引入剩余CSS,这样能够启用缓存,除此之外还可以异步加载它们。

2. 异步加载CSS

CSS会阻塞渲染,在CSS文件请求、下载、解析完成之前,浏览器将不会渲染任何已处理的内容。有时,这种阻塞是必须的,因为我们并不希望在所需的CSS加载之前,浏览器就开始渲染页面。那么将首屏关键CSS内联后,剩余的CSS内容的阻塞渲染就不是必需的了,可以使用外部CSS,并且异步加载。

那么如何实现CSS的异步加载呢?有以下四种方式可以实现浏览器异步加载CSS。

第一种方式是使用JavaScript动态创建样式表link元素,并插入到DOM中。

// 创建link标签
const myCSS = document.createElement( "link" );
myCSS.rel = "stylesheet";
myCSS.href = "mystyles.css";
// 插入到header的最后位置
document.head.insertBefore( myCSS, document.head.childNodes[ document.head.childNodes.length - 1 ].nextSibling );

第二种方式是将link元素的media属性设置为用户浏览器不匹配的媒体类型(或媒体查询),如media="print",甚至可以是完全不存在的类型media="noexist"。对浏览器来说,如果样式表不适用于当前媒体类型,其优先级会被放低,会在不阻塞页面渲染的情况下再进行下载。

当然,这么做只是为了实现CSS的异步加载,别忘了在文件加载完成之后,将media的值设为screenall,从而让浏览器开始解析CSS。

<link rel="stylesheet" href="mystyles.css" media="noexist" onl0ad="this.media='all'">

与第二种方式相似,我们还可以通过rel属性将link元素标记为alternate可选样式表,也能实现浏览器异步加载。同样别忘了加载完成之后,将rel改回去。

<link rel="alternate stylesheet" href="mystyles.css" onl0ad="this.rel='stylesheet'">

上述的三种方法都较为古老。现在,rel="preload"5这一Web标准指出了如何异步加载资源,包括CSS类资源。

<link rel="preload" href="mystyles.css" as="style" onl0ad="this.rel='stylesheet'">

注意,as是必须的。忽略as属性,或者错误的as属性会使preload等同于XHR请求,浏览器不知道加载的是什么内容,因此此类资源加载优先级会非常低。as的可选值可以参考上述标准文档。

看起来,rel="preload"的用法和上面两种没什么区别,都是通过更改某些属性,使得浏览器异步加载CSS文件但不解析,直到加载完成并将修改还原,然后开始解析。

但是它们之间其实有一个很重要的不同点,那就是使用preload,比使用不匹配的media方法能够更早地开始加载CSS。所以尽管这一标准的支持度还不完善,仍建议优先使用该方法。

该标准现在已经是候选标准,相信浏览器会逐渐支持该标准。在各浏览器的支持度如下图所示。


从上图可以看出这一方法在现在的浏览器中支持度不算乐观,不过我们可以通过loadCSS6进行polyfill,所以支持不支持,这都不是事儿。

3. 文件压缩

性能优化时有一个最容易想到,也最常使用的方法,那就是文件压缩,这一方案往往效果显著。

文件的大小会直接影响浏览器的加载速度,这一点在网络较差时表现地尤为明显。相信大家都早已习惯对CSS进行压缩,现在的构建工具,如webpack、gulp/grunt、rollup等也都支持CSS压缩功能。压缩后的文件能够明显减小,可以大大降低了浏览器的加载时间。

4. 去除无用CSS

虽然文件压缩能够降低文件大小。但CSS文件压缩通常只会去除无用的空格,这样就限制了CSS文件的压缩比例。那是否还有其他手段来精简CSS呢?答案显然是肯定的,如果压缩后的文件仍然超出了预期的大小,我们可以试着找到并删除代码中无用的CSS

一般情况下,会存在这两种无用的CSS代码:一种是不同元素或者其他情况下的重复代码,一种是整个页面内没有生效的CSS代码。对于前者,在编写的代码时候,我们应该尽可能地提取公共类,减少重复。对于后者,在不同开发者进行代码维护的过程中,总会产生不再使用的CSS的代码,当然一个人编写时也有可能出现这一问题。而这些无用的CSS代码不仅会增加浏览器的下载量,还会增加浏览器的解析时间,这对性能来说是很大的消耗。所以我们需要找到并去除这些无用代码。

当然,如果手动删除这些无用CSS是很低效的。我们可以借助Uncss7库来进行。Uncss可以用来移除样式表中的无用CSS,并且支持多文件和JavaScript注入的CSS。

前面已经说完了实践型的4个优化技巧,下面我们介绍下建议型的4个技巧

1. 有选择地使用选择器

大多数朋友应该都知道CSS选择器的匹配是从右向左进行的,这一策略导致了不同种类的选择器之间的性能也存在差异。相比于#markdown-content-h3,显然使用#markdown .content h3时,浏览器生成渲染树(render-tree)所要花费的时间更多。因为后者需要先找到DOM中的所有h3元素,再过滤掉祖先元素不是.content的,最后过滤掉.content的祖先不是#markdown的。试想,如果嵌套的层级更多,页面中的元素更多,那么匹配所要花费的时间代价自然更高。

不过现代浏览器在这一方面做了很多优化,不同选择器的性能差别并不明显,甚至可以说差别甚微。此外不同选择器在不同浏览器中的性能表现8也不完全统一,在编写CSS的时候无法兼顾每种浏览器。鉴于这两点原因,我们在使用选择器时,只需要记住以下几点,其他的可以全凭喜好。

  1. 保持简单,不要使用嵌套过多过于复杂的选择器。

  2. 通配符和属性选择器效率最低,需要匹配的元素最多,尽量避免使用。

  3. 不要使用类选择器和ID选择器修饰元素标签,如h3#markdown-content,这样多此一举,还会降低效率。

  4. 不要为了追求速度而放弃可读性与可维护性。

如果大家对于上面这几点还存在疑问,笔者建议大家选择以下几种CSS方法论之一(BEM9,OOCSS10,SUIT11,SMACSS12,ITCSS13,Enduring CSS14等)作为CSS编写规范。使用统一的方法论能够帮助大家形成统一的风格,减少命名冲突,也能避免上述的问题,总之好处多多,如果你还没有使用,就赶快用起来吧。

Tips:为什么CSS选择器是从右向左匹配的?

CSS中更多的选择器是不会匹配的,所以在考虑性能问题时,需要考虑的是如何在选择器不匹配时提升效率。从右向左匹配就是为了达成这一目的的,通过这一策略能够使得CSS选择器在不匹配的时候效率更高。这样想来,在匹配时多耗费一些性能也能够想的通了。

2. 减少使用昂贵的属性

在浏览器绘制屏幕时,所有需要浏览器进行操作或计算的属性相对而言都需要花费更大的代价。当页面发生重绘时,它们会降低浏览器的渲染性能。所以在编写CSS时,我们应该尽量减少使用昂贵属性,如box-shadow/border-radius/filter/透明度/:nth-child等。

当然,并不是让大家不要使用这些属性,因为这些应该都是我们经常使用的属性。之所以提这一点,是让大家对此有一个了解。当有两种方案可以选择的时候,可以优先选择没有昂贵属性或昂贵属性更少的方案,如果每次都这样的选择,网站的性能会在不知不觉中得到一定的提升。

3. 优化重排与重绘

在网站的使用过程中,某些操作会导致样式的改变,这时浏览器需要检测这些改变并重新渲染,其中有些操作所耗费的性能更多。我们都知道,当FPS为60时,用户使用网站时才会感到流畅。这也就是说,我们需要在16.67ms内完成每次渲染相关的所有操作,所以我们要尽量减少耗费更多的操作。

3.1 减少重排

重排会导致浏览器重新计算整个文档,重新构建渲染树,这一过程会降低浏览器的渲染速度。如下所示,有很多操作会触发重排,我们应该避免频繁触发这些操作。

  1. 改变font-sizefont-family

  2. 改变元素的内外边距

  3. 通过JS改变CSS类

  4. 通过JS获取DOM元素的位置相关属性(如width/height/left等)

  5. CSS伪类激活

  6. 滚动滚动条或者改变窗口大小

此外,我们还可以通过CSS Trigger15查询哪些属性会触发重排与重绘。

值得一提的是,某些CSS属性具有更好的重排性能。如使用Flex时,比使用inline-blockfloat时重排更快,所以在布局时可以优先考虑Flex

3.2 避免不必要的重绘

当元素的外观(如color,background,visibility等属性)发生改变时,会触发重绘。在网站的使用过程中,重绘是无法避免的。不过,浏览器对此做了优化,它会将多次的重排、重绘操作合并为一次执行。不过我们仍需要避免不必要的重绘,如页面滚动时触发的hover事件,可以在滚动的时候禁用hover事件,这样页面在滚动时会更加流畅。

此外,我们编写的CSS中动画相关的代码越来越多,我们已经习惯于使用动画来提升用户体验。我们在编写动画时,也应当参考上述内容,减少重绘重排的触发。除此之外我们还可以通过硬件加速16和will-change17来提升动画性能,本文不对此展开详细介绍,感兴趣的小伙伴可以点击链接进行查看。

最后需要注意的是,用户的设备可能并没有想象中的那么好,至少不会有我们的开发机器那么好。我们可以借助Chrome的开发者工具进行CPU降速,然后再进行相关的测试,降速方法如下图所示。


如果需要在移动端访问的,最好将速度限制更低,因为移动端的性能往往更差。

4. 不要使用@import

最后提一下,不要使用@import引入CSS,相信大家也很少使用。

不建议使用@import主要有以下两点原因。

首先,使用@import引入CSS会影响浏览器的并行下载。使用@import引用的CSS文件只有在引用它的那个css文件被下载、解析之后,浏览器才会知道还有另外一个css需要下载,这时才去下载,然后下载后开始解析、构建render tree等一系列操作。这就导致浏览器无法并行下载所需的样式文件。

其次,多个@import会导致下载顺序紊乱。在IE中,@import会引发资源文件的下载顺序被打乱,即排列在@import后面的js文件先于@import下载,并且打乱甚至破坏@import自身的并行下载

所以不要使用这一方法,使用link标签就行了。

总结

至此,我们介绍完了CSS性能优化的4个实践型技巧和4个建议型技巧,在了解这些技巧之后,CSS的性能优化从现在就可以开始了。不要犹豫了,尽快开始吧。

参考文章

  1. Efficiently Rendering CSS

  2. How to write CSS for a great performance web application

  3. CSS performance revisited: selectors, bloat and expensive styles

  4. Avoiding Unnecessary Paints

  5. Five CSS Performance Tools to Speed up Your Website

  6. How and Why You Should Inline Your Critical CSS

  7. Render blocking css

  8. Modern Asynchronous CSS Loading

  9. Preload

作者:奇舞精选 · 高峰
来源:https://juejin.cn/post/6844903649605320711

收起阅读 »

你要懂的单页面应用和多页面应用

单页面应用(SinglePage Web Application,SPA)只有一张Web页面的应用,是一种从Web服务器加载的富客户端,单页面跳转仅刷新局部资源 ,公共资源(js、css等)仅需加载一次,常用于PC端官网、购物等网站如图:单页面应用结构视图多页...
继续阅读 »

单页面应用(SinglePage Web Application,SPA)

只有一张Web页面的应用,是一种从Web服务器加载的富客户端,单页面跳转仅刷新局部资源 ,公共资源(js、css等)仅需加载一次,常用于PC端官网、购物等网站

如图:


单页面应用结构视图

多页面应用(MultiPage Application,MPA)

多页面跳转刷新所有资源,每个公共资源(js、css等)需选择性重新加载,常用于 app 或 客户端等

如图:


多页面应用结构视图

具体对比分析:

单页面应用(SinglePage Web Application,SPA)多页面应用(MultiPage Application,MPA)
组成一个外壳页面和多个页面片段组成多个完整页面构成
资源共用(css,js)共用,只需在外壳部分加载不共用,每个页面都需要加载
刷新方式页面局部刷新或更改整页刷新
url 模式a.com/#/pageone a.com/#/pagetwoa.com/pageone.html a.com/pagetwo.html
用户体验页面片段间的切换快,用户体验良好页面切换加载缓慢,流畅度不够,用户体验比较差
转场动画容易实现无法实现
数据传递容易依赖 url传参、或者cookie 、localStorage等
搜索引擎优化(SEO)需要单独方案、实现较为困难、不利于SEO检索 可利用服务器端渲染(SSR)优化实现方法简易
试用范围高要求的体验度、追求界面流畅的应用适用于追求高度支持搜索引擎的应用
开发成本较高,常需借助专业的框架较低 ,但页面重复代码多
维护成本相对容易相对复杂


作者:boxser
来源:https://juejin.cn/post/6844903512107663368

收起阅读 »

千万别小瞧九宫格 一道题就能让候选人原形毕露!

前言 据不完全统计(其实就统计了自己身边的朋友和同事),在刨除抖音或快手这一类短视频 APP 后,每天在手机上花费时间最长的就是刷微博和逛朋友圈。 在刷微博和逛朋友圈的时候经常会看到这种东西: 它有一个高大上的名字:九宫格。 顾名思义,九宫格通常为如图这种三...
继续阅读 »

前言


据不完全统计(其实就统计了自己身边的朋友和同事),在刨除抖音或快手这一类短视频 APP 后,每天在手机上花费时间最长的就是刷微博和逛朋友圈。


在刷微博和逛朋友圈的时候经常会看到这种东西:



它有一个高大上的名字:九宫格。
顾名思义,九宫格通常为如图这种三行三列的布局。


微信客户端就用到了这种布局方式:



大家最熟悉的朋友圈也采用了九宫格:



还有微博:



它在移动端的运用十分的广泛,而且不仅仅是在移动端的运用,它甚至还运用到了一些面试题中,因为九宫格可以很好的考察面试者的 CSS 功底。


边距九宫格


九宫格通常分为两种,一种是边距九宫格,另一种是边框九宫格。


边距九宫格就是朋友圈那种每张图都带有一定边距的那种:


这种其实反而更简单一些,因为不涉及到边框问题,像这种几行几列的布局用网格布局(grid)简直再合适不过了。


但考虑到大家普遍对网格不太熟悉,所以咱们用同样适合几行几列的表格布局来实现,为什么不用万能的弹性盒子(flex)来做呢?因为下面那道面试题就是用flex实现的,不想用两个一样的布局来实现,为了美观一点,这里使用了一个中文渐变色的库:chinese-gradient,来看代码:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- 在这里用link标签引入中文渐变色 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/chinese-gradient">
<style>
/* 清除默认样式 */
* { padding: 0; margin: 0; }

/* 全屏显示 */
html, body, ul { height: 100% }

/* 父元素 */
ul {
/* 给个合适的宽度 */
width: 100%;

/* 清除默认样式 */
list-style: none;

/* 令其用table方式去显示 */
display: table;

/* 设置间距 */
border-spacing: 3px
}

/* 子元素 */
li {
/* 令其用table-row方式去显示 */
display: table-row
}

/* 孙子元素 */
div {
/* 令其用table-cell方式去显示 */
display: table-cell;

/* 蓝色渐变 */
background: var(--湖蓝)
}
</style>
</head>
<body>
<ul>
<li>
<div></div>
<div></div>
<div></div>
</li>
<li>
<div></div>
<div></div>
<div></div>
</li>
<li>
<div></div>
<div></div>
<div></div>
</li>
</ul>
</body>
</html>
复制代码

运行结果:



可以看到在 DOM 结构上我们并没有用到 <table>、<tr>、<td> 这类传统表格元素,因为在这种情况下只是用到了表格的那种几行几列而已。但实际上九宫格并不是表格,所以为了符合 W3C 的语义化标准,我们采用了其他的 DOM 元素。



在有些适合使用表格布局但又不是表格的情况下,可以利用 display 属性来模仿表格的行为:




  • display: table;相当于把元素的行为变成<table></table>

  • display: inline-table;相当于把元素的行为变成行内元素版的<table></table>

  • display: table-header-group;相当于把元素的行为变成<thead></thead>

  • display: table-row-group;相当于把元素的行为变成<tbody></tbody>

  • display: table-footer-group;相当于把元素的行为变成<tfoot></tfoot>

  • display: table-row;相当于把元素的行为变成<tr></tr>

  • display: table-column-group;相当于把元素的行为变成<colgroup></colgroup>

  • display: table-column;相当于把元素的行为变成<col></col>

  • display: table-cell;相当于把元素的行为变成<td></td><th></th>

  • display: table-caption;相当于把元素的行为变成<caption></caption>


边框九宫格


可能大家看了前面的内容觉得:就这?这么简单还想让人原形毕露?


那咱们来看这么一道题:



要求如下:



  • 边框九宫格的每个格子中的数字都要居中

  • 鼠标经过时边框和数字都要变红

  • 点击九宫格会弹出对应的数字


看起来还是没什么大不了对不对?是不是觉得就是把九宫格加个边框就行了?如果你是这么想的话,那么你写出来的九宫格将会变成这样:



是不是跟想象中的好像不太一样?为什么会这样呢?




因为给每个盒子加入了边框以后,在有边距的情况下看起来都挺正常的,但要将他们合并在一起的话相邻的两个边框就会贴合在一起,肉眼看起来就是一个两倍粗的边框:



那么怎么解决这个问题呢?


解法1


不是相邻的两个边框合并在一起会变粗吗?那么最简单粗暴的办法就是让两个相邻的盒子的其中一个的相邻边不显示边框不就完了!就像这样:



这么做完全可以实现,绝对没毛病。但这种属于笨方法,如果给换成四宫格、六宫格、十二宫格,那么又要重新去想一下该怎么实现,而且写出来的代码也比较冗余,几乎每个盒子都要给它定义一个不同的样式。


如果去参加面试的时候这么实现出来,面试官也不会给你满分,甚至可能连个及格分都不会给。但毕竟算是实现出来了,总比那些没实现出来的强点,不会给零分的。


解法2


上面那种实现方式要给每一个盒子都写一套不同的样式,而且还不适合别的像六宫格、十二宫格这类,代码冗余、可复用性差。


那么怎么才能每个盒子只用到一个样式,并且同样还适用于别的宫格呢?来看看这个思路:



但是仔细一看经不起推敲啊:整个九宫格最右边和最下边的边框都没有了!其实只要咱们在父元素上再加上右侧和下侧的边框即可:



而且并不一定非得是这个方向的,别的方向也可以实现啊,比如酱婶儿的:



酱婶儿的:



还有酱婶儿的:



这种方式不管你是4、6、9还是12宫格,只需在子元素上加一个样式即可,然后再在父元素上加一个互补的边框样式。


解法3


上面那种解法其实已经可以了,但还不是最完美的,那么它都有哪些问题呢?




  • 首先,虽然换成别的宫格也可以复用,但都只适合"满"的情况。比如像朋友圈,最大就是九宫格对吧?但用户可以不是每次都发满九张照片,有可能发7张、有可能发五张,这样的话就会露馅(所以朋友圈采用的是边距九宫格而不是边框九宫格)。




  • 其次,它并不适合这道面试题,因为这道面试题的要求是在鼠标移入时边框变红,而上面那种解法会导致每个盒子的边框都不完整,所以当鼠标移入时效果会变成这样:





那么怎么样才能完美的解出这道题呢?首先每个盒子的边框不能再给它缺斤少两了,但那又会回到最初的那个问题上去:



有的面试题就是这样,在你苦思冥想的时候怎么也想不出来,但是稍微给点思路立马就能明白!


其实就是每个盒子都给它一个负边距,边距的距离恰巧就是边框的粗细,这样后面一个盒子就会"叠加"在前面那个盒子的边框上,我们来写一个粗点的半透明边框演示一下:



中间那些颜色变深了的就是叠在一起的边框,由于是半透明,所以叠在一起时颜色会变深。


不过一些比较细心的朋友可能会纳闷:既然所有盒子都用负边距向左上角移动了,岂不是九宫格不会处在原来的位置上了,没错是这样的!所以我们需要让最左边那一排和最上面那一排不要有负边距,这时候就要考察候选人的CSS水平了,看看他/她能不能够灵活运用伪类选择器:每一行的第一个,应该怎么写?



  • :nth-child(1), :nth-child(4), :nth-child(7)


这样也能实现,不过更好的方式是写成这样:



  • :nth-child(3n+1)


最上面那一排负边距可以不用管,因为如果页面上的九宫格往左边移动了,哪怕只有一两像素,也会导致和页面上的版面无法对齐,而往上移动个一两像素的话谁也看不出来。


但如果要写的话大多数人想的可能是这样:



  • :first-child, :nth-child(2), :nth-child(3)


而更好的方式是这样:



  • :nth-child(-n+3)


每个宫格内的数字要居中,这里推荐用grid,因为九宫格可以用flex去实现,但里面的内容还继续用它去实现的话就体现不出你技术的全面性了,而且在居中这一方面grid可以做到比flex代码更少,即使你对grid不感兴趣,那么只需记住这一固定用法即可:


父元素 {
display: grid;

/* 令其子元素居中 */
place-items: center;
}
复制代码

点击这里查看更多实现居中布局的方式


里面的内容解决了,外面的九宫格咱们来用万能的flex去实现,flex默认是一维布局,但如果仅支持一维的话就不会称之为万能的flex了,思路是这样的,假如每一个宫格宽高为100 x 100,九宫格加起来是300 x 300,每三个就让它换行,这样就可以考察到候选人对flex的灵活运用的程度了:


父元素 {
width: 300px;

/* 设置为flex布局 */
display: flex;

/* 设置换行 */
flex-flow: wrap;
}

子元素 {
width: 100px;
height: 100px;

border: 1px solid black;
}
复制代码

看起来没毛病对不对?实际上确是每行只有两个宫格就会换行,因为加了边框以后子元素的宽高就变成了102 x 102了,三个的话就已经超过了300,所以还没到三个就开始换行了,这时候就考察到候选人的盒模型了:


子元素 {
width: 100px;
height: 100px;

border: 1px solid black;

/* 设置盒模型 */
box-sizing: border-box;
}
复制代码

这样即使加了边框,宽高也还是100,刚好能满3个就换行,想象一下如果你是面试官,直接问盒模型是不是显得很low,但是就这一个小小的九宫格立马就能区分出这个候选人的水平如何。


再接下来就是鼠标移入时边框和里面的内容一起变红,这有啥难的,不就是:


:hover {
/* 红色字体 */
color: red;

/* 红色边框 */
border: 1px solid red;
}
复制代码

还是那句话,这样确实能实现,但如果在咱们写js的过程中像red这种多处地方使用的值是不是一般都会给它设置成变量啊?那么这里要写CSS变量?也可以,但有一个更好的变量叫做currentColor,这个属性可以把它理解成一个内置变量,就像js里的innerWidth(window.innerWidth)一样,不用定义自然就是一个变量。


CSS变量不同的是它取的是自身或父元素上的color值,而且它的兼容性还更好,可以一直兼容到IE9


如果你觉得纳闷:这单词这么长,还不如直接写个red多方便啊,那么请别忘了color是可以继承的!如果在一个外层元素中定义了一个颜色,里面的子元素都可以继承,用JS来控制的话只需要获取外层DOM元素然后修改它的color样式即可。


currentColor作为一个变量,可以用在 border、box-shadow、background、linear-gradient() 等一大堆的 CSS 属性上…甚至连svg中的 fill 和 stroke 都可以使用这个变量,它能做的事情很多,这里为了不跑题就先不展开讲,有兴趣的可以去搜一下。


:hover {
/* 红色字体 */
color: red;

/* 红色边框 */
border: 1px solid;
}
复制代码

修改后的代码如上,为什么没有currentColor?那是因为如果你不写的话,默认就是currentColor,这个关键字代表的就是你当前的color值。



大多数的候选人可能都不会写成这样,如果你作为面试官的话最好是适当的提示一下,看他能不能说出currentColor这个变量或者CSS变量



然后就是点击每个宫格弹出对应的数字,这个考察的是事件冒泡和事件代理:


父元素.addEventListener('click', e => alert(e.target.innerText))
复制代码

你可以观察一下候选人是把事件绑定在父元素上还是一个个的绑定在子元素上,这个问题按理说基本上都不会错。但如果发现候选人一个个把事件绑定在子元素上了,那就可以到此为止了,也不用浪费时间再去问别的问题了,可以十分装B的来一句:行,你的情况我已基本了解了,回去等通知吧!


接下来我们再来写一下完整一点的代码,以便引出下一个问题:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
/* 清除默认样式 */
* { padding: 0; margin: 0; }

/* 全屏显示 */
html, body { height: 100% }

body {
/* 网格布局 */
display: grid;

/* 子元素居中 */
place-items: center;
}

/* 父元素 */
ul {
width: 300px;

/* 清除默认样式 */
list-style: none;

/* 设置为flex布局 */
display: flex;

/* 设置换行 */
flex-flow: wrap;
}

/* 子元素 */
li {
/* 显示为网格布局 */
display: grid;

/* 子元素水平垂直居中 */
place-items: center;

/* 宽高都是100像素 */
width: 100px;
height: 100px;

/* 设置盒模型 */
box-sizing: border-box;

/* 设置1像素的边框 */
border: 1px solid black;

/* 负边距 */
margin: -1px 0 0 -1px;
}

/* 第1、4、7个子元素 */
li:nth-child(3n+1) {
/* 取消左负边距 */
margin-left: 0
}

/* 前三个子元素 */
li:nth-child(-n+3) {
/* 取消上负边距 */
margin-top: 0
}

/* 当鼠标经过时 */
li:hover {
/* 红色字体 */
color: red;

/* 红色边框 */
border: 1px solid;
}
</style>
</head>
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
</ul>
<script>
// 选择ul元素
const ul = document.getElementsByTagName('ul')[0]

// 监听ul元素的点击事件
ul.addEventListener('click', e => alert(e.target.innerText))
</script>
</body>
</html>
复制代码

运行结果:



想知道为什么会这样吗?因为当前这个边框被后面的宫格压住了嘛!那么只需要当鼠标经过时不让后面的压住就好了(调高层级)。


说到调高层级,大家首先想到的可能就是z-index了,这个属性用的最多的地方可能就是绝对定位和固定定位了。但其实很少有人知道,z-index不是只能用在position: xxx的,万能的弹性盒子(display:flex)也是支持z-index的:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
/* 清除默认样式 */
* { padding: 0; margin: 0; }

/* 全屏显示 */
html, body { height: 100% }

body {
/* 网格布局 */
display: grid;

/* 子元素居中 */
place-items: center;
}

/* 父元素 */
ul {
width: 300px;

/* 清除默认样式 */
list-style: none;

/* 设置为flex布局 */
display: flex;

/* 设置换行 */
flex-flow: wrap;
}

/* 子元素 */
li {
/* 显示为网格布局 */
display: grid;

/* 子元素水平垂直居中 */
place-items: center;

/* 宽高都是100像素 */
width: 100px;
height: 100px;

/* 设置盒模型 */
box-sizing: border-box;

/* 设置1像素的边框 */
border: 1px solid black;

/* 负边距 */
margin: -1px 0 0 -1px;
}

/* 第1、4、7个子元素 */
li:nth-child(3n+1) {
/* 取消左负边距 */
margin-left: 0
}

/* 前三个子元素 */
li:nth-child(-n+3) {
/* 取消上负边距 */
margin-top: 0
}

/* 当鼠标经过时 */
li:hover {
/* 红色字体 */
color: red;

/* 红色边框 */
border: 1px solid;

/* 调高层级 */
z-index: 1;
}
</style>
</head>
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
</ul>
<script>
// 选择ul元素
const ul = document.getElementsByTagName('ul')[0]

// 监听ul元素的点击事件
ul.addEventListener('click', e => alert(e.target.innerText))
</script>
</body>
</html>
复制代码

运行结果:



结语


没想到这么一个看似不起眼的九宫格一下子就能考察这么多内容吧!如果面试的时候直接问:



  • 你对 flex 了解的怎么样

  • 当元素的外边距为负值时会有什么样的行为

  • 请实现一下水平垂直居中

  • 了解过 grid 吗

  • 谈一下你对盒模型的理解

  • 说一下事件绑定和事件冒泡

  • CSS3的伪类选择器用的怎么样

  • 当页面元素重叠时如何控制哪个在上哪个在下

  • 在CSS中如何运用变量


直接这么问的话既浪费口舌,又显得很low,而且还不能筛选出真正能够灵活运用技术的候选人。


因为这些问题都不难,一般来说都能答出来,但具体能不能灵活运用就不一定了,而这一道九宫格,就像一面照妖镜一样,瞬间让人原形毕露!


如果你是候选人的话,那么一定要好好练习一下这道题。


如果是面试官的话,那么也推荐你用这道题来考察候选者的技术水平,如果能非常完美的做出来,那么基本上就不用再问其他的CSS题目了,日常开发所用到的样式基本难不倒他/她了,可以直接上JS面试题了。


但如果没做出来也不一定就代表这个人水平不行,可以试着提示一下候选者,然后再问一下其他的CSS题来确定一下此人的水平。


作者:手撕红黑树
来源:https://juejin.cn/post/6886770985060532231
收起阅读 »

仅靠H5标签就能实现收拉效果

前言 最近做项目时碰到这么一个需求: 这有点类似于手风琴效果,但不一样的是很多手风琴效果是同一时间内只能有一个展开,而这个是各个部分独立的,你展不展开完全不会影响我的展开与否。其实这种效果简直再普遍不过了,网上随便一搜就出来一大堆。但不一样的是,我在接到这个...
继续阅读 »

前言


最近做项目时碰到这么一个需求:



这有点类似于手风琴效果,但不一样的是很多手风琴效果是同一时间内只能有一个展开,而这个是各个部分独立的,你展不展开完全不会影响我的展开与否。其实这种效果简直再普遍不过了,网上随便一搜就出来一大堆。但不一样的是,我在接到这个需求的时候突然想起来很久以前看过张鑫旭大佬的一篇文章,模糊的记得那篇文章里说过有个什么很方便的 CSS 属性能够实现这一效果,不用像咱们平时实现的那些展开收起那样写很多的代码,于是就来到他的博客里面一顿搜,找了半天终于发现原来是我记错了,并不是什么 CSS3 属性,而是 HTML5 标签!


details


想要非常轻松的实现一个收拉效果,需要用到三个标签,分别是:<details><summary>以及随意


随意是什么意思?意思是什么标签都可以?


咱们先只写一个<details>标签来看看页面上会出现什么:


<details></details>
复制代码

运行结果:



可以看到非常有意思的一个现象:我们明明什么文字都没有写,但页面上却出现了详细信息这四个字,因为如果你在标签里没有写<summary>的话,浏览器会自动给你补上一个<summary>详细信息</summary>,那有人可能奇怪了,怎么补的是中文呢?那老外不写<summary>的话也会来一个<summary>详细信息</summary>?其实是这样:



现代浏览器经常偷偷获取用户隐私信息,包括但不仅限于用人工智能判断屏幕前的用户是中国人还是外国人,然后根据用户的母语来动态向<summary>标签里加入不同语言的'详细信息'这几个字。




开个玩笑,其实是根据你当前操作系统的语言来判断的,要是你把系统语言改成其它语言的话出现的就不再是'详细信息'这几个中文字符了。


那如果我们在<details>标签里写了<summary>呢?


<details>
<summary>公众号:</summary>
</details>
复制代码

运行结果:



可以看到<summary>里面的文字就会在三角箭头旁边的标题位置展示出来,可是我们展开三角箭头发现里面什么内容也没有,那么内容写在哪呢?


只需写在<summary>的后面就可以了,那是不是还要写个固定标签呢?比如什么<describe>之类的,其实在<summary>之后无论写什么标签都可以,当然必须得是合法的 HTML 标签啊,比如我们写个<h1>标签来试试看:


<details>
<summary>公众号:</summary>
<h1>前端学不动</h1>
</details>
复制代码

运行结果:



再换个别的标签试试:


<details>
<summary>公众号:</summary>
<button>前端学不动</button>
</details>
复制代码

运行结果:



看!我们仅用了三个标签就完成了一个最简单的收拉效果!以前在网上看到类似的效果要么就是 getElementById 获取到 DOM 元素,然后添加 onclick 事件控制下方元素的 style 属性,要么就是纯 CSS 实现,写几个单选按钮配合兄弟选择器来控制后方元素的显隐,抑或是 CSS 与 JS 相结合来实现的,但仅靠 HTML 标签来实现这一效果还是非常清新脱俗的!并且十分简洁、非常节约代码量、也更加直观易于理解。


深入测试


既然<summary>标签后面写什么都行,那么可不可以写很多个标签呢?我们来测试一下:


<details>
<summary>公众号:</summary>
<button>前端学不动</button>
<span>前端学不动</span>
<h1>前端学不动</h1>
<a href="#">前端学不动</a>
<strong>前端学不动</strong>
</details>
复制代码

运行结果:



那展开收起那部分的内容只能放在<summary>标签之后吗?如果放它前面呢:


<details>
<button>前端学不动</button>
<span>前端学不动</span>
<h1>前端学不动</h1>
<a href="#">前端学不动</a>
<strong>前端学不动</strong>
<summary>公众号:</summary>
</details>
复制代码

运行结果:



效果居然一模一样,看来展开收起的那部分应该是在<details>标签内部的除<summary>标签之外的所有内容。那如果写两个<summary>标签呢:


<details>
<button>前端学不动</button>
<span>前端学不动</span>
<h1>前端学不动</h1>
<a href="#">前端学不动</a>
<strong>前端学不动</strong>
<summary>公众号:</summary>
<summary>summary</summary>
</details>
复制代码

运行结果:



可以看到只有第一个出现的<summary>标签是真正的summary,后续出现的其他所有标签(包括其它的<summary>)都是展开收起的那部分。


既然所有标签都可以,那么也包括<details>咯?


<details>
<summary>project</summary>
<details>
<summary>html</summary>
index.html
</details>
<details>
<summary>css</summary>
reset.css
</details>
<details>
<summary>js</summary>
main.js
</details>
</details>
复制代码

运行结果:



这玩意有点意思,利用这种嵌套写法可以轻松实现编辑器左侧的那些文件区的效果。


加入样式


虽然可以很轻松、甚至在不用写 CSS 代码的情况下就实现展开收起效果,但毕竟不写 CSS 只是实现了个最基础的乞丐版效果,很多人都不想要点击的时候出现的那个轮廓:



在谷歌浏览器和 Safari 浏览器下都会出现这个轮廓,火狐就没有这玩意,咱们只需要给<summary>标签设置 outline 属性就可以了,一般如果你的项目引入了抹平浏览器样式间差异的 reset.css 文件的话,就不用写这个 CSS 了,为了方便同时观看 HTML、CSS 和 JS,我们来用 Vue 的格式来写代码:


<template>
<details>
<summary>project</summary>
<details>
<summary>html</summary>
index.html
</details>
<details>
<summary>css</summary>
reset.css
</details>
<details>
<summary>js</summary>
main.js
</details>
</details>
</template>

<style>
summary { outline: none }
</style>
复制代码

运行结果:



这样看起来就舒服多啦!但是还有个问题:那个三角箭头太傻大黑粗了,一般我们很少会用这样的箭头,而且我们也不一定非得让它在左边待着,那么怎么修改箭头的样式呢?


在谷歌浏览器以及 Safari 浏览器下我们需要用::-webkit-details-marker伪元素,在火狐浏览器下我们要用::-moz-list-bullet伪元素,比如我们想让它别那么傻大黑粗:


<template>
<details>
<summary>project</summary>
<details>
<summary>html</summary>
index.html
</details>
<details>
<summary>css</summary>
reset.css
</details>
<details>
<summary>js</summary>
main.js
</details>
</details>
</template>

<style>
summary { outline: none }

/* 谷歌、Safari */
::-webkit-details-marker {
transform: scale(.5);
color: gray
}

/* 火狐 */
::-moz-list-bullet { color: gray }
</style>
复制代码

运行结果:



是不是没那么傻大黑粗了,不过有时我们不想要这个三角形的箭头,想要的是自己自定义的箭头,那么我们就需要先把这个默认的三角给隐藏掉:


<template>
<details>
<summary>project</summary>
<details>
<summary>html</summary>
index.html
</details>
<details>
<summary>css</summary>
reset.css
</details>
<details>
<summary>js</summary>
main.js
</details>
</details>
</template>

<style>
summary { outline: none }

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }
</style>
复制代码

运行结果:



这回箭头没了,我们只需要在<summary>标签里写个箭头就好了,可以用::before::after伪元素,也可以直接在里面写个<img>标签,为了让大家能够直接复制代码到 Vue 环境里运行,在这里我们就不用图片了,直接手写<svg>


<template>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
project
</summary>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
html
</summary>
index.html
</details>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
css
</summary>
reset.css
</details>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
js
</summary>
main.js
</details>
</details>
</template>

<style>
summary {
position: relative;
padding-left: 20px;
outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
position: absolute;
left: 0;
top: 50%;
fill: none;
stroke: gray
}
</style>
复制代码

运行结果:



箭头是变成自定义的了,但是方向却不智能了,不能像原生箭头那样展开收起时会自动改变方向,但是<details>这个标签好就好在它在展开是会自动在标签里添加一个open属性:



我们可以利用它的这一特点,用属性选择器来让<svg>标签进行旋转:


<template>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
project
</summary>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
html
</summary>
index.html
</details>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
css
</summary>
reset.css
</details>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
js
</summary>
main.js
</details>
</details>
</template>

<style>
summary {
position: relative;
padding-left: 20px;
outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
position: absolute;
left: 0;
top: 50%;
transform: rotate(180deg);
transition: transform .2s;
fill: none;
stroke: gray
}

[open] > summary > svg { transform: none }
</style>
复制代码

运行结果:



用 JS 控制 open 属性


既然展开时会自动给<details>标签添加一个open属性,那如果我们用 JS 手动给<details>标签添加或删除open属性,<details>标签会随之展开收起吗?


比如我们用定时器,每隔1秒就自动展开一个,同时收起上一个已被展开过的标签:


<template>
<details v-for="({title, content}, index) of list" :key="title" :open="openIndex === index">
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
{{ title }}
</summary>
{{ content }}
</details>
</template>

<script>
import { defineComponent, ref, onBeforeUnmount } from 'vue'

export default defineComponent(() => {
const list = [{
title: 'html',
content: 'index.html'
}, {
title: 'css',
content: 'reset.css'
}, {
title: 'js',
content: 'main.js'
}]

const openIndex = ref(-1)

const interval = setInterval(() => openIndex.value === list.length
? openIndex.value = 0
: openIndex.value++
, 1000)

onBeforeUnmount(() => clearInterval(interval))

return { list, openIndex }
})
</script>

<style>
summary {
position: relative;
padding-left: 20px;
outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
position: absolute;
left: 0;
top: 50%;
transform: rotate(180deg);
transition: transform .2s;
fill: none;
stroke: gray
}

[open] > summary > svg { transform: none }
</style>
复制代码

运行结果:



既然能靠控制open属性来控制元素的展开收起,那么手风琴效果也很好实现了:只需要保证在当前列表中仅有一个<details>标签有open属性,点击别的标签时就去掉另一个标签的open属性即可:


<template>
<details
v-for="({title, content}, index) of list"
:key="title"
:open="openIndex === index"
@toggle="onChange($event, index)"
>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
{{ title }}
</summary>
{{ content }}
</details>
</template>

<script>
import { defineComponent, ref } from 'vue'

export default defineComponent(() => {
const list = [{
title: 'html',
content: 'index.html'
}, {
title: 'css',
content: 'reset.css'
}, {
title: 'js',
content: 'main.js'
}]

const openIndex = ref(-1)

const onChange = ({ target }, i) => target.open && (openIndex.value = i)

return { list, openIndex, onChange }
})
</script>

<style>
summary {
position: relative;
padding-left: 20px;
outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
position: absolute;
left: 0;
top: 50%;
transform: rotate(180deg);
transition: transform .2s;
fill: none;
stroke: gray
}

[open] > summary > svg { transform: none }
</style>
复制代码

运行结果:




⚠️需要注意的是,在<details>标签展开收起时会触发一个 toggle 事件,和 click、mousemove 等事件用法一致,也会接收一个 event 对象的参数,event.target 是当前触发事件的 DOM,也就是<details>,它会有一个.open属性,值为 true 或 false,代表是否展开收起。



加入动画


那么接下来离一个理想的手风琴效果只差最后一步了:过渡动画


但过渡动画这里有坑,我们先来分析一下思路:在平时就给<details>标签里的内容区(除第一个出现的

标签以外的内容)写上:max-height: 0;

然后在 open 时用属性选择器 [open] 配合后代选择器来给内容区加上 max-height: xxx; 的代码,这样平时在收起时高度就是0,等出现 open 属性时就会慢慢过渡到我们定义的最大高度:


<template>
<details
v-for="({title, content}, index) of list"
:key="title"
:open="openIndex === index"
@toggle="onChange($event, index)"
>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
{{ title }}
</summary>
<ul>
<li v-for="doc of content" :key="doc">{{ doc }}</li>
</ul>
</details>
</template>

<script>
import { defineComponent, ref } from 'vue'

export default defineComponent(() => {
const list = [{
title: 'html',
content: ['index.html', 'banner.html', 'login.html', '404.html']
}, {
title: 'css',
content: ['reset.css', 'header.css', 'banner.css', 'footer.css']
}, {
title: 'js',
content: ['index.js', 'main.js', 'javascript.js']
}]

const openIndex = ref(-1)

const onChange = ({ target }, i) => target.open && (openIndex.value = i)

return { list, openIndex, onChange }
})
</script>

<style>
summary {
position: relative;
padding-left: 20px;
outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
position: absolute;
left: 0;
top: 50%;
transform: rotate(180deg);
transition: transform .2s;
fill: none;
stroke: gray
}

details > ul {
max-height: 0;
margin: 0;
overflow: hidden;
}

[open] > summary > svg { transform: none }
[open] > ul { max-height: 120px }
</style>
复制代码

运行结果:



如果用谷歌浏览器打开的话居然看不到任何的过渡效果!但用火狐打开就有效果:



估计是浏览器的 bug,既然过渡动画(transition)在不同浏览器之间表现不一致,那关键帧动画(keyframes)呢?


<template>
<details
v-for="({title, content}, index) of list"
:key="title"
:open="openIndex === index"
@toggle="onChange($event, index)"
>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
{{ title }}
</summary>
<ul>
<li v-for="doc of content" :key="doc">{{ doc }}</li>
</ul>
</details>
</template>

<script>
import { defineComponent, ref } from 'vue'

export default defineComponent(() => {
const list = [{
title: 'html',
content: ['index.html', 'banner.html', 'login.html', '404.html']
}, {
title: 'css',
content: ['reset.css', 'header.css', 'banner.css', 'footer.css']
}, {
title: 'js',
content: ['index.js', 'main.js', 'javascript.js']
}]

const openIndex = ref(-1)

const onChange = ({ target }, i) => target.open && (openIndex.value = i)

return { list, openIndex, onChange }
})
</script>

<style lang="scss">
summary {
position: relative;
padding-left: 20px;
outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
position: absolute;
left: 0;
top: 50%;
transform: rotate(180deg);
transition: transform .2s;
fill: none;
stroke: gray
}

details > ul {
max-height: 0;
margin: 0;
overflow: hidden;
}

[open] {
> summary > svg { transform: none }
> ul { animation: open .2s both }
}

@keyframes open {
to { max-height: 120px }
}
</style>
复制代码

运行结果:



可以看到关键帧动画在各大浏览器的行为都是一致的,推荐大家使用关键帧动画。


收起动画


上面那种效果已经完全足够满足我们的日常开发需求了,但它仍然有一个小小的遗憾,那就是:收起的时候没有任何的动画效果。



这是因为<details>的行为是靠着 open 属性控制内容显示或隐藏,你可以简单的把它的隐藏理解为display: block;display: none;,虽然这么说可能并不准确,但却非常有助于我们理解<details>的行为:在展开时display: block;突然显示,既然显示了就可以有时间展示我们的展开动画。但在收起时display: none;是突然消失,根本没时间展示我们的收起动画。



那么怎么才能解决这个问题呢?答案就是更改 DOM 结构,我们把原本放在<details>里面那部分需要展开收起的内容元素移到<details>标签的外面去,但一定要在它的后一位,这样就可以方便我们用兄弟选择器配合属性选择器来控制外部元素的显隐了,在<details>标签有 open 属性时我们就让它的后面一个元素用动画展开,没有 open 属性时我们就让后一个元素用动画收起:


<template>
<template v-for="({title, content}, index) of list" :key="title">
<details
:open="openIndex === index"
@toggle="onChange($event, index)"
>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
{{ title }}
</summary>
</details>
<ul>
<li v-for="doc of content" :key="doc">{{ doc }}</li>
</ul>
</template>
</template>

<script>
import { defineComponent, ref } from 'vue'

export default defineComponent(() => {
const list = [{
title: 'html',
content: ['index.html', 'banner.html', 'login.html', '404.html']
}, {
title: 'css',
content: ['reset.css', 'header.css', 'banner.css', 'footer.css']
}, {
title: 'js',
content: ['index.js', 'main.js', 'javascript.js']
}]

const openIndex = ref(-1)

const onChange = ({ target }, i) => target.open && (openIndex.value = i)

return { list, openIndex, onChange }
})
</script>

<style lang="scss">
summary {
position: relative;
padding-left: 20px;
outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
position: absolute;
left: 0;
top: 50%;
transform: rotate(180deg);
transition: transform .2s;
fill: none;
stroke: gray
}

ul {
max-height: 0;
margin: 0;
transition: max-height .2s;
overflow: hidden
}

[open] {
> summary > svg { transform: none }
+ ul { max-height: 120px }
}
</style>
复制代码

运行结果:



结语


如果你的项目不需要这些花里胡哨的动画效果,完全可以只靠 H5 标签去实现,根本不必再去关心展开收起的逻辑了,只需要写一些样式代码就可以了,比如写成暗黑模式:



你的 CSS 只需要专注于暗黑模式本身就够了,是不是很省心呢?


同时这个收拉效果也并不仅仅只适用于手风琴,很多地方都可以用到它,比如这种:


但唯一比较遗憾的事就是这个标签不支持 IE:



不过好在别的浏览器支持的都不错,如果你的项目不需要兼容 IE 的话就请尽情的享受<details>标签所带来的便利吧!


作者:手撕红黑树
来源:https://juejin.cn/post/6912374170743472135
收起阅读 »

后端一次给你10万条数据,如何优雅展示,面试官到底考察我什么

背景面试题:后台传给前端十万条数据,你作为前端如何渲染到页面上?回答者A:我有句话不知当讲不当讲,这什么鬼需求。回答者B:滚,后端,我不要这样的数据,你就不能分页给我吗。回答C:10万条数据这怎么展示,展示了也看不完啊。分析:面试官既然能这么问,我们从技术的角...
继续阅读 »

背景

面试题:后台传给前端十万条数据,你作为前端如何渲染到页面上?

回答者A:我有句话不知当讲不当讲,这什么鬼需求。

回答者B:滚,后端,我不要这样的数据,你就不能分页给我吗。

回答C:10万条数据这怎么展示,展示了也看不完啊。

分析:

面试官既然能这么问,我们从技术的角度出发,探索一下这道题,上手操作了一下:

function loadAll(response) {
  var html = "";
  for (var i = 0; i < 100000; i++) {
      html += "<li>title:" + '我正在测试'+[i] + "</li>";
  }
          $("#content").html(html);
}

在chorme浏览器下面 非常卡顿,刷新页面数据非常卡顿,渲染页面大概花掉10秒左右的时间,卡顿非常明显,性能瓶颈是在将html字符串插入到文档中这个过程上, 也就是性能瓶颈是在将html字符串插入到文档中这个过程上,也就是$("#content").html(html); 这句代码的执行, 毕竟有10万个li元素要被挺入到文档里面, 页面渲染速度缓慢也在情理之中。

解决方案

既然一次渲染10万条数据会造成页面加载速度缓慢,那么我们可以不要一次性渲染这么多数据,而是分批次渲染, 比如一次10000条,分10次来完成, 这样或许会对页面的渲染速度有提升。 然而,如果这13次操作在同一个代码执行流程中运行,那似乎不但无法解决糟糕的页面卡顿问题,反而会将代码复杂化。 类似的问题在其它语言最佳的解决方案是使用多线程,JavaScript虽然没有多线程,但是setTimeout和setInterval两个函数却能起到和多线程差不多的效果。 因此,要解决这个问题, 其中的setTimeout便可以大显身手。 setTimeout函数的功能可以看作是在指定时间之后启动一个新的线程来完成任务。

ajax 请求。。。。

function loadAll(response) {
  //将10万条数据分组, 每组500条,一共200组
  var groups = group(response);
  for (var i = 0; i < groups.length; i++) {
      //闭包, 保持i值的正确性
      window.setTimeout(function () {
          var group = groups[i];
          var index = i + 1;
          return function () {
              //分批渲染
              loadPart( group, index );
          }
      }(), 1);
  }
}

//数据分组函数(每组500条)
function group(data) {
  var result = [];
  var groupItem;
  for (var i = 0; i < data.length; i++) {
      if (i % 500 == 0) {
          groupItem != null && result.push(groupItem);
          groupItem = [];
      }
      groupItem.push(data[i]);
  }
  result.push(groupItem);
  return result;
}
var currIndex = 0;
//加载某一批数据的函数
function loadPart( group, index ) {
  var html = "";
  for (var i = 0; i < group.length; i++) {
      var item = group[i];
      html += "<li>title:" + item.title + index + " content:" + item.content + index + "</li>";
  }
  //保证顺序不错乱
  while (index - currIndex == 1) {
      $("#content").append(html);
      currIndex = index;
  }
}

思考:

面试官为啥会问这样的问题呢?现实中会有这样的需求吗? 我们从技术的角度思考,其实就是考察setTimetout的知识点。面试官就是换汤不换药。当然,其实这道题还有其他的解决方案,可以在评论区讨论学习。


作者:zz
来源:https://juejin.cn/post/6986237263164211207

收起阅读 »