react 服务端渲染
栏目: JavaScript · 发布时间: 5年前
内容简介:关于服务端渲染,这是一个老生常谈的事情了,已经很有成熟的方案在里面,最近接触了一下react的服务端渲染方式,偶有心得,为什么我要写这么一篇文章呢,因为我在研究如何开发的时候,发现网上的资料真的不够齐全,很多导致我花费更多的时间,所以我希望能够给予看到我文章的朋友一些帮助项目github链接:ssr: 服务端渲染
关于服务端渲染,这是一个老生常谈的事情了,已经很有成熟的方案在里面,最近接触了一下react的服务端渲染方式,偶有心得,为什么我要写这么一篇文章呢,因为我在研究如何开发的时候,发现网上的资料真的不够齐全,很多导致我花费更多的时间,所以我希望能够给予看到我文章的朋友一些帮助
项目github链接: github.com/HuskyToMa/h…
语言描述
ssr: 服务端渲染
前期准备
什么是ssr
ssr就是服务器渲染好前端页面然后推送到前端进行展示的一个过程。
为什么需要ssr
- 在单页应用中,是无法很好的支持seo的,所以很多方法是将入口页面用普通的html编写然后剩下的用单页应用的形式,这样就显得不是一个整体的项目。
- 服务端请求文件是直接在本地读取文件的,而不是客户端还需要发送http请求去获取文件内容,所以ssr会比正常的项目速度快一点
- 单页应用的首屏渲染会有一段空白时间,因为js还在加载的时候,页面是没有内容在里面的,而ssr会直接把dom元素渲染到html里面返回回来
前端如何做ssr
既然ssr是服务端渲染,那么肯定是后端进行渲染我们的数据,那不是不能用前端的各种框架了吗?
其实不然,由于nodejs的出现,打破了这么一个僵局,nodejs良好的接洽react框架,以至于我们在node中可以直接使用react开发页面,前后端共用同一套代码,通过react的rendertoString方法直接让react组件转换成dom字符串,然后输入到html模板中,在通过http response返回就直接渲染到页面(当然讲的简单,里面还是有一些东西的,后面慢慢分解)
技术支持
- react:主要库
- redux:状态管理库
- react-router:路由管理库
- webpack:代码编译
- babel:处理es6+的代码
- babel-node:处理nodejs的es6+代码
- nodejs:基础的nodejs知识
- express:nodejs框架
- typescript:js的超集(可以不用)
开始
ssr渲染本身是需要打包两套代码,一个用于服务器,一套用于客户端,所以我需要两套的开发环境以及生成环境
npm init 初始化我们的文件夹
创建文件目录
以下是我个人的习惯目录,可能并不是很正规,看一下即可
- webpack : 包含webpack的配置文件 - antd.config.js:antd的less配置文件 - babel.config.js:webpack到时候使用的配置文件 - webpack.base.conf.js:webpack base文件 - webpack.dev.conf.js: webpack 客户端dev文件 - webpack.devServer.conf.js:webpack 服务端dev文件 - webpack.pro.conf.js:webpack 客户端pro文件 - webpack.proServer.conf.js:webpack 服务端pro文件 - src : 主要js目录 - component:组件内容 - pages :入口文件 - redux :redux入口 - store :生成store的方法 - sass : sass样式目录 - app.js : react文件统一入口 - index.js: 渲染入口 - utils : 主要功能函数 - render.js:主要执行渲染的文件 - getTpl.js:获取模板 - build.js:启动构建的方法 - devServer.js:启动dev模式的文件 - server.js:打包构建的服务端内容 - .gitignore:忽略git上传内容 - .babelrc:node使用的babel配置文件 - tsconfig.json:ts的配置文件 复制代码
开发环境
需要几个必要的包:
react , react-dom , react-redux , redux , react-router , react-router-dom @types/react , @types/react-dom 复制代码
通过npm安装完成之后,我们就能在项目中进行引用了
如果已经按照我刚刚的目录创建好文件了,那么我们可以直接开始配置了。
很详细的配置内容大家基本都清楚,所以我这里就添加一些我自己的修改内容。
babel.conf.js
如果不用服务端渲染的话, 我们可以直接使用根目录中的babelrc文件,但是这次我们用了服务端渲染,所以我单独把wenpack打包的文件拎了出来放在一个js文件中
很多人会问,webpack中不是只能用require引用吗,你这里写export能拿到吗? 这个问题我会在稍后做出解释 export const clientBabelConfig = { presets:[ "@babel/preset-env", "@babel/preset-react" ],// 没有啥特别变化,正常流程 plugins: [ "@babel/plugin-syntax-dynamic-import",// 支持import(),虽然我最后没有用 "@babel/plugin-transform-runtime", "@babel/plugin-proposal-class-properties", [ "import", { "libraryName": "antd", "libraryDirectory": "es", "style": true } ], ], } export const serverBabelConfig = { presets:[ [ "@babel/preset-env", { "modules": false, "targets":{ node: 'current', } } ], "@babel/preset-react" ], plugins:[ [ "import", { "libraryName": "antd", } ], "@babel/plugin-proposal-class-properties", "dynamic-import-node" // node端的import() ] } 复制代码
貌似有一些重复的配置,我当时写完也就没有处理掉了,将就一下哈。
设置好babel的内容,就可以开始设置webpack的配置了,这一步相对简单,毕竟有过webpack配置竟然的人,很快就能够搭好了,我就只讲我这边觉得需要更改的几个内容:
webpack.dev.conf.js
- 需要使用 webpack-manifest-plugin 插件,生成manifest文件,用于读取生成的js目录
- dev环境关掉输出html的插件 html-webpack-plugin
webpack.devServer.conf.js
- output中的libraryTarget要设置成commonjs2
- 添加 target:'node'
- entry: './utils/render.js' // 开发环境中,直接将入口定在render就行了,我们只改变render的内容进行渲染
其余的配置与正常配置基本相近并没有特别大的区别
devServer.js
由于我们要同时启动客户端的代码和服务端的代码,所以我写了一个devServer的文件,内部使用了webpack的nodeAPI来构建环境,以及express来搭建服务端服务
在这文件里,其实分成了两个部分
import webpack from 'webpack'; import middleware from 'webpack-dev-middleware'; import devServerConfig from './webpack/webpack.devSever.conf'; import devClientConfig from './webpack/webpack.dev.conf'; import {ChunkExtractor} from '@loadable/server'; import express from 'express'; import MFS from 'memory-fs'; import path from 'path'; const app = new express(); const fs = new MFS(); const clientCompiler = webpack(devClientConfig); const serverCompiler = webpack(devServerConfig); const PORT = 8080; let renderPath = ''; let render; // 使用webpack-dev-middleware中间件( webpack , devServer用的插件 ) app.use(middleware(clientCompiler , { noInfo: true, serverSideRender: true, publicPath: devClientConfig.output.publicPath, })) // 监听客户端内容编译完成 clientCompiler.hooks.done.tapAsync("done", stats=>{ const info = stats.toJson(); if( stats.hasErrors ) console.log(info.errors); if( stats.hasWarnings ) console.log(info.warnings); }) // 修改服务端编译的输出方式( memory-fs 输入到内存 ) serverCompiler.outputFileSystem = fs; serverCompiler.watch({ aggregateTimeout: 300, poll: 1000, ignored: /node_modules/, },(err , stats)=>{ if(err) return console.log(err); console.log('compiler done'); // 编译到内存的路径 renderPath = path.join( devServerConfig.output.path , devServerConfig.output.filename ); // 读取内容并转成String类型 const content = fs.readFileSync( renderPath , 'utf-8').toString(); // 因为读取的是js文件,所以直接执行可以获取到输出的内容 // new Function 找不到module 所以改用eval,由于在后端所以避免了风险 render = eval(content).default; }) // 设置项目的静态文件地址 app.use( express.static( devServerConfig.output.path ) ); app.get('/*',(req,res)=>{ // console.log( , '111111'); const manifest = JSON.parse(clientCompiler.outputFileSystem.readFileSync(`${clientCompiler.outputPath}/manifest.json`)); res.send( render( req.url , manifest) ); }) app.listen(PORT,function(){ console.log('启动成功:localhost:' + PORT); }) 复制代码
render文件内容
import {Provider} from 'react-redux'; import React from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import getTpl from './getTpl'; import configureStore from 'STORE'; // store 就是我们react设置的store内容 import isObject from 'isobject'; import routerConfig from 'ROUTER'; // 设置store内容 const store = configureStore(); export default function( url , manifest){ if( !routerConfig[url] ){ return null; } const js = manifestJsLoader( manifest ); const css = manifestCssLoader( manifest ); // 获取当前路由下面的组件 const Component = routerConfig[url].component; const html = renderToStaticMarkup( <Provider store={store}><Component/></Provider> ); return getTpl( html , css , js ) } const manifestJsLoader = ( manifest ) => { return Object.keys(manifest) .filter(item=>item.endsWith('.js')) .map(item=>`<script src="${manifest[item]}"></script>`) .join('\n'); } const manifestCssLoader = ( manifest ) => { return Object.keys(manifest) .filter(item=>item.endsWith('.css')) .map(item=>`<link rel="stylesheet" href="${manifest[item]}"/>`) .join('\n'); } 复制代码
getTpl.js
就是用来渲染模板的
const getTpl = ( htmlContent , css , js) => { return ` <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>create-react-project</title> <meta charset="UTF-8" /> <meta property="qc:admins" content="1521476575645356367" /> <!-- <meta name="google" value="notranslate" /> --> <meta http-equiv="pragma" content="no-cache" /> <meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate" /> <meta http-equiv="Expires" content="0" /> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> ${css} </head> <body> <div id="root">${htmlContent}</div> </body> ${js} </html> ` } export default getTpl; 复制代码
很多import中大写的内容我用了webpack的alisa对应的文件可以去github上看下,当然,跟redux数据相对应的内容,需要服务端请求完之后,然后写入getTpl的模板里面,用script标签写一个window的内容,然后客户端进行重绘的时候,去调用window的内容存入store里面。
写好dev文件,基本上pro文件也已经出来了,配置基本相同没有啥区别,唯一的区别在于构建的文件中,webpack的入口我是用server.js 然后生成在dist目录里面。
还有重要的一点是,直接在命令行中运行node XXX.js如果内部使用了es6+的语法,那么是不可以运行的,安装一个babel-node然后用babel-node替换node来使用,但是最好在构建完之后的运行不要用babel-node,应该会增加额外的内容
希望这篇文章对大家有用处
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。