React
为了大型应用而生,Electron
和React-native
赋予了它构建移动端跨平台App
和桌面应用的能力,Taro
则赋予了它一次编写,生成多种平台小程序和React-native
应用的能力,这里特意说下Taro
,它是国产,文档写得比较不错,而且它的升级速度比较快,有issue
我看也会及时解决,他们的维护人员还是非常敬业的!
,
-
Tips
: 本文某些知识点如果介绍不对或者不全的地方欢迎指出,本文可能内容比较多,阅读时间花费比较长,但是希望你可以认真看下去,可以的话最好手把手去实现一些code
,本文所有代码均手写。
本文会从原生浏览器环境,到跨平台开发逐渐去深入介绍, 先给一些资料
- 手写 React 优化脚手架带项目
- react-ssr 的源码
- 手写 Node.js 原生静态资源服务器
- 跨平台 Electron 的 demo
原生浏览器环境:
- 原生浏览器环境其实是最考验前端工程师能力的编程环境,因为我们前端大部分一开始面向浏览器编程,现在很多很多工作 5 -10 年的前端,性能面板 API 都不知道用,怎么看调用函数分析耗时都不知道,这也是最近面试的情况,觉得有人说 35 岁失业的情况,是普遍存在,但是很大部分是你在混啊兄弟。
原生浏览器环境中使用 React
框架,比较常见的是制作单页面 SPA
应用:
原生的 SPA
应用,分以下几种:
- 纯
CSR
渲染(客户端渲染) - 纯
SSR
渲染(服务端渲染) - 混合渲染(预渲染,
webpack
的插件预渲染,Next.js
的约定式路由SSR
, 或者使用Node.js
做中间件,做部分SSR
, 加快首屏渲染,或者指定路由SSR
.)
下面会分别仔细介绍这几种渲染形式的精细化渲染,以及优缺点:
纯 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
文件 - 用户输入了
url
地址栏然后客户端返回静态文件,客户端开始解析 - 客户端解析文件,
js
代码动态生成页面。(这也是为什么说单页面应用的SEO
不友好的原因,初始它只是一个空的div
标签的HTML
文件) - 判断一个页面是不是
CSR
,很大程度上可以根据右键点开查看页面元素,如果只有一个空的div
标签,那么大概率可以说是单页面,CSR
,客户端渲染的网页。
纯 CSR
的应用,如何精细化渲染呢?
单页面采取 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-lazyload
,支持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
的插件,进行快速使用,对于一些数据内容不需要存储数据库的,但是却想要一次拉取,多次复用,那么可以使用这个配置
serverce work 也有它的一套生命周期
- 通常我们如果要使用 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
长列表
]
- react-virtualized-auto-sizer 和 windowScroll 配合一起使用,达到页面复杂效果 + 大数据渲染保持 60FPS。上面的官网里有介绍这些组件~
高计算量的工作交给 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
的新版本调度阶段是拥有三根指针的可中断的链表遍历,所以这样既不影响下面的遍历,也不影响用户交互等行为。 -
requestIdleCallback
, 这个API
目前兼容性不太好,但是在Electron
开发中,可以使用,两者还是有区别的,而且这两个api
用好了可以解决很多复杂情况下的问题~。当然你也可以用上面的api
封装这个api
,也并不是很复杂。
使用 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
的以及一些细节,后面再补充
下面给出一些源码和资料地址:
- 手写 React 优化脚手架带项目
- react-ssr 的源码
- 手写 Node.js 原生静态资源服务器
- 跨平台 Electron 的 demo