Next.js + Move 石头剪刀布
rock-paper-scissors
写在前面
本地
源码:https://github.com/zcy1024/SuiStudy/tree/main/rock-paper-scissors
# 或其它等价的命令来安装依赖并将项目跑起来
pnpm install
pnpm run dev
# http://localhost:3000/
在线(如果没过期的话)
https://rock-paper-scissors.walrus.site/
前端(样式布局)
初始化
用 Sui dApp 项目生成器创建一个使用 Sui Testnet 的 Next.js 框架,一切按照提示进行,生成器指南中有详细操作流程,这里就不再赘述。
config/index.ts是根据你所选择的网络环境进行初始化配置的文件,我们这里不需要动它。contracts文件夹用来存储智能合约,lib/contracts文件夹用来存储前端对链上合约的调用,public文件夹用来存储静态文件,utils文件夹用来存储通用的函数或工具,清空这些文件夹里生成的样板代码(直接删除ts文件)。app文件夹是网页的主体,我们也来对其做一些样板代码清理工作。首先,删除favicon.ico,这是网页标签页的图标,然后,进入page.tsx,将里面的代码除了基础结构外全部清除,就像这样:
'use client'import Image from 'next/image'export default function Home() {return (<div className=""><Image src="/logo/logo.jpg" alt="Sui Logo" width={80} height={40} /></div>);
}
准备好HOH社区的logo,将其替换掉/logo/logo.jpg,接下来更改app/layout.tsx文件中的metadata,该结构中的参数会影响网页标签页的展示内容:
export const metadata: Metadata = {title: "Rock Paper Scissors",description: "Classic Game: Rock Paper Scissors",icons: "/logo/logo.jpeg"
};
在 Next.js 中,使用public文件夹中的静态文件的时候,直接用/来表示public/,上面的<Image src="/logo/logo.jpg"... />也是同理。
最后,简单了解一下app下的其它内容:fonts/、fonts.ts、globals.css是创建 Next.js 框架时自带的字体样式(处理)和全局css样式(已配置好tailwindcss),providers.tsx用来初始化 Sui 网络环境、钱包等配置。
至此,我们运行项目,应该能得到如下界面:

页面搭建
整体布局
- 页面上方一条导航栏,左侧放logo,右侧放连接钱包的按钮。
- 剩余部分都用来作为石头剪刀布的游戏区域。
得益于 Sui dApp 项目生成器的配置,连接钱包的按钮就只需要调用@mysten/dapp-kit中提供的组件ConnectButton。
为了让布局更直观,我们暂时为上下两块区域加上背景颜色:
'use client'import Image from 'next/image'
import {ConnectButton} from "@mysten/dapp-kit";export default function Home() {return (<div className="flex flex-col h-screen mx-64"><div className="bg-red-600 flex justify-between items-center"><Image src="/logo/logo.jpeg" alt="HOH Logo" width={80} height={80} priority={true} /><ConnectButton /></div><div className="flex-1 bg-yellow-600"></div></div>);
}

游戏区域布局
寻找石头、剪刀、布的图片,存储至public/game/目录下,分别以rock.png、scissors.png、paper.png命名,以其中任意一张作为样本,将其放到游戏区域的中央,这将作为游戏开始的点击按键,同时为其绑定触发函数:
const playGame = () => {console.log('play game');
}<div className="relative flex-1 bg-yellow-600"><div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 cursor-pointer" onClick={playGame}><Image src="/game/rock.png" alt="start button" width={100} height={100} priority={true} className="w-auto h-auto" /></div>
</div>

在这个开始按键的上方,是敌方(链上随机)选择区域;下方则是我方(鼠标点击)选择区域。类似的,用flex规划好区域后往里面填充内容:
<div className="relative flex-1 bg-yellow-600"><div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 cursor-pointer" onClick={playGame}><Image src="/game/rock.png" alt="start button" width={100} height={100} priority={true} className="w-auto h-auto" /></div><div className="absolute top-0 left-0 w-full h-1/2 flex justify-evenly items-center"><Image src="/game/rock.png" alt="enemy" width={100} height={100} priority={true} className="w-auto h-auto" /></div><div className="absolute bottom-0 left-0 w-full h-1/2 flex justify-evenly items-center"><Image src="/game/rock.png" alt="rock" width={100} height={100} priority={true} className="w-auto h-auto" /><Image src="/game/scissors.png" alt="scissors" width={100} height={100} priority={true} className="w-auto h-auto" /><Image src="/game/paper.png" alt="paper" width={100} height={100} priority={true} className="w-auto h-auto" /></div>
</div>

这时我们发现,中间的开始按钮失去了作用,仔细观察不难发现,这是因为后续的敌我双方的布局覆盖在了上方,最简单的办法是将开始按钮的代码向下移或者为其自定义z-index属性。
之后,我们来思考一个逻辑,开始按钮和后续的出拳选择是否真的需要同时出现?
我们完全可以先将出拳选择区域隐藏,在点击开始后再让其显现出来,相对应的,开始按钮则需要在点击后隐藏。不难发现,它们的显隐状态归根结底都由一个数据进行控制 —— 是否开始游戏。
const [isPlaying, setIsPlaying] = useState<boolean>(false);
用一个布尔值isPlaying来判断,点击后通过setIsPlaying将其设为真。
对于需要根据该值隐藏的内容,通过className={"..." + (isPlaying ? "..." : "...")}来设置。
为了消失和显现不那么突然,可以增加transition-opacity来实现渐隐渐显效果。
export default function Home() {const [isPlaying, setIsPlaying] = useState<boolean>(false);const playGame = () => {setIsPlaying(true);}return (<div className="flex flex-col h-screen mx-64"><div className="bg-red-600 flex justify-between items-center"><Image src="/logo/logo.jpeg" alt="HOH Logo" width={80} height={80} priority={true}/><ConnectButton/></div><div className="relative flex-1 bg-yellow-600"><divclassName={"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-10 transition-opacity " + (isPlaying ? "opacity-0" : "cursor-pointer opacity-100")}onClick={!isPlaying ? playGame : () => {}}><Image src="/game/rock.png" alt="start button" width={100} height={100} priority={true}className="w-auto h-auto"/></div><div className={"transition-opacity " + (isPlaying ? "opacity-100" : "opacity-0")}><div className="absolute top-0 left-0 w-full h-1/2 flex justify-evenly items-center"><Image src="/game/rock.png" alt="enemy" width={100} height={100} priority={true}className="w-auto h-auto"/></div><div className="absolute bottom-0 left-0 w-full h-1/2 flex justify-evenly items-center"><Image src="/game/rock.png" alt="rock" width={100} height={100} priority={true}className="w-auto h-auto"/><Image src="/game/scissors.png" alt="scissors" width={100} height={100} priority={true}className="w-auto h-auto"/><Image src="/game/paper.png" alt="paper" width={100} height={100} priority={true}className="w-auto h-auto"/></div></div></div></div>);
}

类似的,我们为我方选择区域的三个图添加点击事件,由于它们都是<Image ... />,可以通过同一个类型的点击事件进行获取,最后通过alt属性来区分究竟选择的是石头、剪刀还是布。
const clickChoose = (e: MouseEvent<HTMLImageElement>) => {console.log(e.currentTarget.alt);
}<div className="absolute bottom-0 left-0 w-full h-1/2 flex justify-evenly items-center"><Image src="/game/rock.png" alt="rock" width={100} height={100} priority={true}className={"w-auto h-auto " + (isPlaying ? "cursor-pointer" : "")}onClick={isPlaying ? clickChoose : () => {}}/><Image src="/game/scissors.png" alt="scissors" width={100} height={100} priority={true}className={"w-auto h-auto " + (isPlaying ? "cursor-pointer" : "")}onClick={isPlaying ? clickChoose : () => {}}/><Image src="/game/paper.png" alt="paper" width={100} height={100} priority={true}className={"w-auto h-auto " + (isPlaying ? "cursor-pointer" : "")}onClick={isPlaying ? clickChoose : () => {}}/>
</div>

游戏区域的布局是完成了,我们可以把之前用来辨别区域的背景去掉,纯白色太刺眼,就再加一点点灰色缓冲,但是,为什么开始按钮是石头?不如让它动起来,循环切换石头、剪刀、布,包括敌人(链上随机)在返回结果时也不应该固定显示。
大致思路:将三张图片的文件名放到一个数组中,通过不断加一再对数组长度取余使得下标达成循环,根据当前下标所对应的文件名进行显示渲染。
在utils文件夹下创建三个文件sleep.ts、next.ts、index.ts。
sleep.ts:顾名思义,让程序睡眠,等待多少时间后再继续向下运行。
export default function sleep(ms: number) {return new Promise(resolve => setTimeout(resolve, ms));
}
next.ts:在一个数组中循环不断地取下一个。
// 这里显示标注返回值中依次的类型,方便解构赋值后按照次序获得确切的类型
export default function next<T>(index: number, array: T[]): [number, T] {const len = array.length;index = (index + 1) % len;return [index, array[index]];
}
index.ts:将utils目录下所有导出的东西归档再一同导出,方便其它地方导入。由于这里只有两个函数,便捷性提升得不明显。
import sleep from './sleep';
import next from './next';export {sleep,next
}
回到page.tsx,借助上面的两个小函数来实现每隔一小段时间切换图片的功能:
- 图片名更新要实时作用到页面中,所以需要
useState来创建一个字符串以及改变该字符串的函数:const [loopName, setLoopName] = useState<string>("rock"); - 定义一个下标,表示当前循环到了数组中的哪一项,很自然地想到用
let index = 0;不过,用let定义的变量,除非放到全局,否则每次渲染都会重置。
类似于useState,有一个钩子函数useRef可以解决这个问题:const index = useRef<number>(0);
当需要取值时,用index.current,需要更改值时,也只需要将新值赋值给index.current。 - 定义一个包含三张图片名的数组:
const array = ["rock", "scissors", "paper"]; - 实现一个异步函数,在里面依次调用上面两个小工具,获得数据进行更新,而这个函数则放到
useEffect当中,这个useEffect的依赖项设置为loopName,即每次loopName改变后重新执行。
const waitToDispatch = async () => {await sleep(222);const [ne_idx, name] = next(index.current, array);index.current = ne_idx;setLoopName(name);
}
useEffect(() => {waitToDispatch().then();
}, [loopName]);// 最后,将写死的<Image src="/game/rock.png" ... />改为<Image src={`/game/${loopName}.png`} ... />
// 每次loopName变化,src也会跟着变化
至此,功能已经实现且能够正常运行,不过,如果尝试build会发现其中还有一些警告,接下去来尝试解决一下:
useEffect`中用到了`waitToDispatch`,提示我们最好将其添加为依赖项,于是:`useEffect(..., [loopName, waitToDispatch])
再次build获得一个新的警告,由于waitToDispatch是useEffect的依赖项,所以它当前定义实现的位置,可能会因为重新渲染等因素出现潜在的问题。提示了两个解决方案,一个是转移实现waitToDispatch的位置,另一个是用useCallback包裹它。
用useCallback实现的函数,它不会因为页面重新渲染而改变,除非它检测到它的依赖项发生变化才会更新其中的逻辑,起到缓存、提升性能的作用。
于是,我们用其包裹:
const waitToDispatch = useCallback(async () => {await sleep(222);const [ne_idx, name] = next(index.current, array);index.current = ne_idx;setLoopName(name);
}, [index, array]);
我们知道,array内部的值其实是不会改变的,所以只需要依赖index变化来变化就可以,实际上项目也可以运行,不过又会在build时警告,所以我们将其加上。不过,加上之后,又报了个新的warning,说是由于array是useCallback的依赖项,当前位置可能会出现潜在的问题,需要我们转移array定义的位置,或者用useMemo将其包裹。useMemo和useCallback类似,但是,useMemo得到的是经过逻辑运算后的值,并将这个值缓存下来,以避免重复进行(大量的)逻辑运算,除非它的依赖项的值发生了变化才会重新进行计算。
于是,我们用其包裹:const array = useMemo(() => ["rock", "scissors", "paper"], []);
终于,我们解决了所有警告!附上当下page.tsx的完整代码以及演示动图:
'use client'import Image from 'next/image'
import {ConnectButton} from "@mysten/dapp-kit";
import {MouseEvent, useCallback, useEffect, useMemo, useRef, useState} from "react";
import {sleep, next} from "@/utils"export default function Home() {const [isPlaying, setIsPlaying] = useState<boolean>(false);const playGame = () => {setIsPlaying(true);}const clickChoose = (e: MouseEvent<HTMLImageElement>) => {console.log(e.currentTarget.alt);}const [loopName, setLoopName] = useState<string>("rock");const index = useRef<number>(0);const array = useMemo(() => ["rock", "scissors", "paper"], []);const waitToDispatch = useCallback(async () => {await sleep(222);const [ne_idx, name] = next(index.current, array);index.current = ne_idx;setLoopName(name);}, [index, array]);useEffect(() => {waitToDispatch().then();}, [loopName, waitToDispatch]);return (<div className="flex flex-col h-screen mx-64 bg-gray-50 shadow-md"><div className="flex justify-between items-center"><Image src="/logo/logo.jpeg" alt="HOH Logo" width={80} height={80} priority={true}/><ConnectButton/></div><div className="relative flex-1"><divclassName={"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-10 transition-opacity " + (isPlaying ? "opacity-0" : "cursor-pointer opacity-100")}onClick={!isPlaying ? playGame : () => {}}><Image src={`/game/${loopName}.png`} alt="start button" width={100} height={100} priority={true}className="w-auto h-auto"/></div><div className={"transition-opacity " + (isPlaying ? "opacity-100" : "opacity-0")}><div className="absolute top-0 left-0 w-full h-1/2 flex justify-evenly items-center"><Image src={`/game/${loopName}.png`} alt="enemy" width={100} height={100} priority={true}className="w-auto h-auto"/></div><div className="absolute bottom-0 left-0 w-full h-1/2 flex justify-evenly items-center"><Image src="/game/rock.png" alt="rock" width={100} height={100} priority={true}className={"w-auto h-auto " + (isPlaying ? "cursor-pointer" : "")}onClick={isPlaying ? clickChoose : () => {}}/><Image src="/game/scissors.png" alt="scissors" width={100} height={100} priority={true}className={"w-auto h-auto " + (isPlaying ? "cursor-pointer" : "")}onClick={isPlaying ? clickChoose : () => {}}/><Image src="/game/paper.png" alt="paper" width={100} height={100} priority={true}className={"w-auto h-auto " + (isPlaying ? "cursor-pointer" : "")}onClick={isPlaying ? clickChoose : () => {}}/></div></div></div></div>);
}

智能合约
前端页面布局暂告一段落,从这里开始将用Move编写一个简单的石头剪刀布的智能合约。
来到contracts目录,通过sui move new game命令新建合约代码。删除tests目录,里面用来编写测试代码,我们暂时用不上。打开sources/game.move准备编写合约。
我们想要达成的效果很简单,就是当玩家选择好自己是石头、剪刀还是布之后,通过链上随机的方式得到对方出什么。
于是,我们就需要编写以下内容:
- 通过触发事件的方式来得到随机结果。
- 随机函数。
最终,move代码如下:
module game::game {use sui::event;use sui::random::Random;public struct RandomEvent has copy, drop {chosen: u8}entry fun play(random: &Random, ctx: &mut TxContext) {let mut generator = random.new_generator(ctx);event::emit(RandomEvent {chosen: generator.generate_u8_in_range(1, 3)});}
}
随机得到1~3中的数,由前端处理其对应到石头、剪刀和布,sui move build没问题,sui client publish发布,成功后得到一串信息。
通过命令行调用初步观察结果:
export PACKAGE=0x5780ec9a0ab44c86b957855eab35fa3e0dacb71d683109e40c50f94fca2f411b
sui client call --package $PACKAGE --module game --function play --args 0x8
# output:
╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Transaction Block Events │
├─────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ ┌── │
│ │ EventID: o82egWHVDnqSABWre6tustng5zb6vDBfyQvPBtDGnQs:0 │
│ │ PackageID: 0x5780ec9a0ab44c86b957855eab35fa3e0dacb71d683109e40c50f94fca2f411b │
│ │ Transaction Module: game │
│ │ Sender: 0x9e4092b6a894e6b168aa1c6c009f5c1c1fcb83fb95e5aa39144e1d2be4ee0d67 │
│ │ EventType: 0x5780ec9a0ab44c86b957855eab35fa3e0dacb71d683109e40c50f94fca2f411b::game::RandomEvent │
│ │ ParsedJSON: │
│ │ ┌────────┬───┐ │
│ │ │ chosen │ 3 │ │
│ │ └────────┴───┘ │
│ └── │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯
重复调用几次,发现触发的事件中的值确实是随机的,接着,我们对一些信息进行存储,打开config目录新建key.ts,用来存储发布后的ID,为了方便后续前端调用,我们可以把Random的地址、调用的完整函数名以及触发的EventType也存上并导出。
// UPGRADE_CAP 本文不会用到,但是如果后续有升级合约的需求的话需要提供
export const PACKAGE = "0x5780ec9a0ab44c86b957855eab35fa3e0dacb71d683109e40c50f94fca2f411b"
export const UPGRADE_CAP = "0xb6222d0ab94ca5388b0722de9a4aab7ad10ff74bbe91a00d7c8fd1698d185c95"
export const RANDOM = "0x8"
export const FUNCTION = `${PACKAGE}::game::play`
export const EVENT = `${PACKAGE}::game::RandomEvent`
前端与合约交互
根据 Sui dApp 教学文档,我们在page.tsx中添加以下代码:
const {mutateAsync: signAndExecuteTransaction} = useSignAndExecuteTransaction({execute: async ({bytes, signature}) =>await suiClient.executeTransactionBlock({transactionBlock: bytes,signature,options: {showRawEffects: true,showEvents: true},})
});
通过这段代码我们将得到一个async的函数signAndExecuteTransaction,这将是我们唤起钱包签署交易的入口;
根据useSignAndExecuteTransaction内部的定义,在链上交易成功后会返回带有events的信息,我们所需要的随机数就在里面。
如果对返回值不关心,甚至可以直接const { mutate: signAndExecuteTransaction } = useSignAndExecuteTransaction();来获取入口。
假设我们已经实现了一个名为play的函数,它接受一个参数,就是上面的这个交易入口,我们将在我方游戏区域点击事件中触发:
const clickChoose = async (e: MouseEvent<HTMLImageElement>) => {console.log(e.currentTarget.alt);const chosen = await play(signAndExecuteTransaction);console.log(chosen);
}
play函数我们放在lib/contracts目录下,搭好最基本的函数框架:
export default async function play(signAndExecuteTransaction) {
}
首先需要为这个参数指定类型,回到page.tsx将鼠标悬停在这个定义好的交易入口上,发现它的类型是:UseMutateAsyncFunction<SuiTransactionBlockResponse, UseSignAndExecuteTransactionError, UseSignAndExecuteTransactionArgs, unknown>
于是:
export default async function play(signAndExecuteTransaction: UseMutateAsyncFunction<SuiTransactionBlockResponse, UseSignAndExecuteTransactionError, UseSignAndExecuteTransactionArgs, unknown>) {
}
显然,全是报错,因为很多该导入的没有导入。当然可以在当前文件导入,但如果不止有这一个交易,就需要在各个文件重新做一遍类似的操作,为了更便于管理,我们在lib/contracts下新建一个type.ts,专门用来放交易过程中可能用到的(通用)东西。
(可能会报错包不存在,通过pnpm add -D <name>或者等价的命令将其添加)
import {UseMutateFunction, UseMutateAsyncFunction} from "@tanstack/react-query";
import {SuiTransactionBlockResponse} from "@mysten/sui/client";
import type { SuiSignAndExecuteTransactionInput } from '@mysten/wallet-standard';
import { PartialBy } from "@mysten/dapp-kit/dist/cjs/types/utilityTypes";
import { WalletFeatureNotSupportedError, WalletNoAccountSelectedError, WalletNotConnectedError } from "@mysten/dapp-kit/dist/cjs/errors/walletErrors";
import {Transaction} from "@mysten/sui/transactions";type UseSignAndExecuteTransactionError = WalletFeatureNotSupportedError | WalletNoAccountSelectedError | WalletNotConnectedError | Error;
type UseSignAndExecuteTransactionArgs = PartialBy<Omit<SuiSignAndExecuteTransactionInput, 'transaction'>, 'account' | 'chain'> & {transaction: Transaction | string;
};export type {UseMutateFunction,UseMutateAsyncFunction,SuiTransactionBlockResponse,UseSignAndExecuteTransactionError,UseSignAndExecuteTransactionArgs,}
将上面导出的东西,导入play.ts,剩下要做的就是实现这个函数。同样的,根据Sui dApp教学,依葫芦画瓢:
const tx = new Transaction();
tx.moveCall({target: FUNCTION,arguments: [tx.object(RANDOM)],
});
const response = await signAndExecuteTransaction({transaction: tx});
我们新建了一个交易,内容是调用FUNCTION(我们事先在config/key.ts中定义好了),调用的这个链上函数有一个参数,是一个Random对象,通过tx.object(<Object Address>)来将随机数的对象地址0x8转化为对象。
在唤起钱包签署交易的入口里传入这一笔交易,返回的内容存储在response中。
接下来,只需要在其中找到(对应的)EVENT,再将其中存储的chosen返回即可:
let chosen = 0;
response.events?.forEach(event => {if (event.type === EVENT) {chosen = (event.parsedJson as ParsedJson).chosen;}
});
return chosen;
将项目跑起来,测试是否如我所想的那样执行:

输赢结算
首先,在lib目录下新建一个games目录,里面建一个checkIsWinner.ts用来编写判断输赢的函数。
合约随机出的1~3分别表示石头、剪刀、布,我们将前端的选择,也就是e.currentTarget.alt按照同样的规则转化成数字,从中不难发现一个规律:
石头1 > 剪刀2 > 布3,当我们的选择和链上的随机数的差的绝对值小于等于1的时候,数字小的那一方获胜,否则,将两个数都对3取余数后再执行同样的判断,也就是布3 % 3 = 0 > 石头1 % 3 = 1 > 剪刀2 % 3 = 2。
可以证明,这个取余数的过程最多进行一次就必定会判成胜负,于是,编码如下:
function strToNumber(str: string) {if (str === "rock")return 1;if (str === "scissors")return 2;return 3;
}function check(my: number, move: number) {if (Math.abs(my - move) > 1)return check(my % 3, move % 3);return my < move;
}export default function checkIsWinner(my_choice: string, move_choice: number) {return check(strToNumber(my_choice), move_choice);
}
合约交易成功后,在page.tsx中调用该函数。为了让胜负结算标签受该值控制,我们需要用useState来新建一个变量,同时为该标签绑定一个重开功能的点击事件,重开功能很容易实现,只需要将控制状态的值设为初始值即可:
const [isWinner, setIsWinner] = useState<boolean | null>(null);
const clickChoose = async (e: MouseEvent<HTMLImageElement>) => {const my_chosen = e.currentTarget.alt;const chosen = await play(signAndExecuteTransaction);setIsWinner(checkIsWinner(my_chosen, chosen));
}const gameAgain = () => {setIsWinner(null);setIsPlaying(false);
}{isWinner !== null&&<divclassName="absolute w-full top-1/2 -translate-y-1/2 cursor-pointer animate-bounce text-center"onClick={gameAgain}>{isWinner ? "Congratulations, you’ve got it all!" : "No! Everyone believes you will win next time!"}</div>
}

至此,大体功能已全部实现,当然,这是在没有任何误操作(比如未连接钱包开始游戏等)的前提下,同时,最后那只不断闪动的手也应该有一个最终归宿。不过这剩下的大多都是优化或者美化的环节,这里就不再详细阐述。你也可以根据自己的喜好将这一段留白增添一份天马行空的创意。
加入组织,共同进步!
- Sui 中文开发群(TG)
- M o v e \mathit{Move} Move 语言学习交流群: 79489587
相关文章:
Next.js + Move 石头剪刀布
rock-paper-scissors 写在前面 本地 源码:https://github.com/zcy1024/SuiStudy/tree/main/rock-paper-scissors # 或其它等价的命令来安装依赖并将项目跑起来 pnpm install pnpm run dev # http://localhost:3000/在线(如果没过期的话) …...
[面试]关于Redis 的持久化你了解吗
Redis的持久化是指Redis服务器在关闭或重启时,将内存中的数据保存到磁盘上的一种机制。Redis支持多种持久化方式。 一、RDB(Redis Database)持久化 RDB持久化是Redis默认采用的持久化方式,它将Redis在某个时间点的数据保存到磁盘上…...
Systemd:tmpfiles
Systemd提供了一个结构化的可配置方法来管理临时文件和目录,即systemd-tmpfiles,可以创建、删除和管理临时文件的服务。 $ systemctl list-units --all | grep systemd-tmpfilessystemd-tmpfiles-clean.service load…...
【Flutter 内嵌 android 原生 View以及相互跳转】
Flutter 内嵌 android 原生 View以及相互跳转 一. 内嵌android 原生View二、android 与 flutter 相互跳转 一. 内嵌android 原生View 在android 工程的包名下,也可在MainActivity创建 android 原生view ,继承PlatformView // 1.自定义textview public st…...
python externally-managed-environment 外部管理环境
https://realpython.com/python-virtual-environments-a-primer/?refyaolong.net#why-do-you-need-virtual-environments 简而言之, pip 默认会将您安装的所有外部包放置在 Python 安装路径/site-packages/ 的文件夹中一些Linux 和 macOS操作系统 预装了内部的 P…...
前端 | MYTED单篇TED词汇学习功能优化
文章目录 📚实现效果🐇before🐇after 📚模块实现解析🐇html🐇css🐇javascript 📚实现效果 🐇before 点击TED单篇词汇表按钮,选择对应TED打卡号,…...
64 mysql 的 表锁
前言 我们这里来说的就是 我们在 mysql 这边常见的 几种锁 行共享锁, 行排他锁, 表意向共享锁, 表意向排他锁, 表共享锁, 表排他锁 我们前面了解了行共享锁, 行排他锁, 表意向共享锁, 表意向排他锁 等等相关 我们这里 来看一下 表共享锁, 表排他锁 的获取, 以及 和 其他表级…...
【计网不挂科】计算机网络期末考试——【选择题&填空题&判断题&简述题】题库(1)
前言 大家好吖,欢迎来到 YY 滴计算机网络 系列 ,热烈欢迎! 本章主要内容面向接触过C的老铁 本博客主要内容,收纳了一部门基本的计算机网络题目,供yy应对期中考试复习。大家可以参考 欢迎订阅 YY滴其他专栏!…...
ajax关于axios库的运用小案例
AJAX案例 图书管理 四大功能: 展示图书删除图书编辑图书信息新增图书 步骤 1.bootstrap弹窗来实现新增和编辑图书时出现的弹窗 有两种方案: a.可以用自带的属性来进行弹窗的显示和隐藏 b.可以通过JS进行控制,此操作可以进行自定义&am…...
微搭低代码入门01变量
目录 1 变量的定义2 变量的赋值3 变量的类型4 算术运算符5 字符串的连接6 模板字符串7 检查变量的类型8 解构赋值8.1 数组的解构赋值8.2 对象的解构赋值 9 类型转换9.1 转换为字符串9.2 转换为数字9.3 转换为布尔值 总结 好些零基础的同学,在使用低代码的时候&#…...
盘点2024年10款视频剪辑,哪款值得pick!!
在这个短视频盛行的时代,如何让我们的故事更生动有趣呢?那就要对短视频进行修饰了。这就需要借助视频剪辑工具:而一款好的工具不仅仅是视频的“美颜”,更是创意的灵魂所在!想象一下,运用一款功能齐全的剪辑…...
苹果手机照片批量删除:一键清理,释放空间
在数字化时代,iPhone不仅是我们沟通的桥梁,也是记录生活的重要工具。然而,随着时间的积累,手机中的照片数量不断增加,不仅占用大量存储空间,也让设备变得缓慢。苹果手机照片批量删除成为了一个普遍的需求。…...
《AI 大模型:重塑软件开发新生态》
《AI 大模型:重塑软件开发新生态》 一、AI 大模型引领软件开发新潮流二、AI 大模型在软件开发中的优势(一)提高开发效率(二)减少错误与提升质量(三)激发创新与拓展功能 三、AI 大模型在软件开发…...
uniapp(API-Promise 化)
一、异步的方法,如果不传入 success、fail、complete 等 callback 参数,将以 Promise 返回数据异步的方法,且有返回对象,如果希望获取返回对象,必须至少传入一项 success、fail、complete 等 callback 参数,…...
【考研数学 - 数二题型】考研数学必吃榜(数二)
数学二 suhan, 2024.10 文章目录 数学二一、函数❗1.极限1.1求常见极限1.2求数列极限1.2.1 n项和数列极限1.2.2 n项连乘数列极限1.2.3 递推关系定义的数列极限 1.3确定极限式中的参数1.4无穷小量阶的比较 2.连续2.1判断是否连续,不连续则判断间断点类型2.2证明题 二…...
Redis生产问题(缓存穿透、击穿、雪崩)——针对实习面试
目录 Redis生产问题什么是缓存穿透?如何解决缓存穿透?什么是缓存击穿?如何解决缓存击穿?缓存穿透和缓存击穿有什么区别?什么是缓存雪崩?如何解决缓存雪崩? Redis生产问题 什么是缓存穿透&#x…...
android openGL中模板测试、深度测试功能的先后顺序
目录 一、顺序 二、模板测试 1、概念 2、工作原理 3、关键函数 三、深度测试 1、概念 2、工作原理 3、关键函数 三、模板测试和深度测试的先后顺序 一、顺序 在Android OpenGL中,模板测试(Stencil Testing)是在深度测试࿰…...
CCF PTA 编程培训师资认证2021年7月真题- C++兑换礼品
【题目描述】 小零和小壹是两个爱玩游戏的小孩,他俩平时最擅长的是解谜游戏,可今天 遇到了一个有点难的算法问题,希望能得到你的帮助。 他们面对的是一个电子装置,正面有 n 个排成一列的按钮,按钮上贴着编号 1~n 号的…...
火山引擎云服务docker 安装
安装 Docker 登录云服务器。 执行以下命令,添加 yum 源。 yum update -y yum install epel-release -y yum clean all yum list依次执行以下命令,添加Docker CE镜像源。更多操作请参考Docker CE镜像。 # 安装必要的一些系统工具 sudo yum install -y yu…...
【taro react】 ---- 常用自定义 React Hooks 的实现【六】之类渐入动画效果的轮播
1. 效果 2. 场景 css 效果实现:可以看到效果图中就是一个图片从小到大的切换动画效果,这个效果很简单,使用 css 的 transform 的 scale 来实现图片的从小到大的效果,切换就更加简单了,不管是 opacity 还是 visibility 都可以实现图片的隐藏和显示的切换。React.Children.m…...
浅谈 React Hooks
React Hooks 是 React 16.8 引入的一组 API,用于在函数组件中使用 state 和其他 React 特性(例如生命周期方法、context 等)。Hooks 通过简洁的函数接口,解决了状态与 UI 的高度解耦,通过函数式编程范式实现更灵活 Rea…...
web vue 项目 Docker化部署
Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段: 构建阶段(Build Stage):…...
k8s从入门到放弃之Ingress七层负载
k8s从入门到放弃之Ingress七层负载 在Kubernetes(简称K8s)中,Ingress是一个API对象,它允许你定义如何从集群外部访问集群内部的服务。Ingress可以提供负载均衡、SSL终结和基于名称的虚拟主机等功能。通过Ingress,你可…...
Docker 运行 Kafka 带 SASL 认证教程
Docker 运行 Kafka 带 SASL 认证教程 Docker 运行 Kafka 带 SASL 认证教程一、说明二、环境准备三、编写 Docker Compose 和 jaas文件docker-compose.yml代码说明:server_jaas.conf 四、启动服务五、验证服务六、连接kafka服务七、总结 Docker 运行 Kafka 带 SASL 认…...
Mac软件卸载指南,简单易懂!
刚和Adobe分手,它却总在Library里给你写"回忆录"?卸载的Final Cut Pro像电子幽灵般阴魂不散?总是会有残留文件,别慌!这份Mac软件卸载指南,将用最硬核的方式教你"数字分手术"࿰…...
GitHub 趋势日报 (2025年06月08日)
📊 由 TrendForge 系统生成 | 🌐 https://trendforge.devlive.org/ 🌐 本日报中的项目描述已自动翻译为中文 📈 今日获星趋势图 今日获星趋势图 884 cognee 566 dify 414 HumanSystemOptimization 414 omni-tools 321 note-gen …...
12.找到字符串中所有字母异位词
🧠 题目解析 题目描述: 给定两个字符串 s 和 p,找出 s 中所有 p 的字母异位词的起始索引。 返回的答案以数组形式表示。 字母异位词定义: 若两个字符串包含的字符种类和出现次数完全相同,顺序无所谓,则互为…...
在Ubuntu24上采用Wine打开SourceInsight
1. 安装wine sudo apt install wine 2. 安装32位库支持,SourceInsight是32位程序 sudo dpkg --add-architecture i386 sudo apt update sudo apt install wine32:i386 3. 验证安装 wine --version 4. 安装必要的字体和库(解决显示问题) sudo apt install fonts-wqy…...
CVE-2023-25194源码分析与漏洞复现(Kafka JNDI注入)
漏洞概述 漏洞名称:Apache Kafka Connect JNDI注入导致的远程代码执行漏洞 CVE编号:CVE-2023-25194 CVSS评分:8.8 影响版本:Apache Kafka 2.3.0 - 3.3.2 修复版本:≥ 3.4.0 漏洞类型:反序列化导致的远程代…...
spring boot使用HttpServletResponse实现sse后端流式输出消息
1.以前只是看过SSE的相关文章,没有具体实践,这次接入AI大模型使用到了流式输出,涉及到给前端流式返回,所以记录一下。 2.resp要设置为text/event-stream resp.setContentType("text/event-stream"); resp.setCharacter…...
