分类
js 技术

使用 JS 模拟元素被 click

需求来自于我厂的 QA 产品。在这个产品中,我需要在浏览器插件里模拟用的各种行为,比如:点击。

click 事件前后发生了什么

最初我觉得点击嘛,能有啥问题,就直接广播 click 事件呗。结果发现并非如此。实际上,一次 click 背后,其实有一大套的逻辑:

  1. 移动到按钮上时,会依次触发 mouseovermouseenter 事件,前者冒泡,后者不冒泡
  2. 鼠标按下时,广播 mousedown 事件
  3. 如果此时其它元素有焦点,那么该元素会先失去焦点,并广播 blur 事件
  4. (下一节补充)
  5. 按钮获得焦点,广播 focus 事件
  6. 如果因此影响到 DOM,那么会等待 DOM 变更
  7. 如果鼠标没有离开按钮,按钮广播 mouseup 事件
  8. 最后广播 click 事件
  9. 在移动设备上,可能会有 300ms 延迟

其实(5)并不准确,每一次事件广播都是独立的 event loop ,所以上面每一步都可能产生 DOM 变化和其它次生变化,也可能导致一些操作或功能不符合预期。比如:

  1. 一个搜索框。输入后自动搜索,结果以 dropdown 形式展示在下面
  2. 点击 dropdown 里的条目可以跳转
  3. 搜索框 blur 时 dropdown 移除

此时,dropdown 里的条目可能无法点击。因为点击时,输入框先 blur,之后 dropdown 隐藏,接下来 mouseup 事件触发在别的元素上,于是不会有 click 事件。

最简单的解决方案,给 blur 事件加延迟,10ms 就够。

(关于上面的事件触发顺序,可以用下面的 codepen 尝试)

click 事件对 change 事件的影响

接下来,回到 QA 产品。

我真正踩的坑,是 change 事件没有按照预期触发。大家知道,Vue 组件通过 $emit('input') 可以更新绑定在 v-model 里的值。触发的时机一般通过侦听 DOM input 或者 change 来决定。如果 change 无法正确触发,那么 QA 产品自然而然就无法正常工作。

实际上,在上一节的 click 逻辑中,第(4)步可以展开为:

如果失去焦点的元素是 <input> 或者 <textarea>,则该元素会广播 change 事件。

模拟 click 动作的代码

最后,演示一下最终代码:

const options = {
  bubbles: true,
  cancelable: true,
  view: window,
};
let event = new MouseEvent('mousedown', options);
elem.dispatchEvent(event);
elem.focus();
event = new MouseEvent('mouseup', options);
elem.dispatchEvent(event);
elem.click();

相关链接

分类
npm

几步简单的操作解决 `npm audit` vulnerabilities

NPM 是 JavaScript 生态最重要的组成部分,我们的项目中会大量使用 NPM 安装第三方包(安装后就称为“项目依赖”)解决问题。这些第三方包也会带来他们的依赖,最终一个项目里可能安装成百上千个依赖。

有道是:没有完美的代码,代码里一定藏有隐患。所以用的依赖多了,其中有问题的概率也提升了,三五不时的,npm 就会提示我们:found N low/medium/high severity vulnerabilities

npm 提供命令 npm audit fix,理论上可以修复这些隐患,但在实际操作中,以我的经验来看,并不容易生效。我猜测可能是因为依赖间的复杂关系,想彻底解决并升级不太容易。所以我一般是这样做的:

分类
vue

Vue 2020 年路线图,Vue 3.0 计划于 Q2 发布

昨天 Vue 团队更新了 2020 年的路线图,里面包含了很多 Vue 3.0 的信息。建议大家一定要看原文,地址在:https://github.com/vuejs/vue/projects/6。下面我结合自己的理解翻译一下:

FAQ

问:3.0 啥时候能好?

答:请往后看。另外请注意,这些日期仅供参考,我们团队的首要目标是发布生产级别的高质量代码,不是赶 deadline。

问:3.0 里都有啥变化啊?

答:请自行翻阅最新的 RFC。另外,也要注意核心团队提交的 RFC 草案。

如果某个 RFC 里包含破坏性变更,那么里面一定会有“升级策略”章节,讨论迁移问题。

对于现在的 2.x 用户,我们会提供:

  • 迁移向导
  • 能够兼容 2.x 的兼容性版本(如果能兼容的话),并且对该升级的地方给出提示和升级建议
  • 命令行迁移工具
    • 自动升级能升级的代码
    • 不能自动升级的,扫描出来提示手动升级

问:我是新人,我现在该学 Vue 2.0 还是等 3.0?

答:如果你刚刚开始学习框架,那么应该开始使用 Vue 2。我们没有对 Vue 3 进行巨大的重新设计,所以大部分 Vue 2 知识仍将适用于 Vue 3。 如果你打算学习框架,没有必要等待。

如果你要为某个生产级别的项目选择技术栈:

  • 如果项目需要立即动工:我们仍然建议使用 Vue 2,以便获得完整的、框架级别的支持。 但是,也请留心 3.0 中即将发生的更改,不要使用将被移除的功能,最好也不要使用与 Vue 2 深度耦合的第三方依赖。
  • 如果项目可以等到 Q2 末:那我们建议你等等,用 3.0。

问:以后 2.x 会咋样呢?

答:接下来会有一个小版本(2.7)更新:

  • 将兼容的 3.x 功能反向移植回 2.x
  • 对 3.x 弃用的功能发出警告

这是 2.x 最后一个小版本,并提供长达 18 个月的 LTS(长期支持)。即使在 LTS 结束之后,我们也会继续提供重要的安全更新。

问:Vuex 方面有什么计划么?

答:一方面,我们正在开发Vuex(4.0)版本,其 API与 当前版本(3.0)完全相同,但与 Vue 3 兼容。我们力求向下兼容,让用户可以在 Vue 3 项目中继续使用现有 Vuex 代码。

另一方面,我们也在尝试新的设计,更多的利用 Vue 3 的响应式 API,也让 Vuex API 不那么冗长。 这个新版本暂定为“ vuex-next”,也就是 5.0。 眼下,我们只是在进行早期探索,最早也要到 2020 年第三季度才会发布。

2020 一季度计划

  • 3.0 SSR
  • 3.0 迁移
    • 升级向导(施工中)
    • 2.x 兼容版本
    • 迁移工具
  • 3.0 框架
    • router(施工中)
    • Vuex(施工中)
    • 测试工具(施工中)
    • JSX babel 插件(施工中)(我以为不会有这个东西了呢)
    • CLI
    • Devtools
    • 其它(虽然最后三个没标施工中,不过我觉得多半也是在施工中咯)
  • 3.0 beta
    • Q1 末发布!
    • 3.0 核心现在其实已经完成了,我们希望 API 到这个时候也能稳定下来。
    • 我们还需要更多的时间才能更新周边的库和工具。 如果您的使用场景对 router 和 vuex 没有硬性要求,这个时候就可以开始使用 3.0 了,但最好是非关键性应用程序。

2020 二季度计划

  • 继续之前未完成的 3.0 框架工作
  • 季度中,发布 3.0 RC
    • 冻结 API,不再有重大变化。进入 RC 之前,所有涉及到重大变更的 RFC 都要定案。
    • 全家桶能够和 3.0 版本协同工作。
    • 3.0 版本就绪,此阶段基本可用。仍然会有一些小错误和框架集成问题,在 RC 阶段都会慢慢被解决掉。
  • 3.0 发布管理
    • 回归测试
    • 自动化每晚发布
    • 正式确定版本生命周期
  • 3.0 IE11 兼容性版本
  • 3.0 官方正式版

2020 三季度计划

发布 2.7 版本

  • 反相迁移 3.x 功能到 2.x
  • 对 3.x 中弃用的功能发出警告
  • 2.x 最后的小版本,LTS
分类
nodejs

解决“[ERR_PACKAGE_PATH_NOT_EXPORTED]: No “exports” main resolved”

周末例行升级系统,今天打开项目,npm run dev,就报这个错误。检查代码,没变化,依赖也没变化。因为错误位置在 main.js,尝试给它加上 exports,无果。

Google 之,发现一个非常新的 issue:https://github.com/babel/babel/issues/11216,3天前,来自 @babel/babel 仓库,多半是了。

点进去一看,原来 node.js 从 13.10.1 之后,对 package.json 里的 exports 属性解读出现问题,继而导致 Babel 抛出错误。最简单的解决方法就是升级 Babel 到 7.8.4。

升级后问题解决。

分类
js

使用 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”)

分类
vue

迎接 Vue 3.0

思否(SegmentFault)去年引入了新的运营合伙人,大张旗鼓,计划重新梳理视频教程体系。第一个举措就是推出新系列:思否编程公开课。这个系列的优势在于结合社区运营的知识与一线讲师的实战能力:即社区寻找大家关心的话题,请相关领域的讲师来讲解,达到受众明确、内容扎实的目标。

很高兴能够得到邀请,做了一期《迎接 Vue 3.0》,我也趁机深入了解了一把 Vue 3.0。

Vue 3.0 原计划 2019 年发布,但是开发过程有些波折,一些早期的设计被放弃了,因为不够好;引入了新的更好的设计,于是延期至今,仍然是 alpha 版。在新的设计下,Vue 3.0 更快,快到不需要时间切片;而且还能基本保持与 2.x 版本的兼容。

至于新增的 Compisition API,其实是选修课,类似 Async 之于 Promise。有了它,我们能更好的复用代码、维护代码;不用它,也不耽误我们享受其它升级带来的便利。

其实看过很多 Vue 3.0 的东西之后,我越发感激开发团队为这个框架做得一切,越发感激开源文化为我们带来的新世界。

我备课一向认真充分,所以讲课的内容应该是现在最新最全面的,欢迎大家围观:【思否编程公开课】迎接Vue 3.0

分类
js

利用 Web Speech API 实现语音阅读试题(TTS)

孩子上小学一年级,寒假作业里有一项:每天做20个20以内的加减法。这个作业老师不直接布置,而是让家长负责,方式任意。那么,显而易见,这个工作就由我负责。然后我就顺手抄起 Vue 做了一个 Web App:http://mui.evereditor.com,源代码在:https://github.com/meathill/mui-teacher

然后老婆说,别的教学应用都会把题目念出来,这样比较有上课的感觉,问我能不能也把题目念出来。我之前大略有些印象,可以直接调用浏览器的原生 API 实现 TTS(text to speech),于是就想试试看吧。

使用“Web speech API”作为关键词,很容易找到 MDN 上的这个页面,继而了解到 SpeechSynthesisUtterance 这个类,接下来就简单了,直接参考 Using the Web Speech API 里的 Demo,就完成了下面代码:

doRead(index) {
  const content = this.$refs.line[index].textContent;
  const msg = new SpeechSynthesisUtterance(content.replace('-', '减'));
  speechSynthesis.speak(msg);
},

这里有三个注意事项:

  1. 系统会把 - 读成“至”,但我这里是加减的“减”,所以我要手动把它替换一下
  2. 从某个版本开始,发音必须由用户主动触发,即放在交互性事件里,不能在页面打开时自动读
  3. 发音效果跟浏览器有关,目前 Chrome 和 Safari 效果比较好

这样的 TTS,对于整段文字来说,效果一般,但是读一般的算式足够了,小朋友也挺喜欢。这个 API 还可以用作语音输入,不过考虑到模型的效果,没有尝试,想真正可用的话,还是用那些高级的在线版本吧。


图文无关,想出去玩……

分类
npm

解决新机不能 npm install 私有仓库的问题

前些天换了电脑,从 iMac 27 Retina 2014 late 升级到 iMac 27 2019。关于前因后果,我写在张大妈,感兴趣的同学可以看下:《肉山的电子产品 篇九:机不如新,人不如故——iMac 27 换新记》

换机器要搭环境。有两个选择:

  1. 整盘复制,利用 Time Machine 之类的工具
  2. 自己慢慢搭

考虑到老电脑已经工作了 5 年,经历了小十个系统版本,各种软件删装无数,里面的垃圾很多;再加上我的环境并不复杂,所以我决定自己慢慢搭。

然后遇到一个问题:npm install 会卡住,没有明显的提示信息,只有一段比较可疑:

The authenticity of host 'GitHub.com (1.1.1.1)' can't be established.
ECDSA key fingerprint is SHA256:abcdefg.
Are you sure you want to continue connecting (yes/no)? 

因为项目中用到一些私有仓库的依赖,所以我怀疑是它们的问题。于是我把私有仓库从 package.json 里移除,再安装,成功。看来猜的没错。

然后我手动 git clone 一个项目到本地,clone 的过程中,会看到上面的提示,手动输入 yes,记录目标机器的指纹,视它安全可靠。clone 完成。再把私有仓库依赖加回 package.json,安装,成功。问题解决。

原因:使用 npm 安装 GitHub 仓库作为依赖,实际上等同于使用 git clone 将目标仓库 clone 到 node_modules 里。而 git 协议依托于 ssh 协议。在新机器上安装时,因为没有访问过目标机器,本地不信任目标机器,而 npm 又没有对这个异常做处理,所以就会卡住,不上不下。

分类
js 技术

基于 @vue/cli 的项目配置 browserslist

前些日子虽然写了 最近折腾 @babel/preset-env 的一些小心得,但其实没有正确的理解和配置 browserslist,所以今天问题又来了。

分类
js

Nuxt.js 支持 core-js 3

Vue CLI 升级到 v4 之后,将内部的 core-js 依赖升级到 v3,关于 core-js v3 和 core-js v2 之间的区别,我在 最近折腾 @babel/preset-env 的一些小心得 里简单介绍过。

升级完 Vue CLI 之后,在调用 nuxt generate 生成静态页,就会报错,因为 Nuxt.js 默认使用 core-js 2。这个时候,如果 Nuxt.js 版本在 2.6.0 之后,就只需要修改 nuxt.config.js 里的配置,指定 core-js 的版本:

module.exports = {
  build: {
    babel: {
      presets({ isServer }) {
        return [
          [
            require.resolve('@nuxt/babel-preset-app'),
            // require.resolve('@nuxt/babel-preset-app-edge'), // For nuxt-edge users
            {
              buildTarget: isServer ? 'server' : 'client',
              corejs: { version: 3 }
            }
          ]
        ]
      }
    }
  }
}

参考文档:https://nuxtjs.org/guide/release-notes#v2.6.0

说起来 Nuxt.js,用它发布静态页比想象中复杂,如果你想快速掌握这个技能,不妨看下我的这本小书: