React Hooks

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性

特点:

  • Hook函数通常使用use开头
  • 函数式编程
  • 声明式逻辑

使用注意事项:

  • 只能在React函数组件中调用Hook
  • 只在最顶层使用Hook
  • 不能在if,for,switch等流程语句中使用

为什么要用Hooks?它解决了什么问题?

Hooks的好处有下面几点:

  1. 更好的逻辑复用
  2. 更简洁的代码
  3. 更好的性能
  4. 更好的拓展性,和其他库集成

解决了Class组件带来的一些问题:

  • 复杂组件的逻辑复用难度问题
  • 组件生命周期的混乱问题
  • this指向问题

useEffect使用

传递依赖的三种情况:

  • 不传递依赖项数组(每次组件渲染都会执行,相当于componentDidUpdate)
  • 传递一个空数组作为依赖 [] (相当于componentDidMount)
  • 传递一个或者多个值作为依赖(每次依赖的值变换后会重新执行回掉函数)

示例:

jsx
function App() {
// case 1 (ike componentDidMount)
useEffect(() => {

}, [])


// case 2 (componentDidUpdate)
useEffect(() => {

})

// case 3 (like vue3 watch, count change with exec Do something )
const [count, setCount] = useState(0)
useEffect(() => {
// Do something
}, [count])
}

其他常见Hook用法

useState

jsx
import { useState } from 'react'

function FavoriteColor() {
const [color, setColor] = useState("red");

return <h1>My favorite color is {color}!</h1>
}

更新机制:

useRef 使用

useRef 是 React 的一个 Hook,它能够在函数组件中存储一个可变的引用对象,该对象在组件的整个生命周期内保持不变。useRef
常用于以下几种情况:

  1. 访问 DOM 元素:当你需要直接访问 DOM 元素时,可以使用 useRef 创建一个引用,并将其赋给元素的 ref
    属性。这样,你就可以在组件中直接控制和访问该 DOM 元素。

  2. 保存可变值:如果你需要在组件的渲染周期之间保持一个可变的值,而这个值不应该触发组件的重新渲染,useRef 可以用来保存这个值。

下面是一些示例来说明 useRef 的使用:

示例 1:访问 DOM 元素

import React, { useEffect, useRef } from 'react';

function TextInputWithFocusButton() {
// 创建一个 ref 来存储 textInput 的 DOM 元素
const textInput = useRef(null);

// 点击按钮时将焦点设置到 input 元素上
const focusTextInput = () => {
// 直接访问 DOM 元素,并调用其 focus 方法
textInput.current.focus();
};

return (
<div>
<input type="text" ref={ textInput }/>
<button onClick={ focusTextInput }>Focus the text input</button>
</div>
);
}

export default TextInputWithFocusButton;

示例 2:保存可变值

import React, { useState, useRef, useEffect } from 'react';

function TimerComponent() {
// 用于显示计数器的 state
const [count, setCount] = useState(0);
// 使用 useRef 保存计数器的间隔 ID,不会触发重新渲染
const intervalRef = useRef(null);

// 开始计数器
const startTimer = () => {
// 设置间隔,并保存间隔 ID 到 ref
intervalRef.current = setInterval(() => {
setCount((prevCount) => prevCount + 1);
}, 1000);
};

// 停止计数器
const stopTimer = () => {
clearInterval(intervalRef.current);
};

// 组件卸载时清除计时器
useEffect(() => {
return () => {
clearInterval(intervalRef.current);
};
}, []); // 空依赖数组确保 effect 只在组件挂载和卸载时运行

return (
<div>
<h1>{ count }</h1>
<button onClick={ startTimer }>Start</button>
<button onClick={ stopTimer }>Stop</button>
</div>
);
}

export default TimerComponent;

在上面的计数器示例中,intervalRef 被用来保存定时器的 ID。这个 ID
是可变的,但是我们不希望它的改变导致组件的重新渲染,因此使用 useRef 来存储它是非常合适的。

总结一下,useRef 是一个非常有用的钩子,它能够帮助你在函数组件中管理 DOM 引用和存储跨渲染周期的可变值。

useMemo 使用

介绍:useMemo 是一个 React Hook,它在每次重新渲染的时候能够缓存计算的结果。
作用:如果useMemo的依赖项没有发生改变,它将返回上次缓存的值;否则将再次调用回掉函数,并返回最新结果。

PS: 类似Vue3的computed方法

jsx
function App() {
const [count, setCount] = useState(100)
const [count2, setCount2] = useState(200)

const memoizedValue = useMemo(() => {
return count + count2
}, [count, count2]);


const handleAdd = () => setCount(count + 1)
useEffect(() => {
console.log('memoizedValue', memoizedValue)
}, [memoizedValue]);
}

memo 使用

介绍:React.memo 是一个高阶组件,用于包裹函数组件
作用:当函数组件的Props没有发生变化时,React.memo 将会使用之前渲染的结果,从而避免不必要的重新渲染

jsx
const Greeting = memo(function Greeting({name}) {
return <h1>Hello, {name}!</h1>;
});

export function App() {
return <Greeting name={'Chris'}/>
}

React.memo和 PureComponent的区别?

React.memo 和 PureComponent 都是用于优化 React 应用性能的手段,它们通过减少不必要的渲染来提高组件的渲染效率。尽管它们的目的相同,但它们适用于不同类型的组件,并且内部比较的方式有所不同。

React.memo

React.memo 是一个高阶组件,它的作用是对函数组件进行性能优化。当组件的props没有发生变化时,React.memo 会阻止组件的重新渲染。React.memo
默认仅对props进行浅层比较,也就是说,它只会检查props的一级属性是否相等,如果一级属性都没有发生变化,那么组件就不会重新渲染。

如果你需要进行更深层次的比较,React.memo 允许你提供一个自定义的比较函数作为第二个参数,这个函数接收前后两次的props,并返回一个布尔值,指示它们是否相等。

const MyComponent = React.memo(function MyComponent(props) {
/* render using props */
}, areEqual);

PureComponent

PureComponent 是一个类组件,它继承自 React.Component。与 React.memo 类似,PureComponent
也会在组件的props或者state发生变化时才会进行重新渲染。不同的是,PureComponent
会对组件的props和state进行浅层比较,这意味着只有当props或state的一级属性发生变化时,组件才会重新渲染。

使用 PureComponent 可以通过继承 PureComponent 类来创建组件:

class MyComponent extends React.PureComponent {
render() {
// render logic
}
}

区别总结

  • 应用范围:React.memo 用于函数组件,而 PureComponent 用于类组件。
  • 比较方式:React.memo 和 PureComponent 都默认进行浅层比较,但 React.memo 可以通过第二个参数传入自定义比较函数实现深层比较。
  • 比较内容:PureComponent 会比较state和props,而 React.memo 只比较props。

选择使用 React.memo 还是 PureComponent 取决于你使用的组件类型(函数组件还是类组件)以及你对性能优化的需求。在大多数情况下,这两种方法可以有效减少不必要的渲染,从而提升应用的性能。

useCallback 使用

介绍:useCallback 是一个允许你在多次渲染中缓存函数的 React Hook。

作用:常用于减少不必要的子组件渲染,特别是当回掉函数作为props传递给子组件时

案例:如果count发生变化(useCallback的依赖),handleIncrement会被重新创建,如果count没有改变,即便MyComponent
组件被重新渲染,handleIncrement 的引用也会保持不变

简而言之,useCallback 在多次组件渲染中缓存一个函数,直至这个函数的依赖发生改变。

jsx
import React, { useState, useCallback } from 'react';

function MyComponent({onButtonClick}) {
const [count, setCount] = useState(0);

// 使用 useCallback 来记忆化这个回调函数
const handleIncrement = useCallback(() => {
setCount(count + 1);
}, [count]); // count 改变时,handleIncrement 会被重新创建

return (
<div>
Count: {count}
<button onClick={handleIncrement}>Increment</button>
<button onClick={onButtonClick}>Click me</button>
</div>
);
}

useContext 使用

useContext 是一个 React Hook,可以让你读取和订阅组件中的 context。

useContext不限层级传递数据

jsx

// App.tsx
import { createContext, useState } from 'react'

const ThemeContext = createContext('light');

function App() {
const [theme, setTheme] = useState('dark'); // 假设你要传递的主题是 'dark'

const handleChangeTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light')
}

return (
<ThemeContext.Provider value={theme}>
<ChildComponent changeTheme={handleChangeTheme}/>
</ThemeContext.Provider>
);
}

function ChildComponent({changeTheme}) {
const theme = useContext(ThemeContext); // 直接访问 ThemeContext 的值

return <>
<div>The current theme is: {theme}</div>
<button onClick={changeTheme}>toggle theme</button>
</>
}

export default App;

useReducer 使用

useReducer 返回的 dispatch 函数允许你更新 state 并触发组件的重新渲染。它需要传入一个 action 作为参数

并且dispatch 函数没有返回值。

jsx
import { useReducer } from 'react'

// 需要实现一个 reducer函数, 在这里实现action操作
function reducer(state = {age: 0}, action: { type: 'incremented_age' }) {
switch (action.type) {
case 'incremented_age':
return {
age: state.age + 1
};
default:
throw Error('Unknown action.');
}
}

export function Counter() {
const [state, dispatch] = useReducer(reducer, {age: 42});

return (
<>
<button onClick={() => {
dispatch({type: 'incremented_age'})
}}>
Increment age
</button>
<p>Hello! You are {state.age}.</p>
</>
);
}

使用场景:

  • 你的state是一个数组或者一个对象
  • state变化很复杂,经常一个操作需要修改很多state
  • 你用应用程序比较大,希望UI和业务能够分开维护
  • 需要为了更好的写构建单元测试

Suspense 组件

React Suspense 是 React 中的一个功能,它允许组件“等待”某些内容加载,并在加载时显示一个备用的
UI。这对于提升用户体验是非常有用的,特别是在加载数据或代码时可以显示一个加载指示器,而不是一个空白屏幕或者是一个突然跳出的内容。

Suspense 最初是在 React 16.6 版本中引入,主要用于支持 React.lazy,这是一个允许你动态加载组件的函数。随后,React 团队扩展了
Suspense 的能力,用于支持数据获取等异步操作。

使用 React.lazy 和 Suspense

React.lazy 函数允许你动态导入一个组件,并将其作为一个正常组件渲染。使用 Suspense 可以在组件加载时渲染备用内容,如下所示:

import React, { Suspense, lazy } from 'react';

// 使用 React.lazy 动态导入组件
const SomeComponent = lazy(() => import('./SomeComponent'));

function MyComponent() {
return (
// 使用 Suspense 包裹懒加载的组件
<Suspense fallback={ <div>Loading...</div> }>
<SomeComponent/>
</Suspense>
);
}

export default MyComponent;

在上面的例子中,SomeComponent 是一个懒加载组件,fallback 属性接收的是在组件加载期间展示的 JSX。当 SomeComponent
正在被加载时,用户会看到一个文本 “Loading…”。

使用 Suspense 进行数据获取

从 React 18 开始,Suspense 开始支持数据获取场景。


function MyComponent() {
return (
<Suspense fallback={ <div>Loading data...</div> }>
<DataComponent/>
</Suspense>
);
}

function DataComponent({resource}) {
useEffect(async () => {
// 假设有一个 fetchData 函数可以异步获取数据
const data = await getAsyncData();
}, []);
// 渲染数据
return <div>{ data }</div>;
}

请记住,这种数据获取的方式尚处于实验阶段,并且 React 团队可能会在未来的版本中对其进行调整或完全替换。

总结

React Suspense 是一个用于提高用户体验的功能,它允许你在组件加载或数据获取时显示一个备用的 UI。随着 React 的发展,Suspense
正在变得越来越强大,支持更多的异步场景。不过,如果你打算用于生产环境,建议关注 React 官方的更新,因为相关 API 还在不断演进中。

React合成事件和原生事件有什么区别?

React合成事件(SyntheticEvent)是React为了跨浏览器兼容性而模拟的浏览器原生事件系统。React合成事件和原生的DOM事件有一些不同之处,以及它们带来的好处:

  1. 跨浏览器兼容性:
    合成事件抽象了底层的DOM事件系统,提供了一致的事件对象,无论用户使用什么浏览器。这意味着开发者不需要考虑各种浏览器的事件兼容性问题,React已经为你处理了这部分。

  2. 事件委托:
    React使用单一的事件监听器来处理所有的事件,即事件委托。这意味着所有的事件都会被委托到最外层的元素(通常是document
    ),并由React内部管理。这种方式可以减少内存消耗,提高性能,尤其是在处理大量DOM元素时。

  3. 自动清理:
    在组件卸载时,React会自动清理组件的事件监听器,这减少了内存泄漏的风险,也使得资源管理更加方便。

  4. 事件池(Event Pooling):
    出于性能考虑,React会重用事件对象,将它们放入一个“池”中,并在事件回调函数被调用后清空它们的属性。这意味着你不能异步访问事件对象,除非你调用了event.persist()
    方法,它会将事件对象从池中移出,使其在回调之后仍然可以使用。

  5. 一致的属性名:
    React合成事件对象提供的属性和方法名称都遵循驼峰命名法,而不是原生事件中的全部小写,使得代码风格更加统一。

  6. 更好的集成React特性:
    合成事件天然地和React的状态更新、生命周期方法等特性整合得很好,使得在React生态中处理事件更加自然和高效。

虽然React合成事件提供了很多好处,但也有时你可能需要直接访问原生事件。在React中,你可以通过event.nativeEvent
属性访问到底层的原生事件对象。这样,如果你需要一些合成事件没有提供的特定信息,你仍然可以访问到这些信息。

受控组件和非受控组件的区别?

在React中,受控组件(Controlled Components)和非受控组件(Uncontrolled Components)是处理表单元素的两种不同方式。

受控组件:

受控组件是React中推荐的方式来管理表单数据的。在受控组件中,表单数据是由React组件的state来管理的。这意味着每当表单的输入发生改变时,比如用户输入了新的文本,都会有一个事件处理器函数(如onChange)来更新组件的state,从而更新组件渲染的输入元素的显示值。

受控组件的特点:

  • 输入值绑定到组件的状态上。
  • 必须提供一个onChange处理器来响应数据的变化并更新状态。
  • 数据由React的state完全控制,因此组件的状态是数据的“唯一真相来源”。

非受控组件:

非受控组件不像受控组件那样由React来管理表单数据。在非受控组件中,表单数据是由DOM本身来管理的。通常,你会使用ref来直接从DOM元素中获取表单数据。

非受控组件的特点:

  • 输入值通过ref从DOM中取得,不需要为每个状态更新编写事件处理函数。
  • 可以像传统HTML表单那样工作,使用ref访问DOM节点来获取其当前值。
  • 表单数据通常在需要时才从DOM中取得,而不是每次输入变化时都更新。

如何将非受控组件改成受控组件?

要将非受控组件转换为受控组件,你需要做以下几个步骤:

  1. 在组件的state中添加一个新的state属性来保存输入值。
  2. 将这个新的state属性作为输入元素的value属性。
  3. 创建一个事件处理函数来响应输入值的变化,并更新相关的state。
  4. 将这个事件处理函数绑定到输入元素的onChange事件上。

下面是一个简单的例子来演示如何转换:

非受控组件:

class UncontrolledComponent extends React.Component {
inputRef = React.createRef();

handleSubmit = event => {
alert('A name was submitted: ' + this.inputRef.current.value);
event.preventDefault();
};

render() {
return (
<form onSubmit={ this.handleSubmit }>
<label>
Name:
<input type="text" ref={ this.inputRef }/>
</label>
<button type="submit">Submit</button>
</form>
);
}
}

受控组件:

class ControlledComponent extends React.Component {
state = {name: ''};

handleChange = event => {
this.setState({name: event.target.value});
};

handleSubmit = event => {
alert('A name was submitted: ' + this.state.name);
event.preventDefault();
};

render() {
return (
<form onSubmit={ this.handleSubmit }>
<label>
Name:
<input type="text" value={ this.state.name } onChange={ this.handleChange }/>
</label>
<button type="submit">Submit</button>
</form>
);
}
}

在受控组件的例子中,我们用this.state.name来控制输入元素的值,并用handleChange
事件处理函数来更新state。这样就可以确保组件的state是所有输入数据的唯一来源。

React 性能优化

  • 合理拆分组件颗粒度, 减少整个组件渲染, 以及减少组件render渲染次数
  • 使用React.memo,PureCompoent,useMemo,useCallback等方式进行组件优化
  • 使用Suspense和React.lazy来进行懒加载和异步组件加载
  • 使用React.lazy进行路由懒加载

React Fiber 架构

React Fiber 是 React 16.8 中引入的新的核心算法,它是对 React 核心算法的重写。目的是提高其在渲染大型应用程序时的性能和响应性。Fiber
架构旨在解决以前的堆栈重绘(stack reconciliation)算法在动画、布局和手势等连续性交互方面的不足。

这里有几个关键点来帮助理解 React Fiber 架构:

  1. 增量渲染(Incremental Rendering):Fiber 的主要特点是增量渲染,它允许 React
    将渲染工作分割成多个块并将这些块分散到多个帧中。这种方式可以避免长时间的渲染任务阻塞浏览器主线程,从而保持应用的响应性。

  2. 任务分割(Task Splitting):在 Fiber 架构中,渲染任务可以被分割成小的单元。React
    在执行时可以根据需要中断这些任务,以确保主线程可以处理更紧急的任务,例如用户输入。

  3. 重新设计的调度器(Scheduler):Fiber 引入了一个新的调度器,可以更智能地安排和优先处理更新。这意味着 React
    可以根据任务的优先级,如动画或数据获取,进行调整。

  4. Fiber 节点(Fiber Nodes):在 Fiber 架构中,每个 React 元素都有一个对应的 Fiber 节点。这些节点构成了一个虚拟的树状结构。每个
    Fiber 节点都是一个工作单元,并包含了组件的类型、状态、待处理的更新等信息。

  5. 双缓冲(Double Buffering):在旧的 React 架构中,每次更新都会直接对 DOM 进行操作。而 Fiber
    则使用了类似游戏引擎中的“双缓冲”技术,在内存中构建新的树结构,然后在适当的时候将其一次性更新到 DOM 上。这有助于避免不必要的布局计算和重绘。

  6. 并发模式(Concurrent Mode):Fiber 架构使得 React 可以在其并发模式下工作,这允许组件在不阻塞主线程的情况下,以非同步方式渲染。

Fiber 的引入极大地增强了 React 的能力,使其可以更好地处理动态的、高负载的应用场景,同时为未来的功能更新和优化打下了基础。随着时间的推移,React
团队还在不断改进和优化 Fiber 架构,以提供更好的性能和开发体验。

ErrorBoundary 错误边界组件

React ErrorBoundary(错误边界)是一种 React 组件,它可以捕获其子组件树中发生的 JavaScript 错误,并输出备用
UI,而不是导致整个组件树卸载。这提供了一种更优雅地处理错误并提高应用程序稳定性的方法。

在 React 16 之前,如果组件树中的某个组件发生错误,整个应用会崩溃并显示一个白屏。错误边界的引入改善了这种情况,使得开发者可以控制组件错误的影响范围,并决定如何响应错误。

如何定义一个错误边界组件

要创建一个错误边界组件,你需要在你的 React 组件中定义一个或两个生命周期方法:

  1. static getDerivedStateFromError(error):此生命周期在后代组件抛出错误后被调用,用于渲染备用 UI。
  2. componentDidCatch(error, errorInfo):此生命周期在后代组件抛出错误后被调用,用于记录错误信息。

以下是一个简单的错误边界组件例子:

class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {hasError: false};
}

static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示降级后的 UI
return {hasError: true};
}

componentDidCatch(error, errorInfo) {
// 你同样可以将错误日志上报给服务器
console.error("Caught an error in ErrorBoundary: ", error, errorInfo);
}

render() {
if (this.state.hasError) {
// 你可以渲染任何自定义的降级 UI
return <h1>Something went wrong.</h1>;
}

return this.props.children;
}
}

如何使用错误边界

你可以将错误边界组件当作一个普通组件来使用,将其放置在组件树中任何你想要捕获错误的位置。例如:

<ErrorBoundary>
<MyComponent/>
</ErrorBoundary>

在这个例子中,如果 MyComponent 或者它的任何子组件在渲染过程中、生命周期方法中或者构造函数中抛出了错误,ErrorBoundary
将会捕获它,并渲染备用的 UI,而不是让整个应用崩溃。

错误边界无法捕获以下场景中的错误:

  • 事件处理(了解更多信息,你可以使用 try/catch 语句,而不是错误边界)
  • 异步代码(例如 setTimeout 或者 requestAnimationFrame 回调函数)
  • 服务端渲染
  • 它自己抛出来的错误(而不是其子组件)

错误边界是 React 应用程序中重要的一部分,它们提供了一种优雅处理和响应错误的方式,使得用户体验更加流畅、不会因为单个组件的问题而导致整个应用崩溃。

跨站脚本攻击(Cross-site Scripting, XSS)

攻击方式: 攻击者利用网页开发时留下的漏洞,通过巧妙的方法在目标网站 HTML 页面中注入恶意脚本,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可获取用户的敏感信息如
Cookie、SessionID 等,进而危害数据安全。
本质: 恶意的脚本代码被注入到了页面中,被浏览器执行。

XSS 攻击大致可以分为以下 3 类:

存储型

注入型脚本永久存储在目标服务器上。当浏览器请求数据时,脚本从服务器上传回并执行。
e.g. :用户评论中注入恶意脚本,其他用户查看评论时,恶意脚本被执行。

<!-- 伪造的网页结构示例 -->
<html>
<head>
<title>论坛帖子</title>
</head>
<body>
<h1>欢迎来到我们的论坛!</h1>
<p>这里是用户评论:</p>
<div id="userComments">
<!-- 用户的评论将在这里显示 -->
</div>
</body>
</html>

在没有适当防护的情况下,攻击者可能会提交一个包含恶意JavaScript代码的评论,例如:

<script>alert('XSS攻击');</script>

当其他用户查看评论时,恶意代码将被执行。

反射型(非持久型)

攻击者构造出特殊的 URL,其中包含恶意代码。用户点击包含恶意代码的 URL 时,网站解析执行了恶意代码。
e.g. :攻击者构造出一个链接,链接中包含了恶意代码,用户点击链接后,恶意代码被执行。

<!-- 伪造的网页结构示例 -->
<html lang="en">
<body>
<h1>Hello <span class="name"></span></h1>

</body>
</html>

<script>
const name = new URLSearchParams(window.location.search).get('name')
console.log({name})
document.querySelector('.name').textContent = name
</script>

攻击者构造的恶意 URL如下:
URL: http://localhost:63342/index.html?name=%3Cscript%3Ealert(100)%3C/script%3E

http://oss.anyways.fun/blog/tQB6uf.png

正常情况下页面上是回弹出alert的,但是现在的浏览器都有XSS防护机制,可以看到这里被注入的script脚本被做了特殊字符转译,所以不会弹出。
(实测了Chrome,Safari,Firefox都无效,他们底层都做了XSS防护攻击)

DOM 型

DOM 型 XSS 攻击和存储型、反射型 XSS 攻击的差别在于,DOM 型 XSS 攻击不需要和后端交互,而是通过修改页面的 DOM
节点来完成攻击。

和反射型 XSS 攻击类似,DOM 型 XSS 攻击也是通过 URL 传入恶意代码,但是这种攻击方式不需要后端的参与,而是通过前端的脚本来完成攻击。

防御措施:

  • 对所有用户输入进行适当的转义和过滤,以防止脚本执行
  • 设置CSP(Content Security Policy)来减少XSS攻击的风险。
  • 服务端设置 cookie 为 httpOnly,保护用户cookie不被Javascript读取

2. CSRF攻击 (Cross-Site Request Forgery 跨站请求伪造)

攻击方式: CSRF攻击允许攻击者在不知情的情况下,以受害者的身份发送或改变服务器上的请求。如果受害者在Web应用程序中具有足够的权限,攻击者可以利用这些权限执行恶意操作。
本质: 攻击者引诱用户打开一个链接,在用户已登录被攻击的网站情况下,在恶意网站中请求被攻击网站的接口,此时Cookie信息会被自动带上发送给Hacker服务器。

实现思路

  1. 用户登录:用户在浏览器中登录某个网站,并且该网站的服务器为用户浏览器设置了认证Cookie。
  2. 攻击页面构建:攻击者构造了一个恶意网页,这个网页包含了一些请求目标网站的代码,比如一个自动提交的表单。
  3. 用户访问恶意网页:如果用户在还没有登出之前(即Cookie还有效),不小心访问了这个恶意网页,浏览器会自动发送请求到目标网站。
  4. 浏览器发送请求:因为用户的Cookie仍然有效,目标网站会认为这是用户自己发出的请求,并执行相应的操作。
  5. 攻击完成:攻击者通过这种方式可以实现各种操作,如转账、改密码、发信息等,只要受害者有权限执行的操作。

防御措施:

  • 服务端开启CORS跨域,限制请求来源
  • 使用SameSite Cookie属性:设置Cookie的SameSite属性,可以限制Cookie不随跨站请求发送。
  • 设置Cookie可信任域名:设置Cookie的Domain属性,只允许指定域名发送请求。

3. SQL注入

攻击方式: SQL注入是一种代码注入的攻击方式,通过把SQL命令插入到Web表单提交或输入域名的查询字符串,最终欺骗服务器执行恶意的SQL命令。
本质: 攻击者通过输入恶意的SQL语句,欺骗服务器执行恶意的SQL语句。

防御措施:

  • 使用参数化查询:使用参数化查询可以有效防止SQL注入攻击。
  • 限制数据库的权限:数据库的权限应该尽可能小,只赋予执行必要操作的权限。
  • 对用户输入进行校验:对用户输入的数据进行校验,只允许合法的数据通过。
  • 使用ORM框架:ORM框架可以有效防止SQL注入攻击。

前言

最近在做一个项目,需要使用Puppeteer来爬取一些数据,由于Puppeteer需要Chromium,而在Linux服务器环境下使用Chromium会安装会比较麻烦,所以我选择使用Docker来安装Puppeteer这样可以确保环境的一致性,避免出现依赖问题。

由于我的项目是用Nestjs写的,所以这里我使用Nestjs作为例子,你也可以使用其他Nodejs框架。

环境

  • Docker
  • Linux(我这里用的是Ubuntu 20.04)

Dockerfile

# 使用node 18作为基础镜像
FROM node:18

# 跳过Chromiun下载
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true

# 安装 Google Chrome Stable 版本 (Chrome 路径 /usr/bin/google-chrome)
RUN apt-get update && apt-get install gnupg wget -y && \
wget --quiet --output-document=- https://dl-ssl.google.com/linux/linux_signing_key.pub | gpg --dearmor > /etc/apt/trusted.gpg.d/google-archive.gpg && \
sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' && \
apt-get update && \
apt-get install google-chrome-stable -y --no-install-recommends && \
rm -rf /var/lib/apt/lists/*

# 设置工作目录
WORKDIR /usr/src/app

# Copy package.json and package-lock.json
COPY package*.json ./

# Install app dependencies
RUN npm install

# Bundle app source
COPY . .

# Copy the .env and .env.development files
COPY .env ./

# Creates a "dist" folder with the production build
RUN npm run build

# Expose the port on which the app will run
EXPOSE 3000

# Start the server using the production build
CMD ["npm", "run", "start:prod"]

Puppeteer launch 配置

在Linux下部署的,所以需要对Puppeteer的launch配置进行一些修改,具体如下:

  • 禁用GPU --disable-gpu
  • 使用Linux的Chrome路径 /usr/bin/google-chrome
const isLinux = os.platform() === 'linux';
pupeeteer.launch({
headless: isLinux,
timeout: 1000 * 60,
args: [
isLinux ? '--disable-gpu' : undefined, // Linux下需要禁用GPU, 否则会报错
'--disable-dev-shm-usage',
'--disable-setuid-sandbox',
'--no-sandbox',
'--no-first-run',
].filter(Boolean),
executablePath: isLinux ? '/usr/bin/google-chrome' : '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
})

Docker Build 打包镜像

$ cd /path/to/your/project

$ docker build -t nestjs-puppeteer .

Docker Run 运行镜像

docker run -d -p 3000:3000 -it --rm --name creepeer-app nestjs-puppeteer

介绍

Node.js是一个基于V8引擎的JavaScript运行时环境,它的特点是事件驱动、非阻塞I/O模型。事件驱动模型是基于事件循环机制的,而非阻塞I/O则是通过libuv库实现的,这两个特性是Node.js高效处理并发的关键所在。在本文中,我们将深入探讨这两个概念,并解释它们如何共同工作以提高Node.js应用程序的性能。

Node.js 特点

  • 事件循环 Event Loop
  • 非阻塞I/O (底层libuv实现)

适合处理高并发,低延迟的场景,比如网络应用、聊天室、实时通信等。
擅长做I/O密集型的应用

什么是Event Loop?

Event Loop是一个程序结构,用于等待和发送消息和事件。在Node.js中,它是运行时的核心部分,负责协调事件的顺序和执行回调。Node.js的Event Loop是单线程的,这意味着它一次只能执行一个事件或任务。

Node.js 的事件循环是一种非常高效的方式来处理 I/O 操作,它允许程序在等待 I/O 完成的同时执行其他任务,而不会阻塞整个进程。这使得 Node.js 非常适合构建高吞吐量、低延迟的网络应用程序。

单线程如何保证高并发?

JavaScript是单线程语言,这意味着它一次只能做一件事。 这对于浏览器中运行的代码通常没问题,因为用户界面(UI)交互通常不需要大量并行处理,但是,在服务器端,你可能需要同时处理成千上万的客户端请求。Node.js通过Event Loop实现了一个非阻塞的方式来处理这些请求,这样就可以在不增加更多线程的情况下高效地处理大量并发。

那么,事件循环和非阻塞IO都是是如何工作的呢?

Event Loop的工作原理

非阻塞IO原理

非阻塞IO是一种系统调用,允许程序在等待IO操作(如读取文件或网络通信)完成的同时,继续执行其他任务。这与传统的阻塞IO相对,阻塞IO会导致程序停止执行,直到IO操作完成。

在Node.js中,大多数库函数都是非阻塞的,这意味着它们会立即返回,然后在IO操作完成时通过回调函数通知程序。这种模式允许Node.js应用程序在处理大量IO密集型任务时保持高效率。

餐厅排队的例子就可以很好的解释阻塞IO和非阻塞IO的区别:

  • 阻塞IO: 线下排队(需要一直等待,等到自己了才可以去就餐)
  • 非阻塞IO: 线上取号(可以先拿号,等到号了再过来用餐,期间可以做其他的事情)

说完了非阻塞IO,接下来我们来看看Event Loop是如何工作的。

Event Loop的工作流程

Node.js的Event Loop遵循以下步骤:

Node.js 事件循环流程图

  • 执行同步代码:Node.js首先执行脚本的同步代码,例如变量声明和函数定义。
  • 注册回调:当遇到异步操作时(例如文件读取或数据库查询),Node.js会注册一个回调函数,并继续执行Event Loop的下一个阶段。
  • 事件队列:Node.js维护一个事件队列,其中包含由异步操作触发的事件和回调。
  • 循环:Event Loop循环通过事件队列,并根据需要执行回调。这些回调被执行为了响应外部事件,例如HTTP请求到达或计时器到期。

Node.js的Event Loop包含几个阶段,包括:

  • 定时器阶段:处理setTimeout和setInterval回调。
  • IO回调阶段:处理几乎所有的IO相关的回调函数,例如读取文件、网络请求等。
  • 闲置、准备阶段(idle prepare):这是一个内部阶段,一般不会由用户代码触发。
  • 轮询阶段(Poll):检查新的IO事件,这个阶段,Node.js 将检查是否有新的 I/O 事件需要处理。如果有,它将执行对应的回调函数。。
  • 检查阶段:setImmediate()回调在这个阶段执行。
  • 关闭的回调阶段:例如socket.on(‘close’, …)这种回调。事件循环不断地在这些阶段之间切换,处理队列中的任务,直到队列为空。这种非阻塞的模型使 Node.js 能够处理大量并发连接而不陷入阻塞,因为它可以在等待 I/O 时执行其他任务。

Nodejs的Event Loop和 Javascript的Event Loop有什么区别?

浏览器事件循环流程图

浏览器事件循环

  • 浏览器首先会执行主线程上的代码(相当于宏任务),遇到微任务便将其推入微任务队列中,遇到宏任务便推入宏任务队列中
  • 当主线程中的代码执行完后,会检查微任务队列是否为空,若不为空,则将微任务队列中的微任务推至执行栈中执行。在执行的该微任务的过程中,如果又遇到宏任务则将其推入宏任务队列,遇到微任务则推入微任务队列
    当微任务队列执行完为空时,检查宏任务队列是否为空,如不为空,则将对头宏任务推入执行栈开始执行。该过程中,若遇到宏任务则将其推入宏任务队列,若遇到微任务,则推入微任务队列
  • 每当执行完一个宏任务,不管宏任务队列中是否还存在宏任务,都必须去检查微任务队列中是否有微任务,若存在微任务,则开始执行微任务

这整一个操作一直重复,就是Javascript的事件循环,那它和Nodejs的事件循环有哪些区别呢?

区别:

  • 在浏览器事件循环中,每执行完一个宏任务,便要检查并执行微任务队列;而node事件循环中则是在“上一阶段”执行完,“下一阶段”开始前执行微任务队列中的任务。也就是说,node中的微任务是在两个阶段之间执行的。如果是node10及其之前版本:要看第一个定时器执行完,第二个定时器是否在完成队列中。
  • 在浏览器事件循环中,process.nextTick()属于微任务,而且和其他微任务的优先级是一样的,不存在哪个微任务的优先级高就先执行谁。但是在node中,process.nextTick()的优先级要高于其他微任务,也就是说,在两个阶段之间执行微任务时,若存在process.nextTick(),则先执行它,然后再执行其他微任务。

优化技巧和注意事项

  • 尽量避免在事件循环中执行长时间运行的同步操作,以免阻塞其他任务的执行。
  • 使用async/await和Promises来更好地管理异步流程,使代码更易于理解和维护。
  • 谨慎使用setImmediate(),只在必要的情况下使用,以避免破坏事件循环的正常执行流程。
  • 传输大文件时使用stream进行管道传输,避免一次性读取整个文件到内存中。关于stream的操作,可以参考Node.js Stream stream也有内部的优化策略 Backpressure 背压处理。

背压处理

背压处理是一种流控制策略,用于处理生产者和消费者之间的速度不匹配问题。在Node.js中,stream模块提供了一种内置的背压处理机制,可以帮助应用程序更好地处理大量数据的传输。

在Node.js中,背压(Backpressure)是指在流(Stream)处理中,生产者(数据的写入方)的数据生成速度超过消费者(数据的读取方)处理速度的情况。当这种情况发生时,如果不加以控制,就可能导致内存的过度使用,乃至应用程序的崩溃。背压处理机制就是用来防止这种情况的发生,确保流的稳定性和效率。

在Node.js中,流是基于事件的,可以使用stream模块来处理数据。流分为可读流(Readable),可写流(Writable),双工流(Duplex)和转换流(Transform),每种流都有特定的背压处理机制。

以下是Node.js中处理背压的一些机制:

可读流(Readable):

可读流通过pause()resume()方法提供了手动的流量控制(Flow Control)。
当消费者还在处理数据时,它可以调用pause()方法来暂停流的数据读取。
一旦消费者准备好接收更多数据,可以调用resume()方法来恢复流的数据读取。
在新的流(Streams API的v2以后)中,可读流会在内部自动管理背压,即消费者通过.read()方法读取数据时,流会自动管理背压。
可写流(Writable):

可写流通过write()方法返回的布尔值提供了背压的信号。
write()返回false时,这是一个信号,告诉生产者应该停止写入数据,直到drain事件被触发。
一旦内部缓冲区的数据被消费者(可写流)处理完毕,就会触发drain事件,生产者可以监听这个事件来恢复数据写入。
pipe()方法:

pipe()方法是一个简化流量控制的工具,它自动处理背压,将一个可读流的数据导入到一个可写流。
它监听可读流的data事件来获取数据,并使用可写流的write()方法来写入数据。
如果write()返回false,pipe()会自动暂停可读流的数据读取,等待可写流的drain事件再次恢复读取。
通过这些机制,Node.js在流处理中维持了数据的平衡,防止了内存的浪费,确保了应用程序的稳定性和性能。开发者需要了解这些流控制的概念,并在实现流处理逻辑时恰当地使用它们,以避免背压问题。

什么是设计模式

作者的这个说明解释得挺好

假设有一个空房间,我们要日复一日地往里 面放一些东西。最简单的办法当然是把这些东西 直接扔进去,但是时间久了,就会发现很难从这 个房子里找到自己想要的东西,要调整某几样东 西的位置也不容易。所以在房间里做一些柜子也 许是个更好的选择,虽然柜子会增加我们的成 本,但它可以在维护阶段为我们带来好处。使用 这些柜子存放东西的规则,或许就是一种模

观察者模式 / 发布订阅模式

发布订阅模式(也称为观察者模式)是一种软件设计模式,其中一个对象(称为发布者)维护一系列依赖于它的对象(称为订阅者),当有状态变化时,发布者会自动通知所有订阅者。


class EventEmitter {
constructor() {
this.events = {};
}

// 订阅指定的主题
subscribe(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
const index = this.events[eventName].length - 1;

// 提供取消订阅的方法
return () => {
this.events[eventName].splice(index, 1);
};
}

// 发布事件,触发订阅的回调
publish(eventName, ...args) {
if (this.events[eventName]) {
this.events[eventName].forEach((callback) => {
callback(...args);
});
}
}
}

// 使用示例

// 创建一个事件发射器实例
const eventEmitter = new EventEmitter();

// 订阅事件
const unsubscribe = eventEmitter.subscribe('myEvent', (data) => {
console.log(`Received data: ${data}`);
});

// 发布事件
eventEmitter.publish('myEvent', 'Hello World!'); // 控制台输出:Received data: Hello World!

// 取消订阅
unsubscribe();

// 再次发布事件,由于取消订阅,不会有输出
eventEmitter.publish('myEvent', 'Hello again!');

装饰器模式

/**
* Created by WebStorm.
* User: chrischen
* Date: 2020/7/13
* Time: 10:49 下午
*/

class Math {
@log()
add(a, b) {
return a + b;
}
}

function log(target, name, descriptor) {
console.log(`target:${ target }, name: ${ name }, descriptor: ${ descriptor }`);
let oldValue = descriptor.value;

descriptor.value = function () {
console.log(`调用${ name }参数`, arguments);
return oldValue.apply(target, arguments)
}
return oldValue.apply(target, arguments);
}

let math = new Math();
math.add(1, 2);

单例模式

特点:只能实例化一次
单例模式还分懒汉模式和饿汉模式,懒汉模式就是在需要的时候才实例化,饿汉模式就是一开始就实例化

class Singleton {
constructor(name, age) {
if (Singleton.instance) return Singleton.instance;

this.name = name;
this.age = age;
Singleton.instance = this;
}
}

let singleton1 = new Singleton('Chris', 20);
let singleton2 = new Singleton('Chris', 20);
console.log(singleton1 === singleton2);


工厂模式

工厂模式是一种创建对象的设计模式,不需要指定将要创建的对象的确切类。


class Car {
constructor(options) {
this.doors = options.doors || 4;
this.state = options.state || 'brand new';
this.color = options.color || 'silver';
}
}

class Truck {
constructor(options) {
this.wheelSize = options.wheelSize || 'large';
this.state = options.state || 'used';
this.color = options.color || 'blue';
}
}

class VehicleFactory {
createVehicle(options) {
if (options.vehicleType === 'car') {
return new Car(options);
} else if (options.vehicleType === 'truck') {
return new Truck(options);
}
}
}

const factory = new VehicleFactory();
const car = factory.createVehicle({vehicleType: 'car', color: 'yellow'});

pem to key

key.pem => cert.key

openssl pkey -in key.pem -out chrisorz.cn.key

pem to crt

openssl x509 -in cert.pem -out chrisorz.cn.crt

前言

这篇文章总结一下平时使用到 typescript 的问题,和一些高级用法

泛型 Genericity

理解泛型就两个字:传型, 如果和函数做类比,那范型就是函数的参数,可以传入任意类型的参数

最常见的Array类型就是使用了泛型

在函数中使用

Array.prototype.delete = function <T>(index: number): T {
return this.splice(index, 1)[0];
}

const arr = [1, 2, 3, 4]

arr.delete<number>(1) // 2

const arr2 = ['a', 'b', 'c', 'd']
arr2.delete<string>(1) // 'b'

在interface中使用

interface IBase<T> {
name: T
}

let base: IBase<string> = {
field: 'Chris'
}

let base2: IBase<number> = {
field: 18
}

interface 和 type

为什么要把interfacetype 放在一起呢,因为他们两个都有相似指出,但是又有不同的地方

interface

interface在TypeScript中用于定义对象的形状(shape),即对象应该有哪些属性以及它们的类型。接口是一种强大的方式来定义一个对象可以执行的操作和它应当具有的结构。接口通常用来定义对象的规范,以及类或对象必须实现的方法和属性。


interface Person {
name: string;
age: number;
greet(): void;
}


在上面的例子中,任何实现了Person接口的对象都必须有一个string类型的name属性、一个number类型的age属性,以及一个返回void的greet方法。

接口可以被继承和扩展,允许你创建新的接口,基于一个或多个现有的接口。

type

type是TypeScript中的类型别名,用于给一个类型起一个新的名字。它可以用来定义一个类型可以是什么。类型别名不仅仅可以用来定义对象的形状,还可以用来定义并集、交集、原始类型、元组、数组等。

type Greeting = "Hello" | "Hi" | "Hey";
type ID = string | number;
type User = {
name: string;
id: ID;
};

在上面的例子中,Greeting是一个类型别名,它可以是三个特定的字符串中的一个;ID是一个可以是string或number的类型别名;User是一个对象的类型别名,有name和id属性。

type和interface的区别:

相同点:

  • 都可以用来描述对象或函数的类型。
  • 都可以在TypeScript中用来实现类型检查和智能感知。

不同点:

  • interface可以被继承和扩展,而type不能。但type可以通过交集和并集操作创建新的类型。
  • type可以用于定义其他类型的组合,比如联合类型、元组类型等,而interface通常不用于这些目的。
  • type可以是任何有效的类型,包括基础类型、联合类型、交集类型、元组等,而interface主要用于定义对象的形状或行为。

使用场景

interface适用场景:

  • 当你需要定义一个对象的结构,尤其是当对象有可能被多个不同的类实现时。
  • 当你需要通过继承来扩展现有的接口。
  • 当你想要一个明确的合约来指定一个类必须实现哪些方法和属性时。

type适用场景:

  • 当你需要定义类型的并集或交集。
  • 当你想要定义元组、映射类型或其他复杂的类型结构。
  • 当你需要一个类型别名来简化复杂的类型表达式。
  • 在实际应用中,interfacetype可以互换使用,选择使用哪一个很大程度上取决于个人或团队的偏好,以及具体的使用场景。有些团队可能更倾向于使用interface来定义对象的形状,因为它更加正式,表达了一种契约的概念;而另一些团队可能更偏好type的灵活性。在某些情况下,最佳实践可能是结合使用这两者的特点。

在实际应用中,interfacetype可以互换使用,选择使用哪一个很大程度上取决于个人或团队的偏好,以及具体的使用场景。有些团队可能更倾向于使用interface来定义对象的形状,因为它更加正式,表达了一种契约的概念;而另一些团队可能更偏好type的灵活性。在某些情况下,最佳实践可能是结合使用这两者的特点。

implements

Implements 是一个用来描述类的关键词

interface BasePerson {
name: string;
age: number

walk(speed)
}

class Person implements BasePerson {
name: 'Chris';
age: 18;
walk(speed) {
}
}

交叉类型 , 联合类型

交叉类型 (并集) &

可以理解为,取两个类型的并集

语法 TypeA & TypeB

代码示例:

type Person1 = {
name: string
sex: number
}

type Person2 = {
name: string
age: number
}

// 联合类型
let cross: Person1 & Person2 = {
name: 'Chris',
age : 18,
sex : 1,
};

联合类型 |

语法 typeA | typeB


// 字符串联合类型写法
type A = 'A'
type B = 'B'

let str1: A | B = 'A';
let str2: A | B = 'B';

//---------------------------------------------

// 对象联合类型写法
interface InterfaceA {
name: string
sex: number
}

interface InterfaceB {
name: string
age: number
}

let person2: InterfaceA | InterfaceB = {
name: 'Chris',
age : 18,
sex : 1,
};

内置类型

Typescript 除了内置的基本数据类型,还有一些内部封装好的的类型,这些类型可以帮助我们更好的编写代码

ReturnType

提取函数类型 T 的返回类型。

function f1(): number { return 0; }
type T0 = ReturnType<typeof f1>; // number

Partial

将类型 T 的所有属性变为可选的。

interface Todo {
title: string;
description: string;
}
function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
return { ...todo, ...fieldsToUpdate };
}

Required

将类型 T 的所有属性变为必选的。

interface Props {
a?: number;
b?: string;
}
const obj: Required<Props> = { a: 5, b: 'Hello' }; // 正确

Readonly

interface Todo {
title: string;
}
const todo: Readonly<Todo> = {
title: "Delete inactive users",
};
todo.title = "Hello"; // 错误,title 是只读的

Pick<T, K>

从类型 T 中挑选某些属性 K 来构造类型。

interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Pick<Todo, "title" | "completed">;

Record<K, T>

创建一个类型,其键是 K,值是 T。

type PageOptions = Record<'home' | 'about' | 'contact', { title: string }>;

Exclude<T, U>

从类型 T 中排除可以赋给 U 的所有属性。

type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"

Extract<T, U>

从类型 T 中提取可以赋给 U 的所有属性。

type T0 = Extract<"a" | "b" | "c", "a" | "f">; // "a"

Omit<T, K>

从类型 T 中剔除键 K。

interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Omit<Todo, "description">;

NonNullable

从类型 T 中排除 null 和 undefined

type T0 = NonNullable<string | number | undefined>; // string | number

Parameters

提取函数类型 T 的参数类型组成一个元组类型。

function f1(arg1: number, arg2: string): void {}
type T0 = Parameters<typeof f1>; // [number, string]

InstanceType

获取构造函数类型的实例类型。

class C {
x = 0;
y = 0;
}
type T0 = InstanceType<C>; // C

Uppercase

将字符串类型中的每个字符转换为大写形式。

type T = Uppercase<'hello'>; // 'HELLO'

Lowercase

将字符串类型中的每个字符转换为小写形式。

type T = Lowercase<'HELLO'>; // 'hello'

Capitalize

将字符串类型中的第一个字符转换为大写形式。

type T = Capitalize<'hello'>; // 'Hello'

Uncapitalize

将字符串类型中的第一个字符转换为小写形式。

type T = Uncapitalize<'Hello'>; // 'hello'

高阶使用

typeof (typescript中的typeof)

语法:typeof <variable>

例子:typeof window 获取window的类型

ts中的typeof是获取目标对象的类型(可以动态的推断类型)

let SystemMap = {
MacOS : 1,
Linux : 2,
Windows: 3,
};

type System = typeof SystemMap

// 此时的System的type为
interface {
Linux: number
MacOS: number
Windows: number
}

keyof ,用typeof获取对象的所有key的联合类型

语法:keyof <Type>

例子:keyof Object 获取Object的所有 key 的字符串的联合类型

如果配置keyof 使用,就可以得到 Linux,MacOS,Windows 这几种类型的字符串key值了

let SystemMap = {
MacOS : 1,
Linux : 2,
Windows: 3,
};

// System 的类型为: "Linux" | "MacOS" | "Windows"
type System = keyof typeof SystemMap

in 操作符

语法:[k in Keys]

例子:[k in 'Linux' | 'MacOS' | 'Windows' ] 在type中使用,k 表示被遍历的对象

注意:in操作符只在type中可以使用

**in操作符号约束声明对象的 key 和 value **

let SystemMap = {
MacOS : 1,
Linux : 2,
Windows: 3,
};

type System = keyof typeof SystemMap

// 约定对象的 key 值要在 System 中
type Systems = {
[k in System]: SystemMap
}

let s: Systems = {
MacOS : 100,
Linux : 100,
Windows: 100,
};

typeof 实现取 对象 key , value 类型的联合类型

let systemMap = {
MacOS : 'macos',
Linux : 200,
Windows: 'windows',
};

type Keys = keyof typeof systemMap // "Linux" | "MacOS" | "Windows"

type Values = typeof systemMap[Keys] // number | string

取interface的 value的联合类型

let systemMap = {
MacOS : 'macos',
Linux : 200,
Windows: 'windows',
};

type Keys = keyof systemMap // "MacOS" | "Linux" | "Windows"

type Values = systemMap[Keys] // 200 | "macos" | "windows"

tsconfig.json 配置速查表

{
"compilerOptions": {
/* Basic Options */
"target": "es5" /* target用于指定编译后js文件里的语法应该遵循哪个JavaScript的版本的版本目标: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
"module": "commonjs" /* 用来指定编译后的js要使用的模块标准: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
"lib": ["es6", "dom"] /* lib用于指定要包含在编译中的库文件 */,
"allowJs": true, /* allowJs设置的值为truefalse,用来指定是否允许编译js文件,默认是false,即不编译js文件 */
"checkJs": true, /* checkJs的值为truefalse,用来指定是否检查和报告js文件中的错误,默认是false */
"jsx": "preserve", /* 指定jsx代码用于的开发环境: 'preserve', 'react-native', or 'react'. */
"declaration": true, /* declaration的值为truefalse,用来指定是否在编译的时候生成相应的".d.ts"声明文件。如果设为true,编译每个ts文件之后会生成一个js文件和一个声明文件。但是declaration和allowJs不能同时设为true */
"declarationMap": true, /* 值为truefalse,指定是否为声明文件.d.ts生成map文件 */
"sourceMap": true, /* sourceMap的值为truefalse,用来指定编译时是否生成.map文件 */
"outFile": "./", /* outFile用于指定将输出文件合并为一个文件,它的值为一个文件路径名。比如设置为"./dist/main.js",则输出的文件为一个main.js文件。但是要注意,只有设置module的值为amd和system模块时才支持这个配置 */
"outDir": "./", /* outDir用来指定输出文件夹,值为一个文件夹路径字符串,输出的文件都将放置在这个文件夹 */
"rootDir": "./", /* 用来指定编译文件的根目录,编译器会在根目录查找入口文件,如果编译器发现以rootDir的值作为根目录查找入口文件并不会把所有文件加载进去的话会报错,但是不会停止编译 */
"composite": true, /* 是否编译构建引用项目 */
"incremental": true, /* Enable incremental compilation */
"tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
"removeComments": true, /* removeComments的值为truefalse,用于指定是否将编译后的文件中的注释删掉,设为true的话即删掉注释,默认为false */
"noEmit": true, /* 不生成编译文件,这个一般比较少用 */
"importHelpers": true, /* importHelpers的值为truefalse,指定是否引入tslib里的辅助工具函数,默认为false */
"downlevelIteration": true, /* 当target为'ES5' or 'ES3'时,为'for-of', spread, and destructuring'中的迭代器提供完全支持 */
"isolatedModules": true, /* isolatedModules的值为truefalse,指定是否将每个文件作为单独的模块,默认为true,它不可以和declaration同时设定 */

/* Strict Type-Checking Options */
"strict": true /* strict的值为truefalse,用于指定是否启动所有类型检查,如果设为true则会同时开启下面这几个严格类型检查,默认为false */,
"noImplicitAny": true, /* noImplicitAny的值为truefalse,如果我们没有为一些值设置明确的类型,编译器会默认认为这个值为any,如果noImplicitAny的值为true的话。则没有明确的类型会报错。默认值为false */
"strictNullChecks": true, /* strictNullChecks为true时,null和undefined值不能赋给非这两种类型的值,别的类型也不能赋给他们,除了any类型。还有个例外就是undefined可以赋值给void类型 */
"strictFunctionTypes": true, /* strictFunctionTypes的值为truefalse,用于指定是否使用函数参数双向协变检查 */
"strictBindCallApply": true, /* 设为true后会对bind、call和apply绑定的方法的参数的检测是严格检测的 */
"strictPropertyInitialization": true, /* 设为true后会检查类的非undefined属性是否已经在构造函数里初始化,如果要开启这项,需要同时开启strictNullChecks,默认为false */
"noImplicitThis": true, /* 当this表达式的值为any类型的时候,生成一个错误 */
"alwaysStrict": true, /* alwaysStrict的值为truefalse,指定始终以严格模式检查每个模块,并且在编译之后的js文件中加入"use strict"字符串,用来告诉浏览器该js为严格模式 */

/* Additional Checks */
"noUnusedLocals": true, /* 用于检查是否有定义了但是没有使用的变量,对于这一点的检测,使用eslint可以在你书写代码的时候做提示,你可以配合使用。它的默认值为false */
"noUnusedParameters": true, /* 用于检查是否有在函数体中没有使用的参数,这个也可以配合eslint来做检查,默认为false */
"noImplicitReturns": true, /* 用于检查函数是否有返回值,设为true后,如果函数没有返回值则会提示,默认为false */
"noFallthroughCasesInSwitch": true, /* 用于检查switch中是否有case没有使用break跳出switch,默认为false */

/* Module Resolution Options */
"moduleResolution": "node", /* 用于选择模块解析策略,有'node'和'classic'两种类型' */
"baseUrl": "./", /* baseUrl用于设置解析非相对模块名称的基本目录,相对模块不会受baseUrl的影响 */
"paths": {}, /* 用于设置模块名称到基于baseUrl的路径映射 */
"rootDirs": [], /* rootDirs可以指定一个路径列表,在构建时编译器会将这个路径列表中的路径的内容都放到一个文件夹中 */
"typeRoots": [], /* typeRoots用来指定声明文件或文件夹的路径列表,如果指定了此项,则只有在这里列出的声明文件才会被加载 */
"types": [], /* types用来指定需要包含的模块,只有在这里列出的模块的声明文件才会被加载进来 */
"allowSyntheticDefaultImports": true, /* 用来指定允许从没有默认导出的模块中默认导入 */
"esModuleInterop": true /* 通过为导入内容创建命名空间,实现CommonJS和ES模块之间的互操作性 */,
"preserveSymlinks": true, /* 不把符号链接解析为其真实路径,具体可以了解下webpack和nodejs的symlink相关知识 */

/* Source Map Options */
"sourceRoot": "", /* sourceRoot用于指定调试器应该找到TypeScript文件而不是源文件位置,这个值会被写进.map文件里 */
"mapRoot": "", /* mapRoot用于指定调试器找到映射文件而非生成文件的位置,指定map文件的根路径,该选项会影响.map文件中的sources属性 */
"inlineSourceMap": true, /* 指定是否将map文件的内容和js文件编译在同一个js文件中,如果设为true,则map的内容会以//# sourceMappingURL=然后拼接base64字符串的形式插入在js文件底部 */
"inlineSources": true, /* 用于指定是否进一步将.ts文件的内容也包含到输入文件中 */

/* Experimental Options */
"experimentalDecorators": true /* 用于指定是否启用实验性的装饰器特性 */
"emitDecoratorMetadata": true, /* 用于指定是否为装饰器提供元数据支持,关于元数据,也是ES6的新标准,可以通过Reflect提供的静态方法获取元数据,如果需要使用Reflect的一些方法,需要引入ES2015.Reflect这个库 */
}
"files": [], // files可以配置一个数组列表,里面包含指定文件的相对或绝对路径,编译器在编译的时候只会编译包含在files中列出的文件,如果不指定,则取决于有没有设置include选项,如果没有include选项,则默认会编译根目录以及所有子目录中的文件。这里列出的路径必须是指定文件,而不是某个文件夹,而且不能使用* ? **/ 等通配符
"include": [], // include也可以指定要编译的路径列表,但是和files的区别在于,这里的路径可以是文件夹,也可以是文件,可以使用相对和绝对路径,而且可以使用通配符,比如"./src"即表示要编译src文件夹下的所有文件以及子文件夹的文件
"exclude": [], // exclude表示要排除的、不编译的文件,它也可以指定一个列表,规则和include一样,可以是文件或文件夹,可以是相对路径或绝对路径,可以使用通配符
"extends": "", // extends可以通过指定一个其他的tsconfig.json文件路径,来继承这个配置文件里的配置,继承来的文件的配置会覆盖当前文件定义的配置。TS在3.2版本开始,支持继承一个来自Node.js包的tsconfig.json配置文件
"compileOnSave": true, // compileOnSave的值是truefalse,如果设为true,在我们编辑了项目中的文件保存的时候,编辑器会根据tsconfig.json中的配置重新生成文件,不过这个要编辑器支持
"references": [], // 一个对象数组,指定要引用的项目
}

前言

刚入职新公司,接手到一个NestJS的项目(终于不用写HTML 和 CSS了),用于做前后端API交互的中间层处理,之前也有用NestJS写过一些自己的Demo项目,但是没太系统性的深入学习,现在趁着这次机会系统性的学习一下NestJS,顺便整理成文档…

NestJS是什么?

以下是官网的原话


Nest 是一个用于构建高效,可扩展的 Node.js 服务器端应用程序的框架。它使用渐进式 JavaScript,内置并完全支持 TypeScript(但仍然允许开发人员使用纯 JavaScript 编写代码)并结合了 OOP(面向对象编程),FP(函数式编程)和 FRP(函数式响应编程)的元素。

在底层,Nest使用强大的 HTTP Server 框架,如 Express(默认)和 Fastify。Nest 在这些框架之上提供了一定程度的抽象,同时也将其 API 直接暴露给开发人员。这样可以轻松使用每个平台的无数第三方模块。

Nest 提供了一个开箱即用的应用程序架构,允许开发人员和团队创建高度可测试,可扩展,松散耦合且易于维护的应用程序。


加上我自己的了解,总结一下分为几个特征

  • 是一个NodeJS的框架
  • 内置支持Typescript
  • 支持面向对象编程(OOP) / 函数式编程(FP)/ 函数式响应编程(FRP)
  • HTTP 服务器
    • Express (默认)
    • Fastify
  • 核心特性
    • 依赖注入 - DI
    • 中间件 - Middleware
    • 守卫 - Guards
    • 拦截器 - interceptors
    • 管道 - Pipe
    • 控制器 - Controller
    • 模块 - Module
    • 微服务

本文主要谈及一些和其他Node框架稍微差异的特性,比如依赖注入控制器管道拦截器模块微服务

本文章先从Nest的一个请求的生命周期开始讲解

Nest的一次请求生命周期

请求发起: 中间件 => 守卫 => 拦截器 => 管道 => 控制器

​ ⬇️

响应响应: HTTP Response <= 拦截器 <= 控制器

这次没画图了,以后找到好用的画图工具补一下 😅

依赖注入

在Nest中使用了大量的装饰器语法,依赖注入也是通过装饰器的形式进行实现

使用 @Injectable() 装饰的类,可以在任何地方被注入,下面看依赖注入在Nest中的具体使用


Provides是Nest的最基本的一个概念,许多基本的Nest类可能视为provider-service,repository,helper等等,在实际开发中,比如常用的service, repository。有了依赖注入我们能够提高应用程序的灵活性和模块化程度。举个例子说明:

app.module.t

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
imports: [],
controllers: [AppController],
// 把AppService导入到provides中, 在注入依赖时,使用provides建立对象之间的依赖关系
providers: [AppService],
})
export class AppModule {}

app.services.ts

import { Injectable } from '@nestjs/common';

// 使用 Injectable 装饰器把该类添加到IoC容器中(在其他文件中就可以完成依赖注入并自动实例化 AppService )
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

app.controller.ts

通过该装饰器使Nest知道这个类是一个provider,现在我们使用类构造函数注入该服务

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
// 1. 构造函数中 注入AppService类
constructor(private readonly appService: AppService) {}

// 2. 使用 属性注入的形式 注入 AppService类
// private readonly appService: AppService

@Get()
getHello(): string {
return this.appService.getHello();
}
}

从上面代码来看, 我们在Controller里使用AppService不是通过使用New来实例化, 而是在constuctor声明即可。

可看出依赖注入的两个优势:

  1. 依赖管理交给Nest运行时处理
  2. 依赖项只关注注入类型,不关注具体实例,具有高度解偶性

控制器

一般Node框架(Express,Koa)都有没有控制器这个概念,除了阿里的Egg这种上层的封装框架,Nest 的控制器相当于路由的概念

守卫

未完待续。。。

管道

管道是具有 @Injectable() 装饰器的类。管道应实现 PipeTransform 接口

当在 Pipe 中发生异常,controller 不会继续执行任何方法。

管道有两个类型:

  • 转换:管道将输入数据转换为所需的数据输出
  • 验证:对输入数据进行验证,比如form表单提交的数据类型

在这两种情况下, 管道 参数(arguments) 会由 控制器(controllers)的路由处理程序 进行处理. Nest 会在调用这个方法之前插入一个管道,管道会先拦截方法的调用参数,进行转换或是验证处理,然后用转换好或是验证好的参数调用原方法。

转换管道使用

validation.pipe.ts

import { PipeTransform, ArgumentMetadata } from '@nestjs/common';

export class ValidationPipe implements PipeTransform {
// 每个管道必须提供 transform() 方法。
// 这个方法有两个参数 1.value 2.metadata
transform(value: string, metadata: ArgumentMetadata): number {
return Number(value);
}
}

blog.controller.ts

import { ValidationPipe } from '@pipe/validation.pipe.ts'
@Controller('blog')
export class BlogController {
@Get(':id')
@UsePipes(new ValidationPipe()) // 使用@UsePipes装饰器
findOne(@Param('id') id: string) {
return this.blogService.findOne(+id);
}
}

验证管道使用

validation.pipe.ts

import { PipeTransform, ArgumentMetadata } from '@nestjs/common';

export class ValidationPipe implements PipeTransform {
// 每个管道必须提供 transform() 方法。
// 这个方法有两个参数 1.value 2.metadata
transform(value: string, metadata: ArgumentMetadata): number {
// value 就是接收到的 id
return Number(value);
}
}

blog.controller.ts

参数验证管道,在要验证的管道的前面加上 new Pipe() 将参数作为 pipe 实例里的 transform 方法中的第一个参数

import { ValidationPipe } from '@pipe/validation.pipe.ts'
@Controller('blog')
export class BlogController {
@Get(':id')
// 参数验证管道,
findOne(@Param('id', new ValidationPipe()) id: string) {
return this.blogService.findOne(+id);
}
}

拦截器

拦截器(在使用之前进行拦截):对拦截器装饰的方法的返回值进行拦截。 明白这一点后,就很容易理解 为什么要要手动调用 CallHeadler.handle() 才会触发到被拦截的方法了。

好处

  • 提前发现处理异常
  • 对操作进行收拢,统一处理

每个拦截器都有 intercept() 方法,它接收2个参数。

  • ExecutionContext 实例(与守卫完全相同的对象)ExecutionContext 继承自 ArgumentsHostArgumentsHost 是传递给原始处理程序的参数的一个包装 ,它根据应用程序的类型包含不同的参数数组

  • CallHandler - 调用处理程序,调用这个参数的 handle()方法 (并且已经返回值)被拦截器包裹的方法才被触发,handle() 返回一个 Observable,可以帮助我们进行例如响应操作。

    比如 CatsController 中的 create 方法用了拦截器,在这个拦截器中的 intercept方法内,必须调用 CallHandler.handler() 方法,create才会被执行。不调用CallHandler则不执行create()

拦截器的使用(局部)

拦截器可以装饰在

  1. 在class上 - 这种方式,控制器 实例 的每个方法触发,都会执行拦截器

  2. 在method 上 - 只对当前装饰的方法有效

auto.interceptor.ts

import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';

export class AutoInterceptor implements NestInterceptor {
// 拦截器类 必须实现的一个方法 intercept
intercept(
context: ExecutionContext,
next: CallHandler<any>,
): Observable<any> | Promise<Observable<any>> {
console.log('---------');
return next.handle().pipe();
}
}

auto.interceptor.ts

把拦截器装饰在class上

import { AutoInterceptor } from '../interceptors/auto.interceptor';

@Controller()
@UseInterceptors(AutoInterceptor) // 1. 传入class使用拦截器
// @UseInterceptors(new AutoInterceptor()) // 2. 传入class实例使用拦截器(2选1)
export class BlogController {
findAll(){}
}

auto.interceptor.ts

把拦截器装饰在 method上

export class BlogControll {
@UseInterceptors(AutoInterceptor)
findAll(){}
}

全局拦截器

全局拦截器用于整个应用程序、每个控制器和每个路由处理程序。

这种方式注入不属于任何模块,因此也无法注入依赖项

const app = await NestFactory.create(ApplicationModule);
app.useGlobalInterceptors(new LoggingInterceptor());

为了解决此问题, 您可以使用以下构造直接从 根模块 设置一个拦截器:

app.module

import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { AutoInterceptor } from '../interceptors/auto.interceptor';

@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: AutoInterceptor, // 注册全局拦截器
},
],
})
export class AppModule {}

前言

前断时间在折腾找工作,和租房子搬家,现在总算在新公司稳定下来了,现在才有了时间整理一些知识点,呼...👨‍💻 开始吧

当浏览器加载一个页面时 html 引用的外部资源也会加载,但是这些资源图片,css,js 都不经常变化,如果每次都加载这些资源势必会带来资源浪费,而且加载时间也会变长,影响用户的体验。

http 缓存技术就是为了解决这个问题(或者说所有的缓存都是为了性能的优化,增加用户体验),简单点说就是将加载过的静态文件缓存在客户端本地,下次请求相同的资源时可以直接加载本地的使用。

不过http也有相应的规则和策略,去控制文件何时使用缓存何时不实用缓存,且要保证一旦资源更新缓存也要随之更新

http缓存的作用

  • 降低消耗不必要的带宽
  • 提升用户访问的速度
  • 减轻服务器压力

http 缓存分两种,一种是强缓存又称(私有缓存),一种是协商缓存又称(共享缓存)

强缓存策略(私有缓存)

优先级:Cache-Control > Expires >

直接读取本地缓存的内容,不去请求服务器,返回的状态码是 200。

这里面有一个问题如果不去服务器请求 那如果静态资源更新了浏览器还在使用旧的资源怎么办呢? 答案是在 http 响应头(response headers) 中的设置缓存过期时间。

缓存过期时间又分两种,一种是固定时间,一种是相对时间

  • Expires 是固定时间
  • Cache-Control max-age 是设置相对时间
Expires: Wed, 21 Oct 2015 07:28:00 GMT

Cache-Control: max-age=60

Expires (http1.0)

Expireshttp1.0中定义的缓存字段,当我们请求一个资源,服务器返回时,可以在 response headers 中增加 Expires 字段表示过期时间

注意:Expires 是一个固定的时间值,比如在人类文明历史上的某一刻过期,则会出现进行过期时间对比时,如果客户端时间错乱了,则这个缓存的逻辑也变得错误,比较依赖客户端的时间准确性。

Expires: Wed, 21 Oct 2015 07:28:00 GMT

Cache-Control (http1.1)

Cache-Control:当值设为max-age=300时,则代表在这个请求正确返回时间(浏览器也会记录下来)的5分钟内再次加载资源,就会命中强缓存。

Cache-Control 其他的常用选项

  • max-age:用来设置资源(representations)可以被缓存多长时间,单位为秒;
  • s-maxage:和max-age是一样的,不过它只针对代理服务器缓存而言;
  • public:指示响应可被任何缓存区缓存;
  • private:只能针对个人用户,而不能被代理服务器缓存;
  • no-cache:强制客户端直接向服务器发送请求,也就是说每次请求都必须向服务器发送。服务器接收到 请求,然后判断资源是否变更,是则返回新内容,否则返回304,未变更。这个很容易让人产生误解,使人误
    以为是响应不被缓存。实际上Cache-Control: no-cache是会被缓存的,只不过每次在向客户端(浏览器)提供响应数据时,缓存都要向服务器评估缓存响应的有效性。
  • no-store:禁止一切缓存(这个才是响应不被缓存的意思)。

Cache-Control 是 http是1.1新增的头字段, expires是http1.0的头字段,如何两个同时存在,cache-control 会覆盖 expires

协商缓存

协商缓存分别有两组字段进行控制,并且相互对应,他们分别是 Last-Modified / If-Modified-Since , Etag / If-None-Match


Last-Modified / If-Modify-Slice (http1.0)

last-modified: Wed, 09 Jun 2021 17:36:57 GMT

if-modified-since: Wed, 23 Jun 2021 01:58:39 GMT

响应头 response headers 设置了 Etag 的资源, 再次请求的请求头 request headers 则会带上 If-None-Match 这个字段


  • Last-Modified

    浏览器向服务器发送资源最后的修改时间

  • If-Modify-Slice

    当资源过期时(浏览器判断Cache-Control标识的max-age过期),发现响应头具有Last-Modified声明,则再次向服务器请求时带上头if-modified-since,表示请求时间。服务器收到请求后发现有if-modified-since则与被请求资源的最后修改时间进行对比(Last-Modified),若最后修改时间较新(大),说明资源又被改过,则返回最新资源,HTTP
    200 OK;若最后修改时间较旧(小),说明资源无新修改,响应HTTP 304 走缓存。

Etag / If-None-Match (http 1.1)

etag: W/"141768e3674c4e846c3962bef5f3e919"

if-none-match: W/"141768e3674c4e846c3962bef5f3e919"

响应头 response headers 设置了 Last-Modified 的资源, 再次请求的请求头 request headers 则会带上 If-Modified-Since 这个字段 (可以随便打开一个网站进行验证)


  • Etag

    Etag是属于HTTP 1.1属性,它是由服务器生成返回给前端,用来帮助服务器控制Web端的缓存验证。 ETag的值,是对文件的索引节(INode),大小(Size)和最后修改时间(MTime)进行Hash后得到的。

  • If-None-Match

    当资源过期时,浏览器发现响应头里有Etag,则再次像服务器请求时带上请求头if-none-match(值是Etag的值)。服务器收到请求进行比对,决定返回200或304

结尾总结

Cache-Control > Expires > Etag > Last-Modified

  • Cache-Control 和 Expires 控制强缓存 (由浏览器进行验证,命中后http状态码为200,不请求,1)

  • Etag 和 Last-Modified 控制协商缓存 (由服务器进行验证,命中后http状态码为 304, 会发起请求)

下载vim高亮语法配置


cd ~/.vim/syntax/

wget http://www.vim.org/scripts/download_script.php?src_id=14376 -O nginx.vim

修改filetype.vim 配置

~/.vim/filetype.vim 文件中添加如下配置

" nginx 
au BufNewFile,BufRead /usr/local/etc/nginx setf nginx

/usr/local/etc/nginx 为你的nginx路径

0%