Guten Tag, Freunde!
In diesem Artikel möchte ich Ihnen einige der Funktionen von modernem JavaScript und die vom Browser bereitgestellten Schnittstellen zeigen, die sich auf das Weiterleiten und Rendern von Seiten beziehen, ohne den Server zu kontaktieren.
Quellcode auf GitHub .
Sie können mit dem Code bei CodeSandbox spielen .
Bevor ich mit der Implementierung der Anwendung fortfahre, möchte ich Folgendes beachten:
- Wir werden eine der einfachsten clientseitigen Routing- und Rendering-Optionen implementieren. Einige komplexere und vielseitigere (wenn Sie so skalierbare) Methoden finden Sie hier
- Auf einen Server kann man überhaupt nicht verzichten. Wir werden den Verlauf der aktuellen Browsersitzung bearbeiten: Beim manuellen Neuladen der Seite bevorzugt der Browser den Server, d. H. versucht, eine nicht vorhandene Seite zu erhalten, was zu traurigen Konsequenzen in Form der Unfähigkeit führt, eine Verbindung herzustellen (meine Versuche, den Browser mithilfe eines Servicemitarbeiters zu täuschen, d. h. die an ihn gesendeten Anforderungen zu vertreten, waren erfolglos). Die einzige Aufgabe unseres primitiven Servers besteht darin, auf jede Anfrage in Form von index.html zu antworten. Dadurch kann der Browser zur Ausführung des Client-Skripts navigieren
- Wo immer möglich und angemessen, werden wir dynamische Importe verwenden. Sie können nur die angeforderten Ressourcen laden (zuvor konnte dies nur durch Aufteilen des Codes in Teile (Chunks) mithilfe von Modul-Buildern wie Webpack erfolgen), was sich positiv auf die Leistung auswirkt. Durch die Verwendung dynamischer Importe wird fast der gesamte Code asynchron, was im Allgemeinen auch gut ist, da der Programmfluss nicht blockiert wird.
So lass uns gehen.
Beginnen wir mit dem Server.
Erstellen Sie ein Verzeichnis, gehen Sie dorthin und initialisieren Sie das Projekt:
mkdir client-side-rendering
cd !$
yarn init -yp
//
npm init -y
Abhängigkeiten installieren:
yarn add express nodemon open-cli
//
npm i ...
- express - Node.js Framework, das das Erstellen eines Servers erheblich vereinfacht
- nodemon - ein Tool zum Starten und automatischen Neustarten eines Servers
- open-cli - Ein Tool, mit dem Sie eine Browser-Registerkarte an der Adresse öffnen können, an der der Server ausgeführt wird
Manchmal (sehr selten) öffnet open-cli einen Browser-Tab schneller als nodemon den Server startet. In diesem Fall laden Sie einfach die Seite neu.
Erstellen Sie index.js mit folgendem Inhalt:
const express = require('express')
const app = express()
const port = process.env.PORT || 1234
// src - , , index.html
// , , public
// index.html src
app.use(express.static('src'))
// index.html,
app.get('*', (_, res) => {
res.sendFile(`${__dirname}/index.html`, null, (err) => {
if (err) console.error(err)
})
})
app.listen(port, () => {
console.log(`Server is running on port ${port}`)
})
Erstellen Sie index.html ( Bootstrap wird für das Hauptdesign der Anwendung verwendet ):
<head>
...
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<header>
<nav>
<!-- "data-url" -->
<a data-url="home">Home</a>
<a data-url="project">Project</a>
<a data-url="about">About</a>
</nav>
</header>
<main></main>
<footer>
<p>© 2020. All rights reserved</p>
</footer>
<!-- "type" "module" -->
<script src="script.js" type="module"></script>
</body>
Erstellen Sie für zusätzliches Styling src / style.css:
body {
min-height: 100vh;
display: grid;
justify-content: center;
align-content: space-between;
text-align: center;
color: #222;
overflow: hidden;
}
nav {
margin-top: 1rem;
}
a {
font-size: 1.5rem;
cursor: pointer;
}
a + a {
margin-left: 2rem;
}
h1 {
font-size: 3rem;
margin: 2rem;
}
div {
margin: 2rem;
}
div > article {
cursor: pointer;
}
/* ! . */
div > article > * {
pointer-events: none;
}
footer p {
font-size: 1.5rem;
}
Fügen Sie einen Befehl hinzu, um den Server zu starten und eine Browser-Registerkarte in package.json zu öffnen:
"scripts": {
"dev": "open-cli http://localhost:1234 && nodemon index.js"
}
Wir führen diesen Befehl aus:
yarn dev
//
npm run dev
Weitermachen.
Erstellen Sie ein src / pages-Verzeichnis mit drei Dateien: home.js, project.js und about.js. Jede Seite ist ein standardmäßig exportiertes Objekt mit den Eigenschaften "Inhalt" und "URL".
home.js:
export default {
content: `<h1>Welcome to the Home Page</h1>`,
url: 'home'
}
project.js:
export default {
content: `<h1>This is the Project Page</h1>`,
url: 'project',
}
about.js:
export default {
content: `<h1>This is the About Page</h1>`,
url: 'about',
}
Fahren wir mit dem Hauptskript fort.
Darin verwenden wir den lokalen Speicher zum Speichern und rufen dann (nachdem der Benutzer zur Site zurückgekehrt ist) die aktuelle Seite und die Verlaufs- API ab , um den Browserverlauf zu verwalten.
Für den Speicher wird die setItem- Methode zum Schreiben von Daten verwendet , die zwei Parameter annehmen : den Namen der gespeicherten Daten und die Daten selbst, die in eine JSON-Zeichenfolge konvertiert wurden - localStorage.setItem ('pageName', JSON.stringify (url)).
Verwenden Sie zum Abrufen von Daten die Methode getItem , die den Namen der Daten verwendet. Die vom Speicher als JSON-Zeichenfolge empfangenen Daten werden (in unserem Fall) in eine reguläre Zeichenfolge konvertiert: JSON.parse (localStorage.getItem ('pageName')).
Wie für die Geschichte API werden wir zwei Methoden der Geschichte Objekt durch die vorgesehene Verwendung History - Schnittstelle : replaceState und pushstate .
Beide Methoden verwenden zwei erforderliche und einen optionalen Parameter: Statusobjekt, Titel und Pfad (URL) - history.pushState (Status, Titel [, URL]).
Das Statusobjekt wird verwendet, wenn das Ereignis "popstate" behandelt wird, das beim Objekt "window" auftritt, wenn der Benutzer in einen neuen Status wechselt (z. B. wenn die Zurück-Schaltfläche eines Browser-Bedienfelds gedrückt wird), um die vorherige Seite zu rendern.
Die URL wird verwendet, um den in der Adressleiste des Browsers angezeigten Pfad anzupassen.
Bitte beachten Sie, dass wir dank des dynamischen Imports beim Starten der Anwendung nur eine Seite laden: entweder die Startseite, wenn der Benutzer die Site zum ersten Mal besucht hat, oder die Seite, die er zuletzt angesehen hat. Sie können überprüfen, ob nur die benötigten Ressourcen geladen werden, indem Sie den Inhalt der Registerkarte Netzwerk der Entwicklertools überprüfen.
Erstellen Sie src / script.js:
class App {
//
#page = null
// :
//
constructor(container, page) {
this.$container = container
this.#page = page
//
this.$nav = document.querySelector('nav')
//
// -
this.route = this.route.bind(this)
//
//
this.#initApp(this.#page)
}
//
// url
async #initApp({ url }) {
//
// localhost:1234/home
history.replaceState({ pageName: `${url}` }, `${url} page`, url)
//
this.#render(this.#page)
//
this.$nav.addEventListener('click', this.route)
// "popstate" -
window.addEventListener('popstate', async ({ state }) => {
//
const newPage = await import(`./pages/${state.page}.js`)
//
this.#page = newPage.default
//
this.#render(this.#page)
})
}
//
//
#render({ content }) {
//
this.$container.innerHTML = content
}
//
async route({ target }) {
//
if (target.tagName !== 'A') return
//
const { url } = target.dataset
//
//
//
if (this.#page.url === url) return
//
const newPage = await import(`./pages/${url}.js`)
//
this.#page = newPage.default
//
this.#render(this.#page)
//
this.#savePage(this.#page)
}
//
#savePage({ url }) {
history.pushState({ pageName: `${url}` }, `${url} page`, url)
localStorage.setItem('pageName', JSON.stringify(url))
}
}
//
;(async () => {
//
const container = document.querySelector('main')
// "home"
const pageName = JSON.parse(localStorage.getItem('pageName')) ?? 'home'
//
const pageModule = await import(`./pages/${page}.js`)
//
const pageToRender = pageModule.default
// ,
new App(container, pageToRender)
})()
Ändern Sie den h1-Text im Markup:
<h1>Loading...</h1>
Wir starten den Server neu.
Ausgezeichnet. Alles funktioniert wie erwartet.
Bisher haben wir uns nur mit statischem Inhalt befasst, aber was ist, wenn wir Seiten mit dynamischem Inhalt rendern müssen? Ist es in diesem Fall möglich, auf den Client beschränkt zu sein, oder kann diese Aufgabe nur der Server ausführen?
Nehmen wir an, dass auf der Hauptseite eine Liste der Beiträge angezeigt wird. Wenn Sie auf einen Beitrag klicken, sollte die Seite mit ihrem Inhalt gerendert werden. Die Postseite sollte auch in localStorage bestehen bleiben und nach dem erneuten Laden der Seite gerendert werden (Registerkarte Browser schließen / öffnen).
Wir erstellen eine lokale Datenbank in Form eines benannten JS-Moduls - src / data / db.js:
export const posts = [
{
id: '1',
title: 'Post 1',
text: 'Some cool text 1',
date: new Date().toLocaleDateString(),
},
{
id: '2',
title: 'Post 2',
text: 'Some cool text 2',
date: new Date().toLocaleDateString(),
},
{
id: '3',
title: 'Post 3',
text: 'Some cool text 3',
date: new Date().toLocaleDateString(),
},
]
Erstellen Sie einen Post-Template-Generator (auch in Form von benannten Exporten: Bei dynamischen Importen sind benannte Exporte etwas bequemer als die Standard-Exporte) - src / templates / post.js:
//
export const postTemplate = ({ id, title, text, date }) => ({
content: `
<article id="${id}">
<h2>${title}</h2>
<p>${text}</p>
<time>${date}</time>
</article>
`,
// ,
// : `post/${id}`, post
//
//
url: `post#${id}`,
})
Erstellen Sie eine Hilfsfunktion, um einen Beitrag anhand seiner ID zu finden - src / helpers / find-post.js:
//
import { postTemplate } from '../templates/post.js'
export const findPost = async (id) => {
//
//
//
// ,
const { posts } = await import('../data/db.js')
//
const postToShow = posts.find((post) => post.id === id)
//
return postTemplate(postToShow)
}
Nehmen wir Änderungen an src / pages / home.js vor:
//
import { postTemplate } from '../templates/post.js'
//
export default {
content: async () => {
//
const { posts } = await import('../data/db.js')
//
return `
<h1>Welcome to the Home Page</h1>
<div>
${posts.reduce((html, post) => (html += postTemplate(post).content), '')}
</div>
`
},
url: 'home',
}
Lassen Sie uns src / script.js ein wenig reparieren:
//
import { findPost } from './helpers/find-post.js'
class App {
#page = null
constructor(container, page) {
this.$container = container
this.#page = page
this.$nav = document.querySelector('nav')
this.route = this.route.bind(this)
//
//
this.showPost = this.showPost.bind(this)
this.#initApp(this.#page)
}
#initApp({ url }) {
history.replaceState({ page: `${url}` }, `${url} page`, url)
this.#render(this.#page)
this.$nav.addEventListener('click', this.route)
window.addEventListener('popstate', async ({ state }) => {
//
const { page } = state
// post
if (page.includes('post')) {
//
const id = page.replace('post#', '')
//
this.#page = await findPost(id)
} else {
// ,
const newPage = await import(`./pages/${state.page}.js`)
//
this.#page = newPage.default
}
this.#render(this.#page)
})
}
async #render({ content }) {
this.$container.innerHTML =
// , ,
// ..
typeof content === 'string' ? content : await content()
//
this.$container.addEventListener('click', this.showPost)
}
async route({ target }) {
if (target.tagName !== 'A') return
const { url } = target.dataset
if (this.#page.url === url) return
const newPage = await import(`./pages/${url}.js`)
this.#page = newPage.default
this.#render(this.#page)
this.#savePage(this.#page)
}
//
async showPost({ target }) {
//
// : div > article > * { pointer-events: none; } ?
// , , article,
// , .. e.target
if (target.tagName !== 'ARTICLE') return
//
this.#page = await findPost(target.id)
this.#render(this.#page)
this.#savePage(this.#page)
}
#savePage({ url }) {
history.pushState({ page: `${url}` }, `${url} page`, url)
localStorage.setItem('pageName', JSON.stringify(url))
}
}
;(async () => {
const container = document.querySelector('main')
const pageName = JSON.parse(localStorage.getItem('pageName')) ?? 'home'
let pageToRender = ''
// "post" ..
// . popstate
if (pageName.includes('post')) {
const id = pageName.replace('post#', '')
pageToRender = await findPost(id)
} else {
const pageModule = await import(`./pages/${pageName}.js`)
pageToRender = pageModule.default
}
new App(container, pageToRender)
})()
Wir starten den Server neu.
Die Anwendung funktioniert, stimmt jedoch zu, dass die Struktur des Codes in seiner aktuellen Form zu wünschen übrig lässt. Dies kann beispielsweise durch die Einführung einer zusätzlichen Klasse "Router" verbessert werden, die das Routing von Seiten und Posts kombiniert. Wir werden jedoch die funktionale Programmierung durchlaufen.
Erstellen wir eine weitere Hilfsfunktion - src / helpers / check-page-name.js:
//
import { findPost } from './find-post.js'
export const checkPageName = async (pageName) => {
let pageToRender = ''
if (pageName.includes('post')) {
const id = pageName.replace('post#', '')
pageToRender = await findPost(id)
} else {
const pageModule = await import(`../pages/${pageName}.js`)
pageToRender = pageModule.default
}
return pageToRender
}
Lassen Sie uns src / templates / post.js ein wenig ändern, nämlich: Ersetzen Sie das Attribut "id" des Tags "article" durch das Attribut "data-url" durch den Wert "post # $ {id}":
<article data-url="post#${id}">
Die endgültige Überarbeitung von src / script.js sieht folgendermaßen aus:
import { checkPageName } from './helpers/check-page-name.js'
class App {
#page = null
constructor(container, page) {
this.$container = container
this.#page = page
this.route = this.route.bind(this)
this.#initApp()
}
#initApp() {
const { url } = this.#page
history.replaceState({ pageName: `${url}` }, `${url} page`, url)
this.#render(this.#page)
document.addEventListener('click', this.route, { passive: true })
window.addEventListener('popstate', async ({ state }) => {
const { pageName } = state
this.#page = await checkPageName(pageName)
this.#render(this.#page)
})
}
async #render({ content }) {
this.$container.innerHTML =
typeof content === 'string' ? content : await content()
}
async route({ target }) {
if (target.tagName !== 'A' && target.tagName !== 'ARTICLE') return
const { link } = target.dataset
if (this.#page.url === link) return
this.#page = await checkPageName(link)
this.#render(this.#page)
this.#savePage(this.#page)
}
#savePage({ url }) {
history.pushState({ pageName: `${url}` }, `${url} page`, url)
localStorage.setItem('pageName', JSON.stringify(url))
}
}
;(async () => {
const container = document.querySelector('main')
const pageName = JSON.parse(localStorage.getItem('pageName')) ?? 'home'
const pageToRender = await checkPageName(pageName)
new App(container, pageToRender)
})()
Wie Sie sehen können, bietet uns die Verlaufs-API in Verbindung mit dem dynamischen Import interessante Funktionen, die das Erstellen von Single-Page-Anwendungen (SPA) ohne Serverbeteiligung erheblich vereinfachen.
Wenn Sie nicht wissen, wo Sie mit der Entwicklung Ihrer Anwendung beginnen sollen, beginnen Sie mit der Modern HTML Starter Template .
Kürzlich habe ich eine kleine Untersuchung zu JavaScript-Entwurfsmustern abgeschlossen. Die Ergebnisse können hier eingesehen werden .
Ich hoffe, Sie haben etwas Interessantes für sich gefunden. Vielen Dank für Ihre Aufmerksamkeit.