当前位置: 首页 > news >正文

基于Jenkins+Docker的自动化部署实践——整合Git与Python脚本实现远程部署

环境说明:

  • Ubuntu:v24.04.1 LTS
  • Jekins:v2.491
  • Docker:v27.4.0
  • Gogs:v0.14.0 - 可选。可以选择Github,Gitlab或者Gitea等Git仓库,不限仓库类型
  • 1Panel: v1.10.21-lts - 可选。这里主要用于查看和管理Docker容器
  • 项目使用代码:https://github.com/VinciYan/docker_net8_webapi_fortran.git

Jenkins实现参数化构建

这里通过Docker进行安装

【系统管理】【插件管理】,安装“Publish Over SSH”

【系统管理】【系统配置】,配置“SSH Servers”

在这里插入图片描述

在这里插入图片描述

填写配置信息后点击“Test Configuration​”,显示“Success”说明配置成功,保存配置

新建任务

新建“test”任务,选择“流水线”

在这里插入图片描述

在这里插入图片描述

脚本编写如下:

pipeline {agent anyparameters {string(name: 'GIT_REPO_URL', description: 'Git仓库地址', trim: true)string(name: 'BRANCH', defaultValue: 'main', description: '分支名称', trim: true)choice(name: 'DEPLOY_ENV', choices: ['deploy'], description: '部署环境')  // 改为 deploy 匹配 SSH 配置}environment {APP_NAME = 'myapp'IMAGE_TAG = "${BUILD_NUMBER}"REMOTE_DIR = '/opt'  // 修改为你配置的远程目录}stages {stage('拉取代码') {steps {git url: "${params.GIT_REPO_URL}", branch: "${params.BRANCH}"}}stage('远程构建和部署') {steps {script {sshPublisher(publishers: [sshPublisherDesc(configName: 'deploy',  // 修改为你配置的 Nameverbose: true,transfers: [sshTransfer(sourceFiles: "**/*",remoteDirectory: "${APP_NAME}-${BUILD_NUMBER}",execCommand: """cd ${REMOTE_DIR}/${APP_NAME}-${BUILD_NUMBER}# 构建和部署docker build -t ${APP_NAME}:${IMAGE_TAG} .docker stop ${APP_NAME} || truedocker rm ${APP_NAME} || truedocker run -d --name ${APP_NAME} \-p 8880:5000 \--restart unless-stopped \${APP_NAME}:${IMAGE_TAG}# 清理cd ..rm -rf ${APP_NAME}-${BUILD_NUMBER}docker system prune -f""")])])}}}stage('健康检查') {steps {sleep 15sshPublisher(publishers: [sshPublisherDesc(configName: 'deploy',transfers: [sshTransfer(execCommand: '''max_attempts=5attempt=1while [ $attempt -le $max_attempts ]; doresponse=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8880/health)if [ "$response" = "200" ]; thenecho "Health check succeeded"exit 0fiecho "Attempt $attempt failed, waiting... (Status code: $response)"sleep 10attempt=$((attempt + 1))doneecho "Health check failed after $max_attempts attempts"exit 1''')])])}}}post {success {echo "部署成功: ${APP_NAME}:${IMAGE_TAG}"}failure {echo "部署失败"}cleanup {cleanWs()}}
}

打开【Build with Parameters】填写Git仓库地址和仓库分支,点击【Build】

在这里插入图片描述

打开【Console Output】查看构建日志

在这里插入图片描述

日志显示如下,代表构建完成

...
[Pipeline] step
SSH: Connecting from host [94e37d92d688]
SSH: Connecting with configuration [deploy] ...
SSH: EXEC: completed after 200 ms
SSH: Disconnecting configuration [deploy] ...
SSH: Transferred 0 file(s)
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (Declarative: Post Actions)
[Pipeline] echo
部署成功: myapp:27
[Pipeline] cleanWs
[WS-CLEANUP] Deleting project workspace...
[WS-CLEANUP] Deferred wipeout is used...
[WS-CLEANUP] done
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // withEnv
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS

这时在部署的主机上可以看到自动化部署的Docker容器

在这里插入图片描述

健康检查

由于在项目docker_net8_webapi_fortran中已经实现/health接口,所以在流水线中健康检查逻辑如下,这里支持重试机制

stage('健康检查') {steps {sleep 15sshPublisher(publishers: [sshPublisherDesc(configName: 'deploy',transfers: [sshTransfer(execCommand: '''max_attempts=5attempt=1while [ $attempt -le $max_attempts ]; doresponse=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8880/health)if [ "$response" = "200" ]; thenecho "Health check succeeded"exit 0fiecho "Attempt $attempt failed, waiting... (Status code: $response)"sleep 10attempt=$((attempt + 1))doneecho "Health check failed after $max_attempts attempts"exit 1''')])])}
}

参数化配置Python脚本实现远程部署

刚刚我们通过手动填写参数的方式完成项目自动化构建,进一步,可以使用python脚本远程进行触发构建过程,并且实现参数化配置

import requests
import json
import time
import yaml
import logging
import urllib3
from typing import Optional, Dict, Any
from urllib.parse import quote
from tenacity import retry, stop_after_attempt, wait_exponential# 配置日志
logging.basicConfig(level=logging.INFO,format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',handlers=[logging.FileHandler('deployment.log'),logging.StreamHandler()]
)
logger = logging.getLogger('JenkinsDeployer')class Config:def __init__(self, config_file: str = "config.yaml"):with open(config_file, 'r', encoding='utf-8') as f:self.config = yaml.safe_load(f)# Jenkins配置self.jenkins = self.config['jenkins']self.deployment = self.config['deployment']class JenkinsDeployer:def __init__(self, config: Config):self.config = configself.JENKINS_URL = config.jenkins['url']self.USER = config.jenkins['user']self.API_TOKEN = config.jenkins['token']self.JOB_NAME = config.jenkins['job_name']@retry(stop=stop_after_attempt(3),wait=wait_exponential(multiplier=1, min=4, max=10))def _make_request(self, method: str, url: str, **kwargs) -> requests.Response:"""通用的请求方法,带重试机制"""logger.info(f"发送 {method} 请求到 {url}")response = requests.request(method,url,auth=(self.USER, self.API_TOKEN),timeout=30,verify=False,**kwargs)response.raise_for_status()return responsedef trigger_deploy(self, git_repo_url: str, branch: str = "main", deploy_env: str = "deploy",wait: bool = True) -> Dict[str, Any]:"""触发Jenkins部署"""try:# 参数不进行 URL 编码params = {"GIT_REPO_URL": git_repo_url,"BRANCH": branch, "DEPLOY_ENV": deploy_env}# 构建 URLurl = f"{self.JENKINS_URL}/job/{self.JOB_NAME}/buildWithParameters"logger.info(f"触发构建: {url}")logger.info(f"参数: {json.dumps(params, indent=2, ensure_ascii=False)}")# 发送请求response = self._make_request('POST', url, params=params)if response.status_code == 201:queue_url = response.headers.get('Location')if not queue_url:return {"error": "未获取到队列URL"}logger.info(f"构建队列 URL: {queue_url}")build_number = self._get_build_number(queue_url)if build_number:logger.info(f"部署已触发,构建号: {build_number}")if wait:return self.wait_for_completion(build_number)return {"build_number": build_number, "status": "STARTED"}else:return {"error": "未能获取构建号"}return {"error": f"触发失败: {response.status_code}","details": response.text}except Exception as e:logger.error(f"部署触发失败: {str(e)}", exc_info=True)return {"error": f"部署触发失败: {str(e)}"}def _get_build_number(self, queue_url: str) -> Optional[int]:"""获取构建号"""max_attempts = 10for attempt in range(max_attempts):try:response = self._make_request('GET', f"{queue_url}api/json")logger.info(f"尝试获取构建号 ({attempt + 1}/{max_attempts})")logger.debug(f"队列响应: {response.text}")data = response.json()if "executable" in data and "number" in data["executable"]:return data["executable"]["number"]elif "why" in data:logger.info(f"构建等待中: {data['why']}")time.sleep(2)except Exception as e:logger.error(f"获取构建号失败 ({attempt + 1}/{max_attempts}): {e}")return Nonedef get_build_status(self, build_number: int) -> Dict[str, Any]:"""获取构建状态"""url = f"{self.JENKINS_URL}/job/{self.JOB_NAME}/{build_number}/api/json"try:response = self._make_request('GET', url)build_info = response.json()return {"number": build_info["number"],"result": build_info.get("result", "IN_PROGRESS"),"url": build_info["url"],"duration": build_info["duration"],"timestamp": build_info["timestamp"]}except Exception as e:logger.error(f"获取构建状态失败: {e}", exc_info=True)return {"error": f"获取状态失败: {str(e)}"}def wait_for_completion(self, build_number: int, timeout: int = 300) -> Dict[str, Any]:"""等待部署完成"""start_time = time.time()while time.time() - start_time < timeout:status = self.get_build_status(build_number)if "error" in status:return statusif status["result"] and status["result"] != "IN_PROGRESS":return statuslogger.info(f"部署进行中... ({int(time.time() - start_time)}s)")time.sleep(10)return {"error": "部署超时"}def check_deployment_health(self, host: str, port: int, max_attempts: int = 5) -> bool:"""检查部署的应用是否健康"""health_url = f"http://{host}:{port}/health"for attempt in range(max_attempts):try:response = requests.get(health_url, timeout=5)if response.status_code == 200:logger.info(f"健康检查成功 (尝试 {attempt + 1}/{max_attempts})")return Trueexcept Exception as e:logger.warning(f"健康检查失败 (尝试 {attempt + 1}/{max_attempts}): {e}")if attempt < max_attempts - 1:time.sleep(10)return Falsedef main():# 禁用 SSL 警告urllib3.disable_warnings()try:# 初始化配置config = Config("config.yaml")deployer = JenkinsDeployer(config)# 触发部署result = deployer.trigger_deploy(git_repo_url=config.deployment['git_repo'],branch=config.deployment['branch'],deploy_env="deploy",wait=True)if "error" in result:logger.error(f"部署失败: {result['error']}")return# 执行健康检查health_config = config.deployment['health_check']is_healthy = deployer.check_deployment_health(host=health_config['host'],port=health_config['port'],max_attempts=health_config['max_attempts'])if is_healthy:logger.info("部署成功且应用程序运行正常")else:logger.warning("部署可能成功但健康检查失败")except Exception as e:logger.error(f"部署过程出错: {str(e)}", exc_info=True)if __name__ == "__main__":main()

config.yaml配置文件如下:

jenkins:url: "http://192.168.1.140:8563"user: "admin"token: "1144e4584a109badf5051a42a960aef11d"job_name: "test"deployment:git_repo: "http://192.168.1.140:10880/root/docker_net8_webapi_fortran.git"branch: "master"health_check:host: "192.168.1.140"port: 8880max_attempts: 5

运行python脚本,日志结果如下:

$ python .\main.py
2024-12-24 10:22:06,538 - JenkinsDeployer - INFO - 触发构建: http://192.168.1.140:8563/job/test/buildWithParameters
2024-12-24 10:22:06,539 - JenkinsDeployer - INFO - 参数: {"GIT_REPO_URL": "http://192.168.1.140:10880/root/docker_net8_webapi_fortran.git","BRANCH": "master","DEPLOY_ENV": "deploy"
}
2024-12-24 10:22:06,539 - JenkinsDeployer - INFO - 发送 POST 请求到 http://192.168.1.140:8563/job/test/buildWithParameters
2024-12-24 10:22:06,636 - JenkinsDeployer - INFO - 构建队列 URL: http://192.168.1.140:8563/queue/item/62/
2024-12-24 10:22:06,637 - JenkinsDeployer - INFO - 发送 GET 请求到 http://192.168.1.140:8563/queue/item/62/api/json
2024-12-24 10:22:06,788 - JenkinsDeployer - INFO - 尝试获取构建号 (1/10)
2024-12-24 10:22:06,789 - JenkinsDeployer - INFO - 构建等待中: In the quiet period. Expires in 4.8 sec
2024-12-24 10:22:08,790 - JenkinsDeployer - INFO - 发送 GET 请求到 http://192.168.1.140:8563/queue/item/62/api/json
2024-12-24 10:22:08,921 - JenkinsDeployer - INFO - 尝试获取构建号 (2/10)
2024-12-24 10:22:08,921 - JenkinsDeployer - INFO - 构建等待中: In the quiet period. Expires in 2.7 sec
2024-12-24 10:22:10,929 - JenkinsDeployer - INFO - 发送 GET 请求到 http://192.168.1.140:8563/queue/item/62/api/json
2024-12-24 10:22:11,029 - JenkinsDeployer - INFO - 尝试获取构建号 (3/10)
2024-12-24 10:22:11,029 - JenkinsDeployer - INFO - 构建等待中: In the quiet period. Expires in 0.6 sec
2024-12-24 10:22:13,032 - JenkinsDeployer - INFO - 发送 GET 请求到 http://192.168.1.140:8563/queue/item/62/api/json
2024-12-24 10:22:13,207 - JenkinsDeployer - INFO - 尝试获取构建号 (4/10)
2024-12-24 10:22:13,207 - JenkinsDeployer - INFO - 部署已触发,构建号: 26
2024-12-24 10:22:13,208 - JenkinsDeployer - INFO - 发送 GET 请求到 http://192.168.1.140:8563/job/test/26/api/json
2024-12-24 10:22:13,357 - JenkinsDeployer - INFO - 部署进行中... (0s)
2024-12-24 10:22:23,364 - JenkinsDeployer - INFO - 发送 GET 请求到 http://192.168.1.140:8563/job/test/26/api/json
2024-12-24 10:22:23,640 - JenkinsDeployer - INFO - 部署进行中... (10s)
2024-12-24 10:22:33,644 - JenkinsDeployer - INFO - 发送 GET 请求到 http://192.168.1.140:8563/job/test/26/api/json
2024-12-24 10:22:33,907 - JenkinsDeployer - INFO - 健康检查成功 (尝试 1/5)
2024-12-24 10:22:33,908 - JenkinsDeployer - INFO - 部署成功且应用程序运行正常

定义更多参数

比如定义新增容器名称和暴露端口

在这里插入图片描述

Jenkins脚本:

pipeline {agent anyparameters {string(name: 'GIT_REPO_URL', description: 'Git仓库地址', trim: true)string(name: 'BRANCH', defaultValue: 'main', description: '分支名称', trim: true)string(name: 'APP_NAME', defaultValue: 'myapp', description: '应用名称', trim: true)string(name: 'PORT', defaultValue: '8880', description: '对外暴露端口', trim: true)choice(name: 'DEPLOY_ENV', choices: ['deploy'], description: '部署环境')  // 改为 deploy 匹配 SSH 配置}environment {IMAGE_TAG = "${BUILD_NUMBER}"REMOTE_DIR = '/opt'  // 修改为你配置的远程目录}stages {stage('拉取代码') {steps {git url: "${params.GIT_REPO_URL}", branch: "${params.BRANCH}"}}stage('远程构建和部署') {steps {script {sshPublisher(publishers: [sshPublisherDesc(configName: 'deploy',  // 修改为你配置的 Nameverbose: true,transfers: [sshTransfer(sourceFiles: "**/*",remoteDirectory: "${params.APP_NAME}-${BUILD_NUMBER}",execCommand: """cd ${REMOTE_DIR}/${params.APP_NAME}-${BUILD_NUMBER}# 构建和部署docker build -t ${params.APP_NAME}:${IMAGE_TAG} .docker stop ${params.APP_NAME} || truedocker rm ${params.APP_NAME} || truedocker run -d --name ${params.APP_NAME} \-p ${params.PORT}:5000 \--restart unless-stopped \${params.APP_NAME}:${IMAGE_TAG}# 清理cd ..rm -rf ${params.APP_NAME}-${BUILD_NUMBER}docker system prune -f""")])])}}}stage('健康检查') {steps {sleep 15sshPublisher(publishers: [sshPublisherDesc(configName: 'deploy',transfers: [sshTransfer(execCommand: """max_attempts=5attempt=1while [ \$attempt -le \$max_attempts ]; doresponse=\$(curl -s -o /dev/null -w "%{http_code}" http://localhost:${params.PORT}/health)if [ "\$response" = "200" ]; thenecho "Health check succeeded"exit 0fiecho "Attempt \$attempt failed, waiting... (Status code: \$response)"sleep 10attempt=\$((attempt + 1))doneecho "Health check failed after \$max_attempts attempts"exit 1""")])])}}}post {success {echo "部署成功: ${params.APP_NAME}:${IMAGE_TAG}"}failure {echo "部署失败"}cleanup {cleanWs()}}
}

相应的修改python脚本:

import requests
import json
import time
import yaml
import logging
import urllib3
from typing import Optional, Dict, Any
from urllib.parse import quote
from tenacity import retry, stop_after_attempt, wait_exponential# 配置日志
logging.basicConfig(level=logging.INFO,format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',handlers=[logging.FileHandler('deployment.log'),logging.StreamHandler()]
)
logger = logging.getLogger('JenkinsDeployer')class Config:def __init__(self, config_file: str = "config.yaml"):with open(config_file, 'r', encoding='utf-8') as f:self.config = yaml.safe_load(f)# Jenkins配置self.jenkins = self.config['jenkins']self.deployment = self.config['deployment']class JenkinsDeployer:def __init__(self, config: Config):self.config = configself.JENKINS_URL = config.jenkins['url']self.USER = config.jenkins['user']self.API_TOKEN = config.jenkins['token']self.JOB_NAME = config.jenkins['job_name']@retry(stop=stop_after_attempt(3),wait=wait_exponential(multiplier=1, min=4, max=10))def _make_request(self, method: str, url: str, **kwargs) -> requests.Response:"""通用的请求方法,带重试机制"""logger.info(f"发送 {method} 请求到 {url}")response = requests.request(method,url,auth=(self.USER, self.API_TOKEN),timeout=30,verify=False,**kwargs)response.raise_for_status()return responsedef trigger_deploy(self, git_repo_url: str, branch: str = "main",app_name: str = "myapp",    # 新增参数port: str = "8880",         # 新增参数 deploy_env: str = "deploy",wait: bool = True) -> Dict[str, Any]:"""触发Jenkins部署"""try:# 参数不进行 URL 编码params = {"GIT_REPO_URL": git_repo_url,"BRANCH": branch,"APP_NAME": app_name,    # 新增参数"PORT": port,            # 新增参数"DEPLOY_ENV": deploy_env}# 构建 URLurl = f"{self.JENKINS_URL}/job/{self.JOB_NAME}/buildWithParameters"logger.info(f"触发构建: {url}")logger.info(f"参数: {json.dumps(params, indent=2, ensure_ascii=False)}")# 发送请求response = self._make_request('POST', url, params=params)if response.status_code == 201:queue_url = response.headers.get('Location')if not queue_url:return {"error": "未获取到队列URL"}logger.info(f"构建队列 URL: {queue_url}")build_number = self._get_build_number(queue_url)if build_number:logger.info(f"部署已触发,构建号: {build_number}")if wait:return self.wait_for_completion(build_number)return {"build_number": build_number, "status": "STARTED"}else:return {"error": "未能获取构建号"}return {"error": f"触发失败: {response.status_code}","details": response.text}except Exception as e:logger.error(f"部署触发失败: {str(e)}", exc_info=True)return {"error": f"部署触发失败: {str(e)}"}def _get_build_number(self, queue_url: str) -> Optional[int]:"""获取构建号"""max_attempts = 10for attempt in range(max_attempts):try:response = self._make_request('GET', f"{queue_url}api/json")logger.info(f"尝试获取构建号 ({attempt + 1}/{max_attempts})")logger.debug(f"队列响应: {response.text}")data = response.json()if "executable" in data and "number" in data["executable"]:return data["executable"]["number"]elif "why" in data:logger.info(f"构建等待中: {data['why']}")time.sleep(2)except Exception as e:logger.error(f"获取构建号失败 ({attempt + 1}/{max_attempts}): {e}")return Nonedef get_build_status(self, build_number: int) -> Dict[str, Any]:"""获取构建状态"""url = f"{self.JENKINS_URL}/job/{self.JOB_NAME}/{build_number}/api/json"try:response = self._make_request('GET', url)build_info = response.json()return {"number": build_info["number"],"result": build_info.get("result", "IN_PROGRESS"),"url": build_info["url"],"duration": build_info["duration"],"timestamp": build_info["timestamp"]}except Exception as e:logger.error(f"获取构建状态失败: {e}", exc_info=True)return {"error": f"获取状态失败: {str(e)}"}def wait_for_completion(self, build_number: int, timeout: int = 300) -> Dict[str, Any]:"""等待部署完成"""start_time = time.time()while time.time() - start_time < timeout:status = self.get_build_status(build_number)if "error" in status:return statusif status["result"] and status["result"] != "IN_PROGRESS":return statuslogger.info(f"部署进行中... ({int(time.time() - start_time)}s)")time.sleep(10)return {"error": "部署超时"}def check_deployment_health(self, host: str, port: int, max_attempts: int = 5) -> bool:"""检查部署的应用是否健康"""health_url = f"http://{host}:{port}/health"for attempt in range(max_attempts):try:response = requests.get(health_url, timeout=5)if response.status_code == 200:logger.info(f"健康检查成功 (尝试 {attempt + 1}/{max_attempts})")return Trueexcept Exception as e:logger.warning(f"健康检查失败 (尝试 {attempt + 1}/{max_attempts}): {e}")if attempt < max_attempts - 1:time.sleep(10)return Falsedef main():# 禁用 SSL 警告urllib3.disable_warnings()try:# 初始化配置config = Config("config.yaml")deployer = JenkinsDeployer(config)# 触发部署result = deployer.trigger_deploy(git_repo_url=config.deployment['git_repo'],branch=config.deployment['branch'],app_name=config.deployment['app_name'],  # 新增参数port=config.deployment['port'],          # 新增参数deploy_env="deploy",wait=True)if "error" in result:logger.error(f"部署失败: {result['error']}")return# 执行健康检查health_config = config.deployment['health_check']is_healthy = deployer.check_deployment_health(host=health_config['host'],port=int(config.deployment['port']),  # 使用部署配置的端口max_attempts=health_config['max_attempts'])if is_healthy:logger.info("部署成功且应用程序运行正常")else:logger.warning("部署可能成功但健康检查失败")except Exception as e:logger.error(f"部署过程出错: {str(e)}", exc_info=True)if __name__ == "__main__":main()

config.yaml

jenkins:url: "http://192.168.1.140:8563"user: "admin"token: "1144e4584a109badf5051a42a960aef11d"job_name: "test"deployment:git_repo: "http://192.168.1.140:10880/root/docker_net8_webapi_fortran.git"branch: "master"app_name: "550e8400e29b41d4a716446655440000"  # 新增应用名称配置port: "9085"       # 新增端口配置health_check:host: "192.168.1.140"port: 9085max_attempts: 5

参考

  • jenkins 通过 SSH 远程部署_jenkins ssh 远程部署-CSDN博客​
  • https://github.com/VinciYan/docker_net8_webapi_fortran.git

http://www.mrgr.cn/news/81458.html

相关文章:

  • 贝叶斯分类——数学模型
  • 子Shell及Shell嵌套模式
  • 学习记录2024/12/25;用C语言实现通讯录功能
  • 提升翻译质量的秘密武器:硬提示与软提示微调解析(二)
  • LabVIEW声音信号处理系统
  • Java重要面试名词整理(四):并发编程(下)
  • 大模型+安全实践之春天何时到来?
  • Linux应用软件编程-多任务处理(进程)
  • 深度学习笔记2:使用pytorch构建神经网络
  • 第3章 集合与关系
  • ubuntu20.04 调试bcache源码
  • 【ES6复习笔记】生成器(11)
  • Excel生成DBC脚本源文件
  • 【EtherCATBasics】- KRTS C++示例精讲(2)
  • 【汇编】关于函数调用过程的若干问题
  • ubuntu22.04上安装win10虚拟机,并采用noVNC+frp,让远程通过web访问桌面
  • pip离线批量安装时报错No matching distribution found for【解决方案】
  • 【ES6复习笔记】箭头函数(5)
  • vulnhub靶场(Os-hacknos-3)
  • 【ES6复习笔记】模板字符串(3)
  • 【C++】设计模式
  • FreeSql
  • 【Rust自学】7.1. Package、Crate和定义Module
  • 【ES6复习笔记】函数参数的默认值(6)
  • 【Rust自学】6.4. 简单的控制流-if let
  • 【ES6复习笔记】let 和 const 命令(1)