React
爲了大型應用而生,Electron
和React-native
賦予了它構建移動端跨平臺App
和桌面應用的能力,Taro
則賦予了它一次編寫,生成多種平臺小程序和React-native
應用的能力,這裏特地說下Taro
,它是國產,文檔寫得比較不錯,並且它的升級速度比較快,有issue
我看也會及時解決,他們的維護人員仍是很是敬業的!css
Tips
:本文某些知識點若是介紹不對或者不全的地方歡迎指出,本文可能內容比較多,閱讀時間花費比較長,可是但願你能夠認真看下去,能夠的話最好手把手去實現一些code
,本文全部代碼均手寫。React
框架,比較常見的是製做單頁面SPA
應用:SPA
應用,分如下幾種:純CSR
渲染(客戶端渲染)html
純SSR
渲染(服務端渲染)前端
混合渲染(預渲染,webpack
的插件預渲染,Next.js
的約定式路由SSR
,或者使用Node.js
作中間件,作部分SSR
,加快首屏渲染,或者指定路由SSR
.)java
CSR
渲染RestFul
接口,接口吐回靜態資源文件Node.js
實現代碼const express = require('express')
const app = express()
app.use(express.static('pulic'))//這裏的public就是靜態資源的文件夾,讓客戶端拉取的,這裏的代碼是前端的代碼已經構建完畢的代碼
app.get('/',(req,res)=>{
//do something
})
app.listen(3000,err=>{
if(!err)=>{
console.log('監聽端口號3000成功')
}
})
複製代碼
客戶端收到一個HTML
文件,和若干個CSS
文件,以及多個javaScript
文件react
用戶輸入了url
地址欄而後客戶端返回靜態文件,客戶端開始解析webpack
客戶端解析文件,js
代碼動態生成頁面。(這也是爲何說單頁面應用的SEO
不友好的緣由,初始它只是一個空的div
標籤的HTML
文件)git
判斷一個頁面是否是CSR
,很大程度上能夠根據右鍵點開查看頁面元素,若是隻有一個空的div
標籤,那麼大機率能夠說是單頁面,CSR
,客戶端渲染的網頁。github
#####純CSR
的應用,如何精細化渲染呢?web
CSR
形式,大都依賴框架,Vue
和React
之類。一旦使用這類型技術架構,狀態數據集中管理,單向數據流,不可變數據,路由懶加載,按需加載組件,適當的緩存機制(PWA
技術),細緻拆分組件,單一數據來源刷新組件,這些都是咱們能夠精細化的方向。每每純CSR
的單頁面應用通常不會太複雜,因此這裏不引入PWA
和web work
等等,在後面複雜的跨平臺應用中我會將那些技術蜂擁而上。class app extends React.PureComponent{
///////
}
export default connect(
(({xx,xxx,xxxx,xxxxx}))
////
)(app)
複製代碼
一旦業務邏輯很是複雜的狀況下,假設咱們使用的是
dva
集中狀態管理,同時鏈接這麼多的狀態樹模塊,那麼可能會形成狀態樹模塊中任意的數據刷新致使這個組件被刷新,可是其實這個組件此時是不須要刷新的。面試
這裏能夠將須要的狀態經過根組件用props
傳入,精確刷新的來源,單一可變數據來源追溯性強,也更方便debug
單向數據流不可變數據,經過immutable.js
這個庫實現
import Immutable from require('immutable');
var map1: Immutable.Map<string, number>;
map1 = Immutable.Map({ a: 1, b: 2, c: 3 });
var map2 = map1.set('b', 50);
map1.get('b'); // 2
map2.get('b'); // 50
複製代碼
不可變數據,數據共享,持久化存儲,經過
is
比較,每次map
生成的都是惟一的 ,它們比較的是codehash
的值,性能比經過遞歸或者直接比較強不少。在PureComponent
淺比較很差用的時候
通常的組件,使用PureComponent
減小重複渲染便可
PureComponent,平時咱們建立 React 組件通常是繼承於 Component,而 PureComponent 至關因而一個更純淨的 Component,對更新先後的數據進行了一次淺比較。只有在數據真正發生改變時,纔會對組件從新進行 render。所以能夠大大提升組件的性能。
PureComponent
部分源碼,其實就是淺比較,只不過對一些特殊值進行了判斷:
function is(x: any, y: any) {
return (
(x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y)
);
}
複製代碼
immutable.js和pureComponent
,由於React
一旦根組件被刷新,會自上而下逐漸刷新整個子孫組件,這樣性能損耗重複渲染就會多出不少,因此咱們不只要單一數據來源控制組件刷新,偶爾還須要在shouldComponentUpdate
中對比nextProps和this.props
以及this.state以及nextState
.
路由懶加載+code-spliting
,加快首屏渲染,也能夠減輕服務器壓力,由於不少人可能訪問你的網頁並不會看某些路由的內容
使用react-loadable
,支持SSR
,很是推薦,官方的lazy
不支持SSR
,這是一個遺憾,這裏須要配合wepback4
的optimization
配置,進行代碼分割
Tips:
這裏須要下載支持動態import
的babel預設包 @babel/plugin-syntax-dynamic-import
,它支持動態倒入組件
webpack配置:
optimization: {
runtimeChunk: true,
splitChunks: {
chunks: 'all'
}
}
複製代碼
import React from 'react'
import Loading from './loading-window'//佔位的那個組件,初始加載
import Loadable from 'react-loadable'
const LoadableComponent = Loadable({
loader: () => import('./sessionWindow'),//真正須要加載的組件
loading: Loading,
});
export default LoadableComponent
複製代碼
好了,如今路由懶加載組件以及代碼分割已經作好了,並且它支持SSR
。很是棒
因爲純CSR
的網頁通常不是很複雜,這裏再介紹一個方面,那就是,能不用redux,dva
等集中狀態管理的狀態就不上狀態樹,實踐證實,頻繁更新狀態樹對用戶體驗來講是影響很是大的。這個異步的過程,更耗時。遠不如支持經過props
等方式進行組件間通訊,原則上除了不少組件共享的數據才上狀態樹,不然都採用其餘方式進行通訊。
SSR
,服務端渲染:jade,tempalte,ejs
等模板引擎進行渲染,而後返回給前端對應的HTML
文件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 })//傳入數據給模板引擎
app.get('/',(req,res)=>{
res.send(html)//直接吐渲染好的`html`文件拼接成字符串返回給客戶端
}) //RestFul接口
app.listen(3000,err=>{
//do something
})
複製代碼
webpack4
插件,預渲染指定路由,被指定的路由爲SSR
渲染,後臺0代碼實現const PrerenderSPAPlugin = require('prerender-spa-plugin')
new PrerenderSPAPlugin({
routes: ['/','/home','/shop'],
staticDir: resolve(__dirname, '../dist'),
}),
複製代碼
Node.js
做爲中間件,SSR
指定的路由加快首屏渲染,固然CSS
也能夠服務端渲染,動態Title和meta標籤
,更好的SEO
優化,這裏Node.js
還能夠同時處理數據,減輕前端的計算負擔。我以爲掘金上的神三元那篇文章就寫得很好,後面我本身去逐步實現了一次,感受對SSR
對理解更爲透徹,加上原本就天天在寫Node.js
,還會一點Next,Nuxt
,服務端渲染,以爲大同小異。
服務端渲染本質,在服務端把代碼運行一次,將數據提早請求回來,返回運行後的html
文件,客戶端接到文件後,拉取js
代碼,代碼注水,而後顯示,脫水,js
接管頁面。
同構直出代碼,能夠大大下降首屏渲染時間,通過實踐,根據不一樣的內容和配置能夠縮短40%-65%時間,可是服務端渲染會給服務器帶來壓力,因此折中根據狀況使用。
如下是一個最簡單的服務端渲染,服務端直接吐拼接後的html
結構字符串:
var express = require('express')
var app = express()
app.get('/', (req, res) => {
res.send(
`
<html>
<head>
<title>hello</title>
</head>
<body>
<h1>hello world </h1>
</body>
</html>
`
)
})
app.listen(3000, () => {
if(!err)=>{
console.log('3000監聽')Ï
}
})
複製代碼
只要客戶端訪問
localhost:3000
就能夠拿到數據頁面訪問
redux
的store
狀態樹中的數據一塊兒返回給客戶端,客戶端脫水,渲染。 保證它們的狀態數據和路由一致,就能夠說是成功了。必需要客戶端和服務端代碼和數據一致性,不然SSR
就算失敗。//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函數
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 => {
//若是這個路由對應的組件有loadData方法
if (item.route.loadData) {
item.route.loadData(store);
}
});
console.log(store.getState(),Date.now())
const content = renderToString(
<Provider store={store}>
<StaticRouter location={req.path}>{renderRoutes(routers)}</StaticRouter>
</Provider>
);
return `
<html>
<head>
<title>ssr123</title>
</head>
<body>
<div id="root">${content}</div>
<script>window.context={state:${JSON.stringify(store.getState())}}</script>
<script src="/index.js"></script>
</body>
</html>
`;
};
複製代碼
store
的一致性。上面返回的
script
標籤,裏面已經注水,將在服務端獲取到的數據給到了全局window下的context屬性,在初始化客戶端store
時候咱們給它脫水。初始化渲染使用服務端獲取的數據~
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
生命週期中發送ajax
等獲取數據時候,先判斷下狀態樹中有沒有數據,若是有數據,那麼就不要重複發送請求,致使資源浪費。
多層級路由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(
<Provider store={store}>
<StaticRouter location={req.path}>{renderRoutes(routers)}</StaticRouter>
</Provider>
);
client.js
<Provider store={store}>
<BrowserRouter>{renderRoutes(routers)}</BrowserRouter>
</Provider>
複製代碼
loader
進行CSS
的服務端渲染以及helmet
的動態meta, title
標籤進行SEO
優化等,今天時間緊促,就不繼續寫SSR
了。Electron
極度複雜,超大數據的應用。sqlite,PWA,web work,原生Node.js,react-window,react-lazyload,C++插件等
第一個提到的是sqlite
,嵌入式關係型數據庫,輕量型無入侵性,標準的sql
語句,這裏不作過多介紹。
PWA
,漸進性式web應用,這裏使用webpack4
的插件,進行快速使用,對於一些數據內容不須要存儲數據庫的,可是卻想要一次拉取,屢次複用,那麼可使用這個配置
一般咱們若是要使用 Service Worker 基本就是如下幾個步驟:
首先咱們須要在頁面的 JavaScript 主線程中使用 serviceWorkerContainer.register() 來註冊 Service Worker ,在註冊的過程當中,瀏覽器會在後臺啓動嘗試 Service Worker 的安裝步驟。
若是註冊成功,Service Worker 在 ServiceWorkerGlobalScope 環境中運行; 這是一個特殊的 worker context,與主腳本的運行線程相獨立,同時也沒有訪問 DOM 的能力。
後臺開始安裝步驟, 一般在安裝的過程當中須要緩存一些靜態資源。若是全部的資源成功緩存則安裝成功,若是有任何靜態資源緩存失敗則安裝失敗,在這裏失敗的沒關係,會自動繼續安裝直到安裝成功,若是安裝不成功沒法進行下一步 — 激活 Service Worker。
開始激活 Service Worker,必需要在 Service Worker 安裝成功以後,才能開始激活步驟,當 Service Worker 安裝完成後,會接收到一個激活事件(activate event)。激活事件的處理函數中,主要操做是清理舊版本的 Service Worker 腳本中使用資源。
激活成功後 Service Worker 能夠控制頁面了,可是隻針對在成功註冊了 Service Worker 後打開的頁面。也就是說,頁面打開時有沒有 Service Worker,決定了接下來頁面的生命週期內受不受 Service Worker 控制。因此,只有當頁面刷新後,以前不受 Service Worker 控制的頁面纔有可能被控制起來。
直接上代碼,存儲全部
js文件和圖片
//實際的存儲根據自身須要,並非越多越好。
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
技術對於常常訪問的老客戶來講,首屏渲染提高很是大,特別在移動端,能夠添加到桌面保存。666啊~,在pc
端更多的是緩存處理文件~
使用react-lazyload
,懶加載你的視窗初始看不見的組件或者圖片。
/開箱即用的懶加載圖片
import LazyLoad from 'react-lazyload'
<LazyLoad height={42} offset={100} once> //這裏配置表示佔位符的樣式~。
<img
src={this.state.src}
onError={this.handleError.bind(this)}
className={className || 'avatar'}
/>
</LazyLoad>
記得在移動端的滑動屏幕或者PC端的調用forceCheck,動態計算元素距離視窗的位置而後決定是否顯示真的圖片~
import { forceCheck } from 'react-lazyload';
forceCheck()
複製代碼
import { lazyload } from 'react-lazyload';
//跟上面同理,不過是一個裝飾器,高階函數而已。同樣須要forcecheck()
@lazyload({
height: 200,
once: true,
offset: 100
})
class MyComponent extends React.Component {
render() {
return <div>this component is lazyloaded by default!</div>;
}
}
複製代碼
React
渲染,擁有讓應用擁有60FPS
-很是核心的一點優化List
長列表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個input元素;它們當中任意一個的值發生改變時,myWorker.postMessage([first.value,second.value])會將這2個值組成數組發送給worker。你能夠在消息中發送許多你想發送的東西。
在worker中接收到消息後,咱們能夠寫這樣一個事件處理函數代碼做爲響應(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處理函數容許咱們在任什麼時候刻,一旦接收到消息就能夠執行一些代碼,代碼中消息自己做爲事件的data屬性進行使用。這裏咱們簡單的對這2個數字做乘法處理並再次使用postMessage()方法,將結果回傳給主線程。
回到主線程,咱們再次使用onmessage以響應worker回傳的消息:
myWorker.onmessage = function(e) {
result.textContent = e.data;
console.log('Message received from worker');
}
複製代碼
在這裏咱們獲取消息事件的data,而且將它設置爲result的textContent,因此用戶能夠直接看到運算的結果。
注意: 在主線程中使用時,onmessage和postMessage() 必須掛在worker對象上,而在worker中使用時不用這樣作。緣由是,在worker內部,worker是有效的全局做用域。
注意: 當一個消息在主線程和worker之間傳遞時,它被複制或者轉移了,而不是共享。
開啓
web work
線程,其實也會損耗必定的主線程的性能,可是大量計算的工做交給它也何嘗不可,其實Node.js
和javaScript
都不適合作大量計算工做,這點有目共睹,尤爲是js
引擎和GUI
渲染線程互斥的狀況存在。
React
的Feber
架構diff
算法優化項目requestAnimationFrame
調用高優先級任務,中斷調度階段的遍歷,因爲React
的新版本調度階段是擁有三根指針的可中斷的鏈表遍歷,因此這樣既不影響下面的遍歷,也不影響用戶交互等行爲。使用
requestAnimationFrame
也能夠更好的讓瀏覽器保持60幀的動畫
使用requestAnimationFrame,當頁面處於未激活的狀態下,該頁面的屏幕刷新任務會被系統暫停,因爲requestAnimationFrame保持和屏幕刷新同步執行,因此也會被暫停。當頁面被激活時,動畫從上次停留的地方繼續執行,節約 CPU 開銷。
一個刷新間隔內函數執行屢次時沒有意義的,由於顯示器每 16.7ms 刷新一次,屢次繪製並不會在屏幕上體現出來
在高頻事件(resize,scroll等)中,使用requestAnimationFrame能夠防止在一個刷新間隔內發生屢次函數執行,這樣保證了流暢性,也節省了函數執行的開銷 某些狀況下能夠直接使用requestAnimationFrame替代 Throttle 函數,都是限制回調函數執行的頻率
requestIdleCallback
,這個API
目前兼容性不太好,可是在Electron
開發中,可使用,二者仍是有區別的,並且這兩個api
用好了能夠解決不少複雜狀況下的問題~。固然你也能夠用上面的api
封裝這個api
,也並非很複雜。
當關注用戶體驗,不但願由於一些不重要的任務(如統計上報)致使用戶感受到卡頓的話,就應該考慮使用requestIdleCallback。由於requestIdleCallback回調的執行的前提條件是當前瀏覽器處於空閒狀態。
preload
,prefetch
,dns-prefetch
等指定提早請求指定文件,或者根據狀況,瀏覽器自行決定是否提早dns
預解析或者按需請求某些資源。webpack4
插件實現,目前京東在使用這個方案~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
,不肯定可否準時加載。通常只給一個寫這篇時間太耗時間,
React-native
的以及一些細節,後面再補充