Web应用/服务已经遍布生活的方方面面,每个网站都是一个Web应用,比如搜索引擎、电商平台、视频平台、公司主页、个人博客、电子邮件等。此外,手机和桌面端的联网软件也依赖Web应用,例如微信的即时通讯服务。

如果你想开发一个Web应用,方便自己和他人使用,希望本文能理清一些基本概念,帮你找到学习的路径。

本文预期读者有一定编程基础,了解如ip、端口、域名等计算机网络的基本概念。

Web开发的基本逻辑

我们先感受一下本地应用程序。打开手机的计算器,输入 1+1,得到计算结果 2。在这个过程中,你输入数据,计算器调用手机的硬件(这里是Cpu),在运行环境(这里是操作系统)下,执行被预先用代码编写好的运算逻辑,然后输出结果。

于是我们发现,本地应用程序有四个组成部分:数据,硬件,运行环境,代码。

在Web应用处,这四部分也如此组合,最终成为一个有功能的、可公开访问的网站。不同的是,各部分并不像本地应用程序一样,紧密地耦合在一台机器(如手机)上。

电脑操作系统一般集成了C语言运行时,所以可以运行C语言编写的应用程序。对于Web应用来说,Web浏览器(Web Browser,如谷歌、Edge浏览器)就是运行环境,集成了HTML、CSS、JavaScript三种语言的运行时(之后会详细讲到)。所以你会发现,电脑本地应用程序没法在手机上用,因为它们的操作系统不同。但Web应用既可以在电脑端浏览器里使用,也可以在手机端浏览器里使用。甚至于,你可以给Kindle、Switch等稀奇古怪的设备装上Web浏览器,在上面跑Web应用。这就是Web应用的跨平台兼容性。

为了讲解数据、硬件、代码,我们以百度(www.baidu.com)为例。在电脑端打开Web浏览器,进入百度官网,按 F12打开审查元素,你会看到一个复杂的窗口。先不管那么多,切换到“网络”标签,刷新一下页面,你会看到类似这样的界面:

image.png|1075

对于Web应用来说,每次访问都会向对应的远程计算机请求(request)数据和代码,包含媒体资源(图片、视频等)、格式化文本(列表、字典等)和代码(网站样式、行为)。浏览器收到数据后,处理媒体资源、运行代码,才将完整的网站渲染在窗口中。

此处的远程计算机一般被称为Web服务器(Web Server),专门用于存放该网站的数据,也负责响应请求、执行部分代码。服务器的租赁、流量、防护费用,是一个Web应用支出的大头。

这种通用性浏览器配合专用服务器的架构,称为浏览器-服务器(Browser-Server,BS)架构。在浏览器侧,存在临时把网站资源存在本地,以加快重复访问速度的技术,我们称为缓存(Cache),一般会定期清理。另外有所谓客户端-服务器(Client-Server,CS)架构,使用专门的客户端存储数据,大多数电脑端联网应用如大型网游、手机app,都是CS架构的实例。本文关注的Web应用都是BS架构,CS在此不展开。

回到上图,这个看起来杂乱的列表是一个文件清单,详细列出我们向百度服务器请求的文件,以及加载该文件的耗时。

类型 描述 作用
png, webp, gif 媒体资源 显示图片等
json 格式化文本 提供数据
document HTML代码 网页框架
stylesheet CSS代码 网页样式
script JavaScript代码 网页动态行为

总结一下,Web应用需要专门的服务器保持在线;运行环境是浏览器;数据大部分来自服务器,少部分来自浏览器输入;使用的编程语言是HTML+CSS+JavaScript。

开发一个功德发生器

我们创建一个 index.html文件,并用记事本打开。首先写下HTML语法,指定页面属性,搭建出网页标题、计数、按钮。你不需要看懂每一行是什么意思,发现HTML的功能单元是一个个可嵌套的尖括号(即标签)即可

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
</head>

<body>
    <h1>功德发生器</h1>
    <div id="counter">0</div>
    <button>加功德</button>
</body>
</html>

双击 index.html打开,显示了一些HTML内置的元素,比如 h1标签指定的一级标题,button标签指定的按钮。此时点击按钮是没有效果的

image.png

功德数随点击次数增加需要JavaScript。由于HTML是静态的,不可能在 script标签以外使用变量,我们必须用JavaScript先获取id为counter的元素,再更新它,而不是直接将变量分配给该元素

为了触发JavaScript内变量的自增,还需要创建一个函数(此处命名为 incrementCounter),并将其绑定到按钮上

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
</head>

<body>
    <h1>功德发生器</h1>
    <div id="counter">0</div>
    <button onclick="incrementCounter()">加功德</button>
    <script>
        let count = 0;
        function incrementCounter() {
            count++;
            document.getElementById('counter').innerText = count;
        }
    </script>
</body>
</html>

这时功德发生器的交互功能已经实现了

image.png

然而,这个字体和按钮看起来还是粗糙了些。我们可以在 style标签中用CSS美化之

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <style>
        body {
            display: flex;
            flex-direction: column;
            align-items: center;
        }

        #counter {
            font-size: 2em;
            margin: 20px;
        }

        button {
            padding: 10px 20px;
            font-size: 1em;
            color: white;
            background-color: #5cb85c;
            border: none;
            border-radius: 5px;
            cursor: pointer;
        }
    </style>
</head>

<body>
    <h1>功德发生器</h1>
    <div id="counter">0</div>
    <button onclick="incrementCounter()">加功德</button>
    <script>
        let count = 0;
        function incrementCounter() {
            count++;
            document.getElementById('counter').innerText = count;
        }
    </script>
</body>
</html>

看看美化之后的功德发生器,是不是顺眼多了

image.png

这就是一个Web应用HTML、JavaScript、CSS三剑客的协作方式。以上我们将三种语言都写在同一个HTML文件中,事实上可以拆分为三个文件,使项目结构更清晰。像这样

├── css
│   └── style.css
├── js
│   └── counter.js
└── index.html

style.csscounter.js中分别原封不动地贴入 script标签和 style标签的代码,然后在 index.html中直接引用

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" type="text/css" href="css/style.css">
</head>

<body>
    <h1>功德发生器</h1>
    <div id="counter">0</div>
    <button onclick="incrementCounter()">加功德</button>
    <script src="js/counter.js"></script>
</body>
</html>

现代的Web开发

以上我们实现了一个很简单的Web应用,实际上,大多数Web应用的功能会复杂得多。比如说,我希望记录每天的功德值,再加入统计报表功能。这就需要数据在服务器上持久保存,并在每次访问Web应用时请求历史数据。统计报表的计算,用JavaScript实现也稍显繁琐,如果能用Java或Python就好了。

现代的Web开发已经划分为泾渭分明的两个部分,前端和后端。前端就是用户能直接感知到的部分,包括界面、动效、用户交互,也是传统的Web开发部分,使用HTML+CSS+JavaScript三剑客。后端是用户无法直接感知的,负责读写数据库、身份验证、处理复杂的业务逻辑、向前端传递运算结果等。后端的编程语言选择非常多样化,比如Java,Python,Go,Rust,JavaScript等。

前后端分离是目前Web应用开发的标准范式,这意味着两者可以在不同的目录下,使用不同的运行环境,甚至部署在不同的服务器上。前后端传递数据一般采用Json或XML格式,最常见的是前端主动向后端发送请求、获取请求结果,最后展现到前端。也有后端主动向前端发送请求的情况,例如消息推送。

后端总是以url的形式供前端获取数据,这常被称为api或接口。例如 https://pokemon.fantasticmao.cn/pokemon/list,这是一个公开的GET接口,访问后即可获取宝可梦列表。

image.png

但也有很多api不使用GET方法,于是直接在浏览器访问得不到想要的数据。这在生产环境不是问题,因为前端JavaScript会构造合适的请求。在测试时,可以借助Postman、Apifox等工具,构造POST、UPDATE等其他类型的请求,测试后端数据。

很少有人用原生Java或Python开发后端,总是借助一些高度封装、简单便捷的第三方库完成,这些库被称为框架。常用的框架包括Java的Spring boot,Python的Django、Flask、Fastapi,JavaScript的express等。

后端需要数据库做存储,最常用的数据库是MySQL和PostgreSQL。直接操作数据库需要使用SQL语言,但很多时候我们把数据库的操作映射到高级语言,使用Java或Python语法操作数据库,这个过程称为对象关系映射(Object-Relational Mapping, ORM)。ORM显著提高了数据库操作的维护性和复用性,代价是牺牲了一些效率。

对于业务逻辑简单、主要用于展示内容的Web应用,无头CMS(Headless Content Management System)是另一种后端的选择,例如Storyblok, Strapi。它通常将数据库、管理后台绑定在一起,并提供丰富的api接口,使前端可以对数据进行增删改查。

前端也出现了一批通用开发框架,并以框架为核心逐渐扩展为生态系统,目前态势是Vue和React生态分庭抗体。对于Vue生态,最常见的配置是Vue基础框架 + Vue Router路由管理 + Pinia状态管理。

网页样式用用户体验的影响太过明显,原生CSS孱弱的语法已经无法满足需求,因此陆续出现了许多CSS框架如SCSS、LESS、Tailwind CSS。它们有特定的高级语法,方便定制样式,并在编译后生成CSS文件,以供浏览器渲染。

Web应用虽然具有跨平台兼容性,但各平台屏幕尺寸不同,需要所谓“响应式布局”,来实现对不同屏幕尺寸的适配。响应式布局大多通过CSS,使用如flex布局、媒体查询、容器查询等特性完成。有些CSS框架如Tailwind CSS,也对响应式布局的定义做了优化。

此外还有一些第三方库,整合了一些常用组件如按钮、选择器、输入框、表格等,将它们的HTML+CSS+JavaScript封装,这被称为组件库。Vue生态的知名组件库如Element Plus、Vuetify,React生态的知名组件库如Ant Design、Next UI。

以上框架可以相对自由地组合,形成自己喜欢的技术栈,例如PostgreSQL + Fastapi + Vue + Tailwind CSS + Element Plus。

应用部署

代码写成之后,还需要考虑部署(deployment)问题,也就是把代码放在哪里、使用哪里的计算资源、网络流量。

在本地部署的Web应用,设置恰当的防火墙规则后可供同一局域网内的设备访问,在很多时候足够自用(如 Jupyter, Rstudio Server, Stable Diffusion等)。

如果你需要让局域网之外的人访问,就得将应用部署到一台拥有公网ip的云服务器上。这种服务器可以在各大云厂商如阿里云、腾讯云、华为云租赁,入门款一般提供2核2G、几十G硬盘、4-6Mbps带宽的配置,价格在几十到一两百每年不等。

部署后的应用,会占用宿主机的一个端口(port),并通过地址 ip:port为外网提供服务。端口号通常是可自定义、固定不变的。记得在云服务器防火墙中放行该端口,否则外网无法访问

ip:port通常是很难记忆的,我们可以在域名服务商购买域名,通过DNS解析服务,将域名/子域名解析到自己的公网ip。然后通过反向代理程序如 Nginx,将该服务绑定到子域名上。按照工信部的要求,国内域名需要做ICP备案和公网安备案。

请记住,租赁云服务器有被攻击导致隐私泄露的风险,更有被盗刷流量、产生高额账单的风险。租赁之前,确保自己有基本的服务器防护意识。

如果你的Web应用比较简单,也可以考虑无服务器(Serverless)部署(无头部署),比较知名的工具有 Vercel, Github Pages。你可以将需要部署的代码上传到Github,并授权给无头服务商。无头服务商会为你的项目分配一定的计算资源、部署项目,最终给出一个外网可访问的url。

当你修改项目代码后,直接提交到Github仓库,已部署的应用会自动更新、重新部署。这得益于无头服务商对持续集成/持续部署(CI/CD)的广泛使用。

无头部署的好处是节省租赁服务器的钱,不必担心服务器被攻击造成的损失,部署界面友好,运行稳定,支持随时回滚。坏处是网络优化不一定好。

总结

以上,我们梳理了Web应用开发的基本逻辑,写了一个简单的功德发生器,概述了现在Web开发的技术栈,以及三种不同的部署方式。

玩得开心!