SSR 和前端編譯,在這點上是一樣的
現在我們都是通過組件的方式來開發(fā)前端頁面,在瀏覽器里面,組件渲染時會通過 dom api 對 dom 做增刪改來顯示相應的內容。但在服務端并沒有 dom api,我們可以把組件渲染成 html 字符串,然后下發(fā)到瀏覽器渲染,因為已經有了 html 了,就可以直接渲染成 dom,不再需要執(zhí)行 JS,所以很快。
第一種瀏覽器渲染的方式叫做 CSR (client side render),第二種服務端渲染的方式叫做 SSR(server side render)。
很明顯,SSR 渲染出畫面的速度會很快,因為不需要執(zhí)行 JS ,而是直接解析 html。因此,app 里嵌的頁面基本都用 SSR,這樣體驗會更好。而且低端機執(zhí)行 JS 是可能很慢的,要是 CSR,那頁面可能會有很長一段白屏時間。
此外,SSR 是直接返回了 html,這樣搜索引擎的爬蟲就能從中抓取到具體的內容,就會給更高的搜索權重,也就是更有利于 SEO (search engine optimize)。
在 app 里嵌的頁面、搜索引擎排名優(yōu)化這兩種場景下,我們都要做 SSR。
知道了 SSR 是什么和為什么要做 SSR,那如何實現 SSR 呢?
SSR 實現原理
我們知道 vue 是通過 template 描述頁面結構,而 react 是通過 jsx,但不管是 template 還是 jsx,編譯后都會產生 render function,然后執(zhí)行產生 vdom。
vdom 在瀏覽器里會通過 dom api 增刪改 dom 來完成 CSR,在服務端會通過拼接字符串來完成 SSR。
vdom 是一個樹形結構,那么 SSR 就是遍歷這棵樹,拼接字符串的過程。
看到這張圖,不知你有沒有想起編譯的 generate 階段也是這樣的拼接字符串的過程:
沒錯,SSR 中 vdom 打印成字符串,和編譯中 AST 打印成字符串的邏輯確實是一樣的。
口說無憑,我們來看下兩者的源碼再下結論。
Vue SSR 的渲染流程
vue 提供了 vue-server-renderer 這個包用于 SSR,它的作用就是把 Vue 組件渲染成字符串。
它提供了 createBundleRenderer 的 api:
const bundle = fs.readFileSync(resolve('./dist/server-bundle.js'), 'utf-8');
createBundleRenderer(bundle)
這個 bundle 就是 webpack 編譯產生的目標代碼:
可能你會問,為啥要等 webpack 把代碼編譯成 bundle 才去渲染啊?
因為像 esm 的模塊語法、像 ts、sass 等語法都不是 node 支持的呀,要先把代碼編譯打包成 bundle,這樣才能在 node 里面跑。
這也是為啥提供的 api 叫做 createBundleRenderer。
創(chuàng)建好 renderer 之后,調用 renderToStream 方法,就開始執(zhí)行渲染了。
當然,也可以調用 renderToString,這倆 api 的區(qū)別是一個是邊渲染邊返回內容,一個是完全渲染完再返回內容。
渲染第一步,自然是要把傳入的 bundle 給執(zhí)行了:
這里 runInVm 就是執(zhí)行 bundle 那段字符串的代碼,這是基于 node 提供的 vm 包的 api 實現的:
通過 vm.runInContext 可以在某個上下文中執(zhí)行一段代碼。
執(zhí)行之后,返回的就是 Vue 的實例:
注意,這里是在 node 環(huán)境里創(chuàng)建的 Vue 實例,所以沒有 dom api,不能操作 dom,但可以打印成字符串:
這里 render 的實現就是拼接字符串:
這樣遍歷完一遍 vdom,就拼接好了最終的 html:
把這段 html 返回給瀏覽器即可。這樣我們就實現了 Vue 的 SSR!
小結一下 Vue SSR 的流程:
vue-server-renderer 包提供了 createBundleRenderer 的 api,可以傳入編譯打包后的 bundle 代碼來創(chuàng)建一個 renderer。renderer 有 renderToString 和 renderToStream 的 api。內部會通過 vm.runInContext 來執(zhí)行 bundle 的代碼,產生 Vue 實例,之后把 Vue 實例的 vdom 渲染成 html 字符串。返回這個 html 字符串就實現了 SSR。
當然,實際做 SSR 的時候,我們不會直接用 vue-server-renderer,而是會用封裝了一層的 nuxt.js,因為它對路由等做了處理,并且對工具鏈的封裝也很好,開箱即用。
到了這里,我們可以說 SSR 就是遍歷 vdom 拼接字符串的過程了。
接下來再看下編譯中的 generate 階段:
編譯流程
前端領域的編譯基本都是源碼轉源碼,所以流程都差不多,都是 parse、transform、generate 這三步:
parse 階段把源碼轉為 AST(抽象語法樹),然后 transform 階段會對 AST 做各種增刪改,generate 階段會把修改后的 AST 遞歸打印成字符串。
這里的 generate 階段就像 SSR 的 render 一樣,也是個拼接字符串的過程:
比如 babel 的 generate 的實現是這樣的:
打印 while 節(jié)點:
打印 condition 節(jié)點:
遞歸遍歷 AST,打印每個節(jié)點,拼接字符串,就能產生目標代碼。
所以說,SSR 的 vdom render 和前端編譯的 AST generate 是一樣的邏輯,都是拼接字符串。
當然,也是有很多不同的地方的,比如 SSR 的 vdom 是動態(tài)執(zhí)行 render function 產生的,而編譯中的 AST 是從源碼中靜態(tài)編譯產生的。只是拼接字符串的邏輯一樣。
總結
SSR 渲染首屏畫面速度快,而且利于搜索引擎的抓取,所以在 app 里嵌的頁面、SEO 這兩種場景下,我們都會做 SSR。
SSR 的原理就是把 vdom 打印成 字符串,這和前端編譯中的 generate 階段很類似。
我們看了 Vue 的 vue-server-render 包的源碼,它提供了 createBundleRenderer 的 api,傳入編譯打包后的 bundle 代碼,通過 vm 執(zhí)行它,然后把產生的 Vue 實例的 vdom 打印成 html 字符串,就實現了 SSR。
我們也看了 babel generator 的源碼,它提供了每種節(jié)點的打印邏輯,遞歸遍歷 AST,拼接字符串,就能產生目標代碼。
雖然 SSR 和前端編譯在流程上和目的上都不同,但是在生成代碼這一點上是一樣的,都是把樹形結構打印成字符串。