تحسين تطبيقات Next.js باستخدام Nx
نشرت: 2022-03-10في هذه المقالة ، سنتعرف على كيفية تحسين وبناء تطبيق Next.js عالي الأداء باستخدام Nx وميزاته الغنية. سنتعرف على كيفية إعداد خادم Nx ، وكيفية إضافة مكون إضافي إلى خادم موجود ، ومفهوم monorepo مع تصور عملي.
إذا كنت مطورًا يتطلع إلى تحسين التطبيقات وإنشاء مكونات قابلة لإعادة الاستخدام عبر التطبيقات بشكل فعال ، فستوضح لك هذه المقالة كيفية توسيع نطاق تطبيقاتك بسرعة ، وكيفية العمل مع Nx. للمتابعة ، ستحتاج إلى معرفة أساسية بإطار عمل Next.js و TypeScript.
ما هو Nx؟
Nx هو إطار عمل بناء مفتوح المصدر يساعدك على التصميم والاختبار والبناء على أي نطاق - يتكامل بسلاسة مع التقنيات والمكتبات الحديثة ، مع توفير واجهة سطر أوامر قوية (CLI) والتخزين المؤقت وإدارة التبعية. تقدم Nx للمطورين أدوات وإضافات CLI متقدمة للأطر والاختبارات والأدوات الحديثة.
في هذه المقالة ، سنركز على كيفية عمل Nx مع تطبيقات Next.js. توفر Nx أدوات قياسية للاختبار والتصميم في تطبيقات Next.js ، مثل Cypress و Storybook والمكونات المصممة. تسهل Nx عملية monorepo لتطبيقاتك ، مما يؤدي إلى إنشاء مساحة عمل يمكنها الاحتفاظ بكود المصدر ومكتبات لتطبيقات متعددة ، مما يسمح لك بمشاركة الموارد بين التطبيقات.
لماذا نستخدم Nx؟
يوفر Nx للمطورين قدرًا معقولًا من الوظائف الجاهزة ، بما في ذلك النماذج المعيارية للاختبار الشامل (E2E) لتطبيقك ، ومكتبة التصميم ، و monorepo.
تأتي العديد من المزايا مع استخدام Nx ، وسنتناول القليل منها في هذا القسم.
- تنفيذ المهام على أساس الرسم البياني
يستخدم Nx تنفيذ المهام المستند إلى الرسم البياني الموزع والتخزين المؤقت للحساب لتسريع المهام. سيقوم النظام بجدولة المهام والأوامر باستخدام نظام الرسم البياني لتحديد العقدة (أي التطبيق) التي يجب أن تنفذ كل مهمة. هذا يعالج تنفيذ التطبيقات ويحسن وقت التنفيذ بكفاءة. - اختبارات
توفر Nx أدوات اختبار مكونة مسبقًا لاختبار الوحدة واختبارات E2E. - التخزين المؤقت
تقوم Nx أيضًا بتخزين الرسم البياني للمشروع المخزن مؤقتًا. يمكّنه هذا من إعادة تحليل الملفات المحدّثة فقط. يتتبع Nx الملفات التي تم تغييرها منذ آخر التزام ويتيح لك اختبار وبناء وتنفيذ الإجراءات على تلك الملفات فقط ؛ هذا يسمح بالتحسين المناسب عند العمل مع قاعدة رمز كبيرة. - الرسم البياني التبعية
يمكّنك مخطط التبعية المرئي من فحص كيفية تفاعل المكونات مع بعضها البعض. - سحابة التخزين
يوفر Nx أيضًا التخزين السحابي وتكامل GitHub ، بحيث يمكنك مشاركة الروابط مع أعضاء الفريق لمراجعة سجلات المشروع. - مشاركه الرمز
يمكن أن يكون إنشاء مكتبة مشتركة جديدة لكل مشروع مرهقًا للغاية. يقضي Nx على هذا التعقيد ، مما يتيح لك التركيز على الوظائف الأساسية لتطبيقك. باستخدام Nx ، يمكنك مشاركة المكتبات والمكونات عبر التطبيقات. يمكنك أيضًا مشاركة التعليمات البرمجية القابلة لإعادة الاستخدام بين تطبيقات الواجهة الأمامية والخلفية. - دعم monorepos
يوفر Nx مساحة عمل واحدة لتطبيقات متعددة. من خلال هذا الإعداد ، يمكن لمستودع GitHub واحد أن يضم مصدر الكود لتطبيقات مختلفة ضمن مساحة العمل الخاصة بك.
Nx للمكتبات القابلة للنشر
يتيح لك Nx إنشاء مكتبات قابلة للنشر. يعد هذا ضروريًا عندما يكون لديك مكتبات ستستخدمها خارج monorepo. في أي حالة تقوم فيها بتطوير مكونات واجهة المستخدم التنظيمية مع تكامل Nx Storybook ، ستنشئ Nx مكونات قابلة للنشر جنبًا إلى جنب مع قصصك. يمكن للمكونات القابلة للنشر تجميع هذه المكونات لإنشاء حزمة مكتبة يمكنك نشرها في سجل خارجي. يمكنك استخدام الخيار --publishable
عند إنشاء المكتبة ، على عكس --buildable
، والذي يستخدم لإنشاء مكتبات تُستخدم فقط في monorepo. لا تنشر Nx المكتبات القابلة للنشر تلقائيًا ؛ يمكنك استدعاء البناء عبر أمر مثل nx build mylib
(حيث mylib
هو اسم المكتبة) ، والذي سينتج بعد ذلك حزمة محسّنة في مجلد dist
/ mylib
يمكن نشرها في سجل خارجي.
يمنحك Nx خيار إنشاء مساحة عمل جديدة باستخدام Next.js كإعداد مسبق ، أو لإضافة Next.js إلى مساحة عمل موجودة.
لإنشاء مساحة عمل جديدة باستخدام Next.js كإعداد مسبق ، يمكنك استخدام الأمر التالي:
npx create-nx-workspace happynrwl \ --preset=next \ --style=styled-components \ --appName=todo
سينشئ هذا الأمر مساحة عمل Nx جديدة باستخدام تطبيق Next.js المسمى "todo" styled-components
نمطية كمكتبة التصميم.
بعد ذلك ، يمكننا إضافة تطبيق Next.js إلى مساحة عمل Nx الحالية باستخدام الأمر التالي:
npx nx g @nrwl/next:app
إنشاء تطبيق Next.js و Nx
يشتمل المكون الإضافي Nx لـ Next.js على أدوات ومنفذين لتشغيل تطبيق Next.js وتحسينه. للبدء ، نحتاج إلى إنشاء مساحة عمل Nx جديدة مع next
كإعداد مسبق:
npx create-nx-workspace happynrwl \ --preset=next \ --style=styled-components \ --appName=todo
ستنشئ كتلة التعليمات البرمجية أعلاه مساحة عمل Nx جديدة وتطبيق Next.js. سوف نتلقى مطالبة باستخدام Nx Cloud. في هذا البرنامج التعليمي ، سنختار "لا" ، ثم ننتظر حتى يتم تثبيت تبعياتنا. بمجرد الانتهاء من ذلك ، يجب أن يكون لدينا شجرة ملفات مشابهة لما يلي:
happynrwl ┣ apps ┃ ┣ todo ┃ ┣ todo-e2e ┃ ┗ .gitkeep ┣ libs ┣ node_modules ┣ tools ┣ .editorconfig ┣ .eslintrc.json ┣ .gitignore ┣ .prettierignore ┣ .prettierrc ┣ README.md ┣ babel.config.json ┣ jest.config.js ┣ jest.preset.js ┣ nx.json ┣ package-lock.json ┣ package.json ┣ tsconfig.base.json ┗ workspace.json
في مجلد apps
، سيكون لدينا تطبيق Next.js "todo" ، مع اختبار E2E المُكوَّن مسبقًا لتطبيق المهام. يتم إنشاء كل هذا تلقائيًا باستخدام أداة Nx CLI القوية.
لتشغيل تطبيقنا ، استخدم الأمر npx nx serve todo
. بمجرد الانتهاء من تقديم التطبيق ، سترى الشاشة أدناه:
بناء API
في هذه المرحلة ، قمنا بإعداد مساحة العمل. التالي هو بناء واجهة برمجة تطبيقات CRUD التي سنستخدمها في تطبيق Next.js. للقيام بذلك ، سوف نستخدم Express ؛ لإثبات دعم monorepo ، سنقوم ببناء خادمنا كتطبيق في مساحة العمل. أولاً ، يتعين علينا تثبيت المكون الإضافي Express لـ Nx عن طريق تشغيل هذا الأمر:
npm install --save-dev @nrwl/express
بمجرد الانتهاء من ذلك ، نكون مستعدين لإعداد تطبيق Express في مساحة العمل المتوفرة. لإنشاء تطبيق Express ، قم بتشغيل الأمر أدناه:
npx nx g @nrwl/express:application --name=todo-api --frontendProject=todo
الأمر nx g @nrwl/express:application
يمكننا من خلاله تمرير معلمات مواصفات إضافية ؛ لتحديد اسم التطبيق ، استخدم علامة --name
؛ للإشارة إلى تطبيق الواجهة الأمامية الذي سيستخدم تطبيق Express ، قم بتمرير اسم أحد التطبيقات في مساحة العمل لدينا إلى --frontendProject
. تتوفر بعض الخيارات الأخرى لتطبيق Express. عند الانتهاء من ذلك ، سيكون لدينا بنية ملف محدثة في مجلد apps
مع إضافة مجلد todo-api
إليه.
happynrwl ┣ apps ┃ ┣ todo ┃ ┣ todo-api ┃ ┣ todo-e2e ┃ ┗ .gitkeep …
مجلد todo-api
عبارة عن ملف معياري سريع يحتوي على ملف إدخال main.ts
/** * This is not a production server yet! * This is only minimal back end to get started. */ import * as express from 'express'; import {v4 as uuidV4} from 'uuid'; const app = express(); app.use(express.json()); // used instead of body-parser app.get('/api', (req, res) => { res.send({ message: 'Welcome to todo-api!' }); }); const port = process.env.port || 3333; const server = app.listen(port, () => { console.log(`Listening at http://localhost:${port}/api`); }); server.on('error', console.error);
سننشئ طرقنا داخل هذا التطبيق. للبدء ، سنهيئ مصفوفة من الكائنات بزوجين من قيم المفاتيح ، item
id
، أسفل إعلان التطبيق مباشرة.
/** * This is not a production server yet! * This is only minimal back end to get started. */ import * as express from 'express'; import {v4 as uuidV4} from 'uuid'; const app = express(); app.use(express.json()); // used instead of body-parser let todoArray: Array<{ item: string; id: string }> = [ { item: 'default todo', id: uuidV4() }, ]; …
بعد ذلك ، سنقوم بإعداد المسار لجلب جميع قوائم المهام ضمن app.get()
:
… app.get('/api', (req, res) => { res.status(200).json({ data: todoArray, }); }); …
سيعيد مقطع التعليمات البرمجية أعلاه القيمة الحالية لـ todoArray
. بعد ذلك ، سيكون لدينا مسارات لإنشاء وتحديث وإزالة عناصر المهام من المصفوفة.
… app.post('/api', (req, res) => { const item: string = req.body.item; // Increment ID of item based on the ID of the last item in the array. let id: string = uuidV4(); // Add the new object to the array todoArray.push({ item, id }); res.status(200).json({ message: 'item added successfully', }); }); app.patch('/api', (req, res) => { // Value of the updated item const updatedItem: string = req.body.updatedItem; // ID of the position to update const id: string = req.body.id; // Find index of the ID const arrayIndex = todoArray.findIndex((obj) => obj.id === id); // Update item that matches the index todoArray[arrayIndex].item = updatedItem res.status(200).json({ message: 'item updated successfully', }); }); app.delete('/api', (req, res) => { // ID of the position to remove const id: string = req.body.id; // Update array and remove the object that matches the ID todoArray = todoArray.filter((val) => val.id !== id); res.status(200).json({ message: 'item removed successfully', }); }); …
لإنشاء عنصر مهام جديد ، كل ما نحتاجه هو قيمة العنصر الجديد كسلسلة. سننشئ معرفًا عن طريق زيادة معرف العنصر الأخير في المصفوفة على الخادم. لتحديث عنصر موجود ، سنقوم بتمرير القيمة الجديدة للعنصر ومعرف عنصر العنصر المراد تحديثه ؛ على الخادم ، سنقوم بتكرار كل عنصر باستخدام طريقة forEach
، وتحديث العنصر في المكان الذي يتطابق فيه المعرف مع المعرف المرسل مع الطلب. أخيرًا ، لإزالة عنصر من المصفوفة ، سنرسل معرف العنصر المراد إزالته مع الطلب ؛ بعد ذلك ، نقوم بالتصفية من خلال المصفوفة ، ونعيد مصفوفة جديدة من جميع العناصر التي لا تتطابق مع المعرف المرسل مع الطلب ، مع تخصيص المصفوفة الجديدة للمتغير todoArray
.
ملاحظة: إذا نظرت في مجلد تطبيق Next.js ، فسترى ملف proxy.conf.json
بالتكوين أدناه:
{ "/api": { "target": "http://localhost:3333", "secure": false } }
يؤدي هذا إلى إنشاء وكيل ، مما يسمح لجميع استدعاءات API إلى المسارات المطابقة /api
لاستهداف خادم todo-api
.
إنشاء صفحات Next.js باستخدام Nx
في تطبيق Next.js الخاص بنا ، سننشئ صفحة جديدة ، home
، ومكون عنصر. يوفر Nx أداة CLI لنا لإنشاء صفحة بسهولة:
npx nx g @nrwl/next:page home
عند تشغيل هذا الأمر ، سنحصل على موجه لتحديد مكتبة التصميم التي نريد استخدامها للصفحة ؛ بالنسبة لهذه المقالة ، styled-components
. هاهو! تم إنشاء صفحتنا. لإنشاء مكون ، قم بتشغيل npx nx g @nrwl/next:component todo-item
؛ سيؤدي هذا إلى إنشاء مجلد component
مع مكون todo-item
.
استهلاك API في تطبيق Next.js
في كل عنصر مهمة ، سيكون لدينا زرين ، لتحرير وحذف عنصر المهام. يتم تمرير الوظائف غير المتزامنة التي تؤدي هذه الإجراءات كعناصر من الصفحة الرئيسية.
… export interface TodoItemProps { updateItem(id: string, updatedItem: string): Promise<void>; deleteItem(id: string): Promise<void>; fetchItems(): Promise<any>; item: string; id: string; } export const FlexWrapper = styled.div` width: 100%; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #ccc; padding-bottom: 10px; margin-top: 20px; @media all and (max-width: 470px) { flex-direction: column; input { width: 100%; } button { width: 100%; } } `; export function TodoItem(props: TodoItemProps) { const [isEditingItem, setIsEditingItem] = useState<boolean>(false); const [item, setNewItem] = useState<string | null>(null); return ( <FlexWrapper> <Input disabled={!isEditingItem} defaultValue={props.item} isEditing={isEditingItem} onChange={({ target }) => setNewItem(target.value)} /> {!isEditingItem && <Button onClick={() => setIsEditingItem(true)} > Edit </Button>} {isEditingItem && <Button onClick={async () => { await props.updateItem(props.id, item); //fetch updated items await props.fetchItems(); setIsEditingItem(false) }}> Update </Button>} <Button danger onClick={async () => { await props.deleteItem(props.id); //fetch updated items await await props.fetchItems(); }} > Delete </Button> </FlexWrapper> ); }
بالنسبة لوظيفة التحديث ، لدينا إدخال يتم تعطيله عندما تكون حالة isEditingItem
false
. بمجرد النقر فوق الزر "تحرير" ، يقوم بتبديل حالة isEditingItem
إلى true
ويعرض الزر "تحديث". هنا ، يتم تمكين مكون الإدخال ، ويمكن للمستخدم إدخال قيمة جديدة ؛ عند النقر فوق الزر "تحديث" ، فإنه يستدعي وظيفة updateItem
مع تمرير المعلمات ، ويقوم isEditingItem
مرة أخرى إلى false
.
في مكون الصفحة home
، لدينا الوظائف غير المتزامنة التي تؤدي عملية CRUD.
… const [items, setItems] = useState<Array<{ item: string; id: string }>>([]); const [newItem, setNewItem] = useState<string>(''); const fetchItems = async () => { try { const data = await fetch('/api/fetch'); const res = await data.json(); setItems(res.data); } catch (error) { console.log(error); } }; const createItem = async (item: string) => { try { const data = await fetch('/api', { method: 'POST', body: JSON.stringify({ item }), headers: { 'Content-Type': 'application/json', }, }); } catch (error) { console.log(error); } }; const deleteItem = async (id: string) => { try { const data = await fetch('/api', { method: 'DELETE', body: JSON.stringify({ id }), headers: { 'Content-Type': 'application/json', }, }); const res = await data.json(); alert(res.message); } catch (error) { console.log(error); } }; const updateItem = async (id: string, updatedItem: string) => { try { const data = await fetch('/api', { method: 'PATCH', body: JSON.stringify({ id, updatedItem }), headers: { 'Content-Type': 'application/json', }, }); const res = await data.json(); alert(res.message); } catch (error) { console.log(error); } }; useEffect(() => { fetchItems(); }, []); …
في كتلة التعليمات البرمجية أعلاه ، لدينا fetchItems
، الذي يعيد todoArray
من الخادم. بعد ذلك ، لدينا وظيفة createItem
، والتي تأخذ سلسلة ؛ المعلمة هي قيمة عنصر المهام الجديد. تأخذ الدالة updateItem
معلمتين ، معرف العنصر المراد تحديثه وقيمة updatedItem
. وتقوم وظيفة deleteItem
بإزالة العنصر المطابق للمعرف الذي تم تمريره.
لتقديم عنصر المهام ، نقوم بتعيين حالة items
:
… return ( <StyledHome> <h1>Welcome to Home!</h1> <TodoWrapper> {items.length > 0 && items.map((val) => ( <TodoItem key={val.id} item={val.item} id={val.id} deleteItem={deleteItem} updateItem={updateItem} fetchItems={fetchItems} /> ))} </TodoWrapper> <form onSubmit={async(e) => { e.preventDefault(); await createItem(newItem); //Clean up new item setNewItem(''); await fetchItems(); }} > <FlexWrapper> <Input value={newItem} onChange={({ target }) => setNewItem(target.value)} placeholder="Add new item…" /> <Button success type="submit"> Add + </Button> </FlexWrapper> </form> </StyledHome> ); …
تم إعداد الخادم والواجهة الأمامية لدينا الآن. يمكننا خدمة تطبيق API عن طريق تشغيل npx nx serve todo-api
، وبالنسبة لتطبيق Next.js ، نقوم بتشغيل npx nx serve todo
. انقر فوق الزر "متابعة" ، وسترى صفحة مع عرض عنصر المهام الافتراضي.
لدينا الآن تطبيق Next.js و Express يعملان معًا في مساحة عمل واحدة.
لدى Nx أداة CLI أخرى تتيح لنا عرض الرسم البياني للتبعية لتطبيقنا في تشغيلنا النهائي. قم بتشغيل npx nx dep-graph
، وسنرى شاشة مشابهة للصورة أدناه ، تصور الرسم البياني للتبعية لتطبيقنا.
أوامر CLI الأخرى لـ Nx
-
nx list
يسرد المكونات الإضافية Nx المثبتة حاليًا. -
nx migrate latest
يحدّث الحزم فيpackage.json
إلى أحدث إصدار. -
nx affected
ينفذ الإجراء على التطبيقات المتأثرة أو المعدلة فقط. -
nx run-many --target serve --projects todo-api,todo
يقوم بتشغيل الأمر الهدف عبر جميع المشاريع المدرجة.
خاتمة
كنظرة عامة على Nx ، تناولت هذه المقالة ما تقدمه Nx وكيف تجعل العمل أسهل بالنسبة لنا. انتقلنا أيضًا إلى إعداد تطبيق Next.js في مساحة عمل Nx ، وإضافة مكون إضافي Express إلى مساحة عمل موجودة ، واستخدام ميزة monorepo لإيواء أكثر من تطبيق واحد في مساحة العمل لدينا.
ستجد كود المصدر الكامل في مستودع جيثب. للحصول على معلومات إضافية حول Nx ، راجع الوثائق أو وثائق Nx لـ Next.js.