如何保持你模块的纯粹性

45 阅读4分钟

这些推文引起了我的共鸣,因为它确实会对JavaScript引擎和ES模块的性能产生巨大的影响。这真的是一个错过的机会。目前我们能做的并不多(虽然你可以用动态导入来模拟,但那样你就会有其他问题)。

这让我想到了另外一些我觉得很重要的事情,我想和大家分享一下。我引用了Ingvar的推特

英格瓦在另一个主题中扩展了我的意思(我肯定是不知道的)。

让我们探讨一下为什么这是个好主意,考虑一下下面的情况:

// a.js
import './b'
console.log('ready')

// b.js
import {serverData} from './c'

if (!serverData.user) {
  // redirect to login
  location.assign('/login')
}

// c.js
const el = document.getElementById('server-data')
const json = el.textContent
export const serverData = JSON.parse(json)

c.js 模块将需要index.html 已经被渲染的东西,如。

我希望这段代码能在生产中如期运行。对于这样的代码(或一般的任何不纯的模块),我有几个问题。在我们继续之前,重要的是要认识到,在运行console.log('ready') 行之前,b.jsc.js 中的所有代码都已经先运行了。

当一个开发者导入a.js 模块时,他无法知道后果会是什么。如果事情没有设置好,开发者会看到一个神秘的信息,比如:

Uncaught TypeError: Cannot read property 'textContent' of null

and this because they simply imported a module.

假设a.js 实际上只需要b.js 所暴露的某些实用程序,而实际上根本不需要c.js 模块中的任何东西来运行。在这种情况下,这些模块在做不需要的额外工作。浪费的努力在某些情况下可能是一个相当大的影响,这取决于具体情况。

特别令人恼火的是,当浪费的努力导致了一个神秘的错误。我不仅要弄清楚这个错误是怎么回事,而且我甚至一开始就不需要那段代码来运行!"。

作为一个相关的(也是非常重要的)部分,你不能对那段未使用的代码进行树状摇动!你必须要有足够的时间和精力。

如果我意识到c.js 需要DOM中的JSON,所以我决定在需要c.js 之前将其初始化,像这样:

// a.js
const script = document.createElement('script')
script.setAttribute('id', 'server-data')
script.setAttribute('type', 'application/json')
document.body.appendChild(script)

import './b'
console.log('ready')

不幸的是这是行不通的,因为(根据ES模块的规范)import 语句是在模块的任何代码之前运行的,无论它们出现在代码的什么地方。幸运的是,它们至少是按照它们出现的顺序运行的。因此,要做到这一点,我必须为我的设置代码创建一个新的模块并首先导入该模块:

import './setup'
import './b'
console.log('ready')

测试纯函数比测试不纯函数更容易,这是一个被广泛接受的事实。这同样适用于模块。如果我想测试b.js 模块呢?我必须在导入b.js 之前初始化DOM,这样c.js才能被正确初始化,但这样我怎么再测试它呢?我必须对模块系统做一些奇怪的事情,以便在不同的DOM初始化后再次导入这些模块。

在jest中,你有 jest.resetModules()使得这件事变得简单多了,但它仍然不是超级简单的,对于维护这些测试的人来说也不是直接的。

因此,我将如何把事情改写成纯粹的(在这个意义上,导入模块没有副作用,尽管它们所暴露的函数本身并不纯粹):

// a.js
import {init} from './b'
init()
console.log('ready')

// b.js
import {serverData, init as initC} from './c'

export function init() {
  initC()
  if (!serverData.user) {
    // redirect to login
    location.assign('/login')
  }
}

// c.js
export const serverData = {}
export function init() {
  const el = document.getElementById('server-data')
  const json = el.textContent
  Object.assign(serverData, JSON.parse(json))
}

这如何解决上述问题?

  • 未知的后果:我们正在导入init方法,并从b.jsc.js ,所以我们在不看实现的情况下可能不知道这些到底是干什么的,但我们至少知道它们在做什么。💯
  • 不必要的操作:如果b.js 导出了额外的实用方法,我们可以导入这些方法而不会遇到任何意外。💡
  • 无法选择操作的顺序:如果我们想在a.js 中初始化server-data ,那么我们只需在调用b.js 模块中的init 方法之前进行。✌️
  • 对测试的影响init我们可以很容易地在测试中多次运行b.js 中的函数,在每次测试前用我们需要的东西重新初始化DOM,没有任何麻烦或黑客。🎉

注意,a.js 模块并不纯粹。在某些时候,你的某个模块需要做一些事情来启动一切。这就是a.js 模块的目的。这些模块通常应该是非常小的(通常是你的index.js 入口模块)。

保持你的模块的纯粹性意味着限制它们在模块的根层所做的事情的数量。它可以让你完全避免所提到的问题,使你的代码库更加清晰。我希望这些例子(虽然略显矫揉造作)能对你有所帮助。祝您好运!