大家好,我卡颂。
创新互联公司2013年成立,先为策勒等服务建站,策勒等地企业,进行企业商务咨询服务。为策勒企业网站制作PC+手机+微官网三网同步一站式服务解决您的所有建站问题。
从全球web发展角度看,框架竞争已经从第一阶段的前端框架之争(比如Vue、React、Angular等),过渡到第二阶段的全栈框架之争(比如Next、Nuxt、Remix等)。
这里为什么说全球,是因为国内web发展方向主要是更封闭的小程序生态
在第一阶段的前端框架之争中,不管争论的主题是「性能」还是「使用体验」,最终都会落实到框架底层实现上。
不同框架底层实现的区别,可以概括为「更新粒度的区别」,比如:
那么,进入第二阶段的全栈框架之争后,最终会落实到什么的竞争上呢?
我认为,会落实到「业务逻辑的拆分粒度」上,这也是各大全栈框架未来会卷的方向。
本文会从「实现原理」的角度聊聊业务逻辑的拆分粒度。
「性能」永远是最硬核的指标。在前端框架时期,性能通常指「前端的运行时性能」。
为了优化性能,框架们都在优化各自的运行时流程,比如:
在web中,最基础,也是最重要的性能指标之一是FCP(First Contentful Paint 首次内容绘制),他测量了页面从开始加载到页面内容的任何部分在屏幕上完成渲染的时间。
对于传统前端框架,由于渲染页面需要完成4个步骤:
框架能够优化的,只有步骤2、3,所以FCP指标不会特别好。
SSR的出现改善了这一情况。对于传统的SSR,需要完成:
在第一步就能统计FCP,所以FCP指标优化空间更大。
除此之外,SSR还有其他优势(比如更好的SEO支持),这就是近几年全栈框架盛行的一大原因。
既然大家都是全栈框架,那不同框架该如何突出自己的特点呢?
我们会发现,在SSR场景下,业务代码既可以写在前端,也能写在后端。按照业务代码在后端的比例从0~100%来看:
合理调整框架的这个比例,就能做到差异化竞争。
按照这个思路改进框架,就需要回答一个问题:一段业务逻辑,到底应该放在前端还是后端呢?
这就是本文开篇说的「逻辑拆分」问题。我们可以用「逻辑拆分的粒度」区分不同的全栈框架。
下述内容参考了文章wtf-is-code-extraction。
在Next.js中,文件路径与后端路由一一对应,比如文件路径pages/posts/hello.tsx就对应了路由http(s)://域名/posts/hello。
开发者可以在hello.tsx文件中同时书写前端、后端逻辑,比如如下代码中:
// hello.tsx
export async function getStaticProps() {
const postData = await getPostData();
return {
props: {
postData,
},
};
}
export default function Post({ postData }) {
return (
{postData.title}
{postData.id}
{postData.date}
);
}
通过以上方式,在同一个文件中(hello.tsx),就能拆分出前端逻辑(Post组件逻辑)与后端逻辑(getStaticProps方法)。
虽然以上方式可以分离前端/后端逻辑,但一个组件文件只能定义一个getStaticProps方法。
如果我们还想定义一个执行时机类似getStaticProps的getXXXData方法,就不行了。
所以,通过这种方式拆分前/后端逻辑,属于比较粗的粒度。
我们可以在此基础上修改,改变拆分的粒度。
首先,我们需要改变之前约定的「前/后端代码拆分方式」,不再通过具体的方法名(比如getStaticProps)显式拆分,而是按需拆分方法。
修改后的调用方式如下:
// 修改后的 hello.tsx
export async function getStaticProps() {
const postData = await getPostData();
return {
props: {
postData,
},
};
}
export default function Post() {
const postData = getStaticProps();
return (
{postData.title}
{postData.id}
{postData.date}
);
}
现在,我们可以增加多个后端方法了,比如下面的getXXXData:
export async function getXXXData() {
// ...省略
}
export default function Post() {
const postData = getStaticProps();
const xxxData = getXXXData();
// ...省略
}
但是,Post组件是在前端执行,getStaticProps、getXXXData是后端方法,如果不做任何处理,这两个方法会随着Post组件代码一起打包到前端bundle文件中,如何将他们分离开呢?
这时候,我们需要借助编译技术,上述代码经编译后会变为类似下面的代码:
// 编译后代码
/*#__PURE__*/ SERVER_REGISTER('ID_1', getStaticProps);
/*#__PURE__*/ SERVER_REGISTER('ID_2', getXXXData);
export const method1 = SERVER_PROXY('ID_1');
export const method2 = SERVER_PROXY('ID_2');
export const MyComponent = () => {
const postData = method1();
const xxxData = method2();
// ...省略
}
让我们来解释下其中的细节。
首先,这段编译后代码可以直接在后端执行,执行时会通过框架提供的SERVER_REGISTER方法注册后端方法(比如ID为ID_1的getStaticProps)。
由于SERVER_REGISTER方法前加了/*#__PURE__*/标记,这个文件在打包客户端bundle时,SERVER_REGISTER会被tree-shaking掉。
也就是说,打包后的客户端代码类似如下:
export const method1 = SERVER_PROXY('ID_1');
export const method2 = SERVER_PROXY('ID_2');
export const MyComponent = () => {
const postData = method1();
const xxxData = method2();
// ...省略
}
当以上客户端代码执行时,在前端,SERVER_PROXY方法会根据id请求对应的后端逻辑,比如:
实际上,通过这种方式,可以将任何函数作用域内的逻辑从前端移到后端。
比如在下面的代码中,我们在按钮的点击回调中访问了数据库并做后续处理:
export function Button() {
return (
);
}
这个「按钮点击逻辑」显然无法在前端执行(前端不能直接访问数据库)。但我们可以通过上述方式将代码编译为下面的形式:
import {SERVER_REGISTER, SERVER_PROXY} from 'xxx-framework';
/*#__PURE__*/ SERVER_REGISTER('ID_123', () => {
// 访问数据库
const post = await db.posts.find('xxx');
// ...后续处理
});
export function Button() {
return (
);
}
编译后的代码可以在后端直接执行(并访问数据库)。对于前端,我们再打包一个bundle
(tree-shaking
掉后端代码),类似下面这样:
import {SERVER_PROXY} from 'xxx-framework';
export function Button() {
return (
);
}
相比于粗粒度的逻辑分离方式(文件级别粒度),这种方式的粒度更细(函数级别粒度)。
中粒度的方式有个缺点 —— 分离的方法中不能存在客户端状态。比如下面的例子,点击回调依赖了id状态:
export function Button() {
const [id] = useStore();
return (
);
}
如果遵循之前的分离方式,后端取不到id的值:
import {SERVER_REGISTER, SERVER_PROXY} from 'xxx-framework';
/*#__PURE__*/ SERVER_REGISTER('ID_123', () => {
// 获取不到id的值
const post = await db.posts.find(id);
// ...后续处理
});
export function Button() {
const [id] = useStore();
return (
);
}
为了解决这个问题,我们需要进一步降低逻辑分离的粒度,使粒度达到状态级。
首先,相比于中粒度中将内联方法提取到模块顶层(并标记/*#__PURE__*/)的方式,我们可以将方法提取到新文件中。
对于如下代码,如果想将onClick回调提取为后端方法:
import {callXXX} from 'xxx';
export function() {
return (
);
}
可以将其提取到新文件中:
// hash1.js
import {callXXX} from 'xxx';
export const id1 = () => callXXX();
原文件则编译为:
import {SERVER_PROXY} from 'xxx-framework';
export function() {
return (
);
}
这种方式比中粒度中提到的分离方式更灵活,因为:
当考虑前端状态时,可以将状态作为参数一并传给SERVER_PROXY。
比如对于上面提过的代码:
export function Button() {
const [id] = useStore();
return (
);
}
会编译为单独的文件:
// hash1.js
import {lazyLexicalScope} from 'xxx-framework';
export const id1 = () => {
const [id] = lazyLexicalScope();
const post = await db.posts.find(id);
// ...后续处理
};
与前端代码:
import {SERVER_PROXY} from 'xxx-framework';
export function Button() {
const [id] = useStore();
return (
);
}
其中前端传入的[id]参数在后端方法中可以通过lazyLexicalScope方法获取。
通过这种方式,可以做到状态级别的逻辑分离。
类似前端框架的更新粒度,全栈框架也存在不同粒度,这就是逻辑分离粒度。
按照逻辑分离到后端的粒度划分:
在粗粒度与中粒度之间,还存在一种方案 —— 将组件作为划分粒度的单元,这就是React的Server Component。
「划分粒度」的本质,也是性能的权衡 —— 如果将尽可能多的逻辑放到后端,那么前端页面需要加载的JS代码(逻辑对应的代码)就越少,那么前端花在加载JS资源上的时间就越少。
但是另一方面,如果划分的粒度太细(比如中或细粒度),可能意味着:
所以,具体什么粒度才是最合适的,还有待开发者与框架作者一起探索。
未来,这也会是全栈框架一个主意的竞争方向。
文章名称:未来全栈框架会卷的方向
本文地址:http://www.shufengxianlan.com/qtweb/news36/519736.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联