هيمن React Native و Flutter على تطوير تطبيقات الجوال متعددة المنصات لسنوات، لكن منافسًا جادًّا جديدًا دخل الساحة. Lynx هو إطار العمل مفتوح المصدر الذي بنته ByteDance — وهو المحرك نفسه الذي يشغّل واجهات داخل TikTok يستخدمها مئات الملايين يوميًا. وعْده جريء: اكتب مرة واحدة بتقنيات الويب المألوفة (CSS و React)، واعرض بواجهات أصلية حقيقية، وحافظ على سلاسة التفاعلات بفضل بنية الخيطين المزدوجة الفريدة.
في هذا الدرس، ستبني تطبيق جوال عاملًا باستخدام Lynx و ReactLynx (نكهته الرسمية المبنية على React) من الصفر: إنشاء المشروع بأداة Rspeedy، وتركيب الواجهة من عناصر Lynx، والتنسيق بـ CSS حقيقي، وإدارة الحالة، وعرض قوائم عالية الأداء، وجلب بيانات عن بُعد، واستخدام البرمجة على الخيط الرئيسي للتخلص من تأخير التفاعل.
المتطلبات الأساسية
قبل البدء، تأكد من توفر ما يلي:
- Node.js 18+ مثبّت (يُنصح بـ Node 20 LTS)
- معرفة أساسية بـ React (المكوّنات، الخطافات Hooks، الخصائص Props)
- إلمام بأساسيات TypeScript
- هاتف ذكي (iOS أو Android) لمعاينة تطبيقك عبر تطبيق LynxExplorer، أو محاكي
- محرر أكواد (يُنصح بـ VS Code)
لا حاجة لأي خبرة سابقة في تطوير تطبيقات الجوال — وهذه بالضبط نقطة قوة Lynx.
ما الذي ستبنيه
ستبني DevPulse، تطبيق جوال صغير لكنه متكامل يعرض قائمة قابلة للتمرير لأدوات المطورين الرائجة. وخلال ذلك ستتعلم:
- كيف تحافظ بنية الخيطين في Lynx على استجابة الواجهة
- كيف تركّب الشاشات من عناصر
<view>و<text>و<image>و<list> - كيف تنسّق بـ CSS قياسي — بما في ذلك التدرجات اللونية والانتقالات والحركات
- كيف تتعامل مع النقرات والحالة بخطافات React التي تعرفها مسبقًا
- كيف تجلب بيانات عن بُعد وتعرضها
- كيف تنقل الكود الحساس للتفاعل إلى الخيط الرئيسي عبر توجيه
'main thread'
فهم بنية Lynx
قبل كتابة الكود، من المفيد فهم سبب اختلاف إحساس Lynx عن React Native.
تشغّل معظم أطر الجوال المعتمدة على JavaScript كل الكود على خيط JavaScript واحد. عندما ينشغل هذا الخيط بمطابقة المكوّنات أو معالجة البيانات، تتلعثم النقرات والتمريرات. أما Lynx فيوزّع العمل على خيطين:
- الخيط الرئيسي يتولى عرض الواجهة وإيماءات اللمس والتحديثات المرئية المتزامنة. ويشغّل محركًا خفيفًا يسمى PrimJS.
- الخيط الخلفي يشغّل منطق تطبيقك — كود المكوّنات، والتأثيرات، وجلب البيانات، وقواعد العمل.
النتيجة: حتى لو كان منطق تطبيقك مشغولًا، يبقى التمرير والاستجابة اللمسية سلسين لأن الخيط الرئيسي لا يُحجَز أبدًا بكود العمل الخاص بك. كما يدعم Lynx خاصية العرض الفوري للإطار الأول (IFR) التي تنتج شاشة أولى مرئية غير فارغة بشكل متزامن عند الإطلاق.
ReactLynx هو نموذج البرمجة المبني على React فوق هذا المحرك. تكتب مكوّناتك بـ JSX والخطافات، ويقرر الإطار ما يعمل وأين.
الخطوة 1: إنشاء المشروع بأداة Rspeedy
تُبنى مشاريع Lynx بأداة Rspeedy، وهي أداة بناء مبنية على Rspack توفر تجميعًا سريعًا وإعادة تحميل فورية. أنشئ مشروعًا جديدًا:
npm create rspeedy@latestيطرح المعالج التفاعلي بضعة أسئلة. اختر هذه الإجابات:
? Project name: dev-pulse
? Select framework: ReactLynx
? Select language: TypeScript
? Select additional tools: (لا شيء حاليًا)ثم ثبّت الاعتماديات وشغّل خادم التطوير:
cd dev-pulse
npm install
npm run devيطبع الطرفية رمز QR. لمعاينة التطبيق على جهاز حقيقي:
- ثبّت تطبيق LynxExplorer على هاتفك (متوفر لمحاكيات iOS وأجهزة Android كملفات جاهزة من موقع Lynx الرسمي).
- امسح رمز QR بتطبيق LynxExplorer.
- يظهر تطبيقك على الجهاز مع إعادة تحميل فورية نشطة — عدّل الكود وشاهد التحديث مباشرة.
الخطوة 2: استكشاف هيكل المشروع
افتح المشروع في محررك. الملفات المهمة:
dev-pulse/
├── lynx.config.ts # إعدادات البناء الخاصة بـ Rspeedy
├── src/
│ ├── index.tsx # نقطة الدخول، تركّب المكوّن الجذر
│ ├── App.tsx # المكوّن الجذر
│ └── App.css # أنماط CSS قياسية
├── package.json
└── tsconfig.jsonنقطة الدخول بسيطة بشكل منعش:
// src/index.tsx
import { root } from '@lynx-js/react'
import { App } from './App.js'
root.render(<App />)لاحظ أن الاستيراد يأتي من @lynx-js/react وليس react. يوفر ReactLynx واجهة الخطافات نفسها (useState و useEffect و useCallback وغيرها) مضبوطةً لبيئة التشغيل ثنائية الخيوط.
الخطوة 3: تركيب الواجهة من عناصر Lynx
لا يعرض Lynx عناصر HTML. بدلًا من ذلك، يأتي بمجموعة من العناصر المدمجة التي تُترجم إلى واجهات أصلية على كل منصة:
| عنصر Lynx | أقرب مكافئ ويب | العرض الأصلي |
|---|---|---|
<view> | <div> | UIView / ViewGroup |
<text> | <span> / <p> | UILabel / TextView |
<image> | <img> | عرض صور أصلي |
<scroll-view> | حاوية <div> قابلة للتمرير | حاوية تمرير أصلية |
<list> | قائمة افتراضية | قائمة أصلية بإعادة التدوير |
قاعدة واحدة يجب تذكرها: كل نص يجب أن يوضع داخل عنصر <text>. وضع نص خام مباشرة داخل <view> لن يُعرض.
استبدل محتوى src/App.tsx بنسخة أولى من ترويسة DevPulse:
// src/App.tsx
import './App.css'
export function App() {
return (
<view className="page">
<view className="header">
<text className="header-title">DevPulse</text>
<text className="header-subtitle">
Trending developer tools, refreshed daily
</text>
</view>
</view>
)
}الخطوة 4: التنسيق بـ CSS حقيقي
على عكس كائنات الأنماط بـ JavaScript في React Native، يدعم Lynx ملفات CSS فعلية — بما فيها المحدِّدات والخصائص المخصصة والتدرجات والانتقالات وحركات keyframe. استبدل محتوى src/App.css:
/* src/App.css */
.page {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: #0b1020;
}
.header {
padding: 48px 24px 24px;
background: linear-gradient(135deg, #1e2a5a 0%, #0b1020 100%);
}
.header-title {
font-size: 32px;
font-weight: 700;
color: #ffffff;
}
.header-subtitle {
margin-top: 6px;
font-size: 14px;
color: #8b93b5;
}احفظ، وستُحدَّث المعاينة على الجهاز فورًا. نظام التخطيط الافتراضي هو Flexbox، ويتصرف display: flex كما تتوقعه تمامًا من الويب.
ملاحظة عن الوحدات: يدعم Lynx وحدة px إضافة إلى وحدات متجاوبة مثل rpx (البكسل المتجاوب النسبي لعرض الشاشة)، وهي مفيدة لأحجام متسقة عبر الأجهزة المختلفة.
الخطوة 5: الحالة والأحداث
تستخدم معالجات الأحداث في Lynx البادئة bind بدلًا من بادئة on في React DOM. حدث النقر — المكافئ الجوّال للنقر بالفأرة — يُربط عبر bindtap. أما الحالة فتعمل تمامًا مثل React القياسي.
أضف مرشح تصنيفات تفاعليًا إلى App.tsx:
import { useState } from '@lynx-js/react'
import './App.css'
const CATEGORIES = ['All', 'AI', 'Frontend', 'DevOps'] as const
export function App() {
const [active, setActive] = useState('All')
return (
<view className="page">
<view className="header">
<text className="header-title">DevPulse</text>
<text className="header-subtitle">
Trending developer tools, refreshed daily
</text>
</view>
<view className="filters">
{CATEGORIES.map((cat) => (
<view
key={cat}
className={active === cat ? 'chip chip-active' : 'chip'}
bindtap={() => setActive(cat)}
>
<text className="chip-label">{cat}</text>
</view>
))}
</view>
</view>
)
}ثم نسّق الأزرار:
.filters {
display: flex;
flex-direction: row;
gap: 8px;
padding: 16px 24px;
}
.chip {
padding: 8px 16px;
border-radius: 999px;
background-color: #1a2340;
transition: background-color 0.2s ease;
}
.chip-active {
background-color: #4f6ef7;
}
.chip-label {
font-size: 13px;
color: #ffffff;
}انقر أحد الأزرار على جهازك: ينتقل التمييز فورًا. خلف الكواليس، يعمل معالجك على الخيط الخلفي، لكن Lynx يبقي الاستجابة المرئية سريعة — وفي الخطوة 8 سترى كيف تجعل الاستجابة الحرجة للتأخير فورية حرفيًا.
الخطوة 6: عرض قائمة عالية الأداء بعنصر list
بالنسبة للقوائم الطويلة، يعرض <scroll-view> كل شيء دفعة واحدة — مناسب لشاشة إعدادات، لكنه مهدر لقائمة محتوى. يمنحك عنصر <list> إعادة تدوير أصلية للخلايا، مثل FlatList في React Native أو RecyclerView على Android.
أولًا، عرّف نموذج البيانات وبعض البيانات الأولية. أنشئ src/data.ts:
// src/data.ts
export interface Tool {
id: number
name: string
tagline: string
category: 'AI' | 'Frontend' | 'DevOps'
stars: number
}
export const SEED_TOOLS: Tool[] = [
{ id: 1, name: 'Lynx', tagline: 'Native UI from web tech', category: 'Frontend', stars: 12400 },
{ id: 2, name: 'Rspeedy', tagline: 'Rspack-powered Lynx builds', category: 'Frontend', stars: 3800 },
{ id: 3, name: 'Claude Agent SDK', tagline: 'Build production AI agents', category: 'AI', stars: 9600 },
{ id: 4, name: 'OpenTUI', tagline: 'React for the terminal', category: 'Frontend', stars: 5100 },
{ id: 5, name: 'Kamal', tagline: 'Zero-downtime VPS deploys', category: 'DevOps', stars: 8700 },
{ id: 6, name: 'uv', tagline: 'Blazing-fast Python packaging', category: 'DevOps', stars: 31000 },
]الآن أنشئ مكوّن القائمة في src/Feed.tsx:
// src/Feed.tsx
import type { Tool } from './data.js'
interface FeedProps {
tools: Tool[]
}
export function Feed({ tools }: FeedProps) {
return (
<list
className="feed"
list-type="single"
span-count={1}
scroll-orientation="vertical"
>
{tools.map((tool) => (
<list-item
item-key={`tool-${tool.id}`}
key={`tool-${tool.id}`}
>
<view className="card">
<view className="card-row">
<text className="card-name">{tool.name}</text>
<text className="card-stars">★ {tool.stars}</text>
</view>
<text className="card-tagline">{tool.tagline}</text>
<view className="card-badge">
<text className="card-badge-text">{tool.category}</text>
</view>
</view>
</list-item>
))}
</list>
)
}تفاصيل مهمة:
- كل عنصر ابن لـ
<list>يجب أن يكون<list-item>بخاصيةitem-keyفريدة — هذا ما يمكّن إعادة التدوير. - الخاصية
list-type="single"تعرض قائمة بعمود واحد؛ كما تُدعم الشبكات وتخطيطات الشلال.
اربطه في App.tsx مع تطبيق مرشح التصنيف:
import { useState } from '@lynx-js/react'
import { Feed } from './Feed.js'
import { SEED_TOOLS } from './data.js'
import './App.css'
const CATEGORIES = ['All', 'AI', 'Frontend', 'DevOps'] as const
export function App() {
const [active, setActive] = useState('All')
const visible =
active === 'All'
? SEED_TOOLS
: SEED_TOOLS.filter((t) => t.category === active)
return (
<view className="page">
{/* الترويسة والمرشحات دون تغيير */}
<view className="filters">
{CATEGORIES.map((cat) => (
<view
key={cat}
className={active === cat ? 'chip chip-active' : 'chip'}
bindtap={() => setActive(cat)}
>
<text className="chip-label">{cat}</text>
</view>
))}
</view>
<Feed tools={visible} />
</view>
)
}أضف أنماط البطاقات:
.feed {
flex: 1;
padding: 0 16px;
}
.card {
margin-bottom: 12px;
padding: 18px;
border-radius: 16px;
background-color: #141c36;
}
.card-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.card-name {
font-size: 18px;
font-weight: 600;
color: #ffffff;
}
.card-stars {
font-size: 13px;
color: #f7c948;
}
.card-tagline {
margin-top: 4px;
font-size: 14px;
color: #8b93b5;
}
.card-badge {
margin-top: 12px;
align-self: flex-start;
padding: 4px 10px;
border-radius: 6px;
background-color: #1e2a5a;
}
.card-badge-text {
font-size: 11px;
color: #9db1ff;
}مرّر القائمة على جهازك. بفضل إعادة التدوير الأصلية للخلايا في <list>، يبقى الأداء سلسًا حتى مع آلاف العناصر.
الخطوة 7: جلب بيانات عن بُعد
تحمّل التطبيقات الحقيقية بياناتها من واجهة برمجية. يوفر Lynx واجهة fetch على الخيط الخلفي، وبما أن كود مكوّناتك يعمل هناك أصلًا، فالنمط يبدو مثل React المعتاد:
// src/App.tsx (جزء تحميل البيانات)
import { useEffect, useState } from '@lynx-js/react'
import type { Tool } from './data.js'
import { SEED_TOOLS } from './data.js'
export function useTools() {
const [tools, setTools] = useState<Tool[]>(SEED_TOOLS)
const [loading, setLoading] = useState(false)
useEffect(() => {
setLoading(true)
fetch('https://api.example.com/trending-tools')
.then((res) => res.json())
.then((data: Tool[]) => setTools(data))
.catch(() => {
// نبقي البيانات الأولية كخيار احتياطي رشيق
})
.finally(() => setLoading(false))
}, [])
return { tools, loading }
}استبدل SEED_TOOLS في App بالقيمة المعادة من الخطاف واعرض حالة تحميل خفيفة:
const { tools, loading } = useTools()
// داخل JSX:
{loading ? (
<view className="loading">
<text className="loading-text">Loading tools…</text>
</view>
) : (
<Feed tools={visible} />
)}ولأن الجلب يحدث على الخيط الخلفي، لا تجمّد استجابةُ شبكةٍ بطيئة التمريرَ أو النقرات أبدًا.
الخطوة 8: البرمجة على الخيط الرئيسي لاستجابة فورية
هنا يختلف Lynx فعليًا عن منافسيه. المعالجات المعرّفة بالطريقة العادية تعمل على الخيط الخلفي، ما يتضمن قفزة غير متزامنة لكل حدث. بالنسبة لمعظم التفاعلات هذا غير ملحوظ، لكن للواجهات المدفوعة بالإيماءات (السحب، التمرير السريع، استجابة الضغط) تريد استجابة بلا أي تأخير.
يتيح لك Lynx تعليم دالة لتعمل مباشرة على الخيط الرئيسي عبر التوجيه 'main thread'، وربطها بالبادئة main-thread::
import { useMainThreadRef } from '@lynx-js/react'
import type { MainThread } from '@lynx-js/types'
export function PressableCard(props: { children: React.ReactNode }) {
const cardRef = useMainThreadRef<MainThread.Element>(null)
function onPressDown(e: MainThread.TouchEvent) {
'main thread'
cardRef.current?.setStyleProperty('transform', 'scale(0.97)')
}
function onPressUp(e: MainThread.TouchEvent) {
'main thread'
cardRef.current?.setStyleProperty('transform', 'scale(1)')
}
return (
<view
main-thread:ref={cardRef}
main-thread:bindtouchstart={onPressDown}
main-thread:bindtouchend={onPressUp}
className="card"
>
{props.children}
</view>
)
}يُنفَّذ تأثير التصغير عند الضغط بشكل متزامن مع حدث اللمس — بلا قفزة بين الخيوط، وبلا إطارات مفقودة، حتى لو كان الخيط الخلفي مشغولًا بتحليل استجابة API كبيرة. هذه هي التقنية وراء الإحساس "الفوري" في التطبيقات الإنتاجية المبنية بـ Lynx.
قاعدتان لدوال الخيط الرئيسي:
- يجب أن تبدأ بالتوجيه
'main thread'كأول عبارة فيها. - لا يمكنها الوصول إلا إلى قيم الخيط الرئيسي (المراجع المنشأة بـ
useMainThreadRef، وكائنات الأحداث، ودوال الخيط الرئيسي الأخرى) — وليس حالة React مباشرة.
اختبار التنفيذ
تحقق من عمل كل قدرة قبل النشر:
- إعادة التحميل الفورية: عدّل نص
header-titleوتأكد من تحديث الجهاز دون إعادة تشغيل. - الترشيح: انقر كل زر تصنيف وتأكد من تحديث القائمة وانتقال تمييز الزر النشط.
- التمرير: مرّر القائمة بسرعة — يجب أن يبقى معدل الإطارات سلسًا بفضل إعادة التدوير الأصلية.
- البيانات الاحتياطية: فعّل وضع الطيران وأعد التشغيل؛ يجب أن تظهر البيانات الأولية بدلًا من شاشة فارغة.
- استجابة الضغط: اضغط مطولًا على بطاقة — يجب أن يكون تأثير التصغير فوريًا دون أي تأخير ملحوظ.
لإنتاج حزمة الإصدار، شغّل:
npm run buildيُخرج Rspeedy حزمة Lynx محسّنة جاهزة للتحميل بواسطة غلاف مضيف أصلي (LynxExplorer أثناء التطوير، أو تطبيق الغلاف الخاص بك على iOS/Android في الإنتاج).
استكشاف الأخطاء وإصلاحها
النص لا يظهر. السلاسل النصية الخام داخل <view> لا تُعرض. غلّف كل نص بعنصر <text>.
فشل مسح رمز QR. تأكد من أن هاتفك وحاسوبك على الشبكة نفسها. إذا تعذّر الوصول إلى عنوان خادم التطوير، مرّر مضيفًا صريحًا: npm run dev -- --host 192.168.1.x.
عناصر القائمة متطابقة أو يُعاد تدويرها بشكل خاطئ. تحقق من أن كل <list-item> يملك item-key فريدًا وثابتًا. إعادة استخدام المفاتيح تكسر آلية إعادة التدوير.
دالة 'main thread' ترمي خطأ عند الوصول إلى القيم. لا تستطيع دوال الخيط الرئيسي الإحاطة بحالة الخيط الخلفي. مرّر البيانات عبر قيم main-thread:ref أو معاملات الأحداث بدلًا من ذلك.
الأنماط تُتجاهل بصمت. يدعم Lynx مجموعة واسعة لكن غير كاملة من CSS. فضّل خصائص تخطيط Flexbox؛ وراجع مرجع CSS الرسمي عندما لا يكون لخاصية ما أي تأثير.
الخطوات التالية
- أضف شاشة تفاصيل واستكشف خيارات التوجيه (Routing) في Lynx
- استبدل نقطة النهاية التجريبية بواجهة برمجية حقيقية وأضف السحب للتحديث عبر أحداث
<scroll-view> - استكشف تضمين عرض Lynx داخل تطبيق iOS/Android أصلي قائم — صُمم Lynx للتبني التدريجي
- قارن مع درس Expo React Native ودرس Tauri 2 لسطح المكتب لاختيار الأداة متعددة المنصات المناسبة لكل مشروع
- تبني ميزات ذكاء اصطناعي في تطبيقك؟ راجع دليل Claude Agent SDK بلغة TypeScript
الخلاصة
لقد بنيت تطبيق جوال متعدد المنصات متكاملًا باستخدام Lynx و ReactLynx: أنشأته بأداة Rspeedy، وركّبته من عناصر تُعرض أصليًا، ونسّقته بـ CSS حقيقي، وشغّلته بخطافات React المألوفة، وحسّنته بقائمة معاد تدويرها إضافة إلى البرمجة على الخيط الرئيسي لاستجابة لمس فورية.
بنية الخيطين في Lynx إعادة تفكير جوهرية في حزمة الجوال المدفوعة بـ JavaScript — فخيط الواجهة لا يقع أبدًا رهينة لمنطق عملك. ومع اختبار ByteDance له على نطاق TikTok وإتاحة أدواته مفتوحة المصدر، استحق Lynx مكانًا في قائمتك المختصرة إلى جانب React Native و Flutter لمشروعك الجوّال القادم.