Selamat siang teman!
Pada artikel ini, saya ingin menunjukkan kepada Anda beberapa kemampuan JavaScript modern dan antarmuka yang disediakan oleh browser, terkait dengan perutean dan perenderan halaman tanpa mengakses server.
Kode sumber di GitHub .
Anda dapat bermain dengan kode di CodeSandbox .
Sebelum melanjutkan dengan penerapan aplikasi, saya ingin mencatat hal-hal berikut:
- Kami akan menerapkan salah satu opsi perutean dan perenderan sisi klien yang paling sederhana, beberapa metode yang lebih kompleks dan serbaguna (dapat diskalakan, jika Anda mau) dapat ditemukan di sini
- . : , .. , ( -, .. , ). index.html .
- Jika memungkinkan dan sesuai, kami akan menggunakan impor dinamis. Ini memungkinkan Anda untuk memuat hanya sumber daya yang diminta (sebelumnya ini hanya dapat dilakukan dengan membagi kode menjadi beberapa bagian (chunks) menggunakan pembuat modul seperti Webpack), yang bagus untuk kinerja. Menggunakan impor dinamis akan membuat hampir semua kode kita asinkron, yang, secara umum, juga bagus, karena menghindari pemblokiran aliran program.
Jadi ayo pergi.
Mari kita mulai dengan server.
Buat direktori, buka dan inisialisasi proyek:
mkdir client-side-rendering
cd !$
yarn init -yp
//
npm init -y
Instal dependensi:
yarn add express nodemon open-cli
//
npm i ...
- express - Kerangka kerja Node.js yang membuat pembuatan server jauh lebih mudah
- nodemon - alat untuk memulai dan memulai ulang server secara otomatis
- open-cli - alat yang memungkinkan Anda membuka tab browser di alamat tempat server berjalan
Terkadang (sangat jarang) open-cli membuka tab browser lebih cepat daripada nodemon memulai server. Dalam kasus ini, cukup muat ulang halaman.
Buat index.js dengan konten berikut:
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}`)
})
Buat index.html ( Bootstrap akan digunakan untuk gaya utama aplikasi ):
<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>
Untuk gaya tambahan, buat 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;
}
Tambahkan perintah untuk memulai server dan buka tab browser di package.json:
"scripts": {
"dev": "open-cli http://localhost:1234 && nodemon index.js"
}
Kami menjalankan perintah ini:
yarn dev
//
npm run dev
Bergerak.
Buat direktori src / halaman dengan tiga file: home.js, project.js, dan about.js. Setiap halaman adalah objek ekspor default dengan properti "content" dan "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',
}
Mari beralih ke skrip utama.
Di dalamnya, kami akan menggunakan penyimpanan lokal untuk menyimpan dan kemudian (setelah pengguna kembali ke situs) mendapatkan halaman saat ini dan API Riwayat untuk mengelola riwayat browser.
Sedangkan untuk penyimpanan, metode setItem digunakan untuk menulis data , yang mengambil dua parameter: nama dari data yang disimpan dan datanya sendiri, diubah menjadi string JSON - localStorage.setItem ('pageName', JSON.stringify (url)).
Untuk mendapatkan data, gunakan metode getItem , yang mengambil nama data; data yang diterima dari penyimpanan sebagai string JSON diubah menjadi string biasa (dalam kasus kami): JSON.parse (localStorage.getItem ('pageName')).
Sedangkan untuk History API, kami akan menggunakan dua metode objek history yang disediakan oleh antarmuka History : replaceState dan pushState .
Kedua metode tersebut mengambil dua parameter wajib dan satu parameter opsional: objek status, judul, dan jalur (URL) - history.pushState (status, judul [, url]).
Objek status digunakan saat menangani peristiwa "popstate" yang terjadi pada objek "jendela" saat pengguna bertransisi ke status baru (misalnya, saat tombol kembali dari panel kontrol browser ditekan) untuk membuat halaman sebelumnya.
URL digunakan untuk menyesuaikan jalur yang ditampilkan di bilah alamat browser.
Harap dicatat bahwa berkat impor dinamis, kami hanya memuat satu halaman saat meluncurkan aplikasi: baik halaman beranda, jika pengguna mengunjungi situs untuk pertama kali, atau halaman yang terakhir dilihatnya. Anda dapat memverifikasi bahwa hanya sumber daya yang Anda perlukan yang memuat dengan memeriksa konten tab Jaringan dari alat pengembang.
Buat 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)
})()
Ubah teks h1 di markup:
<h1>Loading...</h1>
Kami memulai ulang server.
Luar biasa. Semuanya bekerja seperti yang diharapkan.
Sejauh ini, kita hanya berurusan dengan konten statis, tetapi bagaimana jika kita perlu merender halaman dengan konten dinamis? Apakah mungkin dalam kasus ini dibatasi untuk klien atau apakah tugas ini hanya dapat dilakukan oleh server?
Mari kita asumsikan bahwa halaman utama adalah untuk menampilkan daftar posting. Saat Anda mengklik postingan, halaman dengan kontennya harus dirender. Halaman posting juga harus tetap ada di localStorage dan dirender setelah halaman dimuat ulang (tutup / buka tab browser).
Kami membuat database lokal dalam bentuk modul JS bernama - 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(),
},
]
Buat generator templat posting (juga dalam bentuk ekspor bernama: untuk impor dinamis, ekspor bernama agak lebih nyaman daripada yang default) - 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}`,
})
Buat fungsi pembantu untuk menemukan posting dengan ID-nya - 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)
}
Mari buat perubahan ke 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',
}
Mari perbaiki sedikit 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)
})()
Kami memulai ulang server.
Aplikasi berfungsi, tetapi setuju bahwa struktur kode dalam bentuknya saat ini meninggalkan banyak hal yang diinginkan. Ini dapat ditingkatkan, misalnya, dengan memperkenalkan kelas tambahan "Router", yang menggabungkan perutean halaman dan posting. Namun, kami akan melalui pemrograman fungsional.
Mari buat fungsi pembantu lain - 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
}
Mari kita ubah sedikit src / templates / post.js, yaitu: ganti atribut "id" dari tag "article" dengan atribut "data-url" dengan nilai "post # $ {id}"
<article data-url="post#${id}">
Revisi terakhir dari src / script.js terlihat seperti ini:
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)
})()
Seperti yang Anda lihat, History API, dalam hubungannya dengan impor dinamis, memberi kami fitur-fitur yang cukup menarik yang sangat memudahkan proses pembuatan aplikasi satu halaman (SPA) dengan hampir tidak ada keterlibatan server.
Jika Anda tidak tahu harus mulai dari mana mengembangkan aplikasi Anda, mulailah dengan Modern HTML Starter Template .
Baru-baru ini saya menyelesaikan penelitian kecil tentang pola desain JavaScript. Hasilnya bisa dilihat disini .
Saya harap Anda menemukan sesuatu yang menarik untuk diri Anda sendiri. Terima kasih atas perhatian Anda.