توسعه وب اپلیکیشن با جاوا اسکریپت و Webpack — راهنمای کاربردی

۴۱۹ بازدید
آخرین به‌روزرسانی: ۰۸ شهریور ۱۴۰۲
زمان مطالعه: ۱۵ دقیقه
توسعه وب اپلیکیشن با جاوا اسکریپت و Webpack — راهنمای کاربردی

آیا تاکنون در مورد ساخت اپلیکیشن‌های مدرن جاوا اسکریپت با ساده‌ترین تنظیمات ممکن اندیشیده‌اید؟ اگر چنین است مقاله مناسبی را برای مطالعه انتخاب کرده‌اید. فریمورک‌های جاوا اسکریپت به ما کمک می‌کنند تا اپلیکیشن‌ها را به روشی تعمیم‌یافته و با اغلب قابلیت‌های رایج بسازیم. با این حال، ممکن است استفاده از یک فریمورک برای الزامات خاص (به خصوص برای پروژه‌های کوچک تا متوسط) زیاده‌روی محسوب می‌شود. در این مقاله می‌خواهیم رویکردی را نشان دهیم که به وسیله آن می‌توانید از قابلیت‌های مدرن جاوا اسکریپت استفاده کنید و وب اپلیکیشن های پیشرونده سفارشی خودتان را بسازید.

همچنین با این رویکرد می‌توانید در صورت تمایل، فریمورک خاص خود را بر مبنای اپلیکیشن‌های ساده ایجاد کنید. این مسئله کاملاً اختیاری است. توان جاوا اسکریپت محض، شما را قادر می‌سازد که صرفنظر از ابزارهایی که استفاده می‌کنید، از سبک کدنویسی خودتان پیروی کنید.

پیش‌نیازها

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

برنامه‌ریزی معماری

ما برای تضمین بارگذاری سریع و تجربه کاربری منسجم، از الگوهای زیر استفاده خواهیم کرد:

  • معماری پوسته اپلیکیشن
  • الگوی PRPL که اختصاری برای عبارت «Push، Render ،Pre-cache ،Lazy loading» است.

تنظیم Build

ما به یک تنظیم Build مناسب نیاز داریم و از این رو از Webpack به همراه الزامات زیر بهره می‌گیریم:

  • پشتیبانی از ES6 و ایمپورت‌های دینامیک
  • پشتیبانی از SASS و CSS
  • توزیع سفارشی و تنظیم Production
  • ساخت Service Worker سفارشی

قابلیت‌های کمینه جاوا اسکریپت خالص

ما در این مقاله به بررسی قابلیت‌های کمینه جاوا اسکریپت می‌پردازیم تا فریمورک‌ها را دور زده و خروجی مورد نظر خود را ایجاد نماییم. بدین ترتیب به شما نشان خواهیم داد که چگونه می‌توانید از قابلیت‌های موجود جاوا اسکریپت ES6 در اپلیکیشن‌های جاوا اسکریپت محض روزمره خود استفاده کنید. این موارد شامل فهرست زیر هستند:

  • ماژول‌های ES6
  • ایمپورت‌های دینامیک
  • ساختار Object Literal یا ساختار کلاس ES6
  • تابع‌های Arrow در ES6
  • Literals-های قالبِ ES6

در انتهای این مقاله دموی اپلیکیشن نمونه‌ای به همراه سورس کد گیت‌هاب آن ارائه شده است.

برنامه‌ریزی معماری

ظهور وب‌اپلیکیشن‌های پیشرونده به ایجاد یک معماری جدید برای ایجاد کارایی هر چه بیشتر در اپلیکیشن کمک  می‌کند. ترکیب کردن الگوهای App Shell و PRPL می‌تواند منجر به تجربه‌های واکنش‌گرایی منسجم و شبیه اپلیکیشن شود.

App Shell و PRPL چه هستند؟

App Shell یک الگوی معماری برای ساخت وب‌اپلیکیشن‌های پیشرونده است که در آن کمترین منابع ضروری برای بارگذاری وب‌سایت ارائه می‌شوند. این موارد اساساً شامل همه منابع ضروری برای بارگذاری اولیه می‌شود. شما می‌توانید منابع ضروری را نیز با استفاده از یک سرویس ورکر کش کنید.

منظور از PRPL موارد زیر است:

  • Push کردن منابع ضروری (به خصوص با استفاده از HTTP/2) برای مسیر اولیه.
  • Render کردن مسیر اولیه.
  • Pre-chashe کردن مسیرها یا فایل‌های باقی‌مانده.
  • بارگذاری Lazy بخش‌هایی از اپلیکیشن بنا به ضرورت (به خصوص زمانی که از سوی کاربر الزامی باشد).

این معماری‌ها در کد چطور به نظر می‌رسند؟

الگوی App Shell و PRPL هر دو با همدیگر برای دستیابی به بهترین رویه مورد استفاده قرار می‌گیرند. App Shell یا پوسته اپلیکیشن شبیه چیزی است که در قطعه کد زیر می‌بینید:

1<!DOCTYPE html>
2<html lang="en">
3
4<head>
5    <meta charset="utf-8" />
6    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
8    <!-- Critical Styles -->
9    <style>
10        html {
11            box-sizing: border-box;
12        }
13        *,
14        *:after,
15        *:before {
16            box-sizing: inherit;
17        }
18        body {
19            margin: 0;
20            padding: 0;
21            font: 18px 'Oxygen', Helvetica;
22            background: #ececec;
23        }
24        header {
25            height: 60px;
26            background: #512DA8;
27            color: #fff;
28            display: flex;
29            align-items: center;
30            padding: 0 40px;
31            box-shadow: 1px 2px 6px 0px #777;
32        }
33        h1 {
34            margin: 0;
35        }
36        .banner {
37            text-decoration: none;
38            color: #fff;
39            cursor: pointer;
40        }
41        main {
42            display: flex;
43            justify-content: center;
44            height: calc(100vh - 140px);
45            padding: 20px 40px;
46            overflow-y: auto;
47        }
48        button {
49            background: #512DA8;
50            border: 2px solid #512DA8;
51            cursor: pointer;
52            box-shadow: 1px 1px 3px 0px #777;
53            color: #fff;
54            padding: 10px 15px;
55            border-radius: 20px;
56        }
57        .button {
58            display: flex;
59            justify-content: center;
60        }
61        button:hover {
62            box-shadow: none;
63        }
64        footer {
65            height: 40px;
66            background: #2d3850;
67            color: #fff;
68            display: flex;
69            align-items: center;
70            padding: 40px;
71        }
72    </style>
73    <title>Vanilla Todos PWA</title>
74</head>
75
76<body>
77
78    <body>
79        <!-- Main Application Section -->
80        <header>
81            <h3><a class="banner"> Vanilla Todos PWA </a></h3>
82        </header>
83        <main id="app"></main>
84        <footer>
85            <span>© 2019 Anurag Majumdar - Vanilla Todos SPA</span>
86        </footer>
87      
88        <!-- Critical Scripts -->
89        <script async src="<%= htmlWebpackPlugin.files.chunks.main.entry %>"></script>
90
91        <noscript>
92            This site uses JavaScript. Please enable JavaScript in your browser.
93        </noscript>
94    </body>
95</body>
96
97</html>

می‌توانید ببینید که پوشه اپلیکیشن شامل یک اسکلت نشانه‌گذاری خالی کمینه است. در خطوط 9 تا 82 چرخه‌های ضروری برای نشانه‌گذاری معرفی شده‌اند تا تجزیه مستقیم CSS به جای لینک کردن به فایل دیگر تضمین شود.

در خطوط 89 تا 96 نشانه‌گذاری پوسته اپلیکیشن اصلی ارائه شده‌اند. این قسمت‌ها (به خصوص درون تگ اصلی در خط 93) در ادامه از سوی جاوا اسکریپت دستکاری خواهند شد.

در خط 99، اسکریپت نقش بیشتری ایفا می‌کند. خصوصیت async به عدم مسدودسازی تجزیه‌کننده در زمان دانلود شدن اسکریپت‌ها کمک می‌کند.

پوسته اپلیکیشن همچنین مراحل Push و Render را در الگوی PRPL تضمین می‌کند. این وضعیت زمانی رخ می‌دهد که HTML از سوی مرورگر برای تشکیل پیکسل‌ها روی صفحه تجزیه شود. پوسته اپلیکیشن به سرعت همه منابع ضروری را می‌یابد. ضمناً «critical scripts» (اسکریپت‌های ضروری) از طریق دستکاری DOM مسئول نمایش «مسیر اولیه» (initial route) یعنی Render هستند.

با این حال اگر از Service Worker برای کش کردن پوسته استفاده نکنیم، در بارگذاری‌های مجدد آتی تأثیری نخواهد داشت و از مزیت‌های عملکردی آن بهره‌مند نخواهیم شد.

قطعه کد زیر یک Service Worker را نمایش می‌دهد که پوسته و همه فایل‌های استاتیک مورد نیاز برای اپلیکیشن را کش می‌کند.

1var staticAssetsCacheName = 'todo-assets-v3';
2var dynamicCacheName = 'todo-dynamic-v3';
3
4self.addEventListener('install', function (event) {
5    self.skipWaiting();
6    event.waitUntil(
7      caches.open(staticAssetsCacheName).then(function (cache) {
8        cache.addAll([
9            '/',
10            "chunks/todo.d41d8cd98f00b204e980.js","index.html","main.d41d8cd98f00b204e980.js"
11        ]
12        );
13      }).catch((error) => {
14        console.log('Error caching static assets:', error);
15      })
16    );
17  });
18
19  self.addEventListener('activate', function (event) {
20    if (self.clients && clients.claim) {
21      clients.claim();
22    }
23    event.waitUntil(
24      caches.keys().then(function (cacheNames) {
25        return Promise.all(
26          cacheNames.filter(function (cacheName) {
27            return (cacheName.startsWith('todo-')) && cacheName !== staticAssetsCacheName;
28          })
29          .map(function (cacheName) {
30            return caches.delete(cacheName);
31          })
32        ).catch((error) => {
33            console.log('Some error occurred while removing existing cache:', error);
34        });
35      }).catch((error) => {
36        console.log('Some error occurred while removing existing cache:', error);
37    }));
38  });
39
40  self.addEventListener('fetch', (event) => {
41    event.respondWith(
42      caches.match(event.request).then((response) => {
43        return response || fetch(event.request)
44          .then((fetchResponse) => {
45              return cacheDynamicRequestData(dynamicCacheName, event.request.url, fetchResponse.clone());
46          }).catch((error) => {
47            console.log(error);
48          });
49      }).catch((error) => {
50        console.log(error);
51      })
52    );
53  });
54
55  function cacheDynamicRequestData(dynamicCacheName, url, fetchResponse) {
56    return caches.open(dynamicCacheName)
57      .then((cache) => {
58        cache.put(url, fetchResponse.clone());
59        return fetchResponse;
60      }).catch((error) => {
61        console.log(error);
62      });
63  }

خطوط 4 تا 17 یک رویداد از تعدادی service worker نصب می‌کند که به کش کردن همه فایل‌های استاتیک کمک می‌کند. در این بخش می‌توانید منابع پوسته اپلیکیشن شامل CSS جاوا اسکریپت، تصاویر و غیره را برای مسیر نخست کش کنید. ضمناً می‌توانید بخش‌های باقیمانده فایل‌های اپلیکیشن را کش کنید تا اطمینان حاصل شود که کل اپلیکیشن می‌تواند به صورت آفلاین نیز اجرا شود. کش کردن فایل‌های استاتیک جدا از پوشه اپلیکیشن اصلی مرحله Pre-cache الگوی PRPL را تضمین می‌کند.

در خط‌های 19 تا 38 رویداد activate برای پاک‌سازی کش‌های استفاده نشده تدارک دیده شده است.

خطوط 40 تا 63 کد فوق به واکشی منابع از کش (در صورت وجود) و یا مراجعه به شبکه است. ضمناً اگر یک فراخوانی شبکه صورت بگیرد، در این صورت آن منبع در کش نیست و در یک کش جداگانه جدید قرار می‌گیرد. این سناریو امکان کش کردن همه داده‌های دینامیک برای یک اپلیکیشن را فراهم می‌سازد.

بدین ترتیب اکثر بخش‌های معماری را پوشش دادیم. تنها بخشی که باقی‌مانده است مرحله «بارگذاری با تأخیر» (Lazy Loading) است که از الگوی PRPL استفاده می‌کند. این بخش را با در نظر گرفتن جاوا اسکریپت در ادامه مورد بررسی قرار می‌دهیم.

تنظیم Build

شاید تاکنون از خود پرسیده باشید یک ساختار معماری خوب بدون تنظیم Build چگونه به دست می‌آید؟ پاسخ در Webpack است. البته ابزارهای دیگری مانند Parcel ،Rollup و غیره نیز وجود دارند، اما همه مفاهیمی که در Webpack به کار می‌گیریم روی هر ابزار دیگری نیز قابل اجرا است.

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

ما می‌دانیم که برای توسعه‌دهندگان چه قدر سخت است که Webpack یا هر ابزار دیگری را از صفر پیکربندی کنند. در این مقاله (+) مطالب خوبی برای کمک به درک مراحل تنظیمات Build ارائه شده است.

هر کجا که در حین مطالعه این مقاله مشکلی داشتید، می‌توانید به مقاله فوق مراجعه کنید. در ادامه ابتدا به بررسی مفاهیم مورد نیاز برای Build می‌پردازیم.

پشتیبانی از ایمپورت‌های ساده و دینامیک

Babel یک transpiler محبوب است که به فرایند بازگردانی یا transpile کردن قابلیت‌های ES6 به نسخه ES5 کمک می‌کند. ما به پکیج‌های زیر برای فراهم ساختن امکان کار Babel با Webpack نیاز داریم:

  • @babel/core
  • @babel/plugin-syntax-dynamic-import
  • @babel/preset-env
  • babel-core
  • babel-loader
  • babel-preset-env

در ادامه یک قطعه کوتاه از کد babelrc می‌بینید:

1{
2    "presets": ["@babel/preset-env"],
3    "plugins": ["@babel/plugin-syntax-dynamic-import"]
4}

در طی مرحله تنظیم babel باید از خط 2، کد زیر در presets را وارد کنیم تا babel بتواند ES6 را به ES5 ترجمه کند و خط سوم در plugins برای ایجاد امکان پشتیبانی از ایمپورت دینامیک با Webpack کاربرد دارد.

در ادامه طرز کار babel به همراه Webpack را می‌بینید:

1module.exports = {
2    entry: {
3        // Mention Entry File
4    },
5    output: {
6        // Mention Output Filenames
7    },
8    module: {
9        rules: [
10            {
11                test: /\.js$/,
12                exclude: /node_modules/,
13                use: {
14                    loader: 'babel-loader'
15                }
16            }
17        ]
18    },
19    plugins: [
20        // Plugins
21    ]
22};

در خطوط 10 تا 17 بارگذاری کننده babel برای راه‌اندازی فرایند transpilation برای babel در فایل webpack.config.js استفاده می‌شود. برای توضیح ساده‌تر بخش‌های دیگر پیکربندی حذف یا کامنت شده‌اند.

پشتیبانی از SASS و CSS

برای راه‌اندازی SASS و CSS باید پکیج‌های زیر را داشته باشید:

  • sass-loader
  • css-loader
  • style-loader
  • MiniCssExtractPlugin

پیکربندی آن‌ها به صورت زیر انجام می‌گیرد:

1module.exports = {
2    entry: {
3        // Mention Entry File
4    },
5    output: {
6        // Mention Output Filenames
7    },
8    module: {
9        rules: [
10            {
11                test: /\.js$/,
12                exclude: /node_modules/,
13                use: {
14                    loader: 'babel-loader'
15                }
16            },
17            {
18                test: /\.scss$/,
19                use: [
20                    'style-loader',
21                    MiniCssExtractPlugin.loader,
22                    'css-loader',
23                    'sass-loader'
24                ]
25            }
26        ]
27    },
28    plugins: [
29        new MiniCssExtractPlugin({
30            filename: '[name].css'
31        }),
32    ]
33};

خطوط 17 تا 25 جایی است که loader-ها ثبت می‌شوند؛ در خطوط 29 تا 31 از آنجا که از یک افزونه برای استخراج فایل CSS استفاده شده ، ما نیز از MiniCssExtractPlugin استفاده می‌کنیم.

تنظیم محیط‌های سفارشی برای توسعه و توزیع

این مهم‌ترین مرحله از فرایند Build است. همه ما می‌دانیم که به یک تنظیم Build برای مراحل توسعه و توزیع برای اپلیکیشن‌های در حال نوشتن نیاز داریم و همچنین باید آن‌ها را در نهایت روی وب توزیع کنیم.

پکیج‌هایی که به این منظور استفاده می‌شوند به شرح زیر هستند:

  • clean-webpack-plugin برای پاکسازی محتوای پوشه dist
  • compression-webpack-plugin برای gzipp کردن محتوای فایل پوشه dist
  • copy-webpack-plugin برای کپی کردن فایل‌های استاتیک یا منابع دیگر از سورس اپلیکیشن به پوشه dist
  • html-webpack-plugin برای ایجاد فایل index.html در پوشه dist
  • webpack-md5-hash برای هش کردن فایل‌های منبع اپلیکیشن در پوشه dist
  • webpack-dev-server برای اجرای سرور توسعه محلی

در ادامه فایل پیکربندی نهایی Webpack را می‌بینید:

1const path = require('path');
2const MiniCssExtractPlugin = require('mini-css-extract-plugin');
3const HtmlWebpackPlugin = require('html-webpack-plugin');
4const WebpackMd5Hash = require('webpack-md5-hash');
5const CleanWebpackPlugin = require('clean-webpack-plugin');
6const CopyWebpackPlugin = require('copy-webpack-plugin');
7const CompressionPlugin = require('compression-webpack-plugin');
8
9module.exports = (env, argv) => ({
10    entry: {
11        main: './src/main.js'
12    },
13    devtool: argv.mode === 'production' ? false : 'source-map',
14    output: {
15        path: path.resolve(__dirname, 'dist'),
16        chunkFilename:
17            argv.mode === 'production'
18                ? 'chunks/[name].[chunkhash].js'
19                : 'chunks/[name].js',
20        filename:
21            argv.mode === 'production' ? '[name].[chunkhash].js' : '[name].js'
22    },
23    module: {
24        rules: [
25            {
26                test: /\.js$/,
27                exclude: /node_modules/,
28                use: {
29                    loader: 'babel-loader'
30                }
31            },
32            {
33                test: /\.scss$/,
34                use: [
35                    'style-loader',
36                    MiniCssExtractPlugin.loader,
37                    'css-loader',
38                    'sass-loader'
39                ]
40            }
41        ]
42    },
43    plugins: [
44        new CleanWebpackPlugin('dist', {}),
45        new MiniCssExtractPlugin({
46            filename:
47                argv.mode === 'production'
48                    ? '[name].[contenthash].css'
49                    : '[name].css'
50        }),
51        new HtmlWebpackPlugin({
52            inject: false,
53            hash: true,
54            template: './index.html',
55            filename: 'index.html'
56        }),
57        new WebpackMd5Hash(),
58        new CopyWebpackPlugin([
59            // {
60            //     from: './src/assets',
61            //     to: './assets'
62            // },
63            // {
64            //     from: 'manifest.json',
65            //     to: 'manifest.json'
66            // }
67        ]),
68        new CompressionPlugin({
69            algorithm: 'gzip'
70        })
71    ],
72    devServer: {
73        contentBase: 'dist',
74        watchContentBase: true,
75        port: 1000
76    }
77});

در خطوط 9 تا 77 کل پیکربندی Webpack در یک تابع قرار دارد که دو آرگومان می‌گیرد. در این جا ما از argv استفاده کرده این یعنی آرگومان‌ها در زمان اجرای دستورهای webpack یا webpack-dev-server ارسال می‌شوند؛ تصویر زیر بخش اسکریپت‌ها را در فایل package.json نمایش می‌دهد.

وب اپلیکیشن های پیشرونده

بر همین اساس اگر دستور npm run build را اجرا کنیم، یک build برای production ایجاد می‌شود و اگر دستور npm run server اجرا شود، یک گردش توسعه با استفاده از سرور توسعه محلی به اجرا درآمده و آغاز خواهد شد.

خطوط 44 تا 77 دلیل نیاز افزونه‌ها و پیکربندی سرور توسعه به تنظیم را نمایش می‌دهد. خطوط 59 تا 66 هر گونه منابع یا فایل‌های استاتیک را که باید از منبع اپلیکیشن کپی شوند نشان می‌دهند.

Build کردن Service Worker سفارشی

از آنجا که همه ما می‌دانیم نوشتن مجدد نام برای همه فایل‌ها جهت کش شدن کار پیچیده‌ای است، می‌خواهیم یک اسکریپت Build برای Service Worker سفارشی بنویسیم که اقدام به کش کردن فایل‌ها در پوشه dist کند و سپس آن‌ها را به عنوان محتوای کش در قالب Service Worker اضافه کنیم. در نهایت فایل Service Worker در پوشه dist نوشته خواهد شد.

مفاهیم مرتبط با فایل Service Worker که صحبتش را کردیم نیز شبیه همین است. اسکریپت مزبور به صورت زیر است:

1const glob = require('glob');
2const fs = require('fs');
3
4const dest = 'dist/sw.js';
5const staticAssetsCacheName = 'todo-assets-v1';
6const dynamicCacheName = 'todo-dynamic-v1';
7
8let staticAssetsCacheFiles = glob
9    .sync('dist/**/*')
10    .map((path) => {
11        return path.slice(5);
12    })
13    .filter((file) => {
14        if (/\.gz$/.test(file)) return false;
15        if (/sw\.js$/.test(file)) return false;
16        if (!/\.+/.test(file)) return false;
17        return true;
18    });
19
20const stringFileCachesArray = JSON.stringify(staticAssetsCacheFiles);
21
22const serviceWorkerScript = `var staticAssetsCacheName = '${staticAssetsCacheName}';
23var dynamicCacheName = '${dynamicCacheName}';
24self.addEventListener('install', function (event) {
25    self.skipWaiting();
26    event.waitUntil(
27      caches.open(staticAssetsCacheName).then(function (cache) {
28        cache.addAll([
29            '/',
30            ${stringFileCachesArray.slice(1, stringFileCachesArray.length - 1)}
31        ]
32        );
33      }).catch((error) => {
34        console.log('Error caching static assets:', error);
35      })
36    );
37  });
38  self.addEventListener('activate', function (event) {
39    if (self.clients && clients.claim) {
40      clients.claim();
41    }
42    event.waitUntil(
43      caches.keys().then(function (cacheNames) {
44        return Promise.all(
45          cacheNames.filter(function (cacheName) {
46            return (cacheName.startsWith('todo-')) && cacheName !== staticAssetsCacheName;
47          })
48          .map(function (cacheName) {
49            return caches.delete(cacheName);
50          })
51        ).catch((error) => {
52            console.log('Some error occurred while removing existing cache:', error);
53        });
54      }).catch((error) => {
55        console.log('Some error occurred while removing existing cache:', error);
56    }));
57  });
58  self.addEventListener('fetch', (event) => {
59    event.respondWith(
60      caches.match(event.request).then((response) => {
61        return response || fetch(event.request)
62          .then((fetchResponse) => {
63              return cacheDynamicRequestData(dynamicCacheName, event.request.url, fetchResponse.clone());
64          }).catch((error) => {
65            console.log(error);
66          });
67      }).catch((error) => {
68        console.log(error);
69      })
70    );
71  });
72  function cacheDynamicRequestData(dynamicCacheName, url, fetchResponse) {
73    return caches.open(dynamicCacheName)
74      .then((cache) => {
75        cache.put(url, fetchResponse.clone());
76        return fetchResponse;
77      }).catch((error) => {
78        console.log(error);
79      });
80  }
81`;
82
83fs.writeFile(dest, serviceWorkerScript, function(error) {
84    if (error) return;
85    console.log('Service Worker Write success');
86});

خطوط 8 تا 18 جایی است که همه محتوای پوشه dist به صورت یک آرایه staticAssetsCacheFiles قرار می‌گیرد.

خطوط 22 تا 85 جایی است که قالب Service Worker که در موردش صحبت کردیم قرار دارد. این مفاهیم دقیقاً مشابه هستند و چون متغیرهای تعریف شده در قالب را داریم، می‌توانیم از قالب Service Worker استفاده کرده و در کاربردهای بعدی از آن بهره بگیریم. این قالب الزامی است، زیرا در خط 33 باید محتوای پوشه dist را به کش اضافه کنیم.

خطوط 87 تا 90 در نهایت یک فایل Service Worker جدید است که در پوشه dist همراه با محتوای پوشه قالب Service Worker یعنی serviceWorkerScript نوشته خواهد شد.

دستور اجرای اسکریپت فوق به صورت node build-sw است و باید پس از اجرای دستور webpack --mode production اجرا شود.

اسکریپت ساخت service worker کمک زیادی به کش کردن آسان فایل‌ها می‌کند. بدین ترتیب می‌توان از آن در پروژه‌های دیگر نیز استفاده کرد زیرا سادگی آن می‌تواند موجب مدیریت راحت‌تر مسئله کش کردن شود.

اگر می‌خواهید از کتابخانه‌ای برای قابلیت‌های مرتبط با وب‌اپلیکیشن‌های پیشرونده استفاده کنید، می‌توانید از Workbox (+) استفاده کنید. این کتابخانه کارهای خوبی انجام می‌دهد و قابلیت‌های جالبی دارد که می‌توانید مورد استفاده قرار دهید.

بررسی نهایی پکیج‌ها

در ادامه فایل نمونه package.json را با همه وابستگی‌هایش می‌بینید:

1{
2  "name": "vanilla-todos-pwa",
3  "version": "1.0.0",
4  "description": "A simple todo application using <span class="englishfont">ES6</span> and Webpack",
5  "main": "src/main.js",
6  "scripts": {
7    "build": "webpack --mode production && node build-sw",
8    "serve": "webpack-dev-server --mode=development --hot"
9  },
10  "keywords": [],
11  "author": "Anurag Majumdar",
12  "license": "MIT",
13  "devDependencies": {
14    "@babel/core": "^7.2.2",
15    "@babel/plugin-syntax-dynamic-import": "^7.2.0",
16    "@babel/preset-env": "^7.2.3",
17    "autoprefixer": "^9.4.5",
18    "babel-core": "^6.26.3",
19    "babel-loader": "^8.0.4",
20    "babel-preset-env": "^1.7.0",
21    "clean-webpack-plugin": "^1.0.0",
22    "compression-webpack-plugin": "^2.0.0",
23    "copy-webpack-plugin": "^4.6.0",
24    "css-loader": "^2.1.0",
25    "html-webpack-plugin": "^3.2.0",
26    "mini-css-extract-plugin": "^0.5.0",
27    "node-sass": "^4.11.0",
28    "sass-loader": "^7.1.0",
29    "style-loader": "^0.23.1",
30    "terser": "^3.14.1",
31    "webpack": "^4.28.4",
32    "webpack-cli": "^3.2.1",
33    "webpack-dev-server": "^3.1.14",
34    "webpack-md5-hash": "0.0.6"
35  }
36}

به خاطر داشته باشید که Webpack به طور مکرر به‌روزرسانی می‌شود و تغییرات به طور مداوم در جامعه مربوطه در حال رخ دادن است و افزونه‌های جدید جایگزین افزونه‌های قبلی می‌شوند. بنابراین مهم است که به مفاهیم مورد نیاز برای تنظیم Build دقت کنید و نه پکیج‌هایی که عملاً استفاده می‌شوند.

قابلیت‌های جاوا اسکریپت

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

در این بخش در مورد قابلیت‌های کمینه محضی که لازم است تا طرح‌بندی اپلیکیشن سازماندهی شود صحبت خواهیم کرد. در ادامه می‌توانید فریمورک‌ها یا پکیج‌های خودتان را به اپلیکیشن اضافه کنید.

ماژول‌های ES6

ما از گزاره‌های ایمپورت و اکسپورت ES6 استفاده می‌کنیم و با هر فایل به صورت یک ماژول ES6 رفتار می‌کنیم. این قابلیت به طور رایج از سوی فریمورک‌های محبوب مانند انگولار و ری‌اکت استفاده می‌شود و کاملاً کارآمد است. با بهره‌گیری از توان پیکربندی Webpack می‌توانیم از توان گزاره‌های ایمپورت و اکسپورت به طور کامل استفاده کنیم.

1import { appTemplate } from './app.template';
2import { AppModel } from './app.model';
3
4export const AppComponent = {
5  // App Component code here...
6};

ساختار Object Literal یا ساختار کلاس ES6

کامپوننت‌های Object Literal بخش بسیار مهمی از اپلیکیشن ما هستند. گرچه می‌توانیم از جدیدترین استانداردهای وب مانند Web Components نیز استفاده کنیم، اما برای این که همه چیز ساده بماند می‌توانیم پا را فراتر گذاشته و از ساختار Object Literal یا ساختار کلاس ES6 استفاده می‌کنیم.

تنها نکته‌ای که نیاز داریم این است که وهله‌ای از آن بسازیم و سپس آن را اکسپورت کنیم. بنابراین برای این که مسائل از این هم ساده‌تر بمانند، می‌خواهیم با ساختار Object Literal این معماری کامپوننت را پیاده‌سازی کنیم.

1import { appTemplate } from './app.template';
2import { AppModel } from './app.model';
3
4export const AppComponent = {
5
6    init() {
7        this.appElement = document.querySelector('#app');
8        this.initEvents();
9        this.render();
10    },
11
12    initEvents() {
13        this.appElement.addEventListener('click', event => {
14            if (event.target.className === 'btn-todo') {
15                import( /* webpackChunkName: "todo" */ './todo/todo.module')
16                    .then(lazyModule => {
17                        lazyModule.TodoModule.init();
18                    })
19                    .catch(error => 'An error occurred while loading Module');
20            }
21        });
22
23        document.querySelector('.banner').addEventListener('click', event => {
24            event.preventDefault();
25            this.render();
26        });
27    },
28
29    render() {
30        this.appElement.innerHTML = appTemplate(AppModel);
31    }
32};

خطوط 4 تا 32 یک شیء به نام AppComponent را اکسپورت می‌کنند که بی‌درنگ برای استفاده در بخش‌های دیگر اپلیکیشن آماده خواهد بود.

شما می‌توانید پا را فراتر گذاشته و از ساختار کلاس ES6 یا کامپوننت‌های وب استاندارد نیز برای رسیدن به روشی «اعلانی» (declarative) برای نوشتن کد استفاده کنید. برای ساده‌تر شدن بحث، ما سعی می‌کنیم اپلیکیشن دمو را به روشی «دستوری» (imperative) بنویسیم.

ایمپورت‌های دینامیک

آیا به خاطر دارید که قبلاً در مورد نبود حرف L در الگوی PRPL صحبت کردیم؟ ایمپورت دینامیک روشی برای بارگذاری با تأثیر کامپوننت یا ماژول‌ها محسوب می‌شود. از آنجا که ما از App Shell و PRPL به صورت همزمان برای کش کردن پوسته و دیگر فایل‌های مسیر استفاده می‌کنیم، ایمپورت‌های دینامیک کامپوننت یا ماژول lazy را به جای شبکه از کش بارگذاری می‌کنند.

توجه داشته باشید که اگر تنها از معماری App Shell استفاده کنیم، فایل‌های باقیمانده اپلیکیشن یعنی محتوای پوشه chunks را نمی‌توان کش کرد.

خطوط 15 تا 19 به کد App Component اشاره دارند. این همان جایی است که ایمپورت دینامیک تأثیر خود را نمایش می‌دهد. اگر روی دکمه‌ای که کلاس btn-todo دارد کلیک کنیم، تنها این TodoModule بارگذاری می‌شود. به هر حال TodoModule صرفاً فایل دیگر جاوا اسکریپت است که شامل مجموعه‌ای از کامپوننت‌های شیء است.

تابع‌های Arrow در ES6 و Literal-های قالب ES6

تابع‌های Arrow می‌توانند به طور خاص در مواردی که بخواهیم مطمئن شویم کلیدواژه this درون تابع قرار دارد استفاده شوند. در این وضعیت this به چارچوب پیرامونی خود که تابع Arrow در آن اعلان شده است اشاره می‌کند. جدا از این کاربرد، این تابع‌ها به ایجاد ساختار مختصر و منسجم بسیار کمک می‌کنند.

1export const appTemplate = model => `
2    <section class="app">
3        <h3> ${model.title} </h3>
4        <section class="button">
5            <button class="btn-todo"> Todo Module </button>
6        </section>
7    </section>
8`;

مثال فوق یک تابع قالبی است که به صورت یک تابع Arrow تعریف شده و یک مدل می‌پذیرد که رشته‌های HTML بازگشت می‌دهد. این رشته شامل داده‌های مدل است. میان‌یابی رشته به کمک literal-های قالبی ES6 اجرا می‌شود. مزیت اصلی literal-های قالبی در رشته‌های چندخطی و «میان‌یابی» (interpolation) داده‌های مدل در رشته‌ها نمایش می‌یابد.

در ادامه یک نکته مختصر برای مدیریت قالب‌بندی کامپوننت و تولید کامپوننت‌های با قابلیت استفاده مجدد مشاهده می‌کنید. با استفاده از تابع reduce می‌توان همه رشته‌های HTML را مانند مثال زیر تجمیع کرد:

1const WorkModel = [
2    {
3        id: 1,
4        src: '',
5        alt: '',
6        designation: '',
7        period: '',
8        description: ''
9    },
10    {
11        id: 2,
12        src: '',
13        alt: '',
14        designation: '',
15        period: '',
16        description: ''
17    },
18    //...
19];
20
21
22const workCardTemplate = (cardModel) => `
23<section id="${cardModel.id}" class="work-card">
24    <section class="work__image">
25        <img class="work__image-content" type="image/svg+xml" src="${
26            cardModel.src
27        }" alt="${cardModel.alt}" />
28    </section>
29    <section class="work__designation">${cardModel.designation}</section>
30    <section class="work__period">${cardModel.period}</section>
31    <section class="work__content">
32        <section class="work__content-text">
33            ${cardModel.description}
34        </section>
35    </section>
36</section>
37`;
38
39export const workTemplate = (model) => `
40<section class="work__section">
41    <section class="work-text">
42        <header class="header-text">
43            <span class="work-text__header"> Work </span>
44        </header>
45        <section class="work-text__content content-text">
46            <p class="work-text__content-para">
47                This area signifies work experience
48            </p>
49        </section>
50    </section>
51    <section class="work-cards">
52        ${model.reduce((html, card) => html + workCardTemplate(card), '')}
53    </section>
54</section>
55`;

قطعه کد فوق واقعاً کار بسیار زیادی انجام می‌دهد. این کد ساده و سرراست است و ربط چندانی به فریمورک‌های موجود ندارد. خطوط 1 تا 9 یک آرایه مدل ساده هستند که تابع reduce روی آن اجرا می‌شود تا خصوصیت قالب با استفاده مجدد را پدید آورد.

خط 53 هم کار معجزه گونه تولید کامپوننت‌های چندگانه با استفاده مجدد را درون یک رشته HTML انجام می‌دهد. تابع reduce در نخستین آرگومان خود، تجمیع‌کننده را دریافت می‌کند و همه مقادیر آرایه در آرگومان دوم ارائه می‌شوند. به لطف این قابلیت‌های ساده ما اینک یک ساختار اپلیکیشن در اختیار داریم. بهترین روش برای یادگیری یک قابلیت، استفاده عملی از آن است، پس با ما همراه باشید تا این قابلیت را عملیاتی کنیم.

دموی اپلیکیشن

اینک شما موفق شده‌اید مباحث نظری این مقاله را پشت سر بگذارید. این مقاله قابلیت‌های زیادی را پوشش داده است. آشنایی با همه مفاهیم و تکنیک‌ها به زمان نیاز دارد. در ادامه دموی یک اپلیکیشن TODO را ملاحظه می‌کنید که به کمک قابلیت‌های مورد بحث در این مقاله ساخته شده است. برای مشاهده عملی آن به این وب‌سایت (+) مراجعه کنید.

وب اپلیکیشن های پیشرونده
جهت مشاهده تصویر در ابعاد اصلی روی آن کلیک کنید.

برای مشاهده ریپازیتوری پروژه نیز به این صفحه (+) گیت‌هاب مراجعه کنید. شما می‌توانید ریپازیتوری پروژه را کلون کرده و برای درک بهتر مثال‌های مفهومی مطرح شده در این مقاله به بررسی کد آن بپردازید.

اپلیکیشن نمونه نهایی

وب‌سایت نهایی یک پورتفولیو است که از صفر تا صد به طور کامل با استفاده از قابلیت‌هایی که در این مقاله توضیح داده شده‌اند، طراحی و توسعه یافته و مهندسی شده است. این اپلیکیشن تک‌صفحه‌ای را می‌توان به ماژول‌ها و کامپوننت‌های سفارشی تقسیم کرد.

این انعطاف‌پذیری و قدرتی که از جاوا اسکریپت محض ناشی می‌شود، چیزی منحصربه‌فرد است و نتایجی شگفت‌انگیز خلق می‌کند. برای مشاهده این وب‌سایت به این لینک (+) مراجعه کنید. برای کسب تجربه عملی به وب‌سایت مراجعه کنید. رنگ‌ها در دمو به طور دقیق بازسازی نشده‌اند. مهندسی که روی این وب‌سایت انجام شده نتیجه زیر را تولید کرده است:

وب اپلیکیشن های پیشرونده

کسب نمره 100 با استفاده از فناوری‌های دیگر کار بسیار دشواری است.

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

==

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

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