有时我们沉浸在学习技术中,以至于忘记了问一些基本问题。如果你在过去几年里开始学习前端软件开发,你很可能很快就被介绍到了React、Vue和Angular等单页应用框架--但它们与网络服务器的交互以及在 "以前的时代 "是怎样的,你可能会觉得有点模糊。
相反,如果你已经用Java、PHP或Ruby等语言做了一段时间的Web开发者,你可能还没有进入单页应用的世界,因为流行的服务器端框架往往有出色的模板功能。
无论你的经验如何,都值得总结一下 "传统的 "Web应用程序是如何工作的,然后看看在单页应用程序的世界中事情有什么不同。
在这篇文章中,我们将讨论什么是传统的Web应用,建立一个简单的应用,然后将其转换为单页应用。
传统的网络应用程序
网络应用程序过去的工作方式如下:用户将在他们的网络浏览器("客户端")中导航到你的网站(让我们称之为mywebsite.com)。这将导致在互联网上向某处的计算机("服务器")发出一个HTTPGET 请求。
服务器会查看这个请求,并作出HTTP响应,除其他信息外,还包括一些HTML。客户端将收到该HTML,并将其呈现给用户,供其查看和互动。
比方说,该HTML有一个链接到位于mywebsite.com/about的 "关于 "页面。如果用户点击了这个链接,浏览器就会执行另一个HTTPGET 请求。同样,这个请求在互联网上被发送并被你的服务器接收。你的服务器看到这是对/about 路线的一个GET 请求。基于这一信息,它发送了一个新的响应,这次是与 "关于 "页面相关的HTML。
当客户端收到这个请求的响应时,整个屏幕被替换成 "关于 "页面的HTML。用户现在可以与 "关于 "页面进行交互。
这种互动可以用下图来表示。
重要的是:对于我们的用户想要的每一个页面,都会在互联网上发送一个新的HTTP请求,返回一堆HTML,而用户的视图则被替换成该HTML。
构建一个传统的网络应用程序
让我们来构建一个传统的网络应用程序。我们的客户端,如上所述,将是你拥有的任何网络浏览器(Chrome、Firefox、Edge等)。我们将在自己的机器上运行我们的服务器,它将用node.js编写,这是一个经常用于编写服务器应用程序的JavaScript运行时间。
如果你的电脑上没有安装node.js,你可以在这里免费下载它。
首先,我们将创建一个新的目录。
mkdir my-website
接下来,我们将导航到该目录,并启动一个新的node项目,并设置所有的默认值。
cd my-website
npm init -y
接下来,我们安装Express,这是一个用于node的web应用框架。
npm install express
现在我们将在我们的服务器上添加一些HTML文件。在当前目录下,创建以下三个文件。
index.html
<html>
<head>
<title>Home</title>
</head>
<body>
<main>
<h1>Welcome to my website!</h1>
<p>This is my internet homepage</p>
<h2>Handy links</h2>
</main>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</body>
</html>
about.html
<html>
<head>
<title>About</title>
</head>
<body>
<main>
<h1>About me</h1>
<p>I'm just a person on the internet!</p>
<h2>Handy links</h2>
</main>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</body>
</html>
contact.html
<html>
<head>
<title>Contact</title>
</head>
<body>
<main>
<h1>Contact me</h1>
<p>I can be reached via carrier pigeon</p>
<h2>Handy links</h2>
</main>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</body>
</html>
很好,我们现在有了所有的内容我们的最后一项任务将是编写服务器代码。让我们创建一个名为index.js 的文件,并在其中添加以下代码。
const express = require('express');
const app = express();
app.get('/', function (_, res) {
res.sendFile(__dirname + '/index.html');
});
app.get('/about', function (_, res) {
res.sendFile(__dirname + '/about.html');
});
app.get('/contact', function (_, res) {
res.sendFile(__dirname + '/contact.html');
});
app.listen(3000, () => {
console.log('Serving the app at http://localhost:3000!');
});
如果你不熟悉node/express,这段代码会创建一个新的express应用程序,然后指定三个路由。
- 如果一个
GET的请求到达了/路由,那么就用内容来响应。index.html - 如果一个
GET请求到达/about路由,则以内容进行响应。about.html - 如果一个
GET的请求到达/contact路由,用内容进行响应。contact.html
让我们来试试我们的应用程序!
在当前目录下,运行以下命令。
node index.js
你应该看到下面的文字。
Serving the app at http://localhost:3000!
使用你选择的浏览器,导航到http://localhost:3000。你应该看到这个应用程序!
每当我们在客户端(浏览器)上点击HTML中的一个链接时,一个HTTP请求就会被发送到我们的服务器,也就是运行我们的node/express应用程序。node/express应用程序解释请求,并发送一些新的HTML文本回来。最后,客户端得到这个HTML并将其显示给我们。
什么是单页应用程序?
我们花了一些时间才走到这里,但现在是回报。我们的 "传统 "应用程序可以被认为是一个多页面应用程序:对于我们在网站上点击的每一个链接,我们都会得到一个全新的HTML页面回来,我们的屏幕完全被刷新了。
但是,如果我们每次想在网站的某个地方进行导航时,不必与服务器联系呢?如果服务器在响应第一个请求时向客户端发送所有必要的信息呢?也许我们会避免一些页面重新加载的时间,因为我们不必一直重新加载服务器。
换句话说,如果我们的客户/服务器流程看起来更像这样呢?
在我们将多页面应用程序转换为单页面应用程序时,让我们牢记这些想法。
转换为单页应用程序
让我们创建一个新的文件,把它叫做spa.html 。这个文件一开始会很简单。它将有我们的main 元素和我们的导航。
spa.html
<html>
<head>
<title>Fancy Single Page App</title>
</head>
<body>
<main>This is the single page app</main>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</body>
</html>
然后我们可以重写我们的服务器代码,说 "无论我们收到什么GET 请求,都要用这个HTML来回应"。
const express = require('express');
const app = express();
app.get('/*', function (_, res) {
res.sendFile(__dirname + '/spa.html');
});
app.listen(3000, () => {
console.log('Serving the app at http://localhost:3000!');
});
现在,让我们使用node index.js 重新运行我们的服务器,并导航到http://localhost:3000。我们应该看到我们的 "这是一个单页应用程序 "文本和网站导航。如果我们点击任何一个链接,我们仍然向我们的服务器发送一个请求,但服务器会发回这个完全相同的HTML。这就是一个单页!
当然,这还不是太有趣。让我们使我们的应用程序开始看起来是正确的:我们可以在HTML中的<script> 标签内添加一些JavaScript,在页面加载时做以下事情。
- 查看当前的路径名称(例如,"/"、"/关于"、"/联系")。
- 根据路径名称,将
main元素的innerHTML设置为正确的内容 - 根据路径名称设置文档标题
spa.html
<html>
<head>
<title>Fancy Single Page App</title>
</head>
<body>
<main>This is the single page app</main>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
<script>
const pages = {
'/': {
title: 'Home',
content: `<h1>Welcome to my website!</h1>
<p>This is my internet homepage</p>
<h2>Handy links</h2>`,
},
'/about': {
title: 'About',
content: `<h1>About me</h1>
<p>I'm just a person on the internet!</p>
<h2>Handy links</h2>`,
},
'/contact': {
title: 'Contact',
content: `<h1>Contact me</h1>
<p>I can be reached via carrier pigeon</p>
<h2>Handy links</h2>`,
},
};
const path = window.location.pathname;
document.querySelector('main').innerHTML = pages[path].content;
document.title = pages[path].title;
</script>
</body>
</html>
这似乎是一个很大的问题!我们在这里所做的是为我们每条不同的路线存储title 和content 在一个叫做pages 的对象中。然后我们使用window.location.pathname 来访问当前的路径。最后,我们根据pages 对象来设置main 元素的innerHTML 属性和document.title 属性。
这个效果看起来与我们最初的应用程序惊人地相似!但我们仍然没有完全进入状态。
但我们还没有完全进入单页应用程序的领域,因为在现实中,每次我们点击我们的一个链接时,我们都会向服务器发送一个请求并得到相同的响应。我们需要防止我们的链接执行GET 请求,而是直接操作我们视图中的HTML。
<html>
<head>
<title>Fancy Single Page App</title>
</head>
<body>
<main>This is the single page app</main>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
<script>
const pages = {
'/': {
title: 'Home',
content: `<h1>Welcome to my website!</h1>
<p>This is my internet homepage</p>
<h2>Handy links</h2>`,
},
'/about': {
title: 'About',
content: `<h1>About me</h1>
<p>I'm just a person on the internet!</p>
<h2>Handy links</h2>`,
},
'/contact': {
title: 'Contact',
content: `<h1>Contact me</h1>
<p>I can be reached via carrier pigeon</p>
<h2>Handy links</h2>`,
},
};
const path = window.location.pathname;
document.querySelector('main').innerHTML = pages[path].content;
document.title = pages[path].title;
const links = document.querySelectorAll('a');
links.forEach((link) => {
link.addEventListener('click', (e) => {
e.preventDefault();
const path = e.target.pathname;
window.history.pushState({}, pages[path].title, path);
document.querySelector('main').innerHTML = pages[path].content;
document.title = pages[path].title;
});
});
</script>
</body>
</html>
最后这段代码为文档中的每个链接添加一个事件监听器。它阻止了默认动作(即执行GET 请求)。相反,它从目标锚元素中抓取路径名,使用window.history.pushState ,在我们眼前改变URL,然后做与我们在加载时执行的代码相同的innerHTML 和title 的改变。
现在我们可以说,我们已经建立了一个基本的单页应用程序如果我们在浏览器中浏览这个应用程序,我们会发现它像预期的那样工作,但没有向服务器发送HTTP请求。
把所有的东西都立即发送到客户端是现实的吗?
在现实中,我们经常有一些数据不能立即发送到客户端。也许我们维护了一个有500,000本书的列表,而用户正在搜索一本。如果在初始页面加载时就把50万本书的标题发送到客户端,那将是一个非常糟糕的主意:带宽和加载时间将是一个天文数字。
不过,这并没有破坏我们的单页应用计划。它只是意味着我们的单页应用程序有时需要从客户端的JavaScript代码中发出请求。我们的服务器可以不把整个HTML文档作为响应返回,而只是返回一些数据,通常是一个JSON对象,我们的单页面应用程序可以用它来填充文档。
我们的实现是黑客式的
我们的实现绝对是黑客式的。任何单页应用程序的生产实现都应该使用一个强大的库/框架,如React、Vue或Angular。这些框架都将直接的文档操作从你那里抽象出来,并实现了许多其他经过测试的功能。
缺点
单页面应用程序有其缺点。如果它们变得足够大,那么你就有可能一次向客户端发送太多的代码,这可能导致用户体验不佳。由于这个问题,像代码分割这样的功能已经被开发出来。代码拆分的作用就像它听起来那样--它将你开发的前端代码拆分成较小的包,并在需要时才将其发送给客户端。
状态管理在单页应用中也变得复杂。在多页面应用程序中,一个请求被发送到服务器,服务器做它需要做的任何数据查询,然后HTML被发送到客户端。在单页面应用程序中,当用户在看HTML文档时,服务器请求是在后台发生的。当这些请求发生时,我们要向用户展示什么?应用程序必须处于某种状态。如果有几个不同的后台请求同时发生怎么办?我们是用一个大的 "加载 "指示器还是用一堆小的指示器?如果三个请求中的一个失败了怎么办?我们是否显示这些请求的数据,然后为第三个请求显示一个失败指标?
总结
我希望这篇文章能给你提供一个关于单页应用程序、传统应用程序或两者的良好提示!