dispatch js实现_基于 React.js 和 Node.js 的SSR实现方案

基础概念

  1. SSR:即服务端渲染(Server Side Render)
  2. 传统的服务端渲染可以使用Java,php 等开发语言来实现,随着 Node.js 和相关前端领域技术的不断进步,前端同学也可以基于此完成独立的服务端渲染。
  3. 过程:浏览器发送请求 -> 服务器运行 React代码生成页面 -> 服务器返回页面 -> 浏览器下载HTML文档 -> 页面准备就绪
  4. 即:当前页面的内容是服务器生成好给到浏览器的。
  5. 对应CSR:即客户端渲染(Client Side Render)
  6. 过程:浏览器发送请求 -> 服务器返回空白 HTML(HTML里包含一个root节点和js文件) -> 浏览器下载js文件 -> 浏览器运行react代码 -> 页面准备就绪
  7. 即:当前页面的内容是JavaScript 渲染出来。
  8. 如何区分页面是否服务端渲染:
  9. 右键点击 -> 显示网页源代码,如果页面上的内容在HTML文档里,是服务端渲染,否则就是客户端渲染。
  10. 对比
  • CSR:首屏渲染时间长,react代码运行在浏览器,消耗的是浏览器的性能
  • SSR:首屏渲染时间短,react代码运行在服务器,消耗的是服务器的性能

为什么要用服务端渲染

  • 首屏加载时间优化,由于SSR是直接返回生成好内容的HTML,而普通的CSR是先返回空白的HTML,再由浏览器动态加载JavaScript脚本并渲染好后页面才有内容;所以SSR首屏加载更快、减少白屏的时间、用户体验更好。
  • SEO (搜索引擎优化),搜索关键词的时候排名,对大多数搜索引擎,不识别JavaScript 内容,只识别 HTML 内容。
  • (注:原则上可以不用服务端渲染时最好不用,所以如果只有 SEO 要求,可以用预渲染等技术去替代)

构建一个服务端渲染的项目

(1) 使用 Node.js 作为服务端和客户端的中间层,承担 proxy代理,处理cookie等操作。

(2) hydrate 的使用:在有服务端渲染情况下,使用hydrate代替render,它的作用主要是将相关的事件注水进HTML页面中(即:让React组件的数据随着HTML文档一起传递给浏览器网页),这样可以保持服务端数据和浏览器端一致,避免闪屏,使第一次加载体验更高效流畅。

1 ReactDom.hydrate(, document.getElementById('root'));

(3) 服务端代码webpack编译:通常会建一个webpack.server.js文件,除了常规的参数配置外,还需要设置target参数为'node'。

 1const serverConfig = { 2 target: 'node', 3 entry: './src/server/index.js', 4 output: { 5 filename: 'bundle.js', 6 path: path.resolve(__dirname, '../dist') 7 }, 8 externals: [nodeExternals()], 9 module: {10 rules: [{11 test: /.js?$/,12 loader: 'babel-loader',13 exclude: [14 path.join(__dirname, './node_modules')15 ]16 }17 ...18 ]19 }20 (此处省略样式打包,代码压缩,运行坏境配置等等...)21 ...22};

(4) 使用react-dom/server下的 renderToString方法在服务器上把各种复杂的组件和代码转化成 HTML 字符串返回到浏览器,并在初始请求时发送标记以加快页面加载速度,并允许搜索引擎抓取页面以实现SEO目的。

 1const render = (store, routes, req, context) => { 2 const content = renderToString(( 3  4  5 
6 {renderRoutes(routes)} 7
8 9 10 ));11 return `12 13 14 ssr15 16 17
${content}
18 19 20 21 `;22}23app.get('*', function (req, res) {24 ...25 const html = render(store, routes, req, context);26 res.send(html);27});

与 renderToString类似功能的还有:

i. renderToStaticMarkup:区别在于renderToStaticMarkup 渲染出的是不带data-reactid的纯HTML,在JavaScript加载完成后因为不认识之前服务端渲染的内容导致重新渲染(可能页面会闪一下)。

ii. renderToNodeStream:将React元素渲染为其初始HTML,返回一个输出HTML字符串的可读流。

iii. renderToStaticNodeStream:与renderToNodeStream此类似,除了这不会创建React在内部使用的额外DOM属性,例如data-reactroot。

(5) 使用redux 承担数据准备,状态维护的职责,通常搭配react-redux, redux-thunk(中间件:发异步请求用到action)使用。(本猿目前使用比较多是就是Redux和Mobx,这里以Redux为例)。

A. 创建store(服务器每次请求都要创建一次,客户端只创建一次):

 1const reducer = combineReducers({ 2 home: homeReducer, 3 page1: page1Reducer, 4 page2: page2Reducer 5}); 6 7export const getStore = (req) => { 8 return createStore(reducer, applyMiddleware(thunk.withExtraArgument(serverAxios(req)))); 9}1011export const getClientStore = () => {12 return createStore(reducer, window.STATE_FROM_SERVER, applyMiddleware(thunk.withExtraArgument(clientAxios)));13}

B. action: 负责把数据从应用传到store,是store数据的唯一来源

 1export const getData = () => { 2 return (dispatch, getState, axiosInstance) => { 3 return axiosInstance.get('interfaceUrl/xxx') 4 .then((res) => { 5 dispatch({ 6 type: 'HOME_LIST', 7 list: res.list 8 }) 9 });10 }11}

C. reducer:接收旧的state和action,返回新的state,响应actions并发送到store。

 1export default (state = { list: [] }, action) => { 2 switch(action.type) { 3 case 'HOME_LIST': 4 return { 5 ...state, 6 list: action.list 7 } 8 default: 9 return state;10 }11}12export default (state = { list: [] }, action) => {13 switch(action.type) {14 case 'HOME_LIST':15 return {16 ...state,17 list: action.list18 }19 default:20 return state;21 }22} 

D. 使用react-redux的connect,Provider把组件和store连接起来

  • Provider 将之前创建的store作为prop传给Provider
1const content = renderToString((2 3 4 
5 {renderRoutes(routes)}6
7 8 9));
  • connect([mapStateToProps],[mapDispatchToProps],[mergeProps], [options])接收四个参数,常用的是前两个属性
  • mapStateToProps 函数允许我们将store中的数据作为 props 绑定到组件上
  • mapDispatchToProps 将action作为props绑定到组件上
1 connect(mapStateToProps(),mapDispatchToProps())(MyComponent)

(6) 使用react-router承担路由职责

服务端路由不同于客户端,它是无状态的。React 提供了一个无状态的组件StaticRouter,向StaticRouter传递当前URL,调用ReactDOMServer.renderToString() 就能匹配到路由视图。

服务端

1import { StaticRouter } from 'react-router-dom';2import { renderRoutes } from 'react-router-config'3import routes from './router.js'456{renderRoutes(routes)}7 8

浏览器端

1import { BrowserRouter } from 'react-router-dom';2import { renderRoutes } from 'react-router-config'3import routes from './router.js'456 {renderRoutes(routes)}78 1const routes = [{ component: Root, 2 routes: [ 3 { path: '/', 4 exact: true, 5 component: Home, 6 loadData: Home.loadData 7 }, 8 { path: '/child/:id', 9 component: Child,10 loadData: Child.loadData11 routes: [12 path: '/child/:id/grand-child',13 component: GrandChild,14 loadData: GrandChild.loadData15 ]16 }17 ]18}]; 1import { matchRoutes } from 'react-router-config' 2 const loadBranchData = (location) => { 3 const branch = matchRoutes(routes, location.pathname) 4 5 const promises = branch.map(({ route, match }) => { 6 return route.loadData 7 ? route.loadData(match) 8 : Promise.resolve(null) 9 })1011 return Promise.all(promises)12}

(7) 写组件注意代码同构(即:一套React代码在服务端执行一次,在客户端再执行一次)

由于服务器端绑定事件是无效的,所以服务器返回的只有页面样式(&注水的数据),同时返回JavaScript文件,在浏览器上下载并执行JavaScript时才能把事件绑上,而我们希望这个过程只需编写一次代码,这个时候就会用到同构,服务端渲染出样式,在客户端执行时绑上事件。

优点: 共用前端代码,节省开发时间

弊端: 由于服务器端和浏览器环境差异,会带来一些问题,如document等对象找不到,DOM计算报错,前端渲染和服务端渲染内容不一致等;前端可以做非常复杂的请求合并和延迟处理,但为了同构,所有这些请求都在预先拿到结果才会渲染。

6e757100253ee33b76fc2f6e4b20b655.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值