前端集成化部署 docker篇

我們可能會(huì)遇到這樣的問(wèn)題,我們手動(dòng)部署項(xiàng)目,可能是node項(xiàng)目,可能是java項(xiàng)目,可能是前端項(xiàng)目,我們安裝的node版本或者jdk,tomcat版本不一致,導(dǎo)致項(xiàng)目會(huì)發(fā)生各種詭異問(wèn)題,有的服務(wù)器就是好使,有的服務(wù)器就是有問(wèn)題,正常來(lái)說(shuō)都是部署漏了點(diǎn)東西。

我們就不能把好的服務(wù)打成包直接拿來(lái)使用么?

布署軟件的問(wèn)題

  • 如果想讓軟件運(yùn)行起來(lái)要保證操作系統(tǒng)的設(shè)置,各種庫(kù)和組件的安裝都是正確的
  • 熱帶魚&冷水魚 冷水魚適應(yīng)的水溫在5-30度,而熱帶魚只能適應(yīng)22-30度水溫,低于22度半小時(shí)就凍死了

常用解決方案和對(duì)比

虛擬機(jī)

虛擬機(jī)(virtual machine)就是帶環(huán)境安裝的一種解決方案。它可以在一種操作系統(tǒng)里面運(yùn)行另一種操作系統(tǒng)

  • 資源占用多
  • 冗余步驟多
  • 啟動(dòng)速度慢

Linux容器

由于虛擬機(jī)存在這些缺點(diǎn),Linux 發(fā)展出了另一種虛擬化技術(shù):Linux 容器(Linux Containers,縮寫為 LXC)。

Linux 容器不是模擬一個(gè)完整的操作系統(tǒng),而是對(duì)進(jìn)程進(jìn)行隔離?;蛘哒f(shuō),在正常進(jìn)程的外面套了一個(gè)保護(hù)層。對(duì)于容器里面的進(jìn)程來(lái)說(shuō),它接觸到的各種資源都是虛擬的,從而實(shí)現(xiàn)與底層系統(tǒng)的隔離。

  • 啟動(dòng)快
  • 資源占用少
  • 體積小

Docker

  • Docker 屬于 Linux 容器的一種封裝,提供簡(jiǎn)單易用的容器使用接口。它是目前最流行的 Linux 容器解決方案。
  • Docker 將應(yīng)用程序與該程序的依賴,打包在一個(gè)文件里面。運(yùn)行這個(gè)文件,就會(huì)生成一個(gè)虛擬容器。程序在這個(gè)虛擬容器里運(yùn)行,就好像在真實(shí)的物理機(jī)上運(yùn)行一樣

Docker和KVM

  1. 啟動(dòng)時(shí)間
  • Docker秒級(jí)啟動(dòng)
  • KVM分鐘級(jí)啟動(dòng)
  1. 輕量級(jí) 容器鏡像通常以M為單位,虛擬機(jī)以G為單位,容器資源占用小,要比虛擬要部署更快速
  • 容器共享宿主機(jī)內(nèi)核,系統(tǒng)級(jí)虛擬化,占用資源少,容器性能基本接近物理機(jī)
  • 虛擬機(jī)需要虛擬化一些設(shè)備,具有完整的OS,虛擬機(jī)開銷大,因而降低性能,沒(méi)有容器性能好
  1. 安全性
  • 由于共享宿主機(jī)內(nèi)核,只是進(jìn)程隔離,因此隔離性和穩(wěn)定性不如虛擬機(jī),容器具有一定權(quán)限訪問(wèn)宿>- 主機(jī)內(nèi)核,存在一下安全隱患
  1. 使用要求
  • KVM基于硬件的完全虛擬化,需要硬件CPU虛擬化技術(shù)支持
  • 容器共享宿主機(jī)內(nèi)核,可運(yùn)行在主機(jī)的Linux的發(fā)行版,不用考慮CPU是否支持虛擬化技術(shù)

Docker的應(yīng)用場(chǎng)景

  • 節(jié)省項(xiàng)目環(huán)境部署時(shí)間

  • 單項(xiàng)目打包

  • 整套項(xiàng)目打包

  • 新開源技術(shù)

  • 環(huán)境一致性

  • 持續(xù)集成

  • 微服務(wù)

  • 彈性伸縮

Docker 體系結(jié)構(gòu)

  • containerd 是一個(gè)守護(hù)進(jìn)程,使用runc管理容器,向Docker Engine提供接口
  • shim 只負(fù)責(zé)管理一個(gè)容器
  • runC是一個(gè)輕量級(jí)工具,只用來(lái)運(yùn)行容器

Docker安裝

  • docker分為企業(yè)版(EE)和社區(qū)版(CE)
  • docker-ce
  • hub.docker
安裝社區(qū)版本docker
yum install -y yum-utils   device-mapper-persistent-data   lvm2
yum-config-manager     --add-repo     https://download.docker.com/linux/centos/docker-ce.repo
yum-config-manager --enable docker-ce-nightly #要每日構(gòu)建版本的 Docker CE
yum-config-manager --enable docker-ce-test  
yum install docker-ce docker-ce-cli containerd.io

docker 啟動(dòng)

systemctl start docker

查看docker版本

    version
docker info

鏡像加速

阿里云鏡像加速

sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
  "registry-mirrors": ["https://fwvjnv59.mirror.aliyuncs.com"]
}
EOF
# 重載所有修改過(guò)的配置文件
sudo systemctl daemon-reload
sudo systemctl restart docker

Docker常用方法

docker image鏡像操作

命令 含義 案例
ls 查看全部鏡像 docker image ls
search 查找鏡像 docker search [imageName]
history 查看鏡像歷史 docker history [imageName]
inspect 顯示一個(gè)或多個(gè)鏡像詳細(xì)信息 docker inspect [imageName]
pull 拉取鏡像 docker pull [imageName]
push 推送一個(gè)鏡像到鏡像倉(cāng)庫(kù) docker push [imageName]
rmi 刪除鏡像 docker rmi [imageName] docker image rmi 2
prune 移除未使用的鏡像,沒(méi)有被標(biāo)記或補(bǔ)任何容器引用 docker image prune
tag 標(biāo)記本地鏡像,將其歸入某一倉(cāng)庫(kù) docker image tag [imageName] [username]/[repository]:[tag]
export 導(dǎo)出容器文件系統(tǒng)tar歸檔文件創(chuàng)建鏡像 docker export -o mysqlv1.tar a404c6c174a2
import 導(dǎo)入容器快照文件系統(tǒng)tar歸檔文件創(chuàng)建鏡像 docker import mysqlv1.tar wp/mysql:v2
save 保存一個(gè)或多個(gè)鏡像到一個(gè)tar歸檔文件 docker save -o mysqlv2.tar wp/mysqlv2:v3
load 加載鏡像存儲(chǔ)文件來(lái)自tar歸檔或標(biāo)準(zhǔn)輸入 docker load -i mysqlv2.tar
build 根據(jù)Dockerfile構(gòu)建鏡像

docker 容器操作

命令 含義 案例
run 從鏡像運(yùn)行一個(gè)容器 docker run ubuntu /bin/echo 'hello-world'
ls 列出容器 docker container ls
inspect 顯示一個(gè)或多個(gè)容器詳細(xì)信息 docker inspect
attach 要attach上去的容器必須正在運(yùn)行,可以同時(shí)連接上同一個(gè)container來(lái)共享屏幕 docker attach
stats 顯示容器資源使用統(tǒng)計(jì) docker container stats
top 顯示一個(gè)容器運(yùn)行的進(jìn)程 docker container top
update 顯示一個(gè)容器運(yùn)行的進(jìn)程 docker container update
port 更新一個(gè)或多個(gè)容器配置 docker container port
ps 查看當(dāng)前運(yùn)行的容器 docker ps -a -l
kill [containerId] 終止容器(發(fā)送SIGKILL ) docker kill [containerId]
rm [containerId] 刪除容器 docker rm [containerId]
start [containerId] 啟動(dòng)已經(jīng)生成、已經(jīng)停止運(yùn)行的容器文件 docker start [containerId]
stop [containerId] 終止容器運(yùn)行 (發(fā)送 SIGTERM ) docker stop [containerId]
logs [containerId] 查看 docker 容器的輸出 docker logs [containerId]
exec [containerId] 進(jìn)入一個(gè)正在運(yùn)行的 docker 容器執(zhí)行命令 docker container exec -it [containerID] /bin/bash
cp [containerId] 從正在運(yùn)行的 Docker 容器里面,將文件拷貝到本機(jī) docker container cp [containID]:app/package.json .
commit [containerId] 創(chuàng)建一個(gè)新鏡像來(lái)自一個(gè)容器 docker commit -a "wp" -m "mysql" a404c6c174a2 mynginx:v1

docker 數(shù)據(jù)盤操作

  • volume
#創(chuàng)建數(shù)據(jù)盤
docker volume create nginx-vol docker volume ls docker volume inspect nginx-vol #把nginx-vol數(shù)據(jù)卷掛載到/usr/share/nginx/html,掛載后容器內(nèi)的文件會(huì)同步到數(shù)據(jù)卷中
docker run -d  --name=nginx1 --mount src=nginx-vol,dst=/usr/share/nginx/html nginx docker run -d  --name=nginx2  -v nginx-vol:/usr/share/nginx/html -p 3000:80 nginx #刪除數(shù)據(jù)卷
docker container stop nginx1 #停止容器
docker container rm nginx1 #刪除容器
docker volume rm nginx-vol  #刪除數(shù)據(jù)庫(kù) 

  • Bind mounts
#此方式與Linux系統(tǒng)的mount方式很相似,即是會(huì)覆蓋容器內(nèi)已存在的目錄或文件,但并不會(huì)改變?nèi)萜鲀?nèi)原有的文件,當(dāng)umount后容器內(nèi)原有的文件就會(huì)還原
#創(chuàng)建容器的時(shí)候我們可以通過(guò)-v或--volumn給它指定一下數(shù)據(jù)盤
#bind mounts 可以存儲(chǔ)在宿主機(jī)系統(tǒng)的任意位置
#如果源文件/目錄不存在,不會(huì)自動(dòng)創(chuàng)建,會(huì)拋出一個(gè)錯(cuò)誤
#如果掛載目標(biāo)在容器中非空目錄,則該目錄現(xiàn)有內(nèi)容將被隱藏
docker run -v /mnt:/mnt -it --name logs centos bash cd /mnt
echo 1 > 1.txt
docker inspect logs
#可以查看到掛載信息
"Mounts": [
    {
        "Source":"/mnt/sda1/var/lib/docker/volumes/dea6a8b3aefafa907d883895bbf931a502a51959f83d63b7ece8d7814cf5d489/_data",
        "Destination": "/mnt",
    }
]
# 指定數(shù)據(jù)盤容器
docker create -v /mnt:/mnt --name logger centos
docker run --volumes-from logger --name logger3 -i -t centos bash cd /mnt 
touch logger3
docker run --volumes-from logger --name logger4 -i -t centos bash cd /mnt
touch logger4

docker 網(wǎng)絡(luò)

安裝Docker時(shí),它會(huì)自動(dòng)創(chuàng)建三個(gè)網(wǎng)絡(luò),bridge(創(chuàng)建容器默認(rèn)連接到此網(wǎng)絡(luò))、 none 、host

  • None:該模式關(guān)閉了容器的網(wǎng)絡(luò)功能,對(duì)外界完全隔離
  • host:容器將不會(huì)虛擬出自己的網(wǎng)卡,配置自己的IP等,而是使用宿主機(jī)的IP和端口。
  • bridge 橋接網(wǎng)絡(luò),此模式會(huì)為每一個(gè)容器分配IP
    可以使用該--network標(biāo)志來(lái)指定容器應(yīng)連接到哪些網(wǎng)絡(luò)
#bridge模式使用 --net=bridge 指定,默認(rèn)設(shè)置
docker network ls #列出當(dāng)前的網(wǎng)絡(luò)
docker inspect bridge #查看當(dāng)前的橋連網(wǎng)絡(luò)
docker run -d --name nginx1 nginx docker run -d --name nginx2 --link nginx1 nginx docker exec -it nginx2 bash
apt update
apt install -y inetutils-ping  #ping
apt install -y dnsutils        #nslookup
apt install -y net-tools       #ifconfig
apt install -y iproute2        #ip
apt install -y curl            #curl
cat /etc/hosts
ping nginx1

# none模式使用--net=none指定
# --net 指定無(wú)網(wǎng)絡(luò)
docker run -d --name nginx_none --net none nginx docker inspect none
docker exec -it nginx_none bash
ip addr

# host模式使用 --net=host 指定
docker run -d --name nginx_host --net host nginx docker inspect host
docker exec -it nginx_host bash
ip addr

端口映射

# 查看鏡像里暴露出的端口號(hào)
docker image inspect nginx
"ExposedPorts": {"80/tcp": {}}
# 讓宿主機(jī)的8080端口映射到docker容器的80端口
docker run -d --name port_nginx -p 8080:80  nginx # 查看主機(jī)綁定的端口
docker container port port_nginx

#指向主機(jī)的隨機(jī)端口
docker run -d --name random_nginx --publish 80 nginx docker port random_nginx

docker run -d --name randomall_nginx --publish-all nginx docker run -d --name randomall_nginx --P nginx 
#創(chuàng)建自定義網(wǎng)絡(luò)
docker network create --driver bridge myweb
# 查看自定義網(wǎng)絡(luò)中的主機(jī)
docker network inspect myweb
# 創(chuàng)建容器的時(shí)候指定網(wǎng)絡(luò) 指定同一個(gè)網(wǎng)絡(luò)的容器是可以互相通信的
docker run -d --name mynginx1  --net myweb nginx docker run -d --name mynginx2  --net myweb nginx docker exec -it mynginx2 bash
ping mynginx1

# 連接到指定網(wǎng)絡(luò)
docker run -d --name mynginx3   nginx docker network connect  myweb mynginx3
docker network disconnect myweb mynginx3

# 移除網(wǎng)絡(luò)
docker network rm myweb

compose 暫時(shí)先不說(shuō),暫時(shí)用到的不多,主要做編排使用,基本上都在使用jekins做編排

部署環(huán)境

node環(huán)境部署

安裝完docker環(huán)境 繼續(xù)安裝node環(huán)境

nvm: # nvm管理node版本
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.1/install.sh | bash
source ~/.bash_profile
nvm ls
nvm install stable 安裝最新的穩(wěn)定版本
nvm use stable
nrm:# 切換node鏡像,修改源為淘寶鏡像
npm i -g nrm
nrm use taobao

安裝pm2 部署線上 node服務(wù)

npm i -g pm2
cd /root/webhook
pm2 start webhook.js --name webhook --watch
pm2 list | pm2 ls

集成項(xiàng)目搭建

回想起以前的前端部署都是前端打個(gè)目標(biāo)文件,壓縮成壓縮包或者rpm安裝包去發(fā)布,如果有多個(gè)環(huán)境還需要一步步的去連服務(wù)器去手動(dòng)發(fā)布

為了解決這種耗人力的工作,這邊推出了一款簡(jiǎn)易的docker發(fā)布項(xiàng)目

我們可以把其中一臺(tái)服務(wù)器配置成發(fā)布服務(wù)器,用來(lái)編譯新鏡像發(fā)布新鏡像,然后直接拷貝鏡像到別的服務(wù)器直接啟動(dòng)

現(xiàn)在我們見一個(gè)node項(xiàng)目docker-hook

此項(xiàng)目的核心是通過(guò)用戶點(diǎn)擊頁(yè)面上的觸發(fā)去動(dòng)態(tài)調(diào)用sh去處理我們的腳本

中間一版本我們是通過(guò)接口調(diào)用觸發(fā),發(fā)現(xiàn)不是很好用,就做一個(gè)可視化平臺(tái)去使用

也可以通過(guò)gitHub的webhook去動(dòng)態(tài)觸發(fā)CI/CD,提交即部署,這邊我就不貼代碼了

這邊主要講思路,貼上部分代碼,如果有需要優(yōu)化的部分麻煩指正

// 本項(xiàng)目使用的是通過(guò)node的child_process spawn開啟一個(gè)子進(jìn)程去處理sh命令
// console log日志是通過(guò)morgan 自定義輸出
// 每個(gè)模塊的sh腳本都會(huì)通過(guò)winston把實(shí)時(shí)日志存儲(chǔ)到對(duì)應(yīng)的模塊日志文件中,文件大問(wèn)題,我們就按天生成一個(gè)文件日志
/** logger.js **/
const winston=require('winston');
const { APP_LIST } = require('./constant')
const { loggerTime } = require('./util')

const loggerList = {};
APP_LIST.forEach(item => {
  loggerList[item.loggerName] = winston.createLogger({
    transports: [
        new (winston.transports.Console)(),
        new (winston.transports.File)({ 
            filename: `public/logs/${item.loggerName}-${loggerTime()}.log`,
            timestamp:'true', 
            maxsize: 10485760, //日志文件的大小
            maxFiles: 10 })
    ]});
});
loggerList['init'] = winston.createLogger({
  transports: [
      new (winston.transports.Console)(),
      new (winston.transports.File)({ 
          filename: `public/logs/init-${loggerTime()}.log`,
          timestamp:'true', 
          maxsize: 10485760, //日志文件的大小
          maxFiles: 10 })
  ]});

module.exports = loggerList;

/** app.js **/
let { spawn } = require('child_process');
/**
 * 統(tǒng)一處理shell腳本執(zhí)行
 */
function handleShellFile(projectName, shellPath, res, req) {
  return resolveFile(shellPath).then(data => {
    // 判斷當(dāng)前是否是成功
    if(!data.success) {
      errorHandle(res);
    }
    let child = spawn('sh', [data.filePath])
    let buffers = [];
    child.stdout.on('data', (buffer) => {
      buffers.push(buffer);
      console.log('實(shí)時(shí)日志:', buffer.toString());
      logger[projectName] && logger[projectName].log("info", `實(shí)時(shí)日志:${buffer.toString()}`);
    })
    child.stdout.on('end',function(buffer){
      let logs = Buffer.concat(buffers, buffer).toString();
      console.log('執(zhí)行完畢');
      logger[projectName] && logger[projectName].log("info", '執(zhí)行完畢');
      res.setHeader('Content-Type', 'application/json');
      res.end(JSON.stringify({ok: true}))
    });
    child.on('close', (code) => {
      if (code !== 0) {
        console.log(`子進(jìn)程退出,退出碼 ${code}`);
      }
    });
  }, error => {
    // 錯(cuò)誤處理顯示返回
    errorHandle(res);
  })
}

shell文件介紹:

├─ docker-hook
│  ├─ sh // shell腳本文件
│  │  ├─ Archer-front-image.sh // 前端版本復(fù)制鏡像
│  │  ├─ Archer-front-remote.sh // 前端版本遠(yuǎn)程打包
│  │  ├─ Archer-front.sh // 前端版本本地打包編譯發(fā)布(本地使用)
│  │  ├─ ar-mock-image.sh // armock項(xiàng)目復(fù)制鏡像
│  │  ├─ ar-mock-remote.sh // armock項(xiàng)目遠(yuǎn)程打包
│  │  ├─ ar-mock.sh // armock項(xiàng)目本地打包編譯發(fā)布(本地使用)
│  │  ├─ env-init.sh // 環(huán)境初始化
│  │  └─ project-init.sh // git項(xiàng)目初始化,幫忙建目錄


env-init.sh可以拷貝到服務(wù)器 一鍵去部署環(huán)境

#!/bin/bash
echo 'docker 環(huán)境初始化'
function docker_install()
{
    echo "檢查Docker......"
    docker -v
  if [ $? -eq  0 ]; then
      echo "檢查到Docker已安裝!"
  else
      echo "安裝docker環(huán)境..."
      yum install -y yum-utils   device-mapper-persistent-data   lvm2
      yum-config-manager     --add-repo     https://download.docker.com/linux/centos/docker-ce.repo
      yum-config-manager --enable docker-ce-nightly #要每日構(gòu)建版本的 Docker CE
      yum-config-manager --enable docker-ce-test  
      yum install -y docker-ce docker-ce-cli containerd.io

      echo '啟動(dòng)docker'
      systemctl start docker

      echo '查看docker'
      docker version
      echo "安裝docker環(huán)境...安裝完成!"
  fi
}
# 執(zhí)行函數(shù)
docker_install

# nrm 是否安裝
function nvm_install()
{
    nvm --version
    if [ $? -eq  0 ]; then
       echo "檢查到nvm已安裝!"
       nvm install v13.14.0 #安裝最新的穩(wěn)定版本
       nvm use v13.14.0
       echo "安裝node環(huán)境...安裝完成!"
    else
      source /root/.bashrc
        echo "安裝nvm失敗..."
    fi
}

# node 是否安裝
function node_install()
{
    echo "檢查node......"
    node -v
    if [ $? -eq  0 ]; then
        echo "檢查到Node已安裝!"
    else
        echo "安裝nvm環(huán)境..."
       curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.1/install.sh | bash
       source /root/.bashrc
       nvm_install
    fi
}

# node_module庫(kù) 安裝監(jiān)測(cè)
function node_module_install()
{
      node --version
    if [ $? -eq  0 ]; then
      echo "安裝nrm源和pm2庫(kù)"
      nrm_install
      pm2_install
    else
        echo "node環(huán)境未安裝成功"
    fi
}

# nrm 安裝監(jiān)測(cè)
function nrm_install()  {
    echo "監(jiān)測(cè)nrm源..."
    nrm --version
    if [ $? -eq 0 ]; then
    echo "已安裝nrm源"
    else 
    npm i -g nrm
    nrm use taobao
    echo "安裝nrm源成功"
    fi
}

# pm2 安裝監(jiān)測(cè)
function pm2_install()  {
    echo "監(jiān)測(cè)pm2庫(kù)..."
    pm2 --version
    if [ $? -eq  0 ]; then
    echo "已安裝pm2庫(kù)"
    else 
    npm i -g pm2
    echo "安裝pm2庫(kù)成功"
    fi
}

# 執(zhí)行函數(shù)
echo '安裝node環(huán)境'
node_install
node_module_install

 如果已經(jīng)安裝過(guò)node,確認(rèn)下是否更新過(guò)~/.bash_profile,沒(méi)有則添加,也可以安裝nvm去管理node export NODE_ENV=/root/node/node-v12.16.2-linux-x64 PATH=$PATH:$HOME/bin:$NODE_ENV/bin 刷新配置文件 source ~/.bash_profile

project.sh文件主要是建立文件目錄,git clone文件并為后續(xù)的部署做準(zhǔn)備

Archer-front.sh拉代碼部署,鏡像生成,容器部署一個(gè)文件搞定

#!/bin/bash
WORK_PATH='/root/front'
cd $WORK_PATH
echo "清除老代碼"
git reset --hard origin/master
git clean -f
echo "拉取最新代碼"
git pull origin master
echo "刪除node_modules文件"
rm -rf ./node_modules
echo "重新安裝依賴"
npm i
echo "編譯打包"
npm run build
echo "開始執(zhí)行構(gòu)建"
docker build -f ./docker/Dockerfile -t archer-front:1.0 .
echo "停止舊的容器并刪除容器"
docker stop archer-front-container
docker rm archer-front-container
echo "啟動(dòng)新容器"
docker run -p 11001:11001 -v /etc/hosts:/etc/hosts --name archer-front-container -itd archer-front:1.0

那么多節(jié)點(diǎn)部署怎么辦呢?

我們可以考慮把當(dāng)前的這個(gè)鏡像導(dǎo)出并導(dǎo)入加載

Archer-front-image.sh

#!/bin/bash
echo "進(jìn)入目錄/root/images"
WORK_PATH='/root'
cd $WORK_PATH
if [ ! -d images  ];then
  mkdir images
fi
IMAGES_PATH='images'
cd $IMAGES_PATH
echo "開始拷貝前端鏡像"
docker save -o image-Archer-front.tar archer-front:1.0
echo "拷貝前端鏡像完成"

其他節(jié)點(diǎn)怎么來(lái)拿呢?可以通過(guò)scp來(lái)拷貝這邊打包出來(lái)的鏡像去使用啊,這就是Archer-front-remote.sh里面的實(shí)現(xiàn)

查看日志功能主要是通過(guò)定時(shí)刷新調(diào)用接口去實(shí)現(xiàn)的,有些low,自己使用不會(huì)有那么大的量,所以沒(méi)走實(shí)時(shí)刷新。

我們?cè)賮?lái)看看效果,是不是很香。

一次發(fā)布,終身好用


最后送大家一波福利

在這里特地講我自己這兩個(gè)月整理的相關(guān)面試題分享給大家,免費(fèi)獲取哦~

獲取方式:

一、搜索QQ群,前端學(xué)習(xí)交流群:954854084

二、點(diǎn)擊加入,與前端大牛一起進(jìn)步!

三、QQ掃描下方二維碼!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容