使用Webpack和高级Web技术开发简单,现代的JavaScript应用程序

在开发下一个Web项目时,您是否考虑过使用最简单的现有技术集? 如果是这样,那么我们今天出版的材料就是专门为您准备的。

存在JavaScript框架来帮助我们使用通用方法来构建具有类似功能的应用程序。 但是,许多应用程序并不需要框架提供的所有功能。 在具有某些特定要求的中小型项目中使用框架很可能是不必要的时间和精力浪费。

图片

在本文中,我们将讨论在Web应用程序开发中使用现代技术的情况,这些应用程序的功能不受框架功能的限制。 顺便说一句,如果您需要它,那么使用此处描述的技术,您可以创建自己的高度专业化的框架。 纯JavaScript和其他基本的Web技术使开发人员能够执行所需的工作,而不会局限于使用的工具范围。

复习


在开始做生意之前,让我们讨论一下我们需要的工具。

▍应用架构


为了确保应用程序的高速加载和可用性,我们将使用以下设计模式:

  • 架构应用程序外壳。
  • PRPL模式(推送,渲染,预缓存,延迟加载)。

build项目建设系统


在我们的项目中,我们需要根据需要定制的高质量装配系统。 在这里,我们将使用Webpack,为项目构建系统提出以下要求:

  • 支持ES6和动态资源导入功能。
  • 支持SASS和CSS。
  • 开发模式和应用程序的实际工作分开配置。
  • 能够自动配置服务工作者。

JavaScript高级JavaScript功能


我们将使用最少的现代JavaScript功能集,使我们能够开发所需的东西。 以下是有问题的功能:

  • 模组
  • 创建对象的不同方式(对象文字,类)。
  • 动态导入资源。
  • 箭头功能。
  • 模板文字。

既然我们对需要的东西有了大致的了解,我们就可以开始开发我们的项目了。

应用架构


渐进式Web应用程序(PWA)的出现推动了Web开发中新体系结构解决方案的出现。 这允许Web应用程序更快地加载和显示。 App Shell架构和PRPL模式的结合可以使Web应用程序快速且响应迅速,类似于常规应用程序。

App什么是App Shell和PRPL?


App Shell是一种用于开发PWA的体系结构模式,当使用PWA时,在网站加载时,对网站操作至关重要的最少资源会发送到用户的浏览器。 这些材料的组成通常包括首次显示应用程序所需的所有资源。 此类资源也可以使用服务工作者进行缓存。

缩写PRPL的解释如下:

  • 推送-向源路由的客户端发送关键资源(特别是使用HTTP / 2)。
  • 渲染-显示原始路线。
  • 预缓存-预先缓存其余路由或资源。
  • 延迟加载-必要时“延迟”加载应用程序的某些部分(尤其是应用户要求)。

▍用代码实现App Shell和PRPL


App Shepp和PRPL模式是共享的。 这使您可以实施高级方法来开发Web项目。 这是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号片段 最重要的样式内置在标记中,没有作为单独的文件显示。 这样做是为了在加载HTML页面时直接处理CSS代码。
  • 2号片段 这是应用程序的“外壳”。 这些区域稍后将由JavaScript代码控制。 对于标识符为app<main id="app"></main> )的main标记中的内容尤其如此。
  • 3号片段。 脚本在这里发挥作用。 async属性允许您在脚本加载期间不阻止解析器。

应用程序的上述“框架”实现了PRPL模式的“推送”和“渲染”步骤。 当浏览器解析HTML代码以形成页面的可视表示形式时,就会发生这种情况。 同时,浏览器可以快速找到对于页面输出至关重要的资源。 此外,此处还提供了脚本(片段3),这些脚本负责通过操纵DOM(在“渲染”步骤)来显示原始路线。

但是,如果我们不使用服务工作程序来缓存应用程序的“外壳”,那么例如在重新加载页面时,我们将不会获得性能提升。

下面的代码显示了一个服务工作者,它在缓存应用程序的框架和所有静态资源。

 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,图像等)。 另外,您可以下载其他应用程序资源,以使其在没有Internet连接的情况下也可以工作。 除骨架缓存外,资源缓存还对应于PRPL模式的“预缓存”步骤。
  • 2号片段 处理activate事件activate未使用的缓存。
  • 3号片段。 这些代码行从缓存中加载资源(如果有的话)。 否则,将发出网络请求。 另外,如果发出网络请求以接收资源,则意味着该资源尚未被缓存。 这样的资源被放置在新的单独的缓存中。 该脚本有助于缓存动态应用程序数据。

迄今为止,我们已经讨论了将在我们的应用程序中使用的大多数体系结构解决方案。 我们还没有谈论的唯一事情是PRPL模式的延迟加载步骤。 稍后我们将返回它,但是现在我们将处理项目组装系统。

项目建设系统


没有良好的项目构建系统,仅凭良好的体系结构还不足以创建高质量的应用程序。 这是Webpack派上用场的地方。 还有其他用于构建项目的工具(捆绑销售商),例如-包裹和汇总。 但是我们也可以使用其他方法来完成基于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

这是与Webpack一起使用的示例.babelrc文件:

 {   "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部分中输入了相应的条目。

of分别设置开发模式和应用程序的实际工作


这是应用程序构建过程中极为重要的部分。 众所周知,在创建应用程序时,某些设置用于构建开发期间使用的版本,而其他设置用于其生产版本。 这是在这里派上用场的软件包列表:

  • clean-webpack-plugin :清除dist文件夹的内容。
  • compression-webpack-plugin :压缩dist文件夹的内容。
  • copy-webpack-plugin :用于将静态资源(例如文件)从具有应用程序源数据的文件夹复制到dist文件夹。
  • html-webpack-plugin :在dist文件夹中创建index.html文件。
  • webpack-md5-hash :用于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参数,该参数表示执行webpackwebpack-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命令,则开发服务器将启动,支持在应用程序上工作的过程。

上面文件的pluginsdevServer显示了如何配置插件和开发服务器。

在以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命令webpack --mode production后执行它。

此处提供的用于构建服务工作者的脚本大大简化了组织文件缓存的任务。 应该注意的是,该脚本已经在实际项目中找到了应用。

如果要使用专门设计的库来解决离线使用渐进式Web应用程序的问题,请查看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现代功能


在开发Web应用程序期间,程序员可以选择-是编写自己的变更检测,路由,数据存储等功能的实现,还是使用现有的程序包。

现在,我们将讨论确保项目正常运行所需的最少技术。 如有必要,可以使用现有框架或软件包扩展这套技术。

▍模块


我们将使用ES6的功能来导入和导出模块,并将每个文件视为ES6模块。 此功能通常在流行的框架(例如Angular和React)中找到,使用起来非常方便。 由于有了Webpack的配置,我们可以使用导入和导出资源的表达式。 这是app.js文件中的外观:

 import { appTemplate } from './app.template'; import { AppModel } from './app.model'; export const AppComponent = { //   App... }; 

create各种创建对象的方法


组件创建是我们应用程序开发过程的重要组成部分。 在这里,有可能使用一些现代工具,例如网络组件,但为了不使项目复杂化,我们将使用普通的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类或Web组件,以一种比此处使用的声明式更接近声明式的方式开发项目。 在这里,为了不使培训项目复杂化,使用了命令式方法。

▍动态导入资源


记住,谈到PRPL模式,我们还没有弄清楚字母L(延迟加载)所代表的部分吗? 动态导入资源可以帮助我们组织组件或模块的延迟加载。 由于我们使用App Shell体系结构和PRPL模式来缓存应用程序及其资源的“骨架”,因此动态导入过程从缓存而不是从网络加载资源。

请注意,如果仅使用App Shell架构,则不会缓存其余的应用程序资源,即chunks文件夹的内容。

在上面的AppComponent组件的代码片段中可以看到动态资源导入的示例,尤其是在配置按钮的click事件的位置(我们正在谈论initEvents()对象的方法)。 即,如果应用程序的用户单击btn-todo按钮,则将加载btn-todo模块。 该模块是一个常规的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函数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, . , , , .

亲爱的读者们! -, ?

Source: https://habr.com/ru/post/zh-CN444342/


All Articles