分类: 前端工具链

  • Turbopack 发布后的各方反应:Vite/Webpack/围观群众

    Turbopack 发布后的各方反应:Vite/Webpack/围观群众

    上周整理了一下 Turbopack 发布后的各方反应,以便让自己和各位同学下一步做决策的时候能有所参考。接着忙碌的一周过去,我发现博客这周还没更,于是赶紧来补一下。

    0. TL;DR

    1. Turbopack 的宣传语更多还是“宣传”,实际效果提升并没有那么巨大。
    2. Turbopack 目前只支持 Next.js + React,且没有建立健全插件机制,所以没有生态可言,如果我们的主要开发环境不一致,可能暂时用不到。
    3. Turbopack 基于 Rust 开发,性能提升主要来自 SWC,所以有时间、有兴趣的话,直接使用 SWC 替换编译工具就好。
    4. 非目标开发者群体建议先不考虑迁移。
    5. Turbopack 对于打包过程有不小的改进,值得持续关注。
    6. 如果你日常使用 React,且想在新技术出现时斩获先机,早点动手也挺好。

    2022-10-26

    首先,10月26日,Vercel 发布了 Turbopack,自命 Webpack 继任者,号称速度比 Wepback 快 700 倍,比 Vite 快 10 倍。

    这些是官推亮点:

    • ~700x faster than Webpack -> 比 Webpack 快 700 倍
    • 10x faster than Vite -> 比 Vite 快 10 倍
    • Native incremental architecture built with Rust -> 基于 Rust,具备原生可增长架构
      ◆ Support for React Server Components -> 支持 React 服务器组件
      ◆ Support for TS, JSX, CSS & more -> 支持 TS、JSX、CSS,以及更多

    尤大观点

    很快,Vite 作者 @youyuxi 就跟进表达反驳:

    “10x faster than Vite” isn’t entirely accurate

    “‘比 Vite 快 10 倍’并不完全准确”,他说。

    然后他进一步介绍了自己的理由:

    1. Vite’s default React HMR is still babel-based, while Next/turbopack use swc/rust-based transform, so the HMR performance difference is a bit of apples to oranges.

    因为 Vite 默认的 HMR 仍然基于 Babel,而Next/Turbopack 使用的是基于 Rust 平台的 SWC,所以这里 HMR 的性能差异是关公战秦琼(这个 thread 后面还有若干理由,我就不一一翻译了):

    Vite can switch to the swc/rust transform if necessary, we currently chose not to do that because adding swc to the deps list is extra weight, and even without it HMR is fast enough.

    Vite 当然可以在必要时切换到 swc/rust,但 Vite 开发团队并不想这样做,因为即使只用 Babel(不那样做),HMR 也足够快。使用 SWC 替换 Babel 只是平添开发负担。

    1. turbopack does address Vite’s request congestion issue on large apps. On Vite side we are also exploring how we can solve this via upcoming browser features like web bundles.
    1. Turbopack 确实解决了 Vite 在大型应用上的请求拥塞问题。我们 Vite 团队这边,也在探索如何解决这个问题,比如,是否可以借助即将推出的浏览器功能(Web Bundles)。

    In the long run we may also consider using turbopack under the hood to replace esbuild/Rollup (where suitable), due to its strong caching capabilities.

    1. 从长远来看,由于 Turbopack 强大的缓存能力,我们也会考虑在底层使用 Turbopack 来替换 esbuild/Rollup。如果合适的话。
    1. I’m glad vercel is investing its resources into a proper native-speed successor to webpack (as it should), and that our work on Vite has pushed this to happen. It’ll be interesting to see how it evolves – my hope is it can be made truly framework agnostic, not Next-first.
    1. 我很高兴 Vercel 愿意投入资源,打造 Webpack 的继任者,并着力提升它们的速度表现(应该如此),我们在 Vite 上的努力也达到了这样的目的。我对它们未来的演变很感兴趣——我希望它不要和框架捆绑在一起,不要 Next-first。

    Sean Larkin 观点

    (Sean Larkin 是 Webpack 的核心团队成员。目前好像在 LinkedIn 工作,好像。)

    Sean Larkin 也表达了自己的观点:

    Few thoughts: 几点想法:

    1. Love the innovation but it looks moderately SWC powers and wish there was some more callout there.
    2. Disappointed how entangled this is with next/turborepo. But Vercel needs to make.
    3. The path of migration from webpack for the avg user will be hard.
    4. I like that the dev server is still bundling modules because ESM slower than raw ESM. I need to peel back more layers to try and get a standalone implementation working.
    5. It’s all in alpha so we need to still look at the broader view but I hope we’re selling less going fwd
    1. 我喜欢创新。但看起来,Turbopack 更多是借力于 SWC,希望对方能表达得清楚一些。
    2. 我对 Turborepo 与 Next 的绑定程度感到失望。但是 Vercel 需要挣到更多的 (天使资金),没办法。
    3. 普通用户想 Webpack 迁移过去,会很艰难。
    4. 我倾向于开发服务器上的模块仍然是打包后的,因为 ESM 比原始 ESM 慢。我需要剥离更多层,才能让独立的实现工作。
    5. 目前它还处于 alpha 阶段,所以我们要看开一点,但我仍然希望,销售手腕少一些,好好干开发。

    2022-10-27 ~ 2022-10-28

    这段时间,@youyuxi 觉得 Turbopack 的数据有问题,因为 Vercel benchmark 的设计有问题,于是他尝试调整测试方式,给出了一些数据。不过后来他删除了这些推文,因为数字不太对。

    2022-10-27

    Vite 核心成员之一,@antfu7 在知乎上表达了自己的观点:

    他认为 Vite 更吸引人的是其插件系统,和构建在上面的生态系统。这样才能将改进快速带给其他领域。Turbopack 方面,静观其变吧。

    https://zhihu.com/question/562349205/answer/2733040669
    如何评价Vercel开源的使用Rust实现的Turbopack? – Anthony Fu的回答 – 知乎

    2022-10-29

    经过几天实验,Vite 作者 @youyuxi 发表了新的推文,新推文介绍了他进一步比较 Turbopack 和 Vite 之间性能之后,得到的新结论。

    I updated the vite vs next/turbo benchmark by using swc to transform React refresh for vite. To avoid confusion, I have deleted the previous two tweets with outdated numbers.

    Latest numbers:

    • root: vite 338.2ms / next 334.6ms
    • leaf: vite 141.8ms / next 84.4ms

    我通过搭配 Vite+SWC,重新评测 React 转换之后,更新 Vite vs Next/Turbopack 的基准测试。为避免混淆,我删除了前两条带有过时数字的推文。

    最新的对比数字:

    • 根:Vite 338.2ms / Next 334.6ms
    • 叶:Vite 141.8ms / Next 84.4ms

    The swc transform helps a lot in the root component case because the file is big, and previously cost 300ms in babel transforms alone. This makes vite almost exactly the same speed with turbopack.

    SWC 转换在根组件时候提供了很大帮助。因为文件很大,以前仅在 Babel 中转换就要花费 300 毫秒。替换之后,Vite 的速度几乎与 Turbopack 完全相同。

    Interestingly, in leaf scenarios turbopack is still 69% faster – this entails that Vite HMR actually caught up with turbopack as the file gets bigger, potentially scaling better in larger apps.

    有趣的是,在叶子场景中,Turbopack 的速度仍然更快,提升约有 69%。——这意味着随着文件变大,Vite HMR 实际上赶上了 Turbopack,在较大的应用程序中可能会有更好的扩展性。

    测试用仓库:https://github.com/yyx990803/vite-vs-next-turbo-hmr

    Because we are now testing the HMR of the same framework with the same set of transforms, the result is more relevant for other frameworks compared to previously posted numbers.

    现在,因为我们使用相同的转换集测试同一框架的 HMR,与之前发布的数字相比,结果的相关度更高。

    Just to add a bit of context – in this benchmark, even before using swc for React transforms, turbo is only 2x compared to vite (compared to marketed 10x). So the difference wasn’t that drastic to begin with.

    顺便讲点相关前提:这套基准测试,甚至在替换使用 SWC 转换 React 之前,与 Vite 相比,Turbo 也会仅 2 倍(他们市场宣称要快 10 倍)。所以其实一直以来,差异都没有那么大。

    2022-10-31 及之后

    本文主体整理于 10 月 31 日,之后 @youyuxi 还在孜孜不倦的跑 benchmark,与各路支持、反对、吃瓜者或讨论或争论。但是他的观点变化不大,所以我也就偷懒不继续翻译了,相信大家看完都能做出自己的判断。

    N 神有一些观点,摘录于此:

    vite 为了提升速度,利用了浏览器的特性,在 dev 阶段不打包而用 esbuild 预编译 esm 小包投送给浏览器, 在 build时候用 rollup 再打包。这样 dev 就会非常快(因为无需打包),但写插件就很分裂,要考虑 dev 和 build 两种情况。并且理论上如果依赖小包过多会肯定会遇到浏览器并发瓶颈减慢速度。

    按 Turbopack 的说法,他 dev 和 build 同样走打包流程,还能比 vite 快,那么铁定是更好的。只是太早期了,现在还没有开放插件生态。而且自命 webpack 继任者没毛病,看 github 上就是 webpack 作者主力搞的。

    但另一方面如果未来 non-bundler 成为主流,前端不再需要打包。turbopack就没用了。vite 抛弃 rollup,build 也走 dev 流程就更美了。

    https://twitter.com/nshen121/status/1587333382362763264

    SWC 作者和 Webpack 作者

    这二位目前都在 Vercel 工作,可能受公司公关限制,都只转发了官推,并没有发表更多意见。


    总结

    Turbopack 可能不如官方所说的那么好。它的确带来了 HMR 的提升,但代价是不健全的插件机制和生态环境,以及难以被前端团队掌握的 Rust 平台。

    未来一段时间,我们还要继续坚持在 Vite 平台上。

  • 浅尝 Monorepo

    浅尝 Monorepo

    最初听说 Monorepo,是群里的同学问我是否了解 lerna,我还真没听说过,于是去学习了一下。简单来说,就是把好多个软件放在一个大型仓库里一起管理。

    0. Monorepo 简介

    为方便灵活使用,我们一般会把软件包拆散,每个小包只负责某个特定功能,通过组合完成复杂功能。这个时候我们有两个选择:

    1. 每个包放在独立仓库,独立管理、独立发布,然后通过 NPM 等包管理工具安装互为依赖。
    2. 所有包放在一起,统一管理,直接内部引用。即 Monorepo。

    我之前在 OpenResty 一直使用前者,而很多开源项目都选择后者,比如 vue-cli。它的仓库里包含了 vue-cli 主体和大量插件,还有测试套件等。

    这样做有几个显而易见的好处:

    1. 这些软件包通常高度耦合,彼此之间功能关联紧密。A 软件 X 版本依赖 B 软件的 Y 版本,B 软件的 Y 版本又依赖 C 软件的 Z 版本。统一管理切换环境更轻松。
    2. 统一管理依赖,既能减少磁盘占用,也可以保证开发环境统一。
    3. 没有编译、没有黑盒,所有源代码对项目成员公开,遇到问题分析调试都容易很多。

    我觉得在 Google 这样的技术流公司这么搞没问题;在崇尚奉献和高参与质量的开源项目里这么搞也很好,但是对很多技术水平一般,自我要求很低的软件公司这么搞就是乱来了。

    1. 问题

    以我曾经短暂工作过的XX办公(为保护当事人隐私我隐去了“金山”二字)为例,使用 monorepo 带来了几个未曾预料的问题:

    1.1 版本管理混乱

    所有产品代码都放在一个仓库里,数个不同的团队同时进行开发,每天会产生大量的 commit。负责合并分支的人既不懂业务也不懂版本管理,基本就是点一下合并按钮,于是大量未经 rebase squash 的 commit 被用 merged 方式合并入主干。整个提交历史混乱不堪毫无价值。

    同时由于采取 merge 方式,仓库里存在大量分支,git 无法判断版本之间的先后关系,也无法自动解决冲突,于是几乎每两三天就遇到冲突无法合并,需要开发人员解决冲突。

    1.2 代码质量参差不齐,且互相影响

    monorepo 好的一方面是代码对大家公开,所有人都可以学习其他人的写法,互相帮助解决问题;坏的一方面是坏的代码影响的也是整个项目仓库。

    有些同学用 VS Code,这本身不是问题,但 ta 们不研究配置、不研究插件,于是代码中很多低级错误——甚至因为低级错误太多,反而不会报错。比如有个事件侦听函数,参数传进来是 e,但是函数体里用的是 event。结果竟然没有报错,也能通过测试(人肉)。我找到开发人员让他改,他发现竟然有一个全局 event 变量,不知道是谁在哪里带进去的……

    【2022-09-20 更新】这里还真触及到我的知识盲区。这是早期 JS 的兼容性设计,即事件触发时 window 上也会有个全局变量 event,主要方便大家乱来。没想到救了 XX 办公。

    1.3 技术栈升级困难

    架构升级我就不谈了,这东西见仁见智,也不是能经常干的事情。这里说的主要是工具链。

    如,eslint、错字检查、安全性检查。好比说,我发现部分代码存在风格问题,希望引入 eslint 检查,并且把 eslint 加入到集成环境里。按说这个想法并不复杂,这些工具都不影响整体架构,只需要外挂到 pre-commit 之类的钩子上即可。但此时必须征得整个开发团队几百号人的同意,那就不是一句话能解决的。

    于是打开 WebStorm,海量的臭气(smell)扑面而来,难以直视。

    1.4 代码互相耦合

    原本 monorepo 只是方便大家阅读代码、维护代码。但是难免会有人滥用,直接引用、导入别人的代码,或者把自己的代码写入别人的目录里。

    比如我想优化构建过程,希望能拆成 ES6 和 ES5 两个版本(因为厂里明确要求支持 ES5 的只有文档、表格、幻灯片)。然后我发现,虽然名义上大家各自维护自己的目录,但实际上跨目录引用比比皆是。简单按照目录区分基本不可能。

    2. 总结

    当然,我数落这些问题,不是想说 monorepo 不好。分散项目独立管理也会有别的问题,比如技术栈不统一、集成困难、人员变更后难以交接继承等。每种技术都会有适合的场景,各位技术决策者也应该从自己的团队实际出发,选择最适合自己的技术选型。

    现在技术领域各种声音分外吵杂,很多公司会把技术方案作为一种宣传方式,名为推广技术实践,实则宣传自己的团队和品牌。如果不加区分盲目追随,就很容易掉进坑里。

    (更多…)
  • Vite 里使用动态加载

    Vite 里使用动态加载

    有时候,我们希望根据用户当前的使用状态决定加载哪些模块。比如一个网页 IDE,用户在写 JS,我们就加载 JS 模块;用户在写 PHP 我们就加载 PHP 模块。这个功能有点类似路由懒加载,但又不完全相同。

    以前在 wepback 里,我们可以通过动态 import 加注释的方式来做:

    const _ = await import(/* webpackChunkName: "lodash" */ 'lodash');
    
    // 通过组合不同注释,还可以实现不同的分包和加载策略
    // Single target
    import(
      /* webpackChunkName: "my-chunk-name" */
      /* webpackMode: "lazy" */
      /* webpackExports: ["default", "named"] */
      'module'
    );
    
    // Multiple possible targets
    import(
      /* webpackInclude: /\.json$/ */
      /* webpackExclude: /\.noimport\.json$/ */
      /* webpackChunkName: "my-chunk-name" */
      /* webpackMode: "lazy" */
      /* webpackPrefetch: true */
      /* webpackPreload: true */
      `./locale/${language}`
    );

    上面的内容可以在 Webpack 官方文档 找到。

    Vite 里提供了类似的功能,不过使用方式不太一样。现在的 Vite 2 已经抛弃了以前的插件式实现,即 vite-plugin-dynamic-import 和 @rollup/plugin-dynamic-import-vars 都不会用到。

    首先,我们要使用 import.meta.glob('./*.js`) 声明哪些文件可能要用到;接下来,我们就可以根据实际需求加载具体文件。大概方式如下:

    <script lang="ts" setup>
    // <script setup> 的代码,相当于 `created`,所以可以使用动态加载
    
    // 未来要使用的变量
    let shareTexts:ShareTexts;
    
    // 声明符合 `/src/data/share*.ts` 的文件都可能要用到
    const modules = import.meta.glob('../data/share*.ts');
    
    // 如果 store 里有 `langName`,就加载 i18n 版,否则加载普通版
    modules[`../data/${store.state.langName ? 'share-i18n' : 'share'}.ts`]()
      .then(data => {
        // 因为用到 esm,所以要加 `default`
        shareTexts = data.default;
      });
    </script>

    接下来,还可以配合 vite.config.js,将相关装入特定分包:

    export default defineConfig(({command, mode}) => {
      return {
        plugins: [
          vue(),
        ],
        build: {
          rollupOptions: {
            output: {
              manualChunks(id) {
                let result;
                if (result = /(shares\/)?share\d\.txt\?raw$/.exec(id)) {
                  return result[1] ? 'share-i18n' : 'share';
                } else if (id.includes('node_modules')) {
                  return 'vendor';
                }
              },
            },
          },
        },
      };
    });

  • 近期帮一个朋友做的 Vue 网站优化方案

    近期帮一个朋友做的 Vue 网站优化方案

    前几天有个朋友找到我,说他们公司的网站产品打开速度不太理想,加载的数据量很大,想优化一下。并且询问我,是不是用微前端会好一些。

    分析

    我看了一下,大概有几个点:

    1. 他们是做自动化运维的,提供多个工具,所有工具都是独立的 Vue 项目
    2. 用户一般先登录 dashboard 页面,然后导航到具体的工具页
    3. 因为每个工具都独立开发、独立打包,所以每个页面都会用到不同的 app.[hash].jschunk.vendors.[hash].js
    4. 于是,用户每次切换工具,都要完整加载一遍公共资源,如 vue 全家桶和 antdv
    5. 已经对一些模块做了 lazy-loading

    于是我得出一些结论:

    1. 首先,因为所有项目的技术栈高度趋同,所以并不需要微前端(微前端的解释在后面)
    2. 现在重复加载最多的应该是 vue 全家桶和 antdv,重复加载的原因是没有拆包。适当拆分打包后资源,应该可以大大提高代码复用率,减轻不同项目间的切换成本
    3. 同时还应开启 http/2,提高连接复用率

    方案

    于是我给朋友提出了以下改进方案:

    1. 整站启用 http/2
    2. 所有项目手动分 chunk
      1. vue 全家桶
      2. antdv
      3. 其它好统计的、全站都在使用的仓库
      4. 其它仓库
    3. 统一所有项目的依赖,提交 lock 文件入库
    4. 所有项目对公共仓库的引用顺序需保持一致,保证 webpack 打包之后的序号能维持一致
    5. 替换页面中的资源位置,指向同一个资源
    6. 延长资源缓存时间,提高利用率

    下一步

    他们需要一些时间来消化和实施这些方案,所以这一次咨询先到这里。

    接下来,我可能会建议他们调整支持的浏览器、增加 ESM build。以及使用 npm workspace 创建一个核心项目,把所有工具项目放到一起构建,减少前面的(2)(4)(5)环节。

    总结和广告

    希望上述方案对大家也有启发和帮助。

    顺便帮他们打个广告吧:

    OpsAny,云原生场景下的智能化运维平台。我们倡导“以资源为中心”和“以应用为中心”相融合的运维理念,提高运维效率、保障业务连续性。

    OpsAny, make ops perfect
    (更多…)
  • 复盘近期升级工具链的过程

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

    公司希望我提升产品在移动端的体验,于是我就打开了 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(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 能尽快修复,我们可以早点把它应用到产品当中。

  • 偶然发现 babel@6 + babel-preset-es2015 的 bug

    偶然发现 babel@6 + babel-preset-es2015 的 bug

    这两天尝试做一套解决方案,能够只编译一套 ES2015+ 代码,在现代浏览器就正常使用,在 IE11 自动切换到 babel-standalone 实时编译。

    然后偶然发现 babel@6 的一个 bug,如下:

    class T {
      foo() {
        const {hasOn: o} = (() => {
          for (let e = 0; e < 1; e++) {
            if (o = 1, 2 >= o) return null;
          }
          var o;
        })() || {};
      }
    }
    
    t = new T();
    t.foo();

    上面这段代码其实是符合语法的。看起来,L3 使用 const 声明了变量 o,然后在 L5 又再次试图给它赋值,似乎有修改常量之嫌。但其实,因为 L5 在另外一个块域里,而且 L7 var o; 会产生变量提升,所以这个赋值操作的是 var o 声明的局部变量。

    如果你把它放在 V8 里,比如 node.js 或者 Chrome 浏览器,它就能正常执行;如果你用 babel@6 + babel-preset-es2015 转译,就会报错,说 o is read only

    产生这个错误的原因是 babel-plugin-transform-es2015-classes 解析上面这段代码时存在错误,导致 babel-plugin-check-es2015-constants 认为常量被修改。不过我也只查到这一步,暂时不知道怎么修复这个 bug,也不知道怎么在 babel@7 里检查这个问题。有兴趣的同学可以试一下。

  • 使用 webpack-rpc-inline-plugin 打包内联函数体

    使用 webpack-rpc-inline-plugin 打包内联函数体

    使用 Puppeteer 的时候,我们常常要使用 page.evaluate() 或者 page.evaluateHandle() 函数到目标页面的环境里执行一段代码。

    如果是简单的代码,比如返回页面标题、改变某个节点的属性,那么直接写在 .evaluate() 里面就行了;但实际生产中,尤其是前厂的 Showman 产品里,要执行的函数往往非常复杂,经常需要组合多个函数:

    page.evaluate(() => {
      function func1() {}
      function func2() {}
      // ...
      function funcN() {}
    
      func1();
      func2();
      // ...
      funcN();
    });

    这种场景下,我们必须用上面这种写法,而不是下面这种我们更熟悉的写法:

    import func1 from './func1';
    import func2 from './func2';
    // ...
    import funcN from './funcN';
    
    page.evaluate(() => {
      func1();
      func2();
      // ...
      funcN();
    });

    因为被执行的函数会被转换成字符串,传输到目标环境,然后重新实例化成函数,再执行。所以上面这种写法,引擎会在全局环境下查找需要的函数,而那些函数都没传递过去,就会找不到。

    如果开发时按照方案一组织代码,会遇到几个问题:

    1. 子函数放在主函数体内部,不方便独立开发、调试、测试
    2. 每个主函数内部都需要写死子函数,不方便共享复用

    所以我就想从工具链入手,写一个专用工具,可以继续用方案二的形式组织代码,但是编译打包之后,就恢复到方案一的状态。

    我选择了 webpack 插件,原因有二:

    1. 我比较熟悉 webpack
    2. 这种情况不能用 loader

    最后我选择在 compilation.afterProcessAssets 钩子处理 JS。此时 JS 已经打包了所有资源,并且经过 terser 压缩。所以我会先将 bundle 解开(直接用 string.substring),然后 return webpack 对象,从中找到目标函数替换。

    具体的代码在 GitHub 仓库里,我就不详细解释了(困了),感兴趣的同学可以看看。

    欢迎需要在 rpc 环境下执行 JS 的同学使用,欢迎反馈需求和问题。

  • babel@6 升级到 babel@7,兼容性代码膨胀的原因

    babel@6 升级到 babel@7,兼容性代码膨胀的原因

    最近尝试把厂里项目的依赖从 babel@6 升级到 babel@7,发现打包之后体积大了很多。于是打开 webpack-bundle-analyzer,果然大部分代码都是 corejs 引入的,项目本身的逻辑只占少部分。

    从报告来看,虽然目标浏览器的版本均高于 Promise 的启动版本(比如 Chrome 32),但 es.promise 仍然会被打包进来。于是以 es.promise 为突破口开始分析,找到答案:因为 JavaScript 引擎 V8 直至 v6.6 版本时,在 Promise 实现方面都存在严重 bug,所以 babel@7 保守地选择 Chrome 66/67 作为临界点。

    想来其它体积多半也是这么加上来的,就不再一个一个排查了。 ​​​

    这就很难处理。不升级吧,新特性兼容不了;升级吧,包体积变大,公司上层又未必同意。

    下一步试试 esbuild 吧,或者回头手动打包一套 polyfill。目前设想的方案是:

    1. 用 babel 之类的工具提取出所有特性
    2. 根据 caniuse 生成必须的特性列表
    3. 像上面说的 Promise,因为 bug 所以必须兼容,我们就不考虑了,可以反过来加一条 eslint 规则规避
    4. 最终生成新的 polyfill 打包进来
    (更多…)
  • 使用 caniuse-lite 检查目标浏览器的特性支持情况

    使用 caniuse-lite 检查目标浏览器的特性支持情况

    起因

    之前得知 loading="lazy" 新特性,正巧在学习如何使用 html-webpack-plugin,于是就写了个 lazyload-webpack-plugin,可以给页面里所有 <img><iframe> 加上 loading="lazy" 属性,以启动原生 lazyload。

    不过当初写得很简单,只会不分青红皂白加属性,甚至可能会覆盖已有的 loading="eager" 属性,引发 bug。所以这几天就想找时间升级一下:

    1. 不再覆盖 loading 属性
    2. 根据 browserslist 得到的目标浏览器器列表采取不同策略
      1. 支持 loading="lazy" 就延续之前的做法
      2. 不支持的话,用 data-src 替换 src,然后在页面里根据浏览器特性处理

    caniuse-lite

    没想到这个需求还挺难满足,找了一圈竟然没有成型的教程,只好自己摸索一下,还好并不复杂。

    以下代码实现了根据环境配置检查目标浏览器是否支持 loading="lazy" 的功能。我用在新版本的 lazyload-webpack-plugin 中,现在可以实现前面说的功能了。

    // caniuse-lite 是官方提供的 caniuse 仓库封装,方便我们查询特性支持
    const lite = require('caniuse-lite');
    const browserslist = require('browserslist');
    // `features` 是特性支持列表,`feature()` 可以将其转换成好用的 json 格式
    const {features, feature: unpackFeature} = lite;
    
    // 这一步,用特性名 'loading-lazy-attr' 获取支持列表
    const feature = unpackFeature(features['loading-lazy-attr']);
    // 直接声明 browserslist 实例,它会自动查找本地 `.browserslistrc` 或环境变量 `BROWSERSLIST` 来生成浏览器列表
    const browsers = browserslist();
    const {stats} = feature;
    // 遍历浏览器列表,根据名称、版本验证对 `loading="lazy"` 的支持情况
    const isSupport = browsers.every(browser => {
      const [name, version] = browser.split(' ');
      const browserData = stats[name];
      const isSupport = browserData && browserData[version] === 'y';
      if (!isSupport) {
        console.log(`[lazyload-webpack-plugin] target browser '${browser}' DOES NOT supported \`loading="lazy"\`.`);
      }
      return isSupport;
    });
    
    module.exports = isSupport;

    问题

    现在的情况是,如果知道特性名称(如“loading-lazy-attr”),可以判断目标浏览器是否支持;但是如果不知道准确名称,就没法判断。如果我想在项目当中使用,比如检查当前代码仓库用到哪些特性不被目标浏览器兼容,并生成 polyfill 套件,就很难操作。

    有待进一步学习。

    自荐

    欢迎有制作静态页面需求的同学使用 lazyload-webpack-plugin<img><iframe> 添加 loading="lazy"。关于使用 webpack 制作多页面站点的经验分享,可以阅读我的这篇文章《使用 Webpack 开发多页面站点》。

    有任何问题、意见、建议,欢迎通过各种方式提给我。

    (更多…)