关于前端:从一次前端公共库的搭建中深入谈谈tree-shaking相关问题

39次阅读

共计 5652 个字符,预计需要花费 15 分钟才能阅读完成。

我的项目背景

随着业务的积攒,前端我的项目之间逐步会产生许多能够跨我的项目复用的逻辑或组件。比方对前端数据库 indexedDB 的封装、对 fetch 申请进度和中断请求性能的扩大、以及可能会在多个我的项目应用的 react 和 vue 组件。以后曾经有一个公共库专门用来收敛 js 的逻辑复用,然而随着雷同技术栈的我的项目逐步减少,仅仅 js 层面的复用曾经不够了,组件也须要跨我的项目复用,而之前的公共库我的项目设计无奈较好的承接 react 和 vue 组件库,于是须要有一个综合的公共库,收纳之前的 js 库、新增 react 组件库、vue2 组件库、vue3 组件库等。

新我的项目摈弃了多仓库的设计形式,采纳了 monorepo 构造。究其原因是因为,vue、react 组件也可能有一部分独特逻辑能够形象到 js 库中,也可能用到 js 库里的一些工具函数。如果采纳多仓库,那么当 js 库更新时,只能手动更新所有依赖它的仓库,而像 lerna 之类的 monorepo 计划,实质上在一个 git 仓库中,依赖通过 Symbolic Link 的模式关联,本仓库的我的项目产生批改天然能够感知到,则能够自动化的解决这个痛点。

dead code removal(无用代码移除)

在组件库我的项目开始前,先调研了同类我的项目设计,尤其关怀其中的按需加载。例如 antd-mobile,依照开发直觉,在 import {Button} from 'antd-mobile' 的时候,只有 antd-mobile 提供了 esm 标准的打包文件,应该能够通过 tree shaking 按需加载。事实是它的确提供了 esm 文件:

如果我在 esm 文件 import 它,它会从 es/index.js 这个文件引入。

如图所示,es/index.js的确是一个合乎 esm 标准的模块入口,所以失常状况下 js 局部是间接能够按需加载的。

然而官网中有这样一段话

那么什么状况下算是不反对 tree shaking 的环境呢?我的总结如下:

1. 应用 commonjs 标准引入模块,例如const {Button} = require('antd-mobile')

2. 应用了 esm,但代码通过 babel 的 preset-env 编译会被转成 commonjs(能够通过 modules: false 解决)

3. 代码初始化时可能有副作用(不齐全等于 FP 的副作用,前面我会解释这一词),但 antd 明确反对 tree shaking,显然不会是这一种

4. 通过其余编译管道时(指像 webpack 的 loader 那样的)可能带入副作用代码

于是为了防止开发者呈现以上状况,antd-mobile 提供了每个组件的分包,并通过 babel-plugin-import 在编译时批改援用代码,间接的达到 tree shaking 的性能。

既然开发环境不可控,所以本我的项目也提供了每个组件的分包,以便在非凡状况下能够按需引入,最初的打包后果像这样

具体如何分包的,感兴趣的能够看看 rollup 配置多入口打包。只有规定一个固定的目录标准,找到总入口和每个组件的入口就好了,前期新增我的项目,只有合乎目录标准,就能够复用。

tree shaking 生效

分完包后顿感神清气爽,当初开发者既能够在总入口利用 tree shaking 拿到 minimized code,也能够间接拿分包了。奔着看看分包有多简洁的心态,点进一个 Loading 组件的打包文件,后果发现区区一个展现 Loading 状态的组件代码超乎预料的多

剖析问题

初步判断可能是因为引入了内部的某个模块,导致把没有应用到的代码也带入了,看打包后果中的确有许多没有应用的代码

源码中的确引入了一个可疑的库(前文所提到的公共 js 库),多进去的代码也的确是这个 js 库里的

那么问题初步认定为,因为应用了 px2vw 函数,导致把 js 库里的其余某些代码也引进来了。

查看那个 js 库

入口文件看上去貌似没什么问题,比照打包后果中多进去的代码是和 sdk 相干的,其余模块的确曾经被 tree shaking 优化掉了,所以也不太可能是它们被转成 commonjs。

那么 sdk 文件到底有什么问题呢?首先咱们来看看哪些状况会让 esm 环境 中的 tree shaking 生效

esm 中 tree shaking 生效的起因

先看 webpack 文档里对于 tree shaking 的一段话

In a 100% ESM module world, identifying side effects is straightforward. However, we aren't there quite yet, so in the mean time it's necessary to provide hints to webpack's compiler on the"pureness" of your code.

如果大家全副都用 esm 模块编写代码,那么辨认『副作用』是很简略的。然而事实不是如此,所以你须要通过配置通知 webpack,你的代码是『污浊的』,以此让 webpack 认为它没有『副作用』。

具体的配置是

{
  "name": "your-project",
  "sideEffects": false
}

这里说的副作用就是影响 tree shaking 的关键因素。它不齐全等于 FP(函数式编程)中的副作用,我认为它只是和 FP 副作用有交加

FP 中的副作用

维基百科 对副作用的解释是:

在计算机科学中,函数副作用 指当调用函数时,除了返回可能的函数值之外,还对主调用函数产生附加的影响。例如批改全局变量(函数外的变量),批改参数,向主调方的终端、管道输入字符或扭转内部存储信息等。

具体在 js 中我总结了以下几点:

  1. 批改了函数上下文以外的变量或参数。上面的 add 和 modify 函数就是有副作用的

    let i = 0
    const add = () => ++i
    
    const o = {a: 0}
    const modify = (_o) => {
      _o.a++
      return _o
    }
    modify(o)
  2. 函数产生了 IO 操作,包含但不限于打印日志、读取写入、操作 dom、网络申请等

    // 打印日志
    const log = (...args) => {console.log(...args)
    }
    
    // 读取 dom
    const query = (identify) => document.querySelector(identify)
    
    // 网络 io
    const get = (url) => fetch(url)

tree shaking 中的副作用

tree shaking 中的『副作用』就在这其中,比方在 import 过程中:

  1. 批改了 window 属性
  2. 可能会触发 getter、setter 的操作,因为没法判断 get、set 中有没有副作用
  3. 打印日志

有些 FP 中的副作用并不算 tree shaking 中的副作用,有些 tree shaking 中的副作用不算 FP 中的副作用,它们是不必要不充沛的关系,只是有交加,上面请看我写的最小测试是否有副作用的 demo

FP 与 tree shaking 副作用的不同与雷同

1. 失常的 export

如上图所示,有一个 utils1.ts 文件,其中用多种形式 export 了 5 个函数

接着在入口文件 index.ts 中引入它们中的函数 a 和用 export default 导出的函数a3,具体见下图

最初用 rollup 打包index.ts,上面是打包后果的局部代码

从上图能够看到后果中只有被引入的 aa3两个函数,,其余函数都没有打包进来,tree shaking 胜利了。

2. 以对象的模式 export

如上图所示,在 utils2.ts 文件中,export default了一个 cd 函数组成的对象,同时独自 export 了函数e

接着咱们在 index.ts 中引入 export default 导出的对象utils2,见下图

并只应用其中的 c 函数,下图用红框标出

最终的打包后果如下

能够看到它把整个对象,即其中的 cd 函数都打包进去了,e函数失常 tree shaking。因为导出的模块是对象,应用时读取对象的办法,所以 rollup 不能提前晓得 js 运行时你会须要哪个办法,只能全副打包进来。

3. 产生副作用

从上图的 utils3.ts 文件中咱们能够看到

  • f函数是一个典型的 FP 副作用函数,因为它会批改内部变量。
  • g函数也是一个 FP 副作用函数,因为它外面有打印操作,同时在它申明后也会触发一个打印console.log(g),即它的父级执行上下文里也有副作用。
  • 单看 h 函数并不是 FP 副作用函数,然而 h 函数内部的上下文是有副作用行为的 (window as any).__h = h,然而h 函数自身并不算。
  • i函数没什么问题,能够在 index.ts 中只引入它,看看其余函数是什么反馈。
  • 最初的 x 函数比拟非凡,在申明它之前先读取了被 Proxy 的对象的属性,实践上 x 函数相干的这些操作都不算 FP 的副作用

而后让咱们来看看最终的打包后果:

从上图能够发现

  • f函数尽管属于 FP 的副作用,但不属于 tree shaking 的副作用,它能够被dead code removal
  • g函数因为在申明期间产生了 console.log 的副作用所以被引入了,为了判断是哪个 console.log 带来的副作用导致的,第二次打包把 console.log(g) 去掉发现它被失常的 tree shaking 了,所以只有在模块申明的过程中产生 console.log 才算 tree shaking 的副作用
  • h函数因为在申明后批改了 window,所以产生了副作用,如果不保留它,万一其余模块会从 window 中读取它就会造成谬误,所以 rollup 不对它tree shaking
  • x函数很非凡,它被去掉了,但下面的 o 对象创立、读取的操作被保留了。之所以这样是因为,开发者可能通过 ProxyObject.defineProperty 等 api 劫持对象的 getter 属性操作符,外面的操作是不可预知的(比方批改了 window),为了保险起见,ProxyObject.defineProperty相干的代码必须保留。然而 x 函数哪怕外面也读取了 o 对象也仍然不会保留,因为 x 只有运行时才会执行 getter,而它没有被引入就不会造成潜在的副作用影响。

有一点令人诧异的是,像这样的代码,rollup 也会对它 tree shaking

要晓得读取属性可能产生副作用,假如对 window.__n 属性增加了 getter 属性操作符,外面用来在全局记录被读取的次数,在 utils3.ts 里应用了 window.__n,预期是window.__n + 1,事实是最终的代码里并没有读取它,window.__n 没有变动。这样就很不合乎直觉。

从以上的几个副作用案例能够看出,tree shaking 的副作用和 FP 的副作用是一个交加的关系,并不齐全相等。

4. import commonjs

那么 rollup 能不能对 commonjs 也能够 tree shaking 呢?比方上面有一个 utils4.common.js 文件

以 exports 对象属性的模式导出了 jk 函数

接着在 index.ts 中只引入 j 函数

最终的打包后果如下所示

能够看到这种模式的 commonjs 模块是 能够 被 tree shaking 的,rollup 能动态剖析出 exports 被增加了哪些函数,其中哪些被 import 了。那是不是所有的 commonjs 都能够呢?上面再看一种模式

5. 另一种模式的 commonjs

先将 module.exports 赋值为一个新的对象,对象里有 l 函数,接着再向其中增加 m 函数

而后咱们在 index.ts 里只引入 l 函数并打包:

能够看到 rollup 把不须要的 m 函数也打包进来了。

如果咱们在 index.ts 里只引入 m 函数并打包:

这次才是正确的 tree shaking。

咱们能够看到不同的 module.exports 形式以及援用不同的函数是会影响 tree shaking 成果的,如果你拿对象赋值给 exports,并且未来引入了这个对象字面量中的办法(即l),那么这个对象残余的办法也会被打包进来;而如果你引入的是非对象字面量中的办法(即前面动静增加的办法m)时,则能失常 tree shaking。

所以第三方 commonjs 库有着 tree shaking 的不确定性,除非不得不,否则尽量用反对 esm 的库。

以上就是对 tree shaking 问题的摸索,具体的代码都在这里:https://replit.com/@HiWayne/r…

顺便安利一下这个平台 https://replit.com,它能够间接运行我的项目和多人合作,并且能够间接复制他人搭好的脚手架模板,有点云开发的感觉。

回到最后的问题(为什么 sdk 文件没有被 tree shaking)

其实答案曾经很显著了,既然文件没有被转换成 commonjs,esm 语法也正确,那无非是 sdk 文件中有以上的某些副作用。毕竟本我的项目中的 sdk 代码是从很多年前的老我的项目里移植过去的,老我的项目并没有思考模块化。间接把 sdk 交给 window 属性这种显著的问题点在移植之初就曾经发现了。但因为客户端和前端通信的须要,代码中还有很多隐式的基于 window 的约定协商,想必可能造成了 tree shaking 的问题。但问题起源曾经确定,剩下的只是工夫问题。

tips:如果代码逻辑简单、代码量大、对老代码不理解,其实有个小技巧能够放大排查范畴。先保留好代码,而后把你感觉可疑的代码块删掉,如果间接删掉会影响上下文那就替换成空实现(反正 runtime 的问题不会在编译期裸露),而后再次打包,这样多测试几次,如果失常 tree shaking 了,那么就是被删掉的代码块内的某些逻辑作怪,再向内排查。

总结

本文从 我的项目创建的背景 我的项目的选型 按需加载的设计 登程,通过在我的项目中遇到的 tree shaking 问题 为探讨中心点,具体介绍了造成 tree shaking 生效的细节和起因,并通过 demo 举例了 tree shaking 副作用和函数式编程(FP)副作用的共同点和区别。如果你也在写 npm 库,心愿通过本文能唤起你对查看 tree shaking 的意识,也心愿能作为大家初探 tree shaking 和函数式编程的 stepping stones :)

正文完
 0