杜尼卜

杜尼卜 查看完整档案

上海编辑湖南人文科技学院  |  计算机软件 编辑潘帕斯  |  Web前端 编辑 zhangbing.site 编辑
编辑

做工程师不做码农、全栈开发工程师、持续学习者

📬 微信公众号:前端全栈开发者
📘 博客主页:https://zhangbing.site
🎁 混饭小铺:https://store.zhangbing.site
🛎 编程日历小程序:https://creatorsdaily.com/f45...

个人动态

杜尼卜 赞了文章 · 4月9日

终于来了,IDEA 2021.1版本正式发布,完美支持WSL 2

先点赞再看,养成好习惯

IntelliJ IDEA 2021.1 EAP 版本已经发布了很久,就在今天,终于等到正式版的发布。这个大版本最大的更新内容,就是支持 WSL 2 和 JAVA 16 了。而且除了支持 WSL 2,也支持其他形式的 “ssh 远程运行”,就像 clion 那样;让你的 java 程序开发在本地,而运行在远程。

赶紧来看看,2021 年这个大版本有哪些更新内容吧!

WSL 2 的支持


都说 Windows 是 Linux 最好的发行版,可是你的 IDE 不支持 WSL 运行那又有何用呢?

现在 IDEA 终于支持了 WSL 2,让我们可以再 Windows 上开发,而运行在 WSL 2 环境下,像 JDK、构建环境(maven/gradle)都可以是 WSL 2 系统中的,实在太爽了。

以后就可以完全用 WSL 2 来进行开发了,日常 Windows,所有开发环境全部 wsl,而且文件系统也是打通的,完全没理由拒绝!

详细的 Windows 10 安装 WSL 2 的教程,可以参见微软的官方文档,跟着文档一步步来就可以了,非常简单。
Windows Subsystem for Linux Installation Guide for Windows 10

运行目标


运行目标,这个功能太香了。我们的程序不光可以运行在本地,在 WSL 2,在远程 SSH 主机,还可以再 Docker 中,一键运行在 Docker。

而且 Docker 对 WSL 2 的支持也非常好,我们还可以运行在 WSL 2 中的 Docker,同时用 Windows 中的 Docker 管理工具,真香!

内置的 HTML 预览器


在 HTML 文件中,只需要点击右上角的 IDEA 图标,就可以使用内置预览器去预览网页了,而且实时刷新,再也不用打开浏览器预览。

搜索范围的增强

以后我们在搜索时,还可以添加外部的依赖到作用域中,完成更全面的搜索。设置入口在Preferences/Settings | Appearance & Behavior | Scopes

Windows 版本的任务栏增强


在任务栏中,对 IDEA 右键会出现最近的项目

增强的 Pull Request 支持


你的提交 PR 操作,以后只需要在 Pull Request 面板中进行了,再也不用命令和网页

支持 Git 提交模板

和其他分支对比文件

现在可以再_Compare with branch_弹框中,与其他分支对比文件了

拆分窗口优化


垂直分割编辑器窗口后,双击 Tab 就可以将当前窗口最大化,再次双击会还原

JSON Path 的支持



以后打开.json文件时,就可以用 JSON Path 过滤 / 转换 / 输出了

JAVA 16 的支持


IDEA 2021.1 版本已经支持了 JAVA 16

更智能的数据检查



IDEA 现在会提示你一些基本的错误,比如数据长度为负数,提示你拆箱装箱等。

浅色 UML 背景的支持


对于一些喜欢用浅色主题的同学来说,以后看 UML 图再也不用深色了

好了,IDEA 2021.1 版本的主要新特性就这些,还有一些 Docker/JavaScript/K8s 的特性,大家有兴趣可以浏览官方说明:https://www.jetbrains.com/idea/whatsnew/

原创不易,转载请联系作者。如果我的文章对您有帮助,请点赞 / 收藏 / 关注鼓励支持一下吧❤❤❤❤❤❤
查看原文

赞 2 收藏 0 评论 1

杜尼卜 发布了文章 · 4月8日

在Vue.js中加载字体的最佳做法

博客原文:https://blog.zhangbing.site/2021/04/07/best-practices-for-loading-fonts-in-vue/

添加字体不应该对性能产生负面影响。在本文中,我们将探讨在 Vue 应用程序中加载字体的最佳实践。

正确声明font-face的字体

确保正确声明字体是加载字体的重要方面。这是通过使用 font-face 属性来声明你选择的字体来实现的。在你的Vue项目中,这个声明可以在你的根CSS文件中完成。在进入这个问题之前,我们先来看看Vue应用的结构。

/root
  public/
    fonts/
      Roboto/
        Roboto-Regular.woff2
        Roboto-Regular.woff
    index.html
  src/
    assets/
      main.css
    components/
    router/
    store/
    views/
    main.js

我们可以像这样在 main.css 中进行 font-face 声明:

// src/assets/main.css

@font-face {
  font-family: "Roboto";
  font-weight: 400;
  font-style: normal;
  font-display: auto;
  unicode-range: U+000-5FF;
  src: local("Roboto"), url("/fonts/Roboto/Roboto-Regular.woff2") format("woff2"), url("/fonts/Roboto/Roboto-Regular.woff") format("woff");
}

首先要注意的是 font-display:auto。使用 auto 作为值可以让浏览器使用最合适的策略来显示字体。这取决于一些因素,如网络速度、设备类型、闲置时间等。

要想更多地控制字体的加载方式,你应该使用 font-display: block,它指示浏览器短暂地隐藏文本,直到字体完全下载完毕。其他可能的值有 swapfallbackoptional。你可以在这里阅读更多关于它们的信息。

需要注意的是 unicode-range: U+000-5FF,它指示浏览器只加载所需的字形范围(U+000 - U+5FF)。你还想使用woff和woff2字体格式,它们是经过优化的格式,可以在大多数现代浏览器中使用。

另外需要注意的是 src 顺序。首先,我们检查字体的本地副本是否可用(local("Roboto”))并使用它。很多Android设备都预装了Roboto,在这种情况下,我们将使用预装的副本。如果没有本地副本,则在浏览器支持的情况下继续下载woff2格式。否则,它会跳至支持的声明中的下一个字体。

预加载字体

一旦你的自定义字体被声明,你可以使用 <link rel="preload"> 告诉浏览器提前预加载字体。在 public/index.html 中,添加以下内容:

<link rel="preload" as="font" href="./fonts/Roboto/Roboto-Regular.woff2" type="font/woff2" crossorigin="anonymous">

rel = “preload” 指示浏览器尽快开始获取资源,as = “font” 告诉浏览器这是一种字体,因此它优先处理请求。还要注意crossorigin=“anonymous",因为如果没有这个属性,预加载的字体会被浏览器丢弃。这是因为浏览器是以匿名方式获取字体的,所以使用这个属性就可以匿名请求。

使用 link=preload 可以增加自定义字体在需要之前被下载的机会。这个小调整大大加快了字体的加载时间,从而加快了您的Web应用程序中的文本渲染。

使用link = preconnect托管字体

当使用Google fonts等网站的托管字体时,你可以通过使用 link=preconnect 来获得更快的加载时间。它告诉浏览器提前建立与域名的连接。

如果您使用的是Google字体提供的Roboto字体,则可以在 public/index.html 中执行以下操作:

<link rel="preconnect" href="https://fonts.gstatic.com">
...
<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet">

这样就可以建立与原点https://fonts.gstatic.com 的初始连接,当浏览器需要从原点获取资源时,连接已经建立。从下图中可以看出两者的区别。

当加载字体时没有使用 link=preconnect 时,你可以看到连接所需的时间(DNS查找、初始连接、SSL等)。当像这样使用link=preconnect 时,结果看起来非常不同。

在这里,你会发现DNS查找、初始连接和SSL所花费的时间已经不存在了,因为前面已经进行了连接。

使用service workers缓存字体

字体是静态资源,变化不大,所以它们是缓存的好候选。理想情况下,您的Web服务器应该为字体设置一个较长的 max-age expires 头,这样浏览器缓存字体的时间就会更长。如果你正在构建一个渐进式网络应用(PWA),那么你可以使用service workers来缓存字体,并直接从缓存中为它们提供服务。

要开始使用Vue构建PWA,请使用vue-cli工具生成一个新项目:

vue create pwa-app

选择Manually select features选项,然后选择Progressive Web App (PWA) Support

这些就是我们生成PWA模板所需要的唯一东西。完成后,你就可以把目录改为 pwa-app,然后为app服务。

cd pwa-app
yarn serve

你会注意到在 src 目录下有一个文件 registerServiceWorker,其中包含了默认的配置。在项目的根目录下,如果vue.config.js 不存在,请创建它,如果存在,请添加以下内容:

// vue.config.js
module.exports = {
  pwa: {
    workboxOptions: {
      skipWaiting: true,
      clientsClaim: true,
    }
  }
}

vue-cli工具使用PWA plugin生成service worker。在底层,它使用Workbox来配置service worker和它控制的元素、要使用的缓存策略以及其他必要的配置。

在上面的代码片段中,我们要确保我们的应用程序始终由service worker的最新版本控制。这是必要的,因为它确保我们的用户总是查看应用程序的最新版本。您可以签出Workbox配置文档,以获得对生成的service worker行为的更多控制。

接下来,我们将自定义字体添加到 public 目录。我有以下结构:

root/
  public/
    index.html
    fonts/
      Roboto/
        Roboto-Regular.woff
        Roboto-Regular.woff2

一旦完成了Vue应用程序的开发,就可以通过从终端运行以下命令来构建它:

yarn build

这将结果输出到 dist 文件夹中。如果你检查文件夹的内容,你会注意到一个类似于 precache-manifest.1234567890.js 的文件。它包含了要缓存的资产列表,这只是一个包含修订版和URL的键值对的列表。

self.__precacheManifest = (self.__precacheManifest || []).concat([
  {
    "revision": "3628b4ee5b153071e725",
    "url": "/fonts/Roboto/Roboto-Regular.woff2"
  },
  ...
]);

public/ 文件夹中的所有内容都是默认缓存的,其中包括自定义字体。有了这个地方,你可以用像service这样的包来serve你的应用程序,或者把 dist 文件夹托管在web服务器上查看结果。你可以在下面找到一个应用程序的截图。

在随后的访问中,字体是从缓存中加载的,这可以加快应用程序的加载时间。

结论

在这篇文章中,我们研究了在Vue应用程序中加载字体时应用的一些最佳实践。使用这些实践将确保你提供的字体看起来不错,而不影响应用的性能。

查看原文

赞 4 收藏 4 评论 0

杜尼卜 发布了文章 · 4月7日

如何在JavaScript中实现队列数据结构

要成为一名优秀的开发人员,需要来自多个学科的知识。

然而,在了解编程语言的基础上,你还必须了解如何组织数据,以便根据任务轻松有效地操作数据。这就是数据结构的作用。

在这篇文章中,我将描述队列数据结构,其具有的操作以及向您展示JavaScript中的队列实现。

1.队列数据结构

如果你喜欢旅行(像我一样),很可能你在机场通过了办理登机手续。如果有很多旅客愿意办理登机手续,自然就会在值机柜台前排起长龙。

刚进入机场并想要办理登机手续的旅客将排队进入队列,而刚刚在服务台办理了登机手续的旅客则可以离开队列。

这是队列的真实示例—队列数据结构以相同的方式工作。

队列是一种“先入先出”(FIFO)数据结构的类型。入队(输入)的第一项是要出队(输出)的第一项。

从结构上说,一个队列有2个指针。队列中最早的排队项目位于队列的顶部,而最新队列的项目位于队列的末尾。

2.队列中的操作

队列主要支持两种操作:入队列(enqueue)和出队列(dequeue)。此外,您可能会发现使用peek和length操作非常有用。

2.1 入队操作

入队操作在队列尾部插入一个项目。

上图中的入队操作将项目 8 插入尾部,8 成为队列的尾部。

queue.enqueue(8);

2.2 出队操作

出队操作提取队列头部的项,队列中的下一项成为头。

在上面的图片中,出队操作从队列中返回并删除项目 7,在退出队列后,项目 2 成为新的头。

queue.dequeue(); // => 7

2.3 Peek操作

Peek操作读取队列的开头,而不会更改队列。

项目 7 是上图中队列的头部,Peek操作只是返回队列的头部——第 7 项,而不修改队列。

queue.peek(); // => 7

2.4 队列长度

长度操作计算队列包含多少个项目。

图片中的队列有4个项目:4627。因此,队列长度为 4

queue.length; // => 4

2.5 队列操作时间复杂度

关于所有的队列操作--enqueue、dequeue、peek和length——重要的是,所有这些操作必须在恒定的时间内 O(1) 执行。

恒定的时间 O(1) 意味着无论队列的大小(它可以有10个或100万个项目):enqueue、dequeue、peek和length操作必须在相对相同的时间内执行。

3.在JavaScript中实现队列

让我们看一下队列数据结构的可能实现,同时维持所有操作必须在恒定时间 O(1) 中执行的要求。

class Queue {
  constructor() {
    this.items = {};
    this.headIndex = 0;
    this.tailIndex = 0;
  }

  enqueue(item) {
    this.items[this.tailIndex] = item;
    this.tailIndex++;
  }

  dequeue() {
    const item = this.items[this.headIndex];
    delete this.items[this.headIndex];
    this.headIndex++;
    return item;
  }

  peek() {
    return this.items[this.headIndex];
  }

  get length() {
    return this.tailIndex - this.headIndex;
  }
}

const queue = new Queue();

queue.enqueue(7);
queue.enqueue(2);
queue.enqueue(6);
queue.enqueue(4);

queue.dequeue(); // => 7

queue.peek();    // => 2

queue.length;    // => 3

Try the demo.

const queue = new Queue() 是创建队列实例的方式。

调用 queue.enqueue(7) 方法会将项目7排队到队列中。

queue.dequeue() 从队列中去队列一个头部的项目,而 queue.peek() 只是Peek头部的项目。

最后,queue.length 显示队列中还有多少项目。

队列方法的复杂性

Queue类的 queue()dequeue()peek()length() 方法仅使用:

  • 属性访问器(例如 this.items[this.headIndex] ),
  • 或执行算术操作(例如 this.headIndex++

因此,这些方法的时间复杂度是恒定时间 O(1)

总结

队列数据结构是“先入先出”(FIFO)的一种:最早入队的项是最早出队的项。

队列有2个主要操作:入队和出队。另外,队列可以具有辅助操作,例如Peek和长度。

所有队列操作必须在恒定时间 O(1) 中执行。

查看原文

赞 0 收藏 0 评论 0

杜尼卜 发布了文章 · 3月31日

WebStorm访谈:我们是如何构建 JavaScript IDE 的?

构建一个IDE是一个广泛而复杂的工程。这似乎很明显,对吧?但你有没有想过,各种零碎的东西是如何组合成一个统一的环境的?引擎之下到底发生了什么?这些都是一些最有趣的问题。

我与WebStorm产品经理Ekaterina Prigara坐了下来,详细讨论了WebStorm本身,我们如何构建它以及我们的未来计划。

嗨,Ekaterina!让我们从谈论您在WebStorm团队中的角色开始。您的职责是什么?

我帮助团队定义产品策略,管理产品路线图。

我尝试分析和验证从各种渠道获得的想法和用例。然后,我将他们带回团队,以及对Web开发生态系统中发生的事情的一些见解。

在必要的时候,我帮助我的同事确定功能和错误修复的优先级,为用户体验提供建议,并与IntelliJ平台团队协调任务。

WebStorm团队有多大?

WebStorm团队共有18人。这个团队是跨职能的,包括开发人员、QA、产品和产品营销经理、技术写手和开发者倡导者。事实上,说到开发者代言人,我们现在就在找一个。

IntelliJ平台,IntelliJ IDEA,WebStorm和其他JetBrains IDE有什么共同点?

WebStorm建立在JetBrains开发的开源IntelliJ平台之上。WebStorm从IntelliJ平台获得其UI和许多功能,例如核心编辑器和Git集成。JetBrains上有130多人在使用该平台,并且WebStorm团队与他们紧密合作。

同时,WebStorm的功能在其他商业JetBrains IDE中也可以使用,如IntelliJ IDEA Ultimate、PhpStorm和PyCharm Professional。从这个意义上说,WebStorm本身不仅是一个独立的产品,而且是所有JetBrains IDE的一部分。

与大多数JetBrains产品一样,WebStorm每年有3个版本。您如何提出有关新版本中包含哪些内容的想法?以及您如何确定哪些功能无法发挥作用?

对于路线图中包含的内容,我们有很多想法来源:

  • 用户的反馈,来自于各种渠道,如问题跟踪器、社交媒体、技术支持和用户体验研究访谈等。
  • 其他IDE和编辑器,包括我们自己的不同语言的IDE。
  • 产品团队成员的亲身体验。
  • 我们在开发生态系统中观察到的变化,比如新的框架、工具、方法和最佳实践的出现。

我们的路线图与我们为WebStorm和基于IntelliJ平台的一系列产品设定的更高层次目标一致。然而,我们没有一个正式的方法来确定优先级。我们使用各种各样的技术(例如,RICE)来帮助我们对任务进行优先排序,我们经常依靠我们的直觉和经验。

我们的路线图非常灵活。如果我们觉得新特性还没有准备好,或者出现了更紧急的情况(例如,新框架发布时出现了中断的变化),我们会毫不犹豫地推迟新特性的发布。

您如何确定发布高质量的产品?

我们拥有一整套的质量保证工程师,他们与开发人员紧密合作。一般而言,我们有一个月左右的时间积极开发和测试新版本。QA工程师在开发新功能和变更时对其进行测试,并寻找可能的回归。他们还与团队中的其他人一起评估新功能的可用性,并寻找遗漏的用例。

我们还从参与“早期访问计划(Early Access Program)”的用户那里获得新功能的早期反馈,该计划在每个主要版本中都会实施。

继续讨论更困难的问题,JavaScript及其框架正在迅速发展,很难跟上吗?

是的,在过去的几年中,JavaScript生态系统中发生了很多事情。即使在WebStorm团队工作了7年之后,我仍然对新技术的出现和模式的转变感到非常兴奋。JavaScript已经走了很长一段路,并且已经成为一种真正强大的语言,更不用说TypeScript了。

对于我们团队来说,最大的挑战是对框架非常务实。我们和我们的用户都有理由对IDE中的框架支持水平抱有很高的期望,为了达到这个水平,我们需要投入数月甚至数年的开发资金。知道这一点,我们经常尝试不急于为新框架提供支持。我们需要根据我们认为会成为下一个大事件的东西来做出选择。有时它们不会成功,但我们试图从错误中学习。

在你看来,如果有的话,哪些新兴的JavaScript相关框架和技术是最有前途或最有趣的?

哈哈,我对各种技术有很多看法,但在决定WebStorm是否支持某项技术时,我尽量做到客观,把数字带到桌面上。

最近让我兴奋的一件事是Tailwind。一开始我有点怀疑,但是我能理解为什么它如此流行。

您认为WebStorm在未来几年会发生怎样的变化?您有什么新的探索方向吗?

我们目前的主要关注点之一是确保有经验和经验较少的开发人员在开始使用WebStorm时能获得良好的第一印象。我们希望IDE能够对用户友好,易于上手。同时,我们也希望它能帮助用户更好地理解他们的项目,成为更好的开发者,无论他们使用什么技术或拥有什么水平的经验。

我们也希望IDE能够解释如何运行和调试应用,提供关于代码的可操作性的见解等等。在UI中平衡简单性,可发现性和指导性是我们面临的最大挑战之一。

您有什么可以与我们分享有关您今年计划的信息吗?您最兴奋的功能或改进是什么?

很多不同的东西。与Code With Me合作开发,改进在Docker和远程机器上运行和调试代码的体验,改进共享设置等等。

查看原文

赞 2 收藏 1 评论 0

杜尼卜 发布了文章 · 3月25日

在React应用中使用Dexie.js进行离线数据存储

博客原文:https://blog.zhangbing.site/2021/03/16/dexie-js-indexeddb-react-apps-offline-data-storage/

离线存储应用程序数据已成为现代Web开发中的必要条件。内置的浏览器 localStorage 可以用作简单轻量数据的数据存储,但是在结构化数据或存储大量数据方面却不足。

最重要的是,我们只能将字符串数据存储在受XSS攻击的 localStorage 中,并且它没有提供很多查询数据的功能。

这就是IndexedDB的亮点。使用IndexedDB,我们可以在浏览器中创建结构化的数据库,将几乎所有内容存储在这些数据库中,并对数据执行各种类型的查询。

在本文中,我们将了解IndexedDB的全部含义,以及如何使用Dexie.js(用于IndexedDB的简约包装)处理Web应用程序中的离线数据存储。

IndexedDB如何工作

IndexedDB是用于浏览器的内置非关系数据库。它使开发人员能够将数据持久存储在浏览器中,即使在脱机时也可以无缝使用Web应用程序。使用IndexedDB时,您会经常看到两个术语:数据库存储和对象存储。让我们在下面进行探讨。

使用IndexedDB创建数据库

IndexedDB数据库对每个Web应用程序来说都是唯一的。这意味着一个应用程序只能从与自己运行在同一域或子域的 IndexedDB 数据库中访问数据。数据库是容纳对象存储的地方,而对象存储又包含存储的数据。要使用IndexedDB数据库,我们需要打开(或连接到)它们:

const initializeDb = indexedDB.open('name_of_database', version)

indexedDb.open() 方法中的 name_of_database 参数将用作正在创建的数据库的名称,而 version 参数是一个代表数据库版本的数字。

在IndexedDB中,我们使用对象存储来构建数据库的结构,并且每当要更新数据库结构时,都需要将版本升级到更高的值。这意味着,如果我们从版本1开始,则下次要更新数据库的结构时,我们需要将 indexedDb.open() 方法中的版本更改为2或更高版本。

使用IndexedDB创建对象存储

对象存储类似于关系数据库(如PostgreSQL)中的表和文档数据库(如MongoDB)中的集合。要在IndexedDB中创建对象存储,我们需要从之前声明的 initializeDb 变量中调用 onupgradeneeded() 方法:

initializeDb.onupgradeneeded = () => {
  const database = initializeDb.result
  database.createObjectStore('name_of_object_store', {autoIncrement: true})
}

在上面的代码块中,我们从 initializeDb.result 属性获取数据库,然后使用其 createObjectStore() 方法创建对象存储。第二个参数 {autoIncrement:true} 告诉IndexedDB自动提供/增加对象存储中项目的ID。

我省略了诸如事务和游标之类的其他术语,因为使用低级IndexedDB API需要进行大量工作。这就是为什么我们需要Dexie.js,它是IndexedDB的简约包装。让我们看看Dexie如何简化创建数据库,对象存储,存储数据以及从数据库查询数据的整个过程。

使用Dexie离线存储数据

使用Dexie,创建IndexedDB数据库和对象存储非常容易:

const db = new Dexie('exampleDatabase')
db.version(1).stores({
  name_of_object_store: '++id, name, price',
  name_of_another_object_store: '++id, title'
})

在上面的代码块中,我们创建了一个名为 exampleDatabase 的新数据库,并将其作为值分配给 db 变量。我们使用 db.version(version_number).stores() 方法为数据库创建对象存储。每个对象存储的值代表了它的结构。例如,当在第一个对象存储中存储数据时,我们需要提供一个具有属性 nameprice 的对象。++id 选项的作用就像我们在创建对象存储区时使用的 {autoIncrement:true} 参数一样。

请注意,在我们的应用程序中使用dexie包之前,我们需要安装并导入它。当我们开始构建我们的演示项目时,我们将看到如何做到这一点。

我们将要建立的

对于我们的演示项目,我们将使用Dexie.js和React构建一个市场列表应用程序。我们的用户将能够在市场列表中添加他们打算购买的商品,删除这些商品或将其标记为已购买。

我们将看到如何使用Dexie useLiveQuery hook来监视IndexedDB数据库中的更改以及在数据库更新时重新呈现React组件。这是我们的应用程序的外观:

设置我们的应用

首先,我们将使用为应用程序的结构和设计创建的GitHub模板。这里有一个模板的链接。点击Use this template(使用此模板)按钮,就会用现有的模板为你创建一个新的资源库,然后你就可以克隆和使用这个模板。

或者,在计算机上安装了GitHub CLI的情况下,您可以运行以下命令从市场列表GitHub模板创建名为 market-list-app 的存储库:

gh repo create market-list-app --template ebenezerdon/market-list-template

完成此操作后,您可以继续在代码编辑器中克隆并打开您的新应用程序。使用终端在应用程序目录中运行以下命令应安装npm依赖项并启动新应用程序:

npm install && npm start

导航到成功消息中的本地URL(通常为http://localhost:3000)时,您应该能够看到新的React应用程序。您的新应用应如下所示:

当您打开 ./src/App.js 文件时,您会注意到我们的应用程序组件仅包含市场列表应用程序的JSX代码。我们正在使用Materialize框架中的类进行样式设置,并将其CDN链接包含在 ./public/index.html 文件中。接下来,我们将看到如何使用Dexie创建和管理数据。

用Dexie创建我们的数据库

要在我们的React应用程序中使用Dexie.js进行离线存储,我们将从在终端中运行以下命令开始,以安装 dexiedexie-react-hooks 软件包:

npm i -s dexie dexie-react-hooks

我们将使用 dexie-react-hooks 包中的 useLiveQuery hook来监视更改,并在对IndexedDB数据库进行更新时重新渲染我们的React组件。

让我们将以下导入语句添加到我们的 ./src/App.js 文件中。这将导入 DexieuseLiveQuery hook:

import Dexie from 'dexie'
import { useLiveQuery } from "dexie-react-hooks";

接下来,我们将创建一个名为 MarketList 的新数据库,然后声明我们的对象存储 items

const db = new Dexie('MarketList');
db.version(1).stores(
  { items: "++id,name,price,itemHasBeenPurchased" }
)

我们的 items 对象存储将期待一个具有属性namepriceitemHasBeenPurchased 的对象,而 id 将由 Dexie 提供。在将新数据添加到对象存储中时,我们将为 itemHasBeenPurchased 属性使用默认布尔值 false,然后在我们从市场清单中购买商品时将其更新为 true

Dexie React hook

让我们创建一个变量来存储我们所有的项目。我们将使用 useLiveQuery 钩子从 items 对象存储中获取数据,并观察其中的变化,这样当 items 对象存储有更新时,我们的 allItems 变量将被更新,我们的组件将用新的数据重新渲染。我们将在 App 组件内部进行:

const App = () => {
  const allItems = useLiveQuery(() => db.items.toArray(), []);
  if (!allItems) return null
  ...
}

在上面的代码块中,我们创建了一个名为 allItems 的变量,并将 useLiveQuery 钩子作为其值。useLiveQuery 钩子的语法类似于React的useEffect钩子,它期望一个函数及其依赖项数组作为参数。我们的函数参数返回数据库查询。

在这里,我们以数组格式获取 items 对象存储中的所有数据。在下一行中,我们使用一个条件来告诉我们的组件,如果 allItems 变量是undefined,则意味着查询仍在加载中。

将items添加到我们的数据库

仍在App组件中,让我们创建一个名为 addItemToDb 的函数,我们将使用该函数向数据库中添加项目。每当我们点击“ADD ITEM(添加项目)”按钮时,我们都会调用此函数。请记住,每次更新数据库时,我们的组件都会重新渲染。

...
const addItemToDb = async event => {
    event.preventDefault()
    const name = document.querySelector('.item-name').value
    const price = document.querySelector('.item-price').value
    await db.items.add({
      name,
      price: Number(price),
      itemHasBeenPurchased: false
    })
  }
...

addItemToDb 函数中,我们从表单输入字段中获取商品名称和价格值,然后使用 db.[name_of_object_store].add 方法将新商品数据添加到商品对象存储中。我们还将 itemHasBeenPurchased 属性的默认值设置为 false

从我们的数据库中删除items

现在我们有了 addItemToDb 函数,让我们创建一个名为 removeItemFromDb 的函数以从我们的商品对象存储中删除数据:

...
const removeItemFromDb = async id => {
  await db.items.delete(id)
}
...

更新我们数据库中的项目

接下来,我们将创建一个名为 markAsPurchased 的函数,用于将商品标记为已购买。我们的函数在调用时,会将物品的主键作为第一个参数——在本例中是 id,它将使用这个主键来查询我们想要标记为购买的物品的数据库。取得商品后,它将其 markAsPurchased 属性更新为 true

...
const markAsPurchased = async (id, event) => {
  if (event.target.checked) {
    await db.items.update(id, {itemHasBeenPurchased: true})
  }
  else {
    await db.items.update(id, {itemHasBeenPurchased: false})
  }
}
...

markAsPurchased 函数中,我们使用 event 参数来获取用户单击的特定输入元素。如果选中其值,我们将itemHasBeenPurchased 属性更新为 true,否则更新为 falsedb.[name_of_object_store] .update() 方法期望该项目的主键作为其第一个参数,而新对象数据作为其第二个参数。

下面是我们的 App 组件在这个阶段应该是什么样子。

...
const App = () => {
  const allItems = useLiveQuery(() => db.items.toArray(), []);
  if (!allItems) return null

  const addItemToDb = async event => {
    event.preventDefault()
    const name = document.querySelector('.item-name').value
    const price = document.querySelector('.item-price').value
    await db.items.add({ name, price, itemHasBeenPurchased: false })
  }

  const removeItemFromDb = async id => {
    await db.items.delete(id)
  }

  const markAsPurchased = async (id, event) => {
    if (event.target.checked) {
      await db.items.update(id, {itemHasBeenPurchased: true})
    }
    else {
      await db.items.update(id, {itemHasBeenPurchased: false})
    }
  }
  ...
}

现在,我们创建一个名为 itemData 的变量,以容纳我们所有商品数据的JSX代码:

...
const itemData = allItems.map(({ id, name, price, itemHasBeenPurchased }) => (
  <div className="row" key={id}>
    <p className="col s5">
      <label>
        <input
          type="checkbox"
          checked={itemHasBeenPurchased}
          onChange={event => markAsPurchased(id, event)}
        />
        <span className="black-text">{name}</span>
      </label>
    </p>
    <p className="col s5">${price}</p>
    <i onClick={() => removeItemFromDb(id)} className="col s2 material-icons delete-button">
      delete
    </i>
  </div>
))
...

itemData 变量中,我们映射了 allItems 数据数组中的所有项目,然后从每个 item 对象获取属性 idnameprice itemHasBeenPurchased。然后,我们继续进行操作,并用数据库中的新动态值替换了以前的静态数据。

注意,我们还使用了 markAsPurchasedremoveItemFromDb 方法作为相应按钮的单击事件侦听器。我们将在下一个代码块中将 addItemToDb 方法添加到表单的 onSubmit 事件中。

准备好 itemData 后,让我们将 App 组件的return语句更新为以下JSX代码:

...
return (
  <div className="container">
    <h3 className="green-text center-align">Market List App</h3>
    <form className="add-item-form" onSubmit={event => addItemToDb(event)} >
      <input type="text" className="item-name" placeholder="Name of item" required/>
      <input type="number" step=".01" className="item-price" placeholder="Price in USD" required/>
      <button type="submit" className="waves-effect waves-light btn right">Add item</button>
    </form>
    {allItems.length > 0 &&
      <div className="card white darken-1">
        <div className="card-content">
          <form action="#">
            { itemData }
          </form>
        </div>
      </div>
    }
  </div>
)
...

在return语句中,我们已将 itemData 变量添加到我们的项目列表中(items list)。我们还使用 addItemToDb 方法作为 add-item-formonsubmit 值。

为了测试我们的应用程序,我们可以返回到我们先前打开的React网页。请记住,您的React应用必须正在运行,如果不是,请在终端上运行命令 npm start。您的应用应该能够像下面的演示一样运行:

我们还可以使用条件用Dexie查询我们的IndexedDB数据库。例如,如果我们要获取价格高于10美元的所有商品,则可以执行以下操作:

const items = await db.friends
  .where('price').above(10)
  .toArray();

您可以在Dexie文档)中查看其他查询方法。

结束语和源码

在本文中,我们学习了如何使用IndexedDB进行离线存储以及Dexie.js如何简化该过程。我们还了解了如何使用Dexie useLiveQuery 钩子来监视更改并在每次更新数据库时重新渲染React组件。

由于IndexedDB是浏览器原生的,从数据库中查询和检索数据比每次需要在应用中处理数据时都要发送服务器端API请求要快得多,而且我们几乎可以在IndexedDB数据库中存储任何东西。

过去使用IndexedDB可能对浏览器的支持是一个大问题,但是现在所有主流浏览器都支持它。在Web应用中使用IndexedDB进行离线存储的诸多优势大于劣势,将Dexie.js与IndexedDB一起使用,使得Web开发变得前所未有的有趣。

这是我们的演示应用程序的GitHub库的链接。

查看原文

赞 2 收藏 0 评论 0

杜尼卜 发布了文章 · 3月21日

使用Vue.js和MJML创建响应式电子邮件

MJML是一种现代的电子邮件工具,使开发人员可以在所有设备和邮件客户端上创建美观、响应迅速的出色电子邮件。这种标记语言是为了减少编写响应式电子邮件的痛苦而设计的。

它的语义语法使其易于使用。它还具有功能丰富的标准组件,可缩短开发时间。在本教程中,我们将使用MJML构建漂亮的响应式邮件,并在多个邮件客户端上进行测试。

开始MJML

你可以使用npm安装MJML,以将其与Node.js或CLI结合使用:

$ npm install -g mjml

构建我们的电子邮件

首先,请创建一个名为 email.mjml 的文件,尽管你也可以选择其他任何名称。创建文件后,我们的响应式电子邮件将分为以下几部分:

  • 公司header
  • 图片header
  • Email介绍
  • 栏目部分
  • 图标
  • 社交图标

栏目

这些部分是我们响应式电子邮件的框架。如上所示,我们的电子邮件将分为六个部分,在我们的 email.mjml 文件中:

<mjml>
  <mj-body>
    <!-- 公司 Header -->
    <mj-section background-color="#f0f0f0"></mj-section>
    <!-- 图片 Header -->
    <mj-section background-color="#f0f0f0"></mj-section>
    <!-- Email 介绍 -->
    <mj-section background-color="#fafafa"></mj-section>
    <!-- 栏目部分 -->
    <mj-section background-color="white"></mj-section>
    <!-- 图标 -->
    <mj-section background-color="#fbfbfb"></mj-section>
    <!-- 社交图标 -->
    <mj-section background-color="#f0f0f0"></mj-section>
  </mj-body>
</mjml>

从上面可以看到,我们正在使用两个MJML组件:mj-bodymj-sectionmj-body 定义了我们电子邮件的起点,而 mj-section 定义了一个包含其他组件的节。

对于定义的每个部分,还定义了具有各自十六进制值的 background-color 属性。

公司 Header

我们电子邮件的此部分仅在中心横幅位置包含我们的公司/品牌名称:

<!-- 公司 Header -->
<mj-section background-color="#f0f0f0">
  <mj-column>
    <mj-text  font-style="bold"
        font-size="20px"
        align="center"
        color="#626262">
    Central Park Cruise
    </mj-text>
  </mj-column>
</mj-section>

mj-column 组件是用来定义一个列。mj-text 组件用于我们的文本内容,并采取字体样式、字体大小、颜色等样式属性。

图片 Header

在本部分中,我们将有一个背景图片和一段文字,它们应代表我们的公司口号。我们还会有一个号召性用语按钮,指向一个包含更多详细信息的页面。

要添加图片标题,你必须将该部分的背景颜色替换为 background-url。与第一个标题相似,你将不得不在垂直和水平方向上居中放置文本,padding保持不变。

按钮的 href 设置按钮的位置。为了让背景在列中呈现全宽,将列宽设置为600px,width=“600px"

我们的电子邮件的这一部分将只包含我们的公司/品牌名称的中心横幅位置。

<!-- Image Header -->
<mj-section background-url="https://ca-times.brightspotcdn.com/dims4/default/2af165c/2147483647/strip/true/crop/2048x1363+0+0/resize/1440x958!/quality/90/?url=https%3A%2F%2Fwww.trbimg.com%2Fimg-4f561d37%2Fturbine%2Forl-disneyfantasy720120306062055"
            background-size="cover"
            background-repeat="no-repeat">
  <mj-column width="600px">
    <mj-text  align="center"
             color="#fff"
             font-size="40px"
             font-family="Helvetica Neue">Christmas Discount</mj-text>
    <mj-button background-color="#F63A4D" href="#">
      See Promotions
    </mj-button>
  </mj-column>
</mj-section>

要使用图像header,我们将向 jms -section 组件添加 background-url 属性,然后使用 background-sizebackground-repeat 属性设置图像的样式。

对于我们的口号文本块,我们使用 align 属性将文本在水平和垂直方向上居中对齐。你还可以根据需要设置文本颜色,字体大小,字体系列等。

号召性用语按钮是使用 mj-button 组件实现的。background-color 属性允许我们指定按钮的背景色,然后使用 href 指定链接或页面的位置。

Email件介绍

简介文字将由标题,主体文字和号召性用语组成。

<!-- Intro text -->
<mj-section background-color="#fafafa">
  <mj-column width="400px">
    <mj-text font-style="bold"
             font-size="20px"
             font-family="Helvetica Neue"
             color="#626262">Ultimate Christmas Experience</mj-text>
    <mj-text color="#525252">
      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin rutrum enim eget magna efficitur, eu semper augue semper. Aliquam erat volutpat. Cras id dui lectus. Vestibulum sed finibus lectus, sit amet suscipit nibh. Proin nec commodo purus. Sed eget nulla elit. Nulla aliquet mollis faucibus.
    </mj-text>
    <mj-button background-color="#F45E43" href="#">Learn more</mj-button>
  </mj-column>
</mj-section>

栏目部分

在这封邮件的部分,我们会有两栏:一栏是描述性的图片,二栏是我们的文字块,用来补充第一部分的图片。

<!-- Side image -->
<mj-section background-color="white">
  <!-- Left image -->
  <mj-column>
    <mj-image width="200px"
              data-original="https://navis-consulting.com/wp-content/uploads/2019/09/Cruise1-1.png"/>
  </mj-column>
  <!-- right paragraph -->
  <mj-column>
    <mj-text font-style="bold"
             font-size="20px"
             font-family="Helvetica Neue"
             color="#626262">
      Amazing Experiences
    </mj-text>
    <mj-text color="#525252">
      Lorem ipsum dolor sit amet, consectetur adipiscing elit. 
      Proin rutrum enim eget magna efficitur, eu semper augue semper. 
      Aliquam erat volutpat. Cras id dui lectus. Vestibulum sed finibus 
      lectus.
    </mj-text>
  </mj-column>
</mj-section>

左侧的第一列使用 mj-image 组件指定要使用的图像。该图像可以是本地文件,也可以是远程托管的图像(在我们的情况下是这样)。

右侧的第二列包含两个文本块,一个用于我们的标题,另一个用于主体文本。

图标

图标部分将分为三列。你还可以添加更多内容,具体取决于你希望电子邮件的外观。

<!-- Icons -->
<mj-section background-color="#fbfbfb">
  <mj-column>
    <mj-image width="100px" data-original="https://191n.mj.am/img/191n/3s/x0l.png" />
  </mj-column>
  <mj-column>
    <mj-image width="100px" data-original="https://191n.mj.am/img/191n/3s/x01.png" />
  </mj-column>
  <mj-column>
    <mj-image width="100px" data-original="https://191n.mj.am/img/191n/3s/x0s.png" />
  </mj-column>
</mj-section>

每列都有其自己的 mj-image 组件,用于渲染图标图像。

社交图标

本部分将包含指向我们的社交媒体帐户的图标。

<mj-section background-color="#e7e7e7">
  <mj-column>
    <mj-social>
      <mj-social-element name="instagram" />
    </mj-social>
  </mj-column>
</mj-section>

MJML带有 mj-social 组件,可轻松用于显示社交媒体图标。在我们的电子邮件中,我们使用了 Twitter mj-social-element

全部放在一起

至此,我们已经实现了所有部分,完整的 email.mjml 应该如下所示:

<mjml>
  <mj-body>
    <!-- Company Header -->
    <mj-section background-color="#f0f0f0">
      <mj-column>
        <mj-text  font-style="bold"
                 font-size="20px"
                 align="center"
                 color="#626262">
          Central Park Cruises
        </mj-text>
      </mj-column>
    </mj-section>
    <!-- Image Header -->
    <mj-section background-url="https://ca-times.brightspotcdn.com/dims4/default/2af165c/2147483647/strip/true/crop/2048x1363+0+0/resize/1440x958!/quality/90/?url=https%3A%2F%2Fwww.trbimg.com%2Fimg-4f561d37%2Fturbine%2Forl-disneyfantasy720120306062055"
                background-size="cover"
                background-repeat="no-repeat">
      <mj-column width="600px">
        <mj-text  align="center"
                 color="#fff"
                 font-size="40px"
                 font-family="Helvetica Neue">Christmas Discount</mj-text>
        <mj-button background-color="#F63A4D" href="#">
          See Promotions
        </mj-button>
      </mj-column>
    </mj-section>
    <!-- Email Introduction -->
    <mj-section background-color="#fafafa">
      <mj-column width="400px">
        <mj-text font-style="bold"
                 font-size="20px"
                 font-family="Helvetica Neue"
                 color="#626262">Ultimate Christmas Experience</mj-text>
        <mj-text color="#525252">
          Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin rutrum enim eget magna efficitur, eu semper augue semper. Aliquam erat volutpat. Cras id dui lectus. Vestibulum sed finibus lectus, sit amet suscipit nibh. Proin nec commodo purus. Sed eget nulla elit. Nulla aliquet mollis faucibus.
        </mj-text>
        <mj-button background-color="#F45E43" href="#">Learn more</mj-button>
      </mj-column>
    </mj-section>
    <!-- Columns section -->
    <mj-section background-color="white">
      <!-- Left image -->
      <mj-column>
        <mj-image width="200px"
                  data-original="https://navis-consulting.com/wp-content/uploads/2019/09/Cruise1-1.png"/>
      </mj-column>
      <!-- right paragraph -->
      <mj-column>
        <mj-text font-style="bold"
                 font-size="20px"
                 font-family="Helvetica Neue"
                 color="#626262">
          Amazing Experiences
        </mj-text>
        <mj-text color="#525252">
          Lorem ipsum dolor sit amet, consectetur adipiscing elit. 
          Proin rutrum enim eget magna efficitur, eu semper augue semper. 
          Aliquam erat volutpat. Cras id dui lectus. Vestibulum sed finibus 
          lectus.
        </mj-text>
      </mj-column>
    </mj-section>
    <!-- Icons -->
    <mj-section background-color="#fbfbfb">
      <mj-column>
        <mj-image width="100px" data-original="https://191n.mj.am/img/191n/3s/x0l.png" />
      </mj-column>
      <mj-column>
        <mj-image width="100px" data-original="https://191n.mj.am/img/191n/3s/x01.png" />
      </mj-column>
      <mj-column>
        <mj-image width="100px" data-original="https://191n.mj.am/img/191n/3s/x0s.png" />
      </mj-column>
    </mj-section>
    <!-- Social icons -->
    <mj-section background-color="#e7e7e7">
      <mj-column>
        <mj-social>
          <mj-social-element name="instagram" />
        </mj-social>
      </mj-column>
    </mj-section>
  </mj-body>
</mjml>

运行我们的应用程序

现在我们已经完成了电子邮件的构建,我们可以继续对其进行编译以查看其外观。为此,我们在终端中键入以下内容:

mjml -r email.mjml -o .
  • -r:允许MJML读取和编译我们的 mjml 文件
  • -o .:告诉MJML将编译后的 mjml 输出保存到同一目录中

MJML完成编译后,你现在应该在同一目录中看到一个 email.html 文件。 使用你喜欢的电子邮件客户端或浏览器打开它,它的外观应类似于下图:

总结

正如我们刚才看到的,MJML帮助我们生成跨多个浏览器和客户机响应的高质量、漂亮的HTML电子邮件。


image

查看原文

赞 4 收藏 1 评论 1

杜尼卜 发布了文章 · 3月17日

改善应用程序性能和代码质量:通过代理模式组合HTTP请求

原文发表于我的博客:blog.zhangbing.site

在前端项目中,我们的网页通常需要向服务器发送多个HTTP请求。

假设我们的产品具有一项功能,即每当用户单击 li 标记时,客户端都会向服务器发送一个HTTP请求。

这是一个简单的Demo:

<html>

<body>
    <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
        <li>5</li>
        <li>6</li>
        <li>7</li>
        <li>8</li>
        <li>9</li>
    </ul>

    <script>
        // Suppose this function is used to make HTTP requests to the server
        var sendHTTPRequest = function(message) {
            console.log('Start sending HTTP message to the server: ', message)
            console.log('1000ms passed')
            console.log('HTTP Request is completed')
        }

        var ul = document.getElementsByTagName('ul')[0];

        ul.onclick = function(event) {
            if (event.target.nodeName === "LI") {

                // Executes this function every time the <li> tag is clicked.
                sendHTTPRequest(event.target.innerText)
            }
        }
    </script>
</body>

</html>

在上面的代码中,我们直接使用简单的 sendHTTPRequest 函数来模拟发送HTTP请求。这样做是为了更好地专注于核心目标,因此我简化了一些代码。

然后,我们将click事件绑定到 ul 元素。每次用户单击诸如 <li> 5 </li> 之类的标记时,客户端将执行 sendHTTPRequest 函数以向服务器发出HTTP请求。

上面的程序是这样的:

为了使你们更容易尝试,我制作了一个Codepen演示:https://codepen.io/bitfishxyz...

当然,在真实的项目中,我们可能会向服务器发送一个文件,推送通知,或者发送一些日志。但为了演示的惯例,我们将跳过这些细节。

好了,这是一个很简单的演示,那么上面的代码有没有什么缺点呢?


如果您的项目非常简单,那么编写这样的代码应该没有问题。但是,如果您的项目很复杂,并且客户端需要频繁向服务器发送HTTP请求,则此代码效率很低。

在上面的示例中,如果任何用户反复快速单击 li 元素会发生什么?这时,我们的客户端需要向服务器发出频繁的HTTP请求,并且每个请求都会消耗大量时间和服务器资源。

客户端每次与服务器建立新的HTTP连接时,都会消耗一些时间和服务器资源。因此,在HTTP传输机制中,一次传输所有文件比多次传输少量文件更为有效。

例如,您可能需要发送五个HTTP请求,每个HTTP请求的HTTP数据包大小为1MB。现在,您一次发送一个HTTP请求,数据包大小为5MB。通常预期后者的性能要比前一个更好。

网页上的大量HTTP请求可能会减慢网页的加载时间,最终损害用户体验。如果加载速度不够快,这可能会导致访问者更快地离开该页面。

因此,在这种情况下,我们可以考虑合并HTTP请求。

在我们目前的项目中,我的思路是这样的:我们可以在本地设置一个缓存,然后在一定范围内收集所有需要发送给服务器的消息,然后一起发送。

你可以暂停一下,自己试着想办法。

提示:您需要创建一个本地缓存对象来收集需要发送的消息。然后,您需要使用定时器定时发送收集到的消息。

这是一个实现。

var messages = [];
var timer;
var sendHTTPRequest = function (message) {
  messages.push(message);
  if (timer) {
    return;
  }
  timer = setTimeout(function () {
    console.log("Start sending messages: ", messages.join(","));
    console.log("1000ms passed");
    console.log("HTTP Request is completed.");

    clearTimeout(timer);
    timer = null;
    messages = [];
  }, 2000);
};

每当客户端需要发送消息,也就是触发一个 onclick 事件的时候,sendHTTPRequest 并不会立即向服务器发送消息,而是先将消息缓存在消息中。然后,我们有一个计时器,该计时器在2秒钟后执行,并且在2秒钟后,该计时器会将所有先前缓存的消息发送到服务器。此更改达到了组合HTTP请求的目的。

测试结果如下:

如你所见,尽管我们多次触发点击事件,但在两秒钟内,我们只发送了一个HTTP请求。

当然,为了方便演示,我将等待时间设置为2秒。如果你觉得这个等待时间太长,你可以缩短这个等待时间。

对于不需要太多实时交互的项目,2秒的延迟并不是一个巨大的副作用,但它可以减轻服务器的很多压力。在适当的情况下,这是非常值得的。


上面的代码确实为项目提供了一些性能改进。但是就代码设计而言,上面的代码并不好。

第一,违反了单一责任原则。sendHTTPRequest 函数不仅向服务器发送HTTP请求,而且还组合HTTP请求。该函数执行过多操作,使代码看起来非常复杂。

如果某个功能(或对象)承担了过多的责任,那么当我们的需求发生变化时,该功能通常将不得不发生重大变化。这样的设计不能有效地应对可能的更改,这是一个糟糕的设计。

我们理想的代码如下所示:

我们没有对 sendHTTPRequest 进行任何更改,而是选择为其提供代理。这个代理函数执行合并HTTP请求的任务,并将合并后的消息传递给 sendHTTPRequest 发送。然后我们以后就可以直接使用 proxySendHTTPRequest 方法了。

您可以暂停片刻,然后尝试自己解决。

这是一个实现:

var proxySendHTTPRequest = (function() {
  var messages = [],
      timer;
  return function(message) {
    messages.push(message);
    if (timer) {
      return;
    }
    timer = setTimeout(function() {
      sendHTTPRequest(messages.join(","));
      clearTimeout(timer);
      timer = null;
      messages = [];
    }, 2000);
  };
})();

其基本思想与前面的代码类似,该代码使用 messages 变量在一定时间内缓存所有消息,然后通过计时器统一地发送它们。此外,这段代码使用了闭包技巧,将 messagestimer 变量放在局部作用域中,以避免污染全局名称空间。

这段代码与前面的代码最大的区别是它没有更改 sendHTTPRequest 函数,而是将其隐藏在 proxySendHTTPRequest 后面。我们不再需要直接访问 sendHTTPRequest,而是使用代理 proxySendHTTPRequest 来访问它。proxySendHTTPRequestsendHTTPRequest 具有相同的参数列表和相同的返回值。

这样的设计有什么好处?

  • 发送HTTP请求和合并HTTP请求的任务交给了两个不同的函数,每个函数专注于一个职责。它遵从单一责任原则,并使代码更容易理解。
  • 由于两个函数的参数是相同的,我们可以简单地用 proxySendHTTPRequest 替换 sendHTTPRequest 的位置,而不需要做任何重大更改。

想象一下,如果将来网络性能有所提高,或者由于某些其他原因,我们不再需要合并HTTP请求。在这一点上,如果我们使用以前的设计,我们将不得不再次大规模地更改代码。在当前的代码设计中,我们可以简单地替换函数名。

事实上,这个编码技巧通常被称为设计模式中的代理模式

所谓的代理模式,其实在现实生活中很好理解。

  • 比方说,你想访问一个网站,但你不想泄露你的IP地址。那么你可以使用VPN,先访问你的代理服务器,然后通过代理服务器访问目标网站。这样目标网站就无法知道你的IP地址了。
  • 有时候,你会把你的真实服务器隐藏在Nginx服务器后面,让Nginx服务器为你的真实服务器处理一些琐碎的操作。

这些都是现实生活中代理模式的例子。

我们不需要为代理模式(或任何其他设计模式)的正式定义而烦恼,我们只需要知道,当客户端没有直接访问它的便利(或能力)时,我们可以提供代理功能(或对象)来控制对目标功能(或对象)的访问即可。客户机实际上访问代理函数(或对象),代理函数对请求进行一些处理,然后将请求传递给目标。

查看原文

赞 1 收藏 1 评论 0

杜尼卜 发布了文章 · 3月17日

Rust与Python:为什么Rust可以取代Python

在本指南中,我们将比较Rust和Python编程语言。我们将讨论每种情况下的适用用例,回顾使用Rust与Python的优缺点,并说明为什么Rust可能会取代Python。

我将介绍以下内容:

  • 什么是Rust?
  • 什么是Python?
  • 何时使用Rust
  • 何时使用Python
  • 为什么Rust可以取代Python

什么是Rust?

Rust是一种多范式语言,使开发人员能够构建可靠且高效的软件。Rust注重安全和性能,类似于C和C++,速度快,内存效率高,没有垃圾收集。它可以与其他语言集成,也可以在嵌入式系统上运行。

Rust拥有优秀的文档、友好的编译器和有用的错误信息,以及先进的工具,包括集成的包管理器、构建工具、智能多编辑器支持、自动完成和类型检查、自动格式化等。

Rust是由Mozilla Research的Graydon Hoare在2010年推出的。虽然与Python相比,Rust是一门年轻的语言,但它的社区却在稳步发展。事实上,在Stack Overflow的 "2020开发者调查 "中,86%的受访者将Rust评为2020年他们最喜欢的编程语言。

乍一看,Rust被静态化和强类型化可能看起来很极端。正如你所看到的,从长远来看,这有助于防止意外的代码行为。

什么是Python?

Python是一种编程语言,旨在帮助开发人员更高效地工作,更有效地集成系统。和 Rust 一样,Python 也是多范式的,并且被设计成可扩展的。如果速度是最重要的,你可以使用低级别的 API 调用,比如 CPython

Python的历史可以追溯到1991年Guido van Rossum推出的Python,它以代码的可读性、消除分号和大括号而闻名。

除了它的可扩展性,Python 是一种解释型语言,这使得它比大多数编译型语言慢。正如你所预料的那样,Python的成熟度很高,它有一个庞大的库的生态系统和一个庞大的专业社区。

何时使用Rust

Rust被应用于系统开发、操作系统、企业系统、微控制器应用、嵌入式系统、文件系统、浏览器组件、虚拟现实的仿真引擎等。

当性能很重要的时候,Rust是一种常用的语言,因为它能很好地处理大量数据。它可以处理CPU密集型的操作,如执行算法,这就是为什么Rust比Python更适合系统开发的原因。

Rust 保证了内存的安全性,让你可以控制线程行为和线程之间的资源分配方式。这使你能够构建复杂的系统,这使Rust比Python更有优势。

总而言之,你应在以下情况下使用Rust:

  • 你的项目需要高性能
  • 你正在构建复杂的系统
  • 你重视内存安全而不是简单性

何时使用Python

Python可以用于许多应用领域,从Web开发,到数据科学和分析,到AI和机器学习,再到软件开发。

Python被广泛用于机器学习,数据科学和AI,因为它是:

  • 简单易写
  • 灵活的
  • 包含大量面向数据的软件包和库
  • 由出色的工具和库生态系统支持

在以下情况下,你应该使用Python:

  • 你需要一种灵活的语言来支持Web开发,数据科学和分析以及机器学习和AI
  • 你重视可读性和简单性
  • 你需要一种对初学者友好的语言
  • 与性能相比,你更喜欢语法简单和开发速度

为什么Rust可以取代Python

考虑到Rust的迅速普及和广泛的用例,它似乎几乎不可避免地会在不久的将来超越Python,以下是一些原因。

性能

Rust超越Python的一个主要原因是性能。因为Rust是直接编译成机器代码的,所以在你的代码和计算机之间没有虚拟机或解释器。

与Python相比,另一个关键优势是Rust的线程和内存管理。虽然Rust不像Python那样有垃圾回收功能,但Rust中的编译器会强制检查无效的内存引用泄漏和其他危险或不规则行为。

编译语言通常比解释语言要快。但是,使Rust处于不同水平的是,它几乎与C和C ++一样快,但是没有开销。

让我们看一个用Python编写的O(log n)程序的示例,并使用迭代方法计算完成任务所需的时间:

import random
import datetime
def binary_searcher(search_key, arr):
  low = 0
  high = len(arr)-1
  while low <= high:
    mid = int(low + (high-low)//2)
    if search_key == arr[mid]:
      return True
    if search_key < arr[mid]:
      high = mid-1
      elif search_key > arr[mid]:
        low = mid+1
return False

输出:

> python -m binny.py
It took 8.6μs to search

现在,让我们来看一下使用迭代方法用Rust编写的定时O(log n)程序:

>use rand::thread_rng;
use std::time::Instant;
use floating_duration::TimeFormat;

fn binary_searcher(search_key: i32, vec: &mut Vec<i32>) -> bool {
  let mut low: usize = 0;
  let mut high: usize = vec.len()-1;
  let mut _mid: usize = 0;
  while low <= high {
    _mid = low + (high-low)/2;
    if search_key == vec[_mid] {
      return true;
    }
    if search_key < vec[_mid] {
      high = _mid - 1;
    } else if search_key > vec[_mid] {
      low = _mid + 1;
    }
  }
  return false;
}

fn main() {
  let mut _rng = thread_rng();
  let mut int_vec = Vec::new();
  let max_num = 1000000;

  for num in 1..max_num {
    int_vec.push(num as i32);
  }
  let start = Instant::now();
  let _result = binary_searcher(384723, &mut int_vec);
  println!("It took: {} to search", TimeFormat(start.elapsed()));
}

输出

> cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.04s
Running target\debug\algo_rusty.exe
It took: 4.6μs to search

在没有任何优化技术的情况下,Rust和Python在同一台机器上执行类似的操作分别需要4.6微秒和8.6微秒。这意味着Python花费的时间几乎是Rust的两倍。

内存管理

Python 和大多数现代编程语言一样,被设计成内存安全的。然而Rust在内存安全方面却让Python望尘莫及,即使没有垃圾回收。

Rust采用了一种独特的方式来确保内存安全,其中涉及所有权系统和借用检查器(borrow checker)。Rust的借用检查器确保引用和指针不会超过它们所指向的数据。

错误检查与诊断

Python和其他语言一样,提供了错误检查和日志机制。但是在让开发者知道出了什么问题的时候,Rust和Python之间有一些对比。

举一个Python变量错误的典型例子:

apple = 15
print('The available apples are:', apple)

Python输出

Traceback (most recent call last):
    File "binny.py", line 2, in <module>
      print('The available apples are:', aple)
    NameError: name 'aple' is not defined

Rust中的类似示例:

fn main() {
  let apple = 15;
  println!("The available apples are:", apple);
}

Rust输出

println!("The available apples are:", aple);
   ^^^^ help: a local variable with a similar name exists: `apple`

在这里,Rust推荐了可能的变量,这些变量可能是你想输入的。Python只会抛出错误,而不会给出如何修复的建议。

举个例子:

fn main() {
  let grass = 13;

  grass += 1;
}

此代码引发错误,因为默认情况下Rust中的变量是不可变的。除非它具有关键字 ou’'t,否则无法更改。

错误:

let grass = 13;
      |         -----
      |         |
      |         first assignment to `grass`
      |         help: make this binding mutable: `mut grass`

修正错误:

fn main() {
  let mut _grass: i32 = 13;

  _grass += 1;
}

如你所见,现在它不会引发任何错误。除此之外,Rust不允许不同的数据类型相互操作,除非将它们转换为相同的类型。

因此,维护Rust代码库通常很容易。除非指定,否则Rust不允许更改。 Python确实允许这种性质的更改。

与大多数编译语言相比,Rust因其速度快、内存安全有保证、超强的可靠性、一致性和用户友好性而备受青睐。在编程中,我们已经到了速度开始变得毫不费力的地步。

随着技术的发展,它变得越来越快,试图在更短的时间内做更多的事情,而不需要那么多的权衡。Rust帮助实现了这一点,同时又不妨碍开发者的工作。当技术试图推动可以实现的边界时,它也会考虑系统的安全性和可靠性,这是Rust背后的主要思想。

并行运算

除了速度外,Python在并行计算方面也有局限性。

Python使用全局解释器锁(GIL),它鼓励只有一个线程同时执行,以提高单线程的性能。这个过程是一个阻碍,因为它意味着你不能使用多个CPU核进行密集计算。

社区

如前所述,Stack Overflow的“ 2020开发人员调查”中有86%的受访者将Rust称为2020年最喜欢的编程语言。

同样,“ 2020 HackerRank开发人员技能报告”的受访者将Rust列为他们计划下一步学习的十大编程语言:

相比之下,2019年的调查将Rust排在列表的底部,这表明Rust开发人员社区正在迅速增长。

正如这些数据所示,Rust正在成为主流开发者社区的一部分。许多大公司都在使用Rust,一些开发者甚至用它来构建其他编程语言使用的库。著名的Rust用户包括Mozilla、Dropbox、Atlassian、npm和Cloudflare等等。

Amazon Web Service还对Lambda,EC2和S3中的性能敏感组件采用了Rust。在2019年,AWS宣布赞助Rust项目,此后为Rust提供了AWS开发工具包。

公司正越来越多地用更高效的编程语言(如Rust)取代速度较慢的编程语言。没有其他语言能像Rust一样在简单和速度之间做出平衡。

总结

Rust已经发展成为一种常用的编程语言,其采用率也因此而增加。虽然Python在机器学习/数据科学社区中占有稳固的地位,但Rust很可能在未来被用作Python库的更有效后端。

Rust具有取代Python的巨大潜力。在目前的趋势下,作为应用、性能和速度方面的首选编程语言,Rust不仅仅是一种编程语言,更是一种思维方式。

查看原文

赞 0 收藏 0 评论 0

杜尼卜 发布了文章 · 3月15日

2021年要了解的34种JavaScript简写优化技术

开发者的生活总是在学习新的东西,跟上变化不应该比现在更难,我的动机是介绍所有JavaScript的最佳实践,比如简写功能,作为一个前端开发者,我们必须知道,让我们的生活在2021年变得更轻松。

你可能做了很长时间的JavaScript开发,但有时你可能没有更新最新的特性,这些特性可以解决你的问题,而不需要做或编写一些额外的代码。这些技术可以帮助您编写干净和优化的JavaScript代码。此外,这些主题可以帮助你为2021年的JavaScript面试做准备。

1.如果有多个条件

我们可以在数组中存储多个值,并且可以使用数组 include 方法。

//Longhand
if (x === 'abc' || x === 'def' || x === 'ghi' || x ==='jkl') {
  //logic
}

//Shorthand
if (['abc', 'def', 'ghi', 'jkl'].includes(x)) {
  //logic
}

2.如果为真…否则简写

这对于我们有 if-else 条件,里面不包含更大的逻辑时,是一个较大的捷径。我们可以简单的使用三元运算符来实现这个简写。

// Longhand
let test: boolean;
if (x > 100) {
  test = true;
} else {
  test = false;
}

// Shorthand
let test = (x > 10) ? true : false;
//or we can use directly
let test = x > 10;
console.log(test);

当我们有嵌套条件时,我们可以采用这种方式。

let x = 300,
test2 = (x > 100) ? 'greater 100' : (x < 50) ? 'less 50' : 'between 50 and 100';
console.log(test2); // "greater than 100"

3.声明变量

当我们要声明两个具有共同值或共同类型的变量时,可以使用此简写形式。

//Longhand 
let test1;
let test2 = 1;

//Shorthand 
let test1, test2 = 1;

4.Null, Undefined,空检查

当我们创建新的变量时,有时我们想检查我们引用的变量的值是否为空或undefined。JavaScript确实有一个非常好的简写工具来实现这些功能。

// Longhand
if (test1 !== null || test1 !== undefined || test1 !== '') {
    let test2 = test1;
}

// Shorthand
let test2 = test1 || '';

5.null值检查和分配默认值

let test1 = null,
    test2 = test1 || '';

console.log("null check", test2); // output will be ""

6.undefined值检查和分配默认值

let test1 = undefined,
    test2 = test1 || '';

console.log("undefined check", test2); // output will be ""

正常值检查

let test1 = 'test',
    test2 = test1 || '';

console.log(test2); // output: 'test'

7.将值分配给多个变量

当我们处理多个变量并希望将不同的值分配给不同的变量时,此简写技术非常有用。

//Longhand 
let test1, test2, test3;
test1 = 1;
test2 = 2;
test3 = 3;

//Shorthand 
let [test1, test2, test3] = [1, 2, 3];

8.赋值运算符简写

我们在编程中处理很多算术运算符,这是将运算符分配给JavaScript变量的有用技术之一。

// Longhand
test1 = test1 + 1;
test2 = test2 - 1;
test3 = test3 * 20;

// Shorthand
test1++;
test2--;
test3 *= 20;

9.如果存在简写

这是我们大家都在使用的常用简写之一,但仍然值得一提。

// Longhand
if (test1 === true) or if (test1 !== "") or if (test1 !== null)

// Shorthand //it will check empty string,null and undefined too
if (test1)

注意:如果test1有任何值,它将在if循环后进入逻辑,该运算符主要用于 nullundefined 的检查。

10.多个条件的AND(&&)运算符

如果仅在变量为 true 的情况下才调用函数,则可以使用 && 运算符。

//Longhand 
if (test1) {
 callMethod(); 
} 

//Shorthand 
test1 && callMethod();

11.foreach循环简写

这是迭代的常用简写技术之一。

// Longhand
for (var i = 0; i < testData.length; i++)

// Shorthand
for (let i in testData) or  for (let i of testData)

每个变量的数组

function testData(element, index, array) {
  console.log('test[' + index + '] = ' + element);
}

[11, 24, 32].forEach(testData);
// logs: test[0] = 11, test[1] = 24, test[2] = 32

12.return中比较

我们也可以在return语句中使用比较。它将避免我们的5行代码,并将它们减少到1行。

// Longhand
let test;
function checkReturn() {
  if (!(test === undefined)) {
    return test;
  } else {
    return callMe('test');
  }
}
var data = checkReturn();
console.log(data); //output test
function callMe(val) {
    console.log(val);
}

// Shorthand
function checkReturn() {
    return test || callMe('test');
}

13.箭头函数

//Longhand 
function add(a, b) { 
   return a + b; 
} 

//Shorthand 
const add = (a, b) => a + b;

更多示例。

function callMe(name) {
  console.log('Hello', name);
}
callMe = name => console.log('Hello', name);

14.短函数调用

我们可以使用三元运算符来实现这些功能。

// Longhand
function test1() {
  console.log('test1');
};
function test2() {
  console.log('test2');
};
var test3 = 1;
if (test3 == 1) {
  test1();
} else {
  test2();
}

// Shorthand
(test3 === 1? test1:test2)();

15. Switch简写

我们可以将条件保存在键值对象中,并可以根据条件使用。

// Longhand
switch (data) {
  case 1:
    test1();
  break;

  case 2:
    test2();
  break;

  case 3:
    test();
  break;
  // And so on...
}

// Shorthand
var data = {
  1: test1,
  2: test2,
  3: test
};

data[something] && data[something]();

16.隐式返回简写

使用箭头函数,我们可以直接返回值,而不必编写return语句。

//longhand
function calculate(diameter) {
  return Math.PI * diameter
}

//shorthand
calculate = diameter => (
  Math.PI * diameter;
)

17.小数基数指数

// Longhand
for (var i = 0; i < 10000; i++) { ... }

// Shorthand
for (var i = 0; i < 1e4; i++) {

18.默认参数值

//Longhand
function add(test1, test2) {
  if (test1 === undefined)
    test1 = 1;
  if (test2 === undefined)
    test2 = 2;
  return test1 + test2;
}

//shorthand
add = (test1 = 1, test2 = 2) => (test1 + test2);
add() //output: 3

19.扩展运算符简写

//longhand

// joining arrays using concat
const data = [1, 2, 3];
const test = [4 ,5 , 6].concat(data);

//shorthand

// joining arrays
const data = [1, 2, 3];
const test = [4 ,5 , 6, ...data];
console.log(test); // [ 4, 5, 6, 1, 2, 3]

对于克隆,我们也可以使用扩展运算符。

//longhand

// cloning arrays
const test1 = [1, 2, 3];
const test2 = test1.slice()

//shorthand

// cloning arrays
const test1 = [1, 2, 3];
const test2 = [...test1];

20.模板文字

如果您厌倦了在单个字符串中使用 + 来连接多个变量,那么这种简写可以消除您的头痛。

//longhand
const welcome = 'Hi ' + test1 + ' ' + test2 + '.'

//shorthand
const welcome = `Hi ${test1} ${test2}`;

21.多行字符串简写

当我们在代码中处理多行字符串时,可以使用以下功能:

//longhand
const data = 'abc abc abc abc abc abc\n\t'
    + 'test test,test test test test\n\t'

//shorthand
const data = `abc abc abc abc abc abc
         test test,test test test test`

22.对象属性分配

let test1 = 'a'; 
let test2 = 'b';

//Longhand 
let obj = {test1: test1, test2: test2}; 

//Shorthand 
let obj = {test1, test2};

23.将字符串转换成数字

//Longhand 
let test1 = parseInt('123'); 
let test2 = parseFloat('12.3'); 

//Shorthand 
let test1 = +'123'; 
let test2 = +'12.3';

24.用解构简写

//longhand
const test1 = this.data.test1;
const test2 = this.data.test2;
const test2 = this.data.test3;

//shorthand
const { test1, test2, test3 } = this.data;

25.用Array.find简写

当我们确实有一个对象数组并且我们想要根据对象属性查找特定对象时,find方法确实很有用。

const data = [
  {
    type: 'test1',
    name: 'abc'
  },
  {
    type: 'test2',
    name: 'cde'
  },
  {
    type: 'test1',
    name: 'fgh'
  },
]
function findtest1(name) {
  for (let i = 0; i < data.length; ++i) {
    if (data[i].type === 'test1' && data[i].name === name) {
      return data[i];
    }
  }
}

//Shorthand
filteredData = data.find(data => data.type === 'test1' && data.name === 'fgh');
console.log(filteredData); // { type: 'test1', name: 'fgh' }

26.查找条件简写

如果我们有代码来检查类型,根据类型需要调用不同的方法,我们可以选择使用多个else ifs或者switch,但是如果我们有比这更好的简写方法呢?

// Longhand
if (type === 'test1') {
  test1();
}
else if (type === 'test2') {
  test2();
}
else if (type === 'test3') {
  test3();
}
else if (type === 'test4') {
  test4();
} else {
  throw new Error('Invalid value ' + type);
}

// Shorthand
var types = {
  test1: test1,
  test2: test2,
  test3: test3,
  test4: test4
};
 
var func = types[type];
(!func) && throw new Error('Invalid value ' + type); func();

27.按位索引简写

当我们遍历数组以查找特定值时,我们确实使用 indexOf() 方法,如果找到更好的方法该怎么办?让我们看看这个例子。

//longhand
if(arr.indexOf(item) > -1) { // item found 
}
if(arr.indexOf(item) === -1) { // item not found
}

//shorthand
if(~arr.indexOf(item)) { // item found
}
if(!~arr.indexOf(item)) { // item not found
}

按位()运算符将返回除-1以外的任何值的真实值。否定它就像做 ~~ 一样简单。另外,我们也可以使用 include() 函数:

if (arr.includes(item)) { 
    // true if the item found
}

28.Object.entries()

此函数有助于将对象转换为对象数组。

const data = { test1: 'abc', test2: 'cde', test3: 'efg' };
const arr = Object.entries(data);
console.log(arr);
/** Output:
[ [ 'test1', 'abc' ],
  [ 'test2', 'cde' ],
  [ 'test3', 'efg' ]
]
**/

29.Object.values()

这也是ES8中引入的一项新功能,该功能执行与 Object.entries() 类似的功能,但没有关键部分:

const data = { test1: 'abc', test2: 'cde' };
const arr = Object.values(data);
console.log(arr);
/** Output:
[ 'abc', 'cde']
**/

30.双按位简写

双重NOT按位运算符方法仅适用于32位整数)

// Longhand
Math.floor(1.9) === 1 // true

// Shorthand
~~1.9 === 1 // true

31.重复一个字符串多次

要一次又一次地重复相同的字符,我们可以使用for循环并将它们添加到同一循环中,但是如果我们有一个简写方法呢?

//longhand 
let test = ''; 
for(let i = 0; i < 5; i ++) { 
  test += 'test '; 
} 
console.log(str); // test test test test test 

//shorthand 
'test '.repeat(5);

32.在数组中查找最大值和最小值

const arr = [1, 2, 3]; 
Math.max(…arr); // 3
Math.min(…arr); // 1

33.从字符串中获取字符

let str = 'abc';

//Longhand 
str.charAt(2); // c

//Shorthand 
Note: If we know the index of the array then we can directly use index insted of character.If we are not sure about index it can throw undefined
str[2]; // c

34.数学指数幂函数的简写

//longhand
Math.pow(2,3); // 8

//shorthand
2**3 // 8
查看原文

赞 84 收藏 59 评论 19

杜尼卜 发布了文章 · 3月9日

面向对象编程是计算机科学的最大错误

C++和Java可能是计算机科学中最严重的错误。两者都受到了OOP创始人Alan Kay本人以及其他许多著名计算机科学家的严厉批评。然而,C++和Java为最臭名昭著的编程范式--现代OOP铺平了道路。

它的普及是非常不幸的,它对现代经济造成了极大的破坏,造成了数万亿美元至数万亿美元的间接损失。成千上万人的生命因OOP而丧失。在过去的三十年里,没有一个行业不受潜伏的OO危机的影响,它就在我们眼前展开。

为什么OOP如此危险?让我们找出答案。

想象一下,在一个美丽的周日下午,带着家人出去兜风。外面的天气很好,阳光明媚。你们所有人都进入车内,走的是已经开过一百万次的同一条高速公路。

然而这次却有些不一样了--车子一直不受控制地加速,即使你松开油门踏板也是如此。刹车也不灵了,似乎失去了动力。为了挽救局面,你铤而走险,拉起了紧急刹车。这样一来,在你的车撞上路边的路堤之前,就在路上留下了一个150英尺长的滑痕。

听起来像一场噩梦?然而这正是2007年9月让-布克特在驾驶丰田凯美瑞时发生的事情。这并不是唯一的此类事件。这是众多与所谓的“意外加速”有关的事件之一。“意外加速”已困扰丰田汽车十多年,造成近百人死亡。汽车制造商很快就将矛头指向了“粘性踏板”、驾驶员失误,甚至地板垫等方面。然而,一些专家早就怀疑可能是有问题的软件在作怪。

为了帮助解决这个问题,请来了美国宇航局的软件专家,结果一无所获。直到几年后,在调查Bookout事件的过程中,另一个软件专家团队才找到了真凶。他们花了近18个月的时间来研究丰田的代码,他们将丰田的代码库描述为“意大利面条代码”——程序员的行话,意思是混乱的代码。

软件专家已经演示了超过1000万种丰田软件导致意外加速的方法。最终,丰田被迫召回了900多万辆汽车,并支付了超过30亿美元的和解费和罚款。

意大利面条代码有问题吗?

Photo by Andrea Piacquadio from Pexels

某些软件故障造成的100条生命是太多了,真正令人恐惧的是,丰田代码的问题不是唯一的。

两架波音737 Max飞机坠毁,造成346人死亡,损失超过600亿美元。这一切都是因为一个软件bug, 100%肯定是意大利面条式代码造成的。

意大利面条式的代码困扰着世界上太多的代码库。飞机上的电脑,医疗设备,核电站运行的代码。

程序代码不是为机器编写的,而是为人类编写的。正如马丁·福勒(Martin Fowler)所说:“任何傻瓜都可以编写计算机可以理解的代码。好的程序员编写人类可以理解的代码。”

如果代码不能运行,那么它就是坏的。然而如果人们不能理解代码,那么它就会被破坏。很快就会。

我们绕个弯子,说说人脑。人脑是世界上最强大的机器。然而,它也有自己的局限性。我们的工作记忆是有限的,人脑一次只能思考5件事情。这就意味着,程序代码的编写要以不压垮人脑为前提。

意大利面条代码使人脑无法理解代码库。这具有深远的影响--不可能看到某些改变是否会破坏其他东西,对缺陷的详尽测试变得不可能。

是什么导致意大利面条代码?

Photo by Craig Adderley from Pexels

为什么代码会随着时间的推移变成意大利面条代码?因为熵--宇宙中的一切最终都会变得无序、混乱。就像电缆最终会变得纠缠不清一样,我们的代码最终也会变得纠缠不清。除非有足够的约束条件。

为什么我们要在道路上限速?是的,有些人总会讨厌它们,但它们可以防止我们撞死人。为什么我们要在马路上设置标线?为了防止人们走错路,防止事故的发生。

类似的方法在编程时完全有意义。这样的约束不应该让人类程序员去实施。它们应该由工具自动执行,或者最好由编程范式本身执行。

为什么OOP是万恶之源?

Photo by NeONBRAND on Unsplash

我们如何执行足够的约束以防止代码变成意大利面条?两个选择--手动,或者自动。手动方式容易出错,人总会出错。因此,自动执行这种约束是符合逻辑的。

不幸的是,OOP并不是我们一直在寻找的解决方案。它没有提供任何约束来帮助解决代码纠缠的问题。人们可以精通各种OOP的最佳实践,比如依赖注入、测试驱动开发、领域驱动设计等(确实有帮助)。然而,这些都不是编程范式本身所能强制执行的(而且也没有这样的工具可以强制执行最佳实践)。

内置的OOP功能都无助于防止意大利面条代码——封装只是将状态隐藏并分散在程序中,这只会让事情变得更糟。继承性增加了更多的混乱,OOP多态性再次让事情变得更加混乱——在运行时不知道程序到底要走什么执行路径是没有好处的,尤其是涉及到多级继承的时候。

OOP进一步加剧了意大利面条代码的问题

缺乏适当的约束(以防止代码变得混乱)不是OOP的唯一缺点。

在大多数面向对象的语言中,默认情况下所有内容都是通过引用共享的。实际上把一个程序变成了一个巨大的全局状态的blob,这与OOP的初衷直接冲突。OOP的创造者Alan Kay有生物学的背景,他有一个想法,就是想用一种类似生物细胞的方式来编写计算机程序的语言(Simula),他想让独立的程序(细胞)通过互相发送消息来进行交流。独立程序的状态绝不会与外界共享(封装)。

Alan Kay从未打算让“细胞”直接进入其他细胞的内部进行改变。然而,这正是现代OOP中所发生的事情,因为在现代OOP中,默认情况下,所有东西都是通过引用来共享的。这也意味着,回归变得不可避免。改变程序的一个部分往往会破坏其他地方的东西(这在其他编程范式,如函数式编程中就不那么常见了)。

我们可以清楚地看到,现代OOP存在着根本性的缺陷。它是每天工作中会折磨你的“怪物”,而且它还会在晚上缠着你。

让我们来谈谈可预测性

Photo by samsommer on Unsplash

意大利面代码是个大问题,面向对象的代码特别容易意大利化。

意大利面条代码使软件无法维护,然而这只是问题的一部分。我们也希望软件是可靠的。但这还不够,软件(或任何其他系统)被期望是可预测的。

任何系统的用户无论如何都应该有同样的可预测的体验。踩汽车油门踏板的结果总是汽车加速。按下刹车应该总是导致汽车减速。用计算机科学的行话来说,我们希望汽车是确定性的

汽车出现随机行为是非常不可取的,比如油门无法加速,或者刹车无法制动(丰田问题),即使这样的问题在万亿次中只出现一次。

然而大多数软件工程师的心态是“软件应该足够好,让我们的客户继续使用”。我们真的不能做得更好吗?当然,我们可以,而且我们应该做得更好!最好的开始是解决我们方案的非确定性

非确定性101

在计算机科学中,非确定性算法是相对于确定性算法而言的,即使对于相同的输入,也可以在不同的运行中表现出不同的行为。

——维基百科关于非确定性算法的文章

如果上面维基百科上关于非确定性的引用你听起来不顺耳,那是因为非确定性没有任何好处。我们来看看一个简单调用函数的代码样本。

console.log( 'result', computea(2) );
console.log( 'result', computea(2) );
console.log( 'result', computea(2) );

// output:
// result 4
// result 4
// result 4

我们不知道这个函数的作用,但似乎在给定相同输入的情况下,这个函数总是返回相同的输出。现在,让我们看一下另一个示例,该示例调用另一个函数 computeb

console.log( 'result', computeb(2) );
console.log( 'result', computeb(2) );
console.log( 'result', computeb(2) );
console.log( 'result', computeb(2) );

// output:
// result 4
// result 4
// result 4
// result 2    <=  not good

这次,函数为相同的输入返回了不同的值。两者之间有什么区别?前者的函数总是在给定相同的输入的情况下产生相同的输出,就像数学中的函数一样。换句话说,函数是确定性的。后一个函数可能会产生预期值,但这是不保证的。或者换句话说,这个函数是不确定的。

是什么使函数具有确定性或不确定性?

  • 不依赖外部状态的函数是100%确定性的。
  • 仅调用其他确定性函数的函数是确定性的。
function computea(x) {
  return x * x;
}

function computeb(x) {
  return Math.random() < 0.9
          ? x * x
          : x;
}

在上面的例子中,computea 是确定性的,在给定相同输入的情况下,它总是会给出相同的输出。因为它的输出只取决于它的参数 x

另一方面,computeb 是非确定性的,因为它调用了另一个非确定性函数 Math.random()。我们怎么知道Math.random()是非确定性的?在内部,它依赖于系统时间(外部状态)来计算随机值。它也不接受任何参数--这是一个依赖于外部状态的函数的致命漏洞。

确定性与可预测性有什么关系?确定性的代码是可预测的代码,非确定性代码是不可预测的代码。

从确定性到非确定性

我们来看看一个加法函数:

function add(a, b) {
  return a + b;
};

我们始终可以确定,给定 (2, 2) 的输入,结果将始终等于 4。我们怎么能这么肯定呢?在大多数编程语言中,加法运算都是在硬件上实现的,换句话说,CPU负责计算的结果要始终保持不变。除非我们处理的是浮点数的比较,(但这是另一回事,与非确定性问题无关)。现在,让我们把重点放在整数上。硬件是非常可靠的,可以肯定的是,加法的结果永远是正确的。

现在,让我们将值 2 装箱:

const box = value => ({ value });

const two = box(2);
const twoPrime = box(2);

function add(a, b) {
  return a.value + b.value;
}

console.log("2 + 2' == " + add(two, twoPrime));
console.log("2 + 2' == " + add(two, twoPrime));
console.log("2 + 2' == " + add(two, twoPrime));

// output:
// 2 + 2' == 4
// 2 + 2' == 4
// 2 + 2' == 4

到目前为止,函数是确定性的!

现在,我们对函数的主体进行一些小的更改:

function add(a, b) {
  a.value += b.value;
  return a.value;
}

console.log("2 + 2' == " + add(two, twoPrime));
console.log("2 + 2' == " + add(two, twoPrime));
console.log("2 + 2' == " + add(two, twoPrime));

// output:
// 2 + 2' == 4
// 2 + 2' == 6
// 2 + 2' == 8

怎么了?突然间,函数的结果不再是可预测的了!它第一次工作正常,但在随后的每次运行中,它的结果开始变得越来越不可预测。它第一次运行得很好,但在随后的每一次运行中,它的结果开始变得越来越不可预测。换句话说,这个函数不再是确定性的。

为什么它突然变得不确定了?该函数修改了其范围外的值,引起了副作用。

让我们回顾一下

确定性程序可确保 2 + 2 == 4,换句话说,给定输入 (2, 2),函数 add 始终应得到 4 的输出。不管你调用函数多少次,不管你是否并行调用函数,也不管函数外的世界是什么样子。

非确定性程序正好相反,在大多数情况下,调用 add(2, 2) 将返回 4 。但偶尔,函数可能会返回3、5,甚至1004。在程序中,非确定性是非常不可取的,希望你现在能明白为什么。

非确定性代码的后果是什么?软件缺陷,也就是通常所说的 “bug”。错误使开发人员浪费了宝贵的调试时间,如果他们进入生产领域,会大大降低客户体验。

为了使我们的程序更可靠,我们应该首先解决非确定性问题。

副作用

Photo by Igor Yemelianov on Unsplash

这给我们带来了副作用的问题。

什么是副作用?如果你正在服用治疗头痛的药物,但这种药物让你恶心,那么恶心就是一种副作用。简单来说,就是一些不理想的东西。

想象一下,你已经购买了一个计算器,你把它带回家,开始使用,然后突然发现这不是一个简单的计算器。你给自己弄了个扭曲的计算器!您输入 10 * 11,它将输出 110,但它同时还向您大喊一百和十。这是副作用。接下来,输入 41+1,它会打印42,并注释“42,生命的意义”。还有副作用!你很困惑,然后开始和你的另一半说你想要点披萨。计算器听到了对话,大声说“ok”,然后点了一份披萨。还有副作用!

让我们回到加法函数:

function add(a, b) {
  a.value += b.value;
  return a.value;
}

是的,该函数执行了预期的操作,将 a 添加到 b。然而,它也引入了一个副作用,调用 a.value += b.value 导致对象 a 发生变化。函数参数 a 引用的是对象 2,因此是 2value 不再等于 2。第一次调用后,其值变为 4,第二次调用后,其值为 6,依此类推。

纯度

在讨论了确定性和副作用之后,我们准备谈谈纯函数,纯函数是指既具有确定性,又没有副作用的函数。

再一次,确定性意味着可预测--在给定相同输入的情况下,函数总是返回相同的结果。而无副作用意味着该函数除了返回一个值之外,不会做任何其他事情,这样的函数才是纯粹的。

纯函数有什么好处?正如我已经说过的,它们是可以预测的。这使得它们非常容易测试,对纯函数进行推理很容易——不像OOP,不需要记住整个应用程序的状态。您只需要关心正在处理的当前函数。

纯函数可以很容易地组合(因为它们不会改变其作用域之外的任何东西)。纯函数非常适合并发,因为函数之间不共享任何状态。重构纯函数是一件非常有趣的事情——只需复制粘贴,不需要复杂的IDE工具。

简而言之,纯函数将欢乐带回到编程中。

面向对象编程的纯度如何?

为了举例说明,我们来讨论一下OOP的两个功能:getter和setter。

getter的结果依赖于外部状态——对象状态。多次调用getter可能会导致不同的输出,这取决于系统的状态。这使得getter具有内在的不确定性

现在说说setter,Setters的目的是改变对象的状态,这使得它们本身就具有副作用

这意味着OOP中的所有方法(也许除了静态方法)要么是非确定性的,要么会引起副作用,两者都不好。因此,面向对象的程序设计绝不是纯粹的,它与纯粹完全相反。

有一个银弹

但是我们很少有人敢尝试。

Photo by Mohamed Nohassi on Unsplash

无知不是耻辱,而是不愿学习。

— Benjamin Franklin

在软件失败的阴霾世界中,仍有一线希望,那将会解决大部分问题,即使不是所有问题。一个真正的银弹。但前提是你愿意学习和应用——大多数人都不愿意。

银弹的定义是什么?可以用来解决我们所有问题的东西。数学是灵丹妙药吗?如果说有什么区别的话,那就是它几乎是一颗银弹。

我们应该感谢成千上万的聪明的男人和女人,几千年来他们辛勤工作,为我们提供数学。欧几里得,毕达哥拉斯,阿基米德,艾萨克·牛顿,莱昂哈德·欧拉,阿朗佐·丘奇,还有很多很多其他人。

如果不确定性(即不可预测)的事物成为现代科学的支柱,你认为我们的世界会走多远?可能不会太远,我们会停留在中世纪。这在医学界确实发生过——在过去,没有严格的试验来证实某种特定治疗或药物的疗效。人们依靠医生的意见来治疗他们的健康问题(不幸的是,这在俄罗斯等国家仍然发生)。在过去,放血等无效的技术一直很流行。像砷这样不安全的物质被广泛使用。

不幸的是,今天的软件行业与过去的医药太相似了。它不是建立在坚实的基础上。相反,现代软件业大多是建立在一个薄弱的摇摇欲坠的基础上,称为面向对象的编程。如果人的生命直接依赖于软件,OOP早就消失了,就像放血和其他不安全的做法一样,被人遗忘了。

坚实的基础

Photo by Zoltan Tasi on Unsplash

有没有其他选择?在编程的世界里,我们能不能有像数学一样可靠的东西?是的,可以!许多数学概念可以直接转化为编程,并为所谓的函数式编程奠定基础。

是什么让它如此稳健?它是基于数学,特别是Lambda微积分。

来做个比较,现代的OOP是基于什么呢?是的,真正的艾伦·凯是基于生物细胞的。然而,现代的Java/C# OOP是基于一组荒谬的思想,如类、继承和封装,它没有天才Alan Kay所发明的原始思想,剩下的只是一套创可贴,用来弥补其劣等思想的缺陷。

函数式编程呢?它的核心构建块是一个函数,在大多数情况下是一个纯函数,纯函数是确定性的,这使它们可预测,这意味着由纯函数组成的程序将是可预测的。它们会永远没有bug吗?不,但是如果程序中有一个错误,它也是确定的——相同的输入总是会出现相同的错误,这使得它更容易修复。

我怎么到这里了?

在过去,在过程/函数出现之前 goto 语句在编程语言中被广泛使用。goto 语句只是允许程序在执行期间跳转到代码的任何部分。这让开发人员真的很难回答 “我是怎么执行到这一步的?” 的问题。是的,这也造成了大量的BUG。

如今,一个非常类似的问题正在发生。只不过这次的难题是 “我怎么会变成这个样子”,而不是 “我怎么会变成这个执行点”。

OOP(以及一般的命令式编程)使得回答 “我是如何达到这个状态的?” 这个问题变得很难。在OOP中,所有的东西都是通过引用传递的。这在技术上意味着,任何对象都可以被任何其他对象突变(OOP没有任何限制来阻止这一点)。而且封装也没有任何帮助--调用一个方法来突变某个对象字段并不比直接突变它好。这意味着,程序很快就会变成一团乱七八糟的依赖关系,实际上使整个程序成为一个全局状态的大块头。

有什么办法可以让我们不再问 “我怎么会变成这样” 的问题?你可能已经猜到了,函数式编程。

过去很多人都抵制停止使用 goto 的建议,就像今天很多人抵制函数式编程,和不可变状态的理念一样。

但是等等,意大利面条代码呢?

在OOP中,它被认为是 “优先选择组成而不是继承” 的最佳实践。从理论上讲,这种最佳做法应该对意大利面条代码有所帮助。不幸的是,这只是一种 “最佳实践”。面向对象的编程范式本身并没有为执行这样的最佳实践设置任何约束。这取决于你团队中的初级开发人员是否遵循这样的最佳实践,以及这些实践是否在代码审查中得到执行(这并不总是发生)。

那函数式编程呢?在函数式编程中,函数式组成(和分解)是构建程序的唯一方法。这意味着,编程范式本身就强制执行组成。这正是我们一直在寻找的东西!

函数调用其他函数,大的函数总是由小的函数组成,就是这样。与OOP中不同的是,函数式编程中的组成是自然的。此外,这使得像重构这样的过程变得极为简单——只需简单地剪切代码,并将其粘贴到一个新的函数中。不需要管理复杂的对象依赖关系,不需要复杂的工具(如Resharper)。

可以清楚地看到,OOP对于代码组织来说是一个较差的选择。这是函数式编程的明显胜利。

但是OOP和FP是相辅相成的!

抱歉让您失望,它们不是互补的。

面向对象编程与函数式编程完全相反。说OOP和FP是互补的,可能就等于说放血和抗生素是互补的,是吗?

OOP违反了许多基本的FP原则:

  • FP提倡纯净,而OOP提倡杂质。
  • FP代码基本上是确定性的,因此是可预测的。OOP代码本质上是不确定性的,因此是不可预测的。
  • 组合在FP中是自然的,在OOP中不是自然的。
  • OOP通常会导致错误百出的软件和意大利面条式的代码。FP产生了可靠、可预测和可维护的软件。
  • 在FP中很少需要调试,而简单的单元测试往往不需要调试。另一方面,OOP程序员生活在调试器中。
  • OOP程序员把大部分时间花在修复bug上。FP程序员把大部分时间花在交付结果上。

归根结底,函数式编程是软件世界的数学。如果数学已经为现代科学打下了坚实的基础,那么它也可以以函数式编程的形式为我们的软件打下坚实的基础。

采取行动,为时已晚

OOP是一个非常大且代价高昂的错误,让我们最终都承认吧。

想到我坐的车运行着用OOP编写的软件,我就害怕。知道带我和我的家人去度假的飞机使用面向对象的代码并没有让我感到更安全。

现在是我们大家最终采取行动的时候了。我们都应该从一小步开始,认识到面向对象编程的危险,并开始努力学习函数式编程。这不是一个快速的过程,至少需要十年的时间,我们大多数人才能实现转变。我相信,在不久的将来,那些一直使用OOP的人将会被视为 “恐龙”,就像今天的COBOL程序员一样,被淘汰。C ++和Java将会消亡, C#将死亡,TypeScript也将很快成为历史。

我希望你今天就行动起来——如果你还没有开始学习函数式编程,就开始学习吧。成为真正的好手,并传播这个词。F#、ReasonML和Elixir都是入门的好选择。


巨大的软件革命已经开始。你们会加入,还是会被甩在后面?

查看原文

赞 0 收藏 0 评论 2

认证与成就

  • 认证信息 前端工程师
  • 获得 2555 次点赞
  • 获得 15 枚徽章 获得 0 枚金徽章, 获得 4 枚银徽章, 获得 11 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • 编程日历小程序

    编程日历小程序基于开源日历书《了不起的程序员2021》制作而成,但比图书更精美丰富。365 天,每天为你呈现一个IT界的重大事件及人物。

注册于 2015-03-20
个人主页被 28k 人浏览