Bonne journée, mes amis!
Dans cet article, je souhaite vous montrer certaines des fonctionnalités du JavaScript moderne et les interfaces fournies par le navigateur liées au routage et au rendu des pages sans contacter le serveur.
Code source sur GitHub .
Vous pouvez jouer avec le code sur CodeSandbox .
Avant de procéder à la mise en œuvre de l'application, je tiens à noter ce qui suit:
- Nous mettrons en œuvre l'une des options de routage et de rendu client les plus simples, quelques méthodes plus complexes et polyvalentes (évolutives, si vous voulez) peuvent être trouvées ici
- . : , .. , ( -, .. , ). index.html .
- Lorsque cela est possible et approprié, nous utiliserons les importations dynamiques. Il vous permet de charger uniquement les ressources demandées (auparavant, cela ne pouvait être fait qu'en divisant le code en parties (morceaux) à l'aide de générateurs de modules comme Webpack), ce qui est bon pour les performances. L'utilisation d'importations dynamiques rendra presque tout notre code asynchrone, ce qui, en général, est également bon, car cela évite de bloquer le déroulement du programme.
Alors allons-y.
Commençons par le serveur.
Créez un répertoire, accédez-y et initialisez le projet:
mkdir client-side-rendering
cd !$
yarn init -yp
//
npm init -y
Installer les dépendances:
yarn add express nodemon open-cli
//
npm i ...
- express - Framework Node.js qui facilite la création d'un serveur
- nodemon - un outil pour démarrer et redémarrer automatiquement un serveur
- open-cli - un outil qui vous permet d'ouvrir un onglet de navigateur à l'adresse où le serveur s'exécute
Parfois (très rarement) open-cli ouvre un onglet de navigateur plus rapidement que nodemon démarre le serveur. Dans ce cas, rechargez simplement la page.
Créez index.js avec le contenu suivant:
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}`)
})
Créez index.html ( Bootstrap sera utilisé pour le style principal de l'application ):
<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>
Pour un style supplémentaire, créez 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;
}
Ajoutez une commande pour démarrer le serveur et ouvrir un onglet de navigateur dans package.json:
"scripts": {
"dev": "open-cli http://localhost:1234 && nodemon index.js"
}
Nous exécutons cette commande:
yarn dev
//
npm run dev
Passer à autre chose.
Créez un répertoire src / pages avec trois fichiers: home.js, project.js et about.js. Chaque page est un objet exporté par défaut avec les propriétés "content" et "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',
}
Passons au script principal.
Dans ce document, nous utiliserons le stockage local pour enregistrer, puis (après le retour de l'utilisateur sur le site) récupérer la page actuelle et l' API History pour gérer l'historique du navigateur.
En ce qui concerne le stockage, la méthode setItem est utilisée pour écrire des données , qui prend deux paramètres: le nom des données stockées et les données elles-mêmes, converties en une chaîne JSON - localStorage.setItem ('pageName', JSON.stringify (url)).
Pour obtenir des données, utilisez la méthode getItem , qui prend le nom des données; les données reçues du stockage sous forme de chaîne JSON sont converties en une chaîne régulière (dans notre cas): JSON.parse (localStorage.getItem ('pageName')).
Comme pour l'API History, nous utiliserons deux méthodes de l'objet history fourni par l'interface History : replaceState et pushState .
Les deux méthodes acceptent deux paramètres obligatoires et un optionnel: un objet d'état, un titre et un chemin (URL) - history.pushState (état, titre [, url]).
L'objet state est utilisé lors de la gestion de l'événement "popstate" qui se produit sur l'objet "window" lorsque l'utilisateur passe à un nouvel état (par exemple, lorsque le bouton Précédent d'un panneau de contrôle du navigateur est enfoncé) pour afficher la page précédente.
L'URL est utilisée pour personnaliser le chemin affiché dans la barre d'adresse du navigateur.
Veuillez noter que grâce à l'import dynamique, nous ne chargeons qu'une seule page lors du lancement de l'application: soit la page d'accueil, si l'utilisateur a visité le site pour la première fois, soit la page qu'il a consultée en dernier. Vous pouvez vérifier que seules les ressources dont vous avez besoin sont en cours de chargement en examinant le contenu de l'onglet Réseau des outils de développement.
Créez 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)
})()
Modifiez le texte h1 dans le balisage:
<h1>Loading...</h1>
Nous redémarrons le serveur.
Excellent. Tout fonctionne comme prévu.
Jusqu'à présent, nous n'avons traité que du contenu statique, mais que faire si nous avons besoin de rendre des pages avec un contenu dynamique? Est-il possible dans ce cas de se limiter au client ou est-ce que seul le serveur peut effectuer cette tâche?
Supposons que la page principale affiche une liste d'articles. Lorsque vous cliquez sur un article, la page avec son contenu doit être rendue. La page de publication doit également persister dans localStorage et s'afficher après le rechargement de la page (onglet fermer / ouvrir le navigateur).
Nous créons une base de données locale sous la forme d'un module JS nommé - 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(),
},
]
Créez un générateur de modèle de publication (également sous la forme d'export nommé: pour l'importation dynamique, l'export nommé est un peu plus pratique que celui par défaut) - 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}`,
})
Créons une fonction d'assistance pour trouver un article par son ID - 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)
}
Modifions src / pages / home.js:
//
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',
}
Corrigeons un peu src / script.js:
//
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)
})()
Nous redémarrons le serveur.
L'application fonctionne, mais convient que la structure du code dans sa forme actuelle laisse beaucoup à désirer. Il peut être amélioré, par exemple, en introduisant une classe supplémentaire "Router", qui combinera le routage des pages et des articles. Cependant, nous passerons par la programmation fonctionnelle.
Créons une autre fonction d'assistance - 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
}
Modifions un peu src / templates / post.js, à savoir: remplacez l'attribut «id» de la balise «article» par l'attribut «data-url» par la valeur «post # $ {id}»:
<article data-url="post#${id}">
La révision finale de src / script.js ressemble à ceci:
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)
})()
Comme vous pouvez le voir, l'API History, en conjonction avec l'importation dynamique, nous fournit des fonctionnalités assez intéressantes qui facilitent grandement le processus de création d'applications à page unique (SPA) avec presque aucune implication du serveur.
Si vous ne savez pas par où commencer le développement de votre application, commencez par le modèle de démarrage HTML moderne .
Récemment, j'ai effectué une petite recherche sur les modèles de conception JavaScript. Les résultats peuvent être consultés ici .
J'espère que vous avez trouvé quelque chose d'intéressant pour vous-même. Merci de votre attention.