注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

如何制作 GitHub 个人主页

iOS
原文链接:http://www.bengreenberg.dev/posts/2023-… 人们在网上首先发现你的地方是哪里?也许你的社交媒体是人们搜索你时首先发现的东西,亦也许是你为自己创建的投资组合网站。然而,如果你使用GitHub来分享你的代码并参与开源...
继续阅读 »

原文链接:http://www.bengreenberg.dev/posts/2023-…


人们在网上首先发现你的地方是哪里?也许你的社交媒体是人们搜索你时首先发现的东西,亦也许是你为自己创建的投资组合网站。然而,如果你使用GitHub来分享你的代码并参与开源项目,那么你的GitHub个人主页可能是人们为了了解你而去的第一个地方。


你希望你的GitHub个人主页说些什么?你希望如何以简明易读的方式向访客表达对你的重要性以及你是谁?无论他们是未来的雇主还是开源项目的潜在合作伙伴,你都必须拥有一个引人注目的个人主页。


使用GitHub Actions,你可以把一个静态的markdown文档变成一个动态的、保持对你最新信息更新的良好体验。那么如何做到这一点呢?


我将向你展示一个例子,告诉你如何在不费吹灰之力的情况下迅速做到这一点。在这个例子中,你将学习如何抓取一个网站并使用这些数据来动态更新你的GitHub个人主页。我们将在Ruby中展示这个例子,但你也可以用JavaScript、TypeScript、Python或其他语言来做。


GitHub个人主页如何运作


你的GitHub个人主页可以通过在网页浏览器中访问github.com/[你的用户名]找到。那么该页面的内容来自哪里?


它存在于你账户中一个特殊的仓库中,名称为你的账户用户名。如果你还没有这个仓库,当你访问github.com/[你的用户名]时,你不会看到任何特殊的内容,所以第一步是确保你已经创建了这个仓库,如果你还没有,就去创建它。


探索仓库中的文件


仓库中唯一需要的文件是README.md文件,它是你的个人主页页面的来源。

./
├── README.md

继续在这个文件中添加一些内容并保存,刷新你的用户名主页,你会看到这些内容反映在那里。


为动态内容添加正确的文件夹


在我们创建代码以使我们的个人主页动态化之前,让我们先添加文件夹结构。


在顶层添加一个名为.github的新文件夹,在.github内部添加两个新的子文件夹:scripts/workflows/


你的文件结构现在应该是这样的:

./
├── .github/
│ ├── scripts/
│ └── workflows/
└── README.md

制作一个动态个人主页


对于这个例子,我们需要做三件事:


  • README中定义一个放置动态内容的地方
  • scripts/中添加一个脚本,用来完成爬取工作
  • workflows/中为GitHub Actions添加一个工作流,按计划运行该脚本

现在让我们逐步实现。


更新README


我们需要在README中增加一个部分,可以用正则来抓取脚本进行修改。它可以是你的具体使用情况所需要的任何内容。在这个例子中,我们将在README中添加一个最近博客文章的部分。


在代码编辑器中打开README.md文件,添加以下内容:

### Recent blog posts

现在我们有了一个供脚本查找的区域。


创建脚本


我们正在构建的示例脚本是用Ruby编写的,使用GitHub gem octokit与你的仓库进行交互,使用nokogiri gem爬取网站,并使用httparty gem进行HTTP请求。


在下面这个例子中,要爬取的元素已经被确定了。在你自己的用例中,你需要明确你想爬取的网站上的元素的路径,毫无疑问它将不同于下面显示的在 posts 变量中定义的,以及每个post的每个titlelink


下面是示例代码,将其放在scripts/文件夹中:

require 'httparty'
require 'nokogiri'
require 'octokit'

# Scrape blog posts from the website
url = "<https://www.bengreenberg.dev/blog/>"
response = HTTParty.get(url)
parsed_page = Nokogiri::HTML(response.body)
posts = parsed_page.css('.flex.flex-col.rounded-lg.shadow-lg.overflow-hidden')

# Generate the updated blog posts list (top 5)
posts_list = ["\n### Recent Blog Posts\n\n"]
posts.first(5).each do |post|
title = post.css('p.text-xl.font-semibold.text-gray-900').text.strip
link = "<https://www.bengreenberg.dev#{post.at_css('a')[:href]}>"
posts_list << "* [#{title}](#{link})"
end

# Update the README.md file
client = Octokit::Client.new(access_token: ENV['GITHUB_TOKEN'])
repo = ENV['GITHUB_REPOSITORY']
readme = client.readme(repo)
readme_content = Base64.decode64(readme[:content]).force_encoding('UTF-8')

# Replace the existing blog posts section
posts_regex = /### Recent Blog Posts\n\n[\s\S]*?(?=<\/td>)/m
updated_content = readme_content.sub(posts_regex, "#{posts_list.join("\n")}\n")

client.update_contents(repo, 'README.md', 'Update recent blog posts', readme[:sha], updated_content)

正如你所看到的,首先向网站发出一个HTTP请求,然后收集有博客文章的部分,并将数据分配给一个posts变量。然后,脚本在posts变量中遍历博客文章,并收集其中的前5个。你可能想根据自己的需要改变这个数字。每循环一次博文,就有一篇博文被添加到post_list的数组中,其中有该博文的标题和URL。


最后,README文件被更新,首先使用octokit gem找到它,然后在README中找到要更新的地方,并使用一些正则: posts_regex = /### Recent Blog Posts\n\n[\s\S]*?(?=<\/td>)/m


这个脚本将完成工作,但实际上没有任何东西在调用这个脚本。它是如何被运行的呢?这就轮到GitHub Actions出场了!


创建Action工作流


现在我们已经有了脚本,我们需要一种方法来按计划自动运行它。GitHub Actions 提供了一种强大的方式来自动化各种任务,包括运行脚本。在这种情况下,我们将创建一个GitHub Actions工作流,每周在周日午夜运行一次该脚本。


工作流文件应该放在.github/workflows/目录下,可以命名为update_blog_posts.yml之类的。以下是工作流文件的内容:

name: Update Recent Blog Posts

on:
schedule:
- cron: '0 0 * * 0' # Run once a week at 00:00 (midnight) on Sunday
workflow_dispatch:

jobs:
update_posts:
runs-on: ubuntu-latest

steps:
- name: Check out repository
uses: actions/checkout@v2

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.1

- name: Install dependencies
run: gem install httparty nokogiri octokit

- name: Scrape posts and update README
run: ruby ./.github/scripts/update_posts.rb
env:
GITHUB_TOKEN: $
GITHUB_REPOSITORY: $

这个工作流是根据cron语法定义的时间表触发的,该时间表指定它应该在每个星期天的00:00(午夜)运行。此外,还可以使用workflow_dispatch事件来手动触发该工作流。


update_posts工作由几个步骤组成:


  • 使用 actions/checkout@v2操作来签出仓库。
  • 使用 ruby/setup-ruby@v1 操作来设置 Ruby,指定的 Ruby 版本为 3.1。
  • 使用 gem install 命令安装所需的 Ruby 依赖(httpartynokogiri 和 octokit)。
  • 运行位于.github/scripts/目录下的脚本 update_posts.rbGITHUB_TOKENGITHUB_REPOSITORY环境变量被提供给脚本,使其能够与仓库进行交互。

有了这个工作流程,你的脚本就会每周自动运行,抓取博客文章并更新README文件。GitHub Actions负责所有的调度和执行工作,使整个过程无缝且高效。


将所有的东西放在一起


如今,你的网络形象往往是人们与你联系的第一个接触点--无论他们是潜在的雇主、合作者,还是开源项目的贡献者。尤其是你的GitHub个人主页,是一个展示你的技能、项目和兴趣的宝贵平台。那么,如何确保你的GitHub个人主页是最新的、相关的,并能真正反映出你是谁?


通过利用 GitHub Actions 的力量,我们展示了如何将你的 GitHub 配置文件从一个静态的 Markdown 文档转变为一个动态的、不断变化关于你是谁的例子。通过本指南提供的例子,你已经学会了如何从网站上抓取数据,并利用它来动态更新你的 GitHub个人主页。虽然我们的例子是用Ruby实现的,但同样的原则也可以用JavaScript、TypeScript、Python或你选择的任何其他语言来应用。


回顾一下,我们完成了创建一个Ruby脚本的过程,该脚本可以从网站上抓取博客文章,提取相关信息,并更新你的README.md文件中的"最近博客文章"部分。然后,我们使用GitHub Actions设置了一个工作流,定期运行该脚本,确保你的个人主页中保持最新的内容。


但我们的旅程并没有就此结束。本指南中分享的技术和方法可以作为进一步探索和创造的基础。无论是从其他来源拉取数据,与API集成,还是尝试不同的内容格式,都有无限的可能性。


因此,行动起来让你的 GitHub 个人主页成为你自己的一个充满活力的扩展。让它讲述你的故事,突出你的成就,并邀请你与他人合作。


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

谈谈饭碗的边界问题

主题 不知觉间,写东西也坚持一年多了,这一年间歇性的思考、充斥着空杯吸收的忙碌和工作之外的尝试,最近一段和领导之间的思考有所共鸣,记录下来,希望能引起边界问题的思考吧。 负重与前行 去年的严冬渲染,在今年3~4月份达到了顶峰,去年的焦虑最重,我所得出来的结论是...
继续阅读 »

主题


不知觉间,写东西也坚持一年多了,这一年间歇性的思考、充斥着空杯吸收的忙碌和工作之外的尝试,最近一段和领导之间的思考有所共鸣,记录下来,希望能引起边界问题的思考吧。


负重与前行


去年的严冬渲染,在今年3~4月份达到了顶峰,去年的焦虑最重,我所得出来的结论是:“即便是有天大的本事,也失去了意义”,得出这个结论的前提是,我已经尽我所能的驱动自己全盘吸收,认真做事,在此之外不停的摸索第二种小范围的业务试水,成功了一部分,但远远达不到预期的效果。


“日中则昃,月盈则食”,也许是预期见底,觉得即便是见底了也没多大了不起的事情,心态上好像好听点叫背水一战、悲观一些叫预期见底,已然死猪不怕开水烫。


搁置争议


因为一些非我这个层级的事情,但莫名其妙的旁观参与,和领导有一番恳谈,最终的思路基本上也就归结为 “还年轻,最终能依靠的也就是自身的能力,这个能力既包含执行、学习速度、业务、当然也包含为人处事的灵活性,但最重要的是选择大于努力”,事实上,从程序员的角度来讲,我已不再年轻,但相较于领导还算年轻,可能是角度不同,认知稍微有些差别,但大的方向没有任何问题,偏重点有所不同,领导对 “业务和学习速度” 很推崇,“工作处事的机变” 差不多是基本要求了😵,至于选择,其实也已经没多大选择了。


于我而言,我的定位其实一直立足于 “执行” 这个层级,并觉得以此为根基,空杯心理去掌握业务、学习速度、文档、软件全周期等内容,这基本也是我一直以来的理念,近一年多,基本接触的形形色色厉害的人有许多,事务杂,内容多,各种杂七杂八的东西,但不妨碍近一年多逐渐的总结和认识不足,可见性的提升是巨大的。 边界的问题,基本上属于 “能力边界是公司给个体划定的边界,你必须符合这个水平线之上,但是个人应该是对自己不设边界,但可以划定阶段方向”,我就以实际的接触来谈。


之后,和相对能够听得进去的同事也有讨论,毕竟绝大多数都是 “鸵鸟心态,今日不忧明日事,大事临身心态蹦”,对互联网从业者,没有人会相信一个人可以在一个公司待一辈子,但即便是有规划者,也很难在局限中做出合适的选择,但总有一条,心中愈惧怕愈是自身欠缺的,也许是个排错的选项。


拉回正题,集中讨论的话题也在于语言发展和执行力的问题上,就软件执行力而言,以单端来说,执行力和责任心,均算不错,但基本有个问题就是人为设定自身边界和定位,导致的结果就是一直在舒适区画圈,也仅此而已,我技术上学习模型基本上属于 结果->解决->问题->资料->细节 ,但接触许多人,往往纯纯的就是依靠,总觉得有人能解决,以蒙混过关的心态解决问题,从来不会涉及一个问题以月为单位摸索,即便这个事情已经过去了。


认知上,年龄到了这个阶段,单纯的开发执行能力在一般的事务上没啥本质的竞争力,因为复杂度的上限就在哪里,同事们在去年的环境思想鞭挞中,已经充分的有了认知,最后的结论都落脚在到那一天再说,陡然之间,可能发生的事情已然有了时间线的征兆,似乎一下子有些不知所措。


所以?


愕然也好,有准备也罢,但于我而言,能力的认可和肯定以及自我的肯定,让我的内心,在逐步见好的招聘中,找到了意义,也期待第二个年头,更强大有底气的自我,后面的着力点也会往行业的宽泛性、汇报交流的表达能力、架构设计的层次化展现力上去争取提升,当然,软件的开发能力是绝对不能松懈的,至于时间和精力,谁说这段没有悄悄提升自己的生产力工具呢, 重复性开发工作和一些杂项,已然没啥提升诉求的工作,必然是要借助工具释放自身的生产力了,而我又该继续往感兴趣的方向去学习了...


容我吐槽


Github语言趋势分析系列貌似发布总是说有啥不符合规范,唉,总是在发布的时候遭退~~~。


另外最近感觉好多外头和工作的事情累加了好几项,心态越发的好了,事情推进的有条不紊的,相比于之前,万事靠自身,其实合作也不错。


PS


发现身边事儿、聊点周奇遇,我是沈二,期待奇遇的互联网灵魂~、一起聊天吹水,探索新的可能~wx:breathingss,入圈吧!


附录



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

SF Symbols 4 使用指南

iOS
本文基于 WWDC 2022 Session 10157 和 Session 10158 梳理,为了更方便没有 SF Symbols 经验的读者理解,也将往年的 SF Symbols 相关内容一并整理。本文从 SF Symbols 4 的新特性切入,讨论 SF...
继续阅读 »

本文基于 WWDC 2022 Session 10157Session 10158 梳理,为了更方便没有 SF Symbols 经验的读者理解,也将往年的 SF Symbols 相关内容一并整理。本文从 SF Symbols 4 的新特性切入,讨论 SF Symbols 这款由系统字体支持的符号库有哪些优点以及该如何使用。在这次 WWDC 2022 中,除了符号的数量的增加到了 4000+ 之外,还有自动渲染模式、可变符号等新特性推出,让 SF Symbols 这把利器变得又更加趁手和锋利了。




本文是 WWDC22 内参 的供稿。



什么是 SF Symbols


符号在界面中起着非常重要的作用,它们能有效地传达意义,它们可以表明你选择了哪些项目,它们可以用来从视觉上区分不同类型的内容,他们还可以节约空间、整洁界面,而且符号出现在整个视觉系统的各处,这使整个用户界面营造了一种熟悉的感觉。


符号的实现和使用方式多种多样,但设计和使用符号时有一个亘古不变的问题,那就是将符号与用户界面的另一个基本元素——「文本」很好地配合。符号和文字在用户界面中以各种不同的大小被使用,他们之间的排列形式、对齐方式、符号颜色、文本字重与符号粗细的协调、本地化配置以及无障碍设计都需要开发者和设计师来细心配置和协调。




为了方便开发者更便捷、轻松地使用符号,Apple 在 iOS 13 中开始引入他们自己设计的海量高质量符号,称之为 SF Symbols。SF Symbols 拥有超过 4000 个符号,是一个图标库,旨在与 Apple 平台的系统字体 San Francisco 无缝集成。每个符号有 9 种字重和 3 种比例,以及四种渲染模式,它们的默认设计都与文本标签对齐,同时这些符号是矢量的,这意味着它们是可以被拉伸的,使得他们在无论用什么大小时都会呈现出很好的效果。如果你想去创造具有相似设计特征或无障碍功能的自定义符号,它们也可以被导出并在矢量图形编辑工具中进行编辑以创建新的符号。


对于开发者来说,这套 SF Symbols 无论是在 UIKit,AppKit 还是 SwiftUI 中都能运作良好,且使用方式也很简单方便,寥寥数行代码就可以实现。对于设计师来说,你只需要为符号只做三个字重的版本,SF Symbols 会自动地帮你生成其余 9 种字重和 3 种比例的符号,然后在 SF Symbols 4 App 中调整四种渲染模式的表现,就制作好了一份可以高度定制化的 symbol。




如何使用 SF Symbols


SF Symbols 4 App


在开始介绍如何使用 SF Symbols 之前,我们可以先下载来自 Apple 官方的 SF Symbols 4 App,这款 App 中收录了所有的 SF Symbols,并且记录了每个符号的名称,支持的渲染模式,可变符号的分层预览,不同语言下的变体,不同版本下可能出现的不同的名称,并且可以实时预览不同渲染模式下不同强调色的不同效果。你可以在这里下载 SF Symbols 4 App。




符号的渲染模式


通过之前的图片你可能已经注意到了,SF Symbols 可以拥有多种颜色,有一些 symbol 还有预设的配色,例如代表天气、肺部、电池的符号等等。如果要使用这些带有自定义颜色的符号,你需要知道,SF Symbols 在逻辑上是预先分层的(如下图的温度计符号就分为三层),根据每一层的路径,我们可以根据渲染模式来调整颜色,而每个 SF Symbols 有四种渲染模式。




单色模式 Monochrome


在 iOS 15 / macOS 11 之前,单色模式是唯一的渲染模式,顾名思义,单色模式会让符号有一个单一的颜色。要设置单色模式的符号,我们只需要设置视图的 tint color 等属性就可以完成。

let image = UIImage(systemName: "thermometer.sun.fill")
imageView.image = image
imageView.tintColor = .systemBlue

// SwiftUI
Image(systemName: "thermometer.sun.fill")
.foregroundStyle(.blue)

分层模式 Hierarchical


每个符号都是预先分层的,如下图所示,符号按顺序最多分成三个层级:Primary,Secondary,Tertiary。SF Symbols 的分层设定不仅在分层模式下有效,在后文别的渲染模式下也是有作用的




分层模式和单色模式一样,可以设置一个颜色。但是分层模式会以该颜色为基础,生成降低主颜色的不透明度而衍生出来的其他颜色(如上上图中的温度计符号看起来是由三种灰色组合而成)。在这个模式中,层级结构很重要,如果缺少一个层级,相关的派生颜色将不会被使用。

let image = UIImage(systemName: "thermometer.sun.fill")
let config = UIImage.SymbolConfiguration(hierarchicalColor: .lightGray)
imageView.image = image
imageView.preferredSymbolConfiguration = config

// SwiftUI
Image(systemName: "thermometer.sun.fill")
.foregroundStyle(.gray)
.symbolRenderingMode(.hierarchical)

调色盘模式 Palette


调色盘模式和分层模式很像,但也有些许不同。和分层模式一样是,调色盘模式也会对符号的各个层级进行上色,而不同的是,调色盘模式允许你自由的分别设置各个层级的颜色。

let image = UIImage(systemName: "thermometer.sun.fill")
let config = UIImage.SymbolConfiguration(paletteColors: [.lightGray, .cyan, .systemTeal])
imageView.image = image
imageView.preferredSymbolConfiguration = config

// SwiftUI
Image(systemName: "thermometer.sun.fill")
.foregroundStyle(.lightGray, .cyan, .teal)

多色模式 Muticolor


在 SF Symbols 中,有许多符号的意象在现实生活中已经深入人心,比如:太阳应该是橙色的,警告应该是黄色的,叶子应该是绿色的的等等。所以 SF Symbols 也提供了与现实世界色彩相契合的颜色模式:多色渲染模式。当你使用多色模式的时候,就能看到预设的橙色太阳符号,红色的闹铃符号,而你不需要指定任何颜色。

let image = UIImage(systemName: "thermometer.sun.fill")
imageView.image = image
imageView.preferredSymbolConfiguration = .preferringMulticolor()

// SwiftUI
Image(systemName: "thermometer.sun.fill")
.symbolRenderingMode(.multicolor)

自动渲染模式 Automatic


谈论完了四种渲染模式,可以发现每次设置 symbol 的渲染模式其实也是一件费心的事情。为了解决这个问题,在最新的 SF Symbols 中,每个 symbol 都有了一个自动渲染模式。例如下图的 shareplay 符号,你可以看到在右侧面板中,shareplay 符号的第二个模式(分层模式)的下方有一个空心小圆点,这意味着该符号在代码中使用时,假如你不去特意配置他的渲染模式,那么他将使用分层模式作为他的默认渲染模式。



你可以在 SF Symbols 4 App 中查询到所有符号的自动渲染模式。





可变颜色


在有的时候,符号并不单单代表一个单独的概念或者意象,他也可以代表一些数值、比例或者程度,例如 Wi-Fi 强度或者铃声音量,为了解决这个问题,SF Symbols 引入了可变颜色这个概念。


你可以在 SF Symbol 4 App 中的 Variable 目录中找到所有有可变颜色的符号,平且可以通过右侧面板的滑块来查看不同百分比程度下可变颜色的形态。另外你也可以注意到,可变颜色的可变部分实际上也是一种分层的表现,但这里的分层和上文提到的渲染模式使用的分层是不同的。一个符号可以在渲染模式中只分两层,在可变颜色的分层中分为三层,下图中第二个符号喇叭 speaker.wave.3.fill 就是如此。关于这里的分层我们会在后文如何制作可变颜色中详细讨论。




在代码中,我们只需要在初始化 symbol 时增加一个 Double 类型的 variableValue 参数,就可以实现可变颜色在不同程度下的不同形态。值得注意的是,假如你的可变颜色(例如上图 Wi-Fi 符号)可变部分有三层,那么这个 variableValue 的判定将会三等分:在 0% 时将不高亮信号,在 0%~33% 时,将高亮一格信号,在 34%~67 % 时,将高亮 2 格信号,在 68% 以上时,将会显示满格信号。

let img = NSImage(symbolName: "wifi", variableValue: 0.2)

可变颜色的可变部分是利用不透明度来实现的,当可变颜色和不同的渲染模式结合后,也会有很好的效果。




如何制作和调整可变颜色


在 SF Symbols 4 App 中,我们可以自定义或者调整可变颜色的表现,接下来我将带着大家以 party.popper 这个符号为基础制作一个带可变颜色的符号。

  1. 首先我们打开 SF Symbols 4 App,在右上角搜索 party.popper,找到该符号后右键选择 复制为1个自定符号。推荐你在上方将符号的排列方式修改为画廊模式,如下图所示。


  2. 可以注意到右下角的  这个板块,这个符号默认是由两个层级组成的,分别是礼花和礼花筒,同时我们也可以看到,礼花和礼花筒又分别是由更零碎的路径组成的,通过勾选子路径我们可以给每个层新增或者减少路径。那我现在想要给这个符号新增一层,我只需要在画廊模式下,将符号的某一部分拖拽到层里就可以。


  3. 通过这样的操作,我们可以将这个符号整理为四层:礼花筒、线条礼花、小球礼花和大球礼花。为了可变颜色的效果,我们需要按照从下到上:礼花筒、线条礼花、大球礼花和小球礼花的顺序去放置层级,另外,我们可以切换到分层模式、调色板模式和多色模式里面去调整成自己喜欢的颜色来预览效果,我这里调整了多色模式中的配色,具体效果如下。


  4. 接下来,我们将前三层,也就是除了礼花筒外的三层,最右侧的可变符号按钮选中,来表示这三层将可以在可变符号的变化范围内活动。接下来,只要点击颜色区域内的可变符号按钮,我们就可以拖动滑块来查看可变颜色的形态。


  5. 至此,我们就完成了一个带可变颜色的自定义符号,我们可以在合适的地方使用这个符号。例如我的 App 有一个 4 个步骤的新手引导,这时候就可以给每一个步骤配备一个符号来让界面变得更加的活泼。


统一注释 Unified annotations


其实我们已经接触到了 Unified annotations 这个过程,它就是将符号的层级,路径以及子路径整理成在四个渲染模式下都能良好工作的过程,就如同上文彩色礼花筒的例子,我们通过统一注释,让彩色礼花筒符号在不同渲染模式、不同环境色、不同主题色下,都能良好的运作。


那一般来说,对于单色模式,不需要过多的调整,它就能保持良好的形态;对于分层模式和调色盘模式,我们需要在给每个层设定好哪个是 Primary 层、哪个是 Secondanry 层以及哪个是 Tertiary 层,这样系统就会按优先级给符号上合适的颜色;对于多色模式,我们可以根据喜好以及符号的意义,给它预设一个合理的颜色,另外还要注意的是,如果设计了可变颜色在符号中,那么要注意保持可变符号的效果在四个渲染模式上都表现正常。


除了这些之外,还有一些特别的地方需要注意,我们以 custom.heart.circle.fill 为例子。你可以注意到,这个垃爱心符号是有一个圆形的背景的,在这种情况下,假如我们按照原来的规则去绘制单色模式,会发现:背景的圆形和爱心的图案将会是同一个颜色,那我们就将看不见圆形背景下的图案了。




这时我们可以使用 Unified annotations 给我们提供的新功能,我们将上图在 板块的爱心,将它从 Draw 改成 Erase,这样,我们就相当于以爱心的形状镂空了这个白色的背景,从而使该图形展现了出来并且在单色模式下能够一直表现正常。同理,在分层模式和调色盘模式中,也有这个 Erase 的功能共大家调整使用。


字重和比例


SF Symbols 和 Apple 平台的系统字体 San Francisco 一样,拥有九种字重和三种比例可以选择,这意味着每个 SF Symbol 都有 27 种样式以供使用。

let config = UIImage.SymbolConfiguration(pointSize: 20, weight: .semibold, scale: .large)
imageView.preferredSymbolConfiguration = config

// SwiftUI
Label("Heart", systemImage: "heart")
.imageScale(.large)
.font(.system(size: 20, weight: .semibold))

符号的字重和文本的字重原理相同,都是通过加粗线条来增加字重。但 SF Symbols 的三种比例尺寸并不是单纯的对符号进行缩放。如果你仔细观察,会发现对于同一个字重,但是不同比例的符号来说,他们线条的粗细是一样的,但是对符号的整体进行了扩充和延展,以应对不一样的使用环境。


要实现这样的效果,意味着每个 symbol 的底层逻辑并不是一张张图片,而是由一组组的路径构成,这也是为什么在当你想要自定义一个属于自己的 symbol 的时候,官方要求你用封闭路径 + 填充效果去完成一个符号,而不是使用一条简单路径 + 路径描边(stroke)来完成一个符号。



更多关于如何制作一个 symbol 的内容,请移步 WWDC 21 内参:定制属于你的 Symbols





除了字重和比例之外,SF Symbols 还在很多方面进行了努力来方便开发者的工作,例如:符号的变体、不同语言下符号的本地化、符号的无障碍化等,关于这些内容,以及其它由于篇幅原因未在本文讨论的细节问题,请移步 WWDC 21 内参:SF Symbols 使用指南


总结


从上文介绍 SF Symbols 的特性和优点我们可以看到,它的出现是为了解决符号与文本之间的协调性问题,在保证了本地化、无障碍化的基础上,Apple 一直在实用性、易用度以及多样性上面给 SF Symbols 加码,目前已经有了 4000+ 的符号可以使用,相信在未来还会有更多。这些符号的样式和图案目前看来并不是那么的广泛,这些有限的符号样式并不能让设计师安心代替所有界面上的符号,但是有失必有得,在这样一个高度统一的平台上,SF Symbols 在规范化、统一化、表现能力、代码与设计上的简易程度,在今年都又进一步的提升了,达到了让人惊艳的程度,随着 SF Symbols 的继续发展,我相信对于部分开发者来说,即将成为一个最优的符号工具🥳。


更多资料


以下是这几年关于 SF Symbols 的资料:



以下是更早的 SF Symbols 资料:



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

用 Metal 画一个三角形(Swift 函数式风格)

iOS
由于今年工作中用得语言换成 Rust/OCaml/ReScript 啦,所以导致我现在写代码更倾向于写函数式风格的代码。 顺便试试 Swift 在函数式方面能达到啥好玩的程度。主要是我不会 Swift,仅仅为了好玩。 创建工程 随便创建个工程,小玩具就不打算跑...
继续阅读 »

由于今年工作中用得语言换成 Rust/OCaml/ReScript 啦,所以导致我现在写代码更倾向于写函数式风格的代码。

顺便试试 Swift 在函数式方面能达到啥好玩的程度。主要是我不会 Swift,仅仅为了好玩。


创建工程


随便创建个工程,小玩具就不打算跑在手机上了,因为我的设备是 ARM 芯片的,所以直接创建个 Mac 项目,记得勾上包含测试。


构建 MTKView 子类


现在来创建个 MTKView 的子类,其实我现在已经不接受这种所谓的面向对象,开发者用这种方式,就要写太多篇幅来描述一个上下文结构跟函数就能实现的动作。

import MetalKit

class MetalView: MTKView {
required init(coder: NSCoder) {
super.init(coder: coder)
device = MTLCreateSystemDefaultDevice()
render()
}
}

extension MetalView {
func render() {
// TODO: 具体实现
}
}

我们这里给 MetalView extension 了一个 render 函数,里面是后续要写得具体实现。


普通的方式画一个三角形


先用常见的方式来画一个三角形

class MetalView: MTKView {
required init(coder: NSCoder) {
super.init(coder: coder)
device = MTLCreateSystemDefaultDevice()
render()
}
}

extension MetalView {
func render() {
guard let device = device else { fatalError("Failed to find default device.") }
let vertexData: [Float] = [
-1.0, -1.0, 0.0, 1.0,
1.0, -1.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0
]

let dataSize = vertexData.count * MemoryLayout<Float>.size
let vertexBuffer = device.makeBuffer(bytes: vertexData, length: dataSize, options: [])
let library = device.makeDefaultLibrary()
let renderPassDesc = MTLRenderPassDescriptor()
let renderPipelineDesc = MTLRenderPipelineDescriptor()
if let currentDrawable = currentDrawable, let library = library {
renderPassDesc.colorAttachments[0].texture = currentDrawable.texture
renderPassDesc.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.5, blue: 0.5, alpha: 1.0)
renderPassDesc.colorAttachments[0].loadAction = .clear
renderPipelineDesc.vertexFunction = library.makeFunction(name: "vertexFn")
renderPipelineDesc.fragmentFunction = library.makeFunction(name: "fragmentFn")
renderPipelineDesc.colorAttachments[0].pixelFormat = .bgra8Unorm
let commandQueue = device.makeCommandQueue()
guard let commandQueue = commandQueue else { fatalError("Failed to make command queue.") }
let commandBuffer = commandQueue.makeCommandBuffer()
guard let commandBuffer = commandBuffer else { fatalError("Failed to make command buffer.") }
let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDesc)
guard let encoder = encoder else { fatalError("Failed to make render command encoder.") }
if let renderPipelineState = try? device.makeRenderPipelineState(descriptor: renderPipelineDesc) {
encoder.setRenderPipelineState(renderPipelineState)
encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
encoder.endEncoding()
commandBuffer.present(currentDrawable)
commandBuffer.commit()
}
}
}
}

然后是我们需要注册的 Shader 两个函数

#include <metal_stdlib>

using namespace metal;

struct Vertex {
float4 position [[position]];
};

vertex Vertex vertexFn(constant Vertex *vertices [[buffer(0)]], uint vid [[vertex_id]]) {
return vertices[vid];
}

fragment float4 fragmentFn(Vertex vert [[stage_in]]) {
return float4(0.7, 1, 1, 1);
}

在运行之前需要把 StoryBoard 控制器上的 View 改成我们写得这个 MTKView 的子类。




自定义操作符


函数式当然不是指可以定义操作符,但是没有这些操作符,感觉没有魂灵,所以先定义个管道符


代码实现

precedencegroup SingleForwardPipe {
associativity: left
higherThan: BitwiseShiftPrecedence
}

infix operator |> : SingleForwardPipe

func |> <T, U>(_ value: T, _ fn: ((T) -> U)) -> U {
fn(value)
}

测试管道符


因为创建项目的时候,勾上了 include Tests,直接写点测试代码,执行测试。

final class using_metalTests: XCTestCase {
// ...

func testPipeOperator() throws {
let add = { (a: Int) in
return { (b: Int) in
return a + b
}
}
assert(10 |> add(11) == 21)
let doSth = { 10 }
assert(() |> doSth == 10)
}
}

目前随便写个测试通过嘞。


Functional Programming


现在需要把上面的逻辑分割成小函数,事实上,因为 Cocoa 的基础是建立在面向对象上的,我们还是没法完全摆脱面向对象,目前先小范围应用它。


生成 MTLBuffer


先理一下逻辑,代码开始是创建顶点数据,生成 buffer

fileprivate let makeBuffer = { (device: MTLDevice) in
let vertexData: [Float] = [
-1.0, -1.0, 0.0, 1.0,
1.0, -1.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0
]

let dataSize = vertexData.count * MemoryLayout<Float>.size
return device.makeBuffer(bytes: vertexData, length: dataSize, options: [])
}

创建 MTLLibrary


接着是创建 MTLLibrary 来注册两个 shader 方法,还创建了一个 MTLRenderPipelineDescriptor 对象用于创建 MTLRenderPipelineState,但是创建的 MTLLibrary 对象是一个 Optional 的,所以其实得有两步,总之先提取它再说吧

fileprivate let makeLib = { (device: MTLDevice) in device.makeDefaultLibrary() }

抽象 map 函数


根据我们有限的函数式编程经验,像 Optional 这种对象大概率有一个 map 函数,所以我们自家实现一个,同时还要写成柯里化的(建议自动柯里化语法糖入常),因为这里有逃逸闭包,所以要加上 @escaping

func map<T, U>(_ transform: @escaping (T) throws -> U) rethrows -> (T?) -> U? {
return { (o: T?) in
return try? o.map(transform)
}
}

处理 MTLRenderPipelineState


这里最终目的就是 new 了一个 MTLRenderPipelineState,顺带处理把程序的一些上下文给渲染管线描述器(MTLRenderPipelineDescriptor),譬如我们用到的着色器(Shader)函数,像素格式。
最后一行直接 try! 不处理错误啦,反正出问题直接会抛出来的

fileprivate let makeState = { (device: MTLDevice) in
return { (lib: MTLLibrary) in
let renderPipelineDesc = MTLRenderPipelineDescriptor()
renderPipelineDesc.vertexFunction = lib.makeFunction(name: "vertexFn")
renderPipelineDesc.fragmentFunction = lib.makeFunction(name: "fragmentFn")
renderPipelineDesc.colorAttachments[0].pixelFormat = .bgra8Unorm
return (try! device.makeRenderPipelineState(descriptor: renderPipelineDesc))
}
}

暂时收尾


已经不想再抽取函数啦,其实还能更细粒度地处理,因为函数式有个纯函数跟副作用的概念,像 Haskell 里是可以用 Monad 来处理副作用的情况,这个主题留给后续吧。先把 render 改造一下

fileprivate let render = { (device: MTLDevice, currentDrawable: CAMetalDrawable?) in
return { state in
let renderPassDesc = MTLRenderPassDescriptor()
if let currentDrawable = currentDrawable {
renderPassDesc.colorAttachments[0].texture = currentDrawable.texture
renderPassDesc.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.5, blue: 0.5, alpha: 1.0)
renderPassDesc.colorAttachments[0].loadAction = .clear
let commandQueue = device.makeCommandQueue()
guard let commandQueue = commandQueue else { fatalError("Failed to make command queue.") }
let commandBuffer = commandQueue.makeCommandBuffer()
guard let commandBuffer = commandBuffer else { fatalError("Failed to make command buffer.") }
let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDesc)
guard let encoder = encoder else { fatalError("Failed to make render command encoder.") }
encoder.setRenderPipelineState(state)
encoder.setVertexBuffer(device |> makeBuffer, offset: 0, index: 0)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
encoder.endEncoding()
commandBuffer.present(currentDrawable)
commandBuffer.commit()
}
}
}

然后再调用,于是就变成下面这副鸟样子

class MetalView: MTKView {
required init(coder: NSCoder) {
super.init(coder: coder)
device = MTLCreateSystemDefaultDevice()
device |> map {
makeLib($0)
|> map(makeState($0))
|> map(render($0, self.currentDrawable))
}
}
}

最后执行出这种效果




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

展开&收起,使用SwiftUI搭建一个侧滑展开页面交互

iOS
项目背景 闲来无事,在使用某云音乐听歌的时候发现一个侧滑展开的内页,交互效果还不错。 那么这一章节中,我们将使用SwiftUI搭建一个侧边展开页面交互。 项目搭建 首先,创建一个新的SwiftUI项目,命名为SlideOutMenu。 逻辑分析 首先我们来分...
继续阅读 »

项目背景


闲来无事,在使用某云音乐听歌的时候发现一个侧滑展开的内页,交互效果还不错。


那么这一章节中,我们将使用SwiftUI搭建一个侧边展开页面交互。


项目搭建


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




逻辑分析


首先我们来分析下基本的逻辑,一般的侧滑展开方式的交互是,在首页右上角有一个“更多”的按钮,点击按钮时,内页菜单从左往右划出,滑出至离右边20~30的位置停止。


然后首页背景将蒙上一个蒙层,点击蒙层时,侧滑展开的页面从右往左收起


简单分析完逻辑后,我们来实现这个交互。


首页入口


首先,我们需要在首页搭建一个入口,示例:

// 顶部导航入口
private var moreBtnView: some View {
    Button(action: {
    }) {
        Image(systemName: "list.bullet")
            .foregroundColor(.black)
    }
}

然后,我们可以使用NavigationViewnavigationBarItems创建顶部导航按钮样式,示例:

var body: some View {
    NavigationView {
        Text("点击左上角侧滑展开")
            .padding()
            .navigationBarTitle("首页", displayMode: .inline)
            .navigationBarItems(leading: moreBtnView)
    }
}



如此,首页入口部分我们就完成了。


左边菜单


接下来,我们来构建左侧菜单的内容。我们可以沿用之前设计过的“设置”页面的结构,我们先来构建栏目结构。示例:

// MARK: 栏目结构
struct listItemView: View {
    var itemImage: String
    var itemName: String
    var body: some View {
        Button(action: {
        }) {
            HStack {
                Image(systemName: itemImage)
                    .font(.system(size: 17))
                    .foregroundColor(.black)
                Text(itemName)
                    .foregroundColor(.black)
                    .font(.system(size: 17))
                Spacer()
                Image(systemName: "chevron.forward")
                    .font(.system(size: 14))
                    .foregroundColor(.gray)
            }.padding(.vertical, 10)
        }
    }
}

在我们构建侧滑展开的页面前,我们需要声明两个变量,一个是侧滑展开的页面的宽度,一个是当前这个页面的位置。示例:

@State var menuWidth = UIScreen.main.bounds.width - 60
@State var offsetX = -UIScreen.main.bounds.width + 60

我们设置的侧滑展开页面的宽度是屏幕宽度-60,而当前侧滑展开页面的位置是负位置,这样就可以在展示的时候先把页面隐藏起来


而当我们点击顶部导航中的“更多”按钮时,将offsetX偏移量X轴坐标设置为0。示例:

// 顶部导航入口
private var moreBtnView: some View {
    Button(action: {
        withAnimation {
            offsetX = 0
        }
    }) {
        Image(systemName: "list.bullet")
            .foregroundColor(.black)
    }
}

然后,我们创建一个新视图来构建侧滑展开的页面内容,示例:

// MARK: 左侧菜单
struct SlideOutMenu: View {
    @Binding var menuWidth: CGFloat
    @Binding var offsetX: CGFloat

    var body: some View {
        Form {
            Section {
            }
            Section {
                listItemView(itemImage: "lock", itemName: "账号绑定")
                listItemView(itemImage: "gear.circle", itemName: "通用设置")
                listItemView(itemImage: "briefcase", itemName: "简历管理")
            }
            Section {
                listItemView(itemImage: "icloud.and.arrow.down", itemName: "版本更新")
                listItemView(itemImage: "leaf", itemName: "清理缓存")
                listItemView(itemImage: "person", itemName: "关于掘金")
            }
        }
        .padding(.trailing, UIScreen.main.bounds.width - menuWidth)
        .edgesIgnoringSafeArea(.all)
        .shadow(color: Color.black.opacity(offsetX != 0 ? 0.1 : 0), radius: 5, x: 5, y: 0)
        .offset(x: offsetX)
        .background(
            Color.black.opacity(offsetX == 0 ? 0.5 : 0)
                .ignoresSafeArea(.all, edges: .vertical)
                .onTapGesture {
                    withAnimation {
                        offsetX = -menuWidth
                    }
                })
    }
}

上述代码中,我们也对页面宽度menuWidth、偏移位置offsetX进行了声明,方便之后我们在ContentView视图中进行双向绑定


我么使用Form表单和Section段落构建样式,这点就不说了。


值得说的一点是,我们设置了在页面展开的时候,也就是offsetX页面偏移量X轴坐标不为0,我们加了一个阴影,完善了侧滑展开页面的悬浮效果


然后使用offset调整页面初始位置。背景部分,除了根据offsetX页面偏移量X轴坐标加了一个蒙层,而且当我们点击的背景的时候,我们将偏移位置offsetX重新赋值,这样就能实现收起的交互效果。


我们在ContentView视图中展示侧滑展开视图,示例:

var body: some View {
    ZStack {
        NavigationView {
            Text("点击左上角侧滑展开")
                .padding()
                .navigationBarTitle("首页", displayMode: .inline)
                .navigationBarItems(leading: moreBtnView)
        }
        SlideOutMenu(menuWidth: $menuWidth, offsetX: $offsetX)
    }
}

项目展示




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


快来动手试试吧。


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

程序员要学会“投资知识”

iOS
啊,富兰克林,那家伙总是说些深刻的道理。嗯,我们真的可以通过早睡早起变成优秀的程序员吗?早起的鸟儿可能抓住虫子,但早起的虫子会怎么样呢? 然而,富兰克林的开场白确实击中了要害 - 知识和经验确实是你最有价值的职业资产。 不幸的是,它们是有限的资产。随着新技术的...
继续阅读 »

啊,富兰克林,那家伙总是说些深刻的道理。嗯,我们真的可以通过早睡早起变成优秀的程序员吗?早起的鸟儿可能抓住虫子,但早起的虫子会怎么样呢?


然而,富兰克林的开场白确实击中了要害 - 知识和经验确实是你最有价值的职业资产。


不幸的是,它们是有限的资产。随着新技术的出现和语言环境的发展,你的知识可能会过时。不断变化的市场力量可能会使你的经验变得陈旧和无关紧要。考虑到技术和社会变革的加速步伐,这可能会发生得特别迅速。


随着你的知识价值的下降,你在公司或客户那里的价值也会降低。我们希望阻止所有这些情况的发生。


学习新知识的能力是你最关键的战略资产。但如何获取学习的方法,知道要学什么呢?


知识投资组合。


我们可以将程序员对计算过程、其工作应用领域的了解以及所有经验视为他们的知识投资组合。管理知识投资组合与管理金融投资组合非常相似:


1、定期的投资者有定期投资的习惯。


2、多样化是长期成功的关键。


3、聪明的投资者在投资组合中平衡保守和高风险高回报的投资。


4、投资者在低点买入,在高点卖出以获取最大回报。


5、需要定期审查和重新平衡投资组合。


为了在职业生涯中取得成功,你必须遵循相同的指导原则管理你的知识投资组合。


好消息是,管理这种类型的投资就像任何其他技能一样 - 它可以被学会。诀窍是从一开始就开始做,并养成习惯。制定一个你可以遵循并坚持的例行程序,直到它变成第二天性。一旦达到这一点,你会发现自己自动地吸收新的知识。


建立知识投资组合。


· 定期投资。 就像金融投资一样,你需要定期地投资你的知识投资组合,即使数量有限。习惯本身和总数量一样重要,所以设定一个固定的时间和地点 - 这有助于你克服常见的干扰。下一部分将列出一些示例目标。


· 多样化。 你知道的越多,你变得越有价值。至少,你应该了解你目前工作中特定技术的细节,但不要止步于此。计算机技术变化迅速 - 今天的热门话题可能在明天(或至少不那么受欢迎)变得几乎无用。你掌握的技能越多,你的适应能力就越强。


· 风险管理。 不同的技术均匀地分布在从高风险高回报到低风险低回报的范围内。把所有的钱都投资在高风险的股票上是不明智的,因为它们可能会突然崩盘。同样,你不应该把所有的钱都投资在保守的领域 - 你可能会错过机会。不要把你的技术鸡蛋都放在一个篮子里。


· 低买高卖。 在新兴技术变得流行之前开始学习可能就像寻找被低估的股票一样困难,但回报可能同样好。在Java刚刚发明出来后学习可能是有风险的,但那些早期用户在Java变得流行时获得了可观的回报。


· 重新评估和调整。 这是一个动态的行业。你上个月开始研究的时髦技术可能现在已经降温了。也许你需要刷新一下你很久没有使用过的数据库技术的知识。或者,你可能想尝试一种不同的语言,这可能使你在新的角色中处于更好的位置......


在所有这些指导原则中,下面这个是最简单实施的。


(程序员的软技能:ke.qq.com/course/6034346)


定期在你的知识投资组合中进行投资。


目标。


既然你有了一些指导原则,并知道何时添加什么到你的知识投资组合中,那么获取构成它的智力资产的最佳方法是什么呢?以下是一些建议:


· 每年学习一门新语言。


不同的语言以不同的方式解决相同的问题。学习多种不同的解决方案有助于拓宽你的思维,避免陷入常规模式。此外,由于充足的免费资源,学习多门语言变得更加容易。


· 每月阅读一本技术书籍。


尽管互联网上有大量的短文和偶尔可靠的答案,但要深入理解通常需要阅读更长的书籍。浏览书店页面,选择与你当前项目主题相关的技术书籍。一旦养成这个习惯,每月读一本书。当你掌握了所有当前使用的技术后,扩大你的视野,学习与你的项目无关的东西。


· 也阅读非技术书籍。


请记住,计算机是被人类使用的,而你所做的最终是为了满足人们的需求 - 这是至关重要的。你与人合作,被人雇佣,甚至可能会面临来自人们的批评。不要忘记这个方程式的人类一面,这需要完全不同的技能(通常被称为软技能,听起来可能很容易,但实际上非常具有挑战性)。


· 参加课程。


在当地大学或在线寻找有趣的课程,或者你可能会在下一个商业博览会或技术会议上找到一些课程。


· 加入当地的用户组和论坛。


不要只是作为观众成员;要积极参与。孤立自己对你的职业生涯是有害的;了解你公司之外的人在做什么。


· 尝试不同的环境。


如果你只在Windows上工作,花点时间在Linux上。如果你对简单的编辑器和Makefile感到舒适,尝试使用最新的复杂IDE,反之亦然。


· 保持更新。


关注不同于你当前工作的技术。阅读相关的新闻和技术文章。这是了解使用不同技术的人的经验以及他们使用的特定术语的极好方式,等等。


持续的投资是至关重要的。一旦你熟悉了一门新的语言或技术,继续前进并学习另一门。


无论你是否在项目中使用过这些技术,或者是否应该将它们放在你的简历上,都不重要。学习过程将拓展你的思维,开启新的可能性,并赋予你在处理任务时的新视角。思想的跨领域交流是至关重要的;尝试将你所学应用到你当前的项目中。即使项目不使用特定的技术,你仍然可以借鉴其中的思想。例如,理解面向对象编程可能会导致你编写更具结构的C代码,或者理解函数式编程范 paradigms 可能会影响你如何处理Java等等。


学习机会。


你正在狼吞虎咽地阅读,始终站在你领域的突破前沿(这并不是一项容易的任务)。然而,当有人问你一个问题,你真的不知道的时候,不要停在那里 - 把找到答案当做一个个人挑战。问问你周围的人或在网上搜索 - 不仅在主流圈子中,还要在学术领域中搜索。


如果你自己找不到答案,寻找能够找到答案的人,不要让问题无解地悬而未决。与他人互动有助于你建立你的人际网络,你可能会在这个过程中惊喜地找到解决其他无关问题的方法 - 你现有的知识投资组合将不断扩展。


所有的阅读和研究需要时间,而时间总是不够的。因此,提前准备,确保你在无聊的时候有东西可以阅读。在医院排队等候时,通常会有很好的机会来完成一本书 - 只需记得带上你的电子阅读器。否则,你可能会在医院翻阅旧年鉴,而里面的折叠页来自1973年的巴布亚新几内亚。


批判性思维。


最后一个要点是对你阅读和听到的内容进行批判性思考。你需要确保你投资组合中的知识是准确的,没有受到供应商或媒体炒作的影响。小心狂热的狂热分子,他们认为他们的观点是唯一正确的 - 他们的教条可能不适合你或你的项目。


不要低估商业主义的力量。搜索引擎有时只是优先考虑流行的内容,这并不一定意味着这是你最好的选择;内容提供者也可以支付费用来使他们的材料排名更高。书店有时会将一本书突出地摆放,但这并不意味着它是一本好书,甚至可能不受欢迎 - 这可能只是有人支付了那个位置。


(程序员的软技能:ke.qq.com/course/6034346)


批判性分析你所阅读和听到的内容。


批判性思维本身就是一个完整的学科,我们鼓励你深入研究和学习这门学科。让我们从这里开始,提出一些发人深省的问题。


· 五问“为什么”。


我最喜欢的咨询技术之一是至少连续问五次“为什么”。这意味着在得到一个答案后,你再次问“为什么”。像一个坚持不懈的四岁孩子提问一样重复这个过程,但请记住要比孩子更有礼貌。这样做可以让你更接近根本原因。


· 谁从中受益?


尽管听起来可能有点功利主义,但追踪金钱的流动往往可以帮助你理解潜在的联系。其他人或其他组织的利益可能与你的利益保持一致,也可能不一致。


· 背景是什么?


一切都发生在自己的背景下。这就是为什么声称“解决所有问题”的解决方案通常站不住脚,宣扬“最佳实践”的书籍或文章经不起审查的原因。 “对谁最好?” 是一个需要考虑的问题,以及关于前提条件、后果以及情况是短期还是长期的问题。


· 在何种情况下和何地可以起作用?


在什么情况下?是否已经太晚了?是否还太早了?不要只停留在一阶思维(接下来会发生什么);参与到二阶思维中:接下来会发生什么?


· 为什么这是一个问题?


是否有一个基础模型?这个基础模型是如何工作的?


不幸的是,如今找到简单的答案是具有挑战性的。然而,通过广泛的知识投资组合,并对你遇到的广泛技术出版物进行一些批判性分析,你可以理解那些复杂的答案。


(程序员的软技能:ke.qq.com/course/6034346)


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

也谈“前端已死”

一、一些迹象 逛社区,偶然看到了这张图片: 嗯……我眉头一皱,久久不语,心想,有这么夸张吗,假的吧? 突然想到,最近我在社区发了个前端招聘的信息,结果简历漫天纷飞,塞爆邮箱。 莫非,前端这个岗位真的不再是供不应求了?🤔 二、原因分析 我细想下,也差不多到时候...
继续阅读 »

一、一些迹象


逛社区,偶然看到了这张图片:



嗯……我眉头一皱,久久不语,心想,有这么夸张吗,假的吧?


突然想到,最近我在社区发了个前端招聘的信息,结果简历漫天纷飞,塞爆邮箱。


莫非,前端这个岗位真的不再是供不应求了?🤔


二、原因分析


我细想下,也差不多到时候了。


从16年到现在,算算,7年的时间了。


前端大火就是从16年开始的,多种原因,包括:


移动互联网的兴起,传统行业的数字化转型,大前端技术的普及等。


紧接着是Vue为代表的前端框架和工具的兴起,使得前端开发的门槛进一步降低,前端也成为进入互联网圈子的最快最容易的跳板,促使前端圈进一步繁荣。


然而,连王菲都知道,没有什么是长盛不衰的。



发展,稳定,衰落是亘古不变的事物发展规律。


各种迹象表明,无论是有意还是无意,目前互联网的发展似乎进入了平稳期,这也意味着岗位的需求也开始变得平稳,而涌入这个行业的新人却没有停止,这就必然导致到了某个时间点,前端从业人员会达到饱和,于是那些没有竞争力的人就会遇到求职困境。


遇困的人多了,在社区的声音多了,自然也就会出现“前端已死”这样的言论。


三、破局之道


想要改变这种现状,只能是下面两种方法。


一是烧香拜佛,祈祷互联网大环境好转,最好再来一波生产力或生产环境的变革,让前端行业再赶上一波发展的春风,催生更大的岗位需求,何愁就业?


但显然,寄希望于大环境是不靠谱的,生产力虽然一定是往上走的,但说不定不是助力行业的发展,而是革了行业的命。


比方说现在很火的chatGPT,你说是会增加前端岗位呢,还是空窗加倍绝绝子?


所以,要想前端碗端得稳,前端饭吃得香,还是得靠下面这个方法,也就是想办法提高个人的核心竞争力。


提高核心竞争力


所谓核心竞争力,说白了,就是你能干别人干不了的活,能做别人做不了的事情。


更直白一点,就是你能给团队创造比别人更多的价值。


很普通的一句话,对不对?但是意识到和意识不到,那可是天差地别。


最近虽然收到了很多简历,但是看完之后都只能无奈摇头,不能说一模一样嘛,可以说极其雷同,缺少区分度。


专业技能均是全覆盖,工作描述均是自己用了什么前端框架,做了什么什么工作。


没有任何吸引人的信息,给人感觉,就是个普通的前端从业人员,领导安排个需求,然后接受,排期,完成开发,上线,这种。



这就……对吧,不是不给机会,实在是给不了。


一百份简历竞争一个招聘HC,肯定是把面试机会留给那些有突出亮点的人的。


拿工作描述举例,你一个一个罗列你做的项目,用了哪些技术有什么用?所有投简历的人都有做项目,都有使用前端技术,你的这些描述完全就是废话,简历扔垃圾箱的那种。


不需要扯那么多,你就说你比别人牛在什么地方!


注意,这个牛,不一定就是技术水平或者业务成果,任何亮点都可以,只要是能够做到别人做不到的事情,同时是对团队有帮助的,都可以。


举几个例子:


– 我参与了团队所有项目的开发,“所有”就是亮点,隐约让人觉得你是可信任的。


– 我是团队下班最晚的,工作最积极的。也是亮点,可以提,工时越长,通常产出越多,性价比就越高。


– 我在团队里做了很多看不见的工作。亮点,主动承担边缘工作不是所有人都可以做到的。


– 我是团队内分享(面授或文章都可以)次数第一。亮点,加分,帮助团队成长也是一种价值产出。


– 我连续获得四星五星荣誉,或者优秀员工称号,加分,公司的认可比自己在简历上吹上天都有用。


甚至是工作以外的特长都可以,我是钓鱼大佬,我是跑步达人,我是综艺专家,我是健身狂人,都可以,因为一个人能坚持自己的爱好并做到出众,也是不简单的。



可偏偏问题就在于,能够获得面试机会的亮点如此简单,很多人却没有,一个也没有。


因为在日常工作中就没有这种意识,就是我要做得比别人更好、我要强化我的优势、我要想办法让团队变得更好的意识。


平时工作就是浑浑噩噩的状态,等需求,写代码,上线,拿钱,一切都是在被动进行,仅把前端当作职业而非事业,总是希望干活少,拿钱多。


所以做事难以精益求精,也不会为了更好的未来努力让当下的自己变得更好,也不会主动做那些工作以外的对团队有帮助的事情,典型的被网上的躺平言论给忽悠瘸了。


弄错了因果,即,我给老板加班,又不会给我涨薪,我为什么要加班?我学习更底层的技术,平时又用不到,我为什么要学?我平时工作那么忙,还要我去写文档做分享,我为什么要做?


所以,找不到工作就不要怨天尤人了,也别说什么“前端已死”,前端行业好着呢,优秀的前端不知道多缺,年薪不知道有多高!


框架的能力


很多人做开发非常熟练,各种得心应手,于是就会觉得自己是个挺有竞争力的前端开发人员。



高启强没有说话,只是呵呵一笑。


这是不小心把框架的能力当作自己的能力了。


大家不妨冷静想一想,借助一个成熟的框架,开发出一个合格的Web应用,他的难度有多高?


更具体点,我们经常使用的各种小程序和快应用,让一个培训班里培训了3个月的新人,以及充足的时间,他能不能捣鼓出来?


答案显而易见,肯定可以,至少绝大多数人都可以。


因为使用一个东西的难度要比创造一个东西的难度低多了。


也就是,基于Vue等前端框架的开发,它是需要技术的,但是,它并不需要的很高的技术。


这种状态最容易迷惑人,所谓满瓶不动半瓶摇。


如果不能跳出自己所处的环境,正在更高的视角看待自己,非常容易对自己在行业所处的层次造成误判,譬如,我明明干活很利索,怎么没有面试机会,一定是我们这个行业出问题了。


这就是误判,有问题的不是行业,而是自己的竞争力不足。


我再说一遍,希望大家不要嫌啰嗦,使用工具的能力,并不能作为核心竞争力,因为现在学习资料很丰富,社区很活跃,什么问题都可以找到解决方案,你能做到的别人也能做到,没有任何优势,不属于竞争力。


反而是下面这些能力有足够的区分度。


  • 比他人涉猎更广,例如音视频处理、图形表现实现或者Node开发有较多经验;
  • JS、CSS等前端基本功扎实,积累深厚,各种API特性了然于心,最佳实践信手捏来;
  • 具有设计审美或者产品嗅觉灵敏,开发的产品体验非常好,干活很细。

拥有这些能力或特质,并在简历上表现出来,最好有材料佐证,那找到一份满意的工作是非常轻松的事情。


就怕一年经验十年用,从此外卖天天送。



当然,不可否认,虽说框架与工具让很多人陷入了温床,但对于国家整个数字化转型和互联网的发展是做出了重大贡献的。


在巨大需求出现的时候,有足够多的人力迅速投身这个行业,带动整个行业的发展。


只是,潮水终会退去,只有那些真正会游泳的才能继续在大海中徜徉。


四、未来如何


常常有人问我,旭哥,我应该学什么才有前途?


每当看到这样的问题,我都会眉头紧锁,过于功利的心态,在技术这条路上注定难有大成。


这就有点类似于养殖业,比如说前两年养鲈鱼很赚钱,结果很多养殖户改养鲈鱼,造成今年鲈鱼泛滥,市场存量是过去数倍,根本卖不出价格,最后赔得裤衩都不剩了。


技术其实也是类似,有人一看前端就业形势大好,都去搞前端,结果“前端已死”。


技术栈也是一样,妄图学完之后自己就成了香饽饽,可能吗?人是趋利性的动物,就算你眼光独到,命运垂怜,抢得先机,但数年之后呢?


所以,其实重要的不是学了什么,而是学得怎么样。


心无旁骛,专注自身,无论学什么,从事哪个职业,只要自己足够有竞争力,都有前途。


无论是历史悠久的后端开发,还是巅峰期早已过去的客户端开发,亦或者是开始进入稳定期的前端开发,均是如此。


前端的未来


随着消费和广告行业的慢慢复苏,前端的就业情况会有所好转。但是……


首先,这个好转不会很快,而是很缓慢那种,因为当一个事物陷入低谷再要起来,前期都是缓慢的,需要升到某一个临界点之后,才会明显加速。


其次,就算前端的就业情况有所恢复,也不可能恢复到疫情之前的那种火热,那个时候遍地都是前端培训班,非常夸张。


至于前端是否会死,这个完全不要担心。


只要互联网还在,前端这个职业就不会消失,因为无论设备介质如何变化,用户的交互行为都不会消失,而前端就是一个处理人机交互的职业。


而人工智能的兴起,确实会对前端这个职业产生影响,是危机但也是机遇,如果你安于现状,则是危机,如果你勤于学习,则人工智能是机遇,会让你的产出更加高效。


这么看来,最核心的竞争力应该是学习的能力!


(完)


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

大专生自学前端求职历险记

关于我 由于高中的游手好闲、不学无术,没有考上大学。去了一所专科学校,本以为自己能够浪子回头,在学校好好学习。可惜的是,来到一个陌生又充满诱惑的城市后,迅速的迷失了自己,天天埋头打游戏,学习的事情早已抛之脑后。 一晃眼,到了2020年,疫情的接踵而至,让我这个...
继续阅读 »

关于我


由于高中的游手好闲、不学无术,没有考上大学。去了一所专科学校,本以为自己能够浪子回头,在学校好好学习。可惜的是,来到一个陌生又充满诱惑的城市后,迅速的迷失了自己,天天埋头打游戏,学习的事情早已抛之脑后。


一晃眼,到了2020年,疫情的接踵而至,让我这个本来没有任何技术、学历的“闲散人士”更加雪上加霜。豪不夸张的说,当时去实习,就差跪着求人家要我,说自己不要薪资。经历过一个月后,也就是2020年5月底,我找到了一份前端开发工作,从此开启了我的前端开发工作之旅。


在专科学校里的时间,我并没有意识到社会市场的残酷,甚至天真的认为自己还是能够辛苦点的找到一份工作。可是,现实给了我当头一棒,没有技术、没有学历、疫情打击。那一段时间应该是真的认知自己的时间,家里也没什么闲钱供我去培训班,我也不知道我出去能干嘛。去看了一圈市场,与跟同学的了解,了解到了前端开发工作,所以就一股脑扎进这个行业当中。


求职之旅


跟大多数人一样,并不知道应该从何处下手,当时在我的认知当中就知道一个 JQuery,所谓的 MVVM 框架简直是一无所知。点开小破站,找到点击率最高的视频,开始自学起来。


了解到一点框架的皮毛、然后死记硬背一点基础,统统写进简历当中。


所以我的学习曲线是如图下所示



跟大多数人一样,我是直接通过框架起手学习的前端。导致了我对于问题的处理能力几乎为零,遇到问题直接就双手离开键盘。看不懂,是真的看不懂(如果有相同感受的可以在评论抠一个 1)。


对着视频学了十天左右,写了一个 demo,屁颠颠的去求职。结果也是可想而知,人家也不是傻子一眼识破。四处碰壁,简历丢出去,根本没人看。兜兜转转持续了一个月左右,终于有一家小公司愿意给一个面试机会,马不停蹄的出发去面试,坐了一个小时左右的地铁抵达一个破旧不堪的写字楼,当时要不是看到周围还有一个高校,我还以为我去了一个搞传销的地方。。。推开一个破旧的们,一个很小的房间,两个人坐在里面给我面试。我也很直白的说自己只会一点点皮毛,他们也很直白的告诉我:我们条件有限,相当于是各取所需。其实老实说,我挺感动的,没有给我画大饼,也很直白的说我图他们要我,他们图我不要啥钱。


最终,我也算是如愿找到了这份实习工作,一个月 2000。也算是不错的结果了。


实习项目开发


去到公司以后,也马不停蹄的开始了开发工作。首先就是让我从一个简单的后台管理系统开始入手。但是问题也来了,我根本不知道什么叫管理系统,连项目搭建我都不会,然后就是两眼一抹黑。不停的去百度,查看如何搭建一个后台管理系统。


老实说,我当时连路由是什么我都不清楚,更别说加一堆乱七八糟的功能在里面了。哪个过程可想而知,多么的折磨人。经历了半个月,模板被我折腾起来了一个简单的样子,对着人家的管理系统样子进行拙劣的模仿。但是 bug 满天飞也是避免不了的问题。并且没有丝毫的设计可言,纯纯的依托答辩。


最后的最后,实在是看不下去了(包括我自己),去网上扒了一个模板开始自己去折腾。为什么一开始不考虑使用模板呢?因为我看不懂代码,下不去手。


虽然最后跌跌撞撞的项目启动起来了,但是也算是我第一次项目开发的经历吧。后续持续的添加一些功能,改动一些简单的样式,还好老板也很佛系,没有为难我,基本上没有魔改模板。所以也算是顺利的完成了后台管理系统的开发任务。


小插曲


在实习工作的期间,在技术群中认识了一个很牛的大佬。经常我在群里问一些傻逼问题(因为自己基础太差了),但是他都会很耐心的给我讲解,甚至是下班后抽出时间给我远程讲课。也算是我的半个引路人吧,让我知道了如何去玩儿前端。在这里手动抠一个感谢🙏🙏🙏。


步入正轨


在经历过第一个项目开发后,也算是知道了框架应该如何去玩儿(也就是知道了框架的 api 如何去调用)。也知道了如何去学好前端,所以慢慢的回头去了解基前端的三大基础知识 js css html


其实我相信很多人跟我一样,开始都是赶鸭子上架的形式去开发项目,遇到问题束手无策;遇到 bug 不知道如何去排查;遇到不知道如何去实现。。。最后我也总结出了问题所在,那就是基础的不扎实,学习顺序的问题,导致了这些问题。


啰嗦一句


哪怕是现在,我有时候跟网友聊天的时候也能听到一些让人不能理解的观点:前端那么简单有什么难度?前端不就是写写页面?前端。。。。


从我的观点出发而言,前端这个岗位确实是属于,宽进严出。想入行确实很容易,毕竟像我这样啥也不懂的,通过十来天的学习都能去做前端开发的事情。


但是,但是,但是,重要的话说三遍,前端的简单是因为它的入行门槛低。但是入门和会还是有本质的区别,绝大多数前端开发工作都是写 后台管理系统,这种开发,都是直接套用现成模板与组件就能够写。如果是定制化开发,脱离了后台管理系统的开发,那还是有手就行吗?


继续步入正轨


在工作的时间中,也认识了很多互联网大厂的大牛:滴滴、网易、腾讯等,经常厚着脸皮去请教他们。但是他们回应最多的是:多看基础,看书!


大佬们都这么说,那还等什么!直接开始行动。


  • 绿宝书:犀牛书
  • 红宝书:javascript高级程序设计
  • 黄宝书:你不知道的js

直接搞起来!虽然我很讨厌看书,但是看到自己实习的 2k 工资,我还不动起来,那可能真就废了。


所以每天下班后,回家翻开书籍,开始看。果不其然,一看就打瞌睡,生涩、枯燥的知识内容。没办法,继续去请教如何看书学习,得到的答案就是:好记性,不如烂笔头。


然后读书的时候,边看边写,跟做笔记一样。效果果然好多了,没那么容易打瞌睡。而且我也买了一些零食(口香糖、耐嚼的肉干之类的)边看边吃,让自己集中注意力。总之是为了能够学到真知识,想尽了各种办法。


半个月后,看了几章节基础,感觉确实潜移默化的改变了一些。写代码的时候不会那么的茫然;反复调试的次数少了一些;知道了更多好用的 api ,代码质量有一定的提高。


读书笔记分享


读书笔记


在这里分享一篇,自己从零开始写的一些笔记。不过自己已经停更很久了。


实习总结


经过两个月的实习后,时间也来到了 2020年7月,我毕业了。我也学到了很多东西,但是我觉得,这样子的工作状态并不是我喜欢的。


回学校简单收拾了一下,也决定了辞职。去找一份更加有前途的工作,当然这里肯定有很多人疑惑:你凭什么啊?确实是如此,包括我的父母,也是很疑惑并且还质疑的问道:你上几个月班,忘了自己的实际情况了?


我也开始反思,自己真的就那么的蠢、那么的不堪吗?


果断辞职


经过我的深思熟虑后,还是在毕业后辞职了。在出租屋沉淀了一个月,这一个月基本上每天只睡了五六个小时,其余时间都花在了基础的夯实上面,狠狠的补充前端基础知识。每天醒来就是:看书、写 demo、请教大佬,每天如此,孜孜不倦。


一个月后,整理自己的简历,然后又开始了自己的求职之旅。


二次求职


求职之路,也并没有自己想的那么顺利。别人也没有因为我简历写的东西多了那么一点可怜的东西而青睐你。


我也在开始反思,自己的辞职是否正确。因为我的本质问题并没有解决:没有学历、没有经验。期间也在自我怀疑、自我安慰,也在凌晨的时候,抓耳挠腮,头发也在开始一大把一大把的掉。


就这样持续了一个月左右,我终于又收到了一份面试邀请。马不停蹄的前去面试,结果却出乎我的意料,他们并没有问我八股文,反而是对我所说的经历感兴趣。我也是添油加醋的说了一顿我的实习经历、辞职后的这一个月的学习经历。


最后的最后,他们通过了我的初试。给我说需要老大亲自面试,我开始很忐忑。但是见到老大后,他是一个很和蔼的老师,并没有刁难我,也没有问我刁钻问题,只是跟我谈了一下基本情况、了解了我的基本情况,就通过了我的二次面试。


二次求职之旅结果


我很幸运,因为,让我去打工的地方是一个资源丰富的高校。我的老大也是院长,初次面试的两位也是两位老师。我也如愿以偿的又有了一份新的工作,接触到了极其丰富的资源。


老师们也很愿意教授知识,让我的技术再次的突飞猛进。


开发项目:


  • 北京冬奥会水立方保电系统
  • 基于负荷聚合的园区能量态势感知与交易系统
  • 电压暂降仿真模拟系统

薪资变化


毕业后,我的薪资也算是以每年翻倍的涨幅进步。也算是我的学习换来的回报吧。还是挺不错的~


现在


截至目前,经过三年零两个月的工作时间,也算是勉强迈入了初级前端开发的门槛吧。不断的学习中,也在积极的参与开源的贡献。



这些都是本人参与开发、贡献的项目,有兴趣可以点开看看。如果觉得有用也可以点一个小星星🌟~~~


最后


学习确实是一个枯燥的过程,也是一个很痛苦的过程。包括自己,如果不是那些大佬对我的帮助,我也不会那么快的进步。最后还是很衷心的感谢他们对我的帮助~


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

利用 UICollectionView 实现图片浏览效果

iOS
废话开篇:利用 UICollectionView 简单实现一个图片浏览效果。 一、效果展示 二、实现思路 1、封装 UICollectionViewLayout ,实现内部 UICollectionViewCell 的布局。 UICollectionView...
继续阅读 »

废话开篇:利用 UICollectionView 简单实现一个图片浏览效果。


一、效果展示




二、实现思路


1、封装 UICollectionViewLayout ,实现内部 UICollectionViewCell 的布局。

UICollectionViewLayout 在封装瀑布流的时候会用到,而且担负着核心功能的实现。其实从另一个角度也可以把 UICollectionViewLayout 理解成“数据源”,这个数据不是 UI 的展示项,而是 UI 的尺寸项。在内部进行预计算 UICollectionViewCellframe


UICollectionViewUIScrollView的子类,只不过,它里面子控件通过“重用”机制实现了优化,一些复用的复杂逻辑还是扔给了系统处理。开发过程中只负责对 UICollectionViewLayout 什么时候需要干什么进行自定义即可。


2、获取 UICollectionView 目前可见的 cells,通过进行缩放、旋转变换实现一些简单的效果。

3、自定义 cell ,修改锚点属性。

三、代码整理


1、PhotoBrowseViewLayout

这里有一点需要注意的,在 UICollectionViewLayout 内部会进行计算每一个 cellframe,在计算过程中,为了更好的展示旋转变换,cell 的锚点会修改到 (0.5,1),那么,为了保证 UI 展示不变,那么,就需要将 y 增加 cell 高度的一半

#import "PhotoBrowseViewLayout.h"

@interface PhotoBrowseViewLayout()

@property(nonatomic,strong) NSMutableArray * attributeArray;

@property(nonatomic,assign) CGFloat cellWidth;

@property(nonatomic,assign) CGFloat cellHeight;

@property(nonatomic,assign) CGFloat sep;

@property(nonatomic,assign) int showCellNum;


@end

@implementation PhotoBrowseViewLayout

- (instancetype)init
{
    if (self = [super init]) {
        self.sep = 20;
        self.showCellNum = 2;
    }
    return self;
}

//计算cell的frame
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
    if (self.cellWidth == 0) {
        self.cellWidth = **self**.collectionView.frame.size.width * 2 / 3.0;
    }
    if (self.cellHeight == 0) {
        self.cellHeight = self.collectionView.frame.size.height;
    }
    CGFloat x = (self.cellWidth + self.sep) * indexPath.item;
//这里y值需要进行如此设置,以抵抗cell修改锚点导致的UI错乱
    CGFloat y = self.collectionView.frame.size.height / 2.0;
    UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];

    attrs.frame = CGRectMake(x, y, self.cellWidth, self.cellHeight);
    return attrs;
}

//准备布局
- (void)prepareLayout
{
    [super prepareLayout];
    NSInteger count = [self.collectionView numberOfItemsInSection:0];
    for (int i = 0; i <count; i++) {
        UICollectionViewLayoutAttributes *attris = [self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]];
        [self.attributeArray addObject:attris];
    }
}

//返回全部cell的布局集合
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    return self.attributeArray;
}

//一次性提供UICollectionView 的 contentSize
- (CGSize)collectionViewContentSize
{
    NSInteger count = [self.collectionView numberOfItemsInSection:0];
    CGFloat maxWidth = count * self.cellWidth + (count - 1) * self.sep;
    return CGSizeMake(maxWidth, 0);
}

- (NSMutableArray *)attributeArray
{

    if (!_attributeArray) {
        _attributeArray = [[NSMutableArray alloc] init];
    }
    return _attributeArray;
}

@end

2、PhotoBrowseCollectionViewCell

这里主要是进行了锚点修改(0.5,1),代码很简单。

#import "PhotoBrowseCollectionViewCell.h"

@interface PhotoBrowseCollectionViewCell()

@property(nonatomic,strong) UIImageView * imageView;

@end

@implementation PhotoBrowseCollectionViewCell


- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame]) {
//设置(0.5,1)锚点,以底部中点为轴旋转
        self.layer.anchorPoint = CGPointMake(0.5, 1);
        self.layer.masksToBounds = YES;
        self.layer.cornerRadius = 8;
    }
    return self;
}

- (void)setImage:(UIImage *)image
{
    self.imageView.image = image;
}


- (UIImageView *)imageView
{

    if (!_imageView) {
        _imageView = [[UIImageView alloc] init];
        _imageView.contentMode = UIViewContentModeScaleAspectFill;
        _imageView.backgroundColor = [UIColor groupTableViewBackgroundColor];
        [self.contentView addSubview:_imageView];
    }
    return _imageView;
}

- (void)layoutSubviews
{
    [super layoutSubviews];
    self.imageView.frame = **self**.contentView.bounds;
}

@end

3、CollectPhotoBrowseView

CollectPhotoBrowseView 负责进行一些 cell 的图形变换。

#import "CollectPhotoBrowseView.h"
#import "PhotoBrowseCollectionViewCell.h"
#import "PhotoBrowseViewLayout.h"

@interface CollectPhotoBrowseView()<UICollectionViewDelegate,UICollectionViewDataSource>

@property(nonatomic,strong) UICollectionView * photoCollectView;

@end

@implementation CollectPhotoBrowseView

- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame]) {
        [self makeUI];
    }
    return self;
}

- (void)makeUI{
//设置自定义 UICollectionViewLayout
    PhotoBrowseViewLayout * photoBrowseViewLayout = [[PhotoBrowseViewLayout alloc] init];
    self.photoCollectView = [[UICollectionView alloc] initWithFrame:self.bounds collectionViewLayout:photoBrowseViewLayout];
    self.photoCollectView.delegate = self;
    self.photoCollectView.dataSource = self;
    [self.photoCollectView registerClass:[PhotoBrowseCollectionViewCell class] forCellWithReuseIdentifier:@"CELL"];
    self.photoCollectView.showsHorizontalScrollIndicator = NO;
    [self addSubview:self.photoCollectView];
//执行一次可见cell的图形变换
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self visibleCellTransform];
    });
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return 20;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    PhotoBrowseCollectionViewCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"CELL" forIndexPath:indexPath];
    [cell setImage: [UIImage imageNamed:[NSString stringWithFormat:@"fd%ld",indexPath.item % 3 + 1]]];
    return cell;
}

#pragma mark - 滚动进行图形变换
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
//滑动的时候,动态进行cell图形变换
    [self visibleCellTransform];
}

#pragma mark - 图形变化
- (void)visibleCellTransform
{
//获取当前可见cell的indexPath集合
    NSArray * visibleItems =  [self.photoCollectView indexPathsForVisibleItems];
//遍历动态进行图形变换
    for (NSIndexPath * visibleIndexPath in visibleItems) {
        UICollectionViewCell * visibleCell = [self.photoCollectView cellForItemAtIndexPath:visibleIndexPath];
        [self transformRotateWithView:visibleCell];
    }
}

//进行图形转换
- (void)transformRotateWithView:(UICollectionViewCell *)cell
{
//获取cell在当前视图的位置
    CGRect rect = [cell convertRect:cell.bounds toView:self];
//计算当前cell中轴线与中轴线的距离的比值
    float present = ((CGRectGetMidX(rect) - self.center.x) / (self.frame.size.width / 2.0));
//根据位置设置选择角度
    CGFloat radian = (M_PI_2 / 15) * present;
//图形角度变换
    CGAffineTransform transformRotate = CGAffineTransformIdentity;
    transformRotate = CGAffineTransformRotate(transformRotate, radian);
//图形缩放变换
    CGAffineTransform transformScale = CGAffineTransformIdentity
    transformScale = CGAffineTransformScale(transformScale,1 -  0.2 *  fabs(present),1 - 0.2 * fabsf(present));
//合并变换
    cell.transform = CGAffineTransformConcat(transformRotate,transformScale);
}

@end

四、总结与思考


UICollectionView 也是 View,只不过系统为了更好的服务于开发者,快速高效的实现某些开发场景,进行了封装与优化,将复杂的逻辑单独的封装成一个管理类,这里就是 UICollectionViewLayout,交给它去做一些固定且复杂的逻辑。所以,自定义复杂UI的时候,就需要将功能模块足够细化,以实现更好的代码衔接。代码拙劣,大神勿笑[抱拳][抱拳][抱拳]


作者:头疼脑胀的代码搬运工
链接:https://juejin.cn/post/7119028552263008293
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

从互联网到国企、从一线城市到三线省会

6月的北京格外的闷热,比起内蒙真的热了不少,整整四个月没来北京了,晚上出高铁来到清河站时还是那么的熟悉,挤上13号线路过五道口、知春路,去西直门换乘2号线,再换上5号线到了宋家庄,最后换上回村的亦庄线,从北京的最西北边走到最东南角。看着地铁上的疲惫的人们,这次...
继续阅读 »

6月的北京格外的闷热,比起内蒙真的热了不少,整整四个月没来北京了,晚上出高铁来到清河站时还是那么的熟悉,挤上13号线路过五道口、知春路,去西直门换乘2号线,再换上5号线到了宋家庄,最后换上回村的亦庄线,从北京的最西北边走到最东南角。看着地铁上的疲惫的人们,这次回来自己更像是一个游客的视角,观察着以前的“自己”。从3月初离职一直没有记录过这段经历,但这次去北京让我觉得有必要写一些自己的感受和体会。


离职前的纠结


意外通过的面试


毕业四年一直从事Java开发,在京东两年左右,2月份很偶然的看到内蒙的一则国企招聘,本着今年大概率要回去工作的想法,顺便就报名了,又很顺利的通过了笔试和面试,面试时特意请假一天从北京跑回内蒙,下午等待面试的时候手机被收走四个小时,四个小时没处理工作消息差点爆炸,各种报警和需求沟通群里被@,面试完急匆匆的坐高铁回北京继续上班。本来只是想试下机会,莫名就通过了,这下轮到自己开始纠结了。


无时无刻的报警&下不了的班


在京东工作应该是我做开发这些年达到的事业最高峰,从之前写简单逻辑的小菜鸟一下子开阔了视野,见到从未了解的新领域,对流量并发有了新的认识。但这份工作确实很辛苦,我们几乎是7*24小时待命,每天都要保持手机开机,随时都会有接口报警,一定要第一时间响应处理,核心接口还要配置语音报警,即使晚上也会直接打电话进行通知,如果不接电话就会被系统记录,还有一些产品运营的问题也会随时发生,某些定位的门店或者商品不展示了,都要及时给人家反馈处理。这两年我们真的不管去哪里都要带着电脑,出去旅游或者逛街都是如此,脑子里的那根弦一直紧紧绷着,就像是悬在头上的达摩克利斯之剑。


还有每天忙碌的工作,写不完的需求,开不完的会,解决不完的问题,下不了的班,从早上九点去了就开始忙碌,经常晚上十点多才可以下班,很多人可能会待到12点甚至更久,但我确实是卷不动了,身上压着的三座大山,需求排期、日常报警、绩效目标,每个月的发版上线是不可能变得,再多事情也得把需求开发完和前后端联调完,再让测试验证通过,而这期间有报警问题也要第一时间处理,不然会记录个人的问题处理能力,如果报警拖久了变成事故,那就是全部人背锅了。每个季度的绩效目标也要完成,否则到了季度末绩效考核验证时,即使需求都写完,报警都处理了,绩效目标没完成也是不合格。时间就是那么多,任何事情的优先级都很高,只能自己不断加班去做。京东工作这两年都没有写过自己的博客,因为确实是没有时间,这些以后想写一个京东工作系列再详细记录下来。


坚持还是放弃


即使吐槽了很多,但压力确实让人成长,这些年是对我职业生涯的重新铸造,就像炼铁般一锤一锤反复敲打,从思维逻辑到开发能力、沟通交流等方面都有了很大的改变,自己逐渐成长为了部门最能背锅的,顶住了网关这个问题爆炸源。此时走难免不甘心,上个季度末刚拿到了A+的绩效,国企面试通过的同时也通过了京东内部晋升评审,正如自己一直喜欢开发这行,眼前也是事业逐渐越来越好,朝着预期的目标不断靠近,此时真的要激流勇退吗。


这个问题真的思考了很久很久,在北京很快乐也很痛苦,做着最喜欢的事情,但这么多年也只有自己,我慢慢认为生活不应该是这样的,生活不应该只有工作,工作是为了生活,但生活不是为了工作。回去之后的问题:


一是:工资大幅度缩水,降到生活快要不能自理,有种刚毕业的感觉。


二是:技术这方面基本就不会再有大的进步。第二点真的是让我最难以接受的,看着京东的神灯社区里面各种技术文章,职业生涯的巅峰就此打住真是非常不甘心。回去之后就有了更多的时间,不再全部投入到技术上,去找朋友,同学,家人,放下背负了太久太久的压力。


在北京感觉自己就是一节电池,现在的我有90%的电量,但如此大的压力不可能一直保持冲劲,等到了互联网人退休年龄,我还能有机会再体面的回去吗,对于北京,年轻人都是一茬一茬的韭菜,


最终还是选择了离开,带着遗憾和不舍,带着对新生活的期待。


国企的压力


与之前每次在北京换工作的压力不同,国企的压力是找不到目标,每天两点一线的生活,早上喝茶下午喝咖啡,看看文档看看资料,写一些工作总结,催一催开发进度,这些就是一天的工作内容,开始的一两个月真的有些迷失,这些就是我想要的吗,安逸圈也未免太安逸了,当你突然从强压状态下换到清闲的环境里,一时之间非常不适应,总感觉人生就要如此荒废过去,前一两个月我总想看别的工作机会,想让自己重新忙起来,以前在井底只能看到那片蓝天,现在好不容易出来拥有了大片蓝天,却又想赶紧再找到自己的井。


学习不止工作


四个月过去了,逐渐开始看清楚自己的目标,也有了一些简单的规划,现在的时间越来越多,其实完全可以做更多自己想做的事情,互联网的知识不再像以前那么集中,需要你有更大的耐心和毅力去坚持学习,可以学到的理论更多,但实践的机会比较少,以前站在巨人的肩膀上用海量的数据和流量来验证,现在还在吃过去的老本,扩充了知识的广度,而深度还停滞在那里。


目前只是摆平了自己的心态,逐渐认清形势再改变还需要时间,时间会让一切都变得更好,只要你愿意的话。以前所有的生活都给了工作,现在工作只是生活的一部分,用生活之余学到的东西继续反哺工作,提高本就不多工作的效率,也顺便去学习业务,技术在三线城市不再是唯一,而究竟如何平衡二者的关系,让自己还能继续拥有竞争力,这是我目前还看不清的。


人际关系


没有绝对的公平,但在北京是有相对的公平,而回到三线城市的国企,公平变得妙不可言,人际关系成为了重中之重,小小的部门内部已然是派系林立,十几个人的关系层级更是深不可测,想起在jd,工位后面坐着小领导,对面最在平台部负责人,管理几百人的领导也是和我们一样坐在一起,同事们经常说:在互联网公司比你大好几级的领导,和你就是平级。而在国企所有的工作,事情都有条条框框去限制,你永远看不清里面的水有多深,同时生活也被同事关系所入侵,大家经常吃饭喝酒聊工作,即使在开怀畅饮的时候也要时刻谨慎提防,说错话和做错事要比想的更加严重。在互联网公司争吵是必不可少的,不吵就说不清楚需求,甩不了锅,而回到这里,所有人都客客气气的,所有人都慈眉善目和你微笑,只是面具背后的脸很难看到。


做技术的本身比较呆板,不会八面玲珑也不会左右逢源,我只想做好自己的事情,做一个不出声的小透明,不争不抢,做自己喜欢的事情。


所见所想


记忆拉回现实,看着北京地铁上的众生相,感觉大家都很疲惫但眼里还有希望,曾经北漂的我现在只想逃离,虽然只回去四个月但依然接受不了快节奏的北京,紧绷了四年的弦已经彻底放松,去总部和过往的同事吃了个饭,大家坐着聊聊天,为他们还能坚持在北京奋斗而加油,每个人都有自己的选择,我的退缩也需要勇气,时至今日也乐得接受自己的选择的路。


如今互联网的大潮正在褪去,可能越来越多的人面临这样的选择,假如我们还有的选的话,其实生活中大部分事情我们是没得选的,生活一步一步推着你往前走。


如果问我,刚毕业选择来大城市后悔吗,我坚信自己不后悔,这里让我看到学到也付出了太多。


如果问我,现如今离开大城市后悔吗,我也坚信自己不后悔,这里没有家没有归宿,我终究要回去,只怕走的越远越迷茫。


作者:AlgoRain
来源:juejin.cn/post/7253115535482437689
收起阅读 »

你网站的网速是很快,但是在没有网络的情况下你怎么办?🐒🐒🐒

web
在现代的网络世界里,5G 网络的普及,我们可以访问一个网站或者使用一个 App 的速度极其快,但是在没有网络的情况下你啥都看不了,只能大眼瞪小眼了。 离线应用是指通过离线缓存技术,让资源在第一次被加载后缓存在本地,下次访问它时就直接返回本地的文件,就算没有网络...
继续阅读 »

在现代的网络世界里,5G 网络的普及,我们可以访问一个网站或者使用一个 App 的速度极其快,但是在没有网络的情况下你啥都看不了,只能大眼瞪小眼了。


离线应用是指通过离线缓存技术,让资源在第一次被加载后缓存在本地,下次访问它时就直接返回本地的文件,就算没有网络连接。


通过离线应用,主要有以下几个优点:



  1. 在没有网络的情况下也能打开网页。

  2. 由于部分被缓存的资源直接从本地加载,对用户来说可以加速网页加载速度,对网站运营者来说可以减少服务器压力以及传输流量费用。


离线应用的核心是离线缓存技术,要实现这种方式,我们可以使用 Service Worker 来实现这种缓存技术。


什么是 Service Worker


Service Worker 服务器和浏览器之间的之间的桥梁或者中间人。


Service Worker 运行在一个与页面 JavaScript 主线程独立的线程上,并且无权访问 DOM 结构。但是它能拦截当前网站所有的请求,对请求使用相应的逻辑进行判断,如果需要向服务器发起请求的就转给服务器,如果可以直接使用缓存的就直接返回缓存不再转给服务器。从而大大提高浏览体验。


注册 Service Worker


要使用 Service Worker,首先我们要判断浏览器是否支持 Service Worker,具体代码逻辑如下:


if (navigator.serviceWorker) {
window.addEventListener("DOMContentLoaded", function () {
navigator.serviceWorker.register("/worker.js");
});
}

这段代码的主要目的是在支持 Service Worker 的浏览器中,当页面加载完成后注册一个指定的 Service Worker 脚本。这个传入的 worker.js 就是 Service Worker 的运行环境。


这个脚本被安装到浏览器中后,就算用户关闭了当前网页,它仍会存在。 也就是说第一次打开该网页时 Service Workers 的逻辑不会生效,因为脚本还没有被加载和注册,但是以后再次打开该网页时脚本里的逻辑将会生效。


Service Worker 安装和激活


注册完成后,worker.js 文件会自动下载、安装,然后激活。它提供了一些 API 给我们做一些监听事件:


self.addEventListener("install", function (e) {
console.log("Service Worker 安装成功");
});

self.addEventListener("fetch", function (event) {
console.log("service worker is fetch");
});

当 install 完成并且成功激活之后,就能够监听 fetch 操作了,如上代码所示,输出结构如下图所示:


20230918074308


使用 Service Workers 实现离线缓存


在上面的内容我们已经知道了 Service Workers 在注册成功后会在其生命周期中派发出一些事件,通过监听对应的事件在特点的时间节点上做一些事情。


在 Service Workers 安装成功后会派发出 install 事件,需要在这个事件中执行缓存资源的逻辑,实现代码如下:


// 当前缓存版本的唯一标识符,用当前时间代替
const cacheKey = new Date().toISOString();

// 需要被缓存的文件的 URL 列表
const cacheFileList = ["/index.html", "/index.js", "/index.css"];

// 监听 install 事件
self.addEventListener("install", function (event) {
// 等待所有资源缓存完成时,才可以进行下一步
event.waitUntil(
caches.open(cacheKey).then(function (cache) {
// 要缓存的文件 URL 列表
return cache.addAll(cacheFileList);
})
);
});

在 install 阶段我们就已经指定了要被缓存的内容了,那么就可以在 fetch 阶段中听网络请求事件去拦截请求,复用缓存,代码如下:


self.addEventListener("fetch", function (event) {
event.respondWith(
// 去缓存中查询对应的请求
caches.match(event.request).then(function (response) {
// 如果命中本地缓存,就直接返回本地的资源
if (response) {
return response;
}
// 否则就去用 fetch 下载资源
return fetch(event.request);
})
);
});

通过上面的操作,创建和添加了一个缓存的库,如下图所示:


20230918080142


缓存更新


线上的代码有时需要更新和重新发布,如果这个文件被离线缓存了,那就需要 Service Workers 脚本中有对应的逻辑去更新缓存。


这可以通过更新 Service Workers 脚本文件做到,浏览器针对 Service Worker 有如下机制:



  1. 每次打开接入了 Service Workers 的网页时,浏览器都会去重新下载 Service Workers 脚本文件,如果发现和当前已经注册过的文件存在字节差异,就将其视为新服务工作线程。

  2. 新 Service Workers 线程将会启动,且将会触发其 install 事件。

  3. 当网站上当前打开的页面关闭时,旧 Service Workers 线程将会被终止,新 Service Workers 线程将会取得控制权。

  4. 新 Service Workers 线程取得控制权后,将会触发其 activate 事件。


新 Service Workers 线程中的 activate 事件就是最佳的清理旧缓存的时间点,代码如下:


var cacheWhitelist = [cacheKey];

self.addEventListener("activate", function (event) {
event.waitUntil(
caches.keys().then(function (cacheNames) {
return Promise.all(
cacheNames.map(function (cacheName) {
// 不在白名单的缓存全部清理掉
if (cacheWhitelist.indexOf(cacheName) === -1) {
// 删除缓存
return caches.delete(cacheName);
}
})
);
})
);
});

这样能确保只有那些我们需要的文件会保留在缓存中,我们不需要留下任何的垃圾,毕竟浏览器的缓存空间是有限的,手动清理掉这些不需要的缓存是不错的主意。


参考资料



总结


Service Worker 作为服务器和浏览器两者之间的桥梁,它并且可以缓存技术,通过这种方式,在断网的时候,去获取缓存中相应的数据以展示给客户显示。


当断网之后,直接给他页面返回一个俄罗斯方块让他玩足一整天。


作者:Moment
来源:juejin.cn/post/7279321729462616121
收起阅读 »

我入职了

web
前言 从5月底离职到现在,一个半月的时间,通过内推+BOSS直聘,前前后后约到了10家面试,终于拿到了一个满意的offer,一家做saas系统的上市公司。 本文就跟大家分享下我这段时间找工作的心路历程,欢迎各位感兴趣的开发者阅读本文。 无所畏惧 6月1号,裸辞...
继续阅读 »

前言


从5月底离职到现在,一个半月的时间,通过内推+BOSS直聘,前前后后约到了10家面试,终于拿到了一个满意的offer,一家做saas系统的上市公司。


本文就跟大家分享下我这段时间找工作的心路历程,欢迎各位感兴趣的开发者阅读本文。


无所畏惧


6月1号,裸辞的第一天,制定了接下来的每日计划,终于可以全身心投入做自己喜欢的事情啦。



  • 06:30,起床、洗漱、蒸包子

  • 07:00,日常学英语

  • 08:00,吃早餐,顺便刷一下BOSS直聘

  • 08:30,日常学算法、看面试题

  • 11:40,出门吃饭,午休

  • 14:00,维护开源项目

  • 18:00,出门吃饭,去附近的湖边逛一圈,放松下心情

  • 20:30,将当天所学做一个总结,归纳成文章

  • 23:00,洗澡睡觉,充实的一天结束


image-20230717212836044


be9877e8144d437c9a2f9ea9b188c7fe


内推情况


通过在掘金、V站和技术群发的文章,为我带来了20多个内推,从大厂到中厂到小厂,约到面试的只有4个。其他的技术部认可我,但是HR卡学历(统招本科)。


image-20230717214830630


image-20230717215823196


image-20230717215836796


image-20230718195936071


无响应式网站开发经验被拒


这是一家杭州的公司,可以远程办公,跟我约了线上面试。做完自我介绍后,他对我的开源项目比较感兴趣,问了我:



  • 你为什么会选择写一个聊天工具来作为开源项目?

  • 你的截图功能是怎么实现的?


行,那我们来聊几个技术问题吧。



  • 讲一下webpack的打包流程

  • webpack的热更新原理是怎样的?

  • 讲一下你对webpack5模块联邦的理解


这些问题回答完后,他问我你有做过响应式网站开发吗?


我:我知道怎么写一个响应式网站,在工作中我没接触过这方面的业务。


面试官:行,那你讲一下要怎么实现一个响应式网站?


我:用css3的媒体查询来实现,如果移动端跟PC端布局差异很大的话,就写两套页面,对应两个域名,服务端根据http请求头判断设备类型来决定是否要重定向到移动端。


面试官:还有其他方案吗?


我:嗯...,应该没有了吧,我只了解过这两种方式。


面试官:好吧,在seo优化方面,前端要从哪些点去考虑?


我:标签语义化、ssr服务端渲染、img标签添加alt属性来、在head中添加meta标签、优化网站的加载速度,提高搜索引擎的排名。


面试官:我的问题问完了,你有什么想了解的?


我:团队人员配比是怎么样的?


面试官:我们这个团队,前端的话有4个人,有2个后端。然后,前端有时候需要用node写一些接口。


我:如果我进去的话,主要负责哪块业务的开发?


面试官:负责一些响应式网站业务的开发,再就是负责我们内部系统的一个开发。


我:行,我的问题就这些。


面试官:OK,那今天的面试就先到这。



大概过了3天时间,也没有给我答复。因为这个是他们老板在v站看到了我的文章,觉得我还不错,加了微信,让他们技术面的我,我也不好意思问结果。


很大可能是因为我没有响应式网站的实际开发经验,所以拒了我吧。😔



期望太高被拒


这是一家上海的公司,他们的主要业务是做产品包装。有自己品牌的网站、小程序、app。他们公司一个负责公司内部事务的人加了我微信,跟我简单聊了下,让我体验下他们的产品,看看有没有什么我能帮到他们的地方。


image-20230718161603524


image-20230718161614565


image-20230718161740495


image-20230718161655844


聊完后,他一直没有主动联系我,我也没有约到其他面试,我就主动出击了,看能不能确定下来,约个面试。


image-20230718162410852


image-20230718162435259


image-20230718162606997


我整理了一套方案,发到了他的邮箱,期望薪资我写了20k,过了两天,他给了我答复,告诉我期望太高。我说薪资可以商量的,但无济于事。


image-20230718164059392


白嫖劳动力


这家公司是做物流的,是一个群友曾经面过的公司,但是最后没去。看到hr在朋友圈发了招聘信息,在招高级前端,就推给我了,约了线下面试。


到公司后,按照惯例填了一张表,写了基本信息。过了一会,一个男的来面我,让我做了自我介绍,顺着我的回答提问了公司的规模以及业务。


提问完成后,他说我看你期望薪资写了15k,你上家才12k,为什么涨幅会这么高?


我:因为我经过两年的努力以及公司业务的积累,自己的技术水平有显著提升。我对这一行很喜欢,平常80%的业余时间都用来学习了。


面试官:好,我让技术来面下你,看看你实力如何。


等了5分钟左右,他来了告诉我说:技术在开会,我先带你做一下机试吧。你把这两个页面(后台管理系统登陆页与后台首页)画出来就行。


我把页面画出来后,又过来一个人看我做的,他说 你就把页面画出来了?我说:对啊,刚才带我过来那个人说让我画页面出来的。


他说,那可能是他没说清楚,那这样肯定是不行的,你要自己重新建项目,把页面画出来后,要调接口的,把整个流程走通才行的。现在已经11点40多了,你下午再过来继续弄吧。


我直接满脸问号,把整个流程走通只是时间问题,你们这个机试到底想考察啥呢?


他说,页面在我们这里不重要,调接口,走通整个流程才重要。


我直接无语了,就说 抱歉,我下午有其他安排了,我就先走了。


image-20230718172136268


焦虑不安


时间来到6月20日,已经好多天没有约到面试了,逐渐焦虑起来了,虽然兜里余粮还有很多,但始终无法静下心来做事情,充满了对未知的恐惧。


就在这时,我还迎来了别人的嘲讽。他成功让我生气了,我努力的平复心情,告诉自己不要把这件事放在心上,通过让自己忙起来转移注意力,通过学习来克制焦虑。


image-20230718191713122


image-20230718191749187



白天我可以通过学习来缓解焦虑,但是一到晚上躺在床上,我就会开始胡思乱想。想着自己一直找不到工作怎么办,难道我真的不适合吃这碗饭吗,我怎么这么差劲,连个面试都约不到...唉,怎么会这样,我明明已经很努力了,为什么结果会是这样...



完善打招呼语


内推无望,BOSS直聘发消息也是送达、已读未回。这个时候,有个网友建议我把招呼语改改,hr不懂什么开源不开源的,他们只会关键词匹配,只要包含了,就会收你简历,于是我就把打招呼语改成了:


image-20230718195551550



招呼语改完后,效果好了一些,终于有HR愿意收我简历了🥳



学历歧视、贬低、pua、拒了offer


改完打招呼语后,我在BOSS直聘上约到了第一家面试,这家公司是做可视化VR编辑器的,团队有30来个人,BOSS直聘的薪资范围是20K~25K。


我经历了五轮面试,拿到了offer,给了18K,但是最终还是拒绝了,本章节就跟大家分享下这段故事。


技术面


技术面是去线下的,按照惯例做完自我介绍,面试官提问了我:



  • 你刚才说你写了个web端的截图插件,你能讲一下你是怎么实现的吗?

  • 我看你上家公司是做动画编辑器的,你在做这个项目的时候有遇到过哪些难点吗?

  • 你刚才提到了你为编辑器做了一些性能优化,你都做了哪些优化?

  • 你刚才说你还实现了svg类型的文本组件搜索功能,你能讲讲你是如何实现的吗?


问完这些后,他说我的问题问完了,你有什么想要了解的吗?


我:团队人员配比是怎么样的?


面试官:我们这边是重前端的,因为是做编辑器嘛,难点在前端这块,目前有4个前端,计划再招3个,再就是有几个做算法的、做c++的,1个产品经理,2个后端,2个UI,3个测试。


我:如果我进去的话,是做哪方面的项目?


面试官:你进来的话,主要是负责VR编辑器项目的,这个项目刚开始做。目前的话,比较累,会加班,基本上是早9晚8,有时候可能要10点才能走。再就是,我们这边是大小周,你能接受的吧?
我:哦哦 明白了,我可以接受


面试官:那行,你稍等下,我让我们的产品经理面下你。


产品经理面


过了一会儿,产品经理过来了。他说:我们的技术对你的评价很高,我再来面面你,你先做个自我介绍吧。做完自我介绍后,产品经理顺着我的介绍进行了提问:



  • 你刚才说你这个截图插件Gitee的产品经理在网上看到了,是码云官方的吗?

  • 我看你上家公司也是做编辑器的,你们这个产品主要面向的用户群体是哪些?

  • 你们这个产品啥时候上线的,你主要负责的是什么?

  • 你们的团队配比是怎么样的?

  • 你们在开发项目时,是如何管理git分支的?


问完这些后,他让我稍等下,让HR来面下我。


过了3分钟左右,他过来说:我们HR这会儿太忙了,抽不开身,这样,你今晚有空吧,我让她跟你电话聊聊。我回答说,7点后我都有空。


HR电话面


因为约了晚上7点的电话面试,所以我就随便吃了点,就匆匆忙忙回家等电话了。我等到了晚上9点,也没电话打过来,我就在boss直聘问了下,对方说:可能是HR忙忘了,我让她明天给你打。


晚上躺床上睡觉的时候,不出意外,我又开始胡思乱想了,心想:我这煮熟的鸭子该不会飞了吧,会不会是面试表现的不好人家婉拒我了呢,会不会是...,又焦虑了。


到了第二天下午2点多的时候,HR终于给我打了电话,问我期望薪资多少。我说22k,她问我上家薪资多少,我说12k。不出意外,她很震惊:你这涨幅也太大了吧,能说说原因吗?我说:你们这里是大小周,工作强度比较大,而且做的项目也是较为复杂的,我看BOSS直聘标的价格也是20k~25k。


她说:我们这个岗位是中、高级前端都招聘的,你这边最低能接受的薪资是多少呢?
我说:20k


她说:行,了解了,我再跟面试官对接下,晚些时候我加你微信聊。


又过了一天,她加了我微信,跟我说:我只匹配他们的中级开发岗位,让架构师再跟我聊聊。


image-20230718210811195


前端架构师面


跟架构师约的是电话面试,做完自我介绍后,他提问了我:



  • 讲一下webpack的打包原理

  • 讲一下webpack的loader和plugin

  • 讲一下webpack5的模块联邦

  • 讲一下Babel的原理,讲一下AST抽象语法树

  • 讲一下你所知道的设计模式

  • 讲一下浏览器的垃圾回收机制

  • 讲一下浏览器的渲染流程

  • 讲一下浏览器多进程的渲染优势

  • 谈谈你对浏览器架构的理解


我回答完之后,他说:我大概知道你的技术水平了。你现在的水平还不到P6,也就P5多一点,远远不及P7。


我刚才问你的问题,你每回答完一个我都问你有没有要补充的,你都说没有,我从你嘴里没听到任何性能优化相关的东西,这些知识现在还都不是你的,你只知道这么个东西,缺乏实践。就好比,我刚问了你垃圾回收机制,你回答的是chrome的,那火狐呢?edge呢?


你对你未来的规划是怎么样的?


我说:我还是以技术为主,我会继续学习来充实自己,未来如果有机会的话,希望能做到技术管理的位置。


面试官冷笑了下说:你一个大专怎么做管理?


我沉默了一会儿说:未来我会把自己的学历提升下的


面试官:你要认清自己的地位,你要想一下你的价值是什么?你能给我们公司带来什么?我们要用到three.js,你只是学过它,没有落地项目做支撑,你进来后我们还是要给你时间来熟悉项目的,跟没学过的人没啥两样。就好比,我问你three.js的坐标系用的是啥,你都不知道。
我:这个我知道,它用的是右手坐标系


面试官楞了一下说:你知道这个也没啥的,这很简单的,我们这边随便拉一个人都会这些,而且比你厉害。


我继续保持沉默。


面试官:我对你的评价就这么多,你在我们这边是能学到很多东西的,你多想想我今天跟你说的,我不知道你的业务能力怎么样,回头我再跟其他面试官聊聊,今天的面试就先到这。


第二天,HR联系我了,跟我说薪资在16k~18k左右,跟我约了下午1点30的面试。


image-20230718215534222


image-20230718215606136


老板面


到公司后,HR直接带我进了老板办公室,跟我说这个是X总,你们聊吧。 跟老板聊了一个多小时,聊的内容大概是谈人生、理想,大概能记得起的一些问题有:



  • 你觉得你是一个什么样的人?

  • 你有哪些优点?

  • 你想成为一个什么样的人?

  • 你觉得你的技术水平怎么样?

  • 如果让你给自己打标签,你会打什么标签?

  • 回看你的过往人生,你后悔吗?


考虑再三 终拒offer


从公司回来后的第二天,HR告诉我面试结束了,最终给我定的薪资是17k,发了offer。


image-20230718222724254


发了offer后,我本该高兴的,但是我却高兴不起来,那一晚我想了很多,觉得早9晚8,大小周。这个钱还是太少了,而且那个前端架构师说的话让我很不舒服,pua的气息太重了。入职后,跟这种人一起工作,我也不会开心。思考再三后,我最终还是拒掉了这个offer。


image-20230718222252549


image-20230718222337117


比较钟意的小外企


这是我在BOSS直聘约到的第二家面试(15k~20k),面试体验很好。到公司后,接待我的人很有礼貌,告诉我前端是技术总监来面的,他还没来,你先坐着等他一会儿。


等了一会儿后,看到了技术总监,主动跟我握了个手。然后说:他临时有个会开,让我稍等下他,然后安排我在会议室坐了会儿,倒了一杯水给我。


我在会议室坐了40多分钟,他会开完了,喊我去办公室聊,按照惯例做完自我介绍后。他问我:



  • 你刚才提到了你做了编辑器的性能优化,你具体是怎么做的?

  • 你们这个编辑器前端编辑的应该是dom吧,最后生成的视频是怎么生成的?

  • 我看你的项目经验都是vue,你应该对vue全家桶都很熟了吧?


问完这些问题后,他用笔记本打开了我简历上的项目,边看边问我这块你是怎么实现的,有没有遇到过啥问题,你是怎么解决的。项目看完后,他说你技术没问题,我了解完了。我跟你介绍下我们这边的项目,我们在做...。介绍完了后,他问了我离职原因,以及我的期望薪资。


我说了20k,他说,站在客观角度来说,你的学历是大专,在我们这里拿到这个数很难,我们也不是什么特别有钱的公司。但是,我们的产品是很有发展前景的,已经拿了一轮800w美金的融资了,这个岗位我在boss直聘挂了1个月了,收到了300多份简历,有很多大厂出来的,但是我都不太满意,偶然间看到你的简历,觉得你是一个爱学习、肯钻研的人,就约你来面试了。你是我面的第一个前端。


我听他这么说后,我就说:那薪资17、18也可以。


他说:行,明白了,我回头跟老板说说,尽量帮你争取。我们这边工作氛围很棒,团队是一支很精湛的团队组成的,我们这边做算法的是麻省理工毕业的,这边的一个后端是之前抖音短视频架构组出来的。你在这里也能学到很多前端之外的东西,我们是早上10点上班,晚上6点30下班,不打卡,双休。


我听他这么说后,觉得很不错,就说:那15k也行。


他说:你也不用太勉强,不然你进来了也不开心,我们这里发展空间很大的,未来拿到更多的融资,你在这里是可以涨薪的。那今天我们就先到这里,后天就是端午节了,这样,我端午节后的那周给你具体的答复。


就这样,我又进入了焦灼的等待期。


端午节后的第2天,那边还没答复,我就主动问了下,他给我的答复是:


image-20230721214840273


又过了3天,一直没约到面试,焦虑的很。我就又厚着脸皮问了下情况,得来的答复是他们还没找到合适的产品经理。(这个时候,心里很难受到极点了,泪水在眼珠里打转,我焦虑到哭了😔)


image-20230721215034742



晚上躺在床上又开始胡思乱想了,觉得老天很不公平,为什么好运总是不能降临到我头上。唉...就这样想着想着,不知想了多久,也不知道自己睡着了没,只记得手机的闹钟响了,关了闹钟继续睡去了...



随遇而安


又浑浑噩噩的过了几天,时间来到7月3日,BOSS直聘有人跟我约面试了,一天下来约了3个面试,都是很多天之前联系的,今天才收了我简历,我的心情终于好了一些。


做物联网的公司


这家公司距离我住的地方很近,步行1.1公里就能到。BOSS直聘标的价格是(15k~18k),到了公司后,前台让我扫二维码关注他们的公众号,填写面试登记表(基本信息、期望薪资、上家公司薪资)。


填写完后,前台带我进了公司,等了5分钟左右,面试官来了,按照惯例做完自我介绍后,他问了我:



  • 你讲一下vue双向绑定的原理

  • 讲一下vue3相比vue2,它在diff算法上做了哪些优化?

  • Vue2为什么要对数组的常用方法进行重写?

  • Vue的nextTick是怎么实现的?

  • 讲一下你对EventLoop的理解吧

  • 讲一下webpack5的模块联邦


这里我讲一下EventLoop这个问题吧,我回答完之后,他反问我:你确定宏任务先执行的吗?我很确信的说,是的,宏任务先执行的。(之所以这么自信是因为我之前特意研究了这方面的知识,写了大量的用例做验证,写了文章做总结,绝对错不了)


那你意思是,setTimeoutPromise().then()先执行,


我回答:是的。


面试官:你回去再查查资料吧,看一看到底是哪个先执行吧。我的问题问完了,你有什么想问我的吗?


我问了他部门做的产品是什么、团队情况、如果我进来的话负责的是哪块的东西。了解完之后,他让我稍等下。


过了3分钟左右,HR过来了,她问我觉得这场面试咋样,刚才面你的人职级在我们这里算是比较高的了,然后她就跟我介绍了她们公司的情况以及福利制度。介绍完之后,她问我说:我对你写的这个期望薪资比较好奇,我看你上家薪资是12k,怎么期望薪资写了18k呢?涨幅这么高。


我说了理由后,她说:今年市场很差,求职者很多,很多公司都在降低成本,你要是放在互联网红利的时候,你这个涨幅没问题,但2023年这个大环境,你这个涨幅是不可能的。你这边最低期望薪资是多少?


我说:16k,她在求职表上用笔写了下。随后她说,那行,今天的面试就先到这,后面我们电话联系。


回到家后,我立马查了我写的那篇事件循环的文章,验证下我有没有记错。看完之后我发现我并没有记错,于是我又问了下AI,他给我的答案是:


image-20230722182035941


我就纳闷儿了,于是我说宏任务先执行的吧,它的回答是:


image-20230722182223460


它还在嘴硬,我就反问了句,你确定?它终于改变口风了。


image-20230722182301304



这家公司是7月5号面的,等了3天都没联系我,看来是有人要价比我低🌚



做交易所的公司


这家公司是在一个技术交流群看到的招聘信息,公司在海外,远程办公的方式,给的薪资是20k~25k。按照惯例做完自我介绍后他问我:



  • 讲一下vue的生命周期

  • 讲一下computed与watch的区别

  • 讲一下vue的双向绑定和原理

  • 讲一下vue3相比vue2有哪些提升

  • 你有开发过不用脚手架的项目吗?

  • seo优化有了解过吗?讲一下你的见解

  • 响应式网站开发你知道哪些方案?


回答完这些问题后,按照惯例我问了他团队的人员情况以及项目情况,就结束了这场面试。他问的问题也很简单,我回答的也不错。但是,过了3天,最终还是没下文。


做工具软件的公司


这家公司是朋友内推的,经历了三轮面试,我看了下BOSS直聘标价是15k~25k。先是用腾讯会议,让打开屏幕共享和摄像头,做一份笔试题。内容是填空题、判断题、代码题。填空跟判断就是一些简单的问题,代码题是:



  • 观察一组数列,写一个方法求出第31个数字是什么?(通过观察后,发现那是一组斐波那契数列

  • 实现一个深拷贝函数

  • 写一个通用的方法来获取地址栏的某个参数对应的值,不能使用正则表达式。


线上技术面


笔试题做完发给HR后,等待了半个小时,面试官进入了腾讯会议,按照惯例做完自我介绍后他问我:



  • vue3的diff算法做了哪些改进

  • vue双向绑定的原理是什么

  • 假设要设计一个全局的弹窗组件你会怎么设计?

  • 如果这个弹窗组件可以弹出多个,消息会垂直排列,新消息会把旧消息顶起来,每个消息都可以设置一个停留时间,到了时间后就会消失,这一块你会怎么设计?

  • 你了解堆这种数据结构吗?讲一讲你对它的理解


回答完这些问题后,我按照惯例问了他项目情况以及我进去后所负责的模块,就结束了这场线上面试,第二天收到了一面通过的答复。


image-20230722234026788


线下总监面


时间来到7月6日,本来是7月5日面试的,但是面试官临时有事改了时间。


image-20230722234450217


这家公司在林和西地铁站这边,地处CBD,公司应该是很有钱的。到了公司后,HR接待了我,带我进了会议室,等了3分钟左右,技术总监过来了,做完自我介绍后,他问我:



  • 挑一个你最拿手的项目讲一下吧

  • 看你写了很多开源项目,是个爱捣鼓的人,讲一下你的开源项目吧

  • 你会Java,是用的SpringBoot吗?你讲一下你这个开源项目的后端服务是怎么设计的吧

  • 你都知道哪些数据库?进行SQL查询时,你有哪些优化手段来优化查询效率

  • 你讲下vue3和vue2的一个区别吧

  • 你觉得你跟别人相比,你的优势是什么?


回答完这些问题后,我问了他团队的规模以及公司的人员情况,他跟我说:我们公司总共有52个人,很大一部分都是程序员,他们都是全能的,任何一个人拉出来,前端、后端、运维都能做,就好比你让运维来写前端的业务代码他也能写,你也看到了,我们目前不缺人,是想招一个优秀的人做候补。我们这边的技术栈是vue和Electron,你进来的话,负责前端页面以及一些node后端服务的编写。你稍等下,我让我们的HR来面下你。


线下HR面


等了4分钟左右,HR来了,她带我去到了另一个会议室聊,她问了我:



  • 你的离职原因是什么?

  • 你对新工作的期望是怎么样的?

  • 如果公司让你休年假,你必须要做一件事情,你会做什么事情?


问完这些问题后,她问了我期望薪资,我说了20k,她说了一些其他的东西,大概意思就是给不到的话你最低期望是多少,我说18k。


她说:行,了解了,我们这边要做一下横向对比,尽快给你答复,你放心无论结果如何,我们都会给你一个答复的。


面试完的第二天,那个hr跟我发消息说结果还没定。


image-20230723002131979


进入新的一周后,她给我发来了感谢信。


image-20230723002232232



只能感叹卷王太多了,全干工程师的价格已经被你们打到18k以下了👍



做旅游的公司


这是一家在BOSS直聘上约到的面试(11k~17k),到了公司后,HR先让我做了一份笔试题,这份笔试题全是八股文,我把答案短的都写了,比较长的就写了面试时候讲。


做完笔试题后,她带我进了会议室,是两个人面我,一个是前端负责人,另一个是他的领导,做完自我介绍后,那个前端负责人说:我之前在网上看到过你的截图插件,写的很不错。我相信你的技术肯定没问题的,他和他的领导交叉问了我问题:



  • vue3相比vue2做了哪些提升?

  • 讲一下vue的diff算法吧

  • 讲一下V8的垃圾回收机制

  • 讲一下chrome是如何渲染一个网页的

  • 大文件分块上传以及断点续传,你会怎么实现


回答问这些问题后,他们让我稍等下,找来了HR跟我聊,HR问了我期望薪资,我说17K,她也惊讶的说,你上家才给你12k,你怎么一下子要求涨幅这么多,是出于什么考虑呢?我说了理由后,她说:结合我们公司的情况和制度,我们这边给不到你这么多。


我:那大概能给到多少呢?


HR:15k,有些事情我要提前跟你说清楚,我们这边试用期是一个月,现在项目组比较忙,是需要加班的,基本上是996,大概要忙到9月份,项目第一期做好后,就可以按照正常时间上下班了。忙的这段时间是可以累积调休的。试用期不缴纳社保,我们只有五险,没有公积金。


我听了这些后,头皮发麻,一时不知道说啥,我就说了:哦哦 好


HR:如果你能接受的话,我这边是没问题的。


我:我要考虑考虑,晚些时候给你答复。


到了第二天,HR在boss直聘上给我发了消息,问我考虑的如何了,我拒绝了她。


image-20230723004628907


做saas系统的上市公司


这家公司是我6月13号在BOSS直聘上沟通的,6月27号收了我简历,7月3号跟我约了面试,一直持续到7月14号,经历了三轮面试,最终拿到了offer。


HR面(线上)


按照惯例做完自我介绍后,HR让我介绍下公司的产品,以及我在公司的一个职位,技术水平在公司排第几,为什么离职,职业规划和一些其他问题:


HR:你能接受出差吗?


我:这个看情况,如果距离不是很远,出差时间不超过1周,交通、住宿这些都能报销的话,我是接受的。


HR:交通、住宿这些肯定都报销,不然谁愿意出差,我们除了这个外,每天还有一个xxx块的补贴。你在广州这边,出差的话就是去深圳,一般也就去个3、4天,你是前端,几乎不怎么出差。


我:哦哦 那可以的


HR:你对加班是怎么看的?


我:加班的话,如果是项目比较急,我是没问题的,但是如果是其他原因的一些强迫加班,我就不太能接受了


HR:我们这边加班的话,是项目比较急的时候才会,加班不会太频繁。如果加班的话,是可以1:1兑换成调休的,法定节假日加班的话,我们会按照法律规定发放3倍工资


我:哦哦 行


HR:你这边是在广州,如果面试通过的话,是广州的编制。我们广州分部在xx,距离这块的话,你能接受吧?


我:我有查过公司的位置,从我住的这边过去也挺近的,40分钟左右就到了,我可以接受


HR:那行,今天的面试就先到这,后面会安排我们的技术面下你。


技术面(线上)


HR面完后,过了一天,跟我约了技术面。


image-20230723083059122


时间来到7月5号,一男一女,两个人一起面的我。按照惯例做完自我介绍后,他们问了我:



  • 我看你写了很多开源项目和技术文章,这是一个很好的习惯,能很多年坚持做一件事,并且能把这件事情做好,你很厉害。

  • 刚才听你自我介绍说你会Java,你Java目前是一个什么水平?

  • 我看你们公司项目是做web动画编辑器的,你在这个项目中担任的角色是什么?有没有什么印象比较深刻的难题,你是如何解决的?

  • 我看你简历上还写了一个海外项目的重构经验,你能介绍下这个项目吗?以及你在这里面担任的角色是什么?

  • 我看你简历上的项目都是以Vue为主的,那你应该对Vue很熟悉,你讲一下watch与computed的区别

  • vue中组件通信都有哪些方式?

  • vuex刷新后数据会丢失,除了把数据放本地存储外,你还知道其他什么方法吗?

  • 我看你写的那个截图的开源项目用到了canvas,你应该对canvas很熟悉了吧,有这样一个场景:超市中的货架,上面有很多商品。现在要把这个货架用canvas画出来,商品需要支持一些交互,调整大小,移动位置,你会怎么实现?


问完这些问题后,按照惯例,我问了下他们的团队情况以及所做的业务,我进去后所负责的模块,就结束了这场面试。


事业部总经理面(线上)


过了一天,告知我技术面通过了,跟我约了第二天的面试,我看到她说:总经理同时面我跟其他两位候选人。我就压力有点大,从业4年了,第一次遇到这种大场面😂


image-20230723084854849


image-20230723085150444


到了约定好的面试时间,我跟其他两位候选人都进入了会议,过了10分钟,总经理还是没有进来,我就私聊问了下HR。过了一会儿,HR进入了会议。她说:总经理临时有点事情,要换个时间约面试了,真不好意思。


image-20230723085623543


时间来到7月10号,总经理进入腾讯会议后,他先让我们轮流做自我介绍,然后抛出问题,让我们挨个回答,最后他做了总结,给我们三个人做了评价:



  • A(1号面试者):你的组织协调能力应该不错

  • B(我):我看了你在掘金上发的文章以及个人网站,能看出来你的技术实力是最强的。

  • C(3号面试者):你的业务能力应该不错


说完这些后,总经理说晚上会抽时间再单独打电话给我们再聊聊,到了第二天早上我一直没等到电话,我就问了下HR。


image-20230723090532956


过了半个小时左右,电话打来了,他问了我离职原因和两个场景题:



  • 前端的框架有很多,当有新项目的时候,你会通过哪些方面来考虑应该使用哪个框架?

  • 有一个上线的项目它是vue2写的,如果想升级到vue3,但是没有太多的专用时间来做这件事,此时你会怎么做?


回答完这些问题后,挂断了电话,下午1点40多的时候,HR联系我说面试通过了,开始走发offer流程了,到时候会有她的另一个同事联系我。


时间来到7月14号,第一面面我的那个人打电话给我了,跟我聊了薪资、福利制度和五险一金,她说我们公司的五险一金是按照实际工资进行缴纳的,没有绩效,有季度奖和年终奖,会按照公司的盈利情况以及你的工作表现进行发放,后面还有其他问题的话,你随时联系加你微信的那个HR,她是华南区域的负责人。


电话挂断后,过了2小时左右吧,HR联系我说发offer了,我突然想到忘记问上下班时间了,我就确认了下(BOSS直聘标记了时间)。


image-20230723093034336


image-20230723092444819



截止发文时间,我已经入职这家公司很多天了,团队氛围很棒。入职的第一天下午,我接到了我们主管的电话,他让我第二天去一趟武汉,事业部的总经理是在武汉分部的,他要见一下你,那边也有前端在,跟你讲解下业务,熟悉熟悉团队的人。


广州这边的后端架构师同事告诉我出差是不需要自己花钱的,公司内部有一个平台可以直接在上面定高铁票和酒店,我的内部OA和钉钉账号后,他教了我怎么操作。


来武汉后,跟这边的团队成员熟悉了下,聊了下业务,主管告诉我说大概7月26号左右就可以回广州了。我们是双休,我入职后的第一个周六、日是在武汉过的,在这边跟群友面了基,逛了下附近的粮道街,去了玫瑰街、黄鹤楼等地方🥳



作者:神奇的程序员
来源:juejin.cn/post/7258952063219384376
收起阅读 »

一个古诗文起名工具

web
大家好,我是 Java陈序员,我们常常会为了给孩子取名而烦恼,取名不仅要好听而且要规避大众化。其实,我们中华文化博大精深,可以借鉴先辈文人们留下的经典诗词中的文字来起名。今天,给大家介绍一个古诗文起名的工具。 这个工具支持从《诗经》、《楚辞》、《唐诗》、《宋词...
继续阅读 »

大家好,我是 Java陈序员,我们常常会为了给孩子取名而烦恼,取名不仅要好听而且要规避大众化。其实,我们中华文化博大精深,可以借鉴先辈文人们留下的经典诗词中的文字来起名。今天,给大家介绍一个古诗文起名的工具。


这个工具支持从《诗经》、《楚辞》、《唐诗》、《宋词》、《乐府诗集》、《古诗三百首》、《著名辞赋》等经典中来生成不同的名字。


Img


我们可以根据自己的姓氏来生成名字,例如《陈》姓:
Img


一次性可以生成六个姓名,并有对应的诗句来源说明,是不是很nice呢!


再比如,《李》姓:
Img


当然了,这个项目没有任何人工智能, 没有判断名字价值的目标函数,所以都是随机生成的。因此可以孕育出一些惊艳、惊鸿一瞥的名字,反之也会生成智障、搞笑的名字,大家可自行甄别。


大家如果对于这个项目感兴趣的话,也可自行下载代码到本地运行:


# 克隆代码
git clone https://github.com/holynova/gushi_namer.git

# 安装依赖
npm install

# 本地调试
npm start

# 编译
npm run build

或者直接使用线上地址:


http://xiaosang.net/gushi_namer/

线上地址也是完美支持移动端的。


Img


大家快把这个地址收藏到收藏夹吃灰吧,以免需要的时候找不到!


最后


推荐的开源项目已经收录到 GitHub 项目,欢迎 Star


https://github.com/chenyl8848/great-open-source-project


大家的点赞、收藏和评论都是对作者的支持,如文章对你有帮助还请点赞转发支持下,谢谢!



作者:Java陈序员
来源:juejin.cn/post/7282692430100201535
收起阅读 »

限流:别说算法了,就问你“阈值”怎么算?

基础 限流是通过限制住流量大小来保护系统,它尤其能够解决异常突发流量打崩系统的问题。 算法 限流算法也可以像负载均衡算法那样,划分成静态算法和动态算法两类。 静态算法包含令牌桶、漏桶、固定窗口和滑动窗口。这些算法就是要求研发人员提前设置好阈值。在算法运行期间...
继续阅读 »

基础


限流是通过限制住流量大小来保护系统,它尤其能够解决异常突发流量打崩系统的问题。


算法


限流算法也可以像负载均衡算法那样,划分成静态算法和动态算法两类。



  • 静态算法包含令牌桶、漏桶、固定窗口和滑动窗口。这些算法就是要求研发人员提前设置好阈值。在算法运行期间它是不会管服务器的真实负载的。

  • 动态算法也叫做自适应限流算法,典型的是 BBR 算法。这一类算法利用一系列指标来判定是否应该减少流量或者放大流量。动态算法和 TCP 的拥塞控制是非常接近的,只不过 TCP 控制的是报文流量,而微服务控制的是请求流量。


令牌桶


系统会以一个恒定的速率产生令牌,这些令牌会放到一个桶里面,每个请求只有拿到了令牌才会被执行。每当一个请求过来的时候,就需要尝试从桶里面拿一个令牌。如果拿到了令牌,那么请求就会被处理;如果没有拿到,那么这个请求就被限流了。


漏桶


漏桶是指当请求以不均匀的速度到达服务器之后,限流器会以固定的速率转交给业务逻辑。


漏桶是绝对均匀的,而令牌桶不是绝对均匀的。


固定窗口与滑动窗口


固定窗口是指在一个固定时间段,只允许执行固定数量的请求。比如说在一秒钟之内只能执行 100 个请求。


滑动窗口类似于固定窗口,也是指在一个固定时间段内,只允许执行固定数量的请求。区别就在于,滑动窗口是平滑地挪动窗口,而不像固定窗口那样突然地挪动窗口。


限流对象


可以是集群限流或者单机限流,也可以是针对具体业务来做限流。


针对业务对象限流,这一类限流对象就非常多样。



  • VIP 用户不限流而普通用户限流。

  • 针对 IP 限流。用户登录或者参与秒杀都可以使用这种限流,比方说设置一秒钟最多只能有 50 个请求,即便考虑到公共 IP 的问题,正常的用户手速也是没那么快的。

  • 针对业务 ID 限流,例如针对用户 ID 进行限流。


限流后的做法



  • 同步阻塞等待一段时间。如果是偶发性地触发了限流,那么稍微阻塞等待一会儿,后面就有极大的概率能得到处理。比如说限流设置为一秒钟 100 个请求,恰好来了 101 个请求。多出来的一个请求只需要等一秒钟,下一秒钟就会被处理。但是要注意控制住超时,也就是说你不能让人无限期地等待下去。

  • 同步转异步。它是指如果一个请求没被限流,那就直接同步处理;而如果被限流了,那么这个请求就会被存储起来,等到业务低峰期的时候再处理。这个其实跟降级差不多。

  • 调整负载均衡算法。如果某个请求被限流了,那么就相当于告诉负载均衡器,应该尽可能少给这个节点发送请求。


亮点


突发流量



漏桶算法非常均匀,但是令牌桶相比之下就没那么均匀。令牌桶本身允许积攒一部分令牌,所以如果有偶发的突发流量,那么这一部分请求也能得到正常处理。但是要小心令牌桶的容量,不能设置太大。不然积攒的令牌太多的话就起不到限流效果了。例如容量设置为 1000,那么要是积攒了 1000 个令牌之后真的突然来了 1000 个请求,它们都能拿到令牌,那么系统可能撑不住这突如其来的 1000 个请求。



请求大小


如果面试官问到为什么使用了限流,系统还是有可能崩溃,或者你在负载均衡里面聊到了请求大小的问题,都可以这样来回答,关键词是请求大小。



限流和负载均衡有点儿像,基本没有考虑请求的资源消耗问题。所以负载均衡不管怎么样,都会有偶发性负载不均衡的问题,限流也是如此。例如即便我将一个实例限制在每秒 100 个请求,但是万一这个 100 个请求都是消耗资源很多的请求,那么最终这个实例也可能会承受不住负载而崩溃。动态限流算法一定程度上能够缓解这个问题,但是也无法根治,因为一个请求只有到它被执行的时候,我们才知道它是不是大请求。



计算阈值


总体上思路有四个:看服务的观测数据、压测、借鉴、手动计算。


看服务的性能数据属于常规解法,基本上就是看业务高峰期的 QPS 来确定整个集群的阈值。如果要确定单机的阈值,那就再除以实例个数。所以你可以这样来回答,关键词是业务性能数据。



我们公司有完善的监控,所以我可以通过观测到的性能数据来确定阈值。比如说观察线上的数据,如果在业务高峰期整个集群的 QPS 都没超过 1000,那么就可以考虑将阈值设定在 1200,多出来的 200 就是余量。 不过这种方式有一个要求,就是服务必须先上线,有了线上的观测数据才能确定阈值。并且,整个阈值很有可能是偏低的。因为业务巅峰并不意味着是集群性能的瓶颈。如果集群本身可以承受每秒 3000 个请求,但是因为业务量不够,每秒只有 1000 个请求,那么我这里预估出来的阈值是显著低于集群真实瓶颈 QPS 的。



压测



不过我个人觉得,最好的方式应该是在线上执行全链路压测,测试出瓶颈。即便不能做全链路压测,也可以考虑模拟线上环境进行压测,再差也应该在测试环境做一个压力测试。



从理论上来说,你可以选择 A、B、C 当中的任何一个点作为你的限流的阈值。


A 是性能最好的点。A 之前 QPS 虽然在上升,但是响应时间稳定不变。在这个时候资源利用率也在提升,所以选择 A 你可以得到最好的性能和较高的资源利用率。


B 是系统快要崩溃的临界点。很多人会选择这个点作为限流的阈值。这个点响应时间已经比较长了,但是系统还能撑住。选择这个点意味着能撑住更高的并发,但是性能不是最好的,吞吐量也不是最高的。


C 是吞吐量最高的点。实际上,有些时候你压测出来的 B 和 C 可能对应到同一个 QPS 的值。选择这个点作为限流阈值,你可以得到最好的吞吐量。


性能 A、并发 B、吞吐量 C。


无法压测:



不过如果真的做不了,或者来不及,或者没资源,那么还可以考虑参考类似服务的阈值。比如说如果 A、B 服务是紧密相关的,也就是通常调用了 A 服务就会调用 B 服务,那么可以用 A 已经确定的阈值作为 B 的阈值。又或者 A 服务到 B 服务之间有一个转化关系。比如说创建订单到支付,会有一个转化率,假如说是 90%,如果创建订单的接口阈值是 100,那么支付的接口就可以设置为 90。



如果我这是一个全新的业务呢?也就是说,你都没得借鉴。这个时候就只剩下最后一招了——手动计算。



实在没办法了,就只能手动计算了。也就是沿着整条调用链路统计出现了多少次数据库查询、多少次微服务调用、多少次第三方中间件访问,如 Redis,Kafka 等。举一个最简单的例子,假如说一个非常简单的服务,整个链路只有一次数据库查询,这是一个会回表的数据库查询,根据公司的平均数据这一次查询会耗时 10ms,那么再增加 10 ms 作为 CPU 计算耗时。也就是说这一个接口预期的响应时间是 20ms。如果一个实例是 4 核,那么就可以简单用 1000ms÷10ms×4=400 得到阈值。




手动计算准确度是很差的。比如说垃圾回收类型语言,还要刨除垃圾回收的开销,相当于 400 打个折扣。折扣多大又取决于你的垃圾回收频率和消耗。



升华:



最好还是把阈值做成可以动态调整的。那么在最开始上线的时候就可以把阈值设置得比较小。后面通过观测发现系统还很健康,就可以继续上调阈值。





此文章为9月Day25学习笔记,内容来源于极客时间《后端工程师的高阶面经》


作者:09cakg86qfjwymvm8cd3h1dew
来源:juejin.cn/post/7282245376425459768
收起阅读 »

百分百空手接大锅

web
背景 愉快的双休周末刚过完,早上来忽然被运营通知线上业务挂了,用户无法下单。卧槽,赶紧进入debug模式,一查原来是服务端返回的数据有问题,赶紧问了服务端,大佬回复说是业务部门配置套餐错误。好在主责不在我们,不过赶紧写了复盘文档,主动找自己的责任,扛起这口大锅...
继续阅读 »

背景


愉快的双休周末刚过完,早上来忽然被运营通知线上业务挂了,用户无法下单。卧槽,赶紧进入debug模式,一查原来是服务端返回的数据有问题,赶紧问了服务端,大佬回复说是业务部门配置套餐错误。好在主责不在我们,不过赶紧写了复盘文档,主动找自己的责任,扛起这口大锅,都怪我们前端,没有做好前端监控,导致线上问题持续两天才发现。原本以为运营会把推辞一下说不,锅是她们的,可惜人家不太懂人情世故,这锅就扣在了技术部头上。虽然但是,我还是静下心来把前端异常监控搞了出来,下次一定不要主动接锅,希望看到本文的朋友们也不要随便心软接锅^_^


监控


因为之前基于sentry做了埋点处理,基础已经打好,支持全自动埋点、手动埋点和数据上报。相关的原理可以参考之前的一篇文章如何从0-1构建数据平台(2)- 前端埋点。本次监控的数据上报也基于sentry.js。那么如何设计整个流程呢。具体步骤如下:




  1. 监控数据分类




  2. 监控数据定义




  3. 监控数据收集




  4. 监控数据上报




  5. 监控数据输出




  6. 监控数据预警




数据分类


我们主要是前端的数据错误,一般的异常大类分为逻辑异常和代码异常。基于我们的项目,由于涉及营收,我们就将逻辑错误专注于支付异常,其他的代码导致的错误分为一大类。然后再将两大异常进行细分,如下:




  1. 支付异常


    1.1 支付成功


    1.2 支付失败




  2. 代码异常


    2.1 bindexception


     2.1.1  js_error

    2.1.2 img_error

    2.1.3 audio_error

    2.1.4 script_error

    2.1.5 video_error



  3. unhandleRejection


    3.1 promise_unhandledrejection_error


    3.2 ajax_error




  4. vueException




  5. peformanceInfo




数据定义


基于sentry的上报数据,一般都包括事件与属性。在此我们定义支付异常事件为“page_h5_pay_monitor”,定义代码异常事件为“page_monitor”。然后支付异常的属性大概为:



pay_time,

pay_orderid,

pay_result,

pay_amount,

pay_type,

pay_use_coupon,

pay_use_coupon_id,

pay_use_coupon_name,

pay_use_discount_amount,

pay_fail_reason,

pay_platment


代码异常不同的错误类型可能属性会有所区别:



// js_error

monitor_type,

monitor_message,

monitor_lineno,

monitor_colno,

monitor_error,

monitor_stack,

monitor_url

// src_error

monitor_type,

monitor_target_src,

monitor_url

// promise_error

monitor_type,

monitor_message,

monitor_stack,

monitor_url

// ajax_error

monitor_type,

monitor_ajax_method,

monitor_ajax_data,

monitor_ajax_params,

monitor_ajax_url,

monitor_ajax_headers,

monitor_url,

monitor_message,

monitor_ajax_code

// vue_error

monitor_type,

monitor_message,

monitor_stack,

monitor_hook,

monitor_url

// peformanceInfo 为数据添加 loading_time 属性,该属性通过entryTypes获取

try {

const observer = new PerformanceObserver((list) => {

for (const entry of list.getEntries()) {

if (entry.entryType === 'paint') {

sa.store.set('loading_time', entry.startTime)

}
}

})

observer.observe({ entryTypes: ['paint'] })

} catch (err) {

console.log(err)

}


数据收集


数据收集通过事件绑定进行收集,具体绑定如下:


import {

BindErrorReporter,

VueErrorReporter,

UnhandledRejectionReporter

} from './report'

const Vue = require('vue')


// binderror绑定

const MonitorBinderror = () => {

window.addEventListener(

'error',

function(error) {

BindErrorReporter(error)

},true )

}

// unhandleRejection绑定 这里由于使用了axios,因此ajax_error也属于promise_error

const MonitorUnhandledRejection = () => {

window.addEventListener('unhandledrejection', function(error) {

if (error && error.reason) {

const { message, code, stack, isAxios, config } = error.reason

if (isAxios && config) {

// console.log(config)

const { data, params, headers, url, method } = config

UnhandledRejectionReporter({

isAjax: true,

data: JSON.stringify(data),

params: JSON.stringify(params),

headers: JSON.stringify(headers),

url,

method,

message: message || error.message,

code

})

} else {

UnhandledRejectionReporter({

isAjax: false,

message,

stack

})

}

}

})

}

// vueException绑定

const MonitorVueError = () => {

Vue.config.errorHandler = function(error, vm, info) {

const { message, stack } = error

VueErrorReporter({

message,

stack,

vuehook: info

})

}

}

// 输出绑定方法

export const MonitorException = () => {

try {

MonitorBinderror()

MonitorUnhandledRejection()

MonitorVueError()

} catch (error) {

console.log('monitor exception init error', error)

}

}


数据上报


数据上报都是基于sentry进行上报,具体如下:



/*

* 异常监控库 基于sentry jssdk

* 监控类别:

* 1、window onerror 监控未定义属性使用 js资源加载失败问题

* 2、window addListener error 监控未定义属性使用 图片资源加载失败问题

* 3、unhandledrejection 监听promise对象未catch的错误

* 4、vue.errorHandler 监听vue脚本错误

* 5、自定义错误 包括接口错误 或其他diy错误

* 上报事件: page_monitor

*/


// 错误类别常量

const ERROR_TYPE = {

JS_ERROR: 'js_error',

IMG_ERROR: 'img_error',

AUDIO_ERROR: 'audio_error',

SCRIPT_ERROR: 'script_error',

VIDEO_ERROR: 'video_error',

VUE_ERROR: 'vue_error',

PROMISE_ERROR: 'promise_unhandledrejection_error',

AJAX_ERROR: 'ajax_error'

}

const MONITOR_NAME = 'page_monitor'

const PAY_MONITOR_NAME = 'page_h5_pay_monitor'

const MEMBER_PAY_MONITOR_NAME = 'page_member_pay_monitor'

export const BindErrorReporter = function(error) {

if (error) {

if (error.error) {

const { colno, lineno } = error

const { message, stack } = error.error

// 过滤

// 客户端会有调用calljs的场景 可能有一些未知的calljs

if (message && message.toLowerCase().indexOf('calljs') !== -1) {

return

}

sa.track(MONITOR_NAME, {

//属性

})

} else if (error.target) {

const type = error.target.nodeName.toLowerCase()

const monitorType = type + '_error'

const src = error.target.src

sa.track(MONITOR_NAME, {

//属性

})

}

}

}

export const UnhandledRejectionReporter = function({

isAjax = false,

method,

data,

params,

url,

headers,

message,

stack,

code

}
) {

if (!isAjax) {

// 过滤一些特殊的场景

// 1、自动播放触发问题

if (message && message.toLowerCase().indexOf('user gesture') !== -1) {

return

}

sa.track(MONITOR_NAME, {

//属性

})

} else {

sa.track(MONITOR_NAME, {

//属性

})

}

}

export const VueErrorReporter = function({ message, stack, vuehook }) {

sa.track(MONITOR_NAME, {

//属性

})

}

export const H5PayErrorReport = ({

isSuccess = true,

amount = 0,

type = -1,

couponId = -1,

couponName = '',

discountAmount = 0,

reason = '',

orderid = 0,

}
) => {

// 事件名:page_member_pay_monitor

sa.track(PAY_MONITOR_NAME, {

//属性

})

}


以上,通过sentry的sa.track进行上报,具体不作展开


输出与预警


数据被上报到大数据平台,被存储到hdfs中,然后我们直接做定时任务读取hdfs进行一定的过滤通过钉钉webhook输出到钉钉群,另外如果有需要做数据备份可以通过hdfs到数据仓库再到kylin进行存储。


总结


数据监控对于大的,特别是涉及营收的平台是必要的,我们在设计项目的时候一定要考虑到,最好能说服服务端,让他们服务端也提供相应的代码监控。ngnix层或者云端最好也来一层。严重的异常可以直接给你打电话,目前云平台都有相应支持。这样有异常及时发现,锅嘛,接到手里就可以精准扔出去了。


作者:CodePlayer
来源:juejin.cn/post/7244363578429030459
收起阅读 »

降本增效后胡诌一下

上周我ld下午突然找我喝咖啡,暗示的事情不言而喻,果然下一波降本增效不期而遇了,当然这次我是主动要的桶,说句实话此时此刻我不太看好阿逼,几次降本之后明显能感觉到人心早就散了,即使留着我估摸着也找不到我喜欢的工作状态了。 另外啊,人到中年的我心态也还是不太稳定啊...
继续阅读 »

上周我ld下午突然找我喝咖啡,暗示的事情不言而喻,果然下一波降本增效不期而遇了,当然这次我是主动要的桶,说句实话此时此刻我不太看好阿逼,几次降本之后明显能感觉到人心早就散了,即使留着我估摸着也找不到我喜欢的工作状态了。


另外啊,人到中年的我心态也还是不太稳定啊,现在整个市场行情挺差的,基本上来说这周也就几家公司约了我面试,我这个时候才感受到之前别人说的手机没响是多么恐怖,相对来说竞争力确实是完全比不上年轻人了。


好在过了几天压力期之后这几天仿佛也想开了,想了想错的也不是我们这些浮萍,而是这个世界。不求一生安好,但求问心无愧吧。


另外其实还有好多想做的事情并没有做完,也还是挺遗憾的。比如最新的kotlin和compose,还有我最近刚打算推进的资源文件治理等等。也算是抱憾而去了啊。


另外前几天那个5000星github大佬也让我有点大大的破防,被人称呼为七年大龄我还是不李姐啊,成年人的世界还真的是很残忍啊


愿后续找工作顺利,对我自己来说吧,我觉得我还是处于技术人当打之年的,我也还是想做些有意思得事,在此与诸君共勉。


年纪越大越喜欢老歌,这几天只能靠沉默是金来安慰自己。冥冥中都注定你我苦与贫,是错永不对真还是真。


作者:究极逮虾户
来源:juejin.cn/post/7281162206947622949
收起阅读 »

有人说SaToken吃相难看,你怎么看。

前言 今天摸鱼逛知乎,偶然看到了一个回答,8月份的,是关于SaToken的,一时好奇就点了进去。 好家伙,因为一个star的问题,提问的人抱怨了许多,我有些意外,就仔细看了下面的评论,想知道一部分人的看法。 案发现场 大体上,分为两派。 一派是...
继续阅读 »

前言



今天摸鱼逛知乎,偶然看到了一个回答,8月份的,是关于SaToken的,一时好奇就点了进去。



1.png



好家伙,因为一个star的问题,提问的人抱怨了许多,我有些意外,就仔细看了下面的评论,想知道一部分人的看法。



案发现场



大体上,分为两派。




一派是对于强制star尤为反感,乃至因爱生恨(打个问号)?




比如下面这种,狂喷作者的。当我看到所谓“花几个工作日自己也能撸一个”这句话的时候,差点没忍住把酱香拿铁喷在电脑上。




本想敲几个字对垒下,但我好歹也是知乎认证的号,想想算了,没必要和这种人打口水仗。



4.png



还有一些是拿数据指责Sa-Token,以及搬出Spring Security做对比的,字里行间一股子微博的味道。



5.png



总而言之,反感这种强制star的人,我发现他们是内心真的极其反感,就像是自己被作者抛弃了一样。



7.png



后面喷着喷着,拔出萝卜带出泥,好吧,ruoyi也被拉出来示众了,这味儿太冲了。



8.png



当然,另一派就是持不同看法的,里面有一句话总结的倒是挺有意思。



6.png



说到这里,其实Sa-Token的作者也亲自下场做了一些解释,比如解释不想star可以如何做,这一点我觉得略显牵强,但后面也给了别的解决方式,听取了部分评论者的中肯意见。



2.png



重要的是,作者最后的回答,就像是无声地呐喊,也许很多喷子接受不了这种呐喊,因为这个“孩子”不是他们的,别人家的孩子跟我有什么关系。



3.png


国内开源现状



通过这个事情,其实勾起了我一些回忆,可能年轻点的程序员是不了解的,国内的开源生态以前是个什么情况。




像我这样年纪稍微大点的可能就见过那个过程,说白了,就是来一批死一批。




没错,国内开源生态就是个充满病菌的牧场,里面养了一群牛羊,结局是大多都病死了,真正能上餐桌的却没几个。




还有人记得当年开源生态圈很离谱的一件事情吗,XXL-JOB的作者发帖伸冤,因为自己的开源项目竟然被某个互联网公司拿去申请了软著。




等于说一个花费心力的项目,仅仅因为开源协议被钻了漏洞,就直接成别人的了,作者没办法只能在网上伸冤求助,以及找开源中国出面解决。




为什么这些公司敢这么做,换成你是作者你接受得了么,你有信心以个人的力量对抗事先有准备的这些打擦边球的侵权么。




因为国内的开源生态就是病态的、畸形的,那几年国内开源项目如雨后春笋,绝大部分作者根本还没有较高的经营意识,凭的就是一腔热爱分享的情怀,以及对拥有自己的一个开源项目这件事的热忱。




然后因为不懂法律,被钻空子,竹篮打水一场空,这样的案例出现一个,就会引起寒蝉效应,开源作者人人自危,谁还敢用授权范围更大的协议。




树上有七只鸟,打死了一只,还剩几只?




然后,再举例说一下上面截图中有喷子提到的ruoyi。




我想问问,现在有多少Java程序员是一路看着ruoyi走过来的。




我猜不多,就算有,也是中途上车的。




我可以简单说下ruoyi当初的处境,虽然只是一个后台管理的项目,我是真没想到时隔多年作者竟然还在写。




当初围绕在ruoyi身边的是一大堆出色的后台管理项目,各具特色,不少都比它要火,但最后具备代表性的只剩ruoyi了。




因为作者一直在迭代,我记得第一次看到ruoyi的时候,作者还写着项目名称的描述,是想象自己未来女儿的名字,所以起了若依。




能坚持这么多年不停歇,那些年你也根本别想凭着开源项目赚什么钱,估计连你工资的零头都没有,但人家还是能迭代到现在。




我就想着,单纯寻思着,也该到了人家收获果实的季节了吧。




我是打心里佩服这些人的,我没觉得比别人差,有些项目花时间我也能写,问题是,我做不到啊,你呢。



总结



如果有一个同行写了开源项目,他想挣钱,我支持,但是项目越来越烂,我会离开,后会无期。




如果有一个同行写了开源项目,他想挣钱,我支持,但是项目越来越好,我会分享,也会付钱。




当我们不断坚持追求,最终换来真正感人的回报,何尝不是生命中最美妙的旋律。




我真诚希望给国内优秀的开源作者更多能挣钱的空间,让那些项目越来越好。




这是我对那些当初“死去”的开源作者的缅怀,也是对未来更多开源作者的殷切期待。




以上纯属个人看法,不收钱的,轻点喷。




如果喜欢,请点赞+关注↑↑↑,持续分享干货和行业动态哦~


作者:程序员济癫
来源:juejin.cn/post/7282696271863906316
收起阅读 »

5分钟看完被讨厌的勇气

是一本什么样的书 是一本心理学书,书中主要观点来自于阿德勒: 阿尔弗雷德·阿德勒(Alfred Adler ,1870年2月7日-1937年5月28日),奥地利精神病学家。人本主义心理学先驱,个体心理学的创始人,曾追随弗洛伊德探讨神经症问题,但也是精神分析学派...
继续阅读 »


是一本什么样的书


是一本心理学书,书中主要观点来自于阿德勒:


阿尔弗雷德·阿德勒(Alfred Adler ,1870年2月7日-1937年5月28日),奥地利精神病学家。人本主义心理学先驱,个体心理学的创始人,曾追随弗洛伊德探讨神经症问题,但也是精神分析学派内部第一个反对弗洛伊德的心理学体系的心理学家。



因全球畅销书《人性的弱点》和《美好的人生》而闻名的戴尔·卡耐基也曾评价阿德勒为“终其一生研究人及人的潜力的伟大心理学家”,而且其著作中也体现了很多阿德勒的思想。同样,史蒂芬·柯维所著的《高效能人士的7个习惯》中的许多内容也与阿德勒的思想非常相近。


可以学到什么


教你获得幸福,教你如何过得爽


怎么做


一、目的论


心理创伤


心理创伤:精神创伤(或心理创伤)是指那些由于生活中具有较为严重的伤害事件所引起的心理、情绪甚至生理的不正常状态(比如一遭被蛇咬,十年怕井绳)


弗洛伊德的原因论,你现在的问题是由于过去的一段悲惨的经历所引发的


阿德勒和弗洛伊德观点完全相反


阿德勒心理学:心理创伤并不存在


人并不是住在客观的世界,而是住在我们自己营造的主观世界(也可以说是你赋予这个经历的意义)




  • 冬暖夏凉的井水,其实是恒定的18度




  • 墨镜一戴,谁也不爱




阿德勒的目的论,人之所以性格扭曲,不是由于过去发生的事情所引发的,而是因为他出于“某种目的”,主动选择了这个扭曲的性格


例子


1、有一个小朋友在学校遭遇过校园霸凌,从此性格变得孤僻,不爱说话;


可以引起父母的关注,害怕再次受到校园霸凌,或者说只是他觉得更舒服的一个状态而已


2、患有脸红恐惧症的女生想要对喜欢的人告白,但又不敢





区别:


原因论:人的现在是由人的过去所决定的,你有怎样的过去,就有怎样的现在


目的论:人的现在是由现在的目的所决定的,而这个目的有可能是存在于你的潜意识中


例子


1、青年在咖啡厅被服务员不小心把咖啡洒在他衣服上了,这是青年下狠心花了大价钱买的一件新衣服啊,所以他忍不住当场大发雷霆,而平时他根本就不会在公共场合大声喧哗



先产生大发雷霆的目的,才产生愤怒的情绪。青年想通过大发雷霆来震慑犯错的这名服务员,进而才使服务员认真听我们讲话(讲道理太麻烦,还不如“表演生气”高效)


2、川剧变脸的家长,愤怒是可收可放的手段


阿德勒认为,恐惧、自卑、愤怒等情绪都是人们逃避现实的工具而已。




人现在所做出的决定,反过来可以影响到过去,改变你过去的意义。


例子


1、爱因斯坦上学的时候连个小板凳都坐不好,所以经常被老师同学嘲笑,后来爱因斯坦成为了世界上最聪明的人,坐不好小板凳就成了一段佳话


「无论之前的人生发生过什么,都对今后的人生如何度过没有影响。」决定自己人生的是活在「此时此刻」的你自己。


二、课题分离


一切烦恼都源于人际关系


例子:


1、你觉得自己穷,是因为你见过富的


2、你觉得自己矮,是因为有比你高的


3、你觉得自己不好看,是因为有比你好看的


烦恼的根源就是和别人比较


如何解决人际关系带来的烦恼


暂时无法在飞书文档外展示此内容


例子:


1、你要辞职去创业,你老婆不同意


「辞职」是你的课题,「老婆不同意」是她的课题


2、孩子想要一个玩具,「想要」是孩子的课题,而给不给买是父母的课题,但孩子由于认知和能力没有发展完全,可能会用一些不当方式去干涉父母的课题,破坏「课题」中的界限。


年幼的孩子可能会用极端的情绪发泄来要求父母满足他们的需求,或者是不喜欢学习,沉迷游戏等等。


但是父母不能因为恪守「课题分离」,就让孩子自生自灭,而是应该培养孩子的兴趣,挖掘他们的潜能,从而让孩子感受到学习的乐趣,或者习得一些适当的寻求需求满足的方法。


(你可以把马拉到水边,但你不能强迫马喝水)


所以,学习「课题分离」的意义并不是让我们对他人的事情置之不理,而是帮助我们理清摆在面前错综复杂的事情或情绪,不受他人课题的裹挟。


人为什么总要去干涉别人或者被别干涉


其实都是为了自己,《自私的基因》里解释生命体只是基因的生存机器,生命体的一切行为都是为了自己更好的生存。


例子:


1、鳄鱼嘴里的牙签鸟



鳄鱼和牙签鸟是一对非常特别的互利共生关系。它们之间的这种特殊关系,使得它们可以在大自然中互相帮助,让彼此成为生存的关键。


不表扬也不批评


表扬也是一种干涉,会让人觉得自己存在的价值是别人的肯定,而不是自己本身


不追求他人认可


我们的存在价值并不是通过他人认可而获得,而是应该通过对集团的贡献而获得的。


如果追求的是他人认可,那么他人不存在的时候,你就不会行动,比如做好事的时候周围没人就不做了,就会被他人所束缚,会因为他人的观点而做出改变


如果我们不是为了别人的认可而存在,那我们应该如何存在


三、共同体感觉


就是把他人看作伙伴,并能够从中感到自己有位置的状态,这就是共同体感觉。


自我接纳


自我肯定是明明做不到但还是暗示自己说“我能行”或者“我很强”,也可以说是一种容易导致优越情结的想法,是对自己撒谎的生活方式。


自我接纳是指假如做不到就诚实地接受这个“做不到的自己”,然后尽量朝着能够做到的方向去努力,不对自己撒谎。


他者信赖


他者信赖就是说在人际交往中我们需要无条件地相信我们自己想去和他建立关系的人,与需要抵押的信用不同,信赖无需任何的附加条件。


背叛是别人的课题,我们没有办法改变。


他者贡献


他者贡献并不是讨好。


我们可以试想一下,是不是每次当自己为他人或是群体做出贡献的时候,我们就会感觉到开心,因为我们在这过程中体会到了自己的价值,他者贡献的目的正是与此相关,他者贡献并非舍弃自身而效劳他人,而是在贡献的过程中,找到自我的真正价值。


而讨好呢?我们就可以将其看做是一种自我牺牲。它是一种过度迎合他人而放弃自我感受的行为,它的目的并非为了找回自我价值,而是取悦他人,以达到不被他人遗弃的目的。所以我们能够看到,他者贡献和讨好有着本质的区别。


工作的本质就是贡献。


正因为接受了真实的自我,也就是自我接纳,才能够不惧背叛地做到他者信赖,而且正因为对他人给予无条件地信赖并能够视他人为自己的伙伴,才能做到他者贡献;同时,正因为对他人有所贡献,才能够体会到我对他人有用,进而接受真实的自己,做到自我接纳。


暂时无法在飞书文档外展示此内容


你只要做到这三步,你就能从他人的评价中获得释放


那么不活在别的评价中,就会被别人所讨厌


如果想要获得真正的自由,就需要有被别人讨厌的勇气


作者表达的并不是所谓自由就是被人讨厌, 而是所谓自由是拥有被别人讨厌的勇气,主旨在“勇气”而不是“被讨厌”,“勇气”是自己的课题 是我们自己可以改变的 “被讨厌”只是别人的课题 所以阿德勒的哲学被称为勇气哲学


最后,把书中的一句话送给大家:


“倘若自己都不为自己活出自己的人生,那还有谁会为自己而活呢?”


作者:VD
来源:juejin.cn/post/7281957723952169000
收起阅读 »

中小企业数字化转型实施过程中的管理和思考

1. 往事再回首 最近年中开部门总结会议,我向公司领导和同事总结了入职近三年以来,企业数字化转型的过程和成果。 我所在的企业是一家中华老字号企业,也是一家传统制造业企业,十几年前由国企转私营。入职前,有关领导和人事部门简要给我介绍了他们企业信息化系统实施情况,...
继续阅读 »

1. 往事再回首


最近年中开部门总结会议,我向公司领导和同事总结了入职近三年以来,企业数字化转型的过程和成果。
我所在的企业是一家中华老字号企业,也是一家传统制造业企业,十几年前由国企转私营。入职前,有关领导和人事部门简要给我介绍了他们企业信息化系统实施情况,停下来大概仅有U8、OA这两个算拿得出手的信息系统,一个用来管理供应链一个用来作日常工作审批。其余像生产管理系统MES,立体库管理系统等都是找一些小公司或个人开发的系统,以长时间无人运维,这些项目甚至连公司、开发人员以及相关资料都找不到了。


未入职之前,公司已在准备相关的上市计划,企业数字化转型已迫在眉睫。企业数字化转型是IT部门打翻身仗的机会,我是顺应公司制定好的数字化转型战略计划后,招兵买马进来的,职责就是协助我的直系领导(CIO)组建一支专业能力足够强的IT团队,制定企业数字化转型战略目标和计划,通过招投标实施信息化项目,来满足企业转型的需求。




大部分中小企业发展到一定规模,都会面临着管理难题,然而,IT部门在老板眼里,往往又是一个不受重视的支出型的部门,大部分的中小型制造型企业的IT部门,活多事杂话语权少,往往充当着修电脑,修网络等等此类的基础设施运维角色。在入职后的前三个月,部门未招聘任何一名员工,只为摸清楚企业信息化的底细.....


三个月的时间,让我清晰的认识到,事情远远不及我想的那么简单,该企业因长期没有信息化项目的实施和开展,公司管理层甚至也是因为上市才有了信息化建设的初步想法,很多人也因为墨守成规,甚至连一些基本的信息化、常用办公软件操作的本领都没有,信息化素质有待提升,IT部门工作开展面临者不小的压力。


2. 工作成果总结


入职公司这三年多以来,我与诸位领导和IT部门的各位同事们,通过夜以继日的不懈奋斗,先后实施了诸多项目。其中包括公司位于广东某市的智能工厂项目(2022年10月正式投产),生产管理系统MES系统(2022年8月正式上线),以及ERP系统SAP(替代U8)(2023年1月1日正式上线),并引入了业务流程管理系统BPM(替代OA)(2022年1月18日正式上线)。


可以说,已经完成了企业在供应链生产制造端的初步转型,在办公自动化上初见成效。未来,我们将实施供应商和经销商协同平台,同时还要在销售端发力,拉起企业数字化转型的大网,覆盖上下游供应商、终端用户,以及企业内所有的员工。


3.反思和总结


3.1 企业如何进行数字化转型


中小企业在数字化转型过程中是十分渴望实现信息化、数字化的。然而,由于他们的信息化底子薄,信息化人才队伍建设难,以及企业文化等种种因素的影响,往往会导致信息化项目无法发挥出其真正的价值。 


 中小企业数字化转型是一项系统工程,需要全面考虑企业的战略目标、技术支持和组织变革,同时注重持续的管理和优化。企业数字化转型主要包含以下步骤。


明确目标:确定数字化转型的目标和期望效果,如提高生产效率、优化供应链管理、拓展新的市场渠道等。


评估现状:分析企业现有的信息化水平、IT基础设施、业务流程和人员能力,并识别存在的痛点和问题。


制定战略计划:根据目标和评估结果,制定详细的数字化转型战略计划,包括具体的项目和时间表。


技术选型与建设:选择适合企业需求的技术解决方案,如企业资源计划(ERP)系统、客户关系管理(CRM)系统、供应链管理系统等,并进行系统的规划、设计和实施。


数据整合与分析:确保各个系统之间的数据交互和共享,建立数据仓库或数据湖,通过数据分析和挖掘获得有价值的洞察。


组织变革与培训:调整组织结构和流程,培养员工适应数字化转型的能力,提供相关培训和支持。


监控与优化:建立监控机制,及时评估数字化转型的效果,并进行优化和调整,以保持持续改进。


在数字化转型过程中,中小企业需要关注以下几个重点:


项目管理:合理规划项目的范围、时间和资源,确保项目的顺利实施和交付。
数据安全:加强信息安全意识,采取有效的措施保护企业数据和客户信息的安全性。
风险管理:评估数字化转型可能面临的风险,制定相应的风险管理措施,并建立灵活应对的机制。
合作伙伴选择:选择可靠的技术供应商和合作伙伴,共同推动数字化转型的实施和成功。


3.2 IT部门如何打好数字化战役


当公司引入新系统、用户提出新需求时,IT部门要抓住这样的契机,推动公司对原有不合理的业务流程进行改造。然而,这是一场硬仗,弄不好可能会项目推进不力,还得罪人。所以要想打好这场仗,首先IT部门内部要统一思想,一致对"外"。明确好我们的使命和愿景,确定好我们工作的推进方法和节奏,保证信息在部门内得到完整的有效的传递。



 IT部门在企业转型过程中,担任十分重要的专业角色。我认为IT部门必须承担的使命,主要包括以下几项:


1、改变用户思维,培养用户习惯:这个过程考验IT部门成员对公司业务的熟悉程度、对行业标准化流程的理解程度,以及谈判能力能否说服用户接受所提出的改造方案。


2、抓住项目机遇,实现战略目标:通过项目的实施,技术的驱动,把原有的一些不合理业务从企业业务中重点拿出来讨论,并制定专业合规的方案,在兼顾业务发展的同时,又要考虑到后期如何监控(建立指标)该项业务改革后的成效。这是一件十分有挑战且对个人能力要求极高的事情,需要在公司各个系统实施前,做好系统信息交互的顶层设计,定好发展方向,才能把此事做好。


3、建立数据指标,监控实施效果:在项目转入运维后,能通过查看数据指标,掌握到设计好的业务方案实施是否成功,带来的效益如何。同时通过建立数据指标,可以辅助高层进行决策,让技术倒逼业务优化,实现技术与业务的双向驱动。


4. 评估数字化转型实施成果


以下是我的个人观点和总结:
我认为,制造型中小企业数字化转型,必须经历从 实现自动化到迈向数字化最后实现智能化的阶段。如何评估企业数字化转型成功与否?如何衡量此时企业数字化程度?


当前信息时代,数据是最重要的生产资源!数据在企业中流通的量级大,效率高,覆盖面广,是数据发挥价值大小的评判标准。企业数字化转型程度,取决于


  • 企业内部是否建立起完整的数据传输链(数据在各系统之间共享)
  • 能够完整的查询到某一项数据从进入系统开始,直至归档为止的完整链路(数据生命周期可追溯)
  • 数据分析,数据可视化提供决策能力(充分发挥数据价值)



5.项目管理中的方法论


在项目管理和实施中,要善于使用管理方法和指导和总结项目工作,以下是常见的几种方法论:


5.1 8020原则


在公司现阶段,生产端数字化目标实现后,用户的需求猛增。此时我提出:利用8020原则,聚焦20%我们认为有价值的需求,把握用户提出需求的机会,对业务流程再进行进一步的完善和提升,剩余的80%需求可以暂缓甚至拒绝。我们目前完成生产端信息化改造后,企业用户也慢慢被我们培养出利用信息化手段和工具解决问题的思维习惯。


随之而来的,就是对IT部门的挑战,用户面对这些新系统,会不断提需求,期望IT部门满足他们。从我目前工作的经历来看,大部分的需求是来自于职员级别的,然而,他们提出的需求,往往是出于减少他们工作量以及出错概率的定位上来提的。


而企业的中层管理者,大都是沉默的,毕竟屁股决定脑袋。需求增多,面临就是开发工作的陡然增加,此时IT部门就要学会与用户斡旋,利用一些特殊的手段,来筛选需求。聚焦对业务有重点提升的需求,才能发挥出IT部门最大的价值! 



5.2 PDCA循环


PDCA循环是一种连续改进的管理方法,它代表了Plan(计划)、Do(执行)、Check(检查)和Act(行动)四个阶段。它的目的是通过循环反复进行这四个阶段,不断完善和优化过程,实现持续的改善和提高。


通过PDCA循环,可以不断发现问题、改进流程和工作方式,提高质量和效率,逐步实现目标。它是一种有效的管理工具,能帮助组织在不断变化的环境中保持竞争力。


5.3 SWOT分析


SWOT分析是一种战略管理工具,用于评估一个组织或个人的优势(Strengths)、劣势(Weaknesses)、机会(Opportunities)和威胁(Threats)。通过进行SWOT分析,可以了解一个组织或个人在特定环境中的情况,并制定相应的战略。
下图是我总结的,我部门团队目前所处的形势: 



6.总结


中小企业数字化转型是一个历史必然的过程。随着信息技术不断发展和普及,企业要想保持竞争力和适应市场变化,数字化转型已成为一种必备的发展战略。在这个过程中,我们积累了许多宝贵的经验,也面临了一些需要注意的问题。


中小企业数字化转型还需要面对市场和竞争的问题。随着数字化转型的普及,市场竞争将变得更加激烈。企业需要及时调整自己的业务模式和市场策略,提高自身的竞争力。


首先,需要明确转型的目标和意义。中小企业进行数字化转型的目的通常是提高效率、降低成本、改善客户体验等。在实施过程中,我们要清楚地定义自己的转型目标,并为此付出努力。同时,也要理解数字化转型的意义,认识到它不仅仅是一次技术升级,更是一次全方位的组织变革。


其次,要注重组织文化和人员培养。数字化转型不仅仅是一项技术工作,更需要对组织文化进行调整和塑造。中小企业需要建立一个支持创新和变革的文化环境,激发员工的数字思维和创新能力。此外,投资人员培养和技能提升也是关键,因为他们将是数字化转型中的推动者和执行者。


此外,应该遵循逐步推进的原则。由于中小企业的资源有限,一次性完成全面的数字化转型可能会带来巨大的压力和风险。因此,我们建议采取渐进的方法,从一个具体的业务领域或流程开始,逐步迭代和扩展。这样可以确保转型的可控性和成功性,并在实施过程中不断调整和改进。


总之,中小企业数字化转型是一项具有重要意义的任务。通过数字化转型,企业可以实现更高效的运营和更好的市场竞争力。然而,数字化转型也需要企业面对一些挑战和问题。只有充分认识到这些问题并采取相应的措施,企业才能顺利地完成数字化转型,实现持续发展。


上一篇:项目进度管理工具:进度网络图


一些数字化转型的参考链接:



本文所述的经验总结仅表示个人经验和观点,希望能为中小企业的数字化转型提供一些借鉴和启示。


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

跨浏览器兼容性指南:解决常见的前端兼容性问题

跨浏览器兼容性是前端开发中至关重要的概念。由于不同浏览器(如Chrome、Firefox、Safari等)在实现Web标准方面存在差异,网页在不同浏览器上可能会呈现不一致的结果。因此,确保网页在各种浏览器上都能正确显示和运行,是提供良好用户体验、扩大受众范围以...
继续阅读 »

跨浏览器兼容性是前端开发中至关重要的概念。由于不同浏览器(如Chrome、Firefox、Safari等)在实现Web标准方面存在差异,网页在不同浏览器上可能会呈现不一致的结果。因此,确保网页在各种浏览器上都能正确显示和运行,是提供良好用户体验、扩大受众范围以及增强网站可访问性的关键。



兼容性测试工具和方法


自动化测试工具的使用 自动化测试工具能够帮助开发者更快速、高效地进行浏览器兼容性测试,以下是一些常用的自动化测试工具:


  1. Selenium:Selenium是一个流行的自动化测试框架,用于模拟用户在不同浏览器上的交互。它支持多种编程语言,并提供了丰富的API和工具,使开发者可以编写功能测试、回归测试和跨浏览器兼容性测试。
  2. TestCafe:TestCafe是一款基于JavaScript的自动化测试工具,用于跨浏览器测试。它不需要额外的插件或驱动程序,能够在真实的浏览器中运行测试,并支持多个浏览器和平台。
  3. Cypress:Cypress是另一个流行的自动化测试工具,专注于现代Web应用的端到端测试。它提供了简单易用的API,允许开发者在多个浏览器中运行测试,并具有强大的调试和交互功能。
  4. BrowserStack:BrowserStack是一个云端跨浏览器测试平台,提供了大量真实浏览器和移动设备进行测试。它允许开发者在不同浏览器上同时运行测试,以检测网页在不同环境中的兼容性问题。

手动测试方法和技巧 除了自动化测试工具,手动测试也是重要的一部分,特别是需要验证用户体验和视觉方面的兼容性。以下是几种常用的手动测试方法和技巧:


  1. 多浏览器测试:在不同浏览器(如Chrome、Firefox、Safari)上手动打开网页,并检查布局、样式和功能是否正常。特别关注元素的位置、尺寸、颜色和字体等。
  2. 响应式测试:使用浏览器的开发者工具或专门的响应式测试工具(如Responsive Design Mode)来模拟不同设备的屏幕尺寸和方向,确保网页在不同设备上呈现良好。
  3. 用户交互测试:模拟用户操作,例如点击按钮、填写表单、滚动页面和使用键盘导航,以确保网页在各种用户交互场景下都能正常运行。
  4. 边界条件测试:测试极端情况下的表现,例如超长文本、超大图片、无网络连接等。确保网页在异常情况下具备良好的鲁棒性和用户友好性。

设备和浏览器的兼容性测试 为了确保网页在不同设备和浏览器上的兼容性,以下是一些建议的测试方法:

  1. 设备兼容性测试:

    • 使用真实设备:将网页加载到不同类型的设备上进行测试,例如桌面电脑、笔记本电脑、平板电脑和智能手机等。
    • 使用模拟器和仿真器:利用模拟器或仿真器来模拟不同设备的环境,并进行测试。常用的模拟器包括Android Studio自带的模拟器和Xcode中的iOS模拟器。
  2. 浏览器兼容性测试:

    • 考虑常见浏览器:测试网页在主流浏览器(如Chrome、Firefox、Safari、Edge)的最新版本上的兼容性。
    • 旧版本支持:如果目标受众使用旧版浏览器,需要确保网页在这些浏览器上也能正常运行。可以使用Can I Use(caniuse.com)等工具来查找特定功能在不同浏览器上的兼容性。
  3. 定期更新测试设备和浏览器:随着时间的推移,新的设备和浏览器版本会发布,因此建议定期更新测试设备和浏览器,以保持兼容性测试的准确性。


常见的前端兼容性问题


我在下面列举了一些常见的兼容性问题,以及解决办法。

  • 浏览器兼容性问题:

    • 不同浏览器对CSS样式的解析差异:使用CSS预处理器(如Less、Sass)可以减少浏览器间的差异,并使用reset.css或normalize.css来重置默认样式。
    • JavaScript API的差异:使用polyfill或Shim库(如Babel、ES5-Shim)来填补不同浏览器之间JavaScript API的差异。
    1. 响应式布局兼容性问题:

      • 媒体查询失效:确保正确使用CSS媒体查询,并对不支持媒体查询的旧版浏览器提供备用样式。
      • 页面在不同设备上的布局错乱:使用弹性布局(Flexbox)、网格布局(Grid)和CSS框架(如Bootstrap)可以有效解决布局问题。
    2. 图片兼容性问题:

      • 不支持的图片格式:使用WebP、JPEG XR等现代图片格式,同时提供备用格式(如JPEG、PNG)以供不支持的浏览器使用。
      • Retina屏幕显示问题:使用高分辨率(@2x、@3x)图片,并通过CSS的background-size属性或HTML的srcset属性适应不同屏幕密度。
    3. 字体兼容性问题:

      • 不支持的字体格式:使用Web字体(如Google Fonts、Adobe Fonts)或@font-face规则,并提供备用字体格式以适应不同浏览器。
      • 字体加载延迟:使用字体加载器(如Typekit、Font Face Observer)来优化字体加载,确保页面内容在字体加载完成前有一致的显示。
    4. JavaScript兼容性问题:

      • 不支持的ES6+特性:使用Babel等工具将新版本的JavaScript代码转换为旧版本的代码,以兼容不支持最新特性的浏览器。
      • 缺乏对旧版浏览器的支持:根据目标用户群体使用的浏览器版本,选择合适的JavaScript库或Polyfill进行填充和修复。
    5. 表单兼容性问题:

      • 不同浏览器对表单元素样式的差异:使用CSS样式重置或规范化库来保证表单元素在各个浏览器上显示一致。
      • HTML5表单元素的不完全支持:使用JavaScript库(如Modernizr)来检测并补充HTML5表单元素的功能支持。
    6. Ajax和跨域请求问题:

      • 浏览器安全策略导致的Ajax跨域问题:通过设置CORS(跨域资源共享)或JSONP(仅适用于GET请求)来解决跨域请求问题。
      • IE浏览器对XMLHttpRequest的限制:使用自动检测并替代方案(如jQuery的AJAX方法),或考虑使用现代的XMLHttpRequest Level 2 API(如fetch)。

    CSS常见的兼容性问题


    CSS兼容性问题是在不同浏览器中,对CSS样式的解析和渲染会存在一些差异。以下是一些常见的CSS兼容性问题以及对应的解决方案:




    1. 盒模型:



      • 问题:不同浏览器对盒模型的解析方式存在差异,导致元素的宽度和高度计算结果不一致。

      • 解决方案:使用CSS盒模型进行标准化,通过设置box-sizing: border-box;来确保元素的宽度和高度包括边框和内边距。




    2. 浮动和清除浮动:



      • 问题:浮动元素可能导致父元素的塌陷问题(高度塌陷)以及与其他元素的重叠问题。

      • 解决方案:可以使用清除浮动的技巧,如在容器元素末尾添加一个空的<div style="clear: both;"></div>元素来清除浮动,或者使用clearfix类来清除浮动(如.clearfix:after { content: ""; display: table; clear: both; })。




    3. 绝对定位和相对定位:



      • 问题:绝对定位和相对定位的元素在不同浏览器中的表现可能存在差异,特别是在z轴上的堆叠顺序。

      • 解决方案:明确设置定位元素的position属性(position: relative;position: absolute;),并使用z-index属性来控制元素的堆叠顺序。




    4. 样式重置与规范化:



      • 问题:不同浏览器对默认样式的定义存在差异,导致页面在不同浏览器中显示效果不一致。

      • 解决方案:引入样式重置或规范化的CSS文件,如Eric Meyer's Reset CSS 或 Normalize.css。这些文件通过将默认样式置为一致的基准值,使页面在各个浏览器上的显示效果更加一致。




    5. 不同浏览器对CSS盒模型的解析差异:



      • 解决方案:使用box-sizing: border-box;样式来确保元素的宽度和高度包括内边距和边框。




    6. CSS选择器差异:



      • 解决方案:避免使用过于复杂的选择器,尽量使用普通的类名、ID或标签名进行选择。如果需要兼容旧版浏览器,请使用Polyfill或Shim库。




    7. 浮动元素引起的布局问题:



      • 解决方案:使用清除浮动(clear float)技术,例如在容器的末尾添加一个具有clear: both;样式的空元素或使用CSS伪类选择器(如:after)清除浮动。




    8. CSS3特性的兼容性问题:



      • 解决方案:使用CSS前缀来适应不同浏览器支持的CSS3属性和特效。例如,-webkit-适用于Chrome和Safari,-moz-适用于Firefox。




    除了以上问题,还可能存在字体、渐变、动画、弹性盒子布局等方面的兼容性问题。在实际开发中,可以使用CSS预处理器(如Less、Sass)来减少浏览器间的差异,并借助Autoprefixer等工具自动添加浏览器前缀,以确保在各种浏览器下的一致性。


    JavaScript常见的兼容性问题


    以下是几个常见的 JavaScript 兼容性问题及其解决方案:

  • 不支持ES6+语法和新的API:(上面有提到)

    • 问题:旧版本的浏览器可能不支持ES6+语法(如箭头函数、let和const等)和新的JavaScript API。
    • 解决方案:使用Babel等工具将ES6+代码转换为ES5语法,以便在旧版本浏览器中运行,并使用polyfill或shim库来提供缺失的JavaScript API支持。
    1. 缺乏对新JavaScript特性的支持:

      • 问题:某些浏览器可能不支持最新的JavaScript特性、方法或属性。
      • 解决方案:在编写代码时,可以检查特定的JavaScript特性是否受支持,然后使用适当的替代方法或实现回退方案。可以使用Can I use (caniuse.com) 等网站来查看浏览器对特定功能的支持情况。
    2. 事件处理程序兼容性问题:

      • 问题:不同浏览器对事件处理程序的绑定、参数传递和事件对象的访问方式存在差异。
      • 解决方案:使用跨浏览器的事件绑定方法(例如addEventListener),正确处理事件对象,并避免依赖事件对象的特定属性或方法。
    3. XMLHttpRequest兼容性问题:

      • 问题:旧版本的IE浏览器(< IE7)使用ActiveX对象而不是XMLHttpRequest。
      • 解决方案:检查浏览器是否支持原生的XMLHttpRequest对象,如果不支持,则使用ActiveX对象作为替代方案。
    4. JSON解析兼容性问题:

      • 问题:旧版本的浏览器可能不支持JSON.parse()JSON.stringify()方法。
      • 解决方案:使用json2.js等JSON解析库来提供对这些方法的支持,或者在必要时手动实现JSON的解析和序列化功能。
    5. DOM操作兼容性问题:

      • 问题:不同浏览器对DOM操作方法(如getElementByIdquerySelector等)的实现方式存在差异。
      • 解决方案:使用跨浏览器的DOM操作库(如jQuery、prototype.js)或使用feature detection技术来检测浏览器对特定DOM方法的支持,并根据情况使用不同的解决方案。
    6. 跨域请求限制:

      • 问题:浏览器的同源策略限制了通过JavaScript进行的跨域请求。
      • 解决方案:使用JSONP、CORS(跨源资源共享)、服务器代理或 WebSocket等技术来绕过跨域请求限制。

    总结


    跨浏览器兼容性是网站和应用程序开发中至关重要的一环。由于不同浏览器对CSS和JavaScript的解析和渲染存在差异,如果不考虑兼容性问题,可能会导致页面在不同浏览器上显示不正确、功能不正常甚至完全无法使用的情况。这将严重影响用户体验,并可能导致流失用户和损害品牌声誉。


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

    Nginx +Tomcat 负载均衡,动静分离集群

    1. 介绍 通常情况下,一个 Tomcat 站点由于可能出现单点故障及无法应付过多客户复杂多样的请求等情况,不能单独应用于生产环境下,所以我们需要一套更可靠的解决方案Nginx 是一款非常优秀的 http 服务器软件,它能够支持高达 5000 个并发...
    继续阅读 »

    1. 介绍


    • 通常情况下,一个 Tomcat 站点由于可能出现单点故障及无法应付过多客户复杂多样的请求等情况,不能单独应用于生产环境下,所以我们需要一套更可靠的解决方案
    • Nginx 是一款非常优秀的 http 服务器软件,它能够支持高达 5000 个并发连接数的响应,拥有强大的静态资源处理能力,运行稳定,并且内存、CPU 等系统资源消耗非常低
    • 目前很多大型网站都应用 Nginx 服务器作为后端网站的反向代理及负载均衡器,来提升整个站点的负载并发能力.

    小结

    • Nginx是一款非常优秀的HTTP服务器软件

    • 支持高达50 000个并发连接数的响应

    • 拥有强大的静态资源处理能力

    • 运行稳定

    • 内存,CPU等系统资源消耗非常低


    1.1. Tomcat重要目录


    1.2. 反向代理




     反向代理(Reverse Proxy)方式是指以代理服务器来接受 Internet 上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给 Internet 上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。


    反向代理是为服务端服务的,反向代理可以帮助服务器接收来自客户端的请求,帮助服务器做请求转发,负载均衡等。


    反向代理对服务端是透明的,对我们是非透明的,即我们并不知道自己访问的是代理服务器,而服务器知道反向代理在为他服务。


    反向代理的优势:


    • 隐藏真实服务器;
    • 负载均衡便于横向扩充后端动态服务;
    • 动静分离,提升系统健壮性。



    Nginx配置反向代理的主要参数


    • upstream服务池名{}
      • 配置后端服务器池,以提供响应数据
      1. proxy_pass http://服务池名
      • 配置将访问请求转发给后端服务器池的服务器处理


    1.3. 动静分离原理


    服务端接收来自客户端的请求中,既有静态资源也有动态资源,静态资源由Nginx提供服务,动态资源Nginx转发至后端


    服务端接收来自客户端的请求中,既有动态资源,也有静态资源。静态资源由ngixn提供服务。动态资源由nginx 转发到后端tomcat 服务器。


    静态页面一般 有html,htm,css 等路径, 动态页面则一般是jsp ,php 等路径。nginx 在站点的location 中 通过正则,或者 前缀,或者 后缀等方法匹配。当匹配到用户访问路径中有 jsp 时,则转发给后端的处理动态资源的web服务器处理。如果匹配到的路径中有 html 时,则nginx 自己处理。 



    1.4. Nginx 静态处理优势

    1. Nginx处理静态页面的效率远高于Tomcat的处理能力
    2. 若Tomcat的请求量为1000次,则Nginx的请求量为6000次
    3. Tomcat每秒的吞吐量为0.6M,Nginx的每秒吞吐量为3 .6M
    4. Nginx处理静态资源的能力是Tomcat处理的6倍

    1.5. 吞吐量 / 吞吐率


    吞吐量是指系统处理客户请求数量的总和,可以指网络上传输数据包的总和,也可以指业务中客户端与服务器交互数据量的总和。


    吞吐率是指单位时间内系统处理客户请求的数量,也就是单位时间内的吞吐量。可以从多个维度衡量吞吐率:①业务角度:单位时间(每秒)的请求数或页面数,即请求数 / 秒或页面数 / 秒;②网络角度:单位时间(每秒)网络中传输的数据包大小,即字节数 / 秒等;③系统角度,单位时间内服务器所承受的压力,即系统的负载能力。


    吞吐率(或吞吐量)是一种多维度量的性能指标,它与请求处理所消耗的 CPU、内存、IO 和网络带宽都强相关。


    2. Nginx+Tomcat负载均衡、动静分离




    1.部署Nginx 负载均衡器

    关闭防火墙
    systemctl stop firewalld
    setenforce 0

    安装
    yum -y install pcre-devel zlib-devel openssl-devel gcc gcc-c++ make

    useradd -M -s /sbin/nologin nginx

    cd /opt
    tar zxvf nginx-1.12.0.tar.gz -C /opt/

    cd nginx-1.12.0/
    ./configure \
    --prefix=/usr/local/nginx \
    --user=nginx \
    --group=nginx \
    --with-file-aio \ #启用文件修改支持
    --with-http_stub_status_module \ #启用状态统计
    --with-http_gzip_static_module \ #启用 gzip静态压缩
    --with-http_flv_module \ #启用 flv模块,提供对 flv 视频的伪流支持
    --with-http_ssl_module #启用 SSL模块,提供SSL加密功能
    --with-stream

    ./configure --prefix=/usr/local/nginx --user=nginx --group=nginx --with-file-aio --with-http_stub_status_module --with-http_gzip_static_module --with-http_flv_module --with-stream

    make && make install
    ln -s /usr/local/nginx/sbin/nginx /usr/local/sbin/

    vim /lib/systemd/system/nginx.service
    [Unit]
    Description=nginx
    After=network.target
    [Service]
    Type=forking
    PIDFile=/usr/local/nginx/logs/nginx.pid
    ExecStart=/usr/local/nginx/sbin/nginx
    ExecrReload=/bin/kill -s HUP $MAINPID
    ExecrStop=/bin/kill -s QUIT $MAINPID
    PrivateTmp=true
    [Install]
    WantedBy=multi-user.target

    chmod 754 /lib/systemd/system/nginx.service
    systemctl start nginx.service
    systemctl enable nginx.service



    2.部署2台Tomcat 应用服务器

    systemctl stop firewalld
    setenforce 0

    tar zxvf jdk-8u91-linux-x64.tar.gz -C /usr/local/

    vim /etc/profile
    export JAVA_HOME=/usr/local/jdk1.8.0_91
    export JRE_HOME=${JAVA_HOME}/jre
    export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib
    export PATH=${JAVA_HOME}/bin:${JRE_HOME}/bin:$PATH

    source /etc/profile

    tar zxvf apache-tomcat-8.5.16.tar.gz

    mv /opt/apache-tomcat-8.5.16/ /usr/local/tomcat

    /usr/local/tomcat/bin/shutdown.sh
    /usr/local/tomcat/bin/startup.sh

    netstat -ntap | grep 8080



    3.动静分离配置

    (1)Tomcat1 server 配置
    mkdir /usr/local/tomcat/webapps/test
    vim /usr/local/tomcat/webapps/test/index.jsp
    <%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
    <html>
    <head>
    <title>JSP test1 page</title> #指定为 test1 页面
    </head>
    <body>
    <% out.println("动态页面 1,http://www.test1.com");%>
    </body>
    </html>


    vim /usr/local/tomcat/conf/server.xml
    #由于主机名 name 配置都为 localhost,需要删除前面的 HOST 配置
    <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true" xmlValidation="false" xmlNamespaceAware="false">
    <Context docBase="/usr/local/tomcat/webapps/test" path="" reloadable="true">
    </Context>
    </Host>

    /usr/local/tomcat/bin/shutdown.sh
    /usr/local/tomcat/bin/startup.sh



    4 Nginx server 配置
    #准备静态页面和静态图片
    echo '<html><body><h1>这是静态页面</h1></body></html>' > /usr/local/nginx/html/index.html
    mkdir /usr/local/nginx/html/img
    cp /root/game.jpg /usr/local/nginx/html/img

    vim /usr/local/nginx/conf/nginx.conf
    ......
    http {
    ......
    #gzip on;

    #配置负载均衡的服务器列表,weight参数表示权重,权重越高,被分配到的概率越大
    upstream tomcat_server {
    server 192.168.85.60:8080 weight=1;
    server 192.168.85.70:8080 weight=1;
    server 192.168.85.80:8080 weight=1;
    }

    server {
    listen 80;
    server_name http://www.wa.com;

    charset utf-8;

    #access_log logs/host.access.log main;

    #配置Nginx处理动态页面请求,将 .jsp文件请求转发到Tomcat 服务器处理
    location ~ .*\.jsp$ {
    proxy_pass http://tomcat_server;
    #设置后端的Web服务器可以获取远程客户端的真实IP
    ##设定后端的Web服务器接收到的请求访问的主机名(域名或IP、端口),默认HOST的值为proxy_pass指令设置的主机名。如果反向代理服务器不重写该请求头的话,那么后端真实服务器在处理时会认为所有的请求都来自反向代理服务器,如果后端有防攻击策略的话,那么机器就被封掉了。
    proxy_set_header HOST $host;
    ##把$remote_addr赋值给X-Real-IP,来获取源IP
    proxy_set_header X-Real-IP $remote_addr;
    ##在nginx 作为代理服务器时,设置的IP列表,会把经过的机器ip,代理机器ip都记录下来
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    #配置Nginx处理静态图片请求
    location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|css)$ {
    root /usr/local/nginx/html/img;
    expires 10d;
    }

    location / {
    root html;
    index index.html index.htm;
    }
    ......
    }
    ......
    }





    3. Nginx 负载均衡模式:


    1. rr 负载均衡模式:
    2. 每个请求按时间顺序逐一分配到不同的后端服务器,如果超过了最大失败次数后(max_fails,默认1),在失效时间内(fail_timeout,默认10秒),该节点失效权重变为0,超过失效时间后,则恢复正常,或者全部节点都为down后,那么将所有节点都恢复为有效继续探测,一般来说rr可以根据权重来进行均匀分配。

      1. least_conn 最少连接:

      优先将客户端请求调度到当前连接最少的服务器。

      1. ip_hash 负载均衡模式:

      每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题,但是ip_hash会造成负载不均,有的服务请求接受多,有的服务请求接受少,所以不建议采用ip_hash模式,session 共享问题可用后端服务的 session 共享代替 nginx 的 ip_hash(使用后端服务器自身通过相关机制保持session同步)。

      1. fair(第三方)负载均衡模式:

      按后端服务器的响应时间来分配请求,响应时间短的优先分配。

      1. url_hash(第三方)负载均衡模式:

      基于用户请求的uri做hash。和ip_hash算法类似,是对每个请求按url的hash结果分配,使每个URL定向到同一个后端服务器,但是也会造成分配不均的问题,这种模式后端服务器为缓存时比较好。

    Nginx 四层代理配置:
    ./configure --with-stream

    和http同等级:所以一般只在http上面一段设置,
    stream {

    upstream appserver {
    server 192.168.80.100:8080 weight=1;
    server 192.168.80.101:8080 weight=1;
    server 192.168.80.101:8081 weight=1;
    }
    server {
    listen 8080;
    proxy_pass appserver;
    }
    }

    http {
    ......

    7层代理与4层代理区别


    总结

    • Nginx 支持哪些类型代理?
      1. 反向代理 代理服务端 7层方代理向代理 4层方向

      2. 正向代理 代理客户端 代理缓存

      3. 7层 基于 http,https,mail 等七层协议的反向代理

      • 使用场景: 动静分离

      • 特点:功能强大,但转发性能较4层偏低

      • 配置: 在http块里设置 upstream 后端服务池: 在seever块里用location匹配动态页面路径,使用 proxy_pass http://服务器池名 进行七层协议(http协议)转发

    http {
    upstream backersrver [weight= fail= ...]
    server IP1: PORT1 [weight= fail= ...]
    ......
    }

    server {
    listen 80;
    server_name XXX;
    location ~ 正则表达式 {
    proxy_pass http://backeserer;
    .......
    }
    }

    }



    1. 4层 基于 IP+(tcp或者udp)端口的代理
    • 使用场景: 负载均衡器 /负载调度器,做服务器集群的访问入口

    • 特点:只能根据IP+端口转发,但转发性能较好

    • 配置: 和http块同一层,一般在http块上面配置

    stream {
    upstream backerserver {
    server IP1:PORT1 [weight= fail= ...]
    server IP2:PORT2 [weight= fail= ...]
    .....
    }

    server {
    listen 80;
    server_name XXX;
    proxy_pass backerserver;
    }


    调度算法 6种


    轮询 加权轮询 最少/小连接 ip_hash fair url_hash


    会话保持
    ip_hash url_hash 可能会导致负载不均衡
    通过后端服务器的session共享来实现


    Nginx+Tomcat 动静分离

    • Nginx处理静态资源请求,Tomcat处理动态页面请求
    • 怎么实现动态分离

      • Nginx使用location去正则匹配用户的访问路径的前缀或者后缀去判断接受的请求是静态的还是动态的,静态资源请求在Nginx本地进行处理响应,动态页面通过反向代理转发给后端应用服务器

      怎么实现反向代理

      • 先在http块中使用upstream模块定义服务器组名,使用location匹配路径在用porxy_pass http://服务器组名 进行七层转发转发

      反向代理2种类型

      • 基于7层的协议http,HTTPS,mail代理
      • 基于4层的IP+(TCP/UDP)PORT的代理

      4层代理配置

      • 在http块同一层上面配置stream模块,在stream模块中配置upstream模块定义服务器组名和服务器列表,在stream模块中的server模块配置监听的IP:端口,主机名,porxy_pass 服务器组名


    Nginx调度策略/负载均衡模式算法6种

     轮询rr    加权轮询weight     最少/小连接least     ip_hash      fair      url_hash    
    配置在upstream 模块中

    Nginx如何实现会话保持

    ip_hash     url_hash    
    通过后端服务器session共享
    使用stick——cookie——insert基于cookie来判断
    通过后端服务器session共享实现

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

    软件开发者的自身修养

    一、工作任务 ① 会议主题: 一般在日常的工作会议中,要总结和反思:我这周干了什么、我下周打算干什么、我遇到了什么问题 ② 编程是需要持续投入精力和注意力的智力活动。注意力是稀缺资源,类似于魔力点数。如果用光了自己的注意力点数,必须花一个小时或者更多的时间做不...
    继续阅读 »

    一、工作任务


    会议主题:
    一般在日常的工作会议中,要总结和反思:我这周干了什么、我下周打算干什么、我遇到了什么问题


    编程是需要持续投入精力和注意力的智力活动。注意力是稀缺资源,类似于魔力点数。如果用光了自己的注意力点数,必须花一个小时或者更多的时间做不需要注意力的事情来补充它


    时间拆分:对于每天的工作时间可以参考番茄工作法策略进行时间拆分


    ④ 专业开发人员评估每个任务的优先级,排除个人的喜好和需要,按照真实紧急程度来执行任务


    小步快跑, 以防步履蹒跚


    ⑥ 专业开发人员会用心管理自己的时间和注意力


    需求预估是软件开发人员面对的最简单、也是最可怕的活动之一了


    ⑧ 业务方觉得预估就是承诺,开发方认为预估就是猜测。两者相差迥异


    ⑨ 需求承诺是必须做到的,是关于确定性的


    ⑩ 专业开发人员能够清楚区分预估和承诺。只有在确切知道可以完成的前提下,他们才会给出承诺


    ① 预估任务:达成共识,把大任务分成许多小任务,分开预估再加总,结果会比单独评估大任务要准确很多?这样做之所以能够提高准确度,是因为小任务的预估错误几乎可以忽略,不会对总得结果产生明显影响


    ② 对需要妥善对待的预估结果,专业开发人员会与团队的其他人协商,以取得共识


    二、测试开发


    ① 在工作中,有一种现象叫观察者效应,或者不确定原则。每次你向业务方展示一项功能,他们就获得了比之前更多的信息,这些新信息反过来又会影响他们对整个系统的看法


    ② 专业开发人员,也包括业务方必须确认,需求中没有任何不确定因素


    ③ 开发人员有责任把验收测试与系统联系起来,然后让这些测试通过


    ④ 请记住,身为专业开发人员,你的职责是协助团队开发出最棒的软件。也就是说,每个人都需要关心错误和疏忽,并协力改正


    单元测试是深入系统内部进行,调用特定类的方法;验收测试则是在系统外部,通常是在API或者UI级别进行


    QC:检验产品的质量,保证产品符合客户的需求,是产品质量检查者;QA:审计过程的质量,保证过程被正确执行,是过程质量审计者


    ⑦ 测试策略:单元测试、组件测试、集成测试、系统测试、探索式测试


    ⑧ 8小时其实非常短暂,只有480分钟,28800秒。身为专业的开发人员,你肯定希望能在这短暂的时间里尽可能高效的工作,取得尽可能多的成果


    ⑨ 再说一次,仔细管理自己的时间是你的责任


    三、孰能生巧


    调试时间和编码时间是一样昂贵的


    ② 管理延迟的诀窍,便是早期监测和保持透明。要根据目标定期衡量进度


    ③ 如果可怜的开发人员在压力之下最终屈服,同意尽力赶上截止日期,结局会十分悲惨。那些开发人员会开始抄近路,会额外加班加点工作,抱着创造奇迹的渺茫希望


    ④ 即使你的技能格外高超,也肯定能从另外一名程序员的思考与想法中获益


    测试代码之匹配于产品代码,就如抗体之匹配于抗原一样


    ⑥ 整洁的代码更易于理解,更易于修改,也更易于扩展。代码更简洁了,缺陷也更少了。整个代码库也会随之稳步改善,杜绝业界常见的放任代码劣化而视若不见的状况


    ⑦ 任何事情,只要想做得快,都离不开练习!无论是搏斗还是编程,速度都来源于练习!从练习中学到很多东西,深入了解解决问题的过程,进而掌握更多的方法,提升专业技能


    关于练习的职业道德职业程序员用自己的时间来练习。老板的职责不包括避免你的技术落伍,也不包括为你打造一份好看的履历


    ⑨ 东西画在纸上与真正做出来,是不一样的


    四、代码优化


    ① 好代码应该可扩展、易于维护、易于修改、读起来应该有散文的韵味……


    ② 在经济全球化时代,企业唯利是图,为提升股价而采用裁员、员工过劳和外包等方式,我遇到的这种缩减开发成本的手段,已经消解了高质量程序的存在价值和适宜了。只要一不小心,我们这些开发人员就可能会被要求、被指示或是被欺骗去花一半的时间写出两倍数量的代码


    ③ 客户所要的任何一项功能,一旦写起来,总是远比它开始时所说的要复杂许多


    ④ 很少有人会认真对待自己说的话,并且说到做到


    言必信,行必果


    ⑥ 如果感到疲劳或者心烦意乱,千万不要编码


    ⑦ 专业开发人员善于合理分配个人时间,以确保工作时间段中尽可能富有成效


    ⑧ 流态区:程序员在编写代码时会进入的一种意识高度专注但思维视野却会收拢到狭窄的状态


    创造性输出依赖于创造性输入


    五、团队开发


    ① 我认为自己是团队的一员,而非凌驾于团队之上


    ② 要勇于承担作为一名手艺人工程师所肩负的重大责任


    ③ 代码中难免会出现bug,但并不意味着你不用对它们负责;没人能写出完美的软件,但这并不表示你不用对不完美负责


    ④ 什么样的代码是有缺陷的呢?那些你没把握的代码都是


    ⑤ 我不是在建议,是在要求!你写的每一行代码都要测试,完毕!


    ⑥ 作为开发人员,你需要有个相对迅捷可靠的机制,以此判断所写的代码可否正常工作,并且不会干扰系统的其他部分


    编程是一种创造性活动,写代码是无中生有的创造过程,我们大胆地从混沌之中创建秩序


    ⑧ 他们各表异议相互说“不”,然后找到了双方都能接受的解决方案。他们的表现是专业的


    ⑨ 许诺“尝试”,意味着只要你再加把劲还是可以达成目标的


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

    Swift中的可选项Optional

    iOS
    为什么需要Optional Swift中引入了可选项(Optional)的概念是为了解决在代码中对于某些变量或常量可能为nil的情况进行处理,从而减少了程序中的不确定性,使得程序更加稳定和安全。 什么是Optional 在Swift中,可选项的类型是使用?来表...
    继续阅读 »

    为什么需要Optional


    Swift中引入了可选项(Optional)的概念是为了解决在代码中对于某些变量或常量可能为nil的情况进行处理,从而减少了程序中的不确定性,使得程序更加稳定和安全。


    什么是Optional


    在Swift中,可选项的类型是使用?来表示的,例如String?即为一个可选的字符串类型,表示这个变量或常量可能为nil。而对于不可选项,则直接使用相应类型的名称,例如String表示一个非可选的字符串类型。

    var str: String = nil
    var str1: String? = nil

    Optional实现原理


    Optional实际上是Swift语言中的一种枚举类型。在Swift中声明Optional类型时,编译器会自动将其转换成对应的枚举类型,例如:

    var optionalValue: Int? = 10
    // 等价于:
    enum Optional<Int> {
        case none
        case some(Int)
    }
    var optionalValue: Optional<Int> = .some(10)

    在上面的代码中,我们声明了一个Optional类型的变量optionalValue,并将其初始化为10。实际上,编译器会自动将其转换为对应的枚举类型,即Optional枚举类型的.some(Int),其中的Int就是我们所声明的可选类型的关联值。


    当我们在使用Optional类型的变量时,可以通过判断其枚举值是.none还是.some来确定它是否为nil。如果是.none,表示该Optional值为空;如果是.some,就可以通过访问其关联值获取具体的数值。


    Optional的源码实现为:

    @frozen public enum Optional<Wrapped> : ExpressibleByNilLiteral {
    case none
    case some(Wrapped)
    }

    • Optioanl其实是标准库里的一个enum类型
    • 用标准库实现语言特性的典型
    • Optional.none 就是nil
    • Optional.some 就是包装了实际的值
    • 泛型属性 unsafelyUnwrapped
    • 理论上我们可以直接调用unsafelyUnwrapped获取可选项的值

    Optional的解包方式


    1. 可选项绑定(Optional Binding)


    使用 if let 或者 guard let 语句来判断 Optional 变量是否有值,如果有值则解包,并将其赋值给一个非可选类型的变量。

    var optionalValue: Int? = 10
    // 可选项绑定
    if let value = optionalValue {
        print("Optional value is \(value)")
    } else {
        print("Optional value is nil")
    }

    可选项绑定语句有两个分支:if分支和else分支。如果 optionalValue 有值,if 分支就会被执行,unwrappedValue 就会被赋值为 optionalValue 的值。否则,执行 else 分支。


    2. 强制解包(Forced Unwrapping)


    使用!来获取一个不存在的可选值会导致运行错误,在使用!强制展开之前必须保证可选项中包含一个非nil的值

    var optionalValue: Int? = 10
    let nonOptionalValue = optionalValue!  // 解包optionalValue值
    print(nonOptionalValue)                // 输出:10

    需要注意的是,如果 Optional 类型的值为 nil,使用强制解包方式解包时,会导致运行时错误 (Runtime Error)。


    3. 隐式解包(Implicitly Unwrapped Optionals)


    在定义 Optional 类型变量时使用 ! 操作符,标明该变量可以被隐式解包。用于在一些情况下,我们可以确定该 Optional 变量绑定后不会为 nil,可以快捷的解包而不用每次都使用 ! 或者 if let 进行解包。

    var optionalValue: Int! = 10
    let nonOptionalValue = optionalValue // 隐式解包
    print(nonOptionalValue) // 输出:10

    需要注意的是,隐式解包的 Optional 如果 nil 的话,会导致 runtime error,所以使用隐式解包 Optional 需要确保其一直有值,否则还是需要检查其非 nil 后再操作。


    总的来说,我们应该尽量避免使用强制解包,而是通过可选项绑定来处理 Optional 类型的值,在需要使用隐式解包的情况下,也要确保其可靠性和稳定性,尽量减少出现运行时错误的概率。


    可选链(Optional Chaining)


    是一种在 Optional 类型值上进行操作的方式,可以将多个 Optional 值的处理放在一起,并在任何一个 Optional 值为 nil 的时刻停止处理。


    通过在 Optional 类型值后面跟上问号 ?,我们就可以使用可选链来访问该 Optional 对象的属性和方法。

    class Person {
        var name: String
        var father: Person?
        init(name: String, father: Person?) {
            self.name = name
            self.father = father
        }
    }
    let father = Person(name: "Father", father: nil)
    let son = Person(name: "Son", father: father)

    // 可选链调用属性
    if let fatherName = son.father?.name {
        print("Father's name is \(fatherName)") // 输出:Father's name is Father
    } else {
        print("Son without father")
    }

    // 可选链调用方法
    if let count = son.father?.name.count {
        print("Father's name has \(count) characters") // 输出:Father's name has 6 characters
    } else {
        print("Son without father")
    }

    在上面的代码中,我们定义了一个 Person 类,并初始化了一个包含父亲(father)的儿子(son)对象。其中,父亲对象的father属性为nil。我们使用问号 ? 来标记 father 对象为 Optional 类型,以避免访问 nil 对象时的运行时错误。


    需要注意的是,如果一个 Optional 类型的属性通过可选链调用后,返回值不是 Optional 类型,那么在可选链调用后,就不再需要加问号 ? 标记其为 Optional 类型了。

    class Person {
        var name: String
        var age: Int?
        init(name: String, age: Int?) {
            self.name = name
            self.age = age
        }
        func printInfo() {
            print("\(name), \(age ?? 0) years old")
        }
    }
    let person = Person(name: "Tom", age: nil)

    // 可选链调用方法后,返回值不再是 Optional 类型
    let succeed = person.printInfo() // 输出:Tom, 0 years old

    在上面的代码中,我们定义了一个 Person 类,并初始化了一个包含年龄(age)的人(person)对象。在可选链调用对象的方法——printInfo() 方法后,因为该方法返回值不是 Optional 类型,所以 returnedValue 就不再需要加问号 ? 标记其为 Optional 类型了。


    Optional 的嵌套


    将一个 Optional 类型的值作为另一个 Optional 类型的值的成员,形成嵌套的 Optional 类型。

    var optionalValue: Int? = 10
    var nestedOptionalValue: Int?? = optionalValue

    在上面的代码中,我们定义了一个 Optional 类型的变量 optionalValue,并将其赋值为整型变量 10。然后,我们将 optionalValue 赋值给了另一个 Optional 类型的变量 nestedOptionalValue,形成了一个嵌套的 Optional 类型。


    在处理嵌套的 Optional 类型时,我们需要特别小心,因为它们的使用很容易造成逻辑上的混淆和错误。为了解决这个问题,我们可以使用 Optional Binding 或者 ?? 操作符(空合并运算符)来降低 Optional 嵌套的复杂度。

    var optionalValue: Int? = 10
    var nestedOptionalValue: Int?? = optionalValue

    // 双重可选项绑定
    if let nestedValue = nestedOptionalValue, let value = nestedValue {
        print(value) // 输出:10
    } else {
        print("Optional is nil")
    }
    // 空合并运算符
    let nonOptionalValue = nestedOptionalValue ?? 0
    print(nonOptionalValue) // 输出:Optional(10)

    在上面的代码中,我们使用了双重可选项绑定来判断 nestedOptionalValue 是否可绑定,以及其嵌套的 Optional 值是否可绑定,并将该值赋值给变量 value,以避免 Optional 值的嵌套。另外,我们还可以使用 ?? 操作符(空合并运算符)来对嵌套的 Optional 值进行默认取值的操作。


    需要注意的是,虽然我们可以使用 ?? 操作符来降低 Optional 值的嵌套,但在具体的实际应用中,我们应该在设计时尽量避免 Optional 值的嵌套,以便代码的可读性和维护性。如果对于某个变量来说,它的值可能为空,我们可以考虑使用默认值或者定义一个默认值的 Optional 值来代替嵌套的 Optional 类型。


    学习 Swift,勿忘初心,方得始终。但要陷入困境时,也不要忘了最初的梦想和时代所需要的技能。


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

    为什么大家都看重学历?

    我刚刚看到一篇很好的年终总结《拒绝躺平,来自底层前端的2022总结》。这是一位高中辍学的掘友,他通过三年自考,最终获得了中山大学的学士学位。我看到后,很有同感,因此想讨论一下关于学历的问题。 我是专科学历,这一点,我在我的年终总结里坦白了:我其实是一名专科生...
    继续阅读 »

    我刚刚看到一篇很好的年终总结《拒绝躺平,来自底层前端的2022总结》。这是一位高中辍学的掘友,他通过三年自考,最终获得了中山大学的学士学位。我看到后,很有同感,因此想讨论一下关于学历的问题。



    我是专科学历,这一点,我在我的年终总结里坦白了:我其实是一名专科生,却在搞人工智能开发。我没有坦白的是,这只是我的第一学历。


    我在二线城市济南。尽管它非说自己是准一线国际大都市。


    12年前,我刚工作那会儿,我感觉学历无所谓。我甚至自傲地看不起高学历的人。因为我没有学历,只能认能力。同样的工作年限,在中小企业里,我能干得了他们干不了的事情。因此,我手下很多本科、很多研究生。


    这,当然是大错特错。不久,我认错了。也想明白了。


    有一次,高中微信群里,班长说,母校要统计从这里走出去的人才。


    人才的标准就是:硕士、博士


    开篇就是一个有争议的话题。


    不止学校,企业也是,对于学历、职称、证书等比较看重,认为那就是能力的象征。


    那么,学历和能力到底有没有关系?


    我不想挨骂,不去讨论这个。聪明的TF男孩,从不去引战,也不当靶子。


    不过我倒很想分析下为什么会出现这种现象。


    如果抛弃学历、证书,那么你认为什么样的人可以称为人才?


    道德素质高的?有专业技能的?开公司挣大钱的?


    对!这些人确实可以算人才。


    那么问题马上来了,一个人站在你面前,你怎么评判他道德素质高


    听别人说的!那么这个“别人”道德素质怎么样?是你亲眼看到的,那么其他人没有你的经历怎么办?你说录像了,他们怀疑是作秀怎么解释?


    再说证书吧,没有钢琴等级证书就不会弹钢琴吗?那么多民间大师,他们弹起来不比大师差。


    是吗?你能听出来C调和E调的区别吗?你又是怎么证明你懂声乐的?你不懂声乐,你又怎么断定,那个流浪汉,比音乐教授弹得还好的?


    发现了吧,没有了学历、证书,带来的问题,比错失人才这个问题更多


    当一个人站在我们面前,或者我们站在别人面前时,对方是无法直接判断你的能力的。


    即便可以通过交谈的方式来验证,但是你也不是哪一行都精通,也没有精力去外聘专家验证,另外还得验证专家靠不靠谱。


    因此,一旦引入学历、证书,最起码官方的资源帮我们验证过了


    就像选种子,个头大的就一定能长得好吗?不一定,也有很小的它就长得好。长得好不好是基因决定,但是你看基因的成本太高,只能看个头


    有人说,可惜了,写的一手好字,只因为没有个博士学历,错失了很多机会。


    其实,博士里面也有很多写字挺好的,中国就是不缺人。


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

    基于协议的业务模块路由管理

    iOS
    概述 这是一个关于业务模块与路由权限的管理方案,用于增强在模块化架构场景下,业务模块的健壮性。 通过对App生命周期的转发,来解除App入口与业务模块管理逻辑的耦合。通过协议来管理API路由,通过注册制实现API的服务发现。 业务模块 重新组织后,业务模块的...
    继续阅读 »

    概述


    这是一个关于业务模块与路由权限的管理方案,用于增强在模块化架构场景下,业务模块的健壮性。


    • 通过对App生命周期的转发,来解除App入口与业务模块管理逻辑的耦合。
    • 通过协议来管理API路由,通过注册制实现API的服务发现。

    业务模块




    重新组织后,业务模块的管理会变得松散,容易实现插拔复用。


    协议

    public protocol SpaceportModuleProtocol {
       var loaded: Bool { get set}
       /// 决定模块的加载顺序,数字越大,优先级越高
       /// - Returns: 默认优先级为1000
       static func modulePriority() -> Int
       /// 加载
       func loadModule()
       /// 卸载
       func unloadModule()

       /// UIApplicationDidFinishLaunching
       func applicationDidFinishLaunching(notification: Notification)
       /// UIApplicationWillResignActive
       func applicationWillResignActive(notification: Notification)
       /// UIApplicationDidBecomeActive
       func applicationDidBecomeActive(notification: Notification)
       /// UIApplicationDidEnterBackground
       func applicationDidEnterBackground(notification: Notification)
       /// UIApplicationWillEnterForeground
       func applicationWillEnterForeground(notification: Notification)
       /// UIApplicationWillTerminate
       func applicationWillTerminate(notification: Notification)
    }

    特性


    • 实现模块加载/卸载保护,模块只会加载/卸载一次。
    • 同一个模块的注册是替换制,新模块会替代旧模块。
    • 提供模块优先级配置,优先级高的模块会更早加载并响应Application的生命周期回调。

    最佳实践

    @UIApplicationMain
    class AppDelegate: UIResponder, UIApplicationDelegate {
       var window: UIWindow?
       func application(_ application: UIApplication, didFinishLaunchingWithOptionslaunchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
           setupModules()
    // ......
           return true
       }
     
       func setupModules() {
           var modules: [SpaceportModuleProtocol] = [
               LoggerModule(),             // 4000
               NetworkModule(),            // 3000
               FirebaseModule(),           // 2995
               RouterModule(),             // 2960
               DynamicLinkModule(),        // 2950
               UserEventRecordModule(),    // 2900
               AppConfigModule(),          // 2895
               MediaModule(),              // 2800
               AdModule(),                 // 2750
               PurchaseModule(),           // 2700
               AppearanceModule(),         // 2600
               AppstoreModule(),           // 2500
               MLModule()                  // 2500
           ]
    #if DEBUG
           modules.append(DebugModule())   // 2999
    #endif
           Spaceport.shared.registerModules(modules)
           Spaceport.shared.enableAllModules()
       }
    }

    协议路由


    协议路由


    通过路由的协议化管理,实现模块/组件之间通信的权限管理。


    • 服务方通过Router Manger注册API协议,可以根据场景提供不同的协议版本。
      • 业务方通过Router Manager发现并使用API协议。


    最佳实践


    实现API协议

    protocol ResultVCRouterAPI {
       @MainActor func vc(from: ResultVCFromType, project: Project) throws -> ResultVC
       @MainActor func vcFromPreview(serviceType: EnhanceServiceType, originalImage:UIImage, enhancedImage: UIImage) async throws -> ResultVC
    }

    class ResultVCRouter: ResultVCRouterAPI {
       @MainActor func vc(from: ResultVCFromType, project: Project) throws -> ResultVC {
           let vc = ResultVC()
           vc.modalPresentationStyle = .overCurrentContext
           try vc.vm.config(project: project)
           vc.vm.fromType = from
           return vc
       }

       @MainActor func vcFromPreview(serviceType: EnhanceServiceType, originalImage:UIImage, enhancedImage: UIImage) async throws -> ResultVC {
           let vc = ResultVC()
           vc.modalPresentationStyle = .overCurrentContext
           try await vc.vm.config(serviceType: serviceType, originalImage: originalImage,enhancedImage: enhancedImage)
           return vc
       }
    }

    注册API协议

    public class RouterManager: SpaceportRouterService {
       public static let shared = RouterManager()
       private override init() {}
       static func API<T>(_ key: TypeKey<T>) -> T? {
           return shared.getRouter(key)
       }
    }

    class RouterModule: SpaceportModuleProtocol {
       var loaded = false
       static func modulePriority() -> Int { return 2960 }
       func loadModule() {
         // 注册API
           RouterManager.shared.register(TypeKey(ResultVCRouterAPI.self), router:ResultVC())
       }
       func unloadModule() { }
    }

    使用协议

    // 通过 RouterManager 获取可用API
    guard let api = RouterManager.API(TypeKey(ResultVCRouterAPI.self)) else { return }
    let vc = try await api.vcFromPreview(serviceType: .colorize, originalImage:originalImage, enhancedImage: enhancedImage)
    self.present(vc, animated: false)

    总结


    我们的业务向模块化、组件化架构演化的过程中,逐步出现跨组件调用依赖嵌套,插拔困难等问题。


    通过抽象和简化,设计了这个方案,作为后续业务组件化的规范之一。通过剥离业务模块的生命周期,以及统一通信的方式,可以减缓业务增长带来的代码劣化问题。


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

    你的代码提交友好吗?

    Git 是目前世界上最先进的分布式版本控制系统,而针对Git代码提交,我们一般对于记录描述怎么操作的呢?当我是个初入行的码农时,我希望你管我怎么提交,一般就几个字,我功能完成即可,例如:git commit -m "调整修改" 当我开始变为资深码农,并且开始...
    继续阅读 »

    Git 是目前世界上最先进的分布式版本控制系统,而针对Git代码提交,我们一般对于记录描述怎么操作的呢?当我是个初入行的码农时,我希望你管我怎么提交,一般就几个字,我功能完成即可,例如:

    git commit -m "调整修改"

    当我开始变为资深码农,并且开始管理整个项目的代码质量以及规范时,看着年轻人提交的代码,你这都是个啥,啥叫调整修改。正如我们看着自己当年写的代码,充满怀疑,这竟然是我写的?


    玩笑归玩笑,规范化的提交真是一个好习惯,在工作中一份清晰简介规范的 Commit Message 能让后续代码审查、信息查找、版本回退都更加高效可靠。


    那么,快捷工具来了,commitizen/cz-cli


    Commit Message标准


    标准包含HeaderBodyFooter三个部分.

    (): 
    // ...

    // ...


    其中,Header 是必需的,Body 和 Footer 非必须。



    1. Header
      Header 部分只有一行,包括三个字段:type(必需)、scope(可选)、subject(必需)


    • type:用于说明类型。可分以下几种类型
    • scope:用于说明影响的范围,比如数据层、控制层、视图层等等。
    • subject:主题,简短描述。一行

    • Body

    对 subject 更详细的描述。


    • Footer

    主要是对于issue的关联。


    安装


    官方意思验证了Node.js 12,14,16版本的Node,而我在18上无任何问题。


    在本例中,我们将设置存储库以使用 AngularJS 的提交消息约定,也称为 traditional-changelog。还有其他适配器,例如cz-customizable


    • 首先,确保全局安装 Commitizen CLI 工具:
    npm install commitizen -g

    • 接下来,在项目中通过输入以下命令初始化以使用cz-conventional-changelog适配器:
    # npm
    commitizen init cz-conventional-changelog --save-dev --save-exact

    # yarn
    commitizen init cz-conventional-changelog --yarn --dev --exact

    # pnpm
    commitizen init cz-conventional-changelog --pnpm --save-dev --save-exact

    注意: 如果要在已经配置过的项目里面覆盖安装,则可以应用强制参数--force。还要了解其它详细信息,只需运行 。commitizen help


    上面的命令都干了什么呢:

    • 安装了cz-conventional-changelog适配器模块
    • 将下载配置保存到了package.json
    • 将适配器配置也写入了package.json 
    ...
    "config": {
    "commitizen": {
    "path": "cz-conventional-changelog"
    }
    }


    针对上面第三点适配器配置,你也可以建立一个.czrc文件,写入:

    {
    "path": "cz-conventional-changelog"
    }

    • 使用
    当我们提交代码时,就可以将`git commit`命令替换成`git cz`,或者别名`cz`,`git-cz`等等。


    [扩展]在项目中本地安装


    上边我们的操作其实可以看到,针对的是自己电脑本地项目,那么如果是多人项目,我们肯定希望每个人都能使用同样的规范,那么可以将命令集成到项目中,那么我们就不能全局安装了:

    npm install --save-dev commitizen

    在 npm 5.2+ 上,可以使用 npx 初始化适配器:

    npx commitizen init cz-conventional-changelog --save-dev --save-exact

    对于以前版本的 npm(< 5.2),使用项目内部命令即可:

    ./node_modules/.bin/commitizen init cz-conventional-changelog --save-dev --save-exact

    然后,您可以在package.json文件中添加命令:

      ...
    "scripts": {
    "commit": "cz"
    }

    这对所有项目使用人员比较统一化,如果他们想进行提交,他们需要做的就是运行npm run commit


    [扩展]通过git commit强制提交


    针对项目管理者,我们定了一个规范,但是没法指望别人会严格遵守,所以如何使用 git 挂钩和命令行选项将 Commitizen 合并到现有工作流中。这对项目维护者很有用,确保对不熟悉 Commitizen 的人的贡献强制执行正确的提交格式。


    首先确保我们是采用项目中本地集成安装了commitizen,然后可以选取以下两种方式之一.


    方法一:传统的 git hooks

    针对自己使用,修改以下文件:.git/hooks/prepare-commit-msg

    #!/bin/bash
    exec < /dev/tty && node_modules/.bin/cz --hook || true

    注意: 如果prepare-commit-msg文件是新建的,需要执行权限chmod 777 .git/hooks/prepare-commit-msg,否则:




    方法二:husky

    对于多用户,我们也可以借助husky来统一提交:

    1. 安装husky
    npm install husky -D

    2. 初始化husky配置
    npm pkg set scripts.prepare="husky install"
    npm run prepare

    3. 添加脚本,我们这边针对提交触发
    npx husky add .husky/prepare-commit-msg "exec < /dev/tty && node_modules/.bin/cz --hook || true"

    疑问: commitizen文档对于husky推荐利用package.json添加husky配置,但是我这边不起作用,后边研究一下原因。


    注意: 一定慎重同时配置husky和本地git hooks,会重复执行。


    全局安装


    我们开发过程中,其实针对每个项目初始化适配器,不太友好,其实还可以全局配置。


    全局安装commitizencz-conventional-changelog

    npm install -g commitizen

    npm install -g cz-conventional-changelog

    用户目录下创建配置文件(Mac下,Linux下同理):

    echo '{ "path": "cz-conventional-changelog" }' > ~/.czrc

    项目和全局都配置了适配器,将先以本地为主。




    VS CODE


    vs code中可以使用git-commit-plugin 插件,这里不过多扩展了。


    访问原文


    你的代码提交友好吗? | DLLCNX的博客


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

    🔥面试官想听的离职原因清单

    大家好,我是沐华。今天聊一个面试的问题 由于面试官还要摸鱼刷沸点,不想花那么多时间一个个面,所以采用群面的方式,就出现了上图这样的场景 交锋 面试官:方便说下离职原因吗? 掘友1:不方便 掘友2:在前公司长期工作量有些太大了,我自己身体上也出现了一些信号,有段...
    继续阅读 »

    大家好,我是沐华。今天聊一个面试的问题


    由于面试官还要摸鱼刷沸点,不想花那么多时间一个个面,所以采用群面的方式,就出现了上图这样的场景


    交锋


    面试官:方便说下离职原因吗?


    掘友1:不方便


    掘友2:在前公司长期工作量有些太大了,我自己身体上也出现了一些信号,有段时间都完全睡不着觉,所以需要切换一个相对来讲工作量符合我个人要求的,比如说周末可以双休这样一个情况,这个对我现在的选择来讲还蛮重要的


    掘友3:本来已经定好的前端负责人(组长),被关系户顶掉了,我需要一个相对公平的竞争环境,所以打算换个公司


    掘友4:实不相瞒,一年前我投过咱们公司(或者面试过但没过),一年了,你知道我这一年是怎么过的吗,因为当时几轮面试都很顺利的,结果却回复我说有更合适的人选了,我难受了很久,因为真的很想进入咱们公司,于是我奋发图强,每天熬夜学习到凌晨两点半,如今学有所成,所以又来了


    掘友5:团队差不多解散了吧,领导层变动,没多久时间原团队基本都走了,相当于解散了吧,现在剩几个关系户,干的不开心


    掘友6:公司要开发一些灰产(买马/赌球/时时彩之类的),老员工都不愿意搞,就都要我来做,我堂堂掘友6可是与赌毒不共戴天的人,怎么会干这种事(就是害怕坐牢),就辞职了(这是位入职时间不长的掘友)


    掘友7:公司业务调整,然后突然让我去外地分公司驻场/让我去搞 flutter(原本是前后端),虽然是个好机会可还是很难受,而且与我的职业发展规划不符,所以不想浪费时间,就第一时间辞职了


    掘友8:前东家对我挺好的,工作也得心应手(进入舒适圈了),只是我不想一直呆在舒适圈,我不是那种混日子的人,所以希望跳出来,找一份更有挑战性,更有成就感的工作,贵公司的岗位很符合我的预期


    掘友9:公司最近经营不理想:1.不给交社保/公积金了,2.拖欠几个月工资了,好不容易攒的奶粉钱都花完啦(虽然还单身,可也是有想法的),为了生活,这不办法呀,3.公司倒闭了,现在十几个同事都在找工作,咱们这还需要前后端、产品、设计、测试吗,我可以内推


    掘友10:您可能也知道现在各行各业行情都不太好,很多公司都裁撤了部分业务,前公司前几年疫情时就已是踏雪而行了,现在在新业务的选择上就决定裁撤掉原来的业务线,我也是其中一员,虽然很遗憾但也能接受吧,在前公司这两年也是学到了很多


    掘友11:我其实挺感谢上家公司的,各方面都挺好的,也给了我很好的成长空间,但是也三年多时间了,我的薪资也没涨过,相信你也知道,其实我现在的薪资能够值得更好的,嗯被认可


    掘友12:克扣工资,领导说以后生产环境上出现一次 bug 就要扣所有参与的人工资,说真的,每天加班加点的干,我们都没问题,可结果就被这样对待,被连带扣了几次之后心里真的很难受


    掘友13:回老家发展咯/对象在这边咯,因为准备结婚了,之后一直在这边发展定居了(这种换城市的回答要给出准备结婚或定居发展这样的原因,不然谈个对象就换城市会显得不靠谱);如果是小城市换大城市,可以直接说是为了高薪来的,因为家里买房了生孩子了啥的经济压力大,顾家其实是能体现稳定的,也给砍价打个预防针


    掘友14:(有断层,面试时间和上次离职时间相隔时间有点长,有两三个月左右的,如果真实情况是家里或者生病啥的直说就好,如果只是找了几个月工作没找到,就要组织下语言了),由于长时间加班的原因,身体受到了影响每天睡不好觉,那段时间一直不在状态,没法好好投入工作,就想休息一段时间,为避免给公司造成不好的影响,所以辞职了。当时领导坚持批我几天假,我自己也不知道具体多久能恢复过来,毕竟那种种状态也不是一天两天了,还是坚持让领导批我的辞职了,然后这段时间我去了哪哪哪,身体已经调整过来了,可以全身心投入工作了,不过现在找工作希望是周末可以双休这样一个情况,这个对我现在的选择来讲还蛮重要的


    (如果断层有一年的左右的,我有一段经历可以给大家参考下)我当时没工作了,家里投了点钱让我和一个亲戚合伙搞了点生意,结果赚了点钱,但那个亲戚喜欢赌钱,被他拿去赌了,输光了,于是我撤出来了


    沐华:就是觉得翅膀硬了,想出去看看(其实这是我入职现公司面试时说的离职原因,当时面试官听着就笑了)


    第一轮回答结束!





    心法


    离职原因真实情况绝大多数情况无非就几种:钱少了,不开心了,被裁了。


    大家都差不多的,面试官心里也知道,可这能直说吗?


    直说也不是不行,但是要注意表达方式,回答时有些场面话/润色一下还是需要的,去掉负面的字句,目的是让人家听的人舒服一点而已,毕竟谁也不喜欢一个陌生人来向自己诉苦抱怨,发牢骚吧,谁都希望认识正能量/积极向上的人吧


    所以回答的关键在于:



    1. 不能是自己的能力、懒惰、不稳定等原因,或可能影响团队的性格缺陷

    2. 不要和现任说前任的不好,除非客观原因没办法,但也要知道即便是前公司真实存在的问题,hr 并不了解真实情况,还是会对是前公司有问题,还是你有问题持怀疑态度的


    就像分手原因,对别人说出来时不能显得自己很绝情,又不能让自己很跌份,而且很忌讳疯狂抹黑前任


    公司想降低用人风险,看我们离职的原因在这里会不会再发生,所以我们回答中应该体现:稳定性、有想法、积极向上、找工作看重什么、想得到什么、有规划不是盲目找的....


    忌讳:升职机会渺茫、个人发展遇到瓶颈、人际关系复杂受人排挤、勾心斗角氛围差...这样的回答会让人质疑是前公司的问题,还是你的能力/情商有问题?


    那么,你觉得最好的答案是什么呢,如果你是面试官,会选谁进入下一轮?


    同时期待掘友们在评论区补充哦


    作者:沐华
    来源:juejin.cn/post/7225432788044267575
    收起阅读 »

    程序员的快乐与苦恼

    “我们朝九晚五上班下班,就是为了有朝一日去探索宇宙的” —— 宇宙探索编辑部 随着大环境的下行,互联网行业也受到一定的冲击,哀鸿遍野。 笔者也没有幸免,培养起来的人马陆续被优化,留下一丢光杆司令,我也回到的业务一线,心里很不是滋味。留下来的人,也不知道这艘船...
    继续阅读 »

    我们朝九晚五上班下班,就是为了有朝一日去探索宇宙的
    —— 宇宙探索编辑部



    随着大环境的下行,互联网行业也受到一定的冲击,哀鸿遍野。


    笔者也没有幸免,培养起来的人马陆续被优化,留下一丢光杆司令,我也回到的业务一线,心里很不是滋味。留下来的人,也不知道这艘船什么时候会沉没… 为了活命而拼命挣扎(内卷)


    负面情绪和焦虑不停侵扰,以至于怀疑,当初选的这条路是不是正确的。


    捡起买了多年,但是一直没看的《人月神话》, 开篇就讲了程序员这个职业的乐趣和苦恼,颇有共鸣,所以拿出来给大家分享


    不管过去多少年,不管你的程序载体是纸带、还是 JavaScript,不管程序跑在高对比(high contract)的终端、还是 iPhone,程序员的快乐和烦恼并没有变化。


    尽管国内软件行业看起来不是那么健康。我相信很多人真正热爱的是编程,而不仅仅是一份工作,就是那种纯粹的热爱。你有没有:



    • 为了修改一个 Bug,茶饭不思

    • 为了一个 idea,可以凌晨爬起来,决战到天亮

    • 我们享受没有人打扰的午后

    • 梦想着参与到一个伟大的开源项目

    • 有强烈的分享欲,希望我们的作品可以帮助到更多人, 希望能得到用户的反馈,即使是一个点赞







    我们的快乐



    《人月神话》:


    首先,这种快乐是一种创建事物的纯粹快乐。如同小孩在玩泥巴时感到快乐一样,成年人喜欢创建事物,特别是自己进行设计。我想这种快乐是上帝创造世界的折射,一种呈现在每片独特的、崭新的树叶和雪花上的喜悦。


    其次,这种快乐来自于开发对他人有用的东西。内心深处,我们期望我们的劳动成果能够被他人使用,并能对他们有所帮助。从这一角度而言,这同小孩用粘士为“爸爸的办公室”捏制铅笔盒没有任何本质的区别。


    第三,快乐来自于整个过程体现出的一股强大的魅力——将相互啮合的零部件组装在一起,看到它们以精妙的方式运行着,并收到了预期的效果。比起弹球游戏机或自动电唱机所具有的迷人魅力,程序化的计算机毫不逊色。


    第四,这种快乐是持续学习的快乐,它来自于这项工作的非重复特性。人们所面临的问题总有这样那样的不同,因而解决问题的人可以从中学习新的事物,有时是实践上的,有时是理论上的,或者兼而有之。


    最后,这种快乐还来自于在易于驾驭的介质上工作。程序员,就像诗人一样,几乎仅仅在单纯的思考中工作。程序员凭空地运用自己的想象,来建造自己的“城堡”。很少有创造介质如此灵活,如此易于精炼和重建,如此容易实现概念上的设想(不过我们将会看到,容易驾驭的特性也有它自己的问题)。


    然而程序毕竞同诗歌不同,它是实实在在的东西;它可以移动和运行,能独立产生可见的输出;它能打印结果,绘制图形,发出声音,移动支架。神话和传说中的魔术在我们的时代已变成现实。在键盘上键入正确的咒语,屏幕会活动、变幻,显示出前所未有的也不可能存在的事物。





    编程就是一种纯粹创造的快乐,而且它的成本很低,我们只需要一台电脑,一个趁手的编辑器,一段不被人打扰的整块时间,然后进入心流状态,脑海中的想法转换成屏幕上闪烁的字符。
    这是多巴胺带给我们的快乐。


    飞机引擎






    我们也有「机械崇拜」,软件不亚于传统的机械的复杂构造。 它远比外界想象的要复杂和苛刻,而我们享受将无数零部件有机组合起来,点击——成功运行的快感。


    我们享受复杂的问题,被抽象、拆解成一个个简单的问题, 认真描绘分层的弧线以及每个模块轮廓,谨慎设计它的每个锯齿和接口。


    我们崇尚有序,赞赏清晰的边界, 为的就是我们创造的世界能够稳定发展。




    我们认为懒惰是我们的优点,我们也崇拜自动化,享受我们数据通过我们建设的管道在不同模块、系统或者机器中传递和加工;享受程序像多米诺骨牌一样,自动构建、测试、发布、部署、分发到每个用户的手中,优雅地跑起来。


    因为懒,我们时常追求创造出能够取代自己的工具,让我们能腾出时间在新的世界探索。比如可以制造出我们的 Moss,帮我们治理让每个程序的生命周期,让它们优雅地死去又重生。




    我们是一群乐于分享和学习的群体,有繁荣的技术社区、各种技术大会、技术群…


    不管是分享还是编程本身,其实都是希望我们的作品能被其他人用到,能产生价值:



    • 我们都有开源梦,多少人梦想着能参与那些广为人知开源项目。很少有哪个行业,有这么一群人, 能够自我组织,用爱发电、完全透明地做出一个个伟大的作品。

    • 我们总会怀揣着乐观的设想,基于这种设想,我们会趋向打造更完美的作品,想象未来各种高并发、极端的场景,我们的程序能够游刃有余。

    • 我们总是不满足于现有的东西,乐于不停地改进,造出更多的轮子,甚至不惜代价推翻重来

    • 我们更会懊恼,自己投入大量精力的项目,无人问津,甚至胎死腹中。




    看着它们,从简单到繁杂,这是一种迭代的快乐。








    我们的苦恼



    《人月神话》
    然而这个过程并不全都是快乐的。我们只有事先了解一些编程固有的苦恼,这样,当它们真的出现时,才能更加坦然地面对。


    首先,苦恼来自追求完美。因为计算机是以这样的方式来变戏法的: 如果咒语中的一个字符、一个停顿,没有与正确的形式一致,魔术就不会出现(现实中,很少有人类活动会要求如此完美,所以人类对它本来就不习惯)。实际上,我认为,学习编程最困难的部分,是将做事的方式向追求完美的方向调整"。




    其次, 苦恼来自由他人来设定目标、供给资源和提供信息。编程人员很少能控制工作环境和工作目标。用管理的术语来说,个人的权威和他所承担的责任是不相配的。不过,似乎在所有的领域中,对要完成的工作,很少能提供与责任相一致的正式权威。而现实情况中,实际(相对于形式)的权威来自于每次任务的完成。


    对于系统编程人员而言,对其他人的依赖是一件非常痛苦的事情。他依靠其他人的程序,而这些程序往往设计得并不合理、实现拙劣、发布不完整(没有源代码或测试用例)或者文档记录得很糟。所以,系统编程人员不得不花费时间去研究和修改,而它们在理想情况下本应该是可拿的、完整的。




    下一个苦恼 —— 概念性设计是有趣的,但寻找琐碎的bug却是一项重复性的活动。伴随着创造性活动的,往往是枯燥沉闷的时间和艰苦的劳动。程序编制工作也不例外。




    另外,人们发现调试和查错往往是线性收敛的,或者更糟糕的是,具有二次方的复杂度。结果,测试一拖再拖,寻找最后一个错误比第一个错误将花费更多的时间。




    最后一个苦恼,有时也是一种无奈 —— 当投入了大量辛苦的劳动,产品在即将完成或者终于完成的时候,却己显得陈旧过时。可能是同事和竞争对手己在追逐新的、更好的构思;也许替代方案不仅仅是在构思,而且己经在安排了。





    前阵子读到了 @doodlewind全职开源,出海创业:我的 2022,说的是他 all in 去做 AFFiNE 。我眼里只有羡慕啊,能够找到 all in 的事业…






    这些年 OKR 也很火,我们公司也跟风了一年; 后面又回到了 KPI,轰轰烈烈搞全员KPI, 抓着每个人, 要定自己的全年KPI; 再后来裁员,KPI 就不再提起了…


    这三个阶段的演变很有意思,第一个阶段,期望通过 OKR 上下打通,将目标捆在一起,让团队自己驱动自己。实际上实施起来很难,让团队和个人自我驱动起来并不是一件容易的事情,虽然用的是 OKR,但内核还是 KPI,或者说 OKR 变成了领导的 OKR。


    后面就变成了 KPI, 限定团队要承担多少销售额,交付多少项目;


    再后来 KPI 都没有了,换成要求每个人设定自己工作日历,不能空转,哪里项目缺资源,就调配到哪里,彻底沦为了人矿…




    能让我们 all in 的事情,首先得是我们认同的事情,其次我们能在这件事情上深度参与和发挥价值,并获得预期的回报。这才能实现「自我驱动」


    对于大部分人来说,很少有这种工作机会,唯一值得 all in的,恐怕就只有自己了。






    所以程序员的苦恼很多,虽然编程是一个创造性的工作,但是我们的工作是由其他人来设定目标和提供资源的。


    也就是说我们只不过是困在敏捷循环里面的一颗螺丝钉,每天在早会上机械复读着:昨天干了什么,今天要干什么。


    企业总会想法设法量化我们的工作,最好是像流水线一样透明、可预测。




    培训机构四个月就能将高中生打造成可以上岗敲代码的程序员。我们这个行业已经不存在我们想象中高门槛。


    程序员可能就是新时代的蓝领工人,如果我们的工作是重复的、可预见的,那本质上就没什么区别了。






    追求完美是好事,也是坏事。苛刻的编译器会提高开发的门槛,但同样可以降低我们犯错的概率。


    计算机几乎不会犯错的,只是我们不懂它,而人经常会犯错。相比苛刻的计算机,人更加可怕:



    • 应付领导或产品拍脑袋的需求

    • 接手屎山代码

    • 浪费时间的会议

    • 狼性文化











    还有一个苦恼是技术的发展实在太快了,时尚的项目生命周期太短,而程序员又是一群喜新厌旧的群体。


    比如在前端,可能两三年前的项目就可以被定义为”老古董”了,上下文切换到这种项目会比较痛苦。不幸的是,这些老古董可能会因为某些程序员的偏见,出现破窗效应,慢慢沦为屎山。


    我们虽然苦恼于项目的腐败,而大多数情况我们也是推手。




    我们还有很多苦恼:



    • 35 岁危机,继续做技术还是转管理

    • 面试的八股文

    • 内卷

    • 被 AI 取代







    对于读者来说,是快乐多一些呢?还是苦恼多一些呢?


    作者:荒山
    来源:juejin.cn/post/7248431478240329789
    收起阅读 »

    程序员转行做运营,降薪降得心甘情愿

    自2019年末新冠疫情爆发以来,近三年的就业形势一直不太乐观,大厂裁员的消息接踵而至。身边的朋友都在感慨:现阶段能保住工作就不错了,新工作就算了。 但,就是在这样严峻的大环境下,我的前同事不三不仅跳槽还转岗,1年的转行之路,经受了各种磨难。通过小摹的热情邀请,...
    继续阅读 »

    自2019年末新冠疫情爆发以来,近三年的就业形势一直不太乐观,大厂裁员的消息接踵而至。身边的朋友都在感慨:现阶段能保住工作就不错了,新工作就算了。


    但,就是在这样严峻的大环境下,我的前同事不三不仅跳槽还转岗,1年的转行之路,经受了各种磨难。通过小摹的热情邀请,和不三聊了聊程序员转运营过程中的经验与心得。


    小摹把这份干货分享出来,希望能为每一位即将转行的伙伴提供动力支撑,也能给其他岗位的朋友新增一些不同视角的思考。


    试用期差点被劝退


    小摹:从事前端四年,是什么让你下定决心转行?


    不三:后续有创业的打算,所以希望自己在了解产品研发的基础上,也多了解一下市场,为自己创业做准备吧。


    小摹:你做的是哪方面的运营呢?这一年的感触如何?


    不三:运营岗位细分很多:新媒体运营、产品运营、用户运营、活动运营、市场推广等,我所从事的是内容运营和用户运营。


    公司是SaaS通信云服务提供商,对于之前从未接触过这方面工作的我而言,门槛比较高。为了能尽快熟悉产品业务,也能让我更了解用户,为后续用户运营和内容运营打基础,领导安排我前期先接触和客户相关的工作。


    我试用期大部分的工作都涉及到和用户打交道,他们总会反馈给我们各种产品的需求和bug,我基本都冲在第一线安抚用户。Bug提交给开发后或许还能尽快修复,而需求反馈过去后,只能等到那句再熟悉不过的话“等排期吧”。


    图片


    刚做运营的前三个月,提给开发的需求大多都被驳回了,要么做出来的东西无法达到预期。那段时间,每天上班心态濒临崩溃,颇有打道回府之意。


    转正之前,领导找我谈了一次话,让我醍醐灌顶:


    运营身为提需求大户,你连需求都没规划好,想一出是一出,产品开发为啥会帮你做?


    你之前是前端,设身处地的想,是不是非常反感产品或运营给你提莫名其妙的需求?不注重用户体验、忽略了产品的长远发展,即便当下你的KPI完成了,你有获得真正的成长,产品有迭代得更好吗?


    在和领导沟通的过程中慢慢意识到,我把自己的位置摆错了,即使运营是结果驱动,但我直面用户,所以我必须要学会洞察用户的心理,重视产品的长远发展,这样才能让我有所进度。


    跟领导聊完之后,我便开始调整了工作状态和节奏,明白了自己的不足,接下来就是有目标、有计划的解决问题。


    回到岗位后,我梳理了公司的业务方向,写好MRD(市场需求报告),重新制定了我的运营策略,提交给了领导。


    三天后,人事找到我:我通过了试用期,成功转正了。


    图片


    我很感谢我的领导,尽管试用期我做得很烂,但他仍然愿意给我机会,让我转正,继续工作。现在回过头看这一年,试用期阶段很痛苦,找不到工作的方向,但后来越来越熟悉了解后,也能更快上手了。


    小摹:你认为一名优秀的运营要具备什么样的特质?


    不三:现在的我只能说刚刚入门,我发现身边的运营大佬身上有以下特点,我希望自己能尽快向他靠拢。




    • 用户体感:所有的产品研发出来后,面向对象一定是用户,那么产品的使用体验、页面设计、活动机制、规则设定是否都能满足用户的胃口。




    如果只是冲着所谓的KPI目标,而忽略了用户体验,或许你会收获万人骂的情况。


    例如,随时随地朋友圈砍一刀的拼夕夕。




    • 把控热点能力:无论做什么方向的运营,都逃不了蹭热点,你可以说蹭热点low,但不可否认它会给自己和产品带来新机遇。




    例如,写一篇文章蹭了热点之后,爆的几率更大;疫情刚出现时,异地办公、社区团购也随之应运而生。




    • 产品思维:互联网运营和产品经理的联系是非常紧密的,所以在推广的过程中,需要和产品部门多多碰撞。这样不仅能收获创意灵感,还能学到不少的产品思维。




    在需求迭代时,应该站在更高的层次思考问题,一味给产品做加法,根本行不通。




    • 数据思维:运营以结果为导向,从数据中发现不足,从数据中发现增长点,弥补缺陷,让增长幅度更大。程序员比较有优势,可以写SQL导数据,但拿到数据只是第一步,还要懂得分析才行。




    • 抵御公关风险:例如我们在做活动时,我们要提前考虑活动的风险有哪些,如何积极应对,当有别有用心的人利用规则薅羊毛时,也应该有相应的解决方案。




    图片


    这段简单且干货的采访随着烧烤啤酒的上桌步入了尾声。最后不三给我说到:


    一年前我调整了自己的职业方向,从前端步入运营,苦涩欢笑并存,有时看着达到目标很是激动,有时苦于KPI的折磨。一年间,我经历了人生的成长,思想也更加成熟。但我还没有达到最终目的地,现在的一切只是为了以后的创业蓄力。我不想一辈子为别人打工,也想为自己活一次。


    图片


    ===


    后记


    小摹见过太多转行失败的案例,所以很为不三感到高兴,不仅仅是为他的转行成功,更多的是他坚定人生的方向,并为之做出了各种努力而高兴。


    给大家分享这段采访经历,是希望大家能尽早对自己的职业生涯有所规划,有了目标后,再细分到某一阶段,这样工作起来积极性也会更高。停止摆烂,对自己负责!


    人生之难,一山又一山,愿你我共赴远山。


    设计1+2,摹客就够了!


    作者:摹客
    来源:juejin.cn/post/7158734145575714853
    收起阅读 »

    因为数据库与项目经理引发的一点小争执,保存留念

    前言        作为刚步入社会的小同学来说,对代码有热情是很好,但是也极其嫌麻烦,明明都做完了还要被要求一遍又一遍的更改,相信大多数人都是嫌麻烦,然后就是两人之间的打情骂俏。 项目经理:你改一改嘛🤤 我:哎呀,好麻烦啊,不给你写了一个么😭 项目经理:你那...
    继续阅读 »

    前言


           作为刚步入社会的小同学来说,对代码有热情是很好,但是也极其嫌麻烦,明明都做完了还要被要求一遍又一遍的更改,相信大多数人都是嫌麻烦,然后就是两人之间的打情骂俏。



    项目经理:你改一改嘛🤤


    我:哎呀,好麻烦啊,不给你写了一个么😭


    项目经理:你那个我数据库不能维护啊,快改改,乖o(^@^)o


    我:😣我不我不,为啥不能维护,我不理解


    项目经理:你去试试😣球球了,你去试试😭


    (当然没我写的这么肉麻嘞🤣,如有雷同,纯属巧合)





           好了,数据库维护,他从前端页面进入后向页面输入肯定要调用sql,问题来了,以下这种形式sql可以是实现随意添加么(没有主键)
    在这里插入图片描述
           我写python的第一反应:这有啥问题么,数据库我会个简单的增删改查,但是我感觉应该有函数可以直接往后加吧(很chun的想法,两种不同的语言怎么可能会一样),于是乎我开始了,漫漫搜索之路(因为回家连不上内网mysql,以下用Oracle代替)


    使用insert函数



    • 数据库基本增加操作:insert into table_name (column1, column2, ...) VALUES (value1, value2, ...),这里直接跳过全字段添加,选取单字段添加,本以为他会如下图:


    INSERT into wang.gjc_data (a1) values ('a');

    在这里插入图片描述



    • 实际上如下图(哪怕是选取单字段也是默认增加一行):
      在这里插入图片描述



           我确实懵了,以前从来没有想过这件事,因为从数据库读取下来很多时候数据第一步就是先转置,感觉有点麻烦吧,因为转置完会出现很多意料之外的情况,但是人家数据库就是这么存的,现在轮到自己建数据库才发现数据库规则可太多了,而且自己上传数据也都是一次上传一行,没遇见过也就没有真正想过数据库在没有主键的情况下可以单单只改一个数据么,但是吧,我头铁啊,python能做到为啥数据库不行,我还是不信,我继续搜




    • 多条一次性插入:INSERT ALL INTO table_name (column1, column2, ...) VALUES (value1_1, value1_2, ...) into table_name(column_name1,column_name2) values (value1,value2)...select * from dual;


    INSERT ALL 
    INTO table_name (A1,A2) values ('a','b')
    INTO table_name (B2,C1) VALUES ('c','d')
    select * from dual;

           结果显而易见,肯定不是我所期望的那个场面,如下图:
    在这里插入图片描述



           说实话我是真搜不着啥信息,找不到想要的答案就全试一遍,撞到南墙就回头了!所以我决定接下来从update语句下手。





    使用update函数


           我想想,update好像无法新增一列,好像还没开始就结束了,但是实际页面肯定需要这个条件,那试试能不能达到自己想要的画面


           因为没有主键,所以我选择直接用update,最后结果与预料的一样,一列全部改变,图下图:


    update  GJC_DATA set GJC_DATA.c2= 'c2'


    在这里插入图片描述
           然后我就想到了第二范式的概念:第二范式要求在满足第一范式的基础上,非码属性必须完全依赖于候选字,也就是要消除部分依赖。
    没有主键形成依赖,不满足第二范式。但是好像就算我加上一列自增主键,也无法用insert插入一个指定位置而不是一次插入一行,但是update是可以实现的,如下图(重新创建一个数据库表):


    CREATE TABLE WANG.gjc_data(
    id int NOT NULL,
    a1 varchar(128),
    a2 varchar(128),
    a3 varchar(128),
    a4 varchar(128),
    a5 varchar(128),
    b1 varchar(128),
    b2 varchar(128),
    c1 varchar(128),
    c2 varchar(128),
    c3 varchar(128),
    c4 varchar(128),
    c5 varchar(128),
    c6 varchar(128),
    c7 varchar(128),
    PRIMARY KEY(id)
    );
    create sequence id_zeng_1
    start with 1 --以1开始
    increment by 1;
    insert into wang.gjc_data (id,A1,b1) values(id_zeng_1.nextval,'a','d');
    insert into wang.gjc_data (id,A1,b1) values(id_zeng_1.nextval,'b','e');
    insert into wang.gjc_data (id,A1,b1) values(id_zeng_1.nextval,'c','f');

    在这里插入图片描述


    update wang.gjc_data set A1='B'  WHERE id=1;

    在这里插入图片描述



           好吧,认清现实了,不过insert一次插入一行,下面直接插一行我python使用的时候早就可以用pandas清空空值,他也无法接受,可能他觉得客户看起来不好看吧,得,那凑活给他改改




    • Oracle数据库


    在这里插入图片描述



    • Jupyter读取Oracle


    在这里插入图片描述


    总结


           到这算是结束了,总结一下,我原以为是我数据库学的不精通,做不到指定位置添加,经过这么一番探索后才发现真的没有这种操作,果然,实践才是检验真理的唯一标准,不遇上这事我还真一直有这个误区,算了,这次被自家人嘲笑就嘲笑了,那也比到时候出差去外面丢人强。



           谨以此文提醒自己,不再犯相同错误,数据库并不可以向excel那样用语句向指定位置插入指定值,更新也是需要设置主键或是一列唯一值去做一个指引;理论知识还是比较薄弱,需要持续加强。



    作者:LoveAndProgram
    来源:juejin.cn/post/7187287554796814393
    收起阅读 »

    如何快速的掌握一门编程语言

    因为飞书底层是用Rust开发的,所以最近一段时间都在写Rust的代码,我对写Rust也越来越顺手,速度甚至已经比我用了很多年的c++要更快了,虽然主要原因是Rust有很多语法糖,可以加快写代码的速度。作为了一个之前完全没接触过Rust的新手,也就花了几天时间,...
    继续阅读 »

    因为飞书底层是用Rust开发的,所以最近一段时间都在写Rust的代码,我对写Rust也越来越顺手,速度甚至已经比我用了很多年的c++要更快了,虽然主要原因是Rust有很多语法糖,可以加快写代码的速度。作为了一个之前完全没接触过Rust的新手,也就花了几天时间,便能熟练的进行Rust项目的实战开发了。


    我想到我曾经还是一位菜鸟程序员得时候,学习并且掌握一门语言要花很长的时间,并且还会到处向朋友炫耀,自己会多少种编程语言,到如今,已经完全不关注自己会多少种编程语言了(因为写过的语言太多,起码也十几种),学习一门新的编程语言所需的成本也已经很低了,一般也几天时间,就能掌握这门语言,我的关注点也从我会多少种语言,转移到这门语言在解决问题和提高效率上的实用性上。


    所以我写这篇文章不是为了介绍Rust怎么学,主要是想讲讲作为一个经验丰富的程序员,如何能做到前面提到的两点:



    1. 如何用很短的时间掌握一门新的编程语言

    2. 如何基于实用性出发选择合适的编程语言


    如何短时间内掌握一门新的编程语言


    编程语言千千万,但是基本都要解决同样的问题,我这里列出了一些最主要的问题:



    1. 如何进行任务调度

    2. 如何处理数据

    3. 如何处理异常

    4. 如何管理内存


    不管我们学习哪种编程语言,都要带着这些共通的问题去进行学习。针对这几个问题,下面我一一进行讲解。


    如何进行任务调度


    在任务调用上,无非就是两种方式:线程和协程。线程和协程的原理我不深入介绍,就简单讲一下,线程就是一个应用线程对应了一个系统的进程,是一对一的关系,而协程是多个协程对应了一个系统进程,是多对一关系。在使用场景,线程适合高CPU消耗任务,而协程适合高频的IO任务。当我们深入掌握了线程和协程的原理,线程安全的原理,线程协作的原理等基础知识点,那么面对任何我们没接触过的新语言时,我们只需要知道该编程语言支持哪种调度方式,比如Java支持线程,Kotlin支持协程(假协程),Rust支持线程和协程,然后再熟悉这门语言进行任务调度,加锁,等待,休眠等特性的代码要怎么写,我们便掌握了这门语言至少20%的知识点了。


    如何处理数据


    在数据处理上,所有的编程语言都需要提供基本的数据结构,如数组,队列,Map等等。我们需要掌握的是数据结构的原理,面对不同的数据类型特性,如何选择更合适的数据结构,而当我们学习一门新的语言时,只需要了解这些基本的数据结构对应的是哪些类即可,比如Rust中的Vec,Java中的ArrayList,其实都是动态数组这一基本的数据结构,所以在Rust学习时,我很快就能熟练的使用Vec等这门语言提供的集合容器。到这里,我们已经掌握了这门新语言40%的知识点了。


    如何处理异常


    异常处理是编程语言中很重要的一块知识,但是新人却很容易忽略。我们都不希望程序动不动就crash了,越是优秀的语言,对异常的处理越是完善,写出的代码crash也越少,比如用Kotlin写的程序,空指针导致的crash比java要少很多。其中Rust是我遇到的在异常处理上最为严苛的,需要手动处理每一个异常。当我们熟悉这门语言该如何处理异常时,到这里便已经掌握了这门新语言60%的知识点了。


    如何管理内存


    习惯了使用解释性语言的开发者可能不太关注内存的管理,当我们使用Java时,Kotlin时,并不需要我们主动去释放内存,因为虚拟机会帮我们做。但是对于其他编译型语言来说,内存的回收和释放,都只能我们自己做了,最常见的就是c++语言,内存的申请和释放都是让我们写起来觉得很麻烦的地方。当我们学一门新语言的时候,一定要熟悉这门语言是怎么管理内存,即使是Java这种不需要我们手动管理内存的语言,我们需要了解它的虚拟机是如何进行内存管理的。当我在学习Rust时,我首先关注的就是Rust需要如何管理内存,因为他是一门编译型语言,性能上不会逊色c++,我以为它依然需要自己手动申请和释放内存,结果发现Rust通过所有权的机制,使得我们不需要自己手动的申请和释放内存,这种机制立刻就让我眼前一亮,因为这是我之前从没接触过的一种新的思维。到这里,当我们掌握了这门语言是如何进行内存管理的,我们便掌握了这门语言80%的知识点了。


    其他


    剩下20%的知识点,包括这门语言基本类型的申明,提供的API,独有的一些特性,语法糖等等,在使用过程中,就能慢慢孰能熟能生巧了。


    基于实用性出发选择合适的编程语言


    我们学习或者在项目中选择一门新的语言,不能谁便拍脑袋,而是要基于实用性的考虑。我常常考虑的主要有下面这些点:



    1. 性能

    2. 简单易用

    3. 安全

    4. 跨平台

    5. 足够多的社区支持


    编译型的语言在性能上是要好于解释性语言的,所以我们在客户端开发时,很多对性能要求高的逻辑都是用c++来写,而不是用Java来写。我在前面提到过飞书的底层是用Rust写的,这里的底层主要是数据层,包括db,网络请求等和数据相关的逻辑,都是用Rust完成,其中一个主要的原因就是因为Rust支持携程,在IO场景上性能会更好的,在加上其他的一些考虑,比如简单易用,跨平台等特性,Rust自然便承担了这一重任。


    其他的点我就不一一展开说了。我们在选择语言的时候,都会有原因,新人可能在意简单易用;创业团队可能在意跨平台;对我来说,我对性能的要求是比较高的,因为我本身就是做性能优化这一领域的。只要我们有自己的原因和目的,而不是盲目的去选择和学习即可。


    android-604356_1280.jpg


    我听不少开发都说过,互联网行业技术更新迭代快,学的知识很容易就过时,然后就需要重新学习。我实际是不认可这些说法的,底层的知识更新迭代很慢的,比如计算机的原理,Linux系统的原理等等,只要我们深入掌握了,其实是可以使用很长时间的,是投入产出比很高的事情,我们认为迭代快的东西,往往都是上层的技术,比如一个框架,一个门编程语言等等。但只要掌握那些迭代慢的底层技术原理,不管上层的技术迭代多块,我们都是能很快速的进行响应和跟进的。


    作者:helson赵子健
    来源:juejin.cn/post/7280746697832169526
    收起阅读 »

    30岁的我终于如愿考上了教师 | 2023年中总结

    今天,应该是我做程序员这个职业的最后一天,我放弃了20+K的工作,明天过后就要离开北京,开启我新的生活。 2023年,我终于如愿的考上了教师,最近两个月为了教师招聘的事情多次往返于北京和老家,至此整个事情终于告一段落,写篇文章分享一下这一年的考试历程,也分享...
    继续阅读 »



    今天,应该是我做程序员这个职业的最后一天,我放弃了20+K的工作,明天过后就要离开北京,开启我新的生活。



    2023年,我终于如愿的考上了教师,最近两个月为了教师招聘的事情多次往返于北京和老家,至此整个事情终于告一段落,写篇文章分享一下这一年的考试历程,也分享给看完上一篇文章一直等待更新的jym。


    书接上回,在得知即将成为爸爸后,我就开始实施了回老家的计划,最终决定考取教师编。大概在去年7月份便开始准备了教师资格证的考试,我报的科目是高中信息技术学科,每天利用下班时间学习一会。这里可以给感兴趣的jy说一下教资的考试内容,教资分为笔试和面试。笔试包括三科,综合素质(科一),教育知识与能力(科二)以及专业课(科三),对于专业课来说,大部分都是平时作为程序员可以接触到的内容,以及我们常说的八股文内容,相对于程序员面试来说知识点简单很多。但是对于科一科二来说真的很让我这个理科生头疼,要背诵的内容真的太多了,没办法硬着头皮也要学,每天早上上班早来一会背一背,晚上回去了就看看培训机构的视频。在七月底,还参加了教资认证必须的普通话考试,获得了一乙的成绩。


    image.png


    时间来到10月,我的女儿出生了,出生那天距离教资考试还剩下1星期,那1星期真的是忙碌、累到不想说话。在考前一周也没有什么复习的机会,就这样忙忙碌碌的去参加了考试。


    考场外


    11月份考试结果出来了,不出意外,果然有没过的科目,科二科三险过,科一考了69分,差一分进入及格线。就这样,我不得以继续准备科一的考试,经过对之前考试的分析,自己失分点应该主要在材料分析和作文上,这次把精力主要集中在练习作文和材料分析上。终于不负众望,在今年3月份的考试中通过科一,接下来就开始准备起了面试。


    image.png


    面试主要是试讲,就是模拟授课,考试时台下坐着3个老师,自己在台上讲,还有模拟提问等环节。网上有着一种说法,说教资面试是人生至尬时刻,每每回想起来都想钻地缝,自己参加之后发现的确是这样,现在想起来还是尬的抠脚。试讲主要就是多说多练,相对于笔试来说通过率还是挺高的。在准备期间,媳妇就充当我的学生,每天晚上练习练习,就这样在5月份我通过了教资面试。


    image.png


    6月份,在经过了体检、教资认定环节之后,便顺利的拿到了教师资格证,当时还是很开心的✌🏻。


    image.png


    拿到证书之后,便是等待教师招聘的过程,在此期间还参加了天津某学校的教师招聘,对于笔试科目已经忘的差不多的我排名几乎倒数。至此开始意识到必须时刻让自己的理论知识保持竞争力,因为教师招聘一般就是提前一周两周发布的,并没有太多的准备机会,只能靠平时的积累。(忽略我潦草的字迹)


    image.png


    时间来到7月,一个令人激动的公告发布了,老家的市区发布了教师招聘的消息,信息技术科目招聘6人,对于往年来说,这个招聘人数很多了。对我来说真的是让人兴奋的消息,得到消息后,我和媳妇当即决定,她和孩子火速赶回老家,留我一人在北京安心复习。其实这次考试我并没有抱太大的希望,因为之前给我的感觉,市区的教师招聘还是比较激烈、比较卷的。所以准备笔试时也没有太大压力,照常复习。半个月后,我赶回老家参与了这次考试,考完当时就觉得凉了,所以把媳妇孩子就接了回来,跟我一起回了北京。但是生活真是惊喜不断,一个星期后查看考试官网公告,我居然进入了面试环节,为了能够安心准备面试,又不得已将孩子再次送回了老家,现在想想这两月也真是够折腾孩子的😆。


    教招面试与教资面试不同,这次面试考的是说课,相对于试讲来说,“尬”点要少一点,主要考察考生表达、授课能力。考试那天,真的是人生中最紧张的一次考试,甚至觉得高考都和它差了一个等级。在巨大的压力和紧张情绪下,我幸运的通过了面试,接下来的8月就是通过体检和政审,被正式录用啦~另外说一下我考的这次教师招聘是属于人事代理,归教育局管,老家那边已经多年不招聘编制教师了,但是据之前考过的朋友和亲戚说人事代理这样的也算挺稳定的,有懂的jy可以在评论区给大家科普一下。


    以上就是这一年的考试历程,因为老家这边过了30就不能参加教师招聘,所以在这一年时间内并且也是仅有的最后一年机会能考下教资+教招,觉得自己很幸运,回想起来真的是觉得感谢自己一年的努力,如愿的完成自己的人生初步规划,觉得一切辛苦都是值得的。


    说点题外话,在这期间的考试过程中,我发现信息技术学科考教师的话,相对于语数外那些科目,网上的资料是比较少的,甚至有的机构不开设信息技术科目。这一年自己也是从不懂到懂的过程一步步走过来的,如果有想考教师的jy有疑问也可以留言为你解答。


    作者:一个小开发
    来源:juejin.cn/post/7273025562141474852
    收起阅读 »

    三个月内遭遇的第二次比特币勒索

    早前搭过一个wiki (可点击wiki.dashen.tech 查看),用于"团队协作与知识分享".把游客账号给一位前同事,其告知登录出错. 用我记录的账号密码登录,同样报错; 打开数据库一看,疑惑全消. To recover your lost Dat...
    继续阅读 »

    早前搭过一个wiki (可点击wiki.dashen.tech 查看),用于"团队协作与知识分享".把游客账号给一位前同事,其告知登录出错.



    用我记录的账号密码登录,同样报错; 打开数据库一看,疑惑全消.




    To recover your lost Database and avoid leaking it: Send us 0.05 Bitcoin (BTC) to our Bitcoin address 3F4hqV3BRYf9JkPasL8yUPSQ5ks3FF3tS1 and contact us by Email with your Server IP or Domain name and a Proof of Payment. Your Database is downloaded and backed up on our servers. Backups that we have right now: mm_wiki, shuang. If we dont receive your payment in the next 10 Days, we will make your database public or use them otherwise.



    (按照今日比特币价格,0.05比特币折合人民币4 248.05元..)


    大多时候不使用该服务器上安装的mysql,因而账号和端口皆为默认,密码较简单且常见,为在任何地方navicat也可连接,去掉了ip限制...对方写一个脚本,扫描各段ip地址,用常见的几个账号和密码去"撞库",几千几万个里面,总有一两个能得手.


    被窃取备份而后删除的两个库,一个是来搭建该wiki系统,另一个是用来亲测mysql主从同步,详见此篇,价值都不大




    实践告诉我们,不要用默认账号,不要用简单密码,要做ip限制。…



    • 登录服务器,登录到mysql:



    mysql -u root -p





    • 修改密码:


    尝试使用如下语句来修改



    set password for 用户名@yourhost = password('新密码');



    结果报错;查询得知是最新版本更改了语法,需用



    alter user 'root'@'localhost' identified by 'yourpassword';




    成功~


    但在navicat里,原连接依然有效,而输入最新的密码,反倒是失败



    打码部分为本机ip


    在服务器执行


    -- 查询所有用户


    select user from mysql.user;


    再执行


    select host,user,authentication_string from mysql.user;



    user及其后的host组合在一起,才构成一个唯一标识;故而在user表中,可以存在同名的root


    使用


    alter user 'root'@'%' identified by 'xxxxxx';

    注意主机此处应为%


    再使用


    select host,user,authentication_string from mysql.user;

    发现 "root@%" 对应的authentication_string已发生改变;


    在navicat中旧密码已失效,需用最新密码才可登录


    参考:


    mysql 5.7 修改用户密码




    关于修改账号,可参考此




    这不是第一次遭遇"比特币勒索",在四月份,收到了这么一封邮件:



    后来证明这是唬人的假消息,但还是让我学小扎,把Mac的摄像头覆盖了起来..


    作者:fliter
    来源:juejin.cn/post/7282666367239995392
    收起阅读 »

    组件阅后即焚?挂载即卸载!看完你就理解了

    web
    前言 上家公司有个需求是批量导出学生的二维码,我一想这简单啊,不就是先批量获取学生数据,然后根据QRcode生成二维码,然后在用html2canvas导出成图片嘛。 由于公司工具库有现成的生成压缩包方法,我只需要获得对应的图片blob就可以了,非常的easy啊...
    继续阅读 »

    前言


    上家公司有个需求是批量导出学生的二维码,我一想这简单啊,不就是先批量获取学生数据,然后根据QRcode生成二维码,然后在用html2canvas导出成图片嘛。
    由于公司工具库有现成的生成压缩包方法,我只需要获得对应的图片blob就可以了,非常的easy啊。


    开始动手


    思路没啥问题,但第一步就犯了难,用过react框架或者其他MVVM框架的都知道,这种类型的框架都是数据驱动视图,也就是说一般情况下,必须先获得数据,然后根据数据才能得到视图。


    但是问题是,html2canvas也是必须需要获取真实dom的快照然后转换成canvas对象。


    听着好像不冲突,诶,我先获取数据,然后渲染出视图,在依次通过html2canvas来生成图片不就完事了嘛!但是想归想,却不能这么做。


    原因主要有两个,一个原因呢是交互逻辑上就行不太通,也不友好。你不能“啪”点一下导出按钮,然后获取数据之后再去等所有数据渲染出对应组件之后,再去延迟处理导出逻辑。(耗时太长)


    另一个原因呢,主要是跟html2canvas这个工具库有关系了,它的原理简单来说呢,就是复制你期望获取截图的那个dom的渲染树,然后根据这个渲染树在当前页面生成一个你看不见的canvas dom对象来。那么问题来了,因为是批量下载,所以肯定会有大量的数据,那么如果不做处理,就会有大量的canvas对象存在当前页面。


    canvas标签是会占用内存的,那么当同时存在过多的canvas时,就会出现一个问题,页面卡顿甚至崩溃。所以,这是第二个原因。


    那么这篇文章主要是解决第一个原因所带来的问题的。


    编程!启动!


    第一步


    那么先简单的随便生成一个组件好了,因为是公司源码嘛,大家懂的都懂。


    interface IProps {
    qrCode: string
    studentName: string
    className: string
    }

    const SaveQRCode = (props: IProps) => {
    const divRef = React.useRef<HTMLDivElement>(null)
    // 具体怎么渲染看你们需求了
    return (
    <div ref={divRef}>XXXXXX</div>
    )
    }

    看到代码,用过html2canvas的小伙伴应该知道ref是干嘛用的了,html2canvas()这个方法的参数是HTMLElement,传统一点的办法呢,可以通过document.getXXXXX这个方法来获取真实的dom元素。那么Ref就是替代前者的,它可以直接通过react框架获取真实的dom元素。


    第二步


    那么最简单的组件我们已经写好了,接下来就是如何动态的挂载这个组件,并且在挂载完之后就立刻卸载它。


    那么先来理一下思路:
    1、动态地挂载这个组件,且不能被用户肉眼观察到
    2、挂载动作执行完立刻执行html2canvas获取canvas对象
    3、通过canvas对象转换成blob对象并返回,或者直接通过回调函数返回canvas对象
    4、组件卸载,清空dom


    那么根据上面几点,可以得出:从外部获取的肯定是有组件这个东西,而挂载的位置则有要求,但并不一定需要从外部获取。


    为了不被样式影响,我们直接在body标签下,再挂载一个div标签,来进行组件的动态渲染和卸载,同时也避免了影响之前dom树的结构。


    思路就说到这了,接下来直接抛出代码:


    const AsyncMountComponent = (
    getElement: (onUnmount: () => void) => ReactNode,
    container: HTMLElement,
    ) => {
    const root = createRoot(container)
    const element = getElement(() => {
    root.unmount()
    container.remove()
    })
    root.render(<Suspense fallback={null}>{element}</Suspense>)
    }

    这里我因为想做的更加通用一点,所以把根节点让外部进行处理,如果希望更加业务一点,比如当前这个场景必然不会让用户可见,可以直接改成


    const AsyncMountComponent = (getElement: (onUnmount: () => void) => ReactNode) => {
    const div = document.createElement('div')
    div.style.position = 'absolute'
    div.style.left = '2000px'
    document.body.appendChild(div)
    const root = createRoot(div)
    const element = getElement(() => {
    root.unmount()
    container.remove()
    })
    root.render(<Suspense fallback={null}>{element}</Suspense>)
    }

    这里的隐藏方式看个人喜好,无所谓。但有一点要注意的是,一定要可见,不然的话html2canvas生成不了图片,这里是最简单粗暴的方式,直接偏移left


    第三步


    那么地基打好了,我们该怎么用这两个东西呢


    interface IProps {
    qrCode: string
    studentName: string
    className: string
    // 这里自然就是获取blob和canvas对象的地方了
    onConfirm?: (data: { canvas: HTMLCanvasElement, blob: Blob }) => void
    // 这里是卸载的地方,由外部决定何时卸载节点,更加自由
    onUnmount?: () => void
    }

    const SaveQRCode = (props: IProps) => {
    const divRef = React.useRef<HTMLDivElement>(null)
    useEffect(() => {
    if (divRef.current && props.onConfirm) {
    html2canvas(divRef.current).then((canvas) => {
    canvas.toBlob((blob) => {
    props.onConfirm!({canvas, blob: blob!})
    props.onUnmount!()
    })
    })
    }
    }, [])
    // 具体怎么渲染看你们需求了
    return (
    <div ref={divRef}>XXXXXX</div>
    )
    }

    首先我们对组件进行修改,因为我的方案是第一种,没有太业务向,所以说一些业务逻辑必然是要到组件层面去处理的,所以添加两个参数,一个获取blobcanvas对象,另一个用来卸载节点。


    至于useEffect就很容易理解了,挂载后用html2canvas处理组件顶层div获取截图,然后返回数据,并卸载节点。


    组件改造完毕了,那我们接下来把这两个组合一下


    const getQRCodeBlobCanvas = async (props: IProps): Promise<{
    canvas: HTMLCanvasElement, blob: Blob
    }> => {
    return new Promise((resolve) => {
    const div = document.createElement('div')
    div.style.position = 'absolute'
    div.style.left = '2000px'
    document.body.appendChild(div)
    asyncMountComponent(
    (dispose) => (<SaveQRCode {...props} onConfirm={resolve} onUnmount={dispose}/>),
    div
    )
    })
    }

    那么一个简单的动态阅后即焚组件就完成了,且可以直接通过方法的形式使用,完美适配批量导出功能,当然也包括单个导出,至于批量导出的细节我就不写了,非常的简单。


    升级V2


    我只提供了最通用一种方式来做这么个阅后即焚组件,之后我闲着无聊,又把它做了一次业务向升级,获得了V2版本


    这个版本呢,你只需要传入一个组件进去,且不用关心何时卸载,它是最真实的阅后即焚。至于数据,会通过Promise的方式返回给用户。


    const Wrapper = ({callback, children}: {  
    callback: (data: { blob: Blob,canvas: HTMLCanvasElement }) => void,
    children: ReactNode
    }
    ) => {
    const divRef = useRef<HTMLDivElement>(null)
    useEffect(() => {
    if (divRef.current) {
    html2canvas(divRef.current).then((canvas) => {
    canvas.toBlob((blob) => {
    callback({canvas, blob: blob!})
    })
    })
    }
    }, [])
    return <div ref={divRef}>
    {children}
    </div>

    }

    const getComponentSnapshotBlobCanvas = (getElement: () => ReactNode): Promise<{canvas:HTMLCanvasElement, blob: Blob}> => {
    return new Promise((resolve) => {
    const div = document.createElement('div')
    div.style.position = 'absolute'
    div.style.left = '2000px'
    document.body.appendChild(div)
    const root = createRoot(div)
    root.render((
    <Wrapper
    callback={(values) =>
    {
    root.unmount()
    div.remove()
    resolve(values)
    }}
    >
    {getElement()}
    </Wrapper>

    ))
    })
    }

    其实也没啥特别的,无非就是把业务层公共的东西封装进了方法里,思路还是上面那个思路。


    那么这篇博客就到这里了,感谢阅读!


    作者:寒拾Ciao
    来源:juejin.cn/post/7278512641781334051
    收起阅读 »

    我的前端故事之终入BAT

    前言 这个周末终于能抽出时间写写了,看到有些朋友在催更还是挺开心的,谢谢大伙。这一篇是整个系列的最后一篇,将会写到现在的时间线,今后应该会根据发生的事,较长时间才更新一次。 如果有细心的朋友看了我之前写的四篇,会发现这一篇我不再以菜鸟前端为题目了,如文章标题所...
    继续阅读 »

    前言


    这个周末终于能抽出时间写写了,看到有些朋友在催更还是挺开心的,谢谢大伙。这一篇是整个系列的最后一篇,将会写到现在的时间线,今后应该会根据发生的事,较长时间才更新一次。


    如果有细心的朋友看了我之前写的四篇,会发现这一篇我不再以菜鸟前端为题目了,如文章标题所见,我成功入职BAT之一,虽然远不能称为技术大佬,
    但摘掉菜鸟的帽子,应该还是可以滴。


    入职金融公司


    回到当时的时间线,这家金融公司规模很大,就叫简单粗暴的叫它大金融吧,万人以上员工,这也是我选择的原因之一。另外虽然这是我当时最高的offer,但回想起来面试难度却比较一般,算法只是问了思路没有要求手写,似乎搞金融的技术都比较保守(或许是我的偏见)。


    去入职之前还是比较紧张的,因为这跟我之前经历的公司规模相差太大了,完全不是一个级别。我几乎没有前端团队协作的经验,之前的两年虽然写了不少代码,但只能说是闭门造车。那时的我不了解前端工程化、甚至没有团队git的使用经验,一看到代码冲突就手忙脚乱好像犯了大错。


    入职当天来到新公司楼下,位于南山核心地段的高端写字楼,物业的小姐姐小哥哥都是俊男美女,令我开了眼界。hr给我签了合同后,我见到了我未来的直系领导湖哥,其实我一度很担忧遇到一个不好相处的领导,毕竟之前遇到的小张总之类的都算是非常规的老板。幸运的是我多虑了,我又遇到了一个相当不错的leader。


    湖哥是80后,本身是做客户端的,组里的老人也几乎都是客户端的,只是我们的业务是新成立的团队所以整个大前端由湖哥管理,所以实际上湖哥在web开发并没有太多的经验,很多时候我可以自己发挥。


    我是这个业务web端的第一个新人,所以湖哥直接带我,在我入职半个月后部门又入职了四五个前端,整个大前端团队十余人,已经算是不错的规模。与此同时,整个部门的研发人员也由一开始的二十多人逐渐扩充到七十多人,囊括算法、服务端、前端、测试。其中也有不少名校毕业、大厂经历的大佬,甚至算法还有博士。搞得我怀疑我是怎么进来的,后期甚至招聘只要211以上,还好我进来得早。


    技术进展&团队角色


    大金融的技术没有我预想的那么高要求,项目还算属于常规的类型,也有算法标注平台,移动端海报编辑器等不错的项目。技术深度对于当时的我已然足够,我迅速熟悉了项目,几乎可以用无痛上手来形容。


    但我仍学到了不少现代前端的知识,比如canvas动画、工程化、持续集成等方面,也了解到原来一个项目可以有那么多服务器、那么多环境。也学到了不少无用的知识,就是那些什么业务条线、触达、拉通之类的互联网黑话。


    之前有提到我是第一个入职的新人,所以我天然有一些团队内优势,比如会担任新同事导师,
    更加熟悉团队内项目,再加上我经历特殊,脑子比较灵活不只是死学钻技术,做人做事情商方面还不错,所以后期算是web侧的核心,用不太好的词形容就是湖哥的嫡系......


    而且湖哥本身不熟悉web开发,有次上面交代了一个需要快速上线内测项目,希望不要服务端参与让我们前端自己解决,我主动跟湖哥申请接过了任务,因为我一直很有兴趣学学nodejs,有带薪实战的机会一定要抓住,即使可能会遇到很多坑。


    我还记得我当时提出的时候湖哥脸上喜悦的表情,因为这事让他很头痛。


    后来我独自加班加点一周自学node搞定了前后端,成功跑通了业务流程,或许在湖哥心里又给我加了一分,从那之后web端的任务基本都是我在分配,他基本不再插手交给我了,
    回顾当时虽然没有title,实际上我已经算是web侧虚线leader。


    更进一步的想法


    大金融的日子就这么在需求与开发中一天天过去,熟悉技术和项目后,并没有太大的波澜。平时很少加班,忙的时候一个月加几天,不忙有空我就学学新技术,后来觉得php实在非主流了,还学了python(还是不喜欢java~)用在了公司项目了,扩充了技术栈。


    不过也是在大金融我才感受到双休的幸福,周末有更多的时间陪对象,不用担心醒来又要上班。我第一次有了公积金,有了通讯补贴之类的杂七杂八的福利,也知道了原来年假这个东西是真实存在的。


    也才意识到之前单休、随意加班的日子并不正常,只是我没介意而已。


    慢慢的,我想要升职加薪,我不再满足18k,现在想来野心其实有点大,因为入职还不到一年而已。可也是在蠢蠢欲动的这个过程中,我发现大金融的阶级相当固化,leader几乎只有更换,没有晋升,而且人员流动极大,这是我之前没有体验过的,新业务并没有盈利,但是之前的疯狂扩张却埋下了隐患,在不到一年里总监、hr、架构师甚至大老板都都有离职,我甚至打听到今年的年终奖要延迟到明年六月才发,我开始意识到不对劲,要是年终不发会影响到薪资流水,我萌生了要跳槽的想法,而且是要年前跳出去。


    还有一个原因是当时正值互联网顶点时期,风头正盛,比18年有过之无不及,有大把的跳槽机会,脉脉上那些大厂应届生晒出的offer薪资多次上了热搜,令我羡慕至极。


    另一方面,咱们这种平民程序员,谁又没有一个大厂梦,虽然我学历很差,但是还是想试试。
    万一成了呢?


    全力准备面试


    跟女友说了想法后她也非常支持,当然这是预期之内的,谁不喜欢自己另一半上进。也给父母说了我一定要在半年内达到30万年薪,算是给自己的一份承诺和压力,意外的是我爸妈都一致
    表示不希望我太累,要生活和身体为重,他们认为我现在已经很不错了。那一辈人能有这种想法真是不容易,很感谢他们第一时间想到的是我的身体,而不是赚多少钱。其实我内心的想法也没那么极端,我知道以我的学历和经历,进大厂的可能微乎其微,我想的是至少要进个中厂,至少进入一个大家都知道的、有名气的公司,像携程、贝壳、唯品会、喜马拉雅、迅雷等,我把想去的公司列了一个目标名单,打算做好十足的准备后都去试试。


    为了激励自己,我很沙雕的在拼多多买了一个条幅挂在家里墙上,想偷懒的时候就看看。


    现在想来非常中二,女友看着家里的横幅一度无语,说像是进了传销窝点。


    那段时间我加足了马力准备面试,公司的需求我通常只用排期的一半时间就可以做完,剩余的时间就用来准备面试,晚上回到家再学习到十点多。具体的过程倒不必刻画得多么艰辛,因为心里对目标的渴望,使我的学习过程充满了动力。总之经过有目标、有规划的准备后,从算法到源码,基本都有底。


    大约持续两个月这样的日子后,我迎来了第一次面试。


    再次挑战大厂


    其实这次面试打乱了我的计划,因为学习计划还有20%左右没有完成,而且我本来是想先找小厂练练手找找感觉,再去试试心仪的公司。但是hr告诉我这个hc近期就会关闭,还不如现在就试试,等我准备好了可能都招满了。简单思考了下我果断决定投递简历,因为这个岗位我非常钟意,可遇不可求。


    这是腾讯PCG的web开发岗,所属于一个绝大多数人都使用或者安装过的国民级应用。


    为一个从小就使用的app写代码是难以想象的事,实在是遥不可及,带着很可能挂简历的预期投递给了hr。


    第二天我就收到了面试邀请,看来我的简历写得还算不赖,hr也说他把简历发群里很快就被一个组挑走了。后来我了解到这个岗位对于我来说是比较偏高的职级,当时的腾讯已经几乎不会设置低职级的社招岗位了,包括现在也是如此。


    了解到后我的压力又大了一分,我没有跟女友说要面试腾讯,因为我怕她预期太高后面很可能会失望,虽然她从不在事业上要求我什么。


    第一面的时间就约在了周六,一下就感受到了可能会很卷,不然不会在周六面试,可是当时的我就算007也得试试,我起点太低,进大厂的渴望早就超越一切了。


    带着紧张的心情迎来了一面,面试官是一个语气非常礼貌的同龄人,全程面试都是在跟我讨论问题一样的气氛,但是态度好归好,难度还是挺高的。


    大厂面试特别之处在于,大厂并不在意候选人在框架上或者具体某项技术的经验,不会要求你一定在某种技术有大量经验,与小公司完全不一样。


    面试范围非常全面,从计算机网络、浏览器到源码、性能都有涉及,这些都答得不错,算法没有太难用不太高效的解法答出来,但是涉及到跨端的底层、node中间件的问题我回答得并不好,因为那时候几乎没有这方面实战经验。面完我就感觉凉了,虽然早有预期但是还是很失落的,又一次机会被我错过。


    虽然心里已经放弃,但我还是积极的跟hr反馈了情况,接着就是等待着"死亡宣判"的消息。


    第二天带着紧张的心情点开了hr的未读消息:


    一面通过!准备二面!


    看到消息的我兴奋得手舞足蹈,长舒口气。现在看来还挺搞笑的,只是一面而已,可对于那时候的我是意义非凡,那是第一次通过了大厂的考验,即使只是一轮。


    二面没有我想象的那么难,比第一面发挥更好,如果按照第一面的标准我应该没问题,果不其然,二面也顺利通过。后来听hr反馈,一面给了我相当高的评价,实属意外,看来自己的感受并不一定准确。


    通过二面之后,我开始有了一些幻想,幻想进入腾讯的那一天,但也知道很可能也只是幻想。
    三面是最难的一面,面委会的一轮。应该是出于要定级的原因,我强烈感受到了面试官想要逼近我的技术极限,虽然没有写算法但整个过程非常压迫,提了很多极其深入和刁钻的问题,结束后我知道这次绝对完了,比一面糟糕多了。


    可是第二天却收到了hr通过的消息,后来才知道面委一般较少挂人,面试也是会故意提高难度,主要是给用人部门做职级参考。


    再后来就是总监面、GM面、Hr面,一共经历了整整六面,加上中间还有一些小波折,接近两个月才面完,鹅厂流程长真是名不虚传。
    等待offer的那段时间真是患得患失,怕没hc,怕学历太差被卡,又怕后面背调不过,总之就是心神不宁,兴奋又惶恐。


    终入鹅厂


    经过漫长的等待,在一个工作日,我终于收到了offer,本来想了很多形容当时心情的词,此时不知道如何去描述。但我也相信经过之前的描述和铺垫,足够把我当时的心情交给大家去想象。


    当天我就提了离职,面对湖哥的失落我感觉自己真不是个人,可是这可是腾讯啊,对不起了湖哥。


    晚上跟女友一起去吃了最爱吃的那家餐厅,庆祝一下,我记得很清楚,本以为那顿饭会是吃得最美味的一顿饭,却没想到是最没味道的一顿饭,因为非常兴奋已经没有精力去品尝菜的味道。跟爸妈分享后,我也感受到了他们对我成长的欣慰,毕竟我上学的时候实在太贪玩了,现在能够一直奋斗向上算是出乎意料,我的学历能进腾讯算是一个小奇迹。


    说起来汗颜,我已经太久太久没能让他们为我骄傲过了。


    再后面,腾讯足足找了六个前同事领导对我进行了背调,顺利通过后,我拿到了象征入职最后一步的鹅厂红围脖,腾讯员工应该都知道我说的是什么。紧接着就是拍工牌照上传,在线入职签合同,英文名是真难取,在我还没入职,就收到短信提醒我电脑等办公设备已经到了工位,新款的macbookPro,4k显示器。人家都说进大厂最开心的就是入职和离职的时候,至少前一半说对了,那段时间我真的好开心。


    至此,尘埃落定,我要进大厂了,而且是top级的大厂,小时候打游戏时天天骂的腾讯。


    还是说一下待遇,透露职级不太合适,类似于阿里P6,这个职级的薪资水平大家网上也能很好查到,就不说具体数字了,对于本科三年经验的我已经相当满意了,而且还有班车食堂顶格公积金等福利。


    总结这几年


    其实写到这里,前几年的成长主要历程就已经结束了,后面虽然也有一些小进步,但没有质的变化,不足以另立篇幅来写,待我积累积累再继续。


    还有就是说腾讯是养老院的,我只能说快卷死我了,至少我所在部门是这样,增长难的背景下实在是好累。


    结尾总结一下自己的历程吧,回顾下自己做对了那些事,做错了哪些事,又收获了什么。


    先说说起做对的选择,回想起来实在太多,或许是因为我不错的判断能力,也可以说是我有很多的好运气。



    1、没有贪图公司名气,选择某软外包去实习,无法留用。


    2、初入社会遇到难题没有放弃,抓住实习机会恶补了技术。


    3、实习结束果断离开小外包,来到深圳,给了自己更多可能性。


    4、脱离舒适区,离开小美妆,如果当时选择躺平应该一辈子都进不了大厂了。


    5、没有去宝能,不然现在可能在举横幅讨薪。


    6、在大金融持续进步学习,成为核心,走上正常职业道路,接触互联网开发。


    7、在察觉到大金融异常后,放弃组内积累忍痛离开(半年后大金融大裁员,年终奖没发)。


    8、遇到好岗位,即使面试没有准备好也果断投简历。


    9、跳槽正好抓住互联网巅峰末尾,趁着大裁员未开始准备跳槽,如果现在去面试绝无可能拿到offer。



    还有一个很重要的运气因素,我一路遇到的直系领导都很好,实属难得。


    再说说我认为自己做错的事,现在看来不算多:



    1、专业课应该好好学,浪费四年时光,不然不至于找工作起点那么低,爬升那么艰难。


    2、应该早点离开小美妆,进入更主流、更互联网的公司,或许可以早一点进入大厂积累更多,现在互联网红利不再,只尝到了一点点甜头。



    从上大学开始回顾,这两点是我比较遗憾的事,当然回头以上帝视角来看,这些都是马后炮了,但持续复盘总是没错的。


    最后,再聊一聊自己这几年的收获和感触吧


    有些大佬看了全文可能会觉得我之前有些地方的描述是不是夸大了,不过就是进个腾讯而已,不至于写得好像功成名就一样。可就像我总说的一样,我的起点很低,985/211的朋友们可能一毕业就进了大厂,挑选着大厂的offer。
    可是环境造就人,我从山村里走出来,个位数的年龄就要天没亮打着手电筒,经过泥泞的山路走上几公里才能到学校。小时候一直在班上名列前茅,后来考到了xx第一中学这样的市重点中学,可我们班高考连一个考上双非一本的同学都没有,如果从小到大都是所在环境的前10%,出生在大山里和出生在一线城市的区别就是垃圾本科和985的区别。


    工作前我以为我赚了钱一定要好好撒欢一下,把工资都用来买之前想买而买不起的东西犒劳下自己,但现实是毕业后就不敢不存钱,从来没有体会过网上那些人月光的感觉。


    现实里我还是很少怨天尤人的,因为一路走来我运气不错,回忆起来小时候也并不觉得苦,而是快乐更多。只是此时此景我想说一下背景,来解释为何我会对一些并不高大上的事物有那么多感触。


    我对现状已然满足,虽然有时也会做做梦,期待年薪百万的那一天,但心里其实不敢再奢求更多;有时也会有一些小骄傲,我好像还是我们学校第一个进入bat的毕业生,虽然只是成为了一线城市一个的打工仔,不过也算优秀的打工仔嘛~


    到这里菜鸟的故事结束了,我的故事还在继续,或许明天升职加薪,也可能是裁员滚蛋,且等我经历后再继续分享。


    PS:其实写这么多我的本意主要还是留给将来的自己看,回顾自己的时光。这系列文章的阅读量加起来也只是寥寥,大约几千吧,感谢这几千位朋友的关注。


    最后祝各位:



    都能做出对的选择,遇到对的人,抓住好的机遇,活出好的人生,以健壮的心态,扛住生活的压测。



    无论你是像我当初一样菜鸟还是功成名就的大佬,共勉!


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

    iOS之WebViewJavascriptBridge浅析

    iOS
    前言 H5页面具有跨平台、开发容易、上线不需要跟随App的版本等优点,但H5页面也有体验不如native好、没有native稳定等问题。所以目前大部分App都是使用Hybrid混合开发的。 当然有了H5页面就少不了H5与native交互,交互就会用到bridg...
    继续阅读 »

    前言


    H5页面具有跨平台、开发容易、上线不需要跟随App的版本等优点,但H5页面也有体验不如native好、没有native稳定等问题。所以目前大部分App都是使用Hybrid混合开发的。


    当然有了H5页面就少不了H5与native交互,交互就会用到bridge的能力了。WebViewJavascriptBridge是一个native与JS进行消息互通的第三方库,本章会简单解析一下WebViewJavascriptBridge的源码和实现原理。


    通讯原理


    JavaScriptCore


    JavaScriptCore作为iOS的JS引擎为原生编程语言OC、Swift 提供调用 JS 程序的动态能力,还能为 JS 提供原生能力来弥补前端所缺能力。
    iOS中与JS通讯使用的是JavaScriptCore库,正是因为JavaScriptCore这种起到的桥梁作用,所以也出现了很多使用JavaScriptCore开发App的框架,比如RN、Weex、小程序、Webview Hybrid等框架。
    如图:




    当然JS引擎不光有苹果的JavaScriptCore,谷歌有V8引擎、Mozilla有SpiderMoney


    JavaScriptCore本章只简单介绍,后面主要解析WebViewJavascriptBridge。因为uiwebview已经不再使用了,所以后面提到的webview都是wkwebview,demo也是以wkwebview进行解析。


    源码解析


    代码结构


    除了引擎层外,还需要native、h5和WebViewJavascriptBridge三层才能完成一整个信息通路。WebViewJavascriptBridge就是中间那个负责通信的SDK。


    WebViewJavascriptBridge的核心类主要包含几个:


    • WebViewJavascriptBridge_JS:是一个JS的字符串,作用是JS环境的Bridge初始化和处理。负责接收native发给JS的消息,并且把JS环境的消息发送给native。
    • WKWebViewJavascriptBridge/WebViewJavascriptBridge:主要负责WKWebView和UIWebView相关环境的处理,并且把native环境的消息发送给JS环境。
    • WebViewJavascriptBridgeBase:主要实现了native环境的Bridge初始化和处理。



    初始化


    WebViewJavascriptBridge是如何完成初始化的呢,首先要有webview容器,所以要对webview容器进行初始化,设置代理,初始化WebViewJavascriptBridge对象,加载URL。

        WKWebView* webView = [[NSClassFromString(@"WKWebView") alloc] initWithFrame:self.view.bounds];
    webView.navigationDelegate = self;
    [self.view addSubview:webView];
    // 开启打印
    [WebViewJavascriptBridge enableLogging];
    // 创建bridge对象
    _bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
    // 设置代理
    [_bridge setWebViewDelegate:self];

    这里加载的就是JSBridgeDemoApp这个本地的html文件。

        NSString* htmlPath = [[NSBundle mainBundle] pathForResource:@"JSBridgeDemoApp" ofType:@"html"];
    NSString* appHtml = [NSString stringWithContentsOfFile:htmlPath encoding:NSUTF8StringEncoding error:nil];
    NSURL *baseURL = [NSURL fileURLWithPath:htmlPath];
    [webView loadHTMLString:appHtml baseURL:baseURL];

    再看一下JSBridgeDemoApp这个html文件。

    function setupWebViewJavascriptBridge(callback) {
    // 第一次调用这个方法的时候,为false
    if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
    // 第一次调用的时候,为false
    if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
    // 把callback对象赋值给对象
        window.WVJBCallbacks = [callback];
    // 加载WebViewJavascriptBridge_JS中的代码
    // 相当于实现了一个到https://__bridge_loaded__的跳转
    var WVJBIframe = document.createElement('iframe');
        WVJBIframe.style.display = 'none';
        WVJBIframe.src = 'https://__bridge_loaded__';
        document.documentElement.appendChild(WVJBIframe);
        setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
        }

    // 驱动所有hander的初始化
    setupWebViewJavascriptBridge(function(bridge) {
    ...
    }

    在JSBridgeDemoApp的script标签下,声明了一个名为setupWebViewJavascriptBridge的方法,在加载html后直接进行了调用。
    setupWebViewJavascriptBridge方法中最核心的代码是:



     创建一个iframe标签,然后加载了链接为 https://bridge_loaded 的内容。相当于在当前页面内容实现了一个到 https://bridge_loaded 的内部跳转。
    ps:iframe标签用于在网页内显示网页,也使用iframe作为链接的目标。


    html文件内部实现了这个跳转后native端是如何监听的呢,在webview的代理里有一个方法:decidePolicyForNavigationAction
    这个代理方法的作用是只要有webview跳转,就会调用到这个方法。代码如下:

    // 只要webview有跳转,就会调用webview的这个代理方法
    - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    if (webView != _webView) { return; }
    NSURL *url = navigationAction.request.URL;
    __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;

    // 如果是WebViewJavascriptBridge发送或者接收消息,则特殊处理。否则按照正常流程处理
    if ([_base isWebViewJavascriptBridgeURL:url]) {
    if ([_base isBridgeLoadedURL:url]) {
    // 是否是 https://__bridge_loaded__ 这种初始化加载消息
    [_base injectJavascriptFile];
    } else if ([_base isQueueMessageURL:url]) {
    // https://__wvjb_queue_message__
    // 处理WEB发过来的消息
    [self WKFlushMessageQueue];
    } else {
    [_base logUnkownMessage:url];
    }
    decisionHandler(WKNavigationActionPolicyCancel);
    return;
    }

    // webview的正常代理执行流程
    ...

    从上面的代码中可以看到,如果监听的webview跳转不是WebViewJavascriptBridge发送或者接收消息就正常执行流程,如果是WebViewJavascriptBridge发送或者接收消息则对此拦截不跳转,并且针对消息进行处理。
    当消息url是https://bridge_loaded 的时候,会去注入WebViewJavascriptBridge_js到JS中:

    // 将WebViewJavascriptBrige_JS中的方法注入到webview中并且执行
    - (void)injectJavascriptFile {
    NSString *js = WebViewJavascriptBridge_js();
    // 把javascript代码注入webview中执行
    [self _evaluateJavascript:js];
    // javascript环境初始化完成以后,如果有startupMessageQueue消息,则立即发送消息
    if (self.startupMessageQueue) {
    NSArray* queue = self.startupMessageQueue;
    self.startupMessageQueue = nil;
    for (id queuedMessage in queue) {
    [self _dispatchMessage:queuedMessage];
    }
    }
    }

    [self _evaluateJavascript:js];就是执行webview中的evaluateJavaScript:方法。把JS写入webview。所以执行完此处代码JS当中就有bridge这个对象了。初始化完成。


    总结:在加载h5页面后会调用setupWebViewJavascriptBridge方法,该方法内创建了一个iframe加载内容为 https://bridge_loaded ,该消息被decidePolicyForNavigationAction监听到,然后执行injectJavascriptFile去读取WebViewJavascriptBridge_js将WebViewJavascriptBridge对象注入到当前h5中。


    WebViewJavascriptBridge 对象


    整个WebViewJavascriptBridge_js文件其实就是一个字符串形式的js代码,里面包含WebViewJavascriptBridge和相关bridge调用的方法。

    // 初始化Bridge对象,OC可以通过WebViewJavascriptBridge来调用JS里面的各种方法
    window.WebViewJavascriptBridge = {
    registerHandler: registerHandler, // JS中注册方法
    callHandler: callHandler, // JS中调用OC的方法
    disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
    _fetchQueue: _fetchQueue, // 把消息转换成JSON串
    _handleMessageFromObjC: _handleMessageFromObjC // OC调用JS的入口方法
    };

    WebViewJavascriptBridge对象里核心的方法有:


    • registerHandler:JS中注册方法
    • callHandler: JS中调用native的方法
    • _fetchQueue: 把消息转换成JSON字符串
    • _handleMessageFromObjC:native调用JS的入口方法

    当初始化完成后,WebViewJavascriptBridge对象和对象里的方法就已经存在并且可用了。


    JS和native是如何相互传递消息的呢?从上面的代码中可以看到如果JS想要发送消息给native就会调用callHandler方法;如果native想要调用JS方法那JS侧就必须先注册一个registerHandler方法。


    相对应的我们看一下native侧是如何与JS传递消息的,其实接口标准是一致的,native调JS的方法使用callHandler方法:

    id data = @{ @"dataFromOC": @"aaaa!" };
    [_bridge callHandler:@"OCToJSHandler" data:data responseCallback:^(id response) {
    NSLog(@"JS回调的数据是:%@", response);
    }];

    JS调native方法在native侧就必须先注册一个registerHandler方法:

        // 注册事件(h5调App)
    [_bridge registerHandler:@"JSTOOCCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
    NSLog(@"JSTOOCCallback called: %@", data);
    responseCallback(@"Response from JSTOOCCallback");
    }];

    也就是说native像JS发送消息的话,JS侧要先注册该方法registerHandler,native侧调用callHandler;
    JS像native发送消息的话,native侧要先注册registerHandler,JS侧调用callHandler。这样才能完成双端通信。


    如图:




    native向JS发送消息


    现在要从native侧向JS侧发送一条消息,方法名为:"OCToJSHandler",并且拿到JS的回调,具体实现细节如下:


    JS侧


    native向JS发送数据,首先要在JS侧去注册这个方法:

    bridge.registerHandler('OCToJSHandler', function(data, responseCallback) {
    ...
    })

    这个registerHandler的实现在WebViewJavascriptBridge_JS是:

    // web端注册一个消息方法,将注册的方法存储起来
    function registerHandler(handlerName, handler) {
    messageHandlers[handlerName] = handler;
    }

    就是将这个注册的方法存储到messageHandlers这个map中,key为方法名称,value为function(data, responseCallback) {}这个方法。


    native侧


    native侧调用bridge的callHandler方法,传参为data和一个callback回调

    id data = @{ @"dataFromOC": @"aaaa!" };
    [_bridge callHandler:@"OCToJSHandler" data:data responseCallback:^(id response) {
    NSLog(@"JS回调的数据是:%@", response);
    }];

    接下来会走到WebViewJavascriptBridgeBase的-sendData: responseCallback: handlerName:方法,该方法中将"data"和"handlerName"存入到一个message字典中,如果存在callback会生成一个callbackId一并存入到message字典中,并且将该回调存入到responseCallbacks中,key为callbackId,value为这个callback。代码如下:

    // 所有信息存入字典
    NSMutableDictionary* message = [NSMutableDictionary dictionary];
    if (data) {
    message[@"data"] = data;
    }
    if (responseCallback) {
    NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
    self.responseCallbacks[callbackId] = [responseCallback copy];
    message[@"callbackId"] = callbackId;
    }
    if (handlerName) {
    message[@"handlerName"] = handlerName;
    }
    [self _queueMessage:message];

    将message存储到队列等待执行,执行该条message时会先将message进行序列化,序列化完成后将message拼接到字符串WebViewJavascriptBridge._handleMessageFromObjC('%@');中,然后执行_evaluateJavascript执行该js方法。

    // 把OC消息序列化、并且转化为JS环境的格式,然后在主线程中调用_evaluateJavascript
    - (void)_dispatchMessage:(WVJBMessage*)message {
    NSString *messageJSON = [self _serializeMessage:message pretty:NO];
    NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
    [self _evaluateJavascript:javascriptCommand];
    }

    _handleMessageFromObjC方法会将messageJSON传递给_dispatchMessageFromObjC进行处理。
    首先将messageJSON进行解析,根据handlerName取出存储在messageHandlers中的方法。如果该message中存在callbackId,将callbackId作为参数生成一个回调放到responseCallback中。
    代码如下:

    function _doDispatchMessageFromObjC() {
    // 解析发送过来的JSON
    var message = JSON.parse(messageJSON);
    var messageHandler;
    var responseCallback;

    // 主动调用
    // 如果有callbackid
    if (message.callbackId) {
    // 将callbackid当做callbackResponseId再返回回去
    var callbackResponseId = message.callbackId;
    responseCallback = function(responseData) {
    // 把消息从JS发送到OC,执行具体的发送操作
    _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
    };
    // 获取JS注册的函数,取出消息里的handlerName
    var handler = messageHandlers[message.handlerName];
    // 调用JS中的对应函数处理
    handler(message.data, responseCallback);
    }
    }

    handler方法其实就是名为"OCToJSHandler"的方法,这时就走到了registerHandler里的那个function(data, responseCallback) {}方法了。我们看一下方法内部的具体实现:

    bridge.registerHandler('OCToJSHandler', function(data, responseCallback) {
    // OC中传过来的数据
    log('从OC传过来的数据是:', data)
    // JS返回数据
    var responseData = { 'dataFromJS':'bbbb!' }
    responseCallback(responseData)
    })

    data就是从native传过来的数据,responseCallback就是保存的回调,然后又生成了新数据作为参数给到了这个回调。


    responseCallback的实现是:

    responseCallback = function(responseData) {
    // 把消息从JS发送到OC,执行具体的发送操作
    _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
    };

    将该方法的handlerName、生成的callbackResponseId(也就是callbackId)以及JS返回的数据一起给到_doSend方法。


    _doSend方法将message存储到sendMessageQueue消息列表中,并使用messagingIframe加载了一次https://wvjb_queue_message

    // 把消息从JS发送到OC,执行具体的发送操作
    function _doSend(message, responseCallback) {
    // 把消息放入消息列表
    sendMessageQueue.push(message);
    // 发出js对oc的调用,让webview执行跳转操作,可以在decidePolicyForNavigationAction:中拦截到js发给oc的消息
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
    }

    这时webview的监听方法decidePolicyForNavigationAction监听到了https://wvjb_queue_message 消息后还是执行WebViewJavascriptBridge._fetchQueue()去取数据,取到数据后根据responseId当初在_responseCallbacks中存储的callback,然后执行callback、移除responseCallbacks中的数据。到此为止,整个native向JS发送消息的过程就完成了。


    总结:


    1. JS中先调用registerHandler将方法存储到messageHandlers中
    2. native调用callHandler:方法,将消息内容存储到message中,回调存储到responseCallbacks中。
    3. 将message消息序列化通过_evaluateJavascript方法执行_handleMessageFromObjC
    4. 将message解析,通过message.handlerName从messageHandlers取出该方法;根据message.callbackId生成回调
    5. 执行该方法,回调

    JS向native发送消息


    从JS向native发消息其实和native向JS发消息的接口层面是差不多的。


    native侧


    native侧首先要注册一个JSTOOCCallback方法

    [_bridge registerHandler:@"JSTOOCCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
    responseCallback(@"Response from JSTOOCCallback");
    }];

    该方法也同样是将该方法的callback存储起来,存储到messageHandlers当中,key就是方法名"JSTOOCCallback",value就是callback。


    JS侧


    JS侧会调用callHandler方法:

    // 调用oc中注册的那个方法
    bridge.callHandler('JSTOOCCallback', {'foo': 'bar'}, function(response) {
    log('JS 取到的回调是:', response)
    })

    这个callHandler方法同样会调用_doSend方法:将callback存储到responseCallbacks中,key为callbakid;将消息存储到sendMessageQueue中;messagingIframe执行https://wvjb_queue_message


    native的decidePolicyForNavigationAction方法监听到该消息后同样通过WebViewJavascriptBridge._fetchQueue()去取消息。


    根据callbackId创建一个responseCallback,根据message的handlerName从messageHandlers取出该回调,然后执行:

    WVJBResponseCallback responseCallback = NULL;
    NSString* callbackId = message[@"callbackId"];
    if (callbackId) {
    responseCallback = ^(id responseData) {
    if (responseData == nil) {
    responseData = [NSNull null];
    }
    WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
    [self _queueMessage:msg];
    };
    } else {
    responseCallback = ^(id ignoreResponseData) {
    // Do nothing
    };
    }

    WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];

    handler(message[@"data"], responseCallback);

    调用完这个方法后,该消息已经收到,然后将回调的内容回调给JS。
    通过上面的代码可以看到,回调JS的内容就是callbackId和responseData生成的message,调用_queueMessage方法。


    _queueMessage方法上面已经看过了,就是序列化消息、加入队列、执行WebViewJavascriptBridge._handleMessageFromObjC('%@');方法。


    JS收到该消息后,处理返回的消息,从responseCallbacks中根据message中的responseId取出callback并且执行。最后删除responseCallbacks中的数据,JS向native发送数据就完成了。


    总结:


    1. native侧调用registerHandler方法注册方法,方法名为JSTOOCCallback,将消息存储到messageHandlers中,key为方法名,value为callback。
    2. JS侧调用callHandler方法:将responseCallback存储到responseCallbacks中;将message存储到sendMessageQueue中;messagingIframe执行 http://wvjb_queue_message
    3. native侧监听到该消息后调用WebViewJavascriptBridge._fetchQueue()去取数据
    4. 根据handlerName从messageHandlers中取出该callback;根据callbackId创建callback对象作为参数放到handlerName的方法中;执行该回调。

    总结


    综上,WebViewJavascriptBridge的核心流程就分析完了,最核心的点是JS通过加载iframe来通知native侧;native侧通过evaluateJavaScript方法去执行JS。


    从整个SDK来看,设计的非常好,值得借鉴学习:


    • 使用外观模式统一调用接口,比如初始化WebViewJavascriptBridge的时候,不需要关心使用方使用的是UIWebView还是WKWebView,内部已经处理好了。
    • 接口统一,不管是native侧还是JS侧,调用方法就是callHandler、注册方法就是registerHandler,不需要关注内部实现,使用非常方便。
    • 代码简洁,逻辑清晰,层次分明。从类的分布就能很清晰的看出各自的功能是什么。
    • 职责单一,比如decidePolicyForNavigationAction方法只负责监听事件、_fetchQueue是负责把消息转换成JSON字符串返回、_doSend是发送消息到native、_dispatchMessageFromObjC是负责处理从OC返回的消息等。虽然decidePolicyForNavigationAction也能接收消息,但这样就不会这么精简了。
    • 扩展性好,目前decidePolicyForNavigationAction虽然只有初始化和发消息两个事件,如果有其他事件还可以再扩展,这也得益于方法设计的职责单一,扩展对原有方法影响会很小。

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

    程序员如何把控自己的职业(陈皓--左耳朵耗子)

    背景 最近,很多小伙伴留言说自己比较焦虑和迷茫。 这让我想起来之前看的 陈皓老师 的文章,这篇文章帮助我很多,也希望它能帮助更多的人。 因此转发~~ 正文 文章转发自《陈皓(左耳朵耗子)》:coolshell.cn/articles/20… 这篇文章的主要...
    继续阅读 »

    背景


    最近,很多小伙伴留言说自己比较焦虑和迷茫。


    这让我想起来之前看的 陈皓老师 的文章,这篇文章帮助我很多,也希望它能帮助更多的人。


    因此转发~~


    正文



    文章转发自《陈皓(左耳朵耗子)》:coolshell.cn/articles/20…



    这篇文章的主要内容主要是我今年3月份在腾讯做的直播,主要是想让一些技术人员对世界有一个大体的认识,并且在这个认识下能够有一个好的方法成就自己。而不是在一脸蒙圈的状态下随波逐流,而日益迷茫和焦虑。直播完后,腾讯方面把我的直播形成文字的形式发了出来,我觉得我可以再做一个精编版。所以,有了这篇文章,希望对大家有帮助。


    对我来说,在我二十多年的工作经历来看,期间经历了很多技术的更新换代,整个技术模式、业务模式也是一直变来变去,我们这群老程序员成长中所经历的技术比今天的程序员玩的还更杂更多。我罗列一下我学过的,而且还被淘汰掉的技术,大家先感受一下。

    - MIS应用开发:FoxPro,PowerBuilder,Delphi
    - OA:Lotus Notes,VBScripts
    - 微软:ODBC/ADO,COM/DCOM,MFC/ATL,J++
    - 服务器:AIX,HP-UX,SCO Unix
    - Web:CGI,ISAPI,SOAP
    - RPC:CICS,Tuxedo
    - J2EE:Websphere,Weblogic
    - DB:Sybase,Informix

    我想说的是,无论过去还是今天,我们这些前浪和你们后浪所面对的技术的挑战和对技术的焦虑感是相似的,我们那个时候不但玩996,还玩封闭开发(就是一周只能回家一天)。当然,唯一好的东西,就是比起今天的程序员来说,我们那个年代没有像微信、微博、知乎,抖音这些巨大消耗你人生的东西,所以,我们的工作、生活和成长都有很效率,不会被打断、喜欢看书、Google还没有被封……当然,那时代没有StackOverlow和Github这样的东西,所以,能完成的东西或质量都一般。


    当然,这里并不是想做一个比较,只是想让大家了解一下两代程序员间的一些问题各有千秋,大同小异。在整个成长过程中,其实有很多东西是相通的,其本上来说,就是下面的三件事——


    第一,如果想要把控技术,应对这个世界的一些变化,需要大致知道这个世界的一些规律和发展趋势,另外还得认识自己,自己到底适合做什么?在这个趋势和规律下属于自己的发挥领域到底是什么?这是我们每个人都需要了解的。


    第二打牢基础,以不变应万变,不管世界怎样变化,我都能很快适应它。基础的重要程度对于你能够飞多高是相当有影响的,懂原理的人比不懂原理的人能做出来的事情或是能解决的问题完全是两个层级的。


    第三,提升成长的效率,因为现在社会的节奏实在太快了,比二十年前快得太多,技术层出不穷,所以我们的成长也要更有效率。效率并不单指的快,效率是怎么样更有效,是有用功除以总功(参看《加班与效率》),怎么学到更有效的东西,或者怎么更有效学习,是我们需要掌握的另一关键。


    下面是我这多年来的一些认识,希望对你有帮助。


    目录


    世界发展趋势


    我个人经历的信息化革命应该分成三个阶段:


    • 1990年代到2000年,这个时代MB时代,是雅虎、新浪、搜狐、网易门户网站的时代,这个时代就是ISP/ICP互联网提供商,把一些资讯数字化,然后发布到网络上。
    • 2000年到2010年,这个时代叫GB时代,或是叫多媒体或UGC时代,上网开始变得普遍了,每个人手里的数码设备开始变得多了起来,可以上传照片,可以上传视频,甚至可以在网上做社交。
    • 2010年到2020年,这个时代叫TB时代,这过去的十年是移动互联网时代,移动互联网只需要手机在线,不需要依靠电脑。因为手机随时在线,所以个人的各种各样的数据始终在被收集,只要用户上网就会产生数据,所以人的行为最终也被数字化了。

    所有的硬件和软件都是跟着需要处理的数据而演进的,我们需要更大的带宽,更大的硬盘,更多的处理器……大到一定时候就只能进入分布式化的技术架构了,再大,数据中心也顶不住了,就会要引入更为分布式的边缘计算了。


    另一方面,从业务上来看,我们可以看到整个世界就在不断地进行数字化,因为,只要数字化了,就可以进行复制传播和计算,只要可以进行计算了,就可以进行数学建模,就可以自动化,只要可以自动化了就可以规模化,只要可能规模化了,就可以改变整个行业。人类的近代史的大趋势基本上都是在解决能源和自动化的事,源源不断的能源是让机器不知疲倦的前提条件,用机器代替牲口,代替人类进行工作是规模化的前提条件。


    所以,技术的演进规律基本是自动化加规模化,从而降低成本,提升效率。这就是为什么世界变得越来越快,人类都快跟不上节奏的原因,主要是整个社会不断被机器、数据所驱动。


    人才需求


    在这个过程中,需要什么样的人?下面是我的一些认识——


    • 技工,在机器和自动化面前,肯定是需要能够操作机器的技术工人了,这类人是有技术的劳动力。在编程的圈子里俗称“码农”,他们并不是真正的工程师,他们只是电脑程序的操作员,所以,随着技术门槛的下降或是技术形式的变更他可能就会变得越来越不值钱,直到被淘汰掉
    • 特种工,这种人是必须了解原理和解决难题的一类人,他们是解决比较难的、特定的一些技术问题。当一种技术被淘汰,他并不容易被淘汰,因为他懂原理,原理就是解决问题的能力,是解决问题的套路和方法
    • 工程师,不但是使用技术,还可以把活儿做好,他们认为代码更多的时间是在维护,这些人使用各种各样的手段和各种技术,精益求精地持续不断地提高代码的易读性、扩展性、可维护性和重用性,这个过程似乎永无止境。对于这些有“洁癖”,有“工匠精神”,有“修养”的技术人员,我们称他们为工程师。这种人做事又稳又快,而且可以做出很多称手的工具和方法论
    • 再往上是设计师和架构人员,这些人主要是开发一些工具,框架,模式,提升软件开发和维护效率,同时也提升用户体验,和提升稳定性、性能、代码重用等,总的来说就是为了降本增效。这类人的工作降低了技术得到门槛,他们把技术门槛降低了以后,就可以把这个技术普及开来,就可以由广大劳工、技工、特殊工人使用了。
    • 还有一类人是经理,经理主要是组织团队、完成项目、创造利润。这类人中,即有身先士卒的leader,也有高高在上的boss,但无论怎么样,这些人只不过是为了让一个公司或是一个团队更好组织在一起的“粘合剂”,这类人只有在大公司中才会变成更有价值。

    这就是我总结的世界需要哪些人才,我们了解这些东西以后大概就明白我们现在所处的位置有什么样的问题,我们应该去什么样的地方。


    Google评分卡


    接下来,我们再来看看Google的SRE的自我评分卡:



    0 – 对于相关的技术领域还不熟悉

    1 – 可以读懂这个领域的基础知识

    2 – 可以实现一些小的改动,清楚基本的原理,并能够在简单的指导下自己找到更多的细节。


    3 – 基本精通这个技术领域,完全不需要别人的帮助

    4 – 对这个技术领域非常的熟悉和舒适,可以应对和完成所有的日常工作。



    • 对于软件领域 – 有能力开发中等规模的程序,能够熟练和掌握并使用所有的语言特性,而不是需要翻书,并且能够找到所有的冷知识。

    • 对于系统领域 – 掌握网络和系统管理的很多基础知识,并能够掌握一些内核知识以运维一个小型的网络系统,包括恢复、调试和能解决一些不常见的故障。


    5 – 对于该技术领域有非常底层的了解和深入的技能。


    6 – 能够从零开发大规模的程序和系统,掌握底层和内在原理,能够设计和部署大规模的分布式系统架构

    7 – 理解并能利用高级技术,以及相关的内在原理,并可以从根本上自动化大量的系统管理和运维工作。

    8 – 对于一些边角和晦涩的技术、协议和系统工作原理有很深入的理解和经验。能够设计,部署并负责非常关键以及规模很大的基础设施,并能够构建相应的自动化设施


    9 – 能够在该技术领域出一本经典的书。并和标准委员会的人一起工作制定相关的技术标准和方法。

    10 – 在该领域写过一本书,被业内尊为专家,并是该技术的发明人。



    SRE需要自评如下这些技术或技能。



    – TCP/IP Networking (OSI stack, DNS etc)

    – Unix/Linux internals

    – Unix/Linux Systems administration

    – Algorithms and Data Structures

    – C/C++

    – Python

    – Java

    – Perl

    – Go

    – Shell Scripting (sh, Bash, ksh, csh)

    – SQL and/or Database Admin

    – Scripting language of your choice (not already mentioned) _____________

    – People Management

    – Project Management



    这个评分卡是面试Google前需要候选人对自己的各种技术进行自评,也算是一种技术人员的等级的度量尺,其把技术的能分成11个等级,我用颜色把其它成四大层级,希望这个评份卡能够给你一个能力提升的参考标准。


    认识自己


    认识了世界是怎么发展的,也知道技术人员的种类和层级,那么还要了解一下自己,因为如果不了解自己,那么你也无法找到自己的路和适合自己的地方。


    我觉得,一个人要认识自己就需要认识自己的特长、兴趣、热情、擅长等,下面是一个认识自己的标准方法:


    • 特长。首先你要找得到自己特长。你要认识自己的特长,找到自己的天赋,找到你在DNA里比别人强的东西,就拿你的DNA跟别人竞争就好了。所以你要找到自己可以干成的事,找到别人找你请教的事,你身边人找你请教就是说明你有特长。这是找到自己特长非常非常重要,扬长避短。
    • 兴趣。如果你没有找到自己特长,就找自己有兴趣有热情的东西。什么叫兴趣?兴趣是再难再累都不会放弃的事。如果你遇到困难就会放弃不叫兴趣,那叫叶公好龙。不怕困难,痴迷其中,就算你没有特长,有了这种特质,你也是头部的人才。
    • 方法。如果你没有特长,没有兴趣和热情就要学方法。这种方法就是要有时间观念,要会做计划,要懂统筹、规划对于做过的事情,犯过的错误多总结,举一反三,喜欢自己找答案,自己探究因果关系,这是一些方法,自己总结一些套路。
    • 勤奋。 如果你没有特长,没有兴趣,也没有方法,你还能做的事就是勤奋,勤奋注定会让你成为一个比较劳累的人,也是很有可能被淘汰的人随着你的年纪越来越大,你的勤奋也会越来越不值钱。因为年轻人会比你更勤奋,比你更勤奋、比你斗志更强,比你能力更强,比你要钱更少的人会出现。勤奋最不值钱,但是只要你勤奋至少能够自食其力。

    以上就是为了应对未来技术变化,作为个人必须要从特长、兴趣、方法一层一层筛选挖掘,如果没有这些你就要努力和勤奋。就只能接受“福报”了


    从我个人而言,我不算是特别聪明的人,但自认为对技术还是比较感兴趣的,难的我不怕。有很多比较难啃的技术,聪明点的人啃一个月就懂了,我不行,我可能啃半年。但是没有关系,知识都是死的,只要不怕困难总有一天会懂的。最可怕是畏难,为自己找借口,这样就不太好了。


    打好基础


    最前面提到我学的各式各样的被淘汰的技术,会让你感觉很迷茫,或是迷失。但前面也提到了“谷歌评分卡”,在这个评分卡中,我们看到了许多基础原理方面的内容,其实要应对未来的变化,很重要的一点就是无招胜有招,以不变应万变。


    变化都是表面的东西,内在的东西其实并没有太多的变化。理论层面上变得不多,反而形式上的东西今天一个花样,明天一个花样,所以如果要去应对这种变化,就一定要打牢自己的基础,提升内功修养。比如像编程的一些方式和套路,修饰模式原理本质,解耦,提升代码的重用度等。提升代码重用度必须解耦,要跟现实解耦,提升抽象,这些都是一些技术基础。无论用什么语言,都是这么做的。


    打牢基础就可以突破瓶颈,不打牢基础没有办法突破瓶颈。在技术世界不要觉得量变会造成质变,这是不可能的。技术这个东西就像搞建筑砌砖头,砌砖头砌的再多也不可能让你能成为一个架构师的,因为你不懂原理,不懂科学方法,你就不可能成长上去的,就像学数学一样,当你掌握了微积分这种大杀器后,你解题的能力是无所披靡,而微积分这种方式绝对不是你能“量变”出来的。


    所以你必须学习基础的理论知识,如果不学这些基础理论知识,还要学习解题思路和方法,如果你只学在表面,那么当这个技术的形式有变化,就会发现以前学的都没用了,要重头学一遍。掌握技术基础可以让自己找到答案和知识,基础是抽象和归纳,很容易形成进一步的推论。我们学的很多技术实现都逃不脱基础原理,不管是Java,还是其他语言,只要用TCP用的都是相同的原理,逃不出范围,只要抓住原理,举一反三,时间一长了,甚至还可以自己推导答案。对于技术的基础,我会把其它成四类:


    • 程序语言:语言的原理,类库的实现,编程技术(并发、异步等),编程范式,设计模式……
    • 系统原理:计算机系统,操作系统,网络协议,数据库原理……
    • 中间件:消息队列,缓存系统,网关代理,调度系统 ……
    • 理论知识:算法和数据结构,数据库范式,网络七层模型,分布式系统……

    这些知识其实就是一个计算机科学专业的学生他所要学习的原理,但可惜的是,我们的一些学校教得也很糟糕,不但老师能力不足,而且放着世界上最优秀的教课书不用了,一定要自己写一本。讲也讲不全,还有各种错误,哎……总之,如果你学习用用到的教材不行,那么可以肯定的是你的学习效率一定是很糟糕的。这就是为什么我们大学上完了,还是跟个傻瓜一样,还要在工作中再重新自学。


    不过,就算自学,这些基础技术大概需要四五年的时间堆叠。我工作二十年了,这二十年来基本还是这些原理没变,无论形式怎么变,但是核心永远还是这些,理论创新很难,这是以不变应万变


    学习效率


    谈到学习效率,就需要拿出这张学习金字塔的图来了。从图可以看到学习方法分布两层,一种是被动学习,也是浅度学习,听讲,阅读,视听,演示都是在被动学习,而与人讨论,自己动手实践,教授给别人是主动学习。主动学习我们称之为深度学习,如果你不能深度学习,你就不能真正学到东西。这也是你会经常有“学那么多干什么,不用就忘了”,这就是浅度学习的症状了。


    下面,我给出一些我自己觉得不错的学习经验:


    1、挑选一手知识和信息源。 对于学习方法:第一我们一定要到知识源去挑选知识,知识信息源非常关键,二手信息丢失太大了,谭浩强写的书就丢失太多信息了。目前计算机一手知识基本都是国外的,所以英文非常重要。我鼓励大家一定读第一手的资料。如果你英语有问题,至少要看翻译过来,最好是原汁原味翻译的,不要我理解了给你讲那种,那种也是被别人嚼一遍再讲给你你没有体会,是别人带着你,别人的体会会影响你,也许你的体会会比他更好,因为是你自己总结出来的东西,所以知识源很重要。


    2、注意原理和基础 第二要注重基础原理。虽然可以忘记这个技术,但是原理记在心里,我可以徒手实现出来,而且通过原理可以更快学习其他类似的技术。所以原理很重要!当你学会C、C++要学Java和GO都很快。


    3、使用知识图谱 一定要学会使用知识图,把知识结构化。从一个技术关键点开始不断地关联和细化下去,比如:关于TCP协议,首先第一个要记住状态图,怎么建立连接,怎么断连接,状态怎么变迁。TCP没有连接,是靠状态维护连接的。其次,要了解TCP怎么保证可靠性,就是丢包以后怎么重传,重传有哪些技术点。然后,重传会让你联想到拥塞控制,拥塞控制到滑动窗口……。这基本就是TCP的所有东西了,找到关键点,然后顺着这个脉络一点点往下想,通过知识图关联就可以进行顺藤摸瓜。我们不需要记所有知识,那些手册的知识不需要记,你知道在哪里能找到就可以了。你脑子里面要有地图,学一个东西就跟在城市生活一样,闭上眼睛就知道地图,A点到B点怎么去大概方向要知道。我在北京我去广州,广州在南边,我大概坐飞机还是火车要心里有数。。


    4、学会举一反三。就是用不同方法学一个东西,比如说学TCP协议,看书是一种方法,编程是另外一种方法,还有用做Debug去看的,用不同方法学一个东西会让你更加熟悉,你学一个知识的同时把周边也学了。比如说学前端能不能把HTTP学一下,比如说长连接、短连接,包括hp1、hp2有一些不一样的东西。


    5、总结和归纳。 只有学会总结和归纳,才能形成自己的思维框架、自己的套路、自己的方法论,以后学这个东西应该怎么学。就像学一门新的语言,不管GO语言,还是Rust语言,第一件事情就是了解内存是怎么管理的,数据类型什么样,第二是泛型怎么搞,第三是并发怎么弄。还有一些抽象怎么弄,比如说怎么解耦,怎么实现多态?套路这种东西只有学的多了以后才能形成套路,如果你只学会一门语言不会有套路,你要每年学门语言,不用学多精,你思考这个语言有什么不一样,为什么这个这种有玩法,那个有那种玩法,这些东西思考多了套路方法论就出来了。比如说Windows和Linux有什么不同,Linux和Unix又有什么不同?只有总结自己的框架、套路和方法,这些才永远不会被淘汰。


    6、实践和坚持。 剩下就是多做多练,多坚持,只有实践才会有经验,只有锻炼了才能够把自己的脂肪变没,所以,要把知识变成技能必须练,就像小学生学会加减乘除,还是要演练,必须多做题,题目做得多了,自然掌握得好。要挑选好的知识源,注重原理技术,有一些原理的基础的书太枯燥,但是我告诉你学习这些基础太值得投入时间,搬砖赚几十元不值得,因为赚的是辛苦钱,老了就赚不了,必须要赚更有能力的钱,这是学习投资。


    小结


    好了,该到这篇文章收尾的时候了,小结一下,如果你想更好的把握时代,提升自己,你需要知道这个时代的趋势是什么,需要什么样的人,这些人需要什么样的能力,这些能力是怎么获得的,投入到基础知识的学习就像“基建”一样,如果基础不好,不能长高,学习能力也是需要适应这个快速时代的重要的基础能力,没有好的学习能力,很快就会掉队被淘汰。


    这些东西,是我从业二十年来的总结和体会,希望对你有用。


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

    世界那么大,我并不想去看看

    回家自由职业有一个半月了,这段时间确实过的挺开心的。 虽然收入不稳定,但小册和公众号广告的收入也足够生活。 我算过,在小县城的家里,早饭 10 元,午饭 60元(按照偶尔改善生活,买份酸菜鱼来算),晚饭 10 元,牛奶零食等 20 元,一天是 100 元。 一...
    继续阅读 »

    回家自由职业有一个半月了,这段时间确实过的挺开心的。


    虽然收入不稳定,但小册和公众号广告的收入也足够生活。


    我算过,在小县城的家里,早饭 10 元,午饭 60元(按照偶尔改善生活,买份酸菜鱼来算),晚饭 10 元,牛奶零食等 20 元,一天是 100 元。


    一年就是 36500 元。


    加上其他的支出,一年生活费差不多 5w 元。


    10 年是 50w,40 年就是 200 万元。


    而且还有利息,也就是说,我这辈子有 200 万就能正常活到去世了。


    我不会结婚,因为我喜欢一个人的状态,看小说、打游戏、自己想一些事情,不用迁就另一个人的感受,可以完全按照自己的想法来生活。


    并不是说我现在有 200 万了,我的意思是只要在二三十年内赚到这些就够了。


    我在大城市打工,可能一年能赚 50 万,在家里自由职业估计一年也就 20 万,但这也足够了。


    而且自由职业能赚到最宝贵的东西:时间。


    一方面是我自己的时间,我早上可以晚点起、晚上可以晚点睡、下午困了可以睡个午觉,可以写会东西打一会游戏,不想干的时候可以休息几天。


    而且我有大把的时间可以来做自己想做的事情,创造自己的东西。


    一方面是陪家人的时间,自从长大之后,明显感觉回家时间很少了,每年和爸妈也就见几天。


    前段时间我爸去世,我才发觉我和他最多的回忆还是在小时候在家里住的时候。


    我回家这段时间,每天都陪着我妈,一起做饭、吃饭,一起看电视,一起散步,一起经历各种事情。


    我买了个投影仪,很好用:



    这段时间我们看完了《皓镧传》、《锦绣未央》、《我的前半生》等电视剧,不得不说,和家人一起看电视剧确实很快乐、很上瘾。


    再就是我还养了只猫,猫的寿命就十几年,彼此陪伴的时间多一点挺好的:



    这些时间值多少钱?没法比较。


    回家这段时间我可能接广告多了一点,因为接一个广告能够我好多天的生活费呢。


    大家不要太排斥这个,可以忽略。


    其实我每次发广告总感觉对不起关注我的人,因为那些广告标题都要求起那种博人眼球的,不让改,就很难受。



    小册的话最近在写 Nest.js 的,但不只是 nest。


    就像学 java,我们除了学 spring 之外,还要学 mysql、redis、mongodb、rabbitmq、kafka、elasticsearch 等中间件,还有 docker、docker compose、k8s 等容器技术。


    学任何别的后端语言或框架,也是这一套,Nest.js 当然也是。


    所以我会在 Nest.js 小册里把各种后端中间件都讲一遍,然后会做 5 个全栈项目。


    写完估计得 200 节,大概会占据我半年的时间。


    这段时间也经历过不爽的事情:





    这套房子是我爸还在的时候,他看邻居在青岛买的房子一周涨几十多万,而且我也提到过可能回青岛工作,然后他就非让我妈去买一套。


    当时 18 年青岛限购,而即墨刚撤市划区并入青岛,不限购,于是正好赶上房价最高峰买的。


    然而后来并没有去住。


    这套房子亏了其实不止 100 万。


    因为银行定存利息差不多 4%,200 万就是每年 8万利息,5年就是 40万。


    但我也看开了,少一百万多一百万对我影响大么?


    并不大,我还是每天花那些钱。


    相比之下,我爸的去世对我的打击更大,这对我的影响远远大于 100 万。


    我对钱没有太大的追求,对很多别人看重的东西也没啥追求。


    可能有的人有了钱,有了时间会选择环游中国,环游世界,我想我不会。


    我就喜欢宅在家里,写写东西、看看小说、打打游戏,这样活到去世我也不会有遗憾。


    我所追求的事情,在我小时候可能是想学医,一直觉得像火影里的纲手那样很酷,或者像大蛇丸那样研究一些东西也很酷。


    但近些年了解了学医其实也是按照固定的方法来治病,可能也是同样的东西重复好多年,并不是我想的那样。


    人这一辈子能把一件事做好就行。


    也就是要找到自己一生的使命,我觉得我找到了:我想写一辈子的技术文章。


    据说最高级的快乐有三种来源:自律、爱、创造。


    写文章对我来说就很快乐,我想他就是属于创造的那种快乐。


    此外,我还想把我的故事写下来,我成长的故事,我和东东的故事,那些或快乐或灰暗的日子,今生我一定会把它们写下来,只是现在时机还不成熟。


    世界那么大,我并不想去看看。


    我只想安居一隅,照顾好家人,写一辈子的技术文章,也写下自己的故事。


    这就是我的平凡之路。


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

    【JD京东抢购】茅台抢购逻辑

    直接进入正文。京东抢购模式有很多种。 普通商品无货,定时查询库存蹲抢普通商品定时发售(库存由0变为有货),定时提前构造订单请求抢预售商品(需要先预约),可以加入购物车,通过购物车结算。这种用常规购物车结算订单接口就行,当然也可以用抢购接口。 这种体现为可以加...
    继续阅读 »

    直接进入正文。京东抢购模式有很多种。


    1. 普通商品无货,定时查询库存蹲抢
    2. 普通商品定时发售(库存由0变为有货),定时提前构造订单请求抢
    3. 预售商品(需要先预约),可以加入购物车,通过购物车结算。这种用常规购物车结算订单接口就行,当然也可以用抢购接口。


    这种体现为可以加购,抢购时候显示两个按钮,加入购物车(黄色)和立即购买(淡绿色)。



    • 预售商品(需要先预约),无法加入购物车,电脑端无法预约,必须手机端预约。这种采用marathon.jd.com/seckillnew/… 接口完成抢购,有完整流程验证和tokenKey(sign),sk验证。


    这种体现为 无法加入购物车,必须手机端才能预约,可购买时候只显示一个红色按钮立即抢购



    逻辑参考GitHub大佬给出的思路。


    第一步:获取跳转链接


    跳转链接是指形如:un.m.jd.com/cgi-bin/app… 的链接,获取该链接,还需要一个前置步骤,即获取token和拼接url。先说获取token,获取token是通过genToken接口获取的,然后将获取到的tokenKey和url拼接起来,得到跳转链接。


    第二步:访问跳转链接


    拿到跳转链接后,直接将该跳转链接仍给浏览器即可,浏览器会经过两次302跳转得到sekill.action链接,从而渲染出提交订单页面,此时我们需要模拟点击“提交订单”按钮,实现抢购。(可以使用Selenium、Pyppeteer或Playwright等类库 来模拟浏览器)


    访问跳转连接,及提交订单的时候需要提供移动端的APP参数抓包获取。Android抓包较为简单,IOS的也不麻烦,就是步骤多了一些。


    然后提取Hades头的信息组成以下参数

            query_params = {
    "functionId": "genToken",
    "clientVersion": "12.0.8",
    "build": "168782",
    "client": "apple",
    "d_brand": "apple",
    "d_model": "iPhone11,4",
    "osVersion": "16.5",
    "screen": "1284*2778",
    "partner": "apple",
    "aid": self.aid,
    "eid": self.eid,
    "sdkVersion": "29",
    "lang": "zh_CN",
    # 'harmonyOs': '0',
    "uuid": self.uuid,
    "area": "4_51026_58465_0",
    "networkType": "wifi",
    "wifiBssid": self.wifiBssid,
    "uts": self.uts,
    "uemps": "0-0-0",
    "ext": '{"prstate":"0","pvcStu":"1"}',
    # 'ef': '1',
    # 'ep': json.dumps(ep, ensure_ascii=False, separators=(',', ':')),
    }

    这种仅仅是前面所需的参数。具体方法还是需要使用这些参数来获取用户的个人信息拿到跳转连接


    如:

        def get_appjmp(self, token_params):
    headers = {"user-agent": self.ua}
    appjmp_url = token_params["url"]
    params = {
    "to": "https://divide.jd.com/user_routing?skuId=%s" % self.skuId,
    "tokenKey": token_params["tokenKey"],
    }

    response = self.s.get(
    url=appjmp_url,
    params=params,
    allow_redirects=False,
    verify=False,
    headers=headers,
    )
    print("Get Appjmp跳转链接-------------->%s" % response.headers["Location"])
    return response.headers["Location"]

    get_appjmp(self, token_params) 函数接受一个名为 token_params 的参数。


    然后发送相关请求后携带参数得到跳转链接。

    • headers 是一个字典,包含了请求头中的 "User-Agent" 字段,用于模拟浏览器的用户代理。

    • appjmp_url 是一个变量,它存储了 token_params 字典中的 "url" 键所对应的值。

    • params 是一个字典,其中包含两个键值对:

    • "to" 键对应的值是一个字符串,使用了 %s 占位符,用于生成跳转链接中的 skuId 参数。

    • "tokenKey" 键对应的值是一个字符串,使用了 token_params 字典中的 "tokenKey" 键所对应的值。

    • 通过调用 self.s.get() 方法发起一个 GET 请求,传入以下参数:

    • url 参数是 appjmp_url,表示要访问的链接地址。

    • params 参数是之前定义的 params 字典,用于添加请求参数。

    • allow_redirects 参数设置为 False,禁止自动重定向。

    • verify 参数设置为 False,跳过 SSL 证书验证。

    • headers 参数是之前定义的 headers 字典,用于设置请求头。

    • 最后,打印获取到的跳转链接的响应头中的 "Location" 字段值,并将其返回。


    抢购返回解决无非就是



    {'errorMessage': '很遗憾没有抢到,再接再厉哦。', 'orderId': 0, 'resultCode': 90016, 'skuId': 0, 'success': False}




    {'errorMessage': '很遗憾没有抢到,再接再厉哦。', 'orderId': 0, 'resultCode': 90008, 'skuId': 0, 'success': False}





    根据其他作者的推测 推测返回 90008 是京东的风控机制,代表这次请求直接失败,不参与抢购。

    小白信用越低越容易触发京东的风控。


    具体代码可参考GitHub地址


    感谢GitHub作者@geeeeeeeek @jd-seckill等


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

    音频播放器-iOS

    iOS
    AudioPlaybackManager 该音频播放器基于 AVPlayer 实现在线/本地播放, 在线播放支持加载本地缓存。支持设置后台播放信息。支持远程控制。 可初始化、可单例。兼容 OC 调用。 代码结构  AudioPlaybackManag...
    继续阅读 »

    AudioPlaybackManager


    该音频播放器基于 AVPlayer 实现在线/本地播放, 在线播放支持加载本地缓存。支持设置后台播放信息。支持远程控制。


    可初始化、可单例。兼容 OC 调用。


    代码结构



     AudioPlaybackManager 为实现基础播放类, 其余功能则分别位于不同文件中, 下边会根据该目录结构来进行对应功能的简单使用讲解。


    播放设置


    基础播放


    设置 playerItem

    let audio = Audio(audioURL: URL)
    AudioPlaybackManager.shared.setupItem(audio, beginTime: 0.0)

    针对 playerItem 添加了 3 个监听, 分别是:

    1. AVPlayerItem.status, 监听 playerItem 状态。

      • 当处于 readyToPlay 状态时, 会在此处获取音频总时长, 同时若 autoPlayWhenItemReady = true 时, 则会自动播放。

        若需要手动播放, 则可在收到 AudioPlaybackManager.readyToPlayNotification 通知或 audioPlaybackManagerRreadyToPlay(_:) 代理方法之后调用 play() 方法即可。

    2. AVPlayerItem.loadedTimeRanges, 监听缓存加载进度, 同步至 loadedTime

    3. AVPlayerItemDidPlayToEndTime, 监听播放完成, 同步至 playStatus = .playCompleted


    属性监听


    • @objc dynamic var playStatus: PlayStatus = .prepare
    enum PlayStatus: Int {
       case prepare, playing, paused, stop, playCompleted, error
    }
    • @objc dynamic var playTime: Float64 = 0

      • 默认为 (1/30)s 回调 1 次
    • @objc dynamic var progress: Float = 0

      • 默认为 (1/30)s 回调 1 次
    • @objc dynamic var duration: Float64 = 0

    • @objc dynamic var loadedTime: Float64 = 0


    以上属性均支持通过 KVO 监听。


    播放控制

    • play()

    • pause()

    • togglePlayPause()

    • stop()

    • switchNext()

      • 收到 AudioPlaybackManager.nextTrackNotification 通知或 audioPlaybackManagerNextTrack(_:) 代理方法后重新设置 setupItem(_:beginTime:)
    • switchPrevious()

      • 收到 AudioPlaybackManager.previousTrackNotification 通知或 audioPlaybackManagerPreviousTrack(_:) 代理方法后重新设置 setupItem(_:beginTime:)

    更多控制


    • skipForward(_ timeInterval: TimeInterval)
    • skipBackward(_ timeInterval: TimeInterval)
    • seekToPositionTime(_ positionTime: TimeInterval)
    • seekToProgress(_ value: Float)
    • beginRewind(rate: Float = -2.0)
    • beginFastForward(rate: Float = 2.0)
    • endRewindFastForward()

    播放被其他 App 影响


    中断


    当电话、闹钟、其它非官方 App 播放(这里涉及到后台播放, 下边会讲)... 时, 若二者不支持混音播放, 那么当前播放则会被系统暂停。这里主动调用了 pause() 来跟随变更播放状态。


    中断恢复播放


    var shouldResumeWhenInterruptEnded = true, 若不期望自动恢复播放, 可将其置为 false


    若中断方在结束播放后告知系统应该通知其他应用程序其已经停用了音频会话, 那么被中断的音频会话则可以选择是否继续播放。


    一般系统 App 都会对此进行通知, 而部分第三方 App 可能没对此进行处理, 那么也将不能自动恢复播放。


    ps: 由于目前没有混音播放的需求, 后续考虑是否要将中断通知转发给开发者来自主控制暂停/播放。


    播放 Route 变更


    外设变更涉及:


    1. 从外音播放改为耳机播放,继续播放;
    2. 耳机播放中,拿掉耳机(AirPods)自动暂停, 戴上继续播放;

    总体可以概括为:

    switch reason {
       case .newDeviceAvailable:
           play()
       case .oldDeviceUnavailable:
           pause()
       default: break
    }

    ps: 其他情况收到 route 变化通知如 AVAudioSession.Category 变更, 则不在该播放器考虑范畴内。


    后台播放


    1. 开启后台播放权限

      1. 设置 setActiveSession(_ enabled: Bool)


    在播放时设置为 true, 播放结束后设置为 false。如果仅在一个特定的控制器内播放的话, 在执行 deinit 方法中设置为 false 也是个不错的选择。


    ps: 该方法设置 AVAudioSession.Category = .playback, AVAudioSession.Mode = .default。会保持应用程序音频在设备静音或屏幕锁定时能够继续播放。


    在线播放加载本地缓存


    var cacheEnabled: Bool, 提供了在线播放缓存开关, 默认关闭状态。


    ps: 在线播放缓存引用了 VIMediaCache 第三方库, 支持自定义缓存目录, 默认存储在 tmp 目录下。想详细了解缓存流程的可以去看下, 文章写的很详细。


    设置后台播放信息展示


    var allowSetNowPlayingInfo: Bool, 默认为开启状态。


    如需展示, 需要在设置 let audio = Audio(audioURL: URL) 时额外对其后台展示信息相关参数进行设置。


    如需获取音频自身音频数据来进行展示, 则设置 useAudioMetadata = true 即可。


    若音频不存在相关元数据, 则可以通过其他相关参数来进行设置。

        /// Audio url.
        open var audioURL: URL

        public init(audioURL: URL) {
            self.audioURL = audioURL
        }

        /// -------------- `MPNowPlayingInfoCenter` --------------

        /// Set `nowPlayingInfo` using audio metadata.
        ///
        /// Default is `false`.
        open var useAudioMetadata: Bool = false

        // Note: If `useAudioMetadata` is set to false, then you can set it through the following properties.

        /// Audio name.
        open var title: String?
        /// Album name.
        open var albumName: String?
        /// Artist.
        open var artist: String?

        /// Artwork.
        open var artworkImage: UIImage?
        open var artworkURL: URL?

    ps: allowSetNowPlayingInfo = true 时播放进度相关信息会跟随一并设置。


    效果图:




    远程控制


    简单远程控制方式

    UIApplication.shared.beginReceivingRemoteControlEvents()
    UIApplication.shared.endReceivingRemoteControlEvents()

    AppDelegate 中实现

    func remoteControlReceived(with event: UIEvent?) {
    if let event = event, event.type == .remoteControl {
           switch event.subtype {
    case .remoteControlPlay:
    case ...
           }
       }
    }

    这种远程控制可满足大部分需求, 并且实现非常简单, 但是存在一个很大的问题, 就是无法实现进度条控制。


    项目远程控制方式


    采用 MPRemoteCommandCenter 方式。


    基础控制功能

    activatePlaybackCommands(_ enabled: Bool)
    activatePreviousTrackCommand(_ enabled: Bool)
    activateNextTrackCommand(_ enabled: Bool)
    activateChangePlaybackPositionCommand(_ enabled: Bool)

    长按 快进/快退


    var remoteControlRewindRate: Float, 默认为 -2.0;


    var remoteControlFastForwardRate: Float, 默认为 2.0;

    activateSeekBackwardCommand(_ enabled: Bool)
    activateSeekForwardCommand(_ enabled: Bool)

    跳跃播放

    复制代码
    activateSkipForwardCommand(_ enabled: Bool, interval: Int = 0)
    activateSkipBackwardCommand(_ enabled: Bool, interval: Int = 0)

    ps: 开启跳跃播放会占用 上一首/下一首 位置。


    关闭远程控制


    在不需要远程控制功能时, 调用 deactivateAllRemoteCommands() 即可完全关闭。




    项目


    好了, 以上基本就是全部使用方法了。源代码及 Demo 可访问 Github 进行查看。


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

    iOS 网速检测方案

    iOS
    背景 为了基于网络状况做更细致的业务策略,需要一套网速检测方案,尽量低成本的评估当前网络状况,所以我们希望检测数据来自于过往的网络请求,而不是专门耗费资源去网络请求来准确评估。 指标计算 一般 RTT 作为网速的主要评估指标,拿到批量的历史请求 RTT 值后,...
    继续阅读 »

    背景


    为了基于网络状况做更细致的业务策略,需要一套网速检测方案,尽量低成本的评估当前网络状况,所以我们希望检测数据来自于过往的网络请求,而不是专门耗费资源去网络请求来准确评估。


    指标计算


    一般 RTT 作为网速的主要评估指标,拿到批量的历史请求 RTT 值后,要如何去计算得到较为准确的目标 RTT 值呢?


    影响 RTT 值的变量主要是:


    1. 网络状况会随时间变化;
    2. 请求来自不同的服务器,性能有差异,容易受到长尾数据影响;

    首先参考 Chrome 的 nqe 源码:chromium.googlesource.com/chromium/sr…


    权重设计


    查阅相关源码后,发现历史请求的 RTT 值会关联一个权重,用于最终的计算,找到计算 RTT 权重的核心逻辑:

    void ObservationBuffer::ComputeWeightedObservations(
    const base::TimeTicks& begin_timestamp,
    int32_t current_signal_strength,
    std::vector<WeightedObservation>* weighted_observations,
    double* total_weight) const {

    base::TimeDelta time_since_sample_taken = now - observation.timestamp();
    double time_weight =
    pow(weight_multiplier_per_second_, time_since_sample_taken.InSeconds());

    double signal_strength_weight = 1.0;
    if (current_signal_strength >= 0 && observation.signal_strength() >= 0) {
    int32_t signal_strength_weight_diff =
    std::abs(current_signal_strength - observation.signal_strength());
    signal_strength_weight =
    pow(weight_multiplier_per_signal_level_, signal_strength_weight_diff);
    }

    double weight = time_weight * signal_strength_weight;


    可以看到权重主要来自两个方面:


    1. 信号权重:与当前信号强度差异越大的 RTT 值参考价值越低;
    2. 时间权重:距离当前时间越久的 RTT 值参考价值越低;

    这个处理能减小网络状况随时间变化带来的影响。


    半衰期设计


    在计算两个权重的时候都是用pow(衰减因子, diff)计算的,那这个“衰减因子”如何得到的呢,以时间衰减因子为例:

    double GetWeightMultiplierPerSecond(
    const std::map<std::string, std::string>& params) {
    // Default value of the half life (in seconds) for computing time weighted
    // percentiles. Every half life, the weight of all observations reduces by
    // half. Lowering the half life would reduce the weight of older values
    // faster.
    int half_life_seconds = 60;
    int32_t variations_value = 0;
    auto it = params.find("HalfLifeSeconds");
    if (it != params.end() && base::StringToInt(it->second, &variations_value) &&
    variations_value >= 1) {
    half_life_seconds = variations_value;
    }
    DCHECK_GT(half_life_seconds, 0);
    return pow(0.5, 1.0 / half_life_seconds);
    }

    其实就是设计一个半衰期,计算得到“每秒衰减因子”,比如这里就是一个 RTT 值和当前时间差异 60 秒则权重衰减为开始的一半。延伸思考一下,可以得到两个结论:


    1. 同等历史 RTT 值量级下,半衰期越小,可信度越高,因为越接近当前时间的网络状况;
    2. 同等半衰期下,历史 RTT 值量级越大,可信度越高,因为会抹平更多的服务器性能差异;

    所以更进一步的话,半衰期可以根据历史 RTT 值的量级来进行调节,找到它们之间的平衡点。


    加权算法设计


    拿到权值后如何计算呢,我们最容易想到的是加权平均值算法,但它同样会受长尾数据的影响。


    比如当某个 RTT 值比正常值大几十倍且权重稍高时,加权平均值也会很大,更优的做法是获取加权中值,这也是 nqe 的做法,伪代码为:

    //按 RTT 值从小到大排序
    samples.sort()
    //目标权重是总权重的一半
    desiredWeight = 0.5 * totalWeight
    //找到目标权重对应的 RTT 值
    cumulativeWeight = 0
    for sample in samples
    cumulativeWeight += sample.weight
    If (cumulativeWeight >= desiredWeight)
    return sample.RTT

    进一步优化


    通过历史网络请求样本数据计算加权中值,根据计算后的 RTT 值区间确定网速状态供业务使用,比如 Bad / Good,这种策略能覆盖大部分情况,但有两个特殊情况需要优化。


    无网络访问场景


    当用户一段时间没有访问网络缺乏样本数据时,引入主动探测策略,发起请求实时计算 RTT 值。


    网络状况快速劣化场景


    若在某一个时刻网络突然变得很差,大量请求堆积在队列中,由于我们 RTT 值依赖于网络请求落地,这时计算的目标 RTT 值具有滞后性。


    为了解决这个问题,可以记录一个“未落地请求”的队列,每次计算 RTT 值之前,前置判断一下“超过某个阈值”的未落地请求“超过某个比例”,视为弱网状态,达到快速感知网络劣化的效果。


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

    被裁后的一天

    距我被裁已经过去 34 天了,我还没有去找工作,接下来的一个月大概率也不会找工作,而是打算和老公去西藏旅游一趟,我对一周或者两周后的西藏之旅满是期待。至于老东家,我对他没有怨言,在那儿工作期间很愉快,被裁的时候他爽快的给了 N+1 的赔偿,离职的过程中没有发生...
    继续阅读 »

    距我被裁已经过去 34 天了,我还没有去找工作,接下来的一个月大概率也不会找工作,而是打算和老公去西藏旅游一趟,我对一周或者两周后的西藏之旅满是期待。至于老东家,我对他没有怨言,在那儿工作期间很愉快,被裁的时候他爽快的给了 N+1 的赔偿,离职的过程中没有发生不愉快的事情。我写这篇文章不是为了缅怀过去或者反思过去,而是记录我的一天,余下的内容没有华丽的词藻,也没有令人深思的道理,只是流水账。


    今天我 7 点 20 就醒了,醒来的前一刻还在做梦,我梦到自己睡不着觉,可想而知那不是一个美梦。醒来的那一瞬间,我甚至没有分辨出是现实还是梦境,头昏昏沉沉的赖了一会儿床,在赖床期间我老公起床了,他洗漱结束的时候,我用手机放了一首歌,他走到床边对我巴拉巴拉说了些话,说的都是些大道理,大概是选择呀未来呀思考呀这一套,如果让我将他的话背下来,可真是在难为我。其实他一大早就讲大道理,我特别不想听,但我觉得他的话有些道理,于是赶紧起了床,洗漱完去跑步。


    今天温度不高,早上很适合跑步,估摸着跑了 15 分钟,没有记录跑了多少公里,我打算明天记录一下跑步的公里数。跑完步给手机充了一会儿就出门吃早餐,需要坐两站地铁才能到那家包子铺,我住的地方距地铁站大概 1.5 公里,这段路可以坐小区的摆渡车,但我选择了走路。


    那家包子铺叫什么名儿?不记得了,更准确的说,我从来没想过要记住它的名字。我买了两个豇豆包,一个卤鸡蛋还有一杯豆浆。为什么要买这些食物呢?不是没有缘由。昨天我只买了两个豇豆包和一杯豆浆,没有吃饱,于是今天加了一个卤鸡蛋。今天买的这些食物,还是没让我吃饱,明天我要再加一个豇豆包。


    为什么要买豇豆包呢?因为我喜欢吃酸豇豆,但是这包子里包的不是酸豇豆,而是新鲜的豇豆,所以第一次吃它的时候它与我的预期不符合,尝了之后又发现新鲜的豇豆也好吃,现在我每次都买豇豆包。今天我看见豇豆包里包的不是豇豆,而是四季豆,四季豆也挺好吃。


    这家包子店里能喝的食物除了有豆浆还有稀饭,它的稀饭不是很稀,我担心豇豆包就着稀饭吃不好下咽,于是买了豆浆。


    吃了早餐我就回家了,到家的第一件事是联系物业师傅来疏通地漏,还让他们处理电动晾衣架不能升降的问题。关于电动晾衣架,我今天学到了一个新知识,有些人可能会认为那是小常识,不值一提,但我还是要写下来——电池放在遥控器里久了会腐蚀铜板,这将导致遥控器通不了电,遥控器就指挥不了晾衣架,我家的晾衣机不能升降就是这个原因的。


    今天我写了两篇技术文章去参加掘金推出的金石计划征文活动,这可以瓜分奖金,这是我第 5 次参加金石计划,每一次都瓜分到了最高奖金,这次我瓜分不到最高奖金了,因为写不出 6 篇原创技术文章。


    等物业师傅处理好地漏和晾衣架,发布了文章,我就出发去健身房游泳,健身房离我家有 4.8 公里左右,出发前我在想是开车还是走路,最终选了走路。


    今后要做什么?在路上我不由得思考。


    今年是我工作的第 6 年,6 年来我断断续续的有考研的想法。为什么想考研呢?在路上我想出了一个原因 —— 源自虚荣心。走着走着想上厕所,路过一家花店,进店问老板附近是否有厕所,他说马路对面有,到了马路对面又向烟酒铺的老板询问厕所的具体位置,如愿的上了厕所,一身轻松。


    上完厕所路过一家包子铺,肚子饿了,于是进店买了 2 个包子,1 个鸡蛋还有 1 碗粥,其实我想喝带丝汤,但是卖完了,所以没喝成。


    吃完午餐继续往健身房走。哦,我想起在包子铺吃午餐的时候看到了一个皮肤特别好的女生。走呀走呀,终于到了健身房,到那儿之后我没有立即去游泳池,而是去休闲区按摩,在按摩椅上躺着的时候,开始思考我是否想继续当程序员,如果当程序员,想做那方面的业务呢?


    我喜欢写作。2021 年期间我写了一部小说,超过 10 万个字,2022 年写我的第一本技术书,上个月才交稿,出版社编辑说稿子问题不大,昨天我根据编辑老师的反馈做了修改,并补充了前言,现在稿件已进入 3 审。我还有一个微信公众号,每个月至少发一篇技术文章。


    如果继续当程序员,我想做内容创作类的产品。


    按摩完就去游泳,游泳池里只有两个人,除了我还有一个小男孩,他挺可爱的。游完 3 圈我离开游泳池去了桑拿室,那儿只有我一个人,当时我突然想到我还想当程序员,不禁眼睛里有了点泪花。


    我很爱哭,被裁后我一个人在家哭了好几次。哭,不是因为被裁,因为以前没被裁的时候,我一个人在家也有哭的时候。桑拿室温度很高,泪花流出来就蒸发了。蒸完桑拿洗完澡打算回家,可是外面在下雨,于是又到按摩椅上躺了一会儿,寻思着等雨停了再回家。第二次按完摩,雨没有停的迹象,我决定淋着雨到外面去打车,16 点到的家,在家吃了一包薯片,看了一会莫言的小说《生死疲劳》,17 点的时候到床上睡觉,18 点的时候才醒,然后到小区门口买了做晚餐的蔬菜,今天晚上煮了面。




    这篇文章写了 2 个小时。最后我想说今天还有一件想做的事情没有做成——开车到兴隆湖吹风。


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

    求你了,别再说不会JSONP了

    JSONP是一种很远古用来解决跨域问题的技术,当然现在实际工作当中很少用到该技术了,但是很多同学在找工作面试过程中还是经常被问到,本文将带您深入了解JSONP的工作原理、使用场景及安全注意事项,让您轻松掌握JSONP。 JSONP是什么? JSONP,全称JS...
    继续阅读 »

    JSONP是一种很远古用来解决跨域问题的技术,当然现在实际工作当中很少用到该技术了,但是很多同学在找工作面试过程中还是经常被问到,本文将带您深入了解JSONP的工作原理、使用场景及安全注意事项,让您轻松掌握JSONP。


    JSONP是什么?


    JSONP,全称JSON with Padding,是一项用于在不同域之间进行数据交互的技术。这项技术的核心思想是通过在页面上动态创建<script>标签,从另一个域加载包含JSON数据的外部脚本文件,然后将数据包裹在一个函数调用中返回给客户端。JSONP不仅简单而且强大,尤其在处理跨域数据请求时表现出色。


    JSONP的工作原理


    JSONP的工作流程如下:


    • 客户端请求数据:首先,客户端会创建一个<script>标签,向包含JSON数据的远程服务器发出请求。这个请求通常包括一个名为callback的参数,用来指定在数据加载完毕后应该调用的JavaScript函数的名称。
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>JSONP Example</title>
    </head>
    <body>
    <h1>JSONP Example</h1>
    <div id="result"></div>

    <script>
    // 定义JSONP回调函数
    function callback(data) {
    const resultDiv = document.getElementById('result');
    resultDiv.innerHTML = `Name: ${data.name}, Age: ${data.age}`;
    }

    // 创建JSONP请求
    const script = document.createElement('script');
    script.src = 'http://localhost:3000/data?callback=callback';
    document.body.appendChild(script);
    </script>
    </body>
    </html>

    • 服务器响应:服务器收到请求后,将JSON数据包装在指定的回调函数中,并将其返回给客户端。响应的内容类似于:
    const Koa = require('koa');
    const Router = require('koa-router');

    const app = new Koa();
    const router = new Router();

    // 定义一个简单的JSON数据
    const jsonData = {
    name: 'John',
    age: 30,
    };

    // 添加路由处理JSONP请求
    router.get('/data', (ctx) => {
    const callback = ctx.query.callback;
    if (callback) {
    ctx.body = `${callback}(${JSON.stringify(jsonData)})`;
    } else {
    ctx.body = jsonData;
    }
    });

    // 将路由注册到Koa应用程序
    app.use(router.routes()).use(router.allowedMethods());

    // 启动Koa应用程序
    const port = 3000;
    app.listen(port, () => {
    console.log(`Server is running on port ${port}`);
    });


    • 客户端处理数据:在客户端的页面中,我们必须事先定义好名为callback的函数,以便在响应被加载和执行时被调用。这个函数会接收JSON数据,供我们在页面中使用。

    JSONP使用场景


    跨域请求:JSONP主要用于解决跨域请求问题,尤其适用于无法通过CORS或代理等方式实现跨域的情况。
    数据共享:在多个域名之间共享数据,可以利用JSONP实现跨域数据共享。
    第三方数据获取:当需要从第三方网站获取数据时,可以使用JSONP技术。


    使用JSONP注意事项


    JSONP的简单性和广泛的浏览器支持使其成为跨域数据交互的强大工具。然而,我们也必须谨慎使用它,因为它存在一些安全考虑,我们分析下它的优缺点:


    优点


    • 简单易用:JSONP非常容易实现和使用,无需复杂的配置。
    • 跨浏览器支持:几乎所有现代浏览器都支持JSONP。
    • 绕过同源策略:JSONP帮助我们绕过了同源策略的限制,轻松获取跨域数据。

    安全考虑


    • XSS风险:JSONP未经过滤的数据可能会引起XSS攻击,因此需要对返回的数据进行过滤和验证。
    • CSRF攻击:使用JSONP时要注意防范CSRF攻击,可以通过添加随机数等方式增强安全性。
    • 仅支持GET请求:JSONP只支持GET请求,不适用于POST等其他HTTP方法。
    • 难以处理HTTP错误:JSONP难以有效处理HTTP错误,在请求失败时的异常处理比较困难。

    随着技术的发展,JSONP已不再是首选跨域解决方案,但了解它的工作原理仍然有助于我们更深入地理解跨域数据交互的基本原理。在实际项目中,根据具体需求和安全考虑,建议优先选择CORS或代理服务器方式处理跨域问题。


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

    为什么日本的网站看起来如此不同

    快来免费体验ChatGpt plus版本的,我们出的钱 体验地址:chat.waixingyun.cn 可以加入网站底部技术群,一起找bug,另外新版作图神器,ChatGPT4 已上线 cube.waixingyun.cn/home 该篇文章讨论了日本网站外观...
    继续阅读 »

    快来免费体验ChatGpt plus版本的,我们出的钱 体验地址:chat.waixingyun.cn 可以加入网站底部技术群,一起找bug,另外新版作图神器,ChatGPT4 已上线 cube.waixingyun.cn/home


    该篇文章讨论了日本网站外观与设计的独特之处。作者指出日本网站设计与西方设计存在明显差异。文章首先强调了日本网站的视觉风格,包括丰富的色彩、可爱的角色和复杂的排版。作者解释了这种风格背后的文化和历史因素,包括日本的印刷传统和动漫文化。


    文章还讨论了日本网站的信息密集型布局,这种布局适应了日本语言的特点,使得页面能够容纳大量文字和图像。此外,文章提到了日本网站的功能丰富性,如弹出式窗口和互动元素,以及这些元素在用户体验方面的作用。


    作者强调了日本网站在技术和创新方面的进步,尽管在过去存在技术限制。最后,文章提出了一些关于如何将日本网站设计的元素应用到其他文化中的建议。


    下面是正文~~~


    多年来,我朋友与日本的网站有过许多接触——无论是研究签证要求、计划旅行,还是简单地在线订购东西。而我花了很长时间才适应这些网站上的大段文字、大量使用鲜艳颜色和10多种不同字体的设计,这些网站就像是直接冲着你扔过来的。




    虽然有许多网站都采用了更简约、易于导航的设计,适应了西方网站的用户,但是值得探究的是为什么这种更复杂的风格在日本仍然盛行。


    只是为了明确起见,这些不是过去的遗迹,而是维护的网站,许多情况下,它们最后一次更新是在2023年。




    我们可以从几个角度来分析这种设计方法:


    • 字体和前端网站开发限制
    • 技术发展与停滞
    • 机构数字素养(或其缺乏)
    • 文化影响

    与大多数话题一样,很可能没有一个正确的答案,而是这个网站设计是随着时间的推移而相互作用的各种因素的结果。


    字体和前端网站开发限制


    对于会一些基本排版知识、掌握适当软件并有一些空闲时间的人来说,为罗马化语言创造新字体可能是一项有趣的挑战。然而,对于日语来说,这是一个完全不同层次的努力。


    要从头开始创建英文字体,需要大约230个字形——字形是给定字母的单个表示(A a a算作3个字形)——或者如果想覆盖所有基于拉丁字母表的语言,则需要840个字形。对于日语而言,由于其三种不同的书写系统和无数的汉字,需要7,000至16,000个字形甚至更多。因此,在日语中创建新字体需要有组织的团队合作和比其拉丁字母表的同行们更多的时间。



    这并不令人意外,因此中文和(汉字)韩文字体也面临着类似的工作量,这导致这些语言通常被称为CJK字体所覆盖。



    由于越来越少的设计师面对这个特殊的挑战,建立网站时可供选择的字体也越来越少。再加上缺乏大写字母和使用日文字体会导致加载时间较长,因为需要引用更大的库,这就不得不采用其他方式来创建视觉层次。


    以美国和日本版的星巴克主页为例:


    美国的:




    日本的




    就这样,我们就可以解释为什么许多日本网站倾向于用文字较多的图片来表示内容类别了。有时,你甚至会看到每个磁贴都使用自己定制的字体,尤其是在限时优惠的情况下。




    技术发展/停滞与机构数字素养


    如果你对日本感兴趣,你可能对现代与过时技术之间的鲜明对比有所了解。在许多地方,先进的技术与完全过时的技术并存。作为世界机器人领导者之一的国家,在台场人工岛上放置了一座真人大小的高达雕像,却仍然依赖软盘和传真机,面对2022年Windows资源管理器关闭时感到恐慌。




    在德国,前总理安格拉·默克尔在2013年称互联网为“未知领域”后,遭到全国范围的嘲笑。然而,这在2018年被前网络安全部长樱田义孝轻易地超越,他声称自己从未使用过电脑,并且在议会被问及USB驱动器的概念时,他被引述为“困惑不解”(来源)。


    对于那些尚未有机会窥探幕后幻象的人来说,这可能听起来很奇怪,但日本在技术素养方面严重落后于更新计划。因此,可以推断这些问题也在阻碍日本网站设计的发展。而具体来说,日本的网页设计正面临着这一挑战——只需在谷歌或Pinterest上搜索日本海报设计,就能看到一个非常不同和现代化的平面设计水平。




    文化影响


    在分析任何设计选择时,不应低估文化习俗、倾向、偏见和偏好的影响。然而,“这是文化”的说法可能过于简单化,并被用作为各种差异辩解的借口。而且,摆脱自己的观点偏见是困难的,甚至可能无法完全实现。


    因此,从我们的角度来看,看这个网站很容易..




    感觉不知所措,认为设计糟糕,然后就此打住。因为谁会使用这个混乱不堪的网站呢?


    这就是因为无知而导致有趣的见解被忽视的地方。现在,我没有资格告诉你日本文化如何影响了这种设计。然而,我很幸运能够从与日本本土人士的交谈中获得启发,以及在日本工作和生活的经验。


    与这个分析相关的一次对话实际上不是关于网站,而是关于YouTube的缩略图 - 有时候它们也同样令人不知所措。




    对于习惯了许多西方频道所采用的极简和时尚设计——只有一个标题、重复的色彩搭配和有限的字体——上面的缩略图确实有些难以接受。然而,当我询问一个日本本土人士为什么许多极受欢迎频道的缩略图都是这样设计时,他对这种设计被视为令人困惑的想法感到惊讶。他认为日本的设计方法使视频看起来更加引人入胜,提供了一些信息碎片,从而使我们更容易做出是否有趣的明智决策。相比之下,我给他看的英文视频缩略图在他看来非常模糊和无聊。


    也许正是这种寻求信息的态度导致了我们的观念如此不同。在日本,对风险的回避、反复核对和对迅速做出决策的犹豫明显高于西方国家。这与更加集体主义的社会心态紧密相连——例如,在将文件发送给商业伙伴之前进行两次(或三次)检查可能需要更长时间,但错误的风险显著降低,从而避免了任何参与者丢面子的情况发生。


    尽管有人认为这只适用于足够高的赌注,而迷惑外国游客似乎不符合条件——搜索一下“Engrish”这个词,然后感谢我吧。


    回到网站设计,这种文化角度有助于解释为什么在线购物、新闻和政府网站在外部观察者看来常常是“最糟糕的罪犯”。毕竟,这些正是需要大量细节直接对应于做出良好购买决策、高效地保持最新信息或确保你拥有某个特定程序的所有必要信息的情况。


    有趣的是,关于美国人和中国/日本人如何感知信息,也有相当多的研究。几项研究的结果似乎表明,例如,日本人更加整体地感知信息,而美国人倾向于选择一个焦点来引导他们的注意力(来源)。这可能给我们提供了另一个线索,解释为什么即使在日语能力较高的情况下,西方人对这类网站也感到困难。


    后但并非最不重要的是,必须说的是,网站并不是在一个在线真空中存在。而且,各种媒体,从小册子或杂志到地铁广告,也使用了尽可能多地压缩信息的布局,人们可能已经习惯了这种无处不在的方式,以至于没有人曾经想过质疑它。


    长话短说,这并不是为了找到标题问题的绝对答案,也不是为了加强日本人独特性的观点,就像日本人论一样。相反,尤其是在看到了几次关注一个解释为“真正答案”的讨论之后,我想展示科技、历史和文化影响的广度,这些最终塑造了这种差异。


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

    《程序员的底层思维》:解密软件背后的16种底层思维能力

    本文是关于《程序员底层思维》的阅读笔记,读后有较大收获,分享给大家。书中介绍了程序员应该具备的16中底层底层思维能力,只有意识到思维能力的存在,我们才有可能去学习、练习和提升。 基础思维能力 抽象思维 抽象思维是程序员最重要的思维能力之一,抽象的过程就是通过归...
    继续阅读 »


    本文是关于《程序员底层思维》的阅读笔记,读后有较大收获,分享给大家。书中介绍了程序员应该具备的16中底层底层思维能力,只有意识到思维能力的存在,我们才有可能去学习、练习和提升。


    基础思维能力


    抽象思维


    抽象思维是程序员最重要的思维能力之一,抽象的过程就是通过归纳概括、分析综合来寻找共性、提炼相关概念的过程。


    软件工程师每天都要动用抽象思维,首先对问题域进行分析、归纳、综合、判断、推理,然后抽象出各种概念,挖掘概念和概念之间的关系,再对问题域进行建模,最后通过编程语言实现业务功能。


    抽象具有层次性,抽象层次越高,内涵越小,外延越大,扩展性越好;反之,抽象层次越低,内涵越大,外延越小,扩展性越差,但语义表达能力越强。


    逻辑思维


    逻辑思维基本包含3个方面的要素。(1)概念:概念是思维的基本单位。(2)判断(proposition,在逻辑学中也叫命题):通过概念对事物是否具有某种属性进行肯定或否定的回答,就是判断。(3)推理(argument,在逻辑学中也叫论证):由一个或几个判断推出另一判断的思维形式,就是推理。


    判断是概念的展开,没有判断,就不能揭示和说明概念。同时,判断也是推理的前提,是正确运用各种推理的必要条件。所谓推理,就是研究语句、判断、命题之间相互关系的学问。建模是一个归纳工作,我们通过抽象问题域里具有共同特性的类来建立模型。为了验证模型的有效性,我们会使用演绎的方法去推演不同的业务场景,看看模型是否能满足业务的需要。


    大多数情况下,我们的思维逻辑链都比较短,短就意味着肤浅,找不到问题的根本原因。延长思维逻辑链的方法之一是5Why思考法,它能够帮助我们找到问题的根本原因。5Why思考法,是指对一个问题连续多次追问为什么,直到找出问题的根本原因。


    丰田汽车公司前副社长大野耐一曾经举了一个通过5Why提问法找到问题根本原因的实例。有一次,大野耐一先生见到生产线上的机器总是停转,虽然修过多次但仍不见好转,便上前询问现场的工作人员。



    问:“为什么机器停了?”(1Why)


    答:“因为机器超载,保险丝烧断了。”


    问:“为什么机器会超载?”(2Why)


    答:“因为轴承的润滑不足。”


    问:“为什么轴承会润滑不足?”(3Why)


    答:“因为润滑泵吸不上来油。”


    问:“为什么润滑泵吸不上来油?”(4Why)


    答:“因为油泵轴磨损、松动了。”


    问:“为什么油泵轴磨损了?”(5Why)


    答:“因为没有安装过滤器,润滑油里混进了铁屑等杂质。”



    结构化思维


    结构化思维是一种以逻辑(事物内在规律)为基础,从无序到有序搭建结构的思维过程,其目的是降低复杂度和认知成本,因为大脑更喜欢概念少、有规律的信息。


    结构是万物之本,小到分子,大到宇宙,只有了解其结构才能真正认识它。


    批判性思维


    批判性思维中的“批判”一词其实不太准确,甚至在中文里有点否定、批评和抨击的负面意思。批判的英文是critical,这个词来自古希腊词kriticos,是分辨力、决断力或决策能力的意思。它强调的是理性和逻辑在思维中的重要性,目的是形成正确的结论,并做出明智的决策和判断。所以批判性思维并不是让你批评、否定或者抨击别人,而是教你如何提升分辨能力、判断力。


    简而言之,批判性思维就是对思维过程的再思考。古希腊哲学家苏格拉底说,未经审视的人生不值得过。同样,未经批判性思维审视过的结论也是不值得相信的。


    维度思维


    一个人的思维层级与其思考的维度是正相关的,这一点可以通过我们的日常语言得到佐证。当我们说这个人很“轴”“一根筋”的时候,实际上是在说他只有一维的线性思维;高手的思考会更加“全面”,因为涉及“面”,所以至少是两个维度的思考;而真正的高手,其思考是成“体系化”的,“体”至少是三维的,也就是说他考虑到了“方方面面”。


    多维度思考是思考的高级阶段,是体系化思考的必备,是解决复杂问题的一把利器。


    分类思维


    当信息量过大时,归类分组能帮助我们理解和处理问题。分类是人类大脑的识别模式,是我们化繁为简的不二法宝。在我们处理问题,特别是复杂问题的时候,分类思维扮演了极其重要的角色。


    分类的目的是找到问题域中的“核心抽象”,基于这些“核心抽象”,我们才能设计相应的领域模型和数据模型;基于这些模型,我们才能构建相应的系统。


    没有完美的分类,任何分类都与进行分类的观察者的视角和目的有关。


    分治思维


    分治的价值在于,我们不应该试着在同一时间把整个问题域都塞进自己的大脑,而应该试着以某种方式去组织问题,以便能够在一个时刻专注于一个特定的部分。


    分治算法主要包含3个步骤:分、治、并。“分”是递归地将原问题分解成小问题;“治”是在解决了各个小问题之后(各个击破之后)合并小问题的解,从而得到整个问题的解;“并”是按原问题的要求,将子问题的解逐层合并,构成原问题的解。


    软件中存在大量的分治思想,比如管道模式、分层架构、分布式架构等,无不体现了分治的强大。


    简单思维


    把一件事情搞复杂是一件简单的事,但要把一件复杂的事变简单,这是一件复杂的事。


    简化本质上是一个熵减活动。所有的事物都在缓慢熵增,就像凯文·凯利在《必然》一书中提到,世间万物都需要额外的能量和秩序来维持自身,无一例外。这就是著名的热力学第二定律,即所有的事物都在缓慢地分崩离析。而熵减就是逆向做功,即通过更多的努力让混乱的系统重新归于秩序。


    简单不是一个简单的目标,而是一个非常高的目标。所有的UNIX哲学浓缩为一条铁律就是KISS原则。


    简单不是简陋。简单是一种洞察问题本质、化繁为简的能力,简陋是对问题不加思考地简单处理,二者有本质区别。简单需要我们付出很多的精力,对问题深入思考,进行熵减逆向做功。往往需要经历简单—复杂—简单的演化过程。


    成长型思维


    想要培养自己的成长型思维,首先要学会正确评价自己,也就是要学会客观地看待自己的状况和水平,不要自视过高,也不要妄自菲薄。其次,不要过分相信天分。一个人一旦相信了天分,就等于相信了自己的水平是基本不变的,就会给自己设限,觉得我只能做这个,我不适合干那个,甚至会觉得努力是一件丢脸的事情,只有笨人才需要努力。


    看过《刻意练习:如何从新手到大师》一书的人应该知道,很多所谓的天才,其实靠的并不是天分,而是努力!想要让自己获得成长和改变,就一定要学会用成长型思维去看待和处理问题,其关键在于不要自我设限。


    专业思维能力


    解耦思维


    在软件领域,“耦合”是指两个事物之间联系的紧密程度。联系越紧密,耦合性越高;联系越少,耦合性越低。解耦就是要减少事物之间联系的紧密程度。


    “计算机中的任何问题,都可以通过加一层来解决”,中间层的价值也在于解耦。


    “高内聚、低耦合”是软件设计追求的重要目标之一,组件、模块、层次设计都应该遵循“高内聚、低耦合”的设计原则。


    应用架构之道,就是要实现业务逻辑和技术细节的解耦。


    契约思维


    “人是生而自由的,但却无往不在枷锁之中”,同样,“写代码是自由的,但无往不在规则之下”。这里的规则包括工程师必须要遵守的程序语言语法、编程规范,以及协议标准。


    为了保证软件编程风格的一致性,减少随心所欲带来的复杂度,我们有必要使用契约思维制定一定程度上的编程规范,去约束团队的行为。规范的价值,就在于它能保证代码的一致性,而一致性在很大程度上可以降低认知成本和复杂度。


    通过在团队中落实命名规范、异常处理规范、架构规范等,可以有效地帮助团队治理代码复杂度。


    社会大规模分工协作离不开契约思维,编程在很大程度上是一种“制定契约”。


    模型思维


    在软件工程中,有两个高阶工作,一个是架构,另一个是建模。如果把写代码比喻成“搬砖”,那么架构和建模就是“设计图纸”了。相比于编码,建模的确是对设计经验和抽象能力要求更高的一种技能。


    简单来说,模型就是对现实的简化抽象。


    领域模型将现实世界抽象为了信息世界,把现实世界中的客观对象抽象为某一种信息结构,而这种信息结构并不依赖于具体的计算机系统。


    领域模型对软件开发至关重要。因为从本质上来说,软件开发就是从问题空间到解决方案空间的映射转化,而领域模型是连接问题和解决方案的桥梁。


    领域模型关注的是领域知识,是业务领域的核心实体,体现了问题域中的关键概念,以及概念之间的联系。领域模型建模的关键在于模型能否显性化、清晰地表达业务语义,其次才是扩展性。


    数据模型关注的是数据存储,所有的业务都离不开数据,以及对数据的CRUD。数据模型建模的决策因素主要是扩展性、性能等非功能属性,无须过多考虑业务语义的表征能力。


    工具化思维


    我们可以把“懒”分为3个境界。(1)最低境界是“实在懒”,拖延症,不到万不得已,不去完成任务。(2)其次是“开明懒”,迅速做完不喜欢的任务,以摆脱之。(3)最高境界是“智慧懒”,使用工具完成不喜欢的任务,以便再也不用做无谓的重复工作,从而一劳永逸。


    量化思维


    No measurement,no improvement.(没有量化,就无法优化。)——“科学管理之父”温斯洛·泰勒


    一个量化的过程大体上可以分为以下3步。(1)定义指标:仔细分析问题,找到那个可以用来量化问题的关键指标。(2)将指标数字化:围绕关键指标,明确需要哪些数据来实现指标的计算,通过数据收集、数据存储、数据展现去呈现指标,也就是数字化的过程。(3)优化指标:有了数据指标之后,要围绕指标数据迭代优化,达成业务目标。


    量化工作本身是一件非常困难和极具挑战的事情,但量化思维要求我们不要轻易放弃关于量化的思考和尝试。没有量化的目标,就像是断了线的风筝,没有方向,缺少指引,飞到哪里是哪里,而量化后的目标可以为我们清楚地指引方向。


    数据思维


    一切业务数据化,一切数据业务化。


    用户在App上的每一次浏览、每一次点击、每一次搜索等业务行为,都会被沉淀为数据保存起来,这种保存业务过程数据的做法叫作业务数据化。这些数据会帮助App更好地认识用户,当用户下次打开App时,利用这些数据,App就可以更精准地为用户进行智能推荐和广告精准投放,这种用数据赋能业务的方法叫作数据业务化。


    产品思维


    工程思维和产品思维是不一样的。工程师追求技术至上,产品经理追求商业价值和用户体验;工程师关注细节,产品经理关注全局;工程师关注How(如何做),产品经理关注Why(为什么)。结合两种思维方式,可以让思考更全面和系统化。


    作为技术人员,我们必须要具备一定的产品思维,这样才能辨别产品需求的真伪,把伪需求挡在外面,从而可以把时间放在真正有价值的项目上,少做一些无效的投入。对于团队的技术负责人来说,这种把关尤为重要。


    了解产品思维,关键要理解产品的三个核心要素:用户、需求、场景。(1)用户是产品要服务的对象,即使用产品的人。(2)需求即产品要解决的核心问题是什么。需要注意的是,需求是分层次的,最浅一层是需求的表象;第二层是观点和背后的目的;最深一层是人性,每个需求挖到最后,都可以归结到人性层面。(3)场景即用户何时何地需要使用产品。


    后记


    希望读者能够把这些底层思维能力内化成自己的“不知道自己知道”。这些底层思维中蕴藏着解决问题的强大力量,当它们与软件设计相遇时,会擦出耀眼的“火花”。


    软件开发行业的匠心和传统行业的匠心不一样,不是重复做简单的事情,你就能把它做好。这就好比你即使做了10年的收银员,也只是一个收银员,无法成为财务总监。在软件开发行业,你需要不断地学习、不断地思考、不断地积累、不断地尝试、不断地失败、不断地创新,才有可能做得好。


    优秀的工程师,心中都有一团火——一种对美的追求和渴望。这需要我们经历无数个不眠之夜,承受很大的压力,受很多委屈,看很多的书,尝试很多别人没有实践过的东西,要具有一颗“不妥协、不将就、不放弃”的倔强的心。这样我们才能做出一些不同凡响的东西,才能活成自己所期望的样子。


    作者:楚兴
    来源:juejin.cn/post/7277830857422602252
    收起阅读 »

    如何努力才能成为核心骨干?

    我相信每个人都想成为组里的核心骨干,不用打酱油,不用干杂活,可以选择最有挑战的工作、最有收益的工作,可以把杂活脏活累活甩给其他人。 有更多的锻炼机会,有更好的晋升机会…… 但组里的核心骨干毕竟是少数,就像年终绩效考评S和 A 绩效总是极少数人。大多数人只能拿 ...
    继续阅读 »

    我相信每个人都想成为组里的核心骨干,不用打酱油,不用干杂活,可以选择最有挑战的工作、最有收益的工作,可以把杂活脏活累活甩给其他人。 有更多的锻炼机会,有更好的晋升机会……


    但组里的核心骨干毕竟是少数,就像年终绩效考评S和 A 绩效总是极少数人。大多数人只能拿 B。


    我的三份工作都把前辈熬走了


    熬走了前辈,我就成为了核心骨干。


    在我待过的三家公司来看,第一家是新创项目,我就是元老,虽然刚毕业,但被安排负责了一部分核心工作,然而另一个名校应届生因为来的晚,只能负责监控打点优化超时的一些杂活,我则始终参与核心业务需求和核心框架升级。我觉得我的能力不比他强,只是我来的早一点罢了。


    第二份工作,我入职时候表现很差。因为公司不加班,我表现的过于放飞自我,下班较早,被老板认为摆烂没上进心。一直被安排负责边缘业务。但是待了一年半不到,比我入职早的、同时入职的都离职跑路了,看得出领导是捏着鼻子把我当核心骨干用。后来换了领导,新的领导更是把我当骨干用。就这样新的一年我接触核心业务,核心技术改造,进步确实比之前干杂活快的多。


    第三份工作是在美团外卖,比较核心的业务,但是一开始也是负责杂活。老员工把他一些不想干的小技术优化分给我,还说"这些容易出成果","呵呵"。这些小活杂活真的干起来很没有意思。但是互联网人员流动快的铁律又出现了,才入职一年,负责核心模块的两个员工相继离职,而我毫无疑问,马上接替他们的工作。之前虽然给他俩打杂,但好歹我平时没少熟悉代码,熟悉业务逻辑,所以最适合接替他俩的自然是我。


    总结下来,这三份工作我从打杂到核心骨干,并不是靠卷出来的。靠内卷上位,真的是非常难的。大家都是一个脑袋,凭什么你更强。大家都是 24 小时,凭什么你更强。你作为后来的,很难挤掉其他人上位。


    当核心骨干要靠【 熬 】


    成核心骨干是适合大多数人的方式。新同学进入到新的环境,想要超越组里的老人,是一件很有挑战的事情。因为大多数工作只要做的久就一定能胜任,系统再复杂再有挑战,组里总会有人熟悉。因为系统代码都是他们写的,你作为一个新人 想追上再超越非常困难。(想要超车,必须要有弯道啊)


    就算你工作能力突出,核心工作就会交给你负责吗?不然。组里的老员工负责核心工作干的好好地,为什么主管会让你负责呢?让你负责核心工作,组里的核心老员工去干啥,干杂活吗?从个人感情上,老员工更亲。从工作经验上老员工更多。新人要想超越,需要付出太多。


    就我个人经验而言,组里每个人都负责其中一部分工作,在没有特殊情况下,不会出现工作内容的交换。(不排除个别团队会轮换工作内容)


    把老员工熬走,你就是老员工。把核心员工熬走,你就是核心员工。”熬“是一种平和的心态,让我们戒骄戒躁,当被安排一些杂活时,能保持平静。只有情绪平和,才能好好地工作。


    互联网公司人员流动快,虽说是熬成老员工。但可能只需要熬一年就成老员工。


    熬是一种良好心态,让我们脚踏实地,眼望天空


    熬着,并不是把上班当煎熬。而是能摆正心态,认识到被分配干杂活是正常的,并不是针对你,每个人都是这么过来的。 熬着是一种耐心,相信早晚有一天轮到自己。


    平时要把每一份交给自己的工作做好。否则,等核心员工离职了,领导也不信任你,还继续让你干杂活,这就真是悲剧了。


    劳资要和他们斗到底


    工作中难免有不顺心的时候,可能是产品经理奇葩需求太多,加班太多。也可能是和上下游团队吵架甩锅。甚至可能是间歇性疲倦。


    不顺心,难免会想着离职跳槽,跳槽也许还能涨薪,诱惑很大。


    但是成为核心骨干后,做最有挑战的工作才能让人成长最快。如果轻易跳槽,就错失最好最快的成长机会。


    当面对工作中的不顺心,要始终保持 一份斗争的心态。告诉自己,谁欺负自己,就和他斗到底。



    1. 产品奇葩需求多,就和他争排期,争需求的不合理,争取领导协调更多人力。

    2. 上下游团队吵架甩锅。就和大家学,如何微笑的吵架,微笑的把锅甩出去。

    3. 加班多,周末就多出去放松、锻炼下身体、吃点好吃的。让自己开心。同时也要向领导寻求帮助,或者传递自己加班太多的委屈。不要让自己的辛苦被领导忽视。


    “劳资要和他们斗到底” 这是对”敌人“的宣战,更是对一个懦弱、爱放弃、爱退缩的自己宣战!


    好事多磨,机会是熬出来的,优秀的人也是熬出来的。不要轻易跳槽哦~


    大家一起共勉。


    作者:他是程序员
    来源:juejin.cn/post/7281499704220106815
    收起阅读 »

    父母在家千万注意别打开“共享屏幕”,银行卡里的钱一秒被转走......

    打开屏幕共享,差点直接被转账 今天和爸妈聊天端午回家的事情,突然说到最近AI诈骗的事情,千叮咛万嘱咐说要对方说方言才行,让他们充分了解一下现在骗子诈骗的手段,顺便也找了一下骗子还有什么其他的手段,打算一起和他们科普一下,结果就发现下面这一则新闻: 在辽宁大连务...
    继续阅读 »

    打开屏幕共享,差点直接被转账


    今天和爸妈聊天端午回家的事情,突然说到最近AI诈骗的事情,千叮咛万嘱咐说要对方说方言才行,让他们充分了解一下现在骗子诈骗的手段,顺便也找了一下骗子还有什么其他的手段,打算一起和他们科普一下,结果就发现下面这一则新闻:


    在辽宁大连务工的耿女士接到一名自称“大连市公安局民警”的电话,称其涉嫌广州一起诈骗案件,让她跟广州警方对接。耿女士在加上所谓的“广州警官”的微信后,这位“警官”便给耿女士发了“通缉令”,并要求耿女士配合调查,否则将给予“强制措施”。随后,对方与耿女士视频,称因办案需要,要求耿女士提供“保证金”,并将所有存款都集中到一张银行卡上,再把钱转到“安全账户”。


    图片


    期间,通过 “屏幕共享”,对方掌握了耿女士银行卡的账号和密码。耿女士先后跑到多家银行,取出现金,将钱全部存到了一张银行卡上。正当她打算按照对方指示,进行下一步转账时,被民警及时赶到劝阻。在得知耿女士泄露了银行卡号和密码后,银行工作人员立即帮助耿女士修改了密码,幸运的是,银行卡的近6万元钱没有受到损失。


    就这手段,我家里的老人根本无法预防,除非把手机从他们手里拿掉,与世隔绝还差不多,所以还是做APP的各大厂商努力一下吧!


    希望各大厂商都能看看下面这个防劫持SDK,让出门在外打工的我们安心一点。


    防劫持SDK


    一、简介


    防劫持SDK是具备防劫持兼防截屏功能的SDK,可有效防范恶意程序对应用进行界面劫持与截屏的恶意行为。


    二、iOS版本


    2.1 环境要求


    条目说明
    兼容平台iOS 8.0+
    开发环境XCode 4.0 +
    CPU架构armv7, arm64, i386, x86_64
    SDK依赖libz, libresolv, libc++

    2.2 SDK接入


    2.2.1 DxAntiHijack获取

    官网下载SDK获取,下面是SDK的目录结构


    1.png


    DXhijack_xxx_xxx_xxx_debug.zip 防劫持debug 授权集成库 DXhijack_xxx_xxx_xxx_release.zip 防劫持release 授权集成库




    • 解压DXhijack_xxx_xxx_xxx_xxx.zip 文件,得到以下文件




      • DXhijack 文件夹



        • DXhijack.a 已授权静态库

        • Header/DXhijack.h 头文件

        • dx_auth_license.description 授权描述文件

        • DXhijackiOS.framework 已授权framework 集成库






    2.2.2 将SDK接入XCode

    2.2.2.1 导入静态库及头文件

    将SDK目录(包含静态库及其头文件)直接拖入工程目录中,或者右击总文件夹添加文件。 或者 将DXhijackiOS.framework 拖进framework存放目录


    2.2.2.2 添加其他依赖库

    在项目中添加 libc++.tbd 库,选择Target -> Build Phases,在Link Binary With Libraries里点击加号,添加libc++.tbd


    2.2.2.3 添加Linking配置

    在项目中添加Linking配置,选择Target -> Build Settings,在Other Linker Flags里添加-ObjC配置


    2.3 DxAntiHijack使用


    2.3.1 方法及参数说明

    @interface DXhijack : NSObject

    +(void)addFuzzy; //后台模糊效果
    +(void)removeFuzzy;//后台移除模糊效果
    @end

    2.3.2 使用示例

    在对应的AppDelegate.m 文件中头部插入


    #import "DXhijack.h"

    //在AppDelegate.m 文件中applicationWillResignActive 方法调用增加
    - (void)applicationWillResignActive:(UIApplication *)application {
    [DXhijack addFuzzy];
    }

    //在AppDelegate.m 文件中applicationDidBecomeActive 方法调用移除
    - (void)applicationDidBecomeActive:(UIApplication *)application {
    [DXhijack removeFuzzy];
    }


    三、Android版本


    3.1 环境要求


    条目说明
    开发目标Android 4.0+
    开发环境Android Studio 3.0.1 或者 Eclipse + ADT
    CPU架构ARM 或者 x86
    SDK三方依赖

    3.2 SDK接入


    3.2.1 SDK获取


    1. 访问官网,注册账号

    2. 登录控制台,访问“全流程端防控->安全键盘SDK”模块

    3. 新增App,填写相关信息

    4. 下载对应平台SDK


    3.2.2 SDK文件结构



    • SDK目录结构 android-dx-hijack-sdk.png



      • dx-anti-hijack-${version}.jar Android jar包

      • armeabiarmeabi-v7aarm64-v8ax86 4个abi平台的动态库文件




    3.2.3 Android Studio 集成

    点击下载Demo


    3.2.3.1 Android Studio导入jar, so

    把dx-anti-hijack-x.x.x.jar, so文件放到相应模块的libs目录下


    android-dx-hijack-as.png



    • 在该Module的build.gradle中如下配置:


     android{
    sourceSets {
    main {
    jniLibs.srcDirs = ['libs']
    }
    }

    repositories{
    flatDir{
    dirs 'libs'
    }
    }
    }


    dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    }



    3.2.3.2 权限声明

    Android 5.0(不包含5.0)以下需要在项目AndroidManifest.xml文件中添加下列权限配置:


    <uses-permission android:name="android.permission.GET_TASKS"/>

    3.2.3.3 混淆配置

    -dontwarn *.com.dingxiang.mobile.**
    -dontwarn *.com.mobile.strenc.**
    -keep class com.security.inner.**{*;}
    -keep class *.com.dingxiang.mobile.**{*;}
    -keep class *.com.mobile.strenc.**{*;}
    -keep class com.dingxiang.mobile.antihijack.** {*;}

    3.3 DxAntiHijack 类使用


    3.3.1 方法及参数说明

    3.3.1.1 初始化


    建议在Application的onCreate下調用


    /**
    * 使用API前必須先初始化
    * @param context
    */

    public static void init(Context context);

    3.3.1.2 反截屏功能


    /**
    * 反截屏功能
    * @param activity
    */

    public static void DGCAntiHijack.antiScreen(Activity activity);

    /**
    * 反截屏功能
    * @param dialog
    */

    public static void DGCAntiHijack.antiScreen(Dialog dialog);

    3.3.1.3 反劫持检测


    /**
    * 调用防劫持检测,通常现在activity的onPause和onStop调用
    * @return 是否存在被劫持风险
    */

    public static boolean DGCAntiHijack.antiHijacking();

    3.3.2 使用示例

    //使用反劫持方法
    @Override
    protected void onPause() {
    boolean safe = DXAntiHijack.antiHijacking();
    if(!safe){
    Toast.makeText(getApplicationContext(), "App has entered the background", Toast.LENGTH_LONG).show();
    }
    super.onPause();
    }

    @Override
    protected void onStop() {
    boolean safe = DXAntiHijack.antiHijacking();
    if(!safe){
    Toast.makeText(getApplicationContext(), "App has entered the background", Toast.LENGTH_LONG).show();
    }
    super.onStop();
    }



    //使用反截屏方法
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    DXAntiHijack.antiScreen(MainActivity.this);
    }

    以上。


    结语


    这种事情层出不穷,真的不是吾等普通民众能解决的,最好有从上至下的政策让相应的厂商(尤其是银行和会议类的APP)统一做处理,这样我们在外打工的人才能安心呀。


    作者:昀和
    来源:juejin.cn/post/7242145254057312311
    收起阅读 »

    我裸辞了,但是没走成!

    人在国企,身不由己!公司福利和薪资还可以,但有个难顶的组长就不可以,说走就走!如果把这个组长换了的话就另说了! 1.为什么突然想不干了? 1.奇葩的新组长 我的前组长辞职了,然后被安排到这个自研项目组,而这个新组长我之前得罪过,老天爷真爱开玩笑! 今年过年前,...
    继续阅读 »

    人在国企,身不由己!公司福利和薪资还可以,但有个难顶的组长就不可以,说走就走!如果把这个组长换了的话就另说了!


    1.为什么突然想不干了?


    1.奇葩的新组长


    我的前组长辞职了,然后被安排到这个自研项目组,而这个新组长我之前得罪过,老天爷真爱开玩笑!


    今年过年前,我主开发的平台要嵌入到他负责的项目里面,一切对接都很顺利,然而某天,有bug,我修复了,在群里面发消息让他合并分支更新一下。他可能没看到,然后我下班后一个小时半,我还在公司,在群里问有没有问题,没回应!


    然后我就坐车回家,半路,产品经理组长、大组长和前组长一个个轮流call我,让我处理一下bug,我就很无语!然后我就荣获在家远程办公,发现根本没问题!然后发现是对方没更新的问题!后面我修复完直接私聊他merge分支更新,以免又这样大晚上烦人!
    而类似的事情接连发生,第三次之后,我忍不住了,直接微信怼了他,他还委屈自己晚上辛苦加班,我就无语大晚有几个人用,晚上更新与第二天早上更新有什么区别?然后就这样彻底闹掰了!


    我就觉得这人很奇葩,有什么问题不能直接跟我沟通,一定要找我的上级一个个间接联系我呢?而且,这更新流程就很有问题,我之前在别的组支援修bug,是大早上发布更新,一整天测试,保证不是晚上的时候出现要紧急处理的问题!


    然后,我跟这人有矛盾后,我就没继续对接这个项目了,前组长安排了别人代替我!


    结果兜兜转转,竟然调到他这里来!作孽啊!


    2.项目组乱糟糟


    新项目组可以看出新组长管理水平很糟糕!


    新组长给自己的定位是什么都管!产品、前后端、测试、业务等,什么都往自己身上揽!他自己觉得很努力,但他不是那部分的专业人员,并不擅长,偏偏还没那个金刚钻揽一堆瓷器活!老爱提建议!


    项目组就两个产品,其中一个是UI设计刚转,还没成长为专业的产品经理,而那个主要负责的产品经理根本忙不过来!


    然后,他一个人搞不定,就开始了PUA大法,周会的时候就会说:“希望大家要把这个项目当成自己的事业来奋斗,一起想,更好地做这个产品!”


    “这个项目集成了那么多的模块功能,如果大家能够做好,对自己有很大的历练和成长!”


    “我们项目是团队的重点项目,好多领导都看好,开发不要仅限于开发,要锻炼产品思维……”


    ……


    简而言之就是,除了本职工作也要搞点产品的工作!


    然后建模师开始写市场调研文档,前后端开发除了要敲代码,还得疯狂想新功能。


    整个组开始陷入搞新东西的奇怪旋涡!


    某次需求评审的时候,因为涉及到大量的文件存储,我提出建议,使用minio,fastdfs,这样就不用每次部署的时候,整体文件还要迁移,结果对方一口拒绝,坚决使用本地存储,说什么不要用XX平台的思想来污染他这个项目,他这个项目就要不需要任何中间件都能部署。


    就很无语!那个部署包又大又冗余,微服务都不用,必须要整包部署整套系统,只想要某几个功能模块都不行,还坚持说这样可以快速整包部署比较好!


    一直搞新功能的问题就是版本更新频繁!一堆新功能都没测清楚就发布,导致产品质量出现严重问题,用户体验极差!终于用户积攒怨气爆发了,在使用群里面@了我们领导,产品质量问题终于被彻底揭开了遮羞布!


    领导开始重视这个产品质量的问题,要求立即整改!


    然后这个新组长开始新一轮搞事!


    “大家保证新功能进度的同时,顺便测试旧功能,尽量不要出bug!”


    意思就是你开发进度也要赶,测试也要搞!


    就不能来点人间发言吗?


    3.工作压力剧增


    前组长是前端的,他带我入门3D可视化的,相处还算融洽!然而他辞职了,去当自由职业者了!


    新组长是后端的,后端组长问题就是习惯以后端思维评估前端工作,给任务时间很紧。时间紧就算了,还要求多!


    因为我之前主开发的项目是可视化平台,对方不太懂,但不妨碍他喜欢异想天开,加个这个,加个那个,他说一句话,就让你自行想象,研究竞品和评估开发时间!没人没资源,空手套白狼,我当时就很想爆他脑袋了!


    我花一个星期集成了可视化平台的SDK,连接入文档都写好了,然后他验收的时候提出一堆动态配置的要求,那么大的可视化平台,他根本没考虑项目功能模块关联性和同步异步操作的问题,他只会提出问题,让你想解决方案!


    然后上个月让我弄个web版的Excel表格,我看了好多开源项目,也尝试二开,增加几个功能,但效率真的好低!于是我就决定自己开发一个!


    我开发了两个星期,他就问我搞定没?我说基本功能可以,复杂功能还在做!


    更搞笑的是,我都开发两个星期了,对方突然中午吃饭的时候微信我,怕进度赶不上,建议我还是用开源的进行二开,因为开源有别人帮忙一起搞。


    我就很无语,人家搞的功能又不是一定符合你的需求,开源不等于别人给你干活,大家都是各干各的,自己还得花精力查看别人代码,等价于没事找事,给自己增加工作量!别人开发的有隐藏问题,出现bug排查也很难搞,而自己开发的,代码熟悉,即便有问题也能及时处理!


    我就说他要是觉得进度赶不上就派个人来帮忙,结果他说要我写方案文档,得到领导许可才能给人。


    又要开发赶进度,又要写文档,哪有那么多时间!最终结果就是没资源,没人手,进度依旧要赶!


    因为我主开发的那个可视化平台在公司里有点小名气,好多平台想要嵌入,然后,有别的平台找到他要加上这个可视化平台,但问题是我很忙,又要维护又要开发,哪搞得了那么多?还说这个很赶!赶你个头!明知道时间没有,就别答应啊!工作排期啊!


    新组长不帮组员解决问题,反而把问题抛给组员,压榨组员就很让人反感!


    2.思考逃离


    基于以上种种!我觉得这里不是一个长久之地,于是想要逃离这里!


    我联系了认识的其他团队的人,别人表示只要领导愿意放人,他们愿意接收我,然后我去咨询一些转团队的流程,那些转团队成功的同事告诉我,转团队最难的是领导放人这关,而且因为今年公司限制招聘,人手短缺,之前有人提出申请,被拒绝了!并且转团队的交接的一两个月内难免要承受一些脸色!有点麻烦!


    我思虑再三,我放弃了转团队这条路,因为前组长走了之后,整个团队只剩下我一个搞3D开发的,估计走不掉!


    3.提出辞职


    忍了两个月,还是没忍住,工作最重要的是开开心心!赚钱是一回事,要是憋出个心理疾病就是大事了!于是我为了自己的身心健康,决定走人!拜拜了喂!老娘不奉陪了!


    周一一大早,我就提交了辞职信,大组长表示很震惊,然后下午的时候,领导和大组长一起来跟我谈话,聊聊我为什么离职?问我有没有意愿当个组长之类的,我拒绝了,我只想好好搞技术!当然我不会那么笨去说别人的坏话得罪人!


    我拿前组长当挡箭牌,说自己特别不习惯这个新组长的管理方式!前组长帮我扛着沟通之类的问题,我只要专心搞开发就好了!


    最终,我意志坚定地挡住了领导和大组长的劝留谈话,并且开始刷面试题,投简历准备寻找新东家!


    裸辞后真的很爽,很多同事得知消息都来关心我什么情况,我心里挺感动的!有人说我太冲动了,可以找好下家再走!但我其实想得很清楚,我没可能要求一个组长委屈自己来适应我,他有他的管理方式,我有我的骄傲!我不喜欢麻烦的事,更不喜欢委屈自己,一个月后走人是最快解决的方案!


    4. 转机


    其实我的离开带来了一点影响,然后加上新组长那个产品质量问题警醒了领导,然后新组长被调去负责别的项目了,换个人来负责现在的项目组,而这个人就是我之前支援过的项目组组长,挺熟悉的!


    新新组长管理项目很有条理也很靠谱,之前支援的项目已经处于稳定运行的状态了,于是让他来接手这个项目!他特意找我谈话,劝我留下来,并且承诺以后我专心搞技术,他负责拖住领导催进度等问题!


    我本来主要就是因为新组长的问题才走人的,现在换了个不错的组长!可以啊!还能苟苟!


    5.反思



    1. 其实整件事情中,我也有错,因为跟对方闹掰了,就拒绝沟通,所以导致很多问题的发生,如果我主动沟通去说明开发难度的问题,并且争取时间,就不至于让自己处于一个精神内耗的不快乐状态。

    2. 发现问题后,我没有尝试去跟大组长反馈,让大组长去治治对方,或者让大组长帮忙处理这个矛盾,我真的太蠢了!

    3. 我性格其实挺暴躁的,看不顺眼就直接怼,讨厌的人就懒得搭理,这样的为人处世挺不讨喜的,得改改这坏脾气!


    作者:敲敲敲敲暴你脑袋
    来源:juejin.cn/post/7241884241616076858
    收起阅读 »

    怎么用一句话证明你在游戏公司里的最底层?

    引言 今天在知乎看到一个有趣的帖子:如何一句话证明你在公司最底层?我们把范围缩小到游戏公司。 关于这个问题,身边80%的朋友描述了自己在公司底层的难忘回忆,还有几位朋友甚至因为这不堪的回忆破防了。 刚进入游戏公司的新人,迷茫是常态。和大家一样,笔者也曾是公司的...
    继续阅读 »

    如何一句话证明你在公司最底层?


    引言


    今天在知乎看到一个有趣的帖子:如何一句话证明你在公司最底层?我们把范围缩小到游戏公司。


    关于这个问题,身边80%的朋友描述了自己在公司底层的难忘回忆,还有几位朋友甚至因为这不堪的回忆破防了。


    刚进入游戏公司的新人,迷茫是常态。和大家一样,笔者也曾是公司的最底层,总觉得每天一睁眼就是各种困难的事等着我:



    担心工作内容不会做,担心与同事沟通不好,担心自己考核不过关......



    今天的这篇文章,大家一起来看看一位位于游戏公司底层的游戏开发者的最底层体验。


    最底层体验


    图片源于网络


    1.介绍一下你自己


    大家好,我是XXX,来自XXX。虽然我是一个新人,但我对游戏充满了热情,这种热情已经伴随我多年。小时候,我就沉迷于各种游戏,从那时起,我就梦想着有一天能够为创造令人陶醉的游戏世界做出贡献。我加入这个行业的目标是成为一个出色的游戏开发者,并参与创造令人惊叹的游戏体验。我相信,通过与这个行业的优秀人才一起工作,我可以不断成长,并为我们的团队和项目做出贡献。谢谢大家,请多多指教。


    此处应有一阵热烈的掌声,那是对一位懵懂的游戏行业新人的勇敢表示敬畏。他或许不知道他的棱角将在这里被磨平。


    熟悉又让人崩溃的弹窗


    2.熟悉项目,体验游戏。


    游戏行业新人刚进到游戏公司,可能第一件事就是登陆公司内部使用的通讯工具。你的直属上司可能早早的在网线那头等候着你的上线。


    你好,XXX。你先接收一下这份文档,仔细阅读一下里面的内容。检出一下公司的游戏项目,然后根据文档把游戏跑起来。体验一下游戏,熟悉一下游戏的每个系统。有问题可以请教你旁边的那位大神,他负责带你。


    好的,谢谢。由于在来公司之前做足了准备,检出项目、运行项目这种小问题肯定难不倒你。这时候你会惊讶,原来这就是大型的商业化游戏项目,看起来有那么点高大上,但是最多的是还是看不懂。不过这游戏玩着好无聊,不是我喜欢的类型。想到未来的日子里,需要不停地重复地在这个游戏里面遨游,"真的会谢"。



    3.分配任务



    • 修改禅道bug序号XXX的问题。

    • 修改活动XXX文本显示异常问题。

    • 修改XXX报错问题,完成禅道单子序号1、2、3、4、5......。


    游戏行业新人的入门任务往往就是这些看起来微不足道,但是却非常细节的问题。正所谓不积跬步无以至千里,通过慢慢处理这些小小的bug和显示异常的问题,无疑是熟悉项目的最好方式。虽然这些都是比较基本的内容,修改bug、调整UI、修复报错。但是能够体现一个新人的基本功:阅读问题描述、理解问题描述、定位问题所在系统、定位系统所在代码、读懂代码原有逻辑、修改错误代码、验证问题是否修复、思考会不会对其他内容造成影响。


    这对于管理者来说是非常合理的,但对于新人来说,未免太过于简单了。


    支线任务


    4.支线任务


    游戏行业新人入门有可能并不能第一时间接触到游戏项目主分支的代码,往往是参与其他的一些分支版本,例如审核服(专门为了应对平台审核员的审核搭建的游戏服)、版署服(用于申请版号专门搭建的游戏服)、海外服(主要负责多语言版本的语言提取、翻译替换、本地化处理)等等。


    安排新人去处理这些支线任务,为的就是让新人从另外一个相对安全的分支去熟悉游戏项目,避免因新人的处理不当造成线上版本出问题,从而造成公司的经济损失。支线任务通常就是枯燥单一的体力劳动,不需要过多的技巧,只需要耐得住寂寞的心。


    图片源于网络


    5.几点下班


    一位有着远大抱负的新人,往往在刚进入公司的日子里,不知道几点下班。领导分配给我的任务,实在太简单了,三两下就完成了,还不到规定的时间。为了能够更加快速地熟悉项目,参与游戏功能的开发,继续研究代码。


    HR说19点下班,但是18点的时候大家都跑去吃饭,不解,跟着。等到19点的时候,果然没有人下班。继续奋笔疾书。20点的时候终于有人下班了,可是领导还是没动静,算了,再看看代码吧。21点,领导好像发现了这个新人,让他早点回去休息。(没有人告诉他,这将是常态。) "没事,我再看会代码,马上就回去了。"


    手机先吃


    6.福利


    同事: “公司发月饼了,你没去领吗?”,“不知道啊,没人通知。我刚来几天。”


    同事: ”我看大家都去领了,现在。“,”我不知道自己是否算正式员工“


    同事: ”你先去看看吧,反正大家都在领。“,兴致冲冲地跑到发月饼的地方。


    发月饼的: “叫什么名字?”,”XXX“


    发月饼的: ”名单上没这个人,不能领!“


    刚加入公司的时候,可能由于没转正或者名字还没有进入公司的名册,往往会导致有些福利不能享受。例如公司发月饼的时候,人人有份,唯独你。又或者公司发奖金,你拿200慰问金。公司发年终奖,你还是拿慰问金。 但是如果你想请假,领导秒批。甚至说你想离职,领导也是轻描淡写,“好的”。没有丝毫的牵挂留恋。这是前所未有的福利。


    结语


    不管怎样,虽然你是公司的最底层,但你是公司中最坚实的基石,因为你在每一颗砖石上都留下了你的汗水和努力,为了让整座大厦能够稳固地矗立在成功的巅峰。加油,请认真工作,积极向上。


    AD:笔者已经上线的小游戏《贪吃蛇掌机经典》《填色之旅》《重力迷宫球》大家可以自行点击搜索体验。


    感兴趣的小伙伴记得关注微信公众号"亿元程序员"哦,一位有着8年游戏行业经验的主程。学习游戏开发不迷路。感谢您的关注,希望能给到您帮助, 也希望通过您能帮助到大家。


    喜欢的可以点个、点个在看哦!请把该文章分享给你觉得有需要的其他小伙伴。谢谢。


    作者:亿元程序员
    来源:juejin.cn/post/7281589318329925689
    收起阅读 »

    uCharts 小程序地图下钻功能

    web
    uCharts 小程序地图下钻功能 最近在研究小程序图表,其中提到了一个地图下钻的功能,感觉挺有意思的,分享一下共同学习! 项目简介 这个Uni-App项目旨在提供一个可交互的地图,允许用户在中国地图的不同层级之间自由切换。用户可以从国家地图开始,然后深入到各...
    继续阅读 »

    uCharts 小程序地图下钻功能


    最近在研究小程序图表,其中提到了一个地图下钻的功能,感觉挺有意思的,分享一下共同学习!


    项目简介


    这个Uni-App项目旨在提供一个可交互的地图,允许用户在中国地图的不同层级之间自由切换。用户可以从国家地图开始,然后深入到各省份地图,最终进入城市地图,点击不同区域/县级市查看详细信息。


    下面是最终效果图👇👇


    1695175245503.png


    文档地址



    准备工作


    在开始之前,请确保你已经安装了Vue.js和Uni-App


    并且准备好了模拟地图数据。这些数据将用于绘制地图。


    地图数据遵循geoJson地图数据交换格式。如果你不熟悉geoJson,可以参考这里


    绘制中国地图


    // 首先引入我们的mock数据
    import mockData from '../../mock/index'

    // onLoad中调用 drawChina 方法来绘制中国地图
    drawChina() {
    uni.setNavigationBarTitle({
    title: '中国地图'
    });
    setTimeout(() => {
    let series = mockData.china.features;
    // 这里循环一下series,把需要的数据增加到serie的属性中,fillOpacity是根据数据来显示的颜色层级透明度
    for (var i = 0; i < series.length; i++) {
    // 这里模拟了随机数据,实际开发中请根据实际情况修改
    series[i].value = Math.floor(Math.random() * 1000)
    series[i].fillOpacity = series[i].value / 1000
    series[i].color = "#0D9FD8"
    }
    // 这里把series赋值给chartData,这样就可以在页面中渲染出来了
    this.chartData = {
    series: series
    };
    }, 100);
    }

    uCharts组件使用


    插件导入后在uni_modules中,命名规则符合easyCom,可以直接在页面中使用


    <qiun-data-charts
    type="map"
    canvas2d=""
    :chartData="chartData"
    :opts="opts"
    :inScrollView="true"
    :pageScrollTop="pageScrollTop"
    tooltipFormat="mapFormat"
    @getIndex="getIndex"
    @complete="complete"
    />

    注释说明:



    • chartData 包含地图数据

    • opts 是我们在 data 中定义的配置项

    • tooltipFormat 类型为字符串,需要指定为 config-ucharts.jsconfig-echarts.js 中 formatter 下的属性值,
      这里我们使用了 mapFormat,可以在 config-ucharts.js 中查看

    • 在页面中必须传入 pageScrollTop,并将 inScrollView 设置为 true,否则可能导致某些地图事件无法触发


    事件说明:



    • @complete 事件是地图绘制完成后触发的事件,我们可以在这个事件中获取地图的实例,
      然后可以调用地图的方法进行进一步操作。

    • @getIndex 事件是地图点击事件,我们可以获取到点击的地图信息,
      根据这个信息来判断是否需要进行下钻操作,如果需要下钻,可以替换 chartData 并重新绘制地图。


    下钻操作


      // 点击地图获取点击的索引
    getIndex(e) {
    console.log('点击地图', e);
    if (e.currentIndex > -1) {
    switch (this.layout) {
    case 'china':
    this.layout = 'province';
    break;
    case 'province':
    this.layout = 'city';
    break;
    case 'city':
    this.layout = 'area';
    break;
    default:
    uni.showModal({
    title: '提示',
    content: '当前已经是最后一级地图,点击空白回到中国地图',
    success: () => {

    }
    });
    break;
    }

    this.drawNext(e.currentIndex);
    } else {
    this.layout = 'china';
    this.drawChina();
    }
    }

    以上代码中,我们通过 currentIndex 来判断当前点击的是哪个地图,然后根据 layout 的值来判断是否需要进行下钻操作。
    如果需要下钻,我们就调用 drawNext 方法来绘制下一级地图。


    这个demo中,我们只模拟了中国地图、省级地图、市级地图和区县级地图,如果在开发中我们需要根据adcode请求后端接口来获取地图数据


    具体代码:git仓库地址


    作者:养乐多多多
    来源:juejin.cn/post/7278945628905226275
    收起阅读 »