Flutter: نشر الويب وسطح المكتب متعدد المنصات

اكتب مرة واحدة، شغّل في كل مكان — الويب وسطح المكتب ينقلان كودك من الجوال إلى العالم.

📋 المتطلبات السابقة: يجب أن تكون قد أتقنت ما يلي أولًا

1. ما ستتعلمه


2. قصة التركيز على الجوال فقط

(1) المشكلة: نمو مستخدمي الجوال يصل للذروة

نمو DAU لـ ShopApp لبوب على الجوال انخفض من 50% شهريًا إلى 5%. بيانات السوق تُظهر: 40% من طلبات التجارة الإلكترونية تأتي من سطح المكتب (بمتوسط طلب أعلى بمقدار الضعف)، وSEO للويب يجلب زيارات مجانية. لكن بوب لديه قاعدة كود Flutter واحدة فقط — بناء الويب/سطح المكتب من الصفر سيستغرق 3 أشهر على الأقل.

(2) حل Flutter متعدد المنصات

Flutter يُجمّع نفس قاعدة الكود للويب وسطح المكتب — تحتاج فقط للتعامل مع تكييف التخطيط والميزات الخاصة بالمنصة.

BASH
# Build for web
flutter build web --wasm

# Build for Windows
flutter build windows

# Build for macOS
flutter build macos
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

(3) النتيجة: صفر تطوير إضافي + 3 أيام لإطلاق نسخة الويب

قضى بوب 3 أيام فقط في تكييف التخطيط، ونسخة الويب كانت جاهزة. لوحة تحكم سطح المكبت أعادت استخدام 80% من كود الجوال وأُنجزت في أسبوعين.


3. بنية النشر متعدد المنصات

100%
graph TD
    SC[Single Codebase] --> WEB[Flutter Web]
    SC --> DSK[Desktop]
    WEB --> WASM[WASM / CanvasKit]
    WEB --> SEO2[URL Strategy / SEO]
    DSK --> WIN[Windows]
    DSK --> MAC[macOS]
    DSK --> LNX[Linux]
    DSK --> WM[window_manager]
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

(1) نظرة عامة على اختلافات المنصات

الميزة الجوال الويب سطح المكتب
الإدخال لمس ماوس + لوحة مفاتيح ماوس + لوحة مفاتيح
الشاشة اتجاه ثابت حجم متغير نافذة قابلة لتغيير الحجم
نظام الملفات صندوق رمل بدون وصول كامل
الإشعارات الفورية FCM/APNs Notification API بدون دعم أصلي
الدقة 1x-4x بكسلات CSS DPI عالي

4. بناء وتحسين Flutter Web

(1) مقارنة محركات عرض الويب

المحرك التحميل الأول أداء العرض التوافق حالة الاستخدام الموصى بها
CanvasKit أكبر (2MB+) عالي (GPU) جيد الافتراضي للإنتاج
عارض HTML أصغر منخفض (DOM) مقبول تطبيقات خفيفة
WASM (Skwasm) متوسط الأعلى Flutter 3.22+ الافتراضي المستقبلي

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: إعدادات بناء الويب

BASH
# Build with CanvasKit (default)
flutter build web

# Build with WASM (Flutter 3.22+, recommended)
flutter build web --wasm

# Build with HTML renderer
flutter build web --renderer html

# Custom base href for subdirectory deployment
flutter build web --base-href "/shop/"
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

(2) استراتيجية URL

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: استراتيجية URL بالمسار

DART
import 'package:flutter/material.dart';
import 'package:flutter_web_plugins/url_strategy.dart';

// ShopApp comes from project lib/app.dart

void main() {
  // Use path URL strategy (clean URLs: /product/42)
  usePathUrlStrategy();

  // OR use hash URL strategy (compatibility: /#/product/42)
  // useHashUrlStrategy();

  runApp(const ShopApp());
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
الاستراتيجية تنسيق URL إعدادات الخادم SEO
المسار /product/42 تحتاج توجيه احتياطي جيد
التجزئة /#/product/42 بدون إعدادات مطلوبة ضعيف

(3) تحسين SEO للويب

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: علامات Meta والمعنوية

HTML
<!-- web/index.html -->
`<head>`
  <meta name="description" content="ShopApp - Million-level products, USD settlement">
  <meta name="keywords" content="ecommerce, shop, deals">
  <meta property="og:title" content="ShopApp">
  <meta property="og:description" content="Cross-platform ecommerce app">
  `<title>`ShopApp - Shop the World</title>
</head>
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
💡 نصيحة: Flutter Web هو SPA — زواحف محركات البحث دعم محدود لـ SPAs. للصفحات الغنية بالمحتوى، فكر في حل SSR (مثل dart_frog) أو العرض المسبق.


5. التخطيط المتجاوب

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: نقاط توقف تكيفية بـ LayoutBuilder

DART
import 'package:flutter/material.dart';

// ResponsiveLayout: three-platform adaptive layout component

class ResponsiveLayout extends StatelessWidget {
  final Widget mobile;
  final Widget tablet;
  final Widget desktop;

  const ResponsiveLayout({
    super.key,
    required this.mobile,
    required this.tablet,
    required this.desktop,
  });

  static bool isMobile(BuildContext context) => MediaQuery.sizeOf(context).width < 600;
  static bool isTablet(BuildContext context) =>
      MediaQuery.sizeOf(context).width >= 600 && MediaQuery.sizeOf(context).width < 1024;
  static bool isDesktop(BuildContext context) => MediaQuery.sizeOf(context).width >= 1024;

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        if (constraints.maxWidth >= 1024) return desktop;
        if (constraints.maxWidth >= 600) return tablet;
        return mobile;
      },
    );
  }
}

// Usage
ResponsiveLayout(
  mobile: const MobileProductGrid(crossAxisCount: 2),
  tablet: const TabletProductGrid(crossAxisCount: 3),
  desktop: const DesktopProductLayout(), // Sidebar + grid
)
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: تخطيط صفحة رئيسية ShopApp للويب

DART
import 'package:flutter/material.dart';

// CategorySidebar / CartPanel / productCards come from the ShopApp project

class WebHomePage extends StatelessWidget {
  const WebHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ShopApp'),
        actions: [
          // Desktop-only: search bar in AppBar
          if (ResponsiveLayout.isDesktop(context))
            const SizedBox(width: 300, child: SearchBar()),
          IconButton(icon: const Icon(Icons.shopping_cart), onPressed: () {}),
          IconButton(icon: const Icon(Icons.person), onPressed: () {}),
        ],
      ),
      body: ResponsiveLayout(
        mobile: _buildMobileLayout(),
        tablet: _buildTabletLayout(),
        desktop: _buildDesktopLayout(),
      ),
    );
  }

  Widget _buildDesktopLayout() {
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // Sidebar categories
        SizedBox(
          width: 240,
          child: CategorySidebar(),
        ),
        // Main content
        Expanded(
          child: GridView.count(
            crossAxisCount: 4,
            children: productCards,
          ),
        ),
        // Cart panel (desktop only)
        SizedBox(
          width: 300,
          child: CartPanel(),
        ),
      ],
    );
  }
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

6. تكييف سطح المكتب

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: window_manager لإدارة النوافذ

DART
import 'package:flutter/material.dart';
import 'package:window_manager/window_manager.dart';

// Dependency: window_manager: ^0.3.0 (add to dependencies)
// ShopApp comes from project lib/app.dart

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await windowManager.ensureInitialized();

  WindowOptions windowOptions = const WindowOptions(
    size: Size(1200, 800),
    minimumSize: Size(800, 600),
    center: true,
    backgroundColor: Colors.transparent,
    skipTaskbar: false,
    titleBarStyle: TitleBarStyle.normal,
    title: 'ShopApp',
  );

  windowManager.waitUntilReadyToShow(windowOptions, () async {
    await windowManager.show();
    await windowManager.focus();
  });

  runApp(const ShopApp());
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: اختصارات لوحة المفاتيح

DART
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

// Dependency: go_router: ^13.0.0 (add to dependencies)
// showSearch / router come from the ShopApp project

class ShortcutWrapper extends StatelessWidget {
  final Widget child;
  const ShortcutWrapper({super.key, required this.child});

  @override
  Widget build(BuildContext context) {
    return Shortcuts(
      shortcuts: {
        LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyF): const Intent(#search),
        LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyK): const Intent(#cart),
        LogicalKeySet(LogicalKeyboardKey.escape): const Intent(#back),
      },
      child: Actions(
        actions: {
          Intent(#search): CallbackAction(onInvoke: (_) => showSearch(context)),
          Intent(#cart): CallbackAction(onInvoke: (_) => context.go('/cart')),
          Intent(#back): CallbackAction(onInvoke: (_) => Navigator.maybePop(context)),
        },
        child: child,
      ),
    );
  }
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

7. التعامل مع اختلافات المنصات

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: استيرادات مشروطة وكشف المنصة

DART
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'dart:io' show Platform;

// Note: dart:io is unavailable on Web — real projects need conditional imports
// NativePayButton / CreditCardOption / PayPalOption come from the ShopApp project

class PlatformHelper {
  static bool get isWeb => kIsWeb;
  static bool get isMobile => !kIsWeb && (Platform.isAndroid || Platform.isIOS);
  static bool get isDesktop => !kIsWeb && (Platform.isWindows || Platform.isMacOS || Platform.isLinux);
  static bool get isApple => !kIsWeb && (Platform.isIOS || Platform.isMacOS);
  static bool get supportsNativePay => isApple || (!kIsWeb && Platform.isAndroid);
}

// Usage in widget
Widget buildPaymentOptions() {
  return Column(children: [
    if (PlatformHelper.supportsNativePay)
      NativePayButton(),
    CreditCardOption(),
    if (!PlatformHelper.isWeb)
      PayPalOption(), // PayPal SDK not available on web
  ]);
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

8. مثال كامل: نقطة دخول ShopApp متعددة المنصات

DART
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:window_manager/window_manager.dart';
import 'dart:io' show Platform;

// Dependencies: flutter_riverpod: ^2.0.0, firebase_core: ^2.0.0, window_manager: ^0.3.0
// DefaultFirebaseOptions / initHive / PlatformHelper / ShortcutWrapper / router / ShopApp come from the ShopApp project
// themeModeNotifierProvider / shopAppLightTheme / shopAppDarkTheme come from the ShopApp theme module

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Platform-specific initialization
  if (PlatformHelper.isDesktop) {
    await _initDesktop();
  }

  // Common initialization
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  await initHive();

  runApp(ProviderScope(child: const ShopApp()));
}

Future<void> _initDesktop() async {
  await windowManager.ensureInitialized();
  const windowOptions = WindowOptions(
    size: Size(1200, 800),
    minimumSize: Size(800, 600),
    title: 'ShopApp Admin',
  );
  windowManager.waitUntilReadyToShow(windowOptions, () async {
    await windowManager.show();
  });
}

class ShopApp extends ConsumerWidget {
  const ShopApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final themeMode = ref.watch(themeModeNotifierProvider);
    return MaterialApp.router(
      title: PlatformHelper.isDesktop ? 'ShopApp Admin' : 'ShopApp',
      theme: shopAppLightTheme,
      darkTheme: shopAppDarkTheme,
      themeMode: themeMode,
      routerConfig: router,
      builder: (context, child) {
        // Desktop: add shortcuts
        if (PlatformHelper.isDesktop) {
          return ShortcutWrapper(child: child!);
        }
        return child!;
      },
    );
  }
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

❓ أسئلة شائعة

س هل Flutter Web مناسب لمواقع SEO؟
ج ليس حقًا. Flutter Web هو SPA — زواحف محركات البحث تواجه صعوبة في فهرسته. للمواقع الغنية بالمحتوى التي تحتاج SEO، استخدم Nuxt/Next.js بدلاً من ذلك. Flutter Web أنسب للتطبيقات الشبيهة بالأدوات.
س ما الفرق بين WASM وCanvasKit؟
ج CanvasKit يعرض باستخدام JS + WebGL؛ WASM يستخدم C++ مُترجم لـ WebAssembly بأداء أعلى. Flutter 3.22+ يوصي بـ WASM.
س هل يمكن نشر تطبيقات سطح المكتب على Mac App Store؟
ج نعم. تحتاج حساب مطور Apple، استخدم Xcode Archive للتغليف كـ DMG/PKG، وقدّم عبر App Store Connect للمراجعة.
س كيف أشارك الكود بين الويب والجوال؟
ج 90% من الكود مشترك بالكامل. فقط الميزات الخاصة بالمنصة (الدفع/الإشعارات/نظام الملفات) تحتاج منطقًا مشروطًا. استخدم PlatformHelper لتغليف اختلافات المنصات.
س ما أفضل نقاط التوقف للتخطيط المتجاوب؟
ج الجوال < 600px، الجهاز اللوحي 600-1024px، سطح المكتب > 1024px. راجع مواصفات نقاط توقف Material Design.
س كيف يقارن أداء تطبيق سطح المكتب بالأصلي؟
ج أداء سطح المكتب في Flutter قريب من الأصلي (عرض GPU)، لكن البدء أبطأ من الأصلي (يحتاج تحميل المحرك). يتفوق على Electron في سيناريوهات الحركة المعقدة.

📖 ملخص


📝 تمارين

  1. أساسي (الصعوبة ⭐): ابنِ نسخة ويب بـ flutter build web، شغّلها في متصفح، واضبط استراتيجية URL بالمسار.
  2. متوسط (الصعوبة ⭐⭐): نفذ ResponsiveLayout بتكييف ثلاث منصات: جوال شبكة عمودين، لوحي ثلاثة أعمدة، سطح مكتب شريط جانبي + أربعة أعمدة.
  3. متقدم (الصعوبة ⭐⭐⭐): نفذ نسخة ShopApp متعددة المنصات كاملة: واجهة متجر إلكتروني متجاوبة للويب + لوحة تحكم إدارية لسطح المكتب (window_manager + اختصارات لوحة المفاتيح + شريط قوائم)، بمشاركة 90% من الكود.

← الدرس السابق | الدرس التالي →

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%