کوئری مدیا در React به روش برنامه نویسی شده — راهنمای کاربردی

۹۶ بازدید
آخرین به‌روزرسانی: ۱۳ شهریور ۱۴۰۲
زمان مطالعه: ۷ دقیقه
کوئری مدیا در React به روش برنامه نویسی شده  — راهنمای کاربردی

طراحی «نقاط توقف» (breakpoints) مبتنی بر کوئری مدیا در فریمورک React.js برخی اوقات برای دستکاری UI به صورت درجا به جای نوشتن CSS و سپس نوشتن کوئری مدیا برای آن بسیار کارآمد خواهد بود. در این مقاله به بررسی پیاده‌سازی قلاب useBreakpoint با استفاده از روش‌هایی به جز رویداد resize می‌پردازیم. بدین ترتیب با کاربرد کوئری مدیا در React به روش برنامه‌نویسی‌شده آشنا می‌شویم.

مطابقت دادن مدیا

برای دستیابی به نقاط توقف کوئری مدیا باید شروع به بررسی مدیای خود بکنیم. به عبارت دیگر باید دستگاه‌هایی که اپلیکیشن روی آن‌ها اجرا شده بررسی کنیم و ببینیم اندازه صفحه کوچک یا بزرگ دارند. به این منظور جاوا اسکریپت یک متد به نام matchMedia روی شیء window ارائه می‌کند.

1window.matchMedia('(max-width: 100px)')

به طور مشابه بررسی می‌کنیم که چه زمانی سند بیشینه عرض 100 پیکسل دارد. متد matchMedia یک شیء MediaQueryList بازگشت می‌دهد که نماینده نتایج تحلیل شده کوئری مدیای مورد نظر است.

اساساً این شیء MediaQueryList همان چیزی است که برای تعیین زمان تطبیق document با کوئری مدیا مورد استفاده قرار می‌دهیم و همچنین document را رصد می‌کنیم تا تشخیص دهیم چه هنگام با کوئری مدیا مطابقت دارد یا ندارد.

1const mql = window.matchMedia(mediaQueryString);

استفاده از MediaQueryList

در زمان استفاده از MediaQueryList دو حالت پیش می‌آید:

  1. هنگامی که می‌خواهیم بی‌درنگ بررسی کنیم آیا سند ما با کوئری مدیا مطابقت دارد یا نه و یا وقتی می‌خواهیم این موضوع را تنها یک بار و احتمالاً پس از نصب شدن کامپوننت اصلی ری‌اکت بررسی کنیم.
  2. هنگامی که به صورت مرتب بررسی می‌کنیم آیا سند با کوئری مدیا مطابقت دارد یا نه. این حالت زمانی رخ می‌دهد که دستگاهی داشته باشیم که عرض یا طول صفحه آن بتواند تغییر یابد. مثلاً هنگامی که جهت گوشی از عمودی به افقی عوض می‌شود، چنین حالتی پیش می‌آید.

در این حالت، هنگامی که لازم باشد بررسی آنی انجام دهیم، می‌توانیم از مشخصه‌های matches در MediaQueryList استفاده کنیم. این یک مشخصه بولی است که اگر سند در حال حاضر با لیست کوئری مدیا مطابقت داشته باشد مقدار true و در غیر این صورت مقدار false بازگشت می‌دهد:

1mql.matches //returns true or false

در مورد حالت دوم، باید رویداد change را روی شیء MediaQueryList رصد کنیم. این شیء برای آغاز گوش دادن به رویداد change، یک متد به نام ()addListener ارائه کرده است. به طور مشابه برای توقف گوش دادن باید از متد ()removeListener استفاده کنیم.

1const listenerFunc = () => {
2// Listening logic goes here
3};
4mql.addListener(listenerFunc);

تابع گوش دادن هر بار که عرض سند تغییر یافت و یا جهت‌گیری دستگاه عوض شد، فراخوانی نمی‌شود، بلکه تنها در دو حالت احضار می‌شود. یکی زمانی که سند با mediaQueryString مطابقت پیدا کند و دوم زمانی که مطابقت آن با mediaQueryString از دست برود.

چه کار باید بکنیم؟

ما یک شیء از کاربر قلاب سفارشی خود انتظار داریم. در این شیء مقدار هر مشخصه یک رشته کوئری مدیا دارد:

1const queries = {
2xs: '(max-width: 320px)', //query for xs devices
3sm: '(max-width: 720px)',
4md: '(max-width: 1024px)'
5}

در خروجی نیز انتظار دریافت یک شیء داریم که همه کلیدهای ارائه شده در شیء queries را داشته باشد. در این شیء هر مقدار مشخصه/کلید بولی خواهد بود. در صورتی که گزاره مطابقت یابد، مقدار آن true و در غیر این صورت مقدار آن false خواهد بود.

برای نمونه فرض کنید عرض صفحه ما 640 پیکسل است. در این صورت خروجی که برای queries فوق انتظار داریم به صورت زیر خواهد بود:

1{
2xs: false,
3sm: true,
4md: true
5}
6// here our matches are sm and md

برای به دست آوردن خروجی فوق مراحل زیر را طی می‌کنیم:

  • حلقه‌ای تعریف می‌کنیم که روی هر مشخصه شیء queries چرخیده و برای هر کوئری ()matchMedia را فرا می‌خواند (کد در ادامه ارائه شده است).
  • هر فراخوانی به matchMedia برای یک کوئری یک شیء MediaQueryList برای آن کوئری بازگشت می‌دهد. ما شیء MediaQueryList را در یک شیء جدید ذخیره می‌کنیم که متناظر با همان کلید است.
  • ضمناً برای به دست آوردن کوئری مطابق با سند (document) جاری و دانستن نوع دستگاه در زمان آغاز به کار، از مشخصه matches روی همه شیءهای MediaQueryList استفاده می‌کنیم.
1const mediaQueryLists = {}; //Add all MediaQueryList object here.
2const keys = Object.keys(queries);
3const matches = {}; //contains initial query matches
4
5keys.forEach(media => {
6if (typeof queries[media] === 'string') {
7/* Adding MediaQueryList object for each and every query to
8  mediaQueryLists object
9 */
10mediaQueryLists[media] = window.matchMedia(queries[media]);
11
12//Get initial matches of each query
13matches[media] = mediaQueryLists[media].matches 
14} else {
15matches[media] = false
16}
17});

اکنون باید حالت‌هایی که کوئری‌های ما در شیء queries تغییر می‌یابند را نیز مدیریت کنیم. این‌ها مواردی هستند که سند مطابقت با یک کوئری را آغاز کرده یا خاتمه می‌بخشد. به این منظور با استفاده از ()addListener به رویداد change روی هر یک از آیتم‌های MediaQueryList گوش می‌کنیم (به کد زیر توجه کنید).

یک تابع handler تعریف می‌کنیم که در آن روی همه کلیدهای شیء queries می‌چرخیم و نتیجه مشخصه matches را برای هر شیء MediaQueryList ذخیره می‌نماییم.

نکته: این تابع handler هر زمان که سند شروع به مطابقت با یک کوئری در شیء queries بکند یا این مطابقت متوقف شود فراخوانی خواهد شد.

1 const handleQueryListener = () => {
2      const updatedMatches = keys.reduce((acc, media) => {
3        acc[media] = !!(mediaQueryLists[media] && mediaQueryLists[media].matches);
4        return acc;
5      }, {})
6      console.log('Matches', updatedMatches)
7    }
8 
9 keys.forEach(media => {
10        if(typeof queries[media] === 'string') {
11          mediaQueryLists[media].addListener(handleQueryListener)
12        }
13      });

پیاده‌سازی در ری‌اکت

بررسی کوئری مدیا در کد ما به صورت یک اثر جانبی است و از این رو این منطق را درون قلاب useEffect قرار می‌دهیم. همچنین باید بررسی کنیم آیا مرورگر از matchMedia پشتیبانی می‌کند یا نه.

در همین راستا یک «حالت» (State) نگهداری می‌کنیم که نتیجه همه کوئری‌های مدیا را که با document مطابقت دارد یا ندارد ذخیره می‌کند.

1const useBreakpoint = (queries) => {
2  const [queryMatch, setQueryMatch] = useState(null);
3
4  useEffect(() => {
5    const mediaQueryLists = {};
6    const keys = Object.keys(queries);
7    
8    // To check whether event listener is attached or not
9    let isAttached = false;
10
11    const handleQueryListener = () => {
12      const updatedMatches = keys.reduce((acc, media) => {
13        acc[media] = !!(mediaQueryLists[media] && mediaQueryLists[media].matches);
14        return acc;
15      }, {})
16      //Setting state to the updated matches 
17      // when document either starts or stops matching a query
18      setQueryMatch(updatedMatches)
19    }
20
21    if (window && window.matchMedia) {
22      const matches = {};
23      keys.forEach(media => {
24        if (typeof queries[media] === 'string') {
25          mediaQueryLists[media] = window.matchMedia(queries[media]);
26          matches[media] = mediaQueryLists[media].matches
27        } else {
28          matches[media] = false
29        }
30      });
31      //Setting state to initial matching queries
32      setQueryMatch(matches);
33      isAttached = true;
34      keys.forEach(media => {
35        if(typeof queries[media] === 'string') {
36          mediaQueryLists[media].addListener(handleQueryListener);
37        }
38      });
39    }
40
41    return () => {
42    //If event listener is attached then remove it when deps change
43      if(isAttached) {
44        keys.forEach(media => {
45          if(typeof queries[media] === 'string') {
46            mediaQueryLists[media].removeListener(handleQueryListener);
47          }
48        });
49      }
50    }
51  }, [queries]);
52
53  return queryMatch;
54}
55
56export default useBreakpoint;

اکنون از این قلاب مانند هر قلاب دیگری در اپلیکیشن خود استفاده می‌کنیم.

1import useBreakpoint from './useBreakpointext'
2const queries = {
3  xs: '(max-width: 320px)',
4  md: '(max-width: 720px)',
5  lg: '(max-width: '1024px)',
6}
7const App = () => {
8  const matchPoints = useBreakpoint(queries);
9  //Rest Code goes here
10}

آیا این روش کارآمدی محسوب می‌شود؟

شاید متوجه شده باشید که این روش چندان بهینه نیست، زیرا اگر مجبور باشیم از قلاب useBreakpoint در هزاران کامپوننت استفاده کنیم، باید useBreakpoint را بارها و بارها فراخوانی کنیم.

بدین ترتیب در صورتی که از چند کامپوننت نصب شده استفاده کنیم و همه آن‌ها از قلاب useBreakpoint استفاده کنند، باید شنونده رویداد change را ثبت کنیم که بسیار غیر بهینه است.

روش بهینه کدام است؟

برای حل این مشکل باید از context ری‌اکت استفاده کنیم. بنابراین به جای استفاده مستقیم از React context از context بهره می‌گیریم و سپس آن را در قلاب سفارشی خود مورد استفاده قرار می‌دهیم. با استفاده از React.createContext(defaltValue) اقدام به ساخت context می‌کنیم.

1const defaultValue = {};
2const BreakpointContext = createContext(defaultValue);

ما باید کدی را که می‌خواهیم به مقدار context دسترسی داشته باشد، درون یک Provider قرار دهیم. در این مورد از BreakpointContext.Provider استفاده می‌کنیم. سپس برای دسترسی به این مقدار از قلاب ری‌اکت به نام useContext استفاده می‌کنیم.

اما به جای استفاده مستقیم از BreakpointContext.Provider یک پوشش پیرامون آن ایجاد می‌کنیم:

1import React, {
2  useState, 
3  createContext,
4  useContext} from 'react';
5
6const defaultValue = {}
7
8const BreakpointContext = createContext(defaultValue);
9
10const BreakpointProvider = ({children, queries}) => {
11  //State in which we maintain our matching query
12  const [queryMatch, setQueryMatch] = useState({});
13  
14  //Our Logic to get matching query goes here
15
16  return (
17    <BreakpointContext.Provider value={queryMatch}>
18      {children}
19    </BreakpointContext.Provider>
20  )
21
22}
23
24function useBreakpoint() {
25  const context = useContext(BreakpointContext);
26  if(context === defaultValue) {
27    throw new Error('useBreakpoint must be used within BreakpointProvider');
28  }
29  return context;
30}
31export {useBreakpoint, BreakpointProvider};

useBreakpoint یک تابع است که در آن مقدار context را بازگشت می‌دهیم و اساساً با نتیجه کوئری مطابقت داد.

ما اپلیکیشن خود را در BreakpointProvider قرار می‌دهیم و یک prop کوئری به آن ارسال می‌کنیم. به این ترتیب useBreakpoint تنها می‌تواند در کامپوننت‌های قرار گرفته درون BreakpointProvider مورد استفاده قرار گیرد. بدین ترتیب در نهایت همه چیز را در کنار هم قرار می‌دهیم:

1import React, {
2  useState,
3  useEffect,
4  createContext,
5  useContext} from 'react';
6
7const defaultValue = {}
8
9const BreakpointContext = createContext(defaultValue);
10
11const BreakpointProvider = ({children, queries}) => {
12  const [queryMatch, setQueryMatch] = useState({});
13
14  useEffect(() => {
15    const mediaQueryLists = {};
16    const keys = Object.keys(queries);
17    let isAttached = false;
18
19    const handleQueryListener = () => {
20      const updatedMatches = keys.reduce((acc, media) => {
21        acc[media] = !!(mediaQueryLists[media] && mediaQueryLists[media].matches);
22        return acc;
23      }, {})
24      setQueryMatch(updatedMatches)
25    }
26
27    if (window && window.matchMedia) {
28      const matches = {};
29      keys.forEach(media => {
30        if (typeof queries[media] === 'string') {
31          mediaQueryLists[media] = window.matchMedia(queries[media]);
32          matches[media] = mediaQueryLists[media].matches
33        } else {
34          matches[media] = false
35        }
36      });
37      setQueryMatch(matches);
38      isAttached = true;
39      keys.forEach(media => {
40        if(typeof queries[media] === 'string') {
41          mediaQueryLists[media].addListener(handleQueryListener)
42        }
43      });
44    }
45
46    return () => {
47      if(isAttached) {
48        keys.forEach(media => {
49          if(typeof queries[media] === 'string') {
50            mediaQueryLists[media].removeListener(handleQueryListener)
51          }
52        });
53      }
54    }
55  }, [queries]);
56
57  return (
58    <BreakpointContext.Provider value={queryMatch}>
59      {children}
60    </BreakpointContext.Provider>
61  )
62
63}
64
65function useBreakpoint() {
66  const context = useContext(BreakpointContext);
67  if(context === defaultValue) {
68    throw new Error('useBreakpoint must be used within BreakpointProvider');
69  }
70  return context;
71}
72export {useBreakpoint, BreakpointProvider};

کاربرد

روش بکارگیری BreakpointProvider به صورت زیر است:

1import React from 'react';
2import ReactDOM from 'react-dom';
3import App from './App';
4import {BreakpointProvider} from './breakpoint'
5
6
7
8const queries = {
9  xs: '(max-width: 320px)',
10  sm: '(max-width: 720px)',
11  md: '(max-width: 1024px)',
12  or: '(orientation: portrait)', // we can check orientation also
13}
14
15ReactDOM.render(
16<BreakpointProvider queries={queries}>
17  <App />
18</BreakpointProvider>, document.getElementById('root'));

روش بکارگیری با useBreakpoint به صورت زیر است:

1import React from 'react'
2import {useBreakpoint} from './breakpoint.js'
3
4const Usage = () => {
5  const breakpoints = useBreakpoint();
6  
7  const matchingList = Object.keys(breakpoints).map(media => (
8    <li key={media}>{media} ---- {breakpoints[media] ? 'Yes' : 'No'}</li>
9  ))
10  
11  return (
12    <ol>
13      {matchingList}
14    </ol>
15  )
16}

با استفاده از این روش می‌توانیم علاوه بر بررسی max-width یا min-width، جهت‌گیری افقی یا عمودی دستگاه و بسیاری از کوئری‌های مدیای دیگر را نیز اجرا کنیم.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۰ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
better-programming
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *