65.9K
CodeProject 正在变化。 阅读更多。
Home

重新思考 Web - 第 1 部分

starIconstarIconstarIconstarIconstarIcon

5.00/5 (11投票s)

2022年9月17日

CPOL

8分钟阅读

viewsIcon

11138

在软件开发领域,我们正处于一个黑暗时代。我们需要一场文艺复兴。

前言

在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# TypeIWhatever),我会明确说明。
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连接可以存在于其中。默认情况下,套接字在发送消息后会自行关闭。我们需要保持它的开放。(例如,通过循环等待接收者)。

这基本上是WebSockets的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控制台中看到一个“>”提示符。

尝试这些命令

  1. a return 4+5;
    b 4+5
  2. NicheNode.Replace("left", `<h3>Replacement</h3>`);
  3. SetSheet(`body { background-color: blue; } `);

    这会覆盖旧的样式。在未来的文章中,我将介绍样式管理。
    只需恢复它。

  4. 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模型中,我们随时去获取我们想要的东西。

  1. Write("person", "name", "Gwyll");
  2. Read("person", "name");

闭运算

在下一篇文章中,我将在此基础上进行扩展。
希望您觉得这篇文章很有趣。

历史

  • 2022年9月17日:初始版本
© . All rights reserved.