标签: JavaScript

  • 【视频教程】技术栈大升级:Vue3 到 Nuxt3(4)深入理解 SSR 和 `useAsyncData`

    【视频教程】技术栈大升级:Vue3 到 Nuxt3(4)深入理解 SSR 和 `useAsyncData`

    2023 年,我个人最大的变化,是从 Vue3 SPA 应用向 Nuxt3 SSR 应用过渡,在预期可能存在 SSR 需求的项目中,都尽量使用 SSR。包括 React 应用,也尽量使用 Next.js,而不是 React SPA。

    这个过程中,面临很多问题,很多思路需要转换,很多以前没关注的点需要关注。本系列视频试图快速教会大家这些要点,帮助大家顺利从 SPA 切换到 SSR。

    这次的视频更偏理论,重点讲解 Nuxt3 如何处理 useAsyncData,以及为了兼顾 SSR 和前端开发所做的渲染策略设计。这部分知识我其实很晚才掌握,因为文档里说的也不太详细;所以既是好消息也是坏消息。好消息是,哪怕你没有掌握,也不太耽误使用 Nuxt3 开发项目;坏消息是,保不齐就会遇到一些奇怪的问题,难以复现和排错。

    视频要点:

    1. 现代化 SSR 的优势
    2. 深入理解 useAsyncData
    3. 使用 Pinia 传递数据
    4. 理解生命周期钩子变化

    如果你对 Vue3 开发、Nuxt3 开发、SSR 感兴趣,欢迎关注我的本系列。如果你对这些话题有疑问,欢迎留言讨论。

  • 在 Code.fun 做 Code Review(四)

    在 Code.fun 做 Code Review(四)

    时光如梭,一晃 2022 年已经过去 2/3,我们一起迎来 9 月。秋风送爽,丹桂漂亮,下面,我们一起回顾 8 月份我在 code.fun 完成的 Code Review 吧。

    关于 Code Review 的介绍和经验,欢迎大家回顾前三篇,本文暂不重复:


    (更多…)
  • 在 Code.fun 做 Code Review(三):聊聊 Promise 的错误处理、如何真正学到技术

    在 Code.fun 做 Code Review(三):聊聊 Promise 的错误处理、如何真正学到技术

    嗯,不知不知觉这个系列写到第三篇,这一篇会改变一下写法,从一次 Code Review 出发,讲解几个技术点,然后分享一下技术学习的经验,以及 Code Review 的用途。希望对大家有帮助。

    顺便推广下前两篇:

    0. 起因

    某天,我给一位同事做 code review,看到下面这段代码:

    const err = new Error('错误信息');
    
    return Promise.reject(err);

    于是我就回复:error 最好直接 throw

    然后他回复我:如果改成 throw 的话,这个 catch 好像是捕获不到 throw 的。

    这就很诡异了,这不符合 Promise 的设计;而 Promise 不是新生事物,它有很大的测试集可以保证行为符合预期,我觉得我们想遇到问题都很困难。于是我就跟这位同事连线,帮他分析问题。

    1. 真正的问题

    连线之后,我发现他的真正问题并不是按照我的要求修改代码之后遇到的。由于基础不太牢靠,他先写了一段实验代码,想要验证我的要求,结果这段代码行为出现异常:

    他希望能够通过 .catch(err => console.log(err)) 捕获并记录错误,但是从控制台的输出来看,错误却是 Uncaught(未捕获)。那么问题出在哪儿呢?

    作为曾经讲解过 Promise 的我当然是一眼就看出来问题所在,但是这位同学却琢磨不透。于是我就把问题发到研发群里,果然又问倒好几位同学。

    现在我请问各位读者老爷,你们知道么?或者换个方向,如果想正常使用 .catch() 捕获到错误,应该怎么修改呢?

    2. 问题解答

    Promise

    这里我们必须回到 Promise 的规范,才能了解上面截图的问题所在(关于详细规范,请参阅 MDN Promise,这里只摘我们需要的部分):

    1. Promise 主要用来改进编写异步函数的体验。
    2. 通过 new Promise(function (resolve, reject) {}) 会创建一个 Promise 实例,其中的参数应该是一个函数(通常是异步函数),接受两个参数:resolvereject。当异步操作成功之后,应调用 resolve(result) 并传递结果;当异步操作失败,应调用 reject(reason) 并传递错误信息。
    3. Promise 有三个状态:pendingfulfilledrejected,起始状态是 pending,变更后,就固定下来,不会再次变更。
    4. 如果异步函数本身抛出错误,Promise 也会进入 rejected 状态。
    5. fulfilled 的 Promise 实例会转入 .then() 处理;rejected 会转入 .catch() 处理。

    我们再回头看上面的截图,这里的问题在于,错误是在 setTimeout(异步函数)的回调函数里抛出的,抛出时当前 Promise 所在的执行栈已经结束了,回调函数是全新开启的执行栈,所以 Promise 无法捕获到它里面的异常;而它也没有主动调用 reject(err) 传出错误,所以就变成了 Uncaught(未捕获)。

    函数执行栈

    既然说到执行栈,我们就顺便补充一下执行栈的知识吧。形如以下代码:

    function a() {
      b();
    }
    function b() {
      c();
    }
    function c() {
      d();
    }
    function d() {
      // do something
    }
    a();

    当函数 a 执行的时候,运行时会开启一个新的执行栈,并且把 a 入栈接下来发现 a 调用了 b,就又会把 b 入栈;然后发现 c……运行时会重复这个过程。当一个函数执行完毕,比如 d,运行时就会把它移出栈,然后继续执行 c 的剩下部分。

    对于 JavaScript 而言,错误是可以冒泡的。即在 d 抛出的异常,如果没有被捕获,它就会一直上浮,直到回到全局。所以我们可以在栈内的任何一个环节捕获它;但如果跨栈,那就无法捕获。

    Event Loop

    异步函数的回调函数会由 Event Loop 开启新栈执行,与所以异步函数自身处于不同的执行栈,所以错误不会被异步函数捕获,自然也不会改变 Promise 的状态。

    解决问题

    所以,要正确捕获错误的话,就需要想办法捕获异步函数的回调函数的错误,即:

    function yy() {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          // 方案1
          reject(new Error('错误信息'));
          // 方案2,实际中很少这么写,这里只是用来演示。但要注意,捕获必须放在回调栈,才能捕获到发生的错误。
          try {
            throw new Error('错误信息');
          } catch (e) {
            reject(e);
          }
        }, 1000);
    
        // 实际场景中,更多是这样的,err 作为参数传给回调函数
        fs.writeFile(path, content, 'utf8', (err) => {
          if (err) {
            return reject(err);
          }
          resolve();
        });
      });
    }

    3. 如何学习技术

    对于上述问题,我们只要掌握 Promise 规范、函数执行栈、Event Loop,就很容易判断。但是如果这三个概念有一个不太清楚,就容易犯迷糊。所以,很多时候,要避免出问题、要保证软件正常工作,我们必须清楚了解每个技术的规范、定义。

    比如 Promise、原型链、闭包等等,它们都不是自然界的产物;而是在长期软件开发实践中,由开发者总结设计出来,用于解决特定问题的发明。他们都有严格的定义、功用、优缺点,等等。我们日常开发,应该把这些东西当成知识储备起来,针对需求做排列组合,给出解决方案。

    有些同学相反,他们会记下来一些技术的用法,然后反推这些技术的特性。如果对一个技术不熟悉,他们会尝试做类似实际场景的实验,然后再想办法搬到实际场景中。这样做,覆盖常见场景可能没问题,遇到陌生的领域就容易踩坑。

    所以,我要强调,对于新技术,大家不熟悉又要尽快用在实际开发中,临时做点实验记住些经验用法当然可以。但不应满足于此,要找时间把技术规范补起来,把完整的设计至少读个几遍。一方面纠正自己的错误实践,另一方面,也可以扩大你对这个技术的应用面。

    4. Code Review 的作用

    这里也不得不提 Code Review 的一个重要作用:

    传承知识。Code Reviewe 是非常好的查缺补漏机会,可以针对性补强开发者的知识盲区,纠正不良习惯。

    通过 Code Review,我发现了一位同事在 Promise 和函数执行栈方面存在知识盲区,然后我借机帮他补齐了这方面的知识。接下来,我把这个问题分享到技术研讨群,还有几位同学也不是很清楚,也趁机补齐了。于是,我厂再出现这个问题的概率,就降低了。换言之,我厂技术群体的下限,就拔高了。

    思否上有同学问:什么样的技术 leader 是称职的? 我的答案第一条就是:

    给团队的技术兜底。通过工具、规范、流程,保证无论开发水平如何,都能尽快提交符合要求、满足规范、质量过硬的代码。

    具体一点:要重视每一次 Code Review,找到问题,解决问题,补全大家的知识点,提升团队下限。

    5. 总结

    上次的 Code Review 分享获得了意料之外的欢迎,希望这次同样能帮助到大家——我已经把上面的问题加入我的面试题库了,哈哈。

    我现在在 code.fun 工作,我们的产品会把设计稿自动转换成代码,期望大大提升前端的开发效率,如果你对这个产品感兴趣,欢迎联系我,或直接试用。我们公司提供远程工作岗位,有兴趣的同学可以联系我;朋友的公司 API7 也在招聘前端,有兴趣可以找我内推。

    如果诸位读者老爷对软件质量管理、软件开发效率、Code Review 有什么想问的、想说的,敬请在评论区与我互动。

    6. 扩展阅读

  • 在 Code.fun 做 Code Review(二)

    在 Code.fun 做 Code Review(二)

    这周周会上,有同事说:

    以前他的 PR 被 approved 就直接合;现在他会等上一两天,期待我来 review。晚上耍手机时如果突然一阵爆响就说明我 reviewing,他就爬起来好好看我的评论然后作出修改,学到很多东西。

    被承认当然很爽、很骄傲。于是我想起这个系列,该再更一篇了。本文选材自七月的 Code Review 经验,趁热先分享一波。更早的 Code Review 过两天再往前翻翻看能否整理出来。

    关于 Code Review 的介绍和经验,欢迎大家回顾:在 Code.fun 做 Code Review,本文我就不重复了。

    0. 不应该使用独立 <script src="...">

    在这个需求中,我们要使用火山引擎统计用户行为,某同事就直接在模版里添加了 <script src="..."> 导入初始化 JS。这段 JS 非常之短,只是帮火山引擎初始化执行环境,但是为了保证执行状态,它既不能 async 也不能 defer

    这样做有几个问题:

    1. <script> 会阻塞后面的 HTML 渲染和 JS 执行;
    2. 这段 JS 虽然短,但是它的下载时间不会短:
      1. 浏览器要解析 DNS,
      2. 然后文件服务器要找到文件
      3. 文件服务器甚至没有 http2

    正确的做法是把它直接塞到现在 JS 里,只增加几十个字节而已。

    1. mounted 钩子添加的事件处理函数没有清理;直接使用 window.onblur

    这位同学犯了两个错误:

    1. 他在 mounted 钩子里添加了事件处理函数,但是并没有在 destroyed 钩子里清理。这可能导致内存泄漏,也可能导致奇怪的表现。
    2. 他直接将处理函数绑在 window 上,如果其它库、其它代码使用了同样的事件,就会导致冲突。

    2. 糟糕的命名

    这里这位同学犯了两个错误:

    1. event 是名词,不应该用做函数名。函数名应该是动词、动宾短语、动副短语,表达一个动作。
    2. event 是个非常非常通用的单词,用作全局函数太容易和其它全局对象起冲突或起混淆,应该加长。

    结合上下文,合适的写法是:triggerHuoshanEvent()

    3. 依赖 JSON.parse()

    我们需要记录一个小数据:sourceType,这个小数据被封装在一堆很大的 JSON 里面。这位同学直接拿 JSON.parse() 解码,然后打点。这样做当然可行,但是效率上非常之浪费。

    大家千万不要小看 JSON.parse(),它其实有很多细节:

    1. JSON 数据格式非常之严格,换言之,绝大对数情况下,必须把所有字符串都分析完才能结束
    2. 这个过程中,会构建大量对象
    3. 之后不再使用,这些对象又要被逐个回收

    所以正确的写法,应该使用字符串匹配或者正则。字符串操作都是 O(1),而且会在匹配到结果后立即返回,所以性能优秀非常多。

    (其实吧,这位同学的正则也有些一言难尽,不过今天先不展开。)

    4. TypeScript 的数据转换

    使用 TypeScript 之后,我们需要声明函数的返回值类型。对于一些比较通用的返回值,比如截图中的 mongoose lean(),就要进行类型声明转换,后面的代码才知道取到的值是什么类型。这里大家要记住,无论是浏览器还是 node,TS 都要经历编译之后,变成 JS 再被执行。

    所以 TS 阶段的类型转换不会出现在最终执行的代码里,也不会影响效率。但是截图中属于不同的情况,它会留下一个 .then() ,并增加一个微任务,不是我们想要的结果。

    5. TS 的枚举值可以适当调整

    TypeScript 提供 enum 关键字,用来声明枚举值,可以大大提升开发效率,降低出错概率。枚举的值可以手动指定,也可以省略,省略后 TS 会自动帮我们从 0 开始递增。上面就是典型的使用场景,列举若干设计软件,最后是 Other(其它),可以做成单选多选放在表单里。

    这段代码的问题在于,设计软件很多,我们列出来的肯定有疏漏;即使没有,将来也会发布更多设计软件。比如有天,我们自己做了设计软件,叫 design.fun,那么该放到哪里呢?放到 Other 前面,它的值就会跟 Other 冲突;放到后面,又会使得数据看起来很奇怪。

    此时只要给 Other 一个大点的值,拉开差距,就可以解决这个问题:

    export enum DesignSoftware {
      Photoshop,
      XD,
      JsDesign,
      Other = 100, // 再大点也行,不过 100 应该够我们用了
    }

    6. 组件名称和根节点样式应包含不少于两个单词

    这个其实是 Vue 官方的风格建议。原因很简单,一个单词,无论作为组件名称还是根节点样式名,都太容易和其它组件重复,也会影响 IDE 里快速跳转的效率,所以尽量多写几个单词,反正编译过程可以自动优化。


    总结

    时间过的真快,距离上次分享 Code Review 经验已经过去四个月了,我在 Code.fun 也是半年多工龄的老员工了。经过半年多的努力,我厂的产品完整了很多,但也很难称得上完善。希望这次分享的内容对大家有用,也希望大家多来试用我们的产品。

    如果诸位读者老爷对软件质量管理、软件开发效率、Code Review 有什么想问的、想说的,敬请在评论区与我互动。

    我现在在 code.fun 工作,我们的产品会把设计稿自动转换成代码,期望大大提升前端的开发效率,如果你对这个产品感兴趣,欢迎联系我,或直接试用。

  • 用 express.js 实现流式输出 HTTP 响应

    用 express.js 实现流式输出 HTTP 响应

    0. 前言

    我们先来总结一下客户端与服务器端的数据交互方式:

    1. ajax,即 XMLHttpRequest,最常见的数据交互方式,适用于较轻量级的一次性请求,比如用户登录、CRUD 等。
    2. SSE,服务器端事件,适用于有大量服务器端推送、且不需要从客户端发送数据的需求,比如盯股票行情
    3. WebSocket,适用于上下行数据都很多、都很频繁的情况,比如在线聊天、网络游戏等

    单从技术角度来看,大概是这几个。根据需求变化,我也可以组合出更多方案。比如,类似 Showman 生成视频这种需求,耗时很长,对服务器资源占用很高,就比较适合:

    1. 发起请求,产生一条任务记录,进入队列
    2. 服务器上运行独立的进程,从队列中取任务出来执行,并记录日志
    3. 客户端不断检查任务进度

    1. 需求

    今天的需求,介于轻量一次性,与需要服务器长时间执行之间,即 FastTest 中的发布功能。这里,我们有以下需求:

    1. 管理员点击 Publish 按钮,开始发布静态网站
    2. 服务器需要先保存数据,然后调用 webpack 发布所有语言版本
    3. 只有少数管理员会使用此功能,服务器压力不大
    4. 管理员希望能了解发布进度,及每一步的状态

    所以,提交任务后等待服务器慢慢跑就不合适;等待请求完成一次性看到所有结果也不合适;最合适的,就是基于长链接,不断获取返回信息,并在前端实时看到进度。也即是:流式输出 HTTP 响应。

    2. 实现

    2.1 后端

    后端实现我选择 node.js + express.js,在后端领域,这两个我比较熟悉。express.js 是在 node.js 网络模块上进行封装得来,使用起来很简单,也支持原生 node.js 方法。

    首先我们创建一个服务器:

    const express = require('express');
    const app = express();
    const port = 3100;
    
    app.listen(port, () => {
      console.log('FastTest Admin API at: ', port);
    });

    然后,我们在需要流式返回响应的接口里设置相应头 Content-type: application/octet-stream。接下来,只要我们不断向输出流写入内容就可以了。哦,对了,结束的时候,我们还要关闭输出流。

    app.post('/data', async(req, res, next) => {
      res.setHeader('Content-type', 'application/octet-stream');
      
      // 第一次返回
      res.write('Local data saved. Start to build dist。files.\n');
    
      // 数次返回
      for (const item of items) {
        await doSomething(item);
        res.write(`${item} done successfully.\n`);
      }
    
      // 最后,全部完成
      res.write('All done.');
      // 关闭输出流
      res.end();
    });

    2.2 axios

    axios 是很流行的 ajax 库,它进行了 Promise 封装,用起来很方便。这里我们要用 onDownloadProgress(即 axios 对 XMLHttpRequest.progress 的封装)获取下载进度,它会在每次服务器返回响应字符串的时候更新,我们只需要截取上次响应之后,这次响应新增的内容,即可。

    function publish(data, onDownloadProgress) {
      return axios.post('/data', data, {
        onDownloadProgress,
      });
    }
    
    async function doPublish() {
      if (isLoading.value) {
        return;
      }
    
      isPublishing.value = true;
      message.value = status.value = null;
    
      try {
        const { cases, lang } = store.state;
        let offset = 0;
        await publish({ cases, lang }, ({ target: xhr }) => {
          // responseText 包含了从一开始到此刻的全部响应内容,所以我们需要从上次结束的位置截取,获得新增的内容
          const { responseText } = xhr;
          const chunk = responseText.substring(offset);
          // 记录这一次的结束位置
          offset = responseText.length;
          currentStatus.value = chunk;
        });
        status.value = true;
        currentStatus.value = '';
        message.value = 'Published successfully.';
      } catch (e) {
        message.value = 'Failed to publish. ' + e.message;
      }
      isPublishing.value = false;
    }

    2.3 Vue3

    相对来说,Vue3 的部分最容易。这里我用了 animate.css 的 flash 效果,让信息更显眼。除此之外,就是简单的赋值。

    <template lang="pug">
    .alert.alert-info.mb-0.me-2.py-1.px-3.animated.flash.infinite.slower(
      v-if="currentStatus",
    ) {{currentStatus}}
    </template>

    3. 效果演示

    流式输出效果演示

    4. 部署

    一般来说,我们很少会直接用 node.js 当服务器,多半会启动 node.js 服务,然后用 nginx 反向代理给用户访问。这里需要注意,nginx 默认会将响应内容存入缓冲区,然后批量返回给客户端。这会导致流式输出无效,变成常规的执行完毕后一次性输出。

    所以我们必须修改 nginx 的配置:

    # 仅展示有关配置
    location /data {
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header Host $http_host;
            proxy_pass http://localhost:3100;
            # 关闭代理缓冲区,所有响应实时输出
            proxy_buffering off;
    }

    5. 总结 & 扩展阅读

    前些天我帮朋友做了个小项目,叫 FastTest。项目很简单,不过我主动扩展了技术选型,使用了非常多的技术,都是浅显入门的程度,非常适合用来做基础入门学习。本文也是从这个项目中提炼出来的。建议大家有空的时候看看,拉到本地,跑起来试试看效果。

    文中的几段代码,分别位于:

    项目的详细介绍请见:超全面全栈入门项目:fastest

    扩展阅读

  • 检查超宽元素的脚本

    检查超宽元素的脚本

    有时候我们制作页面,搞着搞着发现超宽,出现横向滚动条。于是我们就要想办法调整样式,但是往往超宽的只有那么一两个元素,并不是很好找,所以我就写了下面一个脚本,在页面里跑一下就能找到超宽的元素,然后针对性调整一下样式就可以了:

    // 这里的 375 主要针对基于 iPhone 6 开发移动端页面时
    function traverse(parent) {
      let target;
      for (const elem of parent.children) {
        const rect = elem.getBoundingClientRect();
        const {left, width} = rect;
        if (left + width > 375) {
          target = elem;
          break;
        }
        target = traverse(elem);
        if (target) {
          return target;
        }
      }
      if (target) {
        return target;
      }
    }

    使用的时候,打开页面的开发者工具,将这段代码复制到 console 里面,然后执行 traverse(document.body) 就可以找到超宽的元素,然后想办法调整它即可。

    当然我们也可以继续用这个函数探索可疑元素,找到更具体的超宽元素;或者找到其它超宽元素。这些就留给大家自行探索吧。

  • 函数,栈,try…catch,以及异步

    函数,栈,try…catch,以及异步

    前两天《记一个 `try…catch` 异步函数的坑》发出后,有同学表示对最后一句不解:

    异步函数的调用可能并不在当前栈,也无法被 try…catch 在当前栈捕获到错误。

    要解释清楚,一两行是不够的,于是写这篇文章展开解释一下。(这篇文章是基础向。)

    函数与栈

    首先来看这样一段代码:

    function a() {
      // ...
    }
    
    function b() {
      // 代码1 
      a();
      // 代码2
    }
    
    function c() {
      // 代码3
      b()
      // 代码4
    }
    
    c();

    它会怎么执行呢?简单来说是这样:

    1. 构建一个栈
    2. c 推入栈,开始执行 代码3
    3. b 推入栈,开始执行 代码1
    4. 将 a 推入栈,开始执行
    5. a 执行完,出栈
    6. 开始执行 代码 2
    7. b 执行完,出栈
    8. 开始执行 代码 4
    9. c 执行完,出栈

    栈是一个先入后出的数据结构,你可以把它当成一个桶,先放进去的东西会被后放进去的东西压在下面,需要先把上层的东西,也就是后放进去的东西拿出来才能拿到先放进去的东西。

    发生错误

    如果代码执行中发生错误,就会在错误处中断,并逐个出栈。此时我们不仅能看到错误本身,还能看到错误发生处的完整栈信息,对 debug 很有帮助。

    try...catch

    异步函数出现之前,try...catch 只能捕获当前栈的错误。比如,同样是上面那段代码,稍微改动一下:

    // 之前定义 a b c 的代码不变
    
    function d() {
      throw new Error('oh my god');
    }
    
    try {
      c();
    } catch (e) {
      console.error(e);
    }
    
    d();

    这段代码里,c() 执行时如果有错误,可以被 try...catch 捕获到;但是 d() 位于另一个栈(或同一个栈的另一个堆叠),就无法捕获。

    异步 callback & Promise

    Promise 成为规范被纳入标准之前,我们处理异步操作时需要使用回调(callback)。比如侦听事件:

    try {
      $('.btn').on('click', function (e) {
        console.log('hello');
      });
    } catch (e) {
      console.error(e);
    }

    请注意:这里添加侦听函数的代码,也就是 $().on() 的部分,是在当前栈执行的;而用户点击后执行的操作,即 console.log('hello') 的部分,是在将来某个事件调用栈里执行的。所以,如果后面的函数里有问题,那么如上面所示的代码是无法捕获到错误的。

    Promise 也一样。比如下面这段代码:

    new Promise((resolve) => {
      // 执行完后回调
      func0(resolve);
    })
      .then(() => {
        return func1();
      })
      .then(() => {
        return func2();
      });

    这里的 func0func1func2 三个函数都是在不同调用栈里执行的,所以如果你在最外面 try...catch,无法捕获到错误。

    这样会给我们编写代码带来一些困难。比如,有时候我们需要同时发起好几个网络请求,有些会成功有些会失败。我们并不知道每个失败请求对应的构造函数是怎么执行的,只能依靠请求内容进行判断,就比较麻烦。

    这个问题,在异步函数中得到了解决。

    异步函数 async function

    这个变化主要是 await 带来的。

    前篇文章所说,在不使用 await 的情况下,try...catch 只会捕获当前调用栈的错误。对于异步函数来说,它在当前栈会返回 Promise 实例,然后就顺利结束,不会抛出任何错误。所以 try...catch 也无法捕获任何错误。

    但是添加 await 之后,情况就不同了。这个时候,try...catch 会捕获整个 Promise 里所有的错误,即使函数位于不同调用栈,也可以正确捕获并且显示在一起,方便调试。

  • JavaScript 获取正则表达式中子表达式的个数

    JavaScript 获取正则表达式中子表达式的个数

    正如标题所示,我厂有这么一个需求。我不会,老板鄙视我后丢过来一个链接:stackoverflow: Count the capture groups in a qr regex?

    看不太懂 Perl,但是这个思路很棒。所以改写成 JS 版,并记录如下:

    function countCapturingGroups(r){
      r = new RegExp(`|${r.source}`);
      const result = ''.match(r);
      return result.length - 1;
    }
    
    const result = countCapturingGroups(/fo(.)b(..)/);
    console.log(result); // 2
    

    它的原理是这样的。构建一个新正则,包含两部分:空字符和目标正则。空字符正则会完成与目标字符串的匹配,保证有结果(不然的话就会返回 null。接下来 | 会保证后面的正则也是有效的,可以生成包含子表达式结果的数组。

    我们知道,结果是个类数组,结构大约是:

    1. 全部匹配字符串
    2. 0~N 子表达式结果
    3. 其它一些属性

    所以用其长度 – 1 就能获得子表达式的个数。从功耗上来说,这个应该是很节省了。

  • 使用 Proxy 创建有魔术属性/方法的类

    使用 Proxy 创建有魔术属性/方法的类

    前几天在 SF 回答了这个问题:如何使用proxy,如何在内部拦截get方法,然后翻了翻以前写的博客:使用 Proxy 添加魔术属性/方法,发现上次写完代理对象就停笔了,所以今天补全一下:创建有魔术属性/方法的类。这样就比较完整了。

    JavaScript 构造函数的特点

    ES6 增加了 class 关键字,梳理了面向对象的语法,现在我们可以这样定义一个类:

    class Person {
      constructor(name) {
        this.name = name; 
      }
    
      hello() {
        return `Hello, my name is ${this.name}.`;
      }
    }

    如果你有其它面向对象语言的经验,应该很容易理解这段代码。

    不过 JS 的构造函数特别,它支持 return 一个其它对象,作为 new SomeClass() 的结果。(如果不 return 或者 return 一个空对象,那么 new SomeClass() 得到的就是 SomeClass 的实例。)

    也就是说,原则上,我们可以这么做:

    class Person {
      constructor(name) {
        this.name = name;
        return {name: 'Meathill'};
      }
    }
    
    const person = new Person('张三');
    console.log(person.name); // 'Meathill'

    结合 Proxy

    理解了上一节的内容,我们就很容易得到这样一个类:

    class Person {
      constructor(name) {
        this.name = name;
    
        return new Proxy(this, {
          get(target, property) {
            if (property in target) {
              return target[property];
            }
            console.warn('Sorry, I can't do that.');
          }
        }
      }
    }

    在这个类的构造函数里,我返回了一个 Proxy 实例,代理了对真正 Person 实例的访问。当访问的属性/方法在实例上时,就返回需要的属性/方法,否则的话,输出警告。

    实际上,Proxy 的 getset 的功能远不止如此,上面的代码只是一些演示。

    用途

    魔法属性/方法主要有以下用途:

    1. 对象 a 要使用一部分对象 b 的功能,但是又不方便直接用原型链。比如上一篇文章的场景,我提供类 VElement 作为接口,实际完成工作的是另一个沙箱中的 Element。
    2. 不知道会怎么访问对象,希望所有访问都照顾到。
    3. 希望捕获到对对象的修改,也就是 Vue 3.0 的核心修改。

    Vue 3.0

    Vue 1.x & 2.x 期间,都在使用 ES5 的 Object.defineProperty 拦截对对象的修改,实现响应式。这样的做法看起来很神奇,给 Vue 带来了巨大的成功。但是这样做也有坏处:

    1. 声明实例时需要很多预处理工作,而且数据量越大处理的时间越久
    2. 不支持某些数组操作
    3. 不支持其它数据类型,比如 Set、Map
    4. 不支持后续的数据观察

    使用 Proxy 之后,以上问题全部都迎刃而解,甚至,因为 Proxy 是原生 API,性能表现更好,取得了内存减半、速度加倍的效果。

    想了解更多 Vue 3.0 的新特性,可以去看我在 SF 的分享:迎接 Vue 3.0。(注:这是免费广告,我不会从新购用户取得收益。)


    参考文章:

    Constructor, operator “new”(构造函数和操作符 “new”)

  • 使用 Proxy 添加魔术属性/方法

    使用 Proxy 添加魔术属性/方法

    最近在开发我厂的 QA 工具时,遇到一个问题。我需要模拟 Puppeteer 的所有方法,以便兼容原先的 JS 文件。Puppeteer 提供一个 .asElement() 方法,可以把函数执行结果转换成一个伪 DOM Element(如果函数返回的就是 DOM Element 的话),然后我们就可以在 Node.js 里调用原本属于 DOM 的方法,比如 .focus()。Pupputeer 会替我们完成映射和函数调用,并且返回结果。

    对于大部分对象来说,我只要模拟对应的属性、方法,然后用自己的函数实现功能即可。但是 DOM Element 有上百个属性和方法,手工实现一遍实在太低效了。必须寻找其它途径。

    好在我之前看过 Proxy 的介绍,赶紧翻出文档和书又复习两遍,就大概知道怎么做了。

    Proxy 类如其名,可以“代理”对某个对象的访问。你可以把他理解成明星的经纪人。明星成名之前都是自己处理一切事务,有了经纪人之后,大部分事务就由经纪人负责,但仍然有一些事情需要明星自己处理。

    Proxy 的用法很简单,实例化时,把要代理的对象传进去,定义一下代理方法就好。

    const obj = { name: 'meathill' };
    new Proxy(obj, {
      get(target, property) {
        // 如果对象中有要求的属性或方法,则返回
        if (property in target) {
          return target[property];
        }
        // 没有的话,进行其它处理
        return 'hello';
      }
    });
    
    obj.name // 'meathill'
    obj.age // 'hello'
    obj.sex // 'hello'

    接下来,比如我们访问 obj.foo,那么代理就会生效,它会先检查 obj,如果这个对象上本来就有 foo 属性,就会返回;如果没有,则会调用我们定义的方法来处理。

    如此一来,我们可以定义一个 VElement 类,这个类可以实现一些特殊方法,比如 .type(str) 输入,.click() 点击等;然后用 Proxy 代理其它方法和属性,让对象进入插件 Context 执行。


    关于如何创建具有魔法属性/方法的类,请移步阅读 使用 Proxy 创建有魔术属性/方法的类


    Proxy 还有其它方法也很有用,尤其是 get 对应的 set ,以后再介绍。大家可以自己抽空研究下。

    参考

    • 阮一峰的 ES6
    • 《深入理解 ES6》