数栈技术分享前端篇:TS,看你哪里逃~

数栈DTinsight 2021-08-09 17:25:54
大数据


数栈是—站式大数据开发平台,我们在github和gitee上有一个有趣的开源项目:FlinkX,FlinkX是一个基于Flink的批流统一的数据同步工具,既可以采集静态的数据,也可以采集实时变化的数据,是全域、异构、批流一体的数据同步引擎。大家喜欢的话请给我们点个star!star!star!

github开源项目:https://github.com/DTStack/fl...

gitee开源项目:https://gitee.com/dtstack_dev...

有兴趣的话,欢迎大家加入我们的交流社群:30537511(钉钉群)

写在前面

本文难度偏中下,涉及到的点大多为如何在项目中合理应用TS,小部分会涉及一些原理,受众面较广,有无TS基础均可放心食用

阅读完本文,您可能会收获到:

1、若您还不熟悉 TS,那本文可帮助您完成 TS 应用部分的学习,伴随众多 Demo 例来引导业务应用。

2、若您比较熟悉 TS,那本文可当作复习文,带您回顾知识,希望能在某些点引发您新发现和思考。

3、针对于 class 组件的 IState 和 IProps,类比 Hook 组件的部分写法和思考。

TIPS:超好用的在线 TS 编辑器(诸多配置项可手动配置)

传送门:https://www.typescriptlang.org/

什么是 TS

不扯晦涩的概念,通俗来说 TypeScript 就是 JavaScript 的超集,它具有可选的类型,并可以编译为纯 JavaScript 运行。(笔者一直就把 TypeScript 看作 JavaScript 的 Lint)

那么问题来了,为什么 TS 一定要设计成静态的?或者换句话说,我们为什么需要向 JavaScript 添加类型规范呢 ?

经典自问自答环节——因为它可以解决一些 JS 尚未解决的痛点:1、JS 是动态类型的语言,这也意味着在实例化之前我们都不知道变量的类型,但是使用 TS 可以在运行前就避免经典低级错误。

例:Uncaught TypeError:'xxx' is not a function️ 典中典级别的错误 :

JS 就是这样,只有在运行时发生了错误才告诉我有错,但是当 TS 介入后:

 title=

 title=

好家伙!直接把问题在编辑器阶段抛出,nice!

2、懒人狂欢! 规范方便,又不容易出错,对于 VS Code,它能做的最多只是标示出有没有这个属性,但并不能精确的表明这个属性是什么类型,但 TS 可以通过类型推导/反推导(说白话:如果您未明确编写类型,则将使用类型推断来推断您正在使用的类型),从而完美优化了代码补全这一项:

 title=

 title=

1)第一个 Q&A——思考 :提问: 那么我们还能想到在业务开发中 TS 解决了哪些 JS 的痛点呢?回答,总结,补充:

  • 对函数参数的类型限制;
  • 对数组和对象的类型限制,避免定义出错 例如数据解构复杂或较多时,可能会出现数组定义错误 a = { }, if (a.length){ // xxxxx }
  • let functionA = 'jiawen' // 实际上 let functionA: string = 'jiawen'

3、使我们的应用代码更易阅读和维护,如果定义完善,可以通过类型大致明白参数的作用。相信通过上述简单的bug-demo,各位已对TS有了一个初步的重新认识 接下来的章节便正式介绍我们在业务开发过程中如何用好TS。

怎么用 TS

在业务中如何用TS/如何用好TS?这个问题其实和 " 在业务中怎么用好一个API " 是一样的。首先要知道这个东西在干嘛,参数是什么,规则是什么,能够接受有哪些扩展......等等。简而言之,撸它!哪些扩展......等等。 简而言之,撸它!

 title=

 title=

1、TS 常用类型归纳

通过对业务中常见的 TS 错误做出的一个综合性总结归纳,希望 Demos 会对您有收获

1)元语(primitives)之 string number boolean

笔者把基本类型拆开的原因是: 不管是中文还是英文文档,primitives/元语/元组 这几个名词都频繁出镜,笔者理解的白话:希望在类型约束定义时,使用的是字面量而不是内置对象类型,官方文档:

let a: string = 'jiawen';let flag: boolean = false;let num: number = 150interface IState: { flag: boolean; name: string; num: number;}

2)元组

// 元组类型表示已知元素数量和类型的数组,各元素的类型不必相同,但是对应位置的类型需要相同。
let x: [string, number];
x = ['jiawen', 18]; // ok
x = [18, 'jiawen']; // Erro
console.log(x[0]); // jiawen

3)undefined null

let special: string = undefined
// 值得一提的是 undefined/null 是所有基本类型的子类,
// 所以它们可以任意赋值给其他已定义的类型,这也是为什么上述代码不报错的原因

4)object 和 { }

// object 表示的是常规的 Javascript对象类型,非基础数据类型
const offDuty = (value: object) => {
console.log("value is ", value);
}
offDuty({ prop: 0}) // ok
offDuty(null) offDuty(undefined) // Error
offDuty(18) offDuty('offDuty') offDuty(false) // Error
// {} 表示的是 非null / 非undefined 的任意类型
const offDuty = (value: {}) => {
console.log("value is ", value);
}
offDuty({ prop: 0}) // ok
offDuty(null) offDuty(undefined) // Error
offDuty(18) offDuty('offDuty') offDuty(false) // ok
offDuty({ toString(){ return 333 } }) // ok
// {} 和Object几乎一致,区别是Object会对Object内置的 toString/hasOwnPreperty 进行校验
const offDuty = (value: Object) => {
console.log("value is ", value);
}
offDuty({ prop: 0}) // ok
offDuty(null) offDuty(undefined) // Error
offDuty(18) offDuty('offDuty') offDuty(false) // ok
offDuty({ toString(){ return 333 } }) // Error
如果需要一个对象类型,但对属性没有要求,建议使用 object
{} 和 Object 表示的范围太大,建议尽量不要使用

5)object of params

// 我们通常在业务中可多采用点状对象函数(规定参数对象类型)
const offDuty = (value: { x: number; y: string }) => {
console.log("x is ", value.x);
console.log("y is ", value.y);
}
// 业务中一定会涉及到"可选属性";先简单介绍下方便快捷的“可选属性”
const offDuty = (value: { x: number; y?: string }) => {
console.log("必选属性x ", value.x);
console.log("可选属性y ", value.y);
console.log("可选属性y的方法 ", value.y.toLocaleLowerCase());
}
offDuty({ x: 123, y: 'jiawen' })
offDuty({ x: 123 })
// 提问:上述代码有问题吗?
答案:
// offDuty({ x: 123 }) 会导致结果报错value.y.toLocaleLowerCase()
// Cannot read property 'toLocaleLowerCase' of undefined
方案1: 手动类型检查
const offDuty = (value: { x: number; y?: string }) => {
if (value.y !== undefined) {
console.log("可能不存在的 ", value.y.toUpperCase());
}
}
方案2:使用可选属性 (推荐)
const offDuty = (value: { x: number; y?: string }) => {
console.log("可能不存在的 ", value.y?.toLocaleLowerCase());
}

6)unknown 与 any

// unknown 可以表示任意类型,但它同时也告诉TS, 开发者对类型也是无法确定,做任何操作时需要慎重
let Jiaven: unknown
Jiaven.toFixed(1) // Error
if (typeof Jiaven=== 'number') {
Jiaven.toFixed(1) // OK
}
当我们使用any类型的时候,any会逃离类型检查,并且any类型的变量可以执行任意操作,编译时不会报错
anyscript === javascript
注意:any 会增加了运行时出错的风险,不到万不得已不要使用;
如果遇到想要表示【不知道什么类型】的场景,推荐优先考虑 unknown

7)union 联合类型

union也叫联合类型,由两个或多个其他类型组成,表示可能为任何一个的值,类型之间用 ' | '隔开
type dayOff = string | number | boolean
联合类型的隐式推导可能会导致错误,遇到相关问题请参考语雀 code and tips —— 《TS的隐式推导》
.值得注意的是,如果访问不共有的属性的时候,会报错,访问共有属性时不会.上个最直观的demo
function dayOff (value: string | number): number {
return value.length;
}
// number并不具备length,会报错,解决方法:typeof value === 'string'
function dayOff (value: string | number): number {
return value.toString();
}
// number和string都具备toString(),不会报错

8)never

// never是其它类型(包括 null 和 undefined)的子类型,代表从不会出现的值。
// 那never在实际开发中到底有什么作用?这里笔者原汁原味照搬尤雨溪的经典解释来做第一个例子
第一个例子,当你有一个 union type:
interface Foo {
type: 'foo'
}
interface Bar {
type: 'bar'
}
type All = Foo | Bar
在 switch 当中判断 type,TS是可以收窄类型的 (discriminated union):
function handleValue(val: All) {
switch (val.type) {
case 'foo':
// 这里 val 被收窄为 Foo
break
case 'bar':
// val 在这里是 Bar
break
default:
// val 在这里是 never
const exhaustiveCheck: never = val
break
}
}
注意在 default 里面我们把被收窄为 never 的 val 赋值给一个显式声明为 never 的变量。
如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天你的同事改了 All 的类型:
type All = Foo | Bar | Baz
然而他忘记了在 handleValue 里面加上针对 Baz 的处理逻辑,
这个时候在 default branch 里面 val 会被收窄为 Baz,导致无法赋值给 never,产生一个编译错误。
所以通过这个办法,你可以确保 handleValue 总是穷尽 (exhaust) 了所有 All 的可能类型
第二个用法 返回值为 never 的函数可以是抛出异常的情况
function error(message: string): never {
throw new Error(message);
}
第三个用法 返回值为 never 的函数可以是无法被执行到的终止点的情况
function loop(): never {
while (true) {}
}

9)Void

interface IProps {
onOK: () => void
}
void 和 undefined 功能高度类似,但void表示对函数的返回值并不在意或该方法并无返回值

10)enum

笔者认为ts中的enum是一个很有趣的枚举类型,它的底层就是number的实现
1.普通枚举
enum Color {
Red,
Green,
Blue
};
let c: Color = Color.Blue;
console.log(c); // 2
2.字符串枚举
enum Color {
Red = 'red',
Green = 'not red',
};
3.异构枚举 / 有时也叫混合枚举
enum Color {
Red = 'red',
Num = 2,
};
<第一个坑>
enum Color {
A, // 0
B, // 1
C = 20, // 20
D, // 21
E = 100, // 100
F, // 101
}
若初始化有部分赋值,那么后续成员的值为上一个成员的值加1
<第二个坑> 这个坑是第一个坑的延展,稍不仔细就会上当!
const getValue = () => {
return 23
}
enum List {
A = getValue(),
B = 24, // 此处必须要初始化值,不然编译不通过
C
}
console.log(List.A) // 23
console.log(List.B) // 24
console.log(List.C) // 25
如果某个属性的值是计算出来的,那么它后面一位的成员必须要初始化值。
否则将会 Enum member must have initializer.

11)泛型

笔者理解的泛型很白话:先不指定具体类型,通过传入的参数类型来得到具体类型 我们从下述的 filter-demo 入手,探索一下为什么一定需要泛型

泛型的基础样式

​​

\

function fun<T>(args: T): T {
return args
}

如果没接触过,是不是会觉得有点懵?没关系!我们直接从业务角度深入。

1.刚开始的需求:过滤数字类型的数组
declare function filter(
array: number[],
fn: (item: unknown) => boolean
) : number[];
2.产品改了需求:还要过滤一些字符串 string[]
彳亍,那就利用函数的重载, 加一个声明, 虽然笨了点,但是很好理解
declare function filter(
array: string[],
fn: (item: unknown) => boolean
): string[];
declare function filter(
array: number[],
fn: (item: unknown) => boolean
): number[];
3.产品又来了! 这次还要过滤 boolean[]、object[] ..........
这个时候如果还是选择重载,将会大大提升工作量,代码也会变得越来越累赘,这个时候泛型就出场了,
它从实现上来说更像是一种方法,通过你的传参来定义类型,改造如下:
declare function filter<T>(
array: T[],
fn: (item: unknown) => boolean
): T[];

当我们把泛型理解为一种方法实现后,那么我们便很自然的联想到:方法有多个参数、默认值,泛型也可以。

type Foo<T, U = string> = { // 多参数、默认值
foo: Array<T> // 可以传递
bar: U
}
type A = Foo<number> // type A = { foo: number[]; bar: string; }
type B = Foo<number, number> // type B = { foo: number[]; bar: number; }

既然是“函数”,那也会有“限制”,下文列举一些稍微常见的约束。

1. extends: 限制 T 必须至少是一个 XXX 的类型
type dayOff<T extends HTMLElement = HTMLElement> = {
where: T,
name: string
}
2. Readonly<T>: 构造一个所有属性为readonly,这意味着无法重新分配所构造类型的属性。
interface Eat {
food: string;
}
const todo: Readonly<Eat> = {
food: "meat beef milk",
};
todo.food = "no food"; // Cannot assign to 'title' because it is a read-only property.
3. Pick<T,K>: 从T中挑选出一些K属性
interface Todo {
name: string;
job: string;
work: boolean;
type TodoPreview = Pick<Todo, "name" | "work">;
const todo: TodoPreview = {
name: "jiawen",
work: true,
};
todo;

 title=

4. Omit<T, K>: 结合了 T 和 K 并忽略对象类型中 K 来构造类型。
interface Todo {
name: string;
job: string;
work: boolean;
}
type TodoPreview = Omit<Todo, "work">;
const todo: TodoPreview = {
name: "jiawen",
job: 'job',
};
5.Record: 约束 定义键类型为 Keys、值类型为 Values 的对象类型。
enum Num {
A = 10001,
B = 10002,
C = 10003
}
const NumMap: Record<Num, string> = {
[Num.A]: 'this is A',
[Num.B]: 'this is B'
}
// 类型 "{ 10001: string; 10002: string; }" 中缺少属性 "10003",
// 但类型 "Record<ErrorCodes, string>" 中需要该属性,所以我们还可以通过Record来做全面性检查
keyof 关键字可以用来获取一个对象类型的所有 key 类型
type User = {
id: string;
name: string;
};
type UserKeys = keyof User; // "id" | "name"
改造如下
type Record<K extends keyof any, T> = {
[P in K]: T;
};
此时的 T 为 any;
还有一些不常用,但是很易懂的:
6. Extract<T, U> 从T,U中提取相同的类型
7. Partial<T> 所有属性可选
type User = {
id?: string,
gender: 'male' | 'female'
}
type PartialUser = Partial<User> // { id?: string, gender?: 'male' | 'female'}
type Partial<T> = { [U in keyof T]?: T[U] }
8. Required<T> 所有属性必须 << === >> 与Partial相反
type User = {
id?: string,
sex: 'male' | 'female'
}
type RequiredUser = Required<User> // { readonly id: string, readonly gender: 'male' | 'female'}
function showUserProfile (user: RequiredUser) {
console.log(user.id) // 这时候就不需要再加?了
console.log(user.sex)
}
type Required<T> = { [U in keyof T]-?: T[U] }; -? : 代表去掉?

 title=

TS的一些须知

1、TS 的 type 和 interface

1)interface(接口) 只能声明对象类型,支持声明合并(可扩展)。

interface User { id: string}interface User { name: string}const user = {} as Userconsole.log(user.id);console.log(user.name);

 title=

2)type(类型别名)不支持声明合并 -- l类型

type User = {
id: string,
}
if (true) {
type User = {
name: string,
}
const user = {} as User;
console.log(user.name);
console.log(user.id) // 类型“User”上不存在属性“id”。
}

 title=

![]()​

 title=

3)type 和 interface 异同点总结:

a、通常来讲 type 更为通用,右侧可以是任意类型,包括表达式运算,以及映射等;

b、凡是可用 interface 来定义的,type 也可;

c、扩展方式也不同,interface 可以用 extends 关键字进行扩展,或用来 implements 实现某个接口;

d、都可以用来描述一个对象或者函数;

e、type 可以声明基本类型别名、联合类型、元组类型,interface 不行;

f、️ 但如果你是在开发一个包,模块,允许别人进行扩展就用 interface,如果需要定义基础数据类型或者需要类型运算,使用 type;

g、interface 可以被多次定义,并会被视作合并声明,而 type 不支持;

h、导出方式不同,interface 支持同时声明并默认导出,而 typetype 必须先声明后导出;r/>

2、TS 的脚本模式和模块模式

Typescript 存在两种模式,区分的逻辑是,文件内容包不包含 import 或者 export 关键字 。

1)脚本模式(Script), 一个文件对应一个 html 的 script 标签 。

2)模块模式(Module),一个文件对应一个 Typescript 的模块。

脚本模式下,所有变量定义,类型声明都是全局的,多个文件定义同一个变量会报错,同名 interface 会进行合并;而模块模式下,所有变量定义,类型声明都是模块内有效的。

两种模式在编写类型声明时也有区别,例如脚本模式下直接 declare var GlobalStore 即可为全局对象编写声明。

例子:

脚本模式下直接 declare var GlobalStore 即可为全局对象编写声明。

GlobalStore.foo = "foo";
GlobalStore.bar = "bar"; // Error
declare var GlobalStore: {
foo: string;
};

 title=

模块模式下,要为全局对象编写声明需要 declare global

GlobalStore.foo = "foo";
GlobalStore.bar = "bar";
declare global {
var GlobalStore: {
foo: string;
bar: string;
};
}
export {}; // export 关键字改变文件的模式

 title=

3、TS 的索引签名

索引签名可以用来定义对象内的属性、值的类型,例如定义一个 React 组件,允许 Props 可以传任意 key 为 string,value 为 number 的 props

interface Props {
[key: string]: number
}
<Component count={1} /> // OK
<Component count={true} /> // Error
<Component count={'1'} /> // Error

 title=

4、TS 的类型键入

Typescript 允许像对象取属性值一样使用类型

type User = {
userId: string
friendList: {
fristName: string
lastName: string
}[]
}
type UserIdType = User['userId'] // string
type FriendList = User['friendList'] // { fristName: string; lastName: string; }[]
type Friend = FriendList[number] // { fristName: string; lastName: string; }

 title=

在上面的例子中,我们利用类型键入的功能从 User 类型中计算出了其他的几种类型。FriendList[number]这里的 number 是关键字,用来取数组子项的类型。在元组中也可以使用字面量数字得到数组元素的类型。

type group = [number, string]
type First = group[0] // number
type Second = group[1] // string

 title=

5、TS 的断言

1)类型断言不是类型转换,断言成一个联合类型中不存在的类型是不允许的。

function getLength(value: string | number): number {
if (value.length) {
return value.length;
} else {
return value.toString().length;
}
// 这个问题在object of parmas已经提及,不再赘述
修改后:
if ((<string>value).length) {
return (<string>value).length;
} else {
return something.toString().length;
}
}

 title=

断言的两种写法
1. <类型>值: <string>value
2. 或者 value as string
特别注意!!!断言成一个联合类型中不存在的类型是不允许的
function toBoolean(something: string | number): boolean {
return <boolean>something;
}

 title=

2)非空断言符

TypeScript 还具有一种特殊的语法,用于从类型中删除 null 和 undefined 不进行任何显式检查。

在任何表达式之后写入实际上是一个类型断言,表明该值不是 null 或 undefined

function liveDangerously(x?: number | undefined | null) {
// 推荐写法
console.log(x!.toFixed());
}

 title=

如何在 Hook 组件中使用 TS

1、usestate

useState 如果初始值不是 null/undefined 的话,是具备类型推导能力的,根据传入的初始值推断出类型;初始值是 null/undefined 的话则需要传递类型定义才能进行约束。一般情况下,还是推荐传入类型(通过 useState 的第一个泛型参数)。

// 这里ts可以推断 value的类型并且能对setValue函数调用进行约束
const [value, setValue] = useState(0);
interface MyObject {
name: string;
age?: number;
}
// 这里需要传递MyObject才能约束 value, setValue
// 所以我们一般情况下推荐传入类型
const [value, setValue] = useState<MyObject>(null);

 title=

2)useEffect useLayoutEffect

没有返回值,无需类型传递和约束

3)useMemo useCallback

  • useMemo无需传递类型, 根据函数的返回值就能推断出类型。
  • useCallback无需传递类型,根据函数的返回值就能推断出类型。

但是注意函数的入参需要定义类型,不然将会推断为any!

const value = 10;
const result = useMemo(() => value * 2, [value]); // 推断出result是number类型
const multiplier = 2;
// 推断出 (value: number) => number
// 注意函数入参value需要定义类型
const multiply = useCallback((value: number) => value * multiplier, [multiplier]);

 title=

4)useRef

useRef传非空初始值的时候可以推断类型,同样也可以通过传入第一个泛型参数来定义类型,约束ref.current的类型。

1. 如果传值为null
const MyInput = () => {
const inputRef = useRef<HTMLInputElement>(null); // 这里约束inputRef是一个html元素
return <input ref={inputRef} />
}
2. 如果不为null
const myNumberRef = useRef(0); // 自动推断出 myNumberRef.current 是number类型
myNumberRef.current += 1;

 title=

5)useContext

useContext一般根据传入的Context的值就可以推断出返回值。一般无需显示传递类型。

type Theme = 'light' | 'dark';// 我们在createContext就传了类型了const ThemeContext = createContext<Theme>('dark');const App = () => ( <ThemeContext.Provider value="dark"> <MyComponent /> </ThemeContext.Provider>)const MyComponent = () => { // useContext根据ThemeContext推断出类型,这里不需要显示传 const theme = useContext(ThemeContext); return <div>The theme is {theme}</div>

 title=

一些思考

在本文中笔者对TS的基础应用和Hook中的TS做了一些思考,但关于关于TSC如何把TS代码转换为JS代码的内容,这个部分比较冗长,后续可以单独出一篇文章(2)来专门探索。关于TS泛型的底层实现,这个部分比较复杂,笔者还需沉淀,欢迎各位直接留言或在文章中补充!!!

本文由博客一文多发平台 OpenWrite 发布!
版权声明
本文为[数栈DTinsight]所创,转载请带上原文链接,感谢
https://segmentfault.com/a/1190000040481915

  1. HTML + CSS + JavaScript to achieve cool Fireworks (cloud like particle text 3D opening)
  2. HTML + CSS + JavaScript realizes 520 advertising love tree (including music), which is necessary for programmers to express themselves
  3. Solve the problem of Web front-end deployment server (it can be deployed online without a server)
  4. HTML + CSS + JS make wedding countdown web page template (520 / Tanabata Valentine's Day / programmer advertisement)
  5. What else can driverless minibus do besides "Park connection"?
  6. Cloud native leads the era of all cloud development
  7. NRM mirror source management tool
  8. Bring it to you, flex Jiugong
  9. Lolstyle UI component development practice (II) -- button group component
  10. Deconstruction assignment in ES6
  11. Luo 2 peerless Tang clan was officially launched. The official gave a key point, and the broadcast time was implied
  12. 20初识前端HTML(1)
  13. 当新零售遇上 Serverless
  14. 20 initial knowledge of front-end HTML (1)
  15. When new retail meets serverless
  16. [golang] - go into go language lesson 5 type conversion
  17. [golang] - go into go language lesson 6 conditional expression
  18. HTML5(八)——SVG 之 path 详解
  19. HTML5 (8) -- detailed explanation of SVG path
  20. 需要开通VIP以后页面内容才能复制怎么办?控制台禁用javascript即可
  21. Web前端|CSS入门教程(超详细的CSS使用讲解,适合前端初学者)
  22. 实践积累 —— 用Vue3简单写一个单行横向滚动组件
  23. Serverless 全能选手,再下一城
  24. What if you need to open a VIP to copy the page content? Just disable JavaScript on the console
  25. Web front end | CSS introductory tutorial (super detailed CSS explanation, suitable for front-end beginners)
  26. Practice accumulation - write a single line horizontal scroll component simply with vue3
  27. Dili Reba is thin again. She looks elegant and high in a strapless hollow skirt, and her "palm waist" is beautiful to a new height
  28. Serverless all-round player, next city
  29. The difference between MySQL semi synchronous replication and lossless semi synchronous replication
  30. Vue表单设计器的终极解决方案
  31. The ultimate solution for Vue form designer
  32. Nginx从理论到实践超详细笔记
  33. Yu Shuxin's red backless swimsuit is split to the waist and tail, with a concave convex figure and excessive color matching, and his face is white to dazzling
  34. Nginx ultra detailed notes from theory to practice
  35. 【动画消消乐|CSS】086.炫酷水波浪Loading过渡动画
  36. typecho全站启用https
  37. CCTV has another popular employee. The off-site interpretation is very professional, and the appearance ability is no less than that of Wang Bingbing
  38. [animation Xiaole | CSS] 086. Cool water wave loading transition animation
  39. Enable HTTPS in Typecho
  40. 50天用JavaScript完成50个web项目,我学到了什么?
  41. 根据JavaScript中原生的XMLHttpRequest实现jQuery的Ajax
  42. What have I learned from completing 50 web projects with JavaScript in 50 days?
  43. "My neighbor doesn't grow up" has hit the whole network. There are countless horse music circles, and actor Zhou Xiaochuan has successfully made a circle
  44. 根据JavaScript中原生的XMLHttpRequest实现jQuery的Ajax
  45. Implement the Ajax of jQuery according to the native XMLHttpRequest in JavaScript
  46. Implement the Ajax of jQuery according to the native XMLHttpRequest in JavaScript
  47. 30 + women still wear less T-shirts and jeans. If they wear them like stars, they will lose weight
  48. 数栈技术分享前端篇:TS,看你哪里逃~
  49. Several stack technology sharing front end: TS, see where you escape~
  50. 舍弃Kong和Nginx,Apache APISIX 在趣链科技 BaaS 平台的落地实践
  51. Abandon the landing practice of Kong and nginx, Apache apisik on the baas platform of fun chain technology
  52. 浪迹天涯king教你用elementui做复杂的表格,去处理报表数据(合并表头,合并表体行和列)
  53. 前端HTML两万字图文大总结,快来看看你会多少!【️熬夜整理&建议收藏️】
  54. Wandering around the world king teaches you to use elementui to make complex tables and process report data (merge header, merge table body rows and columns)
  55. 路由刷新数据丢失 - vuex数据读取的问题
  56. Front end HTML 20000 word graphic summary, come and see how much you can【 Stay up late to sort out & suggestions]
  57. Route refresh data loss - vuex data reading problem
  58. Systemctl系统启动Nginx服务脚本
  59. Systemctl system startup nginx service script
  60. sleepless