


Flutter adalah rangka kerja UI Google untuk mewujudkan aplikasi mudah alih yang fleksibel dan ekspresif. Ia adalah salah satu rangka kerja yang paling pesat untuk pembangunan aplikasi mudah alih. Fauna, sebaliknya, adalah pangkalan data pelayan tanpa urus niaga, pemaju yang menyokong GraphQL asli. Flutter Fauna adalah perlawanan sempurna yang dibuat oleh syurga. Sekiranya anda ingin membina dan melepaskan aplikasi lengkap yang kaya dengan ciri-ciri dalam masa rekod, Flutter dan Fauna adalah alat yang betul. Dalam artikel ini, kami akan membimbing anda melalui pembinaan aplikasi flutter pertama anda menggunakan backends Fauna dan GraphQL.
Anda boleh mencari kod penuh untuk artikel ini di GitHub.
Objektif pembelajaran
Selepas membaca artikel ini, anda harus tahu bagaimana untuk:
- Sediakan contoh fauna,
- Tulis corak graphql untuk fauna,
- Sediakan klien GraphQL dalam aplikasi Flutter, dan
- Lakukan pertanyaan dan mutasi pada backend GraphQL Fauna.
Fauna vs AWS Amplify vs. Firebase : Apakah masalah yang diselesaikan oleh Fauna? Bagaimana ia berbeza dengan penyelesaian tanpa pelayan lain? Jika anda tidak biasa dengan Fauna dan ingin mengetahui lebih lanjut mengenai perbandingan Fauna dengan penyelesaian lain, saya cadangkan anda membaca artikel ini.
Apa yang kita bina?
Kami akan membina aplikasi mudah alih mudah yang membolehkan pengguna menambah, memadam dan mengemas kini filem kegemaran mereka dan siri TV.
Sediakan Fauna
Pergi ke fauna.com dan buat akaun baru. Selepas log masuk, anda sepatutnya dapat membuat pangkalan data baru.
Namakan pangkalan data anda. Saya menamakan saya flutter_demo. Seterusnya, kita boleh memilih kumpulan rantau. Untuk demo ini, kami akan memilih Klasik. Fauna adalah pangkalan data tanpa pelayan di seluruh dunia. Ia adalah satu-satunya pangkalan data yang menyokong akses membaca dan menulis latensi rendah dari mana-mana sahaja. Fikirkannya sebagai CDN (rangkaian pengedaran kandungan), tetapi ia adalah untuk pangkalan data anda. Untuk mengetahui lebih lanjut mengenai kumpulan rantau ini, ikuti panduan ini.
Menjana kunci pentadbir
Sebaik sahaja pangkalan data dibuat, pergi ke tab Keselamatan. Klik butang kunci baru dan buat kekunci baru untuk pangkalan data anda. Sila simpan kunci ini dengan betul kerana kami memerlukannya untuk operasi GraphQL.
Kami akan membuat kunci pentadbir untuk pangkalan data kami. Kunci dengan peranan pentadbir digunakan untuk menguruskan pangkalan data yang berkaitan, termasuk penyedia akses pangkalan data, subdatabase, dokumen, fungsi, indeks, kunci, token, dan peranan yang ditentukan oleh pengguna. Anda boleh mengetahui lebih lanjut mengenai pelbagai kunci keselamatan Fauna dan peranan akses dalam pautan di bawah.
Menulis corak GraphQL
Kami akan membina aplikasi mudah yang membolehkan pengguna menambah, mengemas kini dan memadam watak TV kegemaran mereka.
Buat projek Flutter baru
Mari buat projek Flutter baru dengan menjalankan arahan berikut.
<code>flutter create my_app</code>
Dalam direktori projek, kami akan membuat fail baru bernama Graphql/schema.graphql.
Dalam fail skema, kami akan menentukan struktur koleksi. Koleksi di fauna adalah serupa dengan jadual dalam SQL. Kami hanya memerlukan satu set sekarang. Kami namakan wataknya.
<code>### schema.graphql type Character { name: String! description: String! picture: String } type Query { listAllCharacters: [Character] }</code>
Seperti yang ditunjukkan di atas, kami menentukan jenis yang dipanggil watak yang mempunyai pelbagai sifat (iaitu, nama, keterangan, gambar, dll.). Hartanah boleh dianggap sebagai lajur pangkalan data SQL atau pasangan nilai utama pangkalan data NoSQL. Kami juga menentukan pertanyaan. Pertanyaan ini mengembalikan senarai peranan.
Sekarang mari kita kembali ke papan pemuka Fauna. Klik GraphQL dan klik Mod Import untuk memuat naik mod kami ke Fauna.
Selepas import selesai, kami akan melihat Fauna menghasilkan pertanyaan dan mutasi GraphQL.
Tidak suka generasi graphql automatik? Ingin mempunyai kawalan yang lebih baik terhadap logik perniagaan anda? Dalam kes ini, Fauna membolehkan anda menentukan parser GraphQL tersuai. Untuk mengetahui lebih lanjut, klik pautan ini.
Menyediakan pelanggan Graphql dalam Aplikasi Flutter
Mari buka fail pubspec.yaml dan tambahkan kebergantungan yang diperlukan.
<code>... dependencies: graphql_flutter: ^4.0.0-beta hive: ^1.3.0 flutter: sdk: flutter ...</code>
Kami telah menambah dua kebergantungan di sini. Graphql_flutter adalah Perpustakaan Pelanggan Graphql Flutter. Ia mengintegrasikan semua ciri moden klien GraphQL ke dalam satu pakej yang mudah digunakan. Kami juga menambah pakej sarang sebagai kebergantungan kami. HIVE adalah pangkalan data nilai utama yang ditulis dalam Pure Dart untuk penyimpanan tempatan. Kami menggunakan sarang untuk cache pertanyaan graphql kami.
Seterusnya, kami akan membuat fail baru lib/client_provider.dart. Kami akan membuat kelas penyedia dalam fail yang akan mengandungi konfigurasi Fauna kami.
Untuk menyambung ke API GraphQL Fauna, kita perlu membuat GraphQLClient terlebih dahulu. GraphqlClient memerlukan cache dan pautan untuk dimulakan. Mari lihat kod di bawah.
<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>
Dalam kod di atas, kami mencipta Valuenotifier untuk membungkus GraphqlClient. Perhatikan bahawa kami mengkonfigurasi authLink pada baris 13-15 (diserlahkan). Pada baris 14, kami menambah kunci pentadbir dari Fauna sebagai sebahagian daripada token. Di sini, saya mengikat kunci admin. Walau bagaimanapun, dalam aplikasi pengeluaran, kita mesti mengelakkan kekunci keselamatan fauna.
Terdapat beberapa cara untuk menyimpan kekunci dalam aplikasi berkilauan. Sila periksa blog ini untuk rujukan.
Kami mahu dapat memanggil pertanyaan dan mutasi dari mana -mana widget dalam aplikasi. Untuk melakukan ini, kita perlu membungkus widget kita menggunakan widget 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>
Seterusnya, kami pergi ke fail main.dart dan bungkus widget utama kami dengan widget ClientProvider. Mari lihat kod di bawah.
<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>
Pada ketika ini, semua widget hiliran kami akan dapat menjalankan pertanyaan dan fungsi mutasi dan boleh berinteraksi dengan API GraphQL.
Halaman permohonan
Permohonan demo mestilah mudah dan mudah difahami. Mari kita teruskan dan buat widget senarai mudah yang akan memaparkan senarai semua peranan. Mari buat fail lib/skrin/list characters.dart baru. Dalam fail ini, kami akan menulis widget baru yang dipanggil 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>
Seperti yang ditunjukkan dalam kod di atas, [baris 37] kami mempunyai gelung yang mengisi senarai dengan beberapa data palsu. Akhirnya, kami akan melakukan pertanyaan GraphQL pada backend Fauna dan dapatkan semua peranan dari pangkalan data. Sebelum kita melakukan ini, mari kita cuba menjalankan permohonan kita. Kami boleh menjalankan aplikasi kami menggunakan arahan berikut
<code>flutter run</code>
Pada ketika ini, kita harus dapat melihat skrin berikut.
Laksanakan pertanyaan dan mutasi
Sekarang kita mempunyai beberapa widget asas yang kita boleh terus menyambung ke pertanyaan GraphQL. Kami mahu mendapatkan semua peranan dari pangkalan data dan bukannya rentetan keras dan melihatnya dalam widget AllCharacters.
Mari kembali ke taman permainan GraphQL Fauna. Perhatikan bahawa kami boleh menjalankan pertanyaan berikut untuk menyenaraikan semua peranan.
<code>query ListAllCharacters { listAllCharacters(_size: 100) { data { _id name description picture } after } }</code>
Untuk melakukan pertanyaan ini dari widget kami, kami perlu membuat beberapa perubahan kepadanya.
<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>
Pertama, kami menentukan rentetan pertanyaan untuk mendapatkan semua peranan dari pangkalan data [baris 5 hingga 17]. Kami membungkus widget senarai menggunakan widget pertanyaan di flutter_graphql.
Jangan ragu untuk melihat dokumentasi rasmi Perpustakaan Flutter_Graphql.
Dalam parameter pilihan pertanyaan, kami menyediakan rentetan pertanyaan GraphQL itu sendiri. Kita boleh lulus sebarang nombor titik terapung untuk parameter pollinterval. Selang Poll Menentukan berapa lama kita mahu memulihkan data dari backend. Widget juga mempunyai fungsi pembina standard. Kami boleh menggunakan fungsi pembina untuk lulus hasil pertanyaan, dapatkan semula fungsi panggil balik, dan dapatkan lebih banyak fungsi panggil balik ke dalam pokok widget.
Seterusnya, saya akan mengemas kini widget Watak untuk memaparkan data aksara pada skrin.
<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>
Tambah data baru
Kami boleh menambah peranan baru ke pangkalan data kami dengan menjalankan mutasi berikut.
<code>mutation CreateNewCharacter($data: CharacterInput!) { createCharacter(data: $data) { _id name description picture } }</code>
Untuk menjalankan mutasi ini dari widget kami, kami perlu menggunakan widget mutasi dari perpustakaan Flutter_Graphql. Mari buat widget baru dengan bentuk mudah untuk pengguna berinteraksi dan memasukkan data. Selepas menyerahkan borang, mutasi createcharacter akan dipanggil.
<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>
Seperti yang dapat dilihat dari kod di atas, widget mutasi berfungsi dengan sangat sama dengan widget pertanyaan. Di samping itu, widget mutasi memberikan kita fungsi oncomplete. Fungsi ini mengembalikan hasil kemas kini dalam pangkalan data selepas mutasi selesai.
Padam data
Kita boleh memadam peranan dari pangkalan data dengan menjalankan mutasi DeleteCharacter. Kita boleh menambah mutan ini ke watak kita dan mencetuskannya apabila butang ditekan.
<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>
Edit data
Data pengeditan adalah sama seperti menambah dan memadam. Ia hanya mutasi lain dalam API GraphQL. Kami boleh membuat widget bentuk role edit yang serupa dengan widget bentuk peranan baru. Satu -satunya perbezaan ialah penyuntingan borang akan menjalankan mutasi updatecharacter. Untuk mengedit, saya mencipta widget baru lib/skrin/edit.dart. Berikut adalah kod untuk widget ini.
<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>
Anda boleh melihat kod penuh artikel ini seperti berikut.
Ada soalan mengenai fauna atau berkibar? Anda boleh menghubungi saya di Twitter @Haqueshadid
Github ### Langkah seterusnya
Tujuan utama artikel ini adalah untuk membuat anda bermula dengan Flutter dan Fauna. Kami hanya menyentuh permukaan di sini. Ekosistem Fauna menyediakan aplikasi mudah alih anda dengan backend yang lengkap, automatik, pemaju yang lengkap sebagai perkhidmatan. Sekiranya matlamat anda adalah untuk melepaskan aplikasi mudah alih rentas platform yang boleh digunakan untuk pengeluaran dalam masa rekod, cuba Fauna dan Flutter .
Saya sangat mengesyorkan anda menyemak laman web dokumentasi rasmi Fauna. Jika anda berminat untuk mempelajari lebih lanjut mengenai klien GraphQL Dart/Flutter, lihat repositori GitHub rasmi Graphql_flutter.
Saya mengucapkan selamat datang kepada anda, sampai jumpa lagi.
Atas ialah kandungan terperinci Cara membina aplikasi mudah alih penuh dengan Flutter, Fauna, dan Graphql. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Sekiranya anda baru -baru ini mula bekerja dengan GraphQL, atau mengkaji semula kebaikan dan keburukannya, anda tidak akan ragu -ragu mendengar perkara seperti "Graphql tidak menyokong caching" atau

Dalam artikel ini kita akan menyelam ke dunia scrollbars. Saya tahu, ia tidak terdengar terlalu glamor, tetapi percayalah, halaman yang direka dengan baik

API Peralihan Svelte menyediakan cara untuk menghidupkan komponen apabila mereka memasuki atau meninggalkan dokumen, termasuk peralihan svelte adat.

Berapa banyak masa yang anda habiskan untuk merancang persembahan kandungan untuk laman web anda? Semasa anda menulis catatan blog baru atau membuat halaman baru, adakah anda memikirkan

Dengan pendakian harga bitcoin baru -baru ini lebih dari 20k $ USD, dan baru -baru ini melanggar 30k, saya fikir ia patut mengambil menyelam yang mendalam kembali ke dalam mewujudkan Ethereum

Perintah NPM menjalankan pelbagai tugas untuk anda, sama ada sebagai satu-satunya atau proses berjalan terus untuk perkara seperti memulakan pelayan atau menyusun kod.

Saya hanya berbual dengan Eric Meyer pada hari yang lain dan saya teringat cerita Eric Meyer dari tahun -tahun pembentukan saya. Saya menulis catatan blog mengenai kekhususan CSS, dan

Artikel ini membincangkan menggunakan CSS untuk kesan teks seperti bayang -bayang dan kecerunan, mengoptimumkannya untuk prestasi, dan meningkatkan pengalaman pengguna. Ia juga menyenaraikan sumber untuk pemula. (159 aksara)


Alat AI Hot

Undresser.AI Undress
Apl berkuasa AI untuk mencipta foto bogel yang realistik

AI Clothes Remover
Alat AI dalam talian untuk mengeluarkan pakaian daripada foto.

Undress AI Tool
Gambar buka pakaian secara percuma

Clothoff.io
Penyingkiran pakaian AI

AI Hentai Generator
Menjana ai hentai secara percuma.

Artikel Panas

Alat panas

SublimeText3 versi Cina
Versi Cina, sangat mudah digunakan

DVWA
Damn Vulnerable Web App (DVWA) ialah aplikasi web PHP/MySQL yang sangat terdedah. Matlamat utamanya adalah untuk menjadi bantuan bagi profesional keselamatan untuk menguji kemahiran dan alatan mereka dalam persekitaran undang-undang, untuk membantu pembangun web lebih memahami proses mengamankan aplikasi web, dan untuk membantu guru/pelajar mengajar/belajar dalam persekitaran bilik darjah Aplikasi web keselamatan. Matlamat DVWA adalah untuk mempraktikkan beberapa kelemahan web yang paling biasa melalui antara muka yang mudah dan mudah, dengan pelbagai tahap kesukaran. Sila ambil perhatian bahawa perisian ini

VSCode Windows 64-bit Muat Turun
Editor IDE percuma dan berkuasa yang dilancarkan oleh Microsoft

SublimeText3 versi Inggeris
Disyorkan: Versi Win, menyokong gesaan kod!

Penyesuai Pelayan SAP NetWeaver untuk Eclipse
Integrasikan Eclipse dengan pelayan aplikasi SAP NetWeaver.