إضافة إمكانية التنقل الفوري وعمليات انتقال سلسة للصفحات إلى تطبيق ويب

1. قبل البدء

يعلّمك هذا الدرس التطبيقي كيفية إضافة تنقّل فوري وانتقالات سلسة للصفحات إلى نموذج تطبيق ويب يتضمّن أحدث واجهات برمجة التطبيقات المتوافقة في Google Chrome.

يتحقق تطبيق الويب من القيم الغذائية للفواكه والخضروات الشائعة. تم إنشاء صفحات قائمة الفاكهة وتفاصيل الفاكهة كتطبيق من صفحة واحدة (SPA)، وتم إنشاء صفحات قائمة الخضراوات وتفاصيل الخضروات كتطبيق تقليدي متعدد الصفحات (MPA).

لقطة شاشة نموذج التطبيق على الأجهزة الجوّالة لقطة شاشة نموذج التطبيق على الأجهزة الجوّالة

وعلى وجه التحديد، يتم تنفيذ العرض المُسبَق والتخزين المؤقت للصفحات (bfcache) والخادم الوكيل الخاص للجلب المُسبَق للتنقل الفوري وعمليات انتقال العناصر الجذر/المشتركة لعمليات الانتقال السلسة للصفحات. عليك تنفيذ العرض المُسبَق والتخزين المؤقت لصفحات MPA وانتقالات العناصر المشتركة لصفحات SPA.

تشكّل سرعة الموقع الإلكتروني دائمًا جانبًا مهمًا من تجربة المستخدم، لذلك طرحت Google مؤشرات أداء الويب الأساسية، وهي مجموعة من المقاييس التي تقيس أداء التحميل والتفاعل والثبات البصري لصفحات الويب لقياس تجربة المستخدم الفعلية. تساعدك أحدث واجهات برمجة التطبيقات في تحسين نتيجة "مؤشرات أداء الويب الأساسية" لموقعك الإلكتروني في هذا المجال، خاصةً من حيث أداء التحميل.

الصورة التوضيحية كيف يحسِّن bfcache وقت التحميل

عرض توضيحي من Mindvalley

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

عرض توضيحي لـ Shared Element Transitions API من pixiv عرض توضيحي لـ Element Transitions API من Tokopedia

عروض توضيحية من pixiv وTokopedia

المتطلبات الأساسية

المعرفة بـ:

ما سوف تتعلمه:

كيفية التنفيذ:

  • العرض المُسبَق
  • bfcache
  • الخادم الوكيل الخاص للجلب المُسبَق
  • انتقالات الجذر/العنصر المشترك

ما الذي ستقوم ببنائه

نموذج لتطبيق ويب تم إنشاؤه باستخدام Next.js يحتوي على أحدث إمكانات المتصفح الفورية والسلسة:

  • تنقُّل شبه فوري باستخدام العرض المُسبَق
  • bfcache للتحميل الفوري باستخدام زري الانتقال للخلف وللأمام في المتصفح
  • انطباعات أولى رائعة من التنقل من مصادر متعددة باستخدام خادم الجلب المسبق الخاص أو Signed Exchange (SXG)
  • انتقال سلس بين الصفحات باستخدام نقل العناصر الجذر أو المشتركة

المتطلبات

  • الإصدار 101 من Chrome أو الإصدارات الأحدث

2. البدء

تفعيل علامات Chrome

  1. انتقِل إلى about://flags، ثم فعِّل علامتَي وقت التشغيل Prerender2 وdocumentTransition API.
  2. إعادة تشغيل المتصفّح

الحصول على الرمز‏

  1. افتح الرمز من مستودع GitHub في بيئة التطوير المفضلة لديك:
git clone -b codelab git@github.com:googlechromelabs/instant-seamless-demo.git
  1. ثبِّت التبعيات المطلوبة لتشغيل الخادم:
npm install
  1. ابدأ تشغيل الخادم على المنفذ 3000:
npm run dev
  1. انتقِل إلى http://localhost:3000 في المتصفّح.

يمكنك الآن تعديل تطبيقك وتحسينه. وعند إجراء تغييرات، تتم إعادة تحميل التطبيق وتظهر التغييرات التي أجريتها مباشرةً.

3- دمج العرض المُسبَق

لغرض هذا العرض التوضيحي، يكون وقت تحميل صفحة تفاصيل الخضروات في نموذج التطبيق بطيئًا جدًا بسبب التأخير العشوائي من جانب الخادم. يمكنك التخلّص من وقت الانتظار هذا من خلال العرض المُسبَق.

لإضافة أزرار العرض المُسبَق إلى صفحة قائمة الخضروات والسماح لها بتشغيل العرض المُسبَق بعد أن ينقر المستخدم على الإعلان:

  1. أنشئ مكوّن زر يُدرج علامة النص البرمجي لقواعد التوقُّع بشكل ديناميكي:

components/prerender-button.js

import { useContext } from 'react'
import ResourceContext from './resource-context'

// You use resource context to manage global states.
// In the PrerenderButton component, you update the prerenderURL parameter when the button is clicked.
export default function PrerenderButton() {
  const { dispatch } = useContext(ResourceContext)
  const handleClick = (e) => {
    e.preventDefault()
    e.stopPropagation()
    const parent = e.target.closest('a')
    if (!parent) {
      return
    }
    const href = parent.getAttribute('href')
    dispatch({ type: 'update', prerenderURL: href })
  }

  return (
    <button className='ml-auto bg-gray-200 hover:bg-gray-300 px-4 rounded' onClick={handleClick}>
      Prerender
    </button>
  )
}
  1. استورِد المكوِّن PrerenderButton في ملف list-item.js.

components/list-item.js

// Codelab: Add a PrerenderButton component.
import PrerenderButton from './prerender-button'

...
function ListItemForMPA({ item, href }) {
  return (
    <a href={href} className='block flex items-center'>
      <Icon src={item.image} />
      <div className='text-xl'>{item.name}</div>
      {/* Codelab: Add PrerenderButton component. */}
      <PrerenderButton />
    </a>
  )
}
  1. أنشِئ مكوِّنًا لإضافة واجهة برمجة تطبيقات قواعد التوقُّع.

يُدرج المكوِّن SpeculationRules علامة نص برمجي ديناميكيًا في الصفحة عندما يعدِّل التطبيق حالة prerenderURL.

components/speculationrules.js

import Script from 'next/script'
import { useContext, useMemo } from 'react'
import ResourceContext from './resource-context'

export default function SpeculationRules() {
  const { state } = useContext(ResourceContext)
  const { prerenderURL } = state

  return useMemo(() => {
    return (
      <>
        {prerenderURL && (
          <Script id='speculationrules' type='speculationrules'>
            {`
            {
              "prerender":[
                {
                  "source": "list",
                  "urls": ["${prerenderURL}"]
                }
              ]
            }
          `}
          </Script>
        )}
      </>
    )
  }, [prerenderURL])
}
  1. دمج المكونات مع التطبيق.

Pages/app.js

// Codelab: Add the SpeculationRules component.
import SpeculationRules from '../components/speculationrules'

function MyApp({ Component, pageProps }) {
  useAnalyticsForSPA()

  return (
    <ResourceContextProvider>
      <Layout>
        <Component {...pageProps} />
      </Layout>
      {/* Codelab: Add SpeculationRules component */}
      <SpeculationRules />
      <Script id='analytics-for-mpa' strategy='beforeInteractive' src='/analytics.js' />
    </ResourceContextProvider>
  )
}

export default MyApp
  1. انقر على العرض المُسبَق.

يمكنك الآن ملاحظة التحسن الكبير في التحميل. في حالة الاستخدام الحقيقية، يتم تشغيل العرض المُسبَق للصفحة التي من المُرجَّح أن يزورها المستخدم في المرة التالية وفقًا لبعض الإرشادات.

فيديو توضيحي نموذجي للتطبيق للعرض المُسبَق

الإحصاءات

بشكلٍ تلقائي، يُرسِل ملف analytics.js في نموذج تطبيق الويب حدث مشاهدة صفحة على الويب عند وقوع حدث DOMContentLoaded. للأسف، ليس من الحكمة أن يتم تنشيط هذا الحدث أثناء مرحلة العرض المُسبَق.

لإضافة حدثَين document.prerendering وprerenderingchange لحلّ هذه المشكلة، اتّبِع الخطوات التالية:

  • أعِد كتابة ملف analytics.js:

public/analytics.js

  const sendEvent = (type = 'pageview') => {
    // Codelab: Make analytics prerendering compatible.
    // The pageshow event could happen in the prerendered page before activation.
    // The prerendered page should be handled by the prerenderingchange event.
    if (document.prerendering) {
      return
    }
    console.log(`Send ${type} event for MPA navigation.`)
    fetch(`/api/analytics?from=${encodeURIComponent(location.pathname)}&type=${type}`)
  }
  ...

  // Codelab: Make analytics prerendering compatible.
  // The prerenderingchange event is triggered when the page is activated.
  document.addEventListener('prerenderingchange', () => {
    console.log('The prerendered page was activated.')
    sendEvent()
  })
  ...

رائع، لقد عدّلت الإحصاءات بنجاح لتتوافق مع العرض المُسبَق. يمكنك الآن الاطّلاع على سجلات مشاهدة الصفحة في الوقت المناسب في وحدة تحكُّم المتصفّح.

4. إزالة أدوات حظر ذاكرة التخزين المؤقت

إزالة معالِج أحداث "unload"

إنّ حدوث حدث unload غير ضروري هو خطأ شائع جدًا، ولم يعُد يُنصَح باستخدامه. وذلك لا يمنع عمل ميزة bfcache من العمل فحسب، بل أنّه غير موثوق به أيضًا. على سبيل المثال، لا يتم تنشيطها دائمًا على الأجهزة الجوّالة وSafari.

بدلاً من حدث unload، يمكنك استخدام الحدث pagehide الذي يتم تشغيله في جميع الحالات عند تنشيط حدث unload وعند وضع صفحة في ذاكرة التخزين المؤقت.

لإزالة معالِج أحداث "unload":

  • في ملف analytics.js، استبدِل رمز معالج أحداث unload برمز معالِج أحداث pagehide:

public/analytics.js

// Codelab: Remove the unload event handler for bfcache.
// The unload event handler prevents the content from being stored in bfcache. Use the pagehide event instead.
window.addEventListener('pagehide', () => {
  sendEvent('leave')
})

تعديل عنوان عنصر التحكّم في ذاكرة التخزين المؤقت

إنّ الصفحات التي يتم عرضها باستخدام عنوان HTTP يتضمّن العنصر Cache-control: no-store لا تستفيد من ميزة استخدام ميزة bfcache في المتصفّح، لذا من المفيد استخدام هذا العنوان. على وجه التحديد، إذا كانت الصفحة لا تحتوي على معلومات مخصّصة أو مُهمّة، مثل حالة تسجيل الدخول، لن تحتاج على الأرجح إلى عرضها باستخدام عنوان HTTP الذي يتضمّن السمة Cache-control: no-store.

لتحديث عنوان التحكم في ذاكرة التخزين المؤقت لنموذج التطبيق:

  • تعديل الرمز البرمجي getServerSideProps:

Pages/vegetables/index.js

export const getServerSideProps = middleware(async (ctx) => {
  const { req, res } = ctx
  // Codelab: Modify the cache-control header.
  res.setHeader('Cache-Control', 'public, s-maxage=10, stale-while-revalidate=59')
  ...

Pages/vegetables/[name].js

export const getServerSideProps = middleware(async (ctx) => {
  const { req, res, query } = ctx
  // Codelab: Modify the cache-control header.
  res.setHeader('Cache-Control', 'public, s-maxage=10, stale-while-revalidate=59')
  ...

تحديد ما إذا تمت استعادة صفحة من ذاكرة التخزين المؤقت

يتم تنشيط حدث pageshow بعد حدث load مباشرةً عند تحميل الصفحة في البداية وفي أي وقت تتم فيه استعادة الصفحة من ذاكرة التخزين المؤقت. يحتوى حدث pageshow على السمة persisted، وهي قيمة صحيحة إذا تمت استعادة الصفحة من bfcache وfalse إذا لم تتم استعادتها. يمكنك استخدام السمة persisted للتمييز بين عمليات تحميل الصفحات العادية وعمليات استعادة ذاكرة التخزين المؤقت. من المفترض أن تطّلع خدمات الإحصاءات الرئيسية على ميزة "التخزين المؤقت للصفحات"، ولكن يمكنك التحقّق مما إذا تمت استعادة الصفحة من خلال هذه الميزة وإرسال الأحداث يدويًا.

لتحديد ما إذا تمت استعادة صفحة من خلال ميزة "التخزين المؤقت للصفحات":

  • أضِف هذا الرمز إلى ملف analytics.js.

public/analytics.js

  // Codelab: Use the pageshow event handler for bfcache.
  window.addEventListener('pageshow', (e) => {
    // If the persisted flag exists, the page was restored from bfcache.
    if (e.persisted) {
      console.log('The page was restored from bfcache.')
      sendEvent()
    }
  })

تصحيح الأخطاء في صفحة ويب

يمكن أن تساعدك "أدوات مطوّري برامج Chrome" في اختبار صفحاتك للتأكُّد من أنّها محسّنة للاستخدام مع ميزة "التخزين المؤقت للصفحات" وتحديد أي مشاكل قد تجعلها غير مؤهَّلة.

لاختبار صفحة معيّنة:

  1. انتقِل إلى الصفحة في Chrome.
  2. في أدوات مطوّري برامج Chrome، انقر على التطبيق > ميزة "التخزين المؤقت للصفحات" > إجراء الاختبار:

تحاول "أدوات مطوّري برامج Chrome" الخروج من الصفحة ثم الرجوع إليها لتحديد ما إذا كان من الممكن استعادة الصفحة من خلال ميزة "التخزين المؤقت للصفحات".

49bf965af35d5324.png

في حال نجاح الإجراء، تخبرك اللوحة بأنّه تمت استعادة الصفحة من خلال ميزة "التخزين المؤقت للصفحات":

47015a0de45f0b0f.png

وإذا لم تنجح الخطوات التي أجريتها، ستُعلمك اللوحة بأنّه لم تتم استعادة الصفحة وبسبب ذلك. إذا كان السبب هو أمر يمكنك معالجته بصفتك مطوِّرًا، ستخبرك اللجنة أيضًا بذلك.

dcf0312c3fc378ce.png

5- تفعيل الجلب المسبق على مستوى المواقع الإلكترونية

تبدأ عملية "الجلب المُسبَق" في عملية الجلب مبكرًا، حتى تكون وحدات البايت متوفِّرة في المتصفِّح عندما ينتقل المستخدم، ما يؤدي إلى تسريع عملية التنقّل. وهذه طريقة سهلة لتحسين مؤشرات Core Web Vitals والموازنة بين بعض أنشطة الشبكة قبل التنقّل. يؤدي ذلك إلى تسريع سرعة عرض أكبر محتوى مرئي (LCP) مباشرةً، وتوفير مساحة أكبر لمهلة الاستجابة الأولى (FID) ومتغيّرات التصميم التراكمية (CLS) أثناء التنقّل.

يُفعِّل الخادم الوكيل الخاص للجلب المُسبَق الجلب المُسبَق من مواقع إلكترونية متعددة، ولكنه لا يكشف عن معلومات خاصة عن المستخدم في الخادم الوجهة.

آلية عمل الخادم الوكيل للجلب الخاص المسبَق

تفعيل "الجلب المسبق على المواقع الإلكترونية" باستخدام "الخادم الوكيل الخاص للجلب المُسبَق"

يحتفظ مالكو المواقع الإلكترونية بالتحكّم في الجلب المسبق من خلال مورد نصائح الزيارات المعروف، على غرار /robots.txt لبرامج زحف الويب، والذي يتيح لخادم HTTP الإعلان عن أنّ برامج التنفيذ يجب أن تطبّق النصيحة المناسبة. في الوقت الحالي، يمكن لمالكي المواقع الإلكترونية تقديم نصيحة إلى الوكيل لمنع اتصالات الشبكة أو تقييدها. في المستقبل، قد تتم إضافة نصائح أخرى.

لاستضافة مرجع نصائح بشأن حركة المرور:

  1. أضِف هذا الملف الذي يشبه JSON:

public/.well-known/traffic-advice

[
  {
    "user_agent": "prefetch-proxy",
    "google_prefetch_proxy_eap": {
      "fraction": 1
    }
  }
]

الحقل google_prefetch_proxy_eap هو حقل خاص لبرنامج استخدام المنتج قبل إطلاقه والحقل fraction هو حقل للتحكّم في جزء عمليات الجلب المُسبَق المطلوبة التي يرسلها الخادم الوكيل الخاص للجلب المُسبَق.

يجب عرض التحذير بشأن حركة المرور باستخدام نوع MIME application/trafficadvice+json.

  1. في ملف next.config.js، اضبط عنوان الاستجابة:

next.config.js

const nextConfig = {
  // Codelab: Modify content-type for traffic advice file.
  async headers() {
    return [
      {
        source: '/.well-known/traffic-advice',
        headers: [
          {
            key: 'Content-Type',
            value: 'application/trafficadvice+json',
          },
        ],
      },
    ]
  },
}

module.exports = nextConfig

6- دمج واجهة برمجة تطبيقات العناصر المشتركة بين العناصر

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

تعمل الرسوم المتحركة المتجانسة التي يتم تحميلها على خفض الحِمل المعرفي لأنّ المستخدمين يظلون في السياق أثناء تنقلهم بين الصفحات، كما يقلّل من وقت الاستجابة المُلاحظ للتحميل لأنّ المستخدمين يرون محتوًى جذابًا ومبهجًا في هذه الأثناء. لهذه الأسباب، توفّر معظم الأنظمة الأساسية إعدادات أساسية سهلة الاستخدام تتيح للمطوّرين إنشاء عمليات انتقال سلسة، مثل Android وiOS وMacOS وWindows.

توفِّر Shared Element Transitions API للمطوّرين الإمكانية نفسها على الويب، بغض النظر عمّا إذا كانت عمليات النقل تمت في مستندات متعددة (MPA) أو داخل المستند (SPA).

عرض توضيحي لـ Shared Element Transitions API من pixiv عرض توضيحي لـ Element Transitions API من Tokopedia

عروض توضيحية من pixiv وTokopedia

لدمج واجهة برمجة التطبيقات Shared Element Transitions API لجزء SPA من نموذج التطبيق، عليك تنفيذ ما يلي:

  1. إنشاء عنصر جذب مخصّص لإدارة عملية النقل في ملف use-page-transition.js:

utils/use-page-transition.js

import { useEffect, useContext, useRef, useCallback } from 'react'
import ResourceContext from '../components/resource-context'

// Call this hook on this first page before you start the page transition. For Shared Element Transitions, you need to call the transition.start() method before the next page begins to render, and you need to do the Document Object Model (DOM) modification or setting of new shared elements inside the callback so that this hook returns the promise and defers to the callback resolve.
export const usePageTransitionPrep = () => {
  const { dispatch } = useContext(ResourceContext)

  return (elm) => {
    const sharedElements = elm.querySelectorAll('.shared-element')
    // Feature detection
    if (!document.createDocumentTransition) {
      return null
    }

    return new Promise((resolve) => {
      const transition = document.createDocumentTransition()
      Array.from(sharedElements).forEach((elm, idx) => {
        transition.setElement(elm, `target-${idx}`)
      })
      transition.start(async () => {
        resolve()
        await new Promise((resolver) => {
          dispatch({ type: 'update', transition: { transition, resolver } })
        })
      })
    })
  }
}

// Call this hook on the second page. Inside the useEffect hook, you can refer to the actual DOM element and set them as shared elements with the transition.setElement() method. When the resolver function is called, the transition is initiated between the captured images and newly set shared elements.
export const usePageTransition = () => {
  const { state, dispatch } = useContext(ResourceContext)
  const ref = useRef(null)
  const setRef = useCallback((node) => {
    ref.current = node
  }, [])

  useEffect(() => {
    if (!state.transition || !ref.current) {
      return
    }
    const { transition, resolver } = state.transition
    const sharedElements = ref.current.querySelectorAll('.shared-element')
    Array.from(sharedElements).forEach((elm, idx) => {
      transition.setElement(elm, `target-${idx}`)
    })
    resolver()
    return () => {
      dispatch({ type: 'update', transition: null })
    }
  })

  return setRef
}
  1. يمكنك استدعاء عنصر الجذب المخصّص usePageTransitionPrep() في صفحة القائمة، ثم استدعاء الدالة غير المتزامنة لتشغيل طريقة transition.start() داخل حدث click.

داخل الدالة، يتم جمع عناصر الفئة shared-element وتسجيلها كعناصر مشتركة.

components/list-item.js

// Codelab: Add the Shared Element Transitions API.
import { usePageTransitionPrep } from '../utils/use-page-transition'
...

function ListItemForSPA({ item, href }) {
  // Codelab: Add Shared Element Transitions.
  const transitionNextState = usePageTransitionPrep()
  const handleClick = async (e) => {
    const elm = e.target.closest('a')
    await transitionNextState(elm)
  }
  return (
    <Link href={href}>
      <a className='block flex items-center' onClick={handleClick}>
        <Icon src={item.image} name={item.name} className='shared-element' />
        <div className='text-xl'>{item.name}</div>
      </a>
    </Link>
  )
}
  1. في صفحة التفاصيل، يمكنك طلب عنصر الجذب usePageTransition() لإنهاء دالة معاودة الاتصال transition.start().

وفي معاودة الاتصال هذه، يتم أيضًا تسجيل العناصر المشتركة في صفحة التفاصيل.

Pages/fruits/[name].js

// Codelab: Add the Shared Element Transitions API.
import { usePageTransition } from '../../utils/use-page-transition'

const Item = ({ data }) => {
  const { name, image, amountPer, nutrition } = data
  // Codelab: Add the Shared Element Transitions API.
  const ref = usePageTransition()

  return (
    <div className={'flex flex-col items-center justify-center py-4 px-4 sm:flex-row'} ref={ref}>
      <div className='flex flex-col items-center sm:w-2/4'>
        <Image
          className='object-cover border-gray-100 border-2 rounded-full shared-element'
          src={image}
          width='240'
          height='240'
          alt={`picture of ${name}`}
        />
        <h1 className='text-4xl font-bold mt-4'>{name}</h1>
      </div>

      <div className='sm:w-2/4 w-full'>
        <Nutrition amountPer={amountPer} nutrition={nutrition} />
      </div>
    </div>
  )
...
}

يمكنك الآن ملاحظة أنّه تتم مشاركة عناصر الصورة في القائمة وصفحات التفاصيل، ويتم ربطها بسلاسة أثناء انتقال الصفحة. يمكنك أيضًا تخصيص الصورة المتحركة لجعلها أكثر روعة باستخدام العناصر الزائفة في CSS.

نموذج فيديو توضيحي للتطبيق بدون نقل العناصر المشتركة فيديو توضيحي نموذجي للتطبيق مع عملية نقل العناصر المشتركة

7. تهانينا

تهانينا! لقد أنشأت تطبيق ويب فوريًا وسلسًا مع تجربة مستخدم بسيطة وجذّابة وبسيطة.

مزيد من المعلومات

العرض المُسبَق

bfcache

الجلب المسبق عبر المواقع الإلكترونية

تنسيقات Signed Exchange

انتقالات الجذر/العنصر المشترك

لا تزال واجهات برمجة التطبيقات هذه في المراحل الأولى من التطوير، لذا يُرجى مشاركة ملاحظاتك على crbug.com أو باعتبارها مشاكل في مستودع GitHub لواجهات برمجة التطبيقات ذات الصلة.