Pull to refresh
2679.92
RUVDS.com
VDS/VPS-хостинг. Скидка 15% по коду HABR15

Разработка простых современных JavaScript-приложений с использованием Webpack и прогрессивных веб-технологий

Reading time 19 min
Views 31K
Original author: Anurag Majumdar
Думали ли вы о том, чтобы воспользоваться при разработке своего очередного веб-проекта простейшим из существующих набором технологий? Если это так — значит материал, перевод которого мы публикуем сегодня, написан специально для вас.

JavaScript-фреймворки существуют для того чтобы помочь нам создавать приложения, обладающие сходными возможностями, используя обобщённый подход. Однако многим приложениям вся та мощь, которую дают фреймворки, не нужна. Использование какого-нибудь фреймворка в проекте небольшого или среднего масштаба, к которому предъявляются некие специфические требования, вполне может оказаться ненужной тратой сил и времени.

image

В этой статье мы поговорим о применении современных технологий при разработке веб-приложений, возможности которых не ограничены возможностями фреймворков. Кстати, если вам это нужно, то вы, с использованием описываемых здесь технологий, сможете создать собственный узкоспециализированный фреймворк. Чистый JavaScript и другие базовые веб-технологии дают разработчикам возможность делать то, что им нужно, не ограничивая себя рамками применяемых ими инструментов.

Обзор


Прежде чем мы приступим к делу, давайте обсудим средства, которые нам понадобятся.

▍Архитектура приложения


Для того чтобы обеспечить высокую скорость загрузки приложения и удобство работы с ним, мы будем пользоваться следующими шаблонами проектирования:

  • Архитектура App Shell.
  • Паттерн PRPL (Push, Render, Pre-cache, Lazy loading).

▍Система сборки проекта


В нашем проекте понадобится качественная, настроенная в соответствии с нашими нуждами, система сборки. Здесь мы будем использовать Webpack, предъявляя к системе сборки проекта следующие требования:

  • Поддержка ES6 и возможностей динамического импорта ресурсов.
  • Поддержка SASS и CSS.
  • Раздельная настройка режимов разработки и реальной работы приложения.
  • Возможность автоматической настройки сервис-воркеров.

▍Современные возможности JavaScript


Мы будем пользоваться минимальным набором современных возможностей JavaScript, позволяющим нам разработать то, что нам нужно. Вот о каких возможностях идёт речь:

  • Модули.
  • Разные способы создания объектов (объектные литералы, классы).
  • Динамический импорт ресурсов.
  • Стрелочные функции.
  • Шаблонные литералы.

Теперь, получив общее представление о том, что нам понадобится, мы готовы к тому, чтобы приступить к разработке нашего проекта.

Архитектура приложения


Появление прогрессивных веб-приложений (Progressive Web Application, PWA) способствовало и приходу в веб-разработку новых архитектурных решений. Это позволило веб-приложениям быстрее загружаться и выводиться на экран. Комбинация архитектуры App Shell и паттерна PRPL может привести к тому, что веб-приложение будет быстрым и отзывчивым, похожим на обычное приложение.

▍Что такое App Shell и PRPL?


App Shell — это архитектурный паттерн, применяемый для разработки PWA, при использовании которого в браузер пользователя, при загрузке сайта, отправляют минимальный объём критически важных для работы сайта ресурсов. В состав этих материалов обычно входят все ресурсы, необходимые для первого вывода приложения на экран. Подобные ресурсы можно и кэшировать с использованием сервис-воркера.

Аббревиатура PRPL расшифровывается следующим образом:

  • Push — отправка клиенту критически важных ресурсов для исходного маршрута (в частности, с использованием HTTP/2).
  • Render — вывод исходного маршрута.
  • Pre-cache — заблаговременное кэширование оставшихся маршрутов или ресурсов.
  • Lazy load — «ленивая» загрузка частей приложения по мере того, как в них возникает необходимость (в частности — по запросу пользователя).

▍Реализация App Shell и PRPL в коде


Паттерны App Shepp и PRPL используются совместно. Это позволяет реализовывать передовые подходы к разработке веб-проектов. Вот как выглядит паттерн App Shell в коде:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <!-- Critical Styles -->
    <!-- Начало фрагмента №1 -->
    <style>
        html {
            box-sizing: border-box;
        }

        *,
        *:after,
        *:before {
            box-sizing: inherit;
        }

        body {
            margin: 0;
            padding: 0;
            font: 18px 'Oxygen', Helvetica;
            background: #ececec;
        }

        header {
            height: 60px;
            background: #512DA8;
            color: #fff;
            display: flex;
            align-items: center;
            padding: 0 40px;
            box-shadow: 1px 2px 6px 0px #777;
        }

        h1 {
            margin: 0;
        }

        .banner {
            text-decoration: none;
            color: #fff;
            cursor: pointer;
        }

        main {
            display: flex;
            justify-content: center;
            height: calc(100vh - 140px);
            padding: 20px 40px;
            overflow-y: auto;
        }

        button {
            background: #512DA8;
            border: 2px solid #512DA8;
            cursor: pointer;
            box-shadow: 1px 1px 3px 0px #777;
            color: #fff;
            padding: 10px 15px;
            border-radius: 20px;
        }

        .button {
            display: flex;
            justify-content: center;
        }

        button:hover {
            box-shadow: none;
        }

        footer {
            height: 40px;
            background: #2d3850;
            color: #fff;
            display: flex;
            align-items: center;
            padding: 40px;
        }
    </style>
    <!-- Конец фрагмента №1 -->

    <title>Vanilla Todos PWA</title>
</head>

<body>

    <body>
        <!-- Main Application Section -->
        <!-- Начало фрагмента №2 -->
        <header>
            <h3><font color="#3AC1EF">▍<a class="banner"> Vanilla Todos PWA </a></font></h3>
        </header>
        <main id="app"></main>
        <footer>
            <span>© 2019 Anurag Majumdar - Vanilla Todos SPA</span>
        </footer>
        <!-- Конец фрагмента №2 -->
      
        <!-- Critical Scripts -->
        <!-- Начало фрагмента №3 -->
        <script async src="<%= htmlWebpackPlugin.files.chunks.main.entry %>"></script>
        <!-- Конец фрагмента №3 -->

        <noscript>
            This site uses JavaScript. Please enable JavaScript in your browser.

        </noscript>
    </body>
</body>

</html>

Изучив этот код, можно понять, что шаблон App Shell предусматривает создание «оболочки» приложения, которая представляет собой его «скелет», содержащий минимум разметки. Разберём этот код (здесь и далее фрагменты кода, на которые мы будем ссылаться при разборе, отмечены комментариями, наподобие <!-- Начало фрагмента №1 -->).

  • Фрагмент №1. Важнейшие стили встроены в разметку, а не представлены в виде отдельных файлов. Сделано это для того чтобы CSS-код был бы обработан непосредственно при загрузке HTML-страницы.
  • Фрагмент №2. Здесь представлена «оболочка» приложения. Этими областями позже будет управлять JavaScript-код. Особенно это относится к тому, что будет находиться в теге main с идентификатором app (<main id="app"></main>).
  • Фрагмент №3. Тут в дело вступают скрипты. Атрибут async позволяет не блокировать парсер во время загрузки скриптов.

Представленный выше «скелет» приложения реализует шаги Push и Render паттерна PRPL. Это происходит при разборе HTML-кода браузером для формирования визуального представления страницы. При этом браузер быстро находит критически важные для вывода страницы ресурсы. Кроме того, здесь представлены скрипты (фрагмент №3), ответственные за вывод исходного маршрута путём манипуляций с DOM (на шаге Render).

Однако если мы не используем сервис-воркер для кэширования «оболочки» приложения, то мы не получим выигрыша в производительности, например, при повторных загрузках страницы.

Ниже показан код сервис-воркера, кэширующего «скелет» и все статические ресурсы приложения.

var staticAssetsCacheName = 'todo-assets-v3';
var dynamicCacheName = 'todo-dynamic-v3';

// Начало фрагмента №1
self.addEventListener('install', function (event) {
    self.skipWaiting();
    event.waitUntil(
      caches.open(staticAssetsCacheName).then(function (cache) {
        cache.addAll([
            '/',
            "chunks/todo.d41d8cd98f00b204e980.js","index.html","main.d41d8cd98f00b204e980.js"
        ]
        );
      }).catch((error) => {
        console.log('Error caching static assets:', error);
      })
    );
  });
// Конец фрагмента №1

// Начало фрагмента №2
  self.addEventListener('activate', function (event) {
    if (self.clients && clients.claim) {
      clients.claim();
    }
    event.waitUntil(
      caches.keys().then(function (cacheNames) {
        return Promise.all(
          cacheNames.filter(function (cacheName) {
            return (cacheName.startsWith('todo-')) && cacheName !== staticAssetsCacheName;
          })
          .map(function (cacheName) {
            return caches.delete(cacheName);
          })
        ).catch((error) => {
            console.log('Some error occurred while removing existing cache:', error);
        });
      }).catch((error) => {
        console.log('Some error occurred while removing existing cache:', error);
    }));
  });
// Конец фрагмента №2

// Начало фрагмента №3
  self.addEventListener('fetch', (event) => {
    event.respondWith(
      caches.match(event.request).then((response) => {
        return response || fetch(event.request)
          .then((fetchResponse) => {
              return cacheDynamicRequestData(dynamicCacheName, event.request.url, fetchResponse.clone());
          }).catch((error) => {
            console.log(error);
          });
      }).catch((error) => {
        console.log(error);
      })
    );
  });
// Конец фрагмента №3

  function cacheDynamicRequestData(dynamicCacheName, url, fetchResponse) {
    return caches.open(dynamicCacheName)
      .then((cache) => {
        cache.put(url, fetchResponse.clone());
        return fetchResponse;
      }).catch((error) => {
        console.log(error);
      });
  }

Разберём этот код.

  • Фрагмент №1. Обработка события install сервис-воркера помогает кэшировать статические ресурсы. Здесь можно поместить в кэш ресурсы «скелета» приложения (CSS, JavaScript, изображения, и так далее) для первого маршрута (в соответствии с наполнением «скелета»). Кроме того, можно загрузить и другие ресурсы приложения, сделав так, чтобы оно могло бы работать без подключения к интернету. Кэширование ресурсов, помимо кэширования «скелета», соответствует шагу Pre-cache паттерна PRPL.
  • Фрагмент №2. При обработке события activate выполняется очистка неиспользуемых кэшей.
  • Фрагмент №3. В этих строках кода выполняется загрузка ресурсов из кэша в том случае, если они там есть. В противном случае выполняются сетевые запросы. Кроме того, если сделан сетевой запрос на получение ресурса, это означает, что этот ресурс ещё не кэширован. Такой ресурс помещают в новый отдельный кэш. Этот сценарий помогает кэшировать динамические данные приложения.

К настоящему моменту мы обсудили большую часть архитектурных решений, которые будут использоваться в нашем приложении. Единственное, о чём мы пока не говорили — это шаг Lazy loading паттерна PRPL. К нему мы вернёмся позже, а пока займёмся системой сборки проекта.

Система сборки проекта


Одной только хорошей архитектуры, без достойной системы сборки проекта, недостаточно для создания качественного приложения. Тут нам и пригодится Webpack. Существуют и другие средства для сборки проектов (бандлеры), например — Parcel и Rollup. Но то, что мы будем реализовывать на базе Webpack, можно сделать и с использованием других средств.

Здесь мы поговорим о том, как интересующие нас возможности связаны с плагинами для Webpack. Это позволит вам быстро ухватить суть создаваемой нами системы сборки. Подбор плагинов для бандлера и его правильная настройка — это важнейший шаг на пути к качественной системе сборки проекта. Вы, освоив эти принципы, сможете пользоваться ими и в будущем, при работе над собственными приложениями.

Непросто настраивать инструменты наподобие Webpack с нуля. В таких случаях полезно иметь под рукой какое-нибудь хорошее справочное руководство. Таким руководством, с использованием которого написана соответствующая часть данного материала, стала эта статья. Если у вас возникнут какие-то сложности с Webpack — обращайтесь к ней. Теперь давайте вспомним и реализуем те требования к системе сборки проекта, о которых мы говорили в самом начале.

▍Поддержка ES6 и возможностей динамического импорта ресурсов


Для реализации этих возможностей нам пригодится Babel — популярный транспилятор, который позволяет преобразовывать код, написанный с использованием возможностей ES6, в код, который может выполняться в ES5-средах. Для того чтобы наладить работу Babel с Webpack, мы можем воспользоваться следующими пакетами:

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

Вот пример файла .babelrc, рассчитанного на использование с Webpack:

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

В ходе настройки Babel строка presets этого файла используется для настройки Babel на транспиляцию ES6 в ES5, а строка plugins — для того, чтобы в Webpack можно было бы пользоваться динамическим импортом.

Вот как Babel используется с Webpack (тут приведён фрагмент файла настроек Webpack — webpack.config.js):

module.exports = {
    entry: {
        // Входные файлы
    },
    output: {
        // Выходные файлы
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader'
                }
            }
        ]
    },
    plugins: [
        // Плагины
    ]
};

В разделе rules этого файла описывается использование загрузчика babel-loader для настройки процесса транспиляции. Остальные части этого файла, ради краткости, опущены.

▍Поддержка SASS и CSS


Для обеспечения поддержки нашей системой сборки проектов SASS и CSS нам понадобятся следующие плагины:

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

Вот как выглядит файл настроек Webpack, в который внесены данные об этих плагинах:

module.exports = {
    entry: {
        // Входные файлы
    },
    output: {
        // Выходные файлы
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader'
                }
            },
            {
                test: /\.scss$/,
                use: [
                    'style-loader',
                    MiniCssExtractPlugin.loader,
                    'css-loader',
                    'sass-loader'
                ]
            }
        ]
    },
    plugins: [
        new MiniCssExtractPlugin({
            filename: '[name].css'
        }),
    ]
};

Загрузчики регистрируются в разделе rules. Так как мы используем плагин для извлечения CSS-стилей, в раздел plugins вносится соответствующая запись.

▍Раздельная настройка режимов разработки и реальной работы приложения


Это — чрезвычайно важная часть процесса сборки приложения. Всем известно, что при создании приложения одни настройки применяются для сборки той его версии, которая используется при разработке, а другие — для его продакшн-версии. Вот перечень пакетов, которые нам здесь пригодятся:

  • clean-webpack-plugin: для очистки содержимого папки dist.
  • compression-webpack-plugin: для сжатия содержимого папки dist.
  • copy-webpack-plugin: для копирования статических ресурсов, например — файлов, из папок с исходными данными приложения в папку dist.
  • html-webpack-plugin: для создания файла index.html в папке dist.
  • webpack-md5-hash: для хэширования файлов приложения в папке dist.
  • webpack-dev-server: для запуска локального сервера, используемого в ходе разработки.

Вот как выглядит итоговый файл webpack.config.js:

const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const WebpackMd5Hash = require('webpack-md5-hash');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');

module.exports = (env, argv) => ({
    entry: {
        main: './src/main.js'
    },
    devtool: argv.mode === 'production' ? false : 'source-map',
    output: {
        path: path.resolve(__dirname, 'dist'),
        chunkFilename:
            argv.mode === 'production'
                ? 'chunks/[name].[chunkhash].js'
                : 'chunks/[name].js',
        filename:
            argv.mode === 'production' ? '[name].[chunkhash].js' : '[name].js'
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader'
                }
            },
            {
                test: /\.scss$/,
                use: [
                    'style-loader',
                    MiniCssExtractPlugin.loader,
                    'css-loader',
                    'sass-loader'
                ]
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin('dist', {}),
        new MiniCssExtractPlugin({
            filename:
                argv.mode === 'production'
                    ? '[name].[contenthash].css'
                    : '[name].css'
        }),
        new HtmlWebpackPlugin({
            inject: false,
            hash: true,
            template: './index.html',
            filename: 'index.html'
        }),
        new WebpackMd5Hash(),
        new CopyWebpackPlugin([
            // {
            //     from: './src/assets',
            //     to: './assets'
            // },
            // {
            //     from: 'manifest.json',
            //     to: 'manifest.json'
            // }
        ]),
        new CompressionPlugin({
            algorithm: 'gzip'
        })
    ],
    devServer: {
        contentBase: 'dist',
        watchContentBase: true,
        port: 1000
    }
});

Вся конфигурация Webpack представлена в виде функции, которая принимает два аргумента. Здесь использован аргумент argv, который представляет аргументы, передаваемые этой функции при выполнении команд webpack или webpack-dev-server. Вот как описание этих команд выглядит в файле проекта package.json:

"scripts": {
    "build": "webpack --mode production && node build-sw",
    "serve": "webpack-dev-server --mode=development --hot",
  },

В результате, если мы выполним команду npm run build, будет выполнена сборка продакшн-версии приложения. Если выполнить команду npm run serve, будет запущен сервер разработки, поддерживающий процесс работы над приложением.

В разделах plugins и devServer вышеприведённого файла показана настройка плагинов и сервера разработки.

В разделе, который начинается со строи new CopyWebpackPlugin, задают ресурсы, которые нужно скопировать из исходных материалов приложения.

▍Настройка сервис-воркера


Все мы знаем о том, что ручное составление списков файлов, например, предназначенных для кэширования, довольно-таки скучное занятие. Поэтому здесь мы воспользуемся специальным скриптом сборки сервис-воркера, который находит файлы в папке dist и добавляет их в качестве содержимого кэша в шаблоне сервис-воркера. После этого файл сервис-воркера будет записан в папку dist. Те концепции, о которых мы говорили в применении к сервис-воркерам, не меняются. Вот код скрипта build-sw.js:

const glob = require('glob');
const fs = require('fs');

const dest = 'dist/sw.js';
const staticAssetsCacheName = 'todo-assets-v1';
const dynamicCacheName = 'todo-dynamic-v1';

// Начало фрагмента №1
let staticAssetsCacheFiles = glob
    .sync('dist/**/*')
    .map((path) => {
        return path.slice(5);
    })
    .filter((file) => {
        if (/\.gz$/.test(file)) return false;
        if (/sw\.js$/.test(file)) return false;
        if (!/\.+/.test(file)) return false;
        return true;
    });
// Конец фрагмента №1

const stringFileCachesArray = JSON.stringify(staticAssetsCacheFiles);

// Начало фрагмента №2
const serviceWorkerScript = `var staticAssetsCacheName = '${staticAssetsCacheName}';
var dynamicCacheName = '${dynamicCacheName}';

self.addEventListener('install', function (event) {
    self.skipWaiting();
    event.waitUntil(
      caches.open(staticAssetsCacheName).then(function (cache) {
        cache.addAll([
            '/',
            ${stringFileCachesArray.slice(1, stringFileCachesArray.length - 1)}
        ]
        );
      }).catch((error) => {
        console.log('Error caching static assets:', error);
      })
    );
  });

  self.addEventListener('activate', function (event) {
    if (self.clients && clients.claim) {
      clients.claim();
    }
    event.waitUntil(
      caches.keys().then(function (cacheNames) {
        return Promise.all(
          cacheNames.filter(function (cacheName) {
            return (cacheName.startsWith('todo-')) && cacheName !== staticAssetsCacheName;
          })
          .map(function (cacheName) {
            return caches.delete(cacheName);
          })
        ).catch((error) => {
            console.log('Some error occurred while removing existing cache:', error);
        });
      }).catch((error) => {
        console.log('Some error occurred while removing existing cache:', error);
    }));
  });

  self.addEventListener('fetch', (event) => {
    event.respondWith(
      caches.match(event.request).then((response) => {
        return response || fetch(event.request)
          .then((fetchResponse) => {
              return cacheDynamicRequestData(dynamicCacheName, event.request.url, fetchResponse.clone());
          }).catch((error) => {
            console.log(error);
          });
      }).catch((error) => {
        console.log(error);
      })
    );
  });

  function cacheDynamicRequestData(dynamicCacheName, url, fetchResponse) {
    return caches.open(dynamicCacheName)
      .then((cache) => {
        cache.put(url, fetchResponse.clone());
        return fetchResponse;
      }).catch((error) => {
        console.log(error);
      });
  }
`;
// Конец фрагмента №2

// Начало фрагмента №3
fs.writeFile(dest, serviceWorkerScript, function(error) {
    if (error) return;
    console.log('Service Worker Write success');
});
// Конец фрагмента №3

Разберём этот код.

  • Фрагмент №1. Здесь список файлов из папки dist помещается в массив staticAssetsCacheFiles.
  • Фрагмент №2. Это — шаблон сервис-воркера, о котором мы говорили. При формировании готового кода используются переменные. Это делает шаблон универсальным, позволяя использовать его в будущем, в ходе развития проекта. Шаблон нам, кроме того, нужен из-за того, что в него мы добавляем сведения о содержимом папки dist, которое может со временем меняться. Для этого используется константа stringFileCachesArray.
  • Фрагмент №3. Здесь только что сформированный код сервис-воркера, хранящийся в константе serviceWorkerScript, записывается в файл, находящийся по адресу dist/sw.js.

Для запуска этого скрипта используется команда node build-sw. Её нужно выполнить после того, как будет завершено выполнение команды webpack --mode production.

Представленный здесь скрипт для сборки сервис-воркера значительно облегчает задачу организации кэширования файлов. Надо отметить, что этот скрипт уже нашёл применение в реальном проекте.

Если вы хотите воспользоваться специальной библиотекой, предназначенной для решения задачи работы прогрессивных веб-приложений в оффлайн-режиме — взгляните на Workbox. Она отличается очень интересными возможностями, поддающимися настройке.

▍Обзор пакетов, используемых в проекте


Вот файл package.json нашего проекта, в котором можно найти сведения о пакетах, используемых в этом проекте:

{
  "name": "vanilla-todos-pwa",
  "version": "1.0.0",
  "description": "A simple todo application using ES6 and Webpack",
  "main": "src/main.js",
  "scripts": {
    "build": "webpack --mode production && node build-sw",
    "serve": "webpack-dev-server --mode=development --hot"
  },
  "keywords": [],
  "author": "Anurag Majumdar",
  "license": "MIT",
  "devDependencies": {
    "@babel/core": "^7.2.2",
    "@babel/plugin-syntax-dynamic-import": "^7.2.0",
    "@babel/preset-env": "^7.2.3",
    "autoprefixer": "^9.4.5",
    "babel-core": "^6.26.3",
    "babel-loader": "^8.0.4",
    "babel-preset-env": "^1.7.0",
    "clean-webpack-plugin": "^1.0.0",
    "compression-webpack-plugin": "^2.0.0",
    "copy-webpack-plugin": "^4.6.0",
    "css-loader": "^2.1.0",
    "html-webpack-plugin": "^3.2.0",
    "mini-css-extract-plugin": "^0.5.0",
    "node-sass": "^4.11.0",
    "sass-loader": "^7.1.0",
    "style-loader": "^0.23.1",
    "terser": "^3.14.1",
    "webpack": "^4.28.4",
    "webpack-cli": "^3.2.1",
    "webpack-dev-server": "^3.1.14",
    "webpack-md5-hash": "0.0.6"
  }
}

Если говорить о поддержке подобных проектов, то надо отметить, что инструменты в экосистеме Webpack обновляются довольно часто. Нередко случается так, что существующие плагины заменяются новыми. Поэтому важно, принимая решения о том, нужно ли, вместо каких-то пакетов использовать более новые, ориентироваться не на сами пакеты, а на те возможности, которые они должны реализовывать. Собственно говоря, именно поэтому выше мы и говорили о том, какую роль играет тот или иной пакет.

Современные возможности JavaScript


В ходе разработки веб-приложения у программиста есть выбор — писать ли собственные реализации таких возможностей, как обнаружение изменений, маршрутизация, хранение данных, или пользоваться существующими пакетами.

Сейчас мы поговорим о минимальном наборе технологий, необходимых для обеспечения работы нашего проекта. При необходимости этот набор технологий можно расширить с использованием существующих фреймворков или пакетов.

▍Модули


Мы будем пользоваться возможностями ES6 по импорту и экспорту модулей, рассматривая каждый файл в виде ES6-модуля. Эта возможность часто встречается в популярных фреймворках, вроде Angular и React, пользоваться ей очень удобно. Благодаря имеющейся у нас конфигурации Webpack мы можем пользоваться выражениями импорта и экспорта ресурсов. Вот как это выглядит в файле app.js:

import { appTemplate } from './app.template';
import { AppModel } from './app.model';

export const AppComponent = {
  // Код компонента App...

};

▍Разные способы создания объектов


Создание компонентов — это важная часть процесса разработки нашего приложения. Тут вполне можно воспользоваться каким-нибудь современным средством, наподобие веб-компонентов, но мы, для того, чтобы не усложнять проект, будем пользоваться обычными объектами JavaScript, создавать которые можно либо с использованием объектных литералов, либо используя синтаксис классов, появившийся в стандарте ES6.

Особенность использования классов для создания объектов заключается в том, что после описания класса нужно создать на его основе экземпляр объекта, а после этого экспортировать этот объект. Для того чтобы упростить всё ещё сильнее, мы будем пользоваться здесь обычными объектами, создаваемыми с помощью объектных литералов. Вот код файла app.js, в котором можно видеть их применение.

import { appTemplate } from './app.template';
import { AppModel } from './app.model';

export const AppComponent = {

    init() {
        this.appElement = document.querySelector('#app');
        this.initEvents();
        this.render();
    },

    initEvents() {
        this.appElement.addEventListener('click', event => {
            if (event.target.className === 'btn-todo') {
                import( /* webpackChunkName: "todo" */ './todo/todo.module')
                    .then(lazyModule => {
                        lazyModule.TodoModule.init();
                    })
                    .catch(error => 'An error occurred while loading Module');
            }
        });

        document.querySelector('.banner').addEventListener('click', event => {
            event.preventDefault();
            this.render();
        });
    },

    render() {
        this.appElement.innerHTML = appTemplate(AppModel);
    }
};

Здесь мы формируем и экспортируем компонент AppComponent, которым сразу же можно пользоваться в других частях приложения.

Вы вполне можете использовать в подобных ситуациях классы ES6 или веб-компоненты, разрабатывая проект в стиле, который ближе к декларативному, чем тот, который используется здесь. Здесь же, для того чтобы не усложнять учебный проект, используется императивный подход.

▍Динамический импорт ресурсов


Помните о том, что мы, говоря о паттерне PRPL, ещё не разобрались с той его частью, которая представлена буквой L (Lazy loading)? Динамический импорт ресурсов — это то, что поможет нам организовать ленивую загрузку компонентов или модулей. Так как мы совместно используем архитектуру App Shell и паттерн PRPL для кэширования «скелета» приложения и его ресурсов, в ходе динамического импорта производится загрузка ресурсов из кэша, а не из сети.

Обратите внимание на то, что если бы мы использовали лишь архитектуру App Shell, то оставшиеся ресурсы приложения, то есть, содержимое папки chunks, не были бы кэшированы.

Пример динамического импорта ресурсов можно увидеть в вышеприведённом фрагменте кода компонента AppComponent, в частности, там, где настраивается обработчик события щелчка по кнопке (речь идёт о методе объекта initEvents()). А именно, если пользователь приложения щёлкнет по кнопке btn-todo, будет загружен модуль todo.module. Этот модуль представляет собой обычный JavaScript-файл, который содержит набор компонентов, представленных в виде объектов.

▍Стрелочные функции и шаблонные литералы


Стрелочные функции особенно полезны в тех случаях, когда нужно, чтобы ключевое слово this в таких функциях указывало бы на контекст, в котором объявлена функция. Помимо этого стрелочные функции позволяют писать код, который оказывается компактнее, чем при использовании обычных функций. Вот пример такой функции:

export const appTemplate = model => `
    <section class="app">
        <h3><font color="#3AC1EF">▍ ${model.title} </font></h3>
        <section class="button">
            <button class="btn-todo"> Todo Module </button>
        </section>
    </section>
`;

Стрелочная функция appTemplate принимает модель (параметр model) и возвращает HTML-строку, содержащую данные, взятые из модели. Формирование строки выполняется с использованием технологии шаблонных литералов. Ими удобно пользоваться для представления многострочных конструкций, в которые нужно добавлять какие-либо данные.

Вот небольшой совет, касающийся шаблонизации компонентов и создания компонентов, которые подходят для повторного использования. Он заключается в использовании метода массива reduce() для сборки HTML-строк:

const WorkModel = [
    {
        id: 1,
        src: '',
        alt: '',
        designation: '',
        period: '',
        description: ''
    },
    {
        id: 2,
        src: '',
        alt: '',
        designation: '',
        period: '',
        description: ''
    },
    //...
];

const workCardTemplate = (cardModel) => `
<section id="${cardModel.id}" class="work-card">
    <section class="work__image">
        <img class="work__image-content" type="image/svg+xml" src="${
            cardModel.src
        }" alt="${cardModel.alt}" />
    </section>
    <section class="work__designation">${cardModel.designation}</section>
    <section class="work__period">${cardModel.period}</section>
    <section class="work__content">
        <section class="work__content-text">
            ${cardModel.description}
        </section>
    </section>
</section>
`;

export const workTemplate = (model) => `
<section class="work__section">
    <section class="work-text">
        <header class="header-text">
            <span class="work-text__header"> Work </span>
        </header>
        <section class="work-text__content content-text">
            <p class="work-text__content-para">
                This area signifies work experience
            </p>
        </section>
    </section>

    <section class="work-cards">
        ${model.reduce((html, card) => html + workCardTemplate(card), '')}
    </section>

</section>
`;

На самом деле, в этом фрагменте кода выполняется серьёзный объём работы. При этом он устроен просто и понятен на интуитивном уровне. Надо отметить, что здесь используются те же приёмы, которые характерны для современных фреймворков.

Проанализируем его:

  • В начале файла находится команда объявления массива model. Это — пример массива с данными модели, который можно обработать с помощью метода reduce() для того чтобы реализовать шаблон, подходящий для повторного использования.
  • В конце файла можно найти команду model.reduce, которая помогает формировать HTML-строку, содержащую множество компонентов, подходящих для повторного использования. Эта функция принимает, в качестве первого аргумента, аккумулятор, а в качестве второго — значения массива.

Благодаря всем рассмотренным выше простым возможностям у нас уже есть структура приложения. Теперь соберём вместе всё то, о чём мы говорили, так как лучший способ изучения технологий — это их практическое использование.

Демонстрационное приложение


Рассмотрим демонстрационное Todo-приложение, которое создано с использованием рассмотренных технологий. Вот как выглядит работа с ним.


Демонстрационное приложение

Поэкспериментировать с ним самостоятельно можно здесь. Вот репозиторий с кодом приложения. Можете его клонировать и, чтобы лучше разобраться в том, что мы обсуждали, как следует проанализировать его код.

Пример продакшн-приложения


Продакшн-приложение, о котором идёт речь, представляет собой сайт-портфолио, созданный с нуля с использованием разобранных здесь технологий. Одностраничное приложение, которым является этот проект, собрано из модулей и компонентов. Те возможности и та гибкость, которые доступны разработчику при использовании чистого JavaScript и базовых веб-технологий, позволяют добиваться замечательных результатов.

Вот как выглядит работа с сайтом, о котором идёт речь.


Сайт-портфолио

Вот ссылка на сайт. А вот результаты анализа производительности этого сайта с помощью Lighthouse.


Анализ производительности сайта

Итоги


Некоторые проекты, ради быстрого достижения желаемого результата, имеет смысл создавать на чистом JavaScript, без использования фреймворков. Такой подход открывает перед программистом свободу самовыражения, позволяя ему создавать именно то, что ему нужно, не ограничивая себя возможностями и особенностями фреймворков.

Уважаемые читатели! Планируете ли вы использовать подход к разработке веб-приложений, предложенный в этом материале?

Tags:
Hubs:
+27
Comments 9
Comments Comments 9

Articles

Information

Website
ruvds.com
Registered
Founded
Employees
11–30 employees
Location
Россия
Representative
ruvds