jenkins流水线+k8s部署springcloud微服务架构项目

文章目录

  • 1.k8s安装
  • 2.jenkins安装
  • 3.k8s重要知识
    • 1.简介
    • 2.核心概念
    • 3.重要命令
      • 1.查看集群消息
      • 2.命名空间
      • 3.资源创建/更新
      • 4.资源查看
      • 5.描述某个资源的详细信息
      • 6.资源编辑
      • 7.资源删除
      • 8.资源重启
      • 9.查看资源日志
      • 10.资源标签
  • 4.k8s控制台
    • 1.登录
    • 2.界面基本操作
      • 1.选择命名空间
      • 2.查看命名空间下的资源(Pods,Deployments,Services,Replica Sets)
      • 3.Pods操作
      • 4.Deployments操作
      • 5.Service操作
  • 5.k8s部署springcloud项目实例
    • 1.环境准备
    • 2.部署到k8s
  • 6.结合jenkins流水线部署springcloud到k8s实例
    • 1.环境准备
    • 2.编写Pipeline流水线脚本--Jenkinsfile
      • 1.参数化构建
      • 2.流水线脚本
        • 1.完整脚本示例
        • 2.具体步骤讲述

1.k8s安装

参考: CentOS 离线环境下搭建 Kubernetes 集群 - 知乎 (zhihu.com)

相关文件下载

链接: https://pan.baidu.com/s/1bzyafpopc-PinhSFTGryHw?pwd=xm3s 提取码: xm3s

2.jenkins安装

可参考我之前的博客:

centos7系统使用docker-compose安装部署jenkins_centos jenkins-CSDN博客

3.k8s重要知识

参考:

计算机 容器 k8s 容器和k8s_mob64ca14133dc6的技术博客_51CTO博客

图解 K8s 核心概念和术语 - 后端进阶 - 博客园 (cnblogs.com)

K8S系列一:概念入门 - 知乎 (zhihu.com)

1.简介

k8s全称Kubernetes,源于希腊语“κυβερνήτης”,意为“舵手”或“飞行员”。借助容器container的另一英文释义“集装箱”,k8s可想象为一艘航行在海面上的轮船,把装载的大量集装箱(容器)送到该去的地方,管理并发挥它们的最大效力。

k8s是比容器更上一层的架构,作为一个编排和管理容器的工具,它主要具备以下三大核心能力:自动调度、自动修复、水平自动伸缩。

  • 自动调度

    k8s可以把用户提交的容器放到k8s集群的任意一个节点,但运行过程中k8s会根据容器大小、规格,各节点的负载程度等实际情况来决定容器应该放到哪一个节点上。

  • 自动修复

    k8s有一个健康检查机制,会监测集群中所有的宿主机,当它发现其中有问题,比如某一个服务器挂了,它可以自动将这个服务器上的服务调度到另外一台主机上进行运行,无需人工干涉。

  • 水平自动伸缩

    k8s还具备业务负载检查的能力,会监测业务上所承担的负载,当达到预期设定的负载指标后,就会根据指标自动触发动态扩容或缩容行为。

2.核心概念

  • 节点

    在这里插入图片描述

    上图是一个典型的k8s架构图

    1. Master节点

      Master 节点也称为控制节点,每个 k8s 集群都有一个 Master 节点负责整个集群的管理控制,我们上面介绍的 k8s 三大能力都是经过 Master 节点发起的,Master 节点包含了以下几个组件:

      在这里插入图片描述

      • API Server:提供了 HTTP Rest 接口的服务进程,所有资源对象的增、删、改、查等操作的唯一入口;
      • Controller Manager:k8s 集群所有资源对象的自动化控制中心;
      • Scheduler:k8s 集群所有资源对象自动化调度控制中心;
      • ETCD:k8s 集群注册服务发现中心,可以保存 k8s 集群中所有资源对象的数据。
    2. Node节点

      Node 节点的作用是承接 Master 分配的工作负载,节点可以是一个虚拟机或者物理机器,取决于所在的集群配置,它主要有以下几个关键组件:

      在这里插入图片描述

      • kubelet:负责 Pod 对应容器的创建、启停等操作,与 Master 节点紧密协作;
      • kube-porxy:实现 k8s 集群通信与负载均衡的组件。

      从图上可看出,在 Node 节点上面,还需要一个容器运行环境,如果使用 Docker 技术栈,则还需要在 Node 节点上面安装 Docker Engine,专门负责该节点容器管理工作。

  • Pods

    K8s中最小的部署单元,一个Pod可以包含一个或多个紧密相关的容器。

    组成pod的所有容器都运行在同一台机器上,不能跨多个节点拆分。

    在这里插入图片描述

    如上是Pod的结构图,从结构图可以看出,它其实是容器的一个上层包装结构,这也就是为什么 K8s 可以支持多种容器类型的原因。

    Pod 可包含多个容器在里面,每个 Pod 至少会有一个 Pause 容器,其它用户定义的容器都共享该 Pause 容器,Pause 容器的主要作用是用于定义 Pod 的 ip 和 volume。

    Pods在k8s集群中的位置如下图所示:

    在这里插入图片描述

  • Deployment

    Deployment的作用是管理和控制Pod和ReplicaSet,管控它们运行在用户期望的状态中。哎,打个形象的比喻,Deployment就是包工头,主要负责监督底下的工人Pod干活,确保每时每刻有用户要求数量的Pod在工作。如果一旦发现某个工人Pod不行了,就赶紧新拉一个Pod过来替换它。

    ReplicaSet 的目的是维护一组在任何时候都处于运行状态的 Pod 副本的稳定集合。 因此,它通常用来保证给定数量的、完全相同的 Pod 的可用性。

    ReplicaSet的作用就是管理和控制Pod,管控他们好好干活。但是,ReplicaSet受控于Deployment。形象来说,ReplicaSet就是总包工头手下的小包工头

  • Service

    Kubernetes 中 Service 是 将运行在一个或一组 Pod 上的网络应用程序公开为网络服务的方法。

    Service定义了Pod的逻辑集合和访问该集合的策略,是真实服务的抽象。Service提供了一个统一的服务访问入口以及服务代理和发现机制,关联多个相同Label的Pod,用户不需要了解后台Pod是如何运行。类似于网关的作用。

  • Labels

    标签 Label 在 k8s 中是一个非常核心的概念,我们可以将 Label 指定到对应的资源对象中,例如 Node、Pod、Deployment、Replica Set、Service 等,一个资源可以绑定任意个 Label,k8s 通过 Label 可实现多维度的资源分组管理,后续可通过 Label Selector 查询和筛选拥有某些 Label 的资源对象,例如创建一个 Pod,给定一个 Label,workerid=123,后续可通过 workerid=123 删除拥有该标签的 Pod 资源。

  • Node

    Node是Pod真正运行的主机,可以物理机,也可以是虚拟机。为了管理Pod,每个Node节点上至少要运行container runtime(比如docker或者rkt)、kubelet和kube-proxy服务。

  • Namespace

    Namespace 顾名思义是命名空间的意思,在 k8s 中主要用于实现资源隔离的目的,用户可根据不同项目创建不同的 Namespace,通过 k8s 将资源分配到不同 Namespace 中,即可实现不同项目的资源隔离

3.重要命令

参考:k8s常用命令大全-持续更新中(欢迎补充)-CSDN博客

k8s基本操作命令-CSDN博客

1.查看集群消息

kubectl cluster-info

在这里插入图片描述

这里的cluster-endpoint是master主机ip,由于配置了/etc/hosts文件,如下图,所以可以用配置的域名代替

在这里插入图片描述

2.命名空间

  • 查看命名空间

    kubectl get namespace
    #缩写
    kubectl get ns
    

    在这里插入图片描述

  • 查看命名空间的所有资源

    kubectl get all [-n default]
    

    -n:指定命名空间,默认default

    在这里插入图片描述

  • 创建命名空间

    kubectl create ns [名称]
    
  • 删除命名空间

    kubectl delete namespace [名称]
    

3.资源创建/更新

  • 用于创建新资源。如果再次运行该命令,则会抛出错误;要求yaml文件的内容是完整的。

    kubectl create -f 文件名
    
  • 用于创建或更新一个资源,如果该资源不存在,则会创建;如果该资源已存在,则会对其进行更新;根据配置文件里面列出来的内容,升级现有的。所以yaml文件的内容可以只写需要升级的属性

    kubectl apply -f 文件名
    

4.资源查看

kubectl get [资源类型] [资源名] [选项]

kubectl get all  # 查看所有的资源信息
kubectl get ns                     # 获取名称空间
kubectl get cs                     # 获取集群健康状态(组件的状态)
kubectl get pods --all-namespaces     # 查看所有名称空间所有的pod
kubectl get pods -A                   # -A是--all-namespaces的简写哇

events:查看集群中的所有日志信息
-o wide : 显示资源详细信息,包括节点、地址…
-o yaml/json: 将当前资源对象输出至 yaml/json 格式文件
–show-labels: 查看当前资源对象的标签
-l key=value: 基于标签进行筛选查找
-A: 查看所有名称空间下的POD
-n 名字空间: 查看该名字下的资源集合
–show-labels: 查看某一资源的标签
-all-namespaces: 查看所有名字空间下的资源

在这里插入图片描述

5.描述某个资源的详细信息

kubectl describe [资源类型] [资源前缀] [选项]

-f: 文件名、目录或文件 URL 的列表,包含要描述的资源。

-n:命名空间

在这里插入图片描述

6.资源编辑

效果类似于kubectl apply -f [文件名],修改后会实时生效

kubectl edit (RESOURCE/NAME | -f FILENAME)

在这里插入图片描述

7.资源删除

通过文件名、标准输入、源名称或标签选择器来删除资源。

kubectl delete [资源类型] [资源名] [选项]

-f:文件名

–all:删除该资源类型下所有资源

kubectl delete pod <pod-name> --grace-period=0 --force
#--grace-period=0 参数表示立即删除 Pod,而不是等待一段时间。
#--force 参数表示强制删除 Pod,即使 Pod 处于未知状态。

8.资源重启

kubectl rollout restart [选项] [资源类型] [资源名]

例如对default命名空间下的名为spmp-camunda的deployment 进行滚动升级(保证一定数量的资源正常运行的同时,先起新的资源,再挂旧的资源的方式逐渐替换原资源)

kubectl rollout restart -n default deployment spmp-camunda

9.查看资源日志

kubectl logs [选项] [pod名] [容器名]

在这里插入图片描述

10.资源标签

  • 给资源打标签

    kubectl label [资源类型] [资源名] <label-key>=<label-value>
    

    举例,给名为node1的节点添加一个disktype=ssd的标签:

    kubectl label nodes node1 disktype=ssd
    
  • 如果要更新标签,可以添加--overwrite标志。例如:

    kubectl label nodes node1 disktype=ssd --overwrite
    
  • 查看资源标签

    kubectl get [资源类型] [资源名] --show-labels
    

    kubectl get nodes --show-labels
    
    kubectl get node node1 --show-labels
    
  • 查看带有特定标签的所有节点

    kubectl get nodes -l <label-key>=<label-value>
    

4.k8s控制台

1.登录

访问地址是:IP+端口

端口:

kubectl edit svc kubernetes-dashboard -n kubernetes-dashboard

在这里插入图片描述

这里就是上面的端口

注意:从K8S的1.19版本开始,已经取消了使用basic-auth-file参数进行用户名密码登录。在1.19之前,可以通过设置来进行用户名密码登录,而且账号和密码必须都是admin。见:https://github.com/kubernetes/kubernetes/pull/89069

在这里插入图片描述

在这里插入图片描述

本次使用的k8s版本是1.26.9,这里我们进入到控制台,可以看到只有两种登录认证方式,一种是token,一种是由Kubeconfig文件

(但这两种我试了都会过期,第二种稍微久一些)

  • token登录

    创建用户,绑定角色,设置永久token 参考:

    K8S系列(二)——安装k8s-dashboard图形化集群管理界面 - 标配的小号 - 博客园 (cnblogs.com)

    两种方法修改k8s dashboard token认证的过期时间_kubectl -n kubernetes-dashboard create token 有效期-CSDN博客

    创建token(修改参数为自己的):

    kubectl create token webui-cluster-admin -n kubernetes-dashboard
    

    在这里插入图片描述

  • Kubeconfig登录

    vim /root/.kube/config
    

    在最后补上刚刚上面生成的token,注意缩进

    在这里插入图片描述

    然后把这个文件复制出来,后面就可以使用这个文件进行登录

2.界面基本操作

1.选择命名空间

左上角下拉框选择

在这里插入图片描述

2.查看命名空间下的资源(Pods,Deployments,Services,Replica Sets)

如下图,点击左边菜单即可查看

在这里插入图片描述

3.Pods操作

  • 进入终端窗口,相当于kubectl exec -it <pod> bash

    在这里插入图片描述

    在这里插入图片描述

  • 还有其他操作,查看日志、修改配置(实时生效)、删除等

4.Deployments操作

  • 修改目标副本数量,如下图所示

    在这里插入图片描述

    在这里插入图片描述

  • 重启(滚动更新)

    在这里插入图片描述

    在这里插入图片描述

  • 其他操作

    同上

5.Service操作

在这里插入图片描述

同上

5.k8s部署springcloud项目实例

1.环境准备

  • 服务器:1台master节点服务器(简称主机),1台node节点服务器(简称从机)。两台机子均安装了docker、k8s,组成了k8s集群

  • springcloud项目中一个测试用的后端服务------这里名为 spmp-camunda

  • 原先spmp-camunda服务已经实现了docker部署,如下是Dockerfile文件

    # 基础镜像
    #FROM openjdk:8-jre
    FROM williamyeh/java8:latest
    MAINTAINER znak
    ENV LANG=zh_CN.UTF8
    ENV LC_ALL=zh_CN.UTF8
    
    ENV PARAMS=""
    
    ENV TZ=PRC
    #RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
    RUN /bin/cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone
    #ADD target/xxl-job-admin-*.jar /app.jar
    VOLUME /opt/app
    
    ADD ffmpeg/ffmpeg-git-amd64-static.tar /opt/app
    ADD ffmpeg/ffmpeg_deploy.sh /opt/app
    
    #
    RUN chmod +x /opt/app/ffmpeg_deploy.sh
    
    # FFmpeg部署脚本执行
    RUN sh /opt/app/ffmpeg_deploy.sh
    
    # 验证FFmpeg安装
    RUN ffmpeg -version
    
    #ARG profileActive=dev
    #ENV profileActive ${profileActive}
    
    ENTRYPOINT ["java", "-Xms1800M","-Xmx4096M","-Xmn256M","-Xss256K","-Duser.timezone=GMT+8", "-Dfile.encoding=UTF-8", "-Dsun.jnu.encoding=UTF-8", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/opt/app/app.jar"]
    

    主要注意

    在这里插入图片描述

    这是将容器内的/opt/app作为容器卷,这里是该项目的jar包存放的目录,也就是说,后续我们使用docker run启动挂载 /opt/app后(如下),以后更新jar包只需要替换宿主机对应目录的jar包,docker restart即可

    docker run -d --restart always --network=host --name spmp-camunda  -m 1024m -v /data/spmp/spmp-camunda:/opt/app -v /etc/hosts:/etc/hosts -d csms-dev:1.2
    
  • springcloud的其他组件,如nacos、gateway也都已经集成

2.部署到k8s

  • 在主机准备两个文件,deployment.yaml和service.yaml(名字可以自定义),分别用于创建deployment资源和service资源

    deployment.yaml

    apiVersion: apps/v1 # 指定api版本,此值必须在kubectl api-versions中。业务场景一般首选”apps/v1“
    kind: Deployment # 资源的类型   
    metadata: # 资源的元数据/属性 
      name: spmp-camunda # 资源的名字,在同一个namespace中必须唯一
      namespace: default 	# 部署在哪个namespace中。不指定时默认为default命名空间
      labels:  		# 自定义资源的标签
        app: spmp-camunda
    spec: # 资源规范字段,定义deployment资源需要的参数属性,诸如是否在容器失败时重新启动容器的属性
      # 声明副本数目
      replicas: 1	
      selector:	# 标签选择器
        matchLabels:	# 匹配标签,需与上面的标签定义的app保持一致
          app: spmp-camunda
      template: # 定义业务模板,如果有多个副本,所有副本的属性会按照模板的相关配置进行匹配
        metadata: # 资源的元数据/属性 
          labels: # 自定义资源的标签
            app: spmp-camunda
        spec: # 资源规范字段
          containers: # Pod中容器列表
          - name: spmp-camunda # 容器的名字
            image: csms-dev:1.2 # 容器使用的镜像地址,这里由于是直接本机镜像,所以直接用 名字:tag
            ports: # 需要暴露的端口库号列表
            # 容器开放对外的端口
            - containerPort: 20414 
            volumeMounts: # 挂载到容器内部的存储卷配置
            - name: app-volume # 引用pod定义的共享存储卷的名称,需用下面volumes[]部分定义的的卷名
              mountPath: /opt/app # 存储卷在容器内mount的绝对路径
            - name: hosts-volume
              mountPath: /etc/hosts
            resources: # 资源管理。资源限制和请求的设置
              limits: # 资源限制的设置,最大使用
                memory: "1024Mi" # 内存,1G = 1024Mi。将用于docker run --memory参数
          volumes: # 在该pod上定义共享存储卷列表
          - name: app-volume # 共享存储卷名称
            hostPath: # 类型为hostPath的存储卷,表示挂载Pod所在宿主机的目录
              path: /data/spmp/spmp-camunda # Pod所在宿主机的目录,将被用于同期中mount的目录
          - name: hosts-volume
            hostPath:
              path: /etc/hosts
          restartPolicy: Always # Pod的重启策略。[Always | OnFailure | Nerver]
          							# Always :在任何情况下,只要容器不在运行状态,就自动重启容器。默认
          							# OnFailure :只在容器异常时才自动容器容器。
          							  # 对于包含多个容器的pod,只有它里面所有的容器都进入异常状态后,pod才会进入Failed状态
          							# Nerver :从来不重启容器
          nodeSelector: # 节点选择器,也可换成affinity亲和性调试,具体的网上查资料即可
            kubernetes.io/hostname: k8s-node2 # 标签key: 标签value,这里用节点名标签选择到了从机服务器,即会将这个deployment部署到从机这台机子
    

    部署好deployment后,要将其作为一个服务暴露给外界访问,则还需要继续部署service

    service.yaml

    apiVersion: v1 # 指定api版本,此值必须在kubectl api-versions中 
    kind: Service # 资源的类型 
    metadata: # 资源的元数据/属性 
      name: spmp-camunda-service # 资源的名字,在同一个namespace中必须唯一
    spec: # 资源规范字段
      selector: # 选择器。选择具有指定label标签的pod作为管理范围
        app: spmp-camunda
      ports:
          # 服务监听的端口号
        - port: 20414 
          # 容器暴露的端口
          targetPort: 20414
          protocol: TCP # 端口协议,支持TCP或UDP,默认TCP
          # 当type=NodePort时,指定映射到物理机的端口号
          nodePort: 30010  
      type: NodePort # service的类型,指定service的访问方式,默认ClusterIP。
          # ClusterIP类型:虚拟的服务ip地址,用于k8s集群内部的pod访问,在Node上kube-porxy通过设置的iptables规则进行转发
          # NodePort类型:使用宿主机端口,能够访问各个Node的外部客户端通过Node的IP和端口就能访问服务器
          # LoadBalancer类型:使用外部负载均衡器完成到服务器的负载分发,需要在spec.status.loadBalancer字段指定外部负载均衡服务器的IP,并同时定义nodePort和clusterIP用于公有云环境。
    

    注:

    port和nodePort都是service的端口,前者暴露给k8s集群内部服务访问,后者暴露给k8s集群外部流量访问。从上两个端口过来的数据都需要经过反向代理kube-proxy,流入后端pod的targetPort上,最后到达pod内的容器。NodePort类型的service可供外部集群访问是因为service监听了宿主机上的端口,即监听了(所有节点)nodePort,该端口的请求会发送给service,service再经由负载均衡转发给Endpoints的节点。

    这里service的type使用的是NodePort,整体下来也就是deployment部署到一个机子,然后service也部署到同台机子,访问时候直接访问这台机子的ip+nodePort 即可

    如果是一个后端项目部署多个节点(集群)的模式,目前暂时的方案是,继续在其他机子也部署deployment和service,最后负载均衡再利用nginx配置一个统一域名转发多个机子来实现。如网上找到的相关图片:

    在这里插入图片描述

  • 部署命令

    kubectl apply -f deployment.yaml #创建或更新deployment
    kubectl apply -f service.yaml #创建或更新service
    kubectl rollout restart -n default deployment spmp-camunda #如果是更新jar包,则需要再走一步滚动更新,不然如果deployment.yaml没修改不会重新启动pods
    

6.结合jenkins流水线部署springcloud到k8s实例

1.环境准备

安装jenkins流水线相关插件(jenkins这里是安装在从机),如下图

在这里插入图片描述

新建 spmp-camunda 项目的构建任务,之前的博客centos7系统使用docker-compose安装部署jenkins_centos jenkins-CSDN博客 里讲了怎么建自由风格的构建任务,这次换成流水线,如下图

在这里插入图片描述

最终效果:

在这里插入图片描述

2.编写Pipeline流水线脚本–Jenkinsfile

1.参数化构建

这里先是需要加几个参数,构建时候用到,如下图

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

2.流水线脚本

在这里插入图片描述

在这里插入图片描述

1.完整脚本示例
pipeline{
    agent any
    environment {
        CHAT_WEBHOOK_URL = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=XXXXXXXXXXXXXXXXXXXXXX"
    }
    tools {
        jdk 'JDK17'
        maven 'maven'
    }
    parameters {
        choice(name: 'Status', choices: ['deploy', 'rollback'], description: '参数是部署deploy、回滚rollback')
        string(name: 'Version', defaultValue: '0', description: '版本号,部署时忽略,回滚时输入回滚的版本号,默认回滚上一次')
        string(name: 'AppName', defaultValue: 'spmp-camunda', description: '项目名称,如spmp-camunda')
    }
    stages{
        stage('拉取代码') {
            steps {
                git branch: 'develop', credentialsId: '4c877bb0-772d-4dbb-b1be-d881abfd8f3e', url: 'http://192.168.100.53:9080/micro-business/spmp-camunda.git'
                echo '拉取代码完成!'
            }
        }
        stage('maven打包') {
            steps {
                script { 
                    withMaven(maven: 'maven', mavenSettingsConfig: 'a30d5ce5-2427-4a42-b2b3-b53fe423a4c1') {
                        sh 'mvn clean install -Dmaven.test.skip=true -DprofileActive=test -Ptest'
                    }
                }
                echo 'maven打包成功!'
            }
        }
        stage('备份文件') {
            when {
                expression { params.Status == 'deploy' }
            }
            steps {
                script {
                    def status = params.Status
                    def buildNumber = env.BUILD_NUMBER
                    def workspace = env.WORKSPACE
                    def Path = "$workspace/spmp-camunda-web/bak/$buildNumber"
                    
                    echo "${Path}"
        
                    sh """
                        echo \"Status:${status}\"
                        echo \"BUILD_NUMBER:${buildNumber}\"
                        echo \"WORKSPACE:${workspace}\"
                        path="${workspace}/spmp-camunda-web/bak/${buildNumber}"
                        if [ -d \$path ]; then
                            echo \"The files already exist\"
                        else
                            mkdir -p \$path
                            echo \"The files mkdir\"
                        fi
                        cp -f ${workspace}/spmp-camunda-web/target/*.jar \$path
                        echo 'bak ------------ Completing!'
                    """
                    
                    echo '备份文件成功!'
                }
            }
        }
        stage('回滚文件') {
            when {
                expression { params.Status == 'rollback' }
            }
            steps {
                script {
                    def status = params.Status
                    def version = params.Version.toInteger()
                    def buildNumber = env.BUILD_NUMBER
                    def workspace = env.WORKSPACE

                    sh """
                        echo \"Status:${status}\"
                        echo \"Version:${version}\"
                        echo \"BUILD_NUMBER:${buildNumber}\"
                        echo \"WORKSPACE:${workspace}\"
                        v="${version}"
                        number="${buildNumber}"
                        if [ \$v -eq 0 ]; then
                          v=\$((\$number - 2))
                        fi
                        echo \"回滚到版本--v:\$v\"
                        cd ${workspace}/spmp-camunda-web/bak/\$v
                        cp -f *.jar ${workspace}/spmp-camunda-web/target/
                    """
                }
                
                echo '回滚文件成功!'
            }
        }
        stage('更新jar包') {
            steps {
                script {
                    def appName = params.AppName
                    def workspace = env.WORKSPACE
                    def JAR_HOME = sh(script: "cd ${workspace}/${appName}-web/target && pwd", returnStdout: true).trim()
                    def WEB_HOME = sh(script: "cd /data/spmp/${appName}/origin && pwd", returnStdout: true).trim()
                    sh """
                       echo \"AppName:${appName}\" 
                       echo \"WORKSPACE:${workspace}\"
                       echo \"JAR_HOME:${JAR_HOME}\" 
                       echo \"WEB_HOME:${WEB_HOME}\"
                       
                       cd ${JAR_HOME}
                       cp -f ${appName}-web-test.jar ${WEB_HOME}
                       cd /data/spmp/${appName}
                       sh rln.sh
                    """
                }
                
                script {
                    def appName = params.AppName
                    echo "停止原docker容器${appName}"
                    sh "docker stop ${appName}"
                }
                
                echo '更新jar包成功!'
            }
        }
        stage('复制部署脚本,替换参数; 发送到k8s主节点') {
            steps {
                script {
                    def appName = params.AppName
                    def port = params.Port
                    def replicas = params.Replicas
                    def nodeName = params.NodeName
                    def nodePort = params.NodePort
                    
                    def YML_HOME = "/data/spmp"
                    def CP_YML_HOME = "/data/spmp/${appName}"
                    
                    sh """
                        echo \"AppName:${appName}\"
                        echo \"Port:${port}\"
                        echo \"Replicas:${replicas}\"
                        echo \"NodeName:${nodeName}\"
                        echo \"NodePort:${nodePort}\"
                        echo \"YML_HOME:${YML_HOME}\"
                        echo \"CP_YML_HOME:${CP_YML_HOME}\"
                        
                        cd ${YML_HOME}
                        cp -f deployment.yaml ${CP_YML_HOME}
                        cp -f service.yaml ${CP_YML_HOME}

                        cd ${CP_YML_HOME}
                        sed -i 's/\${port}/${port}/g' deployment.yaml
                        sed -i 's/\${replicas}/${replicas}/g' deployment.yaml
                        sed -i 's/\${app_name}/${appName}/g' deployment.yaml
                        sed -i 's/\${node_name}/${nodeName}/g' deployment.yaml
                        
                        sed -i 's/\${port}/${port}/g' service.yaml
                        sed -i 's/\${nodePort}/${nodePort}/g' service.yaml
                        sed -i 's/\${app_name}/${appName}/g' service.yaml
                    """
                    echo '复制部署脚本,替换参数成功!'
                    
                    sh """
                        cd ${CP_YML_HOME}
                        scp deployment.yaml root@192.168.100.51:/data/spmp/${appName}/
                        scp service.yaml root@192.168.100.51:/data/spmp/${appName}/
                    """
                    
                    echo '发送部署脚本到k8s主节点成功!'
                }
            }
        }
        stage('部署到k8s(在主节点操作)') {
            steps {
                script {
                    def appName = params.AppName
                    sh """
                        ssh root@192.168.100.51 << remotessh
                        echo \"AppName:${appName}\"
                        cd /data/spmp/${appName}
                        pwd
                        kubectl apply -f deployment.yaml
                        kubectl apply -f service.yaml
                        kubectl rollout restart -n default deployment ${appName}
                        exit
                    """
                    echo '部署到k8s成功!'
                }
            }
        }
        stage('删除历史备份文件') {
            steps {
                script {
                    def reservedNum = 10
                    def rmFileDir = "${WORKSPACE}/spmp-camunda-web/bak"
                    def rootDir = sh(script: "cd ${rmFileDir} && pwd", returnStdout: true).trim()
                    def fileNum = sh(script: "cd ${rmFileDir} && ls -l | grep '^d' | wc -l", returnStdout: true).trim().toInteger()

                    while (fileNum > reservedNum) {
                        def oldFile = sh(script: "cd ${rmFileDir} && ls -rt | head -1", returnStdout: true).trim()
                        sh "cd ${rmFileDir} && rm -rf ${oldFile}"
                        fileNum--
                    }
                }
                echo '删除历史备份文件!'
            }
        }
    }
    post {
        success{
            script {
                def appName = params.AppName
                def changeString = getChangeString()
                def BUILD_TRIGGER_BY = "${currentBuild.getBuildCauses()[0].shortDescription} / ${currentBuild.getBuildCauses()[0].userId}"

                sh """
                    curl "${CHAT_WEBHOOK_URL}" \
                    -H "Content-Type: application/json" \
                    -d '
                       {
                            "msgtype": "markdown",
                            "markdown": {
                             "content": "<font color=#FFA500>**Jenkins任务构建结果通知**</font>
                             >任务名称:<font color=#696969>'"${JOB_NAME}"'</font>
                             >构建版本:<font color=#696969>'"${BUILD_DISPLAY_NAME}"'</font>
                             >构建人:<font color=#696969>'"${BUILD_TRIGGER_BY}"'</font>
                             >任务地址:[点击访问(需登录)]('"${JOB_URL}"')
                             >构建详情:[点击访问(需登录)]('"${BUILD_URL}"')
                             >提交记录:<font color=#696969>'"${changeString}"'</font>
                             >构建状态:<font color=#008000>**Success**</font>
                             >jar包下载:[点击下载(需登录)]('"${BUILD_URL}artifact/com/znak/spmp/camunda/environment/${appName}-web/1.0.0/${appName}-web-1.0.0.jar"')"
                            }
                       }
                    '
                """
            }
        }
        failure{
            script {
                def changeString = getChangeString()
                def BUILD_TRIGGER_BY = "${currentBuild.getBuildCauses()[0].shortDescription} / ${currentBuild.getBuildCauses()[0].userId}"
                sh """
                    curl "${CHAT_WEBHOOK_URL}" \
                    -H "Content-Type: application/json" \
                    -d '
                       {
                            "msgtype": "markdown",
                            "markdown": {
                             "content": "<font color=#FFA500>**Jenkins任务构建结果通知**</font>
                             >任务名称:<font color=#696969>'"${JOB_NAME}"'</font>
                             >构建版本:<font color=#696969>'"${BUILD_DISPLAY_NAME}"'</font>
                             >构建人:<font color=#696969>'"${BUILD_TRIGGER_BY}"'</font>
                             >任务地址:[点击访问(需登录)]('"${JOB_URL}"')
                             >构建详情:[点击访问(需登录)]('"${BUILD_URL}"')
                             >提交记录:<font color=#696969>'"${changeString}"'</font>
                             >构建状态:<font color=#FF0000>**Failure**</font>"
                            }
                       }
                    '
                """
            }
        }
    }
}
@NonCPS
def getChangeString() {
    MAX_MSG_LEN = 100
    def changeString = ""
 
    echo "Gathering SCM changes"
    def changeLogSets = currentBuild.changeSets
    for (int i = 0; i < changeLogSets.size(); i++) {
        def entries = changeLogSets[i].items
        for (int j = 0; j < entries.length; j++) {
            def entry = entries[j]
            truncated_msg = entry.msg.take(MAX_MSG_LEN)
            changeString += " - ${truncated_msg}\n"
        }
    }
    if (!changeString) {
        changeString = " - No new changes"
    }
    return changeString
}
2.具体步骤讲述

这里主要讲k8s相关部分以及集成企微群消息部分

  • 复制部署脚本,替换参数; 发送到k8s主节点

    这一步前置原因是由于为了把deployment.yaml和service.yaml做成不同项目都可使用的模板,修改成了如下的样子(并放在从机 /data/spmp 目录下):

    deployment.yaml

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: ${app_name}
      namespace: default
      labels:
        app: ${app_name}
    spec:
      replicas: ${replicas}
      selector:
        matchLabels:
          app: ${app_name}
      template:
        metadata:
          labels:
            app: ${app_name}
        spec:
          containers:
          - name: ${app_name}
            image: csms-dev:1.2
            ports:
            - containerPort: ${port}
            volumeMounts:
            - name: app-volume
              mountPath: /opt/app
            - name: hosts-volume
              mountPath: /etc/hosts
            resources:
              limits:
                memory: "1024Mi"
          volumes:
          - name: app-volume
            hostPath:
              path: /data/spmp/${app_name}
          - name: hosts-volume
            hostPath:
              path: /etc/hosts
          restartPolicy: Always
          nodeSelector:
            kubernetes.io/hostname: ${node_name}
    
    

    service.yaml

    apiVersion: v1
    kind: Service
    metadata:
      name: ${app_name}-service
    spec:
      selector:
        app: ${app_name}
      ports:
        - port: ${port}
          targetPort: ${port}
          protocol: TCP
          nodePort: ${nodePort}
      type: NodePort
    

    流水线脚本对应步骤

            stage('复制部署脚本,替换参数; 发送到k8s主节点') {
                steps {
                    script {
                        def appName = params.AppName
                        def port = params.Port
                        def replicas = params.Replicas
                        def nodeName = params.NodeName
                        def nodePort = params.NodePort
                        
                        def YML_HOME = "/data/spmp"
                        def CP_YML_HOME = "/data/spmp/${appName}"
                        
                        sh """
                            echo \"AppName:${appName}\"
                            echo \"Port:${port}\"
                            echo \"Replicas:${replicas}\"
                            echo \"NodeName:${nodeName}\"
                            echo \"NodePort:${nodePort}\"
                            echo \"YML_HOME:${YML_HOME}\"
                            echo \"CP_YML_HOME:${CP_YML_HOME}\"
                            
                            cd ${YML_HOME}
                            cp -f deployment.yaml ${CP_YML_HOME}
                            cp -f service.yaml ${CP_YML_HOME}
    
                            cd ${CP_YML_HOME}
                            sed -i 's/\${port}/${port}/g' deployment.yaml
                            sed -i 's/\${replicas}/${replicas}/g' deployment.yaml
                            sed -i 's/\${app_name}/${appName}/g' deployment.yaml
                            sed -i 's/\${node_name}/${nodeName}/g' deployment.yaml
                            
                            sed -i 's/\${port}/${port}/g' service.yaml
                            sed -i 's/\${nodePort}/${nodePort}/g' service.yaml
                            sed -i 's/\${app_name}/${appName}/g' service.yaml
                        """
                        echo '复制部署脚本,替换参数成功!'
                        
                        sh """
                            cd ${CP_YML_HOME}
                            scp deployment.yaml root@192.168.100.51:/data/spmp/${appName}/
                            scp service.yaml root@192.168.100.51:/data/spmp/${appName}/
                        """
                        
                        echo '发送部署脚本到k8s主节点成功!'
                    }
                }
            }
    

    注意点:

    • params.[参数名]可以获取前面配置的参数化构建里的参数;env.[参数名]则可以获取jenkins的环境变量(这里应该直接${}也能直接获取)
    • sh 单行用 ''单引号包裹,多行用两个'''三单引号包裹;如果sh里还要引用参数如前面定义的def参数,或者jenkins环境变量,则单行用""双引号,多行用三个"""双引号包裹。
    • 注意如果是双引号包裹的,"$符号会被转义,当只是想要用字面值的时候,要在前面加上\反斜杠去除默认转义
    • 这里传输到远程,本来是可以用jenkins自带的SSH Publishers功能的,但是流水线这里我配置了一直说连接超时,所以后面改成了scp的方式传输了,然后需要注意scp的方式要先在主机(远程传输的目标)上配置好从机的rsa公钥,即免密登录,不然走到这里要进入输入密码的步骤,就执行不下去了
  • 部署到k8s(在主节点操作)

    流水线脚本对应步骤

    stage('部署到k8s(在主节点操作)') {
                steps {
                    script {
                        def appName = params.AppName
                        sh """
                            ssh root@192.168.100.51 << remotessh
                            echo \"AppName:${appName}\"
                            cd /data/spmp/${appName}
                            pwd
                            kubectl apply -f deployment.yaml
                            kubectl apply -f service.yaml
                            kubectl rollout restart -n default deployment ${appName}
                            exit
                        """
                        echo '部署到k8s成功!'
                    }
                }
            }
    

    注意点:

    • 这里用免密ssh到主机执行命令的方式部署项目到k8s,没有用SSH Publishers的原因同上,部署的命令解释见前面的5.2.
  • 集成企微群消息

    流水线脚本对应步骤

    environment {
        CHAT_WEBHOOK_URL = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=XXXXXXXXXXXXXXXXXXXXXX"
    }
    
    post {
            success{
                script {
                    def appName = params.AppName
                    def changeString = getChangeString()
                    def BUILD_TRIGGER_BY = "${currentBuild.getBuildCauses()[0].shortDescription} / ${currentBuild.getBuildCauses()[0].userId}"
    
                    sh """
                        curl "${CHAT_WEBHOOK_URL}" \
                        -H "Content-Type: application/json" \
                        -d '
                           {
                                "msgtype": "markdown",
                                "markdown": {
                                 "content": "<font color=#FFA500>**Jenkins任务构建结果通知**</font>
                                 >任务名称:<font color=#696969>'"${JOB_NAME}"'</font>
                                 >构建版本:<font color=#696969>'"${BUILD_DISPLAY_NAME}"'</font>
                                 >构建人:<font color=#696969>'"${BUILD_TRIGGER_BY}"'</font>
                                 >任务地址:[点击访问(需登录)]('"${JOB_URL}"')
                                 >构建详情:[点击访问(需登录)]('"${BUILD_URL}"')
                                 >提交记录:<font color=#696969>'"${changeString}"'</font>
                                 >构建状态:<font color=#008000>**Success**</font>
                                 >jar包下载:[点击下载(需登录)]('"${BUILD_URL}artifact/com/znak/spmp/camunda/environment/${appName}-web/1.0.0/${appName}-web-1.0.0.jar"')"
                                }
                           }
                        '
                    """
                }
            }
            failure{
                script {
                    def changeString = getChangeString()
                    def BUILD_TRIGGER_BY = "${currentBuild.getBuildCauses()[0].shortDescription} / ${currentBuild.getBuildCauses()[0].userId}"
                    sh """
                        curl "${CHAT_WEBHOOK_URL}" \
                        -H "Content-Type: application/json" \
                        -d '
                           {
                                "msgtype": "markdown",
                                "markdown": {
                                 "content": "<font color=#FFA500>**Jenkins任务构建结果通知**</font>
                                 >任务名称:<font color=#696969>'"${JOB_NAME}"'</font>
                                 >构建版本:<font color=#696969>'"${BUILD_DISPLAY_NAME}"'</font>
                                 >构建人:<font color=#696969>'"${BUILD_TRIGGER_BY}"'</font>
                                 >任务地址:[点击访问(需登录)]('"${JOB_URL}"')
                                 >构建详情:[点击访问(需登录)]('"${BUILD_URL}"')
                                 >提交记录:<font color=#696969>'"${changeString}"'</font>
                                 >构建状态:<font color=#FF0000>**Failure**</font>"
                                }
                           }
                        '
                    """
                }
            }
        }
    }
    @NonCPS
    def getChangeString() {
        MAX_MSG_LEN = 100
        def changeString = ""
     
        echo "Gathering SCM changes"
        def changeLogSets = currentBuild.changeSets
        for (int i = 0; i < changeLogSets.size(); i++) {
            def entries = changeLogSets[i].items
            for (int j = 0; j < entries.length; j++) {
                def entry = entries[j]
                truncated_msg = entry.msg.take(MAX_MSG_LEN)
                changeString += " - ${truncated_msg}\n"
            }
        }
        if (!changeString) {
            changeString = " - No new changes"
        }
        return changeString
    }
    

    注意点:

    • 需要先安装jenkins插件[Qy Wechat Notification Plugin]

    • 然后在需要接收jenkins构建消息的企业微信群里创建一个机器人,保存好他的Webhook地址

      在这里插入图片描述

      脚本里的CHAT_WEBHOOK_URL就是配置的这个webhook地址

    • getChangeString()方法是获取本次jenkins构建的修改内容(git提交记录),就是下图的内容

      在这里插入图片描述

    • 这部分脚本配置参考:【Jenkins】Jenkins构建结果通知企业微信_jenkins发送到企微的结果配置-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/876116.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

CCS6 软件及仿真器驱动安装

1 CCS6 软件获取 TI 的官网上下载: http://www.ti.com/tools-software/ccs.html 注意 首先 win32 是 CCS 安装包支持 64 位系统,我们电脑也是 64 位系统也是安装的 win32 的安装包,另外 TI 只提供 win32 的安装包,无 win64 的安装包。 2 CCS6 软件安装 CCS如果获取提供的…

第十二周:机器学习笔记

第十二周周报 摘要Abstract机器学习1. Recurrent Neural Network&#xff08;下&#xff09;1.1 RNN的Loss Function怎么求&#xff1f;1.2 RNN奇怪的特性1.3 如何解决 RNN 梯度消失或者爆炸1.4 RNN 其他应用 Pytorch学习1. 现有的网络模型使用以及其修改1.1 在VGG16模型添加Mo…

docker部署bind9

一、部署 ## docker 部署bind9# docker run -d --name bind9 --restartalways --publish 53:53/tcp --publish 53:53/udp --publish 10000:10000/tcp --volume /data/docker/dns-server:/data --env ROOT_PASSWORDroot dhub.kubesre.xyz/sameersbn/bind:9.16.1-20200524# 建数…

小程序——生命周期

文章目录 运行机制更新机制生命周期介绍应用级别生命周期页面级别生命周期组件生命周期生命周期两个细节补充说明总结 运行机制 用一张图简要概述一下小程序的运行机制 冷启动与热启动&#xff1a; 小程序启动可以分为两种情况&#xff0c;一种是冷启动&#xff0c;一种是热…

53.9k star 提升命令行效率的模糊搜索神器--fzf

fzf简介 作为Linux/Unix命令行的重度用户,你是否还在使用繁琐的管道命令与复杂选项组合来过滤文件和数据?其实我们有一个更简单高效的选择 - fzf。 fzf是一个开源的通用模糊搜索工具,可以大幅度提升命令行的使用体验。它的查询运行速度极快,支持预览选中的文件内容,还能与各…

Tableau学习日记

Day1&#xff1a;Tableau简介、条形图与直方图 1.1 Tableau绘制条形图 1.1.1 条形图1&#xff1a;各地区酒店数量 1.1.2 条形图2&#xff1a;各地区酒店均价 1.1.3 堆积图&#xff1a;价格等级堆积图 1.2 Tableau绘制直方图 1.2.1创建评分直方图 Day2&#xff1a;数据处理、…

CSS“多列布局”(补充)——WEB开发系列35

多列布局是一种非常常见的布局方式&#xff0c;适用于内容丰富的页面&#xff0c;如新闻网站、杂志或博客。 一、CSS多列布局概述 CSS多列布局允许我们将内容分成多个垂直列&#xff0c;使页面布局更加灵活和多样化。多列布局的主要属性包括 ​​column-count​​、​​column…

《OpenCV计算机视觉》—— 图像轮廓检测与绘制

文章目录 一、轮廓的检测二、轮廓的绘制图像轮廓检测与绘制的代码实现 三、轮廓的近似 一、轮廓的检测 轮廓检测是指在包含目标和背景的数字图像中&#xff0c;忽略背景和目标内部的纹理以及噪声干扰的影响&#xff0c;采用一定的技术和方法来实现目标轮廓提取的过程注意:做轮…

GPS/LBS/Wi-Fi定位,全安排!—合宙Air201资产定位模组LuatOS快速入门04

经历了hello world、点灯、远程控制三期基础教程&#xff0c;小伙伴们是不是收获满满&#xff0c;期待更高阶的应用呢&#xff1f; 本期&#xff0c;我们将学习合宙Air201的核心功能之一——定位功能&#xff01; Air201定位示例教程 合宙Air201资产定位模组——是一个集成超…

TCP交互通讯在Windows中的频率

在基于TCP协议的交互式通讯中&#xff0c;通过网口进行数据传输时&#xff0c;Windows系统的通讯频率通常受到多方面的限制&#xff0c;很难稳定达到几千Hz。以下是关于频率范围的合理分析及提高频率的措施。 频率限制的原因&#xff1a; 网络延迟&#xff1a;TCP通讯的一个核心…

SpringBoot集成Thymeleaf模板引擎,为什么使用(详细介绍)

学习本技术第一件事&#xff1a;你为什么要使用&#xff0c;解决什么问题的&#xff1f; 1.为什么使用&#xff08;使用背景&#xff09;&#xff1f; 首先应用场景是单体项目&#xff0c;如果是前后端分离就不用关注这个了&#xff0c;因为单体项目你前后端都是写在一个项目…

【CTF MISC】XCTF GFSJ1086 [简单] 简单的base编码 Writeup(Base64编码+循环解码+Base92编码)

[简单] 简单的base编码 你懂base编码吗&#xff1f; 工具 在线BASE92编码解码&#xff1a;https://ctf.bugku.com/tool/base92 解法 Vm0wd2QyUXlVWGxWV0d4V1YwZDRWMVl3WkRSV01WbDNXa1JTVjAxV2JETlhhMUpUVmpBeFYySkVUbGhoTVVwVVZtcEJlRll5U2tWVWJHaG9UVlZ3VlZadGNFSmxSbGw1V…

MySQL详解:数据类型、约束

MySQL 1. 数据类型1.1 数值类型1.1.1 bit 位类型1.1.2 整数数据类型1.1.3 小数类型floatdecimal 1.2 字符类型1.2.1 char1.2.2 varchar 可变长字符串1.2.3 日期和时间类型datedatetimetimestamp 1.2.4 enum1.2.5 set集合查询函数 find_in_set 2. 表的约束2.1 NULL 空属性2.2 默…

《Linux运维总结:基于ARM64+X86_64架构CPU使用docker-compose一键离线部署mongodb 7.0.14容器版副本集群》

总结&#xff1a;整理不易&#xff0c;如果对你有帮助&#xff0c;可否点赞关注一下&#xff1f; 更多详细内容请参考&#xff1a;《Linux运维篇&#xff1a;Linux系统运维指南》 一、部署背景 由于业务系统的特殊性&#xff0c;我们需要面向不通的客户安装我们的业务系统&…

[Visual Stuidio 2022使用技巧]2.配置及常用快捷键

使用vs2022开发WPF桌面程序时常用配置及快捷键。 语言&#xff1a;C# IDE&#xff1a;Microsoft Visual Studio Community 2022 框架&#xff1a;WPF&#xff0c;.net 8.0 一、配置 1.1 内联提示 未开启时&#xff1a; 开启后&#xff1a; 开启方法&#xff1a; 工具-选…

基于微信小程序的垃圾分类(lw+演示+源码+运行)

摘 要 随着生态文明体制改革的不断推进,可持续发展的环保理念逐渐成为社会共识,而在推行环保措施的过程中却困难重重.针对生活废弃物肆意无序投放的问题,尽管目前各大城市相继推出垃圾分类强制性执行的政策法规,但因市民欠缺对垃圾种类的正确认识而导致垃圾分类的施行难度和成…

Luban策划开源工具

一、Luban游戏配置解决方案&#xff0c;是一个强大、易用、优雅、稳定的游戏配置解决方案。它设计目标为满足从小型到超大型游戏项目的简单到复杂的游戏配置工作流需求。luban标准化了游戏配置开发工作流&#xff0c;可以极大提升策划和程序的工作效率。 二、核心特性&#xf…

【代码随想录训练营第42期 Day58打卡 - 图论Part8 - 拓扑排序

目录 一、拓扑排序介绍 定义 特点 实现方法&#xff08;2种&#xff09; 应用 二、题目与题解 题目&#xff1a;卡码网 117. 软件构建 题目链接 题解&#xff1a;拓扑排序 - Kahn算法&#xff08;BFS&#xff09; 三、小结 一、拓扑排序介绍 对于拓扑排序&#xff0c…

攻防世界 ics-05

ics-05 隐藏的变量传参&#xff0c;php弱类型比较 只有设备维护中心可以点击进去 查看源码&#xff0c;发现有个隐藏的超链接变量传参 看到变量传参&#xff0c;有可能存在文件包含漏洞读取源码&#xff0c;这个站是php的站&#xff0c;所以可以使用php伪协议读取源码 index.p…

Web 原生组件化方案:Web Components

你好&#xff0c;我是沐爸&#xff0c;欢迎点赞、收藏、评论和关注。 Web 组件化是一种将Web应用的UI部分拆分成可复用的独立组件的架构方法。这种方法有助于提高代码的可维护性、可重用性和可测试性。 而Web Components 标准则提供了一套原生的API&#xff0c;允许开发者创建…