React服務端渲染+pm2自動化部署

本文是直接着手SSR部分的並經過實戰講述本身遇到的一些問題和方案,須要你們有必定的React,node和webpack基礎能力。skr,skr。css

服務端渲染

Server Slide Rendering服務端渲染,又簡寫爲SSR,他通常被用在咱們的SPA(Single-Page Application),即單頁應用。html

爲何要用SSR?

首先咱們須要知道SSR對於SPA的好處優點是什麼。前端

  • 更好的SEO(Search Engine Optimization)SEO是搜索引擎優化,簡而言之就是針對百度這些搜索引擎,可讓他們搜索到咱們的應用。這裏可能會有誤區,就是我也能夠在index.html上寫SEO,爲何會不起做用。由於React、Vue的原理是客戶端渲染,經過瀏覽器去加載js、css,有一個時間上的延遲,而搜索引擎不會管你的延遲,他就以爲你若是沒加載出來就是沒有的,因此是搜不到的。
  • 解決一開始的白屏渲染,上面講了React的渲染原理,而SSR服務端渲染是經過服務端請求數據,由於服務端內網的請求快,性能好因此會更快的加載全部的文件,最後把下載渲染後的頁面返回給客戶端。

上面提到了服務端渲染和客戶端渲染,那麼它們的區別是什麼呢?

客戶端渲染路線:node

  1. 請求一個html
  2. 服務端返回一個html
  3. 瀏覽器下載html裏面的js/css文件
  4. 等待js文件下載完成
  5. 等待js加載並初始化完成
  6. js代碼終於能夠運行,由js代碼向後端請求數據( ajax/fetch )
  7. 等待後端數據返回
  8. react-dom( 客戶端 )從無到完整地,把數據渲染爲響應頁面

服務端渲染路線:react

  1. 請求一個html
  2. 服務端請求數據( 內網請求快 )
  3. 服務器初始渲染(服務端性能好,較快)
  4. 服務端返回已經有正確內容的頁面
  5. 客戶端請求js/css文件
  6. 等待js文件下載完成
  7. 等待js加載並初始化完成
  8. react-dom( 客戶端 )把剩下一部分渲染完成( 內容小,渲染快 )

其主要區別就在於,客戶端從無到有的渲染,服務端是先在服務端渲染一部分,在再客戶端渲染一小部分webpack

咱們怎麼去作服務端渲染?

咱們這裏是用express框架,node作中間層進行服務端渲染。經過將首頁進行同構處理,讓服務端,經過調用ReactDOMServer.renderToNodeStream方法把Virtual DOM轉換成HTML字符串返回給客戶端,從而達到服務端渲染的目的。nginx

這裏項目起步是已經作完前端和後端,是把已經寫好的React Demo直接拿來用git

服務端渲染開始

既然是首頁SSR,首先咱們要把首頁對應的index.js抽離出來放入咱們服務端對應的server.js,那麼index.js中組件對應的靜態css和js文件咱們須要打包出來。es6

用webpack打包文件到build文件夾

咱們來運行npm run buildweb

咱們能夠看到兩個重要的文件夾,一個是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,就能看到服務端跑起來了。

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,能夠看到報錯了。

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

css-modules-require-hook

  • 由於服務端此時不認識咱們的css文件,咱們須要安裝一個包,來讓服務端處理css文件。
  • npm i css-modules-require-hook -S安裝在生產環境下。
  • 在項目根目錄建立一個crmh.conf.js鉤子文件進行配置,看下圖。

寫入代碼

// 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,會發現又報了個錯。

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的引用名字,卻沒有地址

  • 因此此時要在外面加個殼,把以前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守護進程。

  • 首先咱們得有一臺雲服務器,這裏我是在阿里雲買的一臺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公鑰中,可進入設置,具體看圖

  • 咱們也要把本身電腦上的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
  • 若是上述問題都解決了最後會如圖所示

  • 最後咱們能夠進入服務器,pm2 list,看到成功跑起來了

  • 若是應用在不斷的重啓,說明開啓失敗了,須要pm2 logs看看日誌

  • 咱們能夠訪問服務器地址:8088,並看到應用跑起來了

域名代理

  • 咱們進入阿里雲控制檯解析本身的域名(傳送門☞阿里雲

  • 添加一條記錄

  • 回到服務器,咱們修改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
複製代碼
  • 查看阿里雲控制檯的安全組是否開了對應的端口

  • 最後最後!!!,終於成功了。能夠點擊連接查看一下。 走你!

  • 固然下次若是你想直接更新項目,能夠在項目對應的路徑提交到git上,而後再使用pm2 deploy ecosystem.json production便可在服務器上自動部署

相關文章
相關標籤/搜索