react 服务端渲染
栏目: JavaScript · 发布时间: 6年前
内容简介:关于服务端渲染,这是一个老生常谈的事情了,已经很有成熟的方案在里面,最近接触了一下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,应该会增加额外的内容
希望这篇文章对大家有用处
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。