React服务端渲染+pm2自动化部署 | 您所在的位置:网站首页 › ssr服务端渲染遇到的问题 › React服务端渲染+pm2自动化部署 |
本文是直接着手SSR部分的并通过实战讲述自己遇到的一些问题和方案,需要大家有一定的React,node和webpack基础能力。skr,skr。 服务端渲染Server Slide Rendering服务端渲染,又简写为SSR,他一般被用在我们的SPA(Single-Page Application),即单页应用。 为什么要用SSR?首先我们需要知道SSR对于SPA的好处,优势是什么。 更好的SEO(Search Engine Optimization),SEO是搜索引擎优化,简而言之就是针对百度这些搜索引擎,可以让他们搜索到我们的应用。这里可能会有误区,就是我也可以在index.html上写SEO,为什么会不起作用。因为React、Vue的原理是客户端渲染,通过浏览器去加载js、css,有一个时间上的延迟,而搜索引擎不会管你的延迟,他就觉得你如果没加载出来就是没有的,所以是搜不到的。 解决一开始的白屏渲染,上面讲了React的渲染原理,而SSR服务端渲染是通过服务端请求数据,因为服务端内网的请求快,性能好所以会更快的加载所有的文件,最后把下载渲染后的页面返回给客户端。 上面提到了服务端渲染和客户端渲染,那么它们的区别是什么呢?客户端渲染路线: 请求一个html 服务端返回一个html 浏览器下载html里面的js/css文件 等待js文件下载完成 等待js加载并初始化完成 js代码终于可以运行,由js代码向后端请求数据( ajax/fetch ) 等待后端数据返回 react-dom( 客户端 )从无到完整地,把数据渲染为响应页面服务端渲染路线: 请求一个html 服务端请求数据( 内网请求快 ) 服务器初始渲染(服务端性能好,较快) 服务端返回已经有正确内容的页面 客户端请求js/css文件 等待js文件下载完成 等待js加载并初始化完成 react-dom( 客户端 )把剩下一部分渲染完成( 内容小,渲染快 )其主要区别就在于,客户端从无到有的渲染,服务端是先在服务端渲染一部分,在再客户端渲染一小部分。 我们怎么去做服务端渲染?我们这里是用express框架,node做中间层进行服务端渲染。通过将首页进行同构处理,让服务端,通过调用ReactDOMServer.renderToNodeStream方法把Virtual DOM转换成HTML字符串返回给客户端,从而达到服务端渲染的目的。 这里项目起步是已经做完前端和后端,是把已经写好的React Demo直接拿来用 服务端渲染开始既然是首页SSR,首先我们要把首页对应的index.js抽离出来放入我们服务端对应的server.js,那么index.js中组件对应的静态css和js文件我们需要打包出来。 用webpack打包文件到build文件夹我们来运行npm run build 我们可以看到两个重要的文件夹,一个是js文件夹,一个是css文件夹,他就是我们项目的js和css静态资源文件 将打包后的build文件能在服务端server.js中访问到因为是服务端,我们需要用到express import express from 'express' import reducers from '../src/reducer'; import userRouter from './routes/user' import bodyParser from 'body-parser' import cookieParser from 'cookie-parser' import model from './model' import path from 'path' import https from 'http' import socketIo from 'socket.io' const Chat = model.getModel('chat') //新建app const app = express() //work with express const server = https.Server(app) const io = socketIo(server) io.on('connection',function(socket){ socket.on('sendmsg',function(data){ let {from,to,msg} = data let chatid = [from,to].sort().join('_') Chat.create({chatid,from,to,content:msg},function(e,d){ io.emit('recvmsg',Object.assign({},d._doc)) }) // console.log(data) // //广播给全局 // io.emit('recvmsg',data) }) }) app.use(cookieParser()) app.use(bodyParser.json()) app.use('/user',userRouter) app.use(function(req,res,next){ if(req.url.startsWith('/user/') || req.url.startsWith('/static/')){ return next() } //如果访问url根路径是user或者static就返回打包后的主页面 return res.sendFile(path.resolve('build/index.html')) }) //映射build文件路径,项目上要使用 app.use('/',express.static(path.resolve('build'))) server.listen(8088, function () { console.log('开启成功') }) 复制代码 主要看上面的app.use('/',express.static(path.resolve('build')))和res.sendFile(path.resolve('build/index.html'))这两段代码。 他们把打包后的主页放入服务端代码中返回给客户端。 因为上面我用了import代码,所以我们在开发环境中需要用到babel-cli里的babel-node来编译。 安装npm --registry https://registry.npm.taobao.org i babel-cli -S`,大家如果觉得这样切换源麻烦,可以下个nrm,360度无死角切换各种源,好用! 我们需要修改package.json的启动服务器的npm scripts。"server": "NODE_ENV=test nodemon --exec babel-node server/server.js" cross-env跨平台设置node环境变量的插件。 nodemon和supervisor一样是watch服务端文件,只要一改变就会重新运行,相当于热重载。nodemon更轻量 最后我们来跑一下npm run server,就能看到服务端跑起来了。此时将服务端renderToNodeStream后的代码返回给前端,但是这个时候还是不行,我们执行一下npm run server,可以看到报错了。 写入代码 // css-modules-require-hook module.exports = { generateScopedName: '[name]__[local]___[hash:base64:5]', //下面的代码在本项目中暂时用不到,但是以下配置在我另一个项目中有用到,我来讲一下他的配置 //扩展名 //extensions: ['.scss','.css'], //钩子,这里主要做一些预处理的scss或者less文件 //preprocessCss: (data, filename) => // require('node-sass').renderSync({ // data, // file: filename // }).css, //是否导出css类名,主要用于CSSModule //camelCase: true, }; 复制代码 修改我们的server.js文件,添加import csshook from 'css-modules-require-hook/preset',注意⚠️,一定要把这行代码放在导入App模块之前。 import csshook from 'css-modules-require-hook/preset' //我们的首页入口 import App from '../src/App' 复制代码此时在运行server.js,会发现又报了个错。 运行之后发现又报错了,这个很简单,因为我们只有image的引用名字,却没有地址 到此为止我们开发模式下的SSR搭建完毕,接下来生产模式的坑我来讲一下。 生产环境SSR准备我们上面所讲的只是开发模式下的SSR,因为我们是通过babel-node编译jsx和es6代码的,只要一脱离babel-node就会全错,所以我们需要webpack打包服务端代码 我们需要创建一个webserver.config.js,用来打包server的代码 const path = require('path'), fs = require('fs'), webpack = require('webpack'), autoprefixer = require('autoprefixer'), HtmlWebpackPlugin = require('html-webpack-plugin'), ExtractTextPlugin = require('extract-text-webpack-plugin') cssFilename = 'static/css/[name].[contenthash:8].css'; CleanWebpackPlugin = require('clean-webpack-plugin'); nodeExternals = require('webpack-node-externals'); serverConfig = { context: path.resolve(__dirname, '..'), entry: {server: './server/server'}, output: { libraryTarget: 'commonjs2', path: path.resolve(__dirname, '../build/server'), filename: 'static/js/[name].js', chunkFilename: 'static/js/chunk.[name].js' }, // target: 'node' 指明构建出的代码是要运行在node环境里. // 不把 Node.js 内置的模块打包进输出文件中,例如 fs net 模块等 target: 'node', //指定在node环境中是否要这些模块 node: { __filename: true, __dirname: true, // module:true }, module: { loaders: [{ test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader?cacheDirectory=true', options: { presets: ['es2015', 'react-app', 'stage-0'], plugins: ['add-module-exports', [ "import", { "libraryName": "antd-mobile", "style": "css" } ],"transform-decorators-legacy"] }, },{ test: /\.css$/, exclude: /node_modules|antd-mobile\.css/, loader: ExtractTextPlugin.extract( Object.assign( { fallback: { loader: require.resolve('style-loader'), options: { hmr: false, }, }, use: [ { loader: require.resolve('css-loader'), options: { importLoaders: 1, minimize: true, modules: false, localIdentName:"[name]-[local]-[hash:base64:8]", // sourceMap: shouldUseSourceMap, }, }, { loader: require.resolve('postcss-loader'), options: { ident: 'postcss', plugins: () => [ require('postcss-flexbugs-fixes'), autoprefixer({ browsers: [ '>1%', 'last 4 versions', 'Firefox ESR', 'not ie < 9', // React doesn't support IE8 anyway ], flexbox: 'no-2009', }), ], }, }, ], }, ) ), }, { test: /\.css$/, include: /node_modules|antd-mobile\.css/, use: ExtractTextPlugin.extract({ fallback: require.resolve('style-loader'), use: [{ loader: require.resolve('css-loader'), options: { modules:false }, }] }) }, { test: /\.(jpg|png|gif|webp)$/, loader: require.resolve('url-loader'), options: { limit: 10000, name: 'static/media/[name].[hash:8].[ext]', }, }, { test: /\.json$/, loader: 'json-loader', }] }, // 不把 node_modules 目录下的第三方模块打包进输出文件中, externals: [nodeExternals()], resolve: {extensions: ['*', '.js', '.json', '.scss']}, plugins: [ new CleanWebpackPlugin(['../build/server']), new webpack.optimize.OccurrenceOrderPlugin(), //把第三方库从js文件中分离出来 new webpack.optimize.CommonsChunkPlugin({ //抽离相应chunk的共同node_module minChunks(module) { return /node_modules/.test(module.context); }, //从要抽离的chunk中的子chunk抽离相同的模块 children: true, //是否异步抽离公共模块,参数boolean||string async: false, }), new webpack.optimize.CommonsChunkPlugin({ children:true, //若参数是string即为抽离出来后的文件名 async: 'shine', //最小打包的文件模块数,即要抽离的公共模块中的公共数,比如三个chunk只有1个用到就不算公共的 //若为Infinity,则会把webpack runtime的代码放入其中(webpack 不再自动抽离公共模块) minChunks:2 }), //压缩 new webpack.optimize.UglifyJsPlugin(), //分离css文件 new ExtractTextPlugin({ filename: cssFilename, }), new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), ], } module.exports = serverConfig 复制代码重点⚠️ 指定target,打包出来的代码运行在哪里 指定externals不要把node_modules包打包,因为此项目运行在服务端,直接用外面的node_modules就行。不然打包后会很大。 loader中用babel对js的处理ok,现在来我们改一下package.json的npm scripts,添加一个packServer,顺便改一下build的scripts "scripts": { "clean": "rm -rf build/", "dev": "node scripts/start.js", "start": "cross-env NODE_ENV=development npm run server & npm run dev", "build": "npm run clean && node scripts/build.js && npm run packServer", "test": "nodemon scripts/test.js --env=jsdom", "server": "cross-env NODE_ENV=test nodemon --exec babel-node server/server.js", "gulp": "cross-env NODE_ENV=production gulp", "packServer": "cross-env NODE_ENV=production webpack --config ./config/webserver.config.js" }, 复制代码 packServer指定了生产环境,这在之后会用到。 build是先clean掉build文件夹,在去打包客户端的代码,打包完之后再去打包服务端的代码那么到这里为止我们差不多可以自己试试了 先npm run build,会生成打包后的build文件夹,里面包含了我们的服务端和客户端代码 找到打包后的node文件运行它,在build/server/static/js目录下,可直接node文件启动。这就解决了我们生产环境下的问题。 pm2,服务器自动部署现在我们要把我们的项目部署到服务器上,并用pm2守护进程。 在服务器执行sudo nginx -s reload,重启nginx。此时我们就可以通过我们的域名地址访问到我们的应用了。 这里可能访问会404,这个时候我们需要看一下我们服务器的防火墙,sudo vi /etc/iptables.up.rules,修改mongodb的对外端口,并且重启防火墙sudo iptables-restore < /etc/iptables.up.rules -A INPUT -s 127.0.0.1 -p tcp --destination-port 8088 -m state --state NEW,ESTABLISHED -j ACCEPT -A OUTPUT -d 127.0.0.1 -p tcp --source-port 8088 -m state --state ESTABLISHED -j ACCEPT 复制代码 查看阿里云控制台的安全组是否开了对应的端口最后最后!!!,终于成功了。可以点击链接查看一下。 走你! 当然下次如果你想直接更新项目,可以在项目对应的路径提交到git上,然后再使用pm2 deploy ecosystem.json production即可在服务器上自动部署。 |
CopyRight 2018-2019 实验室设备网 版权所有 |