前面我发过一篇文章,脱离了Spring询问大家能不能继续开发,结果文章下面的评论和回复都告诉我大家伙的基础打得很牢固,该咋写还是咋写。看得我在这内卷的时代瞬间躺平。
那么今天挑战升级,不用任何框架开发 Web 应用程序,你能做到么?
首先,我们要思考一个问题:
不使用框架等同于重复造轮子吗?
过去流行的是 Angular,然后是 React,现在是 Vue.js……其他的像 Ember、Backbone 或 Knockout 什么的几乎都快消失了。一些标准,例如 Web Components,则很少被使用。似乎每年都会发布一些新框架,比如 Svelte、Aurelia,而且每个框架在服务器端都有对应的对象(开头那些框架对应的 NestJS、NextJS 或 Nuxt,Svelte 对应的 Sapper,等等)。非 JavaScript Web 框架(如 Django、Spring、Laravel、Rails 等)就更不用说了。甚至还有框架之上的框架(Quasar、SolidJS)、为框架生成组件代码的框架(Stencil、Mitosis),以及 NCDP(无代码开发平台,No-Code Development Platform)。
这种多样性让想知道哪种技术值得学习的开发人员和技术选型决策者感到困惑。
网络上经常会出现一些比较这些框架的文章,好像是在帮助我们解开这种困惑。但大多数作者通常是带有偏见的,因为他们可能“用过这个框架”,但“只尝试了一些其他的框架”。偏见程度较低的作者总是得出“这取决于具体情况”的结论(取决于性能、工具、支持、社区等),这实际上是一种非结论性的结论。
即使一些基准测试基于同一个应用程序对不同的框架进行了比较,也很难获得真实的结果,因为这种基准测试受限于被测试的应用程序(比如待办事项应用程序)。
框架看起来就像是宗教(或者说是政治):每一个框架都假装自己为开发者提供了解决方案,但每一个又都不一样。它们每一个都声称可以为应用程序提供最好的前景,但关于哪一个真正名副其实的争论又不绝于耳。每一个框架都要求你遵循特定的规则,它们之间可能有相似之处,但要从一个框架转换到另一个框架总是很难。所以没有什么一说:Angular天下第一;Vue是天。ヾ(=・ω・=)o
现在,让我们来看看框架的“无神论”方法:不使用框架。
为什么不使用框架?
实际上,这个想法还很新。早在 2017 年,Django Web 框架联合创始人 Adrian Holovaty 就谈到了他的框架“疲劳”,以及他为什么离开 Django 去构建自己的纯 JS 项目。
有人可能会问,为什么会有人想要在不使用框架的情况下开发 Web 应用程序?为什么不在其他人花了数年时间和精力的成果的基础上做开发?或者是因为 NIH(Not Invented Here)综合症导致人人都想构建定制的框架?
开发人员并不会比一般人更倾向于自找麻烦,实际上,他们可能比任何人都懒:他们只会想写更少的代码(这样他们就可以更少犯错),想要自动化(以避免人为错误)……
能摆谁不摆呢?
但他们又想要敏捷,也就是能够轻松、快速地解决问题。
虽然“快速”似乎是框架承诺的东西(为你搭建脚手架,并增加可靠性),但它不是免费的:它们想让你签署合同,同意支付“税”费,并将你的代码放入“孤井”(“税和孤井”的说法来自 IBM Carbon 系统设计团队负责人 Akira Sud)。
框架税
使用框架是需要付出成本的:
1) 想要一个新功能(即使你不需要所有功能,也必须升级所有东西);
2) 想要修复一个 bug;
3) 不想失去框架的支持(随着新版本的发布,你的应用程序所依赖的版本将会被弃用)。
一旦你把钱放入框架,就很难把它拿出来。要么砸碎他,当然可能最好的方法就是一开始就不塞进去。
框架孤井
除了必须支付“税”费来获得框架的好处之外,如果框架没有标准化,它们还会带来另一个问题。
因为它们强制要求你遵循框架规则——而且每一条规则都不一样——这意味着你的应用程序将与一个专有的生态系统绑定在一起,也就是用专有 API(及其升级过程)锁定你的应用程序代码。这对于你的项目来说是一个冒险的赌注,正如它们所暗示的那样:
当然,在项目刚开始时,你可以选择最流行的框架。对于一个短期的项目来说,这可能是可以接受的,但对于长期项目来说则不然。
框架来来去去。从 2018 年开始,每年都有 1 到 3 个新框架取代旧框架。
不过,标准框架并不存在孤井。在 Web 平台(即浏览器框架)上,使用标准 Web API 可以降低你的投入风险,因为它们可以在大多数浏览器上运行。即使不是所有的浏览器都支持,仍然可以通过 polyfill 来弥补。
例如,现在的 Web 组件既可移植(几乎可以在所有浏览器中使用),又可互操作(可以被任何代码使用,包括专有框架),因为它们可以被封装成任意的 HTML 元素。不仅具备更好的性能,它们的运行时(自定义元素、阴影 DOM、HTML 模板)还作为浏览器的一部分运行,所以它们已经在那里(不需要下载),并且是原生的。
但很少会有开发者试图逃离框架孤井。
那么框架本质上就是不好的吗?
如果是为实现应用程序逻辑而创建自己的框架,那就不能说框架是不好的:任何应用程序都需要实现自己的业务规则。
如果符合以下这些情况,框架就是好的:
去框架化的目标
简单地说,避免使用框架来构建应用程序的目标是:
当然,我们的目标也不能是“重新发明轮子”。我们来看看该怎么做。
框架之外的选择
那么,如何在没有框架的情况下开发应用程序呢?
首先,我们必须明确一个反目标:不要将“不使用框架构建应用程序”与“取代框架”混淆起来了。框架是一种用于托管任意应用程序的通用技术解决方案,所以它们的目标并非你的应用程序,而是所有的应用程序。相反,脱离框架才有可能让你更专注于你的应用程序。
不使用框架开发应用程序并不意味着要重新实现框架。
要评估在不使用框架的情况下构建应用程序的难度,我们要明白:它不像构建框架那么困难,因为以下这些不是我们的目标:
因此,构建一个普通的应用程序并不是一项艰巨的“重新发明轮子”的任务,因为这个“轮子”主要是关于 API/ 合约、实现、通用引擎和相关的优化、调试能力等。放弃通用目标,专注于应用程序的目标,这意味着你可以摆脱大部分目标,而这才是真正的“专注于你的应用程序”。
那么,我们该如何设计和实现一个普通的应用程序?因为大多数应用程序都是使用框架构建的,所以如果没有这些熟悉的工具,确实很难设计出一种方法来实现类似的结果。你必须:
一些作者,如 Jeremy Likness 或 Chris Ferdinandi(被称为“JS 极客”)也提到过这个话题。但是,根据定义,任何一个普通的应用程序都可以选择(或不选择)使用其中的一种技术,具体视需求而定。例如,MeetSpace 的作者只需要使用标准 API 就足以。
接下来,让我们来看看一些常见的“解法”。
标准
标准 API 属于“好的框架”,因为它们:
在选择编程语言时,我们要着重考虑标准。JavaScript 经过多年的发展,现在也包含了在其他编程语言中出现的特性,比如 class 关键字和通过 JSDoc 注释(如 @type)提供有限的类型检查支持。
很多编程语言可以被编译成 JavaScript:TypeScript、CoffeeScript、Elm、Kotlin、Scala.js、Haxe、Dart、Rust、Flow 等。它们都为你的代码添加了不同的价值(类型检查、额外的抽象、语法糖)。普通的应用出现应该使用它们吗?为了回答这个问题,让我们来看看它们是否隐含了与框架相同的缺点:
在一个普通的应用程序中,我们要小心谨慎地使用非超集语言,因为它们或多或少都隐含了一些约束。超集语言(TypeScript、Flow)通过避免“要么全有要么全无”来最小化这些约束,我们应该在它们可以带来价值的地方使用它们。
需要注意的是,在 JavaScript 之上构建的语言层意味着我们的工具链中又增加了一层复杂性,可能会因为某些原因招致失败(见下文)。此外,在经过编译或转译之后,开发阶段的好处也会消失(通常在运行时不会强制执行类型或可见性约束检查)。
开发库
基于不“重写框架”的假设,就会得出普通的 JS 应用程序不应该使用开发库的结论。这是完全错误的。“重新发明轮子”,即从头开始重写一切,并不是一个明智的目标。我们的目标是消除框架(而不是开发库)中隐含的约束,请不要将其与“自己编写一切”的教条混淆在一起。
因此,如果你自己不能编写某些代码(可能是因为没有时间,或者因为需要太多的专业知识),使用开发库并没有什么错。你只需要关心:
需要注意的是,不要被那些声称它们不是框架的文档或文章所迷惑(因为它们“没有被明确定义”成框架,或者没有定义一个“完整的应用程序”):只要隐含了约束,它们就是框架。
模式
Holovaty 说,只是应用模式(不使用框架)来构建软件是不够的。
模式是众所周知的东西,不特定于某种开发过程。它们本身是自我文档化的,因为它们可以被有经验的开发人员快速识别出来。
这里仅举几个例子:
这样的模式有很多:你可以自由地用它们来满足你的需求。如果一个模式为你的应用程序的一个典型问题提供了典型的解决方案,你一定要用它。更宽泛地说,任何符合 SOLID 原则和具有良好内聚力的东西都有利于应用程序的灵活性和可维护性。
更新视图
在面试开发者时,当被问及在构建一个普通应用程序时他们主要会担心哪些东西时,他们大多数会回答:实现复杂的模型变化检测和后续的“视图”更新。这是典型的“工具法则”效应,它会让你按照框架的思路思考问题,但实际上你的一些简单的需求根本不需要用到框架:
模板
开发人员不希望缺失的另一个特性是编写带有动态部分或监听器的 HTML 片段。
首先,DOM API(如 document.createElement("button"))并不是那么难,而且实际上比任何模板语言都更强大,因为你可以全面访问这些 API。编写很长的 HTML 片段可能很乏味,如果它们真的很长,可以将它们拆分成更细粒度的组件。
不过,将这些元素视为模板确实可以提高可读性。那么该如何管理它们呢?这里有多种方法:
模板中的条件或循环语句该怎么办?且不说这可能从来都不是一个好主意(UI 中不应该包含逻辑),你可以(也应该)只用 JS 来实现逻辑,然后使用上面的技术将结果插入到模板中。
事件
现在,我们有了基本的模板,那么该如何将事件绑定到 DOM 节点呢?这里也有几种选择:
那么定制或业务事件该怎么办?如果我需要对应用程序的某个组件触发的一些事件作出反应该怎么办?这里也有多种处理方式:
组件
虽说开发普通的应用程序不同于开发复杂的基础设施(也就是用于托管组件的容器),但如果一些东西在系统中会多次出现,那么将它们设计成可重用组件(与上下文无关)仍然是一个好主意。无论你使用何种技术,也无论是业务还是技术,一定程度粒度的抽象仍然是有用的:将与同一业务概念相关的数据和规则封装成一个可重用的对象,或者构建可以在应用程序多个地方进行实例化的小部件,总归是个好主意。
创建组件的方法有很多,具体视自己的需求而定。早在 2017 年,Mev-Rael 就提出了很多技巧,用于处理 JavaScript 组件的状态、自定义属性和视图。当然,我们不要拘囿于别人推荐的技术,而是要先考虑自己的需求,然后再选择合适的技术。
除了标准的小部件组件(通常是标准的 Web 组件),任何一个组件都应该能够:
在任何情况下,无论你选择了什么样的设计策略,你的组件(或者更具体地说,它的相关“视图”)都必须能够提供一些 HTML 渲染结果。你可以使用包含 HTML 代码的字符串,但 HTMLElement(或 Element)通常是更好的选择(可读性高,直接更新,可以绑定事件处理器),而且性能更好(不需要解析)。
此外,你可能希望使用来自第三方的外部组件。由于专有框架的流行程度较高,它们可以更大程度地利用社区开发的库和组。它们中的大多数实际上与纯 JS 实现的特性(比如 JQuery)并没有太大不同,但问题是,它们缺乏互操作性,所以到最后你会发现自己需要的其实是纯 JS 或 Web 组件。
所幸的是,这样的库确实存在,比如 Vanilla JS Toolkit,尽管可能不太常见。在 Web 组件方面,webcomponents.org 列出了 2000 多个元素。甚至还有普通的 Web 组件,只是它们与我们要讨论的不太相关(更多的是关注轻量级实现,而不是互操作性)。
路由
在 SPA 中管理路由需要使用 Web History API。虽然这并不复杂,但你仍然可能希望将其委托给简单的路由器库,如 Navigo。
你所要做的就是在路由时用一个 DOM 元素替换另一个 DOM 元素(使用 replaceChildren() 或 replaceWith() 方法)。
延迟加载
按需加载 JavaScript 代码是任何一个 Web 应用程序都需要考虑的问题。你一定不希望为了显示一个登录界面而加载全部的应用程序代码。
早在 2009 年,在 Web 框架出现之前,James Burke(Dojo 开发者)就发布了 RequireJS(最开始叫“RunJS”)来解决这个问题。从那时起,随着模块化的出现,出现了更多的技术。从 ES6(2015)开始,我们可以动态加载代码。在 Node 中可以,在浏览器中也可以:
{WelcomeModule} = await import("./welcome/ModuleImpl")``module = new WelcomeModule()
那么如何将模块分拆到单独的文件中?打包器(如 Webpack)可以为你做这些工作。
需要注意的是,在导入路径里你应该只使用常量,否则打包器就无法猜到你想要加载什么,就会将所有可能的文件都打包在一个文件中。例如,await import(./welcome/${moduleName}
) 将把所有东西都打包到指定的目录中,因为打包器不知道变量 moduleName 在运行时会是什么。
原生应用程序
越来越多的框架为原生平台(如 React Native)提供了运行、迁移或编译应用程序的方法,以便将它们作为独立应用程序部署到 Android 或 iOS 移动系统上。
除了考虑开发真正的原生应用程序之外,更普遍的解决方案是将 Web 应用程序嵌入到原生容器中,比如之前的 PhoneGap(现已停止维护)或 Apache Cordova,现在的 NativeScript(它支持框架,如 Angular,也支持普通的应用程序),或者像 Electron 这样的原生 Web 应用程序包装器,或者 Electron 的轻量级后继者 Tauri。
服务器端渲染
很多框架在前端和后端运行的代码是相似的,这样更容易实现对 SEO 友好的服务器端渲染(SSR)。
这可能是一个又酷又便利的特性,但需要注意的是,它也可能导致服务器锁定。因此,在向应用程序引入框架锁定之前,你需要考虑它对项目、基础设施、客户端技术等方面的影响。
所幸的是,你也可以在不使用框架的情况下实现这个特性。
从服务器端渲染
采用普通的实现方案在一开始看起来很简单:不就是返回 HTML 吗?是的,你已经有现成的组件了,但是:
添加交互性
然而,一旦 HTML 元素被转换成字符串,在这些元素上设置的所有事件处理器都丢失了。为了恢复交互性,你需要一些“补水”步骤,也就是注入脚本,让它们在客户端执行。框架因其普适性很难做到这一点。就拿影子 DOM 来说,它们不断尝试改进算法,希望能够以最聪明的方式做到这一点,但如果我们把问题缩小到应用程序层面,就会变得简单很多。
当然,在普通的服务器应用程序中做到这一点也意味着需要将 JS 脚本注入到响应消息中(通过引用或内联,具体取决于你想要怎样的“渐进”程度,比如将 Web 组件所需的代码嵌入到 HTML 响应中,让它们在客户端执行)。
普通的解决方案让你可以控制在哪里、什么时候以及附加哪些东西:你可以先只发送 HTML,再加载基本的交互性 JavaScript,然后加载更多(取决于用户的操作),等等。
这比本文中提到的任何一个东西都简单,因为它们是应用程序代码,而不是通用的框架代码。
国际化
多年来,国际化问题都是通过库来处理的(最终也被集成到框架中)。要自己集成这些库也很容易,但你也可以选择自己实现一个,因为与通用库相比,自己的实现可以支持更简单、更有效的消息类型。
就是这么简单:
interface WelcomeMessages {` `title: string`` greetings(user: string, unreadCount: number): string``}``class WelcomeMessage_en implements WelcomeMessage {` `title = "Welcome !",` `` greetings = (user, unreadCount) => `Welcome ${user}, you have ${unreadCount} unread messages.` ```}``class WelcomeMessage_fr implements WelcomeMessage {` `title = "Bienvenue !",` `` greetings = (user, unreadCount) => `Bienvenue ${user}, vous avez ${unreadCount} nouveaux messages.` ```}
这里为你提供了:
你所需要做的就是(加载和)实例化与用户语言环境相关的消息类。通用库不会提供这种特定于业务的消息类型。
工具
如果你想要摆脱对强约束性软件技术栈的依赖,那你很可能也想摆脱对工具的依赖:你不希望只有靠着它们(它们的局限性、性能、错误、版本)才能向前走。你不希望被一个你无法解决的构建问题(或者需要数小时或数天才能解决)所困扰(特别是如果你使用的是最近构建的版本,而它们还没有经过充分的实战测试)。
话虽如此,你仍然很难避免使用这些工具。大多数情况下,你的产品代码必须以某种方式打成包,包括缩小体积、混淆、代码拆分、摇树优化、延迟加载、包含样式等。毫无疑问,现有的打包工具如 Webpack、Parcel、ESBuild 或 Vite 会做得比你更好。
你所能做的是:
最大的挑战
说到底,最大的挑战不是技术上的,而是关于人的:
其他人可能会跟你说:
毫无疑问,性能最好的框架是那些在普通代码之上添加层数较少的框架。框架的“优化”更多的是为了弥补框架本身的开销。
不使用框架构建 Web 应用程序并非意味着要自己构建框架,它是关于在不使用通用引擎的情况下开发应用程序,目的是:
也就是只编写特定于应用程序的代码(业务和技术),包括使用开发库。你真正应该关注的框架是你自己的框架,也就是那个特定于应用程序的框架。这是真正的“专注于业务”,也是最有效的。
这并没有你想象的那么难,特别是有了现代标准的加持(在必要时主流浏览器可以通过 polyfill 来支持新特性)。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。