这个标题多少有些标题党了,毕竟我作为前端在想做快速原型的时候肯定会优先考虑Next,但这两天写外包项目的途中,我突然激发了一些思考:
•前后端分离的意义是什么?我们真的需要前后端分离吗?•现代前端工具链带给了我们什么?我们失去了什么?•如果仅仅依靠浏览器和Web规范,不寻求前端构建生态链的帮助,我们究竟能走多远?
前后端分离的意义
虽然我自己是干前端的,但之前对前后端分离一直持怀疑态度,因为前端本身就是从Web后端拆分出来的工种,所谓分分合合,分开了不久现在又有了合起来的趋势。
以前,是后端兼职写写HTML,后来被特化为了前端;现在是写JS的人越来越多,在哪都想用JS一把梭,狭义上的“全栈”其实就是指的是用JS同时完成前后端的人(毕竟在古老的年代没有“全栈”一说,大家都是全栈)。
一次当技术Leader的经历
看法发生改变是在接下某个外包项目后:这个外包项目是JS全栈项目,但是并没有前后端分离,而且是以后端(Koa.js)为主导的,前端则大部分是使用模板语言实现,辅以少量的JS脚本(使用jQuery)。
更恐怖的是,这个项目大部分脚手架如ORM、后端框架(可以理解自己在Koa基础上写了个类NestJS的框架)和前端构建逻辑都是原作自己写的,我自己花了大量的时间去理解整个项目的结构。
由于工期紧,我急需一些人来帮我分担简单的前端页面设计,为此,我基于原有框架为其加上了React和Vue的支持,试图让外包能够快速上手添加页面。
然而事与愿违,我发现大部分外包虽然能比较娴熟地出页面(但其实大部分都没考虑到项目的整体可维护性),但他面对我已经搭好的架子仍然无从下手,因为这不是他们熟悉的前端架构,他们不知道他们写的SPA应该加在哪里,每次都要花时间去解释或者自己修改接入。
我陷入了僵局,要找会后端的人简单,会前端的人也简单,但要找到这样一个能熟悉架构、既能改前端又能改后端的人,很难,也很贵。
所以在这里,我第一次体会到了前后端分离的意义,不是技术上的,而是项目管理意义上的,前后端分离的项目从更高的层面来看维护成本更低、长期来看也更能持续发展。
为什么会有前端框架?
其实前端开发相较Web开发而言应该算比较年轻的职业,我记得我小学的时候就玩过WordPress,那时PHP还在大行其道(虽然现在WP也还在用PHP),HTML5还没落地,Flash尚在巅峰时期,jQuery都是新鲜事物。
彼时国内Web开发可能都还是一片混沌,更罔论“前端工程师”作为一个专门的职业出现。
再后来,我们熟悉的Web标准逐渐成型,HTML5、CSS3和ES6的出现,为当下纷繁复杂的Web世界打下了牢固(?)的地基。
我们也迎来了前端开发的第一个时代——jQuery的时代,它是一个对JS DOM操作的极简封装,降低了开发者动态操作DOM的开发成本,促使了第一批动态网页的诞生。
数据驱动的前端框架
然而,jQuery仍采取了传统的面向过程编程思想,形象地说是控制流决定了数据流,而非数据流决定了控制流。
在UI开发中,数据流决定控制流才是更自然的形式,在桌面端有WinForm向WPF的转变,在Web端自然也会有相似的演变,于是Meta/Facebook便带着React轰轰烈烈地来了。
React虽然经历了一系列编程范式的摇摆变化,如从最初的OOP思想(Class组件)到现在的函数式编程(Function组件),其竞争者也在源源不断地解决React本身的问题。但它们的本质都是想要提出一种构建UI和逻辑的更好方式,解决以下痛点问题:
•声明式的渲染,而非过程化的操作,如:自动数据绑定、自动响应式更新等•数据驱动逻辑,如通过接口更新数据即可自动刷新UI•代码的可重用,如单文件组件模式
使用框架的代价
对于久旱逢甘霖的前端开发者而言,上面的糖果实在太诱人了,迫不及待地想要成为这一批吃上螃蟹的人。
然而,凡事总有代价,首当其冲的就是数据绑定所依赖的JSX语法并不被浏览器所支持,这就意味着基于React编写的代码需要再经过一步额外的“编译”才能在浏览器中直接运行,这就是“前端工具链”梦开始的地方。
•编译器说,我要一个适合前端开发的运行时,便有了NodeJS•开发者说,我不想造轮子,便有了npm•开发者说,我要一个统一的构建工具平台来跑轮子,于是便有了webpack
以上三大件(运行时、包管理器和构建平台)构成了整个庞大前端工具链的地基,但这还远远不够,前端开发在这条路上一直越卷越远:
•转化工具还不够好,要可拓展,要Native,要快,于是就从tsc到babel,再到esbuild和swc...•考虑兼容性太麻烦,我就想用最新的语法,于是有了polyfill插件,有了post-css•CSS样式污染严重,于是有了各种预处理器(sass/less)、CSS Modules和CSS-in-JS方案•不想仅仅运行在浏览器上,想要有自己的中间层来渲染HTML、处理API,于是有了Next,有了GraphQL和各种BFF(Backend For Frontend)
随着前端生态链在NodeJS的基础上不断开枝散叶,前端开发变得越来越复杂,边界也不断拓展,但大家可能并没有注意到“前端”代码跑在浏览器上的部分越来越少,跑在本机(Node)上的部分越来越多。
这对于专职于前端开发的人来说可能并不是什么问题,甚至可能是好事(技术深度更深了,可替代性就弱了),但对于做全栈的独立开发者来说,就并非如此了,特别是对于后端开发而言,需要维护两个项目,并理解两套完全不同的开发体系。
全栈开发的变迁
前端生态的蓬勃发展,随之而来的就是后端向Web UI开发的近乎停滞。几乎所有做UI的人都在考虑前端,如何让前端能做更多事,如何让写JS的人也能做整个App,而没有人关心如何让写CPP/Python/Go的人能写出更好看、交互性更强的UI。
诚然,现在Node生态的蓬勃发展已经让JS成为了全栈开发者的第一选择,像Next这样的元框架更是开发者快速实现原型的第一选择。
然而,面向后端的Web UI开发这个市场始终存在,并不会因为市场的忽视就消失,就像WASM(如微软的Blazor,支持将C#代码编译为可执行的Web代码)和HTMX库的爆火就证明了这一市场始终存在。
想要倒行逆施一把...
“元框架”(meta-framework)的概念是前端圈的概念,因为一些用于提供基础功能的视图库(ui library,如React)等占据了“框架”这一生态位,因此像Next、Remix这样提供全套解决方案的真正“框架”(类比Java中的SpringBoot)就只能被叫做“元框架”。
目前来看,所谓“全栈”的流行方案是以前端为主导的元框架,如前段时间因Server Action长得像PHP爆火出圈的NextJS。
这些元框架的特点是围绕前端进行构建:
•围绕JavaScript/TypeScript和NodeJS生态构建,下限高,上限低(指计算密集型应用)•与现代前端技术(如React、Vue等)和前端工具链生态强绑定,必须使用DSL (如Vue的template语法和React/Solid使用的JSX本质上都是一种DSL) 和构建工具•以前端页面和组件为核心,将组件生命周期拆成服务端和客户端两部分•服务端:数据获取和预渲染,甚至可以直接从服务器获取数据从而省略后端开发•客户端:加载页面逻辑•元框架的工作内容就是统一服务端和客户端的逻辑,降低开发者心智负担
但是,在这个项目里,如果给我一次从头再来的机会,我会想继续以后端为中心的架构,尽可能减少对原框架的修改。
那么,如果我们以Express为核心构建一个网站,在2024年的体验如何呢?
模板引擎选哪个?
要想直接在后端框架渲染HTML,一般少不了模板引擎,模板引擎在传统HTML的基础上增加了如组件复用、数据插入等重要功能。
一般来说,我们根据后端语言来选择合适的模板引擎,如Python一般选择jinjia2。
在JS的世界里,我们的可选项有很多,如EJS、Jade/Pug、nunjucks等。这里我们参考express官方的template engine[1]推荐。
这里是我个人比较熟悉或推荐的选择:
•EJS[2]:语法简单直观且高性能的模板语言,是Hexo(一种博客框架)的模板解决方案;•nunjucks[3]:语法与jinjia2类似,功能更多(如模板继承、宏定义等),适合Python转JS的用户,Mozilla出品;•Pug/Jade[4]:非常著名的模板语言,与HTML语法差异较大,Vue支持这一模板语言。
应该用回jQuery...吗?
想到编写“简单”的页面逻辑,大家想到的除了原生JS应该就是jQuery了吧,JQ帮我们封装了常用的DOM操作以提高API的易用性。
趁着jQuery 4.0发布第一版之际,51cto也出来蹭了波大热度:
用 React/Vue 不如用 JQuery,你知道吗?-51CTO.COM [5]
这种小丑文章我们姑且不谈,但JQ的面向过程语法在写交互时属实还有些心智负担。
比如,我们使用JQ实现一个根据状态显隐的组件:
<script src="https://code.jquery.com/jquery-3.6.0.min.js">
<span style="display: none;">Content...
$(document).ready(function() {
// 定义一个变量来控制内容的显示与隐藏
let isOpen = false;
// 绑定点击事件到按钮
$('button').click(function() {
// 切换isOpen的值
isOpen = !isOpen;
// 根据isOpen的值来显示或隐藏内容
if (isOpen) {
$('span').show();
} else {
$('span').hide();
}
});
});
可以看到,我们需要添加