28 maj 2019

Dynamiczne ładowanie plików JavaScript

Większość użytkowników ma określony cel przeglądając witrynę. Na przykład jeden użytkownik może chcieć przeglądać katalog produktów tylko po to, aby zobaczyć, co masz w sklepie. Nie jest mu potrzebny kod js w zakładce "pokaż recenzje" bo tam po prostu nie trafi. Więc serwowanie kodu z całej witryny mija się z celem.

Jeżeli chcesz tworzyć szybkie witryny, musisz przesłać jak najmniej JavaScript do przeglądarek. Pobieranie pakietu JavaScript wymaga nie tylko czasu, ale przeglądarka musi także wyodrębnić kod i przeanalizować go, co również zajmuje dużo czasu.

Powolne działanie witryny sprawia, że użytkownicy odchodzą, optymalny czas ładowania strony to maksymalnie 2-3s. Takie podejście serwowania kodu w małych paczkach jest również dobre dla SEO - Google bardzo lubi szybkie strony ;).

To co powinieneś zrobić, to wysłać tyko JavaScript, którego użytkownik potrzebuje do przeglądania i interakcji z odwiedzanymi stronami. W ten sposób witryna ładuje się znacznie szybciej.

Takie ładowanie możemy przygotować za pomocą webpacka i dynamicznego ładowania.

Gdy korzystasz z importu dynamicznego, wstawiasz minimalną ilość javascirpt na stronie i ładujesz resztę dynamicznie, gdy i jeśli użytkownik tego potrzebuje.

Nasz przykład będzie bardzo prosty, na środku strony będzie znajdował się przycisk po kliknięciu którego zmienimy kolor tła strony. Kliknięcie spowoduje załadowanie pliku js który to właśnie zmieni nam to tło.

Jak skonfigurować dynamiczny import pakietów internetowych w webpack

Zacznijmy od webpacka. Mam nadzieję że wiesz jak instalować pakiety npm?. Masz zainstalowane node?. Jeśli nie, no cóż, może kiedyś powstanie jakiś artykuł. Na obecną chwilę polecam artykuł.

Inicjalizacja projektu. Ja używam yarn, oczywiście nic nie stoi na przeszkodzie użycie npm.

yarn init -y

Powstanie nam plik package.json
Teraz musimy zainstalować wszystkie zależności, najpierw devDependencies

yarn add -D @babel/core @babel/preset-env @babel/plugin-syntax-dynamic-import babel-loader html-webpack-plugin webpack webpack-cli webpack-dev-server

Oraz dependencies

yarn add core-js@3.1.0 regenerator-runtime

Struktura głównego folderu

Teraz przyszedł czas na przygotowanie webpacka. Tworzymy webpack.config.js w głównym folderze.

my-webpack-app
├──sources
├──├──js
├──├──├──background.js
├──├──├──index.js
├──├──index.html
├──.babelrc
├──package.json
└──webpack.config.js

Folder source składa się z foldera js w którym jest background.js oraz index.js a także plik index.html
A tak wygląda webpack.config.js:

const path = require('path');
const HtmlWebPackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = (env, argv) => ({
  // source mapa dla wersji produkcyjnej lub deweloperskiej
  devtool: argv.mode === 'production' ? 'none' : 'source-map',
  mode: argv.mode === 'production' ? 'production' : 'development',
  // nasz główny plik z js
  entry: {
    main: './sources/js/index.js',
  },
  // wyjściowa nazwa pliku
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: './[name].[hash].js',
  },
  // babel-loader konwertujący ES6 nad ES5 to tego służy nam  plik .babelrc
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        },
      },
    ],
  },
  plugins: [
    // plugin czyszczący folder dist
    new CleanWebpackPlugin({
      verbose: true,
    }),
    // plugin który dodaje js do index.html
    new HtmlWebPackPlugin({
      filename: 'index.html',
      template: './sources/index.html',
    }),
  ],
});

Plik index.js

import 'core-js/modules/es.array.iterator'; // potrzebne dla IE11

const button = document.getElementById('background');

button.addEventListener('click', async (event) => {
  event.preventDefault();
  try {
    // zmienna pobierająca z data-module nazwę pliku js
    const background = await import(/* webpackChunkName: "background" */ `./${button.dataset.module}.js`);
    // uruchomienie background.js
    background.default();
  } catch (error) {
    console.log(error);
  }
})

background.js

export default function test() {
  document.body.classList.toggle('bodyColor');
}

index.html

<!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">
<title>Lazy Loading</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css">
<style>
  * {
    padding: 0;
    margin: 0;
  }
  body {
    background: #020024;
    transition: background 0.2s ease-in;
  }
  .bodyColor {
    background: #833ab4;
  }
  .bodyColor button {
    box-shadow: 0px 0px 50px 0px rgba(255, 0, 0, 0.75);
  }
  .container {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    text-align: center;
  }
</style>
</head>
<body>
  <div class="container">
    <button id="background" class="button-primary" data-module="background">
       change the background
    </button>
  </div>
</body>
</html>

W index.html najważniejszą częścią jest data-module

Po kliknięciu w button javascript pobiera value z data-module czyli "background".
Nazwa ta jest częścią nazwy pliku javascript (background.js). I ładuje ją dynamicznie do strony oraz wywołuje zmieniając kolor tła strony.

Do tego również jest potrzebny plik .babelrc. Tak zgadza się jest to plik z kropką. Babel jest to biblioteka za pomocą której konwertujemy nasz kod na zrozumiały dla starszych przeglądarek.

{
  "plugins": [
    "@babel/plugin-syntax-dynamic-import"
  ],
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "browsers": "last 2 versions"
        },
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ]
}

@babel/plugin-syntax-dynamic-import plugin do obsługi import
Później mamy @babel/preset-env a w nim ustawiamy targets, a dokładnie ustawiamy żeby nasz kod był zgodny z 2 ostatnimi wersjami przeglądarek.
useBuiltIns parametr ustawiony na usage zaciąga nam wszystkie zależności. W związku z tym że używamy wielu nowych rzeczy związanych z nowym js jak np. asynk/await, modules itd.
I ostatni parametr corejs w wersji 3. Jest to modularna biblioteka javascript. Składająca się z polyfills dla ECMAScript 2019r .: promises, symbols, collections, iterators i wiele innych funkcji.

Żeby to wszystko uruchomić musimy dodać jeszcze jedną rzecz do package.js

"scripts": {
  "dev": "webpack-dev-server --config webpack.config.js --mode development --open",
  "prod": "webpack --config webpack.config.js --mode production"
},

Za pomocą tego możemy uruchomić naszą stronę, wystarczy w linii komend wpisać.

yarn dev
// lub jeżeli chcemy wygenerować nasze pliki
yarn prod

Po uruchomieniu powinna otworzyć się strona http://localhost:8080/
Uruchom DevTools (F12) w Chromie. Przejdź do zakładki Network
Odśwież stronę przez F5 zobacz że ładuje się jeden plik js o nazwie main.tutaj-hash.js.

Teraz kliknij w button, zmienia się background, ale najważniejsza rzecz to to, że doszedł nam następny plik js background0.tutaj-hash.js
W skrócie kliknięcie w button pobrało nazwę naszego modułu z data-module. Następnie asynchronicznie pobrano plik background.js oraz go wywołano. Plik ten dodał nam klasę "bodyColor" do body zmieniając nam kolor tła.

A tutaj całe repozytorium, oraz działający przykład

Zerknij do tych źródeł tutaj i tutaj

Powyższy przykład działa we wszystkich najnowszych przeglądarkach oraz IE od wersji 11+