قبل البدء في البناء من الصفر تطبيق الويب الحديث ، تحتاج إلى معرفة ما هو تطبيق الويب الحديث؟
تطبيق الويب الحديث (MWA) هو تطبيق يلتزم بجميع معايير الويب الحديثة. من بينها ، يعد تطبيق Progressive Web App القدرة على تنزيل إصدار مستعرض محمول على هاتفك واستخدامه كتطبيق كامل. إنها أيضًا فرصة لتمرير الموقع في وضع عدم الاتصال من جهاز محمول ومن كمبيوتر ؛ تصميم المواد الحديثة. الأمثل محرك البحث الأمثل. وبطبيعة الحال - سرعة تنزيل عالية.

إليك ما سيحدث في MWA لدينا (أنصحك باستخدام هذا التنقل في المقالة):
الأشخاص في Habré هم من رجال الأعمال ، لذلك يمكنك الاتصال على الفور بمخزن GitHub وأرشيف من كل مرحلة من مراحل التطوير والعرض التجريبي . هذه المقالة مخصصة للمطورين الذين لديهم دراية بـ node.js ويتفاعلون. يتم تقديم كل النظرية اللازمة في المجلد الضروري. توسيع آفاقك من خلال النقر على الروابط.
لنبدأ!
1. عالمي
الإجراءات القياسية: قم بإنشاء دليل عمل وتنفيذ git init
. افتح package.json وأضف بضعة أسطر:
"dependencies": { "@babel/cli": "^7.1.5", "@babel/core": "^7.1.6", "@babel/preset-env": "^7.1.6", "@babel/preset-react": "^7.0.0", "@babel/register": "^7.0.0", "babel-loader": "^8.0.4", "babel-plugin-root-import": "^6.1.0", "express": "^4.16.4", "react": "^16.6.3", "react-dom": "^16.6.3", "react-helmet": "^5.2.0", "react-router": "^4.3.1", "react-router-dom": "^4.3.1", "webpack": "^4.26.1", "webpack-cli": "^3.1.2" }
نحن ننفذ npm install
، وبينما يتم تثبيته ، فإننا نفهم.
نظرًا لأننا وصلنا في نهاية عام 2018 و 2019 ، فسيكون تطبيق الويب الخاص بنا عالميًا (أو متماثلًا) ، وستظهر نسخة ECMAScript لا تقل عن ES2017 ، سواء في الخلف أو في المقدمة. للقيام بذلك ، يقوم index.js (ملف إدخال التطبيق) بربط babel / register ، ويقوم بابل أثناء التنقل بتحويل جميع كود ES بعده إلى JavaScript الصديقة للمستعرض باستخدام babel / preset-env و babel / preset-react. لتسهيل عملية التطوير ، عادةً ما أستخدم ملحق استيراد بابل-البرنامج المساعد-الجذر ، والذي ستبدو عليه جميع الواردات من الدليل الجذر "~ /" ، ومن src / - '& /'. بدلاً من ذلك ، يمكنك كتابة مسارات طويلة أو استخدام اسم مستعار من webpack.
index.js
require("@babel/register")(); require("./app");
.babelrc
{ "presets": [ [ "@babel/preset-env", { "targets": { "node": "current" } } ], "@babel/preset-react" ], "plugins": [ ["babel-plugin-root-import", { "paths": [{ "rootPathPrefix": "~", "rootPathSuffix": "" }, { "rootPathPrefix": "&", "rootPathSuffix": "src/" }] }] ] }
الوقت لإعداد Webpack . ننشئ webpack.config.js ونستخدم الكود (يُشار إليه فيما يلي بالتعليقات الواردة في الكود).
const path = require('path'); module.exports = {
من هذه اللحظة ، تبدأ المتعة. حان الوقت لتطوير جانب الخادم للتطبيق. التقديم من جانب الخادم (SSR) عبارة عن تقنية مصممة لتسريع تحميل تطبيق الويب في بعض الأحيان وحل النقاش الأبدي حول تحسين محرك البحث في تطبيق صفحة واحدة (SEO في SPA). للقيام بذلك ، نأخذ قالب HTML ، ونضع المحتوى فيه ونرسله إلى المستخدم. يقوم الخادم بهذا بسرعة كبيرة - يتم رسم الصفحة في غضون مللي ثانية. ومع ذلك ، لا توجد طريقة للتعامل مع DOM على الخادم ، لذلك يقوم جزء العميل من التطبيق بتحديث الصفحة ، ويصبح في النهاية تفاعلي. حسنا؟ نحن نطور!
app.js
import express from 'express' import path from 'path' import stateRoutes from './server/stateRoutes'
الخادم / stateRoutes.js
import ssr from './server' export default function (app) {
يقوم ملف server / server.js بتجميع المحتوى الذي تم إنشاؤه بواسطة رد الفعل ونقله إلى قالب HTML - /server/template.js . تجدر الإشارة إلى أن الخادم يستخدم جهاز توجيه ثابت ، لأننا لا نريد تغيير عنوان URL للصفحة أثناء التحميل. و react-helmet هي مكتبة تعمل على تبسيط العمل بشكل كبير مع بيانات التعريف (وبالفعل مع علامة الرأس).
الخادم / server.js
import React from 'react' import { renderToString } from 'react-dom/server' import { StaticRouter } from 'react-router-dom' import { Helmet } from 'react-helmet' import App from '&/app/App' import template from './template' export default function render(url) {
في server / template.js ، في الرأس نقوم بطباعة البيانات من الخوذة ، قم بتوصيل فافيكون ، وأنماط من الدليل / الأصول الثابتة. في النص الأساسي ، توجد حزمة client.js للمحتوى و webpack في المجلد / public ، ولكن نظرًا لأنه ثابت ، فإننا ننتقل إلى عنوان الدليل الجذر - /client.js.
الخادم / القالب
ننتقل إلى البساطة - جانب العميل. يقوم ملف src / client.js باستعادة HTML الذي تم إنشاؤه بواسطة الخادم دون تحديث DOM ، ويجعله تفاعلي. (المزيد حول هذا هنا ). رد فعل وظيفة هيدرات يفعل هذا. والآن ليس لدينا أي علاقة بجهاز التوجيه الثابت. نستخدم المعتاد واحد - BrowserRouter.
src / client.js
import React from 'react' import { hydrate } from 'react-dom' import { BrowserRouter } from 'react-router-dom' import App from './app/App' hydrate( <BrowserRouter> <App/> </BrowserRouter>, document.querySelector('#app') )
بالفعل في ملفين تمكن عنصر رد الفعل من التطبيق لتضيء. هذا هو المكون الرئيسي لتطبيق سطح المكتب الذي يؤدي التوجيه. رمزها شائع جدًا:
src / app / App.js
import React from 'react' import { Switch, Route } from 'react-router' import Home from './Home' export default function App() { return( <Switch> <Route exact path="/" component={Home}/> </Switch> ) }
حسنًا ، src / app / Home.js. لاحظ كيف تعمل الخوذة - غلاف علامة الرأس المعتاد.
import React from 'react' import { Helmet } from 'react-helmet' export default function Home() { return( <div> <Helmet> <title>Universal Page</title> <meta name="description" content="Modern Web App - Home Page" /> </Helmet> <h1> Welcome to the page of Universal Web App </h1> </div> ) }
مبروك! أخذنا الجزء الأول من تطوير MWA! بقي سوى القليل من اللمسات من أجل اختبار كل شيء. من الناحية المثالية ، يمكنك ملء مجلد / الأصول بملفات نمط عمومي وفافيكون وفقًا للقالب - server / template.js. ليس لدينا أيضًا أوامر لإطلاق التطبيق. العودة إلى package.json :
"scripts": { "start": "npm run pack && npm run startProd", "startProd": "NODE_ENV=production node index.js", "pack": "webpack --mode production --config webpack.config.js", "startDev": "npm run packDev && node index.js", "packDev": "webpack --mode development --config webpack.config.js" }
قد تلاحظ فئتين من الأوامر - Prod و Dev. إنها تختلف في تكوين webpack v4. حول - --mode
يستحق القراءة هنا .
تأكد من تجربة التطبيق العالمي الناتج في المضيف المحلي: 3000
2. المواد واجهة المستخدم
سيركز هذا الجزء من البرنامج التعليمي على الاتصال بتطبيق الويب باستخدام SSR لمكتبة المواد. لماذا بالضبط لها؟ كل شيء بسيط - المكتبة تعمل بنشاط على تطوير وصيانة وتوثيق واسع النطاق. مع ذلك ، يمكنك بناء واجهة مستخدم جميلة لمجرد البصق.
ويرد وصف مخطط الاتصال نفسه ، ومناسبة لتطبيقنا ، هنا . حسنا ، دعنا نفعل ذلك.
تثبيت التبعيات اللازمة:
npm i @material-ui/core jss react-jss
التالي علينا إجراء تغييرات على الملفات الموجودة. في server / server.js ، نلتف تطبيقنا في JssProvider و MuiThemeProvider ، والذي سيوفر مكونات واجهة المستخدم المادية ، والأهم من ذلك ، كائن sheetRegistry - css ، الذي يجب وضعه في قالب HTML. من جانب العميل ، نستخدم MuiThemeProvider فقط ، ونزوده بكائن موضوع.
الخادم ، القالب والعميلالخادم / server.js
import React from 'react' import { renderToString } from 'react-dom/server' import { StaticRouter } from 'react-router-dom' import { Helmet } from 'react-helmet'
الخادم / القالب
export default function template(helmet, content = '', sheetsRegistry) { const css = sheetsRegistry.toString() const scripts = `<script src="/client.js"></script>` const page = `<!DOCTYPE html> <html lang="en"> <head> ... </head> <body> <div class="content">...</div> <style id="jss-server-side">${css}</style> ${scripts} </body> ` return page }
src / client.js
... import MuiThemeProvider from '@material-ui/core/styles/MuiThemeProvider' import createMuiTheme from '@material-ui/core/styles/createMuiTheme' import purple from '@material-ui/core/colors/purple'
الآن أقترح إضافة تصميم أنيق قليلاً إلى المكون الرئيسية. يمكنك إلقاء نظرة على جميع مكونات المواد على موقعه الرسمي على الويب ، وهنا يكفي الورق والزر و AppBar وشريط الأدوات والطباعة.
src / app / Home.js
import React from 'react' import { Helmet } from 'react-helmet' import Paper from '@material-ui/core/Paper' import Typography from '@material-ui/core/Typography' import Button from '@material-ui/core/Button' import Header from './Header'
src / app / Header.js
import React from 'react' import AppBar from '@material-ui/core/AppBar' import Toolbar from '@material-ui/core/Toolbar' import Typography from '@material-ui/core/Typography' export default function Header() { return ( <AppBar position="static"> <Toolbar> <Typography variant="h5" color="inherit"> Modern Web App </Typography> </Toolbar> </AppBar> ) }
الآن يجب أن يتحول شيء مثل هذا:

3. رمز تقسيم
إذا كنت تخطط لكتابة شيء أكثر من قائمة TODO ، فسوف يزداد طلبك بما يتناسب مع حزمة client.js. لتجنب التحميل الطويل للصفحات على المستخدم ، تم اختراع تقسيم الشفرة لفترة طويلة. ومع ذلك ، بمجرد أن رايان فلورنس ، أحد المبدعين في React-router ، أخاف المطورين المحتملين بعبارة:
التوفيق لأولئك الذين يحاولون المقدمة ، خادم تقسيم التطبيقات.
حظ سعيد لكل من يقرر إنشاء تطبيقات ssr مع تقسيم الشفرة
تم صدنا - سنفعل ذلك! تثبيت الضروري:
npm i @babel/plugin-syntax-dynamic-import babel-plugin-dynamic-import-node react-loadable
المشكلة هي وظيفة واحدة فقط - استيراد. يدعم Webpack وظيفة الاستيراد الديناميكية غير المتزامنة هذه ، ولكن تجميع بابل سيكون مشكلة كبيرة. لحسن الحظ ، بحلول عام 2018 ، وصلت المكتبات للمساعدة في التعامل مع هذا. babel / plugin-syntax-dynamic-import و babel-plugin-dynamic-import-node سوف ينقذنا من الخطأ "Unexpected token when using import()"
. لماذا مكتبتان لمهمة واحدة؟ هناك حاجة لعقدة الاستيراد الديناميكية خصيصًا لتقديم الخادم ، وسوف تلتقط الواردات على الخادم أثناء التنقل:
index.js
require("@babel/register")({ plugins: ["@babel/plugin-syntax-dynamic-import", "dynamic-import-node"] }); require("./app");
في الوقت نفسه ، نقوم بتعديل ملف تكوين بابل العالمي .babelrc
"plugins": [ "@babel/plugin-syntax-dynamic-import", "react-loadable/babel", ... ]
ظهرت هنا رد الفعل للتحميل . ستجمع هذه المكتبة ذات الوثائق الممتازة جميع الوحدات المعطلة بواسطة استيراد webpack على الخادم ، وسيقوم العميل باستلامها بنفس السهولة. للقيام بذلك ، يحتاج الخادم إلى تنزيل جميع الوحدات النمطية:
app.js
import Loadable from 'react-loadable' ... Loadable.preloadAll().then(() => app.listen(PORT, '0.0.0.0', () => { console.log(`The app is running in PORT ${PORT}`) })) ...
الوحدات النمطية نفسها سهلة للغاية للاتصال. ألقِ نظرة على الكود:
src / app / App.js
import React from 'react' import { Switch, Route } from 'react-router' import Loadable from 'react-loadable' import Loading from '&/Loading' const AsyncHome = Loadable({ loader: () => import( './Home'), loading: Loading, delay: 300, }) export default function App() { return( <Switch> <Route exact path="/" component={AsyncHome}/> </Switch> ) }
يتم تحميل المكون Home بشكل غير متزامن بشكل غير متزامن ، مما يوضح لحزمة الويب أنه يجب أن يسمى Home (نعم ، هذه حالة نادرة عندما تكون التعليقات منطقية). delay: 300
يعني أنه بعد delay: 300
مللي ثانية من عدم تحميل المكون ، تحتاج إلى إظهار أن التنزيل ما زال مستمراً. إنه يتعامل مع التحميل:
src / Loading.js
import React from 'react' import CircularProgress from '@material-ui/core/CircularProgress'
لتوضيح الخادم الوحدات التي نستوردها ، سنحتاج إلى التسجيل:
Loadable({ loader: () => import('./Bar'), modules: ['./Bar'], webpack: () => [require.resolveWeak('./Bar')], });
ولكن حتى لا نكرر نفس الكود ، يوجد ملحق إضافي قابل للتحميل / babel وصلناه بالفعل بـ .babelrc . الآن وبعد أن عرف الخادم ما الذي تريد استيراده ، تحتاج إلى معرفة ما سيتم تقديمه. سير العمل يشبه قليلا خوذة:
الخادم / server.js
import Loadable from 'react-loadable' import { getBundles } from 'react-loadable/webpack' import stats from '~/public/react-loadable.json' ... let modules = []
للتأكد من قيام العميل بتحميل جميع الوحدات النمطية المقدمة على الخادم ، نحتاج إلى ربطها مع الحزم التي أنشأتها webpack. للقيام بذلك ، قم بإجراء تغييرات على تكوين المجمع. البرنامج المساعد للرد - للتحميل / webpack يكتب جميع الوحدات إلى ملف منفصل. يجب أيضًا إخبار webpack بحفظ الوحدات بشكل صحيح بعد الاستيراد الديناميكي - في كائن الإخراج.
webpack.config.js
const ReactLoadablePlugin = require('react-loadable/webpack').ReactLoadablePlugin; ... output: { path: path.resolve(__dirname, 'public'), publicPath: '/', chunkFilename: '[name].bundle.js', filename: "[name].js" }, plugins: [ new ReactLoadablePlugin({ filename: './public/react-loadable.json', }) ]
نكتب وحدات في القالب ، وتحميلها بدوره:
الخادم / القالب
export default function template(helmet, content = '', sheetsRegistry, bundles) { ... const page = `<!DOCTYPE html> <html lang="en"> <head>...</head> <body> <div class="content"> <div id="app" class="wrap-inner"> <!--- magic happens here --> ${content} </div> ${bundles.map(bundle => `<script src='/${bundle.file}'></script>`).join('\n')} </div> <style id="jss-server-side">${css}</style> ${scripts} </body> ` return page }
يبقى فقط لمعالجة جزء العميل. يقوم الأسلوب Loadable.preloadReady()
بتحميل جميع الوحدات النمطية التي قدمها الخادم للمستخدم مقدمًا.
src / client.js
import Loadable from 'react-loadable' Loadable.preloadReady().then(() => { hydrate( <MuiThemeProvider theme={theme}> <BrowserRouter> <App/> </BrowserRouter> </MuiThemeProvider>, document.querySelector('#app') ) })
انتهى نبدأ وننظر إلى النتيجة - في الجزء الأخير كانت الحزمة ملفًا واحدًا فقط - client.js يزن 265 كيلو بايت ، والآن يوجد 3 ملفات ، أكبرها بوزن 215 كيلو بايت. وغني عن القول ، سوف تزيد سرعة تحميل الصفحة بشكل ملحوظ عند توسيع نطاق المشروع؟

4. مسترجع العداد
الآن سنبدأ في حل المشاكل العملية. كيفية حل المعضلة عندما يكون لدى الخادم بيانات (على سبيل المثال ، من قاعدة بيانات) ، تحتاج إلى عرضها حتى تتمكن برامج البحث من العثور على المحتوى ، ثم استخدام هذه البيانات على العميل.
هناك حل. يتم استخدامه في كل مقال SSR تقريبًا ، ولكن الطريقة التي يتم تنفيذها بها بعيدة كل البعد عن القابلية للتوسعة بشكل جيد. بعبارة بسيطة ، بعد معظم البرامج التعليمية ، لن تكون قادرًا على إنشاء موقع حقيقي باستخدام SSR وفقًا لمبدأ "واحد ، اثنان ، وإنتاج". الآن سأحاول أن أكون نقطة.
نحن بحاجة فقط التكرار . الحقيقة هي أن الإعادة لديها متجر عالمي ، يمكننا نقله من الخادم إلى العميل بنقرة إصبع.
المهم الآن (!): لدينا سبب لوجود ملف server / stateRoutes . يدير كائن initialState الذي يتم إنشاؤه هناك ، ويتم إنشاء متجر منه ، ثم يتم تمريره إلى قالب HTML. يسترد العميل هذا الكائن من window.__STATE__
، window.__STATE__
المتجر ، وهذا كل شيء. يبدو سهلا.
تثبيت:
npm i redux react-redux
اتبع الخطوات المذكورة أعلاه. هنا ، بالنسبة للجزء الأكبر ، تكرار التعليمات البرمجية المستخدمة سابقا.
خادم وعميل معالجة العدادالخادم / stateRoutes.js :
import ssr from './server'
الخادم / server.js :
import { Provider } from 'react-redux' import configureStore from '&/redux/configureStore' ... export default function render(url, initialState) {
الخادم / القالب
export default function template(helmet, content = '', sheetsRegistry, bundles, initialState = {}) { ...
نحصل على متجر على العميل. src / client.js
import Loadable from 'react-loadable' import { Provider } from 'react-redux' import configureStore from './redux/configureStore' ...
منطق التكرار في SSR قد انتهى. الآن ، العمل المعتاد مع الإعادة هو إنشاء متجر ، وإجراءات ، ومخفضات ، والاتصال ، وأكثر من ذلك. آمل أن يكون هذا واضحًا دون توضيح كثير. إن لم يكن ، وقراءة الوثائق .
مسترجع كله هناsrc / redux /igureStore.js
import { createStore } from 'redux' import rootReducer from './reducers' export default function configureStore(preloadedState) { return createStore( rootReducer, preloadedState ) }
src / redux / Actions.js
src / مسترجع / مخفضات
import { INCREASE, DECREASE } from './actions' export default function count(state, action) { switch (action.type) { case INCREASE:
src / app / Home.js
import React from 'react' import { Helmet } from 'react-helmet' import { bindActionCreators } from 'redux' import { connect } from 'react-redux' import * as Actions from '&/redux/actions' import Header from './Header' import Paper from '@material-ui/core/Paper' import Typography from '@material-ui/core/Typography' import Button from '@material-ui/core/Button' const styles = { paper: { margin: 'auto', marginTop: '10%', width: '40%', padding: 15 }, btn: { marginRight: 20 } } class Home extends React.Component{ constructor(){ super() this.increase = this.increase.bind(this) this.decrease = this.decrease.bind(this) }
:

5.
, — . . , , initialState , .
:
npm i mobile-detect
mobile detect user-agent, null .
:
server/stateRoutes.js
import ssr from './server' import MobileDetect from 'mobile-detect' const initialState = { count: 5, mobile: null } export default function (app) { app.get('*', (req, res) => {
— :
الخادم / server.js
... import App from '&/app/App' import MobileApp from '&/mobileApp/App' export default function render(url, initialState, mobile) {
src / client.js
... const state = window.__STATE__ const store = configureStore(state)
react-, . , . src/mobileApp .
6.
Progressive Web App (PWA), Google — , , , .
. : Chrome, Opera Samsung Internet , . iOS Safari, . , . PWA: Windows Chrome v70, Linux v70, ChromeOS v67. PWA macOS — 2019 Chrome v72.
: PWA . , , , .
2 — manifest.json service-worker.js — . — json , , , . Service-worker : push-, .
. , :
public/manifest.json :
{ "short_name": "MWA", "name": "Modern Web App", "description": "Modern app built with React SSR, PWA, material-ui, code splitting and much more", "icons": [ { "src": "/assets/logos/yellow 192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/assets/logos/yellow 512.png", "sizes": "512x512", "type": "image/png" } ], "start_url": ".", "display": "standalone", "theme_color": "#810051", "background_color": "#FFFFFF" }
service-worker', . , , :
public/service-worker.js
PWA , - html-:
server/template.js
export default function template(helmet, content = '', sheetsRegistry, bundles, initialState = {}) { const scripts = `... <script> // service-worker - if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/service-worker.js') .then(registration => { console.log('Service Worker is registered! '); }) .catch(err => { console.log('Registration failed ', err); }); }); } </script>` const page = `<!DOCTYPE html> <html lang="en"> <head> ... <link rel="manifest" href="/manifest.json"> </head> <body> ... ${scripts} </body> ` return page }
انتهى https, , gif demo .
7.
MWA. , , . , SSR Code Splitting, PWA .
, MWA - web.dev :

, — . , , — .
, MWA — opensource . , , !
حظا سعيدا