服务端渲染的项目打包后,需要使用 Node 运行服务端的脚本文件。在我的服务端渲染项目中,客户端和服务端打包的代码都存放在 build
目录下:
build
├── client
│ ├── bundle.6faef.js
│ ├── bundle.6faef.js.map
│ ├── font
│ │ ├── 4123775d92a338f6864d80b112d33f44.ttf
│ │ └── 4470b313b33648e3ba3daa18c9fbc087.eot
│ └── img
│ └── iconfont.svg
├── font
│ ├── 4123775d92a338f6864d80b112d33f44.ttf
│ └── 4470b313b33648e3ba3daa18c9fbc087.eot
├── img
│ └── iconfont.svg
├── manifest.json
├── server.js # 服务端脚本
└── static
├── favicon.ico
├── image
│ ├── 404.png
├── js
│ ├── help.js
│ └── qrcode.min.js
└── lib
└── antd
├── antd.min.css
└── antd.min.css.map
其中 server.js
为服务端的脚本,部署时使用 Node 运行该脚本。为了保证服务可靠性,需要使用 pm2(或者 forever)这样的工具。
配置 PM2
在使用 pm2 之前,先生成一份 pm2 的配置文件:
pm2 init
运行该命令会在项目根目录生成一个 ecosystem.config.js
文件,下面是我填写的项目配置:
module.exports = {
"apps" : [{
"name": "node-app",
"script": "server.js",
"instances":"max",
"exec_mode":"cluster",
}]
};
若要使用 pm2 将项目运行起来,执行 pm2 start ecosystem.config.js
即可。另外,pm2 支持多种形式的配置文件,除了这里我们使用的 ecosystem.config.js
文件,还支持 JSON 文件或者 YML 文件,只需要指定相应的配置文件名即可。
新建 package.deploy.json
这是我个人的一个习惯,为部署操作专门建立一个 package.deploy.json 文件,用来在构建镜像时安装依赖:
{
"name": "CHARLEY_REACT_SSR_KIT",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@babel/runtime": "^7.1.5",
"antd": "^3.10.7",
"axios": "^0.18.0",
"colors": "^1.3.2",
"cookie-parser": "^1.4.3",
"echarts": "^4.2.0-rc.2",
"express": "^4.16.4",
"glob": "^7.1.3",
"html-minifier": "^3.5.21",
"isomorphic-style-loader": "^4.0.0",
"js-cookie": "^2.2.0",
"lodash": "^4.17.11",
"moment": "^2.18.1",
"prop-types": "^15.6.2",
"qs": "^6.5.2",
"react": "^16.6.0",
"react-dom": "^16.6.0",
"react-redux": "^5.1.0",
"react-router-config": "^4.4.0-beta.6",
"react-router-dom": "^4.3.1",
"redux": "^4.0.1",
"redux-thunk": "^2.3.0"
}
}
如果觉得每次都需要手动的从 package.json
文件中复制依赖很麻烦,一不小心忘记复制或者复制错误了还会导致后面构建镜像或者容器运行的过程出错,我们可以将创建 package.deploy.json
的过程写到脚本中。在 script
目录中新建一个 deploy.js
:
const fs = require("fs");
const packageJson = require("../package.json");
const {
name,
version,
description,
main,
keywords,
author,
license,
dependencies
} = packageJson;
const deployPackageJson = {
name,
version,
description,
main,
keywords,
author,
license,
dependencies
}
fs.writeFileSync("package.deploy.json",JSON.stringify(deployPackageJson,null,2));
在每次执行构建前,运行该脚本即可:
node script/deploy.js
创建 wwwroot 目录
在项目根目录创建一个 wwwroot 或者任何你喜欢的其他目录,这个目录是作为构建 Docker 镜像时的根目录。在该目录中创建一个 Dockerfile
,用来构建镜像:
FROM node
RUN mkdir node-app
COPY . node-app
WORKDIR node-app
EXPOSE 9527
RUN npm config set registry "https://registry.npm.taobao.org"
RUN npm install pm2 -g
RUN npm install
WORKDIR build
CMD [ "pm2", "start", "../ecosystem.config.js","--no-daemon"]
在执行构建前,需要将项目打包的 build
目录,以及前面新建的 ecosystem.config.js
和 package.deploy.json
拷贝到 wwwroot 目录中,然后将 package.deploy.json
改名为 package.json
:
cp -rf ./build ecosystem.config.js package.deploy.json ./wwwroot
cd ./wwwroot
mv package.deploy.json package.json
这里有个重点,PM2 默认情况下会在后台运行,当在我们在 Docker 中使用 PM2 的时候,就不能让 PM2 在后台运行,否则容器会在 CMD 命令运行后立即退出。
配置 Jenkins 脚本
接下来配置 Jenkins 的执行脚本,用来进行项目的依赖安装,打包,镜像构建和运行工作。
echo "Shell Step 01 ========> deploy starting···"
echo "Shell Step 02 ========> npm build···"
npm install && npm run build:deploy
node script/deploy.js
echo "Shell Step 03 ========> copy files···"
cp -rf ./build ecosystem.config.js package.deploy.json ./wwwroot
cd wwwroot
mv package.deploy.json package.json
echo "Shell Step 04 ========> docker build···"
(sudo docker rm -f charley-node-app && sudo docker rmi -f charley-node-pm2:1.0)|| echo "continue execute"
docker build -t charley-node-pm2:1.0 .
echo "Shell Step 05 ========> docker run···"
sudo docker run -d -p 10011:9527 --privileged --restart=always --name charley-node-app charley-node-pm2:1.0
echo "Shell Step 06 ========> docker tag···"
sudo docker tag charley-node-pm2:1.0 192.168.0.230:5000/charley-node-pm2:1.0
echo "Shell Step 07 ========> docker push···"
sudo docker push 192.168.0.230:5000/charley-node-pm2:1.0 && sudo docker rmi 192.168.0.230:5000/charley-node-pm2:1.0
echo "Shell Step 08 ========> deploy end."
这里需要解释下第四步(Shell Step 04)的操作:每次使用 Jenkins 构建时,需要构建新的镜像,这就需要删除之前的镜像和容器,然后再构建新的镜像。但在首次构建的时候,服务器上并没有相关的镜像和容器,删除镜像和容器会报错,因此这里使用 echo "continue execute"
输出一点内容,防止因为首次构建镜像和容器不存在时发生异常。
第六步(Shell Step 06)和第七步(Shell Step 07)的操作是将镜像推送到 K8S 中。
相关目录结构
项目的相关目录结构如下(只列出与部署相关的部分):
.
├── README.md
├── build
│ ├── client
│ │ ├── bundle.6faef.js
│ │ ├── bundle.6faef.js.map
│ │ ├── font
│ │ │ ├── 4123775d92a338f6864d80b112d33f44.ttf
│ │ │ └── 4470b313b33648e3ba3daa18c9fbc087.eot
│ │ └── img
│ │ └── iconfont.svg
│ ├── font
│ │ ├── 4123775d92a338f6864d80b112d33f44.ttf
│ │ └── 4470b313b33648e3ba3daa18c9fbc087.eot
│ ├── img
│ │ └── iconfont.svg
│ ├── manifest.json
│ ├── server.js
│ └── static
│ ├── favicon.ico
│ ├── image
│ │ ├── 404.png
│ ├── js
│ │ ├── help.js
│ │ └── qrcode.min.js
│ └── lib
│ └── antd
│ ├── antd.min.css
│ └── antd.min.css.map
├── ecosystem.config.js
├── package.deploy.json
├── package.json
├── script
│ └── deploy.js
└── wwwroot
└── Dockerfile
完。