react 服务端渲染
栏目: JavaScript · 发布时间: 7年前
内容简介:关于服务端渲染,这是一个老生常谈的事情了,已经很有成熟的方案在里面,最近接触了一下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,应该会增加额外的内容
希望这篇文章对大家有用处
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Inside Larry's and Sergey's Brain
Richard Brandt / Portfolio / 17 Sep 2009 / USD 24.95
You’ve used their products. You’ve heard about their skyrocketing wealth and “don’t be evil” business motto. But how much do you really know about Google’s founders, Larry Page and Sergey Brin? Inside......一起来看看 《Inside Larry's and Sergey's Brain》 这本书的介绍吧!