<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ PapayaHUANG - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ freeCodeCamp 是一个免费学习编程的开发者社区，涵盖 Python、HTML、CSS、React、Vue、BootStrap、JSON 教程等，还有活跃的技术论坛和丰富的社区活动，在你学习编程和找工作时为你提供建议和帮助。 ]]>
        </description>
        <link>https://www.freecodecamp.org/chinese/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ PapayaHUANG - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sun, 24 May 2026 19:37:51 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/author/papayahuang/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ 小游戏开发：使用 React 和 Redux Tool Kit 实现俄罗斯方块 ]]>
                </title>
                <description>
                    <![CDATA[ 俄罗斯方块是一款经典小游戏。通过从零开发一款小游戏，我们可以更好地了解开发使用的语言，同时也得到更强烈的“所见即所得”的反馈，所以开发小游戏会是不错的练手项目。 从一个主力语言是JavaScript的开发者角度来分析俄罗斯方块，不同形状的方块的下落、移动、消除其实是 复杂的状态管理和组件通信 。这不刚好就是React和Redux的射程范围嘛！（并没有说它们适合游戏开发） 我将通过这篇博文拆解实现俄罗斯方块的一些核心逻辑，以及Redux Tool Kit [https://redux-toolkit.js.org/]的使用。 你可以在我的Github仓库 [https://github.com/PapayaHUANG/react-redux-tetris]上找到项目代码，也可以通过 Github Page [https://papayahuang.github.io/react-redux-tetris/]来试玩这个游戏。 本文涉及的技术  * JavaScript数组高阶函数  * 使用Redux Tool Kit对React单页面项目进行状态管理  * 使用request ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/build-tetris-game-with-react-and-redux-tool-kit/</link>
                <guid isPermaLink="false">63fcae780687e3060be26c4b</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Redux ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ PapayaHUANG ]]>
                </dc:creator>
                <pubDate>Wed, 01 Mar 2023 03:10:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/03/nik-lUbIun4IL38-unsplash--1-.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>俄罗斯方块是一款经典小游戏。通过从零开发一款小游戏，我们可以更好地了解开发使用的语言，同时也得到更强烈的“所见即所得”的反馈，所以开发小游戏会是不错的练手项目。</p>
<p><img src="https://chinese.freecodecamp.org/news/content/images/2023/02/----2023-02-28-14.36.54.gif" alt="游戏动态展示" width="340" height="608" loading="lazy"></p>
<p>从一个主力语言是JavaScript的开发者角度来分析俄罗斯方块，不同形状的方块的下落、移动、消除其实是 <em>复杂的状态管理和组件通信</em>。这不刚好就是React和Redux的射程范围嘛！（并没有说它们适合游戏开发）</p>
<p>我将通过这篇博文拆解实现俄罗斯方块的一些核心逻辑，以及<a href="https://redux-toolkit.js.org/">Redux Tool Kit</a>的使用。</p>
<p>你可以在<a href="https://github.com/PapayaHUANG/react-redux-tetris">我的Github仓库</a>上找到项目代码，也可以通过<a href="https://papayahuang.github.io/react-redux-tetris/">Github Page</a>来试玩这个游戏。</p>
<h2 id="">本文涉及的技术</h2>
<ul>
<li>JavaScript数组高阶函数</li>
<li>使用Redux Tool Kit对React单页面项目进行状态管理</li>
<li>使用<code>requestAnimationFrame</code>实现动画效果</li>
</ul>
<h2 id="">文件结构</h2>
<h3 id="">项目总结构</h3>
<pre><code>|-- src
    |-- App.js
	|-- index.js
	|-- store.js
	|-- features/
		|-- game/
			|-- components/
			|-- game-slice.js
	|-- styles/
	|-- utils/
</code></pre>
<p>我是通过<code>create-react-app</code>创建的项目，游戏界面及功能所有代码都在<code>src</code>路径下：</p>
<ul>
<li>我保留了<code>index.js</code>和<code>App.js</code>，同时添加了<code>store.js</code>来处理Redux store的配置代码；</li>
<li>样式全部放在了<code>styles</code>文件夹中，游戏运行的核心逻辑放到了<code>utils</code>文件夹中；</li>
<li>Redux官方推荐在使用RTK（Redux Tool Kit）时，创建一个<code>features</code>目录，并把不同的功能模块放在这个目录下，每一个功能模块包含自己的组件、reducer和其他相关文件（如与后端通信的代码），所以我在<code>features</code>目录下创建了<code>game</code>文件夹，并在其下创建了<code>components</code>目录和<code>game-slice.js</code>。</li>
</ul>
<h3 id="">组件结构</h3>
<p>我规划的组件包括：</p>
<pre><code>|-- components/
	|-- Board.js
	|-- Control.js
	|-- MessagePopup.js
	|-- NextBlock.js
	|-- ScoreBoard.js
	|-- Square.js
</code></pre>
<p><img src="https://chinese.freecodecamp.org/news/content/images/2023/02/----.jpeg" alt="组件展示" width="727" height="1024" loading="lazy"></p>
<ul>
<li><code>Board.js</code>是主游戏区</li>
<li><code>Control.js</code>是按钮控制面板</li>
<li><code>MessagePopup.js</code>是游戏结束时弹出的消息框</li>
<li><code>NextBlock.js</code>是下一个形状提示框</li>
<li><code>ScoreBoard.js</code>显示当前分数和历史最高分数</li>
<li><code>Square.js</code>是组成主游戏区和提示框的小方块</li>
</ul>
<h2 id="ui">核心UI组件搭建的逻辑</h2>
<h3 id="">游戏区域的实现</h3>
<h4 id="">主游戏区框架的实现</h4>
<p><img src="https://chinese.freecodecamp.org/news/content/images/2023/02/10-18---1.png" alt="10*18网格" width="499" height="809" loading="lazy"><br>
将主游戏区看作一个10*18的网格，可以通过单项数据为0的<strong>二维数组</strong>实现：</p>
<pre><code class="language-js">//src/utils/index.js

const boardDefault = () =&gt; {
    const rows = 18; //共18行
	const cols = 10; //共10列	
	const array = Array.from(Array(rows), () =&gt; Array(cols).fill(0));
	return array;
};
</code></pre>
<blockquote>
<p>注：在示意图中，我标注了x和y象限，因为之后会使用x和y的值来判断形状在主游戏区的位置。</p>
</blockquote>
<h4 id="">形状的实现</h4>
<p>既然主游戏区可以通过数组来实现，那么不同形状的方块也可以通过数组来实现：</p>
<p>设定每一种形状都用一个4*4的网格作为容器：</p>
<pre><code class="language-js">[

	[0, 0, 0, 0],
	[0, 0, 0, 0],
	[0, 0, 0, 0],
	[0, 0, 0, 0],

],
</code></pre>
<p>在俄罗斯方块游戏中，不同的形状的颜色不同，所以可以用整数<code>1,2,3,4...</code>来区分它们。</p>
<p>使用整数的<strong>另一个好处</strong>是可以和<strong>样式绑定</strong>，先提前设置好每个颜色对应的编号：</p>
<pre><code class="language-css">:root {
	--color-0: #282c34;
	--color-1: #ff6600;
	--color-2: #eec900;
	...
	--color-7: #ff0000;
}

.color-0 {
	background-color: var(--color-0);
}

.color-1 {
	background-color: var(--color-1);
}

...
  
.color-7 {
	background-color: var(--color-7);
}
</code></pre>
<p>然后就可以通过数字<code>1,2,3,4...</code>在前文的4*4网格数组中的排列来表达不同的形状了，需要注意的是每个形状还有旋转后的样式也要考虑进去。</p>
<p><img src="https://chinese.freecodecamp.org/news/content/images/2023/02/--2023-02-28-15.55.33.png" alt="长条形状" width="124" height="107" loading="lazy"></p>
<p>比方说一个长条形状及它旋转后可以表示为：</p>
<pre><code class="language-js">//I

[
	[
		[0, 0, 0, 0],
		[1, 1, 1, 1],
		[0, 0, 0, 0],
		[0, 0, 0, 0],
	],

	[
		[0, 1, 0, 0],
		[0, 1, 0, 0],
		[0, 1, 0, 0],
		[0, 1, 0, 0],
	],
],
</code></pre>
<ul>
<li>把单个形状及其旋转后的样式放在同一个数组中，引用时就可以通过<strong>索引号</strong>来获得形状的旋转效果。</li>
<li>再把所有形状的数组汇总到一个数组中，这样通过<strong>索引号</strong>就把表示颜色的数字和形状挂钩了。</li>
</ul>
<h3 id="">形状到主游戏区的映射</h3>
<p>我们完成了主游戏区和形状分别的实现，那么当游戏进行时，如何把形状映射到主游戏区呢？</p>
<p>在React中，每一次渲染都相当于一次快照，那么就可以把游戏进行时，形状在主游戏区域的每一次移动都看作独立的一次<em>形状数组到主游戏区数组的映射</em>：</p>
<p><img src="https://chinese.freecodecamp.org/news/content/images/2023/03/--2023-03-01-09.54.06.png" alt="快照" width="276" height="385" loading="lazy"><br>
<img src="https://chinese.freecodecamp.org/news/content/images/2023/03/----.jpeg" alt="快照数组" width="734" height="1024" loading="lazy"></p>
<p>具像化我们映射过程就是从以上第一幅图到第二幅图。</p>
<p>代码实现如下：</p>
<pre><code class="language-js">//src/features/game/components/Board.js

//这里的board变量是10*18的由0组成的二位数组
const boardSquare = board.map((rowArray, row) =&gt; {
	return rowArray.map((square, col) =&gt; {
	//col（0-9）row（0-17)
	const blockX = col - x;
	const blockY = row - y;
	//square为0
	let color = square;
	//生成移动的block的颜色
	if (
		blockX &gt;= 0 &amp;&amp;
		blockX &lt; block.length &amp;&amp;
		blockY &gt;= 0 &amp;&amp;
		blockY &lt; block.length
	) {
    //这里的block变量引用了从函数外部取得的表示形状的数组
		color = block[blockY][blockX] === 0 ? color : blockColor;
	}
	//生成key
	const k = row * board[0].length + col;
	return &lt;Square key={k} color={color} /&gt;;
	});
});
</code></pre>
<p>让我们看看上面代码发生了什么：</p>
<ul>
<li><code>x</code>,<code>y</code>是形状起始的横轴（行）和纵轴（列）的坐标，我设定的初始值为<code>x=4, y=-5</code>，这样每一个形状的初始位置就位于主游戏区差不多正上方，随着形状的移动，这两个值也会发生变化；</li>
<li><code>boardSquare</code>函数实际上做了两件事：将我们用数组表达的10*8主游戏区展示到UI；根据主游戏区纵坐标（0-17）、横坐标（0-9）与<code>x</code>、<code>y</code>的差值来定位，将形状数组映射到主游戏区；</li>
<li>首先通过一组嵌套结构的<code>Array.map()</code>的方法，遍历主游戏区所有网格，并将所有网格的<code>color</code>设置为<code>0</code>；</li>
<li>在遍历主游戏区的同时，通过blockX和blockY坐标来锁定位于形状数组内部的方块，如果它的值不为<code>0</code>的话，就返回对应数字的颜色；</li>
<li>因为React要求遍历的组件包含一个独一无二的<code>key</code>，所以通过<code>row * board[0].length + col</code>生成key。</li>
</ul>
<h2 id="">判断边界的逻辑</h2>
<h3 id="">移动范围边界</h3>
<p>形状在主游戏区的移动遵守一定的规则：</p>
<ol>
<li>左右下边都不可越界</li>
<li>如果在主游戏区碰到其他的形状，也不得移动。</li>
</ol>
<p>我们只用检查形状内部每一个方块是否符合以上规则，就可以判断形状是否可以移动了。</p>
<p>此处还需要注意的是，每一形状都被放置在4*4的网格中，所以要排除方块值为<code>0</code>情况：</p>
<pre><code class="language-js">//src/utils/index.js

const canMoveTo = (shape, board, x, y, rotation) =&gt; {
	const currentShape = shapes[shape][rotation];
	for (let row = 0; row &lt; currentShape.length; row++) {
		for (let col = 0; col &lt; currentShape[row].length; col++) {
			if (currentShape[row][col] !== 0) {
				const proposedX = col + x;
				const proposedY = row + y;
			if (proposedY &lt; 0) {
				continue;
				}
			const possibleRow = board[proposedY];
			if (possibleRow) {
				if (
					possibleRow[proposedX] === undefined ||
					possibleRow[proposedX] !== 0
					) {
						return false;
					}
				} else {
				//超越底线
						return false;
					}
				}
			}
		}
	//默认可以移动
	return true;
};
</code></pre>
<ul>
<li>使用<code>canMoveTo</code>函数来判断是否可以移动，函数接受<code>shape</code>（形状数组）、<code>board</code>（主游戏区数组）、<code>x,y</code>（形状定位坐标）、<code>rotation</code>（旋转索引号）作为参数，返回布尔值。</li>
<li>首先默认可以移动；</li>
<li>利用两个嵌套<code>Array.map()</code>遍历形状数组内部的方块，如果方块的值为非<code>0</code>，就根据<code>x</code>和<code>y</code>的值来判断方块位于游戏主区的位置，即<code>proposedX</code>和<code>proposedY</code>；</li>
<li>因为形状的初始情况是位于游戏主区上方，不在主区内，所以存在<code>proposedY</code>小于<code>0</code>的情况，这个时候继续遍历就好；</li>
<li>根据<code>propsedY</code>可以推断出方块位于主游戏区的行数<code>possibleRow</code>，如果不存在<code>possibleRow</code>的话，则说明方块已经超越了主游戏区的<strong>底边</strong>，返回<code>false</code>；</li>
<li>用<code>possibleRow[proposedX]</code>来定位形状方块位于主游戏区该行的哪一个方块，如果值不为<code>0</code>或者为<code>undefined</code>的话，说明主游戏区上<strong>已经有其他形状的方块</strong>或者<strong>超越主游戏区左右边界</strong>，返回<code>false</code>。</li>
</ul>
<h3 id="">消除条件</h3>
<p>当主游戏区一整行填满形状方块就会消除，放在数组的框架下思考，就是检查一行是否全不为<code>0</code>。如果符合条件就删除一整行，然后在数组最开始重新添加一行：</p>
<pre><code class="language-js">//src/utils/index.js

const checkRows = (board) =&gt; {
	for (let row = 0; row &lt; board.length; row++) {
	//检查是否一整行都不为'0'
		if (board[row].indexOf(0) === -1) {
		//如果是，删除这一行
				board.splice(row, 1); 
				//同时在数组最开始添加一行新的数组
				board.unshift(Array(10).fill(0));
			}
	}
}
</code></pre>
<h2 id="">状态管理</h2>
<h3 id="reduxtoolkit">使用Redux Tool Kit</h3>
<p>在这个应用中，我使用Redux Tool Kit（后文简称RTK）来实现状态管理。</p>
<p>RTK对比传统的Redux来说的<strong>优势</strong>在于：</p>
<ol>
<li>更少的样板代码</li>
<li>RTK通过Immer库来实现不可变数据，提高应用程序的性能。</li>
</ol>
<h4 id="slice">创建Slice</h4>
<p>在RTK中，reducer和action集合到slice中，可以通过引用<code>createSlice</code>来实现：</p>
<pre><code class="language-js">//src/features/game/game-slice.js

import {createSlice} from '@reduxjs/toolkit'

const gameSlice = createSlice({
	name:'game',
	initialState: {...},
	reducer: {
		rotate:(state, action) =&gt;{
			...
			},
		moveRight: (state, action) =&gt;{
			...
			},
			...
		},
})

export const {rotate, moveRight} = gameSlice.actions
export default gameSlice.reducer;
</code></pre>
<blockquote>
<p>注：在传统方式中若要修改不可变数据，需要手动复制原始数据，并在副本上进行修改，由于immer库的存在，在编写reducer的时候，可以直接修改<code>state</code>，详细代码可以查看github仓库。</p>
</blockquote>
<h4 id="store">创建store</h4>
<pre><code class="language-js">//src/store.js

import { configureStore } from '@reduxjs/toolkit';
import gameReducer from './features/game/game-slice';

export const store = configureStore({ reducer: { game: gameReducer}})
</code></pre>
<h4 id="">将数据提供给应用</h4>
<pre><code class="language-js">//src/index.js

import { Provider } from 'react-redux';
import { store } from './store';

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(
	&lt;React.StrictMode&gt;
		&lt;Provider store={store}&gt;
			&lt;App /&gt;
		&lt;/Provider&gt;
	&lt;/React.StrictMode&gt;
);
</code></pre>
<h4 id="">使用数据</h4>
<pre><code class="language-js">//src/features/game/components/Control.js

import { useSelector, useDispatch } from 'react-redux';
import { rotate } from '../game-slice';

export default function Controls() {
	const dispatch = useDispatch();
	const isRunning = useSelector((state) =&gt; state.game.isRunning);
    const gameOver = useSelector((state) =&gt; state.game.gameOver);

	const handleRotate = () =&gt; {
		if (!isRunning || gameOver) return;
		dispatch(rotate());
	};

	return (
		&lt;div className="controls"&gt;
		{/* rotation */}
			&lt;button
				disabled={!isRunning || gameOver}
				className="control-button"
				onClick={handleRotate}
			&gt;						
			 Rotate
			&lt;/button&gt;
</code></pre>
<h3 id="">下落动画的实现</h3>
<h4 id="deltatime">Delta Time</h4>
<p>Delta time（Δt）是一个计算两个时间点之间经过的时间差的量。在游戏开发中，delta time 指的是每一帧之间的时间间隔。</p>
<p>实现方块的下落动画，会运用到<code>delta time</code>。</p>
<p>这里我们需要计算两次调用<code>requestAnimationFrame</code>的时间差的值，如果累计差值比我们设定的游戏速度要大，就触发<code>moveDown</code>action。</p>
<p>形状的下落实际上就是在一定时间间隔下更新画面（触发moveDown这个action），不就是定时器嘛！</p>
<h4 id="requestanimationframeuserefuseeffect">使用<code>requestAnimationFrame</code>、<code>useRef</code>和<code>useEffect</code>实现定时器的原因</h4>
<ul>
<li><code>requestAnimationFrame</code></li>
</ul>
<p>一般想到定时器会想到<code>setInterval</code>和<code>setTimeout</code>，但实际上使用<code>requestAnimationFrame</code>的性能更好。</p>
<p>这是因为<code>requestAnimationFrame</code>更符合屏幕的刷新率，通常屏幕的刷新率是60Hz，也就意味着浏览器每间隔16.7毫秒（1000毫秒/60）就会更新一次页面。<code>requestAnimationFrame</code>的回调函数会在浏览器准备好下一帧时调用，从而确保每一帧都能够在屏幕刷新之前完成绘制。</p>
<p>但<code>setTimeout</code>和<code>setInterval</code>的时间并不精准，可能会出现卡顿、延迟等性能问题。</p>
<ul>
<li><code>useRef</code></li>
</ul>
<p>React中的函数组件就像一个快照，每次渲染完成后值就会丢失。所以如果需要让函数“记住”值的话，就需要使用额外的钩子。</p>
<p>在俄罗斯方块游戏中，我们需要对形状下落的帧进行记录，这个值不受组件的生命周期影响，因此需要<code>useRef</code>。</p>
<ul>
<li><code>useEffect</code></li>
</ul>
<p>游戏中只需要在特定时间调用控制下落的函数，并且这个过程是<code>Board.js</code>组件的一个副作用，因此需要使用<code>useEffect</code>。</p>
<pre><code class="language-js">//src/utils/useTimer.js

import { useEffect, useRef } from 'react';
import { useDispatch } from 'react-redux';

export function useTimer(flagVal, benchmark, func) {
	const requestRef = useRef();
	const lastUpdateTimeRef = useRef(0);
	const progressTimeRef = useRef(0);
	
	const dispatch = useDispatch();

	const update = (time) =&gt; {
		requestRef.current = requestAnimationFrame(update);
		if (!flagVal) return;
		if (!lastUpdateTimeRef.current) {
			lastUpdateTimeRef.current = time;
		}
	
		const deltaTime = time - lastUpdateTimeRef.current;
		progressTimeRef.current += deltaTime;
	
		if (progressTimeRef.current &gt; benchmark) {
		dispatch(func());
		progressTimeRef.current = 0;
		}
		lastUpdateTimeRef.current = time;
	};

	useEffect(() =&gt; {
		requestRef.current = requestAnimationFrame(update);
		return () =&gt; cancelAnimationFrame(requestRef.current);
	}, [flagVal]);
}
</code></pre>
<ul>
<li>使用<code>useRef</code>创建了三个引用变量<code>requestRef</code>、<code>lastUpdateTimeRef</code>和<code>progressTimeRef</code>，用于存储定时器的状态和进度。</li>
<li><code>requestRef</code>用于存储当前动画帧的引用， <code>lastUpdateTimeRef</code>用于存储上一次更新的时间戳，<code>progressTimeRef</code>用于存储已经过去的时间。</li>
<li>在<code>update</code>函数中实现定时器逻辑。</li>
<li>首先使用<code>requestAnimationFrame</code>来获取下一帧的引用，并存储到<code>requestRef.current</code>中，以实现流畅的动画效果；</li>
<li>然后通过<code>flagVal</code>的值，来决定是否退出函数；</li>
<li>接着如果没有<code>lastUpdateTimeRef.current</code>的值，就将当前的时间戳(<code>time</code>)存储到里面。</li>
<li>创建<code>deltaTime</code>，它的值为当前时间戳和<code>lastUpdateTimeRef.current</code>的时间差，将每次更新得到的<code>deltaTime</code>都添加到<code>progressTimeRef.current</code>中，记录累计过去的时间；</li>
<li>如果这个累计值大于我们设定的时间间隔<code>benchmark</code>，就执行传入的函数，执行完毕后，将累计时间清零；</li>
<li>最后将当前时间戳存储在<code>lastUpdateTimeRef.current</code>中，以便下一次调用时使用。</li>
<li>在使用这个定时器时，我们传入应用的状态属性<code>isRunning</code>作为<code>flagVal</code>来判断是否需要退出函数，<code>speed</code>作为<code>benchmark</code>来和累积值做对比，控制形状的下移动画的速度，<code>moveDown</code>作为<code>func</code>来实现方块下移的动作。</li>
</ul>
<h2 id="">总结</h2>
<p>本项目通过数组实现了基本的游戏静态画面，动画部分使用了<code>requestAnimationFrame</code>，游戏复杂的状态管理借助了更简洁的RTK。</p>
<p>如果你仔细观察项目代码，会发现我还使用了第三方库Redux Persist来存储最高分数。</p>
<p>如果你想要动手实验一下我在文章中提到的逻辑和状态管理，我准备了<a href="https://github.com/PapayaHUANG/react-redux-tetris-starter">starter仓库</a>，它包含了基础的框架和样式，你可以在此基础上搭建属于你的俄罗斯方块，当然你也可以在我的项目基础上添加新的内容，如不同的关卡（下落速度不同），将按键和键盘绑定等。</p>
<p>希望阅读这篇文章让你有所收获，祝你实验愉快！</p>
<h2 id="">代码仓库</h2>
<ul>
<li><a href="https://github.com/PapayaHUANG/react-redux-tetris">完整项目</a></li>
<li><a href="https://github.com/PapayaHUANG/react-redux-tetris-starter">仅UI部分</a></li>
<li><a href="https://papayahuang.github.io/react-redux-tetris/">试玩</a></li>
</ul>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 浅析 React Hooks：论如何优雅地利用闭包 ]]>
                </title>
                <description>
                    <![CDATA[ 前言 在通关 JS 的道路上你一定也遇到过两个难缠的大 BOSS：this的指向和闭包，而且不能逃课。 在使用 React 时，如果你习惯编写 class 组件，就绕不开this；如果你使用函数组件，就一定会用到 React Hooks，而 React Hooks 又大量使用闭包。（闭包真的无处不在呢！） 本文将用代码示例，仿写一个简化版的 useState Hook，浅析闭包在 React 中的应用。当然本文的代码示例不代表 React 源码的真实情况。 前置条件 想要彻底理解本文内容需要你：  * 对 JS 闭包有大概了解。不太了解也没关系，我会在本文回顾。  * 浏览过 React Hooks 文档 [https://zh-hans.reactjs.org/docs/hooks-intro.html]，最好是使用过    React Hooks。 Let's get our hands dirty! “闭包”是什么？ 请允许我引用来自《你不知道的 JavaScript》对闭包的定义： > ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/react-hooks-how-to-elegantly-leverage-closures/</link>
                <guid isPermaLink="false">63e5d5dfe673d23d28878348</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ PapayaHUANG ]]>
                </dc:creator>
                <pubDate>Wed, 15 Feb 2023 01:19:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/02/ferenc-almasi-tvHtIGbbjMo-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <h2 id="">前言</h2>
<p>在通关 JS 的道路上你一定也遇到过两个难缠的大 BOSS：<code>this</code>的指向和闭包，而且不能逃课。</p>
<p>在使用 React 时，如果你习惯编写 class 组件，就绕不开<code>this</code>；如果你使用函数组件，就一定会用到 React Hooks，而 React Hooks 又大量使用闭包。（闭包真的无处不在呢！）</p>
<p><img src="https://chinese.freecodecamp.org/news/content/images/2023/02/giphy.gif" alt="voldmort laugh" width="241" height="203" loading="lazy"></p>
<p>本文将用代码示例，仿写一个简化版的 useState Hook，浅析闭包在 React 中的应用。当然本文的代码示例不代表 React 源码的真实情况。</p>
<h2 id="">前置条件</h2>
<p>想要彻底理解本文内容需要你：</p>
<ul>
<li>对 JS 闭包有大概了解。不太了解也没关系，我会在本文回顾。</li>
<li>浏览过 React Hooks <a href="https://zh-hans.reactjs.org/docs/hooks-intro.html">文档</a>，最好是使用过 React Hooks。</li>
</ul>
<p>Let's get our hands dirty!</p>
<h2 id="">“闭包”是什么？</h2>
<p>请允许我引用来自《你不知道的 JavaScript》对闭包的定义：</p>
<blockquote>
<p>当函数可以记住并访问所在的词法作用域时，就产生了闭包，即使函数是在当前词法作用域之外执行。</p>
</blockquote>
<p>第一次读到这句话的时候，我深刻地体会到了什么叫“每一个字我都认识，但是我完全不知道它说了什么”的绝望。现在回看，其实《你不知道的 JavaScript》的章节设计得很好，想要真的理解<code>闭包</code>这个概念，推荐熟读前面<code>词法作用域</code>部分，了解了作用域，如何查找变量，闭包问题也就引刃而解了。</p>
<p>对于我个人来说，开悟的那一刻是当我把<code>闭包</code>的英文<code>closure</code>转换成动词词组<code>close over</code>来理解的那一刻。<code>close</code>就是<strong>打包了</strong>当前作用域，而<code>over</code>是<strong>超越了</strong>自己的作用域，在作用域之外也可以执行。</p>
<p><img src="https://chinese.freecodecamp.org/news/content/images/2023/02/mehedi-hasan-k-sh-_jFTWvKrwyY-unsplash.jpg" alt="lightbulb" width="2000" height="2667" loading="lazy"></p>
<p><em>顺便提一句，很多概念直接通过英文来理解会容易很多，另一个例子是作用域的英文：<code>scope</code>。技术书能读英文原版就读原版，比方说《JavaScript 忍者秘籍》，很好的一本书，但是中文翻译了个啥？？？</em></p>
<p>废话就说这么多，请看一个经典的代码示例：</p>
<pre><code class="language-js">function addOne() {
  let num = 1;
  return function inner() {
    num = num + 1;
    return num;
  };
}

const outer = addOne();

console.log(outer()); // 2
console.log(outer()); // 3
console.log(outer()); // 4
console.log(outer()); // 5
num = 999; // 报错
</code></pre>
<p>让我们一起看看在上面的代码片段发生了什么？</p>
<ul>
<li>当我们在调用<code>outer</code>函数的时候，实际调用的是<code>addOne</code>的内部的<code>inner</code>函数。</li>
<li><code>inner</code>函数引用了其作用域之外的<code>num</code>的值。</li>
<li><code>inner</code>函数在自己的作用域（即<code>addOne</code>函数）之外（即<code>outer</code>函数）中执行。</li>
<li>我们通过在<code>addOne</code>函数中返回<code>inner</code>函数（close），并在外部调用（over）实现了一个经典的闭包（closure）。</li>
</ul>
<p>而通过打印到控制台的结果，发现实现了两个效果：</p>
<ol>
<li><code>num</code>为私有变量，尝试在全局修改变量的值会报错。私有变量也是闭包的常用场景之一。</li>
<li>反复调用<code>outer</code>函数可以实现数字的等差累加（+1），也就是说<code>outer</code>是一个<strong>有状态的函数</strong>（stateful function）。</li>
</ol>
<h3 id="statefulfunction">闭包与有状态函数（stateful function）</h3>
<p>有状态函数就是利用<strong>闭包</strong>，使得函数拥有内部状态，这样每一次调用函数会基于上一次的结果产生不一样的结果。（可以对比纯函数的概念理解）</p>
<p>看到这里你是不是想到了什么？React 不就是利用 React Hooks 对组件进行<em>状态管理</em>嘛？！</p>
<h2 id="usestate">仿写 useState</h2>
<h3 id="usestate">编写 useState 函数</h3>
<p>那让我们进入正题，从仿写<code>useState</code>开始。</p>
<p>根据<code>useState</code>的结构，可以得出如果编写一个<code>useState</code>函数的话，需要返回一个数组，包含一个值和一个相关的 setter 函数：</p>
<pre><code class="language-js">function myUseState(initValue) {
  let _value = initValue;
  const state = _value;
  const setState = (newValue) =&gt; {
    _value = newValue;
  };
  return [state, setState];
}
</code></pre>
<p>让我们看看上面的代码片段发生了什么：</p>
<ul>
<li><code>useState</code>函数返回了一个数组，这个数组包含两个变量：<code>state</code>和一个 setter 函数<code>setState</code>。</li>
<li><code>useState</code>函数通过内部变量<code>_value</code>记住了我们传入的初始值，<code>setState</code>引用了这个值，并做修改。</li>
</ul>
<p>在真实的 React 中，我们是通过<code>React.useState</code>来调用这个 hook，那我们需要调整一下上面这个函数的写法，将函数封装在 React 中。</p>
<p>我们先写个简单的练手，把前文的<code>outer</code>函数改写一下：</p>
<pre><code class="language-js">const outer = (function addOne() {
  let num = 1;
  return function inner() {
    num = num + 1;
    return num;
  };
})();

console.log(outer()); // 2
console.log(outer()); // 3
console.log(outer()); // 4
console.log(outer()); // 5
</code></pre>
<p>利用 IIFE 将代码封装后和之前的输出结果一致，同理就可以把<code>myUseState</code>函数改写为：</p>
<pre><code class="language-js">const MyReact = (function () {
  function myUseState(initValue) {
    let _value = initValue;
    const state = _value;
    const setState = (newValue) =&gt; {
      _value = newValue;
    };
    return [state, setState];
  }
  return { myUseState };
})();
</code></pre>
<p>让我们测试一下使用：</p>
<pre><code class="language-js">const [count, setCount] = MyReact.myUseState(1);

console.log(count); //1
setCount(count + 1);
console.log(count); //1
</code></pre>
<p>为什么<code>setCount</code>失效了？让我们来分析一下调用<code>setCount(count + 1)</code>的时候发生了什么：</p>
<ul>
<li><code>setCount</code>是在外部调用了<code>setState</code>函数（over）</li>
<li><code>setState</code>可以访问和修改<code>myUseState</code>函数内部<code>_value</code>的值（close）</li>
<li><code>setState</code>是一个闭包，并且当我们在调用<code>setCount(count + 1)</code>时，确实改变了<code>_value</code>的值</li>
<li>但<code>count</code>拿到的值是返回数组的第一项，并不持续追踪<code>_value</code>值的变化</li>
</ul>
<p>让我们再回到<strong>闭包</strong>的概念：</p>
<blockquote>
<p>当函数可以记住并访问所在的词法作用域时，就产生了闭包，即使函数是在当前词法作用域之外执行。</p>
</blockquote>
<p>很容易以为误闭包记住的是值，但实际上闭包 close（记住）的是<strong>作用域</strong>。那我们只需要把<code>state</code>也修改为<code>_value</code>的闭包，让 <code>state</code> 也记住它的作用域就可以了：</p>
<pre><code class="language-js">const MyReact = (function () {
  function myUseState(initValue) {
    let _value = initValue;
    const state = () =&gt; _value; //state 为 _value 的闭包
    const setState = (newValue) =&gt; {
      _value = newValue;
    };
    return [state, setState]; //外部可以调用 state
  }
  return { myUseState };
})();
</code></pre>
<p>对应打印到控制台的代码也要修改：</p>
<pre><code class="language-js">const [count, setCount] = MyReact.myUseState(1);

console.log(count()); //1
setCount(count() + 1);
console.log(count()); //2
</code></pre>
<h3 id="">编写函数组件</h3>
<p>让我们继续：</p>
<p>在真实的 React Hook 的使用场景中，我们是在一个函数组件中调用 Hook，让我们边调整边完善：</p>
<pre><code class="language-js">function MyComponent() {
  const [count, setCount] = MyReact.myUseState(1);

  return {
    //此处应该为 JSX
  };
}
</code></pre>
<p>假设返回的 JSX 是一个可以展示<code>count</code>的值的标签，以及一个按钮，每次点击之后<code>count</code>的值加 1。</p>
<p>为了简化案例，我们把 JSX 改写为打印到控制台，这样 <code>return</code> 返回的对象就包含两个方法：</p>
<ul>
<li>一个<code>render</code>方法，在控制台输出<code>count</code>的值；</li>
<li>一个<code>click</code>方法，模拟按钮，每次调用改变<code>count</code>的值。</li>
</ul>
<pre><code class="language-js">function MyComponent() {
  const [count, setCount] = MyReact.myUseState(1);

  return {
    render: () =&gt; console.log(count),
    click: () =&gt; setCount(count + 1),
  };
}
</code></pre>
<p>这样我们就构建好了一个函数组件。</p>
<h3 id="">编写渲染方法</h3>
<p>通常我们使用<code>ReactDOM.render（要渲染的内容，在哪里渲染）</code>将函数组件渲染到页面，那么我们的仿写还差一个渲染方法：</p>
<pre><code class="language-js">const MyReact = (function () {
  function myUseState(initValue) {
    ...
    return [state, setState];
  }

  function myRender(Component){
    const _c = Component();
    _c.render();//在控制台打印状态
    return _c;
  }

  return { myUseState, myRender };
})();
</code></pre>
<p>由于在编写函数组件的时候，我们返回的<code>render</code>方法是<code>() =&gt; console.log(count)</code>，直接在控制台打印状态，所以我们对<code>myUseState</code>要稍作修改：</p>
<pre><code class="language-js">const MyReact = (function () {
  let _val; //放在最外层
  function myUseState(initVal) {
    const state = _val || initVal;//不需要使用函数
    const setState = (newVal) =&gt; {
      _val = newVal;
    };
    return [state, setState];
  }
  ...
  return { myUseState, myRender };
})();
</code></pre>
<p>现在完整的代码为：</p>
<pre><code class="language-js">const MyReact = (function () {
  let _value; //在 MyReact 最外层声明变量
  function myUseState(initValue) {
    const state = _val || initVal; //不需要使用函数
    const setState = (newValue) =&gt; {
      _value = newValue;
    };
    return [state, setState];
  }
  function myRender(Component) {
    const _c = Component();
    _c.render(); //在控制台打印状态
    return _c;
  }
  return { myUseState, myRender };
})();

function MyComponent() {
  const [count, setCount] = MyReact.myUseState(1);

  return {
    render: () =&gt; console.log(count),
    click: () =&gt; setCount(count + 1),
  };
}
</code></pre>
<p>我们来查看一下调用效果：</p>
<pre><code class="language-js">var App = MyReact.myRender(MyComponent); //1
App.click();
var App = MyReact.myRender(MyComponent); //2
</code></pre>
<p>简要分析一下在这个代码片段里发生了什么：</p>
<ul>
<li><code>MyReact.myRender</code>方法对我们编写的组件<code>MyReactComponent</code>进行调用，直接将<code>count</code>的值打印到控制台，同时也暴露了<code>click</code>方法；</li>
<li>使用<code>App.click</code>调用暴露的方法，在非<code>setCount</code>的词法作用域调用<code>setCount</code>方法（over），修改了之前的<code>_val</code>值（close）；</li>
<li>当我们再次利用<code>myRender</code>方法获取<code>count</code>值时，<code>count</code>拿到的是修改后<code>_val</code>的值，就从<code>1</code>变成了<code>2</code>。</li>
</ul>
<p>做得不错！</p>
<p>但往往我们在一个函数组件中使用不止一个<code>useState</code>：</p>
<pre><code class="language-js">function MyComponent() {
  const [count, setCount] = MyReact.myUseState(1);
  const [text, setText] = MyReact.myUseState('freeCodeCamp');

  return {
    render: () =&gt; console.log({ count, text }), //同时展现两个变量
    click: () =&gt; setCount(count + 1),
    type: (newText) =&gt; setText(newText), //假设有一个输入框
  };
}
</code></pre>
<p>打印控制台就会出现诡异的效果：</p>
<pre><code class="language-js">var App = MyReact.myRender(MyComponent); //{"count": 1, "text": 1}
App.click();
var App = MyReact.myRender(MyComponent); //{"count": 2, "text": 2}
</code></pre>
<p>这是因为我们只有<code>_val</code>这一个值来存储变量，这个时候我们就需要引入数组：</p>
<pre><code class="language-js">const MyReact = (function () {
  let hooks = [];
  let idx = 0;
  function myUseState(initVal) {
    const state = hooks[idx] || initVal;
    const _idx = idx; //记住这个值
    const setState = (newVal) =&gt; {
      hooks[_idx] = newVal; //调用 setState 修改的是创建 state 同索引所指向的值
    };
    idx++; //这样才能在 hook 数组中记录下一个值
    return [state, setState];
  }

  function myRender(Component) {
    idx = 0; //每一次渲染的时候，索引要回到 0
    const C = Component();
    C.render();
    return C;
  }
  return { myUseState, myRender };
})();
</code></pre>
<p>重新调用：</p>
<pre><code class="language-javascript">var App = MyReact.myRender(Component); //{"count": 1, "text": 'freeCodeCamp'}
App.click();
var App = MyReact.myRender(Component); //{"count": 2, "text": 'freeCodeCamp'}
App.setText('awesome!');
var App = MyReact.myRender(Component); //{"count": 2, "text": 'awesome!'}
</code></pre>
<p>这里要稍微解释一下为什么在<code>myRender</code>函数中，一定要将<code>idx</code>的值重新设置为<code>0</code>。</p>
<p>因为每一次调用<code>MyReact.myRender</code>都是<strong>独立的</strong>，如果不将索引设置为<code>0</code>的话，多次调用<code>useState</code>会将在上次索引上继续索引，这样上次索引的记录就不会被重写，每次渲染<code>Component</code>时就会返回同样的状态值和 setter 函数。</p>
<h2 id="">总结</h2>
<p>探索闭包和 React Hooks 之间的关系是一段有趣的旅程。</p>
<p>你也可以尝试仿写其他 hook 方法。</p>
<p>希望这篇文章可以启发你，在以后的打怪之路上优雅地理解和利用闭包。<br>
<img src="https://chinese.freecodecamp.org/news/content/images/2023/02/--.jpeg" alt="it's only the beginning" width="259" height="194" loading="lazy"></p>
<h2 id="">参考资料</h2>
<p><a href="https://www.netlify.com/blog/2019/03/11/deep-dive-how-do-react-hooks-really-work/">Swyx - Deep dive: How do React hooks really work?</a><br>
<a href="https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e">Rudi Yardley - React hooks: not magic, just arrays</a></p>
<!--kg-card-end: markdown--><p></p><p></p><p> </p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ DOM 幕后揭秘 ]]>
                </title>
                <description>
                    <![CDATA[ 对于一名高效工作的前端开发者来说，了解 JavaScript 中的 DOM 和事件是如何运行非常重要。 本文将讲解 DOM 是什么以及如何运行的。 DOM 是什么 DOM 的全称是文档对象模型，是连接 JavaScript 和 web 浏览器之间的接口。 借助 DOM，你可以编写 JavaScript 来创建、修改、删除 HTML 元素，设置样式、类别和属性，监听以及响应事件。 HTML 文档生成 DOM 树之后，你就可以与之交互。DOM 是一个复杂的 API，包含与 DOM 树交互的方法和属性。 DOM 图解你可以在这个网站 [https://fritscher.ch/dom-css/]视觉化 DOM。 DOM 是如何运行的 – 幕后揭秘 DOM 的组织形式非常巧妙。顶层元素被称作EventTarget。你可以借助下图更好地了解工作原理。 EventTarget接口由可以接收事件、并且可以创建监听器的对象实现。换句话说，任何事件目标都会实现与该接口有关的这三个方法。虽然 Element  及其子项、Document  和 Window  是最常见的事件目标（Even ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/what-is-dom-in-javascript/</link>
                <guid isPermaLink="false">63e9aefdcf34b8063af88b0f</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ DOM ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ PapayaHUANG ]]>
                </dc:creator>
                <pubDate>Fri, 10 Feb 2023 09:30:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/02/What-is-DOM-and-Events--in-JavaScipt--2--1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/what-is-dom-in-javascript/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">What is the DOM? A Behind-the-Scenes Guide</a>
      </p><!--kg-card-begin: markdown--><p>对于一名高效工作的前端开发者来说，了解 JavaScript 中的 DOM 和事件是如何运行非常重要。</p>
<p>本文将讲解 DOM 是什么以及如何运行的。</p>
<h2 id="dom">DOM 是什么</h2>
<p>DOM 的全称是文档对象模型，是连接 JavaScript 和 web 浏览器之间的接口。</p>
<p>借助 DOM，你可以编写 JavaScript 来创建、修改、删除 HTML 元素，设置样式、类别和属性，监听以及响应事件。</p>
<p>HTML 文档生成 DOM 树之后，你就可以与之交互。DOM 是一个复杂的 API，包含与 DOM 树交互的方法和属性。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2022/09/Frame-70-1.png" alt="DOM 图解" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>DOM 图解</figcaption>
</figure>
<p>你可以在<a href="https://fritscher.ch/dom-css/">这个网站</a>视觉化 DOM。</p>
<h2 id="dom">DOM 是如何运行的 – 幕后揭秘</h2>
<p>DOM 的组织形式非常巧妙。顶层元素被称作<code>EventTarget</code>。你可以借助下图更好地了解工作原理。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/09/DOM-behind-the-scene-1.png" alt="DOM-behind-the-scene-1" width="600" height="400" loading="lazy"></p>
<p><code>EventTarget</code>接口由可以接收事件、并且可以创建监听器的对象实现。换句话说，任何事件目标都会实现与该接口有关的这三个方法。虽然 <strong>Element</strong> 及其子项、<strong>Document</strong> 和 <strong>Window</strong> 是最常见的事件目标（EventTarget），但其他对象也可以是。</p>
<p>Window 代表了浏览器窗口。所有全局的 JavaScript 对象、函数和变量都自动归属于 window 对象。全局变量是 window 对象的属性，全局函数是 window 对象的方法。即便是文档对象（HTML DOM）也是 window 对象的属性。</p>
<pre><code class="language-js">window.document.getElementById("header");

//两者相同

document.getElementById("header");
</code></pre>
<p>Nodes 归属于 DOM（即文档对象模型）。在 DOM 中，如元素、属性、文本等都被组织在一个分层级的树状结构中，相互为子或父元素。这些个体就被称为 node（节点）。</p>
<p>上图中的 Node 可以被 JavaScript 对象表示。我们通常使用<code>document.querySelector()</code>, <code>document.getElementById()</code>等方法来调用这些节点。</p>
<p>现在让我们看看文档。</p>
<h2 id="dom">如何使用 DOM 来选择、创建和删除元素</h2>
<p>借助 DOM，我们可以使用 JavaScript 来选择、删除和创建元素。</p>
<h3 id="">如何选择元素</h3>
<p>JavaScript 中有各种各样的方式来选择 HTML 元素：</p>
<ul>
<li>document.getElementById();</li>
<li>document.getElementByClassName();</li>
<li>document.getElementByTagName();</li>
<li>document.querySelector();</li>
<li>document.querySelectorAll();</li>
</ul>
<h4 id="documentgetelementbyid">如何使用 <code>document.getElementById()</code> 方法</h4>
<p><code>getElementById()</code>方法返回的元素 id 与传入的字符串匹配。因为 HTML 元素的 id 是唯一值，所以这种方式的速度最快。</p>
<p>例子:</p>
<pre><code class="language-js">const ele = document.getElementById("IDName");
console.log(ele); // 打印对应id名称的元素
</code></pre>
<h4 id="documentgetelementbyclassname">如何使用<code>document.getElementByClassName()</code>方法</h4>
<p><code>document.getElementByClassName()</code>返回一个<code>HTMLCollection</code>，这个集合中的元素类名与传入的字符串匹配。我们可以同时搜索多个类别名称，传入的时候用空格隔开，它会实时返回的 HTMLCollection。</p>
<p>“实时”是什么意思？就是说，一旦我们给传入的类名添加一个元素，HTMLCollection 会自动获取这个新的元素。</p>
<p>例子:</p>
<pre><code class="language-js">const ele = document.getElementByClassName("ClassName");
console.log(ele); // 打印实时HTMLCollection
</code></pre>
<h4 id="documentgetelementbytagname">如何使用<code>document.getElementByTagName();</code>方法</h4>
<p><code>document.getElementByTagName()</code>返回一个<code>HTMLCollection</code>，这个集合中的元素标签与传入的字符串匹配。可以对任何 HTML 元素调用这个方法，它会返回实时的 HTMLCollection。</p>
<p>例子:</p>
<pre><code class="language-js">const paragraph = document.getElementByTagName("p");
const heading = document.getElementByTagName("h1");

console.log(paragraph); // p 元素 HTMLCollection
console.log(heading); // h1 元素 HTMLCollection
</code></pre>
<h4 id="documentqueryselector">如何使用 document.querySelector() 方法</h4>
<p><code>document.querySelector()</code>返回满足传入条件的第一个 HTML 元素。在这个方法中我们可以传入类名、id和标签名。参见例子：</p>
<pre><code class="language-js">const id = document.querySelector("#idname"); // 使用id
const classname = document.querySelector(".classname"); // 使用类名
const tag = document.querySelector("p"); // 使用标签名
</code></pre>
<p>选择规则：</p>
<ul>
<li>若使用类别名，在开头要添加(.)，如 (".classname")</li>
<li>若使用 id， 在开头要添加(#)，如("#id")</li>
<li>若使用标签，可以直接使用标签名，如("p")</li>
</ul>
<h4 id="documentqueryselectorall">如何使用 document.querySelectorAll() 方法</h4>
<p><code>document.querySelectorAll()</code>是<code>querySelector</code>方法的延伸。这个方法返回<strong>所有</strong>匹配传入值的元素。该方法返回一个不会实时更新的 NodeList。</p>
<pre><code class="language-js">const ele = document.querySelectorAll("p");
console.log(ele); // 返回 p 标签的 NodeList
</code></pre>
<p><strong>注意：</strong> HTMLCollection 是实时更新的，但是 NodeList 并不是。</p>
<h3 id="">如何创建元素</h3>
<p>你可以在 JavaScript 中创建 HTML 元素，并且动态地添加到 HTML 中。可以使用<code>document.createElement()</code>来添加元素，在括号内传入元素的标签名。</p>
<p>元素创建成功之后，可以添加类名，属性以及该元素的文本内容。</p>
<p><strong>例子：</strong></p>
<pre><code class="language-js">const ele = document.createElement("a");
ele.innerText = "Click Me";
ele.classList.add("text-left");
ele.setAttribute("href", "www.google.com");

// 类似于以下标记
// &lt;a href="www.google.com"&gt;Click Me&lt;/a&gt;

// 在 HTML 中更新已有的元素
document.querySelector(".links").prepend(ele);
document.querySelector(".links").append(ele);
document.querySelector(".links").before(ele);
document.querySelector(".links").after(ele);
</code></pre>
<p>在上述例子中，我们在 JavaScript 中创建了一个 anchor 标签，并且添加了类名和属性。在 HTML 中更新这个元素有以下四种方法：</p>
<ul>
<li><code>prepend()</code>：在父节点的第一个子节点之前插入数据</li>
<li><code>append()</code>：对位于最后一个索引的元素之后插入数据或内容</li>
<li><code>before()</code>：在选定的元素前插入数据</li>
<li><code>after()</code>：在选定的元素后插入数据。也可以说是在匹配的其父节点的子节点列表之后插入。（可以不作为元素插入，作为相邻的文本内容插入。）</li>
</ul>
<h3 id="">如何删除元素</h3>
<p>我们已经了解了如何使用 JavaScript 创建元素以及如何将它添加到 HTML 中。但是如果我们想要删除一个 HTML 元素呢？很简单！只需要对这个元素调用<code>remove()</code>方法。</p>
<p><strong>例子：</strong></p>
<pre><code class="language-js">const ele = document.querySelector("p");

// ele被点击后就会被删除
ele.addEventListener('click', function(){
	ele.remove();
})
</code></pre>
<h2 id="javascriptcss">如何通过 JavaScript 来控制 CSS</h2>
<p>我们已经学习了如何通过 JavaScript 来控制 HTML，现在我们来学习如何通过 JavaScript 来控制 CSS，帮助你动态地更改页面。</p>
<p>如果想要通过点击一个元素，就更改这个元素的背景颜色的话，可以通过 JavaScript 来实现。</p>
<p><strong>语法如下：</strong></p>
<pre><code class="language-js">const ele = document.querySelector("desired element");

ele.style.propertyName = value;
// e.g.
//ele.style.color = red;
</code></pre>
<p>当使用 JavaScript 来修改 CSS 属性的时候，需要注意在 CSS 中 <code>-</code> 用来连接单词，如 <code>background-color</code>但在 JavaScript 中我们使用 <code>backgroundColor</code>（驼峰样式）。</p>
<p><strong>例子：</strong></p>
<pre><code class="language-js">const ele = document.querySelector("div");
ele.style.backgroundColor = "red";
</code></pre>
<p>假设你已经给项目编写好了 CSS，你希望通过 JavaScript 来添加类名，可以使用<code>classList</code>。</p>
<p><strong>例子：</strong></p>
<pre><code class="language-js">const ele = document.querySelector(".link");
ele.classList.add("bg-red"); // 为已经存在的列表添加 bg-red 类名
ele.classList.remove("pb-4");// 为已经存在的列表删除 pb-4 类名
ele.classList.toggle("bg-green"); // 将 bg-red 类切换到现有的类列表中，这意味着如果它已经存在，那么它将被删除，如果它不存在，它将被添加。
</code></pre>
<p>当我们使用<code>classList</code>时，直接对元素进行添加、删除或切换类。这就像在更新现有的类。</p>
<p><code>element.className</code>的特别之处在于，它删除了所有现有的类别，然后添加一个新的类别。</p>
<p><strong>例子：</strong></p>
<pre><code class="language-js">const ele = document.querySelector(".link");
ele.classList.add("bg-red"); // 在现有的类别中添加 bg-red 类别
ele.classList.remove("pb-4");//从现有的类别中删除 pb-4 类别

ele.className = "p-10"; // 删除所有现有类别，添加 p-10
</code></pre>
<h2 id="">如何使用事件处理器</h2>
<p>对象状态的改变被称作为<strong>事件</strong>。对事件做出反应的过程被称为<strong>事件处理</strong>。</p>
<p>当用户点击鼠标、让鼠标在元素上滑过、按键等时，事件就发生了。所以当你想在事件发生时做一些事情的话，就可以使用事件处理器来引发这个事件。</p>
<p>我们使用事件处理器在事件发生的时候来执行一些 JavaScript 代码。在 JavaScript 中有许多不同的事件处理器（这里是一份简单的<a href="https://way2tutorial.com/html/html5_events_handler_list.php">列表</a>），但将事件处理器添加到元素的方法相同。</p>
<p><strong>语法如下：</strong></p>
<pre><code class="language-js">const ele = document.querySelector("a");

ele.addEventListener("event", function(){
	// 回调函数
});
</code></pre>
<p>你可以使用的事件</p>
<ul>
<li>click（点击鼠标）</li>
<li>mouseover（鼠标移动到某个元素）</li>
<li>mouseout（鼠标离开某个元素）</li>
<li>keypress（按下键盘）</li>
<li>keydown（按下键盘）</li>
</ul>
<p><strong>这里是使用 “click” 事件的例子：</strong></p>
<pre><code class="language-js">const ele = document.querySelector("a");

ele.addEventListener("click", function(){
	ele.style.backgroundColor = "pink";
});
</code></pre>
<h2 id="">事件传播：冒泡和捕获</h2>
<p>事件传播决定了元素接收事件的顺序。有两种处理 DOM 中事件传播顺序的方式：事件冒泡和事件捕获。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/09/Frame-71-1.png" alt="Frame-71-1" width="600" height="400" loading="lazy"></p>
<h3 id="">什么是事件冒泡</h3>
<p>当一个事件发生在一个组件时，首先会在该组件上触发事件处理器，然后传播到它的父组件，一直向上到其他的父组件。</p>
<p>事件处理器的默认情况时从中心组件一路向外到最外层的组件传播。</p>
<h3 id="">什么是事件捕获</h3>
<p>这个过程和冒泡刚好相反。事件处理器首先作用在父组件，然后再作用到触发处理器的组件。</p>
<p>简言之，事件首先由最外层的元素捕获，然后传递到最内层的元素。这个过程也被称作涓涓细流。</p>
<p><strong>让我们看一下下面的例子：</strong></p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;title&gt;Example&lt;/title&gt;
    &lt;style&gt;
        nav{
            display: flex;
            justify-content: center;
            padding: 30px;
        }

        nav li{
            list-style: none;
            padding: 5px;
        }

        nav li a{
            text-decoration: none;
            padding: 20px;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    
    &lt;div&gt;
        &lt;nav&gt;
            &lt;li&gt;&lt;a href="#"&gt;Home&lt;/a&gt;&lt;/li&gt;
            &lt;li&gt;&lt;a href="#"&gt;About&lt;/a&gt;&lt;/li&gt;
            &lt;li&gt;&lt;a href="#"&gt;Contact&lt;/a&gt;&lt;/li&gt;
        &lt;/nav&gt;
    &lt;/div&gt;

    &lt;script&gt;
        const navbar = document.querySelector("nav");
        navbar.addEventListener('click', function(){
            navbar.style.backgroundColor="green"
        });

        const anchor = document.querySelector("a");
        anchor.addEventListener("click", function(){
            anchor.style.backgroundColor="pink";
        })
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>代码效果如图：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/09/Screenshot-2022-09-26-142920.png" alt="Screenshot-2022-09-26-142920" width="600" height="400" loading="lazy"></p>
<p>仔细看上面的代码示例，我分别在<code>nav</code>标签和<code>anchor</code>标签添加了事件处理器：当你点击<code>nav</code>的时候背景色会变成绿色，而当点击<code>anchor</code>标签的时候，背景色会看成粉色。</p>
<p>但是当你点击<code>anchor</code>标签的时候，anchor 和 nav 的背景色都发生了变化。这是因为<strong>事件冒泡</strong>。</p>
<p><strong>当你只点击 nav 元素的时候发生的事情：</strong></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/09/Frame-72--1-.png" alt="Frame-72--1-" width="600" height="400" loading="lazy"></p>
<p><strong>当你只点击 anchor 元素的时候发生的事情：</strong></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/09/Frame-73--1-.png" alt="Frame-73--1-" width="600" height="400" loading="lazy"></p>
<p>停止事件传播的方式是在事件发起的元素的事件处理器上使用 <code>stopPropagation()</code>。这样就可以阻止 nav 元素因为上面的原因被激活。</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;title&gt;Example&lt;/title&gt;
    &lt;style&gt;
        nav{
            display: flex;
            justify-content: center;
            padding: 30px;
        }

        nav li{
            list-style: none;
            padding: 5px;
        }

        nav li a{
            text-decoration: none;
            padding: 20px;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    
    &lt;div&gt;
        &lt;nav&gt;
            &lt;li&gt;&lt;a href="#"&gt;Home&lt;/a&gt;&lt;/li&gt;
            &lt;li&gt;&lt;a href="#"&gt;About&lt;/a&gt;&lt;/li&gt;
            &lt;li&gt;&lt;a href="#"&gt;Contact&lt;/a&gt;&lt;/li&gt;
        &lt;/nav&gt;
    &lt;/div&gt;

    &lt;script&gt;
        const navbar = document.querySelector("nav");
        navbar.addEventListener('click', function(){
            navbar.style.backgroundColor="green"
        });

        const anchor = document.querySelector("a");
        anchor.addEventListener("click", function(e){
            e.stopPropagation();
            anchor.style.backgroundColor="pink";
        })
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<h2 id="dom">如何遍历 DOM</h2>
<p>“优秀的 JavaScript 开发者应该知道如何遍历 DOM — 即<strong>从一个元素选择到另一个元素的行为</strong> ” – <a href="https://zellwk.com/blog/dom-traversals/">Zell Liew</a></p>
<p>现在我们来看看为什么遍历会比<code>document.querySelector()</code>方法好用，以及如何像专家一样遍历。</p>
<p>有三种遍历 DOM 的方式：</p>
<ul>
<li>向上</li>
<li>向下</li>
<li>相临</li>
</ul>
<h3 id="dom">如何向上遍历 DOM</h3>
<p>有两种向上遍历 DOM 的方式：</p>
<ul>
<li>parentElement</li>
<li>closest</li>
</ul>
<p><code>parentElement</code>是选取父元素的属性，如下：</p>
<pre><code class="language-js">const ele = document.querySelector("a");
console.log(ele.parentElement); // &lt;div&gt;
</code></pre>
<p><code>parentElement</code>可以完美地选择上一层的元素，而<code>closest</code>可以让你选择上几层的元素。 <code>closest</code>可以选择符合选择器最近的祖先元素。</p>
<p>使用<code>closest</code>的例子：</p>
<pre><code class="language-html">&lt;article id="target"&gt;
  &lt;h1 id="heading"&gt;This is the main heading&lt;/h1&gt;
  &lt;div id="outer-div"&gt;
    This is the outer div
    &lt;div id="inner-div"&gt;This is the inner div&lt;/div&gt;
  &lt;/div&gt;
&lt;/article&gt;
</code></pre>
<pre><code class="language-js">const innerDiv = document.querySelector("#inner-div");
console.log(innerDiv.closest("#target")); // article#target
</code></pre>
<p>在上述代码中我们选择离<code>#inner-div</code>最近并且 id 为<code>#target</code>的元素。</p>
<h3 id="dom">如何向下遍历 DOM</h3>
<p>我们可以在选择器中使用<code>children</code>方法来向下遍历，就可以选择到直属子元素。</p>
<p><strong>例子：</strong></p>
<pre><code class="language-html">&lt;div&gt;
    &lt;a href="#"&gt;Link-1&lt;/a&gt;
    &lt;a href="#"&gt;Link-2&lt;/a&gt;
    &lt;a href="#"&gt;Link-3&lt;/a&gt;
    &lt;a href="#"&gt;Link-4&lt;/a&gt;
&lt;/div&gt;
</code></pre>
<pre><code class="language-js">const ele = document.querySelector("div");
const child = ele.children;

console.log(child); // 返回 HTMLCollection
// div 中的 4 个元素
</code></pre>
<h3 id="dom">如何遍历相邻的 DOM</h3>
<p>遍历相邻的 DOM 很有趣，有两种主要的方法：</p>
<ul>
<li>previousElementSibling</li>
<li>nextElementSibling</li>
</ul>
<p>使用<code>previousElementSibling</code>方法，我们可以选择 HTML 中的前一个元素：</p>
<pre><code class="language-html">&lt;div&gt;
    &lt;a href="#"&gt;Link-1&lt;/a&gt;
    &lt;h1&gt;Heading&lt;/h1&gt;
&lt;/div&gt;
</code></pre>
<pre><code class="language-js">const ele = document.querySelector("h1");
console.log(ele.previousElementSibling); // &lt;a href="#"&gt;Link-1&lt;/a&gt;
</code></pre>
<p>使用<code>nextElementSibling</code>，我们可以选择 HTML 中的后一个元素：</p>
<pre><code class="language-html">&lt;div&gt;
    &lt;a href="#"&gt;Link-1&lt;/a&gt;
    &lt;h1&gt;Heading&lt;/h1&gt;
&lt;/div&gt;
</code></pre>
<pre><code class="language-js">const ele = document.querySelector("a");
console.log(ele.nextElementSibling); // &lt;h1&gt;Heading&lt;/h1&gt;
</code></pre>
<h2 id=""><strong>总结</strong></h2>
<p>希望你已经了解 JavaScript 中的 DOM 是如何工作的，感谢阅读！</p>
<p>你可以在以下地方关注我：</p>
<ul>
<li><a href="https://twitter.com/Kedar__98">Twitter</a></li>
<li><a href="https://www.linkedin.com/in/kedar-makode-9833321ab/?originalSubdomain=in">LinkedIn</a></li>
<li><a href="https://www.instagram.com/kedar_98/">Instagram</a></li>
</ul>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 什么是 XSS？如何保护网站免受 DOM 跨站脚本攻击 ]]>
                </title>
                <description>
                    <![CDATA[ 网站安全和漏洞是一个全球性问题，网络安全漏洞正在增加。过去几年，这类案件的平均数量有了显著增长，2021 年达到了历史最高水平。 因此，在本教程中，我们将讨论 DOM XSS 跨站脚本安全问题以及它们对数据产生的影响。请务必阅读到最后。让我们从复习 DOM XSS 跨站脚本安全的基础知识开始。 什么是跨站脚本？ 跨站脚本（也称为 XSS）是一种网站安全问题 [https://www.wix.com/blog/2022/01/website-security/] ，当用户使用脆弱的应用程序时，会危害用户信息和数据。攻击者可以利用此漏洞绕过同源策略，该策略将两个网站彼此隔离。 攻击者可能会利用 XSS 假装自己是用户，执行用户将会执行的操作，并获取用户的信息。如果攻击者拥有权限和特权，攻击者便可以完全访问用户的信息。持续恶化下去，攻击者还可以接管网站的整个操作和性能。 为了帮助你更好地理解这些类型的攻击，我们将讨论 XSS 的运作原理和方法相关的基础知识。 XSS 如何运作？ 跨站脚本利用技术操纵脆弱网站，使其向用户发送危险的 JavaScript。这使攻击者能够在脚本获得用户系 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-protect-against-dom-xss-attacks/</link>
                <guid isPermaLink="false">63da30d9634c950733e7f9c0</guid>
                
                    <category>
                        <![CDATA[ 信息安全 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ PapayaHUANG ]]>
                </dc:creator>
                <pubDate>Wed, 01 Feb 2023 09:34:38 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/02/xss-code-case.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/how-to-protect-against-dom-xss-attacks/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">What is XSS? How to Protect Your Website from DOM Cross-Site Scripting Attacks</a>
      </p><!--kg-card-begin: markdown--><p><strong>网站安全</strong>和漏洞是一个全球性问题，网络安全漏洞正在增加。过去几年，这类案件的平均数量有了显著增长，2021 年达到了历史最高水平。</p>
<p>因此，在本教程中，我们将讨论 DOM XSS 跨站脚本安全问题以及它们对数据产生的影响。请务必阅读到最后。让我们从复习 DOM XSS 跨站脚本安全的基础知识开始。</p>
<h2 id="">什么是跨站脚本？</h2>
<p>跨站脚本（也称为 XSS）是一种<a href="https://www.wix.com/blog/2022/01/website-security/">网站安全问题</a>，当用户使用脆弱的应用程序时，会危害用户信息和数据。攻击者可以利用此漏洞绕过同源策略，该策略将两个网站彼此隔离。</p>
<p>攻击者可能会利用 XSS 假装自己是用户，执行用户将会执行的操作，并获取用户的信息。如果攻击者拥有权限和特权，攻击者便可以完全访问用户的信息。持续恶化下去，攻击者还可以接管网站的整个操作和性能。</p>
<p>为了帮助你更好地理解这些类型的攻击，我们将讨论 XSS 的运作原理和方法相关的基础知识。</p>
<h2 id="xss">XSS 如何运作？</h2>
<p>跨站脚本利用技术操纵脆弱网站，使其向用户发送危险的 JavaScript。这使攻击者能够在脚本获得用户系统访问权限时完全获得用户在网站的权限。但这一切的开始是用户执行 JavaScript。</p>
<h3 id="xss">XSS 攻击的类型</h3>
<h4 id="xss">反射型 XSS</h4>
<p>这种恶意脚本来自 HTTP 请求。这是 XSS 攻击的最基本类型，在这种情况下，应用程序接收到恶意数据后立即向用户反映。</p>
<p>攻击者的负载必须是发送到服务器的请求的一部分，然后反映到用户的应用程序上并执行。</p>
<p>其中一个例子是攻击者想方设法让某人点击钓鱼链接，然后攻击才生效。</p>
<h4 id="xss">存储型 XSS</h4>
<p>这种恶意脚本来自网站的数据库。攻击者在服务器上输入恶意请求，除非手动处理，否则它可能永久存在。</p>
<p>例如，攻击者可以在评论字段中输入恶意脚本，该脚本将对访问该页面的所有人显示。即使没有直接与脚本互动，页面访问者也可能成为此攻击的受害者</p>
<h4 id="domxss">基于 DOM 的 XSS</h4>
<p>这种是更高级的漏洞，它存在于客户端代码中的，而不是在服务器代码中。基于 DOM 的 XSS 既不是反射型的，也不存在于服务器上，而是存在于页面的文档对象模型（DOM）中。Web 应用程序读取恶意代码并将其作为 DOM 的一部分在浏览器中执行，这更难检测，因为它不通过服务器传递。</p>
<p>基于 DOM 的 XSS 攻击中涉及的<strong>安全漏洞</strong>对于大多数网站来说是一个严峻的问题。我们将讨论一些开源 Web 构建平台（例如 WordPress）上最常见的基于 DOM 的 XSS 风险。</p>
<p>攻击者在受害者的浏览器中执行恶意 JavaScript 代码，窃取敏感信息或以受害者的名义执行其他有害的操作。</p>
<p>以下是一个例子：</p>
<pre><code class="language-html">&lt;script&gt;
  //该函数接受一个用户提供的 URL 并展示在页面
  function displayURL(url) {
    // URL 通过 innerHTML 获得，执行 JavaScript 代码
    document.getElementById('display').innerHTML = url;
  }
&lt;/script&gt;

&lt;!-- 用户的输入传递给 displayURL 函数 --&gt;
&lt;p&gt;Enter a URL to display: &lt;input type="text" id="user-input" /&gt;&lt;/p&gt;
&lt;button onclick="displayURL(document.getElementById('user-input').value)"&gt;
  Display URL
&lt;/button&gt;

&lt;!-- URL 在 div 中显示 --&gt;
&lt;div id="display"&gt;&lt;/div&gt;
</code></pre>
<p>请注意本教程仅用于教学目的，帮助你识别和防护 XSS。你不可以复刻类似攻击。</p>
<h2 id="domxsswordpress">DOM XSS – WordPress 漏洞</h2>
<p>WordPress 的 DOM XSS 攻击的主要目标是它的用户。用户输入他们的详细信息，账户和网站凭证以访问他们的 WordPress 站点，这就是 DOM XSS 攻击试图在线损害的内容。攻击者可以使用 DOM XSS 获取用户信息和详细信息，只需点击一下。</p>
<p>这也包括你的 cookies、信息等，使它成为最常见的 <strong>WordPress 安全漏洞</strong>。</p>
<p>以下是你应该牢记的一些顶级 WordPress 网站安全问题，以确保更好的跨站脚本安全</p>
<h3 id="">访问用户私人信息</h3>
<p>一个最常见的 WordPress 安全漏洞与 DOM XSS 攻击有关，攻击者可以获得有用的信息，甚至完全控制用户的网站。这通常会使事态迅速升级，导致完全数据泄露。</p>
<h3 id="">假冒用户</h3>
<p>攻击者可以假装是用户，与受害者的在线用户，客户互动，以获得信息。</p>
<h3 id="">破坏网站</h3>
<p>另一个相关的 XSS 安全问题是，攻击者破坏网站并从用户那里夺取访问权限。这包括在网站上显示与原始网站不同的内容（或不是来自原始网站的内容）。</p>
<p>其他情况可能涉及改变 WordPress 在线的外观。其他人也可能通过安装特定工具使用网站。</p>
<h3 id="">社交工程</h3>
<p>更严重的情况下，攻击者可能通过网络钓鱼影响 WordPress 网站。这是一个常见的网站构建平台安全漏洞，我们马上就要详细讨论。</p>
<p>XSS 跨站脚本安全问题对每个网站的影响各不相同。然而，WordPress 网站通常面临更高的风险，因为用户在网站上保存了个人信息。如果用户是管理员，风险进一步增加，因为攻击者可以破坏整个 WordPress 网站。</p>
<h2 id="domxss">DOM XSS 和非开源网站构建平台</h2>
<p>不同于 WordPress，如 Weebly、Squarespace、Webflow 和 Wix 这类网站构建器是非开源平台。它们允许用户通过拖放式 DIY 功能，在没有任何代码或设计经验的情况下，直观地为业务创建网站。这些平台也努力保护用户的安全。</p>
<p>有大量有用的工具、选项、易于整合的仪表板和托管方式，供用户使用并信任这些平台。当然，网站安全问题对于这些平台的大多数用户来说是一个主要关注点。</p>
<p>许多网站构建器都在努力保护用户网站免受<a href="https://techbullion.com/how-to-secure-your-online-store-from-hackers/">黑客威胁</a>。但是在所有可用的网站构建器中，我相信 Wix 在遵循 NIST 网络安全框架方面做得最好，并已成为该领域的重要贡献者。</p>
<p>Wix 使用如下工具保护用户不受这类攻击的影响：</p>
<ul>
<li>第三方更新项目，防止 DOM XSS 攻击</li>
<li>安全的套接字层，防止网站上的用户被未经授权的访问</li>
<li>全天候的安全网络托管，防止任何不需要的登录或网络钓鱼攻击</li>
<li>只有给原始所有者控制和访问限制网站的管理员特权</li>
<li>显示弱密码并建议更难解密的密码</li>
</ul>
<h2 id="xss">防御 XSS 攻击的方法</h2>
<p>防御 XSS 攻击通常需要多维度的方法，以确保服务器和应用程序不受各种类型攻击的影响。</p>
<p>防御 XSS 攻击的最佳方法是适当地对用户输入进行清理。这意味着确保任何用户输入被正确编码，浏览器则不能将其解释为代码。</p>
<p>此外，你可以使用 web 应用程序防火墙（WAF）来帮助识别和阻止 XSS 攻击。最好将软件和 web 应用程序保持在最新版本，因为许多 XSS 漏洞可以通过最新的安全补丁来解决。</p>
<h3 id="">输入验证</h3>
<p>这种编程技巧确保只有格式正确的数据才能进入软件系统。网站可以允许或阻止某些值，以确保不会有 XSS 侵入其服务器。</p>
<h3 id="">转义或编码用户输入</h3>
<p>编码和转义将用户输入转换为更安全的形式。编码用更安全的等效物替换特殊字符（例如，将 <code>&lt;</code> 转换为 <code>&amp;lt</code>），而转义则添加特殊字符以防注入攻击。</p>
<h3 id="csp">执行内容安全策略（CSP）</h3>
<p>内容安全策略帮助管理员通过限制页面在特定时间内加载的资源来减轻 XSS 攻击的影响。这些资源可以包括可能损害客户端和服务器的脚本和图像。</p>
<h2 id="">底线</h2>
<p>DOM XSS 跨站脚本安全问题对网站用户是一个严峻的问题。但是封闭的代码网站构建平台提供的，如：管理员权限、密码最佳实践、第三方更新等功能，使它们比许多开放代码平台更加安全。</p>
<p>你可以通过过滤输入来防止 XSS 攻击。你也可以通过只接受有效输入来实现。</p>
<p>输出时对数据进行编码，该过程应在 HTTP 响应中完成，以免被读取为活跃内容。根据输出的上下文，可能需要更复杂的编码，例如应用 URL、JavaScript、CSS 和 HTML 编码的组合。</p>
<p>检查响应标头，以便浏览器对内容有适当的解释。</p>
<p>最后，使用内容安全策略（CSP）来最大程度地降低 XSS 攻击的严重性。</p>
<pre><code class="language-html">&lt;meta
  http-equiv="Content-Security-Policy"
  content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';"
/&gt;
</code></pre>
<p>CSP 允许网站从同一个源（即你自己的网站）加载脚本和样式，但它将阻止来自外部来源的脚本和样式。它还将允许使用内联脚本和样式，但阻止 <code>eval()</code> 语句，因为这个语句可用于执行任意代码。</p>
<p>当然，这只是一个简单的例子，你可以根据自己的特殊需求自定义 CSP。有关如何使用 CSP 来防御 XSS 攻击的更多信息，你可以参考内容安全策略级别 2 规范。</p>
<p><em>图片来自 Unsplash（Florian Olivo）。</em></p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 更好的 React 性能——何时使用 useCallback 和 useMemo Hook ]]>
                </title>
                <description>
                    <![CDATA[ 我们都希望构建强大的应用，避免不必要的渲染。有一些钩子可以帮助你实现这个愿望，但你可能不确定钩子的选择和使用时机。 我们将通过本文学习 useCallback  和 useMemo的区别，以及如何衡量在代码中使用它们的收益。 在我们开始之前，请注意以下用于优化 React 的方法实际上是不得已的选择。代码本身可能有许多改进空间，在改进代码前，本文性能提升技巧可能还派不上用场。 但了解这些工具，以及知道如何使用它们很有必要。 帮助你理解文章的资料  * useCallback [https://beta.reactjs.org/apis/react/useCallback]  和 useMemo    [https://beta.reactjs.org/apis/react/useMemo]  的 Beta 版本官方文档  * 示例项目源码 [https://github.com/dastasoft/optimizing-react]  * 示例项目 Demo [https://react-optimisation.dastasoft.com/] 与往常一样，我提供了一个示例项 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/better-react-performance-usecallback-vs-usememo/</link>
                <guid isPermaLink="false">63a5771cf490ad0743626777</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ PapayaHUANG ]]>
                </dc:creator>
                <pubDate>Fri, 23 Dec 2022 06:38:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/12/useCallback-vs-useMemo.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/better-react-performance-usecallback-vs-usememo/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">Better React Performance – When to Use the useCallback vs useMemo Hook</a>
      </p><!--kg-card-begin: markdown--><p>我们都希望构建强大的应用，避免不必要的渲染。有一些钩子可以帮助你实现这个愿望，但你可能不确定钩子的选择和使用时机。</p>
<p>我们将通过本文学习 <code>useCallback</code> 和 <code>useMemo</code>的区别，以及如何衡量在代码中使用它们的收益。</p>
<p>在我们开始之前，请注意以下用于优化 React 的方法实际上是不得已的选择。代码本身可能有许多改进空间，在改进代码前，本文性能提升技巧可能还派不上用场。</p>
<p>但了解这些工具，以及知道如何使用它们很有必要。</p>
<h2 id="">帮助你理解文章的资料</h2>
<ul>
<li><a href="https://beta.reactjs.org/apis/react/useCallback">useCallback</a> 和 <a href="https://beta.reactjs.org/apis/react/useMemo">useMemo</a> 的 Beta 版本官方文档</li>
<li><a href="https://github.com/dastasoft/optimizing-react">示例项目源码</a></li>
<li><a href="https://react-optimisation.dastasoft.com/">示例项目 Demo</a></li>
</ul>
<p>与往常一样，我提供了一个示例项目，以便你在简化的环境中测试本文说明的所有内容。示例项目是对你将要学习要点的总结。</p>
<p>在开始比较这两个钩子之前，让我们回顾一些必要的背景概念。</p>
<h2 id="referentialequality">什么是引用相等（Referential Equality）</h2>
<p>当 React 对比 <code>useEffect</code>、 <code>useCallback</code>的依赖数组的值，或者传入子组件的 props 值时，使用的是 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/is"><code>Object.is()</code></a>。</p>
<p>详细介绍可以查看 <a href="http://Object.is">Object.is</a>，简言之：</p>
<ul>
<li>原始值是相等的（上文链接有少数例外）。</li>
<li>非原始值指向内存中相同的对象。</li>
</ul>
<blockquote>
<p>译者注：原始值指的是数据类型为基本数据类型（如：number、string、boolean 等）时，两个值相等的数据在严格模式下（<code>===</code>）也是相等的。非原始值值的数据类型是引用类型（如：object），由于引用类型存储的是对象的引用，所以只有当两个对象引用相同的底层对象，它们在严格模式下才是相等的。这种比较方式被称为“引用相等”。</p>
</blockquote>
<p>简化示例如下：</p>
<pre><code class="language-js">"string" === "string" // true
0 === 0 // true
true === true // true
{} === {} // false
[] === [] // false

const f = () =&gt; 'Hi'
const f1 = f
const f2 = f

f1 === f1 // true
f1 === f2 // true
</code></pre>
<h2 id="reactmemo">React.memo 的运行机制</h2>
<p>我将简单说明一下<code>React.memo</code>的运行机制（后文也会讲解）。你可以在合适的时候使用它来提升性能。</p>
<p>当想要避免子组件不必要的重新渲染（即便父组件发生了更改），你可以使用 <code>React.memo</code> 打包子组件 – 只要 props 不发生改变，就不会重复渲染。<strong>请注意此处是引用相等</strong>（译者注：沿用了旧版本 React 的<a href="https://reactjs.org/docs/shallow-compare.html">“浅比较”</a>）——子组件不会被重新渲染。</p>
<pre><code class="language-javascript">import { memo } from 'react';

const ChildComponent = (props) =&gt; {
  // ...
};

export default memo(ChildComponent);
</code></pre>
<p>现在你知道 <code>React.memo</code> 的运行机制，让我们开始应用吧。</p>
<h2 id="usecallback">useCallback 的运行机制</h2>
<p><code>useCallback</code> 是 React 用来优化代码的内置钩子之一。但正如你将看到的那样，它并不是直接为性能提升设计的钩子。</p>
<p>简单来说，<code>useCallback</code> 允许你在组件渲染之间保存 <em>函数定义</em>。</p>
<pre><code class="language-js">import { useCallback } from 'react';

const params = useCallback(() =&gt; {
  // ...
  return breed;
}, [breed]);
</code></pre>
<p>使用方法很简单：</p>
<ul>
<li>从 React 引入<code>useCallback</code>，因为它是内置钩子。</li>
<li>打包你想要保存定义的函数。</li>
<li>像使用 <code>useEffect</code>一样，传入依赖数组，告诉 React 这些存储的值（在这里是函数定义）何时更新。</li>
</ul>
<p>需要注意的是 <em>函数定义</em> 部分。它存储定义，而不是执行本身，也不是结果——所以每次调用时都会执行该函数。因此，不要使用这个钩子避免冗长的计算。</p>
<p>那么保存函数定义的好处在哪儿呢？</p>
<h3 id="">回到引用相等</h3>
<p>如果使用的是函数本身，而不是返回值，那么在：</p>
<ul>
<li><code>useEffect</code> 等钩子的依赖数组</li>
<li>子组件的 prop、上下文等</li>
</ul>
<p>要实现渲染之间真正的相等，<code>useCallback</code>就得保存<strong>内存中对同一个对象的的引用</strong>。</p>
<p>如果不使用这个钩子，每一次渲染函数都会重新指向内存中的另一个引用。即便使用<code>React.memo</code>打包子组件，React 也会认为是不同的函数。</p>
<p>你可以通过示例项目测试这个行为。在没有优化的版本中，每一次在输入框填写内容都会引发子组件的副作用。</p>
<p>在示例中，没有优化的版本只会导致一个虚拟的渲染放缓和重新抓取图片。但假设在一个大型的项目中，会导致客户端执行大量计算，或者服务器的巨大开销。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/12/use-callback-referential-equality.png" alt="use-callback-referential-equality" width="600" height="400" loading="lazy"></p>
<h2 id="usememo"><code>useMemo</code> 是如何运作的</h2>
<p>这是今天的第二个内置钩子。你可以把这个钩子当作直接优化的手段，因为它存储函数的结果，除非依赖数组发生变化，函数不会再次执行。</p>
<p>由于它可以存储函数的结果，防止在组件渲染之间重复执行，因此你可以在两种情况下使用此钩子。</p>
<h3 id="">引用相等</h3>
<p>和 <code>useCallback</code> 一样，我们也可以通过 <code>useMemo</code> 来实现引用相等——但这次是结果的相等。</p>
<p>如果函数的返回值类型在渲染间会被当作不同的值对待，如对象或者数组，你可以使用 <code>useMemo</code> 来实现引用相等。</p>
<pre><code class="language-js">import { useMemo } from 'react';

const params = useMemo(() =&gt; {
  // ...
  return { breed };
}, [breed]);
</code></pre>
<p>从上面例子我们可以得出这样使用 <code>useMemo</code>：</p>
<ul>
<li>由 React 引入 <code>useMemo</code>，因为它是内置钩子。</li>
<li>打包你想要保存结果的函数。</li>
<li>像使用 <code>useEffect</code> 一样，传入依赖数组，告诉 React 这些存储的值（在这里是函数的返回值）何时更新。</li>
</ul>
<p>在示例中，函数返回一个对象。 通过 <a href="http://Object.is">Object.is</a> 我们得知对象是不相等的，因为它们存储了不同的内存地址。但是<code>useMemo</code>可以保存相同的引用。</p>
<p>你可以像之前一样在示例项目中测试这个行为。在未优化版本中，每一次按下键盘，都会重新检索图片。使用 <code>useMemo</code>后，相等的返回值被保持，子组件不在重新检索图片。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/12/use-memo-referential-equality.png" alt="use-memo-referential-equality" width="600" height="400" loading="lazy"></p>
<h3 id="">昂贵的计算</h3>
<p>由于使用 <code>useMemo</code>保存了值，避免函数重复执行，所以我们可以使用它避免不必要的昂贵计算，提高网站的性能。</p>
<p>让我们查看示例项目：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/12/use-memo-expensive-calculation.png" alt="use-memo-expensive-calculation" width="600" height="400" loading="lazy"></p>
<p>有一个组件，给定一个数字 n，就会打印出第 n 个斐波那契数。但是算法采用的递归版本性能很差。</p>
<p>你会发现有一个常量不断被重复渲染。示例中的性能标尺（Performance Gauge）会改变 state（每秒添加或者删除方块 60 次）。由于 state 一直发生改变，所以计算斐波那契数的函数也在重复执行，即便给定的数字是一样的。</p>
<p>在这种情况下，当你在非优化版本中使用更大的数字，就会发现性能肉眼可见的下降。优化版本只会在你更改滑块中的数字（更改给定数字）时出现性能峰值，但其余渲染将跳过计算并直接提供结果。</p>
<p>这里的问题是，在我们的日常工作中，不会遇到可以称为“昂贵计算”的计算，使用 <code>useMemo</code> 的决定不一定是“总是”或“从不”。</p>
<h2 id="">何时优化</h2>
<p>到目前为止，你已经了解了通过一些指标确定何时使用不同的钩子来避免不必要的渲染和/或副作用。现在让我们定义一些通用规则来决定在那些不太清楚的情况下到底是否使用这些钩子：</p>
<ul>
<li>回顾你的代码，重新思考代码构建。你会发现最能提升性能的其实是你代码本身。更多信息可以查看 <a href="https://overreacted.io/before-you-memo/">Dan Abramov 的这篇博文</a>。</li>
<li>如果不能证明优化可以带来好处，就不要优化——优化也有成本。</li>
<li>如果你不希望做额外的工作来证明优化可以带来好处，那请诚实对待自己的内心：其实你也不想优化。</li>
</ul>
<h2 id="">如何衡量性能影响/收益</h2>
<p>最重要的优化规则（总是在检查代码之后再使用）是能够衡量更改是否生效以及增益百分比是多少。你这样做不仅是为了可以在下一次绩效评估提高相应的百分比。</p>
<p>当你怀疑存在性能问题或只是想检查代码可以改进部分时，有以下两种选择：</p>
<h3 id="">笨拙的方法</h3>
<p>我把这个方法也纳入到文章中来，是因为让我们面对现实吧：你一直在到处使用 <code>console.log</code> 调试代码，不是吗？别担心，我和你一样。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/12/crappy-debugger-meme.png" alt="crappy-debugger-meme" width="600" height="400" loading="lazy"></p>
<p>尝试衡量性能问题的一种快速方法是找出执行某个动作需要多长时间以及该动作执行了多少次。因此，可以这么做：</p>
<pre><code class="language-js">const t0 = performance.now();
expensiveCalculation(targetNumber);
const t1 = performance.now();
console.log(`Call to expensiveCalculation took ${t1 - t0} milliseconds.`);
console.count('Expensive Calculation');
</code></pre>
<p>但这种方法只能检测出一些你已经怀疑的非常明显的情况。</p>
<p>同时请小心 <code>StrictMode</code>，出于稳定性考虑，它可能会导致 <code>console.count</code> 重复渲染。</p>
<p>现在让我们查看正确的方法。</p>
<h3 id="">专业的方法</h3>
<p>在这个方法中，你将使用官方的 <a href="https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi">React 开发者工具</a>来检查代码片段的性能。一旦你在浏览器添加了这个扩展程序，你就可以打开浏览器，搜索 <code>Profiler</code>。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/12/profiler.png" alt="profiler" width="600" height="400" loading="lazy"></p>
<p>我将通过示例项目演示，你也可以在你自己的项目中测试。</p>
<p>当你点击 <code>record</code> 按钮，然后开始进行你认为需要关注性能的一些行为，profiler 就会保存并且打印出这个过程具体发生的细节和解释。</p>
<p>如在昂贵计算项目中，我们对比的没有优化和 <code>useMemo</code> 版本的结果：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/12/profiler-graph.png" alt="profiler-graph" width="600" height="400" loading="lazy"></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/12/profiler-graph-detailed.png" alt="profiler-graph-detailed" width="600" height="400" loading="lazy"></p>
<p>在两个版本中，我分别点击 record 按钮，等待几秒钟，再次点击 record 按钮获取结果。如你所见，在我们准备的极端案例中，可以见到巨大的性能提升。</p>
<p>让我们仔细观察 profiler 中发生了什么：</p>
<ul>
<li>灰色条目是在渲染间没有发生变化的组件，所以不用担心性能方面的问题。</li>
<li>绿色和黄色条目是发生变化的组件，你可以看到渲染需要多长时间。</li>
<li>如果你点击每一个条目，可以看到更多的解释信息和数据。</li>
</ul>
<p>我之后会出一篇文章详细介绍 profiler，但是现在让我们看几个使用小技巧：</p>
<ul>
<li>在 settings 图标 General 菜单下，勾选 <code>Highlight updates when components render.</code>。这将显示渲染的内容，并可以检测在某些操作下不被渲染的子组件。</li>
<li>在 settings 图标 Profiler 菜单下，勾选 <code>Record why each component rendered while profiling.</code> 这将对正在渲染的组件内容的添加简要说明，或许能帮助你找到哪里需要提升。</li>
</ul>
<h2 id="">总结</h2>
<p>如你所见，这两个常被误解的钩子是完全不同的函数，使用的场景也不太相同。现在你可以检查你现在或者过去的项目，来看看是否误用了这些钩子。</p>
<p>在未来 React 或许能够自动完成优化。但在撰写本文时，优化仍是一个应该谨慎对待并经过全面分析的过程。</p>
<p>我希望你觉得这篇教程有用，能帮助你使用 React 构建性能更好的应用程序。谢谢你阅读本文！</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Express + Node.js 手册——面向初学者的 Express JavaScript 框架（2022 版） ]]>
                </title>
                <description>
                    <![CDATA[ 什么是 Express？ Express 是一个基于 Node.js 的 Web 框架。 Node.js 是一种搭建网络服务和应用的实用工具。 Express 搭建在 Node.js 之上，提供易于使用的功能来满足 Web 服务器的用例需求。它开源、免费、易于扩展并且非常高效。 可以使用各种各样的预构建的包来处理应用中的各项内容。 点击获取 Express 手册的 PDF 和 ePub 版本 [https://thevalleyofcode.com/download/express/]。 目录  * 如何安装 Express  * 第一个 "Hello, World"  * 请求参数  * 如何向客户端发送响应  * 如何发送一个 JSON 响应  * 如何管理 Cookies ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/the-express-handbook/</link>
                <guid isPermaLink="false">63a03e6fa7bffa07c7441952</guid>
                
                    <category>
                        <![CDATA[ Express ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ PapayaHUANG ]]>
                </dc:creator>
                <pubDate>Mon, 19 Dec 2022 04:36:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/12/pexels-paul-ijsendoorn-1181202.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/the-express-handbook/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">The Express +  Node.js Handbook – Learn the Express JavaScript Framework for Beginners</a>
      </p><!--kg-card-begin: markdown--><h2 id="express">什么是 Express？</h2>
<p>Express 是一个基于 Node.js 的 Web 框架。</p>
<p>Node.js 是一种搭建网络服务和应用的实用工具。</p>
<p>Express 搭建在 Node.js 之上，提供易于使用的功能来满足 Web 服务器的用例需求。它开源、免费、易于扩展并且非常高效。</p>
<p>可以使用各种各样的预构建的包来处理应用中的各项内容。</p>
<p><a href="https://thevalleyofcode.com/download/express/">点击获取 Express 手册的 PDF 和 ePub 版本</a>。</p>
<h2 id="">目录</h2>
<ul>
<li><a href="#how-to-install-express">如何安装 Express</a></li>
<li><a href="#the-first-hello-world-example">第一个 "Hello, World"</a></li>
<li><a href="#request-parameters">请求参数</a></li>
<li><a href="#how-to-send-a-response-to-the-client">如何向客户端发送响应</a></li>
<li><a href="#how-to-send-a-json-response">如何发送一个 JSON 响应</a></li>
<li><a href="#how-to-manage-cookies">如何管理 Cookies</a></li>
<li><a href="#how-to-work-with-http-headers">如何处理 HTTP 标头</a></li>
<li><a href="#how-to-handle-redirects">如何处理重定向</a></li>
<li><a href="#routing-in-express">Express 中的路由</a></li>
<li><a href="#templates-in-express">Express 中的模版</a></li>
<li><a href="#express-middleware">Express 中间件</a></li>
<li><a href="#how-to-serve-static-assets-with-express">如何使用 Express 提供静态资源</a></li>
<li><a href="#how-to-send-files-to-the-client">如何向客户端发送文件</a></li>
<li><a href="#sessions-in-express">Express 中的会话</a></li>
<li><a href="#how-to-validate-input-in-express">如何在 Express 中验证输入</a></li>
<li><a href="#how-to-sanitize-input-in-express">如何在 Express 中清理输入</a></li>
<li><a href="#how-to-handle-forms-in-express">如何在 Express 中处理表单</a></li>
<li><a href="#how-to-handle-file-uploads-in-forms-in-express">如何在 Express 中处理表单中的文件上传</a></li>
</ul>
<h2 id="how-to-install-express">如何安装 Express</h2>
<p>你可以使用 npm 在任何项目中安装 Express。</p>
<p>如果是一个空文件夹，首先使用命令创建一个新的 Node.js 项目：</p>
<pre><code>npm init -y
</code></pre>
<p>然后执行：</p>
<pre><code>npm install express
</code></pre>
<p>在项目中安装 Express。</p>
<h2 id="the-first-hello-world-example">第一个 "Hello World"</h2>
<p>我们要创建的第一个示例是一个简单的 Express Web 服务器：</p>
<p>请复制以下代码：</p>
<pre><code class="language-js">const express = require('express');
const app = express();

app.get('/', (req, res) =&gt; res.send('Hello World!'));
app.listen(3000, () =&gt; console.log('Server ready'));
</code></pre>
<p>并将其保存在项目根文件夹的 <code>index.js</code> 文件中，然后通过以下命令启动服务器：</p>
<pre><code>node index.js
</code></pre>
<p>你可以打开浏览器并通过 localhost 导航到 port 3000，就会看到 <code>Hello World!</code> 信息。</p>
<p>上面四行代码在幕后做了很多工作：</p>
<p>首先我们通过 <code>express</code> 变量引用 <code>express</code> 包。</p>
<p>通过调用 <code>express()</code> 方法来实例化一个应用。</p>
<p>一旦创建了应用对象，就使用 <code>get()</code> 方法监听来自 <code>/</code> 路径的 GET 请求。</p>
<p>每一种 HTTP 方法都有一个对应的<strong>动词</strong>：<code>get()</code>、<code>post()</code>、<code>put()</code>、<code>delete()</code>和 <code>patch()</code>：</p>
<pre><code class="language-js">app.get('/', (req, res) =&gt; {
  /* */
});
app.post('/', (req, res) =&gt; {
  /* */
});
app.put('/', (req, res) =&gt; {
  /* */
});
app.delete('/', (req, res) =&gt; {
  /* */
});
app.patch('/', (req, res) =&gt; {
  /* */
});
</code></pre>
<p>这些方法接受一个回调函数（当请求开始时调用），我们需要处理回调函数。</p>
<p>可以在回调中传入一个箭头函数：</p>
<pre><code class="language-js">(req, res) =&gt; res.send('Hello World!');
</code></pre>
<p>Express 在回调中发送两个对象：<code>req</code> 和<code>res</code>，分别代表了请求（Request）和响应（Response）对象。</p>
<p>更多标准可以参考<a href="https://developer.mozilla.org/en-US/docs/Web/API/Request">这里</a>和<a href="https://developer.mozilla.org/en-US/docs/Web/API/Response">这里</a>。</p>
<p>请求是一个 HTTP 请求，它包含了所有请求信息：包括请求参数、标头、请求体等。</p>
<p>响应是 HTTP 响应对象，会返回给客户端。</p>
<p>在这个回调示例中，我们通过<code>Response.send()</code>方法发送 "Hello World!" 字符串给客户端。</p>
<p>这个方法将字符串作为请求体，传输完毕后关闭连接。</p>
<p>Hello World 示例中最后一行代码启动服务器，并且告诉它在 port<code>3000</code>监听。我们传入一个回调，当服务器准备好接受新请求时调用该回调。</p>
<h2 id="request-parameters">请求参数</h2>
<p>我介绍了 Request 对象是如何持有 HTTP 请求信息的。</p>
<p>以下是主要的属性：</p>
<table>
<thead>
<tr>
<th>属性</th>
<th>介绍</th>
</tr>
</thead>
<tbody>
<tr>
<td>.app</td>
<td>持有对 Express app 对象的引用</td>
</tr>
<tr>
<td>.baseUrl</td>
<td>app 响应的基本路径</td>
</tr>
<tr>
<td>.body</td>
<td>包含在请求体提交的数据（必须手动解析和填充后才能访问）</td>
</tr>
<tr>
<td>.cookies</td>
<td>包含由请求发送的 cookies（需要 <code>cookie-parser</code>中间件）</td>
</tr>
<tr>
<td>.hostname</td>
<td><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host">Host HTTP 标头</a>值定义的主机名</td>
</tr>
<tr>
<td>.ip</td>
<td>客户端 IP</td>
</tr>
<tr>
<td>.method</td>
<td>使用的 HTTP 方法</td>
</tr>
<tr>
<td>.params</td>
<td>路由命名参数</td>
</tr>
<tr>
<td>.path</td>
<td>URL 路径</td>
</tr>
<tr>
<td>.protocol</td>
<td>请求协议</td>
</tr>
<tr>
<td>.query</td>
<td>包含请求中使用的所有查询字符串的对象</td>
</tr>
<tr>
<td>.secure</td>
<td>请求是安全时（使用 HTTPS）为 true</td>
</tr>
<tr>
<td>.signedCookies</td>
<td>包含由请求发送的签名 cookies（需要 <code>cookie-parser</code> 中间件）</td>
</tr>
<tr>
<td>.xhr</td>
<td>请求为 <a href="https://www.freecodecamp.org/news/xhr/">XMLHttpRequest</a>时为 true</td>
</tr>
</tbody>
</table>
<h2 id="how-to-send-a-response-to-the-client">如何向客户端发送响应</h2>
<p>在 Hello World 示例中， 我们使用响应对象的<code>send()</code>方法来将一个简单的字符串作为响应，之后关闭连接。</p>
<pre><code class="language-js">(req, res) =&gt; res.send('Hello World!');
</code></pre>
<p>如果你传入一个字符串，它将<code>Content-Type</code>标头设置为<code>text/html</code>。</p>
<p>如果你传入的是对象或者数组，它将<code>Content-Type</code>标头设置为<code>application/json</code>，并将传入的对象或数组解析为<a href="https://www.freecodecamp.org/news/json/">JSON</a>。</p>
<p>之后<code>send()</code>关闭连接。</p>
<p><code>send()</code>自动设置 <code>Content-Length</code> HTTP 响应标头，不像 <code>end()</code> 需要你手动设置。</p>
<h3 id="end">如何使用 end() 发送空响应</h3>
<p>另一个发送响应的方式，是使用<code>Response.end()</code>方法，这种方法不发送任何响应体：</p>
<pre><code class="language-js">res.end();
</code></pre>
<h3 id="http">如何设置 HTTP 响应状态</h3>
<p>在响应对象使用 <code>status()</code> 方法：</p>
<pre><code class="language-js">res.status(404).end();
</code></pre>
<p>或者</p>
<pre><code class="language-js">res.status(404).send('File not found');
</code></pre>
<p><code>sendStatus()</code> 是快捷方式。</p>
<pre><code class="language-js">res.sendStatus(200);
// === res.status(200).send('OK')

res.sendStatus(403);
// === res.status(403).send('Forbidden')

res.sendStatus(404);
// === res.status(404).send('Not Found')

res.sendStatus(500);
// === res.status(500).send('Internal Server Error')
</code></pre>
<h2 id="how-to-send-a-json-response">如何发送一个 JSON 响应</h2>
<p>当你在 Express 中监听路由上的连接时，回调函数将在每次网络调用时被调用，并带有一个 Request 对象实例和一个 Response 对象实例。</p>
<p>示例：</p>
<pre><code class="language-js">app.get('/', (req, res) =&gt; res.send('Hello World!'));
</code></pre>
<p>我们在这里使用 <code>Response.send()</code> 方法，接受任意字符串。</p>
<p>你可以使用<a href="https://www.freecodecamp.org/news/json/">JSON</a>，即使用 <code>Response.json()</code>发送到客户端。</p>
<p>它接受一个对象或者数组，并在发送前将其转换为 JSON 格式：</p>
<pre><code class="language-js">res.json({ username: 'Flavio' });
</code></pre>
<h2 id="how-to-manage-cookies">如何管理 cookies</h2>
<p>使用<code>Response.cookie()</code>方法来控制 cookies。</p>
<p>示例：</p>
<pre><code class="language-js">res.cookie('username', 'Flavio');
</code></pre>
<p>这个方法还接受第三个参数，这个参数包含各种选项：</p>
<pre><code class="language-js">res.cookie('username', 'Flavio', {
  domain: '.flaviocopes.com',
  path: '/administrator',
  secure: true
});

res.cookie('username', 'Flavio', {
  expires: new Date(Date.now() + 900000),
  httpOnly: true
});
</code></pre>
<p>一些你可以设置的有用的参数包括：</p>
<table>
<thead>
<tr>
<th>值</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>domain</code></td>
<td><a href="https://www.freecodecamp.org/news/cookies/#set-a-cookie-domain">cookie 的域名</a></td>
</tr>
<tr>
<td><code>expires</code></td>
<td>设置 <a href="https://www.freecodecamp.org/news/cookies/#set-a-cookie-expiration-date">cookie 过期日期</a>。 如果未设置或者为 0，这个 cookie 将作为会话 cookie。</td>
</tr>
<tr>
<td><code>httpOnly</code></td>
<td>设置 cookie 仅被 web 服务器访问。 具体可查看 <a href="https://www.freecodecamp.org/news/cookies/#httponly">HttpOnly</a></td>
</tr>
<tr>
<td><code>maxAge</code></td>
<td>设置相对于当前时间的过期时间，以毫秒为单位</td>
</tr>
<tr>
<td><code>path</code></td>
<td><a href="https://www.freecodecamp.org/news/cookies/#set-a-cookie-path">cookie 路径</a>。默认值为 '/'</td>
</tr>
<tr>
<td><code>secure</code></td>
<td>标记为<a href="https://www.freecodecamp.org/news/cookies/#secure">cookie HTTPS only</a></td>
</tr>
<tr>
<td><code>signed</code></td>
<td>设置需要签名的 cookie</td>
</tr>
<tr>
<td><code>sameSite</code></td>
<td><a href="https://www.freecodecamp.org/news/cookies/#samesite"><code>SameSite</code></a>的值</td>
</tr>
</tbody>
</table>
<p>清除 cookie 可以使用：</p>
<pre><code class="language-js">res.clearCookie('username');
</code></pre>
<h2 id="how-to-work-with-http-headers">如何处理 HTTP 标头</h2>
<h3 id="http">如何通过请求获取 HTTP 标头值</h3>
<p>可以使用 <code>Request.headers</code> 属性访问所有 HTTP 标头的值：</p>
<pre><code class="language-js">app.get('/', (req, res) =&gt; {
  console.log(req.headers);
});
</code></pre>
<p>使用<code>Request.header()</code> 方法获取单个请求标头的值：</p>
<pre><code class="language-js">app.get('/', (req, res) =&gt; {
  req.header('User-Agent');
});
</code></pre>
<h3 id="http">如何为响应改变 HTTP 标头</h3>
<p>可以使用 <code>Response.set()</code>改变 HTTP 标头值：</p>
<pre><code class="language-js">res.set('Content-Type', 'text/html');
</code></pre>
<p>Content-Type 标头的便捷方式是：</p>
<pre><code class="language-js">res.type('html');
// =&gt; 'text/html'

res.type('json');
// =&gt; 'application/json'

res.type('application/json');
// =&gt; 'application/json'

res.type('png');
// =&gt; image/png:
</code></pre>
<h2 id="how-to-handle-redirects">如何处理重定向</h2>
<p>在 Web 开发中重定向很常见，可以使用 <code>Response.redirect()</code>实现：</p>
<pre><code class="language-js">res.redirect('/go-there');
</code></pre>
<p>这样就创建了一个 302 重定向。</p>
<p>可以使用以下方法创建一个 301 重定向：</p>
<pre><code class="language-js">res.redirect(301, '/go-there');
</code></pre>
<p>你可以指定绝对路径（<code>/go-there</code>）、绝对 URL（<code>https://anothersite.com</code>）、相对路径（<code>go-there</code>）或者 <code>..</code> 返回上一层：</p>
<pre><code class="language-js">res.redirect('../go-there');
res.redirect('..');
</code></pre>
<p>你还可以重定向回 Referrer HTTP 标头值（如果未设置默认值为 <code>/</code>）：</p>
<pre><code class="language-js">res.redirect('back');
</code></pre>
<h2 id="routing-in-express">Express 中的路由</h2>
<p>路由是确定调用 URL 时应该发生什么的过程，或者应用程序的哪些部分应该处理特定的传入请求。</p>
<p>在 Hello World 示例中，我们使用了这段代码：</p>
<pre><code class="language-js">app.get('/', (req, res) =&gt; {
  /* */
});
</code></pre>
<p>这里创建一个路由，访问根域 URL<code>/</code>，并使用 HTTP GET 方法映射到我们需要的响应。</p>
<h3 id="">命名参数</h3>
<p>如果想监听自定义请求怎么办？也许我们想创建一个接受字符串并将其作为大写字母返回的服务——我们不希望参数作为查询字符串发送，而是作为 URL 的一部分发送。在这种情况下，我们使用命名参数：</p>
<pre><code class="language-js">app.get('/uppercase/:theValue', (req, res) =&gt;
  res.send(req.params.theValue.toUpperCase())
);
</code></pre>
<p>如果发送请求到 <code>/uppercase/test</code>，我们会在响应体中得到 <code>TEST</code>。</p>
<p>你可以在同一个 URL 中使用多个命名参数，它们都将存储在 <code>req.params</code>.</p>
<h3 id="">如何使用正则表达式匹配路径</h3>
<p>可以通过一条<a href="https://flaviocopes.com/javascript-regular-expressions/">正则表达式</a> 来匹配多个路径：</p>
<pre><code class="language-js">app.get(/post/, (req, res) =&gt; {
  /* */
});
</code></pre>
<p>以上代码将匹配 <code>/post</code>, <code>/post/first</code>, <code>/thepost</code>, <code>/posting/something</code>等路径。</p>
<h2 id="templates-in-express">Express 中的模版</h2>
<p>Express 能够处理服务器端模板引擎。</p>
<p>模板引擎允许我们向视图添加数据，并动态生成 HTML。</p>
<p>Express 默认使用 Jade。Jade 是 Pug 的旧版本，特指 Pug 1.0。</p>
<p>请注意，由于商标问题，该项目在 2016 年发布第二版时，名称从 Jade 改为 Pug。你仍然可以使用 Jade，又称 Pug 1.0，但往后最好使用 Pug 2.0。</p>
<p>尽管 Jade 的最后一个版本已经陈旧了，但出于向后兼容的原因，它仍然是 Express 中的默认版本。</p>
<p>你可以在任何新项目中使用 Pug 或你选择的引擎。Pug 的官方网站是<a href="https://pugjs.org/">https://pugjs.org/</a>。</p>
<p>可以使用许多不同的模板引擎，包括 Pug、Handlebars、Mustache、EJS 等。</p>
<p>要使用 Pug，我们必须先安装它：</p>
<pre><code class="language-bash">npm install pug
</code></pre>
<p>在初始化 Express 应用程序时，我们需要设置它：</p>
<pre><code class="language-js">const express = require('express');
const app = express();
app.set('view engine', 'pug');
</code></pre>
<p>然后就可以在 <code>.pug</code> 文件中编写模板。</p>
<p>创建一个 about 视图：</p>
<pre><code class="language-js">app.get('/about', (req, res) =&gt; {
  res.render('about');
});
</code></pre>
<p>模板路径为<code>views/about.pug</code>:</p>
<pre><code>p Hello from Flavio
</code></pre>
<p>该模板创建一个 <code>p</code> 标签，内容为 <code>Hello from Flavio</code>。</p>
<p>你也可以使用以下代码插入变量：</p>
<pre><code class="language-js">app.get('/about', (req, res) =&gt; {
  res.render('about', { name: 'Flavio' });
});
</code></pre>
<pre><code>p Hello from #{name}
</code></pre>
<p>更多使用 Pug 的信息，可以查看 <a href="https://flaviocopes.com/pug">Pug 指南</a>。</p>
<p>推荐你使用这个在线 HTML 到 Pug 转换器 <a href="https://html-to-pug.com/">https://html-to-pug.com/</a>。</p>
<h2 id="express-middleware">Express 中间件</h2>
<p>中间件是一个挂钩到路由过程中的函数，在链中的某个点执行任意操作（取决于我们想要它做什么）。</p>
<p>它通常用于编辑请求或响应对象，或者在请求到达路由处理程序代码之前终止请求。</p>
<p>可以通过如下方法将中间件添加到执行栈：</p>
<pre><code class="language-js">app.use((req, res, next) =&gt; {
  /* */
});
</code></pre>
<p>这和定义路由类似，但是在 Request 和 Response 实例之后，我们还引用了 <em>next</em> 中间件函数，并分配给了<code>next</code>变量。</p>
<p>我们总是在中间件函数末尾调用<code>next()</code>以便将执行传递给下一个处理程序。除非我们想提前结束响应并将其发送回客户端。</p>
<p>通常我们通过 <code>npm</code> 包来使用预制的中间件。你可以在<a href="https://expressjs.com/en/resources/middleware.html">这里</a>找到中间件列表。</p>
<p>其中一个预制中间件示例就是 <code>cookie-parser</code>，它可以将 cookie 解析为 <code>req.cookies</code>对象。 你可以使用 <code>npm install cookie-parser</code> 命令安装并使用：</p>
<pre><code class="language-js">const express = require('express');
const app = express();
const cookieParser = require('cookie-parser');

app.get('/', (req, res) =&gt; res.send('Hello World!'));

app.use(cookieParser());
app.listen(3000, () =&gt; console.log('Server ready'));
</code></pre>
<p>我们还可以将中间件函数设置为仅针对特定路由运行（而不是针对所有路由），方法是将其作为路由定义的第二个参数：</p>
<pre><code class="language-js">const myMiddleware = (req, res, next) =&gt; {
  /* ... */
  next();
};

app.get('/', myMiddleware, (req, res) =&gt; res.send('Hello World!'));
</code></pre>
<p>如果需要存储中间件生成的数据，并传递给后续中间件函数或请求处理程序，你可以使用<code>Request.locals</code>对象。它将该数据附加到当前请求：</p>
<pre><code class="language-js">req.locals.name = 'Flavio';
</code></pre>
<h2 id="how-to-serve-static-assets-with-express">如何使用 Express 提供静态资源</h2>
<p>通常图片、CSS 被存储在 <code>public</code>子文件夹，并暴露给根目录：</p>
<pre><code class="language-js">const express = require('express');
const app = express();

app.use(express.static('public'));

/* ... */

app.listen(3000, () =&gt; console.log('Server ready'));
</code></pre>
<p>如果在 <code>public/</code>有一个<code>index.html</code>文件，当你访问根域 URL（<code>http://localhost:3000</code>）时，就会提供静态资源。</p>
<h2 id="how-to-send-files-to-the-client">如何向客户端发送文件</h2>
<p>Express 提供了一个简便的方法将文件转换为附件传输： <code>Response.download()</code>。</p>
<p>一旦用户点击使用此方法发送文件的路由，浏览器将提示用户下载。</p>
<p><code>Response.download()</code> 方法允许发送附加到请求的文件，浏览器不会在页面中显示它，而是将其保存到磁盘。</p>
<pre><code class="language-js">app.get('/', (req, res) =&gt; res.download('./file.pdf'));
</code></pre>
<p>在应用上下文中的示例：</p>
<pre><code class="language-js">const express = require('express');
const app = express();

app.get('/', (req, res) =&gt; res.download('./file.pdf'));
app.listen(3000, () =&gt; console.log('Server ready'));
</code></pre>
<p>你可以将文件设置为使用自定义文件名发送：</p>
<pre><code class="language-js">res.download('./file.pdf', 'user-facing-filename.pdf');
</code></pre>
<p>此方法提供了一个回调函数，你可以使用它在文件发送后执行代码：</p>
<pre><code class="language-js">res.download('./file.pdf', 'user-facing-filename.pdf', (err) =&gt; {
  if (err) {
    //handle error
    return;
  } else {
    //do something
  }
});
</code></pre>
<h2 id="sessions-in-express">Express 中的会话</h2>
<p>默认情况下，Express 请求是有顺序的，但请求之间没有相互链接，所以没有办法知道这个请求是否来自之前已经执行过请求的客户端。</p>
<p>除非使用某种识别机制，否则用户无法识别请求。</p>
<p>会话就应运而生。</p>
<p>使用会话后，你的 API 或网站的每个用户都将被分配一个唯一的会话，就可以存储用户的状态。</p>
<p>我们将使用 <code>express-session</code> 模块来演示，它由 Express 团队维护。</p>
<p>可以使用以下命令安装：</p>
<pre><code class="language-bash">npm install express-session
</code></pre>
<p>安装完毕后，可以通过以下代码实例化：</p>
<pre><code class="language-js">const session = require('express-session');
</code></pre>
<p>它是一个中间件，所以你使用以下代码在 Express 中 <em>安装</em> 它：</p>
<pre><code class="language-js">const express = require('express');
const session = require('express-session');

const app = express();
app.use(
  session({
    secret: '343ji43j4n3jn4jk3n'
  })
);
</code></pre>
<p>编写完毕后，所有应用路由都使用会话。</p>
<p><code>secret</code>是唯一的必填参数，还有许多可选参数。<code>secret</code>必须为一个唯一的随机字符串。</p>
<p>会话被添加到请求，所以可以通过 <code>req.session</code>访问：</p>
<pre><code class="language-js">app.get('/', (req, res, next) =&gt; {
  // req.session
}
</code></pre>
<p>该对象可用于从会话中获取数据，也可用于设置数据：</p>
<pre><code class="language-js">req.session.name = 'Flavio';
console.log(req.session.name); // 'Flavio'
</code></pre>
<p>此数据在存储时被序列化为 <a href="https://www.freecodecamp.org/news/json/">JSON</a>，所以可以安全使用嵌套对象。</p>
<p>你可以使用会话将数据传递给稍后执行的中间件，或者稍后根据后续请求检索数据。</p>
<p>会话数据存储在哪里？这取决于你如何设置<code>express-session</code>模块。</p>
<p>会话数据可被存储在：</p>
<ul>
<li><strong>内存</strong>，不适用于生产</li>
<li><strong>数据库</strong>，如 MySQL 或者 Mongo</li>
<li><strong>内存缓存</strong>，如 Redis 或者 Memcached</li>
</ul>
<p>在 <a href="https://github.com/expressjs/session">https://github.com/expressjs/session</a> 中有一个巨大的第三方包列表，可以实现不同兼容性的缓存存储。</p>
<p>所有解决方案都将会话 ID 存储在 cookie 中，并将数据保存在服务器端。客户端将在 cookie 中接收会话 ID，并将它与每个 HTTP 请求一起发送。</p>
<p>我们将引用该服务器端以将会话 ID 与本地存储的数据相关联。</p>
<p>内存是默认设置，不需要你进行特殊操作。这样很便捷，但它仅用于开发目的。</p>
<p>最好是选择 Redis 之类的内存缓存，需要为其设置自己的基础架构。</p>
<p>另一个常用管理会话的包是<code>cookie-session</code>，与 Redis 巨大的不同是将数据存储在客户端的 cookie。</p>
<p>我不建议这样做，因为将数据存储在 cookie 中意味着它存储在客户端，并在用户发出的每个请求中来回发送。它的大小也有限制，因为它只能存储 4 KB 的数据。</p>
<p>Cookie 也需要受到保护，但默认情况它不受保护，安全 Cookie 可以在 HTTPS 站点上使用，如果你使用代理上网，则需要配置它。</p>
<h2 id="how-to-validate-input-in-express">如何在 Express 中验证输入</h2>
<p>让我们看看如何验证作为输入进入 Express 端点的任何数据。</p>
<p>假设你有一个接受 name、email 和 age 参数的 POST 端点：</p>
<pre><code class="language-js">const express = require('express');
const app = express();

app.use(express.json());

app.post('/form', (req, res) =&gt; {
  const name = req.body.name;
  const email = req.body.email;
  const age = req.body.age;
});
</code></pre>
<p>如何对这些结果执行服务器端验证以确保：</p>
<ul>
<li>name 是包含至少 3 个字符的字符串？</li>
<li>email 是真正的邮箱地址？</li>
<li>age 为 0 到 110 之间的数字？</li>
</ul>
<p>在 Express 中处理来自外部的任何输入的验证的最佳方法是使用 <a href="https://express-validator.github.io"><code>express-validator</code> 包</a>：</p>
<pre><code class="language-bash">npm install express-validator
</code></pre>
<p>引用包中的<code>check</code>和<code>validationResult</code>对象：</p>
<pre><code class="language-js">const { check, validationResult } = require('express-validator');
</code></pre>
<p>在<code>post()</code>调用中，我们将包含<code>check()</code>调用的数组作为第二个参数传入。每一个 <code>check()</code> 都接受参数名作为实参。最后调用<code>validationResult()</code>来验证是否有验证报错，如果有就告知客户端：</p>
<pre><code class="language-js">app.post(
  '/form',
  [
    check('name').isLength({ min: 3 }),
    check('email').isEmail(),
    check('age').isNumeric()
  ],
  (req, res) =&gt; {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.array() });
    }

    const name = req.body.name;
    const email = req.body.email;
    const age = req.body.age;
  }
);
</code></pre>
<p>注意我使用了：</p>
<ul>
<li><code>isLength()</code></li>
<li><code>isEmail()</code></li>
<li><code>isNumeric()</code></li>
</ul>
<p>还有更多方法，都来自 <a href="https://github.com/chriso/validator.js#validators">validator.js</a>，包括：</p>
<ul>
<li><code>contains()</code>, 检查是否包含指定值</li>
<li><code>equals()</code>, 检查是否与指定值相等</li>
<li><code>isAlpha()</code></li>
<li><code>isAlphanumeric()</code></li>
<li><code>isAscii()</code></li>
<li><code>isBase64()</code></li>
<li><code>isBoolean()</code></li>
<li><code>isCurrency()</code></li>
<li><code>isDecimal()</code></li>
<li><code>isEmpty()</code></li>
<li><code>isFQDN()</code>, 检查是否为完全合格的域名</li>
<li><code>isFloat()</code></li>
<li><code>isHash()</code></li>
<li><code>isHexColor()</code></li>
<li><code>isIP()</code></li>
<li><code>isIn()</code>, 检查值是否属于允许值数组</li>
<li><code>isInt()</code></li>
<li><code>isJSON()</code></li>
<li><code>isLatLong()</code></li>
<li><code>isLength()</code></li>
<li><code>isLowercase()</code></li>
<li><code>isMobilePhone()</code></li>
<li><code>isNumeric()</code></li>
<li><code>isPostalCode()</code></li>
<li><code>isURL()</code></li>
<li><code>isUppercase()</code></li>
<li><code>isWhitelisted()</code>, 检查输入是否在白名单内</li>
</ul>
<p>你也可以使用<code>matches()</code>来进行正则表达式验证。</p>
<p>日期可以通过以下方式验证：</p>
<ul>
<li><code>isAfter()</code>, 检查输入的日期是否在你传入的日期之后</li>
<li><code>isBefore()</code>, 检查输入的日期是否在你传入的日期之前</li>
<li><code>isISO8601()</code></li>
<li><code>isRFC3339()</code></li>
</ul>
<p>更多如何使用验证器的方法，<a href="https://github.com/chriso/validator.js#validators">可以参考这份文档</a>。</p>
<p>所有上述验证都可以串联起来：</p>
<pre><code class="language-js">check('name').isAlpha().isLength({ min: 10 });
</code></pre>
<p>如果有任何错误，服务器会自动发送一个响应来传达错误。例如，如果电子邮件无效，将返回以下内容：</p>
<pre><code class="language-js">{
  "errors": [{
    "location": "body",
    "msg": "Invalid value",
    "param": "email"
  }]
}
</code></pre>
<p>可以使用 <code>withMessage()</code>覆盖默认报错：</p>
<pre><code class="language-js">check('name')
  .isAlpha()
  .withMessage('Must be only alphabetical chars')
  .isLength({ min: 10 })
  .withMessage('Must be at least 10 chars long');
</code></pre>
<p>如果你想要编写自定义验证器，可以使用 <code>custom</code> 验证器。</p>
<p>在回调函数中，你可以通过抛出异常或返回被拒绝的 promise 来拒绝验证：</p>
<pre><code class="language-js">app.post(
  '/form',
  [
    check('name').isLength({ min: 3 }),
    check('email').custom((email) =&gt; {
      if (alreadyHaveEmail(email)) {
        throw new Error('Email already registered');
      }
    }),
    check('age').isNumeric()
  ],
  (req, res) =&gt; {
    const name = req.body.name;
    const email = req.body.email;
    const age = req.body.age;
  }
);
</code></pre>
<p>自定义验证器：</p>
<pre><code class="language-js">check('email').custom((email) =&gt; {
  if (alreadyHaveEmail(email)) {
    throw new Error('Email already registered');
  }
});
</code></pre>
<p>也可改写为：</p>
<pre><code class="language-js">check('email').custom((email) =&gt; {
  if (alreadyHaveEmail(email)) {
    return Promise.reject('Email already registered');
  }
});
</code></pre>
<h2 id="how-to-sanitize-input-in-express">如何在 Express 清理输入</h2>
<p>你已经了解了如何验证从外部世界到 Express 应用程序的输入。</p>
<p>当运行面向公众的服务器时，你很快就会学到一件事：永远不要相信输入。</p>
<p>即使在客户端你已经预做了一遍清理，确保人们不会输入奇怪的内容，你仍然会受到人们使用工具（甚至只是浏览器开发工具）直接 POST 到端点的影响。</p>
<p>或者机器人尝试人类已知的每一种可能的攻击组合。</p>
<p>需要做的是在服务器端清理输入。</p>
<p><a href="https://express-validator.github.io"><code>express-validator</code> 包</a>除了可以验证输入以外也可以清理输入。</p>
<p>假设你有一个接受 name、email 和 age 参数的 POST 端点：</p>
<pre><code class="language-js">const express = require('express');
const app = express();

app.use(express.json());

app.post('/form', (req, res) =&gt; {
  const name = req.body.name;
  const email = req.body.email;
  const age = req.body.age;
});
</code></pre>
<p>你可以这样验证输入：</p>
<pre><code class="language-js">const express = require('express');
const app = express();

app.use(express.json());

app.post(
  '/form',
  [
    check('name').isLength({ min: 3 }),
    check('email').isEmail(),
    check('age').isNumeric()
  ],
  (req, res) =&gt; {
    const name = req.body.name;
    const email = req.body.email;
    const age = req.body.age;
  }
);
</code></pre>
<p>你可以通过在验证方法之后串联清理方法来添加清理：</p>
<pre><code class="language-js">app.post(
  '/form',
  [
    check('name').isLength({ min: 3 }).trim().escape(),
    check('email').isEmail().normalizeEmail(),
    check('age').isNumeric().trim().escape()
  ],
  (req, res) =&gt; {
    //...
  }
);
</code></pre>
<p>在代码中我使用的清理方法包括：</p>
<ul>
<li><code>trim()</code>修剪字符串开头和结尾的字符（默认为空格）</li>
<li><code>escape()</code>将 <code>&lt;</code>, <code>&gt;</code>, <code>&amp;</code>, <code>'</code>, <code>"</code>和 <code>/</code>替换成它们对应的 HTML 实体</li>
<li><code>normalizeEmail()</code>规范化电子邮件地址，它接受小写邮件地址或者子地址的选项。（如 <code>flavio+newsletters@gmail.com</code>）</li>
</ul>
<p>其他的清理方法包括：</p>
<ul>
<li><code>blacklist()</code> 删除出现在黑名单中的字符</li>
<li><code>whitelist()</code> 删除未出现在白名单中的字符</li>
<li><code>unescape()</code> 将 HTML 编码的实体替换为<code>&lt;</code>, <code>&gt;</code>, <code>&amp;</code>, <code>'</code>, <code>"</code> 和 <code>/</code></li>
<li><code>ltrim()</code> 类似于 trim()，但只修剪字符串开头的字符</li>
<li><code>rtrim()</code> 类似于 trim()， 但只修剪字符串末尾的字符</li>
<li><code>stripLow()</code>删除通常不可见的 ASCII 控制字符</li>
</ul>
<p>强制转换格式：</p>
<ul>
<li><code>toBoolean()</code> 将输入字符串转换为布尔值。除了 '0'、'false' 和 '' 之外的所有内容都返回 true。在严格模式下，只有 '1' 和 'true' 返回 true。</li>
<li><code>toDate()</code> 将输入字符串转换为日期，如果输入不是日期，则为 null。</li>
<li><code>toFloat()</code> 将输入字符串转换为浮点数，如果输入不是浮点数，则转换为 NaN。</li>
<li><code>toInt()</code>将输入字符串转换为整数，如果输入不是整数，则转换为 NaN。</li>
</ul>
<p>与自定义验证器一样，你可以创建自定义清理器。</p>
<p>在回调函数中，你只需返回清理后的值：</p>
<pre><code class="language-js">const sanitizeValue = (value) =&gt; {
  //sanitize...
};

app.post(
  '/form',
  [
    check('value').customSanitizer((value) =&gt; {
      return sanitizeValue(value);
    })
  ],
  (req, res) =&gt; {
    const value = req.body.value;
  }
);
</code></pre>
<h2 id="how-to-handle-forms-in-express">如何在 Express 中处理表单</h2>
<p>以下是一个 HTML 表单示例：</p>
<pre><code class="language-html">&lt;form method="POST" action="/submit-form"&gt;
  &lt;input type="text" name="username" /&gt;
  &lt;input type="submit" /&gt;
&lt;/form&gt;
</code></pre>
<p>当用户按下提交按钮时，浏览器会自动向页面同源的<code>/submit-form</code> URL 发出 <code>POST</code>请求。浏览器发送表单包含的数据，编码为 <code>application/x-www-form-urlencoded</code>。在此特定示例中，表单数据包含<code>username</code>输入字段值。</p>
<p>表单也可以通过 <code>GET</code> 方法发送数据，但是大多数情况为<code>POST</code>。</p>
<p>表单数据将在 POST 请求体中发送。</p>
<p>可以使用<code>express.urlencoded()</code>中间件提取：</p>
<pre><code class="language-js">const express = require('express');
const app = express();

app.use(
  express.urlencoded({
    extended: true
  })
);
</code></pre>
<p>现在，需要在<code>/submit-form</code> 路由上创建一个 <code>POST</code> 端点，任何数据都可以在 <code>Request.body</code> 访问：</p>
<pre><code class="language-js">app.post('/submit-form', (req, res) =&gt; {
  const username = req.body.username;
  //...
  res.end();
});
</code></pre>
<p>别忘记提前使用<code>express-validator</code>验证数据。</p>
<h2 id="how-to-handle-file-uploads-in-forms-in-express">如何在 Express 中处理表单中的文件上传</h2>
<p>以下代码是允许用户上传文件的 HTML 表单示例：</p>
<pre><code class="language-html">&lt;form method="POST" action="/submit-form" enctype="multipart/form-data"&gt;
  &lt;input type="file" name="document" /&gt;
  &lt;input type="submit" /&gt;
&lt;/form&gt;
</code></pre>
<p>别忘记在表单添加<code>enctype="multipart/form-data"</code>，否则表单不会被上传。</p>
<p>当用户按下提交按钮时，浏览器会自动向页面同源的<code>/submit-form</code> URL 发出 <code>POST</code> 请求。浏览器发送表单包含的数据，但表单未编码为普通表单 <code>application/x-www-form-urlencoded</code>，而是 <code>multipart/form-data</code>。</p>
<p>在服务器端，处理多部分数据可能很棘手且容易出错，因此我们将使用一个名为 <strong>formidable</strong> 的库。<a href="https://github.com/felixge/node-formidable">这里是它的 GitHub 仓库</a> – 拥有超过 4000 颗星，并且维护良好。</p>
<p>可以通过以下命令安装：</p>
<pre><code class="language-bash">npm install formidable
</code></pre>
<p>然后引用到 Node.js 文件：</p>
<pre><code class="language-js">const express = require('express');
const app = express();
const formidable = require('formidable');
</code></pre>
<p>现在，在 <code>/submit-form</code> 路由的 <code>POST</code> 端点中，我们使用 <code>formidable.IncomingForm()</code> 实例化一个新的 Formidable 表单：</p>
<pre><code class="language-js">app.post('/submit-form', (req, res) =&gt; {
  new formidable.IncomingForm();
});
</code></pre>
<p>这样做之后，我们需要解析表单。我们可以通过回调来同步执行此操作，这意味着所有文件都已处理。一旦 formidable 完成，文件就可以被访问：</p>
<pre><code class="language-js">app.post('/submit-form', (req, res) =&gt; {
  new formidable.IncomingForm().parse(req, (err, fields, files) =&gt; {
    if (err) {
      console.error('Error', err);
      throw err;
    }
    console.log('Fields', fields);
    console.log('Files', files);
    for (const file of Object.entries(files)) {
      console.log(file);
    }
  });
});
</code></pre>
<p>或者可以使用事件而不是回调。例如，当每个文件被解析时，或其他事件（例如文件处理完成、接收非文件字段或发生错误）时，都会收到通知：</p>
<pre><code class="language-js">app.post('/submit-form', (req, res) =&gt; {
  new formidable.IncomingForm()
    .parse(req)
    .on('field', (name, field) =&gt; {
      console.log('Field', name, field);
    })
    .on('file', (name, file) =&gt; {
      console.log('Uploaded file', name, file);
    })
    .on('aborted', () =&gt; {
      console.error('Request aborted by the user');
    })
    .on('error', (err) =&gt; {
      console.error('Error', err);
      throw err;
    })
    .on('end', () =&gt; {
      res.end();
    });
});
</code></pre>
<p>无论选择哪种方式，你都将获得一个或多个 Formidable.File 对象，这些对象为你提供有关已上传文件的信息。这些是可以调用的一些方法：</p>
<ul>
<li><code>file.size</code>, 以字节为单位的文件大小</li>
<li><code>file.path</code>, 文件写入的路径</li>
<li><code>file.name</code>, 文件名</li>
<li><code>file.type</code>, 文件的 MIME 类型</li>
</ul>
<p>路径默认为临时文件夹，如果监听 <code>fileBegin</code> 事件可以修改：</p>
<pre><code class="language-js">app.post('/submit-form', (req, res) =&gt; {
  new formidable.IncomingForm()
    .parse(req)
    .on('fileBegin', (name, file) =&gt; {
      file.path = __dirname + '/uploads/' + file.name;
    })
    .on('file', (name, file) =&gt; {
      console.log('Uploaded file', name, file);
    });
  //...
});
</code></pre>
<h2 id="">感谢阅读！</h2>
<p>这就是手册的全部内容。别忘了，如果需要的话，<a href="https://thevalleyofcode.com/download/express/">你可以下载该手册的 PDF 或者 ePub 版本</a>。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 面向初学者的 Linux Shell——解释 Bash、Zsh 和 Fish ]]>
                </title>
                <description>
                    <![CDATA[ 当你打开终端时，很可能发现终端使用 Bash 作为其 UNIX shell [https://zh.m.wikipedia.org/zh-hans/Unix_shell]  环境。当然除了 Bash，还存在其他“shell”环境。 例如 C Shell [https://zh.wikipedia.org/wiki/C_Shell]、Korn Shell [https://zh.wikipedia.org/wiki/KornShell]、Z Shell [https://zh.wikipedia.org/zh-tw/Z_shell]，甚至 Fish Shell。不同的 shell 环境各有千秋，在你选择自己系统的 shell 之前，你应该评估一下。 我将在本文中介绍一些流行的 shell 及主要功能，帮助你做选择。 Bash Shell Bash Shell（或 Bourne Again Shell）是一种 UNIX shell 和命令语言。它是由 Brain Fox 为 GNU 项目编写的，目的是作为 Bourne Shell (sh) 的免费软件替代品。 Bash 于 198 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/linux-shells-explained/</link>
                <guid isPermaLink="false">639ed3c3a7bffa07c744166d</guid>
                
                    <category>
                        <![CDATA[ Linux ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Bash ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ PapayaHUANG ]]>
                </dc:creator>
                <pubDate>Fri, 16 Dec 2022 04:19:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/12/pexels-oleksandr-pidvalnyi-320260.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/linux-shells-explained/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">Linux Shells for Beginners – Bash, Zsh, and Fish Explained</a>
      </p><!--kg-card-begin: markdown--><p>当你打开终端时，很可能发现终端使用 Bash 作为其 <a href="https://zh.m.wikipedia.org/zh-hans/Unix_shell">UNIX shell</a> 环境。当然除了 Bash，还存在其他“shell”环境。</p>
<p>例如 <a href="https://zh.wikipedia.org/wiki/C_Shell">C Shell</a>、<a href="https://zh.wikipedia.org/wiki/KornShell">Korn Shell</a>、<a href="https://zh.wikipedia.org/zh-tw/Z_shell">Z Shell</a>，甚至 Fish Shell。不同的 shell 环境各有千秋，在你选择自己系统的 shell 之前，你应该评估一下。</p>
<p>我将在本文中介绍一些流行的 shell 及主要功能，帮助你做选择。</p>
<h2 id="bashshell">Bash Shell</h2>
<p>Bash Shell（或 Bourne Again Shell）是一种 UNIX shell 和命令语言。它是由 Brain Fox 为 GNU 项目编写的，目的是作为 Bourne Shell (sh) 的免费软件替代品。</p>
<p>Bash 于 1989 年首次发布，它是大多数 Linux 发行版的默认 Shell 环境。其他发行版，如 Kali Linux，使用 Z Shell 作为默认 shell。</p>
<p>Bash 是 Linus Torvalds（Linux 的创建者）移植到 Linux 的首批程序之一。</p>
<p><img src="https://media.geeksforgeeks.org/wp-content/uploads/cli_example.png" alt="Bash" width="600" height="400" loading="lazy"></p>
<p><a href="https://www.geeksforgeeks.org/introduction-linux-shell-shell-scripting/">这是以上图片的来源</a></p>
<p>你应该记住 Bash 也是一种编程语言。所以它即是一个“Shell”，同时你也可以使用 Bash 对行为进行编程，例如，Bash 中的"Hello World"程序：</p>
<pre><code>#!/bin/bash
echo "Hello World"
</code></pre>
<h3 id="bash">Bash 的关键点</h3>
<ul>
<li>因为 Bash 是大多数系统上的默认 shell 环境，大多数用户使用它。</li>
<li>Bash 没有内联通配符表达式。通配符表达式是当你想要在 Shell 中搜索模式（pattern）的时候使用，类似于正则表达式（Regex）。三个主要的通配符是 <code>*</code>、<code>?</code> 和 <code>[]</code>。</li>
<li>不能自动更改目录名称。</li>
<li><code>#</code> 在脚本中被视为注释。</li>
<li>它有<code>shopt</code>（shell option 缩写）设置。</li>
<li>提示符（prompt）有反斜杠转义。</li>
<li>用户配置设置在 <code>.bashrc</code> 中。</li>
</ul>
<h2 id="zshell">Z Shell</h2>
<p>Z Shell 又称 Zsh， 是一个与 Bash 非常相似的 UNIX shell。你还可以使用 Zsh 编写脚本并将 shell 用作命令解释器。</p>
<p>Zsh 是 Bourne shell 的扩展，因此在此之上很多改进。 Zsh 于 1990 年由 Paul Falstad 发布，它具有 Bash、Korn Shell 和 C Shell 共有的一些功能。</p>
<p>macOS 默认使用 Zsh Shell。</p>
<p><img src="https://ohmyz.sh/img/themes/nebirhos.jpg" alt="Zsh Shell" width="600" height="400" loading="lazy"></p>
<p><a href="https://ohmyz.sh/">这是以上图片的来源</a></p>
<h3 id="zsh">Zsh 的关键点</h3>
<ul>
<li>在终端使用时，Zsh 带有自动补全功能。因此，当你按下<code>Tab↹</code>以自动补全你想运行的任何命令时，它不仅为你自动补全，而且弹出下拉菜单，包含所有其他可能的文件和目录。</li>
</ul>
<p><img src="https://i.ibb.co/bswYkn0/0f8c8e1a6016.gif" alt="Zsh Toggle" width="600" height="400" loading="lazy"></p>
<ul>
<li>支持内联通配符表达式</li>
<li>比 Bash 可配置度更高</li>
<li>支持插件和主题。这里是 Zsh 的<a href="https://github.com/unixorn/awesome-zsh-plugins">插件清单</a></li>
</ul>
<p>同时，还有围绕 Z Shell 构建的框架。最受欢迎的框架之一是<a href="https://ohmyz.sh/">Oh My Zsh</a>，它是一个社区驱动的开源框架，用于管理 Zsh 配置。（我用 Oh My Zsh😄）</p>
<p><img src="https://cdn.osxdaily.com/wp-content/uploads/2021/11/oh-my-zsh-mac.jpg" alt="Oh My Zsh" width="600" height="400" loading="lazy"></p>
<p><a href="https://osxdaily.com/2021/11/15/how-install-oh-my-zsh-mac/">这是以上图片的来源</a></p>
<p>Zsh 和 Oh My Zsh 很相似，但并不完全相同。重申一下，Oh My Zsh 是一种管理 Zsh 配置的方式，它不是 Shell 本身。</p>
<h2 id="fishshell">Fish Shell</h2>
<p>Fish 是一个强调交互性和可用性的 UNIX shell 环境。与 Zsh 不同，Fish 旨在为用户提供交互性，而不是信任用户实现自己的配置。</p>
<p>它由 Axel Liljencrantz 于 2005 年创建。由于不符合 POSIX shell 标准，Fish 被认为是“奇异的 shell”。 [[资料来源](<a href="https://en.wikipedia.org/wiki/Fish_(Unix_shell)">https://en.wikipedia.org/wiki/Fish_(Unix_shell)</a>]</p>
<p><img src="http://blog.sudobits.com/wp-content/uploads/2015/06/fish-shell-screenshot.png" alt="Fish Shell" width="600" height="400" loading="lazy"></p>
<p><a href="https://blog.sudobits.com/2015/06/05/fish-a-user-friendly-command-line-shell-for-ubuntulinux/">这是以上图片的来源</a></p>
<h3 id="fish">Fish 的关键点</h3>
<ul>
<li>Fish 根据你的命令历史记录和所在目录提供“键入时搜索”自动建议。与 Bash 的历史搜索类似，Fish Shell 的搜索历史<strong>始终</strong>处于打开状态。这样，用户终端工作时能够获得交互式反馈。</li>
</ul>
<p><img src="https://taskwarrior.org/images/fish.gif" alt="Fish autocomplete" width="600" height="400" loading="lazy"></p>
<p><a href="https://taskwarrior.org/news/news.20140906/">这是以上图片的来源</a></p>
<ul>
<li>Fish 还倾向将功能作为命令而不是语法。这使得功能的选项和帮助文本可见。</li>
<li>由于默认情况下 Fish 已经预置了很多配置，因此它被认为比 Zsh 等其他 <code>sh</code> 选项更适合初学者。</li>
<li>Fish 的脚本语言不同于 Zsh 和 Bash。 Zsh 使用更多简化指令（alias），而 Fish 避免在脚本语言中使用简化指令。</li>
</ul>
<p>如果你只是使用基本命令（如 <code>cd</code>、 <code>cp</code>、 <code>vim</code>、 <code>ssh</code> 等）编写脚本，你将不会注意到 Fish 和 Bash 的脚本语言的工作方式有何不同。</p>
<p>两者最大的区别之一是当你尝试捕获命令的输出时。在 Bash 中你可能习惯于这样：</p>
<pre><code class="language-bash">todays_date=$(date)
echo "Todays date is $todays_date"
</code></pre>
<p><img src="https://i.ibb.co/0hrF0Y3/fa71b0032fba.gif" alt="Output" width="600" height="400" loading="lazy"></p>
<pre><code>Todays Date is Tue Dec 13 15:29:28 CST 2022
</code></pre>
<p>而在 Fish 中，捕获输出的方式不同。Fish 脚本等价物如下：</p>
<pre><code class="language-bash">set date (date)
echo "Todays Date $date"
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/12/ezgif.com-gif-maker.gif" alt="ezgif.com-gif-maker" width="600" height="400" loading="lazy"></p>
<pre><code class="language-bash">todays date is Tue Dec 13 21:35:03 UTC 2022
</code></pre>
<h2 id="">总结</h2>
<p>Bash、Z Shell 和 Fish Shell 各有优点，也有一些相似之处。既然对它们有了更多的了解，你就可以在工作中更有效地使用它们。</p>
<p>如果你想要更可配置的 shell，你可以使用 Zsh（甚至安装 Oh My Zsh）。如果你想要更多交互的终端体验，同时不需要大量配置，你可以使用 Fish Shell。如果你想要经典的感觉，你可以保留 Bash。</p>
<p>选择取决于你作为开发人员的偏好 - 所以只需选择最适合你的 shell。</p>
<p><em>希望这对篇文章对你有所帮助！感谢阅读</em> 🐚🐚🐚</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ React 中的关注点分离——如何使用容器组件和展示组件 ]]>
                </title>
                <description>
                    <![CDATA[ 许多 React 新手会将逻辑和展示代码放在同一个 React 组件中，不知道将两者分离的重要性，对于他们来说，最重要的是代码能运行。 但之后当他们需要对文件进行改动，就面临一项艰巨的任务。这个时候他们就得重新考虑将两者分离的问题。 产生问题的原因是他们不了解关注点分离的概念、展示组件和容器组件模型。所以我将在这篇文章讲解这方面内容，帮助你在项目开发早期缓解这个问题。 本文将深入探讨容器和展示组件，稍微讲解一下关注点分离。 话不多说，让我们开始吧！ 目录  * 什么是关注点分离  * 什么是容器组件和展示组件  * 为什么需要这两种组件  * 展示组件和容器组件示例  * 如何通过 React 钩子取代容器组件  * 总结 什么是关注点分离 关注点分离是一个在编程中广泛使用的概念。它指的是执行不同操作的逻辑不应被分组或结合在一起。 例如接下来的代码示例就违反了关注点分离。我们把获取数据和展示数据放在了同一个组件中。 若要解决这个问题，并且遵循关注点分离，我们应该将两块（即：获取数据和在 UI 上展示）逻辑分开放置在不同的组件。 此时就需要容器组件和展示组件模式。在下文 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/separation-of-concerns-react-container-and-presentational-components/</link>
                <guid isPermaLink="false">639877a1a7bffa07c7441604</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ PapayaHUANG ]]>
                </dc:creator>
                <pubDate>Tue, 13 Dec 2022 04:19:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/12/container-and-presentational-component-pattern-image.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/separation-of-concerns-react-container-and-presentational-components/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">Separation of Concerns in React –How to Use Container and Presentational Components</a>
      </p><!--kg-card-begin: markdown--><p>许多 React 新手会将逻辑和展示代码放在同一个 React 组件中，不知道将两者分离的重要性，对于他们来说，最重要的是代码能运行。</p>
<p>但之后当他们需要对文件进行改动，就面临一项艰巨的任务。这个时候他们就得重新考虑将两者分离的问题。</p>
<p>产生问题的原因是他们不了解关注点分离的概念、展示组件和容器组件模型。所以我将在这篇文章讲解这方面内容，帮助你在项目开发早期缓解这个问题。</p>
<p>本文将深入探讨容器和展示组件，稍微讲解一下关注点分离。</p>
<p>话不多说，让我们开始吧！</p>
<h2 id="">目录</h2>
<ul>
<li><a href="#whatistheseparationofconcerns">什么是关注点分离</a></li>
<li><a href="#whatarecontainerandpresentationalcomponents">什么是容器组件和展示组件</a></li>
<li><a href="#whydoweneedthesecomponents">为什么需要这两种组件</a></li>
<li><a href="#presentationandcontainercomponentexample">展示组件和容器组件示例</a></li>
<li><a href="#howtoreplacecontainercomponentswithreacthooks">如何通过 React 钩子取代容器组件</a></li>
<li><a href="#summary">总结</a></li>
</ul>
<h2 id="whatistheseparationofconcerns">什么是关注点分离</h2>
<p>关注点分离是一个在编程中广泛使用的概念。它指的是执行不同操作的逻辑不应被分组或结合在一起。</p>
<p>例如接下来的代码示例就违反了关注点分离。我们把获取数据和展示数据放在了同一个组件中。</p>
<p>若要解决这个问题，并且遵循关注点分离，我们应该将两块（即：获取数据和在 UI 上展示）逻辑分开放置在不同的组件。</p>
<p>此时就需要容器组件和展示组件模式。在下文将做详细讲解。</p>
<h2 id="whatarecontainerandpresentationalcomponents">什么是容器组件和展示组件</h2>
<p>为了实现关注点分离我们需要两种类型的组件：</p>
<ul>
<li>容器组件</li>
<li>展示组件</li>
</ul>
<h3 id="">容器组件</h3>
<p>是提供、创建和持有数据并服务于子组件的组件。</p>
<p>容器组件的唯一工作是处理数据。它不包含自己的任何 UI。相反，展示组件作为使用这些数据的子组件。</p>
<p>一个简单的示例就是 <code>FetchUserContainer</code> 组件，包含获取所有用户的逻辑。</p>
<h3 id="">展示组件</h3>
<p>主要职责是在 UI 上呈现数据。从容器组件中获取数据。</p>
<p>这些组件是无状态的，除非它们需要状态来呈现 UI。它们不会更改收到的数据。</p>
<p>一个简单的示例就是 <code>UserList</code>组件，展示所有用户。</p>
<h2 id="whydoweneedthesecomponents">为什么需要这两种组件</h2>
<p>我们可以通过一个示例来理解，假设我们需要展示一份从 <a href="https://jsonplaceholder.typicode.com/">JSON placeholder API</a> 获取的帖子列表。代码如下：</p>
<pre><code class="language-typescript">import { useEffect, useState } from 'react';

interface Post {
  userId: number;
  id: number;
  title: string;
  body: string;
}

/**
 * 我们不应该把逻辑和数据展示结合的例子
 */
export default function DisplayPosts() {
  const [posts, setPosts] = useState&lt;Post[] | null&gt;(null);
  const [isLoading, setIsLoading] = useState&lt;Boolean&gt;(false);
  const [error, setError] = useState&lt;unknown&gt;();

  // 逻辑
  useEffect(() =&gt; {
    (async () =&gt; {
      try {
        setIsLoading(true);
        const resp = await fetch('https://jsonplaceholder.typicode.com/posts');
        const data = await resp.json();
        setPosts(data);
        setIsLoading(false);
      } catch (err) {
        setError(err);
        setIsLoading(false);
      }
    })();
  }, []);

  //展示
  return isLoading ? (
    &lt;span&gt;Loading... &lt;/span&gt;
  ) : posts ? (
    &lt;ul&gt;
      {posts.map((post: Post) =&gt; (
        &lt;li key={`item-${post.id}`}&gt;
          &lt;span&gt;{post.title}&lt;/span&gt;
        &lt;/li&gt;
      ))}
    &lt;/ul&gt;
  ) : (
    &lt;span&gt;{JSON.stringify(error)}&lt;/span&gt;
  );
}
</code></pre>
<p>这个组件做了这些事：</p>
<ul>
<li>它包含三个变量： <code>posts</code>, <code>isLoading</code> 和 <code>error</code>。</li>
<li>使用 <code>useEffect</code> 来处理业务逻辑。 从 API 获取数据： <code>[https://jsonplaceholder.typicode.com/posts](https://jsonplaceholder.typicode.com/posts)</code>，获取方法采用的是 <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch">fetch API</a>。</li>
<li>确保数据获取完毕后，使用 <code>setPosts</code>存储到 <code>posts</code>状态。</li>
<li>确保在不同的场景变换 <code>isLoading</code> 和 <code>error</code> 的值。</li>
<li>将这个逻辑放置在一个异步的 IIFE（立即调用函数）。</li>
<li>最后，我们以无序列表的形式返回帖子并映射我们获取的所有帖子。</li>
</ul>
<p>上面的问题是获取数据和显示数据的逻辑被编码到一个组件中。可以说组件现在与逻辑高耦合。这正是我们不想要的。</p>
<p>以下是我们需要容器和展示组件的原因：</p>
<ul>
<li>创建低耦合的组件</li>
<li>保持关注点分离</li>
<li>代码更易重构</li>
<li>代码更有组织性和可维护性</li>
<li>更易测试</li>
</ul>
<h2 id="presentationandcontainercomponentexample">展示组件和容器组件示例</h2>
<p>好了，讲解部分就到这里——让我们从一个简单的例子开始吧。我们将使用与上面相同的示例——从 JSON placeholder API 获取数据。</p>
<p>先理解文件结构：</p>
<ul>
<li>容器组件为 <code>PostContainer</code></li>
<li>有两个展示组件：
<ul>
<li><code>Posts</code>：展示无序列表</li>
<li><code>SinglePost</code>：呈现单个列表标签的组件。即呈现列表的每个元素。</li>
</ul>
</li>
</ul>
<p>注意：我们将把上述所有组件存储在一个名为 <code>components</code> 的单独文件夹中。</p>
<p>了解文件结构后，让我们从容器组件开始：<code>PostContainer</code>。将下面代码复制到 <code>components/PostContainer.tsx</code> 文件。</p>
<pre><code class="language-tsx">import { useEffect, useState } from 'react';
import { ISinglePost } from '../Definitions';
import Posts from './Posts';

export default function PostContainer() {
  const [posts, setPosts] = useState&lt;ISinglePost[] | null&gt;(null);
  const [isLoading, setIsLoading] = useState&lt;Boolean&gt;(false);
  const [error, setError] = useState&lt;unknown&gt;();

  useEffect(() =&gt; {
    (async () =&gt; {
      try {
        setIsLoading(true);
        const resp = await fetch('https://jsonplaceholder.typicode.com/posts');
        const data = await resp.json();
        setPosts(data.filter((post: ISinglePost) =&gt; post.userId === 1));
        setIsLoading(false);
      } catch (err) {
        setError(err);
        setIsLoading(false);
      }
    })();
  }, []);

  return isLoading ? (
    &lt;span&gt;Loading... &lt;/span&gt;
  ) : posts ? (
    &lt;Posts posts={posts} /&gt;
  ) : (
    &lt;span&gt;{JSON.stringify(error)}&lt;/span&gt;
  );
}
</code></pre>
<p>从本文上一节我们看到的例子来看，上面的代码只是包含了获取数据的逻辑。此逻辑存在于 <code>useEffect</code> 中。容器组件将数据传递给 <code>Posts</code> 展示组件。</p>
<p>让我们看一看 <code>Posts</code> 展示组件。将下面代码复制粘贴到 <code>components/Posts.tsx</code> 文件：</p>
<pre><code class="language-tsx">/**
 * 展示组件
 */

import { ISinglePost } from '../Definitions';
import SinglePost from './SinglePost';

export default function Posts(props: { posts: ISinglePost[] }) {
  return (
    &lt;ul
      style={{
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center'
      }}
    &gt;
      {props.posts.map((post: ISinglePost) =&gt; (
        &lt;SinglePost {...post} /&gt;
      ))}
    &lt;/ul&gt;
  );
}
</code></pre>
<p>如你所见，这是一个简单的文件，包含一个 <code>ul</code> 标签——一个无序列表。该组件映射作为 props 传递的<code>posts</code>。然后传递给 <code>SinglePost</code> 组件。</p>
<p>还有另一个呈现列表标签的展示组件，即 <code>li</code> 标签。它显示帖子的标题和正文。将以下代码复制粘贴到 <code>components/SinglePost.tsx</code> 文件中：</p>
<pre><code class="language-tsx">import { ISinglePost } from '../Definitions';

export default function SinglePost(props: ISinglePost) {
  const { userId, id, title, body } = props;
  return (
    &lt;li key={`item-${userId}-${id}`} style={{ width: 400 }}&gt;
      &lt;h4&gt;
        &lt;strong&gt;{title}&lt;/strong&gt;
      &lt;/h4&gt;
      &lt;span&gt;{body}&lt;/span&gt;
    &lt;/li&gt;
  );
}
</code></pre>
<p>正如你所见，这些展示组件只是在屏幕上显示数据。就这样。其他什么都不做。由于它们用于显示数据，因此会有自己的样式。</p>
<p>我们已经设置好组件，让我们回顾一下做了些什么：</p>
<ul>
<li>在例子中没有违反关注点分离的概念。</li>
<li>为每个组件编写单元测试变得更加容易。</li>
<li>代码的可维护性和可读性要好得多。因此，我们的代码库变得更有条理。</li>
</ul>
<p>我们实现了我们想要的，但是我们利用钩子进一步增强这个模式。</p>
<h2 id="howtoreplacecontainercomponentswithreacthooks">如何通过 React 钩子取代容器组件</h2>
<p>自 <strong>React 16.8.0</strong> 以来，借助函数组件和钩子构建和开发组件变得更加容易。</p>
<p>我们将利用这一能力，用钩子替换容器组件。</p>
<p>将以下代码复制粘贴到 <code>hooks/usePosts.ts</code> 文件中：</p>
<pre><code class="language-tsx">import { useEffect, useState } from 'react';
import { ISinglePost } from '../Definitions';

export default function usePosts() {
  const [posts, setPosts] = useState&lt;ISinglePost[] | null&gt;(null);
  const [isLoading, setIsLoading] = useState&lt;Boolean&gt;(false);
  const [error, setError] = useState&lt;unknown&gt;();

  useEffect(() =&gt; {
    (async () =&gt; {
      try {
        setIsLoading(true);
        const resp = await fetch('https://jsonplaceholder.typicode.com/posts');
        const data = await resp.json();
        setPosts(data.filter((post: ISinglePost) =&gt; post.userId === 1));
        setIsLoading(false);
      } catch (err) {
        setError(err);
        setIsLoading(false);
      }
    })();
  }, []);

  return {
    isLoading,
    posts,
    error
  };
}
</code></pre>
<p>在这里：</p>
<ul>
<li>将 <code>PostContainer</code> 组件的逻辑提取到钩子中。</li>
<li>此钩子将返回一个包含 <code>isLoading</code>、<code>posts</code> 和<code>error</code>的对象。</li>
</ul>
<p>现在我们可以简单地移除容器组件 <code>PostContainer</code>。然后，我们可以直接在 <code>Posts</code> 展示组件中使用这个钩子，而不是将容器的数据作为 <code>prop</code> 传递给展示组件。</p>
<p>对<code>Post</code>组件进行以下编辑：</p>
<pre><code class="language-tsx">/**
 * 展示组件
 */

import { ISinglePost } from '../Definitions';
import usePosts from '../hooks/usePosts';
import SinglePost from './SinglePost';

export default function Posts(props: { posts: ISinglePost[] }) {
  const { isLoading, posts, error } = usePosts();

  return (
    &lt;ul
      style={{
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center'
      }}
    &gt;
      {isLoading ? (
        &lt;span&gt;Loading...&lt;/span&gt;
      ) : posts ? (
        posts.map((post: ISinglePost) =&gt; &lt;SinglePost {...post} /&gt;)
      ) : (
        &lt;span&gt;{JSON.stringify(error)}&lt;/span&gt;
      )}
    &lt;/ul&gt;
  );
}
</code></pre>
<p>通过使用钩子，我们消除了存在于这些展示组件之上的额外组件层。</p>
<p>使用钩子，我们获得了与容器/展示组件模式相同的结果。</p>
<h2 id="summary">总结</h2>
<p>通过这篇文章，我们学习了：</p>
<ul>
<li>关注点分离</li>
<li>容器和展示组件</li>
<li>为什么需要这两种组件</li>
<li>钩子如何取代容器组件</li>
</ul>
<p>如果想要了解更多，我强烈推荐你阅读 <a href="https://tanstack.com/table/v8/">react-table:</a>。这个库使用了大量的钩子都是很好的示例。</p>
<p>你可以在 <a href="https://codesandbox.io/s/container-presentation-pattern-lm1osl?file=/src/components/PostContainer.tsx">codesandbox</a> 找到本文的完整代码。</p>
<p>感谢阅读！</p>
<p>可以在 <a href="https://twitter.com/keurplkar">Twitter</a>、<a href="https://github.com/keyurparalkar">GitHub</a>和 <a href="https://www.linkedin.com/in/keyur-paralkar-494415107/">LinkedIn</a> 上关注我。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何部署和验证JSON Web令牌——JWT教程 ]]>
                </title>
                <description>
                    <![CDATA[ 从开始学习JSON Web令牌（JWT），我就一直很好奇，它是如何被验证的。 我了解我们签名了令牌，并且使用签名后的令牌来验证真实性。但是我还是特别好奇以及为什么我之前没有去了解内部的细节。 希望这篇文章可以帮助你理解签名JWT是如何运作的，你是如何签名和验证令牌的。 什么是JWT？ 在我们正式开始之前，让我们快速回顾一下JSON Web令牌到底是什么。 JSON Web令牌（JWT）是一种在两方之间紧凑的、URL安全的传输数据的方式。 它由开放标准（RFC 7519）定义，并由三个部分组成：header（头部）、payload（负载）以及一个加密部分。 JWT在生成时会被签名，相同的签名JWT在收到时会被验证，以确保它在传输过程中没有被修改。 如果你想要了解JWT的细节，我推荐你阅读我的博文——JSON Web令牌（JWT）以及我们为什么使用它 [https://blog.rohitjmathew.space/json-web-token-jwt-and-why-we-use-them]. 为什么你不需要知道签名和验证是如何工作的？🤔 现在的问题是，为什么大部分的J ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-sign-and-validate-json-web-tokens/</link>
                <guid isPermaLink="false">6325843e5ef0a407fd62fd9b</guid>
                
                    <category>
                        <![CDATA[ json ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ PapayaHUANG ]]>
                </dc:creator>
                <pubDate>Mon, 12 Dec 2022 10:19:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/09/otp-article-header.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/how-to-sign-and-validate-json-web-tokens/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">How to Sign and Validate JSON Web Tokens – JWT Tutorial</a>
      </p><!--kg-card-begin: markdown--><p>从开始学习JSON Web令牌（JWT），我就一直很好奇，它是如何被验证的。</p>
<p>我了解我们签名了令牌，并且使用签名后的令牌来验证真实性。但是我还是特别好奇以及为什么我之前没有去了解内部的细节。</p>
<p>希望这篇文章可以帮助你理解签名JWT是如何运作的，你是如何签名和验证令牌的。</p>
<h2 id="jwt">什么是JWT？</h2>
<p>在我们正式开始之前，让我们快速回顾一下JSON Web令牌到底是什么。</p>
<p>JSON Web令牌（JWT）是一种在两方之间紧凑的、URL安全的传输数据的方式。</p>
<p>它由开放标准（RFC 7519）定义，并由三个部分组成：header（头部）、payload（负载）以及一个加密部分。</p>
<p>JWT在生成时会被签名，相同的签名JWT在收到时会被验证，以确保它在传输过程中没有被修改。</p>
<p>如果你想要了解JWT的细节，我推荐你阅读我的博文——<a href="https://blog.rohitjmathew.space/json-web-token-jwt-and-why-we-use-them">JSON Web令牌（JWT）以及我们为什么使用它</a>.</p>
<h2 id="">为什么你不需要知道签名和验证是如何工作的？🤔</h2>
<p>现在的问题是，为什么大部分的JWT资源都会说“然后你签名并验证”就没有别的信息了？答案是因为这些信息很抽象。</p>
<p>就好比当你在开车的时候，你并不需要知道引擎是如何工作的，或者也不需要靠自己来调整引擎，使汽车运行得更好。</p>
<p>相反，你信任制造商利用他们的专业、并且尽责为你制造了对你有用的产品。</p>
<p>同样的，你也不需要了解签名和验证JWT的流程是什么，就可以高效地使用它来验证和授权你的应用和API。</p>
<p>请注意<strong>你大概率是不需要自己签名和验证令牌的</strong>，但了解背后的原理能够帮助你更有信心地使用JWT。但总的来说，身份供应商和身份（Identity-as-a-Service ）即服务平台（如 Auth0、Okta 和 Microsoft Active Directory）可确保此过程很简单。</p>
<p>如果你仍对它是如何运作的感兴趣（和我一样），欢迎你继续阅读。</p>
<h2 id="jsonweb">JSON Web令牌由什么组成？🤔</h2>
<p>我在<a href="https://blog.rohitjmathew.space/json-web-token-jwt-and-why-we-use-them">这篇教程</a>中做了深入的介绍，但是让我们快速回顾一下。</p>
<p>JSON Web令牌由三段URL安全的字符串并用句号 <code>.</code>连接组成。</p>
<h3 id="jwtheader">JWT的Header（头部）部分</h3>
<p>第一个部分是头部，如下：</p>
<p><code>eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9</code></p>
<p>头部是一个JSON对象，包含了一个签名算法和一个令牌类型。它是由base64Url编码而成。</p>
<p>解码后如下：</p>
<pre><code>{
  "alg": "RS256",
  "typ": "JWT"
}
</code></pre>
<h3 id="jwtpayload">JWT的Payload（负载）部分</h3>
<p>第二部分是负载：</p>
<p><code>eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0</code></p>
<p>这是一个包含数据声明的JSON对象，其中包含有关用户的信息和其他与身份验证相关的信息。</p>
<p>是JWT从一个实体传递到另一个实体的信息。它也是base64Url编码的。数据声明如下所示：</p>
<pre><code>{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true,
  "iat": 1516239022
}
</code></pre>
<h3 id="jwt">JWT的加密/签名部分</h3>
<p>最后一部分是加密/签名部分。JWT被签名之后不能在传输的过程中被修改。一旦一个授权的服务器发行了一个令牌，就使用密匙来签名。</p>
<p>当客户端接收到ID的令牌，也通过密匙来验证签名。</p>
<p>签名算法不同，使用的密钥也会有所不同。如果使用的是非对称签名算法，则使用不同的密钥进行签名和验证。在这种情况下，只有授权服务器能够签名令牌。</p>
<h2 id="jwt">JWT中的签名和验证是如何运作的？🤔</h2>
<h3 id="jwt">如何签名一个JWT？</h3>
<p>在这篇文章中，我将使用RS256签名算法。RS256是使用SHA-256的RSA电子签名。</p>
<p>SHA-256是一种非对称密钥加密算法，它使用一对密钥：一个公钥和一个私钥来加密和解密。</p>
<p>在这里，授权服务器将使用私钥，而接收令牌以验证它的应用程序将使用公钥。</p>
<h4 id="">签名输入</h4>
<p>首先让我来看看JWT的前两个部分（头部和负载），如下：</p>
<p><code>eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0</code></p>
<p>基本上就是一个由base64url编码的头部和负载，并且由 <code>.</code>连接。</p>
<pre><code>base64UrlEncode(header) + "." 
+ base64UrlEncode(payload)
</code></pre>
<p>以上就是签名输入。</p>
<h4 id="">对签名输入做哈希加密</h4>
<p>然后我们使用<a href="https://dev.to/wagslane/how-sha-2-works-step-by-step-sha-256-11ci">SHA-256哈希算法</a>对签名输入进行加密。哈希将一个值转换为另一个不同的值。哈希函数使用数学算法从现有值生成新值。</p>
<p>注意:</p>
<ul>
<li>哈希是不可逆的，一旦我们将输入哈希之后，就无法再次获取原有的输入。</li>
<li>如果输入相同，哈希后的结果始终相同。</li>
<li>不存在两个不同的哈希输入产出相同的结果。</li>
</ul>
<pre><code>SHA-256 (
    base64UrlEncode(header) + "." 
    + base64UrlEncode(payload)
)
</code></pre>
<p>现在我们就拥有了哈希后的头部和负载部分，可以用此和其他的哈希结果比较，但是不能逆转返回到最初的签名输入。</p>
<h4 id="">加密签名输入</h4>
<p>接下来，我们给哈希后的签名输入加密。和哈希不同的是，加密是可逆的。授权服务器使用加密私钥给哈希后的签名加密，产生一个结果。</p>
<p>这个最终结果（哈希过、加密过、头部和负载编码后）就是JWT的加密/签名部分。</p>
<pre><code>RSA (
    SHA-256 (
        base64UrlEncode(header) + "." 
        + base64UrlEncode(payload)
    ),
    {RSA Private Key}
)
</code></pre>
<p>这就是JSON Web令牌产生的过程。</p>
<h3 id="jwt">如何验证JWT？</h3>
<p>现在你知道令牌是如何签名的，我们可以进一步了解当收到令牌后，如何验证这个JWT是没有被篡改的。</p>
<p>假设有一个接受JWT的应用，并且需要验证JWT。这个应用也可以访问授权服务器的公钥。</p>
<p>JWT的验证是为了达到一个目的：即我们可以有效地将我们收到的与我们期望的进行比较。</p>
<h4 id="">解码声明</h4>
<p>应用可以对头部和负载解码来获取信息。</p>
<p>请记住，这两个段是用base64Url编码，以使它们是URL安全的。这并不是密码学维度的安全。</p>
<p>你可以使用简单的在线base64解码工具来解码。一旦被解码，我们就可以轻松地读取其中的信息。</p>
<p>例如，我们可以解码头部，看看JWT说它是用什么算法签名的。</p>
<p>解码后的头部如下：</p>
<pre><code>{
  "alg": "RS256",
  "typ": "JWT"
}
</code></pre>
<p>当我们读取JWT头部的算法后，我们应该验证它是否和我们期待的配置匹配，如果不匹配，就马上拒绝这个令牌。</p>
<h4 id="">哈希加密（再次）</h4>
<p>如果令牌中的算法符合我们的期望（即使用RS256算法），我们需要生成头部和负载的SHA-256哈希。</p>
<p>请记住，哈希是不可逆的，但相同的输入总是会产生相同的输出。所以我们将哈希连接在一起的、由base64Url编码的头部和负载。现在我们在应用程序端重新哈希计算签名输入。</p>
<h4 id="">解密</h4>
<p>哈希签名输入也在JWT的签名中，但它已由授权服务器使用私钥加密。应用程序可以访问公钥，因此我们可以解密签名。</p>
<p>完成此操作后，我们就可以访问原始哈希：第一次生成令牌时由授权服务器生成的哈希。</p>
<h4 id="">对比哈希值</h4>
<p>现在我们可以将解密的哈希与计算的哈希进行比较。如果它们相同，那么我们验证JWT头部和负载段中的数据在授权服务器创建令牌到应用程序收到它的之间没有被修改。</p>
<h4 id="">验证令牌声明</h4>
<p>此外，一旦我们验证了签名，我们就可以验证JSON Web令牌的数据。也可以验证负载段中的声明，因为它包含有关令牌颁发者、令牌到期时间、令牌的目标受众、令牌绑定到授权请求的信息等。</p>
<p>这些声明为应用程序提供了签名验证以外的详细信息。</p>
<p>例如，对声明的检查可以揭示技术上有效的令牌实际上是为不同的应用程序或用户准备的、它已经过期、它来自与该应用程序无关的发行者等等。</p>
<h2 id="">总结</h2>
<p>我们已经介绍完毕JWT是如何签名和验证的，希望可以帮助你更好地理解JWT并且使用它。我想再重复一次，<strong>你不需要自己来签名和验证JWT</strong>。</p>
<p>有<a href="https://auth0.com/">Auth0</a>、 <a href="https://www.okta.com/">Okta</a>、<a href="https://www.pingidentity.com/en.html">Ping Identity</a>等身份平台帮助你完成。他们也提供应用端和API端的SDK、验证库和令牌管理系统。</p>
<p>如果你对使用Auth0感兴趣，你需要注册一个账号。你可以<a href="https://a0.to/signup-for-auth0">免费在这里注册</a>。</p>
<p>感谢阅读！希望你觉得这篇文章对你有帮助，我也很感兴趣收听你的想法，回答你的问题。如果你觉得本文对你有帮助，请你转发给其他人。</p>
<p>感谢阅读！:)</p>
<p>P.S 欢迎在<a href="https://www.linkedin.com/in/rohitjmathew/">LinkedIn</a>或<a href="https://twitter.com/iamrohitjmathew">Twitter</a>联系我。</p>
<h2 id="">附录</h2>
<p>以下是撰写这篇文章我参考的资料：</p>
<ul>
<li><a href="https://dev.to/kimmaida/signing-and-validating-json-web-tokens-jwt-for-everyone-25fb">Signing and Validating JSON Web Tokens (JWT) For Everyone</a> 作者：<a href="https://twitter.com/KimMaida">Kim Maida</a></li>
<li><a href="https://auth0.com/blog/json-web-token-signing-algorithms-overview/">JSON Web Token (JWT) Signing Algorithms Overview</a> 来自Auth0</li>
</ul>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 浏览器渲染网页 ]]>
                </title>
                <description>
                    <![CDATA[ 今天，世界各地的计算机和网络通讯速度都变得越来越快。这有利于提升 Web 开发和用户体验。人们通过互联网能够实现的可能性也向前迈了一大步。 显著进步的另一面也意味着有一些人掉队了。在这个数字分化的年代，如何提高网络体验，以触达到更多网络和设备条件没有那么好的用户，是一个难题。 要解决这个难题，必须先理解浏览器上渲染网页的方法。 本文涉及的术语 在开始之前，我需要确保你熟悉本文涉及的术语。其中一些对于新开发者来说可能难以理解。如果你对这部分已经很了解了，可以跳过。  * 服务器：服务器是位于远程（互联网意义上的）电脑，它的职责就是处理客户端发来的请求并且做出响应。  * 客户端：客户端是用于和服务器通信的设备，以此来访问资源。在绝大多数情况下，客户端是可以访问网络的设备。在本文中，网络浏览器充当了客户端的角色。  * CDN：Content Delivery Network（内容分发网络）的首字母缩写。CDN    是“一个互连服务器网络，可加快数据密集型应用程序的网页加载速度”（引用自 AWS    [https://aws.amazon.com/what-is/cdn/]）。 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/web-page-rendering-on-the-browser-different-methods/</link>
                <guid isPermaLink="false">6388671d832e3f078176398d</guid>
                
                    <category>
                        <![CDATA[ Web开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ PapayaHUANG ]]>
                </dc:creator>
                <pubDate>Thu, 01 Dec 2022 02:30:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/12/fav-poster.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/web-page-rendering-on-the-browser-different-methods/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">How Web Pages Get Rendered on the Browser – Different Methods Explained</a>
      </p><!--kg-card-begin: markdown--><p>今天，世界各地的计算机和网络通讯速度都变得越来越快。这有利于提升 Web 开发和用户体验。人们通过互联网能够实现的可能性也向前迈了一大步。</p>
<p>显著进步的另一面也意味着有一些人掉队了。在这个数字分化的年代，如何提高网络体验，以触达到更多网络和设备条件没有那么好的用户，是一个难题。</p>
<p>要解决这个难题，必须先理解浏览器上渲染网页的方法。</p>
<h2 id="">本文涉及的术语</h2>
<p>在开始之前，我需要确保你熟悉本文涉及的术语。其中一些对于新开发者来说可能难以理解。如果你对这部分已经很了解了，可以跳过。</p>
<ul>
<li><strong>服务器</strong>：服务器是位于远程（互联网意义上的）电脑，它的职责就是处理客户端发来的请求并且做出响应。</li>
<li><strong>客户端</strong>：客户端是用于和服务器通信的设备，以此来访问资源。在绝大多数情况下，客户端是可以访问网络的设备。在本文中，网络浏览器充当了客户端的角色。</li>
<li><strong>CDN</strong>：Content Delivery Network（内容分发网络）的首字母缩写。CDN 是“一个互连服务器网络，可加快数据密集型应用程序的网页加载速度”（引用自 <a href="https://aws.amazon.com/what-is/cdn/">AWS</a>）。</li>
<li><strong>构建时</strong>：在构建时，应用程序代码为另一个环境做准备。这里的另一个环境大多数时候指的是互联网上的托管环境。</li>
</ul>
<p>现在让我们学习渲染网站的不同方式。</p>
<h2 id="csr">什么是客户端渲染（CSR）？</h2>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/csr.jpg" alt="csr" width="600" height="400" loading="lazy"></p>
<p>客户端渲染完全依赖 JavaScript 代码在浏览器中生成网页。在页面内容加载完毕，供用户浏览之前，浏览器预先处理 JS 代码。</p>
<p>JavaScript 在下载时就动态辅助定义网站<strong>架构</strong>。这里的架构指的是从 <a href="https://aws.amazon.com/what-is/restful-api/">API</a> 获取数据、 网站导航和一些网站内简单的业务逻辑。</p>
<h3 id="javascript">客户端渲染和 JavaScript 框架</h3>
<p>客户端渲染因为一些 JavaScript 框架或者库，如：React、 Vue 和 Angular 的发布而变得越来越流行。仅当在 HTML 页面的头部（head）引入一个 CDN，这些架构才生效——通常这些 CDN 包含大量的 JS 代码。</p>
<p>毫无疑问大文件意味着更久的下载时间，同时，在首次加载就下载好大文件意味着之后访问网站其他页面的加载时间会大幅下降。</p>
<p>网站首先从 API 获取数据，然后数据被用于填充在客户端渲染的页面。</p>
<p>许多我们现实生活中的渐进式应用（PWA）就是使用 CSR 的示例，如：Spotify、Figma 和 Google Drive。</p>
<h2 id="ssr">什么是服务端渲染（SSR）？</h2>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/ssr.jpg" alt="ssr" width="600" height="400" loading="lazy"></p>
<p>客户端渲染改变了游戏规则，并且这种改变仍在持续。但是，如果细看 CSR 的性能，会发现想要网站具备更多的功能，就需要更多的 JS 代码。也就是上文提到的越多的 JS 代码意味着更久的下载时间。</p>
<p>牺牲首次加载时间来换取网页的快速访问，对于一些人来并不划算。服务器端渲染就应运而生。</p>
<p>SSR 并未解决网页渲染的所有问题，但确实解决了使用 CSR 会面临的问题，如首次加载的时间，更多优势会在后文提到。</p>
<p>服务器端在接收到浏览器的请求后就在服务器渲染生成网页。服务器通过 SSR 渲染整个 HTML、CSS 和资源需求的必要 JavaScript，并且返回给浏览器。</p>
<p>也就意味着网站内容始终是来自服务器的最新的信息。你可以把这个类比为 <a href="https://aws.amazon.com/what-is/restful-api/">REST API</a>——后端内容实时更新。</p>
<p>和其他渲染手段一样，SSR 也有一些弊端。其中一个就是因为需要通过向服务发送请求才能加载网页，所以网络带宽小的用户会收到影响。同时，SSR 也对计算机的算力有要求。</p>
<h2 id="ssg">什么是静态网站生成（SSG）？</h2>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/ssg.jpg" alt="ssg" width="600" height="400" loading="lazy"></p>
<p>静态网站生成是渲染网页一个常用的手段，在 JavaScript 框架诞生之前，大部分的网站都是静态生成的。</p>
<p>静态网站很受欢迎，但是有更好的生成它们的方式。这主要取决于网站是否看重性能。</p>
<h3 id="">但是，静态网站是什么？</h3>
<p>静态网站的渲染和生成是一致的，也就是说静态网站的内容基本上不会被浏览的用户影响，不像使用 CSR 或者 SSR 渲染的 web 应用，用户看到的内容取决于认证和授权。</p>
<p>静态网站是展现不会改变，或者只需要定期更新内容的理想手段。</p>
<h3 id="">静态网站生成详解</h3>
<p>静态网页生成主要设计到自动化构建网页的过程。今天的 JavaScript 框架（如 Nuxt.js、Next.js 等）提供模版引擎，可使用一个模板来构建多个静态页面，可以想像这样可以解决时间。</p>
<p>在 SSR 和 CSR 中，HTML 网页是在构建时须髯安和生成，但是静态网站生成截然不同。在你尝试访问网页之间，SSG 就生成了网站。这就是为什么 SSG 被认为是预渲染，苦差事都在渲染前做完了。</p>
<p>看上去 SSG 可以春风化雨，但使用 SSG 渲染页面有一个大缺点就是必须为网站的每一个可以访问的 URL 生成一个页面，如果你使用<a href="https://nuxtjs.org/docs/directory-structure/pages#dynamic-pages">动态页面</a>，就会更复杂。</p>
<p>这就回到上文提到的，静态网站的理想使用场景是展现不怎么更新的内容，也就是静态网站并不是万金油。</p>
<h2 id="">不同渲染方式优势和折衷</h2>
<p>在分别了解这些渲染方式之后，让我们把知识整合到一起做一下对比。</p>
<p>我们将看三个指标——性能、SEO 和花销。</p>
<h3 id="">性能</h3>
<p>搭建可以兼容不同网络和电脑速度的网站，需要考虑到网站的性能。这里的性能指的是网站的加载速度和从 API 获取数据的速度。</p>
<p>接下来将分析 CSR、SSR 和 SSG 在这方面的表现</p>
<h4 id="">客户端渲染性能</h4>
<p>客户端渲染网站相对较慢。因为一开始需要加载 JS 代码，才能生成用户可以看到的内容。</p>
<p>通常情况下，JS 下载都很慢，特别是使用 JS 框架的时候。客户端渲染网页也调用 API 以从后端获取数据，这样也会增加用户的加载时间。</p>
<h4 id="">服务器端渲染性能</h4>
<p>SSR 渲染网页可以非常快。这主要取决于服务器的速度以及用户电脑的速度，如果两者都达到预期，SSR 在性能方面可以轻松取胜。</p>
<h4 id="">静态网站生成性能</h4>
<p>使用 SSG 生成页面相对较快，因为实际渲染并没有在浏览器发生。</p>
<p>SSG 不需要额外的工作就可以向浏览器提供内容。和 CSR 一样，SSG 可能也需要调用 API 来获取后端数据，这样也会增加用户的加载时间。</p>
<p>最终性能是由网页中使用的 JavaScript 的数量决定的。</p>
<h3 id="seo">搜索引擎优化（SEO）</h3>
<p>页面如果需要曝光的话，就应当重视搜索引擎优化。SEO 决定了内容在如 Google 这样的搜索引擎上的可访问性，同时也决定了网站在搜索引擎结果页面（SERP）上的排名。</p>
<p>让我们看看当这些网站被搜索引擎索引的时候，它们的 SEO 表现如何。</p>
<h4 id="csr">CSR 搜索引擎优化</h4>
<p>使用 CSR 渲染的页面并没有语义化的内容，内容主要靠 JS 生成。这样的弊端是不是所有网络爬虫都支持 JS，因此你的网站可能不能被搜索引擎正确索引。</p>
<h4 id="ssr">SSR 搜索引擎优化</h4>
<p>SSR 渲染通过由服务器发出的最新的内容生成页面，由 SS 渲染的页面可以被爬虫识别也可以被搜索引擎索引。</p>
<h4 id="ssg">SSG 搜索引擎优化</h4>
<p>由 SSG 生成的页面极易被网络爬虫爬取，因为它不完全依赖 JS 渲染。</p>
<h3 id="">花销</h3>
<p>给用户提供最佳的体验很重要，但账单不会自我消化，所以由体验带来的花销约精简越好。</p>
<p>三种渲染手段的经济花销并不相同。让我们仔细分析一下：</p>
<h4 id="csr">CSR 花销</h4>
<p>客户端渲染 100% 在浏览器运行，也就意味着没有额外产生花销。</p>
<h4 id="ssr">SSR 花销</h4>
<p>客户端渲染由远程的服务器生成功能完整的页面，所以为生成额外的计算资源和花销。</p>
<h4 id="ssg">SSG 花销</h4>
<p>没有花销。静态网站生成在构建时完成。因此，生成的网页被托管，不需要服务器额外的渲染。</p>
<h2 id="">总结</h2>
<p>当选择渲染方式的时候，请考虑好你的用例，可以根据本文所学来做判断。不同的渲染方式适用于不同的网站。</p>
<p>电子商务网站的开发者可能会选择 SSR 路线，或者选择更安全的静态网站。同时，web 应用的开发可能不在乎首次加载的时长，只要长远看的用户体验够好。</p>
<p>不论你选用哪种渲染方式，都要确保网站的可访问性——跳出自身经历。最后，千万别忘了 JS 代码的精炼。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 在浏览器点击 URL 之后发生了什么？ ]]>
                </title>
                <description>
                    <![CDATA[ 在这篇文章中，我希望给读者建立一个网络世界的基本印象。我之前的文章都是互联网相关更高阶的内容，如：Angular 之旅 [https://www.freecodecamp.org/news/angular-a-journey-into-one-of-the-most-popular-front-end-tools-in-todays-job-market/] 、React 基础 [https://www.freecodecamp.org/news/start-your-journey-into-the-world-of-react-by-learning-these-basics-d6e05d3655e3/] 等。但在今天这篇文章中，我想带领各位读者一起领略点击 URL 之后的风光。 正如文章标题所示 - 当你点击 URL 之后会发生什么？——让我们开始吧！ 在探讨点击 URL 之后，我们得先了解 URL 是什么，它的各个组成部分，对不对？那么别浪费时间了，让我们开始了解 URL 吧！ URL – 统一资源定位符（Uniform Resource Locator） 正如 URL  ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/what-happens-when-you-hit-url-in-your-browser/</link>
                <guid isPermaLink="false">6384c576832e3f07817636d0</guid>
                
                    <category>
                        <![CDATA[ DNS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 浏览器 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ PapayaHUANG ]]>
                </dc:creator>
                <pubDate>Mon, 28 Nov 2022 04:18:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/11/browser.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/what-happens-when-you-hit-url-in-your-browser/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">What happens when you click on a URL in your browser</a>
      </p><!--kg-card-begin: markdown--><p>在这篇文章中，我希望给读者建立一个网络世界的基本印象。我之前的文章都是互联网相关更高阶的内容，如：<a href="https://www.freecodecamp.org/news/angular-a-journey-into-one-of-the-most-popular-front-end-tools-in-todays-job-market/">Angular 之旅</a>、<a href="https://www.freecodecamp.org/news/start-your-journey-into-the-world-of-react-by-learning-these-basics-d6e05d3655e3/">React 基础</a>等。但在今天这篇文章中，我想带领各位读者一起领略点击 URL 之后的风光。</p>
<p>正如文章标题所示 - <strong>当你点击 URL 之后会发生什么？</strong>——让我们开始吧！</p>
<p>在探讨<strong>点击 URL 之后</strong>，我们得先了解 URL 是什么，它的各个组成部分，对不对？那么别浪费时间了，让我们开始了解 URL 吧！</p>
<h2 id="urluniformresourcelocator">URL – 统一资源定位符（Uniform Resource Locator）</h2>
<p>正如 URL 的全称所述：它表示了我们想要访问的资源的地址。它相当于一个物理世界的<strong>地址</strong>，而在这个地址有你想要交互或者搜寻的信息。</p>
<p>在你的日常生活中，如果你出于工作或者讯息需求想要拜访一位朋友，你首先需要他/她的住址。在网络世界也是如此，你想要访问的网站必须有一个地址。<strong>网站就相当于朋友的房子，而 URL 就是房子的地址</strong>。</p>
<h3 id="url">剖析 URL</h3>
<p>现在我们已经清楚了 URL 是什么，但还不了解它的组成部分，让我开始吧！</p>
<p>看看这个例子：</p>
<p><a href="https://www.example.com/page1">https://www.example.com/page1</a></p>
<p>在示例中，第一个部分是 <strong>‘https’</strong>，它告诉浏览器应该使用哪种协议。协议可以是 <strong>http、https、ftp</strong> 等。<strong>协议</strong>是浏览器用于网络通信的<strong>一组规则</strong>。<strong>'https'</strong> 是安全版本，信息以一种安全的方式交换。</p>
<p>第二部分 <strong>www.example.com</strong> 是<strong>域名</strong>。你可以把它当作你朋友的房子。域名是网站的地址。我们使用域名来访问服务器（训练后的电脑），服务器的任务是给网站提供信息。稍等！我刚刚才说过 URL 是地址，现在又说域名也是地址。可别搞混了！</p>
<h3 id="url">URL 和域名之间的区别</h3>
<p>两者之间最大的不同是 <strong>URL 是完整的地址</strong>。 URL 提供了信息交流的方法以及登录网站之后的路径。而<strong>域名是 URL 的一部分</strong>。</p>
<p>让我们通过之前的示例来了解。你可以认为你朋友的住址是域名，同时 URL 不仅告知了朋友房子的住址（域名），以及你们交流的方式，如去到一个单独的房间（安全）或者当着所有人的面（信息可能会泄漏）。它也告知了路径，即到达房子之后，你应该去哪个房间。因此，域名是 URL 的一部分。一个包含域名和更多信息的地址才是 URL。</p>
<p>希望现在你更清楚 URL 是什么了，让我们进入下一个部分。</p>
<h2 id="">域名</h2>
<p>上文我介绍了域名，但未做深入讨论。现在就来看看，域名是网站的地址。它是浩瀚网络世界中<strong>网站</strong>的<strong>唯一标识</strong>。不能有两个相同的域名，但是 - 对！还有一个“但是”。 域名之间不仅仅是通过这个来区别的，还有其他的内容。</p>
<p>当点击 URL，也就是域名之后，网站被打开，内容呈现出来。每一台计算机都有 IP 地址，用来在互联网通信。正如它的名字“IP 地址”所述，它就是一个地址。当我们<strong>点击</strong>任意<strong>URL</strong>时，我们实际上是在 <strong>点击</strong> 负责提供网站内容（托管）的电脑的 <strong>IP 地址</strong>。</p>
<p>现在你可能会觉得，什么鬼！所有东西都是地址？有 IP 地址的话，还要域名干嘛？为什么不直接用 IP 地址获取内容？</p>
<p>是的！你可以<strong>使用 IP 地址</strong>来<strong>获取内容</strong>，但你真的记得住所有网站对应的 IP 地址吗？显然不行！记住每一个网站<strong>的 IP 地址</strong>可<strong>太难了</strong>，域名才应运而生。</p>
<p>你可以类比通讯录。你记不住每一个人的手机号码，但是你可以记住他们的名字。域名和 IP 地址的关系也同样。你<strong>可记不住</strong>这些吓死人的 <strong>IP 地址</strong>，但是你可以轻松<strong>记住域名</strong>。</p>
<p>大量的数据由数据库维护，并且存储在域名下这个 IP 地址内。存储域名和对应 IP 地址的系统被称为 <strong>DNS（域名系统）</strong> 。（我相信你肯定听说过）</p>
<p>基础介绍得差不多了，让我们进入点击 URL 之后的步骤。</p>
<h2 id="dnslookupip">DNS lookup 查找 IP 地址</h2>
<p>访问 URL 之后，首先要做的是解析与域名相关联的 IP 地址。DNS 辅助这个工作。<strong>DNS 就像电话簿一样，给我们提供与域名关联的 IP 地址</strong>，就如同我电话簿提供和人名相关的手机号码一样。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/06/dns.png" alt="dns" width="600" height="400" loading="lazy"></p>
<p>这是概述，但此域名查询通过<strong>四层</strong>，分以下几个步骤 ：</p>
<p>1. 点击 URL 之后，将检查<strong>浏览器缓存</strong>。因为浏览器会在一段时间内存储你访问过的网站的 DNS 记录，DNS 查询会先查缓存里的与域名关联的 IP 地址。</p>
<p>2. DNS 查询检查的第二个位置是<strong>系统缓存</strong>，其次是<strong>路由缓存</strong>。</p>
<p>3. 如果在上述步骤中，DNS 查询未得到解析，则需要解析服务器的帮助。解析器服务器只不过是 ISP（Internet 服务提供商）。查询被发送到 ISP，DNS 查询在 <strong>ISP 缓存</strong>中运行。</p>
<p>4. 如果在第 3 步中也没有找到结果，则将请求发送到 DNS 层级结构的<strong>顶层或者根服务器</strong>。从来没有发生过在这个阶段没有找到结果的情况，实际上，这部分告诉你可以从那儿找到信息。如果你正在搜索顶级域（.com、.net、.Gov、.org）的 IP 地址，就是告诉解析器服务器搜索 <strong>TLD 服务器</strong>（顶级域）。</p>
<p>5. 现在，解析器要求 TLD 服务器提供域名的 IP 地址。 TLD 存储域名的地址信息。它告诉解析器向<strong>权威性域名服务器</strong>询问。</p>
<p>6. 权威性域名服务器负责了解域名的所有信息。最终，解析器（ISP）获取与域名相关联的 IP 地址，并将其发送回浏览器。</p>
<p>获取 IP 地址后，解析器将其存储在缓存中，这样下次如果出现相同的查询，就不必再次执行所有这些步骤。它可以从缓存得到 IP 地址。</p>
<p>这就是解析与域名关联的 IP 地址所遵循的所有步骤。可以结合下图理解：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/06/dns_resolve.png" alt="dns_resolve" width="600" height="400" loading="lazy"></p>
<h2 id="tcp">由浏览器发起的与服务器的 TCP 连接</h2>
<p>一旦计算机的<strong>IP 地址</strong>（网站信息存储的地方）被<strong>发现</strong>，就会<strong>启动连接</strong>。网络通信遵循<strong>互联网协议</strong>。<strong>TCP/IP</strong> 就是最常见的协议。浏览器和服务器之间的通讯遵循 <strong>‘TCP 三次握手’</strong>。让我们简短地了解一下这个过程：</p>
<p>1. 客户端计算机发送一个<strong>SYN 消息</strong>，确认另一台计算机是否可以建立连接。</p>
<p>2. 之后来到<strong>另一台计算机</strong>，如果可以建立新的连接，会同时发送 SYN 消息和 <strong>确认消息</strong>。</p>
<p>3. 在此之后，<strong>第一台计算机</strong>接收到它的消息，并通过<strong>发送 ACK 消息</strong>进行确认。</p>
<p>可以通过下图理解：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/06/process.png" alt="process" width="600" height="400" loading="lazy"></p>
<h2 id="">通信开始（请求响应过程）</h2>
<p>最终客户端和服务器之间的连接建立，双方可以相互交流信息。连接成功之后，<strong>浏览器(客户端)<strong>向</strong>服务器</strong>发送<strong>请求</strong>，告诉服务器需要什么内容。服务器知道应该对每一种请求做何种反应。因此<strong>服务器响应客户端</strong>，响应包含了请求的所有内容，包括网页、状态码、缓存控制等。然后浏览器渲染请求内容。</p>
<p>就这么多，当你点击 URL 之后，上面的所有步骤就运转起来，当然这些步骤可能在一秒之内就完成了。这就回答了“当我们点击 URL 之后，发生了什么？”这个问题。</p>
<p>感谢你阅读本文！</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 写给 React 开发者的 TypeScript 指南 ]]>
                </title>
                <description>
                    <![CDATA[ 如果你已经使用 React 一段时间，你会察觉 JavaScript 一些自由野性的天性让你难以驾驭（这当然不是 JS 的问题 😄），当你和团队协作的时候，这个特点尤为显著。 或许你不知道，你需要 TypeScript，至少试一下。 我先声明一下，我喜欢 JavaScript 的自由，甚至有相当长的时间，我“反对”使用 TypeScript。 我想和你一起探索 TypeScript 是否值得使用，还只是适合那些不怎么会写代码的人（这是我们团队内部玩笑）。 本文旨在介绍 TS 的基础以便你了解它的优势，决定是否使用它。本文的第二部分会介绍在 React 中的 TS。 目录  * 参考资料  * 为什么使用 ESLint、Prettier 和 Husky  * 何为 TypeScript  * 为什么要使用 TS  * 如何设置 TypeScript  * 购物清单项目示例 * TypeScript ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/typescript-for-react-developers/</link>
                <guid isPermaLink="false">6381ebef206aea0762969c1a</guid>
                
                    <category>
                        <![CDATA[ TypeScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ PapayaHUANG ]]>
                </dc:creator>
                <pubDate>Fri, 25 Nov 2022 10:19:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/11/typescript-cover.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/typescript-for-react-developers/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">TypeScript for React Developers – Why TypeScript is Useful and How it Works</a>
      </p><!--kg-card-begin: markdown--><p>如果你已经使用 React 一段时间，你会察觉 JavaScript 一些自由野性的天性让你难以驾驭（这当然不是 JS 的问题 😄），当你和团队协作的时候，这个特点尤为显著。</p>
<p><strong>或许你不知道，你需要 TypeScript，至少试一下。</strong></p>
<p>我先声明一下，我喜欢 JavaScript 的自由，甚至有相当长的时间，我“反对”使用 TypeScript。</p>
<p>我想和你一起探索 TypeScript 是否值得使用，还只是适合那些不怎么会写代码的人（这是我们团队内部玩笑）。</p>
<p>本文旨在介绍 TS 的基础以便你了解它的优势，决定是否使用它。本文的第二部分会介绍在 React 中的 TS。</p>
<h2 id="">目录</h2>
<ul>
<li><a href="#resources">参考资料</a></li>
<li><a href="#whyuseeslintprettierandhusky">为什么使用 ESLint、Prettier 和 Husky</a></li>
<li><a href="#whatistypescript">何为 TypeScript</a></li>
<li><a href="#whybotherdealingwithts">为什么要使用 TS</a></li>
<li><a href="#howtosetuptypescript">如何设置 TypeScript</a></li>
<li><a href="#sampleshoppinglistproject">购物清单项目示例</a>
<ul>
<li><a href="#typescriptmodules">TypeScript 模块</a></li>
<li><a href="#typescripttypes">TypeScript 类型</a>
<ul>
<li><a href="#inferenceintypescript">TypeScript 中的类型推论</a></li>
<li><a href="#anyandunknownintypescript">TypeScript 中的<code>any</code> 和 <code>unknown</code></a></li>
<li><a href="#arraysintypescript">TypeScript 中的数组</a></li>
<li><a href="#objectsintypescript">TypeScript 中的对象</a></li>
<li><a href="#aliasesintypescript">TypeScript 中的类型别名</a></li>
</ul>
</li>
<li><a href="#functionsintypescript">TypeScript 中的函数</a>
<ul>
<li><a href="#optionalparametersintypescript">TypeScript 中的可选参数</a></li>
</ul>
</li>
<li><a href="#typescriptenums">TypeScript 枚举</a></li>
<li><a href="#typescriptgenerics">TypeScript 泛型</a></li>
<li><a href="#tuplesintypescript">TypeScript 中的元组</a></li>
<li><a href="#classesintypescript">TypeScript 中的类</a></li>
<li><a href="#interfacesintypescript">TypeScript 中的接口</a></li>
<li><a href="#dommanipulationintypescript">TypeScript 中的 DOM 操作</a></li>
</ul>
</li>
<li><a href="#howtocombinereacttypescript">如何结合 React + TypeScript</a>
<ul>
<li><a href="#setup">设置</a></li>
<li><a href="#typingcomponentprops">设置组件 Props 类型</a>
<ul>
<li><a href="#reactbuiltintypes">React 内置类型</a></li>
<li><a href="#returntypeofareactcomponent">React 组件返回类型</a></li>
<li><a href="#combinationswithtemplateliterals">结合模板字面量</a></li>
<li><a href="#howtouseexclude">如何使用<code>Exclude</code></a></li>
<li><a href="#customhtmlcomponents">自定义 HTML 组件</a></li>
</ul>
</li>
<li><a href="#typinghooks">定义 hook 的类型</a>
<ul>
<li><a href="#usestatehook">useState hook</a></li>
<li><a href="#usereducerhook">useReducer hook</a></li>
<li><a href="#usecontext">useContext</a></li>
<li><a href="#userefhook">useRef hook</a></li>
</ul>
</li>
<li><a href="#forwardingref">传递 ref</a></li>
<li><a href="#howtousetypescriptgenericsinreact">如何在 React 中使用 TypeScript 泛型</a></li>
<li><a href="#typingacustomusefetchhook">定义自定义 useFetch Hook 类型</a></li>
</ul>
</li>
<li><a href="#conclusion">总结</a></li>
</ul>
<h2 id="resources">参考资料</h2>
<p>你可以从以下样板着手：</p>
<ul>
<li><a href="https://github.com/dastasoft/react-boilerplate/tree/cra-typescript">Create React App + TypeScript + ESLint + Prettier Boilerplate</a></li>
<li><a href="https://github.com/dastasoft/react-boilerplate/tree/vite-typescript">Vite + TypeScript + ESLint + Prettier Boilerplate</a></li>
</ul>
<p>如果你喜欢游戏编程，可以尝试 <a href="https://phaser.io/">PhaserJS</a>。你可以在浏览器通过创建游戏边玩边学 TypeScript 。</p>
<p>确保你也阅读了 <a href="https://www.typescriptlang.org/docs/handbook/intro.html">TS 官方文档</a>。里面包含大量有用的文档和案例。</p>
<p>另外还有两个示例项目，这样你就可以看到代码是如何实现的：</p>
<h3 id="">购物清单项目</h3>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/shopping-list.jpg" alt="shopping-list" width="600" height="400" loading="lazy"></p>
<p>这是一个简单的体验 TypeScript 开发的项目，不需要 Webpack、React 以及任何其他组件，仅需要把 TypeScript 转换成 JavaScript。</p>
<ul>
<li><a href="https://shopping-list.dastasoft.com/">在线示例</a></li>
<li><a href="https://github.com/dastasoft/shopping-list">源码</a></li>
</ul>
<h3 id="">动漫预告片项目</h3>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/animetrailers-screenshot.jpg" alt="animetrailers-screenshot" width="600" height="400" loading="lazy"></p>
<p>借助 <a href="https://jikan.moe/">JikanAPI</a>我搭建了一个简单的结合 React 和 TypeScript 的应用，该应用提供一系列动画和基本信息，你可以观看你最喜欢的动画的最新预告。</p>
<ul>
<li><a href="https://animetrailers.dastasoft.com/">在线示例</a></li>
<li><a href="https://github.com/dastasoft/animetrailers">源码</a></li>
</ul>
<h2 id="whyuseeslintprettierandhusky">为什么使用 ESLint、Prettier 和 Husky</h2>
<p>在样板中我使用了 Airbnb 的 ESlint 规则、Prettier 建议规则以及 Husky 的提前提交（pre-commit)行为。团队协作的时候，这样可以促使大家遵循同样的代码规则，即便你是单人作业或者学习开发，这样操作也会对你的项目有所助益。</p>
<p>有些 Airbnb 的规则可能会有些奇怪，但是规则都有注解和示例，你可以以此来决定采不采用，如果想要关闭某个规则，可以放在<code>.eslintrc</code>文件中。</p>
<p>这些规则对入门开发和刚刚开始使用 JS 或者 TS 的人来说非常有用。所以我建议你将它们纳入你的项目，尝试一下。 😉</p>
<h2 id="whatistypescript">何为 TypeScript</h2>
<p><a href="https://www.typescriptlang.org/">TypeScript</a>或者 TS 是由微软开发并且维护的开源语言，它具有以下特性：</p>
<ul>
<li>它是一个多范式语言（和 JavaScript 一样）；</li>
<li>它是 JavaScript 的一个替代品（更准确地说，是一个超集）；</li>
<li>它允许静态类型；</li>
<li>它具有额外的特性: 泛型（generics)、接口（interfaces）、元组（tuples）等，将在下文详细说明；</li>
<li>它允许阶段性采用（也就是你可以一个文件一个文件地将现有的项目改写成 TS，而不是一次性改变）；</li>
<li>你可以在前端和后端中使用（和 JS 一样）。</li>
</ul>
<p>浏览器不能解读 TS 代码，必须 <em>转译</em> 为 JS。JS 为动态类型映射值，而 TS 是静态类型，所以不易出错。</p>
<p>React 中已经是通过 <a href="https://babeljs.io/">Babel</a> 转译 JS 了，所以转译代码并不是 TS 额外的优势。</p>
<h2 id="whybotherdealingwithts">为什么要使用 TS</h2>
<p>问题就在这儿： 为什么要使用 TS，JS 不好用吗？你用得不开心吗？你不怕麻烦吗？正如上文所述，过去我们团队内部会取笑像 TS 这样带类型的语言（当时我还在使用 Java）。我们团队会说如果你需要类型，证明你不会正确地写代码。</p>
<p>TypeScript、Java 以及其他一些语言具备<strong>静态类型</strong>，也就是会定义变量的类型。一旦你将变量定义为 <em>string</em> 或者 <em>boolean</em> ，你就不能改变它的类型。</p>
<p>而 JavaScript 拥有<strong>动态类型</strong>。也就是说，变量一开始是字符串，之后可以变为布尔值、数字或者任意你想要的值。变量类型会在运行时动态分配。</p>
<p>当你浏览网络上的 TS 代码，你会看到……（语法糖）。</p>
<p><img src="https://blog.dastasoft.com/_next/image?url=%2Fassets%2Fposts%2Fcontent%2Ftypescript%2Fsyntaxsugar.jpeg&amp;w=1920&amp;q=75" alt="sintactic sugar" title="Syntactic Sugar, syntactic sugar everywhere." width="600" height="400" loading="lazy"></p>
<p>回到我们团队的玩笑，当然这个说法<strong>没错</strong>：如果你知道自己在做什么，你不需要别人不断提醒你这是字符串，也只能是字符串，在某一刻它变成了布尔值或者其他类型……我知道自己在做什么！</p>
<p>真相是人非完人，总有这样的事情发生：</p>
<ul>
<li>赶进度的时候；</li>
<li>心情糟糕的时候；</li>
<li>周五的想法下周一再回顾的时候发现自己无法理解；</li>
<li>团队协作时，团队成员的技术和看法不在一个水平。</li>
</ul>
<p>出于同样的原因，我们使用 IDE、IDE 插件、代码高亮、linter 而不是记事本应用。TypeScript 和这些辅助工具一样。</p>
<p><img src="https://blog.dastasoft.com/_next/image?url=%2Fassets%2Fposts%2Fcontent%2Ftypescript%2Fairbnb.jpg&amp;w=1920&amp;q=75" alt="airbnb bugs" title="Airbnb claims that 38% of bugs on Airbnb could have been prevented by using TypeScript." width="600" height="400" loading="lazy"></p>
<h3 id="">一些常见的问题</h3>
<p>让我们看一看使用和不使用 TS 的一些对比示例：</p>
<h4 id="">拜托，我知道自己用的是什么！</h4>
<pre><code class="language-js">// App.js
import { MemoryRouter as Router } from 'react-router-dom';

import Routes from './routes';

export default function App() {
  return (
    &lt;Router basename="/my-fancy-app"&gt;
      &lt;Routes /&gt;
    &lt;/Router&gt;
  );
}
</code></pre>
<p>你知道上面代码块的问题出在哪儿吗？如果知道的话，请给自己一朵大红花！</p>
<p>这个文件在我的样板中存在了很长时间，这并不是一个 bug，但是…… <code>MemoryRouter</code>并不需要任何 <code>basename</code>。它出现的原因是我之前使用了<code>BrowserRouter</code>，所以需要<code>basename</code>属性。</p>
<p>如果使用 TS 你会被提示 <code>No overload matches this call</code> 告诉你有这个属性的组件并没有被签名。</p>
<p><strong>TypeScript 不仅可以使用静态类型，也可以帮助你决定是否需要其他的库</strong>。这里的库可以是第三方或者同事提供的组件和函数。</p>
<p>肯定会有一些声音——“了解正在使用的库不是必须么”，是的，你是对的。但是让参与项目的每个人都知道每个“外部”库以及版本的细微差别，可是艰巨的任务！</p>
<h4 id="">魔鬼标志变量</h4>
<pre><code class="language-javascript">let isVerified = false;
verifyAmount();

// isVerified = "false"
if (isVerified) proceedPayment();
</code></pre>
<p>我被这个问题困扰了很多次。虽然每次不是一模一样的代码，一些细微的差别，但是你可以从这个示例中体会我的用意：你设置一个布尔值变量来决定一些代码运不运行，但很有可能其他人（或者你自己）后来将布尔值变成了字符串，而非空字符串为真值。</p>
<p>如果使用 TypeScript，会出现报错: <code>The type 'string' is not assignable to the type 'boolean'</code>。代码在编译时就会出现这个报错，不需要等到运行时，那么在生产阶段出现这样的报错的机率非常低。</p>
<p>当然，和前文的规则一样，如果你正确编写代码，这个问题不会发生，如果你采用简洁代码的策略并且在编码的时候非常小心也可以避免这样的错误。<strong>TypeScript 并不是为了让我们偷懒，而是我们的好帮手</strong>，正如代码高亮可以帮助我们避免错误，找出不正常的变量。</p>
<h4 id="">我以为盒子里面的猫是活着的</h4>
<pre><code class="language-ts">const MONTH_SELECT_OPTIONS = MONTHS.map((month) =&gt; ({
  label: getMonthName(month),
  value: month
}));

export default function PaymentDisplayer() {
  const [currentMonthFilter, setCurrentMonthFilter] = useState(
    MONTH_SELECT_OPTIONS[0]
  );

  const onChangeHandler = (option) =&gt; {
    setCurrentMonthFilter(option.value);
  };

  return (
    &lt;select onChange={onChangeHandler}&gt;
      {MONTH_SELECT_OPTIONS.map(({ label, value }) =&gt; (
        &lt;option key="value" value={value}&gt;
          {label}
        &lt;/option&gt;
      ))}
    &lt;/select&gt;
  );
}
</code></pre>
<p>改变一个 state 的类型非常常见（虽然不建议这么做），有些时候会设置一个<code>isError</code> 标志变量，突然从布尔假值变成表示错误信息的字符串（也不建议这么做！）。还有一些时候是无意为之。</p>
<p>编写这段代码的人一开始认为 <code>currentMonthFilter</code> 会存储实际的选项，一个包含 label 和 value 的<code>HTMLOptionElement</code>。之后，同样的开发在另一个天（或者另一个开发）创建了 <code>changeHandler</code>函数并只设置了<code>value</code>而不是整个选项。</p>
<p>上述代码可以运行，为了方便学习我也做了简化，但是假设项目规模更大，特别是组件的行为是由 props 传递的时候，问题就复杂得多。</p>
<p>使用 TypeScript 可以从两个方面解决这个问题：</p>
<ul>
<li>当你将<code>currentMonthFilter</code>的<code>{label: string, value: number}</code>改成<code>number</code>时，静态类型会报错。</li>
<li>下一个步骤的开发人员，在调用服务检索包含这个筛选条件的付款时候，会通过 <em>IntelliSense</em>（VS Code 中的代码提示）了解他从 state 中获得什么类型，这个类型是否和服务匹配。</li>
</ul>
<p>所以使用 TypeScript，<strong>可以从 IDE 检查第三方库提供的函数、参数以及文档或者是同事编写的组件</strong>。</p>
<p>从上文例子中（可能不那么典型），我们可以得出，TypeScript 在 React 的环境中，可以帮助我们：</p>
<ul>
<li>代码前后一致，静态类型一致</li>
<li>提供文档记录，并通过 <em>IntelliSense</em> 了解可能性</li>
<li>尽早发现问题</li>
</ul>
<h2 id="howtosetuptypescript">如何设置 TypeScript</h2>
<p>在本文中我们将使用全局安装。因为我认为第一次探索 TypeScript 应该不受到 Webpack、React 等其他变量的干扰，这样才能更加了解 TypeScript 是如何运行和处理问题的。</p>
<h3 id="typescript">如何全局安装 TypeScript</h3>
<pre><code class="language-bash">npm install -g typescript

#或

yarn install --global typescript
</code></pre>
<h3 id="typescripttsc">TypeScript 编译器（tsc）是如何工作的</h3>
<p>在系统中安装好 TypeScript 之后，就可以使用 TypeScript 的编译器，使用 <code>tsc</code> 命令行。</p>
<p>让我们通过简单配置编译器测试一下：</p>
<ul>
<li>创建一个新的空文件夹。</li>
<li>放置一个<code>index.html</code>文件，文件内容是基础的 HTML5 结构。</li>
<li>在<code>index.html</code>同一层，创建一个空的<code>index.ts</code>文件。</li>
<li>打开终端，并输入<code>tsc --init</code>（假设你是全局安装 TypeScript），便会创建一个 <code>tsconfig.json</code>文件。（我们将在下一章详细探讨这个文件）</li>
</ul>
<p>你的文件夹结构如下：</p>
<pre><code class="language-sh">- index.html
- index.ts
- tsconfig.json
</code></pre>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
  &lt;head&gt;
    &lt;meta charset="UTF-8" /&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge" /&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0" /&gt;
    &lt;title&gt;Document&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>你需要在 HTML 中添加 TS 文件，但是浏览器并不理解 TypeScript，只认识 JavaScript，所以你可以将<code>index.html</code>修改为：</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
  &lt;head&gt;
    &lt;meta charset="UTF-8" /&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge" /&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0" /&gt;
    &lt;title&gt;Document&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;&lt;/body&gt;
  &lt;script src="./index.js"&gt;&lt;/script&gt;
&lt;/html&gt;
</code></pre>
<p>打开一个新的终端，并输入<code>tsc</code>，你的 <code>index.ts</code> 文件将转换为 <code>index.js</code>，浏览器就可以理解。</p>
<p>为了不在每一次将 TS 文件转换为 JS 文件的时候都输入<code>tsc</code>，你可以将 TypeScript 设置为监控模式，使用<code>tsc -w</code>。</p>
<p>现在我建议你同时打开 TS 文件和 JS 文件，在<code>index.ts</code>文件中输入普通的 JS，测试输出是什么。（我们将在接下来的章节大量使用这样的方法）。</p>
<p><img src="https://blog.dastasoft.com/_next/image?url=%2Fassets%2Fposts%2Fcontent%2Ftypescript%2Fside-by-side.png&amp;w=1920&amp;q=75" alt="side by side" title="Do some test using tsc -w option" width="600" height="400" loading="lazy"></p>
<h3 id="tsconfigjson"><code>tsconfig.json</code>里是什么</h3>
<p>跟着文章一边看一边实践的话，通过<code>tsc --init</code>命令，你将创建 <code>tsconfig.json</code>并包含默认配置和初始化的注解。</p>
<p>让我们看一看一些关键的属性：</p>
<ul>
<li><code>target</code>是 TS 代码将要转换成的 JS 的版本。版本主要取决于支持的浏览器，你可能需要使用比较早期版本的 JS。这也是很好的学习资源，你可以修改不同版本来看生成什么样的 JS 代码。</li>
<li><code>module</code> 设置模块的语法。 <code>commonjs</code>默认使用<code>require/module.exports</code>，现代 JS (ES6+)使用<code>import/export</code>。如果你希望使用 <code>import/export</code>，你需要将<code>target</code>设置为 ES6 或更高。 本文中的示例项目将使用这个语法。</li>
<li><code>lib</code>你需要指定你在项目中额外使用的库，检查额外的类型，如 DOM 相关。</li>
<li><code>jsx</code>如果使用 React，我们需要把这一项设置为<code>preserve</code>，也就是由另一个工具（即 Babel）来编译这 JSX，TSC 仅用于检查类型。你也可以设置为<code>react</code>或者<code>react-native</code>。这个配置决定是否使用 TSC 将你的 JSX 代码转换为常规的 JS 代码。大多数情况，我们将这个属性设置为<code>preserve</code>，将文件设置为常规的 JSX 并由 Babel 或者 Webpack 来处理编译工作。</li>
<li><code>outDir</code>是编译后文件存储的地方，例如大部分 React 项目会被存放在<code>build</code>文件。</li>
<li><code>rootDir</code> 是需要被编译的文件的位置，大部分 React 项目的位置为<code>./src</code>。</li>
<li><code>strict</code>开启一系列检查类型的规则，这些规则对"正确"的要求更为严格。我建议在学习阶段将它设置为 false，当你掌握得还不错了之后再开启。记住开启这个选项就是开启了 TS 的所有潜能，其中包含的一些选项你可以单独关闭。</li>
<li><code>include</code> 你想要编译的文件夹，如<code>src</code>文件夹。</li>
<li><code>exclude</code> 你不想要编译的文件夹，如<code>node_modules</code>文件夹。</li>
</ul>
<p>在示例中，我们将 <code>rootDir</code>设置为 <code>./src</code>， <code>outDir</code>设置为 <code>public</code>文件夹。</p>
<h2 id="sampleshoppinglistproject">购物清单项目示例</h2>
<p>项目示例很简单：你可以在购物清单中添加不同的物品、修改数量、删除物品以及查看需要买什么物品。</p>
<p>示例项目是为了让你适应 TypeScript 的工作流。一旦使用 React 环境，Webpack 和其他一些打包器就完成很多神奇的事情，所以我认为了解基础之后再接触 React 的打包器比较重要。</p>
<p>让我们来看看如何使用 TS 来获得一个更优并不容易出错的代码库。</p>
<h3 id="typescriptmodules"> TypeScript 模块</h3>
<p>你如果想使用 ES6 <code>import/export</code>模块，你必须设置 <code>tsconfig</code> ：</p>
<ul>
<li><strong>target</strong>: es6 or higher</li>
<li><strong>module</strong>: es2015 or later</li>
</ul>
<p>并且在<code>index.html</code> 文件中增加模块类型：</p>
<pre><code class="language-html">&lt;script type="module" src="app.js"&gt;&lt;/script&gt;
</code></pre>
<p>注意使用模块有两个弊端：</p>
<ul>
<li>和旧浏览器的兼容性不好</li>
<li>生产阶段的文件很分散，所以在文件间需要使用大量的请求（这可以通过使用 Webpack 这样的打包器来处理）。</li>
</ul>
<h3 id="typescripttypes">TypeScript 类型</h3>
<p>在 JavaScript 中，类型在运行时分配。当编译器遇到变量和值时候再决定它们的类型是什么。这就意味着我们可以这样做：</p>
<pre><code class="language-js">let job = 'Warrior'; // 字符串
let level = 75; // 数字
let isExpansionJob = false; // 布尔值

level = 'iLevel' + 75;
// 现在是一个字符串
</code></pre>
<p>在 TypeScript 中，类型在编译时分配，一旦一个类型被定义就受到该签名的保护：</p>
<pre><code class="language-ts">let job: string = 'Samurai';
let level: number = 75;
let isExpansionJob: boolean = true;

level = 'iLevel' + 75;
// Error, Type string cannot
// be assign to type number!
</code></pre>
<h4 id="inferenceintypescript">TypeScript 中的类型推论</h4>
<p>实际上不需要明确指定变量类型，TS 可以自行推断：</p>
<pre><code class="language-ts">let job = 'Samurai';
let level = 75;
let isExpansionJob = true;

level = 'iLevel' + 75;
// Error, Type string cannot
// be assign to type number!
</code></pre>
<p>在接下来的 React 项目中，我们也会看到类似的推论，如在使用<code>useState</code>的时候：</p>
<pre><code class="language-ts">const [currentMonthFilter, setCurrentMonthFilter] = useState('January');

useEffect(() =&gt; {
  setCurrentMonthFilter(1);
  // Error, Type number cannot
  // be assign to type string!
}, []);
</code></pre>
<h4 id="anyandunknownintypescript">TypeScript 中的`any`和`unknown`</h4>
<p>我一直说 TS 有静态类型，但有一个细微的点需要说明：</p>
<pre><code class="language-ts">let level: any = 10;

level = 'iLevel' + 125;
// OK, still type any

level = false;
// OK, still type any
</code></pre>
<p>欢迎回到 JavaScript！<code>any</code> 是一种动态类型，当你不知道这个变量在未来的值为何时可以使用，当然这也就放弃掉了 TS 提供的所有优势。</p>
<pre><code class="language-ts">let level: any = 10;

level = 'iLevel' + 125;

level = false;

let stringLevel: string = level;
console.log(typeof stringLevel);
stringLevel.replace('false', 'true');
</code></pre>
<p>当你将 <code>level</code>分配给<code>stringLevel</code>时，变量类型并没有变成<code>string</code>，而是保持布尔值。所以<code>replace</code>函数并不存在，代码在运行时失效，你会收到报错： <code>Uncaught TypeError: stringLevel.replace is not a function</code>。</p>
<p>在这种情况下我们可以使用比<code>any</code>更安全的替换方案:</p>
<pre><code class="language-ts">let level: unknown = 10;

level = 'iLevel' + 125;

level = false;

let stringLevel: string = level;
// Error
</code></pre>
<p><code>unknown</code>和<code>any</code>一样，可以分配任何类型，当但你想要将它分配给另外一个变量时，编译器会报错。所以当你不知道变量未来是什么类型的值时，使用 <code>unknown</code> 而不是 <code>any</code>。</p>
<h4 id="arraysintypescript">TypeScript 中的数组</h4>
<pre><code class="language-ts">let job = 'Red Mage';
let level = 75;
let isExpansionJob = false;
let jobAbilities = ['Chainspell', 'Convert'];

jobAbilities.push('Composure'); // OK
jobAbilities.push(2); // Error
jobAbilities[0] = 2; // Error
</code></pre>
<p>在上面例子中，我们定义了一个由字符串组成的数组：<code>jobAbilities</code>，我们可以在这个数组中添加其他的字符串，但是不能添加其他类型的值，也不能将当前值转换成其他类型。因为在声明数组时，我们将类型推论设置为了 <code>string[]</code>。</p>
<pre><code class="language-ts">let job = 'Red Mage';
let level = 75;
let isExpansionJob = false;
let jobAbilities = ['Chainspell', 'Convert'];
let swordSkill = ['B', 5, 144, 398];

swordSkill.push('B+'); // OK
swordSkill.push(230); // OK

swordSkill[1] = 'C';
// OK, the type is not position related

swordSkill.push(true); // Error
</code></pre>
<p>和之前的例子一样，声明时类型推论就形成。所以我们声明了一个由字符串和数组组成的数组<code>swordSkill</code>。</p>
<p>如果你希望指定声明数组的类型，可以：</p>
<pre><code class="language-ts">let jobAbilities: string[] = ['Chainspell', 'Convert'];
let swordSkill: (string | number)[] = ['B', 5, 144, 398];
</code></pre>
<p><code>|</code> 是 <code>union</code>（联合声明）不同的类型。</p>
<h4 id="objectsintypescript">TypeScript 中的对象</h4>
<p>让我们回到例子，不过这一次是以对象的形式：</p>
<pre><code class="language-ts">let job = {
  name: 'Summoner',
  level: 75,
  isExpansion: true,
  jobAbilities: ['Astral Flow', 'Elemental Siphon']
};

job.name = 'Blue Mage'; // OK
job.level = 'Four'; // Error
job.avatars = ['Carbuncle']; // Error
</code></pre>
<ul>
<li><code>job.level = "Four"</code> 不可以实现，因为我们不可以修改属性的类型。对象的属性也是静态类型。</li>
<li><code>job.avatars = ["Carbuncle"]</code> – 我们不能增加新的属性，因为 <code>job</code> 对象已经拥有一个类型和定义好的结构。</li>
</ul>
<pre><code class="language-ts">let job = {
  name: 'Summoner',
  level: 75,
  isExpansion: true,
  jobAbilities: ['Astral Flow', 'Elemental Siphon']
};

job = {
  name: 'Blue Mage',
  level: 4,
  isExpansion: true,
  jobAbilities: ['Azure Lore', 'Burst Affinity']
}; // OK

job = {
  name: 'Corsair',
  level: 25,
  isExpansion: true
}; // Error
</code></pre>
<p>我们可以分配另一个对象，因为对象是由 <code>let</code> 声明的，但必须是一模一样的形式。</p>
<p>停下来思考一下：有多少次你在前端的工作中，在没有检查的情况下，像这样重复对象结构？有多少次你犯过如<code>data.descrption</code>这样的拼写错误，几天之后你发现问题？如果没有发生过，我保证这种问题迟早会发生。</p>
<p>让我们看看如何指定具体类型：</p>
<pre><code class="language-ts">let job: {
  name: string;
  level: number;
  isExpansion: boolean;
  jobAbilities: string[];
} = {
  name: 'Summoner',
  level: 75,
  isExpansion: true,
  jobAbilities: ['Astral Flow', 'Elemental Siphon']
};
</code></pre>
<p>对于一个简单的对象来说，这样可能有一点复杂了，所以我们可以使用<code>类型别名</code>（Type Aliases）。</p>
<h4 id="aliasesintypescript">TypeScript 中的类型别名</h4>
<pre><code class="language-ts">type Job = {
  name: string;
  level: number;
  isExpansion: boolean;
  jobAbilities: string[];
};

let Summoner: Job = {
  name: 'Summoner',
  level: 75,
  isExpansion: true,
  jobAbilities: ['Astral Flow', 'Elemental Siphon']
};

let BlueMage: Job = {
  name: 'Blue Mage',
  level: 4,
  isExpansion: true,
  jobAbilities: ['Azure Lore', 'Burst Affinity']
};
</code></pre>
<p>使用类型别名可以定义可复用的常见类型。在 React、DOM 和其他的库中有很多这种即用型定义类型。</p>
<h3 id="functionsintypescript"> TypeScript 中的函数</h3>
<p>TS 中的函数语法和 JS 类似，但是你可以指定参数类型和返回类型。</p>
<pre><code class="language-ts">type Enemy = {
  name: string;
  hp: number;
  level: number;
  exp: number;
};

let attack = (target: Enemy) =&gt; {
  console.log(`Attacking to ${target.name}`);
};

attack = 'Hello Enemy'; // Error
</code></pre>
<p>在示例中我使用了箭头函数，你也可以使用普通的函数声明。JS 和 TS 函数的两大不同是：</p>
<ul>
<li>你需要指定传入参数的类型，如 <code>target: Enemy</code>。</li>
<li><code>attack</code>变量已经设定了返回类型，之后就不能修改。</li>
</ul>
<p>函数的类型可以这样声明：</p>
<pre><code class="language-ts">let attack = (target: Enemy): void =&gt; {
  console.log(`Attacking to ${target.name}`);
};
</code></pre>
<p>当返回值为空的时候可以用<code>void</code>类型，也不需要指定特定类型。</p>
<pre><code class="language-ts">// let attack = (target: Enemy): number =&gt; {
let attack = (target: Enemy) =&gt; {
  return target.hp - 2;
};
</code></pre>
<p><code>any</code>和<code>void</code>类型有一些不同：</p>
<pre><code class="language-ts">let attack = (target: Enemy): void =&gt; {
  console.log(`Attacking to ${target.name}`);
};

attack = (target: Enemy): number =&gt; {
  return target.hp - 2;
};

// lizard has 200hp
console.log(attack(lizard)); // 198
</code></pre>
<p>上面的示例没有报错：即便你认为将 <code>attack</code>从<code>(target: Enemy) =&gt; void</code>变成了 <code>(target: Enemy) =&gt; number</code>，类型实际上还是 <code>void</code>。</p>
<p>尝试一下如果首先使用 <code>number</code>来定义函数会怎么样：</p>
<pre><code class="language-ts">let attack = (target: Enemy) =&gt; {
  return target.hp - 2;
};

attack = (target: Enemy) =&gt; {
  console.log(`Attacking to ${target.name}`);
}; // Error

let attackResult = attack(lizard);
</code></pre>
<p><code>Type '(target: Enemy) =&gt; void' is not assignable to the type '(target: Enemy) =&gt; number'</code>. <code>Type 'void' is not assignable to the type 'number'</code>，所以在这个情况下 <code>void</code> 和 <code>any</code>类似。</p>
<p><code>attackResult</code>的类型为 <code>number</code>，没有必要重新指定，TS 会通过函数的返回类型完成推论。</p>
<h4 id="optionalparametersintypescript">TypeScript 中的可选参数</h4>
<p>使用 <code>?</code>来定义 TS 函数中的可选参数：</p>
<pre><code class="language-ts">let heal = (target: Player | Enemy, spell: Spell, message?: string) =&gt; {
  if (message) console.log(message);
  return target.hp + spell.power;
};

heal(player1); // Error
heal(player1, cure, 'Healing player1'); // OK
heal(skeleton, cure); // OK
</code></pre>
<p>第一个函数调用不成功是因为我们必须至少传入两个参数，后面两次调用成功。 <code>message</code>是一个可选参数，如果没有传入的话，就被定义为<code>undefined</code>。</p>
<p>这个示例转换为 JS 为：</p>
<pre><code class="language-ts">let heal = (target, spell, message) =&gt; {
  if (message) console.log(message);
  return target.hp + spell.power;
};

heal(player1); // Error
heal(player1, cure, 'Healing player1'); // OK
heal(skeleton, cure); // OK
</code></pre>
<p>两个函数的基本行为一致，只是 JS 的问题会在运行时显示出来，第一个调用出错的原因是不可以从一个没有定义的值获取 <code>power</code>。</p>
<p>从示例中我们可以发现，TS 的函数更安全，因为你不需要依赖外部环境，也清楚知道需要传入什么参数。</p>
<p>这对于其他使用这个函数的开发者说也是一样的，他们知道需要使用什么参数、形式以及会返回什么参数。</p>
<h3 id="typescriptenums">TypeScript 枚举（Enum）</h3>
<p>使用枚举，我们可以定义一个常量集合。</p>
<pre><code class="language-ts">enum BattleMenu {
  ATTACK,
  MAGIC,
  ABILITIES,
  ITEMS,
  DISENGAGE
}

enum Equipment {
  WEAPON = 0,
  HEAD = 1,
  BODY = 2,
  HANDS = 3,
  LEGS = 4
}

console.log(BattleMenu.ATTACK, Equipment.WEAPON);
// 0 0
</code></pre>
<p>枚举默认是自动序列化的，示例中的两种声明方式对等。</p>
<p>也可以使用枚举来存储字符串，我常在 React 中使用枚举来存储路径：</p>
<pre><code class="language-ts">enum Routes {
  HOME = '/',
  ABOUT = '/about',
  BLOG = '/blog'
}
</code></pre>
<h3 id="typescriptgenerics"> TypeScript 泛型（Generics）</h3>
<pre><code class="language-ts">const getPartyLeader = (memberList: Player[]) =&gt; {
  return memberList[0];
};

const partyLeader = getPartyLeader(partyA);
</code></pre>
<p>上面代码想要实现一个 <code>getPartyLeader</code> 函数，返回党魁（数组的第一位）。</p>
<p>如果我们想要函数支持除 <code>Player</code>以外的类型呢？根据我们所知的信息，可以采取这样的做法：</p>
<pre><code class="language-ts">const getPartyLeader = (memberList: Player[] | Enemy[]) =&gt; {
  return memberList[0];
};

const partyLeader = getPartyLeader(partyA);
// Player[] | Enemy[]
</code></pre>
<p>现在我们可以传入 <code>Player</code>组也可以传入 <code>Enemy</code>组，但是 <code>PartyLeader</code>常数可以为两组中任意一种类型，所以使用 <code>Player[] | Enemy[]</code>。</p>
<p>如果我们想要使用指定类型的话，也可以使用泛型：</p>
<pre><code class="language-ts">const getPartyLeader = &lt;T&gt;(memberList: T[]) =&gt; {
  return memberList[0];
};

const partyLeader = getPartyLeader(partyA); // Player
</code></pre>
<p><code>partyA</code> 数组内是<code>Player</code>类型， <code>partyLeader</code> 就为 <code>Player</code>类型。 让我们查看语法：</p>
<ul>
<li><code>T</code> 是通常定义泛型的方法，但是你可以采用任意你喜欢的方式。</li>
</ul>
<p>和使用 <code>any</code>一样， T 接收任意类型。所以我们可以修改传入的参数类型：</p>
<pre><code class="language-ts">type Player = {
  name: string;
  hp: number;
};

type Enemy = {
  name: string;
  hp: number;
};

type Spell = {
  name: string;
  power: number;
};

const getPartyLeader = &lt;T extends { hp: number }&gt;(memberList: T[]) =&gt; {
  return memberList[0];
};

const playerPartyLeader = getPartyLeader(partyOfPlayers); // Ok
const enemyPartyLeader = getPartyLeader(partyOfEnemies); // Ok
const whatAreYouTrying = getPartyLeader(spellList); // Error
</code></pre>
<p>我们只能传入包含<code>hp</code>属性的对象。</p>
<h3 id="tuplesintypescript">TypeScript 中的元祖（Tuples）</h3>
<p>正如我们之前看到的，数组可以包含不同的类型但不受位置限制。元组可以补充这一点。</p>
<pre><code class="language-ts">type Weapon = {
  name: string;
  damage: number;
};

type Shield = {
  name: string;
  def: number;
};

const sword: Weapon = {
  name: 'Onion Sword',
  damage: 10
};

const shield: Shield = {
  name: 'Rusty Shield',
  def: 5
};

let equipment: [Weapon, Shield, boolean];

equipment = [sword, shield, true]; // OK
equipment[2] = false; // OK

equipment = [shield, sword, false]; // Error
equipment[1] = true; // Error
</code></pre>
<p>这样我们就拥有了类数组的类型，它关心类型的放置位置。</p>
<h3 id="classesintypescript"> TypeScript 中的类</h3>
<p>由于从 ES6 开始 JS 中添加了类，TS 和 JS 的类大同小异：</p>
<pre><code class="language-ts">class Job {
  public name: string;
  private level: number;
  readonly isExpansion: boolean;

  constructor(name: string, level: number, isExpansion: boolean) {
    this.name = name;
    this.level = level;
    this.isExpansion = isExpansion;
  }
}

const whiteMage = new Job('White Mage', 75, false);

console.log(whiteMage.name); // "White Mage"
console.log(whiteMage.level); // Error
console.log(whiteMage.isExpansion); // false

whiteMage.name = 'Blue Mage'; // Ok
whiteMage.level = 50; // Error
whiteMage.isExpansion = true; // Error
</code></pre>
<p>在 TS 类中，你可以访问类属性的修饰符（modifiers）：</p>
<ul>
<li><strong>public</strong> - 可以自由访问属性和方法，这时 TS 类的默认值</li>
<li><strong>private</strong> - 只能从声明它的类访问</li>
<li><strong>protected</strong> - 限制声明类和子类访问</li>
<li><strong>readonly</strong> - 标记属性为不可变</li>
</ul>
<h3 id="interfacesintypescript">TypeScript 中的接口（interfaces）</h3>
<p>和 <code>类型别名</code>（type aliases)相同，我们可以使用<code>接口</code>（interfaces）来定义类：</p>
<pre><code class="language-ts">interface Enemy {
  name: string;
  hp: number;
}

let attack = (target: Enemy): void =&gt; {
  console.log(`Attacking to ${target.name}`);
};
</code></pre>
<p>是不是看上去和<code>类型别名</code>很像？那应该使用哪一个呢？两种方法都可以控制不同类型的 TS，并且区别非常小：</p>
<p>你可以遵循以下规则来做取舍：</p>
<ul>
<li>如果你编写面向对象的代码，可以使用接口，如果你编写函数式代码，可以使用 aliases。</li>
<li>公共 API 库、组件类型、state、JSX 使用接口。</li>
</ul>
<p>因此，我在样板中加入了 ESLint 自动将类型别名转换为接口。</p>
<p>如果想要深入了解两者的区别，可以阅读<a href="https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#differences-between-type-aliases-and-interfaces">TS 手册中的这篇文章</a> ，但现在很多使用接口的功能都可以使用类型别名，反之亦然。</p>
<h3 id="dommanipulationintypescript">TypeScript 中的 DOM 操作</h3>
<p>虽然在 React 中直接操作 DOM 的机会不多，但是我觉得还是有必要知道 DOM 的相关知识。</p>
<h4 id="dom">如何从 DOM 检索元素</h4>
<pre><code class="language-ts">// HTMLFormElement | null
const form = document.querySelector('form');

// HTMLElement | null
const otherForm = document.getElementById('myFancyForm');

// HTMLSelectElement
const select = document.createElement('select');
</code></pre>
<p>执行<code>document.querySelector ("form")</code>时， 常量<code>form</code>被类型推论为<code>HTMLFormElement</code> 或 <code>null</code>。 但在第二个例子中，我们通过 ID 来获取 dom，TS 并不知道是什么 HTML 元素，所以推论为泛型 <code>HTMLElement</code>。</p>
<pre><code class="language-ts">const form = document.querySelector('form');

form.addEventListener('submit', (e: Event) =&gt; {
  e.preventDefault();
  console.log(e);
}); // Error
</code></pre>
<p>TS 不知道是否能够通过查询选择器在 HTML 找到元素，所以不能对一个可能为 null 的类型添加 <code>addEventListener</code> 函数，你可以这样修改：</p>
<p>我确认会找到元素：</p>
<pre><code class="language-ts">// HTMLFormElement
const form = document.querySelector('form')!;
</code></pre>
<p>使用 <code>!</code> 告诉 TS 放心，一定不会是<code>null</code>。</p>
<p>如果不为 null 运行：</p>
<pre><code class="language-ts">const form = document.querySelector('form');

form?.addEventListener('submit', (e: Event) =&gt; {
  e.preventDefault();
  console.log(e);
});
</code></pre>
<p>你可能在 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining">JS 可选链式运算符</a>中见过 “?”。</p>
<p>是铸造类型的时候了：</p>
<pre><code class="language-ts">const otherForm = document.getElementById('myFancyForm') as HTMLFormElement;

otherForm.addEventListener('submit', (e: Event) =&gt; {
  e.preventDefault();
  console.log(e);
});
</code></pre>
<p>通过 <code>HTMLFormElement</code> 告诉 TS 会找到什么类型的元素，而不是 <code>null</code>。</p>
<h2 id="dommanipulationintypescript"> 如何结合React + TypeScript</h2>
<p>让我们进入文章的第二个部分，记住，在第一个部分我们探讨了为什么使用 TypeScript，如何使用，以及这个语言的概览。</p>
<p>在这个部分，我们将学习如何在 React 中使用 TypeScript，如何解决相应的难题，以及如何使用 React 和 TypeScript 共同创建一个应用。</p>
<h3 id="setup">设置</h3>
<h4 id="createreactapp">Create React App</h4>
<p>对于 <a href="https://create-react-app.dev">CRA</a> 用户来说，你们只需要设定模板：</p>
<pre><code class="language-bash">npx create-react-app my-awesome-project --template typescript
</code></pre>
<h4 id="vite">Vite</h4>
<p>使用 <a href="https://vitejs.dev">Vite</a> 创建 TypeScript 项目和使用 CLI 一样简单，只需要选择 TypeScript 模板：</p>
<pre><code class="language-bash">npm create vite@latest my-awesome-project
</code></pre>
<h4 id="">添加到现有项目</h4>
<p>如果你想要对已经存在的 JavaScript 项目添加 TypeScript，只需要添加对应开发依赖项：</p>
<pre><code class="language-bash">npm install -D typescript
</code></pre>
<p>需要提醒你的事，如果是首次使用 TypeScript，不建议你从现有的项目着手。因为这样的话，你会不断地认为你已经有一个可以运行的项目了，而使用 TypeScript 不过是做些无所谓的工作。你没办法从中体会 TypeScript 的优势。</p>
<h3 id="typingcomponentprops">设置组件 Props 类型</h3>
<p>在 React 项目中使用 TypeScript 最常用的场景是编写组件 props。</p>
<p>想要正确地编写组件 props，必须定义清楚组件接受什么样的 props、props 类型以及是否是必要的。</p>
<pre><code class="language-ts">// src/components/AnimeDetail/Cover/index.tsx

type CoverProps = {
  url: string;
};

export default function Cover({ url }: CoverProps) {
  // ...
}
</code></pre>
<p>我们只使用 <code>url</code> prop ，类型为 <code>string</code> 并且是强制的。</p>
<p>另一个有多个 props 和可选项的例子：</p>
<pre><code class="language-ts">// src/components/AnimeDetail/StreamingList/PlatformLink/index.tsx

type PlatformLinkProps = {
  name: string;
  url?: string;
};

export default function PlatformLink({ name, url }: PlatformLinkProps) {
  // ...
}
</code></pre>
<p>使用 <code>?</code> 来定义可选参数， TypeScript 知道在这个例子中<code>url</code> 的类型是 <code>string</code>，默认值为<code>undefined</code>，即便未传入<code>url</code> ，消费组件也不会报错。</p>
<p>让我们看一个更复杂的例子：</p>
<pre><code class="language-ts">// src/components/AnimeDetail/Detail/index.tsx

type AnimeType = 'TV' | 'Movie';

type DetailProps = {
  liked: boolean;
  toggleFav: () =&gt; void;
  title: string;
  type: AnimeType;
  episodeCount: number;
  score: number;
  status: string;
  year: number;
  votes: number;
};

export default function Detail({
  liked,
  toggleFav,
  title,
  type,
  episodeCount,
  score,
  status,
  year,
  votes
}: DetailProps) {
  // ...
}
</code></pre>
<p>这次包含众多类型，包括 <code>function</code>和一个自定义类型 <code>AnimeType</code>。</p>
<p>所以总结一下，使用 TS 来编写 props：</p>
<ul>
<li>对于消费组件来说的 props 验证
<ul>
<li>不需要猜测组件需要什么</li>
<li>不需要打开组件源码来检查需要什么数据</li>
</ul>
</li>
<li>自动填充和文档记录
<ul>
<li>直接从消费组件端知道自动填充的 prop 和 value，不需要提前浏览</li>
</ul>
</li>
</ul>
<p><img src="https://blog.dastasoft.com/_next/image?url=%2Fassets%2Fposts%2Fcontent%2Ftypescript2%2Fautocomplete.webp&amp;w=1920&amp;q=75" alt="autocomplete" width="600" height="400" loading="lazy"></p>
<p>如果是使用复杂的组件，或是从第三方库使用组件的，这一定可以派上用场。</p>
<h4 id="reactbuiltintypes"> React 内置类型</h4>
<p>在 React 和很多其他的库中，你会发现大量预置的类型，可以减轻开发者的编写负担。如以下示例：</p>
<pre><code class="language-ts">// src/components/Layout/index.tsx

type LayoutProps = {
  children: React.ReactNode;
};

export default function Layout({ children }: LayoutProps) {
  // ...
}
</code></pre>
<p>一个自定义的 React 组件，接受其他元素作为子元素，在这种情况下 <code>children</code>被定义为<code>ReactNode</code> 类型。</p>
<h5 id="reactfcreactfunctioncomponent">一个关于 React.FC 和 React.FunctionComponent 的提醒</h5>
<p>你可以能会遇到这样定义组件 props 的语法：</p>
<pre><code class="language-ts">type PlatformLinkProps = {
  name: string;
  url?: string;
};

const PlatformLink: React.FC&lt;PlatformLinkProps&gt; = ({ name, url }) =&gt; {
  // ...
};
</code></pre>
<p>使用 <code>React.FC</code>或者使用 <code>React.FunctionComponent</code>时，上面的代码生效，但是你需要知道这样使用的弊端，也就是为什么在本文中我们不这样用：</p>
<ul>
<li>必须使用函数表达式而不是函数声明，虽然这是一个很小的点，但是我的编码习惯是使用函数声明。</li>
<li>不可以使用泛型（之后会讨论）。</li>
<li>在过去，这样会导致 props 非直接地接受<code>children</code>属性，即便该组件并不使用这个属性。直到 React18 之后，这个问题才改善。</li>
</ul>
<h4 id="returntypeofareactcomponen">React 组件的返回类型</h4>
<p>最后一块拼图，组件返回什么？可以使用内置的类型：<code>React.ReactElement</code>、 <code>React.ReactNode</code>和 <code>JSX.Element</code>：</p>
<pre><code class="language-ts">export default function Favorites(): JSX.Element {
  // ...
}
</code></pre>
<p>总结一下：<strong>让 TypeScript 自行推论返回类型</strong>。如果你需要这一部分的详细介绍，我推荐<a href="https://stackoverflow.com/questions/58123398/when-to-use-jsx-element-vs-reactnode-vs-reactelement">阅读这个来自 stack overflow 的帖子</a>。</p>
<h4 id="combinationswithtemplateliterals">集合模板字面量 </h4>
<p>在<a href="https://animetrailers.dastasoft.com/">动漫预告片项目</a>中，我引入的一个自定义 UI 就是很好的示例。你可以查看<code>src/components/UI</code>中的组件，其中大部分内容都会在本文讨论：</p>
<p>让我们看一下自定义组件<code>Position</code>：</p>
<pre><code class="language-ts">// src/components/UI/Position/index.tsx

import React from 'react';

import { StyledPosition } from './StyledPosition';

type VPosition = 'top' | 'bottom';
type HPositon = 'left' | 'right';

export type PositionValues = `${VPosition}-${HPositon}`;

type PositionProps = {
  children: React.ReactNode;
  position?: PositionValues;
};

export default function Position({
  children,
  position = 'top-right'
}: PositionProps) {
  return &lt;StyledPosition position={position}&gt;{children}&lt;/StyledPosition&gt;;
}
</code></pre>
<p>Position 是一个简单的组件，可以与任何其他具有绝对位置的组件一起使用，可以通过 <code>top-left</code>, <code>top-right</code>, <code>bottom-left</code>和 <code>bottom-right</code>将组件放置在四个边上。</p>
<p>使用字面量模板来创建新的类型并不新鲜，在这里有趣的地方是结合 <code>${VPosition}-${HPositon}</code>和联合类型 <code>top</code> | <code>bottom</code> ，TypeScript 会自动生成所有组合，就可以创建我们需要的四种类型。</p>
<h4 id="howtouseexclude">如何使用 `Exclude`</h4>
<p>让我们给上面的例子添加更多值：</p>
<pre><code class="language-ts">type VPosition = 'top' | 'middle' | 'bottom';
type HPositon = 'left' | 'center' | 'right';

export type PositionValues = `${VPosition}-${HPositon}`;
</code></pre>
<p>模板会创建所有可能的组合，所以我们将拥有 <code>"top-left" | "top-center" | "top-right" | "top-left" | "center-left" | "center-right" | "bottom-left" | "bottom-center" | "bottom-right"</code>。</p>
<p>有一条比较奇怪： <code>middle-center</code>，我们只需要<code>center</code>，这时就可以使用 <code>Exclude</code> ：</p>
<pre><code class="language-ts">type PositionValues =
  | Exclude&lt;`${VPosition}-${HPositon}`, 'middle-center'&gt;
  | 'center';
</code></pre>
<p>这时 <code>PositionValues</code>会生成<code>"center" | "top-left" | "top-center" | "top-right" | "middle-left" | "middle-right" | "bottom-left" | "bottom-center" | "bottom-right"</code>。</p>
<p>使用 exclude 删除<code>middle-center</code>之后再添加<code>center</code>。</p>
<h4 id="customhtmlcomponents">自定义 HTML 组件</h4>
<p>如果你想创建一个行为类似<code>input</code>的组件，但是你又不想编写 input HTML 的所有属性和方法，你可以这样：</p>
<pre><code class="language-ts">// src/components/UI/Input/index.tsx

import React from 'react';

import styles from './StyledInput.module.css';

type InputProps = React.ComponentProps&lt;'input'&gt;;

const Input = React.forwardRef(
  (props: InputProps, ref: React.Ref&lt;HTMLInputElement&gt;) =&gt; {
    return &lt;input {...props} className={styles.StyledInput} ref={ref} /&gt;;
  }
);

export default Input;
</code></pre>
<p>使用<code>React.ComponentProps</code>，你可以指定你需要基于什么类型创建一个组件，获取一个真正的 HTML input 的功能来自定义 UI 组件。但如果你想覆盖掉一些属性甚至禁用怎么办？</p>
<h5 id="omit">忽略（Omit）</h5>
<p>让我们以 UI 组件中的 <code>Tag</code>为例：</p>
<pre><code class="language-ts">// src/components/UI/Tag/index.tsx

import React from 'react';

import { StyledTag } from './StyledTag'; // aka a styled span

type TagProps = {
  variant?: 'solid' | 'outlined';
  text: string;
} &amp; Omit&lt;React.ComponentProps&lt;'span'&gt;, 'children'&gt;;

export default function Tag({ text, variant = 'solid' }: TagProps) {
  return &lt;StyledTag variant={variant}&gt;{text}&lt;/StyledTag&gt;;
}
</code></pre>
<p>在这个例子中，我们显式地传递了一个<code>text</code>展示组件的 <code>children</code>。你或许不希望消费组件使用原始的<code>children</code>，你可以忽略掉 <code>React.ComponentProps</code>中的这个属性。</p>
<h3 id="typinghooks"> 定义 Hook 的类型</h3>
<p>现在让我们看一下如何编写 React 中一些常用的 hook。</p>
<h4 id="usestatehook">useState hook</h4>
<p>大多数情况下，<code>useState</code> 不需要额外的操作，TypeScript 会自行推论。但是如果初始值和未来值不同则需要特别声明。</p>
<pre><code class="language-ts">// src/pages/Search.tsx

export default function Search() {
  const [animeList, setAnimeList] = useState&lt;Anime[] | null&gt;(null);
  const [page, setPage] = useState(1);
  // const [page, setPage] = useState&lt;number&gt;(1)
  // ...
}
</code></pre>
<p><code>page</code>的状态可以根据初始值推论为数字，它的行为和注解里的代码一模一样。state 的 setter 也会定义为 <code>React.Dispatch&lt;React.SetStateAction&lt;number&gt;&gt;</code>， <code>number</code>来替换推论的类型。</p>
<p>如果<code>animeList</code> 没有任何显式类型的话就为 <code>null</code>。在组件获取必要的数据之前这都是正确的，但是最终会是包含 <code>Anime</code>类型集合，所以必须将这个类型显式地设置为这两个可能类型的组合。</p>
<p>除了给 useState 的初始 state 设置为 null 以外，还可以：</p>
<pre><code class="language-ts">export default function Search() {
  // const [animeList, setAnimeList] = useState&lt;Anime[] | null&gt;(null)
  const [animeList, setAnimeList] = useState&lt;Anime[]&gt;([]);
  const [anime, setAnime] = useState&lt;Anime&gt;({} as Anime);
  // ...
}
</code></pre>
<p>请仔细观察 <code>anime, setAnime</code> 代码行，它之所以生效是因为这不是一个集合，而是单个元素。<br>
这里的区别在于你对编译器没有完全诚实，你预设会得到这个对象形状（shape）的值，有一定风险。</p>
<pre><code class="language-ts">export default function Search() {
  const [anime, setAnime] = useState&lt;Anime&gt;({} as Anime);
  // ...

  return &lt;div&gt;{anime.coverURL}&lt;/div&gt;;
}
</code></pre>
<p>如果没有在可选项中提供正确的值，会在运行时报错。</p>
<h5 id="stateprops">如何将 state 作为 props 传递</h5>
<p>多数情况下，是由上至下传递 state，并且等 state 完成或者设置好了才能传递到子组件，所以需要在编写 props 的时候想好状态的类型。</p>
<pre><code class="language-ts">type FancyComponentProps = {
  anime: Anime;
  setAnime: React.Dispatch&lt;React.SetStateAction&lt;Anime&gt;&gt;;
};

const FancyComponent = ({ anime, setAnime }: FancyComponentProps) =&gt; {
  // ...
};
</code></pre>
<p>最好清楚自己传递的是什么类型。如果你觉得困难的话，可以使用 IDE 的提示。</p>
<p><img src="https://blog.dastasoft.com/_next/image?url=%2Fassets%2Fposts%2Fcontent%2Ftypescript2%2Ftype-intellisense.webp&amp;w=1920&amp;q=75" alt="type intellisense" width="600" height="400" loading="lazy"></p>
<h4 id="usereducerhook">useReducer hook</h4>
<p>你已经具备正确定义 <code>useReducer</code>所需的所有知识。</p>
<p>下面的例子中我简化了代码，正式代码会在泛型部分讲解：</p>
<pre><code class="language-ts">// src/hooks/useFetch.ts

const enum ACTIONS {
  LOADING,
  FETCHED,
  ERROR
}

type State = {
  data?: Anime[];
  loading: boolean;
  error?: Error;
};

type Action =
  | { type: ACTIONS.LOADING }
  | { type: ACTIONS.FETCHED; payload: Anime }
  | { type: ACTIONS.ERROR; payload: Error };

const initialState: State = {
  loading: true,
  error: undefined,
  data: undefined
};

const fetchReducer = (state: State, action: Action): State =&gt; {
  switch (action.type) {
    case ACTIONS.LOADING:
      return { ...initialState };
    case ACTIONS.FETCHED:
      return { ...initialState, data: action.payload, loading: false };
    case ACTIONS.ERROR:
      return { ...initialState, error: action.payload, loading: false };
    default:
      return state;
  }
};

const [state, dispatch] = useReducer(fetchReducer, initialState);
</code></pre>
<p>和往常一样， <code>status</code> 和 <code>dispatch</code> 来自于使用 <code>useReducer</code>时的 <code>reducer function</code> 以及一个 <code>initial state</code>。你不需要额外编写其他内容，但是你必须编写<code>state</code> 和 <code>actions</code>，因为 state 和 dispatch 的行为是根据它们来的。</p>
<h5 id="initialstate">initialState</h5>
<p>我们可以简化<code>initial state</code> 这个部分。 不是创建一个<code>State</code>类型，而是使用 <code>typeof initialState</code> 来定义。</p>
<pre><code class="language-ts">const initialState: State = {
  loading: true,
  error: undefined,
  data: undefined
};

const fetchReducer = (state: typeof initialState, action: Action) =&gt; {
  // ...
};
</code></pre>
<p>这样写的弊端是无法控制未来的 <code>data</code>和<code>error</code>的值，如果 state 的类型始终保持一致的话，没有问题，除此之外可以使用自定义 <code>State</code>类型。</p>
<h5 id="actions">Actions</h5>
<p>你可以使用组合（union）来处理 reducer 的 action 部分，你也可以选择使用枚举（Emun），这样写的话比在多个地方写字符串要更不容易出错。</p>
<h5 id="reducerfunction">reducer function</h5>
<p>你只需要指定传入函数的参数的类型，这个已经在上一部分完成。</p>
<h5 id="props">作为 props 传递</h5>
<p>另外，如果你需要从 useReducer 传出 prop，你必须编写对应的消费组件 props。</p>
<ul>
<li><code>state</code> 必须是你定义在 <code>initialState</code> 的类型，或者是例子中的自定义 <code>State</code>类型。</li>
<li><code>dispatch</code> 是 <code>React.Dispatch&lt;Action&gt;</code> 中自定义 <code>Action</code>类型。</li>
</ul>
<h4 id="usecontext">useContext</h4>
<p>示例项目中的上下文用于管理你喜欢动漫的列表，你可以在应用不同地方切换它们的 state。 <code>useContext</code> 并不是一个难点，使用它的方法就是上述内容的结合——让我们一起看代码：</p>
<pre><code class="language-ts">// src/context/FavContext.tsx

type FavContextType = {
  favList: Favorite[];
  // setFavList: React.Dispatch&lt;React.SetStateAction&lt;Favorite[]&gt;&gt;
  toggleFav: (id: number, favorite: Favorite) =&gt; void;
};

export const FavContext = createContext({} as FavContextType);

export const FavContextProvider = ({ children }: FavContextProviderProps) =&gt; {
  const [favList, setFavList] = useState&lt;Favorite[]&gt;([]);

  const toggleFav = (id: number, favorite: Favorite) =&gt; {
    /* ... */
  };

  // ...

  return (
    &lt;FavContext.Provider value={{ favList, toggleFav }}&gt;
      {children}
    &lt;/FavContext.Provider&gt;
  );
};
</code></pre>
<p><code>useContext</code>和 <code>useState</code>定义类型的规则相同。在我们的示例中，初始值应该为 null，但是我们使用了一个小技巧，在<code>createContext</code>添加<code>as</code>，并且定义了一个对象，包含一个<code>favourite animes</code>数组，和负责切换的函数。</p>
<p>注解部分是你根据场景需要的特定 setter。</p>
<p>接下来的代码都是你在 <code>useState</code> 中学过的内容。在 <code>Favorite</code> 类型中，useState 会推断必要的类型，这些类型可以直接在消费组件中访问。</p>
<pre><code class="language-ts">// src/components/AnimeDetail/index.tsx

const { favList, toggleFav } = useContext(FavContext);
</code></pre>
<h4 id="userefhook">useRef hook</h4>
<p>可以通过两种方式来使用 <code>useRef</code>，我们将分别讲解。</p>
<h5 id="dom">DOM 引用</h5>
<p>其中一个方式是使用<code>useRef</code>保持一个 DOM 元素的引用 。</p>
<p>在示例项目中，通过持有对动画列表中最后一项的可观察对象的引用，你会发现它可以无限滚动。这让你知道用户何时在视口中查看该项目并触发新的获取。</p>
<p>让我们查看使用 useRef 来引用 DOM 一个更简短的示例，你也可以<a href="https://github.com/dastasoft/animetrailers/blob/main/src/components/AnimeList/index.tsx">查看 useRef + observer 的完整版本</a>：</p>
<pre><code class="language-ts">const myDomReference = useRef&lt;HTMLInputElement&gt;(null);

useEffect(() =&gt; {
  if (myDomReference.current) myDomReference.current.focus();
}, []);
</code></pre>
<p>一个典型的情况是当页面加载的时候，你希望自动聚焦在输入框，只需要指定好引用的 DOM 元素，在这个示例中就是<code>HTMLInputElement</code>。</p>
<p>使用上面代码需要注意的是：</p>
<ul>
<li>钩子会返回一个只读的 <code>current</code> 属性。</li>
<li>你不需要手动编写<code>current</code>，React 会通过<code>React.RefObject&lt;HTMLInputElement&gt;</code>处理。</li>
<li>如果 DOM 元素永远指向现在，你可以将初始值设定为 <code>null!</code> 以避开 if 检查。</li>
</ul>
<h5 id="">可变引用</h5>
<p><code>useRef</code> 的第二个使用场景是在渲染之间保持可变值。例如，在你需要为组件的每个实例提供一个唯一变量的情况下，该变量在渲染之间存在并且不会触发重新渲染。</p>
<pre><code class="language-ts">const isFirstRun = useRef(true);

useEffect(() =&gt; {
  if (isFirstRun) {
    // ...
    isFirstRun.current = false;
  }
}, []);
</code></pre>
<p>使用上面代码，你需要注意的是：</p>
<ul>
<li>现在你可以改变<code>current</code>的值。</li>
<li>React 提供的 <code>React.MutableRefObject&lt;boolean&gt;</code>现在是<code>RefObject</code>内部的<code>MutableRefObject</code>（可变引用对象）。</li>
</ul>
<h3 id="forwardingref">传递 ref</h3>
<p>如果在某些时候你需要像 useRef 部分那样传递对 HTML 元素的引用，那么为该组件编写 props 会略有不同：</p>
<pre><code class="language-ts">// src/components/AnimeGrid/Card/index.tsx

const Card = React.forwardRef(
  (
    { id, coverURL, title, status, score, type, year }: CardProps,
    ref: React.Ref&lt;HTMLImageElement&gt;
  ) =&gt; {
    // ...
  }
);
</code></pre>
<p>要传递引用，需要用<code>React.forwardRef</code>打包组件，这将与组件的常规 props 一起注入 <code>ref</code>（是包装在 <code>React.Ref</code> 类型中的任何 HTML 元素）。</p>
<p>这样我们就知道我们传递的 HTML 元素的类型，如果你的使用场景不是这样，可以使用泛型。</p>
<h3 id="howtousetypescriptgenericsinreact">如何在 React 中使用 TypeScript 范型</h3>
<p>假设我们想通过包装现有的 HTML 元素来创建自定义 UI 组件，但像大多数组件库一样为它提供一组自定义属性。</p>
<p>这些组件库也提供一些灵活性，如哪一个 HTML 元素被渲染由 <code>as</code>属性控制 – <code>Text</code> UI 组件的示例就是这样。</p>
<p>Text UI 组件用来显示一组尺寸和颜色的任意文本，同时我们也希望使用者选择他们需要的 HTML 元素，而不是只限定于 <code>p</code> 或 <code>span</code>。</p>
<p>在这个示例中，你不能提前知道用户会选择什么元素传入组件，所以你需要使用泛型来推断它们使用了什么类型。</p>
<p>所以组件的 prop 如下：</p>
<pre><code class="language-ts">// src/components/UI/Text/index.tsx

type TextOwnProps&lt;T extends React.ElementType&gt; = {
  size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
  variant?: 'base' | 'primary' | 'secondary';
  as?: T | 'div';
};

type TextProps&lt;T extends React.ElementType&gt; = TextOwnProps&lt;T&gt; &amp;
  React.ComponentPropsWithoutRef&lt;T&gt;;

export default function Text&lt;T extends React.ElementType = 'div'&gt;({
  size = 'md',
  variant = 'base',
  children,
  as = 'div'
}: TextProps&lt;T&gt;) {
  // ...
}
</code></pre>
<p>让我们仔细查看上面代码：</p>
<ul>
<li>我们使用<code>T</code>来命名泛型，你也可以使用任意字母。</li>
<li>T 扩展自<code>React.ElementType</code>，即 HTML 元素的最通用的类型。所以我们知道传递给组件的任何东西都是基于 HTML 元素，而不是所有可能的 HTML 元素的手动类型的组合。</li>
<li><code>TextProps</code> 的第二个类型有两个用途：</li>
<li>根据 HTML 元素的类型，我们需要额外的属性。当消费组件使用 Text 组件作为<code>label</code>时，我们希望检查并建议与为<code>span</code>时不同的属性。为此，我们需要使用 <code>React.ComponentProps</code>。在这种情况下，我们不需要引用，因此我们显式使用<code>ComponentPropsWithoutRef</code>类型。</li>
<li><code>React.ComponentProps</code>同时也提供 <code>children</code> prop，所以不需要在 <code>TextOwnProps</code>引入。</li>
<li>你也不需要处理<code>Omit</code> 或者其他排除技术，因为 <code>children</code> 并没有被 <code>TextOwnProps</code> props 修改或者覆盖。</li>
</ul>
<p>通过这个例子，我们有一个非常灵活的组件，它的类型正确并且提供了良好的开发者体验。</p>
<p>在示例项目中，你可以测试不同的自定义 UI 组件，来检查上述模式的实现。</p>
<h3 id="typingacustomusefetchhook"> 定义自定义 useFetch Hook 类型</h3>
<p>在示例项目中，我编写了一个简单的钩子来获取数据，并且利用<code>localStorage</code>来暂时缓存数据，这样就不会超出 API 的限制。这没什么大不了的，但我认为它是本文中解释的所有内容的完整示例。</p>
<p>让我们一起看看这个钩子——但是我更推荐你查看<a href="https://github.com/dastasoft/animetrailers/blob/main/src/hooks/useFetch.t">实际文件</a>，以及理解这本文章不同部分的讲解。</p>
<pre><code class="language-ts">// src/hooks/useFetch.ts

type State&lt;T&gt; = {
  data?: T;
  loading: boolean;
  error?: Error;
};

function useFetch&lt;T = unknown&gt;(
  url?: string,
  { initialFetch, delayFetch }: Options = { initialFetch: true, delayFetch: 0 }
): State&lt;T&gt; {
  // ...
}
</code></pre>
<ul>
<li>钩子接受一个泛型，你并不能提前知道你将处理什么类型的数据。</li>
<li>钩子接受 <code>url</code> 来确定从哪里获取资源，以及一个选项来决定是否需要初始获取，以及两次获取之间有没有延迟。</li>
<li>如果不设置 <code>options</code> 拥有默认值。</li>
<li>钩子返回一个类的 <code>State</code>，由消费组件通过泛型指定。</li>
<li>状态类型定义由消费组件提供的可选数据类型：如加载标志变量，或者发生错误时返回的 error。</li>
</ul>
<p>让我们看一下消费组件的使用情况：</p>
<pre><code class="language-ts">// src/pages/AnimeDetail.tsx

const { data, loading, error } = useFetch&lt;JikanAPIResponse&lt;RawAnimeData&gt;&gt;(
  getAnimeFullById(Number(id))
);
</code></pre>
<ul>
<li><code>getAnimeFullById</code> 返回终端的 URL。</li>
<li><code>useFetch</code>会返回 <code>JikanAPIResponse</code>类型的<code>data</code>，根据情况不同，返回的数据也不同。在我们的示例中为 <code>RawAnimeData</code>。</li>
</ul>
<h2 id="conclusion">总结</h2>
<p>本文探索了 TypeScript 能够解决的一些常见痛点。特别是当你和团队一起工作，并且完全理解每一个组件的输入和输出、钩子以及上下文，TypeScript 非常有用。</p>
<p>使用 TypeScript 意味着代码更加可靠、记录更完善以及可读性更强。同时也更不容易出错并且更好管理。</p>
<p>编写代码不仅仅是创建一个有效的算法。你也和别人一起工作（即便你是独立开发者，你也要发表你的作品，寻求他人的帮助和协作），在这些场景中，组员之间的沟通是关键。</p>
<p>我喜欢将 TypeScript 类比为人类的 Babel：我们通过 Babel 来优化 CPU 的使用，同时也需要一个类似的 Babel 来指导和扩展团队合作。</p>
<p>还剩下一个问题，<strong>什么时候需要使用 TypeScript</strong>?</p>
<ul>
<li>如果你和别人协作，或者你计划发表代码，你可能希望代码可读性更好，更能够清晰表达你的想法。</li>
<li>如果你在编写一个大型项目。</li>
</ul>
<p>所有大项目都是由小项目组成，所以注意这里“大型”的定义。</p>
<p>这篇文章相当长，如果你读到这里，我对你的付出和热情表示感谢。我的初衷并不是文章的曝光，而是解释清楚为什么。</p>
<p>希望你喜欢这篇文章，如果你已经从 JS 转换成 TS，或者两个都在使用，或者思考过是否使用但是暂时不考虑以及其他任何情况——<strong>我非常期待听到你的分享</strong>。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 语义化 HTML 指南——div 的十种替换方案 ]]>
                </title>
                <description>
                    <![CDATA[ 如果你的 HTML 布局是这个样子，请举手： <body>   <div class="header" id="site-header">     <div class="site-nav">       <ul>         <li><a href="/">Home</a></li>         <li><a href="/">About</a></li>         ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/semantic-html-alternatives-to-using-divs/</link>
                <guid isPermaLink="false">636bb2d6c8777b07d5fd1b6a</guid>
                
                    <category>
                        <![CDATA[ 无障碍 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ HTML ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ PapayaHUANG ]]>
                </dc:creator>
                <pubDate>Wed, 09 Nov 2022 04:19:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/11/root-font-size-article-image.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/semantic-html-alternatives-to-using-divs/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">Semantic HTML Guide – 10 Alternatives to Using divs</a>
      </p><!--kg-card-begin: markdown--><p>如果你的 HTML 布局是这个样子，请举手：</p>
<pre><code class="language-html">&lt;body&gt;
  &lt;div class="header" id="site-header"&gt;
    &lt;div class="site-nav"&gt;
      &lt;ul&gt;
        &lt;li&gt;&lt;a href="/"&gt;Home&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href="/"&gt;About&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href="/"&gt;Contact&lt;/a&gt;&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="content-wrap"&gt;
    &lt;div class="intro"&gt;
      This is the introduction to the site, which is full of divs.
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="container"&gt;&lt;/div&gt;
&lt;/body&gt;
</code></pre>
<p>大部分人在搭建 HTML 布局的时候会默认使用 <code>&lt;div&gt;</code> 元素。大多数人会把精力放在更有趣的 CSS 或者 JavaScript。</p>
<p>使用 <code>&lt;div&gt;</code> 元素在搭建页面的时候很简单，但是在后期会造成一些麻烦。</p>
<h2 id="div">仅使用<code>div</code>带来的问题</h2>
<p>使用 <code>&lt;div&gt;</code> 元素本身并没有什么问题。存在即合理。</p>
<p>但大量地使用 DIV 会给你和你的合作伙伴带来麻烦。</p>
<h3 id="">易读性问题</h3>
<p>如果你尝试去看别人的代码，或者是几个月后回看自己的代码，如果整个页面只有 <code>&lt;div&gt;</code>元素，很难快速浏览。</p>
<p>你得花费更多地时间去解构页面，这或许会成为你效率的绊脚石。仅是寻找某一个代码块的结束 <code>&lt;/div&gt;</code> 标签都会花费你大量时间。</p>
<h3 id="">无障碍问题</h3>
<p>遵循无障碍（a11y）原则是实践，不单是考虑颜色、对比和字幕。根据 WHO（联合国卫生组织）的统计结果：全世界约有 2.85 亿人口遭遇视觉损伤，其中 0.39 亿失明，2.46 亿视力低下。</p>
<p>这就是 HTML 应该无障碍的原因，也就是使用语义化代码。</p>
<p>屏幕阅读器需要通过上下文来判断网页的准确信息。 对于屏幕阅读器来说，<code>&lt;div&gt;</code>和<code>&lt;span&gt;</code>这类元素没有任何意义； 而<code>&lt;form&gt;</code>和 <code>&lt;button&gt;</code>这类语义化的元素更好解析。</p>
<h3 id="">一致性问题</h3>
<p>在团队工作中，如果你知道你即将在项目中担任什么角色，你的工作效率会更高，代码中的问题也会越少。</p>
<p>把使用语义化 HTML 设置为一个标准可以使任何刚加入的人马上了解网页布局。</p>
<p>另外，当你开始使用 CSS 来调整 HTML 样式的时候，你会发现语义化的 HTML 的元素更容易被选定。</p>
<h3 id="seo">SEO 问题</h3>
<p>使用语义化标记时，搜索引擎会将这些内容视为重要的关键字，以此来提高页面的搜索排名。 <a href="https://developer.mozilla.org/en-US/docs/Glossary/Semantics">(MDN 网页文档)</a></p>
<p>何为语义化 HTML？</p>
<p>我找到最<a href="https://www.w3schools.com/html/html5_semantic_elements.asp">清晰的定义</a> 是：</p>
<blockquote>
<p>语义元素清楚地向浏览器和开发人员描述了它的含义。</p>
</blockquote>
<p>使用语义化 HTML 带来的改变很像这样的场景：当你指向天空一个物品大呼：“快看！有一个物品”和“快看！有一架飞机”。</p>
<p>清晰描述日常物品使得日常交流更加容易，而语义化 HTML 使得读取代码更加容易。</p>
<p>实际上，语义化 HTML 已经被当作 <a href="https://html.spec.whatwg.org/multipage/grouping-content.html#the-div-element">HTML5 标准</a>：</p>
<blockquote>
<p>鼓励开发者将 <a href="https://html.spec.whatwg.org/multipage/grouping-content.html#the-div-element">div</a> 元素作为备选方案，当实在没有别的元素可以使用的时候再使用。使用比 <code>div</code> 元素更适合的元素可以使网页的无障碍性更好，使开发者更易维护代码。</p>
</blockquote>
<p>举一个例子，哪一个更方便阅读：</p>
<pre><code class="language-html">&lt;div class="quote" id="twain-quote"&gt;
  "Get your facts first, then you can distort them as you please." – Mark Twain
&lt;/div&gt;
</code></pre>
<p>或者</p>
<pre><code class="language-html">&lt;blockquote&gt;
  "Get your facts first, then you can distort them as you please." – Mark Twain
&lt;/blockquote&gt;
</code></pre>
<p>在上面两个例子中，我们看到了 <code>&lt;blockquote&gt;</code> 元素，我们很容易就理解到元素内的文字应该被当作引用来处理格式。</p>
<p>如果使用<code>div</code>可能需要进一步判断，但使用语义化 HTML 总不会错。</p>
<h2 id="htmldiv">HTML 中<code>div</code>的替换元素</h2>
<p>让我们来看看一些常用的<code>div</code> 替换元素。你很有可能见过这些元素，我们将在这里讲解它们存在的原因以及如何使用。</p>
<h3 id="nav"><nav>元素</nav></h3>
<pre><code class="language-html">&lt;nav&gt;
  &lt;ul&gt;
    &lt;li&gt;&lt;a href="/"&gt;Home&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href="/"&gt;About&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href="/"&gt;Contact&lt;/a&gt;&lt;/li&gt;
  &lt;/ul&gt;
&lt;/nav&gt;
</code></pre>
<p>nav 元素正如其名，是用来标记一系列导航连接的元素。</p>
<p>如上文所述，这个标记可以帮助屏幕阅读器判断是否现在就展示所属内容。<code>nav</code> 元素的最佳实践是 HTML 文档中的导航链接代码块。</p>
<h3 id="main"><main> 元素</main></h3>
<pre><code class="language-html">&lt;main&gt;
  &lt;h1&gt;The Godfather of All Content&lt;/h1&gt;
  &lt;h2&gt;The Wedding&lt;/h2&gt;
  &lt;p&gt;
    Why did you go to the police? Why didn't you come to me first? Vito, how do
    you like my little angel? Isn't she beautiful? Only don't tell me you're
    innocent. Because it insults my intelligence and makes me very angry. I see
    you took the name of the town. What was your father's name? The hotel, the
    casino. The Corleone Family wants to buy you out.
  &lt;/p&gt;
&lt;/main&gt;
</code></pre>
<p>和 <code>&lt;nav&gt;</code>类似，这个元素也正如其名（语义化派上了用场）。该元素将页面或者文档的主要内容打包成一个块级元素。main 元素位于两个<code>&lt;body&gt;</code>标签之间。</p>
<h3 id="section"><section> 元素</section></h3>
<pre><code class="language-html">&lt;section&gt;
  &lt;h1&gt;The Best Sandwich Ever&lt;/h1&gt;
&lt;/section&gt;
&lt;section&gt;
  &lt;p&gt;
    The best sandwich is a mutton, lettuce and tomato, where the mutton is nice
    and lean. It's so perky, I love that.
  &lt;/p&gt;
&lt;/section&gt;
</code></pre>
<p><code>&lt;section&gt;</code>元素是取代<code>div</code>来区分不同内容绝佳的例子。</p>
<p>在上面的例子中，我们将一段介绍和段落开头区分成两个 section，这样和<code>&lt;div&gt;</code>类相比在 CSS 文档中就更容易找到这些 section。</p>
<h3 id="header"><header> 元素</header></h3>
<pre><code class="language-html">&lt;header&gt;
  &lt;img src="/" id="logo" /&gt;
&lt;/header&gt;
</code></pre>
<p><code>&lt;header&gt;</code> 元素和<code>&lt;head&gt;</code> 不同，你可以在文档内多次使用。</p>
<p>例如，你可以使用一对<code>&lt;header&gt;</code> 元素来放置一个 logo，以及另一组对 header 来介绍特定的内容，如 article（之后会详细说明）。</p>
<h3 id="footer"><footer> 元素</footer></h3>
<pre><code class="language-html">&lt;footer&gt;
  &lt;p&gt;© 2021 All rights reserved. Don't steal.&lt;/p&gt;
  &lt;p&gt;Contact: &lt;a href="mailto:jiffy@jiffysites.com"&gt;Email Jiffy!&lt;/a&gt;&lt;/p&gt;
&lt;/footer&gt;
</code></pre>
<p>和 <code>&lt;header&gt;</code> 元素一样，你可以在 HTML 文档中的任意地方使用 <code>&lt;footer&gt;</code> 元素。</p>
<p><code>&lt;footer&gt;</code> 的典型案例是展示版权和作者信息，也可以使用 footer 元素来结束 <code>&lt;section&gt;</code> 元素。</p>
<h3 id="div">一些不那么常见的 <code>div</code>替换</h3>
<p>还有一些元素你可能见过，或者只见过一两次。但是当你需要的时候，它们很有用，学习它们可以使得你的代码的易读性更好。</p>
<h3 id="aside"><aside> 元素</aside></h3>
<pre><code class="language-html">&lt;p&gt;
  My favorite TV show of all time is The Muppet Show. It's sweet, funny and
  brilliant.
&lt;/p&gt;
&lt;aside&gt;
  &lt;h3&gt;The Muppet Show&lt;/h3&gt;
  &lt;p&gt;The Muppet Show was created by Jim Henson and aired from 1976 – 1981.&lt;/p&gt;
&lt;/aside&gt;
</code></pre>
<p>在影视和戏剧作品中，aside 是一种戏剧手法，角色会跳脱出主要的对话，和观众进行交流。</p>
<p>我们在 HTML 中也可以这样使用 </p><aside> 元素。我们对一个内容做注解，注解和主题内容区分。我们可以把它放置在侧边栏。<p></p>
<h3 id="code"><code> 元素</code></h3><code>
<pre><code class="language-html">&lt;p&gt;
  You can use this for a piece of code such as:
  &lt;code class="gray-code"&gt;const muppetFrog = 'Kermit'&lt;/code&gt;, which looks
  different from the other text.
&lt;/p&gt;
</code></pre>
<p>如果想要将代码和普通文本区分开来，可以使用 <code>&lt;code&gt;</code> 元素。上面示例在浏览器的渲染结果（添加了一点 CSS 样式）如下：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/07/code-display.png" alt="code-display" width="600" height="400" loading="lazy"></p>
<h3 id="article"><article> 元素</article></h3>
<pre><code class="language-html">&lt;article class="all-muppets"&gt;
  &lt;h1&gt;Muppets&lt;/h1&gt;
  &lt;article class="kermit"&gt;
    &lt;p&gt;Kermit is the Muppet leader.&lt;/p&gt;
  &lt;/article&gt;
  &lt;article class="fozzy"&gt;
    &lt;p&gt;Fozzy is a stand-up bear.&lt;/p&gt;
  &lt;/article&gt;
  &lt;article class="piggy"&gt;
    &lt;p&gt;Don't mess with Miss Piggy.&lt;/p&gt;
  &lt;/article&gt;
&lt;/article&gt;
</code></pre>
<p><code>&lt;article&gt;</code> 元素也是用来和其他元素区分内容的。</p>
<p><code>&lt;article&gt;</code>自成一体，和主内容区分开来。使用这个元素可以更容易使用 CSS 将它与页面其他内容区分开来。</p>
<h3 id="blockquote"><blockquote> 元素</blockquote></h3>
<p><code>&lt;blockquote&gt;</code> 简单明了，将引文和其他文本区分开来，如上文所示。</p>
<h3 id="mark"><mark> 元素</mark></h3>
<pre><code class="language-html">&lt;p&gt;
  This is a sentence about the best Muppet ever, which happens to be
  &lt;mark&gt;Pepe the King Prawn&lt;/mark&gt;.
&lt;/p&gt;
</code></pre>
<p><code>&lt;mark&gt;</code> 元素不仅可以使文本高亮，也可以使文档内容更易被理解，如下：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/07/mark-display.png" alt="mark-display" width="600" height="400" loading="lazy"></p>
<p>请在你的代码中使用这些<code>div</code>替换元素，提高你的代码的易读性。</p>
<h2 id="">总结</h2>
<p>以上只是十个 <code>div</code> 的替换元素，在 HTML 还有另一些语义化元素。</p>
<p>我们不可能在一个文档中把它们全都用了，也不需要记住这一百来个元素。 需要的时候你可以查看 MDN 文档： <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element">HTML 元素参考</a>。</p>
<p>编写语义化 HTML 是一个好习惯，越早开始越好。它提高了易读性、SEO，同时也不要忘了那些视觉障碍的人，他们会非常感谢你的体贴。</p>
<!--kg-card-end: markdown--></code></aside> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 提升你的 JavaScript 调试技能 ]]>
                </title>
                <description>
                    <![CDATA[ 几乎所有写过几行代码的软件工程师都听说过 JavaScript，在 2018 年 JavaScript 是需求量最大 [https://www.codingdojo.com/blog/7-most-in-demand-programming-languages-of-2018/]   的编程语言。 有人喜欢 [https://dev.to/gentlemanoi/why-i-love-javascript-9bg]它，有人讨厌 [https://www.reddit.com/r/webdev/comments/4jf7m0/why_is_javascript_used_extensively_and_hated_at/] 它。不管你的好恶如何，如果使用这个语言你最终必须调试它。 接下来我将分享在我自己的编程过程中给我雪中送炭的一些调试技巧。 最基本常见的技巧 小黄鸭调试法 小黄鸭调试法 [https://en.wikipedia.org/wiki/Rubber_duck_debugging] 是向任何人或者任何物品（没必要一定是对人）讲解你的问题，在这个过程中解决方法就会不那么淘 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/stepping-up-your-javascript-debugging-skills/</link>
                <guid isPermaLink="false">6360e8ad73e419079177bb88</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ PapayaHUANG ]]>
                </dc:creator>
                <pubDate>Tue, 01 Nov 2022 04:36:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/11/1_e3eGMlHCBdhS6Sv9rlEJXg.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/stepping-up-your-javascript-debugging-skills/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">https://www.freecodecamp.org/news/stepping-up-your-javascript-debugging-skills/</a>
      </p><!--kg-card-begin: markdown--><p>几乎所有写过几行代码的软件工程师都听说过 JavaScript，在 2018 年 JavaScript 是<a href="https://www.codingdojo.com/blog/7-most-in-demand-programming-languages-of-2018/">需求量最大</a> 的编程语言。</p>
<p>有人<a href="https://dev.to/gentlemanoi/why-i-love-javascript-9bg">喜欢</a>它，有人<a href="https://www.reddit.com/r/webdev/comments/4jf7m0/why_is_javascript_used_extensively_and_hated_at/">讨厌</a>它。不管你的好恶如何，如果使用这个语言你最终必须调试它。</p>
<p>接下来我将分享在我自己的编程过程中给我雪中送炭的一些调试技巧。</p>
<h3 id="">最基本常见的技巧</h3>
<h4 id="">小黄鸭调试法</h4>
<p><a href="https://en.wikipedia.org/wiki/Rubber_duck_debugging">小黄鸭调试法</a>是向任何人或者任何物品（没必要一定是对人）讲解你的问题，在这个过程中解决方法就会不那么淘气，如你所愿出现在你面前。</p>
<p>“小黄鸭”对你的项目一无所知，所以你必须描述所有事情，在描述的过程中还要验证自己的预设。理想情况下，你会突然开窍：“天呐！答案就在眼前，谢了兄弟，打扰了打扰了。”</p>
<p>而小黄鸭全程都安静如“鸭”，这就是神奇的地方！：）</p>
<h4 id="">你的老朋友——调试日志</h4>
<p>通常在需要调试的时候，你对可能出现问题的地方都有一个模糊的判断。（即便你的判断可能错得离谱，这就是另一个故事了。）如果你在你觉得可能出错的地方设置了记录（log），在之后的调试中，你可能更快接近正确答案。</p>
<p>即便没有调试的需求了，也不要马上就把你设置的打印删除，它们可能在之后的问题调试上派上用场。</p>
<p>我有充分的论据证明为什么你不需要添加调试日志，因为这些记录应该是最初开发的一部分。但是<a href="http://vasir.net/blog/programming/how-logging-made-me-a-better-developer">Erik Hazard</a>早在他的博客中说明了这一切。</p>
<p>之后多使用调试日志。</p>
<h4 id="">断点调试</h4>
<p>调试器（debugger）是一个利器 — 如果<strong>你知道如何使用</strong>。</p>
<p>也就是说：</p>
<ul>
<li>首先你要知道问题是什么</li>
<li>然后对<strong>根源（而不是症状）</strong> 有一些预判</li>
<li>设置断点去验证或者推翻假设</li>
</ul>
<p>在 JavaScript 中，你可以使用浏览器的调试工具，或者在代码中使用<code>debugger</code>关键字来强制暂停执行。</p>
<p>不要随意在这儿那儿都添加断点，使用的时候设定自己的流程并且在脑海中构建你的“暂停键”。</p>
<h3 id="">不那么常见的技巧</h3>
<h4 id="consoletable">console.table</h4>
<p>上文中我们提到了日志的重要性，而我们经常使用的代码是<code>console.log('text')</code>。但如果输出更为复杂怎么办？当然<code>console.log</code> 也可以处理数组和对象。</p>
<p>如果我告诉你，你可以通过优化格式来更快地发现错误呢？<code>console.table</code>就是这样的一种方法，示例如下图：</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/-Ek-xKZX9Bw75cKgaNGvNRQHrmWgqoQ46XKb" alt="-Ek-xKZX9Bw75cKgaNGvNRQHrmWgqoQ46XKb" width="600" height="400" loading="lazy"></p>
<p>console.table 示例</p>
<p>从图中你就可以看出这个样式要友好很多。我强烈建议你多使用这行代码，特别是遇到可遍历的变量时。</p>
<h4 id="">在事件而不是代码行设置断点</h4>
<p>让我们想象这样一个场景：一个 DOM 元素断断续续地变化，并且值是错误的，你不知道为什么会这样。到底是这 29 个函数中的哪一个在调皮捣蛋改变元素？（批注：变化 mutating 通常<a href="https://slemgrim.com/mutate-or-not-to-mutate/">不太好</a>，但这是另一个话题。）</p>
<p>这种情况可以使用 <strong>DOM change breakpoints</strong>。每当元素发生变化，栈追踪就会呈现出来，效果和你在正确的代码行添加断点一样。<a href="https://developers.google.com/web/tools/chrome-devtools/javascript/breakpoints#dom">这里</a>有详细的介绍。</p>
<h4 id="profiling">Profiling</h4>
<p>虽然你需要调试的问题可能并不是性能导向的，或许这方面有些矫枉过正，但我还是要提一句（说不定之后会变成性能问题:) ）。在<a href="https://developers.google.com/web/tools/chrome-devtools/rendering-tools/js-execution">Chrome</a>和 <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Performance/Profiling_with_the_Built-in_Profiler">Firefox</a> 的 profiler 都内置一些功能帮助你发现瓶颈，或者只是观察执行过的函数。你看，这简直就是绝佳的过度工程工具。请<a href="https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/">克制</a>对这些工具的使用。用<a href="https://answers.yahoo.com/question/index?qid=20111106222906AAUSWkm">火炮筒</a>来炮轰苍蝇会导致一些奇怪的副作用。</p>
<h3 id="">总结</h3>
<p>感谢你阅读这篇文章，希望你能够从中有所收获。和往常一样，希望你在评论中也留下一些你的魔法技巧。</p>
<h3 id="">延伸阅读</h3>
<p>除了文中引用的这些文章，这里还有一些相关话题值得你花时间阅读：</p>
<ul>
<li><a href="https://nodejs.org/en/docs/guides/debugging-getting-started/">Node 调试教程</a></li>
<li><a href="https://simpleprogrammer.com/effective-debugging/">John Sonmez 的调试指南</a></li>
<li><a href="https://amzn.to/2lC7kD3">调试它！</a></li>
<li><a href="https://amzn.to/2IrgI5t">调试：解决最难以琢磨的软件和硬件问题的 9 条不可或缺的规则</a></li>
<li><a href="https://developers.google.com/web/tools/chrome-devtools/javascript/">Chrome 调试工具</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Tools/Debugger">Firefox 调试工具</a></li>
<li>‘JSparty’ 的播客，特别是 <a href="https://overcast.fm/+Id5XDQtKY">第 30 期</a> ，文中的 `console.table`就是来自这个播客的启发</li>
</ul>
<p>原文发表在<a href="http://perigk.github.io">我的博客中</a>。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何像专家一样高效使用 Google 搜索 ]]>
                </title>
                <description>
                    <![CDATA[ 你几乎可以在互联网上搜索到任何内容，而Google是大多数人选择搜索信息的主要途径之一。 尽管频繁地使用Google，但是大部分互联网用户都不知道如何快速和高效地使用Google搜索。 可以说使用Google是一门艺术。 想要获得正确的答案，你需要提出正确的问题。想要快速地获得正确的答案，你必须知道 如何  提正确的问题。 每一个人都应该学学这个实操（how）  部分，这也是一篇教程教学目的所在。 以下是一些提示和技巧，帮助你快速、高效地寻找到问题的正确答案。 如何高效地使用Google搜索 1. 使用引号来获得“完全一致”的匹配搜获 当我们按照惯常的习惯在Google上搜索的时候，实际上我们使用的是浅搜索。 这样得到的搜索结果可能会包括你的搜索查询里没有的词。 但如果你将自己的查询请求或者问题（词汇或者短语）用引号打包，像这样 —— "你的问题"，Google就会进入深度搜索模式。这样的搜索结果会包含所有 在引号内你的搜索问题里的词汇。 当你需要在搜索结果中呈现出某个特定的词汇或者短语的时候，这个方法就十分有用。 2. 使用 site:  在特定网站搜索 如果你希 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-google-like-a-pro-10-tips-for-effective-googling/</link>
                <guid isPermaLink="false">63528b30509503074debe446</guid>
                
                    <category>
                        <![CDATA[ Google ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ PapayaHUANG ]]>
                </dc:creator>
                <pubDate>Fri, 21 Oct 2022 08:19:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/10/Group-61.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/how-to-google-like-a-pro-10-tips-for-effective-googling/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">How to Google like a Pro – 10 Tips for More Effective Googling</a>
      </p><!--kg-card-begin: markdown--><p>你几乎可以在互联网上搜索到任何内容，而Google是大多数人选择搜索信息的主要途径之一。</p>
<p>尽管频繁地使用Google，但是大部分互联网用户都不知道如何<strong>快速</strong>和<strong>高效</strong>地使用Google搜索。</p>
<p>可以说使用Google是一门艺术。</p>
<p>想要获得正确的答案，你需要提出正确的问题。想要快速地获得正确的答案，你必须知道 <strong>如何</strong> 提正确的问题。</p>
<p>每一个人都应该学学这个<strong>实操（how）</strong> 部分，这也是一篇教程教学目的所在。 以下是一些提示和技巧，帮助你快速、高效地寻找到问题的正确答案。</p>
<h2 id="google">如何高效地使用Google搜索</h2>
<h3 id="1">1. 使用引号来获得“完全一致”的匹配搜获</h3>
<p>当我们按照惯常的习惯在Google上搜索的时候，实际上我们使用的是<strong>浅</strong>搜索。 这样得到的搜索结果可能会包括你的搜索查询里没有的词。</p>
<p>但如果你将自己的查询请求或者问题（词汇或者短语）用引号打包，像这样 —— <code>"你的问题"</code>，Google就会进入深度搜索模式。这样的搜索结果会<strong>包含所有</strong>在引号内你的搜索问题里的词汇。</p>
<p>当你需要在搜索结果中呈现出某个特定的词汇或者短语的时候，这个方法就十分有用。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/08/img1.PNG" alt="img1" width="600" height="400" loading="lazy"></p>
<h3 id="2site">2. 使用 <code>site:</code> 在特定网站搜索</h3>
<p>如果你希望Google返回的搜索结果是来自某个特定的网站，仅需要在你的搜索问题前添加<code>site:</code> 。</p>
<p>当你想要在某个没有内置搜索选项的网站，或者内置搜索不太好用的网站进行搜索的时候，这个方法可以派上用场。</p>
<p>假设，你的搜索问题是 <strong>site:freecodecamp.org react</strong>， 返回的搜索结果仅会是来自<a href="https://www.freecodecamp.org/">www.freecodecamp.org</a> 关于React的话题。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/08/img2.PNG" alt="img2" width="600" height="400" loading="lazy"></p>
<h3 id="3">3. 使用<code>-</code>在搜索结果中排除某个词汇</h3>
<p>如果你不希望自己的搜索结果中出现某个词汇或者短语，可以在那个单词前添加 <code>-</code> 。</p>
<p>假设你的搜索内容是 =&gt; <strong>how to write components in React -class</strong>（如何编写React组件 -class），返回的搜索结果会不包含“class”。所以， (如果你了解一点React)，你会发现搜索结果只包含函数式组件。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/08/img3.PNG" alt="img3" width="600" height="400" loading="lazy"></p>
<h3 id="4imagesize">4. 使用<code>imagesize:</code>搜索特定大小的图片</h3>
<p>如果你想搜索特定大小的图片，可以在你的搜索查询中使用<code>imagesize:</code>标签，并且标注好以<strong>pixels（像素）</strong> 为单位的宽和高。</p>
<p>尺寸仅以 <strong>pixels</strong> 为单位 – 例如：<code>imagesize:500x500</code>的搜索结果是尺寸为 500px x 500px的图片。 所以如果搜索查询是 =&gt; "cute dog images imagesize:500x600"（可爱小狗图片 imagesize：500*600）的搜索结果是尺寸为<code>500px x 600px</code>的可爱小狗的图片。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/08/img4.PNG" alt="img4" width="600" height="400" loading="lazy"></p>
<h3 id="5filetype">5. 使用<code>filetype:</code>搜索特定文件类型</h3>
<p>如果你希望搜索的结果包含了某个特定的文件类型，如：PDF或者PPT，就可以添加<code>filetype:&lt;扩展名&gt;</code>（不需要尖括号）。 如，<strong>react tutorial filetype:pdf</strong>（react教学 filetype：pdf）的搜索结果是PDF格式的，如图：<br>
<img src="https://www.freecodecamp.org/news/content/images/2022/08/img5.PNG" alt="img5" width="600" height="400" loading="lazy"></p>
<h3 id="6">6. 在搜索中使用通配符<code>*</code></h3>
<p>如果你不确定搜索查询/问题或者忘记了其中的词汇，你可以在搜索中使用通配符<code>*</code>，Google会用相关的词汇来替换通配符。</p>
<p>例如，如果你搜索 =&gt; the * of money（钱的 *），会产生如下结果。 Google会返回一系列搜索页面，包括 "the <em>exchange</em> of money"（金钱交易）、 "the <em>use</em> of money"（金钱的用途）、 "the <em>role</em> of money"（金钱的角色）、 "the <em>psychology</em> of money"（金钱心理学）等。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/08/img6.PNG" alt="img6" width="600" height="400" loading="lazy"></p>
<h3 id="7orand">7. 使用 <code>OR</code>（或）、 <code>AND</code>（且）逻辑合并搜索</h3>
<p>如果你希望搜索结果包含两个词汇，可以在其中使用<code>AND</code> 关键字。例如，你搜索=&gt; <strong>React AND Angular</strong>， Google会返回即包含React，又包含Angular的结果。</p>
<p>同样的，如果你希望搜索结果包含两个词汇中的任意一个， 可以在其中使用 <code>OR</code> 关键字。 例如，你搜索 =&gt; <strong>React OR Angular</strong>，Google会返回包含React或者Angular的结果，甚至包含两者的结果。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/08/img7.PNG" alt="img7" width="600" height="400" loading="lazy"></p>
<h3 id="8afterbefore">8. 在两个数字间使用 <code>AFTER:</code>（在……之后）、 <code>BEFORE:</code>（在……之前） 或 <code>..</code> 来筛选结果</h3>
<p>如果你希望Google的搜索结果是发布在特定年份之后的，你可以使用<code>AFTER:</code>标签。例如，你搜索 =&gt; <strong>React tutorials AFTER:2020</strong>（React教程 AFTER：2020）会返回迟于2020年发布的结果。</p>
<p>同样的，如果添加<code>BEFORE:</code> 标签，会返回早于某个特定年份的结果。</p>
<p>你也可以搜索发布在某个年份范围内的结果，或者介于两个数字之间的任意事物。仅在两个数字之间添加 <code>..</code>即可，如果有计算单位，也可以添加计算单位。<br>
<img src="https://www.freecodecamp.org/news/content/images/2022/08/img8.PNG" alt="img8" width="600" height="400" loading="lazy"></p>
<h3 id="9related">9. 使用 <code>related:</code>查询相关网站</h3>
<p>如果你想知道和某个特定网站相关的其他网站，可以使用<code>related:</code>标签。</p>
<p>例如，你搜索 <strong>related:google.com</strong> 就会得到所有和Google类似的网站，如：Bing、Yahoo、DuckDuckGo等。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/08/img9.PNG" alt="img9" width="600" height="400" loading="lazy"></p>
<h3 id="10cachegoogle">10. 使用<code>cache:</code>查看Google对某个网站的缓存版本</h3>
<p>Google提供网站的缓存版本以提高搜索速度。想要知道Google是否缓存了某个网站，可以在网站URL前面添加<code>cache:</code>。</p>
<p>这一招对于<strong>web开发</strong>来说非常管用，可以检查现在浏览的到底是Google缓存的网站，还是提交了一些修改之后的最近版本的网站。 例如，在写这篇教程的时候（2022年8月4日）搜索 =&gt; <strong>cache:www.sohamderoy.dev</strong> ，得到的结果如下：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/08/img10a.png" alt="img10a" width="600" height="400" loading="lazy"></p>
<p>你可以看到，即便我是在2022年8月4日搜索这个网站，我得到的搜索结果是Google缓存的2022年8月3日的版本。</p>
<p>正如<a href="https://support.google.com/websearch/answer/2466433?visit_id=637790664879774647-1036329470&amp;p=adv_pages_similar&amp;hl=en&amp;rd=1">Google</a>自己描述的一样，我们必须知道“不是所有的搜索运算都会穷举所有结果。”</p>
<p>但我仍相信以上的方法可以帮助你提高使用Google搜索的效率。我希望我的解释足够清晰，并且在日后你能够将这些方法运用在日常的Google搜索中。</p>
<h2 id="">总结</h2>
<p>感谢阅读！希望你喜欢这篇关于提高Google搜索效率的简短教程。</p>
<p>希望你可以转发给自己的朋友——我将非常感谢。你可以在LinkedIn和Twitter上关注我的最新精彩内容！</p>
<h3 id="">社交媒体链接</h3>
<ul>
<li><a href="https://www.linkedin.com/feed/">LinkedIn</a></li>
<li><a href="https://www.sohamderoy.dev/">Website</a></li>
<li><a href="https://blogs.sohamderoy.dev">我的其他博文</a></li>
<li><a href="https://twitter.com/_sohamderoy">Twitter</a></li>
</ul>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
