摘要:往往純的單頁面應(yīng)用一般不會太復(fù)雜,所以這里不引入和等等,在后面復(fù)雜的跨平臺應(yīng)用中我會將那些技術(shù)一擁而上。構(gòu)建極度復(fù)雜,超大數(shù)據(jù)的應(yīng)用。
React為了大型應(yīng)用而生,Electron和React-native賦予了它構(gòu)建移動端跨平臺App和桌面應(yīng)用的能力,Taro則賦予了它一次編寫,生成多種平臺小程序和React-native應(yīng)用的能力,這里特意說下 Taro,它是國產(chǎn),文檔寫得比較不錯,而且它的升級速度比較快,有issue我看也會及時解決,他們的維護人員還是非常敬業(yè)的!
,
Tips:本文某些知識點如果介紹不對或者不全的地方歡迎指出,本文可能內(nèi)容比較多,閱讀時間花費比較長,但是希望你可以認真看下去,可以的話最好手把手去實現(xiàn)一些code,本文所有代碼均手寫。
本文會從原生瀏覽器環(huán)境,到跨平臺開發(fā)逐漸去深入介紹,先給一些資料手寫React優(yōu)化腳手架帶項目
react-ssr的源碼
手寫Node.js原生靜態(tài)資源服務(wù)器
跨平臺Electron的demo
原生瀏覽器環(huán)境:原生瀏覽器環(huán)境其實是最考驗前端工程師能力的編程環(huán)境,因為我們前端大部分一開始面向瀏覽器編程,現(xiàn)在很多很多工作5-10年的前端,性能面板API都不知道用,怎么看調(diào)用函數(shù)分析耗時都不知道,這也是最近面試的情況,覺得有人說35歲失業(yè)的情況,是普遍存在,但是很大部分是你在混啊兄弟。
純CSR渲染(客戶端渲染)
純SSR渲染(服務(wù)端渲染)
混合渲染(預(yù)渲染,webpack的插件預(yù)渲染,Next.js的約定式路由SSR,或者使用Node.js做中間件,做部分SSR,加快首屏渲染,或者指定路由SSR.)
客戶端請求RestFul接口,接口吐回靜態(tài)資源文件
Node.js實現(xiàn)代碼
const express = require("express") const app = express() app.use(express.static("pulic"))//這里的public就是靜態(tài)資源的文件夾,讓客戶端拉取的,這里的代碼是前端的代碼已經(jīng)構(gòu)建完畢的代碼 app.get("/",(req,res)=>{ //do something }) app.listen(3000,err=>{ if(!err)=>{ console.log("監(jiān)聽端口號3000成功") } })
客戶端收到一個HTML文件,和若干個CSS文件,以及多個javaScript文件
用戶輸入了url地址欄然后客戶端返回靜態(tài)文件,客戶端開始解析
客戶端解析文件,js代碼動態(tài)生成頁面。(這也是為什么說單頁面應(yīng)用的SEO不友好的原因,初始它只是一個空的div標簽的HTML文件)
判斷一個頁面是不是CSR,很大程度上可以根據(jù)右鍵點開查看頁面元素,如果只有一個空的div標簽,那么大概率可以說是單頁面,CSR,客戶端渲染的網(wǎng)頁。
單一數(shù)據(jù)來源決定組件是否刷新是精細化最重要的方向。
class app extends React.PureComponent{ /////// } export default connect( (({xx,xxx,xxxx,xxxxx})) //// )(app)
一旦業(yè)務(wù)邏輯非常復(fù)雜的情況下,假設(shè)我們使用的是dva集中狀態(tài)管理,同時連接這么多的狀態(tài)樹模塊,那么可能會造成狀態(tài)樹模塊中任意的數(shù)據(jù)刷新導致這個組件被刷新,但是其實這個組件此時是不需要刷新的。
這里可以將需要的狀態(tài)通過根組件用props傳入,精確刷新的來源,單一可變數(shù)據(jù)來源追溯性強,也更方便debug
單向數(shù)據(jù)流不可變數(shù)據(jù),通過immutable.js這個庫實現(xiàn)
import Immutable from require("immutable"); var map1: Immutable.Map; map1 = Immutable.Map({ a: 1, b: 2, c: 3 }); var map2 = map1.set("b", 50); map1.get("b"); // 2 map2.get("b"); // 50
不可變數(shù)據(jù),數(shù)據(jù)共享,持久化存儲,通過is比較,每次map生成的都是唯一的 ,它們比較的是codehash的值,性能比通過遞歸或者直接比較強很多。在PureComponent淺比較不好用的時候
一般的組件,使用PureComponent減少重復(fù)渲染即可
PureComponent,平時我們創(chuàng)建 React 組件一般是繼承于 Component,而 PureComponent 相當于是一個更純凈的 Component,對更新前后的數(shù)據(jù)進行了一次淺比較。只有在數(shù)據(jù)真正發(fā)生改變時,才會對組件重新進行 render。因此可以大大提高組件的性能。
PureComponent部分源碼,其實就是淺比較,只不過對一些特殊值進行了判斷:
function is(x: any, y: any) { return ( (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) ); }
新,會自上而下逐漸刷新整個子孫組件,這樣性能損耗重復(fù)渲染就會多出很多,所以我們不僅要單一數(shù)據(jù)來源控制組件刷新,偶爾還需要在shouldComponentUpdate中對比nextProps和this.props 以及this.state以及nextState.
路由懶加載+code-spliting,加快首屏渲染,也可以減輕服務(wù)器壓力,因為很多人可能訪問你的網(wǎng)頁并不會看某些路由的內(nèi)容
使用react-loadable,支持SSR,非常推薦,官方的lazy不支持SSR,這是一個遺憾,這里需要配合wepback4的optimization配置,進行代碼分割
Tips:這里需要下載支持動態(tài)import的babel預(yù)設(shè)包 @babel/plugin-syntax-dynamic-import ,它支持動態(tài)倒入組件
webpack配置: optimization: { runtimeChunk: true, splitChunks: { chunks: "all" } }
import React from "react" import Loading from "./loading-window"http://占位的那個組件,初始加載 import Loadable from "react-loadable" const LoadableComponent = Loadable({ loader: () => import("./sessionWindow"),//真正需要加載的組件 loading: Loading, }); export default LoadableComponent
好了,現(xiàn)在路由懶加載組件以及代碼分割已經(jīng)做好了,而且它支持SSR。非常棒
由于純CSR的網(wǎng)頁一般不是很復(fù)雜,這里再介紹一個方面,那就是,能不用redux,dva等集中狀態(tài)管理的狀態(tài)就不上狀態(tài)樹,實踐證明,頻繁更新狀態(tài)樹對用戶體驗來說是影響非常大的。這個異步的過程,更耗時。遠不如支持通過props等方式進行組件間通信,原則上除了很多組件共享的數(shù)據(jù)才上狀態(tài)樹,否則都采用其他方式進行通信。
SSR,服務(wù)端渲染:這里也使用Node.js+express框架
const express= require("express") const app =express() const jade = require("jade") const result = *** const url path = *** const html = jade.renderFile(url, { data: result, urlPath })//傳入數(shù)據(jù)給模板引擎 app.get("/",(req,res)=>{ res.send(html)//直接吐渲染好的`html`文件拼接成字符串返回給客戶端 }) //RestFul接口 app.listen(3000,err=>{ //do something })
const PrerenderSPAPlugin = require("prerender-spa-plugin") new PrerenderSPAPlugin({ routes: ["/","/home","/shop"], staticDir: resolve(__dirname, "../dist"), }),
我覺得掘金上的神三元那篇文章就寫得很好,后面我自己去逐步實現(xiàn)了一次,感覺對SSR對理解更為透徹,加上本來就每天在寫Node.js,還會一點Next,Nuxt,服務(wù)端渲染,覺得大同小異。
服務(wù)端渲染本質(zhì),在服務(wù)端把代碼運行一次,將數(shù)據(jù)提前請求回來,返回運行后的html文件,客戶端接到文件后,拉取js代碼,代碼注水,然后顯示,脫水,js接管頁面。
同構(gòu)直出代碼,可以大大降低首屏渲染時間,經(jīng)過實踐,根據(jù)不同的內(nèi)容和配置可以縮短40%-65%時間,但是服務(wù)端渲染會給服務(wù)器帶來壓力,所以折中根據(jù)情況使用。
以下是一個最簡單的服務(wù)端渲染,服務(wù)端直接吐拼接后的html結(jié)構(gòu)字符串:
var express = require("express") var app = express() app.get("/", (req, res) => { res.send( `hello hello world
` ) }) app.listen(3000, () => { if(!err)=>{ console.log("3000監(jiān)聽")? } })
只要客戶端訪問localhost:3000就可以拿到數(shù)據(jù)頁面訪問
//server.js // server/index.js import express from "express"; import { render } from "../utils"; import { serverStore } from "../containers/redux-file/store"; const app = express(); app.use(express.static("public")); app.get("*", function(req, res) { if (req.path === "/favicon.ico") { res.send(); return; } const store = serverStore(); res.send(render(req, store)); }); const server = app.listen(3000, () => { var host = server.address().address; var port = server.address().port; console.log(host, port); console.log("啟動連接了"); }); //render函數(shù) import Routes from "../Router"; import { renderToString } from "react-dom/server"; import { StaticRouter, Link, Route } from "react-router-dom"; import React from "react"; import { Provider } from "react-redux"; import { renderRoutes } from "react-router-config"; import routers from "../Router"; import { matchRoutes } from "react-router-config"; export const render = (req, store) => { const matchedRoutes = matchRoutes(routers, req.path); matchedRoutes.forEach(item => { //如果這個路由對應(yīng)的組件有l(wèi)oadData方法 if (item.route.loadData) { item.route.loadData(store); } }); console.log(store.getState(),Date.now()) const content = renderToString(); return ` {renderRoutes(routers)} ssr123 ${content}`; };
數(shù)據(jù)注水,脫水,保持客戶端和服務(wù)端store的一致性。
上面返回的script標簽,里面已經(jīng)注水,將在服務(wù)端獲取到的數(shù)據(jù)給到了全局window下的context屬性,在初始化客戶端store時候我們給它脫水。初始化渲染使用服務(wù)端獲取的數(shù)據(jù)~
import thunk from "redux-thunk"; import { createStore, applyMiddleware } from "redux"; import reducers from "./reducers"; export const getClientStore = () => { const defaultState = window.context ? window.context.state : {}; return createStore(reducers, defaultState, applyMiddleware(thunk)); }; export const serverStore = () => { return createStore(reducers, applyMiddleware(thunk)); };
這里注意,在組件的componentDidMount生命周期中發(fā)送ajax等獲取數(shù)據(jù)時候,先判斷下狀態(tài)樹中有沒有數(shù)據(jù),如果有數(shù)據(jù),那么就不要重復(fù)發(fā)送請求,導致資源浪費。
多層級路由SSR
//路由配置文件,改成這種方式 import Home from "./containers/Home"; import Login from "./containers/Login"; import App from "./containers/app"; export default [ { component: App, routes: [ { path: "/", component: Home, exact: true, loadData: Home.loadData }, { path: "/login", component: Login, exact: true } ] } ];
入口文件路由部分改成:
server.js const content = renderToString(); client.js {renderRoutes(routers)} {renderRoutes(routers)}
后續(xù)可能有利用loader進行CSS的服務(wù)端渲染以及helmet的動態(tài)meta, title標簽進行SEO優(yōu)化等,今天時間緊促,就不繼續(xù)寫SSR了。
構(gòu)建Electron極度復(fù)雜,超大數(shù)據(jù)的應(yīng)用。第一個提到的是sqlite,嵌入式關(guān)系型數(shù)據(jù)庫,輕量型無入侵性,標準的sql語句,這里不做過多介紹。
PWA,漸進性式web應(yīng)用,這里使用webpack4的插件,進行快速使用,對于一些數(shù)據(jù)內(nèi)容不需要存儲數(shù)據(jù)庫的,但是卻想要一次拉取,多次復(fù)用,那么可以使用這個配置
serverce work也有它的一套生命周期通常我們?nèi)绻褂?Service Worker 基本就是以下幾個步驟:
首先我們需要在頁面的 JavaScript 主線程中使用 serviceWorkerContainer.register() 來注冊 Service Worker ,在注冊的過程中,瀏覽器會在后臺啟動嘗試 Service Worker 的安裝步驟。
如果注冊成功,Service Worker 在 ServiceWorkerGlobalScope 環(huán)境中運行; 這是一個特殊的 worker context,與主腳本的運行線程相獨立,同時也沒有訪問 DOM 的能力。
后臺開始安裝步驟, 通常在安裝的過程中需要緩存一些靜態(tài)資源。如果所有的資源成功緩存則安裝成功,如果有任何靜態(tài)資源緩存失敗則安裝失敗,在這里失敗的不要緊,會自動繼續(xù)安裝直到安裝成功,如果安裝不成功無法進行下一步 — 激活 Service Worker。
開始激活 Service Worker,必須要在 Service Worker 安裝成功之后,才能開始激活步驟,當 Service Worker 安裝完成后,會接收到一個激活事件(activate event)。激活事件的處理函數(shù)中,主要操作是清理舊版本的 Service Worker 腳本中使用資源。
激活成功后 Service Worker 可以控制頁面了,但是只針對在成功注冊了 Service Worker 后打開的頁面。也就是說,頁面打開時有沒有 Service Worker,決定了接下來頁面的生命周期內(nèi)受不受 Service Worker 控制。所以,只有當頁面刷新后,之前不受 Service Worker 控制的頁面才有可能被控制起來。
直接上代碼,存儲所有js文件和圖片 //實際的存儲根據(jù)自身需要,并不是越多越好。
const WorkboxPlugin = require("workbox-webpack-plugin") new WorkboxPlugin.GenerateSW({ clientsClaim: true, skipWaiting: true, importWorkboxFrom: "local", include: [/.js$/, /.css$/, /.html$/, /.jpg/, /.jpeg/, /.svg/, /.webp/, /.png/], }),
PWA并不僅僅這些功能,它的功能非常強大,有興趣的可以去lavas看看,PWA技術(shù)對于經(jīng)常訪問的老客戶來說,首屏渲染提升非常大,特別在移動端,可以添加到桌面保存。666啊~,在pc端更多的是緩存處理文件~
使用react-lazyload,懶加載你的視窗初始看不見的組件或者圖片。
/開箱即用的懶加載圖片 import LazyLoad from "react-lazyload"//這里配置表示占位符的樣式~。 記得在移動端的滑動屏幕或者PC端的調(diào)用forceCheck,動態(tài)計算元素距離視窗的位置然后決定是否顯示真的圖片~ import { forceCheck } from "react-lazyload"; forceCheck()
懶加載組件
import { lazyload } from "react-lazyload"; //跟上面同理,不過是一個裝飾器,高階函數(shù)而已。一樣需要forcecheck() @lazyload({ height: 200, once: true, offset: 100 }) class MyComponent extends React.Component { render() { return大數(shù)據(jù)React渲染,擁有讓應(yīng)用擁有60FPS -非常核心的一點優(yōu)化this component is lazyloaded by default!; } }
List長列表
]
react-virtualized-auto-sizer和windowScroll配合一起使用,達到頁面復(fù)雜效果+大數(shù)據(jù)渲染保持60FPS。上面的官網(wǎng)里有介紹這些組件~
高計算量的工作交給web wrok線程var myWorker = new Worker("worker.js"); first.onchange = function() { myWorker.postMessage([first.value,second.value]); console.log("Message posted to worker"); } second.onchange = function() { myWorker.postMessage([first.value,second.value]); console.log("Message posted to worker"); }
這段代碼中變量first和second代表2個元素;它們當中任意一個的值發(fā)生改變時,myWorker.postMessage([first.value,second.value])會將這2個值組成數(shù)組發(fā)送給worker。你可以在消息中發(fā)送許多你想發(fā)送的東西。
在worker中接收到消息后,我們可以寫這樣一個事件處理函數(shù)代碼作為響應(yīng)(worker.js):
onmessage = function(e) { console.log("Message received from main script"); var workerResult = "Result: " + (e.data[0] * e.data[1]); console.log("Posting message back to main script"); postMessage(workerResult); }
onmessage處理函數(shù)允許我們在任何時刻,一旦接收到消息就可以執(zhí)行一些代碼,代碼中消息本身作為事件的data屬性進行使用。這里我們簡單的對這2個數(shù)字作乘法處理并再次使用postMessage()方法,將結(jié)果回傳給主線程。
回到主線程,我們再次使用onmessage以響應(yīng)worker回傳的消息:
myWorker.onmessage = function(e) { result.textContent = e.data; console.log("Message received from worker"); }
在這里我們獲取消息事件的data,并且將它設(shè)置為result的textContent,所以用戶可以直接看到運算的結(jié)果。
注意: 在主線程中使用時,onmessage和postMessage() 必須掛在worker對象上,而在worker中使用時不用這樣做。原因是,在worker內(nèi)部,worker是有效的全局作用域。
注意: 當一個消息在主線程和worker之間傳遞時,它被復(fù)制或者轉(zhuǎn)移了,而不是共享。
開啟web work線程,其實也會損耗一定的主線程的性能,但是大量計算的工作交給它也未嘗不可,其實Node.js和javaScript都不適合做大量計算工作,這點有目共睹,尤其是js引擎和GUI渲染線程互斥的情況存在。充分合理利用React的Feber架構(gòu)diff算法優(yōu)化項目
requestAnimationFrame調(diào)用高優(yōu)先級任務(wù),中斷調(diào)度階段的遍歷,由于React的新版本調(diào)度階段是擁有三根指針的可中斷的鏈表遍歷,所以這樣既不影響下面的遍歷,也不影響用戶交互等行為。
使用requestAnimationFrame,當頁面處于未激活的狀態(tài)下,該頁面的屏幕刷新任務(wù)會被系統(tǒng)暫停,由于requestAnimationFrame保持和屏幕刷新同步執(zhí)行,所以也會被暫停。當頁面被激活時,動畫從上次停留的地方繼續(xù)執(zhí)行,節(jié)約 CPU 開銷。
一個刷新間隔內(nèi)函數(shù)執(zhí)行多次時沒有意義的,因為顯示器每 16.7ms 刷新一次,多次繪制并不會在屏幕上體現(xiàn)出來
在高頻事件(resize,scroll等)中,使用requestAnimationFrame可以防止在一個刷新間隔內(nèi)發(fā)生多次函數(shù)執(zhí)行,這樣保證了流暢性,也節(jié)省了函數(shù)執(zhí)行的開銷
某些情況下可以直接使用requestAnimationFrame替代 Throttle 函數(shù),都是限制回調(diào)函數(shù)執(zhí)行的頻率
使用requestAnimationFrame也可以更好的讓瀏覽器保持60幀的動畫
requestIdleCallback,這個API目前兼容性不太好,但是在Electron開發(fā)中,可以使用,兩者還是有區(qū)別的,而且這兩個api用好了可以解決很多復(fù)雜情況下的問題~。當然你也可以用上面的api封裝這個api,也并不是很復(fù)雜。
當關(guān)注用戶體驗,不希望因為一些不重要的任務(wù)(如統(tǒng)計上報)導致用戶感覺到卡頓的話,就應(yīng)該考慮使用requestIdleCallback。因為requestIdleCallback回調(diào)的執(zhí)行的前提條件是當前瀏覽器處于空閑狀態(tài)。
圖中一幀包含了用戶的交互、js的執(zhí)行、以及requestAnimationFrame的調(diào)用,布局計算以及頁面的重繪等工作。
假如某一幀里面要執(zhí)行的任務(wù)不多,在不到16ms(1000/60)的時間內(nèi)就完成了上述任務(wù)的話,那么這一幀就會有一定的空閑時間,這段時間就恰好可以用來執(zhí)行requestIdleCallback的回調(diào),如下圖所示:
使用preload,prefetch,dns-prefetch等指定提前請求指定文件,或者根據(jù)情況,瀏覽器自行決定是否提前dns預(yù)解析或者按需請求某些資源。這里也可以webpack4插件實現(xiàn),目前京東在使用這個方案~
const PreloadWebpackPlugin = require("preload-webpack-plugin") new PreloadWebpackPlugin({ rel: "preload", as(entry) { if (/.css$/.test(entry)) return "style"; if (/.woff$/.test(entry)) return "font"; if (/.png$/.test(entry)) return "image"; return "script"; }, include:"allChunks" //include: ["app"] }),對指定js文件延遲加載~
普通的腳本
給script標簽,加上async標簽,遇到此標簽,先去請求,但是不阻塞解析html等文件~,請求回來就立馬加載
給script標簽,加上defer標簽,延遲加載,但是必須在所有腳本加載完畢后才會加載它,但是這個標簽有bug,不確定能否準時加載。一般只給一個
寫這篇時間太耗時間,而且論壇的在線編輯器到了內(nèi)容很多的時候,非常卡,React-native的以及一些細節(jié),后面再補充下面給出一些源碼和資料地址:
手寫React優(yōu)化腳手架帶項目
react-ssr的源碼
手寫Node.js原生靜態(tài)資源服務(wù)器
跨平臺Electron的demo
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/54242.html
摘要:往往純的單頁面應(yīng)用一般不會太復(fù)雜,所以這里不引入和等等,在后面復(fù)雜的跨平臺應(yīng)用中我會將那些技術(shù)一擁而上。構(gòu)建極度復(fù)雜,超大數(shù)據(jù)的應(yīng)用。 showImg(https://segmentfault.com/img/bVbvphv?w=1328&h=768); React為了大型應(yīng)用而生,Electron和React-native賦予了它構(gòu)建移動端跨平臺App和桌面應(yīng)用的能力,Taro則賦...
摘要:往往純的單頁面應(yīng)用一般不會太復(fù)雜,所以這里不引入和等等,在后面復(fù)雜的跨平臺應(yīng)用中我會將那些技術(shù)一擁而上。構(gòu)建極度復(fù)雜,超大數(shù)據(jù)的應(yīng)用。 showImg(https://segmentfault.com/img/bVbvphv?w=1328&h=768); React為了大型應(yīng)用而生,Electron和React-native賦予了它構(gòu)建移動端跨平臺App和桌面應(yīng)用的能力,Taro則賦...
showImg(https://segmentfault.com/img/bVbw3tK?w=1240&h=827); 前端工程師這個崗位,真的是反人性的 我們來思考一個問題: 一個6年左右經(jīng)驗的前端工程師: 前面兩年在用jQuery 期間一直在用React-native(一步一步踩坑過來的那種) 最近兩年還在寫微信小程序 下面一個2年經(jīng)驗的前端工程師: 并不會跨平臺技術(shù),他的兩年工作都是Reac...
摘要:目前我們的業(yè)務(wù)項目采用的來進行優(yōu)化和首屏性能提升。可變性需要讓開發(fā)人員降低開發(fā)時的基準線,來保證每一個用戶的體驗。對于路由的切分以及庫的引入來說,這一個原則至關(guān)重要??焖偕梢环菡军c的性能審查報告。 The Cost Of JavaScript 2018 關(guān)于原文 原文是在Medium上面看到的,Chrome工程師Addy Osmani發(fā)布的一篇文章,這位的Medium上面的自我介紹里...
閱讀 2028·2021-11-24 10:45
閱讀 1883·2021-10-09 09:43
閱讀 1330·2021-09-22 15:38
閱讀 1255·2021-08-18 10:19
閱讀 2861·2019-08-30 15:55
閱讀 3090·2019-08-30 12:45
閱讀 2993·2019-08-30 11:25
閱讀 391·2019-08-29 11:30