美文网首页让前端飞Web 前端开发
React服务端渲染+pm2自动化部署

React服务端渲染+pm2自动化部署

作者: Shinemax | 来源:发表于2018-07-30 18:05 被阅读16次

    本文是直接着手SSR部分的并通过实战讲述自己遇到的一些问题和方案,需要大家有一定的React,node和webpack基础能力。skr,skr。

    15315912157248fdea6bb73.jpeg

    服务端渲染

    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服务端渲染是通过服务端请求数据,因为服务端内网的请求快,性能好所以会更快的加载所有的文件,最后把下载渲染后的页面返回给客户端。

    上面提到了服务端渲染和客户端渲染,那么它们的区别是什么呢?

    客户端渲染路线:

    1. 请求一个html
    2. 服务端返回一个html
    3. 浏览器下载html里面的js/css文件
    4. 等待js文件下载完成
    5. 等待js加载并初始化完成
    6. js代码终于可以运行,由js代码向后端请求数据( ajax/fetch )
    7. 等待后端数据返回
    8. react-dom( 客户端 )从无到完整地,把数据渲染为响应页面

    服务端渲染路线:

    1. 请求一个html
    2. 服务端请求数据( 内网请求快 )
    3. 服务器初始渲染(服务端性能好,较快)
    4. 服务端返回已经有正确内容的页面
    5. 客户端请求js/css文件
    6. 等待js文件下载完成
    7. 等待js加载并初始化完成
    8. 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

    image

    我们可以看到两个重要的文件夹,一个是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,就能看到服务端跑起来了。
    image

    ReactDOMServer.renderToString/ReactDOMServer.renderToNodeStream

    • 这里我们先讲一下在浏览器中React.createElement把React的类进行实例化,实例化后的组件可以进行mount,最后通过React.render渲染到我们的客户端浏览器界面。
    • 而在服务器中我们可以通过 renderToString或者renderToNodeStream方法把React实例化的组件,直接渲染生成html标签。那么这俩个有什么区别呢?
    • renderToNodeStream是React 16最新发布的东西,它支持直接渲染到节点流。渲染到流可以减少你的内容的第一个字节(TTFB)的时间,在文档的下一部分生成之前,将文档的开头至结尾发送到浏览器。 当内容从服务器流式传输时,浏览器将开始解析HTML文档。速度是renderToString的三倍,所以我们在这里使用renderToNodeStream
    import express from 'express'
    import React from 'react'
    import {renderToStaticMarkup,renderToNodeStream} from 'react-dom/server'
    
    import thunk from 'redux-thunk';
    import { Provider } from 'react-redux';
    import {StaticRouter} from 'react-router-dom'
    import {
      createStore,
      applyMiddleware,
      //组合函数用的
      compose
    } from 'redux';
    import App from '../src/App'
    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()
      }
      const store = createStore(reducers,compose(
        applyMiddleware(thunk)
      ))
      //这个 context 对象包含了渲染的结果
      let context = {}
      const root = (<Provider store={store}>
                        <StaticRouter
                          location={req.url}
                          context={context}
                          >
                            <App></App>
                        </StaticRouter>
                    </Provider>)
      const markupStream = renderToNodeStream(root)
      markupStream.pipe(res,{end:false})
      markupStream.on('end',()=>{
        res.end()
      })
    })
    //映射build文件路径,项目上要使用
    app.use('/',express.static(path.resolve('build')))
    
    
    server.listen(8088, function () {
        console.log('开启成功')
    })
    

    此时将服务端renderToNodeStream后的代码返回给前端,但是这个时候还是不行,我们执行一下npm run server,可以看到报错了。

    image

    css-modules-require-hook/asset-require-hook

    css-modules-require-hook

    • 因为服务端此时不认识我们的css文件,我们需要安装一个包,来让服务端处理css文件。
    • npm i css-modules-require-hook -S安装在生产环境下。
    • 在项目根目录创建一个crmh.conf.js钩子文件进行配置,看下图。
    image

    写入代码

    // 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

    asset-require-hook

    • 这个错误是因为服务端没有处理前端代码需要的图片
    • 需要安装npm i asset-require-hook -S,这个插件用来让服务端处理图片,注意⚠️前提是客户端代码,引用图片都需要require
    • server.js写入代码
    //解决图片问题,客户端代码引用图片都需要require
    import assethook from 'asset-require-hook'
    assethook({
      extensions:['png'],
      //图片大小下于10000的图片会直接base64编码
      limit: 10000
    })
    

    运行之后发现又报错了,这个很简单,因为我们只有image的引用名字,却没有地址

    image
    • 所以此时要在外面加个壳,把之前build之后的静态js、css文件引入进去,添加html、head这些标签。来看完整代码
    import 'babel-polyfill'
    import express from 'express'
    import React from 'react'
    import {renderToString,renderToStaticMarkup,renderToNodeStream} from 'react-dom/server'
    
    //引入css文件和js文件
    import staticPath from '../build/asset-manifest.json'
    
    import thunk from 'redux-thunk';
    import { Provider } from 'react-redux';
    import {StaticRouter} from 'react-router-dom'
    import {
      createStore,
      applyMiddleware,
      //组合函数用的
      compose
    } from 'redux';
    //解决服务端渲染的图片问题 必须放在App之前
    import csshook from 'css-modules-require-hook/preset'
    //解决图片问题,需要require
    import assethook from 'asset-require-hook'
    assethook({
      extensions:['png'],
      limit: 10000
    })
    import App from '../src/App'
    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()
      }
      const store = createStore(reducers,compose(
        applyMiddleware(thunk)
      ))
      const obj = {
        '/msg':'聊天消息列表',
        '/me':'个人中心列表'
      }
      //这个 context 对象包含了渲染的结果
      let context = {}
      res.write(`<!DOCTYPE html>
      <html lang="en">
        <head>
          <meta charset="utf-8">
          <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
          <meta name="theme-color" content="#000000">
          <meta name="description" content="${obj[req.url]}"/>
          <meta name="keywords" content="SSR">
          <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
          <link rel="stylesheet" href="/${staticPath['main.css']}">
          <title>React App</title>
        </head>
        <body>
          <noscript>
            You need to enable JavaScript to run this app.
          </noscript>
          <div id="root">`)
      const root = (<Provider store={store}>
                        <StaticRouter
                          location={req.url}
                          context={context}
                          >
                            <App></App>
                        </StaticRouter>
                    </Provider>)
      const markupStream = renderToNodeStream(root)
      markupStream.pipe(res,{end:false})
      markupStream.on('end',()=>{
        res.write(`</div>
              <script src="/${staticPath['main.js']}"></script>
            </body>
          </html>`)
        res.end()
      })
    })
    //映射build文件路径,项目上要使用
    app.use('/',express.static(path.resolve('build')))
    
    
    server.listen(8088, function () {
        console.log('开启成功')
    })
    
    • 这个时候我们可以在html标签里加上SEO的meta<meta name="keywords" content="SSR">
    • 最后还要把客户端的index.js文件中的渲染机制改成hydrate,不用render,他们之间的区别可以看这个(传送门☞render !== hydrate
    ReactDOM.hydrate(
        (<Provider store={store}>
            <BrowserRouter>
                <App></App>
            </BrowserRouter>
        </Provider>),
        document.getElementById('root')
    )
    

    到此为止我们开发模式下的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守护进程。

    image
    • 首先我们得有一台云服务器,这里我是在阿里云买的一台ubuntu 14.04
    • 需要一个已经备案后的域名,域名也可以在阿里云买。当然也可以不用,可以直接服务器地址访问。
    • ok让我们开始吧。

    服务器部署

    • 在部署到服务器之前我们代码中还有些东西需要修改,修改mongod的连接地址.
    const env = process.env.NODE_ENV || 'development'
    //当生产环境时,需要改变mongodb的连接端口,根据你服务器的mongodb端口来,我这里是19999
    const BASE_URL = env == 'development'?"mongodb://localhost:27017/chat":"mongodb://127.0.0.1:19999/chat";
    
    • 修改客户端socket.io的链接地址const socket = io('ws://host:port'),改成你自己的服务器地址和端口号
    • 我们需要将自己的项目上传至码云。这里我使用码云,主要是因为码云的私仓是免费的。
    • 我们需要进入服务器的ssh目录下复制id_rsa.pub里的公钥放在码云的ssh公钥中,可进入设置,具体看图
    image
    • 我们也要把自己电脑上的ssh公钥在码云中设置,我这里是mac,在自己的用户目录下,可以按cmd+shift+.看隐藏文件(如果你设置过了,这一步就不要了)。
    • 服务器安装git,mongodb,pm2,nginx(如果服务器已经安装过了,就不需要了)
    • 需要开启mongodb
    • 我们在项目根目录新建一个ecosystem.json文件,这个文件是pm2的配置文件,具体的我就不说了,大家如果感兴趣可以去官网看看,(传送门☞pm2官网
    {
      "apps": [
        {
          //应用名称
          "name": "chat",
          //执行文件的路径
          "script": "./build/server/static/js/server.js",
          "env": {
            "COMMON_VARIABLE": "true"
          },
          "env_production": {
            "NODE_ENV": "production"
          }
        }
      ],
      "deploy": {
        "production": {
          //服务器用户
          "user": "xxx",
          //服务器地址
          "host": ["xxx"],
          //服务器端口
          "port": "xxx",
          "ref": "origin/master",
          //这里填你的项目git ssh
          "repo": "xxx",
          //服务器的存放项目路径
          "path": "/www/chat/production",
          "ssh_options": "StrictHostKeyChecking=no",
          //钩子
          "post-deploy": "npm --registry https://registry.npm.taobao.org install && npm run build && pm2 startOrRestart ecosystem.json --env production",
          "env": {
            //环境
            "NODE_ENV": "production"
          }
        }
      }
    }
    
    • 在服务器新建项目目录新建/www/chat/文件夹。
    • 在本地电脑执行 pm2 deploy ecosystem.json production setup
    • 这里大家肯定会报错,这是我故意埋的坑,因为chat文件夹的权限不够,需要进入服务器的www文件夹,执行sudo chmod 777 chat
    • 进入服务器的.bashrc文件,注视掉上面的几行代码
    • source .bashrc重新载入一下.bashrc文件
    • 开启pm2服务 pm2 deploy ecosystem.json production
    • 这里可能有的人会报错,主要原因是本地电脑的pm2的权限问题,需要找到pm2文件夹,chmod 666 pm2
    • 如果上述问题都解决了最后会如图所示
    image
    • 最后我们可以进入服务器,pm2 list,看到成功跑起来了
    image
    • 如果应用在不断的重启,说明开启失败了,需要pm2 logs看看日志
    image
    • 我们可以访问服务器地址:8088,并看到应用跑起来了

    域名代理

    • 我们进入阿里云控制台解析自己的域名(传送门☞阿里云
    image
    • 添加一条记录
    image
    • 回到服务器,我们修改nginx配置文件,通过反向代理,让我们通过域名也可以访问他
    upstream chat {
      server 127.0.0.1:8088;
    }
    
    server {
      listen 80;
      server_name www.webman.vip;
    
      location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forward-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-Nginx-Proxy true;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
    
        proxy_pass http://chat;
        proxy_redirect off;
      }
      # 静态文件地址
      location ~* ^.+\.(jpg|jpeg|gif|png|ico|css|js|pdf|txt){
        root /www/website/production/current/build;
      }
    }
    
    • 在服务器执行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
    
    • 查看阿里云控制台的安全组是否开了对应的端口
    image
    • 最后最后!!!,终于成功了。可以点击链接查看一下。 走你!

    • 当然下次如果你想直接更新项目,可以在项目对应的路径提交到git上,然后再使用pm2 deploy ecosystem.json production即可在服务器上自动部署

    相关文章

      网友评论

        本文标题:React服务端渲染+pm2自动化部署

        本文链接:https://www.haomeiwen.com/subject/szlomftx.html