GVKun编程网logo

Blazor Server 应用程序中进行 HTTP 请求

15

对于想了解Docker+DockerCompose封装web应用的方法步骤的读者,本文将提供新的信息,我们将详细介绍docker封装,并且为您提供关于DockerCLI实战指南:从基础命令到Docke

对于想了解Docker+DockerCompose封装web应用的方法步骤的读者,本文将提供新的信息,我们将详细介绍docker 封装,并且为您提供关于Docker CLI 实战指南:从基础命令到 Dockerfile 构建和 Docker Compose、Docker Compose 和 Docker Swarm 和 Docker Service、Docker Compose 版本过高(Docker 版本不匹配),降低 docker-compose 版本、Docker Compose比Docker Swarm和Docker Stack有什么好处?的有价值信息。

本文目录一览:

Docker+DockerCompose封装web应用的方法步骤(docker 封装)

Docker+DockerCompose封装web应用的方法步骤(docker 封装)

这篇文章会介绍如何将后端、前端和网关通通使用 Docker 容器进行运行,并最终使用 DockerCompose 进行容器编排。

技术栈

前端

  • React
  • Ant Design

后端

  • Go
  • Iris

网关

  • Nginx
  • OpenResty
  • Lua
  • 企业微信

后端构建 api

这里虽然我们写了 EXPOSE 4182,这个只用在测试的时候,生产环境实际上我们不会将后端接口端口进行暴露,
而是通过容器间的网络进行互相访问,以及最终会使用 Nginx 进行转发。

FROM golang:1.15.5

LABEL maintainer="K8sCat <k8scat@gmail.com>"

EXPOSE 4182

ENV GOPROXY=https://goproxy.cn,direct \
    GO111MODULE=on

WORKDIR /go/src/github.com/k8scat/containerized-app/api

COPY . .

RUN go mod download && \
go build -o api main.go && \
chmod +x api

ENTRYPOINT [ "./api" ]

前端构建 web

这里值得一提的是,因为前端肯定会去调用后端接口,而且这个接口地址是根据部署而改变,
所以这里我们使用了 ARG 指令进行设置后端的接口地址,这样我们只需要在构建镜像的时候传入 --build-arg REACT_APP_BASE_URL=https://example.com/api 就可以调整后端接口地址了,而不是去改动代码。

还有一点,有朋友肯定会发现这里同时使用到了 Entrypoint 和 CMD,这是为了可以在运行的时候调整前端的端口,但实际上我们这里没必要去调整,因为这里最终也是用 Nginx 进行转发。

FROM node:lts

LABEL maintainer="K8sCat <k8scat@gmail.com>"

WORKDIR /web

COPY . .

ARG REACT_APP_BASE_URL

RUN npm config set registry https://registry.npm.taobao.org && \
npm install && \
npm run build && \
npm install -g serve

ENTRYPOINT [ "serve", "-s", "build" ]
CMD [ "-l", "3214" ]

网关构建 gateway

Nginx 配置

这里我们就分别设置了后端和前端的上游,然后设置 location 规则进行转发。
这里有几个点可以说一下:

  • 通过 set_by_lua 获取容器的环境变量,最终在运行的时候通过设置 environment 设置这些环境变量,更加灵活
  • server_name 使用到了 $hostname,运行时需要设置容器的 hostname
  • ssl_certificate 和 ssl_certificate_key 不能使用变量设置
  • 加载 gateway.lua 脚本实现企业微信的网关认证
upstream web {
    server ca-web:3214;
}

upstream api {
 server ca-api:4182;
}

server {
 set_by_lua $corp_id ''return os.getenv("CORP_ID")'';
 set_by_lua $agent_id ''return os.getenv("AGENT_ID")'';
 set_by_lua $secret ''return os.getenv("SECRET")'';
 set_by_lua $callback_host ''return os.getenv("CALLBACK_HOST")'';
 set_by_lua $callback_schema ''return os.getenv("CALLBACK_SCHEMA")'';
 set_by_lua $callback_uri ''return os.getenv("CALLBACK_URI")'';
 set_by_lua $logout_uri ''return os.getenv("LOGOUT_URI")'';
 set_by_lua $token_expires ''return os.getenv("TOKEN_EXPIRES")'';
 set_by_lua $use_secure_cookie ''return os.getenv("USE_SECURE_COOKIE")'';

 listen 443 ssl http2;
 server_name $hostname;
 resolver 8.8.8.8;
 ssl_certificate /certs/cert.crt;
 ssl_certificate_key /certs/cert.key;
 ssl_session_cache shared:SSL:1m;
 ssl_session_timeout 5m;
 ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
 ssl_ciphers AESGCM:HIGH:!aNULL:!MD5;
 ssl_prefer_server_ciphers on;
 lua_ssl_verify_depth 2;
    lua_ssl_trusted_certificate /etc/pki/tls/certs/ca-bundle.crt;

 if ($time_iso8601 ~ "^(\d{4})-(\d{2})-(\d{2})T(\d{2})") {
  set $year $1;
  set $month $2;
  set $day $3;
 }
 access_log logs/access_$year$month$day.log main;
 error_log logs/error.log;

 access_by_lua_file "/usr/local/openresty/nginx/conf/gateway.lua";

 location ^~ /gateway {
        root   html;
        index  index.html index.htm;
    }

 location ^~ /api {
        proxy_pass http://api;
        proxy_read_timeout 3600;
        proxy_http_version 1.1;
        proxy_set_header X_FORWARDED_PROTO https;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_set_header Connection "";
    }

 location ^~ / {
        proxy_pass http://web;
        proxy_read_timeout 3600;
        proxy_http_version 1.1;
        proxy_set_header X_FORWARDED_PROTO https;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_set_header Connection "";
    }

 error_page 500 502 503 504 /50x.html;
 location = /50x.html {
  root html;
 }
}

server {
 listen 80;
 server_name $hostname;

 location / {
  rewrite ^/(.*) https://$server_name/$1 redirect;
 }
}

Dockerfile

FROM openresty/openresty:1.19.3.1-centos

LABEL maintainer="K8sCat <k8scat@gmail.com>"

COPY gateway.conf /etc/nginx/conf.d/gateway.conf
COPY gateway.lua /usr/local/openresty/nginx/conf/gateway.lua
COPY nginx.conf /usr/local/openresty/nginx/conf/nginx.conf

# Install lua-resty-http
RUN /usr/local/openresty/luajit/bin/luarocks install lua-resty-http

Lua 实现基于企业微信的网关认证

这里面的一些配置参数都是通过获取 Nginx 设置的变量。

local json = require("cjson")
local http = require("resty.http")

local uri = ngx.var.uri
local uri_args = ngx.req.get_uri_args()
local scheme = ngx.var.scheme

local corp_id = ngx.var.corp_id
local agent_id = ngx.var.agent_id
local secret = ngx.var.secret
local callback_scheme = ngx.var.callback_scheme or scheme
local callback_host = ngx.var.callback_host
local callback_uri = ngx.var.callback_uri
local use_secure_cookie = ngx.var.use_secure_cookie == "true" or false
local callback_url = callback_scheme .. "://" .. callback_host .. callback_uri
local redirect_url = callback_scheme .. "://" .. callback_host .. ngx.var.request_uri
local logout_uri = ngx.var.logout_uri or "/logout"
local token_expires = ngx.var.token_expires or "7200"
token_expires = tonumber(token_expires)

local function request_access_token(code)
    local request = http.new()
    request:set_timeout(7000)
    local res, err = request:request_uri("https://qyapi.weixin.qq.com/cgi-bin/gettoken", {
        method = "GET",
        query = {
            corpid = corp_id,
            corpsecret = secret,
        },
        ssl_verify = true,
    })
    if not res then
        return nil, (err or "access token request failed: " .. (err or "unknown reason"))
    end
    if res.status ~= 200 then
        return nil, "received " .. res.status .. " from https://qyapi.weixin.qq.com/cgi-bin/gettoken: " .. res.body
    end
    local data = json.decode(res.body)
    if data["errcode"] ~= 0 then
        return nil, data["errmsg"]
    else
        return data["access_token"]
    end
end

local function request_user(access_token, code)
    local request = http.new()
    request:set_timeout(7000)
    local res, err = request:request_uri("https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo", {
        method = "GET",
        query = {
            access_token = access_token,
            code = code,
        },
        ssl_verify = true,
    })
    if not res then
        return nil, "get profile request failed: " .. (err or "unknown reason")
    end
    if res.status ~= 200 then
        return nil, "received " .. res.status .. " from https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo"
    end
    local userinfo = json.decode(res.body)
    if userinfo["errcode"] == 0 then
        if userinfo["UserId"] then
            res, err = request:request_uri("https://qyapi.weixin.qq.com/cgi-bin/user/get", {
                method = "GET",
                query = {
                    access_token = access_token,
                    userid = userinfo["UserId"],
                },
                ssl_verify = true,
            })
            if not res then
                return nil, "get user request failed: " .. (err or "unknown reason")
            end
            if res.status ~= 200 then
                return nil, "received " .. res.status .. " from https://qyapi.weixin.qq.com/cgi-bin/user/get"
            end
            local user = json.decode(res.body)
            if user["errcode"] == 0 then
                return user
            else
                return nil, user["errmsg"]
            end
        else
            return nil, "UserId not exists"
        end
    else
        return nil, userinfo["errmsg"]
    end
end

local function is_authorized()
    local headers = ngx.req.get_headers()
    local expires = tonumber(ngx.var.cookie_OauthExpires) or 0
    local user_id = ngx.unescape_uri(ngx.var.cookie_OauthUserID or "")
    local token = ngx.var.cookie_OauthAccessToken or ""
    if expires == 0 and headers["OauthExpires"] then
        expires = tonumber(headers["OauthExpires"])
    end
    if user_id:len() == 0 and headers["OauthUserID"] then
        user_id = headers["OauthUserID"]
    end
    if token:len() == 0 and headers["OauthAccessToken"] then
        token = headers["OauthAccessToken"]
    end
    local expect_token = callback_host .. user_id .. expires
    if token == expect_token and expires then
        if expires > ngx.time() then
            return true
        else
            return false
        end
    else
        return false
    end
end

local function redirect_to_auth()
    return ngx.redirect("https://open.work.weixin.qq.com/wwopen/sso/qrConnect?" .. ngx.encode_args({
        appid = corp_id,
        agentid = agent_id,
        redirect_uri = callback_url,
        state = redirect_url
    }))
end

local function authorize()
    if uri ~= callback_uri then
        return redirect_to_auth()
    end
    local code = uri_args["code"]
    if not code then
        ngx.log(ngx.ERR, "not received code from https://open.work.weixin.qq.com/wwopen/sso/qrConnect")
        return ngx.exit(ngx.HTTP_FORBIDDEN)
    end

    local access_token, request_access_token_err = request_access_token(code)
    if not access_token then
        ngx.log(ngx.ERR, "got error during access token request: " .. request_access_token_err)
        return ngx.exit(ngx.HTTP_FORBIDDEN)
    end

    local user, request_user_err = request_user(access_token, code)
    if not user then
        ngx.log(ngx.ERR, "got error during profile request: " .. request_user_err)
        return ngx.exit(ngx.HTTP_FORBIDDEN)
    end
    ngx.log(ngx.ERR, "user id: " .. user["userid"])

    local expires = ngx.time() + token_expires
    local cookie_tail = "; version=1; path=/; Max-Age=" .. expires
    if use_secure_cookie then
        cookie_tail = cookie_tail .. "; secure"
    end

    local user_id = user["userid"]
    local user_token = callback_host .. user_id .. expires

    ngx.header["Set-Cookie"] = {
        "OauthUserID=" .. ngx.escape_uri(user_id) .. cookie_tail,
        "OauthAccessToken=" .. ngx.escape_uri(user_token) .. cookie_tail,
        "OauthExpires=" .. expires .. cookie_tail,
    }
    return ngx.redirect(uri_args["state"])
end

local function handle_logout()
    if uri == logout_uri then
        ngx.header["Set-Cookie"] = "OauthAccessToken==deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"
        --return ngx.redirect("/")
    end
end

handle_logout()
if (not is_authorized()) then
    authorize()
end

使用 DockerCompose 进行容器编排

这里需要讲几个点:

  • 设置前端的 args 可以在前端构建时传入后端接口地址
  • 设置网关的 hostname 可以设置网关容器的 hostname
  • 设置网关的 environment 可以传入相关配置
  • 最终运行时只有网关层进行暴露端口
version: "3.8"

services:
  api:
    build: ./api
    image: ca-api:latest
    container_name: ca-api

  web:
    build:
      context: ./web
      args:
        REACT_APP_BASE_URL: https://example.com/api
    image: ca-web:latest
    container_name: ca-web
    
  gateway:
    build: ./gateway
    image: ca-gateway:latest
    hostname: example.com
    volumes:
      - ./gateway/certs/fullchain.pem:/certs/cert.crt
      - ./gateway/certs/privkey.pem:/certs/cert.key
    ports:
      - 80:80
      - 443:443
    environment:
      - CORP_ID=
      - AGENT_ID=
      - SECRET=
      - CALLBACK_HOST=example.com
      - CALLBACK_SCHEMA=https
      - CALLBACK_URI=/gateway/oauth_wechat
      - LOGOUT_URI=/gateway/oauth_logout
      - TOKEN_EXPIRES=7200
      - USE_SECURE_COOKIE=true
    container_name: ca-gateway

开源代码

GitHub https://github.com/k8scat/containerized-app
Gitee https://gitee.com/k8scat/containerized-app

到此这篇关于Docker+DockerCompose封装web应用的文章就介绍到这了,更多相关Docker+DockerCompose封装web应用内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!

您可能感兴趣的文章:
  • 浅谈docker-compose网络设置之networks
  • Docker Compose 网络设置详解
  • 浅析docker-compose部署mysql无法访问的问题
  • 安装docker-compose的两种最简方法
  • Docker-compose的安装和设定详细步骤
  • 详解Docker Compose 中可用的环境变量问题
  • 详解通过docker和docker-compose实现eureka高可用

Docker CLI 实战指南:从基础命令到 Dockerfile 构建和 Docker Compose

Docker CLI 实战指南:从基础命令到 Dockerfile 构建和 Docker Compose

Docker 学习路线 11:Docker命令行

Docker CLI (命令行界面) 是一个强大的工具,可让您与 Docker 容器、映像、卷和网络进行交互和管理。它为用户提供了广泛的命令,用于在其开发和生产工作流中创建、运行和管理 Docker 容器和其他 Docker 资源。

安装

要开始使用 Docker CLI,您需要在计算机上安装 Docker。您可以从 Docker 文档的官方安装指南中按照您所使用的操作系统进行安装。

基本命令

以下是一些基本的 Docker CLI 命令,供您熟悉:

  • docker run:从 Docker 映像创建并启动容器
  • docker container:列出正在运行的容器
  • docker image:列出系统中所有可用的映像
  • docker pull:从 Docker Hub 或其他注册表拉取映像
  • docker push:将映像推送到 Docker Hub 或其他注册表
  • docker build:从 Dockerfile 构建映像
  • docker exec:在正在运行的容器中运行命令
  • docker logs:显示容器的日志

Docker Run 选项

docker run 是 Docker CLI 中最重要的命令之一。您可以使用各种选项自定义容器的行为,例如:

  • d, --detach:在后台运行容器
  • e, --env:为容器设置环境变量
  • v, --volume:绑定挂载卷
  • p, --publish:将容器的端口发布到主机
  • name:为容器指定名称
  • restart:指定容器的重启策略
  • rm:容器退出时自动删除容器

Dockerfile

Dockerfile 是一个包含构建 Docker 映像的指令的脚本。您可以使用 Docker CLI 使用 Dockerfile 构建、更新和管理 Docker 映像。

以下是 Dockerfile 的一个简单示例:

# Set the base image to use
FROM alpine:3.7
# Update the system and install packages
RUN apk update && apk add curl
# Set the working directory
WORKDIR /app
# Copy the application file
COPY app.sh .
# Set the entry point
ENTRYPOINT ["./app.sh"]

要构建映像,请使用以下命令:

docker build -t my-image .

Docker Compose

Docker Compose 是一个 CLI 工具,用于使用 YAML 文件定义和管理多容器 Docker 应用程序。它与 Docker CLI 协作,提供了一种一致的方式来管理多个容器及其依赖项。

使用官方的安装指南安装 Docker Compose,然后您可以创建一个 docker-compose.yml 文件来定义和运行多容器应用程序:

version: ''3''
services:  
web:    
image: webapp-image    
ports:      - "80:80"  
database:    
image: mysql    
environment:      - MYSQL_ROOT_PASSWORD=my-secret-pw

使用以下命令运行应用程序:

docker-compose up

总之,Docker CLI 是管理 Docker 容器和资源的强大而多才多艺的工具。一旦熟悉其命令和功能,您将能够轻松开发、维护和部署使用 Docker 的应用程序。

Docker镜像

Docker镜像是一种轻量级、独立、可执行的软件包,其包含了运行应用程序所需的所有组件。这些组件包括:依赖项、库、运行时、系统工具和代码等,以确保应用程序在不同的环境中可以保持一致地运行。

Docker镜像是使用Dockerfile进行构建和管理的。Dockerfile是一个包含了创建Docker镜像所需指令的脚本,提供了一个逐步设置应用程序环境的指南。

使用Docker镜像

Docker CLI提供了多个命令来管理和使用Docker镜像。其中一些重要的命令包括:

  • docker image ls:列出本地系统上所有可用的镜像。
  • docker build:从Dockerfile构建镜像。
  • docker image rm:删除一个或多个镜像。
  • docker pull:从注册表(如Docker Hub)将镜像拉到本地系统。
  • docker push:将镜像推送到仓库。

例如,要从Docker Hub拉取官方的Ubuntu镜像,可以运行以下命令:

docker pull ubuntu:latest

拉取镜像后,可以使用docker run命令创建和运行一个使用该镜像的容器:

docker run -it ubuntu:latest /bin/bash

这个命令将创建一个新的容器,并使用**/bin/bash** shell在容器内启动一个交互式会话。

共享镜像

Docker镜像可以使用容器注册表(如Docker Hub、Google Container Registry或Amazon Elastic Container Registry(ECR))共享和分发。一旦将您的镜像推送到注册表中,其他人就可以轻松地访问和使用它们。

要共享您的镜像,您首先需要使用适当的命名格式对其进行标记:

docker tag <image-id> <username>/<repository>:<tag>

然后,您可以使用以下命令将标记的镜像推送到注册表中:

docker push <username>/<repository>:<tag>

总之,Docker镜像是Docker生态系统中不可或缺的一部分,允许开发人员打包其应用程序、轻松地共享它们,并在不同的环境中确保一致性。通过理解Docker镜像和管理它们的命令,您可以利用容器化的力量,增强您的开发工作流程。在您的开发过程中使用Docker镜像可以大大提高开发效率,减少开发过程中的问题,让您能够更好地专注于应用程序的核心开发。

容器

容器可以被视为轻量级的、独立的可执行软件包,包括运行所需的所有东西,包括代码、运行时、库、环境变量和配置文件。容器将软件与其环境隔离开来,确保其在不同环境下工作一致。

为什么使用容器?

  • 可移植性:容器确保应用程序在不同平台上一致工作,无论是开发人员的笔记本电脑还是生产服务器。这消除了“它在我的机器上运行”的问题。
  • 效率:容器很轻量级,因为它们使用共享资源,没有完整操作系统的开销。这使得启动时间更快,减少资源使用。
  • 可扩展性:容器可以根据工作量轻松地进行缩放,因此非常适合分布式应用程序和微服务。
  • 一致性:容器使得开发人员、QA 和运维团队在整个应用程序生命周期中拥有一致的环境,从而加快了部署流程。
  • 安全性:容器提供了一定程度的隔离,使其与其他容器和底层主机系统隔离开来,有助于维护应用程序的安全性。

使用 Docker CLI 工作

Docker CLI 提供了多个命令,可帮助您创建、管理和与容器交互。一些常用命令包括:

  • docker run:用于创建和启动新容器。
  • docker container ls:列出运行中的容器。
  • docker container stop:停止运行中的容器。
  • docker container rm:删除已停止的容器。
  • docker exec:在运行中的容器中执行命令。
  • docker logs:获取容器的日志,有助于调试问题。

Docker卷

Docker卷是一种用于存储Docker容器生成和使用的数据的机制。它们允许您将数据与容器本身分开,从而轻松备份、迁移和管理持久性数据。

卷的重要性

Docker容器本质上是暂时的,这意味着它们可以轻松地停止、删除或替换。尽管这对应用程序开发和部署非常有利,但处理持久性数据时会带来挑战。这就是卷的作用。它们提供了一种将数据存储和管理与容器的生命周期分开的方法。

卷的类型

Docker中有三种类型的卷:

  • 主机卷:它们存储在主机机器的文件系统中,通常位于/var/lib/docker/volumes目录中。这些可以很容易地访问,但可能会带来可移植性或文件系统兼容性问题。
  • 匿名卷:这些是在运行容器时没有指定卷时自动生成的。它们的ID由Docker生成,也存储在主机机器的文件系统中。
  • 命名卷:与匿名卷类似,命名卷也存储在主机机器的文件系统中。但是,您可以提供自定义名称,这样在其他容器中引用或备份时更容易。

使用Docker CLI管理卷

Docker CLI提供了各种命令来管理卷:

  • docker volume create: 使用给定的名称创建新卷。
  • docker volume ls: 列出系统中的所有卷。
  • docker volume inspect: 提供有关特定卷的详细信息。
  • docker volume rm: 删除卷。
  • docker volume prune: 删除所有未使用的卷。

要在容器中使用卷,可以在docker run命令期间使用-v--volume标志。例如:

docker run -d --name my-container -v my-named-volume:/var/lib/data my-image

此命令使用“my-image”映像创建一个名为“my-container”的新容器,并在容器内部将“my-named-volume”卷挂载到/var/lib/data路径。

Docker 网络

Docker 网络提供了管理容器通信的重要方法。它允许容器使用各种网络驱动程序相互通信并与主机机器通信。通过理解和利用不同类型的网络驱动程序,您可以设计容器网络以适应特定的场景或应用程序需求。

网络驱动程序

Docker 中有几个可用的网络驱动程序。这里,我们将介绍四个最常见的驱动程序:

  • bridge:容器的默认网络驱动程序。它创建了一个私有网络,容器可以相互通信并与主机机器通信。在此网络上的容器可以通过主机网络访问外部资源。
  • host:该驱动程序取消了网络隔离并允许容器共享主机的网络。在网络性能至关重要的情况下,它非常有用,因为它最小化了容器网络的开销。
  • none:该网络驱动程序禁用容器网络。使用此驱动程序的容器在没有任何网络访问的隔离环境下运行。
  • overlay:该网络驱动程序使部署在不同主机上的容器能够相互通信。它专为 Docker Swarm 设计,并且非常适合多主机或基于集群的容器部署。

管理 Docker 网络

Docker CLI 提供了各种命令来管理网络。以下是一些有用的命令:

  • 列出所有网络:docker network ls
  • 检查网络:docker network inspect <network_name>
  • 创建新网络:docker network create --driver <driver_type> <network_name>
  • 将容器连接到网络:docker network connect <network_name> <container_name>
  • 将容器与网络断开连接:docker network disconnect <network_name> <container_name>
  • 删除网络:docker network rm <network_name>

最后

为了方便其他设备和平台的小伙伴观看往期文章:

微信公众号搜索:Let us Coding,关注后即可获取最新文章推送

看完如果觉得有帮助,欢迎 点赞、收藏、关注

Docker Compose 和 Docker Swarm 和 Docker Service

Docker Compose 和 Docker Swarm 和 Docker Service

Docker Compose

介绍

通过yml文件配置,高效管理多个docker,启停

中文文档

https://www.jb51.cc/manual/view/36129.html

安装

# 慢
$ sudo curl -L "https://github.com/docker/compose/releases/download/1.27.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
# 国内镜像
$ sudo curl -L https://get.daocloud.io/docker/compose/releases/download/1.24.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose

授权文件

$ sudo chmod +x /usr/local/bin/docker-compose

测试

$ docker-compose --version
docker-compose version 1.27.4, build 1110ad01

卸载

sudo rm /usr/local/bin/docker-compose

测试使用(官方给的)

1创建文件夹

$ mkdir composetest
$ cd composetest

2写测试程序Python。app.py

import time

import redis
from flask import Flask

app = Flask(__name__)
cache = redis.Redis(host='redis', port=6379)

def get_hit_count():
    retries = 5
    while True:
        try:
            return cache.incr('hits')
        except redis.exceptions.ConnectionError as exc:
            if retries == 0:
                raise exc
            retries -= 1
            time.sleep(0.5)

@app.route('/')
def hello():
    count = get_hit_count()
    return 'Hello World! I have been seen {} times.\n'.format(count)

3创建py文件依赖文件requirements.txt

flask
redis

4创建Dockerfile文件

FROM python:3.7-alpine
workdir /code
ENV FLASK_APP=app.py
ENV FLASK_RUN_HOST=0.0.0.0
RUN apk add --no-cache gcc musl-dev linux-headers
copY requirements.txt requirements.txt
RUN pip install -r requirements.txt
EXPOSE 5000
copY . .
CMD ["flask", "run"]

5创建docker-compose.yml

# version: "3.9" 需要版本对应
version: "3"
services:
  web:
    build: .
    ports:
      - "5000:5000"
  redis:
    image: "redis:alpine"

6使用Compose构建并运行应用程序

docker-compose up
# 后台运行
# docker-compose up -d

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-87feppxX-1608950922543)(C:\Users\admin\AppData\Local\Temp\1608866566146.png)]

  1. docker images 自动下载依赖镜像
  2. 默认服务名: 文件名 _ 服务名 _ num(集群副本数量)
  3. 创建docker compose自己默认网络,并将所有启动的容器添加到网络中
  4. 在同一网络下可以直接使用域名访问

停止

# 前端运行使用 ctrl + c 停止
# 后台运行使用
docker-compose stop

yml文件规则

version: "3" # docker-compose核心版本
services: # 服务
	fw: # 服务名称
		image: redis # 服务配置
		ports: # 服务配置2
      		- "5000:5000"
      	depends_on: # 依赖(启动顺序)
      		- fw2
      		- redis
      	..... # 容器启动的所有配置
	fw2: 
		image: # 服务配置

官方文档 https://docs.docker.com/compose/compose-file/compose-file-v2/

测试开源程序(官方给的)

创建工作文件夹 my_wordpress/

创建docker-compose.yml

version: '3.3'

services:
   db:
     image: MysqL:5.7
     volumes:
       - db_data:/var/lib/MysqL
     restart: always
     environment:
       MysqL_ROOT_PASSWORD: somewordpress
       MysqL_DATABASE: wordpress
       MysqL_USER: wordpress
       MysqL_PASSWORD: wordpress

   wordpress:
     depends_on:
       - db
     image: wordpress:latest
     ports:
       - "8000:80"
     restart: always
     environment:
       wordpress_DB_HOST: db:3306
       wordpress_DB_USER: wordpress
       wordpress_DB_PASSWORD: wordpress
       wordpress_DB_NAME: wordpress
volumes:
    db_data: {}

后台运行

docker-compose up -d

Docker Swarm

  • 集群
  • 必须有两个或两个以上主节点才能运行

Swarm mode cluster

[root@centos3 ~]# docker swarm --help

Usage:  docker swarm COMMAND

Manage Swarm

Commands:
  ca          管理根CA
  init        初始化集群
  join        加入集群
  join-token  创建加入令牌
  leave       离开集群
  unlock      解锁群
  unlock-key  管理解锁密钥
  update      更新集群

Run 'docker swarm COMMAND --help' for more information on a command.

1初始化一个集群

docker swarm init [OPTIONS]
名字,简写默认描述
–advertise-addr通告地址(格式:<ip | interface>:端口)
–autolock启用管理器自动锁定(需要解锁密钥才能启动停止的管理器)
–availabilityactive节点的可用性(“活动”|“暂停”|“漏”)
–cert-expiry2160h0m0s节点证书的有效期(ns | us | ms | s | m | h)
–data-path-addr用于数据路径流量的地址或接口(格式:<ip | interface>)
–dispatcher-heartbeat5S调度员心跳周期(ns | us | ms | s | m | h)
–external-ca一个或多个证书签名端点的规格
–force-new-cluster强制从当前状态创建一个新的群集
–listen-addr0.0.0.0:2377监听地址(格式:<ip | interface>:端口)
–max-snapshots0要保留的附加木筏快照的数量
–snapshot-interval10000Raft快照之间的日志条目数
–task-history-limit5任务历史保留限制
# 初始化集群
docker swarm init --advertise-addr 192.168.0.191

2加入一个节点

docker swarm join [OPTIONS] HOST:PORT
名字,简写默认描述
–advertise-addr通告地址(格式:<ip | interface>:端口)
–availabilityactive节点的可用性(“活动”|“暂停”|“漏”)
–data-path-addr用于数据路径流量的地址或接口(格式:<ip | interface>)
–listen-addr0.0.0.0:2377监听地址(格式:<ip | interface>:端口)
–token进入群的令牌
# 例如加入一个节点
# 令牌SWMTKN-1-3pu6hszjas19xyp7ghgosyx9k8atbfcr8p2is99znpy26u2lkl-7p73s1dx5in4tatdymyhg9hu2
docker swarm join --token SWMTKN-1-3pu6hszjas19xyp7ghgosyx9k8atbfcr8p2is99znpy26u2lkl-7p73s1dx5in4tatdymyhg9hu2 192.168.0.191:2377

3获取令牌

docker swarm join-token [OPTIONS] (worker|manager)
# 工作令牌
docker swarm join-token worker
# 管理令牌
docker swarm join-token manager

查看节点

# 查看节点
docker node

Docker Service

docker service COMMAND
命令描述
docker service create创建一项新服务
docker service inspect显示一项或多项服务的详细信息
docker service logs获取服务或任务的日志
docker service ls列出服务
docker service ps列出一项或多项服务的任务
docker service rm删除一项或多项服务
docker service scale扩展一个或多个复制服务
docker service update更新服务

1创建服务

docker service create [OPTIONS] IMAGE [COMMAND] [ARG...]
名字,简写默认描述
–config指定配置以暴露给服务
–constraint展示位置限制
–container-label容器标签
–credential-spec托管服务帐户的凭证规范(仅限Windows)
–detach,-d真正立即退出,而不是等待服务收敛
–dns设置自定义DNS服务器
–dns-option设置DNS选项
–dns-search设置自定义DNS搜索域
–endpoint-mode要人端点模式(vip或dnsrr)
–entrypoint覆盖图像的默认入口点
–env,-e设置环境变量
–env-file读入环境变量文件
–group为容器设置一个或多个补充用户组
–health-cmd运行以检查运行状况的命令
–health-interval运行检查之间的时间(ms | s | m | h)
–health-retries0需要报告不健康的连续失败
–health-start-period在重新计数到不稳定(ms | s | m | h)之前,容器初始化的开始时间段
–health-timeout允许一次检查运行的最长时间(ms | s | m | h)
–host设置一个或多个自定义主机到IP映射(主机:IP)
–hostname容器主机名
–label, -l服务标签
–limit-cpu限制cpu
–limit-memory0限制记忆
–log-driver记录驱动程序的服务
–log-OPT记录驱动程序选项
–mode复制服务模式(复制或全局)
–mount将文件系统挂载附加到服务
–name服务名称
–network网络附件
–no-healthcheck禁用任何容器指定的HEALTHCHECK
–no-resolve-image不要查询注册表来解析图像摘要和支持的平台
–placement-PREF添加展示位置首选项
–publish,-p将端口发布为节点端口
–quiet,-q抑制进度输出
–read-only将容器的根文件系统挂载为只读
–replicas任务数量
–reserve-cpu预留cpu
–reserve-memory0保留内存
–restart-condition满足条件时重新启动(“none”|“on-failure”|“any”)(默认为“any”)
–restart-delay重启尝试之间的延迟(ns | us | ms | s | m | h)(默认5秒)
–restart-max-attempts放弃前的最大重启次数
–restart-window用于评估重新启动策略的窗口(ns | us | ms | s | m | h)
–rollback-delay0任务回滚之间的延迟(ns | us | ms | s | m | h)(默认值为0)
–rollback-failure-action回滚失败的操作(“暂停”|“继续”)(默认“暂停”)
–rollback-max-failure-ratio0在回滚期间容忍的失败率(默认0)
–rollback-monitor0(ns | us | ms | s | m | h)(默认5秒)每个任务回滚之后的持续时间
–rollback-order回滚顺序(“start-first”|“stop-first”)(默认“stop-first”)
–rollback-parallelism1同时回滚的任务的最大数量(0一次全部回滚)
–secret指定泄露给服务的秘密
–stop-grace-period强制杀死一个容器之前等待的时间(ns | us | ms | s | m | h)(默认10秒)
–stop-signal停止容器的信号
–tty, -t分配一个伪TTY
–update-delay0更新之间的延迟(ns | us | ms | s | m | h)(默认为0)
–update-failure-action更新失败的操作(“暂停”|“继续”|“回滚”)(默认“暂停”)
–update-max-failure-ratio0更新期间容许的失败率(默认0)
–update-monitor0(ns | us | ms | s | m | h)(默认5秒)每个任务更新后的持续时间
–update-order更新顺序(“start-first”|“stop-first”)(默认为“stop-first”)
–update-parallelism1同时更新的最大任务数(0个一次全部更新)
–user,-u用户名或UID(格式:<名称| uid>:<组| gid>)
–with-registry-auth向注册代理发送注册表认证详细信息
–workdir,-w容器内的工作目录
# 例如,启动一个Nginx服务,暴露8080端口
docker service create -p 8080:80 --name Nginx01 Nginx
# docker run 容器启动,单机版本
# docker service 服务启动,支持扩缩容器
# 查看服务
docker service ps Nginx01
# 查看副本
docker service ls
# 查看详情
docker service inspect Nginx01

2更新服务

docker service update [OPTIONS] SERVICE
名字,简写默认描述
–args服务命令参数
–config-add添加或更新服务上的配置文件
–config-RM删除配置文件
–constraint-add添加或更新展示位置约束
–constraint-RM删除约束
–container-label-add添加或更新容器标签
–container-label-rm用钥匙取出容器标签
–credential-spec托管服务帐户的凭证规范(仅限Windows)
–detach,-d立即退出,而不是等待服务收敛
–dns-add添加或更新自定义DNS服务器
–dns-option-add添加或更新DNS选项
–dns-option-rm删除一个DNS选项
–dns-rm删除自定义的DNS服务器
–dns-search-add添加或更新自定义DNS搜索域
–dns-search-rm删除一个DNS搜索域
–endpoint-mode端点模式(vip或dnsrr)
–entrypoint覆盖图像的默认入口点
–env-add添加或更新环境变量
–env-RM删除一个环境变量
–force即使没有更改需要,也强制更新
–group-add向容器添加一个附加的补充用户组
–group-RM从容器中删除先前添加的补充用户组
–health-cmd运行以检查运行状况的命令
–health-interval运行检查之间的时间(ms | s | m | h)
–health-retries0需要报告不健康的连续失败
–health-retries在重新计数到不稳定(ms | s | m | h)之前,容器初始化的开始时间段
–health-timeout允许一次检查运行的最长时间(ms | s | m | h)
–host加添加或更新自定义主机到IP映射(主机:IP)
–host-RM删除自定义的主机到IP映射(主机:IP)
–hostname容器主机名
–image服务图片标签
–label-add添加或更新服务标签
–label-RM用钥匙去除标签
–limit-cpu限制cpu
–limit-memory0限制记忆
–log-driver记录驱动程序的服务
–log-OPT记录驱动程序选项
–mount-add添加或更新服务上的装载
–mount-RM通过目标路径移除一个安装
–network加添加一个网络
–network-RM删除网络
–no-healthcheck禁用任何容器指定的HEALTHCHECK
–no-resolve-image不要查询注册表来解析图像摘要和支持的平台
–placement-PREF-ADD添加展示位置首选项
–placement-PREF-RM删除展示位置偏好设置
–publish相加添加或更新已发布的端口
–publish-RM通过目标端口删除发布的端口
–quiet,-q抑制进度输出
–read-only将容器的根文件系统挂载为只读
–replicas任务数量
–reserve-cpu预留cpu
–reserve-memory0保留内存
–restart-condition条件满足时重新启动(“none”|“on-failure”|“any”)
–restart-delay重启尝试之间的延迟(ns | us | ms | s | m | h)
–restart-max-attempts放弃前的最大重启次数
–restart-window用于评估重新启动策略的窗口(ns | us | ms | s | m | h)
–rollback回退到先前的规范
–rollback-delay0任务回滚之间的延迟(ns | us | ms | s | m | h)
–rollback-failure-action回滚失败的操作(“暂停”|“继续”)
–rollback-max-failure-ratio0在回滚期间容忍的失败率
–rollback-monitor0每个任务回滚后监视失败的持续时间(ns | us | ms | s | m | h)
–rollback-order回滚顺序(“start-first”|“stop-first”)
–rollback-parallelism0同时回滚的任务的最大数量(0一次全部回滚)
–secret-add添加或更新服务的秘密
–secret-RM去掉一个秘密
–stop-grace-period强制杀死一个容器之前的等待时间(ns | us | ms | s | m | h)
–stop-signal停止容器的信号
–tty, -t分配一个伪TTY
–update-delay0更新之间的延迟(ns | us | ms | s | m | h)
–update-failure-action更新失败的操作(“暂停”|“继续”|“回滚”)
–update-max-failure-ratio0更新期间容错的失败率
–update-monitor0(ns | us | ms | s | m | h)每个任务更新后的持续时间
–update-order更新顺序(“start-first”|“stop-first”)
–update-parallelism0同时更新的最大任务数(0个一次全部更新)
–user,-u用户名或UID(格式:<名称| uid>:<组| gid>)
–with-registry-auth向注册代理发送注册表认证详细信息
–workdir,-w容器内的工作目录
# 为 Nginx01 创建 10 个副本
docker service update --replicas 10 Nginx01 
# 或 使用 scale 命令
docker service scale Nginx01=10

移除服务

docker service rm Nginx01

Docker Compose 版本过高(Docker 版本不匹配),降低 docker-compose 版本

Docker Compose 版本过高(Docker 版本不匹配),降低 docker-compose 版本

通过 docker-compose 启动容器,报错:

ERROR: The Docker Engine version is less than the minimum required by Compose. Your current project requires a Docker Engine of version 1.10.0 or greater.

  

升级 Docker 过于麻烦,只能降 docker-compose 的版本。

先看一下我们已经安装的 Docker 版本:

[root@Redmine-186 docker-compose]# docker -v
Docker version 1.7.1, build 786b29d/1.7.1

  经查 Docker Compose Github Docs,发现 docker-compose 1.5.2 版本是兼容 Docker 1.7.1 的:Note that Compose 1.5.2 requires Docker 1.7.1 or later.

  好了,开始降级 docker-compose,先卸载:

# pip uninstall docker-compose

  

再安装指定版本:

# pip install docker-compose==1.5.2

  至此,docker-compose 降版本成功!

docker-compose.yml 版本问题

解决完 docker-compse 版本问题适配之后,对着已有的 docker-compose.yml 执行 “,会提示不能正常识别 docker-compose.yml 文件中的内容。究其原因,是因为我们的 docker-compose 1.5.2 只支持 V1 版本的 docker-compose.yml ,那么好,把现在 V2 版本的 docker-compose.yml 改成 V1 版本的格式。

V1 版本的 docker-compose.yml 只被支持到 docker-compose 1.6.x。再往后的 docker-compose 版本就不再支持 V1 版本的 docker-compose.yml。

先看文档:Compose file versions and upgrading。

V1 版本的 docker-compose.yml 文件格式主要区别就是: 
- 没有开头的 version 声明 
- 没有 services 声明 
- 不支持 depends_on 
- 不支持命名的 volumes, networks, build arguments 声明 
- 其他我没用到的所以没细究的区别

附录

  • How To Install Docker on CentOS 6
  • Docker and docker-compose in CentOS 6
  • 关于 pip 安装时提示 pkg_resources.DistributionNotFound 错误问题
  • CentOS 升级 Python2.7
  • ERROR: The Docker Engine version is less than the minimum required by Compose
  • Docker Compose Github Docs
  • Compose file versions and upgrading

Docker Compose比Docker Swarm和Docker Stack有什么好处?

Docker Compose比Docker Swarm和Docker Stack有什么好处?

从我读到的内容来看,似乎Docker-Compose是一个在单个主机上创建多个容器的工具,而Docker Swarm是一个可以完全相同但具有更多控制权的工具,并且在Docker Stack的帮助下可以在多个主机上完成.我浏览了教程,也遇到了这个帖子:

docker-compose.yml vs docker-stack.yml what difference?

而且我得出的结论是,当你可以将Docker Swarm与Docker Stack一起使用时,没有理由使用Docker-Compose.他们甚至可以使用相同的docker-compose.yml.

似乎Docker-compose出现在swarm和堆栈之前,并且可能是swarm堆栈的新解决方案使得compose过时,但它仍然是遗留原因.这个想法是否正确?如果没有,在构建开发或生产环境方面,Docker-Compose对Docker Swarm和Docker Stack有什么好处?

最佳答案

It seems that Docker-compose came before the swarm and stack and maybe the new solution of swarm + stack makes compose obsolete,but it still remains for legacy reasons. Is this thinking correct?

简而言之,是的. Compose在所有Swarm之前出现(它起源于名为fig的第三方实用程序).更糟糕的是,甚至还有两个不同的Swarms,旧的Swarm(一个是单独的工具)和Swarm模式(这些日子内置于docker二进制文件中).

它似乎正在演变为内置于Docker中的服务和部署概念.但我猜想Docker Compose和Swarm Mode部署的东西会并存一段时间.

知道Docker Compose基础是一个名为libcompose(https://github.com/docker/libcompose)的库是有益的,其他第三方实用程序使用它来支持用于部署的docker-compose.yml文件格式(参见Rancher和rancher-compose作为示例) .我想他们会努力继续支持libcompose.

我不清楚Docker Swarm部署的东西是否实际上使用了libcompose.在我粗略的搜索中,Swarm模式似乎没有实现libcompose并且做了自己的事情.我不确定这与Docker Compose和libcompose的未来有何关系.你认为合适的解释……

今天关于Docker+DockerCompose封装web应用的方法步骤docker 封装的讲解已经结束,谢谢您的阅读,如果想了解更多关于Docker CLI 实战指南:从基础命令到 Dockerfile 构建和 Docker Compose、Docker Compose 和 Docker Swarm 和 Docker Service、Docker Compose 版本过高(Docker 版本不匹配),降低 docker-compose 版本、Docker Compose比Docker Swarm和Docker Stack有什么好处?的相关知识,请在本站搜索。

最近很多小伙伴都在问Blazor Server 应用程序中进行 HTTP 请求这两个问题,那么本篇文章就来给大家详细解答一下,同时本文还将给你拓展ASP.NET Core Blazor 初探之 Blazor Server、Azure Python 函数应用程序中的 HTTP 请求、Blazor Server 和 WebAssembly 应用程序入门指南、Blazor 服务器应用程序中带有 Leaflet Markercluster 的 Mapbox等相关知识,下面开始了哦!

本文目录一览:

Blazor Server 应用程序中进行 HTTP 请求

Blazor Server 应用程序中进行 HTTP 请求

Blazor Server 应用程序中进行 HTTP 请求

翻译自 Waqas Anwar 2021年5月4日的文章 《Making HTTP Requests in Blazor Server Apps》 [1]

Blazor Server 应用使用标准的 ASP.NET Core 应用程序,在服务端执行 .NET 代码。在 Blazor Server 应用程序中,我们可以像在 ASP.NET Core Web 应用程序中那样,使用相同的方式访问任意 .NET 库或服务端功能。这其中的一项功能是,使用 HTTP Client 实例向第三方 Web API 发送 HTTP 请求。在本教程中,我将向您展示创建 HTTP Client 实例的不同方法。另外,我还会向您展示如何在 Blazor Server 应用程序中使用第三方 API 来获取和显示数据。

下载源码[2]

一、第三方 Web API 概览

我们将开发一个 Blazor Server 应用程序,该应用允许用户在 Blazor 页面组件上输入国家代码和年份,然后我们将调用第三方 API 以获取指定国家和年份的公共假期列表。我们使用的第三方 API 是Nager.Date[3],它是一个全球公共假期 API。

这是一个非常简单的 API,您可以轻松地在 Postman 中输入以下 URL 测试此 API。

https://date.nager.at/api/v2/PublicHolidays/2021/CN

该 API 的响应是 JSON 格式的公共假期列表,如下所示:

二、从 Blazor Sever 应用程序开始

在 Visual Studio 2019 中创建一个 Blazor Server 应用程序,并新建一个名为 Models 的文件夹。在 Models 文件夹中添加以下两个模型类,以映射上述 Holidays API 的请求和响应。

HolidayRequestModel.cs

public class HolidayRequestModel
{
    public string CountryCode { get; set; }
    public int Year { get; set; }
}

HolidayResponseModel.cs

public class HolidayResponseModel
{
    public string Name { get; set; }
    public string LocalName { get; set; }
    public DateTime? Date { get; set; }
    public string CountryCode { get; set; }
    public bool Global { get; set; }
}

接下来,在 Pages 文件夹中创建一个新的 Razor 组件 HolidaysExplorer.razor 和代码隐藏文件 HolidaysExplorer.razor.cs。如果您想了解有关 Razor 组件和代码隐藏文件的更多知识,可以阅读文章《Blazor 组件入门指南》。

HolidaysExplorer.razor.cs

public partial class HolidaysExplorer
{
    private HolidayRequestModel HolidaysModel = new HolidayRequestModel();
    private List<HolidayResponseModel> Holidays = new List<HolidayResponseModel>();
 
    [Inject]
    protected IHolidaysApiService HolidaysApiService { get; set; }
 
    private async Task HandleValidSubmit()
    {
        Holidays = await HolidaysApiService.GetHolidays(HolidaysModel);
    }
}

HolidaysModel 字段是 HolidayRequestModel 类的一个实例,它将帮助我们创建一个简单的表单来向用户询问国家代码和年份。下面的代码片段显示了使用 HolidaysModel 对象创建的 Blazor 表单,其中 HandleValidSubmit 方法是使用 Blazor Form OnValidSubmit 事件配置的,用户提交表单时该方法将被调用。

<EditForm Model="@HolidaysModel" OnValidSubmit="@HandleValidSubmit">
     
   <label>Country Code:</label>
   <InputText id="CountryCode" @bind-Value="HolidaysModel.CountryCode"/>
     
   <label>Year:</label>
   <InputNumber id="Year" @bind-Value="HolidaysModel.Year"/>
     
   <buttontype="submit">Submit</button>
     
</EditForm>

Holidays 列表用来显示从第三方 API 返回的假期。我们需要使用一个 @foreach 循环迭代返回的假期来生成一个简单的 bootstrap 表格。

@if (Holidays.Count > 0)
{
    <table>
       <thead>
          <tr>
             <th>Date</th>
             <th>Name</th>
             <th>Local Name</th>
             <th>Country Code</th>
             <th>Global</th>
          </tr>
       </thead>
       <tbody>
          @foreach (var item in Holidays)
          {
              <tr>
                 <td>@item.Date.Value.ToShortDateString()</td>
                 <td>@item.Name</td>
                 <td>@item.LocalName</td>
                 <td>@item.CountryCode</td>
                 <td>@item.Global</td>
              </tr>
          }
       </tbody>
    </table>
}

HolidaysExplorer.razor 视图的完整代码如下:

@page "/"
<h3>Holidays Explorer</h3>
<br />
 
<EditForm Model="@HolidaysModel" OnValidSubmit="@HandleValidSubmit">
 
   <label>Country Code:</label>
   <InputText id="CountryCode" @bind-Value="HolidaysModel.CountryCode"/>
 
   <label>Year:</label>
   <InputNumber id="Year" @bind-Value="HolidaysModel.Year"/>
 
   <buttontype="submit">Submit</button>
 
</EditForm>
 
<br />
@if (Holidays.Count > 0)
{
    <table>
       <thead>
          <tr>
             <th>Date</th>
             <th>Name</th>
             <th>Local Name</th>
             <th>Country Code</th>
             <th>Global</th>
          </tr>
       </thead>
       <tbody>
          @foreach (var item in Holidays)
          {
              <tr>
                 <td>@item.Date.Value.ToShortDateString()</td>
                 <td>@item.Name</td>
                 <td>@item.LocalName</td>
                 <td>@item.CountryCode</td>
                 <td>@item.Global</td>
              </tr>
          }
       </tbody>
    </table>
}

此时如果您运行该应用程序,您将看到一个不显示任何假期的简单 HTML 表单。这是因为方法 HandleValidSubmit 是空的,我们还未调用任何 API 来获取假期数据。

三、在 Blazor Server 应用程序中使用 IHttpClientFactory 创建 HttpClient

在 Blazor Server 应用程序中使用 HttpClient 请求第三方 API 有多种不同的方式,让我们从一个基础的示例开始,在该示例中我们使用 IHttpClientFactory 创建 HttpClient 对象。

在项目中创建一个 Services 文件夹,并创建如下的 IHolidaysApiService 接口。该接口只有一个方法 GetHolidays,它以 HolidayRequestModel 作为参数并返回 HolidayResponseModel 对象的列表。

IHolidaysApiService.cs

public interface IHolidaysApiService
{
    Task<List<HolidayResponseModel>> GetHolidays(HolidayRequestModel holidaysRequest);
}

接下来,在 Services 文件夹中创建一个 HolidaysApiService 类,实现上面的接口。

public class HolidaysApiService : IHolidaysApiService
{
    private readonly IHttpClientFactory _clientFactory;
 
    public HolidaysApiService(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }
 
    public async Task<List<HolidayResponseModel>> GetHolidays(HolidayRequestModel holidaysRequest)
    {
        var result = new List<HolidayResponseModel>();
 
        var url = string.Format("https://date.nager.at/api/v2/PublicHolidays/{0}/{1}", 
            holidaysRequest.Year, holidaysRequest.CountryCode);
 
        var request = new HttpRequestMessage(HttpMethod.Get, url);
        request.Headers.Add("Accept", "application/vnd.github.v3+json");
 
        var client = _clientFactory.CreateClient();
 
        var response = await client.SendAsync(request);
 
        if (response.IsSuccessStatusCode)
        {
            var stringResponse = await response.Content.ReadAsStringAsync();
 
            result = JsonSerializer.Deserialize<List<HolidayResponseModel>>(stringResponse,
                new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
        }
        else
        {
            result = Array.Empty<HolidayResponseModel>().ToList();
        }
 
        return result;
    }
}

在上面的 GetHolidays 方法中,我们首先为第三方 API 创建了一个 URL,并将国家代码和年份参数添加到 URL 中。

var url = string.Format("https://date.nager.at/api/v2/PublicHolidays/{0}/{1}", holidaysRequest.Year, holidaysRequest.CountryCode);

接下来,我们创建了 HttpRequestMessage 对象并配置它以向第三方 API URL 发送 HTTP GET 请求。

var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Add("Accept", "application/vnd.github.v3+json");

可以使用依赖注入 (DI) 请求一个 IHttpClientFactory,这正是我们将其注入到前面类的构造函数的原因。下面这行代码使用 IHttpClientFactory 创建了一个 HttpClient 实例。

var client = _clientFactory.CreateClient();

有了 HttpClient 对象之后,我们简单地调用它的 SendAsync 方法来发送一个 HTTP GET 请求。

var response = await client.SendAsync(request);

如果 API 调用成功,我们使用下面这行代码将其响应读取为字符串。

var stringResponse = await response.Content.ReadAsStringAsync();

最后,我们使用 JsonSerializer 类的 Deserialize 方法反序列化该响应。

result = JsonSerializer.Deserialize<List<HolidayResponseModel>>(stringResponse,
   new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });

在测试该应用程序之前,我们需要在 Startup.cs 文件中注册 HolidaysApiService 服务。我们还需要调用 AddHttpClient 方法注册 IHttpClientFactory。

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddServerSideBlazor();
 
    services.AddSingleton<IHolidaysApiService, HolidaysApiService>();
 
    services.AddHttpClient();
}

运行应用程序并在文本框中提供任意国家代码和年份。点击 Submit 按钮就会在后台调用我们的 GetHolidays 方法,然后您应该能看到如下所示的公共假期列表。

四、在 Blazor Server 应用程序中创建命名 HttpClient 对象

上面的示例适用于您正在重构现有的应用程序,希望在不影响整个应用程序的情况下,在某些方法中使用 IHttpClientFactory 创建 HttpClient 对象的场景。如果您要创建一个全新的应用程序,或者您想要将创建 HttpClient 对象的方式集中化,那么您必须使用命名 HttpClient。

下面是创建命名 HTTP 客户端的好处:

  1. 我们可以为每个 HttpClient 命名,并在应用程序启动时指定与 HttpClient 相关的所有配置,而不是将配置分散在整个应用程序当中。
  2. 我们可以只配置一次命名的 HttpClient,并多次重用它调用一个特定 API 提供者的所有 API。
  3. 我们可以根据这些客户端在应用程序不同区域的使用情况,配置多个不同配置的命名 HttpClient 对象。

我们可以在 Startup.cs 文件的 ConfigureServices 方法中,使用前面用过的名为 AddHttpClient 方法指定一个命名的 HttpClient。

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddServerSideBlazor();
 
    services.AddSingleton<IHolidaysApiService, HolidaysApiService>();
 
    services.AddHttpClient("HolidaysApi", c =>
    {
        c.BaseAddress = new Uri("https://date.nager.at/");
        c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    });
}

我们需要指定客户端的名称(例如 HolidaysApi),我们还可以配置如上所示的 BaseAddressDefaultRequestHeaders 和其他属性。

配置了命名 HttpClient 之后,我们可以使用相同的 CreateClient 方法在整个应用程序中创建 HttpClient 对象,不过这次我们需要指定想要创建哪个已命名的客户端(例如 HolidaysApi)。

HolidaysApiService.cs

public class HolidaysApiService : IHolidaysApiService
{
    private readonly IHttpClientFactory _clientFactory;
 
    public HolidaysApiService(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }
 
    public async Task<List<HolidayResponseModel>> GetHolidays(HolidayRequestModel holidaysRequest)
    {
        var result = new List<HolidayResponseModel>();
 
        var url = string.Format("api/v2/PublicHolidays/{0}/{1}", 
            holidaysRequest.Year, holidaysRequest.CountryCode);
 
        var request = new HttpRequestMessage(HttpMethod.Get, url);
 
        var client = _clientFactory.CreateClient("HolidaysApi");
 
        var response = await client.SendAsync(request);
 
        if (response.IsSuccessStatusCode)
        {
            var stringResponse = await response.Content.ReadAsStringAsync();
 
            result = JsonSerializer.Deserialize<List<HolidayResponseModel>>(stringResponse,
                new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
        }
        else
        {
            result = Array.Empty<HolidayResponseModel>().ToList();
        }
 
        return result;
    }
}

我们在 CreateClient 方法中传递的名称(比如 HolidaysApi)必须与我们在 Startup.cs 文件中配置的名称一致。每次调用 CreateClient 方法时,都会为我们创建一个新的 HttpClient 实例。

另外,我们不需要在请求的 URL 中指定 API 主机名称,因为我们在 Startup.cs 文件中已经指定过基地址了。

再次运行应用程序并提供国家代码和年份值,您应该能看到以下公共假期列表。

 

五、在 Blazor Server 应用程序中创建类型化 HttpClient 对象

创建和使用 HttpClient 对象的第三种选择是使用类型化客户端。这种客户端具有以下好处:

  1. 它们提供与命名客户端同样的功能,但无需使用字符串作为键。
  2. 它们在使用客户端时提供智能感知和编译器帮助。
  3. 它们提供了一个单一的存储单元来配置特定的 HttpClient 并与之交互。例如,我们可以配置针对 Facebook API 的一个特定终端的一个类型化 HttpClient,而且该 HttpClient 可以封装使用该特定终端所需的所有逻辑。
  4. 它们与依赖注入 (DI) 一起使用,可以在需要的地方注入。

要配置类型化的 HTTPClient,我们需要在 Startup.cs 文件中使用相同的 AddHttpClient 方法注册它,但这一次,我们需要传递我们的服务名称 HolidaysApiService 作为它的类型。

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddServerSideBlazor();
 
    services.AddSingleton<IHolidaysApiService, HolidaysApiService>();
 
    services.AddHttpClient<HolidaysApiService>();
}

在上面的代码片段中,HTTP 客户端和我们的服务 HolidaysApiService 都将注册为瞬时客户端和服务。这将允许我们在服务的构造函数中传递 HttpClient,如以下代码片段所示。请注意,HttpClient 是如何公开为服务的 public 属性的。

HolidaysApiService.cs

public class HolidaysApiService : IHolidaysApiService
{
    public HttpClient Client { get; }
 
    public HolidaysApiService(HttpClient client)
    {
        client.BaseAddress = new Uri("https://date.nager.at/");
        client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
        Client = client;
    }
 
    public async Task<List<HolidayResponseModel>> GetHolidays(HolidayRequestModel holidaysRequest)
    {
        var result = new List<HolidayResponseModel>();
 
        var url = string.Format("api/v2/PublicHolidays/{0}/{1}",
            holidaysRequest.Year, holidaysRequest.CountryCode);
 
        var response = await Client.GetAsync(url);
 
        if (response.IsSuccessStatusCode)
        {
            var stringResponse = await response.Content.ReadAsStringAsync();
 
            result = JsonSerializer.Deserialize<List<HolidayResponseModel>>(stringResponse,
                new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
        }
        else
        {
            result = Array.Empty<HolidayResponseModel>().ToList();
        }
 
        return result;
    }
}

类型化客户端的配置也可以不在类型化客户端的构造函数中指定,而在注册期间在 Startup.cs 文件的 ConfigureServices 方法中指定。

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddServerSideBlazor(); 
 
    services.AddHttpClient<IHolidaysApiService, HolidaysApiService>(c =>
    {
        c.BaseAddress = new Uri("https://date.nager.at/");
        c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    });
}

如果您使用的是这种方式,则无需单独注册您的服务。您可以从 ConfigureServices 方法中删除下面这行代码。

services.AddSingleton<IHolidaysApiService, HolidaysApiService>();

可以将 HttpClient 对象密封在一个类型化客户端中,而不公开为 public 属性。然后,我们可以在服务内部的任意方法中使用这个客户端。

public class HolidaysApiService : IHolidaysApiService
{
    private readonly HttpClient _httpClient;
 
    public HolidaysApiService(HttpClient client)
    {
        _httpClient = client;
    }
 
    public async Task<List<HolidayResponseModel>> GetHolidays(HolidayRequestModel holidaysRequest)
    {
        var result = new List<HolidayResponseModel>();
 
        var url = string.Format("api/v2/PublicHolidays/{0}/{1}",
            holidaysRequest.Year, holidaysRequest.CountryCode);
 
        var response = await _httpClient.GetAsync(url);
 
        if (response.IsSuccessStatusCode)
        {
            var stringResponse = await response.Content.ReadAsStringAsync();
 
            result = JsonSerializer.Deserialize<List<HolidayResponseModel>>(stringResponse,
                new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
        }
        else
        {
            result = Array.Empty<HolidayResponseModel>().ToList();
        }
 
        return result;
    }
}

再次运行应用程序,并提供国家代码和年份值,您应该能够看到以下公共假期列表。

 

到此这篇关于Blazor Server 应用程序中进行 HTTP 请求的文章就介绍到这了,更多相关Blazor Server内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!

您可能感兴趣的文章:
  • [Asp.Net Core]用Blazor Server Side实现图片验证码
  • [Asp.Net Core] 浅谈Blazor Server Side
  • Ant Design Blazor 组件库的路由复用多标签页功能
  • HTTP中header头部信息详解
  • Golang简单实现http的server端和client端
  • IOS利用CocoaHttpServer搭建手机本地服务器
  • Golang实现http server提供压缩文件下载功能
  • 在Golang中使用http.FileServer返回静态文件的操作
  • 基于http.server搭建局域网服务器过程解析
  • golang的httpserver优雅重启方法详解

ASP.NET Core Blazor 初探之 Blazor Server

ASP.NET Core Blazor 初探之 Blazor Server

上周初步对Blazor WebAssembly进行了初步的探索(ASP.NET Core Blazor 初探之 Blazor WebAssembly)。这次来看看Blazor Server该怎么玩。

Blazor Server

Blazor 技术又分两种:

  • Blazor WebAssembly

  • Blazor Server

Blazor WebAssembly上次已经介绍过了,这次主要来看看Blazor Server。Blazor Server 有点像WebAssembly的服务端渲染模式。页面在服务器端渲染完成之后,通过SignalR(websocket)技术传输到前端,再替换dom元素。其实不光是页面的渲染,大部分计算也是服务端完成的。Blazor Server模式可以让一些不支持WebAssembly的浏览器可以运行Blazor项目,可是问题也是显而易见的,基于SignalR的双向实时通信给网络提出了很高的要求,一旦用户量巨大,对服务端的水平扩容也带来很大的挑战,Blazor Server的用户状态都维护在服务端,这对服务端内存也造成很大的压力。
我们还是以完成一个简单的CRUD项目为目标来探究一下Blazor Server究竟是什么。因为前面Blazor Webassembly已经讲过了,相同的东西,比如数据绑定,属性绑定,事件绑定等内容就不多说了,请参见ASP.NET Core Blazor 初探之 Blazor WebAssembly。

新建Blazor Server项目

打开vs找到Blazor Server模板,看清楚了不要选成Blazor Webassembly模板。

看看生成的项目结构:

可以看到Blazor Server的项目结构跟ASP.Net Core razor pages 项目是一模一样的。看看Startup是怎么配置的:

  
    
  
  
  
  1. public class Startup

  2. {

  3. public Startup(IConfiguration configuration)

  4. {

  5. Configuration = configuration;

  6. }


  7. public IConfiguration Configuration { get; }


  8. // This method gets called by the runtime. Use this method to add services to the container.

  9. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940

  10. public void ConfigureServices(IServiceCollection services)

  11. {

  12. services.AddRazorPages();

  13. services.AddServerSideBlazor();

  14. services.AddSingleton<WeatherForecastService>();

  15. }


  16. // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.

  17. public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

  18. {

  19. if (env.IsDevelopment())

  20. {

  21. app.UseDeveloperExceptionPage();

  22. }

  23. else

  24. {

  25. app.UseExceptionHandler("/Error");

  26. // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.

  27. app.UseHsts();

  28. }


  29. app.UseHttpsRedirection();

  30. app.UseStaticFiles();


  31. app.UseRouting();


  32. app.UseEndpoints(endpoints =>

  33. {

  34. endpoints.MapBlazorHub();

  35. endpoints.MapFallbackToPage("/_Host");

  36. });

  37. }

  38. }

主要有2个地方要注意:在ConfigureServices方法里注册了Blazor的相关service:

  
    
  
  
  
  1. services.AddServerSideBlazor();

在Configure方法的终结点配置了Blazor相关的映射:

  
    
  
  
  
  1. endpoints.MapBlazorHub();

上次Blazor Webassembly我们的数据服务是通过一个Webapi项目提供的,这次不用了。如果需要提供webapi服务,Blazor Server本身就可以承载,但是Blazor Server根本不需要提供webapi服务,因为他的数据交互都是通过websocket完成的。

实现数据访问

新建student类:

  
    
  
  
  
  1. public class Student

  2. {

  3. public int Id { get; set; }

  4. public string Name { get; set; }


  5. public string Class { get; set; }


  6. public int Age { get; set; }


  7. public string Sex { get; set; }

  8. }

上次我们实现了一个StudentRepository,我们直接搬过来:

  
    
  
  
  
  1. public interface IStudentRepository

  2. {

  3. List<Student> List();


  4. Student Get(int id);


  5. bool Add(Student student);


  6. bool Update(Student student);


  7. bool Delete(int id);

  8. }

  9. }

  
    
  
  
  
  1. public class StudentRepository : IStudentRepository

  2. {

  3. private static List<Student> Students = new List<Student> {

  4. new Student{ Id=1, Name="小红", Age=10, Class="1班", Sex="女"},

  5. new Student{ Id=2, Name="小明", Age=11, Class="2班", Sex="男"},

  6. new Student{ Id=3, Name="小强", Age=12, Class="3班", Sex="男"}

  7. };


  8. public bool Add(Student student)

  9. {

  10. Students.Add(student);


  11. return true;

  12. }


  13. public bool Delete(int id)

  14. {

  15. var stu = Students.FirstOrDefault(s => s.Id == id);

  16. if (stu != null)

  17. {

  18. Students.Remove(stu);

  19. }


  20. return true;

  21. }


  22. public Student Get(int id)

  23. {

  24. return Students.FirstOrDefault(s => s.Id == id);

  25. }


  26. public List<Student> List()

  27. {

  28. return Students;

  29. }


  30. public bool Update(Student student)

  31. {

  32. var stu = Students.FirstOrDefault(s => s.Id == student.Id);

  33. if (stu != null)

  34. {

  35. Students.Remove(stu);

  36. }


  37. Students.Add(student);

  38. return true;

  39. }

  40. }

注册一下:

  
    
  
  
  
  1. services.AddScoped<IStudentRepository, StudentRepository>();

实现学生列表

跟上次一样,先删除默认生成的一些内容,减少干扰,这里不多说了。在pages文件夹下新建student文件夹,新建List.razor文件:

  
    
  
  
  
  1. @page "/student/list"


  2. @using BlazorServerDemo.Model

  3. @using BlazorServerDemo.Data


  4. @inject IStudentRepository Repository


  5. <h1>List</h1>


  6. <p class="text-right">

  7. <a class="btn btn-primary" href="/student/add">Add</a>

  8. </p>


  9. <table class="table">

  10. <tr>

  11. <th>Id</th>

  12. <th>Name</th>

  13. <th>Age</th>

  14. <th>Sex</th>

  15. <th>Class</th>

  16. <th></th>

  17. </tr>

  18. @if (_stutdents != null)

  19. {

  20. foreach (var item in _stutdents)

  21. {

  22. <tr>

  23. <td>@item.Id</td>

  24. <td>@item.Name</td>

  25. <td>@item.Age</td>

  26. <td>@item.Sex</td>

  27. <td>@item.Class</td>

  28. <td>

  29. <a class="btn btn-primary" href="/student/modify/@item.Id">修改</a>

  30. <a class="btn btn-danger" href="/student/delete/@item.Id">删除</a>

  31. </td>

  32. </tr>

  33. }

  34. }


  35. </table>


  36. @code {

  37. private List<Student> _stutdents;


  38. protected override void OnInitialized()

  39. {

  40. _stutdents = Repository.List();

  41. }

  42. }

这个页面是从上次的WebAssembly项目上复制过来的,只改了下OnInitialized方法。上次OnInitialized里需要通过Httpclient从后台获取数据,这次不需要注入HttpClient了,只要注入Repository就可以直接获取数据。
运行一下:

F12看一下这个页面是如何工作的:

首先/student/list是一次标准的Http GET请求。返回了页面的html。从返回的html代码上来看绑定的数据已经有值了,这可以清楚的证明Blazor Server技术使用的是服务端渲染技术。

_blazor?id=Fv2IGD6CfKpQFZ-fi-e1IQ连接是个websocket长连接,用来处理服务端跟客户端的数据交互。

实现Edit组件

Edit组件直接从Webassembly项目复制过来,不用做任何改动。

  
    
  
  
  
  1. @using BlazorServerDemo.Model


  2. <div>

  3. <div class="form-group">

  4. <label>Id</label>

  5. <input @bind="Student.Id" class="form-control" />

  6. </div>

  7. <div class="form-group">

  8. <label>Name</label>

  9. <input @bind="Student.Name" class="form-control" />

  10. </div>

  11. <div class="form-group">

  12. <label>Age</label>

  13. <input @bind="Student.Age" class="form-control" />

  14. </div>

  15. <div class="form-group">

  16. <label>Class</label>

  17. <input @bind="Student.Class" class="form-control" />

  18. </div>

  19. <div class="form-group">

  20. <label>Sex</label>

  21. <input @bind="Student.Sex" class="form-control" />

  22. </div>


  23. <button class="btn btn-primary" @onclick="TrySave">

  24. 保存

  25. </button>


  26. <CancelBtn Name="取消"></CancelBtn>

  27. </div>


  28. @code{


  29. [Parameter]

  30. public Student Student { get; set; }

  31. [Parameter]

  32. public EventCallback<Student> OnSaveCallback { get; set; }


  33. protected override Task OnInitializedAsync()

  34. {

  35. if (Student == null)

  36. {

  37. Student = new Student();

  38. }


  39. return Task.CompletedTask;

  40. }


  41. private void TrySave()

  42. {

  43. OnSaveCallback.InvokeAsync(Student);

  44. }

  45. }

实现新增页面

同样新增页面从上次的Webassembly项目复制过来,可以复用大量的代码,只需改改保存的代码。原来保存代码是通过HttpClient提交到后台来完成的,现在只需要注入Repository调用Add方法即可。

  
    
  
  
  
  1. @page "/student/add"


  2. @using BlazorServerDemo.Model

  3. @using BlazorServerDemo.Data


  4. @inject NavigationManager NavManager

  5. @inject IStudentRepository Repository


  6. <h1>Add</h1>


  7. <Edit Student="Student" OnSaveCallback="OnSave"></Edit>


  8. <div class="text-danger">

  9. @_errmsg

  10. </div>


  11. @code {


  12. private Student Student { get; set; }


  13. private string _errmsg;


  14. protected override Task OnInitializedAsync()

  15. {

  16. Student = new Student()

  17. {

  18. Id = 1

  19. };


  20. return base.OnInitializedAsync();

  21. }


  22. private void OnSave(Student student)

  23. {

  24. Student = student;


  25. var result = Repository.Add(student);


  26. if (result)

  27. {

  28. NavManager.NavigateTo("/student/list");

  29. }

  30. else

  31. {

  32. _errmsg = "保存失败";

  33. }

  34. }


  35. }

这里不再多讲绑定属性,绑定事件等内容,因为跟Webassembly模式是一样的,请参见上一篇。
运行一下 :

我们的页面出来了。继续F12看看页面到底是怎么渲染出来的:

这次很奇怪并没有发生任何Http请求,那么我们的Add页面是哪里来的呢,让我们继续看Websocket的消息:

客户端通过websocket给服务端发了一个消息,里面携带了一个信息:OnLocation Changed "http://localhost:59470/student/add",服务端收到消息后把对应的页面html渲染出来通过Websocket传递到前端,然后前端进行dom的切换,展示新的页面。所以这里看不到任何传统的Http请求的过程。
点一下保存看看发生了什么:

我们可以看到点击保存的时候客户端同样没有发送任何Http请求,而是通过websocket给后台发了一个消息,这个消息表示哪个按钮被点击了,后台会根据这个信息找到需要执行的方法,方法执行完后通知前端进行页面跳转。
但是这里有个问题,我们填写的数据呢?我们在文本框里填写的数据貌似没有传递到后台,这就不符合逻辑了啊。想了下有可能是文本框编辑的时候数据就提交回去了,让我们验证下:

我们一边修改文本框的内容,一边监控websocket的消息,果然发现了,当我们修改完焦点离开文本框的时候,数据直接被传递到了服务器。厉害了我的软,以前vue,angularjs实现的是前端html跟js对象的绑定技术,而Blazor Server这样就实现了前后端的绑定技术,666啊。

实现编辑跟删除页面

这个不多说了使用上面的知识点轻松搞定。
编辑页面:

  
    
  
  
  
  1. @page "/student/modify/{Id:int}"


  2. @using BlazorServerDemo.Model

  3. @using BlazorServerDemo.Data


  4. @inject NavigationManager NavManager

  5. @inject IStudentRepository Repository


  6. <h1>Modify</h1>


  7. <Edit Student="Student" OnSaveCallback="OnSave"></Edit>


  8. <div class="text-danger">

  9. @_errmsg

  10. </div>


  11. @code {

  12. [Parameter]

  13. public int Id { get; set; }


  14. private Student Student { get; set; }


  15. private string _errmsg;


  16. protected override void OnInitialized()

  17. {

  18. Student = Repository.Get(Id);

  19. }


  20. private void OnSave(Student student)

  21. {

  22. Student = student;


  23. var result = Repository.Update(student);


  24. if (result)

  25. {

  26. NavManager.NavigateTo("/student/list");

  27. }

  28. else

  29. {

  30. _errmsg = "保存失败";

  31. }

  32. }

  33. }

删除页面:

  
    
  
  
  
  1. @page "/student/delete/{Id:int}"


  2. @using BlazorServerDemo.Model

  3. @using BlazorServerDemo.Data


  4. @inject NavigationManager NavManager

  5. @inject IStudentRepository Repository


  6. <h1>Delete</h1>


  7. <h3>

  8. 确定删除(@Student.Id@Student.Name

  9. </h3>


  10. <button class="btn btn-danger" @onclick="OnDeleteAsync">

  11. 删除

  12. </button>


  13. <CancelBtn Name="取消"></CancelBtn>


  14. @code {

  15. [Parameter]

  16. public int Id { get; set; }


  17. private Student Student { get; set; }


  18. protected override void OnInitialized()

  19. {

  20. Student = Repository.Get(Id);

  21. }


  22. private void OnDeleteAsync()

  23. {

  24. var result = Repository.Delete(Id);

  25. if (result)

  26. {

  27. NavManager.NavigateTo("/student/list");

  28. }

  29. }

  30. }

运行一下:


Blazor Server总体开发体验上跟Blazor Webassembly模式保持了高度一直。虽然是两种不同的渲染模式:Webassembly是客户端渲染,Server模式是服务端渲染。但是微软通过使用websocket技术作为一层代理,巧妙隐藏了两者的差异,让两种模式开发保持了高度的一致性。Blazor Server除了第一次请求使用Http外,其他数据交互全部通过websocket技术在服务端完成,包括页面渲染、事件处理、数据绑定等,这样给Blazor Server项目的网络、内存、扩展等提出了很大的要求,在项目选型上还是要慎重考虑。

最后demo的源码:BlazorServerDemo

关注我的公众号一起玩转技术


本文分享自微信公众号 - dotNET跨平台(opendotnet)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

Azure Python 函数应用程序中的 HTTP 请求

Azure Python 函数应用程序中的 HTTP 请求

针对此问题,请检查该功能是否及时显示日志。请点击“监控”查看日志,但不要在“日志”窗口查看日志,因为日志有时不会显示在“日志”窗口中。顺便说一下,登录“监控”可能会延迟大约 5 分钟。

enter image description here

然后请检查您的功能是否超时。消费计划中的函数默认设置超时值为5分钟。因此,如果您的请求在函数中存在多个请求,并且每个请求都需要数十秒,请set functionTimeout 在“host.json”中使用 00:10:00functionTimeout 的最大值在消费计划中是 10 分钟)。

如果该函数仍然不起作用,请检查您是否在您的定时器触发函数中请求了其他 HttpTrigger 函数 url(并且 HttpTrigger 函数与您的定时器触发函数在同一个函数应用程序中)。如果是这样,可能会导致一些问题,您可以参考这个post,我过去发现了类似的问题。解决这个问题,只需要另外创建一个函数应用,将定时器触发函数和http触发函数分开即可。

Blazor Server 和 WebAssembly 应用程序入门指南

Blazor Server 和 WebAssembly 应用程序入门指南

翻译自 Waqas Anwar 2021年3月12日的文章 《A Beginner’s Guide To Blazor Server and WebAssembly Applications》 [1]

如果您一直紧跟 .NET 世界的最新发展趋势,那么现在您一定听说过 Blazor。目前在 .NET 社区中有很多关于 Blazor 的宣传,这种宣传最常见的原因是它引入了一些大多数 .NET 开发人员十几年来一直梦寐以求的东西,即:既可以在服务端又可以在浏览器中运行 C# 的能力。Blazor 允许我们使用 HTML、CSS 和 C#(而不是 JavaScript)来构建交互式 Web 应用程序。在本教程中,我将介绍 Blazor 的基本概念,并概述可用于 Blazor 的不同的托管模型。我还将介绍每种托管模型的优缺点,以便您可以为下一个 Blazor 项目托管模型做出最佳的决定。

Blazor 是什么?

Blazor 是一个免费、开源的单页应用程序(SPA)开发框架,使开发人员能够在服务端和客户端上使用 C# 构建交互式 Web 应用程序。Blazor 不需要在客户端上安装任何插件来在浏览器中执行 C#/.NET 代码。它使用 WebAssembly 执行 .NET 代码,WebAssembly 是所有主流浏览器都支持的 Web 标准。Blazor 还可以在服务端运行 .NET 代码并构建 UI,然后通过 SignalR 连接仅将更新的 DOM 传输到客户端。

WebAssembly 是什么?

WebAssembly(有时简写为 Wasm)是一种可移植的二进制格式(低级指令集),被设计用于在任何能够解释这些指令的主机上运行。WebAssembly 的主要目标是允许开发人员构建高性能的 Web 应用程序,但其格式也被设计为可执行于和集成到其他环境中。WebAssembly 目前受到了所有主流浏览器的支持,比如 Chrome、Android 版 Chrome、Edge、Firefox、Safari、Opera 等。

Blazor 托管模型

Blazor 组件模型是 Blazor 的核心,它的设计方式使计算 UI 更改和呈现 UI 彼此分离。这就是为什么无论您使用何种方式渲染您的应用程序,基本的组件模型都保持不变的原因。在撰写本文时,有四种渲染/托管模型可用,它们都处于不同的开发阶段。

  1. Blazor Server

  2. Blazor WebAssembly

  3. Blazor Electron

  4. Mobile Blazor Bindings

Blazor Electron 和 Mobile Blazor Bindings 目前处于实验阶段,Microsoft 尚未承诺发布这些托管模型,因此我不会在本文中讨论它们。

Blazor Server App 是什么?

Blazor Server 应用程序在服务器上运行,可享受完整的 .NET Core 运行时支持。所有处理都在服务器上完成,UI/DOM 更改通过 SignalR 连接回传给客户端。这种双向 SignalR 连接是在用户第一次从浏览器中加载应用程序时建立的。由于 .NET 代码已经在服务器上运行,因此您无需为前端创建 API。您可以直接访问服务、数据库等,并在传统的服务端技术上做任何您想做的事情。

§何时使用 Blazor Server

  1. 当您想在完整的 .NET Core 运行时上运行应用程序时

  2. 当您想要保持应用程序的初始下载大小非常小时

  3. 当您想保持应用启动时间非常快时

  4. 当您想把应用程序的代码保留在服务器上,而不希望它被下载到客户端时

  5. 当您想要一个快速的应用开发周期,而现有的 .NET 开发人员几乎不需要学习曲线时

  6. 当您想让您的应用对搜索引擎友好时

  7. 当您希望应用程序在旧的浏览器上运行,而不依赖于 WebAssembly 时

  8. 当您想在 Visual Studio 中像调试普通 .NET 应用程序一样调试 .NET 代码时

  9. 当您想要构建内部网或低需求的面向公众的应用程序时

§何时不要使用 Blazor Server

  1. 当您的应用程序在高延迟环境中运行时

  2. 当您希望您的应用程序离线工作,而不需要一个与服务器的固定 SignalR 连接时

  3. 当您不想增加服务器资源以响应大量连接的 SignalR 客户端时

Blazor WebAssembly App 是什么?

这种托管模型是现代流行的 SPA 框架(比如 Angular、Vue 和 React)的直接竞争对手,也是大多数开发人员有兴趣学习 Blazor 的主要原因。它允许开发人员使用 C# 取代 JavaScript 编写所有前端 UI 逻辑。在这种托管模型中,第一次请求时,会将应用程序的 DLL 及其所有依赖项和小尺寸的 Mono .NET 运行时下载到客户端。然后,客户端中的 Mono 运行时就会加载并执行应用程序代码。Blazor WebAssembly 程序可以用 C、C# 等其他语言编写,然后编译成 WebAssembly 字节码。

§何时使用 Blazor WebAssembly

  1. 当您想要将整个应用程序编译为静态文件,并将它们提供给客户端,而不需要服务器上的 .NET 运行时时。这意味着您的后端可以用 PHP、Node 或 Rails 编写,并服务于用 Blazor 编写的前端应用程序。

  2. 当您想要构建可以在客户端脱机运行而无需持续连接到服务端的应用程序时。

  3. 当您想要将处理转移到客户端并减少服务端的负载时。

  4. 当您想在客户端和服务端之间共享代码和库时。

§何时不要使用 Blazor WebAssembly

  1. 当由于下载到客户端的文件/DLL 太多,而您无法在有效负载上妥协时。

  2. 当您无法在缓慢的启动时间上(特别是在网络连接不佳的情况下)妥协时。

  3. 当您无法妥协于应用程序必须运行在具有完整的安全限制的浏览器沙箱环境中时。

为了更好地理解 Blazor 托管模型,让我们在 Visual Studio 2019 中分别创建一个 Blazor Server 和 Blazor WebAssembly 应用程序。

在 Visual Studio 2019 中创建 Blazor Server 应用

打开 Visual Studio 2019 并点击创建新项目。从可用的模板列表中选择 Blazor App 模板并点击下一步

指定项目名称(比如 BlazorServerApp)并点击下一步。您将看到下面的对话框,询问您选择要创建的 Blazor 应用程序的类型。我们要创建 Blazor Server 应用,所以请选择 Blazor Server App 并点击 创建 按钮。

Visual Studio 将为我们创建一个 Blazor Server 应用程序,其中在解决方案资源管理器中包含以下文件夹和文件。

让我们来讨论一下 Blazor Server App 中可用的一些重要文件和文件夹。

§Program.cs

这个文件包含 Main 方法,它是项目的入口点。Main 方法中调用 CreateHostBuilder 方法,为我们配置默认的 ASP.NET Core 宿主。

public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<startup>();
});
}

§Startup.cs

它与我们在标准 ASP.NET Core 项目中使用的文件相同。需要重点注意的一点是 ConfigureServices 方法中调用了 AddServerSideBlazor,该方法添加与 Blazor Server App 相关的服务。

public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddSingleton<weatherforecastservice>();
}

在此文件的 Configure 方法中我们还有以下两行重要的代码。MapBlazorHub 方法配置 Blazor Server App 所需的 SignalR Hub Endpoints。MapFallbackToPage 方法会将所有未与任何控制器、razor 页面等匹配的请求映射到 _Host 页面,这将允许所有动态内容请求路由到 SPA 框架,而不是抛出 404 Not Found

app.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});

§_Host.cshtml

这是应用程序的根页面,每个 Razor 组件/页面都将在此 host 页面中呈现。它具有基本的 HTML 元素,例如 html、head 和 body,以及一些特殊元素。请注意,Blazor 是一个基于组件的框架,Blazor 中的每一内容都是一个组件。<component> 指定了我们想让应用程序根组件呈现的位置。

<component type="typeof(App)" render-mode="ServerPrerendered">

该文件还在末尾注入了 blazor.server.js 文件,此 JavaScript 文件包含设置 SignalR 连接到服务端的代码。此连接在浏览器加载应用程序后立即建立,然后被用于服务端和客户端浏览器之间的实时通信。如果您想了解有关 SignalR 的更多知识,请阅读我的文章 Display Live Sports Updates using ASP.NET Core SignalR[2]

<script src="_framework/blazor.server.js"></script>

§App.razor

这是 Blazor App 的主要组件,其主要工作是拦截路由并呈现 Found 或 NotFound 组件。如果找到与路由匹配的组件,则呈现 Found 组件,如果未找到匹配的组件,则呈现 NotFound 组件。(由于代码大小写转换出问题,可阅读原文)

<router appassembly="@typeof(Program).Assembly" preferexactmatches="@true">
<found context="routeData">
<routeview routedata="@routeData" defaultlayout="@typeof(MainLayout)">
</routeview></found>
<notfound>
<layoutview layout="@typeof(MainLayout)">
<p>Sorry, there''s nothing at this address.</p>
</layoutview>
</notfound>
</router>

§MainLayout.cshtml

MainLayout 文件包含应用程序的主布局,其标记可以被多个 Razor 组件共享。这个布局组件通常包含应用程序的常见 UI 元素,例如页眉、菜单、页脚、侧边栏等。为我们生成的默认 MainLayout 有一个侧边栏,用来渲染 NavMenu 组件,它还使用 Razor 语法 @Body 来指定其他组件的内容将在布局标记中呈现的位置。

@inherits LayoutComponentBase

<div class="page">
<div class="sidebar">
<navmenu>
</navmenu></div>

<div class="main">
<div class="top-row px-4">
<a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
</div>

<div class="content px-4">
@Body
</div>
</div>
</div>

§wwwroot 文件夹

该文件夹包含静态文件,例如图片、字体、图标、CSS 和 JavaScript 文件等。

§Pages 和 Shared 文件夹

该文件夹包含我们之前讨论过的 _Host.cshtml 文件以及一些 Razor 组件。Blazor 应用程序是具有 .razor 扩展名的 Razor 组件的集合。其中一些组件称为可路由组件,因为可以使用其路由访问它们。例如,当我们导航到应用程序根 URL 时,将呈现下面的 Index.razor 组件。该 URL 是使用 Index.razor 组件顶部的 @page 指令指定的。

§Index.razor

@page "/"

<h1>Hello, world!</h1>

Welcome to your new app.

<surveyprompt title="How is Blazor working for you?">

请注意,上面的页面还使用了一个子组件 SurveyPrompt,之所以称它为子组件,是因为它没有 @page 指令,它可以被嵌入到其他组件中。

Pages 文件夹中还有一些其他的 razor 组件,这些组件都可以使用文件顶部指定的路径进行访问。例如,当我们导航到 /counter 路径时,Counter 组件将呈现。类似地,FetchData 组件将使用 /fetchdata 路径呈现。

Razor Server 应用程序还有一个包含共享组件的 Shared 文件夹。这些组件可以被整个应用程序中的任何组件使用,就像我们上面看到的 SurveyPrompt 组件一样。Shared 文件夹中另一个有趣的共享组件是 NavMenu 组件,它呈现 Blazor Server 应用程序的顶部导航栏。

§_Imports.razor

该文件类似于我们在 ASP.NET MVC Web 应用程序中的 _ViewImports.cshtml 文件,它包含我们可以在不同 razor 组件中使用的命名空间列表。在 _Imports.razor 文件中声明所有这些命名空间的好处是我们不需要在每个 razor 组件中重复引入它们。

@using System.Net.Http
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop

现在是时候运行我们的 Blazor Server 应用程序并在浏览器中查看它的运行情况了。在 Visual Studio 中按 F5,您将看到一个漂亮的默认 Blazor Server 应用程序。试试从侧边栏导航到不同的页面,并尝试在 Counter 页面上使用计数器,您会注意到没有页面刷新或回传到服务器。一切都像经典的 SPA 那样流畅和快速,浏览器和服务端的所有通信都是使用 SignalR 连接进行的。

您也可以打开浏览器开发者工具,您会注意到所有标准的 CSS 和 JavaScript 文件(包括 blazor.server.js 文件)都下载到了客户端,并通过 Web Sockets 建立了一个 SignalR 连接。

在 Visual Studio 2019 中 创建 Blazor WebAssembly 应用

我们已经了解了 Blazor Server App 的基础知识,并在浏览器中看到了它的运行情况。现在让我们创建一个 Blazor WebAssembly App,以便我们可以理解它们的不同之处。按照我们上面提到的相同步骤,并使用 Blazor App 模板在 Visual Studio 中创建一个新的 Blazor 应用程序。当您被询问选择 Blazor App 的类型时,这次需要选择 Blazor WebAssembly App

Visual Studio 将为我们创建一个 Blazor WebAssembly 应用程序,其中在解决方案资源管理器中包含以下文件夹和文件。

您可以轻松发现这两种类型的应用程序之间的一些差异。例如,在 Blazor WebAssembly 应用程序中没有以下文件:

  1. _Host.cshtml

  2. Error.cshtml

  3. Startup.cs

  4. appsettings.json

§index.html

在 Blazor WebAssembly 应用程序中,我们会在 wwwroot 文件夹中有一个 index.html 文件,作为主页面, 该文件在末尾注入了 blazor.webassembly.js 文件,此文件由框架提供以处理下载 .NET 运行时、Blazor 应用程序及其所有依赖项。它还包含为了运行应用而初始化运行时的代码。

§Program.cs

在 Blazor WebAssembly 应用程序中,应用程序的根组件在 Program.cs 文件中的 Main 方法中指定。应用程序的根组件是 App.razor,你可以看到它是如何被添加到 RootComponents 集合中的。

public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<app>("#app");

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

await builder.Build().RunAsync();
}
}

在 Visual Studio 中按 F5,您将看到一个相似的 Blazor WebAssembly 应用程序。尝试从侧边栏导航到不同的页面,并尝试像之前在 Blazor Server App 中所做的那样在 Counter 页面上使用计数器。一切看起来感觉一模一样,也没有服务器端回传。

正如我们已经知道的那样,Blazor WebAssembly 应用程序会在客户端下载应用程序及其所有依赖项,因此如果您打开浏览器开发者工具,会看到客户端下载了大量 DLL(只会在第一次浏览时下载)。

以上所有文件只会在第一次请求时下载,然后它们被缓存在浏览器中。如果您再次刷新页面,将会看到这一次下载的文件很少。

总结

在本文中,我试图为您介绍 Blazor SPA 框架的基本概念,我们看到了两个 Blazor 应用程序使用两种不同的托管模型进行托管。因为 Blazor 框架严重依赖于 razor 组件,所以两个项目中的大部分代码和文件都是相同的。这些组件是 Blazor 应用程序的构建块,无论使用哪种托管模型,我们都可以以相似的方式构建这些组件。如果您喜欢本文,请分享它并传播知识。


相关链接:

  1. https://www.ezzylearning.net/tutorial/a-beginners-guide-to-blazor-server-and-webassembly-applications A Beginner’s Guide To Blazor Server and WebAssembly Applications ↩︎

  2. https://www.ezzylearning.net/tutorial/display-live-sports-updates-using-asp-net-core-signalr Display Live Sports Updates using ASP.NET Core SignalR ↩︎


作者 :Waqas Anwar
译者 :技术译民
出品 :技术译站(https://ITTranslator.cn/



END



本文分享自微信公众号 - dotNET跨平台(opendotnet)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

Blazor 服务器应用程序中带有 Leaflet Markercluster 的 Mapbox

Blazor 服务器应用程序中带有 Leaflet Markercluster 的 Mapbox

如何解决Blazor 服务器应用程序中带有 Leaflet Markercluster 的 Mapbox?

我在 Blazor 服务器应用程序中尝试了这个 example,但它抛出了这个错误:

leaflet.markercluster.js:6 Uncaught ReferenceError: L is not defined
at leaflet.markercluster.js:6
Error: Microsoft.JSInterop.JSException: Cannot read property ''mapBox'' of undefined
TypeError: Cannot read property ''mapBox'' of undefined
L.mapBox.accesstoken = ''token'';
var map = L.mapBox.map(mapId)
        .setView([19.053534,47.489706],12)
        .addLayer(L.mapBox.styleLayer(''mapBox://styles/mapBox/streets-v11''));

var markers = new L.MarkerClusterGroup();

提前致谢。

解决方法

暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!

如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。

小编邮箱:dio#foxmail.com (将#修改为@)

关于Blazor Server 应用程序中进行 HTTP 请求的介绍现已完结,谢谢您的耐心阅读,如果想了解更多关于ASP.NET Core Blazor 初探之 Blazor Server、Azure Python 函数应用程序中的 HTTP 请求、Blazor Server 和 WebAssembly 应用程序入门指南、Blazor 服务器应用程序中带有 Leaflet Markercluster 的 Mapbox的相关知识,请在本站寻找。

本文标签:

上一篇uniapp项目打包为桌面应用的方法步骤(uniapp怎么打包)

下一篇HTML-Canvas的优越性能以及实际应用(html中canvas什么用)