普通视图

发现新文章,点击刷新页面。
昨天以前Oragekk's Blog

一个 waline 评论系统bug引发的思考

2024年5月23日 08:00

提示

前言: Waline 评论系统这个 bug 有几个月了,现象就是回复完其他人的评论后,评论内容会保留在顶部输入框中,而且不会自动清空。具体见 👉GitHub issuse #2173

许久未见修复,正好我有空,就看看这个问题,接下来跟我一起分析一下这个问题

1. 定位问题

问题现象上边已经描述过了,我们先来定位一下问题。

1.1. 关键

关键在于【不会清空】,即使刷新浏览器,也不会清空,由此可知必然保存在 localStorage 中,打开开发者工具,看看保存在 localStorage 中的数据结构。发现如下图所示

localStorage
localStorage

然后有了key就好办了

1.2. 源码

在 Waline 评论系统源码中,我们全局搜索 WALINE_COMMENT_BOX_EDITOR 结果如下

// walinejs/packages/client/src/composables/inputs.ts
import type { RemovableRef } from '@vueuse/core';
import { useStorage } from '@vueuse/core';

export interface UserMeta {
    nick: string;
    mail: string;
    link: string;
}

export const useUserMeta = (): RemovableRef<UserMeta> =>
useStorage<UserMeta>('WALINE_USER_META', {
    nick: '',
    mail: '',
    link: '',
});

export const useEditor = (): RemovableRef<string> =>
useStorage<string>('WALINE_COMMENT_BOX_EDITOR', '');

继续查找调用,找到CommentBox.vue关键组件,该组件将textarea评论框v-modeluseEditor函数绑定,该函数返回一个RemovableRef,该类型为<string>

const editor = useEditor();
<textarea
  id="wl-edit"
  ref="editorRef"
  v-model="editor"
  class="wl-editor"
  :placeholder="replyUser ? `@${replyUser}` : locale.placeholder"
  @keydown="onKeyDown"
  @drop="onDrop"
  @paste="onPaste"
/>

2. 调试

2.1. 准备工作

  1. 已锁定问题文件为CommentBox.vue,接下来开始 debug,因为准备提 PR,所以先 fork 一份

  2. 按照waline-贡献指南进行准备

  3. 先执行pnpm i & pnpm build,本地调试依赖@waline/api 需要前置 build

  4. 使用 pnpm client:dev 启动 @waline/client 本地开发,由于 waline 是 Client/Server 架构,在调试 client 时,你需要设置 SERVERURL 为调试服务器(可以直接使用 vercel 的服务器),或同时启动下面的 server 开发服务器并使用默认的 localhost:9090

  5. 使用 pnpm server:dev 启动 @waline/server 本地开发,配置必要的本地环境变量至 example/.env。(这里我配置了 leancloud 的环境变量,一直在报错)

2.2. 关键函数 CommentBox.vuesubmitCommentwatch

 const submitComment = async (): Promise<void> => {

     // 此处...省略若干

     try {
         if (recaptchaV3Key)
         comment.recaptchaV3 =
             await useReCaptcha(recaptchaV3Key).execute('social');

         if (turnstileKey)
         comment.turnstile = await useTurnstile(turnstileKey).execute('social');

         const options = {
         serverURL,
         lang,
         token: userInfo.value?.token,
         comment,
         };

         const response = await (props.edit
         ? updateComment({
             objectId: props.edit.objectId,
             ...options,
         })
         : addComment(options));

         isSubmitting.value = false;

         if (response.errmsg) return alert(response.errmsg);

         emit('submit', response.data!);

         editor.value = '';

         previewText.value = '';

         if (props.replyId) emit('cancelReply');
         if (props.edit?.objectId) emit('cancelEdit');
     } catch (err: unknown) {
         isSubmitting.value = false;

         alert((err as TypeError).message);
     }
 };

接下来看 watch

watch(
  () => editor.value,
  (value) => {
    const { highlighter, texRenderer } = config.value;
    content.value = value;
    previewText.value = parseMarkdown(value, {
      emojiMap: emoji.value.map,
      highlighter,
      texRenderer,
    });
    wordNumber.value = getWordNumber(value);

    if (value) autosize(editorRef.value!);
    else autosize.destroy(editorRef.value!);
  },
  {
    immediate: true,
  }
);

2.3. 断点测试,整体流程如下

  1. 监听editor.value,并将editortextarea绑定,用户输入的值自动保存在localStorage

  2. 点击提交按钮,将textarea中的值发送给后端,收到回调后将editor清空,如果抛出异常则不清空

  3. 正常提交评论无异常,在submitComment函数中的editor.value = '';localStorage存储的内容清空

  4. 回复他人评论时,前置流程提交和步骤 3 一致,然后触发了watch,此时watch监听到的value为未被清空的值,既之前用户输入的内容

  5. submitComment中的editor.value = '';watch都打上了断点,先赋值为空,后触发watch,而此时watch的 value 为被清空之前的值,即用户输入的内容

2.4. 分析

问题已经定位到代码级别,接下来只需要找到 watch 被触发的原因即可,初步猜测可能原因:

submitComment是异步函数,其内部赋空值后,editor.value=''没有及时更新,导致watch触发取到旧值

遂添加 watch 的 bebug 函数加以验证

  1. onTrack 将在响应属性或引用作为依赖项被跟踪时被调用。相当于 get
  2. onTrigger 将在侦听器回调被依赖项的变更触发时被调用。相当于 set
watch(
  () => editor.value,
  (value) => {
    const { highlighter, texRenderer } = config.value;
    content.value = value;
    previewText.value = parseMarkdown(value, {
      emojiMap: emoji.value.map,
      highlighter,
      texRenderer,
    });
    wordNumber.value = getWordNumber(value);

    if (value) autosize(editorRef.value!);
    else autosize.destroy(editorRef.value!);
  },
  {
    immediate: true,
    onTrack(e) {
      // 当 editor.value 被追踪为依赖时触发
      debugger;
    },
    onTrigger(e) {
      // 当 editor.value 被更改时触发
      debugger;
    },
  }
);

结果为:

  1. editor.value=''时,onTrigger触发,newValue 为''
  2. 紧接着触发onTrackvalue 为旧值

调整回调的触发时机试试看:

  • Post Watchers

    如果想在侦听器回调中能访问被 Vue 更新之后的所属组件的 DOM,你需要指明 flush: 'post' 选项:

  • 同步侦听器

    它会在 Vue 进行任何更新之前触发:

    注意

    同步侦听器不会进行批处理,每当检测到响应式数据发生变化时就会触发。可以使用它来监视简单的布尔值,但应避免在可能多次同步修改的数据源 (如数组) 上使用。

结果是没有作用,但在更改为 sync 后,发现了一件有趣的事情,因为 sync 不会进行批处理的特性,所以触发了两次onTrack,我们来看一下两次的堆栈信息

第一次
第一次
第二次
第二次

很明显第一次触发是editor.value=''时触发的,第二次触发异步的,在submitComment还未执行完成时就触发了,点击堆栈信息定位到了291行,很明显早于editor.value=''297行。所以取到的是旧的值,虽然断点是editor.value=''先执行,但submitComment是异步的,watch 取到的是旧值,那接下来就需要看 watch是被什么触发了

由于源码中的watch是写在onMounted中的,断点确定二次走到watch中时,是触发了onMounted,那触发onMounted是组件重新加载了,查找后得知,整体逻辑是,评论区域,代码结构如下:

  • 组件为WalineComment.vue,其中包含了评论列表组件 itemCommentCard.vue和顶部默认输入框CommentBox.vue

  • 针对文章发布评论使用的是顶部的CommentBox.vue组件

  • 针对评论回复时,使用的是CommentCard.vue-CommentBox.vue

  • 回复完成后CommentCard.vue-CommentBox.vue销毁,顶部的CommentBox.vue组件重新渲染

顶部的CommentBox.vue组件重新渲染会触发 watch,此时取值是旧值, 经查看,submitComment中的editor.value=''执行完后, localStorage 中的值并未立即修改,所以重新渲染的顶部CommentBox.vue组件在初始化时取到的值仍为localStorage中的旧值。

3. 验证

上边已经基本确定问题出在这句上

editor.value = "";
const editor = useEditor();

而在 1.2 中可以看到useEditor()@vueuse/core的导出函数,怀疑其内部实现有一些异步操作,导致的没有立即更新localStorage

// 更换`editor.value`为
localStorage.setItem('WALINE_COMMENT_BOX_EDITOR', '');
// 或
await nextTick()

3.1. 至此问题解决

相关信息

nextTick()

等待下一次 DOM 更新刷新的工具方法。

  • 类型
function nextTick(callback?: () => void): Promise<void>
  • 详细信息

当你在 Vue 中更改响应式状态时,最终的 DOM 更新并不是同步生效的,而是由 Vue 将它们缓存在一个队列中,直到下一个“tick”才一起执行。这样是为了确保每个组件无论发生多少状态改变,都仅执行一次更新。

nextTick() 可以在状态改变后立即使用,以等待 DOM 更新完成。你可以传递一个回调函数作为参数,或者 await 返回的 Promise。

4. 思考

vueusejs的文档中,其实useStorage应该是一个同步操作,它还有一个useStorageAsync的API,支持异步的响应式Storage,按理说里边不应该包含太多异步的或者延时性的代码,我也找到源码浅浅看了一下,目前还没找到问题在哪里,按照替换localStorage.setItem('WALINE_COMMENT_BOX_EDITOR', '');可行来看,问题就在useStorage身上无疑,后续有时间,会继续探索一下,给官方的PR#2524也已经提了

Jenkins 远程触发构建踩坑记

2024年1月12日 08:00

提示

如果想在代码 Push 后,或者 Merge request 后,自动部署,可以采用多种方案,以下介绍两种

不知道如何配置的同学,可以参考一下

1. 通过 Jenkins 提供的【触发远程构建】

1.1. 勾选【触发远程构建】并填入 token

勾选远程构建开关
勾选远程构建开关

1.2. 配置 API token

  1. 打开 Jenkins 控制台。

  2. 在顶部导航栏中,点击您的用户名,然后选择 "Configure" 选项。

  3. 在配置页面中,向下滚动,找到 "API Token" 部分。

  4. 如果您之前没有生成过 API Token,则点击 "Add new Token" 或 "Generate Token" 按钮。

  5. 在生成或更改 Token 的过程中,您可能需要提供您的 Jenkins 用户密码进行身份验证。

  6. 生成或更改成功后,您将看到新生成的 Token 值。请将其复制并妥善保存,因为在以后的访问中,您将无法再查看该 Token 的值。

1.3. 如何调用 Url

Jenkins 提供了便捷的远程触发功能,但是需要配置一个 token,然后在 push 后,通过 post 请求,调用 Jenkins 的 url 即可触发构建

调用 url 可以使用 python 脚本,或者 curl 命令,一般配合 git 提交,使用 curl 命令居多

下面以 GitLab CI/CD 为例进行举例

GitLab CI/CD 是一个简洁好用的的持续集成/持续交付的框架。通过为你的项目配置一个或者多个 GitLab Runner,然后撰写一个 .gitlab-ci.yml,你就可以很方便地利用 GitLab CI/CD 来为你的项目引入持续集成/交付的功能。比较类似之前介绍过的GitHubAction

GitLab CI/CD 是通过 GitLab Runner 来执行的

GitLab CI/CD 将按照 Stage 定义的顺序来执行,任何一个 Stage 失败,整个 CI/CD 将失败

每一个 Stage 可以被若干个 Job 关联。Stage 在执行的时候,关联到这个 Stage 的所有 Job 都将被执行,不过不同的 Job 可能是并行执行的。

每个 Job 在执行的时候,会先按照缓存策略加载缓存数据,然后按照顺序依次运行 before_script、script 和 after_script 中配置的脚本,运行完毕以后,会将生成的数据保存到缓存中。

1.4. 编写.gitlab-ci.yml

stages:
  - deploy

deploy_to_hyjk_open-x:
  stage: deploy
  only:
    - master
  script:
    - ./deploy.sh

大意是当 master 分支 deploy 时执行 ./deploy.sh 脚本
这里 script 调用了一个 shell 脚本,因为在 yml 文件中写脚本比较麻烦,个人比较习惯在 sh 文件中写脚本,然后在 yml 文件中调用这个脚本即可

1.5. deploy.sh

由于新版本的 Jenkins 安全机制,每次调用 API 前都需要先调用获取获取 CSRF crumb 值的接口

下面脚本演示了,如何先获取 crumb 的值,并在之后触发远程构建 API 的时候,携带这个值

#!/bin/bash

# Jenkins 服务器信息
JENKINS_URL="http://192.168.155.57:8080/"
JENKINS_USERNAME="admin"
JENKINS_API_TOKEN="11abf441f4db6e341ae65b660f74de6e02"

# 获取 CSRF crumb 值
CRUMB=$(curl -s -u "$JENKINS_USERNAME:$JENKINS_API_TOKEN" "$JENKINS_URL/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,\":\",//crumb)")

# 提取 crumb 值
CRUMB_VALUE=$(echo "$CRUMB" | awk -F':' '{print $2}')

# 执行 Jenkins 远程构建
curl -X POST "$JENKINS_URL/job/projectName/build?token=xZ7Y3n9A" \
  --user "$JENKINS_USERNAME:$JENKINS_API_TOKEN" \
  --header "Jenkins-Crumb: $CRUMB_VALUE"

2. 通过 GitLab 的 webhook 触发远程构建

使用 gitlab 的 webhook 配合 Jenkins 实现自动化部署。
Jenkins 需要安装 GitLab Plugin 插件,然后配置 webhook 即可。

2.1. 勾选插件选项

勾选插件选项
勾选插件选项

2.2. 选择配置

插件有很多配置项,这里我们常用的就是过滤分支,可以选择,有以下选项

  • Include
  • Exclude
  • 正则表达式
  • tag
设置token
设置token

2.3. 将生成的 url 和 token 填入 GitLab 中

可以选择触发的 events
填入GitLab

2.4. 查看运行结果

查看运行结果
查看运行结果

Vercel deploy忽略指定分支

2023年3月31日 08:00

一招教你解决Vercel deploy时总是任意分支都可以触发,如果你的项目有多个分支,可以指定某一个或几个有提交时触发deploy

1. 前言

提示

这不是刚把博客部署从GitHub Pages转移到了Vercel上,稍微加快了一点国内的访问速度,然后发现了一个新问题~

原先是推送到main分之,通过Github Action 工作流进行部署到gh-page 分支上

转移到Vercel之后,工作流我并没有删掉,所以流程还是一样,就导致main提交了

Git SSH 密钥配置

2024年1月11日 08:00

在 GitLab 中配置 SSH 密钥

生成 SSH 密钥对

如果您还没有 SSH 密钥对,请首先生成一对密钥。在终端中执行以下命令:

ssh-keygen -t rsa -C "your.email@example.com" -b 4096

这将生成一个 RSA 类型的密钥对,并将私钥保存在 ~/.ssh/id_rsa,公钥保存在 ~/.ssh/id_rsa.pub

复制公钥内容

打开公钥文件 ~/.ssh/id_rsa.pub,将其中的内容复制到剪贴板。您可以使用以下命令来完成:

macOS:

pbcopy < ~/.ssh/id_rsa.pub

GNU/Linux (requires the xclip package):

xclip -sel clip < ~/.ssh/id_rsa.pub

Windows Command Line:

type %userprofile%\.ssh\id_rsa.pub | clip

Git Bash on Windows / Windows PowerShell:

cat ~/.ssh/id_rsa.pub | clip

将您的SSH密钥添加到GitLab。

  1. 登录到 GitLab

  2. 打开您的 GitLab 帐户,在右上角点击个人头像,选择设置

  3. 在左侧导航到 SSH 密钥设置

  4. 在 "SSH Keys" 页面,粘贴之前复制的公钥内容到 "Key" 字段中,并为该密钥添加一个可识别的标题(如 "My SSH Key")。然后点击 "Add Key" 按钮。

验证 SSH 密钥

GitLab 将验证并添加您提供的 SSH 密钥。如果一切顺利,您将在页面上看到成功的消息。

测试 SSH 连接

为了确保 SSH 密钥配置正确,您可以在终端中执行以下命令进行测试:

ssh -T git@gitlab.com

如果配置正确,您将看到与 GitLab 相关的欢迎消息。
现在,您已经成功在 GitLab 中配置了 SSH 密钥。这将允许您通过 SSH 协议进行与 GitLab 之间的安全通信,例如进行代码的克隆、推送和拉取操作。

重要

我们建议您使用ssh密钥而非用户名密码登录

因为有些情况比如更换git客户端、或者某些自动化脚本,是无法使用用户名密码登录的

对于macOS用户,我强烈建议您添加以下命令到zsh的配置文件 ~/.zprofile

ssh-add --apple-use-keychain ~/.ssh/id_rsa &> /dev/null

该命令的作用是将指定的 SSH 私钥文件 id_rsa 添加到 Apple Keychain 中,并将输出静默地丢弃,以确保在终端上不会显示任何输出信息。这通常用于在登录时自动加载 SSH 密钥,并将其保存在钥匙串中,以便在需要时无需再次手动输入密码或密钥口令。

初识Rust

2023年12月8日 01:14

Rust 发展历程

Rust 最早是 Mozilla 雇员 Graydon Hoare 的个人项目。从 2009 年开始,得到了 Mozilla 研究院的资助,2010 年项目对外公布,2010 ~ 2011 年间实现自举。自此以后,Rust 在部分重构 -> 崩溃的边缘反复横跳(历程极其艰辛),终于,在 2015 年 5 月 15 日发布 1.0 版。

在紧锣密鼓的开发过程中,Rust 建立了一个强大且活跃的社区,形成一整套完善稳定的项目贡献机制(Rust 能够飞速发展,与这一点密不可分)。Rust 现在由 Rust 项目开发者社区 维护, Rust 基金会赞助支持。

大家可能疑惑 Rust 为啥用了这么久才到 1.0 版本?与之相比,Go 语言 2009 年发布,却在 2012 年仅用 3 年就发布了 1.0 版本。

  • 首先,Rust 语言特性较为复杂,所以需要全盘考虑的问题非常多;
  • 其次,Rust 当时的参与者太多,七嘴八舌的声音很多,众口难调,而 Rust 开发团队又非常重视社区的意见;
  • 最后,一旦 1.0 快速发布,那绝大部分语言特性就无法再被修改,对于有完美强迫症的 Rust 开发者团队来说,某种程度上的不完美是不可接受的。

因此,Rust 语言用了足足 6 年时间,才发布了尽善尽美的 1.0 版本。

为何又来一门新语言?

简而言之,因为还缺一门无 GC 且无需手动内存管理、性能高、工程性强、语言级安全性以及能同时得到工程派和学院派认可的语言,而 Rust 就是这样的语言。你也可以回忆下熟悉的语言,看是不是有另外一门可以同时满足这些需求:)

至于 Rust 最为人诟病的点,那也就一个:学习曲线陡峭。不过当语言生态起来后,这都不算问题。

缓解内卷

有人说 Rust 作为新语言会增加内卷,其实恰恰相反,Rust 可以缓解内卷。为何不说 C++ 内卷,而说 Java、Python、JS 内卷?不就是后几个相对简单、上手容易嘛?而 Rust 怎么看也是 C++ 级别的上手难度。

其实从我内心不可告人的角度出发,并不希望 Rust 大众化,因为这样可以保饭碗、保薪资,还能拥有行业内的地位。但是从对 Rust 的喜爱角度出发,我还是希望能卷一些。不过,目前来看真的卷不动,现在全世界范围内 Rust 的需求都大于供给,特别是优秀的 Rust 程序员更是难寻。

与 Go 语言相比,成为一名优秀的 Rust 程序员所需的门槛高得多,例如融汇贯通 Rust 语言各种中高级特性、闭着眼睛趟过各种坑、不用回忆无需查找就能立刻写出最合适的包/模块/方法、性能/安全/工程性的权衡选择信手拈来、深层性能优化易如反掌、异步编程小菜一碟,更别说 Rust 之外的操作系统、网络、算法等等相关知识。

所以,Rust 可以缓解内卷,而不是增加内卷。可以说是程序员的福音,不再是被随意替换的螺丝钉。

运行效率

得益于各种零开销抽象、深入到底层的优化潜力、优质的标准库和第三方库实现,Rust 具备非常优秀的性能,和 C、C++ 是 一个级别

同时 Rust 有一个极大的优点:只要按照正确的方式使用 Rust,无需性能优化,就能有非常优秀的表现,不可谓不惊艳。

现在有不少用 Rust 重写的工具、平台都超过了原来用 C、C++ 实现的版本,将老前辈拍死在沙滩上,俨然成为一种潮流~~

开发效率

Rust 的开发效率可以用先抑后扬来形容。在最初上手写项目时,你的开发速度将显著慢于 Go、Java 等语言,不过,一旦开始熟悉标准库、熟悉生命周期和所有权的常用解决方法,开发效率将大幅提升,甚至当形成肌肉记忆后,开发效率将不会慢于这些语言,而且原生就能写出高质量、安全、高效的代码,可以说中高级 Rust 程序员就是高效程序员的代名词。

开源

目前 Rust 的主战场是在开源上,Go 的成功也证明了农村包围城市( 开源包围商业 )的可行性。

  • UI 层开发,Rust 的 WASM 发展的如火如荼,隐隐有王者风范,在 JS 的基础设施领域,Rust 也是如鱼得水,例如 swcdeno 等。同时 nextjs 也是押宝 Rust,可以说 Rust 在前端的成功完全是无心插柳柳成荫。
  • 基础设施层、数据库、搜索引擎、网络设施、云原生等都在出现 Rust 的身影,而且还不少。
  • 系统开发,目前 Linux 已经将 Rust 语言纳入内核,是继 C 语言后第二门支持内核开发的语言,不过刚开始将主要支持驱动开发。
  • 系统工具,现在最流行的就是用 Rust 重写之前 C、C++ 写的一票系统工具,还都获得了挺高的关注和很好的效果,例如 sd, exa, ripgrep, fd, bat 等。
  • 操作系统,正在使用 Rust 开发的操作系统有好几个,其中最有名的可能就是谷歌的 Fuchsia,Rust 在其中扮演非常重要的角色。
  • 区块链,如果 Rust 的份额说第二,应该没人敢稳说自己是第一吧?

类似的还有很多,我们就不一一列举。总之,现在有大量的项目正在被 Rust 重写,同时还有海量的项目在等待被重写,这些都是赚取github 星星和认可的好机会。在其它语言杀成一片红海时,Rust 还留了一大片蓝海等待大家的探索!

相比其他语言 Rust 的优势

由于篇幅有限,我们这里不会讲述详细的对比,就是简单介绍下 Rust 的优势,并不是说 Rust 优于这些语言,大家轻喷:)

Go

Rust 语言表达能力更强,性能更高。同时线程安全方面 Rust 也更强,不容易写出错误的代码。包管理 Rust 也更好,Go 虽然在 1.10 版本后提供了包管理,但是目前还比不上 Rust 。

C++

Rust 与 C++ 的性能旗鼓相当,但是在安全性方面 Rust 会更优,特别是使用第三方库时,Rust 的严格要求会让三方库的质量明显高很多。

语言本身的学习,Rust 的前中期学习曲线会更陡峭,但是在实际的项目开发过程中,C++ 会更难,代码也更难以维护。

Java

除了极少数纯粹的数字计算性能,Rust 的性能全面领先于 Java 。同时 Rust 占用内存小的多,因此实现同等规模的服务,Rust 所需的硬件成本会显著降低。

Python

性能自然是 Rust 完胜,同时 Rust 对运行环境要求较低,这两点差不多就足够抉择了。不过 Python 和 Rust 的彼此适用面其实也不太冲突。

使用现状

  • AWS 从 2017 年开始就用 Rust 实现了无服务器计算平台: AWS Lambda 和 AWS Fargate,并且用 Rust 重写了 Bottlerocket OS 和 AWS Nitro 系统,这两个是弹性计算云 (EC2) 的重要服务
  • Cloudflare 是 Rust 的重度用户,DNS、无服务计算、网络包监控等基础设施都与 Rust 密不可分
  • Dropbox 的底层存储服务完全由 Rust 重写,达到了数万 PB 的规模
  • Google 除了在安卓系统的部分模块中使用 Rust 外,还在它最新的操作系统 Fuchsia 中重度使用 Rust
  • Facebook 使用 Rust 来增强自己的网页端、移动端和 API 服务的性能,同时还写了 Hack 编程语言的虚拟机
  • Microsoft 使用 Rust 为 Azure 平台提供一些组件,其中包括 IoT 的核心服务
  • GitHub 和 npmjs.com,使用 Rust 提供高达每天 13 亿次的 npm 包下载
  • Rust 目前已经成为全世界区块链平台的首选开发语言
  • TiDB,国内最有名的开源分布式数据库

尤其值得一提的是,AWS 实际上在押宝 Rust,内部对 Rust 的使用已经上升到头等公民 first-class 的地位。

Rust 语言版本更新

与其它语言相比,Rust 的更新迭代较为频繁(得益于精心设计过的发布流程以及 Rust 语言开发者团队的严格管理):

  • 每 6 周发布一个迭代版本
  • 2-3 年发布一个新的大版本,例如 Rust 2018 edition,Rust 2021 edition

好处在于,可以满足不同的用户群体的需求:

  • 对于活跃的 Rust 用户,他们总是能很快获取到新的语言内容,毕竟,尝鲜是技术爱好者的共同特点:)
  • 对于一般的用户,edition 大版本的发布会告诉他们:Rust 语言相比上次大版本发布,有了重大的改进,值得一看
  • 对于 Rust 语言开发者,可以让他们的工作成果更快的被世人所知,不必锦衣夜行

总结

连续 6 年最受欢迎的语言当然不是浪得虚名。 无 GC、效率高、工程性强、强安全性以及能同时得到工程派和学院派认可,这些令 Rust 拥有了自己的特色和生存空间。社区的友善,生态的快速发展,大公司的重仓跟进,一切的一切都在说明 Rust 的璀璨未来。

当然,语言毕竟只是工具,我们不能神话它,但是可以给它一个机会,也许,你最终能收获自己的真爱 :)

相信大家听了这么多 Rust 的优点,已经迫不及待想要开始学习旅程,那么容我引用一句 CS(Counter-Strike:反恐精英) 的经典台词:Ok, Let's Rust.

引自:Rust语言圣经(Rust Course)

vuepress-plugin-meting2

2023年10月11日 08:00

前言

嘿~
🍰🍰🍰 播放器有了,撒花✿✿ヽ(°▽°)ノ✿🎉🎉🎉

npm  GitHub stars  downloads  downloads  GitHub license

文档👉🏻戳这里文档

介绍

借鉴了vuepress-plugin-sbaudiovuepress-plugin-meting 在此表示感谢

借鉴MetingJS解析和使用APlayer作为播放组件

安装很方便

使用也很方便

plugins: [
    metingPlugin({
      metingOptions: {
        global:true, // 开启关闭全局播放器
        server: "tencent",
        api: "https://api.injahow.cn/meting/?server=:server&type=:type&id=:id&auth=:auth&r=:r",
        type: "playlist",
        mid: "851947617",
      },
    }),
];

可作为组件引入

谷歌发布多平台应用开发神器Project IDX!PaLM 2加持

2023年8月15日 08:00

8 月 8 日,谷歌宣布推出 AI 代码编辑器 Project IDX,这是一个基于浏览器的开发环境:集成 AI、支持全栈编程语言、跨平台真机预览、一键部署,用于构建全栈网络和多平台应用程序。

一直以来,从 0 开始构建应用,都是一项复杂的工作。尤其是跨越手机、Web 和桌面平台的程序。

这是一片无尽的复杂海洋,需要把技术堆栈融合在一起,来引导、编译、测试、部署、监控应用程序。

多年来,谷歌一直致力于让多平台程序开发流程更快、更顺畅。

经过几个月的成果,团队成功做出了 Project IDX。

谷歌在创建 Project IDX 时并没有构建新的 IDE(集成开发环境),而是使用 VS Code 作为其项目的基础。这让团队能够专注于与 Codey 的集成,Codey 是谷歌基于 PaLM 2 的编程任务基础模型。Project IDX 支持智能代码补全,可以帮助开发者解答一般编码问题,提供与你正在处理的代码有关的特定问题(包括解释能力)的类似 ChatGPT/Bard 的聊天机器人,以及添加如 “添加注释” 等上下文代码操作的能力。
目前,Project IDX 支持 Angular、Flutter、Next.js、React、Svelte 和 Vue 等框架以及 JavaScript 和 Dart 等语言,后续还将支持 Python、Go 和其他语言。

Project IDX 特性

  • 随时随地快速开始:你能够在任何地方、任何设备上进行开发,具有本地开发的全部保真度。每个 Project IDX 工作空间都具有基于 Linux 的虚拟机的全部功能,配合云中托管的通用访问权限。
  • 一键导入或从模板创建:从 GitHub 导入现有项目,让你继续之前的进度。还可以创建新项目,预先包含流行框架的模板,包括 Angular、Flutter、Next.js、React、Svelte、Vue 和如 JavaScript、Dart 以及(即将推出的)Python、Go 等语言。IDX 还在积极努力为更多项目类型和框架添加一流支持。
  • 跨平台预览:应用成功发布后,需要优化设计和不同平台上的行为(像用户那样预览自己的应用),而 IDX 内置的网络预览(目前只支持 web 预览),和即将推出的完全配置的 Android 模拟器和嵌入式 iOS 模拟器,所有这些都可以直接在浏览器中使用。
  • AI 助手:我们花费大量时间编写代码,而人工智能的最新进展创造了巨大的机会,可以让我们的时间变得更加高效。IDX 正在探索 Google 在 AI 领域的创新 —— 包括 为 Android Studio 中的 Studio Bot 提供支持的 Codey 和 PaLM 2 模型、Google Cloud 中的 Duet [4] 等 —— 可以帮助你不仅更快地编写代码,还可以编写更高质量的代码。
  • 一键部署:在最后,将应用推向生产方面的一个常见痛点是部署它。通过集成 Firebase Hosting,通过几次点击就可以部署你的 Web 应用的可共享预览,或通过快速、安全和全球托管平台部署到生产环境。由于 Firebase Hosting 支持由 Cloud Functions 提供动力的动态后端,所以它非常适合像 Next.js 这样的全栈框架。

  • IDX 申请链接https://idx.dev/

IDX 背后的 AI 模型 Codey

根据介绍,IDX 由 Codey 提供支持。

在 Google I/O 2023 大会上,谷歌正式发布 Codey。这是一款新型 AI 驱动工具,能够编写并理解代码内容。这款新工具被外界视为谷歌对于 GitHub Copilot 的回应,属于同 Replit 结盟打造的成果。

Codey 基于谷歌的下一代大语言模型 PaLM 2,并采用谷歌自家产品代码及大量合法许可的源代码作为训练素材。更重要的是,Codey 仍在不断学习和发展,从谷歌服务生态系统的各个项目中持续汲取新的力量。

Codey 支持 20 多种编程语言,包括 Go、谷歌标准 SQL、Java、JavaScript、Python 以及 TypeScript。开发者可以通过 Visual Studio Code、JetBrains IDE、Google Shell 编辑器以及 Google Cloud 托管工作站服务的扩展来访问 Codey。开发者能够直接在 IDE 的聊天框中与该模型交流(例如 Android Studio Bot),或者在文本文件中编写注释以指示其生成相关代码。它支持各种编码任务,通过以下方式帮助开发人员更快地工作并缩小技能差距:

代码完成:Codey 根据提示中输入的代码上下文建议接下来的几行。

代码生成:小程根据开发人员的自然语言提示生成代码。

代码聊天:Codey 允许开发人员与机器人对话,以获得调试、文档、学习新概念和其他与代码相关问题的帮助。

Codey 在处理与编码相关的提示词方面接受了专门训练,谷歌还通过其他训练让该模型学会了处理关于 Google Cloud 的一般查询。

目前在用的辅助AI开发工具

- CodeGeeX 智能编程助手

  • 代码自动生成和补全 CodeGeeX可以根据自然语言注释描述的功能自动生成代码,也可以根据已有的代码自动生成后续代码,补全当前行或生成后续若干行,帮助你提高编程效率。
  • 提供代码翻译能力 基于AI大模型对代码进行语义级翻译,支持多种编程语言互译
  • 自动添加注释 CodeGeeX可以给代码自动添加行级注释,节省大量开发时间。没有注释的历史代码,也不再是问题。

基于CodeGeeX2: 更强大的多语言代码生成模型
是多语言代码生成模型 CodeGeeX (KDD’23) 的第二代模型。不同于一代 CodeGeeX(完全在国产华为昇腾芯片平台训练) ,CodeGeeX2 是基于 ChatGLM2 架构加入代码预训练实现,得益于 ChatGLM2 的更优性能,CodeGeeX2 在多项指标上取得性能提升(+107% > CodeGeeX;仅 60 亿参数即超过 150 亿参数的 StarCoder-15B 近 10%),更多特性包括:

  • 更强大的代码能力:基于 ChatGLM2-6B 基座语言模型,CodeGeeX2-6B 进一步经过了 600B 代码数据预训练,相比一代模型,在代码能力上全面提升,HumanEval-X 评测集的六种编程语言均大幅提升 (Python +57%, C++ +71%, Java +54%, JavaScript +83%, Go +56%, Rust +321%),在 Python 上达到 35.9% 的 Pass@1 一次通过率,超越规模更大的 StarCoder-15B。
  • 更优秀的模型特性:继承 ChatGLM2-6B 模型特性,CodeGeeX2-6B 更好支持中英文输入,支持最大 8192 序列长度,推理速度较一代 CodeGeeX-13B 大幅提升,量化后仅需 6GB 显存即可运行,支持轻量级本地化部署。
    更全面的 AI 编程助手:CodeGeeX 插件(VS Code, Jetbrains)后端升级,支持超过 100 种编程语言,新增上下文补全、跨文件补全等实用功能。结合 Ask CodeGeeX 交互式 AI 编程助手,支持中英文对话解决各种编程问题,包括且不限于代码解释、代码翻译、代码纠错、文档生成等,帮助程序员更高效开发。
  • 更开放的协议:CodeGeeX2-6B 权重对学术研究完全开放,填写登记表申请商业使用。

- AWS CodeWhisperer

4月18日,亚马逊云科技宣布,实时AI编程助手Amazon CodeWhisperer正式可用,同时推出的还有供所有开发人员免费使用的个人版(CodeWhisperer Individual)。CodeWhisperer帮助开发者基于注释生成代码,追踪开源参考,扫描查找漏洞。此外,还可以帮助开发者创建代码胜任如下场景,比如常规、耗时的无差别任务,或是在使用不熟悉的API或SDK时构建示例代码,亦或要正确高效地使用亚马逊云科技API操作,还有其他场景比如编写读写文件、处理图像、编写单元测试等代码。

Amazon CodeWhisperer 为多种编程语言提供基于人工智能(AI)的代码建议,包括 Python、Java、JavaScript、TypeScript、C#、Go、Rust、PHP、Ruby、Kotlin、C、C++、Shell 脚本、SQL 和 Scala。您可以使用来自多个 IDE 的服务,包括 JetBrains IDE(IntelliJ IDEA、PyCharm、WebStorm 和 Rider)、Visual Studio(VS)Code、AWS Cloud9、AWS Lambda 控制台、JupyterLab 和 Amazon SageMaker Studio。

- Poe

国内可注册,使用方便,免费,有针对代码和程序专门优化的gpt-3.5-turbo模型 Assistant
当然还有其他的模型,包含一些限额和付费的,你也可以创建自己的bot

  • Claude-2-100k
  • Claude-instant-100k
  • gpt-4-32k(Powered by gpt-4-32k. Since this is a beta model, the usage limit is subject to change.)
  • Google-PaLM(Powered by Google's PaLM 2 chat-bison-001 model)

CodeGeeX2AWS CodeWhisperer都提供了VSCode的插件可以使用,体验下来,CodeGeeX2支持的文件类型更多一点,而且反应也还算迅速,AWS CodeWhisperer只支持了主流开发语言,对于如dart、Vue、markdown、CSS等非主流编程语言支持效果很差,目前两者都免费,体验下来CodeGeeX2更不错一点,根据注释生成代码,给现有代码生成注释,代码翻译,纠错等功能也比较实用

Some thoughts🤔

看来云上开发将来一定是主流趋势,程序员再也不用因为换设备,换环境而苦恼,集成式一体开发环境,开箱即用,开发随时可中断,可恢复,不受限于设备,环境,地点影响
而且集成AI代码助手,各大厂家都在发力了,微软的GitHub Copilot,亚马逊的CodeWhisperer,还有即将到来的Google的Project IDX 方便快捷的开发,将大量繁琐重复的工作交给AI来执行

That's It

参考资料:

Dart 中的并发

2023年4月26日 08:00

Dart 通过 async-await、isolate 以及一些异步类型概念(例如 FutureStream)支持了并发代码编程。本篇文章会对 async-await、FutureStream 进行简略的介绍,而侧重点放在 isolate 的讲解上。

在应用中,所有的 Dart 代码都在 isolate 中运行。每一个 Dart 的 isolate 都有独立的运行线程,它们无法与其他 isolate 共享可变对象。在需要进行通信的场景里,isolate 会使用消息机制。很多 Dart 应用都只使用一个 isolate,也就是 main isolate。你可以创建额外的 isolate 以便在多个处理器核心上执行并行代码。

尽管 Dart 的 isolate 模型设计是基于操作系统提供的进程和线程等更为底层的原语进行设计的, Dart 虚拟机对其的使用是一个具体的实现,在本篇文章中,我们不对其具体实现展开讨论。

异步的类型和语法

如果你已经对 FutureStream 和 async-await 比较熟悉了,可以直接跳到 isolate 部分 进行阅读。

Future 和 Stream 类型

Dart 语言和库通过 FutureStream 对象,来提供会在当前调用的未来返回某些值的功能。以 JavaScript 中的 Promise 为例,在 Dart 中一个最终会返回 int 类型值的 promise,应当声明为 Future<int>;一个会持续返回一系列 int 类型值的 promise,应当声明为 Stream<int>

让我们用 dart:io 来举另外一个例子。File 的同步方法 readAsStringSync() 会以同步调用的方式读取文件,在读取完成或者抛出错误前保持阻塞。这个会返回 String 类型的对象,或者抛出异常。而与它等效的异步方法 readAsString(),会在调用时立刻返回 Future<String> 类型的对象。在未来的某一刻,Future<String> 会结束,并返回一个字符串或错误。

为什么异步的代码如此重要?

It matters whether a method is synchronous or asynchronous because most apps need to do more than one thing at a time.

大部分应用需要在同一时刻做很多件事。例如,应用可能会发起一个 HTTP 请求,同时在请求返回前对用户的操作做出不同的界面更新。异步的代码会有助于应用保持更高的可交互状态。

异步场景包括调用系统 API,例如非阻塞的 I/O 操作、HTTP 请求或与浏览器交互。还有一些场景是利用 Dart 的 isolate 进行计算,或等待一个计时器的触发。这些场景要么是在不同的线程运行,要么是被系统或 Dart 运行时处理,让 Dart 代码可以在计算时同步运行。

async-await 语法

asyncawait 关键字是用声明来定义异步函数和获取它们的结果的方式。

下面是一段同步代码调用文件 I/O 时阻塞的例子:

void main() {
  // Read some data.
  final fileData = _readFileSync();
  final jsonData = jsonDecode(fileData);

  // Use that data.
  print('Number of JSON keys: ${jsonData.length}');
}

String _readFileSync() {
  final file = File(filename);
  final contents = file.readAsStringSync();
  return contents.trim();
}

下面是类似的代码,但是变成了 异步调用

void main() async {
  // Read some data.
  final fileData = await _readFileAsync();
  final jsonData = jsonDecode(fileData);

  // Use that data.
  print('Number of JSON keys: ${jsonData.length}');
}

Future<String> _readFileAsync() async {
  final file = File(filename);
  final contents = await file.readAsString();
  return contents.trim();
}

main() 函数在调用 _readFileAsync() 前使用了 await 关键字,让原生代码(文件 I/O)执行的同时,其他的 Dart 代码(例如事件处理器)能继续执行。使用 await 后,_readFileAsync() 调用返回的 Future<String> 类型也转换为了 String。从而在将结果 content 赋予变量时,隐式转换为 String 类型。

备注:

await 关键字仅在函数体前定义了 async 的函数中有效。

如下图所示,无论是在 Dart VM 还是在系统中, Dart 代码都会在 readAsString() 执行非 Dart 代码时暂停。在 readAsString() 返回值后,Dart 代码将继续执行。

类似于流程图的图形显示了应用程序代码从开始到退出的执行过程,在这之间等待本地I/O
类似于流程图的图形显示了应用程序代码从开始到退出的执行过程,在这之间等待本地I/O

如果你想了解更多关于 asyncawaitFuture 的内容,可以访问 异步编程 codelab 进行学习。

Isolate 的工作原理

现代的设备通常会使用多核 CPU。开发者为了让程序在设备上有更好的表现,有时会使用共享内容的线程来并发运行代码。然而,状态的共享可能会 产生竞态条件,从而造成错误,也可能会增加代码的复杂度。

Dart 代码并不在多个线程上运行,取而代之的是它们会在 isolate 内运行。每一个 isolate 会有自己的堆内存,从而确保 isolate 之间互相隔离,无法互相访问状态。由于这样的实现并不会共享内存,所以你也不需要担心 互斥锁和其他锁

在使用 isolate 时,你的 Dart 代码可以在同一时刻进行多个独立的任务,并且使用可用的处理器核心。 Isolate 与线程和进程近似,但是每个 isolate 都拥有独立的内存,以及运行事件循环的独立线程。

Platform note: Only the Dart Native platform implements isolates. To learn more about the Dart Web platform, see the Concurrency on the web section.

平台说明

只有Dart Native平台实现了隔离器。要了解更多关于Dart网络平台的信息,请参见Web的并发性部分

主 isolate

在一般场景下,你完全无需关心 isolate。通常一个 Dart 应用会在主 isolate 下执行所有代码,如下图所示:

图中显示了一个主隔离区,它运行,对事件作出反应,然后退出
图中显示了一个主隔离区,它运行main(),对事件作出反应,然后退出

就算是只有一个 isolate 的应用,只要通过使用 async-await 来处理异步操作,也完全可以流畅运行。一个拥有良好性能的应用,会在快速启动后尽快进入事件循环。这使得应用可以通过异步操作快速响应对应的事件。

Isolate 的生命周期

如下图所示,每个 isolate 都是从运行 Dart 代码开始的,比如 main() 函数。执行的 Dart 代码可能会注册一些事件监听,例如处理用户操作或文件读写。当 isolate 执行的 Dart 代码结束后,如果它还需要处理已监听的事件,那么它依旧会继续被保持。处理完所有事件后,isolate 会退出。

一个更一般的图显示,任何隔离体都会运行一些代码,选择性地对事件做出反应,然后退出
一个更一般的图显示,任何隔离体都会运行一些代码,选择性地对事件做出反应,然后退出

事件处理

在客户端应用中,主 isolate 的事件队列内,可能会包含重绘的请求、点击的通知或者其他界面事件。例如,下图展示了包含四个事件的事件队列,队列会按照先进先出的模式处理事件。

一个显示事件被逐一送入事件循环的图。
一个显示事件被逐一送入事件循环的图。

如下图所示,在 main() 方法执行完毕后,事件队列中的处理才开始,此时处理的是第一个重绘的事件。而后主 isolate 会处理点击事件,接着再处理另一个重绘事件。

显示主隔离区逐一执行事件处理程序的图
显示主隔离区逐一执行事件处理程序的图

如果某个同步执行的操作花费了很长的处理时间,应用看起来就像是失去了响应。在下图中,处理点击事件的代码比较耗时,导致紧随其后的事件并没有及时处理。这时应用可能会产生卡顿,所有的动画都无法流畅播放。

图中显示了一个执行时间过长的分接处理程序
图中显示了一个执行时间过长的分接处理程序

在一个客户端应用中,耗时过长的同步操作,通常会导致 卡顿的动画。而最糟糕的是,应用界面可能完全失去响应。

后台运行对象

如果你的应用受到耗时计算的影响而出现卡顿,例如 解析较大的 JSON 文件,你可以考虑将耗时计算转移到单独工作的 isolate,通常我们称这样的 isolate 为 后台运行对象。下图展示了一种常用场景,你可以生成一个 isolate,它将执行耗时计算的任务,并在结束后退出。这个 isolate 工作对象退出时会把结果返回。

A figure showing a main isolate and a simple worker isolate
A figure showing a main isolate and a simple worker isolate

每个 isolate 都可以通过消息通信传递一个对象,这个对象的所有内容都需要满足可传递的条件。并非所有的对象都满足传递条件,在无法满足条件时,消息发送会失败。举个例子,如果你想发送一个 List<Object>,你需要确保这个列表中所有元素都是可被传递的。假设这个列表中有一个 Socket,由于它无法被传递,所以你无法发送整个列表。

你可以查阅 send() 方法 的文档来确定哪些类型可以进行传递。

Isolate 工作对象可以进行 I/O 操作、设置定时器,以及其他各种行为。它会持有自己内存空间,与主 isolate 互相隔离。这个 isolate 在阻塞时也不会对其他 isolate 造成影响。

代码示例

本节将重点讨论使用 Isolate API 实现 isolate 的一些示例。

实现一个简单的 isolate 工作对象

这些例子实现了一个主隔离器,它生成了一个简单的工作隔离器。 Isolate.run() 简化了设置和管理工作者隔离区的步骤:

  1. 生成(启动并创建)一个隔离器
  2. 在生成的隔离体上运行一个函数
  3. 捕获结果
  4. 将结果返回给主隔离区
  5. 工作完成后,终止隔离区的运行
  6. 检查、捕获并将异常和错误抛回给主隔离区

备注

如果你使用Flutter,考虑使用Flutter的compute()函数而不是Isolate.run()compute函数允许你的代码在本地和非本地平台上工作。当只针对原生平台时,使用Isolate.run()以获得更符合人类工程学的的API。

在一个新的隔离区中运行一个现有的方法

主 isolate 的代码如下:

void main() async {
  // Read some data.
  final jsonData = await Isolate.run(_readAndParseJson);

  // Use that data.
  print('Number of JSON keys: ${jsonData.length}');
}

生成的隔离器会执行作为第一个参数传递的函数, _readAndParseJson:

Future<Map<String, dynamic>> _readAndParseJson() async {
  final fileData = await File(filename).readAsString();
  final jsonData = jsonDecode(fileData) as Map<String, dynamic>;
  return jsonData;
}
  1. Isolate.run() 产生了一个隔离器,即后台工作者, 而 main() 则在等待结果
  2. 生成的隔离器会执行传递给 run()的参数: the function _readAndParseJson().
  3. Isolate.run() 从返回中获取结果,并将该值送回主隔离区,从而关闭工作隔离区。
  4. 工作者隔离区会将保存结果的内存转移到主隔离区。它并不复制数据。工作者隔离区会执行一个验证通道,以确保对象被允许转移。

_readAndParseJson() 是一个现有的异步函数,可以很容易地直接在主隔离区中运行。使用 Isolate.run() 来运行它,从而实现了并发性。工作者隔离区完全抽象了_readAndParseJson()的计算过程。它可以在不阻塞主隔离区的情况下完成。

Isolate.run() 的结果总是一个Future,因为主隔离区的代码仍在继续运行。工作者隔离区执行的计算是同步的还是异步的,并不影响主隔离区,因为无论如何,它都是在并发地运行。

Sending closures with isolates

您也可以在主隔离区中直接使用函数字面或闭包,用run() 创建一个简单的工作隔离区。

void main() async {
  // Read some data.
  final jsonData = await Isolate.run(() async {
    final fileData = await File(filename).readAsString();
    final jsonData = jsonDecode(fileData) as Map<String, dynamic>;
    return jsonData;
  });

  // Use that data.
  print('Number of JSON keys: ${jsonData.length}');
}

这个例子的完成情况与前一个例子相同。一个新的隔离器产生了,计算了一些东西,并把结果送了回来。

不过,现在这个隔离体发送的是一个闭包。与典型的命名函数相比,闭包的限制较少,无论是在功能上还是在代码中的编写方式上。在这个例子中,Isolate.run()执行的是看起来像本地代码的东西,同时进行。在这个意义上,你可以把run()想象成一个 "并行运行 "的控制流操作符

实现一个简单的 isolate 工作对象

Isolate.run() a抽取了一些较低级别的、与隔离物相关的API,以简化隔离物管理:

您可以直接使用这些基元来对隔离区的功能进行更精细的控制。例如,run()在返回一条消息后就会关闭其隔离区。如果您想允许多个消息在隔离区之间传递,该怎么办呢?您可以用与run()的实现方式大致相同的方式来设置自己的隔离区,只是以稍微不同的方式利用SendPortsend()方法

如果你想在 isolate 之间建立更多的通信,那么你需要使用 SendPortsend() 方法。下图展示了一种常见的场景,主 isolate 会发送请求消息至 isolate 工作对象,然后它们之间会继续进行多次通信,进行请求和回复。

图中显示了主隔离器催生隔离器,然后发送请求消息,工作隔离器用回复消息进行响应;显示了两个请求-回复循环。
图中显示了主隔离器催生隔离器,然后发送请求消息,工作隔离器用回复消息进行响应;显示了两个请求-回复循环。

下方列举的 isolate 示例 包含了发送多次消息的使用方法:

  • send_and_receive.dart 展示了如何从主 isolate 发送消息至生成的 isolate,与前面的示例较为接近,不过没有使用 run() 方法;
  • long_running_isolate.dart 展示了如何生成一个长期运行、且多次发送和接收消息的 isolate。

性能和 isolate 组

当一个 isolate 调用了 Isolate.spawn(),两个 isolate 将拥有同样的执行代码,并归入同一个 isolate 组 中。 Isolate 组会带来性能优化,例如新的 isolate 会运行由 isolate 组持有的代码,即共享代码调用。同时,Isolate.exit() 仅在对应的 isolate 属于同一组时有效。

某些场景下,你可能需要使用 Isolate.spawnUri(),使用执行的 URI 生成新的 isolate,并且包含代码的副本。然而,spawnUri() 会比 spawn() 慢很多,并且新生成的 isolate 会位于新的 isolate 组。另外,当 isolate 在不同的组中,它们之间的消息传递会变得更慢。

备注

Flutter logoFlutter 不支持 Isolate.spawnUri()

在Web的并发

所有的Dart应用程序都可以使用async-awaitFutureStream进行非阻塞、交错的计算。然而,Dart web 平台并不支持隔离器。Dart网络应用程序可以使用网络工作者在后台线程中运行脚本,这与隔离程序类似。不过,web workers的功能和能力与隔离器有些不同。

例如,当Web工作者在线程之间发送数据时,他们会来回复制数据。不过,数据复制的速度可能非常慢,尤其是对于大的消息。隔离器也做同样的事情,但也提供了API,可以更有效地传输保存消息的内存。

创建Web Worker和Isolates也有不同。你只能通过声明一个单独的程序入口并单独编译来创建网络工作者。启动Web Worker类似于使用Isolate.spoonUri来启动一个隔离器。您也可以使用Isolate.spown来启动一个隔离器,这需要的资源较少,因为它重用了一些与催生隔离器相同的代码和数据。Web Worker没有一个同等的API。

如何利用GitHub Action提交URL到搜索引擎

2023年4月17日 08:00

相关信息

这是一个GitHub Actions的配置文件,整体思路是,获取本次更新的url
(通过对比两次提交的sitemap.xml文件),所以触发条件是deploy分支有推送并修改了sitemap.xml文件,再利用python脚本进行url推送到百度、Bing、Google

脚本代码请看这里👉提交URL到搜索引擎(百度、Bing、Google)

前些天不是写了Bing的提交脚本吗,但是每次都要自己手动跑脚本也很麻烦,但我又没有服务器,所以用上了GitHub Actions了嘛
总的来说做了以下事情:

  1. 提交代码并修改sitemap.xml 后触发工作流
  2. 比对上一次提交和本次提交的新增URL
  3. 将URL分别提交给百度站长、Bing、Google 供搜索引擎快速收录

流程解读

浏览器的事件循环

2023年4月6日 08:00

浏览器的进程模型

何为进程?

程序运行需要有它自己专属的内存空间,可以把这块内存空间简单的理解为进程

何为进程
何为进程

每个应用至少有一个进程,进程之间相互独立,即使要通信,也需要双方同意。

何为线程?

有了进程后,就可以运行程序的代码了。

运行代码的「人」称之为「线程」。

一个进程至少有一个线程,所以在进程开启后会自动创建一个线程来运行代码,该线程称之为主线程。

如果程序需要同时执行多块代码,主线程就会启动更多的线程来执行代码,所以一个进程中可以包含多个线程。

何为线程
何为线程

浏览器有哪些进程和线程?

浏览器是一个多进程多线程的应用程序

浏览器内部工作极其复杂。

为了避免相互影响,为了减少连环崩溃的几率,当启动浏览器后,它会自动启动多个进程。

浏览器有哪些进程和线程
浏览器有哪些进程和线程

可以在浏览器的任务管理器中查看当前的所有进程

其中,最主要的进程有:

  1. 浏览器进程

    主要负责界面显示、用户交互、子进程管理等。浏览器进程内部会启动多个线程处理不同的任务。

  2. 网络进程

    负责加载网络资源。网络进程内部会启动多个线程来处理不同的网络任务。

  3. 渲染进程(本节课重点讲解的进程)

    渲染进程启动后,会开启一个渲染主线程,主线程负责执行 HTML、CSS、JS 代码。

    默认情况下,浏览器会为每个标签页开启一个新的渲染进程,以保证不同的标签页之间不相互影响。

    将来该默认模式可能会有所改变,有兴趣的同学可参见chrome官方说明文档

渲染主线程是如何工作的?

渲染主线程是浏览器中最繁忙的线程,需要它处理的任务包括但不限于:

  • 解析 HTML
  • 解析 CSS
  • 计算样式
  • 布局
  • 处理图层
  • 每秒把页面画 60 次
  • 执行全局 JS 代码
  • 执行事件处理函数
  • 执行计时器的回调函数
  • ......

思考题:为什么渲染进程不适用多个线程来处理这些事情?

要处理这么多的任务,主线程遇到了一个前所未有的难题:如何调度任务?

比如:

  • 我正在执行一个 JS 函数,执行到一半的时候用户点击了按钮,我该立即去执行点击事件的处理函数吗?
  • 我正在执行一个 JS 函数,执行到一半的时候某个计时器到达了时间,我该立即去执行它的回调吗?
  • 浏览器进程通知我“用户点击了按钮”,与此同时,某个计时器也到达了时间,我应该处理哪一个呢?
  • ......

渲染主线程想出了一个绝妙的主意来处理这个问题:排队

排队
排队
  1. 在最开始的时候,渲染主线程会进入一个无限循环
  2. 每一次循环会检查消息队列中是否有任务存在。如果有,就取出第一个任务执行,执行完一个后进入下一次循环;如果没有,则进入休眠状态。
  3. 其他所有线程(包括其他进程的线程)可以随时向消息队列添加任务。新任务会加到消息队列的末尾。在添加新任务时,如果主线程是休眠状态,则会将其唤醒以继续循环拿取任务

这样一来,就可以让每个任务有条不紊的、持续的进行下去了。

整个过程,被称之为事件循环(消息循环)

若干解释

何为异步?

代码在执行过程中,会遇到一些无法立即处理的任务,比如:

  • 计时完成后需要执行的任务 —— setTimeoutsetInterval
  • 网络通信完成后需要执行的任务 -- XHRFetch
  • 用户操作后需要执行的任务 -- addEventListener

如果让渲染主线程等待这些任务的时机达到,就会导致主线程长期处于「阻塞」的状态,从而导致浏览器「卡死」

image-20220810104344296
image-20220810104344296

渲染主线程承担着极其重要的工作,无论如何都不能阻塞!

因此,浏览器选择异步来解决这个问题

异步
异步

使用异步的方式,渲染主线程永不阻塞

面试题:如何理解 JS 的异步?

参考答案:

JS是一门单线程的语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。

而渲染主线程承担着诸多的工作,渲染页面、执行 JS 都在其中运行。

如果使用同步的方式,就极有可能导致主线程产生阻塞,从而导致消息队列中的很多其他任务无法得到执行。这样一来,一方面会导致繁忙的主线程白白的消耗时间,另一方面导致页面无法及时更新,给用户造成卡死现象。

所以浏览器采用异步的方式来避免。具体做法是当某些任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行。

在这种异步模式下,浏览器永不阻塞,从而最大限度的保证了单线程的流畅运行。

JS为何会阻碍渲染?

先看代码

<h1>Mr.Yuan is awesome!</h1>
<button>change</button>
<script>
  var h1 = document.querySelector('h1');
  var btn = document.querySelector('button');

  // 死循环指定的时间
  function delay(duration) {
    var start = Date.now();
    while (Date.now() - start < duration) {}
  }

  btn.onclick = function () {
    h1.textContent = '袁老师很帅!';
    delay(3000);
  };
</script>

点击按钮后,会发生什么呢?

提示

三秒之后h1文字变更,主线程被阻塞

任务有优先级吗?

任务没有优先级,在消息队列中先进先出

消息队列是有优先级的

根据 W3C 的最新解释:

  • 每个任务都有一个任务类型,同一个类型的任务必须在一个队列,不同类型的任务可以分属于不同的队列。
    在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行。
  • 浏览器必须准备好一个微队列,微队列中的任务优先所有其他任务执行
    https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint

随着浏览器的复杂度急剧提升,W3C 不再使用宏队列的说法

在目前 chrome 的实现中,至少包含了下面的队列:

  • 延时队列:用于存放计时器到达后的回调任务,优先级「中」
  • 交互队列:用于存放用户操作后产生的事件处理任务,优先级「高」
  • 微队列:用户存放需要最快执行的任务,优先级「最高」

提示

添加任务到微队列的主要方式主要是使用 Promise、MutationObserver

例如:

// 立即把一个函数添加到微队列
Promise.resolve().then(函数)

浏览器还有很多其他的队列,由于和我们开发关系不大,不作考虑

面试题:阐述一下 JS 的事件循环

参考答案:

事件循环又叫做消息循环,是浏览器渲染主线程的工作方式。

在 Chrome 的源码中,它开启一个不会结束的 for 循环,每次循环从消息队列中取出第一个任务执行,而其他线程只需要在合适的时候将任务加入到队列末尾即可。

过去把消息队列简单分为宏队列和微队列,这种说法目前已无法满足复杂的浏览器环境,取而代之的是一种更加灵活多变的处理方式。

根据 W3C 官方的解释,每个任务有不同的类型,同类型的任务必须在同一个队列,不同的任务可以属于不同的队列。不同任务队列有不同的优先级,在一次事件循环中,由浏览器自行决定取哪一个队列的任务。但浏览器必须有一个微队列,微队列的任务一定具有最高的优先级,必须优先调度执行

面试题:JS 中的计时器能做到精确计时吗?为什么?

参考答案:

不行,因为:

  1. 计算机硬件没有原子钟,无法做到精确计时
  2. 操作系统的计时函数本身就有少量偏差,由于 JS 的计时器最终调用的是操作系统的函数,也就携带了这些偏差
  3. 按照 W3C 的标准,浏览器实现计时器时,如果嵌套层级超过 5 层,则会带有 4 毫秒的最少时间,这样在计时时间少于 4 毫秒时又带来了偏差
  4. 受事件循环的影响,计时器的回调函数只能在主线程空闲时运行,因此又带来了偏差

CSS 属性计算过程

2023年4月6日 08:00

你是否了解 CSS 的属性计算过程呢?

有的同学可能会讲,CSS属性我倒是知道,例如:

p{
  color : red;
}

上面的 CSS 代码中,p 是元素选择器,color 就是其中的一个 CSS 属性。

但是要说 CSS 属性的计算过程,还真的不是很清楚。

没关系,通过此篇文章,能够让你彻底明白什么是 CSS 属性的计算流程。

image-20220813140434032

首先,不知道你有没有考虑过这样的一个问题,假设在 HTML 中有这么一段代码:

<body>
  <h1>这是一个h1标题</h1>
</body>

上面的代码也非常简单,就是在 body 中有一个 h1 标题而已,该 h1 标题呈现出来的外观是如下:

image-20220813140724136

目前我们没有设置该 h1 的任何样式,但是却能看到该 h1 有一定的默认样式,例如有默认的字体大小、默认的颜色。

那么问题来了,我们这个 h1 元素上面除了有默认字体大小、默认颜色等属性以外,究竟还有哪些属性呢?

image-20220815094215982

答案是**该元素上面会有 CSS 所有的属性。**你可以打开浏览器的开发者面板,选择【元素】,切换到【计算样式】,之后勾选【全部显示】,此时你就能看到在此 h1 上面所有 CSS 属性对应的值。

image-20220813141516153
image-20220813141516153

换句话说,我们所书写的任何一个 HTML 元素,实际上都有完整的一整套 CSS 样式。这一点往往是让初学者比较意外的,因为我们平时在书写 CSS 样式时,往往只会书写必要的部分,例如前面的:

p{
  color : red;
}

这往往会给我们造成一种错觉,认为该 p 元素上面就只有 color 属性。而真实的情况确是,任何一个 HTML 元素,都有一套完整的 CSS 样式,只不过你没有书写的样式,大概率可能会使用其默认值。例如上图中 h1 一个样式都没有设置,全部都用的默认值。

但是注意,我这里强调的是“大概率可能”,难道还有我们“没有设置值,但是不使用默认值”的情况么?

image-20220815094458940

嗯,确实有的,所以我才强调你要了解“CSS 属性的计算过程”。

总的来讲,属性值的计算过程,分为如下这么 4 个步骤:

  • 确定声明值
  • 层叠冲突
  • 使用继承
  • 使用默认值

确定声明值

首先第一步,是确定声明值。所谓声明值就是作者自己所书写的 CSS 样式,例如前面的:

p{
  color : red;
}

这里我们声明了 p 元素为红色,那么就会应用此属性设置。

当然,除了作者样式表,一般浏览器还会存在“用户代理样式表”,简单来讲就是浏览器内置了一套样式表。

image-20220813143500066
image-20220813143500066

在上面的示例中,作者样式表中设置了 color 属性,而用户代理样式表(浏览器提供的样式表)中设置了诸如 display、margin-block-start、margin-block-end、margin-inline-start、margin-inline-end 等属性对应的值。

这些值目前来讲也没有什么冲突,因此最终就会应用这些属性值。

层叠冲突

在确定声明值时,可能出现一种情况,那就是声明的样式规则发生了冲突。

此时会进入解决层叠冲突的流程。而这一步又可以细分为下面这三个步骤:

  • 比较源的重要性
  • 比较优先级
  • 比较次序

来来来,我们一步一步来看。

比较源的重要性

当不同的 CSS 样式来源拥有相同的声明时,此时就会根据样式表来源的重要性来确定应用哪一条样式规则。

那么问题来了,咱们的样式表的源究竟有几种呢?

image-20220823180047075

整体来讲有三种来源:

  • 浏览器会有一个基本的样式表来给任何网页设置默认样式。这些样式统称用户代理样式
  • 网页的作者可以定义文档的样式,这是最常见的样式表,称之为页面作者样式
  • 浏览器的用户,可以使用自定义样式表定制使用体验,称之为用户样式

对应的重要性顺序依次为:页面作者样式 > 用户样式 > 用户代理样式

更详细的来源重要性比较,可以参阅 MDNhttps://developer.mozilla.org/zh-CN/docs/Web/CSS/Cascade

我们来看一个示例。

例如现在有页面作者样式表用户代理样式表中存在属性的冲突,那么会以作者样式表优先。

p{
  color : red;
  display: inline-block;
}
image-20220813144222152
image-20220813144222152

可以明显的看到,作者样式表和用户代理样式表中同时存在的 display 属性的设置,最终作者样式表干掉了用户代理样式表中冲突的属性。这就是第一步,根据不同源的重要性来决定应用哪一个源的样式。

比较优先级

那么接下来,如果是在在同一个源中有样式声明冲突怎么办呢?此时就会进行样式声明的优先级比较。

例如:

<div class="test">
  <h1>test</h1>
</div>
.test h1{
  font-size: 50px;
}

h1 {
  font-size: 20px;
}

在上面的代码中,同属于页面作者样式,源的重要性是相同的,此时会以选择器的权重来比较重要性。

很明显,上面的选择器的权重要大于下面的选择器,因此最终标题呈现为 50px

image-20210916151546500

可以看到,落败的作者样式在 Elements>Styles 中会被划掉。

有关选择器权重的计算方式,不清楚的同学,可以进入此传送门:https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity

比较次序

经历了上面两个步骤,大多数的样式声明能够被确定下来。但是还剩下最后一种情况,那就是样式声明既是同源,权重也相同。

此时就会进入第三个步骤,比较样式声明的次序。

举个例子:

h1 {
  font-size: 50px;
}

h1 {
  font-size: 20px;
}

在上面的代码中,同样都是页面作者样式选择器的权重也相同,此时位于下面的样式声明会层叠掉上面的那一条样式声明,最终会应用 20px 这一条属性值。

image-20220823183928330
image-20220823183928330

至此,样式声明中存在冲突的所有情况,就全部被解决了。

使用继承

层叠冲突这一步完成后,解决了相同元素被声明了多条样式规则究竟应用哪一条样式规则的问题。

那么如果没有声明的属性呢?此时就使用默认值么?

No、No、No,别急,此时还有第三个步骤,那就是使用继承而来的值。

例如:

<div>
  <p>Lorem ipsum dolor sit amet.</p>
</div>
div {
  color: red;
}

在上面的代码中,我们针对 div 设置了 color 属性值为红色,而针对 p 元素我们没有声明任何的属性,但是由于 color 是可以继承的,因此 p 元素从最近的 div 身上继承到了 color 属性的值。

image-20220813145102293
image-20220813145102293

这里有两个点需要同学们注意一下。

首先第一个是我强调了是最近的 div 元素,看下面的例子:

<div class="test">
  <div>
    <p>Lorem ipsum dolor sit amet.</p>
  </div>
</div>
div {
  color: red;
}
.test{
  color: blue;
}
image-20220813145652726
image-20220813145652726

因为这里并不涉及到选中 p 元素声明 color 值,而是从父元素上面继承到 color 对应的值,因此这里是谁近就听谁的,初学者往往会产生混淆,又去比较权重,但是这里根本不会涉及到权重比较,因为压根儿就没有选中到 p 元素。

第二个就是哪些属性能够继承?

关于这一点的话,大家可以在 MDN 上面很轻松的查阅到。例如我们以 text-align 为例,如下图所示:

image-20220813150147885
image-20220813150147885

使用默认值

好了,目前走到这一步,如果属性值都还不能确定下来,那么就只能是使用默认值了。

如下图所示:

image-20220813150824752
image-20220813150824752

前面我们也说过,一个 HTML 元素要在浏览器中渲染出来,必须具备所有的 CSS 属性值,但是绝大部分我们是不会去设置的,用户代理样式表里面也不会去设置,也无法从继承拿到,因此最终都是用默认值。

好了,这就是关于 CSS 属性计算过程的所有知识了。

image-20220814234654914

一道面试题

好了,学习了今天的内容,让我来用一道面试题测试测试大家的理解程度。

下面的代码,最终渲染出来的效果,a 元素是什么颜色?p 元素又是什么颜色?

<div>
  <a href="">test</a>
  <p>test</p>
</div>
div {
  color: red;
}

大家能说出为什么会呈现这样的结果么?

解答如下:

image-20220813151941113
image-20220813151941113

实际上原因很简单,因为 a 元素在用户代理样式表中已经设置了 color 属性对应的值,因此会应用此声明值。而在 p 元素中无论是作者样式表还是用户代理样式表,都没有对此属性进行声明,然而由于 color 属性是可以继承的,因此最终 p 元素的 color 属性值通过继承来自于父元素。

你答对了么?-)

JavaScript ES6

2015年9月22日 08:00

JavaScript 有着很奇怪的命名史。

1995 年,它作为网景浏览器(Netscape Navigator)的一部分首次发布,网景给这个新语言命名为 LiveScript。一年后,为了搭上当时媒体热炒 Java 的顺风车,临时改名为了 JavaScript (当然,Java 和 JavaScript 的关系,就和雷锋和雷锋塔一样 —— 并没有什么关系)

歪果仁的笑话怎么一点都不好笑

译者注:wikipedia 的 JavaScript 词条 更详细的叙述了这段历史

1996 年,网景将 JavaScript 提交给 ECMA International(欧洲计算机制造商协会) 进行标准化,并最终确定出新的语言标准,它就是 ECMAScript。自此,ECMAScript 成为所有 JavaScript 实现的基础,不过,由于 JavaScript 名字的历史原因和市场原因(很显然 ECMAScript 这个名字并不令人喜欢……),现实中我们只用 ECMAScript 称呼标准,平时都还是使用 JavaScript 来称呼这个语言。

术语(译者注):

  • 标准(Standard): 用于定义与其他事物区别的一套规则
  • 实现(Implementation): 某个标准的具体实施/真实实践

不过,JavaScript 开发者们并不怎么在乎这些,因为在诞生之后的 15 年里,ECMAScript 并没有多少变化,而且现实中的很多实现都已经和标准大相径庭。其实在第一版的 ECMAScript 发布后,很快又跟进发布了两个版本,但是自从 1999 年 ECMAScript 3 发布后,十年内都没有任何改动被成功添加到官方规范里。取而代之的,是各大浏览器厂商们争先进行自己的语言拓展,web 开发者们别无选择只能去尝试并且支持这些 API。即使是在 2009 年 ECMAScript 5 发布之后,仍然用了数年这些新规范才得到了浏览器的广泛支持,可是大部分开发者还是写着 ECMAScript 3 风格的代码,并不觉得有必要去了解这些规范。

译者注:ECMAScript 第四版草案由于太过激进而被抛弃,Adobe 的 ActionScript 3.0 是 ECMAScript edition 4 的唯一实现( Flash 差点就统一 Web 了)

到了 2012 年,事情突然开始有了转变。大家开始推动停止对旧版本 IE 浏览器的支持,用 ECMAScript 5 (ES5) 风格来编写代码也变得更加可行。与此同时,一个新的 ECMAScript 规范也开始启动。到了这时,大家开始逐渐习惯以对 ECMAScript 规范的版本支持程度来形容各种 JavaScript 实现。在正式被指名为 ECMAScript 第 6 版 (ES6) 之前,这个新的标准原本被称为 ES.Harmony(和谐)。2015 年,负责制定 ECMAScript 规范草案的委员会 TC39 决定将定义新标准的制度改为一年一次,这意味着每个新特性一旦被批准就可以添加,而不像以往一样,规范只有在整个草案完成,所有特性都没问题后才能被定稿。因此,ECMAScript 第 6 版在六月份公布之前,又被重命名为了 ECMAScript 2015(ES2015)

目前,仍然有很多新的 JavaScript 特性或语法正在提议中,包括 decorators(装饰者)async-await(async-await 异步编程模型)static class properties(静态类属性)。它们通常被称为 ES7,ES2016 或者 ES.Next 的特性,不过实际上它们只能被称作提案或者说可能性,毕竟 ES2016 的规范还没有完成,有可能全部都会引入,也有可能一个都没有。TC39 把一个提案分为 4 个阶段,你可以在 Babel 的官网 上查看各个提案目前都在哪个阶段了。

所以,我们该如何使用这一大堆术语呢?下面的列表或许能帮助到你:

  • ECMAScript:一个由 ECMA International 进行标准化,TC39 委员会进行监督的语言。通常用于指代标准本身。
  • JavaScript:ECMAScript 标准的各种实现的最常用称呼。这个术语并不局限于某个特定版本的 ECMAScript 规范,并且可能被用于任何不同程度的任意版本的 ECMAScript 的实现。
  • ECMAScript 5 (ES5):ECMAScript 的第五版修订,于 2009 年完成标准化。这个规范在所有现代浏览器中都相当完全的实现了。
  • ECMAScript 6 (ES6) / ECMAScript 2015 (ES2015):ECMAScript 的第六版修订,于 2015 年完成标准化。这个标准被部分实现于大部分现代浏览器。可以查阅这张兼容性表来查看不同浏览器和工具的实现情况。
  • ECMAScript 2016:预计的第七版 ECMAScript 修订,计划于明年夏季发布。这份规范具体将包含哪些特性还没有最终确定
  • ECMAScript Proposals:被考虑加入未来版本 ECMAScript 标准的特性与语法提案,他们需要经历五个阶段:Strawman(稻草人),Proposal(提议),Draft(草案),Candidate(候选)以及 Finished (完成)。

在这整个 Blog 中,我将把目前的 ECMAScript 版本称作 ES6(因为这是大部分开发者最习以为常的),把明年的规范称作 ES2016(因为,与 ES6/ES2015 不同,这个名字将在整个标准化过程中沿用)并且将那些还没有成为 ECMAScript 定稿或草案的未来语言概念称为 ECMAScript 提案或者 JavaScript 提案。我将尽我所能在任何可能引起困惑的场合沿用这篇文章。

一些资源

来学 JavaScript 吧!

著作权声明

本文译自 ES5, ES6, ES2016, ES.Next: What's going on with JavaScript versioning?
译者 黄玄,首次发布于 Hux Blog,转载请保留以上链接

❌
❌