Slik legger du til nettleservarsler i React-appen din

Sjekk ut bøkene mine på Amazon på https://www.amazon.com/John-Au-Yeung/e/B08FT5NT62

Abonner på e-postlisten min nå på http://jauyeung.net/subscribe/

Med HTML5 Notification API kan nettlesere vise innebygde popup-varsler til brukere. Med varsler kan du vise tekst og ikoner, og også spille av lyd med dem. Den fullstendige listen over alternativer finner du på https://developer.mozilla.org/en-US/docs/Web/API/notification. Brukere må gi tillatelse til å vise varsler når de besøker en nettapp for å se nettleservarsler.

Utviklere har gjort det harde arbeidet for oss hvis vi bruker React fordi en React-komponent er opprettet for å vise nettleservarsler. React-Web-Notification-pakken, som ligger på https://www.npmjs.com/package/react-web-notification kan la oss vise popup-vinduer og håndtere hendelsene som er knyttet til visning av varslene som når bruk klikker på varselet eller håndtere saker når tillatelser eller gitt eller avslått for visningsvarsler.

I denne artikkelen vil vi bygge en passordbehandler som lar deg skrive inn, redigere og slette passord til nettsidene og vise varsler hver gang disse handlingene utføres. Vi vil bruke React til å bygge appen.

For å starte, kjører vi Create React App for å lage appen. Løpe:

npx create-react-app password-manager

for å lage appen. Deretter legger vi til våre egne biblioteker, vi vil bruke Axios for å lage HTTP-forespørsler til backend vår, Formik og Yup for henholdsvis skjemaverdihåndtering og skjemavalidering, MobX for statsadministrasjon, React Bootstrap for styling, React-Copy-To-Clipboard for å la oss kopiere data til utklippstavlen, og React Router for ruting.

Vi installerer dem ved å kjøre:

npm i axios formik mobx mobx-react react-bootstrap react-copy-to-clipboard react-router-fom yup react-web-notifications

Med alle bibliotekene installert, kan vi begynne å bygge appen vår. Vi lager alle filene i src mappe med mindre annet er spesifisert.

Først erstatter vi den eksisterende koden i App.css med:

.bg-primary {  
  background-color: #09d3ac !important;  
}

for å endre bakgrunnsfargen på topplinjen. Neste i App.js , erstatte gjeldende kode med:

import React from "react";  
import { Router, Route } from "react-router-dom";  
import HomePage from "./HomePage";  
import { createBrowserHistory as createHistory } from "history";  
import Navbar from "react-bootstrap/Navbar";  
import Nav from "react-bootstrap/Nav";  
import "./App.css";  
const history = createHistory();

function App({ passwordsStore }) {  
  return (  
    <div className="App">  
      <Router history={history}>  
        <Navbar bg="primary" expand="lg" variant="dark">  
          <Navbar.Brand href="#home">Password Manager</Navbar.Brand>  
          <Navbar.Toggle aria-controls="basic-navbar-nav" />  
          <Navbar.Collapse id="basic-navbar-nav">  
            <Nav className="mr-auto">  
              <Nav.Link href="/" active>Home</Nav.Link>  
            </Nav>  
          </Navbar.Collapse>  
        </Navbar>  
        <Route  
          path="/"  
          exact  
          component={props => (  
            <HomePage {...props} passwordsStore={passwordsStore} />  
          )}  
        />  
      </Router>  
    </div>  
  );  
}export default App;

for å legge til vår React Bootstrap topplinje og ruten vår til hjemmesiden. passwordStore er vår MobX-butikk for lagring av passordlisten vår i grensesnittet.

Opprett deretter HomePage.css og legg til:

.home-page {  
  padding: 20px;  
}

for å legge til litt utfylling på siden vår.

Deretter oppretter du HomePage.js og legg til:

import React from "react";  
import { useState, useEffect } from "react";  
import Table from "react-bootstrap/Table";  
import ButtonToolbar from "react-bootstrap/ButtonToolbar";  
import Button from "react-bootstrap/Button";  
import Modal from "react-bootstrap/Modal";  
import PasswordForm from "./PasswordForm";  
import "./HomePage.css";  
import { deletePassword, getPasswords } from "./requests";  
import { observer } from "mobx-react";  
import { CopyToClipboard } from "react-copy-to-clipboard";  
import Notification from "react-web-notification";

function HomePage({ passwordsStore }) {  
  const [openAddModal, setOpenAddModal] = useState(false);  
  const [openEditModal, setOpenEditModal] = useState(false);  
  const [initialized, setInitialized] = useState(false);  
  const [selectedPassword, setSelectedPassword] = useState({});  
  const [notificationTitle, setNotificationTitle] = React.useState(""); const openModal = () => {  
    setOpenAddModal(true);  
  }; 

  const closeModal = () => {  
    setOpenAddModal(false);  
    setOpenEditModal(false);  
    getData();  
  }; 

  const cancelAddModal = () => {  
    setOpenAddModal(false);  
  }; 

  const editPassword = contact => {  
    setSelectedPassword(contact);  
    setOpenEditModal(true);  
  }; 

  const cancelEditModal = () => {  
    setOpenEditModal(false);  
  }; 

  const getData = async () => {  
    const response = await getPasswords();  
    passwordsStore.setPasswords(response.data);  
    setInitialized(true);  
  }; 

  const deleteSelectedPassword = async id => {  
    await deletePassword(id);  
    setNotificationTitle("Password deleted");  
    getData();  
  }; 

  useEffect(() => {  
    if (!initialized) {  
      getData();  
    }  
  }); 

  return (  
    <div className="home-page">  
      <h1>Password Manager</h1>  
      <Modal show={openAddModal} onHide={closeModal}>  
        <Modal.Header closeButton>  
          <Modal.Title>Add Password</Modal.Title>  
        </Modal.Header>  
        <Modal.Body>  
          <PasswordForm  
            edit={false}  
            onSave={closeModal.bind(this)}  
            onCancelAdd={cancelAddModal}  
            passwordsStore={passwordsStore}  
          />  
        </Modal.Body>  
      </Modal> <Modal show={openEditModal} onHide={closeModal}>  
        <Modal.Header closeButton>  
          <Modal.Title>Edit Password</Modal.Title>  
        </Modal.Header>  
        <Modal.Body>  
          <PasswordForm  
            edit={true}  
            onSave={closeModal.bind(this)}  
            contact={selectedPassword}  
            onCancelEdit={cancelEditModal}  
            passwordsStore={passwordsStore}  
          />  
        </Modal.Body>  
      </Modal>  
      <ButtonToolbar onClick={openModal}>  
        <Button variant="outline-primary">Add Password</Button>  
      </ButtonToolbar>  
      <br />  
      <div className="table-responsive">  
        <Table striped bordered hover>  
          <thead>  
            <tr>  
              <th>Name</th>  
              <th>URL</th>  
              <th>Username</th>  
              <th>Password</th>  
              <th></th>  
              <th></th>  
              <th></th>  
              <th></th>  
            </tr>  
          </thead>  
          <tbody>  
            {passwordsStore.passwords.map(c => (  
              <tr key={c.id}>  
                <td>{c.name}</td>  
                <td>{c.url}</td>  
                <td>{c.username}</td>  
                <td>******</td>  
                <td>  
                  <CopyToClipboard text={c.username}>  
                    <Button  
                      variant="outline-primary"  
                      onClick={() => setNotificationTitle("Username copied")}  
                    >  
                      Copy Username to Clipboard  
                    </Button>  
                  </CopyToClipboard>  
                </td>  
                <td>  
                  <CopyToClipboard text={c.password}>  
                    <Button  
                      variant="outline-primary"  
                      onClick={() => setNotificationTitle("Password copied")}  
                    >  
                      Copy Password to Clipboard  
                    </Button>  
                  </CopyToClipboard>  
                </td>  
                <td>  
                  <Button  
                    variant="outline-primary"  
                    onClick={editPassword.bind(this, c)}  
                  >  
                    Edit  
                  </Button>  
                </td>  
                <td>  
                  <Button  
                    variant="outline-primary"  
                    onClick={deleteSelectedPassword.bind(this, c.id)}  
                  >  
                    Delete  
                  </Button>  
                </td>  
              </tr>  
            ))}  
          </tbody>  
        </Table>  
      </div> {notificationTitle ? (  
        <Notification  
          title={notificationTitle}  
          options={{  
            icon:  
              "http://mobilusoss.github.io/react-web-notification/example/Notifications_button_24.png"  
          }}  
          onClose={() => setNotificationTitle(undefined)}  
        />  
      ) : null}  
    </div>  
  );  
}  
export default observer(HomePage);

Denne komponenten er hjemmesiden til appen vår. Vi har en tabell for å vise listen over passord, en knapp for å legge til en påloggings- og passordoppføring, og knapper i hver rad i tabellen for å kopiere brukernavn og passord, og redigere og slette hver oppføring. Vi har kolonnene navn, URL, brukernavn og passord. CopyToClipboard komponenten lar oss kopiere dataene vi kopierer til text prop av komponenten. Enhver komponent kan være inne i denne komponenten. Vi har en React Bootstrap-modal for å legge til passord og en annen for redigering. PasswordForm er vårt skjema for å legge til passordoppføringene, som vi oppretter senere.

Vi viser varslene hver gang et brukernavn eller passord kopieres, og når en oppføring slettes. Vi gjør dette ved å angi varslingstittelen med setNotificationTitle funksjon. Vi legger til en onClose behandler i Notification komponent slik at varselet vises igjen når det er lukket.

Vi har openModal , closeModal , cancelAddModal og cancelEditModal funksjoner for å åpne og lukke modalene. I editPassword funksjonen kaller vi setSelectedPassword funksjon for å angi passordoppføringen som skal redigeres.

observer vi omslutter HomePage komponenten er for å la oss se de nyeste verdiene fra passwordsStore .

Deretter endrer vi index.js å ha:

import React from "react";  
import ReactDOM from "react-dom";  
import "./index.css";  
import App from "./App";  
import * as serviceWorker from "./serviceWorker";  
import { PasswordsStore } from "./store";  
const passwordsStore = new PasswordsStore();

ReactDOM.render(  
  <App passwordsStore={passwordsStore} />,  
  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](https://bit.ly/CRA-PWA)  
serviceWorker.unregister();

Vi sender inn vår PasswordStore MobX-butikken her, som vil sende den til alle de andre komponentene.

Deretter oppretter vi PasswordForm.js og legg til:

import React from "react";  
import { Formik } from "formik";  
import Form from "react-bootstrap/Form";  
import Col from "react-bootstrap/Col";  
import Button from "react-bootstrap/Button";  
import * as yup from "yup";  
import PropTypes from "prop-types";  
import { addPassword, getPasswords, editPassword } from "./requests";  
import Notification from "react-web-notification";

const schema = yup.object({  
  name: yup.string().required("Name is required"),  
  url: yup  
    .string()  
    .url()  
    .required("URL is required"),  
  username: yup.string().required("Username is required"),  
  password: yup.string().required("Password is required")  
});

function PasswordForm({  
  edit,  
  onSave,  
  contact,  
  onCancelAdd,  
  onCancelEdit,  
  passwordsStore  
}) {  
  const [notificationTitle, setNotificationTitle] = React.useState(""); 
  const handleSubmit = async evt => {  
    const isValid = await schema.validate(evt);  
    if (!isValid) {  
      return;  
    }  
    if (!edit) {  
      await addPassword(evt);  
      setNotificationTitle("Password added");  
    } else {  
      await editPassword(evt);  
      setNotificationTitle("Password edited");  
    }  
    const response = await getPasswords();  
    passwordsStore.setPasswords(response.data);  
    onSave();  
  }; 

  return (  
    <>  
      <Formik  
        validationSchema={schema}  
        onSubmit={handleSubmit}  
        initialValues={contact || {}}  
      >  
        {({  
          handleSubmit,  
          handleChange,  
          handleBlur,  
          values,  
          touched,  
          isInvalid,  
          errors  
        }) => (  
          <Form noValidate onSubmit={handleSubmit}>  
            <Form.Row>  
              <Form.Group as={Col} md="12" controlId="name">  
                <Form.Label>Name</Form.Label>  
                <Form.Control  
                  type="text"  
                  name="name"  
                  placeholder="Name"  
                  value={values.name || ""}  
                  onChange={handleChange}  
                  isInvalid={touched.name && errors.name}  
                />  
                <Form.Control.Feedback type="invalid">  
                  {errors.name}  
                </Form.Control.Feedback>  
              </Form.Group> <Form.Group as={Col} md="12" controlId="url">  
                <Form.Label>URL</Form.Label>  
                <Form.Control  
                  type="text"  
                  name="url"  
                  placeholder="URL"  
                  value={values.url || ""}  
                  onChange={handleChange}  
                  isInvalid={touched.url && errors.url}  
                />  
                <Form.Control.Feedback type="invalid">  
                  {errors.url}  
                </Form.Control.Feedback>  
              </Form.Group> <Form.Group as={Col} md="12" controlId="username">  
                <Form.Label>Username</Form.Label>  
                <Form.Control  
                  type="text"  
                  name="username"  
                  placeholder="Username"  
                  value={values.username || ""}  
                  onChange={handleChange}  
                  isInvalid={touched.username && errors.username}  
                />  
                <Form.Control.Feedback type="invalid">  
                  {errors.username}  
                </Form.Control.Feedback>  
              </Form.Group> <Form.Group as={Col} md="12" controlId="password">  
                <Form.Label>Password</Form.Label>  
                <Form.Control  
                  type="password"  
                  name="password"  
                  placeholder="Password"  
                  value={values.password || ""}  
                  onChange={handleChange}  
                  isInvalid={touched.password && errors.password}  
                />  
                <Form.Control.Feedback type="invalid">  
                  {errors.password}  
                </Form.Control.Feedback>  
              </Form.Group>  
            </Form.Row>  
            <Button type="submit" style={{ marginRight: "10px" }}>  
              Save  
            </Button>  
            <Button type="button" onClick={edit ? onCancelEdit : onCancelAdd}>  
              Cancel  
            </Button>  
          </Form>  
        )}  
      </Formik>  
      {notificationTitle ? (  
        <Notification  
          title={notificationTitle}  
          options={{  
            icon:  
              "http://mobilusoss.github.io/react-web-notification/example/Notifications_button_24.png"  
          }}  
          onClose={() => setNotificationTitle(undefined)}  
        />  
      ) : null}  
    </>  
  );  
}

PasswordForm.propTypes = {  
  edit: PropTypes.bool,  
  onSave: PropTypes.func,  
  onCancelAdd: PropTypes.func,  
  onCancelEdit: PropTypes.func,  
  contact: PropTypes.object,  
  contactsStore: PropTypes.object  
};

export default PasswordForm;

Her legger vi til vårt skjema for å la brukere skrive inn brukernavn og passord til nettsidene deres. Vi bruker Yup schema objektet vi opprettet øverst i koden vår for å sikre at alle feltene er angitt og sjekke at URL-en som er angitt faktisk er en URL. Vi bruker Formik komponent for å håndtere formen for input-endringer og få de nyeste verdiene.

Når skjemaet er kontrollert for å være gyldig innen schema.validate lover å løse til true , deretter addPassword eller editPassword funksjoner fra requests.js , som vi oppretter senere vil bli kalt avhengig av om brukeren legger til eller redigerer en oppføring. Når det lykkes, er getPasswords fra samme fil kalles, og deretter setPasswords fra passwordsStore kalles for å lagre passordene i butikken. Til slutt, onSave sendt inn fra rekvisittene i HomePage komponenten kalles for å lukke modalen.

Vi viser varslene hver gang en oppføring legges til eller redigeres, og når en oppføring slettes. Vi gjør dette ved å angi varslingstittelen med setNotificationTitle funksjon. Vi legger til en onClose behandler i Notification komponent slik at varselet vises igjen når det er lukket.

Deretter oppretter du requests.js og legg til:

const APIURL = 'http://localhost:3000';  
const axios = require('axios');

export const getPasswords = () => axios.get(`${APIURL}/passwords`);
export const addPassword = (data) => axios.post(`${APIURL}/passwords`, data);
export const editPassword = (data) => axios.put(`${APIURL}/passwords/${data.id}`, data);
export const deletePassword = (id) => axios.delete(`${APIURL}/passwords/${id}`);

for å la oss sende forespørsler til vår bakside for å lagre passordoppføringene.

Deretter oppretter vi MobX-butikken vår ved å lage store.js og legg til:

import { observable, action, decorate } from "mobx";

class PasswordsStore {  
  passwords = [];
  setPasswords(passwords) {  
    this.passwords = passwords;  
  }  
}

PasswordsStore = decorate(PasswordsStore, {  
  passwords: observable,  
  setPasswords: action  
});

export { PasswordsStore };

Vi har passwords feltet som kan observeres for den siste verdien hvis vi bryter observer funksjon levert av MobX utenfor en komponent. setPasswords brukes til å angi de siste passordoppføringene i butikken slik at de kan spres til komponentene.

Til slutt, i index.html , erstatter vi den eksisterende koden med:

<!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/](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>Password Manager</title>  
    <link  
      rel="stylesheet"  
      href="[https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css](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>

for å endre tittelen og legge til Bootstrap CSS.

Nå kan vi kjøre appen ved å kjøre set PORT=3001 && react-scripts start på Windows eller PORT=3006 react-scripts start på Linux.

For å starte bakenden, installerer vi først json-server pakke ved å kjøre npm i json-server . Gå deretter til prosjektmappen vår og kjør:

json-server --watch db.json

I db.json , endre teksten til:

{  
  "passwords": [  
  ]  
}

Så vi har passwords endepunkter definert i requests.js tilgjengelig.