分类
技术

欢迎欢迎

欢迎来到我的博客,我是 Meathill,想了解我可以点 关于我

近期作品

欢迎留言点播各种内容。

分类
分享

蝉联思否 Top Writer

去年挺忙,OpenResty Showman 上线开催,虽然还有很多问题,但我想最后再努努力,看看有没有转机;加上其它一些事情,比如做直播、剪视频,其实知乎和思否都没有太坚持。断断续续、写写停停,没想到还能以最后一名之姿忝列思否 2021 Top Writer 问答组队末。

感谢思否认可,2022 年我一定继续努力,不仅继续贡献问答,也多写文章,多发视频,争取年底声望突破 25000(现在 14064)。

T恤和猫摆件均已收到,现在在老家,回去补拍照片。

写本文时,回看去年初获思否 Top Writer 时的激情,也不禁有些唏嘘。2021 年对我来说,颇有些失意。其中有我自己的问题,也有外界的因素,希望 2022 年这些问题不再困扰我,主业副业均能成长。也希望大家多多支持我,评论转发点赞,三连更欢迎。感谢感谢。

分类
招聘

代友招聘:北京/火币/高级前端

我们想招脑子清醒,编码靠谱,能独挡一面,解决技术难题的那种。

招高级到资深,40~50K,上不封顶。

不加班。

岗位职责:

  1. 负责公司产品前端的设计、架构、研发和测试工作;
  2. 参与技术方案评审、代码评审等;
  3. 负责技术调研、方案选型等;
  4. 与后端等上下游同事协作,提升用户体验,推动业务发展

任职要求:

  1. 有5年以上的前端开发经验,扎实的编程功底,良好的设计能力和编程习惯;
  2. 有大型单页面应用开发经验,熟练掌握React前端框架
  3. 熟悉前端工程化手段,具备工程审美,掌握常用构建工具,并具备搭建完整 CI/CD 流程能力
  4. 熟悉 HTTP、WebSocket 协议,具备简单的后端开发能力,能够利用开源软件搭建开发环境
  5. 具备 WebGL、Canvas、SVG、WebAssembly 等技术经验优先,有可视化项目经验优先
  6. 保持学习状态,具备探索精神,不断接受挑战

分类
hackthon

关于我作为前端报名 TiDB Hackthon 2021 然后被毫无悬念地淘汰这档事

2021 年年底,我偶然在推上看到 TiDB 举办 Hackthon 的消息。当时好像已经得知被金山办公优化的消息,所以准备给自己找点事情做,就报名参赛了。

作为一名全栈偏前端工程师,我对数据库的了解停留在基础安装、配置、优化上面。不太了解数据库生态,也不清楚各大厂牌之间的区别。我只在 teahour.fm 上听过 PingCap CTO 黄东旭的访谈,对他们只有一些粗浅的了解:

  1. TiDB 由豌豆荚内部项目孵化而来
  2. 他们试图给 KV 数据库增加类似 MySQL 的 API,让熟悉 MySQL 的用户能够无痛迁移到 TiDB 上
  3. KV 数据库天生便于拆分和分布式,更能适应未来的应用场景
  4. TiDB 是开源的,PingCap 提供基于 TiDB 的 SaaS 服务

Hackthon

2018年年底,我去北京参加了思否组织的 WeGeek 小程序 Hackthon。去之前,我以为 Hackthon 就是现场组队、现场立项,大家比拼脑洞、比拼协作能力。去了之后才知道,很多队伍都是长期合作的小团队,项目可能也做了很久,现场更多是准备答辩。

结果我们当然是陪跑。

这次的情况也类似。赛道提前一个月就公布了,大部分选题多半也是同期开始准备的。前面也说过,我本来就对数据库所知了了,也不熟悉 TiDB,自己没法想出好的选题,只能将希望寄托在被其他队伍招募上。

没成想这是个数据库 Hackthon,对前端的需求约等于零,于是我无队可投……求组队求到最后,我准备不行我就随便整个 Ghost+TiDB 或者 IndexedDB+TiDB 之类的项目随便搞一下,不要交白卷就行。还好最后 TiDB 送温暖,派了个后端来扶贫,勉强帮我凑齐了队伍,确定了选题。

参赛

TiDB 有个周边产品,叫 Chaos Mesh,可以进行混沌测试,测试服务器集群或者容器在各种干扰下会有怎么样的表现。功能不错但体验稍逊,我们就想改进它。

第一个想法是做游戏,我们设计了多个游戏方案,最终又都推翻了,因为 Chaos 其实有很多限制:

  1. 操作有失败的可能,不是每次都能成功
  2. 操作有很多,需要不同的表现方式
  3. 操作过程很长,多则几十分钟到数小时

眼瞅着比赛日一天天逼近,最终我们决定,先改进表单和拓扑图,毕竟纯前端用户界面类的开发,应该不会太难,有机会出成绩。

这是我第一次真正写 React 应用,还是花了不少时间学习和摸索。但是最难的点不在这里,而是,项目里这块实现的不算太好,还有些 Bug……这对我来说就有点超纲了,如果是 Vue+某个我熟悉的组件库,多半也能搞定;但是技术选型我也不熟悉、产品逻辑我也不清楚,确实做不完。

最终只勉强修了一个 GitHub 上的 issue,算没有交白卷吧。

比赛日

比赛日当天,我作为本组代表,到广州分赛区参与答辩。

TiDB Hackthon 的现场组织非常好:工作人员友善热情积极,现场环境舒适宜人,座位宽敞舒服,网速快,还供应三餐、下午茶和宵夜。签到之后就有纪念品,还能抽奖。纪念品里有我最看重的键帽,感觉组织者真的懂。

答辩进行得很顺利,因为项目做的不咋地,所以要说的东西也不多,5分钟时间比较宽松地把做过的工作都介绍了一下,然后坐到一边等待结果。

事实证明我想多了。这次 Hackthon 预留了一个多月的准备时间,更有很多参赛选手从上一届就开始筹划这次的项目。所以从理念、从设计、从实施,都远胜我们。

于是我们毫无悬念在预赛阶段就被淘汰了。

总结

虽然从前期报名到后面开始写代码的过程都很不顺利;立的项也被各路高手从各种方面碾压。但我必须得说,TiDB Hackthon 的组织能力真得很强,参赛选手水平真高,奖品也真给力,非常值得一来。而且仔细看 决赛项目,全都不明觉厉,如果能准备得更充分一些,相信能学到很多东西。

于是,今年我有两个打算:

  1. chaos-mesh 作为学习 react+typescript 的入门项目,争取在春节前把这次立项的功能完成,代码合并入主干(或具备合并的条件)。今年每个季度都能贡献一些代码。
  2. 慢慢积累对 TiDB 的使用经验,争取在年底前能形成一个可行又有价值的提案,然后参加 TiDB Hackthon 2022。
分类
开源 技术

为什么要投资开源项目

在群里聊起开源项目,说到现在投资者很喜欢投做开源软硬件公司,有同学问:“投资开源项目会有哪些形式获得回报呢?”我觉得一句两句说不清楚,所以写一篇博客作答。

开源软件的优势

先说软件。开源软件因其免费、开源的特性,越来越受到各个级别的公司、团队、开发者的欢迎。因为免费,我可以试试,好用就继续用,不好用就换一家;因为开源,如果使用过程中发现一些小问题,我可以自己修、自己适配,大大提升开发效率;同时,由于开源,用户可以更方便地对代码进行安全审计,安全性也会大大提升。

所以,相比于传统付费商业软件,开源软件的用户量一般要大很多,而且往往成长非常快。当使用人数上升到一定级别的时候,开源软件又具备了一些新的优势。

开源软件的用户量很大,使得开源软件的问题更容易被发现,也更容易被修复

会有很多用户贡献使用文档,也会有很多用户在各种问答平台贡献问题解决方案,也会有很多用户写各种教程。如今是视频年代,自然视频领域也有很多用户主动贡献

开源软件的初用成本很低,使得开源软件可以进入更多行业,获得更多使用场景,接触到更多的设备,尤其是新设备。比如,当苹果 iOS 取得成功之后,Google 立刻基于 Linux 开发了 Android 系统,然后理所当然的击败了闭源的 Symbian 和 Windows,成为移动双雄之一,并在移动互联网领域彻底接管了微软的系统软件地位。物联网方面,更是 Linux 各种发行版一枝独秀。

开源软件的用户可能比作者更厉害,使得开源软件可以实现超越式发展。比如当年的 Backbone,它最初的成功得益于 API 设计,但是本身代码质量一般。后面有了大量用户贡献生产实例和改进代码,慢慢在代码质量上也远超一般项目。

开源软件的版权属于全世界,很多厂商都愿意投资开源软件,因为不用担心被竞争对手用专利限制;从国家层面更是如此,开源就不怕卡脖子。《“十四五”软件和信息技术服务业发展规划》解读 中重点强调了开源生态。

开源硬件也差不多,现在不光是开发板,机械臂什么的也都有开源项目,动手能力强者照着完全可以 DIY 出来。对于我国这样有完整工业体系的制造业大国来说非常有优势。

投资开源产业的价值

上面简单梳理了开源软硬件对比传统收费软件行业的优势,接下来深入分析一下投资者,包括投身开源软件的创业者能从中获得哪些特有价值。

获得事实标准

前面说过,开源软件发展的更快,触及行业更广。所以开源软件更容易形成事实标准。比如,现在服务器上装的,几乎都是 Linux;当年 RIA(Rich Internet Application,指功能更强大效果更出彩的网络应用)的开创者、商业软件 Flash,遭遇几乎全开源的 HTML5 挑战,如今坟头草一人多高,很多新晋前端开发者听都没听说……

取得行业标准之后,再想让行业朝着自己喜欢的方向发展,就容易很多。比如 Chrome,如果你对比一下 MV2 阶段的 Browser Extension API,会发现,它基本就是照搬自 Chrome Extension API。结果就是,Safari、Firefox 的扩展也必须兼容 Chrome 规范;Chrome Extension MV3 推出后,其它浏览器开发厂商都要迅速跟进。

如果你需要某个功能,而规范还不支持,那也没关系。你完全可以调整产品线的优先级,让你的需求优先被满足。

掌握制定规范的权力

有时候,事实标准没有那么好拿到,但是只要掌握一定程度的市场,就可以参与规范的制定。能够参与制定规范,就能提前布局产品的设计和生产,从而在后面的竞争中处于优势。相信大家都记得当年的 4G、5G 规范之争吧?

开源不挣钱,但开源软件公司可以挣钱

开源并不是做慈善,做开源软件的公司当然可以赚钱。事实上,目前成功案例很多。比如红帽,比如 Automatic(WordPress 就是他们家的产品,本博客就是用 WordPress 搭的),还有 MySQL、PingCap、F5 等等,不胜枚举。

这些公司的商业模式大约是:

  • 打造一款开源软件,利用开源软件的优势赢下市场
  • 提供基于自家开源软件的服务,比如技术支持、定制开发、咨询培训等等
  • 除了开源免费的社区版,还有给付费用户的高级定制版,一般来说功能更丰富、性能更好

所以投资开源软硬件企业,完全不用担心投资回报。

不惧巨头垄断,突破行业壁垒

开源软件挑战巨头取得胜利的例子更是举不胜举。跟众多“如果 BAT 要做,你怎么办?”的行业比起来,开源软硬件天然不怕专利限制,也不担心巨头在同领域竞争。开源软件即使失败,也是因为自身质量不好,或者瞄准的领域营养不足,或者败于其它开源项目。

国家政策扶持

前面说了,作为 IT 行业的后来者,我们国家要想追上行业壁垒森严的先行者,开源是我们的第一选择。所以未来很长一段时间,开源都会是国家关注且支持的领域。

总结

时代不同了,以前看起来只能用爱发电的开源软硬件也可以一边造福全人类一边造福自己家;开源从业者也可以不靠捐款过活。当然开源也有不少问题,限于篇幅,我就不在本文里讨论了。

上面都是我从一个行业爱好者的角度,做的民科式总结。纰漏错误都有可能,欢迎指出、讨论。

分类
技术

职业生涯的新转折点,2021 技术总结

昨天整体总结了 2021 的职业、生活、副业等方面。今天重点总结一下去年技术方面的学习、分享,再规划下 2022 年计划。

分类
生活

Ciao, 2022

又是新年元旦,按照惯例年终总结。

分类
extension

使用 Chrome extension 操作 CodeMirror

我们知道,浏览器扩展(Chrome extension)可以通过 content script 在目标网页里执行代码。它和目标页面的 JS 处于不同的沙箱,但是共用一套 DOM。所以,content script 无法访问目标页面的 JS 变量,但是可以修改目标页面的 DOM、侦听 DOM 上的事件、或者触发 DOM 事件。

在这种情况下,要操作 CodeMirror 的时候会遇到一些问题。为了性能考虑,CodeMirror 不会把所有代码都渲染出来,而是会按需渲染。比如代码总共 1k 行,但是当前窗口里只能看到 40 行,那么 CodeMirror 就会渲染当前的 40 行,和前后大概各 20 行(方便快速滚屏),总计约 80 行。

换言之,如果无法接触到目标页面的 JS 环境,单纯从 DOM 里无法获取到全部代码,也很难修改 CM 里的代码。于是我们就需要一些特别的技巧——这些技巧在操作其它类似的类库时也会用到,所以写篇博客分享下。

解决方案:向目标网页注入 <script>

此时,我们可以通过创建 <script> 的方式向目标网页注入一段 JS。这个 JS 的执行环境也是目标网页,所以可以访问到目标网页 环境下的 JS 变量。

CodeMirror 会把实例添加到容器节点上,所以我们可以先找到容器节点,即 div.CodeMirror;然后访问它的 CodeMirror 属性找到 CM 实例;最后进行操作。

为了方便 content script 和 CodeMirror 交互,我们必须搭建一个桥梁,也就是下面这段代码:

const cms = document.getElementsByClassName('CodeMirror')
for (const dom of cms) {
  // 这个 textarea 被 CodeMirror 视图所取代,是隐藏的。我们利用它传递代码
  const textarea = dom.previousElementSibling;
  const cm = dom.CodeMirror;
  // 给 CodeMirror 添加清空功能,可以通过在 dom 上广播事件触发
  dom.addEventListener('clear', () => {
    cm.setValue('');
  });
  // `changes` 是 CodeMirror 的事件,在代码改变时触发,这里我们通过侦听它,并且将代码同步到 `textarea` 的方式,方便 content script 访问
  cm.on('changes', cm => {
    if (texture.value === cm.getValue()) {
      return;
    }
    textarea.value = cm.getValue();
  });

  // 为初始化
  textarea.value = cm.getValue();
}

因为我执行 content script 的时机总在页面完成初始化之后,此时所有 CodeMirror 都已就位。如果你要在别的时间执行,可以做一些调整。

分类
vue

Vue3 < script setup > + TypeScript 笔记

近期把一个老项目(Vue 组件库)从 Options API 风格重构成了 <script setup> 风格 + TypeScript,写篇博客记录下坑和心得。

注:这篇博客只记录超出我踩过坑和不熟悉的内容,符合直觉、看了文档写完一次过的部分,我就不记录了。

0. Composition API 与 <script setup>

Vue 3 给我们带来了 Composition API,改变了之前 Options API 使用 mixins 继承和扩展组件的开发模式。这种模式让前端在开发时复用组件、改进组件变得更容易。具体改进我在本文里不再详述,大家可以看下 Vue master 的视频教程:Why the Composition API

之后尤大又提出了 script-setup rfc,进一步改进了使用 Composition API 的体验。简单总结下新提案:

  1. 使用 <script setup> 取代 setup(),里面定义的所有变量,包括函数,都可以直接在模版中使用,不需要手动 return
  2. 可以使用顶级 await
  3. <script setup> 可以与其它 <script> 在同一个 SFC 里共存

实际体验之后,我觉得这个提案的确能节省不少代码,不过也会增加一些复杂度。

1. 导出 name 等非模版属性

<script setup>setup() 的语法糖,主要目的是降低 composition API 的代码复杂度。它直接向模板暴露整个 <script setup> 的上下文,所有定义过的变量自动导出供模版使用,这样我们就不需要手动一个一个 return 了。

所以它就不适合导出其它非模版属性,比如组件名称 name。如有需要,可添加一个普通 <script> 节点,用常见的 export default 导出初始化对象。如下:

<script>
export default {
  name: 'SomeVue3Component',
};
</script>

<script setup>
import {ref} from 'vue';

const foo = ref('bar');
</script>

2. defineProps/defineEmits

要在 <script setup> 里使用 props(传入参数)和 emit(广播事件),需要使用 defineProps()defineEmits() 定义。但要注意,这俩东西其实是编译宏,并非真实函数,不能把它们当作函数来使用。所以也不需要 import,当它们是全局函数直接用就好。同时记得修改 .eslintrc.js 把它们添加到 global 里。

最简单的使用方法如下,以前的 props 定义规则可以沿用。

const props = defineProps({
  foo : {
    type: String,
    default: 'bar',
  },
});
const emit = defineEmits(['change']);

要配合 TypeScript,通常需要使用 withDefaults()props 生成默认值。这也是个宏,不能当函数用,也不能使用当前环境里的变量。改造后的代码如下:

interface Props = {
  foo: string;
};
const props = withDefaults(defineProps<Props>(), {
  foo: 'bar', // 'bar' 不能是变量
});
const {
  foo,
} = toRefs(props);

const emit = defineEmits<{
  (e:'change', value:string),
}>();

这个地方的设计相当不完善,我不知道 Vue 团队会如何改进这里。比如,我们不能使继承 interface,然后再初始化 props,因为继承是常规 ts 语句,而 defineProps 是编译宏,两者的工作环境不一样。而因为无法使用变量,我们也无法将父级组件的 props 混入本地 props。于是复用组件又变得麻烦起来。

耐心等待吧。

3. 使用 undefined 初始化对象

定义 props 问题真不少。有一些参数是可选参数,不一定要定义,也不一定要用到;但是使用 ts 定义时,即使如此,也要初始化他们,可以传值为 undefined,不然 tsc 可能会报告错误:变量可能为空。之所以用 undefined,而不是 null,则是因为 TypeScript 认为 null 是独立类型。

即:

interface Props {
  foo?: string;
}
const props = withDefaults(defineProps<Props>(), {
  foo: undefined,
});
const {
  foo,
} = toRefs(props);

// 接下来才能正常使用
const bar = computed(() => {
  return foo.value || 'bar';
});

4. 使用的变量未初始化错误

有些函数,可以传入为定义变量做参数,但是函数自身的签名没有很好的体现这一点,就会报错。比如 setTimeout,以下这段代码就会报告:

type timer = ReturnType<typeof setTimeout>;
let timeout:timer;
function doAction() {
  clearTimeout(timeout);
}

我确定这段代码没问题,但是 tsc 不给过,只好给 timeout 加上 ! 修饰符,即:let timeout!:timer。这样就可以了。

5. Vue SFC 文件类型定义

为让 tsc 能够正确理解 Vue SFC 格式,需要创建这个描述文件,并且告诉 tsc 加载这个描述文件。

declare module "*.vue" {
  import { defineComponent } from "vue";
  const component: ReturnType<typeof defineComponent>;
  export default component;
}
{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "strict": true,
    "jsx": "preserve",
    "moduleResolution": "node",
    "suppressImplicitAnyIndexErrors": true,
    "allowSyntheticDefaultImports": true,
    "allowJs": true,
    "resolveJsonModule": true,
    "esModuleInterop": true,

    "paths": {
      "@/*": [
        "./src/*"
      ]
    },
    "lib": ["DOM", "ESNext"]
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.vue",
    "test/**/*.ts"
  ],
  "exclude": [
    "node_modules"
  ],
  "files": ["src/types/vue-shims.d.ts"]
}

6. 待解决问题

  • 因为第一次使用 TypeScript,很多不熟悉的地方,tsc 大量报错。但是由于使用了 vue-loader,所以 tsc 报告的错误行号基本都不对,很难查找问题所在,浪费了大量时间。
  • 导入了 moment locale 文件,但是缺少定义,不知道该怎么声明某个对象属于某个接口。可能要通过前面类似定义 Vue SFC 的方式。

7. 项目地址

对项目感兴趣,或者寻求范例的同学可以在 GitHub 上找到这个项目:meathill/muimui-ui: A simple vue 3 ui components suit. (github.com)

分类
前端工具链

复盘近期升级工具链的过程

公司希望我提升产品在移动端的体验,于是我就打开了 Lighthouse,然后看了眼代码,发现有几个问题:

  1. 移动端和 pc 端一起编译,共用一套编译配置
  2. 目标平台包括 IE11
  3. 还在使用 babel@6
  4. 全量引用 babel-polyfill
  5. CDN 没有完全 http2

最后一项联系运维同学解决就好了,我开始尝试解决其它几项。

0. 目标

  1. 移动端和 pc 端采用不同的编译配置
  2. 尽量用最新的工具链
  3. 兼容 IE11

1. 尝试升级到 babel@7

babel@6 停留在 4 年前,存在着各种各样的问题,包括本身的实现和兼容代码都有问题。我希望先升级到 babel@7,以便使用 useBuiltIns: 'usage',减少打包后的代码量。

实际结果很不理想。升级后代码膨胀了很多,经过研究,发现原因是 core-js 策略比较保守,所以多引用了很多兼容性代码,远远多于 babel@6。

比较详细的分析可见:babel@6 升级到 babel@7,兼容性代码膨胀的原因

2. 尝试从 webpack 迁移到 esbuild

接下来尝试离开 webpack + babel 体系,使用 esbuild。先打包一套 es6 代码,供移动端和现代化浏览器使用,然后再生成一套兼容性代码,给 IE11 使用。

相比于 webpack + babel,esbuild 的优势是统一+快。按照官网统计,它的速度可能是 webpack 体系的 100 倍。它的问题是只支持 ES6,也不支持扩展,不像 babel 那样,加个插件就能什么标准都支持。

这次尝试我也失败了。原因倒不是因为 IE11,而是原来的架构跟 webpack 深度耦合,比如:lazy-loading、分割模块、i18n,等。所以迁移成本非常高,发现情况不对之后只好止损。

3. 使用 esbuild-loader 替换 babel-loader

webpack 暂时不能放弃,那就用 esbuild-loader 吧,毕竟要快很多。于是就遭遇到上面说的规范问题。

  1. esbuild 只支持标准规范,原本可以通过 babel plugins 支持的特性现在都不支持了,需要改回去
  2. 一些写的不规范的代码,比如变量先使用后定义,在 var 能跑,但是在 letconst 阶段就跑不了,也得改回去

经过一段时间的折腾,这个尝试还是比较成功。构建时间缩短了一半以上。

4. 尝试用 babel-standalone 编译 IE11 代码

上面一步只算完成一半,因为还不支持 IE11。接下来我们计划使用 babel-standalone 实时转译代码。

babel-standalone 提供在 JS 运行时里实时编译代码的功能,比如做一个在线编辑器,或者应用里内嵌了 V8 等运行时,希望提升兼容性,就可以用这个工具。

这个尝试失败了,因为 IE11 的 JS 运行时效率太差,我们生产级别的 JS 直接就卡死了,且优化不能。

5. 尝试用 SWC 在编译时生成 IE11 代码

SWC 跟 esbuild 比较类似,是另一套新生态尝试,基于 Rust 开发,也能提供极高的编译效率,且支持 ES3。

我希望能把需要在 IE11 运行的几个工具组件单独编译一套,然后根据浏览器入口加载不同的 JS。在开发阶段,则通过 swc-loader 提供兼容性代码。

重构工具链的过程也挺费时,现有构建工具链实在是反人类。不过最终还是完成了,真正使得这次尝试又失败的原因是 SWC 本身的 bug。几个无法 work around 的 bug 导致我只能放弃这个方案。

详见:初试 SWC(Speedy Web Compiler)

不过,SWC 最新版本已经修好了我提交的 bug,大家有机会试试吧。

6. 用 babel 在编译时生成 IE11 代码

最后回到 babel+babel-loader。为了体积考虑,我决定继续使用 babel@6。

然后我发现,目前的代码架构实在是,哎……相信看过 Git 操作特定分支的小技巧 一文的同学都明白我在痛苦什么。

所以又跟项目结构搏斗许久,终于基本完成了这次重构。

7. 结果

这次重构达成了几个目的:

  1. 移除了全量 babel-polyfill,只有 IE11 继续加载
  2. 大部分模块用 ES5 构建,但是不加载 polyfill
  3. 少数模块,不需要兼容 IE11 的使用 ES6 构建

具体数字没机会统计,就不列了。

分类
前端工具链

初试 SWC(Speedy Web Compiler)

SWC 是一个用 Rust 写的编译工具,功能跟 babel 很类似。它的优势在于速度,按照官网所说,在单核上它的速度是 babel 的 20倍,四核上是 babel 的 70 倍。

我最近尝试升级工具链,第一步是使用 esbuild-loader 替代 babel-loader,比较顺利。但是 esbuild 只支持 ES2015,为了支持 IE11,我们还需要再转译一次。这次我想试试 SWC。

安装与配置

安装 swc 很简单。我打算 build 的时候用 @swc/cli 转译;debug IE11 的时候把 swc-loader 套在 esbuild-loader 前面输出。所以需要安装三个包:

nom i @swc/core @swc/cli swc-loader -D

接下来添加配置文件 .swcrc

{
  "sourceMaps": false,
  "module": {
    "type": "umd"
  },
  "minify": true
}

默认目标平台 target: 'es5',不需要再写。其它语言选项也走默认即可。

SWC sourceMap 支持三种配置:truefalseinline,意思一看就明白。但目前 bug 比较多,即使设置 sourceMap:false,也只是不生成 sourcemap 文件,对应的链接标记仍然会生成。

module.type 有另一个 bug:即使目标平台是 ES5,它也会使用 ESM 引入依赖,导致语法解析出错。比如代码中使用了 async function,SWC 转译时就需要 import regeneratorRuntime from 'regenerator-runtime'。所以上面我配置 module.type"umd",一方面避免错误的 ESM import,另一方面可以通过全局方式引用这些库。

minify: true 表示我们希望 SWC 转译代码时顺便把它们压缩一下。官方文档说这个功能还不是很完善,不过我暂时没发现明显的问题。

swc 游乐场

SWC 官方提供预览网站:SWC Playground – SWC。我们可以把源码贴上去,查看编译结果。也可以调整选项,看看不同选项对结果的影响。

问题

把 SWC 放进工具链的过程还算顺利,不过接下来使用的时候却遭遇了不少问题。

首先就是上文说的 ESM 导入的问题。默认情况下 SWC 会 import 'regenerator-runtime',这个问题必须解决。考虑到这个代码特征比较明显,起初,我打算直接字符串过滤掉。后来翻了文档,又在 playground 里尝试了一下,发现 UMD 应该可以解决问题。

然后是语法问题。下面这段代码 SWC 编译错误:

// 输入
let a = 10;
for (let b = 0; b < a; b++) {
    let c = 0, b = 10, d = 100;
    console.log(b);
}

// 输出
var a = 10;
for (var b = 0; b < a; b++) {
    var c = 0, b = 10, d = 100;
    console.log(b);
}

这段代码其实有点意思,我看了很久才找到问题:let 声明变量时,for () 相对于下面的代码块,是上级 context;而 var 时,它们处于同级 context。所以下面这段循环只会运行一次,而不是像上面一样,执行 10 次。

(这段代码很能考察 letvar 的不同,我准备把它加入我的面试题库。)

还有其它一些语法问题,因为上面这个问题无法绕过,意味着使用 SWC 这条路不可行,所以我也没有深入研究,就不一一列举了。

总结

SWC 用户量太小,未知 Bug 很多;开发团队不大,又是用 Rust 写的,也给想贡献代码的前端社区带来很大阻碍。所以目前还难以应对大型项目、工业级别的需求。

不过它的速度的确很快,对改进项目构建速度有很大帮助。希望那些 bug 能尽快修复,我们可以早点把它应用到产品当中。