<!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>
<!-- â„–1 -->
).
main
tag with the app
identifier ( <main id="app"></main>
).async
attribute allows not to block the parser while loading scripts. 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); }); }
install
event helps to cache static resources. Here you can put in the cache the resources of the “skeleton” of the application (CSS, JavaScript, images, and so on) for the first route (in accordance with the content of the “skeleton”). In addition, you can download other application resources by making it work without an internet connection. Resource caching, in addition to skeleton caching, corresponds to the Pre-cache step of the PRPL pattern.activate
event, unused caches are cleared.@babel/core
@babel/plugin-syntax-dynamic-import
@babel/preset-env
babel-core
babel-loader
babel-preset-env
.babelrc
file designed for use with a Webpack:
{ "presets": ["@babel/preset-env"], "plugins": ["@babel/plugin-syntax-dynamic-import"] }
presets
string of this file is used to tune Babel for ES6 to ES5 transpiling, and the plugins
string is used to enable dynamic import in the Webpack.
webpack.config.js
):
module.exports = { entry: { // }, output: { // }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader' } } ] }, plugins: [ // ] };
rules
section of this file describes the use of the babel-loader
to customize the transpiling process. For the sake of brevity, the remaining parts of this file are omitted.
sass-loader
css-loader
style-loader
MiniCssExtractPlugin
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
section. Since we use the plugin to extract CSS styles, the corresponding entry is made in the plugins
section.
clean-webpack-plugin
: to clean the contents of the dist
folder.compression-webpack-plugin
: to compress the contents of the dist
folder.copy-webpack-plugin
: to copy static resources, for example, files, from folders with application source data to the dist
folder.html-webpack-plugin
: to create an index.html
file in the dist
folder.webpack-md5-hash
: for hashing application files in the dist
folder.webpack-dev-server
: to run a local server used during development.webpack.config.js
file looks like:
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 } });
argv
argument, which represents the arguments passed to this function when executing webpack
or webpack-dev-server
commands. Here is how the description of these commands looks in the package.json
project file:
"scripts": { "build": "webpack --mode production && node build-sw", "serve": "webpack-dev-server --mode=development --hot", },
npm run build
, the production version of the application will be built. If you run the npm run serve
command, the development server will be launched that supports the process of working on the application.
plugins
and devServer
the file above show the configuration of plug-ins and the development server.
new CopyWebpackPlugin
, specify the resources that need to be copied from the source materials of the application.
dist
folder and adds them as cache contents in the service worker template. After that, the service worker file will be recorded in the dist
folder. The concepts that we talked about for service workers do not change. Here is the build-sw.js
script code:
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
dist
folder is placed in the array staticAssetsCacheFiles
.dist
folder to it, which can change over time. To do this, use the stringFileCachesArray
constant.serviceWorkerScript
constant is written to the file located at dist/sw.js
node build-sw
command. It needs to be executed after the execution of the webpack --mode production
command is completed.
package.json
our project, where you can find information about the packages used in this project:
{ "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" } }
app.js
file:
import { appTemplate } from './app.template'; import { AppModel } from './app.model'; export const AppComponent = { // App... };
app.js
file in which you can see their use.
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
and export the AppComponent
component, which can immediately be used in other parts of the application.
chunks
folder, would not be cached.
AppComponent
component, in particular, where the event handler for clicking a button is configured (this is a method of the object initEvents()
). Namely, if the application user clicks on the btn-todo
button, the btn-todo
module will be loaded. This module is a regular JavaScript file that contains a set of components represented as objects.
this
in such functions to indicate the context in which the function is declared. In addition, the arrow functions allow you to write code that is more compact than when using ordinary functions. Here is an example of such a function:
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-, , . , , , — .Source: https://habr.com/ru/post/444342/