提高你的模块的实用性

48 阅读6分钟

上周我在我的团队的国际化(又称i18n )解决方案上工作。我们把它叫做react-i18n (如果我们把它开源的话,我们需要重新命名,因为它已经被占用了)。它非常整洁,而且非常小。我不打算谈论为什么我们不使用任何其他无数的工具来做这件事(也许我会把它留到另一篇博文中)。我想谈的是我为使该模块更实用而做的一些事情。

该模块的一个特点是,它将自动为你加载服务器渲染的内容。在PayPal,我们有另一个模块叫react-content-loader 。这是一个快递中间件,依靠Kraken中使用的惯例,根据用户的语言偏好为其插入内容。例如,我们假设你有一个文件:

// locales/US/en/pages/home.properties
header.title=PayPal Rocks
header.subtitle=No really, it does

那么这个中间件将在你的页面底部插入这个内容(对于美国用户,en 作为他们的首选语言)。

然后react-i18n ,就会在客户端自动加载该内容。你所要做的就是使用它:

import getContentForFile from 'react-i18n'
const i18n = getContentForFile('pages/home')

function App() {
  return (
    
      {i18n('header.title')}
      {i18n('header.subtitle')}
    
  )
}

这就是它的工作原理(再次,我相信你们中的一些人想到了其他可以做得更好的,但请不要对我说 "实际上很好"。我知道它们,我保证)。现在你基本上了解了它是如何工作的,我想谈谈我对它的一些改动,以使它更实用

I'll show you

所以你会注意到,当我们在上面的例子中在客户端使用react-i18n ,我们不需要做任何事情来初始化或用内容引导它。它自动从DOM中获取这些内容。它在从react-i18n 导出的main 内完成这个工作。这样,当你导入react-i18n 时,就会为你加载内容。这是一个很方便的功能。但它的代价是,react-i18n中的main 模块在模块的根层有副作用比如说:

// react-i18n/index.js
// ... stuff
// side-effect!
const content = JSON.parse(document.getElementById('react-messages'))
// ... more stuff
export {getContentForFile as default, init}

这给模块的用户带来了一些挑战。这意味着他们必须意识到当他们导入你的模块时会发生什么。他们必须确保在全局环境准备好之前不要导入你的模块。而这个问题不仅表现在应用环境中,也表现在测试环境中!除非你注意在环境没有准备好的时候给出良好的警告(如果你知道的话),否则人们在做一些看似不相关的任务时(比如导入一些模块,而这些模块恰好在依赖关系图的某个地方导入了你的模块),就会得到神秘的错误信息。

另一个问题是,可能有理由对初始化过程进行配置。如果我的节点没有idreact-messages ,而是使用i18n-content 呢?或者,如果我根本没有对信息进行服务器渲染,而它们是来自ajax请求呢?结果发现,react-i18n 实际上暴露了另一个模块react-i18n/bootstrap 来定制这种行为,这很好,但这并不能解决如果有人先导入react-i18n 而发生的问题。

因此,我所做的是将所有的副作用包裹在我导出的一个名为init 的函数中(这与它已经导出的bootstrap 的东西类似):

// react-i18n/index.js
// ... stuff
function init(options) {
  // ... other stuff
  // side-effect! But it's ok now because that's clear
  const messages = JSON.parse(document.getElementById('react-messages'))
  // ... other other stuff
}
// ... more stuff
export {getContentForFile as default, init}

因此,这意味着任何使用该模块的人现在都必须调用init ,但他们是按照自己的方式来做的,而且是在他们想发生的时候,我认为这才是关键的区别。在初始化发生之前,是否有人导入这个模块并不重要。这也给了我们一个机会,如果他们在开始使用这个模块之前未能初始化,我们可以给出一个更有信息量的错误信息。

**关键是,你的模块在被导入时不应该做副作用。取而代之的是导出执行副作用的函数。**这使用户可以控制何时发生什么。如果你能做到的话,最好是完全没有任何副作用(这实际上也可以通过我对react-i18n 的重新设计来实现),但这是另一篇通讯的主题。

以前,这个库实际上只是我们应用程序的一部分。所以我们可以很容易地依赖这样一个事实:JSON对象是一个嵌套对象,其中第一个键是本地化文件的名称,其余的只是该文件内容的嵌套版本(正如你在上面的例子中所看到的)。而文档中的实现和例子都是针对这个用例的。然而,我们正在对这个模块进行 "内部采购"(也许最终会开放采购),所以人们会使用不同的工具,有不同的使用情况。

所以,如果不是太多工作,也不增加太多复杂性,那就尽量使解决方案更通用。所以现在,实现并不关心本地化对象的根层是一个文件名,其余的是该文件的内容。它所关心的是它是一个嵌套的JavaScript对象的事实。这意味着,而以前,你必须这样做:

import getContentForFile from 'react-i18n'
const i18n = getContentForFile('pages/home')

// etc...
i18n('header.title')
// etc...

你现在可以这样做了:

import getContent from 'react-i18n'

// etc...
getContent('pages/home.header.title')
getContent('pages/home')('header.title')
getContent('pages/home.header')('title')
// etc...

因此,每次调用getContent ,都会返回内容,或者如果内容是另一个嵌套对象,就会返回另一个内容获取器函数。我把这称为 "sota-curried",因为它不是真正的currying,但它看起来有点像。

现在PayPal的react-i18n ,因为实现和文档没有假设你在使用react-content-loader ,所以更有普遍意义。而且,事实证明,这样做实际上使实现更简单了!哇哦!

我还应该提到,你无法预测未来,而这正是你在建立一个通用库时必须努力做到的。当你这样做的时候,你需要平衡可用性和 YAGNI原则。我之所以付出这样的努力,只是因为我们要把这个东西从我们的项目中提取出来,以便其他人可以使用它,我们需要支持这些用例。小心过早的优化(这并不限于性能情况,也包括功能/复杂性)。

我还可以谈一些其他的事情,但我要用这个来结束这封邮件。我希望你能找到方法,从你的大多数模块的根层中去除副作用,并找到方法使你的解决方案更加通用,而不牺牲可用性或实现的复杂性。

祝您好运!😎 😎 继续努力

Who's awesome? You are!