重新思考 Web - 第 1 部分
在软件开发领域,我们正处于一个黑暗时代。我们需要一场文艺复兴。
前言
在20世纪90年代中期,软件开发是相当可预测的。客户端-服务器模型已经确立,方法论扎实,应用程序快速且(在工业意义上)美观。就像罗马帝国的巅峰时期一样,一切都很好。然后,Web像野蛮人入侵一样席卷而来,带着其原始的脚本和HTML。当用户抱怨性能和质量时(只需回溯到90年代初的Web“应用”),管理层却无动于衷。为什么?
Web在企业中的最初爆发主要基于一个因素:分发。当时,应用程序的每一次更新都需要带着CD到用户桌面。
随着Web的成形和方法的固化,企业忙于Y2K的补救工作。其他人则试图上网,在那里被找到,并防止用户流失。工具和方法论支持了这一点,而Web的技术核心围绕CSS和SEO构建。
情况已经改变。SEO现在几乎毫无意义。爬虫无论如何都能很好地索引你,但搜索结果现在是付费游戏,你的网站才能显示出来。CSS已经变得无处不在且具有侵入性。它可能是“嘲笑单一职责原则”的 Jeopardy 回答。
它已经占据了如此多的领域,以至于有些人认为元素上的`class`属性意味着CSS类!
尽管如此(以及更多),实现者和开发人员正在做一些令人惊叹的事情。基于文档的方法对信息、宣传和目录类型的网站效果很好——它们有纸质文档的类比。但这种模型并没有让企业应用恢复昔日的辉煌。
再加上我们的对象模型。在60年代和70年代,出现了两种通用的面向对象方法。一种将对象视为“事物”,另一种将对象视为“实体”。今天我们看到的主流对象观都不是。它是“事物”方法的一个变种,其中事物类似于RDBMS表。OO理论中的**关联**被实现为数据库风格的关系。
和许多事情一样,软件充斥着教条和鼓吹教条的鹦鹉。像DRY、SOLID以及其他许多概念被普遍接受。在20年后的今天,它们还有效吗?
我启动了一个“回归基础”的项目,重点关注企业的需求。我将我们今天拥有的工具、技术和知识带回60年代,重新开始。
这些文章分享了我所学到的和创造的。
引言
本文概述了WebSocket
,并展示了如何通过套接字远程控制浏览器。该示例使用Windows控制台向浏览器发送和执行代码。
各部分
浏览器以请求-响应模式工作。但它们也可以以请求-响应模式工作。它们之间的基本区别在于方向。客户端发起请求,服务器发起请求。另一个区别是控制。在RR应用程序中,浏览器/客户端控制工作流。在SR应用程序中,情况颠倒了——服务器在控制。使用Solicit-Response,我们可以远程控制我们的浏览器。
HTTP不支持Solicits,但WebSocket
可以。
WebSockets
创建套接字很简单:(脚本)
new WebSocket(location.origin.replace("http", "ws"));
套接字是同源的,协议不同,所以我们可以使用一点替换技巧。
这会发送一个HTTP GET
请求,并在头部包含一些与套接字相关的信息。
在服务器端,也很简单:(C#概念代码 - 不要.)
MapGet("/", async (HttpContext http) => await http.WebSockets.AcceptWebSocketAsync( ));
服务器(用C#)创建一个具有WebSockets
接口的对象(HttpContext
)。
注意:我将以通用方式使用interface
。如果我指的是C# Type
(IWhatever
),我会明确说明。Accept
方法发送一个透明的响应给浏览器,该响应会触发套接字的onopen
事件。
打开的套接字实际上做不了多少事。它需要连接到系统。如果服务器立即开始发送消息,它们基本上会石沉大海。没有人监听。我们需要监听器。而且,我们需要知道我们有监听器。
self.socket = new WebSocket(location.origin.replace("http", "ws"));
socket.onopen = () => {
socket.onmessage = () => {...});
// Other stuff
socket.send("ready");
}
在onopen
处理程序中,我们可以附加一个onmessage
处理程序,并执行任何其他初始化工作。完成后,我们让服务器知道一切都好——“准备好了”。
服务器上的套接字与我们从C#对象那里期望的有些不同。虽然许多消息组件会“推送”(例如,事件/委托)。我们需要显式地等待套接字。
注意:目前套接字的一个问题是您无法停止等待。取消会终止连接。
WebSocket socket = await http.WebSockets.AcceptWebSocketAsync();
var buffer = new byte[1024];
WebSocketReceiveResult ack = await socket.ReceiveAsync(buffer, CancellationToken.None);
“ack
”对象是一个状态对象。数据/消息在缓冲区中,易于以文本形式获取。
string msg = Encoding.UTF8.GetString(buffer[...ack.Count]);
还有最后一件事。套接字就像嵌套连接——一个HTTP连接内的套接字连接。如果我们不做任何事情,连接会中断或超时。基础设施通过内置的“keep-alive”处理HTTP。HTTP部分基本上未使用,我们告诉它等待响应并且永不发送一个。 “keep-alive”不断延长谎言——“再等30秒……”(残酷)。
当HTTP连接打开时,WebSocket连接可以存在于其中。默认情况下,套接字在发送消息后会自行关闭。我们需要保持它的开放。(例如,通过循环等待接收者)。
这基本上是WebSocket
s的90%。其余的是这种变体(即二进制消息)或粘合剂。
浏览器任务
浏览器中运行JavaScript的“JavaScript虚拟机”(JVM)是一个有趣的生物。虽然它已经被改进和增强,但基本架构自V1以来没有改变。在内部,它看起来很像70/80年代的Motorola 6800/68000 CPU。编程让我想起了BASIC(不是语法,而是方法)以及DOS中的批处理文件或大型机调度程序。我不是在批评。这些都是很好的技术,但在分布式异步计算的时代已经有些过时了。这个主题在其他文章中有探讨。
现在相关的是浏览器代码是解释执行的。忽略任何优化,解释器在看到解释的代码之前对它一无所知。代码何时以及如何到达并不重要,只要在需要时存在即可。这是即时(Just In Time)方法的一个完美环境。
脚本支持函数(function(){ }
),这些函数可以是异步的。async
函数构造函数本身并没有暴露。这段代码将构建并运行一个async
或“标准”函数。
self.AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
self.Execute = async (cmd) => await (new AsyncFunction(cmd))() ?? "Success"
这会运行任何有效的JavaScript代码。如果代码返回一个值,Execute
会返回该值。“Success
”会在代码成功完成且没有返回值时返回。
插入套接字代码
self.AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
self.Execute = async (cmd) => await (new AsyncFunction(cmd))() ?? "Success"
self.socket = new WebSocket(location.origin.replace("http", "ws"));
socket.onopen = async () => {
socket.send("ack");
socket.onmessage = async (msg) => {
try { socket.send(await Execute(msg.data)); }
catch (e) { socket.send(`Fail: ${e}`); }
}
};
这会将函数从消息中“即时”构建,执行它,并通过套接字将结果发送回来。它(忽略优化)的运行方式与页面加载时创建的函数完全相同。
服务器端使用发送和等待行。
string cmd = "return 4+5;"
var buffer = new byte[1024];
_ = socket.SendAsync(new(UTF8.GetBytes(cmd)), Text, true, _ct.None);
var result = await socket.ReceiveAsync(buffer, _ct.None);
string data = Encoding.UTF8.GetString(buffer[..result.Count]);
变量data
将等于“9
”。省略返回(“4+5
”)会将“Success
”放入data
。
通过这种方法,我们可以从服务器将任务(以脚本形式)发送到浏览器。
服务器
为了支持这些,我们需要一个Web服务器。目前相关的服务器代码是:
_ = app.MapGet("/", (HttpContext http) => http.WebSockets.IsWebSocketRequest switch {
false => http.Response.WriteAsync("<!DOCTYPE html><html>%SocketCode%</html>"),
true => Task.Run(async () => {
WebSocket socket = await http.WebSockets.AcceptWebSocketAsync();
....
HTTP和WebSocket
请求可以映射到同一个URL。HttpContext
上的套接字接口提供了一个IsWebSocketRequest
属性来区分。我们可以简单地进行切换。
当为true
时,我们启动socket
进程。当为false
时,则处理标准HTML文档。或其他内容。
浏览器非常宽容。如果我们不提供文档,它会自己创建一个。我们在HTTP请求中关心的只是让套接字代码运行。我们可以将文档包含在
self.SetDocument = (content) => {
const doc = document.open(); doc.write(content); doc.close(); }
我们可以直接发送(使用原始字符串字面量使其变得容易)
http.Response.WriteAsync($$"""
<script>
self.AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
self.Execute = async (cmd) => await (new AsyncFunction(cmd))() ?? "Success"
self.SetDocument = (content) => { const doc = document.open();
doc.write(content); doc.close(); }
self.SetSheet = (content) => { const s = new CSSStyleSheet(); s.replace(content);
document.adoptedStyleSheets = [s]; }
self.socket = new WebSocket(location.origin.replace("http", "ws"));
socket.onopen = async () => {
socket.send("ack");
socket.onmessage = async (msg) => {
try { socket.send(await Execute(msg.data)); }
catch (e) { socket.send(`Fail: ${e}`); }
}
};
</script>
""");
通过代码替换文档不像HTTP GET
。它不会重置窗口或执行上下文。它只替换HTML标签之间的内容。我们在EC self.Thing = ...
中挂载的所有内容都会保留,并可用于新文档。
我们不仅可以动态地替换文档和部分,还可以用CSS做到这一点。注意SetSheet
函数。通过实时插入或删除CSS、Markup或Script,我们可以对浏览器进行“完全控制”。
重要概念点
通过这种方法,我们可以实时控制浏览器的执行上下文和内容。
永远不需要“以防万一”的代码、标记或CSS页面。浏览器中唯一需要存在的是当前可见的伪像。代码可以在实时“制造”(另一篇文章)以适应特定条件和状态。
现在全部整合...
这是一个可用的应用程序。它使用Windows控制台来控制浏览器。
如果您的IDE启动了,请关闭它。代码是.net7 + 原始字符串字面量。
在VS中,项目文件和启动
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
</PropertyGroup>
</Project>
{
"profiles": {
"http": {
"commandName": "Project",
"launchBrowser": false,
"applicationUrl": "https://:5150"
}
}
}
代码如下
using System.Threading;
namespace CP;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using System;
using System.Diagnostics;
using System.Net.WebSockets;
using System.Threading.Tasks;
using static System.Net.WebSockets.WebSocketMessageType;
using static System.Text.Encoding;
using _ct = CancellationToken;
public class Program {
public static void Main(string[] args) {
int port = 5085;
Barrier barrier = new(2);
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.PreferHostingUrls(true).ConfigureKestrel
(s => s.ListenLocalhost(port));
WebApplication app = (WebApplication)builder.Build().UseWebSockets();
_ = app.MapGet("/", (HttpContext http) =>
http.WebSockets.IsWebSocketRequest switch {
false => http.Response.WriteAsync($$"""
<script>
self.AsyncFunction = Object.getPrototypeOf
(async function () { }).constructor;
self.Execute = async (cmd) =>
await (new AsyncFunction(cmd))() ?? "Success"
self.SetDocument = (content) => { const doc = document.open();
doc.write(content); doc.close(); }
self.SetSheet = (content) =>
{ const s = new CSSStyleSheet(); s.replace(content);
document.adoptedStyleSheets = [s]; }
self.Read = (facet, field) => document.querySelector
(`[facet="${facet}"][field="${field}"]`).value;
self.Write = (facet, field, v) =>
document.querySelector
(`[facet="${facet}"][field="${field}"]`).value = v;
self.socket = new WebSocket(location.origin.replace("http", "ws"));
socket.onopen = async () => {
socket.send("ack");
socket.onmessage = async (msg) => {
try { socket.send(await Execute(msg.data)); }
catch (e) { socket.send(`Fail: ${e}`); }
}
};
self.NicheNode = class NicheNode extends HTMLElement
{ #content = null; #name = null;
static Replace(niche, content) {
const n = document.querySelector(`layout-niche[niche="${niche}"]`)
.replaceWith(new NicheNode(niche, content)); }
constructor(name, content) { super();
if (content) this.#content = content;
if (name) this.#name = name; }
connectedCallback() {
if (this.#content != null) this.innerHTML = this.#content;
if (this.#name != null) this.setAttribute("niche", this.#name);
}
};
customElements.define("layout-niche", NicheNode);
</script>
"""),
true => Task.Run(async () => {
WebSocket socket = await http.WebSockets.AcceptWebSocketAsync();
var buffer = new byte[1024];
WebSocketReceiveResult ack =
await socket.ReceiveAsync(buffer, _ct.None);
barrier.SignalAndWait();
_ = socket.SendAsync(new(UTF8.GetBytes($$"""
SetDocument(`
<!DOCTYPE html><html lang="en">
<head><title>Wisp</title>
</head>
<body>
<area-left>
<layout-niche niche="left"></layout-niche>
</area-left>
<area-right>
<layout-niche niche="left">
<input type="text" facet="person"
field="name" value="" />
</layout-niche>
</area-right>
</body>
</html>
`);
""")), Text, true, _ct.None);
_ = await socket.ReceiveAsync(buffer, _ct.None);
_ = socket.SendAsync(new(UTF8.GetBytes($$"""
SetSheet(`
body { display:flex; gap:10px; }
area-left, area-right { display:flex; flex-direction:column;
border: 1px solid black; min-height: 200px; }
area-left { flex:1; }
area-right { flex:2; }
`);
""")), Text, true, _ct.None);
_ = await socket.ReceiveAsync(buffer, _ct.None);
Loop:
string cmd = "";
Console.Write(">");
if( (cmd = Console.ReadLine() ?? throw new()) == "" ) goto Loop;
await socket.SendAsync(new(UTF8.GetBytes(cmd)), Text, true, _ct.None);
var status = await socket.ReceiveAsync(buffer, _ct.None);
Console.WriteLine(UTF8.GetString(buffer[..status.Count]));
goto Loop;
})
});
Task.Factory.StartNew(async () => { await app.StartAsync(); });
_ = Process.Start("explorer", $"https://:{port}");
barrier.SignalAndWait();
_ = new AutoResetEvent(false).WaitOne();
}
}
您应该在Windows控制台中看到一个“>
”提示符。
尝试这些命令
- a
return 4+5;
b4+5
NicheNode.Replace("left", `<h3>Replacement</h3>`);
SetSheet(`body { background-color: blue; } `);
这会覆盖旧的样式。在未来的文章中,我将介绍样式管理。
只需恢复它。SetSheet(` body { display:flex; gap:10px; } area-left, area-right { display:flex; flex-direction:column; border: 1px solid black; min-height: 200px; } area-left { flex:1; } area-right { flex:2; } `);
Solicit
在请求-响应模型中,我们等待用户发布/提交,然后他们发送一些预定义的数据。在Solicit-Response模型中,我们随时去获取我们想要的东西。
Write("person", "name", "Gwyll");
Read("person", "name");
闭运算
在下一篇文章中,我将在此基础上进行扩展。
希望您觉得这篇文章很有趣。
历史
- 2022年9月17日:初始版本