uni-app 애플릿 Laravel+jwt 권한 인증 시리즈
uni-app 개발 튜토리얼 칼럼에서는 일련의 권한 인증 방법을 소개합니다. 내가 추천하는 것:
Uni-APP 개발 튜토리얼 환경 설명 Uni-APP
Laravel 5.7
+ jwt -auth 1.0.0
권한 인증에 대한 전반적인 설명
- 디자인 테이블 구조
- 프런트엔드 요청 클래스
- js 패키지 권한 인증과 관련된 비인식 새로고침 토큰이 포함되어 있습니다.
- laravel 인증 미들웨어에 비인식 새로고침 토큰이 포함되어 있습니다.
- 로그인을 위한 휴대폰 번호 가져오기
- 손쉬운 새로고침 access_token 아이디어
- 미니 프로그램은 로그인 상태를 어떻게 결정하나요?
uni-app
laravel 5.7
+ jwt-auth 1.0.0
权限认证整体说明
- 设计表结构
- 前端 request 类
- 有关权限认证的 js 封装 包含无感知刷新 token
- laravel auth 中间件 包含无感知刷新 token
- 获取手机号登陆
- 无痛刷新 access_token 思路
- 小程序如何判断登陆状态
设计表结构
和一般设计表没有什么区别,如果是多平台小程序,通过 account_id 关联联合表。
CREATE TABLE `users` ( `u_id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '账号id', `u_username` varchar(15) NOT NULL DEFAULT '' COMMENT '手机号隐藏 ', `u_nickname` varchar(15) NOT NULL COMMENT '分配用户名', `u_headimg` varchar(200) DEFAULT NULL COMMENT '头像', `u_province` varchar(50) DEFAULT NULL, `u_city` varchar(50) DEFAULT NULL, `u_platform` varchar(30) NOT NULL COMMENT '平台:小程序wx,bd等', `u_mobile` char(11) NOT NULL COMMENT '手机号必须授权', `u_openid` varchar(100) DEFAULT NULL COMMENT 'openid', `u_regtime` timestamp NULL DEFAULT NULL COMMENT '注册时间', `u_login_time` timestamp NULL DEFAULT NULL COMMENT '最后登陆时间', `u_status` tinyint(3) unsigned NOT NULL DEFAULT '1' COMMENT '0禁用1正常', `account_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '平台联合id', PRIMARY KEY (`u_id`), KEY `platform` (`u_platform`,`u_mobile`) USING BTREE) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4;
2. 前端 request 类
一个较不错的 request 类 luch-request
디자인 테이블 구조
는 일반 디자인 테이블과 다르지 않습니다. 이는 다중 플랫폼 미니 프로그램이며 account_id를 통해 공동 테이블을 연결합니다.
import Request from './request';import jwt from '@/utils/auth/jwt.js'; // jwt 管理 见下文const http = new Request();const baseUrl = 'http://xxx'; // api 地址var platform = ''; // 登陆时需知道来自哪个平台的小程序用户// #ifdef MP-BAIDUplatform = 'MP-BAIDU';// #endif/* 设置全局配置 */http.setConfig((config) => { config.baseUrl = baseUrl; //设置 api 地址 config.header = { ...config.header } return config})/* 请求之前拦截器 */http.interceptor.request((config, cancel) => { if (!platform) {cancel('缺少平台参数');} config.header = { ...config.header, platform:platform } if (config.custom.auth) { // 需要权限认证的路由 需携带自定义参数 {custom: {auth: true}} config.header.Authorization = jwt.getAccessToken(); } return config})http.interceptor.response(async (response) => { /* 请求之后拦截器 */ console.log(response); // 如果是需要权限认证的路由 if(response.config.custom.auth){ if(response.data.code == 4011){ // 刷新 token jwt.setAccessToken(response.data.data.access_token); // 携带新 token 重新请求 let repeatRes = await http.request(response.config); if ( repeatRes ) { response = repeatRes; } } } return response}, (response) => { // 请求错误做点什么 if(response.statusCode == 401){ getApp().globalData.isLogin = false; uni.showToast({icon:'none',duration:2000,title: "请登录"}) }else if(response.statusCode == 403){ uni.showToast({ title: "您没有权限进行此项操作,请联系客服。", icon: "none" }); } return response})export { http}
2. 프론트엔드 요청 클래스
비교적 좋은 요청 클래스luch-request
는 동적으로 수정되는 구성과 인터셉터를 지원하며 유니앱 플러그인 마켓에서 찾을 수 있습니다. . request.js는 변경할 필요가 없습니다. 사용자 정의 논리는 index.js에 있습니다.
index.js
import Vue from 'vue'import App from './App'import { http } from '@/utils/luch/index.js' //这里Vue.prototype.$http = http Vue.config.productionTip = falseApp.mpType = 'app'const app = new Vue({ ...App})app.$mount()
Global mount// #ifndef H5const loginCode = provider => { return new Promise((resolve, reject) => { uni.login({ provider: provider, success: function(loginRes) { if (loginRes && loginRes.code) { resolve(loginRes.code) } else { reject("获取code失败") } }, fail:function(){ reject("获取code失败")} }); })}// #endifexport { loginCode //登录获取code}로그인 후 복사
3.js 권한 인증 관련 패키지
authorize.js
공간상의 이유로 전체 코드는 게시되지 않습니다. 도착하는 데 사용되지 않습니다. 예를 들어, uni.checkSession()은 jwt가 애플릿의 로그인 상태를 인계받는 데 사용되므로 이 메소드는 현재 사용되지 않습니다.
const tokenKey = 'accessToken';//键值const userKey = 'user'; // 用户信息// tokenconst getAccessToken = function(){ let token=''; try {token = 'Bearer '+ uni.getStorageSync(tokenKey);} catch (e) {} return token;}const setAccessToken = (access_token) => { try {uni.setStorageSync(tokenKey, access_token);return true;} catch (e) {return false;}}const clearAccessToken = function(){ try {uni.removeStorageSync(tokenKey);} catch (e) {}}// userinfoconst setUser = (user)=>{ try {uni.setStorageSync(userKey, user);return true;} catch (e) {return false;}}const getUser = function(){ try {return uni.getStorageSync(userKey)} catch (e) {return false;}}const clearUser = function(){ try {uni.removeStorageSync(userKey)} catch (e) {}}export default { getAccessToken,setAccessToken,clearAccessToken,getUser,setUser,clearUser}
jwt.js
는 별다른 코드 없이 access_token을 관리할 수 있도록 특별히 설계되었으며 사용자 정보 관리 기능도 담았습니다. import {loginCode} from '@/utils/auth/authorize.js';import jwt from '@/utils/auth/jwt.js';import {http} from '@/utils/luch/index.js';const login=function(detail){
return new Promise((resolve, reject) => {
loginCode().then(code=>{
detail.code = code;
return http.post('/v1/auth/login',detail);
})
.then(res=>{
jwt.setAccessToken(res.data.data.access_token);
jwt.setUser(res.data.data.user);
getApp().globalData.isLogin = true;
resolve(res.data.data.user);
})
.catch(err=>{
reject('登陆失败')
})
})}export default {login}
로그인 후 복사
import {loginCode} from '@/utils/auth/authorize.js';import jwt from '@/utils/auth/jwt.js';import {http} from '@/utils/luch/index.js';const login=function(detail){ return new Promise((resolve, reject) => { loginCode().then(code=>{ detail.code = code; return http.post('/v1/auth/login',detail); }) .then(res=>{ jwt.setAccessToken(res.data.data.access_token); jwt.setUser(res.data.data.user); getApp().globalData.isLogin = true; resolve(res.data.data.user); }) .catch(err=>{ reject('登陆失败') }) })}export default {login}
auth.js
로그인만 처리하는데 왜 별도의 파일에 넣지 않고 모든 곳에서 사용되기 때문입니다<?phpnamespace App\Http\Middleware;use App\Library\Y;use Closure;use Exception;use Tymon\JWTAuth\Exceptions\JWTException;use Tymon\JWTAuth\Http\Middleware\BaseMiddleware;use Tymon\JWTAuth\Exceptions\TokenExpiredException;class ApiAuth extends BaseMiddleware{ public function handle($request, Closure $next, $guard = 'api') { // 在排除名单中 比如登录 if($request->is(...$this->except)){ return $next($request); } try { $this->checkForToken($request);// 是否携带令牌 if ( $this->auth->parseToken()->authenticate() ) { return $next($request); //验证通过 } }catch(Exception $e){ // 如果token 过期 if ($e instanceof TokenExpiredException) { try{ // 尝试刷新 如果成功 返给前端 关于前端如何处理的 看前边 index.js $token = $this->auth->refresh(); return Y::json(4011, $e->getMessage(),['access_token'=>$token]); }catch(JWTException $e){ // 达到刷新时间上限 return Y::json(401, $e->getMessage()); } }else{ // 其他各种 直接返回 401 状态码 不再细分 return Y::json(401, $e->getMessage()); } } } protected $except = [ 'v1/auth/login', ];}로그인 후 복사
4 laravel auth middleware
약간. 여기에서 jwt-auth 측면에 대해 이야기해 보세요. 1. 토큰이 만료되고 토큰이 새로 고쳐지면 원래 토큰이 "블랙리스트"에 나열되고 무효화됩니다. 실제로 jwt-auth는 블랙리스트를 저장하는 파일도 유지관리하며 유효하지 않은 토큰은 새로 고침 시간 제한에 도달한 경우에만 삭제됩니다. 예를 들어 만료 시간은 10분이고 새로 고침 제한은 1개월입니다. 이 기간 동안 블랙리스트가 대량으로 생성되어 성능에 영향을 미치므로 만료 시간 등을 최대한 조정해 보세요. 시간은 60분, 새로 고침 제한은 2주 또는 만료 시간은 1주일이고 새로 고침 제한은 한 달 동안 문제 없음입니다. 2. 무통 새로 고침 솔루션에 관해서는 토큰이 만료되면 제가 사용하는 프런트 엔드에서 새로 고침을 완료하기 위해 두 가지 요청을 하는데, 사용자는 이를 인식하지 못하고 인터넷에 직접 자동 새로 고침 및 로그인을 요청하는 솔루션이 있습니다. 그것을 사용하지 않았습니다. 이유는 다른 것을 이해할 수 없습니다. 그러나 나는 다양한 jwt 예외를 컴파일했으며 학생들은 필요한 경우 이를 사용자 정의할 수 있습니다. TokenExpiredException이 만료되고 TokenInvalidException이 토큰을 구문 분석할 수 없으며 UnauthorizedHttpException이 토큰을 전달하지 않습니다. JWTException 토큰이 만료되거나 새로 고침 제한에 도달하거나 jwt 내부 오류가 발생합니다.
<template> <view> <button>获取手机号</button> <button>获取用户数据</button> <button>清除用户数据</button> </view></template><script> import auth from '@/utils/auth/auth.js'; import jwt from '@/utils/auth/jwt.js'; var _self; export default{ data() {return {}}, onLoad(option) {}, onShow(){}, methods: { decryptPhoneNumber: function(e){ // console.log(e.detail); if( e.detail.errMsg == "getPhoneNumber:ok" ){ //成功 auth.login(e.detail); } }, me: function(){ this.$http.get('/v1/auth/me',{custom: {auth: true}}).then(res=>{ console.log(res,'success') }).catch(err=>{ console.log(err,'error60') }) }, clear: function(){ jwt.clearAccessToken(); jwt.clearUser(); uni.showToast({ icon: 'success', title: '清除成功', duration:2000, }); } }, components: {} }</script><style></style>
저자는 이런 새로고침은 유지관리가 매우 어렵다고 생각합니다. 일회용 토큰을 직접 사용하는 것이 더 좋으며 만료 후 다시 로그인하는 것이 좋습니다. 일반적으로 높은 보안이 필요하지 않습니다. 다음 번 https 요청 시 일회용 토큰을 사용하는 것이 좋습니다. 여기서 미들웨어는 auth()->check()만 필요하며 true는 로그인 상태를 의미합니다. , false는 로그인되지 않았음을 의미합니다.
5. 로그인할 휴대폰 번호 받기// 登陆
public function login(Request $request)
{
$platform = $request->header('platform');
if(!$platform || !in_array($platform,User::$platforms)){
return Y::json(1001, '不支持的平台类型');
}
$post = $request->only(['encryptedData', 'iv', 'code']);
$validator = Validator::make($post, [
'encryptedData' => 'required',
'iv' => 'required',
'code' => 'required'
]);
if ($validator->fails()) {return Y::json(1002,'非法请求');}
switch ($platform) {
case 'MP-BAIDU':
$decryption = (new BdDataDecrypt())->decrypt($post['encryptedData'],$post['iv'],$post['code']);
break;
default:
$decryption = false;
break;
}
// var_dump($decryption);
if($decryption !== false){
$user = User::where('u_platform',$platform)->where('u_mobile',$decryption['mobile'])->first();
if($user){
$user->u_login_time = date('Y-m-d H:i:s',time());
$user->save();
}else{
$user = User::create([
'u_username'=> substr_replace($decryption['mobile'],'******',3,6),
'u_nickname'=> User::crateNickName(),
'u_platform'=> $platform,
'u_mobile' => $decryption['mobile'],
'u_openid' => $decryption['openid'],
'u_regtime' => date('Y-m-d H:i:s',time())
]);
}
$token = auth()->login($user);
return Y::json(
array_merge(
$this->respondWithToken($token),
['user'=>['nickName'=>$user->u_nickname]]
)
);
}
return Y::json(1003,'登录失败');
}
// 返回 token
protected function respondWithToken($token)
{
return ['access_token' => $token];
}
로그인 후 복사
// 登陆 public function login(Request $request) { $platform = $request->header('platform'); if(!$platform || !in_array($platform,User::$platforms)){ return Y::json(1001, '不支持的平台类型'); } $post = $request->only(['encryptedData', 'iv', 'code']); $validator = Validator::make($post, [ 'encryptedData' => 'required', 'iv' => 'required', 'code' => 'required' ]); if ($validator->fails()) {return Y::json(1002,'非法请求');} switch ($platform) { case 'MP-BAIDU': $decryption = (new BdDataDecrypt())->decrypt($post['encryptedData'],$post['iv'],$post['code']); break; default: $decryption = false; break; } // var_dump($decryption); if($decryption !== false){ $user = User::where('u_platform',$platform)->where('u_mobile',$decryption['mobile'])->first(); if($user){ $user->u_login_time = date('Y-m-d H:i:s',time()); $user->save(); }else{ $user = User::create([ 'u_username'=> substr_replace($decryption['mobile'],'******',3,6), 'u_nickname'=> User::crateNickName(), 'u_platform'=> $platform, 'u_mobile' => $decryption['mobile'], 'u_openid' => $decryption['openid'], 'u_regtime' => date('Y-m-d H:i:s',time()) ]); } $token = auth()->login($user); return Y::json( array_merge( $this->respondWithToken($token), ['user'=>['nickName'=>$user->u_nickname]] ) ); } return Y::json(1003,'登录失败'); } // 返回 token protected function respondWithToken($token) { return ['access_token' => $token]; }
Backend
<?phpnamespace App\Library;use App\Library\Y;class BdDataDecrypt{ private $_appid; private $_app_key; private $_secret; private $_session_key; public function __construct() { $this->_appid = env('BD_APPID'); $this->_app_key = env('BAIDU_KEY'); $this->_secret = env('BD_SECRET'); } public function decrypt($encryptedData, $iv, $code){ $res = $this->getSessionKey($code); if($res === false){return false;} $data['openid'] = $res['openid']; $res = $this->handle($encryptedData,$iv,$this->_app_key,$res['session_key']); if($res === false){return false;} $res = json_decode($res,true); $data['mobile'] = $res['mobile']; return $data; } public function getSessionKey($code) { $params['code'] = $code; $params['client_id'] = $this->_app_key; $params['sk'] = $this->_secret; $res = Y::curl("https://spapi.baidu.com/oauth/jscode2sessionkey",$params,0,1); // var_dump($res); /** * 错误返回 * array(3) { ["errno"]=> int(1104) ["error"]=> string(33) "invalid code , expired or revoked" ["error_description"]=> string(33) "invalid code , expired or revoked" } 成功返回: array(2) { ["openid"]=> string(26) "z45QjEfvkUJFwYlVcpjwST5G8w" ["session_key"]=> string(32) "51b9297ababbcf43c1a099256bf82d75" } */ if( isset($res['error']) ){ return false; } return $res; } /** * 官方 demo * return string(24) "{"mobile":"18288881111"}" or false */ private function handle($ciphertext, $iv, $app_key, $session_key) { $session_key = base64_decode($session_key); $iv = base64_decode($iv); $ciphertext = base64_decode($ciphertext); $plaintext = false; if (function_exists("openssl_decrypt")) { $plaintext = openssl_decrypt($ciphertext, "AES-192-CBC", $session_key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv); } else { $td = mcrypt_module_open(MCRYPT_RIJNDAEL_128, null, MCRYPT_MODE_CBC, null); mcrypt_generic_init($td, $session_key, $iv); $plaintext = mdecrypt_generic($td, $ciphertext); mcrypt_generic_deinit($td); mcrypt_module_close($td); } if ($plaintext == false) { return false; } // trim pkcs#7 padding $pad = ord(substr($plaintext, -1)); $pad = ($pad 32) ? 0 : $pad; $plaintext = substr($plaintext, 0, strlen($plaintext) - $pad); $plaintext = substr($plaintext, 16); $unpack = unpack("Nlen/", substr($plaintext, 0, 4)); $content = substr($plaintext, 4, $unpack['len']); $app_key_decode = substr($plaintext, $unpack['len'] + 4); return $app_key == $app_key_decode ? $content : false; }}
import jwt from '@/utils/auth/jwt.js';Vue.prototype.checkLogin = function(){ var TOKEN = jwt.getAccessToken(); return new Promise((resolve, reject) => { if(TOKEN){ http.get('/v1/auth/check',{custom: {auth: true}}).then(res=>{ // 通过中间件 一定是登陆态 resolve(true); }).catch(err=>{ resolve(false); console.log(err) // 这里是401 403 后端500错误或者网络不好 }) }else{ resolve(false) //没有token 一定是未登陆 } })}
7. 小程序如何判断登陆状态
其实思路也很简单,非前后端分离怎么做的,前后端分离就怎么做,原理一样。非前后端分离,在每次请求时都会读取 session ,那么前后端分离,更好一些,有些公开请求不走中间件,也就无需判断登陆态,只有在需要权限认证的页面,在页面初始化时发出一次请求走中间件,以此判断登陆状态。
定义全局登陆检查函数
import jwt from '@/utils/auth/jwt.js';Vue.prototype.checkLogin = function(){ var TOKEN = jwt.getAccessToken(); return new Promise((resolve, reject) => { if(TOKEN){ http.get('/v1/auth/check',{custom: {auth: true}}).then(res=>{ // 通过中间件 一定是登陆态 resolve(true); }).catch(err=>{ resolve(false); console.log(err) // 这里是401 403 后端500错误或者网络不好 }) }else{ resolve(false) //没有token 一定是未登陆 } })}
笔者最终放弃上面的这种检查登录的方式,直接检验storage中有user和token即视为登录状态。以被动的验证代替主动去验证,就是说用户执行一个请求,返回401,那么就改变登录状态。以后再补充。
前端
<script> export default { data() { return { isLogin:null } }, onLoad() { this.checkLogin().then(loginStatus=>{ this.isLogin = loginStatus; }); }, methods: { }, components: {} }</script>
위 내용은 uni-app 애플릿 Laravel+jwt 권한 인증 시리즈의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

핫 AI 도구

Undresser.AI Undress
사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover
사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool
무료로 이미지를 벗다

Clothoff.io
AI 옷 제거제

AI Hentai Generator
AI Hentai를 무료로 생성하십시오.

인기 기사

뜨거운 도구

메모장++7.3.1
사용하기 쉬운 무료 코드 편집기

SublimeText3 중국어 버전
중국어 버전, 사용하기 매우 쉽습니다.

스튜디오 13.0.1 보내기
강력한 PHP 통합 개발 환경

드림위버 CS6
시각적 웹 개발 도구

SublimeText3 Mac 버전
신 수준의 코드 편집 소프트웨어(SublimeText3)

뜨거운 주제









모바일 인터넷 기술과 스마트폰의 대중화로 인해 WeChat은 사람들의 삶에 없어서는 안될 애플리케이션이 되었습니다. WeChat 미니 프로그램을 사용하면 사람들은 애플리케이션을 다운로드하고 설치하지 않고도 미니 프로그램을 직접 사용하여 몇 가지 간단한 요구 사항을 해결할 수 있습니다. 이 기사에서는 Python을 사용하여 WeChat 애플릿을 개발하는 방법을 소개합니다. 1. 준비 Python을 사용하여 WeChat 애플릿을 개발하기 전에 관련 Python 라이브러리를 설치해야 합니다. 여기서는 wxpy와 itchat 두 라이브러리를 사용하는 것이 좋습니다. wxpy는 위챗 머신입니다

미니 프로그램은 반응을 사용할 수 있습니다. 사용 방법: 1. "react-reconciler"를 기반으로 렌더러를 구현하고 DSL을 생성합니다. 2. DSL을 구문 분석하고 렌더링하기 위한 미니 프로그램 구성 요소를 만듭니다. 3. npm을 설치하고 개발자를 실행합니다. 도구에서 npm을 빌드합니다. 4. 자신의 페이지에 패키지를 소개한 다음 API를 사용하여 개발을 완료합니다.

VSCode에서 uni-app을 개발하는 방법은 무엇입니까? 다음 기사에서는 VSCode에서 uni-app을 개발하는 방법에 대한 튜토리얼을 공유할 것입니다. 이것은 아마도 가장 훌륭하고 자세한 튜토리얼일 것입니다. 와서 살펴보세요!

WeChat 미니 프로그램에서 카드 뒤집기 효과 구현 WeChat 미니 프로그램에서 카드 뒤집기 효과를 구현하는 것은 사용자 경험과 인터페이스 상호 작용의 매력을 향상시킬 수 있는 일반적인 애니메이션 효과입니다. 다음은 WeChat 애플릿에서 카드 뒤집기 효과를 구현하는 방법을 자세히 소개하고 관련 코드 예제를 제공합니다. 먼저, 미니 프로그램의 페이지 레이아웃 파일에 두 개의 카드 요소를 정의해야 합니다. 하나는 앞면 내용을 표시하기 위한 것이고 다른 하나는 뒷면 내용을 표시하기 위한 것입니다. <--index.wxml- ->&l

10월 31일 이 사이트의 소식에 따르면 올해 5월 27일 Ant Group은 '한자 선택 프로젝트'를 시작한다고 발표했으며 최근 새로운 진전을 보였습니다. Alipay는 '한자 선택 - 흔하지 않은 문자' 미니 프로그램을 출시했습니다. 희귀 캐릭터는 희귀 캐릭터 라이브러리를 보완하고 희귀 캐릭터에 대한 다양한 입력 경험을 제공하여 Alipay의 희귀 캐릭터 입력 방법을 개선하는 데 도움을 줍니다. 현재 사용자는 "한자픽업", "희귀문자" 등의 키워드를 검색하여 "특수문자" 애플릿에 진입할 수 있습니다. 미니 프로그램에서 사용자는 시스템에서 인식 및 입력되지 않은 희귀 문자의 사진을 제출할 수 있으며 확인 후 Alipay 엔지니어가 글꼴 라이브러리에 추가 항목을 작성합니다. 이 웹사이트에서는 미니 프로그램에서도 최신 문자 분할 입력 방식을 경험할 수 있다고 밝혔습니다. 이 입력 방식은 발음이 불분명한 희귀 문자를 위해 설계되었습니다. 사용자 해체

uniapp을 사용하여 간단한 지도 탐색을 개발하는 방법은 무엇입니까? 이 기사는 간단한 지도를 만드는 데 도움이 되기를 바랍니다.

유니앱이 미니 프로그램과 H5 사이를 빠르게 전환하려면 구체적인 코드 예제가 필요합니다. 최근 모바일 인터넷의 발전과 스마트폰의 대중화로 인해 미니 프로그램과 H5는 필수 애플리케이션 형태가 되었습니다. 크로스 플랫폼 개발 프레임워크인 uniapp은 코드 세트를 기반으로 작은 프로그램과 H5 간의 변환을 신속하게 실현하여 개발 효율성을 크게 향상시킬 수 있습니다. 이 기사에서는 uniapp이 미니 프로그램과 H5 간의 신속한 변환을 달성하는 방법을 소개하고 구체적인 코드 예제를 제공합니다. 1. 유니앱 유니아 소개

uniapp을 사용하여 스네이크 게임을 개발하는 방법은 무엇입니까? 다음 기사는 uniapp에서 Snake 게임을 구현하는 방법을 단계별로 설명합니다. 도움이 되기를 바랍니다.
