分类: 软件

  • CL-REDIS:Redis 的 Common Lisp 冒险之旅 🗺️

    CL-REDIS 就像一个探险家,带着你深入 Redis 的世界,探索数据存储的奥秘。它是一个快速、可靠的 Common Lisp 客户端,让你可以轻松地与 Redis 服务器进行交互。想象一下,它就像一个经验丰富的向导,带着你穿越 Redis 的广阔领域,为你提供所有你需要探索和管理数据的工具。 🧭

    快速入门:准备出发 🥾

    在你开始你的 Redis 冒险之旅之前,你需要准备一些必需品。首先,确保你有一台正在运行的 Redis 服务器。然后,使用 ql:quickload 'cl-redis 加载 CL-REDIS 库。就像在你的背包里装满地图和指南针一样。

    接下来,你需要连接到 Redis 服务器。你可以使用 (redis:connect :host <host> :port <port>) 函数来建立连接。默认情况下,host127.0.0.1port6379。就像找到你探险的起点一样。

    现在,你可以使用 red 包中的 Redis 命令来与服务器进行交互了。例如,你可以使用 (red:ping) 命令测试连接。就像向你的向导打招呼一样。

    完成你的探险之后,你可以使用 (redis:disconnect) 函数断开连接。或者,你可以使用 with-connection 宏,它会自动为你打开和关闭连接。就像在你的探险结束后,回到你的出发点一样。

    可用命令:你的探险工具箱 🧰

    CL-REDIS 提供了大量的 Redis 命令,让你可以执行各种操作,包括:

    • 字符串操作: SETGETAPPENDINCRDECR 等。就像在你的探险中,记录你的发现和修改你的笔记一样。
    • 哈希操作: HSETHGETHDELHGETALL 等。就像在你的探险中,收集和整理各种信息一样。
    • 列表操作: LPUSHRPUSHLRANGELREM 等。就像在你的探险中,收集和整理各种信息一样。
    • 集合操作: SADDSMEMBERSSISMEMBERSREM 等。就像在你的探险中,收集和整理各种信息一样。
    • 排序集操作: ZADDZRANGEZSCOREZREM 等。就像在你的探险中,收集和整理各种信息一样。
    • 事务操作: MULTIEXECDISCARD 等。就像在你的探险中,执行一系列操作,并确保它们按顺序完成一样。
    • 发布订阅操作: PUBLISHSUBSCRIBEUNSUBSCRIBE 等。就像在你的探险中,与其他探险者进行交流一样。

    代码组织:你的探险地图 🗺️

    CL-REDIS 提供了两个包:REDISRED。所有功能都可以在 REDIS 包中使用。为了避免符号冲突,Redis 命令在这个包中定义时,会加上一个前缀(默认情况下为 red-,在编译时设置)。 RED 包是语法糖,它只是提供了没有前缀的 Redis 命令。因此,它不建议导入,以避免与 COMMON-LISP 包发生符号冲突。你只需要使用包限定的符号名称即可。例如,同一个 Redis 命令(例如 GET)可以调用为 RED-GET(如果你导入了 REDIS 包)或 RED:GET

    安装:准备你的装备 🎒

    CL-REDIS 可通过 quicklisp 获取。它依赖于以下几个库:

    调试和错误恢复:你的探险指南 🧭

    如果 *echo-p*T,所有客户端-服务器通信将被回显到 *echo-stream* 流中,默认情况下为 *standard-output*

    错误处理模仿了 Postmodern。特别是,当发生错误导致通信流中断时,会发出 redis-connection-error 类型的条件,提供一个 :reconnect 重启。如果选择它,整个 Redis 命令将被重新发送,如果重新连接尝试成功。此外,connect 检查是否已经建立了与 Redis 的连接,如果已经建立,则提供两个重启(:leave:replace)。

    当服务器响应错误回复(即以 - 开头的回复)时,会发出 redis-error-reply 类型的条件。

    还有一个高级的 with-persistent-connection 宏,它会尽力做到正确的事情™(即在连接断开后自动重新打开连接一次)。

    高级用法:你的探险技巧 🧗‍♀️

    发布订阅

    由于没有专门的命令通过发布订阅从 Redis 接收消息,你可以使用以下方法:

    (bt:make-thread (lambda ()
                      (with-connection ()
                        (red:subscribe "foo")
                        (loop :for msg := (expect :anything) :do
                          (print msg))))
                    "pubsub-listener")

    要发布消息,可以使用以下方法:

    (with-connection ()
      (red:publish "foo" "test"))

    管道

    为了提高性能,Redis 允许对命令进行管道化,并延迟接收结果,直到最后再批量处理。CL-REDIS 提供了 with-pipelining 宏来支持管道化。比较以下示例中的执行时间(使用管道和不使用管道):6.567 秒 vs. 2023.924 秒!

    (let ((names (let (acc)
                   (dotimes (i 1000 (nreverse acc))
                     (push (format nil "n~a" i) acc))))
          (vals  (let (big-acc)
                   (dotimes (i 1000 (nreverse big-acc))
                     (let (acc)
                       (dotimes (i (random 100))
                         (push (list (random 10) (format nil "n~a" i)) acc))
                       (push (nreverse acc) big-acc))))))
      (time (redis:with-connection ()
              (redis:with-pipelining
                (loop :for k :in names :for val :in vals :do
                  (dolist (v val)
                    (apply #'red:zadd k v)))
                (red:zunionstore "result" (length names) names)
                (red:zrange "result" 0 -1))))
    
      ;; Evaluation took:
      ;;  6.567 seconds of real time
      ;;  3.900243 seconds of total run time (3.200200 user, 0.700043 system)
    
      (time (redis:with-connection ()
              (loop :for k :in names :for val :in vals :do
                (dolist (v val)
                  (apply #'red:zadd k v)))
              (red:zunionstore "result" (length names) names)
              (red:zrange "result" 0 -1))))
    
      ;; Evaluation took:
      ;; 2023.924 seconds of real time
      ;; 3.560222 seconds of total run time (2.976186 user, 0.584036 system)

    请注意,with-pipelining 调用理论上可以嵌套,但结果只对最高级别的管道可用,所有嵌套的管道将返回 :PIPELINED。因此,在这种情况下会发出警告。

    内部机制:你的探险指南 🗺️

    通用函数 tellexpect 根据 规范 实现 Redis 协议。tell 指定了 Redis 请求的格式,expect 指定了响应的处理方式。实现 expect 上另一种方法的最佳方式通常是使用 def-expect-method,它会安排从套接字读取数据,并提供一个 reply 变量,该变量保存从服务器解码的回复数据,并删除了初始字符。例如:

    (def-expect-method :ok
      (assert (string= reply "OK"))
      reply)

    Redis 操作通过 def-cmd 定义为普通函数,只需要提供参数和返回类型。def-cmd 将所有定义的函数名称加上 *cmd-prefix* 前缀,默认情况下为 'red。(请注意,设置 *cmd-prefix* 将在编译时生效)。它还将它们从 REDIS 包导出,并从 RED 包导出,但不带前缀。

    下面是一个命令定义的示例:

    (def-cmd KEYS (pattern) :multi
      "Return all the keys matching the given pattern.")

    请参阅 commands.lisp 查看所有定义的命令。

    未实现的功能:你的探险计划 🗺️

    • 以下命令未实现,因为它们不打算在客户端使用:MONITORDEBUG OBJECTDEBUG SEGFAULT
    • Unix 域套接字支持 – 已计划
    • 一致性哈希 未内置。实际上,这种东西与该库的功能是正交的,可能应该在另一个库中实现。
    • 连接池也没有实现,因为在存在 with-persistent-connection 的情况下,它实际上并不那么需要。持久连接对于专用线程来说更简单、更高效,而且错误更少。但是,连接池还有其他用例,因此它可能会在将来的版本中实现。

    致谢:你的探险伙伴 🤝

    该库由 Vsevolod Dyomkin vseloved@gmail.com 开发和维护。

    在最初阶段,Alexandr Manzyuk manzyuk@googlemail.com 开发了连接处理代码,遵循了 Postmodern 中的实现。后来,它被部分重写,以适应更高级的连接处理策略,例如持久连接。

    许可证:你的探险指南 🧭

    MIT(有关详细信息,请参阅 LICENSE 文件)。

    https://github.com/vseloved/cl-redis/raw/refs/heads/master/README.md

  • Hunchentoot:你的 Common Lisp 网页服务器探险之旅 🗺️

    Hunchentoot 就像一把瑞士军刀,专为 Common Lisp 网页开发者打造。它既是网页服务器,也是构建动态网站的工具箱。想象一下,它就像森林里的一间舒适小屋,随时准备接待你的网页应用,并提供所有你需要构建熊熊烈火和美味数字内容的工具。 🏕️🔥

    下载和安装:快速去商店一趟 🛒

    在你开始构建你的网页杰作之前,你需要收集一些物资。Hunchentoot 依赖于几个其他 Common Lisp 库,比如可靠的 MD5 用于安全,CL-BASE64 用于编码,以及 RFC2388 用于处理网页协议。就像为你的网页服务器准备了一个储备充足的食品储藏室。

    别担心,你可以很容易地从互联网上获取所有这些库。如果你想尝试更刺激的体验,还可以尝试一下 Quicklisp,这是一个方便的工具,可以帮助你管理 Common Lisp 依赖项。就像为你的网页服务器配备了一个私人采购员! 📦

    运行 Hunchentoot:启动引擎 💨

    现在你已经拥有了所有食材,是时候启动引擎了。你可以用一行代码启动一个基本的 Hunchentoot 服务器:

    (hunchentoot:start (make-instance 'hunchentoot:easy-acceptor :port 4242))

    这将在端口 4242 上启动一个监听服务器。然后,你可以在浏览器中输入 http://127.0.0.1:4242/ 来访问它。就像打开你舒适小屋的门,欢迎访客一样。 🚪

    构建你的网页应用:添加家具和装饰 🛋️

    默认情况下,Hunchentoot 会从其源代码树中的 www/ 目录提供文件。想象一下,这就像你的空旷小屋,准备让你用自己的网页内容装饰。你可以使用 ACCEPTOR-DOCUMENT-ROOTACCEPTOR-ERROR-TEMPLATE-DIRECTORY 设置来定制文档根目录和错误模板目录的位置。

    想要为你的网页应用添加一些交互元素吗?Hunchentoot 提供了一个易于使用的框架来创建处理程序。你可以使用 DEFINE-EASY-HANDLER 宏来定义处理程序,它允许你创建响应特定请求的函数。就像在你的小屋里添加一张舒适的沙发和一个熊熊燃烧的壁炉一样。 🛋️🔥

    以下是一个简单的处理程序示例,它向访客说“你好”:

    (hunchentoot:define-easy-handler (say-yo :uri "/yo") (name)
      (setf (hunchentoot:content-type*) "text/plain")
      (format nil "Hey~@[ ~A~]!" name))

    现在,如果你在浏览器中访问 http://127.0.0.1:4242/yo,你将看到“你好!”。如果你访问 http://127.0.0.1:4242/yo?name=Dude,你将看到“你好,Dude!”。就像用友好的“你好!”来迎接你的客人一样。 👋

    Hunchentoot 位于代理服务器之后:添加一个门廊 🚪

    如果你对将你的网页应用暴露在狂野的互联网中感到有点害羞,你可以把它隐藏在一个代理服务器之后。想象一下,这就像在你的小屋前添加一个门廊,提供一层保护和隐私。

    一种常用的方法是使用 Apache 的 mod_proxy 模块。你可以配置它,将请求隧道到在特定端口上运行的 Hunchentoot 服务器。就像有一个友好的邻居为你开门一样。

    接受者:你的网页服务器的守门人 👮‍♀️

    接受者是你的 Hunchentoot 网页服务器的守门人。它们负责监听传入连接和处理请求。想象一下,它们就像你小屋里的友善员工,欢迎访客并确保一切顺利运行。

    Hunchentoot 提供了几种类型的接受者,包括标准的 ACCEPTOR 和用于安全连接的 SSL-ACCEPTOR。你可以通过子类化 ACCEPTOR 类并专门化定义其行为的通用函数来定制接受者的行为。就像在你的小屋的运作方式中添加你自己的个人风格一样。

    会话:跟踪你的访客 👤

    会话允许你跟踪你的访客和他们在你的网页应用上的活动。想象一下,它们就像一本留言簿,你可以记录谁来过你的小屋以及他们做了什么。

    Hunchentoot 提供了自动会话处理的功能,既有使用 cookie 的,也有不使用 cookie 的。你可以通过专门化相应的通用函数来定制会话行为。就像在与你的客人互动的方式中添加个性化的风格一样。

    日志记录:记录事件 📝

    日志记录对于理解你的网页应用的性能和排查任何问题至关重要。想象一下,它就像记录你小屋所有活动的一本日记。

    Hunchentoot 提供了访问和消息的日志记录功能。你可以通过设置 ACCEPTOR-ACCESS-LOG-DESTINATIONACCEPTOR-MESSAGE-LOG-DESTINATION 设置来定制日志记录行为。就像拥有一本日记,你可以记录你小屋的所有重要事件一样。

    错误处理:处理意外的客人 🚨

    即使是构建最好的网页应用,也可能会遇到意想不到的问题。这就是错误处理的用武之地。想象一下,它就像有一个计划来处理可能出现在你小屋的意外客人。

    Hunchentoot 提供了可定制的错误处理机制,允许你优雅地捕获和处理错误。你可以定义错误模板,为你的访客提供信息丰富的消息。就像为你的网页服务器准备了一个储备充足的急救箱一样。

    第三方插件:扩展你的小屋 🏘️

    Hunchentoot 是一款流行的网页服务器,有许多第三方库和框架可以扩展其功能。想象一下,它们就像你小屋的附加部分,比如一个舒适的秋千或一个宽敞的客房。

    一些流行的插件包括 Clack,一个可以与 Hunchentoot 一起使用的网页服务器抽象层,以及 hunchentoot-cgi,它为 Hunchentoot 提供 CGI 处理程序。还有一些框架,比如 RESTASCavemanRadianceSnooze,它们与 Hunchentoot 兼容。就像有一群朋友可以帮助你建造和维护你的小屋一样。

    结论:Hunchentoot – 你的 Common Lisp 网页服务器探险之旅 🏕️

    Hunchentoot 是一款功能强大且用途广泛的网页服务器,它为在 Common Lisp 中构建动态网站提供了坚实的基础。它就像森林里的一间舒适小屋,随时准备让你根据自己的喜好进行定制和装饰。所以,拿起你的工具,收集你的物资,开始构建你自己的网页杰作吧!

    参考文献:

    • Hunchentoot – The Common Lisp web server formerly known as TBNL (https://edicl.github.io/hunchentoot/)
    • Quicklisp (https://www.quicklisp.org/)
    • Clack (https://github.com/clack/clack)
    • hunchentoot-cgi (https://github.com/cyrus-harmon/hunchentoot-cgi)
    • RESTAS (https://github.com/edicl/restas)
    • Caveman (https://github.com/edicl/caveman)
    • Radiance (https://github.com/edicl/radiance)
    • Snooze (https://github.com/edicl/snooze)
    • Weblocks (https://github.com/weblocks/weblocks)
  • 踏入联邦宇宙:你的博客新家 🏡

    你拥有一个博客,一个美丽的小角落,在那里你分享你的想法、理念,甚至可能还有猫的照片。但你可能感觉有点…局促。你渴望更广泛的受众,更丰富的社区,一个让你的文字真正自由漫游的地方。

    别担心,我的朋友,联邦宇宙在等着你!

    想象一下,联邦宇宙是一个巨大的、相互连接的独立社交平台网络,它们都说着同一种语言:ActivityPub。这种神奇的语言可以让你的博客无缝地将更新广播到一个全新的读者世界,而无需离开你的 WordPress 仪表板。

    想象一下:你是一个热爱复古游戏的作家。你倾注了你的心血在你的博客上,分享着深刻的评论和怀旧的故事。你梦想着与其他复古游戏玩家建立联系,但你的博客感觉被困在它自己的小角落里。

    ActivityPub 来了!

    只需一个简单的插件,你的博客就变成了一个联邦个人资料,一个准备好与联邦宇宙中各种人群交往的数字大使。

    让我们来分解一下:

    • 你的博客变成了一个社交中心:你的网站,比如 example.com,变成了一个社交实体。你将在 @example.com@example.com 找到一个博客范围的个人资料,并且你博客上的每个作者都将获得他们自己的个人资料。Jane,你的驻场游戏专家,变成了 @jane@example.com
    • 关注我,关注我! 想象一下,你在 Mastodon(一个流行的联邦宇宙平台)上,偶然发现了一位复古游戏玩家,@pfefferle@mastodon.social。你点击“关注”,然后,砰!每当 @pfefferle 发布帖子时,它都会出现在你的信息流中。现在,想象一下:有人在 Mastodon 上找到了 Jane 的个人资料,@jane@example.com,并点击了“关注”。Jane 在 example.com 上写的每一篇博客文章都会神奇地出现在他们的 Mastodon 信息流中!
    • 声音的交响曲: 通过关注博客范围的个人资料,@example.com@example.com,你将收到来自所有作者的更新,你最喜欢的博客中令人愉快的各种声音。

    但等等,还有更多!

    • 选择的权利: 你可以选择连接到哪些平台,从 Mastodon 到 Pixelfed 到 Friendica,以及更多。
    • 达到新的高度: 你的博客文章现在可以触及超出你网站边界的大量受众。
    • 与你的读者互动: 直接在你的博客上从联邦宇宙用户那里接收评论和回复。

    准备好开始了吗?

    1. 安装 ActivityPub 插件: 只需点击一下!
    2. 配置你的设置: 根据你的喜好调整插件,确保你的博客的作者个人资料页面处于活动状态。
    3. 加入联邦宇宙: 在 Mastodon 等平台上创建一个个人资料,并关注你的博客的个人资料。
    4. 发布一篇新帖子: 看着你的博客帖子神奇地出现在你的联邦宇宙信息流中!

    记住: 你的帖子可能需要几分钟才能出现在你的联邦宇宙信息流中,因为消息是使用延迟的 cron 发送的。别担心,你的帖子会到达,就像你最喜欢的复古游戏中精心安排的存档点一样。

    所以,你在等什么? 加入联邦宇宙,扩大你的影响力,让你的博客的声音在相互连接的社区的广阔网络中回荡。

    博客的未来就在这里,它是联邦的!


    注意: 这只是一个起点。联邦宇宙中充满了无限的可能性,等待着你去探索。

    敬请期待更多联邦宇宙的冒险!

  • Quicklisp:你的 Common Lisp 库一站式商店

    🎉 Quicklisp:Common Lisp 的库管理器 🎉

    Quicklisp 就如同 Common Lisp 的终极包管理器,让你轻松下载、安装和加载库。想象一下,它是 Lisp 世界的“apt-get”或“pip”,但又多了一丝 Lisp 的魔法。

    🚀 Quicklisp:适用于所有 Lisp 的通用工具 🚀

    Quicklisp 与众多 Common Lisp 实现完美兼容,包括 ABCL、Allegro CL、Clasp、Clozure CL、CLISP、CMUCL、ECL、LispWorks、MKCL、SBCL 和 Scieneer CL。它就像你的 Lisp 项目的瑞士军刀,支持你在 Linux、Mac OS X 和 Windows 等不同平台上工作。

    📦 超过 1,500 个库触手可及 📦

    Quicklisp 为你提供了庞大的库生态系统,超过 1,500 个库随时待命,准备融入你的项目。这就像拥有一个装满工具的宝箱,每个工具都旨在解决特定问题或增强你的 Lisp 之旅。

    ✨ Quicklisp 入门:快速指南✨

    1. 下载并加载: 从 Quicklisp beta 网站下载 Quicklisp 文件(quicklisp.lisp)并将其加载到你的 Common Lisp 会话中。
    2. 验证完整性: 为了更安心,下载分离的 PGP 签名文件并将其与 Quicklisp 发布签名密钥进行验证。
    3. 安装 Quicklisp: 加载完成后,运行 (quicklisp-quickstart:install) 来安装 Quicklisp。这将下载并安装必要的文件,包括 ASDF,一个 Common Lisp 的包管理器。
    4. 开始使用 Quicklisp: 安装完成后,你可以使用 (ql:quickload "system-name") 加载库。例如,要加载 vecto 库,你可以使用 (ql:quickload "vecto")

    💡 Quicklisp 实战:真实世界示例 💡

    假设你正在使用 Common Lisp 构建一个 Web 应用程序。你需要一个库来处理 HTTP 请求。Quicklisp 闪亮登场!

    1. 找到合适的库: 使用 (ql:system-apropos "http") 搜索与 HTTP 相关的库。Quicklisp 将返回一个可用库列表。
    2. 加载库: 选择最符合你需求的库,并使用 (ql:quickload "library-name") 加载它。
    3. 开始编码: 现在你就可以使用库的函数和特性来构建你的 Web 应用程序了。

    🚀 超越基础:Quicklisp 高级功能 🚀

    • 卸载库: 使用 (ql:uninstall "system-name") 从系统中删除库。
    • 更新 Quicklisp: 使用 (ql:update-dist "quicklisp")(ql:update-client) 保持你的 Quicklisp 安装和库更新。
    • 查找依赖项: 使用 (ql:who-depends-on "system-name") 发现哪些库依赖于特定库。
    • SLIME 集成: Quicklisp 使得安装和配置 SLIME 变得轻而易举,SLIME 是 Emacs 的强大 Lisp 开发环境。

    🎉 Quicklisp:社区的共同努力 🎉

    Quicklisp 是 Common Lisp 社区协作精神的证明。它是 Zachary Beane 开发和维护的项目,并得到了众多个人的贡献。你可以加入 Quicklisp 讨论组邮件列表或访问 Freenode 上的 #quicklisp 频道,与其他 Quicklisp 用户交流。

    💻 Quicklisp:Common Lisp 开发的未来 💻

    Quicklisp 彻底改变了 Common Lisp 开发,使访问和使用库变得比以往更容易。随着 Common Lisp 生态系统的不断发展,Quicklisp 将在赋能开发人员构建创新且强大的应用程序方面发挥越来越重要的作用。

    参考资料:

  • CFFI 炼丹:用“点”化繁为简 🧙‍♂️

    引言

    CFFI,这个强大的工具,让我们在 Lisp 中调用 C 函数,如同驾驭风火轮,穿梭于两个世界。然而,使用 CFFI 的 API 编写 C 风格代码,有时却像是在泥潭中跋涉,因为你需要不断地传递类型信息,而 C 中的“点”运算符却拥有着神奇的类型推断能力,让我们可以轻松地访问结构体成员。

    为了让 CFFI 也能像 C 一样优雅,我们打造了 cffi-ops 这个炼丹炉,它将 CFFI 的繁琐操作,炼化为简洁的“点”操作,让你在 Lisp 中写 C 代码,如同行云流水般流畅。

    炼丹秘籍:规则与对比

    cffi-ops 的炼丹秘籍,就是将 C 中的“点”运算符,映射到 Lisp 中的宏定义,让 Lisp 代码的结构与 C 代码保持一致。

    C 语法cffi-ops 语法
    x->y.zx->y->z(-> x y z) (注意:xyz 必须与 defcstruct 中定义的符号相同)
    &x->y(& (-> x y))
    *x([] x)
    x[n]([] x n)
    &x[n]x + n(& ([] x n))
    x.y = z(setf (-> x y) z) 如果 z 是一个变量
    (csetf (-> x y) z) 如果 z 是一个 CFFI 指针
    A _a, *a = &_a(clet ((a (foreign-alloca '(:struct A. ))) ...)
    A *a = malloc(sizeof(A. )(clet ((a (cffi:foreign-alloc '(:struct A. ))) ...)
    A _a = *b, *a = &_a(clet ((a ([] b))) ...)
    A *a = b(clet ((a b)) ...)

    炼丹炉的奥妙:CFFI 指针与 clet

    在 Lisp 中,我们无法直接操作 C 的复合类型,因此,绑定和赋值复合类型需要借助 clet (或 clet*) 和 csetf,它们作用于 CFFI 指针,实现对 C 数据的操控。

    炼丹师的助手:arrow-macros

    cffi-ops 的炼丹炉,是建立在 arrow-macros 的基础上,它提供了 -> 宏,让我们可以像在 C 中一样,轻松地访问结构体成员。因此,cffi-opsarrow-macros 相辅相成,让你在 Lisp 中编写 C 代码,更加得心应手。

    炼丹实例:向量加法

    让我们来看一个 C 代码的例子:

    #include <stdlib.h>
    #include <assert.h>
    
    typedef struct {
      float x;
      float y;
      float z;
    } Vector3;
    
    typedef struct {
      Vector3 v1;
      Vector3 v2;
      Vector3 v3;  
    } Matrix3;
    
    void Vector3Add(Vector3 *output, const Vector3 *v1, const Vector3 *v2) {
      output->x = v1->x + v2->x;
      output->y = v1->y + v2->y;
      output->z = v1->z + v2->z;
    }
    
    int main(int argc, char *argv[]) {
      Matrix3 m1[3];
      m1[0].v1.x = 1.0;
      m1[0].v1.y = 2.0;
      m1[0].v1.z = 3.0;
      Matrix3 m2 = *m1;
      Vector3 *v1 = &m2.v1;
      Vector3 *v2 = malloc(sizeof(Vector3));
      ,*v2 = *v1;
      v2->x = 3.0;
      v2->z = 1.0;
      Vector3Add(v1, v1, v2);
      assert(v1->x == 4.0);
      assert(v1->y == 4.0);
      assert(v1->z == 4.0);
      free(v2);
      return 0;
    }

    使用 cffi-ops,我们可以将其改写为 Lisp 代码:

    (defpackage cffi-ops-example
      (:use #:cl #:cffi #:cffi-ops))
    
    (in-package #:cffi-ops-example)
    
    (defcstruct vector3
      (x :float)
      (y :float)
      (z :float))
    
    (defcstruct matrix3
      (v1 (:struct vector3))
      (v2 (:struct vector3))
      (v3 (:struct vector3)))
    
    (defun vector3-add (output v1 v2)
      (clocally
        (declare (ctype (:pointer (:struct vector3)) output v1 v2))
        (setf (-> output x) (+ (-> v1 x) (-> v2 x))
              (-> output y) (+ (-> v1 y) (-> v2 y))
              (-> output z) (+ (-> v1 z) (-> v2 z)))))
    
    (defun main ()
      (clet ((m1 (foreign-alloca '(:array (:struct matrix3) 3))))
        (setf (-> ([] m1 0) v1 x) 1.0
              (-> ([] m1 0) v1 y) 2.0
              (-> ([] m1 0) v1 z) 3.0)
        (clet* ((m2 ([] m1))
                (v1 (& (-> m2 v1)))
                (v2 (foreign-alloc '(:struct vector3))))
          (csetf ([] v2) ([] v1))
          (setf (-> v2 x) 3.0
                (-> v2 z) 1.0)
          (vector3-add v1 v1 v2)
          (assert (= (-> v1 x) 4.0))
          (assert (= (-> v1 y) 4.0))
          (assert (= (-> v1 z) 4.0))
          (foreign-free v2))))

    而没有使用 cffi-ops 的 Lisp 代码则更加冗长:

    (defpackage cffi-example
      (:use #:cl #:cffi))
    
    (in-package #:cffi-example)
    
    (defcstruct vector3
      (x :float)
      (y :float)
      (z :float))
    
    (defcstruct matrix3
      (v1 (:struct vector3))
      (v2 (:struct vector3))
      (v3 (:struct vector3)))
    
    (declaim (inline memcpy))
    (defcfun "memcpy" :void
      (dest :pointer)
      (src :pointer)
      (n :size))
    
    (defun vector3-add (output v1 v2)
      (with-foreign-slots (((xout x) (yout y) (zout z)) output (:struct vector3))
        (with-foreign-slots (((x1 x) (y1 y) (z1 z)) v1 (:struct vector3))
          (with-foreign-slots (((x2 x) (y2 y) (z2 z)) v2 (:struct vector3))
            (setf xout (+ x1 x2) yout (+ y1 y2) zout (+ z1 z2))))))
    
    (defun main ()
      (with-foreign-object (m1 '(:struct matrix3) 3)
        (with-foreign-slots ((x y z)
                             (foreign-slot-pointer
                              (mem-aptr m1 '(:struct matrix3) 0)
                              '(:struct matrix3) 'v1)
                             (:struct vector3))
          (setf x 1.0 y 2.0 z 3.0))
        (with-foreign-object (m2 '(:struct matrix3))
          (memcpy m2 m1 (foreign-type-size '(:struct matrix3)))
          (let ((v1 (foreign-slot-pointer m2 '(:struct matrix3) 'v1))
                (v2 (foreign-alloc '(:struct vector3))))
            (memcpy v2 v1 (foreign-type-size '(:struct vector3)))
            (with-foreign-slots ((x z) v2 (:struct vector3))
              (setf x 3.0 z 1.0))
            (vector3-add v1 v1 v2)
            (with-foreign-slots ((x y z) v1 (:struct vector3))
              (assert (= x 4.0))
              (assert (= y 4.0))
              (assert (= z 4.0)))
            (foreign-free v2)))))

    两种代码在 SBCL 上生成几乎相同的机器码,性能也十分接近。

    总结

    cffi-ops 为我们提供了一种简洁高效的方式,让我们在 Lisp 中编写 C 代码,如同在 C 中一样自然流畅。它将 CFFI 的复杂操作,炼化为简洁的“点”操作,让我们可以专注于代码的逻辑,而不用被繁琐的类型信息所困扰。

    参考文献

    1. cffi-ops
    2. arrow-macros
    3. CFFI
  • 👴🏻 SMIL 驾鹤西去,万寿无疆!

    SMIL,SVG 的原生动画规范,曾经风光无限,凭借着其强大的功能和高效的渲染能力,在 SVG 动画领域呼风唤雨。然而,时过境迁,SMIL 的支持在 WebKit 中日渐式微,而微软的 IE 和 Edge 浏览器更是从未支持过 SMIL,也几乎不可能在未来支持。

    别担心!我们今天就来探讨一些 SMIL 特有的功能,并深入研究如何用其他方法来实现相同的效果,以确保你的动画能拥有更广泛的浏览器兼容性。

    🏃🏻‍♀️ 沿着路径运动

    SMIL 最吸引人的地方之一就是它能够让 SVG 对象沿着路径运动,从而实现更加逼真的动画效果。毕竟,现实生活中很少有物体是沿着直线运动的,沿着路径运动可以让我们模拟现实生活中的各种运动轨迹。

    在过去,你需要将 SVG 路径数据传递给 animateMotion 元素,并使用 path 属性来定义路径数据。然后,你可以通过 xlink:href 属性来指定要进行动画的元素。

    <animateMotion 
      xlink:href="#lil-guy" 
      dur="3s" 
      repeatCount="indefinite" 
      fill="freeze" 
      path="M 25,50 C 37.5,25 37.5,25 50,0 75,50 75,50 100,100 50,100 50,100 0,100 12.5,75 12.5,75 25,50 Z" />

    替代方案:CSS

    幸运的是,现在 CSS 也支持沿着路径运动的功能了!虽然目前支持的浏览器还不多(仅限于 Chrome、Opera 和 Android),但 Sara Soueidan 已经提议在 Edge 中加入该功能,并且得到了强烈的支持,在本文发布时已经获得了超过 420 票。请加入我们,一起呼吁该功能早日实现!Firefox 的投票页面 在这里

    至于 Safari,据我所知,它的支持情况可能需要单独处理。我已经注册了一个 bug 报告,并请求在 CSS 中添加沿着路径运动的功能。

    为了在 CSS 中使用沿着路径运动,你需要将路径数据传递给 offset-path 属性,就像这样:

    .move-me {
      offset-path: path('M3.9,74.8c0,0,0-106.4,75.5-42.6S271.8,184,252.9,106.9s-47.4-130.9-58.2-92s59.8,111.2-32.9,126.1 S5.9,138.6,3.9,74.8z');
    }

    我通常会在 Illustrator 中创建 SVG,然后使用 SVGOMG 进行优化,以获取路径数据。

    在这个例子中,我希望动画对象沿着路径从起点运动到终点,并且路径是一个闭合路径,因为路径数据末尾有一个 z。这意味着路径是一个循环,所以这个小生物最终会回到起点。我在关键帧中设置了这些参数,只指定了 100% 的值,因为默认值为 0

    @keyframes motionpathguy {
      100% {
        motion-offset: 100%;
      }
    }

    然后,将动画应用于元素:

    .move-me {
      animation: motionpathguy 10s linear infinite both;
    }

    替代方案:GreenSock 的沿着路径运动

    如果你想要最广泛的浏览器支持和最灵活的实现方式,那么你应该使用 GreenSock。GSAP 的 Bezier 插件(默认情况下包含在 TweenMax 中)支持 IE7 及更高版本(非 SVG 元素),以及 IE9 及更高版本(SVG 元素),这是目前最广泛的 SVG 动画支持。它在移动设备上也运行得很好。

    我之前在 David Walsh 博客上 写过关于这个插件的文章,但这里只是简要回顾一下,并介绍一些自那以后发布的新功能:

    最初,你需要传递一个值数组:

    bezier: {
      type: "soft",
      values:[{x:10, y:30}, {x:-30, y:20}, {x:-40, y:10}, {x:30, y:20}, {x:10, y:30}],
      autoRotate: true
    }

    但正如你所看到的,你还可以选择自动旋转(或不旋转),就像 SMIL 的 rotate 属性一样。如果你想使用 SMIL 中的 auto-reverseauto:n 参数来指定旋转的初始位置或旋转角度,GSAP 允许你使用 rotation:90 来更改旋转角度,或者如果你需要更精细的控制,可以使用更具体的设置:

    autorotate: [
      first position property, like "x",
      second position property, like "y",
      rotation property, typically "rotation" but can be “rotationY”,
      integer for radians/degrees the rotation starts from like 10,
      boolean for radians or degrees- radians is true
    ]

    在 SMIL 中,你可以对路径或组进行变换,以改变动画对象在运动过程中的方向。在 GSAP 中,你可以通过 autoRotate: false 轻松实现这一点,并使用 set 初始化旋转。你也可以像在 SMIL 中一样,在 SVG 属性本身对元素进行变换,但这有点不太优雅,而且在工作时更难跟踪。

    TweenMax.set("#foo" {
      rotation: 90 // or whatever number
    });

    你还可以将 type 属性设置为 thrusoftquadraticcubic。有关这些属性的更多文档,请查看 GreenSock API 文档thru 属性的一个很好的用途是能够影响元素的弯曲程度。如果你将这些点视为弹跳的坐标,那么弯曲程度将控制在这些点之间采取的路径的直接程度。0 表示直线路径,1 表示稍微松散的路径,2 表示一个漂亮的曲线,而 3 及更高的值将开始在自身上缠绕。

    graph LR
      subgraph 0
        A[0]
      end
      subgraph 1
        B[1]
      end
      subgraph 2
        C[2]
      end
      subgraph 3
        D[3]
      end
      A --> B
      B --> C
      C --> D

    最近,GreenSock 也提供了将路径数据传递给 CSS 和 SMIL 模块的能力,就像使用原生 SMIL 一样。这作为他们 MorphSVG 插件的扩展,因此你需要添加该插件,并像这样使用它:

    TweenMax.to("#lil-guy", 3, {
      bezier: {
        MorphSVGPlugin.pathDataToBezier("#path", {align: "#lil-guy" }), 
        type: "cubic"
      },
      ease: Linear.easeNone,
      repeat: -1
    });
    <path id="path" d="M 25,50 C 37.5,25 37.5,25 50,0 75,50 75,50 100,100 50,100 50,100 0,100 12.5,75 12.5,75 25,50 Z" fill="none" />

    默认情况下,会将我要进行动画的组(在本例中为 #lil-guy)的左上角与路径轨迹对齐。这会导致视觉上的错位。因此,我使用 TweenLite.set#lil-guy 设置为使用中心点:

    TweenLite.set("#lil-guy", {xPercent:-50, yPercent:-50});

    你还可以通过将一个对象作为该方法的第二个参数传递,并在 pathDataToBezier 中定义 offsetXoffsetY 来偏移这些路径,注意,你可能需要扩展 viewBox,以确保你正在进行动画的组或属性不会被裁剪掉。

    // 将路径坐标在 x 轴上偏移 125px,在 y 轴上偏移 50px:
    TweenMax.to("#lil-guy", 3, {
      bezier: {
        values: MorphSVGPlugin.pathDataToBezier("#path", {
          offsetX: 125, 
          offsetY: 50, 
          align: "#lil-guy"
        }),
        type: "cubic"
      },
      ease: Linear.easeNone,
      repeat: -1
    });

    你甚至可以为这个定位定义一个矩阵坐标。

    // 将路径坐标放大 1.25 倍
    // 并将其在 x 轴上偏移 120px
    // 在 y 轴上偏移 30px:
    TweenMax.to("#lil-guy", 3, {
      bezier: {
        values: MorphSVGPlugin.pathDataToBezier("#path", {
          matrix:[1.5,0,0,1.5,120,-30], 
          align:"lil-guy"}),
        type: "cubic"
      },
      ease: Linear.easeNone,
      repeat: -1
    });

    另一个选择是将 align 属性设置为 “relative”。这将防止动画对象跳跃,因为它会保持每个坐标相对于 x:0y:0 的位置。在之前的示例中,我使用 align 将运动与 #lil-guy 组本身配对。

    有关 GreenSock 的 Bezier 插件 API 中这个新功能(新是指在本文发布之日新发布的功能!)的更多信息,请查看他们的 文档,以及这个 很棒的解释视频

    🎭 形状变形

    以前,你可以将路径数据作为值传递给 animate 属性,以使形状变形。Noah Blon 有一个很好的例子:

    替代方案:Snap.svg 或 SVG Morpheus

    一些库提供了变形路径或形状值,例如 Snap.svg 和 SVG Morpheus,但需要注意的是(即使在 SMIL 中也是如此),形状必须具有相同数量的点,否则变形看起来很糟糕,或者完全失败。这在预处理方面令人失望,因为这意味着你必须仔细跟踪你正在制作的内容,或者与你的设计师良好协作,以确保你获得这些(有时是任意的)中间点数据。额外的点也会不必要地膨胀你的代码。

    替代方案:GreenSock MorphSVG

    我强烈推荐 GSAP 的 MorphSVG 插件,因为它可以很好地变形具有不同数量点的形状和路径。请查看本网站徽标上的切换按钮,以演示变形的效果。这里还有另一个例子:

    https://codepen.io/sdras/pen/XqYxoy

    因为 MorphSVG 插件可以对路径数据进行动画处理,所以如果你需要转换形状,可以使用他们的 convertToPath 选项:

    MorphSVGPlugin.convertToPath("ellipse"); 
    // or circle, rect, etc

    这使我们能够进行非常复杂的形状动画,并且是 Web 上所有运动的改变者。

    这个插件还提供了一些额外的功能,使其更加出色。第一个是实用程序插件 findShapeIndex。假设你对形状的变形方式不满意(虽然十有八九自动预设会正常工作),你可以加载该插件(别担心,你不需要在生产中添加额外的重量,因为它不需要),并将两个值传递给它:要进行动画的第一个形状的 ID 和第二个形状的 ID。一个 GUI 会弹出,你可以在其中切换值,它还会自动使用 repeat: -1,以便它会不断地在形状之间循环。

    findShapeIndex("#hex", "#star");
    // you can comment out above line to automatically disable findShapeIndex() UI

    一旦你有了这个额外的值,你就可以在 morphSVG 对象中传递 shapeIndex

    TweenLite.to("#hex", 1, {morphSVG: { shape: "#star", shapeIndex: 1 }});

    第二个额外的功能是该插件能够解析剪切路径,这是其他库无法提供的。最后,你还可以重用第一个起始 ID(而不是必须存储该路径数据以供重用)。值得一提的是,当该插件首次发布时,这些功能不可用,但 GreenSock 认识到需要支持这些功能,因此将其包含在内。

    现在,我们不再受限于指定的点数,我们拓宽了各种效果的可能性。下面,我制作了一些烟雾:

    https://codepen.io/sdras/pen/yXqXzY

    🖱️ DOM 事件

    SMIL 中很好地集成了诸如悬停和点击之类的事件。为了启动动画,可以指定 begin="click"begin="hover"

    <animate 
        xlink:href="#rectblue"
        attributeName="x"
        from="0"
        to="300" 
        dur="1s"
        begin="click"
        values="20; 50"
        keyTimes="0; 1"
        fill="freeze" />

    替代方案:JavaScript

    有像 onmouseenteronmouseleave 这样的原生 DOM 事件,用于悬停,以及 click 事件,用于点击。你可以使用它们来更改事件触发器,从而触发基于 JavaScript 的动画。

    替代方案:JavaScript + CSS

    你可以使用 JavaScript 来更改类名或直接更改 CSS 样式。以下是一种可能性:更改 animation-play-state 以从事件触发器启动动画。

    .st0 {
      animation: moveAcross 1s linear both;
      animation-play-state: paused;
    }
    
    @keyframes moveAcross {
      to {
        transform: translateX(100px);
      }
    }
    document.getElementById("rectblue").addEventListener("click", function() {
      event.target.style.animationPlayState = "running";
    });

    或者在 jQuery 中:

    $(".st0").on("click", function() {
      $(this).css("animation-play-state", "running");
    });

    这种实现不会像 SMIL 示例那样立即将动画重置到开头。如果你想实现这一点,CSS-Tricks 上的一篇之前的文章详细介绍了几种实现方法。

    替代方案:Greensock

    在 GSAP 中,重启更加简单。我们可以将动画添加到时间线中,将其设置为暂停,然后在点击时重启它。这种实现更接近你对 SMIL 的预期,因为我们不需要做任何 hacky 的事情,比如克隆/重新插入 DOM 节点或更改元素上设置的任何属性。

    // 实例化一个 TimelineLite
    var tl = new TimelineLite();
    
    // 将一个动画添加到时间线
    tl.to(foo, 0.5, { left: 100 });
    
    $(".st0").on("click", function() {
      tl.restart();
    });

    ⏱️ 在“Y”完成后运行“X”

    SMIL 还允许更复杂的时间事件,例如 begin="circ-anim.begin + 1s"。这在链接动画时特别有用。

    替代方案:CSS

    在 CSS 中,我们可以通过在第二个值上设置延迟来链接动画:

    .foo {
      animation: foo-move 2s ease both;
    }
    
    .bar {
      animation: bar-move 4s 2s ease both; 
      /* 2 秒的值对应于第一个动画的迭代长度。 */
    }

    这种方法有点令人沮丧,因为你必须确保记住更改第一个间隔以及延迟。

    替代方案:CSS 预处理

    如果我们使用(例如)Sass 中的变量,维护和管理这些间隔会更容易:

    $secs: 2s;
    
    .foo {
      animation: foo-move $secs ease both;
    }
    
    .bar {
      animation: bar-move 4s $secs ease both; 
    }

    现在我们知道,如果我们更新一个值,它们将保持同步。

    但是,如果我们想始终检测动画何时完成,JavaScript 提供了一些不错的原生功能,比如 animationEnd

    $("#rectblue").on("animationend", function() {   
      $(this).closest("svg").find("#rectblue2").css("animation-play-state", "running");     
    });
    
    #rectblue2 {
      animation: moveAcross 2s 1s ease both;
      animation-play-state: paused;
    }

    🕰️ 计时器

    SMIL 还提供了一个 set 属性,它允许你设置一个计时器,以便在指定的时间后执行某些操作。

    <set 
        attributeName="visibility" 
        to="visible" 
        begin="2s" 
        fill="freeze" />

    替代方案:JavaScript

    我们可以使用 setTimeout 来实现相同的行为。

    setTimeout(function() {
      document.getElementById("rectblue").style.visibility = "visible";
    }, 2000);

    替代方案:GreenSock

    GreenSock 提供了 delay 属性,可以实现相同的效果。

    TweenLite.to("#rectblue", 0, {
      delay: 2,
      visibility: "visible"
    });

    🔄 循环

    SMIL 允许你通过 repeatCount 属性来控制动画的循环次数。

    <animateMotion 
      xlink:href="#lil-guy" 
      dur="3s" 
      repeatCount="indefinite" 
      fill="freeze" 
      path="M 25,50 C 37.5,25 37.5,25 50,0 75,50 75,50 100,100 50,100 50,100 0,100 12.5,75 12.5,75 25,50 Z" />

    替代方案:CSS

    在 CSS 中,你可以使用 animation-iteration-count 属性来控制动画的循环次数。

    .foo {
      animation: foo-move 2s ease infinite both;
    }

    infinite 值意味着动画将无限循环。你也可以指定一个具体的数字,例如 animation-iteration-count: 3,表示动画将循环三次。

    替代方案:GreenSock

    GreenSock 提供了 repeat 属性,可以实现相同的行为。

    TweenLite.to("#lil-guy", 3, {
      bezier: {
        values: MorphSVGPlugin.pathDataToBezier("#path", {align: "#lil-guy" }), 
        type: "cubic"
      },
      ease: Linear.easeNone,
      repeat: -1
    });

    repeat: -1 表示动画将无限循环。你也可以指定一个具体的数字,例如 repeat: 3,表示动画将循环三次。

    🎬 动画组

    SMIL 允许你使用 animate 元素来创建动画组,并通过 begin 属性来控制组内动画的执行顺序。

    <animate 
        xlink:href="#rectblue"
        attributeName="x"
        from="0"
        to="300" 
        dur="1s"
        begin="click"
        values="20; 50"
        keyTimes="0; 1"
        fill="freeze" />
    
    <animate 
        xlink:href="#rectblue"
        attributeName="y"
        from="0"
        to="300" 
        dur="1s"
        begin="click + 1s"
        values="20; 50"
        keyTimes="0; 1"
        fill="freeze" />

    替代方案:CSS

    在 CSS 中,你可以使用 animation-delay 属性来控制动画的延迟时间。

    .foo {
      animation: foo-move 2s ease both;
    }
    
    .bar {
      animation: bar-move 4s 1s ease both; 
      /* 1 秒的值对应于第一个动画的迭代长度。 */
    }

    这种方法有点令人沮丧,因为你必须确保记住更改第一个间隔以及延迟。

    替代方案:CSS 预处理

    如果我们使用(例如)Sass 中的变量,维护和管理这些间隔会更容易:

    $secs: 2s;
    
    .foo {
      animation: foo-move $secs ease both;
    }
    
    .bar {
      animation: bar-move 4s $secs ease both; 
    }

    现在我们知道,如果我们更新一个值,它们将保持同步。

    替代方案:GreenSock

    GreenSock 提供了 TimelineLite 类,可以用来创建动画组,并通过 delay 属性来控制组内动画的执行顺序。

    var tl = new TimelineLite();
    
    tl.to("#rectblue", 1, { x: 300 });
    tl.to("#rectblue", 1, { y: 300 }, "+=1");

    +=1 表示第二个动画将在第一个动画完成 1 秒后开始。

    总结

    SMIL 曾经是 SVG 动画的王者,但随着浏览器支持的减少,我们不得不寻找其他替代方案。幸运的是,CSS、JavaScript 和 GreenSock 等工具提供了强大的功能,可以让我们实现 SMIL 中的所有功能,甚至更多。

    选择哪种方法取决于你的需求和偏好。如果你需要最广泛的浏览器支持,那么 GreenSock 是一个不错的选择。如果你更喜欢使用 CSS,那么 CSS 动画是一个不错的选择。如果你需要更灵活的控制,那么 JavaScript 是一个不错的选择。

    无论你选择哪种方法,都希望你能够轻松地创建出令人惊叹的 SVG 动画!

    参考文献

    1. SMIL Is Dead! Long Live SMIL! A Guide To Alternatives To SMIL Features | CSS-Tricks
    2. GreenSock Animation Platform
    3. Snap.svg
    4. SVG Morpheus
    5. David Walsh 博客
  • 🚀 WebTransport:让网络通信更轻更快!

    WebTransport 就像是一场网络通信的革命,它以 HTTP/3 协议为基础,为我们打开了低延迟双向通信的大门。想象一下,未来的网页不再受限于传统的 TCP 连接,而是可以像 UDP 那样自由地发送数据,同时又拥有 HTTP/3 的可靠性。WebTransport 就如同网络世界的“高速公路”,让数据在客户端和服务器之间自由穿梭,为我们带来前所未有的体验。

    🐢 WebTransport 的前世今生

    WebTransport 的诞生并非偶然,它承袭了早期 QuicTransport 的理念,但更进一步,以 HTTP/3 协议为基础,打造了一个更加通用、更易于使用的网络通信 API。WebTransport 的核心在于它既支持数据报 API,也支持数据流 API,满足了不同应用场景的需求。

    数据报 API 就像是网络世界的“快递小哥”,它可以快速地发送和接收数据,但并不保证数据传输的顺序和可靠性。这对于那些对延迟要求极高的应用场景,例如实时游戏、视频直播等,非常适用。

    数据流 API 则像是网络世界的“物流公司”,它可以保证数据的可靠性、有序性,并支持多路数据流的传输。这对于那些需要可靠传输数据的应用场景,例如文件上传、下载等,非常适用。

    💡 WebTransport 的应用场景

    WebTransport 的应用场景非常广泛,它可以用于:

    • 实时游戏:通过数据报 API,以最短延迟时间向服务器发送游戏状态,实现流畅的游戏体验。
    • 媒体流:以极低的延迟接收服务器推送的媒体流,例如视频直播、音频通话等。
    • 实时通知:在网页打开时接收服务器推送的通知,例如消息提醒、更新提示等。

    🤝 WebTransport 与其他技术的比较

    WebTransport 的出现并非要取代现有的网络通信技术,而是提供了一种新的选择,为开发者提供了更多可能性。

    • WebTransport vs. WebSocket:WebTransport 的数据流 API 可以替代 WebSocket,但数据报 API 则提供了 WebSocket 不具备的低延迟特性。WebTransport 在建立连接时也比 WebSocket 更加高效。
    • WebTransport vs. UDP Socket API:WebTransport 并非简单的 UDP Socket API,它在加密和拥塞控制方面进行了优化,更加安全可靠。
    • WebTransport vs. WebRTC 数据通道:WebTransport 可以替代 WebRTC 数据通道,用于客户端-服务器连接,但它不支持点对点通信。WebTransport 的使用也比 WebRTC 更简单易用。

    💻 如何使用 WebTransport

    使用 WebTransport 非常简单,只需要创建 WebTransport 实例并连接到服务器即可。WebTransport 提供了三种不同的流量类型:数据报、单向数据流和双向数据流。

    连接到服务器

    const url = 'https://example.com:4999/foo/bar';
    const transport = new WebTransport(url);
    
    // 等待连接建立
    await transport.ready;
    
    // ...

    使用数据报 API

    // 发送数据报
    const writer = transport.datagrams.writable.getWriter();
    const data1 = new Uint8Array([65, 66, 67]);
    writer.write(data1);
    
    // 接收数据报
    const reader = transport.datagrams.readable.getReader();
    while (true) {
      const { value, done } = await reader.read();
      if (done) {
        break;
      }
      console.log(value);
    }

    使用数据流 API

    // 创建单向数据流
    const stream = await transport.createUnidirectionalStream();
    const writer = stream.writable.getWriter();
    const data1 = new Uint8Array([65, 66, 67]);
    writer.write(data1);
    
    // 接收单向数据流
    const rs = transport.incomingUnidirectionalStreams;
    const reader = rs.getReader();
    while (true) {
      const { done, value } = await reader.read();
      if (done) {
        break;
      }
      // value is an instance of WebTransportReceiveStream
      await readFrom(value);
    }

    🚧 WebTransport 的未来

    WebTransport 作为一项新兴技术,正在不断发展完善。未来,WebTransport 将会更加强大,更加易用,为开发者提供更多可能性,推动 Web 应用的发展。

    📚 参考文献

    1. WebTransport 说明
    2. WebTransport 草稿规范
    3. WebTransport GitHub 代码库
    4. webtransport-ponyfill-websocket
    5. Web Transport GitHub 代码库
  • 💬 让你的网站评论区,摇身一变成 GitHub Discussions!

    💬 让你的网站评论区,摇身一变成 GitHub Discussions!

    你是否厌倦了传统网站评论系统的单调乏味? 想要一个更灵活、更强大、更具社区感的评论系统?别再犹豫,快来体验 giscus 吧!它利用了 GitHub Discussions 的强大功能,将你的网站评论区瞬间升级为一个充满活力的社区中心。

    giscus 的核心思想简单明了: 将你的网站评论与 GitHub Discussions 关联起来,让访客在 GitHub 上直接参与讨论。这不仅能充分利用 GitHub 的强大功能,还能让你的网站评论区更具吸引力。

    想象一下: 你的网站评论区不再是简单的留言板,而是变成了一个充满活力的 GitHub Discussions 页面。访客可以轻松地点赞、评论、关注讨论,甚至还可以使用 GitHub 的强大功能,例如代码块、图片、链接等,让讨论更加丰富多彩。

    giscus 的魅力不止于此:

    🚀 无缝集成: giscus 的安装和配置极其简单,只需添加几行代码,就能将它集成到你的网站中。

    🌐 多语言支持: giscus 支持多种语言,让你可以轻松地为全球用户提供本地化的评论体验。

    🎨 自定义主题: giscus 提供多种主题,让你可以根据网站风格轻松定制评论区的视觉效果。

    🛡️ 安全可靠: giscus 采用 GitHub 的安全机制,确保你的评论区安全可靠。

    💡 灵活配置: giscus 提供多种配置选项,让你可以根据自己的需求定制评论区的功能和外观。

    那么,giscus 究竟是如何运作的呢?

    它就像一个神奇的桥梁,连接着你的网站和 GitHub Discussions。 当访客访问你的网站时,giscus 会使用 GitHub Discussions 搜索 API 查找与当前页面关联的讨论。如果找不到匹配的讨论,giscus 会自动创建一个新的讨论。

    访客可以以两种方式参与讨论:

    • 直接在 GitHub Discussions 中评论: 访客可以直接在 GitHub Discussions 页面上评论,就像在任何其他 GitHub 仓库中一样。
    • 通过 GitHub OAuth 授权: 访客可以通过 GitHub OAuth 授权 giscus app 代表他们发布评论。

    无论访客选择哪种方式,你都可以在 GitHub 上管理评论,就像管理任何其他 GitHub 仓库一样。

    giscus 的配置过程也非常简单:

    首先,你需要选择一个公开的 GitHub 仓库,将你的网站评论与它关联起来。 然后,你需要选择页面与讨论之间的映射关系。giscus 提供多种映射方式,例如:

    • 根据页面路径: giscus 会查找标题中包含页面路径的讨论。
    • 根据页面 URL: giscus 会查找标题中包含页面 URL 的讨论。
    • 根据页面标题: giscus 会查找标题中包含页面标题的讨论。
    • 根据页面元数据: giscus 会查找标题中包含页面元数据的讨论。
    • 根据特定字符串: giscus 会查找标题中包含特定字符串的讨论。
    • 根据特定讨论号: giscus 会直接加载特定编号的讨论。

    最后,你需要选择一些额外的配置选项,例如:

    • 启用主帖子上的反应: 访客可以在主帖子上添加反应,例如点赞、赞赏、疑问等。
    • 输出讨论的元数据: giscus 可以将讨论的元数据发送到父页面,方便你进行分析和处理。
    • 将评论框放在评论上方: 访客可以更容易地发表评论,而无需滚动到讨论底部。
    • 懒加载评论: 评论的加载会延迟到用户滚动到评论容器附近,提高页面加载速度。

    giscus 的强大功能远不止这些,它还提供了丰富的主题选项,让你可以根据网站风格轻松定制评论区的视觉效果。

    如果你正在寻找一个强大、灵活、易于使用的网站评论系统,那么 giscus 绝对是你的不二之选!

    立即体验 giscus,让你的网站评论区焕然一新!

    参考:

    1. giscus
    2. GitHub Discussions
    3. utterances
    4. gitalk
    5. CONTRIBUTING.md
  • 🚀 Kivy: 跨平台应用开发的魔法棒

    在这个日新月异的数字时代,应用开发已经成为了一门炙手可热的技能。但是,面对iOS、Android、Windows、macOS等林林总总的平台,开发者们常常会感到无从下手。如果有一根魔法棒,能够让你一次编写,到处运行,那该有多好?今天,就让我来为你介绍这样一根魔法棒 —— Kivy框架。

    🌈 Kivy:一次编写,处处运行

    Kivy是一个开源的Python库,用于开发跨平台的应用程序。它就像是一个神奇的调色板,让你可以用Python这支画笔,在不同的平台画布上绘制出色彩斑斓的应用程序。

    想象一下,你正在画一幅美丽的风景画。使用Kivy,你只需要画一次,这幅画就可以同时挂在客厅(桌面电脑)、卧室(平板电脑)、甚至是随身携带(智能手机)。是不是很神奇?

    Kivy支持的平台包括:

    • 桌面系统:Windows、macOS、Linux、*BSD Unix
    • 移动设备:iOS(iPad、iPhone)、Android(平板、手机)
    • 其他支持TUIO(Tangible User Interface Objects)的触控设备

    🎨 Kivy的魔力:NUI和图形引擎

    Kivy的核心魔力来自于其自然用户界面(NUI)工具包和强大的图形引擎。就像魔法师挥舞魔杖一样,Kivy让你能够轻松创建出富有视觉吸引力和交互性的应用界面。

    想象一下,你正在设计一个互动游戏。使用Kivy,你可以轻松实现各种炫酷的效果:

    • 多点触控:就像变魔术一样,同时响应多个手指的触摸。
    • 粒子系统:创造出绚丽的视觉特效,仿佛魔法粉末在空中飘散。
    • GPU加速:让你的应用运行得如此流畅,仿佛被施了加速魔法。

    🧙‍♂️ Kivy语言:你的专属咒语书

    Kivy不仅提供了强大的Python API,还创造了一种特殊的”咒语”—— Kivy语言。这种声明式语言就像是一本魔法书,让你能够用简洁优雅的方式描述用户界面。

    例如,创建一个按钮就像念一个简单的咒语:

    Button:
        text: 'Hello World'
        on_press: print("Button pressed!")

    这个”咒语”会召唤出一个写着”Hello World”的按钮,当你点击它时,就会在控制台打印出”Button pressed!”。是不是很神奇?

    🎓 学习Kivy:从学徒到魔法师

    成为一名Kivy魔法师并不难,你只需要掌握一些基本的Python知识。如果你还不熟悉Python,不用担心,这里有一些excellent的学习资源:

    1. Python官方教程:系统全面,适合深入学习。
    2. “10分钟学Python”:快速入门,适合尝鲜。
    3. “笨方法学Python”:强调实践,适合动手能力强的学习者。

    一旦你掌握了基本的Python技能,就可以开始你的Kivy魔法之旅了。本教程将带领你:

    1. 学习Kivy语言的基础知识
    2. 探索Kivy框架的核心概念
    3. 创建一个简单的跨平台应用
    4. 为不同平台打包你的应用
    5. 最后,将你的作品部署到目标设备上

    每个章节都会介绍一个新主题,给你足够的信息开始实践,并提供深入学习的链接。当你完成这个教程后,你就能独立开发Kivy应用了,并且知道在遇到困难时该去哪里寻找答案。

    🚀 开始你的魔法之旅

    好了,理论知识已经足够了,让我们开始动手实践吧!首先,我们需要安装Kivy这根魔法棒。

    安装Kivy最简单的方法就是使用pip。就像施展一个简单的咒语:

    python -m pip install "kivy[base]" kivy_examples

    这个咒语会从PyPI(Python包索引)中召唤出Kivy及其基本依赖,以及一些示例程序。

    如果你想要更多魔法效果(比如音频和视频支持),可以使用这个增强版咒语:

    python -m pip install "kivy[full]"

    安装完成后,你可以通过运行一个简单的示例来测试是否安装成功:

    python kivy_venv/share/kivy-examples/demo/showcase/main.py

    如果一切顺利,你应该能看到一个漂亮的演示程序,展示了Kivy的各种魔法效果。

    🎉 结语

    恭喜你!你已经成功安装了Kivy这根强大的魔法棒,并且对它有了初步的了解。接下来,你将开始一段激动人心的旅程,学习如何用Kivy创造出令人惊叹的跨平台应用。

    记住,每个伟大的魔法师都是从学徒开始的。保持好奇心,不断实践,你终将成为Kivy的大师级魔法师!

    让我们一起期待你用Kivy创造出的第一个魔法作品吧!

    参考文献

    1. Kivy. (2023). Kivy: Cross-platform Python Framework for NUI Development. https://kivy.org/
    2. Kivy Team. (2023). Installing Kivy. Kivy Documentation. https://kivy.org/doc/stable/gettingstarted/installation.html
    3. Python Software Foundation. (2023). The Official Python Tutorial. https://docs.python.org/3/tutorial/
    4. Shaw, Z. A. (2013). Learn Python the Hard Way. Addison-Wesley Professional.
    5. Guido van Rossum. (2009). The History of Python. Python Software Foundation. https://python-history.blogspot.com/
  • 🖱️ BubbleZone:为你的终端应用注入交互活力

    🤯 终端应用开发的苦恼

    想象一下,你正在用 BubbleTea 和 Lipgloss 构建一个炫酷的终端应用。你已经熟练地使用它们抽象出布局、颜色、事件等元素,轻而易举地创建出一个用户友好的界面。你甚至用上了 BubbleTea 的鼠标事件支持,实现了像按钮点击、区域悬停等功能。

    一切看起来都很美好,直到你的应用开始变得复杂起来。多个组件层层嵌套,子组件又有自己的子组件,就像一个错综复杂的迷宫。这时,你想要实现一个简单的功能:点击某个按钮触发特定操作。然而,你需要先定位鼠标点击的位置,然后一层层地向上遍历组件树,判断哪个组件才是被点击的目标。这就像你要在迷宫里找到特定的一粒沙子,费时费力,让人头疼不已。

    ✨ BubbleZone:化解复杂,精准定位

    BubbleZone 正是为了解决这个问题而诞生的。它就像是在迷宫里为每一粒沙子都做了标记,让你能够轻松地找到它们。

    BubbleZone 的工作原理是什么呢?

    1. 标记区域: 使用 zone.Mark() 函数,你可以为任何想要监听鼠标事件的组件添加一个独特的 ID,就像给它们贴上了一张隐形的标签。
    2. 扫描区域: 在根组件的 View() 函数中,使用 zone.Scan() 函数包裹整个应用的输出。这个函数会扫描所有被标记的区域,记录它们的位置信息,并生成一个区域地图。
    3. 精准定位: 当鼠标事件发生时,BubbleZone 会根据区域地图快速定位到被点击的区域,并触发相应的操作。

    🚀 BubbleZone 的优势

    • 高效: BubbleZone 的设计目标之一就是速度快。它在每次渲染时都需要处理区域信息,因此开发者非常注重性能优化,力求将性能影响降到最低。
    • 精准: BubbleZone 能够精准地定位到被点击的区域,即使是在复杂的组件嵌套结构中也能准确无误。
    • 易用: BubbleZone 使用起来非常简单,你只需要几行代码就能为你的应用添加鼠标事件支持。

    💡 使用技巧

    为了帮助你更好地使用 BubbleZone,以下是一些实用技巧:

    • 避免区域重叠: 为了避免不同组件的区域 ID 发生冲突,建议使用 NewPrefix() 函数生成一个唯一的区域前缀。
    • 使用 lipgloss.Width() 计算宽度: BubbleZone 专门针对 lipgloss.Width() 函数进行了优化,确保区域标记不会影响宽度计算结果。
    • 谨慎使用 MaxHeight()MaxWidth() 这两个函数会对组件进行硬裁剪,如果区域标记被裁剪掉,就会导致区域定位失效。
    • 只在根组件中使用 zone.Scan() zone.Scan() 函数应该只在根组件的 View() 函数中使用一次,否则可能会导致区域定位错误。
    • 注意非矩形区域的边界问题: BubbleZone 的区域边界是矩形的,如果你的组件是非矩形的,例如圆形,需要确保区域标记足够大,能够完全覆盖整个组件。

    🎉 让你的终端应用充满活力

    BubbleZone 为你的终端应用带来了前所未有的交互体验,让你的应用不再是冰冷的命令行工具,而是充满活力的交互式应用。

    参考文献:

人生梦想 - 关注前沿的计算机技术 acejoy.com