有些人更喜欢用 React 编写 JavaScript。对于其他人来说,则是 Vue 或 jQuery。对于其他人来说,则是他们自己的一套工具或一个完全空白的文档。有些设置非常简单,有些可以让你快速完成工作,而有些则功能强大,可以让你构建复杂且可维护的应用程序。每个设置都有其优缺点,但在经过活跃社区验证和审查的流行框架中,优点通常大于缺点。
React 和 Vue 是强大的 JavaScript 框架。当然它们是——这就是为什么两者都 在整体使用率中趋势如此之高。但是什么让这些框架和其他框架如此强大?是速度吗?像原生桌面和移动设备这样的其他平台的可移植性?庞大社区的支持?
开发团队的成功始于达成一致。关于如何做事达成一致。如果没有达成一致,代码就会变得杂乱无章,软件很快就会变得不可持续,即使对于相对较小的项目也是如此。因此,框架的大部分(如果不是全部)功能都存在于此协议中。定义每个人都遵守的约定和通用模式的能力。这个想法适用于任何框架,无论是否为 JavaScript。这些“规则”对于开发至关重要,并为任何规模的团队带来 **可维护性**、代码的 **可重用性** 以及 **能够与社区共享** 工作的能力,以便任何人都能理解。这就是为什么我们可以网络搜索一些解决我们问题的组件/插件,而不是自己编写一些东西。我们依赖开源替代方案,无论我们使用什么框架。
不幸的是,框架也存在缺点。正如 Adrian Holovaty(Django 的创建者之一)在他的 演讲 中解释的那样,框架往往会随着针对特定用例的功能而变得笨重和臃肿。这可能是包含了一个非常好的想法。也可能是希望为任何人和每个人的需求提供一刀切的解决方案。也可能是为了保持流行。无论哪种方式,考虑到框架的所有优点,它们也存在缺点。

在服务器渲染的世界中,我们处理从服务器传递到浏览器的內容,这个“框架使命”充满了人们与同事、朋友和其他人分享的技能、想法和解决方案。他们倾向于采用一致的开发规则并在其团队或公司内部执行这些规则,而不是依赖预定义的框架。长期以来,jQuery 一直允许人们通过其插件在全球范围内共享和重用代码,但随着新框架的出现,这个时代正在逐渐消失。
让我们看看任何框架(自定义与否)都应该认为对鼓励可维护和可重用的代码库至关重要的核心要点。
命名约定
有些人可能会说 命名是开发中最困难的部分。无论是命名 JavaScript 变量、HTML 类还是其他任何数量的事物,命名约定都会对我们如何协同工作产生重大影响,因为它为我们的代码添加了上下文和含义。命名约定的一个很好的例子是 BEM,这是一种由 Yandex 的人创建并被许多前端开发人员采用的 CSS 方法。这种标准化和一致性可以帮助我们了解我们的目标,即为命名 JavaScript 选择器制定一个标准约定。
这种情况可能看起来很熟悉:在几个月后重新访问一个项目,你会看到页面上发生了一些行为——也许是你想修改或修复的内容。但是,查找该行为的来源可能是一件棘手的事情。是我应该搜索的 id
属性吗?是其中一个类吗?也许是此特定元素的数据属性?
我想你明白了。有某种约定可以简化流程并让每个人都使用相同的名称会有所帮助。而且我们是否使用类(例如 js-[name]
)或数据属性(例如 data-name="[name]"
)实际上并不重要。重要的是名称符合相同的样式。当然,你遇到的任何框架也会强制执行某种命名约定,无论是 React、Vue、Bootstrap、Foundation 等。
作用域
开发人员可能会在某个时间点与 JavaScript 作用域作斗争,但我们在这里特别关注 DOM 作用域。无论你使用 JavaScript 做什么,你都在使用它来影响某些 DOM 元素。将代码的某些部分限制到 DOM 元素可以提高可维护性并使代码更模块化。React 和 Vue 中的组件以类似的方式运行,尽管它们的“组件”概念与标准 DOM 不同。尽管如此,这个想法仍然将代码的作用域限定到由这些组件呈现的 DOM 元素。
在我们讨论 DOM 操作时,引用组件根元素内的元素非常有帮助,因为它有助于避免不断需要选择元素。 React 和 Vue 以非常好的方式开箱即用地实现了这一点。
生命周期
过去,页面生命周期非常简单:**浏览器加载页面**和**浏览器离开页面**。创建或删除事件侦听器,或在正确的时间运行代码要简单得多,因为你只需在加载时创建所有你需要的东西,并且很少费心禁用你的代码,因为浏览器会默认为你完成此操作。

如今,由于状态管理以及我们更喜欢直接操作 DOM 或使用 JavaScript 加载页面内容的方式,生命周期往往更加动态。不幸的是,这通常会带来一些问题,你可能需要在某些时候重新运行代码的部分。
我无法说出我一生中有多少次不得不修改代码以使我的事件处理程序在重写 DOM 的一部分后正确运行。有时可以使用事件委托来解决此问题。其他时候,你的代码处理程序根本不运行或运行多次,因为它们突然附加了两次。
另一个用例是在更改 DOM 后需要创建库的实例,例如“伪选择”。
无论如何,生命周期都很重要,将代码限制到 DOM 元素当然也有帮助,因为你知道如果元素重新呈现,则需要重新运行应用于该元素的代码。
框架使用函数(如 componentDidMount
或 componentDidUpdate
)执行相同的操作,这为你提供了一种在需要时准确运行代码的简单方法。
可重用性
复制其他地方的代码并重用它非常普遍。谁没有使用过过去项目的代码片段或 StackOverflow 上的答案呢?人们甚至发明了像 npm 这样的服务,其目的只有一个:轻松共享和重用代码。无需重新发明轮子,与他人共享代码既方便又实用,并且通常比从头开始编写更有效。
组件,无论是 React、Vue 还是框架的任何其他构建块,都通过在执行某些特定目的的代码片段周围添加包装器来鼓励可重用性。一个具有强制格式的标准化包装器。我想说,这更多是拥有我们可以构建的基础的副作用,而不是为了可重用性的刻意努力,因为任何基础总是需要它可以运行的某种标准代码格式,就像编程语言具有我们需要遵循的语法一样……但这绝对是一个非常方便且有用的副作用。通过将这种获得的可重用性与 Yarn 等包管理器和 webpack 等捆绑器相结合,我们可以在几乎无需任何努力的情况下,让世界各地许多开发人员编写的代码在我们的应用程序中协同工作。
虽然并非所有代码都是开源且可共享的,但通用的结构也将帮助小型团队中的成员,因此任何人都能够理解团队中编写的大部分代码,而无需咨询其作者。
牢记性能的 DOM 操作
读取和写入 DOM 是很耗费资源的。前端框架的开发者都牢记这一点,并尝试使用状态、虚拟 DOM 和其他方法尽可能地优化,以便在需要时以及在需要的地方对 DOM 进行更改……我们也应该这样做。虽然我们更关注服务器端渲染的 HTML,但大多数代码最终都会读取或写入 DOM。毕竟,这就是 JavaScript 的用途。
虽然大多数 DOM 更改都很小,但它们也可能发生得相当频繁。一个很好的频繁读取/写入的例子是在页面滚动时执行脚本。理想情况下,我们希望避免在处理程序中直接添加类、属性或更改元素内容,即使我们编写相同的类、属性或内容,因为我们的更改仍然会被浏览器处理,并且仍然很昂贵,即使用户没有看到任何变化。
大小
最后但并非最不重要的是:大小。对于框架来说,大小至关重要,或者至少应该如此。本文讨论的代码旨在用作许多项目的基准。它应该尽可能小,并避免任何可能为一次性用例手动添加的冗余代码。理想情况下,它应该是模块化的,以便我们可以根据项目的具体需求,按需提取其中的部分。

虽然自写代码的大小通常是合理的,但许多项目最终都会至少有一些依赖项/库来填补空白,而这些依赖项/库可能会很快使代码变得非常庞大。假设我们在网站的顶级页面上有一个轮播,在更深层的地方有一些图表。我们可以转向现有的库来处理这些事情,例如 slick 轮播(10 KB 压缩/gzip,不包括 jQuery)和 highcharts(73 KB 压缩/gzip)。这超过了 80 KB 的代码,我敢打赌,并非每个字节对我们的项目都是必需的。正如 Addy Osmani 解释 的那样,JavaScript 是昂贵的,其大小与页面上的其他资产不同。下次您发现自己需要使用库时,值得记住这一点,尽管如果好处大于坏处,这并不应该阻止您这样做。
看看我们称为 Gia 的最小解决方案
让我们看看我在 Giant 公司与我的团队一起使用过一段时间的东西。我们喜欢 JavaScript,我们希望直接使用 JavaScript 而不是框架。但同时,我们需要框架提供的**可维护性和可重用性**。我们尝试从流行的框架中获取一些概念,并将它们应用到服务器端渲染的网站中,JavaScript 设置的选择范围非常广泛,但也非常有限。
我们将介绍我们设置的一些基本功能,并重点关注它如何解决我们迄今为止讨论的要点。请注意,我们需求的许多解决方案都直接受到大型框架的启发,因此如果您看到一些相似之处,请知道这不是偶然的。我们称之为 Gia(明白了吧,就像 Giant 的简写?!),您可以在 npm 上获取它,并在 GitHub 上找到源代码。
Gia,就像许多框架一样,是围绕组件构建的,组件为您提供了基本构建块以及一些我们稍后会介绍的优势。但不要将 Gia 组件与 React 和 Vue 中使用的组件概念混淆,在 React 和 Vue 中,所有 HTML 都是组件的产物。这与 Gia 不同。
在 Gia 中,组件是您自己代码的包装器,它在 DOM 元素的范围内运行,并且组件的实例存储在元素本身中。因此,如果元素从 DOM 中移除,垃圾回收器会自动将其实例从内存中移除。组件的源代码可以在 此处 找到,并且非常简单。
除了组件之外,Gia 还包含一些辅助函数来应用、销毁和处理组件实例或组件之间的通信(为方便起见,包含了标准的 事件总线)。
让我们从基本的设置开始。Gia 使用属性来选择元素,以及要执行的组件的名称(即 g-component="[component name]"
)。这强制执行一致的命名约定。运行 loadComponents
函数会创建使用 g-component
属性定义的实例。
查看 Georgy Marchuk 在 CodePen 上创建的笔 基本组件(@gmrchk)。
Gia 组件还允许我们使用 g-ref
属性轻松选择根元素内的元素。所有需要做的就是定义预期的引用以及我们是在处理单个元素还是元素数组。然后可以在组件内的 this.ref
对象中访问引用。
查看 Georgy Marchuk 在 CodePen 上创建的笔 带有 ref 元素的组件(@gmrchk)。
作为奖励,可以在组件中定义默认选项,这些选项会自动被 g-options
属性中包含的任何选项覆盖。
查看 Georgy Marchuk 在 CodePen 上创建的笔 带有选项的组件(@gmrchk)。
组件包含在不同时间执行的方法,以解决生命周期问题。以下是一个示例,展示了如何初始化或移除组件
查看 Georgy Marchuk 在 CodePen 上创建的笔 加载/移除组件(@gmrchk)。
请注意 loadComponents
函数在组件已存在时不会应用相同的组件。
在重新渲染组件的根元素或其中的元素之前,没有必要移除附加到它们的监听器,因为这些元素无论如何都会从 DOM 中移除。但是,可能会在全局对象(例如 window
)上创建一些监听器,例如用于处理 scroll
的监听器。在这种情况下,需要在销毁组件实例之前手动移除监听器,以避免内存泄漏。
组件作用域到 DOM 元素的概念在本质上类似于 React 和 Vue 组件,但有一个关键区别,即 DOM 结构位于组件外部。因此,我们必须确保它适合组件。定义 ref 元素肯定会有所帮助,因为 Gia 组件会告诉您所需的 ref 是否不存在。这使得组件可重用。以下是可轻松重用或共享的基本轮播的示例实现
查看 Georgy Marchuk 在 CodePen 上创建的笔 基本轮播组件(@gmrchk)。
在谈到可重用性时,重要的是要提到组件不必以其现有状态重用。换句话说,我们可以扩展它们来创建新的组件,就像任何其他 JavaScript 类一样。这意味着我们可以准备一个通用组件并在其基础上构建。
举个例子,一个可以为我们提供光标与元素中心之间距离的组件,看起来将来可能会有用。这样的组件可以在 此处 找到。准备好之后,在其基础上构建和使用提供的数字就变得非常容易,如下一个示例在渲染函数中所示,尽管我们可以争论这个特定示例是否有用。
查看 Georgy Marchuk 在 CodePen 上创建的笔 ZMXMJo(@gmrchk)。
让我们尝试研究优化的 DOM 操作。检测是否应该发生 DOM 更改可以手动存储或检查,而无需直接访问 DOM,但这往往需要大量工作,我们可能希望避免。
这就是 Gia 真正从 React 中汲取灵感的地方,它具有简单、精简的组件状态管理。组件的状态设置方式类似于 React 中的状态,使用 setState
函数。
也就是说,我们的组件中不涉及渲染。内容由服务器渲染,因此我们需要在其他地方使用状态更改。状态更改会被评估,任何实际更改都会发送到组件的 stateChange
方法。理想情况下,组件与 DOM 之间的任何交互都应该在这个函数中处理。如果状态的某些部分没有更改,它将不会出现在传递给函数的 stateChanges
对象中,因此不会被处理——DOM 不会在没有真正必要的情况下被触碰。
检查以下示例,其中一个组件显示视口中可见的部分
查看 Georgy Marchuk 在 CodePen 上创建的笔 视口中部分组件(@gmrchk)。
请注意,只有在状态实际发生更改的项目上才会写入 DOM(通过闪烁可视化)。
现在我们来到了我最喜欢的部分!Gia 确实是极简的。包含所有代码(包括所有辅助函数)的整个包仅占用 2.68 KB(压缩/gzip)。更不用说您很可能不需要 Gia 的所有部分,并且最终会使用打包器导入更少的代码。

如前所述,代码的大小可能会因包含第三方依赖项而迅速增加。因此,Gia 还包含代码分割支持,您可以在其中为组件定义依赖项,该依赖项仅在首次初始化组件时加载,而无需对打包器进行任何其他设置。这样,您网站或应用程序深处使用的庞大库就不必减慢速度。
如果您有一天决定确实想要利用大型框架在代码中的所有好处,那么就像加载任何其他组件依赖项一样简单。
class SampleComponent extends Component {
async require() {
this.vue = await import('vue');
}
mount() {
new this.vue({
el: this.element,
});
}
}
结论
我认为这篇文章的主要观点是,编写可维护和可重用代码不需要框架。遵循和执行一些概念(框架也使用这些概念)本身就可以让你走得很远。虽然 Gia 非常简洁,并且没有像 React 和 Vue 这样的巨头提供的许多强大的功能,但它仍然帮助我们获得了长期以来非常重要的干净结构。它还包含了一些更有趣的内容,这里没有列出。如果你喜欢到目前为止的内容,就去看看吧!
用例有很多很多,不同的需求需要不同的方法。各种框架可以为你完成很多工作;在其他情况下,它们可能会限制你。
你的最小设置是什么,以及你如何处理我们在这里讨论的要点?你更喜欢框架还是非框架环境?你是否将框架与像 Gatsby 这样的静态站点生成器结合使用?告诉我吧!
哇,看起来很有前景。我会尽快尝试一下!感谢你的工作和信息。
你是否查看过 t3js,如果是,除了它在模块系统方面有点过时之外,你对此有什么看法?我最近研究了一些针对此场景(服务器渲染网站)的选项,而 t3js 是我找到的唯一一个。非常有兴趣进一步了解 gia。
老实说,我第一次听说 t3js。正如你所说,除了过时之外,它看起来非常简洁。据我了解,它的目标非常相似,但与 Gia 相比,我认为它对编写风格/结构的强制性更强。Gia 更多的是一小段 JavaScript,让你有一个良好的开端并推动你走向模块化,但它仍然只是一小段 JS,你可以用任何方式处理和修改它。
我们还在考虑添加更多功能以处理需要更多控制和将代码分离到多个协同工作的组件中的更大任务,以便在需要时赋予它更多的“应用程序”能力。
精彩的文章,Georgy!
我想了解更多关于使用 Gia 的整体网站结构的信息——例如,页面是否扩展组件、嵌套组件、Gia 和 Swup 演示。