mirror of
https://github.com/DefectingCat/DefectingCat.github.io
synced 2025-07-15 16:51:37 +00:00
Rename post folder name
This commit is contained in:
64
content/mdxData.ts
Normal file
64
content/mdxData.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { hello } from 'content/sandpack/hello-world';
|
||||
import {
|
||||
firstScene,
|
||||
loadBackground,
|
||||
resetStyles,
|
||||
} from 'content/sandpack/how-to-load-a-background-with-threejs';
|
||||
import { Button, Input } from './sandpack';
|
||||
import {
|
||||
miniRedux,
|
||||
multi,
|
||||
MultiStore,
|
||||
Reducer,
|
||||
} from './sandpack/build-own-store-with-usesyncexternalstore';
|
||||
import {
|
||||
genericApp,
|
||||
genericChild,
|
||||
hookApp,
|
||||
} from './sandpack/generic-component-encapsulate-reusable-component';
|
||||
import {
|
||||
store,
|
||||
useDeferredValue,
|
||||
useId,
|
||||
useInsertionEffect,
|
||||
useSyncExternalStore,
|
||||
useTransition,
|
||||
} from './sandpack/react18-new-hooks';
|
||||
|
||||
const data = {
|
||||
sandpack: {
|
||||
common: {
|
||||
Button,
|
||||
Input,
|
||||
},
|
||||
'hello-world': {
|
||||
hello,
|
||||
},
|
||||
'how-to-load-a-background-with-threejs': {
|
||||
firstScene,
|
||||
loadBackground,
|
||||
resetStyles,
|
||||
},
|
||||
'generic-component-encapsulate-reusable-component': {
|
||||
genericApp,
|
||||
genericChild,
|
||||
hookApp,
|
||||
},
|
||||
'react18-new-hooks': {
|
||||
useTransition,
|
||||
useDeferredValue,
|
||||
useId,
|
||||
store,
|
||||
useSyncExternalStore,
|
||||
useInsertionEffect,
|
||||
},
|
||||
'build-own-store-with-usesyncexternalstore': {
|
||||
multi,
|
||||
miniRedux,
|
||||
MultiStore,
|
||||
Reducer,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default data;
|
487
content/posts/about-javascript-this.mdx
Normal file
487
content/posts/about-javascript-this.mdx
Normal file
@ -0,0 +1,487 @@
|
||||
---
|
||||
title: What about this - JavaScript 全面解析
|
||||
date: '2022-10-24'
|
||||
tags: JavaScript
|
||||
---
|
||||
|
||||
## this 全面解析
|
||||
|
||||
this 和动态作用域有些许类似,他们都是在执行时决定的。this 是在调用时被绑定的,完全取决于函数的调用位置。
|
||||
|
||||
### 确定调用位置
|
||||
|
||||
当一个函数被调用是,会创建一个活动记录(执行期上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this 就是这个记录里的一个属性。
|
||||
|
||||
调用位置就是函数在代码中被调用的位置,而不是声明的位置。可以类似于这样来这个记录并分析出函数的真正调用位置。
|
||||
|
||||
```ts
|
||||
function foo() {
|
||||
// 当前调用栈:foo
|
||||
|
||||
console.log('foo');
|
||||
bar();
|
||||
}
|
||||
function bar() {
|
||||
// 当前调用栈:foo --> bar
|
||||
|
||||
console.log('bar');
|
||||
baz();
|
||||
}
|
||||
function baz() {
|
||||
// 当前调用栈:foo --> bar --> baz
|
||||
|
||||
console.log('baz');
|
||||
}
|
||||
foo();
|
||||
```
|
||||
|
||||
### 绑定规则
|
||||
|
||||
this 是在运行时动态绑定的,所以在不同的情况下,this 可能会发生各种意料之外的情况。
|
||||
|
||||
#### 默认绑定
|
||||
|
||||
当函数在全局环境下独立调用时,this 会指向为全局对象。
|
||||
|
||||
```ts
|
||||
var a = 123;
|
||||
function foo() {
|
||||
console.log(this.a); // 123
|
||||
}
|
||||
```
|
||||
|
||||
而当函数处理严格模式下,则不能将全局对象用于默认绑定,因此 this 会绑定到`undefined`
|
||||
|
||||
```ts
|
||||
var a = 123;
|
||||
function foo() {
|
||||
'use strict';
|
||||
console.log(this.a); // TypeError: this is undefined
|
||||
}
|
||||
```
|
||||
|
||||
还有一个微妙的细节,虽然 this 的绑定完全取决于调用的位置,但是只有`foo()`函数本身处于非严格模式才能绑定到全局对象。如果只是函数执行时所在严格模式下,而本身是非严格模式,则不影响默认绑定规则。
|
||||
|
||||
```ts
|
||||
var a = 123;
|
||||
|
||||
function foo() {
|
||||
console.log(this.a);
|
||||
}
|
||||
|
||||
(() => {
|
||||
'use strict';
|
||||
foo();
|
||||
})();
|
||||
```
|
||||
|
||||
> 通常来说不推荐在代码中混用严格模式与非严格模式。
|
||||
|
||||
#### 隐式绑定
|
||||
|
||||
另外一种规则是考虑调用位置是否有上下文对象,或者说某个对象是否包含这个函数。
|
||||
|
||||
```ts
|
||||
function foo(this: typeof obj) {
|
||||
console.log(this.name);
|
||||
}
|
||||
const obj = {
|
||||
name: 'xfy',
|
||||
foo: foo,
|
||||
};
|
||||
obj.foo(); // xfy
|
||||
```
|
||||
|
||||
这种方法可以理解为将`foo()`的函数体赋值给了对象 obj 的一个属性,而执行时是从 obj 作为上下文对象来执行的。所以 this 隐式的绑定到了 obj 对象。
|
||||
|
||||
对象属性引用链中只有上一层或者说最后一层在调用位置中起作用。
|
||||
|
||||
```ts
|
||||
function foo(this: typeof obj) {
|
||||
console.log(this.name);
|
||||
}
|
||||
const obj = {
|
||||
name: 'xfy',
|
||||
foo: foo,
|
||||
};
|
||||
obj.foo(); // xfy
|
||||
|
||||
const alotherObj = {
|
||||
name: 'dfy',
|
||||
obj: obj,
|
||||
};
|
||||
alotherObj.obj.foo(); // xfy
|
||||
```
|
||||
|
||||
**隐式丢失**
|
||||
|
||||
既然会隐式的绑定,那也就会出现隐式的丢失问题。
|
||||
|
||||
```js
|
||||
function foo() {
|
||||
console.log(this.name);
|
||||
}
|
||||
|
||||
const obj = {
|
||||
name: 'xfy',
|
||||
age: 18,
|
||||
foo,
|
||||
};
|
||||
|
||||
const bar = obj.foo; // 函数别名
|
||||
bar();
|
||||
```
|
||||
|
||||
虽然 bar 是`obj.foo`的一个引用,但是它引用的是函数体本身。可以理解为将函数体传递给了 bar 这个变量,这是调用`bar()`是一个不带任何修饰的函数调用,因此使用了默认绑定。
|
||||
|
||||
另一种常见且出乎意料的情况就是在传递回调函数时:
|
||||
|
||||
```js
|
||||
function foo() {
|
||||
console.log(this.name);
|
||||
}
|
||||
|
||||
function doFoo(fn) {
|
||||
fn();
|
||||
}
|
||||
|
||||
const obj = {
|
||||
name: 'xfy',
|
||||
age: 18,
|
||||
foo,
|
||||
};
|
||||
|
||||
doFoo(obj.foo);
|
||||
```
|
||||
|
||||
参数传递其实就是一种隐式赋值,因此我们传入函数是也会被隐式赋值。只要函数体被传递后,且调用时脱离了原有的对象,就会导致 this 的隐式丢失。
|
||||
|
||||
包括`setTimeout()`方法丢失 this 也是同理。
|
||||
|
||||
#### 显式绑定
|
||||
|
||||
因为原型的特性,JavaScript 中函数也自己的属性。大多数宿主环境都会提供`call()`与`apply()`来给我们显式的绑定 this。
|
||||
|
||||
```js
|
||||
function foo() {
|
||||
console.log(this.name);
|
||||
}
|
||||
const obj = {
|
||||
name: 'xfy',
|
||||
age: 18,
|
||||
foo,
|
||||
};
|
||||
foo.call(obj);
|
||||
```
|
||||
|
||||
> call 与 apply 只是传参不同。
|
||||
|
||||
使用显式的绑定可以很好的解决传递参数时隐式丢失 this 的问题
|
||||
|
||||
```js
|
||||
function foo() {
|
||||
console.log(this.name);
|
||||
}
|
||||
const obj = {
|
||||
name: 'xfy',
|
||||
age: 18,
|
||||
foo,
|
||||
};
|
||||
function bar() {
|
||||
foo.call(obj);
|
||||
}
|
||||
setTimeout(bar, 1000);
|
||||
// 同理
|
||||
// setTimeout(() => {
|
||||
// obj.foo();
|
||||
// }, 1000);
|
||||
```
|
||||
|
||||
这里在`bar()`的内部直接手动显式的把`foo()`绑定到了 obj,无论之后怎么调用,在何处调用。都会手动的将 obj 绑定在`foo()`上。这种绑定称之为**硬绑定**。
|
||||
|
||||
不过这种绑定是特意的例子,这里手动为`foo()`绑定到了 obj。在多数情况下,我们可能需要更灵活一点。
|
||||
|
||||
在 [JavaScript 装饰器模式 🎊 - 🍭Defectink (xfy.plus)](https://xfy.plus/defect/javascript-decorator.html) 中介绍了这种工作模式。通过一个包装器配合显式绑定就能解决大部分情况下的问题。
|
||||
|
||||
```js
|
||||
function foo(msg) {
|
||||
console.log(this.name);
|
||||
console.log(msg);
|
||||
}
|
||||
function wrapper(fn, obj) {
|
||||
return (...rest) => {
|
||||
fn.apply(obj, rest);
|
||||
};
|
||||
}
|
||||
const obj = {
|
||||
name: 'xfy',
|
||||
age: 18,
|
||||
};
|
||||
const bar = wrapper(foo, obj);
|
||||
bar('嘤嘤嘤');
|
||||
```
|
||||
|
||||
但包装器不仅仅只是用来解决 this 丢失的问题,但对 this 绑定的问题 ES5 提供了内置的方法`Function.prototype.bind`。
|
||||
|
||||
```js
|
||||
function foo() {
|
||||
console.log(this.name);
|
||||
}
|
||||
const obj = {
|
||||
name: 'xfy',
|
||||
age: 18,
|
||||
};
|
||||
const bar = foo.bind(obj);
|
||||
bar();
|
||||
```
|
||||
|
||||
#### new 绑定
|
||||
|
||||
在传统面向类的语言中,“构造函数”是类中的一些特殊的方法,使用类时会调用类中的构造函数。通常类似于这样:
|
||||
|
||||
```js
|
||||
myObj = new MyClass();
|
||||
```
|
||||
|
||||
在 JavaScript 中,所有函数都可以被 new 操作符所调用。这种调用称为构造函数调用,实质上并不存在所谓的“构造函函数”,只有对于函数的“构造调用”。
|
||||
|
||||
使用 new 来发生构造函数调用时,会执行:
|
||||
|
||||
1. 创建(构造)一个新对象。
|
||||
2. 对新对象执行`[[Prototype]]`连接。
|
||||
3. 对新对象绑定到函数调用的 this。
|
||||
4. 如果函数没有返回其他对象,那么在 new 调用后自动返回这个新对象。
|
||||
|
||||
```js
|
||||
function Foo(name) {
|
||||
this.name = name;
|
||||
}
|
||||
const bar = new Foo('xfy');
|
||||
console.log(bar.name);
|
||||
```
|
||||
|
||||
使用 new 操作符来调用`foo()`时,会构造一个新对象并把它绑定到`foo()`中的 this 上。new 是最后一种可以影响函数调用时 this 绑定行为的方法,我们称之为 new 绑定。
|
||||
|
||||
> ES6 的 class 只是一个语法糖,但是它也解决了一些问题。
|
||||
|
||||
### 优先级
|
||||
|
||||
上述描述的四条规则中,如果某处位置可以应用多条规则时,就要考虑到其优先级的问题。
|
||||
|
||||
毫无疑问,默认绑定肯定是优先级最低的绑定。所以先来考虑隐式绑定与显式绑定之间的优先级,用一个简单的方法就能测试出:
|
||||
|
||||
```js
|
||||
function foo() {
|
||||
console.log(this.age);
|
||||
}
|
||||
|
||||
const xfy = {
|
||||
name: 'xfy',
|
||||
age: 18,
|
||||
foo,
|
||||
};
|
||||
const dfy = {
|
||||
name: 'dfy',
|
||||
age: 81,
|
||||
foo,
|
||||
};
|
||||
|
||||
xfy.foo(); // 18
|
||||
dfy.foo(); // 81
|
||||
|
||||
xfy.foo.call(dfy); // 81
|
||||
dfy.foo.call(xfy); // 18
|
||||
```
|
||||
|
||||
很明显,显式绑定的优先级更高,也就是说在判断时应当先考虑是否存在显式绑定。
|
||||
|
||||
那么 new 绑定与隐式绑定呢?
|
||||
|
||||
```js
|
||||
function foo(msg) {
|
||||
this.a = msg;
|
||||
}
|
||||
|
||||
const xfy = {
|
||||
name: 'xfy',
|
||||
foo,
|
||||
};
|
||||
xfy.foo('test');
|
||||
console.log(xfy);
|
||||
|
||||
const obj = new xfy.foo('this is obj');
|
||||
console.log(obj);
|
||||
```
|
||||
|
||||
可以看到这里对对象 xfy 中隐式绑定的函数进行了 new 操作,而最后的 this 被绑定到了新对象 obj 上,并没有修改 xfy 本身的值。所以 new 绑定的优先级比隐式绑定更高。
|
||||
|
||||
那 new 绑定与显式绑定呢?由于`call/apply`无法与 new 一起使用,所以无法通过`new xfy.foo.call(obj)`来测试优先级,但是我们可以通过硬绑定`bind()`来测试。
|
||||
|
||||
```js
|
||||
function foo(msg) {
|
||||
this.a = msg;
|
||||
}
|
||||
|
||||
const xfy = {
|
||||
name: 'xfy',
|
||||
foo,
|
||||
};
|
||||
|
||||
let obj = {};
|
||||
|
||||
const bar = xfy.foo.bind(obj);
|
||||
bar('obj');
|
||||
console.log(obj);
|
||||
|
||||
// bar was bind to obj
|
||||
const baz = new bar('this is baz');
|
||||
console.log(obj);
|
||||
console.log(baz);
|
||||
```
|
||||
|
||||
可以看到,在硬绑定之后,使用 new 操作对象 obj 的值并没有被改变,反而对 new 的新对象进行了修改。
|
||||
|
||||
但这真的说明 new 绑定比硬绑定优先级更高吗?实则不然,上述结果是因为 ES5 中内置的`Function.prototype.bind()`方法比较复杂,他会对 new 绑定做判断,如果是的话就会使用新创建的 this 替换硬绑定的 this。
|
||||
|
||||
这是来自 [MDN]([Function.prototype.bind() - JavaScript | MDN (mozilla.org)](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/bind#polyfill))的 polyfill bind 的方法,
|
||||
|
||||
```js
|
||||
// Yes, it does work with `new (funcA.bind(thisArg, args))`
|
||||
if (!Function.prototype.bind)
|
||||
(function () {
|
||||
var ArrayPrototypeSlice = Array.prototype.slice;
|
||||
Function.prototype.bind = function (otherThis) {
|
||||
if (typeof this !== 'function') {
|
||||
// closest thing possible to the ECMAScript 5
|
||||
// internal IsCallable function
|
||||
throw new TypeError(
|
||||
'Function.prototype.bind - what is trying to be bound is not callable'
|
||||
);
|
||||
}
|
||||
|
||||
var baseArgs = ArrayPrototypeSlice.call(arguments, 1),
|
||||
baseArgsLength = baseArgs.length,
|
||||
fToBind = this,
|
||||
fNOP = function () {},
|
||||
fBound = function () {
|
||||
baseArgs.length = baseArgsLength; // reset to default base arguments
|
||||
baseArgs.push.apply(baseArgs, arguments);
|
||||
return fToBind.apply(
|
||||
fNOP.prototype.isPrototypeOf(this) ? this : otherThis,
|
||||
baseArgs
|
||||
);
|
||||
};
|
||||
|
||||
if (this.prototype) {
|
||||
// Function.prototype doesn't have a prototype property
|
||||
fNOP.prototype = this.prototype;
|
||||
}
|
||||
fBound.prototype = new fNOP();
|
||||
|
||||
return fBound;
|
||||
};
|
||||
})();
|
||||
```
|
||||
|
||||
在这几段中:
|
||||
|
||||
```js
|
||||
fNOP.prototype.isPrototypeOf(this) ? this : otherThis,
|
||||
// 以及
|
||||
if (this.prototype) {
|
||||
// Function.prototype doesn't have a prototype property
|
||||
fNOP.prototype = this.prototype;
|
||||
}
|
||||
fBound.prototype = new fNOP();
|
||||
```
|
||||
|
||||
该 polyfill 检测了是否是使用 new 绑定,并修改 this 为 new 绑定。
|
||||
|
||||
#### 判断 this
|
||||
|
||||
根据上述优先级,可以得出一些判断 this 的结论(优先级从高到低):
|
||||
|
||||
1. 函数是否在 new 中调用(new 绑定)?
|
||||
|
||||
如果是的话, this 绑定的是新创建的对象。`const bar = new Foo()`
|
||||
|
||||
2. 函数是否通过`call/apply`或者硬绑定调用(显式绑定)?
|
||||
|
||||
如果是的话,this 绑定的是指定的对象。`const bar = foo.call(baz)`
|
||||
|
||||
3. 函数是否在某个上下文对象中调用(隐式绑定)?
|
||||
|
||||
如果是的话,this 绑定在那个上下文对象上。`const bar = obj.foo()`
|
||||
|
||||
4. 上述都不满足,那么就会使用默认绑定。
|
||||
|
||||
### 绑定例外
|
||||
|
||||
凡事都有例外,this 绑定也是同样。在某些情况下看上去可能是绑定某个规则,但实际上应用的可能是默认规则。
|
||||
|
||||
#### 被忽略的 this
|
||||
|
||||
把 null 或者 undefined 作为 this 的绑定对象传入`call/apply`与 bind 方法时,这些值会被忽略,从而应用默认绑定规则。
|
||||
|
||||
也就是说`call/apply`传入 null 或者 undefined 时与之间执行函数本身没有区别。
|
||||
|
||||
```js
|
||||
function foo() {
|
||||
console.log(this.name);
|
||||
}
|
||||
foo.call(null);
|
||||
```
|
||||
|
||||
这样使用`call/apply`的作用是利用他们的一些特性来解决一些小问题。
|
||||
|
||||
例如:展开数组
|
||||
|
||||
```js
|
||||
function bar(a, b) {
|
||||
console.log(a, b);
|
||||
}
|
||||
bar.apply(null, [1, 2]);
|
||||
```
|
||||
|
||||
当然,这在 ES6 中可以使用展开运算符来传递参数:
|
||||
|
||||
```js
|
||||
bar(...[1, 2]);
|
||||
```
|
||||
|
||||
又或是利用 bind 实现柯里化
|
||||
|
||||
```js
|
||||
function bar(a, b) {
|
||||
console.log(a, b);
|
||||
}
|
||||
|
||||
const baz = bar.bind(null, 1);
|
||||
baz(2);
|
||||
```
|
||||
|
||||
这里都是利用忽略 this 产生的一些副作用,但在某些情况下可能不安全,例如函数可能真的使用到了 this ,这在非严格模式下可能会修改全局对象。
|
||||
|
||||
如果真的需要使用这种方法,可以创建一个 DMZ 对象来代替 null。
|
||||
|
||||
```js
|
||||
const ¤ = Object.create(null);
|
||||
foo.call(¤, arg)
|
||||
```
|
||||
|
||||
#### 间接引用
|
||||
|
||||
另外需要注意的是,在某些情况下我们可能会无意的创建一个函数的间接引用。间接引用最容易在赋值期间发生:
|
||||
|
||||
```js
|
||||
function foo() {
|
||||
console.log(this.name);
|
||||
}
|
||||
const o = {
|
||||
foo,
|
||||
};
|
||||
const p = {};
|
||||
(p.foo = o.foo)();
|
||||
```
|
||||
|
||||
赋值表达式`p.foo = o.foo`返回的是目标函数的引用,所以在这里调用实际上是在全局环境下直接调用`foo()`。根据之前的规则,这里会应用默认绑定。
|
399
content/posts/build-own-store-with-usesyncexternalstore.mdx
Normal file
399
content/posts/build-own-store-with-usesyncexternalstore.mdx
Normal file
@ -0,0 +1,399 @@
|
||||
---
|
||||
title: 使用 useSyncExternalStore 构建属于自己的全局状态
|
||||
date: '2022-10-14'
|
||||
tags: [TypeScript, React]
|
||||
---
|
||||
|
||||
在 [React18 new hooks](https://rua.plus/p/react18-new-hooks) 中简单了解过并作过一个简单的全局状态,它的参数只有三个:
|
||||
|
||||
- `subscribe`: 一个当状态更新后可执行的回调函数。该函数会收到一个回调函数,这个回调函数就是当状态后执行,React 用来对比是否需要重新渲染组件。
|
||||
- `getSnapshot`: 返回当前状态的函数。
|
||||
- `getServerSnapshot`: 在服务端渲染时返回当前状态的函数,可选。
|
||||
|
||||
配合上 `useSyncExternalStore` 就可以非常方便的对接外部状态,了解了这个 hook 之后,对自定义的全局状态也有更深的了解。
|
||||
|
||||
简单来说,JavaScript 的变量是没有任何相应式的。也就是说,通常情况下,当我们修改了一个变量,是无法被动的感知到它的变化的。但是也是有办法去实现这一功能的,例如 Vue 2 使用 `Object.defineProperty()` 和 Vue 3 使用的 `Proxy` 来实现的相应式。
|
||||
|
||||
而 React 走了另一条道路,数据不可变。它通过 `setState` 来感知状态的变化,再利用 diff 等方法实现更新。这也就是为什么我们可以利用 `setState({})` 可以强制更新组件。
|
||||
|
||||
正因如此,配合上 `useSyncExternalStore` 我们的外部状态也就可以是一个普通的变量(PlainObject)。在我们更新我们的状态时,利用 `subscribe` 参数接受到的回调(listener)来通知组件状态更新了。最后在使用 `getSnapshot` 来返回新的状态,这就是 `useSyncExternalStore` 大致的工作流程。
|
||||
|
||||
<Image
|
||||
src="/images/p/build-own-store-with-usesyncexternalstore/GlobalStore.png"
|
||||
alt="Global Store"
|
||||
width="1280"
|
||||
height="866"
|
||||
/>
|
||||
|
||||
## 实现一个全局状态
|
||||
|
||||
先来一个最简单的,利用 `useSyncExternalStore` 实现一个全局状态。首先我们需要创建一个普通对象,它主要用于存储状态,并配合 `subscribe` 和 `getSnapshot` 等方法来实现状态的更新。
|
||||
|
||||
```tsx
|
||||
const store: Store = {
|
||||
// 全局状态
|
||||
state: {
|
||||
count: 0,
|
||||
info: 'Hello',
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置新的状态
|
||||
* @param stateOrFn 新状态或设置状态的回调函数
|
||||
*/
|
||||
setState(stateOrFn) {
|
||||
const newState =
|
||||
typeof stateOrFn === 'function' ? stateOrFn(store.state) : stateOrFn;
|
||||
store.state = {
|
||||
...store.state,
|
||||
...newState,
|
||||
};
|
||||
store.listeners.forEach((listener) => listener());
|
||||
},
|
||||
|
||||
/**
|
||||
* 保存 useSyncExternalStore 回调的 listener
|
||||
* 在 setState 中设置过状态后会进行调用
|
||||
*/
|
||||
listeners: new Set(),
|
||||
|
||||
/**
|
||||
* 传递给 useSyncExternalStore 的 subscriber
|
||||
* 负责收集 listener 并返回注销 listener 的方法
|
||||
* @param listener
|
||||
* @returns
|
||||
*/
|
||||
subscribe(listener) {
|
||||
store.listeners.add(listener);
|
||||
return () => {
|
||||
store.listeners.delete(listener);
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 返回当前的状态
|
||||
* @returns
|
||||
*/
|
||||
getSnapshot() {
|
||||
return store.state;
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
在我们的 store 中:
|
||||
|
||||
- `state` :一个普通的对象,它就是我们用于存储全局状态的地方。
|
||||
- `setState` :提供一个类似于 `useState` 的设置状态的方法。
|
||||
- `listeners` :保存 `useSyncExternalStore` 回调的 listener。
|
||||
- `subscribe` :传递给 `useSyncExternalStore` 的 subscriber。
|
||||
- `getSnapshot` :返回当前的状态。
|
||||
|
||||
需要注意的是,`useSyncExternalStore` 会立即调用 `subscribe` 和 `getSnapshot` ,这就导致了我们不能在这些方法中使用 `[this.store](http://this.store)` ,此时的 `this` 还未准备好。
|
||||
|
||||
最后的签名也是比较重要的,`setState` 就参照 `useState` 。接受完整的 state 为参数,并将其设置到我们的状态中。
|
||||
|
||||
而 `useSyncExternalStore` 给我们的 listener 签名就简单多了 `() => void` 。
|
||||
|
||||
```tsx
|
||||
export type State = {
|
||||
count: number;
|
||||
info: string;
|
||||
};
|
||||
export type Store = {
|
||||
state: State;
|
||||
setState: (stateOrFn: State | ((state: State) => State)) => void;
|
||||
subscribe: (listener: () => void) => () => void;
|
||||
listeners: Set<() => void>;
|
||||
getSnapshot: () => State;
|
||||
};
|
||||
```
|
||||
|
||||
### 使用全局状态
|
||||
|
||||
到目前为止还只是创建了一个用于存储和更新状态的对象,在使用上我们直接在组件中配合 `useSyncExternalStore` 来创建我们的状态。这一步就非常的简单,后续的使用就和在使用其他的状态一样。
|
||||
|
||||
```tsx
|
||||
const Couter = () => {
|
||||
const { count, info } = useSyncExternalStore(
|
||||
store.subscribe,
|
||||
store.getSnapshot
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div>
|
||||
Count: <span>{count}</span>
|
||||
</div>
|
||||
<div>
|
||||
Info: <span>{info}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => store.setState((d) => ({ count: d.count + 1 }))}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
<RUASandpack
|
||||
template="react-ts"
|
||||
files={{
|
||||
'/Input.tsx': {
|
||||
code: sandpack.common.Input,
|
||||
hidden: true,
|
||||
},
|
||||
'/Button.tsx': {
|
||||
code: sandpack.common.Button,
|
||||
hidden: true,
|
||||
},
|
||||
'/store.ts': sandpack['react18-new-hooks'].store,
|
||||
'/App.tsx': sandpack['react18-new-hooks'].useSyncExternalStore,
|
||||
}}
|
||||
/>
|
||||
|
||||
## 多个 Store
|
||||
|
||||
上面的实现是直接针对单一的 store 来实现的,直接将 state 和其方法封装在一个对象中。日常的项目中通常会根据功能来创建多个全局状态,避免混乱。
|
||||
|
||||
为了避免每创建一个 store 都要重新写一次同样的方法,我们可以将其封装为一个创建 store 的函数。整体实现都还是一样的,只不过后续我们可以利用这个方法来创建多个 store。
|
||||
|
||||
```tsx
|
||||
export const createStore: CreateStore = <
|
||||
T extends Record<string, unknown> | unknown[]
|
||||
>(
|
||||
initialState: T
|
||||
) => {
|
||||
let state = initialState;
|
||||
|
||||
const listeners = new Set<() => void>();
|
||||
const getSnapshot = () => state;
|
||||
const setState: SetState<T> = (stateOrFn) => {
|
||||
state = typeof stateOrFn === 'function' ? stateOrFn(state) : stateOrFn;
|
||||
listeners.forEach((listener) => listener());
|
||||
};
|
||||
const subscribe = (listener: () => void) => {
|
||||
listeners.add(listener);
|
||||
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
getSnapshot,
|
||||
setState,
|
||||
subscribe,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
对于其方法的签名还是和之前一样,只不过函数还是需要显式注解,因为在创建时我们还需要访问其范型 `T` 。
|
||||
|
||||
```tsx
|
||||
export type SetState<S> = (stateOrFn: S | ((state: S) => S)) => void;
|
||||
export type GetSnapshot<S> = () => S;
|
||||
export type Subscribe = (listener: () => void) => () => void;
|
||||
export type CreateStore = <T extends Record<string, unknown> | unknown[]>(
|
||||
initialState: T
|
||||
) => {
|
||||
getSnapshot: GetSnapshot<T>;
|
||||
setState: SetState<T>;
|
||||
subscribe: Subscribe;
|
||||
};
|
||||
```
|
||||
|
||||
将对应需要的方法在函数中创建,并返回为一个对象。但这里没有将 state 直接返回出去,和上述不同,我们将不再直接访问原始 state,而是配合 `useSyncExternalStore` 封装一个自定义 hook 来返回我们的全局状态。
|
||||
|
||||
```tsx
|
||||
export type Todo = {
|
||||
id: number;
|
||||
content: string;
|
||||
}[];
|
||||
const initialTodo: Todo = [
|
||||
{ id: 0, content: 'React' },
|
||||
{ id: 1, content: 'Vue' },
|
||||
];
|
||||
const todoStore = createStore(initialTodo);
|
||||
export const useTodoStore = (): [Todo, SetState<Todo>] => [
|
||||
useSyncExternalStore(todoStore.subscribe, todoStore.getSnapshot),
|
||||
todoStore.setState,
|
||||
];
|
||||
```
|
||||
|
||||
这里的 `useTodoStore` 将其显式的注解了其返回值,因为返回的是和 `useState` 类似的元组,而默认 TypeScript 推断的类型比较宽松,会推断为 `(Todo | SetState<Todo>)[]` 的数组。
|
||||
|
||||
由于封装了新的 hook,在组件中的使用也就更方便了。在需要不同 store 的组件中直接使用不同的 hook 就能访问到对应的全局状态了。
|
||||
|
||||
```tsx
|
||||
const Todo = () => {
|
||||
const [todos, setTodo] = useTodoStore();
|
||||
const [value, setValue] = useState('');
|
||||
};
|
||||
|
||||
const Count = () => {
|
||||
const [{ count, info }, setState] = useCountStore();
|
||||
};
|
||||
```
|
||||
|
||||
<RUASandpack
|
||||
template="react-ts"
|
||||
files={{
|
||||
'/Input.tsx': {
|
||||
code: sandpack.common.Input,
|
||||
hidden: true,
|
||||
},
|
||||
'/Button.tsx': {
|
||||
code: sandpack.common.Button,
|
||||
hidden: true,
|
||||
},
|
||||
'/multi.ts': sandpack['build-own-store-with-usesyncexternalstore'].multi,
|
||||
'/App.tsx':
|
||||
sandpack['build-own-store-with-usesyncexternalstore'].MultiStore,
|
||||
}}
|
||||
/>
|
||||
|
||||
## Mini Redux
|
||||
|
||||
在模仿 Reudx 之前,应该再熟悉一下 Redux 的工作流程。在 Redux 中,和我们上述的状态一样,状态都是普通对象,且是不可变的数据(只读)。此外,我们的 reducer 也应该保持是纯函数。
|
||||
|
||||
Redux 通过我们创建的 Action 来决定如何更新状态,再通过 reducer 来实际更新状态。reducer 更新状态也非常简单,和 React 的状态类似,我们的状态也是保持不可变的。所以 reducer 会返回整个状态。
|
||||
|
||||
也就是类似于:
|
||||
|
||||
```tsx
|
||||
export type RUAReducer<S extends RUAState, A extends RUAAction> = (
|
||||
state: S,
|
||||
action: A
|
||||
) => S;
|
||||
```
|
||||
|
||||
<Image
|
||||
src="/images/p/build-own-store-with-usesyncexternalstore/ReduxDataFlowDiagram.gif"
|
||||
alt="Redux Data Flow Diagram"
|
||||
width="1440"
|
||||
height="1080"
|
||||
/>
|
||||
|
||||
上述的 `setState` 方法也需要简单调整一下,由于 reducer 是返回整个状态,所以可以直接将返回的新状态赋值给全局状态。
|
||||
|
||||
```tsx
|
||||
const dispatch: RUADispatch<A> = (action) => {
|
||||
state = reducer(state, action);
|
||||
listeners.forEach((listener) => listener());
|
||||
};
|
||||
```
|
||||
|
||||
除此之外,其他配合 `useSyncExternalStore` 的用法没有多大变化。
|
||||
|
||||
```tsx
|
||||
export const createStore = <S extends RUAState, A extends RUAAction>(
|
||||
reducer: RUAReducer<S, A>,
|
||||
initialState: S
|
||||
) => {
|
||||
let state = initialState;
|
||||
|
||||
const listeners = new Set<() => void>();
|
||||
const getSnapshot = () => state;
|
||||
const dispatch: RUADispatch<A> = (action) => {
|
||||
state = reducer(state, action);
|
||||
listeners.forEach((listener) => listener());
|
||||
};
|
||||
const subscribe = (listener: () => void) => {
|
||||
listeners.add(listener);
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
getSnapshot,
|
||||
dispatch,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
在 reducer 方面,也没有什么黑魔法,设置状态后将其返回即可。剩下的就交给 React 了。
|
||||
|
||||
```tsx
|
||||
const reducer: RUAReducer<Todo, TodoAction> = (state, action) => {
|
||||
switch (action.type) {
|
||||
case 'add': {
|
||||
if (action.payload == null) throw new Error('Add todo without payload!');
|
||||
return [
|
||||
...state,
|
||||
{
|
||||
id: state[state.length - 1].id + 1,
|
||||
content: action.payload.toString(),
|
||||
},
|
||||
];
|
||||
}
|
||||
case 'delete': {
|
||||
if (action.payload == null)
|
||||
throw new Error('Delete todo without payload!');
|
||||
return state.filter((todo) => todo.id !== action.payload);
|
||||
}
|
||||
default:
|
||||
throw new Error('Dispatch a reducer without action!');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
签名方面,主要针对 action 做了一些调整,以确保创建 reducer 和 dispatch 时 action 的类型正确。
|
||||
|
||||
```tsx
|
||||
export type RUAState = Record<string, unknown> | unknown[];
|
||||
export type RUAAction<P = unknown, T extends string = string> = {
|
||||
payload: P;
|
||||
type: T;
|
||||
};
|
||||
export type RUAReducer<S extends RUAState, A extends RUAAction> = (
|
||||
state: S,
|
||||
action: A
|
||||
) => S;
|
||||
export type RUADispatch<A extends RUAAction> = (action: A) => void;
|
||||
export type GetSnapshot<S> = () => S;
|
||||
export type Subscribe = (listener: () => void) => () => void;
|
||||
```
|
||||
|
||||
在封装 hook 上,和上述没有多少区别:
|
||||
|
||||
```tsx
|
||||
const todoStore = createStore(reducer, initialTodo);
|
||||
export const useTodoStore = (): [Todo, RUADispatch<TodoAction>] => [
|
||||
useSyncExternalStore(todoStore.subscribe, todoStore.getSnapshot),
|
||||
todoStore.dispatch,
|
||||
];
|
||||
```
|
||||
|
||||
<RUASandpack
|
||||
template="react-ts"
|
||||
files={{
|
||||
'/Input.tsx': {
|
||||
code: sandpack.common.Input,
|
||||
hidden: true,
|
||||
},
|
||||
'/Button.tsx': {
|
||||
code: sandpack.common.Button,
|
||||
hidden: true,
|
||||
},
|
||||
'/store.ts':
|
||||
sandpack['build-own-store-with-usesyncexternalstore'].miniRedux,
|
||||
'/App.tsx': sandpack['build-own-store-with-usesyncexternalstore'].Reducer,
|
||||
}}
|
||||
/>
|
||||
|
||||
只是在 Redux 的三个原则中:
|
||||
|
||||
- Single source of truth
|
||||
- State is read-only
|
||||
- Changes are made with pure functions
|
||||
|
||||
其中的 Single source of truth 没有完全遵守,我们的全局状态可以使用 `createStore` 来创建多个 source,且多个 source 是完全分离的,无法一起访问到。
|
589
content/posts/built-simply-faas.mdx
Normal file
589
content/posts/built-simply-faas.mdx
Normal file
@ -0,0 +1,589 @@
|
||||
---
|
||||
title: 自建简易 FaaS
|
||||
date: '2022-10-10'
|
||||
tags: [JavaScript, TypeScript]
|
||||
---
|
||||
|
||||
近些年来,传统的 IaaS、PaaS 已经无法满足人们对资源调度的需求了。各大云厂商相继开始推出自家的 Serverless 服务。Serverless 顾名思义,它是“无服务器”服务器。不过并不是本质上的不需要服务器,而是面向开发者(客户)无需关心底层服务器资源的调度。只需要利用本身业务代码即可完成服务的运行。
|
||||
|
||||
Serverless 是近些年的一个发展趋势,它的发展离不开 FaaS 与 BaaS。这里不是着重讨论 Serverless 架构的,而是尝试利用 Node.js 来实现一个最简易的 FaaS 平台。顺便还能对 JavaScript 语言本身做进一步更深的研究。
|
||||
|
||||
Serverless 平台是基于函数作为运行单位的,在不同的函数被调用时,为了确保各个函数的安全性,同时避免它们之间的互相干扰,平台需要具有良好的隔离性。这种隔离技术通常被称之为“沙箱”(Sandbox)。在 FaaS 服务器中,最普遍的隔离应该式基于 Docker 技术实现的容器级别隔离。它不同于传统虚拟机的完整虚拟化操作系统,而且也实现了安全性以及对系统资源的隔离。
|
||||
|
||||
但在这我们尝试实现一个最简易的 FaaS 服务,不需要利用上 Docker。基于进程的隔离会更加的轻便、灵活,虽然与容器的隔离性有一定差距。
|
||||
|
||||
## 环境搭建
|
||||
|
||||
这里利用 TypeScript 来对 JavaScript 做更严格的类型检查,并使用 ESlint + Prettier 等工具规范代码。
|
||||
|
||||
初始化环境:
|
||||
|
||||
```bash
|
||||
yarn --init
|
||||
```
|
||||
|
||||
添加一些开发必要工具:
|
||||
|
||||
```bash
|
||||
yarn add typescript ts-node nodemon -D
|
||||
```
|
||||
|
||||
以及对代码的规范:
|
||||
|
||||
```js
|
||||
yarn add eslint prettier eslint-plugin-prettier eslint-config-prettier @typescript-eslint/parser @typescript-eslint/eslint-plugin -D
|
||||
```
|
||||
|
||||
当然不能忘了 Node 本身的 TypeScript lib。
|
||||
|
||||
```bash
|
||||
yarn add @types/node -D
|
||||
```
|
||||
|
||||
## 基础能力
|
||||
|
||||
在 [Nodejs 多进程 | 🍭Defectink](https://www.defectink.com/defect/nodejs-multi-process.html) 一篇中,我们大概的探讨了进程的使用。这里也是类似。在进程创建时,操作系统将给该进程分配对应的虚拟地址,再将虚拟地址映射到真正的物理地址上。因此,进程无法感知真实的物理地址,只能访问自身的虚拟地址。这样一来,就可以防止两个进程互相修改数据。
|
||||
|
||||
所以,我们基于进程的隔离,就是让不同的函数运行再不同的进程中,从而保障各个函数的安全性和隔离性。具体的流程是:我们的主进程(master)来监听函数的调用请求,当请求被触发时,再启动子进程(child)执行函数,并将执行后的结果通过进程间的通信发送给主进程,最终返回到客户端中。
|
||||
|
||||
### 基于进程隔离
|
||||
|
||||
`chlid_process`是 Node.js 中创建子进程的一个函数,它有多个方法,包括 exec、execFile 和 fork。实际上底层都是通过 spawn 来实现的。这里我们使用 fork 来创建子进程,创建完成后,fork 会在子进程与主进程之间建立一个通信管道,来实现进程间的通信(IPC,Inter-Process Communication)。
|
||||
|
||||
其函数签名为:`child_process.fork(modulePath[, args][, options])`。
|
||||
|
||||
这里利用`child.process.fork`创建一个子进程,并利用`child.on`来监听 IPC 消息。
|
||||
|
||||
```ts
|
||||
// master.ts
|
||||
import child_process from 'child_process';
|
||||
|
||||
const child = child_process.fork('./dist/child.js');
|
||||
|
||||
// Use child.on listen a message
|
||||
child.on('message', (message: string) => {
|
||||
console.log('MASTER get message:', message);
|
||||
});
|
||||
```
|
||||
|
||||
在 Node.js 中,process 对象是一个内置模块。在每个进程启动后,它都可以获取当前进程信息以及对当前进程进行一些操作。例如,发送一条消息给主进程。
|
||||
|
||||
子进程则利用 process 模块来和主进程进行通信
|
||||
|
||||
```ts
|
||||
// child.ts
|
||||
import process from 'process';
|
||||
|
||||
process.send?.('this is a message from child process');
|
||||
```
|
||||
|
||||
执行这段方法后,master 就会创建一个子进程,并接收到其发来的消息。
|
||||
|
||||
```bash
|
||||
$ node master.js
|
||||
MASTER get message: this is a message from child process
|
||||
```
|
||||
|
||||
<Image
|
||||
src={'/images/p/built-simply-faas/主进程与子进程间的通信.svg'}
|
||||
alt="主进程与子进程间的通信"
|
||||
width="307"
|
||||
height="87"
|
||||
/>
|
||||
|
||||
到此,我们就实现了主进程与子进程之间的互相通信。但是需要执行的函数通常来自于外部,所以我们需要从外部手动加载代码,再将代码放到子进程中执行,之后将执行完的结果再发送回主进程,最终返回给调用者。
|
||||
|
||||
我们可以再创建一个`func.js`来保存用户的代码片段,同时在主进程中读取这段代码,发送给子进程。而子进程中需要动态执行代码的能力。什么方式能在 JavaScript 中动态的执行一段代码呢?
|
||||
|
||||
### Devil waiting outside your floor
|
||||
|
||||
没错,这里要用到万恶的 evil。在 JavaScript 中动态的加载代码,eval 函数是最简单方便,同时也是最危险和性能最低下的方式。以至于现代浏览器都不愿意让我们使用
|
||||
|
||||
```js
|
||||
console.log(eval('2 + 2'));
|
||||
|
||||
// VM122:1 Uncaught EvalError: Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'self' blob: filesystem:".
|
||||
```
|
||||
|
||||
执行来自用户的函数与普通函数略有一点区别,它与普通的函数不同,它需要利用 IPC 来返回值,而普通函数则之间 return 即可。我们不应该向用户暴露过度的内部细节,所以,用户的函数可以让他长这样:
|
||||
|
||||
```js
|
||||
// func.js
|
||||
|
||||
(event, context) => {
|
||||
return { message: 'it works!', status: 'ok ' };
|
||||
};
|
||||
```
|
||||
|
||||
eval 函数不仅可以执行一行代码片段,它还可以执行一个函数。在拿到用户的匿名函数后,我们可以将其包装成一个立即执行函数(IIFE)的字符串,然后交给 eval 函数进行执行。
|
||||
|
||||
```js
|
||||
const fn = `() => (2 + 2)`;
|
||||
const fnIIFE = `(${fn})()`;
|
||||
console.log(eval(fnIIFE));
|
||||
```
|
||||
|
||||
> 不用担心,evil 会离我们而去的。
|
||||
|
||||
这里我们使用主进程读取用户函数,并使用 IPC 发送给子进程;子进程利用 eval 函数来执行,随后再利用 IPC 将其结果返回给主进程。
|
||||
|
||||
```ts
|
||||
// master.ts
|
||||
import child_process from 'child_process';
|
||||
import fs from 'fs';
|
||||
|
||||
const child = child_process.fork('./dist/child.js');
|
||||
|
||||
// Use child.on listen a message
|
||||
child.on('message', (message: unknown) => {
|
||||
console.log('Function result:', message);
|
||||
});
|
||||
|
||||
// Read the function from user
|
||||
const fn = fs.readFileSync('./src/func.js', { encoding: 'utf-8' });
|
||||
// Sent to child process
|
||||
child.send({
|
||||
action: 'run',
|
||||
fn,
|
||||
});
|
||||
```
|
||||
|
||||
```ts
|
||||
// child.ts
|
||||
import process from 'process';
|
||||
|
||||
type fnData = {
|
||||
action: 'run';
|
||||
fn: () => unknown;
|
||||
};
|
||||
|
||||
// Listen function form master process
|
||||
process.on('message', (data: fnData) => {
|
||||
// Convert user function to IIFE
|
||||
const fnIIFE = `(${data.fn})()`;
|
||||
const result = eval(fnIIFE);
|
||||
// Sent result to master process
|
||||
process.send?.({ result });
|
||||
process.exit();
|
||||
});
|
||||
```
|
||||
|
||||
<Image
|
||||
src="/images/p/built-simply-faas/通过子进程动态运行函数代码片段.svg"
|
||||
alt="主进程与子进程间的通信"
|
||||
width="312"
|
||||
height="232"
|
||||
/>
|
||||
|
||||
### Devil crawling along your floor
|
||||
|
||||
前面我们利用 eval 函数获得了执行动态代码的能力,但与 Devil 做交易是需要付出代价的。很明显,我们付出了不小的安全性以及性能的代价。
|
||||
|
||||
甚至于用户代码能够直接修改 process,导致子进程无法退出等问题:
|
||||
|
||||
```js
|
||||
(event, context) => {
|
||||
process.exit = () => {
|
||||
console.log('process NOT exit!');
|
||||
};
|
||||
return { message: 'function is running.', status: 'ok' };
|
||||
};
|
||||
```
|
||||
|
||||
eval 函数能够访问全局变量的原因在于,它们由同一个执行其上下文创建。如果能让函数代码在单独的上下文中执行,那么就应该能够避免污染全局变量了。
|
||||
|
||||
所以我们得换一个 Devil 做交易。在 Node.js 内置模块中,由一个名为 vm 的模块。从名字就可以得出,它是一个用于创建基于上下文的沙箱机制,可以创建一个与当前进程无关的上下文环境。
|
||||
|
||||
具体方式是,将沙箱内需要使用的外部变量通过`vm.createContext(sandbox)`包装,这样我们就能得到一个 contextify 化的 sandbox 对象,让函数片段在新的上下文中访问。然后,可执行对象的代码片段。在此处执行的代码的上下文与当前进程的上下文是互相隔离的,在其中对全局变量的任何修改,都不会反映到进程中。提高了函数运行环境的安全性。
|
||||
|
||||
```js
|
||||
const vm = require('vm');
|
||||
|
||||
const x = 1;
|
||||
|
||||
const context = { x: 2 };
|
||||
vm.createContext(context); // Contextify the object.
|
||||
|
||||
const code = 'x += 40; var y = 17;';
|
||||
// `x` and `y` are global variables in the context.
|
||||
// Initially, x has the value 2 because that is the value of context.x.
|
||||
vm.runInContext(code, context);
|
||||
```
|
||||
|
||||
在我们的 FaaS 中,我们无须在外层访问新的上下文对象,只需要执行一段函数即可。因此可以通过`vm.runInNewContext(code)`方法来快速创建一个无参数的新上下文,更快速创建新的 sandbox。
|
||||
|
||||
我们只需要替换到 eval 函数即可:
|
||||
|
||||
```ts
|
||||
// child.ts
|
||||
import process from 'process';
|
||||
import vm from 'vm';
|
||||
|
||||
type fnData = {
|
||||
action: 'run';
|
||||
fn: () => unknown;
|
||||
};
|
||||
|
||||
// Listen function form master process
|
||||
process.on('message', (data: fnData) => {
|
||||
// Convert user function to IIFE
|
||||
const fnIIFE = `(${data.fn})()`;
|
||||
const result = vm.runInNewContext(fnIIFE);
|
||||
// Sent result to master process
|
||||
process.send?.({ result });
|
||||
process.exit();
|
||||
});
|
||||
```
|
||||
|
||||
现在,我们实现了将函数隔离在沙箱中执行,流程如图:
|
||||
|
||||
<Image
|
||||
src="/images/p/built-simply-faas/在隔离的沙箱中执行函数.svg"
|
||||
alt="在隔离的沙箱中执行函数"
|
||||
width="307"
|
||||
height="232"
|
||||
/>
|
||||
|
||||
但 vm 真的安全到可以随意执行来自用户的不信任代码吗?虽然相对于 eval 函数来,它隔离了上下文,提供了更加封闭的环境,但它也不是绝对安全的。
|
||||
|
||||
根据 JavaScript 对象的实现机制,所有对象都是有原型链的(类似`Object.crate(null)`除外)。因此 vm 创建的上下文中的 this 就指向是当前的 Context 对象。而 Context 对象是通过主进程创建的,其构造函数指向主进程的 Object。这样一来,通过原型链,用户代码就可以顺着原型链“爬”出沙箱:
|
||||
|
||||
```js
|
||||
import vm from 'vm';
|
||||
(event, context) => {
|
||||
vm.runInNewContext('this.constructor.constructor("return process")().exit()');
|
||||
return { message: 'function is running.', status: 'ok' };
|
||||
};
|
||||
```
|
||||
|
||||
这种情况就会导致非信任的代码调用主程序的`process.exit`方法,从而让整个程序退出。
|
||||
|
||||
也许我们可以切断上下文的原型链,利用`Object.create(null)`来为沙箱创建一个上下文。与任何 Devil 做交易都是需要付出代价的:
|
||||
|
||||
> **The `vm` module is not a security mechanism. Do not use it to run untrusted code**.
|
||||
|
||||
### Devil lying by your side
|
||||
|
||||
好在开源社区有人尝试解决这个问题,其中一个方案就是 vm2 模块。vm2 模块是利用 Proxy 特性来对内部变量进行封装的。这使得隔离的沙箱环境可以运行不受信任的代码。
|
||||
|
||||
当然,我们需要手动添加一下依赖:
|
||||
|
||||
```bash
|
||||
yarn add vm2
|
||||
```
|
||||
|
||||
另一个值得庆幸的是,代码改动也很小。我们只需要对`child.ts`简单修改即可:
|
||||
|
||||
```ts
|
||||
import process from 'process';
|
||||
import { VM } from 'vm2';
|
||||
|
||||
type fnData = {
|
||||
action: 'run';
|
||||
fn: () => unknown;
|
||||
};
|
||||
|
||||
// Listen function form master process
|
||||
process.on('message', (data: fnData) => {
|
||||
// Convert user function to IIFE
|
||||
const fnIIFE = `(${data.fn})()`;
|
||||
const result = new VM().run(fnIIFE);
|
||||
// Sent result to master process
|
||||
process.send?.({ result });
|
||||
process.exit();
|
||||
});
|
||||
```
|
||||
|
||||
## HTTP 服务
|
||||
|
||||
在实现了动态执行代码片段的能力后,为了让函数能够对外提供服务,我们还需要添加一个 HTTP API。这个 API 使得用户可以根据不同的请求路径来动态的执行对应的代码,并将其结果返回给客户端。
|
||||
|
||||
这里 HTTP 服务器选用的是 Koa。
|
||||
|
||||
```bash
|
||||
yarn add koa
|
||||
```
|
||||
|
||||
当然还要有其类型
|
||||
|
||||
```bash
|
||||
yarn add @types/koa -D
|
||||
```
|
||||
|
||||
为了响应 HTTP 请求并运行我们的函数,我们需要进一步的将运行子进行的方法封装为一个异步函数,并在接收到子进程的消息后,直接 resolve 给 Koa。
|
||||
|
||||
将前面的子进程的创建、监听以及读取文件都封装进一个函数:
|
||||
|
||||
```ts
|
||||
// master.ts
|
||||
import child_process from 'child_process';
|
||||
import fs from 'fs/promises';
|
||||
import Koa from 'koa';
|
||||
|
||||
const app = new Koa();
|
||||
|
||||
app.use(async (ctx) => {
|
||||
ctx.response.body = await run();
|
||||
});
|
||||
|
||||
const run = async () => {
|
||||
const child = child_process.fork('./dist/child.js');
|
||||
// Read the function from user
|
||||
const fn = await fs.readFile('./src/func.js', { encoding: 'utf-8' });
|
||||
// Sent to child process
|
||||
child.send({
|
||||
action: 'run',
|
||||
fn,
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// Use child.on listen a message
|
||||
child.on('message', resolve);
|
||||
});
|
||||
};
|
||||
|
||||
app.listen(3000);
|
||||
```
|
||||
|
||||
现在我们的流程如下:
|
||||
|
||||
<Image
|
||||
src="/images/p/built-simply-faas/用户通过HTTP触发函数的流程.svg"
|
||||
placeholder="用户通过HTTP触发函数的流程"
|
||||
priority
|
||||
width="307"
|
||||
height="346"
|
||||
/>
|
||||
|
||||
这样还不够,到目前为止,用户还只是请求的根路径,而我们响应的也只是同一个函数。因此我们还需要一个路由机制来支持不同的函数触发。
|
||||
|
||||
使用`ctx.request.path`就能获取到每次 GET 请求后的路径,所以这里也不用大费周章的去划分路由,直接把路径作为函数名,读取文件,执行即可。所以这里的改造就简单多了:
|
||||
|
||||
```ts
|
||||
// master.ts
|
||||
app.use(async (ctx) => {
|
||||
ctx.response.body = await run(ctx.request.path);
|
||||
});
|
||||
|
||||
const run = async (path: string) => {
|
||||
const child = child_process.fork('./dist/child.js');
|
||||
// Read the function from user
|
||||
const fn = await fs.readFile(`./src/func/${path}.js`, { encoding: 'utf-8' });
|
||||
// Sent to child process
|
||||
child.send({
|
||||
action: 'run',
|
||||
fn,
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// Use child.on listen a message
|
||||
child.on('message', resolve);
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
至此,我们就实现了一个最简单的进程隔离 FaaS 方案,并提供了动态加载函数文件且执行的能力。
|
||||
|
||||
但这还不是全部,还有很多方面的问题值得去优化。
|
||||
|
||||
## 进阶优化
|
||||
|
||||
FaaS 并不只是简单的拥有动态的执行函数的能力就可以了,面对我们的还有大量的待处理问题。
|
||||
|
||||
### 进程管理
|
||||
|
||||
上述的方案看上去已经很理想了,利用子进程和沙箱防止污染主进程。但还有个主要的问题,用户的每一个请求都会创建一个新的子进程,并在执行完后再销毁。对系统来说,创建和销毁进程是一个不小的开销,且请求过多时,过多的进程也可能导致系统崩溃。
|
||||
|
||||
所以最佳的办法是通过进程池来复用进程。如下图,进程池是一种可以复用进程的概念,通过事先初始化并维护一批进程,让这批进程运行相同的代码,等待着执行被分配的任务。执行完成后不会退出,而是继续等待新的任务。在调度时,通常还会通过某种算法来实现多个进程之间任务分配的负载均衡。
|
||||
|
||||
<Image
|
||||
src="/images/p/built-simply-faas/通过进程池管理子进程.svg"
|
||||
placeholder="通过进程池管理子进程"
|
||||
priority
|
||||
width="407"
|
||||
height="346"
|
||||
/>
|
||||
|
||||
早在 Node.js v0.8 中就引入了 cluster 模块。cluster 是对`child_process`模块的一层封装。通过它,我们可以创建共享服务器同一端口的子进程。
|
||||
|
||||
这时候我们就需要对`master.ts`进行大改造了。首先需要将`child_process`更换为 cluster 来管理进程,我们根创建 CPU 超线程数量一半的子进程。这是为了留下多余的超线程给系统已经 Node 的事件循环来工作。顺便在每个子进程中监听对应的 HTTP 端口来启动 HTTP 服务。
|
||||
|
||||
```ts
|
||||
// master.ts
|
||||
import cluster from 'cluster';
|
||||
import os from 'os';
|
||||
|
||||
const num = os.cpus().length;
|
||||
const CPUs = num > 2 ? num / 2 : num;
|
||||
|
||||
if (cluster.isMaster) {
|
||||
for (let i = 0; i < CPUs; i++) {
|
||||
cluster.fork();
|
||||
}
|
||||
} else {
|
||||
const app = new Koa();
|
||||
|
||||
app.use(async (ctx) => {
|
||||
ctx.response.body = await run(ctx.request.path);
|
||||
});
|
||||
|
||||
app.listen(3000);
|
||||
}
|
||||
```
|
||||
|
||||
这里看上去有点匪夷所思,我们都知道,在操作系统中,是不允许多个进程监听同一个端口的。我们的多个子进程看上去监听的都是同一个端口!
|
||||
|
||||
实际上,在 Node.js 的 net 模块中,当当前进程是 cluster 的子进程时,存在一个特殊的处理。
|
||||
|
||||
简单来说就是,当调用 listen 方法监听端口后,它会判断是否处于 cluster 的子进程下。如果是子进程,则会向主进程发送消息,告诉主进程需要监听的端口。当主进程收到消息后,会判断指定端口是否已经被监听,如果没有,则通过端口绑定实现监听。随后,再将子进程加入一个 worker 队列,表明该子进程可以处理来自该端口的请求。
|
||||
|
||||
这样一来,实际上监听的端口的依然是主进程,然后将请求分发给 worker 队列中子进程。分发算法采用了 Round Robin 算法,即轮流处理制。我们可以通过环境变量`NODE_CLUSTER_SCHED_POLICY`或通过配置`cluster.schedulingPolicy`来指定其他的负载均衡算法。
|
||||
|
||||
总的来说,虽然我们的代码看上去是由子进程来多次监听端口,但实际上是由我们的主进程来进行监听。然后就指定的任务分发给子进程进行处理。
|
||||
|
||||
回到我们的逻辑上,由于可以直接在当前代码中判断和创建进程,我们也就不再需要`child.ts`了。子进程也可以直接在作用域中执行 run 函数了。
|
||||
|
||||
所以我们将`master.ts`完整的改造一下,最终我们就实现了基于 cluster 的多进程管理方案:
|
||||
|
||||
```ts
|
||||
import cluster from 'cluster';
|
||||
import os from 'os';
|
||||
import fs from 'fs/promises';
|
||||
import Koa from 'koa';
|
||||
import { VM } from 'vm2';
|
||||
|
||||
const num = os.cpus().length;
|
||||
const CPUs = num > 1 ? Math.floor(num / 2) : num;
|
||||
|
||||
const run = async (path: string) => {
|
||||
try {
|
||||
// Read the function from user
|
||||
const fn = await fs.readFile(`./src/func/${path}.js`, {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
// Use arrow function to handle semicolon
|
||||
const fnIIFE = `const func = ${fn}`;
|
||||
return new VM().run(`${fnIIFE} func()`);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return 'Not Found Function';
|
||||
}
|
||||
};
|
||||
|
||||
if (cluster.isMaster) {
|
||||
for (let i = 0; i < CPUs; i++) {
|
||||
cluster.fork();
|
||||
}
|
||||
} else {
|
||||
const app = new Koa();
|
||||
|
||||
app.use(async (ctx) => {
|
||||
ctx.response.body = await run(ctx.request.path);
|
||||
});
|
||||
|
||||
app.listen(3000);
|
||||
}
|
||||
```
|
||||
|
||||
### 限制函数执行时间
|
||||
|
||||
上述,我们利用多进程方案来提高整体的安全性。但是,目前还没有考虑死循环的情况。当用户编写了一个这样的函数时:
|
||||
|
||||
```js
|
||||
const loop = (event, context) => {
|
||||
while (1) {}
|
||||
return { message: 'this is function2!!!', status: 'ok ' };
|
||||
};
|
||||
```
|
||||
|
||||
我们的进程会一直为其计算下去,无法正常退出,导致资源被占用。所以我们理想的情况下就是在沙箱外限制没个函数的执行时长,当超过限定时间时,之间结束该函数。
|
||||
|
||||
好在,vm 模块赋予了我们这一强大的功能:
|
||||
|
||||
```js
|
||||
vm.runInNewContext({
|
||||
'loop()',
|
||||
{ loop, console },
|
||||
{ timeout: 5000 }
|
||||
})
|
||||
```
|
||||
|
||||
通过 timeout 参数,我们为函数的执行时间限制在 5000ms 内。当死循环的函数执行超 5s 后,随后会得到一个函数执行超时的错误信息。
|
||||
|
||||
由于 vm2 也是基于 vm 进行封装的,因此我们可以在 vm2 中使用和 vm 相同的能力。只需要小小的改动就可以实现限制函数执行时长能力:
|
||||
|
||||
```js
|
||||
return new VM({ timeout: 5000 }).run(`${fnIIFE} func()`);
|
||||
```
|
||||
|
||||
看上去不错!但 Devil 不会就这么轻易放过我们的。JavaScript 本身是单线程的语言,它通过出色的异步循环来解决同步阻塞的问题。异步能解决很多问题,但同时也能带来问题。事件循环机制目前管理着两个任务队列:事件循环队列(或者叫宏任务)与任务队列(常见的微任务)。
|
||||
|
||||
我们可以把每次的事件循环队列内的每次任务执行看作一个 tick,而任务队列就是挂在每个 tick 之后运行的。也就是说微任务只要一直在运行,或者一直在添加,那么就永远进入不到下一次 tick 了。这和同步下死循环问题一样!
|
||||
|
||||
事件循环通常包含:setTimout、setInterval 和 I/O 操作等,而任务队列通常为:`process.nextTick`、Promise、MutationObserver 等。
|
||||
|
||||
VM2 也有类似 VM 的 timeout 设置,但是同样的是,它也是基于事件循环队列所设置的超时。根本来说,它无法限制任务队列中的死循环。
|
||||
|
||||
面对这个难题,考虑了很久,也导致这个项目拖了挺长一段时间的。摸索中想到了大概两个方法能够解决这个问题:
|
||||
|
||||
1. 继续使用 cluster 模块,cluster 模块没有直接的 API 钩子给我们方便的在主进程中实现计时的逻辑。我们可以考虑重写任务分发算法,在 Round Robin 算法的的基础上实现计时的逻辑。从而控制子进程,当子进程超时时,直接结束子进程的声明周期。
|
||||
2. 第二个方法是,放弃使用 cluster 模块,由我们亲自来管理进程的分发已经生命周期,从而达到对子进程设置执行超时时间的限制。
|
||||
|
||||
这两个方法都不是什么简单省事的方法,好在我们有优秀的开源社区。正当我被子进程卡主时,得知了一个名为 [Houfeng/safeify: 📦 Safe sandbox that can be used to execute untrusted code. (github.com)](https://github.com/Houfeng/safeify) 的项目。它属于第二种解决办法,对`child_process`的手动管理,从而实现对子进程的完全控制,且设置超时时间。
|
||||
|
||||
虽然上述写的 cluster 模块的代码需要重构,并且我们也不需要 cluster 模块了。利用 safeify 就可以进行对子进程的管理了。
|
||||
|
||||
所以这里对 Koa 的主进程写法就是最常见的方式,将控制和执行函数的逻辑抽离为一个 middleware,交由路由进行匹配:
|
||||
|
||||
```ts
|
||||
import Koa from 'koa';
|
||||
import runFaaS from './middleware/faas';
|
||||
import logger from 'koa-logger';
|
||||
import OPTION from './option';
|
||||
import router from './routers';
|
||||
import bodyParser from 'koa-bodyparser';
|
||||
import cors from './middleware/CORS';
|
||||
|
||||
const app = new Koa();
|
||||
|
||||
app.use(logger());
|
||||
app.use(bodyParser());
|
||||
app.use(cors);
|
||||
// 先注册路由
|
||||
app.use(router.routes());
|
||||
app.use(router.allowedMethods());
|
||||
// 路由未匹配到的则运行函数
|
||||
app.use(runFaaS);
|
||||
|
||||
console.log(`⚡[Server]: running at http://${OPTION.host}:${OPTION.port} !`);
|
||||
|
||||
export default app.listen(OPTION.port);
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
我的简易 FaaS 基本上到这里就告一段落了,对 Devil 的最后针扎就是限制函数的异步执行时间。实际上还有一些可以优化的点。例如对函数执行资源的限制,即便我们对函数的执行时间有了限制,但在函数死循环的几秒钟,它还是占有了我们 100% 的 CPU。如果多个进程的函数都会占满 CPU 的执行,那么到最后服务器的资源可能会被消耗殆尽。
|
||||
|
||||
针对这个情况也有解决办法:在 Linux 系统上可以使用 CGroup 来对 CPU 和系统其他资源进程限制。其实 safeify 中也有了对 CGroup 的实现,但我最终没有采用作用这个方案,因为在 Docker 环境中,资源本身已经有了一定的限制,而且 Container 中大部分系统文件都是 readonly 的,CGroup 也不好设置。
|
||||
|
||||
还有一个优化的地方就是可以给函数上下文提供一些内置的可以函数,模仿添加 BaaS 的实现,添加一个常用的服务。不过最终这个小功能也没有实现,因为(懒)这本来就是一个对 FaaS 的简单模拟,越是复杂安全性的问题也会随着增加。
|
||||
|
||||
## 推荐
|
||||
|
||||
无利益相关推荐:
|
||||
|
||||
目前市面上大部分对于 Serverless 的书籍都是研究其架构的,对于面向前端的 Serverless 书籍不是很常见。而《前端 Serverless:面向全栈的无服务器架构实战》就是这样一本针对我们前端工程师的书籍,从 Serverless 的介绍,到最后的上云实践,循序渐进。
|
||||
|
||||
本篇也大量参考其中。
|
||||
|
||||
<Image
|
||||
src="/images/p/built-simply-faas/book.jpg"
|
||||
placeholder="book"
|
||||
priority
|
||||
width="392"
|
||||
height="500"
|
||||
/>
|
||||
|
||||
## 把玩
|
||||
|
||||
~~[FaaS](https://demo.defectink.com/faas/#/)~~
|
334
content/posts/create-a-mini-router-for-react.mdx
Normal file
334
content/posts/create-a-mini-router-for-react.mdx
Normal file
@ -0,0 +1,334 @@
|
||||
---
|
||||
title: 现代前端的Web应用路由-为React打造一个迷你路由器
|
||||
date: '2022-08-03'
|
||||
tags: [JavaScript, React]
|
||||
---
|
||||
|
||||
路由不仅仅只是网络的代名词,它更像是一种表达路径的概念。与网络中的路由相似,前端中的页面路由也是带领我们前往指定的地方。
|
||||
|
||||
<Image
|
||||
src="/images/p/create-a-mini-router-for-react/router.webp"
|
||||
alt=""
|
||||
priority
|
||||
width="1366"
|
||||
height="900"
|
||||
/>
|
||||
|
||||
## 现代前端的 Web 应用路由
|
||||
|
||||
时代在变迁,过去,Web 应用的基本架构使用了一种不同于现代路由的方法。曾经的架构通常都是由后端生成的 HTML 模板来发送给浏览器。当我们单击一个标签导航到另一个页面时,浏览器会发送一个新的请求给服务器,然后服务器再将对应的页面渲染好发过来。也就是说,每个请求都会刷新页面。
|
||||
|
||||
自那时起,Web 服务器在设计和构造方面经历了很多发展(前端也是)。如今,JavaScript 框架和浏览器技术已经足够先进,允许我们利用 JavaScript 在客户端渲染 HTML 页面,以至于 Web 应用可以采用更独特的前后的分离机制。在第一次由服务端下发了对应的 JavaScript 代码后,后续的工作就全部交给客户端 JavaScript。而后端服务器负责发送原始数据,通常是 JSON 等。
|
||||
|
||||
<Image
|
||||
src="/images/p/create-a-mini-router-for-react/Web架构.svg"
|
||||
alt="Web架构"
|
||||
width="732"
|
||||
height="462"
|
||||
/>
|
||||
|
||||
在旧架构中,动态内容由 Web 服务器生成,服务器会在数据库中获取数据,并利用数据渲染 HTML 模板发送给浏览器。每次切换页面都会获取新的由服务端渲染的页面发送给浏览器。
|
||||
|
||||
在新架构中,服务端通常只下发主要的 JavaScript 和基本的 HTML 框架。之后页面的渲染就会由我们的 JavaScript 接管,后续的动态内容也还是由服务器在数据库中获取,但不同的是,后续数据由服务器发送原始格式(JSON 等)。前端 JavaScript 由 AJAX 等技术获取到了新数据,再在客户端完成新的 HTML 渲染。
|
||||
|
||||
好像 SPA 的大概就是将原先服务端干的活交给了前端的 JavaScript 来做。事实上,确实如此。AJAX 技术的发展,带动了客户端 JavaScript 崛起,使得原先需要在服务端才能完成渲染的动态内容,现在交给 JavaScript 就可以了。
|
||||
|
||||
这么做的好处有很多:
|
||||
|
||||
- 主要渲染工作在客户端,减少服务器的压力。简单的场景甚至只需要静态服务器。
|
||||
- 新内容获取只需要少量交互,而不是服务端发送渲染好的 HTML 页面。
|
||||
- 可以利用 JavaScript 在客户端修改和渲染任意内容,同时无需刷新整个页面。
|
||||
- ……
|
||||
|
||||
当然同时也有一些缺点,目前最主要的痛点:
|
||||
|
||||
- 首屏/白屏时间:由于 HTML 内容需要客户端 JavaScript 完成渲染,前端架构以及多种因素会影响首次内容的出现时间。
|
||||
- 爬虫/SEO:由于 HTML 内容需要客户端 JavaScript 完成渲染,早期的爬虫可能不会在浏览器环境下执行 JavaScript,这就导致了根本爬取不到 HTML 内容。
|
||||
|
||||
> Google 的爬虫貌似已经可以爬取 SPA。
|
||||
|
||||
## React 的路由
|
||||
|
||||
React 是现代众多成熟的 SPA 应用框架之一,它自然也需要使用路由来切换对应的组件。React Router 是一个成熟的 React 路由组件库,它实现了许多功能,同时也非常还用。
|
||||
|
||||
首先来看下本次路由的基本工作原理,本质上很简单,我们需要一个可以渲染任意组件的父组件 `<Router />` 或者叫 `<router-view>` 之类的。然后再根据浏览器地址的变化,渲染注册路由时对应的组件即可。
|
||||
|
||||
<Image
|
||||
src="/images/p/create-a-mini-router-for-react/迷你路由器.svg"
|
||||
alt="迷你路由器"
|
||||
width="916"
|
||||
height="514"
|
||||
/>
|
||||
|
||||
### 配置文件
|
||||
|
||||
这里选择类似 Vue Router 的配置文件风格,而不是使用类似 `<Route />` 这样的 DOM 结构来注册路由。我们的目的是为了实现一个非常简单的迷你路由器,所以配置文件自然也就很简单:
|
||||
|
||||
```ts
|
||||
import { lazy } from 'react';
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '/',
|
||||
component: lazy(() => import('../pages/Home')),
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
component: lazy(() => import('../pages/About')),
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
一个 `path` 属性,对应了浏览器的地址,或者说 `location.pathname`;一个 `component` 属性,对应了到该地址时所需要渲染的组件。
|
||||
|
||||
甚至还可以使用 `lazy()` 配合 `<Suspense>` 来实现代码分割。
|
||||
|
||||
### 展示路由
|
||||
|
||||
一个非常简单的路由就这样注册好了,接下来就是将对应的组件展示出来。我们都知道,JSX 最终会被 babel 转义为渲染函数,而一个组件的 `<Home />` 写法,基本等同于 `React.createElement(Home)`。[元素渲染 – React (reactjs.org)](https://zh-hans.reactjs.org/docs/rendering-elements.html#updating-the-rendered-element)
|
||||
|
||||
所以动态的渲染指定的组件基本上也就很容易解决,接下来的思路也就很简单了,我们需要:
|
||||
|
||||
- 一个状态:记录当前地址 `location.pathname`;
|
||||
- 根据当前地址在配置文件中寻找对应注册的组件,并将它渲染出来;
|
||||
- 一个副作用:当用户手动切换路由时,该组件需要重新渲染为对应注册的路由;
|
||||
|
||||
先不考虑切换路由的问题,前两个基本上已经就实现了:
|
||||
|
||||
```tsx
|
||||
// Router.tsx
|
||||
import React, { useState, Suspense } from 'react';
|
||||
import routes from './routes';
|
||||
|
||||
const Router: React.FC = () => {
|
||||
// 获取地址,并保存到状态中
|
||||
const [path, setPath] = useState(location.pathname);
|
||||
// 根据地址,寻找对应的组件
|
||||
const element = routes.find((route) => route.path === path)?.component;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Suspense fallback={<p>loading...</p>}>
|
||||
{/* 使用 React.createElement() 渲染组件 */}
|
||||
{element ? React.createElement(element) : void 0}
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Router;
|
||||
```
|
||||
|
||||
看上去很简单,事实上,确实很简单。
|
||||
|
||||
现在我们直接从地址栏访问对应的路由,我们的 Router 组件应该就可以根据已经注册好的路由配置文件来找到正确的组件,并将其渲染出来了。
|
||||
|
||||
<Image
|
||||
src="/images/p/create-a-mini-router-for-react/image-20210823154009498.webp"
|
||||
alt="image-20210823154009498"
|
||||
width="419"
|
||||
height="227"
|
||||
/>
|
||||
|
||||
### 切换路由
|
||||
|
||||
到目前为止,我们实现了根据对应地址访问到对应组件的功能。这是一个路由必不可少的功能,但它还不能称得上是一个简单的路由器,因为它还无法处理用户手动切换的路由,也就是点击标签前往对应的页面。
|
||||
|
||||
简单梳理一下我们需要实现的功能:
|
||||
|
||||
- 一个 Link 组件,用于点击后导航到指定的地址;
|
||||
- 导航到地址后,还要修改浏览器的地址栏,并不真正的发送请求;
|
||||
- 通知 Router 组件,地址已经改变,重新渲染对应路由的组件;
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
to: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const RouterLink: React.FC<Props> = ({ to, children }: Props) => {
|
||||
/**
|
||||
* 处理点击事件
|
||||
* 创建自定义事件监听器
|
||||
* 并将 path 发送给 router
|
||||
* @param e
|
||||
*/
|
||||
const handleClick = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
const nowPath = location.pathname;
|
||||
if (nowPath === to) return; // 原地跳转
|
||||
|
||||
history.pushState(null, '', to);
|
||||
document.dispatchEvent(
|
||||
new CustomEvent<string>('route', {
|
||||
detail: to,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<a href="" onClick={handleClick}>
|
||||
{children}
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RouterLink;
|
||||
```
|
||||
|
||||
我们将 Link 组件实际的渲染为一个 `<a></a>` 标签,这样就能模拟跳转到指定的导航了。这个组件它接收两个参数:`{ to, children }`,分别是前往的路由地址和标签的内容。标签的内容 children 就是展示在 a 标签内的文本。
|
||||
|
||||
我们需要解决的第一个问题就是点击标签后跳转到指定的导航,这里其实需要分成两个部分,第一个部分是悄悄的修改浏览器地址栏,第二个部分则是通知 Router 组件去渲染对应的组件。
|
||||
|
||||
修改地址栏很简单,利用到浏览器的 history API,可以很方便的修改 `pathName` 而不发送实际请求,这里只需要修改到第一个参数 to 即可:`history.pushState(null, '', to);`。
|
||||
|
||||
但使用 `pushState()` 并不会发出任何通知,我们需要自己实现去通知 Router 组件地址已经变化。本来像利用第三方库来实现一个 发布/订阅 的模型的,但这样这个路由器可能就没有那么迷你了。最后发现利用 HTML 的 CustomEvent 可以实现一个简单的消息订阅与发布模型。
|
||||
|
||||
HTML 自定义事件也很简单,我们在对应的 DOM 上 `dispatchEvent` 即可触发一个事件,而触发的这个事件,就是 `CustomEvent` 的实例,甚至还能传递一些信息。在这里,我们将路由地址传递过去。
|
||||
|
||||
```ts
|
||||
document.dispatchEvent(
|
||||
new CustomEvent<string>('route', {
|
||||
detail: to,
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
而在 Router 组件中,只需要和以前一样在对应的 DOM 上去监听一个事件,这个事件就是刚刚发布的 `CustomEvent` 的实例。
|
||||
|
||||
```ts
|
||||
// Router.tsx
|
||||
const handleRoute = (e: CustomEvent<string>) => {
|
||||
console.log(e.detail);
|
||||
setPath(e.detail);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* 监听自定义 route 事件
|
||||
* 并根据 path 修改路由
|
||||
*/
|
||||
document.addEventListener('route', handleRoute as EventListener);
|
||||
|
||||
return () => {
|
||||
// 清除副作用
|
||||
document.removeEventListener('route', handleRoute as EventListener);
|
||||
};
|
||||
}, []);
|
||||
```
|
||||
|
||||
在 Router 组件中,根据接收到的变化,将新的地址保存到状态中,并触发组件重新渲染。
|
||||
|
||||
这样一个最简单的 React 路由就做好了。
|
||||
|
||||
## Vue 的路由
|
||||
|
||||
与 React 同理,二者的路由切换都差不多,其主要思路还是使用自定义事件来订阅路由切换的请求。但 Vue 的具体实现与 React 还是有点不同的。
|
||||
|
||||
### 配置文件
|
||||
|
||||
路由的配置文件还是同理,不同的是,Vue 的异步组件需要在引入时同时引入一个 Loading 组件来实现 Loading 的效果:
|
||||
|
||||
```ts
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import Loading from '../components/common/Loading.vue';
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: defineAsyncComponent({
|
||||
loader: () => import('../views/Home.vue'),
|
||||
loadingComponent: Loading,
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'About',
|
||||
component: defineAsyncComponent({
|
||||
loader: () => import('../views/About.vue'),
|
||||
loadingComponent: Loading,
|
||||
}),
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### 展示路由
|
||||
|
||||
同理,Vue 也是利用根据条件来渲染对应路由的组件。不同的是,我们可以使用模板语法来实现,也可以利用 `render()` 方法来直接渲染组件。
|
||||
|
||||
首先来看看和 React 类似的 `render()` 方法。在 Vue3 中,使用 `setup()` 方法后,可以直接返回一个 `createVNode()` 的函数,这就是 `render()` 方法。所以可以直接写 TypeScript 文件。
|
||||
|
||||
与 React 不同的地方在于,React 每次调用 `setPath(e.detail)` 存储状态时都会重新渲染组件,从而重新执行组件的函数,获取到对应的路由组件。
|
||||
|
||||
但 Vue 不同,如果我们仅仅将路由名称 `e.detail` 保存到状态,但没有实际在 VNode 中使用的话,更新状态时不会重新渲染组件的,也就是说,不会获取到对应的路由组件。所以最佳的办法就是将整个路由组件保存到状态,可保存整个组件无疑太过庞大。好在 Vue3 给了我们另一种解决方法:`shallowRef()`。它会创建一个跟踪自身 `.value` 变化的 ref,但不会使其值也变成响应式的。
|
||||
|
||||
```ts
|
||||
import { createVNode, defineComponent, shallowRef } from 'vue';
|
||||
import routes from './routes';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'RouterView',
|
||||
setup() {
|
||||
let currentPath = window.location.pathname;
|
||||
const component = shallowRef(
|
||||
routes.find((item) => item.path === currentPath)?.component ??
|
||||
'Note found'
|
||||
);
|
||||
|
||||
const handleEvent = (e: CustomEvent<string>) => {
|
||||
console.log(e.detail);
|
||||
currentPath = e.detail;
|
||||
component.value =
|
||||
routes.find((item) => item.path === currentPath)?.component ??
|
||||
'Note found';
|
||||
};
|
||||
|
||||
document.addEventListener('route', handleEvent as EventListener);
|
||||
|
||||
return () => createVNode(component.value);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
而使用模板语法主要是利用到了全局的 `component` 组件,其他部分与 `render()` 方法相同:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<component :is="component"></component>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onUpdated, shallowRef } from 'vue';
|
||||
import routes from './routes';
|
||||
|
||||
let currentPath = window.location.pathname;
|
||||
const component = shallowRef(
|
||||
routes.find((item) => item.path === currentPath)?.component
|
||||
);
|
||||
|
||||
const handleEvent = (e: CustomEvent<string>) => {
|
||||
console.log(e.detail);
|
||||
currentPath = e.detail;
|
||||
component.value = routes.find((item) => item.path === currentPath)?.component;
|
||||
};
|
||||
|
||||
document.addEventListener('route', handleEvent as EventListener);
|
||||
|
||||
onUpdated(() => {
|
||||
console.log(component);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
如今的 JavaScript 做能做到的比以前更加强大,配合多种 HTML API,可以将曾经不可能实现的事变为现实。这个简单的迷你路由,主要的思路就是利用 HTML API 来通知 Router 组件该渲染哪个组件了。配合上 `lazy()` 方法,甚至还能实现代码分割。
|
||||
|
||||
## Demo
|
||||
|
||||
[DefectingCat/react-tiny-router: A tiny react router. (github.com)](https://github.com/DefectingCat/react-tiny-router)
|
149
content/posts/deploy-npm-package-with-turborepo.mdx
Normal file
149
content/posts/deploy-npm-package-with-turborepo.mdx
Normal file
@ -0,0 +1,149 @@
|
||||
---
|
||||
title: 使用 Turborepo 和 rollup 构建 NPM 包
|
||||
date: '2022-10-10'
|
||||
tags: [JavaScript, NPM]
|
||||
---
|
||||
|
||||
对于 monorepo 我使用的是 [turborepo](https://turborepo.org/),创建一个新的项目非常简单:
|
||||
|
||||
```tsx
|
||||
npx create-turbo@latest
|
||||
```
|
||||
|
||||
对于包管理器,这次尝鲜使用了 [pnpm](https://pnpm.io/) 它在安装包到 workspace 中相对于其他两个来说要更加的方便。
|
||||
|
||||
```tsx
|
||||
pnpm add --filter rua-three react react-dom three stats.js -D
|
||||
```
|
||||
|
||||
## Turborepo
|
||||
|
||||
对于 turborepo 来说没有什么需要过多配置的,比较需要注意的就是它的环境变量需要手动在 `turbo.json` 配置文件中声明一次:
|
||||
|
||||
```tsx
|
||||
"globalEnv": [
|
||||
"NODE_ENV"
|
||||
],
|
||||
```
|
||||
|
||||
除此之外,在默认的 tsconfig 中, 其 `target` 是 `es5` 如果需要一些比较先进的语法,例如在类体中实例化另一个类,则就需要将 `target` 设置到 `es6` 及以上。
|
||||
|
||||
```tsx
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
},
|
||||
```
|
||||
|
||||
## rollup
|
||||
|
||||
相比较与 turborepo 来说,rollup 需要注意的地方就多一些。对于 rollup 的安装来说,本身所需要的依赖不是很多,主要是部分插件。
|
||||
|
||||
```tsx
|
||||
"@rollup/plugin-commonjs": "^22.0.2",
|
||||
"@rollup/plugin-node-resolve": "^14.1.0",
|
||||
"rollup": "^2.79.1",
|
||||
"rollup-plugin-typescript2": "^0.34.0",
|
||||
```
|
||||
|
||||
对于一个小包来说,我的目录结构也非常的简单,核心部分就在 `rollup.config.js` 和 `package.json` 。
|
||||
|
||||
```tsx
|
||||
.
|
||||
├── lib
|
||||
│ ├── cjs
|
||||
│ └── esm
|
||||
├── package.json
|
||||
├── README.md
|
||||
├── rollup.config.js
|
||||
├── src
|
||||
│ ├── hooks
|
||||
│ ├── index.ts
|
||||
│ └── three
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
安装完了 rollup 之后,需要对其简单的进行一下配置。对于 input 和 output 是基本操作,没有什么值得注意的。而 external 字段决定了我们依赖是否是外部依赖,从而直接决定了 rollup 会不会将第三方的依赖打包到一起。
|
||||
|
||||
```tsx
|
||||
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import typescript from 'rollup-plugin-typescript2';
|
||||
|
||||
import pkg from './package.json';
|
||||
|
||||
export default {
|
||||
input: 'src/index.ts',
|
||||
output: [
|
||||
{
|
||||
file: pkg.main,
|
||||
format: 'cjs',
|
||||
sourcemap: true,
|
||||
},
|
||||
{
|
||||
file: pkg.module,
|
||||
format: 'es',
|
||||
sourcemap: true,
|
||||
},
|
||||
],
|
||||
external: [
|
||||
...Object.keys(pkg.peerDependencies || {}),
|
||||
'three/examples/jsm/controls/OrbitControls',
|
||||
],
|
||||
plugins: [
|
||||
nodeResolve(),
|
||||
commonjs(),
|
||||
typescript({
|
||||
typescript: require('typescript'),
|
||||
}),
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
通常来说,我们将所依赖的第三方依赖明确的生声明在 `peerDependencies` 中,在根据其 key 设置到 external 中,这样我们的 `peerDependencies` 就不会被一起打包了。
|
||||
|
||||
```tsx
|
||||
"peerDependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"stats.js": "^0.17.0",
|
||||
"three": "^0.144.0"
|
||||
},
|
||||
```
|
||||
|
||||
但在我的例子中,光添加了 `three` 还不够。如果还引入了 `three` 这个包下的其他文件夹内的文件,rollup 并没有非常智能的识别到 `three/examples/jsm/controls/OrbitControls` 也隶属于 `three` 。
|
||||
|
||||
从而将 `OrbitControls` 也打包到一起了,所以在 external 中还需要手动添加。
|
||||
|
||||
```tsx
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
|
||||
```
|
||||
|
||||
## package
|
||||
|
||||
在 `package.json` 中除了 `peerDependencies` 比较重要外,入口文件和类型文件的位置声明也决定了我们的包能否被正常使用。
|
||||
|
||||
```tsx
|
||||
"main": "./lib/cjs/index.js",
|
||||
"module": "./lib/esm/index.js",
|
||||
"types": "./lib/esm/index.d.ts",
|
||||
```
|
||||
|
||||
而 `files` 字段则决定了当我们使用 `npm publish` 时所上传的文件:
|
||||
|
||||
```tsx
|
||||
"files": [
|
||||
"/lib"
|
||||
],
|
||||
```
|
||||
|
||||
## NPM
|
||||
|
||||
在做了适当的配置后,公开一个包到公共源非常简单,只需要两个命令,分别是登录和上传。
|
||||
|
||||
```tsx
|
||||
npm adduser
|
||||
npm publish
|
||||
```
|
||||
|
||||
需要注意的就是不能在镜像源上登陆,当然也不能上传。
|
@ -0,0 +1,357 @@
|
||||
---
|
||||
title: 组件泛型实例-封装可复用的表单组件
|
||||
date: '2022-08-12'
|
||||
tags: [TypeScript, React]
|
||||
---
|
||||
|
||||
当前很多 UI 库都会为我们集成可复用的 Form 组件,并且是开箱即用。但有时候我们往往可能需要为自己的组件集成 Form。单纯的手动管理所有的状态可能不是件理想的活,尤其是表单验证。
|
||||
|
||||
[React Hook Forms](https://react-hook-form.com/) 为我们提供了完善的状态管理,并且可以集成到任何组件中去。
|
||||
|
||||
你可能会问,如今已经有了像是 MUI、Ant Design 等此类优秀的组件库,为什么还需要使用 React Hook Forms 来管理表单。
|
||||
|
||||
[MUI: The React component library you always wanted](https://mui.com/zh/)
|
||||
|
||||
虽然一些优秀的成熟组件库会为我们提供良好的表单解决方案,但它终究需要与组件库一起使用。而并非只是提供表单的状态管理,并没有完全的与组件库解耦合。
|
||||
|
||||
同时,当我们使用诸如 [Daisyui](https://daisyui.com/) 等此类的 CSS 组件时,它们是与状态完全解耦合的。我们需要自己为其维护状态。
|
||||
|
||||
## Hook our form
|
||||
|
||||
对于一个表单来说,提供的表单项越多,所需要的状态管理就越繁琐。不仅仅是状态管理,后续的表单验证才是一个表单的核心所在。
|
||||
|
||||
React Hook Forms 对 TypeScript 支持良好,有了 TypeScript 我们就可以在开发时验证表单类型。而表单的数据类型也是后续封装通用组件较为繁琐的一个地方。
|
||||
|
||||
<RUASandpack
|
||||
template="react-ts"
|
||||
files={{
|
||||
'/App.tsx':
|
||||
sandpack['generic-component-encapsulate-reusable-component'].hookApp,
|
||||
}}
|
||||
customSetup={{
|
||||
dependencies: {
|
||||
'@emotion/react': '^11.10.0',
|
||||
'@emotion/styled': '^11.10.0',
|
||||
'react-hook-form': '^7.34.0',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
React Hook Forms 在使用方面,使用了一个 `register` 函数代替了我们为每个表单项管理状态的步骤。从写法上就可以看出,这个函数返回了我们的表单所需要的属性,以及其状态。
|
||||
|
||||
```tsx
|
||||
<input type="text" id="firstname" {...register('firstName')} />
|
||||
```
|
||||
|
||||
在表单提交方面,`handleSubmit` 方法接受一个回调,其参数就是表单输入后的状态。
|
||||
|
||||
```tsx
|
||||
const onSubmit = handleSubmit((data) => console.log(data));
|
||||
```
|
||||
|
||||
表单验证通过后,就可以成功调用这个函数,以实现我们的表单提交。
|
||||
|
||||
这是一段最基础的用法,没有表单验证提示,仅仅只是接受任何用户输入的数据。并且同样的组件也没有实现复用。
|
||||
|
||||
## Input 组件
|
||||
|
||||
封装一个可复用的 `Input` 组件可能是再简单不过的事情了,对于其参数类型,主要部分还是来自于 `HTMLInput` 。我们只需要个别定义的属性,再利用剩余参数将其全部赋值给真正的 `input`
|
||||
|
||||
```tsx
|
||||
export type FormInputProps = {
|
||||
label?: string | undefined;
|
||||
} & DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
|
||||
```
|
||||
|
||||
```tsx
|
||||
const Input = ({ name, label, ...rest }: FormInputProps) => {
|
||||
return (
|
||||
<>
|
||||
<label htmlFor={name}>{label}</label>
|
||||
|
||||
<S.Wrapper>
|
||||
<S.Input name={name} {...rest} />
|
||||
</S.Wrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
用起来自然也是和常见的组件一样方便:
|
||||
|
||||
```tsx
|
||||
<div>
|
||||
<Input name="firstname" label="First name:" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Input name="lastname" label="Last name:" />
|
||||
</div>
|
||||
```
|
||||
|
||||
但是如果仅仅只是这样,我们的组件还不能与 React Hook Forms 一起工作。因为其核心部分 `register` 函数还无法传递给我们的 `Input` 组件。也就是说我们的组件现在还是不可控的,这时候再尝试提交就会发现无法获取其状态。
|
||||
|
||||
<Image
|
||||
src={
|
||||
'/images/p/generic-component-encapsulate-reusable-component/Untitled.png'
|
||||
}
|
||||
alt="无法获取其状态"
|
||||
width="493"
|
||||
height="478"
|
||||
/>
|
||||
|
||||
当然我们不能简单的将 `register` 函数塞给 `Input` 组件,因为它还没有合适的签名。`register` 函数会根据表单的数据签名和不同的表单项来实现自己的签名。
|
||||
|
||||
从 `register` 函数的签名中就可以看出,它接受一个泛型,该泛型就是对应的表单项类型。
|
||||
|
||||
```tsx
|
||||
register: <"firstName">(name: "firstName", options?: ...)
|
||||
```
|
||||
|
||||
也就是 `FormData` 中的 `firstName` :
|
||||
|
||||
```tsx
|
||||
type FormData = {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
favorite: Pet;
|
||||
};
|
||||
```
|
||||
|
||||
没错,要想正确的给组件中的 `register` 函数签名,我们就得给我们的函数式组件上个泛型。
|
||||
|
||||
## 泛型
|
||||
|
||||
在考虑给组件添加一个泛型之前,需要先简单的了解下泛型是如何工作的。
|
||||
|
||||
一个函数的泛型可以非常的简单,它代表了一个任意的类型值(当然也可以对其进行约束)。并根据指定的参数为泛型时,自动推断该类型值。
|
||||
|
||||
```ts
|
||||
const logAndReturn = <T extends unknown>(target: T) => {
|
||||
console.log(target);
|
||||
return target;
|
||||
};
|
||||
|
||||
// const logAndReturn: <42>(target: 42) => 42
|
||||
logAndReturn(42);
|
||||
// const logAndReturn: <"42">(target: "42") => "42"
|
||||
logAndReturn('42');
|
||||
```
|
||||
|
||||
### 类型别名中的泛型
|
||||
|
||||
类型别名中的泛型与函数不同的是,它需要手动传递一个函数的泛型值(或来自其他地方的泛型),并根据该泛型来决定其值。并且如果泛型有约束的话,还需要符合其约束。
|
||||
|
||||
例如,我们有一个描述个人的类型别名:
|
||||
|
||||
```ts
|
||||
type Person<T extends number | string> = {
|
||||
name: string;
|
||||
age: number;
|
||||
favorite: T;
|
||||
};
|
||||
```
|
||||
|
||||
而我们需要编写一个函数,根据其 `favorite` 来决定打印的值。函数大概长这样:
|
||||
|
||||
```tsx
|
||||
const sayIt = <T extends number | string>(p: Person<T>) => {
|
||||
const type = typeof p.favorite;
|
||||
switch (type) {
|
||||
case 'string':
|
||||
console.log(`My favorite word is: ${p.favorite}`);
|
||||
return;
|
||||
case 'number':
|
||||
console.log(`My favorite number is: ${p.favorite}`);
|
||||
return;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
当指定参数为 `p: Person<T>` 时,就需要将函数的泛型传递给类型别名。且类型别名中的泛型约束在了 `<T extends number | string>` 之间,函数必须保证使其子类型。否则就会提示无法满足其类型。
|
||||
|
||||
<Image
|
||||
src={
|
||||
'/images/p/generic-component-encapsulate-reusable-component/Untitled-1.png'
|
||||
}
|
||||
alt="未约束的泛型"
|
||||
width="516"
|
||||
height="131"
|
||||
/>
|
||||
|
||||
和参数类型,泛型也是向下兼容的,只要保证其类型是子类型即可。也就是说这样也是可以的 `const sayIt = <T extends 42>(p: Person<T>) => {}` 。数字 42 是 `number` 类型的子类型。
|
||||
|
||||
随后在调用函数时,就能发现泛型给我们带来的作用了。
|
||||
|
||||
<Image
|
||||
src={
|
||||
'/images/p/generic-component-encapsulate-reusable-component/Untitled-2.png'
|
||||
}
|
||||
alt="传递数字给泛型"
|
||||
width="416"
|
||||
height="158"
|
||||
/>
|
||||
|
||||
<Image
|
||||
src={
|
||||
'/images/p/generic-component-encapsulate-reusable-component/Untitled-3.png'
|
||||
}
|
||||
alt="传递字符串给泛型"
|
||||
width="504"
|
||||
height="156"
|
||||
/>
|
||||
|
||||
### React 中的泛型
|
||||
|
||||
我们的 React 函数组件也是一个函数,对于泛型的规则同样适用。
|
||||
|
||||
来看一个简单的小组件,该组件可以以一个常见的对象类型 `Record<string, unknown>` 来根据指定的 key 访问其值,并展示在 DOM 上。
|
||||
|
||||
<RUASandpack
|
||||
template="react-ts"
|
||||
files={{
|
||||
'/App.tsx':
|
||||
sandpack['generic-component-encapsulate-reusable-component'].genericApp,
|
||||
'/Child.tsx':
|
||||
sandpack['generic-component-encapsulate-reusable-component'].genericChild,
|
||||
}}
|
||||
/>
|
||||
|
||||
例如,这样的一个值:
|
||||
|
||||
```tsx
|
||||
const testData = {
|
||||
firstName: 'xfy',
|
||||
lastName: 'xfyxfy',
|
||||
};
|
||||
```
|
||||
|
||||
当传递其对应的 key 时,我们的子组件就会展示对应的属性。也就是 `data[key]` ,这是再简单不过的一个属性访问方式了。
|
||||
|
||||
```tsx
|
||||
<Child data={testData} name="firstName" />
|
||||
```
|
||||
|
||||
但不仅如此,我们还希望我们的子组件能够根据已经存在的值,推断出我们能够传递的 key。
|
||||
|
||||
<Image
|
||||
src={
|
||||
'/images/p/generic-component-encapsulate-reusable-component/Untitled-4.png'
|
||||
}
|
||||
alt="类型推断"
|
||||
width="743"
|
||||
height="118"
|
||||
/>
|
||||
|
||||
这正是泛型的作用。
|
||||
|
||||
首先,我们子组件的参数签名必然需要一个泛型。并且我们将泛型约束在为一个常见的对象 `Record<string, unknown>`,且不在乎属性值具体是什么类型(unknown)。
|
||||
|
||||
```tsx
|
||||
type Props<T extends Record<string, unknown>> = {
|
||||
name: keyof T;
|
||||
data: T;
|
||||
};
|
||||
```
|
||||
|
||||
这便是我们组件的参数具体的签名。还记得上述类型别名需要将函数的泛型传递给它吗?接下来就是要给函数式组件添加一个泛型,并将其传递给 `Props` 。
|
||||
|
||||
我们的组件也是一个标准的函数,所以接下来就简单多了。只需要将泛型正确的约束,并传递给别名即可。
|
||||
|
||||
```tsx
|
||||
const Child = <T extends Record<string, unknown>>({
|
||||
name,
|
||||
data,
|
||||
}: Props<T>) => {};
|
||||
```
|
||||
|
||||
```tsx
|
||||
const Child = <T extends Record<string, unknown>>({ name, data }: Props<T>) => {
|
||||
const [showName, setShowName] = useState<T[keyof T]>();
|
||||
const valid = () => {
|
||||
console.log(data[name]);
|
||||
setShowName(data[name]);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>{name}</div>
|
||||
<button onClick={valid}>Show {name}</button>
|
||||
|
||||
<div>{JSON.stringify(showName)}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## 带有泛型的 Input 组件
|
||||
|
||||
`register` 函数对表单项的验证与上述较为类似,它也会根据表单项的 key 来决定传递对应的 name。为了满足 `register` 函数,可复用的 Input 组件就得需要一个泛型,用来接受不同的表单数据类型。
|
||||
|
||||
React hook forms 为我们提前准备好了适用于 `register` 函数的类型别名 `UseFormRegister` ,它会接受一个泛型,该泛型就是我们的表单数据类型。
|
||||
|
||||
所以 `register` 函数的签名看起来就像这样 `register?: UseFormRegister<T>;` 这里的 `T` 就是我们的表单类型。但是我们还不知道传入当前组件中的表单类型是什么,所以我们的组件参数签名也需要一个泛型。
|
||||
|
||||
所以这里我们的组件参数看起来是这样的:
|
||||
|
||||
```tsx
|
||||
export type FormInputProps<TFormValues> = {
|
||||
name: Path<TFormValues>;
|
||||
label?: string | undefined;
|
||||
rules?: RegisterOptions;
|
||||
register?: UseFormRegister<TFormValues>;
|
||||
} & DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
|
||||
```
|
||||
|
||||
值得注意的是,这里给 `input` 使用的 name 属性。因为 `register` 函数注册时使用的名称需要确保为表单类型的中的一个。所以这里需要使用 React hook forms 导出的 `Path<TFormValues>` 类型,以配合 `register` 函数。
|
||||
|
||||
这里就和上述泛型组件很相似了,接下来要做的就是将组件的泛型传递给参数签名:
|
||||
|
||||
```tsx
|
||||
const Input = <T extends Record<string, unknown>>({
|
||||
name,
|
||||
label,
|
||||
...rest
|
||||
}: FormInputProps<T>) => {};
|
||||
```
|
||||
|
||||
这里给组件的泛型小小的约束一下,我们希望传递过来的表单类型是一个普通的对象结构 `<T extends Record<string, unknown>>` 。
|
||||
|
||||
不仅如此,还不能忘了 `register` 函数还需要注册在 DOM 上。
|
||||
|
||||
```tsx
|
||||
<S.Input
|
||||
err={!!errorMsg}
|
||||
name={name}
|
||||
{...(register && register(name, rules))}
|
||||
{...rest}
|
||||
/>
|
||||
```
|
||||
|
||||
得益于泛型的功劳,我们将 `register` 函数传递给 `Input` 组件时,我们的组件就知道了这次表单的类型。并且确定了 `name` 属性的类型。
|
||||
|
||||
<Image
|
||||
src={
|
||||
'/images/p/generic-component-encapsulate-reusable-component/Untitled-5.png'
|
||||
}
|
||||
alt="类型推断"
|
||||
width="623"
|
||||
height="230"
|
||||
/>
|
||||
|
||||
这是因为 `register` 函数本身的签名:`const register: UseFormRegister<FormData>` 。这才使得我们的组件成功接受到了泛型。
|
||||
|
||||
再添加一些 `rules` 以及验证未通过时的提示,这样一个可复用的 React hook form 组件就封装好了。
|
||||
|
||||
```tsx
|
||||
<Input
|
||||
name="lastName"
|
||||
label="Last name:"
|
||||
placeholder="Last Name"
|
||||
register={register}
|
||||
rules={{ required: true }}
|
||||
errorMsg={errors.lastName && 'Please input'}
|
||||
/>
|
||||
```
|
||||
|
||||
<RUACodeSandbox url="https://codesandbox.io/s/reusable-input-o7e4jt?file=/src/App.tsx" />
|
22
content/posts/hello-world.mdx
Normal file
22
content/posts/hello-world.mdx
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
title: Hello world
|
||||
date: '2022-04-06'
|
||||
tags: ['Hello world']
|
||||
---
|
||||
|
||||
## Hello
|
||||
|
||||
This is my first post!
|
||||
|
||||
```ts
|
||||
console.log('Hello world');
|
||||
```
|
||||
|
||||
## Say hello to world
|
||||
|
||||
<RUASandpack
|
||||
template="react"
|
||||
files={{
|
||||
'/App.js': sandpack['hello-world'].hello,
|
||||
}}
|
||||
/>
|
148
content/posts/how-to-load-a-background-with-threejs.mdx
Normal file
148
content/posts/how-to-load-a-background-with-threejs.mdx
Normal file
@ -0,0 +1,148 @@
|
||||
---
|
||||
title: How to load a background with three.js
|
||||
date: '2022-04-13'
|
||||
tags: ['three.js', 'JavaScript']
|
||||
---
|
||||
|
||||
## Three.js setup
|
||||
|
||||
First, we need a little of setup. Let's create a scene and a camera.
|
||||
|
||||
```ts
|
||||
const scene = new THREE.Scene();
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(
|
||||
75,
|
||||
window.innerWidth / window.innerHeight,
|
||||
0.1,
|
||||
1000
|
||||
);
|
||||
```
|
||||
|
||||
The default camera position is in the center at `(0, 0, 0)`.
|
||||
|
||||
And now, We need render scene into our document. So we need a WebGL renderer.
|
||||
|
||||
```ts
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
canvas: document.querySelector('#canvas')
|
||||
antialias: true, // for performance reason, we will trun off antialias
|
||||
});
|
||||
```
|
||||
|
||||
With a little of setup:
|
||||
|
||||
```ts
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
```
|
||||
|
||||
Render to screen :
|
||||
|
||||
```ts
|
||||
renderer.render(scene, camera);
|
||||
```
|
||||
|
||||
Now, we can get a black canvas in our document.
|
||||
|
||||
<RUASandpack
|
||||
template="react"
|
||||
files={{
|
||||
'/App.js': sandpack['how-to-load-a-background-with-threejs'].firstScene,
|
||||
'/styles.css':
|
||||
sandpack['how-to-load-a-background-with-threejs'].resetStyles,
|
||||
}}
|
||||
customSetup={{
|
||||
dependencies: {
|
||||
'stats.js': '^0.17.0',
|
||||
three: '^0.139.2',
|
||||
'lil-gui': '^0.16.1',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
## Load a skyboxes
|
||||
|
||||
Image a camera inside a cube box. The cube box is our scene. just like `const scene = new THREE.Scene();`. The camera is our perspect camera.
|
||||
|
||||
```ts
|
||||
new THREE.PerspectiveCamera();
|
||||
```
|
||||
|
||||
Now, we are inside the cube box. The inside texture of the box is our sky. Our camera can see all six texture surround ours give ours the illusion the we are within a larger environment.
|
||||
|
||||
We know how sky boxes works. But what is a sky boxes?
|
||||
|
||||
### Texture
|
||||
|
||||
We need set texture to cube box each side. so we need six pictures.
|
||||
|
||||
The skybox is six images that can be connected each other.
|
||||
|
||||
<Image
|
||||
src={'/images/p/how-to-load-a-background-with-threejs/Skybox_example.png'}
|
||||
width="512"
|
||||
height="384"
|
||||
/>
|
||||
|
||||
We just need load six images in some order, and set them to the scene background.
|
||||
|
||||
```ts
|
||||
import corona_bk from 'assets/first-project/skyboxes/corona_bk.png';
|
||||
import corona_dn from 'assets/first-project/skyboxes/corona_dn.png';
|
||||
import corona_ft from 'assets/first-project/skyboxes/corona_ft.png';
|
||||
import corona_lf from 'assets/first-project/skyboxes/corona_lf.png';
|
||||
import corona_rt from 'assets/first-project/skyboxes/corona_rt.png';
|
||||
import corona_up from 'assets/first-project/skyboxes/corona_up.png';
|
||||
|
||||
const sky = new THREE.CubeTextureLoader(manager).load([
|
||||
corona_ft,
|
||||
corona_bk,
|
||||
corona_up,
|
||||
corona_dn,
|
||||
corona_rt,
|
||||
corona_lf,
|
||||
]);
|
||||
scene.background = sky;
|
||||
```
|
||||
|
||||
The order is:
|
||||
|
||||
1. front
|
||||
2. back
|
||||
3. up
|
||||
4. down
|
||||
5. right
|
||||
6. left
|
||||
|
||||
If load textrue successfully. we can see some picture of six pictures.
|
||||
|
||||
That's not enough. We are in 3D world, we need to loot around in the cubebox. So we need add a control.
|
||||
|
||||
```ts
|
||||
const controls = new OrbitControls(camera, ref.current!);
|
||||
controls.enablePan = false;
|
||||
controls.update();
|
||||
```
|
||||
|
||||
<RUASandpack
|
||||
template="react"
|
||||
files={{
|
||||
'/App.js': sandpack['how-to-load-a-background-with-threejs'].loadBackground,
|
||||
'/styles.css':
|
||||
sandpack['how-to-load-a-background-with-threejs'].resetStyles,
|
||||
}}
|
||||
customSetup={{
|
||||
dependencies: {
|
||||
'stats.js': '^0.17.0',
|
||||
three: '^0.139.2',
|
||||
'lil-gui': '^0.16.1',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
For OrbitControls, the camera must be in not default position. I set it to `(0, 1, 0)`.
|
||||
|
||||
Also, we need set to the camera up vector `camera.up.set(0, 0, 1);`.
|
||||
|
||||
> OrbitControls must execute update method after that it created `controls.update();`
|
294
content/posts/javascript-practice-multiplication-table.mdx
Normal file
294
content/posts/javascript-practice-multiplication-table.mdx
Normal file
@ -0,0 +1,294 @@
|
||||
---
|
||||
title: JavaScript practice multiplication table
|
||||
date: '2020-12-11'
|
||||
tags: JavaScript
|
||||
---
|
||||
|
||||
<Image
|
||||
src="/images/p/javascript-practice-multiplication-table/logo.webp"
|
||||
alt=""
|
||||
priority
|
||||
width="800"
|
||||
height="503"
|
||||
/>
|
||||
|
||||
从学习 js 到现在,也有一段时间了。曾经也尝试过写一些复杂的实践。后来经过测试,就算在入门阶段到了会写,那么距离最优代码还是有一定的差距。可能隔个几天回来自己都看不懂自己写的是什么了,杂乱无章。
|
||||
|
||||
最近打算先开始写一些简单的案例做做练习。无意中想到了经典的乘法口诀表,正好 for 循环也有过实践了,可以尝试一下。
|
||||
|
||||
> for 循环嵌套的一次练习。
|
||||
|
||||
## 思路
|
||||
|
||||
有了想法之后,就要开始构建实现的思路。由于 css 还没跨过门槛,所以决定就打算输出到 console 里。
|
||||
|
||||
<Image
|
||||
src="/images/p/javascript-practice-multiplication-table/multiplication-table.webp"
|
||||
alt="乘法表"
|
||||
width="798"
|
||||
height="548"
|
||||
/>
|
||||
|
||||
而在 console 输出类似于一个梯形的结构也是需要一个思考的问题。可以打印正确的形状之后,就可以考虑如何输出数字与结果了。
|
||||
|
||||
所以构思分为几步:
|
||||
|
||||
1. 打印正确的形状
|
||||
2. 输出内容
|
||||
|
||||
## 打印正确的形状
|
||||
|
||||
首先先忽略内容,一步一步的打印出所有东西。所以先将内容用`*`代替,只在意形状。
|
||||
|
||||
### 正方形
|
||||
|
||||
要想正确打印出一个正三角,可以先考虑从一个 10x10 正方形开始。
|
||||
|
||||
第一个想法就是将行和列分别输出。首先定义两个变量,一个用于保存`*`,等会打印再同一行循环打印 10 次,第一行的`*`就输出完成。另一个变量用于保存第一行输出的内容。
|
||||
|
||||
使用一个简单的 for 循环就能打印出第一行来:
|
||||
|
||||
```js
|
||||
let x = '*'; //输出“*”
|
||||
let s = ''; //保存列输出
|
||||
for (let i = 0; i < 10; i++) {
|
||||
s += x + ' '; //打印10列“*”
|
||||
}
|
||||
console.log(s);
|
||||
```
|
||||
|
||||
此时就能看到第一行的效果:
|
||||
|
||||
```js
|
||||
console.log(s);
|
||||
//输出:
|
||||
* * * * * * * * * *
|
||||
```
|
||||
|
||||
现在已经完成了 1x10,也就是说再打印出 9 行通用的内容就可以打印出一个 10x10 的正方形了。
|
||||
|
||||
那么接下来的思路和上面的也很相似,只需要**再**定义一个变量用于保存最后 10 行的结果就可以了。for 循环也差不多,只需要将`s`加上换行符打印十次就可以了。
|
||||
|
||||
```js
|
||||
let x = '*'; //输出“*”
|
||||
let s = ''; //保存列输出
|
||||
for (let i = 0; i < 10; i++) {
|
||||
s += x + ' '; //打印10列“*”
|
||||
}
|
||||
let t = ''; //保存行输出
|
||||
for (let i = 0; i < 10; i++) {
|
||||
t += s + '\n'; //再将10列打印10行
|
||||
}
|
||||
console.log(t);
|
||||
```
|
||||
|
||||
这样一个正方形就被打印出来了:
|
||||
|
||||
```js
|
||||
console.log(t);
|
||||
//输出:
|
||||
* * * * * * * * * *
|
||||
* * * * * * * * * *
|
||||
* * * * * * * * * *
|
||||
* * * * * * * * * *
|
||||
* * * * * * * * * *
|
||||
* * * * * * * * * *
|
||||
* * * * * * * * * *
|
||||
* * * * * * * * * *
|
||||
* * * * * * * * * *
|
||||
* * * * * * * * * *
|
||||
```
|
||||
|
||||
### 更好的正方形
|
||||
|
||||
现在离乘法表的三角形应该差切掉一半了。但是在此之前,上述打印一个正方形就用掉了三个变量,并且变量`s`在第一个用完之后便没用了,只是个简单的中间人。思路虽然很清晰,但是最后实现的步骤比较繁琐。
|
||||
|
||||
后来思索了一下,只需要将两个 for 循环嵌套一下,就可以节省下来两个变量,并且整体更加的简洁。
|
||||
|
||||
此时只需要一个变量`s`来保存最后的输出结果。将两个 for 循环嵌套在一起时,它们的运算步骤就变成为:
|
||||
|
||||
1. 进入第一层循环
|
||||
2. 第一层循环内的 for 循环到指定的次数后退出
|
||||
3. 再次执行第一层循环,并重述步骤 2,直到第一层循环达到次数
|
||||
|
||||
这样一个步骤就能够更加简洁的打印出一个正方形。步骤总结就是第一个 for 循环第一次,第二个 for 循环就循环了 10 次,正好可以打印以后 10 个`*`。随后第一个 for 循环进入第二次,并换行,第二个 for 循环再次打印 10 次。这样就两行了,以此类推,就可以打印出 10x10。
|
||||
|
||||
```js
|
||||
let s = ''; //保存结果
|
||||
for (let i = 0; i < 10; i++) {
|
||||
for (let o = 0; o < 10; o++) {
|
||||
s += '* '; //保存行
|
||||
}
|
||||
s += '\n'; //换行
|
||||
}
|
||||
console.log(s);
|
||||
```
|
||||
|
||||
最后的结果和上述还是一样的,但是整体的代码简洁了很多。
|
||||
|
||||
```js
|
||||
console.log(s);
|
||||
//输出:
|
||||
* * * * * * * * * *
|
||||
* * * * * * * * * *
|
||||
* * * * * * * * * *
|
||||
* * * * * * * * * *
|
||||
* * * * * * * * * *
|
||||
* * * * * * * * * *
|
||||
* * * * * * * * * *
|
||||
* * * * * * * * * *
|
||||
* * * * * * * * * *
|
||||
* * * * * * * * * *
|
||||
```
|
||||
|
||||
### 三角形
|
||||
|
||||
有了更好的正方形加持下,就可以轻松的打印出三角形了。
|
||||
|
||||
再看一次上述的 10x10 的步骤,第一个 for 循环第一次,第二个 for 循环就循环了 10 次,正好可以打印以后 10 个`*`。那么一个正三角形的第一行是只有一个`*`的,也就是说 for 循环第一次,第二个 for 循环就循环了 1 次,这样第一行就只有一个`*`。
|
||||
|
||||
实现的整体和打印正方形是没有多少区别的,是需要限制第二个循环的次数,循环到指定的次数后跳出,再由第一个循环换行。
|
||||
|
||||
根据“第一行一个`*`,第二行两个`*`”的规律,正好第一个循环的变量`i`是自增的。只需要将内循环的那个限制条件改为第一个循环的变量。就可以正好根据第一个循环打印出一个三角。
|
||||
|
||||
```js
|
||||
let s = '';
|
||||
for (let i = 0; i < 10; i++) {
|
||||
for (let o = 0; o <= i; o++) {
|
||||
//条件换成第一个循环的次数,达到“递增换行”的效果
|
||||
s += '* ';
|
||||
}
|
||||
s += '\n';
|
||||
}
|
||||
console.log(s);
|
||||
```
|
||||
|
||||
详细工作方式:当`i`等于 1 时,第一个循环进入第二个循环,第二个循环限制条件为`i`,只循环一次,输出一个`*`。随后跳出到第一个循环打印换行,然后第一个循环进入第二次,`i`等于 2。第二个循环正好沿着`i`的自增依次打印出对应数量的`*`。
|
||||
|
||||
输出:
|
||||
|
||||
```js
|
||||
console.log(s);
|
||||
*
|
||||
* *
|
||||
* * *
|
||||
* * * *
|
||||
* * * * *
|
||||
* * * * * *
|
||||
* * * * * * *
|
||||
* * * * * * * *
|
||||
* * * * * * * * *
|
||||
* * * * * * * * * *
|
||||
```
|
||||
|
||||
## 添加内容
|
||||
|
||||
已经跟着第一次打印的正方形,一步一步的能够打印出需要的正三角了。接下来就只需要将`*`替换为乘法表里的内容就可以了。
|
||||
|
||||
### 乘法表
|
||||
|
||||
要想实现一个有规律的方法,诀窍就是仔细观察其实现的规律。在 9 行的乘法表中,乘数和被乘数的关系在随着行数和列数增加而增加。被乘数总是与行数相等,而乘数则是与列数相等。
|
||||
|
||||
首先第一次的想法是,将乘法表的两个乘数由两个变量代替。只要控制好变量的变化就可以了。这种方法用到的变量比较多,完全用两个变量来代替乘数和被乘数。
|
||||
|
||||
第一次的想法没有确切的观察乘法表的规律,在打印正三角的基础上。通过对比大小来控制两个变量的变化。乘数随着行的增加时,是从 1 开始随着列增加的,被乘数一直等于行数。于是在第二个控制行的 for 循环中添加 if 语句,当乘数小于被乘数时++。
|
||||
|
||||
被乘数是随着行增加而增加的,于是当第二个 for 循环结束后,被乘数也++。而乘数则是从头开始,所以重新复制=1。
|
||||
|
||||
```js
|
||||
let a = 1;
|
||||
let b = 1;
|
||||
let s = '';
|
||||
for (let i = 0; i < 9; i++) {
|
||||
for (let o = 0; o <= i; o++) {
|
||||
s += a + '*' + b + '=' + a * b + ' ';
|
||||
if (a < b) {
|
||||
a++;
|
||||
}
|
||||
}
|
||||
b++;
|
||||
a = 1;
|
||||
s += '\n';
|
||||
}
|
||||
```
|
||||
|
||||
这样的方法可以成功的实现所有的效果,但是用的变量还是比较多的。并且语句结构也比较复杂。
|
||||
|
||||
### 更好的乘法表
|
||||
|
||||
前面都是没有仔细去观察乘法表的规律而写出来的,从而使用了好几个变量以及复杂的多的语句。
|
||||
|
||||
只需要详细观察了乘法表里的乘数与被乘数之间的变化关系,就能够发现两次嵌套循环之间变量的关系刚好一样。
|
||||
|
||||
```js
|
||||
let res = '';
|
||||
// i随着行变化(被乘数)
|
||||
for (let i = 1; i <= 9; i++) {
|
||||
// o始终从1开始,小于行数(乘数)
|
||||
for (let o = 1; o <= i; o++) {
|
||||
res += `${o}*${i}=${o * i} `;
|
||||
}
|
||||
res += '\n';
|
||||
}
|
||||
```
|
||||
|
||||
也就是说`i`相当于乘数,它随着每行的增加而增加,并且每行都一样,同时它控制着换行。而变量`o`相当于被乘数,每行从 1 开始,并且小于等于`i`。
|
||||
|
||||
## 操作 DOM
|
||||
|
||||
当时水这篇的时候我还没有学到使用 js 操作 DOM,时隔一年以后,我才想起来这篇还没水完。同时一年后,我还在乘法口诀表(◎﹏◎)
|
||||
|
||||
操作 DOM 并不是很复杂,打印乘法口诀表的主要方法也还是相同的。
|
||||
|
||||
### 插入节点
|
||||
|
||||
本次操作 DOM 最主要的一点还是循环添加 DOM 节点,通过`document.appendChild()`方法来添加生成的节点。HTML 结构也非常简单,外部一个`div`,内部通过`span`和`br`来插入内容。就类似于这样:
|
||||
|
||||
```html
|
||||
<div class="wrapper">
|
||||
<span>1 ✖ 1 = 1</span>
|
||||
<br />
|
||||
<span>2 ✖ 1 = 2</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
虽然这是个小测试,这里还是使用了文档片段( [`DocumentFragment`](https://developer.mozilla.org/en-US/docs/DOM/DocumentFragment))来插入节点。
|
||||
|
||||
```js
|
||||
wrapper = document.querySelector('.wrapper');
|
||||
fragment = document.createDocumentFragment();
|
||||
|
||||
for (let i = 1; i <= 9; i++) {
|
||||
for (let o = 1; o <= i; o++) {
|
||||
cont = fragment.appendChild(document.createElement('span'));
|
||||
cont.textContent = `${i} ✖ ${o} = ${i * o}`;
|
||||
}
|
||||
fragment.appendChild(document.createElement('br'));
|
||||
}
|
||||
wrapper.appendChild(fragment);
|
||||
```
|
||||
|
||||
思维也很简单,`appendChild()`方法会将插入的节点返回,返回后就可以直接使用`textContent`来插入生成的内容。同样的换行也是在外部的 for 循环内插入一个`<br>`标签。
|
||||
|
||||
```js
|
||||
let wrapper, cont, fragment;
|
||||
|
||||
window.onload = function () {
|
||||
wrapper = document.querySelector('.wrapper');
|
||||
fragment = document.createDocumentFragment();
|
||||
|
||||
function x99() {
|
||||
for (let i = 1; i <= 9; i++) {
|
||||
for (let o = 1; o <= i; o++) {
|
||||
cont = fragment.appendChild(document.createElement('span'));
|
||||
cont.textContent = `${i} ✖ ${o} = ${i * o}`;
|
||||
}
|
||||
fragment.appendChild(document.createElement('br'));
|
||||
}
|
||||
wrapper.appendChild(fragment);
|
||||
}
|
||||
x99();
|
||||
};
|
||||
```
|
||||
|
||||
<RUACodepen url="https://codepen.io/Defectink/pen/BaLQZZQ" />
|
326
content/posts/my-develop-environmental.mdx
Normal file
326
content/posts/my-develop-environmental.mdx
Normal file
@ -0,0 +1,326 @@
|
||||
---
|
||||
title: 我的开发环境
|
||||
date: '2022-04-14'
|
||||
tags: [Linux, Server]
|
||||
---
|
||||
|
||||
最近迁移了自己的小服务器,也顺便把本机的环境重新设置了一下,其中环节还是有点复杂的小细节的。所以打算整理下思路,方便以后再设置同样环境。
|
||||
|
||||
<Image
|
||||
src={'/images/p/my-develop-environmental/logo.svg'}
|
||||
width="1400"
|
||||
height="787"
|
||||
placeholder=""
|
||||
priority
|
||||
/>
|
||||
|
||||
## 对于服务器
|
||||
|
||||
目前常用的系统主要是 Ubuntu 和 FreeBSD,到手第一步便是更换到国内的镜像源。
|
||||
|
||||
对于 FreeBSD 目前可用的有中科大的源,pkg 的配置文件位置为:`/etc/pkg/FreeBSD.conf`。
|
||||
|
||||
```bash
|
||||
FreeBSD: {
|
||||
url: "pkg+http://mirrors.ustc.edu.cn/freebsd-pkg/${ABI}/latest",
|
||||
mirror_type: "srv",
|
||||
signature_type: "fingerprints",
|
||||
fingerprints: "/usr/share/keys/pkg",
|
||||
enabled: yes
|
||||
}
|
||||
```
|
||||
|
||||
对于 Ubuntu,随便选一个就可以了。配置文件位于:`/etc/apt/sources.list`。清华大学开源软件镜像站的 [Ubuntu 镜像使用帮助](https://mirrors.tuna.tsinghua.edu.cn/help/ubuntu/)
|
||||
|
||||
```bash
|
||||
# 默认注释了源码镜像以提高 apt update 速度,如有需要可自行取消注释
|
||||
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal main restricted universe multiverse
|
||||
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal main restricted universe multiverse
|
||||
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-updates main restricted universe multiverse
|
||||
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-updates main restricted universe multiverse
|
||||
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-backports main restricted universe multiverse
|
||||
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-backports main restricted universe multiverse
|
||||
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-security main restricted universe multiverse
|
||||
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-security main restricted universe multiverse
|
||||
|
||||
# 预发布软件源,不建议启用
|
||||
# deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-proposed main restricted universe multiverse
|
||||
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-proposed main restricted universe multiverse
|
||||
```
|
||||
|
||||
### 更换 Shell
|
||||
|
||||
Ubuntu 和 FreeBSD 默认 shell 分别是 bash 和 sh,下一步就是切换到 zsh。
|
||||
|
||||
```bash
|
||||
# Ubuntu
|
||||
apt install zsh -y
|
||||
|
||||
# FreeBSD
|
||||
pkg install zsh
|
||||
```
|
||||
|
||||
Oh my zsh 的安装非常的简单,复制一条命令即可。但困难的地方在于我的服务器可能经常连不上 Github,因为它是通过 clone Github 上的仓库来进行安装的。
|
||||
|
||||
```bash
|
||||
sh -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"
|
||||
```
|
||||
|
||||
研究了一下,可以找个 Github 的镜像站手动安装。不过这种方法还是有*一定风险*的,毕竟是直接从别人的站点下载的东西。比较推荐的还是 Gitee 的 [镜像](https://gitee.com/mirrors)。还有个 [fastgit](https://hub.fastgit.org/) 目前也是能用的。
|
||||
|
||||
Oh my zsh 也写了详细了 [manual-installation](https://github.com/ohmyzsh/ohmyzsh#manual-installation) 参考着修改下源的地址就可以了。
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://gitee.com/mirrors/ohmyzsh.git ~/.oh-my-zsh
|
||||
# Create a new zsh configuration file
|
||||
cp ~/.oh-my-zsh/templates/zshrc.zsh-template ~/.zshrc
|
||||
# Change your default shell
|
||||
chsh -s $(which zsh)
|
||||
```
|
||||
|
||||
### Plugins
|
||||
|
||||
我比较常用的是 zsh-autosuggestions 与 zsh-syntax-highlighting,比较可惜的是这俩 Gitee mirror 上都还没有。
|
||||
|
||||
```bash
|
||||
git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions
|
||||
|
||||
# Add to .zshrc
|
||||
plugins=(git zsh-autosuggestions)
|
||||
```
|
||||
|
||||
```bash
|
||||
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git
|
||||
echo "source ${(q-)PWD}/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh" >> ${ZDOTDIR:-$HOME}/.zshrc
|
||||
|
||||
source ./zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
|
||||
```
|
||||
|
||||
### Theme
|
||||
|
||||
通常用的是 powerlevel10k 或者 random。
|
||||
|
||||
对于 powerlevel10k 可以:
|
||||
|
||||
```bash
|
||||
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k
|
||||
# or
|
||||
git clone --depth=1 https://gitee.com/romkatv/powerlevel10k.git ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k
|
||||
```
|
||||
|
||||
还需要添加一下环境变量
|
||||
|
||||
```bash
|
||||
# To customize prompt, run `p10k configure` or edit ~/.p10k.zsh.
|
||||
[[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh
|
||||
POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD=true
|
||||
```
|
||||
|
||||
### Sudo with out password
|
||||
|
||||
Run sudo without password on mostly Linux
|
||||
|
||||
1. Backup /etc/sudoers file, run:
|
||||
|
||||
```bash
|
||||
sudo cp /etc/sudoers /root/sudoers.bak
|
||||
```
|
||||
|
||||
2. Edit the /etc/sudoers file on CentOS:
|
||||
|
||||
```bash
|
||||
sudo visudo
|
||||
```
|
||||
|
||||
3. Run /usr/sbin/rebootcommand without password on CentOS:
|
||||
|
||||
```bash
|
||||
xfy ALL=(ALL) NOPASSWD:ALL
|
||||
```
|
||||
|
||||
4. Save and exit the file.
|
||||
|
||||
### Docker mirror
|
||||
|
||||
对于使用 `systemd` 的系统(Ubuntu 16.04+、Debian 8+、CentOS 7), 在配置文件 `/etc/docker/daemon.json` 中加入:
|
||||
|
||||
```json
|
||||
{
|
||||
"registry-mirrors": ["https://docker.mirrors.ustc.edu.cn/"]
|
||||
}
|
||||
```
|
||||
|
||||
重新启动 dockerd:
|
||||
|
||||
```bash
|
||||
sudo systemctl restart docker
|
||||
```
|
||||
|
||||
## 对于语言
|
||||
|
||||
目前常用的主要是 Node.js 与 Rustup,这俩官方地址的速度也是时好时坏。好在淘宝和中科大有他们的镜像源。
|
||||
|
||||
### Node.js
|
||||
|
||||
主要用的是 nvm,类似于 Oh my zsh,可以 [manual-install](https://github.com/nvm-sh/nvm#git-install)。
|
||||
|
||||
```bash
|
||||
git clone https://gitee.com/mirrors/nvm.git .nvm
|
||||
```
|
||||
|
||||
然后导出对应的变量到当前的 shell 配置文件中:
|
||||
|
||||
```bash
|
||||
export NVM_DIR="$HOME/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
|
||||
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion
|
||||
```
|
||||
|
||||
其次就是安装 Node.js 本身了,nodejs.org 的速度大部分情况下都是很快的,偶尔也会抽风,nvm 也支持对应的 [环境变量](https://github.com/nvm-sh/nvm#use-a-mirror-of-node-binaries)
|
||||
|
||||
```bash
|
||||
export NVM_NODEJS_ORG_MIRROR=https://npm.taobao.org/mirrors/node
|
||||
```
|
||||
|
||||
### Rustup
|
||||
|
||||
Rustup 要相对更加简单的一点,Rustup 默认会读取两个环境变量,将其修改为镜像站点即可:
|
||||
|
||||
```bash
|
||||
export RUSTUP_DIST_SERVER=https://mirrors.ustc.edu.cn/rust-static
|
||||
export RUSTUP_UPDATE_ROOT=https://mirrors.ustc.edu.cn/rust-static/rustup
|
||||
```
|
||||
|
||||
然后再用官方的脚本。
|
||||
|
||||
```bash
|
||||
curl https://sh.rustup.rs -sSf | sh
|
||||
|
||||
source $HOME/.cargo/env
|
||||
```
|
||||
|
||||
对于 Windoiws 环境则需要在“设置”-“高级系统设置”-“环境变量”中添加对应的变量到用户/系统变量中。
|
||||
|
||||
<Image
|
||||
src={'/images/p/my-develop-environmental/windows-environmentail.png'}
|
||||
alt="Windows environmentail"
|
||||
width="1024"
|
||||
height="259"
|
||||
/>
|
||||
|
||||
当然这几个主要的变量可以放在 `.zshrc` 中,以后更新还会用到的。
|
||||
|
||||
### Crate.io
|
||||
|
||||
`~/.cargo/config` [清华大学](https://mirrors.tuna.tsinghua.edu.cn/help/crates.io-index.git/)赛高。
|
||||
|
||||
```
|
||||
[source.crates-io]
|
||||
replace-with = 'tuna'
|
||||
|
||||
[source.tuna]
|
||||
registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"
|
||||
```
|
||||
|
||||
对于 Windows 环境,则需要 `C:\Users\{username}\.cargo` 目录下的 `config` 文件,添加对应的源地址。
|
||||
|
||||
### Go mod
|
||||
|
||||
Go mod 支持 proxy 设置:
|
||||
|
||||
```bash
|
||||
go env -w GO111MODULE=on
|
||||
|
||||
# 1. 七牛 CDN
|
||||
go env -w GOPROXY=https://goproxy.cn,direct
|
||||
|
||||
# 2. 阿里云
|
||||
go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/,direct
|
||||
|
||||
# 3. 官方
|
||||
go env -w GOPROXY=https://goproxy.io,direct
|
||||
```
|
||||
|
||||
### pypi
|
||||
|
||||
同样清华大学的源,pypi 镜像每 5 分钟同步一次。
|
||||
|
||||
```bash
|
||||
pip install pip -U
|
||||
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
```
|
||||
|
||||
如果到 pip 默认源的网络连接较差,临时使用本镜像站来升级 pip:
|
||||
|
||||
```bash
|
||||
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pip -U
|
||||
```
|
||||
|
||||
## 对于本机
|
||||
|
||||
目前主要是使用 WSL2 中的 Ubuntu 作为开发环境的,WSL2 目前比较大的问题就是每次其 IP 地址都会变动,宿主机的地址,也就是它的网关也会一直变。
|
||||
|
||||
好在 DNS 地址默认就是宿主机 Windows 的地址,可以通过一个简单的命令查到它并将其保存到当前环境变量中:
|
||||
|
||||
```bash
|
||||
# 主机 IP 保存在 /etc/resolv.conf 中
|
||||
export host_ip=$(cat /etc/resolv.conf |grep "nameserver" |cut -f 2 -d " ")
|
||||
alias winip='cat /etc/resolv.conf |grep "nameserver" |cut -f 2 -d " "'
|
||||
```
|
||||
|
||||
顺便把内容单独提取到一个小脚本中:
|
||||
|
||||
```bash
|
||||
HOST_IP=$(cat /etc/resolv.conf | grep nameserver | awk '{ print $2 }')
|
||||
WSL_IP=$(hostname -I | awk '{print $1}')
|
||||
|
||||
PROXY_PORT=10809
|
||||
PROXY_HTTP="http://${HOST_IP}:${PROXY_PORT}"
|
||||
|
||||
set_proxy() {
|
||||
export http_proxy="${PROXY_HTTP}"
|
||||
export HTTP_PROXY="${PROXY_HTTP}"
|
||||
|
||||
export https_proxy="${PROXY_HTTP}"
|
||||
export HTTPS_proxy="${PROXY_HTTP}"
|
||||
|
||||
export ALL_PROXY="${PROXY_SOCKS5}"
|
||||
export all_proxy=${PROXY_SOCKS5}
|
||||
|
||||
git config --global http.proxy ${PROXY_HTTP}
|
||||
git config --global https.proxy ${PROXY_HTTP}
|
||||
|
||||
# git ssh proxy
|
||||
sed -i "s/# ProxyCommand/ProxyCommand/" ~/.ssh/config
|
||||
sed -i -E "s/ProxyCommand nc -X connect -x [0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:[0-9]+ %h %p/ProxyCommand nc -X connect -x ${HOST_IP}:${PROXY_PORT} %h %p/" ~/.ssh/config
|
||||
}
|
||||
|
||||
unset_proxy() {
|
||||
unset http_proxy
|
||||
unset HTTP_PROXY
|
||||
unset https_proxy
|
||||
unset HTTPS_PROXY
|
||||
unset ALL_PROXY
|
||||
unset all_proxy
|
||||
|
||||
git config --global --unset http.proxy ${PROXY_HTTP}
|
||||
git config --global --unset https.proxy ${PROXY_HTTP}
|
||||
|
||||
sed -i -E "s/ProxyCommand nc -X connect -x [0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:[0-9]+ %h %p/# ProxyCommand nc -X connect -x 0.0.0.0:0 %h %p/" ~/.ssh/config
|
||||
}
|
||||
|
||||
test_proxy() {
|
||||
echo "Host ip:" ${HOST_IP}
|
||||
echo "WSL ip:" ${WSL_IP}
|
||||
echo "Current proxy:" ${https_proxy}
|
||||
}
|
||||
```
|
||||
|
||||
并放在 `.zshrc` 中,使其可以自动被设置:
|
||||
|
||||
```bash
|
||||
# This is proxy for git.
|
||||
. ~/.config/proxy.sh
|
||||
set_proxy
|
||||
```
|
90
content/posts/object-around-in-threejs.mdx
Normal file
90
content/posts/object-around-in-threejs.mdx
Normal file
@ -0,0 +1,90 @@
|
||||
---
|
||||
title: Three.js 中的物体围绕运动
|
||||
date: '2022-09-02'
|
||||
tags: [Three.js, React]
|
||||
---
|
||||
|
||||
<Image
|
||||
src="/images/p/object-around-in-threejs/around-cover.jpg"
|
||||
width="1800"
|
||||
height="1201"
|
||||
/>
|
||||
|
||||
使一个物体围绕另一个物体运动,主要是保证围绕物体的运动轨迹。在 three.js 中,一个 Mesh 实例可以通过其自身的 `add()` 方法,去添加另一个实例。
|
||||
|
||||
通过此方法就可以达成添加一个实例成为另一个实例的子节点。当父节点进行运动时(例如自身旋转),子节点也会跟随运动。
|
||||
|
||||
在物体围绕运动中,我们可以把围绕物体添加到被围绕物体中。当父节点绕自身旋转时,离父节点有一定距离的围绕物体就会随着父节点的旋转而旋转。但这样做就会导致本不需要运动的父节点自身进行运动。
|
||||
|
||||
## Object3D
|
||||
|
||||
three.js 提供了一个可以在场景中添加的“点”。它与 Mesh 等物体不同,它相当于在场景中添加一个不可见的点。并且它也拥有 `add()` 方法,也就可以添加其他物体到这个点中。
|
||||
|
||||
```tsx
|
||||
const target = new THREE.Object3D();
|
||||
```
|
||||
|
||||
当这个点进行旋转时,被添加的物体也就可以按照预想的方式进行运动。同时它也可以被添加到一个物体中,其位置就会与添加的物体相同,就不用手动为其设置相同的位置了。
|
||||
|
||||
```tsx
|
||||
const target = new THREE.Object3D();
|
||||
|
||||
const material = new THREE.MeshPhongMaterial({
|
||||
color: '#4d4d4d',
|
||||
specular: '#fdfdfd',
|
||||
emissive: 'rgb(0,0,0)',
|
||||
});
|
||||
const boxGeo = new THREE.BoxGeometry(2, 2, 2);
|
||||
const box = new THREE.Mesh(boxGeo, material);
|
||||
box.castShadow = true;
|
||||
box.receiveShadow = true;
|
||||
box.position.set(0, 2, 0);
|
||||
box.add(target);
|
||||
three.scene.add(box);
|
||||
|
||||
const sphereGeo = new THREE.SphereGeometry(0.5, 32, 16);
|
||||
const sphere = new THREE.Mesh(sphereGeo, material);
|
||||
sphere.castShadow = true;
|
||||
sphere.receiveShadow = true;
|
||||
sphere.position.set(-2, 2, 0);
|
||||
|
||||
// target.position.set(0, 2, 0);
|
||||
target.add(sphere);
|
||||
three.scene.add(target);
|
||||
```
|
||||
|
||||
<Image
|
||||
src="/images/p/object-around-in-threejs/around.svg"
|
||||
alt="围绕运动"
|
||||
width="664"
|
||||
height="404"
|
||||
/>
|
||||
|
||||
### 相对位置
|
||||
|
||||
如果被围绕的物体没有通过 `add()` 方法添加 Object3D,那么就需要手动添加 Object3D 到场景中,并设置到特定的位置。可以通过其 `position.set()` 方法设置。这对于 Object3D 本身来说是相对于世界坐标位置的。
|
||||
|
||||
如果被围绕物体已经添加到场景中,通过 `add()` 方法添加 Object3D 也会被添加到场景中。且它的位置与父节点相同。
|
||||
|
||||
对于围绕物体来说,其自身被 Object3D 通过 `add()` 方法添加到 Object3D 中。而它通过 `position.set()` 进行设置时,就是相对与 Object3D 已经设置过的位置进行设置。
|
||||
|
||||
```tsx
|
||||
box.position.set(0, 2, 0);
|
||||
box.add(target);
|
||||
target.add(sphere);
|
||||
sphere.position.set(-2, 0, 0);
|
||||
// sphere position in world is (-2, 2, 0);
|
||||
```
|
||||
|
||||
### 获取世界位置
|
||||
|
||||
默认情况下 `Mesh.position` 得到是其自身的相对位置。获取世界全局位置需要一个 Vector3 来保存,再使用 `Mesh.getWorldPosition` 来获取。
|
||||
|
||||
```tsx
|
||||
const position = new THREE.Vector3();
|
||||
earth.getWorldPosition(position);
|
||||
```
|
||||
|
||||
## Demo
|
||||
|
||||
<RUACodeSandbox url="https://codesandbox.io/s/arounding-box-b1g4qq" />
|
167
content/posts/public-key-cryptgraphy.mdx
Normal file
167
content/posts/public-key-cryptgraphy.mdx
Normal file
@ -0,0 +1,167 @@
|
||||
---
|
||||
title: 公开密钥密码学-非对称加密 🔑
|
||||
date: '2022-10-24'
|
||||
tags: Linux
|
||||
---
|
||||
|
||||
<Image
|
||||
src="/images/p/public-key-cryptgraphy/logo.webp"
|
||||
alt=""
|
||||
priority
|
||||
width="1600"
|
||||
height="900"
|
||||
/>
|
||||
|
||||
GPG/PGP 赛高!
|
||||
|
||||
## 什么是非对称加密?
|
||||
|
||||
人类的历史上加密走了很长的一段路程。想尽了各种办法来保护自己那不想让不该知道的人知道的东西。 加密这东西,在密码学中最直白的解释就是将一般的明文信息改变为难以读取的内容,使其不可读的过程只有拥有解密方法的对象,经由解密过程,才能将密文还原为正常可读的内容。
|
||||
|
||||
大概在 1970 年代中期,所谓的“强加密”的使用开始从政府保密机构延申至公共的领域了,也就是说开始到我们大众都开始接触了。当今世界,加密已经是我们的日常生活中常常见到的东西了。
|
||||
|
||||
例如我们常常访问的带有 SSL/TLS 的网站,这也是非对称加密的一种。所谓的对称加密,它也是密码学中的一种。但他与对称加密不同的是,它需要两个密钥,一个是公开密钥,另一个是私有密钥;一个用作加密,另一个则用作解密。使用其中一个密钥把明文加密后所得的密文,只能用相对应的另一个密钥才能解密得到原本的明文;甚至连最初用来加密的密钥也不能用作解密。
|
||||
|
||||
由于加密和解密需要两个不同的密钥,故被称为非对称加密; 不同于加密和解密都使用同一个密钥的对称加密。虽然两个密钥在数学上相关,但如果知道了中一个,并不能凭此计算出另外一个;因此其中一个可以公开,称为公钥,任意向外发布;不公开的密钥为私钥,必须由用户自行严格秘密保管,绝不透过任途径向任何人提供,也不会透露给被信任的要通信的另一方。
|
||||
|
||||
### 加密
|
||||
|
||||
如果任何人使用公钥加密明文,得到的密文可以透过不安全的途径(如网络)发送,只有对应的私钥持有者才可以解密得到明文;其他人即使从网络上窃取到密文及加密公钥,也无法(在数以年计的合理时间内)解密得出明文。
|
||||
|
||||
典型例子是在网络银行或购物网站上,因为客户需要输入敏感消息,浏览器连接时使用网站服务器提供的公钥加密并上传数据,可保证只有信任的网站服务器才能解密得知消息,不必担心敏感个人信息因为在网络上传送而被窃取。
|
||||
|
||||
在现实世界上可作比拟的例子是,一个传统保管箱,开门和关门都是使用同一条钥匙,这是对称加密;而一个公开的邮箱,投递口是任何人都可以寄信进去的,这可视为公钥;而只有信箱主人拥有钥匙可以打开信箱,这就视为私钥。
|
||||
|
||||
常见的公钥加密算法有:RSA、ElGamal、背包算法、Rabin(RSA 的特例)、迪菲-赫尔曼密钥交换协议中的公钥加密算法、椭圆曲线加密算法(英语:Elliptic Curve Cryptography, ECC)。使用最广泛的是 RSA 算法(由发明者 Rivest、Shmir 和 Adleman 姓氏首字母缩写而来)是著名的公开秘钥加密算法,ElGamal 是另一种常用的非对称加密算法。
|
||||
|
||||
#### 加密过程
|
||||
|
||||
直白的解释:Tom 和 Jerry 想发送一些消息/文件,而不被隔壁的 Spike 知道文件的内容。于是它们机智的采用了非对称加密来保证内容的安全性。
|
||||
|
||||
1. Tom 先生成非对称的两个密钥,分别为公钥 A,私钥 B
|
||||
2. 为了能让 Jerry 发过来的消息被加密了,Tom 先将可公开的公钥 A 发给 Jerry
|
||||
3. 因为公钥 A 是完全可公开的,所以 Spike 知道也没关系
|
||||
4. Jerry 收到 Tom 发的公钥 A,并将自己的文件 X 使用公钥 A 进行加密
|
||||
5. 随后 Jerry 就可以将加密的文件 A(X) 正大光明的发送给 Tom 了
|
||||
6. 此时的 Spike 就算截取到加密过的文件 A(X) 也没有用
|
||||
7. 因为 Tom 收到的加密文件 A(X) 只有它自己的私钥 B 能够解密,于是它收到后可以使用私钥 B 正常解密
|
||||
8. 所以如果 Tom 丢失了它的私钥 B,那么 Tom and Jerry 都无法读取加密的文件 A(X) 了
|
||||
9. (没有私钥就无法解开公钥加密过的信息)
|
||||
10. 相反,Jerry 也可以将自己的公钥发给 Tom,使其加密要发给自己的信息。
|
||||
|
||||
#### 数字签名
|
||||
|
||||
如果某一用户使用他的私钥加密明文,任何人都可以用该用户的公钥解密密文;由于私钥只由该用户自己持有,故可以肯定该文件必定出自于该用户。
|
||||
|
||||
公众可以验证该用户发布的数据或文件是否完整、中途有否曾被篡改,接收者可信赖这条信息确实来自于该用户,该用户亦无法抵赖,这被称作数字签名。 所以我们常常见到提示一定要保护好自己的私钥,因为不仅仅会使得加密失效,还会直接影响签名验证。
|
||||
|
||||
## 非对称加密的软件
|
||||
|
||||
对于软件来说,我们可能经常听说到 GPG 这一词。GPG 的全称是 GNU Privacy Guard(GnuPG 或 GPG)。它是一款非对称加密的软件,是 PGP 加密软件的满足 GPL 的替代物。 也就是说它相对于 PGP 加密来说,它是一款开源软件。
|
||||
|
||||
因为 PGP 的非对称的算法是开源的,所以 GPG 和 PGP 原理是完全一样的。通常我们会见到 GPG/PGP。 所以 PGP 就可以简单了解到它是一款非开源的非对称加密软件了。 PGP(英语:Pretty Good Privacy,中文翻译“优良保密协议”)是一套用于讯息加密、验证的应用程序,采用 IDEA 的散列算法作为加密和验证之用。
|
||||
|
||||
### 多平台的安装与使用
|
||||
|
||||
既然上述已经介绍了它是自由软件,那么它跨平台的几率就很大了,支持的平台也非常的多。在官方网站里,我们可以看到它支持很多平台。
|
||||
|
||||
<Image
|
||||
src="/images/p/public-key-cryptgraphy/GPG多平台.webp"
|
||||
alt="GPG多平台"
|
||||
width="533"
|
||||
height="373"
|
||||
/>
|
||||
|
||||
#### Windows GPG4win
|
||||
|
||||
安装就不再多说,GPG4win 的官网有打包好的 exe 可执行程序,我们直接下载双击安装就好,安装过程也非常的简单,不需要进行任何配置。也就是常说的“无脑 next☀”。
|
||||
|
||||
[Download](https://www.gpg4win.org/get-gpg4win.html)
|
||||
|
||||
GPG4win 是 GPG 在 Windows 平台的一款可视化的非对称加密软件。对于可视化的软件来说,使用也非常的简单明了。 几乎常用的一些功能都非常直白的写在了开打的页面中。基本上只要使用者了解大概的非对称加密的运作原理,就可以很轻松的使用该软件了。
|
||||
|
||||
<Image
|
||||
src="/images/p/public-key-cryptgraphy/Kleopatra.webp"
|
||||
alt="Kleopatra"
|
||||
width="1016"
|
||||
height="539"
|
||||
/>
|
||||
|
||||
<Image
|
||||
src="/images/p/public-key-cryptgraphy/密钥.webp"
|
||||
alt="密钥"
|
||||
width="1016"
|
||||
height="539"
|
||||
/>
|
||||
|
||||
#### Ubuntu & CentOS
|
||||
|
||||
目前最新的 Ubuntu 与 CentOS 的发行版中都带有 GnuPrivacyGuard。也就是 GPG 的一种,所以使用的方法也是大同小异了。 以 Ubuntu 为例: \* 创建密钥
|
||||
|
||||
```
|
||||
gpg --gen-key
|
||||
```
|
||||
|
||||
不知道为啥我的机器在生成密钥的时候会卡住很长时间,导致我没有生成出来。等以后再考虑填这个坑吧。
|
||||
|
||||
- 查看秘钥
|
||||
|
||||
```
|
||||
查看公钥:gpg --list-key
|
||||
查看私钥:gpg --list-secret-keys
|
||||
```
|
||||
|
||||
- 提取秘钥
|
||||
|
||||
```
|
||||
提取公钥:gpg -a --export newkey > newkey.asc
|
||||
提取私钥:gpg -a --export-secret-keys newkey > newkey_pirv.asc
|
||||
```
|
||||
|
||||
- 导入秘钥
|
||||
|
||||
```
|
||||
导入公钥或私钥:gpg --import newkey
|
||||
```
|
||||
|
||||
- 使用公钥加密
|
||||
|
||||
```
|
||||
gpg -ea -r newkey filename
|
||||
```
|
||||
|
||||
- 解密并导出为文件
|
||||
|
||||
```
|
||||
gpg -d test.asc > test
|
||||
```
|
||||
|
||||
- 设置密钥信任程度
|
||||
|
||||
```
|
||||
gpg --edit-key [导入的密钥ID]
|
||||
trust
|
||||
您是否相信这位用户有能力验证其他用户密钥的有效性(查对身份证、通过不同的渠道检查
|
||||
指纹等)?
|
||||
1 = 我不知道或我不作答
|
||||
2 = 我不相信
|
||||
3 = 我勉强相信
|
||||
4 = 我完全相信
|
||||
5 = 我绝对相信
|
||||
m = 回到主菜单
|
||||
```
|
||||
|
||||
## 我的公钥
|
||||
|
||||
如果有小伙伴想和我扮演 Tom and Jerry 的话,或者想校验我的签名的文件的话。欢迎使用下述公钥
|
||||
|
||||
[我的公钥 🔒!](https://1drv.ms/u/s!ArC4gW7Dc7wWhd5PD8R_o6Mmhp2LxA?e=Ivpa8X)
|
||||
|
||||
## 参考
|
||||
|
||||
- [传输层安全性协议](https://zh.wikipedia.org/wiki/%E5%82%B3%E8%BC%B8%E5%B1%A4%E5%AE%89%E5%85%A8%E6%80%A7%E5%8D%94%E5%AE%9A)
|
||||
- [对称密钥加密](https://zh.wikipedia.org/wiki/對稱密鑰加密)
|
||||
- [公开密钥加密](https://zh.wikipedia.org/wiki/公开密钥加密)
|
||||
- [GnuPG](https://zh.wikipedia.org/wiki/GnuPG)
|
||||
- [PGP](https://zh.wikipedia.org/wiki/PGP)
|
||||
- [GnuPrivacyGuardHowto](https://help.ubuntu.com/community/GnuPrivacyGuardHowto)
|
179
content/posts/react18-new-hooks.mdx
Normal file
179
content/posts/react18-new-hooks.mdx
Normal file
@ -0,0 +1,179 @@
|
||||
---
|
||||
title: React 18 中的一些新 hooks
|
||||
date: '2022-10-11'
|
||||
tags: [React, TypeScript]
|
||||
---
|
||||
|
||||
<Image
|
||||
src="/images/p/react18-new-hooks/react-hooks.jpg"
|
||||
alt=""
|
||||
priority
|
||||
width="811"
|
||||
height="541"
|
||||
/>
|
||||
|
||||
## useTransition
|
||||
|
||||
返回一个状态值表示过渡任务的等待状态,以及一个启动该过渡任务的函数。它和 CSS 的过渡没有任何关系。useTransition 的主要目的是作用于在复杂的过渡任务时提供一个优先级较低的更新,过渡任务中触发的更新会让更紧急地更新先进行,比如点击。
|
||||
|
||||
过渡任务中的更新将不会展示由于再次挂起而导致降级的内容。这个机制允许用户在 React 渲染更新的时候继续与当前内容进行交互。
|
||||
|
||||
这里渲染一个长度为 10 万的列表和两个不同的按钮,第一个按钮会增加列表的长度并使第一个状态数值加一。第二个按钮只会增加自己的状态。
|
||||
|
||||
如果在这里设置状态时不使用 `setTransiton` ,那么在整个列表进行渲染时将无法处理其他任何操作。
|
||||
|
||||
在使用了 `setTransition` 后,整个列表进行渲染操作优先级将会比其他更紧急地操作低,从而可以响应第二个按钮点击。
|
||||
|
||||
`useTransition` 返回的元组中包含两个值 `[pending, setTransiton]` ,分别是 `setTransiton` 方法和表示正在过渡的状态 `pending` 。如果需要在过渡时展示特定的 UI 就可以使用 `pending` 来控制状态。
|
||||
|
||||
<RUASandpack
|
||||
template="react-ts"
|
||||
files={{
|
||||
'/Button.tsx': {
|
||||
code: sandpack.common.Button,
|
||||
hidden: true,
|
||||
},
|
||||
'/App.tsx': sandpack['react18-new-hooks'].useTransition,
|
||||
}}
|
||||
/>
|
||||
|
||||
## useDeferredValue
|
||||
|
||||
`useDeferredValue` 接受一个状态值,并返回该的副本。返回的副本状态值将会在其他紧急更新后更新。它与 `useTransition` 比较类似,二者可以搭配使用。
|
||||
|
||||
因为函数的原子性,在整个组件更新(重新渲染)时,子组件也会随着一起更新。这里通过设置了状态 `value` 来对整个组件重新渲染,即使下方的列表没有任何变化也会一起重新渲染。而重新渲染 UI 界面对我们来说就是紧急更新。
|
||||
|
||||
将原本的状态值 `value` 与 `useDeferredValue` 返回的副本相比较就会发现 `value` 会随着 UI 一起被更新,而被延迟的状态 `deferred` 会等待 UI 更新结束后再做更新。
|
||||
|
||||
<RUASandpack
|
||||
template="react-ts"
|
||||
files={{
|
||||
'/Button.tsx': {
|
||||
code: sandpack.common.Button,
|
||||
hidden: true,
|
||||
},
|
||||
'/App.tsx': sandpack['react18-new-hooks'].useDeferredValue,
|
||||
}}
|
||||
/>
|
||||
|
||||
## useId
|
||||
|
||||
`useId` 与其他的都不同,从名字就能看出来它的作用,返回一个在整个 APP 中唯一的 ID。它能够保证每个组件调用它返回的 ID 都唯一,即使是同一个组件被父组件多次调用。
|
||||
|
||||
通常的作用有:
|
||||
|
||||
- 为页面中需要唯一 ID 元素提供 ID,例如 `<label for="ID">` 。
|
||||
- SSR 到客户端注入时需要 ID 避免错误。
|
||||
|
||||
<RUASandpack
|
||||
template="react-ts"
|
||||
files={{
|
||||
'/Input.tsx': {
|
||||
code: sandpack.common.Input,
|
||||
hidden: true,
|
||||
},
|
||||
'/App.tsx': sandpack['react18-new-hooks'].useId,
|
||||
}}
|
||||
/>
|
||||
|
||||
## useSyncExternalStore
|
||||
|
||||
`useSyncExternalStore` 是目前最适合用来取代全局状态库的 hook,尤其是配合 redux 的思想后,简直就是手写的 mini redux。
|
||||
|
||||
首先看下该钩子的前面,其实不是很复杂。它接受一个范型,用于推断返回的状态。剩下的三个参数分别是:
|
||||
|
||||
- `subscribe`: 一个当状态更新后可执行的回调函数。该函数会收到一个回调函数,这个回调函数就是当状态后执行,React 用来对比是否需要重新渲染组件。
|
||||
- `getSnapshot`: 返回当前状态的函数。
|
||||
- `getServerSnapshot`: 在服务端渲染时返回当前状态的函数,可选。
|
||||
|
||||
```tsx
|
||||
useSyncExternalStore<State>(subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => State, getServerSnapshot?: (() => State) | undefined): State
|
||||
```
|
||||
|
||||
`useSyncExternalStore` 在组件中使用与 `useSelector` 很相似:
|
||||
|
||||
```tsx
|
||||
const { count, info } = useSyncExternalStore(
|
||||
store.subscribe,
|
||||
store.getSnapshot
|
||||
);
|
||||
```
|
||||
|
||||
但它的重点还是在 store 上:
|
||||
|
||||
```tsx
|
||||
export type State = {
|
||||
count: number;
|
||||
info: string;
|
||||
};
|
||||
export type Store = {
|
||||
state: State;
|
||||
setState: (
|
||||
stateOrFn: Partial<State> | ((state: State) => Partial<State>)
|
||||
) => void;
|
||||
subscribe: (listener: () => void) => () => void;
|
||||
listeners: Set<() => void>;
|
||||
getSnapshot: () => State;
|
||||
};
|
||||
|
||||
const store: Store = {
|
||||
state: {
|
||||
count: 0,
|
||||
info: 'Hello',
|
||||
},
|
||||
setState(stateOrFn) {
|
||||
const newState =
|
||||
typeof stateOrFn === 'function' ? stateOrFn(store.state) : stateOrFn;
|
||||
store.state = {
|
||||
...store.state,
|
||||
...newState,
|
||||
};
|
||||
store.listeners.forEach((listener) => listener());
|
||||
},
|
||||
listeners: new Set(),
|
||||
subscribe(listener) {
|
||||
store.listeners.add(listener);
|
||||
return () => {
|
||||
store.listeners.delete(listener);
|
||||
};
|
||||
},
|
||||
getSnapshot() {
|
||||
return store.state;
|
||||
},
|
||||
};
|
||||
|
||||
export default store;
|
||||
```
|
||||
|
||||
其中, `listeners` 用于存放 `subscribe` 的回调,在我们更新状态后需要通知 React 来更新组件。所以在 `setState` 中遍历执行。之所以使用 `Set()` 是因为 `subscribe` 还需要返回一个函数用于注销 `listener` 。
|
||||
|
||||
<RUASandpack
|
||||
template="react-ts"
|
||||
files={{
|
||||
'/Input.tsx': {
|
||||
code: sandpack.common.Input,
|
||||
hidden: true,
|
||||
},
|
||||
'/Button.tsx': {
|
||||
code: sandpack.common.Button,
|
||||
hidden: true,
|
||||
},
|
||||
'/store.ts': sandpack['react18-new-hooks'].store,
|
||||
'/App.tsx': sandpack['react18-new-hooks'].useSyncExternalStore,
|
||||
}}
|
||||
/>
|
||||
|
||||
## useInsertionEffect
|
||||
|
||||
`useInsertionEffect` 在通常情况下都用不上,它的唯一目的是对 CSS-in-JS 库很重要。
|
||||
|
||||
CSS-in-JS 库通常需要在运行时插入或修改 `<style>` 标签等。当 CSS 规则被添加或删除时,浏览器必须检查这些规则是否适用于现有的 DOM 树。它必须重新计算所有的样式规则并重新应用它们--而不仅仅是改变了的那些。如果 React 发现另一个组件也产生了一个新的规则,同样的过程会再次发生。
|
||||
|
||||
解决这个问题的最好办法就是所有东西呈现给浏览器绘制前就进行改变。没错 `useInsertionEffect` 与 `useEffect` 有着同样的签名,但它会同步的在所有 DOM 更改之前触发。比 `useLayoutEffect` 还要早触发,这样就可以用于在重绘之前注入样式。
|
||||
|
||||
<RUASandpack
|
||||
template="react-ts"
|
||||
files={{
|
||||
'/App.tsx': sandpack['react18-new-hooks'].useInsertionEffect,
|
||||
}}
|
||||
/>
|
178
content/posts/setting-up-docsearch-for-nextjs.mdx
Normal file
178
content/posts/setting-up-docsearch-for-nextjs.mdx
Normal file
@ -0,0 +1,178 @@
|
||||
---
|
||||
title: Setting up DocSearch for next.js
|
||||
date: '2022-04-18'
|
||||
tags: ['Next.js', 'JavaScript']
|
||||
---
|
||||
|
||||
I use next.js and mdx plugin to build my blog site. It's a next.js SSG project.
|
||||
|
||||
Also it's a JAMStack site. So i need a extenal search engine.
|
||||
|
||||
The Algolia is my first choice. We can build our own Algolia front UI, or use [DocSearch](https://github.com/algolia/docsearch)
|
||||
|
||||
## Purpose
|
||||
|
||||
Algolia split DocSearch into to parts:
|
||||
|
||||
- A cralwer to crawl our sites.
|
||||
|
||||
- A frontend UI liburary to show search result.
|
||||
|
||||
In legacy edition, Algolia provide a docsearch-scraper to build our own crawler.
|
||||
|
||||
Although it's still can plug it to DocSearch v3. But now it's deprecated.
|
||||
|
||||
They introduct the [Algolia Crawler web interface](https://crawler.algolia.com/admin/users/login) to manage the crawler.
|
||||
|
||||
But i can't login with my Algolia account.
|
||||
|
||||
<Image
|
||||
src={
|
||||
'/images/p/setting-up-docsearch-for-nextjs/cannot-login-to-algolia-crawler.png'
|
||||
}
|
||||
alt="Can't login to Algolia Crawler"
|
||||
width="586"
|
||||
height="131"
|
||||
/>
|
||||
|
||||
So i need find another way to generate my post index.
|
||||
|
||||
## Index format
|
||||
|
||||
The DocSearch frontend UI read result as specific format. We just need to provide the same format to DocSearch.
|
||||
|
||||
Then DocSearch fronted UI can works.
|
||||
|
||||
<Image
|
||||
src={'/images/p/setting-up-docsearch-for-nextjs/index-format.png'}
|
||||
alt="Index format"
|
||||
width="516"
|
||||
height="197"
|
||||
/>
|
||||
|
||||
So we need post same format to Algolia.
|
||||
|
||||
## Push our data
|
||||
|
||||
Algolia provide JavaScript API Client to push data to Algolia.
|
||||
|
||||
<Tab defaultValue="yarn">
|
||||
<TabItem label="yarn" value="yarn">
|
||||
<pre>yarn add algoliasearch</pre>
|
||||
</TabItem>
|
||||
<TabItem label="npm" value="npm">
|
||||
<pre>npm install algoliasearch</pre>
|
||||
</TabItem>
|
||||
</Tab>
|
||||
|
||||
The client will help us push data to Algolia. We just need to prepare out data.
|
||||
|
||||
### Docsearch format
|
||||
|
||||
Because Docsearch read result as specific format. our data need to be like this:
|
||||
|
||||
```js
|
||||
{
|
||||
content: null,
|
||||
hierarchy: {
|
||||
lvl0: 'Post',
|
||||
lvl1: slug,
|
||||
lvl2: heading,
|
||||
},
|
||||
type: 'lvl2',
|
||||
objectID: 'id',
|
||||
url: 'url',
|
||||
}
|
||||
```
|
||||
|
||||
### Generate format
|
||||
|
||||
For generate our data, we need:
|
||||
|
||||
- [dotenv](https://www.npmjs.com/package/dotenv): read Algolia app ID and admin key in `.env` file.
|
||||
- [algoliasearch](https://www.npmjs.com/package/algoliasearch): JavaScript API client.
|
||||
- `fs` and `path`: read post file.
|
||||
- [nanoid](https://www.npmjs.com/package/nanoid) (optional): generate unique `objectID`.
|
||||
|
||||
For use ECMAScript `import`, we need set file suffix with `.mjs`. The node.js can use `import` statement.
|
||||
|
||||
```js
|
||||
// build-search.mjs
|
||||
|
||||
import { config } from 'dotenv';
|
||||
import algoliasearch from 'algoliasearch/lite.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { nanoid } from 'nanoid';
|
||||
```
|
||||
|
||||
Next, read post content from file. First we need read whole content from the file:
|
||||
|
||||
```js
|
||||
const files = fs.readdirSync(path.join('pages/p'));
|
||||
```
|
||||
|
||||
Then, prepare a empty array to store post data. And traverse content to generate format we need.
|
||||
|
||||
```js
|
||||
const myPosts = [];
|
||||
files.map((f) => {
|
||||
const content = fs.readFileSync(path.join('pages/p', f), 'utf-8');
|
||||
// const { content: meta, content } = matter(markdownWithMeta);
|
||||
|
||||
const slug = f.replace(/\.mdx$/, '');
|
||||
const regex = /^#{2}(?!#)(.*)/gm;
|
||||
|
||||
content.match(regex)?.map((h) => {
|
||||
const heading = h.substring(3);
|
||||
|
||||
myPosts.push({
|
||||
content: null,
|
||||
hierarchy: {
|
||||
lvl0: 'Post',
|
||||
lvl1: slug,
|
||||
lvl2: heading,
|
||||
},
|
||||
type: 'lvl2',
|
||||
objectID: `${nanoid()}-https://rua.plus/p/${slug}`,
|
||||
url: `https://rua.plus/p/${slug}#${heading
|
||||
.toLocaleLowerCase()
|
||||
.replace(/ /g, '-')}`,
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
The `type` property means level of table of contents.
|
||||
|
||||
I just need h2 title in search result. So just match them with `/^#{2}(?!#)(.*)/gm`.
|
||||
|
||||
And post title is the `lvl1` type:
|
||||
|
||||
```js
|
||||
myPosts.push({
|
||||
content: null,
|
||||
hierarchy: {
|
||||
lvl0: 'Post',
|
||||
lvl1: slug,
|
||||
},
|
||||
type: 'lvl1',
|
||||
objectID: `${nanoid()}-https://rua.plus/p/${slug}`,
|
||||
url: `https://rua.plus/p/${slug}`,
|
||||
});
|
||||
```
|
||||
|
||||
### Push to Algolia
|
||||
|
||||
Algolia API is easy to use. First we need specify the index name.
|
||||
|
||||
```js
|
||||
const index = client.initIndex('RUA');
|
||||
```
|
||||
|
||||
And save the objects.
|
||||
|
||||
```js
|
||||
const algoliaResponse = await index.replaceAllObjects(posts);
|
||||
```
|
||||
|
||||
All done!
|
302
content/sandpack/build-own-store-with-usesyncexternalstore.ts
Normal file
302
content/sandpack/build-own-store-with-usesyncexternalstore.ts
Normal file
@ -0,0 +1,302 @@
|
||||
export const multi = `import { useSyncExternalStore } from 'react';
|
||||
|
||||
export type SetState<S> = (stateOrFn: S | ((state: S) => S)) => void;
|
||||
export type GetSnapshot<S> = () => S;
|
||||
export type Subscribe = (listener: () => void) => () => void;
|
||||
export type CreateStore = <T extends Record<string, unknown> | unknown[]>(
|
||||
initialState: T
|
||||
) => {
|
||||
getSnapshot: GetSnapshot<T>;
|
||||
setState: SetState<T>;
|
||||
subscribe: Subscribe;
|
||||
};
|
||||
|
||||
export const createStore: CreateStore = <
|
||||
T extends Record<string, unknown> | unknown[]
|
||||
>(
|
||||
initialState: T
|
||||
) => {
|
||||
let state = initialState;
|
||||
|
||||
const listeners = new Set<() => void>();
|
||||
const getSnapshot = () => state;
|
||||
const setState: SetState<T> = (stateOrFn) => {
|
||||
state = typeof stateOrFn === 'function' ? stateOrFn(state) : stateOrFn;
|
||||
listeners.forEach((listener) => listener());
|
||||
};
|
||||
const subscribe = (listener: () => void) => {
|
||||
listeners.add(listener);
|
||||
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
getSnapshot,
|
||||
setState,
|
||||
subscribe,
|
||||
};
|
||||
};
|
||||
|
||||
export type Todo = {
|
||||
id: number;
|
||||
content: string;
|
||||
}[];
|
||||
const initialTodo: Todo = [
|
||||
{ id: 0, content: 'React' },
|
||||
{ id: 1, content: 'Vue' },
|
||||
];
|
||||
const todoStore = createStore(initialTodo);
|
||||
export const useTodoStore = (): [Todo, SetState<Todo>] => [
|
||||
useSyncExternalStore(todoStore.subscribe, todoStore.getSnapshot),
|
||||
todoStore.setState,
|
||||
];
|
||||
|
||||
type Count = {
|
||||
count: number;
|
||||
info: string;
|
||||
};
|
||||
const initialCount: Count = {
|
||||
count: 0,
|
||||
info: 'Hello',
|
||||
};
|
||||
const countStore = createStore(initialCount);
|
||||
export const useCountStore = (): [Count, SetState<Count>] => [
|
||||
useSyncExternalStore(countStore.subscribe, countStore.getSnapshot),
|
||||
countStore.setState,
|
||||
];`;
|
||||
|
||||
export const miniRedux = `import { useSyncExternalStore } from 'react';
|
||||
|
||||
export type RUAState = Record<string, unknown> | unknown[];
|
||||
export type RUAAction<P = unknown, T extends string = string> = {
|
||||
payload: P;
|
||||
type: T;
|
||||
};
|
||||
export type RUAReducer<S extends RUAState, A extends RUAAction> = (
|
||||
state: S,
|
||||
action: A
|
||||
) => S;
|
||||
export type RUADispatch<A extends RUAAction> = (action: A) => void;
|
||||
export type GetSnapshot<S> = () => S;
|
||||
export type Subscribe = (listener: () => void) => () => void;
|
||||
|
||||
export const createStore = <S extends RUAState, A extends RUAAction>(
|
||||
reducer: RUAReducer<S, A>,
|
||||
initialState: S
|
||||
) => {
|
||||
let state = initialState;
|
||||
|
||||
const listeners = new Set<() => void>();
|
||||
const getSnapshot = () => state;
|
||||
const dispatch: RUADispatch<A> = (action) => {
|
||||
state = reducer(state, action);
|
||||
listeners.forEach((listener) => listener());
|
||||
};
|
||||
const subscribe = (listener: () => void) => {
|
||||
listeners.add(listener);
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
getSnapshot,
|
||||
dispatch,
|
||||
};
|
||||
};
|
||||
|
||||
export type Todo = {
|
||||
id: number;
|
||||
content: string;
|
||||
}[];
|
||||
const initialTodo: Todo = [
|
||||
{ id: 0, content: 'React' },
|
||||
{ id: 1, content: 'Vue' },
|
||||
];
|
||||
export type TodoAction = RUAAction<number | string, 'add' | 'delete'>;
|
||||
|
||||
const reducer: RUAReducer<Todo, TodoAction> = (state, action) => {
|
||||
switch (action.type) {
|
||||
case 'add': {
|
||||
if (action.payload == null) throw new Error('Add todo without payload!');
|
||||
return [
|
||||
...state,
|
||||
{
|
||||
id: state[state.length - 1].id + 1,
|
||||
content: action.payload.toString(),
|
||||
},
|
||||
];
|
||||
}
|
||||
case 'delete': {
|
||||
if (action.payload == null)
|
||||
throw new Error('Delete todo without payload!');
|
||||
return state.filter((todo) => todo.id !== action.payload);
|
||||
}
|
||||
default:
|
||||
throw new Error('Dispatch a reducer without action!');
|
||||
}
|
||||
};
|
||||
|
||||
const todoStore = createStore(reducer, initialTodo);
|
||||
export const useTodoStore = (): [Todo, RUADispatch<TodoAction>] => [
|
||||
useSyncExternalStore(todoStore.subscribe, todoStore.getSnapshot),
|
||||
todoStore.dispatch,
|
||||
];`;
|
||||
|
||||
export const MultiStore = `import Button from './Button';
|
||||
import Input from './Input';
|
||||
import { useState } from 'react';
|
||||
import { useCountStore, useTodoStore } from './multi';
|
||||
|
||||
const Todo = () => {
|
||||
const [todos, setTodo] = useTodoStore();
|
||||
const [value, setValue] = useState('');
|
||||
const handleAdd = () => {
|
||||
if (!value) return;
|
||||
setTodo((d) => [...d, { id: d[d.length - 1].id + 1, content: value }]);
|
||||
setValue('');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<ul>
|
||||
{todos.map((todo) => (
|
||||
<div key={todo.id}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
className="flex items-center mb-2"
|
||||
>
|
||||
<li className="mr-2">{todo.content}</li>
|
||||
<Button
|
||||
onClick={() =>
|
||||
setTodo((d) => d.filter((item) => item.id !== todo.id))
|
||||
}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div>
|
||||
<Input
|
||||
type="text"
|
||||
className="mr-1"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
<Button onClick={handleAdd}>Add</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Count = () => {
|
||||
const [{ count, info }, setState] = useCountStore();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div>
|
||||
Count: <span>{count}</span>
|
||||
</div>
|
||||
<div>
|
||||
Info: <span>{info}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => setState((d) => ({ ...d, count: d.count + 1 }))}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
type="text"
|
||||
onChange={(e) => setState((d) => ({ ...d, info: e.target.value }))}
|
||||
value={info}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const MultiStore = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="p-4">
|
||||
<Todo />
|
||||
<hr className="my-4" />
|
||||
<Count />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MultiStore;`;
|
||||
|
||||
export const Reducer = `import Button from './Button';
|
||||
import Input from './Input';
|
||||
import { useState } from 'react';
|
||||
import { useTodoStore } from './store.ts';
|
||||
|
||||
const Reducer = () => {
|
||||
const [todos, dispatch] = useTodoStore();
|
||||
const [value, setValue] = useState('');
|
||||
const handleAdd = () => {
|
||||
if (!value) return;
|
||||
dispatch({
|
||||
type: 'add',
|
||||
payload: value,
|
||||
});
|
||||
setValue('');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="p-4">
|
||||
<div>
|
||||
<ul>
|
||||
{todos.map((todo) => (
|
||||
<div key={todo.id}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
className="flex items-center mb-2"
|
||||
>
|
||||
<li className="mr-2">{todo.content}</li>
|
||||
<Button
|
||||
onClick={() => dispatch({ type: 'delete', payload: todo.id })}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div>
|
||||
<Input
|
||||
type="text"
|
||||
className="mr-1"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
<Button onClick={handleAdd}>Add</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Reducer;`;
|
@ -0,0 +1,90 @@
|
||||
export const genericApp = `import "./styles.css";
|
||||
import Child from "./Child";
|
||||
|
||||
const testData = {
|
||||
name: "xfy",
|
||||
age: 18
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<h1>Hello CodeSandbox</h1>
|
||||
<h2>Start editing to see some magic happen!</h2>
|
||||
|
||||
<div>
|
||||
<Child data={testData} name="name" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}`;
|
||||
|
||||
export const genericChild = `import { useState } from "react";
|
||||
|
||||
type Props<T extends Record<string, unknown>> = {
|
||||
name: keyof T;
|
||||
data: T;
|
||||
};
|
||||
|
||||
const Child = <T extends Record<string, unknown>>({ name, data }: Props<T>) => {
|
||||
const [showName, setShowName] = useState<T[keyof T]>();
|
||||
const valid = () => {
|
||||
console.log(data[name]);
|
||||
setShowName(data[name]);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>{name}</div>
|
||||
<button onClick={valid}>Show {name}</button>
|
||||
|
||||
<div>{JSON.stringify(showName)}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Child;`;
|
||||
|
||||
export const hookApp = `import { useForm } from 'react-hook-form';
|
||||
|
||||
type Pet = 'Cat' | 'Dog';
|
||||
type FormData = {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
favorite: Pet;
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<FormData>();
|
||||
const onSubmit = handleSubmit((data) => console.log(data));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div>
|
||||
<label htmlFor="firstname">First name:</label>
|
||||
<input type="text" id="firstname" {...register('firstName')} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="lastname">Last name:</label>
|
||||
<input type="text" id="lastname" {...register('lastName')} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="favorite">Favorite pet:</label>
|
||||
<select id="favorite" {...register('favorite')}>
|
||||
<option value="cat">Cat</option>
|
||||
<option value="dog">Dog</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}`;
|
3
content/sandpack/hello-world.ts
Normal file
3
content/sandpack/hello-world.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const hello = `export default function App() {
|
||||
return <h1>Hello world</h1>
|
||||
}`;
|
116
content/sandpack/how-to-load-a-background-with-threejs.ts
Normal file
116
content/sandpack/how-to-load-a-background-with-threejs.ts
Normal file
@ -0,0 +1,116 @@
|
||||
export const firstScene = `import { useEffect, useRef } from 'react';
|
||||
import * as THREE from 'three';
|
||||
|
||||
export default function App() {
|
||||
const ref = useRef(null);
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(
|
||||
75,
|
||||
window.innerWidth / window.innerHeight,
|
||||
0.1,
|
||||
1000
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
canvas: ref.current,
|
||||
});
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
const render = (time) => {
|
||||
renderer.render(scene, camera);
|
||||
requestAnimationFrame(render);
|
||||
};
|
||||
requestAnimationFrame(render);
|
||||
function onWindowResize() {
|
||||
camera.aspect = window.innerWidth / window.innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
render(0);
|
||||
}
|
||||
window.addEventListener('resize', onWindowResize);
|
||||
return () => {
|
||||
window.removeEventListener('resize', onWindowResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<canvas ref={ref}></canvas>
|
||||
</>
|
||||
)
|
||||
}`;
|
||||
|
||||
export const loadBackground = `import { useEffect, useRef } from "react";
|
||||
import * as THREE from "three";
|
||||
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
|
||||
|
||||
const manager = new THREE.LoadingManager();
|
||||
manager.onProgress = (item, loaded, total) => {
|
||||
console.log(loaded, total);
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
const ref = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const scene = new THREE.Scene();
|
||||
const sky = new THREE.CubeTextureLoader(manager).load([
|
||||
"https://raw.githubusercontent.com/DefectingCat/three-playground/master/src/assets/images/corona/corona_ft.png",
|
||||
"https://raw.githubusercontent.com/DefectingCat/three-playground/master/src/assets/images/corona/corona_bk.png",
|
||||
"https://raw.githubusercontent.com/DefectingCat/three-playground/master/src/assets/images/corona/corona_up.png",
|
||||
"https://raw.githubusercontent.com/DefectingCat/three-playground/master/src/assets/images/corona/corona_dn.png",
|
||||
"https://raw.githubusercontent.com/DefectingCat/three-playground/master/src/assets/images/corona/corona_rt.png",
|
||||
"https://raw.githubusercontent.com/DefectingCat/three-playground/master/src/assets/images/corona/corona_lf.png"
|
||||
]);
|
||||
scene.background = sky;
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(
|
||||
75,
|
||||
window.innerWidth / window.innerHeight,
|
||||
0.1,
|
||||
1000
|
||||
);
|
||||
camera.position.set(0, 1, 0);
|
||||
camera.up.set(0, 0, 1);
|
||||
scene.add(camera);
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
canvas: ref.current
|
||||
});
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
const controls = new OrbitControls(camera, ref.current);
|
||||
controls.enablePan = false;
|
||||
controls.target.set(0, 0, 0);
|
||||
controls.update();
|
||||
const render = (time) => {
|
||||
renderer.render(scene, camera);
|
||||
requestAnimationFrame(render);
|
||||
};
|
||||
|
||||
requestAnimationFrame(render);
|
||||
function onWindowResize() {
|
||||
camera.aspect = window.innerWidth / window.innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
render(0);
|
||||
}
|
||||
window.addEventListener("resize", onWindowResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", onWindowResize);
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
<canvas ref={ref}></canvas>
|
||||
</>
|
||||
);
|
||||
}
|
||||
`;
|
||||
|
||||
export const resetStyles = `* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}`;
|
208
content/sandpack/index.ts
Normal file
208
content/sandpack/index.ts
Normal file
@ -0,0 +1,208 @@
|
||||
export const Button = `type ButtonProps = {} & React.DetailedHTMLProps<
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
HTMLButtonElement
|
||||
>;
|
||||
|
||||
const Button = ({ ...rest }: ButtonProps) => {
|
||||
const { children, className, ...props } = rest;
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className={\`rua-button \${className}\`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
|
||||
<style>
|
||||
{\`.rua-button {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
tab-size: 4;
|
||||
box-sizing: border-box;
|
||||
border-style: solid;
|
||||
border-color: #e5e7eb;
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
font-family: inherit;
|
||||
font-size: 100%;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
margin: 0;
|
||||
text-transform: none;
|
||||
-webkit-appearance: button;
|
||||
background-color: transparent;
|
||||
background-image: none;
|
||||
cursor: pointer;
|
||||
margin-right: 1rem;
|
||||
border-radius: 0.375rem;
|
||||
border-width: 1px;
|
||||
padding-left: 1.25rem;
|
||||
padding-right: 1.25rem;
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
.rua-button:focus {
|
||||
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
||||
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
|
||||
}
|
||||
\`}
|
||||
</style>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button;`;
|
||||
|
||||
export const Input = `type InputProps = {} & React.DetailedHTMLProps<
|
||||
React.InputHTMLAttributes<HTMLInputElement>,
|
||||
HTMLInputElement
|
||||
>;
|
||||
|
||||
const Input = ({ ...rest }: InputProps) => {
|
||||
const { className, ...props } = rest;
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
className={\`rua-input \${className}\`}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
<style>{\`
|
||||
.rua-input {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
tab-size: 4;
|
||||
box-sizing: border-box;
|
||||
border-style: solid;
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
font-family: inherit;
|
||||
font-size: 100%;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
margin: 0;
|
||||
margin-top: 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
border-width: 1px;
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(229 231 235 / var(--tw-border-opacity));
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(55 65 81 / var(--tw-text-opacity));
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
.rua-input:focus {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(96 165 250 / var(--tw-border-opacity));
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
||||
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
|
||||
--tw-ring-color: rgb(147 197 253 / var(--tw-ring-opacity));
|
||||
--tw-ring-opacity: 0.8;
|
||||
}\`}
|
||||
</style>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Input;`;
|
258
content/sandpack/react18-new-hooks.ts
Normal file
258
content/sandpack/react18-new-hooks.ts
Normal file
@ -0,0 +1,258 @@
|
||||
export const useTransition = `import Button from './Button';
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
const UseTransition = () => {
|
||||
const [value, setValue] = useState(0);
|
||||
const [value2, setValue2] = useState(-1);
|
||||
const [length, setLength] = useState(30000);
|
||||
const [pending, setTransiton] = useTransition();
|
||||
|
||||
const handleClick = () => {
|
||||
setValue((v) => v + 1);
|
||||
setTransiton(() => setLength((l) => l + 1));
|
||||
// setLength((l) => l + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="my-4">
|
||||
<Button onClick={handleClick}>{value}</Button>
|
||||
<Button onClick={() => setValue2((v) => v - 1)}>{value2}</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={\`wrapper \${pending && \`fade\`}\`}
|
||||
>
|
||||
{Array.from({ length }).map((_, i) => (
|
||||
<div className="p-2 mb-2 mr-2 rounded-md shadow" key={length - i}>
|
||||
{length - i}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<style>{\`
|
||||
.wrapper {
|
||||
transition: all 0.3 ease;
|
||||
}
|
||||
|
||||
.fade {
|
||||
opacity: 0.5;
|
||||
}\`}
|
||||
</style>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UseTransition;`;
|
||||
|
||||
export const useDeferredValue = `import Button from './Button';
|
||||
import { useDeferredValue, useState } from 'react';
|
||||
|
||||
const UseDeferredValue = () => {
|
||||
const [value, setValue] = useState(0);
|
||||
const deferred = useDeferredValue(value);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="my-4">
|
||||
<div>
|
||||
Deferred:
|
||||
<Button onClick={() => setValue((v) => v + 1)}>{deferred}</Button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
Primtive:
|
||||
<Button onClick={() => setValue((v) => v + 1)}>{value}</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{Array.from({ length: 30000 }).map((_, i) => (
|
||||
<div className="p-2 mb-2 mr-2 rounded-md shadow" key={i}>
|
||||
{i}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UseDeferredValue;`;
|
||||
|
||||
export const useId = `import Input from './Input';
|
||||
import { useId } from 'react';
|
||||
|
||||
const RUAForm = () => {
|
||||
const id = useId();
|
||||
|
||||
return (
|
||||
<>
|
||||
<label htmlFor={\`\${id}\`}>Label 1: </label>
|
||||
<div>
|
||||
<Input type="text" id={\`\${id}\`} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const UseId = () => {
|
||||
return (
|
||||
<>
|
||||
<RUAForm />
|
||||
<RUAForm />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UseId;`;
|
||||
|
||||
export const store = `export type State = {
|
||||
count: number;
|
||||
info: string;
|
||||
};
|
||||
export type Store = {
|
||||
state: State;
|
||||
setState: (
|
||||
stateOrFn: State | ((state: State) => State)
|
||||
) => void;
|
||||
subscribe: (listener: () => void) => () => void;
|
||||
listeners: Set<() => void>;
|
||||
getSnapshot: () => State;
|
||||
};
|
||||
|
||||
const store: Store = {
|
||||
state: {
|
||||
count: 0,
|
||||
info: 'Hello',
|
||||
},
|
||||
setState(stateOrFn) {
|
||||
const newState =
|
||||
typeof stateOrFn === 'function' ? stateOrFn(store.state) : stateOrFn;
|
||||
store.state = {
|
||||
...store.state,
|
||||
...newState,
|
||||
};
|
||||
store.listeners.forEach((listener) => listener());
|
||||
},
|
||||
listeners: new Set(),
|
||||
subscribe(listener) {
|
||||
store.listeners.add(listener);
|
||||
return () => {
|
||||
store.listeners.delete(listener);
|
||||
};
|
||||
},
|
||||
getSnapshot() {
|
||||
return store.state;
|
||||
},
|
||||
};
|
||||
|
||||
export default store;`;
|
||||
export const useSyncExternalStore = `import Button from './Button';
|
||||
import Input from './Input';
|
||||
import { useSyncExternalStore } from 'react';
|
||||
import store from './store';
|
||||
|
||||
const Couter = () => {
|
||||
const { count, info } = useSyncExternalStore(
|
||||
store.subscribe,
|
||||
store.getSnapshot
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div>
|
||||
Count: <span>{count}</span>
|
||||
</div>
|
||||
<div>
|
||||
Info: <span>{info}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => store.setState((d) => ({ ...d, count: d.count + 1 }))}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Infor = () => {
|
||||
const { count, info } = useSyncExternalStore(
|
||||
store.subscribe,
|
||||
store.getSnapshot
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div>
|
||||
Count: <span>{count}</span>
|
||||
</div>
|
||||
<div>
|
||||
Info: <span>{info}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Input
|
||||
type="text"
|
||||
onChange={(e) => store.setState((d) => ({ ...d, info: e.target.value }))}
|
||||
value={info}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const UseSyncExternalStore = () => {
|
||||
return (
|
||||
<>
|
||||
<Couter />
|
||||
<hr className="my-4" />
|
||||
<Infor />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UseSyncExternalStore;`;
|
||||
|
||||
export const useInsertionEffect = `import { useEffect, useLayoutEffect, useInsertionEffect } from 'react';
|
||||
|
||||
const Child = () => {
|
||||
useEffect(() => {
|
||||
console.log('useEffect child is called');
|
||||
});
|
||||
useLayoutEffect(() => {
|
||||
console.log('useLayoutEffect child is called');
|
||||
});
|
||||
useInsertionEffect(() => {
|
||||
console.log('useInsertionEffect child is called');
|
||||
});
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
const UseInsertionEffect = () => {
|
||||
useEffect(() => {
|
||||
console.log('useEffect app is called');
|
||||
});
|
||||
useLayoutEffect(() => {
|
||||
console.log('useLayoutEffect app is called');
|
||||
});
|
||||
useInsertionEffect(() => {
|
||||
console.log('useInsertionEffect app is called');
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Child />
|
||||
<div>Check console in DevTools</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UseInsertionEffect;`;
|
Reference in New Issue
Block a user