Appearance
知识点记录
React+Ts+antd 技术栈
React
React.memo
高阶组件,如果组件在相同的 props 下渲染出相同的结果,使用这个方法处理之后直接复用最近一次的渲染的结果,是一个性能优化的处理方式。
一般子组件最好都是需要使用这个方法处理一下,避免子组件不必要的渲染。
- memo 只检查 props 变成,如果是 useState 之类的变更,它仍会重新渲染
js
function MyComponent(props) {
/* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
/*
如果把 nextProps 传入 render 方法的返回结果与
将 prevProps 传入 render 方法的返回结果一致则返回 true,
否则返回 false
*/
}
export default React.memo(MyComponent, areEqual)
function MyComponent(props) {
/* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
/*
如果把 nextProps 传入 render 方法的返回结果与
将 prevProps 传入 render 方法的返回结果一致则返回 true,
否则返回 false
*/
}
export default React.memo(MyComponent, areEqual)
memo 误用等于反向优化
memo 并不是随意使用的 API,用不好可能会反向优化。
之前公司有非常厉害的同事反馈我们项目中一些 api 用的过于随意,就像memo
、useMemo
这种,在细致的研究了之后发现果然是不能随便使用。下面梳理一下原因:
在特殊的情况下,使用使用memo
这个 API,可以减去没必要组件渲染,所以这是一个 React 的性能优化的一个方向和手段。但并不是所有场景我们都需要使用它,也不是所有的组件都需要使用 memo 包裹一层。
组件为什么会重新渲染
组件的内容之所以会渲染,就是因为组件的的 props 在两次 diff 的比对是否相同时的结果是false
,所以触发了重新渲染。所以 memo 的这个 API 目的是让前后两次 props 的比较的结果变成 true
组件 props 的比较方式
加上 memo 之后,比较方式的逻辑就会不同
组件的 props 是一个对象
默认情况下,两次 diff 其实是判断这个两个对象是否相同(判断的是对象的地址是否相同。)
逻辑可以理解成 :
jsconst diffProps1 = {} const diffProps2 = {} const shouldUpdate = !(diffProps1 === diffProps2)
const diffProps1 = {} const diffProps2 = {} const shouldUpdate = !(diffProps1 === diffProps2)
使用了
memo
后,两次 diff 比对的是:前后两个对象的所有 value 是否相同。jsconst diff1 = { name: 'jimmy', } const diff2 = { name: 'jimmy', } const shouldUpdate = !(diff1.name === diff2.name)
const diff1 = { name: 'jimmy', } const diff2 = { name: 'jimmy', } const shouldUpdate = !(diff1.name === diff2.name)
所以我们在使用了 memo 之后,在一些特殊情况下可以防止组件的没有比较的更新。但是通过上面的简单例子我们可以知道,使用了memo
之后,比较是否相等的判断其实更为复杂了,是一个更不搞笑的比较。
反向优化例子
一个反向优化的例子:
tsx
import { memo, useState } from 'react'
const Item = memo(
({
name,
data,
handle,
}: {
name: string
data: number
handle: () => void
}) => {
console.log('name:', name, 'data', data)
return (
<div>
<p>{name}</p>
<p>{data}</p>
</div>
)
}
)
export default function () {
const [num1, setNum1] = useState(0)
const [num2, _] = useState(0)
const handleAdd = () => {
setNum1(num1 + 1)
}
return (
<div>
<button onClick={handleAdd}>add</button>
<Item name="item1" data={num1} handle={handleAdd} />
<Item name="item2" data={num2} handle={handleAdd} />
</div>
)
}
import { memo, useState } from 'react'
const Item = memo(
({
name,
data,
handle,
}: {
name: string
data: number
handle: () => void
}) => {
console.log('name:', name, 'data', data)
return (
<div>
<p>{name}</p>
<p>{data}</p>
</div>
)
}
)
export default function () {
const [num1, setNum1] = useState(0)
const [num2, _] = useState(0)
const handleAdd = () => {
setNum1(num1 + 1)
}
return (
<div>
<button onClick={handleAdd}>add</button>
<Item name="item1" data={num1} handle={handleAdd} />
<Item name="item2" data={num2} handle={handleAdd} />
</div>
)
}
上面的例子我们虽然给每个Item
都使用了memo
这个 API,但是实际上当父组件状态发生改变时,每个Item
依旧会重新渲染一遍,这是为什么呢?大家可以思考一下。
原因如下:
tsx
const diffProps1 = {
name: 'item1',
data: 0,
handle: () => {
setNum1(num1 + 1)
},
}
const diffProps2 = {
name: 'item1',
data: 0,
handle: () => {
setNum1(num1 + 1)
},
}
const shouldUpdate = !(
(
diffProps1.name === diffProps2.name && // 相等
diffProps1.data === diffProps2.data && // 相等
diffProps1.handle === diffProps2.handle
) // 不相等
)
const diffProps1 = {
name: 'item1',
data: 0,
handle: () => {
setNum1(num1 + 1)
},
}
const diffProps2 = {
name: 'item1',
data: 0,
handle: () => {
setNum1(num1 + 1)
},
}
const shouldUpdate = !(
(
diffProps1.name === diffProps2.name && // 相等
diffProps1.data === diffProps2.data && // 相等
diffProps1.handle === diffProps2.handle
) // 不相等
)
所以在这种情况下,组件也每次会渲染,反而增加了比对的成本,这就是一个名副其实的 反向优化 。
总结
使用这些 React 的性能优化 API 时,我们应该慎用,需要简单的先进性判断,需要衡量少一次的渲染和 diff 的复杂度进行一个简单的对比。如果是渲染一次成本非常高,像渲染markdown
这种级别的,那就放心大胆用,如果多渲染一次没有什么复杂操作,其实也没有必要使用。用了可能反而会给整个项目的性能造成负担!
useModule
UmiJs 的一个 hook,提供消费 Model 能力,相当于是处理全局状态,跨组件通信方式。
useModule 有两个参数,namespace 和 updater。
namespace
- 就是 hooks model 文件的文件名,如上面例子里的useAuthModel
updater
- 可选参数。在 hooks model 返回多个状态,但使用组件仅引用了其中部分状态,并且希望仅在这几个状态更新时 rerender 时使用(性能相关)。
js
import { useModel } from 'umi'
export default () => {
const { user, fetchUser } = useModel('user', model => ({
user: model.user,
fetchUser: model.fetchUser,
}))
return <>hello </>
}
import { useModel } from 'umi'
export default () => {
const { user, fetchUser } = useModel('user', model => ({
user: model.user,
fetchUser: model.fetchUser,
}))
return <>hello </>
}
在项目中使用案例
js
// models文件夹中创建一个dzInfo.ts
import { useState } from 'react';
export default function useDzInfo() {
const [info, setInfo] = useState({ name: 'Jimmy', love: 'xuexue' });
return {
...info,
setInfo,
};
}
// 在其他的任意组件内部使用
import { useModel, memo } from 'umi';
export default memo(()=>{
const { name, love, setInfo } = useModel('dzInfo')
return (
<div>hello world</div>
)
})
// models文件夹中创建一个dzInfo.ts
import { useState } from 'react';
export default function useDzInfo() {
const [info, setInfo] = useState({ name: 'Jimmy', love: 'xuexue' });
return {
...info,
setInfo,
};
}
// 在其他的任意组件内部使用
import { useModel, memo } from 'umi';
export default memo(()=>{
const { name, love, setInfo } = useModel('dzInfo')
return (
<div>hello world</div>
)
})
TypeScript
keyof 操作符
基本使用
ts
let user: Person = {
name: 'jimmy',
age: 22,
}
function getValue(p: Person, k: keyof Person) {
return p[k]
}
getValue(user, 'name')
let user: Person = {
name: 'jimmy',
age: 22,
}
function getValue(p: Person, k: keyof Person) {
return p[k]
}
getValue(user, 'name')
keyof 可以获取 一个类型的 所有的 “键(key)” 组成一个 类似元组类型。
ts
let user: Person = {
name: 'jimmy',
age: 22,
}
function getValue(p: Person, k: keyof Person) {
return p[k]
}
getValue(user, 'name')
// getValue(user, 'love') 编译不通过
let user: Person = {
name: 'jimmy',
age: 22,
}
function getValue(p: Person, k: keyof Person) {
return p[k]
}
getValue(user, 'name')
// getValue(user, 'love') 编译不通过
这个例子中 TS 会做属性检测, 当我们试图传一个 不存在的属性时,编译器会直接报错,这个就是 TS 妙用的一点。
typeof 操作符
typeof 在 js 中就是一个非常有用的一个操作符,可以返回 一个目标值的对象。
在 TS 可以利于这个机制 给某个 目标对象赋值一个类型
ts
type Person = {
name: string
age: number
}
const jimmy: Person = {
name: 'Jimmy',
age: 22,
}
// OtherPerson 的类型是 jimmy这个对象的类型 OtherPerson = Person
type OtherPerson = typeof jimmy
const fn = (a: number, b: number): number => a + b
const f2: typeof fn = (c: number, d: number) => c - d
type Person = {
name: string
age: number
}
const jimmy: Person = {
name: 'Jimmy',
age: 22,
}
// OtherPerson 的类型是 jimmy这个对象的类型 OtherPerson = Person
type OtherPerson = typeof jimmy
const fn = (a: number, b: number): number => a + b
const f2: typeof fn = (c: number, d: number) => c - d
f2 的类型 为 fn 的类型,所以 f2 也必须接受两个参数 且返回值是一个 number 类型的数,这个在看公司同事前辈代码有看到有这样写的 也是非常的好用。
in 操作符
js 中 in 可以用于遍历对象中的 所有的 key,ts 中 in 还能用于处理类型。遍历出一个类型中的每个 key。
ts
type Person = {
name: string
age: number
}
type KeyToNumber<T> = {
/**
* keyof 能够获取 T 所有的 key
* 再使用 in 遍历所有的key
* 最终意思是 将 T 的所有的 key取出来 赋值为number类型
* */
[key in keyof T]: number
}
// 类型赋值 Person 就是 KeyToNumber 的 T
const jimmy: KeyToNumber<Person> = {
name: 15120,
age: 22,
}
type Person = {
name: string
age: number
}
type KeyToNumber<T> = {
/**
* keyof 能够获取 T 所有的 key
* 再使用 in 遍历所有的key
* 最终意思是 将 T 的所有的 key取出来 赋值为number类型
* */
[key in keyof T]: number
}
// 类型赋值 Person 就是 KeyToNumber 的 T
const jimmy: KeyToNumber<Person> = {
name: 15120,
age: 22,
}
泛型约束的使用
我们知道泛型是动态的,可以传递任意类型,但是我们有时候又不能让任意类型都通过泛型来传递,所以这时候我们就需要约束一下我们传递的这个泛型!
ts
function sum<T>(arr: T[]) {
let count = 0
/**
* 运算符“+=”不能应用于类型“T”和“number”。 T 可以是任意类型 如果T 是 undefined 或者其他恶心类型 就会报错
* 所以TS 非常智能的帮我们想到了这一点
* 但是开发者是知道 这个T 大致是什么类型的,所以我们就可以使用类型约束
* 看 sum2
* */
arr.forEach(item => (item += count))
}
function sum2<T extends number>(arr: T[]) {
let count = 0
/**
* 这时候ts就不会报错了, 因为 T 是继承于 number的 可以理解成是 number 的子集 所以肯定是能够正常相加的
*/
arr.forEach(item => (count += item))
}
/**
* U 继承自 T 的所有属性,所以如果 U 不是 T 的属性时 就会提示报错
*/
function pick<T, U extends keyof T>(obj: T, key: U) {
return obj[key]
}
let person = {
name: 'jimmy',
age: 22,
} as { name: string; age: number }
pick(person, 'name')
pick(person, 'age')
// xuexue 不是 person对象的属性 所以TS 直接报错
pick(person, 'xuexue')
function sum<T>(arr: T[]) {
let count = 0
/**
* 运算符“+=”不能应用于类型“T”和“number”。 T 可以是任意类型 如果T 是 undefined 或者其他恶心类型 就会报错
* 所以TS 非常智能的帮我们想到了这一点
* 但是开发者是知道 这个T 大致是什么类型的,所以我们就可以使用类型约束
* 看 sum2
* */
arr.forEach(item => (item += count))
}
function sum2<T extends number>(arr: T[]) {
let count = 0
/**
* 这时候ts就不会报错了, 因为 T 是继承于 number的 可以理解成是 number 的子集 所以肯定是能够正常相加的
*/
arr.forEach(item => (count += item))
}
/**
* U 继承自 T 的所有属性,所以如果 U 不是 T 的属性时 就会提示报错
*/
function pick<T, U extends keyof T>(obj: T, key: U) {
return obj[key]
}
let person = {
name: 'jimmy',
age: 22,
} as { name: string; age: number }
pick(person, 'name')
pick(person, 'age')
// xuexue 不是 person对象的属性 所以TS 直接报错
pick(person, 'xuexue')
类型反转至可选(Partail)
JavaScript 是对值进行编程,而 TypeScript 是对类型进行编程!
假如我们定义了一个 Person 类,这个 Person 类有三个属性,并且都是必填的。我们可以这样写:
ts
type person = {
name: string
age: number
sex: boolean
}
type person = {
name: string
age: number
sex: boolean
}
突然有一天有个修改密码之类的其他逻辑,这个借口需要传手机号,其他信息:name、age、sex 都是可选的,这时候就可以使用一个Partial
来实现一个反选,快速的实现操作:
ts
type NewPerson = Partial<Person> & { phone: number }
type NewPerson = Partial<Person> & { phone: number }
可以将Partial<Person>
理解成是一种类型的函数,可以将 Person 这个类型全部实现一次反选
类型反转至必选(Required)
将所有的属性变为必填项
ts
type Person = {
name?: string
age?: number
sex?: boolean
}
type RequirePerson = Required<Person>
const jimmy: RequirePerson = {
name: 'jimmy',
age: 22,
sex: true,
}
/**
* Required 和 Partial 是互斥的 简单实现一下
*/
type RequireType<T> = {
// [key in keyof T]: T[key] 直接赋值 原来可选的还是继续可选
[key in keyof T]-?: T[key] // 这里细节是 需要使用 -? 可以理解成 将原来的 ? 给 "减" 掉
}
type OtherRequire = RequireType<Person>
/**
* 复习一个 Partial
*/
type OtherPartial<T> = {
[key in keyof T]?: T[key]
}
type OtherPartialType = OtherPartial<OtherRequire>
type Person = {
name?: string
age?: number
sex?: boolean
}
type RequirePerson = Required<Person>
const jimmy: RequirePerson = {
name: 'jimmy',
age: 22,
sex: true,
}
/**
* Required 和 Partial 是互斥的 简单实现一下
*/
type RequireType<T> = {
// [key in keyof T]: T[key] 直接赋值 原来可选的还是继续可选
[key in keyof T]-?: T[key] // 这里细节是 需要使用 -? 可以理解成 将原来的 ? 给 "减" 掉
}
type OtherRequire = RequireType<Person>
/**
* 复习一个 Partial
*/
type OtherPartial<T> = {
[key in keyof T]?: T[key]
}
type OtherPartialType = OtherPartial<OtherRequire>
Record 工具
TS 提供了一个 Reacrd 工具,让我们能够快速的将一个类型的 所有属性值 转换为 另外一个类型。
ts
// string 类型的key 它的值 也全部转为string
const obj: Record<string, string> = {
name: 'zhangsan',
tag: '打工人',
}
// number 类型的key string类型的值
const obj2: Record<number, string> = {
22: 'zhangsan',
33: '打工人',
}
// symbol 类型 的key boolean 类型的值
const obj3: Record<symbol, boolean> = {
[Symbol('symbol')]: true,
[Symbol('symbol~~')]: false,
}
/**
* T extends keyof any 表示的是 T 继承 any类型,
* 继承 并非它本身是 any类型 它也有自己的类型
* (之所以要继承一下 any 类型 是因为 any类型可以有 对象类型 并且其是 所有类型的父集
* 如果直接使用 any 类型 则在这个例子下 不能限制 key 的类型了
*
* 见 OtherRecord2
*/
type OtherRecord<T extends keyof any, K> = {
[key in T]: K
}
const obj4: OtherRecord<symbol, boolean> = {
[Symbol('symbol')]: true,
[Symbol('symbol~~')]: false,
22: 'zhangsan',
}
/**
* key直接使用 any类型 则无法限制 key 自身的类型
*/
type OtherRecord2<K> = {
[key in any]: K
}
const obj5: OtherRecord2<boolean> = {
[Symbol('symbol')]: true,
[Symbol('symbol~~')]: false,
22: false,
}
// string 类型的key 它的值 也全部转为string
const obj: Record<string, string> = {
name: 'zhangsan',
tag: '打工人',
}
// number 类型的key string类型的值
const obj2: Record<number, string> = {
22: 'zhangsan',
33: '打工人',
}
// symbol 类型 的key boolean 类型的值
const obj3: Record<symbol, boolean> = {
[Symbol('symbol')]: true,
[Symbol('symbol~~')]: false,
}
/**
* T extends keyof any 表示的是 T 继承 any类型,
* 继承 并非它本身是 any类型 它也有自己的类型
* (之所以要继承一下 any 类型 是因为 any类型可以有 对象类型 并且其是 所有类型的父集
* 如果直接使用 any 类型 则在这个例子下 不能限制 key 的类型了
*
* 见 OtherRecord2
*/
type OtherRecord<T extends keyof any, K> = {
[key in T]: K
}
const obj4: OtherRecord<symbol, boolean> = {
[Symbol('symbol')]: true,
[Symbol('symbol~~')]: false,
22: 'zhangsan',
}
/**
* key直接使用 any类型 则无法限制 key 自身的类型
*/
type OtherRecord2<K> = {
[key in any]: K
}
const obj5: OtherRecord2<boolean> = {
[Symbol('symbol')]: true,
[Symbol('symbol~~')]: false,
22: false,
}
Pick 工具
此工具的作用是将 T 类型中的 K 键列表提取出来,生成新的子键值对类型。
ts
type Person = {
name: string
age: number
height: number
weight: number
sex: boolean
lover: string
}
/**
* 将 Person 类型中的 name lover age 取出来单独 拼凑成一个类型返回
*/
type OtherPerson = Pick<Person, 'name' | 'lover' | 'age'>
const jimmy: OtherPerson = {
name: 'jimmy',
lover: 'xuexue',
age: 22,
}
/**
* 实现一个 Pick
* K extends keyof T
* - keyof 返回的就是 对象的属性 如: 'name'|'age' K 再继承自这个
* - 所以 K 一定是 T 的属性
* [P in K] 遍历 K 的一个个属性 值为 T[P]
*/
type OtherPick<T, K extends keyof T> = {
[P in K]: T[P]
}
type Person2 = {
name: string
age: number
height: number
weight: number
sex: boolean
lover?: string // lover 为可选
}
const xue: OtherPick<Person2, 'lover' | 'sex'> = {
sex: true,
}
type Person = {
name: string
age: number
height: number
weight: number
sex: boolean
lover: string
}
/**
* 将 Person 类型中的 name lover age 取出来单独 拼凑成一个类型返回
*/
type OtherPerson = Pick<Person, 'name' | 'lover' | 'age'>
const jimmy: OtherPerson = {
name: 'jimmy',
lover: 'xuexue',
age: 22,
}
/**
* 实现一个 Pick
* K extends keyof T
* - keyof 返回的就是 对象的属性 如: 'name'|'age' K 再继承自这个
* - 所以 K 一定是 T 的属性
* [P in K] 遍历 K 的一个个属性 值为 T[P]
*/
type OtherPick<T, K extends keyof T> = {
[P in K]: T[P]
}
type Person2 = {
name: string
age: number
height: number
weight: number
sex: boolean
lover?: string // lover 为可选
}
const xue: OtherPick<Person2, 'lover' | 'sex'> = {
sex: true,
}
Omit 工具
Omit 英文意思是 省略、过滤,此工具可认为是适用于键值对对象的 Exclude,它会去除类型 T 中包含 K 的键值对。
ts
type Person = {
name: string
age: number
height: number
weight: number
sex: boolean
lover: string
}
type Coder = {
// name: string;
// age: number;
// sex: boolean;
dz: string
}
type NotSame = Omit<Person, 'name'>
/**
* NotSame 的类型为:
* {
age: number;
height: number;
weight: number;
sex: boolean;
lover: string;
}
*/
type Person = {
name: string
age: number
height: number
weight: number
sex: boolean
lover: string
}
type Coder = {
// name: string;
// age: number;
// sex: boolean;
dz: string
}
type NotSame = Omit<Person, 'name'>
/**
* NotSame 的类型为:
* {
age: number;
height: number;
weight: number;
sex: boolean;
lover: string;
}
*/
ReturnType 工具
ReturnType 直译 返回值类型 也就是函数返回值类型,传递一个 泛型 获取这个泛型的返回值类型。
ts
type fn = (x: number, y: number) => number
type fnType = ReturnType<fn> // number类型 因为 fn类型的返回值就是 number类型
/**
* 实现一个 ReturnType
* <T extends (...args: any) => any> 约束了 T 是一个函数类型
*
* T 继承一个函数类型的 返回值
* T extends (
...args: any
) => infer R // 使用 infer 推断 是否是一个函数 且 是否有返回 R 类型 如果有 则为 R类型 否则为 any 类型
? R
: any
*/
type OtherReturnType<T extends (...args: any) => any> = T extends (
...args: any
) => infer R
? R
: any
type fnType2 = OtherReturnType<{ name: string }> // 会报错 因为 泛型T 限制了继承自一个 函数类型
type fnType3 = OtherReturnType<fn> // number类型
type fn = (x: number, y: number) => number
type fnType = ReturnType<fn> // number类型 因为 fn类型的返回值就是 number类型
/**
* 实现一个 ReturnType
* <T extends (...args: any) => any> 约束了 T 是一个函数类型
*
* T 继承一个函数类型的 返回值
* T extends (
...args: any
) => infer R // 使用 infer 推断 是否是一个函数 且 是否有返回 R 类型 如果有 则为 R类型 否则为 any 类型
? R
: any
*/
type OtherReturnType<T extends (...args: any) => any> = T extends (
...args: any
) => infer R
? R
: any
type fnType2 = OtherReturnType<{ name: string }> // 会报错 因为 泛型T 限制了继承自一个 函数类型
type fnType3 = OtherReturnType<fn> // number类型
强制类型转换
umi 提供了一个useLocation
钩子,再钩子可以返回路由对象信息,其中就有 query 查询参信息,但是在项目中直接写会报错提示找不到 query 参数。但是实际上我们可以获取到这个参数,为了解决这个问题可以使用强制类型转换,先转成 unkonow 类型,再转成我们想要的类型。
js
import { memo, useState } from 'react';
import { useLocation, Location } from 'umi';
type dz = {
query:{
cardId:number
}
}
export default memo(()=>{
const { query } = useLocation() // ts类型报错
// 解决方式1(不太好) 只有query 其他的属性不能访问了 如 pathname
const { query } = useLocation() as unknow as {
query:{
cardId:number
}
}
// 解决方式2(推荐) location 的其他属性 如 pathname 仍然可以正常访问
const { query } = useLocation() as Location & dz
})
import { memo, useState } from 'react';
import { useLocation, Location } from 'umi';
type dz = {
query:{
cardId:number
}
}
export default memo(()=>{
const { query } = useLocation() // ts类型报错
// 解决方式1(不太好) 只有query 其他的属性不能访问了 如 pathname
const { query } = useLocation() as unknow as {
query:{
cardId:number
}
}
// 解决方式2(推荐) location 的其他属性 如 pathname 仍然可以正常访问
const { query } = useLocation() as Location & dz
})
& 符号在 ts 中表示类型的联合
Location & dz 等于在 Location 类型的基础上加上 dz 这个类型
强制有值
ts 一个“神奇的写法”,如:
js
AddTag.showModal({
data: ['111'],
onSuccess: (value) => {
console.log(value);
// alert(value);
},
});
payload.onSuccess!([form.getFieldsValue()]);
AddTag.showModal({
data: ['111'],
onSuccess: (value) => {
console.log(value);
// alert(value);
},
});
payload.onSuccess!([form.getFieldsValue()]);
onSuccess 是一个函数,我们使用函数的方式进行使用,这里的一个细节是在调用函数的括号前添加了一个 ! ,经过同事的讲解,得知这是 ts 的写法。解析如下:
payload 这个参数有 onSuccess 属性,其可以是函数类型,也可以不传,所以我们直接执行的时候 ts 会警告,有可能没有传这个参数,这是编辑器防止报错的友好提示,但是写代码的是我们自己,我们自己肯定知道有没有传这个函数,所以就可以加这个 ! 相当于告诉编辑器我们一定有传这个值,非常好用!!
接口规范函数
使用接口规范函数相对来说更加的晦涩难懂一点,如下面的例子,我的想法最初是只停留在街口规范一些对象或者类的类型,所以完全不知道应该如果实现一个 SearchFncs 接口的数据,后面才知道当 接口 这样写,是用来规范函数的。
ts
interface SearchFncs {
(source: string, subString: string): boolean
}
let obj: SearchFncs = (source, subString) => {
return Math.random() > 0.4
}
interface SearchFncs {
(source: string, subString: string): boolean
}
let obj: SearchFncs = (source, subString) => {
return Math.random() > 0.4
}
正常情况下我不会这么写,我是会比较习惯使用 type 来规范一个函数类型,会比上面的例子更加清晰一些。收获是知道了是用 接口规范函数 的这一情况。
js
type SearchFncs = (source: string, subString: string) => boolean
let obj: SearchFncs = (source, subString) => {
return Math.random() > 0.4
}
type SearchFncs = (source: string, subString: string) => boolean
let obj: SearchFncs = (source, subString) => {
return Math.random() > 0.4
}
enum key 转为类型
假设我们有一组这样的枚举数据(枚举并非只有数字枚举,虽然数字枚举在大多时候是用的最多的):
ts
enum ReverseCommodityStatus {
'ON_SHELF' = '上架',
'ON_SALE' = '下架',
}
enum ReverseCommodityStatus {
'ON_SHELF' = '上架',
'ON_SALE' = '下架',
}
我们想要定义一个类型,它是 'ON_SHELF'||'ON_SALE'
,这时候我们就可以将keyof
和typeof
配合使用:
ts
type TTabKey = keyof typeof ReverseCommodityStatus // 'ON_SHELF'||'ON_SALE'
type TTabKey = keyof typeof ReverseCommodityStatus // 'ON_SHELF'||'ON_SALE'
这个还是非常实用也是非常常用的一个场景,我们这样只需要定义类型即可,就可以不用再去创建多余的映射关系,代码量少了,也更加优雅了。
enum value 转为类型
和 key 类型转换是同一个场景,我们想要获取一个枚举类型枚举值的集合时,可以这样操作:
ts
enum ReverseCommodityStatus {
'ON_SHELF' = '上架',
'ON_SALE' = '下架',
}
type TTabValue = `${ReverseCommodityStatus}` // '上架'|'下架'
enum ReverseCommodityStatus {
'ON_SHELF' = '上架',
'ON_SALE' = '下架',
}
type TTabValue = `${ReverseCommodityStatus}` // '上架'|'下架'
泛型做动态类型的推导
已知有以下一个类型:
ts
export type RootStackParamList = {
Mine: { userId: number }
Main: undefined
}
export type RootStackParamList = {
Mine: { userId: number }
Main: undefined
}
现有一个函数为navigate()
,接受两个参数,两个参数绑定的关系与类型RootStackParamList
一致,当第一个参数为Mine
时,次二个参数就必须为{ userId: number }
,当第一个参数为Main
时,第二个参数就必须为undefined
,二者一一对应,此时可以配合泛型来重新定义navigate
这个函数
ts
type RootStackParamList = {
Mine: { userId: number }
Main: undefined
}
export function navigates<T extends keyof RootStackParamList>(
name: T,
params: RootStackParamList[T]
) {}
type RootStackParamList = {
Mine: { userId: number }
Main: undefined
}
export function navigates<T extends keyof RootStackParamList>(
name: T,
params: RootStackParamList[T]
) {}
泛型的重点在于类型推导,这个才是泛型的核心,并不是写了<T>
,就一定要传递一个类型!
antd
Form 表单使用细节
antd 的 Form 表单和 Vue 相关的组件的 Form 表单的差别还是非常之大的,如:
js
import { Form, Input } from 'antd'
import { memo } from 'react'
const initMsg = {
username: 'Jimmy',
}
export default memo(() => {
return (
<Form name="basic" initialValues={initMsg} onFinish={() => {}}>
<Form.Item
label="Username"
name="username"
rules={[{ required: true, message: 'Please input your username!' }]}
>
<Input />
</Form.Item>
</Form>
)
})
import { Form, Input } from 'antd'
import { memo } from 'react'
const initMsg = {
username: 'Jimmy',
}
export default memo(() => {
return (
<Form name="basic" initialValues={initMsg} onFinish={() => {}}>
<Form.Item
label="Username"
name="username"
rules={[{ required: true, message: 'Please input your username!' }]}
>
<Input />
</Form.Item>
</Form>
)
})
Form.Item 中如果只放了一个表单组件如 Input,则 Input 会自动的双向绑定至 Form.Item 中的 name 所对应的属性值!
- 有且仅有 Form.Item 中只有表单组件时才会生效,否则都是不绑定成功
如果想要实现如 form 表单之后添加文字,需要嵌套 Form.Item,如:
js
import { Form, Input, Space } from 'antd'
import { memo } from 'react'
const initMsg = {
username: 'Jimmy',
}
export default memo(() => {
return (
<Form name="basic" initialValues={initMsg} onFinish={() => {}}>
<Form.Item
label="Username"
name="username"
rules={[{ required: true, message: 'Please input your username!' }]}
>
<Space>
<Form.Item name="username">
<input />
</Form.Item>
<span style={{ marginBottom: 22, display: 'block' }}>哈哈哈</span>
</Space>
</Form.Item>
</Form>
)
})
import { Form, Input, Space } from 'antd'
import { memo } from 'react'
const initMsg = {
username: 'Jimmy',
}
export default memo(() => {
return (
<Form name="basic" initialValues={initMsg} onFinish={() => {}}>
<Form.Item
label="Username"
name="username"
rules={[{ required: true, message: 'Please input your username!' }]}
>
<Space>
<Form.Item name="username">
<input />
</Form.Item>
<span style={{ marginBottom: 22, display: 'block' }}>哈哈哈</span>
</Space>
</Form.Item>
</Form>
)
})
有一个"对象"类型, 它一定有 name 和 age 这个两个属性
过去这种场景我不会写,只好用 any 代过,但其实使用 any 是个很不好的习惯。可以这样写:
typescript
type TBase = {
name: any
age: any
}
type TUser = TBase & Record<string, any>
const jim: TUser = {
name: 'jimmy',
age: 22,
love: 22,
dshsjhds: 'sdhahsdsd',
}
type TBase = {
name: any
age: any
}
type TUser = TBase & Record<string, any>
const jim: TUser = {
name: 'jimmy',
age: 22,
love: 22,
dshsjhds: 'sdhahsdsd',
}
canvas
本周接到了一个新的需求,在微信小程序里开发 小游戏(Q 版神庙逃亡,躲避障碍--左右横跳上下跳)日后改头换面开源出来再给大家看,现在先保密,大家可以先自行脑补,开发过程非常的煎熬。
一方面是自己虽然有一点点开发小游戏的经验,但说白了那都是小玩具,而且是用比较成熟的游戏开发框架egret 白鹭引擎,而这次用不了这个,要用原生的 canvas 开发,而且是直接要上线跑,让很多 C 端用户能玩
Canvas 固然强大,但是其 API 过于底层,开发起来其实是有很多的学习和试错成本的。
另外一方面是... 给的时间也不是很多,而且最可怕的是根本不知道自己通过加班能不能做得完。
所以整个重构重新写了三遍,周末在家里加班了两天,终于算是完整的理解了 canvas这一整套 API 的核心思想,也基本知道应该怎么做了,应该能够在预期的时间内做完了.......
回顾一下这个煎熬的过程吧:
最早想着是否可以使用 内嵌页面 的方式来实现这个小游戏呢?
很显然是不行,小游戏涉及得分,之后根据得分领取对应的奖励发放至登录的账户,涉及了很多用户信息以及领奖的业务流程,如果内嵌页面很难将这些东西串起来。
那是否可以不用 canvas,直接使用类似操作 DOM 的方式来写呢?如果可以的话那整体难度将下降很多。
这个可以是可以,但是明显感觉这个不是一个正解,所以还是得用 canvas,一方面我自己想着也是这个,另外就是组长也说用这个好。那就用这个吧~
整个小游戏从最早开发开始到现在(现在基本能跑),我重构了三次,也问了组长一个问题:”如果是你,你会怎么做“
最早我尝试直接使用原生的 canvas,动画什么的也全部自己写,结果就是代码很难看,有时候就会自己也突然忘记了上段逻辑是处理啥的来着
后来我尝试搜索一个 canvas 的库,很遗憾由于小程序的原因,很多 canvas 的框架库在小程序里面都不能用,但后面我还是发现了一个专门为小程序涉及的轻量级 canvas 库 wxDraw,很强大,但是也有一点儿问题。
由于这个库在 18 年就停止维护了,而且我发现有很严重的性能问题,看了一下 issues 也有反馈这个,所以写了很多的代码又不能用了,又得重写
再次尝试用原生的 canvas 来写,这回加上了缓动动画库 tween.js,加上之前两次重写其实也有积累一些工具方法,抽象类之类的,所以第三遍很快的就有了雏形, 能跑 🏃🏻♀️了!
总结
这次任务让我算是比较彻底的理解了如果使用 canvas,以及一些 canvas 的使用细节,因为 canvas 实际上是每一帧都是重新画上去的,所以我们如果细节处理不到位就会导致整个页面极其的卡顿。希望这个小游戏今后也能成为我简历上的一个小标签或者亮点吧~
今后我会将这个小游戏改头换面再将它的代码开源出来,供小伙伴们学习。
最后的最后,希望小伙伴们今天遇到的需求都是会做的需求 😊。
业务
前端实现列表筛选功能
前端通过接口获取商品列表数据,后端一次性返回 500 多条数据,由前端实现分页以及组合模糊搜索,分页其实蛮简单的,因为 antd 现成的 table 组件库已经封装了这一功能,真正令我有些头疼的一个功能是组合模糊搜索,因为在我的印象中这些操作都是后端处理的,所以突然交给前端处理一时间有一点懵,业务逻辑如下:
虚拟货品的名称和实际货品的名称这两项需要前端实现模糊搜索,即如果虚拟货品名称输入 ll 能筛选出编号为 hellloaaaa 这条数据,这时候如果关联实际货品名称有输入内容的话,需要在此基础上再进行一次筛选。
最终模糊搜索的逻辑我采用了数据的 includes
方法来实现,因为includes
方法返回得结果直接就是一个布尔值,是最适合我们做判断了,再配合上数组的filters
方法,在filters
方法内部只有includes
方法返回为 true 的时候再 return 出来,就可以实现模糊搜索这个逻辑啦~
部分代码
ts
let arr: ProductListItem[] = data
if (keyword) {
// keyword 判断的是是否有输入 筛选项 之一
arr = data?.filter(item =>
item[form.getFieldValue('keyword').key].includes(keyword.trim())
)
}
// 如果有筛选项2 就可以继续筛选
return arr
let arr: ProductListItem[] = data
if (keyword) {
// keyword 判断的是是否有输入 筛选项 之一
arr = data?.filter(item =>
item[form.getFieldValue('keyword').key].includes(keyword.trim())
)
}
// 如果有筛选项2 就可以继续筛选
return arr
现在回过头来看其实组合筛也还可以不是很难,这个业务也还行,第一次遇到这种需求,小小记录一下~
前端计算时可能会有的问题
前言
0.1+0,2!==0.3,这个问题相信有背过面试八股文的同学都知道,也能说出个所以然来,然而我还是犯了一个错误.....
虽然大部分的金额等等数据后端都会算好了给前端,但是总有一些细枝末节的小点可能会让前端计算,比较少做这一块的我成功的踩坑了。
当产品把图截给我的时候我第一反应还是,会不会是后端返回的两个相减的数据有问题,经过检查发现并不是的,49.06-45.14!==3.92,再次触发了精度问题!
最后的解决方式是,我给计算结果截取了两位小数,但是感觉还是不够智能,但是急于上线的我就先这样写让它先跑着,后面再看看应该如何解决。
心中更加完美一点的解决方式
自己封装一个 加减 的方法,最后的截取的小数点取两个数小数点更多位的那个数的小数数量。
应该还有更好的方法吧?
为什么会触发精度问题(简单解答):传送门
震惊!我的代码被广告屏蔽插件给拦截了
在快下班时,测试同学在正式环境给我反馈了一个特别有意思的 bug(现在想着有意思,那时候真有点慌,毕竟是线上环境~)
正常情况下,页面的显示效果是这样的:
测试同学给我反馈,在他的电脑里无论在哪个 tab 下,都无法获取下面的内容,最终效果始终是这样:
整个过程我的心态发生了巨大的变化,
就光光对比两张图,我的第一想法是:这肯定是 后端大兄弟 的锅,在我的电脑都能正常的访问,跟我一点关系都没有!
但是经过经过检查,发现接口是正常返回的,就是页面没有显示出来,于是我在想,难道是代码写的有问题吗?不应该啊,就正常的一个循环渲染,这也能翻车吗?于是我检查我的代码!
经过检查代码也没有问题,于是我们就在想,会不会是浏览器版本的问题啊,刚好测试同学的 chrome 不是最新版,于是找了公司其他同事的电脑也开始测,发现没有最新版本的有几个也不能正常显示,于是我不知道应该怎么办了,这种版本问题真没遇到过,我写的也是蛮简单的布局的,怎么会触发这个版本问题呢?于是无奈下我只能求助了带我的大哥!
大哥非常仔细通过开发者工具检查页面,发现其实 DOM 元素(结构)其实在页面上已经有了,但是就是不显示,这个过程几个前端围着,一直检查,就是没有发现哪里有问题!
最后!大哥发现!我起的类名有点问题,因为我做这个是广告项,所以我下意识将每一项起名为 adItem !就是这个类名的问题!如果有的浏览器安装了广告拦截的小插件,就会将我的 DOM 元素添加上
display:none
,这就是触发这个问题的原因!
因为公司有同事开了拦截广告的插件,所以在他那就没法正常显示!
解决方案
知道了是什么原因,解决起来就简单了,只要将 拦截广告插件卸载掉就行了!!!开个玩笑,肯定不能那样,正确的做法是只要将类名换个名字,不要用带有敏感的 ad 之类容易引起误会的类名就行了!
收获
通过这个 bug 其实也让我成长了很多,程序员除了要会写代码以外,经验也是一个非常重要的东西,说实话如果是我来找这个 bug,可能就会往 版本兼容 方向,越走越远,越走越错,而有经验的大哥就很快找到原因,是被插件拦截导致的!以后在写一些样式的时候也会注意这个了!
还有一个收获是好像间接的知道一些拦截广告插件的实现原理了,它们应该会先扫描一遍所有的 DOM,将一些有可能是广告的类型加上 display:none
!
差点提桶跑路!前端也能引发大事故~
本周我遇到了一个入行以来遇到的最严重的一个 bug,差点提桶跑路了 😭。那就是: 死循环, 在这里我要承认一下我很菜,不是新手前端,居然还能写出这个?大家听我解释......😢
先上个图吧~大家就知道事情的严重性了......(触发了无限发起网络请求 😱)
在过去,我一直以为一个小小前端在一个项目里所承担的压力是比较小的,比如一个电商项目,我们最起码也要能保证主流程能跑通(也就是支付和下单)。显然,我还是太天真了! 而在我的印象里,后端的压力一直都是比前端大的,因为后端要能保证整个服务的稳定,因为一旦后端服务挂了,那么导致的结果就是整个项目都不能运行,用户不能正常下单,也就会造成经济损失了。
事情是这样的 😔:
本周同事在开发新需求时突然发现,我处理的一个模块在特定的操作流程下,会触发无限的网络请求,而且是实时的。
这就很恐怖了 😱,一旦用户处理操作了以后,又不及时的关闭浏览器,我们这发请求的速度相当于直接DDoS我们后端了,前端亲自给后端做压测~不出意外的话那么后端服务肯定就会直接被我们干崩溃了,那么前面提到的压力就来到我们前端这边了。
那么到底为什么会这样呢 ❓
这里简单的解释一下为什么会触发这个无限的网络请求,正常情况下一个成熟的前端是肯定不会犯这种错误的,这里的原因在于我引用了一个项目共用的组件,这个组件在设计的时候就是单例的设计模式去设计的,所以当页面存在两个以后,并且状态还不一样,就会一直触发状态更新,而状态更新就会触发网络请求,死循环就是这么来的...
最后花费了大半天的时间,请教了原组件的作者,定位问题之后还算是平稳的解决了这个问题了 😊
这件事情也是给我上了一课,一个项目里每个角色都很关键,每个角色也都能触发严重的事故,作为前端开发中除了要做一些基础的测试之外,也要时刻注意逻辑清晰,如果引用第三方的组件也一定要做好全量的测试,避免出现死循环,左后无奈被优化~
安全无小事!
杂谈
在繁杂的工作中记录一些所见所想。
如何更高效的工作
不知道大家是否有因为“效率”一词而感到烦恼,进入新公司的我入职已经一个多月了,这个月中我学习到了一个十分正规的软件开发流程,结实了很多优秀的小伙伴,还有就是对 react、git,等技术有了更加深入的认识,已经能够很好的融入这个团队,以及愉快的进行开发了。正当我洋洋得意的融入的很快的时候,才发现自己其实做的并不是很好,或者说”可以更好“。
组长说了我的效率问题,诚然:入职一个多月的我在产出上确实并没有很明显的提升。但是组长的话有种”一句惊醒梦中人”的感觉,其实也发自内心感谢组长的提醒,每当我有些洋洋得意的时候就需要有一个人为我敲响警钟,就像我组长最后跟我说的:“回家之后可以适当学习一下!”。
正如一个好友说的:在节奏快的公司工作,我们就不能还保留慢节奏的习惯。比如过去的我上午一般不怎么写代码,更习惯看一些技术文档和修一些 bug,这点在快节奏的公司肯定是不能继续做下去的,作为程序员得适应环境做相对应的事情。
如果我们效率不高,就会被测试、产品、后端同学催促,整个项目的进度不大可能因为我们一个前端而放慢速度,本人在初入行的时候就是一个经常被催促的角色,相信我,那种感觉真的不好受!
以上都是一些我对于”效率“的看法,接着给大家分享几个我组长传授给我的几个效率妙招与我自己总结的效率妙招,希望能够帮助一些刚入行还没多久的小伙伴!
在固定的时间修 BUG
bug 是在所难免的,作为程序员经常会面临的局面是:今天的任务还没做完,还有之前历史遗留下来的 bug 要修,这时候应该先做哪个呢?
组长认为:我们可以每天先做当天的任务,到下午 5:30 再开始统一的处理 bug!
细想一下这样真的很棒,我就几乎是反着来的,先修 bug 再做任务,这就可能会导致,我们的开发进度是落后的,也就是效率不高的原因之一,更恐怖的是这会造成无限加班的恐怖循环
在有很多需求时,可以适当在后一个需求多加一些时间
比如我们手上有需求 A、B、C,开发时间都是预计两天完成,这时候报预期时如果都报两天可能也会造成达不到预期的效果(因为修 bug 会消耗掉我们做其他需求的时间),所以我们可以适当的报 A(2 天) B(2.5-3 天) C(3)天
统一记录相关的文档
这个是我认为非常有必要做的事情,尤其是当手上的需求比较多的时候,这时候只要花 5 分钟把这件事情做了,之后的开发会更加的行云流水!这招就是————统一归纳所有的 需求文档、接口文档、修复文档、时间安排文档
示例:
......持续更新
关于效率的问题就跟分享到这里了,最后我想说的是,在工作中还是应该尽量快的完成我们的任务,这点我也和朋友们讨论过,如果我们自己是领导,肯定也会更喜欢效率更高的开发同学,所以为了我们将来的工作、上位、薪资,真的得开始重视这个问题。
程序员“到点就走”与“加班”问题
不知从何时开始,程序员和“加班”被绑定到了一起,这个问题从刚步入工作的我开始便一直隐隐约约的困扰着我,心态也随之有了一些改变,回想一下我大概经历了有这么几个过程吧:
大家都还没走,我再等几分钟吧
像极了母亲教的,大家还没有动筷子,我们不能先吃,等别人吃了再吃。
因为刚入行,所以不会分配什么任务,有也是相对轻松的任务,所以几乎没有加班的困扰,相反有的是一种 下班的困扰。简称:不敢下班~
工作又不能按时完成了,又要加班!烦!不想干了!
入行几个月后,领导逐渐开始安排一些工作,由于对于业务的不理解以及代码量不足的原因,几乎很难按时完成任务,对于我来说这是作为前端程序员来说最噩梦的时候,几乎每天面临着被产品经理、后端、测试、组长(老大)催促,甚至恨不得不吃饭,也要去写 bug。甚至会疑惑:天哪,我为什么会选择做程序员这一行呢。!
这种情况是被迫加班,没办法,只能硬撑着。
"叮叮叮",定的 6 点的闹钟响了,打卡溜了!
工作了一段时间之后,和周围的同事都能“打情骂俏”之后,逐渐的 老员工 化,开始到点就走。
互联网行业现如今已经越来越卷,想想大厂好像几乎都是九点下班,关于“到点就走”,这块也是一直困扰着我的一个点,身边的朋友也是近乎分成两大派系,一个主张到点就走,一个主张学会儿习再走。
由于加班带来的效率提升本身并不明显,加上我也是一个“宁愿学习,也不愿意加班”的人,除非做不完,但是如果能够把两者稍微权衡一下会不会有奇效呢?
权衡了很久,我决定效仿后者,自从工作之后才发觉时间之宝贵,除了周末有点时间学习以外,平时工作日要学习真的蛮难的,所以如果下班之后留在公司学习一会儿再回家好像也还不错,虽然我的观点思想是和前者小伙伴一样到点就走,但是多待一会儿也没有那么糟,说不定能让领导认为你是个还蛮热爱学习的人?当然前提是一定是有去学习而不是去 为了迟点走而迟点走,或者说做给领导看,个人认为那样没什么意义。
大兄弟你们觉得呢?
排查了两个小时的问题,罪魁祸首居然是 yarn.lock😭
本周做一个 RN 项目,实现一个从相册中获取图片,再根据图片解析二维码的需求,做这个需求时引入了两个新的库,它们分别实现读取手机相册、根据图片识别二维码的功能。
过程中遇到了很多问题,因为之前没有接触过 RN,所以拆了很多坑,如果讲的话完全能再起一个新的篇章,就不在这赘述了。
正当我在我分支开发完并且测试完,并且将代码合入到master
准备下班时,测试同学打的最新包出现了一个新的问题,输入框突然就不起作用了!症状如下:
这就很奇怪了,我压根没有动过这块的代码,心想:肯定是哪个倒霉蛋改的,测试同学怎么什么问题都找我啊!没办法了,帮忙看看吧~
当我看了一下分支merge
记录之后,发现只有我往master
合了代码!这说明,这个问题一定是我带上去的,怎么会这样呢?我根本没改这块的代码呀?
于是我开始了漫长的排查,分别试了以下的情况进行排查:
是打包的问题?再打一次包看看
还是不行
是我引入新的库的问题?
删一个库打一次包,删一个库打一个包
还是不行
回到
rebase master
之前这回居然可以了,可到底是咋回事呢,引入的新包也都分别删掉测试了都没有找到问题!
经过至少十次的删除node_modules
,和安装(合并到master
前和合并到master
之后),我发现了一些端倪!分别是如下情况:
合并master
之前使用yarn
安装依赖:
之后:
非常明显的是,之后的安装在走进入条之前,还有一系列东西的安装,于是针对这点,又经过了半个多小时的排查,最后在一个技术大哥提醒下,是不是因为yarn.lock
文件解冲突的时候被我删了?
于是在我将被删除的yarn.lock
文件又加回了项目中,再安装依赖之后打包,那个问题也就消失了。
整个时长经历了长达快两个小时,过程我甚至没有心思去吃晚饭...好在问题成功的解决了。
也算知道了一个非常关键的信息:yarn.lock 文件不能随便删除!
为什么删了 yarn.lock 会导致这个问题?
我的理解是,yarn.lock
存的是一些包的版本信息,我理解的是:就算把yarn.lock
文件删了,执行yarn
的时候也会去package.json
文件中获取依赖包和对应的版本信息再下载呀,之后再生成新的yarn.lock
。
其实仔细查询了一下才知道我们使用yarn
,或者npm install
的时候安装的版本并不是一定按照package.json
来的,讲到这里就需要简单的知道package.json
以及安装模块时的一些细节知识点,如以下这个模块:
json
"dependencies": {
"vitepress": "^0.20.9",
"body-parser": "~1.15.2"
}
"dependencies": {
"vitepress": "^0.20.9",
"body-parser": "~1.15.2"
}
这两个依赖的最前面分别是^
和~
,这两个其实是有讲究的,他们都会涉及一些版本的更新,也就是虽然我们文件写着是这个版本号,但是实际上可能会下载到不同的版本:
~
:下载时会更新到中版本的最新版本
~1.15.2 会下载大于等于 1.15.2 小于 1.16.0 的版本
^
:则是更加大胆,其会去下载 主版本 的最新版本
^1.15.2 会下载大于等于 1.15.2 小于 2.0.0 的版本
而当有 yarn.lock 文件在的时候,其会锁死某个库的指定小版本,就会不发生一些库的自动升级,多人协同时也就不会有一些奇奇怪怪的问题啦~
小结一下
总结下来通过这个 bug,算是让我知道了原来我们简单的执行npm install
或者yarn
的时候,真实下载时还有这种版本号的细节,最关键的是知道了:yarn.lock 文件不能乱删啊 😭
CSDN 真是 “yyds” 呀 🐂🍺
小小的吐槽一下......😌
最近开发了一个 app 的需求,需要使用到蓝牙模块,使用蓝牙链接其他设备用于数据的传输,在上传苹果的AppStore的时候被拒绝了。苹果给了拒绝的原因,以及解决方式。
苹果的 AppStore 过审核也是个技术活
拒绝邮件原文
Guideline 2.5.4 - Performance - Software Requirements
Your app declares support for bluetooth-central in the UIBackgroundModes key in your Info.plist but does not provide Bluetooth Low Energy functionality. The Bluetooth Background modes are for apps that communicate to other devices using Bluetooth Low Energy and the Core Bluetooth framework.
Next Steps
To resolve this issue, please revise your app to add support for Bluetooth Low Energy communication through Core Bluetooth in your app or remove the bluetooth- values from the UIBackgroundModes key.
Resources
For more information on Core Bluetooth, please review the Core Bluetooth Framework Reference.
Guideline 5.1.1 - Legal - Privacy - Data Collection and Storage
We noticed that your app requests the user’s consent to access the Bluetooth, but doesn’t sufficiently explain the use of the Bluetooth in the purpose string.
To help users make informed decisions about how their data is used, permission request alerts need to explain and include an example of how your app will use the requested information.
Next Steps
Please revise the purpose string in your app’s Info.plist file for the Bluetooth to explain why your app needs access.
You can modify your app's Info.plist file using the property list editor in Xcode.
Resources
See examples of helpful, informative purpose strings.
Review a list of relevant property list keys.
Please see attached screenshot for details.
其实苹果官方已经给了很明显的提示了,我也大致知道应该如果修改,但是因为这方便的开发经验毕竟比较少,所以还是看下网上的大家的解决方案吧,于是我显示在谷歌上搜索一下,果然,一堆的解决方案!传送门
由于自己的英语水平并不高,只能算是看的一知半解,为了避免重复送审重复被拒的情况出现,我还是在国内平台搜索了一下(百度),不出意外的也是有解决方案的,但是感觉有一丝答非所问,也是很难找到我想要找到的解答。但是也还是找到了。传送门
比较关键的是我再 csdn 上发现了一个一样问题的中文解答,结果,居然要钱......劝退,告辞!👋🏻
我的想法
我并没有想要跟风黑 csdn,毕竟当初我也是 csdn 的忠实用户,我注册的第一个技术博客网站就是 csdn,所以我觉得 csdn 其实是对于新手来说非常友好的,很多新手都会涌入 csdn 进行学习,这是它的优势 🍺,但是正所谓 得于斯者,毁于斯 随着自己的开发技术的提升和周围大环境潜移默化的影响, csdn 也体现了一些它的不足:
知识大部分比较简单
这个对于新手比较友好,也就是为什么身边的老司机都不用它,甚至会嘲讽 你还用 CSDN?
一些相对冷门一点问题的解答可能比较少
可能因为这个平台老司机都不爱用有关。
好文是真的好,但是好像也比较多水的。
一些强制用户的操作
- 登录了才能复制
- 关注才能查看原文
- 有的内容收费
好像这些有的是用户设置的,要恰饭我是非常理解的,但是正是因为谷歌上的一些免费且没有这些限制,相比较之下就比这个会用的舒服。
对于程序员来说用的舒服应该是非常重要的(至少对于我来说是这样的),所以如果条件允许的话,一些冷门问题还是在 Stack Overflow 之类的搜索会有更加权威的解决!
愿开发中再无 bug peace🤘🏻