注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

路才走了一半,为何停下?(2023 年中总结)

这篇文章姑且算的上是我 2023 年的年中总结。 我最近看到了这样一句话:“网站的流量是由于先前写的文章,你现在的成就是由于之前的努力或者有远见的选择。” 我现在的生活在我看来是舒适安逸的,有着不重的课业,单身,实习并拿着对学生来说不错的薪水,可预见的会到一个...
继续阅读 »

这篇文章姑且算的上是我 2023 年的年中总结。


我最近看到了这样一句话:“网站的流量是由于先前写的文章,你现在的成就是由于之前的努力或者有远见的选择。”


我现在的生活在我看来是舒适安逸的,有着不重的课业,单身,实习并拿着对学生来说不错的薪水,可预见的会到一个不错的公司,并且一直摆烂好像也有一个光明的未来,这份答卷是两年前的我,选定方向,然后持之以恒的努力所带来的。


然后呢,我现在的现状就是,得过且过,然后愉快的摆烂,长期躺尸带来的空虚感以及身体的虚弱感让我开始思考,如果我现在就丢下了笔,我半年后的答卷会在那里? 是依旧保持现状,被身后的小伙伴追上?还是在某一个瞬间醒悟,然后开始改变?


我认为我在前行路上丢掉了很多的东西,所以我想写篇文章来记录,来反思,这篇文章就是如此,所以这篇文章并不会有太多经历的回忆,当然,由于我这半年几乎一直在蔚来实习,其实生活也是相当的单调,接下来让我们进入正题吧。


image.png


Part One 人一旦停下了思考,未来的结果也就注定了。


看一下微信读书的记录吧:


一月份读了9h39min,二月份读了11小时21min,三月份只读了5h28min,四月份呢,也只有11h56min,五月份稍微好些,到了22h46min,六月份第一周,你只读了3min。


我知道你读了很多网络小说,比如《诡秘之主》这类小说,他确实有很多精妙的设计,你不需要动脑子,读起来当然舒适,我曾和一个朋友聊过纯粹的读书是什么,他说:“那本质上是一种玩物丧志。”我无意争论这些书是否会和我吃的饭一样融入血肉,只是对我个人而言,我希望他们留下些什么,每本书至少要让我学到一个道理,否则和沉迷某一件事物无法控制自己有什么区别?我不否认这很功利,但自我提升就是如此。


所以对前半年的第一个反思,就是真真正正的重启思考,为什么项目中的这个事情你没有考虑到,为什么你对边缘案例不够敏感?为什么一个月了这个事情你还没做完,尤其是当这个事情并不困难的情况下?哪些微小的习惯能使得你变得更坚韧?


我向来是一个行动者,但行动的往往太快了,太急于拿到这个结果,在一定程度上要学会思考,克服无脑的鲁莽。



要紧的是果敢的迈出第一步, 对与错先都不管,自古就没有把一切都设计好再开步的事。别想把一切都弄清楚,再去走路。鲁莽者要学会思考, 善思者要克服的是犹豫。 目的渴求完美,举步之际则无需周全。



共勉!


Part Two 像重病时一样珍重自己


在前两周我第一次阳了,高烧之下,身体极其难受,但反而想做些什么,打开莫言的《生死疲劳》,边读边想。


很奇怪,在这种难受的时刻,我把我的时间看的更加重要了,我不愿意去刷短视频,看没用的小说,我的清醒的时间并不多,在有限的时间中,我总先想把时间要用的更有价值。


有位朋友曾向我表明过他的一种人生态度:“我经常会想象,死亡过后是一种什么状态,虚无?黑暗?所以我每一天,都很感恩,我活着,所以我格外珍惜每一天,和我相处的每个人。”


当时生了病才明白,只有当我们意识到我们时间的珍贵,才会去珍惜当下的时间,以及相处的人。


那为什么不从现在开始,保持感恩,然后用好每天的时间呢?去见新的人也好,去学习也好,去锻炼也好,总之做我觉得有价值而非产生快感的事情。


image.png


Part Three 想想下一本读什么?


真正的阅读者每天给自己规定的读书时间是多少?3h?4h?8h?


是 1 min。


只有当开始不那么困难的时候,我们才会轻松的坚持下来,这个说的是开始。


而当我们开始阅读 1min,我们当然不会满足于此,我们还会继续去阅读。


读书是如此,背单词同样是如此,开启学习也是如此。


当我们读书/做事的时候,如果我们不想着下一件/下一本是什么,我们会做什么?


对于我个人来说,我会干完这个事情之后立刻觉得累了,躺在床上开始刷手机,一刷就是1h+,玩完了意识到自己的空虚,然后开始后悔,这就是我个人的惯性,或者说是习惯,怎么克服呢?


想想下一本读什么!


image.png


Part Four 几个道理,再开始吧


这里做了一个相当大的知识导图,我先放出来一部分截图吧~


总的来说就是觉得自己可以做的事情相当的多。


image.png


image.png


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

Kotlin和Swift的前世一定是兄

Swift介绍 Swift这门编程语言主要用于iOS和MacOS的开发,可以说是非常流行的一门编程语言,我只想说,如果你会Kotlin,那么你学习Swift会非常容易,反之亦然。下载XCode,然后你就可以创建Playground练习Swift语法了。 pl...
继续阅读 »

Swift介绍


Swift这门编程语言主要用于iOS和MacOS的开发,可以说是非常流行的一门编程语言,我只想说,如果你会Kotlin,那么你学习Swift会非常容易,反之亦然。下载XCode,然后你就可以创建Playground练习Swift语法了。
截屏2023-06-26 22.39.18.png
playground这个名字起的好,翻译成中文就是操场,玩的地方,也就是说,你可以尽情的测试你的Swift代码。


声明变量和常量


Kotlin的写法:

var a: Int = 10
val b: Float = 20f

Swift的写法:

var a: Int = 10
let b: Float = 20.0

你会发现它俩声明变量的方式一模一样,而常量也只是关键字不一样,数据类型我们暂不考虑。


导包


Kotlin的写法:

import android.app.Activity

Swift的写法:

import SwiftUI

这里kotlin和swift的方式一模一样。


整形


Kotlin的写法:

val a: Byte = -10
val b: Short = 20
val c: Int = -30
val d: Long = 40

Swift的写法:

let a: Int8 = -10
let b: Int16 = 20
let c: Int32 = -30
let d: Int = -30
let e: Int64 = 40
let f: UInt8 = 10
let g: UInt16 = 20
let h: UInt32 = 30
let i: UInt = 30
let j: UInt64 = 40

Kotlin没有无符号整型,Swift中Int32等价于Int,UInt32等价于UInt。无符号类型代表是正数,所以没有符号。


基本运算符


Kotlin的写法:

val a: Int = 10
val b: Float = 20f
val c = a + b

Swift的写法:

let a: Int = 10
let b: Float = 20
let c = Float(a) + b

Swift中没有隐式转换,Float类型不用写f。这里Kotlin没那么严格。


逻辑分支


Kotlin的写法:

val a = 65
if (a > 60) {
}

val b = 1
when (b) {
1 -> print("b等于1")
2 -> print("b等于2")
else -> print("默认值")
}

Swift的写法:

let a = 65
if a > 60 {
}

let b = 1
switch b {
case 1:
print("b等于1")
case 2:
print("b等于2")
default:
print("默认值")
}

Swift可以省略if的括号,Kotlin不可以。switch的写法倒是有点像Java了。


循环语句


Kotlin的写法:

for (i in 0..9) {
}

Swift的写法:

for var i in 0...9 {
}
// 或
for var i in 0..<10 {
}

Kotlin还是不能省略括号。


字符串


Kotlin的写法:

val lang = "Kotlin"
val str = "Hello $lang"

Swift的写法:

let lang = "Swift"
let str = "Hello \(lang)"

字符串的声明方式一模一样,拼接方式略有不同。


数组


Kotlin的写法:

val arr = arrayOf("Hello", "JYM")
val arr2 = emptyArray<String>()
val arr3: Array<String>

Swift的写法:

let arr = ["Hello", "JYM"]
let arr2 = [String]()
let arr3: [String]

数组的写法稍微有点不同。


Map和Dictionary


Kotlin的写法:

val map = hashMapOf<String, Any>()
map["name"] = "张三"
map["age"] = 100

Swift的写法:

let dict: Dictionary<String, Any> = ["name": "张三", "age": 100]

Swift的字典声明时必须初始化。Map和Dictionary的本质都是哈希。


函数


Kotlin的写法:

fun print(param: String) : Unit {
}

Swift的写法:

func print(param: String) -> Void {
}

func print(param: String) -> () {
}

除了关键字和返回值分隔符不一样,其他几乎一模一样。


高阶函数和闭包


Kotlin的写法:

fun showDialog(build: BaseDialog.() -> Unit) {
}

Swift的写法:

func showDialog(build: (dialog: BaseDialog) -> ()) {
}

Kotlin的高阶函数和Swift的闭包是类似的概念,用于函数的参数也是一个函数的情况。


创建对象


Kotlin的写法:

val btn = Button(context)

Swift的写法:

let btn = UIButton()

这里kotlin和swift的方式一模一样。


类继承


Kotlin的写法:

class MainPresenter : BasePresenter {
}

Swift的写法:

class ViewController : UIViewController {
}

这里kotlin和swift的方式一模一样。


Swift有而Kotlin没有的语法


guard...else的语法,通常用于登录校验,条件不满足,就执行else的语句,条件满足,才执行guard外面的语句。

guard 条件表达式 else {
}

另外还有一个重要的语法就是元组。元祖在Kotlin中没有,但是在一些其他编程语言中是有的,比如Lua、Solidity。元组主要用于函数的返回值,可以返回一个元组合,这样就相当于函数可以返回多个返回值了。
Swift的元组:

let group = ("哆啦", 18, "全宇宙最强吹牛首席前台")

Lua的多返回值:

function group() return "a","b" end

Solidity的元组:

contract MyContract {
mapping(uint => string) public students;

function MyContract(){
students[0] = "默认姓名";
students[1] = "默认年龄";
students[2] = "默认介绍";
}

function printInfo() constant returns(string,uint,string){
return("哆啦", 18, "全宇宙最强吹牛首席前台");
}
}

总结


编程语言很多地方都是相通的,学会了面向对象编程,你学习其他编程语言就会非常容易。学习一门其他编程语言的语法是很快的,但是要熟练掌握,还需要对该编程语言的API有大量的实践。还是那句话,编程语言只是工具,你的编程思维的高度才是决定你水平的重要指标。所以我给新入行互联网的同学的建议是,你可以先学习面向对象的编程思想,不用局限于一门语言,可以多了解下其他的编程语言,选择你更喜欢的方向。选择好后,再深耕一门技术。每个人的道不一样,可能你就更适合某一个方向。


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

Flutter这么火为什么不了解一下呢?(上)

Flutter这么火为什么不了解一下呢?(上) Flutter是Google移动UI框架,用以创建高质量的native接口,真正跨平台,同时在iOS和Android上运行。Flutter是免费开源的,全球开发者及组织均可以使用。 Flutter有又几个特点...
继续阅读 »

Flutter这么火为什么不了解一下呢?(上)



Flutter是Google移动UI框架,用以创建高质量的native接口,真正跨平台,同时在iOS和Android上运行。Flutter是免费开源的,全球开发者及组织均可以使用。



Flutter有又几个特点:

1.快速开发 毫秒级的热加载快速地将修改应用到app。使用丰富的可完全自定义的组件在几分钟内就可以构建native界面。


2.极具表现力,灵活的UI 快速地将特性集中到native终端用户体验。利用分层结构可以完整地自定义UI,进而完成快速绘制及灵活的设计。


3.native性能 Flutter组件包含了所有平台的关键差异,例如滚动,导航,图标和字体。使得Flutter在iOS和Android上使用可以获得完全的native性能体验。


快速开发

Flutter热加载技术有助于你快速且简单地进行试验,构建UI,增加特性,并且快速修复bug。体验不到一秒的重新加载体验。


img


漂亮的UI

Flutter内置MD设计风格及iOS组件,更有丰富的手势API,流畅的滚动体验和平台认同感会让用户感到愉悦。


img


img


现代的响应式框架(Modern,reactive framework)

利用Flutter响应式框架和丰富的平台,布局和功能组件是的UI构建非常简单。使用灵活并且强大的API(2D,动画,手势,性能等)可以解决在UI上各种问题。

  int counter = 0;

 void increment() {
   // Tells the Flutter framework that state has changed,
   // so the framework can run build() and update the display.
   setState(() {
     counter++;
  });
}

 Widget build(BuildContext context) {
   // This method is rerun every time setState is called.
   // The Flutter framework has been optimized to make rerunning
   // build methods fast, so that you can just rebuild anything that
   // needs updating rather than having to individually change
   // instances of widgets.
   return new Row(
     children: <Widget>[
       new RaisedButton(
         onPressed: increment,
         child: new Text('Increment'),
      ),
       new Text('Count: $counter'),
    ],
  );
}
}

使用native特性和SDKs

我们使用平台APIs,第三方SDKs和native代码开发APP。Flutter可以让你在iOS和Android继续使用Java,Swift,Objective-C代码并且使用native特性。


访问平台特性很简单。下边的代码片段开始:

    var batteryLevel = 'unknown';
   try {
       int result = await methodChannel.invokeMethod('getBatteryLevel');
       batteryLevel = 'Battery level: $result%';
    } on PlatformException {
       batteryLevel = 'Failed to get battery level.';
    }
     setState(() {
       _batteryLevel = batteryLevel;
    });
}

学习如何使用包(packages),或者写platform channels,使用native代码,APIs和SDKs。


统一的开发标准



Flutter拥有工具及库帮助你简单快速地在iOS和Android上实现你的想法。若你还没有任何移动开发经验,那么Flutter将会是你构建漂亮的移动APP的一种简单快速的额方式。若你是有经验的iOS或者Android开发人员,那么你可以使用Flutter组件,并且继续使用已有的Java/Objective-C/Swift程序。



构建 漂亮的APP UI 丰富的2D GPU加速APIs 响应式框架 动画/动作 APIs 兼容Android Material组件及苹果组件样式


流程的编码体验 急速热加载技术 IntelliJ:重构,自动补足功能等 Dart语言及核心库 包管理


拥有App所有特性 与移动OS APIs&SDKs互操作性 Maven/Java Cocoapods/ObjC/Swift


优化 测试 Unit测试 继承测试 无设备测试


Debug IDE debug 基于网络debug 异步/唤醒感知 表达式求值程序


配置 时间线 CPU和内存 应用性能图标


部署 编译 Native ARM程序 消除无效代码


发布 App市场 Play Store


标题安装Flutter



在国内安装Flutter需要首先需要一个值得信任的国内镜像。在镜像上边保存着Flutter需要的依赖及相关库,包等。为了使用Flutter,需要使用一个备用存储位置,我们需要配置环境变量。 配置环境变量名: PUB_HOSTED_URL 和 FLUTTER_STORAGE_BASE_URL。



在windows系统中,需要在环境变量设置中添加:


PUB_HOSTED_URL : pub.flutter-io.cn FLUTTER_STORAGE_BASE_URL : storage.flutter-io.cn


然后运行Git命令(前提是安装了GitBash工具):

git clone -b dev https://github.com/flutter/flutter.git Flutter


Flutter文件夹需要注意:文件夹存放的路径上不要出现空格,否则在IDE中进行工程创建后会有警告,SDK环境路径上存在分隔符。



在clone完成之后,即Flutter Sdk下载完毕,还需要配置Flutter环境: xxxx/Flutter/bin目录下。


重新打开一个命令行,在其中输入命令

flutter doctor

进行环境及缺失的依赖检查,并下载需要的依赖。 运行效果如下图:


img


在环境及相关依赖检查完成之后,可以开始在Android Studio中进行创建工程行为。



注意:Android Studio 预览版中无法保证运行Flutter成功。因此需要使用稳定版AS,且需要3.0版本以上。



Android Studio中需要安装Flutter Plugin,Dart Plugin两个插件。


Dart SDK也需要手动安装,直接下载zip包免安装。


成功准备好IDE环境之后,就可以创建Flutter Project了,默认创建Flutter Application就可以了,按照IDE创建提示一直到最终完成。



需要注意:同样由于网络环境,直接运行Flutter Project是不可行的,UI会一直停留在Gradle正在初始化工程。这时需要修改build.gradle配置中的中央Maven库到一个可信赖的公共Maven库。 这里我修改成Ali的Maven库

buildscript {
   ext.kotlin_version = '1.1.51'
   repositories {
       maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
       google()
  }
   // ......
}

// ......

allprojects {
   repositories {
       maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
  }
   google()
}
// ......


然后再次sync工程,进行运行。


img


首个创建的Flutter Project工程结构如下:


img



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

github365天保持常绿的“旁门左道”

目录 前言 项目初始化 自动提交的原理 编写核心代码 仓库的设置 总结 一、前言 作为一名程序员,我们肯定有自己的github账号,在github个人主页会展示你最近十二个月的提交情况,用绿色的深浅表示你当日提交次数的多少。如果你将个人的github主页链...
继续阅读 »

github365天保持常绿的“旁门左道”.png


目录



  1. 前言

  2. 项目初始化

  3. 自动提交的原理

  4. 编写核心代码

  5. 仓库的设置

  6. 总结


一、前言


作为一名程序员,我们肯定有自己的github账号,在github个人主页会展示你最近十二个月的提交情况,用绿色的深浅表示你当日提交次数的多少。如果你将个人的github主页链接放在简历上,面试官在面试时便可能会点进去,首先映入眼帘的便是这提交次数展示区域。但学生党平时要忙于个人的学业、社畜党要忙于自己的工作,几乎没有多少时间来做自己的项目、建设自己的github仓库,这样则可能会导致这片区域是空白的,像下面一样:



如果这片区域是空白的或者近十二个月只有零零散散的提交记录,说明个人github的活跃度不够,进而说明对技术探索学习的积极性也不够,这会给面试官留下不好的印象。


如果你在为你的github提交记录担忧,那么就可阅读本篇文章。本篇文章带大家借助GitHub Actions实现github每日自动提交,让你的github365天保持常绿,像下面这样:



当然这只是偷懒的做法,正如文章标题所说的“旁门左道”,可供娱乐和学习,但还是希望大家能有效的提交代码,这样才是真正巩固和学习技术的有效途径。


二、项目初始化


首先在自己的github上创建一个名为autocommit-robot的仓库,然后git clone 项目地址到本地打开即可,此处不赘述。



在项目根目录下执行npm init命令(根据自己实际需要进行配置,否则一直按回车键用默认配置即可)进行npm初始化。同时发现此时项目并没有.gitignore文件,可在项目根目录下新建一个.gitignore文件,配置按下面来即可:


.DS_Store
.vite-ssg-dist
.vite-ssg-temp
*.local
dist
dist-ssr
node_modules
.idea/
*.log

至此,项目的初始化工作便已完成,项目的目录如下所示:



三、自动提交的原理


本文章介绍的github自动提交功能是借助GitHub Actions实现的,Github Actions是最近几年新加进来的功能,可以用于给项目做CI/CD。原先你如果有这个需求,可能要借助Travis等三方网站。新建目录和配置文件 Github Actions使用yml格式做配置。首先要在你的项目中建立一下目录:.github/workflows/在workflows目录中新增yaml配置文件,文件名任意。通过目录名可以看出Github将此类任务称为“工作流”。其它关于Github Actions的基础使用,可看这篇文章:【白嫖Github Action做定时任务】。下面是我的.github\workflows\robot.yml内容:


name: autocommit-robot

on:
schedule:
- cron: "0 0 * * *"

jobs:
bots:
runs-on: ubuntu-latest
steps:
- name: "Checkout code"
uses: actions/checkout@v1

- name: "Set node"
uses: actions/setup-node@v1
with:
node-version: 16.x

- name: "Install"
run: npm install

- name: "Run bash"
run: node index.js

- name: "Commit"
uses: EndBug/add-and-commit@v4
with:
author_name: XC0703
author_email: example@qq.com
message: "feat: save robot"
add: "pictures/*"

env:
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}

上面这段内容定义了一个名为autocommit-robot的github工作流,其作用是每天自动将一张图片添加到Git仓库中。具体来说,它包含以下步骤:



  1. 设置GitHub Actions的运行时间为每天0点0分

  2. 在Ubuntu操作系统上运行该脚本

  3. 使用Node.js安装所需的依赖项

  4. 运行index.js文件,该文件中定义了从外部API获取一张图片并将其保存到pictures文件夹中的逻辑

  5. 使用add-and-commit操作自动将新添加的图片提交到Git仓库,并指定提交信息为feat: save robot


总的来说,这个脚本是一段自动化流程,能够帮助我们实现每天自动将一张图片添加到Git仓库并提交的功能。


注意:GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}为仓库范围的访问令牌,按照上面链接文章中所讲的步骤去申请一个个人令牌,然后将个人令牌的值存储在GitHub Secrets中(下面会讲怎么设置这个令牌)。这里后面的那个ACCESS_TOKEN为笔者自己的token名称,根据自己的实际情况进行替换,不要照搬。


四、仓库的设置


4.1 配置 GitHub Secrets


GitHub Secrets是 GitHub 提供的一种功能,用于存储和管理敏感数据、密钥和凭据。它们可用于在 GitHub Actions 或其他 CI/CD(持续集成/持续部署)流程中安全地访问这些敏感信息。


GitHub Secrets 的主要作用如下:



  1. 存储敏感数据:可以使用 GitHub Secrets 存储敏感数据,例如 API 密钥、数据库凭据、访问令牌等,而不是将它们明文写入代码或配置文件中。

  2. 保护代码安全:通过将敏感数据存储在 Secrets 中,可以避免将其提交到代码库中,从而降低了泄露敏感信息的风险。

  3. 安全访问凭据:在 CI/CD 流程中,可以使用 Secrets 来访问所需的敏感数据。例如,在构建和部署过程中,可以在脚本中使用 Secrets 来进行身份验证,而无需明文写入凭据。

  4. 多环境支持:Secrets 可以在不同的分支或环境中设置不同的值,这样可以轻松管理不同环境所需的不同凭据。

  5. 灵活的权限控制:GitHub Secrets 具有灵活的权限控制机制,可以根据需要对不同角色或团队进行访问限制,以确保敏感数据的安全性。


通俗地讲就是执行工作流的过程中,需要使用一些敏感数据(如个人令牌、服务器账号密码等),不能直接写在文件中,通过key/value的形式配置在 GitHub 中,使用的时候只需要输入key就能找到对应的value,这样就避免了将敏感数据直接暴露在文件中。


配置过程如下:




4.2 开启工作流权限


如果没有开启,执行工作流会报错。


配置如下:




五、编写核心代码


在第三步中提到每日要自动运行index.js文件,这个文件中定义了从外部API获取一张图片并将其保存到pictures文件夹中的逻辑。实现这个逻辑要借助requestpathfs三个依赖包,三个包作用如下:



  • request 模块用于向指定 URL 发送 HTTP 请求,并获取响应结果。它提供了对常见的 HTTP 动词(如 GET、POST 等)以及请求头、响应头等信息的控制。在这里,request 模块用于从 Robohash API 获取一张随机图片,将其作为响应结果返回。

  • path 模块提供了用于处理文件路径的相关方法,例如path.resolve() 用于将多个路径拼接成一个绝对路径,path.dirname() 用于获取一个路径的目录名部分,path.extname() 用于获取文件扩展名等。在这里,path 模块path.resolve() 方法被用于生成绝对路径,path.join() 方法可以将多个参数拼接成一个路径字符串。

  • fs 模块则提供了文件系统相关的操作方法,例如读写文件、创建文件夹、删除文件等。在这里,fs 模块的 fs.createWriteStream() 方法被用于创建可写流,将获取到的图片流写入到本地文件中。


npm install request path fs

index.js文件如下:


const request = require("request");
const path = require("path");
const fs = require("fs");
const id = (~~(Math.random() * 100000)).toString(); // 获取小于10w的数字
const url = `https://robohash.org/${id}`;
const dirPath = path.resolve(__dirname, "pictures");
// 这一步的处理,因为github获取的时间时区是美国时区,所以获取到的时间格式是6/16/2023,我们可以先split后将年份放到数组第一位,这样就是需要的文件名格式了
const dateArr = new Date().toLocaleDateString().split("/"); // 本地调试时用.toLocaleDateString("en")
dateArr.unshift(dateArr.pop());
const date = dateArr.join("-");
request(url).pipe(fs.createWriteStream(`${dirPath}/${date}.png`));

这段代码主要实现了从Robohash API获取一张随机生成的图片,并将其保存到本地指定文件夹中。具体来说,它包含以下步骤:



  1. 引入必要的模块:requestpathfs

  2. 生成一个小于10万的随机数字 id

  3. 使用 robohash.org 的API,构造访问地址 url,id作为参数的一部分

  4. 使用 path.resolve() 方法创建 dirPath 文件夹路径,用于存储图片文件

  5. 获取当前日期,并按照年-月-日的格式拼接成字符串 date

  6. 使用 request(url) 发起GET请求,获取图片资源流。可以理解为通过这个 url 去获得一张随机图片。

  7. 将获取到的图片资源流通过 pipe() 方法管道流到 fs.createWriteStream() 中,将其写入到 ${dirPath}/${date}.png 文件中,这样就将获取到的图片保存到了指定的本地文件夹。


最后,每当这段代码执行一次,就会从 robohash.org 获取一张随机的图片,并将其保存到本地的 ${dirPath}/${date}.png 文件中。


此时,项目开发完成,将本地更新推送到远程github仓库即可。


六、总结


本篇文章带大家借助GitHub Actions实现github每日自动提交的功能,项目源码地址如下:


github.com/XC0703/auto…


若不想自己重新开发一份,也可以直接fork我的仓库,这样我每日的自动提交也会同步到你们的仓库中。


最后,感谢您的阅读,如对您有

作者:明远湖之鱼
来源:juejin.cn/post/7253673249159200825
帮助,欢迎点赞收藏。

收起阅读 »

MySQL:我的从库竟是我自己!?

本文将通过复制场景下的异常分析,介绍手工搭建MySQL主从复制时需要注意的关键细节。 作者:秦福朗 爱可生 DBA 团队成员,负责项目日常问题处理及公司平台问题排查。热爱互联网,会摄影、懂厨艺,不会厨艺的 DBA 不是好司机,didi~ 本文来源:原创投稿 ...
继续阅读 »

本文将通过复制场景下的异常分析,介绍手工搭建MySQL主从复制时需要注意的关键细节。



作者:秦福朗


爱可生 DBA 团队成员,负责项目日常问题处理及公司平台问题排查。热爱互联网,会摄影、懂厨艺,不会厨艺的 DBA 不是好司机,didi~


本文来源:原创投稿




  • 爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。


背景


有人反馈装了一个数据库,来做现有库的从库。做好主从复制关系后,在现有主库上使用 show slave hosts; 管理命令去查询从库的信息时,发现从库的 IP 地址竟是自己的 IP 地址,这是为什么呢?


因生产环境涉及 IP,端口等保密信息,以下以本地环境来还原现象。


本地复现


基本信息


主库从库
IP10.186.65.3310.186.65.34
端口66076607
版本8.0.188.0.18

问题现象


不多说,先上图,以下为在主库执行 show slave hosts; 出现的现象:



可以看到这里的 Host 是主库的 IP 地址。


我们登陆从库查看一下 show slave status\G


mysql> show slave status\G
*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: 10.186.65.33
Master_User: universe_op
Master_Port: 6607
Connect_Retry: 60
Master_Log_File: mysql-bin.000002
Read_Master_Log_Pos: 74251749
Relay_Log_File: mysql-relay.000008
Relay_Log_Pos: 495303
Relay_Master_Log_File: mysql-bin.000002
Slave_IO_Running: Yes
Slave_SQL_Running: Yes

我们看到确实从库是在正常运行的,且复制的源就是主库。


为什么执行 show 命令看到的 Host 和实际的情况对不上呢?


查阅资料


首先查阅官方文档,关于 show slave hosts; 语句的解释:




  • 首先说明 8.0.22 之后版本的 show slave hosts 语句被废弃(可执行),改为 show replicas,具体机制还是一样的。

  • 这里说明了各个数据的来源,多数来源于 report-xxxx 相关参数,其中 Host 的数据来自于从库的 report_host 这个参数。


然后,我们测试在从库执行 show variables like "%report%";


mysql> show variables like "%report%";
+-----------------+--------------+
| Variable_name | Value |
+-----------------+--------------+
| report_host | 10.186.65.33 |
| report_password | |
| report_port | 6607 |
| report_user | |
+-----------------+--------------+
4 rows in set (0.01 sec)

可以看到这里显示的就是主库的 IP。


我们再查询 report_host 的参数基本信息:



可以看到该参数非动态配置,在从库注册时上报给主库,所以主库上执行 show slave hosts; 看到的是 IP 是从这里来的,且无法在线修改。


最后也通过查看从库上的 my.cnf 上的 report_port 参数,证实确实是主库的 IP:



结论


经了解,生产上的从库是复制了主库的配置文件来部署的,部署时没有修改 report_host 这个值,导致启动建立复制后将 report_host 这个 IP 传递给主库,然后主库查询 show slave hosts 时就出现了自己的 IP,让主库怀疑自己的从库竟然是自己。


生产上大部分人知道复制主库的配置文件建立新库要修改 server_id 等相关 ID 信息,但比较容易忽略掉 report_ipreport_port 等参数的修改,这个需要引起注意,虽然错误之后看起来对

作者:爱可生开源社区
来源:juejin.cn/post/7254043722946199609
复制运行是没影响的。

收起阅读 »

短链的原理

1. 什么是短链? 在使用网址访问访问网站的时候,可能都会遇到一个很长的链接,多则可能几百个字符,这样的链接看起来非常的长 https://www.google.com/search?q=%E9%95%BF%E9%93%BE%E9%93%BE%E6%8E%A5...
继续阅读 »

1. 什么是短链?


在使用网址访问访问网站的时候,可能都会遇到一个很长的链接,多则可能几百个字符,这样的链接看起来非常的长


https://www.google.com/search?q=%E9%95%BF%E9%93%BE%E9%93%BE%E6%8E%A5&rlz=1C5GCEM_enCN1065&oq=%E9%95%BF%E9%93%BE%E9%93%BE%E6%8E%A5&aqs=chrome..69i57j0i13i512j0i10i13i512l2j0i13i30j0i10i13i30j0i13i30l4.4703j0j15&sourceid=chrome&ie=UTF-8


像以上这种,就是长链链接,看起来非常的臃肿,虽然说链接里面携带的一些参数使链接变长是不可避免的,但是我们可以使用另一种方法,既能使链接携带参数也能使链接为短链接,后面会讲到短链的原理和实现方式。


https://juejin.cn/post/7254039051588599864


像以上这种就是短链,使用短链有以下几种优点。



  1. 相比长链更加简洁。

  2. 便于使用,粘贴复制分享给别人时较为便捷,有些平台对分享内容的长度有所限制(微博只能发140字),这个时候使用短链可以输入更多的内容。

  3. 短链生成的二维码更容易识别。


2. 短链的原理


实现短链的核心原理就是链接映射表和302临时重定向


当我们用户用短链访问服务器时,服务器会将用户携带的短链在服务器中的链接映射表中寻找唯一与之对应的长链接,寻找到后返回重定向,重定向至长链接的网页,这样用户就可以不直接携带长链接,而是携带一个短链接,间接的去访问长链接,这个中间层就是服务器,服务器会在中间做一个链接映射和重定向的操作。


image.png


3. 短链的实现方式


3.1. 自增id法


自增ID的方法也叫做永不重复法,即采用发号器原理来实现,每一个url对应一个数字,然后自增,可以理解为ID,然后将ID进行相应的转换(比如进制转换),由于ID是唯一的,所以转换出来的结果也是唯一的。短网址的长度一般设为 6 位,而每一位是由 [a - z, A - Z, 0 - 9] 总共 62 个字母组成的,所以 6 位的话,总共会有 62^6 ~= 568亿种组合,基本上够用了。


3.2. 算法生成


将长链接做一次哈希算法之后就可以生产一个短链接。


我们都知道哈希算法是一种摘要算法,它的作用是:对任意一组输入数据进行计算,得到一个固定长度的输出摘要。


我们常见的哈希算法有:MD5、SHA-1、SHA-256、SHA-512 算法等。但我们最好还是使用另一种叫做 MurmurHash 的哈希算法。


因为 MD5 和 SHA 哈希算法,它们都是加密的哈希算法,也就是说我们无法从哈希值反向推导出原文,从而保证了原文的保密性。


但对于我们这个场景而言,我们并不关心安全性,我们关注的是运算速度以及哈希冲突。而 MurmurHash 算法是一个非加密哈希算法,所以它的速度回更快。


哈希冲突


学过 HashMap 的同学都知道,哈希冲突是哈希算法不可避免的问题。而解决哈希冲突的方式有两种,分别是:链表法和重哈希法。HashMap 使用了链表法,但我们这里使用的是重哈希法。


所谓的重哈希法,指的是当发生哈希冲突的时候,我们在原有长链后面加上固定的特殊字符,后续拿

作者:1in
来源:juejin.cn/post/7254039051588599864
出长链时再将其去掉.

收起阅读 »

5分钟,带你迅速上手“Markdown”语法

web
本篇将重点讲解:Markdown的 “语法规范” 与 “上手指南”。 一、Markdown简介 Markdown是一种文本标记语言,它容易上手、易于学习,排版清晰明了、直观清晰。常用于撰写 “技术文档” 、 “技术博客” 、 “开发文档” 等等。 总之,如...
继续阅读 »

本篇将重点讲解:Markdown的 “语法规范”“上手指南”





一、Markdown简介


Markdown是一种文本标记语言,它容易上手、易于学习,排版清晰明了、直观清晰。常用于撰写 “技术文档”“技术博客”“开发文档” 等等。
总之,如果你是一名开发者,并且你有写博客的欲望与想法时,使用Markdown是你不二的选择。




二、Markdown语法


接下来,我们来看一下Markdown“标准语法”


我们看下大纲,其中包括:



1、标题


  • 标准语法:使用1~6“#”符 + “空格” + “你的标题”。


# 一级标题
## 二级标题
### 三级标题
#### 四级标题
##### 五级标题
###### 六级标题


  • 效果图解:




注:#和「标题」之间有一个空格,这是最标准的语法格式。
有些编辑器做了兼容,有的并没有。所以最好要加上空格。



2、列表


  • 标准语法:使用-符,在文本前加入-符即可。


- 文本1
- 文本2
- 文本3

如果你希望有序,在文本前加上1. 2. 3. 4. ...


1. 文本1
2. 文本2
3. 文本3


注:-1. 2. 等和文本之间要保留一个字符的空格。




  • 效果图解:



3、超链接



  • 标准语法:[链接名](链接url)




  • 效果图解:





4、图片


  • 标准语法:


![图片名](链接url)


  • 效果图解:



5、引用



  • 标准语法:> 文本




  • 效果图解:





6、斜体、加粗



  • 标准语法
    斜体*文本*
    加粗**文本**
    斜体&加粗***文本***




  • 效果图解:





7、代码块



  • 标准语法:
    ``` 你的代码 ```(前面3个点,后面3个点)




  • 效果图解:







8、表格


  • 标准语法:


dog | bird | cat
----|------|----
foo | foo | foo
bar | bar | bar
baz | baz | baz


  • 效果图解:





9、特殊标记


  • 标准语法:``


`特殊样式`


  • 效果图解:





10、分割线



  • 标准语法:--- 最少3个




  • 效果图解:





11、常用html标记

注意:html标记只适合辅助使用,不一定所有编辑器都能生效。



  • 标准语法:


换行符:<br/> (或者使用Markdown标准语法:空格+空格+回车,但我感觉不是很直观)
上:<sup>文本</sup>
下:<sub>文本</sub>



  • 效果图解:





三、Markdown优点



  • 纯文本,所以兼容性极强,可以用所有文本编辑器打开。

  • 让作者更专注于写作而不是排版。(大家都是技术人员嘛..)

  • 格式转化方便,markdown文本可以很轻松转成htmlpdf等等。(图个方便嘛)

  • 语法简单

  • 可读性强,配合表格、引用、代码块等等,让读者瞬间“懂
    作者:齐舞647
    来源:juejin.cn/post/7254107670012510245
    你”。

收起阅读 »

🤣泰裤辣!这是什么操作,自动埋点,还能传参?

web
前言 在上篇文章讲了如何通过手写babel插件自动给函数埋点之后,就有同学问我,自动插入埋点的函数怎么给它传参呢?这篇文章就来解决这个问题我讲了通过babel来实现自动化埋点,也讲过读取注释给特定函数插入埋点代码,感兴趣的同学可以来这里 给所有函数都添加埋...
继续阅读 »


前言


在上篇文章讲了如何通过手写babel插件自动给函数埋点之后,就有同学问我,自动插入埋点的函数怎么给它传参呢?这篇文章就来解决这个问题
我讲了通过babel来实现自动化埋点,也讲过读取注释给特定函数插入埋点代码,感兴趣的同学可以来这里





效果是这样的
源代码:


//##箭头函数
//_tracker
const test1 = () => {};

const test1_2 = () => {};

转译之后:


import _tracker from "tracker";
//##箭头函数
//_tracker
const test1 = () => {
_tracker();
};

const test1_2 = () => {};

代码中有两个函数,其中一个//_tracker的注释,另一个没有。转译之后只给有注释的函数添加埋点函数。
要达到这个效果就需要读取函数上面的注释,如果注释中有//_tracker,我们就给函数添加埋点。这样做避免了僵硬的给每个函数都添加埋点的情况,让埋点更加灵活。




那想要给插入的埋点函数传入参数应该怎么做呢?
传入参数可以有两个思路,



  • 一个是将参数也放在注释里面,在babel插入代码的时候读取下注释里的内容就好了;

  • 另一个是将参数以局部变量的形式放在当前作用域中,在babel插入代码时读取下当前作用域的变量就好;


下面我们来实现这两个思路,大家挑个自己喜欢的方法就好


参数放在注释中


整理下源代码


import "./index.css";

//##箭头函数
//_tracker,_trackerParam={name:'gongfu', age:18}
const test1 = () => {};

//_tracker
const test1_2 = () => {};


代码中,有两个函数,每个函数上都有_tracker的注释,其中一个注释携带了埋点函数的参数,待会我们就要将这个参数放到埋点函数里



关于如何读取函数上方的注释,大家可以这篇文章:(),我就不赘述了




准备入口文件


index.js


const { transformFileSync } = require("@babel/core");
const path = require("path");
const tracker = require("./babel-plugin-tracker-comment.js");

const pathFile = path.resolve(__dirname, "./sourceCode.js");

//transform ast and generate code
const { code } = transformFileSync(pathFile, {
plugins: [[tracker, { trackerPath: "tracker", commentsTrack: "_tracker",commentParam: "_trackerParam" }]],
});

console.log(code);



和上篇文章的入口文件类似,使用了transformFileSyncAPI转译源代码,并将转译之后的代码打印出来。过程中,将手写的插件作为参数传入plugins: [[tracker, { trackerPath: "tracker", commentsTrack: "_tracker"}]]。除此之外,还有插件的参数



  • trackerPath表示埋点函数的路径,插件在插入埋点函数之前会检查是否已经引入了该函数,如果没有引入就需要额外引入。

  • commentsTrack标识埋点,如果函数前的注释有这个,就说明函数需要埋点。判断的标识是动态传入的,这样比较灵活

  • commentParam标识埋点函数的参数,如果注释中有这个字符串,那后面跟着的就是参数了。就像上面源代码所写的那样。这个标识不是固定的,是可以配置化的,所以放在插件参数的位置上传进去


编写插件


插件的功能有:



  • 查看埋点函数是否已经引入

  • 查看函数的注释是否含有_tracker

  • 将埋点函数插入函数中

  • 读取注释中的参数


前三个功能在上篇文章(根据注释添加埋点)中已经实现了,下面实现第四个功能


const paramCommentPath = hasTrackerComments(leadingComments, options.commentsTrack);
if (paramCommentPath) {
const param = getParamsFromComment(paramCommentPath, options);
insertTracker(path, param, state);
}

//函数实现
const getParamsFromComment = (commentNode, options) => {
const commentStr = commentNode.node.value;
if (commentStr.indexOf(options.commentParam) === -1) {
return null;
}

try {
return commentStr.slice(commentStr.indexOf("{"), commentStr.indexOf("}") + 1);
} catch {
return null;
}
};

const insertTracker = (path, param, state) => {
const bodyPath = path.get("body");
if (bodyPath.isBlockStatement()) {
let ast = template.statement(`${state.importTackerId}(${param});`)();
if (param === null) {
ast = template.statement(`${state.importTackerId}();`)();
}
bodyPath.node.body.unshift(ast);
} else {
const ast = template.statement(`{
${state.importTackerId}(${param});
return BODY;
}`
)({ BODY: bodyPath.node });
bodyPath.replaceWith(ast);
}
};


上述代码的逻辑是检查代码是否含有注释_tracker,如果有的话,再检查这一行注释中是否含有参数,最后再将埋点插入函数。
在检查是否含有参数的过程中,用到了插件参数commentParam。表示如果注释中含有该字符串,那后面的内容就是参数了。获取参数的方法也是简单的字符串切割。如果没有获取到参数,就一律返回null



获取参数的复杂程度,取决于。先前是否就有一个规范,并且编写代码时严格按照规范执行。
像我这里的规范是埋点参数commentParam和埋点标识符_tracker必须放在一行,并且参数需要是对象的形式。即然是对象的形式,那这一行注释中就不允许就其他的大括号符号"{}"
遵从了这个规范,获取参数的过程就变的很简单了。当然你也可以有自己的规范



在执行插入逻辑的函数中,会校验参数param是否为null,如果是null,生成ast的时候,就不传入param了。



当然你也可以一股脑地传入param,不会影响结果,顶多是生成的埋点函数会收到一个null的参数,像这样_tracker(null)



第四个功能也实现了,来看下完整代码


完整代码


const { declare } = require("@babel/helper-plugin-utils");
const { addDefault } = require("@babel/helper-module-imports");
const { template } = require("@babel/core");
//judge if there are trackComments in leadingComments
const hasTrackerComments = (leadingComments, comments) => {
if (!leadingComments) {
return false;
}
if (Array.isArray(leadingComments)) {
const res = leadingComments.filter((item) => {
return item.node.value.includes(comments);
});
return res[0] || null;
}
return null;
};

const getParamsFromComment = (commentNode, options) => {
const commentStr = commentNode.node.value;
if (commentStr.indexOf(options.commentParam) === -1) {
return null;
}

try {
return commentStr.slice(commentStr.indexOf("{"), commentStr.indexOf("}") + 1);
} catch {
return null;
}
};

const insertTracker = (path, param, state) => {
const bodyPath = path.get("body");
if (bodyPath.isBlockStatement()) {
let ast = template.statement(`${state.importTackerId}(${param});`)();
if (param === null) {
ast = template.statement(`${state.importTackerId}();`)();
}
bodyPath.node.body.unshift(ast);
} else {
const ast = template.statement(`{
${state.importTackerId}(${param});
return BODY;
}`
)({ BODY: bodyPath.node });
bodyPath.replaceWith(ast);
}
};


const checkImport = (programPath, trackPath) => {
let importTrackerId = "";
programPath.traverse({
ImportDeclaration(path) {
const sourceValue = path.get("source").node.value;
if (sourceValue === trackPath) {
const specifiers = path.get("specifiers.0");
importTrackerId = specifiers.get("local").toString();
path.stop();
}
},
});

if (!importTrackerId) {
importTrackerId = addDefault(programPath, trackPath, {
nameHint: programPath.scope.generateUid("tracker"),
}).name;
}

return importTrackerId;
};

module.exports = declare((api, options) => {

return {
visitor: {
"ArrowFunctionExpression|FunctionDeclaration|FunctionExpression|ClassMethod": {
enter(path, state) {
let nodeComments = path;
if (path.isExpression()) {
nodeComments = path.parentPath.parentPath;
}
// 获取leadingComments
const leadingComments = nodeComments.get("leadingComments");
const paramCommentPath = hasTrackerComments(leadingComments,options.commentsTrack);

// 如果有注释,就插入函数
if (paramCommentPath) {
//add Import
const programPath = path.hub.file.path;
const importId = checkImport(programPath, options.trackerPath);
state.importTackerId = importId;

const param = getParamsFromComment(paramCommentPath, options);
insertTracker(path, param, state);
}
},
},
},
};
});



运行代码


现在可以用入口文件来使用这个插件代码了


node index.js

执行结果
image.png



运行结果符合预期



可以看到我们设置的埋点参数确实被放到函数里面了,而且注释里面写了什么,函数的参数就会放什么,那么既然如此,可以传递变量吗?我们来试试看


import "./index.css";

//##箭头函数
//_tracker,_trackerParam={name, age:18}
const test1 = () => {
const name = "gongfu2";
};

const test1_2 = () => {};

在需要插入的代码中,声明了一个变量,然后注释的参数刚好用到了这个变量。
运行代码看看效果
image.png
可以看到,插入的参数确实用了变量,但是引用变量却在变量声明之前,这肯定不行🙅。得改改。
需要将埋点函数插入到函数体的后面,并且是returnStatement的前面,这样就不会有问题了


const insertTrackerBeforeReturn = (path, param, state) => {
//blockStatement
const bodyPath = path.get("body");
let ast = template.statement(`${state.importTackerId}(${param});`)();
if (param === null) {
ast = template.statement(`${state.importTackerId}();`)();
}
if (bodyPath.isBlockStatement()) {
//get returnStatement, by body of blockStatement
const returnPath = bodyPath.get("body").slice(-1)[0];
if (returnPath && returnPath.isReturnStatement()) {
returnPath.insertBefore(ast);
} else {
bodyPath.node.body.push(ast);
}
} else {
ast = template.statement(`{ ${state.importTackerId}(${param}); return BODY; }`)({ BODY: bodyPath.node });
bodyPath.replaceWith(ast);
}
};

这里将insertTracker改成了insertTrackerBeforeReturn
其中关键的逻辑是判断是否是一个函数体,



  • 如果是一个函数体,就判断有没有return语句,

    • 如果有return,就放在return前面

    • 如果没有return,就放在整个函数体的后面



  • 如果不是一个函数体,就直接生成一个函数体,然后将埋点函数放在return的前面


再来运行插件:
image.png



很棒,这就是我们要的效果😃




完整代码


const { declare } = require("@babel/helper-plugin-utils");
const { addDefault } = require("@babel/helper-module-imports");
const { template } = require("@babel/core");
//judge if there are trackComments in leadingComments
const hasTrackerComments = (leadingComments, comments) => {
if (!leadingComments) {
return false;
}
if (Array.isArray(leadingComments)) {
const res = leadingComments.filter((item) => {
return item.node.value.includes(comments);
});
return res[0] || null;
}
return null;
};

const getParamsFromComment = (commentNode, options) => {
const commentStr = commentNode.node.value;
if (commentStr.indexOf(options.commentParam) === -1) {
return null;
}

try {
return commentStr.slice(commentStr.indexOf("{"), commentStr.indexOf("}") + 1);
} catch {
return null;
}
};

const insertTrackerBeforeReturn = (path, param, state) => {
//blockStatement
const bodyPath = path.get("body");
let ast = template.statement(`${state.importTackerId}(${param});`)();
if (param === null) {
ast = template.statement(`${state.importTackerId}();`)();
}
if (bodyPath.isBlockStatement()) {
//get returnStatement, by body of blockStatement
const returnPath = bodyPath.get("body").slice(-1)[0];
if (returnPath && returnPath.isReturnStatement()) {
returnPath.insertBefore(ast);
} else {
bodyPath.node.body.push(ast);
}
} else {
ast = template.statement(`{ ${state.importTackerId}(${param}); return BODY; }`)({BODY: bodyPath.node });
bodyPath.replaceWith(ast);
}
};


const checkImport = (programPath, trackPath) => {
let importTrackerId = "";
programPath.traverse({
ImportDeclaration(path) {
const sourceValue = path.get("source").node.value;
if (sourceValue === trackPath) {
const specifiers = path.get("specifiers.0");
importTrackerId = specifiers.get("local").toString();
path.stop();
}
},
});

if (!importTrackerId) {
importTrackerId = addDefault(programPath, trackPath, {
nameHint: programPath.scope.generateUid("tracker"),
}).name;
}

return importTrackerId;
};

module.exports = declare((api, options) => {

return {
visitor: {
"ArrowFunctionExpression|FunctionDeclaration|FunctionExpression|ClassMethod": {
enter(path, state) {
let nodeComments = path;
if (path.isExpression()) {
nodeComments = path.parentPath.parentPath;
}
// 获取leadingComments
const leadingComments = nodeComments.get("leadingComments");
const paramCommentPath = hasTrackerComments(leadingComments,options.commentsTrack);

// 如果有注释,就插入函数
if (paramCommentPath) {
//add Import
const programPath = path.hub.file.path;
const importId = checkImport(programPath, options.trackerPath);
state.importTackerId = importId;

const param = getParamsFromComment(paramCommentPath, options);
insertTrackerBeforeReturn(path, param, state);
}
},
},
},
};
});


参数放在局部作用域中


这个功能的关键就是读取当前作用域中的变量。


在写代码之前,来定一个前提:当前作用域的变量名也和注释中参数标识符一致,也是_trackerParam


准备源代码


import "./index.css";

//##箭头函数
//_tracker,_trackerParam={name, age:18}
const test1 = () => {
const name = "gongfu2";
};

const test1_2 = () => {};

//函数表达式
//_tracker
const test2 = function () {
const age = 1;
_trackerParam = {
name: "gongfu3",
age,
};
};

const test2_1 = function () {
const age = 2;
_trackerParam = {
name: "gongfu4",
age,
};
};

代码中,准备了函数test2test2_1。其中都有_trackerParam作为局部变量,但test2_1没有注释//_tracker


编写插件


if (paramCommentPath) {
//add Import
const programPath = path.hub.file.path;
const importId = checkImport(programPath, options.trackerPath);
state.importTackerId = importId;

//check if have tackerParam
const hasTrackParam = path.scope.hasBinding(options.commentParam);
if (hasTrackParam) {
insertTrackerBeforeReturn(path, options.commentParam, state);
return;
}

const param = getParamsFromComment(paramCommentPath, options);
insertTrackerBeforeReturn(path, param, state);
}

这个函数的逻辑是先判断当前作用域中是否有变量_trackerParam,有的话,就获取该声明变量的初始值。然后将该变量名作为insertTrackerBeforeReturn的参数传入其中。
我们运行下代码看看
image.png



运行结果符合预期,很好




完整代码


const { declare } = require("@babel/helper-plugin-utils");
const { addDefault } = require("@babel/helper-module-imports");
const { template } = require("@babel/core");
//judge if there are trackComments in leadingComments
const hasTrackerComments = (leadingComments, comments) => {
if (!leadingComments) {
return false;
}
if (Array.isArray(leadingComments)) {
const res = leadingComments.filter((item) => {
return item.node.value.includes(comments);
});
return res[0] || null;
}
return null;
};

const getParamsFromComment = (commentNode, options) => {
const commentStr = commentNode.node.value;
if (commentStr.indexOf(options.commentParam) === -1) {
return null;
}

try {
return commentStr.slice(commentStr.indexOf("{"), commentStr.indexOf("}") + 1);
} catch {
return null;
}
};

const insertTrackerBeforeReturn = (path, param, state) => {
//blockStatement
const bodyPath = path.get("body");
let ast = template.statement(`${state.importTackerId}(${param});`)();
if (param === null) {
ast = template.statement(`${state.importTackerId}();`)();
}
if (bodyPath.isBlockStatement()) {
//get returnStatement, by body of blockStatement
const returnPath = bodyPath.get("body").slice(-1)[0];
if (returnPath && returnPath.isReturnStatement()) {
returnPath.insertBefore(ast);
} else {
bodyPath.node.body.push(ast);
}
} else {
ast = template.statement(`{${state.importTackerId}(${param}); return BODY;}`)({BODY: bodyPath.node });
bodyPath.replaceWith(ast);
}
};


const checkImport = (programPath, trackPath) => {
let importTrackerId = "";
programPath.traverse({
ImportDeclaration(path) {
const sourceValue = path.get("source").node.value;
if (sourceValue === trackPath) {
const specifiers = path.get("specifiers.0");
importTrackerId = specifiers.get("local").toString();
path.stop();
}
},
});

if (!importTrackerId) {
importTrackerId = addDefault(programPath, trackPath, {
nameHint: programPath.scope.generateUid("tracker"),
}).name;
}

return importTrackerId;
};

module.exports = declare((api, options) => {

return {
visitor: {
"ArrowFunctionExpression|FunctionDeclaration|FunctionExpression|ClassMethod": {
enter(path, state) {
let nodeComments = path;
if (path.isExpression()) {
nodeComments = path.parentPath.parentPath;
}
// 获取leadingComments
const leadingComments = nodeComments.get("leadingComments");
const paramCommentPath = hasTrackerComments(leadingComments,options.commentsTrack);

// 如果有注释,就插入函数
if (paramCommentPath) {
//add Import
const programPath = path.hub.file.path;
const importId = checkImport(programPath, options.trackerPath);
state.importTackerId = importId;

//check if have tackerParam
const hasTrackParam = path.scope.hasBinding(options.commentParam);
if (hasTrackParam) {
insertTrackerBeforeReturn(path, options.commentParam, state);
return;
}

const param = getParamsFromComment(paramCommentPath, options);
insertTrackerBeforeReturn(path, param, state);
}
},
},
},
};
});


总结:


这篇文章讲了如何才埋点的函数添加参数,参数可以写在注释里,也可以写在布局作用域中。支持动态传递,非常灵活。感兴趣的金友们可以拷一份代码下来跑一跑,相信你们会很有成就感的。


下篇文章来讲讲如何在create-reate-app中使用我们手写的babel插件。



相关文章:



  1. 通过工具babel,给函数都添加埋点

  2. 通过工具babel,根据注释添加埋点


作者:慢功夫
来源:juejin.cn/post/7254032949229764669

收起阅读 »

👀SSO单点登录 知多少

为撒我们要使用单点登录 通常情况下我们开发一套系统时,我们都会写一套登录页面。当用户需要访问系统时需要先进行账号登录然后进入系统使用。但是当我们有多个系统时难道需要用户登录多次吗?显然这种方式是不优雅的😶‍🌫️,此时就轮到单点登录登场了。本文将主要以前端的视角...
继续阅读 »

为撒我们要使用单点登录


通常情况下我们开发一套系统时,我们都会写一套登录页面。当用户需要访问系统时需要先进行账号登录然后进入系统使用。但是当我们有多个系统时难道需要用户登录多次吗?显然这种方式是不优雅的😶‍🌫️,此时就轮到单点登录登场了。本文将主要以前端的视角来聊一聊该如何落地单点登录。



单点登录的英文名叫做:Single Sign On(简称SSO)😶‍🌫️



单点登录能干嘛


简单粗暴的说单点登录就是



一处登录 处处登录 🤡



单点登录在前端的系统模型


单点登录实施方案


单点登录目前有两种方案



  • cookie 共享方案(子域名方案)

  • ticket 方案(凭证交换)


这两个方案是有共同点的,我们看看它们有哪些共同的部分



  • 需要一个独立的登录系统

  • 通用重定向的方式完成子系统登录



结合以上可以发现,这两个方案其实在前端的差异性并不大,主要就是需要考虑如何实现登录态共享。



cookie 共享方案


传统的登录方式是用户在登录页完成登录,然后后端通过 set-cookie 将 token 写入到浏览器的 cookie 当中之后的每次请求都会携带token完成身份验证。此时的登录模块和业务模块是同一系统下的不同页面。



单点登录模型下的登录模块和业务模块是独立的不同系统




结合以上流程图我们来分析一波,图中的Asso都是example.com的子域名,当用户访问A-client时会向A-server发送身份验证请求检验当前请求是否携带有效token,如无效则返回特定 code A-client接到返回后根据 code 做出不同响应。



  • A-server返回 token 有效,则继续保持对A-client的访问。

  • A-server返回 token 无效。


    • A-client重定向至SSO-client并携带redirect参数http://sso.example.com?redirect=a.example.com




    • 当用户在SSO-client完成登录后(SSO-server 通过 set-cookie 设置 token 为子域名可访问),再通过redirect参数重定向回A-client




    • 回到A-clientA-client再次向A-server发起身份验证,这次请求会默认携带上domain=example.com的cookie.token,A-server收到token检验有效后,返回用户信息继续保持用户对A-client的有效访问。






至此单点登录的核心逻辑就梳理完成了。



注意:以上是以每套系统都有独立的后端服务(同源)做出的流程,但是有时候我们的后端服可能是同一个,这时候我们在 ABC系统中的请求可能会有跨域/跨站等问题。
解决办法也很多,需要看是跨域,还是跨站问题。
这里贴一个跨域的处理方法 note.youdao.com/s/Tjuuv8Mv 也可以用反向代理去规避跨域问题。




同源:协议+主机名+端口 都相同

同站:二级域名+顶级域名 相同即可



ticket 临时凭证


ticket 方案总体上和 cookie 方案总体差不多,主要区别在于获取 token 的形式不同。

cookie 方案主要是依赖浏览的特性来完成 token 共享。

ticket 则是由各系统携带 ticket 这个临时凭证去换取token



从上图可以看出 ticket 方案不同的地方在于:




  • 用户首次访问A-client时 token 不存在A-client会重定向至SSO-client,当用户在SSO-client登录成功后SSO-server会 set-cookie 将 token 注入到浏览器中(7天免登录),并返回 ticket 参数SSO-client收到登录成功结果后再次带着 ticket 参数重定向回A-client (http://a.example.com?ticket=xxxxxxx)




  • 此时A-client时会多一个 ticket 参数。




  • A-client中的 token 过期时或不存在时,如果 ticket 存在A-server就会带着ticket去请求SSO-server检验ticket是否有效,当SSO-server返回有效的话就表示用户在SSO中是有效登录的。




  • 验证成功后A-server下发 token set-cookie 进A-client的cookie中。




至此完成登录。



以上多是以前端的角度在分析单点登录的实现逻辑,还有很多可以修改的地方。例如后端是同一套服务,token是放在cookie,还是local storage或是JS内存中都是根据业务需求改变的。




以及安全问题,这里就不展开讨论啦



作者:Wanp
来源:juejin.cn/post/7254027262885855291
收起阅读 »

作为一名前端给自己做一个算命转盘不过分吧

web
算命转盘 前言 给自己做一个算命转盘,有事没事算算命,看看运势挺好的(虽然我也看不懂)。 这个算命转盘我是实现在了自己的个人博客中的这里是地址,感兴趣可以点进去看看。 实现过程 开发技术:react + ts 该转盘主要是嵌套了三层 圆形滚动组件 来实现的,...
继续阅读 »

算命转盘


zodiac.gif

前言


给自己做一个算命转盘,有事没事算算命,看看运势挺好的(虽然我也看不懂)。


这个算命转盘我是实现在了自己的个人博客中的这里是地址,感兴趣可以点进去看看。


实现过程


开发技术:react + ts


该转盘主要是嵌套了三层 圆形滚动组件 来实现的,再通过 ref 绑定组件,调用其中的 scrollTo 方法即可使组件发生指定的滚动,再传入随机数,即可实现随机旋转效果,通过嵌套三层该组件实现三层的随机旋转,模拟“算命”效果。


// 这是精简后的代码
export default () => {
const onScrollCircle = () => {
const index = Math.floor(Math.random() * zodiacList.length)
scrollCircleRef.current?.scrollTo({index, duration: 1000})
}
return (
<>
<ScrollCircle ref={scrollCircleRef}></ScrollCircle>
<button onClick={() => onScrollCircle}>点击旋转</button>
</>

)
}

三层大致结构如下:具体代码可以看码上掘金



  • 转盘的第一层


export default () => {
return (
<ScrollCircle>
{list.map((item, index) => (
<ScrollCircle.Item
key={index}
index={index}
>

<CircleItem />
</ScrollCircle.Item>
))}
</ScrollCircle>

)
}


  • 转盘的第二层


const CircleItem = () => {
return (
<ScrollCircle>
{list.map((item, index) => (
<ScrollCircle.Item
key={index}
index={index}
>

<CircleItemChild />
</ScrollCircle.Item>
))}
</ScrollCircle>

)
}


  • 转盘的第三层


const CircleItemChild = () => {
return (
<ScrollCircle>
{list.map((item, index) => (
<ScrollCircle.Item
key={index}
index={index}
>

<div>
内容
</div>
</ScrollCircle.Item>
))}
</ScrollCircle>

)
}

圆形滚动组件


现在的 圆形滚动组件 支持展示到上下左右中各个方向上,要是大家使用过程中有什么意见可以提一下,我尽力实现,当然能提 pr 最好了(∪^ェ^∪)。


组件源码地址


线上Demo演示地址


image.png

主要是在旧版的基础上不断完善而来的,旧版圆形滚动组件的 往期文章


props等使用文档


ScrollCircle


属性名描述类型默认值
listLength传入卡片的数组长度number(必选)
width滚动列表的宽度string"100%"
height滚动列表的高度string"100%"
centerPoint圆心的位置"center" , "auto" , "left" , "right" , "bottom" , "top""auto (宽度大于高度时在底部,否则在右侧)"
circleSize圆的大小"inside" , "outside""outside (圆溢出包裹它的盒子)"

其他的属性...(篇幅问题就不全放上来了,可以直接去线上Demo演示地址查看)


centerPoint


主要通过该属性,将圆心控制到上下左右中间位置。


属性名描述
auto自动适应,当圆形区域宽度大于高度时,圆心会自动在底部,否则在右边
center建议搭配 circleSize='inside' 一起使用(让整个圆形在盒子内部)
left让圆心在左边
top让圆心在顶部
right让圆心在右边
bottom让圆心在底部

作者:滑动变滚动的蜗牛
来源:juejin.cn/post/7254014646779428922
收起阅读 »

前端行业:是否已走到了十字路口?

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

一、一些迹象


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



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


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


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


二、原因分析


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


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


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


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


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


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



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


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


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


三、破局之道


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


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


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


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


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


提高核心竞争力


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


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


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


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


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


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



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


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


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


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


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


举几个例子:


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


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


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


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


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


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



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


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


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


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


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


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


框架的能力


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



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


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


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


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


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


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


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


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


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


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


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


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



  • 比他人涉猎更广,例如音视频处理、图形表现实现或者Node开发有较多经验;

  • JS、CSS等前端基本功扎实,积累深厚,各种API特性了然于心,最佳实践信手捏来;

  • 具有设计审美或者产品嗅觉灵敏,开发的产品体验非常好,干活很细。


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


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



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


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


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


四、未来如何


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


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


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


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


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


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


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


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


前端的未来


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


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


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


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


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


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


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



原文地址:blog.csdn.net/m0_60961651…



作者:嚣张农民
来源:juejin.cn/post/7254097346762211385
收起阅读 »

Android 实现单指滑动、双指缩放照片

一、前景提示 最近接到一个查看大图的需求,现在图片展示还不够大,要求还要能缩小能放大还能保存照片。直接开始Google实现方式。 二、实现功能 根据查询到的结果分为两种,一个是使用手势监听来实现,第二种监听触摸事件来实现 手势监听-- ScaleGestur...
继续阅读 »

一、前景提示


最近接到一个查看大图的需求,现在图片展示还不够大,要求还要能缩小能放大还能保存照片。直接开始Google实现方式。


二、实现功能


根据查询到的结果分为两种,一个是使用手势监听来实现,第二种监听触摸事件来实现



  • 手势监听-- ScaleGestureDetector Google提供的手势监听类

  • 触摸事件--OnTouchListener 自己监听触摸事件自己实现放大缩小的逻辑


2.1 手势监听


先写布局文件

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_example"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="Hello World!"
android:scaleType="fitCenter"
android:src="@drawable/muffin_7870491_1920"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

再去实现手势监听方法

class MainActivity : AppCompatActivity() {
private lateinit var mScaleGestureDetector: ScaleGestureDetector
private var mScaleFactor: Float = 1.0f
private lateinit var mImageView: AppCompatImageView

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mImageView = findViewById(R.id.iv_example)
mScaleGestureDetector = ScaleGestureDetector(this, ScaleGestureListener())
mImageView.setOnTouchListener { _, event ->
mScaleGestureDetector.onTouchEvent(event)
true
}
}


private inner class ScaleGestureListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
mScaleFactor *= detector.scaleFactor
// 限制缩放因子在0.1到10.0
mScaleFactor = mScaleFactor.coerceIn(0.1f, 10.0f)
mImageView.scaleX = mScaleFactor
mImageView.scaleY = mScaleFactor
return true
}
}
}

代码很简单直接使用ScaleGestureDetector去监听触摸事件,手势本质也是Google内部监听事件判断再回调给我们使用。当然我们这里不去查看源码,只看实现过程。
在使用过程中发现这种缩放并不平滑,而且响应有点慢,有延迟。猜想内部是由很多其他的判断吧。那我们只想简单一点怎么搞呢,那就是自己去判断缩放,还有实现单指滑动用手势也不太好实现的样子。所以我们试试第二种方式实现也就是触摸事件。


2.2 触摸事件


首先我们实现一下缩放,我们还是沿用上次使用onTouchListener来处理我们的触摸事件,布局文件中需要把imageView的缩放属性改为矩阵 android:scaleType="matrix"

private var startMatrix = Matrix()
mImageView.setOnTouchListener { _, event ->
when(event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_POINTER_DOWN -> {
// 记录双指按下的位置和距离
startDistance = getDistance(event)
if (startDistance > 10f) {
startMatrix.set(mImageView.imageMatrix)
mode = 2
}
return@setOnTouchListener true
}
}
true
}

没有自己处理过触摸事件的小伙伴可能会好奇MotionEvent.ACTION_MASK是什么,其实这个是为了处理多点触摸事件加的一个flag和action做and操作,我们就能处理ACTION_POINTER_DOWN和ACTION_POINTER_UP这两个多点触摸事件。
看下代码逻辑,我们先计算两个手指的距离,如果距离大于10就证明是缩放操作,设置成我们自己定义的模式,再把imageView的矩阵保存,后续对照片移动,缩放都是通过变换矩阵来实现的。
至于计算两个手指之间的距离用的勾股定理,来个示意图,大家就明白了。


yuque_diagram.jpg


计算如下。

 private fun getDistance(event: MotionEvent): Float {
val dx = event.getX(0) - event.getX(1)
val dy = event.getY(0) - event.getY(1)
return sqrt(dx * dx + dy * dy)
}

通过计算能得到直角边和邻边,对他们使用勾股定理就能得到斜边的值,也就是两个手指之间的距离。
有做过触摸事件监听的同学就应该知道,我们下一步要监听移动事件了也就是MotionEvent.ACTION_MOVE。

mImageView.setOnTouchListener { _, event ->
when (event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_POINTER_DOWN -> {
// 记录双指按下的位置和距离
startDistance = getDistance(event)
if (startDistance > 10f) {
startMatrix.set(mImageView.imageMatrix)
mode = 2
}
return@setOnTouchListener true
}
MotionEvent.ACTION_MOVE -> {
if (mode == 2) {
// 双指缩放
val currentDistance = getDistance(event)
if (currentDistance > 10f) {
val scale = currentDistance / startDistance
mImageView.imageMatrix = startMatrix.apply {
postScale(scale, scale, getMidX(event), getMidY(event))
}
}
}
return@setOnTouchListener true
}
MotionEvent.ACTION_POINTER_UP -> {
mode = 0
return@setOnTouchListener true
}

else -> return@setOnTouchListener true

}

}

这里在move事件中我们也需要对手指之间的距离进行计算,如果距离超过10,就开始计算缩放倍数,通过postScale进行矩阵变换。
在MotionEvent.ACTION_POINTER_UP事件中对mode值进行复位操作,毕竟还有个单指拖动操作。
如果大家把上面的代码运行过就会发现怎么图片没有居中显示,这是因为我们的缩放属性被改为矩阵也就是android:scaleType="matrix",那么想要图片居中显示怎么操作呢,只需要在触摸时去改变缩放属性,其他的时候不变即可。
我们把imageView恢复成android:scaleType="fitCenter",在onTouchListener中加入(放在when前即可)

mImageView.scaleType = ImageView.ScaleType.MATRIX

这样一开始就可以保持图片在中央了。
这样缩放功能实现了,下面实现单指拖动功能,思路很简单记录第一次按下的位置,在移动过程中计算应该需要偏移的距离,再记录下当前的位置,以便于下次计算。

private var lastX = 0f
private var lastY = 0f
mImageView.setOnTouchListener { _, event ->
mImageView.scaleType = ImageView.ScaleType.MATRIX
when (event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_DOWN -> {
// 记录单指按下的位置
lastX = event.x
lastY = event.y
mode = 1
startMatrix.set(mImageView.imageMatrix)
return@setOnTouchListener true
}
MotionEvent.ACTION_POINTER_DOWN -> {
// 记录双指按下的位置和距离
startDistance = getDistance(event)
if (startDistance > 10f) {
startMatrix.set(mImageView.imageMatrix)
mode = 2
}
return@setOnTouchListener true
}
MotionEvent.ACTION_MOVE -> {
if (mode == 1) {
// 单指拖动
val dx = event.x - lastX
val dy = event.y - lastY
mImageView.imageMatrix = startMatrix.apply {
postTranslate(dx, dy)
}
lastX = event.x
lastY = event.y
} else if (mode == 2) {
// 双指缩放
val currentDistance = getDistance(event)
if (currentDistance > 10f) {
val scale = currentDistance / startDistance
mImageView.imageMatrix = startMatrix.apply {
postScale(scale, scale, getMidX(event), getMidY(event))
}
}
}
return@setOnTouchListener true
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> {
mode = 0
return@setOnTouchListener true
}

else -> return@setOnTouchListener true

}

}

代码实现和思路一样,我们还需要在MotionEvent.ACTION_UP中复位模式,调用postTranslate进行偏移。
这样基本上功能我们都简单实现了。下面我们就需要优化了代码,如果各位跟着实现了,就会发现缩放倍数太大了导致轻轻动一下就会放很大,还有别的都是需要我们优化的。


三、功能优化


3.1 优化缩放倍数太大问题


其实这个问题和我们处理move事件有关系,熟悉Android事件机制都知道一个完整的事件流程就是down->move.....move->up。知道了这个之后,再仔细看我们的代码

val currentDistance = getDistance(event)
if (currentDistance > 10f) {
val scale = currentDistance / startDistance
mImageView.imageMatrix = startMatrix.apply {
postScale(scale, scale, getMidX(event), getMidY(event))
}
}

在move事件中我们这样处理的,计算缩放倍数然后缩放,大体一看是没有什么问题的。但是,我们的move事件不止执行一次,这就导致我们的缩放不止执行一次,每次都是在原来的基础上放大或者缩小。所以轻轻移动倍数就会很多。
最简单的办法就是我们记录一下move过程中累计的倍数,如果到达最大值或者最小值就不让放大或者缩小了。代码如下。

if (scale > 1.0f) {
sumScale += scale
} else {
sumScale -= scale
}
if (sumScale >= maxScale || sumScale <= minScale) {
return@setOnTouchListener true
}

简单但是有效的方式。其中max和min,可以自己赋值。


3.2 保持原图不缩小


实现起来也很简单,需要先定义一个变量记录当前缩放之后的倍数。大家测试就会发现,如果是放大操作那么倍数就会大于1如果是缩小倍数就会比1 小。我们就可以利用这点来处理我们的逻辑。

private var lastScaleFactor = 1f
if (scale * lastScaleFactor > 1.0f) {
if (sumScale >= maxScale || sumScale <= minScale) {
return@setOnTouchListener true
}
sumScale += scale
mImageView.imageMatrix = startMatrix.apply {
postScale(scale, scale, getMidX(event), getMidY(event))
lastScaleFactor *= scale
}
} else {
sumScale -= scale
}

demo在这里点我点我


tips:demo好像不是放大不是很顺畅,但是在项目里用Gilde加载后很流畅,猜测是照片大小问题。但是思路是一样的问题不大。


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

Android动态权限申请从未如此简单

前言 注:只想看实现的朋友们可以直接跳到最后面的最终实现 大家是否还在为动态权限申请感到苦恼呢?传统的动态权限申请需要在Activity中重写onRequestPermissionsResult方法来接收用户权限授予的结果。试想一下,你需要在一个子模块中申请权...
继续阅读 »

前言


注:只想看实现的朋友们可以直接跳到最后面的最终实现


大家是否还在为动态权限申请感到苦恼呢?传统的动态权限申请需要在Activity中重写onRequestPermissionsResult方法来接收用户权限授予的结果。试想一下,你需要在一个子模块中申请权限,那得从这个模块所在的ActivityonRequestPermissionsResult中将结果一层层再传回到这个模块中,相当的麻烦,代码也相当冗余和不干净,逼死强迫症。


使用


为了解决这个痛点,我封装出了两个方法,用于随时随地快速的动态申请权限,我们先来看看我们的封装方法是如何调用的:

activity.requestPermission(Manifest.permission.CAMERA, onPermit = {
//申请权限成功 Do something
}, onDeny = { shouldShowCustomRequest ->
//申请权限失败 Do something
if (shouldShowCustomRequest) {
//用户选择了拒绝并且不在询问,此时应该使用自定义弹窗提醒用户授权(可选)
}
})

这样是不是非常的简单便捷?申请和结果回调都在一个方法内处理,并且支持随用随调。


方案


那么,这么方便好用的方法是怎么实现的呢?不知道小伙伴们在平时开发中有没有注意到过,当你调用startActivityForResult时,AS会提示你该方法已被弃用,点进去看会告诉你应该使用registerForActivityResult方法替代。没错,这就是androidx给我们提供的ActivityResult功能,并且这个功能不仅支持ActivityResult回调,还支持打开文档,拍摄照片,选择文件等各种各样的回调,同样也包括我们今天要说的权限申请


其实Android在官方文档 请求运行时权限 中就已经将其作为动态权限申请的推荐方法了,如下示例代码所示:

val requestPermissionLauncher =
registerForActivityResult(RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
// Permission is granted. Continue the action or workflow in your
// app.
} else {
// Explain to the user that the feature is unavailable because the
// feature requires a permission that the user has denied. At the
// same time, respect the user's decision. Don't link to system
// settings in an effort to convince the user to change their
// decision.
}
}

when {
ContextCompat.checkSelfPermission(
CONTEXT,
Manifest.permission.REQUESTED_PERMISSION
) == PackageManager.PERMISSION_GRANTED -> {
// You can use the API that requires the permission.
}
shouldShowRequestPermissionRationale(...) -> {
// In an educational UI, explain to the user why your app requires this
// permission for a specific feature to behave as expected, and what
// features are disabled if it's declined. In this UI, include a
// "cancel" or "no thanks" button that lets the user continue
// using your app without granting the permission.
showInContextUI(...)
}
else -> {
// You can directly ask for the permission.
// The registered ActivityResultCallback gets the result of this request.
requestPermissionLauncher.launch(
Manifest.permission.REQUESTED_PERMISSION)
}
}

说到这里,可能有小伙伴要质疑我了:“官方文档里都写明了的东西,你还特地写一遍,还起了这么个标题,是不是在水文章?!”


莫急,如果你遵照以上方法这么写的话,在实际调用的时候会直接发生崩溃:

java.lang.IllegalStateException: 
LifecycleOwner Activity is attempting to register while current state is RESUMED.
LifecycleOwners must call register before they are STARTED.

这段报错很明显的告诉我们,我们的注册工作必须要在Activity声明周期STARTED之前进行(也就是onCreate时和onStart完成前),但这样我们就必须要事先注册好所有可能会用到的权限,没办法做到随时随地有需要时再申请权限了,有办法解决这个问题吗?答案是肯定的。


绕过生命周期检测


想解决这个问题,我们必须要知道问题的成因,让我们带着问题进到源码中一探究竟:

public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
@NonNull ActivityResultContract<I, O> contract,
@NonNull ActivityResultCallback<O> callback) {
return registerForActivityResult(contract, mActivityResultRegistry, callback);
}

public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
@NonNull final ActivityResultContract<I, O> contract,
@NonNull final ActivityResultRegistry registry,
@NonNull final ActivityResultCallback<O> callback) {
return registry.register(
"activity_rq#" + mNextLocalRequestCode.getAndIncrement(), this, contract, callback);
}

public final <I, O> ActivityResultLauncher<I> register(
@NonNull final String key,
@NonNull final LifecycleOwner lifecycleOwner,
@NonNull final ActivityResultContract<I, O> contract,
@NonNull final ActivityResultCallback<O> callback) {

Lifecycle lifecycle = lifecycleOwner.getLifecycle();

if (lifecycle.getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {
throw new IllegalStateException("LifecycleOwner " + lifecycleOwner + " is "
+ "attempting to register while current state is "
+ lifecycle.getCurrentState() + ". LifecycleOwners must call register before "
+ "they are STARTED.");
}

registerKey(key);
LifecycleContainer lifecycleContainer = mKeyToLifecycleContainers.get(key);
if (lifecycleContainer == null) {
lifecycleContainer = new LifecycleContainer(lifecycle);
}
LifecycleEventObserver observer = new LifecycleEventObserver() { ... };
lifecycleContainer.addObserver(observer);
mKeyToLifecycleContainers.put(key, lifecycleContainer);

return new ActivityResultLauncher<I>() { ... };
}

我们可以发现,registerForActivityResult实际上就是调用了ComponentActivity内部成员变量的mActivityResultRegistry.register方法,而在这个方法的一开头就检查了当前Activity的生命周期,如果生命周期位于STARTED后则直接抛出异常,那我们该如何绕过这个限制呢?


其实在register方法的下面就有一个同名重载方法,这个方法并没有做生命周期的检测:

public final <I, O> ActivityResultLauncher<I> register(
@NonNull final String key,
@NonNull final ActivityResultContract<I, O> contract,
@NonNull final ActivityResultCallback<O> callback) {
registerKey(key);
mKeyToCallback.put(key, new CallbackAndContract<>(callback, contract));

if (mParsedPendingResults.containsKey(key)) {
@SuppressWarnings("unchecked")
final O parsedPendingResult = (O) mParsedPendingResults.get(key);
mParsedPendingResults.remove(key);
callback.onActivityResult(parsedPendingResult);
}
final ActivityResult pendingResult = mPendingResults.getParcelable(key);
if (pendingResult != null) {
mPendingResults.remove(key);
callback.onActivityResult(contract.parseResult(
pendingResult.getResultCode(),
pendingResult.getData()));
}

return new ActivityResultLauncher<I>() { ... };
}

找到这个方法就简单了,我们将registerForActivityResult方法调用替换成activityResultRegistry.register调用就可以了


当然,我们还需要注意一些小细节,检查生命周期的register方法同时也会注册生命周期回调,当Activity被销毁时会将我们注册的ActivityResult回调移除,我们也需要给我们封装的方法加上这个逻辑,最终实现就如下所示。


最终实现

private val nextLocalRequestCode = AtomicInteger()

private val nextKey: String
get() = "activity_rq#${nextLocalRequestCode.getAndIncrement()}"

fun ComponentActivity.requestPermission(
permission: String,
onPermit: () -> Unit,
onDeny: (shouldShowCustomRequest: Boolean) -> Unit
) {
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
onPermit()
return
}
var launcher by Delegates.notNull<ActivityResultLauncher<String>>()
launcher = activityResultRegistry.register(
nextKey,
ActivityResultContracts.RequestPermission()
) { result ->
if (result) {
onPermit()
} else {
onDeny(!ActivityCompat.shouldShowRequestPermissionRationale(this, permission))
}
launcher.unregister()
}
lifecycle.addObserver(object : LifecycleEventObserver {
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event == Lifecycle.Event.ON_DESTROY) {
launcher.unregister()
lifecycle.removeObserver(this)
}
}
})
launcher.launch(permission)
}

fun ComponentActivity.requestPermissions(
permissions: Array<String>,
onPermit: () -> Unit,
onDeny: (shouldShowCustomRequest: Boolean) -> Unit
) {
var hasPermissions = true
for (permission in permissions) {
if (ContextCompat.checkSelfPermission(
this,
permission
) != PackageManager.PERMISSION_GRANTED
) {
hasPermissions = false
break
}
}
if (hasPermissions) {
onPermit()
return
}
var launcher by Delegates.notNull<ActivityResultLauncher<Array<String>>>()
launcher = activityResultRegistry.register(
nextKey,
ActivityResultContracts.RequestMultiplePermissions()
) { result ->
var allAllow = true
for (allow in result.values) {
if (!allow) {
allAllow = false
break
}
}
if (allAllow) {
onPermit()
} else {
var shouldShowCustomRequest = false
for (permission in permissions) {
if (!ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) {
shouldShowCustomRequest = true
break
}
}
onDeny(shouldShowCustomRequest)
}
launcher.unregister()
}
lifecycle.addObserver(object : LifecycleEventObserver {
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event == Lifecycle.Event.ON_DESTROY) {
launcher.unregister()
lifecycle.removeObserver(this)
}
}
})
launcher.launch(permissions)
}

总结


其实很多实用技巧本质上都是很简单的,但没有接触过就很难想到,我将我的开发经验分享给大家,希望能帮助到大家。


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

IDEA 常用配置指南

1、外观配置 1.1 基本配置图 1.1-1 修改更改主题 + 背景图片 如果IDEA版本是2023.1.2以后的版本可以开启 newUI 体验新版的UI界面,我个人是挺喜欢的🌝 1.2 快捷键配置图1.2-1 修改快捷键 2、配置开发环境 2.1 配置G...
继续阅读 »

1、外观配置


1.1 基本配置

图 1.1-1 修改更改主题 + 背景图片
image-20230705155717189.png


如果IDEA版本是2023.1.2以后的版本可以开启 newUI 体验新版的UI界面,我个人是挺喜欢的🌝



1.2 快捷键配置

图1.2-1 修改快捷键
image-20230705160242097.png

2、配置开发环境


2.1 配置GIT

图2.1-1配置git
image-20230705161247492.png

2.2 配置maven



2.3 配置JDK
















图 2.3-1 配置项目的JDK
image-20230707163308798.png
image-20230707163311898.png


配置项目的语言版本指的是设置 JDK 版本,比如有些比较老的项目需要JDK-6启动,此时不需要安装JDK-6,可以把语言版本设置为6即可运行项目。



3、编辑器设置


3.1 基本配置













图 3.1-1 代码补全提示去掉匹配规则
image-20230705162157455.png












图 3.1-2 配置字体样式和大小
image-20230705162514296.png


字体建议使用等宽字体,如 Consolas 或者 JetBrains Mono,最近英特尔开源的 intel-one-mono 字体也挺好用的,喜欢的话可以安装一下。














图3.1-3 配置行号显示和方法分割
image-20230706084729859.png

3.3 编码风格配置













图 3.3-1 Java引入折叠
image-20230705162853383.png












图 3.3-2 Java代码超字符数换行
image-20230706090037457.png

3.4 配置代码模板


可以生成常见的代码模板,方便开发使用



  1. 配置文件模板,如图3.4-1所示;

  2. 配置文件的所有者信息,新建文件后会添加在类的头部,如图3.4-2所示;

  3. 代码生成模板,输入关键词,点击回车后即可触发生成代码,配置如图 3.4-3所示,使用效果如图3.4-4所示;













图 3.4-1 文件生成模板
image-20230705163048097.png












图 3.4-2 文件头设置
image-20230705163545871.png


















图 3.4-3 配置代码生成模板
image-20230705165216751.png
image-20230705164753838.png
image-20230705164825676.png


代码生成模板建议建立新的分组后,在新的分组内编写代码生成模板。

















图 3.4-4 代码模板使用效果
image-20230705165006840.png
image-20230705165017812.png

3.5 配置编辑器编码格式


建议都设置为 UTF-8 避免出现文件乱码问题













3.5-1 配置编辑器编码格式
image-20230705163642870.png

3.6 配置忽略的文件和文件夹


建议在忽略文件和文件夹内配置IDEA编辑器生成的文件,防止git提交时提交IDEA配置文件被打🦉













图 3.6-1 配置忽略的文件和文件夹
image-20230705165811144.png

4、好玩的插件

插件名称
Alibaba Java Coding阿里巴巴代码规范检查插件,根据《Java开发手册》进行代码规范性检查
Easy Code代码生成器,配置好代码模板后,可以从数据库一键生成从dao层到service层的代码
GenerateSerialVersionUID一键生成SerivalVersionUID
Jrebel and XRebel热部署插件,代码更改后点击build可以免重启热部署
JSON ParserJSON 格式化,在IDEA内增加JSON格式化窗口
LeetCode Editor实现IDEA内刷LeetCod题目
MyBatisX方便找到mapper和XML的映射
Rainbow Brackets彩虹括号,方便查看代码
Star Wars Progress Bar星球大战主题进度条
Translation翻译插件,支持翻译代码、注释等等
camelCase快捷的从全大写、下划线、大驼峰、小驼峰命名之间切换
作者:RealPluto
链接:https://juejin.cn/post/7253743476390740005
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

这么好的Android开发辅助工具App不白嫖可惜了

过年期间闲来没事,手撸了一个辅助Android开发调试的工具App,适合Android开发者和测试同学使用。 Github地址下载, Gitee地址下载(需要登录gitee) 或者去Google Play安装 功能概览 对我这样的懒人开发者来说,反复的做同样一...
继续阅读 »

过年期间闲来没事,手撸了一个辅助Android开发调试的工具App,适合Android开发者和测试同学使用。


Github地址下载,
Gitee地址下载(需要登录gitee)


或者去Google Play安装


功能概览


对我这样的懒人开发者来说,反复的做同样一件事简直太煎熬了,因此我把我平时开发中需要反复操作的命令和一些繁琐的操作整理成了一个工具。


废话不多说, 先上图了解下工具的大概功能有哪些(内容比截图丰富,欢迎下载体验)












CodeCrafts的核心是一个可拖动的侧边栏的悬浮窗,悬浮窗可以折叠或展开,悬浮窗中包含5大块功能分别对应一个TAB, 这5大块功能分别是应用控制、开发者选项、常用功能,常用系统设置和全局功能


请看视频预览:


floating-bar.gif


高清原图 introduction-floating-bar.gif


功能明细


1. 应用控制


应用控制能力将一些日常开发过程中对应用的一些繁琐的操作或者命令行指令转变为可视化的操作,而且还有自动收集和整理Crash, ANR日志,并且可以自动关联Logcat日志


文字太繁琐, 请直接看视频


application-control.gif


高清原图 introduction-application-controls.gif


2. 开发者选项


这里的开发者选项功能是将系统的开发者选项中一些最常用的开关放在悬浮窗中, 随时启用或关闭。
优势是不需要频繁去系统的开发者选项中去找对应开关,一键开闭。


我调研了其他有类似能力的工具App,都是引导用户去开发者选项中去开启或关闭功能。CodeCrafts一键开闭,无需跳转到系统开发者选项页面。


请看视频预览:


developer-options.gif


p2.gif


3. 最常用功能


没什么好介绍的,略。


4. 常用系统设置页面


这里承载了一些开发过程中经常需要打开的系统设置页面的快捷按钮,没什么好介绍的,略


5. 全局功能


这里的全局是相对于应用控制的,应用控制可以选择你正在开发的任意一款App, 然后应用控制中的所有能力都是对你的这个App的操作。 而全局控制中的功能不针对选中的App,所有App都适用


5.1 实时数据(Realtime data)


实时数据会随着当前页面变化或者系统事件实时变化



(以上图为例介绍, 实时数据的内容不仅仅只有这些)

内容含义用途
org.chromium.chrome.browser.firstrun.FirstRunActivity当前Activity的类名代码定位
launch time: 208ms当前Activity的冷启动耗时启动优化
com.android.chrome当前Activity所在应用的包名常用信息
Chrome(uid: 10163)当前Activity所在应用的名称和UID常用信息
pid: 23017当前Activity的进程ID常用信息
192.168.2.56,...当前系统的IP地址,可能有多个adb connect等
system当前应用是系统应用
allowBackUp当前应用有allowBackUp属性告警
实时数据未来还会有更多的扩展内容

5.2 不锁定屏幕


不会进入锁屏状态,也不会灭屏,避免开发过程中老是自动锁屏。


和系统开发者选项中的功能类似,区别是无论是否插入USB线都有效,开发者选项中的拔掉USB线后就无效了。
都可以用,具体选择看你的使用场景。


5.3 Latest Crashes


显示缓存中最近发生的Crash的调用堆栈,可能为空也可能不止一个Crash堆栈, 需要自行查看是否是你关注的Crash。


使用说明


CodeCrafts的很多功能依赖Shell权限, 如果发现存在功能不可用的情况,一般都是shell权限获取失败了, 只需要通过在电脑终端输入adb命令"adb tcpip 5555"指令, CodeCrafts就可以自动获取shell权限了。


image.png


adb tcpip 5555



  1. 第一次使用,连接电脑终端发送"adb tcpip 5555" 或

  2. 手机断电重启,连接电脑终端发送"adb tcpip 5555" 或

  3. 莫名其妙功能不能用了,连接电脑终端发送"adb tcpip 5555"


新增功能


有不少人反馈对CodeCrafts的实现原理感兴趣,后面新增的功能尽量配上实现原理



  1. CodeCrafts之断点调试 (1.0.15新增)


建设中



  1. 文件沙盒, 快速浏览App的文件目录

  2. 自动化,自动化点击,输入(比如自动跳广告,自动输入账号密码?)

  3. 组件检查, 快速查看View的类型, id, 颜色等

  4. ...


后期规划



  1. 悬浮窗的tab和内容可动态配置

  2. 应用控制增加应用性能数据

  3. 提供外部SDK接口,外部应用可接入CodeCrafts进行定制化改造


CodeCrafts持续更新中...


Github地址下载,
Gitee地址下载(需要登录gitee)


或者去Google Play安装


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

聊聊自己进入大厂前,创业公司的经历,我学到了什么?

前言 自从毕业开始工作之后,我坚持写了 2 年多的日记,基本节奏是每半年会写一篇,目的是为了记录自己成长的痕迹。 未来某个时间自己再回首的时候,能回味到此时此刻自己在做什么。当时的选择是否正确,是否在此时此刻,看到了未来发展的趋势,跟上了正确的车。 现在是 2...
继续阅读 »

前言


自从毕业开始工作之后,我坚持写了 2 年多的日记,基本节奏是每半年会写一篇,目的是为了记录自己成长的痕迹。


未来某个时间自己再回首的时候,能回味到此时此刻自己在做什么。当时的选择是否正确,是否在此时此刻,看到了未来发展的趋势,跟上了正确的车。


现在是 23 年 6 月,我第一次羊了,上一轮虽然逃过去了,但是这一次没能幸免,现在还在发着低烧。


距离上次写日记又过去了半年多,自己也步入 25 岁,这半年发生了很多事情,体会到时间变得更快了。


工作第一年:误打误撞加入创业公司



在来到字节之前,经历了校招被毁约,来到了创业公司,在创业公司工作了一年,作息是大小周,早 9 晚 9 ,对我来说这是一段比较记忆深刻的经历,教会了我很多互联网行业的东西。



校招被毁约


19 年的时候签约了一家金融公司,也是临近 20 年毕业前几十天,才被公司以疫情经济情况不好的理由,告知单方面毁约。


那个时候的我还是很害怕的,一点是临近毕业,大多校招已停止,第二点是自己有一段时间没有复习,很多知识已经遗忘。


一个朋友得知了我的情况,内推我到一家创业公司,也是内推第二天一大早就去公司面试,还好我还有一些基础在脑子里,现场面了两轮,当场就通过了,成为了前端的实习生一枚,公司也是答应毕业之后就转正。


面试通过之后,第二天就去公司上班了。第一天入职就拿到了 Macbook,也是第一次用 Mac 办公,想象那时的自己,就跟刚进城的人一样。


公司的结构


一个公司,总共就 50 来个人,做着 DAU 百万的产品。由于公司不大,所以做各种不同事情的人(销售、CEO、内容运营、新媒体运营、算法、产品),都坐在一起。敲代码的时候还能听到销售打电话的声音,因为是教育产品,所以销售的合作方大多是猿辅导、好未来、作业帮、学霸君等教育头部公司。


虽然公司小,但是产品各个环节非常全面:



  • ABTest

  • 算法

  • Hive


公司小的好处是,决策层级少,一个想法想要落地是很快的。在公司 1 年期间,快速上线了 3 款小程序,也快速从 0 到 1 上线了金融产品。PC、H5、小程序开发都有涉及。


整个团队的氛围是非常 Open 的,可以试验各种新技术,不像大厂内部会封装一些框架,有内部标准,不好去实践一些开源的框架。


与普通公司不同的地方


个人扮演的角色


我觉得最大不同的地方是角色,我是一名前端,但不仅扮演着前端的角色。要对产品体验负责,也要自己设计页面 UI。


公司没有测试,所以在日常开发中,每个研发和产品都是 QA,要对 C 端百万 DAU 产品对应的用户体验负责。


每个人都对需求负责,一个需求上线之后,可以自己写 SQL 去 Hive 里捞埋点数据,验证 AB 的可行性。


可以深入感受互联网不同角色发挥的作用


在创业公司,因为每周都有公司周会,大家会聚在一起聊每周各部门的进展,在会上,也可以全流程的了解到一个产品的完整生命周期。




  • 内容运营视角:



    • 做公众号,发文章,在存在粉丝基础的情况下,头版收入是很可观的,可以达到 6 位数+。




  • 产品运营视角:



    • 做竞品分析,看出对方哪方面做的好,我们要抄袭哪快的功能。

    • 做电话回访,了解用户的痛点,尤其针对停留时间较长的重度 C 端用户。




  • 销售视角:



    • 通过和大型教育机构合作,由于家长是很愿意为孩子进行教育付费的,所以通过弹窗收集信息配合电销,可以达到很可观的收入。




  • 算法视角:



    • 通过 AB 试验调整算法策略,可以优化广告收入,另外也可以提前计算预测插入广告可能带来的收益。

    • 调整算法策略,也可以优化用户停留时长,增强用户粘性。




  • 数据分析:



    • 通过 Hive 离线数据计算,可以生成一些报表数据,给提供用户信息聚合查看功能。




  • 产品视角:



    • 在产品基本功能打磨完毕后,要尽可能往付费方向引导。




  • 运维视角:



    • 将服务迁移到 K8S 集群,可以降低维护成本。




  • 后端视角:



    • 和算法、数分团队配合,另外还需要负责机器运维相关。




  • 老板视角:



    • 关注一些重要事项的进展,以及查看上线后的数据,是否符合产品预期。

    • 最终产品需要自负盈亏,功能不能一步设计到位,也需要把一些功能做成付费使用的。

    • 关注公司每个方向资金支出情况,控制公司收支,避免快速花光融资资金。保持自己的股份不被过度稀释。




我的直观感受是,自己虽然初入茅庐,但通过这一年的感知,深入理解了 C 端产品的全流程。这对我来说也是一笔很大的财富。


营收方面


App 内广告占大头,开屏广告>插屏广告>贴片广告,其次公众号文章等也是赚钱的利器,销售带来的收益远不如以上两个。总共这些一个月7位数还是有的。实际上最大的开销除了人力成本,还是服务器的成本,这个成本逼近7位数。


创业公司的生命周期



  1. 公司在快速发展期,有很多功能需要开发,这时是需要人的时候,会无限招人。

  2. 在产品 2-3 年之后,如没有新的大方向,进入一段停滞期,指的是 DAU 的不增长或下降。

  3. 产品稳定期,不再需要人,核心骨干退出团队,HC 缩减,产品转向以盈利为目标。

  4. 自负盈亏,break even。不再为公司资金发愁,不再需要融资。

  5. 保持公司运作,通过手段维持 DAU 和用户付费意愿,通过一些预消费手段留住用户,扩大收益。


快速验证


快速验证是 CEO 经常提的一个点,不过在王兴和张一鸣成功的经验来看,这也是正确的。


快速验证是说快速从 0 到 1 上线一个产品,冷启动或硬广,在短期查看一个产品的数据,如果产品数据不够理想,便放弃产品。试验下一个风口上的题材。


像美团,或者字节现在也在使用这种策略,快速上线 App 并试错,留下那些抓住用户的产品。


公司的瓦解


一个产品的瓶颈


当一个产品被打磨到 3 年之后,一般来说主流程就比较完善了,换句话说是用户需要的功能,产品都有了。这个时候也就过了 PM 发力 RD 开发的时期,在这之后即便这个公司只有运营,也可以保持产品正常运行。


创业公司的问题


CEO 的话语权会很大


一个人带来的决策不一定合理,当产品的发展不再合理时。大家会出现不满情绪,久而久之大家也不再团结协作,在快速上线几个小程序无果之后,3 个月内 50% 的研发团队成员纷纷离职了,不过大伙也很厉害,离职之后大多都去了大厂。


转变方向为营收优先


通过缩减一系列支出,想方设法让公司达到赚钱的状态。


手段有:砍 HC,团建,下线产品不需要的费钱的功能。


另外我也是一步步看着,公司从半层多楼的工区面积,变成 5分之二,4分之一的大小,最后工区被卖掉,撤离北京。


我的离开


我的离开也是必然,在后期被拉到老板的新产品线帮忙做产品从 0 到 1 建设。对当时工作还不到 1 年的我来说,还是很有压力,独自 own 一个私人银行项目。


在长时间宣传下,仍是没有用户使用,我能明显的感受到,新产品前景是渺茫的,只是老板的一厢情愿。另外新产品线的研发非常少,只是一番的催活,其实过程也决定了结果,产品是做不成的。在这种情况下,我提出了离职。


不过我也很感谢这段经历,能让我对从 0 到 1 创业有新的理解,另外也锻炼了我的抗压能力,增强了技术积累。


最近的工作


工作上


工作上在建设插件市场,提供了一种能快速开发页面组件的方式,能直接嵌入组件到前端中,类似动态执行模块联邦注入的组件。是一块很有意思的功能,类似于 Chrome 应用商城,其实开发工具建设一直是我比较喜欢也擅长的方向,未来也会继续在这方面努力,学习其它语言,做更快更高效的工具,为开发提效。


详细可以看这篇我今年写的文章 带你从 0 到 1 实现前端「插件市场架构」


能力提升方面


编程技能


学习并实战了以下技能:



  • VSCode 插件开发


  • Rush.js

    • 大型项目构建管理。



  • Golang (MySQL / Redis / Kafka)

    • 主要还是 API 层面的熟悉,目的还是为了能用非 NodeJS 语言写一写后端,以及了解更多的后端知识。



  • Rust
    稍微了解了一下语法,之前也写了一篇文章:以 JS 的视角带你入门 Rust 语言,快速上手


开源库


轻量的模块联邦


非编程相关


最近这 2 年,锻炼了画图、写 PRD、拉通对齐的能力,大厂更加专精一个方面,这让我能静下心来,不再像创业公司一样,受老板的影响,不再做快速迭代的事情,而是把产品打磨好,更加以用户角度出发思考用户需要什么,补齐什么功能。


愿望


毕业之后,由于疫情一直都是在国内旅游。还没出过国,希望疫情后每年自己都能出去走走,行万里路。把最好的景色都记录下来,拓宽眼界,放松心情。我很喜欢大海,尤其是四环环海的小岛,看着大海能让自己的心平静下来。接下来还有几个非常想去的地方、意大利、冰岛、新西兰、瑞士,夏威夷,希望能在 30 岁之前达成目标。


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

鹅厂七年半,写在晋升失败的不眠之夜

夜半惊醒 看了下时间,凌晨4:37,辗转反侧。3.7号大领导告诉我这次10升11没通过,这是我知道晋级失败的第4个夜晚,从震惊到否认,到愤怒,到麻木,再到冷静。我的技术能力、思考深度、攻坚能力明明是小组里突出的一个,但在此次技术职级的晋升上,却名落孙山,落在同...
继续阅读 »

夜半惊醒


看了下时间,凌晨4:37,辗转反侧。3.7号大领导告诉我这次10升11没通过,这是我知道晋级失败的第4个夜晚,从震惊到否认,到愤怒,到麻木,再到冷静。我的技术能力、思考深度、攻坚能力明明是小组里突出的一个,但在此次技术职级的晋升上,却名落孙山,落在同组小伙伴之后。技术人员,终究困于技术的窠臼。


工作经历


浑浑噩噩的四年


我毕业就进入鹅厂工作,15年,正是鹅厂大杀四方的一年,至今犹记得当时拿到鹅厂 offer 时的兴奋和同学眼中的艳羡。来了公司后,跟着项目组开始创新项目,一眼望到头的挣不来钱,浑浑噩噩的干了3年,和一起进公司的小伙伴们收入差距越来越大。


好在我的驱力不错,既然挣不到钱,那就好好积累技术能力,提升自己。那段时间,周围没有伙伴,没有对比,没有好的机会,只剩下我一个人慢慢悠悠的学习积累,我以为自己足够努力,没有荒废时间;后来才发现自己其实是井底之蛙,一起入职的小伙伴付出了百倍的努力和数不清的不眠之夜,1年多就已经能 cover 当时的业务了,没过2年就已经提干了。


2018年底,我满怀信心去答辩后台 3.1,没过。我当时还觉得评委装逼,挑刺,后来回头看写的都是s,一点含量都没有。


时来运转


2019年3月,答辩刚结束。部门业务调整,我们被整组划到了一个相对挣钱的业务上,leader 也调整成后台 leader。


新业务还是挣钱,凭着第一个项目积累的技术和项目经验,我得到了新领导的认可,第一年干的轻轻松松就挣到了远远超出之前的收入,和之前累死累活拿温饱线形成鲜明对比,可见大厂里跟对项目是多么的重要。


只是没想到第一年就是巅峰,后面随着贸易战、反垄断,鹅厂形势越来越差,我们业务收入也受到极大影响,人员也越来越冗余。挣的钱少了,分蛋糕的人还多了,可想而知,收入越来越差。


在这里,我成功从8级升到了10级工程师。说起来我们这一届实名惨,第一批毕业要等1年才能晋级,最后一批8升9需要公司范围内通道评审,第一批9升10走BG内评审,第一批10升11走部门内评审,全程试验品,一样没落下。


10升11


去年年底公司晋升改革,10升11评审下放部门,职级和待遇不再挂钩,不仅看重”武功“,还看中”战功“,同时传闻这次晋升名额极少。大领导找我谈话,要不就别去了?我想了想这两年的技术和项目积累,不甘心。


整个中心,就我和同组的小伙伴两个人一起去,我挑的是个一直在做的有技术挑战、持续优化的项目,小伙伴挑的是挣钱的项目。我准备了几个周末,小伙伴临时抱佛脚,结果小伙伴过了,我没过。评委说,我过度设计(优化过头)、没有实战演练容灾。


我和大领导说这个和我预期差太多了,我和小伙伴一起预答辩过,都知道讲的啥,咱是技术评审,我的项目技术含量、架构和挑战明明更好,为啥是我没过?大领导说你是认知偏差,人家讲的确实好。不忿,遂找评委 battle,咱要真按这个说法,能不能对参加评审的项目一致对待,不要双标?且不说我是不是过度设计,凭啥对我的项目就要求容灾演练,对别人的项目就视而不见?评委不语,你的项目对部门价值没产生什么收益和价值。


部门400+人,4个晋升11级名额,大概率是一个中心一个。一个技术评审,糅合了许多超过技术方面的考量,业务挣不挣钱和技术有啥关系?技术好的业务就能挣钱?业务挣钱多的是技术好的原因?


我以前也晋级失败过,但是我认输,因为相对而言,整个BG一起评审,你没过就是你技术差,其他因素影响没有这么大。这次明明是我更有技术深度、更投心思在优化上,却事与愿违。


反思与感悟


反思一下。千头万绪,毛主席说,那是没有抓住主要矛盾。


大的方面上讲:这两年大环境差,公司收入减少,需要降本增效。我要是管理层,也会对内部薪资成本、晋升等要求控制增速;政策制定上,也会将资源更倾斜于现金流业务,控制亏损业务的支出。在这个大的背景下,公司不愿意养“能力强,战功少”的技术骨干了;更愿意养“能力强,战功多”的人员。希望员工把精力都聚焦在业务挣钱上。


部门层面上讲:和公司政策一脉相承。晋升名额少,那紧着挣钱的中心、挣钱的小组、挣钱的个人给。这个意义上讲,职级体系已经不是衡量技术能力的标尺了。你技术能力强,没用。你得是核心,得是领导认可的人。


中心层面上讲:要争取名额,否则怎么团结手底下人干活。而且名额要给业务干活干的最出色的那个人,其他人就往后稍稍。我要是我领导,年前也会劝退我的。毕竟我技术能力虽然强,更多的是问题专家的角色,而不是业务核心。


且预测一下,中心之间肯定进行了各种资源置换。评审估计流于形式。慢说我被挑出来了问题,就是挑不出来问题也得给你薅下来。我就是和小伙伴互换项目去讲,估计还是小伙伴过,到时候评委就该说我“你做这东西有啥难度,我叫个毕业生都能来做”了。此处我想起了一副对联,不太合适,就是很搞笑。“说你行你就行不行也行,说你不行你就不行行也不行”,横批是“不服不行”。



从个人角度讲,付出和收获不成正比,难受吗?那肯定的。但谁让方向一开始就错了呢?这世界不公平的事情多了,不差这一个。



重新出发


综上来说,短期内,不改变发力方向到业务上,后面可以说晋升无望。以前老是觉得自己技术能力强,心高气傲,心思没在业务上,勤于术而懒于道,实在是太过幼稚。做了几年都对很多东西不甚了了。


今后,需要趁着还能拼的时候拼一下,潜心业务。编码和开发只是很小一部分,更要从:



  1. 业务大局出发,实时数据驱动,监控、统计一定要完善,要有看板,否则无法衡量。

  2. 主动探索业务,给产品经理出主意,一起合力把产品做上去做大,提升对业务的贡献,探索推荐、探索推广渠道等等。产品做不上去,个人也没机会发展。

  3. 多做向上管理,和领导、大领导多沟通。做好安排的每一件小事,同时主动汇报,争取重获信任。

  4. 主动承担,做一个领导眼里靠谱放心的人。

  5. 多思考总结,多拔高提升,不要做表面的勤奋工作,看似没有浪费时间,实则每分每秒都在浪费时间。

  6. 多社交,多沟通,多交流,打破技术人员的牢笼。


凡事都有两面性,福兮祸所依,祸兮福所倚。多受挫折、早受挫折不是坏事。


2023,就立个 flag 在这里吧。


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

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

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

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


离职前的纠结


意外通过的面试


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


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


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


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


坚持还是放弃


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


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


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


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


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


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


国企的压力


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


学习不止工作


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


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


人际关系


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


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


所见所想


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


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


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


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


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

人需借以虚名而成事

前言 自从我看冯唐老师推荐的《资治通鉴》之后,对历史有那么一丢丢的兴趣,直到在短视频里面刷到《百家讲坛》里面几位老师的讲解片段,以及跟自己经历结合,瞬间满满的共鸣,是啊时代一直在变化,而里面的为人处事、社会规则凡此种种却从来没有太大变化,这就是我们需要去学习...
继续阅读 »

前言




自从我看冯唐老师推荐的《资治通鉴》之后,对历史有那么一丢丢的兴趣,直到在短视频里面刷到《百家讲坛》里面几位老师的讲解片段,以及跟自己经历结合,瞬间满满的共鸣,是啊时代一直在变化,而里面的为人处事、社会规则凡此种种却从来没有太大变化,这就是我们需要去学习历史的原因。


所以有了以下感慨。



<<百家讲坛>> 不愧是经典,整本历史歪歪斜斜写着两字:权谋,谋事、谋人,加上不确定因素,天时地利人和,最终变成命运。
人的一生就像书中一行字轻描淡写,绘成一笔~



人需借以虚名而成事




这里解释一下,人需要倚靠外部的这些名号、头衔从而能有比较大的作为。为什么这么讲呢,听我细细道来~


人生需要四行


这是王立群老师讲的,人生要想有点成就需要四行,首先你自己行,你自身本身够硬,能够做好事,其次是有人说你行,从古代的推荐机制,比如说优秀的人推荐其他优秀的人才对不对,李白的大名远扬,唐玄宗在公主引荐之下见了皇帝,第三个说你行的人要行,推荐你的那个人本身能力要强,不然没有说服力,第四是身体行,很多机会是熬出来的,比如司马懿伺候了3位君主,兢兢业业,最终才能完成自己的大业。


何为借以虚名?


1、刘备的例子,他在比较年长的时候还无法实现自己的基业,寄人篱下,他登场的时候是怎样的呢,名号叫刘皇叔,翻开长长的祖谱,八辈子打不到杆哈哈哈,不过因为当时被曹操挟持,所以需要有汉族帮忙自己,所以顺理成章的提拔上来。


2、项羽出生在一个贵族家庭。他的父亲项梁是楚国将领,曾经在楚汉战争中和刘邦一起作战。而他的母亲也是楚国的贵族,是楚王的女儿。


相比之下,刘邦的出身则比较低微。他的祖父是一个农民,父亲是一个小县官。他自己也曾经当过一个普通的县尉,但因为功绩不够而被罢免。


尽管项羽出身高贵,但他的性格比较豁达,喜欢豪情壮志和享乐人生,行事大胆不拘小节。刘邦则更加谨慎稳重,注重政治规划和长远发展。


3、王立群老师讲述过他自身的故事,小学的时候成绩优秀得到保送,后面名额被学校给了别人,自己去了私立的学校,半工半读,到了大学还是遇到了同样的遭遇,后面就到比较好的大学教书,但是有一天校长跟他讲他的成绩还是出身问题需要去另一所低一点的学校教书,他当时就哭了,命运比较坎坷的。


4、工作中,我们常常会听到,某位领导自我介绍某某大厂经历,还有某某大学毕业,还有当你投一些大厂的核心部门的时候,都会对你学历进行考核。


所以我们从上面3个例子来看,人生确实正如王立群老师讲的,需要四行,当你自己不行的时候,即使有机会也会最终流失掉;当没有推荐你的时候,也会埋没在茫茫人海中,并不是说是金子总会发光,现实是伯乐更加重要;当说你行的人没有能力的时候也不行,名牌大学、大厂经历也是这样,通过一个有名声的物体从而表示你也可以,也很厉害哈哈;最后身体是本钱,这个就不用过多的讲解了。


读历史的目的,从前人的故事、经历中,学习到点什么,化为自身的经验、阅历。


我发现现在很多人都喜欢出去玩,说的开阔视野,扩展见识,在我看来读历史才是真正的拓宽自己的眼界,原来几百年前也有人跟我一样遇到同样的问题,也有那些胸怀大志的三国名将郁郁不得志。


历史是一本书籍,它可以很薄,几句话、几行字就概括了一个朝代,它也可以很厚,厚到整个朝代的背景,比如气候情况、经济情况、外交情况、内政情况,各个出名的人物事迹,那岂是几页纸能讲得完的,是多么的厚重。


如果人不能借以虚名怎么做事?


修炼两件事情




同样百家讲坛也给出了一个比较好的见解,一个人想要成事,需要两个明,一个是高明、一是精明。高明是你有足够的眼光去发现,发现机会也好,发现危机也好,往往人生的成就需要这种高瞻远瞩的;另一个精明,是指在做事上打磨,细心主动,王阳明讲的知行合一,里面很关键的一点就是事上练,方法论在实战中去验证总结。


心态上练




李连杰在采访中说了这么一个耐人寻味的故事,他发现不管是有钱的人还是没有钱的人都会生气,比如一些老板在为几百万损失破口大骂,也有人因为几万的丢失是否生气,也有人丢了几块钱很愤怒。他觉得人都会愤怒,只是那个导致他愤怒的级别不一样而已。


他早年给自己定目标,发现当达到目标之后,发现还是有人比自己厉害,所以即使达到目标也没有想象快乐;即使有钱人无法解决痛苦的问题,比如生老病死。


所以人生意义在哪里?


我认为如果你奔着成事的想法,多修炼上面两件事,如果没有发展机会,不如躺平,积累自己的能力,就像道德经里面的无为而为,而心态的修炼是人生终极修炼,因为生死才是人生最大的难关,人应该怎么脱离时间束缚,不受衰老影响。


恰恰历史给了我们启示,要有所作为,能够为人类历史上留下精神粮食,做出浓厚贡献,就像我们都没有见过孔子、老子吧,但是他们的精神至今都在影响着我们。


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

央国企也开始欠薪停薪了

上海某院自6月29日起,全员停工三个月,理由是现阶段已经没有新的设计项目需要完成。去年该公司还对疫情后行业复苏充满期待,连续在两地新开了分公司。今年的市场让人猝不及防。近日,网上曝出“某建筑企业工资停了”,引发众人热议。有网友表示“半年没发工资了”“已经好几年...
继续阅读 »

上海某院自6月29日起,全员停工三个月,理由是现阶段已经没有新的设计项目需要完成。去年该公司还对疫情后行业复苏充满期待,连续在两地新开了分公司。今年的市场让人猝不及防。



近日,网上曝出“某建筑企业工资停了”,引发众人热议。有网友表示“半年没发工资了”“已经好几年只交保险不发工资了。”......



有人说某铁和某建都发不出工资了,震惊。


有人说上班以来就没有发过100%的工资,以后也不会补。


有的小朋友被吓到了,说考虑安稳才选的国有企业,已经开始担心这种情况了。

评论区的前辈也是毫不留情:“我本来也是求稳去了国企,现在也发不出来了”、“市属国企、半年没发工资了。”、“能拿一天是一天”



还有人预测国企也顶不住了,开始要裁员了。


由于行情不好,许多设计师都倾向于跳槽去国企,大家的心态都是:“虽然给的薪水不如民企,但是至少能旱涝保收。“然而,现在建筑行业的国企也被曝出停发工资,实在令人惊愕。

房企、建企欠薪被处罚

水泥内参了解到,近期多地公布建筑工程领域企业欠薪行为,至少有164家建企、房企被通报或处罚。

江苏省住建厅公布拖欠工资引发群体性事件被限制市场准入及通报批评企业和人员名单。对126家企业予以全省范围内限制建筑市场准入;对44家企业予以全省通报批评;对42名从业人员予以全省限制建筑市场准入;对86名从业人员予以全省通报批评。


上海市对2023年建筑工程领域发生的拖欠农民工工资事件中负有相应责任的企业和项目负责人进行通报前公示,17家建筑工程企业被通报

济南市住房和城乡建设局发布关于公布涉访拖欠农民工工资项目专项检查情况的通知,9家房地产企业未按月足额拨付人工费,移送市城管局进行处罚


烟台市住建局对存在拖欠农民工工资、消极对待清欠工作行为,并造成不良社会影响的32家开发施工企业,以及私自截留、挪用农民工工资的55名内部承包人,实施暂停网签备案、列入信用管理黑名单、农民工工资支付监管平台公示曝光等多重处罚。

义乌市通报近期因欠薪违法行为被市人社部门行政处罚的有关企业,11家建筑领域企业被列入信用惩戒名单。


云南省人力资源和社会保障厅向社会公布了《2023年第二批重大劳动保障违法案件和2023年第一批拖欠农民工工资失信联合惩戒对象名单》。云南宝栋建设工程有限公司拖欠192名劳动者劳动报酬8743759元;云南齐优建筑劳务有限公司拖欠88名劳动者劳动报酬3417179.00元;陕西凯得瑞建设工程有限公司拖欠9名劳动者劳动报酬135050元;山东美达建工集团股份有限公司因拖欠83名劳动者工资1499378.30元等4家企业被列入拖欠农民工工资失信联合惩戒对象名单

台州市通报建筑工程领域农民工工资支付责任履行不到位的10家典型建筑施工企业:广东禾润建设工程有限公司、浙江普川装饰工程有限公司、江苏中南建筑产业集团有限责任公司、浙江广优建设有限公司、中铁北京工程局集团第五工程有限公司、中国二十二冶集团有限公司、浙江祥生建设工程有限公司、杭州庆强建设有限公司、浙江海大建设有限公司、九华恒业建设有限公司。

马鞍山市通报全市工程建设领域工资支付诚信等级评定情况,对存在拖欠工资、违法发包(转包)等行为的28家企业评为B级,给予通报批评


合肥市通报拖欠农民工工资失信联合惩戒对象名单,7家建筑公司及负责人被处罚

湖北省人力资源和社会保障厅公布2023年第二批拖欠劳动报酬典型案件,2家建筑相关企业被通报... ...


凉山州会理船城牌水泥厂欠薪1739.62万元

2月13日,凉山州会理船城牌水泥有限责任公司管理人发布职工债权公示称,截至2023年2月10日,船城水泥公司职工债权总额为人民币17,396,229.04元(包括欠付职工工资、补偿金、职工垫缴的基本养老保险和基本医疗保险费用、伤残等。会理市人民法院于2022年4月21日作出裁定受理船城水泥公司的破产清算。

拉萨某水泥厂欠薪536万元

西藏自治区人民检察院网站2023年1月18日消息,拉某某等人与拉萨市某水泥有限公司签订运输合同,并按合同约定拉某某等人承运该公司的运输项目。2021年6月起,该公司因资金周转问题,导致拉某某等225户878人运输款未得到支付,未支付运输款金额为536万元。达孜区人民检察院认为,水泥公司与878人存在合同关系,水泥公司应按照合同约定支付相关的款项。

此外,烟台磐龙水泥有限公司被曝光拖欠员工工资3年,约70-80人为此上访。目前,厂里的设备已经被老板卖掉。

武宣县桐岭镇人民政府消息,2022年7月至2023年4月期间,叶某等12人在桐岭镇华润水泥厂从事工程建设相关项目,完工后何老板仍拖欠其5万元多工资未支付,多次追讨无果。

崇左市某建材水泥厂被责令停产后,造成全体职工失业。水泥厂长期拖欠工资,让部分职工生活陷入困境。河市白塔镇显德汪创业水泥拖欠工资据悉,员工不干了,最后一个月工资不给,而且还是压一个工资的。

据悉,除了拖欠工资外,近几年由于企业效益不好,水泥厂裁员降薪的事情也时有发生,有业内人员表示,别说降薪了,现在能按时发工资就已经超越很多人了......

来源:http://www.163.com/dy/article/I8VT204K0552CAGB.html

收起阅读 »

vue3 表单封装遇到的一个有意思的问题

web
前言 最近在用 vue3 封装 element 的表单时遇到的一个小问题,这里就简单记录一下过程。话不多说直接上代码!!! 正文 部分核心代码 import { ref, defineComponent, renderSlot, type PropType, ...
继续阅读 »

前言


最近在用 vue3 封装 element 的表单时遇到的一个小问题,这里就简单记录一下过程。话不多说直接上代码!!!


正文


部分核心代码


import { ref, defineComponent, renderSlot, type PropType, type SetupContext } from 'vue';
import { ElForm, ElFormItem, ElRow, ElCol } from 'element-plus';
import type { RowProps, FormItemProps, LabelPosition } from './types';
import formItemRender from './CusomFormItem';
import { pick } from 'lodash-es';

const props = {
formRef: {
type: String,
default: 'customFormRef',
},
modelValue: {
type: Object as PropType<Record<string, unknown>>,
default: () => ({}),
},
rowProps: {
type: Object as PropType<RowProps>,
default: () => ({
gutter: 24,
}),
},
formData: {
type: Array as PropType<FormItemProps[]>,
default: () => [],
},
labelPosition: {
type: String as PropType<LabelPosition>,
default: 'right',
},
labelWidth: {
type: String,
default: '150px',
},
};

const elFormItemPropsKeys = [
'prop',
'label',
'labelWidth',
'required',
'rules',
// 'error',
// 'showMessage',
// 'inlineMessage',
// 'size',
// 'for',
// 'validateStatus',
];

export default defineComponent({
name: 'CustomForm',
props,
emits: ['update:modelValue'],
setup(props, { slots, emit, expose }: SetupContext) {
const customFormRef = ref();

const mValue = ref({ ...props.modelValue });

watch(
mValue,
(newVal) => {
emit('update:modelValue', newVal);
},
{
immediate: true,
deep: true,
},
);

// 表单校验
const validate = async () => {
if (!customFormRef.value) return;
return await customFormRef.value.validate();
};

// 表单重置
const resetFields = () => {
if (!customFormRef.value) return;
customFormRef.value.resetFields();
};

// 暴漏方法
expose({ validate, resetFields });

// col 渲染
const colRender = () => {
return props.formData.map((i: FormItemProps) => {
const formItemProps = { labelWidth: props.labelWidth, ...pick(i, elFormItemPropsKeys) };
return (
<ElCol {...i.colProps}>
<ElFormItem {...formItemProps}>
{i.formItemType === 'slot'
? renderSlot(slots, i.prop, { text: mValue.value[i.prop], props: { ...i } })
: formItemRender(i, mValue.value)}
</ElFormItem>
</ElCol>

);
});
};

return () => (
<ElForm ref={customFormRef} model={mValue} labelPosition={props.labelPosition}>
<ElRow {...props.rowProps}>
{colRender()}
<ElCol>
<ElFormItem labelWidth={props.labelWidth}>{renderSlot(slots, 'action')}</ElFormItem>
</ElCol>
</ElRow>
</ElForm>

);
},
});

<script setup lang="ts">
import CustomerForm from '/@/components/CustomForm';
const data = ref([
{
formItemType: 'input',
prop: 'name',
label: 'Activity name',
placeholder: 'Activity name',
rules: [
{
required: true,
message: 'Please input Activity name',
trigger: 'blur',
},
{ min: 3, max: 5, message: 'Length should be 3 to 5', trigger: 'blur' },
],
},
{
formItemType: 'select',
prop: 'region',
label: 'Activity zone',
placeholder: 'Activity zone',
options: [
{
label: 'Zone one',
value: 'shanghai',
},
{
label: 'Zone two',
value: 'beijing',
},
],
},
{
formItemType: 'inputNumber',
prop: 'count',
label: 'Activity count',
placeholder: 'Activity count',
},
{
formItemType: 'date',
prop: 'date',
label: 'Activity date',
type: 'datetime',
placeholder: 'Activity date',
},
{
formItemType: 'radio',
prop: 'resource',
label: 'Resources',
options: [
{ label: 'Sponsorship', value: '1' },
{ label: 'Venue', value: '2' },
],
},
{
formItemType: 'checkbox',
prop: 'type',
label: 'Activity type',
options: [
{ label: 'Online activities', value: '1', disabled: true },
{ label: 'Promotion activities', value: '2' },
{ label: 'Offline activities', value: '3' },
{ label: 'Promotion activities', value: '4' },
{ label: 'Simple brand exposure', value: '5' },
],
},
{
formItemType: 'input',
prop: 'desc',
type: 'textarea',
label: 'Activity form',
placeholder: 'Activity form',
},
{
formItemType: 'slot',
prop: 'test',
label: 'slot',
},
]);
const model = reactive({
name: '',
region: '',
count: 0,
date: '',
resource: '',
type: [],
desc: '',
test: '1111',
});
const formRef = ref();
const submitForm = () => {
const valid = formRef.value.validate();
if (valid) {
console.log(model);
} else {
return false;
}
};

const resetForm = () => {
formRef.value.resetFields();
};
</script>

<template>
<div class="wrap">
<CustomerForm
ref="formRef"
:v-model="model"
:formData="data"
>

<template #test="scope">
{{ scope.text }}
</template>
<template #action>
<el-button type="primary" @click="submitForm()">Create</el-button>
<el-button @click="resetForm()">Reset</el-button>
</template>
</CustomerForm>
</div>
</template>


<style scoped>
.wrap {
margin: 30px auto;
width: 600px;
height: auto;
}
</style>



问题现象


代码其实非常简单,运行起来也很正常很流畅😀😀😀,但是当我填写完表单后点击提交按钮,打印model的值时,发现值全没给上。


微信截图_20230709120015.png


原因分析


这里经过两年半的尝试,终于发现在定义model时,将const model = reactive({xxx}) 改为 const model = ref({xxx}) 后就正常了。思考了一下 ref 定义的对象,源码上最后通过 toReactive 还是被转化为 reactive,ref 用法上需要 .value, 数据上这两者应该没有什么不同。然后我就去把 reactive、ref 又看了看也没发现问题。在emit('update:modelValue', newVal) 处打印也是正常的。


watch( mValue,
(newVal) => {
console.log('newVal>>>', newVal)
emit('update:modelValue', newVal);
},
{ immediate: true, deep: true, }
);

最后有意思的是,我把 const model 改成 let model tmd居然也正常了,这就让我百思不得其解了😕😕😕


解决


其实上面 debugger 后,就确定了方向 肯定是emit('update:modelValue', newVal)这里出问题了,回到使用组件,把v-model 拆解一下,此时还看不出来问题。


1688879457596.jpg


换成:modelValue="model" @update:model-value="update(e)"问题立马出现了,ts已经提示了 model是常量!


微信截图_20230709131250.png


这样问题就非常明了了,这就解释了 let 可以 const 不行,但你好歹报个错啊 😤😤😤 坑死人不偿命,可见即使在 template 里面这样写@update:model-value="model = $event" ts 也无能为力!
回过头再来看看 ref 为啥可行呢?当改成ref时,


  const update = (e) => {
model.value = e;
};

update是要.value 的,修改常量对象里面属性是正常的。再想想 ref 的变量在 template 中 vue 已经帮我们解过包了,v-model 语法糖拿着属性直接赋值并不会产生问题。而常量 reactive 则不能修改,也可以在在里面再包裹一层对象,但这样就有点冗余了。


总结


总结起来就是,const 定义的 reactive 对象,v-model 去更新整个对象的时候失败,常量不能更改,也没有给出任何报错或提示!


唉!今年太难了。前端路漫漫其修远兮,还需

作者:Pluto5280
来源:juejin.cn/post/7253453908039123005
更加卷地而行!😵😵😵

收起阅读 »

极致舒适的Vue弹窗使用方案

web
一个Hook让你体验极致舒适的Dialog使用方式! Dialog地狱 为啥是地狱? 因为凡是有Dialog出现的页面,其代码绝对优雅不起来!因为一旦你在也个组件中引入Dialog,就最少需要额外维护一个visible变量。如果只是额外维护一个变量这也不是不...
继续阅读 »

一个Hook让你体验极致舒适的Dialog使用方式!


image.png


Dialog地狱


为啥是地狱?


因为凡是有Dialog出现的页面,其代码绝对优雅不起来!因为一旦你在也个组件中引入Dialog,就最少需要额外维护一个visible变量。如果只是额外维护一个变量这也不是不能接受,可是当同样的Dialog组件,即需要在父组件控制它的展示与隐藏,又需要在子组件中控制。


为了演示我们先实现一个MyDialog组件,代码来自ElementPlus的Dialog示例


<script setup lang="ts">
import { computed } from 'vue';
import { ElDialog } from 'element-plus';

const props = defineProps<{
visible: boolean;
title?: string;
}>();

const emits = defineEmits<{
(event: 'update:visible', visible: boolean): void;
(event: 'close'): void;
}>();

const dialogVisible = computed<boolean>({
get() {
return props.visible;
},
set(visible) {
emits('update:visible', visible);
if (!visible) {
emits('close');
}
},
});
</script>

<template>
<ElDialog v-model="dialogVisible" :title="title" width="30%">
<span>This is a message</span>
<template #footer>
<span>
<el-button @click="dialogVisible = false">Cancel</el-button>
<el-button type="primary" @click="dialogVisible = false"> Confirm </el-button>
</span>
</template>
</ElDialog>
</template>

演示场景


就像下面这样:


Kapture 2023-07-07 at 22.44.55.gif


示例代码如下:


<script setup lang="ts">
import { ref } from 'vue';
import { ElButton } from 'element-plus';

import Comp from './components/Comp.vue';
import MyDialog from './components/MyDialog.vue';

const dialogVisible = ref<boolean>(false);
const dialogTitle = ref<string>('');

const handleOpenDialog = () => {
dialogVisible.value = true;
dialogTitle.value = '父组件弹窗';
};

const handleComp1Dialog = () => {
dialogVisible.value = true;
dialogTitle.value = '子组件1弹窗';
};

const handleComp2Dialog = () => {
dialogVisible.value = true;
dialogTitle.value = '子组件2弹窗';
};
</script>

<template>
<div>
<ElButton @click="handleOpenDialog"> 打开弹窗 </ElButton>
<Comp text="子组件1" @submit="handleComp1Dialog"></Comp>
<Comp text="子组件2" @submit="handleComp2Dialog"></Comp>
<MyDialog v-model:visible="dialogVisible" :title="dialogTitle"></MyDialog>
</div>
</template>

这里的MyDialog会被父组件和两个Comp组件都会触发,如果父组件并不关心子组件的onSubmit事件,那么这里的submit在父组件里唯一的作用就是处理Dialog的展示!!!🧐这样真的好吗?不好!


来分析一下,到底哪里不好!


MyDialog本来是submit动作的后续动作,所以理论上应该将MyDialog写在Comp组件中。但是这里为了管理方便,将MyDialog挂在父组件上,子组件通过事件来控制MyDialog


再者,这里的handleComp1DialoghandleComp2Dialog函数除了处理MyDialog外,对于父组件完全没有意义却写在父组件里。


如果这里的Dialog多的情况下,简直就是Dialog地狱啊!🤯


理想的父组件代码应该是这样:


<script setup lang="ts">
import { ElButton } from 'element-plus';

import Comp from './components/Comp.vue';
import MyDialog from './components/MyDialog.vue';

const handleOpenDialog = () => {
// 处理 MyDialog
};
</script>

<template>
<div>
<ElButton @click="handleOpenDialog"> 打开弹窗 </ElButton>
<Comp text="子组件1"></Comp>
<Comp text="子组件2"></Comp>
</div>
</template>

在函数中处理弹窗的相关逻辑才更合理。


解决之道


🤔朕观之,是书之文或不雅,致使人之心有所厌,何得无妙方可解决?


依史记之辞曰:“天下苦Dialog久矣,苦楚深深,望有解脱之道。”于是,诸位贤哲纷纷举起讨伐Dialog之旌旗,终“命令式Dialog”逐渐突破困境之境地。


image.png


没错现在网上对于Dialog的困境,给出的解决方案基本上就“命令式Dialog”看起来比较优雅!这里给出几个网上现有的命令式Dialog实现。


命令式一


codeimg-facebook-shared-image (5).png


吐槽一下~,这种是能在函数中处理弹窗逻辑,但是缺点是MyDialog组件与showMyDialog是两个文件,增加了维护的成本。


命令式二


基于第一种实现的问题,不就是想让MyDialog.vue.js文件合体吗?于是诸位贤者想到了JSX。于是进一步的实现是这样:


codeimg-facebook-shared-image (7).png


嗯,这下完美了!🌝


doutub_img.png


完美?还是要吐槽一下~



  • 如果我的系统中有很多弹窗,难道要给每个弹窗都写成这样吗?

  • 这种兼容JSX的方式,需要引入支持JSX的依赖!

  • 如果工程中不想即用template又用JSX呢?

  • 如果已经存在使用template的弹窗了,难道推翻重写吗?

  • ...


思考


首先承认一点命令式的封装的确可以解决问题,但是现在的封装都存一定的槽点。


如果有一种方式,即保持原来对话框的编写方式不变,又不需要关心JSXtemplate的问题,还保存了命令式封装的特点。这样是不是就完美了?


那真的可以同时做到这些吗?


doutub_img (2).png


如果存在一个这样的Hook可以将状态驱动的Dialog,转换为命令式的Dialog吗,那不就行了?


它来了:useCommandComponent


image.png


父组件这样写:


<script setup lang="ts">
import { ElButton } from 'element-plus';

import { useCommandComponent } from '../../hooks/useCommandComponent';

import Comp from './components/Comp.vue';
import MyDialog from './components/MyDialog.vue';

const myDialog = useCommandComponent(MyDialog);
</script>

<template>
<div>
<ElButton @click="myDialog({ title: '父组件弹窗' })"> 打开弹窗 </ElButton>
<Comp text="子组件1"></Comp>
<Comp text="子组件2"></Comp>
</div>
</template>

Comp组件这样写:


<script setup lang="ts">
import { ElButton } from 'element-plus';

import { useCommandComponent } from '../../../hooks/useCommandComponent';

import MyDialog from './MyDialog.vue';

const myDialog = useCommandComponent(MyDialog);

const props = defineProps<{
text: string;
}>();
</script>

<template>
<div>
<span>{{ props.text }}</span>
<ElButton @click="myDialog({ title: props.text })">提交(需确认)</ElButton>
</div>
</template>

对于MyDialog无需任何改变,保持原来的样子就可以了!


useCommandComponent真的做到了,即保持原来组件的编写方式,又可以实现命令式调用


使用效果:


Kapture 2023-07-07 at 23.44.25.gif


是不是感受到了莫名的舒适?🤨


不过别急😊,要想体验这种极致的舒适,你的Dialog还需要遵循两个约定!


两个约定


如果想要极致舒适的使用useCommandComponent,那么弹窗组件的编写就需要遵循一些约定(其实这些约定应该是弹窗组件的最佳实践)。


约定如下:



  • 弹窗组件的props需要有一个名为visible的属性,用于驱动弹窗的打开和关闭。

  • 弹窗组件需要emit一个close事件,用于弹窗关闭时处理命令式弹窗。


如果你的弹窗组件满足上面两个约定,那么就可以通过useCommandComponent极致舒适的使用了!!



这两项约定虽然不是强制的,但是这确实是最佳实践!不信你去翻所有的UI框看看他们的实现。我一直认为学习和生产中多学习优秀框架的实现思路很重要!



如果不遵循约定


这时候有的同学可能会说:哎嘿,我就不遵循这两项约定呢?我的弹窗就是要标新立异的不用visible属性来控制打开和关闭,我起名为dialogVisible呢?我的弹窗就是没有close事件呢?我的事件是具有业务意义的submitcancel呢?...


doutub_img.png


得得得,如果真的没有遵循上面的两个约定,依然可以舒适的使用useCommandComponent,只不过在我看来没那么极致舒适!虽然不是极致舒适,但也要比其他方案舒适的多!


如果你的弹窗真的没有遵循“两个约定”,那么你可以试试这样做:


<script setup lang="ts">
// ...
const myDialog = useCommandComponent(MyDialog);

const handleDialog = () => {
myDialog({
title: '父组件弹窗',
dialogVisible: true,
onSubmit: () => myDialog.close(),
onCancel: () => myDialog.close(),
});
};
</script>

<template>
<div>
<ElButton @click="handleDialog"> 打开弹窗 </ElButton>
<!--...-->
</div>
</template>

如上,只需要在调用myDialog函数时在props中将驱动弹窗的状态设置为true,在需要关闭弹窗的事件中调用myDialog.close()即可!


这样是不是看着虽然没有上面的极致舒适,但是也还是挺舒适的?


源码与实现


实现思路


对于useCommandComponent的实现思路,依然是命令式封装。相比于上面的那两个实现方式,useCommandComponent是将组件作为参数传入,这样保持组件的编写习惯不变。并且useCommandComponent遵循单一职责原则,只做好组件的挂载和卸载工作,提供足够的兼容性



其实useCommandComponent有点像React中的高阶组件的概念



源码


源码不长,也很好理解!在实现useCommandComponent的时候参考了ElementPlus的MessageBox


源码如下:


import { AppContext, Component, ComponentPublicInstance, createVNode, getCurrentInstance, render, VNode } from 'vue';

export interface Options {
visible?: boolean;
onClose?: () => void;
appendTo?: HTMLElement | string;
[key: string]: unknown;
}

export interface CommandComponent {
(options: Options): VNode;
close: () => void;
}

const getAppendToElement = (props: Options): HTMLElement => {
let appendTo: HTMLElement | null = document.body;
if (props.appendTo) {
if (typeof props.appendTo === 'string') {
appendTo = document.querySelector<HTMLElement>(props.appendTo);
}
if (props.appendTo instanceof HTMLElement) {
appendTo = props.appendTo;
}
if (!(appendTo instanceof HTMLElement)) {
appendTo = document.body;
}
}
return appendTo;
};

const initInstance = <T extends Component>(
Component: T,
props: Options,
container: HTMLElement,
appContext: AppContext | null = null
) =>
{
const vNode = createVNode(Component, props);
vNode.appContext = appContext;
render(vNode, container);

getAppendToElement(props).appendChild(container);
return vNode;
};

export const useCommandComponent = <T extends Component>(Component: T): CommandComponent => {
const appContext = getCurrentInstance()?.appContext;

const container = document.createElement('div');

const close = () => {
render(null, container);
container.parentNode?.removeChild(container);
};

const CommandComponent = (options: Options): VNode => {
if (!Reflect.has(options, 'visible')) {
options.visible = true;
}
if (typeof options.onClose !== 'function') {
options.onClose = close;
} else {
const originOnClose = options.onClose;
options.onClose = () => {
originOnClose();
close();
};
}
const vNode = initInstance<T>(Component, options, container, appContext);
const vm = vNode.component?.proxy as ComponentPublicInstance<Options>;
for (const prop in options) {
if (Reflect.has(options, prop) && !Reflect.has(vm.$props, prop)) {
vm[prop as keyof ComponentPublicInstance] = options[prop];
}
}
return vNode;
};

CommandComponent.close = close;

return CommandComponent;
};

export default useCommandComponent;

除了命令式的封装外,我加入了const appContext = getCurrentInstance()?.appContext;。这样做的目的是,传入的组件在这里其实已经独立于应用的Vue上下文了。为了让组件依然保持和调用方相同的Vue上下文,我这里加入了获取上下文的操作!


基于这个情况,在使用useCommandComponent时需要保证它在setup中被调用,而不是在某个点击事件的处理函数中哦~


最后


如果你觉得useCommandComponent对你在开发中有所帮助,麻烦多点赞评论收藏😊


如果useCommandComponent对你实现某些业务有所启发,麻烦多点赞评论收藏😊


如果...,麻烦多点赞评论收藏😊


如果大家有其他弹窗方案,欢迎留言交流哦!


1632388279060.gif

收起阅读 »

前端业务代码,怎么写测试用例?

web
为什么前端写测试用例困难重重 关于不同测试的种类,网上有很多资料,比如单元、集成、冒烟测试,又比如 TDD BDD 等等,写测试的好处也不用多说,但是就前端来说,写测试用例,特别是针对业务代码测试用例写还是不太常见的事情。我总结的原因有如下几点: 搭建测试环...
继续阅读 »

为什么前端写测试用例困难重重


关于不同测试的种类,网上有很多资料,比如单元、集成、冒烟测试,又比如 TDD BDD 等等,写测试的好处也不用多说,但是就前端来说,写测试用例,特别是针对业务代码测试用例写还是不太常见的事情。我总结的原因有如下几点:



  • 搭建测试环境比较麻烦,什么 jest config、mock 这个、mock 那个,有那个时间写完 mock,都能写完业务代码了

  • 网上能找到的测试教程资料都是简单的 demo,与真实业务场景不匹配,看了这些 demo,还是不知道怎么写测试

  • 网上很难找到合适的模版项目,像 antd 这种都是针对公共 UI 组件的测试用例,对我们写业务逻辑的测试用例没有太大的参考价值

  • 业务需求改动频繁,导致维护测试用例的成本高


我最近在做一个 React Native 项目,想践行 TDD 开发,所以我花了几天时间,梳理了市面上常见的前端测试工具,看了 N 个前端测试实践的文章,最终选择了大道至简,只用下面两个库:



  • jest,不多说,最流行的类 react 项目的测试框架

  • react-test-renderer,用于测试组件 UI,搭配 jest 的快照功能一起使用,让测试 UI 变得不再繁琐


业务代码的测试用例之心法


不要这样写业务代码的测试用例


不要面向实现写测试用例,比如针对某个组件,把每个 props 都写一个测试用例,而 props 很有可能因为业务改动或重构等原因改动,导致我们也要改动相应的测试用例代码,尽管测试用例本身没有错误。


页面跳转、没有任何交互的静态页面、兼容性、


业务代码要怎么写测试


为了平衡开发时间和写测试用例的时间,我认为对于业务代码来说,测试用例不需要面面俱到,什么逻辑都写个测试用例。我们只需要关注用户交互相关的逻辑,具体来说,我会重点关注以下方面:



  • pure 组件的 UI 是否有对应的测试用例

  • 面向功能测试,比如用户输入、点击按钮、加载数据时的 UI、数据为空时的 UI

  • 针对工具函数的各种输入输出测试


写测试用例所需的成本由低到高依次是:

reducer → pure component → business component → DOM testing → e2e

其中 pure component 指的是只有 props 的,只负责渲染的 dummy component。Business compoent 指的是包含 store dispatcher、api fetch、副作用等业务逻辑的业务组件。


程度越靠后,测试的成本越高,所以我们可以花多些精力在测试组件和 reducer 上,少花时间在 DOM 测试和 e2e 测试上。而对于 reducer、pure component、business component 来说,它们的测试用例是相辅相成的,因为 business component 里就包括了 reducer 的使用和 pure component 的渲染,

所以测 business compoent,就等于侧面测到了 reducer 和 pure component。这个测试方法在 Redux 官网也有提到:

完全避免直接测试任何 Redux 代码,将其视为实现细节cn.redux.js.org/usage/writi…


案例:如何测试 pure component


Dumb Component 只用来接收 props 并进行展示,所以它更易于测试,我们只需要 mock 父组件传来的 props 即可,然后搭配 Jest 的 snapshot 快照来判断测试用例是否通过。


比如我们要测试 Tag Component,这个组件的功能很简单,就是展示标签 UI:


Pasted image 20230708154654.png


我们可以用快照测试来记录下这个组件的 UI,如果以后 UI 有改动,这条测试用例就会报错。比如我们现在多了一个业务逻辑,需要每个标签都自动带上 [],好比之前标签展示的是 text,根据业务逻辑,现在标签展示的是 [text]


我们修改 Tag 组件,添加相应的业务逻辑:


Pasted image 20230708155008.png


这时候跑测试用例,可以发现用例报错,而且我们可以报错结果知道组件的 UI 进行了哪些改动,如果这个改动是符合我们期待的,那么直接更新 snapshot 即可:


Pasted image 20230708155111.png


同时,提交代码的时候,这条测试用例对应的 snapshot 也会跟着一起 commit,在 Code Review 阶段我们可以根据 snapshot 来直观的看到组件 UI 进行了哪些改动,美滋滋啊。


如何对 Reducer 进行测试


用 Redux 作为状态管理工具时,一种比较好的编程范式是,让 Store 提供数据,组件只负责渲染数据。组件 UI 可能会因为业务变动而频繁的更改,而 Redux 中的数据逻辑不会经常更改,所以在没有任何像上面那种组件 UI 的快照测试时,可以优先测试 Redux,后期补上组件的快照测试。


工作流:



  1. 先写测试用例,开一个 snapshot

  2. 开启 jest --watch,编写 action 和 reducer 相关代码

  3. 当 snapshot 是我们期待的值,就保存这个 snapshot

  4. 完成测试用例
    作者:Kz
    来源:juejin.cn/post/7253102401452032055
    的编写

收起阅读 »

为什么谷歌搜索不支持无限分页

这是一个很有意思却很少有人注意的问题。 当我用Google搜索MySQL这个关键词的时候,Google只提供了13页的搜索结果,我通过修改url的分页参数试图搜索第14页数据,结果出现了以下的错误提示: 百度搜索同样不提供无限分页,对于MySQL关键词,百度...
继续阅读 »

这是一个很有意思却很少有人注意的问题。


当我用Google搜索MySQL这个关键词的时候,Google只提供了13页的搜索结果,我通过修改url的分页参数试图搜索第14页数据,结果出现了以下的错误提示:


Google不能无限分页


百度搜索同样不提供无限分页,对于MySQL关键词,百度搜索提供了76页的搜索结果。


百度不能无限分页


为什么不支持无限分页


强如Google搜索,为什么不支持无限分页?无非有两种可能:



  • 做不到

  • 没必要


「做不到」是不可能的,唯一的理由就是「没必要」。


首先,当第1页的搜索结果没有我们需要的内容的时候,我们通常会立即更换关键词,而不是翻第2页,更不用说翻到10页往后了。这是没必要的第一个理由——用户需求不强烈。


其次,无限分页的功能对于搜索引擎而言是非常消耗性能的。你可能感觉很奇怪,翻到第2页和翻到第1000页不都是搜索嘛,能有什么区别?


实际上,搜索引擎高可用和高伸缩性的设计带来的一个副作用就是无法高效实现无限分页功能,无法高效意味着能实现,但是代价比较大,这是所有搜索引擎都会面临的一个问题,专业上叫做「深度分页」。这也是没必要的第二个理由——实现成本高。


我自然不知道Google的搜索具体是怎么做的,因此接下来我用ES(Elasticsearch)为例来解释一下为什么深度分页对搜索引擎来说是一个头疼的问题。


为什么拿ES举例子


Elasticsearch(下文简称ES)实现的功能和Google以及百度搜索提供的功能是相同的,而且在实现高可用和高伸缩性的方法上也大同小异,深度分页的问题都是由这些大同小异的优化方法导致的。


什么是ES


ES是一个全文搜索引擎。


全文搜索引擎又是个什么鬼?


试想一个场景,你偶然听到了一首旋律特别优美的歌曲,回家之后依然感觉余音绕梁,可是无奈你只记得一句歌词中的几个字:「伞的边缘」。这时候搜索引擎就发挥作用了。


使用搜索引擎你可以获取到带有「伞的边缘」关键词的所有结果,这些结果有一个术语,叫做文档。并且搜索结果是按照文档与关键词的相关性进行排序之后返回的。我们得到了全文搜索引擎的定义:



全文搜索引擎是根据文档内容查找相关文档,并按照相关性顺序返回搜索结果的一种工具



2022-06-08-085125.png


网上冲浪太久,我们会渐渐地把计算机的能力误以为是自己本身具备的能力,比如我们可能误以为我们大脑本身就很擅长这种搜索。恰恰相反,全文检索的功能是我们非常不擅长的。


举个例子,如果我对你说:静夜思。你可能脱口而出:床前明月光,疑是地上霜。举头望明月,低头思故乡。但是如果我让你说出带有「月」的古诗,想必你会费上一番功夫。


包括我们平时看的书也是一样,目录本身就是一种符合我们人脑检索特点的一种搜索结构,让我们可以通过文档ID或者文档标题这种总领性的标识来找到某一篇文档,这种结构叫做正排索引


目录就是正排索引


而全文搜索引擎恰好相反,是通过文档中的内容来找寻文档,诗词大会中的飞花令就是人脑版的全文搜索引擎。


飞花令就是全文搜索


全文搜索引擎依赖的数据结构就是大名鼎鼎的倒排索引(「倒排」这个词就说明这种数据结构和我们正常的思维方式恰好相反),它是单词和文档之间包含关系的一种具体实现形式。


单词文档矩阵


打住!不能继续展开了话题了,赶紧一句话介绍完ES吧!



ES是一款使用倒排索引数据结构、能够根据文档内容查找相关文档,并按照相关性顺序返回搜索结果的全文搜索引擎



高可用的秘密——副本(Replication)


高可用是企业级服务必须考虑的一个指标,高可用必然涉及到集群和分布式,好在ES天然支持集群模式,可以非常简单地搭建一个分布式系统。


ES服务高可用要求其中一个节点如果挂掉了,不能影响正常的搜索服务。这就意味着挂掉的节点上存储的数据,必须在其他节点上留有完整的备份。这就是副本的概念。


副本


如上图所示,Node1作为主节点,Node2Node3作为副本节点保存了和主节点完全相同的数据,这样任何一个节点挂掉都不会影响业务的搜索。满足服务的高可用要求。


但是有一个致命的问题,无法实现系统扩容!即使添加另外的节点,对整个系统的容量扩充也起不到任何帮助。因为每一个节点都完整保存了所有的文档数据。


因此,ES引入了分片(Shard)的概念。


PB级数量的基石——分片(Shard)


ES将每个索引(ES中一系列文档的集合,相当于MySQL中的表)分成若干个分片,分片将尽可能平均地分配到不同的节点上。比如现在一个集群中有3台节点,索引被分成了5个分片,分配方式大致(因为具体如何平均分配取决于ES)如下图所示。


分片


这样一来,集群的横向扩容就非常简单了,现在我们向集群中再添加2个节点,则ES会自动将分片均衡到各个节点之上:


横向扩展


高可用 + 弹性扩容


副本和分片功能通力协作造就了ES如今高可用支持PB级数据量的两大优势。


现在我们以3个节点为例,展示一下分片数量为5,副本数量为1的情况下,ES在不同节点上的分片排布情况:


主分片和副分片的分布


有一点需要注意,上图示例中主分片和对应的副本分片不会出现在同一个节点上,至于为什么,大家可以自己思考一下。


文档的分布式存储


ES是怎么确定某个文档应该存储到哪一个分片上呢?



通过上面的映射算法,ES将文档数据均匀地分散在各个分片中,其中routing默认是文档id。


此外,副本分片的内容依赖主分片进行同步,副本分片存在意义就是负载均衡、顶上随时可能挂掉的主分片位置,成为新的主分片。


现在基础知识讲完了,终于可以进行搜索了。


ES的搜索机制


一图胜千言:


es搜索



  1. 客户端进行关键词搜索时,ES会使用负载均衡策略选择一个节点作为协调节点(Coordinating Node)接受请求,这里假设选择的是Node3节点;

  2. Node3节点会在10个主副分片中随机选择5个分片(所有分片必须能包含所有内容,且不能重复),发送search request;

  3. 被选中的5个分片分别执行查询并进行排序之后返回结果给Node3节点;

  4. Node3节点整合5个分片返回的结果,再次排序之后取到对应分页的结果集返回给客户端。



注:实际上ES的搜索分为Query阶段Fetch阶段两个步骤,在Query阶段各个分片返回文档Id和排序值,Fetch阶段根据文档Id去对应分片获取文档详情,上面的图片和文字说明对此进行了简化,请悉知。



现在考虑客户端获取990~1000的文档时,ES在分片存储的情况下如何给出正确的搜索结果。


获取990~1000的文档时,ES在每个分片下都需要获取1000个文档,然后由Coordinating Node聚合所有分片的结果,然后进行相关性排序,最后选出相关性顺序在990~100010条文档。


深度分页


页数越深,每个节点处理的文档也就越多,占用的内存也就越多,耗时也就越长,这也就是为什么搜索引擎厂商通常不提供深度分页的原因了,他们没必要在客户需求不强烈的功能上浪费性能。


<
作者:蝉沐风
来源:juejin.cn/post/7136009269903622151
hr/>

完。

收起阅读 »

关于浏览器缓存策略这件事儿

web
前言 我们打开百度这个网站并刷新多次时时,注意到百度的logo是没有每次都加载一遍的。我们知道图片是img标签中的src属性加载出来的,这也需要浏览器去请求图片资源的,那么为什么刷新多次浏览器只请求了一次图片资源呢?这就涉及到了浏览器的缓存策略了,这张图片被浏...
继续阅读 »

前言


我们打开百度这个网站并刷新多次时时,注意到百度的logo是没有每次都加载一遍的。我们知道图片是img标签中的src属性加载出来的,这也需要浏览器去请求图片资源的,那么为什么刷新多次浏览器只请求了一次图片资源呢?这就涉及到了浏览器的缓存策略了,这张图片被浏览器缓存下来了!


正文


一、为什么要有浏览器的缓存策略?



  • 提升用户体验,减少页面重复的http请求


二、为什么通过浏览器url地址栏访问的html页面不缓存?



  • 强制刷新页面浏览器url地址栏访问资源 时,浏览器默认会在请求头中设置Cache-control: no-cache,如设置该属性浏览器就会忽略响应头中的 Cache-control


如何优化网络资源请求的时间呢?有以下三种方式。


三、CDN网络分发



CDN:CDN会通过负载均衡技术,将用户的请求定向到最合适缓存服务器上去获取内容。



比如说,北京的用户,我们让他访问北京的节点,深圳的用户,我们让他访问深圳的节点。通过就近访问,加速用户对网站的访问,进而解决Internet网络拥堵状况,提高用户访问网络的响应速度。


四、强缓存



强缓存是浏览器的缓存策略,后端设置响应头中的属性值就能设置文件资源在浏览器的缓存时间过了缓存的有效期再次访问时,文件资源需再次加载



强缓存有两种方式来控制资源被浏览器缓存的时长:



  1. 后端设置响应头中的 Cache-control: max-age=3600 来控制缓存时长(为一个小时)

  2. 后端设置响应头中的 Expires:xxx 来控制缓存的截止日期(截止日期为xxx)


我们直接上代码让你更好理解,我们需要实现一个页面,页面上需展现一个标题一张图片


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>Earth</h1>
<img src="assets/image/earth.jpeg" alt="">
</body>
</html>

const http = require('http');
const path = require('path');
const fs = require('fs');
const mime = require('mime'); //接收一个文件后缀,返回一个文件类型

const server = http.createServer((req, res) => {
const filePath = path.resolve(__dirname, `www/${req.url}`) //resolve合并地址
if (fs.existsSync(filePath)) { //判断路径是否有效
const stats = fs.statSync(filePath) //获取文件信息
const isDir = stats.isDirectory() //是否是文件夹
if (isDir) {
filePath = path.join(filePath, 'index.html')
}
//读取文件
if (!isDir || fs.existsSync(filePath)) {

//判断前端请求的路径的后缀名是图片还是文本
const { ext } = path.parse(filePath) //.html .jpeg

const time = new Date(Date.now() + 3600000).toUTCString() //定义时间 作为缓存时间的有效期

let status = 200

res.writeHead(status, {
'Content-Type': `${mime.getType(ext)};charset=utf-8`,
'Cache-control': 'max-age=3600', //缓存时长为一小时
// 'expires': time //截止日期 缓存一小时后过期
})

if (status === 200) {
const fileStream = fs.createReadStream(filePath) //将文件读成流类型
fileStream.pipe(res) //将文件流导入响应体
}else{
res.end();
}

}
}
})

server.listen(3000, () => {
console.log('listening on port 3000');
})

第一次运行:
image.png


刷新页面后,可以看到图片资源没有重新加载:
image.png


三、协商缓存


我们想象这样的场景:当我们偷偷把图片偷偷换成另一张图片,图片名依然和之前那张一样,会是什么结果呢?

操作后,刷新页面发现图片还是之前那张图片,并没有换成新的!那这就出事儿了,后端图片换了,用户看到的还是老图片,有一种方案是改变图片资源的名字,直接请求最新图片资源,但这并不是最优方案,终极方案是需要协商缓存的帮忙。



协商缓存也是浏览器的缓存策略,它也有两种方式辅助强缓存,来判断文件资源是否被修改



1. 后端设置响应头中的 last-modified: xxxx



  • 辅助强缓存,让URL地址栏请求的资源也能被缓存

  • 辅助强缓存,借助请求头中的if-modified-since来判断资源文件是否被修改,如果被修改则返回新的资源,否则返回304状态码,让前端读取本地缓存


代码如下:


const http = require('http');
const path = require('path');
const fs = require('fs');
const mime = require('mime'); //接收一个文件后缀,返回一个文件类型

const server = http.createServer((req, res) => {
const filePath = path.resolve(__dirname, `www/${req.url}`) //resolve合并地址
if (fs.existsSync(filePath)) { //判断路径是否有效
const stats = fs.statSync(filePath) //获取文件信息
const isDir = stats.isDirectory() //是否是文件夹
if (isDir) {
filePath = path.join(filePath, 'index.html')
}
//读取文件
if (!isDir || fs.existsSync(filePath)) {

//判断前端请求的路径的后缀名是图片还是文本
const { ext } = path.parse(filePath) //.html .jpeg

const time = new Date(Date.now() + 3600000).toUTCString() //定义时间 作为缓存时间的有效期

const timeStamp = req.headers['if-modified-since'] //请求头的if-modified-since字段
let status = 200

//判断文件是否修改过
if (timeStamp && Number(timeStamp) === stats.mtimeMs) { //timeStamp为字符串 转换为number类型判断
status = 304
}

res.writeHead(status, {
'Content-Type': `${mime.getType(ext)};charset=utf-8`,
'Cache-control': 'max-age=3600', //缓存时长为一小时 //max-age=0或no-cache不需要缓存
// 'expires': time //截止日期 缓存一小时后过期
'last-modified': stats.mtimeMs //文件最后一次修改时间
})

if (status === 200) {
const fileStream = fs.createReadStream(filePath) //将文件读成流类型
fileStream.pipe(res) //将文件流导入响应体
}else{
res.end();
}

}
}
})

server.listen(3000, () => {
console.log('listening on port 3000');
})

我们只要看last-modified这个字段的值有无变化即可:
image.png


2. Etag:文件的标签



  • 请求头中会被携带If--Match

  • Etag保证了每一个资源是唯一的,资源变化都会导致Etag变化。服务器根据If--Match值来判断是否命中缓存。 当服务器返回304的响应时,由于ETag重新生成过,response header中还会把这个ETag返回,即使这个ETag跟之前的没有变化。


image.png

代码如下:


const http = require('http');
const path = require('path');
const fs = require('fs');
const mime = require('mime'); //接收一个文件后缀,返回一个文件类型
const md5 = require('crypto-js/md5');

const server = http.createServer((req, res) => {
const filePath = path.resolve(__dirname, `www/${req.url}`) //resolve合并地址
if (fs.existsSync(filePath)) { //判断路径是否有效
const stats = fs.statSync(filePath) //获取文件信息
const isDir = stats.isDirectory() //是否是文件夹
if (isDir) {
filePath = path.join(filePath, 'index.html')
}
//读取文件
if (!isDir || fs.existsSync(filePath)) {

//判断前端请求的路径的后缀名是图片还是文本
const { ext } = path.parse(filePath) //.html .jpeg
const content = fs.readFileSync(filePath);
let status = 200

//判断文件是否被修改过
if (req.headers['if-none-match'] == md5(content)) {
status=304
}

res.writeHead(status, {
'Content-Type': `${mime.getType(ext)};charset=utf-8`,
'Cache-control': 'max-age=3600', //缓存时长为一小时 //max-age=0或no-cache不需要缓存
'Etag': md5(content) //文件资源的md5值
})

if (status === 200) {
const fileStream = fs.createReadStream(filePath) //将文件读成流类型
fileStream.pipe(res) //将文件流导入响应体
} else {
res.end();
}

}
}
})

server.listen(3000, () => {
console.log('listening on port 3000');
})


最后附上一张图便于更好理解浏览器的缓存策略:


image.png

收起阅读 »

什么是圈复杂度?如何降低圈复杂度?

圈复杂度:理解和降低代码复杂性 在软件开发中,代码的复杂性是一个重要的考量因素。圈复杂度是一种用于衡量代码复杂性的指标,它可以帮助开发者评估代码的可读性、可维护性和可测试性。本文将详细介绍圈复杂度的概念,并提供几种降低圈复杂度的方法。同时,我们还将探讨如何在前...
继续阅读 »

圈复杂度:理解和降低代码复杂性


在软件开发中,代码的复杂性是一个重要的考量因素。圈复杂度是一种用于衡量代码复杂性的指标,它可以帮助开发者评估代码的可读性、可维护性和可测试性。本文将详细介绍圈复杂度的概念,并提供几种降低圈复杂度的方法。同时,我们还将探讨如何在前端开发中使用ESLint和VS Code工具来设置和检测圈复杂度。


什么是圈复杂度?


圈复杂度是由Thomas J. McCabe于1976年提出的一种软件度量指标,用于衡量程序中的控制流程复杂性。它通过计算代码中的判断语句和循环语句的数量来评估代码的复杂性。圈复杂度的值越高,代码的复杂性就越高,理解和维护代码的难度也就越大。


圈复杂度的计算方法是通过构建程序的控制流图,然后统计图中的节点数和边数来得出结果。每个判断语句(如if语句)和循环语句(如for循环)都会增加控制流图中的节点数和边数。圈复杂度的值等于图中边数减去节点数,再加上2。这个值表示了代码中独立路径的数量,即代码执行的可能路径数。


圈复杂度的计算方式可以通过以下步骤进行:




  1. 首先,将程序转换为控制流图(Control Flow Graph,CFG)。控制流图是一种图形表示方法,用于描述程序中的控制流程,包括各种条件和循环语句。




  2. 在控制流图中,每个节点表示程序中的一个基本块(Basic Block),即一组连续的语句序列,没有分支或跳转语句。




  3. 接下来,计算控制流图中的节点数量(N)和边数量(E)。节点数量即为程序中的基本块数量,边数量表示基本块之间的控制流转移关系。




  4. 根据以下公式计算圈复杂度(V):

    V = E - N + 2


    公式中的2表示程序的入口和出口节点,因为每个程序都至少有一个入口和一个出口。




为什么要降低圈复杂度?


高圈复杂度的代码往往难以理解和维护。当代码的复杂性增加时,开发者需要花费更多的时间和精力来理解代码的逻辑和执行路径。这不仅增加了开发和调试的难度,还可能导致代码中隐藏的逻辑错误。


圈复杂度代码状况可测性维护成本
1-10清晰、结构化
10-20复杂
20-30非常复杂
>30不可读不可测非常高

降低圈复杂度有助于提高代码的可读性和可维护性。简化代码结构可以使代码更易于理解,减少错误的引入,并提高代码的可测试性。此外,降低圈复杂度还有助于改善代码的性能,因为简单的代码通常执行更快。


如何降低圈复杂度?


以下是几种降低圈复杂度的常用方法:


1. 减少条件语句的嵌套


条件语句的嵌套是导致圈复杂度增加的常见原因之一。当条件语句嵌套层级过多时,代码的可读性和可维护性都会受到影响。为了降低圈复杂度,可以考虑使用早期返回(early return)的方式来减少条件语句的嵌套。通过在函数内部尽早返回结果,可以避免深层嵌套的条件判断。


function calculateGrade(score) {
if (score >= 90) {
return 'A';
}
if (score >= 80) {
return 'B';
}
if (score >= 70) {
return 'C';
}
return 'D';
}

2. 拆分复杂函数


函数的复杂性是导致圈复杂度升高的另一个常见原因。当一个函数包含过多的逻辑和操作时,它往往难以理解和维护。为了降低圈复杂度,可以将复杂的函数拆分成多个小函数,每个函数只负责一个特定的任务。这样可以提高代码的可读性和可维护性,并且使得每个函数的圈复杂度更低。


function calculateGrade(score) {
if (score >= 90) {
return 'A';
}
return calculateGradeForLowerScores(score);
}

function calculateGradeForLowerScores(score) {
if (score >= 80) {
return 'B';
}
if (score >= 70) {
return 'C';
}
return 'D';
}

3. 使用循环和迭代替代重复的代码块


重复的代码块会增加代码的复杂性和重复性。为了降低圈复杂度,可以使用循环和迭代来替代重复的代码块。通过将重复的逻辑抽象成一个函数,并在循环中调用该函数,可以减少代码的重复性和复杂性。


function printNumbers() {
for (let i = 1; i <= 10; i++) {
console.log(i);
}
}

4. 使用适当的数据结构和算法


选择适当的数据结构和算法可以帮助降低代码的复杂性和提高性能。例如,使用哈希表可以减少查找操作的复杂度,使用排序算法可以提高搜索和比较的效率。通过选择合适的数据结构和算法,可以降低代码的圈复杂度并提高代码的执行效率。


使用ESLint检测圈复杂度


ESLint是一个流行的JavaScript代码检查工具,它可以帮助开发者发现和修复代码中的问题,包括圈复杂度。ESLint提供了许多规则和插件,可以配置和检测圈复杂度。


在ESLint中,可以使用complexity规则来设置圈复杂度的阈值。通过在配置文件中设置适当的阈值,可以在代码检查过程中发现圈复杂度过高的代码段,并及时进行优化和重构。


// .eslintrc.js
module.exports = {
rules: {
complexity: ['error', 15], // 设置圈复杂度阈值为15
},
};

使用VS Code工具检测圈复杂度


VS Code是一款流行的代码编辑器,它提供了许多插件和工具,可以帮助开发者提高代码质量和效率。在VS Code中,可以使用插件如ESLint、CodeMetrics等来检测圈复杂度。


安装ESLint插件后,可以在VS Code的设置中配置圈复杂度的阈值,并在编辑器中实时检测代码的圈复杂度。通过设置合适的阈值,可以在开发过程中及时发现和解决代码复杂性问题。


结论


圈复杂度是衡量代码复杂性的重要指标,通过降低圈复杂度可以提高代码的可读性、可维护性和可测试性。在前端开发中,使用ESLint和VS Code工具可以帮助我们设置和检测圈复杂度,并及时发现和解决代码中的复杂性问题。通过合理的代码设计和优化,我们可以编写出更简洁、高效和易于维护的代码。


希望本文对你理解圈复杂度以及降低

作者:jungang
来源:juejin.cn/post/7253291161397559353
代码复杂性有所帮助!

收起阅读 »

js十大手撕代码

web
前言 js中有很多API贼好用,省下了很多工夫,你知道它的原理吗?这篇文章对它们做一个总结。 正文 一、手撕instanceof instanceof的原理:通过判断对象的原型是否等于构造函数的原型来进行类型判断 代码实现: const myInstanc...
继续阅读 »

前言


js中有很多API贼好用,省下了很多工夫,你知道它的原理吗?这篇文章对它们做一个总结。


正文


一、手撕instanceof



  • instanceof的原理:通过判断对象的原型是否等于构造函数的原型来进行类型判断

  • 代码实现:


const myInstanceOf=(Left,Right)=>{
if(!Left){
return false
}
while(Left){
if(Left.__proto__===Right.prototype){
return true
}else{
Left=Left.__proto__
}
}
return false
}

//验证
console.log(myInstanceOf({},Array)); //false

二、手撕call,apply,bind


call,apply,bind是通过this的显示绑定修改函数的this指向


1. call


call的用法:a.call(b) -> 将a的this指向b

我们需要借助隐式绑定规则来实现call,具体实现步骤如下:

往要绑定的那个对象(b)上挂一个属性,值为需要被调用的那个函数名(a),在外层去调用函数。


function foo(x,y){
console.log(this.a,x+y);
}

const obj={
a:1
}

Function.prototype.myCall=function(context,...args){
if(typeof this !== 'function') return new TypeError('is not a function')
const fn=Symbol('fn') //使用Symbol尽可能降低myCall对其他的影响
context[fn]=this //this指向foo
const res=context[fn](...args) //解构,调用fn
delete context[fn] //不要忘了删除obj上的工具函数fn
return res //将结果返回
}

//验证
foo.myCall(obj,1,2) //1,3

2. apply


apply和call的本质区别就是接受的参数形式不同,call接收零散的参数,而apply以数组的方式接收参数,实现思路完全一样,代码如下:


function foo(x,y){
console.log(this.a,x+y);
}

const obj={
a:1
}

Function.prototype.myApply=function(context,args){
if(typeof this !== 'function') return new TypeError('is not a function')
const fn=Symbol('fn') //尽可能降低myCall对其他的影响
context[fn]=this
context[fn](...args)
delete context[fn]
}

//验证
foo.myApply(obj,[1,2]) //1,3

3. bind


bind和call,apply的区别是会返回一个新的函数,接收零散的参数

需要注意的是,官方bind的操作是这样的:



  • 当new了bind返回的函数时,相当于new了foo,且new的参数需作为实参传给foo

  • foo的this.a访问不到obj中的a


function foo(x,y,z){
this.name='zt'
console.log(this.a,x+y+z);
}

const obj={
a:1
}


Function.prototype.myBind=function(context,...args){

if(typeof this !== 'function') return new TypeError('is not a function')

context=context||window

let _this=this

return function F(...arg){
//判断返回出去的F有没有被new,有就要把foo给到new出来的对象
if(this instanceof F){
return new _this(...args,...arg) //new一个foo
}
_this.apply(context,args.concat(arg)) //this是F的,_this是foo的 把foo的this指向obj用apply
}
}

//验证
const bar=foo.myBind(obj,1,2)
console.log(new bar(3)); //undefined 6 foo { name: 'zt' }


三、手撕深拷贝


这篇文章中详细记录了实现过程
【js手写】浅拷贝与深拷贝


四、手撕Promise


思路:



  • 我们知道,promise是有三种状态的,分别是pending(异步操作正在进行), fulfilled(异步操作成功完成), rejected(异步操作失败)。我们可以定义一个变量保存promise的状态。

  • resolve和reject的实现:把状态变更,并把resolve或reject中的值保存起来留给.then使用

  • 要保证实例对象能访问.then,必须将.then挂在构造函数的原型上

  • .then接收两个函数作为参数,我们必须对所传参数进行判断是否为函数,当状态为fulfilled时,onFulfilled函数触发,并将前面resolve中的值传给onFulfilled函数;状态为rejected时同理。

  • 当在promise里放一个异步函数(例:setTimeout)包裹resolve或reject函数时,它会被挂起,那么当执行到.then时,promise的状态仍然是pending,故不能触发.then中的回调函数。我们可以定义两个数组分别存放.then中的两个回调函数,将其分别在resolve和reject函数中调用,这样保证了在resolve和reject函数触发时,.then中的回调函数即能触发。


代码如下:


const PENDING = 'pending'
const FULFILLED = 'fullfilled'
const REJECTED = 'rejected'

function myPromise(fn) {
this.state = PENDING
this.value = null
const that = this
that.resolvedCallbacks = []
that.rejectedCallbacks = []

function resolve(val) {
if (that.state == PENDING) {
that.state = FULFILLED
that.value = val
that.resolvedCallbacks.map((cb)=>{
cb(that.value)
})
}
}
function reject(val) {
if (that.state == PENDING) {
that.state = REJECTED
that.value = val
that.rejectedCallbacks.map((cb)=>{
cb(that.value)
})
}
}

try {
fn(resolve, reject)
} catch (error) {
reject(error)
}

}

myPromise.prototype.then = function (onFullfilled, onRejected) {
const that = this
onFullfilled = typeof onFullfilled === 'function' ? onFullfilled : v => v
onRejected= typeof onRejected === 'function' ? onRejected : r => { throw r }

if(that.state===PENDING){
that.resolvedCallbacks.push(onFullfilled)
that.resolvedCallbacks.push(onRejected)
}
if (that.state === FULFILLED) {
onFullfilled(that.value)
}
if (that.state === REJECTED) {
onRejected(that.value)
}
}

//验证 ok ok
let p = new myPromise((resolve, reject) => {
// reject('fail')
resolve('ok')
})

p.then((res) => {
console.log(res,'ok');
}, (err) => {
console.log(err,'fail');
})

五、手撕防抖,节流


这篇文章中详细记录了实现过程
面试官:什么是防抖和节流?如何实现?应用场景?


六、手撕数组API


1. forEach()


思路:



  • forEach()用于数组的遍历,参数接收一个回调函数,回调函数中接收三个参数,分别代表每一项的值、下标、数组本身。

  • 要保证数组能访问到我们自己手写的API,必须将其挂到数组的原型上


代码实现:


const arr = [
{ name: 'zt', age: 18 },
{ name: 'aa', age: 19 },
{ name: 'bb', age: 18 },
{ name: 'cc', age: 21 },
]

//代码实现
Array.prototype.my_forEach = function (callback) {
for (let i = 0; i < this.length; i++) {
callback(this[i], i, this)
}
}

//验证
arr.my_forEach((item, index, arr) => { //111 111
if (item.age === 18) {
item.age = 17
return
}
console.log('111');
})


2. map()


思路:



  • map()也用于数组的遍历,与forEach不同的是,它会返回一个新数组,这个新数组是map接收的回调函数返回值

    代码实现:


const arr = [
{ name: 'zt', age: 18 },
{ name: 'aa', age: 19 },
{ name: 'bb', age: 18 },
{ name: 'cc', age: 21 },
]

Array.prototype.my_map=function(callback){
const res=[]
for(let i=0;i<this.length;i++){
res.push(callback(this[i],i,this))
}
return res
}

//验证
let newarr=arr.my_map((item,index,arr)=>{
if(item.age>18){
return item
}
})
console.log(newarr);
//[
// undefined,
// { name: 'aa', age: 19 },
// undefined,
// { name: 'cc', age: 21 }
//]

3. filter()


思路:



  • filter()用于筛选过滤满足条件的元素,并返回一个新数组


代码实现:


const arr = [
{ name: 'zt', age: 18 },
{ name: 'aa', age: 19 },
{ name: 'bb', age: 18 },
{ name: 'cc', age: 21 },
]

Array.prototype.my_filter = function (callback) {
const res = []
for (let i = 0; i < this.length; i++) {
callback(this[i], i, this) && res.push(this[i])
}
return res
}

//验证
let newarr = arr.my_filter((item, index, arr) => {
return item.age > 18
})
console.log(newarr); [ { name: 'aa', age: 19 }, { name: 'cc', age: 21 } ]

4. reduce()


思路:



  • reduce()用于将数组中所有元素按指定的规则进行归并计算,返回一个最终值

  • reduce()接收两个参数:回调函数、初始值(可选)。

  • 回调函数中接收四个参数:初始值 或 存储上一次回调函数的返回值、每一项的值、下标、数组本身。

  • 若不提供初始值,则从第二项开始,并将第一个值作为第一次执行的返回值


代码实现:


const arr = [
{ name: 'zt', age: 18 },
{ name: 'aa', age: 19 },
{ name: 'bb', age: 18 },
{ name: 'cc', age: 21 },
]

Array.prototype.my_reduce = function (callback,...arg) {
let pre,start=0
if(arg.length){
pre=arg[0]
}
else{
pre=this[0]
start=1
}
for (let i = start; i < this.length; i++) {
pre=callback(pre,this[i], i, this)
}
return pre
}

//验证
const sum = arr.my_reduce((pre, current, index, arr) => {
return pre+=current.age
},0)
console.log(sum); //76


5. fill()


思路:



  • fill()用于填充一个数组的所有元素,它会影响原数组 ,返回值为修改后原数组

  • fill()接收三个参数:填充的值、起始位置(默认为0)、结束位置(默认为this.length-1)。

  • 填充遵循左闭右开的原则

  • 不提供起始位置和结束位置时,默认填充整个数组


代码实现:


Array.prototype.my_fill = function (value,start,end) {
if(!start&&start!==0){
start=0
}
end=end||this.length
for(let i=start;i<end;i++){
this[i]=value
}
return this
}

//验证
const arr=new Array(7).my_fill('hh',null,3) //往数组的某个位置开始填充到哪个位置,左闭右开
console.log(arr); //[ 'hh', 'hh', 'hh', <4 empty items> ]


6. includes()


思路:



  • includes()用于判断数组中是否包含某个元素,返回值为 true 或 false

  • includes()提供第二个参数,支持从指定位置开始查找


代码实现:


const arr = ['a', 'b', 'c', 'd', 'e']

Array.prototype.my_includes = function (item,start) {
if(start<0){start+=this.length}
for (let i = start; i < this.length; i++) {
if(this[i]===item){
return true
}
}
return false
}

//验证
const flag = arr.my_includes('c',3) //查找的元素,从哪个下标开始查找
console.log(flag); //false


7. join()


思路:



  • join()用于将数组中的所有元素指定符号连接成一个字符串


代码实现:


const arr = ['a', 'b', 'c']

Array.prototype.my_join = function (s = ',') {
let str = ''
for (let i = 0; i < this.length; i++) {
str += `${this[i]}${s}`
}
return str.slice(0, str.length - 1)
}

//验证
const str = arr.my_join(' ')
console.log(str); //a b c

8. find()


思路:



  • find()用于返回数组中第一个满足条件元素,找不到返回undefined

  • find()的参数为一个回调函数


代码实现:


const arr = [
{ name: 'zt', age: 18 },
{ name: 'aa', age: 19 },
{ name: 'bb', age: 18 },
{ name: 'cc', age: 21 },
]

Array.prototype.my_find = function (callback) {
for (let i = 0; i < this.length; i++) {
if(callback(this[i], i, this)){
return this[i]
}

}
return undefined
}

//验证
let j = arr.my_find((item, index, arr) => {
return item.age > 19
})
console.log(j); //{ name: 'cc', age: 21 }

9. findIndex()


思路:



  • findIndex()用于返回数组中第一个满足条件索引,找不到返回-1

  • findIndex()的参数为一个回调函数


代码实现:


const arr = [
{ name: 'zt', age: 18 },
{ name: 'aa', age: 19 },
{ name: 'bb', age: 18 },
{ name: 'cc', age: 21 },
]

Array.prototype.my_findIndex = function (callback) {
for (let i = 0; i < this.length; i++) {
if(callback(this[i], i, this)){
return i
}
}
return -1
}


let j = arr.my_findIndex((item, index, arr) => {
return item.age > 19
})
console.log(j); //3

10. some()


思路:



  • some()用来检测数组中的元素是否满足指定条件。

  • 有一个元素符合条件,则返回true,且后面的元素会再检测。


代码实现:


const arr = [
{ name: 'zt', age: 18 },
{ name: 'aa', age: 19 },
{ name: 'bb', age: 18 },
{ name: 'cc', age: 21 },
]

Array.prototype.my_some = function (callback) {
for (let i = 0; i < this.length; i++) {
if(callback(this[i], i, this)){
return true
}
}
return false
}

//验证
const flag = arr.some((item, index, arr) => {
return item.age > 20
})
console.log(flag); //true

11. every()


思路:



  • every() 用来检测所有元素是否都符合指定条件。

  • 有一个不满足条件,则返回false,后面的元素都会再执行。


代码实现:


const arr = [
{ name: 'zt', age: 18 },
{ name: 'aa', age: 19 },
{ name: 'bb', age: 18 },
{ name: 'cc', age: 21 },
]

Array.prototype.my_every = function (callback) {
for (let i = 0; i < this.length; i++) {
if(!callback(this[i], i, this)){
return false
}
}
return true
}

//验证
const flag = arr.my_every((item, index, arr) => {
return item.age > 16
})
console.log(flag); //true


七、数组去重


1. 双层for循环 + splice()


let arr = [1, 1, '1', '1', 2, 2, 2, 3, 2]
function unique(arr) {
for (let i = 0; i < arr.length; i++) {
for (let j = i + 1; j < arr.length; j++) {
if (arr[i] === arr[j]) {
arr.splice(j, 1)
j-- //删除后j向前走了一位,下标需要减一,避免少遍历一位
}
}
}
return arr
}

console.log(unique(arr)) //[ 1, '1', 2, 3 ]

2. 排序后做前后比较


let arr = [1, 1, '1', '1', 2, 2, 2, 3, 2]

function unique(arr) {
let res = []
let seen //记录上一次比较的值
let newarr=[...arr] //解构出来,开辟一个新数组
newarr.sort((a,b)=>a-b) //sort会影响原数组 n*logn
for (let i = 0; i < newarr.length; i++) {
if (newarr[i]!==seen) {
res.push(newarr[i])
}
seen=newarr[i]
}
return res
}

console.log(unique(arr)) //[ 1, '1', 2, 3 ]

3. 借助include


let arr = [1, 1, '1', '1', 2, 2, 2, 3, 2]

function unique(arr) {
let res = []
for (let i = 0; i < arr.length; i++) {
if(!res.includes(arr[i])){
res.push(arr[i])
}
}
return res
}

console.log(unique(arr)) //[ 1, '1', 2, 3 ]

4. 借助set


let arr = [1, 1, '1', '1', 2, 2, 2, 3, 2]
const res1 = Array.from(new Set(arr));
console.log(res1); //[ 1, '1', 2, 3 ]

八、数组扁平化


1. 递归


let arr1 = [1, 2, [3, 4, [5],6]]

function flatter(arr) {
let len = arr.length
let result = []
for (let i = 0; i < len; i++) { //遍历数组每一项
if (Array.isArray(arr[i])) { //判断子项是否为数组并拼接起来
result=result.concat(flatter(arr[i]))//是则使用递归继续扁平化
}
else {
result.push(arr[i]) //不是则存入result
}
}
return result
}

console.log(flatter(arr1)) //[ 1, 2, 3, 4, 5, 6 ]

2. 借助reduce (本质也是递归)


let arr1 = [1, 2, [3, 4, [5],6]]

const flatter = arr => {
return arr.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
}, [])
}
console.log(flatter(arr1)) //[ 1, 2, 3, 4, 5, 6 ]

3. 借助正则


let arr1 = [1, 2, [3, 4, [5],6]]

const res = JSON.parse('[' + JSON.stringify(arr1).replace(/\[|\]/g, '') + ']');
console.log(res) //[ 1, 2, 3, 4, 5, 6 ]

九、函数柯里化


思路:



  • 函数柯里化是只传递给函数一部分参数调用它,让它返回一个函数去处理剩下的参数

  • 传入的参数大于等于原始函数fn的参数个数,则直接执行该函数,小于则继续对当前函数进行柯里化,返回一个接受所有参数(当前参数和剩余参数) 的函数


代码实现:


const my_curry = (fn, ...args) => 
args.length >= fn.length
? fn(...args)
: (...args1) => curry(fn, ...args, ...args1);

function adder(x, y, z) {
return x + y + z;
}
const add = my_curry(adder);
console.log(add(1, 2, 3)); //6
console.log(add(1)(2)(3)); //6
console.log(add(1, 2)(3)); //6
console.log(add(1)(2, 3)); //6

十、new方法


思路:



  • new方法主要分为四步:

    (1) 创建一个新对象

    (2) 将构造函数中的this指向该对象

    (3) 执行构造函数中的代码(为这个新对象添加属性

    (4) 返回新对象


function _new(obj, ...rest){
// 基于obj的原型创建一个新的对象
const newObj = Object.create(obj.prototype);

// 添加属性到新创建的newObj上, 并获取obj函数执行的结果.
const result = obj.apply(newObj, rest);

// 如果执行结果有返回值并且是一个对象, 返回执行的结果, 否则, 返回新创建的对象
return typeof result === 'object' ? result : newObj;
}



总结不易,

作者:zt_ever
来源:juejin.cn/post/7253260410664419389
动动手指给个赞吧!💗

收起阅读 »

抛开场景,一味的吹捧路由懒加载就像在耍流氓🤣

异步路由长啥样? 原理不做过多介绍(大家都知道),直接上代码了 一般来说,只有SPA需要异步路由。当配置了异步路由,通过split chunk,打包后都会生成单独的chunk,以webpack为例,建议添加魔法注释,并开启prefetch,以进一步提升体验im...
继续阅读 »

异步路由长啥样?


原理不做过多介绍(大家都知道),直接上代码了


一般来说,只有SPA需要异步路由。当配置了异步路由,通过split chunk,打包后都会生成单独的chunk,以webpack为例,建议添加魔法注释,并开启prefetch,以进一步提升体验

import loadable from '@loadable/component';

const XXXA = loadable(() => import('@/views/xx/XXXA'/* webpackChunkName: 'XXXA', webpackPrefetch: true */));

const XXXB = loadable(() => import('@/views/xx/XXXB'/* webpackChunkName: 'XXXB', webpackPrefetch: true */));

// ...

const allRoutes: IRoute[] = [
{
path: '/xxx-a',
component: XXXA,
},
{
path: '/xxx-b',
component: XXXB,
},
// ...
];

// ...

SPA应用,白屏现象能不能解决??


首先我们要搞明白的是,异步路由给我们解决的痛点是啥?


balabala....


Yes, 你打的很对👍🏾,就是为了缩减🐷包的大小,减少资源加载的时间。


但是,结合页面的加载过程,我先给您下个结论:


“SPA无论怎么优化,都无法避免白屏的现象产生,哪怕网速快,一闪而过(实际上你调成3g或带宽更低的网络,白屏一直伴随着你🤮)”


为啥🤬?



  • SPA的入口是index.html, 初始dom,只有div#app,并且一般都没有任何样式

  • 页面加载需要先加载必要js,比如main.[hash].js

  • main.[hash].js加载并解析成功,页面才会正常展示,so,空档期一定存在,这就是白屏现象的原因所在(注意:此时和是否有其他路由没有任何关系


为什么说异步路由不是100%保险??



问题的根本在于这句话:异步路由能提升首屏用户体验 👀



但是,很多人不知道,这里的首屏并不是单指index.html,举个例子:


对于移动端混合应用开发,首屏可能变成SPA中的任何一个路由路径。原因也很简单,APP首页中有很多菜单:每个菜单都可以配置路径,这些路径很可能来自同一个SPA。 我们暂且把这些路径叫做一级路由(不包括index.html)


对于以上场景,首屏不再单一。


开始划重点



  • 上述一级路由(不包括index.html)请不要配置成异步加载,使用普通的import即可, 让它打进主包中。

  • 对于其他非一级路由,只是通过push、replace进行跳转,那么配置为异步路由就很合适


实际案例分析


还是先下个结论:


比如有个路由/xx-a,它作为一级路由配置在了APP菜单中,那么它就变成了"所谓的首屏页面",如果我们还是使用异步路由,就会延长白屏的时间

import loadable from '@loadable/component';

const XxxA = loadable(() => import('@/views/xx/XxxA'/* webpackChunkName: 'XxxA', webpackPrefetch: true */));

// ...

const allRoutes: IRoute[] = [
{
path: '/xxx-a',
component: XXXA,
},
// ...
];

// ...

此时,build目录中存在



  • main.[hash].js (包含react路由逻辑

  • XxxA.[hash].js


此时,打卡devtools,就会知道,network中资源加载顺序如下



  1. index.html

  2. main.js

  3. 如果index.html中引入了其他资源, 比如jquery, lodash...,也会优先download这些资源(哪怕你配置的defer或者async)

  4. 当main加载并执行后,才会触发路由逻辑,并开始加载XxxA.[hash].js

  5. 加载XxxA.[hash].js成功,开始解析执行XxxA,XxxA页面才会被正常渲染


以上过程中,白屏的开始是main的加载和执行(包含了路由逻辑)的时间消耗产生的,而XxxA.[hash].js的加载解析和执行,又无疑增加了空档期,这样白屏时间也就被延长了


若index.html中引入了若干其他script资源,并且处于http1.1的服务器环境中,这个现象就会变得特别明显



  • 因为1.1多路复用对于资源的请求数量有限制,chrome下6个作为一组


我们可以借助network和performance进行实际演示说明:


为了演示,我们将网络调慢些(实际上客户的网络环境也有这样的情况


image.png


network



后续每组都需等待前一组加载完成,才开始加载



image.png


只有main加载成功并执行,才会触发路由逻辑,从而开始加载XxxA脚本。如果网络慢,main.js加载过程就会更长,从而间接导致XxxA脚本的加载和解析执行被推迟, 这无疑也就延长了白屏时间!!🦮


performance截图 (异步路由)


image.png


performance截图 (非异步路由)



此时代码逻辑被打进了main.js中,直接第一波解析执行即可,很明显缩短了白屏时间。



image.png


对比一下,就可以一目了然,哎,什么也不说了~


总结


异步路由并不能100%缩短白屏时间,最关键的是我们要知道“首屏”这个词的意义,它并不是单指index.html入口,它可以是SPA中的任何一个路由(结合混合移动开发场景就知道了)


So:



  • 如果SPA中的“首屏”只有一个,不存在移动混合开发场景或者路径分享场景(比如分享微信,支付宝等),那所有路由都可以进行异步加载

  • 如果存在移动混合开发场景或者路径分享场景,那对应的路由请不要异步加载,使用import即可。


好了,到此结束,如果对您有帮助,还望点个小💖💖


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

学前端必读的从输入url到页面渲染全过程

从输入 URL 到页面展示这中间到底发生了什么?,这是一道非常经典的面试题,这一过程涉及到了计算机网络、操作系统、Web等一系列知识,如果对这一过程有非常好的了解,对以后的开发甚至是程序的优化都是非常有益的。现将过程梳理如下: 1. 解析URL 分析所需要使用...
继续阅读 »

从输入 URL 到页面展示这中间到底发生了什么?,这是一道非常经典的面试题,这一过程涉及到了计算机网络、操作系统、Web等一系列知识,如果对这一过程有非常好的了解,对以后的开发甚至是程序的优化都是非常有益的。现将过程梳理如下:


1. 解析URL


分析所需要使用的传输协议和请求的资源路径。如果url中的协议或主机名不合法,将会把地址栏中输入的内容传递给搜索引擎,如果没有问题,浏览器会检查url中是否出现了非法字符,如果存在,对非法字符(空格、汉字等双字节字符)进行转义后在进行下一过程。


编码和解码:



  • encodeURI()/decodeURI():encodeURI()函数只会把参数中的空格编码为%20,汉字进行编码,其余特殊字符不会转换

  • encodeURIComponent()/decodeURIComponent():由于这个方法对:/都进行了编码,所以不能用它来对网址进行编码,适合对URL中的参数进行编码

  • escape()/unescape():将字符的unicode编码转化为16进制序列,不对ASCII字母和数字进行编码,也不会对' * @ - _ + . / '这些ASCII符号进行编码,用于服务器与服务器端传输多
let url = "http://www.baidu.com/test /中国"
console.log(encodeURI(url)); // http://www.baidu.com/test%20/%E4%B8%AD%E5%9B%BD
let url1 = `https://www.baidu.com/from=${encodeURIComponent('http://wwws.udiab.com')}`
console.log(url1); // https://www.baidu.com/from=http%3A%2F%2Fwwws.udiab.com
console.log(escape(url)); // http%3A//www.baidu.com/test%20/%u4E2D%u56FD

1.1 URL地址格式


传统格式:scheme://host:port/path?query#fragment。例:http://www.urltest.cn/system/user…



  • scheme(必写):协议http(超文本传输协议)、https(安全超文本传输协议)、ftp(文件传输协议,用于将文件下载或上传至网站)、file(计算机上的文件)

  • host(必写):域名或IP地址

  • port(可省略):端口号,http默认80,https默认443

  • path:路径,例如 /system/user

  • query:参数,例如 username=falcon&age=18

  • fragment:锚点(哈希hash),用于定位页面的某个位置


Restful格式:可以通过不同的请求方式(get、post、put、delete)来实现不同的效果



2. 缓存判断


浏览器缓存就是浏览器将用户请求过的资源存储到本地电脑。当浏览器再次访问时就可以直接从本地加载,不需要去服务器请求,能有效减少不必要的数据传输,减轻服务器负担,提升网站性能,提高客户端网页打开速度。浏览器缓存一般分为强缓存和协商缓存


image.png



  • 浏览器在请求某一资源时,会先获取该资源缓存的header信息,判断是否命中强缓存(cache-control、expires信息),命中则直接从缓存中获取资源信息,不会向服务器发起请求;

  • 如果没有命中强缓存,浏览器会发送请求(携带该资源缓存的第一次请求返回的header字段信息,Last-Modified/If-Modified-Since、Etag/If-None-Match)到服务器,由服务器根据请求中携带的相关header字段进行对比来判断是否命中协商缓存,命中则返回新的header信息更新缓存中对应的header信息,不返回资源内容,浏览器直接从缓存中获取;否则返回最新的资源内容


2.1 强缓存



  • expires,绝对时间字符串,发送请求在这个时间之前有效,之后无效


catch-control,几个比较常用的字段如下:



  • max-age=number,相对字段,利用资源第一次请求时间和Cache-Control设定的有效期,计算出一个资源过期时间,然后再进行比较

  • no-cache,不使用本地缓存,需要使用协商缓存

  • no-store,禁止浏览器缓存数据,每次用户都需要向服务器发送请求获取完整资源

  • public,可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器

  • private,只能被终端用户浏览器缓存


【注】



  • 强缓存如何重新加载缓存过的资源?使用强缓存,不用向服务器发送请求就可以获取到资源,如果在强缓存期间,资源发生了变化,浏览器就一直得不到最新的资源,如何操作:通过更新页面中引用的资源路径,让浏览器主动放弃缓存,加载新的资源

  • 如果二者同时存在,cache-control的优先级高于expires


2.2 协商缓存


Last-Modified/If-Modified-Since



  • 浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,会在response的header上加上Last-Modified(表示资源在服务器上的最后修改时间)字段

  • 浏览器再次跟服务器请求这个资源时,在request的header上加上If-Modified-Since(值就是上一次请求返回的Last-Modified值)字段,来判断是否发生变化,没变化返回304,从缓存中加载,也不会重新在response的header上添加Last-Modified;发生变化则直接从服务器加载,并且更新Last-Modified值


Etag/If-None-Match



  • 这两个值是由服务器生成的每个资源的唯一标识字符串,只要资源变化值就会发生改变

  • 判断过程与上面一组逻辑类似

  • 不同的是,当服务器返回304时,由于Etag重新生成过,response的header还是会把这个Etag返回


【注】



  • 为什么需要Etag?一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变修改时间);某些文件修改非常频繁(例如在秒以下的时间进行修改);某些服务器不能精确得到文件的最后修改时间等这些情况下,利用Etag能够更加精确的控制缓存

  • 两者可以一起使用,服务器会优先验证Etag,在一致的情况下,才会继续比对Last-Modified


2.3 用户行为对缓存的影响


image.png


3. DNS解析


获取输入url中的域名对应的IP地址。



  • 第一步,检查浏览器缓存中是否缓存过该域名对应的IP地址;

  • 第二步,检查本地的hosts文件(系统缓存)

  • 第三步,本地域名解析服务器进行解析;

  • 第四步,根域名解析服务器进行解析;

  • 第五步,gTLD服务器进行解析(顶级域名)

  • 第六步,权威域名服务器进行解析,最终获得域名IP地址;


3.1 域名层级结构图


image.png



  • 根域:位于域名空间最顶层,一般用一个点“.”表示

  • 顶级域:一般表示一种类型的组织机构或者国家地区。.net(网络供应商) .com(工商企业) .org(团体组织) .edu(教育机构) .gov(政府部门) .cn(中国国家域名)

  • 二级域:用来标明顶级域内一个特定的组织。.com.cn .net.cn .edu.cn

  • 子域:二级域下所创建的各级域名,各个组织或用户可以自由申请注册

  • 主机:位于域名空间最下层,一台具体的计算机。完整格式域:http://www.sina.com.cn


3.2 递归查询、迭代查询


用户向本地DNS服务器发起请求属于递归请求;本地DNS服务器向各级域名服务器发起请求属于迭代请求
image.png



  • 递归查询:以本地DNS服务器为中心,客户端发出请求报文后就一直处于等待状态,直到本地DNS服务器发来最终查询结果

  • 迭代查询:DNS服务器如有客户端请求数据则返回正确地址;没有则返回一个指针;按指针继续查询


4. TCP三次握手建立连接


image.png



  • 第一步,客户端发送SYN包(seq=x)到服务器,等待服务器确认

  • 第二步,服务器收到SYN包,确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(seq=y),即SYN+ACK包

  • 第三步,客户端收到SYN+ACK包,向服务器发送确认包ACK(ack=y+1)


三次握手完成,客户端和服务器正式开始传递数据


4.1 TCP、UDP



  • TCP 面向连接的协议,只有建立后才可以传递数据

  • UDP 无连接的协议,可以直接传送数据,传输效率较高,但不能保证数据的完整性


5. 发起http、https请求


5.1 http1.0、http1.1、http2.0区别



5.2 http、https



  • https需要CA申请证书,一般需要交费

  • http运行在TCP之上,明文传输;https运行在SSL/TLS之上,SSL/TLS运行在TCP之上,加密传输

  • http默认端口80,https默认端口443

  • https可以有效的防止运营商劫持


5.3 XSS、CSRF


XSS


XSS(Cross-site Scripting)。跨域脚本攻击,指通过利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序等。


分类如下:


image.png



  • 反射型。发出请求时,xss代码出现在URL中,作为输入提交到服务器端,服务器端解析后响应,xss代码随响应内容一起传回浏览器,最后浏览器解析执行xss代码。例如:"http://www.a.com/xss/reflect…"
    image.png

  • 存储型。和反射型的差别是提交的代码会存储在服务器端(数据库、内存、文件系统等),下次请求目标页面时不用再提交XSS代码。例如:留言板xss,用户提交留言到数据库,目标用户查看留言板时,留言的内容会从数据库查询出来并显示,浏览器解析执行,触发xss攻击

  • DOM型。不需要服务器的参与,触发xss靠的是浏览器端的DOM解析,完全是客户端触发


防御措施:



  • 过滤。对用户的输入(和URL参数)进行过滤。移除用户输入的和事件相关的属性,如onerror、onclick等;移除用户输入的Style节点、Script节点(一定要特别注意,它是支持跨域的)、Iframe节点

  • 编码。对输出进行html编码。对动态输出到页面的内容进行html编码,使脚本无法再浏览器中执行

  • 服务端设置会话Cookie的HTTP Only属性,这样客户端的JS脚本就不能获取cookie信息了


CSRF


CSRF(Cross-site request forgery)。跨站请求伪造,攻击者通过伪造用户的浏览器请求,向用户曾经认证访问过的网站发送出去,使目标网站接收并误以为是用户的真实操作而去执行命令。常用于转账、盗号、发送虚假消息等
image.png


防御措施:



  • token验证。服务器返回给客户端一个token信息,客户端带着token发送请求,如果token不合法,服务器拒绝这个请求

  • 隐藏令牌。将token隐藏在http的header中

  • referer验证。页面请求来源验证,只接受本站的请求,其他进行拦截


5.4 get、post


GET



  • 一般用于获取数据;

  • 参数放在url中;

  • 浏览器回退或刷新无影响;

  • 请求可被缓存;

  • 请求的参数放url上,有长度限制;

  • 请求的参数只能是ASCII码;

  • url对所有人可见,安全性差;

  • get产生一个tcp数据包,hearder、data一起发送一次请求,服务器返回200


POST



  • 一般用于向后台传递数据、创建数据;

  • 参数放在body里;

  • 浏览器回退或刷新数据会被需重提交;

  • 请求不会被缓存;

  • 请求的参数放body上,无长度限制;

  • 请求的参数类型无限制,允许二进制数据;

  • 请求参数不会被保存在浏览器历史或web服务器日志中,相对更安全;

  • post产生两个tcp数据包,先发送header,返回100 continue,再发送data,服务器返回200,但不是绝对的,Firefox只发送一次


5.5 状态码


1XX - 通知



  • 100 -- 客户端继续发送请求

  • 101 -- 切换协议


2XX - 成功



  • 200 -- 请求成功,一般应用与 get 或 post 请求

  • 201 -- 请求成功并创建新的资源,


3XX - 重定向



  • 301 -- 永久移动,请求的资源已被永久移动到新的 url,返回信息包括新的 url,浏览器会自动定向到新的 url,今后所有的请求都是新的 url

  • 302 -- 临时移动,资源只是临时移动,客户端应继续使用旧的 url

  • 304 -- 所请求的资源未修改,不会返回任何资源。浏览器请求的时候,会先访问强缓存,没有则访问协商缓存,协商缓存命中,资源未修改,返回 304


4XX - 客户端错误



  • 400 -- 客户端请求的语法错误,服务器无法理解(z 字段类型,或对应的值类型不一致;或者没有进行 JSON.toStringfy 的转换)

  • 401 -- 请求需要用户的认证

  • 403 -- 服务器理解客户端请求,但是拒绝执行

  • 404 -- 服务器无法根据客户端的请求找到资源


5XX - 服务器端错误



  • 500 -- 服务器内部错误,无法完成资源的请求

  • 501 -- 服务器不支持请求功能,无法完成资源请求

  • 502 -- 网关或代理服务器向远程服务器发送请求返回无效


5.6 跨域



  • 跨域:浏览器不能执行其他网站的脚本,这是由于同源策略(同协议、同域名、同端口)限制造成的

  • 同源策略限制的行为:cookie,localstorage和IndexDB无法读取;DOM无法获取;Ajax请求不能发送


跨域的几种解决方式:



  • jsonp,实现原理是<script>标签的src可以发跨域请求,不受同源策略限制,缺点是只能实现get一种请求

  • document.domain + iframe跨域domain属性可返回下载当前文档的服务器域名,此方案仅限主域相同,子域不同的跨域场景

  • 跨域资源共享(CORS)只服务端设置Access-Control-Allow-Origin即可,前端无需设置;若要带cookie请求,前后端都需要设置

  • nginx反向代理跨域,项目中常用的一种方案

  • html5的postMessage(跨文档消息传输),WebSocket(全双工通信、实时通信)


6. 返回数据


当页面请求发送到服务器端后,服务器端会返回一个html文件作为响应


7. 页面渲染


7.1 加载过程



  • HTML会被渲染成DOM树。HTML是最先通过网址请求过来的,请求过来之后,HTML本身会由一个字节流转化成一个字符流,浏览器端拿到字符流,之后通过词法分析,将相应的词法分析成相应的token,转化不同的token tag,然后通过token类型append到DOM树

  • 遇到link token tag,去请求css,然后对css进行解析,生成CSSOM树

  • DOM树和CSSOM树结合形成Render Tree,再进行布局和渲染

  • 遇到script tag,然后去请求JS相关的web资源,请求回来的js交给浏览器的v8引擎进行解析
    image.png


7.2 加载特点



  • html文档解析,对tag依次从上到下解析,顺序执行

  • html中可能会引入很多css,js的web资源,这些资源在浏览器中是并发加载的。

  • DOM树和CSSOM树通常是并行构建的, 所以CSS加载不会阻塞DOM的解析;Render树依赖DOM树和CSSOM树进行,所以CSS加载会阻塞DOM的渲染css会阻塞js文件执行,但不会阻塞js文件下载,因为GUI渲染线程与JavaScript线程互斥,JS有可能影响样式;js会阻塞DOM的解析(把js文件放在最下面),也就会阻塞DOM的渲染,同时js顺序执行,也会阻塞后续js逻辑的执行

  • 依赖关系。页面渲染依赖于css的加载;js的执行顺序依赖关系;js逻辑对于dom节点的依赖关系,有些js需要去获取dom节点

  • 引入方式。直接引入,不会阻塞页面渲染;defer不会阻塞页面渲染,顺序执行;async不会阻塞页面渲染,先到先执行,不保证顺序;异步动态js,需要的时候引入


【注】css样式置顶;js脚本置底;用link代替import;合理使用js异步加载


资源加载完成后,通过样式计算、布局设置、分层、绘制等过程,将页面呈现出来


7.3 重绘回流


根据渲染树,浏览器可以计算出网页中有哪些节点,各节点的CSS以及从属关系,发生回流;根据渲染树以及回流得到的节点信息,计算出每个节点在屏幕中的位置,发生重绘



  • 元素的规模尺寸、布局、显隐性发生变化时,发生回流,每个页面至少产生一次回流,第一次加载

  • 元素的外观、风格、颜色等发生变化而不影响布局,发生重绘

  • 回流一定发生重绘,重绘不一定发生回流


【避免措施】


样式设置



  • 避免使用层级较深的选择器

  • 避免使用 css 表达式

  • 元素适当的定义高度或最小高度

  • 给图片设置尺寸

  • 不要使用 table 布局

  • 能 css 实现的,尽量不要使用 js 实现


渲染层



  • 将需要多次重绘的元素独立为 render layer,如设置 absolute,可以减少重绘范围

  • 对于一些动画元素,使用硬件渲染


DOM 优化



  • 缓存 DOM

  • 减少 DOM 深度及 DOM 数量

  • 批量操作 DOM

  • 批量操作 CSS 样式

  • 在内存中操作 DOM

  • DOM 元素离线更新

  • DOM 读写分离

  • 事件代理

  • 防抖和节流

  • 及时清理环境


TCP四次挥手断开连接


image.png



  • 客户端发送一个FIN(seq=u)数据包到服务器,用来关闭客户端到服务器的数据连接

  • 服务器接受FIN数据包,发送ACK(seq=u+1)数据包到客户端

  • 服务器关闭与客户端的连接并发送一个FIN(seq=w)数据包到客户端,请求关闭连接

  • 客户端发送ACK(seq=w+1)数据包到服务器,服务器在收到ACK数据包后进行CLOSE状态,客户端在一定时间没有收到服务器的回复证明其关闭后,也进入关闭状态

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

VUE3基础学习(一)环境搭建与简单上手

VUE是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型。 个人学习感受:构建模板,通过数据就可以生成展示的html,上手简单,快速。 引用:VUE官网 ...
继续阅读 »

VUE是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型。


个人学习感受:构建模板,通过数据就可以生成展示的html,上手简单,快速。


引用:VUE官网



  • 声明式渲染:Vue 基于标准 HTML 拓展了一套模板语法,使得我们可以声明式地描述最终输出的 HTML 和 JavaScript 状态之间的关系。

  • 响应性:Vue 会自动跟踪 JavaScript 状态并在其发生变化时响应式地更新 DOM。


开始:


1.安装环境


1.node.js (已安装 16.0 或更高版本的 Node.js)


说明:Node.js是一种基于Chrome V8引擎的JavaScript运行环境,是一个可以在服务器端运行JavaScript的开源工具。
NodeJS已经集成了npm,所以npm也一并安装好了。
验证测试: node -v npm -v


1688628179293(1).png


2.cnpm


说明 :由于npm的服务器在海外,所以访问速度比较慢,访问不稳定 ,cnpm的服务器是由淘宝团队提供 服务器在国内cnpm是npm镜像。但是一般cnpm只用于安装时候,所以在项目创建与卸载等相关操作时候我们还是使用npm。


全局安装cnpm


npm install -g cnpm --registry=https://registry.npm.taobao.org


验证测试: cnpm -v


1688628363694(1).png


1.IDE与简单上手


1.IDE:webstorm


说明:我个人一直在用jetbrains 旗下的各种 IDA ,我使用起来比较熟练。


配置IDE:


f3a5978c8419300747f83d9ce163328.png


fc016670459439a8f9bdbb6448d5936.png


80a50b2d56df3c9a4a9c6941dd60d89.png


baa93f92dd8b8e24326e4d17888d9b7.png


2.简单上手:


到需要创建项目的文件目录


npm init vue@latest


这一指令将会安装并执行 create-vue,它是 Vue 官方的项目脚手架工具。你将会看到一些诸如 TypeScript 和测试支持之类的可选功能提示:


1688629335156.png


如果不确定是否要开启某个功能,你可以直接按下回车键选择 No。在项目被创建后,通过以下步骤安装依赖并启动开发服务器:


之后执行:


npm install


1688629446854(1).png


执行完安装后执行启动:


npm run dev


1688629550944(1).png


执行成功: 打开网页 http://localhost:5173/


1688629581157.png


当你准备将应用发布到生产环境时,请运行:


npm run build


此命令会在 ./dist 文件夹中为你的应用创建一个生产环境的构建版本。


简单例子 与 说明:


<script setup>
defineProps({
msg: {
type: String,
required: true
}
})
</script>

<!--模板区域-->
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3> You’ve successfully created a project with <a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> + <a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
</h3>
</div>
</template>
<!--样式区域-->
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
position: relative;
top: -10px;
}
</style>

作者:王二蛋与他的张大花
链接:https://juejin.cn/post/7252537738276208699
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Vue 为什么要禁用 undefined?

vue
Halo Word!大家好,我是大家的林语冰(挨踢版)~ 今天我们来伪科普一下——Vue 等开源项目为什么要禁用/限用 undefined? 敏感话题 我们会讨论几个敏感话题,包括但不限于—— 测不准的 undefined 如何引发复合 BUG? 薛定谔的...
继续阅读 »

Halo Word!大家好,我是大家的林语冰(挨踢版)~


今天我们来伪科普一下——Vue 等开源项目为什么要禁用/限用 undefined




敏感话题


我们会讨论几个敏感话题,包括但不限于——



  1. 测不准的 undefined 如何引发复合 BUG?

  2. 薛定谔的 undefined 如何造成二义性?

  3. 未定义的 undefined 为何语义不明?


懂得都懂,不懂关注,日后再说~




1. 测不准的 undefined 如何引发复合 BUG?


一般而言,开源项目对 undefined 的使用有两种保守方案:



  • 禁欲系——能且仅能节制地使用 undefined

  • 绝育系——禁用 undefined


举个粒子,Vue 源码就选择用魔法打败魔法——安排黑科技 void 0 重构 undefined


vue-void.png


事实上,直接使用 undefined 也问题不大,毕竟 undefined 表面上还是比较有安全感的。


readonly-desc.gif


猫眼可见,undefined 是一个鲁棒只读的属性,表面上相当靠谱。


虽然 undefined 自己问题不大,但最大的问题在于使用不慎可能会出 BUG。undefined 到底可能整出什么幺蛾子呢?


你知道的,不同于 null 字面量,undefined 并不恒等于 undefined 原始值,比如说祂可以被“作用域链截胡”。


举个粒子,当 undefined 变身成为 bilibili,同事的内心是崩溃的。


bilibili.png


猫眼可见,写做 undefined 变量,读做 'bilbili' 字符串,这样的代码十分反人类。


这里稍微有点违和感。机智如你可能会灵魂拷问,我们前面不是已经证明了 undefined 是不可赋值的只读属性吗?怎么祂喵地一言不合说变就变,又可以赋值了呢?来骗,来偷袭,不讲码德!


这种灵异现象主要跟变量查找的作用域链机制有关。读写变量会遵循“就近原则”优先匹配,先找到谁就匹配谁,就跟同城约会一样,和樱花妹异地恋的优先级肯定不会太高,所以当前局部作用域的优先级高于全局作用域,于是乎 JS 会优先使用当前非全局同名变量 undefined


换而言之,局部的同名变量 undefined 屏蔽(shadow,AKA“遮蔽”)了全局变量 globalThis.undefined


关于作用域链这种“远亲不如近邻”的机制,吾愿赐名为“作用域链截胡”。倘若你不会搓麻将,你也可以命名为“作用域链抢断”。倘若你不会打篮球,那就叫“作用域链拦截”吧。


globalThis.undefined 确实是只读属性。虽然但是,你们重写非全局的 undefined,跟我 globalThis.undefined 有什么关系?


周树人.gif


我们总以为 undefined 短小精悍,但其实 globalThis.undefined 才能扬长避短。


当我们重新定义了 undefinedundefined 就名不副实——名为 undefined,值为任意值。这可能会在团队协作中引发复合 BUG。


所谓“复合 BUG”指的是,单独的代码可以正常工作,但是多人代码集成就出现问题。


举个粒子,常见的复合 BUG 包括但不限于:



  • 命名冲突,比如说 Vue2 的 Mixin 就有这个瑕疵,所以 Vue3 就引入更加灵活的组合式 API

  • 作用域污染,ESM 模块之前也有全局作用域污染的老毛病,所以社区有 CJS 等模块化的轮子,也有 IIFE 等最佳实践

  • 团队协作,Git 等代码版本管理工具的开发冲突


举个粒子,undefined 也可能造成类似的问题。


complex-bug.png


猫眼可见,双方的代码都问题不大,但放在一起就像水遇见钠一般干柴烈火瞬间爆炸。


这里分享一个小众的冷知识,这样的代码被称为“Jenga Code”(积木代码)。


Jenga 是一种派对益智积木玩具,它的规则是,先把那些小木条堆成一个规则的塔,玩家轮流从下面抽出一块来放在最上面,谁放上之后木塔垮掉了,谁就 GG 了。


jenga.gif


积木代码指的是一点点的代码带来了亿点点的 BUG,一行代码搞崩整个项目,码农一句,可怜焦土。


换而言之,这样的代码对于 JS 运行时是“程序正义”的,对于开发者却并非“结果正义”,违和感拉满,可读性和可为维护性十分“赶人”,同事读完欲哭无泪。


所谓“程序正义”指的是——JS 运行时没有“阳”,不会抛出异常,直接挂掉,浏览器承认你的代码 Bug free,问题不大。


祂敢报错吗?祂不敢。虽然但是,无症状感染也是感染。你敢这么写吗?你不敢。除非忍不住,或者想跑路。


举个粒子,“离离原上谱”的“饭圈倒牛奶”事件——



  • 有人鞠躬尽瘁粮食安全

  • 有人精神饥荒疯狂倒奶


这种行为未必违法,但是背德,每次看到只能无视,毕竟语冰有“傻叉恐惧症”。


“程序正义”不代表“结果正义”,代码能 run 不代表符合“甲方肝虚”,不讲码德可能造成业务上的技术负债,将来要重构优化来还债。所谓“前猫拉屎,后人铲屎”大抵也是如此。


综上所述,要警惕测不准的 undefined 在团队开发中造成复合 BUG。




2. 薛定谔的 undefined 如何造成二义性?


除了复合 BUG,undefined 还可能让代码产生二义性。


代码二义性指的是,同一行代码,可能有不同的语义。


举个粒子,JS 的一些代码解读就可能有歧义。


mistake.png


undefined 也可能造成代码二义性,除了上文的变量名不副实之外,还很可能产生精神分裂的割裂感。


举个粒子,代码中存在两个一龙一猪的 undefined


default.png


猫眼可见,undefined 的值并不相同,我只觉得祂们双标。


undefined 变量之所以是 'bilibili' 字符串,是因为作用域链就近屏蔽,cat 变量之所以是 undefined 原始值,是因为已声明未赋值的变量默认使用 undefined 原始值作为缺省值,所以没有使用局部的 undefined 变量。


倘若上述二义性强度还不够,那我们还可以写出可读性更加逆天的代码。


destruct.png


猫眼可见,undefined 有没有精神分裂我不知道,但我快精神分裂了。


代码二义性还可能与代码的执行环境有关,譬如说一猫一样的代码,在不同的运行时,可能有一龙一猪的结果。


strict-mode.png


猫眼可见,我写你猜,谁都不爱。


大家大约会理直气壮地反驳,我们必不可能写出这样不当人的代码,var 是不可能 var 的,这辈子都不可能 var


问题在于,墨菲定律告诉我们,只要可能有 BUG,就有可能有 BUG。说不定你的猪队友下一秒就给你来个神助攻,毕竟不是每个人都像你如此好学,既关注了我,还给我打 call。


语冰以前也不相信倒牛奶这么“离离原上谱”的事件,但是写做“impossible”,读做“I M possible”。


事实上,大多数教程一般不会刻意教你去写错误的代码,这其实恰恰剥夺了我们犯错的权利。不犯错我们就不会去探究为什么,而对知识点的掌握只停留在表面是什么,很多人知错就改,下次还敢就是因为缺少了试错的成就感和多巴胺,不知道 BUG 的 G 点在哪里,没有形成稳固的情绪记忆。


请相信我,永远写正确的代码本身就是一件不正确的事情,你会看到这期内容就是因为语冰被坑了气不过,才给祂载入日记。


语冰很喜欢的一部神作《七龙珠》里的赛亚人,每次从濒死体验中绝处逢生战斗力就会增量更新,这个设定其实蛮科学的,譬如说我们身边一些“量变到质变”的粒子,包括但不限于:



  • 骨折之后骨头更加坚硬了

  • 健身也是肌肉轻度撕裂后增生

  • 记忆也是不断复习巩固


语冰并不是让大家在物理层面去骨折,而是鼓励大家从 BUG 中学习。私以为大神从来不是没有 BUG,而是 fix 了足够多的 BUG。正如爱迪生所说,我没有失败 999 次,而是成功了 999 次,我成功证明了那些方法完全达咩。


综上所述,undefined 的二义性在于可能产生局部的副作用,一猫一样的代码在不同运行时也可以有一龙一猪的结果,最终导致一千个麻瓜眼中有一千个哈利波特,读码人集体精神分裂。




3. 未定义的 undefined 为何语义不明?


除了可维护性感人的复合 BUG 和可读性感人的代码二义性,undefined 自身的语义也很难把握。


举个粒子,因为太麻烦就全写 undefined 了。


init.png


猫眼可见,原则上允许我们可以无脑地使用 undefined 初始化任何变量,万物皆可 undefined


虽然但是,绝对的光明等于绝对的黑暗,绝对的权力导致绝对的腐败。undefined 的无能恰恰在于祂无所不能,语冰有幸百度了一本书叫《选择的悖论》,这大约也是 undefined 的悖论。


代码是写给人看的,代码的信息越具体明确越好,偏偏 undefined 既模糊又抽象。你知道的,我们接触的大多数资料会告诉我们 undefined 的意义是“未定义/无值”。


虽然但是,准确而无用的观念,终究还是无用的。undefined 的正确打开方式就是无为,使用 undefined 的最佳方式是不使用祂。




免责声明



本文示例代码默认均为 ESM(ECMAScript Module)筑基测评,因为现代化前端开发相对推荐集成 ESM,其他开发环境下的示例会额外注释说明,edge cases 的解释权归大家所有。



今天的《ES6 混合理论》合集就讲到这里啦,我们将在本合集中深度学习若干奇奇怪怪的前端面试题/冷知识,感兴趣的前端爱好者可以关注订阅,也欢迎大家自由言论和留言许愿,共享 BUG,共同内卷。


吾乃前端的虔信徒,传播 BUG 的福音。


我是大家的林语冰,我们一期一会,不散不见,掰掰~


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

老菜鸟为什么喜欢代码重构

“花有重开日,人无再少年”,诸位,如果再给你们一次机会还会选择做个码农吗? “今年花胜去年红,可惜明年花更好”,诸位,对码农生涯还有多少期盼? “君看今日树头花,不是去年枝上朵”,诸位,可曾想过:如果在几年前便拥有现在的技术、薪资,面对生活、谈女朋友会不会是另...
继续阅读 »

“花有重开日,人无再少年”,诸位,如果再给你们一次机会还会选择做个码农吗?


“今年花胜去年红,可惜明年花更好”,诸位,对码农生涯还有多少期盼?


“君看今日树头花,不是去年枝上朵”,诸位,可曾想过:如果在几年前便拥有现在的技术、薪资,面对生活、谈女朋友会不会是另外一番滋味呢...


好了!扯多了,还是谈一谈正题:老菜鸟为什么喜欢代码重构!


屎山上挖呀挖


最近几个月流行的"花园种花"跟大家分享一下:


222213.jpg

小朋友们准备开始
在小小的花园里面 ~ 挖呀挖呀挖 ~ 种小小的种子 ~ 开小小的花
在大大的花园里面 ~ 挖呀挖呀挖 ~ 种大大的种子 ~ 开大大的花
在特别大的花园里面 ~ 挖呀挖呀挖 ~ 种特别大的种子 ~ 开特别大的花
在什么样的花园里面 ~ 挖呀挖呀挖 ~ 种什么样的种子 ~ 开什么样的花
在特别大的花园里面 ~ 挖呀挖呀挖 ~ 种特别大的种子 ~ 开特别大的花


每天对着自己项目中的代码,真的想改编一下:

在小小的屎山里面   ~ 挖呀挖呀挖 ~ 
在大大的屎山里面 ~ 挖呀挖呀挖 ~
在特别大的屎山里面 ~ 挖呀挖呀挖 ~
在特别大的屎山里面 ~ 挖呀挖呀挖 ~

没被铲走的屎山就是好屎山



诸位,想不想一睹屎山真容?----去你自己项目上找吧,哈哈; 重构并不一定是因为屎山,屎山可能不一定需要重构





  1. 于公司、项目而言,能正常运行、满足客户需要的代码就是OK的,甭管它屎山还是花园,能帮公司赚到钱的就是王道也!




  2. 为什么会有屎山的存在,源于项目早期的设计、架构不合理、中后期编码不规范、或者说压根就没有架构、没有规范;




  3. 很多大厂或一些比较讲究的公司,有自己的架构师、质量团队,那他们产出的项目质量就会非常高,就算是重构也可能是跨代、技术层面的升级,绝非屎山引起的重构!




  4. 爷爷都是从孙子过来的,谁还没个小菜成长记呢,谁小时候(初级阶段)还没(写过一些垃圾代码)在裤子上拉过屎呢;能接受别人的喷说明你心智成熟了,能发现项目中的糟糕代码说明你技术成长了;




  5. 最重要的一点,用发展的眼光看问题,以当下的技术、潮流去审视多年前的项目,还是要充满敬意,而不只是吐槽!




优化


你需要可能是优化


重构并不一定是因为屎山,可能是技术自身的转换升级; 屎山可能不一定要重构,或许只需要做代码的优化; 倘若项目的技术栈还比较年轻,那我们面对的可能是优化,而不是重构;



那糟糕的代码源自何处呢?


  • 曾经的你? 曾经梦想仗剑走天涯,如今大肚秃顶撸代码;

  • 已经离职的同事?人走茶凉,雁过拔毛;


话说谁还不是从小菜成长起来的呢,当你发现项目上诸多不合理时,说明你的技术已经成长、提高了不少;



  • 可能你接手了一个旧的项目,看到的代码是公司几年前的产品设计

  • 可能你自己今年前写的代码,因为项目赚钱了,又要升级

  • 可能你接手了(不太讲究的)同事的代码

  • 可能就是因为赶进度,只要功能实现


如果仅仅是代码不够友好,我们需要的或许只是长期优化了...


网友谈重构



当你看到眼前的屎山会作何感想? TMD,怎么会会会有如此的代码呢,某某某真**,ε=(´ο`*)))唉? 正常,如果你没有这样的感慨,下文就不用看了,直接吐槽就行了...



看看网友回复



  • 看心情




  • 别自己找trouble




  • 又不是不能用,你领导怕成本太高吧,新项目可以用新架构




  • 除非你自己愿意花时间去重构,不然哪个老板舍得花这个钱和时间




  • 应该选择成为领导,让底下人996用新技术重构
    .... 下面的更绝




  • 小伙子还年轻吧,动不动就重构




  • 代码和你 有一个能跑就行
    哈哈,太多了,诸位,你会怎么想,面对糟糕的代码你会重构吗?




cg3.jpg


问题



当你发现问题时说明你用心了;当你吐槽槽糕代码时,说明你技术提升了;当你想爆粗口时说明你对美好生活是充满向往的;



那么,你的项目上可能有哪些问题呢(以前端代码为例)?



  • 技术栈过于古老

  • 架构设计不合理

  • 技术选型、规范不合理

  • 不合理的三目运算符

  • 过多的if嵌套

  • 回调地狱

  • 冗余的方法、函数

  • 全局变量混乱

  • 项目结构混乱

  • 路由管理糟糕

  • 状态数据管理混乱

  • CSS样式样式混乱

  • .....
    .... 哪些该重构,哪些该优化?


sikao6.jpg


机会



“祸兮福之所倚,福兮祸之所伏”, 问题的背后往往就是机会;



路人甲: 明明就是一座屎山,又何来机会一说?有扒拉屎山的功夫,搞点新技能,搞点原创不开心吗?答案是肯定的


路人乙: 解决屎 OR 新项目搭建,会选择哪个呢? 我想脑子正常点的人应该会选择重新搭建吧!


个人觉得对于经验比较丰富的开发当然选容易的,对于经验不丰富的开发而言当然也选容易的!对于有一定基础 && 想要快速提升综合能力者,解决屎山或许别有一番滋味,未尝不是一件闻起来臭吃起来香的(臭豆腐)幸事;



  • 理解老旧项目的初始架构、设计有助于了解、理解技术发展的脉络

  • 有很多老旧项目的设计、架构是非常优秀的,值得去深入学习背后的思想

  • 重构的整个过程也是不断审视自己不足的过程,查漏补缺,提升最短的那块板

  • 一切技术服务于业务,重构的过程也是深入理解业务的过程

  • 有机会重构是一种幸福,面对诸多压力本身就是是一种心智的磨练,不经历风云怎能见彩虹; 大成功者必是大磨难者!


挑战



问题的背后是机会, 机会的身边往往伴随着诸多挑战;面对重构不去争取,那老油条和小菜鸟有有什么区别(什么价值),不要等到退出行业了,再说: “曾经有一次重构的机会摆在我面前,我没有珍惜,等到了失去的时候才后悔莫及,尘世间最痛苦的事莫过于此。如果老板可以再给我一个再来一次的机会的话,我会说:重构!重构! 重构!。如果一定要加一个期限的话,我希望是公司设定的时间之前!”



QQ截图20230628143530.jpg



  • 时间:领导、公司会不会给予足够的时间深入重构,万一没有如期完成呢?

  • 回报:重构是否能达到预期,是否能带来质变级的体验,万一重构后没成功呢?

  • 能力:自身能力是否能承担得起重构的重任,是否具备一定的抗压能力

  • 博弈:重构也是一种资源的博弈,考虑如何让自己的利益最大化的


重构




  • 重构的机会应该是去争取的,而不是被赋予的




  • 生活中处处有压力,又总是压得人喘不过气,如果连个代码重构都不敢想、不敢搞,那生活中的种种不如意又当如何?正如那句话:“做人如果没有梦想,和咸鱼有什么区别?”




QQ截图20230628143731.jpg


收获


其实,个人感觉收获最大的还是心智的磨练; 其次是技术的提升;


结语


生活已经够累了,跟大家闲扯一下,放松!放松!放松! 最重要的天热了要多喝水,多吃蔬菜和水果,适度的体育活动!欢迎大家说说自己的看法


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

为了摸鱼,我开发了一个工具网站

       大家好,我是派大星,由于前段时间实习入职,所以把时间以及精力都放在熟悉公司业务以及从工作中提升自己的业务逻辑,空余时间也是放在了学习新技术上,到目前为止也是参与了公司3个项目的开发团队中,参与过程中犯过错,...
继续阅读 »

       大家好,我是派大星,由于前段时间实习入职,所以把时间以及精力都放在熟悉公司业务以及从工作中提升自己的业务逻辑,空余时间也是放在了学习新技术上,到目前为止也是参与了公司3个项目的开发团队中,参与过程中犯过错,暴露出了很多的不足,丧失过信心,学生时期所带的傲气也是被一点一点的慢慢的打磨掉,正是因为这些,带给我的成长是巨大的。好了,闲言少叙,下面让我们进入今天的主题。


创作背景


       因为”懒“得走路,所以发明了汽车飞机,因为”懒“得干苦力活,所以发明了机器帮助我们做,很早之前看到过这个梗,而我这次同样也是因为”懒“,所以才开发出了这个工具。这里先卖个关子,先不说这个工具的作用,容我向大家吐槽一下这一段苦逼的经历。在我实习刚入职不久,就迎来了自己第一个任务,由于自己对所参与的项目的业务并不太了解,所以只能先做一些类似测试的工作,比如就像这次,组长给了我一份Json 文件,当我打开文件后看到数据都是一些地区名称,但当我随手的将滚动条往下一拉,瞬间发现不对劲,因为这个小小的文件行数竟然达到了1w+❗❗❗❗


在这里插入图片描述


不禁让我脊背发凉,但是这时我的担心还没达到最坏的地步,毕竟我还对具体的任务不了解。但当组长介绍任务内容,主要是让我将这些数据添加到数据库对应的表中,由于没有sql脚本,只有这个json 文件,需要手动去操作,而且给我定的任务周期是两天。听到这个时间时内心的慌张瞬间消失了,因为在之前我就了解过Navicat支持Json格式的文件直接导入数据,一个这么简单的任务给我两天时间,这不是非要让我带薪学习。


猥琐流口水表情包_表情包大全_尼凸图片网_一个图片、头像、表情包分享的网站


当我接下任务自信打开Navicat的导入功能时发现了一个重要问题,虽然它支持字段映射,但是给的Json数据是省市区地址名称,里面包含着各种嵌套,说实话到想到这里我已经慌了,而且也测试了一下字段只能单个的批量导入,而且不支持嵌套的类型,突然就明白为什么 给我两天的时间。在这里插入图片描述这时候心里只能默默祈祷已经有大神开发出了能处理这种数据的工具网站,但是经过一个小时的艰苦奋斗,但最终依旧是没有结果,网上有很多JsonSQl的工具网站,但是很多都支持简单支持一下生成创建表结构的语句,当场心如死灰,跑路的心都有了。但最终还是咬着牙 手动初始化数据,其过程中的“趣味” 实属无法用语言表达……


        上述就是这个工具的开发背景,也是怕以后再给我分配这么“有趣” 的任务。那么下面就给大家分享一下我自制的 Json转译SQL 工具,而且它也是一个完全免费的工具网站,同时这次也是将项目进行了 开源分享,大家也可以自己用现成的代码完成本地部署测试,感兴趣的同学可以自行拉取代码!



开源地址:github.com/pdxjie/sql-…



项目简介


Sql-Translation (简称ST)是一个 Json转译SQL 工具,在同类工具的基础上增强了功能,为节省时间、提高工作效率而生。并且遵循 “轻页面、重逻辑” 的原则,由极简页面来处理复杂任务,且它不仅仅是一个项目,而是以“降低时间成本、提高效率”为目标的执行工具。
在这里插入图片描述


技术选型


前端:



  • Vue

  • AntDesignUI组件库

  • MonacoEditor 编辑器

  • sql-formatter SQL格式化


后端:



  • SpringBoot

  • FastJson


项目特点



  • 内置主键JSON块如果包含id字段,在选择建表操作模式时内部会自动为id设置primary key

  • 支持JSON数据生成建表语句:按照内置语法编写JSON,支持生成创建表的SQL语句

  • 支持JSON数据生成更新语句:按照内置语法编写JSON,支持生成创更新的SQL语句,可配置单条件、多条件更新操作

  • 支持JSON数据生成插入语句:按照内置语法编写JSON,支持生成创插入的SQL语句,如果JSON中包含 多层 (children)子嵌套,可按照相关语法指定作为父级id的字段

  • 内置操作语法:该工具在选取不同的操作模式时,内置特定的使用语法规范

  • 支持字段替换:需转译的JSON中字段与对应的SQL字段不一致时可以选择字段替换

  • 界面友好:支持在线编辑JSON代码,支持代码高亮、语法校验、代码格式化、查找和替换、代码块折叠等,体验良好


解决痛点


下面就让我来给大家介绍一下Sql-Translation 可以解决哪些痛点问题:




  • 需要将大量JSON中的数据导入到数据库中,但是JSON中包含大量父子嵌套关系 ——> 可以使用本站




  • 在进行JSON数据导入数据库时,遇到JSON字段与数据库字段不一致需要替换字段时 ——> 可以使用本站




  • 根据Apifox工具来实现更新或新增接口(前提是对接口已经完成了设计工作),提供了Body体数据,而且不想手动编写SQL时 ——> 可以使用本站




对上述三点进行进行举例说明(按照顺序):


第一种情况:

{
"id": "320500000",
"text": "苏州工业园区",
"value": "320500000",
"children": [
{
"id": "320505006",
"text": "斜塘街道",
"value": "320505006",
"children": []
},
{
"id": "320505007",
"text": "娄葑街道",
"value": "320505007",
"children": []
},
....
]
}

第二种情况:
在这里插入图片描述


第三种情况:
在这里插入图片描述


以上内容就是该工具的简单介绍,由于该工具内置了部分语法功能,想要了解本工具全部工具以及想要动手操作的的同学请点击前往操作文档 ,该操作文档中包含了具体的语法介绍以及每种转换的具体示例数据 提供测试使用。


地址传送门



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

大喊一声Fuck!代码就能跑了是什么体验?

大喊一声Fuck!代码就能跑了是什么体验? 1 前言 大家好,我是心锁,23届准毕业生。 程序员的世界,最多的不是代码,而是💩山和bug。 近期我在学习过程中,在github找到了这么一个项目,能在我们输错命令之后,大喊一声Fuck即可自动更正命令,据说喊得越...
继续阅读 »

大喊一声Fuck!代码就能跑了是什么体验?


1 前言


大家好,我是心锁,23届准毕业生。


程序员的世界,最多的不是代码,而是💩山和bug。


近期我在学习过程中,在github找到了这么一个项目,能在我们输错命令之后,大喊一声Fuck即可自动更正命令,据说喊得越大声效果越好。


c37237b03e45fed8c2828c6f7abb93b9


2 项目基本介绍


thefuck是一个基于Python编写的项目,它能够自动纠正你在命令行中输入的错误命令。如果你输错了一个命令,只需要在命令行中输入“fuck”,thefuck就会自动纠正你的错误。该项目支持众多的终端和操作系统,包括Linux、macOS和Windows。


43885f5e1f8c7ff2b3392d297c855609


2.1 环境要求



  • python环境(3.4+)


2.2 安装方式


thefuck支持brew安装,非常方便,在macOS和Linux上都可以通过brew安装。

brew install thefuck

也支持通过pip安装,便携性可以说是一流了。

pip3 install thefuck

2.3 配置环境变量


建议将下边的代码配置在环境变量中(.bash_profile.bashrc.zshrc),不要问为什么,问就是有经验。

eval $(thefuck --alias)
eval $(thefuck --alias FUCK)
eval $(thefuck --alias fuck?)
eval $(thefuck --alias fuck?)

接着运行source ~/.bashrc(或其他配置文件,如.zshrc)确认更改立即可用。


3 使用效果


Untitled


03cf7e926946b7d8a3da902841c3c5b1


4 thefuck的工作原理


thefuck的工作原理非常简单。当你输入一个错误的命令时,thefuck会根据你输入的命令和错误提示自动推测你想要输入的正确命令,并将其替换为正确的命令。thefuck能够自动推测正确的命令是因为它内置了大量的规则,这些规则能够帮助thefuck智能地纠正错误的命令。


所以,该项目开放了自定义规则。


4.1 创建自己的规则


如果thefuck内置的规则不能够满足你的需求,你也可以创建自己的规则。thefuck的规则是由普通的Python函数实现的。你可以在~/.config/thefuck/rules目录下创建一个Python脚本,然后在其中定义你的规则函数。


以创建一个名为my_rule的规则为例,具体步骤如下:


4.1.1 创建rule.py文件


~/.config/thefuck/rules目录下创建一个Python脚本,比如my_rules.py


4.1.2 遵循的规则


在自定义脚本中,必须实现以下两个函数,match显然是用来匹配命令是否吻合的函数,而get_new_command则会在match函数返回True时触发。

match(command: Command) -> bool
get_new_command(command: Command) -> str | list[str]

同时可以包含可选函数,side_effect的作用是开启一个副作用,即除了允许原本的命令外,你可以在side_effect做更多操作。

side_effect(old_command: Command, fixed_command: str) -> None

5 yarn_uninstall_to_remove


以创建一个名为yarn_uninstall_to_remove的规则为例,该规则会在我们错误使用yarn uninstall …命令时,自动帮助我们修正成yarn remove … 。具体步骤如下:


5.1 创建yarn_uninstall_to_move.py文件


~/.config/thefuck/rules目录下创建一个Python脚本,yarn_uninstall_to_remove.py


5.2 编写代码

from thefuck.utils import for_app

@for_app('yarn')
def match(command):
return 'uninstall' in command.script

def get_new_command(command):
return command.script.replace('uninstall', 'remove')

priority=1 # 优先级,数字越小优先级越高

5.3 效果


Untitled


6 总结


世界之大,无奇不有。不得不说的是,伴随着AI的逐渐发展,类似这种项目未来一定是优先接入AI者才可以继续发展。


友情提示,喊fuck的时候先设置后双击control打开听写功能,喊完再点击一下control完成输入。


Untitled


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

一次线上事故,我顿悟了异步的精髓

在高并发的场景下,异步是一个极其重要的优化方向。 前段时间,生产环境发生一次事故,笔者认为事故的场景非常具备典型性 。 写这篇文章,笔者想和大家深入探讨该场景的架构优化方案。希望大家读完之后,可以对异步有更深刻的理解。 1 业务场景 老师登录教研平台,会看到课...
继续阅读 »

在高并发的场景下,异步是一个极其重要的优化方向。


前段时间,生产环境发生一次事故,笔者认为事故的场景非常具备典型性


写这篇文章,笔者想和大家深入探讨该场景的架构优化方案。希望大家读完之后,可以对异步有更深刻的理解。


1 业务场景


老师登录教研平台,会看到课程列表,点击课程后,课程会以视频的形式展现出来。



访问课程详情页面,包含两个核心动作:




  1. 读取课程视频信息 :


    从缓存服务器 Redis 获取课程的视频信息 ,返回给前端,前端通过视频组件渲染。




  2. 写入课程观看行为记录 :


    当教师观看视频的过程中,浏览器每隔3秒发起请求,教研服务将观看行为记录插入到数据库表中。而且随着用户在线人数越多,写操作的频率也会指数级增长。




上线初期,这种设计运行还算良好,但随着在线用户的增多,系统响应越来越慢,大量线程阻塞在写入视频观看进度表上的 Dao 方法。上。


首先我们会想到一个非常直观的方案,提升写入数据库的能力



  1. 优化 SQL 语句;

  2. 提升 MySQL 数据库硬件配置 ;

  3. 分库分表。


这种方案其实也可以满足我们的需求,但是通过扩容硬件并不便宜,另外写操作可以允许适当延迟和丢失少量数据,那这种方案更显得性价比不足。


那么架构优化的方向应该是: “减少写动作的耗时,提升写动作的并发度” , 只有这样才能让系统更顺畅的运行。


于是,我们想到了第二种方案:写请求异步化



  • 线程池模式

  • 本地内存 + 定时任务

  • MQ 模式

  • Agent 服务 + MQ 模式


2 线程池模式


2014年,笔者在艺龙旅行网负责红包系统相关工作。运营系统会调用红包系统给特定用户发送红包,当这些用户登录 app 后,app 端会调用红包系统的激活红包接口 。


激活红包接口是一个写操作,速度也比较快(20毫秒左右),接口的日请求量在2000万左右。


应用访问高峰期,红包系统会变得不稳定,激活接口经常超时,笔者为了快速解决问题,采取了一个非常粗糙的方案:


"控制器收到请求后,将写操作放入到独立的线程池中后,立即返回给前端,而线程池会异步执行激活红包方法"。


坦率的讲,这是一个非常有效的方案,优化后,红包系统非常稳定。


回到教研的场景,见下图,我们也可以设计类似线程池模型的方案:



使用线程池模式,需要注意如下几点:



  1. 线程数不宜过高,避免占用过多的数据库连接池 ;

  2. 需要考虑评估线程池队列的大小,以免出现内存溢出的问题。


3 本地内存 + 定时任务


开源中国统计浏览数的方案非常经典。


用户访问过一次文章、新闻、代码详情页面,访问次数字段加 1 , 在 oschina 上这个操作是异步的,访问的时候只是将数据在内存中保存,每隔固定时间将这些数据写入数据库。



示例代码如下:



我们可以借鉴开源中国的方案 :



  1. 控制器接收请求后,观看进度信息存储到本地内存 LinkedBlockingQueue 对象里;

  2. 异步线程每隔1分钟从队列里获取数据 ,组装成 List 对象,最后调用 Jdbc batchUpdate 方法批量写入数据库;

  3. 批量写入主要是为了提升系统的整体吞吐量,每次批量写入的 List 大小也不宜过大 。


这种方案优点是:不改动原有业务架构,简单易用,性能也高。该方案同样需要考虑内存溢出的风险。


4 MQ 模式


很多同学们会想到 MQ 模式 ,消息队列最核心的功能是异步解耦,MQ 模式架构清晰,易于扩展。



核心流程如下:



  1. 控制器接收写请求,将观看视频行为记录转换成消息 ;

  2. 教研服务发送消息到 MQ ,将写操作成功信息返回给前端 ;

  3. 消费者服务从 MQ 中获取消息 ,批量操作数据库 。


这种方案优点是:



  1. MQ 本身支持高可用和异步,发送消息效率高 , 也支持批量消费;

  2. 消息在 MQ 服务端会持久化,可靠性要比保存在本地内存高;


不过 MQ 模式需要引入新的组件,增加额外的复杂度。


5 Agent 服务 + MQ 模式


互联网大厂还有一种常见的异步的方案:Agent 服务 + MQ 模式。



教研服务器上部署 Agent 服务(独立的进程) , 教研服务接收写请求后,将请求按照固定的格式(比如 JSON )写入到本次磁盘中,然后给前端返回成功信息。


Agent 服务会监听文件变动,将文件内容发送到消息队列 , 消费者服务获取观看行为记录,将其存储到 MySQL 数据库中。


还有一种演进,假设我们不想在应用中依赖消息队列,不生成本地文件,可以采用如下的方式:



这种方案最大的优点是:架构分层清晰,业务服务不需要引入 MQ 组件。


笔者原来接触过的性能监控平台,或者日志分析平台都使用这种模式。


6 总结


学习需要一层一层递进的思考。


第一层:什么场景下需要异步



  • 大量写操作占用了过多的资源,影响了系统的正常运行;

  • 写操作异步后,不影响主流程,允许适当延迟;


第二层:异步的外功心法


本文提到了四种异步方式:



  • 线程池模式

  • 本地内存 + 定时任务

  • MQ 模式

  • Agent 服务 + MQ 模式


它们的共同特点是:将写操作命令存储在一个池子后,立刻响应给前端,减少写动作的耗时。任务服务异步从池子里获取任务后执行。


第三层:异步的本质


在笔者看来,异步是更细粒度的使用系统资源的一种方式


在教研课程详情场景里,数据库的资源是固定的,但写操作占据大量数据库资源,导致整个系统的阻塞,但写操作并不是最核心的业务流程,它不应该占用那么多的系统资源。


我们使用异步的解决方案时,无论是使用线程池,还是本地内存 + 定时任务 ,亦或是 MQ ,对数据库资源的使用都需要在合理的范围内,只有这样系统才能顺畅的运行。


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

在线代码编辑器介绍与选型

web
引言 作为数据生产和管理的平台,数据平台的一大核心功能是在线数据开发,工欲善其事必先利其器,所以平台具备一个功能较为丰富、用户体验友好的在线代码编辑器,就成为了前提条件。 经历最近一两年的代码编辑器方案调研、选型和开发,我们对内部平台使用的代码编辑器进行了统一...
继续阅读 »

引言


作为数据生产和管理的平台,数据平台的一大核心功能是在线数据开发,工欲善其事必先利其器,所以平台具备一个功能较为丰富、用户体验友好的在线代码编辑器,就成为了前提条件。


经历最近一两年的代码编辑器方案调研、选型和开发,我们对内部平台使用的代码编辑器进行了统一和升级,并根据用户需求和业务场景进行了插件化定制,其底层是使用了 Monaco Editor 来进行二次开发。


本文主要是结合自己的理解,对代码编辑器相关知识进行整理,跟大家分享。


1. 在线代码编辑器是什么?


1.1 介绍


在线代码编辑器是一种基于 Web 技术开发的代码文本编辑器,可以在 Web 浏览器中直接使用。它通常包括用户界面模块、文本处理模块、插件扩展模块等模块;用户可以通过 Web 编辑器创建、编辑各种类型的文本文件,例如 HTML、CSS、JavaScript、Markdown 等。


1.2 分类


我们先来看看编辑器的分类:


类型描述典型产品优势劣势
远古编辑器textarea 或contentEditable+execCommand早期轻型编辑器(《100行代码带你实现一个编辑器》系列)门槛低,短时间内快速研发无法定制
contentEditable+文档模型借助contentEditable,各种拦截用户操作draftjs (react)、quilljs (vue)、prosemirror(util)站在浏览器的肩膀上,可以实现绝大多数的业内需求无法突破浏览器本身的限制(排版)
独立开发脱离浏览器自带编辑能力,独立做光标和排版引擎Google Docs、WPS等所有内容都把握在自己手上,排版随意个性化技术难度较高,研发成本较大

第一类编辑器,其劣势明显:由于重度依赖浏览器 execCommand 接口,而该接口支持的能力非常有限,故大多数功能无法订制,比如 fontSize 只能设置 1 - 7。另外兼容性也是一大问题,例如 Safari 并没有支持 heading 的设置。参考 MDN。而且该类编辑器基本都会直接将 HTML 作为数据模型(Model)来使用,这样会引发另外一个问题:相同的UI,可能对应了不同的DOM结构。举个例子,对于“加粗字体”这个用户输入,在 chrome 上,是添加了<blod>标签,ie11上则是添加了<strong>标签。


第二类编辑器与上一类编辑器最大的不同是定义了自己的 Model 层,所有视图(View)都与 Model 一一对应,并且一切 View 的变化都将由 Model 层的变化引发。为了做到这一点,需要拦截一切用户操作,准确识别用户意图,再对 Model 层进行正确的修改。坑点主要来自于对用户操作的拦截以及浏览器实现层面上的一些疑难杂症。故该类编辑器实现中的 hack 代码会非常多,理解起来比较困难。


第三类编辑器,采用隐藏textarea方案,它只负责接收输入事件,其他视图输出全靠自己,相对来说,更容易解耦。因为基本脱离了浏览器原生的光标,这块可以实现出更强大的功能。排版引擎可以自己搞,只要码力够强,想搞一个从从上往下从右往左的富文本编辑器也没问题,也带来了各种各样的可能,比如可以通过将 View 层用 canvas 实现,以规避很多兼容性问题。


2. 一款优秀的在线代码编辑器需要有哪些功能?


下面我们来看一下一个可用于生产环境的在线代码编辑器需要有哪些能力和模块:



2.1 核心模块


模块名模块描述
文本编辑用于处理用户输入的文本内容,管理文本状态,还包括实现文本的插入、删除、替换、撤销、重做等操作
语言实现语言高亮、代码分析、代码补全、代码提示&校验等能力
主题主要用于实现主题的管理、注册、切换、等功能
渲染主要完成编辑器的整体设计与生命周期管理
命令 & 快捷键管理注册和编辑的各种命令,比如查找文件、撤销、复制&粘贴等,同时也支持将命令以快捷键的形式暴露给用户
通信 & 数据流管理编辑器各模块之前的通信,以及数据存储、流转过程

2.2 扩展模块


模块名模块描述
文本能力扩展在现有处理文本的基础上进行功能扩展,比如修改获取文本方式。
语言扩展包括自定义新语言,扩展现有语言的关键字,完善代码解析、提示&校验等能力。
主题扩展包括自定义新主题,扩展现有主题的能力
命令扩展增加新命令,或者改写&扩展现有命令

3. 开源市场上有哪些代码编辑器?


目前开源市场使用较多的代码编辑器主要有 3 个,分别是 Monaco Editor(第三类)、Ace(第三类)和 Code Mirror(第二类)。本文也将带大家去了解他们的整体架构,做一些对比分析。


3.1 Monaco Editor


基本介绍:


类别描述
介绍是一个功能相对比较完整的代码编辑器,实现使用了 MVP 架构,采用了模块化和组件化的思想,其中编辑器核心代码部分是与 vscode 共用的,从源码目录中能看到有很多 browser 与 common 的目录区分。
仓库地址github.com/microsoft/v…
入口文件/editor/editor.main.ts
开始使用editor.create()方法来自 /editor/standalone/browser/standaloneEditor.ts

目录结构:


├── base        			# 通用工具/协议和UI库
│ ├── browser # 基础UI组件,DOM操作,事件
│ ├── common # diff计算、处理,markdown解析器,worker协议,各种工具函数
├── editor # 代码编辑器核心
| ├── browser # 在浏览器环境下的实现,包括了用于处理 DOM 事件、测量文本尺寸和位置、渲染文本等功能的代码。
| ├── common # 浏览器和 Node.js 环境下共用的代码,其中包括了文本模型、文本编辑操作、语法分析等功能的实现
| ├── contrib # 扩展模块,包含很多额外功能 查找&替换,代码片段,多光标编辑等等
| └── standalone # 实现了一个完整的编辑器界面,也是我们通常使用的完整编辑器
├── language # 前端需要的几种语言类型,与basic-languages不同的是,这里的实现语言功能更完整,包含关键字提示与语法校验等
├── basic-languages # 基础语言声明,里面只包含了关键字的罗列,主要用于关键字的高亮,不包含提示和语法校验

特点:



  • 多线程处理,主要分为 主线程 和 语言服务线程(使用了 Web Worker 技术 来模拟多线程,主要通过 postMessage 来进行消息传递)

    • 主线程:主要负责处理用户与编辑器的交互操作,以及渲染编辑器的 UI 界面,还负责管理编辑器的生命周期和资源,例如创建和销毁编辑器实例、加载和卸载语言服务、加载和卸载扩展等。

    • 语言服务线程:负责提供代码分析、语法检查等功能,以及处理与特定语言相关的操作。




DOM 结构:


<div class="monaco-editor" role="presentation">
<div class="overflow-guard" role="presentation">
<div class="monaco-scrollable-element editor-scrollable" role="presentation">
<!--实现行高亮-->
<div class="monaco-editor-background" role="presentation"></div>
<!--实现关键字背景高亮-->
<div class="view-overlays" role="presentation">
<div>...</div>
</div>
<!--每一行内容-->
<div class="view-lines" role="presentation">
<div>...</div>
</div>
<!--光标-->
<div class="monaco-cursor-layer" role="presentation"></div>
<!--文本输入框-->
<textarea class="monaco-editor-textarea"></textarea>
<!--横向滚动条-->
<div class="scrollbar horizontal"></div>
<!--纵向滚动条-->
<div class="scrollbar vertical"></div>
</div>
</div>
</div>


3.2 Code Mirror


基本介绍:


类别描述
介绍CodeMirror 6 是一款浏览器端代码编辑器,基于 TypeScript,该版本进行了完全的重写,核心思想是模块化和函数式,支持超过 14 种语言的语法高亮,亮点是高性能、可扩展性高以及支持移动端。
仓库地址github.com/codemirror
入口文件由于高度模块化,没有一个集成的入口文件,这里放上核心库@codemirror/view的入口文件:src/index.ts

开始使用


import { EditorState } from '@codemirror/state'; import { EditorView, keymap } from '@codemirror/view';
import { defaultKeymap } from '@codemirror/commands';
let startState = EditorState.create({
doc: 'console.log("hello, javascript!")',
extensions: [keymap.of(defaultKeymap)],
});
let view = new EditorView({
state: startState,
parent: document.body,
});

目录结构:


高度模块化(分为多个仓库),这里放上比较核心的库的分布和内部结构


核心模块:提供了编辑器视图(@codemirror/view)、编辑器状态(@codemirror/state)、基础命令(@codemirror/commands)等基础功能。


语言模块:提供了不同编程语言的语法高亮、自动补全、缩进等功能,例如@codemirror/lang-javascript@codemirror/lang-sql@codemirror/lang-python 等。


主题模块:提供了不同风格的编辑器主题,例如 @codemirror/theme-one-dark


扩展模块:提供了一些额外的编辑器功能,例如行号(@codemirror/gutter)、折叠(@codemirror/fold)、括号匹配(@codemirror/matchbrackets)等。


内部结构,以@codemirror/view为例:


├── src                         # 源文件夹
│ ├── editorview.ts # 编辑器视图层
│ ├── decoration.ts # 视图装饰
│ ├── cursor.ts # 光标的渲染
│ ├── domchange.ts # DOM 改变相关的逻辑
│ ├── domobserver.ts # 监听 DOM 的逻辑
│ ├── draw-selection.ts # 绘制选区
│ ├── placeholder.ts # placeholder的渲染
│ ├── ...
├── test # 测试用例
| ├── webtest-domchange.ts # 测试监听到 DOM 变化后的一系列处理。
| ├── ...

特点:


指导 CodeMirror 架构设计的核心观点是函数式代码(纯函数),它会创建一个没有副作用的新值,和命令式代码交互更方便。而浏览器 DOM 很明显也是命令式思维,和 CodeMirror 集成的大部分系统类似。


CodeMirror 6 的 state 表现层是严格函数式的 - 即 document 和 state 数据结构都是不可变的,而能操作它们的都是纯函数,view 包将它们封装在一个命令式接口中。


所以即使 editor 已经转到了新的 state,而旧的 state 依然原封不动的存在,保存旧状态和新状态在面对处理 state 改变的情况下极为有利,这也意味着直接改变一个 state 值,或者添加额外 state 属性的命令式扩展都是不建议的,后果也不太可控。


CodeMirror 处理状态更新的方式受 Redux 启发,除了极少数情况(如组合和拖拽处理),视图的状态完全是由 EditorState 里的 state 属性决定的。


通过创建一个描述改变document、selection 或其他 state 属性的 transaction,以这种函数调用方式来更新 state。这个 transaction 之后可以通过 dispatched 分发,告诉 view 更新 state,更新新 state 对应的 DOM 展示。


let transaction = view.state.update({ changes: { from: 0, insert: "0" }})
console.log(transaction.state.doc.toString()) // "0123"
// 此刻视图依然显示的旧状态
view.dispatch(transaction)
// 现在显示新状态了

典型的用户交互数据流如下图:



view 监听事件变化。当 DOM 事件发生时(或者快捷键触发的命令,或者由扩展注册的事件处理器),CodeMirror会把这些事件转换为新的状态 transcation,然后分发。此时生成一个新的 state,当接收到新 state 后就会去更新 DOM。


DOM 结构:


<div class="cm-editor [theme scope classes]">
<div class="cm-scroller">
<div class="cm-content" contenteditable="true">
<div class="cm-line">Content goes here</div>
<div class="cm-line">...</div>
</div>
</div>
</div>


cm-editor 为一个 editor view 实例(在 merge-view,也就是代码对比情况下,给做了一个合并,其实还是两个 editor view 合在一起)


cm-scroller 为编辑器主展示区,并且展示了滚动条


cm-tooltip-autocomplete 为展示一些独立的层,比如代码提示,代码补全等


cm-gutter 是行号


cm-content 是编辑器的内容区


cm-layer 是跟 content 平级的,主要负责自定义指针和选区的展示


view-port 为CodeMirror 的一个优化,只解析和渲染了这个可视区域内的 DOM


cm-line 是每一行的内容,里面就是真实的 DOM 了


line-decorator 是提供给插件使用,用来装饰每一行的


在这个架构下,每个 editor 比较独立,可以渲染多个



3.3 Ace


基本介绍:


类别描述
介绍基于 Web 技术的代码编辑器,可以在浏览器中运行,高性能,体积小,功能全是它的主要优点。支持了超过120种语言的语法高亮,超过20个不同风格的主题,与 Sublime,Vim 和 TextMate 等本地编辑器的功能和性能相匹配。
仓库地址github.com/ajaxorg/Ace
入口文件/src/Ace.js
开始使用Ace.edit()

目录结构:


Ace 的目录结构相对简单,按功能分成了一个个不同的 js 文件,我这里列举其中一部分,部分较为复杂的功能除了提供了入口 js 文件以外,还在对应同级建立了文件夹里面实现各种逻辑,这里列举了 layer (渲染层) 为例子。


src/
├── layer #渲染分层实现
├── cursor.js #鼠标滑入层
├── decorators.js #装饰层,例如波浪线
├── lines.js #行渲染层
├── text.js #文本内容层
├── ...
├── ... #其他功能,例如 keybord
├── Ace.js #入口文件
├── ...
├── autocomplete.js #定义了编辑器补全相关内容
├── clipboard.js #定义了pc移动端兼容的剪切板
├── config.js
├── document.js
├── edit_session.js #定义了 Session 对象
├── editor.js #定义了 editor 对象
├── editor_keybinding.js #键盘事件绑定
├── editor_mouse_handler.js
├── virtual_renderer.js #定义了渲染对象 Renderer,引用了 layer 中定义的个种类
├── ...
├── mode.js
├── search.js
├── selection.js
├── split.js
└── theme.js

特点:



  • 事件驱动

    • Ace 中提供了丰富的事件系统,以供使用者直接使用或者自定义,并且通过对事件的触发和响应来进行内部数据通信实现代码检查,数据更新等等



  • 多线程

    • Ace 编辑器将解析代码的任务交给 Web Worker 处理,以提高代码解析的速度并避免阻塞用户界面。在 Web Worke r中,Ace 使用 Acorn库来解析 JavaScript 代码,并将解析结果发送回主线程进行处理




DOM 结构:


<div class="ace-editor">

<textarea
class="ace_text-input"
wrap="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
>

</textarea>
<!-- 行号区域 -->
<div class="ace_gutter" aria-hidden="true">
<div
class="ace_layer ace_gutter-layer"
>

<div class="ace_gutter-cell" >1 <span></span></div>
</div>
</div>
<!-- 内容区域 -->
<div class="ace_scroller" >
<div class="ace_content">
<div class="ace_layer ace_print-margin-layer">
<div class="ace_print-margin" style="left: 580px; visibility: visible;"></div>
</div>
<div class="ace_layer ace_marker-layer">
<div class="ace_active-line"></div>
</div>
<div class="ace_layer ace_text-layer" >
<div class="ace_line" >
<span class="ace_keyword">select</span>
<span class="ace_keyword">from</span>
<span class="ace_string">'xxx'</span>
</div>
<div class="ace_line"></div>
</div>
<div class="ace_layer ace_marker-layer"></div>
<div class="ace_layer ace_cursor-layer ace_hidden-cursors">
<!-- 光标 -->
<div class="ace_cursor"></div>
</div>
</div>
</div>
<!-- 纵向滚动条 -->
<div class="ace_scrollbar ace_scrollbar-v">
<div class="ace_scrollbar-inner" >&nbsp;</div>
</div>
<!-- 横行滚动条 -->
<div class="ace_scrollbar ace_scrollbar-h">
<div class="ace_scrollbar-inner">&nbsp;</div>
</div>

</div>

4. 整体对比


4.1 功能完整度


类别Monaco EditorCode MirrorAce
代码主题内置 3 种,可扩展基于扩展来支持,现有官方 1 种内置 20+,可扩展
语言内置 70+, 可扩展基于扩展来支持,现有官方 16 种内置 110+,可扩展
代码提示/自动补全只支持 4 种语言,官方提供了自动补全的基础插件,可自行实现基于扩展来支持,官方提供了自动补全的基础插件只支持 4 种语言,官方提供了自动补全的基础插件,可自行实现
代码折叠
快捷键
多光标编辑
代码检查只支持 4 种语言,官方提供了自动补全的基础插件,可自行实现基于扩展来支持,官方提供了代码检查的基础插件只支持 4 种语言,官方提供了自动补全的基础插件,可自行实现
代码对比❌,需自己扩展
MiniMap❌,需自己扩展❌,需自己扩展
多文本管理❌,需自己扩展
多视图❌,需自己扩展
协同编辑可引入额外插件支持 github.com/convergence…架构支持
移动端支持

4.2 性能体验


类别Monaco EditorCode MirrorAce
核心包大小800KB 左右核心包 115 KB 左右(未压缩)200KB 左右(不同版本有轻微出入)
编辑器渲染 (无代码)400ms 左右仅核心包情况下,120ms 左右185 ms 左右(实际使用包)

5. 结论与展望


一年前我们因为Monaco Editor丰富的生态、迅猛的迭代速度、开箱即用的特性和 VSCode 同款编辑器背书等原因选择了基于它来进行二次开发和插件化定制(后续文章会对这些定制开发做分享)。但由于编辑器的使用场景日渐多样化,个性化,以及移动端的占比日渐增加,我们对 Monaco Editor 的底层支持也越来越感觉到不足和乏力。对于这些点,我们的计划是先使用CodeMirror 6来支持移动端的代码编辑,然后逐步实

作者:pdai0001525
来源:juejin.cn/post/7252589598152851517
现代码编辑器的自研。

收起阅读 »

人需借以虚名而成事

前言 自从我看冯唐老师推荐的《资治通鉴》之后,对历史有那么一丢丢的兴趣,直到在短视频里面刷到《百家讲坛》里面几位老师的讲解片段,以及跟自己经历结合,瞬间满满的共鸣,是啊时代一直在变化,而里面的为人处事、社会规则凡此种种却从来没有太大变化,这就是我们需要去学习...
继续阅读 »

bcf7ea99ded21c1a1ac9a1d85b59724c.jpeg


前言




自从我看冯唐老师推荐的《资治通鉴》之后,对历史有那么一丢丢的兴趣,直到在短视频里面刷到《百家讲坛》里面几位老师的讲解片段,以及跟自己经历结合,瞬间满满的共鸣,是啊时代一直在变化,而里面的为人处事、社会规则凡此种种却从来没有太大变化,这就是我们需要去学习历史的原因。


所以有了以下感慨。



<<百家讲坛>> 不愧是经典,整本历史歪歪斜斜写着两字:权谋,谋事、谋人,加上不确定因素,天时地利人和,最终变成命运。
人的一生就像书中一行字轻描淡写,绘成一笔~



人需借以虚名而成事




这里解释一下,人需要倚靠外部的这些名号、头衔从而能有比较大的作为。为什么这么讲呢,听我细细道来~


人生需要四行


这是王立群老师讲的,人生要想有点成就需要四行,首先你自己行,你自身本身够硬,能够做好事,其次是有人说你行,从古代的推荐机制,比如说优秀的人推荐其他优秀的人才对不对,李白的大名远扬,唐玄宗在公主引荐之下见了皇帝,第三个说你行的人要行,推荐你的那个人本身能力要强,不然没有说服力,第四是身体行,很多机会是熬出来的,比如司马懿伺候了3位君主,兢兢业业,最终才能完成自己的大业。


何为借以虚名?


1、刘备的例子,他在比较年长的时候还无法实现自己的基业,寄人篱下,他登场的时候是怎样的呢,名号叫刘皇叔,翻开长长的祖谱,八辈子打不到杆哈哈哈,不过因为当时被曹操挟持,所以需要有汉族帮忙自己,所以顺理成章的提拔上来。


2、项羽出生在一个贵族家庭。他的父亲项梁是楚国将领,曾经在楚汉战争中和刘邦一起作战。而他的母亲也是楚国的贵族,是楚王的女儿。


相比之下,刘邦的出身则比较低微。他的祖父是一个农民,父亲是一个小县官。他自己也曾经当过一个普通的县尉,但因为功绩不够而被罢免。


尽管项羽出身高贵,但他的性格比较豁达,喜欢豪情壮志和享乐人生,行事大胆不拘小节。刘邦则更加谨慎稳重,注重政治规划和长远发展。


3、王立群老师讲述过他自身的故事,小学的时候成绩优秀得到保送,后面名额被学校给了别人,自己去了私立的学校,半工半读,到了大学还是遇到了同样的遭遇,后面就到比较好的大学教书,但是有一天校长跟他讲他的成绩还是出身问题需要去另一所低一点的学校教书,他当时就哭了,命运比较坎坷的。


4、工作中,我们常常会听到,某位领导自我介绍某某大厂经历,还有某某大学毕业,还有当你投一些大厂的核心部门的时候,都会对你学历进行考核。


所以我们从上面3个例子来看,人生确实正如王立群老师讲的,需要四行,当你自己不行的时候,即使有机会也会最终流失掉;当没有推荐你的时候,也会埋没在茫茫人海中,并不是说是金子总会发光,现实是伯乐更加重要;当说你行的人没有能力的时候也不行,名牌大学、大厂经历也是这样,通过一个有名声的物体从而表示你也可以,也很厉害哈哈;最后身体是本钱,这个就不用过多的讲解了。


读历史的目的,从前人的故事、经历中,学习到点什么,化为自身的经验、阅历。


我发现现在很多人都喜欢出去玩,说的开阔视野,扩展见识,在我看来读历史才是真正的拓宽自己的眼界,原来几百年前也有人跟我一样遇到同样的问题,也有那些胸怀大志的三国名将郁郁不得志。


历史是一本书籍,它可以很薄,几句话、几行字就概括了一个朝代,它也可以很厚,厚到整个朝代的背景,比如气候情况、经济情况、外交情况、内政情况,各个出名的人物事迹,那岂是几页纸能讲得完的,是多么的厚重。


如果人不能借以虚名怎么做事?


修炼两件事情




同样百家讲坛也给出了一个比较好的见解,一个人想要成事,需要两个明,一个是高明、一是精明。高明是你有足够的眼光去发现,发现机会也好,发现危机也好,往往人生的成就需要这种高瞻远瞩的;另一个精明,是指在做事上打磨,细心主动,王阳明讲的知行合一,里面很关键的一点就是事上练,方法论在实战中去验证总结。


心态上练




李连杰在采访中说了这么一个耐人寻味的故事,他发现不管是有钱的人还是没有钱的人都会生气,比如一些老板在为几百万损失破口大骂,也有人因为几万的丢失是否生气,也有人丢了几块钱很愤怒。他觉得人都会愤怒,只是那个导致他愤怒的级别不一样而已。


他早年给自己定目标,发现当达到目标之后,发现还是有人比自己厉害,所以即使达到目标也没有想象快乐;即使有钱人无法解决痛苦的问题,比如生老病死。


所以人生意义在哪里?


我认为如果你奔着成事的想法,多修炼上面两件事,如果没有发展机会,不如躺平,积累自己的能力,就像道德经里面的无为而为,而心态的修炼是人生终极修炼,因为生死才是人生最大的难关,人应该怎么脱离时间束缚,不受衰老影响。


恰恰历史给了我们启示,要有所作为,能够为人类历史上留下精神粮食,做出浓厚贡献,就像我们都没有见过孔子、老子吧,但是他们的精神至今都在影响着我们。


作者:大鸡腿同学
来源:juejin.cn/post/7252251628090490917
收起阅读 »

前一阵闹得沸沸扬扬的IP归属地,到底是怎么实现的?

大家好,我是王老师,一直在准备写这篇稿子,但是事情太多一直耽误了,导致一直拖一直拖,结果就从最近变成了前一阵子。这下好了,不会有人说我蹭热度了。 大家都知道,前一阵子抖音和微博开始陆续上了IP归属地的功能,引起了众多热议。有大批在国外的老铁们开始"原形毕露...
继续阅读 »

大家好,我是王老师,一直在准备写这篇稿子,但是事情太多一直耽误了,导致一直拖一直拖,结果就从最近变成了前一阵子。这下好了,不会有人说我蹭热度了。


image.png


image.png


大家都知道,前一阵子抖音和微博开始陆续上了IP归属地的功能,引起了众多热议。有大批在国外的老铁们开始"原形毕露",被定位到国内来,那么IP归属到底是怎么实现的呢?那么网红们的归属地到底对不对呢?这篇文章帮大家揭晓。


一.第一步:如何拿到用户的真实IP


大家都知道,我们一般想访问公网,一般必须具备上网环境,那么我们开通宽带之后,运营商会给我们分配一个IP地址。一般IP地址我们都是自动分配的。所以我们不知道本机地址是什么?想知道自己的ip公网地址,可以通过百度搜索IP查看自己的ip位置
image.png


那么问题来了。百度是怎么知道我的公网IP的?


一般情况,用户访问我们的服务网络拓扑如下:


image.png


用户通过域名或者IP访问门户,然后请求到后端服务。这样的话后端服务就可以通过request.getRemoteAddr();方法获取用户的ip。


SpringBoot获取IP如下:


@RestController
public class IpController {

  @RequestMapping("/getIp")
  public String hello(HttpServletRequest request) {
      String ip = request.getRemoteAddr();
      System.out.println(ip);
      return ip;
  }
}

将服务部署到服务端,然后请求该接口,即可获取IP信息,如下图:


image.png


但是为什么我们获取的IP和百度搜出来的不一样呢?


1.1内网IP和外网IP


打开电脑CMD,输出ipconfig命令,查看本机的IP地址,发现我们本机地址和程序获取的地址是一样的。


image.png


其实,网络也是分内网IP和公网IP的。内网也成局域网。对于像公司,学校这种一般内部建立自己的局域网,对内部的信息进行传输时,都是通过内网相互通讯,建立局域网内网通讯节省了公网IP资源,并且通信效率也有很大的提升。当然非局域网内的设备则无法向内网的设备发送信息。


但是机器想要访问互联网的资源时,则需要机器拥有外网带宽,也就是我们所说的分配公网IP,负责也是无法访问互联网资源的。


image.png


因此,我们把服务部署在同一局域网内,客户端使用内网进行通信,因此获取的就是内网IP地址。但访问百度是需要使用公网访问,因此百度搜出来的IP就是公网IP地址。


1.2.为什么有时候获取到的客户端IP有问题?


当我们兴致勃勃的把IP获取的功能搞上去之后,发现获取的IP都是同一个?这是为什么呢?不可能只是一个用户在访问呀?查询IP信息之后发现,原来是我们部署的一台负载均衡的IP地址。


image.png


那么后端服务获取的地址都是负载均衡如nginx的地址。那么怎么透过负载均衡获取真实的地址呢?


透明的代理服务器在将客户端的访问请求转发到下一环节的服务器时,会在HTTP的请求头中添加一条X-Forwarded-For记录,用于记录客户端的IP,格式为X-Forwarded-For:客户端IP。如果客户端和服务器之间有多个代理服务器,则X-Forwarded-For记录使用以下格式记录客户端IP和依次经过的代理服务器IP:X-Forwarded-For:客户端IP, 代理服务器1的IP, 代理服务器2的IP, 代理服务器3的IP, ……


因此,常见的Web应用服务器可以通过解析X-Forwarded-For记录获取客户端真实IP。


public static String getIp(HttpServletRequest request) {
  String ip = request.getHeader("x-forwarded-for");

  if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
      ip = request.getRemoteAddr();
  } else if (ip.length() > 15) {
      //多次反向代理后会有多个ip值,第一个ip才是真实ip
      String[] ips = ip.split(",");
      for (int index = 0; index < ips.length; index++) {
          String strIp = ips[index];
          ip = strIp;
          break;
      }
  }
  return ip;
}

第二步:如何解析IP


IP来了,我们怎么解析呢:


IP的解析一般都要借助第三方软件使用了,第三方一般也分为离线库和在线库



  • 离线库支持的有如:IPIP,使用离线库的好处是解析效率高,性能好,问题就是IP库要经常更新。如果大家需要我私信我可以提供给大家比较新版本的ip库。

  • 在线库则各大云厂商接口能力都有支持。在线版本的好处是更新即时,问题就是接口查询性能和使用TPS有要求。


以下演示借助IP库离线IP解析方式:


借助IP库就可以帮我们实现ip地址的解析。


public static void main(String[] args) {
  IpAddrInfo IpAddrInfo = IPAddr.getInstance().putLocInfo("114.103.71.226");
  System.out.println(JSONObject.toJSONString(IpAddrInfo));
}

public IpAddrInfo putLocInfo(String ip) {
  IpAddrInfo info = new IpAddrInfo();
  if (StringUtils.isNotBlank(ip)) {
      try {
          DistrictInfo addrInfo = db.findInfo(ip, "CN");
          info.setCity(addrInfo.getCityName());
          info.setCountry(addrInfo.getCountryName());
          info.setCountryCode(addrInfo.getChinaAdminCode());
          info.setIsp(addrInfo.getIsp());
          info.setLat(addrInfo.getLatitude());
          info.setLon(addrInfo.getLongitude());
          info.setProvince(addrInfo.getRegionName());
          info.setTimeZone(addrInfo.getTimeZone());
          System.out.println(addrInfo.toString());
      } catch (IPFormatException e) {
          e.printStackTrace();
      } catch (InvalidDatabaseException e) {
          e.printStackTrace();
      }
  }
  return info;
}

image.png


其实IP的定位解析其实就是一个巨大的位置库,同时IP数量也是有限制的,因此同一个Ip也可能会分配到不同的区域,因此影响IP解析位置准确率的有几个方面
1、位置库不精准,导致解析偏差大或者地区字段确实
2、离线库更新不及时
并且海外的一般有专门的离线库去支持,使用同一套离线库并不一定支持海外IP的解析,所以本次受影响最大的海外网红门被解析到中国各个地区,被大家认为造假,当然也包括真的有造假。不过上线了这个功能也是有好处的,至少网络不是法外之地,大家也要有序的健康的冲浪,拒绝网络暴力。


好了,今天就到这里,我是王老狮,一个有想法有内涵的工程狮,关注我,学习更多技术知识。


收起阅读 »

DevOps发版失败,发版仿佛回到了石器时代😣

前言 事情是这样的,公司项目发版使用的 Azure DevOps,前段时间测试老师找过来说代码发布失败了,有代码报错,让我查一下,刚开始我猜测应该代码哪里写的有问题,之前也遇到过这种问题,改一下应该就行了 万万没想到,世界真奇妙...... 项目框架及版本 ...
继续阅读 »

前言


事情是这样的,公司项目发版使用的 Azure DevOps,前段时间测试老师找过来说代码发布失败了,有代码报错,让我查一下,刚开始我猜测应该代码哪里写的有问题,之前也遇到过这种问题,改一下应该就行了


万万没想到,世界真奇妙......



项目框架及版本



  • JS框架 Angular: 8.1.0

  • TypeScript: 3.4.3

  • UI库 devextreme:19.2.5


来分析一下遇到的问题


本地排查


下面是报错截图,看里面的内容似乎是代码报错导致的



具体看一下,看起来像 xxx.ts 里面的代码有问题,npm run build-uat 命令导致的发版失败



在代码仓库中找到了对应的报错文件并找到了控制台中报错行数 11:30, 看着没有问题,整个文件的代码都看了,没有异常的地方



npm run start 本地环境运行查看项目相关报错文件,查看报错情况,没有报错,使用 npm run build-uat 本地打包也正常,没有报错



为了排查个人电脑独特环境配置情况下才正常的情况,找了其他两个前端开发老师执行 npm run build-uat 打包命令,也都正常



果然,我写的代码怎么会有这种问题!😏


本地排查已经说明代码没有问题了,问题应该出在 DevOps 服务的环境上面


服务端排查


把本地的代码 pushGitLab 仓库,重新在 DevOps 平台页面上生成测试效果,发现不管生成几次,都是这个报错,DevOps 服务器不知道地址,也没有权限账号,找到有权限的架构部门相关的技术老师,说明原因后,让别人帮忙把代码单独在服务器上的镜像中运行看看效果


这个是单独在服务器上运行以后的报错截图,还是这个报错



第二天再去找架构部负责帮忙排查的那个老师,那个老师说排查后没检查出什么原因,发版的话先走其他方式吧,后面再研究研究看看,提供了一个临时方案


临时方案


由于 npm run build-uat 命令打包失败,又不能不用 DevOps 平台,让我们把线上的打包的环节放到本地,把 DevOps 的上的 installbuild 安装打包命令先去掉,只留下其他命令脚本;把代码仓库的 .gitignore 中的 dist 文件忽略放开,然后提交把打包好的 dist 文件提交到线上的仓库中



都这年头了自动化发版不能用,仿佛一下子回到了技术领域的石器时代




现在的样子


架构部门的技术老师没搞定,反馈给公司的架构师看了,说帮忙弄下,后来也没了动静,然后就一直用的临时方案到现在,就是我们组的这几个前端开发悲剧了,每次提交都是拉一堆文件,然后本地打包,每次打包十几分钟,然后再提交一堆上去,每次几十个文件,仓库越来越大,本地运行打包速度也会越来越慢



以目前的情况,感觉项目仓库迟早要崩呀 😟



这种问题怎么解决




  1. 和架构部的人也不熟,上次找别人还是和我们这边测试负责人一起去的,别的部门也有自己部门的任务,可能帮忙看了看没找到问题后面也就没顾上再看了,自己直接去找到级别也不够,只能再次给我们这边的测试负责人和部门负责人反馈,推动上级让他们去协调跟进




  2. 技术层面上分析这个打包问题,看起来是 DevOps 中的 Docker 镜像容器环境出的问题,那么是不是可以本地安装Docker 拉取 Node 镜像搭建前端环境容器,本地在 Docker 容器中进行测试,如果没问题,可以把本地的 Docker 镜像导出来提供给架构部再次测试





还有更好的解决方案吗?


作者:草帽lufei
来源:juejin.cn/post/7252540034677522490

收起阅读 »

剑走偏锋,无头浏览器是什么神奇的家伙

web
浏览器是再熟悉不过的东西了,几乎每个人用过,比如 Chrome、FireFox、Safari,尤其是我们程序员,可谓开发最强辅助,摸鱼最好的伴侣。 浏览器能干的事儿,无头浏览器都能干,而且很多时候比标准浏览器还要更好用,而且能实现一些很好玩儿的功能,我们能借...
继续阅读 »

浏览器是再熟悉不过的东西了,几乎每个人用过,比如 Chrome、FireFox、Safari,尤其是我们程序员,可谓开发最强辅助,摸鱼最好的伴侣。



浏览器能干的事儿,无头浏览器都能干,而且很多时候比标准浏览器还要更好用,而且能实现一些很好玩儿的功能,我们能借助无头浏览器比肩标准浏览器强大的功能,而且又能灵活的用程序控制的特性,做出一些很有意思的产品功能来,稍后我们细说。


什么是浏览器


关于浏览器还有一个很好玩儿的梗,对于一些对计算机、对互联网不太了解的同学,你跟他说浏览器,他/她就默认是百度了,因为好多小白的浏览器都设置了百度为默认页面。所以很多小白将浏览器和搜索引擎(99%是百度)划等号了。



浏览器里我百分之99的时间都是用 Chrome,不过有一说一,这玩意是真耗内存,我基本上是十几、二十几个的 tab 开着,再加上几个 IDEA 进程,16G 的内存根本就不够耗的。


以 Chrome 浏览器为例,Chrome 由以下几部分组成:



  1. 渲染引擎(Rendering Engine):Chromium使用的渲染引擎主要有两个选项:WebKit和Blink。WebKit是最初由苹果开发的渲染引擎,后来被Google采用并继续开发。Blink则是Google从WebKit分支出来并进行独立开发的渲染引擎,目前Chromium主要使用Blink作为其默认的渲染引擎。

  2. JavaScript引擎(JavaScript Engine):Chromium使用V8引擎作为其JavaScript引擎。V8是由Google开发的高性能JavaScript引擎,它负责解析和执行网页中的JavaScript代码。

  3. 网络栈(Network Stack):Chromium的网络栈负责处理网络通信。它支持各种网络协议,包括HTTP、HTTPS、WebSocket等,并提供了网络请求、响应处理和数据传输等功能。

  4. 布局引擎(Layout Engine):Chromium使用布局引擎来计算网页中元素的位置和大小,并确定它们在屏幕上的布局。布局引擎将CSS样式应用于DOM元素,并计算它们的几何属性。

  5. 绘制引擎(Painting Engine):绘制引擎负责将网页内容绘制到屏幕上,生成最终的图像。它使用图形库和硬件加速技术来高效地进行绘制操作。

  6. 用户界面(User Interface):Chromium提供了用户界面的支持,包括地址栏、标签页、书签管理、设置等功能。它还提供了扩展和插件系统,允许用户根据自己的需求进行个性化定制。

  7. 其他组件:除了上述主要组件外,Chromium还包括其他一些辅助组件,如存储系统、安全模块、媒体处理、数据库支持等,以提供更全面的浏览器功能。


Chrome 浏览器光源码就有十几个G,2000多万行代码,可见,要实现一个功能完善的浏览器是一项浩大的工程。


什么是无头浏览器


无头浏览器(Headless Browser)是一种浏览器程序,没有图形用户界面(GUI),但能够执行与普通浏览器相似的功能。无头浏览器能够加载和解析网页,执行JavaScript代码,处理网页事件,并提供对DOM(文档对象模型)的访问和操作能力。


与传统浏览器相比,无头浏览器的主要区别在于其没有可见的窗口或用户界面。这使得它在后台运行时,不会显示实际的浏览器窗口,从而节省了系统资源,并且可以更高效地执行自动化任务。


常见的无头浏览器包括Headless Chrome(Chrome的无头模式)、PhantomJS、Puppeteer(基于Chrome的无头浏览器库)等。它们提供了编程接口,使开发者能够通过代码自动化控制和操作浏览器行为。


无头浏览器其实就是看不见的浏览器,所有的操作都要通过代码调用 API 来控制,所以浏览器能干的事儿,无头浏览器都能干,而且很多事儿做起来比标准的浏览器更简单。


我举几个常用的功能来说明一下无头浏览器的主要使用场景



  1. 自动化测试: 无头浏览器可以模拟用户行为,执行自动化测试任务,例如对网页进行加载、表单填写、点击按钮、检查页面元素等。

  2. 数据抓取: 无头浏览器可用于爬取网页数据,自动访问网站并提取所需的信息,用于数据分析、搜索引擎优化等。

  3. 屏幕截图: 无头浏览器可以加载网页并生成网页的截图,用于生成快照、生成预览图像等。

  4. 服务器端渲染: 无头浏览器可以用于服务器端渲染(Server-side Rendering),将动态生成的页面渲染为静态HTML,提供更好的性能和搜索引擎优化效果。

  5. 生成 PDF 文件:使用浏览器自带的生成 PDF 功能,将目标页面转换成 PDF 。


使用无头浏览器做一些好玩的功能


开篇就说了使用无头浏览器可以实现一些好玩儿的功能,这些功能别看不大,但是使用场景还是很多的,有些开发者就是抓住这些小功能,开发出好用的产品,运气好的话还能赚到钱,尤其是在国外市场。(在国内做收费的产品确实不容易赚到钱)


下面我们就来介绍两个好玩儿而且有用的功能。


前面的自动化测试、服务端渲染就不说了。


自动化测试太专业了,一般用户用不到,只有开发者或者测试工程师用。


服务端渲染使用无头浏览器确实没必要,因为有太多成熟的方案了,连 React 都有服务端渲染的能力(RSC)。


网页截图功能


我们可能见过一些网站提供下载文字卡片或者图文卡片的功能。比如读到一段想要分享的内容,选中之后将文本端所在的区域生成一张图片。



其实就是通过调用浏览器自身的 API page.screenshot,可以对整个页面或者选定的区域生成图片。


通过这个方法,我们可以做一个浏览器插件,用户选定某个区域后,直接生成对应的图片。这类功能在手机APP上很常见,在浏览器上一搬的网站都不提供。


说到这儿好像和无头浏览器都没什么关系吧,这都是标准浏览器中做的事儿,用户已经打开了页面,在浏览器上操作自己看到的内容,顺理成章。


但是如果这个操作是批量的呢,或者是在后台静默完成的情况呢?


那就需要无头浏览器来出手了,无头浏览器虽然没有操作界面,但是也具备绘制引擎的完整功能,仍然可以生成图像,利用这个功能,就可以批量的、静默生成图像了,并且可以截取完整的网页或者部分区域。


Puppeteer 是无头浏览器中的佼佼者,提供了简单好用的 API ,不过是 nodejs 版的。


如果是用 Java 开发的话,有一个替代品,叫做 Jvppeteer,提供了和 Puppeteer 几乎一模一样的 API。


下面这段代码就展示了如何用 Jvppeteer 来实现网页的截图。


下面这个方法是对整个网页进行截图,只需要给定网页 url 和 最终的图片路径就可以了。


public static boolean screenShotWx(String url, String path) throws IOException, ExecutionException, InterruptedException {
BrowserFetcher.downloadIfNotExist(null);
ArrayList arrayList = new ArrayList<>();
// MacOS 要这样写,指定Chrome的位置
String executablePath = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
LaunchOptions options = new LaunchOptionsBuilder().withExecutablePath(executablePath).withArgs(arrayList).withHeadless(true).withIgnoreHTTPSErrors(true).build();
// Windows 和 Linux 这样就可以,不用指定 Chrome 的安装位置
//LaunchOptions options = new LaunchOptionsBuilder().withArgs(arrayList).withHeadless(true).withIgnoreHTTPSErrors(true).build();
arrayList.add("--no-sandbox");
arrayList.add("--disable-setuid-sandbox");
arrayList.add("--ignore-certificate-errors");
arrayList.add("--disable-gpu");
arrayList.add("--disable-web-security");
arrayList.add("--disable-infobars");
arrayList.add("--disable-extensions");
arrayList.add("--disable-bundled-ppapi-flash");
arrayList.add("--allow-running-insecure-content");
arrayList.add("--mute-audio");
Browser browser = Puppeteer.launch(options);
Page page = browser.newPage();
page.setJavaScriptEnabled(true);
page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36 Edg/83.0.478.37");
page.setCacheEnabled(true);
page.onConsole((msg) -> {
log.info("==> {}", msg.text());
});


PageNavigateOptions pageNavigateOptions = new PageNavigateOptions();
pageNavigateOptions.setTimeout(1000000);
//dom加载完毕就算导航完成
pageNavigateOptions.setWaitUntil(Collections.singletonList("domcontentloaded"));
page.goTo(url, pageNavigateOptions, true);

autoScroll(page);
ElementHandle body = page.$("body");
double width = body.boundingBox().getWidth();
double height = body.boundingBox().getHeight();
Viewport viewport = new Viewport();

viewport.setWidth((int) width); // 设置视口宽度
viewport.setHeight((int) height + 100); // 设置视口高度
page.setViewport(viewport);
ScreenshotOptions screenshotOptions = new ScreenshotOptions();
screenshotOptions.setType("jpeg");
screenshotOptions.setFullPage(Boolean.FALSE);
//screenshotOptions.setClip(clip);
screenshotOptions.setPath(path);
screenshotOptions.setQuality(100);
// 或者转换为 base64
//String base64Str = page.screenshot(screenshotOptions);
//System.out.println(base64Str);

browser.close();
return true;
}

一个自动滚屏的方法。


虽然可以监听页面上的事件通知,比如 domcontentloaded,文档加载完成的通知,但是很多时候并不能监听到网页上的所有元素都加载完成了。对于那些滚动加载的页面,可以用这种方式模拟完全加载,加载完成之后再进行操作就可以了。


使用自动滚屏的操作,可以模拟我们人为的在界面上下拉滚动条的操作,随着滚动条的下拉,页面上的元素会自然的加载,不管是同步的还有延迟异步的,比如图片、图表等。


private static void autoScroll(Page page) {
if (page != null) {
try {
page.evaluate("() => {\n" +
" return new Promise((resolve, reject) => {\n" +
" //滚动的总高度\n" +
" let totalHeight = 0;\n" +
" //每次向下滚动的高度 500 px\n" +
" let distance = 500;\n" +
" let k = 0;\n" +
" let timeout = 1000;\n" +
" let url = window.location.href;\n" +
" let timer = setInterval(() => {\n" +
" //滚动条向下滚动 distance\n" +
" window.scrollBy(0, distance);\n" +
" totalHeight += distance;\n" +
" k++;\n" +
" console.log(`当前第${k}次滚动,页面高度: ${totalHeight}`);\n" +
" //页面的高度 包含滚动高度\n" +
" let scrollHeight = document.body.scrollHeight;\n" +
" //当滚动的总高度 大于 页面高度 说明滚到底了。也就是说到滚动条滚到底时,以上还会继续累加,直到超过页面高度\n" +
" if (totalHeight >= scrollHeight || k >= 200) {\n" +
" clearInterval(timer);\n" +
" resolve();\n" +
" window.scrollTo(0, 0);\n" +
" }\n" +
" }, timeout);\n" +
" })\n" +
" }");
} catch (Exception e) {

}
}
}

调用截图方法截图,这里是对一篇公众号文章进行整个网页的截图。


public static void main(String[] args) throws Exception {
screenShotWx("https://mp.weixin.qq.com/s/MzCyWqcH1TCytpnHI8dVjA", "/Users/fengzheng/Desktop/PICTURE/wx.jpeg");
}

或者也可以截取页面中的部分区域,比如某篇文章的正文部分,下面这个方法是截图一个博客文章的正文部分。


public static boolean screenShotJueJin(String url, String path) throws IOException, ExecutionException, InterruptedException {
BrowserFetcher.downloadIfNotExist(null);
ArrayList arrayList = new ArrayList<>();
String executablePath = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
LaunchOptions options = new LaunchOptionsBuilder().withExecutablePath(executablePath).withArgs(arrayList).withHeadless(true).withIgnoreHTTPSErrors(true).build();

//LaunchOptions options = new LaunchOptionsBuilder().withArgs(arrayList).withHeadless(true).withIgnoreHTTPSErrors(true).build();
arrayList.add("--no-sandbox");
arrayList.add("--disable-setuid-sandbox");
Browser browser = Puppeteer.launch(options);
Page page = browser.newPage();

PageNavigateOptions pageNavigateOptions = new PageNavigateOptions();
pageNavigateOptions.setTimeout(1000000);
//dom加载完毕就算导航完成
pageNavigateOptions.setWaitUntil(Collections.singletonList("domcontentloaded"));
page.goTo(url, pageNavigateOptions, true);

WaitForSelectorOptions waitForSelectorOptions = new WaitForSelectorOptions();
waitForSelectorOptions.setTimeout(1000 * 15);
waitForSelectorOptions.setVisible(Boolean.TRUE);
// 指定截图的区域
ElementHandle elementHandle = page.waitForSelector("article.article", waitForSelectorOptions);
Clip clip = elementHandle.boundingBox();
Viewport viewport = new Viewport();
ElementHandle body = page.$("body");
double width = body.boundingBox().getWidth();
viewport.setWidth((int) width); // 设置视口宽度
viewport.setHeight((int) clip.getHeight() + 100); // 设置视口高度
page.setViewport(viewport);
ScreenshotOptions screenshotOptions = new ScreenshotOptions();
screenshotOptions.setType("jpeg");
screenshotOptions.setFullPage(Boolean.FALSE);
screenshotOptions.setClip(clip);
screenshotOptions.setPath(path);
screenshotOptions.setQuality(100);
// 或者生成图片的 base64编码
String base64Str = page.screenshot(screenshotOptions);
System.out.println(base64Str);
return true;
}


调用方式:


public static void main(String[] args) throws Exception {
screenShotJueJin("https://juejin.cn/post/7239715628172902437", "/Users/fengzheng/Desktop/PICTURE/juejin.jpeg");
}

最后的效果是这样的,可以达到很清晰的效果。



网页生成 PDF 功能


这个功能可太有用了,可以把一些网页转成离线版的文档。有人说直接保存网页不就行了,除了程序员,大部分人还是更能直接读 PDF ,而不会用离线存储的网页。


我们可以在浏览器上使用浏览器的「打印」功能,用来将网页转换成 PDF 格式。



但这是直接在页面上操作,如果是批量操作呢,比如想把一个专栏的所有文章都生成 PDF呢,就可以用无头浏览器来做了。


有的同学说,用其他的库也可以呀,Java 里面有很多生成 PDF 的开源库,可以把 HTML 转成 PDF,比如Apache PDFBox、IText 等,但是这些库应对一般的场景还行,对于那种页面上有延迟加载的图表啊、图片啊、脚本之类的就束手无策了。


而无头浏览器就可以,你可以监听页面加载完成的事件,可以模拟操作,主动触发页面加载,甚至还可以在页面中添加自定义的样式、脚本等,让生成的 PDF 更加完整、美观。


下面这个方法演示了如何将一个网页转成 PDF 。


public static boolean pdf(String url, String savePath) throws Exception {
Browser browser = null;
Page page = null;
try {
//自动下载,第一次下载后不会再下载
BrowserFetcher.downloadIfNotExist(null);
ArrayList arrayList = new ArrayList<>();
// MacOS
String executablePath = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
LaunchOptions options = new LaunchOptionsBuilder().withExecutablePath(executablePath).withArgs(arrayList).withHeadless(true).withIgnoreHTTPSErrors(true).build();
// windows 或 linux
//LaunchOptions options = new LaunchOptionsBuilder().withArgs(arrayList).withHeadless(true).withIgnoreHTTPSErrors(true).build();

arrayList.add("--no-sandbox");
arrayList.add("--disable-setuid-sandbox");
arrayList.add("--ignore-certificate-errors");
arrayList.add("--disable-gpu");
arrayList.add("--disable-web-security");
arrayList.add("--disable-infobars");
arrayList.add("--disable-extensions");
arrayList.add("--disable-bundled-ppapi-flash");
arrayList.add("--allow-running-insecure-content");
arrayList.add("--mute-audio");

browser = Puppeteer.launch(options);
page = browser.newPage();

page.onConsole((msg) -> {
log.info("==> {}", msg.text());
});

page.setViewport(viewport);
page.setJavaScriptEnabled(true);
page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36 Edg/83.0.478.37");
page.setCacheEnabled(true);

//设置参数防止检测
page.evaluateOnNewDocument("() =>{ Object.defineProperties(navigator,{ webdriver:{ get: () => undefined } }) }");
page.evaluateOnNewDocument("() =>{ window.navigator.chrome = { runtime: {}, }; }");
page.evaluateOnNewDocument("() =>{ Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] }); }");
page.evaluateOnNewDocument("() =>{ Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5,6], }); }");

PageNavigateOptions pageNavigateOptions = new PageNavigateOptions();
pageNavigateOptions.setTimeout(1000000);
//dom加载完毕就算导航完成
pageNavigateOptions.setWaitUntil(Collections.singletonList("domcontentloaded"));

page.goTo(url, pageNavigateOptions, true);
// 添加自定义演示
StyleTagOptions styleTagOptions1 = new StyleTagOptions();
styleTagOptions1.setContent("html {-webkit-print-color-adjust: exact} .table > table > tr:nth-child(1),.table > table > tr:nth-child(2) {background: #4074b0;} #tableB td:nth-child(2) {width:60%;}");
page.addStyleTag(styleTagOptions1);

//滚屏
autoScroll(page);
Thread.sleep(1000);

PDFOptions pdfOptions = new PDFOptions();
// pdfOptions.setHeight("5200");
pdfOptions.setPath(savePath);
page.pdf(pdfOptions);

} catch (Exception e) {
log.error("生成pdf异常:{}", e.getMessage());
e.printStackTrace();
} finally {
if (page != null) {
page.close();
}
if (browser != null) {
browser.close();
}
}
return true;
}

调用生成 PDF 的方法,将一个微信公众号文章转成 PDF。


    public static void main(String[] args) throws Exception {
String pdfPath = "/Users/fengzheng/Desktop/PDF";
String filePath = pdfPath + "/hello.pdf";
JvppeteerUtils.pdf("https://mp.weixin.qq.com/s/MzCyWqcH1TCytpnHI8dVjA", filePath);
}

最终的效果,很清晰,样式都在,基本和页面一模一样。


作者:古时的风筝
来源:juejin.cn/post/7243780412547121208

收起阅读 »

如何优化 electron 应用在低配电脑秒启动

背景 古茗门店使用的收银机,有些会因为使用年限长、装了杀毒软件、配置低等原因性能较差,导致进钱宝启动响应较慢。然后店员在双击进钱宝图标后,发现没反应,就会重复点击 因此我们希望优化到即使在这些性能不太好的收银机上,也能让进钱宝有较快的启动体验 优化思路 测...
继续阅读 »


背景


古茗门店使用的收银机,有些会因为使用年限长、装了杀毒软件、配置低等原因性能较差,导致进钱宝启动响应较慢。然后店员在双击进钱宝图标后,发现没反应,就会重复点击


因此我们希望优化到即使在这些性能不太好的收银机上,也能让进钱宝有较快的启动体验
lAHPKHtEUt3mDUzM8Mzw_240_240.gif


优化思路



  • 测量,得到一个大概的优化目标,并发现可优化的阶段

  • 主要方向是优化主进程创建出窗口的时间、让渲染进程页面尽快显示

  • 性能优化好后,尽量让人感觉上更快点

  • 上报各阶段耗时,建立监控机制,发现变慢了及时优化


测量


测量主进程


编写一个 bat文件 放到应用根目录,通过bat启动程序并获取初始启动时间:


@echo off

set "$=%temp%\Spring"
>%$% Echo WScript.Echo((new Date()).getTime())
for /f %%a in ('cscript -nologo -e:jscript %$%') do set timestamp=%%a
del /f /q %$%
echo %timestamp%
start yourAppName.exe

pause

项目内可以使用如下api打印主进程各时间节点:


this.window.webContents.executeJavaScript(
`console.log('start', ${start});console.log('onReady', ${onReady});console.log('inCreateWindow', ${inCreateWindow});console.log('afterCreateWindow', ${afterCreateWindow});console.log('beforeInitEvents', ${beforeInitEvents});console.log('afterInitEvents', ${afterInitEvents});console.log('startLoad', ${startLoad});`
);

如果发现主进程有不正常的耗时,可以通过v8-inspect-profiler捕获主进程执行情况,最终生成的文件可以放到浏览器调试工具中生成火焰图


测量渲染进程


1、可以console打印时间点,可以借助preformance API获取一些时间节点


2、可以使用preformance工具测白屏时间等


image.png


进钱宝测量结果


以下测量结果中每一项都是时间戳,括号里是距离上一步的时间(ms)


最简单状态(主进程只保留唤起主渲染进程窗口的逻辑):


执行exe(指双击应用图标)开始执行主进程代码主进程ready事件开始初始化渲染进程窗口开始加载渲染进程资源
16776661416191677666142152(+533)1677666142224(+72)1677666142364(+140)1677666142375(+11)

未优化状态:


执行exe开始执行主进程代码主进程ready事件开始初始化渲染进程窗口开始加载渲染进程资源
16776694148861677669417742(+2856)1677669417856(+114)1677669418043(+187)1677669418061(+18)

通过上述数据,能看出主进程最大的卡点是执行exe到开始执行代码之间


渲染进程的白屏时间,最初测试大概是1000ms


那么我们的优化目标,就是往最简单应用的时间靠齐,优化重点就是主进程开始执行代码时间,和渲染进程白屏时间


优化步骤


一、让主进程代码尽快执行


使用常见的方式,打包、压缩、支持tree-shaking,让代码体积尽可能的小;


可以把一些依赖按需加载,减少初始包体积


代码压缩


使用electron的一个好处是:chrome版本较高,不用pollyfill,可以直接使用很新的es特性


直接编译目标 ecma2020!!


优化tree-shaking


主进程存在global对象,但一些配置性的变量尽量不要挂载在global上,可以放到编译时配置里,以支持更好的tree-shaking


const exendsGlobal = {
__DEV__,
__APP_DIR__,
__RELEASE__,
__TEST__,
__LOCAL__,
__CONFIG_FILE__,
__LOG_DRI__,
GM_BUILD_ENV: JSON.stringify(process.env.GM_BUILD_ENV),
};

// 这里把一些变量挂载在global上,这样不利于tree-shaking
Object.assign(global, exendsGlobal);

慎用注册快捷方式API


实测这样的调用是存在性能损耗的


globalShortcut.register('CommandOrControl+I', () => {
this.window.webContents.openDevTools();
});
// 这个触发方式,我们改为了在页面某个地方连点三下,因为事件监听基本没性能损耗
// 或者把快捷方式的注册在应用的生命周期中往后移,尽量不影响应用的启动

优化require


因为require在node里是一个耗时操作,而主进程最终是打包成一个cjs格式,里面难免有require


可以使用 node --cpu-prof --heap-prof -e "require('request')" 获取一个包的引用时长。
如下是一些在我本机的测量结果:


时长(ms)
fs-extra83
event-kit25
electron-store197
electron-log61
v8-compile-cache29

具体理论分析可以看这里:
如何加快 Node.js 应用的启动速度


因此我们可以通过一些方式优化require



  • 把require的包打进bundle

    • 有两个问题

      • bundle体积会增加,这样还是会影响代码编译和加载时间

      • 有些库是必须require的,像node和electron的原生api;就进钱宝来说,我们可以通过其他方式优化掉require,因此没使用这种方式





  • 按需require

  • v8 code cache / v8 snapshot

  • 对应用流程做优化,通过减少启动时的事务,来间接减少启动时的require量


按需require


比如fx-extra模块的按需加载方式:


const noop = () => {};

const proxyFsExtra = new Proxy(
{},
{
get(target, property) {
return new Proxy(noop, {
apply(target, ctx, args) {
const fsEx = require('fs-extra');
return fsEx[property](...args);
},
});
},
}
);

export default proxyFsExtra;

前面的步骤总是做了没坏处,但这个步骤因为要重构代码,因此要经过验证


因此我们测量一下:


执行exe开始执行主进程代码主进程ready事件开始初始化渲染进程窗口开始加载渲染进程资源
16776740873441677674089485(+2141)167767408960616776740898641677674089934

可以看出,主进程开始执行时间已经有了较大优化(大概700ms)


v8-compile-cache


可以直接用 v8-compile-cache 这个包做require缓存


简单测试如下:


image.png


脚本执行时间从388到244,因此这个技术确实是能优化执行时间的


但也有可能没有优化效果:


image.png


在总require较少,且包总量不大的情况下,做cache是没有用的。实测对进钱宝也是没用的,因为经过后面的流程优化步骤,进钱宝代码的初始require会很少。因此我们没有使用这项技术


但我们还是可以看下这个包的优化机制,这个包核心代码如下,其实是重写了node的Module模块的_compile函数,编译后把V8字节码缓存,以后要执行时直接使用缓存的字节码省去编译步骤


Module.prototype._compile = function(content, filename) {
...

// 读取编译缓存
var buffer = this._cacheStore.get(filename, invalidationKey);

// 这一步是去编译代码,但如果传入的cachedData有值,就会直接使用,从而跳过编译
// 如果没传入cachedData,这段代码就会产生一份script.cachedData
var script = new vm.Script(wrapper, {
filename: filename,
lineOffset: 0,
displayErrors: true,
cachedData: buffer,
produceCachedData: true,
});

// 上面的代码会产生一份编译结果,把编译结果写入本地文件
if (script.cachedDataProduced) {
this._cacheStore.set(filename, invalidationKey, script.cachedData);
}

// 运行代码
var compiledWrapper = script.runInThisContext({
filename: filename,
lineOffset: 0,
columnOffset: 0,
displayErrors: true,
});

...
};

这里有个可能的优化点:v8-compile-cache 只是缓存编译结果,但require一个模块除了编译,还有加载这个io操作,因此是否可以考虑连io一起缓存


v8-snapshot


image.png


原理是:把代码执行结果的内存,做一个序列化,存到本地,真正执行时,直接加载然后反序列化到内存中


这样跳过了代码编译和执行两个阶段,因此可以提升应用的初始化速度。


优化效果:


image.png


对react做快照后,代码中获取的react对象如下图,实际上获得的是一份react库代码执行后的内存快照,跟正常引入react库没什么区别:


image.png


这个方案看起来很香,但也存在两个小问题:


1、不能对有副作用的代码做snapshot


因为只是覆写内存,而没有实际代码执行,因此如果有 读写文件、操作dom、console 等副作用,是不会生效的


因此这个步骤更多是针对第三方库,而不是业务代码


2、需要修改打包配置


目前项目一般通过import引用各种包,最终把这些包打包到bundle中;但该方案会在内存直接生成对象,并挂载在全局变量上,因此要使用snapshot,代码中包引用方式需要修改,这个可以通过对编译过程的配置实现


这个技术看起来确实能有优化效果,但考虑如下几点,最后我们没有去使用这项技术:



  • 对主进程没用,因为主进程刚进来就是要做打开窗口这个副作用;

  • 对渲染进程性价比不高,因为

    • 我们的页面渲染已经够快(0.2s)

    • 启动时,最大的瓶颈不在前端,而在服务端初始化,前端会长时间停留在launch页面等待服务端初始化,基于这一点,对渲染进程js初始化速度做极限优化带来的收益基本没有,我们真实需要的是让渲染进程能尽快渲染出来一些可见的东西让用户感知

    • 维护一个新模块、修改编译步骤、引入新模块带来的潜在风险




snapshot具体应用方式可看文尾参考文章


二、优化主进程流程,让应该先做的事先做,可以后做的往后放


D2E73602-B81D-4b87-8929-427AB6C51C2A.png
基于上图的思想,我们对bundle包做了拆分:


image.png


新的测量数据:


执行exe开始执行主进程代码主进程ready事件开始初始化渲染进程窗口开始加载渲染进程资源
16779113945161677911395044(+528)1677911395133(+89)--

可以看出,到这里主进程已经跟最简单状态差不多了。而且这一步明显优化非常明显。而这一步做的事情核心就是减少初始事务,从而减少了初始代码量以减少编译和加载负担,也避免了初始时过多比较耗性能的API的执行(比如require,比如new BrowserWindow())。


那么我们主进程优化基本已经达到目标


三、让渲染进程尽快渲染


requestIdleCallback


程序刚启动的时候,CPU占用会很高(100%),因此有些启动任务可以通过requestIdleCallback,在浏览器空闲时间执行,让浏览器优先去渲染


去掉或改造起始时调用sendSync以及使用electron-store的代码


原因是sendSync是同步执行,会阻塞渲染进程


而electron-store里面初始时会调用sendSync


只加载首屏需要的css


对首屏不需要的ui库、components做按需加载,以减少初始css量,首屏尽量只加载首屏渲染所需的css



因为css量会影响页面的渲染性能


使用 tailwind 的同学可能会发现一个现象:如果直接加载所有预置css,页面动画会非常卡,因此 tailwind 会提供 Purge 功能自动移除未使用的css



少用或去掉modulepreload


我们使用的是vite,他会自动给一些js做modulepreload。但实测modulepreload(不是preload)是会拖慢首屏渲染的,用到的同学可以测测看


四、想办法让应用在体验上更快


使用骨架屏提升用户体感


程序开始执行 -> 页面开始渲染, 这段时间内可以使用骨架屏让用户感知到应用在启动,而不是啥都没有


我们这边用c++写了个只有loading界面的exe,在进钱宝启动时首先去唤起这个exe,等渲染进程渲染了,再关掉他(我们首屏就是一个很简单的页面,背景接近下图的纯色,因此loading界面也做的比较简单)


动画.gif


渲染进程骨架屏


渲染进程渲染过程:加载解析html -> 加载并执行js渲染


在js最终执行渲染前,就是白屏时间,可以在html中预先写一点简单的dom来减少白屏时间


一个白屏优化黑科技


我们先看两种渲染效果:


渲染较快的

image.png


image.png


渲染较慢的

image.png


image.png


接下来看下代码区别:


快的代码:
<div id="root">
<span style="color: #000;">哈哈</span> <!-- 就比下面那个多了这行代码 -->
<div class="container">
<div class="loading">
<span></span>
</div>
</div>
</div>

慢的代码:
<div id="root">

<div class="container">
<div class="loading">
<span></span>
</div>
</div>
</div>

就是多了一行文字,就会更快地渲染出来


从下图可以看到,文字渲染出来的同时,背景色和loading动画(就中间那几个白点)也渲染出来了


image.png


有兴趣的可以测一下淘宝首页,如果去掉所有文字,还是会较快渲染,但如果再去掉加载的css中的一个background: url(.....jpg),首次渲染就会变慢了


我猜啊。。。 这个叫信息优先渲染原则。。。🐶就是文字图片可以明确传递信息,纯dom不知道是否传递信息,而如果页面里有明确能传递信息的东西,就尽快渲染出来,否则,渲染任务就可能排到其他初始化任务后面了。


当然了,这只是我根据测试结果反推出来的猜测🐶


好了,现在我们也可以让渲染进程较快的渲染了(至少能先渲染出来一个骨架屏🤣)


五、其他


升级electron版本


electron 官方也是在不断优化bug和性能的


保证后续的持续优化


因为经过后续的维护,比如有人给初始代码加了些不该加的重量,是有可能导致性能下降的


因此我们可以对各节点的数据做上报,数据大盘,异常告警,并及时做优化,从而能持续保证性能


总结


本文介绍了electron应用的优化思路和常见的优化方案。并在进钱宝上取得了实际效果,我们在一台性能不太好的机器上,把感官上的启动时间从10s优化到了1s(可能有人会提个问题,上面列的时间加起来没有10s,为啥说是10s。原因是我们最初是在渲染进程的did-finish-load事件后才显示窗口的,这个时间点是比较晚的)


这其中最有效的步骤是优化流程,让应该先做的事先做,可以往后的就往后排,根据这个原则进行拆包,可以使得初始代码尽可能的简单(体积小,require少,也能减少一些耗性能的动作)。


另外有些网上看起来很秀的东西,不一定对我们的应用有用,是要经过实际测量和分析的,比如code-cache 和 snapshot


还有个点是,如果想进一步提升体验,可以先启动骨架屏应用,再通过骨架屏应用启动进钱宝本身,这样可以做到ms级启动体验,但这样会使骨架屏显示时间更长点(这种体验也不好),也需要考虑win7系统会不会有dll缺失等兼容问题


最后


关注公众号「Goodme前端团队」,获取更多干货实践,欢迎交流分享~


参考文档


v8 code cache


v8.dev/blog/improv…

v8.dev/blog/code-c…

fed.taobao.org/blog/taofed…

blog.csdn.net/szengtal/ar…


v8 snapshot


http://www.javascriptcn.com/post/5eedbc…

blog.inkdrop.app/how-to-make…

github.com/inkdropapp/…


其他


zhuanlan.zhihu.com/p/420238372


blog.csdn.net/qq_37939251…


medium.com/@felixriese…


zhuanlan.zhihu.com/p/376

638202

收起阅读 »

面试官您好,这是我写的TodoList

web
前段时间看到掘金上有人二面被面试官要求写一个TodoList,今天趁着上班没啥事情,我也来写一个小Demo玩玩。 功能 一个TodoList大致就是长成这个样子,有一个输入框,可以通过输入任务名称进行新增,每个任务可以进行勾选,切换已完成和未完成状态,还可以...
继续阅读 »

前段时间看到掘金上有人二面被面试官要求写一个TodoList,今天趁着上班没啥事情,我也来写一个小Demo玩玩。


image.png


功能


一个TodoList大致就是长成这个样子,有一个输入框,可以通过输入任务名称进行新增,每个任务可以进行勾选,切换已完成和未完成状态,还可以删除。


组件设计


组件拆分


接下来,我们可以从功能层次上来拆分组件


image.png



  1. 最外层容器组件,只做一个统一的汇总(红色)

  2. 新增组件,管理任务的输入(绿色)

  3. 列表组件,管理任务的展示(紫色),同时我们也可以将每一个item拆分成为单独的组件(粉色)


数据流


组件拆分完毕之后,我们来管理一下数据流向,我们的数据应该存放在哪里?


我们的数据可以放在新增组件里面吗?不可以,我们的数据是要传递到列表组件进行展示的,他们两个是兄弟组件,管理起来非常不方便。同理,数据也不能放在列表组件里面。所以我们把数据放在我们的顶级组件里面去管理。


我们在最外层容器组件中把数据定义好,并写好删除,新增的逻辑,然后将数据交给列表组件进行展示,列表组件只管数据的展示,不管具体的实现逻辑,我只要把列表id抛出来,调用你传递的删除函数就可以了


现在,我们引出组件设计时的一些原则



  1. 从功能层次上拆分一些组件

  2. 尽量让组件原子化,一个组件只做一个功能就可以了,可以让组件吸收复杂度。每个组件都实现一部分功能,那么整个大复杂度的项目自然就被吸收了

  3. 区分容器组件和UI组件。容器组件来管理数据,具体的业务逻辑;UI组件就只管显示视图


image.png


数据结构的设计


一个合理的数据结构应该满足以下几点:



  1. 用数据描述所有的内容

  2. 数据要结构化,易于操作遍历和查找

  3. 数据要易于扩展,方便增加功能


[
{
id:"1",
title:'标题一',
completed:false
},
{
id:"2",
title:'标题二',
completed:false
}
]

coding


codesandbox.io/s/todolist-…


反思


看了下Antd表单组件的设计,它将一个Form拆分出了Form和Form.item


image.png


image.png


为什么要这么拆分呢?


上文说到,我们在设计一个组件的时候,需要从功能上拆分层次,尽量让组件原子化,只干一件事情。还可以让容器组件(只管理数据)和渲染组件(只管理视图)进行分离


通过Form表单的Api,我们可以发现,Form组件可以控制宏观上的布局,整个表单的样式和数据收集。Form.item控制每个字段的校验等。


个人拙见,如有

作者:晨出
来源:juejin.cn/post/7252678036692451388
不妥,还请指教!!!

收起阅读 »

给你十万条数据,给我顺滑的渲染出来!

web
前言 这是一道面试题,这个问题出来的一刹那,很容易想到的就是for循环100000次吧,但是这方案着实让浏览器崩溃啊!还有什么解决方案呢? 正文 1. for 循环100000次 虽说for循环有点low,但是,当面试官问,为什么会让浏览器崩溃的时候,你知道咋...
继续阅读 »

前言


这是一道面试题,这个问题出来的一刹那,很容易想到的就是for循环100000次吧,但是这方案着实让浏览器崩溃啊!还有什么解决方案呢?


正文


1. for 循环100000次


虽说for循环有点low,但是,当面试官问,为什么会让浏览器崩溃的时候,你知道咋解释吗?

来个例子吧,我们需要在一个容器(ul)中存放100000项数据(li):



我们的思路是打印js运行时间页面渲染时间,第一个console.log的触发时间是在页面进行渲染之前,此时得到的间隔时间为JS运行所需要的时间;第二个console.log是在 setTimeout 中的,它的触发时间是在渲染完成,在下一次Event Loop中执行的。



<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<ul id="ul"></ul>

<script>
let now = Date.now(); //Date.now()得到时间戳

const total = 100000
const ul = document.getElementById('ul')

for (let i = 0; i < total; i++) {
let li = document.createElement('li')
li.innerHTML = ~~(Math.random() * total)
ul.appendChild(li)
}
console.log('js运行时间',Date.now()-now);

setTimeout(()=>{
console.log('总时间',Date.now()-now);
},0)
console.log();
</script>
</body>

</html>

运行可以看到这个数据:


image.png

这渲染开销也太大了吧!而且它是十万条数据一起加载出来,没加载完成我们看到的会是一直白屏;在我们向下滑动过程中,页面也会有卡顿白屏现象,这就需要新的方案了。继续看!


2. 定时器


我们可以使用定时器实现分页渲染,我们继续拿上面那份代码进行优化:


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<ul id="ul"></ul>

<script>
let now = Date.now(); //Date.now()得到时间戳

const total = 100000 //总共100000条数据
const once = 20 //每次插入20条
const page = total / once //总页数
let index = 1
const ul = document.getElementById('ul')

function loop(curTotal, curIndex) {
if (curTotal <= 0) { 判断总数居条数是否小于等于0
return false
}
let pageCount = Math.min(curTotal, once) //以便除不尽有余数
setTimeout(() => {
for (let i = 0; i < pageCount; i++) {
let li = document.createElement('li')
li.innerHTML = curIndex + i + ':' + ~~(Math.random() * total)
ul.appendChild(li)
}
loop(curTotal - pageCount, curIndex + pageCount)
}, 0)
}
loop(total, index)
</script>
</body>

</html>

运行后可以看到这十万条数据并不是一次性全部加载出来,浏览器右方的下拉条有顺滑的效果哦,如下图:


进度条.gif

但是当我们快速滚动时,页面还是会有白屏现象,如下图所示,这是为什么呢?


st.gif
可以说有两点原因:



  • 一是setTimeout的执行时间是不确定的,它属于宏任务,需要等同步代码以及微任务执行完后执行。

  • 二是屏幕刷新频率受分辨率和屏幕尺寸影响,而setTimeout只能设置一个固定的时间间隔,这个时间不一定和屏幕刷新时间相同。


3. requestAnimationFrame


我们这次采用requestAnimationFrame的方法,它是一个用于在下一次浏览器重绘之前调用指定函数的方法,它是 HTML5 提供的 API。



我们插入一个小知识点, requestAnimationFrame 和 setTimeout 的区别:

· requestAnimationFrame的调用频率通常为每秒60次。这意味着我们可以在每次重绘之前更新动画的状态,并确保动画流畅运行,而不会对浏览器的性能造成影响。

· setIntervalsetTimeout它可以让我们在指定的时间间隔内重复执行一个操作,不考虑浏览器的重绘,而是按照指定的时间间隔执行回调函数,可能会被延迟执行,从而影响动画的流畅度。



还有一个问题,我们多次创建li挂到ul上,这样会导致回流,所以我们用虚拟文档片段的方式去优化它,因为它不会触发DOM树的重新渲染!


<!DOCTYPE html>
<html lang="en">

![rf.gif](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3eab42b37f53408b981411ee54088d5a~tplv-k3u1fbpfcp-watermark.image?)
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

![st.gif](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3e922cc57a044f5e9e48e58bda5f6756~tplv-k3u1fbpfcp-watermark.image?)
<body>
<ul id="ul"></ul>

<script>
let now = Date.now(); //Date.now()得到时间戳

const total = 10000
const once = 20
const page = total / once
let index = 1
const ul = document.getElementById('ul')

function loop(curTotal, curIndex) {
if (curTotal <= 0) {
return false
}
let pageCount = Math.min(curTotal, once) //以便除不尽有余数
requestAnimationFrame(()=>{
let fragment = document.createDocumentFragment() //虚拟文档
for (let i = 0; i < pageCount; i++) {
let li = document.createElement('li')
li.innerHTML = curIndex + i + ':' + ~~(Math.random() * total)
fragment.appendChild(li)
}
ul.appendChild(fragment)
loop(curTotal - pageCount, curIndex + pageCount)
})
}
loop(total, index)
</script>
</body>

</html>

可以看到它白屏时间没有那么长了:
rqf.gif

还有没有更好的方案呢?当然有!往下看!


4. 虚拟列表


我们可以通过这张图来表示虚拟列表红框代表你的手机黑条代表一条条数据


image.png

思路:我们只要知道手机屏幕最多能放下几条数据,当下拉滑动时,通过双指针的方式截取相应的数据就可以了。

🚩 PS:为了防止滑动过快导致的白屏现象,我们可以使用预加载的方式多加载一些数据出来。



代码如下:


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<title>虚拟列表</title>
<style>
.v-scroll {
height: 600px;
width: 400px;
border: 3px solid #000;
overflow: auto;
position: relative;
-webkit-overflow-scrolling: touch;
}

.infinite-list {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}

.scroll-list {
left: 0;
right: 0;
top: 0;
position: absolute;
text-align: center;
}

.scroll-item {
padding: 10px;
color: #555;
box-sizing: border-box;
border-bottom: 1px solid #999;
}
</style>
</head>

<body>
<div id="app">
<div ref="list" class="v-scroll" @scroll="scrollEvent($event)">
<div class="infinite-list" :style="{ height: listHeight + 'px' }"></div>

<div class="scroll-list" :style="{ transform: getTransform }">
<div ref="items" class="scroll-item" v-for="item in visibleData" :key="item.id"
:style="{ height: itemHeight + 'px',lineHeight: itemHeight + 'px' }">
{{ item.msg }}</div>
</div>
</div>
</div>

<script>
var throttle = (func, delay) => { //节流
var prev = Date.now();
return function () {
var context = this;
var args = arguments;
var now = Date.now();
if (now - prev >= delay) {
func.apply(context, args);
prev = Date.now();
}
}
}
let listData = []
for (let i = 1; i <= 10000; i++) {
listData.push({
id: i,
msg: i + ':' + Math.floor(Math.random() * 10000)
})
}

const { createApp } = Vue
createApp({
data() {
return {
listData: listData,
itemHeight: 60,
//可视区域高度
screenHeight: 600,
//偏移量
startOffset: 0,
//起始索引
start: 0,
//结束索引
end: null,
};
},
computed: {
//列表总高度
listHeight() {
return this.listData.length * this.itemHeight;
},
//可显示的列表项数
visibleCount() {
return Math.ceil(this.screenHeight / this.itemHeight)
},
//偏移量对应的style
getTransform() {
return `translate3d(0,${this.startOffset}px,0)`;
},
//获取真实显示列表数据
visibleData() {
return this.listData.slice(this.start, Math.min(this.end, this.listData.length));
}
},
mounted() {
this.start = 0;
this.end = this.start + this.visibleCount;
},
methods: {
scrollEvent() {
//当前滚动位置
let scrollTop = this.$refs.list.scrollTop;
//此时的开始索引
this.start = Math.floor(scrollTop / this.itemHeight);
//此时的结束索引
this.end = this.start + this.visibleCount;
//此时的偏移量
this.startOffset = scrollTop - (scrollTop % this.itemHeight);
}
}
}).mount('#app')
</script>
</body>

</html>

可以看到白屏现象解决了!


zz.gif

结语


解决十万条数据渲染的方案基本都在这儿了,还有更好

作者:zt_ever
来源:juejin.cn/post/7252684645979111461
的方案等待大佬输出!

收起阅读 »

我的程序人生之星耀大地

    继上篇iOS- flutter之后,我深感技术在不断发展和进步的同时,也为我们带来了许多挑战。随着年龄的增长,我越来越意识到想要在某一技术领域深耕并非易事。然而,正是这种挑战激发了我对技术的热爱和追求。在这个过程...
继续阅读 »

    继上篇iOS- flutter之后,我深感技术在不断发展和进步的同时,也为我们带来了许多挑战。随着年龄的增长,我越来越意识到想要在某一技术领域深耕并非易事。然而,正是这种挑战激发了我对技术的热爱和追求。在这个过程中,我遇到了一个新项目,《星耀大地》这是一个致力于实现弱网和无网络状态下信息传输的项目。


    这个项目的核心目标是利用北斗三号短报文能力,实现与运营商陆地基站的天地互联。这是一个充满挑战的任务,因为我们需要解决在恶劣的网络环境下进行稳定、高效的信息传输。为了实现这个目标,我们不断尝试、不断探索,从技术方案的确定、算法的优化,每一步都充满了困难和挑战。

     而我也从技术角色完成了管理的角色转换,在思考问题的方式上不再纠结与如何实现,以及具体的某一算法,而是整体推进的方向,以及产品的创新性。更多是思考在实施过程中可能遇到的困难与风险。我需要在不断尝试和探索新的技术和方法的同时,注意风险的管理和控制的灵活性。

      这种转变对我来说是一种挑战,但也是对我在技术和管理两方面理解和掌握的考验。我认为这也是在技术领域深耕的必要素质。

    项目还在继续,文章也还未结束,只是记录一下最近的心态变化。鸡汤:“请保持对技术的热情和面对挑战的勇气

    

收起阅读 »

学会用Compose来做loading动效,隔壁设计师小姐姐投来了羡慕的目光

最近一直在用Compose练习做动效果,但是动效做的再多,在实际做项目的时候,最常用到动效的就是一些loading框,上拉加载下拉刷新之类的场景,我们以前往往遇到这样的需求的时候,会直接问设计师要个切图,或者一个lottie的json文件,但是除了问设计师要资...
继续阅读 »

最近一直在用Compose练习做动效果,但是动效做的再多,在实际做项目的时候,最常用到动效的就是一些loading框,上拉加载下拉刷新之类的场景,我们以前往往遇到这样的需求的时候,会直接问设计师要个切图,或者一个lottie的json文件,但是除了问设计师要资源,我们能不能自己来做一些动效呢,下面我们就来做几个


源码地址


转圈的点点


之前在网上看见别人用CSS写了一个效果,几个圆点绕着圆心转圈,每一个圆点依次变换着大小以及透明值,那么既然CSS可以,我们用Compose能不能做出这样的效果呢,就来试试看吧,首先既然是绕着圆心转圈的,那么就需要把圆心的坐标以及半径确定好


image.png

代码中centerXcenterY就是圆心坐标,radius就是圆的半径,mSize获取画布的大小,实时来更新圆心坐标和半径,然后还需要一组角度,这些角度就是用来绘制圆周上的小圆点的


image.png

angleList里面保存着所有圆点绘制的角度,那么有了角度我们就可以利用正玄余玄公式算出每一个圆点的中心坐标值,公式如下


image.png

pointXpointY就是计算每一个圆点的中心坐标函数,那么我们就能利用这个函数先绘制出一圈圆点


image.png
image.png

圆点画好了,如何让它们转圈并且改变大小与透明值呢,其实这个过程我们可以看作是每一个圆点绘制的时候不断的在改变自身的半径与透明值,所以我们可以再创建两个数组,分别保存变化的半径与透明值


image.png

然后在绘制圆点的时候,我们可以通过循环动画让每一个圆点循环从radiusalphaList两个list里面取值,那么就能实现大小与透明值的变化了


image.png
0620aa2.gif

还差一点,就是让每一个点变化的大小与透明值不一样,那么我们只需要增加一个逻辑,每一次获取到一个listIndex值的时候,我们就让它加一,然后当大小要超过radiusListalphaList的临界值的时候,就让下标值变成0然后在重新计算,就能实现让每一个点的大小与透明值不同了,代码如下


image.png

这样我们这个动效就完成了,最终效果图如下所示


0620aa3.gif

七彩圆环


这个动效主要用到了Modifier.graphicsLayer操作符,可以看下这个操作符里面都有哪些参数


image.png

可以看到这个函数里面提供了许多参数,基本都跟图形绘制有关系,比如大小,位移,透明,旋转等,我们这次先用到了旋转相关的三个参数rotationX,rotationY,rotationZ,比如现在有一个Ring的函数,这个函数的功能就是画一个圆环


image.png

然后我们在页面上创建三个圆环,并且分别进行x,y,z轴上的旋转,代码如下


image.png

旋转使用了角度0到360度变化的循环动画,那么得到的效果就像下面这样


0625aa1.gif

会发现看起来好像只有前两个圆环动了,第三个没有动,其实第三个也在动,它是在绕着z轴转动,就像一个车轮子一样,现在我们尝试下将这三个圆环合并成一个,让一个圆环同时在x,y,z轴上旋转,会有什么效果呢


0625aa2.gif

圆环的旋转马上就变得立体多了,但是只有一个圆环未免显得有点单调了,我们多弄几个圆环,而且为了让圆环旋转的时候互相之间不重叠,我们让每一个圆环旋转的角度不一样,如何做?我们现在只有一种旋转动画,可以再做两个动画,分别为60到420度的旋转和90到450度的旋转,代码如下


image.png

然后这里有三个动画,方向也有xyz三个轴,通过排列组合的思想,一共可以有六个不同方向旋转的圆环,于是我们创建六个圆环,在x,y,z轴上代入不同的旋转角度


image.png
0625aa3.gif

现在我们再给圆环染个色,毕竟叫七彩圆环,怎么着也得有七种颜色,所以在Ring函数里面定义一个七种颜色的数组,然后创建个定时器,定时从颜色数组中拿出不同的色值给圆环设置上


image.png

有个index的变量默认指向数组的第一个颜色,然后每经过500毫秒切换一个色值,并且当index指向数组最后一个色值的时候,重新再设置成指向第一个,我们看下效果


0625aa4.gif

我们看到圆环可以自动改变自身的颜色了,但是效果还是有些单调,我们再优化下,将每一个圆环初始的颜色设置成不同的颜色,那么就要给Ring函数多加一个初始下标的参数,就叫startIndex,然后原来创建index的初始值就从0变成startIndex,其他不变,代码如下


image.png
image.png

现在差不多每一个圆环都有自己的“想法”了,旋转角度不一样,初始颜色也不一样,最终效果图我们看下


0625aa5.gif

七彩尾巴


一样都是七彩,上面做了个圆环,这里我们做个尾巴,怎么做呢?首先我们从画一个圆弧开始


image.png

圆弧就是一个不与圆心相连的扇形,所以我们用到了drawArc函数,然后参数我们随意先设置了几个,就得到了一个初始角度为0,跨度为150度的圆弧了


image.png

然后我们现在让这个圆弧转起来,通过循环改变startAngle就能达到圆弧旋转的效果,所以我们这里添加上一个循环动画


image.png
0625aa6.gif

就得到这样一个旋转的圆弧了,现在部分app里面的loading动画估计就是用的这样的样式,我们现在就在这个样式的基础上,将它变成一个会变颜色的尾巴,首先如何做成一个尾巴呢,貌似没有这样的属性,所以我们只能靠自己画了,我们可以把尾巴看成是若干个圆弧叠放在一起的样子,每一个圆弧的其实角度逐渐变大,sweepAngle逐渐变小,圆弧粗细也逐渐变大,当这些圆弧都画完之后,视觉上看起来就像是一根尾巴了,所以我们需要三个数组,分别存放需要的初始角度,sweepAngle以及圆弧粗细


image.png

然后遍历strokeList,将三个数组对应下标所对应的值取出来用来绘制圆弧,绘制的代码如下


image.png

再运行一遍代码,一根会转圈的红色尾巴就出来了


0625aa7.gif

接下来就是如何改变尾巴的颜色,我们可以像之前画圆环那样的方式来画尾巴,但是那样子的话尾巴切换颜色的过程就会变的很生硬,我们这里用另一个方式,就是animateColorAsState函数,同样也是有一个定时器定时获取一个色值List的颜色,然后将获取到的颜色设置到animateColorAsStatetargetValue里去,最后将动画生成的State<Color>设置到圆弧的color属性里去,代码如下


image.png
image.png

最终我们就获得了一个会变颜色的尾巴


0625aa8.gif

风车


风车的绘制方式有很多种,最简单两个交叉的粗线条就能变成一个风车,或者四个扇形也可以变成一个风车,这里我使用贝塞尔曲线来绘制风车,使用到的函数是quadraticBezierTo,这个一般是拿来绘制二阶曲线的,首先我们先来画第一个叶片


image.png

我们看到这里的控制点选择了画布中心,以及左上角和画布上沿的中点这三个位置,得到的这样我们就获得了风车的一个叶片了


image.png

我们再用同样的方式画出其余三个叶片,代码如下


image.png
image.png

一个风车就画出来了,是不是很快,现在就是如何让风车动起来了,这个我们可以使用之前说到的函数graphicsLayer,并且使用rotationZ来实现旋转,但是如果仅仅只是z轴上的旋转的话,还可以使用另一个函数rotate,它里面默认就是调用的graphicsLayer函数


image.png

现在可以在上层调用Windcar函数,并让它转起来


image.png
0625aa9.gif

稍作优化一下,给风车加个手持棍子,这个只需要将Windcar函数与一个Spacer组件套在一个Box里面就好了


image.png

这样我们的风车也完成了,最终效果如下


0625aa10.gif

总结


有没有觉得用Compose就能简简单单做出以前必须找设计师要切图或者json文件才能实现的动效呢,我们不妨也去试试看,把自己项目中那些loading动效用Compose去实现一下。


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

Android适配:判断机型和系统

在Android开发中,我们总是会碰到各种各样的适配问题。如果要解决适配问题,我们必须就要解决,出现问题的是什么机型?出现问题的是什么系统?怎么判断当前机型是不是出问题的机型?这几个问题。这篇文章,就将介绍如何判断机型和系统,介绍目前应该如何解决这些问题。 判...
继续阅读 »

在Android开发中,我们总是会碰到各种各样的适配问题。如果要解决适配问题,我们必须就要解决,出现问题的是什么机型?出现问题的是什么系统?怎么判断当前机型是不是出问题的机型?这几个问题。这篇文章,就将介绍如何判断机型和系统,介绍目前应该如何解决这些问题。


判断指定的机型


在Android里面可以通过 android.os.Build这个类获取相关的机型信息,它的参数如下:(这里以一加的手机为例)

Build.BOARD = lahaina
Build.BOOTLOADER = unknown
Build.BRAND = OnePlus //品牌名
Build.CPU_ABI = arm64-v8a
Build.CPU_ABI2 =
Build.DEVICE = OP5154L1
Build.DISPLAY = MT2110_13.1.0.100(CN01) //设备版本号
Build.FINGERPRINT = OnePlus/MT2110_CH/OP5154L1:13/TP1A.220905.001/R.1038728_2_1:user/release-keys
Build.HARDWARE = qcom
Build.HOST = dg02-pool03-kvm97
Build.ID = TP1A.220905.001
Build.IS_DEBUGGABLE = false
Build.IS_EMULATOR = false
Build.MANUFACTURER = OnePlus //手机制造商
Build.MODEL = MT2110 //手机型号
Build.ODM_SKU = unknown
Build.PERMISSIONS_REVIEW_REQUIRED = true
Build.PRODUCT = MT2110_CH //产品名称
Build.RADIO = unknown
Build.SERIAL = unknown
Build.SKU = unknown
Build.SOC_MANUFACTURER = Qualcomm
Build.SOC_MODEL = SM8350
Build.SUPPORTED_32_BIT_ABIS = [Ljava.lang.String;@fea6460
Build.SUPPORTED_64_BIT_ABIS = [Ljava.lang.String;@3a22d19
Build.SUPPORTED_ABIS = [Ljava.lang.String;@2101de
Build.TAGS = release-keys
Build.TIME = 1683196675000
Build.TYPE = user
Build.UNKNOWN = unknown
                                                                            Build.USER = root

其中重要的属性已经设置了注释,所有的属性可以看官方文档。在这些属性中,我们一般使用 Build.MANUFACTURER 来判断手机厂商,使用 Build.MODEL 来判断手机的型号。


tips: 如果你是使用kotlin开发,可以使用 android.os.Build::class.java.fields.map { "Build.${it.name} = ${it.get(it.name)}"}.joinToString("\n") 方便的获取所有的属性


上面的获取机型的代码在鸿蒙系统(HarmonyOS)上也同样适用,下面是在华为P50 Pro的机型上测试打印的日志信息:


Build.BOARD = JAD
Build.BOOTLOADER = unknown
Build.BRAND = HUAWEI
Build.CPU_ABI = arm64-v8a
Build.CPU_ABI2 = 
Build.DEVICE = HWJAD
Build.DISPLAY = JAD-AL50 2.0.0.225(C00E220R3P4)
Build.FINGERPRINT = HUAWEI/JAD-AL50/HWJAD:10/HUAWEIJAD-AL50/102.0.0.225C00:user/release-keys
Build.FINGERPRINTEX = HUAWEI/JAD-AL50/HWJAD:10/HUAWEIJAD-AL50/102.0.0.225C00:user/release-keys
Build.HARDWARE = kirin9000
Build.HIDE_PRODUCT_INFO = false
Build.HOST = cn-east-hcd-4a-d3a4cb6341634865598924-6cc66dddcd-dcg9d
Build.HWFINGERPRINT = ///JAD-LGRP5-CHN 2.0.0.225/JAD-AL50-CUST 2.0.0.220(C00)/JAD-AL50-PRELOAD 2.0.0.4(C00R3)//
Build.ID = HUAWEIJAD-AL50
Build.IS_CONTAINER = false
Build.IS_DEBUGGABLE = false
Build.IS_EMULATOR = false
Build.IS_ENG = false
Build.IS_TREBLE_ENABLED = true
Build.IS_USER = true
Build.IS_USERDEBUG = false
Build.MANUFACTURER = HUAWEI
Build.MODEL = JAD-AL50
Build.NO_HOTA = false
Build.PERMISSIONS_REVIEW_REQUIRED = true
Build.PRODUCT = JAD-AL50
Build.RADIO = unknown
Build.SERIAL = unknown
Build.SUPPORTED_32_BIT_ABIS = [Ljava.lang.String;@a90e093
Build.SUPPORTED_64_BIT_ABIS = [Ljava.lang.String;@8ce98d0
Build.SUPPORTED_ABIS = [Ljava.lang.String;@366a0c9
Build.TAGS = release-keys
Build.TIME = 1634865882000
Build.TYPE = user
Build.UNKNOWN = unknown

判断手机厂商的代码如下:

//是否是荣耀设备
fun isHonorDevice() = Build.MANUFACTURER.equals("HONOR", ignoreCase = true)
//是否是小米设备
fun isXiaomiDevice() = Build.MANUFACTURER.equals("Xiaomi", ignoreCase = true)
//是否是oppo设备
//realme 是oppo的海外品牌后面脱离了;一加是oppo的独立运营品牌。因此判断
//它们是需要单独判断
fun isOppoDevice() = Build.MANUFACTURER.equals("OPPO", ignoreCase = true)
//是否是一加手机
fun isOnePlusDevice() = Build.MANUFACTURER.equals("OnePlus", ignoreCase = true)
//是否是realme手机
fun isRealmeDevice() = Build.MANUFACTURER.equals("realme", ignoreCase = true)
//是否是vivo设备
fun isVivoDevice() = Build.MANUFACTURER.equals("vivo", ignoreCase = true)
//是否是华为设备
fun isHuaweiDevice() = Build.MANUFACTURER.equals("HUAWEI", ignoreCase = true)

需要判断指定的型号的代码如下:

//判断是否是小米12s的机型
fun isXiaomi12S() = isXiaomiDevice() && Build.MODEL.contains("2206123SC") //xiaomi 12s

如果你不知道对应机型的型号,可以看基于谷歌维护的表格,支持超过27,000台设备。如下图所示:



判断手机的系统


除了机型外,适配过程中我们还需要考虑手机的系统。但是相比于手机机型,手机的系统的判断就没有统一的方式。下面介绍几个常用的os的判断


● 鸿蒙

private static final String HARMONY_OS = "harmony";
/**
* check the system is harmony os
*
* @return true if it is harmony os
*/
public static boolean isHarmonyOS() {
    try {
        Class clz = Class.forName("com.huawei.system.BuildEx");
        Method method = clz.getMethod("getOsBrand");
        return HARMONY_OS.equals(method.invoke(clz));
    } catch (ClassNotFoundException e) {
        Log.e(TAG, "occured ClassNotFoundException");
    } catch (NoSuchMethodException e) {
        Log.e(TAG, "occured NoSuchMethodException");
    } catch (Exception e) {
        Log.e(TAG, "occur other problem");
    }
    return false;
}

● Miui

fun checkIsMiui() = !TextUtils.isEmpty(getSystemProperty("ro.miui.ui.version.name"))

private fun getSystemProperty(propName: String): String? {
    val line: String
    var input: BufferedReader? = null
    try {
        val p = Runtime.getRuntime().exec("getprop $propName")
        input = BufferedReader(InputStreamReader(p.inputStream), 1024)
        line = input.readLine()
        input.close()
    } catch (ex: IOException) {
        Log.i(TAG, "Unable to read sysprop $propName", ex)
        return null
    } finally {
        if (input != null) {
            try {
                input.close()
            } catch (e: IOException) {
                Log.i(TAG, "Exception while closing InputStream", e)
            }
        }
    }
    return line
}

● Emui 或者 Magic UI


Emui是2018之前的荣耀机型的os,18年之后是 Magic UI。历史关系如下图所示:



判断的代码如下。需要注意是对于Android 12以下的机型,官方文档并没有给出对于的方案,下面代码的方式是从网上找的,目前测试了4台不同的机型,均可正在判断。

fun checkIsEmuiOrMagicUI(): Boolean {
    return if (Build.VERSION.SDK_INT >= 31) {
//官方方案,但是只适用于api31以上(Android 12)
        try {
            val clazz = Class.forName("com.hihonor.android.os.Build")
            Log.d(TAG, "clazz = " + clazz)
            true
        }catch (e: ClassNotFoundException) {
            Log.d(TAG, "no find class")
            e.printStackTrace()
            false
        }
    } else {
//网上方案,测试了 荣耀畅玩8C
// 荣耀20s、荣耀x40 、荣耀v30 pro 四台机型,均可正常判断
        !TextUtils.isEmpty(getSystemProperty("ro.build.version.emui"))
    }
}

● Color Os


下面是网上判断是否是Oppo的ColorOs的代码。经测试,在 OPPO k10 、 oppo findx5 pro、 oneplus 9RT 手机上都是返回 false,只有在 realme Q3 pro 机型上才返回了 true。

//这段代码是错的
fun checkIsColorOs() = !TextUtils.isEmpty(getSystemProperty("ro.build.version.opporom"))

从测试结果可以看出上面这段代码是错的,但是在 ColorOs 的官网上,没有找到如何判断ColorOs的代码。这种情况下,有几种方案:


1.  判断手机制造商,即 Build.MANUFACTURER 如果为 oneplus、oppo、realme就认为它是ColorOs


2.  根据系统应用的包名判断,即判断是否带有 com.coloros.* 的系统应用,如果有,就认为它是ColorOs


这几种方案都有很多问题,暂时没有找到更好的解决方法。


● Origin Os

//网上代码,在 IQOQ Neo5、vivo Y50、 vivo x70三种机型上
//都可以正常判断
fun checkIsOriginOs() = !TextUtils.isEmpty(getSystemProperty("ro.vivo.os.version"))

总结


对于手机厂商和机型,我们可以通过Android原生的 android.os.Build  类来判断。使用 Build.MANUFACTURER 来判断手机厂商,使用 Build.MODEL 来判断手机的型号。如果是不知道的型号,还可以尝试在谷歌维护的手机机型表格中查询。


但是对于厂商的系统,就没有统一的判断方法了。部分厂商有官方提供的判断方式,如Miui、Magic UI;部分厂商暂时没有找到相关的内容。这种情况下,只能通过网上的方式判断,但是部分内容也不靠谱,如判断Oppo的ColorOs。如果你有靠谱的方式,欢迎补充。


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

如果启动一个未注册的Activity

简述 要启动未注册的Activity主要是要逃避AMS的检测,思路是,检测前要启动的Activity换成注册的,检测通过了,再在启动前换回来。这里主要是两个点。检测前,hookAMS。检测后hookHandler。hook点有很多尽量找静态变量、单例和publ...
继续阅读 »

简述


要启动未注册的Activity主要是要逃避AMS的检测,思路是,检测前要启动的Activity换成注册的,检测通过了,再在启动前换回来。这里主要是两个点。检测前,hookAMS。检测后hookHandler。hook点有很多尽量找静态变量单例public


hookAMS


1、android 11举例,启动acitivty是在ATMS中(11之前是AMS,这个自己可以去适配)


image.png


2、拿到ATMS的代理。


3、然后ATMS整个动态代理在startActivity之前将Intent 偷梁换柱


4、换成已经注册的Activity之后记得原目标Acitivty存起来,在骗完AMS之后换回来

 
public static void hookAMS() {
// 10之前
try {
Class<?> clazz = Class.forName("android.app.ActivityTaskManager");
Field singletonField = clazz.getDeclaredField("IActivityTaskManagerSingleton");

singletonField.setAccessible(true);
Object singleton = singletonField.get(null);




Class<?> singletonClass = Class.forName("android.util.Singleton");
Field mInstanceField = singletonClass.getDeclaredField("mInstance");
mInstanceField.setAccessible(true);
Method getMethod = singletonClass.getMethod("get");
Object mInstance = getMethod.invoke(singleton);

Class IActivityTaskManagerClass = Class.forName("android.app.IActivityTaskManager");

Object mInstanceProxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class[]{IActivityTaskManagerClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

if ("startActivity".equals(method.getName())) {
int index = -1;

// 获取 Intent 参数在 args 数组中的index值
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Intent) {
index = i;
break;
}
}
// 生成代理proxyIntent -- 孙悟空(代理)的Intent
Intent proxyIntent = new Intent();
// 这个包名是宿主的
proxyIntent.setClassName("com.leo.amsplugin",
ProxyActivity.class.getName());

// 原始Intent能丢掉吗?保存原始的Intent对象
Intent intent = (Intent) args[index];
proxyIntent.putExtra(TARGET_INTENT, intent);

// 使用proxyIntent替换数组中的Intent
args[index] = proxyIntent;
}

// 原来流程
return method.invoke(mInstance, args);
}
});

// 用代理的对象替换系统的对象
mInstanceField.set(singleton, mInstanceProxy);
} catch (Exception e) {
e.printStackTrace();
}
}

hookHandler


hookAMS完成,欺骗了AMS,接下来要把Intent中的原目标扶起回正位,
启动Activity要用handler,我们从这里hook吧


1、Activtiy thread 中的handler用来启动activity class H extends Handler


2、handlerMessage中的EXECUTE_TRANSACTION(159)来启动activity


3、
final ClientTransaction transaction = (ClientTransaction) msg.obj;--包含Intent


mTransactionExecutor.execute(transaction);--执行启动


launchActivityItem中有Intent,而ta继承于ClientTransactionItem,而ClientTransaction中包含List<ClientTransactionItem>


4、所以我只要拿到msg就可以拿到Intent
msg.obj --> ClientTransaction --> List mActivityCallbacks(LaunchActivityItem)
--> private Intent mIntent 替换


image.png


5、handlerMessage(MSG)之前有个callback也可以拿到msg。则会callback是一个接口,如果重写这个接口可就可重新handlerMessage这个方法,然后操作msg。


6、ActivityThread当中,Handler的构建没有传参数。

...//去ActivityThread.java里看
@UnsupportedAppUsage
final H mH = new H();
...
class H extends Handler //也没写构造方法

...//去Handler.java里看

@Deprecated
public Handler() {
this(null, false);
}

7、实际上callback是看,那么我自己替换系统的call就可以啦


8、那我通过反射拿Handler中的mCallback

 public void hoodHandler() {
try {
Class<?> clazz = Class.forName("android.app.ActivityThread");
Field activityThreadField = clazz.getDeclaredField("sCurrentActivityThread");
activityThreadField.setAccessible(true);
Object activityThread = activityThreadField.get(null);

Field mHField = clazz.getDeclaredField("mH");
mHField.setAccessible(true);
final Handler mH = (Handler) mHField.get(activityThread);

Field mCallbackField = Handler.class.getDeclaredField("mCallback");
mCallbackField.setAccessible(true);

mCallbackField.set(mH, new Handler.Callback() {

@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case 159:
// msg.obj = ClientTransaction
try {
// 获取 List<ClientTransactionItem> mActivityCallbacks 对象
Field mActivityCallbacksField = msg.obj.getClass()
.getDeclaredField("mActivityCallbacks");
mActivityCallbacksField.setAccessible(true);
List mActivityCallbacks = (List) mActivityCallbacksField.get(msg.obj);

for (int i = 0; i < mActivityCallbacks.size(); i++) {
// 打印 mActivityCallbacks 的所有item:
//android.app.servertransaction.WindowVisibilityItem
//android.app.servertransaction.LaunchActivityItem

// 如果是 LaunchActivityItem,则获取该类中的 mIntent 值,即 proxyIntent
if (mActivityCallbacks.get(i).getClass().getName()
.equals("android.app.servertransaction.LaunchActivityItem")) {
Object launchActivityItem = mActivityCallbacks.get(i);
Field mIntentField = launchActivityItem.getClass()
.getDeclaredField("mIntent");
mIntentField.setAccessible(true);
Intent proxyIntent = (Intent) mIntentField.get(launchActivityItem);

// 获取启动插件的 Intent,并替换回来
Intent intent = proxyIntent.getParcelableExtra(TARGET_INTENT);
if (intent != null) {
mIntentField.set(launchActivityItem, intent);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
break;
}
return false;
}
});
} catch (Exception e) {
e.printStackTrace();
}

}

总结


一个分为两步


1、hookAMS主要就是逃避ams检测,让ams检测的是一个已经注册了的activity。


2、hookHandler在生成activity之前再把activity换回来。


所以一定要熟悉动态代理,反射和Activity的启动流程。


主要通过hook,核心在于hook点


插桩
1、尽量找 静态变量 单利
2、public


动态代理


AMS检测之前我改下


image.png


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

Android 冷启动优化的3个小案例

背景 为了提高App的冷启动耗时,除了在常规的业务侧进行耗时代码优化之外,为了进一步缩短启动耗时,需要在纯技术测做一些优化探索,本期我们从类预加载、Retrofit 、ARouter方面进行了进一步的优化。从测试数据上来看,这些优化手段的收益有限,可能在中端机...
继续阅读 »

背景


为了提高App的冷启动耗时,除了在常规的业务侧进行耗时代码优化之外,为了进一步缩短启动耗时,需要在纯技术测做一些优化探索,本期我们从类预加载、Retrofit 、ARouter方面进行了进一步的优化。从测试数据上来看,这些优化手段的收益有限,可能在中端机上加起来也不超过50ms的收益,但为了冷启动场景的极致优化,给用户带来更好的体验,任何有收益的优化手段都是值得尝试的。


类预加载


一个类的完整加载流程至少包括 加载、链接、初始化,而类的加载在一个进程中只会触发一次,因此对于冷启动场景,我们可以异步加载原本在启动阶段会在主线程触发类加载过程的类,这样当原流程在主线程访问到该类时就不会触发类加载流程。


Hook ClassLoader 实现


在Android系统中,类的加载都是通过PathClassLoader 实现的,基于类加载的父类委托机制,我们可以通过Hook PathClassLoader 修改其默认的parent 来实现。


首先我们创建一个MonitorClassLoader 继承自PathClassLoader,并在其内部记录类加载耗时

class MonitorClassLoader(
dexPath: String,
parent: ClassLoader, private val onlyMainThread: Boolean = false,
) : PathClassLoader(dexPath, parent) {

val TAG = "MonitorClassLoader"

override fun loadClass(name: String?, resolve: Boolean): Class<*> {
val begin = SystemClock.elapsedRealtimeNanos()
if (onlyMainThread && Looper.getMainLooper().thread!=Thread.currentThread()){
return super.loadClass(name, resolve)
}
val clazz = super.loadClass(name, resolve)
val end = SystemClock.elapsedRealtimeNanos()
val cost = end - begin
if (cost > 1000_000){
Log.e(TAG, "加载 ${clazz} 耗时 ${(end - begin) / 1000} 微秒 ,线程ID ${Thread.currentThread().id}")
} else {
Log.d(TAG, "加载 ${clazz} 耗时 ${(end - begin) / 1000} 微秒 ,线程ID ${Thread.currentThread().id}")
}
return clazz;

}
}

之后,我们可以在Application attach阶段 反射替换 application实例的classLoader 对应的parent指向。


核心代码如下:

    companion object {
@JvmStatic
fun hook(application: Application, onlyMainThread: Boolean = false) {
val pathClassLoader = application.classLoader
try {
val monitorClassLoader = MonitorClassLoader("", pathClassLoader.parent, onlyMainThread)
val pathListField = BaseDexClassLoader::class.java.getDeclaredField("pathList")
pathListField.isAccessible = true
val pathList = pathListField.get(pathClassLoader)
pathListField.set(monitorClassLoader, pathList)

val parentField = ClassLoader::class.java.getDeclaredField("parent")
parentField.isAccessible = true
parentField.set(pathClassLoader, monitorClassLoader)
} catch (throwable: Throwable) {
Log.e("hook", throwable.stackTraceToString())
}
}
}

主要逻辑为



  • 反射获取原始 pathClassLoader 的 pathList

  • 创建MonitorClassLoader,并反射设置 正确的 pathList

  • 反射替换 原始pathClassLoader的 parent指向 MonitorClassLoader实例


这样,我们就获取启动阶段的加载类了



基于JVMTI 实现


除了通过 Hook ClassLoader的方案实现,我们也可以通过JVMTI 来实现类加载监控。关于JVMTI 可参考之前的文章 juejin.cn/post/694278…


通过注册ClassPrepare Callback, 可以在每个类Prepare阶段触发回调。




当然这种方案,相比 Hook ClassLoader 还是要繁琐很多,不过基于JVMTI 还可以做很多其他更强大的事。


类预加载实现


目前应用通常都是多模块的,因此我们可以设计一个抽象接口,不同的业务模块可以继承该抽象接口,定义不同业务模块需要进行预加载的类。

/**
* 资源预加载接口
*/
public interface PreloadDemander {
/**
* 配置所有需要预加载的类
* @return
*/
Class[] getPreloadClasses();
}

之后在启动阶段收集所有的 Demander实例,并触发预加载

/**
* 类预加载执行器
*/
object ClassPreloadExecutor {


private val demanders = mutableListOf<PreloadDemander>()

fun addDemander(classPreloadDemander: PreloadDemander) {
demanders.add(classPreloadDemander)
}

/**
* this method shouldn't run on main thread
*/
@WorkerThread fun doPreload() {
for (demander in localDemanders) {
val classes = demander.preloadClasses
classes.forEach {
val classLoader = ClassPreloadExecutor::class.java.classLoader
Class.forName(it.name, true, classLoader)
}
}
}

}

收益


第一个版本配置了大概90个类,在终端机型测试数据显示 这些类的加载需要消耗30ms左右的cpu时间,不同类加载的消耗时间差异主要来自于类的复杂度 比如继承体系、字段属性数量等, 以及类初始化阶段的耗时,比如静态成员变量的立即初始化、静态代码块的执行等。


方案优化思考


我们目前的方案 配置的具体类列表来源于手动配置,这种方案的弊端在于,类的列表需要开发维护,在版本快速迭代变更的情况下 维护成本较大, 并且对于一些大型App,存在着非常多的AB实验条件,这也可能导致不同的用户在类加载上是会有区别的。


在前面的小节中,我们介绍了使用自定义的 ClassLoader可以手动收集 启动阶段主线程的类列表,那么 我们是否可以在端上 每次启动时 自动收集加载的类,如果发现这个类不在现有 的名单中 则加入到名单,在下次启动时进行预加载。 当然 具体的策略还需要做详细设计,比如 控制预加载名单的列表大小, 被加入预加载名单的类最低耗时阈值, 淘汰策略等等。


Retrofit ServiceMethod 预解析注入


背景


Retrofit 是目前最常用的网络库框架,其基于注解配置的网络请求方式及Adapter的设计模式大大简化了网络请求的调用方式。 不过其并没有采用类似APT的方式在编译时生成请求代码,而是采用运行时解析的方式。


当我们调用Retrofit.create(final Class service) 函数时,会生成一个该抽象接口的动态代理实例。



接口的所有函数调用都会被转发到该动态代理对象的invoke函数,最终调用loadServiceMethod(method).invoke 调用。



在loadServiceMethod函数中,需要解析原函数上的各种元信息,包括函数注解、参数注解、参数类型、返回值类型等信息,并最终生成ServiceMethod 实例,对原接口函数的调用其实最终触发的是 这个生成的ServiceMethod invoke函数的调用。


从源码实现上可以看出,对ServiceMethod的实例做了缓存处理,每个Method 对应一个ServiceMethod。


耗时测试


这里我模拟了一个简单的 Service Method, 并调用archiveStat 观察首次调用及其后续调用的耗时,注意这里的调用还未触发网络请求,其返回的是一个Call对象。




从测试结果上看,首次调用需要触发需要消耗1.7ms,而后续的调用 只需要消耗50微妙左右。



优化方案


由于首次调用接口函数需要触发ServiceMethod实例的生成,这个过程比较耗时,因此优化思路也比较简单,收集启动阶段会调用的 函数,提前生成ServiceMethod实例并写入到缓存中。


serviceMethodCache 的类型本身是ConcurrentHashMap,所以它是并发安全的。



但是源码中 进行ServiceMethod缓存判断的时候 还是以 serviceMethodCache为Lock Object 进行了加锁,这导致 多线程触发同时首次触发不同Method的调用时,存在锁等待问题



这里首先需要理解为什么这里需要加锁,其目的也是因为parseAnnotations 是一个好事操作,这里是为了实现类似 putIfAbsent的完全原子性操作。 但实际上这里加锁可以以 对应的Method类型为锁对象,因为本身不同Method 对应的ServiceMethod实例就是不同的。 我们可以修改其源码的实现来避免这种场景的锁竞争问题。




当然针对我们的优化场景,其实不修改源码也是可以实现的,因为 ServiceMethod.parseAnnotations 是无锁的,毕竟它是一个纯函数。 因此我们可以在异步线程调用parseAnnotations 生成ServiceMethod 实例,之后通过反射 写入 Retrofit实例的 serviceMethodCache 中。这样存在的问题是 不同线程可能同时触发了一个Method的解析注入,但 由于serviceMethodCache 本身就是线程安全的,所以 它只是多做了一次解析,对最终结果并无影响。


ServiceMethod.parseAnnotations是包级私有的,我们可以在当前工程创建一个一样的包,这样就可以直接调用该函数了。 核心实现代码如下

package retrofit2

import android.os.Build
import timber.log.Timber
import java.lang.reflect.Field
import java.lang.reflect.Method
import java.lang.reflect.Modifier

object RetrofitPreloadUtil {
private var loadServiceMethod: Method? = null
var initSuccess: Boolean = false
// private var serviceMethodCacheField:Map<Method,ServiceMethod<Any>>?=null
private var serviceMethodCacheField: Field? = null

init {
try {
serviceMethodCacheField = Retrofit::class.java.getDeclaredField("serviceMethodCache")
serviceMethodCacheField?.isAccessible = true
if (serviceMethodCacheField == null) {
for (declaredField in Retrofit::class.java.declaredFields) {
if (Map::class.java.isAssignableFrom(declaredField.type)) {
declaredField.isAccessible =true
serviceMethodCacheField = declaredField
break
}
}
}
loadServiceMethod = Retrofit::class.java.getDeclaredMethod("loadServiceMethod", Method::class.java)
loadServiceMethod?.isAccessible = true
} catch (e: Exception) {
initSuccess = false
}
}

/**
* 预加载 目标service 的 相关函数,并注入到对应retrofit实例中
*/
fun preloadClassMethods(retrofit: Retrofit, service: Class<*>, methodNames: Array<String>) {
val field = serviceMethodCacheField ?: return
val map = field.get(retrofit) as MutableMap<Method,ServiceMethod<Any>>

for (declaredMethod in service.declaredMethods) {
if (!isDefaultMethod(declaredMethod) && !Modifier.isStatic(declaredMethod.modifiers)
&& methodNames.contains(declaredMethod.name)) {
try {
val parsedMethod = ServiceMethod.parseAnnotations<Any>(retrofit, declaredMethod) as ServiceMethod<Any>
map[declaredMethod] =parsedMethod
} catch (e: Exception) {
Timber.e(e, "load method $declaredMethod for class $service failed")
}
}
}

}

private fun isDefaultMethod(method: Method): Boolean {
return Build.VERSION.SDK_INT >= 24 && method.isDefault;
}

}

预加载名单收集


有了优化方案后,还需要收集原本在启动阶段会在主线程进行Retrofit ServiceMethod调用的列表, 这里采取的是字节码插桩的方式,使用的LancetX 框架进行修改。



目前名单的配置是预先收集好,在配置中心进行配置,运行时根据配置中写的配置 进行预加载。 这里还可以提供其他的配置方案,比如 提供一个注解用于标注该Retrofit函数需要进行预解析,



之后,在编译期间收集所有需要预加载的Service及函数,生成对应的名单,不过这个方案需要一定开发成本,并且需要去修改业务模块的代码,目前的阶段还处于验证收益阶段,所以暂未实施。


收益


App收集了启动阶段20个左右的Method 进行预加载,预计提升10~20ms。


ARouter


背景


ARouter框架提供了路由注册跳转 及 SPI 能力。为了优化冷启动速度,对于某些服务实例可以在启动阶段进行预加载生成对应的实例对象。


ARouter的注册信息是在预编译阶段(基于APT) 生成的,在编译阶段又通过ASM 生成对应映射关系的注入代码。



而在运行时以获取Service实例为例,当调用navigation函数获取实例最终会调用到 completion函数。



当首次调用时,其对应的RouteMeta 实例尚未生成,会继续调用 addRouteGroupDynamic函数进行注册。



addRouteGroupDynamic 会创建对应预编译阶段生成的服务注册类并调用loadInto函数进行注册。而某些业务模块如何服务注册信息比较多,这里的loadInto就会比较耗时。



整体来看,对于获取Service实例的流程, completion的整个流程 涉及到 loadInto信息注册、Service实例反射生成、及init函数的调用。 而completion函数是synchronized的,因此无法利用多线程进行注册来缩短启动耗时。


优化方案


这里的优化其实和Retroift Service 的注册机制类似,不同的Service注册时,其对应的元信息类(IRouteGroup)其实是不同的,因此只需要对对应的IRouteGroup加锁即可。 另外由于这部分代码现在可能多线程同时在进行,部分逻辑需要进行二次判断,


image.png


在completion的后半部分流程中,针对Provider实例生产的流程也需要进行单独加锁,避免多次调用init函数。



收益


根据线下收集的数据 配置了20+预加载的Service Method, 预期收益 10~20ms (中端机) 。


其他


后续将继续结合自身业务现状以及其他一线大厂分享的样例,在 x2c、class verify、禁用JIT、 disableDex2AOT等方面继续尝试优化。


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

这一曲终落泉城,也始于泉城

22年的春季站在岔路口四顾茫然,灵魂里的信念仿佛已被层层迷雾遮蔽。与大多人一样,我开始了考研的复习,但是到暑假之前的种种原因,让我深刻认识到自己未来想走的路是什么,我坚信无论怎样未来还是会开启自己的代码人生。也许是冥冥之中的巧合,随意投了一份简历之后,见鬼了一...
继续阅读 »

22年的春季站在岔路口四顾茫然,灵魂里的信念仿佛已被层层迷雾遮蔽。与大多人一样,我开始了考研的复习,但是到暑假之前的种种原因,让我深刻认识到自己未来想走的路是什么,我坚信无论怎样未来还是会开启自己的代码人生。也许是冥冥之中的巧合,随意投了一份简历之后,见鬼了一样收到了一个offer,经过权衡我更加坚定了自己的选择提前入海。随后梁溪城(无锡)下开始了自己的第一份实习。


f4f07a90ecd1bfb57309dcc4b4cd243.jpg
仔细室下的春季


梁溪城内边余尺,姑苏城外不姑苏。


初出茅庐,满腔热血腾。初入社会的心态是充满着好奇和壮志满怀的,有点类似于刘姥姥进大观园的样子,但这也是大多数从农村走入社会该有的样子。


2158fb3d035ce57708e85a34b30fffc.jpg
梁溪城晴


然而实习的日子没过多久,就出现了疫情开始了居家办公的日子,居家办公的生活还挺好,哈哈,自己一直处于电脑从来没有开启的状态。但疫情下的生活也并不是很好,吃饭只能去吃盒饭,供应的关系失衡使得饭的质量以及价格都不尽人意,好在没过多久疫情就结束了,线下上班,开始接触微服务,此时此刻java狗也算是开始见识了传呼其神的微服务。

d59cf7d2e8dc4d2ab01833fc670cdb4.jpg
梁溪城雨


渴望改变的灵魂,安逸是禁锢。舒适的日子久了,总想着外面更广阔的天空,这也许是大多数青年的想法。由于长时间没有机会接触到实际的开发中,感觉天天浑浑噩噩,想着要改变的决心。第一份实习在草草的两个月结束,下一站钱塘(杭州)下看西湖。

7f0c0e7d6e1d1f29f8c735f54e15292.jpg
梁溪城黄昏


钱塘西湖波光粼,芜湖花街夜亮灯。


西湖悠悠水溢情,钱塘无情终相别。当一个人努力过后,最后没有得到自己想要的,会有少许的悲伤,微微一笑过去的终将过去。好在实习的最后一站遇到很多好的人,也算是不虚此行。


bfd71a4c180e34a9e3b8071e534ed1d.jpg
钱塘西湖黄昏


南行的最后一站,芜湖。偶然的巧合有机会去到芜湖,此次的经历也是让我想出行的心付出了行动,迈出了步伐(之前是一直很宅那种)。

961bea13150d5bc498a5634b8b80a7d.jpg
芜湖花街


白日炎炎游北平,夜幕潇潇离别行。


毕业最后一站,北京。这一站也是收获满满。现在也可以在别人面前zb,我可是见过北京城的人,虽然没有去到长城,我不是好汉,哈哈哈。


5079c035d17d5bb1e8d2c17153b8620.jpg
天坛晴


ef520aeac9d951a89abad90c78fe9d1.jpg
北京夜


曲终人落泉阳城,大明湖畔少年情。


毕业后的第一站,大明湖畔泉城。


08e656a0990b0f61557c4275163c6c8.jpg
大明湖夜


回首向来萧瑟处,归去,也无风雨也无晴。


回望过去一年,入世从懵懵懂懂到懵懂,个人的心态,观念都在改变。只要是自己想做的,认为可做的,毫不手软的作出选择,不再迷茫,不再畏手畏脚,同时也是一直坚信自己的选择。看待问题的角度,不能只看那些不好的一面,也要看到好的一面,这样的心境才会是乐观豁达。一个人真正的走向成熟的标志,就是他不愿意越来越多说,而是学会适当的闭嘴。


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

骨灰级程序员那些年曾经告诉我们的高效学习的态度

一、背景 近日听闻某骨灰级程序员突然与世长辞,记得再2019年的时候有幸读到他的专栏,专栏中关于如何高效学习的总结让我收货颇丰,老师说: 学习是一件“逆人性”的事,就像锻炼身体一样,需要人持续付出,会让人感到痛苦,并随时想找理由放弃。 大部分人都认为自己爱学习...
继续阅读 »

一、背景


近日听闻某骨灰级程序员突然与世长辞,记得再2019年的时候有幸读到他的专栏,专栏中关于如何高效学习的总结让我收货颇丰,老师说:


学习是一件“逆人性”的事,就像锻炼身体一样,需要人持续付出,会让人感到痛苦,并随时想找理由放弃。


大部分人都认为自己爱学习,但是:



  • 他们都是只有意识没有行动,他们是动力不足的人。

  • 他们都不知道自己该学什么,他们缺乏方向和目标。

  • 他们都不具备自主学习的能力,没有正确的方法和技能。

  • 更要命的是,他们缺乏实践和坚持。


对于学习首先需要做的是端正态度,如果不能自律,不能坚持,不能举一反三,不能不断追问等,那么,无论有多好的方法,你都不可能学好。所以,有正确的态度很重要。



二、 主动学习和被动学习


老师关于主动学习和被动学习的见解也让我们技术人员收货颇丰。


1946 年,美国学者埃德加·戴尔(Edgar Dale)提出了「学习金字塔」(Cone of Learning)的理论。之后,美国缅因州国家训练实验室也做了相同的实验,并发布了「学习金字塔」报告。



人的学习分为「被动学习」和「主动学习」两个层次。



  • 被动学习:如听讲、阅读、视听、演示,学习内容的平均留存率为 5%、10%、20% 和 30%。

  • 主动学习:如通过讨论、实践、教授给他人,会将原来被动学习的内容留存率从 5% 提升到 50%、75% 和 90%。


这个模型很好地展示了不同学习深度和层次之间的对比。


我们可以看到,你听别人讲,或是自己看书,或是让别人演示给你,这些都不能让你真正获得学习能力,因为你是在被别人灌输,在听别人说。


你开始自己思考,开始自己总结和归纳,开始找人交流讨论,开始践行,并开始对外输出,你才会掌握到真正的学习能力。


所以,学习不是努力读更多的书,盲目追求阅读的速度和数量,这会让人产生低层次的勤奋和成长的感觉,这只是在使蛮力。要思辨,要践行,要总结和归纳,否则,你只是在机械地重复某件事,而不会有质的成长的。



好多书中也这么说:



这里推荐阅读下这本书:



三、深度学习和浅度学习


对于当前这个社会:



  • 大多数人的信息渠道都被微信朋友圈、微博、知乎、今日头条、抖音占据着。这些信息渠道中有营养的信息少之又少。

  • 大多数公司都是实行类似于 996 这样的加班文化,在透支和消耗着下一代年轻人,让他们成长不起来。

  • 因为国内互联网访问不通畅,加上英文水平受限,所以,大多数人根本没法获取到国外的第一手信息。

  • 快餐文化盛行,绝大多数人都急于速成,心态比较浮燥,对事物不求甚解。


所以,在这种环境下,你根本不需要努力的。你只需要踏实一点,像以前那样看书,看英文资料,你只需要正常学习,根本不用努力,就可以超过你身边的绝大多数人。


在这样一个时代下,种种迹象表明,快速、简单、轻松的方式给人带来的快感更强烈,而高层次的思考、思辨和逻辑则被这些频度高的快餐信息感所弱化。于是,商家们看到了其中的商机,看到了如何在这样的时代里怎么治愈这些人在学习上的焦虑,他们在想方设法地用一些手段推出各种代读、领读和听读类产品,让人们可以在短时间内体会到轻松获取知识的快感,并产生勤奋好学和成长的幻觉。


这些所谓的“快餐文化”可以让你有短暂的满足感,但是无法让你有更深层次的思考和把知识转换成自己的技能的有效路径,因为那些都是需要大量时间和精力的付出,不符合现代人的生活节奏。人们开始在朋友圈、公众号、得到等这样的地方进行学习,导致他们越学越焦虑,越学越浮燥,越学越不会思考。于是,他们成了“什么都懂,但依然过不好这一生”的状态。


只要你注意观察,就会发现,少数的精英人士,他们在训练自己获取知识的能力,他们到源头查看第一手的资料,然后,深度钻研,并通过自己的思考后,生产更好的内容。而绝大部分受众享受轻度学习,消费内容。你有没有发现,在知识的领域也有阶层之分,那些长期在底层知识阶层的人,需要等着高层的人来喂养,他们长期陷于各种谣言和不准确的信息环境中,于是就导致错误或幼稚的认知,并习惯于那些不费劲儿的轻度学习方式,从而一点点地丧失了深度学习的独立思考能力,从而再也没有能力打破知识阶层的限制,被困在认知底层翻不了身。


四、如何深度学习


有4点最关键:



  • 高质量的信息源和第一手的知识。

  • 把知识连成地图,将自己的理解反述出来。

  • 不断地反思和思辨,与不同年龄段的人讨论。

  • 举一反三,并践行之,把知识转换成技能。


从以上4点来说,学习会有三个步骤。


知识采集。信息源是非常重要的,获取信息源头、破解表面信息的内在本质、多方数据印证,是这个步骤的关键。


知识缝合。所谓缝合就是把信息组织起来,成为结构体的知识。这里,连接记忆,逻辑推理,知识梳理是很重要的三部分。


技能转换。通过举一反三、实践和练习,以及传授教导,把知识转化成自己的技能。这种技能可以让你进入更高的阶层。


我觉得这是任何人都是可以做到的,就是看你想不想做了。


五、学习的目的



5.1、学习是为了找到方法


学习不仅仅是为了找到答案,而更是为了找到方法。很多时候,尤其是中国的学生,他们在整个学生时代都喜欢死记硬背,因为他们只有一个 KPI,那就是在考试中取得好成绩,所以,死记硬背或题海战术成了他们的学习习惯。然而,在知识的海洋中,答案太多了,你是记不住那么多答案的。


只有掌握解题的思路和方法,你才算得上拥有解决问题的能力。所有的练习,所有的答案,其实都是在引导你去寻找一种“以不变应万变”的方法或能力。在这种能力下,你不需要知道答案,因为你可以用这种方法很快找到答案,找到解,甚至可以通过这样的方式找到最优解或最优雅的答案。


这就好像,你要去登一座山,一种方法是通过别人修好的路爬上去,一种是通过自己的技能找到路(或是自己修一条路)爬上去。也就是说,需要有路才爬得上山的人,和没有路能造路的人相比,后者的能力就会比前者大得多得多。所以,学习是为了找到通往答案的路径和方法,是为了拥有无师自通的能力。


5.2、学习是为了找到原理


学习不仅仅是为了知道,而更是为了思考和理解。在学习的过程中,我们不是为了知道某个事的表面是什么,而是要通过表象去探索其内在的本质和原理。真正的学习,从来都不是很轻松的,而是那种你知道得越多,你的问题就会越多,你的问题越多,你就会思考得越多,你思考得越多,你就会越觉得自己知道得越少,于是你就会想要了解更多。如此循环,是这么一种螺旋上升上下求索的状态。


但是,这种循环,会在你理解了某个关键知识点后一下子把所有的知识全部融会贯通,让你赫然开朗,此时的那种感觉是非常美妙而难以言语的。在学习的过程中,我们要不断地问自己,这个技术出现的初衷是什么?是要解决什么样的问题?为什么那个问题要用这种方法解?为什么不能用别的方法解?为什么不能简单一些?


这些问题都会驱使你像一个侦探一样去探索背后的事实和真相,并在不断的思考中一点一点地理解整个事情的内在本质、逻辑和原理。一旦理解和掌握了这些本质的东西,你就会发现,整个复杂多变的世界在变得越来越简单。你就好像找到了所有问题的最终答案似的,一通百通了。


5.3、学习是为了了解自己


学习不仅仅是为了开拓眼界,而更是为了找到自己的未知,为了了解自己。英文中有句话叫:You do not know what you do not know,可以翻译为:你不知道你不知道的东西。也就是说,你永远不会去学习你不知道其存在的东西。就好像你永远 Google 不出来你不知道的事,因为对于你不知道的事,你不知道用什么样的关键词,你不知道关键词,你就找不到你想要的知识。


这个世界上有很多东西是你不知道的,所以,学习可以让你知道自己不知道的东西。只有当我们知道有自己不知道的东西,我们才会知道我们要学什么。所以,我们要多走出去,与不同的人交流,与比自己聪明的人共事,你才会知道自己的短板和缺失,才会反过来审视和分析自己,从而明白如何提升自己。


山外有山,楼外有楼,人活着最怕的就是坐井观天,自以为是。因为这样一来,你的大脑会封闭起来,你会开始不接受新的东西,你的发展也就到了天花板。开拓眼界的目的就是发现自己的不足和上升空间,从而才能让自己成长。


5.4、学习是为了改变自己


学习不仅仅是为了成长,而更是为了改变自己。很多时候,我们觉得学习是为了自己的成长,但是其实,学习是为了改变自己,然后才能获得成长。为什么这么说呢?我们知道,人都是有直觉的,但如果人的直觉真的靠谱,那么我们就不需要学习了。而学习就是为了告诉我们,我们的很多直觉或是思维方式是不对的,不好的,不科学的。


只有做出了改变后,我们才能够获得更好的成长。你可以回顾一下自己的成长经历,哪一次你有质的成长时,不是因为你突然间开窍了,开始用一种更有效率、更科学、更系统的方式做事,然后让你达到了更高的地方。不是吗?当你学习了乘法以后,在很多场景下,就不需要用加法来统计了,你可以使用乘法来数数,效率提升百倍。


当你有一天知道了逻辑中的充要条件或是因果关系后,你会发现使用这样的方式来思考问题时,你比以往更接近问题的真相。学习是为了改变自己的思考方式,改变自己的思维方式,改变自己与生俱来的那些垃圾和低效的算法。总之,学习让我们改变自己,行动和践行,反思和改善,从而获得成长。


六、总结


**1、首先,学习是一件“逆人性”的事,就像锻炼身体一样,需要人持续付出,但会让人痛苦,并随时可能找理由放弃。如果你不能克服自己 DNA 中的弱点,不能端正自己的态度,不能自律,不能坚持,不能举一反三,不能不断追问等,那么,无论有多好的方法,你都不可能学好。因此,有正确的态度很重要。

**


2、要拥有正确的学习观念:学习不仅仅是为了找到答案,而更是为了找到方法;学习不仅仅是为了知道,而更是为了思考和理解;学习不仅仅是为了开拓眼界,而更是为了找到自己的未知,为了了解自己;学习不仅仅是为了成长,而更是为了改变自己,改变自己的思考方式,改变自己的思维方式,改变自己与生俱来的那些垃圾和低效的算法。


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