注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

iOS 灵动岛上岛指南

零、关于灵动岛的认识灵动岛,即实时活动(Live Activity)它允许人们以瞥见的形式来观察事件或任务的状态.我的理解是"我不需要一直盯着看,但是我偶尔想看的时候能很方便的看到".这就需要再设计的时候尽可能扔掉没用的信息,保持信息的简洁.实时活动的事件构成...
继续阅读 »

零、关于灵动岛的认识

灵动岛,即实时活动(Live Activity)

它允许人们以瞥见的形式来观察事件或任务的状态.我的理解是"我不需要一直盯着看,但是我偶尔想看的时候能很方便的看到".这就需要再设计的时候尽可能扔掉没用的信息,保持信息的简洁.

实时活动的事件构成最好是包含明确开始 + 结束的事件.例如:外卖、球赛等.

实时活动在结束前最多存活8小时,结束后在锁屏界面最多再保留4小时.

关于更多灵动岛(实时活动)的最佳实践及设计思路可以参考一下:
知乎-苹果开放第三方App登岛,灵动岛设计指南来了!

一、灵动岛的UI布局

接入灵动岛后,有的设备支持(iPhone14Pro / iPhone14ProMax)展示灵动岛+通知中心,有的设备不支持灵动岛则只在通知中心展示一条实时活动的通知.
所以以下四种UI都需要实现:

1.紧凑型


2. 最小型


3. 扩展型


4. 通知


二、代码实现

1.在主工程中创建灵动岛Widget工程

Xcode -> Editor -> Add Target


如图勾选即可


2.在主工程的info.plist中添加key

Supports Live Activities = YES (允许实时活动)

Supports Live Activities Frequent Updates = YES(实时活动支持频繁更新) 这个看项目的需求,不是强制的


3.添加主工程与widget数据交互模型

在主工程中,新建Swift File,作为交互模型的文件.这里将数据管理与模型都放到这一个文件里了.


创建文件后的目录结构


import Foundation
import ActivityKit

//整个数据交互的模型
struct TestWidgetAttributes: ActivityAttributes {
    public typealias TestWidgetState = ContentState
    //可变参数(动态参数)
    public struct ContentState: Codable, Hashable {
        var data: String
    }
    //不可变参数 (整个实时活动都不会改变的参数)

    var id: String

}

如果参数过多.或者与OC混编,默认给出的这种结构体可能无法满足要求.此时可以使用单独的模型对象,这样OC中也可直接构造与赋值.注意,此处的模型需要遵循Codable协议

import Foundation
import ActivityKit

struct TestWidgetAttributes: ActivityAttributes {
    public typealias TestWidgetState = ContentState
    //可变参数(动态参数)
    public struct ContentState: Codable, Hashable {
        var dataModel: TestLADataModel
    }
    //不可变参数 (整个实时活动都不会改变的参数)
    //var name: String
}

@objc public class TestLADataModel: NSObject, Codable {
    @objc var idString : String = ""
    @objc var nameDes : String = ""
    @objc var contentDes : String = ""
    @objc var completedNum : Int//已完成人数
    @objc var notCompletedNum : Int//未完成人数
    var allPeopleNum : Int {
        get {
            return completedNum + notCompletedNum
        }
    }

    public override init() {
        self.nameDes = ""
        self.contentDes = ""
        self.completedNum = 0
        self.notCompletedNum = 0
        super.init()
    }

    /// 便利构造
    @objc convenience init(nameDes: String, contentDes: String, completedNum: Int, notCompletedNum: Int) {
        self.init()
        self.nameDes = nameDes
        self.contentDes = contentDes
        self.completedNum = completedNum
        self.notCompletedNum = notCompletedNum
    }
}

4.Liveactivity widget的UI

打开前文创建的widget,我的叫demoWLiveActivity.swift

这里给出了默认代码的注释,具体的布局代码就不再此处赘述了.

import ActivityKit
import WidgetKit
import SwiftUI

struct demoWLiveActivity: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: demoWAttributes.self) { context in
            // 锁屏之后,显示的桌面通知栏位置,这里可以做相对复杂的布局
            VStack {
                Text("Hello")
            }
            .activityBackgroundTint(Color.cyan)
            .activitySystemActionForegroundColor(Color.black)
        } dynamicIsland: { context in
            DynamicIsland {
                /*
                 这里是长按灵动岛[扩展型]的UI
                 有四个区域限制了布局,分别是左、右、中间(硬件下方)、底部区域
                 */
                DynamicIslandExpandedRegion(.leading) {
                    Text("Leading")
                }
                DynamicIslandExpandedRegion(.trailing) {
                    Text("Trailing")
                }
                DynamicIslandExpandedRegion(.center) {
                    Text("Center")
                }
                DynamicIslandExpandedRegion(.bottom) {
                    Text("Bottom")
                    // more content
                }
            } compactLeading: {
                // 这里是灵动岛[紧凑型]左边的布局
                Text("L")
            } compactTrailing: {
                // 这里是灵动岛[紧凑型]右边的布局
                Text("T")
            } minimal: {
                // 这里是灵动岛[最小型]的布局(有多个任务的情况下,展示优先级高的任务,位置在右边的一个圆圈区域)
                Text("Min")
            }
            .widgetURL(URL(string: "http://www.apple.com"))
            .keylineTint(Color.red)
        }
    }
}

5.Liveactivity 的启动 / 更新(主工程) / 停止

启动

let attributes = TestWidgetAttributes()
let initialConetntState = TestWidgetAttributes.TestWidgetState(dataModel: dataModel)
do {
let activity = try Activity.request(
attributes: attributes,
content: .init(state: initialConetntState, staleDate: nil),
pushType: nil
//              pushType: .token
)
print("请求开启实时活动: \(activity.id)")
} catch (let error) {
print("请求开启实时出错: \(error.localizedDescription)")
}

更新

let updateState = TestWidgetAttributes.TestWidgetState(dataModel: dataModel)
let alertConfig = AlertConfiguration(
title: "\(dataModel.nameDes) has taken a critical hit!",
body: "Open the app and use a potion to heal \(dataModel.nameDes)",
sound: .default
)
await activity.update(
ActivityContent<TestWidgetAttributes.ContentState>(
state: updateState,
staleDate: nil
),
alertConfiguration: alertConfig
)
print("更新实时活动: \(activity.id)")

结束

let finalContent = TestWidgetAttributes.ContentState(
dataModel: TestLADataModel()
)
let dismissalPolicy: ActivityUIDismissalPolicy = .default
await activity.end(
ActivityContent(state: finalContent, staleDate: nil),
dismissalPolicy: dismissalPolicy)
removeActivityState(id: idString);
print("结束实时活动: \(activity)")

三、更新数据

数据的更新主要通过两种方式:

1.服务端推送

2.主工程更新

其中主工程的更新参见(2.5.Liveactivity 的启动 / 更新(主工程) / 停止)

这里主要讲通过推送方式的更新

首先为主工程开启推送功能,但不要使用registerNotifications()为ActivityKit推送通知注册您的实时活动,具体的注册方法见下.

1. APNs 认证方式选择

APNs认证方式分为两种:

1.cer证书认证

2.Token-Based认证方式

此处只能选择Token-Based认证方式,选择cer证书认证发送LiveActivity推送时,会报TopicDisallowed错误.

Token-Based认证方式的key生产方法 参见:Apple Documentation - Establishing a token-based connection to APNs

2. Liveactivity 的启动

let attributes = TestWidgetAttributes()
let initialConetntState = TestWidgetAttributes.TestWidgetState(dataModel: dataModel)
do {
let activity = try Activity.request(
attributes: attributes,
content: .init(state: initialConetntState, staleDate: nil),
pushType: .token//须使用此值,声明启动需要获取token
)
//判断启动成功后,获取推送令牌 ,发送给服务器,用于远程推送Live Activities更新
//不是每次启动都会成功,当已经存在多个Live activity时会出现启动失败的情况
print("请求开启实时活动: \(activity.id)")
Task {
for await pushToken in activity.pushTokenUpdates {
let pushTokenString = pushToken.reduce("") { $0 + String(format: "x", $1) }
//这里拿到用于推送的token,将该值传给后端
pushTokenDidUpdate(pushTokenString, pushToken);
}
}
} catch (let error) {
print("请求开启实时出错: \(error.localizedDescription)")
}

3. 模拟推送

1.可以使用terminal简单的构建推送,这里随便放上一个栗子

2.也可以使用这个工具 - SmartPushP8

此处使用SmartPushP8


发出推送后,在设备上查看推送结果即可.

注意:模拟器也是可以收到liveactivity的推送的但是需要满足:使用T2安全芯片 or 运行macOS13以上的 M1 芯片设备

四、问题排查

推送失败:

1.TooManyProviderTokenUpdates

测试环境对推送次数有一定的限制.尝试切换线路(sandbox / development)可以获得更多推送次数.

如果切换线路无法解决问题,建议重新run一遍工程,这样可以获得新的deviceToken,完成推送测试.

2.InvalidProviderToken / InternalServerError

尝试重新选择证书,重新run工程吧...暂时无解.

推送成功,但设备并未收到更新

这种情况需要打开控制台来观察日志

1.选择对应设备

2.点击错误和故障

3.过滤条件中添加这三项进程

liveactivitiesd
apsd
chronod

4.点击开始

在下方可以看到错误日志.

demo参考:demo


作者:大功率拖拉机
链接:https://juejin.cn/post/7254101170951192613
来源:稀土掘金

收起阅读 »

因为美工小妹妹天还没亮就下班,我做了一版可以把svg文件转成web组态组件的svg编辑器

web
自我介绍 本猫生活在东北的一个四线小城市,目前在一家小单位任职前端工程师的职位。早八晚五,生活充实,好不快活! 噩耗 Ctrl+c,Ctrl+v。Ctrl+c,Ctrl+v。新的一天开始了,本猫正在努力的工作着。看着旁边的美工小妹妹,我的口水止不住的往下流,别...
继续阅读 »

自我介绍


本猫生活在东北的一个四线小城市,目前在一家小单位任职前端工程师的职位。早八晚五,生活充实,好不快活!


噩耗


Ctrl+c,Ctrl+vCtrl+c,Ctrl+v。新的一天开始了,本猫正在努力的工作着。看着旁边的美工小妹妹,我的口水止不住的往下流,别误会,是因为公司没有几个人,所以我们每天中午都在一起吃饭,而且我本来身为一个吃货,想到午饭就会流口水,很合理吧。


正当我沉迷在幻想之际,手机的一声震动把我拽回到现实。


“来我办公室一趟!”

竟然是老板给我发的消息!!!

难不成是我每天辛苦的工作被他发现了?要给我涨工资?

我一溜烟的从工位冲了出去,直奔老板的办公室。

这一路上我想了很多:想我这么多年的辛劳付出终于达到了回报、想我迎娶白富美的现场何其壮观、想我出任ceo之后回村又是多么的风光......

终于在三秒之后,我来到了老板的办公室,见到了我最敬爱的老板。


“猫啊,听说你最近表现不错”

还没等我说话,老板又接着说到:

“咱们公司最近新接了一个项目要交给你完成,你有没有信心啊?”

我拍了拍胸脯:

“必须的!老板您说让我做什么我就做什么”

然后老板对我展开了需求攻势:

“巴拉巴拉,如此如此,这般这般,巴巴拉拉加班拉拉巴巴”

“什么?加班!”

其实老板说了那么多我是左耳听,右耳冒,但是加班这两个重重的砸在了我心上。我突然感觉一股热流冲到了头顶,双腿也止不住的颤抖,随即我双手用力的支撑在了老板的办公桌上,才勉强地站稳脚跟。

老板似乎也看出了我的不适,继续说道:

“小猫啊!你年龄这么小,正是应该努力奋斗的时候,我像你这么大的时候每天只睡十分钟,才有了今天的成就。巴拉巴拉。。。。”

听着老板的经历,我不由得留下了感动的泪水。原来老板是如此的器重我,耐心的劝导我只为了让我成长,想到我刚才还想向老板提涨工资的事,我的脸唰的一下就红了。我赶紧向老板鞠了一躬,猫着腰往办公室外面跑。

“谢谢老板关心,我一定完美完成任务!”

“公司就你一个前端,一定得好好做啊!”

随着老板的声音越来越小,我知道我已经离开了老板的办公室,我挺起胸膛,这一刻我仿佛重生了一般,浑身充满了干劲。迈着自信的步伐,我回到了我的工位。


打开电脑,老板已经把项目需求文档发给了我,打开1个Gdoc文件,首页赫然印着几个大字:项目工期一个月


我眼前一黑,晕了过去


曙光


“醒醒!醒醒!你怎么睡着了呢?”

我闻到一股淡淡的清香味,原来是我旁边的美工小妹妹在叫我。

“老板发你的新需求的项目文档你看了吧?老板让我们一起做这个项目”

“看到了看到了”

我惊喜着大叫一声,丝毫不顾忌周围同事的目光。我把头发往后捋了捋,望向我旁边的美工小妹妹:

“放心吧,包在我身上!”

“好的吧,那我先回去了,需要我做什么跟我说吖!”

我比了一个ok的手势,目送美工小妹妹转身回到了她的工位上。


需求分析


于是乎,我赶紧打开了文档,细心的分析了这个项目的需求。

原来这个项目是希望完成一个web端的拓扑图,要求使用svg技术实现,布局大概是长这样:


布局.png


功能呢,也不复杂,可以从左侧的节点区把节点拖到画布,在画布上选中绘制的节点可以缩放旋转,在右侧属性区可以快捷的更改节点的一些属性


美工小妹妹就负责配合我去设计svg的图形


我冷笑一声,这还不简单?真是小看了我这个代码吸血鬼。我打开搜索引擎,用出了我的绝招



“无敌暴龙cv大法”



不出半天,上面的布局就被我给做好了。


根据项目需求还需要去实现拖动的功能,凭借我多年的工作经验,我很快便找到了如下可以进行参考的文章:


一个低代码(可视化拖拽)教学项目


详细的拖拽需求以及缩放旋转的操作这篇文章里都有讲,我这里就不重复赘述了,经过了几天没日没夜的开发,主要说下我遇到的问题吧!


上面文章包含的项目实际上是采用定位来实现的,本身也是支持集成svg文件的,我们先看一下svg文件如何集成:


svg节点定义:


<template>
   <div class="svg-star-container">
       <svg xmlns="http://www.w3.org/2000/svg" version="1.1">
           <circle cx="100" cy="50" r="40" stroke="black" stroke-width="2" :fill="fill" />
       </svg>
   </div>
</template>

<script>
export default {
   props: {
       fill: {
           type: String,
           require: true,
           default: '#ff0000',
      },
  },
}
</script>

右侧属性面板定义:


<template>
   <div>
       <el-form>
           <el-form-item label="填充色">
               <el-color-picker v-model="curComponent.fill" />
           </el-form-item>
       </el-form>
   </div>
</template>

上面这是我新集成的一个svg图形,里面只有一个圆形,我这里希望可以在右侧改变圆形的填充(fill)颜色,集成之后的效果就是这样的:


集成效果.png


其实本质上是可以满足我们的需求的,因为我们也就是想改改svgfill或者是stroke这些基本属性的,但是我们的项目大概有300多个组件,这样一来我需要写600多vue文件。上面的示例我只是演示了如何更改svgfill属性,没做缩放适配效果是这样的:


未适配.png


所以我还需要手动调整每个文件去适配缩放,这个工作量堪比吨级,我的脑中顿时思绪万千:


想起来这几日美工小妹妹准时下班向我发我来的甜蜜问候


美工下班.png


想起来昨天向公司申请换键盘的尴尬经历


申请键盘.png


望着手机屏幕反射出连续高强度加班憔悴的我,看着我新换回来CV按键清晰可见的键盘,看着美工小妹妹键盘上那纤细的手指,我不由得感叹道:这么好看的手,不多做几个图可惜了。于是乎我的脑海里萌生了一个大胆的想法:



我要写个逻辑自动把svg文件转成组件,每个svg文件写个配置文件就能在右侧属性区动态的设置svgfillstroke等属性!!!



理想很丰满,现实很骨感


开始我是打算用viteraw以字符串的方式加载svg文件,再用v-html指令将字符串渲染成html


<script setup lang="ts">
import testSvgString from '../assets/vue.svg?raw'
</script>

<template>
 <div>
   <div v-html="testSvgString"></div>
   <div v-html="testSvgString"></div>
 </div>
</template>



效果是这样的:


raw效果.png


这个方案确实行得通,不过如果真正绘制图像的话,会导致dom节点变的很多很多。


dom节点.png


而且现在还没有考虑怎么适应svg节点的实际大小,怎么动态的改变节点fill等基本属性。。。


越想头越大,迷迷糊糊中我仿佛身处在一片森林中,映入我眼帘的是一个正在从我面前河里飘出来的白发老人


“少年哟,你掉的是这个<symbol>标签呢?还是这个<use>标签呢?”


我猛然惊醒


皇天不负有心人


对啊,之前我们用过的一个项目里面的icon图标就是一个个文件,当时是用的这个插件vite-plugin-svg-icons


它可以把svg文件加载成symbol标签,然后在需要的地方用use标签引用就可以了


说干就干,于是我赶紧熟练的打开了搜索引擎,开始了我的求知之路


终于


这个基于 vue3.2+ts 实现的 svg 可视化 web 组态编辑器项目诞生了!


项目介绍


svg文件即组件,引入并编写好配置文件后之后无需进行额外配置,编辑器会自适应解析加载组件。
同时支持自定义svg组件和传统的vue组件


开源地址


github


gitee


在线预览


绘画


选中左侧的组件库,按住鼠标左键即可把组件拖动到画布中


绘画.gif


操作


选中绘制好的节点后会出现锚点,可以直接进行移动、缩放、旋转等功能,右侧属性面板可以设置配置好的节点的属性,鼠标右键可以进行一些快捷操作


操作.gif


连线


鼠标移动到组件上时会出现连线锚点,左键点击锚点创建线段,继续左键点击画布会连续创建线段,右键停止创建线段,鼠标放在线段上会出现线段端点提示,拖动即可重新设置连线,选中线段后还可以在右侧的动画面板设置线段的动画效果


连线.gif


支持集成到已有项目


脚手架项目


# 创建项目(已有项目跳过此步骤)
npm init vite@latest

# 进入项目目录
cd projectname

# 安装插件
pnpm i webtopo-svg-edit

# 安装pinia
pnpm i pinia

# 修改main.ts 注册pinia
import { createPinia } from 'pinia';
const app = createApp(App);
app.use(createPinia());
app.mount('#app')

#在需要的页面引入插件
import { WebtopoSvgEdit,WebtopoSvgPreview } from 'webtopo-svg-edit';
import 'webtopo-svg-edit/dist/style.css'

umd方式集成


<!DOCTYPE html>
<html>
 <head>
   <title>webtopo-svg-edit Example</title>
   <link href="https://unpkg.com/webtopo-svg-edit@0.0.8/dist/style.css" rel="stylesheet" />
   <script src="https://unpkg.com/vue@3.2.6/dist/vue.global.prod.js"></script>
   <script src="https://unpkg.com/vue-demi@0.13.11/lib/index.iife.js"></script>
   <script src="https://unpkg.com/pinia@2.0.33/dist/pinia.iife.prod.js"></script>
   <script src="https://unpkg.com/webtopo-svg-edit@0.0.8/dist/webtopo-svg-edit.umd.js"></script>
 </head>
 <body>
   <div id="app"></div>
   <script>
     const pinia = Pinia.createPinia()
     const app = Vue.createApp(WebtopoYLM.WebtopoSvgEdit)
     app.use(pinia)
     app.mount('#app')
   
</script>
 </body>
</html>


es module方式集成


<!DOCTYPE html>
<html>
 <head>
   <title>webtopo-svg-edit Example</title>
   <link href="https://unpkg.com/webtopo-svg-edit@0.0.8/dist/style.css" rel="stylesheet" />
 </head>
 <body>
   <div id="app"></div>
 </body>
</html>
<script type="importmap">
{
  "imports": {
    "vue": "https://unpkg.com/vue@3.2.47/dist/vue.esm-browser.prod.js",
    "@vue/devtools-api": "https://cdn.jsdelivr.net/npm/@vue/devtools-api/lib/esm/index.min.js",
    "vue-demi": "https://unpkg.com/vue-demi@0.13.11/lib/index.mjs",
    "pinia": "https://unpkg.com/pinia@2.0.29/dist/pinia.esm-browser.js",
    "WebtopoYLM": "https://unpkg.com/webtopo-svg-edit@0.0.8/dist/webtopo-svg-edit.es.js"
  }
}
</script>
<script type="module">
 import { createApp } from 'vue'
 import { createPinia } from 'pinia'
 import { WebtopoSvgEdit } from 'WebtopoYLM'
 const app = createApp(WebtopoSvgEdit)
 app.use(createPinia())
 app.mount('#app')
</script>


后记


“报告老板,以前一个月的工作,现在7天就能做完!”


“小伙子你很有前途,等公司赚钱了一定不会忘了你的!”


“老板这是哪里话,牛马的命也是命,当牛做马是我的荣幸!”


“行了,没什么事就去忙吧,7天之后等你们的好消息!”


凌晨三点,我看着美工小妹妹忙碌的身影,不由得嘴角上扬


嘿嘿!天

作者:咬轮猫
来源:juejin.cn/post/7260126013111812153
还没亮,谁也别想走!

收起阅读 »

北漂五年,我回家了。后悔吗?

2017年毕业后,我来到了北京,成为了北漂一族。五年后,我决定回家了。也许是上下班一个多小时的通勤,拥挤的地铁压得我喘不过气;也许是北漂五年依然不能适应干燥得让人难受的气候。北京很大,大得和朋友机会要提前两个小时出门;北京很小,我每天的活动范围就只有公司、出租...
继续阅读 »

2017年毕业后,我来到了北京,成为了北漂一族。五年后,我决定回家了。也许是上下班一个多小时的通勤,拥挤的地铁压得我喘不过气;也许是北漂五年依然不能适应干燥得让人难受的气候。北京很大,大得和朋友机会要提前两个小时出门;北京很小,我每天的活动范围就只有公司、出租屋两点一线。今年我觉得是时候该回家乡了。


1280X1280 (1).JPEG


(在北京大兴机场,天微微亮)


有些工作你一面试就知道是坑


决定回家乡后,我开始更新自己的简历。我想过肯定会被降薪,但是没想到降薪幅度会这么大,成都前端岗位大多都是1w左右,想要双休那就更少了。最开始面试的一些岗位是单休或者大小周,后面考虑了一下最后都放弃了。那时候考虑得很简单,一是我没开始认真找工作,只是海投了几个公司,二是我觉得我找工作这儿时间还比较短,暂时找不到满意的很正常。


辞职后,我的工作还没有着落,于是决定先不找了,出去玩一个月再说。工作了这么久,休息一下不为过吧,于是在短暂休息了一个月后,我又开始认真找工作。


但是,但是没想到成都的就业环境还蛮差的,找工作的第二个月还是没有合适的,当时甚至有点怀疑人生了,难道我做的这个决定是错误的?记得我面试过一家公司,那家公司应该是刚刚成立的,boss上写的员工数是15个,当时我想着,刚成立的公司嘛,工资最开始低点也行,等公司后续发展起来了,升职加薪岂不美滋滋。


面试时,我等了老板快半小时,当时我对这家公司的观感就不太好了。但想着来都来了,总不能浪费走的这一趟。结果,在面试的时候老板开始疯狂diss我的技术不行,会的技能太少,企图用这种话来让我降薪。我是怎么知道他想通过这种方式让我降薪呢,因为最后那老板说“虽然你技术不行,但是我很看好你的学习能力,给你开xxx工资你愿意来吗?”


也是因为这次面试,我在招聘软件上看到那种小公司都不轻易去面试了,简直浪费我时间。


1280X1280.JPEG


(回家路上骑自行车等红绿灯,我的地铁卡被我甩出去了,好险,但是这张地铁卡最后还是掉了,还是在我刚充值完100后,微笑)


终于,找了大概3个月,终于找到一家还算不错的公司,在一家教育行业的公司做前端。双休,工资虽然有打折,但是在我能接受的范围内。


有些人你一见面就知道是正确的


其实我打算回家乡还有一个重要原因是通过大厂相亲角网恋了一个女孩子,她和我是一个家乡的。我们刚认识的时候几乎每天都在煲电话粥,基本上就是陪伴入眠,哈哈哈哈哈。语言的时候她还会唱歌给我听,偏爱、有可能的夜晚......都好好听,声音软绵绵的。认识一个月后,我们回了一趟成都和她面基。一路上很紧张,面基的时候也很害怕自己有哪里做得不好的地方,害怕给她留下不好的印象。我们面基之后一个月左右就在一起啦。有些人真的是你一见面就知道她是正确的那个人,一见面心里有一个声音告诉你“嗯,就是她了!”。万幸,我遇到了。


58895b3fc4db3554881bdbcaa35384f.jpg


1280X1280 (2).JPEG


说一些我们在一起后的甜蜜瞬间吧


打语言电话的时候,听着对方的呼吸声入睡;


走在路上的时候,我牵她的手,她会很顺其自然地与我十指相扣;


在一起吃饭的时候,她会把自己最好吃的一半分享给我;



总结


回到正题,北漂五年。我回家了,后悔吗?不后悔。离开北京快一年了,有时候还是会想念自己还呆在北京的不足10平米的小出租屋里的生活,又恍惚“噢,我已经回四川了啊”。北漂五年,我还是很感激那段时间,让刚毕业的我迅速成长成可以在工作上独当一面的合格的程序员,让我能有拿着不菲的收入,有一定的积蓄,有底气重新选择;感谢大厂相亲角,让我遇见我的女朋友,让我不再是单身狗。

作者:川柯南
来源:juejin.cn/post/7152045204311113736

收起阅读 »

被约谈,两天走人,一些思考

五档尼卡引爆全场 前言 个人身边发生的事,分享自己的一些思考,有不同意见是正常的,欢迎探讨交流 来龙去脉 上周坐我前面的前端开发工程师突然拿了张纸去找业务线领导签字了,领导坐我旁边,我看两人表情都认真严肃,一句话没说,那个前端同事签完字就坐自己工位上了,似乎...
继续阅读 »

五档尼卡引爆全场



前言


个人身边发生的事,分享自己的一些思考,有不同意见是正常的,欢迎探讨交流


来龙去脉


上周坐我前面的前端开发工程师突然拿了张纸去找业务线领导签字了,领导坐我旁边,我看两人表情都认真严肃,一句话没说,那个前端同事签完字就坐自己工位上了,似乎有什么事发生


微信上问了一句:什么情况?


前端同事:裁员,最好准备



公司现状


从我去年入职公司后,就在喊降本增效了,周一晨会时不时也会提一下降本增效,毕竟大环境不好,公司的业务这两年也受到不小的影响


今年好几个项目组人手不够,两三月前还在疯狂面试前后端测试产品,我们这边的业务线前端都面试都超过五十个人了,最后招了一个前端一个后端一个测试


想着这种情况,公司薪资给的也不高,新员工不大量招就算降本了吧,再优化优化各方面流程等提提效率,没想到降本的大刀直接落下来首先砍掉的是技术开发人员


裁员情况


公司北京总部这边,目前我们部门是裁了两个前端一个后端,其他部门也有有裁员,人数岗位就不清楚了


从被裁掉的同事那边了解到的消息,上周三下班后下午找他们谈的,周四交接,周五下班后就走了,按照法律规定赔偿


上周只是一个开始,应该是边裁边看,什么时候结束最终裁员比例目前还不清楚,由其他来源得到的消息来源说是这次裁员力度很大


现在如果不是核心项目员工,如果不是和领导关系比较好的员工,每个人头上都悬着一把达摩克利斯之剑


个人思考


看待裁员


我认为首先是放平心态吧


国际经济形去全球化,贸易战,疫情,到现在的各种制裁,俄乌战争等,极端气候频发,真是多灾多难的年代


裁员这几年大家也见多了,该来的总会来


我认为裁员好说,正常赔偿就行,好聚好散,江湖再见


企业层面


裁员也是企业激发组织活力的一种方式,正常看待就行,关于企业组织活力介绍的,这里推荐一本前段时间刚读完的一本书 《熵减:华为活力之源》



熵是来源于物理科学热力学第二定律的概念,热力学第二定律又称熵增定律。熵增表现为功能减弱直到逐渐丧失,而熵减表现为功能增强...



个人层面


1.如果公司正常走法律流程,拿赔偿走人,继续找工作,找工作的过程也能发现自己的不错,更加了解市场,甚至倒逼自己成长


2.如果公司只想着降低成本,不做人事,有那种玩下三滥手段的公司,一定要留好证据,拍照,录音,截图,保存到自己的手机或者云盘里,不想给赔偿或恶意玩弄手段的,果断仲裁,我们员工相对企业来讲是弱势群体,这时候要学会用法律武器保护自己(可能也是唯一的武器)



这年头行情不好,老板损失的可能只是近期收益,有的员工失去的可能是全家活下去的希望



日常准备


做好记录


日常自己工作上的重大成果,最好定期梳理一下,或者定期更新简历,也可以不更新简历,找地方记录下来,例如项目上的某个重大模块的开发升级,或者做的技术上的性能优化等,我是有写笔记博客的习惯,技术相关的有时间一般会写成文章发到社区里


保持学习


日常保持学习的基本状态,这个可能我们每个人都会有这个想法,但是能定期沉下心来去学习提升,系统地去提升自己的时候,很少能坚持下来,万事开头难,开头了以后剩下的是坚持,我自己也是,有些事情经常三天打鱼,两天晒网,一起加油


关注公司


如果公司有查考勤,或者重点强调考勤了,一般都是有动作了,我们公司这次就是,年中会后的第二周吧,大部门通报考勤情况,里面迟到的还有排名,没多久就裁员了


保护自己


有的公司可能流程操作不规范,也有的可能不想赔偿或者少赔偿,可能会在考勤上做文章,例如迟到啥的,如果公司有效益不好的苗头,一定要注意自己这方面的考勤,以及自己的绩效等,做好加班考勤截图,领导HR与自己的谈话做好录音,录屏等,后面可能用的上,也可能会让自己多一点点谈判筹码


经营关系


虽然裁员明面上都是根据工作表现来的,好多时候大家表现都差不多,这个时候就看人缘了,和领导关系好的,一般都不是优先裁员对象,和领导团队成员打成一片真的很重要



以前我还有过那种想法:


我一个做技术的,我认真做好我自己的工作不就行了?专心研究技术,经过多年的工作发现,很多时候真的不行,我们不是做的那种科研类的,只有自己能搞,国内的大部分软件开发岗可能都是用的开源的技术做业务相关的,这种没什么技术难度,技术上来看基本没有什么替代性的难度


可能可替代性比较难的就是某个技术人长期负责的某个大模块,然后写了一堆屎山吧,毕竟拉屎容易,吃屎难


越是优秀的代码,可读性越强,简洁优雅,像诗一样



关于简历


如果是刚毕业的,可能简历上还好,大部分都优化都是已经是有一定的工作经验了,简历的更新就比较重要了,尤其工作了两三年了,如果简历看起来内容很少,不是那么丰富或者看起来很简陋,在简历筛选这一关会降低自己的面试几率,这时候一定要丰富一下,也有一些可能不知道自己简历是否丰富的,网上有那种简历模板可以搜搜看看,也可以找大佬帮忙看看,也有技术圈提供简历优化的有偿服务


再找工作


我个人的感觉是如果还是继续老本行继续打工,这年头行情不好,最好第一时间找工作,不能因为拿了赔偿就想着休一个月再说之类的,我周围有那种本来准备休半个月或一个月的,结果一下子休了一年以上的,我面试的时候如果遇到那种空窗期很长的,如果第一轮技术面能力都差不多的情况,到第二轮的领导面或者HR面,他们有优先考虑让空窗期短的人加入


关于空窗期


基本所有的公司都会关注离职空窗期,如果这个空窗期时间长了,那么求职的竞争力会越来越小,我在面试的时候我也会比较关注空窗期,因为我会有如下思考(举个例子,纯属乐子哈)


1.为什么这个人求职者三个月多了不找工作,家里有矿?家里有矿还上班,工作不会是找个地方打发时间的吧



我朋友的朋友就是这样,北京土著,家中独子,前几年拆迁了,家里好几套房,自己开俩车,人家上班就是找地方交个社保,顺便打发一下时间




2.能力不行吗?找工作这么久都没找到,是太菜了吗?还是太挑剔了?长时间不敲代码,手也生疏了,来我们团队行不行呀,我们这里赶项目压力这么大,招进来万一上手一段时间干不了怎么办,自己还被牵连了



几年前在某家公司做团队leader的时候,我们做的又是AI类项目,用的技术也比较前沿,当时AI的生态还不完善,国内做AI的大部分还处于摸索阶段,项目中用的相关技术栈也没个中文文档,由于公司创业公司,价格给的很低,高手招不进来,没办法只能画饼招综合感觉不错的那种,结果好几个人来了以后又是培训,又是有把手把手教的,结果干了没多久干不动走了,留下的烂摊子还得自己处理



关于社保


如果自己家里没矿,最好还是别让社保断了,拿北京举例,社保断了影响医疗报销,影响买车摇号等等


如果实在没找到工作,又马上要断缴社保了,可以找个第三方机构帮忙代缴,几千块钱,这时候的社保补缴相对来讲代价就比较高了



我遇到的情况是,社保断了一个月,后来找到工作了,第三方机构补缴都补不了,后来一通折腾总算弄补缴上了



关于入职


先拿offer,每一家公司的面试都认真对待,抱着一颗交流开放互相尊重的心


如果自己跳槽频繁,再找公司,可能需要考虑一下自己是否能够长待了,跳槽越频繁,后面找工作越困难,没有哪个公司希望招的人干一年就走了


所以面试结束后,最好根据需要面试情况,以及网上找到的资料,分析一下公司的业务模式了,分析这家公司的行业地位,加入的业务线或者部门是否赚钱,所在的团队在公司属于什么情况,分析团队是否是边缘部门,招聘的业务线是否核心业务线,如果不是核心业务线,可能过段时间效益不好还会被砍掉,有时候虽然看拿了对应的赔偿,但是再找工作,与其他同级选手对比的话,竞争力会越来越低


不论是技术面试官,还是负责面试的HR,大部分也都是公司的普通员工,他们可能不会为公司考虑,基本都会为自己考虑的,万一招了个瘟神到公司或者团队里,没多久自己团队也解散了怎么整



这里也许迷信了,基于我的一些经历来看有些人确实会有一些人是看风水,看人分析运势的


之前在创业公司的时候,有幸和一些投资人,上市公司的总裁,央企董事长等所谓的社会高层接触过,越是那些顶级圈里的人,有些人似乎很看中这个,他们有人研究周易,有人信仰佛教,有人招聘必须看人面相,有人师从南怀瑾等等



再次强调


每个人的经历,认知都是不一样的,同样的人不同角度下的世界也是不一样的,有不同意见是非常正常的,欢迎探讨交流不一样的心得,互相学习,共同进步


作者:草帽lufei
来源:juejin.cn/post/7264236820725366840
收起阅读 »

Flutter:创建和发布一个 Dart Package

在 Dart 生态系统中使用 packages(包) 实现代码的共享,比如一些 library 和工具。本文旨在介绍如何创建和发布一个 package。 通常来讲,我们所说的 package 一般都是指 library package,即可以被其他的 pac...
继续阅读 »

在 Dart 生态系统中使用 packages(包) 实现代码的共享,比如一些 library 和工具。本文旨在介绍如何创建和发布一个 package。


通常来讲,我们所说的 package 一般都是指 library package,即可以被其他的 package 所依赖,同时它自身也可以依赖其他 package。本文中说的 package 也都默认是指 library package


1.package 的组成


下图展示了最简单的 library package 布局:








  • library package 中需要包括 pubspec.yaml 文件lib 目录





  • library 的 pubspec.yaml 文件和应用程序的 pubspec.yaml 没有本质区别。





  • library 的代码需要位于 lib 目录 下,且对于其他 package 是 公开的。你可以根据需要在 lib 下创建任意目录。但是如果你创建的目录名是 src 的话,会被当做 私有目录,其他 package 不能直接使用。目前一般的做法都是把代码放到 lib/src 目录下,然后将需要公开的 API 通过 export 进行导出。




2.创建一个 package


假设我们要开发一个叫做 yance 的 package。


2.1 通过 IDE 创建一个 package








我们来看看创建好的一个 package 工程的结构:





可以看到 lib 目录和 pubspec.yaml 文件已经默认给我们创建好了。


2.2 认识 main library


我们打开 lib 目录,会发现有一个默认和 package 项目名称同名的 dart 文件,我们把这个文件成为 main library。因为我的 package 名称是 yance,因此,我的 main libraryyance.dart





main library 的作用是用来声明所有需要公开的 API。


我们打开 yance.dart 文件:


library yance;

/// A Calculator.
class Calculator {
  /// Returns [value] plus 1.
  int addOne(int value) => value + 1;
}

第一行使用 library 关键字。这个 library 是用来为当前的 package 声明一个唯一标识。也可以不声明 library,在不声明 library 的情况下,package 会根据当前的路径及文件生成一个唯一标记。


如果你需要为当前的 package 生成 API 文档,那么必须声明 library。


至于 library 下面的 Calculator 代码只是一个例子,可以删除。


前面说了 main library 的作用是用来声明公开的 API,下面我们来演示一下,如何声明。


2.3 在 main library 中公开 API


我们在 lib 目录下新建一个 src 目录,后面所有的 yance package 的实现代码都统一放在 src 目录下,记住,src 下的所有代码都是私有的,其他项目或者 package 不能直接使用。


我们在 src 目录下,创建一个 yance_utils.dart 文件,在里面简单写一点测试代码:


class YanceUtils {
  /// Returns [value] plus 1.
  int addOne(int value) => value + 1;
}

好了,现在需求来了,我要将 YanceUtils 这个工具类声明为一个公开的 API ,好让其他项目或者 package 可以使用。


那么就需要在 yance.dart 这个 main library 中使用 export 关键字进行声明,格式为:


export 'src/xxx.dart';

输入 src 关键字,然后选择 src/ 这个路径:





然后再输入 yance_utils.dart 即可:


library yance;

export 'src/yance_utils.dart';

这样就完成了 API 的公开,yance_utils.dart 里面所有的内容,都可以被其他项目所引用:


import 'package:yance/yance.dart';

class MyDemo{
  void test() {
    var yanceUtils = YanceUtils();
    var addOne = yanceUtils.addOne(1);
    print('结果:$addOne}');
  }
}

此时,可能大家会有个疑问,使用 export 'src/xxx.dart' 的方式,会将该 dart 文件里所有的内容都完全公开,那假如该文件里的内容,我只想公开一部分,该如何操作呢?


需要使用到 show 关键字:


export 'src/xxx.dart' show 需要公开的类名or方法名or变量名

/// 多个公开的 API 用逗号分隔开

还是以 yance_utils.dart 为例子,我们在 yance_utils.dart 再添加一点代码:


String yanceName = "123";

void yanceMain() {
  print('调用了yanceMain方法');
}

class YanceUtils {
  /// Returns [value] plus 1.
  int addOne(int value) => value + 1;
}

class StringUtils {
  String getStr(String value) => value.replaceAll("/""_");
}

此时,我想公开 yanceName 属性yanceMain() 方法YanceUtils 类,可以这样声明:


library yance;

export 'src/yance_utils.dart' show YanceUtils, yanceName, yanceMain;

使用 show 不仅可以避免导出过多的 API,而且可以为开发者提供公开的 API 的概览。


3.发布一个 package


开发完成自己的 package 后,就可以将其发布到 pub.dev 上了。


发布 package 大致需要 5 个步骤:





下面会一一解答每一个步骤。


3.1 关于 pub.dev 的一些政策说明





  • 发布是永久的


只要你在 pub.dev 上发布了你的 package,那么它就是永久存在,不会允许你删除它。这样做的目的是为了保护依赖了你 package 的项目,因为你的删除操作会给他们的项目带来破坏。





  • 可以随时发布 package 的新版本,而旧版本对未升级的用户仍然可用。





  • 对于那些已经发布,但不再维护的 package,你可以把它标记为终止(discontinued)。




进入到 package 页面上的 Admin 标签栏,可以将 package 标记为终止。








标记为终止(discontinued)的 package,以前发布的版本依然留存在 pub.dev 上,并可以被看到,但是它有一个清楚的 终止 徽章,而且不会出现在搜索结果中。


3.2 发布前的准备


3.2.1 首先需要一个 Google 账户


Google 账户申请地址:传送门




如果之前你登录过任何 Google 产品(例如 Gmail、Google 地图或 YouTube),这就意味着你已拥有 Google 帐号。你可以使用自己创建的同一组用户名和密码登录任何其他 Google 产品。



3.2.2 检查 LICENSE 文件


package 必须包含一个 LICENSE 文件。推荐使用 BSD 3-clause 许可证,也就是 Dart 和 Flutter 团队所使用的开源许可证。


参考:


Copyright 2021 com.yance. All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above
      copyright notice, this list of conditions and the following
      disclaimer in the documentation and/or other materials provided
      with the distribution.
    * Neither the name of Google Inc. nor the names of its
      contributors may be used to endorse or promote products derived
      from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

3.2.3 检查包大小


通过 gzip 压缩后,你的 package 必须小于 100 MB


如果它所占空间过大,考虑将它分割为几个小的 package。或者使用 .pubignore 移除不需要的文件,或者减少包含资源或实例的数量。


3.2.4 检查依赖项


package 应该尽量只依赖于托管在 pub.dev 上的库,以避免不必要的风险。


3.3 编写几个重要的文件 🔺


3.3.1 README.md


README.md 的内容在 pub.dev 上会当做一个页面进行展示:





3.3.2 CHANGELOG.md


如果你的 package 中有 CHANGELOG.md 文件,同样会被作为一个页面(Changelog)进行展示:





来看一个例子:


# 1.0.1

Fixed missing exclamation mark in `sayHi()` method.

# 1.0.0

**Breaking change:** Removed deprecated `sayHello()` method.
Initial stable release.

## Upgrading from 0.1.x

Change all calls to `sayHello()` to instead be to `sayHi()`.

# 0.1.1

Deprecated the `sayHello()` method; use `sayHi()` instead.

# 0.1.0

Initial development release.

3.3.3 pubspec.yaml


pubspec.yaml 文件被用于填写关于 package 本身的细节,例如它的描述,主页等等。这些信息将被展现在页面的右侧。





一般来说,需要填写这些信息:





注意:


目前 author 信息已经不需要了,所以大家可以把 author 给删除掉。


3.4 预发布


预发布使用如下命令。执行预发布命令不会真的发布,它可以帮助我们验证填写的发布信息是否符合 pub.dev 的规范,同时展示所有会发布到 pub.dev 的文件。


dart pub publish --dry-run

比如,我运行的结果是:


chenyouyu@chenyouyudeMacBook-Pro-2 yance % dart pub publish --dry-run
Publishing yance 0.0.1 to https://pub.dartlang.org:
|-- CHANGELOG.md
|-- LICENSE
|-- README.md
|-- lib
|   |-- src
|   |   '-- yance_utils.dart
|   '
-- yance.dart
|-- pubspec.yaml
|-- test
|   '-- yance_test.dart
'
-- yance.iml
Package validation found the following potential issue:
* Your pubspec.yaml includes an "author" section which is no longer used and may be removed.

Package has 1 warning.

它提示我们author 信息已经不需要了,可以删除。


删除后,再次运行就没有警告了。





3.5 正式发布


当你已经准备好正式发布你的 package 后,移除 --dry-run 参数:


dart pub publish







点击链接会跳转浏览器验证账户,验证成功后,会有提示:





账户验证通过后,会继续执行上传任务:





此时,去 pub.dev 上就能看到发布成功的 package 了:





pub.dev 会检测 package 支持哪些平台,并呈现到 package 的页面上。


注意:


正式发布可能需要科学上网。


4.参考文章



作者:有余同学
来源:mdnice.com/writing/d5460df39ddd4649be9b102ccb2fb0b2
收起阅读 »

内向的我,到底应该怎么和领导相处啊

务必一而再,再而三,三而不竭; 千次万次,毫不犹豫地救自己于人间水火。 上一篇文章,我们谈到了双减是双减,你是你。 但凡有点野心的,都是既在卷自己,又在鸡孩子。 我们应该考虑的,不是要不要让孩子努力,而是怎样用正确的方法,让孩子快乐地学习。 文...
继续阅读 »

务必一而再,再而三,三而不竭;
千次万次,毫不犹豫地救自己于人间水火。





上一篇文章,我们谈到了双减是双减,你是你。


但凡有点野心的,都是既在卷自己,又在鸡孩子。


我们应该考虑的,不是要不要让孩子努力,而是怎样用正确的方法,让孩子快乐地学习。


文章发出后,有位做金融的读者联系东哥咨询,家庭的问题怎么都好解决,职场上挑战更大,尤其自己性格内向。


金融问,东哥你说,内向的人应该怎么和领导相处?




这个问题,东哥很能感同身受。


我也性格内向,轻度社恐。所以年轻时在职场中,也一样困惑。


每一个问题背后,都是其底层价值观的影响。


你问内向的人如何和领导相处,这是表面问题,而且有技巧能借用。


比如我们之前谈到的要做好向上管理,要相信领导永远是正确的。


要去讲故事、去争资源,去打最硬的仗,去赢得尊重,去成为老板手中最锋利的刀。


这些技巧,并没有触及根本,一点不重要。


什么问题重要?


第一,你的关注点是自己性格内向,也就是说你不满意自己的性格。


这是表面问题。


你真正不满意的,其实是自己的财富状态,职业发展,幸福指数。


如果你已经身价千万,财务自由,还会为自己性格内、不知道应该怎么和领导相处向苦恼么?


所以你的关注点,应该在财富上,而不是性格上。




金融说,财富要,但外向的人也很让人羡慕,我能怎么改善下自己的性格呢?


我说,科学家多年以来最感兴趣的一个问题是,到底人的哪些特征是天生的,哪些特征是受后天教育和环境影响的?


通过对同卵双胞胎几十年的研究,最后的共识是,先天因素远远大于后天因素。


首先,任何一种能够测量的特征,包括智商、兴趣爱好、性格、体育、幽默感,甚至爱不爱打手机,所有这些东西都是天生的。


其次,后天环境对智力和性格的影响非常有限。先天因素是主要的,后天因素是次要的。


家庭环境可以在一定程度上左右一对同卵双胞胎小时候的行为,以至于他们可能会有不同的爱好和个性。


但等他们长大以后,他们的先天特征会越来越突出,他们会越来越像,他们在摆脱家庭对他们“真实的自我”的影响。


这并不是说家教完全没用。


家教可以左右基因表达,可以鼓励孩子发挥他天生的特长,也可以压制他天生的性格缺陷。


只不过这个作用是有限的。


既然后天作用有限,就不应该花太多心力在上面。


雷茵霍尔德·尼布尔在他著名的《宁静祷词》里说的话,在过去的一个多世纪里,曾使无数人动容



愿上帝赐予我从容去接受我不能改变的,
赐予我勇气去改变我可以改变的,
并赐予我智慧去分辨这两者间的区别。



识别出来了性格不可变,这就是大智慧。


世人皆苦,性格外向的人也有他的苦恼,只是你不知道罢了。


比如你,智商在线,年轻时能耐得住寂寞寒窗苦读,现在名校毕业搞金融。


智商和勤奋,也都是天赋,没有这些特质的人,又去哪里说理?


内向而聪慧,外向却愚蠢,你选哪个?


你真正想要的,是在自己已有的好特质上,再加上更有利的特质,比如外向且聪慧。


这是一种不切实际的贪婪。




金融说,做自己是好,但这个内向的性格,会影响赚钱,影响积累财富。


如果我资产千万财务自由,就不用为性格苦恼,问题是我现在资产没有千万。


我说,谁告诉你性格内向影响赚钱?


现代社会需要互相协作,所以性格外向的确会有一些优势。


但同时现代世界的一个好处是,大家可以发挥各自强项分工合作,一起赚钱。


罗永浩在做英语培训的时候,发现自己的脾气不好,性格上的缺点在办公室里暴露得很多,很苦恼,还特意去问了冯唐。冯唐回答说



这个苦恼完全没有必要。
假如你需要做的事情一共有12件,那么你只要做好其中的六七件,就能成就这个企业在商业上的成功。
因此,你只要把自己擅长的那六七件事做好,其他的找人补就行了,千万不要想着把12件事全做好才能成就一个企业。



你去阿里和腾讯看看,那里面有很多人不爱跟人相处,但他们爱跟程序打交道,这就可以了。


像阿里和腾讯这种大企业,有非常多各种各样性格的人,但都没有影响他们取得成功。


所以真正影响赚钱的,是自己的强项。


不断加强自己的强项,做成 1 米宽 10000米深的优势,然后以自己为中心,寻找合作的人。


总想着改变自己,其实是因为自己的强项不够强,就像用提升弱项来弥补。


走偏了。




第二,你的关注点是和领导相处。


往更广了说,你关注的是与高能级者的相处之道。


和领导相处让你有压力,是因为他的能量密度比你高,你在仰视他。


仰视的结果,就是失去了平常心,动作变形。


几千年中央集权的结果,让整个国民对权力,有种天然的崇拜。


辫子不见了,无形的辫子还在,膝盖总是软的,总想找个皇上跪一下。


《遥远的救世主》是本奇书,里面丁元英和韩楚风喝酒的时候提到



中国的传统文化是皇恩浩大的文化,它的实用是以皇天在上为先决条件。
中国为什么穷?穷就穷在幼稚的思维,穷在期望救主、期望救恩的文化上。
这是一个渗透到民族骨子里的价值判断体系。



这种弱势文化,会让人丧失自己,也就无法得到对方平等的尊重。


要学会用平常心对待高能级的人。


平视他,平等的相处,平等的沟通,和他交换价值,互相利用。


金融问,道理是这么个道理,但怎么能达到这个状态?


我说,仰望高能级者的时候,其实是你有所图。


希望能从他那里,得到本不属于你的东西,所以就想着怎么去迎合和讨好。


也就是说



想得到的东西 = 尚有欠缺的能力 + 降低身价的讨好



这是贪婪。


要想得到你要的东西,最好的办法是让你自己配得起那样东西。


强者身边,从来都不缺想讨好他的人。


真正的强者,需要的是能给他创造价值的人,有自己的强项,能弥补他欠缺的东西的人。


所以与其仰视讨好,不如平等合作。


这时候强者是你赚钱的工具,你则是他的合作者,是以谓之双赢。


成年人在一起,总要互相能有所收获,关系才能长久。


只有一方长期的、单方面的输出,终究不免渐行渐远。




所以





  • 当你以为自己在因为性格苦恼时,其实是在为财富苦恼;



  • 当你在为财富苦恼时,其实是在为能力苦恼;



  • 感受到了能力不足,就想通过情绪价值弥补;



  • 当你努力弥补弱项的时候,就走偏了。



  • 没有人会因为你弱项没那么弱与你合作,只会因为你的强项足够强而产生链接。


所以,接受自己的性格,然后强化自己的强项,这才是赚钱的正道。


务必一而再,再而三,三而不竭,千次万次,毫不犹豫地救自己于人间水火。


好好爱自己。



作者:jetorz
来源:mdnice.com/writing/44e869ae99de41a3b393654c15ad3566
收起阅读 »

iOS 16 又又崩了

背景iOS 16 崩了: juejin.cn/post/715360…iOS 16 又崩了:juejin.cn/post/722551…本文分析的崩溃同样只在 iOS16 系统会触发,我们的 APP 每天有 2k+ 崩溃上报。崩溃原因:Cannot ...
继续阅读 »

背景

iOS 16 崩了: juejin.cn/post/715360…
iOS 16 又崩了:juejin.cn/post/722551…
本文分析的崩溃同样只在 iOS16 系统会触发,我们的 APP 每天有 2k+ 崩溃上报。

崩溃原因:

Cannot form weak reference to instance (0x1107c6200) of class _UIRemoteInputViewController. It is possible that this object was over-released, or is in the process of deallocation.
无法 weak 引用类型为 _UIRemoteInputViewController 的对象。可能是因为这个对象被过度释放了,或者正在被释放。weak 引用已经释放或者正在释放的对象会 crash,这种崩溃业务侧经常见于在 dealloc 里面使用 __weak 修饰 self。
_UIRemoteInputViewController 明显和键盘相关,看了下用户的日志也都是在弹出键盘后崩了。

崩溃堆栈:

0	libsystem_kernel.dylib	___abort_with_payload()
1 libsystem_kernel.dylib _abort_with_payload_wrapper_internal()
2 libsystem_kernel.dylib _abort_with_reason()
3 libobjc.A.dylib _objc_fatalv(unsigned long long, unsigned long long, char const*, char*)()
4 libobjc.A.dylib _objc_fatal(char const*, ...)()
5 libobjc.A.dylib _weak_register_no_lock()
6 libobjc.A.dylib _objc_storeWeak()
7 UIKitCore __UIResponderForwarderWantsForwardingFromResponder()
8 UIKitCore ___forwardTouchMethod_block_invoke()
9 CoreFoundation ___NSSET_IS_CALLING_OUT_TO_A_BLOCK__()
10 CoreFoundation -[__NSSetM enumerateObjectsWithOptions:usingBlock:]()
11 UIKitCore _forwardTouchMethod()
12 UIKitCore -[UIWindow _sendTouchesForEvent:]()
13 UIKitCore -[UIWindow sendEvent:]()
14 UIKitCore -[UIApplication sendEvent:]()
15 UIKitCore ___dispatchPreprocessedEventFromEventQueue()
16 UIKitCore ___processEventQueue()
17 UIKitCore ___eventFetcherSourceCallback()
18 CoreFoundation ___CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__()
19 CoreFoundation ___CFRunLoopDoSource0()
20 CoreFoundation ___CFRunLoopDoSources0()
21 CoreFoundation ___CFRunLoopRun()
22 CoreFoundation _CFRunLoopRunSpecific()
23 GraphicsServices _GSEventRunModal()
24 UIKitCore -[UIApplication _run]()
25 UIKitCore _UIApplicationMain()

堆栈分析

崩溃发生在系统函数内部,先分析堆栈理解崩溃的上下文,好在 libobjc 有开源的代码,极大的提高了排查的效率。

_weak_register_no_lock

抛出 fatal errr 最上层的代码,删减部分非关键信息后如下。

id 
weak_register_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id, WeakRegisterDeallocatingOptions deallocatingOptions)
{
objc_object *referent = (objc_object *)referent_id;
if (deallocatingOptions == ReturnNilIfDeallocating ||
deallocatingOptions == CrashIfDeallocating) {
bool deallocating;
if (!referent->ISA()->hasCustomRR()) {
deallocating = referent->rootIsDeallocating();
}
else {
deallocating =
! (*allowsWeakReference)(referent, @selector(allowsWeakReference));
}

if (deallocating) {
if (deallocatingOptions == CrashIfDeallocating) {
_objc_fatal("Cannot form weak reference to instance (%p) of " <=== 崩溃
"class %s. It is possible that this object was "
"over-released, or is in the process of deallocation.",
(void*)referent, object_getClassName((id)referent));
} else {
return nil;
}
}
}
}

直接原因是  _UIRemoteInputViewController 实例的 allowsWeakReference 返回了 false。

options == CrashIfDeallocating 就会 crash。否则的话返回 nil。不过 CrashIfDeallocating 写死在了代码段,没有权限修改。整个 storeWeak 的调用链路上都没有可以 hook 的方法。

__UIResponderForwarderWantsForwardingFromResponder

调用 storeWeak 的地方反汇编

if (r27 != 0x0) {
r0 = [[&var_60 super] init];
r27 = r0;
if (r0 != 0x0) {
objc_storeWeak(r27 + 0x10, r25);
objc_storeWeak(r27 + 0x8, r26);
}
}

xcode debug r27 的值

<_UITouchForwardingRecipient: 0x2825651d0> - recorded phase = began, autocompleted phase = began, to responder: (null), from responder: (null)

otool 查看 _UITouchForwardingRecipient 这个类的成员变量

ivars          0x1cfb460 __OBJC_$_INSTANCE_VARIABLES__UITouchForwardingRecipient
entsize 32
count 4
offset 0x1e445d0 _OBJC_IVAR_$__UITouchForwardingRecipient.fromResponder 8
name 0x19c7af3 fromResponder
type 0x1a621c5 @"UIResponder"
alignment 3
size 8
offset 0x1e445d8 _OBJC_IVAR_$__UITouchForwardingRecipient.responder 16
name 0x181977f responder
type 0x1a621c5 @"UIResponder"

第一个 storeweak  赋值 offset 0x10 responder: UIResponder 取值 r25。

第二个 storeweak 赋值 offset 0x8 fromResponder: UIResponder 取值 r26。

XCode debug 采集 r25 r26 的值

到这里就比较清晰了,_UITouchForwardingRecipient 是在保存响应者链。其中_UITouchForwardingRecipient.responder = _UITouchForwardingRecipient.fromResponder.nextReponder(这里省略了一长串的证明过程,最近卷的厉害,没有时间整理之前的文档了)。崩溃发生在 objc_storeWeak(_UITouchForwardingRecipient.responder), 我们可以从 nextReponder 这个方法入手校验 responder 是否合法。

结论

修复方案

找到 nextresponder_UIRemoteInputViewController 的类,hook 掉它的 nextresponder 方法,在new_nextresponder 方法里面判断,如果 allowsWeakReference == NO 则 return nil
在崩溃的地址断点,可以找到这个类是 _UISizeTrackingView

- (UIResponder *)xxx_new_nextResponder {
    UIResponder *res = [self xxx_new_nextResponder];
    if (res == nil){
        return nil;
    }
    static Class nextResponderClass = nil;
    static bool initialize = false;
    if (initialize == false && nextResponderClass == nil) {
        nextResponderClass = NSClassFromString(@"_UIRemoteInputViewController");
        initialize = true;
    }

if (nextResponderClass != nil && [res isKindOfClass:nextResponderClass]) {
if ([res respondsToSelector:NSSelectorFromString(@"allowsWeakReference")]) {
BOOL (*allowsWeakReference)(id, SEL) =
(__typeof__(allowsWeakReference))class_getMethodImplementation([res class], NSSelectorFromString(@"allowsWeakReference"));
if (allowsWeakReference && (IMP)allowsWeakReference != _objc_msgForward) {
if (!allowsWeakReference(res, @selector(allowsWeakReference))) {
return nil;
}
}
}
}
return res;
}

友情提示

1. 方案里面涉及到了两个私有类,建议都使用开关下发,避免审核的风险。

2. 系统 crash 的修复还是老规矩,一定要加好开关,限制住系统版本,在修复方案触发其它问题的时候可以及时回滚,hook 存在一定的风险,这个方案 hook 的点相对较小了。

3. 我只剪切了核心代码,希望看懂并认可后再采用这个方案。

作者:yuec
链接:https://juejin.cn/post/7240789855138873403
来源:稀土掘金

收起阅读 »

喂!不用这些网站,你哪来的时间摸鱼?

一些我常用且好用的在线工具Postcat - 在线API 开发测试工具postcat.com/ API 开发测试工具Postcat 是一个强大的开源、免费的、跨平台(Windows、Mac、Linux、Browsers...)的 ...
继续阅读 »

一些我常用且好用的在线工具

Postcat - 在线API 开发测试工具
postcat.com/ API 开发测试工具


Postcat 是一个强大的开源、免费的、跨平台(Windows、Mac、Linux、Browsers...)的 API 开发测试工具,支持 REST、Websocket 等协议(即将支持 GraphQL、gRPC、TCP、UDP),帮助你加速完成 API 开发和测试工作。它非常适合中小团队及个人使用。

在保证 Postcat 轻巧灵活的同时,它设计了一个强大的插件系统,让您可以一键使用插件来增强它的功能。


因此 Postcat 理论上是一个拥有无限可能的 API 产品,可以从Logo 中看到,我们也形象地为它加上了一件披风,代表它的无限可能。

Excalidraw - 在线白板画图

ajietextd.github.io/ 一个开源的虚拟手绘风格的白板。创建任何漂亮的手绘图。



EmojiXD - Emoji表情

emojixd.com/ EmojiXD 是一本线上Emoji百科全书📚,收录了所有emoji


Carbon - 在线生成代码图片

carbon.now.sh/Carbon 能够轻松地将你的源码生成漂亮的图片并分享。


Pixso - 产品设计协作一体化工具

pixso.cn/ Pixso,一站式完成原型、设计、交互与交付,为数字化团队协作提效
原来不用注册 现在需要注册了


作者:前端小蜗
链接:https://juejin.cn/post/7243680457815261221
来源:稀土掘金

收起阅读 »

金三银四好像消失了,IT行业何时复苏!

疫情时候不敢离职,以为熬过来疫情了,行情会好一些,可是疫情结束了,反而行情更差了, 这是要哪样 我心中不由一万个 草泥🐴 路过自我10连问我的心情自去年下半年以来,互联网行业一片寒冬传言,众多企业倒闭,裁员。本以为随着疫情、春季和金融楼市的回暖,一切都会变好。...
继续阅读 »

疫情时候不敢离职,以为熬过来疫情了,行情会好一些,可是疫情结束了,反而行情更差了, 这是要哪样 我心中不由一万个 草泥🐴 路过


自我10连问

我的心情

自去年下半年以来,互联网行业一片寒冬传言,众多企业倒闭,裁员。本以为随着疫情、春季和金融楼市的回暖,一切都会变好。然而,站在这个应该是光明的时刻,举世瞩目的景象却显得毫无生气。令人失望的是,我们盼望已久的春天似乎仍未到来。
我的工作生涯
我已经从业近十年,然而最近两年一直在小公司中工作,

我的技术和经历并不出色。随着年龄的增长,是否我的技能也在快速提高呢?我们该如何前进呢 ,转产品,产品到达极限,转管理,可是不会人情事故,

我们该如何继续前进呢?目前还没有人给出答案。

第一家公司

我记得那是很早的时候了,那个时候简历投递出去,就马上会收到很多回复,不像现在 ,
失联招聘, 前程堪忧,boss直坑,
你辛苦的写完简历,满怀期待投递了各大招聘平台,可等来的 却是已读未回,等的心也凉透了。
好怀念之前的高光时刻 神仙打架日子
前面我面试 几乎一周都安排满了,都面试不过来,我记得那会最多时候一天可以跑三家面试哈哈哈,也是很拼命的,有面试机会谁不想多试试呢
我第一家进入的是一个外包公司,叫xxx东软集团, 那个时候也不不懂,什么是外包给公司,只看工资给的所有offer中最高的,然后就去了哈哈哈哈。
入职第一天,我背着我自己的电脑满怀着激动就去了,然后被眼前一幕吸引了,办公的人真多啊,办公室都是拿透明玻璃隔开那种,人挺多,我一想公司还挺大的,
随后我就被带到也是一个玻璃格子办公室,里面就三个人,加我一个4个。
我害怕极了,这个时候一个稍微有一些秃顶的 大叔过来了 哈哈哈(内心台词,早就知道这一行干就了,会秃头这不会就是下一个我把)
他把我安排在了靠近玻璃门的也就是大门位置,这是知道我准备随时跑路节奏吗。然后就去忙他自己的了。整个上午我就像是一个被遗忘在角落里的人一样。根本没人管我,就这样第一天结束了,我尴尬了做了一整天。
这工作和我想象的有点不太一样啊!
后面第三天还是如此,办公室里依旧是那么几个人,直到第四天,大叔来了,问我直到多线程吗,让我用多线程写一个抽奖的活动项目。(内心我想终于有事情干了,可是也高兴不起来谁知道怎么写)
不过好在,他也没有说什么时候交,只是说写完了给他看一下,经过我几天的,复制粘贴工程师 一顿谷歌,百度,终于是勉强写出来了。。。。。
后面,就又陆陆续续来了几个小伙伴,才开始新项目和开会,第一份工作大致就是这样开始了我的职业生涯。怎么说呢和我想象的有所不一样,但又有一些失望。
后面干了1年多,我就离职了原因是太累了没时间休息,一个项目接着一个项目的

第二家公司

在离开第一家公司时候,我休息了好长一段时间,调整了我自己的状态
也了解了什么是外包公司,什么是工作外派,也是我这一次就在投递简历,和面试时候刻意去避免进那种外包,和外派公司。
面试什么也还算顺利,不到半个月就拿到了offer。 但是工资总体来说比上一家是要少一点,但是我也接受了,是一家做本地生鲜电商公司,,本来生活科技有公司, 我觉得公司氛围,和公司都挺不错的,就入职了。
入职了我们那个项目经理还算很热情的,让同事帮我开git账号,开了邮箱,我自己拉取了公司项目,然后同事帮我运行调试环境第一天,项目什么都跑了起来,
你知道的,每次去一家新公司,开始新项目难的是项目复杂配置文件,和各种mave包依赖,接口,环境冲突,所以跑起来项目自己一个人摸索还是需要一些时间的。
在这家公司前期也还不错,公司维护自己项目,工作时间也比较自由和灵活,
大体流程是,每次有新的pm时候 产品经理就会组织各个部门开会
h5端-移动端-接口端开会讨论需求细节和实现,如果有问题头就会pass掉
然后产品经理就会把需求指派到每一个头,头把需求指派给组员,然后组员按照
redmine 上截止时间开发需求,
开发过程中自己去找对应接口负责方,其他业务负责方 去对接数据,没有问题了就可以提交给指定测试组测试了。
然后测试组头会把,测试分配给他们组员,进行测试。
有问题了就会在指派回来给对应负责各个开发同学去解决bug,直到测试完成。测试会让你提交堡垒环境 然后等待通知发布上线,
我们一般是晚上8点时候发布,发布时候,一般开发人员都要留守,直到发布上线没有问题了,才可以回家。如果弄得很晚的话,第二天可以晚点上班。
这一点是我觉得比较好的地方,工作时间弹性比较自由。
记得有一次生产事故。
我影响很深刻,东西上线了,然后产品经理说这个和他设计的预期的不符合要求,需要重新写,那一晚我们整组弄得很晚,就是为了等我弄好去吃饭。
你知道人在心急如焚情况下,是写不好代码的最后还是同事帮我一起完成了产品经理变态需求修改。。。。。。(也就在那时候我才知道产品经理和开发为什么不和了)
因为五行相克
因为经常这样发版,然后一起吃饭公司报销。我们组员和领导关系还算不错氛围还挺好。在这一家公司我待了挺久的。
离职原因
后期因为说公司项目战略升级。空降了一位携程cto,还带来了他的手下人,我们组头,职权被削弱了,我不在由原来头管理了。再加上后面一些其他原因。老同事一个一个走了。
最后这个组只剩下我,和一个进来不久新同事。 不久我也离职了。

第三家公司


这次离职后,我调整休息了差不多有一年,中间离开上海去了江苏,因为家里,女朋友等各种事情。后面我才又从新去了上海,开始找工作。
找工作期间投奔的同事,合同事住一起。
这次面试我明显感觉,有一些慌张了,可能是太久没上班原因,有一些底气不足。好在也是找到了工作虽然不太理想。
这个过程太曲折了,后面公司终究没有扛过疫情,可以说在疫情边缘倒闭了,钱赔偿也没拿到,。。。这里就不赘述了。
IT行业如何破局 大家有什么想法和故事吗。可以关注 程序员三时公众号 进行技术交流讨论
嗯~还想往下面写一点什么,,,下一篇分享一下我现在工作和未来思考


作者:程序员三时
链接:https://juejin.cn/post/7231608749588693048
来源:稀土掘金
收起阅读 »

2023年35大龄程序员最后的挣扎

一、自身情况 我非科班出身,年龄到了35岁。然后剧本都差不多,2022年12月各种裁员,失业像龙卷风一样席卷社会各个角落。 其实30岁的时候已经开始焦虑了,并且努力想找出路。 提升技术,努力争增加自己的能力。 努力争取进入管理层,可是卷无处不在,没有人离开这...
继续阅读 »

一、自身情况


我非科班出身,年龄到了35岁。然后剧本都差不多,2022年12月各种裁员,失业像龙卷风一样席卷社会各个角落。



  1. 其实30岁的时候已经开始焦虑了,并且努力想找出路。

  2. 提升技术,努力争增加自己的能力。

  3. 努力争取进入管理层,可是卷无处不在,没有人离开这个坑位,你努力的成效很低。

  4. 大环境我们普通人根本改变不了。

  5. 自己大龄性价比不高,中年危机就是客观情况。

  6. 无非就是在本赛道继续卷,还是换赛道卷的选择了。


啊Q精神:我还不是最惨的那一批,最惨的是19年借钱买了恒大的烂尾楼,并且在2021年就失业的那拨人。简直不敢想象,那真是绝望啊。心里不够坚强的,想不开轻生的念头都会有。我至少拿了点赔偿,手里还有些余粮,暂时饿不死。


二、大环境情况




  1. 大环境不好已经不是秘密了,整个经济走弱。大家不敢消费,对未来信心不足已经是板上钉钉的事了。




  2. 这剧本就是30年前日本的剧本,不敢说一摸一样。可以说大差不差了,互联网行业的薪资会慢慢的回归平均水平,或者技术要求在提升一个等级。




  3. 大部分普通人,还是做应用层拧螺丝,少部分框架师能造轮子也就是2:8理论。




  4. 能卷进这20%里,就能在上一层楼。也不是说这行就不行了,只不过变成了存量市场,而且坑位变少,人并没有变少还增加了。




  5. 不要怀疑自己的能力,这也不是你的问题了,是外部环境导致的市场萎缩。我们能做的就是,脱下孔乙己的长衫,先保证生活。努力干活,不违法乱纪做什么都是光荣了,不要带有色眼镜看待任何人。




三、未来出路


未来的出路在哪里?


这个我也很迷惑,因为大佬走的路,并不是我们这些普通的不能在普通的人能够走的通的。当然也有例外的情况,这些就是幸存者偏差了。


我先把chartGPT给的答应贴出来:


可以看到chartGPT还是给出,相对可行有效的方案。当然这些并不是每个人都适用。


我提几个普通人能做的建议(普通人还是围绕生存在做决策):



  1. 有存款的,并且了解一些行业的可以开店,比如餐饮店,花店,水果店等。

  2. 摆摊,国家也都改变政策了。

  3. 超市,配送员,外卖员。

  4. 开滴滴网约车。

  5. 有能力的,可以润出G。可以吸一吸GW“free的air”,反正都是要被ZBJ榨取的。


以上都是个人不成熟的观点,jym多多包涵。


每个行业都卷,没有很好的建议都是走一步算一步,保持学习,减少精神内耗


作者:可乐泡枸杞
链接:https://juejin.cn/post/7230656455808335930
来源:稀土掘金
收起阅读 »

一位大厂程序员的随想:6年前刚读硕士,6年后将35岁危机

全文4300字,整体目录如下 作者简介:持续探索副业的大厂奶爸程序员 2020年,华科硕士毕业,非科班转行成程序员 2022年,进入某互联网大厂,打工搬砖 2023年,开始探索副业,目前主攻IP+工具开发 正文开始 一位朋友转载我的故事到公众号后,突然就...
继续阅读 »

全文4300字,整体目录如下




作者简介:持续探索副业的大厂奶爸程序员 2020年,华科硕士毕业,非科班转行成程序员 2022年,进入某互联网大厂,打工搬砖 2023年,开始探索副业,目前主攻IP+工具开发



正文开始


一位朋友转载我的故事到公众号后,突然就让意识到


现在29岁的我


往回看6年,我刚进入华科读硕士


往后看6年,我将面临网上所说的35岁中年危机


因此,借此机会,聊下我对未来的思考和回顾下简单过去的6年



未来6年-战略上乐观,战术上悲观


看待未来,我需要保持乐观,只有这样,才能不为未来的不确定而过分焦虑


还是学生时代的时候,因为对这程序员这行业不清楚,当时就很害怕网上常说的35岁的失业危机,为此还在网上查了各种各样的信息,整天忐忑不已。


可真正进入了这个行业以后,才发现危机远没有想象中的恐怖,原来,恐惧真的源于对未知的不确定。


身边也有好些35以上的朋友,他们有的还在程序员这行,有的已经转行了。虽然整体来看,薪酬水平或者薪酬增长速度不如之前,但远没有到达山穷水尽的地步。


即使是现在ai时代的到来,我依然相信,只要程序员去积极的拥抱ai,使用ai去做更多创造性的工作,也不会突然就失业。


但同时,如果35岁的我,还是会被失业危机所困的话,那么一定就是平常的日子太过懈怠,处于温水煮青蛙的状态。


22年刚入大厂的半年里,基本就处于这个状态,除了工作外,剩下的时间基本都用来娱乐了,成长很是有限。


因此,我需要在战术上保持悲观,要不断成长,要确保自己将主要精力放下以下三方面的事情


1、做好主业,保持市场竞争力,被裁/失业时,能快速找到工作


2、开展第二曲线,降低未来失业可能带来的现金流锻炼的风险


3、爱护好自己的身体,照顾好家人,帮助朋友。


先来聊下第二点和第三点吧,第一点在文末聊。


未来6年-做好第二曲线


为什么开展


2022年过年期间,开始意识到现在的看似高薪工作并不稳定,需要在工作外,建立第二曲线(也就是副业),降低未来的风险。


原因有二,内心的渴望+外在的环境


内在的渴望就是,其实自己一直是一个很爱好学习的人,也希望做出点成绩获得外界认可的人。


在3月之前,也一直在保持学习,科学习的那点热情基本全用在了阅读各种书籍以及得到上,看了几十本书,学了好几本课程,可是成长却极为有限。


幸而在3月的时候遇见了生财有术,看见了更多的可能性,也提升了很多认知,因而,内在的渴望进一步扩大。


外在的环境,一方面是工作的不确定性,另一方面,是身上责任的加重。


自动20年当程序员以来,身边的朋友一茬接一茬的换,有的突然就被迫失业了,有的就跳槽了,有些朋友,甚至都没来得及告别,就已经后会无期了。


再加上网上的铺天盖地的悲观主义和程序员危机。想开展副业,抵抗未来的不确定的决心越来越强。 目前还没房贷车贷,这里的加重倒不是说现金流上的压力加重


只是觉得,作为一个父亲,应该为孩子去铺一条更好的道路,不希望等到我孩子需要我支持帮助的时候,我却面临中年危机。


同时,我也希望孩子从我这里获得更多的认知和经验,而仅仅只继续专注于程序员的话,这个希望是有点难以实现的。(因为我个人觉得,程序员这行,距离真实的商业事件挺远的)


这几个月的效果


到目前为止,从2023年3月算起,差不多开展5个月了,在金钱上的收获很少,累计也没超过500吧。


先后做过


1、小程序(做了2款小程序,但都是学习阶段的程序,未盈利)


2、小红书无货源店铺(赚了200多吧,其实还是朋友的支持)


3、公众号流量主(赚了没超过50吧)


说下后2个没赚大钱的最大原因吧:我有个很大的毛病,就是爱学习,但不注重学习的结果,在实际执行过程中,碰到点问题就会泄气。


同时,过分在意做事的时间成本,导致执行力不够。(后2个项目,其实只要投入时间认真去做,都不只赚我这点钱。)


不过虽然金钱上的收获不多,在技能、认知和人脉上还是提升了很多


人脉上,认识了好些其他行业的朋友,各行各业的都有。 认知上,知道了要多输出表达、要有流量意识、要懂得链接他人 技能上,也是突破了后端能力,会了一点vue app,小程序搭建能力。


当然,最重要的是,这个过程极大的提高了我对未来的信心


因为我知道,只要认真专注的执行某一个赚钱的领域,我就能一定能赚到一点钱。


不再是之前那种担心如果失业了,就前途一片阴暗的感觉了。


对接下来的思考


接下来的6年,为了发展好第二曲线。我需要做以下的事情:


1、需要克服执行力差、技术傲慢、纸上谈兵等一系列的问题,去扎实的投入实战中。


2、在过程中,尽早找到适合自己的长期事业,并专注的投入(我希望在30岁以前能够找到。)


3、相信积累的力量,不断坚持。


6年以后的我,一定能够发展好自己的第二曲线。


未来6年-爱护自己,照顾家人,帮助朋友


从6年后的视角看,其实最重要的是这三件事,爱护好自己,照顾好家人,帮助好朋友


爱护自己


健康是一切的起点,没有健康的话,其他所有的都是白搭。


现在的身体状况应该是挺糟糕的,肥胖而且不运动,6年后最容易出现的问题,应该就是肥胖带来的问题了。


也因此


1、需要有意识的去控制自己的体重,定期体检,适当运动。


2、平常养好身体,工作上不要太用力,压力不要太大。


照顾家人


6年后,孩子就到了上小学的年纪了。父母也都65左右了,这么看的话,主要是父母的健康问题需要考虑。


也因此


1、已经给父母买了医疗险,但还没给岳父母买,需要2023年落实


2、每年带父母/岳父母 体检。


帮助朋友


志同道合的朋友,于我来说,是不可或缺的,也是能极大的提升幸福感的。


也因此


1、积极拓展志同道合的朋友


2、维护好现有的朋友,真诚利他。


(最近建了个程序员副业群,欢迎私聊加入)


好,接下里回顾下过去的6年


过去6年-转行当程序员


为什么转行


我来自湖南农村,家里挺穷,是那种穷到连上大学学费都要借的那种。


2012-2016年在华科读本科,在校就是天天混日子,大四想考华科电气没考上,毕业时连份工作都没有,于是决定二战考研。考完研后,在湖南省长沙市新东方做了八年的小学奥数老师,保底薪资5k,钱少事多的一份工作。


2017年秋,以笔试和面试都是专业第一的成绩,顺利成为一位硕士。


在2017年开始读硕士时,实验室的师兄就丢给我一本《21天精通Java》,说:“你先学习这个哈,后面做实验会用到”。也因此,开始接触Java。(事实,我到现在都没有精通Java )


2018年,实验室接了头部水电企业的一个项目,需要给他们做一个系统,我就参与进来了,然后,还去这个头部企业公司内部实习了半年。


在那里工作,我看到那些公司的员工有的40 50岁了,每天都是在办公室上来了又走,每天的工作都规律的不行,中午午休2个半小时,下午5点半准时下班。有事没事去打个乒乓球,跑个步什么的。


那时候还年轻啊,也没有足够的经验认知,就觉得,这样安逸的生活,一眼看到头的生活,完全不是我想要的。我还年轻,还有大好年华,我要去闯荡,去见识更多的可能性,去看更多的世界。(事实证明,随便在哪工作,你都可以去看大千事件)


于是,从2018年开始就开始坚定的要转行。


转行成功的因素


现在看,非科班转行成功主要有3个因素:


一是学历给了我很大的加成。我是985本硕,在2020年的就业市场上,还是有很大竞争优势的。


二是实验室恰好有一两个项目和IT搭边。现在好多转行的人,做的项目基本都是往上那种通用的项目,这种项目,要是深耕下去的话,确实也能收获很多。但一般转行的人,但研究的比较浅,也因此,在项目上没有多少竞争优势。


三是我自己也还算刻苦。记得当时,经常一两点在那看《深入理解Java虚拟机》、《Java并发编程》等。花了3个月一页页的看完了《算法.第4版》。甚至还花了2个月恶补了计算机基础。同时,也在CSDN上输出自己的学习记录


最后,也是2020年的顺利的校招毕业,拿到当时挺高年薪的offer,进入了北京某头部地产当Java工程师


这是我当时的面试经历 app.yinxiang.com/fx/fc7e01fa…


过去6年- 跳槽到大厂的经历


想跳槽的原因


2020年7月进入公司,从2021年下半年开始,很明显的感觉整个部门的业务动荡。


再加上身边的人一个个的被裁了,虽然说我是校招+管培生,裁员短期内不会落到我头上,但我知道,这一天迟早会到来。


(后来也表明,22年开始,公司开始裁我们这些校招生了。)


当然,还有另外一个很重要的因素,当初和夫人异地恋,我们相约在深圳见面。


关于我在这家公司的情况,请见这个链接:北京,再见。下一站,深圳


跳槽的过程


我这个人脑子比较笨,技术底子也差。但肯下苦功夫 。


从2022年9月开始,以极客时间为主要学习渠道,开始疯狂的学习。主要学习的就是和八股文相关的课程。(记得那时候,身边的朋友都说,你是真的能学的进去阿,也有好几个朋友,被我卷的也开始看书学习了)。


从2021年12月开始,知道要为2022年的3月的黄金跳槽期做准备了。于是给自己列了个学习计划,并差不多严格执行了。


从21年12月开始,知道要为22年的3月的黄金跳槽期做准备了。于是给自己列了个学习计划,并差不多严格执行了。


与此同时,我发现思维导图很适合做这种八股文的笔记和辅助记忆,于是就在ProcessOn上持续记录学习笔记。(后来还将笔记分享给你100+朋友)


刘卡卡 | ProcessOn


一个人学习的道路总是艰辛的,经常感觉坚持不下去,感觉很孤独,没人交流。幸好在1月进入了知识星球代码随想录,里面都是为了找到好工作而奋斗的人,大家一起交流探讨,互相打卡监督,整个人的学习劲头也开始上来了。


也是在2022年3月底,面了差不多10家公司后,如愿以偿的拿到了现在的深圳大厂的工作。


过去6年- 大厂一年多以来的感想


2022年4月,成功进入大厂 。


前面3-4个月的时候,真的很累,一来是不并不适应大厂的自己干自己活的氛围,二来也是技术上也还待欠缺,三是业务复杂度很高,四是每天要应对Oncall处理。


但干了半年左右后,也就开始适应了。(人果然是一种适应性的动物。)


现在的我,在大厂内,就是当一名勤勤恳恳的螺丝钉,


同时在心态上,也有了很大的转变。


1、接受自己不爱竞争的性格,只要自己心里不卷的话,其他人也就卷不到我。


2、将工作看的很清晰,工作就是为了挣钱,因此,如果工作上有什么不如意的地方,切莫影响到自己的生活,不值当。


当然,工作中也不能躺平,要在日常的工作中去多做积累经验,沉淀知识,保持市场竞争力。


好了,洋洋洒洒写了4000多字了,就先到这吧,希望6年后的我,看到这篇文章的时候,能说一句:


你真的做到了,谢谢你这6年的努力


作者:刘卡卡
来源:juejin.cn/post/7263669060473520186
收起阅读 »

Android协程带你飞越传统异步枷锁

引言 在Android开发中,处理异步任务一直是一项挑战。以往的回调和线程管理方式复杂繁琐,使得代码难以维护和阅读。Jetpack引入的Coroutine(协程)成为了异步编程的新标杆。本文将深入探讨Android Jetpack Coroutine的使用、原...
继续阅读 »

引言


在Android开发中,处理异步任务一直是一项挑战。以往的回调和线程管理方式复杂繁琐,使得代码难以维护和阅读。Jetpack引入的Coroutine(协程)成为了异步编程的新标杆。本文将深入探讨Android Jetpack Coroutine的使用、原理以及高级用法,助您在异步编程的路上游刃有余。


什么是Coroutine?


Coroutine是一种轻量级的并发设计模式,它允许开发者以顺序代码的方式处理异步任务,避免了传统回调和线程管理带来的复杂性。它建立在Kotlin语言的suspend函数上,suspend函数标记的方法能够挂起当前协程的执行,并在异步任务完成后恢复执行。


Coroutine的优势



  • 简洁:通过简洁的代码表达异步逻辑,避免回调地狱。

  • 可读性:顺序的代码结构使得逻辑更加清晰易懂。

  • 卓越的性能:Coroutine能够有效地利用线程,避免过度的线程切换。

  • 取消支持:通过Coroutine的结构,方便地支持任务取消和资源回收。

  • 适用范围广:从简单的后台任务到复杂的并发操作,Coroutine都能应对自如。


Coroutine的原理


挂起与恢复


当遇到挂起函数时,例如delay()或者进行网络请求的suspend函数,协程会将当前状态保存下来,包括局部变量、指令指针等信息,并暂停协程的执行。然后,协程会立即返回给调用者,释放所占用的线程资源。一旦挂起函数的异步操作完成,协程会根据之前保存的状态恢复执行,就好像从挂起的地方继续运行一样,这使得异步编程变得自然、优雅。


线程调度与切换


Coroutine使用调度器(Dispatcher)来管理协程的执行线程。主要的调度器有:



  • Dispatchers.Main:在Android中主线程上执行,用于UI操作。

  • Dispatchers.IO:在IO密集型任务中使用,比如网络请求、文件读写。

  • Dispatchers.Default:在CPU密集型任务中使用,比如复杂的计算。


线程切换通过withContext()函数实现,它智能地在不同的调度器之间切换,避免不必要的线程切换开销,提高性能。


异常处理与取消支持


Coroutine支持异常处理,我们可以在协程内部使用try-catch块来捕获异常,并将异常传播到协程的外部作用域进行处理,这使得我们能够更好地管理和处理异步操作中出现的异常情况。


同时,Coroutine支持任务的取消。当我们不再需要某个协程执行时,可以使用coroutineContext.cancel()或者coroutinecope.cancel()来取消该协程。这样,协程会自动释放资源,避免造成内存泄漏。


基本用法


并发与并行


使用async函数,我们可以实现并发操作,同时执行多个异步任务,并等待它们的结果。而使用launch函数,则可以实现并行操作,多个协程在不同线程上同时执行。


val deferredResult1 = async { performTask1() }
val deferredResult2 = async { performTask2() }

val result1 = deferredResult1.await()
val result2 = deferredResult2.await()

超时与异常处理


通过withTimeout()函数,我们可以设置一个任务的超时时间,当任务执行时间超过指定时间时,会抛出TimeoutCancellationException异常。这使得我们能够灵活地处理超时情况。


try {
withTimeout(5000) {
performLongRunningTask()
}
} catch (e: TimeoutCancellationException) {
// 处理超时情况
}

组合挂起函数


Coroutine提供了一系列的挂起函数,例如delay()withContext()等。我们可以通过asyncawait()函数将这些挂起函数组合在一起,实现复杂的异步操作。


val result1 = async { performTask1() }.await()
val result2 = async { performTask2() }.await()

与jetpack联动


当使用Jetpack组件和Coroutine结合起来时,我们可以在Android应用中更加优雅地处理异步任务。下面通过一个示例演示如何在ViewModel中使用Jetpack组件和Coroutine来处理异步数据加载:


创建一个ViewModel类,例如MyViewModel.kt,并在其中使用Coroutine来加载数据:


import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import kotlinx.coroutine.Dispatchers

class MyViewModel : ViewModel() {

fun loadData() = liveData(Dispatchers.IO) {
emit(Resource.Loading) // 发送加载中状态

try {
// 模拟耗时操作
val data = fetchDataFromRemote()
emit(Resource.Success(data)) // 发送加载成功状态
} catch (e: Exception) {
emit(Resource.Error(e.message)) // 发送加载失败状态
}
}

// 假设这是一个网络请求的方法
private suspend fun fetchDataFromRemote(): String {
// 模拟耗时操作
delay(2000)
return "Data from remote"
}
}

创建一个Resource类用于封装数据状态:


sealed class Resource<out T> {
object Loading : Resource<Nothing>()
data class Success<T>(val data: T) : Resource()
data class Error(val message: String?) : Resource<Nothing>()
}

在Activity或Fragment中使用ViewModel,并观察数据变化:


class MyActivity : AppCompatActivity() {

private val viewModel: MyViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my)

viewModel.loadData().observe(this) { resource ->
when (resource) {
is Resource.Loading -> {
// 显示加载中UI
}
is Resource.Success -> {
// 显示加载成功UI,并使用resource.data来更新UI
val data = resource.data
}
is Resource.Error -> {
// 显示加载失败UI,并使用resource.message显示错误信息
val errorMessage = resource.message
}
}
}
}
}

在以上示例中,ViewModel中的loadData()方法使用Coroutine的liveData构建器来执行异步任务。我们通过emit()函数发送不同的数据状态,Activity(或Fragment)通过观察LiveData来处理不同的状态,并相应地更新UI。


结论


Android Jetpack Coroutine是异步编程的高级艺术。通过深入理解Coroutine的原理和高级用法,我们可以写出更加优雅、高效的异步代码。掌握Coroutine的挂起与恢复、线程切换、异常处理和取消支持,使得我们能够更好地处理异步操作,为用户带来更出色的应用体验。

作者:午后一小憩
来源:juejin.cn/post/7264399534474297403

收起阅读 »

项目使用redis做缓存,除了击穿,穿透,雪崩,我们还要考虑哪些!!!

大家好,我是小趴菜,相信大家在项目中都是用过redis,比如用来做一个分布式缓存来提高程序的性能 当使用到了redis来做缓存,那么我们就必须要考虑几个问题,除了缓存击穿,缓存穿透,缓存雪崩,那么我们还需要考虑哪些问题呢? 高并发写 对于高并发情况下,比如直播...
继续阅读 »

大家好,我是小趴菜,相信大家在项目中都是用过redis,比如用来做一个分布式缓存来提高程序的性能


当使用到了redis来做缓存,那么我们就必须要考虑几个问题,除了缓存击穿,缓存穿透,缓存雪崩,那么我们还需要考虑哪些问题呢?


高并发写


对于高并发情况下,比如直播下单,直播下单跟秒杀不一样,秒杀是有限定的库存,但是直播下单是可以一直下的,而且是下单越多越好的。比如说我们的库存有10万个,如果这个商品特别火,那么可能一瞬间流量就全都打过来了。虽然我们的库存是提前放到redis中,并不会去访问MySql,那么这时候所有的请求都会打到redis中。


image.png


表面看起来确实没问题,但是你有没有想过,即使你做了集群,但是访问的还是只有一个key,那么最终还是会落到同一台redis服务器上。这时候key所在的那台redid就会承载所有的请求,而集群其它机器根本就不会访问到,这时候你确定你的redis能扛住吗???如果这时候读的请求很多,你觉得你的redis能扛住吗?


所以对于这种情况我们可以采用数据分片的解决方案,比如你有10万个库存,那么这时候可以搞10台redis服务器,每台redis服务器上放1万个库存,这时候我们可以通过用户的ID进行取模,然后将用户流量分摊到10台redis服务器上


image.png


所以对于热点数据来说,我们要做的就是将流量进行分摊,让多台redis分摊承载一部分流量,尤其是对于这种高并发写来讲


高并发读


使用redis做缓存可以说是我们项目中使用到的最多的了,可能由于平时访问量不高,所以我们的redis服务完全可以承载这么多用户的请求


但是我们可以想一下,一次reids的读请求就是一次的网络IO,如果是1万次,10万次呢?那就是10万次的网络IO,这个问题我们在工作中是不得不考虑的。因为这个开销其实是很大的,如果访问量太大,redis很有可能就会出现一些问题


image.png


我们可以使用本地缓存+redis分布式缓存来解决这个问题,对于一些热点读数据,更新不大的数据,我们可以将数据保存在本地缓存中,比如Guava等工具类,当然本地缓存的过期时间要设置的短一点,比如5秒左右,这时候可以让大部分的请求都落在本地缓存,不用去访问redis


如果这时候本地缓存没有,那么再去访问redis,然后将redis中的数据再放入本地缓存中即可


加入了多级缓存,那么就会有相应的问题,比如多级缓存如何保证数据一致性


总结


没有完美的方案,只有最适合自己的方案,当你决定采用了某种技术方案的时候,那么势必会带来一些其它你需要考虑的问题,redis也一样,虽然我们使用它来做缓存可以提高我们程序的性能,但是在使用redis做缓存的时候,有些情况我们也是需要考虑到的,对于用户访问量不高来说,我们直接使用redis完全是够用的,但是我们可以假设一下,如果在高并发场景下,我们的方案是否能够支持我们的业务


作者:我是小趴菜
来源:juejin.cn/post/7264475859659079736
收起阅读 »

基于css3写出的流水加载效果

web
准备部分 这里写入基本的html样式,这里还设置了水球的css样式,用于css样式中的计算--i:1是一种自定义的CSS变量,可能用于控制样式中的计数 <body> <div class="box"> <d...
继续阅读 »

准备部分



这里写入基本的html样式,这里还设置了水球的css样式,用于css样式中的计算--i:1是一种自定义的CSS变量,可能用于控制样式中的计数



<body>
<div class="box">
<div class="loader">
<!-- 这里设置了水球的css样式变量,用于css样式中的计算 -->
<span style="--i:1"></span>
<span style="--i:2"></span>
<span style="--i:3"></span>
<span style="--i:4"></span>
<span style="--i:5"></span>
<span style="--i:6"></span>
<span style="--i:7"></span>
<span style="--i:8"></span>
</div>
</div>
</body>

设置基本的css背景及其样式


image.png


* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

.box {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #0c1b21;
}
/* 设置流水区域 */
.loader {
position: relative;
width: 250px;
height: 250px;
background: #666;
animation: animate 12s linear infinite;
}

下面进行详细的css设计



这里通过伪元素设计了第一个小球的效果,通过定位定位到左上角,,设置了大小为40,并且设置了颜色和圆角色设置,同时添加了阴影效果,形成了如下的圆球水滴效果,通过渐变函数linear-gradient是一个渐变函数,用于创建线性渐变背景,设置了颜色和倾斜的角度



image.png


    .loader span {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
/* 定位 伪元素,设置流水的珠子 */
.loader span::before {
content: '';
position: absolute;
top: 0;
width: 40px;
height: 40px;
/* 珠子颜色设置 */
background: linear-gradient(45deg, #c7eeff, #03a9f4);
border-radius: 50%;
/* 设置阴影 */
box-shadow: 0 0 30px #00bcd4;
}

接下来根据html中定义的css变量,来设置不同方向的数据


image.png


.loader span{
/* 设置不同方向的小球 */
transform: rotate(calc(45deg*var(--i)));
}

将小球向内收缩一部分


image.png


.loader span::before {
left: calc(50% - 20px);
}

动画设置


在这里设置了旋转动画,并且在整个区域添加入了动画


image.png


/* 这里设置了旋转动画 */
@keyframes animate {
0% {
transform: rotate(0deg);
}

100% {
transform: rotate(360deg);
}
}


接下来补一下html


这里设置了4个span元素,用来形成四个小球,在容器内沿着小球的方向进行转动,形成一个如下的的效果,并且通过在html中给span元素加入css变量来控制每个小球的延迟效果


ezgif.com-video-to-gif.gif


去掉背景颜色后


ezgif.com-video-to-gif.gif


<body>
<div class="box">
<div class="loader">
<!-- 这里设置了水球的css样式变量,用于css样式中的计算 --i:1是一种自定义的CSS变量,可能用于控制样式中的计数 -->
<span style="--i:1"></span>
<span style="--i:2"></span>
<span style="--i:3"></span>
<span style="--i:4"></span>
<span style="--i:5"></span>
<span style="--i:6"></span>
<span style="--i:7"></span>
<span style="--i:8"></span>
<!-- 这里设置了光晕效果,使得出现5个转动的小球 -->
<span class="rotate" style="--j:0"></span>
<span class="rotate" style="--j:1"></span>
<span class="rotate" style="--j:2"></span>
<span class="rotate" style="--j:3"></span>
<span class="rotate" style="--j:4"></span>
</div>
<div class="word">加载中</div>
</div>

</body>


.rotate {
animation: animate 4s ease-in-out infinite;
/* 设置延迟 */
animation-delay: calc(-0.2s*var(--j));
}
.loader {
filter: url(#fluid);
/* 去掉临时背景颜色为透明 */
background: transparent;
}

接下来补全所需要的html



这里不全了所需要的html,加入了svg部分,使用过svg,对图形进行高斯模糊处理,然后对图形的颜色进行变化,通过颜色矩阵来实现颜色变化,这里使用的颜色矩阵将每个像素的红、绿、蓝三个通道的颜色值分别乘以1,不变化;将透明度乘以20,增加透明度;最后将透明度减去10,进一步增加透明度。这段代码可能被使用在创建视觉效果中,比如给图像添加模糊效果并调整其透明度,从而实现一种"流体"或"柔和"的视觉效果
并且定义了一个filter的滤镜效果
“in"属性的值为"SourceGraphic”,表示将滤镜应用在源图形上,“stdDeviation"属性的值为"10”,表示高斯模糊的参数,即模糊程度



<body>
<div class="box">
<!-- 这里使用了svg,svg是可缩放矢量图形的标签,通过创建和操作svg,使得图形通过缩放而不失去真的在各种尺寸和分辨率下呈现 -->
<!-- 这段代码的作用是先对图形进行高斯模糊处理,然后对图形的颜色进行变换。具体的颜色变换可以通过颜色矩阵来实现,
这里使用的颜色矩阵将每个像素的红、绿、蓝三个通道的颜色值分别乘以1,不变化;将透明度乘以20,增加透明度;
最后将透明度减去10,进一步增加透明度。这段代码可能被使用在创建视觉效果中,比如给图像添加模糊效果并调整其透明度,
从而实现一种"流体"或"柔和"的视觉效果 -->

<svg>
<!-- 这是定义一个滤镜效果的元素,其中"id"属性的值为"fluid",用于给滤镜效果命名。 -->
<filter id="fluid">
<!-- 这是一个高斯模糊滤镜效果,用于对图形进行模糊处理。“in"属性的值为"SourceGraphic”,表示将滤镜应用在源图形上,
“stdDeviation"属性的值为"10”,表示高斯模糊的参数,即模糊程度。 -->

<feGaussianBlur in="SourceGraphic" stdDeviation="10"></feGaussianBlur>
<!-- 这是一个颜色矩阵滤镜效果,用于对图形的颜色进行变换。
"values"属性的值为一个包含20个数字的字符串,表示颜色矩阵的变换矩阵。 -->

<feColorMatrix values="
1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 20 -10 "
>

</feColorMatrix>
</filter>
</svg>
<div class="loader">
<!-- 这里设置了水球的css样式变量,用于css样式中的计算 --i:1是一种自定义的CSS变量,可能用于控制样式中的计数 -->
<span style="--i:1"></span>
<span style="--i:2"></span>
<span style="--i:3"></span>
<span style="--i:4"></span>
<span style="--i:5"></span>
<span style="--i:6"></span>
<span style="--i:7"></span>
<span style="--i:8"></span>
<!-- 这里设置了光晕效果,使得出现5个转动的小球 -->
<span class="rotate" style="--j:0"></span>
<span class="rotate" style="--j:1"></span>
<span class="rotate" style="--j:2"></span>
<span class="rotate" style="--j:3"></span>
<span class="rotate" style="--j:4"></span>
</div>
<div class="word">加载中</div>
</div>
</body>

再继续设置svg的样式,这里将被svg偏移的容器位置归于中心位置,并且设置了文字效果及其设置了文字的缩放动画


    svg {
width: 0;
height: 0;
}

.word {
position: absolute;
color: #fff;
font-size: 1.2em;
animation: words 3s linear infinite;
}
/* 这里设置了文字的缩放动画 */
@keyframes words {
0% {
transform: scale(1.2);
}

25% {
transform: scale(1);
}

50% {
transform: scale(0.8);
}

75% {
transform: scale(1);
}

100% {
transform: scale(1.2);
}
}

效果展示


ezgif.com-video-to-gif (1).gif


总结



实现这个效果关键在最后的那个svg的使用,通过添加svg给了一个高斯模糊的效果,从而才会使得明显的小球变成了这种连在一起的流动液体样式的小球,要学习一下svg效果,在他也是属于html5里面的一部分内容,仅供自己学习记录和新的展示



作者:如意呀
来源:juejin.cn/post/7263064267560976442
收起阅读 »

如何自动打开你的 App?

相信大家在刷 某博 / 某书 / 某音 的时候,最能体会什么叫做 条条大路通 tao bao。经常是你打开一个 App,不小心点了下屏幕,就又打开了另一个 App 了。 那么这种自动打开一个 App 到底是怎么实现的呢? URL Scheme 首先是最原始的方...
继续阅读 »

相信大家在刷 某博 / 某书 / 某音 的时候,最能体会什么叫做 条条大路通 tao bao。经常是你打开一个 App,不小心点了下屏幕,就又打开了另一个 App 了。


那么这种自动打开一个 App 到底是怎么实现的呢?


URL Scheme


首先是最原始的方式 URL Scheme。



URL Scheme 是一种特殊的 URL,用于定位到某个应用以及应用的某个功能。



它的格式一般是: [scheme:][//authority][path][?query]


scheme 代表要打开的应用,每个上架应用商店的 App 所注册的 scheme 都是唯一的;后面的参数代表应用下的某个功能及其参数。


在 IOS 上配置 URL Scheme


在 XCode 里可以轻松配置


image.png


在 Android 上配置 URL Scheme


Android 的配置也很简单,在 AndroidManifest.xml 文件下添加以下配置即可


image.png


通过访问链接自动打开 App


配置完成后,只要访问 URL Scheme 链接,系统便会自动打开对应 scheme 的 App。


因此,我们可以实现一个简单的 H5 页面来承载这个跳转逻辑,然后在页面中通过调用 location.href=schemeUrl 或者 <a href='schemeUrl' /> 等方式来触发访问链接,从而自动打开 App


优缺点分析


优点: 这个是最原始的方案,因此最大的优点就是兼容性好


缺点:



  1. 通过 scheme url 这种方式唤起 App,对于 H5 中间页面是无法感知的,并不知道是否已经成功打开 App

  2. 部分浏览器有安全限制,自动跳转会被拦截,必须用户手动触发跳转(即 location.href 行不通,必须 a 标签)

  3. 一些 App 会限制可访问的 scheme,你必须要在白名单内,否则也会被拦截跳转

  4. 通过 scheme url 唤起 App 时,浏览器会提示你是否确定要打开该 App,会影响用户体验


DeepLink


通过上述缺点我们可以看出,传统的 URL Scheme 在用户体验上是存在一定缺陷的。


因此,DeepLink 诞生了。


DeepLink 的宗旨就是通过传统的 HTT P链接就可以唤醒app,而如果用户没有安装APP,则会跳转到该链接对应的页面。


IOS Universal Link


在 IOS 上一般称之为 Universal Link。


【配置你的 Universal Link 域名】


首先要去 Apple 的开发者平台上配置你的 domains,假设是: mysite.com


image.png


【配置 apple-app-site-association 文件】


在该域名根目录下创建一个 .well-known 路径,并在该路径下放置 apple-app-site-association 文件。


文件内容包含 appID 以及 path,path如果配置 /app 则表示访问该域名下的 /app 路径均能唤起App


该文件内容大致如下:


{
"applinks": {
"apps": [],
"details": [
{
"appID": "xxx", // 你的应用的 appID
"paths": [ "/app/*"]
}
]
}
}

【系统获取配置文件】


上面两步配置成功后,当用户 首次安装App 或者后续每次 覆盖安装App 时,系统都会主动去拉取域名下的配置文件。



即系统会主动去拉取 https://mysite.com/.well-known/apple-app-site-association 这个文件



然后根据返回的 appID 以及 path 判断访问哪些路径是需要唤起哪个App


【自动唤起 App】


当系统成功获取配置文件后,只要用户访问 mysite.com/app/xxx 链接,系统便会自动唤起你的 App。


同时,客户端还可以进行一些自定义逻辑处理:


客户端会接收到 NSUserActivity 对象,其 actionType 为 NSUserActivityTypeBrowsingWeb,因此客户端可以在接收到该对象后做一些跳转逻辑处理。


image.png


Android DeepLink


与 IOS Universal Link 原理相似,Android系统也能够直接通过网站地址打开应用程序对应的内容页面,而不需要用户选择使用哪个应用来处理网站地址


【配置 AndroidManifest.xml】
在 AndroidManifest 配置文件中添加对应域名的 intent-filter:


scheme 为 https / http;


host 则是你的域名,假设是: mysite.com


image.png


【生成 assetlinks.json 文件】


首先要去 Google developers.google.com/digital-ass… 生成你的 assetlinks json 文件。


image.png


【配置 assetlinks.json 文件】


生成文件后,同样的需要在该域名根目录下创建一个 .well-known 路径,并在该路径下放置 assetlinks.json 配置文件,文件内容包含应用的package name 和对应签名的sha哈希


【系统获取配置文件】


配置成功后,当用户 首次安装App 或者后续每次 覆盖安装App 时,系统会进行以下校验:



  1. 如果 intent-filter 的 autoVerify 设置为 true,那么系统会验证其



  • Action 是否为 android.intent.action.VIEW

  • Category 是否为android.intent.category.BROWSABLE 和 android.intent.category.DEFAULT

  • Data scheme 是否为 http 或 https



  1. 如果上述条件都满足,那么系统将会拉取该域名下的 json 配置文件,同时将 App 设置为该域名链接的默认处理App


【自动唤起 App】


当系统成功获取配置文件后,只要用户访问 mysite.com/app/xxx 链接,系统便会自动唤起你的 App。


优缺点分析


【优点】



  1. 用户体验好:可以直接打开 App,没有弹窗提示

  2. 唤起App失败则会跳转链接对应的页面


【缺点】



  1. iOS 9 以后才支持 Universal Link,

  2. Android 6.0 以后才支持 DeepLink

  3. DeepLink 需要依赖远程配置文件,无法保证每次都能成功拉取到配置文件


推荐方案: DeepLink + H5 兜底


基于前面两种方案的优缺点,我推荐的解决方案是配置 DeepLink,同时再加上一个 H5 页面作为兜底。


首先按照前面 DeepLink 的教程先配置好 DeepLink,其中访问路径配置为 https://mysite.com/app


接着,我们就可以在 https://mysite.com/app 路径下做文章了。在该路径下放置一个 H5 页面,内容可以是引导用户打开你的 App。


当用户访问 DeepLink 没有自动打开你的 App 时,此时用户会进入浏览器,并访问 https://mysite.com/app 这个 H5 页面。


在 H5 页面中,你可以通过浏览器 ua 获取当前的系统以及版本:



  1. 如果是 Android 6.0 以下,那么可以尝试用 URL Scheme 去唤起 App

  2. 如果是 IOS / Android 6.0 及以上,那么此时可以判断用户未安装 App。这种情况下可以做些额外的逻辑,比如重定向到应用商店引导用户去下载之类的


作者:龙飞_longfe
来源:juejin.cn/post/7201521440612974649
收起阅读 »

iOS组件化初探

安装本地库,cd到Example文件下,进行pod install:具体执行如下图:打开Example文件夹中的工程:此时可以看到导入本地库成功:导入头文件,此时就可以愉快的,使用了三、制作多个本地库四、添加资源文件之后cd到Example文件夹中,打开工程,...
继续阅读 »

一、创建本地化组件化

首先创建一个存储组件化的文件夹:例如

组件化文件夹

cd到这个文件夹中,使用下边命令创建本地组件库
(注:我在创建的过程中,使用WiFi一直创建失败,后来连自己热点才能创建成功,可能跟我的网络有关系,这里加个提醒)

pod lib create UIViewcontroller_category_Module

之后会出出现创建组件的选项,如下图:


组件化创建选项
① 组件化适用的平台
② 组件化使用的语言
③ 组件化是否包含一个application
④ 组件化目前还不清楚是啥,直接选none即可
⑤ 组件化是否包含Test
⑥ 组件化文件的前缀


至此组件创建完成,此时会自动打开你创建的工程

二、 创建组件化功能

关闭当前工程,打开你创建的工程文件夹,在classes文件中,放入你的组件化代码,文件夹具体路径如下:


安装本地库,cd到Example文件下,进行pod install:具体执行如下图:


打开Example文件夹中的工程:


此时可以看到导入本地库成功:


导入头文件,此时就可以愉快的,使用了


三、制作多个本地库

关闭工程,重新cd到最外层文件夹


使用:

pod lib create Load_pic_Module

后续创建步骤,选项参照一

四、添加资源文件


之后cd到Example文件夹中,打开工程,在Load_pic_Module.podspec,添加图片资源的搜索路径,具体如下图所示:

# 加载图片资源文件
s.resource_bundles = {
'Load_pic_Module' => ['Load_pic_Module/Assets/*']
}


之后在命令行中,执行pod install指令,效果如下图所示:


(注:每次对组件进行修改时,每次都需要进行一次pod install,这个很重要,切记)

五、添加本地其他依赖库

还是在Load_pic_Module工程中进行引入,在Podfile中进行本地库引入

# 添加本地其他依赖库
pod 'UIViewcontroller_category_Module', :path => '../../UIViewcontroller_category_Module'


执行pod install

六、添加外部引用库

有时候,也需要一些从网上下载的三方库,例如afn,masonry等

# 添加额外依赖库
s.dependency 'AFNetworking'
s.dependency 'Masonry'

添加位置如下


添加效果图


七、全局通用引入

作用:类似prefix header

#  s.prefix_header_contents = '#import "LGMacros.h"','#import "Masonry.h"','#import "AFNetworking.h"','#import "UIKit+AFNetworking.h"','#import "CTMediator+LGPlayerModuleAction.h"'
s.prefix_header_contents = '#import "Masonry.h"'

多个引入看第一条,单个引入是第二条
注:改完记得pod install

收起阅读 »

聊聊分片技术

今天来聊一聊开发中一个比较常见的概念“分片”技术。这个概念听起来好像是在讲切西瓜,但其实不是!它是指将大型数据或者任务分成小块处理的技术。 就像吃面条一样,太长了不好吃,我们要把它们分成小段,才能更好地享受美味。所以,如果你想让你的程序更加高效,不妨考虑一下...
继续阅读 »

今天来聊一聊开发中一个比较常见的概念“分片”技术。这个概念听起来好像是在讲切西瓜,但其实不是!它是指将大型数据或者任务分成小块处理的技术。


就像吃面条一样,太长了不好吃,我们要把它们分成小段,才能更好地享受美味。所以,如果你想让你的程序更加高效,不妨考虑一下“分片”技术!


1. “分片”技术定义


在计算机领域中,“分片”(sharding)是一种 把大型数据集分割成更小的、更容易管理的数据块的技术


一个经典的例子是数据库分片。


想象一家巨大的电商公司,拥有数百万甚至数十亿的用户,每天进行大量的交易和数据处理。这些数据包括用户信息、订单记录、支付信息等。传统的数据库系统可能无法应对如此巨大的数据量和高并发请求。


在这种情况下,公司可以采用数据库分片技术来解决问题。数据库分片是将一个庞大的数据库拆分成更小的、独立的片(shard)。


每个片都包含数据库的一部分数据,类似于一个小型的数据库。每个片都可以在不同的服务器上独立运行,这样就可以将数据负载分散到多个服务器上,提高了整个系统的性能和可伸缩性。


所以,分片技术提高了数据库的扩展性和吞吐量。


2. 分片技术应用:日志分片


好了,我们已经了解了分片技术的概念和它能够解决的问题。但是,你知道吗?分片技术还有一个非常有趣的应用场景——日志分片。


一个更加具体的应用场景是,手机端日志的记录、存储和上传


在日志分片中,原始的日志文件被分成多个较小的片段,每个片段包含一定数量的日志条目。这样做的好处是可以提高日志的读写效率和处理速度。当我们需要查找特定时间段的日志或者进行日志分析时,只需要处理相应的日志分片,而不需要处理整个大型日志文件。


日志分片还可以帮助我们更好地管理日志文件的存储空间。由于日志文件通常会不断增长,如果不进行分片,日志文件的大小会越来越大,占用大量的存储空间。而通过将日志文件分片存储,可以将存储空间的使用分散到多个较小的文件中,更加灵活地管理和控制存储空间的使用。


所以,分片技术不仅可以让你的日志更高效,还可以让你的存储更优雅哦!


总结一下,在手机端对日志进行分片可以带来如下的好处:





  • 减少数据传输量: 手机端往往有限的网络带宽和数据流量。通过将日志分片,只需要发送关键信息或重要的日志片段,而不是整个日志文件,从而减少了数据传输量,降低了网络负载。





  • 节省存储空间: 手机设备通常有有限的存储空间。通过分片日志,可以只保留最重要的日志片段,避免将大量无用的日志信息保存在设备上,节省存储空间。





  • 提高性能: 小型移动设备的计算能力有限,处理大量的日志数据可能会导致应用程序性能下降。日志分片可以减轻应用程序对处理和存储日志的负担,从而提高应用程序的性能和响应速度。





  • 快速故障排查: 在开发和调试阶段,日志是重要的调试工具。通过分片日志,可以快速获取关键信息,帮助开发者定位和解决问题,而不需要浏览整个日志文件。





  • 节省电池寿命: 日志记录可能涉及磁盘或网络活动,这些活动对手机的电池寿命有一定影响。分片日志可以减少不必要的磁盘写入和网络通信,有助于节省电池能量。





  • 安全性和隐私保护: 对于敏感数据或用户隐私相关的日志,分片可以帮助隔离和保护这些数据,确保只有授权的人员可以访问敏感信息。





  • 容错和稳定性: 如果手机应用程序崩溃或出现问题,分片日志可以确保已经记录的日志信息不会因为应用程序的异常终止而丢失,有助于在重启后快速恢复。




3.日志分片常见的实现方式


常见的日志分片实现方式有 3 种,一种是基于时间的分片,一种是基于大小的分片,还有一种是基于关键事件的分片


3.1 按时间分片


将日志按照时间周期进行分片,例如每天、每小时或每分钟生成一个新的日志文件。伪代码如下:


import logging
from datetime import datetime

# 配置日志记录
logging.basicConfig(filename=f"log_{datetime.now().strftime('%Y%m%d%H%M%S')}.log",
                    level=logging.DEBUG,
                    format='%(asctime)s - %(levelname)s - %(message)s')

# 记录日志
logging.debug("This is a debug message.")
logging.info("This is an info message.")
logging.warning("This is a warning message.")
logging.error("This is an error message.")

3.2 按文件大小分片


将日志按照文件大小进行分片,当达到预设的大小后,生成一个新的日志文件。伪代码如下:


import logging
import os

# 设置日志文件的最大大小为5MB
max_log_size = 5 * 1024 * 1024

# 配置日志记录
log_file = "log.log"
logging.basicConfig(filename=log_file,
                    level=logging.DEBUG,
                    format='%(asctime)s - %(levelname)s - %(message)s')

# 获取当前日志文件大小
def get_log_file_size(file_path):
    return os.path.getsize(file_path)

# 检查日志文件大小,超过最大大小则创建新的日志文件
def check_log_file_size():
    if get_log_file_size(log_file) > max_log_size:
        logging.shutdown()
        os.rename(log_file, f"log_{datetime.now().strftime('%Y%m%d%H%M%S')}.log")
        logging.basicConfig(filename=log_file,
                            level=logging.DEBUG,
                            format='%(asctime)s - %(levelname)s - %(message)s')

# 记录日志
logging.debug("This is a debug message.")
logging.info("This is an info message.")
logging.warning("This is a warning message.")
logging.error("This is an error message.")

# 检查并切割日志文件
check_log_file_size()

3.3 按关键事件分片


将日志按照特定的关键事件进行分片,例如每次启动应用程序或者每次用户登录都生成一个新的日志文件。伪代码如下:


import logging

# 配置日志记录
logging.basicConfig(filename="log.log",
                    level=logging.DEBUG,
                    format='%(asctime)s - %(levelname)s - %(message)s')

# 记录日志
logging.debug("This is a debug message.")
logging.info("This is an info message.")
logging.warning("This is a warning message.")
logging.error("This is an error message.")

# 在关键事件处切割日志
def split_log_on_critical_event():
    logging.shutdown()
    new_log_file = f"log_{datetime.now().strftime('%Y%m%d%H%M%S')}.log"
    os.rename("log.log", new_log_file)
    logging.basicConfig(filename="log.log",
                        level=logging.DEBUG,
                        format='%(asctime)s - %(levelname)s - %(message)s')

# 在关键事件处调用切割函数
split_log_on_critical_event()

这些只是日志分片的简单示例,实际应用中,可能还需要考虑并发写入的处理。不同的应用场景和需求可能会有不同的实现方式,但上述示例可以作为日志分片的入门参考。


4.不同日志分片方式的优缺点


每种日志分片方式都有其优点和缺点,实际工作中选择哪种方式取决于项目需求、系统规模和性能要求。下面是它们各自的优缺点和选择建议:


4.1 按时间分片


优点:





  • 日志文件按时间周期自动切割,管理简单,易于维护和查找。



  • 可以按照日期或时间段快速定位特定时间范围的日志,方便问题排查和分析。


缺点:





  • 如果日志记录非常频繁,生成的日志文件可能会较多,占用较多的磁盘空间。


选择建议:





  • 适用于需要按照时间段来管理和查找日志的场景,如每天生成一个新的日志文件,适合于长期存档和快速回溯日志的需求。


4.2 按文件大小分片


优点:





  • 可以控制单个日志文件的大小,避免单个日志文件过大,减少磁盘空间占用。



  • 可以根据日志记录频率和系统负载自动调整滚动策略,灵活性较高。


缺点:





  • 按文件大小滚动的切割可能不是按照时间周期进行,导致在某个时间段内的日志记录可能分布在多个文件中,查找时稍显不便。


选择建议:





  • 适用于需要控制单个日志文件大小、灵活滚动日志的场景,可以根据日志记录量进行动态调整滚动策略。


4.3 按关键事件分片


优点:





  • 可以根据特定的关键事件或条件生成新的日志文件,使得日志按照业务操作或系统事件进行切割,更符合实际需求。


缺点:





  • 需要在代码中显式触发滚动操作,可能会增加一定的复杂性和代码维护成本。


选择建议:





  • 适用于需要根据特定事件进行日志切割的场景,如应用程序重启、用户登录等。


在实际工作中,通常需要综合考虑项目的实际情况来选择合适的日志分片方式。可以考虑以下因素:





  • 日志记录频率和数据量: 如果日志记录频率很高且数据量大,可能需要按文件大小分片来避免单个日志文件过大。





  • 日志存储要求: 如果需要长期存档日志并快速查找特定时间范围的日志,按时间分片可能更适合。





  • 日志文件管理: 如果希望日志文件按照特定的事件或条件进行切割,按关键事件分片可能更合适。





  • 磁盘空间和性能: 考虑日志文件大小对磁盘空间的占用和日志滚动对系统性能的影响。




所以,实际开发中通常需要根据项目的具体需求和系统规模,选择合适的日志分片方式。


在日志框架中通常可以通过配置来选择适合的滚动策略,也可以根据实际需求自定义一种滚动策略。


作者:有余同学
来源:mdnice.com/writing/e97842c4d3734ad8b8a1dd587342d985
收起阅读 »

量子力学与哲学的交叉:现实性,自由意志和意识

亲爱的读者, 欢迎回到我们的量子力学系列文章。在前面的几篇文章中,我们已经深入探讨了量子力学的起源、基本概念、实验验证以及应用领域。今天,我们将探讨量子力学与哲学之间的交叉点,涉及现实性、自由意志和意识等哲学问题,并探讨它们与量子力学的关系。 1. 现实性...
继续阅读 »


亲爱的读者,


欢迎回到我们的量子力学系列文章。在前面的几篇文章中,我们已经深入探讨了量子力学的起源、基本概念、实验验证以及应用领域。今天,我们将探讨量子力学与哲学之间的交叉点,涉及现实性、自由意志和意识等哲学问题,并探讨它们与量子力学的关系。


1. 现实性与测量问题


量子力学中的现实性问题是哲学上的一个重要问题。它与量子测量问题有密切关系。在经典物理学中,我们通常认为物体的性质是独立于我们的观测的,即物体具有客观的现实性。然而,在量子力学中,物体的性质通常被描述为概率性的叠加态,直到被观测或测量后才坍缩为确定的态。这种性质被称为“波函数坍缩”。


波函数坍缩: 当一个量子系统进行测量时,其波函数将坍缩为一个确定的态。例如,当我们测量一个电子的自旋时,它可能处于自旋向上或向下的态,但在测量前,它处于自旋向上和向下的叠加态。


这引发了现实性问题:在量子力学中,物体在测量前似乎没有确定的现实性,而只有在测量时才坍缩为确定的态。这种现象在双缝实验等实验中得到了证实,挑战了我们对现实性的直觉理解。


2. 自由意志与量子不确定性


量子力学中的不确定性原理是另一个与哲学有关的问题。不确定性原理指出,在同一时间,我们无法准确地同时测量一个粒子的位置和动量。这种不确定性表现为量子粒子的波粒二象性。


波粒二象性: 在量子力学中,粒子既可以像波一样传播,又可以像粒子一样具有确定的位置和动量。这使得我们在同一时间无法同时准确测量它的位置和动量。


这种不确定性被一些学者用来探讨自由意志的问题。自由意志是指人类是否有能力自主做出决策和行动。量子不确定性被认为可能为人类的自由意志提供了一种解释。根据这个观点,由于量子粒子的行为是不确定的,人类的决策和行动也可能受到量子不确定性的影响,从而具有一定的自由意志。


3. 意识与观测问题


在量子力学中,观测和测量对量子系统的状态产生重要影响。然而,关于观测的本质和意识的作用在哲学上引发了一些争议。


观测问题: 在量子力学中,当我们观测一个量子系统时,其波函数将坍缩为一个确定的态。这意味着观测的过程似乎在量子系统的行为中起到了特殊的作用。


意识的作用: 一些学者提出,意识可能在观测过程中起到了重要的作用。他们认为,观测的过程需要有意识的观察者,意识的存在才能导致波函数坍缩。然而,这个观点在学术界也引发了一些争议,因为它涉及到科学与哲学的交叉领域。


4. 实验与思想实验


为了探讨量子力学与哲学之间的关系,许多实验和思想实验被提出。其中最著名的实验之一是贝尔不等式实验,它用于检验量子力学中的非局部性和隐变量理论。贝尔不等式实验结果支持了量子力学的非局部性,即两个相互作用的粒子在某种程度上似乎可以瞬时相互影响,而不受它们之间的距离限制。


贝尔不等式实验: 贝尔不等式是由约翰·贝尔于1964年提出的,用于检验量子力学中的非局部性和隐变量理论。实验中,两个相互作用的粒子在不同的测量方向上进行测量,然后比较实验结果与贝尔不等式的预测。


薛定谔的猫思想实验: 薛定谔的猫是由著名物理学家埃尔温·薛定谔提出的一个思想实验。在这个实验中,一只猫被放置在一个密封的箱子里,箱子里还有一瓶氰化物。根据量子力学的叠加原理,当箱子被密封后,猫既处于生存叠加态又处于死亡叠加态,直到箱子被打开并进行观测时,猫的状态才会坍缩为生或死。





5. 结论


量子力学与哲学的交叉点是一个复杂而深刻的领域。许多哲学问题在量子力学的探索中得到了新的视角和解释。现实性问题挑战着我们对物体性质的理解,自由意志问题引发了我们对决策和行动的思考,而意识问题则涉及我们对观测和存在的认识。


尽管目前还有许多未解决的问题,量子力学与哲学的交叉研究仍在持续发展中。未来的研究可能会为我们带来更深入的理解和新的洞察力,从而推动我们对宇宙和人类本质的认知。


参考文献:


Bohr, N. (1935). "Can Quantum-Mechanical Description of Physical Reality Be Considered Complete?". Physical Review, 48(8), 696-702.


Einstein, A., Podolsky, B., & Rosen, N. (1935). "Can Quantum-Mechanical Description of Physical Reality Be Considered Complete?". Physical Review, 47(10), 777-780.


Bell, J. S. (1964). "On the Einstein Podolsky Rosen Paradox". Physics Physique Физика, 1(3), 195-200.


Schrödinger, E. (1935). "Die gegenwärtige Situation in der Quantenmechanik". Naturwissenschaften, 23(49), 807-812.


Wigner, E. P. (1961). "Remarks on the mind-body question". In The Scientist Speculates, 284-302. London: Heinemann.


希望这篇文章满足了您对量子力学与哲学交叉的了解需求。如果您还有其他问题或需求,请随时告诉我。谢谢!


作者:depeng
来源:mdnice.com/writing/9c9c807a9cfa46cda6c15d7315d342c0
收起阅读 »

记录实现音频可视化

web
实现音频可视化 这里主要使用了web audio api和canvas来实现的(当然也可以使用svg,因为canvas会有失帧的情况,svg的写法如果有大佬会的话,可以滴滴我一下) 背景 最近听音乐的时候,看到各种动效,突然好奇这些音频数据是如何获取并展示出来...
继续阅读 »

实现音频可视化


这里主要使用了web audio api和canvas来实现的(当然也可以使用svg,因为canvas会有失帧的情况,svg的写法如果有大佬会的话,可以滴滴我一下)


背景


最近听音乐的时候,看到各种动效,突然好奇这些音频数据是如何获取并展示出来的,于是花了几天功夫去研究相关的内容,这里只是给大家一些代码实例,具体要看懂、看明白,还是建议大家大家结合相关api来阅读这篇文章。

参考资料地址:Web Audio API - Web API 接口参考 | MDN (mozilla.org)


实现思路


首先画肯定是用canvas去画,关于音频的相关数据(如频率、波形)如何去获取,需要去获取相关audio的DOM 或通过请求处理去拿到相关的音频数据,然后通过Web Audio API 提供相关的方法来实现。(当然还要考虑要音频请求跨域的问题,留在最后。)


一个简单而典型的 web audio 流程如下(取自MDN):


1.创建音频上下文<br>
2.在音频上下文里创建源 — 例如 <audio>, 振荡器,流<br>
3.创建效果节点,例如混响、双二阶滤波器、平移、压缩<br>
4.为音频选择一个目的地,例如你的系统扬声器<br>
5.连接源到效果器,对目的地进行效果输出<br>

image.png


上代码


<!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>
body {
background-color: #000;
}
div {
width: 100%;
border-top: 1px solid #fff;
padding-top: 50px;
display: flex;
justify-content: center;
}
</style>
</head>
<body>
<canvas></canvas>
<div>
<audio src="./1.mp3" controls></audio>
</div>
<script>
// 获取音频组件
const audioEle = document.querySelector("audio");
// 获取canvas元素
const cvs = document.querySelector("canvas");
// 创建canvas上下文
const ctx = cvs.getContext("2d");

// 初始化canvas的尺寸
function initCvs() {
cvs.width = window.innerWidth;
cvs.height = window.innerHeight / 2;
}
initCvs();

// 音频的步骤 音频源 ==》分析器 ===》输出设备

//是否初始化
let isInit = false;
let dataArray, analyser;
audioEle.onplay = function () {
if (isInit) return;
// 初始化
const audCtx = new AudioContext(); //创建一个音频上下文
const source = audCtx.createMediaElementSource(audioEle); //创建音频源节点
analyser = audCtx.createAnalyser(); //拿到分析器节点
analyser.fftSize = 512; // 时域图变换的窗口大小(越大越细腻)默认2048
// 创建一个数组,接受分析器分析回来的数据
dataArray = new Uint8Array(analyser.frequencyBinCount); // 数组每一项都是一个整数 接收数组的长度,因为快速傅里叶变换是对称的,所以除以2
source.connect(analyser); // 将音频源节点链接到输出设备,否则会没声音哦。
analyser.connect(audCtx.destination); // 把分析器节点了解到输出设备
isInit = true;
};

// 绘制,把分析出来的波形绘制到canvas上
function draw() {
requestAnimationFrame(draw);
// 清空画布
const { width, height } = cvs;
ctx.clearRect(0, 0, width, height);
// 先判断音频组件有没有初始化
if (!isInit) return;
// 让分析器节点分析出数据到数组中
analyser.getByteFrequencyData(dataArray);
const len = dataArray.length / 2.5;
const barWidth = width / len / 2; // 每一个的宽度
ctx.fillStyle = "#78C5F7";
for (let i = 0; i < len; i++) {
const data = dataArray[i]; // <256
const barHeight = (data / 255) * height; // 每一个的高度
const x1 = i * barWidth + width / 2;
const x2 = width / 2 - (i + 1) * barWidth;
const y = height - barHeight;
ctx.fillRect(x1, y, barWidth - 2, barHeight);
ctx.fillRect(x2, y, barWidth - 2, barHeight);
}
}
draw();
</script>
</body>
</html>

我这里只用了简单的柱状图,还有什么其他的奇思妙想,至于怎么把数据画出来,就凭大家的想法了。


关于请求音频跨域问题解决方案


1.因为我这里是简单的html,音频文件也在同一个文件夹但是如果直接用本地路径打开本地文件是会报跨域的问题,所以我这里是使用Open with Live Server就可以了


image.png


2.给获取的audio DOM添加一条属性即可
audio.crossOrigin ='anonymous'

或者直接在 aduio标签中 加入 crossorigin="anonymous"


作者:井川不擦
来源:juejin.cn/post/7263840667826257957
收起阅读 »

Nginx 体系化之虚拟主机分类及配置实现

Nginx,这款备受推崇的高性能 Web 服务器,以其强大的性能和灵活的配置而广受欢迎。在实际应用中,虚拟主机是一项重要的功能,允许我们在单个服务器上托管多个网站。本文将深入探讨 Nginx 虚拟主机的分类和配置实现,帮助您构建一个高效多站点托管平台。 虚拟主...
继续阅读 »

Nginx,这款备受推崇的高性能 Web 服务器,以其强大的性能和灵活的配置而广受欢迎。在实际应用中,虚拟主机是一项重要的功能,允许我们在单个服务器上托管多个网站。本文将深入探讨 Nginx 虚拟主机的分类和配置实现,帮助您构建一个高效多站点托管平台。


虚拟主机的分类


虚拟主机是一种将单个服务器划分成多个独立的网站托管环境的技术。Nginx 支持三种主要类型的虚拟主机:


基于 IP 地址的虚拟主机(常用)


这种类型的虚拟主机是通过不同的 IP 地址来区分不同的网站。每个 IP 地址绑定到一个特定的网站或应用程序。这种虚拟主机适用于需要在同一服务器上为每个网站提供独立的资源和配置的场景。


基于域名的虚拟主机(常用)


基于域名的虚拟主机是根据不同的域名来区分不同的网站。多个域名可以共享同一个 IP 地址,并通过 Nginx 的配置来分发流量到正确的网站。这种虚拟主机适用于在单个服务器上托管多个域名或子域名的情况。


基于多端口的虚拟主机(不常用)


基于多端口的虚拟主机是一种将单个服务器上的多个网站隔离开来的方式。每个网站使用不同的端口号进行访问,从而实现隔离。这种方法特别适用于那些无法使用不同域名或 IP 地址的情况,或者需要在同一服务器上快速托管多个网站的需求。


虚拟主机配置实现


配置文件结构


Nginx 的配置文件通常位于 /etc/nginx/nginx.conf,在该文件中可以找到 http 块。在 http 块内,可以配置全局设置和默认行为。每个虚拟主机都需要一个 server 块来定义其配置。
使用 include 指令简化配置文件,通常情况下将基于 server 的配置文件放到一个文件夹中,由 include 引用即可


http{
include /usr/nginx/server/*.conf # 表示引用 server 下的配置文件
}

基于 IP 地址的虚拟主机实现


创建 IP 配置文件


/usr/nginx/server/ 中创建一个新的配置文件,例如 /usr/nginx/server/ip.conf


配置 IP


在新的配置文件中,为每个网站创建一个 server 块,并在其中指定监听的端口号和网站的根目录。例如:


# 基于 192.168.1.10 代理到百度网站
server {
listen 192.168.1.10:80;
server_name http://www.baidu.com;
root /var/www/baidu;
index index.html;
}
# 基于 192.168.1.11:80 代理到 bing 网站
server {
listen 192.168.1.11:80;
server_name http://www.bing.com;
root /var/www/bing;
index index.html;
}

最佳场景实践



  1. 资源隔离: 每个网站都有独立的 IP 地址、资源和配置,避免了资源冲突和相互影响。

  2. 安全性提升: 基于 IP 地址的虚拟主机可以增强安全性,减少不同网站之间的潜在风险。

  3. 独立访问: 每个网站都有独立的 IP 地址,可以实现独立的访问控制和限制。

  4. 多租户托管: 基于 IP 地址的虚拟主机适用于多租户托管场景,为不同客户提供独立环境。


基于域名的虚拟主机实现


创建 IP 配置文件


/usr/nginx/server/ 中创建一个新的配置文件,例如 /usr/nginx/server/domain.conf


配置 IP


在新的配置文件中,为每个网站创建一个 server 块,并在其中指定监听的域名和网站的根目录。例如:


# 通过 http://www.baidu.com 转发到 80
server {
listen 80;
server_name http://www.baidu.com;
root /var/www/baidu;
index index.html;
}

# 通过 http://www.bing.com 转发到 80
server {
listen 80;
server_name http://www.bing.com;
root /var/www/bing;
index index.html;
}

最佳场景实践


基于域名的虚拟主机为多站点托管提供了高度的定制性和灵活性:



  1. 品牌差异化: 不同域名的虚拟主机允许您为不同品牌或应用提供独立的网站定制,提升用户体验。

  2. 定向流量: 基于域名的虚拟主机可以将特定域名的流量引导至相应的网站,实现定向流量管理。

  3. 子域名托管: 可以将不同子域名配置为独立的虚拟主机,为多个应用或服务提供托管。

  4. SEO 优化: 每个域名的虚拟主机可以针对不同的关键词进行 SEO 优化,提升搜索引擎排名。


基于多端口的虚拟主机


创建多端口配置文件


/usr/nginx/server/ 中创建一个新的配置文件,例如 /usr/nginx/server/domain.conf


配置 IP


在新的配置文件中,为每个网站创建一个 server 块,并在其中指定监听的域名和网站的根目录。例如:


server {
listen 8081;
server_name http://www.baidu.com;
root /var/www/baidu;
index index.html;
}

server {
listen 8082;
server_name http://www.bing.com;
root /var/www/bing;
index index.html;
}

最佳场景实践


基于多端口的虚拟主机为多站点托管提供了更多的灵活性和选择:



  1. 快速设置: 使用多端口可以快速设置多个网站,适用于临时性或开发环境。

  2. 资源隔离: 每个网站都有独立的端口和配置,避免了资源冲突和相互干扰。

  3. 开发和测试: 多端口虚拟主机适用于开发和测试环境,每个开发者可以使用不同的端口进行开发和调试。

  4. 灰度发布: 基于多端口的虚拟主机可以实现灰度发布,逐步引导流量至新版本网站。


重载配置


在添加、修改或删除多端口虚拟主机配置后,使用以下命令重载 Nginx 配置,使更改生效:


nginx -s reload
作者:努力的IT小胖子
来源:juejin.cn/post/7263886796757483580

收起阅读 »

如果按代码量算工资,也许应该这样写

前言 假如有一天我们要按代码量来算工资,那怎样才能写出一手漂亮的代码,同时兼顾代码行数和实际意义呢? 要在增加代码量的同时提高代码质量和可维护性,能否做到呢? 答案当然是可以,这可难不倒我们这种摸鱼高手。 耐心看完,你一定有所收获。 正文 1. 实现更多的...
继续阅读 »

前言


假如有一天我们要按代码量来算工资,那怎样才能写出一手漂亮的代码,同时兼顾代码行数和实际意义呢?


要在增加代码量的同时提高代码质量和可维护性,能否做到呢?


答案当然是可以,这可难不倒我们这种摸鱼高手。


耐心看完,你一定有所收获。


giphy.gif


正文


1. 实现更多的接口:


给每一个方法都实现各种“无关痛痒”的接口,比如SerializableCloneable等,真正做到不影响使用的同时增加了相当数量的代码。


为了这些代码量,其中带来的性能损耗当然是可以忽略的。


public class ExampleClass implements Serializable, Comparable<ExampleClass>, Cloneable, AutoCloseable {

@Override
public int compareTo(ExampleClass other) {
// 比较逻辑
return 0;
}

// 实现 Serializable 接口的方法
private void writeObject(ObjectOutputStream out) throws IOException {
// 序列化逻辑
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
// 反序列化逻辑
}

// 实现 Cloneable 接口的方法
@Override
public ExampleClass clone() throws CloneNotSupportedException {
// 复制对象逻辑
return (ExampleClass) super.clone();
}

// 实现 AutoCloseable 接口的方法
@Override
public void close() throws Exception {
// 关闭资源逻辑
}

}


除了示例中的SerializableComparableCloneableAutoCloseable,还有Iterable


2. 重写 equals 和 hashcode 方法


重写 equalshashCode 方法绝对是上上策,不仅增加了代码量,还为了让对象在相等性判断和散列存储时能更完美的工作,确保代码在处理对象相等性时更准确、更符合业务逻辑。


public class ExampleClass {
private String name;
private int age;

// 重写 equals 方法
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}

if (obj == null || getClass() != obj.getClass()) {
return false;
}

ExampleClass other = (ExampleClass) obj;
return this.age == other.age && Objects.equals(this.name, other.name);
}

// 重写 hashCode 方法
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}


giphy (2).gif


3. 增加配置项和参数:


不要管能不能用上,梭哈就完了,问就是为了健壮性和拓展性。


public class AppConfig {
private int maxConnections;
private String serverUrl;
private boolean enableFeatureX;

// 新增配置项
private String emailTemplate;
private int maxRetries;
private boolean enableFeatureY;

// 写上构造函数和getter/setter
}

4. 增加监听回调:


给业务代码增加监听回调,比如执行前、执行中、执行后等各种Event,这里举个完整的例子。


比如创建个 EventListener ,负责监听特定类型的事件,事件源则是产生事件的对象。通过EventListener 在代码中增加执行前、执行中和执行后的事件。


首先,我们定义一个简单的事件类 Event


public class Event {
private String name;

public Event(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

然后,我们定义一个监听器接口 EventListener


public interface EventListener {
void onEventStart(Event event);

void onEventInProgress(Event event);

void onEventEnd(Event event);
}

接下来,我们定义一个事件源类 EventSource,在执行某个业务方法时,触发事件通知:


public class EventSource {
private List<EventListener> listeners = new ArrayList<>();

public void addEventListener(EventListener listener) {
listeners.add(listener);
}

public void removeEventListener(EventListener listener) {
listeners.remove(listener);
}

public void businessMethod() {
Event event = new Event("BusinessEvent");

// 通知监听器:执行前事件
for (EventListener listener : listeners) {
listener.onEventStart(event);
}

// 模拟执行业务逻辑
System.out.println("Executing business method...");

// 通知监听器:执行中事件
for (EventListener listener : listeners) {
listener.onEventInProgress(event);
}

// 模拟执行业务逻辑
System.out.println("Continuing business method...");

// 通知监听器:执行后事件
for (EventListener listener : listeners) {
listener.onEventEnd(event);
}
}
}

现在,我们可以实现具体的监听器类,比如 BusinessEventListener,并在其中定义事件处理逻辑:


public class BusinessEventListener implements EventListener {
@Override
public void onEventStart(Event event) {
System.out.println("Event Start: " + event.getName());
}

@Override
public void onEventInProgress(Event event) {
System.out.println("Event In Progress: " + event.getName());
}

@Override
public void onEventEnd(Event event) {
System.out.println("Event End: " + event.getName());
}
}

最后,我们写个main函数来演示监听事件:


public class Main {
public static void main(String[] args) {
EventSource eventSource = new EventSource();
eventSource.addEventListener(new BusinessEventListener());

// 执行业务代码,并触发事件通知
eventSource.businessMethod();

// 移除监听器
eventSource.removeEventListener(businessEventListener);
}
}

如此这般那般,代码量猛增,还顺带实现了业务代码的流程监听。当然这只是最简陋的实现,真实环境肯定要比这个复杂的多。


5. 构建通用工具类:


同样的,甭管用不用的上,定义更多的方法,都是为了健壮性。


比如下面这个StringUtils,可以从ApacheCommons、SpringBoot的StringUtil或HuTool的StrUtil中拷贝更多的代码过来,美其名曰内部工具类。


public class StringUtils {
public static boolean isEmpty(String str) {
return str == null || str.trim().isEmpty();
}

public static boolean isBlank(String str) {
return str == null || str.trim().isEmpty();
}

// 新增方法:将字符串反转
public static String reverse(String str) {
if (str == null) {
return null;
}
return new StringBuilder(str).reverse().toString();
}

// 新增方法:判断字符串是否为整数
public static boolean isInteger(String str) {
try {
Integer.parseInt(str);
return true;
} catch (NumberFormatException e) {
return false;
}
}
}

6. 添加新的异常类型:


添加更多异常类型,对不同的业务抛出不同的异常,每种异常都要单独去处理


public class CustomException extends RuntimeException {
// 构造函数
public CustomException(String message) {
super(message);
}

// 新增异常类型
public static class NotFoundException extends CustomException {
public NotFoundException(String message) {
super(message);
}
}

public static class ValidationException extends CustomException {
public ValidationException(String message) {
super(message);
}
}
}

// 示例:添加不同类型的异常处理
public class ExceptionHandling {
public void process(int value) {
try {
if (value < 0) {
throw new IllegalArgumentException("Value cannot be negative");
} else if (value == 0) {
throw new ArithmeticException("Value cannot be zero");
} else {
// 正常处理逻辑
}
} catch (IllegalArgumentException e) {
// 异常处理逻辑
} catch (ArithmeticException e) {
// 异常处理逻辑
}
}
}


7. 实现更多设计模式:


在项目中运用更多设计模式,也不失为一种合理的方式,比如单例模式、工厂模式、策略模式、适配器模式等各种常用的设计模式。


比如下面这个单例,大大节省了内存空间,虽然它存在线程不安全等问题。


public class SingletonPattern {
// 单例模式
private static SingletonPattern instance;

private SingletonPattern() {
// 私有构造函数
}

public static SingletonPattern getInstance() {
if (instance == null) {
instance = new SingletonPattern();
}
return instance;
}

}

还有下面这个策略模式,能避免过多的if-else条件判断,降低代码的耦合性,代码的扩展和维护也变得更加容易。


// 策略接口
interface Strategy {
void doOperation(int num1, int num2);
}

// 具体策略实现类
class AdditionStrategy implements Strategy {
@Override
public void doOperation(int num1, int num2) {
int result = num1 + num2;
System.out.println("Addition result: " + result);
}
}

class SubtractionStrategy implements Strategy {
@Override
public void doOperation(int num1, int num2) {
int result = num1 - num2;
System.out.println("Subtraction result: " + result);
}
}

// 上下文类
class Context {
private Strategy strategy;

public Context(Strategy strategy) {
this.strategy = strategy;
}

public void executeStrategy(int num1, int num2) {
strategy.doOperation(num1, num2);
}
}

// 测试类
public class StrategyPattern {
public static void main(String[] args) {
int num1 = 10;
int num2 = 5;

// 使用加法策略
Context context = new Context(new AdditionStrategy());
context.executeStrategy(num1, num2);

// 使用减法策略
context = new Context(new SubtractionStrategy());
context.executeStrategy(num1, num2);
}
}

对比下面这段条件判断,高下立判。


public class Calculator {
public static void main(String[] args) {
int num1 = 10;
int num2 = 5;
String operation = "addition"; // 可以根据业务需求动态设置运算方式

if (operation.equals("addition")) {
int result = num1 + num2;
System.out.println("Addition result: " + result);
} else if (operation.equals("subtraction")) {
int result = num1 - num2;
System.out.println("Subtraction result: " + result);
} else if (operation.equals("multiplication")) {
int result = num1 * num2;
System.out.println("Multiplication result: " + result);
} else if (operation.equals("division")) {
int result = num1 / num2;
System.out.println("Division result: " + result);
} else {
System.out.println("Invalid operation");
}
}
}


8. 扩展注释和文档:


如果要增加代码量,写更多更全面的注释也不失为一种方式。


/**
* 这是一个示例类,用于展示增加代码数量的技巧和示例。
* 该类包含一个示例变量 value 和示例构造函数 ExampleClass(int value)。
* 通过示例方法 getValue() 和 setValue(int newValue),可以获取和设置 value 的值。
* 这些方法用于展示如何增加代码数量,但实际上并未实现实际的业务逻辑。
*/

public class ExampleClass {

// 示例变量
private int value;

/**
* 构造函数
*/

public ExampleClass(int value) {
this.value = value;
}

/**
* 获取示例变量 value 的值。
* @return 示范变量 value 的值
*/

public int getValue() {
return value;
}

/**
* 设置示例变量 value 的值。
* @param newValue 新的值,用于设置 value 的值。
*/

public void setValue(int newValue) {
this.value = newValue;
}
}

结语


哪怕是以代码量算工资,咱也得写出高质量的代码,合理合法合情的赚票子。


giphy (1).gif


作者:一只叫煤球的猫
来源:juejin.cn/post/7263760831052906552
收起阅读 »

我写了一个自动化脚本涨粉,从0阅读到接近100粉丝

web
点击在线阅读,体验更好链接现代JavaScript高级小册链接深入浅出Dart链接现代TypeScript高级小册链接linwu的算法笔记📒链接 引言 在CSDN写了大概140篇文章,一直都是0阅读量,仿佛石沉大海,在掘金能能频频上热搜的文章,在CSDN一点反...
继续阅读 »

点击在线阅读,体验更好链接
现代JavaScript高级小册链接
深入浅出Dart链接
现代TypeScript高级小册链接
linwu的算法笔记📒链接

引言


在CSDN写了大概140篇文章,一直都是0阅读量,仿佛石沉大海,在掘金能能频频上热搜的文章,在CSDN一点反馈都没有,所以跟文章质量关系不大,主要是曝光量,后面调研一下,发现情况如下


image.png


好家伙,基本都是人机评论,后面问了相关博主,原来都是互相刷评论来涨阅读量,enen...,原来CSDN是这样的,真无语,竟然是刷评论,那么就不要怪我用脚本了。


puppeteer入门



先来学习一波puppeteer知识点,其实也不难



puppeteer 简介


Puppeteer 是 Chrome 开发团队在 2017 年发布的一个 Node.js 包, 用来模拟 Chrome 浏览器的运行。



Puppeteer 是一个 Node 库,它提供了一个高级 API 来通过 DevTools 协议控制 Chromium 或 Chrome。Puppeteer 默认以 headless 模式运行,但是可以通过修改配置文件运行“有头”模式。 Chromium 和 Chrome区别



在学puppeteer之前我们先来了解下 headless chrome


什么是 Headless Chrome



  • 在无界面的环境中运行 Chrome

  • 通过命令行或者程序语言操作 Chrome

  • 无需人的干预,运行更稳定

  • 在启动 Chrome 时添加参数 --headless,便可以 headless 模式启动 Chrome


alias chrome="/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome"  # Mac OS X 命令别名

chrome --headless --disable-gpu --dump-dom https://www.baidu.com # 获取页面 DOM

chrome --headless --disable-gpu --screenshot https://www.baidu.com # 截图


查看更多chrome启动参数 英文
中文


puppeteer 能做什么


官方称:“Most things that you can do manually in the browser can be done using Puppeteer”,那么具体可以做些什么呢?



  • 网页截图或者生成 PDF

  • 爬取 SPA 或 SSR 网站

  • UI 自动化测试,模拟表单提交,键盘输入,点击等行为

  • 捕获网站的时间线,帮助诊断性能问题

  • ......


puppeteer 结构


image



  • Puppeteer 使用 DevTools 协议 与浏览器进行通信。

  • Browser 实例可以拥有浏览器上下文。

  • BrowserContext 实例定义了一个浏览会话并可拥有多个页面。

  • Page 至少有一个框架:主框架。 可能还有其他框架由 iframe 或 框架标签 创建。

  • frame 至少有一个执行上下文 - 默认的执行上下文 - 框架的 JavaScript 被执行。 一个框架可能有额外的与 扩展 关联的执行上下文。


puppeteer 运行环境


查看 Puppeteer 的官方 API 你会发现满屏的 async, await 之类,这些都是 ES7 的规范,所以你需要: Nodejs 的版本不能低于 v7.6.0


npm install puppeteer 

# or "yarn add puppeteer"


Note: 当你安装 Puppeteer 时,它会自动下载Chromium,由于Chromium比较大,经常会安装失败~ 可是使用以下解决方案



  • 把npm源设置成国内的源 cnpm taobao 等

  • 安装时添加--ignore-scripts命令跳过Chromium的下载 npm install puppeteer --ignore-scripts

  • 安装 puppeteer-core 这个包不会去下载Chromium


puppeteer 基本用法


先打开官方的入门demo


const puppeteer = require('puppeteer');

(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
await page.screenshot({path: 'example.png'});

await browser.close();
})();

上面这段代码就实现了网页截图,先大概解读一下上面几行代码:



  1. 先通过 puppeteer.launch() 创建一个浏览器实例 Browser 对象

  2. 然后通过 Browser 对象创建页面 Page 对象

  3. 然后 page.goto() 跳转到指定的页面

  4. 调用 page.screenshot() 对页面进行截图

  5. 关闭浏览器


是不是觉得好简单?


puppeteer.launch(options)


options 参数详解


参数名称参数类型参数说明
ignoreHTTPSErrorsboolean在请求的过程中是否忽略 Https 报错信息,默认为 false
headlessboolean是否以”无头”的模式运行 chrome, 也就是不显示 UI, 默认为 true
executablePathstring可执行文件的路劲,Puppeteer 默认是使用它自带的 chrome webdriver, 如果你想指定一个自己的 webdriver 路径,可以通过这个参数设置
slowMonumber使 Puppeteer 操作减速,单位是毫秒。如果你想看看 Puppeteer 的整个工作过程,这个参数将非常有用。
argsArray(String)传递给 chrome 实例的其他参数,比如你可以使用”–ash-host-window-bounds=1024x768” 来设置浏览器窗口大小。
handleSIGINTboolean是否允许通过进程信号控制 chrome 进程,也就是说是否可以使用 CTRL+C 关闭并退出浏览器.
timeoutnumber等待 Chrome 实例启动的最长时间。默认为30000(30秒)。如果传入 0 的话则不限制时间
dumpioboolean是否将浏览器进程stdout和stderr导入到process.stdout和process.stderr中。默认为false。
userDataDirstring设置用户数据目录,默认linux 是在 ~/.config 目录,window 默认在 C:\Users{USER}\AppData\Local\Google\Chrome\User Data, 其中 {USER} 代表当前登录的用户名
envObject指定对Chromium可见的环境变量。默认为process.env。
devtoolsboolean是否为每个选项卡自动打开DevTools面板, 这个选项只有当 headless 设置为 false 的时候有效

puppeteer如何使用



下面介绍 10 个关于使用 Puppeteer 的用例,并在介绍用例的时候会穿插的讲解一些 API,告诉大家如何使用 Puppeteer:



01 获取元素及操作


如何获取元素?



  • page.$('#uniqueId'):获取某个选择器对应的第一个元素

  • page.$$('div'):获取某个选择器对应的所有元素

  • page.$x('//img'):获取某个 xPath 对应的所有元素

  • page.waitForXPath('//img'):等待某个 xPath 对应的元素出现

  • page.waitForSelector('#uniqueId'):等待某个选择器对应的元素出现



Page.$(selector) 获取单个元素,底层是调用的是 document.querySelector() , 所以选择器的 selector 格式遵循 css 选择器规范




Page.$$(selector) 获取一组元素,底层调用的是 document.querySelectorAll(). 返回 Promise(Array(ElemetHandle)) 元素数组.




const puppeteer = require('puppeteer');

async function run (){
const browser = await puppeteer.launch({headless:false,defaultViewport:{width:1366,height:768}});
const page = await browser.newPage();
await page.goto('https://www.baidu.com');
const input_area = await page.$("#kw");
await input_area.type("Hello Wrold");

const search_btn = await page.$('#su');
await search_btn.click();

}

run();

02 获取元素属性


Puppeteer 获取元素属性跟我们平时写前段的js的逻辑有点不一样,按照通常的逻辑,应该是现获取元素,然后在获取元素的属性。但是上面我们知道 获取元素的 API 最终返回的都是 ElemetHandle 对象,而你去查看 ElemetHandle 的 API 你会发现,它并没有获取元素属性的 API.


事实上 Puppeteer 专门提供了一套获取属性的 API, Page.$eval() 和 Page.$$eval()


Page.$$eval(selector, pageFunction[, …args]), 获取单个元素的属性,这里的选择器 selector 跟上面 Page.$(selector) 是一样的。


const value = await page.$eval('input[name=search]', input => input.value);
const href = await page.$eval('#a", ele => ele.href);
const content = await page.$eval('
.content', ele => ele.outerHTML);

const puppeteer = require('puppeteer');

async function run (){
const browser = await puppeteer.launch({headless:false,defaultViewport:{width:1366,height:768}});
const page = await browser.newPage();
await page.goto('https://www.baidu.com');
const input_area = await page.$("#kw");
await input_area.type("Hello Wrold");

const search_btn = await page.$('#su');
await search_btn.click();

await page.waitFor('div#content_left > div.result-op.c-container.xpath-log',{visible:true});

let resultText = await page.$eval('div#content_left > div.result-op.c-container.xpath-log',ele=> ele.innerText)
console.log("result Text= ",resultText);



}

run();

03 处理多个元素




const puppeteer = require('puppeteer');

async function run() {
const browser = await puppeteer.launch({
headless: false,
defaultViewport: {
width: 1280,
height: 800,
},
slowMo: 200,
});
const page = await browser.newPage();
await page.goto('https://www.baidu.com');
const input_area = await page.$('#kw');
await input_area.type('Hello Wrold');
await page.keyboard.press('Enter');
const listSelector = 'div#content_left > div.result-op.c-container.xpath-log';
// await page.waitForSelector(listSelector);
await page.waitFor(3 * 1000);

const list = await page.$$eval(listSelector, (eles) =>
eles.map((ele) => ele.innerText)
);
console.log('List ==', list);
}

run();


04 切换frame


一个 Frame 包含了一个执行上下文(Execution Context),我们不能跨 Frame 执行函数,一个页面中可以有多个 Frame,主要是通过 iframe 标签嵌入的生成的。其中在页面上的大部分函数其实是 page.mainFrame().xx 的一个简写,Frame 是树状结构,我们可以通过page.frames()获取到页面所有的 Frame,如果想在其它 Frame 中执行函数必须获取到对应的 Frame 才能进行相应的处理



const puppeteer = require('puppeteer')

async function anjuke(){
const browser = await puppeteer.launch({headless:false});
const page = await browser.newPage();
await page.goto('https://login.anjuke.com/login/form');

// 切换iframe

await page.frames().map(frame => {console.log(frame.url())})
const targetFrameUrl = 'https://login.anjuke.com/login/iframeform'
const frame = await page.frames().find(frame => frame.url().includes(targetFrameUrl));

const phone= await frame.waitForSelector('#phoneIpt')
await phone.type("13122022388")
}

anjuke();

05 拖拽验证码操作


const puppeteer = require('puppeteer')

async function aliyun(){
const browser = await puppeteer.launch({headless:false,ignoreDefaultArgs:['--enable-automation']});
const page = await browser.newPage();
await page.goto('https://account.aliyun.com/register/register.htm',{waitUntil:"networkidle2"});

const frame = await page.frames().find(frame=>{
console.log(frame.url())
return frame.url().includes('https://passport.aliyun.com/member/reg/fast/fast_reg.htm')

})

const span = await frame.waitForSelector('#nc_1_n1z');
const spaninfo = await span.boundingBox();
console.log('spaninfo',spaninfo)

await page.mouse.move(spaninfo.x,spaninfo.y);
await page.mouse.down();

const div = await frame.waitForSelector('div#nc_1__scale_text > span.nc-lang-cnt');
const divinfo = await div.boundingBox();

console.log('divinfo',divinfo)
for(var i=0;i<divinfo.width;i++){
await page.mouse.move(spaninfo.x+i,spaninfo.y);
}
await page.mouse.up();
}

aliyun();


06 模拟不同设备


const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
const iPhone = devices['iPhone 6'];

puppeteer.launch().then(async browser => {
const page = await browser.newPage();
await page.emulate(iPhone);
await page.goto('https://www.baidu.com');
// 其他操作...
await browser.close();
});

07 请求拦截



const puppeteer = require('puppeteer');
async function run () {
const browser = await puppeteer.launch({
headless:false,
defaultViewport:{
width:1280,
height:800
}
})
const page = await browser.newPage();
await page.setRequestInterception(true);
page.on('request', interceptedRequest => {
const blockTypes = new Set(['image', 'media', 'font']);
const type = interceptedRequest.resourceType();
const shouldBlock = blockTypes.has(type);
if (shouldBlock) {
interceptedRequest.abort();
} else {
interceptedRequest.continue();
}

});
await page.goto('https://t.zhongan.com/group');
}

run();

08 性能分析



const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.tracing.start({path: 'trace.json'});
await page.goto('https://t.zhongan.com/group');
await page.tracing.stop();
browser.close();
})();

09 生成pdf



const URL = 'http://es6.ruanyifeng.com';
const puppeteer = require('puppeteer');
const fs = require('fs');

fs.mkdirSync('es6-pdf');

(async () => {
let browser = await puppeteer.launch();
let page = await browser.newPage();
await page.goto(URL);
await page.waitFor(5000); // 等待五秒,确保页面加载完毕

// 获取左侧导航的所有链接地址及名字
let aTags = await page.evaluate(() => {
let eleArr = [...document.querySelectorAll('#sidebar ol li a')];
return eleArr.map((a) =>{
return {
href: a.href.trim(),
name: a.text
}
});
});

// 先将本页保存成pdf,并关闭本页
console.log('正在保存:0.' + aTags[0].name);
await page.pdf({path: `./es6-pdf/0.${aTags[0].name}.pdf`});

// 遍历节点数组,逐个打开并保存 (此处不再打印第一页)
for (let i = 1, len = aTags.length; i < len; i++) {
let a = aTags[i];
console.log('正在保存:' + i + '.' + a.name);
page = await browser.newPage();
await page.goto(a.href);
await page.waitFor(5000);
await page.pdf({path: `./es6-pdf/${i + '.' + a.name}.pdf`});
}
browser.close();
})


10 自动化发布微博


const puppeteer = require('puppeteer');
const {username,password} = require('./config')

async function run(){
const browser = await puppeteer.launch({
headless:false,
defaultViewport:{width:1200,height:700},
ignoreDefaultArgs:['--enable-automation'],
slowMo:200,
args:['--window-size=1200,700']})
const page = await browser.newPage();

await page.goto('http://wufazhuce.com/',{waitUntil:'networkidle2'});
const OneText = await page.$eval('div.fp-one-cita > a',ele=>ele.innerText);
console.log('OneText:',OneText);


await page.goto('https://weibo.com/',{waitUntil:'networkidle2'});
await page.waitFor(2*1000);
await page.reload();

const loginUserInput = await page.waitForSelector('input#loginname');
await loginUserInput.click();
await loginUserInput.type(username);

const loginUserPasswdInput = await page.waitForSelector('input[type="password"]');
await loginUserPasswdInput.click();
await loginUserPasswdInput.type(password);

const loginBtn = await page.waitForSelector('a[action-type="btn_submit"]')
await loginBtn.click();

const textarea = await page.waitForSelector('textarea[class="W_input"]')
await textarea.click();
await textarea.type(OneText);

const sendBtn = await page.waitForSelector('a[node-type="submit"]');
await sendBtn.click();
}

run();

CSDN的脚本


image.png



这里注意CSDN有反扒机制,规则自己琢磨就行,我贴了伪代码,核心代码就不开放,毕竟自己玩玩就行了



const puppeteer = require('puppeteer');

async function autoCommentCSDN(username, password, targetBlogger, commentContent) {
const browser = await puppeteer.launch({ headless: false }); // 打开有头浏览器
const page = await browser.newPage();

// 登录CSDN
await page.goto('https://passport.csdn.net/login');
await page.waitForTimeout(1000); // 等待页面加载

// 切换到最后一个Tab (账号登录)
// 点击“密码登录”
const passwordLoginButton = await page.waitForXPath('//span[contains(text(), "密码登录")]');
await passwordLoginButton.click();


// 输入用户名和密码并登录
const inputFields = await page.$$('.base-input-text');

await inputFields[0].type( username);
await inputFields[1].type( password);
await page.click('.base-button');

await page.waitForNavigation();

// // 跳转到博主的首页
await page.goto(`https://blog.csdn.net/${targetBlogger}?type=blog`);

// // 点击第一篇文章的标题,进入文章页面
await page.waitForSelector('.list-box-cont', { visible: true });
await page.click('.list-box-cont');
// // 获取文章ID
console.log('page.url()',page.url())

// await page.waitForTimeout(1000); // 等待页面加载


await page.goto('https://blog.csdn.net/weixin_52898349/article/details/132115618')


await page.waitForTimeout(1000); // 等待页面加载

console.log('开始点击评论按钮...')

console.log('page.url()',page.url())

// 获取当前页面的DOM内容

const bodyHTML = await page.evaluate(() => {
return document.body.innerHTML;
});

console.log(bodyHTML);

// await page.waitForSelector('.comment-side-tit');

// const commentInput = await page.$('.comment-side-tit');

// await commentInput.click();

// 等待评论按钮出现

// 点击评论按钮

// await page.waitForSelector('.comment-content');
// const commentInput = await page.$('.comment-content textarea');
// await commentInput.type(commentContent);
// const submitButton = await page.$('.btn-comment-input');
// await submitButton.click();

// console.log('评论成功!');
// await browser.close();

}

// 请替换以下参数为您的CSDN账号信息、目标博主和评论内容
const username = 'weixin_52898349';
const password = 'xxx!';
const targetBlogger = 'weixin_52898349'; // 目标博主的CSDN用户名
const commentContent = '各位大佬们帮忙三连一下,非常感谢!!!!!!!!!!!'; // 评论内容

autoCommentCSDN(username, password, targetBlogger, commentContent);




作者:linwu
来源:juejin.cn/post/7263871284010106938
收起阅读 »

清朝项目太臭怎么办?TS重构它!

web
多图预警,流量党慎入 图片:均图片在上,描述在下 全文:6474字 阅读需要约28分钟 项目背景 最近公司要求给一个老项目加功能,具体就是把原来免费的服务改成付费的,然后再加一点其他的功能,我之前看到过那个项目的线上,这么一说,顿感不妙。截个图简单感受一下:...
继续阅读 »

多图预警,流量党慎入


图片:均图片在上,描述在下


全文:6474字 阅读需要约28分钟



项目背景


最近公司要求给一个老项目加功能,具体就是把原来免费的服务改成付费的,然后再加一点其他的功能,我之前看到过那个项目的线上,这么一说,顿感不妙。截个图简单感受一下:


image.png


农业数字云页面


image-20230804132617537


告警平台页


image.png
天气监测页面(navBar直接没了,我天😅)


image-20230804132711975


气象大数据页,好家伙直接空壳页面,也是没有navBar了,好像进入了一个全新的大屏项目。


报错什么的咱就不说了,光log信息就看的眼花缭乱,点几下给你log十几行出来,你说log就log吧,把名字取好也行,不过各位,看图😅。


好好好,虽然线上看起来没那么漂亮,可能人家代码写的工整呢?


那咱接下来看看代码,我草!等会jym,咱先不看代码,光这目录结构就把俺老猪吓一跳:


image-20230804133247244


image-20230804134709932


image-20230804133320761


image-20230804133541655


牛蛙牛蛙,今天算开眼了,根目录下命名个entries,好家伙下面还有components、views、App.vue(这是把另一个项目整个搬过来了是吧😅),根目录下同时拥有:utils、tools、service,好家伙,直接跟俺摆迷魂阵!更离谱的是,view下面还有view,view下的文件夹还有components,组件和页面完全分不清啊!敢问掘友们见过这种史诗级屎目录吗?


被目录结构震惊之后,先平复一下心情,看一下代码,代码写的还是挺工整的


做好准备,图来了:


先拿个vue组件看看:


image-20230804134138764


(那个分页函数是我后来写的)嗯~中规中矩,我还可以接受,毕竟命名什么的还算规范,看个十几分钟就没问题了,继续看个组件


image-20230804134418622


啊~行,配合模板部分还是能看懂的,继续来:


image-20230804134856519


啊?我可以接受两个函数长得差不多,但是功能应该也差不多吧?你这addControl2里面的功能和addControl完全不一样啊!!一个是绑定控制地图时的监听回调,一个是添加地图控件,我承认这两种确实都能称之为 Control...但是


vue组件写的还都可以接受


image-20230804135816345


接口封装也行,命名和注释都有


image-20230804140404214


这哥们自己用正则写了个时间格式化函数,我只能说很强,但是没必要。


好吧,看到这,虽然每一项都还算过得去,但是如果让我维护这样的项目,实在是太累了(我截图不是个例各位,每个文件中都有类似的)。改了一周后,我申请了重构,没想到当即被领导拍板,重构吧,交给你了。


敲定技术栈


因为旧的项目变量和全局变量、路由传参、mixin混用,导致页面内的变量来源难以追踪,很多时候知道是这个值引起的问题,但是就是找不到这个值哪来(可能被路由传了好多级才过来)。由此,我准备用 ts + pinia 做类型和状态管理。


旧项目(webpack)每次冷启动或打包都要花费 20s 左右的时间,热更新在 1 - 2s之间还行。由于这个网站一直在使用阿里云云效在线部署,所以我也没特意看打包后的大小,但是估计要在几十上下,其实这些问题都不大,主要优化一些静态文件和无用的代码就好了。但是我还是因此选择了使用 vite


因为旧项目使用了vue2、mixin和vuex和vue-router导致 变量污染问题严重 。为了解决这个问题,除了上面提到的 ts + pinia ,还选用了vue3,因为vue3天然不支持mixin,使用组合式API很容易可以更清晰的实现mixin可以做到的所有功能。


在新项目中,使用vue-router时,只作为备用方案,因为在重构时进行了新一轮的需求整理,直接将告警平台移除,后面两个页面的核心功能合并到农业数字云,在此场景下,新版的农业数字云只需要一个路由就可以承载所有的功能了。并且,使用了pinia-plugin-persistedstate插件对pinia做了全局的持久化,这里要说明一点,很多人觉得在本地存储太多数据不太好,尤其是像我这样一股脑的全部持久化,其实,在localStorage允许的情况下,把一些常用数据甚至是状态信息存储在本地,是效率最高也是性能最好的方式。


最终技术栈为:vue3 + pinia + vue-router + ts


开干


概述


因为是旧的项目,所以接口基本都全,整理出来的新需求,后端大哥跟我一起做,速度很快,我们第一周就已经完成了整个项目的99%。


由于重构系统并没有出新的设计图,导致我页面端两眼一抹黑,只能靠模仿旧的大致样式来做(因为和旧版逻辑架构完全不一样了,所以大部分还是重做的),最终做出了一个更清爽的前端页面:


image-20230804143240923


image-20230804143358520


登录页也不是一个单独的页面了,几乎所有的功能都改成了轻量级的弹窗。


这个项目完整的展开后是这种样子:


image-20230804143517623


对比旧的


image-20230804143615322


重构和优化


页面变化


可以看到明显变化的地方有:



  1. 新增了行政区域级地块

  2. 不再对播种、未播种和样板田分类,而是使用tag徽标

  3. 不再对每个地块单独展示服务项,而是选中地块时使用同一个区域展示

  4. 移除了信息重复的图表,移除了冗余的图例、地图控件、农事记录

  5. 灾害信息亦作为服务项、不再单独列出

  6. 新增了时间轴替代了原本的轮播图


代码变化


不明显的变化在于:


移除所有生产环境的log

image-20230804144029382


控制台干净到一尘不染,生产环境的log全部被移除了,且使用ts编写,运行阶段基本不会出错,使用了autofit.js,一行轻松适配设计稿下任何分辨率(就是强无敌)


移除所有第三方大型UI组件库

image-20230804144258724


未使用任何大型UI组件库(我管你能不能树摇),这也使得该项目体积被压缩到了极限,打包后仅有 1M+


清晰代码结构

image-20230804144439678


代码结构清晰,现在内容比较少,但不难看出结构还是比较规范的,基本沿用了vite创建的默认结构,且静态文件基本没有,这也是保证打包体积的重要因素。


使用pinia持久化存储

image-20230804144806244


因为全篇使用了 pinia 持久化存储,开发时不用关心数据什么时候get什么时候set了,随时使用store,即可得到、设置数据,极大的提高了开发幸福感。


接口分类封装

image-20230804145117239


接口分类封装,调用时可以清楚的知道在调用哪个厂商的接口,并且有ts加持,绝不会少一个、错一个参数。


简化拦截器

image-20230804145317150


极简拦截器,拦截器本质上就是一个错误截获器,只需要保证后续流程不崩溃就可以了,所以这里只做了最简单的拦截器,非常好用。


简化路由、使用组件

image-20230804145538621


不出所料的,这个项目没有用到路由切换,所有的内部功能都以轻量化弹窗的形式展示,唯一需要跳转的是支付页面,但那是另一个项目,我们的解决方案是直接带着用户token跳转,简单粗暴。


基于mqtt理念的发布订阅范式

image-20230804145855326


我选择完全相信vue3的watch,以这种方式编码乍一看会很难阅读,但是实际上是仿照了mqtt的消息订阅机制,把watch当成一个subscribe,把store.xxx当成一个topic,你会发现这种写法再好理解不过了,并且这种做法很爽,你如果需要订阅一个数据的变化而做出一些操作的话,就写watch就好了。


清除冗余代码

image-20230804150605361


没有冗余代码,没有!函数、变量命名清晰规范,主打的就是一个清晰,你看我这段代码需要注释吗?


编码规范

image-20230804150758323


规范、规范、还是他妈的规范,这就是规范的节流器,都跟着写!


使用ts定义类型

image-20230804150944425


明确的类型,定义类型可能多花五分钟,但是会在编码时节省一小时。


开发轻量级必要组件

image-20230804151157258


自定义selecter、toast等组件,不需要的一点不写,主打的就是一个轻量。


打包


整个开发流程下来,打包体积达到了惊人的1.94M(少林功夫好耶,太棒辽啊🥳),线上运行表现基本令人满意(少林功夫,够劲!顶呱呱呀🥳)


image.png


完事


虽然在第一周改完99%后,又加了大大小小的新需求和新改动,不过在规范的开发面前都迎刃而解,编码成了一种享受,对于这种轻量级项目,就要用轻量级的方法去实现,搞得笨重的像一头装甲熊(没说沃利贝尔),没有任何意义,关于这次重构,除了技术上的学习,还有理念上的进步,往往一个前端项目,只需要保证页面不卡顿、不报错、不崩溃就行了,不要剪了芝麻丢了西瓜、头重脚轻、舍本逐末、南辕北辙。我是德莱厄斯,共勉。


作者:德莱厄斯
来源:juejin.cn/post/7263315523537928250
收起阅读 »

近年来项目研发之怪现状

简述 近年来,机缘巧合之下接触了不少toG类项目。项目上颇多事情,令人疑惑频频。然而屡次沟通,却都不了了之,长此以往,心力愈发交瘁,终究心灰意冷,再无劝谏之心。 令人困惑的项目经理 孟子说天时不如地利,地利不如人和。而项目上遇到的很多事情,天时、地利终为...
继续阅读 »

简述



近年来,机缘巧合之下接触了不少toG类项目。项目上颇多事情,令人疑惑频频。然而屡次沟通,却都不了了之,长此以往,心力愈发交瘁,终究心灰意冷,再无劝谏之心。



令人困惑的项目经理



孟子说天时不如地利,地利不如人和。而项目上遇到的很多事情,天时、地利终为少数,多数在人和。



立项开工,项目经理自然是项目上的第一把手。既为第一把手,自要有调兵遣将,排兵布阵的能耐。


当然用我们业内的话来说,可分为下面几类:


第一等的自然是懂业务又懂技术,这样的项目经理可运筹帷幄之中,决胜千里之外,当然这般的项目经理可遇而不可求。


这第二等的懂业务不懂技术,或者懂技术不懂业务,这样的项目经理,辅以数名参将,只要不瞎指挥,也可稳扎稳打,有功无过。


第三等的项目经理,业务与技术皆是不懂,如这般的项目经理,若尽职尽责,配先锋、军师、参将、辎重,最好再辅之以亲信,也可功成身退。若其是领导亲信,那更可说是有惊无险了。


而这第四等的,业务与技术不懂也就罢了,既无调兵遣将之才,又无同甘共苦之心,更是贻误战机,上下推诿。若其独断专横,那便是孔明在世也捧不起来。



有这般一个项目,公司未设需求经理,常以项目经理沟通需求。工期八月,立项后,多次催促,却不与甲方沟通,以至硬生生拖了两月之后才去。然而不通业务,不明技术。甲方被生耗两个月才沟通需求,这样的情况下,如何能顺利进行,以至于项目返工现象,比比皆是。多次提及需求管理,亦是左耳进右耳出。类类数落问题,甲方、研发、产品都有问题,独独他自身若皎皎之明月,灿灿之莲花。然而纵是项目成员承星履草,夜以继日,交付一版之后。举目皆是项目经理之间的恭维之词。



我有很多朋友是优秀的项目经理。言必行,行必果。沟通起来非常愉悦。偶尔遇到一个这样的人,确实让我大开眼界。


其实我也想过,这并非是项目经理职位的问题,实在是个别人自身的问题,这样的人,在任何岗位都是令人恼火的。


技术人员的无力感


我们互联网从业者经常听到一个词,技术债。技术债是令人难受的,尤其是在做项目的时候。做产品,我们可以制定完善的迭代周期,而项目,当需求都无法把控的时候,那么就意味着一切都是可变的。


糟糕的事情是,我遇到了这样的项目。前期无法明确的需求,项目期间,子虚乌有的需求管理,项目中不断的需求变更,deadline的不断临近,最终造就了代码的无法维护。


我从未想过,在同一个紧迫的时间阶段,让研发进行需求研发、bug修复、代码解耦,仿佛每一件事情都很重要。当然,我更建议提桶跑路,这样的项目管理,完全是忽视客观现实的主观意识。


前端规范难落地


公司是有前端规范的,然而前端规范的落地却很糟糕。如果使用TS,那么对于诸多时间紧,任务重,且只有一名前端开发人员的项目来说,显得太过冗余了。所以依旧使用js,那么代码中单个性化不会少见。使用esLint怎么样呢?这当然很棒,直到你发现大部分成员直接将esLint的检查注释了。或许还可以依靠团队内不断的宣讲与code Review,这是个好主意,然而你会发现,公司的code Review也是那么形式化的过程。


或许对一些企业来说,代码的规范性不重要,所谓的技术类的东西都显得没那么重要。只有政府将钱塞到它的口袋里这件事,很重要。


崩盘的时间管理


那么,因为各方面的原因,项目不可避免的走向了失控。时间管理的崩溃,项目自然开始了不断的延期。在私下里,一些擅长酒桌文化的甲方与项目经理,开始了酒桌上的攀谈,推杯换盏之间,开始了走形式的探讨。灯红酒绿之间,公司又开始了例行的恭维。


当然,我依旧无法理解,即使管理的如此糟糕,只要在酒桌上称兄道弟,那便什么问题都没有了?若是如此,项目经理面试的第一道题,一定是酒量如何了。

作者:卷不动咯
来源:juejin.cn/post/7263372536791433275

收起阅读 »

即时通讯 IM 永久免费版+高额赠费,环信发布「着陆」计划!

环信一直致力于提供稳定、安全、易用的即时通讯云服务,10年来积累了40余万开发者,我们提供了永久免费版、专业版、旗舰版等版本。如果您希望从第三方切换接入环信的 IM 服务,欢迎了解并加入环信「着陆」计划,我们为所有用户(包括免费版)提供全套迁移方案及文档,高额...
继续阅读 »

环信一直致力于提供稳定、安全、易用的即时通讯云服务,10年来积累了40余万开发者,我们提供了永久免费版、专业版、旗舰版等版本。如果您希望从第三方切换接入环信的 IM 服务,欢迎了解并加入环信「着陆」计划,我们为所有用户(包括免费版)提供全套迁移方案及文档,高额赠费,同时环信技术支持团队将免费7*12小时为您的迁移工程保驾护航。

环信「着陆」计划适用客户




「着陆」计划方案概述


环信「着陆」计划提供如下完善的迁移方案、迁移服务保障、业务政策,这套方案经受过拥有「数千万级用户」和「数亿级消息量」的客户项目验证,迁移过程敏捷、平稳、安全,已经成功为多家客户实现了系统迁移。

迁移方案分为【一次性迁移方案】和【平滑迁移方案】,两个方案均可以达到从第三方IM厂商迁移到环信 IM 系统的能力。


【一次性迁移方案】


适用于新应用上架,可以强制所有的老应用升级至新应用,此方式下不存在新老应用的兼容问题。
优势:集成时间短、工作量小、维护一套系统;
劣势:有可能损失用户;
角度:需要客户判断,让用户强制更新APP是否会导致用户卸载不更新或者使用环境无法更新的情况,如果几率小可以采用该方案。

【平滑迁移方案】


在迁移过程中,环信 IM 服务器和原 IM 服务器同时提供服务,新应用和旧应用并存,支持新旧应用互通。待用户逐步更新至新应用,原 IM 服务器停止服务。
优势:不损失用户,客户无感知
劣势:前期需要客户维护两套系统,工作量多几个环节。
角度:需要客户判断,如果需要新老用户同时并存,保证用户的体验和避免损失,建议采用平滑迁移方案。


项目迁移整体进程


迁移环节分为5步,根据业务复杂度不同预计2周~4周完成:



迁移服务保障

为保障每一位客户平稳有序迁移至环信系统,「着陆」计划提供以下全程免费的迁移服务保障,协助客户做好数据快速迁移工作,实现新老系统的快速切换和上线。

1. 详尽的《环信IM平滑迁移手册》
该手册从迁移准备、环境搭建、迁移步骤、数据导入、消息格式转换、服务端集成等流程均有详细文档说明。

2. 迁移解决方案讲解及建议
根据客户具体的业务场景,制定系统迁移解决方案,环信解决方案架构师将提供具体的方案讲解、培训及建议。

3. 搭建中转服务器方案指导
根据客户具体场景,提供专业的服务端解决方案,指导协助客户搭建中转服务器等。

4、全程免费技术支持
提供全程即时沟通渠道,创建1vs1技术支持群,7x24h提供免费技术咨询,高效解决问题。


迁移福利政策


从其他三方平台迁移至环信 IM 系统的客户,均享有以下业务政策





项目迁移案例

某垂直招聘领域客户,拥有6000万注册用户、300万日活的招工大数据平台,最初使用友商 IM作为Android、iOS、20多个小程序平台的沟通通道,后期因为IM系统稳定性等多种原因迁移到环信。

采用环信VIP集群,确保系统的安全、稳定和资源冗余能力

项目迁移从6月初开始接触,经过1个月的方案论证于7月初开始进行集成,先后完成了三个客户端SDK+服务端SDK集成、中转服务器的开发等工作,并于7月底完成原始数据的迁移。待测试完成后,在8月份分批开放小程序、iOS端和Android上线。

项目期间,环信除了提供标准迁移接口,同时还为该客户单独开发了一些接口,用于满足特殊业务场景的需求。

迁移完成后,线上业务稳定有序,项目圆满交付。


加入「着陆」计划


环信秉承以客户为本,不负每一位客户的信赖与支持,诚邀您加入「着陆」计划,与环信携手并进,共铸辉煌!如果你的项目正在或可能要涉及到 IM 系统迁移,或想了解《环信IM平滑迁移手册》详细说明,欢迎通过以下联系方式与我们聊聊。



业务联系电话:400-622-1776

快速注册环信:https://console.easemob.com/user/register

了解环信 IM:https://www.easemob.com/product/im

环信「着陆」计划:https://www.easemob.com/event/landing


收起阅读 »

恋人没在身边?这些小动作出轨几率竟高达76%!

“前几天我突然注意到,我老公最近总是在深夜加班,有时候甚至一周加班3次以上,快12点才下班回家。你说,他是不是在外面有了新欢?”我的好闺蜜李芳跟我吐露,一脸失落。 常与异性同事出去应酬 “上周我老公又说要和一个新来的女同事叫林萍一起出去谈项目。我...
继续阅读 »

“前几天我突然注意到,我老公最近总是在深夜加班,有时候甚至一周加班3次以上,快12点才下班回家。你说,他是不是在外面有了新欢?”我的好闺蜜李芳跟我吐露,一脸失落。



常与异性同事出去应酬



“上周我老公又说要和一个新来的女同事叫林萍一起出去谈项目。我提出一起去,他还拒绝了。他们公司这些应酬我都不放心,谁知道喝多了会发生什么。”



李芳愤愤不平地说。


其实,我理解李芳的担心。经常与异性同事单独出去应酬,的确很容易引起歧义。但作为老公,他也有自己的社交圈子。如果李芳表达疑虑后,老公还能体贴她的感受,不再频繁应酬就最好不过了。但如果老公不改,李芳也要学会给他合理的空间,不要过分限制,免得适得其反。


主动帮异性同事搬重物



“还有一次,他们单位搬家,我老公就主动帮那个女同事林萍一起搬箱子。也不知道他们在搬箱子的时候聊了些什么,搬完还笑得那么开心。”



说到这里,李芳红了眼眶,泪水在眼眶里打转。


帮忙搬东西本身无可厚非,但过分殷勤确实也容易引发类似误会。作为朋友,我建议李芳不要先入为主,与老公沟通后再下判断。如果仅是顺手帮同事的举动,也不必过分猜疑。


经常参加异性同事的生日party



“上个月那个女同事林萍生日,我老公还特意去参加了她的生日party,送了礼物。他居然都不跟我提前说一声,我要不是看到他朋友圈,都不知道这件事。”



李芳愤愤道。


参加同事生日party本是正常的社交活动,但老公不告知李芳确实有些过分。作为朋友,我劝李芳要学会委婉地表达自己的感受,让老公理解你的想法,而不是一味地指责。相处之道需要双方不断磨合,这也需要时间。


过于关注异性同事的社交动态



“我还发现我老公最近老是看那个林萍的朋友圈,所有的照片都点赞。我跟他提这事,人家说只是礼貌性赞一下,又不是什么大不了的事。太不正常了!我都开始怀疑他俩之间有事了。”



说到这,李芳已经禁不住泪水盈眶。


关注同事动态固然正常,但过分频繁的点赞确实也容易引起误会。不过我认为李芳不应仅凭此就随意猜疑,更好的方法是与老公坦诚交流这个问题,给老公一个解释的机会,也让老公注意到李芳内心的不安,从而做出调整。


......


经过与李芳的交流,我理解她的疑虑和不安。但与其胡思乱想导致误会,不如与老公好好沟通,让他知道你的想法,给他一个解释的机会。只有双方互相理解信任,婚姻才能长久。相信李芳也能做出正确的选择,与老公建立平等和睦的关系。


作者:wuxuan0208
来源:mdnice.com/writing/93d11978e60f43f698ffe292bd2e6f61
收起阅读 »

成功人士早已悉知的20条道理

1.对抗内卷的最好方法是找到自己独特的生态位,也就是自己的独特性,不可替代性 2.人对自己失去东西的恐惧远大于想要获得东西的欲望 3.我们喜欢那些认为我们是对的人和观点,我们讨厌那些认为我们是错的人和观点 4.能说会道不是一个好销售,投其所好才是 5....
继续阅读 »

1.对抗内卷的最好方法是找到自己独特的生态位,也就是自己的独特性,不可替代性


2.人对自己失去东西的恐惧远大于想要获得东西的欲望


3.我们喜欢那些认为我们是对的人和观点,我们讨厌那些认为我们是错的人和观点


4.能说会道不是一个好销售,投其所好才是


5.同一层面的问题不可能在同一层面得到解决,只有在高于他的层面才能解决


6.能力驱动成功,但是当能力无法被衡量时社会网络驱动成功


7.任何一家公司,最重要的三个因素就是人,钱,事


8.人在财务状况越糟糕的时候,赌性也就越强,长此以往恶性循环


9.由于大多数人都是不自信的,仅仅表现得很自信就已经超越了大多数人


10.只要比普通人忍受更多痛苦,总有普通人得不到的机会向你打开


11.天下没有卑微的工作,赚钱机会就在泥里面去找


12.认清自己的优势和弱势,信息差永远存在


13.这个世界永远有懒人,也就永远总有靠信息差赚钱


14.有街头智慧的人往往具有几个特质:察言观色,捕捉需求,极端务实,没有幻想,专注目标,忍受痛苦


15.任何的知识智慧和想法,如果没有变成结果,他都只是你的潜力,而不是你的实力


16.###### 任何事业干的出色的人,都会告诉你,脑力劳动最大的门槛其实就是 体力


17.那些最能把书本智慧和街头智慧结合起来的人,最后都具有极强的逆袭能力。


18.如何挖掘大城市的受益?建立更多链接,进入好公司,参加同行交流、建立跨行人脉


19.有三样东西可以让人的友情变得深厚,就是健康,财富,后代。可以从这三个方面拉近人与人的关系


20.在三十岁之前,大多数人用钱来赚钱的能力,远不如自己通过劳动来赚钱


作者:美人薇格
来源:mdnice.com/writing/98d59933de5749baa9c66a23d1b3fdd1
收起阅读 »

分享我使用两年的极简网页记事本

web
若无单独说明,按照文章代码块中命令的顺序,一条一条执行,即可实现目标。 适用系统:Debian 系发行版,包括 Ubuntu 和 Armbian,其他发行版按流程稍改命令一般也可。 走通预计时间:10 分钟(Docker) 可以访问这个实...
继续阅读 »

若无单独说明,按照文章代码块中命令的顺序,一条一条执行,即可实现目标。 适用系统:Debian 系发行版,包括 Ubuntu 和 Armbian,其他发行版按流程稍改命令一般也可。




走通预计时间:10 分钟(Docker)





可以访问这个实例: https://forward.vfly.app/index.php ,试试怎么样,公开使用的网页记事本。


minimalist-web-notepad



image.png

image.png



这个网页记事本是我 2 年前玩机子初期的一大驱动力。当时主要从手机上浏览信息,刚转变到在电脑上处理信息,需要一种简便的渠道在两者之间传递文本、网址。




网盘太重,微信需要验证,tg 很好,但在找到这个记事本后,都是乐色,这就是最好的全平台传递文本的工具。



极简网页 记事本,是一个使用浏览器访问的轻量好用的记事本,专注于文本记录。


Github:pereorga/minimalist-web-notepad: Minimalist Web Notepad (github.com)




使用方法





  1. 访问网页: https://forward.vfly.app/index.php



  2. 它会随机分配 5 个字符组成的地址,如 https://forward.vfly.app/5b79m ,如果想指定地址,只需要访问时手动修改,如 https://forward.vfly.app/this_is_a_path 。下面以 5b79m 为例。



  3. 在上面编辑文本



  4. 等待一会(几秒,取决于延迟),服务端就会存储网页内容到名为 5b79m 的文件里。



  5. 关闭网页,如果关闭太快,会来不及保存,丢失编辑。



  6. 在其他平台再访问同样的网址,就能剪切内容了 ٩۹(๑•̀ω•́ ๑)۶



只要不关闭过快和在两个网页同时编辑,它都能很好地工作。因为极简,项目作者不会考虑增加多余功能。




webnote-in-phone_compressed.webp

webnote-in-phone_compressed.webp


在远控其他电脑时,用这个先传递命令,在目标电脑上使用,非常方便,而且适应性强。多个手机之间也一样。或者用于临时传送敏感数据,避免受到平台审查。


使用 Docker 安装网页 记事本


GitHub: pereorga/minimalist-web-notepad at docker (github.com)


全复制并执行,一键创建工作目录并开放端口


myserve="webnote"
sudo ufw allow 8088/tcp comment $myserve && sudo ufw reload
cd ~/myserve/
wget https://github.com/pereorga/minimalist-web-notepad/archive/refs/heads/docker.zip
unzip docker.zip && mv minimalist-web-notepad-docker webnote
cd webnote

根据注释自定义,然后执行,一键创建 docker-compose.yml 文件


cat > docker-compose.yml << EOF
---

version: "2.4"
services:
  minimalist-web-notepad:
    build: .
    container_name: webnote
    restart: always
    ports:
     - "8088:80"
    volumes:
     - ./_tmp:/var/www/html/_tmp
EOF

前面的 5b79m 就存储在 _tmp 中。


构建并启动容器(完成后就可以访问网页了,通过 http://ip_addr_or_domain:8088 访问。将 ip_addr_or_domain 替换为服务器的 IP 或域名)


docker compose up -d

Docker 版很久没更新了,有技术的可以参考博文中原生安装流程创建镜像。


迁移


数据都在 /var/www/webnote/_tmp 中,也就是 ~/myserve/webnote/_tmp,在新机子上重新部署一遍,复制这个目录到新机子上即可。




受限于篇幅,如果对原生安装(Apache + PHP)网页 记事本 感兴趣,请到我的博客浏览,链接在下面。



原文链接: https://blog.vfly2.com/2023/08/a-minimalist-web-notepad-used-for-two-years/ 版权声明:本博客所有文章除特別声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 承飞之咎 (blog.vfly2.com)


作者:AhFei
来源:mdnice.com/writing/611872e312654a22aa2472d71a6b3844
收起阅读 »

当我遇见了强制横屏签字的需求...

web
序言 人的一生就是进行尝试,尝试的越多,生活就越美好。——爱默生 在前一阶段的工作中,突然接到了这个需求:手写签批的页面在移动端竖屏时强制页面横屏展示进行签字,一开始我觉着只要将页面使用 CSS3 的 transform 进行 rotate 一下就可以了...
继续阅读 »

序言



人的一生就是进行尝试,尝试的越多,生活就越美好。——爱默生



在前一阶段的工作中,突然接到了这个需求:手写签批的页面在移动端竖屏时强制页面横屏展示进行签字,一开始我觉着只要将页面使用 CSS3 的 transform 进行 rotate 一下就可以了,但是当我尝试后发现并不是像我想象的那样简单。


vue2实现手写签批


在介绍横屏签字之前,我想先说明一下我实现签批使用的插件以及插件所调用的方法,这样在之后说到横屏签字的时候,大佬们不会感觉唐突。


vue-signature-pad


项目使用 vue-signature-pad 插件进行签名功能实现,强调一下如果使用vue2进行开发,安装的 vue-signature-pad 的版本我自测 2.0.5 是可以的


安装


npm i vue-signature-pad@2.0.5

引入


// main.js
import Vue from 'vue'
import App from './App.vue'

import VueSignaturePad from 'vue-signature-pad'

Vue.use(VueSignaturePad)

Vue.config.productionTip = false

new Vue({
render: (h) => h(App),
}).$mount('#app')

使用 vue-signature-pad 完成签批功能


这里我为了方便直接写了一个demo放到App.vue中,没有封装成组件


// app.vue
<template>
<div id="app">
<div style="background: #fff">
<vue-signature-pad
id="signature"
width="95%"
height="400px"
ref="signaturePad"
:options="options"
/>

</div>

<button @click="save">保存</button>
<button @click="resume">重置</button>
</div>

</template>

<script>
export default {
name: 'App',
data() {
return {
options: {
penColor: '#000',
},
}
},
methods: {
save() {
const { isEmpty, data } = this.$refs.signaturePad.saveSignature()
console.log(isEmpty)
console.log(data)
},

//清除重置
resume() {
this.$refs.signaturePad.clearSignature()
},
},
}
</script>


<style lang="scss">
html,
body {
padding: 0;
margin: 0;
}
#app {
width: 100vw;
height: 100vh;
background: #ececec;
}
</style>



代码比较通俗易懂,就是调用组件封装好的方法,保存后能够解构出data为base64编码的图片
Kapture 2023-07-28 at 10.27.49.gif
之后需要将base64编码格式转换成File文件格式的图片最后进行接口请求,那么转换方法如下展示👇🏻


<template>
<div id="app">
<div style="background: #fff">
<vue-signature-pad
id="signature"
width="95%"
height="300px"
ref="signaturePad"
:options="options"
/>

</div>

<div v-for="(item, index) in imgList" :key="index">
<img :src="item.src" alt="" width="100" />
</div>

<button @click="save" class="btn">保存</button>
<button @click="resume" class="btn">重置</button>
</div>

</template>

<script>
export default {
name: 'App',
data() {
return {
options: {
penColor: '#000',
},
imgList: [],
}
},
methods: {
save() {
const { isEmpty, data } = this.$refs.signaturePad.saveSignature()
this.imgList.push({
src: data,
})
let res = this.dataURLtoFile(data, 'demo')
console.log(res)
},

// 清除重置
resume() {
this.$refs.signaturePad.clearSignature()
},

// 将base64转换为文件
dataURLtoFile(dataurl, filename) {
var arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n)

while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}

return new File([u8arr], filename, { type: mime })
},
},
}
</script>


<style lang="scss">
html,
body {
padding: 0;
margin: 0;
}
#app {
width: 100vw;
height: 100vh;
background: #ececec;
}

.btn {
width: 35%;
color: #fff;
background: #5daaf3;
border: none;
height: 40px;
border-radius: 20px;
margin-top: 20px;
margin-left: 40px;
}
</style>


调用后,打印出转换成文件的图片如图


image.png
之后根据需求调用接口将文件图片作为入参即可。


阶段总结


经过上面的操作,我们就实现了前端的签批的完整流程,还是比较容易理解的。




新的需求


在实现这个功能不久之后,客户那边提出了新的需求:手机竖屏时将签字功能进行横屏展示。


错误思路


刚开始接到这个需求的时候,通过我所掌握的技术首先就是想到用CSS3的transform:rotate方法进行页面90deg的旋转,将签字组件也进行旋转之后进行签名;由于我对canvas不是很了解,所以我把包裹在签字组件外的div标签进行了旋转后签字发现落笔点位置错乱。


    <div style="background: #fff; transform: rotate(-90deg)">
<vue-signature-pad
id="signature"
width="95%"
height="300px"
ref="signaturePad"
:options="options"
/>

</div>


改变思路


既然不能旋转外层的div,那我想到一种欺骗方式:不旋转div,样式修改成与横屏样式相似,然后将生成的图片进行一个旋转,这样就ok了!那么我们的目标就明确了,找到能够旋转bas64编码的方法然后返回一个旋转后的base64图片在转换成file文件传递给后端问题就解决了。

经过一个苦苦寻找,终于找到了方法并实现了这个功能,话不多说,先撸为敬(样式大佬们自己改下,我这里展示下转换后的图片)。


<template>
<div id="app">
<div style="background: #fff">
<vue-signature-pad
id="signature"
width="95%"
height="300px"
ref="signaturePad"
:options="options"
/>

</div>

<div v-for="(item, index) in imgList" :key="index">
<img :src="item.src" alt="" width="100" />
</div>

<div class="buttons">
<button @click="save" class="btn">保存</button>
<button @click="resume" class="btn">重置</button>
</div>
</div>

</template>

<script>
export default {
name: 'App',
data() {
return {
options: {
penColor: '#000',
},
imgList: [],
fileList: [],
}
},
methods: {
save() {
const { isEmpty, data } = this.$refs.signaturePad.saveSignature()

this.rotateBase64Img(data, 90, (res) => {
console.log(res) // 旋转后的base64图片src
this.fileList.push({
file: this.dataURLtoFile(res, 'sign'),
name: 'sign',
})
this.imgList.push({
src: res,
})
})
},

// 清除重置
resume() {
this.$refs.signaturePad.clearSignature()
},

// 将base64转换为文件
dataURLtoFile(dataurl, filename) {
var arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n)

while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}

return new File([u8arr], filename, { type: mime })
},

// 通过canvas旋转图片
rotateBase64Img(src, edg, callback) {
var canvas = document.createElement('canvas')
var ctx = canvas.getContext('2d')

var imgW //图片宽度
var imgH //图片高度
var size //canvas初始大小

if (edg % 90 != 0) {
console.error('旋转角度必须是90的倍数!')
throw '旋转角度必须是90的倍数!'
}
edg < 0 && (edg = (edg % 360) + 360)
const quadrant = (edg / 90) % 4 //旋转象限
const cutCoor = { sx: 0, sy: 0, ex: 0, ey: 0 } //裁剪坐标

var image = new Image()

image.crossOrigin = 'anonymous'
image.src = src

image.onload = function () {
imgW = image.width
imgH = image.height
size = imgW > imgH ? imgW : imgH

canvas.width = size * 2
canvas.height = size * 2
switch (quadrant) {
case 0:
cutCoor.sx = size
cutCoor.sy = size
cutCoor.ex = size + imgW
cutCoor.ey = size + imgH
break
case 1:
cutCoor.sx = size - imgH
cutCoor.sy = size
cutCoor.ex = size
cutCoor.ey = size + imgW
break
case 2:
cutCoor.sx = size - imgW
cutCoor.sy = size - imgH
cutCoor.ex = size
cutCoor.ey = size
break
case 3:
cutCoor.sx = size
cutCoor.sy = size - imgW
cutCoor.ex = size + imgH
cutCoor.ey = size + imgW
break
}

ctx.translate(size, size)
ctx.rotate((edg * Math.PI) / 180)
ctx.drawImage(image, 0, 0)

var imgData = ctx.getImageData(
cutCoor.sx,
cutCoor.sy,
cutCoor.ex,
cutCoor.ey
)

if (quadrant % 2 == 0) {
canvas.width = imgW
canvas.height = imgH
} else {
canvas.width = imgH
canvas.height = imgW
}
ctx.putImageData(imgData, 0, 0)
callback(canvas.toDataURL())
}
},
},
}
</script>


<style lang="scss">
html,
body {
padding: 0;
margin: 0;
}
#app {
width: 100vw;
height: 100vh;
background: #ececec;
}

.btn {
width: 35%;
color: #fff;
background: #5daaf3;
border: none;
height: 40px;
border-radius: 20px;
margin-top: 20px;
margin-left: 40px;
}
</style>


那么经过翻转后当我们横屏移动设备时,保存出的图片会进行90度旋转,传递给后端的图片就是正常的了✅(代码可直接食用)


处理细节


后来我发现签字的笔锋太细了,打印出来的效果很差,于是通过查阅,只要设置 options 中的 minWidth和maxWidth 大一些即可
到此所有需求就已经都解决了。


总结


其实平时开发中没有对canvas用到很多,导致对这块的知识很薄弱,我在查阅的时候找到过用原生实现此功能,不过因为时间不够充裕,为了完成需求耍了一个小聪明,后续应该对canvas更多的了解一下,在深入了解上面的旋转方法具体是如何实现的,希望这篇文章能够对遇到这种需求并且时间紧迫的你有所帮助!如果有更好的方式,也希望大佬们分享,

作者:爱泡澡的小萝卜
来源:juejin.cn/post/7260697932173590565
交流经验变得更强!!

收起阅读 »

如何找到方向感走出前端职业的迷茫区

引言 最近有几天没写技术文章了,因为最近我也遇到了前端职业的迷茫,于是我静下来,回想了下这几年来在工作上处理问题的方式,整理了下思路 ,写了这一片文章。 关于对前端职业的迷茫,如何摆脱或者说衰减,我觉得最重要的是得找到一个自己愿意持续学习、有领域知识积累的细...
继续阅读 »

引言


image.png
最近有几天没写技术文章了,因为最近我也遇到了前端职业的迷茫,于是我静下来,回想了下这几年来在工作上处理问题的方式,整理了下思路 ,写了这一片文章。


关于对前端职业的迷茫,如何摆脱或者说衰减,我觉得最重要的是得找到一个自己愿意持续学习、有领域知识积累的细分方向。工作了3-5年的同学应该需要回答这样一个问题,自己的技术领域是什么?前端工程化、nodejs、数据可视化、互动、搭建、多媒体?如果确定了自己的技术领域,前端的迷茫感和方向感应该会衰弱很多。关于技术领域的学习可以参照 前端开发如何给自己定位?初级?中级?高级!这篇,来确定自己的技术领域。


前端职业是最容易接触到业务,对于业务的要求,都有很大的业务压力,但公司对我们的要求是除了业务还要体现技术价值,这就需要我们做事情之前有充分的思考。在评估一个项目的时候,要想清楚3个问题:业务的目标是什么、技术团队的策略是什么,我们作为前端在里面的价值是什么。如果3个问题都想明白了,前后的衔接也对了,这事情才靠谱。


我们将从业务目标、技术团队策略和前端在其中的价值等方面进行分析。和大家一起逐渐走出迷茫区。


业务目标


image.png
前端开发的最终目标是为用户提供良好的使用体验,并支持实现业务目标。然而,在不同的项目和公司中,业务目标可能存在差异。有些项目注重界面的美观和交互性,有些项目追求高性能和响应速度。因此,作为前端开发者,我们需要了解业务的具体需求,并确保我们的工作能够满足这些目标。


举例来说,假设我们正在开发一个电商网站,该网站的业务目标是提高用户购买商品的转化率。作为前端开发者,我们可以通过改善页面加载速度、优化用户界面和提高网站的易用性来实现这一目标。



  1. 改善页面加载速度: 使用懒加载(lazy loading)来延迟加载图片和其他页面元素,而不是一次性加载所有内容。


htmlCopy Code
<img src="placeholder.jpg" data-src="image.jpg" class="lazyload">

javascriptCopy Code
document.addEventListener("DOMContentLoaded", function() {
var lazyloadImages = document.querySelectorAll(".lazyload");

function lazyLoad() {
lazyloadImages.forEach(function(img) {
if (img.getBoundingClientRect().top <= window.innerHeight && img.getBoundingClientRect().bottom >= 0 && getComputedStyle(img).display !== "none") {
img.src = img.dataset.src;
img.classList.remove("lazyload");
}
});
}

lazyLoad();

window.addEventListener("scroll", lazyLoad);
window.addEventListener("resize", lazyLoad);
});


  1. 优化用户界面: 使用响应式设计确保网站在不同设备上都有良好的显示效果。


htmlCopy Code
<meta name="viewport" content="width=device-width, initial-scale=1.0">

cssCopy Code
@media (max-width: 768px) {
/* 适应小屏幕设备的样式 */
}

@media (min-width: 769px) and (max-width: 1200px) {
/* 适应中等屏幕设备的样式 */
}

@media (min-width: 1201px) {
/* 适应大屏幕设备的样式 */
}


  1. 提高网站易用性: 添加搜索功能和筛选功能,使用户能够快速找到他们想要购买的商品。


htmlCopy Code
<form>
<input type="text" name="search" placeholder="搜索商品">
<button type="submit">搜索</button>
</form>

<select name="filter">
<option value="">全部</option>
<option value="category1">分类1</option>
<option value="category2">分类2</option>
<option value="category3">分类3</option>
</select>

javascriptCopy Code
document.querySelector("form").addEventListener("submit", function(e) {
e.preventDefault();
var searchQuery = document.querySelector("input[name='search']").value;
// 处理搜索逻辑
});

document.querySelector("select[name='filter']").addEventListener("change", function() {
var filterValue = this.value;
// 根据筛选条件进行处理
});

协助技术团队制定策略


image.png
为了应对前端开发中的挑战,协助技术团队需要制定相应的策略。这些策略可以包括技术选型、代码规范、测试流程等方面。通过制定清晰的策略,团队成员可以更好地协作,并在面对困难时有一个明确的方向。


举例来说,我们的团队决定采用React作为主要的前端框架,因为它提供了组件化开发和虚拟DOM的优势,能够提高页面性能和开发效率。同时,我们制定了一套严格的代码规范,包括命名规范、文件组织方式等,以确保代码的可读性和可维护性。



  1. 组件化开发: 创建可重用的组件来构建用户界面,使代码更模块化、可复用和易于维护。


jsxCopy Code
// ProductItem.js
import React from "react";

function ProductItem({ name, price, imageUrl }) {
return (
<div className="product-item">
<img src={imageUrl} alt={name} />
<div className="product-details">
<h3>{name}</h3>
<p>{price}</p>
</div>
</div>

);
}

export default ProductItem;


  1. 虚拟DOM优势: 通过使用React的虚拟DOM机制,只进行必要的DOM更新,提高页面性能。


jsxCopy Code
// ProductList.js
import React, { useState } from "react";
import ProductItem from "./ProductItem";

function ProductList({ products }) {
const [selectedProductId, setSelectedProductId] = useState(null);

function handleItemClick(productId) {
setSelectedProductId(productId);
}

return (
<div className="product-list">
{products.map((product) => (
<ProductItem
key={product.id}
name={product.name}
price={product.price}
imageUrl={product.imageUrl}
onClick={() =>
handleItemClick(product.id)}
isSelected={selectedProductId === product.id}
/>
))}
</div>

);
}

export default ProductList;


  1. 代码规范示例: 制定一套严格的代码规范,包括命名规范、文件组织方式等。


命名规范示例:



  • 使用驼峰式命名法:例如,productItem而不是product_item

  • 组件命名使用大写开头:例如,ProductList而不是productList

  • 常量全大写,使用下划线分隔单词:例如,API_URL


文件组织方式示例:


Copy Code
src/
components/
ProductList.js
ProductItem.js
utils/
api.js
styles/
product.css
App.js
index.js

前端的价值


image.png
作为前端开发者,在业务中发挥着重要的作用,并能为团队和产品创造价值。前端的价值主要体现在以下几个方面:


1. 用户体验


前端开发直接影响用户体验,良好的界面设计和交互能够提高用户满意度并增加用户的黏性。通过技术的提升,我们可以实现更流畅的页面过渡效果、更友好的交互反馈等,从而提高用户对产品的喜爱度。


例如,在电商网站的商品详情页面中,我们可以通过使用React和动画库来实现图片的缩放效果和购物车图标的动态变化,以吸引用户的注意并提升用户体验。


jsxCopy Code
import React from 'react';
import { Motion, spring } from 'react-motion';

class ProductDetail extends React.Component {
constructor(props) {
super(props);
this.state = {
isImageZoomed: false,
isAddedToCart: false,
};
}

handleImageClick = () => {
this.setState({ isImageZoomed: !this.state.isImageZoomed });
};

handleAddToCart = () => {
this.setState({ isAddedToCart: true });
// 添加到购物车的逻辑
};

render() {
const { isImageZoomed, isAddedToCart } = this.state;

return (
<div>
<img
src={product.image}
alt={product.name}
onClick={this.handleImageClick}
style={{
transform: `scale(${isImageZoomed ? 2 : 1})`,
transition: 'transform 0.3s',
}}
/>

<button
onClick={this.handleAddToCart}
disabled={isAddedToCart}
className={isAddedToCart ? 'disabled' : ''}
>

{isAddedToCart ? '已添加到购物车' : '添加到购物车'}
</button>
</div>

);
}
}

export default ProductDetail;

2. 跨平台兼容性


在不同的浏览器和设备上,页面的呈现效果可能会有所差异。作为前端开发者,我们需要解决不同平台和浏览器的兼容性问题,确保页面在所有环境下都能正常运行。


通过了解各种前端技术和标准,我们可以使用一些兼容性较好的解决方案,如使用flexbox布局代替传统的浮动布局,使用媒体查询来适配不同的屏幕尺寸等。



  1. 使用Flexbox布局代替传统的浮动布局: Flexbox是一种弹性布局模型,能够更轻松地实现自适应布局和等高列布局。


cssCopy Code
.container {
display: flex;
flex-direction: row;
justify-content: space-between;
}

.item {
flex: 1;
}


  1. 使用媒体查询适配不同的屏幕尺寸: 媒体查询允许根据不同的屏幕尺寸应用不同的CSS样式。


cssCopy Code
@media (max-width: 767px) {
/* 小屏幕设备 */
}

@media (min-width: 768px) and (max-width: 1023px) {
/* 中等屏幕设备 */
}

@media (min-width: 1024px) {
/* 大屏幕设备 */
}


  1. 使用Viewport单位设置响应式元素: Viewport单位允许根据设备的视口尺寸设置元素的宽度和高度。


cssCopy Code
.container {
width: 100vw; /* 100% 视口宽度 */
height: 100vh; /* 100% 视口高度 */
}

.element {
width: 50vw; /* 50% 视口宽度 */
}


  1. 使用Polyfill填补兼容性差异: 对于一些不兼容的浏览器,可以使用Polyfill来实现缺失的功能,以确保页面在各种环境下都能正常工作。


htmlCopy Code
<script src="polyfill.js"></script>

3. 性能优化


用户对网页加载速度的要求越来越高,前端开发者需要关注页面性能并进行优化。这包括减少HTTP请求、压缩和合并资源、使用缓存机制等。


举例来说,我们可以通过使用Webpack等构建工具来将多个JavaScript文件打包成一个文件,并进行代码压缩,从而减少页面的加载时间。


结论


image.png
作为前端开发者,我们经常面临各种挑战,如业务目标的实现、技术团队策略的制定等。通过不断学习和提升,我们可以解决前端开发中的各种困难,并为业务目标做出贡献。同时,我们的工作还能够直接影响用户体

作者:Jony_men
来源:juejin.cn/post/7262133010912100411
验,提高产品的竞争。

收起阅读 »

前端简洁表单模型

web
大家好,我是前端菜鸡木子 今天想和大家浅谈下前端表单的简洁模型。说起表单大家一定都不陌生,因为各自团队内部一定充斥着各种或简单或复杂的表单场景。为了解决表单开发问题,市面上也有着许多优秀的表单解决方案,例如:Formily、Ant Design、FormRen...
继续阅读 »

gabriel-ramos-azbe3hSHNHU-unsplash.jpg


大家好,我是前端菜鸡木子


今天想和大家浅谈下前端表单的简洁模型。说起表单大家一定都不陌生,因为各自团队内部一定充斥着各种或简单或复杂的表单场景。为了解决表单开发问题,市面上也有着许多优秀的表单解决方案,例如:FormilyAnt DesignFormRender 等。这些框架的底层都维护着一套基础的「表单模型」,虽然框架不同,但是「表单模型」的设计却是基本一致,只是上层应用层的设计会随着业务的需求进行调整。今天的主题也会围绕着「表单模型」进行展开


前言


本文是偏基础层面的介绍,不会涉及到太多框架的源码解析。另外,我会以最近如日中天的 Formily 为例进行讲解,大家如果对 Formily 不太了解,可以先去了解和使用。


表单模型的基础概念


我们知道一个表单包含了 N 多个字段,每个字段都需要用户输入或者联动带出,当用户输入完成之后我们可以通过 Form.Values 的形式直接获取到表单内部 N 多个字段的值,那么这是如何实现的呢?


我们通过一张图来简单阐述下:


yuque_diagram.png


其中:



  • Form:是通过 JS 维护的一个表单模型实例,FormilycreateForm 返回的就是这个实例,它负责维护表单的所有数据和每个字段 Field 的实例

  • Field: 是通过 JS 维护的每一个字段的实例,它负责维护当前字段的所有数据和状态

  • Component: 是每个字段对应的展示层组件,可以是 Input 或者 Select,也可以是其它的自定义组件


从图中不难看出,每个 Field 都对应着一个展示层的 Component,当用户在 Component 层输入时,会触发 props.onChange 事件,然后在事件内部将用户输入的值传入到 Field 里。同时当 Field 值变化时 (比如初始化时的默认值,或者通过 field.setValue 修改字段的值 ),又会将 Field.value 通过 props.value 的形式传入到 Component 内部,以此来达到 ComponetField 的数据联动。


我们可以看下在 Formily 内部是如何实现的(已对源码进行一些优化和注释):


const renderComponent = () => {
// 获取 Field 的 value
const value = !isVoidField(field) ? field.value : undefined;

// 设置 onChange 事件
const onChange = !isVoidField(field)
? (...args: any[]) => {
field.onInput(...args)
field.componentProps?.onChange?.(...args)
}
: field.componentProps?.onChange

// 生成 Field 对应的 Component
return React.createElement(
getComponent(field.componentType),
{
value,
onChange,
},
content
)
}

这里面的 onChange 事件里触发了 field.onInput 的事件,在 field.onInput 内会做两件事情:



  • onChange 携带的 value 赋值给 field.value

  • onChange 携带的 value 赋值给 form.values


这里需要额外说明的是,一个 Form 会通过「路径」系统聚合多个 Field,每个 Field.value 也是通过路径系统被聚合到 Form.values 下。


我们通过一个简单的 demo 来介绍下路径的概念:


const formValues = {
key1: {
key2: 'value',
}
};

我们通过 key1.key2 可以找到一个具体的值,这个 key1.key2 就是一个路径。在 Formily 内维护了一个高级的路径模块,感兴趣的可以去看下 form-path


表单模型的响应式


聊完表单模型的基础概念后,我们知道



  • Component 组件通过 props.onChange 将用户的数据回传到 FieldForm 实例内

  • Field 实例内的 value 会通过 props.value 形式传递到 Component 组件内


那么问题来了,Field 实例内部的 value 改变后,Component 组件是如何做到细粒度的重新渲染呢?


不卖关子,直接公布答案:



  • formily: 通过 formily/reactive 进行响应式跟踪,能知道具体是哪个组件依赖了 Field.value, 并做到精准刷新

  • Antd:通过 rc-field-form/useForm 这个 hook 来实现,本质上是通过 const [, forceUpdate] = React.useState({}); 来实现的


虽然这两种方法都能实现响应式,但是 Ant 的方式比较暴力,当其中一个 Field.value 发生改变时,整个表单组件都需要 render 。而 Formily 能通过 formily/reacitve 追踪到具体改变的 Field 对应的 Componet 组件,只让这个组件进行 render



formily/reactive 实现比较复杂,这边不会深入探讨具体实现方式,感兴趣的小伙伴可以看下这篇文章 从零开始撸一个「响应式」框架 (本质上是通过 Proxy 来拦截 getset,从而实现依赖追踪)



接下来,我们就看下如何借助 formily/reactive 来实现响应式


第一步:我们需要在 Field 初始化时将 value 变成响应式:


import { define, observable } from '@formily/reactive'

class Field {
constructor(props) {
// 初始化 value 值
this.value = props.value;

// 将 this.value 变成响应式
define(this, {
value: observable.computed
})
}
}

第二步:对 Field 对应的 Componet 进行下 "包装":


import { observer } from '@formily/reactive-react'

const ReactiveComponentInernal = () => {
// renderComponent 源码在 「基础概念」章节里
return renderComponent();
}

export const FieldComponent = observer(ReactiveComponentInernal);


observer 内部也和 rc-field-form/useForm 类似,通过 const [, forceUpdate] = React.useState({}); 来实现依赖改变时,子组件级别的动态 render



到此为止,表单模型的响应式也基本完成了


表单模型的规范


有了以上的表单模型,我们就可以构建一个简单的表单框架。但是真实的业务场景却不可能这么简单,迎面而来的第一个问题就是「联动」,举个例子:


QQ20230729-134607-HD.gif


需求:当城市名称改变后,城市编码字段需要联动带出对应的值。我们可以快速想到两种方案:



  • 方案1:在 城市名称 字段的 onChange 事件里通过 form.values.cityCode = hz 的形式去动态修改 城市编码 字段。

  • 方案2:在 城市编码 字段里显示的配置对 城市名称 字段的依赖,同时需要配置依赖改变时的处理逻辑,例如:


const formSchema = {
cityName: {
'x-component': 'Select',
},
cityCode: {
'x-component': 'Input',
'x-reactions': {
dependencies: ['cityName'],
fulfill: {
state: {
value: '{{ $deps[0]?.value }}',
},
},
},
},
};

无论方案 1 还是方案 2 都能实现需求,但是两个方案各有缺点


方案 1 有两个问题:



  • 问题一:打破了【表单模型的基础概念】,cityName 对应的组件的 onChange 事件里「直接」对 cityNamecityCode 字段进行了修改。

  • 问题二:我们不能「直观」的看到 cityCodecityName 字段产生了依赖,只有在看具体代码时才能知道


方案 2 也会有两个问题:



  • 问题一:schema 本身的可读性不强,且使用 formily schema 时,配置内容比较多

  • 问题二:使用 schema 配置 x-component-props 时不能使用 ts 特性


当表单逐渐复杂起来的时候,方案 1 的弊端会逐步显现出来,字段间会产生诸多的 「幽灵」依赖和控制,导致后续迭代的时候根本无从下手。所以在我自己的团队内部,我们规定出了几条「表单模型」的使用规范:




  • 规范 1: 每个 Field 对应的 Component 只对自己的字段负责,不允许通过 Form api 直接修改其他字段

  • 规范 2: 在 formSchema 里需要维护表单的所有字段配置和依赖,字段间不允许出现「幽灵」依赖

  • 规范 3: 尽量不要使用 form.setValuesform.queryField('xxx').setValue 等动态修改字段值的 Form api(特殊场景除外)

  • 规范 4: 表单涉及到的所有字段都尽量存储到表单模型中,不要使用外部变量来保存



这些规范其实是个普适性的范式,无论你在使用 Formily 也好,还是 Ant Design 也好,都需要去遵守。规范 2 里我用了 Formilyschema 来说明,但如果你使用的是 Ant Design,可以把 formSchema 理解为 <Form.Item reaction={{ xxx }}></Form.Item>



其实 formily 的 schema 最终会通过 RecursionField 组件递归渲染成具体的 FormItem 形式



表单模型的应用层


有了上述的「表单模型」概念和规范之后,我们就可以来构建表单模型的应用层了


yuque_diagram(1).png



  • Form Scheam: 整个表单的配置中心,负责表单各个字段的配置和联动 、校验等,它只负责定义不负责实现。它可以是个 Json Schema,也可以是 Ant Design<FormItem>

  • Form Component: 表单内每个字段的 UI 层组件,可以再分为:基础组件业务组件,每个组件都只负责和自己对应的 Field 字段交互

  • 业务逻辑:将复杂业务抽象出来的业务逻辑层,纯 JS 层。当然这一层是虚拟的概念,它可以存在于 Form Componet 里,也可以放在入口的 Index Component 内。如果业务复杂, 也可以放到 hooks 里或者单独的 JS 模块内部


有了应用层架构后,在写具体表单页面时,我们需要在脑海中清晰的勾勒出每层(Schema Component Logic)的设计。当页面足够简单时,也许会没有 Logic 层,Component 层也可以直接使用自带的基础表单组件,但是在设计层面我们不能混淆


表单模型的实践 - Formily


从去年开始,我们团队便引入 formily 作为中后台表单解决方案。在不断的实践过程中,我们逐步形成了一套自己的开发范式。主要有以下几个方面


Formily 的取舍


我们借助了 formily 的以下几个能力:



  • formily/reactive: 通过 reacitve 响应式框架来构建业务侧的数据模型

  • formily/schema: 通过 json-schema 配置来描述整个表单字段的属性,当然其背后还携带着 formily 关于 schema 的解析、渲染能力

  • formily/antd: 一些高级组件


同时,我们也在尽量避免使用 formily 的一些灵活 API:



  • Form 相关 API:比如 useFormform.setValues 等,我们不希望在任何组件内部都能「方便」的窜改整个表单的所有字段值,如果当前字段对 XX 字段有依赖或者影响,你应该在 schema 里显示的声明出来,而不是偷偷摸摸的修改。

  • Query 相关 API: 比如 form.query('field'),原因同上


当然,这不代表我们绝不会使用这些 API ,比如在表单初始化时需要回填信息的场景,我们就会用到 form.setValues 。我想说明的是不能滥用!!!


静态化的 schema


我们认为 schema 和普通的 JSX 相差不大,只不过前者是通过 JSON 标准语言来表述而已,举个例子:


// chema 形式
const formSchema = {
name: {
type: 'string',
'x-decorate': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '请输入名称'
}
}
}

// jsx 形式
const Form = () => {
return (
<Form>
<FormItem name"name">
<Input placeholder="请输入名称" />
</FormItem>
</Form>

)
}

schema 最终也会被 formily/react 解析成 jsx 格式。那为什么我们推荐使用 schema 呢?



  • 原因一:schema 可以做到足够的静态化,避免我们做一些灵活的动态操作 (在 jsx 里我们几乎能做通过 form 实例动态的做任何事情)

  • 原因二: schema 更容易被解析和生成,为之后的智能化生成做铺垫(不一定是低代码)


表单模型的挑战


在真实业务开发过程中,我们对表单模型的使用会出现一些问题,以两个常见的问题为例:



  • 问题 1:我们是通过表单的 UI 结构来设计 schema 还是通过表单数据结构来设计?

  • 问题 2:有时候为了简单,我们会设计出一个巨大的 Component,这个 Componet 对应的 Field 嵌套了很多层字段


下面这个案例就可能触发上述的两个问题:


demo2.gif


其中,每个分类都对应着一组商品,所以最终表单的数据格式应该是这样的:


{
categoryList: [
{
categoryName: '分类一'
productList: [{ productName: '商品一', others: 'xxx' }],
},
{
categoryName: '分类二'
productList: [{ productName: '商品二', others: 'xxx' }],
}
],
}

我们提供两种思路来设计这个表单


方案一


我们发现简单的通过 ArrayTable 是实现不出这种交互的,所以我们直接设计出一个大而全的 Component,那么我们的实现方式应该是这样的:


// 设计一个大而全组件,过滤组件内部实现
const BigComponent = (props) => {
return (
<Row>
<CategoryArrayTable />
<ProductArrayTable />
</Row>

)
};

// schema 设计
const formSchema = {
categoryList: {
type: 'array',
'x-component': BigComponent,
}
}

在这种方案里,BigComponent 组件需要 onChange 整个表单的值(多层嵌套的对象数组),这会出现一个问题:formSchema 里看不到表单的所有字段配置,如果字段间需要有联动,那么只能在 BigComponent 组件内部去实现(违反了规范2)。


方案二


我们认为 schema 是面对表单数据结构设计的,Component 是面对 UI 设计的,两者的设计思路是分开的(但是在大多数场景下两者的设计结果是一致的)
那么我们的实现方式应该是这样的:


// 基于 formily/antd/ArrayTable + formily/react RecursionField 来实现
const CategoryArrayTable = (props) => {
return (
<Row>
<ArrayTableWithoutProductList />
<ArrayTableWithProductList />
</Row>

)
};

// schema 设计
const formSchema = {
categoryList: {
type: 'array',
'x-component': CategoryArrayTable,
items: {
categoryName: {
type: 'string',
'x-component': 'Select',
},
productList: {
type: 'array,
'
x-component': 'ArrayTable',
items: {
productName: {
type: '
string',
'
x-component': 'Select',
},
others: {},
}
}
},
}
}

在这种方案的 schema 里能够直接反映出表单的所有字段配置,一目了然,而且真实的代码实现会比方案一简洁很多


但是呢,这个方案有个难点,需要开发者对 formily 的渲染机制,主要是 RecursionFieldArrayTable 的源码有一定程度的了解。


当然,还有很多其他的方案可以实现这个需求,这边只是拿出两个方案来对比下设计思路上的差异,虽然最终的方案取舍是根据团队内部协商 + 规范而定的,但是在我自己的团队里,我们一直保持着一种设计准则:



schema 是面对表单结构的,Component 是面对 UI 的



后续


在实践过程中,我们发现了一些待优化点:


1、我们发现对于复杂的表单页面,schema 的配置会非常冗长,如果 schema 足够静态化的话,我们是否可以简化对 schema 的编写,同时能提高 schema 的可读性呢?低代码平台是个方案,但是太重,是否可以考虑弄个 vsocde 插件类接管 schema ?


2、如果表单配置、表单子组件、业务逻辑都由 schemaComponentLogic Fucntion 来负责了,我们是否可以取消表单页面的入口组件 index.tsx 呢?


当然随着对表单的不断深入研究,还有很多其他问题可以优化和解决

作者:木与子
来源:juejin.cn/post/7261262567304921146
,这边就不一一列举了

收起阅读 »

技术负责人如何应对工作中频频被打断

本文翻译自《Managing your interrupt rate as a tech lead》原文分为三篇,作者是 Nicholas C. Zakas,他也是《Professional JavaScript for Web Developers, 3rd...
继续阅读 »

本文翻译自《Managing your interrupt rate as a tech lead》原文分为三篇,作者是 Nicholas C. Zakas,他也是《Professional JavaScript for Web Developers, 3rd Edition》的作者。本文主要是在讲 tech lead 如何进行时间管理,写得特别好,建议仔细阅读一下。



我在 2008 年成为了技术负责人(tech lead),起初并不知道这是一种怎样的体验。在我的认知里,技术负责人和其他软件工程师没有太大区别,唯一的差别是技术负责人可以对技术决策做出最终决定。然而,我没想到的是,在那之后的日子里,发生了很大的变化。特别让我震惊的是,要越来越多的时间被花费到讨论上,而不是编码。


我的日程被会议填满了,在这些会议里与产品经理、项目经理和工程经理讨论项目。与此同时,其他工程师也经常打断我,向我问一些问题。不知不觉,我似乎没有完成太多工作。


对于新的技术负责人来说,这是一种常见的经历:你之前所掌握的时间管理技巧不再起作用。相反,你每天都会被频繁的打断,让你无法完成自己的工作,而且也没人来指导你如何去应对工作频频被打断这一问题。


1. 种瓜得瓜,种豆得豆


1.1 为什么技术负责人会遇到工作被打断问题


尽管技术负责人的角色在不同公司甚至不同项目中有所不同,但通常由两类任务组成:




  • 帮助他人 - 作为技术负责人,你会花费一部分时间帮助其他工程师完成他们的工作。这可能包括提供正式的代码或规范审查、指导、检查进展、回答问题,或其他各种事项,以帮助团队前进。




  • 自己的工作 - 你也需要有自己的产出。这可能是代码、技术规范、演示文稿、项目计划或其他你作为主要推动者的工作。




在与他人合作和独立开展工作之间来回切换,需要不同的时间管理技巧,这通常是技术负责人所面临的难题。他们觉得自己的工作是确保团队没有困扰,并让团队不断地前进。因此,一旦处于失联的状态,就会对工作效率产生负面影响。他们鼓励团队成员在任何时候都可以打断他们寻求帮助,通常通过实时通信渠道,比如聊天或即时消息。他们认为这样做可以建立信任,同时确保团队成员不会因为等待回复而受阻。虽然这种方法背后的思路值得称赞,即希望为团队成员提供支持,但最终结果是一天的工作混乱不堪,很难或根本无法在个人的工作上取得进展。


由于每次被打断需要大约 15 分钟才能重新集中注意力,技术负责人在这种以打断为驱动的环境下很难完成任何事情。那么解决方案是什么呢?


1.2 你的团队实际上并不需要那么依赖于你


当我第一次成为技术负责人时,我们团队使用 Yahoo Messenger 进行沟通。(对于不了解的人来说,Yahoo Messenger 是 2000 年代流行的即时通讯程序。)作为一个分布在不同楼层、不同校区乃至不同国家的团队,能够快速找到有问题的人是我们日常工作中的重要部分。作为技术负责人,我经常收到来自团队中 24 名前端工程师以及后端工程师、产品经理和工程经理的消息。


我的压力特别大,以至于我与我的主管开了一次会,探讨我是否适合这个职位。我向他描述了我的一天是如何度过的,他非常冷静地给了我一些建议。



“起初,你很好地引导团队成员在需要帮助时来找你。现在,你需要引导他们自己解决问题。并非每个问题都是 Nicholas (本文作者) 的问题。如果是只有你才能解决的问题,那就没问题,你可以去解决。但是如果是其他人可以解决的问题,就让他们自己去解决吧。”



新上任的技术负责人通常会认为,只有他们尽可能多参与项目才能成功,起初我也是这样想的。事实上,你参与项目的部分越多,团队的功能性就越低,进展就越慢。你的团队成员(理论上)被雇佣是因为他们也是有能力的成年人,可以在你的帮助下或者没有你的情况下交付高质量的软件。有时候你会帮助他们解决问题,但大多数时候他们有能力自己解决问题。


这是否意味着人们在解决问题时不会像你帮助他们那样迅速?实际上,并非如此,而这两者也没什么关系。每个人在解决之前从未遇到过的问题时,都会比较慢。通过自己努力解决问题,比直接得到解决方案更有助于巩固学习。通过过早地介入并为他人解决问题,实际上会剥夺他们更具成效的学习体验。通过努力解决问题所学到的经验是成长的源泉。


1.3 你得到的打断是你奖励的结果


虽然一些打断是因为人们遇到困难,但很多打断并不是因为他们真的遇到麻烦,而是因为他们不想花时间自己解决问题。如果他们可以发信息给你,并在几分钟内得到回应,那么他们为什么要花 15 分钟自己解决问题呢?这太低效了!当他们得到他们寻求的答案时,你们都会获得多巴胺的刺激,你为帮助别人前进感到自豪。这是一个上瘾的行为,对你们两个人都没有好处。


在我在 Yahoo 工作时,我注意到一个令人沮丧的模式:人们给我发送只是写着“嗨”的消息。我会回复以了解他们想要什么,结果发现那只是一些他们本可以自己处理的普通问题。这个“嗨”是另一种即时满足的行为,这只是一次询问,以确定我是否有空,然后再决定是否打扰我告知他们想要什么。


经过几个星期的这种情况,我决定尝试一种不同的方法。当我收到一个只说“嗨”(或“你好”或类似的)的消息时,我不会立即回复。我会等待看看他们是否会跟进并解释他们需要什么,如果他们没有,那就结束了。而且大多数情况是他们并未跟进,这告诉我两件事:1)他们所需要的其实并不那么重要,2)他们找到了如何自己处理的方法。而且作为额外的收获,一旦我这样做了几个星期,“嗨”这样的消息几乎就消失了。


这对我来说是一个重要的教训:你得到的打断是你奖励的结果。如果我继续迅速回复“嗨”的消息,我将继续收到它们。通过不立即回复,我给出了负面反馈,教导每个人不要打扰我。他们无法通过这种行为获得他们寻求的多巴胺刺激。另一方面,如果他们给我发具体的问题,我会立即回答。所以我在不想要的打断上提供了负面反馈,而在我想要的打断上提供了积极反馈。


要控制你不想要的打断,只需不立即回应。练习在收到不想要的打断后等待一个小时,逐渐教导人们不要期望即时满足感。(如果一个小时感觉太长,可以先等待 15 分钟再回应。设定一个计时器,直到它响起后再回应。你会习惯的。)


1.4 其他你可能在鼓励的打断方式


当然,“嗨”消息并不是你可能无意中鼓励的唯一交互方式。每当我听到有人抱怨与他人的互动时,我经常会想起首席执行官教练杰瑞·科隆纳问他的客户的这个问题:



“你如何能够串联一切来制造那些你说你不想要的条件?”



你遇到的许多打断都直接与你对待它们的方式有关。因此,每当你遇到一个不受欢迎且不重要的打断时,请问自己你做了什么来鼓励这种行为,以及你能够做些什么来阻止它。以下是一些人们常常困扰的其他常见打断情况:




  • 下班后的电子邮件 - 我曾经与很多人谈过,他们认为下班后的电子邮件是他们工作生活的正常组成部分。虽然对某些人来说可能是真的,但下班后的电子邮件几乎总是因为你回复了下班后的电子邮件而产生的。当人们知道你在下午 5 点后查看电子邮件时,他们就会在下午 5 点后发送电子邮件;当人们知道你在下午 5 点后不查看电子邮件时,你将在下午 5 点后几乎不会收到电子邮件(或者至少,你收到的电子邮件不需要立即回复)。




  • 阻塞时间内的会议 - 一些技术负责人学会在日程安排中为工作或个人任务设置阻塞时间,但当有人在这段时间安排了会议时,他们却接受了。猜猜发生了什么?一旦你这样做了,你就传达了这种打断是适当且可接受的信号,因此这种情况会更加频繁发生。当我通过拒绝会议邀请将我的阻塞时间视为神圣而保护起来时,我就不再收到那些会议请求了。




  • 专注时间的打断 - 如果你与团队在办公室共同工作,建议设置专注时间指示器,让人们知道你不希望被打断。常见的指示器包括戴着耳罩式耳机、小纸条上贴着绿色/红色标记以及方格间上方挂着红绿灯。当然,当你设定了专注时间指示器后,需要拒绝试图打断你的人。保护你的专注时间会逐渐教导人们等待。




当然,你肯定会遇到更多你鼓励但实际上想要避免的行为的情况。你能够避免所有打断吗?当然不能。有一些合理的打断,但这种情况比填满你整天并造成不必要压力的打断要少得多。


1.5 小结


技术负责人需要学会平衡自己的工作任务和帮助团队其他成员。这必然会产生一些摩擦,因为大多数技术负责人倾向于将团队的需求放在个人之上,所以他们会明确或含蓄地鼓励打断。虽然这可能对团队中的其他人有利,但这也意味着你没有时间完成自己的工作。


好消息是,你对于接受打断拥有 99% 的控制权。当现状不适合你时,你不必接受它。通过每一次互动,你都在训练你的同事什么样的打断是可接受的,如果你遭到了你不想要的打断,那么你可以通过消除积极反馈来改变这种情况。


你的团队成员是成年人,他们被聘请来做一项特定的工作,虽然通过不断打断你他们可能更高效,但他们也可以在不打断你的情况下正常工作。你可以鼓励他们尝试自己解决一些问题,如果真的遇到困难再向你寻求帮助,或者告诉他们你现在不方便但几个小时后会有时间,看看会发生什么。在第二部分中,我将讨论如何管理你的日程,以最大程度地减少对你工作的不必要的打断。


2. 主动出击,填充日程


如果你和大多数人一样,你的工作日程主要用于展示给他人你的空闲时间。当别人想和你交谈时,他们会在你的日程上的空白时间段安排会议。随着越来越多的会议被安排,空余时间变得更少,留给别人与你安排会议的选项也减少了。在成为技术负责人之前,你的会议数量仍然给你留下了很多编码的空闲时间。毕竟,你的主要工作仍然是产出代码,你的同事(希望如此)会尽量减少打断。但作为技术负责人,这些空闲时间段被占用得更快。你永远不知道会议请求何时会显示在你的日程上,这使得开始任何需要专注的任务变得困难。幸运的是,还有另一种方法。


2.1 倒置你的日程,使用时间块


另一种方法叫做时间块。与其让你的日程大部分为空白,让人们通过发送会议邀请来填满它,不如从给特定任务设置时间块开始。也许你需要编写或审核技术规范?在日程上为此安排一个小时的时间块。你需要编写一些代码吗?在日程上留出 90 分钟的时间块。你负责批准拉取请求吗?早上和晚上各留出 30 分钟的时间块来处理这些事务。如果你在想,“等一下。如果我用所有这些任务填满我的日程,看起来我比实际上要忙”,那么是时候换一种思路了。


无论你需要做什么,都需要时间,甚至包括人们认为是“免费”的事情,比如查看电子邮件或吃午饭。当这些任务不在你的日程上时,实际上会产生你有空闲时间的错觉。这些事情仍然需要完成。只不过看你的日程的同事不知道你何时会做这些事情。对他们来说,一个空闲时间段意味着你可以参加会议,即使你计划在周三下午处理那个迭代任务。


通过在日程上放置你需要完成的实际任务,你正在做两件事情:




  • 准确地规划你的日程安排,所以你知道什么时候该做什么。如果一个会议邀请在任务时间块期间到来,你清楚知道如果接受它,你将放弃什么。




  • 让同事们知道你真正有空参加会议的时间。当你的日程被任务填满,并留出一些空闲时间段时,人们通常会默认在这些时间段安排会议。这样,周三下午的迭代任务就不会被打断。




无论哪种情况,你都在为你的可用性和时间使用方式创建一个更准确的视图。


2.2 常见的需要放在日程上的任务


确定要放在日程上的任务比看起来更具挑战性。一些任务很容易界定,并可能存在于迭代计划中,但你每天还做些什么呢?以下是一个非详尽无遗的任务类型列表,可以放在你的日程上:




  • 异步沟通 - 你肯定需要时间检查公司使用的任何沟通系统,无论是电子邮件、Slack、Yammer 还是其他任何方式。你可能会在一天中的某个时候定期进行这样的检查,但更好的方法是在一天中安排时间进行检查。




  • 午餐/休息 - 每当你想自己花点时间时,把它放在你的日程上。全天都要定期休息和恢复精力,将这些放在日程上会提醒你去做。




  • 家庭事务处理 - 如果你有任何需要定期检查的个人问题,也要放在你的日程上。




  • 社交媒体检查 - 不管在工作中是否检查社交媒体是一个好主意,很多人都这样做。如果这是你的例行事项,应该放在你的日程上。(如果这是你的工作内容,那肯定应该放在你的日程上。)




  • 分配的任务 - 无论你使用的是迭代、看板还是其他形式的计划,你可能被分配了一些任务,有交付内容和截止日期。把它们放在你的日程上,并具体标注你将要完成的任务(而不是“编码时间”,你可能不知道自己打算做什么)。




  • 代码审查 - 作为技术负责人,审查同事的代码是一项常见任务。你可能希望每天安排时间进行审查,甚至一天进行几次,以确保人们不受阻碍。




  • 临时任务 - 你可能会有一些自己分派的任务,这些任务也应该放在你的日程上。再次提醒,务必标记出这个时间块,以便清楚知道你打算用那段时间做什么。例如,审查规范、审查简历、撰写博客文章、撰写文档和准备演示文稿等任务都是这样的例子,这些任务可能没有在你的工单系统中正式分配给你,但你仍然需要完成它们。




  • 日程计划 - 每周五,花 15 到 30 分钟的时间来设置下周的时间块。你应该对自己即将处理的任务有足够的了解,以便使自己能在周一上班时准确安排时间。




从这个列表中可以看出,你的日程不再只是用来安排会议。它可以用于任何需要消耗你时间的任务。会议将始终通过会议组织者找到你的日程,而这些任务只有在你有意添加它们时才会出现在日程上。


时间块的黄金法则是:如果做某事需要时间,那么它就会出现在你的日程上。


2.3 消除时间块中的干扰


也许时间块中最重要的部分是在预定时间段内消除所有干扰。毕竟,如果你在检查规范的一个小时时间段内被电子邮件、Slack 或社交媒体打断,那么这个时间块有多有用呢?因此,消除干扰对于使时间块成功非常关键。


当你在进行任务块时,尽量消除所有干扰。以下是一些建议:




  • 关闭电子邮件、Slack 和社交媒体(不仅仅是最小化窗口,要关闭应用程序)。你应该在日程上安排了定期检查的时间。




  • 考虑将手机设置为“勿扰”模式,以消除应用程序的提示音和信息通知。




  • 关闭任何你没有在使用的应用程序(包括网页浏览器)。




  • 如果你正在使用网页浏览器,请关闭所有你没有在积极使用的标签。可以使用像 Momentum2 这样的扩展来帮助你在打开新标签时集中注意力。




如果你正在使用具有“专注模式”的应用程序,请将其打开。例如,Visual Studio Code 有一个名为“禅意模式”的功能,可以将屏幕上的其他内容屏蔽掉。


总体目标是,当你说你要在周一的 13 点到 14 点之间检查规范时,你实际上是在检查规范,而不会被其他事情分心。只专注于你在此期间分配给自己的任务,并一直工作直到完成或达到下一个合适停止点。


2.4 日程的前后变化


为了让你对时间块在实践中是什么样子有一个概念,看看在实施时间块之前和之后,你的日程可能会是什么样子是很有帮助的。如果你是一位技术负责人,尚未设置用于时间块的日程,你的日程可能如下所示:



之前的日程:每天早上 9:30-9:45 进行日常站立会议;周三中午吃团队午餐;一对一会议和其他会议零散地安排在整个星期中。


紫色的预约日程是由你的团队设定的,蓝色的预约日程是你与其他人实时互动的时间。当你看这个日程时,除了每天上午 9 点 30 分的日常站立会议和每周三下午 1 点的团队午餐外,日程上大部分都是空白。当然,它实际上并不是空的 - 你需要那段空闲的时间来完成被分配给你的所有工作,以及预期的但通常未计算在内的所有工作。无论何时有人想要与你安排事情,你日程上的任何空闲时间都可以使用,在这个日程上,有很多空闲时间。


一旦你过渡到时间块,你的日程会更像这样:



时间块日程:每天都有特定的任务在特定的时间安排,中间夹杂了其他会议


当你看到一个时间块日程时,很容易发现原始日程上的所有空白空间实际上并不是空闲的。那些日常站立会议、团队午餐以及第一个日程中的其他所有会议仍然存在,但现在你可以看到在这些会议之间你要完成什么任务。我用红色标记了涉及特定软件的任务,用黄色表示你独自完成的任务,用绿色表示休息时间。你的日程上仍然有空闲时间,但比之前少得多。


2.4 保护你的时间块


在为日程设置时间块并计划消除干扰之后,还有最后一步需要注意:保护你的时间块免受同事的干扰。根据你所在公司的日程文化,你的同事可能能够看到你在时间块上标记的内容,或者他们只会看到那段时间是“忙碌”的。在这两种情况下,你可能会在你的日程被阻塞的时间段收到会议请求。你如何应对这些请求将决定你的同事是否会尊重你的日程。


正如在第一部分中所讨论的,你会得到更多你所鼓励的打扰。如果你总是接受在时间块上的会议请求,那么你就传达了你的时间块并不重要,别人可以随时安排会议。另一方面,如果你总是拒绝在时间块上的会议请求,人们可能会感到沮丧,并抱怨你从来没有空闲。自然而然,正确的做法在于取中间道路。


假设人们在很大程度上遵守你的日程可用性,只偶尔会在时间块上发送会议请求,最简单的做法是询问这个请求的重要性。毫无疑问,在紧急情况下,你需要改变当天的计划并重新安排时间块,但这应该是例外而不是规则。规则是让人们找到你日程上的一个空闲时间段,并在那里请求会议。如果由于某种原因无法找到合适的时段,那么你的责任就是确定会议请求的重要性,只接受最重要的邀请。如果副总裁想要召集一组 10 个人讨论新举措,那么你很可能需要调整你的日程安排;如果同事想请教你对他们撰写的内容的意见,那可以等待。


在处理这些请求时要根据情况行事,但始终以拒绝为默认,并提议一个你有空闲时间的新日期和时间。有时可能要等到下周才有空闲,对许多请求来说这是可以接受的。


2.5 小结


在成为技术负责人之前,你可能没有花太多时间管理自己的日程,所以你的日程很可能大部分都是空闲时间,偶尔会安排一些会议。剩下的时间都是属于你自己的,而且大部分时间都用来完成任务。偶尔出现在你日程上的会议并不是什么大问题,因为你有很多空闲时间来履行其他的职责。


作为一名技术负责人,你需要平衡自己的任务和帮助他人的工作,如果允许他人通过会议请求来控制你的日程,那么你将经常被打断,计划好的事情也会受到干扰。解决方案是使用时间块,在你的日程上创建专门用于特定任务的时间块。这些任务包括查看电子邮件、代码审查和编写代码等。任何需要时间或专注力来完成的事情都应该成为你日程上的一个时间块,这样你和同事们都知道在这段时间里你是忙碌的。


仅仅表示在某个时间段你很忙是不够的。你需要通过关闭电子邮件、聊天和社交媒体来消除自己的干扰,以便能够专注于自己的工作;你可以安排一段时间来稍后查看这些。当你为某个特定任务安排了时间块时,只专注于这个任务,不要处理其他事情。


当你收到与你的时间块重叠的会议请求时,你还需要保护你被阻塞的时间。你如何应对这样的请求将决定其他人是否尊重你的日程。如果你总是在被阻塞的时间接受会议邀请,你就会继续收到这些请求。通过拒绝大部分请求并建议一个更合适的时间来保护你的日程。是的,会有紧急情况需要腾出时间,但那应该是例外而不是规则。


也许你会想,这听起来很好,但这是否意味着我整天都在忽视我的同事?我什么时候能和他们交流?答案是在办公时间内,这将在本系列的第三部分进行讨论。


3. 集中处理,提前预约


如果你按照之前两部分的建议去做,现在你可能已经让你的同事们不再随意打断你。你停止了奖励那些你不希望发生的打扰行为,所以这些情况变得越来越少。你已经在日程上划分出了专门用于任务的时间,并拒绝了在这些时间段发送的会议请求。你还选定了每天的特定时间来回复电子邮件和 Slack 消息。但是你感到不舒服。似乎你在忽视你的同事。你喜欢你新的高效水平,但不喜欢感觉自己让队友失望。这就是办公时间(Office hours)的用处所在。


3.1 将打断集中在办公时间中


大学和学院通常要求教授有办公时间,以便学生在课外获得额外的帮助或提问。教授每周发布他们在办公室的时间,学生知道他们可以随时过去(或在这个时间段内安排一个时间)寻求额外的帮助。这样教授就可以不需要处理来自所有学生的不断问题和打扰。作为技术负责人,你也可以采取同样的做法。


还记得系列文章第一部分提到的那些随机的“嘿”消息吗?你可能遇到了类似的情况,人们只需要你几分钟的时间来回答问题。这是你作为技术负责人的工作的一部分,而且这种情况永远不会消失。相反,你可以将所有这些打扰集中到一周的几个特定时间段内。这样,你就不会整天接收到随机的请求,而是让队友们知道他们可以在办公时间内向你提问,并乐意回答他们的问题。


由于你已经在日程上划分时间块,你可以每周多次为办公时间添加时间块。根据你的打断频率,你可能想每天预留一个小时,或者在周一、周三和周五预留两个小时......确切的时间安排可以根据你的需求进行调整。最重要的是,这是一个专门用于集中处理打扰的时间块,而不是让它们在全天不定期发生。


如果你使用的是 Google Calendar,在这种情况下有一个完美适用的功能,叫做预约日程(appointment schedules)。你可以创建一个预约日程,让人们在你的时间段内请求预约。当人们查看你的日程时,他们将看到你的预约日程,并可通过链接在该时间段内安排时间。如果你没有使用 Google Calendar,你可以在日程上创建一个标记为“办公时间”的时间块,并将其设置为“可用”而不是“忙碌”,以便让其他人轻松看到你什么时候有空。


起初,你可能会遇到一些抵触这种变化的反对声。人们会觉得如果不能立即联系到你,就无法发挥自己的最大效率。因此,我建议每天开始先安排一个办公时间,并等到你和团队都适应了之后再调整(在极端情况下,每天安排两个时间段可能会有用)。如果人们知道他们需要等待与你交谈的最长时间是 24 小时,那么他们就会感到更少的焦虑。后来,你可以调整时间安排以更好地满足团队的需求。


几周后,你可能会发现,与之前不经安排的打断相比,你收到的计划好的打断变少了。为什么呢?因为人们在等待与你见面的过程中解决了自己的问题。这是一件好事,因为团队正在学会更有韧性和独立解决问题,从而释放出你的时间,让你专注于自己的工作任务。


此时,你可能正在思考,那对于其他的所有会议,我该怎么办呢?


3.2 为其他会议设置预约时间块


为办公时间预留时间块实际上只是在你的日程上设立特定类型的预约时间块,也就是说这些是用于可能与他人进行预约的定期时间段。技术负责人经常被拉入不同类型的会议中:与其他工程师的一对一会议、与经理的会议、产品或架构审查等。如果有你需要参加的定期会议,好消息是,这些已经在你的日程上有所体现,并且我鼓励你将其设置为重复会议,这样你就不必再去考虑它们了。对于其他类别的会议,人们请求你的时间,最好为这些目的设置预约时间块。


如何设置这些时间块完全取决于你。也许你想要为一对一会议单独设置专门的时间块,与其他类型的会议分开。也许只有一个大的“会议”时间块,可以安排任何类型的会议。你可以根据你经常参加的会议选择最合适的系统。而且请友善地对待你的同事:在早晨或深夜预留会议时间并不能解决问题。确保你的预约时间段是在人们最有可能请求的时间段内。


当然,有时候人们需要在你规定的预约时间之外安排会议,但希望他们提前联系你,告诉你原因。当然,你不能告诉工程副总裁说你不能参加临时会议。但是当你开始更好地管理你的日程时,你会发现某些会议会在特定的时间出现,然后你就可以相应地调整你的时间块。


3.3 日程的前后变化


之前,我向您展示了在使用时间块之前,您的日程可能会是什么样子:



之前的日程:每天早上 9:30 到 9:45 有团队站立会议;周三中午有团队午餐;一对一会议和其他会议零星分布在整个周内。


接下来,您学习了如何使用时间块将任务安排到日程中,并初步规划了您一周的安排。现在,当您添加预约时间块时,您将更清楚地知道特定类型的会议安排在何处:



时间块日程:每天都有特定时间分配给具体任务,办公时间和一对一会议也分别安排在本周的特定时间段内。


这是一个完全按时间块划分的日程,其中包括办公时间和一对一会议的预约时间块,这两种是技术负责人经常被要求参加的重复性会议。办公时间比较规律,通常是午饭后的 30 或 60 分钟;而一对一会议的时间段则根据可用性安排在周二至周四的不同时间。请记住,尽管这些时间已被预留,但您可能并没有在这些时间段内有任何预约。目标是将它们安排到您的日程中,以便在需要时您知道何时会发生。


请记住,您不需要将每一天的每个小时都预留出来。当您在周五进行每周日程规划时,您可能会发现您的任务和预约时间块并没有填满整个日程,这是可以接受的。只要为您知道需要做的所有事情预留了时间块,留下空闲时间段并不是错的。您甚至可能会发现到了周五,所有的空闲时间段都被填满了。


那么,如何过渡到这个新系统呢?


3.4 沟通至关重要


为了更好地控制你的中断率,你必须明确告知什么样的打扰是可以接受的,以及何时可以打扰。突然改变你的行为会给团队带来不必要的干扰,也会让团队成员更难接受。你可以通过提前告知团队并对其反馈持开放态度来尽量减少这种干扰。一个好的方法是解释说你将尝试这个新流程六周,并根据之后的反馈进行调整。


以下是一封重设关于打扰期望的电子邮件示例:



大家好,


作为技术负责人,我在不断发展的过程中发现自己的时间利用效率不够高,也无法达到我希望的效果。为了解决这个问题,我计划从下周一开始做一些工作上的改变,并且想提前告诉大家以防有任何问题或顾虑。我将进行以下改变:


Slack:我将限制在 Slack 上花费的时间。我发现经常被通知打扰很难集中注意力完成任务。我每天只会检查三次 Slack,并且只在那些时间段内回复消息。如果你需要得到简短的答复,Slack 是与我联系的最佳方式。


技术帮助:我每天下午 2 点到 3 点将作为我解答技术问题的办公时间。请在那段时间内安排一个 15 分钟的时间块,我很乐意直接与你沟通。这样,我就能在我知道自己可以集中注意力的时候全神贯注地帮助你。


一对一会议:如果你需要安排更长时间的讨论,请使用我日程上的一对一时间段。
其他会议:如果你需要安排其他类型的会议,请在我的日程中找到空闲时间段。如果你在你所需的时间范围内找不到合适的时间,请告诉我,我会调整我的日程安排。


紧急情况:如果有任何需要我立即处理的紧急情况,请打电话(而不是发短信)给我的手机。我关闭了短信通知,但电话总是会接通。


我计划尝试这个流程六周,看看效果如何,并且非常希望在过程中得到你们的反馈,以便我在有需要时进行调整。同时,如果你有任何问题或顾虑,请随时与我联系。



当你发送这样的电子邮件时,你很少会遇到对改变的阻力。明确要求反馈并允许人们提出问题可以降低问题发生的可能性。你也没有指责别人或告诉别人不要打扰你。相反,你重新界定了希望如何与你进行不同类型的讨论和如何安排时间。这种清晰度在某些情况下会使人们更有可能找你,因为他们不再担心在不合适的时候打扰你。


3.5 小结


技术负责人的角色充满挑战,通常情况下,你的经理可能并不是一个技术负责人,可能无法为你提供关于如何管理时间的最佳指导。在帮助他人和处理自己的工作之间找到合适的平衡非常重要,而管理被打扰的频率是找到这种平衡的最佳方式之一。


管理打扰频率的一种方法是通过时间块划分来调整你的日程,确保每个你想要专注完成的任务都安排在特定的时间段。这样可以防止人们在这些时间安排会议。只要你专注于手头的任务,关闭其他应用程序并避免其他干扰,你会发现你能做更多的事情。为电子邮件和 Slack 设置时间块,确保你在一天中与团队成员保持正常的沟通。


为了弥补随机打扰的损失,安排定期的办公时间可以让团队成员知道什么时候可以与你联系。具体的办公时间安排取决于你和团队的决定,但应该每周进行几次。在找到最适合你的团队的办公时间安排之前,你可能需要经历几轮尝试。你还可以为其他类型的定期会议,如一对一会议或产品评审,设置类似的预约时间块。


作为技术负责人,很大程度上是关于有效分配时间来帮助团队实现目标。本文讨论的工具可以帮助你重新掌握自己的时间,并使你在交付结果方面更加高效,同时让团队能够在没有持续打扰

作者:KooFE
来源:juejin.cn/post/7263085999970861116
的情况下成长和学习。

收起阅读 »

三言两语说透柯里化和反柯里化

web
JavaScript中的柯里化(Currying)和反柯里化(Uncurrying)是两种很有用的技术,可以帮助我们写出更加优雅、泛用的函数。本文将首先介绍柯里化的概念、实现原理和应用场景,然后介绍反柯里化的概 念、实现原理和应用场景,通过大量的代码示例帮助读...
继续阅读 »

JavaScript中的柯里化(Currying)和反柯里化(Uncurrying)是两种很有用的技术,可以帮助我们写出更加优雅、泛用的函数。本文将首先介绍柯里化的概念、实现原理和应用场景,然后介绍反柯里化的概 念、实现原理和应用场景,通过大量的代码示例帮助读者深入理解这两种技术的用途。


JavaScript中的柯里化


概念


柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由数学家Haskell Curry命名。


简单来说,柯里化可以将使用多个参数的函数转换成一系列使用一个参数的函数。例如:


function add(a, b) {
  return a + b; 
}

// 柯里化后
function curriedAdd(a) {
  return function(b) {
    return a + b;
  }
}

实现原理


实现柯里化的关键是通过闭包保存函数参数。以下是柯里化函数的一般模式:


function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      }
    }
  }
}

curry函数接受一个fn函数为参数,返回一个curried函数。curried函数检查接收的参数个数args.length是否满足fn函数需要的参数个数fn.length。如果满足,则直接调用fn函数;如果不满足,则继续返回curried函数等待接收剩余参数。


这样通过闭包保存每次收到的参数,直到参数的总数达到fn需要的参数个数,然后将保存的参数全部 apply fn执行。


利用这个模式可以轻松将普通函数柯里化:


// 普通函数
function add(a, b) {
  return a + b;


// 柯里化后
let curriedAdd = curry(add); 
curriedAdd(1)(2); // 3

应用场景



  1. 参数复用


柯里化可以让我们轻松复用参数。例如:


function discounts(price, discount) {
  return price * discount;
}

// 柯里化后
const tenPercentDiscount = discounts(0.1); 
tenPercentDiscount(500); // 50
tenPercentDiscount(200); // 20


  1. 提前返回函数副本


有时我们需要提前返回函数的副本给其他模块使用,这时可以用柯里化。


// 模块A
function ajax(type, url, data) {
  // 发送ajax请求
}

// 柯里化后
export const getJSON = curry(ajax)('GET');

// 模块B
import { getJSON } from './moduleA'

getJSON('/users', {name'John'});


  1. 延迟执行


柯里化函数在调用时并不会立即执行,而是返回一个函数等待完整的参数后再执行。这让我们可以更加灵活地控制函数的执行时机。


let log = curry(console.log);

log('Hello'); // 不会立即执行

setTimeout(() => {
  log('Hello'); // 2秒后执行
}, 2000);

JavaScript中的反柯里化


概念


反柯里化(Uncurrying)与柯里化相反,它将一个接受单一参数的函数转换成接受多个参数的函数。


// 柯里化函数  
function curriedAdd(a) {
  return function(b) {
    return a + b;
  }
}

// 反柯里化后
function uncurriedAdd(a, b) {
  return a + b; 
}

实现原理


反柯里化的关键是通过递归不停调用函数并传入参数,Until参数的数量达到函数需要的参数个数。


function uncurry(fn) {
  return function(...args) {
    let context = this;
    return args.reduce((acc, cur) => {
      return acc.call(context, cur); 
    }, fn);
  }
}

uncurry 接收一个函数 fn,返回一个函数。这个函数利用reduce不停调用 fn 并传入参数,Untilargs所有参数都传给 fn


利用这个模式可以轻松实现反柯里化:


const curriedAdd = a => b => a + b;

const uncurriedAdd = uncurry(curriedAdd);
uncurriedAdd(1, 2); // 3

应用场景



  1. 统一接口规范


有时我们会从其他模块接收到一个柯里化的函数,但我们的接口需要一个普通的多参数函数。这时可以通过反柯里化来实现统一。


// 模块A导出
export const curriedGetUser = id => callback => {
  // 调用callback(user)
};

// 模块B中
import { curriedGetUser } from './moduleA';

// 反柯里化以符合接口
const getUser = uncurry(curriedGetUser); 

getUser(123user => {
  // use user
});


  1. 提高参数灵活性


反柯里化可以让我们以任意顺序 passes 入参数,增加了函数的灵活性。


const uncurriedLog = uncurry(console.log);

uncurriedLog('a''b'); 
uncurriedLog('b''a'); // 参数顺序灵活


  1. 支持默认参数


柯里化函数不容易实现默认参数,而反柯里化后可以方便地设置默认参数。


function uncurriedRequest(url, method='GET', payload) {
  // 请求逻辑
}

大厂面试题解析


实现add(1)(2)(3)输出6的函数


这是一道典型的柯里化面试题。解析:


function curry(fn) {
  return function curried(a) {
    return function(b) {
      return fn(a, b);
    }
  }
}

function add(a, b) {
  return a + b;
}

const curriedAdd = curry(add);

curriedAdd(1)(2)(3); // 6

利用柯里化技术,我们可以将普通的 add 函数转化为 curriedAdd,它每次只接收一个参数,并返回函数等待下一个参数,从而实现了 add(1)(2)(3) 的效果。


实现单参数compose函数


compose函数可以将多个函数合并成一个函数,这也是一道常见的柯里化面试题。解析:


function compose(fn1) {
  return function(fn2) { 
    return function(x) {
      return fn1(fn2(x));
    };
  };
}

function double(x) {
  return x * 2;
}

function square(x) {
  return x * x;
}

const func = compose(double)(square);

func(5); // 50

利用柯里化,我们创建了一个单参数的 compose 函数,它每次返回一个函数等待下一个函数参数。这样最终实现了 compose(double)(square) 的效果。


反柯里化Function.bind


Function.bind 函数实现了部分参数绑定,这本质上是一个反柯里化的过程。解析:


Function.prototype.uncurriedBind = function(context) {
  const fn = this;
  return function(...args) {
    return fn.call(context, ...args);
  } 
}

function greet(greeting, name) {
  console.log(greeting, name);
}

const greetHello = greet.uncurriedBind('Hello');
greetHello('John'); // Hello John

uncurriedBind 通过递归调用并传参实现了反柯里化,使 bind 参数从两步变成一步传入,这也是 Function.bind 的工作原理。


总结


柯里化和反柯里化都是非常有用的编程技巧,让我们可以写出更加灵活通用的函数。理解这两种技术的实现原理可以帮助我们更好地运用它们。在编码中,我们可以根据需要决定是将普通函数柯里化,还是将柯里化函数反柯里化。合理运用这两种技术可以大大

作者:一码平川哟
来源:juejin.cn/post/7262349502920605753
提高我们的编程效率。

收起阅读 »

【KRouter】一个简单轻量的 Kotlin 路由框架

KRouter(Kotlin-Router) 是一个非常轻量级的 Kotlin 路由框架。具体而言,KRouter 是一个通过 URI 发现接口实现类的框架。就像这样:val homeScreen = KRouter.route<Screen>("...
继续阅读 »

KRouter(Kotlin-Router) 是一个非常轻量级的 Kotlin 路由框架

具体而言,KRouter 是一个通过 URI 发现接口实现类的框架。就像这样:

val homeScreen = KRouter.route<Screen>("screen/home?name=zhangke")

起因是段时间用 Voyager 时发现模块间的互相通信没这么灵活,需要一些配置,以及 DeepLink 的使用也有点奇怪,相比较而言我更希望能用路由的方式来实现模块间通信,于是就有了这个库。

github.com/0xZhangKe/K…

主要通过 KSP、ServiceLoader 以及反射实现。

使用

上面的那行代码几乎就是全部的使用方式了。

正如上面说的,这个是用来发现接口实现类并且通过 URI 匹配目的地的库,那么我们需要先定义一个接口。

interface Screen

然后我们的项目中与很多各自独立的模块,他们都会实现这个接口,并且每个都有所不同,我们需要通过他们各自的路由(即 URI )来进行区分。

// HomeModule
@Destination("screen/home")
class HomeScreen(@Router val router: String = "") : Screen

// ProfileModule
@Destination("screen/profile")
class ProfileScreen : Screen {
@Router
lateinit var router: String
}

现在我们的两个独立的模块都有了各自的 Screen 了,并且他们都有自己的路由地址。

val homeScreen = KRouter.route<Screen>("screen/home?name=zhangke")
val profileScreen = KRouter.route<Screen>("screen/profile?name=zhangke")

现在就可以通过 KRouter 拿到这两个对象了,并且这两个对象中的 router 属性会被赋值为具体调用 KRouter.route 时的路由。这样你就可以在 HomeScreen 以及 ProfileScreen 拿到通过 uri 传的参数了,然后可以使用这些参数做一些初始化之类的操作。

@Destination

Destination 注解用于注解一个目的地,它包含两个参数:

  • route: 目的地的唯一标识的路由地址,必须是个 URI 类型的 String,不需要包含 query。
  • type : 路由目的地的接口,如果这个类只有一个父类或接口的话是不用设置这个参数的,可以自动推断出来,但如果包含多个父类就需要通过 type 显示指定了。

然后还有个很重要的点,Destination 注解的类,也就是目的地类,必须包含一个无参构造器,否则 ServiceLoader 无法创建对象,对于 Kotlin 类来说,需要保证构造器中的每个入参都有默认值。

@Router

Router 注解用于表示目的地类中的那个属性是用来接受传入的 router 参数的,该属性必须是 String 类型。

标记了该注解的属性会被自动赋值,也可以不设置改注解。

举例来说,上面的例子中的 HomeScreen 对象被创建完成后,其 router 字段的值为 screen/home?name=zhangke

特别注意,如果 @Router 注解的属性不在构造器中,那么需要设置为可修改的,即 Kotlin 中的 var 修饰的变量属性。

KRouter

KRouter 是个单例类,其中只有一个方法。

inline fun <reified T : Any> route(router: String): T?

包含一个范形以及一个路由地址,路由地址可以包含 query 也可以不包含,匹配目的地时会忽略 query 字段。

匹配成功后会通过这个 uri 构建对象,并将 uri 传递给改对象中的 @router 注解标注的字段。

集成

首先需要在项目中集成 KSP

然后添加依赖:

// module's build.gradle.kts
implementation("com.github.0xZhangKe.KRouter:core:0.1.5")
ksp("com.github.0xZhangKe.KRouter:compiler:0.1.5")

因为是使用了 ServiceLoader ,所以还需要设置 SourceSet。

// module's build.gradle.kts
kotlin {
sourceSets.main {
resources.srcDir("build/generated/ksp/main/resources")
}
}

或许你还需要添加 JitPack 仓库:

maven { setUrl("https://jitpack.io") }

原理

正如上面所说,本框架主要使用 ServiceLoader + KSP + 反射实现。

框架主要包含两部分,一是编译阶段的部分,二是运行时部分。

KSP 插件

KSP 插件相关的代码在 compiler 模块。

KSP 插件的主要作用是根据 Destination 注解生成 ServiceLoader 的 services 文件

KSP 的其他代码基本都差不多,主要就是先配置 services 文件,然后根据注解获取到类,然后通过 Visitor 遍历处理,我们直接看 KRouterVisitor 即可。

override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
val superTypeName = findSuperType(classDeclaration)
writeService(superTypeName, classDeclaration)
}

在 visitClassDeclaration 方法主要做两件事情,第一是获取父类,第二是写入或创建 services 文件。

流程就是先获取 type 指定的父类,没有就判断只有一个父类就直接返回,否则抛异常。

// find super-type by type parameter
val routerAnnotation = classDeclaration.requireAnnotation<Destination>()
val typeFromAnnotation = routerAnnotation.findArgumentTypeByName("type")
?.takeIf { it != badTypeName }

// find single-type
if (classDeclaration.superTypes.isSingleElement()) {
val superTypeName = classDeclaration.superTypes
.iterator()
.next()
.typeQualifiedName
?.takeIf { it != badSuperTypeName }
if (!superTypeName.isNullOrEmpty()) {
return superTypeName
}
}

获取到之后我们需要按照 ServiceLoader 的要求将接口或抽象类的权限定名作为文件名创建一个文件。

然后再将实现类的权限定名写入该文件。

val resourceFileName = ServicesFiles.getPath(superTypeName)
val serviceClassFullName = serviceClassDeclaration.qualifiedName!!.asString()
val existsFile = environment.codeGenerator
.generatedFile
.firstOrNull { generatedFile ->
generatedFile.canonicalPath.endsWith(resourceFileName)
}
if (existsFile != null) {
val services = existsFile.inputStream().use { ServicesFiles.readServiceFile(it) }
services.add(serviceClassFullName)
existsFile.outputStream().use { ServicesFiles.writeServiceFile(services, it) }
} else {
environment.codeGenerator.createNewFile(
dependencies = Dependencies(aggregating = false, serviceClassDeclaration.containingFile!!),
packageName = "",
fileName = resourceFileName,
extensionName = "",
).use {
ServicesFiles.writeServiceFile(setOf(serviceClassFullName), it)
}
}

这样就自动生成了 ServiceLoader 所需要的 services 文件了。

KRouter

KRouter 主要做三件事情:

  • 通过 ServiceLoader 获取接口所有的实现类。
  • 通过 URI 匹配具体的目的地类。
  • 通过 URI 构建目的地类对象。

第一件事情很简单:

inline fun <reified T> findServices(): List<T> {
val clazz = T::class.java
return ServiceLoader.load(clazz, clazz.classLoader).iterator().asSequence().toList()
}

获取到之后就可以通过 URL 来开始匹配。

匹配方式就是获取每个目的地类的 Destination 注解中的 router 字段,然后与路由进行对比。

fun findServiceByRouter(
serviceClassList: List<Any>,
router: String,
): Any? {
val routerUri = URI.create(router).baseUri
val service = serviceClassList.firstOrNull {
val serviceRouter = getRouterFromClassAnnotation(it::class)
if (serviceRouter.isNullOrEmpty().not()) {
val serviceUri = URI.create(serviceRouter!!).baseUri
serviceUri == routerUri
} else {
false
}
}
return service
}

private fun getRouterFromClassAnnotation(targetClass: KClass<*>): String? {
val routerAnnotation = targetClass.findAnnotation<Destination>() ?: return null
return routerAnnotation.router
}

因为匹配策略是忽略 query 字段,所以只通过 baseUri 匹配即可。

下面就是创建对象,这里有两种情况需要考虑。

第一是 @Router 注解在构造器中,这种情况需要重新使用构造器创建对象。

第二种是 @Router 注解在普通属性中,此时直接使用 ServiceLoader 创建好的对象然后赋值即可。

如果在构造器中,先获取 routerParameter 参数,然后通过 PrimaryConstructor 重新创建对象即可。

private fun fillRouterByConstructor(router: String, serviceClass: KClass<*>): Any? {
val primaryConstructor = serviceClass.primaryConstructor
?: throw IllegalArgumentException("KRouter Destination class must have a Primary-Constructor!")
val routerParameter = primaryConstructor.parameters.firstOrNull { parameter ->
parameter.findAnnotation<Router>() != null
} ?: return null
if (routerParameter.type != stringKType) errorRouterParameterType(routerParameter)
return primaryConstructor.callBy(mapOf(routerParameter to router))
}

如果是普通的变量属性,那么先获取到这个属性,然后做一些类型权限之类的校验,然后调用 setter 赋值即可。

private fun fillRouterByProperty(
router: String,
service: Any,
serviceClass: KClass<*>,
): Any? {
val routerProperty = serviceClass.findRouterProperty() ?: return null
fillRouterToServiceProperty(
router = router,
service = service,
property = routerProperty,
)
return service
}

private fun KClass<*>.findRouterProperty(): KProperty<*>? {
return declaredMemberProperties.firstOrNull { property ->
val isRouterProperty = property.findAnnotation<Router>() != null
isRouterProperty
}
}

private fun fillRouterToServiceProperty(
router: String,
service: Any,
property: KProperty<*>,
) {
if (property !is KMutableProperty<*>) throw IllegalArgumentException("@Router property must be non-final!")
if (property.visibility != KVisibility.PUBLIC) throw IllegalArgumentException("@Router property must be public!")
val setter = property.setter
val propertyType = setter.parameters[1]
if (propertyType.type != stringKType) errorRouterParameterType(propertyType)
property.setter.call(service, router)
}

OK,以上就是关于 KRouter 的所有内容了。


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

一篇文章了解Kotlin的泛型

Kotlin 泛型类型Kotlin 的泛型特性允许我们编写出更加灵活和通用的代码,提高了代码的可重用性和类型安全性。本文将介绍 Kotlin 中的四种泛型类型类型参数星号投影型变泛型限制类型参数定义一个泛型类或函数时,使用尖括号 < >&...
继续阅读 »

Kotlin 泛型类型

Kotlin 的泛型特性允许我们编写出更加灵活和通用的代码,提高了代码的可重用性和类型安全性。

本文将介绍 Kotlin 中的四种泛型类型

  • 类型参数
  • 星号投影
  • 型变
  • 泛型限制

类型参数

定义一个泛型类或函数时,使用尖括号 < > 来指定类型参数。例如,以下是一个将泛型类型 T 用作参数的示例:

class MyList<T> { ... }

在这个示例中,T 是一个占位符类型参数,用于表示某个类型。在使用该类时,可以通过指定实际的类型参数来创建具体类型的实例。例如:

val list = MyList<String>()

在这个示例中,我们创建了一个 MyList 类型的实例,并将 String 类型指定为其类型参数。这意味着 list 变量可以存储 String 类型的元素。

星号投影

星号投影是一种特殊语法,用于表示您不关心实际类型参数的情况。通过使用 * 替代类型参数,您可以指定该参数将被忽略。例如,以下是一个使用星号投影的示例:

fun printList(list: List<*>) {
for (item in list) {
println(item)
}
}

在这个示例中,printList 函数接收一个 List<*> 类型的参数,该类型使用星号投影来表示它可以存储任何类型的元素。循环遍历该列表,并将每个元素输出到控制台。

型变

型变是指泛型类型之间的继承关系。在 Kotlin 中,有三种型变:in、out 和 invariant。这些型变用于描述子类型和超类型之间的关系,并影响如何将泛型类型赋值给其他类型。

  1. in 型变:用于消费型位置(比如方法参数),表示只能从泛型类型中读取数据,不能写入数据。

    interface Source<out T> {
    fun next(): T
    }

    fun demo(strs: Source<String>) {
    val objects: Source<Any> = strs
    // ...
    }

    在这个示例中,我们定义了一个泛型接口 Source,并使用 out 关键字将其标记为协变类型。这意味着我们可以将 Source<String> 类型的对象视为 Source<Any> 类型的对象,并将其赋值给 objects 变量。

  2. out 型变:用于生产型位置(比如返回值),表示只能向泛型类型中写入数据,不能读取数据。

    interface Sink<in T> {
    fun put(element: T)
    }

    fun demo(sinkOfAny: Sink<Any>) {
    val sinkOfString: Sink<String> = sinkOfAny
    // ...
    }

    在这个示例中,我们定义了一个泛型接口 Sink,并使用 in 关键字将其标记为逆变类型。这意味着我们可以将一个 Sink<Any> 类型的对象视为 Sink<String> 类型的对象,并将其赋值给 sinkOfString 变量。

  3. invariant 型变:默认情况下,Kotlin 中的泛型类型都是不变(invariant)的。这意味着不能将一个 List<String> 类型的对象视为 List<Any> 类型的对象。

泛型限制

泛型限制用于约束泛型类型可以具体化为哪些类型。例如,使用 where 关键字可以给泛型类型添加多个限制条件。以下是一个使用泛型限制的示例:

fun <T> showItems(list: List<T>) where T : CharSequence, T : Comparable<T> {
list.filter { it.length > 5 }.sorted().forEach(::println)
}

在这个示例中,我们定义了一个名为 showItems 的函数,它接受一个 List<T> 类型的参数,并对该列表进行过滤、排序和输出操作。其中,T 是一个泛型类型参数,用于表示列表中的元素类型。

为了限制 T 的类型,我们使用 where 关键字并添加了两个限制条件:T 必须实现 CharSequence 接口和 Comparable 接口。这意味着当我们调用 showItems 函数时,只能传递那些既实现了 CharSequence 接口又实现了 Comparable 接口的类型参数。

需要注意的是,在 Kotlin 中使用泛型限制时,限制条件必须放在 where 关键字之后,并且使用逗号 , 分隔各个限制条件。如果有多个限制条件,建议将它们放在新行上,以提高代码的可读性。


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

优雅可拓展的登录封装,让你远离if-else

前言Hi,大家好,我是你们的秃头朋友程序员小甲,相信各位码农朋友在搭建从0到1项目时在搭建完基建等任务后,最先去做的都是去搭建系统的用户体系,那么每一个码农朋友都会去编码属于自己系统的一套用户登录注册体系;但是登录方式极其多样,光小甲一个人对接的就有googl...
继续阅读 »

前言

Hi,大家好,我是你们的秃头朋友程序员小甲,相信各位码农朋友在搭建从0到1项目时在搭建完基建等任务后,最先去做的都是去搭建系统的用户体系,那么每一个码农朋友都会去编码属于自己系统的一套用户登录注册体系;但是登录方式极其多样,光小甲一个人对接的就有google登录,苹果登录,手机验证码,微信验证码登录,微博登录等各种各样的登录;

针对这么多的登录方式,小甲是如何进行功能接入的呢?(Ps:直接switch-case和if-else接入不香吗,又不是不能用,这其实是小甲做功能时最真实的想法了,但是迫于团队老大哥的强大气场,小甲自然不敢这样硬核编码了),接下来就让秃头小甲和大伙一起分享一下是怎么让普普通通的登录也能玩出逼格的!(由于篇幅过长,接下来进入硬核时刻,希望各位能挺住李云龙二营长的意大利跑前进哈)

功能实现

技术栈:SpringBoot,MySQL,MyBatisPlus,hutool,guava,Redis,Jwt,Springboot-emial等;

sdk组件架构

项目结构包:

  1.    tea-api(前台聚合服务)
  2.    tea-mng(后管聚合服务)
  3.    tea-sdk(SpringBoot相关组件模块)
  4.    tea-common(公共模块,提供一些工具类支持和公有类引用)

项目结构引用关系: sdk引入了common包,api和mng引入了sdk包;

封装思路

思路一:通过前端登录路由请求头key值通过反射生成对应的LoginProvider类来进行登录业务逻辑的执行。具体的做法如下:

  1. 在classPath路径下新增一个json/Provider.json文件,json格式如下图所示:

1683047225979.png

  1. 定义具体的Provider继承基类Provider,秃头小甲这里定义了一般业务系统最常对接的集中Provider(PS:由于google登录和App登录主要是用于对接海外业务,因此小甲这里就没把集成代码放出来了)如下图是小甲定义的几个Provider:

1683047738587.png

其中UserLoginService是所有Provider的基类接口,封装了模板方法。EmialLoginProvider类主要是实现邮箱验证码登录,PasswordProvider用于实现账号密码登录,PhoneLoginProvider是用于手机号验证码登录.WbLoginProvider用于实现PC端微博授权登录,WxLoginPrvider用于实现微信PC端授权登录;

3.EmailLoginProvider和PhoneLoginProvider需要用到验证码校验,因此需要实现UserLoginService接口的验证码获取,并将获取到的验证码存储到redis中;

4.将前端的路由gateWay作为key值,需要加载的动态类名作为value值。定义一个LoginService业务处理类,类中定义一个Map缓存对象,在bean注入加载到ioc容器时,通过读取解析json文件对Map缓存进行反射属性注入,该设计理念参考了Springboot的SPI注入原理以此实现对Provider的可拔插操作;

思路二:

  1. 通过SpringBoot事件监听机制,通过前端路由请求头的key值发布生成不同的ApplicationEvent事件,利用事件监听对业务处理解耦;
  2. 定义具体的Event事件以及Listener;
  3. 根据前端路由gateWay值生成需要发布的Event事件基类,在具体的listener类上根据@EventListener注解来对具体的事件进行监听处理;

思路对比

思路一通过模板+工厂+反射等设计模式的原理对多方式登录方式来达到解耦和拓展,从而规避了开发人员大量的if-else或switch等硬编码的方式,思路二通过模板+工厂+事件监听机制等设计模式也做到了对多方式登录的解耦和拓展,两种思路均能做到延伸代码的拓展性的作用;

封装源码

1.基类UserLoginService

/**
* 登录
*
* @param req 登录请求体
* @return
*/
LoginResp login(LoginReq req);


/**
* 验证码获取
*
* @param req 登录请求体
* @return
*/
LoginResp vertifyCode(LoginReq req);

2.拓展类Provider代码

public class EmailLoginProvider implements UserLoginService {

@Override
public LoginResp login(LoginReq req) {
UserService userService = SpringUtil.getBean(UserService.class);
User user = userService.getOne(Wrappers.lambdaQuery(new User()).eq(User::getEmail, req.getEmail()).eq(User::getStatus, 1));
if (Objects.isNull(user)) {
return null;
}
String redisKey = req.getEmail();
RedisTemplate redisTemplate = SpringUtil.getBean(StringRedisTemplate.class);
String code = (String) redisTemplate.opsForValue().get(redisKey);
if (StringUtils.isEmpty(code)||!code.equals(req.getCode())) {
return null;
}
String token = JwtParse.getoken(user);
LoginResp resp = new LoginResp();
resp.setToken(token);
return resp;
}

@Override
public LoginResp vertifyCode(LoginReq req) {
String redisKey = req.getEmail();
LoginResp resp = new LoginResp();
RedisTemplate redisTemplate = SpringUtil.getBean(StringRedisTemplate.class);
String code = (String) redisTemplate.opsForValue().get(redisKey);
if (StringUtils.isNotEmpty(code)) {
resp.setCode(code);
return resp;
}
MailService mailService = SpringUtil.getBean(MailService.class);
String mailCode = CodeUtils.make(4);
mailService.sendMail(req.getEmail(), "邮箱验证码", mailCode);
redisTemplate.opsForValue().set(req.getEmail(), mailCode);
return resp;
}
}
public class PasswordProvider implements UserLoginService {

@Override
public LoginResp login(LoginReq req) {
UserService userService = SpringUtil.getBean(UserService.class);
User user = userService.getOne(Wrappers.lambdaQuery(new User()).eq(User::getPassword, req.getPassword()).eq(User::getStatus, 1));
if (Objects.isNull(user)) {
return null;
}
String token = JwtParse.getoken(user);
LoginResp resp = new LoginResp();
resp.setToken(token);
return resp;
}

@Override
public LoginResp vertifyCode(LoginReq req) {
return null;
}
}
public class PhoneLoginProvider implements UserLoginService {

@Override
public LoginResp login(LoginReq req) {
UserService userService = SpringUtil.getBean(UserService.class);
User user = userService.getOne(Wrappers.lambdaQuery(new User()).eq(User::getPhone, req.getPhone()).eq(User::getStatus, 1));
if (Objects.isNull(user)) {
return null;
}
String redisKey = req.getPhone();
RedisTemplate redisTemplate = SpringUtil.getBean(RedisTemplate.class);
String code = (String) redisTemplate.opsForValue().get(redisKey);
if (!code.equals(req.getCode())) {
return null;
}
String token = JwtParse.getoken(user);
LoginResp resp = new LoginResp();
resp.setToken(token);
return resp;
}

@Override
public LoginResp vertifyCode(LoginReq req) {
String redisKey = req.getPhone();
LoginResp resp = new LoginResp();
RedisTemplate redisTemplate = SpringUtil.getBean(RedisTemplate.class);
String code = (String) redisTemplate.opsForValue().get(redisKey);
if (StringUtils.isNotEmpty(code)) {
resp.setCode(code);
return resp;
}
MailService mailService = SpringUtil.getBean(MailService.class);
String mailCode = CodeUtils.make(4);
mailService.sendMail(req.getPhone(), "手机登录验证码", mailCode);
redisTemplate.opsForValue().set(req.getEmail(), mailCode);
return resp;
}
}
public class WxLoginProvider implements UserLoginService {

@Override
public LoginResp login(LoginReq req) {
WxService wxService = SpringUtil.getBean(WxService.class);
WxReq wxReq = new WxReq();
wxReq.setCode(req.getAuthCode());
WxResp token = wxService.getAccessToken(wxReq);
String accessToken = token.getAccessToken();
if (StringUtils.isEmpty(accessToken)) {

}
wxReq.setOpenid(token.getOpenid());
WxUserInfoResp userInfo = wxService.getUserInfo(wxReq);
//根据unionId和openid查找一下当前用户是否已经存在系统,如果不存在,帮其注册这里单纯是为了登录;
UserService userService = SpringUtil.getBean(UserService.class);
User user = userService.getOne(Wrappers.lambdaQuery(new User()).eq(User::getOpenId, token.getOpenid()).eq(User::getUnionId, token.getUnionId()));
if (Objects.isNull(user)) {

}
String getoken = JwtParse.getoken(user);
LoginResp resp = new LoginResp();
resp.setToken(getoken);
return resp;
}

@Override
public LoginResp vertifyCode(LoginReq req) {
return null;
}
}

3.接口暴露Service--LoginService源码

@Service
@Slf4j
public class LoginService {

private Map<String, UserLoginService> loginServiceMap = new ConcurrentHashMap<>();

@PostConstruct
public void init() {
try {
List<JSONObject> jsonList = JSONArray.parseObject(ResourceUtil.getResource("json/Provider.json").openStream(), List.class);
for (JSONObject object : jsonList) {
String key = object.getString("key");
String className = object.getString("value");
Class loginProvider = Class.forName(className);
UserLoginService loginService = (UserLoginService) loginProvider.newInstance();
loginServiceMap.put(key, loginService);
}
} catch (Exception e) {
log.info("[登录初始化异常]异常堆栈信息为:{}", ExceptionUtils.parseStackTrace(e));
}
}

/**
* 统一登录
*
* @param gateWayRoute 路由路径
* @param req 登录请求
* @return
*/
public RetunrnT<LoginResp> login(String gateWayRoute, LoginReq req) {
UserLoginService userLoginService = loginServiceMap.get(gateWayRoute);
LoginResp loginResp = userLoginService.login(req);
return RetunrnT.success(loginResp);
}


/**
* 验证码发送
*
* @param gateWayRoute 路由路径
* @param req 登录请求
* @return
*/
public RetunrnT<LoginResp> vertifyCode(String gateWayRoute, LoginReq req) {
UserLoginService userLoginService = loginServiceMap.get(gateWayRoute);
LoginResp resp = userLoginService.vertifyCode(req);
return RetunrnT.success(resp);
}

}

4.邮件发送Service具体实现--MailService

public interface MailService {

/**
* 发送邮件
*
* @param to 收件人
* @param subject 主题
* @param content 内容
*/
void sendMail(String to, String subject, String content);
}
@Service
@Slf4j
public class MailServiceImpl implements MailService {

/**
* Spring Boot 提供了一个发送邮件的简单抽象,直接注入即可使用
*/
@Resource
private JavaMailSender mailSender;
/**
* 配置文件中的发送邮箱
*/
@Value("${spring.mail.from}")
private String from;

@Override
@Async
public void sendMail(String to, String subject, String content) {
//创建一个邮箱消息对象
SimpleMailMessage message = new SimpleMailMessage();
//邮件发送人
message.setFrom(from);
//邮件接收人
message.setTo(to);
//邮件主题
message.setSubject(subject);
//邮件内容
message.setText(content);
//发送邮件
mailSender.send(message);
log.info("邮件发成功:{}", message.toString());
}
}

5.token生成JsonParse类

private static final String SECRECTKEY = "zshsjcbchsssks123";

public static String getoken(User user) {
//Jwts.builder()生成
//Jwts.parser()验证
JwtBuilder jwtBuilder = Jwts.builder()
.setId(user.getId() + "")
.setSubject(JSON.toJSONString(user)) //用户对象
.setIssuedAt(new Date())//登录时间
.signWith(SignatureAlgorithm.HS256, SECRECTKEY).setExpiration(new Date(System.currentTimeMillis() + 86400000));
//设置过期时间
//前三个为载荷playload 最后一个为头部 header
log.info("token为:{}", jwtBuilder.compact());
return jwtBuilder.compact();
}

6.微信认证授权Service---WxService


public interface WxService {

/**
* 通过code获取access_token
*/
WxResp getAccessToken(WxReq req);

/**
* 通过accessToken获取用户信息
*/
WxUserInfoResp getUserInfo(WxReq req);
}
@Service
@Slf4j
public class WxServiceImpl implements WxService {

@Resource
private WxConfig wxConfig;


@Override
public WxResp getAccessToken(WxReq req) {
req.setAppid(wxConfig.getAppid());
req.setSecret(wxConfig.getSecret());
Map map = JSON.parseObject(JSON.toJSONString(req), Map.class);
WxResp wxResp = JSON.parseObject(HttpUtil.createGet(wxConfig.getTokenUrl()).formStr(map).execute().body(), WxResp.class);
return wxResp;
}

@Override
public WxUserInfoResp getUserInfo(WxReq req) {
req.setAppid(wxConfig.getAppid());
req.setSecret(wxConfig.getSecret());
Map map = JSON.parseObject(JSON.toJSONString(req), Map.class);
return JSON.parseObject(HttpUtil.createGet(wxConfig.getGetUserUrl()).formStr(map).execute().body(), WxUserInfoResp.class);
}
}

功能演练

1683049554852.png

项目总结

相信很多小伙伴在平时开发过程中都能看到一定的业务硬核代码,前期设计不合理,后续开发只能在前人的基础上不断的进行if-else或者switch来进行业务的功能拓展,千里之行基于跬步,地基不稳注定是要地动山摇的,希望在接下来的时光,秃头小甲也能不断提升自己的水平,写出更多有水准的代码;

碎碎念时光

首先很感谢能看完全篇幅的各位老铁兄弟们,希望本篇文章能对各位和秃头小甲一样码农有所帮助,当然如果各位技术大大对这模块做法有更优质的做法的,也欢迎各位技术大大能在评论区留言探讨,写在最后~~~~~~ 创作不易,希望各位老铁能不吝惜于自己的手指,帮秃头点下您宝贵的赞把!


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

Kotlin的语法糖到底有多甜?

JYM大家好,好久没来写文了。今天带给大家 Kotlin 的内容,可能一些常关注我的朋友也发现了,在我之前的文章中就开始用 Kotlin 代码做代码示例了,这是因为最近一年我都在高强度使用 Kotlin 进行后端开发。相信很多安卓开发的朋友早就开始用上 Kot...
继续阅读 »

JYM大家好,好久没来写文了。

今天带给大家 Kotlin 的内容,可能一些常关注我的朋友也发现了,在我之前的文章中就开始用 Kotlin 代码做代码示例了,这是因为最近一年我都在高强度使用 Kotlin 进行后端开发。

相信很多安卓开发的朋友早就开始用上 Kotlin 了,但是许多后端对这门语言应该还是不太了解,反正在我的朋友圈里没有见到过用 Kotlin 写后端的程序员存在,在我亲身用了一年 Kotlin 之后已经不太想用 Java 进行代码开发了,起码在开发效率方面就已经是天差地别了,所以今天特地给大家分享一下 Kotlin 的好,希望能带领更多人入坑。

1. 第一段代码

很多人说 Kotlin 就是披了一层语法糖的 Java,因为它百分百兼容 Java,甚至可以做到 Kotlin 调用 Java 代码。

其实我对这个说法是赞同的,但是又不完全一样,因为 Kotlin 有自己的语法、更有自己的编译器、还有着多端支持,更有着自己的设计目标。

我更倾向于把 Kotlin 看成一个 JVM 系语言,就像 Scala 语言一样,只是恰好 Kotlin 有一个设计目标就是百分百兼容 Java。

在语言层面,Kotlin 几乎是借鉴了市面上所有的现代化语言的强大特性,协程、函数式、扩展函数、空安全,这些广受好评的特性全部都有。

而且从我个人感受来看,我用过 Java、Kotlin、JS、Go、Dart、TS、还有一点点 Python,我觉得 JS 和 Kotlin 的语法是比较方便易用的,Dart、Java 和 Go 的语法都不是太方便易用,语法的简单也在一定程度上减少了开发者的心智负担。

下面我将用一段代码,来简单说明一下 Kotlin 常见的语法特性:

fun main(args: Array<String>) {
val name = "Rookie"

// Hello World! Rookie
println("Hello World! $name")

// Hello World! 82,111,111,107,105,101
println("Hello World! ${name.chars().toList().joinToString(",")}")

test(createPerson = { Person("Rookie", 25) })

}

data class Person(
var name: String = "",
var age: Int = 0,
)

fun test(createPerson: () -> Person, test : String = "test"): Person {

val person = createPerson()

// Person(name=Rookie, age=25)
println(person)

return person
}

上面是一段简简单单的 Kotlin 代码,但是却可以看出 Kotlin 的很多特性,请听我娓娓道来~

  1. Kotlin 的启动方法也是 main 函数,但是 Kotlin 移除了所有基础类型,一切皆对象,比如 Java 中的数组对应的就是 Array 类,int 对应的是 Int 类。
  2. Kotlin 使用类型推断来声明类型,一共有两个关键字,val 代表这是一个不可变变量,var 代表这是一个可变的变量,这两个关键字选用我感觉比 JS 还要好。
  3. Kotlin 代码每一行不需要英文分号结尾。
  4. Kotlin 支持字符串模板,可以直接字符串中使用 '$' 符号放置变量,如果你想放置一个函数的计算结果,需要用 '${}' 来包裹。
  5. Kotlin 是一个函数式语言,支持高阶函数,闭包、尾递归优化等函数式特性。
  6. Kotlin 为了简化 Java Bean,支持了数据类 data class,它会自动生成无参构造、getter、setter、equals()、hashCode()、copy()、toJSON()、toString() 方法。
  7. Kotlin 的函数关键字是 fun,返回值在函数的最后面,变量名在类型的前面,几乎新兴语言都是这样设计的,可以明显感受到语言设计者想让我们更多关注业务含义而非数据类型。
  8. Kotlin 具有一些类似 go 和 Python 的内置函数,比如 println。
  9. Kotlin 的函数参数支持默认值。
  10. Kotlin 不支持多参数返回,但是为了解决这个问题它内置了两个类:Pair 和 Triple,分别可以包装两个返回值和三个返回值。

2. 基础常用特性

了解了一些 Kotlin 的基础语法之后,我再来介绍一些常用的基础特性。

第一个就是空安全和可空性。

Kotlin 中的变量可以声明为非空和可空,默认的声明都是非空,如果需要一个变量可空的,需要在类型后面加一个问号,就像这样:

fun start() {
val name1 : String = ""
val name : String? = null
}

函数的参数声明也一样,也会区分非空和可空,Kotlin 编译器会对代码上下文进行检查,在函数调用处也会对变量是否可空进行一致性检查,如果不通过则会有编译器提醒,我是强烈建议不用可空变量,一般都可以通过默认值来处理。

那么如果你接手的是前人代码,他声明变量为可空,但是希望为空的时候传递一个默认值,则可以使用这个语法进行处理:

fun start() {
val name : String? = null
println(name ?: "Rookie")
}

这是一个类似三元表达式的语法(Elvis 运算符),在 Kotlin 中极其常见,除此之外你还可以进行非空调用:

fun start() {
val name : String? = null
println(name?.chars() ?: "Rookie")
}

这段代码就表示:如果变量不为空就调用 chars 方法,如果为空则返回默认值 Rookie,在所有可空变量上都支持这种写法,并且支持链式调用。

第二个常用特性是异常处理, 写到这里突然想到了一个标题,Kotlin 的异常处理,那叫一个优雅!!!

fun start() {
val person = runCatching {
test1()
}.onFailure {

}.onSuccess {

}.getOrNull() ?: Person("Rookie", 25)
}

fun test1() : Person {
return Person()
}

这一段代码中的 test1 方法你可以当作一个远程调用方法或者逻辑方法,对了,这里隐含了一个语法,就是一个函数中的最后一行的计算结果是它的返回值,你不需要显示的去写 return。

我使用 runCatching 包裹我们的逻辑方法,然后有三个链式调用:

  1. onFailure:当逻辑方法报错时会进入这个方法。
  2. onSuccess:当逻辑方法执行成功时会进入这个方法。
  3. getOrNull:当逻辑方法执行成功时正常返回,执行失败时返回一个空变量,然后我们紧跟一个 ?: ,这代表当返回值为空时我们返回自定义的默认值。

如此一来,一个异常处理的闭环就完成了,每一个环节都会被考虑到,这些链式调用的方法都是可选的,如果你不手动调用处理会有默认的处理方式,大家伙觉得优雅吗?

第三个特性是改进后的流程控制。

fun start() {
val num = (1..100).random()
val name = if (num == 1) "1" else { "Rookie" }
val age = when (num) {
1 -> 10
2 -> 20
else -> { (21..30).random() }
}
}

我们先声明一个随机数,然后根据条件判断语句返回不同的值,其中 Java 中的 Switch 由 When 语法来替代。

而且这里每一段表达式都可以是一个函数,大家可以回忆一下,如果你使用 Java 来完成通过条件返回不同变量的逻辑会有多麻烦。

如果大家在不了解 Kotlin 的情况下尝试用更简单的方式来写逻辑,可以问问类似 ChatGPT 这种对话机器人来进行辅助你。

3. 常用内置函数

就像每个变量类型都有 toString 方法一样,Kotlin 中的每个变量都具有一些内置的扩展函数,这些函数可以极大的方便我们开发。

apply和also

fun start() {
val person = Person("Rookie", 25)

val person1 = person.apply {
println("name : $name, age : $age, This : $this")
}

val person2 = person.also {
println("name : ${it.name}, age : ${it.age}, This : $it")
}
}

这两个函数调用之后都是执行函数体后返回调用变量本身,不同的是 apply 的引用为 this,内部取 this 变量时不需要 this.name 可以直接拿 name 和 age 变量。

而 also 函数则默认有一个 it,it 就是这个变量本身的引用,我们可以通过这个 it 来获取相关的变量和方法。

run 和 let

fun start() {
val person = Person("Rookie", 25)

val person1 = person.run {
println("name : $name, age : $age, This : $this")
"person1"
}

val person2 = person.let {
println("name : ${it.name}, age : ${it.age}, This : $it")
"person2"
}
}

run 函数和 let 函数都支持返回与调用变量不同的返回值,只需要将返回值写到函数最后一行或者使用 return 语句进行返回即可,上例中 person 变量进行调用之后的返回结果就是一个 String 类型。

在使用上的具体差异也就是引用对象的指向不同,具体更多差异可以看一下网络上总结,我这里表明用法就可以了。

除了这四个函数之外,还有许多的类似函数帮我们来做一些很优雅的代码处理和链式调用,但是我根本没有用过其它的函数,这四个函数对我来说已经足够了,有兴趣的朋友可以慢慢发掘。

4. 扩展函数与扩展属性

上文了我们举了几个常见的内置函数,其实他们都是使用 Kotlin 的扩展函数特性实现的。

所谓扩展函数就是可以为某个类增加扩展方法,比如给 JDK 中的 String 类增加一个 isRookie 方法来判断某个字符串是否是 Rookie:

fun start() {
val name = "rookie"
println(name.isRookie())
}

fun String.isRookie(): Boolean {
return this == "Rookie"
}

this 代表了当前调用者的引用,利用扩展函数你可以很方便的封装一些常用方法,比如 Long 型转时间类型,时间类型转 Long 型,不比像以前一样再用工具类做调用了。

除了扩展函数,Kotlin 还支持扩展属性:

fun start() {
val list = listOf(1, 2, 3)
println(list.maxIndex)
}

val <T> List<T>.maxIndex: Int
get() = if (this.isEmpty()) -1 else this.size - 1

通过定义一个扩展属性和定义它的 get 逻辑,我们就可以为 List 带来一个全新属性——maxIndex,这个属性用来返回当前 List 的最大元素下标。

扩展函数和扩展属性多用于封闭类,比如 JDK、第三方 jar 包作为扩展使用,它的实际使用效果其实和工具类是一样的,只不过更加优雅。

不过借用这个能力,Kotlin 为所有的常用类都增加了一堆扩展,比如 String:

基本上你可以想到的大部分函数都已经被 Kotlin 内置了,这就是 Kotlin 的语法糖。

5. Kotlin的容器

终于来到我们这篇文章的大头了,Kotlin 中的容器基本上都是:List、Map 扩展而来,作为一个函数式语言,Kotlin 将容器分为了可变与不可变。

我们先来看一下普遍的用法:

fun start() {
val list = listOf(1, 2, 3)
val set = setOf(1, 2, 3)
val map = mapOf(1 to "one", 2 to "two", 3 to "three")
}

上面的例子中,我们使用三个内置函数来方便的创建对应的容器,但是此时创建的容器是不可变的,也就是说容器内的元素只能读取,不能添加、删除和修改。

当然,Kotlin 也为此类容器增加了一些方法,使其可以方便的增加元素,但实际行为并不是真的往容器内增加元素,而是创建一个新的容器将原来的数据复制过去:

fun start() {
val list = listOf(1, 2, 3).plus(4)
val set = setOf(1, 2, 3).plus(4)
val map = mapOf(1 to "one", 2 to "two", 3 to "three").plus(4 to "four")
}

如果我们想要创建一个可以增加、删除元素的容器,也就是可变容器,可以用以下函数:

fun start() {
val list = mutableListOf(1, 2, 3)
val set = mutableSetOf(1, 2, 3)
val map = mutableMapOf(1 to "one", 2 to "two", 3 to "three")
}

讲完了,容器的创建,可以来聊聊相关的一些操作了,在 Java 中有一个 Stream 流,在 Stream 中可以很方便的做一些常见的函数操作,Kotlin 不仅完全继承了过来,还加入了大量方法,大概可以包含以下几类:

  1. 排序:sort
  2. 乱序:shuffle
  3. 分组:group、associate、partition、chunked
  4. 查找:filter、find
  5. 映射:map、flatMap
  6. 规约:reduce、min、max

由于函数实在太多,我不能一一列举,只能给大家举一个小例子:filter:

一个 filter 有这么多种多样的函数,几乎可以容纳你所有的场景,这里说两个让我感觉到惊喜的函数:chunked 和 partition。

chunked 函数是一个分组函数,我常用的场景是避免请求量过大,比如在批量提交时,我可以将一个 list 中的元素进行 1000 个一组,每次提交一组:

fun start() {
val list = mutableListOf(1, 2, 3)

val chunk : List<List<Int>> = list.chunked(2)
}

示例代码中为了让大家看的清楚我故意声明了类型,实际开发中可以不声明,会进行自动推断。

在上面这个例子中,我将一个 list 进行每组两个进行分组,最终得到一个 List<List> 类型的变量,接下来我可以使用 forEach 进行批量提交,它底层通过 windowed 函数进行调用,这个函数也可以直接调用,有兴趣的朋友可以研究一下效果,通过名字大概可以知道是类似滑动窗口。

partition 你可以将其看作一个分组函数,它算是 filter 的补充:

fun start() {
val list = mutableListOf(1, 2, 3)

val partition = list.partition { it > 2 }

println(partition.first)
println(partition.second)
}

它通过传入一个布尔表达式,将一个 List 分为两组,返回值是上文提到过的 Pair 类型,Pair 有两个变量:first 和 second。

partition 函数会将符合条件的元素放到 first 中去,不符合条件的元素放到 second 中,我自己的使用的时候很多是为了日志记录,要把不处理的元素也记录下来。

容器与容器之间还可以直接通过类似:toList、toSet之类的方法进行转换,非常方便,转换 Map 我一般使用 associate 方法,它也有一系列方法,主要作用就是可以转换过程中自己指定 Map 中的 K 和 V。

6. 结束语

不知不觉都已经快四千字了,我已经要结束这篇文章了,但是仍然发现几乎什么都没写,也对,这只是一篇给大家普及 Kotlin 所带来效率的提升的文章,而不是专精的技术文章。

正如我在标题中写的那样:Kotlin 的语法糖到底有多甜?Kotlin 的这一切我都将其当作语法糖,它能极大提高我的开发效率,但是一些真正 Kotlin 可以做到而 Java 没有做到的功能我却没有使用,比如:协程。

由于我一直是使用 Kotlin 写后端,而协程的使用场景我从来没有遇到过,可能做安卓的朋友更容易遇到,所以我没有对它进行举例,对于我来说,Kotlin 能为我的开发大大提效就已经很不错了。

使用 Kotlin 有一种使用 JS 的感觉,有时候可以一个方法开头就写一个 return,然后链式调用一直到方法结束。

我还是蛮希望 Java 开发者们可以转到 Kotlin,感受一下 Kotlin 的魅力,毕竟是百分百兼容。

在这里要说一下我使用的版本,我使用的是 JDK17、Kotlin 1.8、Kotlin 编译版本为 1.8,也就是说 Kotlin 生成的代码可以跑在最低 JDK1.8 版本上面,这也是一个 Kotlin 的好处,你可以通过升级 Kotlin 的版本体验最新的 Kotlin 特性,但是呢,你的 JDK 平台不用变。

对了,Kotlin 将反射封装的极好,喜欢研究的朋友也可以研究一下。

好了,这篇文章就到这里,希望大家能帮我积极点赞,提高更新动力,人生苦短,我用KT。


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

MySQL Join原理

Join的类型left join,以左表为驱动表,以左表作为结果集基础,连接右表的数据补齐到结果集中right join,以右表为驱动表,以右表作为结果集基础,连接左表的数据补齐到结果集中inner join,结果集取两个表的交集full join,结果集取两...
继续阅读 »

Join的类型

  1. left join,以左表为驱动表,以左表作为结果集基础,连接右表的数据补齐到结果集中
  1. right join,以右表为驱动表,以右表作为结果集基础,连接左表的数据补齐到结果集中
  1. inner join,结果集取两个表的交集
  1. full join,结果集取两个表的并集
    1. mysql没有full join,union取代
    2. union与union all的区别为,union会去重
  1. cross join 笛卡尔积
    1. 如果不使用where条件则结果集为两个关联表行的乘积
    2. 与,的区别为,cross join建立结果集时会根据on条件过滤结果集合
  1. straight_join
    1. 严格根据SQL顺序指定驱动表,左表是驱动

Join原理

本质上可以理解为嵌套循环的操作,驱动表作为外层for循环,被驱动表作为内层for循环。根据连接组成数据的策略可以分为三种算法。

Simpe Nested-Loop Join

  1. 连接比如有A表,B表,两个表JOIN的话会拿着A表的连表条件一条一条在B表循环,匹配A表和B表相同的id 放入结果集,这种效率是最低的。

Index Nested-Loop Join

  1. 执行流程(磁盘扫描)
    1. 从表t1中读入一行数据 R;
    2. 从数据行R中,取出a字段到表t2里进行树搜索查找
    3. 取出表t2中满足条件的行,跟R组成一行,作为结果集的一部分;
    4. 重复执行步骤1到3,直到表t1的末尾循环结束。
  1. 而对于每一行R,根据a字段去表t2查找,走的是树搜索过程。

Block Nested-Loop Join

  1. mysql使用了一个叫join buffer的缓冲区去减少循环次数,这个缓冲区默认是256KB,可以通过命令show variables like 'join_%'查看
  2. 其具体的做法是,将第一表中符合条件的列一次性查询到缓冲区中,然后遍历一次第二个表,并逐一和缓冲区的所有值比较,将比较结果加入结果集中
  3. 只有当JOIN类型为ALL,index,rang或者是index_merge的时候才会使用join buffer,可以通过explain查看SQL的查询类型。

Join优化

  1. 为了优化join算法采用Index nested-loop join算法,在连接字段上建立索引字段
  2. 使用数据量小的表去驱动数据量大的表
  3. 增大join buffer size的大小(一次缓存的数据越多,那么外层表循环的次数就越少)
  4. 注意连接字段的隐式转换与字符编码,避免索引失效

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

为什么很多公司都开始使用Go语言了?

Go
为什么很多公司都开始使用Go语言了?写在前面最近和几个小伙伴们在写字节跳动第五届青训营后端组的大作业。接近尾期了,是时候做一些总结了,那从什么地方开始呢?那就从我们为什么要选择Go语言开始吧~🌐项目地址📝项目文档越来越多的互联网大厂开始使用Go语言了,譬如腾讯...
继续阅读 »

为什么很多公司都开始使用Go语言了?

写在前面

最近和几个小伙伴们在写字节跳动第五届青训营后端组的大作业。

接近尾期了,是时候做一些总结了,那从什么地方开始呢?那就从我们为什么要选择Go语言开始吧~


越来越多的互联网大厂开始使用Go语言了,譬如腾讯、美团、滴滴、百度、Google、bilibili...

还有最初使用Python的字节跳动,甚至已经全面拥向Go了。这么多国内外首屈一指的公司,都在开始使用它了,它到底有什么优势呢?这就得谈谈它的一些优势了。

ps:当然了,还有Go-To-Byte的成员,想要学习go语言,并且用它完成青训营的大项目呐~

Go的一些优势

说起优势,在某些方面多半是因为它有一些别人没有的特性,或者优化了别人麻烦的地方,相比起来,才会更胜一筹。那我们来了解一下Go的一些特点吧,但在了解生硬的特点之前,我们先来看看其它几种常见的语言:

常见的一些语言

这里不是对比哟,不是说谁好谁坏,而是小马过河,因人而异~

1、C/C++

C语言是在1971年的时候,被大神Ken Thompson和Dennis Ritchie发明的,而Go语言的主导开发者之一就是Ken Thompson,所以在很多地方和C语言类似,(比如struct、Printf、&取值符)

C/C++也作为很多初学初学的语言,它们都是直接编译为机器码,所以执行效率会更高,并且都不需要执行环境,用户的使用成本会更低,不像很多语言还需要安装所需的环境。

也因为这些原因,它们的一次编码或编译只适用于一种平台,对于不同操作系统而言,有时需要修改编码再编译,有时直接重新编译即可。

而且对于开发者也"很不友好"😒,需要自己处理垃圾回收(GC)的问题。编码时,还需要考虑,堆上的内存什么时候free、delete?代码会不会造成内存泄露、不安全?

2、Java

自己作为一个从Java来学习Go的菜鸟,还未正式开发,就感到开发效率会比Java低了(个人感觉,不喜勿喷)~😁

Java直接编译成字节码(.class),这种编译产物是介于原始编码和机器码的一种中间码。这样的话,Java程序就需要特定的执行环境(JVM)了,执行效率相比会低一些,还可能有虚拟化损失。但是这样也有一个好处就是可以编译一次,多处执行(跨平台)。而且它也是自带GC

3、JavaScript

Python一样,JS是一种解释型语言,它们不需要编译,解释后即可运行。所以Js也是需要特定的执行环境(浏览器引擎) 的。

将其代码放入浏览器后,浏览器需要解析代码,所以也会有虚拟化损失Js只需要浏览器即可运行,所以它也是跨平台的。

再谈Go

看完了前面几种常见语言的简单介绍。C/C++性能很高,因为它直接编译为二进制,且没有虚拟化损失,Go觉得还不错;Java自动垃圾回收机制很好,Go觉得也不错;Js一次编码可以适用可以适用多种平台Go觉得好极了;而且Go天然具备高并发的能力,是所有语言无可比及的。那我们来简单总结一下吧!

  1. 自带运行环境Runtime,且无须处理GC问题

Go程序的运行环境可厉害了,其实大部分语言都有Runtime的概念,比如Java,它程序的运行环境是JVM,需要单独安装。对于Java程序,如果不经过特殊处理,只能运行在有JMV环境的机器上。

Go程序是自带运行环境的,Go程序的Runtime会作为程序的一部分打包进二进制产物,和用户程序一起运行,也就是说Runtime也是一系列.go代码和汇编代码等,用户可以“直接”调用Runtime的函数(比如make([]int, 2, 6),这样的语法,其实就是去调用Runtime中的makeslice函数)。对于Go程序,简单来说就是不需要安装额外的运行环境,即可运行。除非你需要开发Go的程序。

正因为这样,Go程序也无须处理GC的问题,全权交由Runtime处理(反正要打包到一起)

  1. 快速编译,且跨平台

不同于C/C++,对于多个平台,可能需要修改代码后再编译。也不同于Java的一次编码,编译成中间码运行在多个平台的虚拟机上。Go只需要一次编码,就能轻松在多个平台编译成机器码运行。

值得一提的就是它这跨平台的能力也是Runtime赋予的,因为Runtime有一定屏蔽系统调用的能力。

  1. 天然支持高性能高并发,且语法简单、学习曲线平缓

C++处理并发的能力也不弱,但由于C++的编码要求很高,如果不是很老练、专业的C++程序员,可能会出很多故障。而Go可能经验不是那么丰厚,也能写出性能很好的高并发程序。

值得一提的就是它这超强的高并发,也是Runtime赋予的去处理协程调度能力。

  1. 丰富的标准库、完善的工具链

对于开发者而言,安装好Golang的环境后,就能用官方的标准库开发很多功能了。比如下图所示的很多常用包:

而且Go自身就具有丰富的工具链(比如:代码格式化、单元测试、基准测试、包管理...)

  1. 。。。。。。

很多大厂开始使用Go语言、我们团队为什么使用GoLang,和这些特性,多少都有一些关系吧~


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

认识自动化测试

自动化测试有以下几个概念:单元测试集成测试E2E 测试快照测试测试覆盖率TDD 以及 BDD 等简述项目开发过程中会有几个经历。版本发布上线之前,会有好几个小时甚至是更长时间对应用进行测试,这个过程非常枯燥而痛苦代码的复杂度达到了一定的级别,当维护者的数量不止...
继续阅读 »

自动化测试有以下几个概念:

  1. 单元测试
  2. 集成测试
  3. E2E 测试
  4. 快照测试
  5. 测试覆盖率
  6. TDD 以及 BDD 等

简述

项目开发过程中会有几个经历。

  1. 版本发布上线之前,会有好几个小时甚至是更长时间对应用进行测试,这个过程非常枯燥而痛苦
  2. 代码的复杂度达到了一定的级别,当维护者的数量不止一个人,你应该会逐渐察觉到你在开发新功能或修复 bug 的时候,会变得越发小心翼翼,即使代码看起来没什么问题,但你心里还是会犯嘀咕:会不会引起其他的bug。
  3. 对项目中的代码进行重构的时候,会花费大量的时间进行回归测试

这些问题都是由于大多数使用最基本的手动测试的方式所带来的问题,解决它可以引入自动化测试方式。

我们日常的开发中,代码的完工其实并不等于开发的完工。如果没有测试,不能保证代码能够正常运行。

如何进行应用程序测试?

  • 手动测试:通过测试人员与应用程序的交互来检查其是否正常工作。
  • 自动化测试:编写应用程序来替代人工检验。

手动测试

开发者都懂得手动测试代码。在编写完源代码之后,下一步理所当然就是去手动测试它。

手动测试的优势在于足够简单灵活,但是缺点也很明显:

  • 手动不适合大型项目
  • 忘记测试某项功能
  • 大部分时间都在做回归测试

虽然有一部分手动测试时间是花在测试新特性上,但是大部分时间还是用来检查之前的特性是否仍正常工作。这种测试被称为回归测试。回归测试对人来说是非常困难的任务————它们是重复性的,要求投入很多注意力,而且没有创造性的输入。总之,这种测试太枯燥了。幸运的是,计算机特别擅长此类工作,这也是自动化测试可以大展身手的地方!

自动化测试

自动化测试是利用计算机程序检查软件是否运行正常的测试方法。换句话说,就是用其他额外的代码检查被测软件的代码。当测试代码编写完之后,就可以不费吹灰之力地进行无数次重复测试。

可使用多种不同的方法来编写自动化测试脚本:

  • 可以编写通过浏览器自动执行的程序
  • 可以直接调用源代码里的函数
  • 也可以直接对比程序渲染之后的截图

每一种方法的优势各不相同,但它们有一大共同点:相比手动测试而言节省了大量时间以及提高了程序的稳定性。

自动化测试还有很多优点,比如:

  • 尽早的发现程序的 bug 和不足
  • 增强程序员对程序健壮性、稳定性的信心
  • 改进设计
  • 快速反馈,减少调试时间
  • 促进重构

当然,自动化测试不可能保证一个程序是完全正确的,而且事实上,在实际开发过程中,编写自动化测试代码通常是开发者不太喜欢的一个环节。大多数情况下,前端开发者在开发完一项功能后,只是打开浏览器手动点击,查看效果是否正确,之后就很少对该块代码进行管理。造成这种情况的原因主要有两个:

  • 一个是业务繁忙,没有时间进行测试的编写
  • 另一个是该如何编写测试

测试类型

前端开发最常见的测试主要是以下几种:

  • 单元测试:验证独立的单元是否正常工作
  • 集成测试:验证多个单元协同工作
  • 端到端测试:从用户角度以机器的方式在真实浏览器环境验证应用交互
  • 快照测试:验证程序的 UI 变化

单元测试

单元测试是对应用程序最小的部分(单元)运行测试的过程。通常,测试的单元是函数,但在前端应用中,组件也是被测单元。

单元测试可以单独调用源代码中的函数并断言其行为是否正确。

// sum.js
function sum(a, b) {
return a + b;
}
module.exports = sum;
// sum.test.js
const sum = require('./sum');

test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
PASS  ./sum.test.js
✓ adds 1 + 2 to equal 3 (5ms)

与端到端测试不同,单元测试运行速度很快,只需要几秒钟的运行时间,因此可以在每次代码变更后都运行单元测试,从而快速得到变更是否破坏现有功能的反馈。

单元测试应该避免依赖性问题,比如不存取数据库、不访问网络等等,而是使用工具虚拟出运行环境。这种虚拟使得测试成本最小化,不用花大力气搭建各种测试环境。

单元测试的优点:

  • 提升代码质量,减少 bug
  • 快速反馈,减少调试时间
  • 让代码维护更容易
  • 有助于代码的模块化设计
  • 代码覆盖率高

单元测试的缺点:

  • 由于单元测试是独立的,所以无法保证多个单元运行到一起是否正确

常见的 JavaScript 单元测试框架:

  • Jest
  • Mocha
  • Jasmine
  • Karma
  • ava
  • Tape

Mocha 跟 Jest 是用的较多的两个单元测试框架,基本上前端单元测试就在这两个库之间选了。总的来说就是 Jest 功能齐全,配置方便,Mocha 灵活自由,自由配置。

推荐使用Jest。

集成测试

定义集成测试的方式并不相同,尤其是对于前端。有些人认为在浏览器环境上运行的测试是集成测试;有些人认为对具有模块依赖性的单元进行的任何测试都是集成测试;也有些人认为任何完全渲染的组件测试都是集成测试。

优点:

  • 由于是从用户使用角度出发,更容易获得软件使用过程中的正确性
  • 集成测试相对于写了软件的说明文档
  • 由于不关注底层代码实现细节,所以更有利于快速重构
  • 相比单元测试,集成测试的开发速度要更快一些

缺点:

  • 测试失败的时候无法快速定位问题
  • 代码覆盖率较低
  • 速度比单元测试要慢

端到端测试(E2E)

E2E(end to end)端到端测试是最直观可以理解的测试类型。在前端应用程序中,端到端测试可以从用户的视角通过浏览器自动检查应用程序是否正常工作。

想象一下,你正在编写一个计算器应用程序,并且你想测试两个数求和的运算方法是否正确。你可以编写一个端到端测试,打开浏览器,加载计算器应用程序,单击“1”按钮,单击加号“+”按钮,再次单击“1”按钮,单击等号“=”,最后检查屏幕是否显示正确结果“2”。

编写完一个端到端测试后,可以根据自己的需求随时运行它。想象一下,相比执行数百次同样的手动测试,这样一套测试代码可以节省多少时间!

优点:

  • 真实的测试环境,更容易获得程序的信心

缺点:

  • 首先,端到端测试运行不够快。启动浏览器需要占用几秒钟,网站响应速度又慢。通常一套端到端测试需要 30 分钟的运行时间。如果应用程序完全依赖于端到端测试,那么测试套件将需要数小时的运行时间。
  • 端到端测试的另一个问题是调试起来比较困难。要调试端到端测试,需要打开浏览器并逐步完成用户操作以重现 bug。本地运行这个调试过程就已经够糟糕了,如果测试是在持续集成服务器上失败而不是本地计算机上失败,那么整个调试过程会变得更加糟糕。

一些流行的端到端测试框架:

快照测试

快照测试类似于“找不同”游戏。快照测试会给运行中的应用程序拍一张图片,并将其与以前保存的图片进行比较。如果图像不同,则测试失败。这种测试方法对确保应用程序代码变更后是否仍然可以正确渲染很有帮助。

传统快照测试是在浏览器中启动应用程序并获取渲染页面的屏幕截图。它们将新拍摄的屏幕截图与已保存的屏幕截图进行比较,如果存在差异则显示错误。这种快照测试在操作系统或浏览器存在版本间差异时,即使快照并没有改变,也会遇到测试失败问题。

使用 Jest 测试框架编写快照测试。取代传统对比屏幕截图的方式,Jest 快照测试可以对 JavaScript 中任何可序列化值进行对比。可以使用它们来比较前端组件的 DOM 输出。

应用场景:

  • 开发纯函数库,建议写更多的单元测试 + 少量的集成测试
  • 开发组件库,建议写更多的单元测试、为每个组件编写快照测试、写少量的集成测试 + 端到端测试
  • 开发业务系统,建议写更多的集成测试、为工具类库、算法写单元测试、写少量的端到端测试

测试覆盖率

测试覆盖率是衡量软件测试完整性的一个重要指标。掌握测试覆盖率数据,有利于客观认识软件质量,正确了解测试状态,有效改进测试工作

度量测试覆盖率:

  • 代码覆盖率
  • 需求覆盖率

代码覆盖率

一种面向软件开发和实现的定义。它关注的是在执行测试用例时,有哪些软件代码被执行到了,有哪些软件代码没有被执行到。被执行的代码数量与代码总数量之间的比值,就是代码覆盖率

根据代码粒度的不同,代码覆盖率可以进一步分为四个测量维度。它们形式各异,但本质是相同的。

  • 行覆盖率(line coverage):是否每一行都执行了?
  • 函数覆盖率(function coverage):是否每个函数都调用了?
  • 分支覆盖率(branch coverage):是否每个if代码块都执行了?
  • 语句覆盖率(statement coverage):是否每个语句都执行了?

如何度量代码覆盖率呢?一般可以通过第三方工具完成,比如 Jest 自带了测试覆盖率统计。

这些度量工具有个特点,那就是它们一般只适用于白盒测试,尤其是单元测试。对于黑盒测试(例如功能测试/系统测试)来说,度量它们的代码覆盖率则相对困难多了。

需求覆盖率

对于黑盒测试,例如功能测试/集成测试/系统测试等来说,测试用例通常是基于软件需求而不是软件实现所设计的。因此,度量这类测试完整性的手段一般是需求覆盖率,即测试所覆盖的需求数量与总需求数量的比值。视需求粒度的不同,需求覆盖率的具体表现也有不同。例如,系统测试针对的是比较粗的需求,而功能测试针对的是比较细的需求。当然,它们的本质是一致的。

如何度量需求覆盖率呢?通常没有现成的工具可以使用,而需要依赖人工计算,尤其是需要依赖人工去标记每个测试用例和需求之间的映射关系。

对于代码覆盖率来说,广为诟病的一点就是 100% 的代码覆盖率并不能说明代码就被完全覆盖没有遗漏了。因为代码的执行顺序和函数的参数值,都可能是千变万化的。一种情况被覆盖到,不代表所有情况被覆盖到。

对于需求覆盖率来说,100% 的覆盖率也不能说“万事大吉”。因为需求可能有遗漏或存在缺陷,测试用例与需求之间的映射关系,尤其是用例是否真正能够覆盖对应的测试需求,也可能是存在疑问的。

总结

适用于不同的场景,有各自的优势与不足。需要注意的是,它们不是互相排斥,而是相互补充的。

关于测试覆盖率,最重要的一点应该是迈出第一步,即有意识地去收集这种数据。没有覆盖率数据,测试工作会有点像在“黑灯瞎火”中走路。有了覆盖率数据,并持续监测,利用和改进这个数据,才是一条让测试工作越来越好的光明大道。

是不是所有代码都要有测试用例支持呢?

测试覆盖率还是要和测试成本结合起来,比如一个不会经常变的公共方法就尽可能的将测试覆盖率做到趋于 100%。而对于一个完整项目,前期先做最短的时间覆盖 80% 的测试用例,后期再慢慢完善。

经常做更改的活动页面我认为没必要必须趋近 100%,因为要不断的更改测试永用例,维护成本太高。

大多数情况下,将 100% 代码覆盖率作为目标并没有意义。

实现 100% 代码覆盖率不仅耗时,而且即使代码覆盖率达到 100%,测试也并非总能发现 bug。有时你可能还会做出错误的假设,当你调用一个 API 代码时,假定的是该 API 永远不会返回错误,然而当 API确实在生产环境中返回错误时,应用就崩溃了。


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

2023年story的年中总结

故事之前一、关于过去今天是2023年5月28号,距离2022年总结已经过去了近半年了,我和我的小伙伴回到了去年同样的茶馆,品一杯和以往一样的茶,但讲述和以往不一样的故事。自从去年之后,我们定下了一个传统,每个半年只要我们还在杭州,我们就会聚到一起来一次总结,讲...
继续阅读 »

故事之前

一、关于过去

今天是2023年5月28号,距离2022年总结已经过去了近半年了,我和我的小伙伴回到了去年同样的茶馆,品一杯和以往一样的茶,但讲述和以往不一样的故事。

自从去年之后,我们定下了一个传统,每个半年只要我们还在杭州,我们就会聚到一起来一次总结,讲述我们每个人这个半年发生的故事。

2022年虽然过去了,但是对于我来说依然历历在目,我去年一年都很焦虑,也没有特别值得骄傲的事情,生活上更没有重要的突破比如:买房、买车、谈恋爱、创业、出国游等等。所以去年年末的时候我就痛定思痛2023年一定要做些什么,思前想后,我给2023的我定下了作品的主题,意思是2023年我可以没买车、没买房、没谈恋爱、即便看起来一事无成,但是我必须要留下一些作品,这个作品是我努力用心付出做出来的东西。只要完成了好的作品那么就没有人可以说我的2023是失败的,是虚度的。

当钢琴曲响起的时候,世界就会静下来,而我脑子里面就不自觉的慢慢回忆这个半年发生的事情,我很感谢你能够耐心观看这篇文章,接下来一起来看一看2023前半年关于我的故事!

故事之中

一、年度日历

一月

不知从什么时候起,没有鞭炮声了,过年的快乐慢慢在变少,而烦恼却越来越多。因为我母亲生病的原因,在大年初四就需要去西安做放疗,为了方便更快的去西安,不耽误治疗,今年我和父母都在离西安更近的安康的亲戚家过年。很感谢小姨在过年期间都很照顾我们,对我妈妈可以说是无微不至,我再一次感受到对于父辈来说,他们的情感真的很深,虽然现在都说亲戚间关系变淡了,但是我相信我小姨和我妈妈就是最好的朋友和闺蜜,互相都有无话不谈的亲情和感情。

因为亲戚过生日的原因,我第一次带父母和我小姨一家去KTV唱歌,给他们庆生,在我记忆中这应该是第一次在父母面前唱歌,我小姨他们也很开心氛围很好。

我还第一次陪小七(我的亲外甥女)玩了很久,我给她买吃的,她想吃什么我就他她买什么,想玩什么游乐设施就带她去玩,在我这里主打的就是一个嚎无人性的宠爱,哈哈。

这个年过的很短,只有短短的不到5天时间就要去西安了,我意识到所有的奔波都是因为我没有在西安定下来,我需要加快在西安买房定居下来的速度了。

二月

不知用了多少的零碎时间,我用心准备的网站终于在我的努力下上线了,从买域名到开发到部署,我都自己感受了一遍,也踩了很多的坑,但是皇天不负有心人,当我写了用零碎时间个人建站这篇文章发表之后,居然在后来成为了一篇算是高赞的文章,我的第一个作品虽然不完美,但是也收到了很多掘有的鼓励,直到现在我每天都会点开我的自己的网站看一下,它也管理着我每一天的行为和一年的年度目标,我正在受益于这个网站带给我的好处。

我也在写系列的chrome插件的文章,收录在我的chorme插件专栏里面,我希望把这个chorme相关的知识做成一个系统的知识产品,这也算我自己的年度作品呀!

二月的感受是充实的,并且在继续努力中。。。

三月

三月份有一本书很影响我,就是张鑫旭老师(旭哥)的《技术写作指南》。这本书让我坚定了要好好培养写作习惯的决心,我还特别写了一篇读后感,不仅如此,文章中特别提到了分享的力量,如果写了东西就可以勇敢分享出来,不要怕别人的恶评和瞧不起,要让自己的领导和同事看到,否则没有人会觉得你在努力,也更不会去关注你,如果都不关注你,怎么可能获得职场上的晋升。因此不要害怕展现自己,因此之后几乎大部分文章我都会在朋友圈分享出来,要利用一切方式提升自己的影响力,不要做无名小卒,任何人都可以把自己忽略。所以在这里我要感谢旭哥的书籍,你在无形中影响了很多很多人,并且是积极饿影响。

所以在那之后,我几乎每一本都会写读后感,无论是小说还是技术书籍,每个月也必产出两篇优质的技术文章,我给自己定了严酷的惩罚,因此我坚信没有任何事情可以阻挡我完成博客读书写作的目标。

截屏2023-05-28 下午4.28.38.png

四月

四月份我依然注重自己的作品,所以我在思考自己要做一个开源的作品,最终这个东西出来了,它叫做story

截屏2023-05-28 下午4.33.21.png

这个是一个monorepo风格的工程化解决方案,算是自己工作以来的一个沉淀,它不是什么特别出色的作品,但我会用一年时间去用心打造,算是自己开源的一次尝试,就像现在我正在做一个web端截图的组件,很有意思,它会成为一个属于我自己的前端工程化的方案。路过的程序员朋友也可以看一下哈,最新的代码分支是feat-tool。

五月

五月的核心是java学习,我现在已经确定了自己的发展方向了——web端产品,因为考虑到未来的城市选择和发展,可能这个方向更适合我,所以在未来我会专注于web端BS架构的技术,做web端产品体验的方向,因此前后端我都需要比较专业熟悉,一年之后我也会考虑专职做java开发,等到2年过后,前后端都掌握好之后更好的向上发展。

这个月我看了很多的java教程,算是在恶补java吧!

这个月我也经历了一个很重要的事情,我朋友给我介绍了一个女生,我们认识了一下,我们微信聊的不错。我是一个只要想做就会去做的人,所以这个月我回了一趟西安去见她,但是见面之后发现和微信聊天还是很不一样的,没有太多共同话题,而且我也不知道聊些什么,所以后来就不了了之了。

六月

六月份是上半年的最后一个月,我遇到了小R同学,她是一个特别细腻和用心的女生,我们一起做了很多事情,一起去看电影,去玩蹦床,去逛街,去弹吉他,谢谢她在我枯燥的日子里增添了许多美好的时刻。让我整个上半年能够一个很好的句号结束。

二、年度KPI

在今年年初的时候,我给自己设定了几个KPI,也就是这一年我需要完成的事情。

截屏2023-05-28 下午4.47.50.png

目标随时都可以新增,但是一定要保证过去的某些个目标已经大概率可以完成才可以。我的个人网站有一个类似大屏的页面可以看到目标完成情况,我真的享受这种目标被一点点推进的感觉。

分析

健身:目前健身计划有点不符合预期,已经5个月了,但是应该完成到41%左右的,但是之前因为五一放假的关系,耽搁了几天,不过后面跑勤一些应该问题也不大,可以完成。

博客,读书都是正常进行的,我发现只要对自己狠一些,其实定个死目标就一定可以完成,最重要我定的目标其实也不高,按照自己的赛道慢慢超越自己就好了。

英语还未开始,因为去年错过了考试,今年的考试应该在12月份左右,我需要最后几个月好好复习,通过的可能性更大一些,不然现在刷题到时候考就忘了。

java还挺有意思的,继续学习吧!但我需要稳一点,我有的时候在急功近利了,java这个东西应该不要求快,而是基础知识要打牢固才是真的学到了。

三、心动时刻

我很珍惜在我们的时间流逝的过程中那些个心动时刻,不要误会哈,除了看到美女有心动时刻以外。生活中还有其他的一些心动时刻更让人流连忘返。

影响力

5月份的某一刻,我正在刷掘金,我看到了这个内容

WechatIMG26.jpeg

这一刻我真的内心非常涌动,我曾经在我的文章中说过一句话,在这个世界上没有什么比当你得知你对别人施加了一定的积极的影响之后更感动了。我更加坚定了写作的意义,可能这种事情没有为我们带来一分钱的收获,甚至让我们耗费了大量的时间精力,但是你真的会感到自己影响到了别人,生命的厚度也因此而增加,我可能明白了为什么神光大佬那么执着于技术写作了。

心流

5月份的某个周六,早上我买了一杯生椰拿铁,简单吃了一点东西。然后我开始写一篇文章,不知不觉一直写到了下午4点钟,写完之后感到前所未有的充实。也就是那一刻我感到生命的厚度增加了。虽然这篇文章点赞很少,看的人也不多,但是我能够真切的感受到我居然也有“专注,认真”的这样一种品质。我由此感受到了个结论:自信从来不是靠别人夸奖而获得,而是自己真正的认可自己。

运动的执着

如果原生家庭没能让自己感到骄傲,那么我们的自信到底怎么建立呢!虽然可能是错的,但我仍然感觉大部分普通孩子其实是自卑的,没有优越的家庭环境,处在社会的最底层,别人看起来唾手可得的东西自己需要拼了老命才能获得,不仅如此,反而会因为自卑错过很多本该的到的机会,从而陷入更为窘迫的境地。这样更多如我一般的人要怎么才可能破局呢!答案只有一个,靠自己,靠自己的坚定和执着,靠自己从来不轻易违背自己的目标和承诺,靠自己小心翼翼的保护自己的自尊心和少的可怜的自信。

当我年初定下250天健身目标之后,无论发生什么,我都会坚定的执行这个目标,不是喜欢跑步,而是为了捍卫目标。

故事之后

一、6个月

2023年还剩下6个月,这6个月任重而道远,我会继续推进目标栏上的目标,并且慢慢把英语放到中心上来,算法、react原理、vue原理都要抓起来了,老早学过,现在感觉都忘了好多了,以下是下半年的核心:

  1. 英语BEC考试
  2. chrome插件专栏
  3. story工程化方案

二、未来

我目前在现在的公司其实是很开心的,今年遇到了好几个很有意思的小伙伴,和去年完全不一样,去年还有些孤独,但今年来了几个新伙伴我们就是无话不谈的饭搭子,说说笑笑就是一天,而且今年工作起来更加的得心应手了,如果可以的话,我很希望在这里一直做下去,明年希望在薪资上有一个可观的涨幅,因为这个对我来说特别的重要。

二、感恩

最后我想以感恩来结束这篇文章的写作,我们生活在一个并不孤独的世界,有的人仅仅是出现在我们的生命中,就值得用心去感恩!

我很感恩小R同学,谢谢你出现在我的生命中,我真的很感谢你对我的生日那么的用心,给我写了每一岁的信,尤其是我出生那天的报纸让我非常惊讶,我可能是上辈子修来的福分才能遇到这么一位在乎我和关心我的女朋友,希望我也能在你的生命中带给你更多真正优质的东西。

感谢幸福之家的天豪、涛涛、绍路你们的存在让我在杭州有了亲兄弟和家的感觉,尤其是有一天我很晚没回家,你们都给我打电话,还想着给我送伞,我那一刻甚至快流下了眼泪,我觉得原来被人在乎和关心真的很好。

感恩公司的旭哥、亮琦、振明、张写,今年有你们让我觉得原来工作中欢声笑语的感觉真的好温暖呀,这样的工作氛围真的很好,尤其是每次下班回去健身回家的时候,就像上学时一起奔跑的少年一样,很清爽,很舒服。

感恩凯凯同学,今年特别感谢你给我介绍女朋友,每次回西安第一个就希望去找你,特别希望以后在西安我们买房能买到一起!这样不就又能够一起玩了嘛哈哈!

感恩赵勇、陈欢、邓撼、旭东同学,你们都是杭州俱乐部的成员,我们每月的总结一定要一直坚持下去,正正是因为这里我才能够每一天都不懈怠自己,因为大家都真的很努力,这样的团队怎么能不让人热爱呢!

感恩jerry、Andy,今年你们两位大佬能够来找我,我很开心,以往真的很难想过居然有一天还能和你们做朋友,我也会慢慢让自己变得强大,变得和原来的你们一样优秀。

感恩我姐,我不在西安的时候,医院的事情都是你在操心,我没有办法时时刻刻关注到很多事情,但是你都是晚上上班,白天又可能去看望爸妈,这让我很惭愧,但是我知道这是没有办法的事情,我唯一能够做的就是不断的提升自己,不过我承诺这只是暂时的。


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

三十岁前端的破冰尝试

本人简介JavaScrip码农,今年三十,宿主是非互联网类型的外企,提供的内存虽然不大,但也基本够存活。工作之余,我的主题就是咸鱼。但或许是我的咸度不够,最近开始腐烂了,尤其是夜深人静,主要的信息输入被关闭之后,我就感觉内在的信息流在脑海里乱窜,各种健康指数开...
继续阅读 »

本人简介

JavaScrip码农,今年三十,宿主是非互联网类型的外企,提供的内存虽然不大,但也基本够存活。

工作之余,我的主题就是咸鱼。但或许是我的咸度不够,最近开始腐烂了,尤其是夜深人静,主要的信息输入被关闭之后,我就感觉内在的信息流在脑海里乱窜,各种健康指数开始飙升。就像是一台老旧的电脑,非要带最新的显卡游戏,发出嘤嘤嘤的EMO声,最后在卡死在昏睡页面。

大多时候醒来会一切安好,像是被删去了前一晚的日志。但有时也会存有一些没删除干净的缓存,它们就像是病毒,随着第二天的重启复苏。我会感到无比的寒冷,冷到我哪怕是饥饿也不敢出门,只有戴上口罩会给我一丝丝的勇气。

这种寒冷会刺激着我无病呻吟,我会感到惊恐和害怕,害怕某天被宿主的回收机制发现这里的不正常,然后被文明的光辉抹除,就如新冠背后那鲜红的死亡人数一样。

或许是幼年求学寄人篱下时烙下的病根,但那时候心田干涸了还可以哭泣。如今呢,心田之上早已是白雪皑皑。

这些年也有人帮助过我,我也努力挣扎过,但大多时候毫无章法,不仅伤了别人的心,也盲目地消耗着心中的热血,愧疚与自责的泪水最终只是让冰层越积越深。

今天也不知哪根筋抽抽了,想着破冰。

嗯,就是字面上的意思,满脑子都是“破冰”二字……

破冰项目

发表这个稿子算是破冰的第一步~

项目的组织架构初步定为凌凌漆,敏捷周期为一周,其中周日进行复盘和制定新计划,其余作为执行日。由于项目长期且紧迫,年假就不予考虑了,病假可以另算,津贴方面目前只考虑早餐,其他看项目发展情况再做调整。

硬件层面

目前作息相当紊乱,供电稳定性差,从近几年的硬件体验报告可以看出,总体运行还算正常,但小毛病层出不穷,电压不稳是当前主要矛盾。OKR如下:

O:保持一个良好的作息
KR1: 保证每天八小时的睡眠。
KR2:保证每天凌晨前关灯睡下。
KR3:保证每天早上九点前起床。

软件层面

英语是硬伤,其次是底层算法需要重写,不然跑着跑着还是会宕机。

翻译是个不错的路子,但数据源是个头痛的问题……肯定得找和技术相关的东西来翻译,并且可以有反馈。嗯…… 想到可以找掘金里已经有的翻译文章,截取其中一小段来进行快速试错。

至于底层算法的问题,此前在leetcode练过一段时间,但仅停留在已知的变得熟练,未知的依旧不会。

因此我觉得有必要先梳理出关于算法的个人认知的知识体系……

总结下来下一阶段任务:

  1. 选择一篇翻译文章,找到其原文,选其中完整的一段进行翻译。
  2. 根据当前认知画个关于算法的思维导图。

下周日会出这周的运行报告以及新一期的计划表。

最后随想

若是觉得我这样的尝试也想试一试,欢迎在评论附上自己的链接,一起尝试,相互借鉴,共同进步~


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

看见的熄灭了,消失的忘记了

许多年前,也许是许多世纪前,我在陶盆里养了两条鱼。在此之前,陶盆里下着漫长的雨,一只蟾蜍在里面参禅,显得很寂寞。若干年后,雨下累了,月亮从桂枝上醒来,蟾蜍还困在禅意里。他俩都不知道对方是自己另外一个化身。陶盆之外是原始的黑暗。烛龙蜷在黑暗之中想心事,有一天,我...
继续阅读 »

许多年前,也许是许多世纪前,我在陶盆里养了两条鱼。在此之前,陶盆里下着漫长的雨,一只蟾蜍在里面参禅,显得很寂寞。若干年后,雨下累了,月亮从桂枝上醒来,蟾蜍还困在禅意里。他俩都不知道对方是自己另外一个化身。

陶盆之外是原始的黑暗。烛龙蜷在黑暗之中想心事,有一天,我打开他的心事,点亮烛火,开始寂静地书写。我写到一些年代和场景,陶盆哭了,而那时,我的字句里还没有它。写到后来,雨水也哭了,而那时,它正想去陶盆漫长地飘落。

那一年春天,池塘边生出青草。母亲说,鱼该上岸了,它原本就是鹿。我让自己躺在梨花和雨飘过的窗下,漫不经心地构思字句。我想到,在原始的黑暗中,陶盆是唯一的光亮。年代在陶盆里不断进化,最后演变成大大小小的裂纹。

这时,邻家的女孩汲水归来,唱起一支悠长而陈旧的民歌,悠长得不知所终,陈旧到诗经出现之前。我构思的景象开始土崩瓦解,最后只剩下两粒羞涩的字。雨水下累那天,我将它们埋进陶盆,如同把秘密埋进心里。我知道,从此那两粒字将被我反复书写和记忆。

后来桂花开了,秋香飘过窗前,母亲说鹿该下水了,它原本就是鱼。月光下,鹿还在岸边吃草,两粒羞涩的字已化作游鱼,首尾相依,你追我赶,将万物搅成巨大的漩涡。黑暗坍塌了,烛龙收起心事,将我关在年代和场景的中心。

一天午后女孩停在窗前告诉我,很久以前,烛龙来到梦里替她照亮,她看见我在烛龙紧闭的心房喂鱼,大大小小的裂纹蛛网似的从四周缓慢地向我爬来。她还说,陶盆哭的时候,她看见雨水哭了,雨水哭的时候,她醒了。

这是一个幸福而悲伤的午后。我对她说,你在我的梦里梦见我的时候,我正在构思和书写。烛龙,游鱼,裂纹,以及你和你的梦,仅是我漫长书写中的一些温暖字句。这些字句有的已经完成,有的尚未写到,最终都会与我精心构思的景象一样,除了在某些悲伤的时刻对我有所安慰外,将变得毫无意义。我还告诉她,其实陶盆并不存在,雨水也不存在,蟾蜍偶尔闪过的禅意,或者月亮久已遗忘的光线之中,包含了我所有的书写。

2010-3-27


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

学习能力必然是职场的核心能力

最近新工作的编程语言换为了Golang,同时也在面试招聘相关岗位的人才。通过简历面试(别人的经历),以及自己的亲身学习经历,真切的感受到学习能力将是未来的一大竞争力。从面试方面来看,大多数人工作稳定之后便失去了学习能力,以为现在的工作可以长久的干下去。结果,互...
继续阅读 »

最近新工作的编程语言换为了Golang,同时也在面试招聘相关岗位的人才。通过简历面试(别人的经历),以及自己的亲身学习经历,真切的感受到学习能力将是未来的一大竞争力。

从面试方面来看,大多数人工作稳定之后便失去了学习能力,以为现在的工作可以长久的干下去。结果,互联网的风停下来之后,市场的需求变了,从单一的编程语言、单一业务的能力变成更加综合的能力,需要的人逐渐变为T型人才甚至π型人才。此时,学习能力就变得更加重要。否则,面临的只能是市场的淘汰。

下面分享一下自己最近三周学习Golang的一些经验和方法,大家可以拿来借鉴的其他学习方面上:

第一、实践。任何的学习都离不开实践。不能够运用到实践中的学习大概率是无效学习,而实践也是学习最有效的手段。在刚开学学习Golang时,找了一份基础语法的文档,花一两个小时看了一遍,知道常见的语法结构怎么用的,便开始搭建项目,写业务功能。其实这样的效果最快,以具体的功能实践来驱动学习,同时把对这方面的手感和思路锻炼出来。

第二、系统学习。单纯动手实践的过程中会掺杂着业务逻辑的实现,学习效率和范围上会有一些局限,属于用到什么学什么,缺点是不够系统。这时还需要一两本书,通读全书,帮助系统的了解这门语言(或某个行业)是怎么运作的,整个生态是什么样的,底层逻辑是怎样的,以便查漏补缺。在系统学习这块,建议以书籍为主,书籍的优势就是方便、快捷、系统、准确。

第三、交流。之前找一个懂的大佬请教和交流不是那么容易。但随着AI的发展,交流形式不仅仅限于大佬了,也可以是GPT。GPT最强大的能力是无所不知,知无不言。当然,对于它提供的结果也需要辩证的去看,某些地方可能会有错误,但大方向基本上是没错的,再辅以佐证,基本上能够解决80%的问题。

如果有机会参与面试,无论是作为面试官或者被面试者,都是一个交流的过程。在相互沟通的过程中了解市场需要什么,市场流行什么。

最后,针对某些问题,还是得去跟大佬交流才行,交流的过程中会碰撞出很多火花来。比如,不断的迭代某个算法,学到更好的实现方式,了解到你不知道的知识点等。曾经,一个字符串截取的功能,与大佬交流了三次,升级了三版,也学到了不同的API的使用方法和特性。

第四,输出。检验是否学会的一个标准就是你能否清晰的给别人描述出来,让别人听得懂。这一条是否很耳熟?对,它就是费曼学法,世界公认的最快的学习法。如果没办法很好的表达,说明这块掌握的还不是很清楚。当然,这个过程中也属于交流,也会拿到别人的反馈,根据别人的反馈来认识到自己的掌握程度和薄弱点。

第五,利用别人的时间。个人的时间总是有限的,不可能什么事情都自己做,也不可能都亲手验证。而作为管理者,最大的技能之一就是靠别人、靠团队来实现目标。那么,一个技术方案是否可行,是否有问题,也可以交给别人来调研、实践、验证。这样,可以让学习的效率并行起来。

另外,我们可能都听说过“一万小时定律”,这个概念是极具迷惑性的,会让你觉得学习任何东西都需要花费大量的时间的。其实不然,一万小时定律指的是学习一个复杂的领域并且成为这个领域的专家。

而我们在生活和实践的过程中,往往不需要什么方面都成为专家,只需要知道、掌握或会用某一领域的知识即可。对于入门一个新领域,一般来说,可能只需要20小时、100小时不等,没有想象中那么难。对于一个懂编程语言的人来说,从零学习另外一门语言,一般也就一两周时间就可以上手了。因此,我们不要对此产生畏惧心理。

上面讲的是学习方法,但最根本的是学习的意愿。你是选择花一年时间学习一门技术,然后重复十年,还是愿意每年都不断的学习迭代自己?两者的结果差距超乎你的想象。


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

像黑客一样使用 Linux 命令行

前言##之前看到一篇介绍 IntelliJ IDEA 配置的文章,它里面用的是 gif 动态图片进行展示,我觉得很不错。所以在我今天以及以后的博文中,我也会尽量使用 gif 动图进行展示。制作 gif 动图很花时间,为了把我的博客打造成精品我也是蛮拼的了。使用...
继续阅读 »

前言##

之前看到一篇介绍 IntelliJ IDEA 配置的文章,它里面用的是 gif 动态图片进行展示,我觉得很不错。所以在我今天以及以后的博文中,我也会尽量使用 gif 动图进行展示。制作 gif 动图很花时间,为了把我的博客打造成精品我也是蛮拼的了。使用动图的优点是演示效果好,缺点是动图体积过大,为了降低图片体积,我只能降低分辨率了。

关于高效使用命令行这个话题,在网上已经是老生常谈了。而且本文也借鉴了 CSDN 极客头条中推荐了的《像黑客一样使用 Linux 命令行》。但是在本文中,也有不少我自己的观点和体会,比如我会提到有些快捷键要熟记,有些则完全不需要记,毕竟我们的记忆力也是有限的,我还会提到一些助记的方法。所以,本文绝对不是照本宣科,值得大家拥有,请大家一定记得点赞。

使用 tmux 复用控制台窗口##

高效使用命令行的首要原则就是要尽量避免非命令行的干扰,什么意思呢?就是说一但开启了一个控制台窗口,就尽量不要再在桌面上切换来切换去了,不要一会儿被别的窗口挡住控制台,一会儿又让别的窗口破坏了控制台的背景,最好是把控制台最大化或全屏,最好连鼠标都不要用。只有这样,才能达到比较高的效率。但是在实际工作中,我们又经常需要同时在多个控制台中进行工作,例如:在一个控制台中运行录制屏幕的命令,在另外一个控制台中工作,或者在一个控制台中工作,在另外一个控制台中阅读文档。如果既想在多个控制台中工作,又不想一大堆窗口挡来挡去、换来换去的话,就可以考虑试试 tmux 了。如下图:

tmux 的功能很多,什么 Session 啊、Detach 啊、Atach 啊什么的我们暂时不用去关心,只用好它的控制台窗口复用功能就行了。tmux 中有 window 和 pane 的概念,tmux 可以创建多个 window,这些 window 是不会互相遮挡的,每次只显示一个 window,其它的 window 会自动隐藏,可以使用快捷键在 window 之间切换。同时,可以把一个 window 切分成多个 pane,这些 pane 同时显示在屏幕上,可以使用快捷键在 pane 之间切换。

tmux 的快捷键很多,要想全面了解 tmux 的最好办法当然是阅读 tmux 的文档了,使用命令 man tmux 就可以了。但是我们只需要记住少数几个重要的快捷键就可以了,如下表:

快捷键功能
Ctrl+B c创建一个 window
Ctrl+B [n][p]切换到下一个窗口或上一个窗口
Ctrl+B &关闭当前窗口
Ctrl+B "将当前 window 或 pane 切分成两个 pane,上下排列
Ctrl+B %将当前 window 或 pane 切分成两个 pane,左右排列
Ctrl+B x关闭当前 pane
Ctrl+B [↑][↓][←][→]在 pane 之间移动
Ctrl+[↑][↓][←][→]调整当前 pane 的大小,一次调整一格
Alt+[↑][↓][←][→]调整当前 pane 的大小,一次调整五格

tmux 的快捷键比较特殊,除了调整 pane 大小的快捷键之外,其它的都是先按 Ctrl+B,再按一个字符。先按 Ctrl+B,再按 c,就会创建一个 window,这里 c 就是 create window。先按 Ctrl+B,再按 n 或者 p,就可以在窗口之间切换,它们是 next window 和 previous window 的意思。关闭窗口是先按 Ctrl+B,再按 &,这个只能死记。先按 Ctrl+B,再按 " ,表示上下拆分窗口,可以想象成单引号和双引号在键盘上是上下铺关系。先按 Ctrl+B,再按 % 表示左右拆分窗口,大概是因为百分数都是左右书写的吧。至于在 pane 之间移动和调整 pane 大小的方向键,就不用多说了吧。

在命令行中快速移动光标##

在命令行中输入命令时,经常要在命令行中移动光标。这个很简单嘛,使用左右方向键就可以了,但是有时候我们输入了很长一串命令,却突然要修改这个命令最开头的内容,如果使用向左的方向键一个字符一个字符地把光标移到命令的开头,是否太慢了呢?有时我们需要直接在命令的开头和结尾之间切换,有时又需要能够一个单词一个单词地移动光标,在命令行中,其实这都不是事儿。如下图:

这几种移动方式都是有快捷键的。其实一个字符一个字符地移动光标也有快捷键 Ctrl+B 和 Ctrl+F,但是这两个快捷键我们不需要记,有什么能比左右方向键更方便的呢?我们真正要记的是下面这几个:

快捷键功能
Ctrl + A将光标移动到命令行的开头
Ctrl + E将光标移动到命令行的结尾
Alt + B将光标向左移动一个单词
Alt + F将光标向右移动一个单词

这几个快捷键太好记了,A 代表 ahead,E 代表 end,B 代表 back,F 代表 forward。为什么按单词移动光标的快捷键都是以 Alt 开头呢?那是因为按字符移动光标的快捷键把 Ctrl 占用了。但是按字符移动光标的快捷键我们用不到啊,因为我们有左右方向键啊。

在命令行中快速删除文本##

对输入的内容进行修改也是我们经常要干的事情,对命令行进行修改就涉及到先删除一部分内容,再输入新内容。我们碰到的情况是有时候只需要修改个别字符,有时候需要修改个别单词,而有时候,输入了半天的很长的一段命令,我们说不要就全都不要了,整行删除。常用的删除键当然是 BackSpace 和 Delete 啦,不过一次删除一个字符,是否太慢了呢?那么,请熟记以下几个快捷键吧:

快捷键功能
Ctrl + U删除从光标到行首的所有内容,如果光标在行尾,自然就整行都删除了啊
Ctrl + K删除从光标到行尾的所有内容,如果光标在行首,自然也是整行都删除了啊
Ctrl + W删除光标前的一个单词
Alt + D删除光标后的一个单词
Ctrl + Y将刚删除的内容粘贴到光标处,有时候删错了可以用这个快捷键恢复删除的内容

效果请看下图:

这几个快捷键也是蛮好记的,U 代表 undo,K 代表 kill,W 代表 word,D 代表 delete, Y 代表 yank。其中比较奇怪的是 Alt+D 又是以 Alt 开头的,那是因为 Ctrl+D 又被占用了。Ctrl+D 有几个意思,在编辑命令行的时候它代表删除一个字符,当然,这个快捷键其实我们用不到,因为 BackSpace 和 Delete 方便多了。在某些程序从 stdin 读取数据的时候,Ctrl+D 代表 EOF,这个我们偶尔会用到。

快速查看和搜索历史命令##

对于曾经运行过的命令,除非特别短,我们一般不会重复输入,从历史记录中找出来用自然要快得多。我们用得最多的就是 ↑ 和 ↓,特别是不久前才刚刚输入过的命令,使用 ↑ 向上翻几行就找到了,按一下 Enter 就执行,多舒服。但是有时候,明明记得是不久前才用过的命令,但是向上翻了半天也没找到,怎么办?那只好使用 history 命令来查看所有的历史记录了。历史记录又特别长,怎么办?可以使用 history | less 和 history | grep '...'。但是还有终极大杀招,那就是按 Ctrl+R 从历史记录中进行搜索。按了 Ctrl+R 之后,每输入一个字符,都会和历史记录中进行增量匹配,输入得越多,匹配越精确。当然,有时候含有相同搜索字符串的命令特别多,怎么办?继续按 Ctrl+R,就会继续搜索下一条匹配的历史记录。如下图:

这里,需要记住的命令和快捷键如下表:

命令或快捷键功能
history查看历史记录
historyless分页查看历史记录
historygrep '...'在历史记录中搜索匹配的命令,并显示
Ctrl + R逆向搜索历史记录,和输入的字符进行增量匹配
Esc停止搜索历史记录,并将当前匹配的结果放到当前输入的命令行上
Enter停止搜索历史记录,并将当前匹配的结果立即执行
Ctrl + G停止搜索历史记录,并放弃当前匹配的结果
Alt + >将历史记录中的位置标记移动到历史记录的尾部

这里需要注意的是,当我们在历史记录中搜索的时候,是有位置标记的,Ctrl+R 是指从当前位置开始,逆向搜索,R 代表的是 reverse,每搜索一条记录,位置标记都会向历史记录的头部移动,下次搜索又从这里开始继续向头部搜索。所以,我们一定要记住快捷键 Alt+>,它可以把历史记录的位置标记还原。另外需要注意的是停止搜索历史记录的快捷键有三个,如果按 Enter 键,匹配的命令就立即执行了,如果你还想有修改这条命令的机会的话,一定不要按 Enter,而要按 Esc。如果什么都不想要,就按 Ctrl+G 吧,它会还你一个空白的命令行。

快速引用和修饰历史命令##

除了查看和搜索历史记录,我们还可以以更灵活的方式引用历史记录中的命令。常见的简单的例子有 !! 代表引用上一条命令,!$代表引用上一条命令的最后一个参数,^oldstring^newstring^代表将上一条命令中的 oldstring 替换成 newstring。这些操作是我们平时使用命令行的时候的一些常用技巧,其实它们的本质,是由 history 库提供的 history expansion 功能。Bash 使用了 history 库,所以也能使用这些功能。其完整的文档可以查看 man history 手册页。知道了 history expansion 的理论,我们还可以做一些更加复杂的操作,如下图:

引用和修饰历史命令的完整格式是这样的:

![!|[?]string|[-]number]:[n|x-y|^|$|*|n*|%]:[h|t|r|e|p|s|g]

可以看到,一个对历史命令的引用被 : 分为了三个部分,第一个部分决定了引用哪一条历史命令;第二部分决定了选取该历史命令中的第几个单词,单词是从0开始编号的,也就是说第0个单词代表命令本身,第1个到最后一个单词代表命令的参数;第三部分决定了对选取的单词如何修饰。下面我列出完整表格:

表格一、引用哪一条历史命令:

操作符功能
!所有对历史命令的引用都以 ! 开始,除了 oldstringnewstring^ 形式的快速替换
!n引用第 n 条历史命令
!-n引用倒数第 n 条历史命令
!!引用上一条命令,等于 !-1
!string逆向搜索历史记录,第一条以 string 开头的命令
!?string[?]逆向搜索历史记录,第一条包含 string 的命令
oldstringnewstring^对上一条命令进行快速替换,将 oldstring 替换为 newstring
!#引用当前输入的命令

表格二、选取哪一个单词:

操作符功能
0第0个单词,在 shell 中就是命令本身
n第n个单词
第1个单词,使用 ^ 时可以省略前面的冒号
$最后一个单词,使用 $ 是可以省略前面的冒号
%和 ?string? 匹配的单词,可以省略前面的冒号
x-y从第 x 个单词到第 y 个单词,-y 代表 0-y
*除第 0 个单词外的所有单词,等于 1-$
x*从第 x 个单词到最后一个单词,等于 x-$,可以省略前面的冒号
x-从第 x 个单词到倒数第二个单词

表格三、对选取的单词做什么修饰:

操作符功能
h选取路径开头,不要文件名
t选取路径结尾,只要文件名
r选取文件名,不要扩展名
e选取扩展名,不要文件名
s/oldstring/newstring/将 oldstring 替换为 newstring
g全局替换,和 s 配合使用
p只打印修饰后的命令,不执行

这几个命令其实挺好记的,h 代表 head,只要路径开头不要文件名,t 代表 tail,只要路径结尾的文件名,r 代表 realname,只要文件名不要扩展名,e 代表 extension,只要扩展名不要文件名,s 代表 substitute,执行替换功能,g 代表 global,全局替换,p 代表 print,只打印不执行。有时候光使用 :p 还不够,我们还可以把这个经过引用修饰后的命令直接在当前命令行上展开而不立即执行,它的快捷键是:

操作符功能
Ctrl + Alt + E在当前命令行上展开历史命令引用,展开后不立即执行,可以修改,按 Enter 后才会执行
Alt + ^和上面的功能一样

这两个快捷键,记住一个就行。这样,当我们对历史命令的引用修饰完成后,可以先展开来看一看,如果正确再执行。眼见为实嘛,反正我是每次都展开看看才放心。

录制屏幕并转换为 gif 动画图片##

最后,给大家展示我做 gif 动画图片的过程。我用到的软件有 recordmydesktopmplayer 和 convert。使用 recordmydesktop 时需要一个单独的控制台来运行录像功能,录像完成后需要在该控制台中输入 Ctrl+C 终止录像。所以我用到了 tmux 。首先,我启动 tmux,然后运行 recordmydesktop --full-shots --fps 2 --no-sound --no-frame --delay 5 -o ~/图片/record_to_gif.ogv命令开始录像。由于 recordmydesktop 运行后不会马上退出,录像开始后,这个 window 就被占用了,所以我按 Ctrl+B c 让 tmux 再创建一个 window,然后在这个 window 中做的任何操作都会被录制下来。被录制的操作完成后,按 Ctrl+B n 切换到 recordmydesktop 命令运行的窗口,按 Ctrl+C 终止录像。然后,使用 mplayer -ao null record_to_gif.ogv -vo jpeg:outdir=./record_to_gif 将录制的视频提取为图片。当然,这时的图片比较多,为了缩减最后制作成的 gif 文件的大小,我们可以删掉其中无关紧要的帧,只留下关键帧。最后使用命令 convert -delay 100 record_to_gif/* record_to_gif.gif 生成 gif 动画。整个过程如下图:

最后生成的 gif 图片一般都比较大,往往超过 20M,如果时间长一点,超过 60M 也是常事儿。而制作成 gif 之前每一帧图片也就 200k 左右而已。我想可能是因为 gif 没有像 jpeg 或 png 这么好的压缩算法吧。gif 对付向量图效果很不错,对付照片和我这样的截图,压缩就有点力不从心了。博客园允许上传的图片每张不能超过 10M,所以,为了减小 gif 文件的体积,我只有用 convert -resize 1024x576 record_to_gif.gif record_to_gif_small.gif 命令将图片变小后再上传了。

总结##

使用 Linux 命令行的技巧还有很多,我这里不可能全部讲到。学习 Linux 命令行的最好办法当然还是使用 man bash 查看 Bash 的文档。但是我这里讲的内容已经可以显著提高使用命令行的效率了,至少这两天下来,我觉得我自己有了质的飞跃。另外,在博客中使用 gif 动态图片做示例,我觉得也是我写博客以来一个质的飞跃。希望大家喜欢。


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

Flutter 混合架构方案探索

得益于 Flutter 优秀的跨平台表现,混合开发在如今的 App 中随处可见,如最近微信公布的小程序新渲染引擎 Skyline 发布正式版也在底层渲染上使用了 Flutter,号称渲染速度提升50%。在现有的原生 App 中引入 Flutter 来开发不是一...
继续阅读 »

得益于 Flutter 优秀的跨平台表现,混合开发在如今的 App 中随处可见,如最近微信公布的小程序新渲染引擎 Skyline 发布正式版也在底层渲染上使用了 Flutter,号称渲染速度提升50%。

在现有的原生 App 中引入 Flutter 来开发不是一件简单的事,需要解决混合模式下带来的种种问题,如路由栈管理、包体积和内存突增等;另外还有一种特殊的情况,一个最初就由 Flutter 来开发的 App 也有可能在后期混入、原生 View 去开发。

我所在的团队目前就是处于这种情况,Flutter 目前在性能表现上面还不够完美,整体页面还不够流畅,并且在一些复杂的页面场景下会出现比较严重的发热行为,尽管目前 Flutter 团队发布了新的渲染引擎 impeller,它在 iOS 上表现优异,流畅度有了质的提升,但还是无法完全解决一些性能问题且 Android 下 impeller 也还没开发完成。

为了应对当下出现的困局和以后可能出现的未知问题,我们期望通过混合模式来扩宽更多的可能性。

路由管理

混合开发下最难处理的就是路由问题了,我们知道原生和 Flutter 都有各自的路由管理系统,在原生页面和 Flutter 页面穿插的情况下如何统一管理和互相交互是一大难点。目前比较流行的单引擎方案,代表框架是闲鱼团队出品flutter_boost;flutter 官方代表的多引擎解决方案 FlutterEngineGroup

单引擎方案 flutter_boost

flutter_boost 通过复用 Engine 达到最小内存的目的

在引擎处理上,flutter_boost 定义了一个通用的 CacheId:"flutter_boost_default_engine",当原生需要跳转到 Flutter 页面时,通过FlutterEngineCache.getInstance().get(ENGINE_ID); 获取同一个 Engine,这样无论打开了多少如图中的 A、B、C 的 Flutter 页面时,都不会产生额外的Engine内存损耗。

public class FlutterBoost {
public static final String ENGINE_ID = "flutter_boost_default_engine";
...
}

另外,双端都注册了导航的接口,通过Channel来通知,用于请求路由变化、页面返回以及页面的生命周期处理等。在这种模式下,这一层Channel的接口处理是重点。

多引擎方案 FlutterEngineGroup

为了应对内存爆炸问题,官方对多引擎场景做了优化,FlutterEngineGroup应运而生,FlutterEngineGroup下的 Engine 共用一些通用的资源,例如GPU 上下文、线程快照等,生成额外的 Engine 时,号称内存占用缩小到 180k。这个程度,基本可以视为正常的损耗了。

以上图中的 B、C 页面为例,两者都是 Flutter 页面,在 FlutterEngineGroup 这种处理下,因为它们所在的 Engine 不是同一个,这会产生完全的隔离行为,也就是 B、C 页面使用不同的堆栈,处在不同的 Isolate 中,两者是无法直接进行交互的。

多引擎的优点是:它可以抹掉上图所示的 F、E、C 和 D、A 等内部路由,每次新增 Flutter 页面时,全部回调到原生,让原生生成新的 Engine 去承载页面,这样路由的管理全部由原生去处理,一个 Engine 只对应一个 Flutter 页面。

但它也会带来一些额外的处理,像上面提到的,处在不同 Engine 下的Flutter 页面之间是无法直接交互的,如果涉及到需要通知和交互的场景,还得通过原生去转发。

关于FlutterEngineGroup的更多信息,可以参考官方说明

性能对比

官方号称 FlutterEngineGroup 创建新的 Engine 只会占用 180k 的内存,那么是不是真就如它所说呢?下面我们来针对上面这两种方案做一个内存占用测试

flutter_boost

测试机型:OPPO CPH2269

测试代码:github.com/alibaba/flu…

内存 dump 命令: adb shell dumpsys meminfo com.idlefish.flutterboost.example

条件PSSRSS最大变化
1 Native88667165971
+26105+28313+27M
1 Native + 1 Flutter114772194284
-282+1721+1M
2 Native + 2 Flutter114490196005
+5774+5992+6M
5 Native + 5 Flutter120264201997
+13414+14119+13M
10 Native + 10 Flutter133678216116

第一次加载 Flutter 页面时,增加 27M 左右内存,此后多开一个页面内存增加呈现从 1M -> 2M -> 2.6 M 这种越来越陡的趋势(数值只是参考,因为其中有 Native 页面,只看趋势变化上看)

FlutterEngineGroup

测试机型:OPPO CPH2269

测试代码:github.com/flutter/sam…

内存 dump 命令: adb shell dumpsys meminfo dev.flutter.multipleflutters

条件PSSRSS最大变化
1 Native45962140817
+29822+31675+31M
1 Native + 1 Flutter75784172492
-610+2063+2M
2 Native + 2 Flutter75174174555
+7451+7027+3.7M
5 Native + 5 Flutter82625181582
+8558+7442+8M
10 Native + 10 Flutter91183189024

第一次加载 Flutter 页面时,增加 31M 左右内存,此后多开一个页面内存增加呈现从 1M -> 1.2M -> 1.6 M 这种越来越陡的趋势(数值只是参考,因为其中有 Native 页面,只看趋势变化上看)

结论

两个测试使用的是不同的 demo 代码,不能通过数值去得出孰优孰劣。但通过数值的表现,我们基本可以确认,两个方案都不会带来异常的内存暴涨,完全在可以接受的范围。

PlatformView

PlatformView 也可实现混合 UI,Flutter 中的 WebView 就是通过 PlatformView 这种方式引入的。

PlatformView 允许我们向 Flutter 界面中插入原生 View,在一个页面的最外层包裹一层 PlatformView,路由的管理都由 Flutter 来处理。这种方式下没有额外的 Engine 产生,是最简单的混合方式。

但它也有缺点,不适合主 Native 混 Flutter 的场景,而现在大多都是以主 Native 混 Flutter的场景为主。另外,PlatformView 因其底层实现,会出现兼容性问题,在一些机型下可能会出现键盘问题、闪烁或其它的性能开销,具体可看这篇介绍

数据共享

原生和 Flutter 使用不同的开发语言去开发,所以在一侧定义的数据结构对象和内存对象对方都无法感知,在数据同步和处理上必须使用其它手段。

MethodChannel

Flutter 开发者对 MethodChannel 一定不陌生,开发当中免不了跟原生交互,MethodChannel 是双向设计,即允许我们在 Flutter 中调用原生的方法,也允许我们在原生中调用 Flutter 的方法。对 Channel 不太了解的可以看一下官方文档,如文档中提到的,这个通道传输的过程中需要将数据编解码,对应的关系以kotlin为例(完整的映射可以查看文档):

Dart                         | Kotlin      |
| -------------------------- | ----------- |
| null | null |
| bool | Boolean |
| int | Int |
| int, if 32 bits not enough | Long |
| double | Double |
| String | String |
| Uint8List | ByteArray |
| Int32List | IntArray |
| Int64List | LongArray |
| Float32List | FloatArray |
| Float64List | DoubleArray |
| List | List |
| Map | HashMap |

本地存储

这种方式比较容易理解,将本地存储视为中转站,Flutter中将数据操作存储到本地上,回到原生页面时在某个时机(如onResume)去查询本地数据库即可,反之亦然。

问题

不管是MethodChannel或是本地存储,都会面临一个问题:对象的数据结构是独立的,两边需要重复定义。比如我在 Flutter 中有一个 Student 对象,Android 端也要定义一个同样结构的 Student,这样才能方便操作,现在我将Student student转成Unit8List传到Android,Channel中解码成Kotlin能操作的ByteArray,再将ByteArray转译成AndroidStudent对象。

class Student {
String name;
int age;
Student(this.name, this.age);
}

对于这个问题最好的解决办法是使用DSL一类的框架,如Google的ProtoBuf,将同一份对象配置文件编译到不同的语言环境中,便能省去这部分双端重复定义的行为。

图片缓存

在内存方面,如果同样的图片在两边都加载时,会使得原生和 Flutter 都会产生一次缓存。在 Flutter 下默认就会缓存在ImageCache中,原生下不同的框架由不同的对象负责,为了去掉重复的图片缓存,势必要统一图片的加载管理。

阿里的方案也是如此,通过外接原生图片库,共享图片的本地文作缓存和内存缓存。它的实现思路是通过自定义ImageProviderCodec,对接外部图库,获取到图片数据做解析,对接的处理是通过扩展 Flutter Engine。

如果期望不修改Flutter Engine,也可通过外接纹理的方式去处理。通过PlatformChannel去请求原生,使到图片的外接纹理数据,通过TextTure组件展示图片。

// 自定义 ImageProvider 中,通过 Channel 去请求 textureId
var id = await _channel.invokeMethod('newTexture', {
"imageUrl": imageUrl,
"width": width ?? 0,
"height": height ?? 0,
"minWidth": constraints.minWidth,
"minHeight": constraints.minHeight,
"maxWidth": constraints.maxWidth,
"maxHeight": constraints.maxHeight,
"cacheKey": cacheKey,
"fit": fit.index,
"cacheOriginFile": cacheOriginFile,
});

// ImageWidget 中展示时通过 textureId 去显示图片
SizedBox(
width: width,
heigt: height,
child: Texture(
filterQuality: FilterQuality.high,
textureId: _imageProvider.textureId.value,
),
)

总结

不同业务对于混合的程度和要求有所要求,并没有万能的方案。比如我团队的情况就是主Flutter混原生,在路由管理上我选择了PlatformView这种处理模式,这种方式更容易开发和维护,后期如果发现有兼容性问题,也可过渡到flutter_boostFlutterEngineGroup上。


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

7个你应该知道的Glide的使用技巧

对于Android开发者来说,Glide是最常使用的库。这里介绍了开发过程中,7个使用Glide的技巧。不要使用wrap_content不清楚你是否这样使用过,把 ImageView 的宽和高设置成 wrap_content,并通过Glide来加载图...
继续阅读 »

对于Android开发者来说,Glide是最常使用的库。这里介绍了开发过程中,7个使用Glide的技巧。

不要使用wrap_content

不清楚你是否这样使用过,把 ImageView 的宽和高设置成 wrap_content,并通过Glide来加载图片

<ImageView
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

Glide.with(context)
.load(url)
.into(image)

为什么不建议把ImageView设置成 wrap_content,我们看一下Glide的文档是怎么说的(文档地址中文地址 最新英文地址):

文档上写得很明显,在某些情况下会使用屏幕的尺寸代替 wrap_content,这可能导致原来的小图片变成大图,Glide transform 问题分析这篇文章就介绍了这种问题。为了避免这种情况发生,我们最好是不要使用 wrap_content。当然如果你实在是需要使用 wrap_content,你可以按照Glide的建议,使用Target.SIZE_ORIGINAL。

需要注意的是:使用Target.SIZE_ORIGINAL 在加载大图时可能造成oom,因此你需要确保加载的图片不会太大。

自定义内存缓存大小

在某些情况下,我们可能需要自定义Glide的内存缓存大小和Bitmap池的大小,比如图片显示占大头的app,就希望Glide的图片缓存大一些。Glide内部使用MemorySizeCalculator类来决定内存缓存和Bitmap池的大小。

@GlideModule
class MyGlideModel: AppGlideModule() {

override fun applyOptions(context: Context, builder: GlideBuilder) {
super.applyOptions(context, builder)
//通过MemorySizeCalculator获取MemoryCache和BitmapPool的size大小
val calculator = MemorySizeCalculator.Builder(context).build()
val defaultMemoryCacheSize = calculator.memoryCacheSize
val defaultBitmapPoolSize = calculator.bitmapPoolSize

//根据业务计算出需要的缓存大小,这里简化处理,都乘以1.5
val customMemoryCacheSize = (1.5 * defaultMemoryCacheSize).toLong()
val customBitmapPoolSize = (1.5 * defaultBitmapPoolSize).toLong()
//设置缓存
builder.setMemoryCache(LruResourceCache(customMemoryCacheSize))
builder.setBitmapPool(LruBitmapPool(customBitmapPoolSize))
}
}

memoryCache 和 BitmapPool 的区别:

  • memoryCache:通过key-value才缓存数据,缓存之前用过的Bitmap
  • BitmapPool:重用Bitmap对象的对象池,根据Bitmap的宽高来复用。复用的原理可以看Bitmap全解析

具体区别见What is difference between MemoryCacheSize and BitmapPoolSize in Glide

自定义磁盘缓存

Glide 使用 DiskLruCacheWrapper 作为默认的 磁盘缓存 。 DiskLruCacheWrapper 是一个使用 LRU 算法的固定大小的磁盘缓存。默认磁盘大小为 250 MB ,位置是在应用的 缓存文件夹 中的一个 特定目录 。我们也可以自定义磁盘缓存,代码如下:

@GlideModule

class MyGlideModel: AppGlideModule() {

override fun applyOptions(context: Context, builder: GlideBuilder) {
super.applyOptions(context, builder)
val size: Long = 1024 * 1024 * 100 //100MB
builder.setDiskCache(InternalCacheDiskCacheFactory(context, cacheFolderName, size))
}
}

牢记在onLoadCleared释放图片资源

如上图Glide的官方文档所示,我们在使用Target时,必须在重新绘制(通常是View)或改变其可见性之前,你必须确保在onResourceReady中收到的任何当前Drawable不再被使用。这是因为Glide内部缓存在内存不足或者主动回收Glide.get(context).clearMemory()时,会回收Bitmap,如果此时ImageView还使用被回收的Bitmap,就会发生 trying to use a recycled bitmap 的错误。

解决办法是不再使用在onResourceReady中获取的Bitmap,代码如下:

        Glide.with(this)
.load(Url)
.into(object : CustomTarget<Bitmap>(width, height) {
override fun onResourceReady(
resource: Bitmap,
transition: Transition<in Bitmap>?,
) {
mBitmap = resource
}

override fun onLoadCleared(placeholder:Drawable?){
mBitmap = null
}
})

优先加载指定图片

如上图所示,当一个页面有多个图片时,我们希望某些图片优先被加载出来(这个界面里面是上面的一拳超人的封面),某些图片后加载,比如这个界面里的互动点评的用户头像列表。Glide提供了优先级来解决这个问题,它的优先级如下:

  • Priority.LOW
  • Priority.NORMAL
  • Priority.HIGH
  • Priority.IMMEDIATE

使用代码如下:

    Glide
.with(context)
.load("url")
.priority(Priority.LOW)//底优先级的图片
.into(imageView);

Glide
.with(context)
.load("url")
.priority(Priority.HIGH)//高优先级的图片
.into(imageView);

注意:优先级高的加载任务会尽量首先启动,但是无法保证加载开始或完成的顺序。

使用Glide前,先判断页面是否回收

一般我们会通过网络请求来获取图片的链接,再通过Glide来加载图片,代码如下:

service?.fetchUserProfile(id) { result, errMsg, icon ->
if (result == 200) {
Glide.with(context)
.load(icon)
.into(view)
}
}

但是这里有个问题,当界面被destory后,这个网络请求刚好成功了,调用Glide.with就会发生 You cannot start a load for a destroyed activity错误。解决方法是在调用Glide.with前先判断,代码如下:


service?.fetchUserProfile(id) { result, errMsg, icon ->
if (result == 200) {
if (context is FragmentActivity) {
if ((context as FragmentActivity).isFinishing || (context as FragmentActivity).isDestroyed) {
return
}
}
Glide.with(context)
.load(icon)
.into(view)
}
}

加载大图时使用skipMemoryCache

当我们使用Glide加载大图时,应该避免使用内存缓存,如果不好好处理可能发生oom。在Glide中,我们可以使用skipMemoryCache来跳过内存缓存。代码如下:

    Glide.with(context)
.load(url)
.skipMemoryCache(true)
.into(imageview)

与skipMemoryCache对应的是 onlyRetrieveFromCache,它只从缓存中获取对象,不会从网络或者本地缓存中就直接加载失败。


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

RecyclerView刷新后定位问题

问题描述做需求开发时,遇到RecyclerView刷新时,通常会使用notifyItemXXX方法去做局部刷新。但是刷新后,有时会遇到RecyclerView定位到我们不希望的位置,这时候就会很头疼。这周有时间深入了解了下RecyclerView的源码,大致梳...
继续阅读 »

问题描述

做需求开发时,遇到RecyclerView刷新时,通常会使用notifyItemXXX方法去做局部刷新。但是刷新后,有时会遇到RecyclerView定位到我们不希望的位置,这时候就会很头疼。这周有时间深入了解了下RecyclerView的源码,大致梳理清楚刷新后位置跳动的原因了。

原因分析

先简单描述下RecyclerView在notify后的过程:

  1. 根据是否是全量刷新来选择触发RecyclerView.RecyclerViewDataObserver的onChanged方法或onItemRangeXXX方法

onChanged会直接调用requestlayout来重新layuout。 onItemRangeXXX会先把刷新数据保存到mAdapterHelper中,然后再调用requestlayout 2. 进入dispatchLayout流程 这一步分为三个步骤:

  • dispatchLayoutStep1:处理adapter的更新、决定哪些view执行动画、保存view的信息
  • dispatchLayoutStep2:真正执行childView的layout操作
  • dispatchLayoutStep3:触发动画、保存状态、清理信息

需要注意的是,在onMeasure的过程中,如果传入的measureMode不是exactly,会去调用dispatchLayoutStep1和dispatchLayoutStep2从而取得真正需要的宽高。 所以在dispatchLayout会先判断是否需要重新执行dispatchLayoutStep1和dispatchLayoutStep2

重点分析dispatchLayoutStep2这一步: 核心操作在 mLayout.onLayoutChildren(mRecycler, mState)这一行。以LinearLayoutManager为例继续往下挖:

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
...
final View focused = getFocusedChild();
if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
|| mPendingSavedState != null) {
mAnchorInfo.reset();
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
// 关键步骤1,寻找锚点View位置
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
mAnchorInfo.mValid = true;
} else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)
>= mOrientationHelper.getEndAfterPadding()
|| mOrientationHelper.getDecoratedEnd(focused)
<= mOrientationHelper.getStartAfterPadding())) {
mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));
}
...
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForEnd;
//关键步骤2,从锚点View位置往后填充
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
final int lastElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
//如果锚点位置后面数据不足,无法填满剩余的空间,那把剩余空间加到顶部
extraForStart += mLayoutState.mAvailable;
}
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForStart;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
//关键步骤3,从锚点View位置向前填充
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;

if (mLayoutState.mAvailable > 0) {
//如果锚点View位置前面数据不足,那把剩余空间加到尾部再做一次尝试
extraForEnd = mLayoutState.mAvailable;
// start could not consume all it should. add more items towards end
updateLayoutStateToFillEnd(lastElement, endOffset);
mLayoutState.mExtraFillSpace = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
}
}

先解释一下锚点View,锚点View在一次layout过程中的位置不会发生变化,即之前在哪里显示,这次layout完还在哪,从视觉上看没有位移。

总结一下,mLayout.onLayoutChildren主要做了以下几件事:

  1. 调用updateAnchorInfoForLayout方法确定锚点view位置
  2. 从锚点view后面的位置开始填充,直到后面空间被填满或者已经遍历到最后一个itemView
  3. 从锚点view前面的位置开始填充,直到空间被填满或者遍历到indexe为0的itemView
  4. 经过第三步后仍有剩余空间,则把剩余空间加到尾部再做一次尝试

所以回到一开始的问题,RecyclerView在notify之后位置跳跃的关键在于锚点View的确定,也就是updateAnchorInfoForLayout方法,所以下面重点看下这个方法:

private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
AnchorInfo anchorInfo) {
if (updateAnchorFromPendingData(state, anchorInfo)) {
if (DEBUG) {
Log.d(TAG, "updated anchor info from pending information");
}
return;
}

if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
if (DEBUG) {
Log.d(TAG, "updated anchor info from existing children");
}
return;
}
if (DEBUG) {
Log.d(TAG, "deciding anchor info for fresh state");
}
anchorInfo.assignCoordinateFromPadding();
anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
}

这个方法比较短,所以代码全贴出来了。如果是调用了scrollToPosition后的刷新,会通过updateAnchorFromPendingData方法确定锚点View位置,否则调用updateAnchorFromChildren来计算:

private boolean updateAnchorFromChildren(RecyclerView.Recycler recycler,
RecyclerView.State state, AnchorInfo anchorInfo) {
if (getChildCount() == 0) {
return false;
}
final View focused = getFocusedChild();
if (focused != null && anchorInfo.isViewValidAsAnchor(focused, state)) {
anchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));
return true;
}
if (mLastStackFromEnd != mStackFromEnd) {
return false;
}
View referenceChild =
findReferenceChild(
recycler,
state,
anchorInfo.mLayoutFromEnd,
mStackFromEnd);
if (referenceChild != null) {
anchorInfo.assignFromView(referenceChild, getPosition(referenceChild));
...
return true;
}
return false;
}

代码比较简单,如果有焦点View,并且焦点View没被remove,则使用焦点View作为锚点。否则调用findReferenceChild来查找:

View findReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state,
boolean layoutFromEnd, boolean traverseChildrenInReverseOrder) {
ensureLayoutState();

// Determine which direction through the view children we are going iterate.
int start = 0;
int end = getChildCount();
int diff = 1;
if (traverseChildrenInReverseOrder) {
start = getChildCount() - 1;
end = -1;
diff = -1;
}

int itemCount = state.getItemCount();

final int boundsStart = mOrientationHelper.getStartAfterPadding();
final int boundsEnd = mOrientationHelper.getEndAfterPadding();

View invalidMatch = null;
View bestFirstFind = null;
View bestSecondFind = null;

for (int i = start; i != end; i += diff) {
final View view = getChildAt(i);
final int position = getPosition(view);
final int childStart = mOrientationHelper.getDecoratedStart(view);
final int childEnd = mOrientationHelper.getDecoratedEnd(view);
if (position >= 0 && position < itemCount) {
if (((RecyclerView.LayoutParams) view.getLayoutParams()).isItemRemoved()) {
if (invalidMatch == null) {
invalidMatch = view; // removed item, least preferred
}
} else {
// b/148869110: usually if childStart >= boundsEnd the child is out of
// bounds, except if the child is 0 pixels!
boolean outOfBoundsBefore = childEnd <= boundsStart && childStart < boundsStart;
boolean outOfBoundsAfter = childStart >= boundsEnd && childEnd > boundsEnd;
if (outOfBoundsBefore || outOfBoundsAfter) {
// The item is out of bounds.
// We want to find the items closest to the in bounds items and because we
// are always going through the items linearly, the 2 items we want are the
// last out of bounds item on the side we start searching on, and the first
// out of bounds item on the side we are ending on. The side that we are
// ending on ultimately takes priority because we want items later in the
// layout to move forward if no in bounds anchors are found.
if (layoutFromEnd) {
if (outOfBoundsAfter) {
bestFirstFind = view;
} else if (bestSecondFind == null) {
bestSecondFind = view;
}
} else {
if (outOfBoundsBefore) {
bestFirstFind = view;
} else if (bestSecondFind == null) {
bestSecondFind = view;
}
}
} else {
// We found an in bounds item, greedily return it.
return view;
}
}
}
}
// We didn't find an in bounds item so we will settle for an item in this order:
// 1. bestSecondFind
// 2. bestFirstFind
// 3. invalidMatch
return bestSecondFind != null ? bestSecondFind :
(bestFirstFind != null ? bestFirstFind : invalidMatch);
}

解释一下,查找过程会遍历RecyclerView当前可见的所有childView,找到第一个没被notifyRemove的childView就停止查找,否则会把遍历过程中找到的第一个被notifyRemove的childView作为锚点View返回。

这里需要注意final int position = getPosition(view);这一行代码,getPosition返回的是经过校正的最终position,如果ViewHolder被notifyRemove了,这里的position会是0,所以如果可见的childView都被remove了,那最终定位的锚点View是第一个childView,锚点的position是0,偏移量offset是这个被删除的childView的top值,这就会导致后面fill操作时从位置0开始填充,先把position=0的view填充到偏移量offset的位置,再往后依次填满剩余空间,这也是导致画面上的跳动的根本原因。


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

如何开启一个线程,开启大量线程会有什么问题,如何优化?(美团面试问道)

这是我一个朋友在美团面试中遇到的一个问题,今天拿出来解析一下正文如何开启一个线程如何开启一个线程,再JDK中的说明为:/** * ... * There are two ways to create a new thread of execution. O...
继续阅读 »

这是我一个朋友在美团面试中遇到的一个问题,今天拿出来解析一下

正文

如何开启一个线程

如何开启一个线程,再JDK中的说明为:

/**
* ...
* There are two ways to create a new thread of execution. One is to
* declare a class to be a subclass of <code>Thread</code>.
* The other way to create a thread is to declare a class that
* implements the <code>Runnable</code> interface.
* ....
*/
public class Thread implements Runnable{
     
}

Thread源码的类描述中有这样一段,翻译一下,只有两种方法去创建一个执行线程,一种是声明一个Thread的子类,另一种是创建一个类去实现Runnable接口。

继承Thread类
public class ThreadUnitTest {

   @Test
   public void testThread() {
       //创建MyThread实例
       MyThread myThread = new MyThread();
       //调用线程start的方法,进入可执行状态
       myThread.start();
  }

   //继承Thread类,重写内部run方法
   static class MyThread extends Thread {

       @Override
       public void run() {
           System.out.println("test MyThread run");
      }
  }
}
实现Runnable接口
public class ThreadUnitTest {

   @Test
   public void testRunnable() {
       //创建MyRunnable实例,这其实只是一个任务,并不是线程
       MyRunnable myRunnable = new MyRunnable();
       //交给线程去执行
       new Thread(myRunnable).start();
  }

   //实现Runnable接口,并实现内部run方法
   static class MyRunnable implements Runnable {

       @Override
       public void run() {
           System.out.println("test MyRunnable run");
      }
  }
}
实现Callable

其实实现Callback接口创建线程的方式,归根到底就是Runnable方式,只不过它是在Runnable的基础上又增加了一些能力,例如取消任务执行等。

public class ThreadUnitTest {

   @Test
   public void testCallable() {
       //创建MyCallable实例,需要与FutureTask结合使用
       MyCallable myCallable = new MyCallable();
       //创建FutureTask,与Runnable一样,也只能算是个任务
       FutureTask<String> futureTask = new FutureTask<>(myCallable);
       //交给线程去执行
       new Thread(futureTask).start();

       try {
           //get方法获取任务返回值,该方法是阻塞的
           String result = futureTask.get();
           System.out.println(result);
      } catch (ExecutionException e) {
           e.printStackTrace();
      } catch (InterruptedException e) {
           e.printStackTrace();
      }
  }

   //实现Callable接口,并实现call方法,不同之处是该方法有返回值
   static class MyCallable implements Callable<String> {

       @Override
       public String call() throws Exception {
           Thread.sleep(10000);
           return "test MyCallable run";
      }
  }
}

Callable的方式必须与FutureTask结合使用,我们看看FutureTask的继承关系:

//FutureTask实现了RunnableFuture接口
public class FutureTask<V> implements RunnableFuture<V> {

}

//RunnableFuture接口继承Runnable和Future接口
public interface RunnableFuture<V> extends Runnable, Future<V> {
   void run();
}

开启大量线程会引起什么问题

在Java中,调用Thread的start方法后,该线程即置为就绪状态,等待CPU的调度。这个流程里有两个关注点需要去理解。

start内部怎样开启线程的?看看start方法是怎么实现的。

// Thread类的start方法
public synchronized void start() {
       // 一系列状态检查
       if (threadStatus != 0)
           throw new IllegalThreadStateException();
 
       group.add(this);
         
       boolean started = false;
       try {
            //调用start0方法,真正启动java线程的地方
           start0();
           started = true;
      } finally {
           try {
               if (!started) {
                group.threadStartFailed(this);
              }
          } catch (Throwable ignore) {
          }
      }
  }
 
//start0方法是一个native方法
private native void start0();

JVM中,native方法与java方法存在一个映射关系,Java中的start0对应c层的JVM_StartThread方法,我们继续看一下:

JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
 JVMWrapper("JVM_StartThread");
 JavaThread *native_thread = NULL;
 bool throw_illegal_thread_state = false;
{
 
   MutexLocker mu(Threads_lock);
   // 判断Java线程是否已经启动,如果已经启动过,则会抛异常。
   if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) {
     throw_illegal_thread_state = true;
  } else {
     //如果没有启动过,走到这里else分支,去创建线程
     //分配c++线程结构并创建native线程
     jlong size =
            java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));
 
     size_t sz = size > 0 ? (size_t) size : 0;
     //注意这里new JavaThread
     native_thread = new JavaThread(&thread_entry, sz);
     if (native_thread->osthread() != NULL) {
       native_thread->prepare(jthread);
    }
  }
}
......
 Thread::start(native_thread);

走到这里发现,Java层已经过渡到native层,但远远还没结束:

JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) :
                         Thread()
  {
    initialize();
    _jni_attach_state = _not_attaching_via_jni;
    set_entry_point(entry_point);
    os::ThreadType thr_type = os::java_thread;
    thr_type = entry_point == &compiler_thread_entry ? os::compiler_thread :
                                                       os::java_thread;
    //根据平台,调用create_thread,创建真正的内核线程                      
    os::create_thread(this, thr_type, stack_sz);
  }
 
  bool os::create_thread(Thread* thread, ThreadType thr_type,
                         size_t req_stack_size) {
      ......
      pthread_t tid;
      //利用pthread_create()来创建线程
      int ret = pthread_create(&tid, &attr, (void* (*)(void*)) thread_native_entry, thread);
      ......
      return true;
}

pthread_create方法,第三个参数表示启动这个线程后要执行的方法的入口,第四个参数表示要给这个方法传入的参数:

static void *thread_native_entry(Thread *thread) {
......
 //thread_native_entry方法的最下面的run方法,这个thread就是上面传递下来的参数,也就是JavaThread
 thread->run();
......
 return 0;
}

终于开始执行run方法了:

//thread.cpp类
void JavaThread::run() {
......
 //调用内部thread_main_inner  
 thread_main_inner();
}
 
void JavaThread::thread_main_inner() {
 if (!this->has_pending_exception() &&
  !java_lang_Thread::is_stillborn(this->threadObj())) {
  {
     ResourceMark rm(this);
     this->set_native_thread_name(this->get_thread_name());
  }
   HandleMark hm(this);
   //注意:内部通过JavaCalls模块,调用了Java线程要执行的run方法
   this->entry_point()(this, this);
}
 DTRACE_THREAD_PROBE(stop, this);
 this->exit(false);
 delete this;
}

一条U字型代码调用链至此结束:

  • Java中调用Thread的star方法,通过JNI方式,调用到native层。
  • native层,JVM通过pthread_create方法创建一个系统内核线程,并指定内核线程的初始运行地址,即一个方法指针。
  • 在内核线程的初始运行方法中,利用JavaCalls模块,回调到java线程的run方法,开始java级别的线程执行。
线程如何调度

计算机的世界里,CPU会分为若干时间片,通过各种算法分配时间片来执行任务,有耳熟能详时间片轮转调度算法、短进程优先算法、优先级算法等。当一个任务的时间片用完,就会切换到另一个任务。在切换之前会保存上一个任务的状态,当下次再切换到该任务,就会加载这个状态, 这就是所谓的线程的上下文切换。很明显,上下文的切换是有开销的,包括很多方面,操作系统保存和恢复上下文的开销、线程调度器调度线程的开销和高速缓存重新加载的开销等。

image.png

经过上面两个理论基础的回顾,开启大量线程引起的问题,总结起来,就两个字——开销。

消耗时间:线程的创建和销毁都需要时间,当数量太大的时候,会影响效率。 消耗内存:创建更多的线程会消耗更多的内存,这是毋庸置疑的。线程频繁创建与销毁,还有可能引起内存抖动,频繁触发GC,最直接的表现就是卡顿。长而久之,内存资源占用过多或者内存碎片过多,系统甚至会出现OOM。 消耗CPU。在操作系统中,CPU都是遵循时间片轮转机制进行处理任务,线程数过多,必然会引起CPU频繁的进行线程上下文切换。这个代价是昂贵的,某些场景下甚至超过任务本身的消耗。

如何优化

线程的本质是为了执行任务,在计算机的世界里,任务分大致分为两类,CPU密集型任务和IO密集型任务。

CPU密集型任务,比如公式计算、资源解码等。这类任务要进行大量的计算,全都依赖CPU的运算能力,持久消耗CPU资源。所以针对这类任务,其实不应该开启大量线程。因为线程越多,花在线程切换的时间就越多,CPU执行效率就越低,一般CPU密集型任务同时进行的数量等于CPU的核心数,最多再加个1。 IO密集型任务,比如网络读写、文件读写等。这类任务不需要消耗太多的CPU资源,绝大部分时间是在IO操作上。所以针对这类任务,可以开启大量线程去提高CPU的执行效率,一般IO密集型任务同时进行的数量等于CPU的核心数的两倍。 另外,在无法避免,必须要开启大量线程的情况下,我们也可以使用线程池代替直接创建线程的做法进行优化。线程池的基本作用就是复用已有的线程,从而减少线程的创建,降低开销。在Java中,线程池的使用还是非常方便的,JDK中提供了现成的ThreadPoolExecutor类,我们只需要按照自己的需求进行相应的参数配置即可,这里提供一个示例。

/**
* 线程池使用
*/
public class ThreadPoolService {

   /**
    * 线程池变量
    */
   private ThreadPoolExecutor mThreadPoolExecutor;

   private static volatile ThreadPoolService sInstance = null;

   /**
    * 线程池中的核心线程数,默认情况下,核心线程一直存活在线程池中,即便他们在线程池中处于闲置状态。
    * 除非我们将ThreadPoolExecutor的allowCoreThreadTimeOut属性设为true的时候,这时候处于闲置的核心         * 线程在等待新任务到来时会有超时策略,这个超时时间由keepAliveTime来指定。一旦超过所设置的超时时间,闲     * 置的核心线程就会被终止。
    * CPU密集型任务 N+1   IO密集型任务   2*N
    */
   private final int CORE_POOL_SIZE = Runtime.getRuntime().availableProcessors() + 1;
   /**
    * 线程池中所容纳的最大线程数,如果活动的线程达到这个数值以后,后续的新任务将会被阻塞。包含核心线程数+非*     * 核心线程数。
    */
   private final int MAXIMUM_POOL_SIZE = Math.max(CORE_POOL_SIZE, 10);
   /**
    * 非核心线程闲置时的超时时长,对于非核心线程,闲置时间超过这个时间,非核心线程就会被回收。
    * 只有对ThreadPoolExecutor的allowCoreThreadTimeOut属性设为true的时候,这个超时时间才会对核心线       * 程产生效果。
    */
   private final long KEEP_ALIVE_TIME = 2;
   /**
    * 用于指定keepAliveTime参数的时间单位。
    */
   private final TimeUnit UNIT = TimeUnit.SECONDS;
   /**
    * 线程池中保存等待执行的任务的阻塞队列
    * ArrayBlockingQueue 基于数组实现的有界的阻塞队列
    * LinkedBlockingQueue 基于链表实现的阻塞队列
    * SynchronousQueue   内部没有任何容量的阻塞队列。在它内部没有任何的缓存空间
    * PriorityBlockingQueue   具有优先级的无限阻塞队列。
    */
   private final BlockingQueue<Runnable> WORK_QUEUE = new LinkedBlockingDeque<>();
   /**
    * 线程工厂,为线程池提供新线程的创建。ThreadFactory是一个接口,里面只有一个newThread方法。 默认为DefaultThreadFactory类。
    */
   private final ThreadFactory THREAD_FACTORY = Executors.defaultThreadFactory();
   /**
    * 拒绝策略,当任务队列已满并且线程池中的活动线程已经达到所限定的最大值或者是无法成功执行任务,这时候       * ThreadPoolExecutor会调用RejectedExecutionHandler中的rejectedExecution方法。
    * CallerRunsPolicy 只用调用者所在线程来运行任务。
    * AbortPolicy 直接抛出RejectedExecutionException异常。
    * DiscardPolicy 丢弃掉该任务,不进行处理。
    * DiscardOldestPolicy   丢弃队列里最近的一个任务,并执行当前任务。
    */
   private final RejectedExecutionHandler REJECTED_HANDLER = new ThreadPoolExecutor.AbortPolicy();

   private ThreadPoolService() {
  }

   /**
    * 单例
    * @return
    */
   public static ThreadPoolService getInstance() {
       if (sInstance == null) {
           synchronized (ThreadPoolService.class) {
               if (sInstance == null) {
                   sInstance = new ThreadPoolService();
                   sInstance.initThreadPool();
              }
          }
      }
       return sInstance;
  }

   /**
    * 初始化线程池
    */
   private void initThreadPool() {
       try {
           mThreadPoolExecutor = new ThreadPoolExecutor(
                   CORE_POOL_SIZE,
                   MAXIMUM_POOL_SIZE,
                   KEEP_ALIVE_TIME,
                   UNIT,
                   WORK_QUEUE,
                   THREAD_FACTORY,
                   REJECTED_HANDLER);
      } catch (Exception e) {
           LogUtil.printStackTrace(e);
      }
  }

   /**
    * 向线程池提交任务,无返回值
    *
    * @param runnable
    */
   public void post(Runnable runnable) {
       mThreadPoolExecutor.execute(runnable);
  }

   /**
    * 向线程池提交任务,有返回值
    *
    * @param callable
    */
   public <T> Future<T> post(Callable<T> callable) {
       RunnableFuture<T> task = new FutureTask<T>(callable);
       mThreadPoolExecutor.execute(task);
       return task;
  }
}

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

代码改变生活

故事背景在我的老家,西南边陲的某个小县城,因为物资匮乏、基础设施落后,“赶集”这项活动不知流传了多少年。以县城北部区域的四个乡镇为界,每天都有一个集,暂且就叫A、B、C、D集吧。没有官方通知,也没有统一组织方,到了日子大家就会不约而同的前往集市,采购生活用品、...
继续阅读 »

故事背景

在我的老家,西南边陲的某个小县城,因为物资匮乏、基础设施落后,“赶集”这项活动不知流传了多少年。以县城北部区域的四个乡镇为界,每天都有一个集,暂且就叫A、B、C、D集吧。没有官方通知,也没有统一组织方,到了日子大家就会不约而同的前往集市,采购生活用品、炫点小吃、添置衣物、卖点家里的劳动剩余。
赶集的规律是“空三赶四”,ABCDABCD如此循环往复,就A集来说就是A _ _ _ A _ _ _ A。
在早些年间农忙时节,家里人不知白天黑夜地在田间地头劳作,根本记不住今夕是何年,为了确定今天是什么集,经常需要跑出家门问问其他邻居,亦或是看看路上有没有前往集市的“马的”。日子一天一天过,集市的规律就如二十四节气般从来没有混乱过。
2020年新冠肆虐,为减少人员流动,很多自然村封村封路,更别提集市了。就这样集市暂停了一段时间,待情况好转,集市恢复了往常的热闹,还是固定的时间、固定过的地点,大家又聚集在了一起。周期循环规律并没有因为集市暂停而被打断,这让我好生惊讶。

抽象一下

观察规律

其实上面已经给出规律了,就是ABCDABCD循环,每隔三天重复一个集。
假设第1天是A集,那么第5天、第9天都是A集,按常见程序数组从0开始的惯例,那就是
第0天是A集,第4天、第8天都是A集。 

如果我们已知2017-02-05是A集,要计算今天是什么集,其实就是计算今天距离2017-02-05相差多少天,然后对4取余,根据余数可得:

  • 余数为0就是A集
  • 余数为1就是B集
  • 余数为2就是C集
  • 余数为3就是D集

代码实现

Java版

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

public class Test {

public static void main(String[] args) throws ParseException {

String[] names = new String[]{"A集", "B集", "C集", "D集"};

DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
Date standard = dateFormat.parse("2007-02-05");
Calendar c1 = Calendar.getInstance();
c1.setTime(standard);

Calendar now = Calendar.getInstance();
int diff = now.get(Calendar.DAY_OF_YEAR) - now.get(Calendar.DAY_OF_YEAR);
System.out.println(names[diff % 4]);
}

}

Go版

package main

import (
"fmt"
"time"
)

const (
STANDARD = "2017-02-05"
)

var names = []string{"A集", "B集", "C集", "D集"}

func main() {
today := time.Now()
standard, _ := time.Parse("2006-01-02", STANDARD)
fmt.Println(names[int(today.Sub(standard).Hours()/24)%4])
}

快捷指令

最后

借助一个简单的数学求余就能解决生活中的问题,即使现在常年在外,但每当回想起家乡、回想起家乡的集市,我还是会掏出手机看下今天是什么集,大概这就是乡愁吧!


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