自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

?未來(lái)全??蚣軙?huì)卷的方向

開(kāi)發(fā) 前端
本文會(huì)從「實(shí)現(xiàn)原理」的角度聊聊業(yè)務(wù)邏輯的拆分粒度。

大家好,我卡頌。

從全球web發(fā)展角度看,框架競(jìng)爭(zhēng)已經(jīng)從第一階段的前端框架之爭(zhēng)(比如Vue、React、Angular等),過(guò)渡到第二階段的全??蚣苤疇?zhēng)(比如Next、Nuxt、Remix等)。

這里為什么說(shuō)全球,是因?yàn)閲?guó)內(nèi)web發(fā)展方向主要是更封閉的小程序生態(tài)

在第一階段的前端框架之爭(zhēng)中,不管爭(zhēng)論的主題是「性能」還是「使用體驗(yàn)」,最終都會(huì)落實(shí)到框架底層實(shí)現(xiàn)上。

不同框架底層實(shí)現(xiàn)的區(qū)別,可以概括為「更新粒度的區(qū)別」,比如:

  • Svelte更新粒度最細(xì),粒度對(duì)應(yīng)到每個(gè)狀態(tài)
  • Vue更新粒度中等,粒度對(duì)應(yīng)到每個(gè)組件
  • React更新粒度最粗,粒度對(duì)應(yīng)到整個(gè)應(yīng)用

那么,進(jìn)入第二階段的全??蚣苤疇?zhēng)后,最終會(huì)落實(shí)到什么的競(jìng)爭(zhēng)上呢?

我認(rèn)為,會(huì)落實(shí)到「業(yè)務(wù)邏輯的拆分粒度」上,這也是各大全??蚣芪磥?lái)會(huì)卷的方向。

本文會(huì)從「實(shí)現(xiàn)原理」的角度聊聊業(yè)務(wù)邏輯的拆分粒度。

邏輯拆分意味著什么

「性能」永遠(yuǎn)是最硬核的指標(biāo)。在前端框架時(shí)期,性能通常指「前端的運(yùn)行時(shí)性能」。

為了優(yōu)化性能,框架們都在優(yōu)化各自的運(yùn)行時(shí)流程,比如:

  • 更好的虛擬DOM算法。
  • 更優(yōu)秀的AOT編譯時(shí)技術(shù)。

在web中,最基礎(chǔ),也是最重要的性能指標(biāo)之一是FCP(First Contentful Paint 首次內(nèi)容繪制),他測(cè)量了頁(yè)面從開(kāi)始加載到頁(yè)面內(nèi)容的任何部分在屏幕上完成渲染的時(shí)間。

對(duì)于傳統(tǒng)前端框架,由于渲染頁(yè)面需要完成4個(gè)步驟:

  1. 加載HTML。
  2. 加載框架運(yùn)行時(shí)代碼。
  3. 加載業(yè)務(wù)代碼。
  4. 渲染頁(yè)面(此時(shí)統(tǒng)計(jì)FCP)。

框架能夠優(yōu)化的,只有步驟2、3,所以FCP指標(biāo)不會(huì)特別好。

SSR的出現(xiàn)改善了這一情況。對(duì)于傳統(tǒng)的SSR,需要完成:

  1. 加載帶內(nèi)容的HTML(此時(shí)統(tǒng)計(jì)FCP)。
  2. 加載框架運(yùn)行時(shí)代碼。
  3. 加載業(yè)務(wù)代碼。
  4. hydrate頁(yè)面。

在第一步就能統(tǒng)計(jì)FCP,所以FCP指標(biāo)優(yōu)化空間更大。

除此之外,SSR還有其他優(yōu)勢(shì)(比如更好的SEO支持),這就是近幾年全??蚣苁⑿械囊淮笤?。

既然大家都是全??蚣埽遣煌蚣茉撊绾瓮怀鲎约旱奶攸c(diǎn)呢?

我們會(huì)發(fā)現(xiàn),在SSR場(chǎng)景下,業(yè)務(wù)代碼既可以寫(xiě)在前端,也能寫(xiě)在后端。按照業(yè)務(wù)代碼在后端的比例從0~100%來(lái)看:

  • 0%邏輯在后端,對(duì)應(yīng)純前端框架渲染的應(yīng)用。
  • 100%邏輯在后端,對(duì)應(yīng)PHP時(shí)代純后端渲染的頁(yè)面。

合理調(diào)整框架的這個(gè)比例,就能做到差異化競(jìng)爭(zhēng)。

按照這個(gè)思路改進(jìn)框架,就需要回答一個(gè)問(wèn)題:一段業(yè)務(wù)邏輯,到底應(yīng)該放在前端還是后端呢?

這就是本文開(kāi)篇說(shuō)的「邏輯拆分」問(wèn)題。我們可以用「邏輯拆分的粒度」區(qū)分不同的全??蚣堋?/p>

下述內(nèi)容參考了文章wtf-is-code-extraction。

粗粒度

在Next.js中,文件路徑與后端路由一一對(duì)應(yīng),比如文件路徑pages/posts/hello.tsx就對(duì)應(yīng)了路由http(s)://域名/posts/hello。

開(kāi)發(fā)者可以在hello.tsx文件中同時(shí)書(shū)寫(xiě)前端、后端邏輯,比如如下代碼中:

  • Post組件對(duì)應(yīng)代碼會(huì)在前端執(zhí)行,用于渲染組件視圖。
  • getStaticProps方法會(huì)在代碼編譯時(shí)在后端執(zhí)行,執(zhí)行的結(jié)果會(huì)在Post組件渲染時(shí)作為props傳遞給它。
// hello.tsx

export async function getStaticProps() {
  const postData = await getPostData();
  return {
    props: {
      postData,
    },
  };
}

export default function Post({ postData }) {
  return (
    <Layout>
      {postData.title}
      <br />
      {postData.id}
      <br />
      {postData.date}
    </Layout>
  );
}

通過(guò)以上方式,在同一個(gè)文件中(hello.tsx),就能拆分出前端邏輯(Post組件邏輯)與后端邏輯(getStaticProps方法)。

雖然以上方式可以分離前端/后端邏輯,但一個(gè)組件文件只能定義一個(gè)getStaticProps方法。

如果我們還想定義一個(gè)執(zhí)行時(shí)機(jī)類似getStaticProps的getXXXData方法,就不行了。

所以,通過(guò)這種方式拆分前/后端邏輯,屬于比較粗的粒度。

中粒度

我們可以在此基礎(chǔ)上修改,改變拆分的粒度。

首先,我們需要改變之前約定的「前/后端代碼拆分方式」,不再通過(guò)具體的方法名(比如getStaticProps)顯式拆分,而是按需拆分方法。

修改后的調(diào)用方式如下:

// 修改后的 hello.tsx
export async function getStaticProps() {
  const postData = await getPostData();
  return {
    props: {
      postData,
    },
  };
}

export default function Post() {
  const postData = getStaticProps();
  return (
    <Layout>
      {postData.title}
      <br />
      {postData.id}
      <br />
      {postData.date}
    </Layout>
  );
}

現(xiàn)在,我們可以增加多個(gè)后端方法了,比如下面的getXXXData:

export async function getXXXData() {
  // ...省略
}

export default function Post() {
  const postData = getStaticProps();
  const xxxData = getXXXData();
  
  // ...省略
}

但是,Post組件是在前端執(zhí)行,getStaticProps、getXXXData是后端方法,如果不做任何處理,這兩個(gè)方法會(huì)隨著Post組件代碼一起打包到前端bundle文件中,如何將他們分離開(kāi)呢?

這時(shí)候,我們需要借助編譯技術(shù),上述代碼經(jīng)編譯后會(huì)變?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();

  // ...省略
}

讓我們來(lái)解釋下其中的細(xì)節(jié)。

首先,這段編譯后代碼可以直接在后端執(zhí)行,執(zhí)行時(shí)會(huì)通過(guò)框架提供的SERVER_REGISTER方法注冊(cè)后端方法(比如ID為ID_1的getStaticProps)。

由于SERVER_REGISTER方法前加了/*#__PURE__*/標(biāo)記,這個(gè)文件在打包客戶端bundle時(shí),SERVER_REGISTER會(huì)被tree-shaking掉。

也就是說(shuō),打包后的客戶端代碼類似如下:

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í)行時(shí),在前端,SERVER_PROXY方法會(huì)根據(jù)id請(qǐng)求對(duì)應(yīng)的后端邏輯,比如:

  • 發(fā)起id為ID_1的請(qǐng)求,后端會(huì)執(zhí)行g(shù)etStaticProps并返回結(jié)果。
  • 發(fā)起id為ID_2的請(qǐng)求,后端會(huì)執(zhí)行g(shù)etXXXData并返回結(jié)果。

實(shí)際上,通過(guò)這種方式,可以將任何函數(shù)作用域內(nèi)的邏輯從前端移到后端。

比如在下面的代碼中,我們?cè)诎粹o的點(diǎn)擊回調(diào)中訪問(wèn)了數(shù)據(jù)庫(kù)并做后續(xù)處理:

export function Button() {
  return (
    <button onClick={async () => {
      // 訪問(wèn)數(shù)據(jù)庫(kù)
      const post = await db.posts.find('xxx');
      // ...后續(xù)處理
    }}>
     請(qǐng)求數(shù)據(jù)
   </button>
  );
}

這個(gè)「按鈕點(diǎn)擊邏輯」顯然無(wú)法在前端執(zhí)行(前端不能直接訪問(wèn)數(shù)據(jù)庫(kù))。但我們可以通過(guò)上述方式將代碼編譯為下面的形式:

import {SERVER_REGISTER, SERVER_PROXY} from 'xxx-framework';

/*#__PURE__*/ SERVER_REGISTER('ID_123', () => {
  // 訪問(wèn)數(shù)據(jù)庫(kù)
  const post = await db.posts.find('xxx');
  // ...后續(xù)處理
});

export function Button() {
  return (
    <button onClick={async () => {
      await SERVER_PROXY('ID_123');
    })}>
     請(qǐng)求數(shù)據(jù)
   </button>
  );
}

編譯后的代碼可以在后端直接執(zhí)行(并訪問(wèn)數(shù)據(jù)庫(kù))。對(duì)于前端,我們?cè)俅虬粋€(gè)bundletree-shaking掉后端代碼),類似下面這樣:

import {SERVER_PROXY} from 'xxx-framework';

export function Button() {
  return (
    <button onClick={async () => {
      await SERVER_PROXY('ID_123');
    })}>
     請(qǐng)求數(shù)據(jù)
   </button>
  );
}

相比于粗粒度的邏輯分離方式(文件級(jí)別粒度),這種方式的粒度更細(xì)(函數(shù)級(jí)別粒度)。

細(xì)粒度

中粒度的方式有個(gè)缺點(diǎn) —— 分離的方法中不能存在客戶端狀態(tài)。比如下面的例子,點(diǎn)擊回調(diào)依賴了id狀態(tài):

export function Button() {
  const [id] = useStore();
  return (
    <button onClick={async () => {
      const post = await db.posts.find(id);
      // ...后續(xù)處理
    }}>
     click
   </button>
  );
}

如果遵循之前的分離方式,后端取不到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 (
    <button onClick={async () => {
      await SERVER_PROXY('ID_123');
    })}>
     請(qǐng)求數(shù)據(jù)
   </button>
  );
}

為了解決這個(gè)問(wèn)題,我們需要進(jìn)一步降低邏輯分離的粒度,使粒度達(dá)到狀態(tài)級(jí)。

首先,相比于中粒度中將內(nèi)聯(lián)方法提取到模塊頂層(并標(biāo)記/*#__PURE__*/)的方式,我們可以將方法提取到新文件中。

對(duì)于如下代碼,如果想將onClick回調(diào)提取為后端方法:

import {callXXX} from 'xxx';

export function() {
  return (
    <button onClick={() => callXXX()}>
     click
   </button>
  );
}

可以將其提取到新文件中:

// hash1.js
import {callXXX} from 'xxx';
export const id1 = () => callXXX();

原文件則編譯為:

import {SERVER_PROXY} from 'xxx-framework';

export function() {
  return (
    <button onClick={async () => SERVER_PROXY('./hash1.js', 'id1')}>
     click
   </button>
  );
}

這種方式比中粒度中提到的分離方式更靈活,因?yàn)椋?/p>

  • 省去了標(biāo)記/*#__PURE__*/。
  • 省去了先在后端注冊(cè)方法(SERVER_REGISTER)。

當(dāng)考慮前端狀態(tài)時(shí),可以將狀態(tài)作為參數(shù)一并傳給SERVER_PROXY。

比如對(duì)于上面提過(guò)的代碼:

export function Button() {
  const [id] = useStore();
  return (
    <button onClick={async () => {
      const post = await db.posts.find(id);
      // ...后續(xù)處理
    }}>
     click
   </button>
  );
}

會(huì)編譯為單獨(dú)的文件:

// 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 (
    <button onClick={async () => SERVER_PROXY('./hash1.js', 'id1', [id])}>
     click
   </button>
  );
}

其中前端傳入的[id]參數(shù)在后端方法中可以通過(guò)lazyLexicalScope方法獲取。

通過(guò)這種方式,可以做到狀態(tài)級(jí)別的邏輯分離。

總結(jié)

類似前端框架的更新粒度,全??蚣芤泊嬖诓煌6龋@就是邏輯分離粒度。

按照邏輯分離到后端的粒度劃分:

  • 粗粒度:以文件作為前/后端邏輯分離的粒度,比如Next.js。
  • 中粒度:以方法作為前/后端邏輯分離的粒度。
  • 細(xì)粒度:以狀態(tài)作為前/后端邏輯分離的粒度,比如Qwik。

在粗粒度與中粒度之間,還存在一種方案 —— 將組件作為劃分粒度的單元,這就是React的Server Component。

「劃分粒度」的本質(zhì),也是性能的權(quán)衡 —— 如果將盡可能多的邏輯放到后端,那么前端頁(yè)面需要加載的JS代碼(邏輯對(duì)應(yīng)的代碼)就越少,那么前端花在加載JS資源上的時(shí)間就越少。

但是另一方面,如果劃分的粒度太細(xì)(比如中或細(xì)粒度),可能意味著:

  • 更大的后端運(yùn)行時(shí)壓力(畢竟很多原本前端執(zhí)行的邏輯放到了后端)。
  • 降低部分前端交互的響應(yīng)速度(有些前端交互還得先去后端請(qǐng)求回交互對(duì)應(yīng)代碼再執(zhí)行)。

所以,具體什么粒度才是最合適的,還有待開(kāi)發(fā)者與框架作者一起探索。

未來(lái),這也會(huì)是全棧框架一個(gè)主意的競(jìng)爭(zhēng)方向。

責(zé)任編輯:姜華 來(lái)源: 魔術(shù)師卡頌
相關(guān)推薦

2022-07-06 11:21:11

JHipsterJavaJavaScript

2022-04-13 08:00:00

Hilla開(kāi)發(fā)Java

2016-04-08 14:32:32

全棧工程師世界

2013-01-19 09:45:31

App移動(dòng)開(kāi)發(fā)趨勢(shì)

2025-03-03 07:40:00

2023-08-21 09:51:57

全棧軟件開(kāi)發(fā)

2023-12-05 13:10:00

ReflexPython

2022-08-22 08:05:17

Fresh框架Remix

2017-03-08 11:10:30

存儲(chǔ)網(wǎng)絡(luò)閃存

2022-09-27 08:19:20

前端React

2018-10-25 16:22:53

華為

2023-09-17 12:21:21

RemixNext.js

2024-10-11 14:33:15

ReactRemix用戶

2013-12-09 09:42:50

JavaScript全棧式

2011-03-17 17:06:38

數(shù)據(jù)庫(kù)發(fā)展方向

2019-10-14 15:14:17

存儲(chǔ)云存儲(chǔ)人工智能

2022-05-06 11:56:27

元宇宙大會(huì)

2014-06-09 11:33:05

硅光子光通信技術(shù)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)