> 웹 프론트엔드 > CSS 튜토리얼 > Flutter, Fauna 및 GraphQL을 사용하여 풀 스택 모바일 애플리케이션을 구축하는 방법

Flutter, Fauna 및 GraphQL을 사용하여 풀 스택 모바일 애플리케이션을 구축하는 방법

Lisa Kudrow
풀어 주다: 2025-03-21 10:34:13
원래의
317명이 탐색했습니다.

Flutter, Fauna 및 GraphQL을 사용하여 풀 스택 모바일 애플리케이션을 구축하는 방법

Flut 모바일 애플리케이션 개발을위한 가장 빠르게 성장하는 프레임 워크 중 하나입니다. 반면, 동물 군은 기본 그래프 QL을 지원하는 트랜잭션, 개발자 친화적 인 서버리스 데이터베이스입니다. Flutter Fauna는 천국이 만든 완벽한 경기입니다. 레코드 시간에 기능이 풍부한 풀 스택 애플리케이션을 구축하고 출시하려면 Flutter 및 Fauna가 올바른 도구입니다. 이 기사에서는 동물 군과 GraphQL 백엔드를 사용하여 첫 번째 플러터 응용 프로그램의 구성을 안내합니다.

GitHub 에서이 기사의 전체 코드를 찾을 수 있습니다.

학습 목표

이 기사를 읽은 후에는 다음과 같은 방법을 알아야합니다.

  1. 동물 군 사례를 설정하고
  2. 동물 군에 대한 그래프 QL 패턴 쓰기,
  3. Flutter 응용 프로그램에서 GraphQL 클라이언트를 설정하고
  4. FAUNA GraphQL 백엔드에서 쿼리 및 돌연변이를 수행하십시오.

동물 군AWS Amplify vs. Firebase : 동물 군은 어떤 문제를 해결합니까? 다른 서버리스 솔루션과 어떻게 다른가요? 동물 군에 익숙하지 않고 동물 군과 다른 솔루션의 비교에 대해 더 많이 배우고 싶다면이 기사를 읽어보십시오.

우리는 무엇을 만들고 있습니까?

사용자가 좋아하는 영화 및 TV 시리즈 캐릭터를 추가, 삭제 및 업데이트 할 수있는 간단한 모바일 앱을 구축합니다.

동물 군을 설정하십시오

fauna.com으로 이동하여 새 계정을 만듭니다. 로그인 한 후 새 데이터베이스를 만들 수 있어야합니다.

데이터베이스의 이름을 지정하십시오. 나는 내 flutter_demo를 지명했다. 다음으로 지역 그룹을 선택할 수 있습니다. 이 데모를 위해 클래식을 선택합니다. 동물 군은 전 세계적으로 분산 된 서버리스 데이터베이스입니다. 저도의 저조도 읽기 및 쓰기 액세스를 지원하는 유일한 데이터베이스입니다. CDN (Content Distribution Network)으로 생각하지만 데이터베이스를위한 것입니다. 지역 그룹에 대한 자세한 내용은이 안내서를 따르십시오.

관리자 키를 생성합니다

데이터베이스가 생성되면 보안 탭으로 이동하십시오. 새 키 버튼을 클릭하고 데이터베이스의 새 키를 만듭니다. 이 키를 GraphQL 작업에 필요 하므로이 키를 올바르게 유지하십시오.

데이터베이스의 관리자 키를 만들 것입니다. 관리자 역할이있는 키는 데이터베이스 액세스 제공 업체, 서브 디타베이스, 문서, 기능, 인덱스, 키, 토큰 및 사용자 정의 역할을 포함한 관련 데이터베이스를 관리하는 데 사용됩니다. 동물 군의 다양한 보안 키에 대해 자세히 알아볼 수 있으며 아래 링크에서 액세스 역할을 수행 할 수 있습니다.

그래프 QL 패턴 작성

사용자가 좋아하는 TV 캐릭터를 추가, 업데이트 및 삭제할 수있는 간단한 앱을 구축합니다.

새로운 플러터 프로젝트를 만듭니다

다음 명령을 실행하여 새로운 플러터 프로젝트를 만들어 봅시다.

 <code>flutter create my_app</code>
로그인 후 복사

프로젝트 디렉토리에서 GraphQL/Schema.graphQL이라는 새 파일을 만듭니다.

스키마 파일에서 컬렉션의 구조를 정의합니다. 동물 군의 컬렉션은 SQL의 테이블과 유사합니다. 우리는 지금 하나의 세트 만 있으면됩니다. 우리는 그것을 캐릭터로 지정합니다.

 <code>### schema.graphql type Character { name: String! description: String! picture: String } type Query { listAllCharacters: [Character] }</code>
로그인 후 복사

위에서 볼 수 있듯이 여러 속성 (예 : 이름, 설명, 그림 등)이있는 문자라는 유형을 정의합니다. 속성은 SQL 데이터베이스의 열 또는 NOSQL 데이터베이스의 키 값 쌍으로 취급 될 수 있습니다. 우리는 또한 쿼리를 정의합니다. 이 쿼리는 역할 목록을 반환합니다.

이제 동물 군 대시 보드로 돌아 갑시다. GraphQL을 클릭하고 가져 오기 모드를 클릭하여 모드를 동물 군에 업로드하십시오.

가져 오기가 완료되면 동물 군이 GraphQL 쿼리 및 돌연변이를 생성하는 것을 볼 수 있습니다.

Automatic GuphQL을 좋아하지 않습니까? 비즈니스 논리를 더 잘 제어하고 싶습니까? 이 경우 동물 군을 사용하면 사용자 정의 그래프 QL 파서를 정의 할 수 있습니다. 자세한 내용은이 링크를 클릭하십시오.

Flutter 응용 프로그램에서 GraphQL 클라이언트 설정

pubspec.yaml 파일을 열고 필요한 종속성을 추가합시다.

 <code>... dependencies: graphql_flutter: ^4.0.0-beta hive: ^1.3.0 flutter: sdk: flutter ...</code>
로그인 후 복사

여기에 두 가지 의존성을 추가했습니다. GraphQL_Flutter는 Flutter의 GraphQL 클라이언트 라이브러리입니다. GraphQL 클라이언트의 모든 최신 기능을 사용하기 쉬운 패키지로 통합합니다. 우리는 또한 하이브 패키지를 우리의 종속성으로 추가했습니다. Hive는 로컬 스토리지를 위해 Pure Dart로 작성된 가벼운 키 값 데이터베이스입니다. Hive를 사용하여 GraphQL 쿼리를 캐시합니다.

다음으로 새 파일 Lib/Client_Provider.dart를 생성합니다. 당사는 동물 군 구성이 포함 된 파일에 제공자 클래스를 만들 것입니다.

동물 군의 GraphQL API에 연결하려면 먼저 GraphQLClient를 만들어야합니다. GraphQlclient에는 캐시와 링크가 초기화되어야합니다. 아래 코드를 살펴 보겠습니다.

 <code>// lib/client_provider.dart import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:flutter/material.dart'; ValueNotifier<graphqlclient> clientFor({ @required String uri, String subscriptionUri, }) { final HttpLink httpLink = HttpLink( uri, ); final AuthLink authLink = AuthLink( getToken: () async => 'Bearer fnAEPAjy8QACRJssawcwuywad2DbB6ssrsgZ2-2', ); Link link = authLink.concat(httpLink); return ValueNotifier<graphqlclient> ( GraphQLClient( cache: GraphQLCache(store: HiveStore()), link: link, ), ); }</graphqlclient></graphqlclient></code>
로그인 후 복사

위의 코드에서는 GraphQLClient를 감싸는 ValueNotifier를 만듭니다. 13-15 행에서 AuthLink를 구성했습니다 (강조 표시). 14 행에서, 우리는 토큰의 일부로 동물 군에서 관리자 키를 추가했습니다. 여기서 관리 키를 하드 코딩했습니다. 그러나 생산 응용 프로그램에서는 동물 군의 보안 키를 하드 코딩하지 않아야합니다.

플러터 애플리케이션에 키를 저장하는 방법에는 여러 가지가 있습니다. 참조는이 블로그를 확인하십시오.

응용 프로그램의 위젯에서 쿼리 및 돌연변이를 호출 할 수 있기를 원합니다. 이를 위해서는 GraphQLProvider 위젯을 사용하여 위젯을 감싸 야합니다.

 <code>// lib/client_provider.dart .... /// 使用`graphql_flutter`客户端包装根应用程序。 /// 我们使用缓存进行所有状态管理。 class ClientProvider extends StatelessWidget { ClientProvider({ @required this.child, @required String uri, }) : client = clientFor( uri: uri, ); final Widget child; final ValueNotifier<graphqlclient> client; @override Widget build(BuildContext context) { return GraphQLProvider( client: client, child: child, ); } }</graphqlclient></code>
로그인 후 복사

다음으로 Main.Dart 파일로 이동하여 ClientProvider 위젯으로 기본 위젯을 랩합니다. 아래 코드를 살펴 보겠습니다.

 <code>// lib/main.dart ... void main() async { await initHiveForFlutter(); runApp(MyApp()); } final graphqlEndpoint = 'https://graphql.fauna.com/graphql'; class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return ClientProvider( uri: graphqlEndpoint, child: MaterialApp( title: 'My Character App', debugShowCheckedModeBanner: false, initialRoute: '/', routes: { '/': (_) => AllCharacters(), '/new': (_) => NewCharacter(), } ), ); } }</code>
로그인 후 복사

이 시점에서 모든 다운 스트림 위젯은 쿼리 및 돌연변이 기능을 실행할 수 있으며 GraphQL API와 상호 작용할 수 있습니다.

응용 프로그램 페이지

데모 응용 프로그램은 간단하고 이해하기 쉬워야합니다. 계속해서 모든 역할의 목록을 표시 할 간단한 목록 위젯을 작성하겠습니다. 새 파일 Lib/Screens/Character-List.dart를 만들어 봅시다. 이 파일에서는 AllCharacters라는 새로운 위젯을 작성합니다.

 <code>// lib/screens/character-list.dart.dart class AllCharacters extends StatelessWidget { const AllCharacters({Key key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( body: CustomScrollView( slivers: [ SliverAppBar( pinned: true, snap: false, floating: true, expandedHeight: 160.0, title: Text( 'Characters', style: TextStyle( fontWeight: FontWeight.w400, fontSize: 36, ), ), actions:<widget> [ IconButton( padding: EdgeInsets.all(5), icon: const Icon(Icons.add_circle), tooltip: 'Add new entry', onPressed: () { Navigator.pushNamed(context, '/new'); }, ), ], ), SliverList( delegate: SliverChildListDelegate([ Column( children: [ for (var i = 0; i _CharacterTileeState(); } class _CharacterTileState extends State<charactertile> { @override Widget build(BuildContext context) { return Container( child: Text("Character Tile"), ); } }</charactertile></widget></code>
로그인 후 복사

위 코드에서 볼 수 있듯이 [37 행] 우리는 가짜 데이터로 목록을 채우는 for 루프가 있습니다. 마지막으로 동물 군집에서 GraphQL 쿼리를 수행하고 데이터베이스에서 모든 역할을 수행합니다. 이렇게하기 전에 응용 프로그램을 실행 해 보겠습니다. 다음 명령을 사용하여 응용 프로그램을 실행할 수 있습니다

 <code>flutter run</code>
로그인 후 복사

이 시점에서 다음 화면을 볼 수 있어야합니다.

쿼리 및 돌연변이를 실행하십시오

이제 GraphQL 쿼리에 계속 연결할 수있는 몇 가지 기본 위젯이 있습니다. 우리는 하드 코딩 된 문자열 대신 데이터베이스에서 모든 역할을 가져 와서 AllCharacters 위젯에서보고 싶습니다.

동물 군의 GraphQL 놀이터로 돌아 갑시다. 다음 쿼리를 실행하여 모든 역할을 나열 할 수 있습니다.

 <code>query ListAllCharacters { listAllCharacters(_size: 100) { data { _id name description picture } after } }</code>
로그인 후 복사

위젯 에서이 쿼리를 수행하려면 약간의 변경을 수행해야합니다.

 <code>import 'package:flutter/material.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:todo_app/screens/Character-tile.dart'; String readCharacters = ";";"; query ListAllCharacters { listAllCharacters(_size: 100) { data { _id name description picture } after } } ";";";; class AllCharacters extends StatelessWidget { const AllCharacters({Key key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( body: CustomScrollView( slivers: [ SliverAppBar( pinned: true, snap: false, floating: true, expandedHeight: 160.0, title: Text( 'Characters', style: TextStyle( fontWeight: FontWeight.w400, fontSize: 36, ), ), actions:<widget> [ IconButton( padding: EdgeInsets.all(5), icon: const Icon(Icons.add_circle), tooltip: 'Add new entry', onPressed: () { Navigator.pushNamed(context, '/new'); }, ), ], ), SliverList( delegate: SliverChildListDelegate([ Query(options: QueryOptions( document: gql(readCharacters), // 我们要执行的graphql查询pollInterval: Duration(seconds: 120), // 重新获取间隔), builder: (QueryResult result, { VoidCallback refetch, FetchMore fetchMore }) { if (result.isLoading) { return Text('Loading'); } return Column( children: [ for (var item in result.data\['listAllCharacters'\]['data']) CharacterTile(Character: item, refetch: refetch), ], ); }) ]) ) ], ), ); } }</widget></code>
로그인 후 복사

먼저 쿼리 문자열을 정의하여 데이터베이스 [5 ~ 17 행]에서 모든 역할을 가져옵니다. flutter_graphql에서 쿼리 위젯을 사용하여 목록 위젯을 래핑합니다.

flutter_graphql 라이브러리의 공식 문서를 자유롭게보십시오.

쿼리 옵션 매개 변수에서 GraphQL 쿼리 문자열 자체를 제공합니다. Pollinterval 매개 변수의 부동 소수점 번호를 전달할 수 있습니다. 설문 조사 간격은 백엔드에서 데이터를 수립하려는 시간을 정의합니다. 위젯에는 표준 빌더 기능도 있습니다. Builder 함수를 사용하여 쿼리 결과를 전달하고 콜백 함수를 다시 작성하며 더 많은 콜백 기능을 위젯 트리에 가져올 수 있습니다.

다음으로 화면에 문자 데이터를 표시하도록 문자 타일 위젯을 업데이트합니다.

 <code>// lib/screens/character-tile.dart ... class CharacterTile extends StatelessWidget { final Character; final VoidCallback refetch; final VoidCallback updateParent; const CharacterTile({ Key key, @required this.Character, @required this.refetch, this.updateParent, }) : super(key: key); @override Widget build(BuildContext context) { return InkWell( onTap: () { }, child: Padding( padding: const EdgeInsets.all(10), child: Row( children: [ Container( height: 90, width: 90, decoration: BoxDecoration( color: Colors.amber, borderRadius: BorderRadius.circular(15), image: DecorationImage( fit: BoxFit.cover, image: NetworkImage(Character['picture']) ) ), ), SizedBox(width: 10), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( Character['name'], style: TextStyle( color: Colors.black87, fontWeight: FontWeight.bold, ), ), SizedBox(height: 5), Text( Character['description'], style: TextStyle( color: Colors.black87, ), maxLines: 2, ), ], ) ) ], ), ), ); } }</code>
로그인 후 복사

새 데이터를 추가하십시오

다음 돌연변이를 실행하여 데이터베이스에 새로운 역할을 추가 할 수 있습니다.

 <code>mutation CreateNewCharacter($data: CharacterInput!) { createCharacter(data: $data) { _id name description picture } }</code>
로그인 후 복사

위젯 에서이 돌연변이를 실행하려면 flutter_graphql 라이브러리의 돌연변이 위젯을 사용해야합니다. 사용자가 데이터를 상호 작용하고 입력 할 수있는 간단한 양식으로 새 위젯을 만들어 봅시다. 양식을 제출 한 후 CreateCharacter 돌연변이가 호출됩니다.

 <code>// lib/screens/new.dart ... String addCharacter = ";";"; mutation CreateNewCharacter(\$data: CharacterInput!) { createCharacter(data: \$data) { _id name description picture } } ";";";; class NewCharacter extends StatelessWidget { const NewCharacter({Key key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Add New Character'), ), body: AddCharacterForm() ); } } class AddCharacterForm extends StatefulWidget { AddCharacterForm({Key key}) : super(key: key); @override _AddCharacterFormState createState() => _AddCharacterFormState(); } class _AddCharacterFormState extends State<addcharacterform> { String name; String description; String imgUrl; @override Widget build(BuildContext context) { return Form( child: Padding( padding: EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ TextField( decoration: const InputDecoration( icon: Icon(Icons.person), labelText: 'Name *', ), onChanged: (text) { name = text; }, ), TextField( decoration: const InputDecoration( icon: Icon(Icons.post_add), labelText: 'Description', ), minLines: 4, maxLines: 4, onChanged: (text) { description = text; }, ), TextField( decoration: const InputDecoration( icon: Icon(Icons.image), labelText: 'Image Url', ), onChanged: (text) { imgUrl = text; }, ), SizedBox(height: 20), Mutation( options: MutationOptions( document: gql(addCharacter), onCompleted: (dynamic resultData) { print(resultData); name = ''; description = ''; imgUrl = ''; Navigator.of(context).push( MaterialPageRoute(builder: (context) => AllCharacters()) ); }, ), builder: ( RunMutation runMutation, QueryResult result, ) { return Center( child: ElevatedButton( child: const Text('Submit'), onPressed: () { runMutation({ 'data': { ";picture";: imgUrl, ";name";: name, ";description";: description, } }); }, ), ); } ) ], ), ), ); } }</addcharacterform></code>
로그인 후 복사

위의 코드에서 볼 수 있듯이 돌연변이 위젯은 쿼리 위젯과 매우 유사하게 작동합니다. 또한, 돌연변이 위젯은 우리에게 oncompleter 기능을 제공합니다. 이 기능은 돌연변이가 완료된 후 데이터베이스에서 업데이트 결과를 반환합니다.

데이터 삭제

Deletecharacter 돌연변이를 실행하여 데이터베이스에서 역할을 삭제할 수 있습니다. 우리는이 돌연변이를 캐릭터에 추가하고 버튼을 누르면 트리거 할 수 있습니다.

 <code>// lib/screens/character-tile.dart ... String deleteCharacter = ";";"; mutation DeleteCharacter(\$id: ID!) { deleteCharacter(id: \$id) { _id name } } ";";";; class CharacterTile extends StatelessWidget { final Character; final VoidCallback refetch; final VoidCallback updateParent; const CharacterTile({ Key key, @required this.Character, @required this.refetch, this.updateParent, }) : super(key: key); @override Widget build(BuildContext context) { return InkWell( onTap: () { showModalBottomSheet( context: context, builder: (BuildContext context) { print(Character['picture']); return Mutation( options: MutationOptions( document: gql(deleteCharacter), onCompleted: (dynamic resultData) { print(resultData); this.refetch(); }, ), builder: ( RunMutation runMutation, QueryResult result, ) { return Container( height: 400, padding: EdgeInsets.all(30), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children:<widget> [ Text(Character['description']), ElevatedButton( child: Text('Delete Character'), onPressed: () { runMutation({ 'id': Character['_id'], }); Navigator.pop(context); }, ), ], ), ), ); } ); } ); }, child: Padding( padding: const EdgeInsets.all(10), child: Row( children: [ Container( height: 90, width: 90, decoration: BoxDecoration( color: Colors.amber, borderRadius: BorderRadius.circular(15), image: DecorationImage( fit: BoxFit.cover, image: NetworkImage(Character['picture']) ) ), ), SizedBox(width: 10), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( Character['name'], style: TextStyle( color: Colors.black87, fontWeight: FontWeight.bold, ), ), SizedBox(height: 5), Text( Character['description'], style: TextStyle( color: Colors.black87, ), maxLines: 2, ), ], ) ) ], ), ), ); } }</widget></code>
로그인 후 복사

데이터 편집

데이터 편집은 추가 및 삭제와 동일합니다. GraphQL API의 또 다른 돌연변이 일뿐입니다. 새로운 역할 양식 위젯과 유사한 편집 역할 양식 위젯을 만들 수 있습니다. 유일한 차이점은 양식을 편집하면 UpdateCharacter 돌연변이가 실행된다는 것입니다. 편집을 위해 새로운 위젯 lib/screens/edit.dart를 만들었습니다. 이 위젯의 ​​코드는 다음과 같습니다.

 <code>// lib/screens/edit.dart String editCharacter = """ mutation EditCharacter(\$name: String!, \$id: ID!, \$description: String!, \$picture: String!) { updateCharacter(data: { name: \$name description: \$description picture: \$picture }, id: \$id) { _id name description picture } } """; class EditCharacter extends StatelessWidget { final Character; const EditCharacter({Key key, this.Character}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Edit Character'), ), body: EditFormBody(Character: this.Character), ); } } class EditFormBody extends StatefulWidget { final Character; EditFormBody({Key key, this.Character}) : super(key: key); @override _EditFormBodyState createState() => _EditFormBodyState(); } class _EditFormBodyState extends State<editformbody> { String name; String description; String picture; @override Widget build(BuildContext context) { return Container( child: Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ TextFormField( initialValue: widget.Character['name'], decoration: const InputDecoration( icon: Icon(Icons.person), labelText: 'Name *', ), onChanged: (text) { name = text; } ), TextFormField( initialValue: widget.Character['description'], decoration: const InputDecoration( icon: Icon(Icons.person), labelText: 'Description', ), minLines: 4, maxLines: 4, onChanged: (text) { description = text; } ), TextFormField( initialValue: widget.Character['picture'], decoration: const InputDecoration( icon: Icon(Icons.image), labelText: 'Image Url', ), onChanged: (text) { picture = text; }, ), SizedBox(height: 20), Mutation( options: MutationOptions( document: gql(editCharacter), onCompleted: (dynamic resultData) { print(resultData); Navigator.of(context).push( MaterialPageRoute(builder: (context) => AllCharacters()) ); }, ), builder: ( RunMutation runMutation, QueryResult result, ) { print(result); return Center( child: ElevatedButton( child: const Text('Submit'), onPressed: () { runMutation({ 'id': widget.Character['_id'], 'name': name != null ? name : widget.Character['name'], 'description': description != null ? description : widget.Character['description'], 'picture': picture != null ? picture : widget.Character['picture'], }); }, ), ); } ), ] ) ), ); } }</editformbody></code>
로그인 후 복사

이 기사의 전체 코드를 다음과 같이 볼 수 있습니다.

동물 군이나 플러터에 대해 궁금한 점이 있습니까? Twitter @haqueshadid로 저에게 연락 할 수 있습니다

github ### 다음 단계

이 기사의 주요 목적은 Flutter and Fauna로 시작하는 것입니다. 우리는 여기서 표면 만 닿습니다. FAUNA Ecosystem은 모바일 애플리케이션에 서비스로 완전하고 자동 스케일링, 개발자 친화적 인 백엔드를 제공합니다. 목표가 레코드 시간에 생산에 사용할 수있는 크로스 플랫폼 모바일 앱을 출시하는 것이라면 동물 군과 플러터를 사용해보십시오.

Fauna의 공식 문서 웹 사이트를 확인하는 것이 좋습니다. Dart/Flutter의 GraphQL 클라이언트에 대해 더 자세히 알고 싶다면 GraphQL_Flutter의 공식 GitHub 저장소를 확인하십시오.

나는 당신에게 행복한 프로그래밍을 기원합니다. 다음에 뵙겠습니다.

위 내용은 Flutter, Fauna 및 GraphQL을 사용하여 풀 스택 모바일 애플리케이션을 구축하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿