注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Swift是时候使用Codable了

用不起: 苹果发布Swift支持Codable已经有一定历史年限了,为什么还用不起来,无非就是苹果的Codable太强势了, 比如模型里的定义比数据返回的json多一个key,少一个key,key的值类型不匹配(如定义为String,返回的是Int),苹果老子...
继续阅读 »

用不起:


苹果发布Swift支持Codable已经有一定历史年限了,为什么还用不起来,无非就是苹果的Codable太强势了,


比如模型里的定义比数据返回的json多一个key,少一个key,key的值类型不匹配(如定义为String,返回的是Int),苹果老子直接掀桌子,整个模型为nil。这。。。


而且模型的属性想要默认值,无。。。




你牛,牛到大家不知道怎么用


于是网络一边夸他Codable好用,一边真正工程开发中却还用不起来。


搞起来:


最近研究网上有没有好用的Codable库的时候,找到了这个。2021 年了,Swift 的 JSON-Model 转换还能有什么新花样github.com/iwill/ExCod…


经过他的封装,把苹果包装的服服帖帖。经测试,解决如下问题:

  1. 多一个key
  2. 少一个key
  3. key的类型不匹配的时候,自动做类型转换
  4. 默认值处理好。 



他的模型定义可以简化为:

struct testModel: ExAutoCodable {
@ExCodable
var courseId: Int = -1
@ExCodable
var totalSectionCount: Int = -1 // 总的章节
@ExCodable
var courseImageUrl: String = ""
@ExCodable
var tudiedSectionCount: Int = 0 // 已经学习章节
}

既然他这么好,那就用起来啰喂,,,,等等,等等


定义模型这样,竟然不行:

struct testModel: ExAutoCodable {
@ExCodable
var jumpParam: [String: Any]? = [:]

@ExCodable
var matchs: [Any] = []
}

苹果老子说Any不支持Codable???转模型的时候,这个全是空,nil。


一看工程,基本每个模型的定义都有这个呀,全有Any的定义,懵逼


研究起来:


通过研究stackoverflow.com/questions/4…, 发现可以给Any封装一个支持Codable的类型,比如AnyCodable这样。然后模型里面用到Any的,全部给换成AnyCodable。




模型改为如下,使用AnyCodable

struct testModel: ExAutoCodable {
@ExCodable
var jumpParam: [String: AnyCodable]? = [:]

@ExCodable
var matchs: [AnyCodable] = []
}

AnyCodable.swift代码如下:

//
// AnyCodable.swift
//
// 因为Any不支持Codable,但是模型里面经常会用到[String: Any]。
// 所以添加类AnyCodable,代替Any,来支持Codable, 如:[String: AnyCodable]。
// https://stackoverflow.com/questions/48297263/how-to-use-any-in-codable-type

import Foundation

public struct AnyCodable: Decodable {
var value: Any

struct CodingKeys: CodingKey {
var stringValue: String
var intValue: Int?
init?(intValue: Int) {
self.stringValue = "\(intValue)"
self.intValue = intValue
}
init?(stringValue: String) { self.stringValue = stringValue }
}

init(value: Any) {
self.value = value
}

public init(from decoder: Decoder) throws {
if let container = try? decoder.container(keyedBy: CodingKeys.self) {
var result = [String: Any]()
try container.allKeys.forEach { (key) throws in
result[key.stringValue] = try container.decode(AnyCodable.self, forKey: key).value
}
value = result
} else if var container = try? decoder.unkeyedContainer() {
var result = [Any]()
while !container.isAtEnd {
result.append(try container.decode(AnyCodable.self).value)
}
value = result
} else if let container = try? decoder.singleValueContainer() {
if let intVal = try? container.decode(Int.self) {
value = intVal
} else if let doubleVal = try? container.decode(Double.self) {
value = doubleVal
} else if let boolVal = try? container.decode(Bool.self) {
value = boolVal
} else if let stringVal = try? container.decode(String.self) {
value = stringVal
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "the container contains nothing serialisable")
}
} else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not serialise"))
}
}
}

extension AnyCodable: Encodable {
public func encode(to encoder: Encoder) throws {
if let array = value as? [Any] {
var container = encoder.unkeyedContainer()
for value in array {
let decodable = AnyCodable(value: value)
try container.encode(decodable)
}
} else if let dictionary = value as? [String: Any] {
var container = encoder.container(keyedBy: CodingKeys.self)
for (key, value) in dictionary {
let codingKey = CodingKeys(stringValue: key)!
let decodable = AnyCodable(value: value)
try container.encode(decodable, forKey: codingKey)
}
} else {
var container = encoder.singleValueContainer()
if let intVal = value as? Int {
try container.encode(intVal)
} else if let doubleVal = value as? Double {
try container.encode(doubleVal)
} else if let boolVal = value as? Bool {
try container.encode(boolVal)
} else if let stringVal = value as? String {
try container.encode(stringVal)
} else {
throw EncodingError.invalidValue(value, EncodingError.Context.init(codingPath: [], debugDescription: "The value is not encodable"))
}
}
}
}

这个结合Excodable,经过测试,完美。数据转换成功。


如果模型的定义忘记了,还是定义为Any呢。 再给Excodable库里面的源码,做安全检查,修改代码如下:

public extension Encodable {
func encode(to encoder: Encoder, nonnull: Bool, throws: Bool) throws {
var mirror: Mirror! = Mirror(reflecting: self)
while mirror != nil {
for child in mirror.children where child.label != nil {
try (child.value as? EncodablePropertyWrapper)?.encode(to: encoder, label: child.label!.dropFirst(), nonnull: false, throws: false)
// 注意:Any不支持Codable, 可以使用AnyCodable代替。
// 注意枚举类型,要支持Codable
assert((child.value as? EncodablePropertyWrapper) != nil, "模型:\(mirror)里面的属性:\(child.label) 需要支持 Encodable")
}
mirror = mirror.superclassMirror
}
}
}

public extension Decodable {
func decode(from decoder: Decoder, nonnull: Bool, throws: Bool) throws {
var mirror: Mirror! = Mirror(reflecting: self)
while mirror != nil {
for child in mirror.children where child.label != nil {
try (child.value as? DecodablePropertyWrapper)?.decode(from: decoder, label: child.label!.dropFirst(), nonnull: false, throws: false)
// 注意:Any不支持Codable, 可以使用AnyCodable代替。
// 注意枚举类型,要支持Codable
assert((child.value as? DecodablePropertyWrapper) != nil, "模型:\(mirror)里面的属性:\(child.label) 需要支持 Decodable")
}
mirror = mirror.superclassMirror
}
}
}

嗯,这下模型如果定义为Any,可以在运行的时候报错,提醒要改为AnyCodable。


能愉快的编码了。。。


不过总感觉还差点东西。


再研究起来:


找到这个 github.com/levantAJ/An…


可以实现

let dictionary: [String: Any] = try container.decode([String: Any].self, forKey: key)
let array: [Any] = try container.decode([Any].self, forKey: key)

通过自定义[String: Any]和[Any]的解码,实现Any的Codble。


是否可以把这个合并到Excodable里面吧,从而什么都支持了,666。


在Excodable里面提issues,作者回复有空可以弄弄。


我急用呀,那就搞起来。


花了九牛二虎,终于搞出下面兼容代码:

// Make `Any` support Codable, like: [String: Any], [Any]
fileprivate protocol EncodableAnyPropertyWrapper {
func encode<Label: StringProtocol>(to encoder: Encoder, label: Label, nonnull: Bool, throws: Bool) throws
}
extension ExCodable: EncodableAnyPropertyWrapper {
fileprivate func encode<Label: StringProtocol>(to encoder: Encoder, label: Label, nonnull: Bool, throws: Bool) throws {
if encode != nil { try encode!(encoder, wrappedValue) }
else {
let t = type(of: wrappedValue)
if let key = AnyCodingKey(stringValue: String(label)) {
if (t is [String: Any].Type || t is [String: Any?].Type || t is [String: Any]?.Type || t is [String: Any?]?.Type) {
var container = try encoder.container(keyedBy: AnyCodingKey.self)
try container.encodeIfPresent(wrappedValue as? [String: Any], forKey: key)
} else if (t is [Any].Type || t is [Any?].Type || t is [Any]?.Type || t is [Any?]?.Type) {
var container = try encoder.container(keyedBy: AnyCodingKey.self)
try container.encodeIfPresent(wrappedValue as? [Any], forKey: key)
}
}
}
}
}
fileprivate protocol DecodableAnyPropertyWrapper {
func decode<Label: StringProtocol>(from decoder: Decoder, label: Label, nonnull: Bool, throws: Bool) throws
}
extension ExCodable: DecodableAnyPropertyWrapper {
fileprivate func decode<Label: StringProtocol>(from decoder: Decoder, label: Label, nonnull: Bool, throws: Bool) throws {
if let decode = decode {
if let value = try decode(decoder) {
wrappedValue = value
}
} else {
let t = type(of: wrappedValue)
if let key = AnyCodingKey(stringValue: String(label)) {
if (t is [String: Any].Type || t is [String: Any?].Type || t is [String: Any]?.Type || t is [String: Any?]?.Type) {
let container = try decoder.container(keyedBy: AnyCodingKey.self)
if let value = try container.decodeIfPresent([String: Any].self, forKey: key) as? Value {
wrappedValue = value
}
} else if (t is [Any].Type || t is [Any?].Type || t is [Any]?.Type || t is [Any?]?.Type) {
let container = try decoder.container(keyedBy: AnyCodingKey.self)
if let value = try container.decodeIfPresent([Any].self, forKey: key) as? Value {
wrappedValue = value
}
}
}
}
}
}

再在他用的地方添加

// MARK: - Encodable & Decodable - internal

public extension Encodable {
func encode(to encoder: Encoder, nonnull: Bool, throws: Bool) throws {
var mirror: Mirror! = Mirror(reflecting: self)
while mirror != nil {
for child in mirror.children where child.label != nil {
if let wrapper = (child.value as? EncodablePropertyWrapper) {
try wrapper.encode(to: encoder, label: child.label!.dropFirst(), nonnull: false, throws: false)
} else { //添加
try (child.value as? EncodableAnyPropertyWrapper)?.encode(to: encoder, label: child.label!.dropFirst(), nonnull: false, throws: false)
}
}
mirror = mirror.superclassMirror
}
}
}

public extension Decodable {
func decode(from decoder: Decoder, nonnull: Bool, throws: Bool) throws {
var mirror: Mirror! = Mirror(reflecting: self)
while mirror != nil {
for child in mirror.children where child.label != nil {
if let wrapper = (child.value as? DecodablePropertyWrapper) {
try wrapper.decode(from: decoder, label: child.label!.dropFirst(), nonnull: false, throws: false)
} else { //添加
try (child.value as? DecodableAnyPropertyWrapper)?.decode(from: decoder, label: child.label!.dropFirst(), nonnull: false, throws: false)
}
}
mirror = mirror.superclassMirror
}
}
}


完美:


综上,终于可以让Excodable库支持[String: Any]和[Any]的Codable了,撒花撒花。


从而模型定义这样,也能自动编解码:

struct testModel: ExAutoCodable {
@ExCodable
var jumpParam: [String: Any]? = [:]

@ExCodable
var matchs: [Any] = []
}

针对这个库的更新修改,改到这github.com/yxh265/ExCo…


也把对应的更新提交给Excodable的作者了,期待合并。
(作者iwill说,用ExCodable提供的 ExCodableDecodingTypeConverter 协议来实现是否可行。
我看了,因为Any不支持Codable,所以要想用ExCodableDecodingTypeConverter协议,也得要大改。也期待作者出马添加这个功能。)


最后的使用方法:


引入如下:

pod 'ExCodable', :git => 'https://github.com/yxh265/ExCodable.git', :commit => '4780fb8'

模型定义:

struct TestStruct: ExAutoCodable {
@ExCodable // 字段和属性同名可以省掉字段名和括号,但 `@ExCodable` 还是没办法省掉
var int: Int = 0
@ExCodable("string", "str", "s", "nested.string") // 支持多个 key 以及嵌套 key 可以这样写
var string: String? = nil
@ExCodable
var anyDict: [String: Any]? = nil
@ExCodable
var anyArray: [Any] = []
}

编解码:

let test = TestStruct(int: 304, string: "Not Modified", anyDict: ["1": 2, "3": "4"], anyArray: [["1": 2, "3": "4"]])
let data = try? test.encoded() as Data?
let copy1 = try? data?.decoded() as TestStruct?
let copy2 = data.map { try? TestStruct.decoded(from: $0) }
XCTAssertEqual(copy1, test)
XCTAssertEqual(copy2, test)

引用:


2021 年了,Swift 的 JSON-Model 转换还能有什么新花样


github.com/iwill/ExCod…


stackoverflow.com/questions/4…


stackoverflow.com/questions/4…


Property wrappers in Swift和Codable


作者:清点游玩
链接:https://juejin.cn/post/7168748765946806303
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

iOS应用内弹窗通知怎么实现?其实很简单,这样,这样,再这样.....你学会了么?

iOS
携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第29天,点击查看活动详情。 项目背景 消息通知可以及时地将状态、内容的更新触达到用户,用户则可以根据收到的消息做后续判断。这是最常见的信息交换方式的产品设计。 而顶部向下弹出的消息通知本质上...
继续阅读 »

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第29天,点击查看活动详情


项目背景


消息通知可以及时地将状态、内容的更新触达到用户,用户则可以根据收到的消息做后续判断。这是最常见的信息交换方式的产品设计。


而顶部向下弹出的消息通知本质上是根据条件触发的“中提醒”通知类型,示例:每次在网购时,支付成功后在App会展示消息通知。


因此本章中,我们就来试试使用SwiftUI来实现应用内弹窗通知交互。


项目搭建


首先,创建一个新的SwiftUI项目,命名为NotificationToast


消息弹窗样式

我们构建一个新的视图NotificationToastView,然后声明好弹窗视图的内容变量,示例:

struct NotificationToastView: View {
    var notificationImage: String
    var notificationTitle: String
    var notificationContent: String
    var notificationTime: String

    var body: some View {
        //弹窗样式
    }
}

上述代码中,我们声明了4个String类型的变量:notificationImage图标信息、notificationTitle标题信息、notificationContent内容信息、notificationTime推送时间。


然后我们构建样式内容,示例:

HStack {
    Image(notificationImage)
        .resizable()
        .aspectRatio(contentMode: .fit)
        .frame(width: 60)
        .clipShape(Circle())
        .overlay(Circle().stroke(Color(.systemGray5), lineWidth: 1))
    VStack(spacing: 10) {
        HStack {
            Text(notificationTitle)
                .font(.system(size: 17))
                .foregroundColor(.black)
            Spacer()
            Text(notificationTime)
                .font(.system(size: 14))
                .foregroundColor(.gray)
        }
        Text(notificationContent)
            .font(.system(size: 14))
            .foregroundColor(.black)
            .lineLimit(4)
            .multilineTextAlignment(.leading)
    }
}
.padding()
.frame(minWidth: 10, maxWidth: .infinity, minHeight: 10, maxHeight: 80)
.background(.white)
.cornerRadius(8)
.shadow(color: Color(.systemGray4), radius: 5, x: 1, y: 1)
.padding()

上述代码中,我们构建了样式排布,Image使用notificationImage图片信息变量,它和其他元素是HStack横向排布关系。


右边则是HStack横向排布的notificationTitle标题变量的文字和notificationTime推送时间的文字,使用Spacer撑开。


而底下是notificationContent内容信息,它和标题信息及推送时间信息是VStack纵向排布。


我们在ContentView中展示看看效果,示例:

NotificationToastView(notificationImage: "me", notificationTitle: "文如秋雨", notificationContent: "一只默默努力变优秀的产品汪,独立负责过多个国内细分领域Top5的企业级产品项目,擅长B端、C端产品规划、产品设计、产品研发,个人独立拥有多个软著及专利,欢迎产品、开发的同僚一起交流。", notificationTime: "2分钟前")


消息弹窗交互


交互方面,我么可以做个简单的交互,创建一个按钮,点击按钮时展示消息弹窗,消息弹窗显示时等待2秒后自动消失。


实现逻辑也很简单,我们可以让弹窗加载的时候在视图之外,然后点击按钮的时候,让消息弹窗从下往下弹出,然后等待2秒后再回到视图之外


首先我们声明一个偏移量,定义消息弹窗的初始位置,示例:

@State var offset: CGFloat = -UIScreen.main.bounds.height / 2 - 80

然后给弹窗视图加上偏移量和动画的修饰符,示例:

ZStack {
    NotificationToastView(notificationImage: "me", notificationTitle: "文如秋雨", notificationContent: "一只默默努力变优秀的产品汪,独立负责过多个国内细分领域Top5的企业级产品项目,擅长B端、C端产品规划、产品设计、产品研发,个人独立拥有多个软著及专利,欢迎产品、开发的同僚一起交流", notificationTime: "2分钟前")
        .offset(x: 0, y: offset)
        .animation(.interpolatingSpring(stiffness: 120, damping: 10))
    Button(action: {
        if offset <= 0 {
            offset += 180
            DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                self.offset -= 180
            }
        }
    }) {
        Text("弹出通知")
    }
}

上述代码中,我们让NotificationToastView弹窗视图偏移位置Y轴为我们声明好的变量offset位置,然后使用ZStack叠加展示一个按钮,当我们offset在视图外时,点击按钮修改偏移量的位置为180,然后调用成功后等待2秒再扣减偏移量回到最初的位置


项目预览


我们看看最终效果。


恭喜你,完成了本章的全部内容!

快来动手试试吧。

如果本专栏对你有帮助,不妨点赞、评论、关注~

作者:文如秋雨
链接:https://juejin.cn/post/7136104673248804878
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

最新前端技术趋势

前端的车轮滚滚向前,轮子造的越来越圆,速度造的越来越快,每个人都在适应这个轮子的节奏,稍微不注意就会被甩出车轮之外。狼狈不堪之外还会发自心底的大喊一声:别卷了!! 话虽这么说,但现实就是这样,无论是客观还是主观因素都不得不让你继续的往前走。既然是往前走, 那么...
继续阅读 »

前端的车轮滚滚向前,轮子造的越来越圆,速度造的越来越快,每个人都在适应这个轮子的节奏,稍微不注意就会被甩出车轮之外。狼狈不堪之外还会发自心底的大喊一声:别卷了!!


话虽这么说,但现实就是这样,无论是客观还是主观因素都不得不让你继续的往前走。既然是往前走,
那么能知道一些前面有啥东西岂不是更好,也许能少走弯路。



自己对前端23年大概的技术做了一些展望,想到什么写什么。毕竟谁都不知道会不会突然间又出了个frontEndGPT打翻了所有人的饭碗。



1 AI


最先说的肯定是AI,22年末,23年初的chatgpt让AI话题火的一塌糊涂,同时也被认为是一次重大的技术革新,技术革新带来的就是重塑,一切都要被重塑,你的职业,你的工作。
视觉层面的stable diffusionmidjourney已经对设计师产生了重大影响,而涉及到视觉ui层面的话,前端肯定是绕不开的部分。虽然目前没有直接的对前端产生影响,但下面的就一个就不只是对前端了,而是对整个程序员都产生了影响。

  • copilot


Your AI pair programmer. Trained on billions of lines of code, GitHub Copilot turns natural language prompts into coding suggestions across dozens of languages.


看看醒目的文字就知道,程序员或多或少都会被影响了。


还有什么CodeWhisperer, Cursor等也都是AI辅助编程。


更有FrontyLocofy等将图片AI分析为HTML文档,无代码快速建站,figma快速解析成代码等,虽然是提效不少,但谁能说这些不是对前端的一种重塑呢。


2 主流框架


随着React,Vue等框架进一步的普及,现在前端想要脱离它们的场景越来越少了。那么它们的下一步规划,也会对我们产生一些不小的影响。


react


react 18以后,react似乎是对create-react-app这种项目启动方式也不怎么主推了,毕竟速度摆在那里,没有任何优势。而使用直接竞争对手的产品似乎又不太合适,而直接说又不用又显得跟开源精神不吻合(当然竞争对手的一些基本特性也确实和现有的构建思想不太吻合)。


他们似乎采取一种围魏救赵的方式,着重宣传next的方式。next不仅仅是一个ssr的框架,同时它也支持csr,ssg等不同的方式(next13开始,对于客户端组件和服务端组件可以有了比较好的区分)。同时next与react有着千丝万缕的联系,而next正在进行一个新的构建工具的替换。
next采用了turbopack,也是Webpack作者TobiasKoppers的作品,官方说它更新速度比Vite也要快10倍、比Webpack快700倍


而react也应该大概率会引入turbopack(当然它如果继续搞前端脚手架的话)。当然也可能会直接使用next环境。


至于快多少,以及评价基准等我们可以看下turbopack真比Vite快10倍吗?


next


next最新版本也加入了很多的特性,比如server component理念,约定式路由的更改,流式渲染,客户端组件与服务端组件分离更简单,更好的构建速度等等功能。可以让开发体验,用户体验更好,性能也会有响应的提升。


vite


这个不用说了,优秀的构建速度以及越来越丰富的社区,让其在22年有了很大提升。随着浏览器的逐步升级,23年vite肯定也会是重大的一年。


webpack


虽然5有了好多的功能提升,不过速度似乎一直是一个绕不过去的坎。就连作者也已经开始搞turbopack了,虽然加入swc能让编译有很大提升,但是目前从我身边的人的了解看,越来越多的人开始转向vite等其它方式了


turbopack


是webpack作者去的新公司开发的一款基于rust的打包工具。官方明确说明就是为了替换webpack。 同时强调webpack是这十年最火的工具,那turbopack就定位成未来几年的工具。由于作者和webpack, Vercel, next, React这些之间千丝万缕的联系,很难不说未来React也许会和这个打包工具绑定上。下面是官方提供的速度参考




而除了turbopack外,同一团队还在做Turborepo


这是一款项目管理的工具,最主要的面向场景是Monorepo这种复杂的多项目管理


Monorepo有很多优势,但是在多个项目中会有很多复杂的构建过程和相对闭塞的构建步骤,每一次上线都是耗时严重。所以Turborepo是为了解决这个问题出现的,让一些构建重复的构建步骤提炼出来,基于整个Monorepo项目的维度来管理多个子项目。


同时对于单个CI的构建步骤,解决每台机器,每个人都要单独构建的问题,还提炼了类似store的方案,脱离了构建环境,只跟项目绑定,当然这个方案是否使用要看你自己,毕竟原理是把构建产物放到第三方储存,而第三方又不是一个类似npm的开源机构。


3 服务端


node从7,8年前的爆火,到现在的不温不火,前端语言介入服务端这个命题似乎现在是越来越清晰了。那就是定位,可以做网关,可以做转发,可以做一些数据代理合并,定位清晰node依然有自己的使用场景。
node也马上到了20版本,迎来了一些特性

  • esm的更好支持,

  • 测试功能更加丰富

  • V8 引擎更新至 11.3,与Chromium113版本大部分相同

  • 支持以虚拟机的方式,动态运行js代码

  • WebAssembly的支持(实验性的)


于此同时,node曾经的作者Ryan Dahl几年前搞的Deno似乎没有太多的消息,似乎在向商业化方向前进。打造出Deno Deploy及其即时边缘渲染SSR框架Deno Fresh


4 其它


WebAssembly, 元框架,ts,微前端?


引用:


http://www.infoq.cn/article/9qu…
turbo.build/pack/docs/w…
http://www.robinwieruch.de/web-develop…


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

北美 2023 被裁员的感悟

不贩卖焦虑,就事论事,希望能帮助到有需要的朋友。 很多人觉得在裁员之前是没有任何迹象的,其实真的不是这样。 公司在裁员的过程中有很多要裁员的迹象,我会在另外一篇文章中对我遇到的一些裁员信号设置一些雷区和警告,当你遇到上面的这些信号的时候,直觉告诉你需要马上考虑...
继续阅读 »

不贩卖焦虑,就事论事,希望能帮助到有需要的朋友。


很多人觉得在裁员之前是没有任何迹象的,其实真的不是这样。


公司在裁员的过程中有很多要裁员的迹象,我会在另外一篇文章中对我遇到的一些裁员信号设置一些雷区和警告,当你遇到上面的这些信号的时候,直觉告诉你需要马上考虑寻找下一个替代方案了。


因为当这些信号的任何一个或者多个同时出现的时候就意味着裁员在进行中了,通常会在 3 到 6 个月左右发生。




在公司的职位


在被裁公司的职位是 Tech Lead。


虽然这个职位并不意味着你对其他同事而言能够获得更多的有效信息,但是通常可能会让自己与上级有更好的沟通管道。


但是,非常不幸的是这家公司的沟通渠道非常有问题。


因为负责相关开发部分的副总是从 PHP 转 Java 的,对 Java 的很多情况都不非常明确,所以他非常依赖一个架构师。


但,公司大部分人都认为这个架构师的要求是错误的,并且是非常愚蠢的。


比如说要求代码必须要放在一行上面,导致代码上面有不少行甚至超过了 1000 个字符。


所有开发都对这个要求非常不理解,并且多次提出这种要求是无理并且愚蠢的,我们组是对这个要求反应最激烈,并且抵触最大的(也有可能是因为我的原因,我不希望在明显错误的地方让步;我可以让步,但是需要给一个能说服的理由)。


然而,这个所谓的架构师就利用 PR 合并的权力,不停的让我们的组员进行修改。


裁员之前


正是因为在公司的职位和上面说到的和架构师直接的冲突。


在 6 个月之前,我就已经和组里的同事说要准备进行下一步了,你们该面试的就面试了,不要拖延。


在这个中间过程中,我的领导还找我谈过一次。领导的意思就是他非常同意我们的有关代码 PR 的要求,也觉得这些要求都是狗屁。


但,负责开发的副总,认为我们组现在是所有组里面最差的。


可能当时没有认真理解这句话的意思,我们组从所有组里面最好的,变成最差的只用了 2 周(一个 Sprint)的时间。


在这次谈话后,我更加坚信让我的组员找下一家的信息了,对他们中途要面试其他公司我都放开一面。


非常不幸的,我自己那该死的拖延症,我是把我自己的简历准备好了,但是还没有来得及投就等来了真正裁员的这一天。


深刻的教训和学到的经验:


如果公司的运营或者管理让感觉到不舒服,并且已经有开始寻找下家的想法的时候,一定要尽快准备,马上实施,不要拖延


这就是我在上面标黑马上的原因。


裁员过程


裁员过程非常简单和迅速,并且在毫不知情的情况下进行。


在周四的时候,公司的高层提示所有的会议今天全部取消,并且把应该 11 点开的全公司会议提前到了 9 点。


因为很多人都没有准备,所以很多人也没有参加。


后来才知道,9 点就是宣布裁员的开始,事后知道裁员比率为 40%。


然后就是各个部门找自己的被裁的员工开会,这个会议通常首先是一个 Zoom 的 Group 会议,说了一堆屁话,就是什么这是不得已的决定呀,什么乱七八糟的东西。


当然,在这个时候你还需要或者期待公司给你什么合理的理由呢?


然后就是 P&C 人员说话,基本上就是每个人 15 分钟的时间单独 Zoom。


这个 15 分钟,主要就是读下文件了,至于 2 个会议上是不是开摄像头,随意。


你愿意开也行,不愿意开也行,反正上面的所有人都心不在焉。我是懒得开,因为和你谈话的人,你都根本不认识。


第二个会议就是 P&C,这个会议主要就是和你说说被裁员后可以有的一些福利和什么的,对我个人来说我更关注的是补偿。


至于 401K 和里面的资金都是可以自行转的,也没啥需要他们说的,了解到补偿就是 6 周工资,不算多也凑合能接受。


负责裁员的人说,还有什么需要问的,我简单的回答了下 All Set 然后 have a nice day 就好了。毕竟他们只是具体做事的人,问他们也问不出个所以然,这有啥的。


裁员之后


裁员之后,感觉所有认识的被裁的同事都是懵的。


开完 15 分钟的 P&C 会议后,基本上电脑和邮箱马上就不能用了。公司貌似说电脑可以自己留着,但是上面的数据会被远程清理掉。


留在公司里面的东西会有人收拾后寄到家里。


我在公司里的办公桌就属于离职型办公桌,简单的来说,上面只有一台不属于我的显示器,另外就是从其他地方拿过来的一盒消毒湿巾,公司里面压根没有我需要的东西。


很多人认为公司禁用账户有点太不讲人情,其实从技术层面来说根本没有什么,因为所有的管理都是 LDAP,直接在 LDAP 上禁用你账户就好了,没啥稀奇的。


中午的时候,被裁的同事都互相留下了手机号码,感觉大家因为我在裁员列表里面感觉有点扯。另外更扯的同事在这个公司工作了 7 年了,也在列表里面(所有 PHP 的基础架构都是他写的和建立的)。


虽然最开始和这个同事有过一些摩擦,但是这个印度的同事真的挺好的,我们都觉得他挺不错,也愿意和他一起共事。


很多人,包括我。都对这个同事感觉不值,也觉得这很扯。


奈何公司的选择就是一些阿谀奉承,天天扯淡的人,比如说那个奇葩的架构师。


没多久,被裁的同事建了一个群,然后把我给拉进去了,主要还是我们组里面的同事,大家希望能够分享一些面试经验和机会,偶尔吐槽下。


在晚上的时候,突然收到另外一个同事的 LinkedIn 好友邀请,他不在这次裁员内。


但是他也被降职了,他本来是 Sr 开发人员和小组长,后来被提拔成架构师了,现在连小组长都不是了。


他和我说,如果需要帮助的话,他会尽量帮忙,并且还给他的一些曾经的招聘专员账号推送给了我。


我也非常感谢他们,虽然经历过,但是也收获了一些朋友,虽然说在美国职场比较难收获朋友,但是也并不是完全这样的。


没有了利益的纠葛,更容易说点实话。


http://www.ossez.com/t/topic/144…


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

何不食肉糜?

21年的时候,微博上有过一番口诛笔伐,就是就是管清友建议刚开始工作的年轻人就近租房不要把时间浪费在上班的路上,要把时间利用起来投资自己,远比省下的房租划算。 视频见这里:http://www.bilibili.com/video/BV1Bb… 当时我印象非常...
继续阅读 »

21年的时候,微博上有过一番口诛笔伐,就是就是管清友建议刚开始工作的年轻人就近租房不要把时间浪费在上班的路上,要把时间利用起来投资自己,远比省下的房租划算。


视频见这里:http://www.bilibili.com/video/BV1Bb…



当时我印象非常深刻,微博评论是清一色的 “何不食肉糜”,或者说“房租你付?”


可能是因为这件事情的刺激,管清友后来才就有了“我特别知道年轻人建议专家不要建议”的言论。


对还是错?


在我看来,管清友的这个建议可以说是掏心掏肺,非常真诚,他在视频里也说了,他是基于很多实际案例才说的这些话,不是说教。


为什么我这么肯定呢?


很简单,我就是代表案例。


我第一家公司在浦东陆家嘴,四号线浦东大道地铁站旁边,我当时来上海的时候身无分文,借的家里的钱过来的,我是贫困家庭。


但,为了节约时间,我就在公司附近居住,步行五分钟,洗头洗澡都是中午回住的地方完成,晚上几乎都是11:00之后回去,倒头就睡,因为时间可以充分利用。


节约的时间,我就来研究前端技术,写代码,写文章,做项目,做业务,之前的小册(免费章节,可直接访问)我也提过,有兴趣的可以去看看。


现在回过头来看那段岁月,那是充满了感激和庆幸,自己绝对做了一个非常正确的决定,让自己的职业发展后劲十足。


所以,当看到管清友建议就近租房的建议,我是非常有共鸣的,可惜世界是参差的,管清友忽略了一个事实,那就是优秀的人毕竟是少数,知道如何主动投资自己的人也是凤毛麟角,他们根本就无法理解。


又或者,有些人知道应该要投资自己,但是就是做不到,毕竟辛苦劳累,何苦呢,做人,不就是应该开心快乐吗?


说句不好听的,有些人的时间注定就是不值钱的。


工作积极,时间长是种优势?


一周前,我写了篇文章,谈对“前端已死”的看法,其中提到了“团队下班最晚,工作最积极”可以作为亮点写在简历里。


结果有人笑出了声。



好巧的是,管清友的租房建议也有人笑了,出没出声并不知道。



也有人回复“何不食肉糜”。


这有些出乎我的意料,我只是陈述一个简单的事实,却触动了很多人的敏感神经。


我突然意识到,有些人可能有一个巨大的认知误区,就是认为工作时长和工作效率是负相关的,也就是那些按时下班的是效率高,下班晚的反而是能力不足,因为代码不熟,bug太多。



雷军说你说的很有道理,我称为“劳模”是因为我工作能力不行。


你的leader也认为你说的对,之前就是因为我每天准时下班,证明了自己的能力,所以自己才晋升的。


另外一个认知误区在于,把事实陈述当作目标指引。


如果你工作积极,是那种为自己而工作的人,你就在简历中体现,多么正常的建议,就好比,如果你是北大毕业的,那你就在简历中体现,没任何问题吧。


我可没有说让你去拼工作时长,装作工作积极,就好比我没有让你考北大一样。


你就不是这种类型的人,对吧,你连感同身受都做不到,激动个什么呢,还一大波人跟着喊666。


当然,我也理解大家的情绪,我还没毕业的时候,也在黑心企业待过,钱少事多尽煞笔,区别在于,我相对自驱力和自学能力强一些,通过自己的努力跳出了这个循环。


但大多数人还是被工作和生活推着走,所以对加班和内卷深恶痛绝,让本就辛苦的人生愈发艰难,而这种加班和内卷并没有带来收入的提升。


那问题来了,有人通过努力奋斗蒸蒸日上,有人的辛苦努力原地踏步,同样的,有的人看到建议觉得非常有用,有的人看到建议觉得何不食肉糜,区别在哪里呢?


究竟是资本作恶呢?还是自己能力不足呢?


那还要建议吗?


管清友不再打算给年轻人建议了,我觉得没必要。


虽然,大多数时候,那些听得进去建议的人大多不需要建议,而真正需要建议的又听不进,但是,那么多年轻人,总有一部分潜力股,有一些真正需要帮助的人。


他们可能因为环境等原因,有短暂的迷茫与不安,但是,来自前人发自真心的建议或许可以让他们坚定自己前进方向,从而走出不一样的人生。


就像当年我被乔布斯的那些话语激励过那般。


所以,嘲笑之人任其笑之,只要能帮助到部分人,那就有了价值。


因此,我不会停止给出那些我认为对于成长非常有帮助的建议。


(完)


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

放心,前端死不了

前言 今年春天莫名冒出一个“前端已死”的话题,搞得同行们人人自危。 这个话题和当下的环境是分不开的,如果环境很好,工作好找,那这个话题也就不攻自破了,不会流传开来。 网络上的任何事情都可以在《乌合之众》书中找到答案。大众言论没有理性,全是极端,要么封神,要么踩...
继续阅读 »

前言


今年春天莫名冒出一个“前端已死”的话题,搞得同行们人人自危。


这个话题和当下的环境是分不开的,如果环境很好,工作好找,那这个话题也就不攻自破了,不会流传开来。


网络上的任何事情都可以在《乌合之众》书中找到答案。大众言论没有理性,全是极端,要么封神,要么踩死。


今天能找到不错的工作,就感觉自己马上就要晋升 CEO 迎娶白富美了。但一旦明天情况不太好,就感觉自己马上要死了。


“前端已死”这个话题就属于后者。



原文地址 juejin.cn/post/722432… ,非作者允许不得转载



互联网行业会持续发展


前端技术是依托于互联网行业的,只要行业还在,它就会有用武之地,就会有价值。塞班已死,因为诺基亚不在了。


虽然现在电脑手机早已普及完成,增长率没那么大了,但互联网在各个领域的应用却不断更新升级。它已经嵌入到我们生活的各个领域了,就像水和电一样。


有互联网行业,就需要各种 toC 的页面、公众号、小程序,各种 toB 的管理系统和富客户端应用。就需要前端开发技术,需要前端人才。


你可能盲从“前端已死”,但你绝不会同意“互联网将死”,这就很矛盾 —— 当然我们生活、网络处处都矛盾。


递弱代偿的基本规律


递弱代偿,是我国哲学家王东岳提出的事物发展理论。对于这世界的任何事物,只要是发展,就一定会“递弱代偿”。


举个例子,低等的单细胞生物,一个细胞即可完成吃喝拉撒各种功能。这就很像小创业公司,或者一个行业的发展初期,都是全能型人才,拿起键盘写代码,抄起拖把扫厕所。个体很厉害,但整体很低级。


再往下发展到多细胞生物,开始有了细胞功能的分化,有的专门呼吸、有的专门消化。这就像初具规模的小公司,行业开始有了发展和进化,人才开始分角色了。有的人专门写代码,有的人专门扫卫生


再慢慢发展 N 步,到了高级哺乳动物,体内细胞分为上百种,严格划分了各种职能,最终拼接为一个高级的整体。公司和行业也一样,当前互联网从业人员的角色有几十种,大家各司其职,才做成了几万人的大公司。


不光互联网行业,像其他常见的行业,工业、汽车、医疗,这些成熟的领域,哪些不是严格细分角色的?


就连现在的网络诈骗,也都是有规模、有组织、有上下线的。


所以,互联网行业要发展,就一定会遵循这个规律,继续细分角色,前端永远都是一个重要角色。


如果今年前端被全栈替代了,明年运维也被全栈替代了,后年产品也被全栈替代了 …… 那就违背这个哲学规律了,那就说明互联网会倒退 —— 这显然不对


PS:发展不一定是线性的,它可能是起伏的,有强增长期,一定就有缓和期 —— 但整体一定是继续增长的。


前端没有可替代技术


Android 和 IOS 被小程序替代了很多,但前端目前没有发现什么可替代技术,反而前端早已慢慢的占据其他技术领域,如客户端、服务端。


新的技术如 Flutter WebAssembly 都是对当下前端技术的补充,真实 TS 也不是为了替代 ES ,它也是一个补充和备选方案。


浏览器 + HTML CSS JS + HTTP ,目前没有任何技术可以替代它,反而它们正在加速进化和完善。Vue React 小程序,目前也已经牢牢占据了高地,前端技术的范式早已形成。


ChatGPT 代替不了程序员


无论网上怎么宣传,当下所有的民生领域的人工智能,包括智能驾驶,严格来说都是:人工智能辅助


辅助,就是为人服务的。为人的自主思考能力和创造力服务,不是为机械重复的工作服务。


ChatGPT 本质上就是一个搜索引擎的二次封装,它更能理解你的输入意图,它更精确的帮你拼接返回结果。但它就是一个辅助工具,用好了可以帮你提升效率,但前提是得有人去用它。


你可以想一下日常的工作内容,开会,沟通,扯皮这些,AI 能参与进来吗?

它能理解你的业务需求吗?

它能帮你完成复杂模块的开发和联调吗?

它能帮你和其他同事沟通吗?


它能做的只是帮你写一部分固定的代码和文档,仅此而已。

现在有搜索引擎,我们说“面向 google 编程”,以后可能就变为“面向 ChatGPT 编程”,提升效率。


大环境总会有起伏,这是常态


大概从 15-16 年开始,当时随便培训一下,出来就一万多的工资(北京为例),直到前两年,行业的增长率特别快。这一代人也吃到了行业的红利,多少挣了点钱。


但无论哪个行业,都不能指望他一直这么增长,有增长就有缓和。再牛的人也不能影响到大环境,庄稼不收年年种,三穷三富活到老。


缓和期过去,还会迎来下一轮增长期。


总结

  • 互联网行业会持续发展

  • 发展就要递弱代偿,细分行业角色

  • 前端没有可替代的技术,是一个重要角色


所以,前端死不了,只是最近因环境原因暂时蛰伏,发展缓慢而已。


当下建议


有工作的就先稳住,千万别裸辞。哪怕工作不顺利,毕竟拿人钱受人管。


已经裸辞或者被毕业的,也不要过于焦虑,即便工作机会少这也不是你的问题,人斗不过大环境的。该学习就学习,该面试还是去面试,把自己该做的做了就是,其他的就别管了,淡定。


之前上班加班忙成狗,现在难得有时间,正好自由安排做一点自己之前想做的事。毕竟人生苦短,不是光为了工作,每个人都有自己独有的爱好。


也许过了这一阵,你再想这么自由就没机会了。有些事儿,现在不做,这辈子也许就不再有机会和动力了。


如果你想反驳我的观点


以你想的为准,你说的对。


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

2023 了,不会还要做官网吧!

web
随着 AIGC 的兴起,移动互联网仿佛一瞬间也不香了,吃到红利的头部应用还在稳坐泰山,后起者在各种 xx 已死的声浪中,不知所措,信心尽失。流量的走势左右了太多人的思绪,太多的决策,所以当你在 2023 年,PC 互联网已经凉到不能再凉的时候,接到一个官网的需...
继续阅读 »

随着 AIGC 的兴起,移动互联网仿佛一瞬间也不香了,吃到红利的头部应用还在稳坐泰山,后起者在各种 xx 已死的声浪中,不知所措,信心尽失。流量的走势左右了太多人的思绪,太多的决策,所以当你在 2023 年,PC 互联网已经凉到不能再凉的时候,接到一个官网的需求,你会怎么去做呢,耐心看完,希望对你有所帮助。


官网开发作为前端的传统艺能已经诞生很久了,与现在主流的 react/vuecsr 框架开发不同,官网对 seo 有一定要求,所以咱们的网页不再是开局一个空 html,渲染全靠 js 了。传统的官网开发,一般采用 WordPress 建站,很好的技术栈 php + html/css/js 前后台一把梭,方便快捷,架构成熟。不过当遇到公司的前后端技术栈是 java + react 时,这个方案显然对开发人员并不友好,作为一名思维跳脱,勇于承担的前端,灵光乍现,我们是不是可以自己独立的完成整个官网呢。


在掘金上搜了搜,居然还有现成的课程,看看时间还挺新的,不如就从它入手吧。




作为网站尊贵的会员,我用兑换券换到了这门课程,开始了如饥似渴的学习。文章内容还是比较浅显易懂的,跟着操作下一个网站的雏形就出现了。课程中的技术栈主要采用 next.js + strapi 完成对官网的开发,相信各位看官并不陌生,业界知名的 react ssr 框架和 headless cms 系统,基础的介绍就不多说了,来分享点实操中得到经验教训吧。


next.js 相关


收获到的知识


1. 如何调试 next.js 服务端渲染的逻辑


之前,我只会使用 console.log 在启动的命令行里去判断代码的问题,现在我知道了使用 cross-env NODE_OPTIONS='--inspect' next dev 命令,他会开启一个 nodejsinspect 调试窗口,因为 next.js 的系统对象都很复杂,层级深,如果 console.log 的话很觉得很烦人,通过如上的方式就能便捷。






2. 生成有参数的静态页面的方法


其实像官网流量挺小的,没有必要走静态导出的 ssg ,使用 ssr 也完全没有问题,但是鉴于对 k8s 扩容,服务器费用之类的知识不太了解,官网的迭代频率也很低,所以本次采用了 ssg 的方案,要求所有子页面都要生成, 这就不得不提到内置的 api名为 getStaticPaths了。他会在打包阶段,先从一个列表接口拿到所有的id,再执行 getStaticProps 中的逻辑,拿到所有详情数据。比如我们的故事详情页,共有两条数据,id为1,2,执行完后,就会在对应目录下生成两个 html






得到的经验教训


1. 移动端适配


一个简易的判断移动端的代码很容易写,先不纠结完备性,大致如下:

/mobile|android|iphone|ipad|phone/i.test(window.navigator.userAgent.toLowerCase())

然而在 next.jsssg 方案中你却很难拿到,因为 render 里不能有对 window 的直接使用。我想了很多取巧的方法,但是都不行,如果是 ssr 还可以用请求头的 user-agent 来判断,作为属性透传到组件中,如课程一样。但 ssg 你只能使用响应式,同时渲染2套布局,才是最简单的做法。仔细想想,因为 html 字符串的构建是在 build 的时候就已经决定了,所以不能动态的拼接成不同的 dom 也就很好理解了。



PS: 感谢 @wonderL17 指出,也可以使用nginx配置的方式,将对应路径转发到为移动端生成的页面。
如果页面复用逻辑多,且能很好支持响应式,可以使用响应式。如果完全是另一套风格,则可以使用配置ng方式去处理,更为优秀,代码会更好维护且生成的页面更小。



2. api目录的处理


next.js 中的 api 目录可以承担接口的功能,课程中在 api 层进行了一次中转,我对此呈保留意见,想法如下:

  • 如果 strapi 不能直接支持一定的并发量,那么使用 api 来代理请求一样不能达到。

  • 中间层越多,可能出现的协作编码问题就越多,比如 strapi 返回的数据不符合预期,有些人会去修改 strapi,有些人会去改 next.js 的 api,这样会造成项目维护的不统一。


其实简单的对 strapi 加个跨域的配置,就能很好的直接在每个组件内使用,这对于一般的官网是完全可以满足的,而且代码清晰整洁。


strapi 相关


strapi 暂时没有给我带来新的知识,因为没有对他的原理进行研究,最核心的功能,编辑类型,即可生成对应的增删改查界面是最值得学习的部分,可是也是他最成熟的部分,开发嘛,能用就行,所以只有使用上的踩坑记录。但不得不说,strapi 的模块设计的挺不错的(除开dashboard ui定制外),有兴趣的可以了解下代码原理和设计思路。


得到的经验教训


1. 数据库(!!!!!)


请一定不要用 sqlite,不然你会哭的。因为项目肯定是需要多人协作的,强大如你 merge 代码对你来说轻而易举,但是你会 merge db 文件吗?相信我,你不会。


所以不要等项目已成,你才想起来去换个数据库,那么你即将面临,如果将 sqlite 数据导出成 postgresql 或者 mysql 数据的问题,听上去并不难吧,我花了 1~2 天时间才把这个无意义的工作做完。虽然能查到很多方案,但开源的不好用,付费的不想试,还是老老实实导出 sql,再导入是最快的。(不详述了,都是辛酸,你懂的,装了一堆东西,要么环境问题,要么功能问题,迟迟无法完成迁移的痛)


所以一开始就选好数据库,很关键,这样既方便了协作开发,又方便了后续使用。因为 4.x? 具体是几不清楚,relations 排序不生效(postgresql | mysql),sqlite 没有这个问题,所以替换数据库后,升级下 strapi,我升级的是4.10.6,顺便可以把依赖里的 sharp 删掉,有时候要装很久。


2. OSS(!!!!)


oss 一样至关重要,如果你使用了 strapi 的媒体库功能,请一定先配置好自定义的 oss,使用这个库就好了 strapi-provider-upload-oss,配置起来很方便,如果你头铁,一开始就是不配,你将面临,将上传好的图片删除,再重新上传一遍,没错他没有批量替换的功能,只能手动操作,我也没有花很久,1个小时,重新换了70~80张图片(关键素材上次用完还删了)。


配置如下:

// config/plugins.js

module.exports = ({ env }) => ({
upload: {
config: {
provider: "strapi-provider-upload-oss", // full package name is required
providerOptions: {
accessKeyId: "xxxx", // 用你的 oss 配置把 xxx 换掉,但千万别上传到开源库里(如github, gitee)哦
accessKeySecret: "xxx",
region: "xxx",
bucket: "xxx",
uploadPath: "/strapi/static",
baseUrl: "xxx",
timeout: 3000,
secure: true,
},
},
},
});

3. 数据的格式化(!!!)


课程中自己定义 removeTime, removeAttrsAndId 等方法,对数据进行处理,可能写的比较早,还没有成熟的转换库,这里介绍下 strapi-plugin-transformer,可以快速的去掉没必要的层级结构和一些属性,相当好用。配置如下:

// config/plugins.js

module.exports = ({ env }) => ({
transformer: {
enabled: true,
config: {
responseTransforms: {
removeAttributesKey: true,
removeDataKey: true,
},
},
},
});

这个操作可以避免对数据进行过多的处理,也就意味着 src/api/xx 里的代码,你基本不需要手动修改了,大大提升了后台配置的开发效率。C端对数据的处理也更简单了


4. 一些小的注意点

  • config 目录下添加插件,需要创建 plugins.js 文件,少写了 s 会导致插件不生效。(PS:嗯,就是粗心的我建了 plugin.js 还怪人家插件不好使~)

  • .cache 还挺有用的,一些修改不生效,可以试试 build 后再重启试试。


一些拓展能力


strapi 的初始状态很难满足直接交付给运营配置,最大的坑点在于类型定义是英文的,还没有层级结构,这里参考了这篇文章提到的方案:juejin.cn/post/721922…,来对 dashboard 进行定制。功能主要分为3个步骤,patch-package 使用参考原文章即可。

  • 类型汉化


只需要修改 admin/app.js 即可


  • 类型层级&排序:.cache/admin/src/content-manger/pages/App/LeftMenu 文件

// 目录排序,1,1.1,2,2.1
function compareDirectories(formatter, dir1, dir2) {
// 提取目录中的数字和点号
const regex = /(\d+|\.)+/g;
const arr1 = dir1.match(regex);
const arr2 = dir2.match(regex);

if (!arr1 || !arr2) return formatter.compare(dir1, dir2);

// 比较每个部分的数字
for (let i = 0; i < Math.max(arr1.length, arr2.length); i++) {
const num1 = parseFloat(arr1[i]) || 0; // 如果无法解析为数字,默认为0
const num2 = parseFloat(arr2[i]) || 0;

if (num1 < num2) {
return -1;
} else if (num1 > num2) {
return 1;
}
}

// 如果所有部分都相同,则按照长度进行比较
if (arr1.length < arr2.length) {
return -1;
} else if (arr1.length > arr2.length) {
return 1;
}

return 0; // 目录完全相同
}



// Sort correctly using the language
// 这条注释下,用目录排序的方法替代原有 sort

// SubNavLink 内,使用 dangerouslySetInnerHTML 体现目录层级
<span dangerouslySetInnerHTML={{__html: link.title.replace(/([0-9]+.)/g, (a, b, index) => {
if (index <= 1) return '';

return '   ';
})}}></span>


这样你就能得到一个这样的配置目录




  • 项目部署

因为我这边项目是 ssg,修改完内容是需要触发流水线重新部署的,如果像想避免这个工作,可以增加一些按钮,触发 webhook。这样只要排版是够用的,就不需要开发介入了。


如下示例,修改的是 .cache/admin/src/pages/HomePage/index.js




以上改动想要生效,记得用 patch-package,这也是我说他的定制模块不友好的原因~


总结


next.jsstapi 的学习,踩坑,使用经验就是这么多了,起初发起这个项目,是因为我们官网的框架有些老了,用的 fis3,有时候法务、市场同学来找替换素材,都是没什么工作难度的事儿,浪费时间,也整的挺烦的,所以借着机会升级了一下,以后就事半功倍了。


至于在当今这个时代,官网的 seo 是否还有意义,是否还能够为公司带来不俗的自然增量,这个我最感兴趣的事儿,迟迟没能发起。


因为在公司,这是涉及很多部门(品牌,公关,法务,市场,产品,设计)的事儿,普通开发并不能调动资源,我也很期待,如果有幸能发起一个这样的项目,并不断通过技术上的优化为业务带来新的增量,那里可能会用到更多的贴合业务的 ssr 技术,到时候有机会再和大家分享下~


但我也想对技术感兴趣的朋友说,底层的技术重构也是很有魅力的一件事儿,尽管没有业务方的支持,如果你持之以恒的来做,复刻原产品,也能让这个产品在技术层面上焕然一新。


谨以此文,与君共勉!


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

你在公司混的差,可能和组织架构有关!

如果你接触过公司的面试工作,一定见过很多来自大公司的渣渣。这些人的薪资和职位,比你高出很多,但能力却非常一般。 如果能力属实,我们大可直接把这些大公司的员工打包接收,也免了乱七八糟的面试工作。但可惜的是,水货的概率通常都比较大,新的公司也并不相信他们的能力。尤...
继续阅读 »

如果你接触过公司的面试工作,一定见过很多来自大公司的渣渣。这些人的薪资和职位,比你高出很多,但能力却非常一般。


如果能力属实,我们大可直接把这些大公司的员工打包接收,也免了乱七八糟的面试工作。但可惜的是,水货的概率通常都比较大,新的公司也并不相信他们的能力。尤其是这两年互联网炸了锅,猪飞的日子不再,这种情况就更加多了起来。


反过来说也一样成立,就像是xjjdog在青岛混了这么多年,一旦再杀回北上广,也一样是落的下乘的评价。


除了自身的努力之外,你在上家公司混的差,还与你在组织架构中所处于的位置和组织架构本身有关。


一般公司会有两种组织架构方式:垂直化划分层级化划分


1. 垂直划分


垂直划分,多以业务线为模型进行划分。各条业务线共用公司行政资源,相互之间关联不大。


各业务线之间,内部拥有自治权。


image.png


如上图所示,公司共有四个业务线。




  • 业务线A,有前端和后端开发。因为成员能力比较强,所以没有测试运维等职位;




  • 业务线B倡导全栈技能,开发后台前端一体化;




  • 业务线C的管理能力比较强,仅靠少量自有研发,加上大量的外包,能够完成一次性工作。




  • 业务线D是传统的互联网方式,专人专岗,缺什么招什么,不提倡内部转岗




运行模式




  1. 业务线A缺人,缺项目,与业务线BCD无任何关系,不允许借调




  2. 业务线发展良好,会扩大规模;其他业务线同学想要加入需要经过复杂的流程,相当于重新找工作




  3. 业务线发展萎靡,会缩减人员,甚至会整体砍掉。优秀者会被打散吸收进其他业务线




好处




  1. 业务线之间存在竞争关系,团队成员有明确的奋斗目标和危机意识




  2. 一条业务线管理和产品上的失败,不会影响公司整体运营




  3. 可以比较容易的形成单向汇报的结构,避免成本巨大且有偏差的多重管理




  4. 便于复制成功的业务线,或者找准公司的发展重点




坏处




  1. 对业务线主要分管领导的要求非常高




  2. 多项技术和产品重复建设,容易造成人员膨胀,成本浪费




  3. 部门之间隔阂加大,共建、合作困难,与产品化相逆




  4. 业务线容易过度自治,脱离掌控




  5. 太激进,大量过渡事宜需要处理




修订


为了解决上面存在的问题,通常会有一个协调和监管部门,每个业务线,还需要有响应的协调人进行对接。以以往的观察来看,效果并不会太好。因为这样的协调,多陷于人情沟通,不好设计流程规范约束这些参与人的行为。


image.png


在公司未摸清发展方向之前,并不推荐此方式的改革。它的本意是通过竞争增加部门的进取心,通过充分授权和自治发挥骨干领导者的作用。但在未有成功案例之前,它的结果变成了:寄希望于拆分成多个小业务线,来解决原大业务线存在的问题。所以依然是处于不太确定的尝试行为。


2. 水平划分


水平划分方式,适合公司有确定的产品,并能够形成持续迭代的团队。


它的主要思想,是要打破“不会做饭的项目经理不是好程序员”的思维,形成专人专业专岗的制度。


这种方式经历了非常多的互联网公司实践,可以说是最节约研发成本,能动性最高的组织方式。主要是因为:




  • 研发各司其职,做好自己的本职工作可以避免任务切换、沟通成本,达到整体最优




  • 个人单向汇报,组织层级化,小组扁平化。“替领导负责,就是替公司负责”




  • 任何职位有明确的JD,可替换性高,包括小组领导




这种方式最大的问题就是,对团队成员的要求都很高。主动性与专业技能都有要求,需要经过严格的面试筛选。


坏处




  • 是否适合项目类公司,存疑




  • 存在较多技术保障部门,公共需求 下沉容易造成任务积压




  • 需要对其他部门进行整合,才能发挥更大的价值




分析


image.png


如上图,大体会分为三层。




  • 技术保障,保障公司的底层技术支撑,问题处理和疑难问题解决。小组多但人少,职责分明




  • 基础业务,公司的旗舰业务团队,需求变更小但任何改动都非常困难。团队人数适中




  • 项目演化,纯项目,可以是一锤子买卖,也可以是服务升级,属于朝令夕改类需求的聚居地。人数最多




可以看到项目演化层,多是脏活,有些甚至是尝试性的项目-----这是合理的。




  1. 技术保障和基础业务的技术能力要求高,业务稳定,适合长期在公司发展,发展属性偏技术的人群,流动性小,招聘困难




  2. 项目演化层,业务多变,项目奖金或者其他回报波动大,人员流动性高,招聘容易




成功的孵化项目,会蜕变成产品,或者基础业务,并入基础业务分组。


从这种划分可以看出,一个人在公司的命运和发展,在招聘入职的时候就已经确定了。应聘人员可以根据公司的需求进行判断,提前预知自己的倾向。


互联网公司大多数将项目演化层的人员当作炮灰,因为他们招聘容易,团队组件迅速,但也有很多可能获得高额回报,这也是很多人看中的。


3.组合


组合一下垂直划分和层级划分,可以是下面这种效果。


image.png


采用层级+垂直方式进行架构。即:首选层级模式,然后在项目演化层采用垂直模式,也叫做业务线,拥有有限的自治权。


为每一个业务线配备一个与下层产品化或者技术保障对接的人员。


绩效方面,上层的需求为下层的实现打分。基础业务和技术保障,为绿色的协调人员打分。他们的利益是一致的。


End


大公司出来的并不一定是精英,小公司出来的也并不一定是渣渣。这取决于他在公司的位置和所从事的内容。核心部门会得到更多的利益,而边缘的尝试性部门只能吃一些残羹剩饭。退去公司的光环,加上平庸的项目经历,竞争力自然就打上一个折扣。


以上,仅限IT行业哦。赵家人不在此列。


作者:小姐姐味道
来源:juejin.cn/post/7203651773622452261
收起阅读 »

解决扫码枪因输入法中文导致的问题

web
问题 最近公司项目上遇到了扫码枪因搜狗/微软/百度/QQ等输入法在中文状态下,使用扫码枪扫码会丢失字符的问题 思考 这种情况是由于扫码枪的硬件设备,在输入的时候,是模拟用户键盘的按键来实现的字符输入的,所以会触发输入法的中文模式,并且也会触发输入法的自动联想。...
继续阅读 »

问题


最近公司项目上遇到了扫码枪因搜狗/微软/百度/QQ等输入法在中文状态下,使用扫码枪扫码会丢失字符的问题


思考


这种情况是由于扫码枪的硬件设备,在输入的时候,是模拟用户键盘的按键来实现的字符输入的,所以会触发输入法的中文模式,并且也会触发输入法的自动联想。那我们可以针对这个来想解决方案。


方案一


首先想到的第一种方案是,监听keydown的键盘事件,创建一个字符串数组,将每一个输入的字符进行比对,然后拼接字符串,并回填到输入框中,下面是代码:


function onKeydownEvent(e) {
this.code = this.code || ''
const shiftKey = e.shiftKey
const keyCode = e.code
const key = e.key
const arr = ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-']
this.nextTime = new Date().getTime()
const timeSpace = this.nextTime - this.lastTime
if (key === 'Process') { // 中文手动输入
if (this.lastTime !== 0 && timeSpace <= 30) {
for (const a of arr) {
if (keyCode === 'Key' + a) {
if (shiftKey) {
this.code += a
} else {
this.code += a.toLowerCase()
}
this.lastTime = this.nextTime
} else if (keyCode === 'Digit' + a) {
this.code += String(a)
this.lastTime = this.nextTime
}
}
if (keyCode === 'Enter' && timeSpace <= 30) {
if (String(this.code)) {
// TODO
dosomething....
}
this.code = ''
this.nextTime = 0
this.lastTime = 0
}
}
} else {
if (arr.includes(key.toUpperCase())) {
if (this.lastTime === 0 && timeSpace === this.nextTime) {
this.code = key
} else if (this.lastTime !== 0 && timeSpace <= 30) {
// 30ms以内来区分是扫码枪输入,正常手动输入时少于30ms的
this.code += key
}
this.lastTime = this.nextTime
} else if (arr.includes(key)) {
if (this.lastTime === 0 && timeSpace === this.nextTime) {
this.code = key
} else if (this.lastTime !== 0 && timeSpace <= 30) {
this.code += String(key)
}
this.lastTime = this.nextTime
} else if (keyCode === 'Enter' && timeSpace <= 30) {
if (String(this.code)) {
// TODO
dosomething()
}
this.code = ''
this.nextTime = 0
this.lastTime = 0
} else {
this.lastTime = this.nextTime
}
}
}


这种方案能解决部分问题,但是在不同的扫码枪设备,以及不同输入法的情况下,还是会出现丢失问题


方案二


使用input[type=password]来兼容不同输入的中文模式,让其只能输入英文,从而解决丢失问题


这种方案网上也有不少的参考

# 解决中文状态下扫描枪扫描错误

# input type=password 取消密码提示框


使用password密码框确实能解决不同输入法的问题,并且Focus到输入框,输入法会被强制切换为英文模式


添加autocomplete="off"autocomplete="new-password"属性


官方文档:
# 如何关闭表单自动填充


但是在Chromium内核的浏览器,不支持autocomplete="off",并且还是会出现这种自动补全提示:


image.png


上面的属性并没有解决浏览器会出现密码补全框,并且在输入字符后,浏览器还会在右上角弹窗提示是否保存:


image.png


先解决密码补全框,这里我想到了一个属性readonly,input原生属性。input[type=password]readonly
时,是不会有密码补全的提示,并且也不会弹窗提示密码保存。


那好,我们就可以在输入前以及输入完成后,将input[type=password]立即设置成readonly


但是需要考虑下面几种情况:



  • 获取焦点/失去焦点时

  • 当前输入框已focus时,再次鼠标点击输入框

  • 扫码枪输出完成最后,输入Enter键时,如果清空输入框,这时候也会显示自动补全

  • 清空输入框时

  • 切换离开页面时


这几种情况都需要处理,将输入框变成readonly


我用vue+element-ui实现了一份,贴上代码:


<template>
<div class="scanner-input">
<input class="input-password" :name="$attrs.name || 'one-time-code'" type="password" autocomplete="off" aria-autocomplete="inline" :value="$attrs.value" readonly @input="onPasswordInput">
<!-- <el-input ref="scannerInput" v-bind="$attrs" v-on="$listeners" @input="onInput"> -->
<el-input ref="scannerInput" :class="{ 'input-text': true, 'input-text-focus': isFocus }" v-bind="$attrs" v-on="$listeners">
<template v-for="(_, name) in $slots" v-slot:[name]>
<slot :name="name"></slot>
</template>
<!-- <slot slot="suffix" name="suffix"></slot> -->
</el-input>
</div>
</template>

<script>
export default {
name: 'WispathScannerInput',
data() {
return {
isFocus: false
}
},
beforeDestroy() {
this.$el.firstElementChild.setAttribute('readonly', true)
this.$el.firstElementChild.removeEventListener('focus', this.onPasswordFocus)
this.$el.firstElementChild.removeEventListener('blur', this.onPasswordBlur)
this.$el.firstElementChild.removeEventListener('blur', this.onPasswordClick)
this.$el.firstElementChild.removeEventListener('mousedown', this.onPasswordMouseDown)
this.$el.firstElementChild.removeEventListener('keydown', this.oPasswordKeyDown)
},
mounted() {
this.$el.firstElementChild.addEventListener('focus', this.onPasswordFocus)
this.$el.firstElementChild.addEventListener('blur', this.onPasswordBlur)
this.$el.firstElementChild.addEventListener('click', this.onPasswordClick)
this.$el.firstElementChild.addEventListener('mousedown', this.onPasswordMouseDown)
this.$el.firstElementChild.addEventListener('keydown', this.oPasswordKeyDown)

const entries = Object.entries(this.$refs.scannerInput)
// 解决ref问题
for (const [key, value] of entries) {
if (typeof value === 'function') {
this[key] = value
}
}
this['focus'] = this.$el.firstElementChild.focus.bind(this.$el.firstElementChild)
},
methods: {
onPasswordInput(ev) {
this.$emit('input', ev.target.value)
if (ev.target.value === '') {
this.$el.firstElementChild.setAttribute('readonly', true)
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
})
}
},
onPasswordFocus(ev) {
this.isFocus = true
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
})
},
onPasswordBlur() {
this.isFocus = false
this.$el.firstElementChild.setAttribute('readonly', true)
},
// 鼠标点击输入框一瞬间,禁用输入框
onPasswordMouseDown() {
this.$el.firstElementChild.setAttribute('readonly', true)
},
oPasswordKeyDown(ev) {
// 判断enter键
if (ev.key === 'Enter') {
this.$el.firstElementChild.setAttribute('readonly', true)
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
})
}
},
// 点击之后,延迟200ms后放开readonly,让输入框可以输入
onPasswordClick() {
if (this.isFocus) {
this.$el.firstElementChild.setAttribute('readonly', true)
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
}, 200)
}
},
onInput(_value) {
this.$emit('input', _value)
},
getList(value) {
this.$emit('input', value)
}
// onChange(_value) {
// this.$emit('change', _value)
// }
}
}
</script>

<style lang="scss" scoped>
.scanner-input {
position: relative;
height: 36px;
width: 100%;
display: inline-block;
.input-password {
width: 100%;
height: 100%;
border: none;
outline: none;
padding: 0 16px;
font-size: 14px;
letter-spacing: 3px;
background: transparent;
color: transparent;
// caret-color: #484848;
}
.input-text {
font-size: 14px;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
pointer-events: none;
background-color: transparent;
::v-deep .el-input__inner {
// background-color: transparent;
padding: 0 16px;
width: 100%;
height: 100%;
}
}

.input-text-focus {
::v-deep .el-input__inner {
outline: none;
border-color: #1c7af4;
}
}
}
</style>


至此,可以保证input[type=password]不会再有密码补全提示,并且也不会再切换页面时,会弹出密码保存弹窗。
但是有一个缺点,就是无法完美显示光标。如果用户手动输入和删除,使用起来会有一定的影响。


我想到过可以使用模拟光标,暂时不知道可行性。有哪位同学知道怎么解决的话,可以私信我,非常感谢


作者:香脆又可口
来源:juejin.cn/post/7265666505102524475
收起阅读 »

假如面试官让你讲一下新版雪花算法

这是一篇由 Seata 的官网上一篇叫做“关于新版雪花算法的答疑”的文章引发的思考。 seata.io/zh-cn/blog/… 还是有点意思的,结合自己的理解和代码,加上画几张图来拆解一下 Seata 里面的“改良版雪花算法”。 虽然是在 Seata ...
继续阅读 »

这是一篇由 Seata 的官网上一篇叫做“关于新版雪花算法的答疑”的文章引发的思考。



seata.io/zh-cn/blog/…



image.png


还是有点意思的,结合自己的理解和代码,加上画几张图来拆解一下 Seata 里面的“改良版雪花算法”。


虽然是在 Seata 里面看到的,但是本篇文章的内容和 Seata 框架没有太多关系,反而和数据库的基础知识有关。


所以,即使你不了解 Seata 框架,也不影响你阅读。


当你理解了这个类的工作原理之后,你完全可以把这个只有 100 多行的类搬运到你的项目里面,然后就变成你的了。


dddd(懂的都懂)!



先说问题



雪花算法的使用场景:


如果你的项目中涉及到需要一个全局唯一的流水号,比如订单号、流水号之类的,又或者在分库分表的情况下,需要一个全局唯一的主键 ID 的时候,就需要一个算法能生成出这样“全局唯一”的数据。


一般来说,我们除了“全局唯一”这个基本属性之外,还会要求生成出来的 ID 具有“递增趋势”,这样的好处是能减少 MySQL 数据页分裂的情况,从而减少数据库的 IO 压力,提升服务的性能。


此外,在当前的市场环境下,不管你是啥服务,张口就是高并发,我们也会要求这个算法必须得是“高性能”的。


雪花算法,就是一个能生产全局唯一的、递增趋势的、高性能的分布式 ID 生成算法。


关于雪花算法的解析,网上相关的文章比雪花还多,这个玩意,应该是“面试八股文”中重点考察模块,分布式领域中的高频考题之一,如果是你的盲区的话,可自行去了解一下。


比如一个经典的面试题就是:


雪花算法最大的缺点是什么?


背过题的小伙伴应该能立马答出来:时钟敏感


因为在雪花算法中,由于要生成单调递增的 ID,因此它利用了时间的单调递增性,所以是强依赖于系统时间的。


如果系统时间出现了回拨,那么生成的 ID 就可能会重复。


而“时间回拨”这个现象,是有可能出现的,不管是人为的还是非人为的。


当你回答出这个问题之后,面试官一般会问一句:


那如果真的出现了这种情况,应该怎么办呢?


很简单,正常来说只要不是不是有人手贱或者出于泄愤的目的进行干扰,系统的时间漂移是一个在毫秒级别的极短的时间。


所以可以在获取 ID 的时候,记录一下当前的时间戳。然后在下一次过来获取的时候,对比一下当前时间戳和上次记录的时间戳,如果发现当前时间戳小于上次记录的时间戳,所以出现了时钟回拨现象,对外抛出异常,本次 ID 获取失败。


理论上当前时间戳会很快的追赶上上次记录的时间戳。


但是,你可能也注意到了,“对外抛出异常,本次 ID 获取失败”,意味着这段时间内你的服务对外是不可使用的。


比如,你的订单号中的某个部分是由这个 ID 组成的,此时由于 ID 生成不了,你的订单号就生成不了,从而导致下单失败。


再比如,在 Seata 里面,如果是使用数据库作为 TC 集群的存储工具,那么这段时间内该 TC 就是处于不可用状态。


你可以简单的理解为:基础组件的错误导致服务不可用。



再看代码



基于前面说的问题,Seata 才提出了“改良版雪花算法”。



seata.io/zh-cn/blog/…



图片


在介绍改良版之前,我们先把 Seata 的源码拉下来,瞅一眼。


在源码中,有一个叫做 IdWorker 的类:



io.seata.common.util.IdWorker



再来看一下它的提交记录:


图片


2020 年 5 月 4 日第一次提交,从提交时的信息可以看出来,这是把分布式 ID 的生成策略修改为 snowflake,即雪花算法。


同时我们也能在代码中找到前面提到的“对外抛出异常,本次 ID 获取失败”相关代码,即 nextId  方法,它的比较方式就是用当前时间戳和上次获取到的时间戳做对比:



io.seata.common.util.IdWorker#nextId



图片


这个类的最后一次提交是 2020 年 12 月 15 日:


图片


这一次提交对于 IdWorker 这个类进行了大刀阔斧的改进,可以看到变化的部分非常的多:


图片


我们重点关注刚刚提到的 nextId 方法:


图片


整个方法从代码行数上来看都可以直观的看到变化,至少没有看到抛出异常了。


这段代码到底是怎么起作用的呢?


首先,我们得理解 Seata 的改良思路,搞明白思路了,再说代码就好理解一点。


在前面提到的文章中 Seata 也说明了它的核心思路:


图片


原版的雪花算法 64 位 ID 是分配这样的:


图片


可以看到,时间戳是在最前面的,因为雪花算法利用了时间的单调递增的特性。


所以,如果前面的时间戳一旦出现“回退”的情况,即打破了“时间的单调递增”这个前提条件,也就打破了它的底层设计。


它能怎么办?


它只能给你抛出异常,开始摆烂了。


可以看到节点 ID 长度为二进制的 10 位,也就是说最多可以服务于 1024 台机器,所以你看 Seata 最开始提交的版本里面,有一个在 1024 里面随机的动作。


因为算法规定了,节点 ID 最多就是 2 的 10 次方,所以这里的 1024 这个值就是这样来的:


图片


包括后面有大佬觉得用这个随机算法一点都不优雅,就把这部分改成了基于 IP 去获取:


图片


看起来有点复杂,但是我们仔细去分析最后一行:



return ((ipAddressByteArray[ipAddressByteArray.length - 2] & 0B11) << Byte.SIZE) + (ipAddressByteArray[ipAddressByteArray.length - 1] & 0xFF);



变量 & 0B11 运算之后的最大值就是 0B11 即 3。


Byte.SIZE = 8。


所以,3 << 8,对应二进制 1100000000,对应十进制 768。


变量 & 0xFF 运算之后的最大值就是 0xFF 即 255。


768+255=1023,取值范围都还是在 [0,1023] 之间。


然后你再看现在最新的算法里面,官方的老哥们认为获取 IP 的方式不够好:


图片


所以又把这个地方从使用 IP 地址改成了获取 Mac 地址。


图片


最后一行是这样的:



return ((mac[4] & 0B11) << 8) | (mac[5] & 0xFF);



那么理论上的最大值就是 768 | 255 ,算出来还是 1023。


所以不管你怎么玩出花儿来,这个地方搞出来的数的取值范围就只能是 [0,1023] 之间。


别问,问就是规定里面说了,算法里面只给节点 ID 留了 10 位长度。


最后,就是这个 12 位长度的序列号了:


图片


这个玩意没啥说的,就是一个单纯的、递增的序列号而已。


既然 Seata 号称是改良版,那么具体体现在什么地方呢?


简单到你无法想象:


图片


是的,仅仅是把时间戳和节点 ID 换个位置就搞定了。


然后每个节点的时间戳是在 IdWorker 初始化的时候就设置完成了,具体体现到代码上是这样的:



io.seata.common.util.IdWorker#initTimestampAndSequence



图片


在获取 ID 的过程中,只有在初始化的时候获取过一次系统时间,之后和它就再也没有关系了。


所以,Seata 的分布式 ID 生成器,不再依赖于时间。


然后,你再想想另外一个问题:


由于序列号只有 12 位,它的取值范围就是 [0,4095]。如果我们序列号就是生成到了 4096 导致溢出了,怎么办呢?



很简单,序列号重新归 0,溢出的这一位加到时间戳上,让时间戳 +1。


那你再进一步想想,如果让时间戳 +1 了,那么岂不是会导致一种“超前消费”的情况出现,导致时间戳和系统时间不一致了?


朋友,慌啥啊,不一致就不一致呗,反正我们现在也不依赖于系统时间了。


然后,你想想,如果出现“超前消费”,意味着什么?


意味着在当前这个毫秒下,4096 个序列号不够用了。


4096/ms,约 400w/s。


你啥场景啊,怎么牛逼?


(哦,原来是面试场景啊,那懂了~)


另外,官网还抛出了另外一个问题:这样持续不断的"超前消费"会不会使得生成器内的时间戳大大超前于系统的时间戳,从而在重启时造成 ID 重复?


你想想,理论上确实是有可能的。假设我时间戳都“超前消费”到一个月以后了。


那么在这期间,你服务发生重启时我会重新获取一次系统时间戳,导致出现“时间回溯”的情况。


理论上确实有可能。



但是实际上...


看看官方的回复:


图片



别问,问就是不可能,就算出现了,最先崩的也不是我这个地方。



说了这么多其实就记住住这个图,就完事了:


图片


那么问题又来了:


改良版的算法是单调递增的吗?



在单节点里面,它肯定是单调递增的,但是如果是多个节点呢?


在多个节点的情况下,单独看某个节点的 ID 是单调递增的,但是多个节点下并不是全局单调递增。


因为节点 ID 在时间戳之前,所以节点 ID 大的,生成的 ID 一定大于节点 ID 小的,不管时间上谁先谁后。



这一点我们也可以通过代码验证一下,代码的意思是三个节点,每个节点各自生成 5 个 ID:


图片


从输出来看,一眼望去,生成的 ID 似乎是乱序的,至少在全局的角度下,肯定不是单调递增的:


但是我们把输出按照节点 ID 进行排序,就变成了这样,单节点内严格按照单调递增,没毛病:


图片


而在原版的雪花算法中,时间戳在高位,并且始终以系统时钟为准,每次生成的时候都会严格和系统时间进行对比,确保没有发生时间回溯,这样可以保证早生成的 ID 一定小于晚生成的 ID ,只有当 2 个节点恰好在同一时间戳生成 ID 时,2 个 ID 的大小才由节点 ID 决定。


这样看来,Seata 的改进算法是不是错的?


先别急,继续往下看。



分析一波



分析之前,先抛出官方的回答:


图片


先来一个八股文热身:


请问为什么不建议使用 UUID 作为数据库的主键 ID ?


就是为了避免触发 MySQL 的页分裂从而影响服务性能。


比如当前主键索引的情况是这样的:


图片


如果来了一个  433,那么直接追加在当前最后一个记录 432 之后即可。


图片


但是如果我们要插入一个 20 怎么办呢?


那么数据页 10 里面已经放满了数据,所以会触发页分裂,变成这样:


图片


进而导致上层数据页的分裂,最终变成这样的一个东西:


图片


上面的我们可以看出页分裂伴随着数据移动,所以我们应该尽量避免。


理想的情况下,应该是把一页数据塞满之后,再新建另外一个数据页,这样 B+ tree 的最底层的双向链表永远是尾部增长,不会出现上面画图的那种情况:在中间的某个节点发生分裂。


那么 Seata 的改良版的雪花算法在不具备“全局的单调递增性”的情况下,是怎么达到减少数据库的页分裂的目的的呢?


我们还是假设有三个节点,我用 A,B,C 代替,在数值上 A < B < C,采用的是改良版的雪花算法,在初始化的情况下是这样的。


图片


假设此时,A 节点申请了一个流水号,那么基于前面的分析,它一定是排在 A-seq1 之后,B-seq1 之前的。


但是这个时候数据页里面的数据满了,怎么办?


分裂呗:


图片


又来了 A-seq3 怎么办?


问题不大,还放的下:


图片


好,这个时候数据页 7 满了,但是又来了 A-seq4,怎么办?


只有继续分裂了:


图片


看到这个分裂的时候,你有没有嗦出一丝味道,是不是有点意思了?


因为在节点 A 上生成的任何 ID 都一定小于在节点 B 上生成的任何 ID,节点 B 和节点 C 同理。


在这个范围内,所有的 ID 都是单调递增的:


图片


而这样的范围最多有多少个?


是不是有多少个节点,就有多少个?


那么最多有多少个节点?


图片


2 的 10 次方,1024 个节点。


所以官方的文章中有这样的一句话:



新版算法从全局角度来看,ID 是无序的,但对于每一个 workerId,它生成的 ID 都是严格单调递增的,又因为 workerId 是有限的,所以最多可划分出 1024 个子序列,每个子序列都是单调递增的。



经过前面的分析,每个子序列总是单调递增的,所以每个子序列在有限次的分裂之后,最终都会达到稳态。


或者用一个数学上的说法:该算法是收敛的。


再或者,放个图看看:


图片


我想说作者画的时候尽力了,至于你看懂看不懂的,就看天意了。


页分裂


前面写的所有内容,你都能在官网上前面提到的两个文章中找到对应的部分。


但是关于页分裂部分,官方并没有进行详细说明。本来也是这样的,人家只是给你说自己的算法,没有必要延伸的太远。


“刚才说到了页分裂”,以面试官的嘴脸怎么可能放过你,“展开讲讲?”


链接已放,自行展开:



mysql.taobao.org/monthly/202…



还是先搞个图:


图片


问,在上面的这个 B+ tree 中,如果我要插入 9,应该怎么办?


因为数据页中已经没有位置了,所以肯定要触发页分裂。


会变成这样:


图片


这种页分裂方式叫做插入点(insert point)分裂。


其实在 InnoDB 中最常用的是另外一种分裂方式,中间点(mid point)分裂。


如果采用中间点(mid point)分裂,上面的图就会变成这样:


图片


即把将原数据页面中的 50% 数据移动到新页面,这种才是普遍的分裂方法。


这种分裂方法使两个数据页的空闲率都是 50%,如果之后的数据在这两个数据页上的插入是随机的话,那么就可以很好地利用空闲空间。


但是,如果后续数据插入不是随机,而是递增的呢?


比如我插入 10 和 11。


插入 10 之后是这样的:


图片


插入 11 的时候又要分页了,采用中间点(mid point)分裂就变成了这样:


图片


你看,如果是在递增的情况下,采用中间点(mid point)分裂,数据页 8 和 20 的空间利用率只有 50%。


因为数据的填充和分裂的永远是右侧页面,左侧页面的利用率只有 50%。


所以,插入点(insert point)分裂是为了优化中间点(mid point)分裂的问题而产生的。


InnoDB 在每个数据页上专门有一个叫做 PAGE_LAST_INSERT 的字段,记录了上次插入位置,用来判断当前插入是是否是递增或者是递减的。


如果是递减的,数据页则会向左分裂,然后移动上一页的部分数据过去。


如果判定为递增插入,就在当前点进行插入点分裂。


比如还是这个图:


图片


上次插入的是记录 8,本次插入 9,判断为递增插入,所以采用插入点分裂,所以才有了上面这个图片。


好,那么问题就来了,请听题:


假设出现了这种情况,阁下又该如何应对?


图片


在上面这个图的情况下,我要插入 10 和 9:


当插入 10 的时候,按 InnoDB 遍历 B+ tree 的方法会定位到记录 8,此时这个页面的 PAGE_LAST_INSERT 还是 8。所以会被判断为递增插入,在插入点分裂:


图片


同理插入 9 也是这样的:


图片


最终导致 9、10、11 都独自占据一个 page,空间利用率极低。


问题的根本原因在于每次都定位到记录 8(end of page),并且都判定为递增模式。


哦豁,你说这怎么办?


答案就藏在这一节开始的时候放的链接中:


图片


前面所画的图都是在没有并发的情况下展开的。


但是在这个部分里面,牵扯到了更为复杂的并发操作,同时也侧面解释了为什么 InnoDB 在同一时刻只能有有一个结构调整(SMO)进行。


这里面学问就大了去了,有兴趣的可以去了解一下 InnoDB 在 B+ tree 并发控制上的限制,然后再看看 Polar index 的破局之道。


反正我是学不动了。


哦,对了。前面说了这么多,还只是聊了页分裂的情况。


有分裂,就肯定有合并。


那么什么时候会触发页合并呢?


页合并会对我们前面探讨的 Seata 的改良版雪花算法带来什么影响呢?


别问了,别问了,学不动了,学不动了。


作者:essenceNow
来源:juejin.cn/post/7265641370495451148
收起阅读 »

iOS:零碎整理iOS音视频开发API

在ios开发过程中,音频经常会用到,而音频根据使用场合分为音效和音乐,音效一般只播放1~2秒ios音效支持的格式: ios 支持的音频格式有:aac、alac、he-aac、iLBc、IMA4、Linea PCM、MP3、CAF,其中,aac、alac、he-...
继续阅读 »

在ios开发过程中,音频经常会用到,而音频根据使用场合分为音效和音乐,音效一般只播放1~2秒

  • ios音效支持的格式: ios 支持的音频格式有:aac、alac、he-aac、iLBc、IMA4、Linea PCM、MP3、CAF,其中,aac、alac、he-aac、mp3、caf支持硬件解码,其他只支持软件解码, 软件界面因为比较耗电,所以,我们在开发过程中,经常采用的是caf、mp3

  • 音频库: AVFoundation.framework

代码

// 打开资源
NSURL* url =[[NSBundle mainBundle]URLForResource:@"m_03" withExtension:@"wav"];
SystemSoundID soundID;
AudioServicesCreateSystemSoundID((__bridge CFURLRef)(url), &soundID);
// 播放音效
AudioServicesPlaySystemSound(self.soundID);
// 删除音效
AudioServicesDisposeSystemSoundID(self.soundID);
  • 框架

  • 加载音乐资源并播放

AVAudioPlayer* player = musicDict[fileName];
if (!player) {
NSURL* url = [[NSBundle mainBundle] URLForResource:fileName withExtension:nil];
NSCAssert(url != nil, @"fileName not found musics");

NSError* error;
player = [[AVAudioPlayer alloc]initWithContentsOfURL:url error:&error];
if (error) {
NSLog(@"load music error");
return;
}
[musicDict setObject:player forKey:fileName];
}
if (player.isPlaying == NO) {
[player play];
}
  • 暂停 停止操作

[player pause];// 暂停
[player stop];// 停止
[player isplaying];// 是否在播放

好了,现在能播放音乐了,但我们在看其他的应用的时候,一般当应用切换到后台的时候也能播放音乐,那这个又是如何实现的呢?这个只要设置音频的后台播放,具体为:

1> 在后台开启一个任务

- (void)applicationDidEnterBackground:(UIApplication *)application
{
// 开启后台任务,让音乐继续播放
[application beginBackgroundTaskWithExpirationHandler:nil];
}

  2> 设置项目配置文件


   3> 设置音频链接会话,这个主要告诉设备如何处理音频事件的

1234// 设置音频会话类型``   ``AVAudioSession* session = [AVAudioSession sharedInstance];``   ``[session setCategory:AVAudioSessionCategorySoloAmbient error:``nil``];``   ``[session setActive:``YES error:``nil``];

这里有很多会话类型,如果想详细了解,可参考:blog.csdn.net/daiyelang/a…

现在应该可以播放音乐了。


作者:会飞的金鱼
链接:https://juejin.cn/post/7238110426147373112
来源:稀土掘金
收起阅读 »

iOS中UICollectionView的item增删、拖拽和排序动画

iOS
效果图 这个是前段时间项目新增的一个功能,刚刚开始组员是用UIScrollView + UIView 实现的,但这种实现方式属实是有点low,后续闲暇时笔者用UICollectionView简单实现了下。 思路 简单理一下思路,首先是把整个页面先布局出来,这...
继续阅读 »

效果图




这个是前段时间项目新增的一个功能,刚刚开始组员是用UIScrollView + UIView 实现的,但这种实现方式属实是有点low,后续闲暇时笔者用UICollectionView简单实现了下。


思路


简单理一下思路,首先是把整个页面先布局出来,这里涉及到一个UICollectionViewSection背景色的问题,有需要的可以点这里,有详细的介绍。移动动画也很简单,先获取起点cell和终点cell,再新建一个动画的AnimationItem,根据获取的起点和终点cell动画就行,最后再实现拖拽排序效果。


大致总结一下:

  • 布局

  • 移动动画

  • 拖拽排序


下面就根据思路一步步来。我们一定要有一个意识,不管是多么复杂的动画,只要把它分解开来,按步骤一步一步实现就很简单。


实现


我们就跟上面的思路一步一步实现。


布局


首先想到肯定是新建一个Model来管理这个数据,新建Model也要有点技巧。

struct ItemModel {
    var section: Int = -1 // cell的section索引
    var item: Int = -1 // cell的item索引
    var name: String = "" // 名称
    var isAdded: Bool = false // 是否添加到首页应用(第0区)
    var id: String { // 唯一标识,可以用这个来命名图片的名称,也可以用来作判断
        get {
            "\(section)_\(item)"
        }
    }
    init(){}
}

看得出来,ItemModel的属性section + item = IndexPath,可以根据 model 知道当前cell的所在位置了。


笔者这用的是 struct ,感觉用 class 会更好点,因为后续会改变数组中Model的属性值。已经写了就懒得再改了。


存在2个数组数据:

  • var editItems = [ItemModel](),由前一页传入的、可编辑、拖拽的数据,位于UICollectionView的第0个Section。

  • var datas = [[ItemModel]](),按照Section的顺序,存放所有的数据。


注意:Section要从1开始,因为第0个Section是可以编辑拖拽的区域。


datas中存放全部的数据:

for i in 0..<names.count {
let subNames = names[i]
var items = [ItemModel]()
for j in 0..<subNames.count {
var model = ItemModel()
model.section = i+1 // 注意这里的Section要从1开始
model.item = j
model.name = subNames[j]
model.isAdded = editItems.contains(where: { $0.id == model.id})
items.append(model)
}
datas.append(items)
}

根据数据布局UICollectionView


移动动画


移动只要2个操作,添加应用和删除应用。


添加


笔者这里规定了最多可以添加8个应用。


大致思路:

  • 获取当前点击的 cell,为了得到其坐标作为动画起始位置

  • 在 collectionView 中插入一个空白的 cell 占位,此举是为了增加或减少行数的动画过渡更自然;对应也应该在 editItems 中添加一个空白的 model 作为数据源,等移动动画结束后再给model重新赋值。

  • 获取新插入的空白 cell,为了得到其坐标作为动画的结束位置

  • 生成动画的 cell,起始 -> 结束 动画。

  • 更新数据,刷新


删除


思路与添加雷同,且比之更简单


具体的思路和步骤,代码中都有一步步的注释,可自行查阅。


拖拽排序


这个拖拽排序,在iOS11之前的比较麻烦,都是靠自己计算,这里也简单说下思路:


iOS11.0之前的实现思路

  1. 在UICollectionView上添加一个长按的手势

  2. 在UICollectionView上面添加一个浮动隐藏的cell,便于拖拽

  3. 通过长按操作找到需要被拖动的cellA

  4. 通过拖动cellA找到找到和它交换位置的cellB

  5. 交换cellA和cellB的位置

  6. 替换数据源,把起始位置的数据模型删除,然后将起始位置的数据模型插入到拖拽位置


这种比较复杂的是结合位置判断需要交换的cell。但是在iOS11之后,UICollectionView新增了dragDelegatedropDelegate,用来实现拖拽排序的效果。


dragDelegate、dropDelegate


直接上代码:

collectionView.dragDelegate = self
collectionView.dropDelegate = self
collectionView.dragInteractionEnabled = true
collectionView.reorderingCadence = .immediate
collectionView.isSpringLoaded = true

  • dragInteractionEnabled 属性要设置为 true,才可以进行 drag 操作。此属性在 iPad 默认是 true,在 iPhone 默认是 false。

  • reorderingCadence 重排序节奏,可以调节集合视图重排序的响应性。

  • UICollectionViewReorderingCadenceImmediate 默认值。当开始移动的时候就立即回流集合视图布局,实时的重新排序。

  • UICollectionViewReorderingCadenceFast 快速移动,不会立即重新布局,只有在停止移动的时候才会重新布局

  • UICollectionViewReorderingCadenceSlow 停止移动再过一会儿才会开始回流,重新布局

  • isSpringLoaded 弹性加载效果,也可以使用代理方法:func collectionView(_ collectionView: UICollectionView, shouldSpringLoadItemAt indexPath: IndexPath, with context: UISpringLoadedInteractionContext) -> Bool。

需要实现UICollectionViewDropDelegateUICollectionViewDragDelegate协议方法。下面是常用的几个方法,按照调用的先后顺序说明一下:

/*
* 识别到拖动,一次拖动一个;若一次拖动多个,则需要选中多个
* 提供一个给定 indexPath 的可进行 drag 操作的 item
* NSItemProvider, 拖放处理时,携带数据的容器,通过对象初始化,该对象需满足 NSItemProviderWriting 协议
*/
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem]

/*
* 使用自定义预览,如果该方法没有实现或者返回nil,那么整个 cell 将用于预览
* UIDragPreviewParameters有2个属性:backgroundColor设置背景颜色;visiblePath设置视图的可见区域
* 笔者使用这个方法除去了拖拽过程中item的阴影
*/
func collectionView(_ collectionView: UICollectionView, dragPreviewParametersForItemAt indexPath: IndexPath) -> UIDragPreviewParameters?

/*
* 开始拖拽后,继续添加拖拽的任务,处理雷同`itemsForBeginning`方法
*/
func collectionView(_ collectionView: UICollectionView, itemsForAddingTo session: UIDragSession, at indexPath: IndexPath, point: CGPoint) -> [UIDragItem]

/*
* 拖拽开始,可自行处理
*/
func collectionView(_ collectionView: UICollectionView, dragSessionWillBegin session: UIDragSession)

/*
* 判断对应的 item 能否被执行drop会话,是否能放置
*/
func collectionView(_ collectionView: UICollectionView, canHandle session: UIDropSession) -> Bool

/*
* 处理拖动中放置的策略,此方法会 频繁调用,在此方法中应尽可能减少工作量。
* 四种分别:move移动;copy拷贝;forbidden禁止,即不能放置;cancel用户取消。
* 效果一般使用2种:.insertAtDestinationIndexPath 挤压移动;.insertIntoDestinationIndexPath 取代。
* 在某些情况下,目标索引路径可能为空(比如拖到一个没有cell的空白区域)你可以通过 session.locationInView 做你自己的命中测试
*/
func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal

/*
* 当drop会话进入到 collectionView 的坐标区域内就会调用
*/
func collectionView(_ collectionView: UICollectionView, dropSessionDidEnter session: UIDropSession)

/*
* 结束放置时的处理
* 如果该方法不做任何事,将会执行默认的动画
*/
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator)

/*
* 拖拽开始,可自行处理
*/
func collectionView(_ collectionView: UICollectionView, dragSessionDidEnd session: UIDragSession)

/*
* 当dropSession 完成时会被调用,不管结果如何。一般进行清理或刷新操作
*/
func collectionView(_ collectionView: UICollectionView, dropSessionDidEnd session: UIDropSession)

这里大概的拖拽动画就差不多了,代码中会有更详细的注释。


总结


将一个大功能拆分成一个个小模块,按部就班一步一步实现就不难了。这里只是中间囊括了各种小动画和刷新,设计好思路,不行就多试几次肯定可以。


代码自取:RCDragDropAnimation




若存在什么不对的地方,欢迎指正!


作者:云层之上
链接:https://juejin.cn/post/7246777949100933177
来源:稀土掘金
收起阅读 »

iOS 中如何精准还原 Sketch 线性渐变效果

iOS
背景 这样的渐变效果当然用切图是可以方便的实现,但切图不够灵活,而且会增加包大小 那如何用代码实现呢? 首先看下 iOS 中渐变的几个参数 colors startPoint endPoint locations colors 很好获取,其他三个参数怎么...
继续阅读 »

背景




这样的渐变效果当然用切图是可以方便的实现,但切图不够灵活,而且会增加包大小


那如何用代码实现呢?


首先看下 iOS 中渐变的几个参数



  • colors

  • startPoint

  • endPoint

  • locations


colors 很好获取,其他三个参数怎么办呢,似乎只能看图猜出个大概?


猜想


众所周知,sketch 有一键导出标注的插件 ,但是只能获取到 locations 信息

background-image: linear-gradient(-73deg, #361CE6 0%, #7DA7EB 50%, #96A4FF 100%);

并且这个 -73deg 对于 iOS 中的 startPoint``endPoint 来说还不太友好,需要经过一番转换。


这个时候心中有个想法💡,这个插件能导出这些信息应该是对 sketch 的源文件进行了解析,那么 sketch 的源文件是个什么样的文件呢,会不会像 .ipa 那样是个压缩包呢?


实践


file 命令可以查看文件的信息

file Test.sketch

输出如下结果

Test.sketch: Zip archive data, at least v2.0 to extract, compression method=deflate

可以看到这确实是一个压缩包

那就可以用 unzip 命令来解压一下

unzip Test.sketch -d ./temp

👀看看解压出了个啥呢?

.
├── document.json
├── meta.json
├── pages
│   └── 7832D4DC-A896-40BE-8F96-45850CE9FC53.json
├── previews
│   └── preview.png
└── user.json

有 json 文件!欣喜若狂😁!!!最终在 pages 这个目录下的 json 文件找到了想要的东西

{
"_class": "gradient",
"elipseLength": 0,
"from": "{1.1356274384397782, 0.99999999999999978}",
"gradientType": 0,
"to": "{-0.13533980933892775, -0.49069446290249097}",
"stops": [
{
"_class": "gradientStop",
"position": 0,
"color": {
"_class": "color",
"alpha": 1,
"blue": 0.903056932532269,
"green": 0.1092045213150163,
"red": 0.2098672970162421
}
},
{
"_class": "gradientStop",
"position": 0.4973543951952161,
"color": {
"_class": "color",
"alpha": 1,
"blue": 0.9204804793648098,
"green": 0.6532974892326747,
"red": 0.4919794574547816
}
},
{
"_class": "gradientStop",
"position": 1,
"color": {
"_class": "color",
"alpha": 1,
"blue": 1,
"green": 0.6418734727143864,
"red": 0.5896740424923781
}
}
]
}

结论

  • from 是 startPoint

  • to 是 endPoint

  • stops 中的 position 是 locations


Tips: UI 给的 sketch 文件可能图层太多,json 文件会非常大,打开比较卡,可以把图层复制到自己新建的 sketch 文件中再解压


作者:LittleYuuuuu
链接:https://juejin.cn/post/7222179242946641978
来源:稀土掘金
收起阅读 »

iOS17适配指南之UIContentUnavailableView(一)

iOS
介绍 新增视图,表示内容不可达,特别适用于没有数据时的占位视图。 UIContentUnavailableConfigurationUIContentUnavailableView 的配置参数,用于设置不可达时的占位内容。既可以使用 UIKit,又可以使用 S...
继续阅读 »

介绍


新增视图,表示内容不可达,特别适用于没有数据时的占位视图。


UIContentUnavailableConfiguration

  • UIContentUnavailableView 的配置参数,用于设置不可达时的占位内容。

  • 既可以使用 UIKit,又可以使用 SwiftUI。

  • 系统提供了 3 种配置,分别为empty()、loading()与search()。

  • UIViewController 增加了一个该类型的参数contentUnavailableConfiguration,用于设置view内容不可达时的占位内容。


案例一

import UIKit

class ViewController: UIViewController {
lazy var tableView: UITableView = {
let tableView = UITableView(frame: UIScreen.main.bounds, style: .plain)
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "abc")
return tableView
}()
// UIContentUnavailableView
lazy var unavailableView: UIContentUnavailableView = {
var config = UIContentUnavailableConfiguration.empty()
// 配置内容
config.text = "暂无数据"
config.textProperties.color = .red
config.secondaryText = "正在加载数据..."
config.image = UIImage(systemName: "exclamationmark.triangle")
config.imageProperties.tintColor = .red
var buttonConfig = UIButton.Configuration.filled()
buttonConfig.title = "加载数据"
config.button = buttonConfig
config.buttonProperties.primaryAction = UIAction(title: "") { _ in
self.loadData()
}
var backgroundConfig = UIBackgroundConfiguration.listPlainCell()
backgroundConfig.backgroundColor = .systemGray6
config.background = backgroundConfig
// 创建UIContentUnavailableView
let unavailableView = UIContentUnavailableView(configuration: config)
unavailableView.frame = UIScreen.main.bounds
return unavailableView
}()
var content: [String] = []

override func viewDidLoad() {
super.viewDidLoad()

view.addSubview(tableView)
if content.isEmpty {
view.addSubview(unavailableView)
}
}

func loadData() {
content = ["iPhone 12 mini", "iPhone 12", "iPhone 12 Pro", "iPhone 12 Pro Max",
"iPhone 13 mini", "iPhone 13", "iPhone 13 Pro", "iPhone 13 Pro Max",
"iPhone 14", "iPhone 14 Plus", "iPhone 14 Pro", "iPhone 14 Pro Max"]
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.tableView.reloadData()
self.unavailableView.removeFromSuperview()
}
}
}

// MARK: - UITableViewDataSource
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return content.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "abc", for: indexPath)
cell.textLabel?.text = content[indexPath.row]
cell.imageView?.image = UIImage(systemName: "iphone")
return cell
}
}

效果:


案列二

import UIKit

class ViewController: UIViewController {
lazy var emptyConfig: UIContentUnavailableConfiguration = {
var config = UIContentUnavailableConfiguration.empty()
config.text = "暂无数据"
config.image = UIImage(systemName: "exclamationmark.triangle")
return config
}()

override func viewDidLoad() {
super.viewDidLoad()

contentUnavailableConfiguration = emptyConfig
}

// MARK: - 更新UIContentUnavailableConfiguration
override func updateContentUnavailableConfiguration(using state: UIContentUnavailableConfigurationState) {
// 切换
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
let loadingConfig = UIContentUnavailableConfiguration.loading()
self.contentUnavailableConfiguration = loadingConfig
}
// 移除
DispatchQueue.main.asyncAfter(deadline: .now() + 6) {
self.contentUnavailableConfiguration = nil
self.view.backgroundColor = .systemTeal
}
}
}


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

收起阅读 »

你还在傻傻的打开页面输用户名和密码?来我教你实现自动化

web
背景 Hello~,大家好! 本文和各位分享一个有趣的事情! 我司主要的客户是银行,随着银行对信息安全越来越重视,我司积极配合银行防范信息安全,因此我司产品都从之前的外网开发引入了深信服的云桌面开发。由于笔者用的 Mac 电脑,云桌面 win7,天差地别的体验...
继续阅读 »

背景


Hello~,大家好! 本文和各位分享一个有趣的事情!


我司主要的客户是银行,随着银行对信息安全越来越重视,我司积极配合银行防范信息安全,因此我司产品都从之前的外网开发引入了深信服的云桌面开发。由于笔者用的 Mac 电脑,云桌面 win7,天差地别的体验我就先忍了😭,但是从 Mac 电脑桌面到进入云桌面,需要登录一些莫名其妙的 VPN、网址,这个过程是令人反感的、重复的、也是非常恶心的,每天都要这样、要等待很久......🤮



  1. 打开Mac上的xxxTrust并登录

  2. 打开Chrome

  3. 打开xxx网址

  4. 输入用户名、密码(不能自动填充那种)

  5. 点击登录

  6. 进入云桌面资源页面,点击一个资源,会自动调起 Mac 上安装的一个什么 VDIxxx 的软件

  7. 成功进入云桌面


我丢,各位来说说,这个过程是不是很恶心。作为一个技术人,我们不能忍受这种机械式的操作,我们要去做出改变。不能让自己一直做这些重复的恶心操作,于是我就想着 能不能像我打开 Mac 桌面的一个应用那样,中间的步骤自动完成,直到进入云桌面? 这就是笔者本文的主题。


实现自动化


其实笔者也没有一步到位——能不能像我打开 Mac 桌面的一个应用那样,中间的步骤自动完成,直到进入云桌面?。实现过程中,有一些新想法,接下来就分享一下我从开始有这个想法到实现的过程:


ConsoleSnippets


一开始: 我已经登录了 xxxTrust,笔者在浏览器已经打开了xxx网址,只是不想输密码,我就想起浏览器开发者工具 ConsoleSnippets,可以在里面写一些脚本,然后可以快捷执行:


image.png

打开控制台 -> 快捷键 command + p -> 输入!,选择执行哪个 Snippets -> 回车。看下效果:
1.gif


感觉还行是吧,那接着来,我们现在进入了资源管理的界面,接下来需要手动点击打开一个云桌面的资源,同样地,接着建一个 Snippets


image.png

看下效果:
1.gif


OK!成功进入,但是现在还需要我们手动去执行脚本,而且要执行两个。于是就有了新的想法。


篡改猴


能不能在对应的页面自动执行上面写好的脚本?


此时笔者想到了一个 Chrome 插件 —— 篡改猴,也叫油猴脚本,简单理解它的作用就是可以在你指定的网页中执行你写入的脚本。那就装一个呗!(Chrome商店需要🪜,可直接使用Edge浏览器)


image.png

打开管理面板,新建两个脚本:
image.png


编辑:
截屏2023-08-11 11.46.22.png


截屏2023-08-11 12.43.48.png

然后改一下设置,在 document-end 执行我们的脚本,此时可能还获取不到dom,因此使用 setTimeout
截屏2023-08-11 12.45.57.png
再来看下效果:


1.gif
鼠标一下没动哦!你以为这就完了?并没有,接着看。


自动化打开应用


能不能自动打开xxxTrustChrome,并自动打开登录的网址?


咱们一开始就说了,在访问网址之前,还需要链接VPN、打开浏览器,那就来吧!


截屏2023-08-11 13.21.17.png

在Mac上有两个东西可以完成自动操作:


截屏2023-08-11 13.22.20.png
通过自动操作配置出来的,也支持转变成快捷指令。我理解这俩应该是差不多的东西,来看下我们如何实现:


image.png
操作步骤:

  1. 打开 xxxTrust

  2. 通过 AppleScript,设置延时6秒,因为打开上面的 app 过后,有一个自动登录的过程,我们设置的长一点

  3. 打开 Chrome

  4. 打开登录网址


配置自动化操作和快捷指令都是可视化的,很容易上手,在此不过多介绍,感兴趣的掘友可自行探索哦!至于AppleScript,我只能告诉你是 GPT 教我这么写的。


最后一步就是把这个快捷指令发布到桌面:
截屏2023-08-11 13.33.00.png


看下最终的效果:
1.gif


这不,又多了几分钟摸鱼时间!😂


总结


笔者通过真实的一个场景,借助 Snippets篡改猴快捷指令自动操作 等工具实现自动化完成进入云桌面的一系列流程。笔者只实现了 Mac 的,windows 系统肯定也有类似的工具等待各位去探索(比如 python 脚本应该就能实现打开应用等操作)。


除此之外呢,还想表达一个观点就是——我们应该把那些机械式的活交给机械去做,比如在平时的开发中,总是CRUD?能不能高效CRUD?对吧!把这些对自己能力提升没有意义的工作,想办法用程序去实现了,岂不是美滋滋!


好了,本次分享就到此结束了,感谢阅读哦!


如果本文对你有一点点帮助,点个赞支持一下吧,你的每一个【】都是我创作的最大动力 ^_^


作者:Lvzl
来源:juejin.cn/post/7265744160694911028
收起阅读 »

代码中被植入了恶意删除操作,太狠了!

背景 在交接的代码中做手脚进行删库等操作,之前只是网上听说的段子,没想到上周还真遇到了,并且亲自参与帮忙解决。 事情是这样的,一老板接手了一套系统,可能因为双方在交接时出现了什么不愉快的事情,对方不提供源代码,只是把生产环境的服务器打了一个镜像给到对方。 对方...
继续阅读 »

背景


在交接的代码中做手脚进行删库等操作,之前只是网上听说的段子,没想到上周还真遇到了,并且亲自参与帮忙解决。


事情是这样的,一老板接手了一套系统,可能因为双方在交接时出现了什么不愉快的事情,对方不提供源代码,只是把生产环境的服务器打了一个镜像给到对方。


对方拿到镜像恢复之后,系统起来怎么也无法正常处理业务,于是就找到我帮忙看是什么原因。经过排查,原来交接的人在镜像中做了多处手脚,多处删除核心数据及jar包操作。下面来给大家细细分析排查过程。


排查过程


由于只提供了镜像文件,导致到底启动哪些服务都是问题。好在是Linux操作系统,镜像恢复之后,通过history命令可以查看曾经执行了哪些命令,能够找到都需要启动哪些服务。但服务启动之后,业务无法正常处理,很多业务都处于中间态。


原本系统是可以正常跑业务的,打个镜像之后再恢复就不可以了?这就奇怪了。于是对项目(jar包或war)文件进行排查,查看它们的修改时间。


在文件的修改时间上还真找到了一些问题,发现在打镜像的两个小时前,项目中一个多个项目底层依赖的jar包被修改过,另外还有两个class文件被修改过。


于是,就对它们进行了重点排查。首先反编译了那两个被修改过的class文件,在代码中找到了可疑的地方。


可疑代码


在两个被修改的类中都有上述代码。最开始没太留意这段代码,但直觉告诉我不太对,一个查询业务里面怎么可能出现删除操作呢?这太不符合常理了。


于是仔细阅读上述代码,发现上述红框中的代码无论何时执行最终的结果都是id=1。你是否看出来了?问题就出在三目表达式上,无论id是否为null,id被赋的值都是1。看到这里,也感慨对方是用心了。为了隐藏这个目的,前面写了那么多无用的代码。


但只有这个还不是什么问题,毕竟如果只是删除id为1的值,也只是删除了一条记录,影响范围应该有限。


紧接着反编译了被修改的jar包,依次去找上述删除方法的底层实现,看到如下代码:


删除操作


原来前面传递的id=1是为了配合where条件语句啊,当id=1被传递进来之后,就形成了where 1=1的条件语句。这个大家在mybatis中拼接多条件语句时经常用到。结果就是一旦执行了上述业务逻辑,就会触发删除T_QUART_DATA全表数据的操作。


T_QUART_DATA表中是用于存储触发定时任务的表达式,到这里也就明白了,为啥前面的业务跑不起来,全部是中间态了。因为一旦在业务逻辑中触发开关,把定时任务的cron表达式全部删除,十多个定时任务全部歇菜,业务也就跑步起来了。


找到了问题的根源,解决起来就不是啥事了,由于没有源代码,稍微费劲的是只能把原项目整个反编译出来,然后将改修改地方进行了修改。


又起波折


本以为到此问题已经解决完毕了,没想到第二天又出现问题了,项目又跑不起来了。经过多方排查和定位,感觉还有定时任务再进行暗箱操作。


于是通过Linux的crontab命令查看是否有定时任务在执行,执行crontab -ecrontab -l,还真看到有三个定时任务在执行。跟踪到定时任务执行的脚本中,而且明目张胆的起名deleteXXX:


删除脚本


而在具体的脚本中,有如下执行操作:


删除核心依赖包


这下找到为什么项目中第二天为啥跑不起来了,原来Linux的定时任务将核心依赖包删除了,并且还会去重启服务。


为了搞破坏,真是煞费苦心啊。还好的是这个jar包在前一天已经反编译出来了,也算有了备份。


小结


原本以为程序员在代码中进行删库操作或做一些其他小手脚只是网络上的段子,大多数人出于职业操守或个人品质是不会做的。没想到这还真遇到了,而且对方为了隐藏删除操作,还做了一些小伪装,真的是煞费苦心啊。如果有这样的能力和心思,用在写出更优秀的代码或系统上或许更好。


当然,不知道他们在交接的过程中到底发生了什么,竟然用这样的方式对待昔日合作的伙伴。之所以写这篇文章,是想让大家学习如何排查代码问题的过程,毕竟用到了不少知识点和技能,但这并不是教大家如何去做手脚。无论怎样,最起码的职业操守还是要有

作者:程序新视界
来源:juejin.cn/post/7140066341469290532
的,这点不接受反驳。

收起阅读 »

为什么App独立开发最好别做日记、记账

我在前几天发了条帖子说(新人)独立开发选择笔(日)记、记账、Todo主流三件套之一等于加速死亡。引发了一点点🤏🏻争议。刚好我在选择 app 方向的时候也深思过这个问题,所以我展开讲讲,分享一下我的想法。 认清自己的定位 在独立开发刚开始做,羽翼未丰的时候,你对...
继续阅读 »

我在前几天发了条帖子说(新人)独立开发选择笔(日)记、记账、Todo主流三件套之一等于加速死亡。引发了一点点🤏🏻争议。刚好我在选择 app 方向的时候也深思过这个问题,所以我展开讲讲,分享一下我的想法。


认清自己的定位


在独立开发刚开始做,羽翼未丰的时候,你对自己的定位就应该是一个游击队队员。这也是独立开发的天然优势,因为小,所以灵活机动。游击战的核心奥义是流动性和速决性。因为你没有一个根据地需要防守,你可以找到一个薄弱的地方进攻。因为这个地方薄弱,所以你集中优势兵力可以很快的决胜。因为你小,所以赚到一笔是一笔。因为你是在边缘薄弱的地方建立的优势,这个小细分市场的利润不足以吸引大部队来,你就有了自己的根据地。你想想红军的根据地都是在什么地方,都是在两省交界的山里。可没听说根据地在上海、北京的。


举个例子,一个小业务,几个月工作量,可以有10万利润。对于一个互联网团队是看不上的。一个完整的互联网团队,一个CEO,一个产品,一个设计,两个研发(前端+后端),一个测试,加上行政,加上公司的运营费用。这一套组合拳摊下来,为了组织的长期利益,短期的一个20万利润的项目他们是看不上的。但是如果你是独立开发,两个人搞两个月,赚10万,你做不做?或者说你能做吗?你能做的,只是赚的不多。但是公司这个事情就没法做。这就是大象踩不死蚂蚁的道理。有人觉得就算两个人两个月赚10万,一个月人均25000也不是什么大钱。确实不是很多的钱,但这也是独立开发普遍心态上的一个问题:你想要的太多。平民独立开发早期最关键的是找到一个持续赚钱的方式,你有实力让自己活下来,后期就可以慢慢发展。而不是一上来就觉得一年要赚100万,然后以一个游击队的姿态去攻打大城市。你要赚大钱,就要有对应的能力。但是很多起步的独立开发者并没有这样的能力,那就是一个不匹配的目标了。


局部创新在笔记领域不是决定性的优势


很多人觉得我做笔记,我有一个想法,我有一个痛点,市面上的笔记没有。我进行了一个局部创新,可能是更好看一点点的设计,或者一个特别的笔记格式。我有一个局部优势,我可以做,我入场。


第一个问题:某些品类的局部优势没有决定性。换一句话说,也许你的确解决了一个其他笔记没有的痛点,但是这个不能转化成你的产品整体优势。不足以转化成足够的收入。


如果你解决了一个普遍痛点,一个高商业价值的痛点,有什么理由现有的成熟笔记app不做呢?请问你作为一个独立开发者开发这个功能要做多久?你有没可能一年里没有任何风声开发一个杀手功能,出来的时候瞬间占领用户心智,用户蜂拥而来?在一个成熟品类里,这样很难的。诺曼底登陆不是游击队能做到的。你要是有这个能力和信心,你就不应该做独立开发,你应该成立公司高举高打。真实的现状是如果你的一个功能受欢迎,成熟的笔记团队花一两个月也就做出来了。


而且笔记类还有一个特点:他有数据积累。用户价值=新体验-旧体验-替换成本。假设你的新体验确实有优势,大于现有产品的旧体验,但是笔记类的替换成本是很高的。我已经在这里积累了好几年的数据,一个新的、个人的、上线没多久的数据积累类,替换成本是很高的。我需要同时愿意抛弃我的旧数据,还需要对你建立信任。 所以除非领头的app犯错误,否则后来的小app是没有机会超过他的。因为他有先发优势,他可以在看到你之后,完善自己,提高自己的旧体验。你不能静态的认为头部的成功的app会停在那里。


成熟品类的高入局门槛


笔记类app自 AppStore 开始有以来,就有人做了。这意味着在你的 app 上线之前,高付费意愿的用户已经付费购买过了。这意味着,如果你要获得存量用户,你需要超越现有品类的成功 app,你需要有更好的整体体验,你还需要有足够全面的运营能力。让这些买了其他 app 的用户愿意选择你。你做笔记 app,一上线,真正的笔记用户心里已经有一个对标的门槛了。本来你在农村盖个楼,是个一层土楼也可以,是个木楼也行,大家都认可是个楼。你在市中心盖一个土楼,大家就会觉得你简陋,功能不全了。用户心智里已经把这个品类的成熟应用当做了一个基线。


再说新的用户。新的用户如果要用一个笔记,上 AppStore 搜,上社交平台搜,肯定看到的大量都是成熟 app 的推荐。他们本来就是历经时代活下来的,自然是得到了用户的认可。既然存在了这么久,自然知道他们的人就多。所以你在自然新增流量上也很劣势。


所以你做一个成熟品类,意味着你要成功,你就必须有一个极具说服力的产品优势,加足够好的产品质量,加足够高效的运营。


我有两个理财项目,一个项目利润年化10%,一个项目利润年后1%,你选哪个?你既然都是在做新产品,为什么要选一个更难的赛道?是你觉得自己命中注定要开发一个笔记app,还是你贫瘠的想象力只想到了做笔记?你确定是在100个产品评估出的最好的方向是笔记?


对比一个新市场,比如最近很火的套壳的 gpt 应用。这是一个全新品类,意味着用户心里是没有对标品的。当用户下载你的 app 时,他不会期待你应该有怎样的功能,他没有对比的对象。这个品类里因为没有头部应用,大家搜索的时候也就看到什么下什么。你会有自然新增流量。因为大家都是差不多时间起步的,其他app不会领先很多,意味着你也有可能建立产品优势,至少你没什么太大的劣势,更有希望建立用户口碑。


长线和短线


投资理财主要有两个流派:长线和短线。长线就关注这个企业未来长期的发展,关注企业的价值。如果这个企业是低估的,就可以持有,因为他们评估出未来这个企业会成长。短期的波动对他们并不构成干扰。大概就是大家口中说的价值投资吧。还有一类是短线,他们不关注未来的情况,买入一只股票只关心这只股票下个月会不会涨,明天会不会涨。因此这个股票的已经100倍PE对他们也没影响,后面有人接盘就可以了。量化交易大多是这样的短线逻辑,于是他们有着高换手率。


长线和短线都是合理的策略。最大的问题是,你不能用抱着长线的心态做短线。这大概就是接盘股民的心态吧,他们持有一只股票的时候觉得这个企业未来会成长。但是短期市场遇冷跌了20%,看到很多人抛了,他们就觉得受不了了,于是割肉离场。买的时候听的是做长线人的意见,卖的时候是跟着做短线的人卖的。


把这个逻辑放到 app 上也是这样的。笔记 app 是一个长线价值,越到后面越值钱,做的越久产品优势越大,用户粘性越高。但是你抱着短线的心态进来做,做了4 个月,收入用户都没起色,你怀疑自己,团队开始有意见,于是你就放弃了。这就是问题所在了,大多数独立开发者没有耐心在一个赛道持续亏损做两年,不具备做长线的心态和财力。


所以我建议独立开发起步的时候多关注短线价值。就是你投入三五个月,能有起色的方向。做三五个月,要不就要赚钱,要不就要有用户口碑。一鸟在手胜过二鸟在林。等你解决了起步的时候生存问题,再考虑农村包围城市的问题。


谈谈番茄钟


也有人觉得番茄钟是独立开发的重灾区。虽然番茄钟似乎是一个红海了,但是我却觉得番茄钟反而是一个可以做的赛道。不知道大家有没有留意一下这个有这个功能的app,似乎万物都可以番茄钟。谜底时钟有番茄钟,滴答清单有番茄钟。番茄钟在设计上可以极简,可以是我的番茄,可以是小鸡,可以是面条,可以是像素。


image.png
IMG\_5978.png
IMG\_5979.png
IMG\_5980.png

那么为什么番茄钟可以呢?


番茄钟替换成本低。因为是及时性的工具类,历史积累的番茄时间统计并不太重要。在功能操作上也很简单,核心交互就是点击一下开始计时,25分钟后提醒你要休息5分钟。很容易可以完成一个基础任何。


设计差异化可以成为付费点,市场容量大。 因为交互很简单,所以做出突出的设计成本可以接受。因为这个品类里,设计可以成为卖点,设计又是一个各有所好的事情,因此天然会有很多不同的需求。某个番茄钟可能做的很好用,很好看,但是他不可能吃掉全部用户。一件短袖设计的再好看,也不可能让所有人都买单。


由此我们可以得出一个结论:番茄钟有短线价值。并且番茄钟引入养成和成就体系以后,有可能变成一个长线产品。如果你非要卷,去卷番茄钟吧。


最后


地上有两张钱,一张100,一张10块,你先捡哪张?我想结果是不言而喻的。独立开发者的一个生态位优势就是可以在一个小领域建立极高的人效比。这个小领域有两个可能:一个是这个领域太小太细分,只能容得下小团队做(高人效);这个领域是新的,还没人知道这里行不行,于是小成本的独立开发先做了(高灵活)。建议各位独立开发者如果要做死亡加速三件套的产品的话三思而后行。


作者:没故事的卓同学
来源:juejin.cn/post/7265967971162898487
收起阅读 »

万能的异步处理方案

异步处理通用方案 前言 良好的系统设计必须要做到开闭原则,随着业务的不断迭代更新,核心代码也会被不断改动,出错的概率也会大大增加。但是大部分增加的功能都是在扩展原有的功能,既要保证性能又要保证质量,我们往往都会使用异步线程池来处理,然而却增加了很多不确定性因素...
继续阅读 »

异步处理通用方案


前言


良好的系统设计必须要做到开闭原则,随着业务的不断迭代更新,核心代码也会被不断改动,出错的概率也会大大增加。但是大部分增加的功能都是在扩展原有的功能,既要保证性能又要保证质量,我们往往都会使用异步线程池来处理,然而却增加了很多不确定性因素。 由此我设计了一套通用的异步处理SDK,可以很轻松的实现各种异步处理


目的


通过异步处理不仅能够保证方法能够得到有效的执行而且不影响主流程


更重要的是各种兜底方法保证数据不丢失,从而达到最终一致性\color{red}最终一致性


优点


无侵入设计,独立数据库,独立定时任务,独立消息队列,独立人工执行界面(统一登录认证)


使用spring事务事件机制,即使异步策略解析失败也不会影响业务


如果你的方法正在运行事务,会等事务提交后再处理事件


就算事务提交了,异步策略解析失败了,我们还有兜底方案执行(除非数据库有问题,消息队列有问题,方法有bug)


组件


kafka 消息队列


xxl job 定时任务


mysql 数据库


spring 切面


vue 界面


设计模式


策略


模板方法


动态代理


流程图


image.png


数据库脚本


CREATE TABLE `async_req` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`application_name` varchar(100) NOT NULL DEFAULT '' COMMENT '应用名称',
`sign` varchar(50) NOT NULL DEFAULT '' COMMENT '方法签名',
`class_name` varchar(200) NOT NULL DEFAULT '' COMMENT '全路径类名称',
`method_name` varchar(100) NOT NULL DEFAULT '' COMMENT '方法名称',
`async_type` varchar(50) NOT NULL DEFAULT '' COMMENT '异步策略类型',
`exec_status` tinyint NOT NULL DEFAULT '0' COMMENT '执行状态 0:初始化 1:执行失败 2:执行成功',
`exec_count` int NOT NULL DEFAULT '0' COMMENT '执行次数',
`param_json` longtext COMMENT '请求参数',
`remark` varchar(200) NOT NULL DEFAULT '' COMMENT '业务描述',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_applocation_name` (`application_name`) USING BTREE,
KEY `idx_exec_status` (`exec_status`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='异步处理请求';

CREATE TABLE `async_log` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`async_id` bigint NOT NULL DEFAULT '0' COMMENT '异步请求ID',
`error_data` longtext COMMENT '执行错误信息',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_async_id` (`async_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='异步处理日志';

异步策略


image.png


安全级别


image.png


执行状态


image.png


流程图


image.png


image.png
image.png

apollo 配置


# 开关:默认关闭
scm.async.enabled=true

# 数据源 druid
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/fc_async?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true
spring.datasource.username=user
spring.datasource.password=xxxx
spring.datasource.filters=config
spring.datasource.connectionProperties=config.decrypt=true;config.decrypt.key=yyy
#静态地址
spring.resources.add-mappings=true
spring.resources.static-locations=classpath:/static/


# 以下配置都有默认值
# 核心线程数
async.executor.thread.corePoolSize=10
# 最大线程数
async.executor.thread.maxPoolSize=50
# 队列容量
async.executor.thread.queueCapacity=10000
# 活跃时间
async.executor.thread.keepAliveSeconds=600

# 执行成功是否删除记录:默认删除
scm.async.exec.deleted=true

# 自定义队列名称前缀:默认应用名称
scm.async.topic=应用名称

# 重试执行次数:默认5次
scm.async.exec.count=5

# 重试最大查询数量
scm.async.retry.limit=100

# 补偿最大查询数量
scm.async.comp.limit=100

用法


1,异步开关
scm.async.enabled=true

2,在需要异步执行的方法加注解 (必须是spring代理方法)
@AsyncExec(type = AsyncExecEnum.SAVE_ASYNC, remark = "数据字典")

3,人工处理地址
http://localhost:8004/async/index.html

注意


1,应用名称
spring.application.name

2,队列名称
${scm.async.topic:${spring.application.name}}_async_queue
自定义topic:scm.async.topic=xxx

3,自己业务要做幂等

4,一个应用公用一个队列
自产自消

5,定时任务
异步重试定时任务(2分钟重试一次,可配置重试次数)
异步补偿定时任务(一小时补偿一次,创建时间在一小时之前的)

效果展示


image.png


image.png


作者:三火哥
来源:juejin.cn/post/7266087843239084090
收起阅读 »

电话背调,我给他打了8分

前段时间招聘的一位开发,待了两三周,拿到了京东的offer,离职了。在离职的后一天,接到了他新公司的背调电话,几乎每项都给他打了8分。这个分数打的有点虚,单纯只是为了不影响他下家的入职。 离职之前,收到他在飞书上查看电话号码的消息,大概也猜到是在填写背调人信息...
继续阅读 »

前段时间招聘的一位开发,待了两三周,拿到了京东的offer,离职了。在离职的后一天,接到了他新公司的背调电话,几乎每项都给他打了8分。这个分数打的有点虚,单纯只是为了不影响他下家的入职。


离职之前,收到他在飞书上查看电话号码的消息,大概也猜到是在填写背调人信息,但自始至终,他也没打一声招呼,让给个好评。


离职最后一天,办完手续,没跟任何人打一个招呼,不知什么时候就消失了。


当初他刚入职一周时,其实大家都已经看出他在沟通上有很大问题,还想着如何对他有针对性的安排工作和调整,发挥他的长处,避免他的短处。但没想到这么快就离职了。在他提离职时,虽没过多挽留,但给了一些过来人的建议,很明显也听不进去。


站在旁观者的角度来看,他的职业生涯或即将面临到的事几乎能看得清清楚楚,但他有自己的坚持,别人是没办法的。


就着这事,聊聊最近对职场上关于沟通的一些思考:


第一,忌固执己见


职场中最怕遇到的一种人就是固执己见的人。大多数聪明人,在遇到固执己见的人时,基本上都会在三言两语之后停止与其争辩。因为,人一旦在自己的思维层次形成思维闭环,是很难被说服的。


而对于固执己见的人,失去的是新的思维、新的思想、纠错学习的机会,甚至是贵人的相助。试想一下,本来别人好像给你提建议,指出一条更好的路,结果换来的是争辩,是抬杠,聪明人都会敬而远之,然后默默地在旁边看着你掉坑里。


真正牛的人,基本上都是兼听则明,在获得各类信息、建议之后,综合分析,为己所用。


第二,不必说服,尊重就好


站在另外一个方面,如果一件事与己无关,别人有不同的意见,或者这事本身就是别人负责,那么尊重就好,不必强行说服对方,不必表现自己。


曾看到两个都很有想法的人,为一件事争论好几天,谁也无法说服谁。一方想用权力压另一方,另一方也不care,把简单的事情激化,急赤白脸的。


其实争论的核心只是展现形式不同而已,最终只是在争情绪、争控制感、争存在感而已,大可不必。


对于成年人,想说服谁都非常难的。而工作中的事,本身就没有对错,只有优劣,大多数时候试一下就知道了。


有句话说的非常好,“成年人的世界只做筛选,不做教育”。如果说还能做点什么,那就是潜移默化的影响别人而已。


第三,不懂的领域多听少说


如果自己对一个领域不懂,最好少发表意见,多虚心学习、请教即可。任正非辞退写《万言书》的员工的底层逻辑就是这个,不懂,不了解情况,还草率提建议,只是哗众取宠、浪费别人时间。


如果你不懂一个领域,没有丰富的背景知识和基础理论支撑,在与别人沟通的过程中,强行提建议,不仅露怯,还会惹人烦。即便是懂,也需要先听听别人的看法和视角解读。


站在另一个角度,如果一个不懂的人来挑战你的权威,质疑你的决定,笑一笑就好,不必与其争辩。


郭德纲的一段相声说的好:如果你跟火箭专家说,发射火箭得先抱一捆柴,然后用打火机把柴点着,发射火箭。如果火箭专家看你一眼,就算他输。


第四,没事多夸夸别人


在新公司,学到的最牛的一招就是夸人。之前大略知道夸人的效果,但没有太多的去实践。而在新公司,团队中的几个大佬,身体力行的在夸人。


当你完成一件事时,夸“XXX,真牛逼!”,当你解决一个问题时,夸“还得是XXX,不亏是这块的专家”。总之,每当别人有好的表现时,总是伴随着夸赞和正面响应。于是整个团队的氛围就非常好。


这事本身也不需要花费什么成本,就是随口一句话的事,而效果却非常棒。与懂得“人捧人,互相成就彼此,和气生财”的人相处,是一种非常愉悦的体验。


前两天看到一条视频,一位六七岁的小姑娘指派正在玩游戏的父亲去做饭,父亲答应了。她妈妈问:你是怎么做到的?她说:夸他呀。


看看,这么小的小孩儿都深谙的人性,我们很多成人却不懂,或不愿。曾经以为开玩笑很好,现在发现“夸”才是利器,同时一定不要开贬低性的玩笑。


其实,职场中还有很多基本的沟通规则,比如:分清无效沟通并且及时终止谈话、适当示弱、认真倾听,积极反馈、少用反问等等。


当你留意和思考这些成型的规则时,你会发现它们都是基于社会学和心理学的外在呈现。

作者:程序新视界
来源:juejin.cn/post/7265978883123298363
很有意思,也很有用。

收起阅读 »

AI欣赏-街头少女🔥🔥🔥

描述 💯💯💯 你更喜欢哪一位选手呢? 自己制作的一些AI绘画关键词,一是为了保存下来,二是分享给大家看看,可以收集一些意见和建议、想法等等。所以欢迎大家踊跃发言! 本次文章的描述主题如下:一个女孩在街头大笑,湿透的,辫子,写实风格的,电影级别的,HDR的。Lo...
继续阅读 »

描述


💯💯💯 你更喜欢哪一位选手呢?


自己制作的一些AI绘画关键词,一是为了保存下来,二是分享给大家看看,可以收集一些意见和建议、想法等等。所以欢迎大家踊跃发言!


本次文章的描述主题如下:一个女孩在街头大笑,湿透的,辫子,写实风格的,电影级别的,HDR的。

  • Lora:无

  • Embeddings:ng_deepnegative_v1_75t [1a3e]


Prompt



a young woman, street, laughing, ponytails, (hdr:1.3), (muted colors:1.2), dramatic, complex background, cinematic, filmic, (rutkowski, artstation:0.8), soaking wet,



Negative Prompt



(nsfw:2),Multiple people,easynegative,(worst quality:2),(low quality:2),lowres,(monochrome:1.4),(grayscale:1.4),big head,severed legs,short legs,missing legs,acnes,skin blemishes,age spot,backlight,(ugly:1.4),(duplicate:1.4),(morbid:1.2),(mutilated:1.2),mutated hands,(poorly drawn hands:1.4),blurry, (bad anatomy:1.4),(bad proportions:1.4),(disfigured:1.4),(unclear eyes:1.4),bad hands, bad tooth,missing fingers,extra digit,bad body,NG_DeepNegative_V1_75T,glans,EasyNegative:0.5,gross proportions.short arm,(missing arms:1.4),missing thighs,missing calf,mutation,duplicate,morbid,mutilated,poorly drawn cloth,strange finger,bad finger,(mutated hands and fingers:1.4),(text:1.4), bad-artist, bad_prompt_version2, bad-hands-5, bad-image-v2-39000,



基础配置




生成图的效果展示


选手1




选手2




选手3




选手4




选手5




选手6




选手7




选手8




选手9




投票


🌺 请开始诸位的投票吧!评论区见!!


作者:襄垣
链接:https://juejin.cn/post/7223267912727298103
来源:稀土掘金
收起阅读 »

iPhone两秒出图,目前已知的最快移动端Stable Diffusion模型来了

Stable Diffusion (SD)是当前最热门的文本到图像(text to image)生成扩散模型。尽管其强大的图像生成能力令人震撼,一个明显的不足是需要的计算资源巨大,推理速度很慢:以 SD-v1.5 为例,即使用半精度存储,其模型大小也有 1.7...
继续阅读 »

Stable Diffusion (SD)是当前最热门的文本到图像(text to image)生成扩散模型。尽管其强大的图像生成能力令人震撼,一个明显的不足是需要的计算资源巨大,推理速度很慢:以 SD-v1.5 为例,即使用半精度存储,其模型大小也有 1.7GB,近 10 亿参数,端上推理时间往往要接近 2min。


为了解决推理速度问题,学术界与业界已经开始对 SD 加速的研究,主要集中于两条路线:(1)减少推理步数,这条路线又可以分为两条子路线,一是通过提出更好的 noise scheduler 来减少步数,代表作是 DDIM [1],PNDM [2],DPM [3] 等;二是通过渐进式蒸馏(Progressive Distillation)来减少步数,代表作是 Progressive Distillation [4] 和 w-conditioning [5] 等。(2)工程技巧优化,代表作是 Qualcomm 通过 int8 量化 + 全栈式优化实现 SD-v1.5 在安卓手机上 15s 出图 [6],Google 通过端上 GPU 优化将 SD-v1.4 在三星手机上加速到 12s [7]。


尽管这些工作取得了长足的进步,但仍然不够快。


近日,Snap 研究院推出最新高性能 Stable Diffusion 模型,通过对网络结构、训练流程、损失函数全方位进行优化,在 iPhone 14 Pro 上实现 2 秒出图(512x512),且比 SD-v1.5 取得更好的 CLIP score。这是目前已知最快的端上 Stable Diffusion 模型!



论文地址:arxiv.org/pdf/2306.00…
Webpage: snap-research.github.io/SnapFusion


核心方法


Stable Diffusion 模型分为三部分:VAE encoder/decoder, text encoder, UNet,其中 UNet 无论是参数量还是计算量,都占绝对的大头,因此 SnapFusion 主要是对 UNet 进行优化。具体分为两部分:(1)UNet 结构上的优化:通过分析原有 UNet 的速度瓶颈,本文提出一套 UNet 结构自动评估、进化流程,得到了更为高效的 UNet 结构(称为 Efficient UNet)。(2)推理步数上的优化:众所周知,扩散模型在推理时是一个迭代的去噪过程,迭代的步数越多,生成图片的质量越高,但时间代价也随着迭代步数线性增加。为了减少步数并维持图片质量,我们提出一种 CFG-aware 蒸馏损失函数,在训练过程中显式考虑 CFG (Classifier-Free Guidance)的作用,这一损失函数被证明是提升 CLIP score 的关键!


下表是 SD-v1.5 与 SnapFusion 模型的概况对比,可见速度提升来源于 UNet 和 VAE decoder 两个部分,UNet 部分是大头。UNet 部分的改进有两方面,一是单次 latency 下降(1700ms -> 230ms,7.4x 加速),这是通过提出的 Efficient UNet 结构得到的;二是 Inference steps 降低(50 -> 8,6.25x 加速),这是通过提出的 CFG-aware Distillation 得到的。VAE decoder 的加速是通过结构化剪枝实现。




下面着重介绍 Efficient UNet 的设计和 CFG-aware Distillation 损失函数的设计。


(1)Efficient UNet


我们通过分析 UNet 中的 Cross-Attention 和 ResNet 模块,定位速度瓶颈在于 Cross-Attention 模块(尤其是第一个 Downsample 阶段的 Cross-Attention),如下图所示。这个问题的根源是因为 attention 模块的复杂度跟特征图的 spatial size 成平方关系,在第一个 Downsample 阶段,特征图的 spatial size 仍然较大,导致计算复杂度高。




为了优化 UNet 结构,我们提出一套 UNet 结构自动评估、进化流程:先对 UNet 进行鲁棒性训练(Robust Training),在训练中随机 drop 一些模块,以此来测试出每个模块对性能的真实影响,从而构建一个 “对 CLIP score 的影响 vs. latency” 的查找表;然后根据该查找表,优先去除对 CLIP score 影响不大同时又很耗时的模块。这一套流程是在线自动进行,完成之后,我们就得到了一个全新的 UNet 结构,称为 Efficient UNet。相比原版 UNet,实现 7.4x 加速且性能不降。


(2)CFG-aware Step Distillation


CFG(Classifier-Free Guidance)是 SD 推理阶段的必备技巧,可以大幅提升图片质量,非常关键!尽管已有工作对扩散模型进行步数蒸馏(Step Distillation)来加速 [4],但是它们没有在蒸馏训练中把 CFG 纳入优化目标,也就是说,蒸馏损失函数并不知道后面会用到 CFG。这一点根据我们的观察,在步数少的时候会严重影响 CLIP score。


为了解决这个问题,我们提出在计算蒸馏损失函数之前,先让 teacher 和 student 模型都进行 CFG,这样损失函数是在经过 CFG 之后的特征上计算,从而显式地考虑了不同 CFG scale 的影响。实验中我们发现,完全使用 CFG-aware Distillation 尽管可以提高 CLIP score, 但 FID 也明显变差。我们进而提出了一个随机采样方案来混合原来的 Step Distillation 损失函数和 CFG-aware Distillation 损失函数,实现了二者的优势共存,既显著提高了 CLIP score,同时 FID 也没有变差。这一步骤,实现进一步推理阶段加速 6.25 倍,实现总加速约 46 倍。


除了以上两个主要贡献,文中还有对 VAE decoder 的剪枝加速以及蒸馏流程上的精心设计,具体内容请参考论文。


实验结果


SnapFusion 对标 SD-v1.5 text to image 功能,目标是实现推理时间大幅缩减并维持图像质量不降,最能说明这一点的是下图:




该图是在 MS COCO’14 验证集上随机选取 30K caption-image pairs 测算 CLIP score 和 FID。CLIP score 衡量图片与文本的语义吻合程度,越大越好;FID 衡量生成图片与真实图片之间的分布距离(一般被认为是生成图片多样性的度量),越小越好。图中不同的点是使用不同的 CFG scale 获得,每一个 CFG scale 对应一个数据点。从图中可见,我们的方法(红线)可以达到跟 SD-v1.5(蓝线)同样的最低 FID,同时,我们方法的 CLIP score 更好。值得注意的是,SD-v1.5 需要 1.4min 生成一张图片,而 SnapFusion 仅需要 1.84s,这也是目前我们已知最快的移动端 Stable Diffusion 模型!


下面是一些 SnapFusion 生成的样本:




更多样本请参考文章附录。


除了这些主要结果,文中也展示了众多烧蚀分析(Ablation Study)实验,希望能为高效 SD 模型的研发提供参考经验:


(1)之前 Step Distillation 的工作通常采用渐进式方案 [4, 5],但我们发现,在 SD 模型上渐进式蒸馏并没有比直接蒸馏更有优势,且过程繁琐,因此我们在文中采用的是直接蒸馏方案。




(2)CFG 虽然可以大幅提升图像质量,但代价是推理成本翻倍。今年 CVPR’23 Award Candidate 的 On Distillation 一文 [5] 提出 w-conditioning,将 CFG 参数作为 UNet 的输入进行蒸馏(得到的模型叫做 w-conditioned UNet),从而在推理时省却 CFG 这一步,实现推理成本减半。但是我们发现,这样做其实会造成图片质量下降,CLIP score 降低(如下图中,四条 w-conditioned 线 CLIP score 均未超过 0.30, 劣于 SD-v1.5)。而我们的方法则可以减少步数,同时将 CLIP score 提高,得益于所提出的 CFG-aware 蒸馏损失函数!尤其值得主要的是,下图中绿线(w-conditioned, 16 steps)与橙线(Ours,8 steps)的推理代价是一样的,但明显橙线更优,说明我们的技术路线比 w-conditioning [5] 在蒸馏 CFG guided SD 模型上更为有效。




(3)既有 Step Distillation 的工作 [4, 5] 没有将原有的损失函数和蒸馏损失函数加在一起,熟悉图像分类知识蒸馏的朋友应该知道,这种设计直觉上来说是欠优的。于是我们提出把原有的损失函数加入到训练中,如下图所示,确实有效(小幅降低 FID)。




总结与未来工作


本文提出 SnapFusion,一种移动端高性能 Stable Diffusion 模型。SnapFusion 有两点核心贡献:(1)通过对现有 UNet 的逐层分析,定位速度瓶颈,提出一种新的高效 UNet 结构(Efficient UNet),可以等效替换原 Stable Diffusion 中的 UNet,实现 7.4x 加速;(2)对推理阶段的迭代步数进行优化,提出一种全新的步数蒸馏方案(CFG-aware Step Distillation),减少步数的同时可显著提升 CLIP score,实现 6.25x 加速。总体来说,SnapFusion 在 iPhone 14 Pro 上实现 2 秒内出图,这是目前已知最快的移动端 Stable Diffusion 模型。


未来工作:


1.SD 模型在多种图像生成场景中都可以使用,本文囿于时间,目前只关注了 text to image 这个核心任务,后期将跟进其他任务(如 inpainting,ControlNet 等等)。




  1. 本文主要关注速度上的提升,并未对模型存储进行优化。我们相信所提出的 Efficient UNet 仍然具备压缩的空间,结合其他的高性能优化方法(如剪枝,量化),有望缩小存储,并将时间降低到 1 秒以内,离端上实时 SD 更进一步。




参考文献


[1] Denoising Diffusion Implicit Models, ICLR’21


[2] Pseudo Numerical Methods for Diffusion Models on Manifolds, ICLR’22


[3] DPM-Solver: A Fast ODE Solver for Diffusion Probabilistic Model Sampling in Around 10 Steps, NeurIPS’22


[4] Progressive Distillation for Fast Sampling of Diffusion Models, ICLR’22


[5] On Distillation of Guided Diffusion Models, CVPR’23


[6] http://www.qualcomm.com/news/onq/20…


[7] Speed Is All You Need: On-Device Acceleration of Large Diffusion Models via GPU-Aware Optimizations, CVPR’23 Workshop


作者:机器之心
链接:https://juejin.cn/post/7244452476191850557
来源:稀土掘金
收起阅读 »

使用脚本更新 macOS 壁纸,让你每天看到不同的美景🖼️

在macOS系统中,我们可以轻松地更换桌面壁纸。但是,如果你每天都想要一张新的壁纸,手动更换就会变得十分繁琐。幸运的是,我们可以使用bash脚本和unsplash API自动更新壁纸。 步骤1:获取unsplash API密钥 首先,你需要注册一个unspla...
继续阅读 »

在macOS系统中,我们可以轻松地更换桌面壁纸。但是,如果你每天都想要一张新的壁纸,手动更换就会变得十分繁琐。幸运的是,我们可以使用bash脚本和unsplash API自动更新壁纸。


步骤1:获取unsplash API密钥


首先,你需要注册一个unsplash账户,并申请一个API密钥。这个API密钥将允许你通过编程方式访问unsplash图片库。


步骤2:编写bash脚本


创建一个新的文本文件,然后在其中添加以下代码:

#!/bin/bash

# set the unsplash API access key
access_key="YOUR_UNSPLASH_API_ACCESS_KEY"

# define the query to search for wallpaper images
query="nature"

# search for a random wallpaper image
result=$(/usr/bin/curl -s -H "Authorization: Client-ID $access_key" "https://api.unsplash.com/photos/random?query=$query")

# extract the image URL from the JSON response
image_url=$(echo "$result" | /opt/homebrew/bin/jq -r '.urls.full')

# download the image
/usr/bin/curl -s "$image_url" > ~/Pictures/wallpaper.jpg

# set the image as the desktop wallpaper
osascript -e "tell application \"Finder\" to set desktop picture to POSIX file \"$HOME/Pictures/wallpaper.jpg\""

这段代码会使用unsplash API搜索与“nature”相关的随机图片,并将其下载到“~/Pictures/wallpaper.jpg”文件中。然后,它会使用AppleScript将下载的图片设置为桌面壁纸。


步骤3:运行bash脚本


将文件保存为“update-wallpaper.sh”,然后打开终端并导航到该文件所在的目录。运行以下命令以使脚本可执行:

chmod +x update-wallpaper.sh

现在,你可以通过在终端中输入以下命令来运行脚本:

./update-wallpaper.sh

步骤4:设置定时任务

脚本依赖:curl、jq、bash、unsplash,使用 which 获取路径,然后替换脚本里的curl和jq。

which curl
which jq

你可以将该脚本设置为定时任务,以便每天自动更新壁纸。打开“终端”并输入以下命令以编辑cron定时任务:

crontab -e

然后,添加以下行:

0 9 * * * /path/to/update-wallpaper.sh

这将在每天上午9点运行该脚本。

现在,你可以坐下来,放松一下,让你的macOS自动更新壁纸。享受吧!

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

收起阅读 »

完全免费白嫖 GPT-4 的终极方案!

GPT-4 目前是世界上最强的多模态大模型,能力甩 GPT-3.5 好几条街。 大家都希望早日用上 GPT-4,不过目前体验 GPT-4 的渠道非常有限,要么就是开通 ChatGPT 尊贵的 Plus 会员,即使你开了会员,也是有限制的,每 3 小时只能发送 ...
继续阅读 »


GPT-4 目前是世界上最强的多模态大模型,能力甩 GPT-3.5 好几条街。


大家都希望早日用上 GPT-4,不过目前体验 GPT-4 的渠道非常有限,要么就是开通 ChatGPT 尊贵的 Plus 会员,即使你开了会员,也是有限制的,每 3 小时只能发送 25 条消息。。。


要么就去 OpenAI 官网申请 GPT-4 的 API,但是目前申请到 API 的小伙伴非常少,你以为申请到 API 就可以用了吗?GPT-4 的 API 价格超级无敌贵,是 GPT-3.5 价格的 30 倍,你敢用吗?😄


然而,但是,既然我写了这篇文章,肯定是要告诉那一个惊天大幂幂的!


现在完全免费白嫖 GPT-4 的机会来了,不仅可以白嫖,还可以直接作为 API 来调用!


不仅能够作为 API 调用,我还接入了公众号给大家白嫖,你说气人不气人?



下面言归正传,开始手把手教大家如何免费白嫖 GPT-4


gpt4free-ts 介绍


GPT4Free 大家应该都知道吧?它上线几周就在 GitHub 上揽收了接近 4w 的 Star。原因就在于其提供了对 GPT-4 及 GPT-3.5 免费且几乎无限制的访问。该项目通过对各种调用了 OpenAI API 网站的第三方 API 进行逆向工程,达到使任何人都可以免费访问该流行 AI 模型的目的。


这就相当于什么?假设地主家有一个粮仓,你往他家的粮仓偷偷插了一根管子,不停地向外抽米,他全然不知,所以你也不用交钱,一切费用由地主承担


现在接入 GPT-4 的第三方网站就相当于那个地主,懂了吧?


但是这个项目并没有封装 API,而且目前也不太能用了。


作为开发者,我们想要的肯定是 API 啊!这就要提到今天的主角了:gpt4free-ts


这个项目是用 TypeScript 写的,相当于 GPT4Free 的 TypeScript 版本,但是更方便部署,而且封装了 API,简直就是开发者的福音,就他了!


这个项目向多个地主家的粮仓插了管子,其中最强大的地主就是 forefront.ai,这个地主家的粮仓里就包含了 GPT-4 这个香饽饽,而且还有 Claude,就嫖他了!


除了 forefront 之外,它接的粮仓还挺多的。。



大批量注册临时邮箱


forefront 的 GPT-4 模型是有限制的,每个账号每 3 小时内只能发送 5 条消息


所以接下来需要用到一个非常神奇的服务叫 RapidAPI你可以通过这个 API 来获取无穷无尽的临时邮箱,然后再用这些无穷无尽的临时邮箱去注册无穷无尽的 forefront 账号。


这么一说,你是不是就悟了?哈哈哈


首先你需要在这里注册一个账号并登录:rapidapi.com/calvinlovel…


然后需要在 Pricing 页面开启订阅:



一般情况下订阅免费套餐即可,一天可以调用 100 次。


如果你有更高的需求,可以考虑订阅更高级的套餐(比如你的用户数量特别多)。


订阅完了之后,你就能看到 API Key 了。这个 Key 我们后面会用到。



Sealos 云操作系统介绍


单机操作系统大家应该都知道吧?Windows、macOS、Linux 这些都属于单机操作系统,为什么叫单机操作系统呢?因为他的内存啊,CPU 啊,都在一台机器上,你不可能用其他机器的内存和 CPU。


那么什么是云操作系统呢?就是把一群机器的 CPU 和内存看成一个整体,然后给用户提供一个交互界面,用户可以通过这个交互界面来操作所有的资源。


懂 K8s 的玩家可能要说了:这个我懂,K8s 就可以!


如果我们的目标愿景是一个云操作系统,K8s 充其量只能是这个云操作系统的内核,就像 Linux 内核一样。完整的云操作系统需要一个像 Windows 和 Ubuntu 操作系统那样的交互界面,也就是操作系统发行版


对于云操作系统来说,Sealos 就是那个发行版。



链接:cloud.sealos.io




有人可能会把云操作系统理解成“Web 界面”,但其实不是,Sealos 云操作系统完全是类似于 Windows 和 macOS 桌面的那种逻辑,并不是 Web 界面。我只需要点几下鼠标,一个应用就装好了,老夫并不知道什么容器什么 K8s。


数据库也一样,小鼠标一点,一个分布式数据库就装好了。


我知道,这时候云原生玩家要坐不住了,您别着急,看到桌面上的终端了没?



终端只是这个云操作系统中的一个 App 而已。同理,容器管理界面仍然可以作为云操作系统的 App,我管你是 Kubernetes Dashboard、Rancher、KubeSphere 还是 Kuboard,都可以作为 App 装在这个云操作系统中。这时候对于云原生专家而言,仍然可以命令行咔咔秀操作,也可以通过各种管理界面来管理容器。


云操作系统嘛,就是要什么人都能用才行,不管你是什么角色,都能在这个操作系统里找到你想要的 App 去完成你的使命


安装 gpt4free-ts


接下来才是这篇文章的重头戏。


我要教大家如何在 Sealos 中点几下鼠标就能安装一个 gpt4free-ts 集群


没错,就是 gpt4free-ts 集群。


什么叫集群?就是说我要运行一群 gpt4free-ts 实例,然后前面加一个负载均衡作为对外的 API 入口。


下面的步骤非常简单,楼下的老奶奶都会,是真的,当时我就在楼下看她操作


首先进入 Sealos 云操作系统的界面:**cloud.sealos.io**。


然后打开桌面上的应用管理 App:



点击「新建应用」:



在启动参数中,按照以下方式进行设置:

1.应用名称随便写,比如 gpt4free。
2.镜像名称是:xiangsx/gpt4free-ts:latest
CPU 和内存需要根据应用的实际情况来填写。这个应用运行之后默认会启动两个 Chrome 浏览器来模拟登录 forefront,每次对话会从里面取一个账号来使用,次数用完了会自动注册新账号(因为每个账号每 3 小时只能发送 5 条信息)。我们可以通过环境变量来修改启动的浏览器数量,所以需要根据你的浏览器数量来确定 CPU 和内存。 我自己把浏览器数量设置为 3,所以需要的内存和 CPU 比较多(后面会告诉你怎么设置环境变量)。
3.实例数根据自己的实际需求填写,我需要接入公众号,粉丝比较多,一个实例才 3 个账号(因为我一个实例跑了 3 个浏览器),根本不够用,所以我开了 3 个实例。
4.容器暴露端口指定为 3000。
5.打开外网访问。



继续往下,展开高级设置,点击「编辑环境变量」:



填入以下环境变量:

rapid_api_key=<rapid_api_key>
DEBUG=0
POOL_SIZE=3


⚠️注意:请将 <rapid_api_key> 替换为你自己的 key。



其中 POOL_SIZE 就是浏览器数量,每个浏览器会登录一个 forefront 账号。你可以根据自己的需要调整浏览器数量,并根据浏览器数量调整 CPU 和内存。如果你不知道怎么调整合适,建议无脑跟着本文操作。



继续,点击「新增存储卷」:



容量只需 1G,挂载路径设置为 /usr/src/app/run




这个存储的作用是为了保存已登录的账号。已经注册的账号 3 个小时以后还可以重新使用,不用再浪费邮箱去注册新账号。



最终点击右上角的「部署应用」,即可完成部署:



最终要等待所有的实例都处于 Running 状态,才算是启动成功了。



点击右边的复制按钮,便可复制 API 的外网地址:



我们来测一下这个 API:



完美!打完收工!


接入微信公众号


什么?你想将这个 API 接入自己的公众号?换个形式吧!直接来看直播吧,我们会通过直播活动手把手教你如何将 GPT-4 免费接入公众号、网页等各种前端。


活动链接:forum.laf.run/d/684


当然,直播过程不会直接教你如何接入公众号,而是“授你🫵以渔”,告诉你如何使用 Laf 来通过各种姿势调用这个 API,最终你也接入公众号也罢,网页前端也罢,那都不是事儿~

作者:米开朗基杨
链接:https://juejin.cn/post/7241790368949190693
来源:稀土掘金
收起阅读 »

雷军写的代码上热搜了!

就在昨天,「雷军写的代码」相关话题先后上了一波热搜和热榜。 出于好奇,第一时间点进去围观了一波。 原来雷总马上要在8月14日举办他的2023年度演讲了,并且也放出了对应的演讲海报。 这个海报可以说暗藏玄机,放大后仔细一看,好家伙,密密麻麻全都是代码这是...
继续阅读 »

就在昨天,「雷军写的代码」相关话题先后上了一波热搜和热榜。




出于好奇,第一时间点进去围观了一波。



原来雷总马上要在8月14日举办他的2023年度演讲了,并且也放出了对应的演讲海报。



这个海报可以说暗藏玄机,放大后仔细一看,好家伙,密密麻麻全都是代码这是!



看一下代码细节,都是类似MOVJMPPUSHLOOP等这些指令。


这不就是自己当年上学时学得瑟瑟发抖痛哭涕零的汇编语言嘛。。



这一瞬间就让我想起了当年微博上的这张图:



早在十几年前,微博上就曾流传过雷军早年所写的一段完整的汇编代码。



当时雷军也转发过,并表示这个程序的第一个版本是他1989年写的,怀念当初写程序的快乐时光。



在网上经常会看到关于雷军代码水平到底如何的讨论帖子。


可以说,看看雷军早年写的这段代码相信心里基本就有答案了。


在早年那个机器硬件水平和性能都十分受限的年代,为了满足某些需求,可以说对编码的程序员提出了非常苛刻的要求。


开发者往往只能使用非常底层的编程语言去实现某些程序,同时还需要把代码优化做到极致,这本就非常考验程序员的基本功和编程底子。


各方面信息都显示,作为一个程序员来说,雷军不仅仅是合格,说他是非常厉害的大神那也丝毫没有毛病。


就像这次热搜话题下稚晖君大佬的一篇动态所言,雷军作为老一代程序员的代表和创业楷模,确实值得敬佩。



文章的最后,我们也找到了当年雷军所写的这段汇编代码的完整版,这里也分享给大家。


咳咳,前方高能!!!


作者:CodeSheep
链接:https://juejin.cn/post/7265679390242537512
来源:稀土掘金
收起阅读 »

吐槽大会,来瞧瞧资深老前端写的垃圾代码

忍无可忍,不吐不快。 本期不写技术文章,单纯来吐槽下公司项目里的奇葩代码,还都是一些资深老前端写的,希望各位对号入座。 知道了什么是烂代码,才能写出好代码。 别说什么代码和人有一个能跑就行的话,玩笑归玩笑。 人都有菜的时候,写出垃圾代码无可厚非,但是工作几年了...
继续阅读 »

忍无可忍,不吐不快。


本期不写技术文章,单纯来吐槽下公司项目里的奇葩代码,还都是一些资深老前端写的,希望各位对号入座。


知道了什么是烂代码,才能写出好代码。


别说什么代码和人有一个能跑就行的话,玩笑归玩笑。


人都有菜的时候,写出垃圾代码无可厚非,但是工作几年了还是写垃圾代码,有点说不过去。


我认为的垃圾代码,不是你写不出深度优先、广度优先的算法,也不是你写不出发布订阅、策略模式的 if else


我认为垃圾代码都有一个共性:看了反胃。就是你看了天然的不舒服,看了就头疼的代码,比如排版极丑,没有空格、没有换行、没有缩进。

优秀的代码就是简洁清晰,不能用策略模式优化 if else 没关系,多几行代码影响不大,你只要清晰,就是好代码。


有了差代码、好代码的基本标准,那我们也废话不多说,来盘盘这些资深前端的代码,到底差在哪里。


---------------------------------------------更新------------------------------------------------


集中回答一下评论区的问题:


1、项目是原来某大厂的项目,是当时的外包团队写的,整体的代码是还行的。eslintcommitlint 之类的也是有的,只不过这些都是可以禁用的,作用全靠个人自觉。


2、后来项目被我司收购,原来的人也转为我司员工,去支撑其他项目了。这个项目代码也就换成我们这批新来的维护了。很多代码是三几年前留下的确实没错,谁年轻的时候没写过烂代码,这是可以理解的。只不过同为三年前的代码,为啥有人写的简单清晰,拆分清楚,有的就这么脏乱差呢?


3、文中提到的很多代码其实是最近一年写的,写这些功能的前端都是六七年经验的,当时因为项目团队刚组建,没有前端,抽调这些前端过来支援,写的东西真的非常非常不规范,这是让人难以理解的,他们在原来的项目组(核心产品,前端基建和规范都很厉害)也是这么写代码的吗?


4、关于团队规范,现在的团队是刚成立没多久的,都是些年轻人,领导也不是专业前端,基本属于无前端 leader 的状态,我倒是很想推规范,但是我不在其位,不好去推行这些东西,提了建议,领导是希望你简单写写业务就行(说白了我不是 leader 说啥都没用)。而且我们这些年轻前端大家都默认打开 eslint,自觉性都很高,所以暂时都安于现状,代码 review 做过几次,都没太大的毛病,也就没继续做了。


5、评论区还有人说想看看我写的代码,我前面文章有,我有几个开源项目,感兴趣可以自己去看。项目上的代码因为涉及公司机密,没法展示。


6、本文就是篇吐槽,也不针对任何人,要是有人看了不舒服,没错,说的就是你。要是没事,大家看了乐呵乐呵就行。轻喷~


文件命名千奇百怪


同一个功能,都是他写的,三个文件夹三种奇葩命名法,最重要的是第三个,有人能知道它这个文件夹是什么功能吗?这完全不知道写的什么玩意儿,每次我来看代码都得重新理一遍逻辑。




组件职责不清


还是刚才那个组件,这个 components 文件夹应该是放各个组件的,但是他又在底下放一个 RecordDialog.jsx 作为 components 的根组件,那 components 作为文件名有什么意义呢?




条件渲染逻辑置于底层


这里其实他写了几个渲染函数,根据客户和需求的不同条件性地渲染不同内容,但是判断条件应该放在函数外,和函数调用放在一起,根据不同条件调用不同渲染函数,而不是函数内就写条件,这里就导致逻辑内置,过于分散,不利于维护。违反了我们常说的高内聚、低耦合的编程理念。




滥用、乱用 TS


项目是三四年前的项目了,主要是类组件,资深前端们是属于内部借调来支援维护的,发现项目连个 TS 都没有,不像话,赶紧把 TS 配上。配上了,写了个 tsx 后缀,然后全是无类型或者 anyscript。甚至于完全忽视 TSESlint 的代码检查,代码里留下一堆红色报错。忍无可忍,我自己加了类型。




留下大量无用注释代码和报错代码


感受下这个注释代码量,我每次做需求都删,但是几十个工程,真的删不过来。






丑陋的、隐患的、无效的、混乱的 css


丑陋的:没有空格,没有换行,没有缩进


隐患的:大量覆盖组件库原有样式的 css 代码,不写类名实现 namespace


无效的:大量注释的 css 代码,各种写了类名不写样式的垃圾类名


混乱的:从全局、局部、其他组件各种引入 css 文件,导致样式代码过于耦合




一个文件 6 个槽点


槽点1:代码的空行格式混乱,影响代码阅读


槽点2:空函数,写了函数名不写函数体,但是还调用了!


槽点3:函数参数过多不优化


槽点4:链式调用过长,属性可能存在 undefined 或者 null 的情况,代码容易崩,导致线上白屏


槽点5:双循环嵌套,影响性能,可以替换为 reduce 处理


槽点6:参数、变量、函数的命名随意而不语义化,完全不知道有何作用,且不使用小驼峰命名




变态的链式取值和赋值


都懒得说了,各位观众自己看吧。




代码拆分不合理或者不拆分导致代码行数超标


能写出这么多行数的代码的绝对是人才。


尽管说后来维护的人加了一些代码,但是其初始代码最起码也有近 2000 行。




这是某个功能的数据流文件,使用 Dva 维护本身代码量就比 Redux 少很多了,还是能一个文件写出这么多。导致到现在根本不敢拆出去,没人敢动。




杂七杂八的无用 js、md、txt 文件


在我治理之前,充斥着非常多的无用的、散乱的 md 文档,只有一个函数却没有调用的 js 文件,还有一堆测试用的 html 文件。


实在受不了干脆建个文件夹放一块,看起来也要舒服多了。




less、scss 混用


这是最奇葩的。




特殊变量重命名


这是真大佬,整个项目基建都是他搭的。写的东西也非常牛,bug 很少。但是大佬有一点个人癖好,例如喜欢给 window 重命名为 G。这虽然算不上大缺点,但真心不建议大家这么做,window 是特殊意义变量,还请别重命名。

const G = window;
const doc = G.document;

混乱的 import


规范的 import 顺序,应该是框架、组件等第三方库优先,其次是内部的组件库、包等,然后是一些工具函数,辅助函数等文件,其次再是样式文件。乱七八糟的引入顺序看着都烦,还有这个奇葩的引入方式,直接去 lib 文件夹下引入组件,也是够奇葩了。


总而言之,css 文件一般是最后引入,不能阻塞 js 的优先引入。




写在最后


就先吐槽这么多吧,这些都是平时开发过程中容易犯的错误。希望大家引以为戒,不然小心被刀。


要想保持一个好的编码习惯,写代码的时候就得时刻告诉自己,你的代码后面是会有人来看的,不想被骂就写干净点。


我觉得什么算法、设计模式都是次要的,代码清晰,数据流向清晰,变量名起好,基本 80% 不会太差。


不过说这么多,成事在人。


不过我写了一篇讲解如何写出简洁清晰代码的文章,我看不仅是一年前端需要学习,六七年的老前端也需要看看。

作者:北岛贰
链接:https://juejin.cn/post/7265505732158472249
来源:稀土掘金
收起阅读 »

接口测试神器:ApiKit

想给大家分享一款技术人必备的接口测试神器:ApiKit,应该是我目前用过,算得上良心的接口工具1.背景 作为互联网行业技术从业者,接口调试是必不可少的一项技能,通常我们都会选择使用 Postman 这类工具来进行接口调试,在接口调试方面 Postman 做的确...
继续阅读 »

想给大家分享一款技术人必备的接口测试神器:ApiKit,应该是我目前用过,算得上良心的接口工具

1.背景


作为互联网行业技术从业者,接口调试是必不可少的一项技能,通常我们都会选择使用 Postman 这类工具来进行接口调试,在接口调试方面 Postman 做的确实非常出色。


但是在整个软件开发过程中,接口调试只是其中的一部分,还有很多事情 Postman 是无法完成的,或者无法高效完成,比如:接口文档定义、Mock 数据、接口自动化测试等等。


今天给大家推荐的一款神器: ApiKit=API 管理 + Mock + 自动化测试 + 异常监控 + 团队协作

1.1聊一聊接口管理的现状


对于接口管理的现状来说,目前行业大部分采取的解决方案有如下几种:

1.使用 **Swagger**管理接口文档。 

2. 使用 Postman 调试接口。

3.使用 **RAP或Easy Mock**来进行 **Mock**数据。 

4. 使用 JMeter 做接口自动化测试。


而上述的接口管理手段,咋一看,貌似没有什么问题,但仔细分析,不难发现,当中存在的问题还真不少,比如要维护不同工具,并且这些工具之间数据一致性非常困难、非常低效。这里不仅仅是工作量的问题,更大的问题是多个系统之间数据不一致,导致协作低效,频繁出问题,开发人员、测试人员工作起来也痛苦不堪。


设想一下这样的一个协作流程(官方示例):



1. 开发人员在Swagger定义好文档后,接口调试的时候还需要去 Postman 再定义一遍。 2. 前端开发Mock 数据的时候又要去RAPEasy Mock定义一遍,手动设置好 Mock 规则。 3. 测试人员需要去 JMeter定义一遍。 4. 前端根据 RAPEasy Mock定义 Mock 出来的数据开发完,后端根据 Swagger定义的接口文档开发完,各自测试测试通过了,本以为可以马上上线,结果一对接发现各种问题:原来开发过程中接口变更,只修改了 Swagger,但是没有及时同步修改 RAPEasy Mock。 5. 同样,测试在 JMeter 写好的测试用例,真正运行的时候也会发现各种不一致。 6. 时间久了,各种不一致会越来越严重。

ApiKit介绍

官方对ApiKit定位是,API 管理 + Mock + 自动化测试 + 异常监控 + 团队协作 

结合 API 设计、文档管理、自动化测试、监控、研发管理和团队协作的一站式 API 生产平台,从个人开发者到跨国企业用户,Apikit 帮助全球超过50万开发者和10万家企业更快、更好且更安全地开发和使用 API 

概括来讲,ApiKit 常用功能分为五类: 

1.智能且强大的 Mock 前端团队可以在 API 还没开发完成的情况下,借助 Mock API 实现预对接,加速开发进程。测试团队可以通过 Mock API 解耦不必要的系统,完成集成测试 

2.快速生成和管理所有 API 文档 无论您使用什么语言开发,Apikit 都可以帮您统一规范地管理起来,并提供强大的文档管理、协作、测试、分享功能 

3. 零代码自动化测试 Apikit 提供了 API 测试功能,支持自动生成测试数据,能够通过Javascript 对请求报文、返回结果等进行加解密、签名等处理;提供强大、易用的企业级 API 自动化测试解决方案,5分钟快速上手,提高 95% 以上回归测试效率,人人皆可使用的“零代码”自动化测试平台; 

4. 领先的 API 团队协作功能 无论您使用什么语言开发,Apikit 都可以帮您统一规范地管理起来,并提供强大的文档管理、协作、测试、分享功能 

5.还有更多的 Devops 功能 API 异常监控,对接CI/CD、DevOps 平台,支持主流IM ,也可通过自由拓展。 

ApiKit 小试牛刀 

接下来,带着大家,简单体验一下ApiKit的使用。

Apikit 有三种客户端,你可以依据自己的情况选择。三种客户端的数据是共用的,因此你可以随时切换不同的客户端。

我们推荐使用新推出的 Apikit PC 客户端,PC端拥有线上产品所有的功能,并且针对本地测试、自动化测试以及使用体验等方面进行了强化,可以提供最佳的使用感受。 



我们建议对本地测试有需求的用户使用PC端,可满足更多本地测试需求。



发起 API 测试


进入 API 文档详情页,点击上方 测试 标签,进入 API 测试页,系统会根据API文档自动生成测试界面并且填充测试数据。




填写请求参数


首先填写好请求参数。



请求头部


您可以输入或导入请求头部。批量导入的数据格式为 key : value ,一行一条header信息,如:

Connection: keep-alive
Content-Encoding: gzip
Content-Type: application/json
Date: Mon, 30 Dec 2019 20:49:45 GMT

请****求体


请求体提供了五种类型:

  1. Form-data(表单)

  2. JSON

  3. XML

  4. Raw(自定义文本类型数据)

  5. Binary(字节流、文件参数)


产品中提供了的 JSON 和 XML 编辑器,当您已经在 API 文档中定义好 API 的请求数据结构时,只需要在测试界面填写各个字段的值,系统会自动转换为相应的 JSON 和 XML 结构的请求数据。


Query 参数


Query参数指的是地址栏中跟在问号?后面的参数,如以下地址中的 user_name 参数:

/user/login?user_name=jackliu

批量导入的数据格式为 ?key=value ,通过&分隔多个参数,如:

api.eolinker.com/user/login?user_name=jackliu&user_password=hello

REST参数

REST参数指的是地址栏被斜杠/分隔的参数,如以下地址中的user_name、user_password参数。

/user/login/{user_name}/{user_password}

注意,只需要在URL中使用 {} 将REST参数括起来,下方的请求参数名中不需要使用 {} 。


处理脚本


脚本分为 前置脚本后置脚本 两种,分别对应 API 请求前 和 返回数据后 的两个阶段。您可以通过编写 Javascript 代码,在 API 前置脚本中改变请求参数,或者是在 API 后置脚本中改变返回结果。


脚本常用于以下几种情况:

1.API 请求前对请求参数进行复制、加解密等操作,比如进行Body进行整体签名 

2. API 返回结果后对结果进行解密等


发起的API请求会依次经过以下流程。其中如果您没有编写相应的API脚本,则会略过API脚本处理阶段。



管****理 Cookie


当您测试需要 Cookie 的 API 时,可以先进行一次 API 登录或者在 Cookie 管理里添加所需的 Cookie 信息,系统会自动将 Cookie 储存起来,下次测试其他相同域名的 API 时会自动传递 Cookie 请求参数。




查看测试结果


填写好请求参数后,点击测试按钮即可得到测试报告,报告包括以下内容:

1.返回头部 

2. 返回内容 

3.实际请求头部 

4. 实际请求内容 

5.请求时间分析




快速生成mock


在高级mock页面,选择添加为mock,可快速生成mock。



将测试用例请求参数和返回参数自动带到mock的请求报文和响应报文中。



ApiKit 更多特性


新建 API 文档



团队协作,API分享



高级mock



创建自动化测试



API 异常警告



环境管理



前后置脚本



创建项目



APIHub




关于 ApiKit 的更多功能,值得你来尝试体验!


传送门:


http://www.eolink.com/?utm_source…


小结


虽然 ApiKit 目前有些功能还并不完善,但整的来说,ApiKit 还是不错的,也为接口开发调试测试提供了一种效率更佳的的解决方案。

作者:CV_coder
链接:https://juejin.cn/post/7237024604962766909
来源:稀土掘金
收起阅读 »

新时代,你需要了解一下苹果的 VisionOS 系统

iOS
这是一个全新的平台。熟悉的框架和工具。请准备好为 Apple vision Pro 设计和构建全新的应用程序和游戏世界。 沉浸的光谱。 Apple vision Pro 提供无限的空间画布供您探索、试验和玩耍,让您自由地完全重新思考您的 3D 体验。人们可以在...
继续阅读 »

这是一个全新的平台。熟悉的框架和工具。请准备好为 Apple vision Pro 设计和构建全新的应用程序和游戏世界。


沉浸的光谱。


Apple vision Pro 提供无限的空间画布供您探索、试验和玩耍,让您自由地完全重新思考您的 3D 体验。人们可以在与周围环境保持联系的同时与您的应用互动,或者完全沉浸在您创造的世界中。您的体验可以是流畅的:从一个窗口开始,引入 3D 内容,过渡到完全身临其境的场景,然后马上回来。


选择权在您手中,这一切都始于 visionOS 上的空间计算构建块。




窗口(Windows)


您可以在 visionOS 应用程序中创建一个或多个窗口。它们使用 SwiftUI 构建,包含传统视图和控件,您可以通过添加 3D 内容来增加体验的深度。


体积(Volumes)


使用 3D 体积为您的应用添加深度。 Volumes 是一种 SwiftUI 场景,可以使用 RealityKit 或 Unity 展示 3D 内容,从而创建可从共享空间或应用程序的完整空间中的任何角度观看的体验。


空间(Space)


默认情况下,应用程序启动到共享空间,在那里它们并排存在——很像 Mac 桌面上的多个应用程序。应用程序可以使用窗口和音量来显示内容,用户可以将这些元素重新放置在他们喜欢的任何位置。为了获得更身临其境的体验,应用程序可以打开一个专用的完整空间,其中只会显示该应用程序的内容。在完整空间内,应用程序可以使用窗口和体积、创建无限的 3D 内容、打开通往不同世界的门户,甚至可以让某人完全沉浸在某个环境中。




Apple 框架 - 扩展空间计算


SwiftUI


无论您是要创建窗口、体积还是空间体验,SwiftUI 都是构建新的 visionOS 应用程序或将现有 iPadOS 或 iOS 应用程序引入该平台的最佳方式。凭借全新的 3D 功能以及对深度、手势、效果和沉浸式场景类型的支持,SwiftUI 可以帮助您为 Vision Pro 构建精美且引人入胜的应用程序。 RealityKit 还与 SwiftUI 深度集成,以帮助您构建清晰、响应迅速且立体的界面。 SwiftUI 还可以与 UIKit 无缝协作,帮助您构建适用于 visionOS 的应用程序。


RealityKit


使用 Apple 的 3D 渲染引擎 RealityKit 在您的应用程序中呈现 3D 内容、动画和视觉效果。 RealityKit 可以自动调整物理光照条件并投射阴影、打开通往不同世界的门户、构建令人惊叹的视觉效果等等。为了创作您的材料,RealityKit 采用了 MaterialX,这是一种用于指定表面和几何着色器的开放标准,由领先的电影、视觉效果、娱乐和游戏公司使用。


ARKit


在 vision Pro 上,ARKit 可以完全了解一个人的周围环境,为您的应用提供与周围空间交互的新方式。默认情况下,ARKit 支持内核系统功能,您的应用程序在共享空间中时会自动受益于这些功能——但是当您的应用程序移动到完整空间并请求许可时,您可以利用强大的 ARKit API,例如平面估计、场景重建、图像锚点、世界轨道和骨骼手部轨道。所以在墙上泼水。从地板上弹起一个球。通过将现实世界与您的内容融合在一起,打造令人惊叹的体验。


Accessibility


visionOS 的设计考虑了可访问性,适用于希望完全通过眼睛、声音或两者的组合与设备交互的人。对于喜欢以不同方式导航内容的人,Pointer Control 允许他们选择食指、手腕或头部作为替代指针。您可以使用已在其他 Apple 平台上使用的相同技术和工具为 visionOS 创建易于访问的应用程序,并帮助使 vision Pro 成为每个人的绝佳体验。




您需要的所有工具。


Xcode


visionOS 的开发从 Xcode 开始,其中包括 visionOS SDK。将 visionOS 目标添加到您现有的项目或构建一个全新的应用程序。在 Xcode 预览中迭代您的应用程序。在全新的 visionOS Simulator 中与您的应用程序交互,探索各种房间布局和照明条件。创建测试和可视化以探索空间内容的碰撞、遮挡和场景理解。


reality composer Pro


探索全新的 reality composer Pro,旨在让您轻松预览和准备 visionOS 应用程序的 3D 内容。随 Xcode 一起提供的 reality composer Pro 可以帮助您导入和组织资产,例如 3D 模型、材料和声音。最重要的是,它与 Xcode 构建过程紧密集成以预览和优化您的 visionOS 资产。


Unity


现在,您可以使用 Unity 强大、熟悉的创作工具来创建新的应用程序和游戏,或者为 visionOS 重新构想现有的 Unity 创建的项目。除了熟悉的 Unity 功能(如 AR foundation)之外,您的应用程序还可以获得 visionOS 的所有优势,例如直通和动态注视点渲染。通过将 Unity 的创作和模拟功能与 RealityKit 管理的应用程序渲染相结合,使用 Unity 创建的内容在 visionOS 上看起来和感觉起来就像在家里一样。




您的 visionOS 之旅从这里开始。


visionOS SDK 本月晚些时候与 Xcode、visionOS 模拟器、reality composer Pro、文档、示例代码、设计指南等一起发布。


为 visionOS 做准备


无论您已经在 App Store 上拥有应用程序,还是这是您第一次为 Apple 平台开发应用程序,您现在都可以做很多事情来为 visionOS SDK 的到来做好准备。了解如何更新您的应用程序并探索现有框架,让您更轻松地开始使用 visionOS。


Prepare for visionOS


了解 visionOS


visionOS 拥有一流的框架和工具,是帮助您创造令人难以置信的空间体验的完美平台。无论您是在构想游戏、构建媒体体验、设计与 SharePlay 的连接和协作时刻、创建业务应用程序,还是更新您的网站以支持 visionOS,我们都有会议和信息来帮助您制定计划。为第 46 场 WWDC23 会议准备好 visionOS SDK,以帮助您了解平台开发、空间体验设计以及测试和工具。


Learn about visionOS


与苹果合作


在为 visionOS 开发应用程序和游戏时,获得 Apple 的直接支持。了解即将举行的活动、测试机会和其他计划,以支持您为此平台创造令人难以置信的体验。


Learn about working with Apple


#visionOS #苹果MR #苹果VR #苹果AR


翻译原文地址


作者:稻草人家
链接:https://juejin.cn/post/7241393511618347045
来源:稀土掘金
收起阅读 »

Mac效率神器Alfred Workflows

Alfred 是 Mac 上一款著名的效率应用,强大的功能和众多的扩展能让你在实际操作中大幅提升工作效率, 这里简单介绍下 Alfred 的工作流 背景&效果展示 作为一名程序员经常会遇到时间戳转时间、时间转时间戳的情况,以前都会打开网页在线工具进行...
继续阅读 »

Alfred 是 Mac 上一款著名的效率应用,强大的功能和众多的扩展能让你在实际操作中大幅提升工作效率,
这里简单介绍下 Alfred 的工作流



背景&效果展示


作为一名程序员经常会遇到时间戳转时间、时间转时间戳的情况,以前都会打开网页在线工具进行转换。
每次打开浏览器找到网址,然后复制内容进行转换都需要 5秒以上的时间。
有什么快捷的方式能帮助我们快速的进行这个操作吗,这里我想到的 Alfred 的工作流。
Alfred工作流可以直接脚本开发,下面是工作流开发完的效果。


1)首先唤起Alfred输入框,这里看自己设置的快捷键了
2)输入这里工作流对应的keyword (tm或tmt)然后空格输入需要转换的内容
3)回车键将转换之后的内容复制到剪切板


这个简单的工作流可以实现linux类型的时间戳转换成 yyyy-MM-dd HH:mm:ss 类型的时间字符串,
也可以将 yyyy-MM-dd HH:mm:ss 类型的时间字符串转换成时间。
转换完的内容会自动放到剪切板里面,可以直接使用 command + v 进行粘贴,也可以使用Alfred的历史剪切板进行复制。


工作流开发


1、创建空的工作流


为了简单快捷这里使用的是python开发的这个功能。
开发这个工作流首先需要再 Alfred 面板上创建一个空的工作流
Alfred -> Preferences -> Workflows -> 左下角的 + -> Blank Workflows
如下图: 



2、添加流程节点


这里使用的是 Script Filter类型的节点,可以支持keyword触发。
节点配置如下,keyword 为触发的命令关键字。下面是运行命令的配置 Script输入框中写需要运行的命令,
这里使用 {query} 方式将转换的内容传递给python脚本。 



复制到剪切板的节点如下,创建好节点用线连接就行。 



3、python脚本开发


脚本的位置放到当前工作流的根目录就行,这样不行写绝对路径,也方便工作流的导出。
1. 打开工作流根目录:右键选中工作流,点 open in find 或者 open in terminal 打开工作流根目录:右键选中工作流,点 open in find 或者 open in terminal 


2.Alfred官方提供了一个python类库方便开发工作流。在工作流根目录执行命令

pip install --target=. Alfred-Workflow

3.创建脚本 timestamp_2_time.py,然后开发对应的代码即可 。代码里面有注释大家可以看下

# -*- coding: utf-8 -*-
import sys
from datetime import datetime
from workflow import Workflow, ICON_CLOCK # 导包

def main(wf):
query = wf.args[0] # 获取传入的参数,这里能获取到需要转换的呢绒
if not query:
return
# 时间戳转时间字符串的方法
d = datetime.fromtimestamp(int(query) / 1000)
str1 = d.strftime("%Y-%m-%d %H:%M:%S")
'''
调用框架的方法添加运行的结果
可选参数是标题、副标题,arg是下一个节点的入参,icon是这个item展示的图标
如果有多个结果可以放多个,然后通过上下键选择
'''
wf.add_item(title=query, subtitle=str1, arg=str1, valid=True, icon=ICON_CLOCK)
# 展示结果内容list
wf.send_feedback()


if __name__ == '__main__':
'''构造 Workflow 对象,运行完退出
'''
wf = Workflow()
sys.exit(wf.run(main))

通过这几行简单的代码实现了时间戳转换成时间的小功能,
相比于以前的使用网页的形式,这个工作流可以将时间缩短到1秒,每次为你省下 4 秒钟的时间 😂😂😂


debug


开发的时候可能会遇到bug,可以通过下图方式打开运行日志查问题。 



开发好的工作流要使用直接导入就行


tmwf.tar
下载之后 tar -xvf tmwf.tar 解压导入就行。
这里需要注意下,我本地的python路径是 /usr/local/bin/python 大家需要换成自己的python路径。

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

axios使用异步方式无感刷新token,简单,太简单了

🍉 废话在前 写vue的伙伴们无感刷新token相信大家都不陌生了吧,刚好,最近自己的一个项目中就需要用到这个需求,因为之前没有弄过这个,研究了一个上午,终于还是把它拿下了,小小的一个token刷新😏。 下面接着分析一下踩到的坑以及解决思路 🍗 接着踩坑 我...
继续阅读 »

🍉 废话在前


写vue的伙伴们无感刷新token相信大家都不陌生了吧,刚好,最近自己的一个项目中就需要用到这个需求,因为之前没有弄过这个,研究了一个上午,终于还是把它拿下了,小小的一个token刷新😏。




下面接着分析一下踩到的坑以及解决思路


🍗 接着踩坑


我按照之前传统的方式在返回拦截器里面进行token刷新,正常的数据可以返回,但是这个时候会有比较麻烦的地方,就是请求的数据可以在拦截器里面得到,但是不能渲染到界面上(看到这里的时候我是懵的)。


看一下代码

service.interceptors.response.use(
response => {
const res = response.data
//刷新token的时候,可以从这里拦截到新数据,但是没有显示在页面上
console.log('拦截数据:',res)
if (loadingInstance) {
loadingInstance.close();
NProgress.done();
}
switch (res.code) {
case 200:
return res
case 401:
router.push({
path: "/login",
})
localStorage.clear();
Notification.error({
title: '令牌过期',
message: '当前身份信息超过三天已失效,请您重新登录',
duration: 0
});
break;
default:
Message({
message: res.message || '请求错误',
type: 'error',
duration: 5 * 1000
})
break;
}
},
error => {
switch (error.response.status) {
case 401:
MessageBox.confirm('身份认证已过期,是否刷新本页继续浏览?', '提示', {
confirmButtonText: '继续浏览',
cancelButtonText: '退出登录',
type: 'warning'
}).then(() => {
axios.post('/api/token/refresh/', {
refresh: localStorage.getItem("retoken")
}).then(response => {
let res = response.data || {}
if (res.code == 200) {
let token = res.data.result;
localStorage.setItem("token", token.access);
localStorage.setItem("retoken", token.refresh);
error.response.config.baseURL = '';
error.response.config.headers['Authorization'] = 'Bearer ' + localStorage.getItem("token")
window.location.reload();
} else {
router.push({
path: "/login",
})
localStorage.clear();
Notification.error({
title: '令牌过期',
message: '当前身份信息超过三天已失效,请您重新登录',
duration: 0
});
}
}).catch(err => {
router.push({
path: "/login",
})
localStorage.clear();
Notification.error({
title: '认证失败',
message: '信息认证失败,请重新登录',
duration: 0
});
})
}).catch(() => {
router.push({
path: "/login",

})
Message({
message: '已退出',
type: 'success',
})
localStorage.clear();
});
break;
case 404:
Notification.error({
title: '404错误',
message: '服务器请求错误,请联系管理员或稍后重试。错误状态码:404',
});
break
default:
Notification.error({
title: '请求错误',
message: '服务器请求错误,请联系管理员或稍后重试',
});
break;
}
if (loadingInstance) {
loadingInstance.close();
NProgress.done();
}
return Promise.reject(error);

}
)

懵归懵,好在已经发现这个问题了,剩下的怎么解决呢?


当时想的是和后端配合,让后端直接发一个token过期的时间戳给我,我直接把这个时间戳放到localStrage里面,通过这个localStrage,直接在前端进行判断token的过期时间进行请求拦截,如果当前请求的时间大于了这个localStrage里面的时间,就说明token过期了,我这边就需要重新请求token了,而不要后端去进行token的验证。


信心满满的弄了一下,发现行不通,token过期的时候,后端直接一个错误401,前端就又回到解放前了。而且用时间戳的方式很容易出现bug,且在前端进行token时长的验证,很容易出现问题。因此,我还是觉得再研究一下上面那一段代码。


当然踩到坑不只是这个,还有百度和chatGPT,真的,看了一下没有找到一个可行的,总结一下主要有以下几种

  1. 状态管理vuex

  2. 路由router

  3. 时间戳(和我刚刚那种方式差不多)

  4. 依赖注入inject

  5. 刷新界面(我最开始那种方式,但是刷新的时候会出现页面白屏,且用户如果在页面上有一些自己输入的数据也会被清空,用户体验感不好)


以上方式我先不管行不行,但是麻烦是肯定的,做前端讲究的就一个字:“懒”
不是,应该是 “高效快捷”
所以这些方式就pass掉了


只能想想为什么拦截器里面可以得到数据,为什么页面位置得不到数据了


🥩 解决思路


下面是我的解决思路,有不对的地方还请看到的大神指出来一下😁


当用户发起请求的时候,因为刷新token的http状态码是401,这个时候axios的响应拦截器就直接进行错误捕获了,到了这里,因为数据已经返回了,但是因为是错误数据,页面得到的这个数据不可用且当前请求已经结束了,当然这里对状态码401是进行处理了的(应该获取token了)。


采用普通的获取方式来获取token,因为异步的原因,我们获取token的同时页面也在做刷新,token获取的同时,界面也刷新完毕了(但是是没有数据的,不做错误捕获会报错),因此我们在获取token完毕,且用新的token去获取数据时,拦截器里面会有数据,但是界面已经休息了,就不会把拦截器里面的新数据刷新到页面了。


因此这个地方需要对获取token的过程进行一下请求阻塞,把获取token的请求变成同步的。到这里就差不多了。直接把响应拦截器里面的error函数变成同步不就行了吗,async + await可以出来了。


以上是自己当时的想法,简单说来就是 页面刷新需要慢我获取token一步 ,通过这个方式也确实做到了无感刷新🤣


希望以上的能帮助到你,有什么好的思路也欢迎评论区指出。


🍓完整代码


这个是用了2.13.2版本的element-ui以及nprogress的一个axios代码模块,包含了一个下载文件的模块。


如果需要的话可以根据自己的需求来进行修改

import axios from 'axios'
import {
Message,
Loading,
Notification,
} from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import router from '@/router/index'
const baseURL = '/api'
const service = axios.create({
baseURL,
timeout: 6000
})
NProgress.configure({
showSpinner: false
}) // NProgress Configuration
let loadingInstance = undefined;
service.interceptors.request.use(
config => {
NProgress.start()
loadingInstance = Loading.service({
lock: true,
text: '正在加载,请稍候...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.3)'
})
config.headers['Authorization'] = 'Bearer ' + localStorage.getItem("token")
return config;
},
error => {
return Promise.reject(error)
}
)

service.interceptors.response.use(
response => {
const res = response.data

if (loadingInstance) {
loadingInstance.close();
NProgress.done();
}
switch (res.code) {
case 200:
return res
case 401:
router.push({
path: "/login",
})
localStorage.clear();
Notification.error({
title: '认证失效',
message: '当前身份信息超过三天已失效,请您重新登录',
duration: 0
});
break;
default:
Message({
message: res.message || '请求错误',
type: 'error',
duration: 5 * 1000
})
break;
}
},
async error => {
switch (error.response.status) {
case 401:
const err401Data = error.response.data || {}
if (err401Data.code !== "token_not_valid") {
router.push({
path: "/login",
})
localStorage.clear();
Notification.error({
title: '认证失败',
message: '身份信息认证失败,请您重新登录',
duration: 0
});
return
}
try {
const res = await service.post('/token/refresh/', {
refresh: localStorage.getItem("retoken")
})
if (res.code == 200) {
let token = res.data.result;
localStorage.setItem("token", token.access);
localStorage.setItem("retoken", token.refresh);
error.response.config.baseURL = ''
error.response.config.headers['Authorization'] = 'Bearer ' + localStorage.getItem("token")
return service(error.response.config)
} else {
router.push({
path: "/login",
})
localStorage.clear();
Notification.error({
title: '认证失败',
message: '当前身份认证信息已失效,请您重新登录',
duration: 0
});
}
} catch (error) {
router.push({
path: "/login",
})
localStorage.clear();
Notification.error({
title: '认证失败',
message: '身份认证失败,请您重新登录,失败原因:' + error.message,
duration: 0
});
}

break
case 404:
Notification.error({
title: '404错误',
message: '服务器请求错误,请联系管理员或稍后重试。错误状态码:404',
});
break
default:
Notification.error({
title: '请求错误',
message: '服务器请求错误,请联系管理员或稍后重试',
});
break;
}
if (loadingInstance) {
loadingInstance.close();
NProgress.done();
}
return Promise.reject(error);

}
)

// 文件下载通用方式
export const requestFile = axios.create({
baseURL,
timeout: 0, //关闭超时时间
});

requestFile.interceptors.request.use((config) => {
config.headers['Authorization'] = 'Bearer ' + localStorage.getItem("token") //携带的请求头
config.responseType = 'blob';
loadingInstance = Loading.service({
lock: true,
text: '正在下载,请稍候...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.3)'
})
return config;
});

requestFile.interceptors.response.use(
(response) => {
let res = response.data;
if (loadingInstance) {
loadingInstance.close()
}
// const contentType = response.headers['content-type'];//获取返回的数据类型
let blob = new Blob([res], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" //文件下载以及上传类型为 .xlsx
});
let url = window.URL.createObjectURL(blob);
// 创建一个链接元素
let link = document.createElement('a');
link.href = url;
link.download = '产品列表.xlsx'; // 自定义文件名
link.click();


},
(err) => {
Message({
message: '操作失败,请联系管理员',
type: 'error',
})
if (loadingInstance) {

loadingInstance.close()
}
}
);


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

🎉前端开发书籍推荐🎉

文章首发公众号:萌萌哒草头将军,欢迎关注 🎉SolidJS响应式原理和简易实现🎉 做为多年学习JavaScript的开发者,一路走来,几多坎坷,回头再看来时的路,特别感谢下面几本书带我入行前端。 💎《JavaScript高级程序设计》 推荐指数:⭐⭐⭐⭐⭐...
继续阅读 »

文章首发公众号:萌萌哒草头将军,欢迎关注



🎉SolidJS响应式原理和简易实现🎉


做为多年学习JavaScript的开发者,一路走来,几多坎坷,回头再看来时的路,特别感谢下面几本书带我入行前端。


💎《JavaScript高级程序设计》




推荐指数:⭐⭐⭐⭐⭐


推荐理由:内容扎实,且不深奥。


这本书在我大学的时候就已经买了,当时经济实力有限,买的是影印版,不过也被我研读了多遍,并且做了详细的记录。这本书见证了我的前端之路,一直被我珍藏至今。




现在已经出了第四版了。前端入门进阶宝典,所以也被广大开发者称为《红宝书》。


💎《Javascript权威指南》




推荐指数:⭐⭐⭐⭐⭐


推荐理由:内容全面,讲解详细,配合红宝书效果更佳。


该书被称为《犀牛书》,书如其名,真的是权威指南,即使你买了红宝书,我也推荐你买一本,因为这两本对相同的知识讲解,侧重点不同。


比如对于闭包,红宝书很详细,从作用域,到作用域链,再到活动对象,讲解由浅到深,十分详细,而《犀牛书》中仅仅是给出闭包的概念,然后举例说明差异。


但是《犀牛书》对于类型转换toStringvalueof讲解则十分详细,《红宝书》则浅浅的带过。


《犀牛书》更像是一本字典,有不懂的问题,可以及时查漏补缺。


💎《图解HTTP》




推荐指数:⭐⭐⭐⭐


推荐理由:图文并茂,简洁明了


这本书作为第三本推荐,绝不是空穴来风,大量的图来解释枯燥的概念,形象生动,老少皆宜。


前端开发一大部分的时间都是在和后端的接口打交道,而Http无疑是沟通的桥梁。


读完这本书,你将会了解到网络分层模型Http协议和TCP/IP的关系、后端的数据怎么从服务端到达浏览器的、常用Http状态码的含义、请求头的各种含义、你输入url浏览器发生了什么等热门面试题的答案。


我买的是三件套《图解Http》《图解TCP/IP》《图解网络硬件》,《图解网络硬件》一点也不推荐,不懂的硬件直接百度吧,还是彩色图片。如果你是做运维相关的前端开发,《图解TCP/IP》同样值得一看。




💎《数据结构与算法JavaScript描述》




推荐指数:⭐⭐⭐⭐


推荐理由:进阶利器,闭眼入就对了。


大多数同学选择前端,主要还是因为数据结构和算法方面比较薄弱,但是这本书却使用了简洁的方法实现了各种数据结构和算法。


对于难懂的数据结构,有详细的结构图解释,是我读过最容易理解的版本了,不过这本书目前还是ES5语法版本实现,我在前面的文章中使用ES6语法实现过,并做了部分笔记。


👉【数据结构】我的学习笔记




💎《JavaScript设计模式》




推荐指数:⭐⭐⭐


推荐理由:常见的设计模式都有,讲解的比较简单易懂,但是实现比较简陋。


这本书是你入门中级后继续提升的有利法宝,不管什么框架,底层都逃不出两三个设计模式的,所以十分推荐你进阶的时候去读它。


目前有两本名为《JavaScript设计模式》的书,我买的是徐涛翻译版本影印版(和红宝书一起买的),但是最近查阅发现流行的是张容铭著作的版本,这里请自行斟酌买哪个版本。


我买的这个版本将设计模式分为创建型、行为型和结构型三种,前面部分分别讲解了十三种设计模式,后半部分讲解了老牌框架JQuery设计的各种设计模式,虽然从现在的情况看JQuery已经凉了,但是它的设计智慧,真的令人敬佩。


这本书的缺点也是语法版本较旧,不过我也写了最新语法的部分笔记。


👉超级简单的设计模式,看不懂你来打我


今天的内容就这些了,如果你有更好的书籍,可以告诉我!


现在,关注我的公众号会有送书福利,具体请在公众号回复:活动,即可查看详情


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

推荐 6 个 火火火火 的开源项目

本期推荐开源项目目录:1、ChatGPT 网页应用(AI)2、AI 换脸(AI)3、API 调用 Midjourney 进行 AI 画图(AI)4、如何使用 Open AI 的 API?(AI)5、中华古诗词数据库6、动画编程 01. ChatGPT 网页应用...
继续阅读 »

本期推荐开源项目目录:

1、ChatGPT 网页应用(AI)
2、AI 换脸(AI)
3、API 调用 Midjourney 进行 AI 画图(AI)
4、如何使用 Open AI 的 API?(AI)
5、中华古诗词数据库
6、动画编程


01. ChatGPT 网页应用


基于 ChatGPT-Next-Web 二次开发的 ChatGPT 网页付费系统,包含用户管理模块和后台看板。


ChatGPT-Admin-Web 付费系统包含七个模块,包括:内容接口、用户系统、支付、敏感词过滤、自由聊天、分销、收益


编辑


添加图片注释,不超过 140 字(可选)


开源地址:github.com/AprilNEA/Ch…



编辑


添加图片注释,不超过 140 字(可选)


02. AI 换脸


适用于视频聊天的 AI 换脸模型,你可以使用这个 AI 模型替换摄像头中的面部或视频中的面部。这是一些例子:


编辑


添加图片注释,不超过 140 字(可选)


开源地址:github.com/iperov/Deep…


03. API 调用 Midjourney 进行 AI 画图


通过代理 MidJourney 的 Discord 频道,实现 api 形式调用AI绘图。


前提是你要注册 Midjourney 账号、并在 Discord 创建在自己的频道和机器人,然后就可以根据这个项目的指引一步步去使用 Api 调用 Midjourney 了。


开源地址:github.com/novicezk/mi…


编辑


添加图片注释,不超过 140 字(可选)


04. 如何使用 Open AI 的 API?


Open AI-Cook Book 是一本 Open AI 的 API 使用指南,提供了一些通过 Open AI 的 API 搭建任务的示例代码。


开源地址:github.com/openai/open…


编辑


添加图片注释,不超过 140 字(可选)


05. 中华古诗词数据库


为了让古诗词这个人类瑰宝传承下去,中华古诗词数据库诞生了。


这个项目整理了中华大量的古诗词,支持 Json 格式。数据库包含唐宋两朝近一万四千古诗人的作品, 接近 5.5 万首唐诗、26 万宋诗. 两宋时期1564位词人,21050首词。


开源地址:github.com/chinese-poe…


编辑


添加图片注释,不超过 140 字(可选)



  1. 动画编程


Motion Canvas 是一个 TypeScript 库,可以通过编程的方式生成动画,并提供所述动画的实时预览的编辑器。


开源地址:github.com/motion-canv…




编辑


添加图片注释,不超过 140 字(可选)


历史盘点


逛逛 GitHub 每天推荐一个好玩有趣的开源项目。历史推荐的开源项目已经收录到 GitHub 项目,欢迎 Star:

地址:https://github.com/Wechat-ggGitHub/Awesome-GitHub-Repo


作者:逛逛GitHub
链接:https://juejin.cn/post/7240690534075318309
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

rpc比http好吗,缪论?

是什么,如何理解 RPC(Remote Procedure Call) 直译就是远程过程调用 HTTP(HyperText Transfer Protorl) 直译就是超文本传输协议 RPC和HTTP都是 请求-响应协议,但是因为出现的时机、设计理念、约定协议...
继续阅读 »

是什么,如何理解


RPC(Remote Procedure Call) 直译就是远程过程调用


HTTP(HyperText Transfer Protorl) 直译就是超文本传输协议


RPC和HTTP都是 请求-响应协议,但是因为出现的时机、设计理念、约定协议、效率、应用范围、使用规则等不同,所以是不同的名字,本质都是为了分布式系统间的通信而生,是一种应用层通信(请求-响应)协议(从OSI网络模型来看)。

RPC是 Bruce Jay Nelson 在1981年创造的术语,HTTP是在1990年左右产生的(可以参看维基百科)


RPC协议 和 RPC,到底叫什么?RPC协议=RPC


HTTP协议、HTTP,到底叫什么?HTTP协议=HTTP


RPC|HTTP只是大家的简称

1、HTTP协议不仅仅只有协议,还有超文本,传输,以及很多功能(比如编解码、面试经常背的各种参数的作用)

2、RPC协议也不仅仅只有协议,还有 编解码,服务注册发现,负载均衡等


RPC协议本质上定义了一种通信的流程,而具体的实现技术是没有约束的,每一种RPC框架都有自己的实现方式,我认为HTTP也是RPC的一种实现方式


协议直白来讲是一种约定,rpc和http都是为了服务器间的通信而生,都需要制定一套标准协议来进行通信。不过HTTP比较火,是一个全世界的统一约定,使用比较广泛。但通用也意味着冗余,所以后来又产生了很多RPC框架(自定义协议,具备优秀的性能等)


我们可以自定义RPC请求/响应 包含的消息头和消息体结构,自定义编解码方式,自定义网络通信方式,只要clientserver消息的发送和解析能对应即可,这些问题确认下来,一个RPC框架就设计出来了


下面先从请求过程看一下RPC和HTTP都会经历哪些阶段,然后再分阶段去做对比

一次请求的过程



从请求链路可以看到,最核心的只有三层:编解码、协议、网络通信


下面会从这3个角度去对比HTTP和RPC


HTTP VS RPC自定义协议


HTTP和RPC 2个关键词不具备可比较性,因为RPC包含了HTTP。


但是RPC自定义协议(thrift, protobuf, dubbo, kitex-thrift等) 是RPC的具体实现,HTTP也是RPC的具体实现,它们是具备可比较性的


编解码(序列化)



  • 序列化: 指将程序运行过程中的动态内存数据(java的class、go的struct)转化为硬盘中静态二进制数据的过程,以方便网络传输。

  • 反序列化:指将硬盘中静态二进制数据转化为程序运行过程中的动态内存数据的过程,以方便程序计算。


HTTP/1.1 一般用json


自定义RPC协议 一般用 thrift、protobuf


kitex序列化协议


协议层


编码之后,数据转换成字节流,但是RPC通信时,每次请求发送的数据大小不是固定的,那么为了区分消息的边界,避免粘包、半包等现象,我们需要定义一种协议,来使得接收方能够正确地读出不定长的内容。简单点说,通信协议就是约定客户端和服务器端传输什么数据,以及如何解析数据。


可参考

1、kitex:概览,传输协议

2、dubbo:triple 协议,概览


可以思考一下 序列化、传输协议、网络通信的关系,下面以kitex为例进行分析


kitex codec 接口定义kitex thrift 序列化实现kitex ttheader协议,kitex 发送请求核心代码


可以发现 Encode中,先根据message构造出header,写入out,然后再把data(实际的业务数据)写到out。

encode函数完全遵守 ttheader协议去构造数据。

最后再把out通过网络库发送出去

网络通信层

网络通信层主要提供一个易用的网络库,封装了操作系统提供的socket api。


HTTP的长连接和TCP长连接不是一个东西,需要注意下,TCP Keepalive是操作系统实现的功能,并不是TCP协议的一部分,需要在操作系统下进行相关配置(只能保证网络没问题,不能代表服务没问题)


其中 HTTP2 拥有多路复用、优先级控制、头部压缩等优势


可以参考


kitex:连接类型


RPC自定义协议 和 HTTP的使用场景


公司内部的微服务,对客户端提供的服务 适合用RPC,更好的性能


对外服务、单体服务、为前端提供的服务适合用HTTP


我的思考


rpc在编解码、协议层、网络通信 都比HTTP有更大的优势,那为啥不把HTTP换成RPC呢

1、人的认知,HTTP已经深入人心(或者说生态好,通用性强),几乎所有的机器、浏览器和语言默认都会支持。但是自定义RPC协议 可能很多人都没听过(比如kitex、dubbo等),还让别人支持,根本不可能。

  • 需要建设全局的DNS等等,HTTP链路中的组件都需要换成 自定义的那一套,成本极高。
  • 但是公司内部可以搞成一套,可以极大提高性能,何乐而不为。

  • 我见过的案例是很多时候并没有深入思考为什么用,而是大家都这么用,我也这么用。

2、浏览器只支持 http协议。而且浏览器不支持自定义编解码的解析
      为啥大家面向浏览器/前端 不用自定义编解码?
     http不仅可以传输json、还可以传输二进制、图片等。所以协议层可以用http,编解码用protobuf/thrift也是可行的。
  • 公司内部实际案例:服务端和客户端交互时,为了提高性能,采用protobuf编解码数据,使用http协议传输数据。

  • 但是每次请求/响应数据都是不可读的。服务端会把protobuf编码前的数据转为json,用于打印log/存储,方便排查问题。

3、RPC框架 可以自定义负载均衡,重试机制,高可用,流量控制等策略。这些是HTTP不能支持的
  • 我理解是协议层用的http,但是内部的运行机制还是自定义的。http只是定义了传输数据的格式。举个例子:http的流量控制其实用的是 tcp的滑动窗口,http协议本身不具备这些功能。但是rpc是可以自己加这些功能的。这些功能必然有数据传输,这个传输协议用的http。

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

收起阅读 »

SpringBoot获取不到用户真实IP怎么办

今天周六,Binvin来总结一下上周开发过程中遇到的一个小问题,项目部署后发现服务端无法获取到客户端真实的IP地址,这是怎么回事呢?给我都整懵逼了,经过短暂的思考,我发现了问题的真凶,那就是我们使用了Nginx作的请求转发,这才导致了获取不到客户端真实的IP地...
继续阅读 »
今天周六,Binvin来总结一下上周开发过程中遇到的一个小问题,项目部署后发现服务端无法获取到客户端真实的IP地址,这是怎么回事呢?给我都整懵逼了,经过短暂的思考,我发现了问题的真凶,那就是我们使用了Nginx作的请求转发,这才导致了获取不到客户端真实的IP地址,害,看看我是怎么解决的吧!

问题原因

客户端请求数据时走的是Nginx反向代理,默认情况下客户端的真实IP地址会被其过滤,使得SpringBoot程序无法直接获得真实的客户端IP地址,获取到的都是Nginx的IP地址。

解决方案:

通过更改Nginx配置文件将客户端真实的IP地址加到请求头中,这样就能正常获取到客户端的IP地址了,下面我一步步带你看看如何配置和获取。
修改Nginx配置文件

#这个参数设置了HTTP请求头的Host字段,host表示请求的Host头,也就是请求的域名。通过这个设置,Nginx会将请求的Host头信息传递给后端服务。
proxy_set_header Host $host;
#这个参数设置了HTTP请求头的X−Real−IP字段,remote_addr表示客户端的IP地址。通过这个设置,Nginx会将客户端的真实IP地址传递给后端服务
proxy_set_header X-Real-IP $remote_addr;
#这个参数设置了HTTP请求头的 X-Forwarded-For字段,"X-Forwarded-For"是一个标准的HTTP请求头,用于表示HTTP请求经过的代理服务器链路信息,proxy_add_x_forwarded_for表示添加额外的服务器链路信息。
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

修改后我的nginx.conf中的server如下所示

server {
listen 443 ssl;
server_name xxx.com;

ssl_certificate "ssl证书pem文件";
ssl_certificate_key "ssl证书key文件";
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
ssl_prefer_server_ciphers on;

location / {
root 前端html文件目录;
index index.html index.htm;
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
# 关键在下面这个配置,上面的配置自己根据情况而定就行
location /hello{
proxy_pass http://127.0.0.1:8090;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

SpringBoot代码实现

第一种方式:在代码中直接通过X-Forwarded-For获取到真实IP地址

@Slf4j
public class CommonUtil {
/**
* <p> 获取当前请求客户端的IP地址 </p>
*
* @param request 请求信息
* @return ip地址
**/
public static String getIp(HttpServletRequest request) {
if (request == null) {
return null;
}
String unknown = "unknown";
// 使用X-Forwarded-For就能获取到客户端真实IP地址
String ip = request.getHeader("X-Forwarded-For");
log.info("X-Forwarded-For:" + ip);
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
log.info("Proxy-Client-IP:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
log.info("WL-Proxy-Client-IP:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
log.info("HTTP_X_FORWARDED_FOR:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED");
log.info("HTTP_X_FORWARDED:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_CLUSTER_CLIENT_IP");
log.info("HTTP_X_CLUSTER_CLIENT_IP:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
log.info("HTTP_CLIENT_IP:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_FORWARDED_FOR");
log.info("HTTP_FORWARDED_FOR:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_FORWARDED");
log.info("HTTP_FORWARDED:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_VIA");
log.info("HTTP_VIA:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("REMOTE_ADDR");
log.info("REMOTE_ADDR:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
log.info("getRemoteAddr:" + ip);
}
return ip;
}

第二种方式:在application.yml文件中加以下配置,直接通过request.getRemoteAddr()并可以获取到真实IP

server:
port: 8090
tomcat:
#Nginx转发 获取客户端真实IP配置
remoteip:
remote-ip-header: X-Real-IP
protocol-header: X-Forwarded-Proto

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

收起阅读 »

69.9K Star,最强开源内网穿透工具:frp

作为一名开发者,有很多场景需要用到内网穿透,比如:我们在接入一些大平台做第三方应用时,在本地开发微信公众号工具的时候需要让微信平台能否访问到本地提供的接口。除此之外,还有很多其他场景,也会用到,比如:把放在家里的NAS或服务器暴露到公网上,这样在外面的时候也可...
继续阅读 »

作为一名开发者,有很多场景需要用到内网穿透,比如:我们在接入一些大平台做第三方应用时,在本地开发微信公众号工具的时候需要让微信平台能否访问到本地提供的接口。除此之外,还有很多其他场景,也会用到,比如:把放在家里的NAS或服务器暴露到公网上,这样在外面的时候也可以随时随地的访问。


说到内网传统,TJ君第一个想到的是国内最早的一款知名软件:花生壳。但是今天不是要推荐它,而是要推荐一个更牛的开源项目:frp!该项目目前已经收获了69.9 K Star,在GitHub上获得了极大的认可!


下载安装

frp目前已经提供了大部分操作系统的支持版本,通过这个链接:github.com/fatedier/fr… 就可以下载到适合你使用的安装。


以Windows的包为例,解压后可以获得这些内容:


frps是服务端程序,frpc是客户端程序。ini文件就是对应的配置文件。


首发 blog.didispace.com/tj-opensour…,转载请注明出处


暴露内网服务


内网穿透的玩法有很多,这里列举一个比较常见的例子。


比如:我要暴露一个只有自己能访问到服务器。那么可以这样配置:


配置 frps.ini,并启动服务端 frps

[common]
bind_port = 7000

在需要暴露到外网的机器上部署 frpc,配置如下:

[common]
server_addr = x.x.x.x
server_port = 7000

[secret_ssh]
type = stcp
# 只有 sk 一致的用户才能访问到此服务
sk = abcdefg
local_ip = 127.0.0.1
local_port = 22

在想要访问内网服务的机器上也部署 frpc,配置如下:

[common]
server_addr = x.x.x.x
server_port = 7000

[secret_ssh_visitor]
type = stcp
# stcp 的访问者
role = visitor
# 要访问的 stcp 代理的名字
server_name = secret_ssh
sk = abcdefg
# 绑定本地端口用于访问 SSH 服务
bind_addr = 127.0.0.1
bind_port = 6000

把frpc也都启动起来之后,通过 SSH 就可以访问内网机器了

ssh -oPort=6000 test@127.0.0.1

其他支持


除了上面的玩法之外,frp还有很多玩法,比如:

1、自定义域名访问内网的 Web 服务

2、转发 DNS 查询请求

3、转发 Unix 域套接

4、对外提供简单的文件访问服务

5、为本地 HTTP 服务启用 HTTPS

6、点对点内网穿透


篇幅有限,具体如何配置这里就不多说了,有需要的读者可以直接查看官方文档,均有详细的服务端客户端配置案例。


最后,奉上相关链接:


开源地址:github.com/fatedier/fr…
文档地址:gofrp.org/docs/

作者:程序猿DD
链接:https://juejin.cn/post/7263283712224395321
来源:稀土掘金
收起阅读 »

你的野心距离成功,就差一个机会

这几天,深圳的几个达友张罗着找时间线下聚聚。 这两天在看《狂飙》。 自从开始写公众号后,业余时间不是在码字,就是在思考怎么码字。 这么热门的片子,加一起才看 6 集。 里面的张颂文,演技的确赞。 不过东哥想的是,演技这么好,怎么出道20年才火...
继续阅读 »

这几天,深圳的几个达友张罗着找时间线下聚聚。





这两天在看《狂飙》。


自从开始写公众号后,业余时间不是在码字,就是在思考怎么码字。


这么热门的片子,加一起才看 6 集。


里面的张颂文,演技的确赞。


不过东哥想的是,演技这么好,怎么出道20年才火起来?


他缺的,是什么?







人怎么能成功?简单地说



成功 = 能力 × 机会



能力是修炼内功,让自己变得更专业,成为头部的专家,是自己应该搞定的事情。


机会这事儿,就朦胧多了。


酒香不怕巷子深,是物质稀缺时代的事情。


整个镇子只有一家酿酒,巷子再深都有酒鬼登门。


而当前世界已经进入到了产能过剩、需求不足的时代。


国民总时间恒定,大家在存量的池子里杀的头破血流。


没机会展现的才华,只能被风沙埋没。


机会,越来越重要。


你与成功之间,可能就差一个机会。


比如张颂文,没机会拿到好剧本好角色,就只能是个小演员。


46 岁,买不起房子,没存款,感觉自己好失败。


每天都被拒绝,甚至侮辱地拒绝,让他滚蛋。


所以我们不能只死磕自己苦练内功,坐等被赏识被发现。


傻傻的,感动自己。


要为自己的野心,创造机会。


怎么创造呢?




去机会更多的地方。 在水多的地方打井,选鱼多的地方捞鱼。


最关键的选择,一个是城市,一个是赛道。


做金融,就到上海和深圳,搞互联网,就去北京和深圳。


东哥做香港保险,一个很大的优势就是所在的城市。


从福田站到香港西九龙,高铁15分钟。


市中心到市中心。


赛道选择上,做和钱近的工作,做可积累的工作。


能和人打交道,就别和机器打交道;能做销售,就别做售后。


销售看到的都是机会和钱,售后看到的都是负面问题。




向上链接,寻找大节点,利用好高能级的关系


人和人能量密度不同,大节点就是个人崛起的发动机。


达叔曾谈到过,在他写公众号的过程中,曾被欧神、医业观察的星哥、凯叔药械升职记的凯叔推荐。


三次推荐,引来了大批流量,成就了达叔的崛起。


在职场,就是要发现身边的强者,做深度绑定,成为强者权力结构件的一部分。


多花心思,多花钱,努力走进领导的小圈子,成为他身边的人。


利用他的势能,实现职业和财富的崛起。


就像《人民的名义》里面,高育良提到汉大帮的时候说



主观上说,我从没想过把人民赋予的权力向任何一位学生私相授受,但客观上也许私相授受了。
做了这么多年的法学教授,教了这么多年书,学生少不了,对自己学生呢,谁都不可能没感情,用人时就难免有偏爱。






进入不了核心圈子,就注定是个边缘人物,最后沦为炮灰。


如果在食物、资源匮乏的时候,你不坐在桌子旁,大概率就得躺在桌子上。


需要注意的是,链接的能级差不能太大。


基层员工,就别总想着去链接总经理董事长。


除非你是王思聪,他是王健林。


县官不如现管,链接好那个直接自己升职加薪的人,最有价值。




多和人互动,无论是线上还是线下。


人和人之间的互动永远是这个世界的最核心的算法,剩下全是工具。


职场中,多和实权派互动,和业务部门互动,和给公司赚钱最多的部门互动。


最忌讳的,就是整天对着电脑研究计算模型。


上班一天,接触的人不过办公室里这三五个,和固定的几个窗口,加一起超不过10个人。


能深入沟通和交流的,只有个位数。


除非你能成为公司最顶级的技术大牛,非你不可的那种,否则随时都可能被干掉。


东哥在自己的在每一篇文章后,都会附上了自己的个人微信,也是希望能和大家链接。


短视频时代,能潜下心来阅读文字本身就很可贵。


尤其还是东哥这种枯燥乏味谈赚钱的文章。


但有趣的是,很多人加微信以后一声不吱,甚至我主动询问也不回应。


这样的链接很难有实际价值。


只有沟通彼此的链接才会有价值,无论这个价值是信息、资源还是项目。


没有互动,彼此就仅仅是微信系统里面的几个号码。




多尝试,多拓展。机会是干出来的,不是想出来的。


就像猫王和村上春树的故事。


猫王是二十世纪收入最高的歌星,在卖唱及作明星之前是一位货车司机,每月的收入只数百美元。


工余之暇,他去试唱,被唱片公司相中,然后一举成名。


每年的收入以千万美元计。


村上春树则是在看棒球比赛的时候,突然想要写本小说。


比赛一结束,他就立马赶到文具店买了笔和纸,开始创作他的第一本小说《且听风吟》。


人生有无数种可能。当前的状态,不代表永远。


状态之外,要不断向外拓展,比如猫王的试镜,村上春树的小说。


只有不断地试,才会发现身上其他可能。


否则就只能,守着安静的沙漠 等待花开,看着别人的快乐默默感慨。


尤其自媒体,轻资产,风险可控,是时代给予我们的拓展机会。


多想想,自己有什么东西,可以不断打磨放大?


写公众号这几个月,和各路大 V 沟通,大开眼界。


医生讲医疗,房 V 讲房产都是常规操作。


搞心理学的做星座内容,看桃花运、分析财运,居然也有不少客户。


擅长做饭的可以做美食博主,身材好的可以做服装博主。


你擅长的是什么,能打出什么机会?




用无限游戏的思维方式,多推销自己。


无论做什么,总有一双眼睛在背后默默看着你。


合作的过程就是展现自己的机会。


工作中做汇报做提案,与同事沟通方案,最高任务是什么?


有人会说是把事情搞定,所以应该对事不对人。


对项目推进有利的,就要据理力争。


实际上,这样的工作是低效的,有时候甚至会起反作用。


同样的事情,不同的人沟通,常会有完全相反的结果。


对事不对人,现实中不存在。


理念和现实冲突时,错的永远是理念,不会是现实


沟通的最高任务,不是推销自己对项目的主张,而是推销自己这个人


从这个角度想,我们就不会局限于一城一地之得失,一朝一夕之荣辱。


而会站在一个更高层面,整个职业生涯的高度,来看待当下的问题。


项目成败的因素有很多,不一定是我们可以控制的。


但项目可以失败,人不可以失败


每一次沟通,都要展现自己足够靠谱,足够专业,是更长远合作的好伙伴。


所以要利用好每一次沟通,甚至是每一次刁难。


都是在给自己种善因。




资源也好,技术也罢,多分享,多展现。


不要藏着掖着,害怕教会徒弟饿死师傅。


太阳下面没有新鲜事,你能找到,别人也能找到。


真正的强者,你打压不了。


与其彼此竞争拼个头破血流,不如投资强者。


在他发展壮大的必然道路上,助他一臂之力。


也助以后的自己一臂之力。


你能成就多少强者,就有多大能量。


就像春秋时期齐桓公,想拜鲍叔牙为相,同时还要宰了对手公子纠的谋士管仲。


谁知鲍叔牙对他说:“我的才能只能让齐国平安,如果您要称霸天下,一定得拜管仲为相。”


齐桓公从之,尽释前嫌,拜管仲为国相,终成王霸之业,鲍叔牙也成就了自己千古美名。




结合多动手和多分享的思路,打造自己的产品。


就像伟大的程序员 Linus 说的



Talk is cheap. Show me the code.
被扯没用的,给我看代码。



产品是创作能力的背书,是自控能力的表现,是你最大的筹码。


自己的作品,可以理解为一种社交货币。


打磨作品就是往里钱,在将来的某个时刻,可以兑换它。


陌生人介绍的时候,就可以直接把作品丢出来。


有了作品,就有了各种合作的可能。


东哥这么小一个号,都有人过来谈合作,有时会一些意外的机会。




最后一点,给生活增加一些随机性。


平时晚上在家读书写作充电。下班时,朋友邀请参加一个陌生的聚会,去不去?


马上出发。


这就是生活中的随机性,说不定在聚会过程中会有新的收获。


一个漂亮的妞做女朋友,或一个合作的机会赚点钱。


这就是《黑天鹅》里面提到的正面黑天鹅


努力把自己暴露在可能发生正向意外的环境里。


用时间上微小的损失,换正面黑天鹅出现的可能,进而突破路径依赖。


就像这几天,深圳的几个达友张罗着找时间线下聚聚。


这群人里面有做医疗的,做芯片贸易的,做生物的,还有东哥是卖保险的。


彼此之间可能交流什么,完全未知。


但也就是因为这种未知,才能让自己突破日常的局限,也就更值钱。




供给过剩的时代,无数人在存量的池子里杀得头破血流。


给自己多创造一些机会,杀出来的可能就大一些

  • 去鱼多的地方捞鱼,挤到机会多的地方;
  • 向上链接,发现强者,进入到强者的核心圈子;
  • 多和人互动,不要沉迷于技术;
  • 基于自身优势多对外拓展,寻找机会;
  • 无限游戏思维,办事儿的时候笑面挑战,推销自己;
  • 多分享,尤其挖掘并投资潜力股;
  • 打造自己的作品,积累社交货币;
  • 给生活增加一些随机性;


避免像张颂文,满腹才华,却走了漫长的二十年。


你距离成功,也许只缺一个机会。


那就造一个出来。



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

this指向哪?

web
谁调用就指向谁 this 指向哪?先记住一句话:谁调用就指向谁,记住这句话就成功一大半了。 var userName = '张三' function fn() { var userName = '李四' console.log(this.use...
继续阅读 »

谁调用就指向谁


this 指向哪?先记住一句话:谁调用就指向谁,记住这句话就成功一大半了。


var userName = '张三'
function fn() {
var userName = '李四'
console.log(this.userName) // 张三
}
fn()

函数fn相当于挂在window,调用fn()等同于window.fn(),所以fn可以看做是被window调用,此时this便指向window,所以输出结果是函数外的userName变量。


再看下面的示例:


var userName = '张三'
var obj = {
userName: '李四',
fn: function() {
console.log(this.userName) // 李四
}
}

obj.fn()

函数fn被定义在对象obj里,通过obj.fn()调用,回到我们开头的那句话:谁调用就指向谁fnobj调用,所以this指向obj,输出结果是李四。


如果使用windowd.obj.fn()调用呢?结果还是一样的,因为最终是obj调用,所以this指向的还是obj


接着往下看下面这段代码:


var userName = '张三'
var obj = {
userName: '李四',
fn: function() {
console.log(this.name) // 张三
}
}

var func = obj.fn
func()

输出结果为张三,因为代码中只是将obj.fn赋值给func,此时函数fn还没有被调用,真正调用是在func()func挂在window下,所以最终相当于是window调用了函数fn,所以this指向的是window


还是应了那句话:谁调用就指向谁


其他情况


1、作为一个函数被直接调用


当函数被直接调用,没有挂在任何对象上时,this指向window


var userName = '张三'
var obj = {
userName: '李四',
fn: function() {
function a() {
console.log(this.userName) // 张三
}
a()
}
}
obj.fn()

这里在fn中定义了一个函数a并调用,注意这里函数a只是在函数fn内,它并没有挂在哪个对象上,也没有通过其他方式被调用,而是作为一个函数被直接调用,所以this指向window


2、箭头函数


箭头函数的特点是没有自己的this也不能通过new调用。箭头函数通过作用域向上查找,找到离包裹它最近的那一个普通函数的this作为自己的this,如果没有则指向全局对象window


var userName = '张三'
var obj = {
userName: '李四',
fn: function() {
var func = () => {
console.log(this.userName) // 李四
}
func()
},
a: {
b: () => {
console.log(this.userName) // 张三
}
}
}
obj.fn()
obj.a.b()

func是一个箭头函数,箭头函数通过向上查找找到fnfnthis指向obj,所以箭头函数的this指向的也是obj,输出结果:李四。


obj.a.b()则指向window,输出结果:张三。


3、new 构造函数


当函数通过使用new调用时,new过程中会创建一个新的对象,而this便是指向这个新创建的对象。


function fn(name) {
this.userName = name
this.age = 18
console.log(this) // {userName: '张三', age: 18}
}
let data = new fn('张三')
// new的同时内部会默认把this做回返回值返回
console.log('data: ', data) // data: {userName: '张三', age: 18}

如何改变this指向


1、call、apply、bind


可以通过callapplybind的第一个参数传值作为this值,改变this指向。


obj.fn('hello') // hello,张三
obj.fn.call({ userName: '李四' }, 'hello') // hello,李四
obj.fn.apply({ userName: '王五' }, ['hello']) // hello,王五
obj.fn.bind({ userName: '老六' }, 'hello')() // hello,老六

2、call、apply、bind区别


从上面的使用可以看到,这三个API的参数形式除了第一个参数外是有一些区别的



  • callbind是多参数的形式:call(thisArg, arg1, arg2...)

  • apply是数组的形式:call(thisArg, [arg1, arg2...])


另外一个区别是:bind的返回值是一个函数,需要自己再手动调用一次,而callapply<

作者:狂砍2分4篮板
来源:juejin.cn/post/7265534390506635319
/code>则不用。

收起阅读 »

孩子是双眼皮还是单眼皮?来自贝叶斯算法的推测

问题描述 最近家里有了宝宝,孩子他妈很希望孩子早日长出双眼皮,并因为他至今是单眼皮而有些担心。虽然我小时候也是单眼皮,后来才显现出双眼皮,但不排除孩子长大后仍是单眼皮的概率。为此我感到需要计算一下孩子是单眼皮基因的概率。 我家的情况是这样,宝爸宝妈、爷爷奶...
继续阅读 »

问题描述


最近家里有了宝宝,孩子他妈很希望孩子早日长出双眼皮,并因为他至今是单眼皮而有些担心。虽然我小时候也是单眼皮,后来才显现出双眼皮,但不排除孩子长大后仍是单眼皮的概率。为此我感到需要计算一下孩子是单眼皮基因的概率。


我家的情况是这样,宝爸宝妈、爷爷奶奶、姥姥姥爷都是双眼皮。


查了一下资料,双眼皮是显性基因,因此除非宝爸宝妈都是杂合性基因且都贡献单眼皮片段,孩子才能是单眼皮。


这里做一下假设,全部人群中有3/4是双眼皮,双眼皮人群中纯合基因有1/2。即



其中S表示双眼皮,D表示单眼皮,C表示纯合基因,Z表示杂合基因。


祖父辈人基因类型的后验概率


父母双方的情况是对等的,因此只挑选其中一方进行计算。以父亲为例,爷爷奶奶可能的基因类型组合有:CC,CZ和ZZ。先验概率为:



已知父亲是双眼皮,则爷爷奶奶是CC组合的后验概率为:



类似地可以算出



进行归一化后得到:



这里之所以要进行归一化,是因为在计算过程中对概念进行了替换,我们利用了爷爷奶奶都是双眼皮的信息。


父母基因类型的概率


仍然以父亲为对象进行计算,其是纯合基因的概率为:



是杂合基因的概率为:



剩下1/20的概率是表现为单眼皮的概率,需要排除掉。也就是说,在观察到爷爷奶奶父亲都是双眼皮的情况下,父亲是纯合基因的概率为






































,是杂合基因的概率为







































孩子双眼皮的概率


综合以上,孩子是双眼皮的概率为:



可见这个概率是很高的。


压力测试


在之前的计算里面,由于没有一般人群的统计数据,我们假设全部人群中有3/4是双眼皮,双眼皮人群中纯合基因有1/2。这里我们换一组数据,假设全部人群中有1/4是双眼皮,双眼皮人群中纯合基因有1/4,看看这样会对结果造成多大影响。


这种情况下



父母任意一方是纯合基因的概率为







































,是杂合基因的概率为









































最终得到孩子是双眼皮的概率是



可见概率依然非常高。“六个钱包”都是双眼皮是一个非常可靠的信号。


作者:Kelly1024
来源:mdnice.com/writing/f71210a4999f4bd2a674f9bead00551c
收起阅读 »

原来,我们的代码就是这样被污染的...

最近 CR 了这样一段代码: 我们团队的变量命名规范是小写驼峰,但是这里可以看到,一个 http api 接口请求的工具函数的入参却是下划线。这是一个内部项目,前后端都是我们团队开发的。这个项目的代码随处都能看到这样的不符合规范的痕迹,而且屡禁不止。我不禁在...
继续阅读 »

最近 CR 了这样一段代码:


插图1.png


我们团队的变量命名规范是小写驼峰,但是这里可以看到,一个 http api 接口请求的工具函数的入参却是下划线。这是一个内部项目,前后端都是我们团队开发的。这个项目的代码随处都能看到这样的不符合规范的痕迹,而且屡禁不止。我不禁在想,为什么会这样?


先探究一下这个问题的表面原因,让我们从底层开始:



  • DB 表结构按照团队规范,字段名是下划线分隔的

  • node 服务定义 entity 的时候,直接复用 DB 表结构字段名

  • service 模块在查询 DB 之后,直接回吐结果,所以 service 类返回的数据结构的变量名也是下划线的

  • node api 的 controller 通过调用 service 模块的函数,获取数据处理之后,也直接回吐下划线的变量名的数据结构

  • 因为 node api 的接口协议是下划线的,所以 web 页面请求 http api 的出入参也是下划线的,如上图所示

  • 因为 http api 返回的结果是下划线的,然后页面逻辑直接使用,最后,页面逻辑也出现了大量下划线


这是一段挺长的链路,只要我们在后面的任意一个环节处理一下变量名的转换,都能避免这个问题,但是并没有。


插图2.png


为什么会这样?稍微探究一下这里的深层原因,也是挺有意思的。


首先,项目启动时没有严格把控代码质量。这里的原因有很多,比如项目工期紧、主要以完成功能为主、内部项目不需要要求这么严格、项目启动时是直接 copy 另外一个项目的,那个项目也是这样写的等等。但是,这些统统都是借口!我是要负主要责任的。


其次,没有开发同学想过要去优化这里的代码。参与这个项目的同学并不是不知道如何去解决这个问题,但就是没人想要去解决这个问题。大家更多的是选择“入乡随俗”,别人这么写,我也这么写吧。


最后,Code Review 没有严抓。问题都是越早处理成本越小,如果在早期我们就开始严抓 Code Review 的话,说不定就能及时改善这个问题了。


这个案例是一个非常真实的破窗效应案例,这里面有不少地方值得我们深思的。


首先,它会污染整个项目。如果我们在项目的一开始就没有把控好代码质量,那我们的项目代码很快就会被污染。最开始的开发同学可能知道历史原因,自然不会觉得不自然。后续加入的维护者看到项目代码是这样子的,就会误以为这个项目的代码规范就是这样子的,于是也“入乡随俗”,最终整个项目的代码就被污染,破烂不堪。


其次,它会污染依赖的项目。比如这里就是 node 项目先被污染,对外提供的 api 也受到污染,然后调用这些 api 的 web 项目也被污染了。很多时候,api 的接口协议都是由后端开发来定,如果碰到一些缺乏经验的后端开发,前端同学会收到一些很奇怪的接口协议,比如字段命名规范不统一,冗余字段,设计不合理等等,由于前端的话语权较弱的缘故,很可能会被动接受,如果处理不当,前端项目的代码就会受到污染了。


最后,它会污染整个开发团队!初创的开发同学就不说了;后续的维护者看到代码是这样子的,不是错误认知这个项目的代码规范,就是错误认知团队的代码规范;更为糟糕的是,阅读项目源码的其他同学也会受到污染:“哦,原来别人也是这样写代码的”。噩梦由此而生...


插图3.png


破窗就像病毒一样,快速并疯狂地污染整个项目代码,然后传染给开发团队,最后扩散到其他团队。这个病毒传染的范围很广,速度很快,一不留神,整个团队就会沦陷。那我们应该怎样医治呢? 主要有 3 个方向:


首先,增强个人免疫力。形成个人的编码风格,然后坚持它,这是治本的良方。良好的编码风格是不会损坏个人的编码效率的,反而会有助于提升个人的研发效率,主要体现在以下几个方面:



  • 减少低级错误

  • 提升代码的可阅读性,提升代码的可维护性,从而提升团队协作效率

  • 形成“肌肉记忆”,提升编码效率

  • 避免返工,主要体现在糟糕的设计问题和编码风格冲突问题


其次,做好防护,避免传播。比如我们这个简单的案例,在整个链路的任何一个环节,都能轻松处理这个问题,这样就不会传播到后面的环节了。很多人碰到一些历史破窗代码时,可能会觉得修复这些破窗成本太大了,那就不要去修复,只需要保证自己写的新代码是良好的,也是一个不错的方案。已经中毒的人我们没有能力医治的时候,起码,我们可以认真做好防护,避免病毒进一步传播!


最后,对症下药医治。只有千日做贼,那有千日防贼,我们总是需要想办法医治这个病,而治病的药方就是重构。这里就不深入讲了,重构也是个大学问,只需要知道,越早重构,成本越低,比如在 Code Review 的时候就要严抓这些问题,并跟踪修复情况。


不知道,你的“免疫力”修炼得怎么样了?



【讨论问题】


除了这里的案例,还有很多其他类型的破窗,你都碰到了哪些?


欢迎在评论区分享你的想法,一起讨论。


作者:潜龙在渊灬
来源:juejin.cn/post/7252888158828642360
收起阅读 »

面试官:“只会这一种懒加载实现思路?回去等通知吧”

web
思路一:监听滚动事件 监听滚动事件指的是:通过监听页面的滚动事件,判断需要懒加载的元素是否进入可视区域。当元素进入可视区域时,动态加载对应的资源。这种方式需要手动编写监听滚动事件的逻辑,可能会导致性能问题,如滚动时的抖动和卡顿。 关键 API getBo...
继续阅读 »

思路一:监听滚动事件



监听滚动事件指的是:通过监听页面的滚动事件,判断需要懒加载的元素是否进入可视区域。当元素进入可视区域时,动态加载对应的资源。这种方式需要手动编写监听滚动事件的逻辑,可能会导致性能问题,如滚动时的抖动和卡顿。



关键 API



  • getBoundingClientRect() 方法返回的对象包含以下属性:



  • top:元素上边缘相对于视窗的距离。

  • right:元素右边缘相对于视窗的距离。

  • bottom:元素下边缘相对于视窗的距离。

  • left:元素左边缘相对于视窗的距离。

  • width:元素的宽度(可选)。

  • height:元素的高度(可选)。


可以看下下面的图。
image.png


来看看代码示例


html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lazy Loading Exampletitle>
<style>
img {
width: 100%;
height: 300px;
display: block;
}
style>
head>
<body>
<img data-src="https://via.placeholder.com/300x300?text=Image+1" src="" alt="Image 1">
<img data-src="https://via.placeholder.com/300x300?text=Image+2" src="" alt="Image 2">
<img data-src="https://via.placeholder.com/300x300?text=Image+3" src="" alt="Image 3">
<script>
function isInViewport(element) {
const rect = element.getBoundingClientRect();
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
const windowWidth = window.innerWidth || document.documentElement.clientWidth;

return (
rect.
top >= 0 &&
rect.
left >= 0 &&
rect.
bottom <= windowHeight &&
rect.
right <= windowWidth
);
}

function lazyLoad() {
const images = document.querySelectorAll('img[data-src]');
for (const img of images) {
if (isInViewport(img)) {
img.
src = img.getAttribute('data-src');
img.
removeAttribute('data-src');
}
}
}

window.addEventListener('scroll', lazyLoad);
window.addEventListener('resize', lazyLoad);
window.addEventListener('DOMContentLoaded', lazyLoad);

script>
body>
html>

这里的思路就相对简单了,核心思路就是判断这个元素在不在我的视口里面。


同样的思路下面还有另一种实现方式,是用 offsetTopscrollTopinnerHeight做的,这个很常见,我们就不说了。


思路二:Intersection Observer API



这是一种现代浏览器提供的原生 API,用于监控元素与其祖先元素或顶级文档视窗(viewport)的交叉状态。当元素进入可视区域时,会自动触发回调函数,从而实现懒加载。相比于监听滚动事件,Intersection Observer API 更高效且易于使用。



关键 API



  • Intersection Observer API:创建一个回调函数,该函数将在元素进入或离开可视区域时被调用。回调函数接收两个参数:entries(一个包含所有被观察元素的交叉信息的数组)和 observer(观察者实例)。


其中 entries 值得一提,entries 是一个包含多个 IntersectionObserverEntry 对象的数组,每个对象代表一个观察的元素(target element)与根元素(root element)相交的信息。IntersectionObserverEntry 对象包含以下属性:



  1. intersectionRatio: 目标元素和根元素相交区域占目标元素总面积的比例,取值范围为 0 到 1。

  2. intersectionRect: 目标元素和根元素相交区域的边界信息,是一个 DOMRectReadOnly 对象。

  3. isIntersecting: 布尔值,表示目标元素是否正在与根元素相交。

  4. rootBounds: 根元素的边界信息,是一个 DOMRectReadOnly 对象。

  5. target: 被观察的目标元素,即当前 IntersectionObserverEntry 对象所对应的 DOM 元素。

  6. time: 观察到的相交时间,是一个高精度时间戳,单位为毫秒。


这个最近我们组里的姐姐封了一个懒加载组件,通过单例模式 + Intersection Observer API,然后在外部控制 v-if 就好了,非常 nice。


由于代码保密性,我这里只能提供一个常规实现方法qwq


html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lazy Loading Example - Intersection Observertitle>
<style>
img {
width: 100%;
height: 300px;
display: block;
}
style>
head>
<body>
<img data-src="https://via.placeholder.com/300x300?text=Image+1" src="" alt="Image 1">
<img data-src="https://via.placeholder.com/300x300?text=Image+2" src="" alt="Image 2">
<img data-src="https://via.placeholder.com/300x300?text=Image+3" src="" alt="Image 3">
<script>
function loadImage(img) {
img.
src = img.getAttribute('data-src');
img.
removeAttribute('data-src');
}

function handleIntersection(entries, observer) {
entries.
forEach(entry => {
if (entry.isIntersecting) {
loadImage(entry.target);
observer.
unobserve(entry.target);
}
});
}

function lazyLoad()
script>
body>
html>


思路三:虚拟列表



对于长列表数据,可以使用虚拟列表技术来实现懒加载。虚拟列表只会渲染当前可视区域内的列表项,当用户滚动列表时,动态更新可视区域内的内容。这种方式可以大幅减少 DOM 节点的数量,提高页面性能。



这个我之前专门有文章聊过:b站面试官:如果后端给你 1w 条数据,你如何做展示?


就不赘述啦~


那么这就是本篇文章的全部内容啦~


作者:阳树阳树
来源:juejin.cn/post/7265329025899823159
收起阅读 »

年中总结:浑浑噩噩

学业 今年好像一直在虚假地忙碌,都是忙给别人看到,自己的进度并没有多少。 会议论文还在审稿,能不能过还是两说,除此之外好像没什么学业进度了。 每当有人问我:忙什么呢? 我就理直气壮地说,“赶论文呢”。 说得多了,自己也就信了,一旦自己都信了,就开始行色匆匆,...
继续阅读 »

学业


今年好像一直在虚假地忙碌,都是忙给别人看到,自己的进度并没有多少。


会议论文还在审稿,能不能过还是两说,除此之外好像没什么学业进度了。



每当有人问我:忙什么呢?


我就理直气壮地说,“赶论文呢”。


说得多了,自己也就信了,一旦自己都信了,就开始行色匆匆,一副“谁也别理我,忙着呢”的架势。……就是靠着这点虚张声势的忙碌,我活的了一种滥竽充数的成人感。




稀里糊涂掉进学术圈子里,好像一个瘸子掉到了一个舞池子里,偏偏又是一个要强的个性,没有科研天分,还要打肿脸充胖子,硬着头皮在这个舞池子里一瘸一拐地跳下去。



感情


又是单身的一年,近乎偏执的纯爱党。身边朋友嘲笑“你想和人从朋友做起,慢慢了解了再在一起,这种现在哪有啊!你只能遇到渣男!!!”


遇到喜欢的男孩子,我去追了,但是我太主动了,人家不喜欢主动的女孩子,其实主要是不喜欢我。最后不了了之了。但我依旧认为,



“女生太主动了,是不是不太好?”


“没什么好不好的,爱情本来就没有什么公式。”



找过很多借口,帮助自己走出来。但是说来说去还是真心喜欢过才会找那么多的借口。



“你说会不会是这样,我其实也并不喜欢他,喜欢的不过是自己的面子?


“也许吧。”但这又说明什么?你可以说“我喜欢的不是他,而是自己的面子”,或者“我喜欢的不是他,我只是在逃避孤独”,或者,“我喜欢的不是真正的他,而是一个想象出来的他” “我喜欢的不是他,而是被人疼爱的那种感觉”·····这样的句子可以无限造下去,但结果殊途同归,就是你在乎。



生活


因为身体和学业问题,一度陷入焦虑。每天过的很差劲,不断内耗。


不想给朋友们倾诉,在他们眼里我永远都是那个元气少女。身边人都说“谁抑郁了,你都不可能抑郁。”


但是那段时间我真的是很难过很难过。我之前也是这样,但是把所有的负面情绪都给了我喜欢的男孩子。可能让人家觉得我就是鱼鱼女孩,其实我并不是。我觉得是我的错,我不该一直跟他倾诉我压力有多大。



传递悲伤这件事本身,就是一种暴力。悲伤不该特地说给别人听。



以至于后来,我真的很舍不得,但是我什么都不敢和他说。我怕这样的想念没有诚意,我怕他觉得我把他当作垃圾桶。所以只能自己解决。



人生若有知己相伴固然妙不可言,但那可遇而不可求,真的,也许既不可遇又不可求,可求的只有你自己,你要俯下身去,朝着幽暗深处的自己伸出手去。



读书


最近摆烂中,最近三个月只看了七本书。是时候放下手机重拾阅读兴趣了。


我是个特别喜欢看爱情小说的人,喜欢看别人的爱情观。但是今年上半年看的微乎其微,主要都是健康、社会、个人成长相关的内容。


比如《中国营养科学全书》,比如《孤独社会》等等。至于小说,国内的作家的只看了刘震云的《一句顶一万句》,我果然是不太喜欢国内的小说,太过沉重了。比如:



一开始觉得没有话说是两人不爱说话,后来发现不爱说话和没话说是两回事。不爱说话是心里还有话,没话说是干脆什么都没有了。




将心腹话说给朋友,没想到朋友一掰,这些自己说过的话,都成了刀子,反过头扎向自己。



推荐一本八嘎国的小说《星期一,喝抹茶》。没什么营养内容,就是适合消磨时间的那种小短文。每一个小故事的配角都会成为下一个小故事的主角,透露着生活中的小确幸。



“不用配合她呀”——我想起了富贵子刚才说的话。她确实没有配合我,她一直坚守着自己认准的道路,并且尊重了我的选择。



展望


生活都是你自己的,自己经历,自己成长,后果也要自己承担。不要再这么浑浑噩噩地过下去了。先做应该做的,

作者:Roche
来源:juejin.cn/post/7265210393046810664
再做喜欢做的事情吧。

收起阅读 »

动态样式去哪儿了?

web
本文作者是蚂蚁集团前端工程师豆酱,众所周知,antd v5 使用了 CSS-in-JS 技术从而支持混合、动态样式的需求。相对的它需要在运行时生成样式,这会造成一定的性能损耗。因此我们研发了组件库级别的 @ant-design/cssinjs 库,通过一定的约...
继续阅读 »

本文作者是蚂蚁集团前端工程师豆酱,众所周知,antd v5 使用了 CSS-in-JS 技术从而支持混合、动态样式的需求。相对的它需要在运行时生成样式,这会造成一定的性能损耗。因此我们研发了组件库级别的 @ant-design/cssinjs 库,通过一定的约束提升缓存效率,从而达到性能优化的目的。不过我们并不止步于此。我们可以通过一些逻辑,直接跳过运行时生成样式的阶段。


动态样式去哪儿了?


如果你研究过 Ant Design 的官网,你会发现 Ant Design 的组件并没有动态插入 来控制样式,而是通过 CSS 文件来控制样式:

image.png


image.png


document.head 里有几个 css 文件引用:



  • umi.[hash].css

  • style-acss.[hash].css


前者为 dumi 生成的样式内容,例如 Demo 块、搜索框样式等等。而后者则是 SSR 生成的样式文件。在定制主题文档中,我们提过可以通过整体导出的方式将页面中用到的组件进行预先烘焙,从而生成 css 文件以供缓存命中从而提升下一次打开速度。这也是我们在官网中使用的方式。所以 Demo 中的组件,其实就是复用了这部分样式。 等等!CSS-in-JS 不是需要在运行时生成样式的 hash 然后通过 进行对齐的么?为什么 css 文件也可以对齐?不用着急,我们慢慢看。

CSS-in-JS 注水


应用级的 CSS-in-JS 方案会对生成的样式计算出 hash 值,并且将其存入 Cache 中。当下次渲染时,会先从 Cache 中查找是否存在对应的样式,如果存在则直接使用,否则再生成一次。这样就可以避免重复生成样式,从而提升性能。


image.png


每个动态插入到页面中的样式同样以为 hash 作为唯一标识符。如果页面中已经存在该 hash 的 ,则说明 SSR 中做过 inline style 注入。那么 就不用再次创建。 你可以发现,虽然 的节点创建可以省略,但是因为 hash 依赖于计算出的样式内容。所以即便页面中已经有可以复用的样式内容,它仍然免不了需要计算一次。实属不划算。

组件级 CSS-in-JS


组件级别的 CSS-in-JS 一文中,我们提过。Ant Design 的 Cache 机制并不需要计算出完整的样式。对于组件库而言,只要通过 Token 和 ComponentName 就可以确定生成样式一致性,所以我们可以提前计算出 hash 值: 也因此,我们发现可以复用这套机制,实现在在客户端侧感知组件样式是否已经注入过。


SSR HashMap


在 @ant-design/cssinjs 中,Cache 本身包含了每个元素对应的 style 和 hash 信息。过去的 extractStyle 方法只取 Cache 中 style 的内容进行封装:


// e.g. Real world path is much more complex

{

  "bAMbOo|Button": ["LItTlE"":where(.bAMbOo).ant-btn { color: red }"],

  "bAMbOo|Spin": ["liGHt"":where(.bAMbOo).ant-spin { color: blue }"]

}

提取:


:where(.bAMbOo).ant-btn {

  color: red;

}

:where(.bAMbOo).ant-spin {

  color: blue;

}

为了复用样式,我们更进一步。将 path 和 hash 值也进行了抽取:


{

  "bAMbOo|Button": "LItTlE",

  "bAMbOo|Spin""liGHt"

}

并且也打成 css 样式:


// Just example. Not real world code

.cssinjs-cache-path {

  content'bAMbOo|Button:LItTlE;bAMbOo|Spin:liGHt';

}

这样 SSR 侧就将我们所需的信息全部留存了下来,接下去只需要在客户端进行提取即可。


CSR HashMap


在客户端则简单的多,我们通过 getComputedStyle 提取 HashMap 信息留存即可:


// Just example. Not real world code

const measure = document.createElement('div');

measure.className = 'cssinjs-cache-path';

document.body.appendChild(measure);

 

// Now let's parse the `content`

const { content } = getComputedStyle(measure);

在组件渲染阶段,useStyleRegister 在计算 CSS Object 之前,会先在 HashMap 中查找 path 是否存在。如果存在,则说明该数据已经通过服务端生成。我们只需要将样式从现有的 里提取出来即可:

// e.g. Real world path is much more complex

{

  "bAMbOo|Button": ["LItTlE""READ_FROM_INLINE_STYLE"],

  "bAMbOo|Spin": ["liGHt""READ_FROM_INLINE_STYLE"]

}

而对于 CSS 文件提供的样式(比如官网的使用方式),它不像 会被移除,我们直接标记为来自于 CSS 文件即可。和 inline style 一样,它们会在 useInsertionEffect 阶段被跳过。

// e.g. Real world path is much more complex

{

  "bAMbOo|Button": ["LItTlE""__FROM_CSS_FILE__"],

  "bAMbOo|Spin": ["liGHt""__FROM_CSS_FILE__"]

}

总结


CSS-in-JS 因为运行时的性能损耗而被人诟病。而在 Ant Design 中,如果你的应用使用了 SSR,那么在客户端侧就可以直接跳过运行时生成样式的阶段从而提升性能。当然,我们会继续跟进 CSS-in-JS 的发展,

作者:支付宝体验科技
来源:juejin.cn/post/7265249262329839672
为你带来更好的体验。

收起阅读 »

如何去除图片马赛克?

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究! 一、前言 对图像不了解的人时常妄想去除马赛克是可以实现的,严格意义来说这确实是无法实现的。而深度学习是出现,让去除马赛克成为可能。 为了理解去除马赛克有多难,我们需要...
继续阅读 »

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!



一、前言


对图像不了解的人时常妄想去除马赛克是可以实现的,严格意义来说这确实是无法实现的。而深度学习是出现,让去除马赛克成为可能。


为了理解去除马赛克有多难,我们需要知道马赛克是什么。观感上,马赛克就是方块感。当我们观察图像像素时, 马赛克表现为下图的情况:



原图右下角有十字,而添加马赛克后右下角一片都变成了同一像素,如果我们没保留原图,那么我们无法还原,也不知道是否还原了原图。因为原图已经被破坏了,这也是为什么马赛克是不可修复的。


那神经网络又是如何让修复成为可能呢?其实无论什么方式的修复,都是一种估计,而不是真正的修复。神经网络去除马赛克的操作其实是生成马赛克那部分内容,然后替代马赛克,从而达到修复的效果。


这种修复并不是还原,而是想象。假如我们对一张人脸打了马赛克,神经网络可以去除马赛克,但是去除后的人脸不再是原来那个人了。


二、实现原理


2.1 自编码器


图像修复的方法有很多,比如自编码器。自编码器是一种自监督模型,结构简单,不需要人为打标,收敛迅速。其结构如图:



编码器部分就是用于下采样的卷积网络,编码器会把图片编码成一个向量,而解码器则利用转置卷积把编码向量上采样成和原图大小一致的图片,最后我们把原图和生成结果的MSE作为损失函数进行优化。当模型训练好后,就可以用编码器对图片进行编码。


2.2 自编码器去除马赛克


那自编码器和去除马赛克有什么联系呢?其实非常简单,就是原本我们是输入原图,期望解码器能输出原图。这是出于我们希望模型学习如何编码图片的原图。而现在我们想要模型去除马赛克,此时我们要做的就是把马赛克图片作为输入,而原图作为输出,这样来训练就可以达到去除马赛克的效果了:



关于关于这种实现可以参考:juejin.cn/post/721068…


2.3 自编码器的问题


自编码器有个很明显的问题,就是图片经过编码器后会损失信息,而解码器的结果自然也会存在一些问题。这样既达不到去除马赛克的功能,连还原的原图都有一些模糊。


这里可以利用FPN的思想来改进,当自编码器加入FPN后,就得到了UNet网络结构。


2.4 UNet网络


UNet结构和自编码器类似,是一个先下再上的结构。和自编码器不同的时,UNet会利用编码器的每个输出,将各个输出与解码器的输入进行concatenate,这样就能更好地保留原图信息。其结构如下图:



UNet原本是用于图像分割的网络,这里我们用它来去除马赛克。


在UNet中,有几个部分我们分别来看看。


2.4.1 ConvBlock


在UNet中,有大量连续卷积的操作,这里我们作为一个Block(蓝色箭头),它可以实现为一个层,用PyTorch实现如下:


class ConvBlock(nn.Module):
def __init__(self, in_channels, out_channels):
super().__init__()
self.model = nn.Sequential(
nn.Conv2d(in_channels, out_channels, 3, 1, 1),
nn.BatchNorm2d(out_channels),
nn.ReLU(),
nn.Conv2d(out_channels, out_channels, 3, 1, 1),
nn.BatchNorm2d(out_channels),
nn.ReLU()
)

def forward(self, inputs):
return self.model(inputs)

这里其实就是两次卷积操作,这里的目的是提取当前感受野的特征。


2.4.2 ConvDown


经过连续卷积后,会使用卷积网络对图片进行下采样,这里把stride设置为2即可让图片缩小为原来的1/2。我们同样可以实现为层:


class ConvDown(nn.Module):
def __init__(self, channels):
super().__init__()
self.model = nn.Sequential(
nn.Conv2d(channels, channels, 3, 2, 1),
nn.BatchNorm2d(channels),
nn.ReLU()
)

def forward(self, inputs):
return self.model(inputs)

这里只有一个卷积,而且stride被设置为了2。


2.4.3 ConvUp


接下来是解码器部分,这里多了一个上采用的操作,我们可以用转置卷积完成,代码如下:


class ConvUp(nn.Module):
def __init__(self, channels):
super().__init__()
self.model = nn.Sequential(
nn.ConvTranspose2d(channels, channels // 2, 2, 2),
nn.BatchNorm2d(channels // 2),
nn.ReLU()
)

def forward(self, inputs):
return self.model(inputs)

上面是层可以把图片尺寸扩大为2倍,同时把特征图数量缩小到1/2。这里缩小特征图的操作是为了concatenate操作,后面详细说。


三、完整实现


首先,导入需要用的模块:


import os
import random
import torch
from torch import nn
from torch import optim
from torch.utils import data
import matplotlib.pyplot as plt
from torchvision import transforms
from torchvision.transforms import ToTensor
from PIL import Image, ImageDraw, ImageFilter
from torchvision.utils import make_grid

下面开始具体实现。


3.1 创建Dataset


首先创建本次任务需要的数据集,分布大致相同的图片即可,代码如下:


class ReConstructionDataset(data.Dataset):
def __init__(self, data_dir=r"G:/datasets/lbxx", image_size=64):
self.image_size = image_size
# 图像预处理
self.trans = transforms.Compose([
transforms.Resize(image_size),
transforms.CenterCrop(image_size),
transforms.ToTensor(),
# transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
# 保持所有图片的路径
self.image_paths = []
# 读取根目录,把所有图片路径放入image_paths
for root, dirs, files in os.walk(data_dir):
for file in files:
self.image_paths.append(os.path.join(root, file))

def __getitem__(self, item):
# 读取图片,并预处理
image = Image.open(self.image_paths[item])
return self.trans(self.create_blur(image)), self.trans(image)

def __len__(self):
return len(self.image_paths)


@staticmethod
def create_blur(image, return_mask=False, box_size=200):
mask = Image.new('L', image.size, 255)
draw = ImageDraw.Draw(mask)
upper_left_corner = (random.randint(0, image.size[0] - box_size), random.randint(0, image.size[1] - box_size))
lower_right_corner = (upper_left_corner[0] + box_size, upper_left_corner[1] + box_size)
draw.rectangle([lower_right_corner, upper_left_corner], fill=0)
masked_image = Image.composite(image, image.filter(ImageFilter.GaussianBlur(15)), mask)
if return_mask:
return masked_image, mask
else:
return masked_image

Dataset的实现与以往基本一致,实现init、getitem、len方法,这里我们还实现了一个create_blur方法,该方法用于生成矩形马赛克(实际上是高斯模糊)。下面是create_blur方法生成的图片:



3.2 网络构建


这里我们需要使用前面的几个子单元,先实现编码器,代码如下:


class UNetEncoder(nn.Module):
def __init__(self):
super().__init__()
self.blk0 = ConvBlock(3, 64)
self.down0 = ConvDown(64)
self.blk1 = ConvBlock(64, 128)
self.down1 = ConvDown(128)
self.blk2 = ConvBlock(128, 256)
self.down2 = ConvDown(256)
self.blk3 = ConvBlock(256, 512)
self.down3 = ConvDown(512)
self.blk4 = ConvBlock(512, 1024)

def forward(self, inputs):
f0 = self.blk0(inputs)
d0 = self.down0(f0)
f1 = self.blk1(d0)
d1 = self.down1(f1)
f2 = self.blk2(d1)
d2 = self.down2(f2)
f3 = self.blk3(d2)
d3 = self.down3(f3)
f4 = self.blk4(d3)
return f0, f1, f2, f3, f4

这里就是ConvBlok和ConvDown的n次组合,最终会得到一个1024×4×4的特征图。在forward中,我们返回了5个ConvBlok返回的结果,因为在解码器中我们需要全部使用。


接下来是解码器部分,这里与编码器相反,代码如下:


class UNetDecoder(nn.Module):
def __init__(self):
super().__init__()
self.up3 = ConvUp(1024)
self.blk3 = ConvBlock(1024, 512)
self.up2 = ConvUp(512)
self.blk2 = ConvBlock(512, 256)
self.up1 = ConvUp(256)
self.blk1 = ConvBlock(256, 128)
self.up0 = ConvUp(128)
self.blk0 = ConvBlock(128, 64)
self.last_conv = nn.Conv2d(64, 3, 3, 1, 1)

def forward(self, inputs):
f0, f1, f2, f3, f4 = inputs
u3 = self.up3(f4)
df2 = self.blk3(torch.concat((f3, u3), dim=1))
u2 = self.up2(df2)
df1 = self.blk2(torch.concat((f2, u2), dim=1))
u1 = self.up1(df1)
df0 = self.blk1(torch.concat((f1, u1), dim=1))
u0 = self.up0(df0)
f = self.blk0(torch.concat((f0, u0), dim=1))
return torch.tanh(self.last_conv(f))

解码器的inputs为编码器的5组特征图,在forward时需要与上采样结果concatenate。


最后,整个网络组合起来,代码如下:


class ReConstructionNetwork(nn.Module):

def __init__(self):
super().__init__()
self.encoder = UNetEncoder()
self.decoder = UNetDecoder()

def forward(self, inputs):
fs = self.encoder(inputs)
return self.decoder(fs)

3.3 网络训练


现在各个部分都完成了,可以开始训练网络:


device = "cuda" if torch.cuda.is_available() else "cpu"


def train(model, dataloader, optimizer, criterion, epochs):
model = model.to(device)
for epoch in range(epochs):
for iter, (masked_images, images) in enumerate(dataloader):
masked_images, images = masked_images.to(device), images.to(device)
outputs = model(masked_images)
loss = criterion(outputs, images)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if (iter + 1) % 100 == 1:
print("epoch: %s, iter: %s, loss: %s" % (epoch + 1, iter + 1, loss.item()))
with torch.no_grad():
outputs = make_grid(outputs)
img = outputs.cpu().numpy().transpose(1, 2, 0)
plt.imshow(img)
plt.show()
torch.save(model.state_dict(), '../outputs/reconstruction.pth')


if __name__ == '__main__':
dataloader = data.DataLoader(ReConstructionDataset(r"G:\datasets\lbxx"), 64)
unet = ReConstructionNetwork()
optimizer = optim.Adam(auto_encoder.parameters(), lr=0.0002)
criterion = nn.MSELoss()
train(unet, dataloader, optimizer, criterion, 20)

训练完成后,就可以用来去除马赛克了,代码如下:



dataloader = data.DataLoader(ReConstructionDataset(r"G:\datasets\lbxx"), 64, shuffle=True)
unet = ReConstructionNetwork().to(device)
unet.load_state_dict(torch.load('../outputs/reconstruction.pth'))
for masked_images, images in dataloader:
masked_images, images = masked_images.to(device), images.to(device)
with torch.no_grad():
outputs = unet(masked_images)
outputs = torch.concatenate([images, masked_images, outputs], dim=-1)
outputs = make_grid(outputs)
img = outputs.cpu().numpy().transpose(1, 2, 0)
img = cv2.normalize(img, , 0, 255, cv2.NORM_MINMAX, cv2.CV_8U)
Image.fromarray(img).show()


下面是生成结果。左侧为原图,中间为添加马赛克后的图片,右侧则是去除马赛克后的结果:



整体来说效果比较不错。本文的方法不只可以用来去除马赛克,还可以完成图像重构。比如老化的图片、被墨汁污染的图片等,都可以用本文的方法完成重构。另外,本文的数据有限,实现效果并不通用,有需求的读者可以移步CodeFormer项目:github.com/sczhou/Code…


作者:ZackSock
来源:juejin.cn/post/7265310874488520765
收起阅读 »

iOS设置圆角后阴影不显示

iOS
问题 设计图中View有阴影和圆角,里面填充了四个按钮。同时设置View的圆角和阴影,阴影并不显示。试了很多次,找了很多办法,记录一下过程。 为了展示父视图的圆角,设置了masksToBounds=YES,将超出父视图的内容clip掉,这样圆角就OK了。不设置...
继续阅读 »

问题


设计图中View有阴影和圆角,里面填充了四个按钮。同时设置View的圆角和阴影,阴影并不显示。试了很多次,找了很多办法,记录一下过程。


为了展示父视图的圆角,设置了masksToBounds=YES,将超出父视图的内容clip掉,这样圆角就OK了。不设置时,Subview会超出父视图,看起来像圆角没有设置成功。


接着给父视图设置Shadow却没有成功。一开始以为是代码的问题,写了很多次,数值啊颜色啊大小啊都设置的很大,却还是不显示。百度了一下,提示阴影为超出父视图的部分,如果使用masksToBounds会把超出部分切掉,但去掉masksToBounds会导致圆角失效。


解决方案


给View外面套一层ShadowView,把阴影加到ShadowView上,不设置masksToBounds,再设置真正需要圆角的View,设置masksToBounds即可。

self.shadowView.layer.cornerRadius = 50;
self.shadowView.layer.shadowOffset = CGSizeMake(1, 5);
self.shadowView.layer.shadowOpacity = 0.8;
self.shadowView.layer.shadowColor = [UIColor lightGrayColor].CGColor;
self.actionView.layer.cornerRadius = 50;
self.actionView.layer.masksToBounds = YES;

设置没有SubView的View时并不需要设置masksToBounds属性就可以同时拥有圆角和阴影,但有SubView时为了不让它超出父视图内容(如果都为白色的话超出看着特别像设置没有成功的样子)就必须设置masksToBounds属性了,才会有上面的问题。


Demo


写了一个Demo,展示一下不同的设置效果,给大家参考。
github.com/Yadea-Web/R…

1.第一个View是没有Subview的情况,直接设置cornerRadius即可,不需要设置masksToBounds属性。

2.第二个View有一个子View,不设置masksToBounds属性显示阴影但圆角没生效,因为子视图超出了。

3.第三个View有一个子View,设置masksToBounds属性圆角生效了,但阴影消失了,因为把超出部分截掉了。

4.第四个View有一个子View,设置View的阴影,不设置masksToBounds。设置Subview的cornerRadius,再设置masksToBounds属性。Subview将超出部分截掉,外层展示它应该展示的阴影效果。

5.第五第六展示的是有多个子控件的情况,与三四类似,详见Demo。



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

使用 AndroidX 增强 WebView 的能力

在应用开发过程中,为了在多个平台上保持一致的用户体验和提高开发效率,许多应用程序选择使用 H5 技术。在 Android 平台上,通常使用 WebView 组件来承载 H5 内容以供展示。 WebView 存在的问题 自 Android Lollipop 起,...
继续阅读 »

在应用开发过程中,为了在多个平台上保持一致的用户体验和提高开发效率,许多应用程序选择使用 H5 技术。在 Android 平台上,通常使用 WebView 组件来承载 H5 内容以供展示。


WebView 存在的问题


自 Android Lollipop 起,WebView 组件的升级已经独立于 Android 平台。然而,控制 WebView 的 API(android.webkit) 仍然与平台升级相关。这意味着应用开发者只能使用当前平台所定义的接口,而无法充分利用 WebView 的全部能力。例如: WebView.startSafeBrowsing API 在 Android 8.1 上被添加,该 Feature 由 WebView 提供,即使我们在 Android 7.0 更新 WebView 拥有了该 Feature ,由于 Android 7.0 没有 WebView.startSafeBrowsing API ,我们也没办法使用该功能。


WebView 的实现基于 Chromium 开源项目,而 Android 则基于 AOSP 项目,这两个项目有着不同的发布周期,WebView 往往一个月就可以推出下一个版本,而 Android 则需要一年的时间,对于 WebView 新增的 Feature 我们最迟需要一年才能使用。


AndroidX Webkit 的出现


为了解决上面平台能力和 WebView 不匹配的问题,我们可以独立于平台之外定义一套 WebView API ,并让它随着 WebView 的 Feature 更新 API ,这样解决了现有的问题却导入了另一个问题——如何将新定义的 WebView API 和 WebView 进行衔接。


从应用开发的角度,系统 WebView 难以修改,自己编译定制一个 WebView 并随着 apk 提供是一个很好方案。这时候,我们可以轻松的解决衔接问题,并能够按照需求,任意增改 Feature 而不必等官方更新。同时解决了兼容问题和 WebView 内核碎片化的问题。腾讯 X5 ,UC U4 等都是这个方案。维护一份 WebView 并不是一件容易的事,需要投入更多的人力支持,因为将 WebView 打入包中,还伴随着包体积的急剧增加。


从 Android 官方的角度,可以推动 WebView 上游支持该 WebView API , 而这正是 AndroidX Webkit 的解决方案。Android 官方将定义的 WebView API 放置到 AndroidX Webkit 库,以支持频繁的更新,并在 WebView 上游增加“胶水层”与 AndroidX Webkit 进行衔接,这样在旧版的 Android 平台上,只要安装了拥有"胶水"层代码的 WebView ,也就拥有了新版平台的功能。



“胶水层” 是在某个版本之后才后才支持的,旧版本的 WebView 内核并不支持,这也是为什么在调用之前始终应该检查 isFeatureSupported 的原因。



AndroidX Webkit 的功能


初步了解了 AndroidX Webkit 的产生和实现原理,下面带领大家看一下它都提供了哪些新能力能够增强我们的 WebView 。


向下兼容


如上文分析,AndroidX Webkit 提供了向下的兼容,如下面代码所示,由 WebViewCompat 提供兼容的接口调用。



需要注意的是在调用之前对 WebViewFeature 的检查,对于每个 Feature ,AndroidX Webkit 会取平台和 WebView 所提供 Feature 的并集 ,在调用某个 API 之前必须进行检查,如果平台和 WebView 均不支持该API则将抛出 UnsupportedOperationException 异常。

// Old code:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
   WebView.startSafeBrowsing(appContext, callback);
}

// New code:
if (WebViewFeature.isFeatureSupported(WebViewFeature.START_SAFE_BROWSING)) {
   WebViewCompat.startSafeBrowsing(appContext, callback);
}

如果我们扒开 WebViewCompat 的外衣查看他的源码(如下所示),会发现如果在当前版本 Platform API 提供了接口,就会直接调用 Platform API 的接口,而对于低版本,则由 AndroidX Webkit 和 WebView 的"通道"提供服务。

// WebViewCompat#startSafeBrowsing
public static void startSafeBrowsing(@NonNull Context context,
@Nullable ValueCallback<Boolean> callback) {
ApiFeature.O_MR1 feature = WebViewFeatureInternal.START_SAFE_BROWSING;
if (feature.isSupportedByFramework()) {
ApiHelperForOMR1.startSafeBrowsing(context, callback);
} else if (feature.isSupportedByWebView()) {
getFactory().getStatics().initSafeBrowsing(context, callback);
} else {
throw WebViewFeatureInternal.getUnsupportedOperationException();
}
}

对比上面的代码,使用平台 API(old code)时仅可以支持 90% 的用户,而使用 AndroidX Webkit(new code) 则可以覆盖大约 99% 的用户。


代理功能支持


一直以来WebView 的代理设置异常繁琐,当遇到复杂的代理规则就无能为力了。在 AndroidX Webkit 中增加了 ProxyController API 用于为 WebView 设置代理。ProxyConfig.Builder 类提供了设置代理以及配置代理的绕过方式等方法,通过组合可以满足复杂的代理场景。

if (WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) {
ProxyConfig proxyConfig = new ProxyConfig.Builder()
.addProxyRule("localhost:7890") //添加要用于所有 URL 的代理
.addProxyRule("localhost:1080") //优先级低于第一个代理,仅在上一个失败时应用
.addDirect() //当前面的代理失败时,不使用代理直连
.addBypassRule("www.baidu.com") //该网址不使用代理,直连服务
.addBypassRule("*.cn") //以.cn结尾的网址不使用代理
.build();
Executor executor = ...
Runnable listener = ...
ProxyController.getInstance().setProxyOverride(proxyConfig, executor, listener);

以上代码定义了一个复杂的代理场景,我们为 WebView 设置了两个代理服务器,localhost:1080 仅当 localhost:7890 失败的情况下启用,addDirect 声明了如果两个服务器都失败则直连服务器,addBypassRule 规定了 http://www.baidu.com 和以 .so 结尾的域名始终不应该使用代理。


白名单代理


如果仅有少量的 URL 需要配置代理,我们可以使用 setReverseBypassEnabled(true) 方法将addBypassRule 添加的 URL 转变为使用代理服务器,而其他的 URL 则直连服务。


安全的 WebView 和 Native 通信支持


建立 WebView 和 Native 的双向通信是使用 Hybrid 混合开发模式的基础,在之前 Android 已经提供了一些机制能够让完成基本的通信,但是已有的接口都存在一些安全和性能问题,在 AndroidX 中增加了一个功能强大的接口 addWebMessageListener 兼顾了安全和性能等问题。


代码示例中将 JavaSript 对象 replyObject 注入到匹配 allowedOriginRules的上下文中,这样只有在可信的网站中才能被使用此对象,也就防止了不明来源的网络攻击者对该对象的利用。

// App (in Java)
WebMessageListener myListener = new WebMessageListener() {
  @Override
  public void onPostMessage(WebView view, WebMessageCompat message, Uri sourceOrigin,
           boolean isMainFrame, JavaScriptReplyProxy replyProxy) {
    // do something about view, message, sourceOrigin and isMainFrame.
    replyProxy.postMessage("Got it!");
  }
};

HashSet<String> allowedOriginRules = new HashSet<>(Arrays.asList("[https://example.com](https://example.com/)"));
// Add WebMessageListeners.

WebViewCompat.addWebMessageListener(webView, "replyObject", allowedOriginRules,myListener);
调用上述方法之后,在 JavaScript 上下文中我们就可以访问 myObject ,调用 postMessage 就可以回调 Native 端的 onPostMessage 方法并自动切换到主线程执行,当 Native 端需要发送消息给 WebView 时,可以通过 JavaScriptReplyProxy.postMessage 发送到 WebView ,并将消息传递给 onmessage 闭包。
// Web page (in JavaScript)
myObject.onmessage = function(event) {
  // prints "Got it!" when we receive the app's response.
  console.log(event.data);
}
myObject.postMessage("I'm ready!");

文件传递


在以往的通讯机制中,如果我们想传递一个图片只能将其转换为 base64 等进行传输,如果曾经使用过 shouldOverrideUrlLoading 拦截 url 大概率会遇见传输瓶颈,AndroidX Webkit 中很贴心的提供了字节流传递机制。


Native 传递文件给 WebView

// App (in Java)
WebMessageListener myListener = new WebMessageListener() {
  @Override
  public void onPostMessage(WebView view, WebMessageCompat message, Uri sourceOrigin,
           boolean isMainFrame, JavaScriptReplyProxy replyProxy) {
    // Communication is setup, send file data to web.
    if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER)) {
      // Suppose readFileData method is to read content from file.
      byte[] fileData = readFileData("myFile.dat");
      replyProxy.postMessage(fileData);
    }
  }
}
// Web page (in JavaScript)
myObject.onmessage = function(event) {
  if (event.data instanceof ArrayBuffer) {
    const data = event.data;  // Received file content from app.
    const dataView = new DataView(data);
    // Consume file content by using JavaScript DataView to access ArrayBuffer.
  }
}
myObject.postMessage("Setup!");

WebView 传递文件给 Native

// Web page (in JavaScript)
const response = await fetch('example.jpg');
if (response.ok) {
    const imageData = await response.arrayBuffer();
    myObject.postMessage(imageData);
}
// App (in Java)
WebMessageListener myListener = new WebMessageListener() {
  @Override
  public void onPostMessage(WebView view, WebMessageCompat message, Uri sourceOrigin,
           boolean isMainFrame, JavaScriptReplyProxy replyProxy) {
    if (message.getType() == WebMessageCompat.TYPE_ARRAY_BUFFER) {
      byte[] imageData = message.getArrayBuffer();
      // do something like draw image on ImageView.
    }
  }
};

深色主题的支持


Android 10 提供了深色主题的支持,但是在 WebView 中显示的网页却不会自动显示深色主题, 这就表现出严重的割裂感,开发者只能通过修改 css 来达到目的,但这往往费时费力还存在兼容性问题,Android 官方为了改善这一用户体验,为 WebView 提供了深色主题的适配。


一个网页如何表现是和prefers-color-scheme and color-scheme 这两个 Web 标准互操作的。 Android官方提供了一张表阐述了他们之间的关系。


上面这张图比较复杂,简单来说如果你想让 WebView 的内容和应用的主题相匹配,你应该始终定义深色主题并实现 prefers-color-scheme ,而对于未定义 prefers-color-scheme 的页面,系统按照不同的策略选择算法生成或者显示默认页面。



以 Android 12 或更低版本为目标平台的应用 API 设计过于复杂,以 Android 13 或更高版本为目标平台的应用精简了 API ,具体变更请参考官方文档



JavaScript and WebAssembly 执行引擎支持


我们有时候我们会在程序中运行 JavaScript 而不显示任何 Web 内容,比如小程序的逻辑层,使用 WebView 本能够满足我们的要求但是浪费了过多的资源,我们都知道在 WebView 中真正负责执行 JavaScript 的引擎是 V8 ,但是我们又无法直接使用,所以我们的安装包中出现了各种各样的引擎:HermesJSCV8等。


Android 发现了这”群雄割据“的局面,推出了AndroidX JavascriptEngine,JavascriptEngine 直接使用了 WebView 的 V8 实现,由于不用分配其他 WebView 资源所以资源分配更低,并可以开启多个独立运行的环境,还针对传递大量数据做了优化。


代码展示了执行 JavaScript 和 WebAssembly 代码的使用:

if(!JavaScriptSandbox.isSupported()){
return;
}
//连接到引擎
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceAsync(context);
//创建上下文 上下文间有简单的数据隔离
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate();
//执行函数 && 获取结果
final String code = "function sum(a, b) { let r = a + b; return r.toString(); }; sum(3, 4)";
ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code);
String result = resultFuture.get(5, TimeUnit.SECONDS);
Futures.addCallback(resultFuture,
new FutureCallback<String>() {
@Override
public void onSuccess(String result) {
text.append(result);
}
@Override
public void onFailure(Throwable t) {
text.append(t.getMessage());
}
},
mainThreadExecutor); //Wasm运行
final byte[] hello_world_wasm = {
0x00 ,0x61 ,0x73 ,0x6d ,0x01 ,0x00 ,0x00 ,0x00 ,0x01 ,0x0a ,0x02 ,0x60 ,0x02 ,0x7f ,0x7f ,0x01,
0x7f ,0x60 ,0x00 ,0x00 ,0x03 ,0x03 ,0x02 ,0x00 ,0x01 ,0x04 ,0x04 ,0x01 ,0x70 ,0x00 ,0x01 ,0x05,
0x03 ,0x01 ,0x00 ,0x00 ,0x06 ,0x06 ,0x01 ,0x7f ,0x00 ,0x41 ,0x08 ,0x0b ,0x07 ,0x18 ,0x03 ,0x06,
0x6d ,0x65 ,0x6d ,0x6f ,0x72 ,0x79 ,0x02 ,0x00 ,0x05 ,0x74 ,0x61 ,0x62 ,0x6c ,0x65 ,0x01 ,0x00,
0x03 ,0x61 ,0x64 ,0x64 ,0x00 ,0x00 ,0x09 ,0x07 ,0x01 ,0x00 ,0x41 ,0x00 ,0x0b ,0x01 ,0x01 ,0x0a,
0x0c ,0x02 ,0x07 ,0x00 ,0x20 ,0x00 ,0x20 ,0x01 ,0x6a ,0x0b ,0x02 ,0x00 ,0x0b,
};
final String jsCode = "android.consumeNamedDataAsArrayBuffer('wasm-1').then(" +
"(value) => { return WebAssembly.compile(value).then(" +
"(module) => { return new WebAssembly.Instance(module).exports.add(20, 22).toString(); }" +
")})";
boolean success = js.provideNamedData("wasm-1", hello_world_wasm);
if (success) {
FluentFuture.from(js.evaluateJavaScriptAsync(jsCode))
.transform(this::println, mainThreadExecutor)
.catching(Throwable.class, e -> println(e.getMessage()), mainThreadExecutor);
} else {
// the data chunk name has been used before, use a different name
}

更多支持


AndroidX Webkit 是一个功能强大的库,由于篇幅原因上文将开发者比较常用的功能进行了列举,AndroidX 还提供对 WebView 更精细化的控制,对 Cookie 的便捷访问、对 Web 资源的便捷访问,对 WebView 性能的收集,还有对大屏幕的支持等等强大的 API,大家可以查看发布页面查看最新的功能。


写在最后


本文从实际矛盾出发,带领大家思考 AndroidX Webkit 的产生原因和实现原理,对于AndroidX Webkit 的几个功能分别做了简单的介绍,希望大家能在这篇文章获得一点启发和帮助。


作者:简绘Android
链接:https://juejin.cn/post/7259762775365320741
来源:稀土掘金
收起阅读 »

我的又一个神奇的框架——Skins换肤框架

为什么会有换肤的需求 app的换肤,可以降低app用户的审美疲劳。再好的UI设计,一直不变的话,也会对用户体验大打折扣,即使表面上不说,但心里或多或少会有些难受。所以app的界面要适当的改版啊,要不然可难受死用户了,特别是UI设计还相对较丑的。 换肤是什么 换...
继续阅读 »

为什么会有换肤的需求


app的换肤,可以降低app用户的审美疲劳。再好的UI设计,一直不变的话,也会对用户体验大打折扣,即使表面上不说,但心里或多或少会有些难受。所以app的界面要适当的改版啊,要不然可难受死用户了,特别是UI设计还相对较丑的。


换肤是什么


换肤是将app的背景色、文字颜色以及资源图片,一键进行全部切换的过程。这里就包括了图片资源和颜色资源。


Skins怎么使用


Skins就是一个解决这样一种换肤需求的框架。

// 添加以下代码到项目根目录下的build.gradle
allprojects {
repositories {
maven { url "https://jitpack.io" }
}
}
// 添加以下代码到app模块的build.gradle
dependencies {
// skins依赖了dora框架,所以你也要implementation dora
implementation("com.github.dora4:dora:1.1.12")
implementation 'com.github.dora4:dview-skins:1.4'
}

我以更换皮肤颜色为例,打开res/colors.xml。

<!-- 需要换肤的颜色 -->
<color name="skin_theme_color">@color/cyan</color>
<color name="skin_theme_color_red">#d23c3e</color>
<color name="skin_theme_color_orange">#ff8400</color>
<color name="skin_theme_color_black">#161616</color>
<color name="skin_theme_color_green">#009944</color>
<color name="skin_theme_color_blue">#0284e9</color>
<color name="skin_theme_color_cyan">@color/cyan</color>
<color name="skin_theme_color_purple">#8c00d6</color>
将所有需要换肤的颜色,添加skin_前缀和_skinname后缀,不加后缀的就是默认皮肤。
然后在启动页应用预设的皮肤类型。在布局layout文件中使用默认皮肤的资源名称,像这里就是R.color.skin_theme_color,框架会自动帮你替换。要想让框架自动帮你替换,你需要让所有要换肤的Activity继承BaseSkinActivity。
private fun applySkin() {
val manager = PreferencesManager(this)
when (manager.getSkinType()) {
0 -> {
}
1 -> {
SkinManager.changeSkin("cyan")
}
2 -> {
SkinManager.changeSkin("orange")
}
3 -> {
SkinManager.changeSkin("black")
}
4 -> {
SkinManager.changeSkin("green")
}
5 -> {
SkinManager.changeSkin("red")
}
6 -> {
SkinManager.changeSkin("blue")
}
7 -> {
SkinManager.changeSkin("purple")
}
}
}

另外还有一个情况是在代码中使用换肤,那么跟布局文件中定义是有一些区别的。

val skinThemeColor = SkinManager.getLoader().getColor("skin_theme_color")
这个skinThemeColor拿到的就是当前皮肤下的真正的skin_theme_color颜色,比如R.color.skin_theme_color_orange的颜色值“#ff8400”或R.id.skin_theme_color_blue的颜色值“#0284e9”。
SkinLoader还提供了更简洁设置View颜色的方法。
override fun setImageDrawable(imageView: ImageView, resName: String) {
val drawable = getDrawable(resName) ?: return
imageView.setImageDrawable(drawable)
}

override fun setBackgroundDrawable(view: View, resName: String) {
val drawable = getDrawable(resName) ?: return
view.background = drawable
}

override fun setBackgroundColor(view: View, resName: String) {
val color = getColor(resName)
view.setBackgroundColor(color)
}

框架原理解析

先看BaseSkinActivity的源码。

package dora.skin.base

import android.content.Context
import android.os.Bundle
import android.util.AttributeSet
import android.view.InflateException
import android.view.LayoutInflater
import android.view.View
import androidx.collection.ArrayMap
import androidx.core.view.LayoutInflaterCompat
import androidx.core.view.LayoutInflaterFactory
import androidx.databinding.ViewDataBinding
import dora.BaseActivity
import dora.skin.SkinManager
import dora.skin.attr.SkinAttr
import dora.skin.attr.SkinAttrSupport
import dora.skin.attr.SkinView
import dora.skin.listener.ISkinChangeListener
import dora.util.LogUtils
import dora.util.ReflectionUtils
import java.lang.reflect.Constructor
import java.lang.reflect.Method
import java.util.*

abstract class BaseSkinActivity<T : ViewDataBinding> : BaseActivity<T>(),
ISkinChangeListener, LayoutInflaterFactory {

private val constructorArgs = arrayOfNulls<Any>(2)

override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {
if (createViewMethod == null) {
val methodOnCreateView = ReflectionUtils.findMethod(delegate.javaClass, false,
"createView", *createViewSignature)
createViewMethod = methodOnCreateView
}
var view: View? = ReflectionUtils.invokeMethod(delegate, createViewMethod, parent, name,
context, attrs) as View?
if (view == null) {
view = createViewFromTag(context, name, attrs)
}
val skinAttrList = SkinAttrSupport.getSkinAttrs(attrs, context)
if (skinAttrList.isEmpty()) {
return view
}
injectSkin(view, skinAttrList)
return view
}

private fun injectSkin(view: View?, skinAttrList: MutableList<SkinAttr>) {
if (skinAttrList.isNotEmpty()) {
var skinViews = SkinManager.getSkinViews(this)
if (skinViews == null) {
skinViews = arrayListOf()
}
skinViews.add(SkinView(view, skinAttrList))
SkinManager.addSkinView(this, skinViews)
if (SkinManager.needChangeSkin()) {
SkinManager.apply(this)
}
}
}

private fun createViewFromTag(context: Context, viewName: String, attrs: AttributeSet): View? {
var name = viewName
if (name == "view") {
name = attrs.getAttributeValue(null, "class")
}
return try {
constructorArgs[0] = context
constructorArgs[1] = attrs
if (-1 == name.indexOf('.')) {
// try the android.widget prefix first...
createView(context, name, "android.widget.")
} else {
createView(context, name, null)
}
} catch (e: Exception) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
null
} finally {
// Don't retain references on context.
constructorArgs[0] = null
constructorArgs[1] = null
}
}

@Throws(InflateException::class)
private fun createView(context: Context, name: String, prefix: String?): View? {
var constructor = constructorMap[name]
return try {
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
val clazz = context.classLoader.loadClass(
if (prefix != null) prefix + name else name).asSubclass(View::class.java)
constructor = clazz.getConstructor(*constructorSignature)
constructorMap[name] = constructor
}
constructor!!.isAccessible = true
constructor.newInstance(*constructorArgs)
} catch (e: Exception) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
null
}
}

override fun onCreate(savedInstanceState: Bundle?) {
val layoutInflater = LayoutInflater.from(this)
LayoutInflaterCompat.setFactory(layoutInflater, this)
super.onCreate(savedInstanceState)
SkinManager.addListener(this)
}

override fun onDestroy() {
super.onDestroy()
SkinManager.removeListener(this)
}

override fun onSkinChanged(suffix: String) {
SkinManager.apply(this)
}

companion object {
val constructorSignature = arrayOf(Context::class.java, AttributeSet::class.java)
private val constructorMap: MutableMap<String, Constructor<out View>> = ArrayMap()
private var createViewMethod: Method? = null
val createViewSignature = arrayOf(View::class.java, String::class.java,
Context::class.java, AttributeSet::class.java)
}
}
我们可以看到BaseSkinActivity继承自dora.BaseActivity,所以dora框架是必须要依赖的。有人说,那我不用dora框架的功能,可不可以不依赖dora框架?我的回答是,不建议。Skins对Dora生命周期注入特性采用的是,依赖即配置。
package dora.lifecycle.application

import android.app.Application
import android.content.Context
import dora.skin.SkinManager

class SkinsAppLifecycle : ApplicationLifecycleCallbacks {

override fun attachBaseContext(base: Context) {
}

override fun onCreate(application: Application) {
SkinManager.init(application)
}

override fun onTerminate(application: Application) {
}
}
所以你无需手动配置<meta-data android:name="dora.lifecycle.config.SkinsGlobalConfig" android:value="GlobalConfig"/>,Skins已经自动帮你配置好了。那么我顺便问个问题,BaseSkinActivity中最关键的一行代码是哪行?LayoutInflaterCompat.setFactory(layoutInflater, this)这行代码是整个换肤流程最关键的一行代码。我们来干预一下所有Activity onCreateView时的布局加载过程。我们在SkinAttrSupport.getSkinAttrs中自己解析了AttributeSet。
/**
* 从xml的属性集合中获取皮肤相关的属性。
*/
fun getSkinAttrs(attrs: AttributeSet, context: Context): MutableList<SkinAttr> {
val skinAttrs: MutableList<SkinAttr> = ArrayList()
var skinAttr: SkinAttr
for (i in 0 until attrs.attributeCount) {
val attrName = attrs.getAttributeName(i)
val attrValue = attrs.getAttributeValue(i)
val attrType = getSupportAttrType(attrName) ?: continue
if (attrValue.startsWith("@")) {
val ref = attrValue.substring(1)
if (TextUtils.isEqualTo(ref, "null")) {
// 跳过@null
continue
}
val id = ref.toInt()
// 获取资源id的实体名称
val entryName = context.resources.getResourceEntryName(id)
if (entryName.startsWith(SkinConfig.ATTR_PREFIX)) {
skinAttr = SkinAttr(attrType, entryName)
skinAttrs.add(skinAttr)
}
}
}
return skinAttrs
}

我们只干预skin_开头的资源的加载过程,所以解析得到我们需要的属性,最后得到SkinAttr的列表返回。

package dora.skin.attr

import android.view.View
import android.widget.ImageView
import android.widget.TextView
import dora.skin.SkinLoader
import dora.skin.SkinManager

enum class SkinAttrType(var attrType: String) {

/**
* 背景属性。
*/
BACKGROUND("background") {
override fun apply(view: View, resName: String) {
val drawable = loader.getDrawable(resName)
if (drawable != null) {
view.setBackgroundDrawable(drawable)
} else {
val color = loader.getColor(resName)
view.setBackgroundColor(color)
}
}
},

/**
* 字体颜色。
*/
TEXT_COLOR("textColor") {
override fun apply(view: View, resName: String) {
val colorStateList = loader.getColorStateList(resName) ?: return
(view as TextView).setTextColor(colorStateList)
}
},

/**
* 图片资源。
*/
SRC("src") {
override fun apply(view: View, resName: String) {
if (view is ImageView) {
val drawable = loader.getDrawable(resName) ?: return
view.setImageDrawable(drawable)
}
}
};

abstract fun apply(view: View, resName: String)

/**
* 获取资源管理器。
*/
val loader: SkinLoader
get() = SkinManager.getLoader()
}

当前skins框架只定义了几种主要的换肤属性,你理解原理后,也可以自己进行扩展,比如RadioButton的button属性等。


开源项目传送门


如果你要深入理解完整的换肤流程,请阅读skins的源代码,[github.com/dora4/dview…] 。

作者:dora
链接:https://juejin.cn/post/7258483700815609916
来源:稀土掘金
收起阅读 »

工信部又出新规!爬坑指南

一、背景 工信部最近发布了新的入网要求,明确了app进网检测要求的具体变化,主要涉及到一些app权限调用,个人信息保护,软件升级以及敏感行为。为了不影响app的正常运行,依据工信部的文件进行相关整改,下文将从5个方向来阐述具体的解决思路。 二、整改 2.1 个...
继续阅读 »

一、背景


工信部最近发布了新的入网要求,明确了app进网检测要求的具体变化,主要涉及到一些app权限调用,个人信息保护,软件升级以及敏感行为。为了不影响app的正常运行,依据工信部的文件进行相关整改,下文将从5个方向来阐述具体的解决思路。


二、整改


2.1 个人信息保护


2.1.1 基本模式(无权限、无个人信息获取模式)


这次整改涉及到最大的一个点就是基本模式,基本模式指的是在用户选择隐私协议弹窗时,不能点击“不同意”即退出应用,而是需要给用户提供一个除联网功能外,无任何权限,无任何个人信息获取的模式且用户能正常使用。


这个说法有点抽象,我们来看下友商已经做好的案例。


腾讯视频


从腾讯视频的策略来看,用户第一次使用app,依旧会弹出一个“用户隐私协议”弹窗供用户选择,但是和以往不同的是,“不同意”按钮替换为了“不同意并进入基本功能模式”,用户点击“不同意并进入基本功能模式”则进入到一个简洁版的页面,只提供一些基本功能,当用户点击“进入全功能模式”,则再次弹出隐私协议弹窗。当杀死进程后,再次进入则直接进入基本模式。


网易云音乐


网易云音乐和腾讯视频的产品策略略有不同,在用户在一级授权弹窗点击“不同意”,会跳转至二级授权弹窗,当用户在二级弹窗上点击“不同意,进入基本功能模式”,才会进入基本功能页面,在此页面上点击“进入完整功能模式”后就又回到了二级授权页。当用户杀死进程,重新进入app时,还是会回到一级授权页。


网易云音乐比腾讯视频多了一个弹窗,也只是为了提升用户进入完整模式的概率,并不涉及到新规。


另外,B站、酷狗音乐等都已经接入了基本模式,有兴趣的伙伴可以自行下载体验。


2.1.2 隐私政策内容


如果app存在读取并传送用户个人信息的行为,需要检查其是否具备用户个人信息收集、使用规则,并明确告知读取和传送个人信息的目的、方式和范围。


判断权限是否有读取、修改、传送行为,如果有,需要在隐私协议中明文告知。


举个例子,app有获取手机号码并且保存在服务器,需要在协议中明确声明:读取并传送用户手机号码。


2.2 app权限调用


2.2.1 应用内权限调用

  • 获取定位信息和生物特征识别信息

在获取定位信息以及生物特征识别信息时需要在调用权限前,单独向用户明示调用权限的目的,不能用系统权限弹窗替代。


如上图,申请位置权限,需要在申请之前,弹出弹窗供用户选择,用户同意调用后才可以申请位置权限。

其他权限

其他权限如上面隐私政策一样,需要在调用时,声明是读取、修改、还是传送行为,如下图


2.3 应用软件升级


2.3.1 更新


应用软件或插件更新应在用户授权的情况下进行,不能直接更新,另外要明确告知用户此行为包含下载和安装。


简单来说,就是在app进行更新操作时,需要用弹窗告知用户,是否更新应用,更新的确认权交给用户,并且弹窗上需要声明此次更新有下载和安装两个操作。如下图

2.4 应用签名

需要保证签名的真实有效性。

作者:付十一
链接:https://juejin.cn/post/7253610755126476857
来源:稀土掘金
收起阅读 »

为什么大部分人做不了架构师?

腾小云导读成为架构师,是许多程序员的职业梦想。然而其中只有少数有着丰厚编码积累、超强自驱力和独到思维的程序员才能最终成为架构师。其实,日常工作中小到某个功能的开发,大到整个业务系统的设计,都可以看到架构设计的影子。《从0开始学架构》一书是颇受程序员欢迎的架构设...
继续阅读 »

腾小云导读

成为架构师,是许多程序员的职业梦想。然而其中只有少数有着丰厚编码积累、超强自驱力和独到思维的程序员才能最终成为架构师。其实,日常工作中小到某个功能的开发,大到整个业务系统的设计,都可以看到架构设计的影子。《从0开始学架构》一书是颇受程序员欢迎的架构设计入门教程。接下来本文作者将提取该书籍之精髓,结合自身经验分享架构设计常见方法以及高可用、高性能、可扩散架构模式的实现思路,将架构设计思维“为我所用”、提升日常研效。希望对你有帮助~

目录

1 基本概念与设计方法

2 高性能架构模式

2.1 存储高性能

2.2 计算高性能

3 高可用架构模式

3.1 理论方法

3.2 存储高可用

3.3 计算高可用

4 可扩展架构模式

5 总结

之前本栏目《腾讯专家10年沉淀:后海量时代的架构设计》、《工作十年,在腾讯沉淀的高可用系统架构设计经验》两篇文章中,两位腾讯的开发者结合自身经历,分享了架构设计的实践经验。而本期,本栏目特邀腾讯云对《从0开始学架构》一书提取精髓,并结合亲身经验做分享。

01、基本概念与设计方法

在讲解架构思想之前,我们先统一介绍一下基本概念的含义,避免每个人对系统、框架、架构这些名词的理解不一致导致的误解。下面是《从0开始学架构》作者对每个名词的定义。其作用域仅限本文范畴,不用纠结其在其他上下文中的意义。

系统:系统泛指由一群有关联的个体组成,根据某种规则运作,能完成个别元件不能单独完成的工作的群体。 子系统:子系统也是由一群有关联的个体所组成的系统,多半会是更大系统中的一部分。模块:从业务逻辑的角度来拆分系统后,得到的单元就是“模块”。划分模块的主要目的是职责分离。组件:从物理部署的角度来拆分系统后,得到的单元就是“组件”。划分组件的主要目的是单元复用。框架:是一整套开发规范,是提供基础功能的产品。架构:关注的是结构,是某一套开发规范下的具体落地方案,包括各个模块之间的组合关系以及它们协同起来完成功能的运作规则。
由以上定义可见,所谓架构,是为了解决软件系统的某个复杂度带来的具体问题,将模块和组件以某种方式有机组合,基于某个具体的框架实现后的一种落地方案。

而讨论架构时,往往只讨论到系统与子系统这个顶层的架构。

可见,要进行架构选型,首先应该知道自己要解决的业务和系统复杂点在哪里,是作为秒杀系统有瞬间高并发,还是作为金融科技系统有极高的数据一致性和可用性要求等。

一般来说,系统的复杂度来源有以下几个方面:

高性能

如果业务的访问频率或实时性要求较高,则会对系统提出高性能的要求。

如果是单机系统,需要利用多进程、多线程技术。

如果是集群系统,则还涉及任务拆分、分配与调度,多机器状态管理,机器间通信,当单机性能达到瓶颈后,即使继续加机器也无法继续提升性能,还是要针对单个子任务进行性能提升。

高可用

如果业务的可用性要求较高,也会带来高可用方面的复杂度。高可用又分为计算高可用和存储高可用。

针对计算高可用,可以采用主备(冷备、温备、热备)、多主的方式来冗余计算能力,但会增加成本、可维护性方面的复杂度。

针对存储高可用,同样是增加机器来冗余,但这也会带来多机器导致的数据不一致问题,如遇到延迟、中断、故障等情况。难点在于怎么减少数据不一致对业务的影响。

既然主要解决思路是增加机器来做冗余,那么就涉及到了状态决策的问题。即如果判断当前主机的状态是正常还是异常,以及异常了要如何采取行动(比如切换哪台做主机)。

对主机状态的判断,多采用机器信息采集或请求响应情况分析等手段,但又会产生采集信息这一条通信链路本身是否正常的问题,下文会具体展开讨论。事实上,状态决策本质上不可能做到完全正确。

而对于决策方式,有以下几种方式:

独裁式:存在一个独立的决策主体来收集信息并决定主机,这样的策略不会混乱,但这个主体本身存在单点问题。 协商式:两台备机通过事先指定的规则来协商决策出主机,规则虽然简单方便,但是如果两台备机之间的协商链路中断了,决策起来就会很困难,比如有网络延迟且机器未故障、网络中断且机器未故障、网络中断其机器已故障,多种情况需要处理。民主式:如果有多台备机,可以使用选举算法来投票出主机,比如 Paxos 就是一种选举算法,这种算法大多数都采取多数取胜的策略,算法本身较为复杂,且如果像协商式一样出现连接中断,就会脑裂,不同部分会各自决策出不同结果,需要规避。

可扩展性

众所周知在互联网行业只有变化才是永远不变的,而开发一个系统基本都不是一蹴而就的,那应该如何为系统的未来可能性进行设计来保持可扩展性呢?

这里首先要明确的一个观点就是,在做系统设计时,既不可能完全不考虑可扩展性,也不可能每个设计点都考虑可扩展性,前者很明显,后者则是为了避免舍本逐末,为了扩展而扩展,实际上可能会为不存在的预测花费过多的精力。

那么怎么考虑系统的未来可能性从而做出相应的可扩展性设计呢?这里作者给出了一个方法:只预测两年内可能的变化,不要试图预测五年乃至十年的变化。因为对于变化快的行业来说,预测两年已经足够远了,再多就可能计划赶不上变化。而对变化慢的行业,则预测的意义更是不大。

要应对变化,主要是将变与不变分隔开来。

这里可以针对业务,提炼变化层和稳定层,通过变化层将变化隔离。比如通过一个 DAO 服务来对接各种变化的存储载体,但是上层稳定的逻辑不用知晓当前采用何种存储,只需按照固定的接口访问 DAO 即可获取数据。

也可以将一些实现细节剥离开来,提炼出抽象层,仅在实现层去封装变化。比如面对运营上经常变化的业务规则,可以提炼出一个规则引擎来实现核心的抽象逻辑,而具体的规则实现则可以按需增加。

如果是面对一个旧系统的维护,接到了新的重复性需求,而旧系统并不支持较好的可扩展性,这时是否需要花费时间精力去重构呢?作者也提出了《重构》一书中提到的原则:事不过三,三则重构。

简而言之,不要一开始就考虑复杂的做法去满足可扩展性,而是等到第三次遇到类似的实现时再来重构,重构的时候采取上述说的隔离或者封装的方案。

这一原则对新系统开发也是适用的。总而言之就是,不要为难以预测的未来去过度设计,为明确的未来保留适量的可扩展性即可。

低成本

上面说的高性能、高可用都需要增加机器,带来的是成本的增加,而很多时候研发的预算是有限的。换句话说,低成本往往并不是架构设计的首要目标,而是设计架构时的约束限制。

那如何在有限的成本下满足复杂性要求呢?往往只有“创新”才能达到低成本的目标。举几个例子:

NoSQL 的出现是为解决关系型数据库应对高并发的问题。 全文搜索引擎的出现是为解决数据库 like 搜索效率的问题。Hadoop 的出现是为解决文件系统无法应对海量数据存储与计算的问题。Facebook 的 HipHop PHP 和 HHVM 的出现是为解决 PHP 运行低效问题。新浪微博引入 SSD Cache 做 L2 缓存是为解决 Redis 高成本、容量小、穿透 DB 的问题。Linkedin 引入 Kafka 是为解决海量事件问题。
上述案例都是为了在不显著增加成本的前提下,实现系统的目标。

这里还要说明的是,创造新技术的复杂度本身就是很高的,因此一般中小公司基本都是靠引入现有的成熟新技术来达到低成本的目标;而大公司才更有可能自己去创造新的技术来达到低成本的目标,因为大公司才有足够的资源、技术和时间去创造新技术。

安全

安全是一个研发人员很熟悉的目标,从整体来说,安全包含两方面:功能安全和架构安全。

功能安全是为了“防小偷”,即避免系统因安全漏洞而被窃取数据,如 SQL 注入。常见的安全漏洞已经有很多框架支持,所以更建议利用现有框架的安全能力,来避免重复开发,也避免因自身考虑不够全面而遗漏。在此基础上,仍需持续攻防来完善自身的安全。

架构安全是为了“防强盗”,即避免系统被暴力攻击导致系统故障,比如 DDOS 攻击。这里一方面只能通过防火墙集运营商或云服务商的大带宽和流量清洗的能力进行防范,另一方面也需要做好攻击发现与干预、恢复的能力。

规模

架构师在宣讲时往往会先说自己任职和设计过的大型公司的架构,这是因为当系统的规模达到一定程度后,复杂度会发生质的变化,即所谓量变引起质变。

这个量,体现在访问量、功能数量及数据量上。

访问量映射到对高性能的要求。功能数量需要视具体业务会带来不同的复杂度。而数据量带来的收集、加工、存储、分析方面的挑战,现有的方案基本都是基于 Google 的三篇大数据论文的理论:

Google File System 是大数据文件存储的技术理论。Google Bigtable 是列式数据存储的技术理论。Google MapReduce 是大数据运算的技术理论。
经过上面的分析可以看到,复杂度来源很多,想要一一应对,似乎会得到一个复杂无比的架构,但对于架构设计来说,其实刚开始设计时越简单越好,只要能解决问题,就可以从简单开始再慢慢去演化,对应的是下面三条原则:

合适原则:不需要一开始就挑选业界领先的架构,它也许优秀,但可能不那么适合自己,比如有很多目前用不到的能力或者大大超出诉求从而增加很多成本。其实更需要考虑的是合理地将资源整合在一起发挥出最大功效,并能够快速落地。简单原则:有时候为了显示出自身的能力,往往会在一开始就将系统设计得非常复杂,复杂可能代表着先进,但更可能代表着“问题”,组件越多,就越可能出故障,越可能影响关联着的组件,定位问题也更加困难。其实只要能够解决诉求即可。演化原则:不要妄想一步到位,没有人可以准确预测未来所有发展,软件不像建筑,变化才是主题。架构的设计应该先满足业务需求,适当的预留扩展性,然后在未来的业务发展中再不断地迭代,保留有限的设计,修复缺陷,改正错误,去除无用部分。这也是重构、重写的价值所在。

即使是 QQ、淘宝这种如今已经非常复杂的系统,刚开始时也只是一个简单的系统,甚至淘宝都是直接买来的系统,随着业务发展也只是先加服务器、引入一些组件解决性能问题,直到达到瓶颈才去重构重写,重新在新的复杂度要求下设计新的架构。

明确了设计原则后,当面对一个具体的业务,我们可以按照如下步骤进行架构设计:

识别复杂度:无论是新设计一个系统还是接手一个混乱的系统,第一步都是先将主要的复杂度问题列出来,然后根据业务、技术、团队等综合情况进行排序,优先解决当前面临的最主要的复杂度问题。复杂度的主要来源上文已经说过,可以按照经验或者排查法进行分析。方案对比:先看看业界是否有类似的业务,了解他们是怎么解决问题的,然后提出3~5个备选方案,不要只考虑做一个最优秀的方案,一个人的认知范围常常是有限的,逼自己多思考几个方案可以有效规避因为思维狭隘导致的局限性,当然也不要过多,不用给出非常详细的方案,太消耗精力。备选方案的差异要比较明显,才有扩宽思路和对比的价值。设计详细方案:当多个方案对比得出最终选择后,就可以对目标方案进行详细的设计,关键细节需要比较深入,如果方案本身很复杂,也可以采取分步骤、分阶段、分系统的实现方式来降低实现复杂度。当方案非常庞大的时候,可以汇集一个团队的智慧和经验来共同设计,防止因架构师的思维盲区导致问题。

02、高性能架构模式

2.1 存储高性能

互联网业务大多是数据密集型的业务,其对性能的压力也常常来自于海量用户对数据的高频读写压力上。因此解决高性能问题,首先要解决数据读写的存储高性能问题。

读写分离

在大多数业务中,用户查询和修改数据的频率是不同的,甚至是差别很大的,大部分情况下都是读多写少的,因此可以将对数据的读和写操作分开对待,对压力更大的读操作提供额外的机器分担压力,这就是读写分离。

读写分离的基本实现是搭建数据库的主从集群,根据需要提供一主一从或一主多从。

注意是主从不是主备,从和备的差别在于从机是要干活的。

通常在读多写少的情况下,主机负责读写操作,从机只负责读操作,负责帮主机分担读操作的压力。而数据会通过复制机制(如全同步、半同步、异步)同步到从机,每台服务器都有所有业务数据。

既然有数据的同步,就一定存在复制延迟导致的从机数据不一致问题,针对这个问题有几种常见的解法,如:

写操作后同一用户一段时间内的读操作都发给主机,避免数据还没同步到从机,但这个逻辑容易遗漏。读从机失败后再读一次主机,该方法只能解决新数据未同步的问题,无法解决旧数据修改的问题(不会读取失败),且二次读取主机会给主机带来负担,容易被针对性攻击。关键读写操作全部走主机,从机仅负责非关键链路的读,该方法是基于保障关键业务的思路。
除了数据同步的问题之外,只要涉及主从机同时支持业务访问的,就一定需要制定请求分配的机制。上面说的几个问题解法也涉及了一些分配机制的细节。具体到分配机制的实现来说,有两种思路:

程序代码封装:实现简单,可对业务定制化,但每个语言都要自己实现一次,且很难做到同步修改,因此适合小团队。中间件封装:独立出一套系统管理读写的分配,对业务透明,兼容 SQL 协议,业务服务器就无需做额外修改适配。需要支持多语言、完整的 SQL 语法,涉及很多细节,容易出 BUG,且本身是个单点,需要特别保障性能和可用性,因此适合大公司。
分库分表

除了高频访问的压力,当数据量大了以后,也会带来数据库存储方面的压力。此时就需要考虑分库分表的问题。分库分表既可以缓解访问的压力,也可以分散存储的压力。

先说分库,所谓分库,就是指业务按照功能、模块、领域等不同,将数据分散存储到不同的数据库实例中。

比如原本是一个 MySQL 数据库实例,在库中按照不同业务建了多张表,大体可以归类为 A、B 两个领域的数据。现在新建一个库,将原库中 A 领域的数据迁移到新的库中存储,还是按需建表,而 B 领域的数据继续留在原库中。

分库一方面可以缓解访问和存储的压力,另一方面也可以增加抗风险能力,当一个库出问题后,另一个库中的数据并不会受到影响,而且还能分开管理权限。

但分库也会带来一些问题:原本同一个库中的不同表可以方便地进行联表查询,分库后则会变得很复杂。由于数据在不同的库中,当要操作两个库中的数据时,无法使用事务操作,一致性也变得更难以保障。而且当增加备库来保障可用性的时候,成本是成倍增加的。

基于以上问题,初创的业务并不建议在一开始就做这种拆分,会增加很多开发时的成本和复杂度,拖慢业务的节奏。

再说分表。所谓分表,就是将原本存储在一张表里的数据,按照不同的维度,拆分成多张表来存储。

按照诉求与业务的特性不同,可以采用垂直分表或水平分表的方式。

垂直分表相当于垂直地给原表切了一刀,把不同的字段拆分到不同的子表中,这样拆分后,原本访问一张表可以获取的所有字段,现在则需要访问不同的表获取。

垂直分表适合将表中某些不常用又占了大量空间的列(字段)拆分出去,可以提升访问常用字段的性能。

但相应的,当真的需要的字段处于不同表中时,或者要新增记录存储所有字段数据时,要操作的表变多了。

水平分表相当于横着给原表切了一刀,那么原表中的记录会被分散存储到不同的子表中,但是每张子表的字段都是全部字段。

水平分表适合表的量级很大以至影响访问性能的场景,何时该拆分并没有绝对的指标,一般记录数超过千万时就需要警觉了。

不同于垂直分表依然能访问到所有记录,水平分表后无法再在一张表中访问所有数据了,因此很多查询操作会受到影响,比如 join 操作就需要多次查询后合并结果,count 操作也需要计算多表的结果后相加,如果经常用到 count 的总数,可以额外维护一个总数表去更新,但也会带来数据一致性的问题。

值得特别提出的是范围查询,原本的一张表可以通过范围查询到的数据,分表后也需要多次查询后合并数据,如果是业务经常用到的范围查询,那建议干脆就按照这种方式来分表,这也是分表的路由方式之一:范围路由。

所谓路由方式是指:分表后当新插入记录时,如何判断该往哪张表插入。常用的插入方式有以下三种:

范围路由:按照时间范围、ID 范围或者其他业务常用范围字段路由。这种方式在扩充新的表时比较方便,直接加表给新范围的数据插入即可,但是数量和冷热分布可能是不均匀的。 Hash 路由:根据 Hash 运算来路由新记录插入的表,这种方式需要提前就规划好分多少张表,才能决定 Hash 运算方式。但表数量其实很难预估,导致未来需要扩充新表时很麻烦,但数据在不同表中的分布是比较均匀的。配置路由:新增一个路由表来记录数据 id 和表 id 的映射,按照自定义的方式随时修改映射规则,设计简单,扩充新表也很方便。但每次操作表都需要额外操作一次路由表,其本身也成为了单点瓶颈

无论是垂直分表还是水平分表,单表切分为多表后,新的表即使在同一个数据库服务器中,也可能带来可观的性能提升,如果性能能够满足业务要求,可以不拆分到多台数据库服务器,毕竟分库也会引入很多复杂性的问题;如果单表拆分为多表后,单台服务器依然无法满足性能要求,那就不得不再次进行业务分库的设计了。

NoSQL 数据库

上面发分库分表讨论的都是关系型数据库的优化方案,但关系型数据库也有其无法规避的缺点,比如无法直接存储某种结构化的数据、扩展表结构时会锁表影响线上性能、大数据场景下 I/O 较高、全文搜索的功能比较弱等。

基于这些缺点,也有很多新的数据库框架被创造出来,解决其某方面的问题。

比如以 Redis 为代表的的 KV 存储,可以解决无法存储结构化数据的问题;以 MongoDB 为代表的的文档数据库可以解决扩展表结构被强 Schema 约束的问题;以 HBase 为代表的的列式数据库可以解决大数据场景下的 I/O 问题;以 ES 为代表的的全文搜索引擎可以解决全文检索效率的问题等。

这些数据库统称为 NoSQL 数据库,但 NoSQL 并不是全都不能写 SQL,而是 Not Only SQL 的意思。

NoSQL 数据库除了聚焦于解决某方面的问题以外也会有其自身的缺点,比如 Redis 没有支持完整的 ACID 事务、列式存储在更新一条记录的多字段时性能较差等。因此并不是说使用了 NoSQL 就能一劳永逸,更多的是按需取用,解决业务面临的问题。

关于 NoSQL 的更多了解,推荐大家可以看看《NoSQL 精粹》这本书。

缓存

如果 NoSQL 也解决不了业务的高性能诉求,那么或许你需要加点缓存

缓存最直接的概念就是把常用的数据存在内存中,当业务请求来查询的时候直接从内存中拿出来,不用重新去数据库中按条件查询,也就省去了大量的磁盘 IO 时间。

一般来说缓存都是通过 Key-Value 的方式存储在内存中,根据存储的位置,分为单机缓存和集中式缓存。单机缓存就是存在自身服务器所在的机器上,那么势必会有不同机器数据可能不一致,或者重复缓存的问题,要解决可以使用查询内容做路由来保障同一记录始终到同一台机器上查询缓存。集中式缓存则是所有服务器都去一个地方查缓存,会增加一些调用时间。

缓存可以提升性能是很好理解的,但缓存同样有着它的问题需要应对或规避。数据时效性是最容易想到的问题,但也可以靠同时更新缓存的策略来保障数据的时效性,除此之外还有其他几个常见的问题。

如果某条数据不存在,缓存中势必查不到对应的 KEY,从而就会请求数据库确认是否有新增加这条数据,如果始终没有这条数据,而客户端又反复频繁地查询这条数据,就会变相地对数据库造成很大的压力,换句话说,缓存失去了保护作用,请求穿透到了数据库,这称为缓存穿透。

应对缓存穿透,最好的手段就是把“空值”这一情况也缓存下来,当客户端下次再查询时,发现缓存中说明了该数据是空值,则不会再问询数据库。但也要注意如果真的有对应数据写入了数据库,应当能及时清除”空值“缓存。

为了保障缓存的数据及时更新,常常都会根据业务特性设置一个缓存过期时间,在缓存过期后,到再次生成期间,如果出现大量的查询,会导致请求都传递到数据库,而且会多次重复生成缓存,甚至可能拖垮整个系统,这就叫缓存雪崩,和缓存穿透的区别在于,穿透是面对空值的情况,而雪崩是由于缓存重新生成的间隔期大量请求产生的连锁效应。

既然是缓存更新时重复生成所导致的问题,那么一种解法就是在缓存重新生成前给这个 KEY 加锁,加锁期间出现的请求都等待或返回默认值,而不去都尝试重新生成缓存。

另一种方法是干脆不要由客户端请求来触发缓存更新,而是由后台脚本统一更新,同样可以规避重复请求导致的重复生成。但是这就失去了只缓存热点数据的能力,如果缓存因空间问题被清除了,也会因为后台没及时更新导致查不到缓存数据,这就会要求更复杂的后台更新策略,比如主动查询缓存有效性、缓存被删后通知后台主动更新等。

虽说在有限的内存空间内最好缓存热点数据,但如果数据过热,比如微博的超级热搜,也会导致缓存服务器压力过大而崩溃,称之为缓存热点问题。

可以复制多份缓存副本,来分散缓存服务器的单机压力,毕竟堆机器是最简单有效。此处也要注意,多个缓存副本不要设置相同的缓存过期时间,否则多处缓存同时过期,并同时更新,也容易引起缓存雪崩,应该设置一个时间范围内的随机值来更新缓存。

2.2 计算高性能

讲完存储高性能,再讲计算高性能,计算性能的优化可以先从单机性能优化开始,多进程、多线程、IO 多路复用、异步 IO 等都存在很多可以优化的地方,但基本系统或框架已经提供了基本的优化能力,只需使用即可。

负载均衡

如果单机的性能优化已经到了瓶颈,无法应对业务的增长,就会开始增加服务器,构建集群。对于计算来说,每一台服务器接到同样的输入,都应该返回同样的输出,当服务器从单台变成多台之后,就会面临请求来了要由哪一台服务器处理的问题,我们当然希望由当前比较空闲的服务器去处理新的请求,这里对请求任务的处理分配问题,就叫负载均衡。

负载均衡的策略,从分类上来说,可以分为三类:

DNS 负载均衡:通过 DNS 解析,来实现地理级别的均衡,其成本低,分配策略很简单,可以就近访问来提升访问速度,但 DNS 的缓存时间长,由于更新不及时所以无法快速调整,且控制权在各域名商下,且无法根据后端服务器的状态来决定分配策略。 硬件负载均衡:直接通过硬件设备来实现负载均衡,类似路由器路由,功能和性能都很强大,可以做到百万并发,也很稳定,支持安全防护能力,但是同样无法根据后端服务器状态进行策略调整,且价格昂贵。软件负载均衡:通过软件逻辑实现,比如 nginx,比较灵活,成本低,但是性能一般,功能也不如硬件强大。

一般来说,DNS 负载均衡用于实现地理级别的负载均衡;硬件负载均衡用于实现集群级别的负载均衡;软件负载均衡用于实现机器级别的负载均衡。

所以部署起来可以按照这三层去部署,第一层通过 DNS 将请求分发到北京、上海、深圳的机房;第二层通过硬件负载均衡将请求分发到当地三个集群中的一个;第三层通过软件策略将请求分发到具体的某台服务器去响应业务。

就负载均衡算法来说,多是我们很熟悉的算法,如轮询、加权轮询、负载最低优先、性能最优优先、Hash 分配等,各有特点,按需采用即可。

03、高可用架构模式

3.1 理论方式

CAP 与 BASE
在说高可用之前,先来说说 CAP 理论,即:

在一个分布式系统(指互相连接并共享数据的节点的集合)中,当涉及读写操作时,只能保证一致性(Consistence)、可用性(Availability)、分区容错性(Partition Tolerance)三者中的两个,另外一个必须被牺牲。

大家可能都知道 CAP 定理是什么,但大家可能不知道,CAP 定理的作者(Seth Gilbert & Nancy Lynch)其实并没有详细解释 CAP 三个单词的具体含义,目前大家熟悉的解释其实是另一个人(Robert Greiner)给出的。而且他还给出了两版有所差异的解释。

书中第二版解释算是对第一版解释的加强,他要加强的点主要是:

CAP 描述的分布式系统,是互相连结并共享数据的节点的集合。因为其实并不是所有的分布式系统都会互连和共享数据。CAP 理论是在涉及读写操作的场景下的理论,而不是分布式系统的所有功能。一致性只需要保障客户端读操作能读到最新的写操作结果,并不要求时时刻刻分布式系统的数据都是一致的,这是不现实的,只要保障客户读到的一致即可。可用性要求非故障的节点在合理的时间内能返回合理的响应,所谓合理是指非错误、非超时,即使数据不是最新的数据,也是合理的“旧数据”,是符合可用性的。分区容错性要求网络分区后系统能继续履行职责,不仅仅要求系统不宕机,还要求能发挥作用,能处理业务逻辑。比如接口直接返回错误其实也代表系统在运行,但却没有履行职责。
在分布式系统下,P(分区容忍)是必须选择的,否则当分区后系统无法履行职责时,为了保障 C(一致性),就要拒绝写入数据,也就是不可用了

在此基础上,其实我们能选择的只有 C+P 或者 A+P,根据业务特性来选择要优先保障一致性还是可用性。

在选择保障策略时,有几个需要注意的点:

CAP 关注的其实是数据的粒度,而不是整个系统的粒度,因此对于系统内的不同数据(对应不同子业务),其实是可以按照业务特性采取不同的 CAP 策略的。CAP 实际忽略了网络延迟,也就是允许数据复制过程中的短时间不一致,如果某些业务比如金融业务无法容忍这一点,那就只能对单个对象做单点写入,其他节点备份,无法做多点写入。但对于不同的对象,其实可以分库来实现分布式。当没有发生分区现象时,也就是不用考虑 P 时,上述限制就不存在,此时应该考虑如何保障 CA。当发生分区后,牺牲 CAP 的其中一个并不代表什么都不用做,而是应该为分区后的恢复 CA 做准备,比如记录分区期间的日志以供恢复时使用。

随 CAP 的一个退而求其次,也更现实的追求,是 BASE 理论,即基本可用,保障核心业务的可用性;软状态,允许系统存在数据不一致的中间状态;最终一致性,一段时间后系统应该达到一致。

FMEA 分析法

要保障高可用,我们该怎么下手呢?俗话说知己知彼才能有的放矢,因此做高可用的前提是了解系统存在怎样的风险,并且还要识别出风险的优先级,先治理更可能发生的、影响更大的风险。说得简单,到底怎么做?业界其实已经提供了排查系统风险的基本方法论,即 FMEA(Failure mode and effects analysis)——故障模式与影响分析。

FMEA 的基本思路是,面对初始的架构设计图,考虑假设其中某个部件发生故障,对系统会造成什么影响,进而判断架构是否需要优化。

具体来说,需要画一张表,按照如下步骤逐个列出:

功能点:列出业务流程中的每个功能点。故障模式:量化描述该功能可能发生怎样的故障,比如 MySQL 响应时间超过3秒。故障影响:量化描述该每个故障可能导致的影响,但不用非常精确,比如20%用户无法登录。严重程度:设定标准,给每个影响的严重程度打分。故障原因:对于每个故障,考虑有哪些原因导致该故障。故障概率:对于每个原因,考虑其发生的概率,不用精确,分档打分即可。风险程度:=严重程度 * 故障概率,据此就可以算出风险的处理优先级了,肯定是程度分数越高的越应该优先解决。已有措施、解决措施、后续规划:用于梳理现状,思考未来的改进方案等。
基于上面这套方法论,我们可以有效地对系统的风险进行梳理,找出需要优先解决的风险点,从而提高系统的可用性。

除了 FMEA,其实还有一种应用更广泛的风险分析和治理的理论,即 BCP——业务连续性计划,它是一套基于业务规律的规章流程,保障业务或组织在面对突发状况时其关键业务功能可以持续不中断。

相比 FMEA,BCP 除了评估风险及重要程度,还要求详细地描述应对方案、残余风险、灾备恢复方案,并要求进行相应故障的培训和演习安排,尽最大努力保障业务连续性。

知道风险在哪、优先治理何种风险之后,就可以着手优化架构。和高性能架构模式一样,高可用架构也可以从存储和计算两个方面来分析。

3.2 存储高可用

存储高可用的本质都是通过将数据复制到多个存储设备,通过数据冗余的方式来提高可用性。

双机架构

让我们先从简单的增加一台机器开始,即双机架构。

当机器变成两台后,根据两台机器担任的角色不同,就会分成不同的策略,比如主备、主从、主主。

主备复制的架构是指一台机器作为客户端访问的主机,另一台机器纯粹作为冗余备份用,当主机没有故障时,备机不会被客户端访问到,仅仅需要从主机同步数据。这种策略很简单,可以应对主机故障情况下的业务可用性问题,但在平常无法分担主机的读写压力,有点浪费。

主从复制的架构和主备复制的差别在于,从机除了复制备份数据,还需要干活,即还需要承担一部分的客户端请求(一般是分担读操作)。当主机故障时,从机的读操作不会受到影响,但需要增加读操作的请求分发策略,且和主备不同,由于从机直接提供数据读,如果主从复制延迟大,数据不一致会对业务造成更明显的影响。

对于主备和主从两种策略,如果主机故障,都需要让另一台机器变成主机,才能继续完整地提供服务,如果全靠人工干预来切换,会比较滞后和易错,最好是能够自动完成切换,这就涉及双机切换的策略。

在考虑双机切换时,要考虑什么?首先是需要感知机器的状态,是两台机器直连传递互相的状态,还是都传递给第三方来仲裁?所谓状态要包含哪些内容才能定义一台主机是故障呢?是发现一次问题就切换还是多观察一会再切换?切换后如果主机恢复了是切换回来还是自动变备机呢?需不需要人工二次确认一下

这些问题可能都得根据业务的特性来得出答案,此处仅给出三种常见的双机切换模式:

互连式:两台机器直接连接传递信息,并根据传递的状态信息判断是否要切换主机,如果通道本身发生故障则无法判断是否要切换了,可以再增加一个通道构成双通道保障,不过也只是降低同时故障的概率。中介式:通过第三方中介来收集机器状态并执行策略,如果通道发生断连,中介可以直接切换其他机器作为主机,但这要求中介本身是高可用的,已经有比较成熟的开源解决方案如 zookeeper、keepalived。模拟式:备机模拟成客户端,向主机发送业务类似的读写请求,根据响应情况来判断主机的状态决定是否要切换主机,这样可以最真实地感受到客户端角度下的主机故障,但和互连式不同,能获取到的其他机器信息很少,容易出现判断偏差。
最后一种双机架构是主主复制,和前面两种只有一主的策略不同,这次两台都是主机,客户端的请求可以达到任何一台主机,不存在切换主机的问题。但这对数据的设计就有了严格的要求,如果存在唯一 ID、严格的库存数量等数据,就无法适用,这种策略适合那些偏临时性、可丢失、可覆盖的数据场景。

数据集群

采用双机架构的前提是一台主机能够存储所有的业务数据并处理所有的业务请求,但机器的存储和处理能力是有上限的,在大数据场景下就需要多台服务器来构成数据集群。

如果是因为处理能力达到瓶颈,此时可以增加从机帮主机分担压力,即一主多从,称为数据集中集群。这种集群方式需要任务分配算法将请求分散到不同机器上去,主要的问题在于数据需要复制到多台从机,数据的一致性保障会比一主一从更为复杂。且当主机故障时,多台从机协商新主机的策略也会变得复杂。这里有开源的 zookeeper ZAB 算法可以直接参考。

如果是因为存储量级达到瓶颈,此时可以将数据分散存储到不同服务器,每台服务器负责存储一部分数据,同时也备份一部分数据,称为数据分散集群。数据分散集群同样需要做负载均衡,在数据分区的分配上,hadoop 采用独立服务器负责数据分区的分配,ES 集群通过选举一台服务器来做数据分区的分配。除了负载均衡,还需要支持扩缩容,此外由于数据是分散存储的,当部分服务器故障时,要能够将故障服务器的数据在其他服务器上恢复,并把原本分配到故障服务器的数据分配到其他正常的服务器上,即分区容错性。

数据分区 

数据集群可以在单台乃至多台服务器故障时依然保持业务可用,但如果因为地理级灾难导致整个集群都故障了(断网、火灾等),那整个服务就不可用了。面对这种情况,就需要基于不同地理位置做数据分区。

做不同地理位置的数据分区,首先要根据业务特性制定分区规则,大多还是按照地理位置提供的服务去做数据分区,比如中国区主要存储中国用户的数据。

既然分区是为了防灾,那么一个分区肯定不止存储自身的数据,还需要做数据备份。从数据备份的策略来说,主要有三种模式:

集中式:存在一个总备份中心,所有的分区数据都往这个总中心备份,设计起来简单,各个分区间没有联系,不会互相影响,也很容易扩展新的分区。但总中心的成本较高,而且总中心如果出故障,就要全部重新备份。互备式:每个分区备份另一个分区的数据,可以形成一个备份环,或者按地理位置远近来搭对备份,这样可以直接利用已有的设备做数据备份。但设计较复杂,各个分区间需要联系,当扩展新分区时,需要修改原有的备份线路。独立式:每个分区配备自己的备份中心,一般设立在分区地理位置附近的城市,设计也简单,各个分区间不会影响,扩展新分区也容易。但是成本会很高,而且只能防范城市级的灾难。

3.3 计算高可用

从存储高可用的思路可以看出,高可用主要是通过增加机器冗余来实现备份,对计算高可用来说也是如此。通过增加机器,分担服务器的压力,并在单机发生故障的时候将请求分配到其他机器来保障业务可用性。

因此计算高可用的复杂性也主要是在多机器下任务分配的问题,比如当任务来临(比如客户端请求到来)时,如何选择执行任务的服务器?如果任务执行失败,如何重新分配呢?这里又可以回到前文说过的负载均衡相关的解法上。

计算服务器和存储服务器在多机器情况下的架构是类似的,也分为主备、主从和集群。

主备架构下,备机仅仅用作冗余,平常不会接收到客户端请求,当主机故障时,备机才会升级为主机提供服务。备机分为冷备和温备。冷备是指备机只准备好程序包和配置文件,但实际平常并不会启动系统。温备是指备机的系统是持续启动的,只是不对外提供服务,从而可以随时切换主机。

主从架构下,从机也要执行任务,由任务分配器按照预先定义的规则将任务分配给主机和从机。相比起主备,主从可以发挥一定的从机性能,避免成本空费,但任务的分配就变得复杂一些。

集群架构又分为对称集群和非对称集群。

对称集群也叫负载均衡集群,其中所有的服务器都是同等对待的,任务会均衡地分配到每台服务器。此时可以采用随机、轮询、Hash 等简单的分配机制,如果某台服务器故障,不再给它分配任务即可。

非对称集群下不同的服务器有不同的角色,比如分为 master 和 slave。此时任务分配器需要有一定的规则将任务分配给不同角色的服务器,还需要有选举策略来在 master 故障时选择新的 master。这个选举策略的复杂度就丰俭由人了。

异地多活

讲存储高可用已经说过数据分区,计算高可用也有类似的高可用保障思路,归纳来说,它们都可以根据需要做异地多活,来提高整体的处理能力,并防范地区级的灾难。异地多活中的”异地“,就是指集群部署到不同的地理位置,“活”则强调集群是随时能提供服务的,不同于“备”还需要一个切换过程。

按照规模,异地多活可以分为同城异区、跨城异地和跨国异地。显而易见,不同模式下能够应对的地区级故障是越来越高的,但同样的,距离越远,通信成本与延迟就越高,对通信通道可用性的挑战也越高。因此跨城异地已经不适合对数据一致性要求非常高的业务,而跨国异地往往是用来给不同国家的用户提供不同服务的。

由于异地多活需要花费很高的成本,极大地增加系统复杂度,因此在设计异地多活架构时,可以不用强求为所有业务都做异地多活,可以优先为核心业务实现异地多活。尽量保障绝大部分用户的异地多活,对于没能保障的用户,通过挂公告、事后补偿、完善失败提示等措施进行安抚、提升体验。毕竟要做到100%可用性是不可能的,只能在能接受的成本下尽量逼近,所以当可用性达到一定瓶颈后,补偿手段的成本或许更低。

在异地部署的情况下,数据一定会冗余存储,物理上就无法实现绝对的实时同步,且距离越远对数据一致性的挑战越大,虽然可以靠减少距离、搭建高速专用网络等方式来提高一致性,但也只是提高而已,因此大部分情况下, 只需考虑保障业务能接受范围下的最终一致性即可。

在同步数据的时候,可以采用多种方式,比如通过消息队列同步、利用数据库自带的同步机制同步、通过换机房重试来解决同步延迟问题、通过 session id 让同一数据的请求都到同一机房从而不用同步等。

可见,整个异地多活的设计步骤首先是对业务分级,挑选出核心业务做异地多活,然后对需要做异地多活的数据进行特征分析,考虑数据量、唯一性、实时性要求、可丢失性、可恢复性等,根据数据特性设计数据同步的方案。最后考虑各种异常情况下的处理手段,比如多通道同步、日志记录恢复、用户补偿等,此时可以借用前文所说的 FMEA 等方法进行分析。

接口级故障

前面讨论的都是较为宏观的服务器、分区级的故障发生时该怎么办,实际上在平常的开发中,还应该防微杜渐,从接口粒度的角度,来防范和应对接口级的故障。应对的核心思路依然是优先保障核心业务和绝大部分用户可用。

对于接口级故障,有几个常用的方法:限流、排队、降级、熔断。其中限流和排队属于事前防范的措施,而降级和熔断属于接口真的故障后的处理手段。

限流的目的在于控制接口的访问量,避免被高频访问冲垮。

从限流维度来说,可以基于请求限流,即限制某个指标下、某个时间段内的请求数量,阈值的定义需要基于压测和线上情况来逐步调优。还可以基于资源限流,比如根据连接数、文件句柄、线程数等,这种维度更适合特殊的业务。

实现限流常用的有时间窗算法和桶算法。

时间窗算法分为固定时间窗和滑动时间窗。

固定时间窗通过统计固定时间周期内的量级来决定限流,但存在一个临界点的问题,如果在两个时间窗的中间发生超大流量,而在两个时间窗内都各自没有超出限制,就会出现无法被限流拦截的接口故障。因此滑动时间窗采用了部分重叠的时间统计周期来解决临界点问题。

桶算法分为漏桶和令牌桶。

漏桶算法是将请求放入桶中,处理单元从桶里拿请求去进行处理,如果桶堆满了就丢弃掉新的请求,可以理解为桶下面有个漏斗将请求往处理单元流动,整个桶的容量是有限的。这种模式下流入的速率取决于请求的频率,当桶内有堆积的待处理请求时,流出速率是匀速的。漏桶算法适用于瞬时高并发的场景(如秒杀),处理可能慢一点,但可以缓存部分请求不丢弃。

令牌桶算法是在桶内放令牌,令牌数是有限的,新的请求需要先到桶里拿到令牌才能被处理,拿不到就会被丢弃。和漏桶匀速流出处理不同,令牌桶还能通过控制放令牌的速率来控制接收新请求的频率,对于突发流量,可靠累计的令牌来处理,但是相对的处理速度也会突增。令牌桶算法适用于控制第三方服务访问速度的场景,防止压垮下游。

除了限流,还有一种控制处理速度的方法就是排队。当新请求到来后先加入队列,出队端通过固定速度出队处理请求,避免处理单元压力过大。队列也有长度限制,其机制和漏桶算法差不多。

如果真的事前防范真的被突破了,接口很可能或已经发生了故障,还能做什么呢?

一种手段是熔断,即当处理量达到阈值,就主动停掉外部接口的访问能力,这其实也是一种防范措施,对外的表现虽然是接口访问故障,但系统内部得以被保护,不会引起更大的问题,待存量处理被消化完,或者外部请求减弱,或完成扩容后,再开放接口。熔断的设计主要是阈值,需要按照业务特点和统计数据制定。

当接口故障后(无论是被动还是主动断开),最好能提供降级策略。降级是丢车保帅,放弃一下非核心业务,保障核心业务可用,或者最低程度能提供故障公告,让用户不要反复尝试请求来加重问题了。比起手动降级,更好的做法也是自动降级,需要具备检测和发现降级时机的机制。

04、可扩展架构模式

再回顾一遍互联网行业的金科玉律:只有变化才是不变的。在设计架构时,一开始就要抱着业务随时可能变动导致架构也要跟着变动的思想准备去设计,差别只在于变化的快慢而已。因此在设计架构时一定是要考虑可扩展性的。

在思考怎样才是可扩展的时候,先想一想平常开发中什么情况下会觉得扩展性不好?大都是因为系统庞大、耦合严重、牵一发而动全身。因此对可扩展架构设计来说,基本的思想就是拆分。

拆分也有多种指导思想,如果面向业务流程来谈拆分,就是分层架构;如果面向系统服务来谈拆分,就是 SOA、微服务架构;如果面向系统功能来拆分,就是微内核架构。

分层架构

分层架构是我们最熟悉的,因为互联网业务下,已经很少有纯单机的服务,因此至少都是 C/S 架构、B/S 架构,也就是至少也分为了客户端/浏览器和后台服务器这两层。如果进一步拆分,就会将后台服务基于职责进行自顶向下的划分,比如分为接入层、应用层、逻辑层、领域层等。

分层的目的当然是为了让各个层次间的服务减少耦合,方便进行各自范畴下的优化,因此需要保证各层级间的差异是足够清晰、边界足够明显的,否则当要增加新功能的时候就会不知道该放到哪一层。各个层只处理本层逻辑,隔离关注点。

额外需注意的是一旦确定了分层,请求就必须层层传递,不能跳层,这是为了避免架构混乱,增加维护和扩展的复杂度,比如为了方便直接跨层从接入层调用领域层查询数据,当需要进行统一的逻辑处理时,就无法切面处理所有请求了。

SOA 架构

SOA 架构更多出现在传统企业中,其主要解决的问题是企业中 IT 建设重复且效率低下,各部门自行接入独立的 IT 系统,彼此之间架构、协议都不同,为了让各个系统的服务能够协调工作,SOA 架构应运而生。

其有三个关键概念:服务、ESB 和松耦合。

服务是指各个业务功能,比如原本各部门原本的系统提供的服务,可大可小。由于各服务之间无法直接通信,因此需要 ESB,即企业服务总线进行对接,它将不同服务连接在一起,屏蔽各个服务的不同接口标准,类似计算机中的总线。松耦合是指各个服务的依赖需要尽量少,否则某个服务升级后整个系统无法使用就麻烦了。

这里也可以看出,ESB 作为总线,为了对接各个服务,要处理各种不同的协议,其协议转换耗费了大量的性能,会成为整个系统的瓶颈。

微服务

微服务是近几年最耳熟能详的架构,其实它和 SOA 有一些相同之处,比如都是将各个服务拆分开来提供能力。但是和 SOA 也有一些本质的区别,微服务是没有 ESB 的,其通信协议是一致的,因此通信管道仅仅做消息的传递,不理解内容和格式,也就没有 ESB 的问题。而且为了快速交付、迭代,其服务的粒度会划分地更细,对自动化部署能力也就要求更高,否则部署成本太大,达不到轻量快速的目的。

当然微服务虽然很火,但也不是解决所有问题的银弹,它也会有一些问题存在。如果服务划分的太细,那么互相之间的依赖关系就会变得特别复杂,服务数量、接口量、部署量过多,团队的效率可能大降,如果没有自动化支撑,交付效率会很低。由于调用链太长(多个服务),因此性能也会下降,问题定位会更困难,如果没有服务治理的能力,管理起来会很混乱,不知道每个服务的情况如何。

因此如何拆分服务就成了每个使用微服务架构的团队的重要考量点。这里也提供一些拆分的思路:

三个火枪手原则:考虑每三个人负责一个服务,互相可以形成稳定的人员备份,讨论起来也更容易得出结论,在此基础上考虑能负责多大的一个服务。基于业务逻辑拆分:最直观的就是按逻辑拆分,如果职责不清,就参考三个火枪手原则确定服务大小。基于稳定性拆分:按照服务的稳定性分为稳定服务和变动服务,稳定服务粒度可以粗一些,变动服务粒度可以细一些,目的是减少变动服务之间的影响,但总体数量依然要控制。基于可靠性拆分:按照可靠性排序,要求高的可以拆细一些,由前文可知,服务越简单,高可用方案就会越简单,成本也会越低。优先保障核心服务的高可用。基于性能拆分:类似可靠性,性能要求越高的,拆出来单独做高性能优化,可有效降低成本。
微服务架构如果没有完善的基础设施保障服务治理,那么也会带来很多问题,降低效率,因此根据团队和业务的规模,可以按以下优先级进行基础设施的支持:

优先支持服务发现、服务路由、服务容错(重试、流控、隔离),这些是微服务的基础。接着支持接口框架(统一的协议格式与规范)、API 网关(接入鉴权、权限控制、传输加密、请求路由等),可以提高开发效率。然后支持自动化部署、自动化测试能力,并搭建配置中心,可以提升测试和运维的效率。最后支持服务监控、服务跟踪、服务安全(接入安全、数据安全、传输安全、配置化安全策略等)的能力,可以进一步提高运维效率。
微内核架构
最后说说微内核架构,也叫插件化架构,顾名思义,是面向功能拆分的,通常包含核心系统和插件模块。在微内核架构中,核心系统需要支持插件的管理和链接,即如何加载插件,何时加载插件,插件如何新增和操作,插件如何和核心引擎通信等。

举一个最常见的微内核架构的例子——规则引擎,在这个架构中,引擎是内核,负责解析规则,并将输入通过规则处理后得到输出。而各种规则则是插件,通常根据各种业务场景进行配置,存储到数据库中。

05、总结

人们通常把某项互联网业务的发展分为四个时期:初创期、发展期、竞争期和成熟期。

在初创期通常求快,系统能买就买,能用开源就用开源,能用的就是好的,先要活下来;到了发展期开始堆功能和优化,要求能快速实现需求,并有余力对一些系统的问题进行优化,当优化到顶的时候就需要从架构层面来拆分优化了;进入竞争期后,经过发展期的快速迭代,可能会存在很多重复造轮子和混乱的交互,此时就需要通过平台化、服务化来解决一些公共的问题;最后到达成熟期后,主要在于补齐短板,优化弱项,保障系统的稳定。

在整个发展的过程中,同一个功能的前后要求也是不同的,随着用户规模的增加,性能会越来越难保障,可用性问题的影响也会越来越大,因此复杂度就来了。

对于架构师来说,首要的任务是从当前系统的一大堆纷繁复杂的问题中识别出真正要通过架构重构来解决的问题,集中力量快速突破,但整体来说,要徐徐图之,不要想着用重构来一次性解决所有问题。

对项目中的问题做好分类,划分优先级,先易后难,才更容易通过较少的资源占用,较快地得到成果,提高士气。然后再循序渐进,每个阶段控制在 1~3 个月,稳步推进。

当然,在这个过程中,免不了和上下游团队沟通协作,需要注意的是自己的目标和其他团队的目标可能是不同的,需要对重构的价值进行换位思考,让双方都可以合作共赢,才能借力前进。

还是回到开头的那句话,架构设计的主要目的是为了解决软件系统复杂度带来的问题。首先找到主要矛盾在哪,做到有的放矢,然后再结合知识、经验进行设计,去解决面前的问题。

祝各位开发者都成为一名合格的架构师。以上便是本次分享的全部内容,如果觉得内容有用,欢迎转发分享。

-End-

原创作者|Cloudox


作者:腾讯云开发者
链接:https://juejin.cn/post/7251779626682023994
来源:稀土掘金

收起阅读 »

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

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

前言


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


来龙去脉


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


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


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


公司现状


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


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


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


裁员情况


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


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


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


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


个人思考


看待裁员


我认为首先是放平心态吧


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


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


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


企业层面


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



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



个人层面


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


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



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



日常准备


做好记录


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


保持学习


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


关注公司


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


保护自己


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


经营关系


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



以前我还有过那种想法:


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


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


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



关于简历


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


再找工作


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


关于空窗期


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


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



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




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



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



关于社保


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


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



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



关于入职


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


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


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


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



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


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



再次强调


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

作者:草帽lufei
链接:https://juejin.cn/post/7264236820725366840
来源:稀土掘金
收起阅读 »

《有钱人和你想的不一样》阅读笔记

作者哈维·艾克,世界顶级商业教练。 作者从小家境窘困,十三岁就开始打工,曾经送报、卖冰淇淋、摆摊子卖货物,还到海滩上兜售防晒油。在约克大学读了一年之后,决定开始全心追求他致富的梦想。他辗转在美国西岸和东岸几个城市里做过许多工作,甚至自己当老板,创业十几次却都...
继续阅读 »

作者哈维·艾克,世界顶级商业教练。


作者从小家境窘困,十三岁就开始打工,曾经送报、卖冰淇淋、摆摊子卖货物,还到海滩上兜售防晒油。在约克大学读了一年之后,决定开始全心追求他致富的梦想。他辗转在美国西岸和东岸几个城市里做过许多工作,甚至自己当老板,创业十几次却都无法成功。


多年的挫败没有把他打垮。他又开了一家贩售体育用品的公司,在两年半里快速成长为连锁企业,开设了十家分店。然后,他把这项事业卖给一家名列《财星》五百大的企业。他赚了一笔大钱。可是,由于他的错误投资和大肆挥霍,不到两年就把钱莫名其妙用光了。他又回到了起点。


他痛定思痛,回顾自己的成长背景和金钱观,分析各种与致富有关的内在思惟,体会到许多与金钱、致富梦想和人生态度有关的道理。他发现,每一个人心里都有一张金钱蓝图,假如不修改这张蓝图,那么就算赚了大钱,这笔钱也不会留在身边太久。


带着这样的信念,他彻底修改了自己的金钱蓝图,最后成为了超级大富翁,并且越来越有钱。


成功之后,他致力于帮助更多人实现致富的梦想,超越现有生活,让人生变得不同。他的潜能开发公司红遍全美,以独特的平民气息吸引了来自世界各地的无数普罗大众,一堂课经常塞满了两千人。很多上过他课的人表示,这堂课大大改变了他们的人生,使他们开始累积财富,得到快乐与成功。


现实社会


大多数人的现状:





  • 对其财政状况担忧而夜不能寐,早上睁眼起来,就去上班



  • 怀抱希望,自己马上就能赚到可以付清账单的钱。而当他们收入提高时,开支又梦魇般地同步增多



  • 唯一解决方法就是努力工作。希望通过多赚钱,最终变得节俭。但是生活水平同收入平行增长。负债日益增加。


这部分人就必须越来越努力而长时间地工作才能保持生活水平,以偿还债务和账单。


像仓鼠的轮子:尽管他们尽全力,却没有进步。同理一些人不管多努力地工作,只要处于仓鼠的轮子中,他就是原地踏步,这种现象在《财务自由之路2》被称为仓鼠之轮。


财富档案


书里讲了17个有钱人的思维习惯,其实书里有钱人17个思维习惯和大多数成功人士书籍所讲大同小异。我摘录几种我认为重要的:





  1. 有钱人相信:"我创造我的人生"。穷人相信:"人生发生在我的身上"。必须相信,只有你能造成自己的成功,也只有你能造成你的平庸,只有你能造成自己为钱辛苦或是前途茫茫。不管出于自觉还是不自觉的原因,你的人生状态都是自己造成的。行动上远离负面能量的人,每天检视自己的行为



  2. 有钱人努力让自己有钱。穷人一直想着要变得有钱。如果你并没有达到你嘴巴上所说的很渴望达到的财富成就,很可能是1. 潜意识时并不真的想变得有钱;2. 并不想付出应有的代价去创造财富



  3. 有钱人专注于机会。穷人专注于障碍。有钱人冒的是经过计划的风险,他们会做研究,付出必要的努力,再根据具体的资讯和事实做出决定。穷人,他们期待失败。他们对于自己和自己的能力都没有信心。穷人相信,如果事情行不通,就像世界末日来临。而正因为他们总是看见阻碍,所以他们通常都不愿意冒险。



  4. 有钱人欣赏其他的有钱人和成功人士,穷人讨厌有钱人和成功人士。不论你现在什么状态,首先做到不讨厌其他人,积极从其他人身上学到优点或教训



  5. 有钱人乐意宣传自己和自己价值观,穷人把推销和宣传看成不好的事。敢于推销自己和自己的产品,学习推销能力,不会或不敢推销一定不会成功。



  6. 有钱人大于他们的问题,穷人小于他们的问题。遇到问题不要急于去抱怨和责怪,因为抱怨和责怪来自于恐惧,要明白如果遇到了一个很大的问题,说明自己很小,要么列出多条解决方案去解决问题,要么行动起来去将自己变大。不要专注于问题,而专注于目标。



  7. 有钱人选择根据结果拿酬劳。穷人选择根据时间拿酬劳。稳定的薪酬会阻碍你挣更多的钱。要么选择可以以自己的成果或者公司的运营来领薪的工作,要么自己当老板,可以从兼职做起。



  8. 有钱人专注于自己的净值。穷人专注于自己的工作收入。净值指你所拥有的所有东西,现金,不动产,股票等等可以转化成现金的东西。提高收入,不乱花钱增加存款,有了存款加强投资,简化不必要的花销。看理财类书籍



  9. 有钱人管理他们的钱,穷人很会搞丢他们的钱。不是等我拥有了很多钱后我再开始管理,而是我开始管理我的钱了以后,我才会拥有很多钱。所以会管理钱很重要,不要在乎他们的多少。



  10. 有钱人让钱帮他们工作。穷人幸苦工作挣钱。一开始你努力工作挣钱,之后就要让钱为你努力工作。被动收入很重要,一定要去培养和开发。被动收入大于你的支出时,你才算真正的财务自由



  11. 有钱人就算恐惧也会采取行动,穷人却会让恐惧阻止他们行动有些事一开始做起来是痛苦的,但是你坚持下去就可以穿过痛苦区到达舒适区,不断的扩大你的舒适区因为舒适区和财务区是成正比的,如果一直待在舒适区你是不会成长的



  12. 有钱人持续学习成长,穷人以为他们知道了一切。一件事只有你经历了才有资格说你知道,否则你只是听说过看到过或读到过。如果能学到一个知识或者有了一点进步就是值得的。


最后





  • 行动比不行动好,只会YY没卵用



  • 培养财商很重要,有点小钱绝对不是只有买!买!买!



  • 月光族没希望,劳碌命,学会储蓄,积累一笔初始资金



  • 巴菲特说的:世界上最好的投资,就是投资你自己


如果我的文章对你有帮助,您的👍就是对我的最大支持^_^。

作者:凌览
来源:mdnice.com/writing/d714945d3c0f4a29b02c5e4488341632
收起阅读 »

《Thinking In Java》作者:不要使用并发!

前言 今天纯粹就是带你们来读读书的~ 最近除了工作,特地买回了自己很喜欢的作者新发售的一本书《On Java》,作者是我的老朋友布鲁斯·埃克尔,在Java领域很有名,你可能没听过他的名字,但极有可能听过他的另一本书《Thinking In Java》,我想很...
继续阅读 »

前言



今天纯粹就是带你们来读读书的~


最近除了工作,特地买回了自己很喜欢的作者新发售的一本书《On Java》,作者是我的老朋友布鲁斯·埃克尔,在Java领域很有名,你可能没听过他的名字,但极有可能听过他的另一本书《Thinking In Java》,我想很多Java工程师都读过这本书,可以说是Java编程思想的良心之作。


虽然布鲁斯是我的老朋友,但我不得不吐槽一下,大概通读了一遍《On Java》之后,我心里大体认为是不如《Thinking In Java》的,可能和写小说一样,读者的要求高了,而作者的年纪大了。


我认识布鲁斯很多年了,他是个比较幽默风趣的人,经常在书中直言不讳某编程语言的垃圾之处,同时又对该语言的未来做一点展望,算是一个很中肯且典型的直男程序猿。


最后说一点,我认识他,他不认识我。





正文



我着重看了自己比较感兴趣的并发编程这一块,想知道这位大佬对于目前Java并发编程是否有新的看法和意见,不出我所料,他没讲什么重要的东西,但是好像又讲了,带着吐槽批判式的口吻,陈列了他喜欢和讨厌Java并发编程的地方。


所以我把一些我觉得有意思的地方画出来,分享给大家,看看一个资深Java大佬对并发编程的理解。



1、大佬的并发定律


111.png



其实看到作者研究出的这4条定律时,我还是挺意外的,第一句就点题了,不要使用并发。


仔细想想好像也对……再琢磨一下咦有感觉……最后回忆一下这些年参与的项目……哇擦好有道理!


接下来3条基本算是总纲了,后面的内容都是对这几条的说明。





2、你已埋下的隐患


222.png



这里就是对2、3条的具体说明了,有些话我觉得略显啰嗦,我把对于程序员来讲比较重要的一句话画出来了。


你很容易写出一个看起来运行正常但实际上有问题的并发程序。


看到这句话的时候是不是已经开始默默打开自己的IDEA了,然后审视了一遍自己提交的代码?


别看了,你埋的炸弹还少么,能看出花来吗。


看清楚作者后面那句:你这个问题只有满足最罕见的条件时,才会将自己暴露出来。


我可以这么说,在座绝大部分同行去了下一家公司干活,可能上一家公司的新同事才会在你毫不知情的时候默默踩到你埋的地雷然后被炸个粉碎,而你在新公司也正在踩别人的雷,出来混都是要还的。





3、别否认你就是这种人


333.png



看到这里的时候,我忍不住亲了布鲁斯一口,他痛快的描述出了我一直以来在工作中说不清道不明的烦躁,因为你总会遇到这样的人,同时很难发现自己到底是不是这样的人。


我在工作前3年其实如履薄冰,感觉自己什么都学了,但去了公司发现什么都不会,怀揣着自我否定一点点完成别人布置的任务,直到工作5年以后才有一种醍醐灌顶的感觉,理解了自己做的是什么,接下来要学习哪个方向,以前学到那么多东西究竟是怎么串联起来的,这是一种打通任督二脉的满足感。


等到工作8年之后,才真正开始回头看Java语言,对以前烦厌欲呕的Java基础提起莫名的兴趣,同时喜欢看书,写案例,尝试阅读别人的源码等等,此时我才真正有自己一只腿迈进Java领域的意识。


同时,在工作中会对许多能力一般但沟通较为偏执的同事产生抵触情绪,我有时会认为这是一种大人看小孩耍脾气的感觉,这个只有在工作多年之后才会产生,作者很准确的阐述出了我描绘不出的这种解释。


同样的,我认为在这个成长的过程中,我一定也成为过别人心中眼高手低的人。


我在这里能分享给大家的经验就是,在工作中多学习少争论,多和厉害的人走近一点,虚心把对方的东西都学过来,长此以往你会进步神速,这不是你在网上学习能得到的,一定是在工作中。





4、高级Javaer都有过的想法


444.png



这里我为什么专门画出来,因为很多高级javaer一定有过类似的想法,就是发现了Java并不擅长做并发编程,是否可以用其他语言来完成,而Java只做他自己擅长的事。


至少我以前就想过,可现实层面我认为是异想天开的,尤其是工作中,基本都是团队开发,这种想法就已经几乎被pass掉了,同时为了某一个领域的实现专门引入一门编程语言甚至体系,得不偿失,毕竟Java不擅长但却成熟,光是网上卖课郎告诉你的就有N种诸如《Java千亿级高并发解决方案》、《Java万亿级电商实战》等等这样的受用终生的鬼东西。


而你辛辛苦苦跟着学完后,发现玛德用不上,就像你学了《九阴真经》后以为可以当武林盟主最终却进了铁匠铺,而铁匠铺老板还不想听你鬼扯只想让你每天加班多打几把武器。


图片中我还画了个圈,我想不少人应该知道这门语言,还蛮有名的,就是国内不太火,这有编程历史因素在里面,其实还有一门语言也蛮适合的,而且这几年也挺火,我想你也猜到了,我觉得5年+的Java工程师都应该关注甚至学习一下。





5、我和大佬不谋而合


555.png



这是接近尾声的部分了,也是这位作者熟悉的笔法,发泄完自己的情绪后又开始对Java的某新版本极尽赞美,典型的被PUA了。


但不得不说,Java8我也认为是革命性的版本,在这个版本发布以前,作为Java工程师你甚至不会想到它敢做到这个地步,就像布鲁斯书中讲的,这是史诗般的魔法。


你可以在Java8的版本里发现一些其他语言的影子,这没什么,天下语言一大抄,发展到一定程度,已经是避免不了的趋势了。


重要的是,这个版本给Java上油了,为后续的版本提供了活力,而Java17作为官方长久支持版本的其中一个非常重要的版本,你可以发现有其他框架给它背书,比如SpringBoot3只支持Java17,而Jenkins也宣布在新版本放弃Java8并且该团队更推荐Java17,IDEA后续新版本可能也会放弃Java8,这明显就是小圈子,有利益的勾连,但对Java本身发展不是坏事。


所以,Java8的核心技术点最应该学习,如果现在还一点不会,赶紧学吧,我认为这是后续版本的基础了,lambada表达式、stream流不必说了,是Java8版本的核心技术,CompletableFuture作为Java8并发编程中最大的改进要花时间好好学习,这也是本书作者所提到的,而且后面专门花了一个大章来讲CompletableFuture。


作者虽然一直强调不要使用并发,但却对Java8的并发编程工具花了较大篇幅,我个人认为他更多的是一种见猎心喜,可是我们面试经常会问到这个工具类相关的东西,看一下大佬对该工具的理解还是很有用的。





总结



《On Java》这本书说实话,我觉得没有作者的《Thinking In Java》写得好,可能有多种原因导致。


我说下我觉得不好的主要感受在哪里,一是有些地方翻译的不好,会给你带来困惑,二是作者给出的一些案例有自己的风格,而且例子我没觉得那么通俗易懂。


但总体上还是值得一看,尤其是他穿插了很多和其他如C/C++、GO等语言的比较,还包含了自己对Java的理解,尤其是一些编程思想很直接,最后给出了林林总总有接近70条的编程指南,我认为对于初学者树立未来工作中的编程思想是很有用的。


这位作者的文字中弥漫着一股浓烈的不推荐使用并发编程的味道,我觉得是他多年工作的心得,所以大家在往后的工作中不妨可以借鉴下大佬的思维。


好了,我今天也就是带你读了下书,读的还开心吗。







本人原创文章纯手打,觉得有一滴滴帮助的话就请点个赞和收藏吧~


本人长期分享工作中的感悟、经验及实用案例,喜欢的话也可以进入个人主页关注一下哦~


作者:程序员济癫
来源:juejin.cn/post/7147523943321042980
收起阅读 »