新聞中心
大家好,我卡頌。

創(chuàng)新互聯(lián)公司2013年成立,先為策勒等服務(wù)建站,策勒等地企業(yè),進行企業(yè)商務(wù)咨詢服務(wù)。為策勒企業(yè)網(wǎng)站制作PC+手機+微官網(wǎng)三網(wǎng)同步一站式服務(wù)解決您的所有建站問題。
從全球web發(fā)展角度看,框架競爭已經(jīng)從第一階段的前端框架之爭(比如Vue、React、Angular等),過渡到第二階段的全??蚣苤疇帲ū热鏝ext、Nuxt、Remix等)。
這里為什么說全球,是因為國內(nèi)web發(fā)展方向主要是更封閉的小程序生態(tài)
在第一階段的前端框架之爭中,不管爭論的主題是「性能」還是「使用體驗」,最終都會落實到框架底層實現(xiàn)上。
不同框架底層實現(xiàn)的區(qū)別,可以概括為「更新粒度的區(qū)別」,比如:
- Svelte更新粒度最細,粒度對應(yīng)到每個狀態(tài)
- Vue更新粒度中等,粒度對應(yīng)到每個組件
- React更新粒度最粗,粒度對應(yīng)到整個應(yīng)用
那么,進入第二階段的全??蚣苤疇幒?,最終會落實到什么的競爭上呢?
我認為,會落實到「業(yè)務(wù)邏輯的拆分粒度」上,這也是各大全??蚣芪磥頃淼姆较颉?/p>
本文會從「實現(xiàn)原理」的角度聊聊業(yè)務(wù)邏輯的拆分粒度。
邏輯拆分意味著什么
「性能」永遠是最硬核的指標。在前端框架時期,性能通常指「前端的運行時性能」。
為了優(yōu)化性能,框架們都在優(yōu)化各自的運行時流程,比如:
- 更好的虛擬DOM算法。
- 更優(yōu)秀的AOT編譯時技術(shù)。
在web中,最基礎(chǔ),也是最重要的性能指標之一是FCP(First Contentful Paint 首次內(nèi)容繪制),他測量了頁面從開始加載到頁面內(nèi)容的任何部分在屏幕上完成渲染的時間。
對于傳統(tǒng)前端框架,由于渲染頁面需要完成4個步驟:
- 加載HTML。
- 加載框架運行時代碼。
- 加載業(yè)務(wù)代碼。
- 渲染頁面(此時統(tǒng)計FCP)。
框架能夠優(yōu)化的,只有步驟2、3,所以FCP指標不會特別好。
SSR的出現(xiàn)改善了這一情況。對于傳統(tǒng)的SSR,需要完成:
- 加載帶內(nèi)容的HTML(此時統(tǒng)計FCP)。
- 加載框架運行時代碼。
- 加載業(yè)務(wù)代碼。
- hydrate頁面。
在第一步就能統(tǒng)計FCP,所以FCP指標優(yōu)化空間更大。
除此之外,SSR還有其他優(yōu)勢(比如更好的SEO支持),這就是近幾年全??蚣苁⑿械囊淮笤颉?/p>
既然大家都是全??蚣埽遣煌蚣茉撊绾瓮怀鲎约旱奶攸c呢?
我們會發(fā)現(xiàn),在SSR場景下,業(yè)務(wù)代碼既可以寫在前端,也能寫在后端。按照業(yè)務(wù)代碼在后端的比例從0~100%來看:
- 0%邏輯在后端,對應(yīng)純前端框架渲染的應(yīng)用。
- 100%邏輯在后端,對應(yīng)PHP時代純后端渲染的頁面。
合理調(diào)整框架的這個比例,就能做到差異化競爭。
按照這個思路改進框架,就需要回答一個問題:一段業(yè)務(wù)邏輯,到底應(yīng)該放在前端還是后端呢?
這就是本文開篇說的「邏輯拆分」問題。我們可以用「邏輯拆分的粒度」區(qū)分不同的全??蚣?。
下述內(nèi)容參考了文章wtf-is-code-extraction。
粗粒度
在Next.js中,文件路徑與后端路由一一對應(yīng),比如文件路徑pages/posts/hello.tsx就對應(yīng)了路由http(s)://域名/posts/hello。
開發(fā)者可以在hello.tsx文件中同時書寫前端、后端邏輯,比如如下代碼中:
- Post組件對應(yīng)代碼會在前端執(zhí)行,用于渲染組件視圖。
- getStaticProps方法會在代碼編譯時在后端執(zhí)行,執(zhí)行的結(jié)果會在Post組件渲染時作為props傳遞給它。
// 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方法。
如果我們還想定義一個執(zhí)行時機類似getStaticProps的getXXXData方法,就不行了。
所以,通過這種方式拆分前/后端邏輯,屬于比較粗的粒度。
中粒度
我們可以在此基礎(chǔ)上修改,改變拆分的粒度。
首先,我們需要改變之前約定的「前/后端代碼拆分方式」,不再通過具體的方法名(比如getStaticProps)顯式拆分,而是按需拆分方法。
修改后的調(diào)用方式如下:
// 修改后的 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}
);
}現(xiàn)在,我們可以增加多個后端方法了,比如下面的getXXXData:
export async function getXXXData() {
// ...省略
}
export default function Post() {
const postData = getStaticProps();
const xxxData = getXXXData();
// ...省略
}但是,Post組件是在前端執(zhí)行,getStaticProps、getXXXData是后端方法,如果不做任何處理,這兩個方法會隨著Post組件代碼一起打包到前端bundle文件中,如何將他們分離開呢?
這時候,我們需要借助編譯技術(shù),上述代碼經(jīng)編譯后會變?yōu)轭愃葡旅娴拇a:
// 編譯后代碼
/*#__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();
// ...省略
}讓我們來解釋下其中的細節(jié)。
首先,這段編譯后代碼可以直接在后端執(zhí)行,執(zhí)行時會通過框架提供的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();
// ...省略
}當(dāng)以上客戶端代碼執(zhí)行時,在前端,SERVER_PROXY方法會根據(jù)id請求對應(yīng)的后端邏輯,比如:
- 發(fā)起id為ID_1的請求,后端會執(zhí)行g(shù)etStaticProps并返回結(jié)果。
- 發(fā)起id為ID_2的請求,后端會執(zhí)行g(shù)etXXXData并返回結(jié)果。
實際上,通過這種方式,可以將任何函數(shù)作用域內(nèi)的邏輯從前端移到后端。
比如在下面的代碼中,我們在按鈕的點擊回調(diào)中訪問了數(shù)據(jù)庫并做后續(xù)處理:
export function Button() {
return (
);
}這個「按鈕點擊邏輯」顯然無法在前端執(zhí)行(前端不能直接訪問數(shù)據(jù)庫)。但我們可以通過上述方式將代碼編譯為下面的形式:
import {SERVER_REGISTER, SERVER_PROXY} from 'xxx-framework';
/*#__PURE__*/ SERVER_REGISTER('ID_123', () => {
// 訪問數(shù)據(jù)庫
const post = await db.posts.find('xxx');
// ...后續(xù)處理
});
export function Button() {
return (
);
}編譯后的代碼可以在后端直接執(zhí)行(并訪問數(shù)據(jù)庫)。對于前端,我們再打包一個bundle(tree-shaking掉后端代碼),類似下面這樣:
import {SERVER_PROXY} from 'xxx-framework';
export function Button() {
return (
);
}相比于粗粒度的邏輯分離方式(文件級別粒度),這種方式的粒度更細(函數(shù)級別粒度)。
細粒度
中粒度的方式有個缺點 —— 分離的方法中不能存在客戶端狀態(tài)。比如下面的例子,點擊回調(diào)依賴了id狀態(tài):
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);
// ...后續(xù)處理
});
export function Button() {
const [id] = useStore();
return (
);
}為了解決這個問題,我們需要進一步降低邏輯分離的粒度,使粒度達到狀態(tài)級。
首先,相比于中粒度中將內(nèi)聯(lián)方法提取到模塊頂層(并標記/*#__PURE__*/)的方式,我們可以將方法提取到新文件中。
對于如下代碼,如果想將onClick回調(diào)提取為后端方法:
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 (
);
}這種方式比中粒度中提到的分離方式更靈活,因為:
- 省去了標記/*#__PURE__*/。
- 省去了先在后端注冊方法(SERVER_REGISTER)。
當(dāng)考慮前端狀態(tài)時,可以將狀態(tài)作為參數(shù)一并傳給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);
// ...后續(xù)處理
};與前端代碼:
import {SERVER_PROXY} from 'xxx-framework';
export function Button() {
const [id] = useStore();
return (
);
}其中前端傳入的[id]參數(shù)在后端方法中可以通過lazyLexicalScope方法獲取。
通過這種方式,可以做到狀態(tài)級別的邏輯分離。
總結(jié)
類似前端框架的更新粒度,全??蚣芤泊嬖诓煌6?,這就是邏輯分離粒度。
按照邏輯分離到后端的粒度劃分:
- 粗粒度:以文件作為前/后端邏輯分離的粒度,比如Next.js。
- 中粒度:以方法作為前/后端邏輯分離的粒度。
- 細粒度:以狀態(tài)作為前/后端邏輯分離的粒度,比如Qwik。
在粗粒度與中粒度之間,還存在一種方案 —— 將組件作為劃分粒度的單元,這就是React的Server Component。
「劃分粒度」的本質(zhì),也是性能的權(quán)衡 —— 如果將盡可能多的邏輯放到后端,那么前端頁面需要加載的JS代碼(邏輯對應(yīng)的代碼)就越少,那么前端花在加載JS資源上的時間就越少。
但是另一方面,如果劃分的粒度太細(比如中或細粒度),可能意味著:
- 更大的后端運行時壓力(畢竟很多原本前端執(zhí)行的邏輯放到了后端)。
- 降低部分前端交互的響應(yīng)速度(有些前端交互還得先去后端請求回交互對應(yīng)代碼再執(zhí)行)。
所以,具體什么粒度才是最合適的,還有待開發(fā)者與框架作者一起探索。
未來,這也會是全??蚣芤粋€主意的競爭方向。
網(wǎng)站題目:?未來全??蚣軙淼姆较?
路徑分享:http://fisionsoft.com.cn/article/ccisoho.html


咨詢
建站咨詢
