六、用户首选项和默认值

任何足够大的 JavaScript 应用都需要配置其组件。 组件配置的范围和性质因应用的不同而不同。 在配置组件时,有许多规模因素需要考虑,我们将在整个章节中讨论这些因素。

我们将从识别我们必须处理的偏好类型开始,本章的其余部分将通过这些偏好的具体规模问题以及如何围绕它们工作。

首选类型

在设计大型 JavaScript 架构时,我们主要关注三种类型的首选项。 这些是场所、行为和外观。 在本节中,我们将为每个首选项类别提供一个定义。

场地

今天的应用不能只支持一个地区,如果他们要在全球范围内成功的话。 由于全球化和互联网的存在,对在世界另一个地方创建的应用的需求成为了新的规范。 因此,我们必须以一种能够无缝地适应许多地区的方式来设计我们的 JavaScript 架构。 一个地区的用户应该能够像任何其他地区的用户一样轻松和放心地使用我们的应用。

注释

使组件能够使用任何地区的过程称为国际化。 然后,为应用创建特定于语言环境的数据的过程称为本地化

国际化/本地化之所以如此困难,是因为它涉及到用户界面的每个视觉方面。 尽管有许多组件不关心区域设置(例如控制器或集合),但这可能会造成相当大的影响。 例如,任何在模板中硬编码的字符串标签,现在都需要通过感知地区的翻译机制。

语言翻译本身就已经够难的了。 但是 locale 数据包含任何与使用我们软件的给定文化相关的东西。 例如,用于日期/时间或货币值的格式。 这些只是最常见和最直接的元素。 内容可以直接变化到数量的测量方式,或者整个页面的布局。

行为

我们组件的大多数行为方面驻留在代码中,并且是不变的。 由于不同的偏好而发生的行为变化是微妙的,但却是重要的。 当有许多相互作用的组件时,必然会有一个不兼容的组合导致问题。

例如,在我们的组件实现中找到的函数可能会获得一个值,它用来从配置值计算一些东西。 这可能是用户的偏好,也可能是我们为了可维护性而放置的东西。

注释

在本章的其余部分,我们将把单个配置值作为首选项。 我们将给定组件中所有首选项的聚合效果称为配置。

行为偏好会对用户看到的内容产生不同的影响。 一个简单的例子是关闭或禁用组件。 这个首选项将导致组件不再呈现在 UI 中。 另一个首选项将决定显示多少元素。 这里的一个常见示例是,用户告诉应用每个页面需要看到多少搜索结果。

这些类型的首选项并不总是直接映射到最终用户。 也就是说,组件可能具有某些不直接向用户公开的首选项。 它存在的唯一目的可能是开发人员的灵活性,以减少我们编写的代码量。 可配置组件有多种形式,从这个角度来看,我们需要确保相应地处理它们,以帮助扩展我们的软件。

我们需要考虑的不仅仅是前端组件,因为给定的偏好可能会改变后端行为。 这可以像查询参数首选项一样简单,也可以是导致使用不同 API 端点的另一个首选项。 所有这些看似无害的偏好加起来在整个应用中产生深远的影响,可能会影响系统的其他用户。

外观

如果一个现代 JavaScript 应用要跨用户群体进行扩展,那么它的外观必须是可配置的。 这个要求可以在任何地方从一个可配置的标志,到有可能彻底改变 UI 的外观和感觉的可互换的主题。

一般来说,外观的更改围绕着 CSS 属性(如字体、颜色、宽度、边框半径等)进行。 虽然大多数 CSS 实现确实没有被大多数 JavaScript 开发人员触及,但我们仍然需要意识到主题边界。

例如,如果我们对外观和它的配置方式很灵活,我们可以让用户在运行时选择自己的主题。 因此,我们需要实现一个用户交互的主题切换机制。 此外,主题化的 ui 意味着首选项需要存储和加载到某个地方。

这就是粗粒度主题——那么细粒度外观配置呢? 然而,前者更为普遍,配置个别组件的特定样式并非不可能。 外观粒度级别与其他影响伸缩性的因素一致,比如软件部署的位置和配置 api 的功能。

支持地区

在我们的所有组件中提供国际化支持是个好主意。 事实上,有很多的 JavaScript 工具可以帮助完成这个任务。 有些更独立,有些更适合特定框架。 使用这些工具很容易,但是本地化还有很多需要考虑的地方,特别是在可伸缩的环境中。

决定支持的地区

一旦我们有了具有国际化支持的产品软件,下一步就是决定支持哪些地区。 在执行确保所有组件国际化的第一步时,我们只使用一种语言环境——默认语言环境。 这在一开始是没问题的,在我们的第一个辅助语言环境支持需求之前可能需要几年的时间。

这通常发生在较新的软件项目中。 我们知道国际化应该在我们的优先事项清单上,但很容易因为其他事情正在进行而偏离方向。 支持不把精力花在现场支持上的主要理由是,现场支持不是马上就需要的。 反对这种思维方式的论点是,随着组件的增长,国际化是非常难以实现的。 所以这又是一个与规模相关的权衡。 我们是想让我们的应用跨文化扩展,还是立即上市时间更重要?

除了特殊的情况,我们假设国际化是必须的—我们需要优先考虑我们将支持哪些地区,而不是那些可以等待的地区。 例如,在实际需要大量语言环境支持之前就瞄准它是一个糟糕的主意。 区域设置占用物理空间,需要有人维护这些区域设置。 因此,如果没有客户为这种增加的可伸缩性复杂性付出代价,这是不值得的。

相反,选择的地区应该完全基于客户需求。 如果我们在一个地方有数百人寻求支持,而询问另一个地方的人不到 12 人,那么优先级应该是显而易见的。 如果我们将区域设置支持的优先级与特性支持的优先级相同,将会很有帮助。

维护本地环境

首先,如果我们支持一个给定的区域设置,我们需要翻译字符串消息,显示在整个 UI 中。 其中一些字符串是在模板文件中静态编码的,而其他字符串是在 JavaScript 模块中找到的。 只要找到这些字符串,并将它们转换一次就好了。 但琴弦很少会永远保持不变——经常会有细微的调整。 此外,随着软件的发展和组件的增加,需要翻译的字符串也越来越多。

字符串翻译的比例因素是我们支持的语言环境的数量——这就是为什么我们需要认真地只支持有限数量的语言环境,而我们可以侥幸成功。 复杂性并不止于此。 例如,有些消息字符串可以直接从源语言映射到目标语言。 像语法屈折——单词如何在修饰的基础上获得不同的意思——这样的事情就不是那么简单了。 事实上,这些用法有时需要专门使用国际化库。

其他可本地化的数据,如日期/时间格式,不需要太多的维护。 对于给定的语言环境,整个应用使用一到两种格式。 对于这样的格式,客户可能会对其文化中使用的标准格式感到满意。 幸运的是,我们可以在项目中使用公共区域数据存储库(CLDR)数据——一个可下载的公共区域数据存储库。 这是一个很好的起点,因为这些数据在大多数情况下都足够好,并且很容易根据请求重写。

设置区域设置

一旦我们有了国际化库和几个地区,我们就可以开始从不同文化的角度测试我们的应用的行为。 对于这种行为,有许多项需要考虑。 例如,我们需要为用户提供便利的地区选择,我们需要跟踪该选择。

选择地区

在 JavaScript 应用中,有两种常见的区域设置方法。 第一种方法是使用accept-language请求头。 第二种方法是在用户设置页面上使用选择器小部件。

accept-language方法的优点是不涉及用户输入。 我们的应用被发送到用户的浏览器语言首选项,从那里,我们可以设置语言环境。 挑战在于,从可用性的角度和实现的角度来看,这种方法可能有太多的限制。 例如,用户可能无法控制他们的浏览器语言首选项,或者浏览器可能没有应用支持的地区首选项。

注释

accept-language请求头的另一个技术挑战是,没有简单的方法将请求头从浏览器传递给 JavaScript 代码——这有点疯狂,因为浏览器中有这两种代码! 例如,如果我们的 JavaScript 代码需要知道地区偏好,以便它可以加载适当的地区数据,它将需要访问accept-language头文件。 要做到这一点,我们需要后台黑客。

更灵活的方法是向用户提供一个地区选择器小部件,然后从那里显式地显示用户希望激活的地区。 然而,我们需要找到一种方法来存储这个区域设置,这样用户就不必重复选择他们的区域设置。

存储区域设置参数

区域设置首选项一旦被用户选择,就可以存储为一个 cookie 值。 下次在浏览器中加载应用时,我们将准备好区域设置首选项。 然后我们可以用适当的选择标记选择器,并加载相关的区域设置数据。

在 cookie 中存储区域设置首选项的问题是,如果用户移动到另一个浏览器,将需要重复相同的选择过程。 这可能是一个真正的问题,因为如今用户的移动性越来越强,在一台设备上做出的改变应该反映在应用使用的任何地方。 而这对于饼干来说是不可能的。

如果我们使用一个后端 API 来存储区域设置首选项,用户就可以在任何地方使用它。 下一个挑战是加载相关的区域设置数据,以便我们的其他组件可以使用它。 通常,我们希望在开始呈现数据之前准备好这些数据,因此它是我们向后端发出的第一个请求。 有时,所有地区作为一个资源一起提供服务。 如果我们支持很多地区,这可能是个问题,因为加载它的前期成本。

另一方面,一旦我们加载了区域设置首选项,我们就只能加载立即需要的区域设置。 这将提高初始加载时间,但代价是切换到新的语言环境会更慢。 这种情况不太可能经常发生,所以最好不要加载从未使用过的区域设置数据。

Storing locale preferences

JavaScript 应用首先加载区域设置首选项,然后使用它来加载本地数据

locale in uri

除了将本地首选项存储在后端或作为 cookie 值外,区域设置还可以作为 URI 的一部分进行编码。 通常,它们被表示为两个字符的代码,例如enfr,并且位于 URI 的开头。 使用这种方法的优点是首选项不需要存储空间。 我们仍然可能希望用户使用一个选择器来选择他们首选的语言环境,但这将导致存储一个新的 URI 而不是首选项值。

像这样在 uri 中编码首选区域设置与基于 cookie 的方法有相同的缺点。 虽然我们可以将一个 URI 标记为书签,或者将一个 URI 传递给其他人——他们将看到与我们相同的 locale——但问题是,这不是一个永久的首选项。 请注意,我们总是可以存储首选项并在加载应用时更新 URI。 但是,由于路由和 URI 生成的复杂性,这种方法伸缩性不好。

通用组件配置

正如我们在前面关于区域设置首选项的小节中所看到的,我们需要加载一个首选项值,然后每个组件都可以使用这个值。 或者在 locale 中可能只有一个组件,但是这个首选项值会间接影响所有组件。 除了区域设置之外,我们还需要在组件中配置许多其他东西。 本节从一般的角度来看这个问题。 首先,我们需要决定给定组件的哪些方面是可配置的,然后是在运行时将这些首选项导入组件的机制。

决定配置值

组件配置的第一步是决定首选项——组件的哪些方面需要配置,哪些方面可以保持静态? 这还远远不是一门精确的科学,因为我们后来往往会意识到一些静态的东西应该是可配置的。 尝试和错误是寻找可配置偏好的最佳过程,特别是在我们的软件刚刚起步的时候。 过多的初始配置考虑是扩展的瓶颈。

当某些内容不可配置时,它具有简单性的优点。 它更有结构感,活动部分更少。 这就消除了潜在的边缘情况和性能问题。 使价值可配置的预先理由并不经常发生。 随着软件的成熟,我们将有一个更好的视角,将一些偏好放在适当的位置,我们将有一个更好的想法,期待什么。

例如,我们将开始看到多个组件之间的重复。 它们在很大程度上是相同的,只有细微的变化。 如果我们不断添加新类型的组件,而这些组件之间的差异微乎其微,那么我们就会遇到规模的麻烦。 我们的代码库将增长到难以管理的规模,并且我们将使开发人员感到困惑,因为给定组件的职责将变得模糊。

这就是,我们利用可配置性来实现规模。 这是通过引入支持新组件类型的首选项来实现的。 例如,假设我们需要一个新视图,该视图除了处理 DOM 事件的方式之外,还需要与已经在多个地方使用的另一个视图相同。 我们不会实现新的视图类型,而是增强现有的视图,以接受覆盖此事件默认值的新函数值。

另一方面,我们不能随意地引入组件首选项。 当我们这样做的时候,我们用新的瓶颈代替了旧的扩展瓶颈。 需要考虑性能,因为我们添加的每一个新的可配置首选项都会影响性能。还有代码的复杂性——使用首选项不像使用静态值那样简单。 可能会引入与其他开发人员在相同开发周期中引入的其他首选项不一致的首选项。 最后,还要跟踪和记录给定组件可用的所有各种首选项。

存储并硬编码默认值

就组件而言,首选项应该尽可能接近于常规的 JavaScript 变量。 这使我们的代码保持了灵活性——用静态值替换首选项不会有太大的影响。 常规变量通常用初始值声明,首选项也应该用默认值声明。 这样的话,如果我们因为某些原因不能得到存储在后台的首选项,软件将继续使用一个合理的默认值。

对于任何首选项都应该总是有一个备用默认值,并且这些值应该记录在某处。 理想情况下,使用的默认值适用于一般情况,因此并非每个首选项都需要为了使用该软件而进行修改。 如果出于某种原因,我们不能从后端访问存储的配置值,那么硬编码的默认值将使软件继续运行,尽管使用的配置并不理想。

提示

有时候,不能访问配置值是不可能的,软件应该快速失败,而不是使用硬编码的默认值。 虽然软件使用默认设置是完全正常的,但这取决于我们的客户和他们的部署,这种模式可能比软件不可用更糟糕。 部署大规模 JavaScript 应用时需要考虑的一些问题。

默认的首选项值使得在后端删除修改过的首选项值是安全的。 可以将其视为对出厂设置操作的重置。 换句话说,如果我们通过调整偏好值将问题引入到软件中,我们可以只删除我们的存储值。 如果不需要在后端存储默认值,那么就没有覆盖默认值的风险。

Stored and hard-coded default values

默认值总是存在的,但是很容易被后端的首选项值覆盖

后端影响

如果我们在后端存储首选项值,为用户提供可移植性,那么我们需要一些机制,允许我们在配置存储中放入新的首选项值,并检索我们的首选项。 理想情况下,这应该是一个 API,允许我们定义任意的键值首选项,并允许我们通过一个请求检索所有配置。

这对前端开发非常有价值的原因是,我们可以在开发组件时为它们定义新的偏好,而不会对后端团队造成干扰。 就后端 API 而言,前端配置是任意的——无论是否有 UI, API 的工作方式都是一样的。

有时候,这实际上是一件让人头痛的事情。 如果只有很少的变化——整个应用只需要少量的配置值,该怎么办? 如果是这种情况,我们可以考虑维护一个静态 JSON 文件,作为前端配置。 它是任意的,我们可以特别定义首选项,就获取首选项值而言,它的工作原理与 API 相同。

当存在用户定义的首选项时,这种方法就不太管用了。 例如,用户的首选区域设置。 我们的应用可能指定了一个默认的区域设置,直到用户更改它。 他们改变了自己的偏好,而不是系统中的每个用户。 这就是我们需要前面提到的配置 API 的地方。 它在数据库中存储这些值的方式很可能是用户敏感的。 但并非所有偏好值都是如此; 有些是部署运营商设置的,用户不能触摸。

Backend implications

当前用户会话可用于加载特定于该用户的首选项; 这与系统设置不同,系统设置不会因用户而变化

加载配置值

有两种方法可以加载前端所需的配置。 第一种方法是加载所有配置,因为任何东西都在 UI 中呈现。 这意味着在路由开始处理任何事情之前,我们要等待配置可用。 这通常意味着等待加载配置数据的承诺。 这里明显的缺点是,初始加载时间会受到影响。 好处是,我们已经拥有了前进所需要的一切——没有更多的配置请求。

我们可以在浏览器中使用本地存储来缓存首选项值。 它们很少改变,而且这种策略有可能提高初始加载性能。 另一方面,它增加了复杂性—所以只有在有很多配置值并且加载它们所花费的时间很明显的情况下才考虑这一点。

首选项值可以按需加载,而不是预先加载所有配置的。 也就是说,当组件即将被实例化时,会请求它的配置。 这具有高效的吸引力,但再次强调,需要多少配置才能保证如此复杂? 努力在可能的情况下预先加载所有应用配置。

Loading configuration values

与后端通信的配置组件为获取或设置首选项值的任何组件提供了抽象

配置行为

如果实现的很好,我们组件的行为在很大程度上是自包含的。 他们暴露给外界的是偏好,这些偏好会对他们的行为做出微妙的调整。 这可能是着眼于内部的东西——比如使用的模型类型,或者首选算法。 它可以是面向用户的,例如启用组件或设置显示模式。 正是这些偏好帮助我们扩展组件以适应各种环境。

启用和禁用组件

一旦我们的软件达到一定的临界量,并不是所有的功能都与所有的用户相关。 简单的能力切换组件之间的启用/禁用的状态是一个强大的工具。 对于作为软件供应商的我们和我们的客户来说都是如此。 例如,我们知道在我们的软件中某些用户角色需要某些特性,但它们不是常见的情况。 为了更好地为普通用户优化,我们可以选择禁用某些不经常使用的高级功能。 这可以清理布局,提高性能,等等。

另一方面,我们可能会在默认情况下打开所有功能,但如果组件能够关闭,那么用户就可以决定哪些功能与它们相关。 如果他们能够按照自己的喜好安排 UI,删除对他们没有特别用处的元素,那么就能获得更好的用户体验。

在这两种情况下,就整体布局而言都有影响。 如果我们不花时间以可伸缩的方式设计布局,那么切换组件真的不会增加任何价值。 在设计布局的过程中,我们需要遍历用户可能使用的或我们自己可能使用的各种配置场景。

Enabling and disabling components

禁用页面上的组件可能会更新页面布局; 我们的风格需要能够处理这一点

改变数量

UI 中显示的内容数量最多只是设计时的猜测。 我们希望列表中显示的项目数量是最佳数量,用户不必为改变这些类型的首选项而烦恼。 问题是数量是非常主观的。 它更多的是关于使用我们的应用执行任务的个人,取决于他们的习惯,他们在使用我们的软件时做了什么,以及许多其他因素,数量偏好默认可能不是最佳的。

一个常见的量问题是我想在屏幕上显示多少实体? 实体可以是在整个应用中使用的通用网格小部件、搜索结果页面或任何呈现事物集合的其他东西。 我们可以选择默认显示较小数量,同时允许满足用户需求的较大数量。

提示

检查用户提供的首选项总是一个好主意。 一种保护措施是在适当的地方放置一个允许值的选择,而不是接受任意的用户输入。 例如,我们不应该允许在一个网格中渲染 1000 个实体。 尽管,返回该数据的 API 也应该检查并限制数量参数。

另一个数量方面的考虑是我们需要显示哪些实体属性? 在网格的情况下,我们可能希望看到某些列,而隐藏其他列。 像这样的偏好应该是持久的,因为如果我们努力建立我们想要看到的数据,我们就不会想要重复这样的努力。

当我们改变数量偏好时,就会产生后端影响。 在要渲染多少实体的情况下,我们可能希望在获取数据时将这个约束传递给 API——获取我们不打算显示的东西是没有意义的。 也可能有模型或集合的含义。 在确定我们希望在特定 UI 区域中显示哪些数据的情况下,我们可以只向模型或集合询问它们所拥有的数据的子集。

改变顺序

集合在 UI 中呈现的顺序是另一种常见的行为偏好,也是我们最希望支持的。 这里最大的影响是配置某些东西的默认顺序。 例如,按照修改日期对每个集合进行排序,以便最近的实体最先出现,这是一个很好的默认设置。

许多网格组件允许用户在升序和降序之间切换给定列的顺序。 这些都是行动,不一定是偏好。 然而,如果默认顺序不是我们想要的,那么它们可能会变成令人讨厌的操作。 因此,我们可能希望引入一种方法,让用户为任何给定的网格提供默认排序首选项,同时保留单击列标题进行特别排序的能力。

更复杂的排序首选项是可能的,可单击的列标题在这里并不总是有帮助。 例如,如果我们想要按没有在 UI 中呈现的东西(如相关性或畅销书)进行订购,该怎么办? 也许我们可以使用一个控制方法,但它是的另一个潜在偏好——因为它可以帮助提供更好的体验。

// users.js
export default class Users {

    // Accepts a "collection" array, and an "order"
    // string.
    constructor(collection, order) {
        this.collection = collection;
        this.order = order;

        // Creates an iterator so we can iterate over
        // the "collection" array without having to
        // directly access it.
        this[Symbol.iterator] = function*() {
            for (let user of this.collection) {
                yield user;
            }
        };
    }

    set order(order) {

        // When the order break it down into it's parts,
        // the "key" and the "direction".
        var [ key, direction ] = order.split(' ');

        // Sorts the collection. If the property value can be
        // converted to lower case, they it's converted to avoid
        // case inconsistencies.
        this.collection.sort((a, b) => {
            var aValue = typeof a[key].toLowerCase === 'function' ?
                a[key].toLowerCase() : a[key];

            var bValue = typeof b[key].toLowerCase === 'function' ?
                b[key].toLowerCase() : b[key];

            if (aValue < bValue) {
                return -1;
            } else if (aValue > bValue) {
                return 1;
            } else {
                return 0;
            }
        });

        // If the direction is "desc", we need to reverse the sort.
        if (direction === 'desc') {
            this.collection.reverse();
        }
    }

}

// main.js
import Users from 'users.js';

var users = new Users([
    { name: 'Albert' },
    { name: 'Craig' },
    { name: 'Beth' }
], 'name');

console.log('Ascending order...');
for (let user of users) {
    console.log(user.name);
}
//
// Albert
// Beth
// Craig

users.order = 'name desc';

console.log('Descending order...');
for (let user of users) {
    console.log(user.name);
}
//
// Craig
// Beth
// Albert

配置通知

当用户在我们的应用中执行某些操作时,比如打开或关闭某些操作,我们需要提供关于该操作状态的反馈。 它成功了吗? 它失败了吗? 这是跑步吗? 这些通常是通过通知来完成的,呈现为屏幕角落或面板某处的短暂弹出窗口。

用户可能想要控制他们如何被通知的某些方面——没有什么比收到我们不关心的垃圾信息更令人恼火的了。 因此,与通知相关的一个首选项可能是通知主题的选择。 例如,我们可能希望不通知不相关的实体类型。

另一个潜在的偏好可能是给定通知在屏幕上保持活动状态的持续时间。 例如,它是应该一直呆在原地直到我们承认它,还是应该在三秒钟后消失? 在极端情况下,用户可能会想要完全关闭通知,如果没有其他方法可以让它们不那么烦人。 如果需要的话,总是有操作日志供以后方便地浏览。

内联选项

我们如何收集用户偏好输入? 对于不太活跃的全局应用首选项,将设置页面划分为多个类别可能是有意义的。 然而,必须在设置页面上配置特定于单个小部件的内容有点烦人。 有时使用内联选项会更好。

内联意味着用户可以使用作为 UI 一部分的元素设置他们的首选项。 例如,选择要在网格中显示的特定列。 将这样的偏好隐藏在某个设置页面中是没有多大意义的。 当偏好控件相对于它们所控制的对象进行定位时,需要的解释就更少了。 当控件与上下文相关时,用户通常可以更容易地理解其含义。

注释

上下文偏好控制的缺点是它们有可能使 UI 混乱。 如果页面上有很多组件,每个组件上都有首选项控件,那么我们很可能会造成混乱,而不是方便。

改变外观和感觉

今天,应用的外观和感觉是静态的、不变的方面已经不常见了。 相反,他们提供了一些主题供用户选择。 或者,软件内置了对轻松创建主题的支持。 这允许我们的客户决定我们的软件应该如何为他们的用户寻找。 除了更新应用的外观和感觉的打包主题外,还可以设置单独的样式首选项。

主题工具

如果我们想让我们的应用能够根据要求改变主题,我们必须在 CSS 和使用它的标记中加入大量的设计和架构。 虽然这个主题已经超出了本书的范围,但是有必要看看帮助生成主题的可用工具。

在这方面,我们可以使用的第一个工具是 CSS 框架。 和 JavaScript 框架一样,CSS 框架定义了一致的模式和约定。 然后,就由我们这些组件作者来确定如何将这些 CSS 模式应用到组件及其生成的标记上。 可以把主题看作是一堆风格偏好。 当配置改变时,外观会因为新的首选项值而改变。 使 CSS 模块成为主题的是,与应用使用的所有其他主题定义相同的属性——只是这些属性的值发生了变化。

我们可以使用的另一个工具是后端构建过程的一部分—css 编译器。 这些工具接收使用 CSS 方言的文件,并对它们进行预处理。 这些类型的预处理器语言的优点在于,我们可以更好地控制如何指定样式首选项。 例如,CSS 中没有变量这样的东西,但是预处理器有它们,这是一个非常方便的可配置特性。

选择主题

一旦我们有了一个可主题的用户界面,我们就需要一种方法来加载特定的主题实例。 即使不允许用户选择自己选择的主题,通过改变偏好值来改变设计也是很好的。 当我们决定实现一个新的设计时,这无疑会使部署到生产环境中变得更加简单。

未来,我们可能会决定让用户选择自己的主题。 例如,我们可能已经获得了许多用户,但现在却出现了对这种能力的需求。 我们可以像在系统中使用的任何其他首选项值一样创建主题选择器。 我们需要有某种主题选择小部件,用户所做的选择可以映射到路径,因为这可能是将一个主题交换到另一个主题所需要的全部内容。

另一种可能是根据用户的角色将不同的主题设置为默认主题。 以为例,如果管理员登录,那么有一个不同的视觉提示会很有帮助,提示您实际上是作为特定类型的用户登录的。 这种类型的东西在有截图的场景中很有帮助。

个人风格偏好

应用的外观可以在单个元素级别更改。 也就是说,如果我们想改变某个东西的宽度,我们可以在屏幕上改变它。 或者也许我们不喜欢正在使用的字体,我们也想改变它,但没有别的。

应该避免这些类型的细粒度样式首选项,因为它们伸缩性不好。 我们的组件必须注意特定的样式考虑,这在大多数情况下降低了组件的真正用途。 在某些情况下,为屏幕选择不同的布局没有坏处,因为这通常意味着将一个 CSS 类交换到另一个 CSS 类。

另一种可能是使用拖放交互来设置物体的大小。 但是,最好将这些交互保存为短暂的交互,而不是持久的首选项。 我们希望优化公共配置值,而根据个人喜好调整元素大小并没有什么共同之处。

性能影响

在本章的最后,我们将概述到目前为止讨论的各种配置领域所带来的性能影响。 如果我们真的需要某个区域的配置值,因为它们会增加价值,那么它们可能会损害整体性能——因此我们需要以某种方式抵消这种成本。

可配置区域设置性能

到目前为止,与区域设置相关的最显著的性能瓶颈是初始负载。 这是因为在实际为用户呈现任何内容之前,我们必须加载所有的 locale 数据。 这包括字符串消息翻译,以及本地化所需的所有其他数据。 当预先加载了多个区域设置时,初始化期间的性能会受到进一步的限制。

提高加载性能的最佳方法是只加载用户实际需要的语言环境。 一旦他们设置了这个首选项,他们就不太可能频繁地更改它,因此在附近准备好其他地区数据并没有真正的好处。

由于大量数据需要通过我们正在使用的本地化机制,所以在渲染视图时不可避免地会出现减速。 这本身不太可能导致性能问题,因为大多数操作都是小而高效的——简单的查找和字符串格式化。 但是额外的开销是存在的,并且需要加以考虑。

可配置行为性能

改变组件行为的配置对性能的影响也很小。 事实上,可配置行为的性能特征类似于可配置区域设置的性能特征。 最大的挑战是初始配置负载。 在那之后,它只是一个执行查找的问题,这是快速的。

需要注意的是,当我们有很多组件需要配置时。 虽然单个查找速度很快,但当有大量查找时,性能会受到影响。 这需要相当长的一段时间才能达到这一点,但风险仍然存在。

下面的示例演示了如何在集合排序时进行配置,从而影响其他依赖于顺序且经常被调用的操作的性能:

// users.js
export default class Users {

    // The users collection excepts data, and an
    // "order" property name.
    constructor(collection, order) {
        this.collection = collection;
        this.order = order;
        this.ordered = !!order;
    }

    // Whenever the "order" property is set, we need
    // to sort the internal "collection" array.
    set order(key) {
        this.collection.sort((a, b) => {
            if (a[key] < b[key]) {
                return -1;
            } else if (a[key] > b[key]) {
                return 1;
            } else {
                return 0;
            }
        });
    }

    // Finds the smallest item of the collection. If the
    // collection is ordered, then we can just return the
    // first collection item. Otherwise, we need to iterate
    // over the collection to find the smallest item.
    min(key) {
        if (this.ordered) {
            return this.collection[0];
        } else {
            var result = {};
            result[key] = Number.POSITIVE_INFINITY;

            for (let item of this.collection) {
                if (item[key] < result[key]) {
                    result = item;
                }
            }

            return result;
        }
    }

    // The inverse of the "min()" function, returns the
    // last collection item if ordered. Otherwise, it looks
    // for the largest item.
    max(key) {
        if (this.ordered) {
            return this.collection[this.collection.length - 1];
        } else {
            var result = {};
            result[key] = Number.NEGATIVE_INFINITY;

            for (let item of this.collection) {
                if (item[key] > result[key]) {
                    result = item;
                }
            }

            return result;
        }
    }

}

// main.js
import Users from 'users.js';

var users;

// Creates an "ordered" users collection.
users = new Users([
    { age: 23 },
    { age: 19 },
    { age: 51 },
    { age: 39 }
], 'age');

// Calling "min()" and "max()" doesn't result in
// two iterations over the collection because they're
// already ordered.
console.log('ordered min', users.min());
console.log('ordered max', users.max());
//
// ordered min {age: 19}
// ordered max {age: 51}

// Creates an "unordered" users collection.
users = new Users([
    { age: 23 },
    { age: 19 },
    { age: 51 },
    { age: 39 }
]);

// Every time "min()" or "max()" is called, we
// have to iterate over the collection to find
// the smallest or largest item.
console.log('unordered min', users.min('age'));
console.log('unordered max', users.max('age'));
//
// unordered min {age: 19}
// unordered max {age: 51}

行为偏好可以用来将一个功能完全替换为另一个功能。 他们可能有相同的接口,但有不同的实现。 决定在运行时使用哪个函数并不昂贵,但也需要考虑内存消耗。 例如,如果我们的应用中有许多支持不同函数的首选项,那么除了将函数存储为首选项值外,我们还必须存储默认实现。

可配置的主题性能

唯一真正的延迟,我们可以期待从可配置主题是初始成本,确定使用哪个主题。 然后是下载它并将样式应用到标记的过程——这与使用单一静态样式集的应用没有任何不同。 如果我们允许用户切换主题,需要等待新的 CSS 和相关的静态资源下载和渲染。

小结

本章介绍了在大规模 JavaScript 应用中可配置性的概念。 主要的配置类别是区域设置、行为和外观。 locale 是当今 web 应用的重要组成部分,因为没有什么能阻止世界上任何地方的人们使用我们的应用。 不过,国际化也存在规模挑战。 它给我们的开发生命周期增加了复杂性,并且还增加了维护本地环境的成本。

首选项需要存储在某个地方。 将它们存储在浏览器中可以工作,但这种方法不能移植性。 将首选项存储在后端并在应用初始化时加载它们更为合适。 扩展大量首选项存在许多挑战,包括区分用户定义的首选项和系统首选项。 我们是否包含了合理的硬编码默认值并不重要。

应用的样式是另一个可配置的维度。 有一些框架和构建工具可以帮助我们为外观和感觉构建主题。 可配置组件的性能考虑很少—下一章将讨论在扩展软件时突然出现的性能挑战。