likes
comments
collection
share

如何实现前端工程的CI/CD?

作者站长头像
站长
· 阅读数 13

虽然现在大部分前端开发不需要进行项目运维,但是前端项目如何在线上环境跑起来的,大家也还是需要了解一下的。本文将介绍一些前端项目部署及CI/CD常见的解决方案。

GitHub Page

  • 我们先创建个项目,命名用账号名称.github.io的后缀,例如我的waltiu.github.io

    记得把github设置中的worrkflow读写权限打开,要不然会报git denied to github-actions[bot]`或`Process completed with exit code 128

  • 代码中新建.github\workflows\deploy.github.yml 内容如下:

    name: GitHub Pages
    on:
      push:
        branches:
          - master
    jobs:
      deploy:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v2
            with:
              submodules: true
    
          - name: Setup Node
            uses: actions/setup-node@v2
            with:
              node-version: "14.x"
          # 使用缓存依赖
          - name: Get yarn cache  
            id: yarn-cache
            run: echo "::set-output name=dir::$(yarn cache dir)"
          - uses: actions/cache@v1
            with:
              path: ${{ steps.yarn-cache.outputs.dir }}
              key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
              restore-keys: |
                ${{ runner.os }}-yarn-
    
          - name: Install Dependencies
            run: yarn 
    
          - name: Build
            run: yarn run build
    
          - name: Deploy
            uses: peaceiris/actions-gh-pages@v3
            with:
              github_token: ${{ secrets.GITHUB_TOKEN }}     # github 会在workflw中自动生成
              publish_dir: "dist"
              publish_brach: "gh-pages" # 静态资源的分支
              force_orphan: true
    

    我们修改下代码,在actions中就看到github自动走了该流程。

    如何实现前端工程的CI/CD? 会发现分支里多了一个我们上面配置的publish_brach: "gh-pages" ,里面的内容正是我们构建的产物 publish_dir: "dist",主界面也多了一个github-pages的选项,我们可以点进入查看我们部署的版本。

    如何实现前端工程的CI/CD? 需要保证我们这里的路径和构建分支是同一个!

    如何实现前端工程的CI/CD? 这样我们就可以看到我们部署的项目,github默认是只允许搭建一个网站,不允许创建多个github page。

  • 创建多个github page

    我们修改下我们的项目名称为test,并等待github重新部署github page后访问

    如何实现前端工程的CI/CD? waltiu.github.io/test 这是修改后的github page地址,但是发现访问后报错了,由于我们访问的路径是/test,但是静态js和css资源还是在访问跟项目根路径,我们应该调整下静态资源的访问路径也是/test。

    如何实现前端工程的CI/CD? 那我修改我们构建工具中的静态资源路径,以vite为例(webpack类似),我们先修改下packjson的名字为test:

    如何实现前端工程的CI/CD?

    如何实现前端工程的CI/CD? 如果我们有使用路由的话,可以根据自己的项目实际情况修改下路由前缀;

  • 如何实现个性化域名:

    我们先去阿里云的域名控制台添加一条域名解析,配置可以参数如下,替换记录为你自己的github page

    如何实现前端工程的CI/CD? 这时我们通过域名直接访问,会出现这个错误,那么我们需要到github上也配置下我们刚才解析的域名

    如何实现前端工程的CI/CD?

    如何实现前端工程的CI/CD?

netlify

我们先登陆下官网,可以使用Github账号登陆, 按照推荐的流程创建一个team;

新建site,从github中导入一个项目,可以在第三步的时候根据自己项目修改下构建命令

如何实现前端工程的CI/CD?

如何实现前端工程的CI/CD?

如何实现前端工程的CI/CD?

我们后续可以在设置中,修改我们的构建参数,比如修改打包后的静态资源地址等

如何实现前端工程的CI/CD?

使用netlify如何个性化域名呢

先配置下我们要个性化的域名:

如何实现前端工程的CI/CD?

如何实现前端工程的CI/CD? 然后和Github就类似了,我们去阿里云为域名添加一条解析记录

如何实现前端工程的CI/CD?

自己的服务器(docker + nginx)

下面以阿里云为例,带大家从0到1部署项目,我们先选择一个linux服务器:

如何实现前端工程的CI/CD?

  • 接下来我们安装一下我们需要的依赖

    1. Nvm + Node + Npm + Pm2
    curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
    
    export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
    [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
    
    nvm install 18.16.0
    npm install pm2@latest -g
    

    2. Nginx

     yum install nginx
    

    3. Docker

    # 普通linux镜像
    curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun
    # 或者
    curl -sSL https://get.daocloud.io/docker | sh
    
    # Alibaba Cloud Linux镜像
    # https://help.aliyun.com/document_detail/264695.html
    yum -y install dnf
    dnf -y install docker
    

    4. Git

    yum install git
    

    最后检查下各个依赖是否安装成功,分别执行-v:

    如何实现前端工程的CI/CD?

  • 将前端工程部署到docker里,那我们会有2种方法来实现,下面分别介绍:

    1. docker内部静态资源替换

      • 创建nginx容器并运行

        执行以下命令后,在浏览器中输入ip:8080测试容器是否生效 如何实现前端工程的CI/CD?

        优化下,容器内外做个映射 如何实现前端工程的CI/CD?

      • gitub workflows 自动构建并替换文件

        先来领取个阿里云服务器的密钥

        如何实现前端工程的CI/CD?

        绑定到自己的服务器上,然后记得重启项目 如何实现前端工程的CI/CD?

        github上添加刚才阿里云下载的密钥

        如何实现前端工程的CI/CD?

        如何实现前端工程的CI/CD?

        最后在前端代码库中新建一个.github/workflows/deploy-page.yml:

        如何实现前端工程的CI/CD?

      • ps: 遇到的一些问题

        1. Alibaba Cloud Linux镜像 镜像需要使用dnf按照docker
        2. 密钥绑定机器后需要重启下
        3. ssh私钥配置到github上时需要给----END RSA PRIVATE KEY----- 后加个空格换行,要不然会报 Load key "/home/runner/.ssh/deploy_key": error in libcrypto
        4. 需要在主机上执行yum install rsync,要不然会报rsync: connection unexpectedly closed (0 bytes received so far) [sender]
        5. nginx如果访问不通,可以排查下端口是否开放了
    2. docker容器更新,重启

      • github workflow 创建docker镜像并推到阿里云镜像服务

        新建一个Dockerfile用来创建镜像

        如何实现前端工程的CI/CD? 我们去阿里云申请一个镜像仓库,先申请命名空间

        如何实现前端工程的CI/CD?

        创建镜像仓库,也可以绑定我们的git账号

        如何实现前端工程的CI/CD?

        如何实现前端工程的CI/CD? 接下来我们完善我们的github workflows:

        先在GitHub密钥中配一下我们的docker容器账号密码(也就是阿里云的账号密码)

        如何实现前端工程的CI/CD? 前端项目中创建.github\workflows\docker-image.yml,大家要根据自己阿里云镜像服务配置来替换圈来的这些内容哦!

        如何实现前端工程的CI/CD? 然后看下github action是否顺利执行和阿里云镜像是否被创建

        如何实现前端工程的CI/CD?

        如何实现前端工程的CI/CD?

      • 新建watch-images服务,并设置阿里云的触发器

        我们先创建一个简单的node服务,具体内容后面再补

        const http = require("http");
        http
          .createServer((req, res) => {
            if (req.method === "get" && req.url === "/") {
              console.log(req.url);
              res.end(req.url);
              //...
            }
            if (req.method === "post" && req.url === "/") {
              console.log("post");
              //...
            }
          })
          .listen(3000, () => {
            console.log("server is ready");
          });
        
        

        我们去服务器上运行一下,并测试下访问下 http://ip/?a=1

        如何实现前端工程的CI/CD?

        http://ip/?a=1设置到阿里云的触发器上,ip替换成真实ip

        如何实现前端工程的CI/CD? 我们更新下我们的服务监听脚本,然后打一些日志

        const http = require("http");
        const url = require("url");
        
        const TYPE_POST = "POST";
        const Type_GET = "GET";
        
        const resolvePost = (req) => {
          return new Promise((resolve) => {
            let chunk = "";
            req.on("data", (data) => {
              chunk += data;
            });
            req.on("end", () => {
              resolve(JSON.parse(chunk));
            });
          });
        };
        
        http
          .createServer(async (req, res) => {
            res.writeHead(200, { "Content-Type": "application/json" });
            if (req.method === Type_GET) {
              const { pathname, query } = url.parse(req.url, true);
              const response = {
                data: "Hello World!",
                url: req.url,
                method: req.method,
                pathname,
                query,
              };
              console.log(new Date().toLocaleString(),response)
              res.end(JSON.stringify(response));
            }
            if (req.method === TYPE_POST) {
              const data = await resolvePost(req);
              const response= {
                data,
                url: req.url,
                method: req.method,
              }
              console.log(new Date().toLocaleString(),response)
              res.end(
                JSON.stringify(response)
              );
            }
          })
          .listen(3000, () => {
            console.log("server is ready");
          })                
        

        我们可以使用pm2替换下node,来守护进程,并查看下日志

        如何实现前端工程的CI/CD?

        如何实现前端工程的CI/CD?

        如何实现前端工程的CI/CD? Pm2和阿里云镜像服务触发器的日志都有,一切ok了

      • 优化Watch-image服务,在请求中更新并重启容器

        优化watch.js,get走前端模拟测试,post走阿里云镜像的触发器

        /**
         * 请求url参数
         * nameSpace  阿里云容器命名空间
         * name  阿里云实例名称
         * version  版本
         * port 服务端口号
         * containerName  容器名称
         */
        const { exec, execSync } = require("child_process");
        const http = require("http");
        const url = require("url");
        
        const TYPE_POST = "POST";
        const Type_GET = "GET";
        
        const resolvePost = (req) => {
          return new Promise((resolve) => {
            let chunk = "";
            req.on("data", (data) => {
              chunk += data;
            });
            req.on("end", () => {
              resolve(JSON.parse(chunk));
            });
          });
        };
        
        http
          .createServer(async (req, res) => {
            res.writeHead(200, { "Content-Type": "application/json" });
            let shellUrl = "";
            // 测试时可以去掉sh,或者在git bash上运行
            const { pathname, query } = url.parse(req.url, true);
            if (req.method === Type_GET) {
              const response = {
                result: "Hello World!",
                url: req.url,
                method: req.method,
                pathname,
                query,
              };
              console.log(new Date().toLocaleString(), response);
              const path = `${query.nameSpace}/${query.name}:${query.version}`;
              shellUrl = `sh update.sh ${path}  ${query.port}  ${query.containerName}`;
              console.log(shellUrl, "shell");
              if (
                query.nameSpace &&
                query.name &&
                query.version &&
                query.port &&
                query.containerName
              ) {
                execSync(shellUrl);
              }
              res.end(JSON.stringify(response));
            }
            if (req.method === TYPE_POST) {
              const result = await resolvePost(req);
              const response = {
                result,
                url: req.url,
                method: req.method,
                port: query.port || 8888,
                containerName: query.containerName,
              };
              console.log(new Date().toLocaleString(), response);
              const path = `${result.repository.namespace}/${result.repository.name}:${result.push_data.tag}`;
              shellUrl = `sh update.sh ${path}  ${response.port}  ${query.containerName}`;
              console.log(shellUrl, "shell");
              execSync(shellUrl);
              res.end(JSON.stringify(response));
            }
          })
          .listen(3000, () => {
            console.log("server is ready");
          });
        

        添加update.sh脚本,每次接收请求后删除容器->更新镜像->重启容器。同时又预留一些参数变量,可以通过执行脚本的时候传进来。可以使用阿里云触发器传过来的镜像参数,也可以自己模拟。我们将脚本的日志放到log.text中,日志进行叠加。

           #!/bin/bash
        
            path=$1
            port=$2
            containerName=$3
            username=$4
            password=$5
        
            imagepath=registry.cn-beijing.aliyuncs.com/$path
        
        
            # 这里大家可以提前在安装依赖的时候就执行一遍 docker login ,就不用输入密码(docker login --username=waltiu registry.cn-beijing.aliyuncs.com)
        
            # 或者在执行脚本的时候输入下密码
            # echo -e "---------docker Login--------"
            # docker login --username=$username  --password=$password   # 你docker的用户名和密码
        
            echo $(date +%F%n%T)  >> log.txt
        
            echo  路径:$path  容器名称: $containerName   端口: $port  >> log.txt
        
            echo -e "---------docker restart--------"  >> log.txt
        
            echo -e "---------docker Stop--------" >> log.txt
            docker stop  $containerName  # 停止容器
            echo -e "---------docker Rm--------" >> log.txt
            docker rm  	 $containerName  # 删除容器
        
            echo -e "---------docker Pull--------" >> log.txt
            docker pull $imagepath		 # 更新镜像
        
            echo -e "---------docker Create and Start--------" >> log.txt
            docker run --rm -d -p $port:80 --name $containerName $imagepath # 重启容器
            echo -e "---------deploy Success--------" >> log.txt
        
            # 本地调试可以打开
            # sleep 1000
        
            # echo -e "---------deploy end--------"
        

        下面尝试改下代码,查看后台日志,检测下更改是否生效!

        如何实现前端工程的CI/CD?

        如何实现前端工程的CI/CD? 后台日志正常,阿里云和github的构建记录也是有的。这只是重新构建的一种方法,也可以全部流程都在github workflows里完成,比如打包完镜像后,直接访问服务,在workflow里把脚本放到服务器中并执行。

        • ps: 遇到的一些问题

          1. 创建dockerfile的时候要指定工作区,要不然会报npm ERR! Tracker "idealTree" already exists
          2. 服务器执行sh脚本默认时没有权限的,需要添加sh前缀,sh update.sh
          3. 像上面这样执行sh脚本是没有日志,我们手动加个日志,echo的内容后加个 >> log.txt 或者 >log.txt

总结:前端项目自动化部署的方法很多,我这里只做个抛砖引玉,大家有其他方法也可以一起沟通,项目中遇到的一些工具放到下面这个github里,欢迎大家查看。 github.com/waltiu/serv…