اذهب إلى المحتوى

تنفيذ عملية تسجيل الدخول في الواجهة الأمامية في تطبيقات React


ابراهيم الخضور

لقد انصب اهتمامنا في القسمين السابقين على الواجهة الخلفية بشكل رئيسي، فلم تُدعم الواجهة الأمامية بوظائف إدارة المستخدمين التي أضفناها إلى الواجهة الخلفية في القسم 4 السابق (الذي يبدأ من درس مدخل إلى Node.js وExpress).

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

سنضيف جزءًا من وظيفة إدارة المستخدمين إلى الواجهة الأمامية. سنبدأ بواجهة تسجيل الدخول، وسنفترض عدم إمكانية إضافة مستخدمين جدد من الواجهة الأمامية.

أُضيف نموذج تسجيل الدخول إلى أعلى الصفحة، وكذلك نُقل نموذج إضافة ملاحظة جديدة إلى أعلى قائمة الملاحظات

login_form_01.png

سيبدو شكل المكوّن App كالتالي:

const App = () => {
  const [notes, setNotes] = useState([]) 
  const [newNote, setNewNote] = useState('')
  const [showAll, setShowAll] = useState(true)
  const [errorMessage, setErrorMessage] = useState(null)
  const [username, setUsername] = useState('')   
  const [password, setPassword] = useState('') 
  useEffect(() => {
    noteService
      .getAll().then(initialNotes => {
        setNotes(initialNotes)
      })
  }, [])

  // ...

  const handleLogin = (event) => {    
event.preventDefault()    
console.log('logging in with', username, password)  
}
  return (
    <div>
      <h1>Notes</h1>

      <Notification message={errorMessage} />

      <form onSubmit={handleLogin}>        
          <div>          
          username            
<input            
type="text"            
value={username}            
name="Username"            
onChange={({ target }) => setUsername(target.value)}          
/>        
</div>        
    <div>
    password
<input
type="password"            
value={password}
name="Password"            
onChange={({ target }) => setPassword(target.value)}          
/>        
</div>        
    <button type="submit">login</button>      
</form>
      // ...
    </div>
  )
}

export default App

يمكن أن تجد شيفرة التطبيق بوضعها الحالي في الفرع part5-1 على GitHub

نتعامل مع نموذج تسجيل الدخول بالطريقة ذاتها التي تعاملنا فيها مع النماذج في القسم 2. تحتوي حالة التطبيق على حقول لكلمة السر واسم المستخدم لتخزين البيانات الموجودة في نموذج تسجيل الدخول. ولتلك الحقول معالجات أحداث تُزامن التغيرات في الحقول مع حالة المكون App. تعتمد معالجات الأحداث مبدأً بسيطًا، حيث يمرر كائن لها كوسيط ثم تفكك الحقل عن الكائن وتخزن قيمته في حالة التطبيق.

({ target }) => setUsername(target.value)

لم نضف التابع handlelogin المسؤول عن التعامل مع البيانات في النموذج بعد.

تجري عملية تسجيل الدخول بإرسال طلب HTTP POST إلى عنوان الموقع api/login/. لهذا سنضع الشيفرة المسؤولة عن هذا الطلب في وحدة مستقلة خاصة باسم login.js ضمن المجلد services.

سنستخدم async/await بدلًا من الوعود للتعامل مع طلبات HTTP.

import axios from 'axios'
const baseUrl = '/api/login'

const login = async credentials => {
  const response = await axios.post(baseUrl, credentials)
  return response.data
}

export default { login }

يمكن إضافة التابع المسؤول عن تسجيل الدخول كالتالي:

import loginService from './services/login' 

const App = () => {
  // ...
  const [username, setUsername] = useState('') 
  const [password, setPassword] = useState('') 
  const [user, setUser] = useState(null)
  const handleLogin = async (event) => {
    event.preventDefault()

    try {
        const user = await loginService.login({
            username, password,
        })
        setUser(user)
        setUsername('')
        setPassword('')
    }
      catch (exception) 
      {      
          setErrorMessage('Wrong credentials')      
          setTimeout(() => {        
              setErrorMessage(null)      
          }, 5000)    
      }  
  }

  // ...
}

إن نجح تسجيل الدخول، ستُفرَّغ حقول نموذج التسجيل، وتُحفظ استجابة الخادم (بما فيها الشهادة والتفاصيل) في الحقل user من حالة التطبيق. أما إن فشل تسجيل الدخول، أو نتج خطأ عن تنفيذ التابع loginService.login، سيتلقى المستخدم إشعارًا بذلك، ولن يتلقى المستخدم إشعارًا بنجاح العملية.

لنعدّل التطبيق بحيث يظهر نموذج تسجيل الدخول فقط إذا لم يكن قد سجل المستخدم دخوله بعد. أي عندما تكون قيمة الحقل user لاشيء (Null). وكذلك لن يظهر نموذج إضافة ملاحظة جديدة إن لم يسجل المستخدم دخوله، أي يجب أن يحتوي الحقل user تفاصيل المستخدم.

سنضيف دالتين مساعدتين إلى المكوِّن App لتوليد النموذجين:

const App = () => {
  // ...

  const loginForm = () => (
    <form onSubmit={handleLogin}>
      <div>
        username
          <input
          type="text"
          value={username}
          name="Username"
          onChange={({ target }) => setUsername(target.value)}
        />
      </div>
      <div>
        password
          <input
          type="password"
          value={password}
          name="Password"
          onChange={({ target }) => setPassword(target.value)}
        />
      </div>
      <button type="submit">login</button>
    </form>      
  )

  const noteForm = () => (
    <form onSubmit={addNote}>
      <input
        value={newNote}
        onChange={handleNoteChange}
      />
      <button type="submit">save</button>
    </form>  
  )

  return (
    // ...
  )
}

ثم سنصيّرهما شرطيًا:

const App = () => {
  // ...

  const loginForm = () => (
    // ...
  )

  const noteForm = () => (
    // ...
  )

  return (
    <div>
      <h1>Notes</h1>

      <Notification message={errorMessage} />

      {user === null && loginForm()}      
      {user !== null && noteForm()}
      <div>
        <button onClick={() => setShowAll(!showAll)}>
          show {showAll ? 'important' : 'all'}
        </button>
      </div>
      <ul>
        {notesToShow.map((note, i) => 
          <Note
            key={i}
            note={note} 
            toggleImportance={() => toggleImportanceOf(note.id)}
          />
        )}
      </ul>

      <Footer />
    </div>
  )
}

سيبدو شكلها غريبًا قليلًا، إلا أنها حيلة في React تُستخدم لتصيير النماذج شرطيًا:

{
  user === null && loginForm()
}

فإن كانت نتيجة تنفيذ العبارة الأولى (خطأ)، أو كانت قيمتها خاطئة، لن تُنفَّذ العبارة الثانية (العبارة التي تولد النموذج).

كما يمكننا تنفيذ ذلك مباشرة باستخدام العامل الشرطي:

return (
  <div>
    <h1>Notes</h1>

    <Notification message={errorMessage}/>

    {user === null ?
      loginForm() :
      noteForm()
    }

    <h2>Notes</h2>

    // ...

  </div>
)

إن كانت نتيجة الموازنة user === null صحيحة، سيُنفَّذ الأمر ()loginForm، وإلا سيُنفَّذ الأمر ()noteForm.

سنجري تعديلًا أخيرًا يظهر فيه اسم المستخدم على الشاشة عندما يسجل دخوله:

return (
  <div>
    <h1>Notes</h1>

    <Notification message={errorMessage} />

    {user === null ?
      loginForm() :
      <div>
        <p>{user.name} logged-in</p>
        {noteForm()}
      </div>
    }

    <h2>Notes</h2>

    // ...

  </div>
)

لم نقدم الحل المثالي بعد، لكننا سنكتفي بما فعلناه حاليًا.

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

ستجد شيفرة التطبيق ضمن الفرع part5-2 على GitHub.

إنشاء ملاحظات جديدة

تُحفظ الشهادة التي تعاد من الخادم بعد نجاح تسجيل الدخول ضمن الحقل user.token في حالة التطبيق.

const handleLogin = async (event) => {
  event.preventDefault()
  try {
    const user = await loginService.login({
      username, password,
    })

    setUser(user)
    setUsername('')
    setPassword('')
  } 
    catch (exception) 
    {
    // ...
  }
}

لنصلح عملية إنشاء ملاحظة جديدة لتعمل مع الواجهة الخلفية، بإضافة شهادة المستخدم (بعد أن يسجل دخوله) إلى ترويسة تفويض طلب HTTP.

ستصبح الوحدة noteService على النحو التالي:

import axios from 'axios'
const baseUrl = '/api/notes'

let token = null
const setToken = newToken => {  
    token = `bearer ${newToken}
`}
const getAll = () => {
  const request = axios.get(baseUrl)
  return request.then(response => response.data)
}

const create = async newObject => {
  const config = {    
      headers: { Authorization: token },
  }
  const response = await axios.post(baseUrl, newObject, config)
  return response.data   
}

const update = (id, newObject) => {
  const request = axios.put(`${ baseUrl } /${id}`, newObject)
  return request.then(response => response.data)
}

export default { getAll, create, update, setToken }

تحتوي الوحدة على المتغير الخاص token، الذي يمكن تغيير قيمته باستخدام الدالة setToken التي يصدّرها النموذج. تضع الدالة الشهادة في ترويسة التفويض باستعمال async/await. ولاحظ أن هذه الدالة ستُمرَّر كمعامل ثالث إلى التابع post العائد للمكتبة axios. ينبغي تغيير معالج الحدث المسؤول عن تسجيل الدخول ليستدعي التابع (noteService.setToken(user.token عند نجاح تسجيل الدخول:

const handleLogin = async (event) => {
  event.preventDefault()
  try {
    const user = await loginService.login({
      username, password,
    })

    noteService.setToken(user.token)    
    setUser(user)
    setUsername('')
    setPassword('')
  } catch (exception) {
    // ...
  }
}

وهكذا سنتمكن من إضافة ملاحظات جديدة.

تخزين الشهادة في ذاكرة المتصفح

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

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

window.localStorage.setItem('name', 'juha tauriainen')

حيث يخزّن التابع في الشيفرة السابقة مثلًا، المعامل الثاني كقيمة للمفتاح name.

يمكن الحصول على قيمة المفتاح باستخدام التابع getItem.

window.localStorage.getItem('name')

كما يمكن حذفه باستخدام التابع removeItem.

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

إن القيم التي تحفظ في الذاكرة من النوع DOMstrings، لذلك لن نتمكن من حفظ كائن JavaScript كما هو. إذًا، يجب تحويل الكائن إلى تنسيق JSON أولًا باستخدام التابع JSON.stringify. وبالعكس لابد من تحويل المعلومات التي تُقرأ من الذاكرة إلى كائن JavaScript باستخدام التابع JSON.parse.

ستصبح شيفرة تسجيل الدخول كالتالي:

  const handleLogin = async (event) => {
    event.preventDefault()
    try {
      const user = await loginService.login({
        username, password,
      })

      window.localStorage.setItem(        
          'loggedNoteappUser', JSON.stringify(user)
      )       
      noteService.setToken(user.token)
      setUser(user)
      setUsername('')
      setPassword('')
    } catch (exception) {
      // ...
    }
  }

وهكذا سنحتفظ ببيانات تسجيل الدخول في الذاكرة المحلية والتي يمكن عرضها على الطرفية:

local_storage_values_02.png

سنتمكن أيضًا من تحري الذاكرة المحلية باستخدام أدوات التطوير. فإن كنت من مستخدمي Chrome، توجه إلى النافذة Application واختر Local storage (ستجد تفاصيل أكثر على موقع مطوري Google)، بينما لو كنت من مستخدمي Firefox توجه إلى النافذة Storage واختر Local Storage (ستجد تفاصيل أكثر على موقع مطوري Mozilla).

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

أما الطريقة الصحيحة لتنفيذ الأمر هي استخدام خطاف التأثير، وهي آلية قدمناها لأول مرة في القسم 2، واستعملناها لإحضار الملاحظات من الخادم.

نستطيع استخدام عدة خطافات تأثير، لذلك سننشئ واحدًا جديدًا ليتعامل مع أول تحميل للصفحة.

const App = () => {
  const [notes, setNotes] = useState([]) 
  const [newNote, setNewNote] = useState('')
  const [showAll, setShowAll] = useState(true)
  const [errorMessage, setErrorMessage] = useState(null)
  const [username, setUsername] = useState('') 
  const [password, setPassword] = useState('') 
  const [user, setUser] = useState(null) 

  useEffect(() => {
    noteService
      .getAll().then(initialNotes => {
        setNotes(initialNotes)
      })
  }, [])

  useEffect(() => {    
      const loggedUserJSON= window.localStorage.getItem('loggedNoteappUser')    
      if (loggedUserJSON) {      
          const user = JSON.parse(loggedUserJSON)      
          setUser(user)      
          noteService.setToken(user.token)    
      }  
  }, 
            [])
  // ...
}

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

يمكن تسجيل الخروج باستعمال الطرفية وهذا كافٍ حتى اللحظة. حيث يمكن تسجيل الخروج بتنفيذ الأمر:

window.localStorage.removeItem('loggedNoteappUser')

أو بالأمر التالي الذي يفرّغ الذاكرة المحليّة:

window.localStorage.clear()

يمكنك إيجاد الشيفرة الحالية للتطبيق ضمن الفرع part5-3 على GitHub.

التمارين 5.1 - 5.4

سنطور في هذه التمارين واجهة أمامية لتطبيق قائمة المدونات. يمكنك الاستفادة من التطبيق المساعد الموجود على GitHub كأساس لحلك. يتوقع التمرين أن تعمل الواجهة الخلفية على المنفذ 3001.

يكفي أن تسلم التمرين بشكله النهائي، كما يمكنك ترك تعليق بعد كل تمرين، لكن ذلك غير ضروري.

تذكرك التمارين القليلة الأولى بكل ما تعلمته حتى الآن عن React. وستحمل أيضًا قدرًا من التحدي، وخاصة إن لم تكن واجهتك الخلفية مكتملة. قد يكون من الأفضل استخدام الواجهة الخلفية لنموذج الحل الذي اعتمدناه في القسم 4.

تذكر كل الطرق التي تعلمناها في تنقيح التطبيقات أثناء كتابتك للشيفرة، وابق عينيك على طرفية التطوير دائمًا.

تحذير: إن لاحظت أنك تستخدم await/async مع then، فتأكد أنك ستقع في الأخطاء بنسبة %99. استخدم أحد الأسلوبين وليس كلاهما.

5.1 الواجهة الأمامية لقائمة المدونات: الخطوة 1

انسخ التطبيق المساعد من GitHub بتنفيذ الأمر التالي:

git clone https://github.com/fullstack-hy2020/bloglist-frontend

أزل ملف تهيئة git من التطبيق الذي نسخته:

cd bloglist-frontend   // go to cloned repository
rm -rf .git

يمكنك تشغيل التطبيق بالطريقة الاعتيادية، لكن عليك أولًا تثبيت الاعتماديات:

npm install
npm start

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

user_login_form_03.png

ستظهر قائمة المدونات بالإضافة إلى اسم المستخدم عندما ينجح في تسجيل دخوله.

login_ok_04.png

لا يجب عليك تخزين تفاصيل تسجيل الدخول ضمن الذاكرة المحلية للمتصفح بعد.

ملاحظة: يمكن أن تساعدك الشيفرة التالية في تنفيذ التصيير الشرطي للصفحة عند تسجيل الدخول:

  if (user === null) {
    return (
      <div>
        <h2>Log in to application</h2>
        <form>
          //...
        </form>
      </div>
    )
  }

  return (
    <div>
      <h2>blogs</h2>
      {blogs.map(blog =>
        <Blog key={blog.id} blog={blog} />
      )}
    </div>
  )
}

الواجهة الأمامية لقائمة المدونات: الخطوة 2

اجعل تسجيل الدخول مستمرًا مستخدمًا الذاكرة المحلية للمتصفح، وأضف طريقة لتسجيل الخروج.

perm_login_05.png

تأكد من عدم تذكر المتصفح أية تفاصيل عن المستخدم بعد تسجيل الخروج.

5.3 الواجهة الأمامية لقائمة المدونات: الخطوة 3

وسع التطبيق بحيث يتمكن المستخدم من إضافة مدونات جديدة.

add_new_blog_06.png

5.4 الواجهة الأمامية لقائمة المدونات: الخطوة 4 *

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

notification_new_blog_07.png

وإشعار كهذا عند فشل تسجيل الدخول:

notification_login_failed_08.png

يجب أن يظهر الإشعار لثوان عدة، وليس ضروريًا إضافة الألوان.

ترجمة -وبتصرف- للفصل Login in frontend من سلسلة Deep Dive Into Modern Web Development


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...