大冬天的非常冷，打工人很辛苦，需要一杯奶茶暖暖肚子。
今天周五，坐在工位上发呆，刷着掘金突然想起来很久没来自己的小站看看了。看到几个月以前写的web3学习笔记，当时奔着高薪、远程办公学的，还花了钱买棵，现在也已经放弃了（web3感觉岗位少、容易跑路），继续做开发的话不如继续做前端或者学习一下python更加靠谱。现在这份工作并不开心，也对未来充满了迷茫。努力工作买辆摩托车，一路骑到西藏，新疆，云南，还抽空考了摩托车驾照。去这些地方的路很不好骑，天气也十分恶劣，当然我也没去。是时候实现这些目标了，思考了很久，决定今年结束或者明年年初辞职去做一些自己想做的事情。
到目前为止都很基础，暂时还没有强度web3学习笔记(三)12.映射（Mapping）和地址（Address）12.1.Addresses （地址）以太坊区块链由account组成，把他理解为银行账户，每个账户都有地址，这是账户的唯一标识符。例如：0x0cE446255506E92DF41614C46F1d6df9Cc96918312.2.Mapping（映射）这个和其他语言的概念是一样的：// 地址 到 uint类型变量的映射,映射名 accountBalance mapping (address => uint) public accountBalance; // 反过来的例子 mapping (uint => string) userIdToName;13.Msg.sender在 Solidity 中，有一些全局变量可以被所有函数调用。 其中一个就是 msg.sender，它指的是当前调用者（或智能合约）的 address。一个合约只会在区块链上什么也不干，除非说有人调用了合约中的某些方法（函数）。所以说msg.sdner总是存在的。mapping (address => uint) favoriteNumber; function setMyNumber(uint _myNumber) public { // 更新favoriteNumber这个映射来将_myNumber储存在msg.sender名下 favoriteNumber[msg.sender] = _myNumber; } function whatIsMyNumber() public view returns (uint) { // 拿到储存在address(地址)下的值 return favoriteNumber[msg.sender]; }这个例子中，任何人都能调用setMyNumber在我们合约的地址下存入一个uint。然后可以调用whatIsMyNumber返回储存的uint。14.Requirerequire使得函数在执行过程中，当不满足某些条件时抛出错误，并停止执行。这个就很类似try catch或者说return了。function sayHi(string _name) public returns (string) { // 在solidity中，是没有办法直接比较字符串的，所以要用之前学到的keccak256转换成一个256位的16进制数字比较 require(keccak256(_name) == keccak256("zs")); // 当_name的值为 "zs"的时候才能返回 "Hi!"，不然就抛出错误停止执行 return "Hi!" }所以说，在函数中使用require做前提判断是非常有必要的。15.继承（Inheritance）在一个合约中，如果写了太多的代码，后期管理维护起来非常困难，所以我们要想办法把他们整理好，分散到多个合约中。contract Father { function sayHi() public returns (string) { return "Hi!"; } } // 由于Son是从Father那里继承的，所以当编译部署了Son后，他可以访问Father中的公共方法（Son也能说Hi） contract Son is Father{ function sayHello() public returns (string) { return "Hello!"; } }16.引入（Import）当有多个文件的时候，就可以使用import把一个文件引入另一个文件，这个就不多说了，太熟悉了。// father.sol contract Father { function sayHi() public returns (string) { return "Hi!"; } }// son.sol import "./father.sol"; contract Son is Father{ function sayHello() public returns (string) { return "Hello!"; } }17.Storage与MemoryStorage指的是永久存在区块链中的变量。Memory变量是临时的，我把它理解为存在内存中的，当外部函数对某合约调用完成的时候，这个变量就移除了。大多数的时候，都用不到这些关键字，默认情况下Solidity会自动处理他们。contract SandwichFactory { struct Sandwich { string name; string status; } Sandwich[] sandwiches; function eatSandwich(uint _index) public { // 定义了 mySandwich 数据数据，放到链上 Sandwich storage mySandwich = sandwiches[_index]; mySandwich.status = "Eaten!"; // 定义了另一个变量 anotherSandwich，放在内存中 Sandwich memory anotherSandwich = sandwiches[_index + 1]; anotherSandwich.status = "Eaten!"; } }18.更多关于函数可见性除 public 和 private 属性之外，Solidity 还使用了另外两个描述函数可见性的修饰词：internal（内部） 和 external（外部）。总结一下这几个的区别：public可以在合约内部和外部访问自动生成一个同名的getter函数其他合约和外部账户都可以读取该变量private只能在定义它的合约内部访问派生合约不能访问外部合约不能访问internal只能在定义它的合约内部和派生合约中访问外部合约不能访问external只能从合约外部调用不能在合约内部直接调用（除非使用this.function()）对于接收大量数据的函数，使用external可以更节省gascontract AccessModifierExample { // 状态变量 uint public publicVar; // 任何人都可以读取 uint private privateVar; // 只有本合约可以访问 uint internal internalVar; // 本合约和继承的合约可以访问 // uint external externalVar; // 错误！状态变量不能是external // 函数 function publicFunction() public { // 可以调用合约内的任何函数 privateFunction(); internalFunction(); // 调用external函数需要用this this.externalFunction(); } function privateFunction() private { // 只能在当前合约内部调用 } function internalFunction() internal { // 可以在当前合约和继承合约中调用 } function externalFunction() external { // 只能从合约外部调用，或通过this.externalFunction() } }19.与其他合约的交互如果我们写的合约需要和区块链上的其他的合约交互的话，则需先定义一个interface (接口)。假如链上有这样一个合约：contract LuckyNumber { // 定义了一个映射 地址=>uint mapping(address => uint) numbers; function setNum(uint _num) public { // 把一个存入一个num numbers[msg.sender] = _num; } function getNum(address _myAddress) public view returns (uint) { // 根据_myAddress获取存入的num return numbers[_myAddress]; } }然后定义一个interface：// 这个形式看起来很像一个合约，但是里面只是定义了要使用的函数，而且没有函数体，没有逻辑 contract NumberInterface { function getNum(address _myAddress) public view returns (uint); }使用这个interface：contract MyContract { // 假设NumberInterfaceAddress的地址是这个0xab38... address NumberInterfaceAddress = 0xab38...; // NumberInterface(NumberInterfaceAddress)就能获取到要用的那个合约对象，用JS的来说就是那个合约实例，然后把实例给numberContract NumberInterface numberContract = NumberInterface(NumberInterfaceAddress); // 还是用JS来说，就是用实例上的方法了哈哈哈 // 但是要注意的就是，那个方法必须是public或者external function someFunction() public { uint num = numberContract.getNum(msg.sender); } }20.处理多返回值和JS不同的地方，JS中的函数只能return一个值，要想返回多个值一般用对象包一层，但是在Solidity中，函数可以返回多个值：// 可以看到，每个返回值都要定义类型 function multipleReturns() internal returns(uint a, uint b, uint c) { return (1, 2, 3); } // 这样写就会报错，不会自动一一对应上的，需要我们按照顺序写类型 function multipleReturns() internal returns(string a, uint b, uint c) { return (1, "2", 3); }我们可以这样处理返回的多个值：function processMultipleReturns() external { uint a; uint b; uint c; // 这样来做批量赋值，说白了也就是一个个接受，注意顺序 (a, b, c) = multipleReturns(); } // 或者如果我们只想返回其中一个变量: function getLastReturnValue() external { uint c; // 可以对其他字段留空: (,,c) = multipleReturns(); }21.if条件判断在Solidity中的if基本和JS中差不多：function eatBLT(string sandwich) public { // 还是那个道理，Solidity中不能直接比较字符串，要比较字符串就只能比较字符串的 keccak256 哈希码 if (keccak256(sandwich) == keccak256("BLT")) { eat(); } }
主要是学习solidity基础web3学习笔记(二)1.合约Solidity 的代码都包裹在合约里面， 一份合约就是以太应币应用的基本模块， 所有的变量和函数都属于一份合约。// 表示代码所使用的软件许可(license) // SPDX-License-Identifier: MIT // 表示使用的Solidity版本，这里`^0.8.21`表示的是不小于0.8.21版本 pragma solidity ^0.8.21; // contract创建合约，并命名为`HelloWeb3` contract HelloWeb3{ // 声明了一个字符串，赋值为`Hello Web3!` string public _string = "Hello Web3!"; }2.状态变量和整数上一步，我们做了一个合约的外壳，下面学习怎么使用变量状态变量是被永久地保存在合约中。也就是说它们被写入以太币区块链中，想象成写入一个数据库。contract Example { // 这个无符号整数将会永久的被保存在区块链中 // 定义了a的类型为uint，赋值100 uint a = 100; }2.1.uintuint 无符号数据类型， 指其值不能是负数。对于有符号的整数存在名为 int 的数据类型。uint实际上是unint256的简写，所以我们后面也会用到unint8，unint16等等。3.数学运算在 Solidity 中，数学运算很直观明了，与其它程序设计语言相同:加法: x + y减法: x - y,乘法: x * y除法: x / y取模 / 求余: x % y (例如, 13 % 5 余 3, 因为13除以5，余3)乘方: x**ypragma solidity ^0.4.19; contract HelloWeb3 { uint a = 16; uint b = 10 ** a; }4.结构体如果需要更加复杂的数据类型，就需要使用到结构体，个人感觉类似JS中或者其他语言中的类和对象。struct Person { uint age; string name; }5.数组跟其他语言一样，solidity也有数组，不过分两种：静态数组和动态数组。// 固定长度为2，类型为uint uint[2] array1; // 固定长度为5，类型为string string[5] array2; // 动态数组，可以动态添加元素 uint[] array3; // 甚至还有机构体数组 Person[] array4; 5.1.公共数组可以使用public修饰数组，其他合约可以从这个数组读取数据，但是不能写入数据。struct Person { uint age; string name; } Person[] public people;6.定义函数// 这里变量用了_做开头，但这个不是语法要求，是一种习惯 function foo(string _name, uint _age){ }7.使用结构体和数组struct Person { uint age; string name; } Person[] public peopleList; // 创建一个新的Person: Person person1 = Person(172, "zs"); // 将新创建的person1添加进people数组: peopleList.push(person1);当然也可以把创建和push合为一步peopleList.push(Person(172,"zs"))结合一下上面写的函数，定义一个函数，函数里面创造了一个People，然后在peopleList的尾部加入这个People。function foo(string _name, uint _age){ // 创建的People的参数由函数传入 peopleList.push(Person(_age,_name)) }8.私有/公共函数在合约中定义的函数默认为public的，也就是公共的。这就意味着其他的合约也可以调用这个函数，这样是不安全的。所以要有一个习惯，这个函数确定是公共的才把他设置为public。公共的对立面就是私有，那么如何定义一个私有的函数呢？uint[] numbers; // 在函数名字后面使用private关键字，就能把他设置为私有 // 私有函数的名字用_开头，这是一种习惯 function _addToArray(uint _number) private { numbers.push(_number); }9.函数的更多属性9.1.返回值// 函数如果有返回值，有语法规定 // returns 定义了返回值的类型为 string string str = "hello"; function sayHello() public returns (string) { return str; }9.2函数的修饰符上面的sayHello没有改变任何状态，他只是返回了str，这种情况下我们可以把函数定义为view，这个意思就是他只能读取状态而不能改变状态。function sayHello() public view returns (string) { return str; }此外，还有另一个修饰符pure，这个表示函数没有读取也没有修改状态。function _multiply(uint a, uint b) private pure returns (uint) { return a * b; }10.Keccak256 和 类型转换10.1.Keccak256// 有一个方法是把一个字符串转换为一个256位的16进制数字 keccak256("aaab"); //6e91ec6b618bb462a4a6ee5aa2cb0e9cf30f7a052bb467b0ba58b8748c00d2e5不过需要注意的是，在区块链中安全地产生一个随机数是很困难的事情，这个方法是不安全的。10.2.类型转换uint8 a = 5; uint b = 6; // 这样写会报错，因为a * b返回的是uint。 // 这个类似TS中的类型报错一样 uint8 c = a * b; // 转换一下b的类型，这样就可以了 uint8 c = a * uint8(b);11.事件事件是合约和区块链通讯的一种机制。前端要监听某些事件然后做出处理。// 使用event建立事件 event AddEvent(uint x, uint y, uint res); function add(uint _x, uint _y) public { uint res = _x + _y; // 触发事件 AddEvent(_x, _y, res); return res; }// 然后前端就可以监听这个事情，做出一些处理 YourContract.AddEvent(function(err,res)){ }
亲测非常好用，Vue2和Vue3都可以使用，本文演示Vue3的用法Vue中强大的图片预览组件，v-viewr,支持缩放、旋转等操作作者网站：传送门1.创建demo# 创建一个vue3项目，取名v-viewer npm create vue@latest # 进入目录 cd .\v-viewer\ # 安装依赖 npm i # 安装v-viewer npm install v-viewer@next viewerjs创建以后，清空一下App.vue中默认的组件以及main.ts中引入的样式，方面我们后面操作。然后创建一个组件用来测试效果// App.vue <TestViewer /> // main.ts import { createApp } from 'vue' import App from './App.vue' const app = createApp(App) app.use(VueViewer) app.mount('#app')// components/TestViewer.vue 2.全局引入v-viewer引入v-viewer及必需的css样式，并使用app.use()注册插件，之后即可使用。有三种使用方法：1.组件；2.指令；3.api引入以后这三种方式都能够使用。// main.ts import { createApp } from 'vue' import App from './App.vue' // 引入viewer的样式 import 'viewerjs/dist/viewer.css' import VueViewer from 'v-viewer' const app = createApp(App) // 使用viewer app.use(VueViewer) app.mount('#app')3.使用组件形式和指令形式一行，都会展示图片列表3.1.组件形式// components/TestViewer.vue // 直接使用全局注册的组件<viewer> <viewer :images="images"> </viewer> 3.2.指令形式// components/TestViewer.vue // 在一个div上使用v-viewer即可 3.3.api形式这种形式可以不用展示出图片列表，例如下图，可以点击按钮唤起预览。要是用这种形式，使用setup就不方便了，因为要使用this。不使用this也可以，那就不能全局引入了。下面演示两种形式：// components/TestViewer.vue // 全局引入，使用this调用api Click to show 非全局引入，去掉main.ts中的app.use(VueViewer) Click to show 4.配置项以上基本就可以满足绝大部分需求了，如果要更加的定制化的话，建议把产品经理打一顿。更多的配置项要自己去viewerjs慢慢看了。
纯理论，我都听睡着了。第一天，通过视频来了解区块链原理，B站视频：传送门1.从比特币到区块链比特币是电子现金系统，不依赖于第三方而实现点对点的线上交易区块链是保障比特币可以运行的底层技术区块链解决的最重要的问题是交易各方之间的信任问题2.区块链如何运行2.1.区块与区块链区块：一段时间的账单区块链：账本2.2.账户体系公钥和私钥私钥：只有用户知道的秘钥，用来签名交易公钥：哈希处理后的用户标识符2.3.多人竞争记账为了决定谁有权记录新区块，许多区块链使用工作量证明(PoW)机制：矿工们竞争解决复杂的数学难题（找到使区块头哈希值小于特定目标的随机数）第一个解决问题的矿工获得奖励2.4.UTXO模型未花费交易输出(UTXO)模型是比特币等区块链使用的账户模型：每笔交易都消耗之前交易的输出并创建新的输出钱包余额是所有未花费输出的总和，而不是直接记录账户余额交易输入必须完全消耗，多余部分作为找零返回这种模型有助于提高隐私性和并行处理能力2.5.”双花“和最长链原则双花是指同一笔资金被花费两次的尝试：区块链通过共识机制防止双花链出现分叉的时候，网络认可最长的链作为权威版本交易通常需要等待多个确认才会视为最终确认，防止短期分叉导致的双花2.6.区块链分类公有链：任何人都可以参与，完全去中心化，如比特币、以太坊联盟链：由预选的组织共同维护，部分去中心化，如Hyperledger Fabric私有链：由单一实体控制，中心化程度高，但提供更好的性能和隐私保护3.区块链的骨骼与灵魂3.1.非对称加密函数RSA原理：基于大数分解的困难性，使用两个大质数的乘积作为密钥的基础ECC原理：基于椭圆曲线上的离散对数问题3.2.哈希函数哈希函数特性：单向性：无法从哈希值反推原始数据确定性：相同输入产生相同输出雪崩效应：输入微小变化导致输出显著不同抗碰撞性：难以找到产生相同哈希值的两个不同输入梅克尔树:结构：二叉哈希树，叶节点是数据块的哈希值，非叶节点是其两个子节点哈希值的哈希工作原理：交易被哈希为叶节点相邻的哈希值对被组合并再次哈希，形成上一层节点此过程重复直到获得单一根哈希（梅克尔根）优势：快速验证：可以仅使用梅克尔根验证区块完整性高效证明：轻节点可以通过梅克尔证明（约log₂(n)个哈希）验证交易包含在区块中节省存储空间：轻节点无需存储完整区块数据区块指针：概念：每个区块通过存储前一个区块的哈希值形成链式结构3.3.拜占庭容错问题拜占庭将军问题：问题描述：多个将军需要协调一致的进攻或撤退决策，但通信信道不可靠且将军中可能有叛徒拜占庭容错（BFT）：定义：系统能够在部分节点出现任意故障（包括恶意行为）的情况下继续正常工作理论限制：容忍f个拜占庭故障需要至少3f+1个节点3.4.主流的共识机制PoW（工作量证明）：原理：矿工通过解决计算难题（寻找特定哈希值）来获得创建新区块的权利PoS（权益证明）：原理：节点根据持有代币的数量和时间获得创建新区块的概率DPoS（委托权益证明）：原理：代币持有者投票选出有限数量的代表（通常21-101个）来验证交易和创建区块PBFT（实用拜占庭容错）：原理：通过多轮投票达成共识，适用于已知验证者身份的系统4.没看下去，太理论了不知道在讲什么，有时间慢慢看
面试造轮子，工作用轮子0.前言今天参加了一场面试，面试官问了一个问题：如何实现SSR？给我问的有点懵，因为以往都是借助现有的框架比如：Nuxt或者Next来做SSR，自己确实不知道如何实现SSR。面试的时候这个问题没有答好，心里面虽然知道是怎么一回事，但是没有办法表达出来，归根结底还是没有手动实现过。1.创建项目安装依赖注意，node版本要在18以上，不然有可能会出问题。首先新建一个项目目录react-ssr-videos，初始化项目：npm init -y安装依赖：npm install react react-dom express npm install --save-dev @babel/core @babel/preset-env @babel/preset-react babel-loader webpack webpack-cli webpack-node-externals nodemon npm-run-all css-loader style-loader安装了一堆东西，简单的来说一下这些都是干什么的，其实不用太过在意，因为现在也不可能让你手动实现SSR，我们只要了解这整个过程就好。核心的依赖，虽然说是手动实现SSR，但是我也不想从原生JS开始写起，还是要用到React的：react - React库的核心包，提供创建组件和管理组件状态的功能。react-dom - 提供DOM特定的方法，包括renderToString（服务器端渲染）和hydrateRoot（客户端水合）。express - Node.js的web应用框架，用于创建HTTP服务器和API路由。node-fetch - 在服务器端使用与浏览器fetch API兼容的网络请求工具。其余的都是些构建编译的东西，感觉确实很复杂：webpack - JavaScript应用的静态模块打包工具，负责将项目代码转换成浏览器可运行的格式。webpack-cli - Webpack的命令行工具，允许通过命令行运行webpack。webpack-node-externals - Webpack插件，防止将node_modules打包进服务器端代码中。@babel/core - Babel的核心包，负责将现代JavaScript代码转换为向后兼容的版本。@babel/preset-env - Babel预设，根据目标环境自动确定需要的转换。@babel/preset-react - Babel预设，添加对JSX语法的支持。@babel/register - 允许在Node.js环境中直接运行使用ES模块和JSX的代码。babel-loader - Webpack加载器，使Webpack能够使用Babel进行代码转换。nodemon - 开发工具，监视文件变化并自动重启Node.js应用。npm-run-all - 允许并行或顺序运行多个npm脚本。style-loader - 将CSS插入到DOM中。css-loader - 解析CSS文件中的@import和url()。2.创建配置文件首先我们需要两个webpack的配置文件，因为我们实际上要为这个应用构建两个不同的包，一个用于服务端，也就是node，一个用于客户端，也就是浏览器。可能会有这个问题：都已经在服务端处理好了，为什么还要为客户端打一个包呢？试想一下，如果不给客户端打包，也就是客户端只有页面没有JS，那么客户端就没有办法交互：点击按钮没反应，输入字符也没有反应。。。。所以说我们不仅仅要有服务端的包，也需要客户端的包，通俗点来讲就是：也就是打包一份JS给客户端。都说到这了，那就顺便提一下，水合的过程：浏览器加载服务器渲染的HTML（用户立即看到页面内容）浏览器加载并执行客户端JavaScript bundleReact通过hydrate函数"接管"已存在的HTML节点React将事件处理器附加到这些DOM节点React构建内部的组件树并与已渲染的HTML匹配此时，页面看起来没有变化，但已经变成了可交互的应用OK，回归正题，把配置完成：// webpack.client.js - 客户端Webpack配置 const path = require('path'); module.exports = { entry: './src/client/index.js', output: { path: path.resolve(__dirname, 'public'), filename: 'bundle.js', publicPath: '/' }, mode: 'development', module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env', '@babel/preset-react'] } } }, { test: /\.css$/, use: ['style-loader', 'css-loader'] } ] } };// webpack.server.js - 服务端Webpack配置 const path = require('path'); const nodeExternals = require('webpack-node-externals'); module.exports = { entry: './src/server/index.js', output: { path: path.resolve(__dirname, 'build'), filename: 'server.js', publicPath: '/' }, target: 'node', externals: [nodeExternals()], mode: 'development', module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env', '@babel/preset-react'] } } }, { test: /\.css$/, use: ['css-loader'] } ] } };// .babelrc - Babel配置 { "presets": [ "@babel/preset-env", "@babel/preset-react" ] }// package.json { "name": "react-ssr-video-list", "version": "1.0.0", "description": "React SSR视频列表应用", "main": "index.js", "scripts": { "dev:build-client": "webpack --config webpack.client.js --watch", "dev:build-server": "webpack --config webpack.server.js --watch", "dev:server": "nodemon ./build/server.js", "dev": "npm-run-all --parallel dev:*", "build:client": "webpack --config webpack.client.js", "build:server": "webpack --config webpack.server.js", "build": "npm-run-all --parallel build:*", "start": "node ./build/server.js" }, "dependencies": { "express": "^4.17.1", "node-fetch": "^2.6.1", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@babel/core": "^7.15.0", "@babel/preset-env": "^7.15.0", "@babel/preset-react": "^7.14.5", "babel-loader": "^8.2.2", "css-loader": "^6.7.1", "style-loader": "^3.3.1", "nodemon": "^2.0.12", "npm-run-all": "^4.1.5", "webpack": "^5.50.0", "webpack-cli": "^4.8.0", "webpack-node-externals": "^3.0.0" } }3.创建React组件使用开放接口https://api.apiopen.top/api/getMiniVideo?page=0&size=10，这个接口是用来获取短视频列表的，现成的可以直接使用，然后我们就这个接口来做一个视频列表页面。// src/client/components/VideoList.js import React, { useState } from 'react'; import './VideoList.css'; const VideoList = ({ videos: initialVideos, currentPage: initialPage = 0, total }) => { const [videos, setVideos] = useState(initialVideos || []); const [currentPage, setCurrentPage] = useState(initialPage); const [loading, setLoading] = useState(false); const pageSize = 10; // 计算总页数 const totalPages = Math.ceil(total / pageSize); // 加载新的一页视频 const fetchVideos = async (page) => { setLoading(true); try { const response = await fetch(`/api/videos?page=${page}`); const data = await response.json(); setVideos(data.videos); setCurrentPage(page); // 更新URL但不刷新页面 const url = new URL(window.location); url.searchParams.set('page', page); window.history.pushState({}, '', url); } catch (error) { console.error('获取视频失败:', error); } finally { setLoading(false); } }; // 前一页 const handlePrevPage = () => { if (currentPage > 0 && !loading) { fetchVideos(currentPage - 1); } }; // 后一页 const handleNextPage = () => { if (currentPage < totalPages - 1 && !loading) { fetchVideos(currentPage + 1); } }; return ( 迷你视频列表 共 {total} 个视频 {loading && 加载中... } {!loading && videos.length === 0 ? ( 没有找到视频 ) : ( videos.map(video => ( {video.title} {video.alias} )) )} <button onClick={handlePrevPage} disabled={currentPage <= 0 || loading} className="pagination-button" > 上一页 第 {currentPage + 1} 页 / 共 {totalPages} 页 = totalPages - 1 || loading} className="pagination-button" > 下一页 ); }; export default VideoList;// src/client/components/App.js import React from 'react'; import VideoList from './VideoList'; const App = ({ initialData }) => { return ( <VideoList videos={initialData.videos} currentPage={initialData.currentPage} total={initialData.total} /> ); }; export default App;/* src/client/components/VideoList.css */ .video-container { max-width: 1200px; margin: 0 auto; padding: 20px; font-family: "Helvetica Neue", Arial, sans-serif; background-color: #f5f5f5; } .video-header { display: flex; align-items: center; justify-content: space-between; padding: 20px 0; border-bottom: 1px solid #eee; } .video-title { margin: 0; color: #1a73e8; font-size: 24px; } .video-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; margin: 20px 0; } .video-card { background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 10px rgba(0,0,0,0.1); transition: transform 0.2s; } .video-card:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(0,0,0,0.15); } .video-thumbnail { position: relative; height: 0; padding-bottom: 56.25%; overflow: hidden; } .video-image { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; } .video-info { padding: 12px; } .video-name { margin: 0 0 8px 0; font-size: 16px; font-weight: bold; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .video-author { margin: 0; font-size: 14px; color: #666; display: flex; align-items: center; } .author-avatar { width: 24px; height: 24px; border-radius: 50%; margin-right: 8px; } .video-pagination { display: flex; justify-content: center; align-items: center; padding: 20px 0; } .pagination-button { background: #1a73e8; color: white; border: none; padding: 8px 16px; margin: 0 10px; border-radius: 4px; cursor: pointer; font-weight: bold; } .pagination-button:disabled { background: #ccc; cursor: not-allowed; } .page-info { font-size: 14px; } .loading { text-align: center; padding: 20px; grid-column: 1 / -1; } .no-videos { grid-column: 1 / -1; text-align: center; padding: 50px; font-size: 18px; color: #666; }4.客户端入口文件和服务端文件// src/client/index.js import React from 'react'; import { hydrateRoot } from 'react-dom/client'; import App from './components/App'; // 从window获取服务器注入的初始数据 const initialData = window.__INITIAL_DATA__; // 使用hydrateRoot而不是render，这会复用服务器已渲染的DOM hydrateRoot( document.getElementById('root'), <App initialData={initialData} /> ); // 记录水合完成，用于调试 console.log('客户端水合(hydration)完成 - 应用已可交互');// src/server/index.js import express from 'express'; import React from 'react'; import { renderToString } from 'react-dom/server'; import path from 'path'; import App from '../client/components/App'; const app = express(); const PORT = process.env.PORT || 3000; // 提供静态文件 app.use(express.static('public')); // API端点 - 视频列表 app.get('/api/videos', async (req, res) => { const page = parseInt(req.query.page) || 0; try { // 使用Node.js内置的fetch API替代node-fetch const response = await fetch(`https://api.apiopen.top/api/getMiniVideo?page=${page}&size=10`); const data = await response.json(); if (data.code === 200 && data.result) { res.json({ videos: data.result.list, total: data.result.total, currentPage: page }); } else { throw new Error('API返回格式错误'); } } catch (error) { console.error('API错误:', error); res.status(500).json({ error: '获取视频数据失败' }); } }); // HTML模板函数 function htmlTemplate(reactApp, initialData) { return ` 迷你视频列表 - SSR示例 ${reactApp} `; } // 服务器端渲染 - 视频列表页 app.get('/', async (req, res) => { const page = parseInt(req.query.page) || 0; try { // 使用Node.js内置的fetch API替代node-fetch const response = await fetch(`https://api.apiopen.top/api/getMiniVideo?page=${page}&size=10`); const data = await response.json(); if (data.code !== 200 || !data.result) { throw new Error('API返回格式错误'); } // 准备初始数据 const initialData = { videos: data.result.list, total: data.result.total, currentPage: page }; // 渲染React组件 const reactApp = renderToString( <App initialData={initialData} /> ); // 发送完整HTML res.send(htmlTemplate(reactApp, initialData)); } catch (error) { console.error('渲染错误:', error); res.status(500).send(` 错误 服务器错误 抱歉，无法加载视频数据，请稍后再试。 刷新页面 `); } }); // 处理其他所有路由，重定向到首页 app.get('*', (req, res) => { res.redirect('/'); }); // 启动服务器 app.listen(PORT, () => { console.log(`服务器运行在 http://localhost:${PORT}`); });5.项目目录react-ssr-videos/ ├── src/ │ ├── client/ │ │ ├── components/ │ │ │ ├── App.js # 主应用组件 │ │ │ ├── VideoList.js # 视频列表组件 │ │ │ └── VideoList.css # 视频列表样式 │ │ └── index.js # 客户端入口文件 │ └── server/ │ └── index.js # 服务器入口文件 ├── public/ # 客户端构建输出目录 ├── build/ # 服务器构建输出目录 执行npm run build就会产生了 ├── webpack.client.js # 客户端webpack配置 ├── webpack.server.js # 服务器webpack配置 ├── .babelrc # Babel配置 └── package.json # 项目依赖和脚本6.核心讲解如何在服务端渲染React组件的？服务器端渲染React组件的核心是通过renderToString函数。这是React DOM服务器包中的一个函数，专门用于将React组件转换为HTML字符串。// src/server/index.js import { renderToString } from 'react-dom/server';这个函数接收React元素作为参数，并返回表示该组件的HTML字符串：// src/server/index.js const reactApp = renderToString(<App initialData={initialData} />);服务端如何获取数据？还是通过调接口的，那就要问了：接口的参数怎么来的？当我们访问http://localhost:3000/?page=1的时候，很明显带了page=1，那么服务端就可以从这里获取到参数。app.get('/', async (req, res) => { const page = parseInt(req.query.page) || 0; // ... });完整的流程app.get('/', async (req, res) => { try { // 1. 解析请求参数 const page = parseInt(req.query.page) || 0; // 2. 获取外部数据 const response = await fetch(`https://api.apiopen.top/api/getMiniVideo?page=${page}&size=10`); const data = await response.json(); // 3. 准备组件初始数据 const initialData = { videos: data.result.list, total: data.result.total, currentPage: page }; // 4. 使用renderToString渲染React组件 const reactApp = renderToString(<App initialData={initialData} />); // 5. 将组件HTML和初始数据注入HTML模板 const html = ` 视频列表 ${reactApp} `; // 6. 发送完整HTML响应 res.send(html); } catch (error) { // 错误处理 res.status(500).send('服务器错误'); } });7.总结1. 服务器渲染阶段当用户访问网站时：接收请求：服务器接收到HTTP请求数据获取：服务器从API获取视频数据组件渲染：使用renderToString将React组件树转换为HTML字符串数据注入：将初始数据作为全局变量(window.__INITIAL_DATA__)注入到HTML中发送响应：将完整的HTML发送给浏览器这使得用户可以立即看到页面内容，而不需要等待JS加载和执行。2. 客户端接管阶段当浏览器加载页面后：加载JS：浏览器加载并执行bundle.js获取初始数据：从window.__INITIAL_DATA__获取服务器注入的数据水合过程：React使用hydrateRoot接管已有的DOM节点，而不是重新创建绑定事件：React添加事件处理器和其他客户端功能应用可交互：页面变得完全可交互，如分页按钮现在可以使用
看完通关TS，不做标题党0.准备安装环境# 全局安装TS npm i typescript -g # 检查TS版本 tsc -v # 安装tsc-node npm i tsc-node -g # 生产TS配置文件 tsc --init1.基础类型字符串类型let a: string = 'asdf'数字类型JS中任意数字类型都没问题。let notANumber: number = NaN;//Nan let num: number = 123;//普通数字 let infinityNumber: number = Infinity;//无穷大 let decimal: number = 6;//十进制 let hex: number = 0xf00d;//十六进制 let binary: number = 0b1010;//二进制 let octal: number = 0o744;//八进制s布尔类型let flag: boolean = true注意一点的是使用new Boolean()会报错，因为new Boolean()结果是一个对象。// 不能将类型“Boolean”分配给类型“boolean”。 // “boolean”是基元，但“Boolean”是包装器对象。如可能首选使用“boolean”。 let flag: boolean = new Boolean(1)空值类型用来表示一个函数没有返回值。function fn(): void { console.log('log') }null和undefined类型undefined表示声明了没有赋值，null表示声明以后赋值了空值。let u: undefined = undefined;//定义undefined let n: null = null;//定义null可以把undefined赋值给void：let u: void = undefined非严格模式下，也可以把null赋值给void，但是严格模式下不行：// 严格模式下会出现报错提示： // 不能将类型“null”分配给类型“void”。 let n: void = null;非严格模式下null和undefined可以赋值给任何类型，但是在严格模式下null只能赋值给null或者any类型；undefined只能赋值undefined，void，或者any类型。let n1: any = null let n2: null = null let n3: undefined = null // 报错 let u1: any = undefined; let u2: null = undefined; // 报错 let u3: undefined = undefined;2.any和unknow所有的类型都可以赋值给any类型，any类型也可以，是完全跳过了TS的检查。let anyValue: any = 10; let str1: string = anyValue; 所有类型也都可以赋值给unkonw类型，但是unknown 类型的值只能赋值给 unknown 或 any 类型。let unknownValue: unknown = "hello"; let str2: string = unknownValue; // 错误：不能将类型 'unknown' 分配给类型 'string'any 可以直接操作，不进行类型检查。console.log(anyValue.length); // 允许（即使可能运行时出错） anyValue.callSomeMethod(); // 允许（即使可能运行时出错）unknown 不能直接操作。console.log(unknownValue.length); // 错误：对象的类型为 'unknown' unknownValue.callSomeMethod(); // 错误：对象的类型为 'unknown'3.接口和对象类型在TS中，我们用来约束对象的类型主要用的就是interface（接口）。//这样写是会报错的 因为我们在person定义了a，b但是对象里面缺少b属性 //使用接口约束的时候不能多一个属性也不能少一个属性 //必须与接口保持一致 interface Person { b: string, a: string } // 报错提示少了b属性 const person: Person = { a: "213" }重名的interface会进行合并：interface Person { a: string, } interface Person { b: number, } // 最终Person接口定义的类型是： // interface Person { // a: string // b: number, // }也可以可以使用extends继承：interface A { a: string, } interface B extends A { b: number } // 最终B的定义的接口是 // interface B { // a: string, // b: number // }可以使用?来定义这个属性为可选属性，意思就是有没有都行：interface A { a: string, b?: number } // 并没有报错提示说少了b属性，因为b是可选的 let a: A = { a: '123' }有一种情况就是我们不知道会对这个对象添加什么属性，这个时候可以使用任意属性：// 我们定义了[propName: string]: any; // 允许添加新的任意属性 interface Test { a?: string, b: string, [propName: string]: any; } // 我们添加了c属性和d属性都没有出现报错 const obj: Test = { a: "213", b: "123", c: 'asdfasdf', d: true } // 需要注意的是，一旦定义了任意属性，那么确定属性和可选属性的类型都必须是它的类型的子集 // 我们定义了[propName: string]: boolean; interface Test { a?: string, b: string, [propName: string]: boolean; } // 这个时候c属性会报错，因为string并不是boolean的子集 const obj: Test = { a: "213", b: "123", c: 'asdfasdf', d: true }当我们不希望有一些属性会被更改，就在属性前面加上readonly来表示这个属性可以获取，但是不能改：interface Test { readonly a: string, } const obj: Test = { a: "213", } // 会有报错提示：无法为“a”赋值，因为它是只读属性。 obj.a = 123为这个对象添加一个方法可以这样写：// 用到了前面说过的void，表示这个函数没有返回值 interface Person { a?: string, toEat: () => void } const person: Person = { a: "213", toEat: () => { console.log(123) } }4.数组类型用来限制数组的类型类型[ ]// 自由发挥，例如arr1就表示数字类型的数组 let arr1: number[] = [123] // 字符串类型数组 let arr2: string[] = ['1', '2', '3', '1'] // any类型的数组 let arr3: any[] = [1, 2, 3,] // 定义好类型以后，不能放入其他的类型 let arr4: number[] = [1, 2, 3, '4'] // 会报错，因为有一个字符串 arr4.push('5')// 也会报错，不能通过方法加入不符合类型的元素数组泛型关于泛型，后面还会再提到。let arr1: Array<number> = [1, 2, 3, 4, 5] let arr2: Array<string> = ['1', '2', '3', '4', '5'] let arr3: Array<boolean> = [true, false, true]用接口表示数组interface NumberArray { [index: number]: number; } let arr: NumberArray = [1, 1, 2, 3, 5]; //表示：只要索引的类型是数字时，那么值的类型必须是数字。arguments类数组function Arr(...args: any[]): void { // args是一个数组，打印可以看到结果 let arr1: any[] = args console.log(arr1) // 但是arguments不是一个数组，它是一个类数组对象，这样写会报错的 let arr2: any[] = arguments console.log(arr2); // 我们应该使用IArguments，这个是TS内置的可以直接使用 let arr3: IArguments = arguments console.log(arr3); } Arr(111, 222, 333)5.函数类型// 和定义对象的接口有点像，不能多传，也不能少传 const fn = (name: string, age:number): string => { return name + age } fn('张三',18) // 也可以通过?来表示这个参数是可选的 const fn2 = (name: string, age?: number): string => { return name + age } fn2('张三', 18) // 还可以给定默认值，给定了默认值不传也不会报错 const fn3 = (name: string = 'zs'): string => { return name } fn3() 还可以使用interface来定义函数：//定义参数 num 和 num2 ：后面定义返回值的类型 interface Add { (num: number, num2: number): number } const fn: Add = (num: number, num2: number): number => { return num + num2 } fn(5, 5) interface User { name: string; age: number; } function getUserInfo(user: User): User { return user }有的时候不知道后面会传入几个参数，可以使用...items:any[]：const fn = (array:number[],...items:any[]):any[] => { console.log(array,items) return items } let a:number[] = [1,2,3] fn(a,'4','5','6')函数重载：先声明多个函数签名（不包含实现），然后提供一个实现签名（通常使用更宽泛的类型）来处理所有情况。// 这里看不懂没有关系，后面会说typeof的使用方法 // 这段代码大概得意思就是说，a和b可能是string，也有可能是number // 当a: number, b: number的时候返回值就是number // 当a: string, b: string的时候返回值就是string // 当a: string, b: number或者a: number, b: string会直接报错提示，因为没有这样的签名 // 函数重载签名 function add(a: number, b: number): number function add(a: string, b: string): string // 实现签名（必须兼容所有重载签名） function add(a: number | string, b: number | string) { if (typeof a === "number" && typeof b === "number") { return a + b } if (typeof a === "string" && typeof b === "string") { return a + b } } // 使用： const num = add(1, 2); // 返回类型是 number const str = add("Hello", "World"); // 返回类型是 string6.联合类型 | 交叉类型联合类型就像是“或”的关系：// 这就表示了a的类型是string或者number，反正两种都行 let a: string | number = '123' a = 123同理，交叉类型像是“且”的关系，寻找两种类型之间的交叉：// 要注意，必须要有公共交叉部分，不然就是unknow类型 let a: string & number // a的类型为unknow interface A { a: string } interface B { b: number } // obj的类型为： // interface Obj { // a: string, // b: number // } let obj: A & B = { a: '123123', b: 123123 }7.类型断言类型断言就是告诉TS，我确定这个是什么类型，你不要想太多：interface A { run: string } interface B { build: string } const fn = (type: A | B): string => { // 这样写是有警告的，因为type是A或者B类型，但是B类型上没有run方法 return type.run } const fn2 = (type: A | B): string => { // 我们可以使用类型断言告诉TS，我确定传进来的一定是A类型，你不要管我 return (type as A).run }假如说有一个数组，数组里面的元素可以随意改变，我们又不想让他改变可以怎么做呢？// 使用 as const let arr = [112, 23, 5456,] as const // 报错提示 无法为“0”赋值，因为它是只读属性。 arr[0] = 7898.Class类TS可以对类进行约束：// 先写一个VueCls准备试下实现Vue类 interface Options { el: string | HTMLElement } interface VueCls { options: Options init(): void } // 然后写上以下代码，应该会有提示 // implements:实现，实施 class Vue implements VueCls { }鼠标一点就能自动的实现了，但是还是会有报错的提示的，因为类中options没有赋值，我们需要手动写constructor来完成赋值：interface Options { el: string | HTMLElement } interface VueCls { options: Options init(): void } class Vue implements VueCls { options: Options // 或者通过!操作符告诉TS这个值一定会有，你不要担心，这样不赋值也不会报错 // options!: Options constructor(options: Options) { this.options = options } init(): void { throw new Error("Method not implemented.") } }类的3个修饰符：public默认就是public// 其实默认就是public class Person { public name: string; public constructor(name: string) { this.name = name; } } const person = new Person("Alice"); console.log(person.name); // 可以访问private成员仅限当前类内部访问，子类和外部均不可访问。class Person { private age: number; constructor(age: number) { this.age = age; } getAge() { return this.age; // 类内部可以访问 } } const person = new Person(30); // console.log(person.age); // 错误：属性"age"为私有属性，只能在类"Person"中访问protected成员允许在当前类和子类中访问，外部不可访问。class Person { protected age: number; constructor(age: number) { this.age = age; } } class Employee extends Person { getAge() { return this.age; // 子类中可以访问 } } const employee = new Employee(25); // console.log(employee.age); // 错误：属性"age"受保护，只能在类"Person"及其子类中访问readonly属性只能在声明时或构造函数中初始化，之后不可修改。class Person { readonly name: string; constructor(name: string) { this.name = name; } changeName() { // this.name = "Bob"; // 错误：无法分配到"name"，因为它是只读属性 } }staticclass MathHelper { static PI: number = 3.14159; static calculateCircumference(radius: number): number { return 2 * MathHelper.PI * radius; } } console.log(MathHelper.PI); // 通过类名访问静态属性abstract// 要注意的是abstract抽象方法只能出现在abstract抽象类中 // 意思就是方法前面加了abstract修饰符，那么类前面也要加abstract abstract class Animal { abstract makeSound(): void; // 抽象方法，没有实现 move(): void { console.log("Moving..."); } } class Dog extends Animal { makeSound() { console.log("Woof!"); } } // const animal = new Animal(); // 错误：无法创建抽象类的实例 const dog = new Dog(); dog.makeSound(); // "Woof!"9.元组个人理解其实也是一个数组，不过在定义的时候限制了数组的元素的个数以及类型。// 这是一个普通的数组，arr的类型为(string | number)[] let arr = [1223, '123213'] // arr2就可以说是一个元组，限制了数组的类型和个数[string, number, boolean] let arr2: [string, number, boolean] = ['123123', 123123, true] // 元组可以越界，但是越界也会受到类型的限制 arr2.push(false) // 可以 arr.push({ a: 123 }) // 会有报错提示：类型“{ a: number; }”的参数不能赋给类型“string | number”的参数。 // 还可以给每个类型加上tag，看上去会更清晰，比如： let arr3: [name: string, age: number] = ['zs', 18] // 可以获取对应tag的类型，比如： type A = typeof arr3[0] // 获取到类型为string type B = typeof arr3[1] // 获取到类型为number // 可以决定那个元素是可选的 let arr4: [name: string, age?: number] = ['zs'] // 不写第二个元素不会报错10.枚举// number类型的枚举 enum Week { Monday = 1, Tuesday, Wensday, ThirsDay, Friday, Sarturday, Sunday } // 可以看的出数字枚举TS会自动推断出Friday的值为5。 console.log(Week.Friday);// 5 // 数字枚举不仅可以通过key取值，还能通过值取key console.log(Week[5]);// Fridayenum Week { Monday = "MyMonday", Tuesday = "MyTuesday", Wensday = "Wensday", ThirsDay = "ThirsDay", Friday = "Friday", Sarturday = "Sarturday", Sunday = "sunday" } // 字符串类型的枚举不能推断出值 console.log(Week.ThirsDay); // ThirsDay // 字符串类型枚举不能通过值取key console.log(Week["MyMonday"]); // 会报错11.类型推论|类型别名类型推论是说TS有能力推断出这个变量的类型是什么let a = '123' // 推断出a的类型是string a = 123 // 这个时候就会报错了，因为不能把number赋给string类型类型别名就是说给一个类型命名，在类型比较复杂的时候会用到// 假如说我们有一些数据 let a: string | number = 1 let b: string | number = 2 let c: string | number = 3 // 每次都要重复写 string | number 就会很烦 // 用一个TypeData来把string | number装起来，方便使用 type TypeData = string | number let e: TypeData = 4 let d: TypeData = 5 let g: TypeData = '6'// extends在type中表示包含 // 下面的代码表示1是否作为number的子类型 type num = 1 extends number ? 1 : 012.nevernever 类型在 TS中表示永远不会出现的值的类型。它有几个重要的使用场景和应用：// 函数一运行就抛出错误，这样的就是never function throwError(message: string): never { throw new Error(message); } // 无限循环也是never function infiniteLoop(): never { while (true) { // 无限循环 } }// 在switch的判断中，用来兜底的 // 这样做的好处：可以尝试一下，如果说添加了一个d，但是忘记改fn中的逻辑，那么就会有报错提示， type A = 'a' | 'b' | 'c' function fn(value:A) { switch (value) { case "a": break case "b": break case "c": break default: //是用于场景兜底逻辑 const error:never = value; return error } }13.symbol类型symbol类型的值是通过Symbol构造函数创建的。// 就算参数一致，构造出来的symbol也是不一样的 const sym1 = Symbol('value') const sym2 = Symbol('value') // 严格模式下会出现报错提示：此比较似乎是无意的，因为类型“typeof sym1”和“typeof sym2”没有重叠。 // 这证明了sym1必不可能和sym2相等 console.log(sym1 === sym2); symbol类型的数据经常用作对象的key：// 就算参数一致，构造出来的symbol也是不一样的 const sym = Symbol('value') let obj = { [sym]: 123 } console.log(obj[sym]); // 123对象中symbol类型的key是没有办法通过遍历拿到的：const sym = Symbol('value') let obj = { [sym]: 123 } // 控制台并不会输出任何东西 for (const key in obj) { console.log(key); }那么我们如何才能拿到呢？// Object.getOwnPropertySymbols() const symKeys = Object.getOwnPropertySymbols(obj) console.log(symKeys); // 控制台有输出// Reflect.ownKeys()，可以拿到所有的key，包括symbol类型的key const objKeys = Reflect.ownKeys(obj) console.log(symKeys);14.泛型泛型也是一种数据类型，有以下特点：特点一：定义时不明确使用时必须明确某种具体数据类型的数据类型。特点二：编译期间进行数据类型检查的数据类型。// 定义这个接口的时候，并不知道value属性是什么类型，就用一个参数代替 interface Ref<T> { value: T } // 定义的时候不明确，但是使用的时候就要明确，需要传入一个类型 let ref: Ref<string> = { value: "string" }再举个例子为什么要这么用呢？// 假如有这样两个方法 // 观察发现这两个方法几乎一模一样，参数和返回值的类型不一样 // 那有没有什么好的方法只写一个方法就能解决呢？ const fn1 = (a: string, b: string): string[] => { return [a, b] } const fn2 = (a: number, b: number): number[] => { return [a, b] } // 那么这个时候泛型的优势就凸显出来了 // 这里其实还用到了反向推断泛型，自动推出了T的类型为number // 不反推的话也可以这样用 let arr = fn3<number>(1, 2) const fn3 = <T>(a: T, b: T): T[] => { return [a, b] } let arr = fn3(1, 2)更多的例子：// 类型别名使用泛型 type A<T> = string | number | T // 接口使用泛型 interface B<T> { a: T } // 函数使用泛型，除了上面用到的箭头函数，也可以写关键词function定义的函数 function fn<T>(a: T, b: T) { return [a, b] }15.泛型约束// 这样写肯定会报错的，因为有无法相加的可能出现，比如undefined + undefined const fn = <T>(a: T, b: T) => { return a + b // 会出现报错提示 } // 这个泛型也太泛了，需要我们对他进行约束，保证可以相加 // 在泛型中，extends就表示左边的类型是右边类型的子集 const fn2 = <T extends number>(a: T, b: T) => { return a + b } // 再举个例子 interface Len { length: number } //这样做的目的就是保证T里面有length这个属性 const fn3 = <T extends Len>(data: T) => { return data.length }16.keyofkeyof 操作符接收一个对象类型，并返回由该类型的所有键组成的字符串或数字字面量联合类型：// 使用typeof来得到这个obj的类型，鼠标悬浮可以查看到 type A = { name: string; age: number; id: number; } // B类型实际是 'name' | 'age' | 'id' type B = keyof Akeyof常常与泛型结合，特别是创建通用类型安全的函数时候：// 写一个获取对象属性值的函数 // K的类型收到约束，必须是 keyof T的子集 function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } // 这里推出了T的类型为 // { // name: string; // age: number; // address: string; //} // 那么K的类型就为 'name'|'age'|'address' const person = { name: "zs", age: 30, address: "Wonderland" }; // 类型安全的属性访问 const name1 = getProperty(person, "name"); // 类型为 string const age = getProperty(person, "age"); // 类型为 number // 错误：参数'"job"'不能赋给类型'"name" | "age" | "address"' // getProperty(person, "job"); 17.namespace命名空间先说一下这个功能用的比较少了，大部分都是用ES 模块了，但是还是可以了解一下的。比如说我们在做一些跨端项目，需要比较清晰的分类方法：// 假如我们做一个项目，里面又有H5的方法，又有安卓的方法，又有IOS的方法 namespace H5 { // 需要注意，必须使用export导出这个方法才能在namespace外面使用 export const h5Fn = () => { console.log('H5方法'); } } namespace Android { export const androidFn = () => { console.log('Android方法'); } } namespace IOS { export const iosFn = () => { console.log('IOS方法'); } } namespace Test1 { // 命名空间也是可以嵌套的，同样也需要导出 export namespace Test2 { export const testFn = () => { console.log('Test2方法'); } } } H5.h5Fn() Android.androidFn() // 调用嵌套命名空间的方法 Test1.Test2.testFn() // 还有一些规则，命名空间也是可以想interface一样合并的，可以自行测试18.模块解析AMD，CMD奇奇怪怪的自行了解吧，我们现在常用的模块规范CommonJS还有ESM(ES6模块化)。// CommonJS // 导出 //------------------- // math.js module.exports = { add: function(a, b) { return a + b; }, multiply: function(a, b) { return a * b; } }; // 或者单独导出 exports.subtract = function(a, b) { return a - b; }; //------------------- // 导入 const math = require('./math'); math.add(2, 3); // 5// ESM // 导出 //------------------------ // math.js export function add(a, b) { return a + b; } export function multiply(a, b) { return a * b; } // 默认导出 export default function divide(a, b) { return a / b; } //------------------------ // 导入 import { add, multiply } from './math.js'; import divide from './math.js'; // 导入所有 import * as math from './math.js';他们俩之间的不同点：语法// CommonJS使用 require()引入 module.exports导出 // ESM使用import引入, export导出加载时机// CommonJS是运行的时候同步加载，整个模块对象被加载进内存 // ESM是编译时候加载，确定依赖关系，运行的时候异步加载导入类型// CommonJS是值的拷贝，如果导出的值在原模块中改变，导入的值不会变 // ESM是对变量本身的引用环境// CommonJS node.js中 // ESM 浏览器环境19.declare声明文件当我们在项目中安装了axios时候引入axios并不会出现什么报错：// 不会报错 // 按住Ctrl点击axios能进入axios的声明文件 import axios from "axios";但是当我们安装了一些比较老的库，没有声明文件，就会出现报错。例如express：// express下飘红报错了 import express from 'express'遇到这种情况，可以根据提示中执行npm i --save-dev @types/express来解决，或者手写声明文件，下面来尝试手写声明文件。// 首先创建一个typings用来存放声明文件 // 然后在typings下新建一个express.d.ts// index.ts import express from 'express' const app = express() const router = express.Router() app.use('/api', router) router.get('/list', (req, res) => { res.json({ code: 200 }) }) app.listen(9001,()=>{ console.log(9001) })// express.d.ts // 基本就是哪里缺什么就补什么 declare module 'express' { interface Router { get(path: string, cb: (req: any, res: any) => void): void } interface App { use(path: string, router: any): void listen(port: number, cb?: () => void): void } interface Express { (): App Router(): Router } const express: Express export default express }20.mixin混入对象的混入const obj1 = { name: 'zs' } const obj2 = { age: 18 } // 1.使用扩展运算符 const obj3 = { ...obj1, ...obj2 } // 2.使用 Object.assign const obj4 = Object.assign({}, obj1, obj2) //注意了这两种方式实际都是浅拷贝（只有第一层的属性会脱离引用） // 题外话：如何深拷贝呢？使用 structuredClone const deepcloneObj = structuredClone(obj1)类的混入// 定义 Mixin 的构造函数类型 type Constructor<T = {}> = new (...args: any[]) => T; // 定义可飞行 Mixin function CanFly<TBase extends Constructor>(Base: TBase) { return class extends Base { fly() { console.log("Flying!"); } }; } // 定义可游泳 Mixin function CanSwim<TBase extends Constructor>(Base: TBase) { return class extends Base { swim() { console.log("Swimming!"); } }; } // 组合 Mixin class Animal { name: string; constructor(name: string) { this.name = name; } } const FlyingSwimmingAnimal = CanSwim(CanFly(Animal)); // 使用 const superDuck = new FlyingSwimmingAnimal("Duck"); superDuck.fly(); // "Flying!" superDuck.swim(); // "Swimming!" 21.装饰器首先要把tsconfig.json中的这两项配置打开（不然可泵会有报错）：然后安装tsc-node，后面编译运行就用tsc-node 文件名。npm install -g typescript@latest什么是装饰器：看例子下面这个例子说明了装饰器的好处就是节省了代码，然后也不用去破坏A的结构。// 写一个类装饰器函数，会把自动把类的构造函数传入当做第一个参数 const Watcher: ClassDecorator = (target) => { target.prototype.getParams = <T>(params: T): T => { return params } } // 用的时候使用@+函数名就可以了 @Watcher class A { constructor() { } } const a = new A() console.log((a as any).getParams('abc')); // abc装饰器工厂：说白了就是使用@Watcher的时候我想传入一个参数咋办： // 我们就给他再包一层用来接受参数，里面返回一个普通的装饰器函数 const Watcher = (value: string) => { const fn: ClassDecorator = (target) => { target.prototype.getParams = <T>(params: T): T => { return params } target.prototype.value = value } return fn } @Watcher('123') class A { constructor() { } } const a = new A() as any console.log(a.getParams('abc')); // abc console.log(a.value); // 123 进一步来看看方法装饰器：const meet = (name: string) => { const fn: MethodDecorator = (value, key, descriptor) => { // 可以打印这三个参数看看 console.log(value, key, descriptor); // value:{} 就是原型对象 // key:getName 对应的方法名 // descriptor:{ // value: [Function: getName], // writable: true, // enumerable: false, // configurable: true // } console.log('name', name); } return fn } class A { constructor() { } @meet('zs') getName(): string { return 'ls' } } // 编译过后控制台输出了zs属性装饰器：const met: PropertyDecorator = (...args) => { console.log(args); } class A { @met name: string = 'zs' constructor() { } } // 控制台输输出 [ {}, 'name', undefined ]参数装饰器：const met: ParameterDecorator = (...args) => { console.log(args); } class A { constructor() { } setParasm(@met name: string = '213') { } } // 控制台输出 [ {}, 'setParasm', 0 ] // 前面两个就不说了，0就跟表示位置，第一个0，第二个就是122.发布订阅发布订阅就是一种设计模式，是一种思想没有固定的代码。很多框架都在使用比如Vue，electron，还有一些组件通信插件eventBus，addEventListener等等。// 这个其实就是一个发布订阅，这是一个触发器 document.addEventListener('click', function () { console.log('点击了'); }); // 如何自己实现一个事件呢 document.addEventListener('abc', function () { // 控制台输出了 点击了abc console.log('点击了abc'); }); // 创建了一个事件，相当于一个订阅中心 const e = new Event('abc') // 使用dispatchEvent派发这个事件 document.dispatchEvent(e) 手写来实现发布订阅： interface I { events: Map<string, Function[]> // 事件的数组 once: (event: string, callback: Function) => void // 只触发一次的功能 on: (event: string, callback: Function) => void // 订阅 emit: (event: string, ...args: any[]) => void // 派发 off: (event: string, callback: Function) => void // 删除 } class Eimitter implements I { events: Map<string, Function[]> constructor() { this.events = new Map() } // 只触发一次的功能 once(event: string, callback: Function) { const fn = (...args: any[]) => { callback(...args) // 立马回收 this.off(event, fn) } this.on(event, fn) } on(event: string, callback: Function) { if (this.events.has(event)) { // 说明存过了 const callbackList = this.events.get(event) // 第二次存就把事件push进去 callbackList && callbackList.push(callback) } else { // 说明第一次存 this.events.set(event, [callback]) } } emit(event: string, ...args: any[]) { const callbackList = this.events.get(event) if (callbackList) { callbackList.forEach((fn) => { fn(...args) }) } } off(event: string, callback: Function) { const callbackList = this.events.get(event) if (callbackList) { // 说明有这个事件 const index = callbackList.indexOf(callback) if (index !== -1) { // 说明有这个事件 callbackList.splice(index, 1) } } } } const bus = new Eimitter() // 定义callback const fn1 = (b: boolean, n: number) => { console.log(1, b, n); } const fn2 = (b: boolean, n: number) => { console.log(2, b, n); } bus.on('message', fn1) bus.on('message', (b: boolean, n: number) => { console.log(2); }) // 取消订阅fn1 bus.off("message", fn1) bus.emit('message', false, 1) bus.once('message2', fn2) // 控制台可以看到就算触发多次也只会触发一次 bus.emit('message2', true, 3) bus.emit('message2', true, 3) bus.emit('message2', true, 3) bus.emit('message2', true, 3) bus.emit('message2', true, 3)23.类型守卫用一下这些方法，可以看出来，能够把传入的any类型的数据做一个过滤，过滤到想要的类型，这个也就是类型收缩。// typeof // typeof可以用来判断基本数据类型，但是不能用来判断引用数据类型 const isString = (val: any) => typeof (val) === 'string' console.log(isString('123')); // true console.log(isString(123)); // false // instanceof // instanceof可以用来判断引用数据类型 const isObject = (val: any) => val instanceof Object const isAarry = (val: any) => val instanceof Array console.log(isObject({ a: '123' })); // true console.log(isAarry([11, 2, 3])); // true // Array.isAarry // Array.isArray()可以用来判断数组 const isAarry2 = (val: any) => Array.isArray(val) console.log(isAarry2([12, 2, 3])); // true类型谓词：首先来看一个小题目。// 实现一个函数，该函数可以传入任何类型 // 但是如果是object 就检查里面的属性，如果里面的属性是number就取两位 // 如果是string就去除左右空格 // 如果是函数就执行 const isString = (data: any) => typeof (data) === 'string' const isNumber = (data: any) => typeof (data) === 'number' const isFunction = (data: any) => typeof (data) === 'function' // 一般写这个就是太长一串了Object.prototype.toString.call(data) === '[object Object]' // 语法糖简写 ({}).toString.call(data) === '[object Object]' const isObject = (data: any) => ({}).toString.call(data) === '[object Object]' const fn = (data: any) => { if (isObject(data)) { // 不能使用 for in 遍历，因为for in 遍历的是对象的原型链上的属性 // Object.keys(data) 返回一个数组，数组的元素是data对象的属性名 Object.keys(data).forEach(key => { let val = data[key] // data[key] 取出data对象的属性值 if (isNumber(val)) { data[key] = val.toFixed(2) } else if (isString(val)) { data[key] = val.trim() } else if (isFunction(val)) { data[key]() } }) } return data } const obj = { a: 1.123, b: ' 123 ', c: () => { console.log('fn') } } console.log(fn(obj)); 题目不难，但是在写的过程中发现一个问题：为什么已经使用了自己定义的方法收窄了类型，但是写的时候没有代码提示？而且检查发现val类型是any这个时候我们就要使用到类型谓词实现自定义守卫：// 类型谓词能让TS更好的推断变量的类型 // :data is string 如果返回的是true,那么这个data一定就是string const isString = (data: any): data is string => typeof (data) === 'string' const isNumber = (data: any): data is number => typeof (data) === 'number' const isFunction = (data: any): data is Function => typeof (data) === 'function'加上类型谓词就发现有代码提示了，而且TS能推断出类型了：23.协变和逆变协变：interface A { name: string; age: number; } interface B { name: string; age: number; sex: string; } // 可以发现B的属性完全覆盖了A的属性 let a: A = { name: 'zs', age: 1 }; let b: B = { name: 'ls', age: 18, sex: 'male' }; // 发现b可以赋值给a，但是a不能赋值给b // 再通俗一点就是a要的属性b都有，但是b要的属性a不一定有 a = b b = a // 报错逆变：interface A { name: string; age: number; } interface B { name: string; age: number; sex: string; } let fn1 = (a: A) => { console.log(a); } let fn2 = (b: B) => { console.log(b); } // 如果是一个函数的话，就刚好和协变反过来 // 这个就叫做逆变 fn2 = fn1 fn1 = fn2 // 报错24.泛型工具TS一共有内置的许多工具，其实翻译一下就很好理解了。都以对象类型为例子：// 定义了一个接口Person interface Person { name: string; age: number; sex: string; height: number; weight: number; }Partial部分的把一个对象类型所有的属性变成可选。// Partial<T>的作用是将类型T的所有属性变为可选属性 type PersonPartial = Partial<Person>; // Partial<Person>的结果是： // type PersonPartial = { // name?: string | undefined; // age?: number | undefined; // sex?: string | undefined; // height?: number | undefined; // weight?: number | undefined; // }Required必须的把一个对象类型所有的属性变成必须。// Required<T>的作用是将类型T的所有属性变为必选属性 type RquiredPerson = Required<PersonPartial>; // Required<Person>的结果是：(可以看出，使用之前PersonPartial都是可选属性的，这下又变回来了) // type RquiredPerson = { // name: string; // age: number; // sex: string; // height: number; // weight: number; // }Pick选择把一个对象类型部分属性选出来新组成一个类型。// Pick<T, K>的作用是将类型T的属性K提取出来 type PickPerson = Pick<Person, 'name' | 'age'>; // Pick<Person, 'name' | 'age'>的结果是： // type PickPerson = { // name: string; // age: number; // }Exclude排除把一个联合类型中排除掉一些类型。// Exclude<T, U>的作用是将类型T中属于类型U的属性排除掉 type ExcludePerson = Exclude<keyof Person, 'name' | 'age'>; // type ExcludePerson = "sex" | "height" | "weight" // Exclude只能用于联合类型，所以keyof Person是一个联合类型，'name' | 'age'也是一个联合类型Omit省略把一个对象类型部分属性排除掉。// Omit<T, K>的作用是将类型T中的属性K排除掉 type OmitPerson = Omit<Person, 'name' | 'age'>; // type OmitPerson = { // sex: string; // height: number; // weight: number; // }Readonly只读把一个对象类型所有的属性变成只读。// Readonly<T>的作用是将类型T的所有属性变为只读属性 type ReadonlyPerson = Readonly<Person>; // type ReadonlyPerson = { // readonly name: string; // readonly age: number; // readonly sex: string; // readonly height: number; // readonly weight: number; // }Record记录创造一个对象类型。// Record<K, T>的作用是构造一个类型，这个类型的属性名是K中的属性，属性值是T类型 type RecordPerson = Record<'name' | 'age', string>; // type RecordPerson = { // name: string; // age: string; // }ReturnType获取这个函数类型的返回值类型// ReturnType<T>的作用是获取函数类型T的返回值类型 const fn = () => { return 1 } // 这里的typeof用来获取函数的类型 type ReturenTypePerson = ReturnType<typeof fn>; // ReturnType<() => Person>的结果是： // type ReturenTypePerson = number25.inferinfer 是 TypeScript 中的一个关键字，用于在 条件类型 中推断类型。它的作用是从一个复杂类型中提取出某个部分的类型。心语语法：T extends infer U ? U : never// 推断出数组中的元素类型 type ElementType<T> = T extends (infer U)[] ? U : never; // 示例 type Numbers = number[]; type NumberType = ElementType<Numbers>; // number // 提取出出函数返回值（实现ReturnType） type ReturnType1<T> = T extends (...args: any[]) => infer R ? R : never; // 示例 function foo() { return 42; } type FooReturnType = ReturnType1<typeof foo>; // number // 提取函数参数类型 type Parameters<T> = T extends (...args: infer P) => any ? P : never; // 示例 function bar(a: string, b: number) { console.log(a, b); } type BarParams = Parameters<typeof bar>; // [string, number]
产品经理：你做一个GPT那样的打字效果出来1.获取Api KeyApi Key可以通过https://openrouter.ai获取，自行注册一个就可以免费试用了。这里我选的是Mistral 7B Instruct(free)，为了下面的步骤顺利进行，推荐和我使用一样的。然后就跳转到这个页面，再次点击创建按钮如图输入就行然后就获取到了key，注意自己保存下来2.项目代码搭建项目，我使用的是Next.js，根据个人习惯喜欢用什么框架就用什么框架。传送门：Nextjs# 执行创建Nextjs项目 npx create-next-app@latest我选择了这些配置给出项目代码可以直接食用：app/ ├── components/ │ ├── ApiKeyForm.tsx │ ├── ChatInput.tsx │ └── MessageList.tsx ├── types/ │ └── chat.ts ├── layout.tsx └── page.tsx// app/components/ApiKeyForm.tsx 'use client'; import React, { useState } from 'react'; import { Key } from 'lucide-react'; import type { ApiKeyFormProps } from '../types/chat'; export default function ApiKeyForm({ onSubmit }: ApiKeyFormProps) { const [apiKey, setApiKey] = useState(''); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (apiKey.trim()) { onSubmit(apiKey.trim()); } }; return ( <Key className="w-12 h-12 text-blue-500" /> 输入 OpenRouter API Key 请输入您的API Key以开始对话 setApiKey(e.target.value)} className="w-full p-3 border border-gray-200 rounded-lg focus:outline-none focus:border-blue-500" placeholder="sk-or-v1-..." /> 确认 ); }// app/components/ChatInput.tsx 'use client'; import React, { useState } from 'react'; import { Send, Loader2 } from 'lucide-react'; import type { ChatInputProps } from '../types/chat'; export default function ChatInput({ onSend, isLoading }: ChatInputProps) { const [input, setInput] = useState(''); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!input.trim() || isLoading) return; await onSend(input.trim()); setInput(''); }; return ( setInput(e.target.value)} placeholder="发送消息..." className="w-full p-4 pr-12 rounded-lg border border-gray-200 focus:outline-none focus:border-blue-500" disabled={isLoading} /> {isLoading ? ( <Loader2 className="w-5 h-5 animate-spin" /> ) : ( <Send className="w-5 h-5" /> )} ); } // app/components/MessageList.tsx 'use client'; import React, { useEffect, useRef } from 'react'; import { User, Bot } from 'lucide-react'; import type { MessageListProps } from '../types/chat'; export default function MessageList({ messages }: MessageListProps) { const messagesEndRef = useRef<HTMLDivElement>(null); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); if (messages.length === 0) { return ( <Bot className="w-12 h-12 mb-4" /> 开始一个新的对话 ); } return ( {messages.map(message => ( {message.role === 'assistant' && ( <Bot className="text-white w-5 h-5" /> )} {message.content} {message.isStreaming && ( ▋ )} {message.role === 'user' && ( <User className="text-gray-600 w-5 h-5" /> )} ))} ); }// app/types/chat.ts export type Message = { id: string; content: string; role: 'user' | 'assistant'; isStreaming?: boolean; }; export type ApiKeyFormProps = { onSubmit: (apiKey: string) => void; }; export type ChatInputProps = { onSend: (message: string) => Promise<void>; isLoading: boolean; }; export type MessageListProps = { messages: Message[]; };// app/layout.tsx import { Inter } from 'next/font/google' import './globals.css' // 使用 Inter 字体 const inter = Inter({ subsets: ['latin'] }) // Root Layout export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( {children} ) }// app/page.tsx 'use client'; import { useState } from 'react'; import ApiKeyForm from './components/ApiKeyForm'; import ChatInput from './components/ChatInput'; import MessageList from './components/MessageList'; import type { Message } from './types/chat'; export default function Home() { const [messages, setMessages] = useState<Message[]>([]); const [isKeySet, setIsKeySet] = useState(false); const [apiKey, setApiKey] = useState(''); const [isLoading, setIsLoading] = useState(false); const handleKeySubmit = (key: string) => { setApiKey(key); setIsKeySet(true); }; const handleSendMessage = async (content: string) => { const userMessage: Message = { id: Date.now().toString(), content, role: 'user' }; setMessages(prev => [...prev, userMessage]); setIsLoading(true); try { const response = await fetch('https://openrouter.ai/api/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, }, body: JSON.stringify({ model: 'mistralai/mistral-7b-instruct', messages: [...messages, userMessage].map(msg => ({ role: msg.role, content: msg.content })), stream: true, }), }); if (!response.ok) throw new Error('API request failed'); const reader = response.body?.getReader(); const decoder = new TextDecoder(); const messageId = Date.now().toString(); // 初始化助手消息 setMessages(prev => [...prev, { id: messageId, content: '', role: 'assistant', isStreaming: true }]); // 处理流式响应 while (reader) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split('\n').filter(line => line.trim() !== ''); for (const line of lines) { if (line.startsWith('data: ')) { const data = line.slice(6); if (data === '[DONE]') continue; try { const parsed = JSON.parse(data); const content = parsed.choices[0]?.delta?.content || ''; setMessages(prev => prev.map(msg => msg.id === messageId ? { ...msg, content: msg.content + content } : msg )); } catch (e) { console.error('Error parsing stream:', e); } } } } // 完成流式输出 setMessages(prev => prev.map(msg => msg.id === messageId ? { ...msg, isStreaming: false } : msg )); } catch (error) { console.error('Error:', error); setMessages(prev => [...prev, { id: Date.now().toString(), content: '抱歉，发生了错误。请稍后重试。', role: 'assistant' }]); } finally { setIsLoading(false); } }; if (!isKeySet) { return <ApiKeyForm onSubmit={handleKeySubmit} />; } return ( <MessageList messages={messages} /> <ChatInput onSend={handleSendMessage} isLoading={isLoading} /> ); }前端页面展示3.流式传输流式响应允许服务器以小块（chunks）的形式逐步发送数据，而不必等待整个响应准备完毕。工作流程：客户端发送请求服务器接收请求并开始处理服务器处理出一部分数据后立即发送这部分数据客户端立即开始处理这部分数据服务器继续处理并发送更多数据块这个过程持续到全部数据传输完毕在这个项目中，流式响应特别适合模拟打字的效果，具体细节在于：建立流式请求：const response = await fetch(url, { // ...其他配置 body: JSON.stringify({ // ...其他参数 stream: true, // 启用流式响应 }), });使用 Reader 接口处理流：const reader = response.body?.getReader(); const decoder = new TextDecoder(); while (reader) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); // 处理收到的数据块 }增量更新 UI：setMessages(prev => prev.map(msg => msg.id === messageId ? { ...msg, content: msg.content + newContent } : msg ));4.分析处理流式响应代码发送用户消息// 这部分创建用户消息对象，添加到消息列表，并设置加载状态。 const userMessage: Message = { id: Date.now().toString(), content, role: 'user' }; setMessages(prev => [...prev, userMessage]); setIsLoading(true);发送API请求const response = await fetch('https://openrouter.ai/api/v1/chat/completions', { method: 'POST', headers: { ... }, body: JSON.stringify({ model: 'mistralai/mistral-7b-instruct', messages: [...messages, userMessage].map(msg => ({ role: msg.role, content: msg.content })), stream: true, // 启用流式响应 }), });准备读取流数据// getReader()：获取一个可以读取流的阅读器 const reader = response.body?.getReader(); // TextDecoder：用于将二进制数据转换为文本 const decoder = new TextDecoder(); // messageId：为AI回复创建一个唯一ID const messageId = Date.now().toString();创建空的AI回复消息// 这里创建一个空消息，稍后会逐步填充内容。 setMessages(prev => [...prev, { id: messageId, content: '', // 初始内容为空 role: 'assistant', isStreaming: true // 标记为正在流式接收 }]);流式读取数据的核心循环while (reader) { const { done, value } = await reader.read(); if (done) break; // 如果读取完毕就退出循环 const chunk = decoder.decode(value); // 将二进制数据解码为文本 const lines = chunk.split('\n').filter(line => line.trim() !== ''); // 按行分割解析每个数据块for (const line of lines) { if (line.startsWith('data: ')) { const data = line.slice(6); // 移除"data: "前缀 if (data === '[DONE]') continue; // 特殊标记，表示流结束 try { const parsed = JSON.parse(data); // 解析JSON数据 const content = parsed.choices[0]?.delta?.content || ''; // 提取新内容片段流式响应会发送多个"data:"开头的行，每行包含一个JSON。格式类似：data: {"choices":[{"delta":{"content":"你"}}]} data: {"choices":[{"delta":{"content":"好"}}]}更新消息内容// 这里不是替换整个消息，而是逐步追加新内容，实现了内容"流入"的效果。 setMessages(prev => prev.map(msg => msg.id === messageId ? { ...msg, content: msg.content + content } // 追加新内容到现有内容 : msg ));完成流处理// 当全部内容接收完成后，移除流式状态标记（停止显示闪烁的光标）。 setMessages(prev => prev.map(msg => msg.id === messageId ? { ...msg, isStreaming: false } // 标记流结束 : msg ));错误处理catch (error) { console.error('Error:', error); setMessages(prev => [...prev, { id: Date.now().toString(), content: '抱歉，发生了错误。请稍后重试。', role: 'assistant' }]); }
明文传一个手机号怎么了，反正都开户了0.前言某次面试了一家银行的前端，被问到了关于前端安全方面的问题，具体就是前端如何做好加密解密，尤其是涉及到了用户信息，比如用户的手机号、身份证号之类的数据怎么处理。这确实是一个很重要的问题，尤其在金融类项目中，对数据安全性要求极高。今天就来详细讲解前端加密解密的相关知识。1.CryptoJS首先需要了解一下CryptoJS：这是一个强大的加密库，提供了多种加密算法，包括 AES、MD5、SHA 等。在前端项目中使用非常广泛。1.1 基础使用// 引入 import CryptoJS from 'crypto-js'; // 加密 function encrypt(data, key) { return CryptoJS.AES.encrypt(data, key).toString(); } // 解密 function decrypt(encryptedData, key) { const bytes = CryptoJS.AES.decrypt(encryptedData, key); return bytes.toString(CryptoJS.enc.Utf8); } // 使用示例 const key = 'secret key 123'; const text = '要加密的内容'; const encrypted = encrypt(text, key); console.log('加密后：', encrypted); const decrypted = decrypt(encrypted, key); console.log('解密后：', decrypted);1.2 进阶使用// 使用CBC模式的AES加密 const encrypted = CryptoJS.AES.encrypt('hello', 'key', { mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, iv: CryptoJS.enc.Utf8.parse('1234567890123456') }).toString(); // 对应的解密 const decrypted = CryptoJS.AES.decrypt(encrypted, 'key', { mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, iv: CryptoJS.enc.Utf8.parse('1234567890123456') }).toString(CryptoJS.enc.Utf8);1.3 其他功能// MD5哈希 const md5Hash = CryptoJS.MD5('要哈希的内容').toString(); // SHA256哈希 const sha256Hash = CryptoJS.SHA256('要哈希的内容').toString(); // Base64编码 const base64 = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse('hello'));2.AES对称加密AES是一种对称加密算法，所谓对称加密，就是加密和解密使用相同的密钥。就像一把钥匙开一把锁，这个钥匙既可以用来锁门，也可以用来开门。AES加密有几种不同的工作模式：2.1ECB模式最简单的加密模式，但安全性较低，因为相同的明文会产生相同的密文。const encryptedECB = CryptoJS.AES.encrypt('hello', 'key', { mode: CryptoJS.mode.ECB }).toString();2.2 CBC模式更安全的加密模式，需要提供一个初始向量（IV），相同的明文会产生不同的密文。const encryptedCBC = CryptoJS.AES.encrypt('hello', 'key', { mode: CryptoJS.mode.CBC, iv: CryptoJS.enc.Utf8.parse('1234567890123456') }).toString();对称加密的优点是速度快，适合加密大量数据，但缺点是密钥的传输和保管比较困难。因为如果把秘钥直接写到前端项目中，可能会有泄露的风险。在实际的项目中，比较推荐使用CBC模式，更加的安全可靠。3.RSA非对称加密RSA是一种非对称加密算法，它使用一对密钥：公钥和私钥。公钥可以公开给任何人，用于加密数据；而私钥必须严格保密，用于解密数据。3.1 基本原理使用公钥加密的数据只能用私钥解密，这就保证了数据的安全性。即使黑客获取了公钥，也无法解密数据。3.2 使用场景RSA主要用于：加密密钥的传输数字签名加密敏感信息（如密码）3.3 示例代码// 使用公钥加密 const encrypted = crypto.publicEncrypt( publicKey, Buffer.from('要加密的内容') ).toString('base64'); // 私钥解密在服务器端进行4.实际项目中的应用在实际项目中，尤其是金融类项目，通常会结合使用这两种加密方式。让我们来看几个具体的场景：（具体使用哪种方式，还是要看项目的要求，可以只使用AES，也可以只使用RSA，当然也可以两个结合一起）AES的例子就不列举了，前面列举过了。4.1 密码加密密码直接使用 RSA 加密就可以了，原因是：密码数据量小，RSA 的性能问题影响不大密码安全要求高，需要更强的加密方式只需要从客户端传到服务器，不需要双向通信使用公钥加密，私钥解密的方式非常适合密码传输// 从服务器获取公钥 async function getPublicKey() { const response = await fetch('/api/getPublicKey'); const { publicKey } = await response.json(); return publicKey; } // 加密密码 async function encryptPassword(password) { const publicKey = await getPublicKey(); return crypto.publicEncrypt( publicKey, Buffer.from(password) ).toString('base64'); }4.2 敏感信息传输对于身份证号、银行卡号等敏感信息，通常使用 AES + RSA 的组合加密方式：class SecurityService { async encryptSensitiveData(data) { // 1. 生成随机的 AES 密钥 const aesKey = CryptoJS.lib.WordArray.random(16).toString(); // 2. 用 AES 加密数据 const encryptedData = CryptoJS.AES.encrypt(JSON.stringify(data), aesKey, { mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, iv: CryptoJS.enc.Utf8.parse('1234567890123456') }).toString(); // 3. 用 RSA 加密 AES 密钥 const publicKey = await this.getPublicKey(); const encryptedKey = crypto.publicEncrypt( publicKey, Buffer.from(aesKey) ).toString('base64'); // 4. 返回加密后的数据和密钥 return { data: encryptedData, key: encryptedKey }; } }
准备找工作了，希望一切顺顺利利。（顺便展示一下自己）