One place for hosting & domains

      помощью

      Настройка компонентов React с помощью свойств


      Автор выбрал Creative Commons для получения пожертвования в рамках программы Write for DOnations.

      Введение

      В этом руководстве вы создадите пользовательские компоненты, передавая свойства в ваш компонент. Свойства — это аргументы, которые вы предоставляете элементу JSX. Они выглядят как стандартные HTML-свойства, но не являются предопределенными и могут использовать несколько типов данных JavaScript, включая числа, строки, функции, массивы и даже другие компоненты React. Ваши пользовательские компоненты могут использовать свойства для отображения данных или использования этих данных, чтобы сделать компоненты интерактивным. Свойства — это ключ к созданию компонентов, которые можно легко адаптировать под разные ситуации, а их знание даст вам инструменты для разработки пользовательских компонентов, которые могут обрабатывать уникальные ситуации.

      После добавления свойств в ваш компонент вы будете использовать PropTypes для определения типа данных, которые ожидает компонент. PropTypes — это простая система типов для проверки того, что данные соответствуют ожидаемым типам во время исполнения. Они будут использоваться как в качестве документации, так и в качестве механизма проверки ошибок, который позволит сохранить прогнозируемое поведение вашего приложения по мере его масштабирования.

      К концу этого руководства вы сможете использовать разные свойства для создания небольшого приложения, которое будет использовать массив данных о животных и отображать информацию, включая имя, научное наименование, размер, питание и дополнительную информацию.

      Примечание. В качестве первого шага необходимо настроить пустой проект, на основе которого вы будете выполнять упражнение из руководства. Если у вас уже есть рабочий проект и вы хотите напрямую работать со свойствами, начните выполнение руководства с шага 2.

      Предварительные требования

      Шаг 1 — Создание пустого проекта

      На этом шаге мы создадим новый проект, используя Create React App. Затем вы удалите пример проекта и связанные файлы, которые устанавливаются при инициализации проекта. В заключение вы создадите простую структуру файлов для организации ваших компонентов.

      Создайте новый проект. В командной строке запустите следующий скрипт для установки нового проекта с помощью create-react-app:

      • npx create-react-app prop-tutorial

      После завершения создания проекта перейдите в его директорию:

      В новой вкладке или окне терминала запустите проект, используя скрипт start Create React App​​​. Браузер будет выполнять автообновление при изменениях, поэтому вы должны оставить этот скрипт работающим все время при работе:

      Вы получите запущенный локальный сервер. Если проект не был открыт в браузере, вы можете открыть его, перейдя на страницу http://localhost:3000/. Если вы запустили приложение на удаленном сервере, воспользуйтесь адресом http://your_IP_address:3000.

      Ваш браузер загрузит простое приложение React в качестве элемента Create React App:

      Шаблон проекта React

      Вы создадите совершенно новый набор пользовательских компонентов. Для начала избавьтесь от шаблонного кода, чтобы получить пустой проект.

      Откройте src/App.js в текстовом редакторе. Это корневой компонент, который встраивается в страницу. Все компоненты будут запускаться отсюда. Дополнительную информацию об App.js можно найти в статье Настройка проекта React с помощью Create React App.

      Откройте src/App.js с помощью следующей команды:

      Вы увидите следующий файл:

      prop-tutorial/src/App.js

      import React from 'react';
      import logo from './logo.svg';
      import './App.css';
      
      function App() {
        return (
          <div className="App">
            <header className="App-header">
              <img src={logo} className="App-logo" alt="logo" />
              <p>
                Edit <code>src/App.js</code> and save to reload.
              </p>
              <a
                className="App-link"
                href="https://reactjs.org"
                target="_blank"
                rel="noopener noreferrer"
              >
                Learn React
              </a>
            </header>
          </div>
        );
      }
      
      export default App;
      

      Удалите строку import logo from './logo.svg';​​. Затем замените весь код оператора return, который должен возвращать набор пустых тегов: <></>​​​. В результате вы получите страницу валидации, которая ничего не возвращает. Окончательный код будет выглядеть следующим образом:

      prop-tutorial/src/App.js

      
      import React from 'react';
      import './App.css';
      
      function App() {
        return <></>;
      }
      
      export default App;
      

      Сохраните изменения и закройте текстовый редактор.

      В заключение удалите логотип. Вы не будете использовать ее в вашем приложении. Кроме того, вам нужно удалить неиспользуемые файлы, которые не пригодятся вам в работе. Это позволит вам избежать путаницы в будущем.

      В окне терминала введите следующую команду:

      Если вы откроете ваш браузер, то увидите пустой экран.

      пустой экран в chrome

      Теперь, когда вы успешно очистили пример проекта Create React App, создайте простую структуру файлов. Это позволит вам поддерживать ваши компоненты изолированными и независимыми.

      Создайте каталог с именем components в каталоге src. В нем будут храниться все ваши пользовательские компоненты.

      Каждый компонент будет иметь собственный каталог для хранения файла компонента, а также стилей, изображений, если таковые имеются, и тестов.

      Создайте каталог для App:

      Переместите все файлы App в этот каталог. Используйте подстановочный символ * для выбора любых файлов, начинающихся с App. вне зависимости от их расширения. Затем используйте команду mv для их отправки в новый каталог.

      • mv src/App.* src/components/App

      В заключение обновите относительный путь импорта в index.js, который представляет собой корневой компонент и инициализирует весь процесс.

      Оператор импорта должен указывать на файл App.js в каталоге App, поэтому необходимо внести следующее изменение:

      prop-tutorial/src/index.js

      import React from 'react';
      import ReactDOM from 'react-dom';
      import './index.css';
      import App from './components/App/App';
      import * as serviceWorker from './serviceWorker';
      
      ReactDOM.render(
        <React.StrictMode>
          <App />
        </React.StrictMode>,
        document.getElementById('root')
      );
      
      // If you want your app to work offline and load faster, you can change
      // unregister() to register() below. Note this comes with some pitfalls.
      // Learn more about service workers: https://bit.ly/CRA-PWA
      serviceWorker.unregister();
      

      Сохраните и закройте файл.

      Теперь, когда проект настроен, вы можете создать ваш первый компонент.

      Шаг 2 — Настройка динамических компонентов со свойствами

      На этом шаге вы создадите компонент, который будет изменяться на основе передаваемой информации, т.е. свойств. Свойства — это аргументы, которые вы передаете функции или классу, но так как ваши компоненты трансформируются в HTML-объекты с JSX, вы будете передавать свойства в виде HTML-атрибутов. В отличие от HTML-элементов, вы можете передавать множество разных типов данных, от строк до массивов, объектов и даже функций.

      Здесь вы создадите компонент, который будет отображать информацию о животных. Этот компонент будет получать имя и научное наименование животного в форме строк, размер в форме целого числа, питание в форме массива строк и дополнительную информацию в форме объекта. Вы будете передавать информацию новому компоненту в форме свойств и использовать эти данные в вашем компоненте.

      К концу этого шага у вас будет пользовательский компонент, который будет использовать разные свойства. Также вы сможете повторно использовать компонент для отображения массива данных, используя общий компонент.

      Добавление данных

      Во-первых, вам потребуются шаблонные данные. Создайте файл в каталоге src/App с именем data.

      • touch src/components/App/data.js

      Откройте новый файл в текстовом редакторе:

      • nano src/components/App/data.js

      Далее добавьте массив объектов, который вы будете использовать в качестве примера данных:

      prop-tutorial/src/components/App/data.js

      export default [
        {
          name: 'Lion',
          scientificName: 'Panthero leo',
          size: 140,
          diet: ['meat'],
        },
        {
          name: 'Gorilla',
          scientificName: 'Gorilla beringei',
          size: 205,
          diet: ['plants', 'insects'],
          additional: {
            notes: 'This is the eastern gorilla. There is also a western gorilla that is a different species.'
          }
        },
        {
          name: 'Zebra',
          scientificName: 'Equus quagga',
          size: 322,
          diet: ['plants'],
          additional: {
            notes: 'There are three different species of zebra.',
            link: 'https://en.wikipedia.org/wiki/Zebra'
          }
        }
      ]
      

      Массив объектов содержит разные данные и дает вам возможность использовать различные свойства. Каждый объект представляет собой отдельное животное с именем, научным наименованием, размером, питанием и опциональным полем дополнительных данных, которое будет содержать ссылки или заметки. В этом коде вы также экспортировали массив default.

      Сохраните и закройте файл.

      Создание компонентов

      Затем создайте временный компонент AnimalCard. Этот компонент будет получать свойства и отображать данные.

      Сначала создайте каталог в src/components​​​ с названием AnimalCard, а затем файл с именем src/components/AnimalCard/AnimalCard.js и CSS-файл с именем src/components/AnimalCard/AnimalCard.css.

      • mkdir src/components/AnimalCard
      • touch src/components/AnimalCard/AnimalCard.js
      • touch src/components/AnimalCard/AnimalCard.css

      Откройте AnimalCard.js​​​ в текстовом редакторе:

      • nano src/components/AnimalCard/AnimalCard.js

      Добавьте базовый компонент, который импортирует CSS и возвращает тег <h2>.

      prop-tutorial/src/components/AnimalCard/AnimalCard.js

      import React from 'react';
      import './AnimalCard.css'
      
      export default function AnimalCard() {
        return <h2>Animal</h2>
      }
      

      Сохраните и закройте файл. Теперь вам нужно импортировать данные и компонент в базовый компонент App.

      Откройте src/components/App/App.js​​​:

      • nano src/components/App/App.js

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

      prop-tutorial/src/components/App/App.js

      import React from 'react';
      import data from './data';
      import AnimalCard from '../AnimalCard/AnimalCard';
      import './App.css';
      
      function App() {
        return (
          <div className="wrapper">
            <h1>Animals</h1>
            {data.map(animal => (
              <AnimalCard key={animal.name}/>
            ))}
          </div>
        )
      }
      
      export default App;
      

      Сохраните и закройте файл. Здесь вы используете метод массива .map() для итерации по данным. Кроме добавления цикла у вас также имеется оборачивающий div с классом, который вы будете использовать для стилей, и тег <h1> для наименования вашего проекта.

      После сохранения данных и перезагрузки браузера вы увидите наименование для каждой карточки.

      Проект React в браузере без стилей

      Далее добавьте несколько стилей для выравнивания элементов. Откройте App.css:

      • nano src/components/App/App.css

      Замените его содержимое на следующее для организации элементов:

      prop-tutorial/src/components/App/App.css

      .wrapper {
          display: flex;
          flex-wrap: wrap;
          justify-content: space-between;
          padding: 20px;
      }
      
      .wrapper h1 {
          text-align: center;
          width: 100%;
      }
      

      Здесь используется flexbox для приведения в порядок данных и их выравнивания. padding добавляет некоторое пространство в окне браузера. justify-content будет обеспечивать дополнительное пространство между элементами, а .wrapper h1​​​ будет размещать заголовок Animal на всю его длину.

      Сохраните и закройте файл. Когда вы сделаете это и обновите браузер, то увидите, что между данными теперь есть определенное свободное пространство.

      Проект React в браузере с распределенными данными

      Добавление свойств

      Теперь, когда у вас есть настроенные компоненты, вы можете добавить свое первое свойство. При итерации по данным у вас был доступ к каждому объекту в массиве data и элементам, которые он содержит. Вы будете добавлять каждый элемент данных в отдельное свойство, которое затем будет использоваться в компоненте AnimalCard.

      Откройте App.js:

      • nano src/components/App/App.js

      Добавьте свойство name в AnimalCard.

      prop-tutorial/src/components/App/App.js

      import React from 'react';
      ...
      function App() {
        return (
          <div className="wrapper">
            <h1>Animals</h1>
            {data.map(animal => (
              <AnimalCard
                key={animal.name}
                name={animal.name}
              />
            ))}
          </div>
        )
      }
      
      export default App;
      

      Сохраните и закройте файл. Свойство name выглядит как стандартный HTML-атрибут, но вместо строки вы передаете свойство name объекта animal в фигурных скобках.

      Теперь, когда вы передали одно свойство в новый компонент, необходимо использовать его. Откройте AnimalCard.js:

      • nano src/components/AnimalCard/AnimalCard.js

      Все свойства, которые вы передаете в компонент, будут собираться в объект, который будет первым аргументом вашей функции. Выполните деструктуризацию объекта для извлечения отдельных свойств:

      prop-tutorial/src/components/AnimalCard/AnimalCard.js

      
      import React from 'react';
      import './AnimalCard.css'
      
      export default function AnimalCard(props) {
        const { name } = props;
        return (
          <h2>{name}</h2>
        );
      }
      

      Обратите внимание, что вам не нужно деструктурировать свойства для его использования, но это полезный метод для работы с шаблонными данными в этом руководстве.

      После деструктурирования объекта вы можете использовать отдельные элементы данных. В данном случае вы будете использовать заголовок в теге <h2>, включающий значение в фигурных скобках, чтобы React мог использовать его в качестве JavaScript.

      Также вы можете использовать свойство в объекте prop, используя запись через точку. В качестве примера вы можете создать элемент <h2>​​​ следующим образом: <h2>{props.title}</h2>​​​. Преимущество деструктурирования состоит в том, что вы можете собирать неиспользуемые свойства и использовать оператор rest объекта.

      Сохраните и закройте файл. Сделав это и перезагрузив браузер, вы увидите конкретное имя для каждого животного вместо заполнителя.

      Проекты React с указанием имен животных

      Свойство name представляет собой строку, но строка может быть любым типом данных, который вы можете передавать функции JavaScript. Чтобы посмотреть, как это работает, добавьте остальные данные.

      Откройте файл App.js:

      • nano src/components/App/App.js

      Добавьте свойство для каждого из следующих элементов: scientificName, size, diet и additional. Они сдержат строки, целые числа, массивы и объекты.

      prop-tutorial/src/components/App/App.js

      import React from 'react';
      ...
      
      function App() {
        return (
          <div className="wrapper">
            <h1>Animals</h1>
            {albums.map(album => (
              <AnimalCard
                additional={animal.additional}
                diet={animal.diet}
                key={animal.name}
                name={animal.name}
                scientificName={animal.scientificName}
                size={animal.size}
              />
            ))}
          </div>
        )
      }
      
      export default App;
      

      Поскольку вы создаете объект, вы можете добавить их в любом желаемом порядке. Использование алфавитного порядка повышает читаемость списка свойств, особенно для больших списков. Также вы можете добавить их в одной строке, но их разделение на отдельные строки делает код более читабельным.

      Сохраните и закройте файл. Откройте AnimalCard.js.

      • nano src/components/AnimalCard/AnimalCard.js

      На этот раз выполните деструктуризацию свойств списка параметров функции и используйте данные в компоненте:

      prop-tutorial/src/components/AnimalCard/AnimalCard.js

      import React from 'react';
      import './AnimalCard.css'
      
      export default function AnimalCard({
        additional,
        diet,
        name,
        scientificName,
        size
      }) {
        return (
          <div>
            <h2>{name}</h2>
            <h3>{scientificName}</h3>
            <h4>{size}kg</h4>
            <div>{diet.join(', ')}.</div>
          </div>
        );
      }
      

      После получения данных вы можете добавить scientificName и size в теги заголовка, но вам нужно превратить массив в строку, чтобы React мог отобразить их на странице. Вы можете сделать это с помощью join(', '), который будет создавать разделенный запятой список.

      Сохраните и закройте файл. Сделав это и обновив браузер, вы увидите структурированные данные.

      Проект React с полным списком данных для животных

      Вы можете создать аналогичный список с объектом additional, но вместо этого добавьте функцию для оповещения пользователя с данными. Это даст вам возможность передавать функции в виде свойств, а затем использовать данные внутри компонента при вызове функции.

      Откройте App.js:

      • nano src/components/App/App.js

      Создайте функцию showAdditionalData, которая будет конвертировать объект в строку и отображать ее в качестве оповещения.

      prop-tutorial/src/components/App/App.js

      import React from 'react';
      ...
      
      function showAdditional(additional) {
        const alertInformation = Object.entries(additional)
          .map(information => `${information[0]}: ${information[1]}`)
          .join('n');
        alert(alertInformation)
      };
      
      function App() {
        return (
          <div className="wrapper">
            <h1>Animals</h1>
            {data.map(animal => (
              <AnimalCard
                additional={animal.additional}
                diet={animal.diet}
                key={animal.name}
                name={animal.name}
                scientificName={animal.scientificName}
                showAdditional={showAdditional}
                size={animal.size}
              />
            ))}
          </div>
        )
      }
      
      export default App;
      

      Функция showAdditional конвертирует объект в массив пар, где первый элемент — это ключ, а второй — значение. Затем она преобразовывает данные, конвертируя пару ключей в строку. После этого функция добавляет разделитель строки n, прежде чем передавать полную строку в сигнальную функцию.

      Поскольку JavaScript может принимать функции в качестве аргументов, React также может принимать функции в качестве свойств. Поэтому вы можете передать showAdditional в AnimalCard в качестве свойства с именем showAdditional.

      Сохраните и закройте файл. Откройте AnimalCard:

      • nano src/components/AnimalCard/AnimalCard.js

      Извлеките функцию showAdditional из объекта свойства, затем создайте <button> с событием onClick, которое вызывает функцию с объектом additional:

      prop-tutorial/src/components/AnimalCard/AnimalCard.js

      import React from 'react';
      import './AnimalCard.css'
      
      export default function AnimalCard({
        additional,
        diet,
        name,
        scientificName,
        showAdditional,
        size
      }) {
        return (
          <div>
            <h2>{name}</h2>
            <h3>{scientificName}</h3>
            <h4>{size}kg</h4>
            <div>{diet.join(', ')}.</div>
            <button onClick={() => showAdditional(additional)}>More Info</button>
          </div>
        );
      }
      

      Сохраните файл. Сделав это и обновив браузер, вы увидите кнопку после каждой карточки. При нажатии на кнопку, вы получите оповещение с дополнительными данными.

      Оповещение с информацией

      Если вы попробуете нажать More Info для карточки Lion, то получите ошибку. Это связано с тем, что для льва дополнительные данные отсутствуют. Вы увидите, как исправить это, в шаге 3.

      В заключение давайте добавим немного оформления в наш проект. Добавьте className для animal-wrapper в div в AnimalCard:

      prop-tutorial/src/components/AnimalCard/AnimalCard.js

      import React from 'react';
      import './AnimalCard.css'
      
      export default function AnimalCard({
      ...
        return (
          <div className="animal-wrapper">
      ...
          </div>
        )
      }
      

      Сохраните и закройте файл. Откройте AnimalCard.css:

      • nano src/components/AnimalCard/AnimalCard.css

      Добавьте CSS для добавления тонкой границы и отступа для карточек и кнопки:

      prop-tutorial/src/components/AnimalCard/AnimalCard.css

      .animal-wrapper {
          border: solid black 1px;
          margin: 10px;
          padding: 10px;
          width: 200px;
      }
      
      .animal-wrapper button {
          font-size: 1em;
          border: solid black 1px;
          padding: 10;
          background: none;
          cursor: pointer;
          margin: 10px 0;
      }
      

      Это код CSS добавит небольшую границу для карточки и заменит стиль кнопки, используемый по умолчанию, на границу и отступ. cursor: pointer​​​ будет менять курсор при наведении на кнопку.

      Сохраните и закройте файл. После выполнения этих действий и обновления браузера вы увидите данные в отдельных карточках.

      Проект React с добавленными стилями для карточек животных

      На данный момент вы создали два пользовательских компонента. Вы передали данные второму компоненту из первого компонента, используя свойства. Свойства включают разные данные, такие как строки, целые числа, массивы, объекты и функции. В вашем втором компоненте вы использовали свойства для создания динамического компонента с помощью JSX.

      В следующем шаге вы будете использовать типизированную систему prop-types для указания структуры, которую ваш компонент ожидает увидеть, что будет обеспечивать предсказуемость в приложении и предотвращать ошибки.

      Шаг 3 — Создание предсказуемых свойств с помощью PropTypes и defaultProps

      На этом шаге вы добавите простую систему типизации для ваших компонентов с помощью PropTypes. PropTypes действуют как прочие системы типизации, прямо определяя тип данных, который вы ожидаете получить от определенного свойства. Они также дают вам возможность определять данные по умолчанию в случаях, когда свойство не всегда требуется. В отличие от большинства систем типизации, PropTypes выполняет проверку при исполнении, так что если свойства не соответствуют типу, код скомпилируется, но в консоли будет отображена ошибка.

      К концу этого шага вы обеспечите предсказуемость вашего пользовательского компонента, определив тип для каждого свойства. Это позволит гарантировать, что другой человек, который будет работать с компонентом, будет иметь четкое представление о структуре данных, которая требуется компоненту.

      Пакет prop-types устанавливается вместе с Create React App, поэтому для его использования необходимо только импортировать его в ваш компонент.

      Откройте AnimalCard.js:

      • nano src/components/AnimalCard/AnimalCard.js

      Затем импортируйте PropTypes из prop-types:

      prop-tutorial/src/components/AnimalCard/AnimalCard.js

      import React from 'react';
      import PropTypes from 'prop-types';
      import './AnimalCard.css'
      
      export default function AnimalCard({
      ...
      }
      

      Добавьте PropTypes прямо в функцию компонента. В JavaScript функции являются объектами, что означает, что вы можете добавить свойства, используя точечный синтаксис. Добавьте следующие PropTypes в AnimalCard.js:

      prop-tutorial/src/components/AnimalCard/AnimalCard.js

      import React from 'react';
      import PropTypes from 'prop-types';
      import './AnimalCard.css'
      
      export default function AnimalCard({
      ...
      }
      
      AnimalCard.propTypes = {
        additional: PropTypes.shape({
          link: PropTypes.string,
          notes: PropTypes.string
        }),
        diet: PropTypes.arrayOf(PropTypes.string).isRequired,
        name: PropTypes.string.isRequired,
        scientificName: PropTypes.string.isRequired,
        showAdditional: PropTypes.func.isRequired,
        size: PropTypes.number.isRequired,
      }
      

      Сохраните и закройте файл.

      Как видите, существует множество разных PropTypes. Это только небольшой пример; изучите официальную документацию React, чтобы узнать об остальных типах, которые вы можете использовать.

      Давайте начнем со свойства name. Здесь вы указываете, что в name должна находиться строка. Свойство scientificName аналогично предыдущему примеру. sizeчисло, которое может включать как числа с плавающей точкой, например, 1.5, так и целые числа, например 6. showAdditional — это функция (func).

      diet же имеет определенные отличия. В данном случае вы указываете, что diet представляет собой массив, но вам также необходимо указать, что будет содержать этот массив. В нашем случае массив будет содержать только строки. Если вы хотите смешивать типы, то можете использовать другое свойство oneOfType, которое принимает массив валидных значений PropTypes. Вы можете использовать oneOfType где угодно, так что если вы хотите, чтобы size принимал числа или строки, внесите следующие изменения:

      size: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
      

      Свойство additional также немного более сложное. В этом случае вы указываете объект, но, чтобы внести большую ясность, вы указываете, что должен содержать объект. Для этого вы можете использовать PropTypes.shape, принимающий объект с дополнительными полями, которым требуются собственные значения PropTypes. В нашем случае link и notes имеют значение PropTypes.string.

      В настоящее время все данные имеют правильную форму и отвечают свойствам. Чтобы узнать, что произойдет, если значение PropTypes не будет совпадать, откройте ваши данные:

      • nano src/components/App/data.js

      Измените для size значение на строку в первом элементе:

      prop-tutorial/src/components/App/data.js

      export default [
        {
          name: 'Lion',
          scientificName: 'Panthero leo',
          size: '140',
          diet: ['meat'],
        },
      ...
      ]
      

      Сохраните файл. После выполнения этих действий и обновления браузера вы увидите ошибку в консоли.

      Error

      index.js:1 Warning: Failed prop type: Invalid prop `size` of type `string` supplied to `AnimalCard`, expected `number`. in AnimalCard (at App.js:18) in App (at src/index.js:9) in StrictMode (at src/index.js:8)

      Браузер с ошибкой типизации

      В отличие от других систем типизации, таких как TypeScript, PropTypes не будет генерировать предупреждение на этапе сборки, поэтому если ошибки кода отсутствуют, то код все равно скомпилируется. Это означает, что вы можете случайно опубликовать код с ошибками свойств.

      Измените данные обратно на правильный тип:

      prop-tutorial/src/components/App/data.js

      export default [
        {
          name: 'Lion',
          scientificName: 'Panthero leo',
          size: 140,
          diet: ['meat'],
        },
      ...
      ]
      

      Сохраните и закройте файл.

      Откройте AnimalCard.js:

      • nano src/components/AnimalCard/AnimalCard.js

      Каждое свойство, за исключением additional, имеет параметр isRequired. Это означает, что они должны быть обязательно указаны. Если вы не укажете обязательное свойство, код все равно скомпилируется, но вы получите ошибку исполнения в консоли.

      Если свойство не обязательное, вы можете добавить значение по умолчанию. Рекомендуется всегда добавлять значение по умолчанию для предотвращения ошибок при исполнении, если свойство не обязательное. Например, в компоненте AnimalCard вы вызываете функцию с данными additional. Если их там нет, функция попытается получить их и изменить несуществующий объект, в результате чего приложение упадет.

      Чтобы предотвратить эту проблему, добавьте значение defaultProp для additional:

      prop-tutorial/src/components/AnimalCard/AnimalCard.js

      import React from 'react';
      import PropTypes from 'prop-types';
      import './AnimalCard.css'
      
      export default function AnimalCard({
      ...
      }
      
      AnimalCard.propTypes = {
        additional: PropTypes.shape({
          link: PropTypes.string,
          notes: PropTypes.string
        }),
      ...
      }
      
      AnimalCard.defaultProps = {
        additional: {
          notes: 'No Additional Information'
        }
      }
      

      Вы добавляете значение defaultProps в функцию, используя точечный синтаксис, как вы это делали для propTypes, затем вы добавляете значение по умолчанию, которое компонент должен использовать, если свойство не определено. В нашем случае вы подставляете форму additional, включая сообщение, которое не содержит дополнительной информации.

      Сохраните и закройте файл. После этого обновите браузер. После обновления страницы нажмите кнопку More Info​​​ карточки Lion. У нее нет поля additional в данных, поэтому свойство не определено. Но AnimalCard будет заменять свойство по умолчанию.

      Браузер с сообщением по умолчанию в оповещении

      Теперь ваши свойства хорошо задокументированы и являются либо обязательными, либо имеют значение по умолчанию для обеспечения предсказуемого кода. Это позволит будущим разработчикам (в том числе вам) понимать, какие свойства требуются компоненту. Это облегчит повторное использование ваших компонентов, предоставляя вам полную информацию о том, как компонент будет использовать данные, которые он получает.

      Заключение

      В этом руководстве вы создали несколько компонентов, которые используют свойства для отображения информации из родительского элемента. Свойства дают вам гибкость, необходимую для разбивки крупных компонентов на более мелкие и понятные части. Теперь, когда ваши данные больше не имеют тесной привязки к отображаемой информации, вы можете самостоятельно делать выбор в отношении сегментации вашего приложения.

      Свойства — это важнейший инструмент для создания сложных приложений, дающий возможность создания компонентов, которые могут адаптироваться к получаемым данным. С помощью PropTypes вы создаете предсказуемые и понятные компоненты, которые дадут команде возможность повторно использовать работу друг друга для создания гибкой и стабильной базы кода. Если вас интересуют другие обучающие модули по React, ознакомьтесь с нашей страницей тем по React или вернитесь на страницу серии Программирование на React.js.



      Source link

      Обеспечение безопасности Apache с помощью Let’s Encrypt в Ubuntu 20.04


      Введение

      Let’s Encrypt — это центр сертификации, упрощающий получение и установку бесплатных сертификатов TLS/SSL, обеспечивая шифрование HTTPS на веб-серверах. Это упрощает процесс посредством предоставления программного клиента Certbot, который пытается автоматизировать большинство необходимых шагов (или все шаги). В настоящее время весь процесс получения и установки сертификата полностью автоматизирован — как в Apache, так и Nginx.

      В этом руководстве мы будем использовать Certbot для получения бесплатного сертификата SSL для Apache в Ubuntu 20.04, и убедимся, что сертификат настроен на автоматическое обновление.

      В этом обучающем руководстве используется отдельный файл виртуального хоста вместо файла конфигурации Apache по умолчанию — для настройки веб-сайта, который будет защищен Let’s Encrypt. Рекомендуется создавать новые файлы виртуального хоста Apache для каждого домена, размещенного на сервере, т. к. это позволяет избежать распространенных ошибок, и при этом поддерживаются файлы конфигурации по умолчанию в качестве резервной настройки.

      Предварительные требования

      Для данного обучающего руководства вам потребуется следующее:

      • Один сервер Ubuntu 20.04, настроенный в соответствии с обучающим руководством Начальная настройка сервера Ubuntu 20.04, включая пользователя без прав root с привилегиями sudo и брандмауэр.

      • Зарегистрированное полное доменное имя. Во всем обучающем руководстве мы будем использовать your_domain в качестве примера. Вы можете купить доменное имя на Namecheap, получить его бесплатно на Freenom или воспользоваться услугами любого предпочитаемого регистратора доменных имен.

      • На вашем сервере должны быть настроены обе нижеследующие записи DNS. В руководстве Введение в DigitalOcean DNS содержится подробная информация по их добавлению.

        • Запись A, где your_domain указывает на публичный IP-адрес вашего сервера.
        • Запись A, где www.your_domain указывает на публичный IP-адрес вашего сервера.
      • Apache, установленный в соответствии с указаниями руководства Установка Apache в Ubuntu 20.04. Убедитесь, что у вас имеется файл виртуального хоста для вашего домена. В этом обучающем руководстве мы будем использовать /etc/apache2/sites-available/your_domain.conf в качестве примера.

      Шаг 1 — Установка Certbot

      Для получения сертификата SSL с помощью Let’s Encrypt нам сначала необходимо установить программное обеспечение Certbot на вашем сервере. Для этого мы будем использовать репозитории пакетов Ubuntu по умолчанию.

      Нам нужны два пакета: certbot и python3-certbot-apache. Последний — плагин, который интегрирует Certbot с Apache, позволяя автоматизировать получение сертификата и настройку HTTPS на веб-сервере с помощью одной команды.

      • sudo apt install certbot python3-certbot-apache

      Чтобы подтвердить установку, нажмите Y, а затем ENTER.

      Теперь Certbot установлен на сервере. На следующем шаге мы будем проверять конфигурацию Apache, чтобы убедиться, что ваш виртуальный хост настроен корректно. Благодаря этому пользовательский скрипт certbot сможет выявлять ваши домены и изменять настройку вашего веб-сервера для автоматического использования нового сертификата SSL.

      Шаг 2 — Проверка конфигурации виртуального хоста Apache

      Чтобы автоматически получить и настроить SSL для вашего веб-сервера, Certbot должен найти корректный виртуальный хост в ваших файлах конфигурации Apache. Имена доменов ваших серверов будут получены из директив ServerName и ServerAlias, определенных в блоке конфигурации VirtualHost.

      Если вы выполнили шаг по настройке виртуального хоста из руководства по установке Apache, то у вас уже должен быть настроен блок VirtualHost для вашего домена в /etc/apache2/sites-available/your_domain.conf​​​​​​ с ServerName, а также правильно заданные директивы ServerAlias.

      Чтобы проверить это, откройте для вашего домена файл виртуального хоста с помощью nano или предпочитаемого текстового редактора:

      • sudo nano /etc/apache2/sites-available/your_domain.conf

      Найдите существующие строки ServerName и ServerAlias. Они должны выглядеть следующим образом:

      /etc/apache2/sites-available/your_domain.conf

      ...
      ServerName your_domain
      ServerAlias www.your_domain
      ...
      

      Если вы уже настроили параметры ServerName и ServerAlias, то можете выйти из текстового редактора и переходить к следующему шагу. Если вы используете nano, то можете выйти, введя CTRL+X, а затем Y и ENTER для подтверждения.

      Если конфигурация вашего виртуального хоста не соответствует данному примеру, обновите ее соответствующим образом. После завершения сохраните файл и закройте редактор. Затем запустите следующую команду для подтверждения сделанных изменений:

      • sudo apache2ctl configtest

      Должен быть получен ответ Syntax OK. Если появится сообщение об ошибке, повторно откройте файл виртуального хоста и проверьте, нет ли опечаток или отсутствующих символов. Когда синтаксис файла конфигурации станет корректным, перезагрузите Apache, чтобы изменения вступили в силу:

      • sudo systemctl reload apache2

      С этими изменениями Certbot сможет найти корректный блок VirtualHost и обновить его.

      Далее мы обновим брандмауэр, чтобы разрешить трафик HTTPS.

      Шаг 3 — Доступ к HTTPS через брандмауэр

      Если у вас включен брандмауэр UFW в соответствии с предварительными требованиями, то вам потребуется изменить настройки для поддержки трафика HTTPS. После установки Apache зарегистрирует несколько разных профилей приложений UFW. Мы можем использовать профиль Apache Full, чтобы разрешить трафик HTTP и HTTPS на сервере.

      Чтобы узнать, какой трафик разрешен на сервере, можно использовать следующую команду:

      Если вы следовали указаниям в одном из наших руководств по установке Apache, то результат должен выглядеть примерно следующим образом, т.е. сейчас должен быть разрешен только трафик HTTP через порт 80:

      Output

      Status: active To Action From -- ------ ---- OpenSSH ALLOW Anywhere Apache ALLOW Anywhere OpenSSH (v6) ALLOW Anywhere (v6) Apache (v6) ALLOW Anywhere (v6)

      Чтобы дополнительно активировать трафик HTTPS, разрешите профиль Apache Full и удалите лишний профиль Apache:

      • sudo ufw allow 'Apache Full'
      • sudo ufw delete allow 'Apache'

      Теперь ваш статус будет выглядеть следующим образом:

      Output

      Status: active To Action From -- ------ ---- OpenSSH ALLOW Anywhere Apache Full ALLOW Anywhere OpenSSH (v6) ALLOW Anywhere (v6) Apache Full (v6) ALLOW Anywhere (v6)

      Теперь вы готовы запустить Certbot и получить сертификаты.

      Шаг 4 — Получение сертификата SSL

      Certbot предоставляет широкий выбор способов получения сертификатов SSL с помощью плагинов: Плагин Apache изменит конфигурацию Apache и перезагрузит ее, когда это потребуется. Для использования этого плагина введите следующую команду:

      Этот скрипт предложит вам ответить на ряд вопросов, чтобы настроить сертификат SSL. Сначала он запросит действующий адрес электронной почты. Это письмо будет использоваться для уведомлений о продлении и безопасности:

      Output

      Saving debug log to /var/log/letsencrypt/letsencrypt.log Plugins selected: Authenticator apache, Installer apache Enter email address (used for urgent renewal and security notices) (Enter 'c' to cancel): you@your_domain

      После предоставления действующего адреса электронной почты нажмите ENTER, чтобы перейти к следующему шагу. Затем вам будет предложено подтвердить согласие с условиями обслуживания Let’s Encrypt. Вы можете подтвердить это, нажав A, а затем ENTER:

      - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      Please read the Terms of Service at
      https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf. You must
      agree in order to register with the ACME server at
      https://acme-v02.api.letsencrypt.org/directory
      - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      (A)gree/(C)ancel: A
      

      Далее вам будет предложено предоставить организации Electronic Frontier Foundation свой адрес электронной почты, чтобы получать новости и иную информацию. Если не хотите подписываться на их рассылку, то введите N. Если хотите — введите Y. Затем нажмите ENTER, чтобы перейти к следующему шагу.

      - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      Would you be willing to share your email address with the Electronic Frontier
      Foundation, a founding partner of the Let's Encrypt project and the non-profit
      organization that develops Certbot? We'd like to send you email about our work
      encrypting the web, EFF news, campaigns, and ways to support digital freedom.
      - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      (Y)es/(N)o: N
      

      На следующем шаге вам будет предложено сообщить Certbot, для каких доменов вы хотите активировать HTTPS. Указанные имена доменов автоматически получены из конфигурации вашего виртуального хоста Apache, поэтому важно убедиться, что настройки ServerName и ServerAlias вашего виртуального хоста правильные. Если хотите активировать HTTPS для всех указанных доменных имен (рекомендуется), то можно оставить командную строку пустой и нажать ENTER, чтобы продолжить. В противном случае выберите домены, для которых вы хотите активировать HTTPS, указав все подходящие номера через запятую и/или пробелы, и нажмите ENTER.

      Which names would you like to activate HTTPS for?
      - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      1: your_domain
      2: www.your_domain
      - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      Select the appropriate numbers separated by commas and/or spaces, or leave input
      blank to select all options shown (Enter 'c' to cancel):
      

      Вывод будет выглядеть следующим образом:

      Obtaining a new certificate
      Performing the following challenges:
      http-01 challenge for your_domain
      http-01 challenge for www.your_domain
      Enabled Apache rewrite module
      Waiting for verification...
      Cleaning up challenges
      Created an SSL vhost at /etc/apache2/sites-available/your_domain-le-ssl.conf
      Enabled Apache socache_shmcb module
      Enabled Apache ssl module
      Deploying Certificate to VirtualHost /etc/apache2/sites-available/your_domain-le-ssl.conf
      Enabling available site: /etc/apache2/sites-available/your_domain-le-ssl.conf
      Deploying Certificate to VirtualHost /etc/apache2/sites-available/your_domain-le-ssl.conf
      

      Далее вам будет предложено выбрать, перенаправлять трафик HTTP на HTTPS или нет. На практике это означает, что те, кто посещает ваш веб-сайт через незашифрованные каналы (HTTP), будут автоматически перенаправляться на адрес HTTPS вашего веб-сайта. Выберите 2, чтобы активировать перенаправление, или 1, если хотите сохранить и HTTP, и HTTPS в качестве отдельных методов доступа к вашему веб-сайту.

      Please choose whether or not to redirect HTTP traffic to HTTPS, removing HTTP access.
      - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      1: No redirect - Make no further changes to the webserver configuration.
      2: Redirect - Make all requests redirect to secure HTTPS access. Choose this for
      new sites, or if you're confident your site works on HTTPS. You can undo this
      change by editing your web server's configuration.
      - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      Select the appropriate number [1-2] then [enter] (press 'c' to cancel): 2
      
      

      После этого шага конфигурация Certbot будет завершена, и вам будут предоставлены последние замечания о новом сертификате, о том, где находятся полученные файлы, и о том, как протестировать вашу конфигурацию при помощи внешнего инструмента, который анализирует подлинность вашего сертификата:

      - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      Congratulations! You have successfully enabled https://your_domain and
      https://www.your_domain
      
      You should test your configuration at:
      https://www.ssllabs.com/ssltest/analyze.html?d=your_domain
      https://www.ssllabs.com/ssltest/analyze.html?d=www.your_domain
      - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      
      IMPORTANT NOTES:
       - Congratulations! Your certificate and chain have been saved at:
         /etc/letsencrypt/live/your_domain/fullchain.pem
         Your key file has been saved at:
         /etc/letsencrypt/live/your_domain/privkey.pem
         Your cert will expire on 2020-07-27. To obtain a new or tweaked
         version of this certificate in the future, simply run certbot again
         with the "certonly" option. To non-interactively renew *all* of
         your certificates, run "certbot renew"
       - Your account credentials have been saved in your Certbot
         configuration directory at /etc/letsencrypt. You should make a
         secure backup of this folder now. This configuration directory will
         also contain certificates and private keys obtained by Certbot so
         making regular backups of this folder is ideal.
       - If you like Certbot, please consider supporting our work by:
      
         Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
         Donating to EFF:                    https://eff.org/donate-le
      
      

      Теперь ваш сертификат установлен и загружен в конфигурацию Apache. Попробуйте перезагрузить веб-сайт с помощью https://, и посмотрите на индикатор безопасности в браузере. Он должен указывать, что ваш веб-сайт защищен надлежащим образом — как правило, на это указывает значок замка в адресной строке.

      Вы можете использовать SSL Labs Server Test для проверки класса вашего сертификата и получения подробной информации о нем с точки зрения внешней службы.

      На следующем (последнем) шаге мы протестируем функцию автоматического обновления Certbot, гарантирующую автоматическое обновление вашего сертификата до даты окончания его действия.

      Шаг 5 — Проверка автоматического обновления Certbot

      Сертификаты Let’s Encrypt действительны только в течение 90 дней. Это призвано стимулировать пользователей автоматизировать процесс обновления сертификатов, а также гарантировать, что сроки действия сертификатов, попавших в руки злоумышленников, будут истекать быстрее.

      Пакет certbot, который мы установили, обеспечивает обновление приложений посредством добавления скрипта обновления в /etc/cron.d — им управляет служба systemctl, которая называется certbot.timer. Этот скрипт запускается дважды в день и автоматически обновляет любой сертификат, до истечения срока которого осталось менее 30 дней.

      Чтобы проверить статус этой службы и убедиться, что она активна и запущена, можно использовать следующую команду:

      • sudo systemctl status certbot.timer

      Вывод будет выглядеть следующим образом:

      Output

      ● certbot.timer - Run certbot twice daily Loaded: loaded (/lib/systemd/system/certbot.timer; enabled; vendor preset: enabled) Active: active (waiting) since Tue 2020-04-28 17:57:48 UTC; 17h ago Trigger: Wed 2020-04-29 23:50:31 UTC; 12h left Triggers: ● certbot.service Apr 28 17:57:48 fine-turtle systemd[1]: Started Run certbot twice daily.

      Чтобы протестировать процесс обновления, можно сделать запуск «вхолостую» с помощью certbot:

      • sudo certbot renew --dry-run

      Если ошибок нет, то все в порядке. При необходимости Certbot обновит ваши сертификаты и перезагрузит Apache для получения изменений. Если процесс автоматического обновления когда-нибудь не выполнится, то Let’s Encrypt отправит сообщение на указанный вами адрес электронной почты с предупреждением о том, что срок действия сертификата подходит к концу.

      Заключение

      В этом обучающем руководстве вы установили клиент Let’s Encrypt certbot, настроили и установили сертификат SSL для своего домена и подтвердили, что служба автоматического обновления Certbot активна в systemctl. Если возникнут дополнительные вопросы об использовании Certbot, то лучше всего начинать искать ответы в их документации.



      Source link

      Использование Go с MongoDB с помощью драйвера Go MongoDB


      Автор выбрал фонд Free Software Foundation для получения пожертвования в рамках программы Write for DOnations.

      Введение

      Много лет MongoDB зависела от решений, создаваемых общими усилиями, но затем ее разработчики объявили, что работают над официальным драйвером для Go. В марте 2019 года этот новый драйвер достиг уровня готовности к эксплуатации в версии 1.0.0, и с тех пор он регулярно обновляется.

      Как и другие официальные драйвера MongoDB, драйвер Go является неотъемлемой частью языка программирования Go и обеспечивает удобную возможность использования MongoDB в качестве решения для баз данных программы Go. Он полностью интегрирован с API MongoDB, а также имеет все функции запросов, индексирования и агрегирования API и другие продвинутые функции. В отличие от сторонних библиотек, он будет полностью поддерживаться инженерами MongoDB, поэтому вы сможете быть уверенными в его дальнейшей разработке и поддержке.

      В этом обучающем руководстве вы начнете использовать официальный драйвер MongoDB Go. Вы установите драйвер, подключитесь к базе данных MongoDB и выполните несколько операций CRUD. В процессе вы создадите программу диспетчера задач для управления задачами с помощью командной строки.

      Предварительные требования

      Для этого обучающего руководства вам потребуется следующее:

      • Go, установленный на вашем компьютере, и рабочее пространство Go, настроенное в соответствии с разделом Установка Go и настройка локальной среды программирования. В этом обучающем руководстве проект будет называться tasker. Вам потребуется Go v1.11 или выше, установленный на компьютере с активированными модулями Go.
      • MongoDB, установленный для вашей операционной системы в соответствии с разделом Установка MongoDB. MongoDB 2.6 или выше — минимальная версия, поддерживаемая драйвером MongoDB Go.

      Если вы используете Go v1.11 или 1.12, то убедитесь, что модули Go активированы, присвоив переменной среды GO111MODULE значение on — так, как показано ниже:

      Дополнительную информацию об активации переменных среды можно найти в этом обучающем руководстве: Чтение и установка переменных сред и переменных оболочки.

      Команды и код, указанные в этом руководстве, были протестированы в Go версии v1.14.1 и MongoDB версии v3.6.3.

      Шаг 1 — Установка драйвера MongoDB Go

      На этом шаге вы установите пакет драйвера Go для MongoDB и импортируете его в ваш проект. Также вы подключитесь к вашей базе данных MongoDB и проверите состояние подключения.

      Теперь создайте новый каталог для этого обучающего руководства в файловой системе:

      После настройки каталога проекта измените его с помощью следующей команды:

      Затем инициализируйте проект Go с файлом go.mod. Этот файл определяет требования проекта и блокирует зависимости от правильных версий:

      Если директория вашего проекта находится за пределами директории $GOPATH, то вам нужно указать путь импорта вашего модуля следующим образом:

      • go mod init github.com/<your_username>/tasker

      Теперь файл go.mod будет выглядеть следующим образом:

      go.mod

      module github.com/<your_username>/tasker
      
      go 1.14
      

      Добавьте драйвер MongoDB Go в качестве зависимости для вашего проекта, используя следующую команду:

      • go get go.mongodb.org/mongo-driver

      Результат будет выглядеть примерно следующим образом:

      Output

      go: downloading go.mongodb.org/mongo-driver v1.3.2 go: go.mongodb.org/mongo-driver upgrade => v1.3.2

      Теперь файл go.mod будет выглядеть следующим образом:

      go.mod

      module github.com/<your_username>/tasker
      
      go 1.14
      
      require go.mongodb.org/mongo-driver v1.3.1 // indirect
      

      Затем создайте файл main.go в корневом каталоге проекта и откройте его в текстовом редакторе:

      Для начала работы с драйвером импортируйте следующие пакеты в файл main.go:

      main.go

      package main
      
      import (
          "context"
          "log"
      
          "go.mongodb.org/mongo-driver/mongo"
          "go.mongodb.org/mongo-driver/mongo/options"
      )
      

      Здесь вы добавляете пакеты mongo и опций, которые предоставляет драйвер MongoDB Go.

      Затем, в зависимости от импорта, создайте новый клиент MongoDB и подключитесь к запущенному серверу MongoDB:

      main.go

      . . .
      var collection *mongo.Collection
      var ctx = context.TODO()
      
      func init() {
          clientOptions := options.Client().ApplyURI("mongodb://localhost:27017/")
          client, err := mongo.Connect(ctx, clientOptions)
          if err != nil {
              log.Fatal(err)
          }
      }
      

      mongo.Connect() принимает Context и объект options.ClientOptions, который используется для установки строки подключения и других параметров драйвера. Вы можете посмотреть документацию по опциям пакетов, чтобы узнать, какие варианты конфигурации доступны.

      Context — крайний срок, указывающий, когда операция должна остановиться и показать результат. Это помогает предотвращать снижение производительности работы в производственных системах, когда отдельные операции работают медленно. В этом коде вы передаете context.TODO(), чтобы указать, что вы не уверены, какой именно контекст нужно использовать сейчас, но планируете добавить контекст в дальнейшем.

      Затем убедимся, что ваш сервер MongoDB был обнаружен и подключен для успешного использования метода Ping. Добавьте следующий код под функцией init:

      main.go

      . . .
          log.Fatal(err)
        }
      
        err = client.Ping(ctx, nil)
        if err != nil {
          log.Fatal(err)
        }
      }
      

      Если есть какие-либо ошибки при подключении к базе данных, то программа должна быть отключена, пока вы пытаетесь решить проблему, поскольку нет смысла поддерживать работу программы без активного подключения к базе данных.

      Добавьте следующий код для создания базы данных:

      main.go

      . . .
        err = client.Ping(ctx, nil)
        if err != nil {
          log.Fatal(err)
        }
      
        collection = client.Database("tasker").Collection("tasks")
      }
      

      Вы создаете базу данных tasker и набор задач для хранения задач, которые вы создадите. Также вы настроили команду collection в качестве переменной уровня пакетов, чтобы можно было использовать подключение к базе данных на разных этапах пакетов.

      Сохраните и закройте файл.

      На этот момент полный main.go выглядит следующим образом:

      main.go

      package main
      
      import (
          "context"
          "log"
      
          "go.mongodb.org/mongo-driver/mongo"
          "go.mongodb.org/mongo-driver/mongo/options"
      )
      
      var collection *mongo.Collection
      var ctx = context.TODO()
      
      func init() {
          clientOptions := options.Client().ApplyURI("mongodb://localhost:27017/")
          client, err := mongo.Connect(ctx, clientOptions)
          if err != nil {
              log.Fatal(err)
          }
      
          err = client.Ping(ctx, nil)
          if err != nil {
              log.Fatal(err)
          }
      
          collection = client.Database("tasker").Collection("tasks")
      }
      

      Вы выполнили настройку вашей программы для подключения к серверу MongoDB с помощью драйвера Go. На следующем шаге вы продолжите создание программы диспетчера задач.

      Шаг 2 — Создание программы интерфейса командной строки

      На этом шаге вы установите хорошо известный пакет cli (интерфейса командной строки) для оказания помощи в разработке программы диспетчера задач. Он предоставляет интерфейс, которым вы можете воспользоваться для быстрого создания современных инструментов командной строки. Например, этот пакет дает возможность задавать субкоманды для вашей программы, делающие работу с командной строкой более похожей на git.

      Запустите следующую команду для добавления пакета в качестве зависимости:

      • go get github.com/urfave/cli/v2

      Затем откройте файл main.go еще раз:

      Добавьте следующий выделенный код в файл main.go:

      main.go

      package main
      
      import (
          "context"
          "log"
          "os"
      
          "github.com/urfave/cli/v2"
          "go.mongodb.org/mongo-driver/mongo"
          "go.mongodb.org/mongo-driver/mongo/options"
      )
      . . .
      

      Вы импортируете пакет cli, как упоминалось. Также вы импортируете пакет os, который вы будете использовать для передачи аргументов командной строки в вашу программу:

      Добавьте следующий код после функции init, чтобы создать программу интерфейса командной строки и начать компиляцию при помощи кода:

      main.go

      . . .
      func main() {
          app := &cli.App{
              Name:     "tasker",
              Usage:    "A simple CLI program to manage your tasks",
              Commands: []*cli.Command{},
          }
      
          err := app.Run(os.Args)
          if err != nil {
              log.Fatal(err)
          }
      }
      

      Этот фрагмент кода создает программу интерфейса командной строки под названием tasker и добавляет краткое описание использования, которое будет отображаться при запуске программы. Набор командной строки находится в том месте, где вы будете добавлять команды для вашей программы. Команда Run отображает список аргументов для подходящей команды.

      Сохраните и закройте файл.

      Вот команда, которая вам потребуется для создания и запуска программы:

      Вывод должен выглядеть так:

      Output

      NAME: tasker - A simple CLI program to manage your tasks USAGE: main [global options] command [command options] [arguments...] COMMANDS: help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --help, -h show help (default: false)

      Программа запускает и отображает текст справки, это полезно для изучения возможностей и способов использования программы.

      На следующих шагах вы будете повышать эффективность вашей программы, добавляя субкоманды для управления задачами в MongoDB.

      Шаг 3 — Создание задачи

      На этом шаге вы добавите субкоманду в вашу программу CLI при помощи пакета cli. В конце этого раздела вы сможете добавить новую задачу в базу данных MongoDB, используя новую команду add (добавить) в вашей программе CLI.

      Начнем с открытия файла main.go:

      Затем импортируем пакеты go.mongodb.org/mongo-driver/bson/primitive, time (времени) и errors (ошибок):

      main.go

      package main
      
      import (
          "context"
          "errors"
          "log"
          "os"
          "time"
      
          "github.com/urfave/cli/v2"
          "go.mongodb.org/mongo-driver/bson/primitive"
          "go.mongodb.org/mongo-driver/mongo"
          "go.mongodb.org/mongo-driver/mongo/options"
      )
      . . .
      

      Затем создадим новую структуру для представления одной задачи базе данных и ее вставки непосредственно перед функцией main:

      main.go

      . . .
      type Task struct {
          ID        primitive.ObjectID `bson:"_id"`
          CreatedAt time.Time          `bson:"created_at"`
          UpdatedAt time.Time          `bson:"updated_at"`
          Text      string             `bson:"text"`
          Completed bool               `bson:"completed"`
      }
      . . .
      

      Вы будете использовать пакет primitive для установки типа ID каждой задачи, поскольку MongoDB использует ObjectID для поля _idпо умолчанию. Еще одно действие MongoDB по умолчанию — имя поля из строчных букв используется в качестве ключа для каждого экспортированного поля, когда ему присваивают серию, но это можно изменять с использованием тегов структуры bson.

      Затем создадим функцию, которая получает экземпляр Task и сохраняет его в базе данных. Добавьте этот фрагмент кода после функции main:

      main.go

      . . .
      func createTask(task *Task) error {
          _, err := collection.InsertOne(ctx, task)
        return err
      }
      . . .
      

      Метод collection.InsertOne() добавляет выделенную задачу в набор баз данных и возвращает ID документа, который был добавлен. Поскольку вам не потребуется этот идентификатор, вы удаляете его, присваивая его оператору подчеркивания.

      Следующий шаг — добавление новой команды в программу диспетчера задач для создания новых задач. Назовем ее add:

      main.go

      . . .
      func main() {
          app := &cli.App{
              Name:  "tasker",
              Usage: "A simple CLI program to manage your tasks",
              Commands: []*cli.Command{
                  {
                      Name:    "add",
                      Aliases: []string{"a"},
                      Usage:   "add a task to the list",
                      Action: func(c *cli.Context) error {
                          str := c.Args().First()
                          if str == "" {
                              return errors.New("Cannot add an empty task")
                          }
      
                          task := &Task{
                              ID:        primitive.NewObjectID(),
                              CreatedAt: time.Now(),
                              UpdatedAt: time.Now(),
                              Text:      str,
                              Completed: false,
                          }
      
                          return createTask(task)
                      },
                  },
              },
          }
      
          err := app.Run(os.Args)
          if err != nil {
              log.Fatal(err)
          }
      }
      

      Каждая новая команда, добавляемая в вашу программу CLI, помещается в список командной строки. Каждый элемент включает имя, описание использования и действия. Это код, который будет запускаться при исполнении команд.

      В этом коде вы берете первый аргумент для add (добавления) и используете его для установки свойства Text новой задачи, а также присваиваете соответствующие значения по умолчанию другим свойствам. Новая задача передается далее в createTask, которая вносит задачу в базу данных и возвращает nil, если все идет хорошо, после чего команда прекращается.

      Сохраните и закройте файл.

      Протестируйте ее, добавив несколько задач с помощью команды add. В случае успешного выполнения на экране не будет ошибок:

      • go run main.go add "Learn Go"
      • go run main.go add "Read a book"

      Теперь, когда вы можете успешно добавлять задачи, давайте реализуем способ отображения всех задач, которые вы добавляли в базу данных.

      Шаг 4 — Перечисление всех задач

      Перечисление документов в коллекции можно сделать с помощью метода collection.Find(), который предполагает фильтр, а также указатель значения, в которое можно расшифровать результат. Его значение — Cursor, которое предоставляет поток документов, которые можно обрабатывать пошагово, расшифровывая отдельные документы по очереди. Затем Cursor закрывается после завершения его использования.

      Откройте файл main.go:

      Обязательно импортируйте пакет bson:

      main.go

      package main
      
      import (
          "context"
          "errors"
          "log"
          "os"
          "time"
      
          "github.com/urfave/cli/v2"
          "go.mongodb.org/mongo-driver/bson"
          "go.mongodb.org/mongo-driver/bson/primitive"
          "go.mongodb.org/mongo-driver/mongo"
          "go.mongodb.org/mongo-driver/mongo/options"
      )
      . . .
      

      Затем создайте следующие функции сразу после выполнения createTask:

      main.go

      . . .
      func getAll() ([]*Task, error) {
        // passing bson.D{{}} matches all documents in the collection
          filter := bson.D{{}}
          return filterTasks(filter)
      }
      
      func filterTasks(filter interface{}) ([]*Task, error) {
          // A slice of tasks for storing the decoded documents
          var tasks []*Task
      
          cur, err := collection.Find(ctx, filter)
          if err != nil {
              return tasks, err
          }
      
          for cur.Next(ctx) {
              var t Task
              err := cur.Decode(&t)
              if err != nil {
                  return tasks, err
              }
      
              tasks = append(tasks, &t)
          }
      
          if err := cur.Err(); err != nil {
              return tasks, err
          }
      
        // once exhausted, close the cursor
          cur.Close(ctx)
      
          if len(tasks) == 0 {
              return tasks, mongo.ErrNoDocuments
          }
      
          return tasks, nil
      }
      

      BSON (JSON в двоичном коде) — метод представления документов в базе данных MongoDB, а пакет bson это то, что помогает нам работать с объектами BSON в Go. Тип bson.D, используемый в функции getAll(), представляет документ BSON, а также используется в тех случаях, когда порядок свойств важен. Передавая bson.D{}} в качестве фильтра в Tasks(), вы указываете, что хотите сопоставить все документы в коллекции.

      В функции filterTasks() вы пошагово выполняете Cursor, возвращаемый методом collection.Find(), и расшифровываете каждый документ в экземпляр Task (задачи). Затем каждая задача добавляется в список задач, созданных при запуске функции. После завершения использования Cursor будет закрыт, а задачи будут возвращены список задач.

      Прежде чем вы создадите команду для перечисления всех задач, создадим функцию helper (помощник), которая принимает на себя долю задач и отображает стандартный вывод. Вы будете использовать пакет color для придания цвета выводимым результатам.

      Чтобы начать использовать этот пакет, установите его с помощью следующей команды:

      • go get gopkg.in/gookit/color.v1

      Вывод должен выглядеть так:

      Output

      go: downloading gopkg.in/gookit/color.v1 v1.1.6 go: gopkg.in/gookit/color.v1 upgrade => v1.1.6

      И импортируйте его в файл main.go вместе с пакетом fmt:

      main.go

      package main
      
      import (
          "context"
          "errors"
        "fmt"
          "log"
          "os"
          "time"
      
          "github.com/urfave/cli/v2"
          "go.mongodb.org/mongo-driver/bson"
          "go.mongodb.org/mongo-driver/bson/primitive"
          "go.mongodb.org/mongo-driver/mongo"
          "go.mongodb.org/mongo-driver/mongo/options"
          "gopkg.in/gookit/color.v1"
      )
      . . .
      

      Затем создайте новую функцию printTasks после функции main:

      main.go

      . . .
      func printTasks(tasks []*Task) {
          for i, v := range tasks {
              if v.Completed {
                  color.Green.Printf("%d: %sn", i+1, v.Text)
              } else {
                  color.Yellow.Printf("%d: %sn", i+1, v.Text)
              }
          }
      }
      . . .
      

      Функция printTasks берет на себя список задач, пошагово выполняет каждую задачу и отображает результат в стандартном выводе — зеленым цветом для обозначения завершенных задач и желтым — незавершенных задач.

      Теперь добавьте следующие выделенные строки для создания новой команды all в списке командной строки. Эта команда отобразит все дополнительные задачи в стандартном выводе:

      main.go

      . . .
      func main() {
          app := &cli.App{
              Name:  "tasker",
              Usage: "A simple CLI program to manage your tasks",
              Commands: []*cli.Command{
                  {
                      Name:    "add",
                      Aliases: []string{"a"},
                      Usage:   "add a task to the list",
                      Action: func(c *cli.Context) error {
                          str := c.Args().First()
                          if str == "" {
                              return errors.New("Cannot add an empty task")
                          }
      
                          task := &Task{
                              ID:        primitive.NewObjectID(),
                              CreatedAt: time.Now(),
                              UpdatedAt: time.Now(),
                              Text:      str,
                              Completed: false,
                          }
      
                          return createTask(task)
                      },
                  },
                  {
                      Name:    "all",
                      Aliases: []string{"l"},
                      Usage:   "list all tasks",
                      Action: func(c *cli.Context) error {
                          tasks, err := getAll()
                          if err != nil {
                              if err == mongo.ErrNoDocuments {
                                  fmt.Print("Nothing to see here.nRun `add 'task'` to add a task")
                                  return nil
                              }
      
                              return err
                          }
      
                          printTasks(tasks)
                          return nil
                      },
                  },
              },
          }
      
          err := app.Run(os.Args)
          if err != nil {
              log.Fatal(err)
          }
      }
      
      . . .
      

      Команда all извлекает все задачи, перечисленные в базе данных, и отображает их в стандартном выводе. Если задачи отсутствуют, вместо этого откроется подсказка с предложением добавить новую задачу.

      Сохраните и закройте файл.

      Создайте программу и запустите ее с помощью команды all:

      Она отобразит все задачи, которые вы добавили на данный момент:

      Output

      1: Learn Go 2: Read a book

      Теперь, когда вы можете просматривать все задачи базы данных, добавим возможность обозначать задачу как завершенную на следующем шаге.

      Шаг 5 — Завершение задачи

      На этом шаге вы создадите новую субкоманду с именем done, которая позволит обозначать существующую задачу в базе данных как завершенную. Для обозначения задачи как завершенной вы можете использовать метод collection.FindOneAndUpdate(). Это позволяет вам найти документ в коллекции и обновить некоторые или все его свойства. В этом методе требуется фильтр для определения местонахождения документа и обновления документа для описания операции. Оба созданы при помощи типов bson.D.

      Начнем с открытия файла main.go:

      Вставьте следующий фрагмент кода после функции filterTasks:

      main.go

      . . .
      func completeTask(text string) error {
          filter := bson.D{primitive.E{Key: "text", Value: text}}
      
          update := bson.D{primitive.E{Key: "$set", Value: bson.D{
              primitive.E{Key: "completed", Value: true},
          }}}
      
          t := &Task{}
          return collection.FindOneAndUpdate(ctx, filter, update).Decode(t)
      }
      . . .
      

      Функция совпадает с первым документом, где текстовое свойство равняется параметру text. В документе update (обновления) указано, что свойству completed (завершено) можно присвоить значение true (истина). Если в операции FindOneAndUpdate() имеется ошибка, то она будет возвращена командой completeTask(). В противном случае будет возвращено nil.

      Затем добавим новую команду done в вашу программу CLI (интерфейса командной строки), обозначающую задачу как завершенную:

      main.go

      . . .
      func main() {
          app := &cli.App{
              Name:  "tasker",
              Usage: "A simple CLI program to manage your tasks",
              Commands: []*cli.Command{
                  {
                      Name:    "add",
                      Aliases: []string{"a"},
                      Usage:   "add a task to the list",
                      Action: func(c *cli.Context) error {
                          str := c.Args().First()
                          if str == "" {
                              return errors.New("Cannot add an empty task")
                          }
      
                          task := &Task{
                              ID:        primitive.NewObjectID(),
                              CreatedAt: time.Now(),
                              UpdatedAt: time.Now(),
                              Text:      str,
                              Completed: false,
                          }
      
                          return createTask(task)
                      },
                  },
                  {
                      Name:    "all",
                      Aliases: []string{"l"},
                      Usage:   "list all tasks",
                      Action: func(c *cli.Context) error {
                          tasks, err := getAll()
                          if err != nil {
                              if err == mongo.ErrNoDocuments {
                                  fmt.Print("Nothing to see here.nRun `add 'task'` to add a task")
                                  return nil
                              }
      
                              return err
                          }
      
                          printTasks(tasks)
                          return nil
                      },
                  },
                  {
                      Name:    "done",
                      Aliases: []string{"d"},
                      Usage:   "complete a task on the list",
                      Action: func(c *cli.Context) error {
                          text := c.Args().First()
                          return completeTask(text)
                      },
                  },
              },
          }
      
          err := app.Run(os.Args)
          if err != nil {
              log.Fatal(err)
          }
      }
      
      . . .
      

      Вы используете аргумент, переданный команде done для нахождения первого документа, чье свойство text совпадает. В случае обнаружения свойству completed в документе будет привоено значение true.

      Сохраните и закройте файл.

      Затем запустите программу с помощью команды done:

      • go run main.go done "Learn Go"

      Если вы снова будете использовать команду all, то увидите, что задача, которая была обозначена как завершенная, теперь отображается зеленым.

      Скриншот вывода на терминал после завершения задачи

      Иногда вы захотите просматривать только те задачи, которые еще не выполнялись. На следующем шаге мы добавим эту функцию.

      Шаг 6 — Отображение только незавершенных задач

      На этом шаге вы будете включать код для извлечения незавершенных задач из базы данных с помощью драйвера MongoDB. Незавершенные задачи — те, чьему свойству completed присвоено значение false.

      Давайте добавим новую функцию, которая извлекает задачи, которые еще не выполнены. Откройте файл main.go:

      Добавьте этот фрагмент кода после функции completeTask:

      main.go

      . . .
      func getPending() ([]*Task, error) {
          filter := bson.D{
              primitive.E{Key: "completed", Value: false},
          }
      
          return filterTasks(filter)
      }
      . . .
      

      Вы создаете фильтр при помощи пакетов bson и primitive из драйвера MongoDB, который будет сопоставлять документы, чьему свойству completed присвоено значение false. Затем вызывающему выдается список незавершенных задач.

      Вместо создания новой команды для перечисления незавершенных задач давайте сделаем это действием по умолчанию при запуске программы без каких-либо команд. Вы можете сделать это, добавив в программу свойство Action следующим образом:

      main.go

      . . .
      func main() {
          app := &cli.App{
              Name:  "tasker",
              Usage: "A simple CLI program to manage your tasks",
              Action: func(c *cli.Context) error {
                  tasks, err := getPending()
                  if err != nil {
                      if err == mongo.ErrNoDocuments {
                          fmt.Print("Nothing to see here.nRun `add 'task'` to add a task")
                          return nil
                      }
      
                      return err
                  }
      
                  printTasks(tasks)
                  return nil
              },
              Commands: []*cli.Command{
                  {
                      Name:    "add",
                      Aliases: []string{"a"},
                      Usage:   "add a task to the list",
                      Action: func(c *cli.Context) error {
                          str := c.Args().First()
                          if str == "" {
                              return errors.New("Cannot add an empty task")
                          }
      
                          task := &Task{
                              ID:        primitive.NewObjectID(),
                              CreatedAt: time.Now(),
                              UpdatedAt: time.Now(),
                              Text:      str,
                              Completed: false,
                          }
      
                          return createTask(task)
                      },
                  },
      . . .
      

      Свойство Action выполняет действие по умолчанию, когда программа выполняется без каких-либо субкоманд. Здесь отображается логика перечисления незавершенных задач. Вызывается функция getPending(), и конечные задачи отображаются в стандартном выводе с помощью printTasks(). Если незавершенных задач нет, отображается подсказка, предлагающая пользователю добавить новую задачу с помощью команды add.

      Сохраните и закройте файл.

      Запуск программы сейчас без добавления каких-либо команд отобразит все незавершенные задачи в базе данных:

      Вывод должен выглядеть так:

      Output

      1: Read a book

      Теперь, когда вы можете указывать незавершенные задачи, добавим другую команду, которая позволяет просматривать только завершенные задачи.

      Шаг 7 — Отображение завершенных задач

      На этом шаге вы добавите новую субкоманду finished, которая извлекает задачи из базы данных и отображает их на экране. Это подразумевает фильтрацию и возврат задач, чьему свойству completed присвоено значение true.

      Откройте файл main.go:

      Добавьте следующий код в конце файла:

      main.go

      . . .
      func getFinished() ([]*Task, error) {
          filter := bson.D{
              primitive.E{Key: "completed", Value: true},
          }
      
          return filterTasks(filter)
      }
      . . .
      

      Как и в случае с функцией getPending(), вы добавили функцию getFinished(), которая возвращает список завершенных задач. В данном случае свойству completed в фильтре присвоено значение true, поэтому будут выведены только те документы, которые соответствуют этому состоянию.

      Затем создайте команду finished, выводящую все завершенные задачи:

      main.go

      . . .
      func main() {
          app := &cli.App{
              Name:  "tasker",
              Usage: "A simple CLI program to manage your tasks",
              Action: func(c *cli.Context) error {
                  tasks, err := getPending()
                  if err != nil {
                      if err == mongo.ErrNoDocuments {
                          fmt.Print("Nothing to see here.nRun `add 'task'` to add a task")
                          return nil
                      }
      
                      return err
                  }
      
                  printTasks(tasks)
                  return nil
              },
              Commands: []*cli.Command{
                  {
                      Name:    "add",
                      Aliases: []string{"a"},
                      Usage:   "add a task to the list",
                      Action: func(c *cli.Context) error {
                          str := c.Args().First()
                          if str == "" {
                              return errors.New("Cannot add an empty task")
                          }
      
                          task := &Task{
                              ID:        primitive.NewObjectID(),
                              CreatedAt: time.Now(),
                              UpdatedAt: time.Now(),
                              Text:      str,
                              Completed: false,
                          }
      
                          return createTask(task)
                      },
                  },
                  {
                      Name:    "all",
                      Aliases: []string{"l"},
                      Usage:   "list all tasks",
                      Action: func(c *cli.Context) error {
                          tasks, err := getAll()
                          if err != nil {
                              if err == mongo.ErrNoDocuments {
                                  fmt.Print("Nothing to see here.nRun `add 'task'` to add a task")
                                  return nil
                              }
      
                              return err
                          }
      
                          printTasks(tasks)
                          return nil
                      },
                  },
                  {
                      Name:    "done",
                      Aliases: []string{"d"},
                      Usage:   "complete a task on the list",
                      Action: func(c *cli.Context) error {
                          text := c.Args().First()
                          return completeTask(text)
                      },
                  },
                  {
                      Name:    "finished",
                      Aliases: []string{"f"},
                      Usage:   "list completed tasks",
                      Action: func(c *cli.Context) error {
                          tasks, err := getFinished()
                          if err != nil {
                              if err == mongo.ErrNoDocuments {
                                  fmt.Print("Nothing to see here.nRun `done 'task'` to complete a task")
                                  return nil
                              }
      
                              return err
                          }
      
                          printTasks(tasks)
                          return nil
                      },
                  },
              }
          }
      
          err := app.Run(os.Args)
          if err != nil {
              log.Fatal(err)
          }
      }
      . . .
      

      Команда finished извлекает задачи, чьему свойству completed присвоено значение true с помощью функции getFinished(). Затем она передает их в функцию printTasks, чтобы они отображались в стандартном выводе.

      Сохраните и закройте файл.

      Запустите следующую команду:

      Вывод должен выглядеть так:

      Output

      1: Learn Go

      На последнем шаге вы дадите пользователям возможность удалять задачи из базы данных.

      Шаг 8 — Удаление задачи

      На этом шаге вы добавите новую субкоманду delete (удаление), чтобы пользователи могли удалять задачу из базы данных. Для удаления одной задачи вы будете использовать метод collection.DeleteOne() из драйвера MongoDB. Также он использует фильтр для сопоставления удаляемого документа.

      Откройте файл main.go еще раз:

      Добавьте функцию deleteTask для удаления задач из базы данных непосредственно после функции getFinished:

      main.go

      . . .
      func deleteTask(text string) error {
          filter := bson.D{primitive.E{Key: "text", Value: text}}
      
          res, err := collection.DeleteOne(ctx, filter)
          if err != nil {
              return err
          }
      
          if res.DeletedCount == 0 {
              return errors.New("No tasks were deleted")
          }
      
          return nil
      }
      . . .
      

      В этом методе deleteTask имеется строковый аргумент, представляющий удаляемую задачу. Создается фильтр для сопоставления задачи, чьему свойству text присвоен строковый аргумент. Вы передаете фильтр в метод DeleteOne(), который сопоставляет элемент коллекции и удаляет его.

      Вы можете проверить свойство DeletedCount по результату метода DeleteOne, чтобы подтвердить, были ли удалены какие-либо документы. Если фильтр не сможет сопоставить удаляемый документ, то DeletedCount будет равно нулю, и вы сможете вывести ошибку.

      Теперь добавьте новую команду rm, как выделено:

      main.go

      . . .
      func main() {
          app := &cli.App{
              Name:  "tasker",
              Usage: "A simple CLI program to manage your tasks",
              Action: func(c *cli.Context) error {
                  tasks, err := getPending()
                  if err != nil {
                      if err == mongo.ErrNoDocuments {
                          fmt.Print("Nothing to see here.nRun `add 'task'` to add a task")
                          return nil
                      }
      
                      return err
                  }
      
                  printTasks(tasks)
                  return nil
              },
              Commands: []*cli.Command{
                  {
                      Name:    "add",
                      Aliases: []string{"a"},
                      Usage:   "add a task to the list",
                      Action: func(c *cli.Context) error {
                          str := c.Args().First()
                          if str == "" {
                              return errors.New("Cannot add an empty task")
                          }
      
                          task := &Task{
                              ID:        primitive.NewObjectID(),
                              CreatedAt: time.Now(),
                              UpdatedAt: time.Now(),
                              Text:      str,
                              Completed: false,
                          }
      
                          return createTask(task)
                      },
                  },
                  {
                      Name:    "all",
                      Aliases: []string{"l"},
                      Usage:   "list all tasks",
                      Action: func(c *cli.Context) error {
                          tasks, err := getAll()
                          if err != nil {
                              if err == mongo.ErrNoDocuments {
                                  fmt.Print("Nothing to see here.nRun `add 'task'` to add a task")
                                  return nil
                              }
      
                              return err
                          }
      
                          printTasks(tasks)
                          return nil
                      },
                  },
                  {
                      Name:    "done",
                      Aliases: []string{"d"},
                      Usage:   "complete a task on the list",
                      Action: func(c *cli.Context) error {
                          text := c.Args().First()
                          return completeTask(text)
                      },
                  },
                  {
                      Name:    "finished",
                      Aliases: []string{"f"},
                      Usage:   "list completed tasks",
                      Action: func(c *cli.Context) error {
                          tasks, err := getFinished()
                          if err != nil {
                              if err == mongo.ErrNoDocuments {
                                  fmt.Print("Nothing to see here.nRun `done 'task'` to complete a task")
                                  return nil
                              }
      
                              return err
                          }
      
                          printTasks(tasks)
                          return nil
                      },
                  },
                  {
                      Name:  "rm",
                      Usage: "deletes a task on the list",
                      Action: func(c *cli.Context) error {
                          text := c.Args().First()
                          err := deleteTask(text)
                          if err != nil {
                              return err
                          }
      
                          return nil
                      },
                  },
              }
          }
      
          err := app.Run(os.Args)
          if err != nil {
              log.Fatal(err)
          }
      }
      . . .
      

      Как и в случае со всеми остальными ранее добавленными субкомандами, команда rm использует первый аргумент для сопоставления задачи в базе данных и ее удаления.

      Сохраните и закройте файл.

      Вы можете указывать незавершенные задачи, запустив программу без передачи субкоманд:

      Output

      1: Read a book

      Запуск субкоманды rm в задаче «Читать книгу» удалит ее из базы данных:

      • go run main.go rm "Read a book"

      Если вы снова укажете все незавершенные задачи, то увидите, что задача «Читать книгу» больше не отображается, а вместо этого отображается подсказка, предлагающая создать новую задачу:

      Output

      Nothing to see here Run `add 'task'` to add a task

      На этом шаге вы добавили функцию для удаления задач из базы данных.

      Заключение

      Вы успешно создали программу «диспетчер задач» интерфейса командной строки и заодно научились основам применения драйвера MongoDB Go.

      Обязательно посмотрите полную документацию по драйверу MongoDB Go в GoDoc, чтобы узнать больше о функциях этого драйвера. Документация, описывающая применение агрегирования или транзакций, может представлять особый интерес для вас.

      Окончательный код этого обучающего руководства можно найти в репозитории GitHub.



      Source link