
<!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-envbabel-corebabel-loaderbabel-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-loadercss-loaderstyle-loaderMiniCssExtractPlugin 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.jsnode 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/
All Articles