Дайте Grunt генерувати index.html для різних налаштувань


208

Я намагаюся використовувати Grunt як інструмент побудови мого веб-сайту.

Я хочу мати принаймні дві установки:

I. Налаштування розробки - завантажуйте сценарії з окремих файлів, без конкатенації,

тому мій index.html буде виглядати приблизно так:

<!DOCTYPE html>
<html>
    <head>
        <script src="js/module1.js" />
        <script src="js/module2.js" />
        <script src="js/module3.js" />
        ...
    </head>
    <body></body>
</html>

II. Налаштування виробництва - завантажуйте мої сценарії, мінімізовані та об'єднані в один файл,

відповідно index.html:

<!DOCTYPE html>
<html>
    <head>
        <script src="js/MyApp-all.min.js" />
    </head>
    <body></body>
</html>

Питання в тому, як я можу змусити грунт зробити ці index.html залежно від конфігурації під час запуску grunt devчи grunt prod?

Чи, можливо, я копаю в неправильному напрямку, і було б легше завжди генерувати, MyApp-all.min.jsале всередину цього помістити або всі мої сценарії (з’єднані), або сценарій завантажувача, який асинхронно завантажує ці сценарії з окремих файлів?

Як ви це робите, хлопці?


3
Спробуйте інструмент Yeoman, який включає в себе завдання 'usemin', яке робить те, що ви. Крім того, генератори Yeamon включають в себе безліч «добрих практик», які легко засвоїти, які важко засвоїти при використанні нового інструменту.
EricSonaron

Відповіді:


161

Нещодавно я виявив ці v0.4.0сумісні завдання Grunt :

Нижче наведено фрагменти з мого Gruntfile.js.

Налаштування ENV:

env : {

    options : {

        /* Shared Options Hash */
        //globalOption : 'foo'

    },

    dev: {

        NODE_ENV : 'DEVELOPMENT'

    },

    prod : {

        NODE_ENV : 'PRODUCTION'

    }

},

Попередній процес:

preprocess : {

    dev : {

        src : './src/tmpl/index.html',
        dest : './dev/index.html'

    },

    prod : {

        src : './src/tmpl/index.html',
        dest : '../<%= pkg.version %>/<%= now %>/<%= ver %>/index.html',
        options : {

            context : {
                name : '<%= pkg.name %>',
                version : '<%= pkg.version %>',
                now : '<%= now %>',
                ver : '<%= ver %>'
            }

        }

    }

}

Завдання:

grunt.registerTask('default', ['jshint']);

grunt.registerTask('dev', ['jshint', 'env:dev', 'clean:dev', 'preprocess:dev']);

grunt.registerTask('prod', ['jshint', 'env:prod', 'clean:prod', 'uglify:prod', 'cssmin:prod', 'copy:prod', 'preprocess:prod']);

І у /src/tmpl/index.htmlфайлі шаблону (наприклад):

<!-- @if NODE_ENV == 'DEVELOPMENT' -->

    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.js"></script>
    <script src="../src/js/foo1.js"></script>
    <script src="../src/js/foo2.js"></script>
    <script src="../src/js/jquery.blah.js"></script>
    <script src="../src/js/jquery.billy.js"></script>
    <script src="../src/js/jquery.jenkins.js"></script>

<!-- @endif -->

<!-- @if NODE_ENV == 'PRODUCTION' -->

    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>

    <script src="http://cdn.foo.com/<!-- @echo name -->/<!-- @echo version -->/<!-- @echo now -->/<!-- @echo ver -->/js/<!-- @echo name -->.min.js"></script>

<!-- @endif -->

Я впевнений, що моя установка відрізняється від більшості людей, і корисність вищезазначеного буде залежати від вашої ситуації. Для мене, хоча це дивовижний шматочок коду, Yeoman grunt-usemin є більш надійним, ніж мені особисто потрібно.

ПРИМІТКА. Я щойно відкрив перераховані вище завдання сьогодні, тому у мене може бути відсутність функції та / або мій процес може змінитися вниз. Поки що я люблю простоту і можливості, які можуть запропонувати grunt-precesscess і grunt-env . :)


Оновлення січня 2014 року:

Мотивоване голосуванням проти

Коли я опублікував цю відповідь, для Грунта не було багато варіантів, 0.4.xякі пропонували рішення, що працювало на мої потреби. Тепер, через кілька місяців, я б здогадався, що там є більше варіантів, які можуть бути кращими, ніж те, що я розмістив тут. Хоча я все ще особисто використовую та насолоджуюсь використанням цієї техніки для моїх побудов , я прошу майбутніх читачів витратити час, щоб прочитати інші наведені відповіді та вивчити всі варіанти. Якщо ви знайдете краще рішення, будь ласка, опублікуйте свою відповідь тут.

Оновлення лютого 2014 року:

Я не впевнений, чи допоможе комусь допомогти, але я створив цей демо-репозиторій на GitHub, який показує повну (і більш складну настройку), використовуючи методику, яку я описав вище.


Дякую, я перевірю це!
Дмитро Пашкевич

3
Ваше рішення врятувало мене години удару головою об стіну. Дякую.
sthomps

1
@sthomps Радий, що це допомогло! З тих пір, як я виявив ці завдання, я любив робочий процес. FYI, я зробив одну невелику зміну процесу ... Замість того, щоб передати кілька змінних контексту до моїх HTML-шаблонів, я вирішив передати один var, path : '/<%= pkg.name %>/dist/<%= pkg.version %>/<%= now %>/<%= ver %>'який містить усі варіанти (це мій шлях збірки). На моєму шаблоні я буду мати: <script src="http://cdn.foo.com<!-- @echo path -->/js/bulldog.min.js"></script>. У всякому разі, я щаслива, що мені вдалося заощадити трохи часу! : D
mhulse

4
Ви можете зробити те ж саме, використовуючи лише grunt-template , просто перейшовши в інший dataоб'єкт для dev / prod.
Mathias Bynens

2
Людина, яку я люблю це рішення. Він чистий, читабельний і не надто спроектований.
Gaurang Patel

34

Я придумав власне рішення. Ще не відшліфований, але я думаю, що я рухатимусь у цьому напрямку.

По суті, я використовую grunt.template.process (), щоб генерувати index.htmlшаблон із шаблону, який аналізує поточну конфігурацію та створює список моїх вихідних вихідних файлів або посилання на один файл із мінімізованим кодом. Наведений нижче приклад - для js-файлів, але той самий підхід можна поширити на css та будь-які інші текстові файли.

grunt.js:

/*global module:false*/
module.exports = function(grunt) {
    var   // js files
        jsFiles = [
              'src/module1.js',
              'src/module2.js',
              'src/module3.js',
              'src/awesome.js'
            ];

    // Import custom tasks (see index task below)
    grunt.loadTasks( "build/tasks" );

    // Project configuration.
    grunt.initConfig({
      pkg: '<json:package.json>',
      meta: {
        banner: '/*! <%= pkg.name %> - v<%= pkg.version %> - ' +
          '<%= grunt.template.today("yyyy-mm-dd") %> */'
      },

      jsFiles: jsFiles,

      // file name for concatenated js
      concatJsFile: '<%= pkg.name %>-all.js',

      // file name for concatenated & minified js
      concatJsMinFile: '<%= pkg.name %>-all.min.js',

      concat: {
        dist: {
            src: ['<banner:meta.banner>'].concat(jsFiles),
            dest: 'dist/<%= concatJsFile %>'
        }
      },
      min: {
        dist: {
        src: ['<banner:meta.banner>', '<config:concat.dist.dest>'],
        dest: 'dist/<%= concatJsMinFile %>'
        }
      },
      lint: {
        files: ['grunt.js'].concat(jsFiles)
      },
      // options for index.html builder task
      index: {
        src: 'index.tmpl',  // source template file
        dest: 'index.html'  // destination file (usually index.html)
      }
    });


    // Development setup
    grunt.registerTask('dev', 'Development build', function() {
        // set some global flags that all tasks can access
        grunt.config('isDebug', true);
        grunt.config('isConcat', false);
        grunt.config('isMin', false);

        // run tasks
        grunt.task.run('lint index');
    });

    // Production setup
    grunt.registerTask('prod', 'Production build', function() {
        // set some global flags that all tasks can access
        grunt.config('isDebug', false);
        grunt.config('isConcat', true);
        grunt.config('isMin', true);

        // run tasks
        grunt.task.run('lint concat min index');
    });

    // Default task
    grunt.registerTask('default', 'dev');
};

index.js (the index task):

module.exports = function( grunt ) {
    grunt.registerTask( "index", "Generate index.html depending on configuration", function() {
        var conf = grunt.config('index'),
            tmpl = grunt.file.read(conf.src);

        grunt.file.write(conf.dest, grunt.template.process(tmpl));

        grunt.log.writeln('Generated \'' + conf.dest + '\' from \'' + conf.src + '\'');
    });
}

Нарешті, index.tmplз поколінням логіки випечений:

<doctype html>
<head>
<%
    var jsFiles = grunt.config('jsFiles'),
        isConcat = grunt.config('isConcat');

    if(isConcat) {
        print('<script type="text/javascript" src="' + grunt.config('concat.dist.dest') + '"></script>\n');
    } else {
        for(var i = 0, len = jsFiles.length; i < len; i++) {
            print('<script type="text/javascript" src="' + jsFiles[i] + '"></script>\n');
        }
    }
%>
</head>
<html>
</html>

UPD. З'ясувалося, що Yeoman , який базується на грунті, має вбудоване завдання usemin, яке інтегрується в систему побудови Yeoman. Він генерує виробничу версію index.html з інформації у версії розробки index.html, а також інших налаштувань середовища. Трохи витончений, але цікавий для погляду.


5
grunt-template - це дуже легка обгортка навколоgrunt.template.process()(для чого ви тут користуєтесь), що зробить це ще простіше. Ви можете зробити те ж саме, використовуючи grunt-template , просто перейшовши в іншийdataоб’єкт для dev / prod.
Mathias Bynens

15

Мені тут не подобаються рішення (включаючи те, що я раніше давав ), і ось чому:

  • Проблема з найбільш високою оцінкою відповіді полягає в тому, що вам потрібно вручну синхронізувати список тегів сценаріїв при додаванні / перейменуванні / видаленні файлу JS.
  • Проблема з прийнятою відповіддю полягає в тому, що ваш список файлів JS не може відповідати шаблону. Це означає, що вам доведеться оновити його вручну в Gruntfile.

Я зрозумів, як вирішити обидва ці питання. Я налаштував своє завдання на грунт, щоб кожен раз, коли файл доданий чи видалявся, теги сценарію автоматично створюються для відображення цього. Таким чином, вам не потрібно змінювати ваш html-файл або грунт-файл під час додавання / видалення / перейменування файлів JS.

Підсумовуючи, як це працює, у мене є HTML-шаблон із змінною для тегів скрипту. Я використовую https://github.com/alanshaw/grunt-include-replace, щоб заповнити цю змінну. У режимі розроблення ця змінна походить від шаблону глобалізації всіх моїх файлів JS. Завдання перегляду перераховує це значення, коли додається або видаляється файл JS.

Тепер, щоб отримати різні результати в режимі dev або prod, ви просто заповнюєте цю змінну з іншим значенням. Ось код:

var jsSrcFileArray = [
    'src/main/scripts/app/js/Constants.js',
    'src/main/scripts/app/js/Random.js',
    'src/main/scripts/app/js/Vector.js',
    'src/main/scripts/app/js/scripts.js',
    'src/main/scripts/app/js/StatsData.js',
    'src/main/scripts/app/js/Dialog.js',
    'src/main/scripts/app/**/*.js',
    '!src/main/scripts/app/js/AuditingReport.js'
];

var jsScriptTags = function (srcPattern, destPath) {
    if (srcPattern === undefined) {
        throw new Error("srcPattern undefined");
    }
    if (destPath === undefined) {
        throw new Error("destPath undefined");
    }
    return grunt.util._.reduce(
        grunt.file.expandMapping(srcPattern, destPath, {
            filter: 'isFile',
            flatten: true,
            expand: true,
            cwd: '.'
        }),
        function (sum, file) {
            return sum + '\n<script src="' + file.dest + '" type="text/javascript"></script>';
        },
        ''
    );
};

...

grunt.initConfig({

    includereplace: {
        dev: {
            options: {
                globals: {
                    scriptsTags: '<%= jsScriptTags(jsSrcFileArray, "../../main/scripts/app/js")%>'
                }
            },
            src: [
                'src/**/html-template.html'
            ],
            dest: 'src/main/generated/',
            flatten: true,
            cwd: '.',
            expand: true
        },
        prod: {
            options: {
                globals: {
                    scriptsTags: '<script src="app.min.js" type="text/javascript"></script>'
                }
            },
            src: [
                'src/**/html-template.html'
            ],
            dest: 'src/main/generatedprod/',
            flatten: true,
            cwd: '.',
            expand: true
        }

...

    jsScriptTags: jsScriptTags

jsSrcFileArrayє вашим типовим шахрайським файлом. jsScriptTagsприймає jsSrcFileArrayі поєднує їх разом з scriptтегами з обох сторін. destPath- це префікс, який я хочу отримати для кожного файлу.

А ось як виглядає HTML:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <title>Example</title>

</head>

<body>    
@@scriptsTags
</body>
</html>

Тепер, як ви бачите в конфігурації, я генерую значення цієї змінної у вигляді жорстко кодованого scriptтегу, коли він працює в prodрежимі. У режимі розробки ця змінна розшириться до такого значення:

<script src="../../main/scripts/app/js/Constants.js" type="text/javascript"></script>
<script src="../../main/scripts/app/js/Random.js" type="text/javascript"></script>
<script src="../../main/scripts/app/js/Vector.js" type="text/javascript"></script>
<script src="../../main/scripts/app/js/StatsData.js" type="text/javascript"></script>
<script src="../../main/scripts/app/js/Dialog.js" type="text/javascript"></script>

Повідомте мене, якщо у вас є питання.

PS: Це шалена кількість коду для того, що я хотів би зробити у кожному додатку JS на стороні клієнта. Я сподіваюся, що хтось може перетворити це на плагін для багаторазового використання. Можливо, я буду колись.


1
Звучить багатообіцяюче. Будь-який шанс ви могли поділитися деякими фрагментами?
Адам Маршалл

I've set up my grunt task so that every time a file is added or deleted, the script tags automatically get generated to reflect thatЯк ти це зробив?
CodyBugstein

2
Інше питання: чи знаєте ви спосіб просто видалити блоки <script>тегів HTML ?
CodyBugstein

@Imray не з верхньої частини голови. Ви маєте на увазі без будь-якої форми шаблонування (наприклад, grunt-include-substitute)? Перша думка, що спливає мені в голову, буде xslt. Напевно, не дуже вдале рішення.
Даніель Каплан

1
Ця відповідь пляма на, хоча я особисто видалений destPathз jsScriptTagsі помінявся grunt.file.expandMappingз grunt.file.expandяк файлами , які я хотів вже були в правильних місцях. Це дуже спростило речі. Дякую @DanielKaplan, ти врятував мені величезну кількість часу :)
DanielM

13

Я певний час задавав собі те саме питання, і я думаю, що цей плагін-грунт міг бути налаштований на те, щоб робити те, що ви хочете: https://npmjs.org/package/grunt-targethtml . Він реалізує умовні теги HTML, які залежать від грунтової цілі.


2
Я бачив цей плагін, але мені не подобається ідея вручну вказувати всі файли (і фактично мати будь-яку логіку) в моєму index.html, тому що у мене вже є список вихідних js / css файлів у моїй конфігурації grunt і don ' не хочу повторити себе. Підсумок - це не в index.html, де слід вирішити, які файли включати
Дмитро Пашкевич

+1 для grunt-targethtml. Хоча трохи негарно додати, якщо заяви у вашому index.html "вирішувати", які активи завантажувати. Все-таки це має сенс. Це місце, яке ви зазвичай шукаєте, щоб включити ресурси у свій проект. Крім того, наступні дії змусили мене перевірити грунт-внесок. У ньому є чудові речі.
карбоксакс

8

Я шукав більш просте, прямолінійне рішення, тому я поєднав відповідь з цього питання:

Як розмістити блок if if в gruntfile.js

і придумав такі прості кроки:

  1. Зберігайте дві версії ваших індексних файлів у списку та називайте їх index-development.html та index-prodoction.html.
  2. Використовуйте таку логіку у вашому блоці konkat / copy Gruntfile.js для файлу index.html:

    concat: {
        index: {
            src : [ (function() {
                if (grunt.option('Release')) {
                  return 'views/index-production.html';
                } else {
                  return 'views/index-development.html';
                }
              }()) ],
           dest: '<%= distdir %>/index.html',
           ...
        },
        ...
    },
  3. запустіть 'grunt --Release', щоб вибрати файл index-production.html і залиште прапор, щоб мати версію розробки.

Немає нових плагінів для додавання чи налаштування та немає нових завдань на грунт.


3
Єдиним недоліком тут є два файли index.html, які потрібно підтримувати.
Адам Маршалл

5

Це грухте завдання з назвою scriptlinker виглядає як простий спосіб додати скрипти в режимі розробки . Можливо, ви, можливо, спершу можете запустити лаконічну задачу, а потім вказати її на ваш об'єднаний файл у режимі prod.


+1. Документація заплутана, і деякі речі (appRoot, відносні) не завжди працюють за призначенням, але все-таки: корисний інструмент.
hashchange

1
@hashchange Я не використовую цей інструмент. Я в кінцевому підсумку скористався github.com/alanshaw/grunt-include-replace замість цього. У мене в HTML-файлі є змінна, що представляє теги сценарію. Потім я заповнюю цю змінну рядком html, який я хочу. У режимі розробки ця змінна - це список скриптів. У режимі prod ця змінна - це об'єднана, мінімізована версія.
Даніель Каплан

Дякуємо за вказівник на грунт-включення-заміну. (Мені фактично потрібен інструмент для додавання всіх специфікаційних файлів у каталозі до файлу Mocha index.html. Scriptlinker це добре.)
hashchange

@hashchange Ви маєте рацію щодо висмоктування документації. Як сказати, куди слід розмістити плитки сценарію у вашому HTML-файлі?
Даніель Каплан

1
Ви визначаєте коментар HTML. Подивіться на цей файл . Вкладення трапляються на <!--SINON COMPONENT SCRIPTS-->і <!--SPEC SCRIPTS-->. І ось це завдання Grunt, яке виконує (фактичне робоче, на відміну від матеріалів у документах). Сподіваюсь, що це допомагає;)
хеджмен

5

грунт-дом-манджер читає та обробляє HTML за допомогою селекторів CSS. Вих. читати теги з вашого HTML. Видаліть вузли, додайте вузли тощо.

Ви можете використовувати grunt-dom-monger, щоб прочитати всі файли JS, які пов'язані вашим index.html, зменшити їх, а потім знову використати grunt-dom-manger, щоб змінити ваш index.html, щоб зв'язати лише поменшений JS


5

Я знайшов плагін-грунт під назвою grunt-dev-prod-switch. Все, що він робить, - це прокоментувати певні блоки, які він шукає, базуючись на опції --env, яку ви передаєте на грунт (хоча це обмежує вас у програмі dev, prod та test).

Щойно ви встановите його, як пояснено тут , ви можете запустити, наприклад:

grunt serve --env=dev, і все, що він робить, це прокоментувати блоки, які обгорнуті

    <!-- env:test/prod -->
    your code here
    <!-- env:test/prod:end -->

і це відміняє блоки, які загорнуті

    <!-- env:dev -->
    your code here
    <!-- env:dev:end -->

Він також працює на javascript, я використовую його для встановлення правильної IP-адреси, до якої можна підключитися для мого бекенд-API. Блоки просто змінюються на

    /* env:dev */
    your code here
    /* env:dev:end */

У вашому випадку це було б так просто:

<!DOCTYPE html>
<html>
    <head>
        <!-- env:dev -->
        <script src="js/module1.js" />
        <script src="js/module2.js" />
        <script src="js/module3.js" />
        ...
        <!-- env:dev:end -->
        <!-- env:prod -->
        <script src="js/MyApp-all.min.js" />
        ...
        <!-- env:prod:end -->
    </head>
    <body></body>
</html>

4

grunt-bake - це фантастичний сценарій бурчання, який би тут чудово працював. Я використовую його в моєму сценарії автоматичної збірки JQM.

https://github.com/imaginethepoet/autojqmphonegap

Подивіться на мій файл grunt.coffee:

bake:
    resources: 
      files: "index.html":"resources/custom/components/base.html"

Це переглядає всі файли в base.html і засмоктує їх, щоб створити index.html працює чудово для багатосторінкових програм (phonegap). Це дозволяє полегшити розробку, оскільки всі розробники не працюють над однією довгою програмою на одній сторінці (запобігаючи безлічі конфліктних перевірок). Натомість ви можете розбивати сторінки та працювати над меншими фрагментами коду та компілювати на повну сторінку за допомогою команди watch.

Бейк зчитує шаблон із base.html та вводить компоненти компонентів html на годинник.

<!DOCTYPE html>

jQuery Mobile Demos

app.initialize ();

<body>
    <!--(bake /resources/custom/components/page1.html)-->
    <!--(bake /resources/custom/components/page2.html)-->
    <!--(bake /resources/custom/components/page3.html)-->
</body>

Ви можете зробити цей крок далі і додати ін'єкції на свої сторінки для "меню", "спливаючих вікон" тощо, щоб ви могли дійсно розбити сторінки на більш дрібні керовані компоненти.


Можливо, ви можете покращити свою відповідь за допомогою демо-коду, який використовує грунт-бейк?
Дмитро Пашкевич

4

Використовуйте комбінацію провідних https://github.com/taptapship/wiredep та використовуйте https://github.com/yeoman/grunt-usemin , щоб грунтовно взяти на себе ці завдання. Wiredep додасть вашим залежностям один файл сценарію за один раз, а usemin об'єднає їх у єдиний файл для виробництва. Потім це може бути досягнуто лише за допомогою деяких коментарів у html. Наприклад, мої пакети bower автоматично включаються і додаються в html при запуску bower install && grunt bowerInstall:

<!-- build:js /scripts/vendor.js -->
<!-- bower:js -->
<!-- endbower -->
<!-- endbuild -->

2

Ця відповідь не для нобудів!

Використовуйте шаблони Jade ... передача змінних до шаблону Jade - це стандартний випадок використання

Я використовую grunt (grunt-contrib-jade), але вам не доведеться використовувати grunt. Просто використовуйте стандартний нефритовий модуль npm.

Якщо ви використовуєте грунт, то ваш gruntfile хотів би щось схоже на ...

jade: {
    options: {
      // TODO - Define options here
    },
    dev: {
      options: {
        data: {
          pageTitle: '<%= grunt.file.name %>',
          homePage: '/app',
          liveReloadServer: liveReloadServer,
          cssGruntClassesForHtmlHead: 'grunt-' + '<%= grunt.task.current.target %>'
        },
        pretty: true
      },
      files: [
        {
          expand: true,
          cwd: "src/app",
          src: ["index.jade", "404.jade"],
          dest: "lib/app",
          ext: ".html"
        },
        {
          expand: true,
          flatten: true,
          cwd: "src/app",
          src: ["directives/partials/*.jade"],
          dest: "lib/app/directives/partials",
          ext: ".html"
        }
      ]
    }
  },

Тепер ми можемо легко отримати доступ до даних, переданих грунтом у шаблоні Jade.

Так само, як і підхід, який застосовує Modernizr, я встановлюю CSS-клас на тег HTML відповідно до значення передаваної змінної і можу звідти використовувати логіку JavaScript на основі наявності класу CSS чи ні.

Це чудово, якщо використовувати Angular, оскільки ви можете робити ng-if для включення елементів на сторінку на основі наявності класу.

Наприклад, я можу включити сценарій, якщо клас присутній ...

(Наприклад, я можу включити сценарій перезавантаження в реальному часі у розробці, але не у виробництві)

<script ng-if="controller.isClassPresent()" src="//localhost:35729/livereload.js"></script> 

2

Розглянемо processhtml . Це дозволяє визначити кілька "цілей" для збірок. Коментарі використовуються для умовного включення або виключення матеріалу з HTML:

<!-- build:js:production js/app.js -->
...
<!-- /build -->

стає

<script src="js/app.js"></script>

Навіть мається на увазі зробити вишукані речі, як це (див . ПРОЧИТАТИ ):

<!-- build:[class]:dist production -->
<html class="debug_mode">
<!-- /build -->

<!-- class is changed to 'production' only when the 'dist' build is executed -->
<html class="production">
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.