React - отличный фреймворк для создания интерактивных веб-приложений. Он поставляется с минимальным набором функций. Он может отображать вашу страницу при обновлении данных и предоставляет удобный синтаксис для упрощения написания кода. Мы можем сделать наши приложения мобильными с помощью таких UI-фреймворков, как Bootstrap.
Bootstrap поставляется с классами CSS, которые мы можем применять к нашим элементам, чтобы выборочно отображать их на экране разной ширины, что дает нам большую гибкость при работе с отображаемыми элементами с экранами разных размеров.
В этой истории мы создадим приложение, использующее API New York Times с многоязычными возможностями. Вы можете просмотреть статический текст в приложении на английском или французском языках. Приложение может получать новости из разных разделов, а также выполнять поиск.
Перед созданием приложения вам необходимо зарегистрироваться для получения ключа API на https://developer.nytimes.com/.
Чтобы начать сборку приложения, мы используем утилиту командной строки Create React App для генерации кода каркаса. Чтобы использовать его, мы запускаем npx create-react-app nyt-app
, чтобы создать код в папке nyt-app
.
После этого нам нужно установить некоторые библиотеки. Нам нужен HTTP-клиент Axios, библиотека для преобразования объектов в строки запроса, библиотека Bootstrap, чтобы все выглядело лучше, и React Router для маршрутизации и простого создания форм с Formik и Yup.
Для перевода и локализации мы используем библиотеку React-i18next, которая позволяет нам переводить наш текст на английский и французский языки. Для установки библиотек запускаем npm i axios bootstrap formik i18next i18next-browser-languagedetector i18next-xhr-backend querystring react-bootstrap react-i18next react-router-dom yup
.
Теперь, когда у нас установлены все библиотеки, мы можем приступить к написанию кода. Для простоты помещаем все в папку src
. Начнем с изменения App.js
. Заменим существующий код на:
import React from "react"; import { Router, Route, Link } from "react-router-dom"; import HomePage from "./HomePage"; import TopBar from "./TopBar"; import { createBrowserHistory as createHistory } from "history"; import "./App.css"; import SearchPage from "./SearchPage"; import { useTranslation } from "react-i18next"; import { useState, useEffect } from "react"; const history = createHistory(); function App() { const { t, i18n } = useTranslation(); const [initialized, setInitialized] = useState(false); const changeLanguage = lng => { i18n.changeLanguage(lng); }; useEffect(() => { if (!initialized) { changeLanguage(localStorage.getItem("language") || "en"); setInitialized(true); } }); return ( <div className="App"> <Router history={history}> <TopBar /> <Route path="/" exact component={HomePage} /> <Route path="/search" exact component={SearchPage} /> </Router> </div> ); } export default App;
Это корневой компонент нашего приложения, который загружается при первой загрузке приложения.
Мы используем функцию useTranslation
, а библиотека react-i18next
возвращает объект со свойством t
и свойством i18n
. Здесь мы деструктурировали свойства возвращенного объекта в его собственные переменные.
Мы будем использовать t
, который принимает ключ перевода, чтобы получить английский или французский текст в зависимости от установленного языка. В этом файле мы используем функцию i18n
, чтобы установить язык с помощью предоставленной функции i18n.changeLanguage
. Мы также устанавливаем язык из локального хранилища, если он предоставляется, чтобы выбранный язык сохранялся после каждого обновления.
Мы также добавляем сюда маршруты для наших страниц, которые будут использоваться маршрутизатором React.
В App.css
мы помещаем:
.center { text-align: center; }
Это центрирует текст.
Далее делаем домашнюю страницу. СоздаемHomePage.js
, а в файл помещаем:
import React from "react"; import { useState, useEffect } from "react"; import Form from "react-bootstrap/Form"; import ListGroup from "react-bootstrap/ListGroup"; import Card from "react-bootstrap/Card"; import Button from "react-bootstrap/Button"; import { getArticles } from "./requests"; import { useTranslation } from "react-i18next"; import "./HomePage.css"; const sections = `arts, automobiles, books, business, fashion, food, health, home, insider, magazine, movies, national, nyregion, obituaries, opinion, politics, realestate, science, sports, sundayreview, technology, theater, tmagazine, travel, upshot, world` .replace(/ /g, "") .split(","); function HomePage() { const [selectedSection, setSelectedSection] = useState("arts"); const [articles, setArticles] = useState([]); const [initialized, setInitialized] = useState(false); const { t, i18n } = useTranslation(); const load = async section => { setSelectedSection(section); const response = await getArticles(section); setArticles(response.data.results || []); }; const loadArticles = async e => { if (!e || !e.target) { return; } setSelectedSection(e.target.value); load(e.target.value); }; const initializeArticles = () => { load(selectedSection); setInitialized(true); }; useEffect(() => { if (!initialized) { initializeArticles(); } }); return ( <div className="HomePage"> <div className="col-12"> <div className="row"> <div className="col-md-3 d-none d-md-block d-lg-block d-xl-block"> <ListGroup className="sections"> {sections.map(s => ( <ListGroup.Item key={s} className="list-group-item" active={s == selectedSection} > <a className="link" onClick={() => { load(s); }} > {t(s)} </a> </ListGroup.Item> ))} </ListGroup> </div> <div className="col right"> <Form className="d-sm-block d-md-none d-lg-none d-xl-none"> <Form.Group controlId="section"> <Form.Label>{t("Section")}</Form.Label> <Form.Control as="select" onChange={loadArticles} value={selectedSection} > {sections.map(s => ( <option key={s} value={s}>{t(s)}</option> ))} </Form.Control> </Form.Group> </Form> <h1>{t(selectedSection)}</h1> {articles.map((a, i) => ( <Card key={i}> <Card.Body> <Card.Title>{a.title}</Card.Title> <Card.Img variant="top" className="image" src={ Array.isArray(a.multimedia) && a.multimedia[a.multimedia.length - 1] ? a.multimedia[a.multimedia.length - 1].url : null } /> <Card.Text>{a.abstract}</Card.Text> <Button variant="primary" onClick={() => (window.location.href = a.url)} > {t("Go")} </Button> </Card.Body> </Card> ))} </div> </div> </div> </div> ); } export default HomePage;
В этом файле мы отображаем адаптивный макет, в котором есть левая панель, когда экран широкий, и раскрывающийся список на правой панели, если его нет.
Когда мы отображаем элементы в выбранном разделе, мы выбираем их на левой панели или в раскрывающемся списке.
Для отображения элементов мы используем виджет Card из React Bootstrap.
Мы также используем функцию t
, предоставляемую react-i18next
, для загрузки текста из нашего файла перевода, который мы создадим.
Чтобы загрузить начальные записи статей, мы запускаем функцию в обратном вызове функции useEffect
, чтобы показать загрузку элементов один раз из API New York Times. Нам нужен флаг initialized
, чтобы функция в обратном вызове не загружалась при каждом повторном рендеринге. В раскрывающемся списке мы добавили код для загрузки статей при изменении выбора.
Чтобы сделать макет адаптивным, мы используем утилиты отображения Bootstrap для отображения левой панели и раскрывающегося списка в зависимости от размера экрана. Левая панель отображается только на более широких экранах при добавлении классов d-none d-md-block d-lg-block d-xl-block
, а раскрывающийся список отображается на узких экранах при добавлении классов d-sm-block d-md-none d-lg-none d-xl-none
.
Точки останова для sm
, md
, lg
и xl
перечислены здесь. sm
- менее 576 пикселей в ширину, md
- от 576 до 767 пикселей в ширину, lg
- от 768 до 991 пикселей в ширину и xl
- от 992 до 1200 пикселей в ширину.
Документация Bootstrap перечисляет все классы для отображения и скрытия на разных типах экранов.
Затем создаемHomePage.css
, добавляя:
.link { cursor: pointer; } .right { padding: 20px; } .image { max-width: 400px; text-align: center; } .sections { margin-top: 20px; }
Мы меняем стиль курсора для кнопки Go и добавляем отступ на правой панели.
Далее мы создаем файл для загрузки переводов и установки языка по умолчанию. Создайте файл с именем i18n.js
и добавьте:
import i18n from "i18next"; import { initReactI18next } from "react-i18next"; import { resources } from "./translations"; import Backend from "i18next-xhr-backend"; import LanguageDetector from "i18next-browser-languagedetector"; i18n .use(Backend) .use(LanguageDetector) .use(initReactI18next) .init({ resources, lng: "en", fallbackLng: "en", debug: true, interpolation: { escapeValue: false, }, }); export default i18n;
В этом файле мы загружаем переводы из файла и устанавливаем язык по умолчанию на английский. Поскольку react-i18next
ускользает от всего, мы можем установить escapeValue
в false
для interpolation
, поскольку это избыточно.
Нам нужен файл для размещения кода для выполнения HTTP-запросов. Для этого мы создаем файл с именем requests.js
и добавляем:
const APIURL = "https://api.nytimes.com/svc"; const axios = require("axios"); const querystring = require("querystring"); export const search = data => { Object.keys(data).forEach(key => { data["api-key"] = process.env.REACT_APP_APIKEY; if (!data[key]) { delete data[key]; } }); return axios.get( `${APIURL}/search/v2/articlesearch.json?${querystring.encode(data)}` ); }; export const getArticles = section => axios.get( `${APIURL}/topstories/v2/${section}.json?api-key=${process.env.REACT_APP_APIKEY}` );
Мы загружаем ключ API из переменной process.env.REACT_APP_APIKEY
, которая предоставляется переменной среды в файле .env
, расположенном в корневой папке. Вы должны создать это сами. Там положите:
REACT_APP_APIKEY='you New York Times API key'
Замените значение справа на ключ API, который вы получили после регистрации на веб-сайте API New York Times.
Далее мы создаем страницу поиска. Создайте файл с именем SearchPage.js
и добавьте:
import React from "react"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import "./SearchPage.css"; import * as yup from "yup"; import { Formik } from "formik"; import Form from "react-bootstrap/Form"; import Col from "react-bootstrap/Col"; import Button from "react-bootstrap/Button"; import { Trans } from "react-i18next"; import { search } from "./requests"; import Card from "react-bootstrap/Card"; const schema = yup.object({ keyword: yup.string().required("Keyword is required"), }); function SearchPage() { const { t } = useTranslation(); const [articles, setArticles] = useState([]); const [count, setCount] = useState(0); const handleSubmit = async e => { const response = await search({ q: e.keyword }); setArticles(response.data.response.docs || []); }; return ( <div className="SearchPage"> <h1 className="center">{t("Search")}</h1> <Formik validationSchema={schema} onSubmit={handleSubmit}> {({ handleSubmit, handleChange, handleBlur, values, touched, isInvalid, errors, }) => ( <Form noValidate onSubmit={handleSubmit} className="form"> <Form.Row> <Form.Group as={Col} md="12" controlId="keyword"> <Form.Label>{t("Keyword")}</Form.Label> <Form.Control type="text" name="keyword" placeholder={t("Keyword")} value={values.keyword || ""} onChange={handleChange} isInvalid={touched.keyword && errors.keyword} /> <Form.Control.Feedback type="invalid"> {errors.keyword} </Form.Control.Feedback> </Form.Group> </Form.Row> <Button type="submit" style={{ marginRight: "10px" }}> {t("Search")} </Button> </Form> )} </Formik> <h3 className="form"> <Trans i18nKey="numResults" count={articles.length}> There are <strong>{{ count }}</strong> results. </Trans> </h3> {articles.map((a, i) => ( <Card key={i}> <Card.Body> <Card.Title>{a.headline.main}</Card.Title> <Card.Text>{a.abstract}</Card.Text> <Button variant="primary" onClick={() => (window.location.href = a.web_url)} > {t("Go")} </Button> </Card.Body> </Card> ))} </div> ); } export default SearchPage;
Здесь мы создаем форму поиска с полем ключевого слова, используемым для поиска в API. Когда пользователь нажимает кнопку "Поиск", он будет искать в API New York Times статьи с использованием этого ключевого слова.
Мы используем Formik для обработки изменений значений формы и для того, чтобы значения были доступны в объекте e
в параметре handleSubmit
, который мы можем использовать. Мы используем React Bootstrap для кнопок, элементов формы и карточек. После нажатия кнопки «Поиск» устанавливается переменная articles
и загружаются карточки для статей.
Мы используем компонент Trans
, предоставленный react-i18next
, для перевода текста, который имеет некоторые динамические компоненты, как в примере выше. У нас есть переменная в тексте для количества результатов. Всякий раз, когда у вас есть что-то подобное, вы заключаете его в компонент Trans
, а затем передаете переменные, как в приведенном выше примере, передавая переменные как свойства. Затем вы покажете переменную в тексте между тегами Trans
.
Мы также сделаем интерполяцию доступной в переводах, добавив “There are <1>{{count}}</1> results.”
в английский перевод и “Il y a <1>{{count}}</1> résultats.”
во французский перевод.
Тег 1
соответствует тегу strong
. Номер в данном случае произвольный. Пока шаблон согласуется с шаблоном компонента, он будет работать. Таким образом, тег strong
в этом случае всегда должен быть 1
в строке перевода.
Чтобы добавить упомянутые выше переводы вместе с остальными переводами, создайте файл с именем translations.js
и добавьте:
const resources = { en: { translation: { "New York Times App": "New York Times App", arts: "Arts", automobiles: "Automobiles", books: "Books", business: "Business", fashion: "Fashion", food: "Food", health: "Health", home: "Home", insider: "Inside", magazine: "Magazine", movies: "Movies", national: "National", nyregion: "New York Region", obituaries: "Obituaries", opinion: "Opinion", politics: "Politics", realestate: "Real Estate", science: "Science", sports: "Sports", sundayreview: "Sunday Review", technology: "Technology", theater: "Theater", tmagazine: "T Magazine", travel: "Travel", upshot: "Upshot", world: "World", Search: "Search", numResults: "There are <1>{{count}}</1> results.", Home: "Home", Search: "Search", Language: "Language", English: "English", French: "French", Keyword: "Keyword", Go: "Go", Section: "Section", }, }, fr: { translation: { "New York Times App": "App New York Times", arts: "Arts", automobiles: "Les automobiles", books: "Livres", business: "Entreprise", fashion: "Mode", food: "Aliments", health: "Santé", home: "Maison", insider: "Initiée", magazine: "Magazine", movies: "Films", national: "Nationale", nyregion: "La région de new york", obituaries: "Notices nécrologiques", opinion: "Opinion", politics: "Politique", realestate: "Immobilier", science: "Science", sports: "Des sports", sundayreview: "Avis dimanche", technology: "La technologie", theater: "Théâtre", tmagazine: "Magazine T", travel: "Voyage", upshot: "Résultat", world: "Monde", Search: "Search", numResults: "Il y a <1>{{count}}</1> résultats.", Home: "Page d'accueil", Search: "Chercher", Language: "La langue", English: "Anglais", French: "Français", Keyword: "Mot-clé", Go: "Aller", Section: "Section", }, }, }; export { resources };
У нас есть статические переводы текста и интерполированный текст, о котором мы упоминали выше, в этом файле.
Наконец, мы создаем верхнюю панель, создав TopBar.js
и добавив:
import React from "react"; import Navbar from "react-bootstrap/Navbar"; import Nav from "react-bootstrap/Nav"; import NavDropdown from "react-bootstrap/NavDropdown"; import "./TopBar.css"; import { withRouter } from "react-router-dom"; import { useTranslation } from "react-i18next"; function TopBar({ location }) { const { pathname } = location; const { t, i18n } = useTranslation(); const changeLanguage = lng => { localStorage.setItem("language", lng); i18n.changeLanguage(lng); }; return ( <Navbar bg="primary" expand="lg" variant="dark"> <Navbar.Brand href="#home">{t("New York Times App")}</Navbar.Brand> <Navbar.Toggle aria-controls="basic-navbar-nav" /> <Navbar.Collapse id="basic-navbar-nav"> <Nav className="mr-auto"> <Nav.Link href="/" active={pathname == "/"}> {t("Home")} </Nav.Link> <Nav.Link href="/search" active={pathname.includes("/search")}> {t("Search")} </Nav.Link> <NavDropdown title={t("Language")} id="basic-nav-dropdown"> <NavDropdown.Item onClick={() => changeLanguage("en")}> {t("English")} </NavDropdown.Item> <NavDropdown.Item onClick={() => changeLanguage("fr")}> {t("French")} </NavDropdown.Item> </NavDropdown> </Nav> </Navbar.Collapse> </Navbar> ); } export default withRouter(TopBar);
Мы используем компонент NavBar
, предоставляемый React Boostrap, и добавляем раскрывающийся список для пользователей, чтобы выбрать язык, и когда они щелкают эти элементы, они могут установить язык.
Обратите внимание, что мы обернули компонент TopBar
функцией withRouter
, чтобы получить значение текущего маршрута с помощью свойства location
и использовать его, чтобы установить, какая ссылка активна, установив свойство active
в компонентах Nav.Link
.
Наконец, мы заменяем существующий код в index.html
на:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="theme-color" content="#000000" /> <meta name="description" content="Web site created using create-react-app" /> <link rel="apple-touch-icon" href="logo192.png" /> <!-- manifest.json provides metadata used when your web app is installed on a user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ --> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <!-- Notice the use of %PUBLIC_URL% in the tags above. It will be replaced with the URL of the `public` folder during the build. Only files inside the `public` folder can be referenced from the HTML. Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> <title>React New York Times App</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous" /> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> <!-- This HTML file is a template. If you open it directly in the browser, you will see an empty page. You can add webfonts, meta tags, or analytics to this file. The build step will place the bundled scripts into the <body> tag. To begin the development, run `npm start` or `yarn start`. To create a production bundle, use `npm run build` or `yarn build`. --> </body> </html>
Это добавляет CSS Bootstrap и изменяет заголовок приложения.
После того, как все работы проделаны, при запуске npm start
получаем: