分類
發燒車訊

Spring Boot 集成 Swagger 構建接口文檔

在應用開發過程中經常需要對其他應用或者客戶端提供 RESTful API 接口,尤其是在版本快速迭代的開發過程中,修改接口的同時還需要同步修改對應的接口文檔,這使我們總是做着重複的工作,並且如果忘記修改接口文檔,就可能造成不必要的麻煩。

為了解決這些問題,Swagger 就孕育而生了,那讓我們先簡單了解下。

Swagger 簡介

Swagger 是一個規範和完整的框架,用於生成、描述、調用和可視化 RESTful 風格的 Web 服務

總體目標是使客戶端和文件系統作為服務器,以同樣的速度來更新。文件的方法、參數和模型緊密集成到服務器端的代碼中,允許 API 始終保持同步。

下面我們在 Spring Boot 中集成 Swagger 來構建強大的接口文檔。

Spring Boot 集成 Swagger

Spring Boot 集成 Swagger 主要分為以下三步:

  1. 加入 Swagger 依賴
  2. 加入 Swagger 文檔配置
  3. 使用 Swagger 註解編寫 API 文檔

加入依賴

首先創建一個項目,在項目中加入 Swagger 依賴,項目依賴如下所示:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>

加入配置

接下來在 config 包下創建一個 Swagger 配置類 Swagger2Configuration,在配置類上加入註解 @EnableSwagger2,表明開啟 Swagger,注入一個 Docket 類來配置一些 API 相關信息,apiInfo() 方法內定義了幾個文檔信息,代碼如下:

@Configuration
@EnableSwagger2
public class Swagger2Configuration {

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                // swagger 文檔掃描的包
                .apis(RequestHandlerSelectors.basePackage("com.wupx.interfacedoc.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("測試接口列表")
                .description("Swagger2 接口文檔")
                .version("v1.0.0")
                .contact(new Contact("wupx", "https://www.tianheyu.top", "wupx@qq.com"))
                .license("Apache License, Version 2.0")
                .licenseUrl("http://www.apache.org/licenses/LICENSE-2.0.html")
                .build();
    }
}

列舉其中幾個文檔信息說明下:

  • title:接口文檔的標題
  • description:接口文檔的詳細描述
  • termsOfServiceUrl:一般用於存放公司的地址
  • version:API 文檔的版本號
  • contact:維護人、維護人 URL 以及 email
  • license:許可證
  • licenseUrl:許可證 URL

編寫 API 文檔

domain 包下創建一個 User 實體類,使用 @ApiModel 註解表明這是一個 Swagger 返回的實體,@ApiModelProperty 註解表明幾個實體的屬性,代碼如下(其中 getter/setter 省略不显示):

@ApiModel(value = "用戶", description = "用戶實體類")
public class User {

    @ApiModelProperty(value = "用戶 id", hidden = true)
    private Long id;

    @ApiModelProperty(value = "用戶姓名")
    private String name;

    @ApiModelProperty(value = "用戶年齡")
    private String age;

    // getter/setter
}

最後,在 controller 包下創建一個 UserController 類,提供用戶 API 接口(未使用數據庫),代碼如下:

@RestController
@RequestMapping("/users")
@Api(tags = "用戶管理接口")
public class UserController {

    Map<Long, User> users = Collections.synchronizedMap(new HashMap<>());

    @GetMapping("/")
    @ApiOperation(value = "獲取用戶列表", notes = "獲取用戶列表")
    public List<User> getUserList() {
        return new ArrayList<>(users.values());
    }

    @PostMapping("/")
    @ApiOperation(value = "創建用戶")
    public String addUser(@RequestBody User user) {
        users.put(user.getId(), user);
        return "success";
    }

    @GetMapping("/{id}")
    @ApiOperation(value = "獲取指定 id 的用戶")
    @ApiImplicitParam(name = "id", value = "用戶 id", paramType = "query", dataTypeClass = Long.class, defaultValue = "999", required = true)
    public User getUserById(@PathVariable Long id) {
        return users.get(id);
    }

    @PutMapping("/{id}")
    @ApiOperation(value = "根據 id 更新用戶")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "id", value = "用戶 id", defaultValue = "1"),
            @ApiImplicitParam(name = "name", value = "用戶姓名", defaultValue = "wupx"),
            @ApiImplicitParam(name = "age", value = "用戶年齡", defaultValue = "18")
    })
    public User updateUserById(@PathVariable Long id, @RequestParam String name, @RequestParam Integer age) {
        User user = users.get(id);
        user.setName(name);
        user.setAge(age);
        return user;
    }

    @DeleteMapping("/{id}")
    @ApiOperation(value = "刪除用戶", notes = "根據 id 刪除用戶")
    @ApiImplicitParam(name = "id", value = "用戶 id", dataTypeClass = Long.class, required = true)
    public String deleteUserById(@PathVariable Long id) {
        users.remove(id);
        return "success";
    }
}

啟動項目,訪問 http://localhost:8080/swagger-ui.html,可以看到我們定義的文檔已經在 Swagger 頁面上显示了,如下圖所示:

到此為止,我們就完成了 Spring Boot 與 Swagger 的集成。

同時 Swagger 除了接口文檔功能外,還提供了接口調試功能,以創建用戶接口為例,單擊創建用戶接口,可以看到接口定義的參數、返回值、響應碼等,單擊 Try it out 按鈕,然後點擊 Execute 就可以發起調用請求、創建用戶,如下圖所示:

註解介紹

由於 Swagger 2 提供了非常多的註解供開發使用,這裏列舉一些比較常用的註解。

@Api

@Api 用在接口文檔資源類上,用於標記當前類為 Swagger 的文檔資源,其中含有幾個常用屬性:

  • value:定義當前接口文檔的名稱。
  • description:用於定義當前接口文檔的介紹。
  • tag:可以使用多個名稱來定義文檔,但若同時存在 tag 屬性和 value 屬性,則 value 屬性會失效。
  • hidden:如果值為 true,就會隱藏文檔。

@ApiOperation

@ApiOperation 用在接口文檔的方法上,主要用來註解接口,其中包含幾個常用屬性:

  • value:對API的簡短描述。
  • note:API的有關細節描述。
  • esponse:接口的返回類型(注意:這裏不是返回實際響應,而是返回對象的實際結果)。
  • hidden:如果值為 true,就會在文檔中隱藏。

@ApiResponse、@ApiResponses

@ApiResponses 和 @ApiResponse 二者配合使用返回 HTTP 狀態碼。@ApiResponses 的 value 值是 @ApiResponse 的集合,多個 @ApiResponse 用逗號分隔,其中 @ApiResponse 包含的屬性如下:

  • code:HTTP狀態碼。
  • message:HTTP狀態信息。
  • responseHeaders:HTTP 響應頭。

@ApiParam

@ApiParam 用於方法的參數,其中包含以下幾個常用屬性:

  • name:參數的名稱。
  • value:參數值。
  • required:如果值為 true,就是必傳字段。
  • defaultValue:參數的默認值。
  • type:參數的類型。
  • hidden:如果值為 true,就隱藏這個參數。

@ApiImplicitParam、@ApiImplicitParams

二者配合使用在 API 方法上,@ApiImplicitParams 的子集是 @ApiImplicitParam 註解,其中 @ApiImplicitParam 註解包含以下幾個參數:

  • name:參數的名稱。
  • value:參數值。
  • required:如果值為 true,就是必傳字段。
  • defaultValue:參數的默認值。
  • dataType:數據的類型。
  • hidden:如果值為 true,就隱藏這個參數。
  • allowMultiple:是否允許重複。

@ResponseHeader

API 文檔的響應頭,如果需要設置響應頭,就將 @ResponseHeader 設置到 @ApiResponseresponseHeaders 參數中。@ResponseHeader 提供了以下幾個參數:

  • name:響應頭名稱。
  • description:響應頭備註。

@ApiModel

設置 API 響應的實體類,用作 API 返回對象。@ApiModel 提供了以下幾個參數:

  • value:實體類名稱。
  • description:實體類描述。
  • subTypes:子類的類型。

@ApiModelProperty

設置 API 響應實體的屬性,其中包含以下幾個參數:

  • name:屬性名稱。
  • value:屬性值。
  • notes:屬性的註釋。
  • dataType:數據的類型。
  • required:如果值為 true,就必須傳入這個字段。
  • hidden:如果值為 true,就隱藏這個字段。
  • readOnly:如果值為 true,字段就是只讀的。
  • allowEmptyValue:如果為 true,就允許為空值。

到此為止,我們就介紹完了 Swagger 提供的主要註解。

總結

Swagger 可以輕鬆地整合到 Spring Boot 中構建出強大的 RESTful API 文檔,可以減少我們編寫接口文檔的工作量,同時接口的說明內容也整合入代碼中,可以讓我們在修改代碼邏輯的同時方便的修改接口文檔說明,另外 Swagger 也提供了頁面測試功能來調試每個 RESTful API。

如果項目中還未使用,不防嘗試一下,會發現效率會提升不少。

本文的完整代碼在 https://github.com/wupeixuan/SpringBoot-Learn 的 interface-doc 目錄下。

最好的關係就是互相成就,大家的在看、轉發、留言三連就是我創作的最大動力。

參考

http://swagger.io

https://github.com/wupeixuan/SpringBoot-Learn

《Spring Boot 2 實戰之旅》

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

網頁設計最專業,超強功能平台可客製化

分類
發燒車訊

一次找出範圍內的所有素數,埃式篩法是什麼神仙算法?

本文始發於個人公眾號:TechFlow,原創不易,求個關注

今天這篇是算法與數據結構專題的第23篇文章,我們繼續數論相關的算法,來看看大名鼎鼎的埃式篩法。

我們都知道在數學領域,素數非常重要,有海量的公式和研究關於素數,比如那個非常著名至今沒有人解出來的哥德巴赫猜想。和數學領域一樣,素數在信息領域也非常重要,有着大量的應用。舉個簡單的例子,很多安全加密算法也是利用的質數。我們想要利用素數去進行各種計算之前,總是要先找到素數。所以這就有了一個最簡單也最不簡單的問題,我們怎麼樣來尋找素數呢?

判斷素數

尋找素數最樸素的方法當然是一個一個遍歷,我們依次遍歷每一個數,然後分別判斷是否是素數。所以問題的核心又回到了判斷素數上,那麼怎麼判斷一個數是不是素數呢?

素數的性質只有一個,就是只有1和它本身這兩個因數,我們要判斷素數也只能利用這個性質。所以可以想到,假如我們要判斷n是否是素數,可以從2開始遍歷到n-1,如果這n-1個數都不能整除n,那麼說明n就是素數。這個我沒記錯在C語言的練習題當中出現過,總之非常簡單,可以說是最簡單的算法了。

def is_prime(n):
    for i in range(2, n):
        if n % i == 0:
            return False
    return n != 1

顯然,這個算法是可以優化的,比如當n是偶數的時候,我們根本不需要循環,除了2意外的偶數一定是合數。再比如,我們循環的上界其實也沒有必要到n-1,到就可以了。因為因數如果存在一定是成對出現的,如果存在小於根號n的因數,那麼n除以它一定大於根號n。

這個改進也很簡單,稍作改動即可:

def is_prime(n):
    if n % 2 == 0 and n != 2:
        return False
    for i in range(3, int(math.sqrt(n) + 1)):
        if n % i == 0:
            return False
    return n != 1

這樣我們把O(n)的算法優化到了O()也算是有了很大的改進了,但是還沒有結束,我們還可以繼續優化。數學上有一個定理,只有形如6n-1和6n+1的自然數可能是素數,這裏的n是大於等於1的整數。

這個定理乍一看好像很高級,但其實很簡單,因為所有自然數都可以寫成6n,6n+1,6n+2,6n+3,6n+4,6n+5這6種,其中6n,6n+2,6n+4是偶數,一定不是素數。6n+3可以寫成3(2n+1),顯然也不是素數,所以只有可能6n+1和6n+5可能是素數。6n+5等價於6n-1,所以我們一般寫成6n-1和6n+1。

利用這個定理,我們的代碼可以進一步優化:

def is_prime(n):
    if n % 6 not in (1, 5) and n not in (2, 3):
        return False
    for i in range(3, int(math.sqrt(n) + 1)):
        if n % i == 0:
            return False
    return n != 1

雖然這樣已經很快了,但仍然不是最優的,尤其是當我們需要尋找大量素數的時候,仍會消耗大量的時間。那麼有沒有什麼辦法可以批量查找素數呢?

有,這個方法叫做埃拉托斯特尼算法。這個名字念起來非常拗口,這是一個古希臘的名字。此人是個古希臘的大牛,是大名鼎鼎的阿基米德的好友。他雖然沒有阿基米德那麼出名,但是也非常非常厲害,在數學、天文學、地理學、文學、歷史學等多個領域都有建樹。並且還自創方法測量了地球直徑、地月距離、地日距離以及黃赤交角等諸多數值。要知道他生活的年代是兩千五百多年前,那時候中國還是春秋戰國時期,可以想見此人有多厲害。

埃式篩法

我們今天要介紹的埃拉托斯特尼算法就是他發明的用來篩選素數的方法,為了方便我們一般簡稱為埃式篩法或者篩法。埃式篩法的思路非常簡單,就是用已經篩選出來的素數去過濾所有能夠被它整除的數。這些素數就像是篩子一樣去過濾自然數,最後被篩剩下的數自然就是不能被前面素數整除的數,根據素數的定義,這些剩下的數也是素數。

舉個例子,比如我們要篩選出100以內的所有素數,我們知道2是最小的素數,我們先用2可以篩掉所有的偶數。然後往後遍歷到3,3是被2篩剩下的第一個數,也是素數,我們再用3去篩除所有能被3整除的數。篩完之後我們繼續往後遍歷,第一個遇到的數是7,所以7也是素數,我們再重複以上的過程,直到遍歷結束為止。結束的時候,我們就獲得了100以內的所有素數。

如果還不太明白,可以看下下面這張動圖,非常清楚地還原了這整個過程。

不見圖 請翻牆

這個思想非常簡單,理解了之後寫出代碼來真的很容易:

def eratosthenes(n):
    primes = []
    is_prime = [True] * (n + 1)
    for i in range(2, n+1):
        if is_prime[i]:
            primes.append(i)
            # 用當前素數i去篩掉所有能被它整除的數
            for j in range(i * 2, n+1, i):
                is_prime[j] = False
    return primes

我們運行一次代碼看看:

和我們的預期一樣,獲得了小於100的所有素數。我們來分析一下篩法的複雜度,從代碼當中我們可以看到,我們一共有了兩層循環,最外面一層循環固定是遍歷n次。而裏面的這一層循環遍歷的次數一直在變化,並且它的運算次數和素數的大小相關,看起來似乎不太方便計算。實際上是可以的,根據素數分佈定理以及一系列複雜的運算(相信我,你們不會感興趣的),我們是可以得出篩法的複雜度是

極致優化

篩法的複雜度已經非常近似了,因為即使在n很大的時候,經過兩次ln的計算,也非常近似常數了,實際上在絕大多數使用場景當中,上面的算法已經足夠應用了。

但是仍然有大牛不知滿足,繼續對算法做出了優化,將其優化到了的複雜度。雖然從效率上來看並沒有數量級的提升,但是應用到的思想非常巧妙,值得我們學習。在我們理解這個優化之前,先來看看之前的篩法還有什麼可以優化的地方。比較明顯地可以看出來,對於一個合數而言,它可能會被多個素數篩去。比如38,它有2和19這兩個素因數,那麼它就會被置為兩次False,這就帶來了額外的開銷,如果對於每一個合數我們只更新一次,那麼是不是就能優化到了呢?

怎麼樣保證每個合數只被更新一次呢?這裏要用到一個定理,就是每個合數分解質因數只有的結果是唯一的。既然是唯一的,那麼一定可以找到最小的質因數,如果我們能夠保證一個合數只會被它最小的質因數更新為False,那麼整個優化就完成了。

那我們具體怎麼做呢?其實也不難,我們假設整數n的最小質因數是m,那麼我們用小於m的素數i乘上n可以得到一個合數。我們將這個合數消除,對於這個合數而言,i一定是它最小的質因數。因為它等於i * n,n最小的質因數是m,i 又小於m,所以i是它最小的質因數,我們用這樣的方法來生成消除的合數,這樣來保證每個合數只會被它最小的質因數消除。

根據這一點,我們可以寫出新的代碼:

def ertosthenes(n):
    primes = []
    is_prime = [True] * (n+1)
    for i in range(2, n+1):
        if is_prime[i]:
            primes.append(i)
        for j, p in enumerate(primes):
            # 防止越界
            if p > n // i:
                break
            # 過濾
   is_prime[i * p] = False
            # 當i % p等於0的時候說明p就是i最小的質因數
            if i % p == 0:
                break
                
    return primes

總結

到這裏,我們關於埃式篩法的介紹就告一段落了。埃式篩法的優化版本相對來說要難以記憶一些,如果記不住的話,可以就只使用優化之前的版本,兩者的效率相差並不大,完全在可以接受的範圍之內。

篩法看着代碼非常簡單,但是非常重要,有了它,我們就可以在短時間內獲得大量的素數,快速地獲得一個素數表。有了素數表之後,很多問題就簡單許多了,比如因數分解的問題,比如信息加密的問題等等。我每次回顧篩法算法的時候都會忍不住感慨,這個兩千多年前被發明出來的算法至今看來非但不過時,仍然還是那麼巧妙。希望大家都能懷着崇敬的心情,理解算法當中的精髓。

如果喜歡本文,可以的話,請點個關注,給我一點鼓勵,也方便獲取更多文章。

本文使用 mdnice 排版

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計最專業,超強功能平台可客製化

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※教你寫出一流的銷售文案?

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家公司費用怎麼算?

分類
發燒車訊

容器技術之Docker私有鏡像倉庫docker-distribution

  在前邊的博客中我們說到docker的架構由docker客戶端、服務端以及倉庫組成;docker倉庫就是用來存放鏡像的地方;其實docker registry我們理解為存放docker鏡像倉庫的倉庫比較準確吧;因為docker的鏡像倉庫通常是把同一類的鏡像用不同的版本來區別,而registry則是用來存放這些倉庫的倉庫;默認安裝docker都是從dockerhub鏡像倉庫下載鏡像;其實在生產環境中,我們很少去公有倉庫上下載鏡像,原因之一是公有倉庫中的鏡像在生產環境中使用,有些不適配,通常我們是去公有倉庫下載基礎鏡像,然後基於基礎鏡像構建適合自己生產環境中的鏡像;其次公有倉庫鏡像有很多都不是安全的鏡像,這麼說吧,我們不確定自己下載的鏡像是否有後門,是否有挖礦代碼,所以基於種種因素,我們還是有必要搭建自己私有的鏡像倉庫;今天我們就來聊一聊docker的私有鏡像倉庫的搭建;

  1、查看docker-distribution包簡介

[root@docker_registry ~]# yum info docker-distribution
Loaded plugins: fastestmirror
Loading mirror speeds from cached hostfile
 * base: mirrors.aliyun.com
 * extras: mirrors.aliyun.com
 * updates: mirrors.aliyun.com
Available Packages
Name        : docker-distribution
Arch        : x86_64
Version     : 2.6.2
Release     : 2.git48294d9.el7
Size        : 3.5 M
Repo        : extras/7/x86_64
Summary     : Docker toolset to pack, ship, store, and deliver content
URL         : https://github.com/docker/distribution
License     : ASL 2.0
Description : Docker toolset to pack, ship, store, and deliver content

[root@docker_registry ~]# 

  提示:docker-distribution這個包就是提供簡單倉庫服務軟件實現;

  2、安裝docker-distribution

[root@docker_registry ~]# yum install -y docker-distribution
Loaded plugins: fastestmirror
Loading mirror speeds from cached hostfile
 * base: mirrors.aliyun.com
 * extras: mirrors.aliyun.com
 * updates: mirrors.aliyun.com
Resolving Dependencies
There are unfinished transactions remaining. You might consider running yum-complete-transaction, or "yum-complete-transaction --cleanup-only" and "yum history redo last", first to finish them. If those don't work you'll have to try removing/installing packages by hand (maybe package-cleanup can help).
The program yum-complete-transaction is found in the yum-utils package.
--> Running transaction check
---> Package docker-distribution.x86_64 0:2.6.2-2.git48294d9.el7 will be installed
--> Finished Dependency Resolution

Dependencies Resolved

===================================================================================================================
 Package                         Arch               Version                               Repository          Size
===================================================================================================================
Installing:
 docker-distribution             x86_64             2.6.2-2.git48294d9.el7                extras             3.5 M

Transaction Summary
===================================================================================================================
Install  1 Package

Total download size: 3.5 M
Installed size: 12 M
Downloading packages:
docker-distribution-2.6.2-2.git48294d9.el7.x86_64.rpm                                       | 3.5 MB  00:00:03     
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
  Installing : docker-distribution-2.6.2-2.git48294d9.el7.x86_64                                               1/1 
  Verifying  : docker-distribution-2.6.2-2.git48294d9.el7.x86_64                                               1/1 

Installed:
  docker-distribution.x86_64 0:2.6.2-2.git48294d9.el7                                                              

Complete!
[root@docker_registry ~]# 

  3、查看docker-distribution安裝了那些文件

[root@docker_registry ~]# rpm -ql docker-distribution
/etc/docker-distribution/registry/config.yml
/usr/bin/registry
/usr/lib/systemd/system/docker-distribution.service
/usr/share/doc/docker-distribution-2.6.2
/usr/share/doc/docker-distribution-2.6.2/AUTHORS
/usr/share/doc/docker-distribution-2.6.2/CONTRIBUTING.md
/usr/share/doc/docker-distribution-2.6.2/LICENSE
/usr/share/doc/docker-distribution-2.6.2/MAINTAINERS
/usr/share/doc/docker-distribution-2.6.2/README.md
/var/lib/registry
[root@docker_registry ~]# 

  提示:/etc/docker-distribution/registry/config.yml這個文件用於配置registry的配置文件;/usr/bin/registry是二進制應用程序;/usr/lib/systemd/system/docker-distribution.service 這個文件是docker-distribution的unit file;/var/lib/registry這個目錄用於存放我們上傳到registry上的鏡像存放地;

  4、查看配置文件

[root@docker_registry ~]# cat /etc/docker-distribution/registry/config.yml
version: 0.1
log:
  fields:
    service: registry
storage:
    cache:
        layerinfo: inmemory
    filesystem:
        rootdirectory: /var/lib/registry
http:
    addr: :5000
[root@docker_registry ~]# 

  提示:這個配置文件是一個yml語法的配置文件,從上面的信息可以看到,默認情況docker-distribution監聽在tcp的5000端口;存放鏡像的目錄是/var/lib/registry/目錄下;

  5、啟動docker-distribution

[root@docker_registry ~]# systemctl start docker-distribution
[root@docker_registry ~]# ss -tnl
State       Recv-Q Send-Q            Local Address:Port                           Peer Address:Port              
LISTEN      0      128                           *:22                                        *:*                  
LISTEN      0      100                   127.0.0.1:25                                        *:*                  
LISTEN      0      128                          :::22                                       :::*                  
LISTEN      0      100                         ::1:25                                       :::*                  
LISTEN      0      128                          :::5000                                     :::*                  
[root@docker_registry ~]# 

  提示:可以看到5000端口已經處於監聽狀態了;到此docker-distribution就啟動起來了;這個倉庫服務很簡陋,沒有用戶認證功能,默認是基於http通信而非https,所以從某些角度講,不是一個安全的倉庫;所以一般不見在互聯網上使用,在自己的內外環境中可以使用;

  這裏補充一點,docker的鏡像通常是 registry地址加repository名稱加版本這三部分組成,registry可以是域名,可以是ip地址加端口,也可以說域名加端口,默認https是443端口,http是80端口,如果不寫端口默認是443而非80(原因是docker默認不支持從http協議的倉庫下載/上傳鏡像);例如 quay.io/coreos/flannel:v0.12.0-s390x  從這個鏡像名我們就可以知道registry是https://quay.io;repository名稱為coreos/flannel 版本是v0.12.0-s390x;

  示例:下載第三方倉庫鏡像到本地

  提示:可以看到下載下來的鏡像名稱就是我們剛才說的registry+repository+版本;從上面的信息我們可以總結一點,docker鏡像的名稱(標籤)反應了該鏡像來自哪個registry的那個倉庫;所以我們要下載私有鏡像倉庫中的鏡像就需要把加上私有registry的名稱或地址+repository+版本來下載私有鏡像倉庫中的鏡像;同理上傳鏡像也需要寫明上傳到那個registry中的那個repository中去;

  示例:上傳本地鏡像到私有倉庫

  提示:要把本地倉庫鏡像傳到私有倉庫中去,首先我們要把本地鏡像打一個新的標籤,按照我們剛才上面說的邏輯,然後在上傳新打到標籤的鏡像到私有倉庫就可以了;從上面的信息我們看到當我們打好標籤后,上傳鏡像時報錯了,提示我們倉庫不是https的;默認情況docker不支持http明文上傳/下載鏡像;如果我們非要用http上傳下載鏡像我們需要在配置文件中明確的告訴docker非安全倉庫地址;

  配置docker支持私有倉庫上傳下載鏡像

[root@docker_registry ~]# cat /etc/docker/daemon.json
{
        "registry-mirrors": ["https://registry.docker-cn.com","https://cyr1uljt.mirror.aliyuncs.com"],
        "insecure-registries": ["192.168.0.99:5000"]
}

[root@docker_registry ~]# systemctl daemon-reload    
[root@docker_registry ~]# systemctl restart docker 

  提示:我們通過在配置文件中配置insecure-registries來告訴docker192.168.0.99:5000這個registry是不安全的,但是我們信任這個倉庫,大概就是這個意思嘛;通常我們是寫主機名然後配合hosts文件來解析的方式來對registry解析;從而把鏡像命名為主機名+倉庫名+版本的形式;如下所示;這裏還需要注意一點insecure-registries後面的列表中的倉庫如果有域名,域名不能有下劃線(“_”),否則重啟docker會起不來;

[root@docker_registry ~]# cat /etc/docker/daemon.json 
{
        "registry-mirrors": ["https://registry.docker-cn.com","https://cyr1uljt.mirror.aliyuncs.com"],
        "insecure-registries": ["192.168.0.99:5000","docker-registry.io:5000"]

}
[root@docker_registry ~]# cat /etc/hosts
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
192.168.0.99 docker-registry.io registry
192.168.0.22 docker-node01.io node01
192.168.0.23 docker-node02.io node02
[root@docker_registry ~]# systemctl restart docker
[root@docker_registry ~]# docker info
Client:
 Debug Mode: false

Server:
 Containers: 0
  Running: 0
  Paused: 0
  Stopped: 0
 Images: 1
 Server Version: 19.03.11
 Storage Driver: overlay2
  Backing Filesystem: xfs
  Supports d_type: true
  Native Overlay Diff: true
 Logging Driver: json-file
 Cgroup Driver: cgroupfs
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
  Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog
 Swarm: inactive
 Runtimes: runc
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: 7ad184331fa3e55e52b890ea95e65ba581ae3429
 runc version: dc9208a3303feef5b3839f4323d9beb36df0a9dd
 init version: fec3683
 Security Options:
  seccomp
   Profile: default
 Kernel Version: 3.10.0-693.el7.x86_64
 Operating System: CentOS Linux 7 (Core)
 OSType: linux
 Architecture: x86_64
 CPUs: 4
 Total Memory: 1.785GiB
 Name: docker_registry
 ID: R34V:IG2F:23I6:6WG6:FFQ4:75SV:3UKZ:RFH7:DGCO:QS7V:CS7K:NSH6
 Docker Root Dir: /var/lib/docker
 Debug Mode: false
 Registry: https://index.docker.io/v1/
 Labels:
 Experimental: false
 Insecure Registries:
  192.168.0.99:5000
  docker-registry.io:5000
  127.0.0.0/8
 Registry Mirrors:
  https://registry.docker-cn.com/
  https://cyr1uljt.mirror.aliyuncs.com/
 Live Restore Enabled: false

[root@docker_registry ~]#

  提示:重啟docker后,如果在docker info 中能夠看到我們配置的內容說明配置生效了;現在我們再來傳我們新打的標籤的鏡像,看看是否能夠傳到我們的私有倉庫呢?

[root@docker_registry ~]# docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
docker-registry.io:5000/centos   7                   b5b4d78bc90c        4 weeks ago         203MB
centos                           7                   b5b4d78bc90c        4 weeks ago         203MB
192.168.0.99:5000/flannel        v0.12.0-s390x       57eade024bfb        2 months ago        56.9MB
quay.io/coreos/flannel           v0.12.0-s390x       57eade024bfb        2 months ago        56.9MB
[root@docker_registry ~]# docker push 192.168.0.99:5000/flannel:v0.12.0-s390x
The push refers to repository [192.168.0.99:5000/flannel]
b67de7789e55: Pushed 
4c4bfa1b47e6: Pushed 
3b7ae8a9c323: Pushed 
fbd88a276dca: Pushed 
271ca11ef489: Pushed 
1f106b41b4d6: Pushed 
v0.12.0-s390x: digest: sha256:3ce5b8d40451787e1166bf6b207c7834c13f7a0712b46ddbfb591d8b5906bfa6 size: 1579
[root@docker_registry ~]# docker push docker-registry.io:5000/centos:7
The push refers to repository [docker-registry.io:5000/centos]
edf3aa290fb3: Pushed 
7: digest: sha256:c2f1d5a9c0a81350fa0ad7e1eee99e379d75fe53823d44b5469eb2eb6092c941 size: 529
[root@docker_registry ~]# 

  提示:可以看到我們上傳的兩個鏡像都完成了上傳沒有報錯,接下來我們去/var/lib/registry/這個目錄,看看是否有這兩個鏡像相關目錄?

[root@docker_registry ~]# tree /var/lib/registry/
/var/lib/registry/
└── docker
    └── registry
        └── v2
            ├── blobs
            │   └── sha256
            │       ├── 13
            │       │   └── 13b80a37370b57f558a2e06092c39224e5d1ebac50e48df0afdeb43cf2303e60
            │       │       └── data
            │       ├── 17
            │       │   └── 176bad61a3a435da03ec603d2bd8f7a69286d92f21f447b17f21f0bc4e085bde
            │       │       └── data
            │       ├── 1b
            │       │   └── 1b56fbc8a8e10830867455c0794a70f5469c154cdc013554daf501aeda3f30fe
            │       │       └── data
            │       ├── 26
            │       │   └── 266247e2e603e1c840f97cb4d97a08b9184344e9802966cb42c93d21c407839f
            │       │       └── data
            │       ├── 3c
            │       │   └── 3ce5b8d40451787e1166bf6b207c7834c13f7a0712b46ddbfb591d8b5906bfa6
            │       │       └── data
            │       ├── 42
            │       │   └── 42d8e66fa893de4beb5d136b787cf182b24b7f4972c4212b9493b661ad1d7e85
            │       │       └── data
            │       ├── 52
            │       │   └── 524b0c1e57f8ee5fee01a1decba2f301c324a6513ca3551021264e3aa7341ebc
            │       │       └── data
            │       ├── 57
            │       │   └── 57eade024bfbd48c45ef2bad996c4d6a0fa41b692247294745265af738066813
            │       │       └── data
            │       ├── 85
            │       │   └── 85ecb68de4693bb4093d923f6d1062766e4fa7cbb3bf456a2bc19dd3e6c5e6c6
            │       │       └── data
            │       ├── b5
            │       │   └── b5b4d78bc90ccd15806443fb881e35b5ddba924e2f475c1071a38a3094c3081d
            │       │       └── data
            │       └── c2
            │           └── c2f1d5a9c0a81350fa0ad7e1eee99e379d75fe53823d44b5469eb2eb6092c941
            │               └── data
            └── repositories
                ├── centos
                │   ├── _layers
                │   │   └── sha256
                │   │       ├── 524b0c1e57f8ee5fee01a1decba2f301c324a6513ca3551021264e3aa7341ebc
                │   │       │   └── link
                │   │       └── b5b4d78bc90ccd15806443fb881e35b5ddba924e2f475c1071a38a3094c3081d
                │   │           └── link
                │   ├── _manifests
                │   │   ├── revisions
                │   │   │   └── sha256
                │   │   │       └── c2f1d5a9c0a81350fa0ad7e1eee99e379d75fe53823d44b5469eb2eb6092c941
                │   │   │           └── link
                │   │   └── tags
                │   │       └── 7
                │   │           ├── current
                │   │           │   └── link
                │   │           └── index
                │   │               └── sha256
                │   │                   └── c2f1d5a9c0a81350fa0ad7e1eee99e379d75fe53823d44b5469eb2eb6092c941
                │   │                       └── link
                │   └── _uploads
                └── flannel
                    ├── _layers
                    │   └── sha256
                    │       ├── 13b80a37370b57f558a2e06092c39224e5d1ebac50e48df0afdeb43cf2303e60
                    │       │   └── link
                    │       ├── 176bad61a3a435da03ec603d2bd8f7a69286d92f21f447b17f21f0bc4e085bde
                    │       │   └── link
                    │       ├── 1b56fbc8a8e10830867455c0794a70f5469c154cdc013554daf501aeda3f30fe
                    │       │   └── link
                    │       ├── 266247e2e603e1c840f97cb4d97a08b9184344e9802966cb42c93d21c407839f
                    │       │   └── link
                    │       ├── 42d8e66fa893de4beb5d136b787cf182b24b7f4972c4212b9493b661ad1d7e85
                    │       │   └── link
                    │       ├── 57eade024bfbd48c45ef2bad996c4d6a0fa41b692247294745265af738066813
                    │       │   └── link
                    │       └── 85ecb68de4693bb4093d923f6d1062766e4fa7cbb3bf456a2bc19dd3e6c5e6c6
                    │           └── link
                    ├── _manifests
                    │   ├── revisions
                    │   │   └── sha256
                    │   │       └── 3ce5b8d40451787e1166bf6b207c7834c13f7a0712b46ddbfb591d8b5906bfa6
                    │   │           └── link
                    │   └── tags
                    │       └── v0.12.0-s390x
                    │           ├── current
                    │           │   └── link
                    │           └── index
                    │               └── sha256
                    │                   └── 3ce5b8d40451787e1166bf6b207c7834c13f7a0712b46ddbfb591d8b5906bfa6
                    │                       └── link
                    └── _uploads

65 directories, 26 files
[root@docker_registry ~]# 

  提示:可以看到對應目錄下有兩個子目錄就是以我們上傳的鏡像名稱命名的;

  示例:查看私有倉庫中存在的進行列表

[root@docker_registry ~]# curl docker-registry.io:5000/v2/_catalog
{"repositories":["centos","flannel"]}
[root@docker_registry ~]# 

  示例:下載私有倉庫中的鏡像到本地

[root@docker_node01 ~]# ip a l ens33
2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 00:0c:29:22:36:7f brd ff:ff:ff:ff:ff:ff
    inet 192.168.0.22/24 brd 192.168.0.255 scope global ens33
       valid_lft forever preferred_lft forever
    inet6 fe80::20c:29ff:fe22:367f/64 scope link 
       valid_lft forever preferred_lft forever
[root@docker_node01 ~]# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
linux1874/myimg     v0.1                e408b1c6e04f        2 weeks ago         1.22MB
wordpress           latest              c3fa1c8546fb        5 weeks ago         540MB
mysql               5.7                 f965319e89de        5 weeks ago         448MB
alpine              v3                  f70734b6a266        6 weeks ago         5.61MB
nginx               1.14-alpine         8a2fb25a19f5        14 months ago       16MB
httpd               2.4.37-alpine       dfd436f9a5d8        17 months ago       91.8MB
[root@docker_node01 ~]# docker pull 192.168.0.99:5000/flannel:v0.12.0-s390x
v0.12.0-s390x: Pulling from flannel
176bad61a3a4: Pull complete 
13b80a37370b: Pull complete 
42d8e66fa893: Pull complete 
266247e2e603: Pull complete 
1b56fbc8a8e1: Pull complete 
85ecb68de469: Pull complete 
Digest: sha256:3ce5b8d40451787e1166bf6b207c7834c13f7a0712b46ddbfb591d8b5906bfa6
Status: Downloaded newer image for 192.168.0.99:5000/flannel:v0.12.0-s390x
192.168.0.99:5000/flannel:v0.12.0-s390x
[root@docker_node01 ~]# docker images
REPOSITORY                  TAG                 IMAGE ID            CREATED             SIZE
linux1874/myimg             v0.1                e408b1c6e04f        2 weeks ago         1.22MB
wordpress                   latest              c3fa1c8546fb        5 weeks ago         540MB
mysql                       5.7                 f965319e89de        5 weeks ago         448MB
alpine                      v3                  f70734b6a266        6 weeks ago         5.61MB
192.168.0.99:5000/flannel   v0.12.0-s390x       57eade024bfb        2 months ago        56.9MB
nginx                       1.14-alpine         8a2fb25a19f5        14 months ago       16MB
httpd                       2.4.37-alpine       dfd436f9a5d8        17 months ago       91.8MB
[root@docker_node01 ~]# 

  提示:下載私有倉庫中的鏡像,默認情況docker也是不支持直接訪問http協議的倉庫,需要我們手動去配置insecure-registries,然後重啟docker才可以;

  示例:刪除私有倉庫中的鏡像

  1、獲取對應鏡像的sha256的值 curl –header “Accept:application/vnd.docker.distribution.manifest.v2+json” -I -X GET http://<registry addr>/v2/<image name>/manifests/<image tag>

  2、刪除對應鏡像版本元數據 curl -I -X DELETE http://<registry addr>/v2/<image name>/manifests/<image digest>

  提示:如果響應405方法不被允許;我們需要修改私有倉庫的配置文件,將其配置為允許刪除;如下

[root@docker_registry ~]# cat /etc/docker-distribution/registry/config.yml
version: 0.1
log:
  fields:
    service: registry
storage:
    delete:
        enabled: true
    cache:
        layerinfo: inmemory
    filesystem:
        rootdirectory: /var/lib/registry
http:
    addr: :5000
[root@docker_registry ~]# systemctl restart docker-distribution           
[root@docker_registry ~]# curl -IX DELETE http://docker-registry.io:5000/v2/centos/manifests/sha256:c2f1d5a9c0a81350fa0ad7e1eee99e379d75fe53823d44b5469eb2eb6092c941
HTTP/1.1 202 Accepted
Docker-Distribution-Api-Version: registry/2.0
Date: Sat, 06 Jun 2020 19:55:52 GMT
Content-Length: 0
Content-Type: text/plain; charset=utf-8

[root@docker_registry ~]#

  提示:degest值包含”sha256:”

  3、垃圾回收清理

[root@docker_registry ~]# registry garbage-collect /etc/docker-distribution/registry/config.yml 
centos
flannel
flannel: marking manifest sha256:3ce5b8d40451787e1166bf6b207c7834c13f7a0712b46ddbfb591d8b5906bfa6 
flannel: marking blob sha256:57eade024bfbd48c45ef2bad996c4d6a0fa41b692247294745265af738066813
flannel: marking blob sha256:176bad61a3a435da03ec603d2bd8f7a69286d92f21f447b17f21f0bc4e085bde
flannel: marking blob sha256:13b80a37370b57f558a2e06092c39224e5d1ebac50e48df0afdeb43cf2303e60
flannel: marking blob sha256:42d8e66fa893de4beb5d136b787cf182b24b7f4972c4212b9493b661ad1d7e85
flannel: marking blob sha256:266247e2e603e1c840f97cb4d97a08b9184344e9802966cb42c93d21c407839f
flannel: marking blob sha256:1b56fbc8a8e10830867455c0794a70f5469c154cdc013554daf501aeda3f30fe
flannel: marking blob sha256:85ecb68de4693bb4093d923f6d1062766e4fa7cbb3bf456a2bc19dd3e6c5e6c6
myweb
myweb: marking manifest sha256:aaf04cf567a776e36eb3b0bafaec17ed8d9e0a743bdb897dca13f251250ae493 
myweb: marking blob sha256:4f406abeaab7f848178867409142090d1a551b22b968be6a6dae733c8403738e
myweb: marking blob sha256:524b0c1e57f8ee5fee01a1decba2f301c324a6513ca3551021264e3aa7341ebc
myweb: marking blob sha256:c941076f9075280c41b502283f37ab8bafef3a66f4a7ba299838dce07641a48d
test
test: marking manifest sha256:5ecad23ab8a52e55f93968f708d325261032dd613287aec92e7cf8ddd6426635 
test: marking blob sha256:370e3a843c3cb12700301e3f87f929939146cd8b676260bedcd83aaa7fcc2939
test: marking blob sha256:524b0c1e57f8ee5fee01a1decba2f301c324a6513ca3551021264e3aa7341ebc
test: marking manifest sha256:da8b53210bf1f4dc4873bbd5589abad616663cda45205ae3a4fffb0729d2730d 
test: marking blob sha256:461f6ceabc885e2e90b5f9ee82aefc9a30a39510c40e7cd8fb7436a4d340fe1d
test: marking blob sha256:524b0c1e57f8ee5fee01a1decba2f301c324a6513ca3551021264e3aa7341ebc
test: marking blob sha256:035e026f1d6b0acba3413ba616dcbabf75d20e945778c52716e601255452b7b7

17 blobs marked, 2 blobs eligible for deletion
blob eligible for deletion: sha256:b5b4d78bc90ccd15806443fb881e35b5ddba924e2f475c1071a38a3094c3081d
INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/b5/b5b4d78bc90ccd15806443fb881e35b5ddba924e2f475c1071a38a3094c3081d  go.version=go1.9.4 instance.id=b3029d7f-99e8-4941-8c87-989514b584ea
blob eligible for deletion: sha256:c2f1d5a9c0a81350fa0ad7e1eee99e379d75fe53823d44b5469eb2eb6092c941
INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/c2/c2f1d5a9c0a81350fa0ad7e1eee99e379d75fe53823d44b5469eb2eb6092c941  go.version=go1.9.4 instance.id=b3029d7f-99e8-4941-8c87-989514b584ea
[root@docker_registry ~]# 

  測試:下載docker-registry.io:5000/centos:7 看看是否還能下載?

[root@docker_node01 ~]# docker pull 192.168.0.99:5000/centos:7
Error response from daemon: manifest for 192.168.0.99:5000/centos:7 not found: manifest unknown: manifest unknown
[root@docker_node01 ~]# 

  提示:以上提示告訴我們沒有對應鏡像的元數據信息;說明我們私有倉庫沒有對應鏡像;以上方法適合精準刪除某個鏡像的某個版本,如果是刪除一個倉庫,直接刪除 /var/lib/registry/docker/registry/v2/repositories/下對應倉庫的目錄,然後在用registry命令做垃圾回收;如下

[root@docker_registry ~]# ll /var/lib/registry/docker/registry/v2/repositories/
total 0
drwxr-xr-x 5 root root 55 Jun  6 14:16 centos
drwxr-xr-x 5 root root 55 Jun  6 14:15 flannel
drwxr-xr-x 5 root root 55 Jun  6 15:25 myweb
drwxr-xr-x 5 root root 55 Jun  6 15:24 test
[root@docker_registry ~]# rm -rf /var/lib/registry/docker/registry/v2/repositories/test
[root@docker_registry ~]# rm -rf /var/lib/registry/docker/registry/v2/repositories/myweb
[root@docker_registry ~]# ll /var/lib/registry/docker/registry/v2/repositories/
total 0
drwxr-xr-x 5 root root 55 Jun  6 14:16 centos
drwxr-xr-x 5 root root 55 Jun  6 14:15 flannel
[root@docker_registry ~]# registry garbage-collect /etc/docker-distribution/registry/config.yml 
centos
flannel
flannel: marking manifest sha256:3ce5b8d40451787e1166bf6b207c7834c13f7a0712b46ddbfb591d8b5906bfa6 
flannel: marking blob sha256:57eade024bfbd48c45ef2bad996c4d6a0fa41b692247294745265af738066813
flannel: marking blob sha256:176bad61a3a435da03ec603d2bd8f7a69286d92f21f447b17f21f0bc4e085bde
flannel: marking blob sha256:13b80a37370b57f558a2e06092c39224e5d1ebac50e48df0afdeb43cf2303e60
flannel: marking blob sha256:42d8e66fa893de4beb5d136b787cf182b24b7f4972c4212b9493b661ad1d7e85
flannel: marking blob sha256:266247e2e603e1c840f97cb4d97a08b9184344e9802966cb42c93d21c407839f
flannel: marking blob sha256:1b56fbc8a8e10830867455c0794a70f5469c154cdc013554daf501aeda3f30fe
flannel: marking blob sha256:85ecb68de4693bb4093d923f6d1062766e4fa7cbb3bf456a2bc19dd3e6c5e6c6

8 blobs marked, 9 blobs eligible for deletion
blob eligible for deletion: sha256:370e3a843c3cb12700301e3f87f929939146cd8b676260bedcd83aaa7fcc2939
INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/37/370e3a843c3cb12700301e3f87f929939146cd8b676260bedcd83aaa7fcc2939  go.version=go1.9.4 instance.id=1a15f1e7-194f-4a82-b79d-9f437d975f6e
blob eligible for deletion: sha256:524b0c1e57f8ee5fee01a1decba2f301c324a6513ca3551021264e3aa7341ebc
INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/52/524b0c1e57f8ee5fee01a1decba2f301c324a6513ca3551021264e3aa7341ebc  go.version=go1.9.4 instance.id=1a15f1e7-194f-4a82-b79d-9f437d975f6e
blob eligible for deletion: sha256:5ecad23ab8a52e55f93968f708d325261032dd613287aec92e7cf8ddd6426635
INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/5e/5ecad23ab8a52e55f93968f708d325261032dd613287aec92e7cf8ddd6426635  go.version=go1.9.4 instance.id=1a15f1e7-194f-4a82-b79d-9f437d975f6e
blob eligible for deletion: sha256:aaf04cf567a776e36eb3b0bafaec17ed8d9e0a743bdb897dca13f251250ae493
INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/aa/aaf04cf567a776e36eb3b0bafaec17ed8d9e0a743bdb897dca13f251250ae493  go.version=go1.9.4 instance.id=1a15f1e7-194f-4a82-b79d-9f437d975f6e
blob eligible for deletion: sha256:035e026f1d6b0acba3413ba616dcbabf75d20e945778c52716e601255452b7b7
INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/03/035e026f1d6b0acba3413ba616dcbabf75d20e945778c52716e601255452b7b7  go.version=go1.9.4 instance.id=1a15f1e7-194f-4a82-b79d-9f437d975f6e
blob eligible for deletion: sha256:461f6ceabc885e2e90b5f9ee82aefc9a30a39510c40e7cd8fb7436a4d340fe1d
INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/46/461f6ceabc885e2e90b5f9ee82aefc9a30a39510c40e7cd8fb7436a4d340fe1d  go.version=go1.9.4 instance.id=1a15f1e7-194f-4a82-b79d-9f437d975f6e
blob eligible for deletion: sha256:4f406abeaab7f848178867409142090d1a551b22b968be6a6dae733c8403738e
INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/4f/4f406abeaab7f848178867409142090d1a551b22b968be6a6dae733c8403738e  go.version=go1.9.4 instance.id=1a15f1e7-194f-4a82-b79d-9f437d975f6e
blob eligible for deletion: sha256:c941076f9075280c41b502283f37ab8bafef3a66f4a7ba299838dce07641a48d
INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/c9/c941076f9075280c41b502283f37ab8bafef3a66f4a7ba299838dce07641a48d  go.version=go1.9.4 instance.id=1a15f1e7-194f-4a82-b79d-9f437d975f6e
blob eligible for deletion: sha256:da8b53210bf1f4dc4873bbd5589abad616663cda45205ae3a4fffb0729d2730d
INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/da/da8b53210bf1f4dc4873bbd5589abad616663cda45205ae3a4fffb0729d2730d  go.version=go1.9.4 instance.id=1a15f1e7-194f-4a82-b79d-9f437d975f6e
[root@docker_registry ~]# 

  提示:這種方式比較粗暴簡單,通常是一個倉庫里只有一個版本鏡像可以使用這種方式刪除,如果一個倉庫有多個版本,那麼還是建議使用第一種方式;

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※超省錢租車方案

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

※推薦台中搬家公司優質服務,可到府估價

分類
發燒車訊

用雲開發Cloudbase,實現小程序多圖片內容安全監測

前言

相比於文本的安全檢測,圖片的安全檢測要稍微略複雜一些,當您讀完本篇,將get到

  • 圖片安全檢測的應用場景
  • 解決圖片的安全校驗的方式
  • 使用雲調用方式對圖片進行檢測
  • 如何對上傳圖片大小進行限制
  • 如何解決多圖上傳覆蓋問題

示例效果

當用戶上傳敏感違規圖片時,禁止用戶上傳發布,並且做出相對應的用戶友好提示

應用場景

通常,在校驗一張圖片是否含有違法違規內容相比於文本安全的校驗,同樣重要,有如下應用

  • 圖片智能鑒黃:涉及拍照的工具類應用(如美拍,識圖類應用)用戶拍照上傳檢測;電商類商品上架圖片檢測;媒體類用戶文章里的圖片檢測等
  • 敏感人臉識別:用戶頭像;媒體類用戶文章里的圖片檢測;社交類用戶上傳的圖片檢測等,凡是有用戶自發生產內容的都應當提前做檢測

解決圖片的安全手段

在小程序開發中,提供了兩種方式

  • HTTPS調用
  • 雲調用

HTTPS 調用的請求接口地止

https://api.weixin.qq.com/wxa/img_sec_check?access_token=ACCESS_TOKEN

檢測圖片審核,根據官方文檔得知,需要兩個必傳的參數:分別是:access_token(接口調用憑證),media(要檢測的圖片文件)

對於HTTPS調用方式,願意折騰的小夥伴可以參考文本內容安全檢測(上篇)的處理方式,處理大同小異,本篇主要以雲開發的雲調用為主

功能實現:小程序端邏輯

對於wxml與wxss,大家可以自行任意修改,本文重點在於圖片安全的校驗

<view class="image-list">
<!-- 显示圖片 -->
   <block wx:for="{{images}}" wx:key="*this"><view class="image-wrap">
       <image class="image" src="{{item}}" mode="aspectFill" bind:tap="onPreviewImage" data-imgsrc="{{item}}"></image><i class="iconfont icon-shanchu" bind:tap="onDelImage" data-index="{{index}}"></i></view>
   </block>
   <!-- 選擇圖片 -->
   <view class="image-wrap selectphoto" hidden="{{!selectPhoto}}" bind:tap="onChooseImage"><i class="iconfont icon-add"></i></view>
   </view>
   <view class="footer"><button class="send-btn"  bind:tap="send">發布</button>
   </view>

對應的wxss代碼

.footer {
  display: flex;
  align-items: center;
  width: 100%;
  box-sizing: border-box;
  background: #34bfa3;
}

.send-btn {
  width: 100%;
  color: #fff;
  font-size: 32rpx;
  background: #34bfa3;
}

button {
  border-radius: 0rpx;
}

button::after {
  border-radius: 0rpx !important;
}

/* 圖片樣式 */
.image-list {
  display: flex;
  flex-wrap: wrap;
  margin-top: 20rpx;
}

.image-wrap {
  width: 220rpx;
  height: 220rpx;
  margin-right: 10rpx;
  margin-bottom: 10rpx;
  position: relative;
  overflow: hidden;
  text-align: center;
}

.image {
  width: 100%;
  height: 100%;
}

.icon-shanchu {
  position: absolute;
  top: 0;
  right: 0;
  width: 40rpx;
  height: 40rpx;
  background-color: #000;
  opacity: 0.4;
  color: #fff;
  text-align: center;
  line-height: 40rpx;
  font-size: 38rpx;
  font-weight: bolder;
}

.selectphoto {
  border: 2rpx dashed #cbd1d7;
  position: relative;
}

.icon-add {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  color: #cbd1d7;
  font-size: 60rpx;
}

最終呈現的UI,由於只是用於圖片檢測演示,UI方面可忽略,如下所示

對應的JS代碼

/*
* 涉及到的API:wx.chooseImage  從本地相冊選擇圖片或使用相機拍照
*(https://developers.weixin.qq.com/miniprogram/dev/api/media/image/wx.chooseImage.html)


*
*
*/// 最大上傳圖片數量
const MAX_IMG_NUM = 9;

const db = wx.cloud.database(); // 初始化雲數據庫
Page({

  /**
   * 頁面的初始數據
   */
  data: {
    images: [],  // 把上傳的圖片存放在一個數組對象裏面
    selectPhoto: true, // 添加+icon元素是否显示
  },

  /**
   * 生命周期函數--監聽頁面加載
   */
  onLoad: function (options) {

  },

  // 選擇圖片
  onChooseImage() {
    // 還能再選幾張圖片,初始值設置最大的數量-當前的圖片的長度
    let max = MAX_IMG_NUM - this.data.images.length; 
    wx.chooseImage({
      count: max,               // count表示最多可以選擇的圖片張數
      sizeType: ['original', 'compressed'], //  所選的圖片的尺寸
      sourceType: ['album', 'camera'],  // 選擇圖片的來源
      success: (res) => {                     // 接口調用成功的回調函數console.log(res)
        this.setData({                       // tempFilePath可以作為img標籤的src屬性显示圖片,下面是將后添加的圖片與之前的圖片給追加起來
          images: this.data.images.concat(res.tempFilePaths)
        })
        // 還能再選幾張圖片
        max = MAX_IMG_NUM - this.data.images.length
        this.setData({
          selectPhoto: max <= 0 ? false : true  // 當超過9張時,加號隱藏
        })
      },
    })
  },

  // 點擊右上方刪除圖標,刪除圖片操作
  onDelImage(event) {
    const index = event.target.dataset.index;
    // 點擊刪除當前圖片,用splice方法,刪除一張,從數組中移除一個                                                       
    this.data.images.splice(index, 1)
    this.setData({
      images: this.data.images
    })
    // 當添加的圖片達到設置最大的數量時,添加按鈕隱藏,不讓新添加圖片
    if (this.data.images.length == MAX_IMG_NUM - 1) {
      this.setData({
        selectPhoto: true,
      })
    }
  },
})

最終實現的前端UI效果如下所是:

您現在看到的效果,沒有任何雲函數代碼,只是前端的純靜態展示,對於一些涉嫌敏感圖片,是有必要進行做過濾處理的

功能實現:雲函數側邏輯

在cloudfunctions目錄文件夾下創建雲函數imgSecCheck

並在該目錄下創建config.json,配置參數如下所示

{
  "permissions": {
    "openapi": [
      "security.imgSecCheck"
    ]
  }
}

配置完后,在主入口index.js中,如下所示,通過security.imgSecCheck接口,並傳入media對象

// 雲函數入口文件
const cloud = require('wx-server-sdk');
cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV
})

// 雲函數入口函數
exports.main = async (event, context) => {
  const wxContext = cloud.getWXContext()
  try {
    const result = await cloud.openapi.security.imgSecCheck({
      media: {
        contentType: 'image/png',
        value: Buffer.from(event.img)   // 這裏必須要將小程序端傳過來的進行Buffer轉化,否則就會報錯,接口異常
      }
      
    })

    if (result && result.errCode.toString() === '87014') {
      return { code: 500, msg: '內容含有違法違規內容', data: result }
    } else {
      return { code: 200, msg: '內容ok', data: result }
    }
  } catch (err) {
    // 錯誤處理
    if (err.errCode.toString() === '87014') {
      return { code: 500, msg: '內容含有違法違規內容', data: err }
    }
    return { code: 502, msg: '調用imgSecCheck接口異常', data: err }
  }
}

您會發現在雲函數端,就這麼幾行代碼,就完成了圖片安全校驗

而在小程序端,代碼如下所示

// miniprogram/pages/imgSecCheck/imgSecCheck.js
// 最大上傳圖片數量
const MAX_IMG_NUM = 9;

const db = wx.cloud.database()
Page({

  /**
   * 頁面的初始數據
   */
  data: {
    images: [],
    selectPhoto: true, // 添加圖片元素是否显示
  },

  /**
   * 生命周期函數--監聽頁面加載
   */
  onLoad: function (options) {

  },
  // 選擇圖片
  onChooseImage() {
    // const that = this;  // 如果下面用了箭頭函數,那麼這行代碼是不需要的,直接用this就可以了的// 還能再選幾張圖片,初始值設置最大的數量-當前的圖片的長度
    let max = MAX_IMG_NUM - this.data.images.length; 
    wx.chooseImage({
      count: max,
      sizeType: ['original', 'compressed'],
      sourceType: ['album', 'camera'],
      success: (res) => {  // 這裏若不是箭頭函數,那麼下面的this.setData的this要換成that上面的臨時變量,作用域的問題,不清楚的,可以看下this指向相關的知識
       console.log(res)
       // tempFilePath可以作為img標籤的src屬性显示圖片
        const  tempFiles = res.tempFiles;
        this.setData({
          images: this.data.images.concat(res.tempFilePaths)
        })
        // 在選擇圖片時,對本地臨時存儲的圖片,這個時候,進行圖片的校驗,當然你放在最後點擊發布時,進行校驗也是可以的,只不過是一個前置校驗和後置校驗的問題,我個人傾向於在選擇圖片時就進行校驗的,選擇一些照片時,就應該在選擇時階段做安全判斷的, 小程序端請求雲函數方式// 圖片轉化buffer后,調用雲函數
        console.log(tempFiles);
        tempFiles.forEach(items => {
          console.log(items);
          // 圖片轉化buffer后,調用雲函數
          wx.getFileSystemManager().readFile({
            filePath: items.path,
            success: res => {
                  console.log(res);
                   wx.cloud.callFunction({  // 小程序端請求imgSecCheck雲函數,並傳遞img參數進行檢驗
                    name: 'imgSecCheck',
                    data: {
                      img: res.data
                    }
            })
            .then(res => {
               console.log(res);
               let { errCode } = res.result.data;
               switch(errCode) {
                 case 87014:
                   this.setData({
                      resultText: '內容含有違法違規內容'
                   })
                   break;
                 case 0:
                   this.setData({
                     resultText: '內容OK'
                   })
                   break;
                 default:
                   break;
               }
 
            })
            .catch(err => {
               console.error(err);
            })
            },
            fail: err => {
              console.error(err);
            }
          })
        })
        
            
        // 還能再選幾張圖片
        max = MAX_IMG_NUM - this.data.images.length
        this.setData({
          selectPhoto: max <= 0 ? false : true  // 當超過9張時,加號隱藏
        })
      },
    })
  },

  // 刪除圖片
  onDelImage(event) {
    const index =  event.target.dataset.index;
    // 點擊刪除當前圖片,用splice方法,刪除一張,從數組中移除一個
    this.data.images.splice(index, 1);
    this.setData({
      images: this.data.images
    })
    // 當添加的圖片達到設置最大的數量時,添加按鈕隱藏,不讓新添加圖片
    if (this.data.images.length == MAX_IMG_NUM - 1) {
      this.setData({
        selectPhoto: true,
      })
    }
  },
})

示例效果如下所示:

至此,關於圖片安全檢測就已經完成了,您只需要根據檢測的結果,做一些友好的用戶提示,或者做一些自己的業務邏輯判斷即可

常見問題

如何對上傳的圖片大小進行限制

有時候,您需要對用戶上傳圖片的大小進行限制,限制用戶任意上傳超大圖片,那怎麼處理呢,在微信小程序裏面,主要藉助的是wx.chooseImage這個接口成功返回后臨時路徑的res.tempFiles中的size大小判斷即可進行處理

具體實例代碼如下所示

// 選擇圖片
  onChooseImage() {
    // 還能再選幾張圖片,初始值設置最大的數量-當前的圖片的長度
    let max = MAX_IMG_NUM - this.data.images.length; 
    wx.chooseImage({
      count: max,
      sizeType: ['original', 'compressed'],
      sourceType: ['album', 'camera'],
      success: (res) => {
        console.log(res)
        const  tempFiles = res.tempFiles;
        this.setData({
          images: this.data.images.concat(res.tempFilePaths)  // tempFilePath可以作為img標籤的src屬性显示圖片
        })
        // 在選擇圖片時,對本地臨時存儲的圖片,這個時候,進行圖片的校驗,當然你放在最後點擊發布時,進行校驗也是可以的,只不過是一個前置校驗和後置校驗的問題,我個人傾向於在選擇圖片時就進行校驗的,選擇一些照片時,就應該在選擇時階段做安全判斷的, 小程序端請求雲函數方式// 圖片轉化buffer后,調用雲函數
        console.log(tempFiles);
        tempFiles.forEach(items => {
          if (items && items.size > 1 * (1024 * 1024)) {  // 限製圖片的大小
            wx.showToast({
              icon: 'none',
              title: '上傳的圖片超過1M,禁止用戶上傳',
              duration: 4000
            })
            // 超過1M的圖片,禁止用戶上傳
          }
          console.log(items);
          // 圖片轉化buffer后,調用雲函數
          wx.getFileSystemManager().readFile({
            filePath: items.path,
            success: res => {
                  console.log(res);
                   wx.cloud.callFunction({   // 請求調用雲函數imgSecCheck
                    name: 'imgSecCheck',
                    data: {
                      img: res.data
                    }
            })
            .then(res => {
               console.log(res);
               let { errCode } = res.result.data;
               switch(errCode) {
                 case 87014:
                   this.setData({
                      resultText: '內容含有違法違規內容'
                   })
                   break;
                 case 0:
                   this.setData({
                     resultText: '內容OK'
                   })
                   break;
                 default:
                   break;
               }
            })
            .catch(err => {
               console.error(err);
            })
            },
            fail: err => {
              console.error(err);
            }
          })
        })
       
        // 還能再選幾張圖片
        max = MAX_IMG_NUM - this.data.images.length
        this.setData({
          selectPhoto: max <= 0 ? false : true  // 當超過9張時,加號隱藏
        })
      },
    })
  },

注意: 使用微信官方的圖片內容安全接口進行校驗,限製圖片大小限制:1M,否則的話就會報錯

也就是說,對於超過1M大小的違規圖片,微信官方提供的這個圖片安全接口是無法進行校驗的

這個根據自己的業務而定,在小程序端對用戶上傳圖片的大小進行限制如果您覺得微信官方提供的圖片安全接口滿足不了自己的業務需求,那麼可以選擇一些其他的圖片內容安全校驗的接口的

這個圖片安全校驗是非常有必要的,用戶一旦上傳非法圖片,一旦通過網絡進行傳播,產生了社會影響,平台是有責任的,這種前車之鑒是有的

如何解決多圖上傳覆蓋的問題

對於上傳圖片來說,這個wx.cloud.uploadFileAPI接口只能上傳一張圖片,但是很多時候,是需要上傳多張圖片到雲存儲當中的,當點擊發布的時候,我們是希望將多張圖片都上傳到雲存儲當中去的

這個API雖然只能每次上傳一張,但您可以循環遍歷多張圖片,然後一張一張的上傳的

在cloudPath上傳文件的參數當中,它的值:需要注意:文件的名稱

那如何保證上傳的圖片不被覆蓋,文件不重名的情況下就不會被覆蓋

而在選擇圖片的時候,不應該上傳,因為用戶可能有刪除等操作,如果直接上傳的話會造成資源的浪費

而應該在點發布按鈕的時候,才執行上傳操作,文件不重名覆蓋的示例代碼如下所示

      let promiseArr = []
      let fileIds = []      // 將圖片的fileId存放到一個數組中
      let imgLength = this.data.images.length;
      // 圖片上傳
      for (let i = 0; i < imgLength; i++) {
        let p = new Promise((resolve, reject) => {
        let item = this.data.images[i]
          // 文件擴展名
          let suffix = /\.\w+$/.exec(item)[0]; // 取文件后拓展名
          wx.cloud.uploadFile({      // 利用官方提供的上傳接口
            cloudPath: 'blog/' + Date.now() + '-' + Math.random() * 1000000 + suffix,  // 雲存儲路徑,您也可以使用es6中的模板字符串進行拼接的
            filePath: item,   // 要上傳文件資源的路徑
            success: (res) => {
              console.log(res);
              console.log(res.fileID)
              fileIds = fileIds.concat(res.fileID)       // 將新上傳的與之前上傳的給拼接起來
              resolve()
            },
            fail: (err) => {
              console.error(err)
              reject()
            }
          })
        })
        promiseArr.push(p)
      }
      // 存入到雲數據庫,其中這個Promise.all(),等待裏面所有的任務都執行之後,在去執行後面的任務,也就是等待上傳所有的圖片上傳完后,才能把相對應的數據存到數據庫當中,具體與promise相關問題,可自行查漏
      Promise.all(promiseArr).then((res) => {
          db.collection('blog').add({ // 查找blog集合,將img,時間等數據添加到這個集合當中
            data: {
              img: fileIds,
              createTime: db.serverDate(), // 服務端的時間
            }
          }).then((res) => {
            console.log(res);
            this._hideToastTip();
            this._successTip();
          })
        })
        .catch((err) => {
          // 發布失敗console.error(err);
        })

上面通過利用當前時間+隨機數的方式進行了一個區分,規避了上傳文件同名的問題

因為這個上傳接口,一次性只能上傳一張圖片,所以需要循環遍歷圖片,然後一張張的上傳

一個是上傳到雲存儲中,另一個是添加到雲數據庫集合當中,要分別注意下這兩個操作,雲數據庫中的圖片是從雲存儲中拿到的,然後再添加到雲數據庫當中去的

示例效果如下所示:

將上傳的圖片存儲到雲數據庫中

注意:添加數據到雲數據庫中,需要手動創建集合,不然是無法上傳不到雲數據庫當中的,會報錯

至此,關於敏感圖片的檢測,以及多圖片的上傳到這裏就已經完成了

如下是完整的小程序端邏輯示例代碼

// miniprogram/pages/imgSecCheck/imgSecCheck.js
// 最大上傳圖片數量
const MAX_IMG_NUM = 9;
const db = wx.cloud.database()
Page({

  /**
   * 頁面的初始數據
   */
  data: {
    images: [],
    selectPhoto: true, // 添加圖片元素是否显示
  },

  /**
   * 生命周期函數--監聽頁面加載
   */
  onLoad: function (options) {

  },

  // 選擇圖片
  onChooseImage() {
    // 還能再選幾張圖片,初始值設置最大的數量-當前的圖片的長度
    let max = MAX_IMG_NUM - this.data.images.length;
    wx.chooseImage({
      count: max,
      sizeType: ['original', 'compressed'],
      sourceType: ['album', 'camera'],
      success: (res) => {
        console.log(res)
        const tempFiles = res.tempFiles;
        this.setData({
          images: this.data.images.concat(res.tempFilePaths) // tempFilePath可以作為img標籤的src屬性显示圖片
        })
        // 在選擇圖片時,對本地臨時存儲的圖片,這個時候,進行圖片的校驗,當然你放在最後點擊發布時,進行校驗也是可以的,只不過是一個前置校驗和後置校驗的問題,我個人傾向於在選擇圖片時就進行校驗的,選擇一些照片時,就應該在選擇時階段做安全判斷的, 小程序端請求雲函數方式
        // 圖片轉化buffer后,調用雲函數
        console.log(tempFiles);
        tempFiles.forEach(items => {
          if (items && items.size > 1 * (1024 * 1024)) {
            wx.showToast({
              icon: 'none',
              title: '上傳的圖片超過1M,禁止用戶上傳',
              duration: 4000
            })
            // 超過1M的圖片,禁止上傳
          }
          console.log(items);
          // 圖片轉化buffer后,調用雲函數
          wx.getFileSystemManager().readFile({
            filePath: items.path,
            success: res => {
              console.log(res);
              this._checkImgSafe(res.data); // 檢測圖片安全校驗
            },
            fail: err => {
              console.error(err);
            }
          })
        })


        // 還能再選幾張圖片
        max = MAX_IMG_NUM - this.data.images.length
        this.setData({
          selectPhoto: max <= 0 ? false : true // 當超過9張時,加號隱藏
        })
      },
    })
  },

  // 刪除圖片
  onDelImage(event) {
    const index = event.target.dataset.index;
    // 點擊刪除當前圖片,用splice方法,刪除一張,從數組中移除一個
    this.data.images.splice(index, 1);
    this.setData({
      images: this.data.images
    })
    // 當添加的圖片達到設置最大的數量時,添加按鈕隱藏,不讓新添加圖片
    if (this.data.images.length == MAX_IMG_NUM - 1) {
      this.setData({
        selectPhoto: true,
      })
    }
  },

  // 點擊發布按鈕,將圖片上傳到雲數據庫當中
  send() {
    const images = this.data.images.length;
    if (images) {
      this._showToastTip();
      let promiseArr = []
      let fileIds = []
      let imgLength = this.data.images.length;
      // 圖片上傳
      for (let i = 0; i < imgLength; i++) {
        let p = new Promise((resolve, reject) => {
          let item = this.data.images[i]
          // 文件擴展名
          let suffix = /\.\w+$/.exec(item)[0]; // 取文件后拓展名
          wx.cloud.uploadFile({   // 上傳圖片至雲存儲,循環遍歷,一張張的上傳
            cloudPath: 'blog/' + Date.now() + '-' + Math.random() * 1000000 + suffix,
            filePath: item,
            success: (res) => {
              console.log(res);
              console.log(res.fileID)
              fileIds = fileIds.concat(res.fileID)
              resolve()

            },
            fail: (err) => {
              console.error(err)
              reject()
            }
          })
        })
        promiseArr.push(p)
      }
      // 存入到雲數據庫
      Promise.all(promiseArr).then((res) => {
          db.collection('blog').add({ // 查找blog集合,將數據添加到這個集合當中
            data: {
              img: fileIds,
              createTime: db.serverDate(), // 服務端的時間
            }
          }).then((res) => {
            console.log(res);
            this._hideToastTip();
            this._successTip();
          })
        })
        .catch((err) => {
          // 發布失敗
          console.error(err);
        })
    } else {
      wx.showToast({
        icon: 'none',
        title: '沒有選擇任何圖片,發布不了',
      })
    }

  },

  // 校驗圖片的安全
  _checkImgSafe(data) {
    wx.cloud.callFunction({
        name: 'imgSecCheck',
        data: {
          img: data
        }
      })
      .then(res => {
        console.log(res);
        let {
          errCode
        } = res.result.data;
        switch (errCode) {
          case 87014:
            this.setData({
              resultText: '內容含有違法違規內容'
            })
            break;
          case 0:
            this.setData({
              resultText: '內容OK'
            })
            break;
          default:
            break;
        }
      })
      .catch(err => {
        console.error(err);
      })
  },

  _showToastTip() {
    wx.showToast({
      icon: 'none',
      title: '發布中...',
    })
  },

  _hideToastTip() {
    wx.hideLoading();
  },

  _successTip() {
    wx.showToast({
      icon: 'none',
      title: '發布成功',
    })
  },
})

完整的示例wxml,如下所示

<view class="image-list">
<!-- 显示圖片 -->
<block wx:for="{{images}}" wx:key="*this">
     <view class="image-wrap"><image class="image" src="{{item}}" mode="aspectFill" bind:tap="onPreviewImage" data-imgsrc="{{item}}"></image><i class="iconfont icon-shanchu" bind:tap="onDelImage" data-index="{{index}}"></i>
     </view>
</block>
<!-- 選擇圖片 -->
<view class="image-wrap selectphoto" hidden="{{!selectPhoto}}" bind:tap="onChooseImage"><i class="iconfont icon-add"></i></view>
</view>
<view class="footer">
   <button class="send-btn"  bind:tap="send">發布</button>
</view>
<view>
    檢測結果显示: {{ resultText }}
</view>

您可以根據自己的業務邏輯需要,一旦檢測到圖片違規時,禁用按鈕狀態,或者給一些用戶提示,都是可以的,在發布之前或者點擊發布時,進行圖片內容安全的校驗都可以,一旦發現圖片有違規時,就不讓繼續後面的操作的

結語

本文主要通過藉助官方提供的圖片security.imgSecCheck

接口,實現了對圖片安全的校驗,實現起來,是相當的方便的,對於基礎性的校驗,利用官方提供的這個接口,已經夠用了的,但是如果想要更加嚴格的檢測,可以引入一些第三方的內容安全強強校驗,確保內容的安全

實現了如何對上傳的圖片大小進行限制,以及解決同名圖片上傳覆蓋的問題

如果大家對文本內容安全校驗以及圖片安全校驗仍然有什麼問題,可以在下方留言,一起探討。

雲開發公眾號:騰訊云云開發

雲開發技術文檔:https://cloudbase.net

雲開發技術交流加Q群:601134960

更多精彩
掃描二維碼了解更多

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※回頭車貨運收費標準

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※推薦評價好的iphone維修中心

※教你寫出一流的銷售文案?

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

分類
發燒車訊

系統化學習多線程(一)

大綱

————————-學前必讀———————————-

學習不能快速成功,但一定可以快速入門
整體課程思路:
1.實踐為主,理論化偏少
2.課程筆記有完整的案例和代碼,(為了學習效率)再開始之前我會簡單粗暴的介紹知識點案例思路,
有基礎的同學聽了之後可以直接結合筆記寫代碼,
如果沒聽懂再向下看視頻,我會手把手編寫代碼和演示測試結果;
3.重要提示,學編程和學游泳一樣,多實踐學習效率才高,理解才透徹;
4.編碼功底差的建議每個案例代碼寫三遍,至於為什麼…<<賣油翁>>…老祖宗的智慧

————————————————————————-

 1.線程

1.1.什麼是線程

線程(英語:thread)是操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以併發多個線程,每條線程并行執行不同的任務。在Unix System V及SunOS中也被稱為輕量進程(lightweight processes),但輕量進程更多指內核線程(kernel thread),而把用戶線程(user thread)稱為線程。(來自百度百科)

一個進程可以有很多線程,每條線程并行執行不同的任務。

1.2.多線程hello word

需求:模擬在計算上一邊聽歌一邊打遊戲

三種實現方案如下:

TestDemo

 1 package com.wfd360.thread;
 2 
 3 import com.wfd360.thread.demo01.GameRunnable;
 4 import com.wfd360.thread.demo01.MusicRunnable;
 5 import com.wfd360.thread.demo02.GameThread;
 6 import com.wfd360.thread.demo02.MusicThread;
 7 import org.junit.Test;
 8 
 9 /**
10  * @author 姿勢帝-博客園
11  * @address https://www.cnblogs.com/newAndHui/
12  * @WeChat 851298348
13  * @create 05/03 5:27
14  * @description 需求分析:
15  * 1.模擬一邊打遊戲一邊聽音樂,在控制台打印輸出模擬
16  * 2.把兩個業務封裝成獨立的線程,實現接口Runnable或繼承Thread,通過看源碼你會發現Thread類實現了接口Runnable,使用本質上這兩種方法時一樣的。
17  * 3.Thread類提供兩個方法,線程主題方法run,啟動線程方法start
18  */
19 public class TestDemo {
20     /**
21      * 方式1:實現接口Runnable
22      */
23     @Test
24     public void testRunnable() throws InterruptedException {
25         System.out.println("-------test start-------");
26         // 實例對象
27         MusicRunnable music = new MusicRunnable();
28         GameRunnable game = new GameRunnable();
29         // 創建線程
30         Thread musicThread = new Thread(music);
31         Thread gameThread = new Thread(game);
32         // 啟動線程
33         musicThread.start();
34         gameThread.start();
35         System.out.println("--------等待其他線程執行--------------");
36         Thread.sleep(5 * 1000);
37         System.out.println("-------test end-------");
38     }
39 
40     /**
41      * 方式2:繼承Thread
42      */
43     @Test
44     public void testThread() throws InterruptedException {
45         System.out.println("-------test start-------");
46         // 創建線程
47         MusicThread musicThread = new MusicThread();
48         GameThread gameThread = new GameThread();
49         // 啟動線程
50         musicThread.start();
51         gameThread.start();
52         System.out.println("--------等待其他線程執行--------------");
53         Thread.sleep(5 * 1000);
54         System.out.println("-------test end-------");
55     }
56 
57     /**
58      * 方式3:簡寫,這種寫法一般我們在做模擬測試的使用,在正式代碼中建議不使用,可讀性較差
59      */
60     @Test
61     public void testThreadSimple() throws InterruptedException {
62         System.out.println("-------test start-------");
63         // 創建線程
64         Thread musicThread = new Thread(() -> {
65             for (int i = 0; i < 100; i++) {
66                 System.out.println("=======聽音樂中============" + i);
67             }
68         });
69         Thread gameThread = new Thread(() -> {
70             for (int i = 0; i < 100; i++) {
71                 System.out.println("=======打遊戲中============" + i);
72             }
73         });
74         // 啟動線程
75         musicThread.start();
76         gameThread.start();
77         System.out.println("--------等待其他線程執行--------------");
78         Thread.sleep(5 * 1000);
79         System.out.println("-------test end-------");
80     }
81 }


 實現接口Runnable

 1 package com.wfd360.thread.demo01;
 2 
 3 /**
 4  * @author 姿勢帝-博客園
 5  * @address https://www.cnblogs.com/newAndHui/
 6  * @WeChat 851298348
 7  * @create 05/03 5:31
 8  * @description
 9  */
10 public class GameRunnable implements Runnable {
11     @Override
12     public void run() {
13         for (int i = 0; i < 100; i++) {
14             System.out.println("=======打遊戲中============" + i);
15         }
16     }
17 }

GameRunnable

 1 package com.wfd360.thread.demo01;
 2 
 3 /**
 4  * @author 姿勢帝-博客園
 5  * @address https://www.cnblogs.com/newAndHui/
 6  * @WeChat 851298348
 7  * @create 05/03 5:29
 8  * @description
 9  */
10 public class MusicRunnable implements Runnable {
11     @Override
12     public void run() {
13         for (int i = 0; i < 100; i++) {
14             System.out.println("=======聽音樂中============"+i);
15         }
16     }
17 }

MusicRunnable

 繼承Thread

 1 package com.wfd360.thread.demo02;
 2 
 3 /**
 4  * @author 姿勢帝-博客園
 5  * @address https://www.cnblogs.com/newAndHui/
 6  * @WeChat 851298348
 7  * @create 05/03 6:00
 8  * @description
 9  */
10 public class GameThread extends Thread {
11     @Override
12     public void run() {
13         for (int i = 0; i < 100; i++) {
14             System.out.println("-------遊戲中----------"+i);
15         }
16     }
17 }

GameThread GameThread

 1 package com.wfd360.thread.demo02;
 2 
 3 /**
 4  * @author 姿勢帝-博客園
 5  * @address https://www.cnblogs.com/newAndHui/
 6  * @WeChat 851298348
 7  * @create 05/03 6:00
 8  * @description
 9  */
10 public class MusicThread extends Thread {
11     @Override
12     public void run() {
13         for (int i = 0; i < 100; i++) {
14             System.out.println("-------音樂中----------" + i);
15         }
16     }
17 }


 總結

啟動線程兩種方式:

    1.通過繼承Thread類

    2.實現Runnable接口

 使用哪種方式更好?

區別: 

一個類如果繼承了其他類,就無法在繼承Thread類,在Java中,一個類只能繼承一個類,而一個類如果實現了一個接口,還可以實現其他接口,接口是可以多實現的,所以說

Runable的擴展性更強,但是繼承的方式更簡單,個人建議一般情況下使用Thread;

實現接口Runnable或繼承Thread,通過看源碼你會發現Thread類實現了接口Runnable,使用本質上這兩種方法是一樣的

啟動線程流程:

    創建啟動線程的方式一:繼承Thread類

       1.將業務方法封裝成線程對象,自定義類t extends Thread類; 

       2.覆寫run方法: 覆寫第一步中的run方法;

       3.創建自定義對象t

       4.啟動線程 t.start();

   創建啟動線程方式二:實現Runnable接口

      1.將業務方法封裝成線程對象,自定義類t implements Runnable接口;

      2.實現第一步中的run方法

      3.創建自定義對象t

      4.啟動線程 new Thread(t).start();

1.3.對主線程與創建線程執行順序的理解

問題:
直接寫一個簡單的HelloWorld 程序,有沒有線程?
==>有一個主線程,在垃圾回收的時候,有gc 線程。

 1 package com.wfd360.thread;
 2 
 3 import org.junit.Test;
 4 
 5 /**
 6  * @author 姿勢帝-博客園
 7  * @address https://www.cnblogs.com/newAndHui/
 8  * @WeChat 851298348
 9  * @create 05/04 11:09
10  * @description <p>
11  * 問題:
12  * 直接寫一個簡單的HelloWorld 程序,有沒有線程?
13  * ==>有一個主線程,在垃圾回收的時候,有gc 線程。
14  * 結論:一旦線程啟動起來之後就是獨立的,和創建環境沒有關係;
15  * 啟動線程不能直接調用run方法,必須調用start方法;
16  * </p>
17  */
18 public class TestDemo02 {
19     /**
20      * 如果把創建線程放在循環語句的 下 面,會交替出現嗎
21      * ==>否,因為主線程執行完成后才會啟動hello線程
22      *
23      * @throws Exception
24      */
25     @Test
26     public void test1() throws Exception {
27         System.out.println("---test start-------");
28         // 執行主線程
29         for (int i = 0; i < 100; i++) {
30             System.out.println("-----test1--------" + i);
31         }
32         // 啟動hello線程
33         new HelloThread().start();
34         System.out.println("=======等待執行完成===========");
35         Thread.sleep(5 * 1000);
36         System.out.println("---test end-------");
37     }
38 
39     /**
40      * 如果把創建線程放在循環語句的 上 面,會交替出現嗎
41      * ==>可能會,可能不會,可能出現for循環完之後,線程還沒有啟動完;
42      *
43      * @throws Exception
44      */
45     @Test
46     public void test2() throws Exception {
47         System.out.println("---test start-------");
48         // 啟動hello線程
49         new HelloThread().start();
50         // 執行主線程
51         for (int i = 0; i < 100; i++) {
52             System.out.println("-----test1--------" + i);
53         }
54         System.out.println("=======等待執行完成===========");
55         Thread.sleep(5 * 1000);
56         System.out.println("---test end-------");
57     }
58 
59     /**
60      * 採用內部類的方式定義一個hello線程對象
61      */
62     class HelloThread extends Thread {
63         @Override
64         public void run() {
65             for (int i = 0; i < 100; i++) {
66                 System.out.println("-----HelloThread--------" + i);
67             }
68         }
69     }
70 }

TestDemo02

結論:一旦線程啟動起來之後就是獨立的,和創建環境沒有關係;
啟動線程不能直接調用run方法,必須調用start方法;

 1.4.對sleep方法的理解

package com.wfd360.thread;

/**
 * @author 姿勢帝-博客園
 * @address https://www.cnblogs.com/newAndHui/
 * @WeChat 851298348
 * @create 05/04 11:34
 * @description <p>
 * Thread類的方法:
 * static void sleep(long millis) 在指定的毫秒數內讓當前正在執行的線程休眠(暫停執行);
 * </p>
 */
public class TestSleep {
    /**
     * 做一個簡易倒計時,10秒鐘,控制台每一秒輸出一個数字,如10,9,8,7.....0
     */
    public static void main(String[] args) throws Exception {
        System.out.println("---test start-------");
        for (int i = 10; i >= 0; i--) {
            Thread.sleep(1 * 1000);
            System.out.println(i);
        }
        System.out.println("---test end-------");
    }
}

1.5.線程名稱的設置與獲取

繼承方式

簡單需求:使用多線程模擬多窗口售票

 1 package com.wfd360.thread.demo03Ticket;
 2 
 3 /**
 4  * @author 姿勢帝-博客園
 5  * @address https://www.cnblogs.com/newAndHui/
 6  * @WeChat 851298348
 7  * @create 05/04 11:55
 8  * @description <p>
 9  * 模擬多線程售票
10  * </p>
11  */
12 public class TicketThread extends Thread {
13     // 假定票總是100張
14     private static Integer num = 100;
15 
16     @Override
17     public void run() {
18         // 只要有票就一直售票
19         while (num > 0) {
20             System.out.println("正在出售第" + num + "張票");
21             --num;
22         }
23         System.out.println("===售票結束===");
24     }
25 }

TicketThread

test

/**
     * 測試模擬三個窗口售票
     * @throws InterruptedException
     */
    @Test
    public void testTicketThread() throws InterruptedException {
        System.out.println("---test start-------");
        // 模擬多3個窗口售票
        TicketThread ticketThread1 = new TicketThread();
        TicketThread ticketThread2 = new TicketThread();
        TicketThread ticketThread3 = new TicketThread();
        // 啟動線程售票
        ticketThread1.start();
        ticketThread2.start();
        ticketThread3.start();
        System.out.println("======等待售票============");
        Thread.sleep(5 * 1000);
        System.out.println("---test end-------");
    }

結果:

1.在售票過程中不能區分售出的票是那個窗口售出的,解決通過線程名稱判斷

2.有重複售出的票(後面的線程同步解決)

解決第一個問題,設置獲取線程名稱,通過Thread對象裏面自帶的getName,setName方法

 具體代碼

設置線程名稱

 獲取線程名稱

 上面講了繼承的方式獲取線程名稱,那麼實現接口Runnable的方式怎麼獲取設置勒

繼承Thread的方式,可以通過getName的方式獲取當前線程的名稱?
那使用Runnable的方式,能通過getName獲取嘛?

getName方法是Thread類的,但是TicketThread現在並沒有繼承Thread類,而是實現了Runnable接口.

問題:如果實現Runnable接口,怎麼獲取線程名稱?

 思考:TicketThread類裏面的代碼要執行,它肯定存在於某個線程中, 就比如寫個helloword打印語句,是不是也處於一個主線程中,那這裏怎麼獲取線程名稱?
通過動態獲取,當程序正在執行的時候,獲取當前正在執行的線程名稱。怎麼獲取?
在Thread類裏面有個靜態的方法currentThread() 方法,返回當前正在執行的線程引用;

Thread.currentThread().getName

那怎麼設置線程名稱?

Thread類裏面有個name字段,相當於Thread類把它包裝了一下:

通過源碼可以發現,構造方法裏面還有可以傳一個名字:

具體實現代碼如下

 

 總結:

繼承方式設置\獲取線程名稱通過 Thread對象裏面的 setName,getName方法;

實現接口方式設置名稱通過 new Thread(‘線程實例對象’, “線程名稱”),獲取線程名稱通過:Thread.currentThread().getName

1.6.Thread的join方法

void join() 方法 :等待該線程終止
void join(long millis) 方法 :等待該線程終止的時間最長為millis毫秒

需求: 當主線程運行到20的時候(i =20)的時候,讓JoinThread線程加進來直到執行完成,在執行主線程.

 1 package com.wfd360.thread;
 2 
 3 import org.junit.Test;
 4 
 5 /**
 6  * @author 姿勢帝-博客園
 7  * @address https://www.cnblogs.com/newAndHui/
 8  * @WeChat 851298348
 9  * @create 05/04 6:31
10  * @description
11  */
12 public class Test05Join {
13     /**
14      * 需求:
15      * 當主線程for循環到i=20時,等JoinThread線程執行完成后,在執行for循環的線程
16      * @throws InterruptedException
17      */
18     @Test
19     public void testJoinThread() throws InterruptedException {
20         System.out.println("---test start-------");
21         // 開啟線程
22         JoinThread thread = new JoinThread();
23         thread.start();
24         // 循環打印線程
25         for (int i = 0; i < 100; i++) {
26             System.out.println("======testJoinThread=========="+i);
27             Thread.sleep(1);
28             if (i==20){
29                 // 等線程JoinThread執行完成
30                 thread.join();
31             }
32         }
33         System.out.println("=============等待線程執行完成===================");
34         Thread.sleep(10*1000);
35         System.out.println("---test end-------");
36     }
37     
38     class JoinThread extends Thread {
39         @Override
40         public void run() {
41             for (int i = 0; i < 100; i++) {
42                 System.out.println("=====JoinThread=======" + i);
43                 // 模擬處理很多業務耗時1毫秒
44                 try {
45                     Thread.sleep(1);
46                 } catch (InterruptedException e) {
47                     e.printStackTrace();
48                 }
49             }
50         }
51     }
52 }

Test05Join

1.7.線程優先級

直接上代碼

 1 package com.wfd360.thread;
 2 
 3 /**
 4  * @author 姿勢帝-博客園
 5  * @address https://www.cnblogs.com/newAndHui/
 6  * @WeChat 851298348
 7  * @create 05/05 8:17
 8  * @description <p>
 9  * 1.==>線程優先級的理解:
10  * 線程的優先級和生活中類似,高優先級線程的執行優先於低優先級線程;
11  * 並不是絕對的,可能優先級高的線程優先 比 優先級低的線程先執行,只能說,高優先級的線程優先執行的幾率更多;
12  * (比如兩個線程,一個優先級高,一個優先級低,如果一共運行一個小時,優先級高的線程執行遠遠大於優先級低的但是並不是說優先級高的先執行完,
13  * 在執行優先級低的)
14  * 2.==>重新設置線程優先級
15  * int getPriority() 返回線程的優先級。
16  * void setPriority(int newPriority) 更改線程的優先級。Java線程的優先級從1到10級別,值越大優先級越高.
17  * 3.==>線程的默認優先級受創建線程的環境影響,默認值5,自定義線程的默認優先級和創建它的環境的線程優先級一致
18  * </p>
19  */
20 public class Test06Priority {
21     /**
22      * 測試獲取線程優先級,設置線程優先級,驗證線程優先級受創建環境影響
23      * @param args
24      */
25     public static void main(String[] args) {
26         Thread threadMain = Thread.currentThread();
27         // 獲取默認優先級数字
28         System.out.println("main線程默認優先級:" +threadMain.getPriority());// 5
29         // 重新設置默認優先級数字
30         threadMain.setPriority(8);
31         // 再次重新獲取優先級数字
32         System.out.println("main線程修改后的優先級:" +threadMain.getPriority());// 8
33         // 創建一個線程查看優先級
34         Thread thread = new Thread();
35         System.out.println("thread線程的優先級:" +thread.getPriority());// 8 受創建環境影響
36     }
37 }

1.8.後台線程,即守護線程

直接看代碼

 1 package com.wfd360.thread;
 2 
 3 import com.wfd360.thread.demo04Daemon.DaemonThreaad;
 4 
 5 /**
 6  * @author 姿勢帝-博客園
 7  * @address https://www.cnblogs.com/newAndHui/
 8  * @WeChat 851298348
 9  * @create 05/05 9:12
10  * @description <p>
11  * 後台線程,即守護線程
12  * 後台線程:指為其他線程提供服務的線程,也稱為守護線程。JVM的垃圾回收線程就是一個後台線程。
13  * 需求:嘗試把線程標記為後台線程或者標記為(前台)線程;
14  * Thread類提供的方法:
15  * 方法1: void setDaemon(boolean on) 將該線程標記為守護線程或用戶線程,true為後台線程,false為用戶線程(前台線下)
16  * 怎樣測試該線程是否是守護線程?
17  * 方法2:isDaemon()  測試該線程是否為守護線程. true為後台線程,false為用戶線程(前台線下)
18  * <p>
19  * 結論1:活動的線程(已經在執行的線程t.start())不能設置後台線程,即主線程不能設置為後台線程。
20  * 結論2: 自定義線程的默認狀態和環境有關,後台線程中創建的線程默認是後台線程,前台線程中創建的線程為前台線程.
21  * 結論3: 前台線程執行完后,會直接關閉後台線程,即自定義的後台線程不一定能執行完成
22  * </p>
23  */
24 public class Test07Daemon {
25     /**
26      * 測試1
27      * 查看主線程的狀態,嘗試更改
28      * 結論:活動的線程不能設置為後台線程
29      *
30      * @param args
31      */
32     public static void main1(String[] args) {
33         Thread threadMain = Thread.currentThread();
34         System.out.println("是後台線程么:" + threadMain.isDaemon());// false
35         threadMain.setDaemon(true); // 報錯,活動的線程不能設置為後台線程
36         System.out.println("修改后是後台線程么:" + threadMain.isDaemon());
37     }
38 
39     /**
40      * 測試2
41      * 查看主線程中 創建線程的狀態,嘗試更改;
42      *
43      * @param args
44      */
45     public static void main2(String[] args) {
46         Thread thread = new Thread();
47         // false
48         System.out.println("是後台線程么:" + thread.isDaemon());
49         // 修改為後台線程
50         thread.setDaemon(true);
51         System.out.println("修改后是後台線程么:" + thread.isDaemon());
52     }
53 
54     /**
55      * 測試3
56      * 查看主線程中 創建線程的狀態,嘗試更改,讓線程處於活動狀態在修改->報錯;
57      *
58      * @param args
59      */
60     public static void main3(String[] args) {
61         DaemonThread thread = new DaemonThread();
62         // 讓線程處於活躍狀態
63         thread.start();
64         // false
65         System.out.println("是後台線程么:" + thread.isDaemon());
66         // 修改為後台線程,報錯,當前已經是活躍狀態(thread.start())不能修改為後台線程
67         thread.setDaemon(true);
68         System.out.println("修改后是後台線程么:" + thread.isDaemon());
69     }
70 
71     /**
72      * 測試4
73      * 前台線程執行完后,會直接關閉後台線程,即如果後台線程不一定能執行完成
74      * 可以通過修改等待執行時間來觀察DaemonThread線程的數組輸出變化
75      *
76      * @param args
77      */
78     public static void main(String[] args) throws InterruptedException {
79         DaemonThread thread = new DaemonThread();
80         // 修改為後台線程
81         thread.setDaemon(true);
82         // 讓線程處於活躍狀態
83         thread.start();
84         System.out.println("========等待後台線程執行============");
85         Thread.sleep(5 * 1000);
86     }
87 }
 1 package com.wfd360.thread.demo04Daemon;
 2 
 3 /**
 4  * @author 姿勢帝-博客園
 5  * @address https://www.cnblogs.com/newAndHui/
 6  * @WeChat 851298348
 7  * @create 05/05 9:27
 8  * @description
 9  */
10 public class DaemonThread extends Thread {
11     @Override
12     public void run() {
13         for (int i = 0; i < 10; i++) {
14             System.out.println("===="+i);
15             try {
16                 Thread.sleep(1000);
17             } catch (InterruptedException e) {
18                 e.printStackTrace();
19             }
20         }
21     }
22 }

DaemonThread

線程基礎相關的方法定義就先到這裏,下一篇我們將進入線程同步.

https://www.cnblogs.com/newAndHui/p/12831089.html

系統化的在線學習:點擊進入學習

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!

分類
發燒車訊

山地大猩猩的家園不平靜 剛果維龍加國家公園12名護管員遭殺害

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

分類
發燒車訊

非洲豬瘟迫降! 韓媒爆北韓豬瘟再起 邊境恐成傳播溫床

摘錄自2020年5月5日自由時報報導

北韓在去年5月底爆出非洲豬瘟,9月時更爆出豬瘟疫情全面失控,但之後卻未有通報案例。不過南韓媒體爆料,非洲豬瘟在北韓又再度爆發,尤其是黃海北道、平安南道以及平安北道三處。

由脫北者經營的南韓媒體《每日北韓》報導,平安南道消息人士透露,非洲豬瘟再次席捲北韓,獸醫機構視其為緊急狀態,尤其是黃海北道、平安南道以及平安北道三處的私人豬舍或合作經營農場。

由於北韓當局最近發布《非洲豬瘟防疫指南》,因此有分析指出,北韓非洲豬瘟疫情仍持續。北韓官報也向民眾宣導牲畜疾病防疫對民眾的重要性。

生活環境
國際新聞
北韓
非洲豬瘟
食品安全

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計最專業,超強功能平台可客製化

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※教你寫出一流的銷售文案?

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家公司費用怎麼算?

分類
發燒車訊

疫情下的奇景!西班牙西北部棕熊出沒 150年來首見

摘錄自2020年5月5日自由時報報導

根據《CNN》報導,位於西班牙西北部加利西亞(Galicia)奧倫塞的「O Invernadeiro」自然保護區,近日透過架設在園內的攝影機拍到一頭年輕公熊活動的畫面,據分析,這頭公熊年紀在3至5歲左右,健康狀況良好。

園方表示,棕熊是西班牙原生物種,從1973年起便被列入野生動物保護名單,過往雖有文獻紀錄常出沒在加利西亞地區,但這次是150年內首度有棕熊被觀測到在加利西亞南部出現,意義非凡。

生態保育
生物多樣性
國際新聞
西班牙
棕熊
動物與大環境變遷
武漢肺炎

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※超省錢租車方案

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

※推薦台中搬家公司優質服務,可到府估價

分類
發燒車訊

麥肯錫報告:後疫情時代下的氣候變遷

轉載自台大風險社會與政策研究中心;編譯:倪茂庭(風險中心助理研究員)、吳玗恂(風險中心助理研究員)

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※回頭車貨運收費標準

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※推薦評價好的iphone維修中心

※教你寫出一流的銷售文案?

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

分類
發燒車訊

30萬價格卻有50萬級的大氣場豪華中大型轎車

內飾方面也保持了旗艦車型應有的氣場,棕色為主的色調,大量實木材質飾板提升了不少檔次感,沉穩而大方,電動吸合門這麼高逼格的配置40。90萬的輝昂居然配備了,要知道同價位的BBA都沒有的東西,檔次感一下子就上去了。

在外打拚多年的老陳買了輛車子,過年帶着媳婦回到村子。

村民都投來了羡慕的眼光,鄰居家小黃問他:“陳哥賺了不少錢吧,都換了五六十萬的車子了”。

老陳心裏偷着樂:“嘿嘿,這豪華中大型轎車裸車才20幾萬呢,氣場就是強大,”

一說起豪華中大型轎車,大家都犹如耳濡目染,基本是被德系車如奧迪A6L、奔馳E級、寶馬5系等車型所包攬,但是如果價格去到30萬出頭,就只能是買到乞丐版車型了,那還不如買一些擁有強大氣場而且有着很高行車品質的車型,而且性價比也比較高,一起來看一下吧!

雷克薩斯-ES

指導價:29.80-49.80萬

說起雷克薩斯品牌總是給人一種溫文爾雅的感覺,前臉誇張的紡錘形設計進氣格柵,搭配外圈鍍鉻飾條,極具視覺衝擊感,提升了不少氣場,流暢的車身線條,立體感十足的尾燈,使得整輛車的氣質都提升了。

不同配置間的車型內飾材質也是略有不同,但是做工和品質還是一如既往的上乘,即使是最低配車型,也配備了胎壓監測、無鑰匙啟動/進入、上坡輔助、電動天窗、倒車影像、自動頭燈等配置,非常實用。

座椅採用了打孔皮革材料,坐上去感覺很厚實,與身體十分貼合,舒適性好,動力方面提供了2.0L最大功率167馬力或者2.5L最大功率184馬力的發動機,匹配6擋手自一體變速器,輕鬆好開才是重點,輸出和換擋都非常平順。

上汽大眾-輝昂

指導價:34.90-65.90萬

輝昂是上汽大眾打造的首款中大型轎車,與奧迪A6L出自MLB同一平台,足以吸引人的眼球,在大眾透視套娃式的外觀設計中,輝昂還是有這獨特的氣質的,寬大的前臉線條,雙邊四齣的排氣管裝飾罩,氣場還是挺嚇唬人的。

內飾方面也保持了旗艦車型應有的氣場,棕色為主的色調,大量實木材質飾板提升了不少檔次感,沉穩而大方,電動吸合門這麼高逼格的配置40.90萬的輝昂居然配備了,要知道同價位的BBA都沒有的東西,檔次感一下子就上去了。

輝昂的軸距達到了3009mm,想怎麼坐就怎麼坐,蹺二郎腿什麼的不在話下,寬厚的座椅設計人體工程學很到位,乘坐舒適性良好,動力提供了2.0T或者3.0T V6發動機的選擇,搭配7擋雙離合變速器,開起來很輕鬆就能上手駕馭,整車調校偏舒適,底盤是一如既往的沉穩。

英菲尼迪(進口)-Q70

指導價:39.98-64.98萬

作為英菲尼迪家族的旗艦豪華轎車,Q70L有着略帶攻擊性的外觀設計,菱形進氣格柵變得更加年輕了,犀利的全LED大燈組被大面積的鍍鉻飾條包裹,豪華氛圍濃厚,而車尾部的造型非常的飽滿、健碩,整體風格更加運動化。

環抱式的內飾設計給人很熟悉的感覺,真皮包裹的中控台手感很好,大量木紋飾板的點綴,加上中控上的石英鐘,豪華感非常強,除了最低配車型外,全系標配BOSE音響,還有電動吸合門也是全系標配的,這配置實在夠強大的。

座椅寬大厚實,對身體的各部位支撐到位,乘坐感受很出色,後排空間絕對是Q70L的一大亮點,3050mm的軸距競爭力很強,動力提供了V6布局的2.5L或者3.5L自然吸氣發動機,全系標配駕駛模式切換,動力輸出很線性,發動機聲音在高轉速是令人興奮的,但是不會給人很激烈駕駛的慾望。

總結:30萬左右的價格,選擇這些非主流的中大型豪華轎車,卻有着50萬級別車該有的氣場,而且配置上比寶馬奔馳奧迪那些主流品牌車型更為豐富,可以作為購車的一個新選擇。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!