注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

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
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

程序员的三件大事(一)!护眼

脱发和近视,当代年轻人两大痛点,我个人也饱受其害; 脱发呢,最近情况稳定些,基本不掉了但是还是以很稀疏的形式存在。 护眼需求逐渐凸显出来,因为近期睡醒之后眼睛会超级干涩,转动还会痛。 确实,自从入了程序员这行,我每天的屏幕使用时间基本在10-14个小时内。 我...
继续阅读 »

脱发和近视,当代年轻人两大痛点,我个人也饱受其害;


脱发呢,最近情况稳定些,基本不掉了但是还是以很稀疏的形式存在。


护眼需求逐渐凸显出来,因为近期睡醒之后眼睛会超级干涩,转动还会痛。


确实,自从入了程序员这行,我每天的屏幕使用时间基本在10-14个小时内。


我的工作、娱乐、生活接近80%的人类行为都需要电子设备当做媒介实现。很担心自己老了之后青光眼白内障(当然也不确定能不能活到老),但适当的担心与改变十分必要。


图文无关


护眼OKR


护眼的核心就是护睫状肌!

睫状肌是眼内的一种平滑肌,主要作用是通过牵拉改变晶体形状,帮助我们迅速锚定视物时的最佳焦距。


它的工作原理也简单,看远处舒张,看近处收缩。我们一直盯着屏幕,导致它处于一个持久收缩的状态,


假性近视就是这块肌肉短时间内放松不下去,导致看远处模糊(不准确,我医学白痴,类推感觉)。时间久了就容易导致眼轴长度被改变,假近视变真近视;下面提供几种方法帮助大家适当调节睫状肌:


方法一:眨眼


正常人类,眨眼次数是20次/min。你自己想想盯代码、看文档、看番剧、打游戏是不是基本都不咋眨眼。沙漠里的蜥蜴没眼睑都知道干了用舌头舔舔眼睛,你一天上班8-9个小时,就别给泪腺放假了,好兄弟要有难同当。


可以搞个贴纸,放在电脑屏幕旁边,记得眨眼。


方法二:望远


这个小时候家长和老师基本不厌其烦的说过,用眼一小时,远眺5分钟。看看绿色,看看树。如果你像我一样,工位不在窗边,人又比较懒咋办呢。可以看看远处同事的绿植🪴,尽量找正方向的同事,一直看一边也担心会有斜视的风险(不是


如果觉得一直看同事会影响办公室气氛,没关系这还有个20-20-20法则


每隔20分钟休息20秒,目光离开屏幕,向20英尺(约合6米)以外的草地、绿树或其他物体眺望,不眯眼不眨眼,全神贯注凝视并辨认其轮廓,使眼睛处于一种活动的过程中,可起到调节灵敏度的作用。


要是没有同事,或者没有绿植,而且不想动怎么办?


这有一张远眺图,请根据下述用法服用,最好是打印出来或者淘宝买一张贴墙上(你要是说没有墙,那我真没办法了,想吃点啥吃点啥吧)


远眺图


使用说明:


1、远眺距离为1米-2.5米,每日眺望5次以上,每次3—15分钟。

2、要思想集中,认真排除干扰,精神专注,高度标准为使远眺图的中心成为使用者水平视线的中心点。

3、远眺开始,双眼看整个图表,产生向前深进的感觉,然后由外向内逐步辨认每一层的绿白线条。


方法三:睡觉


最好的护眼方法就是不用,直接闭上歇8个小时。有相关研究说要早睡(看到这我基本就不往下看了,每天不到1点睡不着人士选择性忽略这条)


方法四:促进血液循环


作为一名办公室圈养型社畜,我基本跟电脑椅是形影不离的好兄弟。久坐,导致人血液循环慢。循环一慢,这个血液输送的养分就跟不上了。而眼睛这么精密的人体构造,遍布嗷嗷待哺的眼部细胞;养分和废物全靠血液代谢。你慢了,它就危了。


咋办?促进循环,慢跑20min(你家住的离公司近的有福了,直接每天跑步上下班)。


跑步没意思可以跳绳,肥宅体重大跳绳伤膝盖可以做眼保健操,只促进眼部血液循环。


做眼保健操手酸,可以淘宝买蒸汽眼贴热敷促循环,眼贴嫌贵那就回家热水毛巾敷一样的。


勤快有勤快的玩法,懒又懒的方式。少给自己找借口,多去想想怎么做,这样我们才能相对健康地活到老年。


方法五:吃补剂


这个有点带货嫌疑,我不细说了。护眼吃胡萝卜素、叶黄素、维生素A就完事儿了。


可以食疗(吃胡萝卜、橙子、动物肝脏)


可以药补(维生素片)


方法六:买个好屏幕


IMG_1304.JPG


我公司发了一个屏幕,联想的,清晰度一般,亮度也差,看着我就浑身难受,于是就买了个LG的4K屏。


因为是敲代码所以不用买144Hz高刷,60Hz够用(又省不少钱)。色彩还原度好一些,亮度高一点就行。把联想的当竖屏看代码,LG的4K当主屏看文档和前端效果,很舒服。


写到这基本也够用了,我去拉个屎,88~


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

认识自己,曾经的选择,是否如愿?一位渗透测试工程师的自述感言!!

前言   如今,快到而立之年,想想距离毕业已经过去5年了,5年的时间做过结构设计(本专业),做过通信,做过开发,在到如今的网络安全,经历了大大小小的事情,相对于同届同龄的应该算经历比较丰富、成长比较曲折的吧!下面我就分享一下我的成长之路,给那些将要作选择还未...
继续阅读 »

前言




  如今,快到而立之年,想想距离毕业已经过去5年了,5年的时间做过结构设计(本专业),做过通信,做过开发,在到如今的网络安全,经历了大大小小的事情,相对于同届同龄的应该算经历比较丰富、成长比较曲折的吧!下面我就分享一下我的成长之路,给那些将要作选择还未作选择的人一些参照,希望能给你们照亮一些前路。




高中毕业




  高中毕业是第一次做选择的时候,也是人生当中很关键的一个选择,影响深远,当时高中也没好好学,填志愿的时候也没人指导,所以就自己想了一下,和同学一起填报某个学校,当时土木也比较火,就选了优先选了土木、自己当时也比较喜欢上网,当时也接触一些黑客技术,觉得很牛逼,但是当时也没啥概念,第二志愿就选了一个计算机,但是两个都没录取上,当时选了服从调剂,录取的是一个应用化工专业。


  当然,第一次作选择,最好有人建议,有人指导,不要自己盲目去选择,经历太少,考虑太少,选择的路可能比较弯曲、坎坷。指导建议的人当然阅历丰富,眼界宽广才行。




大学生活




  大学现在想起来是挺美好的,当时并不觉得,果然是失去才会后悔,刚入大学那会,学校给了调剂的机会,当时也没啥犹豫的,直接就调剂到建筑类的专业,就打打游戏、谈谈恋爱过了2年,考试挂科一直挂到毕业清考,2年啥也没学,大三要工作了,慌了,自己害怕找工作,家里也要往上考,正好那就专升本,想着考不上就要工作了,就坚持坚持努力一下,玩了2年,突然像高中一样学习,真是艰难、痛苦,熬过了半年,功夫不负有心人,喜提录取通知书,和我一起考的室友,没有考上,一个班二十个报名的就考上几个,都说是我运气好,运气+努力才是真的,运气是建立在努力的基础上的,所以别小看任何人,说不定某一次或某几次那些不可能会成为你眼里的奇迹。


  又来一次大学生活还是没好好珍惜,挂科、挂科还是挂科,不过最好的结果还是遇到了现在的媳妇,作为过来人说一句,大学尽量谈谈恋爱,什么工作再谈,主要是你知道你以后的工作能接触到女生吗,说不定就是一群抠脚大汉天天在一起吹牛逼,又面临毕业,又一次躲避,考研好理由,可惜没考上,时不待我,只能找工作,靠家里的关系,找个小设计院,就省去了找工作的麻烦,担了人情。


  珍惜大学生活,做些有意义的事,这些有意义当然是对你自己,以后你会发现浪费时间就是浪费钱,浪费青春,最主要是钱。




工作经历




  第一份工作本专业的结构设计,听起来挺牛逼的,比土木其他的工作要好一点,脑力劳动,也是天天加班,很累,靠关系找的工作,工资不提,就是有人教,这一点好,学是学到了不少,听老板天天画饼,从同事那了解公司的情况,半年后离职了


  离职之后,第一次自己找工作,找了一个多月,去了好几个城市,一开始找工作很自信,认为自己很牛逼,往往这都是错觉,第一次被现实狠狠的按在地上摩擦,想过转行,不过都是各种培训班,想想也是,没人要一个什么都不会的人,还给你发工资养着你,不太可能,公司要你来干活的,要不然就是物美价廉,什么都不会或者说只会一点点,技能很懂的很少,那这时候就惨了,公司宁愿要应届实习生,也不会要这样的,除非公司特别缺人,而你刚好表现的还可以,有点基础,工资要的比市场低,这种情况真的很少。慎重离职,除非你很优秀,没什么技能就不要考虑离职和工资的事。




   第二份工作开始偏离专业的通信基础结构设计,开始各种沟通,这样工作已经偏离设计了,都是生产化流程,主要是沟通,工作很轻松。因为会开车,能喝酒,跟领导关系好,跟着去应酬,开车接送客户之类的,领导也教了不少人生经验,确实学到了很多,一年的时间,接触到各个层次的人,跟不同职业层次的人闲聊,增长你的见闻和眼界,深深感受到了每个人对生活的无奈、心酸。一直的在社会的海洋里挣扎起伏,有人随波逐流、有人迎风破浪,不同的态度,不同的选择,会有不同的生活。


  厌倦了灯红酒绿,厌倦了勾心斗角,终于拨开了迷雾,认真思考自己是否喜欢、想要这样的生活,然而并非如此,毅然决然的离职,并写了一篇文章,祭奠逝去的生活。


                     看风雨,品岁月


  春风细雨述青春,夏夜繁星映心尘, 寒霜嬉染红叶落,风雪埋藏岁月痕。


  听风雨道历程,看岁月写人生。一段征程,一场梦;一首歌谣,一段情。尘世纷扰,喧声缭绕,身处凡尘缭绕的烟火,又怎能在如烟世海中找到最初的方向。每当在这物欲横流的社会中摇摆不定,又怎能想到最初的梦想。风很大,路还很长,当现实的风浪击打你的脸庞,是否还能抬头望向远方,那微弱的灯光,是否还在你的心上。一段迷茫,一段坚强,幡然醒悟的你是否还知世事无常,环顾回望那渐远的身影叹一声人走茶凉,或许还有人在远方,等你找到来时的方向。


   总有一些无知的人,随波逐流享受欢快时光。总有一些愚昧的人,蝇头小利奢取他人帮忙。总有一些善心的人,惯出那些所谓理所应当。何不曾想,自己的时光浪费在这些所谓的理所应当,拿所谓世俗中的脸面束缚自己欢乐时光。为何失去方向,为何失去梦想,叹一声,看不破的人心,用不起的善良,撕不破的脸面,得不到的奢望。


   倾城春色,终究只是繁华过往。往事惊心,不该失去应有的平静。抛开岁月的刻痕,细读风雨,抚平焦躁不安的自己,抬头看一看前方,好似有人儿在等你。


  真正的平静,不是避开车马喧嚣,而是在心中修篱种菊。尽管如流往事,每一天都涛声依旧,只要我们消除执念,便可寂静安然。愿每个人在纷呈世相中不会迷失荒径,可以端坐磐石上,醉倒落花前。


   繁事诸多,渐欲迷焉。心之所向,凡不是绿草茵茵、姹紫嫣红之地。身之所倚,须不是几亩良田、遮风避雨之所。身有所倚,莫不寻心之所向也,不畏饥辘,不知风雨 。 渐行渐远渐无书,水阔鱼沉何处问?世事终无常,谁?不知人走茶凉! — 何为向往?


   谁人不知春来到,叶绿花红人儿笑!




  第三份工作之前就是规划自己人生,把自己转行的想法告诉家人,却迎来的众多反对,唯有得到媳妇一人的支持,这时候我做出了转行的决定,自己的路自己走,不论结果如何,自己担着。


  首先我觉得现在最火的和最有前景的是互联网行业,但是互联网行业也分好多方向,无非最简单的最多的就是开发,自己内心是对黑客技术比较感兴趣,就加了各种群去问已经从业的人员,所有人的答案,最多的前提就是先学语言,语言也是比较好入门的,又去问了转行的同学培训机构的事,由于资金有限,就选择了自学,选择了java,从网上买了一套java视频,看了大半年,年后开始找了工作,找的时间也比较也大概有一个月,先在一家小公司干了后端开发大概有3个月,后来又去了现在公司做前端半年,一心想进安全,但是入门较高,只能在开发先做着,开发部门可能觉得我水平不高,工作也不怎么加班,安全部门代码审计又缺人,又招不到人,就建议我转过去,我顿时心花怒放,终于踏进了安全行业,虽然当时不知道代码审计是干啥的,先进去就对了,也是因为有开发基础吧,学安全真的很快,2个星期就能独立做代审任务了,3个月之后又开始做渗透测试,直到现在,大概工作不到2年的时间,一直在努力学习技术,不断学习前沿新的技术,现在回想,不管是从生活、工作、行业前景、工资等方面来看,都觉得当初选择是对的。


  人生没那么多时间给你迷茫,青春不容得浪费,做好规划,按规划完成人生目标,努力才能带给你更多的运气


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

三分钟,趁同事上厕所的时间,我覆盖了公司的正式环境数据

大家好啊,又跟大家见面了,最近有个需求就是批量修改公司的数据报表,正式环境!! 而且要执行update!! update it_xtgnyhcebg I set taskStatus = XXX 而且是没有加where条件的,相当于全表更新,这可马虎不得,我...
继续阅读 »

大家好啊,又跟大家见面了,最近有个需求就是批量修改公司的数据报表,正式环境!!
而且要执行update!!


update it_xtgnyhcebg I set taskStatus = XXX

而且是没有加where条件的,相当于全表更新,这可马虎不得,我们在任何操作正式数据库之前一定一定要对数据库备份!!不要问我怎么知道的,因为我就因为有一次把测试环境的数据覆盖到正式环境去了。。。


在这里插入图片描述


别到时候就后悔莫及,那是没有用的!


在这里插入图片描述
在这里插入图片描述


由于这个需求是需要在跨库操作的,所以我们在查询数据的时候需要带上库的名称,例如这样


SELECT
*
FROM
BPM00001.ACT_HI_PROCINST P
LEFT JOIN BPM00001.ACT_HI_VARINST V ON V.PROC_INST_ID_ = P.ID_
AND V.NAME_ = '__RESULE'


这样如果我们在任何一个库里面,只要在一个mysql服务里面都可以访问到这个数据
查出这个表之后
在这里插入图片描述
我们需要根据这里的内容显示出不同的东西
就例如说是“APPROVAL”我就显示“已通过”
这就类似与java中的Switch,其实sql也能实现这样的效果
如下:
在这里插入图片描述
这就是sql的case语句的使用
有了这些数据之后我们就可以更新数据表了,回到我们之前讨论过的,这是及其危险的操作
我们先把要set的值给拿出来
在这里插入图片描述


在这里插入图片描述
但是我们怎么知道这个里面的主键呢?
你如果直接这么加,肯定是不行的
在这里插入图片描述
所以我们需要在sql后面加入这样的一条语句
在这里插入图片描述
注意,这个语句一定要写在set语句的里面,这样sql就能依据里面判断的条件进行一一赋值
最后,将这个sql语句执行到生产库中


拓展:


作为查询语句的key绝对不能重复,否则会失败(找bug找了半天的人的善意提醒)
例如上面的语句中P.BUSINESS_KEY_必须要保证是唯一的!!


在这里插入图片描述
成功执行!!!
怎么样,这些sql

作者:掉头发的王富贵
来源:juejin.cn/post/7244563144671723576
的小妙招你学会了吗?

收起阅读 »

在职场中,比写好代码更重要的事,你一定要知道!

当涉及职场技能的探讨时,如何去编写高质量的代码通常是我们追求的目标。然而,随着时间的推移和思维的转变,我逐渐认识到,在职场中,有比写出优秀代码更为重要的事情存在。 这个事情不仅仅关乎我们的技术能力,而是涉及到我们的思维方式和工作方法;关乎我们能把工作做到什么样...
继续阅读 »

当涉及职场技能的探讨时,如何去编写高质量的代码通常是我们追求的目标。然而,随着时间的推移和思维的转变,我逐渐认识到,在职场中,有比写出优秀代码更为重要的事情存在。


这个事情不仅仅关乎我们的技术能力,而是涉及到我们的思维方式和工作方法;关乎我们能把工作做到什么样的级别,关乎到能给自己带来多大的成长和给公司带来多大价值。


在本篇文章中,我们将探讨逻辑思维与结构化思维。常言道:让你与众不同的不是努力,而是思维方式。所以无论你是一名开发人员、设计师、项目经理,或者在任何与技术相关的领域工作,掌握逻辑思维和结构化思维都是至关重要的。


我们为什么要掌握逻辑思维和结构化思维



在职场中,你应该见过那种初次见面便能感觉到他工作能力很强的人。



  • 听他发言,说话非常有条理

  • 看他文档目录层级清晰、各部分内容之间逻辑清晰,看他文档本身变成一种享受

  • 听他给解决方案。有框架、有依据,直击问题要害



那这种感觉是从哪来的呢?就是依托于清晰的逻辑思维与结构化思维




逻辑思维和结构化思维核心影响你与他人的关系,影响信息传递效率,影响你的个人影响力,是非常显性化的两种思维方式,是其他思维方式的基础。显性化是你只要刻意练习,你便能习得这项能力,并且很快在工作中应用得到正反馈,提高你的工作效率、加强你的沟通表达能力。


如果你的思考和表达没有逻辑与结构,这其实是一种灾难。核心不是自己会怎么样,而是非常浪费别人的时间。大家的时间都很宝贵,你浪费了别人的时间,别人怎么还会有耐心信任你。


职场人首要学习这两种思维方式,逻辑思维提高自身说服力,结构化思维提高信息传递效率。越早期越有这种意识,刻意培养自己,越能较早形成自己的核心竞争力。如果你说你不知道自己核心竞争力是什么,那么刻意培养的逻辑思维与结构化思维也可以让你在一堆人中脱颖而出。


逻辑思维


逻辑思维是指我们对问题和情况进行推理和分析的能力。它帮助我们理清思路,从复杂的问题中找到简单而合理的解决方案。当我们在编写代码时,逻辑思维帮助我们识别问题、设计算法和调试错误。然而,逻辑思维不仅仅适用于编码。在职场中,无论是解决问题、制定计划还是做出决策,逻辑思维都是我们必备的工具。


比如在日常的研发工作中,经常会出现因为逻辑不严谨导致给出的解决方案没法真正解决问题,浪费资源不说,还错过了时间窗口。有时候项目上线后看不到明显的转化,根本原因在一开始分析问题的思路就出错了,问题和解决方案之间没有形成逻辑闭环,你预想的“好”其实是错的。





业务:我们想要更换用户个人中心关于订单节点的文案。


我:为什么呢?


业务:因为我们发现当前商城的退换货满意度比天猫京东均低。


我:那低的原因是文案导致的吗?改了文案以后就可以提高退货满意度了吗?



在这个例子里可以发现业务想要做的行动跟原因并不匹配。假如真改了文案,退换货满意度就能提升到跟天猫京东一样了吗?如果事先不进行因果关系的纠正,没有想清楚匆忙投入开发,最终导致功能上线了实际问题也没得到解决。


如何提升逻辑思维


工作中大大小小的事情都需要做决策,回答一些问题,给出解决方案。当你尝试给出一个结论时,这个结论尽要符合逻辑。那么,如何去体现你的逻辑思维呢?


第一种:因果关联



我最近在商城增加分期免息支付方式,转化率得到了提升,为什么会提升?


分期免息降低了购买门槛,让本来没有能力的人可以购买。


结论是:分期免息可以提升转化率。


论据是:分期免息降低购买门槛,这个点无法再产生任何质疑。



世间万物都存在普遍联系,逻辑思维中最重要的就是找准结论与事实之间的因果联系。运用因果逻辑进行推理,一定不要仅停留在单一的因果层次上,必须从多个角度去研究事物发生的原因以及推出的结果。


比如,分析事物之间不同因果联系产生的不同结论。通常情况下,我们在进行因果关系推理时,必须重视因果分析,需要注意的有以下几方面:


① 有明确的结论产生


但是不要回答:也许吧....先看看....不知道结论是什么。再或者出现了问题讲述了一大段原因,但是否产生影响或损失无结论,下一步有无行动也无结论。没有结论的表现就是别人听了你的阐述,像没有听一样,也不知道下步该干什么。


讲因果关系时首要明确结论,且结论肯定、不模糊。听到结论的人明确知道自己下步要做什么行动。明确得出下步不做什么也是一种结论。




② 分析产生问题的真正原因


有些情况下,原因可以分为很多层次,有些现象在表面上看来是引发结果的原因,但其实不然,因为在它们背后还存在着引发它们的原因。


对于拥有多个引发原因的结果,如果仅停留在某个单一层面上,将这一原因当作引发结果的最终因素,论点就会变得相对肤浅,并且很难将分析的问题理清楚,这样的因果逻辑推理得出的结果所拥有的说服力必然不大。


我们常说的第一性原理是讲此点,从头算起,只采用最基本的事实作为依据,然后再层层推导,得出结论。有经验虽好,但有时候人们由于惯性陷入经验主义,而忘记质疑自己“为什么一定是这样”?


在平常的工作中,我们一定要学会多问自己几个为什么。



  1. 平时工作只知道自己在做什么,那就是What

  2. 这样当然是不够的,还要常问自己,再来一遍what(我做这的这个到底是什么?)

  3. why(为什么要做,为什么要这么做)

  4. how(怎么做比较好)

  5. why not(为什么不能那样做呢?)


如果你在尝试得出一个结论时,自己没先在内心先演练一遍,跟他人交流时,很容易受到挑战。




③ 分析主要原因和次要原因


很多情况下,一种结果的引发原因可能有很多种,这时我们必须分清其中的主次原因,准确抓住主要原因,通过引起结果的最基本因素来进行逻辑推理


分析主次原因才会懂真正的MVP。MVP高度训练你对问题做拆解的能力,锻炼把一件事儿做成的能力。什么是当下最主要的矛盾,最应该被解决的痛点是什么,为什么是先做A而不是先做B的逻辑依据。如果一个人一上来就要做大而全的解决方案或者想要走捷径去做很容易实现的次要原因,也容易被受到质疑。




第二种:MECE原则


因果关系强调纵向思考逻辑,一环扣一环,回答“为什么”,MECE原则强调横向思考逻辑,思考无遗漏。



MECE原则来自《金字塔原理》,指相互独立不重复,完全穷尽无遗漏。



  • 各部分之间相互独立(MutuallyExclusive),没有重叠,有排他性;

  • 所有部分完全穷尽(CollectivelyExhaustive),没有遗漏。



干一件事情之前,我们先把它可能涉及到的要素全部列出来,运用思维导图工具,一层一层剖析到底;将这些要素按相同的性质来进行整理归纳,划分层级,最终呈现在你眼前的思维导图架构就会清晰明了,所有可能触发的点一目了然。


① 时间顺序


时间顺序强调一定要在某个时间节点发生了什么事情对下个时间节点产生影响。有明确时间差的按照时间顺序思考。比如说,过去—现在—未来,阶段1—阶段2—阶段3。


② 流程顺序


按照事物发生的流程顺序思考,先有什么再有什么,什么数据的变动会导致另外一块数据变动,这样避免我们改动了某块功能而遗留另外一块的功能。比如用户在电商网站上下单支付,先后有订单系统和支付系统均要参与这个过程。


③ 结构顺序


做完某件事情一定要所有相关点都能实现,如果一个点遗漏,导致整体无法完成。比如某个支付方式在指定金额区间范围内有效


结构化思维


然而,单纯的逻辑思维并不足以让我们在职场中取得长期成功。这时,结构化思维发挥着关键的作用。结构化思维是指我们将复杂问题和信息组织成有条理的结构,以便更好地理解和处理。它帮助我们将任务分解成可管理的部分,建立清晰的工作流程,并确保每个步骤都有明确的目标和结果。



在职场中:



  • 同样沟通事情,有的人三句话就能说清楚,而你可能说了10分钟也说不到核心;

  • 同样是做汇报,有的人用5页PPT就能说服对方,而你可能写了20多页还要被反问想表达什么;

  • 同样是阐述解决方案,有的人清晰讲出背景、问题、原因、影响和举措,而你挤牙膏式的回答一句紧接着再被问一句。



如果说一个人沟通表达能力差,情商是一个因素,但还有一个更关键点:结构化思维。如果对方听了半天都不知道你在表达什么,情商再高也失去色彩。


人类大脑在处理信息的时候,有两个规律:


第一,不能一次太多,太多信息会让我们的大脑觉得负荷过大;


第二,喜欢有规律的信息。


表达能力强的人,不是比你更聪明,而是知道大脑这个特点,更懂得通过有效的结构化思维,快速对信息进行归纳和整理进行传递。 大脑容易记住有规律的东西,那么你在信息传递时尽量使用规律的东西来传递。把无序变得有规律的过程即结构化思维。


结构化思维是一种从整体到局部、从框架到细节的思维方式。 它要求思考者不先入为主,不会过快地陷入细节,而要经常留意事物的整体框架,在框架的基础上去拓展细节。先看能够解决问题的关键方面,然后再往下分析,从而实现从总体到局部的鸟瞰,最典型的就是金字塔结构图。



结构化思维渗透在工作的方方面面,是建立在逻辑思维之上的另一种显性化思维方式,不可或缺。结构化思维可以带来显著的工作效率提升,尤其是在沟通中。


那为什么在沟通中结构化思维可以发挥巨大作用呢?


因为人和人之间信息差。 结构的越上层,彼此之间信息差越小;结构的越下层,彼此之间信息差越大。如果一上来陷入到最下层的细节之中,对方大概率听不懂时沟通出现低效。


如何提升结构化思维


如果说逻辑思维一定程度上跟人天生智力水平有关,即我们常说的一个人聪不聪明,那么结构化思维则可以通过刻意训练习得。结构化思维完全由自己从0到1主动规划所得,就如同有些人可以做出好看的PPT,其实也是懂得了PPT背后的套路。


提到结构化思维,不得不去看的《金字塔原理》一书,主要有4个原则:结论先行,以下统上,归类分组,逻辑递进。



论:结论先行


证:以上统下


类:归类分组


比:逻辑递进




结构化思维有2种方法:自下向上组结构和自上向下套框架。


第一种:自下向上找结构


自下向上组结构核心在于这个结构是你自创的。根据你自己对接收到信息的理解,把信息重新组装的过程。


比如给领导讲业务数据的PPT,首先会介绍分析数据的整体框架,其次再给一个实际的数据分析的概览,再往下去细看每块的细分数据,针对每块的数据给出结论和TO DO。



  • 整体数据指标体系(理论)—— 1页PPT

  • 总体数据指标概览(实际数据)—— 1页PPT

  • 分模块1数据指标—— 1页PPT

  • 分模块2数据指标—— 1页PPT

  • 分模块3数据指标—— 1页PPT

  • 每页PPT里的结论和TO DO

  • 总结—— 1页PPT


先框架后细节,先总结后具体,先结论后原因,先观点后建议,先重要后次要。 这样,才能让对方第一时间抓住重点信息,知道我们要传递的核心内容。


站在自己视角时,先把所有零散的点穷举,再看点与点之间的关联性连接成面,面最终再成体。概括起来大致分为以下4步:



  • 尽可能列出所有思考的要点

  • 找出关系,进行分类(找出要点间的逻辑关系,利用 MECE 原则归类分组)

  • 总结概括要点,提炼观点

  • 观点补充,完善思路




第二种:自上向下套框架


如果你是做已存领域的问题解决方案,那通过自上而下找结构:思考一个框架,然后将信息或解决方案放入框架。自上向下套框架依赖我们自身积累了多少种框架,在实际场景中可以随时被调用。




结构化思维是一个建立清晰、稳定、有序的思考结构,学到这个结构之后,知识体系从零散化到系统化,从无序到有序,从低效到高效。


不断的进行归纳和总结,养成做任何事都进行阶段性总结和复盘的习惯是训练结构化思维的核心。思维如果懒惰任何种方法技能也无济于事。


混沌大学创办人李善友教授说过:成年人学习的目的,应该是追求更好的思维模型,而不是更多的知识,在一个落后的思维模型里,即使增加更再多的信息量,也只是低水平的重复。


逻辑思维与结构化思维本身是专门的学科,并不是本文几千字就可以讲清楚的。但我核心想表达的点是:这2种思维方式相当重要,需要刻意去学习和培养。一些行业内的通用书籍,如《金字塔原理》值得反复阅读并且在日常工作中训练。


思维决定了你的认知水平和成长速度。 工作上没有好的思维方式,再努力也是无效努力。思维方式需要主动意识到高价值然后刻意训练习得。愿我们都能拥有更好的思维模型,开启不一样的人生。


作者:黄勇超
来源:juejin.cn/post/7252171148757418039

收起阅读 »

十年码农内功:经历篇

分享工作中重要的经历,可以当小说来看 一、伪内存泄漏排查 1.1 背景 我们原来有一个刚用 C++ 写的业务服务,迟迟不敢上线,原因是内存泄漏问题一直解决不了。现象是,服务上线后,内存一直慢慢每隔几秒上涨4/8KB,直到服务下线。 我刚入职不久,领导让我来查...
继续阅读 »

分享工作中重要的经历,可以当小说来看



一、伪内存泄漏排查


1.1 背景


我们原来有一个刚用 C++ 写的业务服务,迟迟不敢上线,原因是内存泄漏问题一直解决不了。现象是,服务上线后,内存一直慢慢每隔几秒上涨4/8KB,直到服务下线。


我刚入职不久,领导让我来查这个问题,非常具有挑战性,也是领导对我的考察!


1.2 分析


心路历程1:工具分析


使用 Valgrind 跑 N 遍服务,结果中都没有发现内存泄漏,但是有很多没有被释放的内存和很多疑似内存泄漏。实在没有发现线索。


心路历程2:逐个模块排查


工具分析失败,那就挨个模块翻看代码,并且逐个模块写demo验证该模块是否有泄漏(挂 Valgrind),很遗憾,最后还是没有找到内存泄漏。


心路历程3:不抛弃不放弃


这个时候两周快过去了,领导说:“找不到内存泄漏那就先去干别的任务吧”,感觉到一丝凉意,我说:“再给我点时间,快找到了”。这样顶着巨大压力加班加点的跑Valgrind,拿多次数据结果进行对比,第一份跑 10 分钟,第二份跑 20 分钟,看看有哪些差异或异常,寻找蛛丝马迹。遗憾的是还是没有发现内存泄漏在哪。


功夫不负有心人,看了 N 份结果后,对一个队列产生了疑问,它为啥这么大,队列长度 1000 万,直觉告诉我,它不正常。


去代码中找这个队列,确实在初始化的时候设置了 1000 万长度,这个长度太大了。


1.3 定位


进队列需要把虚拟地址映射到物理地址,物理内存就会增加,但是出队列物理内存不会立刻回收,而是保留给程序一段时间(当系统内存紧张时会主动回收),目的是让程序再次使用之前的虚拟地址更快,不需要再次申请物理内存映射了,直接使用刚才要释放的物理内存即可。


当服务启动时,程序在这 1000 万队列上一直不停的进/出队列,有点像貔貅,光吃不拉,物理内存自然会一直涨,直到貔貅跑到了队尾,物理内存才会达到顶峰,开始处在一个平衡点。


图1 中,红色代表程序占用的物理内存,绿色为虚拟内存。



图1


然而每次上线还没等 到达平衡点前就下线了,担心服务内存一直涨,担心出事故就停服务了。解决办法就是把队列长度调小,最后调到了 2 万,再上线,貔貅很快跑到了队尾,达到了平衡点,内存就不再增涨。


其实,本来就没有内存泄漏,这就是伪内存泄漏。


二、周期性事故处理


2.1 背景


我们有一个业务,2019 年到 2020 年间发生四次(1025、0322、0511 和 0629)大流量事故,事故时网络流量理论峰值 3000 Gbps,导致网络运营商封禁入口 IP,造成几百万元经济损失,均没有找到具体原因,一开始怀疑是服务器受到网络攻击。


后来随着事故发生次数增加,发现事故发生时间具有规律性,越发感觉不像是被攻击,而是业务服务本身的流量瞬间增多导致。服务指标都是事故造成的结果,很难倒推出事故原因。


2.2 猜想(大胆假设)


2.2.1 发现事故大概每50天发生一次


清晰记得 2020 年 7 月 15 日那天巡检服务时,我把 snmp.xxx.InErrors 指标拉到一年的跨度,如图2 发现多个尖刺的间距似乎相等,然后我就看了下各个尖刺时间节点,记录下来,并且具体计算出各个尖刺间的间隔记录在下面表格中。着实吓了一跳,大概是 50 天一个周期。并且预测了 8月18日 可能还有一次事故。



图2 服务指标


事故时间相隔天数
2019.09.05-
2019.10.2550天
2019.12.1450天
2020.02.0149天
2020.03.2250天
2020.05.1150天
2020.06.2949天
2020.08.18预计

2.2.2 联想50天与uint溢出有关


7 月 15 日下班的路上,我在想 3600(一个小时的秒数),86400(一天的秒数),50 天,5 x 8 等于 40,感觉好像和 42 亿有关系,那就是 uint(2^32),就往上面靠,怎么才能等于 42 亿,86400 x 50 x 1000 是 40 多亿,这不巧了嘛!拿出手机算了三个数:


2^32                  = 4294967296 
3600 * 24 * 49 * 1000 = 4233600000
3600 * 24 * 50 * 1000 = 4320000000

好巧,2^32 在后面的两个结果之间,4294967296 就是 49 天 16 小时多些,验证了大概每 50 天发生一次事故的猜想。



图3 联想过程


2.3 定位(小心求证)


2.3.1 翻看代码中与时间相关的函数


果然找到一个函数有问题,下面的代码,在 64 位系统上没有问题,但是在 32 位系统上会发生溢出截断,导致返回的时间是跳变的,不连续。图4 是该函数随时间输出的折线图,理想情况下是一条向上的蓝色直线,但是在 32 位系统上,结果却是跳变的红线。


uint64_t now_ms() {
struct timeval t;
gettimeofday(&t, NULL);
return t.tv_sec * 1000 + t.tv_usec / 1000;
}


图4 函数输出


这里解释一下,问题出在了 t.tv_sec * 1000,在 32 位系统上会发生溢出,高于 32 位的部分被截断,数据丢失。不幸的是我们的客户端有一部分是 32 位系统的。


2.3.2 找到出问题的逻辑


继续追踪使用上面函数的逻辑,发现一处问题,客户端和服务端的长链接需要发Ping保活,下一次发Ping时间等于上一次发Ping时间加上 30 秒,代码如下:


next_ping = now_ms() + 30000;

客户端主循环会不断判断当前时间是否大于 next_ping,当大于时发 Ping 保活,代码如下:


if (now_ms() > next_ping) {
send_ping();
next_ping = now_ms() + 30000;
}

那怎么就出现大流量打到服务器呢?举个例子,如图3,假如当前时间是 6月29日 20:14:00(20:14:26 时 now_ms 函数返回 0),now_ms 函数的返回值超级大。


那么 next_ping 等于 now_ms() 加上 30000(30s),结果会发生 uint64 溢出,反而变的很小,这就导致在接下来的 26 秒内,now_ms函数返回值一直大于 next_ping,就会不停发 Ping 包,产生了大量流量到服务端。


2.3.3 客户端实际验证


找到一个有问题的客户端设备,把它本地时间拨回 6月29日 20:13:00,让其自然跨过 20:14:26,发现客户端本地 log 中有大量发送 Ping 包日志,8 秒内发送 2 万多个包。证实事故原因就是这个函数造成的。解决办法是对 now_ms 函数做如下修改:


uint64_t now_ms() {
struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t);
return uint64_t(t.tv_sec) * 1000 + t.tv_nsec / 1000 / 1000;
}

2.3.4 精准预测后面事故时间点


因为客户端发版周期比较长,需要做好下次事故预案,及时处理事故,所以预测了后面多次事故。


时间戳(ms)16进制北京时间备注
15719580303360x16E000000002019/10/25 07:00:30历史事故时间
15762529976320x16F000000002019/12/14 00:03:17不确定
15805479649280x170000000002020/02/01 17:06:04不确定
15848429322240x171000000002020/03/22 10:08:52历史事故时间
15891378995200x172000000002020/05/11 03:11:39历史事故时间
15934328668160x173000000002020/06/29 20:14:26历史事故时间
15977278341120x174000000002020/08/18 13:17:14精准预测事故发生
16020228014080x175000000002020/10/07 06:20:01精准预测事故发生
16063177687040x176000000002020/11/25 23:22:48精准预测事故发生

2.4 总结


该事故的难点在于大部分服务端的指标都是事故导致的结果,并且大流量还没有到业务服务,就被网络运营商封禁了 IP;并且事故周期跨度大,50 天发生一次,发现规律比较困难。


发现规律是第一步,重点是能把 50 天和 uint32 的最大值联系起来,这一步是解决该问题的灵魂。



  • 大胆假设:客户端和服务端的代码中与时间相关的函数有问题;

  • 小心求证:找到有问题的函数,别写代码验证,最后通过复现定位问题;


经过不屑努力从没有头绪到逐渐缩小排查范围,最后定位和解决问题。

作者:科英
来源:juejin.cn/post/7252159509837119546

收起阅读 »

程序猿的九年广漂

** 程序猿的九年广漂感悟 广漂第九年了,成为了一名老码农,混迹于各种论坛博客中,我感觉总得写点什么.讲讲技术也好,发泄一下码农的无奈也好.希望对年轻的同行有点帮助吧. 非广东人,但从小在广东生活,一口不标准的粤语,毕业自然而然就就来了广州,总以为广州是最合...
继续阅读 »

**


程序猿的九年广漂感悟


广漂第九年了,成为了一名老码农,混迹于各种论坛博客中,我感觉总得写点什么.讲讲技术也好,发泄一下码农的无奈也好.希望对年轻的同行有点帮助吧.

image.png
非广东人,但从小在广东生活,一口不标准的粤语,毕业自然而然就就来了广州,总以为广州是最合适的城市,无论饮食和文化,跟大多数毕业生一样,总会有一些不着边际的理由漂泊在北上广深,没有过来人指导职业规划,全凭一股初生牛犊不怕虎的拼劲.其实这是一件很可怕且无奈的事情.建议大家多听听张雪峰对大学生的指导.不说太多废话,先讲自己的经历吧.
14-15年毕业季,毕业于很普通的二本母校,在面试公司实在是抬不起头来,后来入职了一家搞微商的公司,一个人干三个人的活,写Android和java后端,天天加班,老板天天喂鸡汤大饼,周末单休,4000大洋的无保障金,还发不出来,年轻的时候只想学技术,希望早日月薪过万.当时住在一个城中村里面,每天来回160分钟的公交地铁,路上都在看慕课网和掘金,这个期间虽然穷且幼稚,但是技术成长很快,相信和大多数刚刚毕业的大学生一样吧,因为想法不多,没有家庭,没有职业规划,才能一心一意的把技术学好.


同时期,那些在老家有规划的同学在干嘛呢,有继承家里工厂的,有直接考试公务员的,他们的人生规划很清晰,有工作然后找老婆,然后家里帮助买房成家,哦对了,我老家附近是二线城市,这一点很重要.

毕业的第一家公司让我像个无头苍蝇,微商公司出来后呢,或多或少也算对我们天朝社会有了一点认知,想找稳定一点的工作,最起码是正常发工资的,很快就找了家超甲级写字楼的中外合资公司,开头就是广东XXXX公司,琶洲那边,直面广东大珠江.每月薪资8500大洋,主攻Android.那时候996不叫流行,叫盛行.


WeChat459ffe44abc76d02a65e224e7348c39a.jpg


期间有几件有意思的事情.

第一件是公司有很多老外,当时有个德国投资商需要几个产品和设计一起对需求,第一天就跟国产同事说,我工作的节奏很快,也会有加班,你们一定要跟上节奏.当时刚刚毕业于西安外语的小妹妹不知道怎么翻译的,回来给我们说的一愣一愣的,一个月后,德国佬全身而退了,还说了一些让我们啼笑皆非的话,说你们中国人加班太严重了,好像不需要自己的私人时间一样.当时还没有’卷”这种说法.跟着德国佬对需求的产品设计回到国人领导的团队后,感叹,跟着德国人做事太舒服了,几乎也没有加班,节奏慢的舒坦;


第二件事,公司后来给我配了个小弟,让我好好带,小弟的薪资是9500.这货是刚刚培训出来的,理论知识牛逼,啥也做不了.一个月后我怕累死自己,于是让他走人了,后来公司又找了大专没有毕业的小伙子,技术很扎实,薪资5000.领导还说给多了,这小伙子很实在,后来一直跟了我很多年,现在想想有点对不住他,一直没有给他带个好坑位.


第三件事,因为写字楼周末是不开中央空调的,所以我们经常加班的冷热不知;但是资本家可不管这些,虽然公司经常不准时发工资,套路可不少,比如招人来试用期过后就裁员,不多不少3个月,反正便宜好用,或者招外包人员来用几天给人充当大公司的样子,然后在绩效方面下功夫,很多很多套路,有时候不得不佩服国人在这方面的造诣.还有经常在午休期间听到老板撕心裂肺的骂人,哈哈哈,那时候想不明白这货是怎么做成老板的,很多年后向明白了,这货才符合国人老板形象.对了,这家公司背靠我们伟大的某种天朝机构.


这家公司是我呆过最差的公司,通宵加班是常有的,你们打工的最长加班时间是多长呢,我是两天两夜....你们肯定很想知道这家公司结局怎样,最老外肯定是撤资走人的,公司最后就剩几个无关紧要的人.你们以为是失败了吗,其实是相当成功的,虽然坑了一波人,但是老板们的豪车豪宅是实现了的,快钱已经到手了,你们细细品....


任职这家中外合资的公司期间,其实说不清是什么人生态度,我除了正常打工还干嘛呢,被朋友忽悠去创业,经常折腾到晚上两三点.年轻就是好呀,好像不用睡觉一样.后来有一件事对我打击很大.我接了一个中国移动的小单子,3万块,每天晚上在家工作到2点左右,刚刚开始还没有什么,一个星期后,突然重感冒,发烧,后来直接请假在家休息了一段时间,也就是这个期间,我的体重从60公斤一下子飙到了65公斤.至于职业规划,买车买房,成家立业啥的,不好意思,8500的月薪,住在见不到阳光的城中村的我,真的不知道自己应该想什么.除了提高技术拼体力以外,我们能想的很少,当然家里也经常叫回去考公务员,但是人出来了,想回去真的很难很难.


这里再说一下同时期的好友,做公务员的朋友,已经在老家买了房子了,有了心爱的姑娘了,虽然自称是房奴,但是公积金已经绰绰有余.接手家里生意的朋友已经做的风生水起.在这里提醒一下刚刚毕业的小码农,家庭的高度以及父母提供的规划很重要.



从那家公司出来以后,我突然想去国企看看,因为那段时间太累了,想要周末,想正常上下班.这里就要提到一家老牌国企.某电视台,做某电视台新闻某电app.当时面试已经通过了,人事谈下来的薪资是1w,很多公司给的薪资都是一点几的w,但是还是想去国企,可能真的是加班怕了吧.一个二十出头的小伙子,经常通宵加班,心里打击还是很大的.但是这家某视台的国企呢,比较让人摸不着头脑,人事已经口头答应录用了,但是offer迟迟没有下来,后来我入职了另外一家公司半个月以后,offer终于来了,人事还介绍了一番,问我什么时候可以入职.后来还发生了一件特别有趣的事情.你以为的国企就很稳定,并没有,一年后,我成为了面试官,他们整个小组的人来求职,是我亲自面试的......


再说说接下来吧,我去了一家上市公司,公司很大,薪资很高,刚刚开始很闲,那时候的我除了学技术以外,终于有时间思考下一步怎么办,这是我遇到的福利最好的一家公司,不过我后来还是离职了,其实任职期间我已经在找下家了.这家公司的项目我认为是没有前途的,后来事实证明确实没有前景,项目很快就裁掉了.这家公司也有一件小插曲,当时公司安排了7个实习生让我带,让我在两个星期内完成一个教育类app,大公司是有各种考核的,每天都要写日报,我当时的前一个星期的日报都是写怎么搭框架,怎么模块化项目, 后来项目经理每天都找到我,她说不关心我做什么框架,什么模块化,他要的是马上实现UI上面的功能,并且她能够体验的到这些功能的完成度,必须要知道我在干什么,对了,这家公司如果使用了第三方库的话,需要提交给上级申请,并且要写评估报告的.对,你没有听错.要求全部使用原生的框架,举个例子,我用 了okhttp,然后被叼了一顿.哈哈哈哈!有一次我去看了一下别人的日报怎么写的,很亮眼睛,全部都是今天我优化了哪里哪里的代码,完成了哪里哪里的注释....


是不是有小伙伴问我为什么不去某大型互联网公司试试,这还用说,任职期间我肯定是有去面试的,某讯面试通过了,但是我不喜欢深圳,放弃了,某易的话,我请了三天假去面试,我印象中最后一轮是某易的某邮箱的组长面的我,拒绝我的理由是因为我不懂C++,当时我挺沮丧的,并且怼了他,简历上就可以看出我不精通C++,电话面试也可以看出我是否精通C++,为什么让我花了几天来面试....他不说话.多年后想了想,其实完全没有必要,一个邮箱app很难吗,拒绝我肯定是其他原因的.谁有话语权就有决定权.出来工作有一点必须要尽快学会成长的,不要对没有意义的事情较劲.


接着讲吧,某上市公司出来之后,我去了一家我当时认为还是比较ok的公司,干了很多年,一直干到公司上市.....我不太想说太多这家公司,怕暴露自己最好的青春都给了这家公司.


说说同时期的好朋友怎么样了吧,某公务员朋友,在家里的帮助下,已经副科,孩子已经打酱油了,他每天聊天的积极性很高,你懂的,家里的帮助下,买了个大房子.另一个接手家庭生意的朋友,已经把公司做到了当地的第一,什么奔驰宝马豪宅,早就实现了,孩子都三个了.也有同学会问,北上广深漂泊的同学呢,他们基本上都回老家了.不好意思,没有太多的人生赢家,大部分的人都是普普通通的.房子始终是我们绕不过的一道坎,尤其是大城市,想通过打工实现大城市安家立业,是一件很难很难的事情,刚刚毕业的程序员小伙伴要考虑清楚这一点哦.


差点忘记了这是一个技术论坛,我们再来聊聊技术吧,可能会有小伙伴好奇我的技术怎么样,ios Android python 设计 都还行,因为永远都有新东西出来,没人敢说自己在新的技术点上多牛,也不敢保证自己永远处于热爱技术的阶段上,至于什么架构师啥的,项目管理什么的,对于目前的我来说就跟煮饭一样简单.目前的阶段处于,什么可以赚钱我就可以学什么,但是大多数的技术都是不赚钱的,你细品...

奉劝新入技术坑的小伙伴,不要纠结于自己学哪种技术,更不要要纠结于哪个技术点谁比较牛逼,你们较劲的东西资本家看着像一个笑话.技术仅仅只是一个工具,先确定好自己要做什么,再决定自己携带什么工具.

WeChat039aa9244fd4185fb0728e11854eeeef.jpg


再说说三十多岁的程序员的未来方向在哪里,先说说找工作吧,我这个阶段一旦被裁员,大概率找不到工作,不要纠结市场环境为什么这么奇怪,你可以怀疑资本家的人品,但是请不要怀疑资本家对市场的把控.不要问我怎么知道,也不要举例反驳我,市场上总有特例,我们只能拥抱市场大环境.这些年来,我带过团队很多,经常招人,有时候都会感叹我出去以后怎么找工作,慢慢的,我现在已经能接受自己失业后送外卖和开滴滴了.对,你没有听错,宇宙的尽头可能是考公,也可能是美团或者饿了么.


说说职业分水岭吧,第一个5年,你跟身边的同学可能差别不大,无非就是结婚与不结婚,但是到了第二个五年,你会慢慢的发现,原来不同的职业不同的城市,幸福感真的不一样,选择远远比努力重要,做一个普通人就是要很努力很努力的,所以不要看不起一毕业就考公的同学,也不要说谁谁谁读大专是混日子啥的,请不要忽视体制带来的便利,体制这个词你细品.


WeChat5fa4443854247035029c1d17309afe99.jpg


写了那么多,来个总结吧,毕竟是小爽文嘛,是要写点个人主观的想法的.技术本身是没有出路的,出路在业务,业务驱动技术的开发.所以不要过多纠结于太多技术点,技术牛逼的人未必能赚多钱,技术牛逼的人不一定能去bat,也可能在缅甸菲律宾或者91啥先生的.对了,我忘了说,这些年副业赚到的钱是打工的好几倍,所以你细品.


一个人是否考虑技术上长远发展,首先要考虑自己的家庭,以及自己的身边的资源.如果有条件毕业就回家考公上岸的话,尽量去试试.大城市的技术之路往往伴随着城中村,出租房,996.所以多花点心思在业务上.希望在座的小伙伴尽快规划好自己的职业道路,我文中提了那么多同时期的朋友,不得不承认他们有更好的家庭资源,我想说的是,要懂得利用自己的家庭资源尽早规划自己的职业道路,如果没有足够的家庭资源,更要尽早规划自己的职业道路.技术不是出路,只是暂时的活路

作者:给朕下跪
来源:juejin.cn/post/7252195699524403259
.且行且珍惜!!!!

收起阅读 »

记一次修改一行代码导致的线上BUG

web
背景介绍 先描述一下需求,要在一个老项目里根据type类型,给一个试题题干组件新增一个class样式,type是在url地址栏上面携带的。简单,一行代码搞定,五分钟部署,十分钟留给测试,然后跟车上线,打卡下班! 《凉凉》送给自己 看标题就知道结果了,第二天下午...
继续阅读 »

1920_1200_20100319011154682575.jpg


背景介绍


先描述一下需求,要在一个老项目里根据type类型,给一个试题题干组件新增一个class样式type是在url地址栏上面携带的。简单,一行代码搞定,五分钟部署,十分钟留给测试,然后跟车上线,打卡下班!


《凉凉》送给自己


看标题就知道结果了,第二天下午现网问题来了,一线反馈某个页面题干不展示了,值班同事排查一圈,找到我说我昨天加的代码报错了!


006Cmetyly1ff16b3zxvxj308408caa8.jpg


惊了,就加了一行业务代码,其他都是样式,测试也通过了,这也能有问题?绩效C打底稳了(为方便写文章,实际判断用变量代替):


<div :class="{'addClass': $route.query.type === 'xx'}">
...
</div>

temp.png
问题其实很简单,$route为undefined了,导致query获取有问题,这让我点怀疑自己,难道这写错了?管不了太多,只能先兼容上线了。


$route && $route.query && $route.query.type

其实是可以用?.简写的,但是这个项目实在不“感动”了,保险写法,解决问题优先。提申请,拉评审,走流程,上线,问题解决,松口气,C是保住了。


问题分析


解决完问题,还要写线上问题分析报告,那只能来扒一扒代码来看看了。首先,这个项目使用的是多页应用,每个页面都是一个新的SPA,我改的页面先叫组件A吧,组件A在页面A里被使用,没问题;组件A同样被页面B使用,报错了。那接下来简单了,看代码:


// 2022-09-26 新增
import App from '@/components/pages/页面A'
import router from '@/config/router.js'
// initApp 为封装的 new Vue
import { initApp, Vue } from '../base-import'
initApp(App, router)

// 2020-10-18 新增
import App from '@/components/pages/页面b'
new Vue({
el: '#app',
components: { App },
template: '<App/>'
})

两个页面的index.js文件,两种写法,一个引用了router,一个没有引用,被这个神仙代码整懵了。然后再看了一下其他页面,也都是两种写法掺着写的,心态崩了。这分析报告只能含着泪写了...


最后总结



  1. 问题不是关键,关键的是代码规范;

  2. 修改新项目之前,最好看一下代码逻辑,有熟悉的同事最好,可以沟通了解一下业务(可以避免部分问题);

  3. 当想优化之前代码的时候,要全面评估,统一优化,上面的写法我也找同事了解了,因为之前写法不满足当时的需求,他就封装了新方法,但是老的没有修改,所以就留了坑;


作者:追风筝的呆子
来源:juejin.cn/post/7252198762625089596
收起阅读 »

什么!一个项目给了8个字体包???

web
🙋 遇到的问题 在一个新项目中,设计统一了项目中所有的字体,并提供了字体包。在项目中需要按需引入这些字体包。 首先,字体包的使用分为了以下几种情况: 无特殊要求的语言使用字体A,阿拉伯语言使用字体B; 加粗、中等、常规、偏细四种样式,AB两种字体分别对应使用...
继续阅读 »

🙋 遇到的问题


在一个新项目中,设计统一了项目中所有的字体,并提供了字体包。在项目中需要按需引入这些字体包。


首先,字体包的使用分为了以下几种情况:



  1. 无特殊要求的语言使用字体A,阿拉伯语言使用字体B;

  2. 加粗、中等、常规、偏细四种样式,AB两种字体分别对应使用 BoldMediumRegularThin 四种字体包;


所以,我现在桌面上摆着 8 个字体包:



  • A-Bold.tff

  • A-Medium.tff

  • A-Regular.tff

  • A-Thin.tff

  • B-Bold.tff

  • B-Medium.tff

  • B-Regular.tff

  • B-Thin.tff


image.png
不同语言要使用不同的字体包,不同粗细也要使用不同的字体包!


还有一个前提是,设计给的设计图都是以字体A为准,所以在 Figma 中复制出来的 CSS 代码中字体名称都是A。


刚接到这个需求时还是比较懵的,一时想不出来怎么样才能以最少的逻辑判断最少的文件下载最少的代码改动去实现在不同情况下自动的去选择对应的字体包。


因为要涉及到语言的判断,最先想到的还是通过 JS,然后去添加相应的类名。但这样也只能判断语言使用A或B,粗细还是解决不了。


image.png


看来还是要用 CSS 解决。


首先我将所有的8个字体先定义好:


@font-face {
font-family: A-Bold;
src: url('./fonts/A-Bold.ttf');
}

/* ... */

@font-face {
font-family: B-Thin;
src: url('./fonts/B-Thin.ttf');
}

image.png


🤲🏼 如何根据粗细程度自动选择对应字体包


有同学可能会问,为什么不直接使用 font-weight 来控制粗细而是用不同的字体包呢?


我们来看下面这个例子,我们使用同一个字体, font-weight 分别设置为900、500、100,结果我们看到的字体粗细是一样的。


对的,很多字体不支持 font-weight 所以我们需要用不同粗细的字体包。


image.png


所以,我们可以通过 @font-face 中的 font-weight 属性来设置字体的宽度:


@font-face {
font-family: A;
src: url('./fonts/A-Bold.ttf');
font-weight: 600;
}
@font-face {
font-family: A;
src: url('./fonts/A-Medium.ttf');
font-weight: 500;
}
@font-face {
font-family: A;
src: url('./fonts/A-Regular.ttf');
font-weight: 400;
}
@font-face {
font-family: A;
src: url('./fonts/A-Thin.ttf');
font-weight: 300;
}

注意,这里我们把字体名字都设为相同的,如下图所示,这样我们就成功的解决了第一个问题:不同粗细也要使用不同的字体包;


image.png


并且,如果我们只是定义而未真正使用时,不会去下载未使用的字体包,再加上字体包的缓存策略,就可以最大程度节省带宽:


image.png


🔤 如何根据不同语言自动选择字体包?


通过张鑫旭的博客找到了解决办法,使用 unicode-range 设置字符 unicode 范围,从而自定义字体包。


unicode-range 是一个 CSS 属性,用于指定字体文件所支持的 Unicode 字符范围,以便在显示文本时选择适合的字体。


它的语法如下:


@font-face {
font-family: "Font Name";
src: url("font.woff2") format("woff2");
unicode-range: U+0020-007E, U+4E00-9FFF;
}

在上述例子中,unicode-range 属性指定了字体文件支持的字符范围。使用逗号分隔不同的范围,并使用 U+XXXX-XXXX 的形式表示 Unicode 字符代码的范围。


通过设置 unicode-range 属性,可以优化字体加载和页面渲染性能,只加载所需的字符范围,减少不必要的网络请求和资源占用。


通过查表得知阿拉伯语的 unicode 的范围为:U+06??, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF, U+10A60-10A7F, U+10A80-10A9F 这么几个区间。所以我们设置字体如下,因为设计以 A 字体为准,所以在 Figma 中给出的样式代码字体名均为 A,所以我们把 B 字体的字体名也设置为 A:


image.png


当使用字体的字符中命中 unicode-rang 的范围时,自动下载相应的字体包。


@font-face {
font-family: A;
src: url('./fonts/A-Bold.ttf');
font-weight: 600;
}

@font-face {
font-family: A;
src: url('./fonts/A-Medium.ttf');
font-weight: 500;
}

@font-face {
font-family: A;
src: url('./fonts/A-Regular.ttf');
font-weight: 400;
}

@font-face {
font-family: A;
src: url('./fonts/A-Thin.ttf');
font-weight: 300;
}

:root {
--ARABIC_UNICODE_RANGE: U+06??, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF, U+10A60-10A7F, U+10A80-10A9F;
}
@font-face {
font-family: A;
src: url('./fonts/B-Bold.ttf');
font-weight: 600;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
@font-face {
font-family: A;
src: url('./fonts/B-Medium.ttf');
font-weight: 500;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
@font-face {
font-family: A;
src: url('./fonts/B-Regular.ttf');
font-weight: 400;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
@font-face {
font-family: A;
src: url('./fonts/B-Thin.ttf');
font-weight: 300;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
p {
font-family: A;
}

总结


遇到的问题:



  1. 两种字体,B 字体为阿拉伯语使用,A 字体其他语言使用。根据语言自动选择。

  2. 根据字宽自动选择相应的字体包。

  3. 可以直接使用 Figma 中生成的样式而不必每次手动改动。

  4. 尽可能节省带宽。


我们通过 font-weight 解决了问题2,并通过 unicode-range 解决了问题1。


并且实现了按需下载相应字体包,不使用时不下载。


Figma 中的代码可以直接复制粘贴,无需任何修改即可根据语言和自宽自动使用相应字体包。




参考资料:http://www.zhangxinxu.com/wordpr

作者:Mengke
来源:juejin.cn/post/7251884086536781880
ess/2…

收起阅读 »

关于Java已死,看看国外开发者怎么说的

博主在浏览 medium 社区时,发现了一篇点赞量 1.5k 的文章,名称叫《Java is Dead — 5 Misconceptions of developers that still think Java is relevant today!》直译过来...
继续阅读 »


博主在浏览 medium 社区时,发现了一篇点赞量 1.5k 的文章,名称叫《Java is Dead — 5 Misconceptions of developers that still think Java is relevant today!》直译过来就是《Java 已死 — 开发人员对 Java 在现代编程语言中的5个误解》。这篇文章可以说是标题党得典范,热度全靠标题蹭 😂。当然本文重点在于文章评论区。作者因为标题党惨着评论区大佬们怒怼,不敢回复。


原文地址:medium.com/@sidh.thoma… Thomas



推荐博主开源的 H5 商城项目waynboot-mall,这是一套全部开源的微商城项目,包含三个项目:运营后台、H5 商城前台和服务端接口。实现了商城所需的首页展示、商品分类、商品详情、商品 sku、分词搜索、购物车、结算下单、支付宝/微信支付、收单评论以及完善的后台管理等一系列功能。 技术上基于最新得 Springboot3.0、jdk17,整合了 MySql、Redis、RabbitMQ、ElasticSearch 等常用中间件。分模块设计、简洁易维护,欢迎大家点个 star、关注博主。


github 地址:github.com/wayn111/way…



下面是文章内容:



人们仍然认为 Java 与当今时代相关,这是一种常见的误解。事实上 Java 是一种正在消亡的编程语言。 Java 一直是世界上使用最广泛、最流行的编程语言之一,但它很快就会面临消亡的危险。如今 Java 拥有庞大而活跃的开发者社区,并且仍然用于广泛的应用程序,包括 Web 开发、移动应用程序开发和企业级软件开发,但 Java 能在未来 10 年生存吗?让我们看看开发者对 Java 有哪些误解:



误解 1:Java 拥有庞大且活跃的开发者社区。世界各地有数百万 Java 开发人员,该语言在开发人员共享知识和资源的在线论坛和社区中占有重要地位。



虽然情况仍然如此,但开发人员转向其他平台和编程语言的速度很能说明问题,我个人也看到开发人员惊慌失措地跳槽。主要问题是 Java 作为一种编程语言还没有现代化,因此它仍然很冗长,通过一个步履蹒跚但极其笨重的类型系统结合了静态和动态类型之间最糟糕的两个世界,并且要求在具有以下功能的 VM 上运行宏观启动时间(对于长时间运行的服务器来说不是问题,但对于命令行应用程序来说是痛苦的)。虽然它现在表现得相当不错,但它仍然无法与 C 或 C++ 竞争,并且只要有一点爱,C#、Go、Rust 和 Python 就可以或将会在该领域超越它。对于现实世界的生产服务器,它往往需要大量的 JVM 调整,而且很难做到正确。



误解 2:Java 的应用范围很广。 Java 不仅仅是一种 Web 开发语言,还用于开发移动应用程序、游戏和企业级软件。这种多功能性使其成为许多不同类型项目的有价值的语言。



Java 不再是移动应用程序开发(尤其是 Android)首选的编程语言。 Kotlin 现在统治着 Android,大多数 Android 开发者很久以前就已经跳槽了。就连谷歌也因为几年前与甲骨文的惨败而放弃了 Java 作为 Android 的事实上的语言。 Java 作为一种 Web 开发语言也早已失去了它的受欢迎程度。就企业开发而言,Java 在大型企业中仍然适用,因为它可靠且稳定。尽管许多初创公司并未将 Java 作为企业软件的首选,但他们正在使用其他替代方案。



误解 3:Java 是基础语言。许多较新的编程语言都是基于 Java 的原理和概念构建的,并且旨在以某种方式与其兼容。这意味着即使 Java 的受欢迎程度下降,它的原理和概念仍然具有相关性。



虽然 Java 确实是许多人开始编程之旅的基础语言,但事实是 Java 仍然非常陈旧且不灵活。最重要的是,与其他现代编程语言相比,它仍然很冗长,这意味着它需要大量代码来完成某些任务。这会使编写简洁、优雅的代码变得更加困难,并且可能需要更多的精力来维护大型代码库。此外,Java 是静态类型的这一事实意味着它可能比动态类型语言更严格且灵活性较差,这可能会让一些开发人员感到沮丧。



误解 4:Java 得到各大公司的大力支持。 Oracle 是维护和支持 Java 的公司,对该语言有着坚定的承诺,并持续投资于其开发和改进。此外,包括 Google 和 Amazon 在内的许多大公司都在其产品和服务中使用 Java。



Oracle 的 Java 市场份额正在快速被竞争对手夺走。见下图:



尽管下图显示甲骨文仍然拥有最大的市场份额,但其份额已减少了一半以上。 2020 年,甲骨文占据了“大约 75% 的 Java 市场”,而现在的份额还不到 35%。


根据 New Relic 的数据,排名第二的是亚马逊,自 2021 年 11 月发布 Java 17 以来,其份额急剧上升,当时其份额几乎与 Eclipse Adoptium 相同。



误解 5:Java 在学校和大学中广泛教授。 Java 是一种流行的编程概念教学语言,经常用于学校和大学的计算机科学课程。这意味着有源源不断的新开发人员正在学习 Java 并熟悉其功能。



这种情况正在发生很大的变化。渴望成为软件开发人员的年轻大学生正在迅速转向其他编程语言。由于对这些其他编程语言的普遍需求,这越来越多地促使学院和大学寻找替代方案。


我知道这是一个有争议的话题。虽然我也认为 Java 是一种彻底改变了软件编写方式的语言,并为其他编程语言树立了可以效仿的基准。但不幸的是,该语言的所有权掌握在公司手中,在没有留下太多财务收益的情况下,该公司没有动力继续改进它。



OK,文章内容就这么多,下面是本文重点!



评论区



喜闻乐见评论区来了 😎,看看国外开发者怎么反驳这篇文章得,本文选取评论点赞量较高得5条评论放在下文。



评论一


来自Migliorabile



作者不知道什么是编程语言、它为什么存在以及它在哪里使用。

仅因为许多程序员都在应用程序中最简单的部分工作,就认为 Java 与 Python 等效,这是完全错误的。

假设自因为使用自行车的人比驾驶采矿机的人多,我就认为自行车比卡特彼勒采矿机更好,这是不对得。



评论二


来自Khalid Hamid



哈哈哈,我想说他甚至可能不是一个程序员,可能会做一些 JavaScript 的事情,即使如此,将 JavaScript 和 TypeScript 归类为两种语言也是没有意义的。

在安卓开发中,他不明白 Kotlin 是什么,虽然它确实有效。



评论三


来自Dan Decker



每次看到这样的文章我都会直接去看评论。(喜闻乐见评论区🤔)



评论四


来自Max Dancona



对于成熟,我有一些话要说。我过去三份工作中有两份是在一些公司开始使用一种性感的新语言(即 ruby 和 python),然后付钱给像我这样的人用 Java 重写他们的应用程序。



评论五


来自Marco Kneubühler



作者似乎不明白编程语言的风格是出于不同的目的而存在的,语言之间进行比较没有意义, 比如拿 sql 或 html/css 与 java 来比?语言是一个丰富的生态系统,我们需要为特定目的选择正确的语言。因此需要多语言开发人员而不是教条主义。



总结


博主这里说下自己得看法,虽然作者对于自己得观点进行了5个误解的阐述,但是博主是并不认同得。



  • 文章的标题就是一个误导性的问题,暗示了 Java 已经不行。事实上 Java 仍然是一门非常流行和强大的编程语言,它在很多领域都有广泛的应用和优势,如移动应用、Web 应用、可穿戴设备、大数据、云计算等。Java 也有不断地更新和改进,引入了很多新的特性和功能,以适应不断变化的技术需求。

  • Java 也有庞大的社区和丰富的资源,为开发者提供了很多支持和帮助。根据 GitHub Octoverse Report 2022,Java 是第三大最受欢迎的语言,仅次于 JavaScript、Python。根据 JetBrains State of Developer Ecosystem 2022,Java 是过去12个月内使用占有率排名第五的语言,占据了 48% 的份额。根据 StackOverflow Developer Survey 2022,最常用的编程语言排行榜中 Java 是排名第六的语言,占据了 33.27% 的份额。这些数据都表明 Java 并没有死亡或不在流行,而是仍然保持着其重要的地位。


GitHub Octoverse Report 2022


JetBrains State of Developer Ecosystem 2022


StackOverflow Developer Survey 2022



  • 文中说 Java 是一门过时和冗长的语言,它没有跟上时代的变化,而其他语言如 Python、JavaScript 和 Kotlin 等都更加简洁和现代化。这个观点忽略了 Java 的设计哲学和目标。Java 是一门成熟、稳定、跨平台、高性能、易维护、易扩展的编程语言,它注重可读性、健壮性和兼容性。Java 的语法可能相对复杂,但它也提供了很多强大的特性和功能,如泛型、注解、枚举、lambda 表达式、流 API、模块化系统等。

  • Java 也没有停止创新和改进,它在近几年引入了很多新的特性和功能,如 Record 类、密封类、模式匹配、文本块、虚拟线程、外部函数和内存API等。其他语言可能在某些方面比 Java 更加简洁或现代化,但它们也有自己的局限和缺点,比如运行速度慢、类型系统弱、错误处理困难等。不同的语言适合不同的场景和需求,并不是说一种语言就可以完全取代另一种语言。


总之,我觉得 Java 在未来会被替代的可能性很小,但也不能掉以轻心,在后端开发领域,Go 已经在逐步蚕食 Java 得份额,今年非常火得 ai 模型领域相关,大部分代码也是基于 Python 编写。Java 需要在保持优势领域地位后持续地创新和改进。



关注公众号【waynblog】每周分享技术干货、开源项目、实战经验、高效开发工具等,您的关注将是我的更新动力!


作者:waynaqua
来源:juejin.cn/post/7252127579195736119

收起阅读 »

挤爆服务器,北大法律大模型ChatLaw火了:直接告诉你张三怎么判!

语言大模型不断向垂直行业领域拓展,这次出圈的是北大法律大模型。 大模型又「爆了」。 昨晚,一个法律大模型 ChatLaw 登上了知乎热搜榜榜首。热度最高时达到了 2000 万左右。 这个 ChatLaw 由北大团队发布,致力于提供普惠的法律服务。一方面当前全...
继续阅读 »

语言大模型不断向垂直行业领域拓展,这次出圈的是北大法律大模型。



大模型又「爆了」。


昨晚,一个法律大模型 ChatLaw 登上了知乎热搜榜榜首。热度最高时达到了 2000 万左右。


这个 ChatLaw 由北大团队发布,致力于提供普惠的法律服务。一方面当前全国执业律师不足,供给远远小于法律需求;另一方面普通人对法律知识和条文存在天然鸿沟,无法运用法律武器保护自己。


大语言模型最近的崛起正好为普通人以对话方式咨询法律相关问题提供了一个绝佳契机。



目前,ChatLaw 共有三个版本,分别如下:




  • ChatLaw-13B,为学术 demo 版,基于姜子牙 Ziya-LLaMA-13B-v1 训练而来,中文各项表现很好。但是,逻辑复杂的法律问答效果不佳,需要用更大参数的模型来解决;




  • ChatLaw-33B,也为学术 demo 版,基于 Anima-33B 训练而来,逻辑推理能力大幅提升。但是,由于 Anima 的中文语料过少,问答时常会出现英文数据;




  • ChatLaw-Text2Vec,使用 93w 条判决案例做成的数据集,基于 BERT 训练了一个相似度匹配模型,可以将用户提问信息和对应的法条相匹配。




根据官方演示,ChatLaw 支持用户上传文件、录音等法律材料,帮助他们归纳和分析,生成可视化导图、图表等。此外,ChatLaw 可以基于事实生成法律建议、法律文书。该项目在 GitHub 上的 Star 量达到了 1.1k。



官网地址
http://www.chatlaw.cloud/


论文地址
arxiv.org/pdf/2306.16…


GitHub 地址
github.com/PKU-YuanGro…


目前,由于 ChatLaw 项目太过火爆,服务器暂时崩溃,算力已达上限。该团队正在修复,感兴趣的读者可以在 GitHub 上部署测试版模型。


小编本人也还在内测排队中。所以这里先展示一个 ChatLaw 团队提供的官方对话示例,关于日常网购时可能会遇到的「七天无理由退货」问题。不得不说,ChatLaw 回答挺全的。



不过,小编发现,ChatLaw 的学术 demo 版本可以试用,遗憾的是没有接入法律咨询功能,只提供了简单的对话咨询服务。这里尝试问了几个问题。





其实最近发布法律大模型的不只有北大一家。上个月底,幂律智能联合智谱 AI 发布了千亿参数级法律垂直大模型 PowerLawGLM。据悉该模型针对中文法律场景的应用效果展现出了独特优势。


图源:幂律智能


ChatLaw 的数据来源、训练框架


首先是数据组成。ChatLaw 数据主要由论坛、新闻、法条、司法解释、法律咨询、法考题、判决文书组成,随后经过清洗、数据增强等来构造对话数据。同时,通过与北大国际法学院、行业知名律师事务所进行合作,ChatLaw 团队能够确保知识库能及时更新,同时保证数据的专业性和可靠性。下面我们看看具体示例。


基于法律法规和司法解释的构建示例:



抓取真实法律咨询数据示例:



律师考试多项选择题的建构示例:



然后是模型层面。为了训练 ChatLAW,研究团队在 Ziya-LLaMA-13B 的基础上使用低秩自适应 (Low-Rank Adaptation, LoRA) 对其进行了微调。此外,该研究还引入 self-suggestion 角色,来缓解模型产生幻觉问题。训练过程在多个 A100 GPU 上进行,并借助 deepspeed 进一步降低了训练成本。


如下图为 ChatLAW 架构图,该研究将法律数据注入模型,并对这些知识进行特殊处理和加强;与此同时,他们也在推理时引入多个模块,将通识模型、专业模型和知识库融为一体。


该研究还在推理中对模型进行了约束,这样才能确保模型生成正确的法律法规,尽可能减少模型幻觉。



一开始研究团队尝试传统的软件开发方法,如检索时采用 MySQL 和 Elasticsearch,但结果不尽如人意。因而,该研究开始尝试预训练 BERT 模型来进行嵌入,然后使用 Faiss 等方法以计算余弦相似度,提取与用户查询相关的前 k 个法律法规。


当用户的问题模糊不清时,这种方法通常会产生次优的结果。因此,研究者从用户查询中提取关键信息,并利用该信息的向量嵌入设计算法,以提高匹配准确性。


由于大型模型在理解用户查询方面具有显著优势,该研究对 LLM 进行了微调,以便从用户查询中提取关键字。在获得多个关键字后,该研究采用算法 1 检索相关法律规定。



实验结果


该研究收集了十余年的国家司法考试题目,整理出了一个包含 2000 个问题及其标准答案的测试数据集,用以衡量模型处理法律选择题的能力。


然而,研究发现各个模型的准确率普遍偏低。在这种情况下,仅对准确率进行比较并无多大意义。因此,该研究借鉴英雄联盟的 ELO 匹配机制,做了一个模型对抗的 ELO 机制,以便更有效地评估各模型处理法律选择题的能力。以下分别是 ELO 分数和胜率图:



通过对上述实验结果的分析,我们可以得出以下观察结果


(1)引入与法律相关的问答和法规条文的数据,可以在一定程度上提高模型在选择题上的表现;


(2)加入特定类型任务的数据进行训练,模型在该类任务上的表现会明显提升。例如,ChatLaw 模型优于 GPT-4 的原因是文中使用了大量的选择题作为训练数据;


(3)法律选择题需要进行复杂的逻辑推理,因此,参数量更大的模型通常表现更优。


参考资料



[1]https://www.zhihu.com/question/610072848

[2]https://mp.weixin.qq.com/s/bXAFALFY6GQkL30j1sYCEQ


作者:夕小瑶科技说
来源:juejin.cn/post/7252172628450541624

收起阅读 »

十年码农内功:分布式

分布式协议一口气学完 一、Raft 协议 1.1 基础概念 Raft 算法是通过一切以领导者为准的方式,实现一系列数据的共识和各节点日志的一致。Raft是强领导模型,集群中只能有一个领导者。 成员身份:领导者(Leader)、跟随者(Follower)和候选...
继续阅读 »

分布式协议一口气学完



一、Raft 协议


1.1 基础概念


Raft 算法是通过一切以领导者为准的方式,实现一系列数据的共识和各节点日志的一致。Raft是强领导模型,集群中只能有一个领导者。


成员身份:领导者(Leader)、跟随者(Follower)和候选者(Candidate)。



图1 成员身份



  • 领导者:集群中霸道总裁,一切以我为准,处理写请求、管理日志复制和不断地发送心跳信息。

  • 跟随者:普通成员,处理领导者发来的消息,发现领导者心跳超时,推荐自己成为候选人。

  • 候选人:先给自己投一票,然后请求其他集群节点投票给自己,得票多者成为新的领导者。

  • 任期编号:每任领导者都有任期编号。当领导者心跳超时,跟随者会变成候选人,任期编号 +1,然后发起投票。任期编号小的服从编号大的。

  • 心跳超时:每个跟随者节点都设置了随机心跳超时时间,目的是避免跟随者们同时成为候选人,同时发起投票。

  • 选举超时:每轮选举的结束时间,随机选举超时时间可以避免多个候选人同时结束选举未果,然后同时发起下一轮选举


1.2 领导选举


1.2.1 选举规则



  • 领导者周期性地向跟随者发送心跳消息,告诉跟随者我是领导者,阻止跟随者发变成候选人发起新选举;

  • 如果跟随者的随机心跳超时了,那么认为没有领导者了,推荐自己为候选人,发起新的选举;

  • 在一轮选举中,赢得一半以上选票的候选人,即成为新的领导者;

  • 在一轮选举中,每个节点对每个任期编号的选举只能投出一票,先来先服务原则;

  • 日志完整性高(也就是最后一条日志项对应的任期编号值更大,索引号更大)的跟随者A拒绝给完整性低的候选人B投票,即使B的任期编号大;


1.2.2 选举动画



图2 初始选举



图3 领导者宕机/断网



图4 第一轮选举未果,发起第二轮选举


1.3 日志复制


日志项(Log Entry):是一种数据格式,它主要包含索引值(Log index)、任期编号(Term)和 指令(Command)。



  • 索引值:它是用来标识日志项的,是一个连续的、单调递增的整数值。

  • 任期编号:创建这条日志项的领导者的任期编号。

  • 指令:一条由客户端请求指定的、服务需要执行的指令。



图5 日志信息


1.3.1 日志复制动画



图6 简单日志复制



图7 复杂日志复制


1.3.2 日志恢复


每次选举出来的Leader一定包含在多数节点上最新的已经提交的日志,新的Leader将会覆盖其他节点上不一致的数据。


虽然新Leader一定包括上一个Term的Leader已提交(Committed)日志,但是可能也包含上一个Term的Leader的未提交(Uncommitted)日志。


这部分未提交日志需要转变为Committed,相对比较麻烦,需要考虑Leader多次切换且未完成日志恢复,需要保证最终提案是一致的、确定的,不然就会产生所谓的幽灵复现问题。


为了将上一个Term未提交的日志转为已提交,Raft算法要求Leader当选后立即追加一条Noop的特殊内部日志,并立即同步到其它节点,实现前面未提交日志全部隐式提交。


这样保证客户端不会读到未提交数据,因为只有Noop被大多数节点同意并提交了之后(这样可以连带往期日志一起同步),服务才会对外正常工作;


Noop日志本身是一个分界线,Noop之前的日志被提交,之后的日志将会被丢弃。Noop日志仅包含任期编号和日志索引值,没有指令。


日志“幽灵复现”的场景



图8


第一步,A是领导者,在本地记录4和5日志,并没有提交,然后挂了。



图9


第二步,由于B的日志索引值比C的大,B成为了领导者,仅把日志3同步给了C,然后挂了。



图10


第三步,A恢复了,并且成为了领导者,然后把未提交的日志4和5同步给了B和C(C在A成为了领导者之后、同步日志之前恢复了),然后ABC都提交了日志4和5,就这样原本客户端写失败的日志4和5复活了,进而客户端会读到其认为未提交的日志(实际上集群日志已提交)。


Noop解决日志复现


第一步,同上面一样。


第二步,由于B的日志索引值比C的大,B成为了领导者,这次不仅把日志3同步给了C,还记录了一个Noop日志,并且同步给了C。



图11


第三步,当A恢复了,想成为领导者,发现自己的日志任期编号和日志索引值都不是最大的,即使B挂了也还有C,A也就成为不了领导者,乖乖使用B的日志覆盖自己的日志。


1.4 成员变更


集群成员变更最大的风险是可能同时出现 2 个领导者。比如在成员变更时,节点 A、B 和 C 之间发生了分区错误,节点 A、B 组成旧集群(ABC)中的“大多数”。


而节点 C 和新节点 D、E 组成了新集群(ABCDE)的“大多数”,它们可能会选举出新的领导者(比如节点 C)。结果出现了同时存在 2 个领导者的情况。违反了Raft协议中领导者唯一性原则。



图12 集群(ABC)同时增加节点D和E


最初解决办法是联合共识(Joint Consensus),但实现起来难,后来 Raft 的作者就提出了一种改进后的方法,单节点变更(single-server changes)。


在正常情况下,旧集群的“大多数”和新集群的“大多数”都会有一个重叠的节点。



图13 集群(ABCD)增加新节点E



图14 集群(ABCDE)删除节点A



图15 集群(ABC)增加新节点D



图16 集群(ABCD)删除节点A


需要注意的是,在分区错误、节点故障等情况下,如果并发执行单节点变更,那么就可能出现一次单节点变更尚未完成,新的单节点变更又在执行,导致集群出现 2 个领导者的情况。


二、Gossip 协议


Gossip协议,顾名思义,就像流言蜚语一样,利用一种随机、带有传染性的方式,将信息传播到整个网络中,并在一定时间内,使得系统内的所有节点数据一致。Gossip其实是一种去中心化思路的分布式协议,解决信息在集群中的传播和最终一致性。


2.1 原理


Gossip协议的消息传播主要有两种:反熵(Anti-Entropy)和谣言传播(Rumor-Mongering)。


2.1.1 反熵:节点相对固定,节点数量不多,以固定概率传播所有的数据


每个节点周期性地随机选择其他节点,通过互相交换各自的所有数据来消除两者之间的差异,实现数据的最终一致性。反熵非常可靠,但每次节点两两交换各自的所有数据会带来非常大的通信负担,因此不会频繁使用。通过引入校验和等机制,可以降低需要对比的数据量和传播消息量。


反熵 使用“simple epidemics”方式,其包含两种状态:susceptible和infective,这种模型也称为SI model。处于infective状态的节点代表其有数据更新,并且会将这个数据分享给其他节点;处于susceptible状态的节点代表其并没有收到来自其他节点的更新。



图17 反熵


2.2.2 谣言传播:节点动态变化,节点数量较多,仅传播新到达的数据


当一个节点有了新信息后,这个节点变成活跃状态,并周期性地向其他节点传播新信息。直到所有的节点都知道该新信息。由于节点之间只传播新信息,所以大大减少了通信负担。


谣言传播 使用“complex epidemics”方法,比反熵 多了一种状态:removed,这种模型也称为SIR model。处于removed状态的节点说明其已经接收到来自其他节点的更新,但是其并不会将这个更新分享给其他节点。因为谣言消息会在某个时间标记为removed,然后不会再被传播给其他节点,所以谣言传播有极小概率使得所有节点数据不一致。



图18 谣言传播


一般来说,为了在通信代价和可靠性之间取得折中,需要将这两种方法结合使用。


2.2 通信方式


节点间的交互主要有三种方式:推、拉和推/拉



图19 节点状态


2.2.1 推送模式(push)


节点A随机选择联系节点B,并向其发送自己的信息,节点B在收到信息后比较/更新自己的数据。



图20 推方式


2.2.2 拉取模式(pull)


节点A随机选择联系节点B,从对方获取信息,节点A在收到信息后比较/更新自己的数据。



图21 拉方式


2.2.3 推/拉模式(push/pull)


节点A向选择的节点B发送信息,同时从对方获取信息,节点A和节点B在收到信息后各自比较/更新自己的数据。



图22 推/拉方式


2.3 优缺点



  • 优点

    • 可扩展性(Scalable): Gossip协议是可扩展的,一般需要 O(logN) 轮就可以将信息传播到所有的节点,其中N代表节点的个数。每个节点仅发送固定数量的消息,并且与网络中节点数目无关。在数据传送时,节点并不会等待消息的Ack,所以消息传送失败也没有关系,因为可以通过其他节点将消息传递给之前传送失败的节点。允许节点的任意增加和减少,新增节点的数据最终会与其他节点一致。

    • 容错(Fault-tolerance): 网络中任何节点的重启或者宕机都不会影响 Gossip 消息的传播,具有天然的分布式系统容错特性。

    • 健壮性(Robust): Gossip协议是去中心化的协议,集群中的所有节点都是对等的,没有特殊的节点,所以任何节点出现问题都不会阻止其他节点继续发送消息。任何节点都可以随时加入或离开,而不会影响系统的整体服务质量。

    • 最终一致性(Convergent consistency): Gossip协议实现信息指数级的快速传播,因此在有新信息需要传播时,消息可以快速地发送到全局节点,在有限的时间内能够做到所有节点都拥有最新的数据。



  • 缺点

    • 消息延迟:节点随机向少数几个节点发送消息,消息最终是通过多个轮次的散播而到达全网,不可避免的造成消息延迟。

    • 消息冗余:节点定期随机选择周围节点发送消息,而收到消息的节点也会重复该步骤,不可避免地引起同一节点消息多次接收,增加消息处理压力。




三、参考


作者:科英
来源:juejin.cn/post/7251501954156855352

收起阅读 »

用 node 实战一下 CSRF

web
前言 之前面试经常被问到 CSRF, 跨站请求伪造 大概流程比较简单, 大概就是用户登录了A页面,存下来登录凭证(cookie), 攻击者有诱导受害者打开了B页面, B页面中正好像A发送了一个跨域请求,并把cookie进行了携带, 欺骗浏览器以为是用户的行为...
继续阅读 »

前言


之前面试经常被问到 CSRF, 跨站请求伪造



大概流程比较简单, 大概就是用户登录了A页面,存下来登录凭证(cookie), 攻击者有诱导受害者打开了B页面, B页面中正好像A发送了一个跨域请求,并把cookie进行了携带, 欺骗浏览器以为是用户的行为,进而达到执行危险行为的目的,完成攻击



上面就是面试时,我们通常的回答, 但是到底是不是真是这样呢? 难道这么容易伪造吗?于是我就打算试一下能不能实现


接下来,我们就通过node起两个服务 A服务(端口3000)和B服务(端口4000), 然后通过两个页面 A页面、和B页面模拟一下CSRF。


我们先约定一下 B页面是正常的页面, 起一个 4000 的服务, 然后 A页面为伪造者的网站, 服务为3000


先看B页面的代码, B页面有一个登录,和一个获取数据的按钮, 模拟正常网站,需要登录后才可以获取数据


<body>
<div>
正常 页面 B
<button onclick="login()">登录</button>
<button onclick="getList()">拿数据</button>
<ul class="box"></ul>
<div class="tip"></div>
</div>
</body>
<script>
async function login() {
const response = await fetch("http://localhost:4000/login", {
method: "POST",
});
const res = await response.json();
console.log(res, "writeCookie");
if (res.data === "success") {
document.querySelector(".tip").innerHTML = "登录成功, 可以拿数据";
}
}

async function getList() {
const response = await fetch("http://localhost:4000/list", {
method: "GET",
});

if (response.status === 500) {
document.querySelector(".tip").innerHTML = "cookie失效,请先登录!";
document.querySelector(".box").innerHTML = "";
} else {
document.querySelector(".tip").innerHTML = "";
const data = await response.json();
let html = "";
data.map((el) => {
html += `<div>${el.id} - ${el.name}</div>`;
});
document.querySelector(".box").innerHTML = html;
}
}
</script>

在看B页面的服务端代码如下:


const express = require("express");
const app = express();

app.use(express.json()); // json
app.use(express.urlencoded({ extends: true })); // x-www-form-urlencoded

app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
// 允许客户端跨域传递的请求头
res.header("Access-Control-Allow-Headers", "Content-Type");
next();
});

app.use(express.static("public"));

app.get("/list", (req, res) => {
const cookie = req.headers.cookie;
if (cookie !== "user=allow") {
res.sendStatus("500");
} else {
res.json([
{ id: 1, name: "zhangsan" },
{ id: 2, name: "lisi" },
]);
}
});

app.post("/login", (req, res) => {
res.cookie("user", "allow", {
expires: new Date(Date.now() + 86400 * 1000),
});
res.send({ data: "success" });
});

app.post("/delete", (req, res) => {
const cookie = req.headers.cookie;
if (req.headers.referer !== req.headers.host) {
console.log("should ban!");
}
if (cookie !== "user=allow") {
res.sendStatus("500");
} else {
res.json({
data: "delete success",
});
}
});

app.listen(4000, () => {
console.log("sever 4000");
});

B 服务有三个接口, 登录、获取列表、删除。 再触发登录接口的时候,会像浏览器写入cookie, 再删除或者获取列表的时候,都先检测有没有将指定的cookie传回,如果有就认为有权限


然后我们打开 http://localhost:4000/B.html 先看看B页面功能是否都正常


image.png


我们看到此时 B 页面功能和接口都是正常的, cookie 也正常进行了设置,每次获取数据的时候,都是会携带cookie到服务端校验的


那么接下来我们就通过A页面,起一个3000端口的服务,来模拟一下跨域情况下,能否完成获取 B服务器数据,调用 B 服务器删除接口的功能


A页面代码


  <body>
<div>
伪造者页面 A
<form action="http://localhost:4000/delete" method="POST">
<input type="hidden" name="account" value="xiaoming" />
</form>
<script>
// 这行可以放到控制台执行,便于观察效果
// document.forms[0].submit();
</script>
</div>
<ul class="box"></ul>
<div class="tip"></div>
</body>

A页面服务端代码


  <body>
<div>
伪造者页面 A
<form action="http://localhost:4000/delete" method="POST">
<input type="hidden" name="account" value="xiaoming" />
</form>
<script>
// 这行可以放到控制台输入
// document.forms[0].submit();
</script>
<script src="http://localhost:4000/list"></script>
</div>

</body>

于是在我们 访问 http://localhost:3000/A.html 页面的时候发现, 发现list列表确实,请求到了, 控制台输入 document.forms[0].submit() 时发现,确实删除也发送成功了, 是不是说明csrf就成功了呢, 但是其实还不是, 关键的一点是, 我们在B页面设置cookie的时候, domain设置的是 localhost 那么其实在A页面, 发送请求的时候cookie是共享的状态, 真实情况下,肯定不会是这样, 那么为了模拟真实情况, 我们把 http://localhost:3000/A.html 改为 http://127.0.0.1:3000/A.html, 这时发现,以及无法访问了, 那么这是怎么回事呢, 说好的,cookie 会在获取过登录凭证下, 再次访问时可以携带呢。


image.png


于是,想了半天也没有想明白, 难道是浏览器限制严格进行了限制, 限制规避了这个问题? 难道我们背的面试题是错误的?


有知道的

作者:重阳微噪
来源:juejin.cn/post/7250374485567340603
小伙伴,欢迎下方讨论

收起阅读 »

前端流程图插件对比选型

web
前言 前端领域有多种流程库可供选择,包括但不限于vue-flow、butterfly、JointJS、AntV G6、jsPlumb和Flowchart.js。这些库都提供了用于创建流程图、图形编辑和交互的功能。然而,它们在特性、易用性和生态系统方面存在一些差...
继续阅读 »

Snipaste_2023-07-04_15-49-12.png


前言


前端领域有多种流程库可供选择,包括但不限于vue-flow、butterfly、JointJS、AntV G6、jsPlumb和Flowchart.js。这些库都提供了用于创建流程图、图形编辑和交互的功能。然而,它们在特性、易用性和生态系统方面存在一些差异。


流程图插件汇总


序号名称地址
1vue-flowgithub.com/bcakmakoglu…
2butterflygithub.com/alibaba/but…
3JointJShttp://www.jointjs.com/
4AntV G6antv-2018.alipay.com/zh-cn/g6/3.…
5jsPlumbgithub.com/jsplumb/jsp…
6Flowchart.jsgithub.com/adrai/flowc…

流程图插件分析


vue-flow


简介


vue-flowReactFlow 的 Vue 版本,目前只支持 在Vue3中使用,对Vue2不兼容,目前国内使用较少。包含四个功能组件 core、background、controls、minimap,可按需使用。


使用


Vue FlowVue下流程绘制库。安装:
npm i --save @vue-flow/core 安装核心组件
npm i --save @vue-flow/background 安装背景组件
npm i --save @vue-flow/controls 安装控件(放大,缩小等)组件
npm i --save @vue-flow/minimap 安装缩略图组件

引入组件:
import { Panel, PanelPosition, VueFlow, isNode, useVueFlow } from '@vue-flow/core'
import { Background } from '@vue-flow/background'
import { Controls } from '@vue-flow/controls'
import { MiniMap } from '@vue-flow/minimap'

引入样式:
@import '@vue-flow/core/dist/style.css';
@import '@vue-flow/core/dist/theme-default.css';

优缺点分析


优点:



  1. 轻松上手:内置缩放和平移功能、元素拖动、选择等等。

  2. 可定制:使用自定义节点、边缘和连接线并扩展Vue Flow的功能。

  3. 快速:链路被动更改,仅重新渲染适当的元素。

  4. 工具和组合:带有图形助手和状态可组合函数,用于高级用途。

  5. 附加组件:背景(内置模式、高度、宽度或颜色),小地图(右下角)、控件(左下角)。


缺点:



  1. 仓库迭代版本较少,2022年进入首次迭代。

  2. 国内使用人数少,没有相关技术博客介绍,通过官网学习。


butterfly


简介


Butterfly是由阿里云-数字产业产研部孵化出来的的图编辑器引擎,具有使用自由、定制性高的优势,已支持上百张画布。号称 “杭州余杭区最自由的图编辑器引擎”。


使用



  • 安装


//
npm install butterfly-dag --save


  • 在 Vue3 中使用


<script lang="ts" setup>
import {TreeCanvas, Canvas} from 'butterfly-dag';
const root = document.getElementById('chart')
const canvas = new Canvas({
root: root,
disLinkable: true, // 可删除连线
linkable: true, // 可连线
draggable: true, // 可拖动
zoomable: true, // 可放大
moveable: true, // 可平移
theme: {
edge: {
shapeType: "AdvancedBezier",
arrow: true,
arrowPosition: 0.5, //箭头位置(0 ~ 1)
arrowOffset: 0.0, //箭头偏移
},
},
});
canvas.draw(mockData, () => {
//mockData为从mock中获取的数据
canvas.setGridMode(true, {
isAdsorb: false, // 是否自动吸附,默认关闭
theme: {
shapeType: "circle", // 展示的类型,支持line & circle
gap: 20, // 网格间隙
background: "rgba(0, 0, 0, 0.65)", // 网格背景颜色
circleRadiu: 1.5, // 圆点半径
circleColor: "rgba(255, 255, 255, 0.8)", // 圆点颜色
},
});
});
</script>

<template>
<div class="litegraph-canvas" id="chart"></div>
</template>

优缺点分析


优点:



  1. 轻松上手:基于dom的设计模型大大方便了用户的入门门槛,提供自定义节点,锚点的模式大大降低了用户的定制性。

  2. 多技术栈支持:支持 jquery 基于 dom 的设计,也包含 butterfly-react、butterfly-vue 两种设计。

  3. 核心概念少而精:提供 画布(Canvas)、节点(Node)、线(Edge)等核心概念。

  4. 优秀的组件库支持:对于当前使用组件库来说,可以大量复用现有的组件。


缺点:



  1. butterfly 对 Vue的支持不是特别友好,这跟阿里的前端技术主栈为React有关,butterfly-vue库只支持 Vue2版本。在Vue3上使用需要对 butterfly-drag 进行封装。


JointJS


简介


创建静态图表或完全交互式图表工具,例如工作流编辑器、流程管理工具、IVR 系统、API 集成器、演示应用程序等等。


属于闭源收费项目,暂不考虑。


AntV G6


简介


AntV 是蚂蚁金服全新一代数据可视化解决方案,致力于提供一套简单方便、专业可靠、无限可能的数据可视化最佳实践。G6 是一个图可视化引擎。它提供了图的绘制、布局、分析、交互、动画等图可视化的基础能力。G6可以实现很多d3才能实现的可视化图表。


使用



  • 安装


npm install --save @antv/g6	//安装


  • 在所需要的文件中引入


<template>
/* 图的画布容器 */
<div id="mountNode"></div>
</template>

<script lang="ts" setup>
import G6 from '@antv/g6';
// 定义数据源
const data = {
// 点集
nodes: [
{
id: 'node1',
x: 100,
y: 200,
},
{
id: 'node2',
x: 300,
y: 200,
},
],
// 边集
edges: [
// 表示一条从 node1 节点连接到 node2 节点的边
{
source: 'node1',
target: 'node2',
},
],
};

// 创建 G6 图实例
const graph = new G6.Graph({
container: 'mountNode', // 指定图画布的容器 id
// 画布宽高
width: 800,
height: 500,
});
// 读取数据
graph.data(data);
// 渲染图
graph.render();
</script>



优缺点分析


优点:



  1. 强大的可定制性:G6 提供丰富的图形表示和交互组件,可以通过自定义配置和样式来实现各种复杂的图表需求。

  2. 全面的图表类型支持:G6 支持多种常见图表类型,如关系图、流程图、树图等,可满足不同领域的数据可视化需求。

  3. 高性能:G6 在底层图渲染和交互方面做了优化,能够处理大规模数据的展示,并提供流畅的交互体验。


缺点:



  1. 上手难度较高:G6 的学习曲线相对较陡峭,需要对图形语法和相关概念有一定的理解和掌握。

  2. 文档相对不完善:相比其他成熟的图表库,G6 目前的文档相对较简单,部分功能和使用方法的描述可能不够详尽,需要进行更深入的了解与实践。


jsPlumb


简介


一个用于创建交互式、可拖拽的连接线和流程图的 JavaScript 库。它在 Web 应用开发中广泛应用于构建流程图编辑器、拓扑图、组织结构图等可视化操作界面。


使用


<template>
<div ref="container">
<div ref="sourceElement">Source</div>
<div ref="targetElement">Target</div>
</div>

</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { jsPlumb } from 'jsplumb';

const container = ref<HTMLElement | null>(null);
const sourceElement = ref<HTMLElement | null>(null);
const targetElement = ref<HTMLElement | null>(null);

onMounted(() => {
// 创建 jsPlumb 实例
const jsPlumbInstance = jsPlumb.getInstance();

// 初始化 jsPlumb 实例设置
if (container.value) {
jsPlumbInstance.setContainer(container.value);
}

// 创建连接线
if (sourceElement.value && targetElement.value) {
jsPlumbInstance.connect({
source: sourceElement.value,
target: targetElement.value,
});
}
});
</script>

优缺点分析


优点:



  1. 简单易用:jsPlumb 提供了直观的 API 和丰富的文档,比较容易上手和使用。

  2. 可拓展性:允许开发人员根据自己的需求进行定制和扩展,使其适应不同的应用场景。

  3. 强大的连接功能:jsPlumb 允许创建各种连接类型,包括直线、曲线和箭头等,满足了复杂交互需求的连接效果。
    缺点:

  4. 文档更新不及时:有时候,jsPlumb 的官方文档并没有及时更新其最新版本的特性和用法。

  5. 性能考虑:在处理大量节点、连接线或复杂布局时,jsPlumb 的性能可能受到影响,需要进行优化。


Flowchart.js


简介


Flowchart.js 是一款开源的JavaScript流程图库,可以使用最短的语法来实现在页面上展示一个流程图,目前大部分都是用在各大主流 markdown 编辑器中,如掘金、csdn、语雀等等。


使用


flowchat
start=>start: 开始
end=>end: 结束
input=>inputoutput: 我的输入
output=>inputoutput: 我的输出
operation=>operation: 我的操作
condition=>condition: 确认
start->input->operation->output->condition
condition(yes)->end
condition(no)->operation

优缺点


优点:



  1. 使用方便快捷,使用几行代码就可以生成一个简单的流程图。

  2. 可移植:在多平台上只需要写相同的代码就可以实现同样的效果。


缺点:



  1. 可定制化限制:对于拥有丰富需求的情况下,flowchartjs只能完成相对简单的需求,没有高级的定制化功能。

  2. 需要花费一定时间来学习他的语法和规则,但是flowchartjs的社区也相对不太活跃。


对比分析




  1. 功能和灵活性:



    • Butterfly、G6 和 JointJS 是功能较为丰富和灵活的库。它们提供了多种节点类型、连接线样式、布局算法等,并支持拖拽、缩放、动画等交互特性。

    • Vue-Flow 来源于 ReactFlow 基于 D3和vueuse等库,提供了 Vue 组件化的方式来创建流程图,并集成了一些常见功能。

    • jsPlumb 专注于提供强大的连接线功能,具有丰富的自定义选项和功能。

    • Flowchart.js 则相对基础,提供了构建简单流程图的基本功能。




  2. 技术栈和生态系统:



    • Vue-Flow 是基于 Vue.js 的流程图库,与 Vue.js 生态系统无缝集成。

    • Butterfly 是一个基于 TypeScript 的框架,适用于现代 Web 开发。

    • JointJS、AntV G6 和 jsPlumb 可以与多种前端框架(如Vue、React、Angular等)结合使用。

    • AntV G6 是 AntV 团队开发的库,其背后有强大的社区和文档支持。




  3. 文档和学习曲线:



    • Butterfly、G6 和 AntV G6 都有完善的文档和示例,提供了丰富的使用指南和教程。

    • JointJS 和 jsPlumb 也有较好的文档和示例资源,但相对于前三者较少。

    • Flowchart.js 的文档相对较少。




  4. 兼容性:



    • Butterfly、JointJS 和 G6 库在现代浏览器中表现良好,并提供了兼容低版本浏览器

    • 作者:WayneX
      来源:juejin.cn/post/7251835247595110457
      l>

收起阅读 »

全局唯一ID生成

在Java中,可以使用多种方法生成唯一的ID。下面我将介绍几种常用的方法: UUID(Universally Unique Identifier):UUID是一种128位的唯一标识符。它可以通过java.util.UUID类来生成,使用UUID.rando...
继续阅读 »

在Java中,可以使用多种方法生成唯一的ID。下面我将介绍几种常用的方法:




  1. UUID(Universally Unique Identifier):UUID是一种128位的唯一标识符。它可以通过java.util.UUID类来生成,使用UUID.randomUUID()方法返回一个新的UUID。UUID的生成是基于时间戳和计算机MAC地址等信息,因此几乎可以保证全局唯一性。


    import java.util.UUID;

    public class UniqueIdExample {
    public static void main(String[] args) {
    UUID uuid = UUID.randomUUID();
    String id = uuid.toString();
    System.out.println(id);
    }
    }



  2. 时间戳:可以使用当前时间戳作为唯一ID。使用System.currentTimeMillis()方法可以获取当前时间的毫秒数作为ID值。需要注意的是,时间戳只是在同一台机器上保持唯一性,在分布式系统中可能存在重复的风险。


    public class UniqueIdExample {
    public static void main(String[] args) {
    long timestamp = System.currentTimeMillis();
    String id = String.valueOf(timestamp);
    System.out.println(id);
    }
    }



  3. Snowflake算法:Snowflake是Twitter开源的一种分布式ID生成算法,可以生成带有时间戳、机器ID和序列号的唯一ID。可以使用第三方库(如Twitter的Snowflake)来生成Snowflake ID。Snowflake ID的生成是基于时间序列、数据中心ID和机器ID等参数的。


    import com.twitter.snowflake.SnowflakeIdGenerator;

    public class UniqueIdExample {
    public static void main(String[] args) {
    SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator();
    long id = idGenerator.nextId();
    System.out.println(id);
    }
    }



以上是一些常用的生成唯一ID的方法,每种方法都有自己的特点和适用场景。选择合适的方法要根据具体需求、性能要求

作者:Lemonade22
来源:juejin.cn/post/7250037058684583995
以及系统架构来决定。

收起阅读 »

为什么选择 Next.js 框架?

web
前言 Next.js 框架作为一种强大而受欢迎的工具,为开发人员提供了许多优势和便利。本文将探讨 Next.js 框架的优点,并解释为什么选择 Next.js 是一个明智的决策。 文档:nextjs.org/docs 强大的服务端渲染和静态生成能力: Ne...
继续阅读 »

前言


Next.js 框架作为一种强大而受欢迎的工具,为开发人员提供了许多优势和便利。本文将探讨 Next.js 框架的优点,并解释为什么选择 Next.js 是一个明智的决策。



文档:nextjs.org/docs



强大的服务端渲染和静态生成能力:


Next.js 框架提供了先进的服务端渲染(SSR)和静态生成(SSG)能力,使得我们能够在服务器上生成动态内容并将其直接发送给客户端,从而大大减少首次加载的等待时间。这样可以提高网站的性能、搜索引擎优化(SEO)以及用户体验。


简化的数据获取:


Next.js 提供了简单易用的数据获取方法,例如 getServerSidePropsgetStaticProps,使得从后端获取数据并将其注入到组件中变得非常容易。这种无缝的数据获取流程,可以让开发人员专注于业务逻辑而不用过多关注数据获取的细节。


优化的路由系统:


Next.js 内置了灵活而强大的路由功能,使得页面之间的导航变得简单直观。通过自动化的路由管理,我们可以轻松地构建复杂的应用程序,并实现更好的用户导航体验。


支持现代前端技术栈:


Next.js 是建立在 React 生态系统之上的,因此可以充分利用 React 的强大功能和丰富的社区资源。同时,Next.js 也支持最新的 JavaScript(ES6+)特性,如箭头函数、模块化导入导出、解构赋值等,让开发人员可以使用最新的前端技术来构建现代化的应用。


简化的部署和扩展:


Next.js 提供了轻松部署和扩展应用程序的工具和解决方案。借助 Vercel、Netlify 等平台,我们可以快速将应用程序部署到生产环境,并享受高性能、弹性扩展的好处。Next.js 还支持构建静态站点,可以轻松地将应用部署到 CDN 上,提供更快的加载速度和更好的全球可访问性。


大型社区支持:


Next.js 拥有庞大的开发者社区,其中有许多优秀的开源项目和库。这意味着你可以从社区中获取到大量的学习资源、文档和支持。无论是在 Stack Overflow 上寻求帮助,还是参与讨论,你都能够从其他开发人员的经验中获益。


什么环境下需要选择nextjs框架?


需要服务端渲染或静态生成:


如果你的应用程序需要在服务器端生成动态内容,并将其直接发送给客户端,以提高性能和搜索引擎优化,那么 Next.js 是一个很好的选择。它提供了强大的服务端渲染和静态生成能力,使得构建高性能的应用变得更加简单。


需要快速开发和部署:


Next.js 提供了简化的开发流程和快速部署的解决方案。它具有自动化的路由管理、数据获取和构建工具,可以提高开发效率。借助 Vercel、Netlify 等平台,你可以轻松地将 Next.js 应用部署到生产环境,享受高性能和弹性扩展的好处。


基于 React 的应用程序:


如果你已经熟悉 React,并且正在构建一个基于 React 的应用程序,那么选择 Next.js 是自然而然的。Next.js 是建立在 React 生态系统之上的,提供了与 React 紧密集成的功能和工具。


需要良好的 SEO 和页面性能:


如果你的应用程序对搜索引擎优化和良好的页面性能有较高的要求,Next.js 可以帮助你实现这些目标。通过服务端渲染和静态生成,Next.js 可以在初始加载时提供完整的 HTML 内容,有利于搜索引擎索引和页面的快速呈现。


需要构建现代化的单页应用(SPA):


尽管 Next.js 可以支持传统的多页面应用(MPA),但它也非常适合构建现代化的单页应用(SPA)。你可以使用 Next.js 的路由系统、数据获取和状态管理功能,构建出功能丰富且响应快速的 SPA。


与nextjs相似的框架?


Nuxt.js:


Nuxt.js 是一个基于 Vue.js 的应用框架,提供了类似于 Next.js 的服务端渲染和静态生成功能。它通过使用 Vue.js 的生态系统,使得构建高性能、可扩展的 Vue.js 应用变得更加简单。


Gatsby:


Gatsby 是一个基于 React 的静态网站生成器,具有类似于 Next.js 的静态生成功能。它使用 GraphQL 来获取数据,并通过预先生成静态页面来提供快速的加载速度和良好的SEO。


Angular Universal:


Angular Universal 是 Angular 框架的一部分,提供了服务端渲染的能力。它可以生成动态的 HTML 内容,从而加快首次加载速度,并提供更好的 SEO 和用户体验。


Sapper:


Sapper 是一个基于 Svelte 的应用框架,支持服务端渲染和静态生成。它提供了简单易用的工具和流畅的开发体验,帮助开发者构建高性能的 Sv

作者:嚣张农民
来源:juejin.cn/post/7251875626906599485
elte 应用程序。

收起阅读 »

为什么你非常不适应 TypeScript

web
前言 在群里看到一些问题和言论:为什么你们这么喜欢“类型体操”?为什么我根本学不下去 TypeScript?我最讨厌那些做类型体操的了;为什么我学了没过多久马上又忘了? 有感于这些问题,我想从最简单的一个角度来切入介绍一下 TypeScript,并向大家介绍并...
继续阅读 »

前言


在群里看到一些问题和言论:为什么你们这么喜欢“类型体操”?为什么我根本学不下去 TypeScript?我最讨厌那些做类型体操的了;为什么我学了没过多久马上又忘了?


有感于这些问题,我想从最简单的一个角度来切入介绍一下 TypeScript,并向大家介绍并不是只要是个类型运算就是体操。并在文中介绍一种基本思想作为你使用类型系统的基本指引。


引子


我将从一个相对简单的 API 的设计过程中阐述关于类型的故事。在这里我们可以假设我们现在是一个工具的开发者,然后我们需要设计一个 API 用于从对象中拿取指定的一些 key 作为一个新的对象返回给外面使用。


垃圾 TypeScript


一个人说:我才不用什么破类型,我写代码就是要没有类型,我就是要随心所欲的写。然后写下了这段代码。


declare function pick(target: any, ...keys: any): any

他的用户默默的写下了这段代码:


pick(undefined, 'a', 1).b

写完运行,发现问题大条了,控制台一堆报错,接口数据也提交不上去了,怎么办呢?


刚学 TypeScript


一个人说:稍微检查一下传入类型就好了,别让人给我乱传参数就行。


declare function pick(target: Record<string, unknown>, ...keys: string[]): unknown

很好,上面的问题便不复存在了,API 也是基本可用的了。但是!当对象复杂的时候,以及字段并不是短单词长度的时候就会发现了一个没解决的问题。


pick({ abcdefghijkl: '123' }, 'abcdefghikjl')

从肉眼角度上,我们很难发现这前后的不一致,所以我们为什么要让调用方的用户自己去 check 自己的字段有没有写对呢?


不就 TypeScript


一个人说:这还不简单,用个泛型加 keyof 不就行了。


declare function pick<
T extends Record<string, unknown>
>(target: T, ...keys: keyof T[]): unknown

我们又进一步解决的上面的问题,但是!还是有着相似的问题,虽然我们不用检查 keys 是不是传入的是一个正确的值了,但是我们实际上对返回的值也存在一个类似的问题。


pick({ abcdefghijkl: '123' }, 'abcdefghijkl').abcdefghikjl



  • 一点小小的拓展


    在这里我们看起来似乎是一个很简单的功能,但实际上蕴含着一个比较重要的信息。


    为什么我们之前的方式都拿不到用户传入进来的类型信息呢?是有原因的,当我们设计的 API 的时候,前面的角度是从,如何校验类型方向进行的思考。


    而这里是尝试去通过约定好的一种规则,通过 TypeScript 的隐式类型推断获得到传入的类型,再通过约定的规则转化出一种新的类型约束来对用户的输入进行限制。




算算 TypeScript


一个人说:好办,算出来一个新的类型就好了。


declare function pick<
T extends Record<string, unknown>,
Keys extends keyof T
>(target: T, ...keys: Keys[]): {
[K in Keys]: T[K]
}

到这里已经是对类型的作用有了基础的了解了,能写出来符合开发者所能接受的类型相对友好的代码了。我们可以再来思考一些更特殊的情况:


// 输入了重复的 key
pick({ a: '' }, 'a', 'a')

完美 TypeScript


到这里,我们便是初步开始了类型“体操”。但是在本篇里,我们不去分析它。


export type L2T<L, LAlias = L, LAlias2 = L> = [L] extends [never]
? []
: L extends infer LItem
? [LItem?, ...L2T<Exclude<LAlias2, LItem>, LAlias>]
: never

declare function pick<
T extends Record<string, unknown>,
Keys extends L2T<keyof T>
>(target: T, ...keys: Keys): Pick<T, Keys[number] & keyof T>

const x0 = pick({ a: '1', b: '2' }, 'a')
console.log(x0.a)
// @ts-expect-error
console.log(x0.b)

const x1 = pick({ a: '1', b: '2' }, 'a', 'a')
// ^^^^^^^^
// TS2345: Argument of type '["a", "a"]' is not assignable to parameter of type '["a"?, "b"?] | ["b"?, "a"?]'.
//   Type '["a", "a"]' is not assignable to type '["a"?, "b"?]'.
//     Type at position 1 in source is not compatible with type at position 1 in target.
//       Type '"a"' is not assignable to type '"b"'.

一个相对来说比较完美的 pick 函数便完成了。


总结


我们再来回到我们的标题吧,从我对大多数人的观察来说,很多的人开始来使用 TypeScript 有几种原因:



  • 看到大佬们都在玩,所以自己也想来“玩”,然后为了过类型校验而去写

  • 看到一些成熟的项目在使用 TypeScript ,想参与贡献,参与过程中为了让类型通过而想办法去解决类型报错

  • 公司整体技术栈采用的是 TypeScript ,要用 TypeScript 进行业务编写,从而为了过类型检查和 review 而去解决类型问题


诸如此类的问题还有很多,我将这种都划分为「为了解决类型检查的问题」而进行的类型编程,这也是大多数人为什么非常不适应 TypeScript,甚至不喜欢他的一个原因。这其实对学习 TypeScript 并不是一个很好的思路,在这里我觉得我们需要站在设计者的角度去对类型系统进行思考。我觉得有以下几个角度:



  • 类型检查到位

  • 类型提示友好

  • 类型检查严格

  • 扩展性十足


我们如果站在这几个角度对我们的 API 进行设计,我们可以发现,开发者能够很轻松的将他们需要的代码编写出来,而尽量不用去翻阅文档,查找 example。


希望通过我的这篇分享,大家能对 TypeScript 多一些理解,并参与到生态中来,守护我们的 JavaScript。




2023/06/27 更新



理性探讨,在评论区说什么屎不是屎的,嘴巴臭可以不说话的。


没谁逼着你一定要写最后一种层次的代码,能力不足可以学啊,不喜欢可以不学啊,能达到倒数第二个就已经很棒啊。


最后一种只是给大家看看 TypeScript 的一种可能,而不是说你应该这么做的。


作者:一介4188
来源:juejin.cn/post/7248599585751515173

收起阅读 »

次世代前端视图框架都在卷啥?

web
上图是 State of JavaScript 2022 前端框架满意度排名。前三名分别是 Solid、Svelte、Qwik。我们可以称他们为次世代前端框架的三大代表,前辈是 React/Angular/Vue。 目前 React/Augular/Vue 还...
继续阅读 »

state of JavaScript 2022 满意度排名


上图是 State of JavaScript 2022 前端框架满意度排名。前三名分别是 SolidSvelteQwik。我们可以称他们为次世代前端框架的三大代表,前辈是 React/Angular/Vue
目前 React/Augular/Vue 还占据的主流的市场地位, 现在我们还不知道下一个五年、十年谁会成为主流,有可能前辈会被后浪拍死在沙滩上, 也有可能你大爷还是你大爷。


就像编程语言一样,尽管每年都有新的语言诞生,但是撼动主流编程语言的地位谈何容易。在企业级项目中,我们的态度会趋于保守,选型会偏向稳定、可靠、生态完善的技术,因此留给新技术的生存空间并不多。除非是革命性的技术,或者有大厂支撑,否则这些技术或框架只会停留小众圈子内。



比如有一点革命性、又有大厂支撑的 Flutter。





那么从更高的角度看,这些次时代的前端视图框架在卷哪些方向呢?有哪些是革命性的呢?


先说一下本文的结论:



  • 整体上视图编程范式已经固化

  • 局部上体验上内卷






视图编程范式固化


从 JQuery 退出历史舞台,再到 React 等占据主流市场。视图的编程范式基本已经稳定下来,不管你在学习什么视图框架,我们接触的概念模型是趋同的,无非是实现的手段、开发体验上各有特色:



  • 数据驱动视图。数据是现代前端框架的核心,视图是数据的映射, View=f(State) 这个公式基本成立。

  • 声明式视图。相较于上一代的 jQuery,现代前端框架使用声明式描述视图的结构,即描述结果而不是描述过程。

  • 组件化视图。组件是现代前端框架的第一公民。组件涉及的概念无非是 props、slots、events、ref、Context…






局部体验内卷


回顾一下 4 年前写的 浅谈 React 性能优化的方向,现在看来依旧不过时,各大框架无非也是围绕着这些「方向」来改善。


当然,在「框架内卷」、「既要又要还要」时代,新的框架要脱颖而出并不容易,它既要服务好开发者(开发体验),又要服务好客户(用户体验) , 性能不再是我们选择框架的首要因素。




以下是笔者总结的,次世代视图框架的内卷方向:



  • 用户体验

    • 性能优化

      • 精细化渲染:这是次世代框架内卷的主要战场,它们的首要目的基本是实现低成本的精细化渲染

        • 预编译方案:代表有 Svelte、Solid

        • 响应式数据:代表有 Svelte、Solid、Vue、Signal(不是框架)

        • 动静分离





    • 并发(Concurrent):React 在这个方向独枳一树。

    • 去 JavaScript:为了获得更好的首屏体验,各大框架开始「抛弃」JavaScript,都在比拼谁能更快到达用户的眼前,并且是完整可交互的形态。



  • 开发体验

    • Typescript 友好:不支持 Typescript 基本就是 ca

    • 开发工具链/构建体验: Vite、Turbopack… 开发的工具链直接决定了开发体验

    • 开发者工具:框架少不了开发者工具,从 Vue Devtools 再到 Nuxt Devtools,酷炫的开发者工具未来可能都是标配

    • 元框架: 毛坯房不再流行,从前到后、大而全的元框架称为新欢,内卷时代我们只应该关注业务本身。代表有 Nextjs、Nuxtjs










精细化渲染






预编译方案


React、Vue 这些以 Virtual DOM 为主的渲染方式,通常只能做到组件级别的精细化渲染。而次世代的 Svelte、Solidjs 不约而同地抛弃了 Virtual DOM,采用静态编译的手段,将「声明式」的视图定义,转译为「命令式」的 DOM 操作


Svelte


<script>
let count = 0

function handleClick() {
count += 1
}
</script>

<button on:click="{handleClick}">Clicked {count} {count === 1 ? 'time' : 'times'}</button>

编译结果:


// ....
function create_fragment(ctx) {
let button
let t0
let t1
let t2
let t3_value = /*count*/ (ctx[0] === 1 ? 'time' : 'times') + ''
let t3
let mounted
let dispose

return {
c() {
button = element('button')
t0 = text('Clicked ')
t1 = text(/*count*/ ctx[0])
t2 = space()
t3 = text(t3_value)
},
m(target, anchor) {
insert(target, button, anchor)
append(button, t0)
append(button, t1)
append(button, t2)
append(button, t3)

if (!mounted) {
dispose = listen(button, 'click', /*handleClick*/ ctx[1])
mounted = true
}
},
p(ctx, [dirty]) {
if (dirty & /*count*/ 1) set_data(t1, /*count*/ ctx[0])
if (
dirty & /*count*/ 1 &&
t3_value !== (t3_value = /*count*/ (ctx[0] === 1 ? 'time' : 'times') + '')
)
set_data(t3, t3_value)
},
i: noop,
o: noop,
d(detaching) {
if (detaching) {
detach(button)
}

mounted = false
dispose()
},
}
}

function instance($$self, $$props, $$invalidate) {
let count = 0

function handleClick() {
$$invalidate(0, (count += 1))
}

return [count, handleClick]
}

class App extends SvelteComponent {
constructor(options) {
super()
init(this, options, instance, create_fragment, safe_not_equal, {})
}
}

export default App

我们看到,简洁的模板最终被转移成了底层 DOM 操作的命令序列。


我写文章比较喜欢比喻,这种场景让我想到,编程语言对内存的操作,DOM 就是浏览器里面的「内存」:



  • Virtual DOM 就是那些那些带 GC 的语言,使用运行时的方案来屏蔽 DOM 的操作细节,这个抽象是有代价的

  • 预编译方案则更像 Rust,没有引入运行时 GC, 使用了一套严格的所有权和对象生命周期管理机制,让编译器帮你转换出安全的内存操作代码。

  • 手动操作 DOM, 就像 C、C++ 这类底层语言,需要开发者手动管理内存


使用 Svelte/SolidJS 这些方案,可以做到修改某个数据,精细定位并修改 DOM 节点,犹如我们当年手动操作 DOM 这么精细。而 Virtual DOM 方案,只能到组件这一层级,除非你的组件粒度非常细。








响应式数据


和精细化渲染脱不开身的还有响应式数据


React 一直被诟病的一点是当某个组件的状态发生变化时,它会以该组件为根,重新渲染整个组件子树,如果要避免不必要的子组件的重渲染,需要开发者手动进行优化(比如 shouldComponentUpdatePureComponentmemouseMemo/useCallback)  。同时你可能会需要使用不可变的数据结构来使得你的组件更容易被优化。


在 Vue 应用中,组件的依赖是在渲染过程中自动追踪的,所以系统能精确知晓哪个组件确实需要被重渲染。


近期比较火热的 signal (信号,Angular、Preact、Qwik、Solid 等框架都引入了该概念),如果读者是 Vue 或者 MobX 之类的用户, Signal 并不是新的概念。


按 Vue 官方文档的话说:从根本上说,信号是与 Vue 中的 ref 相同的响应性基础类型。它是一个在访问时跟踪依赖、在变更时触发副作用的值容器。


不管怎样,响应式数据不过是观察者模式的一种实现。相比 React 主导的通过不可变数据的比对来标记重新渲染的范围,响应式数据可以实现更细粒度的绑定;而且响应式的另一项优势是它的可传递性(有些地方称为 Props 下钻(Props Drilling))。






动静分离


Vue 3 就是动静结合的典型代表。在我看来 Vue 深谙中庸之道,在它身上我们很难找出短板。


Vue 的模板是需要静态编译的,这使得它可以像 Svelte 等框架一样,有较大的优化空间;同时保留了 Virtual DOM 和运行时 Reactivity,让它兼顾了灵活和普适性。


基于静态的模板,Vue 3 做了很多优化,笔者将它总结为动静分离吧。比如静态提升、更新类型标记、树结构打平,无非都是将模板中的静态部分和动态部分作一些分离,避免一些无意义的更新操作。


更长远的看,受 SolidJS 的启发, Vue 未来可能也会退出 Vapor 模式,不依赖 Virtual DOM 来实现更加精细的渲染。








再谈编译时和运行时


编译时和运行时没有优劣之分, 也不能说纯编译的方案就必定是未来的趋势。


这几年除了新的编译时的方案冒出来,宣传自己是未来;也有从编译时的焦油坑里爬出来, 转到运行时方案的,这里面的典型代表就是 Taro。


Taro 2.0 之前采用的是静态编译的方案,即将 ’React‘ 组件转译为小程序原生的代码:


Untitled


但是这个转译工作量非常庞大,JSX 的写法千变万化,非常灵活。Taro 只能采用 穷举 的方式对 JSX 可能的写法进行了一 一适配,这一部分工作量很大,实际上 Taro 有大量的 Commit 都是为了更完善的支持 JSX 的各种写法。这也是 Taro 官方放弃这种架构的原因。


也就是说 Taro 也只能覆盖我们常见的 JSX 用法,而且我们必须严格遵循 Taro 规范才能正常通过。


有非常多的局限:



  • 静态的 JSX

  • 不支持高阶组件

  • 不支持动态组件

  • 不支持操作 JSX 的结果

  • 不支持 render function

  • 不能重新导出组件

  • 需要遵循 on*、render* 约束

  • 不支持 Context、Fragment、props 展开、forwardRef

  • ….


有太多太多的约束,这已经不是带着镣铐跳舞了,是被五花大绑了。




使用编译的方案不可避免的和实际运行的代码有较大的 Gap,源码和实际运行的代码存在较大的差别会导致什么?



  • 比较差的 Debug 体验。

  • 比较黑盒。


我们在歌颂编译式的方案,能给我们带来多大的性能提升、带来多么简洁的语法的同时。另一方面,一旦我们进行调试/优化,我们不得不跨越这层 Gap,去了解它转换的逻辑和底层实现。


这是一件挺矛盾的事情,当我们「精通」这些框架的时候,估计我们已经是一个人肉编译器了。


Taro 2.x 配合小程序, 这对卧龙凤雏, 可以将整个开发体验拉到地平线以下。




回到这些『次世代』框架。React/Vue/Angular 这些框架先入为主, 在它们的教育下,我们对前端视图开发的概念和编程范式的认知已经固化。


Untitled


比如在笔者看来 Svelte 是违法直觉的。因为 JavaScript 本身并不支持这种语义。Svelte 要支持这种语义需要一个编译器,而作为一个 JavaScript 开发者,我也需要进行心智上的转换。


而 SolidJS 则好很多,目之所及都是我们熟知的东西。尽管编译后可能是一个完全不一样的东西。



💡 Vue 曾经也过一个名为**响应性语法糖的实验性功能来探索这个方向,但最后由于这个原因**,废弃了。这是一次明智的决定



当然,年轻的次世代的前端开发者可能不这么认为,他们毕竟没有经过旧世代框架的先入为主和洗礼,他们更能接受新的开发范式,然后扛起这些旗帜,让它们成为未来主流。


总结。纯编译的方能可以带来更简洁的语法、更多性能优化的空间,甚至也可以隐藏一些跨平台/兼容性的细节。另一方面,源码和实际编译结果之间的 Gap,可能会逼迫开发者成为人肉编译器,尤其在复杂的场景,对开发者的心智负担可能是翻倍的。


对于框架开发者来说,纯编译的方案实现复杂度会更高,这也意味着,会有较高贡献门槛,间接也会影响生态。








去 JavaScript


除了精细化渲染,Web 应用的首屏体验也是框架内卷的重要方向,这个主要的发展脉络,笔者在 现代前端框架的渲染模式 一文已经详细介绍,推荐大家读一下:


Untitled


这个方向的强有力的代表主要有 Astro(Island Architecture 岛屿架构)、Next.js(React Server Component)、Qwik(Resumable 去 Hydration)。


这些框架基本都是秉承 SSR 优先,在首屏的场景,JavaScript 是「有害」的,为了尽量更少地向浏览器传递 JavaScript,他们绞尽脑汁 :



  • Astro:’静态 HTML‘优先,如果想要 SPA 一样实现复杂的交互,可以申请开启一个岛屿,这个岛屿支持在客户端进行水合和渲染。你可以把岛屿想象成一个 iframe 一样的玩意。

  • React Server Component: 划分服务端组件和客户端组件,服务端组件仅在服务端运行,客户端只会看到它的渲染结果,JavaScript 执行代码自然也仅存于服务端。

  • Qwik:我要直接革了水合(Hydration)的命,我不需要水合,需要交互的时候,我惰性从服务端拉取事件处理器不就可以了…


不得不说,「去 JavaScript」的各种脑洞要有意思多了。






总结


本文主要讲了次世代前端框架的内卷方向,目前来看还处于量变的阶段,并没有脱离现在主流框架的心智模型,因此我们上手起来基本不会有障碍。


作为普通开发者,我们可以站在更高的角度去审视这些框架的发展,避免随波逐流和无意义的内卷。






扩展阅读



作者:荒山
来源:juejin.cn/post/7251763342954512440
收起阅读 »

为了娃的暑期课,老父亲竟然用上了阿里云高大上的 Serverless FaaS!!!

web
起因 事件的起因是,最近家里的俩娃马上要放暑假了,家里的老母亲早早的就规划好了姐姐弟弟的暑期少年宫课程,奈何有些想上个课程一直没有”抢“到课程。平时带娃在少年宫上课的父母可能懂的,一般少年宫的课程都是提前预报名,然后会为了公平起见进行摇号,中者缴费。本来是一件...
继续阅读 »

起因


事件的起因是,最近家里的俩娃马上要放暑假了,家里的老母亲早早的就规划好了姐姐弟弟的暑期少年宫课程,奈何有些想上个课程一直没有”抢“到课程。平时带娃在少年宫上课的父母可能懂的,一般少年宫的课程都是提前预报名,然后会为了公平起见进行摇号,中者缴费。本来是一件比较合理的处理方式,但奈何不住各位鸡娃的父母们的上有政策下有对策的路子。



第一阶段:靠数量提高命中率 ,大家都各自报了很多不同课程,防止因为摇号没摇上,导致落空。我们家也是一样操作~~~。但是这里也会出现另一种状况,当摇号结束,大家缴费期间,有的摇中家长,发现课程多了或者有些课程和课外兴趣班冲突,或者种种其他原因,不想再上暑期课程了,就会取消这门课程。 即时你缴费了,后面也是可以取消的,只是会扣除一些费用。
第二阶段:捡漏,有报多的家长,就有没有抢到合适课程的家长。没错,说的正是我们家 哈哈。在我老婆规划中,我们还有几门课程没有摇中,那这个时候怎么办呢?只能蹲守,人工不定时的登录查课,寄期望于有些家长退课了,我们好第一时间补上去。


当当当,作为一个程序员老父亲,这个时候终于排上用场了~~~,花了一个晚上,写了个定时查询脚本+通知,当有课放出,咱们就通知一下领导(老婆大人)定夺,话说这个小查课定时任务深受领导的高度表扬。
好了起因就是这样,下面我们回到正题,给大家实操下如何使用阿里云的Serverless 函数,来构建这个小定时脚本。


架构


很简单的架构图,只用到了这么几个组件,



  • Serverless FC 无服务器函数,承载逻辑主体

  • OSS 存储中间结果数据

  • RAM是对计算函数赋予角色使其有对应权限。

  • 企业微信机器人,企业微信本身可以随便注册,拉个企业微信群,加入一个群机器人,就可以作为消息触达端。



实践


函数计算FC



本次实操中,我们需要先了解阿里云的函数计算FC几个概念,方便我们后面操作理解:



相关官方资料:基本概念

下面我只列了本次操作涉及到的概念,更详细资料,建议参考官方文档。




  • 服务:服务是函数计算资源管理的单位,是符合微服务理念的概念。从业务场景出发,一个应用可以拆分为多个服务。从资源使用维度出发,一个服务可以由多个函数组成。

  • FC函数:函数计算的资源调度与运行是以函数为单位。FC函数由函数代码和函数配置构成。FC函数 必须从属于服务,同一个服务下的所有函数共享一些相同的设置,例如服务授权、日志配置。函数的相关操作

  • 层:层可以为您提供自定义的公共依赖库、运行时环境及函数扩展等发布与部署能力。您可以将函数依赖的公共库提炼到层,以减少部署、更新时的代码包体积,也可以将自定义的运行时,以层部署在多个函数间共享。

  • 触发器:触发器是触发函数执行的方式。在事件驱动的计算模型中,事件源是事件的生产者,函数是事件的处理者,而触发器提供了一种集中、统一的方式来管理不同的事件源


创建函数



  1. 函数计算FC--> 任务--> 选择创建函数




  1. 配置函数


这里我截了个长屏,来给大家逐个解释



tips: 如果大家也有截长屏需求,推荐chrome 中的插件:Take Webpage Screenshots Entirely - FireShot




  • 函数方式:我的小脚本是python 代码,我直接使用自定义运行环境,如果你想了解这三种方式区别,建议详细阅读这篇文章:函数计算支持的多语言运行时信息

  • 服务名称:我们如果初次创建,选择创建一个服务,然后填入自己设定的服务名字即可

  • 函数代码:这里我选择运行时python 3.9 , 示例代码(代码等我们创建完成之后,再填充自己的代码逻辑)

  • 高级配置: 这里如果是初学者,个人建议尽量选最小配置,因为函数计算是按你使用的资源*次数 收费的, 这里我改成了资源粒度,0.05vCpu 128MB,并发度 1

  • 函数变量:我暂时不需要,就没有设置,如果你需要外部配置一些账号密码,可以使用这种方式来配置

  • 触发器:这里展示出了函数计算的协同作用,可以通过多种云服务产品来进行事件通知触发,我们这里的样例只需要一个定时轮询调度,所以这里我使用了定时触发器,5分钟调用一次。



配置依赖



函数整体创建成功之后,点击函数名称,进入函数详情页



函数代码模块填充本地已经调试好的代码, 测试函数,发现相关依赖并没有,这里我们需要编辑层,来将python相关依赖文件引入, 点击上图中编辑层



我选择的是在线构建依赖层,按照requirements.txt的格式书写,然后就可以在线安装了,很方便。创建成功之后,回到编辑层位置,选择刚开始创建的层,点击确定,既可,这样就不会再报相关依赖缺失了。


配置OSS映射


我的小脚本里,需要存储中间数据,因为函数计算FC本身是无状态的,所以需要借助外部存储,很自然的就会想到用OSS来存储。但是如何正确的使用OSS桶来存储中间数据呢?
官方关于python操作OSS的教程:python 操作 OSS



# -*- coding: utf-8 -*-
import oss2
# 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
auth = oss2.Auth('<yourAccessKeyId>', '<yourAccessKeySecret>')
# Endpoint以杭州为例,其它Region请按实际情况填写。
bucket = oss2.Bucket(auth, 'http://oss-cn-hangzhou.aliyuncs.com', '<yourBucketName>')

这里操作基本都会使用到 AK,SK。 但是基于云上的安全的实践操作以及要定期更换ak,sk来保证安全,尽量不要直接在代码中使用ak, sk来调用api 。那是否有其他更合理的操作方式?
我找到了这篇文章 配置OSS文件系统



函数计算支持与OSS无缝集成。您可以为函数计算的服务配置OSS挂载,配置成功后,该服务下的函数可以像使用本地文件系统一样使用OSS存储服务。



个人推荐这种解决方案



  • 只需要配置对应函数所授权的角色策略中,加上对相应的挂载OSS桶的读写权限

  • 这个操作符合最小粒度的赋予权限,同时也减少代码开发量,python可以像操作本地磁盘一样,操作oss,简直不要太方便~~~

  • 同时也不需要担心所谓的ak sk泄漏风险以及需要定期更换密钥的麻烦,因为就不存在使用ak sk


我最后也是用这种方式,配置了oss文件系统映射到函数运行时的环境磁盘上。


企业微信机器人


企业微信可以直接注册,不需要任何费用,之后两个人拉一个群,添加一个群机器人即可。
可以参考官方文档:如何使用群机器人 来用python 发送群消息,很简单的一段代码既可完成发送消息通知。


wx_url = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxx-xxxxx-xxxxx"
def sendWechatBotMsg(msg,collagues):
"""艾特指定同事,并发送指定信息"""
data = json.dumps({"msgtype": "text", "text": {"content": msg, "mentioned_list":collagues}})
r = requests.post(wx_url, data, auth=('Content-Type', 'application/json'))

最后效果如图所示:



总结


通过日常生活中的一个小场景,实践了下阿里云的高大上的Serverless FC 服务。个人选择这种无服务器函数计算,也是结合了成本的因素。
给大家对比下两种方案价格:



  • 传统云主机方式:


阿里云官方ECS主机的定价:实例价格信息
最便宜的一档: 1vCPU 1GB 实例, 每个月也要34.2 RMB 还没有包括挂载的磁盘价格 ,以及公网带宽费用




  • Serverless FC


而使用无服务器函数计算服务, 按使用时长和资源计费,像我这种最小资源粒度就可以满足同时调度次数是周期性的,大大消减了费用, 我跑了大概一周的时间 大概花费了 0.16 RMB,哈哈 简直是不能再便宜了。大家感兴趣的也可以动手实践下自己的需求场景。




云计算已经是当下技术人员的必学的一门课程,如果有时间也鼓励大家可以多了解学习,提升自己的专业能力。感兴趣的朋友,如果有任何问题,需要沟通交流也可以添加我的个人微信 coder_wukong,备注:云计算,或者关注我的公众号 WuKongCoder日常也会不定期写一些文章和思考。




如果觉得文章不错,欢迎大家点赞,留言,转发,收藏 谢谢大家,我们下篇文章再会~~~



参考资料


中国唯一入选 Forrester 领导者象限,阿里云 Serverless 产品能力全球第一

函数计算支持的多语言运行时信息

阿里云OSS文档:python 操作 OSS

阿里云函数计算文档:配置OSS文件系统

企业微信文档:如何使用群机器人

让 Serverless 更普惠

Serverless 在阿里云函数计算中的实践


作者:WuKongCoder
来源:juejin.cn/post/7251786717652107301
收起阅读 »

你还在用传统轮播组件吗?来看看遮罩轮播组件

web
背景 最近有一个页面改版的需求,在UI走查阶段,设计师说原来的轮播组件和新版页面UI整体风格不搭,所以要换掉。 这里就涉及到两种轮播组件,一种是传统的轮播组件,一种是设计师要的那种。 传统的轮播组件,大家都见过,原理也清楚,就是把要轮播的图片横向排成一个队列,...
继续阅读 »

背景


最近有一个页面改版的需求,在UI走查阶段,设计师说原来的轮播组件和新版页面UI整体风格不搭,所以要换掉。


这里就涉及到两种轮播组件,一种是传统的轮播组件,一种是设计师要的那种。


传统的轮播组件,大家都见过,原理也清楚,就是把要轮播的图片横向排成一个队列,把他们当成一个整体,每次轮换,其实是把这个队列整体往左平移X像素,这里的X通常就是一个图片的宽度。
这种效果可以参见vant组件库里的swipe组件


而我们设计师要的轮播效果是另外一种,因为我利用端午假期已经做好了一个雏形,所以大家可以直接看Demo


当然你也可以直接打开 腾讯视频APP 首页,顶部的轮播,就是我们设计师要的效果。


需求分析


新式轮播,涉及要两个知识点:



  • 图片层叠

  • 揭开效果


与传统轮播效果一个最明显的不同是,新的轮播效果需要把N张待轮播的图片在Z轴上重叠放置,每次揭开其中的一张,下一张是自然漏出来的。这里的实现方式也有多种,但最先想到的还是用zindex的方案。


第二个问题是如何实现揭开的效果。这里就要使用到css3的新属性mask。
mask是一系列css的简化属性。包括mask-image, mask-position等。
因为mask的系列属性还有一定的兼容性,所以一部分浏览器需要带上-webkit-前缀才能生效。


还有少数浏览器不支持mask属性,退化的情况是轮播必须有效,但是没有轮换的动效。


实现


有了以上的分析,就可以把效果做出来了。核心代码如下:


<script setup lang="ts">
import { ref, onMounted, watch } from "vue";
// 定义属性
const props = defineProps([
'imgList',
'duration',
'transitionDuration',
'maskPositionFrom',
'maskPositionTo',
'maskImageUrl'
]);
// 定义响应式变量
const currentIndex = ref(0);
const oldCurrentIndex = ref(0);
const imgList = ref([...props.imgList, props.imgList[0]]);
const getInitZindex = () => {
const arr = [1];
for (let i = imgList.value.length - 1; i >= 1; i--) {
arr.unshift(arr[0] + 1);
}
return arr;
}
const zIndexArr = ref([...getInitZindex()]);
const maskPosition = ref(props.maskPositionFrom || 'left');
const transition = ref(`all ${props.transitionDuration || 1}s`);
// 设置动画参数
const transitionDuration = props.transitionDuration || 1000;
const duration = props.duration || 3000;

// 监听currentIndex变化
watch(currentIndex, () => {
if (currentIndex.value === 0) {
zIndexArr.value = [...getInitZindex()];
}
maskPosition.value = props.maskPositionFrom || 'left';
transition.value = 'none';
})
// 执行动画
const execAnimation = () => {
transition.value = `all ${props.transitionDuration || 1}s`;
maskPosition.value = props.maskPositionFrom || 'left';
maskPosition.value = props.maskPositionTo || 'right';
oldCurrentIndex.value = (currentIndex.value + 1) % (imgList.value.length - 1);
setTimeout(() => {
zIndexArr.value[currentIndex.value] = 1;
currentIndex.value = (currentIndex.value + 1) % (imgList.value.length - 1);
}, 1000)
}
// 挂载时执行动画
onMounted(() => {
const firstDelay = duration - transitionDuration;
function animate() {
execAnimation();
setTimeout(animate, duration);
}
setTimeout(animate, firstDelay);
})
</script>
<template>
<div class="fly-swipe-container">
<div class="swipe-item"
:class="{'swipe-item-mask': index === currentIndex}"
v-for="(url, index) in imgList"
:key="index"
:style="{ zIndex: zIndexArr[index],
'transition': index === currentIndex ? transition : 'none',
'mask-image': index === currentIndex ? `url(${maskImageUrl})` : '',
'-webkit-mask-image': index === currentIndex ? `url(${maskImageUrl})`: '',
'mask-position': index === currentIndex ? maskPosition: '',
'-webkit-mask-position': index === currentIndex ? maskPosition: '' }"
>

<img :src="url" alt="">
</div>
<div class="fly-indicator">
<div class="fly-indicator-item"
:class="{'fly-indicator-item-active': index === oldCurrentIndex}"
v-for="(_, index) in imgList.slice(0, imgList.length - 1)"
:key="index">
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.fly-swipe-container {
position: relative;
overflow: hidden;
width: 100%;
height: inherit;
.swipe-item:first-child {
position: relative;
}
.swipe-item {
position: absolute;
width: 100%;
top: 0;
left: 0;
img {
display: block;
width: 100%;
object-fit: cover;
}
}
.swipe-item-mask {
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: cover;
-webkit-mask-size: cover;
}
.fly-indicator {
display: flex;
justify-content: center;
align-items: center;
z-index: 666;
position: relative;
top: -20px;
.fly-indicator-item {
margin: 0 5px;
width: 10px;
height: 10px;
border-radius: 50%;
background: gray;
}
.fly-indicator-item-active {
background: #fff;
}
}
}
</style>

这是一个使用 Vue 3 构建的图片轮播组件。在这个组件中,我们可以通过传入一组图片列表、切换动画的持续时间、过渡动画的持续时间、遮罩层的起始位置、遮罩层的结束位置以及遮罩层的图片 URL 来自定义轮播效果。


组件首先通过 defineProps 定义了一系列的属性,并使用 ref 创建了一些响应式变量,如 currentIndexoldCurrentIndeximgListzIndexArr 等。


onMounted 钩子函数中,我们设置了一个定时器,用于每隔一段时间执行一次轮播动画。
在模板部分,我们使用了一个 v-for 指令来遍历图片列表,并根据当前图片的索引值为每个图片元素设置相应的样式。同时,我们还为每个图片元素添加了遮罩层,以实现轮播动画的效果。


在样式部分,我们定义了一些基本的样式,如轮播容器的大小、图片元素的位置等。此外,我们还为遮罩层设置了一些样式,包括遮罩图片的 URL、遮罩层的位置等。


总之,这是一个功能丰富的图片轮播组件,可以根据传入的参数自定义轮播效果。


后续


因为mask可以做的效果还有很多,后续该组件可以封装更多轮播效果,比如从多个方向的揭开效果,各种渐变方式揭开效果。欢迎使用和提建议。


仓库地址:github.com/cunzai

zhuyi…

收起阅读 »

你们公司的官网被搜索引擎收录了吗?

web
前言 前段时间,我司的官网要改版。老板们手一挥,提出了以下几点需求 网站要大气,炫酷,有科技感 图片文字要高大上 注重SEA、SEO优化,用户查找关键字后,我们公司的网站排名要显示在前列 为此,我们还专门买了一个SEO优化的课程,大张旗鼓的学习了一通。至于...
继续阅读 »

1.jpg


前言


前段时间,我司的官网要改版。老板们手一挥,提出了以下几点需求



  • 网站要大气,炫酷,有科技感

  • 图片文字要高大上

  • 注重SEA、SEO优化,用户查找关键字后,我们公司的网站排名要显示在前列


为此,我们还专门买了一个SEO优化的课程,大张旗鼓的学习了一通。至于效果如何,1个月见分晓


那么如何编写 JavaScript 代码以有利于 SEO 和 SEA 呢?


下面仅展示被被谷歌搜索引擎收录的


SEA、SEO优化


保持好的网页结构



  1. 使用语义化的 HTML结构


HTML语义化是指使用恰当的HTML标签来描述网页内容的结构和含义,以提高网页的可读性、可访问性和搜索引擎优化。



  • header: 网站的页眉部分

  • nav: 定义网站的主要导航链接

  • main: 定义页面的主要内容区域,每个页面应该只有一个<main>标签

  • section: 定义页面中的独立区块, 例如文章、产品列表等

  • article: 定义独立的文章内容,通常包含标题、作者、发布日期等信息

  • aside: 定义页面的侧边栏或附属信息区域

  • footer: 网站的页脚部分


<header>
<h1>官网</h1>
<nav>
<ul>
<li><a href="#">首页</a></li>
<li><a href="#">关于我们</a></li>
<li><a href="#">联系我们</a></li>
</ul>
</nav>
</header>

<main>
<div>欢迎来到我们的网站</div>
<p>这里是网站的主要内容。</p>
</main>


<section>
<h2>最新文章</h2>
<article>
<h3>文章标题</h3>
<p>文章内容...</p>
</article>
<article>
...
</article>
</section>


<aside>
<h3>最新消息</h3>
<ul>
<li><a href="#">链接1</a></li>
...
</ul>
</aside>

<article>
<h2>消息1</h2>
<p>文章内容...</p>
</article>


<footer>
<p>版权所有 &copy; 2023</p>
<p>联系我们:info@example.com</p>
</footer>



  1. 提供准确且吸引人的页面标题和描述


准确且简洁的标题和描述,有利于吸引访问者和搜索引擎的注意



  • 页面标题: Title

  • 页面描述: Meta Description


<head>
<title>精美手工艺品——手工制作的独特艺术品</title>
<meta name="description" content="我们提供精美手工艺品的设计与制作,包括陶瓷、木雕、织物等。每件艺术品都是独一无二的,以精湛的工艺和创造力打动您的心灵。欢迎浏览我们的作品集。">
</head>


标题要小于50个字符,描述要小于150字符





  1. 在关键位置使用关键字: 包括标题、段落文本、链接文本和图片的 alt 属性。



    • 段落文本: 自然的使用关键字,有助于搜索引擎收录

    • 链接文本: 使用描述性的链接文本,并在其中包含关键字,这有助于搜索引擎理解链接指向的内容

    • 图片的 alt 属性: 对于每个图像,使用描述性的 alt 属性来说明图像内容,并在其中包含关键字。这不仅有助于视力障碍用户理解图像,还可以提供关键字相关的图像描述给搜索引擎。




<h1>欢迎来到精美手工艺品网店</h1>
<p>我们提供各种精美手工艺品,包括陶瓷、木雕、织物等。每个艺术品都是由我们经验丰富的工匠手工制作而成,展现了精湛的工艺和创造力。</p>
<p>浏览我们的<a href="/products" title="手工艺品产品列表">产品列表</a>,您将发现独特的艺术品,适合作为礼物或收藏。</p>
<img src="product.jpg" alt="陶瓷花瓶 - 手工制作的精美艺术品" />


一个页面要保证有且只有h1标签



使用友好的 URL 结构


使用友好的URL结构是一个重要的优化策略,它可以提升网站的可读性、可维护性和用户体验



  • 使用关键字: 在URL中使用关键字,以便用户和搜索引擎可以更好地理解页面的主题和内容, URL中多个关键词使用连字符字符 "-"进行分隔。

  • 结构层次化: 层次化的URL结构来反映内容的结构和关系

  • 避免使用参数: 尽量避免在URL中使用过多的参数,特别是使用随机字符串或数字作为参数

  • 尽量使用永久链接: 尽可能使用永久链接,避免频繁更改URL

  • 尽量保持URL简洁: 避免过长的URL。短连接更易于分享和记忆


<!-- 不友好的URL -->
https://example.com/index.html?category=7&product=12345
https://example.com/qinghua/porcelain

<!-- 友好的URL -->
https://example.com/porcelain/qinghua
https://example.com/blog/friendly-urls


  1. 重要链接不要用JS


搜索引擎爬虫通常不会执行 JavaScript,并且依赖 JavaScript 的链接可能无法被爬虫正确解析和索引



使用标准的 <a> 标签进行跳转,避免使用 JavaScript 跳转




  1. 使用W3C规范


使用W3C规范是确保你的网页符合Web标准并具有良好可访问性的重要方式


不符合W3C的规范:



  • 未闭合的标签

  • 未正确嵌套的元素

  • 行内元素包裹块状元素


<!-- 未闭合的标签 -->
<p>This is a paragraph with no closing tag.
<!-- 未正确嵌套的元素 -->
<div><p>This paragraph is inside a div but not closed properly.</div></p>
<!-- 行内元素包裹块状元素 -->
<span><p>This paragraph is inside a div but not closed properly.</p></span>

响应式设计和移动优化


Google 现在使用了移动优先索引, 搜索引擎更倾向于优先索引和显示移动友好的网页


使用响应式设计,使你的网页在各种设备上都能正确显示。



  1. 响应式设计:确保网页具有响应式设计,能够适应不同设备的屏幕尺寸

  2. 关注移动友好性:确保网页在移动设备上加载和显示良好


JavaScript使用和加载优化


搜索引擎爬虫通常不会执行 JavaScript,并且在抓取和索引页面时可能会忽略其中的动态内容




  1. 加载时间优化: 通过压缩和合并 JavaScript文件,减小文件大小,以及使用异步加载和延迟加载的方式,可以提高网页的加载速度




  2. 避免使用AJAX技术加载核心内容: 对于核心内容,避免使用 AJAX 或动态加载方式,而是在初始页面加载时就呈现。这样可以确保搜索引擎能够正确抓取和索引核心内容,提高网页的可见性和相关性。




  3. 减少懒加载、瀑布流、上拉刷新、下载加载、点击更多等互动加载: 这些常见的页面优化方式虽然有利于用户体验。但搜索引擎爬虫不会执行 JavaScript,并且在抓取和索引页面时可能会忽略其中的动态内容。




  4. js阻塞后保证页面正常运行: 确保网站在没有 JavaScript 的情况下仍然能够正常运行。这有助于搜索引擎爬虫能够正确索引你的网页内容。




性能和体验优化



  1. 提高网站加载速度: 搜索引擎和用户都更喜欢快速加载的网页,提高页面的转加载速度,会对搜索引擎排名产生积极影响。

  2. 优化移动体验: 在移动设备上,用户的粘性和耐心被放大,优化移动体验,减少用户的流失率,会对移动搜索排名产生积极影响。

  3. 无障碍: 在 Web 开发无障碍性意味着使尽可能多的人能够使用 Web 站点, 增加用户人群的受众,会提高搜索引擎排名


内容更新



  1. 内容持续更新: 搜索引擎比较喜欢新鲜的内容,如果网站内容长期不更新的话,搜索引擎就会厌烦我们的网站。反之,我们频繁的更新新闻、博客等内容,会大大的提高

  2. 网页数量尽可能的多: 尽可能的让网页超过15个,



频繁修改或调整网站结构的话就相当于修改了搜索引擎爬取网站的路径,导致网站即使更新再多的内容也难以得到收录



监测


索引


在浏览器中输入 site:你的地址(此方法仅适合谷歌,百度则直接搜索URL地址)


查看是否被索引



  1. 进入Google Search Console

  2. 进入URL检测工具。

  3. 将需要索引的URL粘贴到搜索框中。

  4. 等待谷歌检测URL。

  5. 点击“请求编入索引”按钮。


image.png


收录


点击网址检查: 如果页面被索引,那么会显示“URL is on Google(URL在谷歌中)”。


image.png


如何去收录


image.png


但是,请求编入收录索引不太可能解决旧页面的索引问题,并且这只是一个最原始的方式,提交链接不能确保你的URL一定被收录,尤其是百度。


参考11个让百度快速收录网站的奇思淫技


总结


持续的优化和监测是关键,以确保你的策略和实践符合不断变化的搜索引擎算法和用户需求。


期待一个月后见分晓啦!


参考文献



  1. 11个让百度快速收录网站的奇思淫技

  2. search

  3. JavaScript与SEO之间的藕断丝连关系<
    作者:高志小鹏鹏
    来源:juejin.cn/post/7251786985535275067
    /a>

收起阅读 »

实现联动滚动

序言 在垂直滑动的过程中可以横向滚动内容。 效果 代码 就一个工具类就行了。可以通过root view向上查找recycleView。自动添加滚动监听。在子view完全显示出来以后,才分发滚动事件。这样用户才能看清楚第一个。 需要一个id资源<?xml...
继续阅读 »

序言


在垂直滑动的过程中可以横向滚动内容。


效果


在这里插入图片描述


代码


就一个工具类就行了。可以通过root view向上查找recycleView。自动添加滚动监听。在子view完全显示出来以后,才分发滚动事件。这样用户才能看清楚第一个。


需要一个id资源

<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="key_rv_scroll_helper" type="id"/>
</resources>

RVScrollHelper

package com.trs.myrb.provider.scroll;

import android.view.View;
import android.view.ViewParent;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import com.trs.myrb.R;
import com.trs.myrb.douyin.action.TRSFunction;


/**
* <pre>
* Created by zhuguohui
* Date: 2023/5/12
* Time: 13:49
* Desc:用于监听recycleView的垂直滚动,然后将垂直滚动转发给其他类。
*
* //在provider中使用
* RVScrollHelper.create(binding.getRoot(), dy -> {
* binding.rvHotVideos.scrollBy(dy,0);
* return null;
* });
* </pre>
*/
public class RVScrollHelper implements View.OnAttachStateChangeListener {
TRSFunction<Integer, Void> scrollCallBack;
View child;
RecyclerView recyclerView;
private int recyclerViewHeight;

public static RVScrollHelper create(View child,TRSFunction<Integer, Void> scrollCallBack){
if(child==null){
return null;
}
Object tag = child.getTag(R.id.key_rv_scroll_helper);
if(!(tag instanceof RVScrollHelper)){
RVScrollHelper helper = new RVScrollHelper(child, scrollCallBack);
tag=helper;
child.setTag(R.id.key_rv_scroll_helper,helper);
}
return (RVScrollHelper) tag;
}

private RVScrollHelper(View child, TRSFunction<Integer, Void> scrollCallBack) {
this.scrollCallBack = scrollCallBack;
this.child = child;
this.child.addOnAttachStateChangeListener(this);

}

@Override
public void onViewAttachedToWindow(View v) {

if(child==null){
return;
}
if (recyclerView == null) {
recyclerView = getRecyclerView(v);
recyclerViewHeight = recyclerView.getHeight();
recyclerView.addOnScrollListener(onScrollListener);
}

}


@Override
public void onViewDetachedFromWindow(View v) {

if(recyclerView!=null) {
recyclerView.removeOnScrollListener(onScrollListener);
recyclerView=null;
}
}

private RecyclerView.OnScrollListener onScrollListener=new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {

}

@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
//只有在view全部显示出来以后,才发送滚动事件
boolean isAllShow=isAllShow();
if(isAllShow){
if(scrollCallBack!=null){
scrollCallBack.call(dy);

}
}
}
};

private boolean isAllShow() {
int top = child.getTop();
int bottom = child.getBottom();
return bottom>=0&&bottom<=recyclerViewHeight;

}

private RecyclerView getRecyclerView(View v) {
ViewParent parent = v.getParent();
while (!(parent instanceof RecyclerView) && parent != null) {
parent = parent.getParent();
}
if(parent!=null){
return (RecyclerView) parent;
}
return null;
}




}


使用


在provider中使用。

  RVScrollHelper.create(holder.itemView, dy -> {
recyclerView.scrollBy(dy, 0);
return null;
});

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

每日一题:Zygote 为什么不采用Binder机制进行IPC通信呢?

在android面试中,我们常会遇到Framework面试相关问题,而今天要分享的就是Zygote 为什么不采用Binder机制进行IPC通信呢? 其主要考察的是程序员对binder的理解和zygote fork的理解。 问题正解: 这个很重要的原因是如果zy...
继续阅读 »

在android面试中,我们常会遇到Framework面试相关问题,而今天要分享的就是Zygote 为什么不采用Binder机制进行IPC通信呢?


其主要考察的是程序员对binder的理解和zygote fork的理解。


问题正解:


这个很重要的原因是如果zygote采用binder 会导致 fork出来的进程产生死锁。


在UNIX上有一个 程序设计的准则:多线程程序里不准使用fork。


这个规则的原因是:如果程序支持多线程,在程序进行fork的时候,就可能引起各种问题,最典型的问题就是,fork出来的子进程会复制父进程的所有内容,包括父进程的所有线程状态。那么父进程中如果有子线程正在处于等锁的状态的话,那么这个状态也会被复制到子进程中。父进程的中线程锁会有对应的线程来进行释放锁和解锁,但是子进程中的锁就等不到对应的线程来解锁了,所以为了避免这种子进程出现等锁的可能的风险,UNIX就有了不建议在多线程程序中使用fork的规则。


在Android系统中,Binder是支持多线程的,Binder线程池有可以有多个线程运行,那么binder 中就自然会有出现子线程处于等锁的状态。那么如果Zygote是使用的binder进程 IPC机制,那么Zygote中将有可能出现等锁的状态,此时,一旦通过zygote的fork去创建子进程,那么子进程将继承Zygote的等锁状态。这就会出现子进程一创建,天生的就在等待线程锁,而这个锁缺没有地方去帮它释放,子进程一直处于等待锁的状态。


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

小白如何快速提升技术水平?我的建议是,多加班

经常在技术社区看到有小白提问如何提升技术水平,我也曾是小白,我也曾经问过这个问题,我也不是什么天赋异禀之人,和大部分人都一样,所以我能够理解小白在问这个问题时的感受,作为过来人,解决这个问题的路径其实是非常清晰的,网上这类的答案也很多,不过看过我文章的读者都应...
继续阅读 »

经常在技术社区看到有小白提问如何提升技术水平,我也曾是小白,我也曾经问过这个问题,我也不是什么天赋异禀之人,和大部分人都一样,所以我能够理解小白在问这个问题时的感受,作为过来人,解决这个问题的路径其实是非常清晰的,网上这类的答案也很多,不过看过我文章的读者都应该能够感觉到我是个比较务实的人,对于冠冕堂皇实际上屁用没有的东西不太感冒,所以我的这篇文章可能跟其他人的不要太一样,我只从实际出发,谈谈我的想法


以下内容对于大部分 0-3年经验的小白都适用,仅针对想靠这行混碗饭吃的人(这类人才是这个行业的主力军),天赋异禀的少数群体看个乐子就行


基础知识必须掌握


ReactVue 大行其道的当下,我见过很多人竟然连基本的 js api 都搞不清楚,方法不知道,参数不清楚,掌握的少的可怜的 js 知识,比如 requestAnimationFrameObject.defineProperty等还都纯粹是因为跟 vuereact扯上了关系面试要用所以才看了两眼,我无法理解如果连基础知识都掌握不了的话,又该如何去 深入 理解这些框架?


实际工作用的确实也是框架,很少有需要用原生 js 去实现什么复杂功能的地方,但这并不代表这不重要,恰恰相反,如果不是急着要找工作的话,比如在校大学生,我认为还是先把基础弄清楚,再顺带着学一学框架,无论是什么框架,都是对于底层技术的封装,reactvue等框架更新的确实很快,很多人都在说学不动了,实际上是你学错了,这些上层的东西根本不需要追着学,它们只是趁手的工具,工具是为你所服务的,但现在你却说工具让你追得很累,这本身就是有问题的


我上大学的时候,那个还是 angular 市场占有率最高,我那个时候也学了 angular,但是现在我再看 angular发现早就不是当初我学的那个东西了,假设我现在需要用到 angular,现在学和一路追着 angular那么多版本更新一个个学下来,有区别吗?没区别,都是工具,只要能实现功能就行了,我管你是 angular还是 react,管你是 vue2.x 还是 vue3.x,反正对着文档一把梭就行了,都是基于 jsdsl封装罢了,无非是玩得花不花而已


你可能会说,现在都在用框架,面试问的也是框架知识,基础知识知不知道有什么关系平时根本用不到啊,我并不认同,我就举个例子,经常看到有人说问



只学过vue,现在工作需要用react,怎么办,react好学吗?



我看了就很迷惑,你要是问我vuereact牛不牛逼,我肯定会伸出大拇指绝对牛逼,但你要问我难不难学,那我不能理解了,不就是一个框架吗有什么难不难好不好学的,我只能猜测这人可能一上来就是跟风学了vue,啥基础知识都不懂,要是老老实实先掌握了基础知识,再顺手学个框架,好好研究下框架与基础之间的通用关系,根本不会问这个问题


我不否认有些小众的东西确实难以理解,但像vuereact这种市场占有率很高的大众化框架,不可能存在什么技术门槛的,管你是React还是 Svelte,一天看文档,两天速通,三天上手写业务就是标准流程


当然了,想深入还是要花费些功夫的,但是恕我直言,以绝大多数人(包括我自己)写的业务屎山的逻辑难度而言,根本不需要什么深度技术,深入学习研究更多的是提升自己的认知水平


为了快速找工作,你可以在什么基础知识都不懂的情况下就跟风学框架,问题不大,毕竟吃饭要紧,但是如果你想把饭碗尽可能拿的更久一点或者更进一步,跳过的东西,你必须要再捡回来


另外,多提一嘴,前端三剑客包括 jscsshtml,虽然后两者的重要程度远不能跟 js 相提并论,但既然是基础知识,都还是需要掌握的,作为一名业务前端,总不能跟别人说你不太懂 css吧?


多看


如何掌握基础知识,首先你得知道有哪些基础知识,你作为一个啥也不懂一片空白的新人,了解这个领域最应该做的事情就是多看,看什么?看书、看视频


看书


关于前端书籍推荐的话题或者书单等,网上已经有很多了,我就不再赘述了,不过我发现有些书单也太特么长了,几十上百本书哪有那么多时间去看,好多我名字都没听过也往上凑,这可能会让很多人打退堂鼓,甚至直接摆烂,先进收藏夹再说,至于什么时候看就再说吧


如果只谈基础知识的,我认为只有两本书是必须要看的,《JavaScript高级程序设计》《JavaScript权威指南》,也就是常说的红宝书和犀牛书,这两本书比较厚,但相比于几十上百本书而言,已经很明确了很清晰了,只需要看这两本就行了,是必须要看的,由于这两本书都是出版了较长时间的书籍,一些更新的内容没有包括在内,那么这部分知识就需要自行去网上获取了,比如 ECMAScript 6 入门


至于其他的几十上百本,说实话,看哪本都行,不是说其他书不重要,哪本书都有其存在的意义,但只是相比于这两本没那么重要,多看肯定比不看强,这两本书是必看的综合基础,其他书是在你在掌握了基础之后,根据你的实际情况,提升在不同细分领域水平的助攻


技术社区上的文章也姑且算是书的一种形式吧,零散的文章用于开阔视野紧跟潮流风向,系列文章用于加深在细分领域的认知


看视频


我知道很多人看不起视频学习的,认为效率太低,除此之外还因为跟培训班扯上关系,也让很多自诩清高的人认为太low拉低自己的层次,所以对于看视频学习持否定态度


首先,我也认为看视频学习确实效率很低,但你得分清是对什么层次的人来说的


对于有了三年(只是大概估个数,少部分人可能只需要一年)以上工作经验的人来说,再看视频学习,那确实效率太低了,但是对于三年以下甚至是初识技术世界的小白来说,看视频的效率不是低,而是高,年限越低,看视频学习的效率越高,直到突破了三年这个节点后,看视频学习的效率才开始低于看书


我就是从小白过来的,我很清楚小白对于技术的认知是什么样的,那真是狗屁不通,让这样的人抱着厚厚的一本书在那看,每个字都认识,可一连起来就不知道为什么这样了,书籍内的世界虽然无比广阔,但无人指引,你跑了半天可能还在绕着圈,天地再大与你何干?


但视频就不同了,视频承载的信息量远低于书籍,但那却能给你更清晰的指引,一步步地告诉你该怎么走,万事开头难,入门的时候慢一点没关系,主要是要能入门,入对门,而不是像无头苍蝇一样乱撞


鄙人就是靠视频入门的,一开始大学里学的是 java,但后来对前端感兴趣,转投前端,只能自学,但又不知如何下手,明知道红宝书和犀牛书是很好的书籍,奈何看着看着就想睡觉,每个字都能看懂,但一合上书就忘了今天看了啥,机缘巧合之下在某专门卖视频课的网站买了视频开始学习,这才逐步进入正轨,一改之前苦大仇恨不得要领,每天跟着视频课都学得很开心,因为能学进脑子里了,每天都能感觉到自己又多进步了一点,如此良性循环才算是入了门,入了门之后,才终于具备了自学的能力,因为知道路在哪里了,知道该怎么走了


视频的信息量低吗?低,无论是以前还是现在我都这么认为


但视频课的效率一定低吗?不一定,对于已经入了门有了足够经验的人来说,效率是低的,因为确实信息密度低嘛,但对于小白来说,重要的是要先入门,哪种方式能先入门哪种方式才是效率高的



掘金小册我认为是介于书籍和视频之间的一种形式,兼顾了二者的部分优点



多写


看了不一定就是会了,必须要亲自上手写才能考验出是否是真的懂了,看视频或者看书,每一页都能看懂,但是真正让你写了却不知道如何下手,那不叫懂,上学时对着答案做题就没有不会的,但没了答案或者换了个形式你就不会了,那不叫会



写什么?写所有你能写的代码,无论是什么代码


写多少?能写多少是多少,写得越多越好



有小白曾问过我,说他看别人写的代码都能看懂也能理解,但是让自己独立写的话,他就不知道该怎么写了,为什么会出现这种情况?我想都不想就知道这人代码写得太少了


这一行绝大部分人干的活,包括一些所谓的大厂员工(比如我),实际上真的都只是搬砖活而已,根本不需要太高的技术含量,也就是远不到比拼智商的时候,只要你智商和别人差得不太多,那么别人能够做到的事情,你肯定也能做到,如果你做不到,只是因为你懒罢了


这么说吧,我认为对于大多数人来说,如果他们有机会从一开始就进入并夕夕过上 997 的工作模式,只要不是真的智商有问题,那么经过了两年的高强度输出之后,技术能力怎么也能达到这个行业的中上水准,这就是多写带来的收益


你一年写得代码比其他人三年写得还多,一些代码编写规范或者设计模式你背都背下来了,水平还能怎么低?


没错,虽然zz不正确,但我依然鼓励加班,但,需要注意前提


前提是你是个初入技术大门的小白(老油条就没必要掺和了,瞎内卷),想快速提升技术水平不怕吃苦,只是想靠这行混碗饭并不天赋异禀,年轻力壮有冲劲,且你在的公司确实能让你学到一些东西。


有些人可能会说,想多写代码不一定非要加班啊,我完全可以自己整个项目自己写,我的看法是,可以当然可以,但恕我直言,你自己捣鼓的那个东西也就是个 Demo,你不面对复杂多变的需求,没有庞大的用户体量,没有苛刻的产品、测试,你是很难碰到有意义的问题的,毕竟自己写的东西自己怎么看都是对的


也有人会说,公司确实清闲,没啥需要加班的怎么办?好办啊,我从业那么多年,我还就没见过完美的项目,绝大多数情况下都是问题一个接一个的屎山,屎山虽然满是毒,但如果你能摸索着尝试改善屎山,在实践中去思考去总结去行动,对你的提升速度可比光看书强得多得多了。但是,必须要注意,自己折腾可以,千万要注意安全,别把跑得好好的屎山给整塌方了


加班是给自己加的,如果你在加班之后,完全感觉不到自己有什么收获只是想骂公司傻x,那么就没必要加这个班了


多思考


如果你能做到前一条的话,再做这一条就是顺理成章的事情了(除非你懒)


我刚入门编程的时候,完全不理解函数是什么,为什么要抽离函数,我只知道人家都说抽离函数好,所以我就抽离,至于为什么抽离、什么时候抽离、怎么抽离,我一概不知,只是照葫芦画瓢,我没有专门去理解这件事,因为我尝试过但是理解不了(就是笨吧),于是就不管了,但忽然有一天我发现我不知道什么时候已经理解这回事了,想了半天,我只能归结于我写代码写多了,自动理解了


但这不是一蹴而就的,这是一个逐渐积累的过程,可能我在每次封装函数的时候都会思考一点,我为什么封装了这个函数,给我带来了哪些好处,那么当我写得代码足够多,封装的函数足够多之后,我就完全理解了这回事


写代码的时候要思考,为什么我要这么写,还有没有更好的写法?相同的逻辑,别人是那样写的,我是这样写的,差别是什么,各自有什么优缺点?这个代码把我坑惨了,要是下次换我来写,我会怎么设计?


不必专门腾出时间来冥想,这些思考完全可以是零碎的、一闪而过的念头,但必须要有这种习惯,当你写得足够多的时候,这些零散的思考汇聚起来终有一天能给你带来别样的启发


小结


绝大部分人(包括我)所能用上的技术,都谈不上高深,并不需要什么天赋,确实就只是熟能生巧的事情,辛苦一两年,把这些熟练掌握,然后后面才有余力去做技术之外的事情,当然,你也会发现,相比于这些事情,

作者:清夜
来源:juejin.cn/post/7251792725069512763
技术是真的单纯和简单

收起阅读 »

开发这么久,gradle 和 gradlew 啥区别、怎么选?

使用 Gradle 的开发者最常问的问题之一便是: gradle 和 gradlew 的区别? 。 这两个都是应用在特定场景的 Gradle 命令。通过本篇文章你将了解到每个命令干了什么,以及如何在两个命令中做选择。 快速摘要 如果你正在开发的项目当中已经包...
继续阅读 »

使用 Gradle 的开发者最常问的问题之一便是: gradlegradlew 的区别?


这两个都是应用在特定场景的 Gradle 命令。通过本篇文章你将了解到每个命令干了什么,以及如何在两个命令中做选择。



快速摘要


如果你正在开发的项目当中已经包含 gradlew 脚本,安啦,可以一直使用它。没有包含的话,请使用 gradle 命令生成这个脚本。


想知道为什么吗,请继续阅读。



gradle 命令


如果你从 Gradle 官网(gradle.org/releases)下载和安装了 Gradle 的话,你便可以使用安装在 bin 路径下的 gradle 命令了。当然你记得将该 bin 路径添加到设备的 PATH 环境变量中。


此后,在终端上运行 gradle 的话,你会看到如下输出:



你会注意到输出里打印了 Gradle 的版本,它对应着你运行的 gradle 命令在设备中的 Gradle 安装包版本。这听起来有点废话,但在谈论 gradlew 的时候需要明确这点,这很重要。


通过这个本地安装的 Gradle,你可以使用 gradle 命令做很多事情,包括:



  • 使用 gradle init 命令创建一个新的 Gradle 项目或者使用 gradle wrapper 命令创建 gradle wrapper 目录及文件

  • 在一个 Gradle 项目内使用 gradle build 命令进行 Gradle 编译

  • 通过 gradle tasks 命令查看当前的 Gradle 项目中支持哪些 task


上述的命令均使用你本地安装的 Gradle 程序,无论你安装的是什么版本。


如果你使用的是 Windows 设备,那么 gradle 命令等同于 gradle.bat,gradlew 命令等同于 gradlew.bat,非常简单。


gradlew 命令


gradlew 命令,也被了解为 Gradle wrapper,与 gradle 命令相比它是略有不同的。它是一个打包在项目内的脚本,并且它参与版本控制,所以当年复制了某项目将自动获得这个 gradlew 脚本。


“可那又如何?”


好吧,如果你这么想。让我告诉你,它有很多重要的优势。


1. 无需本地安装 gradle


gradlew 脚本不依赖本地的 Gradle 安装。在设备上第一次运行的时候会从网络获取 Gradle 的安装包并缓存下来。这使得任何人、在任何设备上,只要拷贝了这个项目就可以非常简单地开始编译。


2. 配置固定的 gradle 版本


这个 gradlew 脚本和指定的 Gradle 版本进行绑定。这非常有用,因为这意味着项目的管理者可以强制要求该项目编译时应当使用的 Gradle 版本。


Gradle 特性并不总是互相兼容各版本的,所以使用 Gradle wrapper 可以确保项目每次编译都能获得一致性的结果。


当然这需要编译项目的人使用 gradlew 命令,如下是在项目内运行 ./gradlew 的示例:



输出和运行 gradle 命令的结果比较相似。但仔细查看你会发现版本不一样,不是上面的 6.8.2 而是 6.6.1


这个差异说重要也重要,说不重要也不重要。


但当使用 gradlew 的话可以免于担心由于 Gradle 版本导致的不一致性,缘自它可以保证所有的团队成员以及 CI 服务端都会使用相同的 Gradle 版本来构建这个项目。


另外,几乎所有使用 gradle 命令可以做的事情,你也可以使用 gradlew来完成。比如编译一个项目就是 ./gradlew build


如果你愿意的话,可以拷贝 示例项目 并来试一下gradlew


gradle 和 gradlew 对比


至此你应该能看到在项目内使用 gradlew 通常是最佳选择。确保 gradlew 脚本受到版本控制,这样的话你以及其他开发者都可以收获如上章节提到的好处。


但是,难道没有任何情况需要使用 gradle 命令了吗?当然有。如果你期望在一个空目录下搭建一个新的 Gradle 项目,你可以使用 gradle init 来完成。这个命令同样会生成 gradlew 脚本。


(如下的表格简单列出两者如何选)可以说,使用 gradlew 确实是 Gradle 项目的最佳实践。

你想做什么?gradle 还是 gradlew?
编译项目gradlew
测试项目gradlew
项目内执行其他 Gradle taskgradlew
初始化一个 Gradle 项目或者生成 Gradle wrappergradle
作者:TechMerger
链接:https://juejin.cn/post/7144558236643885092
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

放弃熬夜,做清晨的霸主

前言 不知道最近大家有没有在 b 站刷到硬核的HeyMatt老师一个视频,标题叫做放弃熬夜,做清晨的霸主(人生效率的巨变)。 抱着随便看看的心情点了进去后,我沉默了并思考了片刻,我决定按照他视频里的建议和方法尝试一下。 在尝试早起将近一个月的时间后,我发现,...
继续阅读 »

前言



  • 不知道最近大家有没有在 b 站刷到硬核的HeyMatt老师一个视频,标题叫做放弃熬夜,做清晨的霸主(人生效率的巨变)

  • 抱着随便看看的心情点了进去后,我沉默了并思考了片刻,我决定按照他视频里的建议和方法尝试一下。

  • 在尝试早起将近一个月的时间后,我发现,我的效率确实是有了质的提升,接下来我会根据HeyMatt老师提到的方法和我一些实践来进行说明,感兴趣的小伙伴欢迎收藏慢慢看。


🕐 极致利用晚上时间的错觉



  • 会不会有很多小伙伴会有这种情况,每天辛勤劳作后,到了11点半大脑就会提示你:累了一天了,要不要放松一下呢?视频里说到,这种大脑暗示的放松大体分为三种:

    • 开始刷视频,打个游戏,借助浅层的刺激感来放松

    • 点个宵夜,搞个小烧烤吃吃,借助食物换取特定心境

    • 想一些过往能够牵动情绪的往事,沉浸在起伏连绵的情绪中



  • 绝了,以上三种我都尝试过,全中,但是作为程序员我还会有其他的几种:

    • 学习知识📖

    • 优化代码💼

    • 加快需求进度,赶需求🏃



  • 我经常会有这种想法,如果晚上11点半到1点半我可以把这些事情做完或者做多一点,那么我的时间就会被延长🕐。

  • 错❌,看了这个视频后我真的悟了,我花掉了N个晚上的两个小时,但是换不回来人生相应的发展,甚至很多质量很差的决策、代码都是在这个时间段产出的。

  • 可能你确实在这晚上获得了很多愉悦感,但是这个愉悦感是没有办法持续的第二天又赖床又想逃避,你会去想我白白浪费了晚上两个小时刷剧,过了一个晚上这个愉悦感在你早上醒来的时候会忽然转化为你的焦虑感

  • 确实是这样的,特别是在周末熬夜的时候,你会潜意识的特别晚睡,第二天让睡眠拉满,直接到中午才起床,但其实这样不是浪费了更多的时间吗?


🤔 三个风险



  • HeyMatt老师提到在熬夜的这些时间,面临了至少三个风险。


时间的消耗不可控



  • 就拿我来举例,我前段时间老是想着公司需求怎么做,需求的方案是不是不完整,是不是有可以优化的点,要修复的Bug怎么定位,怎么解决。

  • 我不自觉的就会想,噢我晚上把它给搞定,那么第二天就可以放下心去陪家人出去走走。

  • 可是事实呢?运气好一点或许可以在2个小时解决1点准时睡觉,但是运气不好时,时间会损耗越来越多,2个半小时,3个小时,4个小时,随着时间的消逝,问题没有解决就会越发焦虑,不禁查看时间已经凌晨3-4点了。

  • 就更不用说以前大学的时候玩游戏,想着赢一局就睡觉,结果一晚上都没赢过...😓


精神方面的损耗



  • 当我们消耗了晚上睡眠时间来工作、来学习、来游戏,那么代价就是你第二天会翻倍的疲惫。

  • 你会不自觉的想要睡久一点,因为这样才能弥补你精神的损耗,久而久之你就会养成晚睡晚起的习惯,试问一下自己有多久没有在周末看过清晨的阳光了?

  • 再说回我,当我前一个晚上没有解决问题带着焦虑躺在床上时,我脑子会不自觉全是需求、Bug,这真的不夸张,我真的睡着了都会梦到我在敲代码。这其实就是一种极度焦虑而缺乏休息的大脑能干出来的事情。

  • 我第二天闹钟响了想起我还有事情没做完,就会强迫自己起床,让自己跟**“想休息的大脑”**打架,久而久之这危害可想而知。


健康维度的损耗



  • 随着熬夜次数的增多,年龄的增长,很多可见或不可见的身体预警就会越来越多,具体有什么危害,去问AI吧,它是懂熬夜的。



🔥 做清晨的霸主



  • 那么怎么解决这些问题呢,其实很简单,把晚上11.30后熬夜的时间同等转化到早上即可,比如11.30-1.30,那么就转化到6.30-8.30,这时候就会有同学问了:哎呀小卢,你说的这么简单,就是起不来呀!!

  • 别急,我们都是程序员,最喜欢讲原理了,HeyMatt老师也将原理告诉了我们。


赖床原理



  • 其实我们赖床起不来的很大一部分原因是自己想太多了。

  • 闹钟一响,你会情不自禁去思考,“我真的要现在起床吗?” “我真的需要这一份需要早起的工作吗?” “我起床之后我需要干什么?” “这么起来会不会很累,要不还是再睡一会,反正今天不用上班?”

  • 这时候咱们大脑就处于一种**“睡眠”“清醒”**的重叠状态,就跟叠buffer一样,大脑没有明确的收到指令是要起床还是继续睡。

  • 当我们想得越多,意识就变得越模糊,但是大脑不愿意去思考,大脑无法清晰地识别并执行指令,导致我们又重新躺下了。


练就早起



  • 在一次采访中,美国作家 Jocko Willink 老师提出了一种早起方法::闹钟一响,你的大脑什么都不要想,也不需要去想,更不用去思考,让大脑一片空白,你只需执行动作即可。

  • 而这个动作其实特别简单,就是坐起来--->站起来--->去洗漱,什么都不用想,只用去做就好。

  • 抱着试一试的心态,我尝试了一下这种方法,并在第二天调整了闹钟到 6:30。第二天闹钟一响,直接走进卫生间刷个牙洗个脸,瞬间清醒了,而且我深刻的感觉到我的专注力精神力有着极大的提升,大脑天然的认为现在是正常起床,你是需要去工作和学习👍。

  • 绝了,这个方法真的很牛*,这种方法非常有效,让我觉得起床变得更容易了,推荐大家都去试试,你会回来点赞的。


克服痛苦



  • 是的没错,上面这种办法是会给人带来痛苦的,在起床的那一瞬间你会感觉仿佛整个房间的温度都骤降了下来,然后,你使劲从被窝里钻出来,脚底下着地的瞬间,你感到冰凉刺骨,就像是被一桶冰水泼醒一样。你感到全身的毛孔都瞬间闭合,肌肉僵硬,瑟瑟发抖,好像一股冰冷的气流刺痛着你的皮肤。

  • 但是这种痛苦是锐减的,在三分钟之后你的痛苦指数会从100%锐减到2%

  • 带着这种征服痛苦的快感,会更容易进入清晨的这两小时的写作和工作中。


✌️ 我得到了什么



  • 那么早起后,我收获了什么呢❓❓


更高效的工作时间



  • 早起可以让我在开始工作前有更多的时间来做自己想做的事情,比如锻炼、读书、学习新技能或者提升自己的专业知识等,这些事情可以提高我的效率专注力,让我在工作时间更加高效。

  • 早起可以让我更容易集中精力,因为此时还没有太多事情干扰我的注意力。这意味着我可以更快地完成任务,更少地分心更少地出错


更清晰的思维



  • 早上大脑比较清醒,思维更加清晰,这有助于我更好地思考解决问题,我不用担心我在早上写的需求方案是否模糊,也能更好的做一些决策

  • 此外,早起还可以让我避免上班前匆忙赶路的情况,减少心理上的紧张压力


更多可支配的时间



  • 早起了意味着早上两个最清醒的时间随便我来支配,我可以用半小时运动,再用10分钟喝个咖啡,然后可以做我喜欢做的事情。

  • 可以用来写代码,可以用来写文章,也可以用来运营个人账号

  • 可以让我有更多的时间规划安排工作,制定更好的工作计划时间管理策略,从而提高工作效率减少压力


更好的身体健康



  • 空腹运动对我来说是必须要坚持的一件事情,早起可以让我有更多的时间来锻炼身体,这对程序员来说非常重要,因为长时间的坐着工作容易导致身体不健康

  • 用来爬楼,用来跑步,用来健身环等等等等,随便我支配,根本不用担心下班完了后缺乏运动量。


👋 写在最后



  • 我相信,我坚持了一年后,我绝对可以成为清晨的霸主,你当然也可以。

  • 而且通过早起不思考这个方法,很多在生活有关于拖延的问题都可以用同样的方式解决,学会克服拖延直接去做,在之后就会庆幸自己做出了正确的决定

  • 如果您觉得这篇文章有帮助到您的的话不妨🍉🍉关注+点赞+收藏+评论+转发🍉🍉支持一下哟~~😛您的支持就是我更新的最大动力。

  • 如果想跟我一起讨论和学习更多的前端知识可以加入我的前端交流学习群,大家一起畅谈天下~~~

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

什么是 HTTP 长轮询?

什么是 HTTP 长轮询? Web 应用程序最初是围绕客户端/服务器模型开发的,其中 Web 客户端始终是事务的发起者,向服务器请求数据。因此,没有任何机制可以让服务器在没有客户端先发出请求的情况下独立地向客户端发送或推送数据。 为了克服这个缺陷,Web 应用...
继续阅读 »

什么是 HTTP 长轮询?


Web 应用程序最初是围绕客户端/服务器模型开发的,其中 Web 客户端始终是事务的发起者,向服务器请求数据。因此,没有任何机制可以让服务器在没有客户端先发出请求的情况下独立地向客户端发送或推送数据。


为了克服这个缺陷,Web 应用程序开发人员可以实施一种称为 HTTP长轮询的技术,其中客户端轮询服务器以请求新信息。服务器保持请求打开,直到有新数据可用。一旦可用,服务器就会响应并发送新信息。客户端收到新信息后,立即发送另一个请求,重复上述操作。


什么是 HTTP 长轮询?


那么,什么是长轮询?HTTP 长轮询是标准轮询的一种变体,它模拟服务器有效地将消息推送到客户端(或浏览器)。


长轮询是最早开发的允许服务器将数据“推送”到客户端的技术之一,并且由于其寿命长,它在所有浏览器和 Web 技术中几乎无处不在。即使在一个专门为持久双向通信设计的协议(例如 WebSockets)的时代,长轮询的能力仍然作为一种无处不在的回退机制占有一席之地。


HTTP 长轮询如何工作?


要了解长轮询,首先要考虑使用 HTTP 的标准轮询。


“标准”HTTP 轮询


HTTP 轮询由客户端(例如 Web 浏览器)组成,不断向服务器请求更新。


一个用例是想要关注快速发展的新闻报道的用户。在用户的浏览器中,他们已经加载了网页,并希望该网页随着新闻报道的展开而更新。实现这一点的一种方法是浏览器反复询问新闻服务器“内容是否有任何更新”,然后服务器将以更新作为响应,或者如果没有更新则给出空响应。浏览器请求更新的速率决定了新闻页面更新的频率——更新之间的时间过长意味着重要的更新被延迟。更新之间的时间太短意味着会有很多“无更新”响应,从而导致资源浪费和效率低下。


HTTP 轮询


上图:Web 浏览器和服务器之间的 HTTP 轮询。服务器向立即响应的服务器发出重复请求。


这种“标准”HTTP 轮询有缺点:



  • 更新请求之间没有完美的时间间隔。请求总是要么太频繁(效率低下)要么太慢(更新时间比要求的要长)。

  • 随着规模的扩大和客户端数量的增加,对服务器的请求数量也会增加。由于资源被无目的使用,这可能会变得低效和浪费。


HTTP 长轮询解决了使用 HTTP 进行轮询的缺点



  1. 请求从浏览器发送到服务器,就像以前一样

  2. 服务器不会关闭连接,而是保持连接打开,直到有数据供服务器发送

  3. 客户端等待服务器的响应。

  4. 当数据可用时,服务器将其发送给客户端

  5. 客户端立即向服务器发出另一个 HTTP 长轮询请求


HTTP 长轮询


上图:客户端和服务器之间的 HTTP 长轮询。请注意,请求和响应之间有很长的时间,因为服务器会等待直到有数据要发送。


这比常规轮询更有效率。



  • 浏览器将始终在可用时接收最新更新

  • 服务器不会被永远无法满足的请求所搞垮。


长轮询有多长时间?


在现实世界中,任何与服务器的客户端连接最终都会超时。服务器在响应之前保持连接打开的时间取决于几个因素:服务器协议实现、服务器体系结构、客户端标头和实现(特别是 HTTP Keep-Alive 标头)以及用于启动的任何库并保持连接。


当然,许多外部因素也会影响连接,例如,移动浏览器在 WiFi 和蜂窝连接之间切换时更有可能暂时断开连接。


通常,除非您可以控制整个架构堆栈,否则没有单一的轮询持续时间。


使用长轮询时的注意事项


在您的应用程序中使用 HTTP 长轮询构建实时交互时,需要考虑几件事情,无论是在开发方面还是在操作/扩展方面。



  • 随着使用量的增长,您将如何编排实时后端?

  • 当移动设备在WiFi和蜂窝网络之间快速切换或失去连接,IP地址发生变化时,长轮询会自动重新建立连接吗?

  • 通过长轮询,您能否管理消息队列并如何处理丢失的消息?

  • 长轮询是否提供跨多个服务器的负载平衡或故障转移支持?


在为服务器推送构建具有 HTTP 长轮询的实时应用程序时,您必须开发自己的通信管理系统。这意味着您将负责更新、维护和扩展您的后端基础设施。


服务器性能和扩展


使用您的解决方案的每个客户端将至少每 5 分钟启动一次与您的服务器的连接,并且您的服务器将需要分配资源来管理该连接,直到它准备好满足客户端的请求。一旦完成,客户端将立即重新启动连接,这意味着实际上,服务器将需要能够永久分配其资源的一部分来为该客户端提供服务。当您的解决方案超出单个服务器的能力并且引入负载平衡时,您需要考虑会话状态——如何在服务器之间共享客户端状态?您如何应对连接不同 IP 地址的移动客户端?您如何处理潜在的拒绝服务攻击?


这些扩展挑战都不是 HTTP 长轮询独有的,但协议的设计可能会加剧这些挑战——例如,您如何区分多个客户端发出多个真正的连续请求和拒绝服务攻击?


消息排序和排队


在服务器向客户端发送数据和客户端发起轮询请求之间总会有一小段时间,数据可能会丢失。


服务器在此期间要发送给客户端的任何数据都需要缓存起来,并在下一次请求时传递给客户端。


HTTP 长轮询 MQ


然后出现几个明显的问题:



  • 服务器应该将数据缓存或排队多长时间?

  • 应该如何处理失败的客户端连接?

  • 服务器如何知道同一个客户端正在重新连接,而不是新客户端?

  • 如果重新连接花费了很长时间,客户端如何请求落在缓存窗口之外的数据?


所有这些问题都需要 HTTP 长轮询解决方案来回答。


设备和网络支持


如前所述,由于 HTTP 长轮询已经存在了很长时间,它在浏览器、服务器和其他网络基础设施(交换机、路由器、代理、防火墙)中几乎得到了无处不在的支持。这种级别的支持意味着长轮询是一种很好的后备机制,即使对于依赖更现代协议(如 WebSockets )的解决方案也是如此。


众所周知,WebSocket 实现,尤其是早期实现,在双重 NAT 和某些 HTTP 长轮询运行良好的代理环境中挣扎。


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