最新公告
  • 欢迎您光临网站无忧模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Docker 部署 React 全栈应用(三)

    正文概述 掘金(娃嘎嘎)   2020-12-21   1225

    因字数限制,只能分开发,接上面 juejin.cn/post/690853…

    三、部署篇

    上面简单介绍了 docker 的一些用法,借用静态 HTML 文件与 nignx 镜像创建运行了一个容器,但是其远远不止这些,下面就通过部署本博客来作为例子再探探里面的一些知识点。

    源码地址:https://github.com/Moon-Future/react-blog,可下载下来看着目录更清晰。

    为了统一维护 docker 文件,以下将 docker 相关文件都放在各自目录下 docker 文件下,所以要特别主题构建上下文(context)的确定。

    1. 部署前台 blog

    在 blog 目录下创建 docker 目录,docker 目录下创建三个文件

    • .dockerignore:拷贝文件忽略列表
    • Dockefile
    • docker-compose.yml

    .dockerignore

    node_modules
    .next
    

    Dockefile

    # node 镜像
    # apline 版本的node会小很多
    FROM node:12-alpine
    
    # 在容器中创建目录
    RUN mkdir -p /usr/src/app
    
    # 指定工作空间,后面的指令都会在当前目录下执行
    WORKDIR /usr/src/app
    
    # 拷贝 package.json
    COPY package.json /usr/src/app
    
    # 安装依赖
    RUN npm i --production --registry=https://registry.npm.taobao.org
    
    # 拷贝其他所有文件到容器(除了 .dockerignore 中的目录和文件)
    COPY . /usr/src/app
    
    # build
    RUN npm run build
    
    # 暴露端口 9000
    EXPOSE 9000
    
    # 运行容器时执行命令,每个 Dokcerfile 只能有一个 CMD 命令,多个的话只有最后一个会执行
    CMD [ "npm", "start" ]
    

    在 blog 目录下运行以下命令可以生成镜像 react_blog:blog

    $ docker build -f docker/Dockerfile . -t react_blog:blog
    

    第一次运行安装依赖时有点慢(有个 sharp 特别慢...),刚开始我使用 node-sass 时,安装总是报错,后来索性就换成了 less,省心。如果想用 yarn 安装的话,这里 Dockerfile 里 npm 相关的命令也可以换成对于的 yarn 命令。

    漫长的等待后终于 build 成功,下面一些信息就是 npm run build 生成的文件

    Docker 部署 React 全栈应用(三)

    看看生成的镜像

    $ docker images
    REPOSITORY   TAG         IMAGE ID       CREATED         SIZE
    react_blog   blog        fef06dfed97f   3 minutes ago   329MB
    nginx        latest      ae2feff98a0c   31 hours ago    133MB
    node         12-alpine   844f8bb6a3f8   3 weeks ago     89.7MB
    

    然后来生成并运行容器

    $ docker run -itd -p 9000:9000 --name react_blog_blog react_blog:blog
    

    这里参数再说明一下:

    • -i 参数让容器的标准输入持续打开,--interactive
    • -t 参数让 Docker 分配一个伪终端,并绑定到容器的标准输入上, --tty
    • -d 参数让容器在后台,以守护进程的方式执行,--detach(Run container in background and print container ID)
    • --name 参数指定容器唯一名称,若不指定,则随机一个名称

    -it 一般同时加上,-d 参数如果不加的话,运行容器成功时,会进入一个终端命令界面,要想退出的话只能 Ctrl + C,退出之后容器也就退出了,docker ps -a 可以看到容器状态是 Exited (0) ,可以使用 docker start container 再次开启。加上 -d 的话容器就会直接在后台运行,一般的话就加上 -d。大家可以试试,之后再删除容器就可以了。

    Docker 部署 React 全栈应用(三)

    以上容器运行成功的话,在浏览器通过 服务器ip:9000 就可以访问到页面啦,Mac 或者 Windows 本地的话 localhost:9000 就可以访问啦。

    docker-compose.yml

    version: '3'
    services:
      web:
        build:
          context: ../
          dockerfile: ./docker/Dockerfile
        image: react_blog:blog
        ports:
          - 9000:9000
        container_name: react_blog_blog
    

    上面咱们通过 docker builddocker run 等命令先生成容器,再生成并运行容器,是不是有点繁琐,命令不好记,输入也麻烦,这里我们就可以利用 docker-compose 来简化执行命令。

    我们看一下文件内容:

    • web:服务名
    • build:构建相关,后面执行 docker-compose 命令路径要和 docker-compose.yml 同一路径,所以这里 context 构建上下文选择上一层源码目录,dockerfile 就是当前目录里的 Dockerfile
    • image:镜像名,如果有就直接使用,没有就通过上面的 Dockerfile 生成
    • ports:端口映射
    • container_name:容器名称,唯一。若不写,则为 当前目录_服务名_index,index 数字(从1累加),若这里为 docker_web_1

    可以把上面用 Dockerfile 生成的容器删了 docker rm -f react_blog_blog,用 docker-compose up 生成试试

    在 docker 目录下执行命令

    $ docker-compose up -d
    

    要想重新生成镜像可以 docker-compose up -d --build

    以上便把 blog 前端页面部署好了,现在只是单独部署学习,后面会删了和后台与接口一起部署。

    2. 部署后台 admin

    现在来单独部署 admin,在 Docker 篇时,我们已经使用到 admin 来简单部署学习制作镜像和生成容器,这里依然先在 admin 目录下生成生成环境静态文件

    $ npm run build
    

    在 admin 下创建 docker 目录用来存放 docker 相关文件,docker 目录下创建以下文件:

    Dockerfile

    FROM nginx
    
    # 删除 Nginx 的默认配置
    RUN rm /etc/nginx/conf.d/default.conf
    
    EXPOSE 80
    

    注意这里和上面的一些区别,

    • 这里把 nginx 默认的配置删除了,之后我们自己配置一个
    • 没有 COPY 静态文件到容器,在 docker-compose.yml 通过挂在的方式实现

    docker-compose.yml

    version: '3'
    services:
      admin:
        build: 
          context: ../
          dockerfile: ./docker/Dockerfile
        image: react_blog:admin
        ports: 
          - 9001:80
        volumes: 
          - ../build:/www
          - ./nginx.conf:/etc/nginx/conf.d/nginx.conf
        container_name: react_blog_admin
    

    这里多了 volumes (卷) 项,参数是数组,对应 宿主机文件:容器内文件

    • ../build:/www,build 内的文件挂在到容器 /www 目录下
    • ./nginx.conf:/etc/nginx/conf.d/nginx.conf,nginx.conf 挂载到容器 /etc/nginx/conf.d/nginx.conf 这个文件

    这样做的好处是,当宿主机上的文件变动后,容器内的文件也会自动变动,相应的容器内文件变动,宿主机文件也会变动。这样之后源代码变动,重新打包生成 build 后,只需要放到服务器对应目录下,容器类 /www 下的类容就会是最新的,而不需要一次次的去执行 Dockerfile 拷贝 build 文件到容器内,数据库的数据通常也是这样保存在宿主机内,而防止容器删除时丢失数据。

    同理 nginx.conf 配置文件也是一样,不过改动 nginx 配置文件后,要重启以下容器才生效 docker restart container

    来运行容器吧,在 docker 目录下执行命令

    $ docker-compose up -d
    

    查看容器是否运行成功

    $ docker ps -a
    CONTAINER ID   IMAGE          COMMAND             CREATED         STATUS          PORTS            NAMES
    7db8ce1c6814   react_blog:admin   "/docker-entrypoint.…"   16 minutes ago   Up 16 minutes   0.0.0.0:9001->80/tcp   react_blog_admin
    

    运行失败的可以 docker logs container 查看日志

    运行成功的话,在浏览器通过 服务器ip:9001 就可以访问到页面啦,Mac 或者 Windows 本地的话 localhost:9001 就可以访问啦。

    nginx.conf

    server {
      listen 80;
      sendfile on;
      sendfile_max_chunk 1M;
      tcp_nopush on;
      gzip_static on;
    
      location / {
        root /www;
        index index.html;
      }
    }
    

    root 记得和上面挂在目录相同

    3. 部署服务接口 service + Mysql

    现在我们来部署服务接口,在 service 目录下创建 docker 目录,docker 目录下创建以下文件:

    .dockerignore

    node_modules
    .github
    article
    

    article 目录用来存放博客内容文件

    Dockerfile

    FROM node:alpine
    
    # 配置环境变量
    ENV NODE_ENV production
    
    # 这个是容器中的文件目录
    RUN mkdir -p /usr/src/app 
    
    # 设置工作目录
    WORKDIR /usr/src/app
    
    # 拷贝package.json文件到工作目录
    # !!重要:package.json需要单独添加。
    # Docker在构建镜像的时候,是一层一层构建的,仅当这一层有变化时,重新构建对应的层。
    # 如果package.json和源代码一起添加到镜像,则每次修改源码都需要重新安装npm模块,这样木有必要。
    # 所以,正确的顺序是: 添加package.json;安装npm模块;添加源代码。
    COPY package.json /usr/src/app/package.json
    
    # 安装npm依赖(使用淘宝的镜像源)
    # 如果使用的境外服务器,无需使用淘宝的镜像源,即改为`RUN npm i`。
    RUN npm i --production --registry=https://registry.npm.taobao.org
    
    # 拷贝所有源代码到工作目
    COPY . /usr/src/app
    
    # 暴露容器端口
    EXPOSE 9002
    
    CMD npm start
    

    docker-compose.yml

    version: '3'
    services:
      service:
        build:
          context: ../
          dockerfile: ./docker/Dockerfile
        image: react_blog:service
        ports:
          - 9002:9002
        depends_on:
          - db
        environment:
          MYSQL_HOST: localhost
          MYSQL_USER: root
          MYSQL_PASSWORD: 8023
        volumes:
          - ../article:/usr/src/app/article
        container_name: react_blog_service
      db:
        image: mysql
        # volumes:
        #    - /db_data:/var/lib/mysql
        ports:
          - 33061:3306
        command: --default-authentication-plugin=mysql_native_password
        environment:
          MYSQL_ROOT_PASSWORD: 8023
          # MYSQL_USER: root
          MYSQL_PASSWORD: 8023
          MYSQL_DATABASE: react_blog
        container_name: react_blog_mysql
    

    注意这里有运行了两个服务 service、db

    service 服务是后端接口:

    • deponds_on:运行时会先运行 deponds_on 列表里的服务,防止依赖项还没运行,自己会报错

    • command:从 MySQL8.0 开始,默认的加密规则使用的是 caching_sha2_password,此命令可以更改加密规则。不加可能会报错 Client does not support authentication protocol requested by server; consider upgrading MySQL client

    • environment:环境变量,这里会传入代码中,在代码 /config/secret.js(secret-temp.js) 里面可以会使用到

      /**
       * secret.js 模板
       */
        
      module.exports = {
        // mysql 连接配置
        mysql: {
          host: process.env.MYSQL_HOST || 'localhost',
          port: process.env.MYSQL_PORT || '3306',
          user: process.env.MYSQL_USER || 'xxx',
          password: process.env.MYSQL_PASSWORD || 'xxx',
          database: process.env.MYSQL_DATABASE || 'xxxxxx',
        },
        // jwt
        tokenConfig: {
          privateKey: 'xxxxxxxxxxxxxxxxxxxxxxxxx',
        },
      }
      
    • volumes:这里我把文章写入宿主机了,挂载到容器里

    db 服务是 Mysql 数据库:

    • volumes:数据设置存储在宿主机

    • ports:端口映射,宿主机通过 33061 端口可以访问容器内部 Mysql,我们之后就可以通过 Navicat 或其他数据库可视化工具来连接

    • environment:配置数据库

      • MYSQL_ROOT_PASSWORD 必须要带上,设置 ROOT 账号的密码
      • MYSQL_USER 容器登录 MySQL 用户名,注意,这里如果是 root 会报错 ERROR 1396 (HY000): Operation CREATE USER failed for 'root'@'%' ,根据 github.com/docker-libr… 可知,已经存在一个 root 用户,无法再创建,所以这个可以不带,就默认 root 用户登录,如果带的话就不要是 root,会新建一个账户
      • MYSQL_PASSWORD 容器登录 Mysql 密码,对用户名 MYSQL_USER,如果是 ROOT,密码就是 MYSQL_ROOT_PASSWORD,如果是其他,就是设置新密码
      • MYSQL_DATABASE 创建一个 react_blog 数据库,也可以不填,后面再进入容器或者 Navicat 创建,但是这里因为后端代码要连接到 react_blog 数据库,不创建的会连接会保存,所以还是加上。(实在不想加也可以后见创建好数据库后,才运行两个容器)

    在 service/docker 目录下执行命令

    $ docker-compose up -d
    

    运行成功的话,看看 images 和 container

    $ docker images
    REPOSITORY   TAG         IMAGE ID       CREATED             SIZE
    react_blog   service     89139d833458   About an hour ago   150MB
    react_blog   admin       1b5d6946f1fe   32 hours ago        133MB
    react_blog   blog        fef06dfed97f   35 hours ago        329MB
    nginx        latest      ae2feff98a0c   2 days ago          133MB
    mysql        latest      ab2f358b8612   6 days ago          545MB
    node         12-alpine   844f8bb6a3f8   3 weeks ago         89.7MB
    

    可以看到多了 Mysql 和 react_blog:blog 镜像

    $ docker ps -a
    CONTAINER ID   IMAGE             COMMAND       CREATED           STATUS        PORTS                       NAMES
    5878940d7626   react_blog:blog   "docker..."   5 seconds ago     Up 4 seconds  0.0.0.0:9000->9000/tcp      react_blog_blog
    3bff0060de19   react_blog:admin  "/docker…"    3 minutes ago     Up 18 seconds 0.0.0.0:9001->80/tcp        react_blog_admin
    d8a899232e8c   react_blog:service "docker…"    About a           Exited (1) 5 minutes ago                  react_blog_service
    a9da07ff5cae   mysql              "docker…"    About an hr       33060/tcp, 0.0.0.0:33061->3306/tcp        react_blog_mysql
    

    可以看到多了 react_blog_service 和 react_blog_mysql 容器,其中 react_blog_service 容器运行失败了,显示没事失败的先别高兴,咱们来看看日志

    $ docker logs react_blog_service
    
    ...
    errno: "ECONNREFUSED"
    code: "ECONNREFUSED"
    syscall: "connect"
    address: "127.0.0.1"
    port: 3306
    fatal: true
    name: "ECONNREFUSEDError"
    pid: 47
    hostname: d8a899232e8c
    ...
    

    可以看出是数据库连接失败了,在上面 docker-compose.yml 中我们定义的环境变量 MYSQL_HOST=localhost 传给后端代码来连接数据库,每个容器都相当一一个独立的个体,localhost 是 react_blog_service 自己的 ip (127.0.0.1),当然是访问不到 react_blog_mysql,这个问题我们在下一节再来解决,先来说说 Mysql。

    上面可以看到 Mysql 容器已经成功运行,我们可以进入容器内部连接 Mysql,还记得怎么进入容器吗

    $ docker exec -it react_blog_mysql /bin/sh
    
    $ ls
    bin  boot  dev	docker-entrypoint-initdb.d  entrypoint.sh  etc	home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
    $ mysql -uroot -p
    Enter password: 
    Welcome to the MySQL monitor.  Commands end with ; or \g.
    Your MySQL connection id is 12
    Server version: 8.0.22 MySQL Community Server - GPL
    
    Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.
    
    Oracle is a registered trademark of Oracle Corporation and/or its
    affiliates. Other names may be trademarks of their respective
    owners.
    
    Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
    
    mysql> 
    
    

    可以看到顺利连接 Mysql,输入 exit 可以退出容器。我们也可以使用可视化工具来连接,我这里使用 Navicat 来连接

    Docker 部署 React 全栈应用(三)

    Docker 部署 React 全栈应用(三)

    注意这里的端口 33061,上面我们通过端口映射,通过宿主机端口 33061 可以访问到 Mysql 容器内端口 3306,所以就连接上啦。

    4. 容器互联

    上面留了一个问题,service 连接数据库失败,现在我们来尝试解决。参考 Docker 筑梦师系列(一):实现容器互联

    4.1 Network 类型

    Network,顾名思义就是 “网络”,能够让不同的容器之间相互通信。首先有必要要列举一下 Docker Network 的五种驱动模式(driver):

    • bridge:默认的驱动模式,即 “网桥”,通常用于单机(更准确地说,是单个 Docker 守护进程)
    • overlay:Overlay 网络能够连接多个 Docker 守护进程,通常用于集群,后续讲 Docker Swarm 的文章会重点讲解
    • host:直接使用主机(也就是运行 Docker 的机器)网络,仅适用于 Docker 17.06+ 的集群服务
    • macvlan:Macvlan 网络通过为每个容器分配一个 MAC 地址,使其能够被显示为一台物理设备,适用于希望直连到物理网络的应用程序(例如嵌入式系统、物联网等等)
    • none:禁用此容器的所有网络

    默认情况下,创建的容器都在 bridge 网络下,如下如所示,各个容器通过 dokcer0 可连接到宿主机HOST,并且各自分配到 IP,这种情况下,容器间互相访问需要输入对方的 IP 地址去连接。

    Docker 部署 React 全栈应用(三)

    查看 network 列表

    $ docker network ls
    NETWORK ID     NAME             DRIVER    SCOPE
    a75e040b03ed   bridge           bridge    local
    13545e6a3970   docker_default   bridge    local
    5ec462838a1c   host             host      local
    c726e6887f10   none             null      local
    

    这里有 4 的 network,默认本来只有 3 个,没有 docker_default,我也是写到这里才发现创建了一个 docker_default 网络,查找官网(Networking in Compose)才发现,通过 docker-compose 来生成运行容器时,如果没指定 network,会自动创建一个 network,包含当前 docker-compose.yml 下的所有容器,network 名字默认为 目录_default ,这里目录就是 docker 恰好我们这个几个 docker-compose.yml 都是放在 docker 目录下,所以创建的几个容器都是在 docker_default 网络里。可以一下命令查看网络详细信息

    $ docker network inspect docker_default
    
    [
        {
            "Name": "docker_default",
            "Id": "13545e6a39708344b363b7fc16eefeb6775c37773222804ebd5b5fb6f28c38bb",
            "Created": "2020-12-16T11:03:37.2152073+08:00",
            "Scope": "local",
            "Driver": "bridge",
            "EnableIPv6": false,
            "IPAM": {
                "Driver": "default",
                "Options": null,
                "Config": [
                    {
                        "Subnet": "172.24.0.0/16",
                        "Gateway": "172.24.0.1"
                    }
                ]
            },
            "Internal": false,
            "Attachable": true,
            "Ingress": false,
            "ConfigFrom": {
                "Network": ""
            },
            "ConfigOnly": false,
            "Containers": {
                "23891d43187e046eea25936dc0ab703964cc6c7213bb150ae9529da3e2e57662": {
                    "Name": "react_blog_mysql",
                    "EndpointID": "649857f928e0444500cfd296035869678bf26162d429a4499b262776b2a1d264",
                    "MacAddress": "02:42:ac:18:00:03",
                    "IPv4Address": "172.24.0.3/16",
                    "IPv6Address": ""
                },
                "3bff0060de19fc973039c07c1931e2c1efe30c6707bcd77d2ff7ea4dc01aaf63": {
                    "Name": "react_blog_admin",
                    "EndpointID": "25d8fa518b0ce27498f562372c3424aee174cb1d8fbf9f2445f1c6af8e6aab7f",
                    "MacAddress": "02:42:ac:18:00:02",
                    "IPv4Address": "172.24.0.2/16",
                    "IPv6Address": ""
                },
                "5878940d7626a9fb20622cde4002075e390e5036036bafb99d80454d6cba594b": {
                    "Name": "react_blog_blog",
                    "EndpointID": "a3f8ee36eda09f524be7ea16a67a1e13e62cf558e5480218bb523f877d478e4a",
                    "MacAddress": "02:42:ac:18:00:04",
                    "IPv4Address": "172.24.0.4/16",
                    "IPv6Address": ""
                }
            },
            "Options": {},
            "Labels": {
                "com.docker.compose.network": "default",
                "com.docker.compose.project": "docker",
                "com.docker.compose.version": "1.25.1"
            }
        }
    ]
    

    可以看到 docker_default 网关地址为 172.24.0.1 ,其他几个容器 IP 分别为 172.24.0.3172.24.0.2172.24.0.4,所以这里的情况是这样的

    Docker 部署 React 全栈应用(三)

    上面说了默认网络 bridge 下容器见访问只能输入 IP 地址来连接,而自定义的网络还可以通过容器名来连接

    这就可以避免每次生成容器 IP 会变的问题了。知道了这些,我们在 service 接口里就可已通过 react_blog_mysql 来连接 react_blog_mysql 容器了,service/docker/docker-compose.yml 修改如下:

    version: '3'
    services:
      service:
        build:
          context: ../
          dockerfile: ./docker/Dockerfile
        image: react_blog:service
        ports:
          - 9002:9002
        restart: on-failure
        depends_on:
          - db
        environment:
          MYSQL_HOST: react_blog_mysql # 此处 localhost 换为 mysql 容器名,在同一个自定义网络下,变会自动解析为 IP 连接
          MYSQL_USER: root
          MYSQL_PASSWORD: 8023
        volumes:
          - ./article:/usr/src/app/article
        container_name: react_blog_service
      db:
        image: mysql
        ports:
          - 33061:3306
        restart: on-failure
        command: --default-authentication-plugin=mysql_native_password
        environment:
          MYSQL_ROOT_PASSWORD: 8023
          MYSQL_PASSWORD: 8023
          MYSQL_DATABASE: react_blog
        container_name: react_blog_mysql
    

    在此运行命令

    $ docker-compose up -d --build
    

    Docker 部署 React 全栈应用(三)

    可以看到服务容器已正常运行,docker logs react_blog_service 查看日志也没有报错,说明已经连接数数据库,在代码你我加了一个 get 测试接口,在浏览器输入 IP:9002/api/test/get 或者 localhost:9002/api/test/get,会返回一个 json 对象

    {"message":"Hello You Got It"}
    

    这里我试了 N 久,一直有问题,

    • Mysql 创建失败,environment 我加了一个 MYSQL_USER: root,结果一直报错 ERROR 1396 (HY000): Operation CREATE USER failed for 'root'@'%' ,根据 github.com/docker-libr… 可知,已经存在一个 root 用户,无法再创建,所以这个可以不带,就默认 root 用户登录,如果带的话就不要是 root,会新建一个账户。这里直接去掉 MYSQL_USER,使用 root 登录

    • service 创建失败,日志报错没连接上 Mysql,我试试了好久,最后发现重启一下 service docker start react_blog_service 就可以了,所以我觉得应该是 Mysql 创建好后,数据口等一些配置还没搞好,所以 service 还连接不上,就一直报错,等一会重新运行 service 就好了,所以这里加上了 restart 参数,报错就重新启动,这样就不用自己去重启了,等一会,看日志没问题,就是连接成功了。

      明明使用了 depends_on,为什么还会有这种问题呢,我也不太清楚,不过官网有这段示例:

      version: "3.9"
      services:
        web:
          build: .
          depends_on:
            - db
            - redis
        redis:
          image: redis
        db:
          image: postgres
      

    我们再来看看 docker_default 网络

    $ docker network inspect docker_default
    
    [
        {
            "Name": "docker_default",
            "Id": "13545e6a39708344b363b7fc16eefeb6775c37773222804ebd5b5fb6f28c38bb",
            "Created": "2020-12-16T11:03:37.2152073+08:00",
            "Scope": "local",
            "Driver": "bridge",
            "EnableIPv6": false,
            "IPAM": {
                "Driver": "default",
                "Options": null,
                "Config": [
                    {
                        "Subnet": "172.24.0.0/16",
                        "Gateway": "172.24.0.1"
                    }
                ]
            },
            "Internal": false,
            "Attachable": true,
            "Ingress": false,
            "ConfigFrom": {
                "Network": ""
            },
            "ConfigOnly": false,
            "Containers": {
                "3bff0060de19fc973039c07c1931e2c1efe30c6707bcd77d2ff7ea4dc01aaf63": {
                    "Name": "react_blog_admin",
                    "EndpointID": "25d8fa518b0ce27498f562372c3424aee174cb1d8fbf9f2445f1c6af8e6aab7f",
                    "MacAddress": "02:42:ac:18:00:02",
                    "IPv4Address": "172.24.0.2/16",
                    "IPv6Address": ""
                },
                "5878940d7626a9fb20622cde4002075e390e5036036bafb99d80454d6cba594b": {
                    "Name": "react_blog_blog",
                    "EndpointID": "a3f8ee36eda09f524be7ea16a67a1e13e62cf558e5480218bb523f877d478e4a",
                    "MacAddress": "02:42:ac:18:00:04",
                    "IPv4Address": "172.24.0.4/16",
                    "IPv6Address": ""
                },
                "83005eec8d50071a6c23a2be4af8552983c09c532e937f04d79f02f8eb68acc9": {
                    "Name": "react_blog_mysql",
                    "EndpointID": "265ed7793c98287a05ccf8997e81671287a02ee8ea464984996083a34abe10dd",
                    "MacAddress": "02:42:ac:18:00:03",
                    "IPv4Address": "172.24.0.3/16",
                    "IPv6Address": ""
                },
                "937339a37ce726e704ec21b31b4028a97967a00de01438557e5a60d8538a51c8": {
                    "Name": "react_blog_service",
                    "EndpointID": "934d26f32a2b23e2cb4691020cb93d26c97b9647108047b492c3f7dd2be6faef",
                    "MacAddress": "02:42:ac:18:00:05",
                    "IPv4Address": "172.24.0.5/16",
                    "IPv6Address": ""
                }
            },
            "Options": {},
            "Labels": {
                "com.docker.compose.network": "default",
                "com.docker.compose.project": "docker",
                "com.docker.compose.version": "1.25.1"
            }
        }
    ]
    

    可以看到 react_blog_service 也已正常加入网络,IP 为 172.24.0.5

    4.2 自定义 Network

    docker_default 网络是根据目录来创建的,恰巧我们这几个项目 docker-compose.yml 文件都放在 docker 目录下,所以都在一个网络,如果名称变了就不在一个网络,并且之后项目可能还会有 docker 目录,全部都在一个网络也是不太好的,所以这里我们来自定义本次项目的网络。

    blog/docker/docker-compose.yml

    version: '3'
    services:
      blog:
        build:
          context: ../
          dockerfile: ./docker/Dockerfile
        image: react_blog:blog
        ports:
          - 9000:9000
        networks: 
          - react_blog
        container_name: react_blog_blog
    networks: 
      react_blog:
    

    admin/docker/docker-compose.yml

    version: '3'
    services:
      admin:
        build:
          context: ../
          dockerfile: ./docker/Dockerfile
        image: react_blog:admin
        ports:
          - 9001:80
        volumes:
          - ../build:/www
          - ./nginx.conf:/etc/nginx/conf.d/nginx.conf
        networks:
          - react_blog
        container_name: react_blog_admin
    networks:
      react_blog:
    

    service/docker/docker-compose.yml

    version: '3'
    services:
      service:
        build:
          context: ../
          dockerfile: ./docker/Dockerfile
        image: react_blog:service
        ports:
          - 9002:9002
        depends_on:
          - db
        environment:
          - MYSQL_HOST=react_blog_mysql # 此处 localhost 换为 mysql 容器名,在同一个自定义网络下,变会自动解析为 IP 连接
          - MYSQL_USER=root
          - MYSQL_PASSWORD=8023
        volumes:
          - ./article:/usr/src/app/article
        networks:
          - react_blog
        container_name: react_blog_service
      db:
        image: mysql
        ports:
          - 33061:3306
        command: --default-authentication-plugin=mysql_native_password
        environment:
          - MYSQL_ROOT_PASSWORD=8023
          - MYSQL_USER=root
          - MYSQL_PASSWORD=8023
          - MYSQL_DATABASE=react_blog
        networks:
          - react_blog
        container_name: react_blog_mysql
    networks:
      react_blog:
    
    • 与services 同级的 networks:创建一个新的 network,这里生成的 network 最终名称也会加上目录名,docker_react_blog。
    • 服务内部的 networks:加入哪些网络,参数带 “-” 说明是数组,可以加入多个网络,这里我们全部加入 react_blog,不分前后端了

    注意:

    这样在 dockor-compose.yml 里生成的 network 都会加上当前目录名,若想不带,可以自己先生成一个

    $ docker network create my_net
    

    然后在 dockor-compose.yml 里

    version: '3'
    services:
      service:
        build:
          context: ../
          dockerfile: ./docker/Dockerfile
        image: react_blog:service
        ports:
          - 9002:9002
        depends_on:
          - db
        environment:
          - MYSQL_HOST=react_blog_mysql # 此处 localhost 换为 mysql 容器名,在同一个自定义网络下,变会自动解析为 IP 连接
          - MYSQL_USER=root
          - MYSQL_PASSWORD=8023
        volumes:
          - ./article:/usr/src/app/article
        networks:
          - my_net
        container_name: react_blog_service
      db:
        image: mysql
        ports:
          - 33061:3306
        command: --default-authentication-plugin=mysql_native_password
        environment:
          - MYSQL_ROOT_PASSWORD=8023
          - MYSQL_USER=root
          - MYSQL_PASSWORD=8023
          - MYSQL_DATABASE=react_blog
        networks:
          - my_net
        container_name: react_blog_mysql
    networks:
      my_net:
      	external: true
    

    加个 external 参数则使用已经创建的 network(my_net),不会再去创建或加上目录名。

    我们再来重新创建容器,先删除全部容器

    $ docker stop $(docker ps -aq) 
    $ docker rm $(docker ps -aq)
    

    在进入各个目录分别执行 docker-compose up -d,在运行第一个时会看到 Creating network "docker_react_blog" with the default driver 这句话,说明创建了一个新的 network,我们来看看

    $ docker network ls
    NETWORK ID     NAME                DRIVER    SCOPE
    a75e040b03ed   bridge              bridge    local
    13545e6a3970   docker_default      bridge    local
    e1ceb437a4fd   docker_react_blog   bridge    local
    5ec462838a1c   host                host      local
    c726e6887f10   none                null      local
    
    $ docker network inspect docker_react_blog
    [
        {
            "Name": "docker_react_blog",
            "Id": "e1ceb437a4fdc5de91e51ff8831e21b565c92754159ad7057de36758e548a92f",
            "Created": "2020-12-19T01:39:02.201644444+08:00",
            "Scope": "local",
            "Driver": "bridge",
            "EnableIPv6": false,
            "IPAM": {
                "Driver": "default",
                "Options": null,
                "Config": [
                    {
                        "Subnet": "172.18.0.0/16",
                        "Gateway": "172.18.0.1"
                    }
                ]
            },
            "Internal": false,
            "Attachable": true,
            "Ingress": false,
            "ConfigFrom": {
                "Network": ""
            },
            "ConfigOnly": false,
            "Containers": {
                "00da404f6f050b9b2f20e39bbb136fef614e8dfee85ec31bd6000bfd59cc2dab": {
                    "Name": "react_blog_mysql",
                    "EndpointID": "1cb966cc731eca3e9721e6d3edcfcac6152b66051faa934557f567e9e36c75c6",
                    "MacAddress": "02:42:ac:12:00:04",
                    "IPv4Address": "172.18.0.4/16",
                    "IPv6Address": ""
                },
                "ad1480e48e8e7ed160b1d4bcf7eed77d74505aea7581d48d8931206772b5d805": {
                    "Name": "react_blog_service",
                    "EndpointID": "8866c3457382d6baa945da09aef40da54c7dfdea0f393485001c35bb37d201a0",
                    "MacAddress": "02:42:ac:12:00:05",
                    "IPv4Address": "172.18.0.5/16",
                    "IPv6Address": ""
                },
                "b518d40b5021d3fdec7b7e62fbaa47b8a705a38346ccba2b9814174e46b67cd0": {
                    "Name": "react_blog_admin",
                    "EndpointID": "9a58ff20dc57d4d1fa6af83482051a68e80e22a5e37cf8e0cb3570b78102f107",
                    "MacAddress": "02:42:ac:12:00:03",
                    "IPv4Address": "172.18.0.3/16",
                    "IPv6Address": ""
                },
                "db0050257a8e8a0fa430ea04b009ae819dbf04ef001cf1027ec2b5565403b48e": {
                    "Name": "react_blog_blog",
                    "EndpointID": "664794ed292871bc7fd8e1c4eaa56f682a6be5d653209f84158f3334a4f30660",
                    "MacAddress": "02:42:ac:12:00:02",
                    "IPv4Address": "172.18.0.2/16",
                    "IPv6Address": ""
                }
            },
            "Options": {},
            "Labels": {
                "com.docker.compose.network": "react_blog",
                "com.docker.compose.project": "docker",
                "com.docker.compose.version": "1.25.1"
            }
        }
    ]
    

    4.3 调用接口

    现在还有一个问题,我们在代码中调用接口形式是 http://localhost:9002/api/xxx ,在 react_blog_blog 容器中调用接口 localhost 是本身自己,没有调到 react_blog_service 里面的接口。

    针对 admin

    在代码中,我们这样来调接口

    const HOST = process.env.NODE_ENV === 'development' ? 'http://localhost:9002' : ''
    
    const API = {
      getArticleList: HOST + '/api/getArticleList',
      getArticle: HOST + '/api/getArticle',
      addArticle: HOST + '/api/addArticle',
      delArticle: HOST + '/api/delArticle',
    
      getTagList: HOST + '/api/getTagList',
      addTag: HOST + '/api/addTag',
      delTag: HOST + '/api/delTag',
    
      register: HOST + '/api/register',
      login: HOST + '/api/login',
    }
    
    export default API
    

    Docker 部署 React 全栈应用(三)

    会发现接口 404,我们通过 nginx 来代理接口请求

    admin/docker/nginx.conf

    server {
      listen 80;
      sendfile on;
      sendfile_max_chunk 1M;
      tcp_nopush on;
      gzip_static on;
    
      location /api {
        proxy_pass http://react_blog_service:9002;
      }
    
      location / {
        root /www;
        index index.html;
      }
    }
    

    以 /api 为开头的请求,我们都转发到 react_blog_service 容器 9002 端口,将 nginx.conf 拖到服务器,因为我们是将此文件挂载到容器内部的,所以这里只需要重启一下容器

    $ docker restart react_blog_admin
    

    再看看请求接口,可以看到请求 200 成功,返回数据,如果返回 500,说明数据库还没建表,将目录下 react_blog.sql 导入数据库就可以了。

    Docker 部署 React 全栈应用(三)

    针对 blog

    开始我以为通过环境变量(Next 中要存储在运行时变量里 Runtime Configuration)来传递请求 HOST (react_blog_service || localhost) ,但发现 react_blog_service 直接拼在前端接口里访问是不可行的(getServerSideProps 可行),所以最后还是改为 nginx 来代理请求,并且后面我们肯定还是要通过域名来访问网站的,所以还是需要 nginx,那么我们就为前台页面来加一个 nginx 容器。

    1、创建环境变量

    blog/docker/Dockerfile

    # node 镜像
    # apline 版本的node会小很多
    FROM node:12-alpine
    
    # 在容器中创建目录
    RUN mkdir -p /usr/src/app
    
    # 指定工作空间,后面的指令都会在当前目录下执行
    WORKDIR /usr/src/app
    
    # 拷贝 package.json
    COPY package.json /usr/src/app
    
    # 安装依赖
    RUN npm i --production --registry=https://registry.npm.taobao.org
    
    # 拷贝其他所有文件到容器(除了 .dockerignore 中的目录和文件)
    COPY . /usr/src/app
    
    # build 阶段获取
    ENV HOST react_blog_service ## 增加一个环境变量,在 build 阶段可获取到,一定放在 npm run build 前一行
    
    # build
    RUN npm run build
    
    # 暴露端口 9000
    EXPOSE 9000
    
    # 运行容器时执行命令,每个 Dokcerfile 只能有一个 CMD 命令,多个的话只有最后一个会执行
    CMD [ "npm", "start" ]
    

    代码中,设置运行是变量 blog/next.config.js

    const withCSS = require('@zeit/next-css')
    const withLess = require('@zeit/next-less')
    module.exports = () =>
      withLess({
        ...withCSS(),
        // 改为 nginx 代理
        publicRuntimeConfig: {
          HOST: process.env.HOST || 'localhost', // 如果是 docker build,此处 process.env.HOST,否则就 localhsot,不影响本地运行
        },
      })
    

    blog/config/api

    import getConfig from 'next/config'
    const { publicRuntimeConfig } = getConfig()
    
    const SSRHOST = `http://${publicRuntimeConfig.HOST}:9002`
    const HOST = `http://localhost:9002`
    
    export const SSRAPI = {
      getArticleList: SSRHOST + '/api/getArticleList',
      getArticle: SSRHOST + '/api/getArticle',
    }
    
    export const API = {
      getArticleList: HOST + '/api/getArticleList',
      getArticle: HOST + '/api/getArticle',
    }
    

    这里有点麻烦,我不知道我的理解对不对,但试了多种情况只有这种本地和 docker 部署才都可以。

    • 如果是本地运行(不使用 docker),服务端获取数据(getServerSideProps)和页面中获取数据直接使用服务接口地址(localhost:9002)即可

    • 如果是 docker 运行,服务端获取数据(getServerSideProps)需要直接带上服务接口容器地址,无法通过 nginx 代理,页面中获取数据调用接口则职能通过 nginx 代理的方式

    2、nginx 代理

    修改 blog/docker/docker-compose.yml,增加一个 nginx 容器

    version: '3'
    services:
      blog:
        build:
          context: ../
          dockerfile: ./docker/Dockerfile
        image: react_blog:blog
        # ports:
        #   - 9000:9000
        networks:
          - react_blog
        container_name: react_blog_blog
      nginx:
        build:
          context: ../
          dockerfile: ./docker/Dockerfile-nginx
        image: react_blog:nginx
        ports:
          - 9000:80
        volumes:
          - ./nginx.conf:/etc/nginx/conf.d/nginx.conf
        networks:
          - react_blog
        container_name: react_blog_nginx
    networks:
      react_blog:
    

    blog/docker/Dockerfile-nginx

    FROM nginx
    
    # 删除 Nginx 的默认配置
    RUN rm /etc/nginx/conf.d/default.conf
    
    EXPOSE 80
    

    blog/docker/nginx.conf

    server {
      listen 80;
      sendfile on;
      sendfile_max_chunk 1M;
      tcp_nopush on;
      gzip_static on;
    
      location /api {
        proxy_pass http://react_blog_service:9002;
      }
    
      location / {
        proxy_pass http://react_blog_blog:9000;
      }
    }
    

    3、生成容器

    因为 blog 的内容有变,所以需要重新生成镜像,使用 docker-compose up -d --build 会重新下载 npm node_modules,比较慢,所以还是先生成镜像。

    在 blog 目录下执行

    $ docker build -f docker/Dockerfile . -t react_blog:blog
    

    在 blog/docker 下执行

    $ docker-compose up -d
    

    运行成功的话,再试试接口就可以获取数据啦。

    5. 连接宿主机 Mysql

    上面遇到一个问题,在上面过程中,我的服务器(宿主机)上的 Mysql 出现了问题,连接时报错 2013 lost connection to mysql server at 'reading initial communication packet',我也不知道是什么原因引起的,解决方式是运行命令 systemctl start mysqld.service 启动 Mysql 服务,也不知是哪理影响到了。因为之前其他项目都是单独部署的,没使用 docker,数据都在宿主机 Mysql 上,所以我还是跟倾向于统一管理,自适应宿主机一个 Mysql,下面来看看怎么实现吧。

    这里有两种方式

    方式一:network_mode: host

    修改 service/docker/docker-compose.yml

    version: '3'
    services:
      service:
        build:
          context: ../
          dockerfile: ./docker/Dockerfile
        image: react_blog:service
        ports:
          - 9002:9002
        restart: on-failure
        # depends_on:
        #   - db
        environment:
          # MYSQL_HOST: react_blog_mysql # 此处 localhost 换为 mysql 容器名,在同一个自定义网络下,变会自动解析为 IP 连接
          MYSQL_USER: root
          MYSQL_PASSWORD: 8023
        volumes:
          - ../article:/usr/src/app/article
        network_mode: host
        # networks:
        #   - react_blog
        container_name: react_blog_service
      # db:
      #   image: mysql
      #   ports:
      #     - 33061:3306
      #   restart: on-failure
      #   command: --default-authentication-plugin=mysql_native_password
      #   environment:
      #     MYSQL_ROOT_PASSWORD: 8023
      #     MYSQL_PASSWORD: 8023
      #     MYSQL_DATABASE: react_blog
      #   networks:
      #     - react_blog
      #   container_name: react_blog_mysql
    networks:
      react_blog:
    

    service/docker 下执行命令

    $ docker-compose up -d
    
    $ docker ps -a
    CONTAINER ID   IMAGE                COMMAND                  CREATED          STATUS          PORTS    NAMES
    af9e525e7d14   react_blog:service   "docker-entrypoint.s…"   28 seconds ago   Up 26 seconds            react_blog_service
    

    可以看到 service 运行正常,且没有端口映射,docker inspect react_blog_service 也没有分配 IP,这种就相当于一个 Node 应用自己连接到宿主机 Mysql。但是对于页面接口请求来说,因为 react_blog_service 已不在 docker_react_blog ,所以就要使用宿主机 IP 地址来访问了。

    nginx.conf

    server {
      listen 80;
      sendfile on;
      sendfile_max_chunk 1M;
      tcp_nopush on;
      gzip_static on;
    
      location /api {
        # proxy_pass http://react_blog_service:9002;
        proxy_pass http://xxx.xx.xxx.x:9002; # xxx.xx.xxx.x 为宿主机(服务器)IP 
      }
    
      location / {
        proxy_pass http://react_blog_blog:9000;
      }
    }
    

    服务端渲染接口也是一样

    # node 镜像
    # apline 版本的node会小很多
    FROM node:12-alpine
    
    # 在容器中创建目录
    RUN mkdir -p /usr/src/app
    
    # 指定工作空间,后面的指令都会在当前目录下执行
    WORKDIR /usr/src/app
    
    # 拷贝 package.json
    COPY package.json /usr/src/app
    
    # 安装依赖
    RUN npm i --production --registry=https://registry.npm.taobao.org
    
    # 拷贝其他所有文件到容器(除了 .dockerignore 中的目录和文件)
    COPY . /usr/src/app
    
    # build 阶段获取,xxx.xx.xxx.x 为宿主机(服务器)IP
    ENV HOST xxx.xx.xxx.x
    
    # build
    RUN npm run build
    
    # 暴露端口 9000
    EXPOSE 9000
    
    # 运行容器时执行命令,每个 Dokcerfile 只能有一个 CMD 命令,多个的话只有最后一个会执行
    CMD [ "npm", "start" ]
    

    这种方式是不是很麻烦,还要暴露服务器 IP 地址,所以我选择方式二

    方式二:

    修改 service/docker/docker-compose.yml

    version: '3'
    services:
      service:
        build:
          context: ../
          dockerfile: ./docker/Dockerfile
        image: react_blog:service
        ports:
          - 9002:9002
        restart: on-failure
        # depends_on:
        #   - db
        environment:
          MYSQL_HOST: 172.17.0.1
          MYSQL_USER: root
          MYSQL_PASSWORD: 8023
        volumes:
          - ../article:/usr/src/app/article
        networks:
          - react_blog
        container_name: react_blog_service
      # db:
      #   image: mysql
      #   ports:
      #     - 33061:3306
      #   restart: on-failure
      #   command: --default-authentication-plugin=mysql_native_password
      #   environment:
      #     MYSQL_ROOT_PASSWORD: 8023
      #     MYSQL_PASSWORD: 8023
      #     MYSQL_DATABASE: react_blog
      #   networks:
      #     - react_blog
      #   container_name: react_blog_mysql
    networks:
      react_blog:
    

    这里 MYSQL_HOST 为 172.17.0.1,上面也说了,容器可以通过此 IP 来连接到宿主机,所以这就连接上宿主机的 Mysql 了,其他的地方就不需要改了。

    6. 一个 docker-compoer.yml

    前面用了 3 个 docker-compose.yml 来启动各自的项目,还是挺繁琐的,我们来写一个汇总的,一个命令运行所以,当然后面某一个项目需要重新跑,也可以进入各自目录去运行自己的 docker-compose.yml

    在项目根目录创建 docker/docker-compose.yml,创建 docker 目录,是为了创建的 network 和单个项目运行是创建的一致

    version: '3'
    services:
      blog:
        build:
          context: ../blog
          dockerfile: ./docker/Dockerfile
        image: react_blog:blog
        networks:
          - react_blog
        container_name: react_blog_blog
      nginx:
        build:
          context: ../blog
          dockerfile: ./docker/Dockerfile-nginx
        image: react_blog:nginx
        ports:
          - 9000:80
        volumes:
          - ../blog/docker/nginx.conf:/etc/nginx/conf.d/nginx.conf
        networks:
          - react_blog
        container_name: react_blog_nginx
      admin:
        build:
          context: ../admin
          dockerfile: ./docker/Dockerfile
        image: react_blog:admin
        ports:
          - 9001:80
        volumes:
          - ../admin/build:/www
          - ../admin/docker/nginx.conf:/etc/nginx/conf.d/nginx.conf
        networks:
          - react_blog
        container_name: react_blog_admin
      service:
        build:
          context: ../service
          dockerfile: ./docker/Dockerfile
        image: react_blog:service
        ports:
          - 9002:9002
        restart: on-failure
        environment:
          MYSQL_HOST: 172.17.0.1
          MYSQL_USER: root
          MYSQL_PASSWORD: 8023
        volumes:
          - ../service/article:/usr/src/app/article
        networks:
          - react_blog
        container_name: react_blog_service
    networks:
      react_blog:
    

    停止并删除之前创建的所有容器

    $ docker stop $(docker ps -aq)
    $ docker rm $(docker ps -aq)
    

    进入 /docker 目录执行,

    $ docker-compose up -d
    Building nginx
    Step 1/3 : FROM nginx
     ---> ae2feff98a0c
    Step 2/3 : RUN rm /etc/nginx/conf.d/default.conf
     ---> Running in bb163c42c6b5
    Removing intermediate container bb163c42c6b5
     ---> 282cb303dddf
    Step 3/3 : EXPOSE 80
     ---> Running in 9b77ebd39952
    Removing intermediate container 9b77ebd39952
     ---> fbb18dda70af
    Successfully built fbb18dda70af
    Successfully tagged react_blog:nginx
    WARNING: Image for service nginx was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
    Building admin
    Step 1/3 : FROM nginx
     ---> ae2feff98a0c
    Step 2/3 : RUN rm /etc/nginx/conf.d/default.conf
     ---> Using cache
     ---> 282cb303dddf
    Step 3/3 : EXPOSE 80
     ---> Using cache
     ---> fbb18dda70af
    Successfully built fbb18dda70af
    Successfully tagged react_blog:admin
    WARNING: Image for service admin was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
    Creating react_blog_admin   ... done
    Creating react_blog_service ... done
    Creating react_blog_blog    ... done
    Creating react_blog_nginx   ... done
    
    $ docker ps -a
    CONTAINER ID   IMAGE      			COMMAND    CREATED          STATUS         PORTS                    NAMES
    1fbb15abdd30   react_blog:service   "docker"   13 seconds ago   Up 6 seconds   0.0.0.0:9002->9002/tcp   react_blog_service
    fbee53e25c3a   react_blog:admin     "/docker"  13 seconds ago   Up 6 seconds   0.0.0.0:9001->80/tcp     react_blog_admin
    70cb25f87d14   react_blog:blog      "docker"   13 seconds ago   Up 6 seconds   9000/tcp                 react_blog_blog
    aa9fbf2afea4   react_blog:nginx     "/docker"  13 seconds ago   Up 6 seconds   0.0.0.0:9000->80/tcp     react_blog_nginx
    

    运行成功~

    7. 域名

    我现在是通过宿主机的 nginx 来代理域名访问 IP:9000,然后访问到 react_blog_nginx 容器,本想是直接在 react_blog_nginx 中做代理,但是试了没成功。想了想,访问 react_blog_nginx 是通过端口映射,宿主IP:9000 访问到的,如果在 react_blog_nginx 内部配置域名,总感觉是无法访问,这点还没想过,这几天再试试。

    结语

    终于写完了,写之前已经学习尝试了好久,以为很有把握了,结果在写的过程中又遇到一堆问题,一个问题可能都会卡好久天,各种百度,Google,油管都用上啦,总算解决了遇到的所有问题,当然这些问题可能只满足了我现在的部署需求,其中还有很多知识点,没有接触到,不过没关系,我就是想成功部署前端项目就可以了。

    以上便是 docker 部署前端项目的所有笔记,内容比较啰嗦,希望能帮助后来的同学少走一点坑,因为有些是自己的理解,可能会有错误,还请大家指正,互相学习,over。


    下载网 » Docker 部署 React 全栈应用(三)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元