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…...

基础算法练习--滑动窗口(已完结)
算法介绍 滑动窗口算法来自tcp协议的一种特性,它的高效使得其也变成了算法题的一种重要考点.滑动窗口的实现实际上也是通过两个指针前后遍历集合实现,但是因为它有固定的解题格式,我将其单独做成一个篇章. 滑动窗口的解题格式: 首先,定义两个指针left和right,与双指针不同的…...

深度学习经典模型之ZFNet
1 ZFNet 1.1 模型介绍 ZFNet是由 M a t t h e w Matthew Matthew D . Z e i l e r D. Zeiler D.Zeiler和 R o b Rob Rob F e r g u s Fergus Fergus在AlexNet基础上提出的大型卷积网络,在2013年ILSVRC图像分类竞赛中以11.19%的错误率获得冠军(实际…...

Linux系统-ubuntu系统安装
作者介绍:简历上没有一个精通的运维工程师。希望大家多多关注作者,下面的思维导图也是预计更新的内容和当前进度(不定时更新)。 这是Linux进阶部分的最后一大章。讲完这一章以后,我们Linux进阶部分讲完以后,我们的Linux操作部分就…...

2-Ubuntu/Windows系统启动盘制作
学习目标: 掌握使用Win32DiskImager、Rufus等工具制作系统启动盘的基本步骤。独立将ISO镜像文件写入USB闪存驱动器,确保在需要时顺利安装或修复系统。通过学习如何选择正确的源文件和目标驱动器,理解启动盘的使用场景和注意事项,…...

你使用过哪些MySQL中复杂且使用不频繁的函数?
在MySQL中,除了常用的SELECT、INSERT、UPDATE等基本操作外,还有许多复杂且功能强大的函数,它们能够处理各种复杂的数据处理需求。这些函数虽然在日常开发中可能不常使用,但在特定场景下却能够发挥巨大的作用。下面,我将…...

Redis-07 Redis哨兵
操作实现 此处应该6台虚拟机,其中3台是哨兵,但因为内存限制没有那么多 1.将sentinel文件拷贝到/myredis目录下 2.sentinel.conf文件重要参数 新建配置文件sentinel26379.conf sentinel26380.conf sentinel26381.conf bind 0.0.0.0 daemonize yes pr…...

7.qsqlquerymodel 与 qtableview使用
目录 qtableview 委托QStyledItemDelegateQAbstractItemDelegateCheckBoxItemDelegate使用qtableview控制列宽,行高,隐藏拖拽行列 qtableview 委托 //设置单元格委托 void setItemDelegate(QAbstractItemDelegate *delegate); QAbstractItemDelegate *it…...

状态模式(State Pattern)详解
1. 引言 在很多软件系统中,对象的行为往往依赖于其内部状态,而状态的变化又会影响对象的行为。状态模式(State Pattern)为解决这一问题提供了一种优雅的方法。通过将状态的行为封装到独立的状态对象中,可以使得对象在…...

ajax微信静默登录不起效不跳转问题
问题描述: 今天通过ajax调用方式做微信静默登录,发现本地可以跳转,到线上地址死活都不跳转,就像没起作用一般,经许久排查发现,是因为https和http域名的问题,线上只配置了http域名࿰…...

参数估计理论
估计理论的主要任务是在某种信号假设下,估算该信号中某个参数(比如幅度、相位、达到时间)的具体取值。 参数估计:先假定研究的问题具有某种数学模型, 如正态分布,二项分布,再用已知类别的学习样…...