標籤: 新北清潔

  • 深入理解React:事件機制原理

    深入理解React:事件機制原理

    目錄

    • 序言
    • DOM事件流
      • 事件捕獲階段、處於目標階段、事件冒泡階段
      • addEventListener 方法
    • React 事件概述
    • 事件註冊
      • document 上註冊
      • 回調函數存儲
    • 事件分發
    • 小結
    • 參考

    1.序言

    React 有一套自己的事件系統,其事件叫做合成事件。為什麼 React 要自定義一套事件系統?React 事件是如何註冊和觸發的?React 事件與原生 DOM 事件有什麼區別?帶着這些問題,讓我們一起來探究 React 事件機制的原理。為了便於理解,此篇分析將盡可能用圖解代替貼 React 源代碼進行解析。

    2.DOM事件流

    首先,在正式講解 React 事件之前,有必要了解一下 DOM 事件流,其包含三個流程:事件捕獲階段、處於目標階段和事件冒泡階段。

    W3C協會早在1988年就開始了DOM標準的制定,W3C DOM標準可以分為 DOM1、DOM2、DOM3 三個版本。

    從 DOM2 開始,DOM 的事件傳播分三個階段進行:事件捕獲階段、處於目標階段和事件冒泡階段。

    (1)事件捕獲階段、處於目標階段和事件冒泡階段

    示例代碼:

    <html>
        <body>
            <div id="outer">
    	    <p id="inner">Click me!</p>
    	</div>
        </body>
    </html>
    

    上述代碼,如果點擊 <p>元素,那麼 DOM 事件流如下圖:

    (1)事件捕獲階段:事件對象通過目標節點的祖先 Window 傳播到目標的父節點。

    (2)處於目標階段:事件對象到達事件目標節點。如果阻止事件冒泡,那麼該事件對象將在此階段完成后停止傳播。

    (3)事件冒泡階段:事件對象以相反的順序從目標節點的父項開始傳播,從目標節點的父項開始到 Window 結束。

    (2)addEventListener 方法

    DOM 的事件流中同時包含了事件捕獲階段和事件冒泡階段,而作為開發者,我們可以選擇事件處理函數在哪一個階段被調用。

    addEventListener() 方法用於為特定元素綁定一個事件處理函數。addEventListener 有三個參數:

    element.addEventListener(event, function, useCapture)
    

    另外,如果一個元素(element)針對同一個事件類型(event),多次綁定同一個事件處理函數(function),那麼重複的實例會被拋棄。當然如果第三個參數capture值不一致,此時就算重複定義,也不會被拋棄掉。

    3.React 事件概述

    React 根據W3C 規範來定義自己的事件系統,其事件被稱之為合成事件 (SyntheticEvent)。而其自定義事件系統的動機主要包含以下幾個方面:

    (1)抹平不同瀏覽器之間的兼容性差異。最主要的動機。

    (2)事件”合成”,即事件自定義。事件合成既可以處理兼容性問題,也可以用來自定義事件(例如 React 的 onChange 事件)。

    (3)提供一個抽象跨平台事件機制。類似 VirtualDOM 抽象了跨平台的渲染方式,合成事件(SyntheticEvent)提供一個抽象的跨平台事件機制。

    (4)可以做更多優化。例如利用事件委託機制,幾乎所有事件的觸發都代理到了 document,而不是 DOM 節點本身,簡化了 DOM 事件處理邏輯,減少了內存開銷。(React 自身模擬了一套事件冒泡的機制)

    (5)可以干預事件的分發。V16引入 Fiber 架構,React 可以通過干預事件的分發以優化用戶的交互體驗。

    注:「幾乎」所有事件都代理到了 document,說明有例外,比如audiovideo標籤的一些媒體事件(如 onplay、onpause 等),是 document 所不具有,這些事件只能夠在這些標籤上進行事件進行代理,但依舊用統一的入口分發函數(dispatchEvent)進行綁定。

    4.事件註冊

    React 的事件註冊過程主要做了兩件事:document 上註冊、存儲事件回調。

    (1)document 上註冊

    在 React 組件掛載階段,根據組件內的聲明的事件類型(onclick、onchange 等),在 document 上註冊事件(使用addEventListener),並指定統一的回調函數 dispatchEvent。換句話說,document 上不管註冊的是什麼事件,都具有統一的回調函數 dispatchEvent。也正是因為這一事件委託機制,具有同樣的回調函數 dispatchEvent,所以對於同一種事件類型,不論在 document 上註冊了幾次,最終也只會保留一個有效實例,這能減少內存開銷。

    示例代碼:

    function TestComponent() {
      handleFatherClick=()=>{
    		// ...
      }
    
      handleChildClick=()=>{
    		// ...
      }
    
      return <div className="father" onClick={this.handleFatherClick}>
    	<div className="child" onClick={this.handleChildClick}>child </div>
      </div>
    }
    

    上述代碼中,事件類型都是onclick,由於 React 的事件委託機制,會指定統一的回調函數 dispatchEvent,所以最終只會在 document 上保留一個 click 事件,類似document.addEventListener('click', dispatchEvent),從這裏也可以看出 React 的事件是在 DOM 事件流的冒泡階段被觸發執行。

    (2)存儲事件回調

    React 為了在觸發事件時可以查找到對應的回調去執行,會把組件內的所有事件統一地存放到一個對象中(listenerBank)。而存儲方式如上圖,首先會根據事件類型分類存儲,例如 click 事件相關的統一存儲在一個對象中,回調函數的存儲採用鍵值對(key/value)的方式存儲在對象中,key 是組件的唯一標識 id,value 對應的就是事件的回調函數。

    React 的事件註冊的關鍵步驟如下圖:

    5.事件分發

    事件分發也就是事件觸發。React 的事件觸發只會發生在 DOM 事件流的冒泡階段,因為在 document 上註冊時就默認是在冒泡階段被觸發執行。

    其大致流程如下:

    1. 觸發事件,開始 DOM 事件流,先後經過三個階段:事件捕獲階段、處於目標階段和事件冒泡階段
    2. 當事件冒泡到 document 時,觸發統一的事件分發函數 ReactEventListener.dispatchEvent
    3. 根據原生事件對象(nativeEvent)找到當前節點(即事件觸發節點)對應的 ReactDOMComponent 對象
    4. 事件的合成
      1. 根據當前事件類型生成對應的合成對象
      2. 封裝原生事件對象和冒泡機制
      3. 查找當前元素以及它所有父級
      4. 在 listenerBank 中查找事件回調函數併合成到 events 中
    5. 批量執行合成事件(events)內的回調函數
    6. 如果沒有阻止冒泡,會將繼續進行 DOM 事件流的冒泡(從 document 到 window),否則結束事件觸發

    注:上圖中阻止冒泡是指調用stopImmediatePropagation 方法阻止冒泡,如果是調用stopPropagation阻止冒泡,document 上如果還註冊了同類型其他的事件,也將會被觸發執行,但會正常阻斷 window 上事件觸發。了解兩者之間的詳細區別

    示例代碼:

    class TestComponent extends React.Component {
    
      componentDidMount() {
        this.parent.addEventListener('click', (e) => {
          console.log('dom parent');
        })
        this.child.addEventListener('click', (e) => {
          console.log('dom child');
        })
        document.addEventListener('click', (e) => {
          console.log('document');
        })
        document.body.addEventListener('click', (e) => {
          console.log('body');
        })
        window.addEventListener('click', (e) => {
          console.log('window');
        })
      }
    
      childClick = (e) => {
        console.log('react child');
      }
    
      parentClick = (e) => {
        console.log('react parent');
      }
    
      render() {
        return (
          <div class='parent' onClick={this.parentClick} ref={ref => this.parent = ref}>
            <div class='child' onClick={this.childClick} ref={ref => this.child = ref}>
              Click me!
            </div>
          </div>)
      }
    }
    

    點擊 child div 時,其輸出如下:

    在 DOM 事件流的冒泡階段先後經歷的元素:child <div> -> parent <div> -> <body> -> <html> -> document -> window,因此上面的輸出符合預期。

    6.小結

    React 合成事件和原生 DOM 事件的主要區別:

    (1)React 組件上聲明的事件沒有綁定在 React 組件對應的原生 DOM 節點上。

    (2)React 利用事件委託機制,將幾乎所有事件的觸發代理(delegate)在 document 節點上,事件對象(event)是合成對象(SyntheticEvent),不是原生事件對象,但通過 nativeEvent 屬性訪問原生事件對象。

    (3)由於 React 的事件委託機制,React 組件對應的原生 DOM 節點上的事件觸發時機總是在 React 組件上的事件之前。

    7.參考

    javascript中DOM0,DOM2,DOM3級事件模型解析

    Event dispatch and DOM event flow

    EventTarget.addEventListener() – Web API 接口參考| MDN

    合成事件

    談談React事件機制和未來(react-events)

    React源碼解讀系列 – 事件機制

    一文吃透 react 事件機制原理

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
    【其他文章推薦】

    USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

    台北網頁設計公司這麼多該如何選擇?

    ※智慧手機時代的來臨,RWD網頁設計為架站首選

    ※評比南投搬家公司費用收費行情懶人包大公開

    ※幫你省時又省力,新北清潔一流服務好口碑

    ※回頭車貨運收費標準

  • Python異常處理

    Python異常處理

    Python中的異常處理

    異常分類

      程序中難免出現錯誤,總共可分為兩種。

     

      1.邏輯錯誤

      2.語法錯誤

     

      對於剛接觸編程的人來說,這兩個錯誤都會經常去犯,但是隨着經驗慢慢的積累,語法錯誤的情況會越來越少反而邏輯錯誤的情況會越來越多(因為工程量巨大)。不論多麼老道的程序員都不可避免出現這兩種錯誤。

     

    異常的三大信息

      異常其實就是程序運行時發生錯誤的信號,我們寫代碼的過程中不可避免也最害怕的就是出現異常,然而當程序拋出異常時實際上會分為三部分,即三大信息。

     

     

    常用的異常類

      在Python中一切皆對象,異常本身也是由一個類生成的,NameError其實本身就是一個異常類,其他諸如此類的異常類還有很多。

    Python中常見的異常類 
    AttributeError 試圖訪問一個對象沒有的屬性,比如foo.x,但是foo並沒有屬性x
    IOError 輸入/輸出異常;基本上是無法打開文件
    ImportError 無法引入模塊或包;基本上是路徑問題或名稱錯誤
    IndentationError 語法錯誤(的子類) ;代碼沒有正確對齊
    IndexError 下標索引超出序列邊界,比如當x只有三個元素,卻試圖訪問x[5]
    KeyError 試圖訪問字典里不存在的鍵
    KeyboardInterrupt Ctrl+C被按下
    NameError 試圖使用一個還未被賦予對象的變量
    SyntaxError Python代碼非法,代碼不能編譯(其實就是語法錯誤,寫錯了)
    TypeError 傳入對象類型與要求的不符合
    UnboundLocalError 試圖訪問一個還未被設置的局部變量,基本上是由於另有一個同名的 全局變量,導致你以為正在訪問它
    ValueError 傳入一個調用者不期望的值,即使值的類型是正確的

     

    異常處理

      我們可以來用某些方法進行異常捕捉,當出現異常時我們希望代碼以另一種邏輯運行,使得我們的程序更加健壯,這個就叫做異常處理。異常處理是非常重要的,本身也並不複雜,千萬不可馬虎大意。

      但是切記不可濫用異常處理,這會使得你的代碼可讀性變差。

     

    if else處理異常

      ifelse本身就具有處理異常的功能,他們更多的是在我們能預測到可能出現的範圍內進行規避異常,對於我們不能預測的異常來說就顯得不是那麼的好用。如下:

    # ==== if else 處理異常 ====
    
    while 1:
        select = input("請輸入数字0進行關機:").strip()
        if select.isdigit():  # 我們可以防止用戶輸入非数字的字符
            if select == "0":
                print("正在關機...")
                break
        print("輸入有誤,請重新輸入")
    
    # ==== 執行結果 ====
    
    """
    請輸入数字0進行關機:關機
    輸入有誤,請重新輸入
    請輸入数字0進行關機:關機啊
    輸入有誤,請重新輸入
    請輸入数字0進行關機:0
    正在關機...
    """

      這種異常處理機制雖然非常簡單,但是並不靈活,我們可以使用更加簡單的方式來處理他們。

     

    try except處理異常

     

      try:代表要檢測可能出現異常的代碼塊

      except:當異常出現后的處理情況

     

      執行流程:

        try中檢測的代碼塊 —> 如果有異常 —> 執行except代碼塊 —> 執行正常邏輯代碼 —> 程序結束

        try中檢測的代碼塊 —> 如果沒有異常 —> 執行完畢try中的代碼塊 —> 執行正常邏輯代碼 —> 程序結束

     

    # ====  try except 執行流程 有異常的情況 ====
    
    li = [1,2,3]
    
    try:
        print("開始執行我try了...")
        print(li[10])  # 出錯點...
        print("繼續執行我try...")
    except IndexError as e:
        print("有異常執行我except...")
    
    print("正常邏輯代碼...")
    
    # ==== 執行結果 ====
    
    """
    開始執行我try了...
    有異常執行我except...
    正常邏輯代碼...
    """ 
    # ====  try except 執行流程 無異常的情況 ====
    
    li = [1,2,3]
    
    try:
        print("開始執行我try了...")
        print(li[2])
        print("繼續執行我try...")
    except IndexError as e:
        print("有異常執行我except...")
    
    print("正常邏輯代碼...")
    
    # ==== 執行結果 ====
    
    """
    開始執行我try了...
    3
    繼續執行我try...
    正常邏輯代碼...
    """ 
    # ==== try except 處理異常 ====
    
    while 1:
        try:  # try檢測可能出錯的語句,一旦出錯立馬跳轉到except語句塊執行代碼。
            select = int(input("請輸入数字0進行關機:").strip())
            if select == 0:
                print("正在關機...")
                break
            print("輸入有誤,請重新輸入...")
        except ValueError as e:  # 當執行完except的代碼塊后,程序運行結束,其中e代表的是異常信息。
            print("錯誤信息是:",e)
            print("輸入有誤,請重新輸入")
    
    # ==== 執行結果 ====
    
    """
    請輸入数字0進行關機:1
    輸入有誤,請重新輸入...
    請輸入数字0進行關機:tt
    錯誤信息是: invalid literal for int() with base 10: 'tt'
    輸入有誤,請重新輸入
    請輸入数字0進行關機:0
    正在關機...
    """

     

    多段except捕捉多異常

      我們可以使用try和多段except的語法來檢測某一代碼塊,可以更加方便的應對更多類型的錯誤,Ps不常用:

    # ==== 多段 except 捕捉多異常 ====
    
    while 1:
    
        li = [1,2,3,4]
        dic = {"name":"Yunya","age":18}
    
        li_index = input("請輸入索引:")
        dic_key = input("請輸入鍵的名稱:")
    
        if li_index.isdigit():
            li_index = int(li_index)
    
        try:
            print(li[li_index])
            print(dic[dic_key])
        except IndexError as e1:  # 注意,先拋出的錯誤會直接跳到其處理的except代碼塊,而try下面的語句將不會被執行。
            print("索引出錯啦!")
        except KeyError as e2:
            print("鍵出錯啦!")
    
    # ==== 執行結果 ====
    
    """
    請輸入索引:10
    請輸入鍵的名稱:gender
    索引出錯啦!
    請輸入索引:2
    請輸入鍵的名稱:gender
    3
    鍵出錯啦!
    """

     

    元組捕捉多異常

      使用多段except捕捉多異常會顯得特別麻煩,這個時候我們可以使用(異常類1,異常類2)來捕捉多異常,但是需要注意的是,對比多段except捕捉多異常來說,這種方式的處理邏輯會顯得較為複雜(因為只有一段處理邏輯),如下:

    # ====  元組捕捉多異常 ====
    
    while 1:
    
        li = [1,2,3,4]
        dic = {"name":"Yunya","age":18}
    
        li_index = input("請輸入索引:")
        dic_key = input("請輸入鍵的名稱:")
    
        if li_index.isdigit():
            li_index = int(li_index)
    
        try:
            print(li[li_index])
            print(dic[dic_key])
        except (IndexError,KeyError) as e:  # 使用()的方式可以同時捕捉很多異常。
            print("出錯啦!")
    
    # ==== 執行結果 ====
    
    """
    請輸入索引:10
    請輸入鍵的名稱:gender
    出錯啦!
    請輸入索引:2
    請輸入鍵的名稱:gender
    3
    出錯啦!
    """

      可以看到,不管是那種錯誤都只有一種應對策略,如果我們想要多種應對策略就只能寫if判斷來判斷異常類型再做處理。所以就會顯得很麻煩,如下:

    # ====  元組捕捉多異常 ====
    
    while 1:
    
        li = [1,2,3,4]
        dic = {"name":"Yunya","age":18}
    
        li_index = input("請輸入索引:")
        dic_key = input("請輸入鍵的名稱:")
    
        if li_index.isdigit():
            li_index = int(li_index)
    
        try:
            print(li[li_index])
            print(dic[dic_key])
        except (IndexError,KeyError) as e:
            # 判斷異常類型再做出相應的對應策略
            if isinstance(e,IndexError):
                print("索引出錯啦!")
            elif isinstance(e,KeyError):
                print("鍵出錯啦!")
    
    # ==== 執行結果 ====
    
    """
    請輸入索引:10
    請輸入鍵的名稱:gender
    索引出錯啦!
    請輸入索引:2
    請輸入鍵的名稱:gender
    3
    鍵出錯啦!
    """

     

    萬能異常Exception

      我們可以捕捉Exception類引發的異常,它是所有異常類的基類。(Exception類的父類則是BaseException類,而BaseException的父類則是object類)

    # ====  萬能異常Exception ====
    
    while 1:
    
        li = [1,2,3,4]
        dic = {"name":"Yunya","age":18}
    
        li_index = input("請輸入索引:")
        dic_key = input("請輸入鍵的名稱:")
    
        if li_index.isdigit():
            li_index = int(li_index)
    
        try:
            print(li[li_index])
            print(dic[dic_key])
        except Exception as e:  #使用 Exception來捕捉所有異常。
            # 判斷異常類型再做出相應的對應策略
            if isinstance(e,IndexError):
                print("索引出錯啦!")
            elif isinstance(e,KeyError):
                print("鍵出錯啦!")
    
    # ==== 執行結果 ====
    
    """
    請輸入索引:10
    請輸入鍵的名稱:gender
    索引出錯啦!
    請輸入索引:2
    請輸入鍵的名稱:gender
    3
    鍵出錯啦!
    """

     

     

    try except else聯用

      這種玩法比較少,else代表沒有異常發生的情況下執行的代碼,執行順序如下:

     

      try中檢測的代碼塊 —> 如果有異常 —> 終止try中的代碼塊繼續執行 —> 執行except代碼塊 —> 執行正常邏輯代碼 —> 程序結束

      try中檢測的代碼塊 —> 如果沒有異常 —> 執行完畢try中的代碼塊 —> 執行else代碼塊 —> 執行正常邏輯代碼 —> 程序結束

     

    # ====  try except else聯用 有異常的情況====
    
    li = [1,2,3]
    
    try:
        print("開始執行我try了...")
        print(li[10])  # 出錯點...
        print("繼續執行我try...")
    except IndexError as e:
        print("有異常執行我except...")
    else:
        print("沒有異常執行我else...")
    print("正常邏輯代碼...")
    
    # ==== 執行結果 ====
    
    """
    開始執行我try了...
    有異常執行我except...
    正常邏輯代碼...
    """
    # ====  try except else聯用 無異常的情況====
    
    li = [1,2,3]
    
    try:
        print("開始執行我try了...")
        print(li[2])
        print("繼續執行我try...")
    except IndexError as e:
        print("有異常執行我except...")
    else:
        print("沒有異常執行我else...")
    print("正常邏輯代碼...")
    
    # ==== 執行結果 ====
    
    """
    開始執行我try了...
    3
    繼續執行我try...
    沒有異常執行我else...
    正常邏輯代碼...
    """

     

    try except finally聯用

      finally代表不論拋異常與否都會執行,因此常被用作關閉系統資源的操作,關於try,except,else,finally他們的優先級如下:

     

      有異常的情況下:

        try代碼塊

        終止try代碼塊繼續執行

        except代碼塊

        finally代碼塊

        正常邏輯代碼

     

      無異常的情況下:

        try代碼塊

        else代碼塊

        finally代碼塊

        正常邏輯代碼

     

    # ====  try except else finally 執行流程 有異常的情況 ====
    
    li = [1,2,3]
    
    try:
        print("開始執行我try了...")
        print(li[10])  # 出錯點...
        print("繼續執行我try...")
    except IndexError as e:
        print("有異常執行我except...")
    else:
        print("沒有異常執行我else...")
    finally:
        print("不管有沒有異常都執行我finally...")
    
    print("正常邏輯代碼...")
    
    # ==== 執行結果 ====
    
    """
    開始執行我try了...
    有異常執行我except...
    不管有沒有異常都執行我finally...
    正常邏輯代碼...
    """
    # ====  try except else finally 執行流程 無異常的情況 ====
    
    li = [1,2,3]
    
    try:
        print("開始執行我try了...")
        print(li[2])
        print("繼續執行我try...")
    except IndexError as e:
        print("有異常執行我except...")
    else:
        print("沒有異常執行我else...")
    finally:
        print("不管有沒有異常都執行我finally...")
    
    print("正常邏輯代碼...")
    
    # ==== 執行結果 ====
    
    """
    開始執行我try了...
    3
    繼續執行我try...
    沒有異常執行我else...
    不管有沒有異常都執行我finally...
    正常邏輯代碼...
    """

     

    自定義異常

    raise主動拋出異常

      在某些時候我們可能需要主動的去阻止程序的運行,主動的拋出一個異常。可以使用raise來進行操作。這個是一種非常常用的手段。

    # ====  raise使用方法  ====
    
    print("----1----")
    print("----2----")
    print("----3----")
    raise Exception("我也不知道是什麼類型的異常...")
    print("----4----")
    print("----5----")
    print("----6----")
    
    # ==== 執行結果 ====
    
    """
    ----1----
    ----2----
    ----3----
    Traceback (most recent call last):
      File "C:/Users/Administrator/PycharmProjects/learn/元類編程.py", line 6, in <module>
        raise Exception("我也不知道是什麼類型的異常...")
    Exception: 我也不知道是什麼類型的異常...
    
    Process finished with exit code 1
    """

     

    自定義異常類

      前面已經說過一切皆對象,異常也來自一個對象。因此我們也可以自己來定製一個對象。注意,自定義異常類必須繼承BaseException類。

    # ====  自定義異常類  ====
    
    class MyError(BaseException):
        pass
    
    raise MyError("我的異常")
    
    
    
    # ==== 執行結果 ====
    
    """
    Traceback (most recent call last):
      File "C:/Users/Administrator/PycharmProjects/learn/元類編程.py", line 6, in <module>
        raise MyError("我的異常")
    __main__.MyError: 我的異常
    """

     

    擴展:斷言assert

      斷言是一個十分裝逼的使用,假設多個函數進行計算,我們已經有了預期的結果只是在做一個算法的設計。如果函數的最後的結果不是我們本來預期的結果那麼寧願讓他停止運行也不要讓錯誤繼續擴大,在這種情況下就可以使用斷言操作,使用斷言會拋出一個AssertionError類的異常。

    # ====  斷言assert  ====
    
    def calculate():
        """假設在做非常複雜的運算"""
        return 3+2*5
    
    res = calculate()
    
    assert res == 25  # AssertionError
    
    print("算法測試通過!你真的太厲害了")
    
    # ==== 執行結果 ====
    
    """
    Traceback (most recent call last):
      File "C:/Users/Administrator/PycharmProjects/learn/元類編程.py", line 8, in <module>
        assert res == 25
    AssertionError
    """

     

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

    【其他文章推薦】

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

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

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

    南投搬家公司費用需注意的眉眉角角,別等搬了再說!

    新北清潔公司,居家、辦公、裝潢細清專業服務

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

  • 在樹莓派上讀取土壤濕度傳感器讀數-Python代碼實現及常見問題(全面簡單易懂)

    在樹莓派上讀取土壤濕度傳感器讀數-Python代碼實現及常見問題(全面簡單易懂)

    本篇文章簡單介紹了如何在樹莓派上配置土壤濕度傳感器以讀取土壤濕度及代碼實現。

    主要包含有以下4個模塊:

    一、土壤濕度傳感器常見類型及介紹

    二、實驗所需設備

    三、設備連線方式與Python代碼實現

    四、常見問題及注意事項

    需要哪個模塊的內容直接跳轉去看即可~

     

    一、土壤濕度傳感器常見類型及介紹

     土壤濕度傳感器,又名土壤水分傳感器、土壤墒情傳感器、土壤含水量傳感器等。顧名思義,主要用來測量土壤相對含水量,做土壤墒情監測。在智能農業,農業灌溉和林業防護等領域極廣。該傳感器價格低廉,如果想在家製作一個簡易的智能自動化作物灌溉系統,有了它,再加上溫濕度傳感器、樹莓派/Arduino就可以輕鬆完成。

    常見的土壤傳感器分為兩類,電阻型和電容型土壤濕度傳感器。但原理大同小異,都是測量土壤中水分的體積含量,並以電壓表示水分值。

    (一)電阻型土壤濕度傳感器

    常見的有傳感器型號有YL-69(下圖左)和FC-28(下圖右)。這是一種低技術含量的傳感器這類傳感器由兩部分組成,帶探針的傳感器,A to D(模擬信號轉数字信號)电子板(校準靈敏度主板),兩者用母對母杜邦線連接。                

                它根據土壤的介電常數(土壤的導電能力)來估計土壤體積水含量,工作時,使用兩個探針讓電流通過土壤,然後讀取電阻來獲得濕度水平。水分越多,土壤導電越容易(電阻越小),而土壤乾燥,導電越差(電阻越大)。土壤中的濕度是一個連續變化的一系列值,為模擬信號,使用A to D接線板之後可以將從環境中得來的模擬信號轉成数字信號,在該板上有兩個指示燈,PWR-LED和DO-LED,前者檢測是否插好電源,如果電源的正負極連接正確,則會亮起,如下圖(左)所示。在這裏我使用的是YL-69型號,燈為綠色,也有一些廠商生產的傳感器指示燈為紅色。該傳感器在輸出数字信號時,可以使用改錐調節板上的電位計(藍色中間有十字架的部位)來提前設定閾值大小,一旦土壤濕度達到或大於閾值,則DO-LED亮起,如下圖(中)所示。

     电子板從左到右的標記為AO、DO、GND、VCC,如下圖(右)所示。AO和DO為信號引腳,如果需要模擬信號,則連接AO,輸出的模擬值是介於所提供的電壓值到0V間的變化的電壓值,如果輸出0V,則代表土壤導電性不好,即水分含量低,可以用這個電壓值來估計土壤濕度。如果需要数字信號輸出則連接DO,簡單的輸出0和1,可直接通過信號燈判斷土壤中水分是否低於閾值,高於則“開”,低則“關”。GND表示接地,VCC連接電源,但在這個項目中,我們將單獨利用模擬輸出。

                   

     該傳感器的優點是價格低廉,且有指示燈,觀察方便,但由於土壤環境是酸性的(acidic),隨着時間的推移,土壤里的化學物質會使得探針氧化(oxidize)導致測量不準確,所以需要時不時的進行更換以保證測量的準確性。

    (二)電容型傳感器

    相比較前一類型的傳感器,這類傳感器就顯得“光禿禿”了,只有一個組件,沒有指示燈,且只能輸出模擬信號。它區別於電阻傳感器,利用電容感應原理來檢測土壤濕度,避免了電阻式傳感器極易被腐蝕的問題,生命周期較長,缺點是不能用指示燈判斷傳感器是否正常工作,它同時只提供模擬信號。如圖設計DF Robot的一款傳感器,內置穩壓芯片,支持3.3V-5.5V寬電壓工作。DFRobot-Gravity接口具有兼容性,可直接和Gravity IO擴展板相連接。輸出電壓為0-3VDC。

    在自動化澆灌系統中,濕度傳感器用於測量土壤中水分,可以提前預設一個閾值,一旦土壤中的水分低於此閾值,則啟動連接着蓄水池(家用拿礦泉水瓶裝滿水替代即可)的水泵噴水,等到水分值超過預設值,則水泵暫停工作。

     

    二、實驗所需設備

    樹莓派 3 b+/樹莓 4 b/樹莓派Zero W

    MCP3008

    麵包板

    跳線(公對母,母對母,最好多準備一些)

    土壤傳感器(電容式、電阻式均可)

    一杯水(可以將傳感器放入水中來觀察濕度讀數的變化,若沒有條件也可直接用手握住傳感器的探針)

    MCP3008

    由於樹莓派沒有模擬信號引腳,所以沒有辦法直接輸出模擬信號數值,此時我們需要使用MCP3008集成電路。

    MCP3008 IC(Integrated Circuit)是一個8通道,10位的具有SPI串行接口的A / D轉換器(模擬-数字轉換器),共有16個引腳,可以用來解決模擬引腳問題(MCP3004也是ADC,不過為4路,體型更小)MCP3008使用SPI總線協議從樹莓派接收模擬輸入值。它有8個模擬輸入(ch0-ch7),另外一列的8個引腳中有4個電源和地引腳和4個連接樹莓派的引腳,它產生範圍為0-1023的輸出值(注意:0表示0V, 1023表示3.3V)。

    三、設備連線方式與代碼實現

    在此實驗中,主要介紹和使用的FC-28型號土壤濕度傳感器,但其它型號的傳感器使用,連線均與此相同。

    (一)設備連線方式

    1.電路圖及說明

    MCP3008共有16個引腳,其中8個用於記錄模擬輸入值,位於CH0-CH7(引腳1-8),4個通信引腳通過SPI協議方法與樹莓派通信,還有2個電源引腳,2個接地引腳。

    (二)代碼實現

    1. 啟用樹莓派的SPI接口

    樹莓派的SPI接口與SSH、VNC服務相同,是默認關閉的,需要我們在配置中打開此服務才可以使用。

    按照以下步驟啟動終端並輸入以下命令:

    (1)打開樹莓派配置選項

    sudo raspi-config
    

    (2)導航到Interface選項,啟用SPI接口。

    (3)重啟樹莓派

    reboot
    

    2. 安裝spidev庫

    光啟用SPI接口,但是樹莓派還是無法讀取傳感器傳過來的值,spidev庫將幫助通過SPI接口讀取傳感器值。

     使用以下命令安裝spidev庫:

    sudo apt-get install git python-dev
    git clone git://github.com/doceme/py-spidev
    cd py-spidev/
    sudo python setup.py install
    

    3. 安裝numpy庫

    我們從傳感器獲得的值還是電壓值而非土壤濕度值,土壤濕度需要使用百分比的形式體現,為了將輸出值轉換為百分比,還需要使用numpy庫。我們從MCP3008 IC接收到的輸出值是在前面提到的0-0123範圍內的數值,仍需要將把這些值映射到0-100,以得到一個百分比。

    使用以下命令安裝numpy模塊:

    sudo apt-get install python-numpy
    

    4. Python代碼

    # Importing modules
    import spidev # To communicate with SPI devices
    from numpy import interp  # To scale values
    from time import sleep  # To add delay
    
    
    # Start SPI connection
    spi = spidev.SpiDev() # Created an object
    spi.open(0,0) 
    
    
    # Read MCP3008 data
    def analogInput(channel):
      spi.max_speed_hz = 1350000
      adc = spi.xfer2([1,(8+channel)<<4,0])
      data = ((adc[1]&3) << 8) + adc[2]
      return data
    
    
    while True:
      output = analogInput(0) # Reading from CH0
      output = interp(output, [0, 1023], [100, 0])
      output = int(output)
      print("Moisture:", output)
      sleep(0.1)

    當從土壤濕度傳感器讀取模擬輸出值時,它以百分比測量濕度,使用來自numpy庫的特定interp模塊進行映射得到從0-100的值。

    四、常見問題及注意事項

    (一)常見問題

    1. 持續輸出0或100,無論探針是否放入水中均沒有變化

    2. 沒有操作探針,但讀數呈有規律地變化

    針對以上出現有以下幾種解決方案

    1.檢查樹莓派的SPI服務有無正確打開

    2.先檢查線有沒有接穩,查看是不是線的連接順序(傳感器的正負極有沒有接反,與樹莓派的連線有沒有串行)有誤

    3. 線是否有生鏽或損壞(之前第一次做實驗時,各種調試都出現不了正確結果,後來才發現是有幾根線生鏽了所以不通)

    (二)注意事項 

    1. 盡量使用長線,便於看清連線位置

    2. 盡量不要使用拼接線(一根公對公,一根母對母拼接成公對母的),這樣需要照顧的線更多,也更容易出紕漏

    3. 一定一定要有耐心,出現問題后按照順序逐一排查。因為涉及到的連線較多,對硬件小白來說,很容易眼花繚亂想放棄,但是太簡單的東西誰都能做,能攻破學習或者生活中一個個難關的人才能有所成長啊~

    如果你在配置土壤濕度傳感器時或使用樹莓派時遇見了什麼問題,歡迎在評論區寫下,看到了會及時答覆。期待與大家一起學習。

    文字及圖片部分來源:https://maker.pro/raspberry-pi/tutorial/interfacing-soil-moisture-sensor-with-raspberry-pi

    轉發請標明來源。祝大家學派happy!

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

    【其他文章推薦】

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

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

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

    ※幫你省時又省力,新北清潔一流服務好口碑

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

  • 都在講DevOps,但你知道它的發展趨勢嗎?

    都在講DevOps,但你知道它的發展趨勢嗎?

    根據最近的一項集體研究,DevOps的市場在2017年創造了約29億美元的產值,預計到2022年,這個数字將達到約66億美元。人工智能的融入和安全性的融入,加上向自動化的巨大轉變,可合理預測,在2020年,DevOps將成為軟件工程的主流模式。

    DevOps具有以下優勢:

    ●對需求變更的迅速響應

    ●超快的交付速度及靈活的安全部署

    ●建立完善的協作溝通渠道

    ●快速識別代碼中的錯誤或漏洞

    ●讓團隊將注意力集中在其他關鍵的事情上,而不是集中在安全特性上

     

    越來越多的企業正採用DevOps的產品交付模式:根據Statista的統計數據,全面採用DevOps的企業數量從2017年的約10%增長到了2018年的17%。

    而devops也將在2020年迎來新趨勢。

     

    自動化成為焦點

    實施DevOps產品交付模式的組織已經見證了極高的效率和超快速的部署速度。在提到DevOps時,我們主要討論的是DevOps自動化,零接觸自動化是未來的發展方向。在DevOps生命周期的7C(持續發展、持續集成、持續測試、持續反饋、持續監測、持續部署、持續運維)中,應用自動化是未來的關鍵,因為預計這將是2020年的主要目標之一。

    注意力從CI管道轉移到DevOps的裝配線

    DevOps的重要目標是改進交付過程的計劃階段和自動化階段之間的協作。這不僅僅關乎CI(持續集成),更重要的是關乎CD(持續交付)。許多組織正在投入額外的精力和時間來使公司軟件開發的整個過程自動化。因此,對於這些組織來說,現在是聯繫DevOps諮詢服務提供商的時候了。預計到2020年,注意力將從CI管道轉移到DevOps的裝配線。裝配線的一些共同優點如下:

    ●原生集成

    ●堅固的嵌套可見性

    ●適當互用性的完美持續交付

    ●基於團隊的分析以及商業智能

    ●快速實現和擴展“一切皆代碼”理念

     

    對無服務器架構的使用增加

    使用無服務器架構可以將DevOps提升到更高的水平,這並不意味着沒有服務器,而是使用雲服務的整體架構。FaaS(Function as a Service,功能即服務)和BaaS(Backend as a Service,後端即服務)是無服務器架構的兩個關鍵方面。通過採用這種無服務器體繫結構,企業可以節省時間、降低成本,並擁有具有彈性的、靈活的工作流。

    “一切皆代碼”的概念

    程序編碼是IT部門及其服務系統的骨幹。對DevOps自動化工具和腳本的充分理解將支配整個2020年。這個特定IT領域的前景與產品的未來取決於開發人員、測試人員及運維人員的技術能力。現在,隨着交付周期的縮短,需要引入代碼來提高軟件生產周期的效率。“一切皆代碼”的概念是在DevOps內部完成代碼的SDLC的實踐。如果軟件測試人員還不開始學習編程和編寫測試腳本,工作很可能會受到阻礙。

    更好的嵌入式安全性

    隨着安全漏洞的出現,越來越多的大小企業意識到網絡安全的重要性。2020年,DevOps預計將迅速將安全問題納入流程。DevSecOps首先在應用程序的開發生命周期中注入安全性,這有助於減少各種缺陷和漏洞,增加業務信譽。公司轉向DevSecOps促使項目中每個人都擔負安全方面的責任,這將在軟件開發過程中帶來很棒的協作,因為它確保了軟件開發過程始終保持完美、高效和可操作。

    人工智能的興起和數據科學的飛速發展

    隨着人工智能驅動的應用程序大量增加,數據科學正在推動越來越多的公司在其工作流程中採用DevOps理念。隨着數據科學和開發團隊在軟件開發、部署以及人工智能驅動的應用程序管理方面的效率越來越高,這將會進一步推動數據科學的發展。

    2020年的主要目標是實現零接觸自動化。 持續不斷的人工智能和數據科學熱潮改變着遊戲規則。 許多應用程序都引入了人工智能,這已經促使多個DevOps團隊通過人工智能和數據科學實現自動化,數據科學團隊和開發團隊相輔相成地提高彼此的技能與交付水平。

    對無服務器架構的使用增加

    使用無服務器架構可以將DevOps提升到更高的水平,這並不意味着沒有服務器,而是使用雲服務的整體架構。FaaS(Function as a Service,功能即服務)和BaaS(Backend as a Service,後端即服務)是無服務器架構的兩個關鍵方面。通過採用這種無服務器體繫結構,企業可以節省時間、降低成本,並擁有具有彈性的、靈活的工作流。

    Kubernetes長足發展

    Kubernetes提供了基於容器技術的分佈式架構領先方案產品,因自身性能及易用性,已經成為應用廣泛的容器技術。伴隨着各類企業進一步通過深度採用容器技術來運行它們的雲原生應用,K8s將會迎來更廣的普及、更大的發展。

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

    【其他文章推薦】

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

    新北清潔公司,居家、辦公、裝潢細清專業服務

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

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

    ※超省錢租車方案

  • 使用 Masstransit中的 Request/Response 與 Courier 功能實現最終一致性

    簡介

      目前的.net 生態中,最終一致性組件的選擇一直是一個問題。本地事務表(cap)需要在每個服務的數據庫中插入消息表,而且做不了此類事務 比如:創建訂單需要 餘額滿足+庫存滿足,庫存和餘額處於兩個服務中。masstransit 是我目前主要用的方案。以往一般都用 masstransit 中的 sagas 來實現 最終一致性,但是隨着併發的增加必定會對sagas 持久化的數據庫造成很大的壓力,根據stackoverflow 中的一個回答 我發現了 一個用  Request/Response 與 Courier 功能 實現最終一致性的方案 Demo地址。

    Masstransit 中 Resquest/Response 功能 

     消息DTO

        public class SampleMessageCommand
        {
        }
    

     消費者

        public class SampleMessageCommandHandler : IConsumer<SampleMessageCommand>
        {
            public async Task Consume(ConsumeContext<SampleMessageCommand> context)
            {
                await context.RespondAsync(new SampleMessageCommandResult() { Data = "Sample" });
            }
        }
    

     返回結果DTO

     

        public class SampleMessageCommandResult
        {
            public string Data { get; set; }
        }
    

     調用方式與註冊方式略過,詳情請看 官方文檔。

      

      本質上使用消息隊列實現 Resquest/Response,客戶端(生產者)將請求消息發送至指定消息隊列並賦予RequestId和ResponseAddress(臨時隊列 rabbitmq),服務端(消費者)消費消息並把 需要返回的消息放入指定ResponseAddress,客戶端收到 Response message  通過匹配 RequestId 找到 指定Request,最後返回信息。

    Masstransit 中 Courier  功能

      通過有序組合一系列的Activity,得到一個routing slip。每個 activity(忽略 Execute Activities) 都有 Execute 和 Compensate 兩個方法。Compensate 用來執撤銷 Execute 方法產生的影響(就是回退 Execute 方法)。每個 Activity Execute 最後都會 調用 Completed 方法把 回退所需要的的信息記錄在message中,最後持久化到消息隊列的某一個消息中。

     餘額扣減的Activity ,這裏的 DeductBalanceModel 是請求扣減的數據模型,DeductBalanceLog 是回退時需要用到的信息。

    public class DeductBalanceActivity : IActivity<DeductBalanceModel, DeductBalanceLog>
        {
            private readonly ILogger<DeductBalanceActivity> logger;
            public DeductBalanceActivity(ILogger<DeductBalanceActivity> logger)
            {
                this.logger = logger;
            }
            public async Task<CompensationResult> Compensate(CompensateContext<DeductBalanceLog> context)
            {
                logger.LogInformation("還原餘額");
                var log = context.Log; //可以獲取 所有execute 完成時保存的信息
                //throw new ArgumentException("some things were wrong");
                return context.Compensated();
            }
    
            public async Task<ExecutionResult> Execute(ExecuteContext<DeductBalanceModel> context)
            {
    
                logger.LogInformation("扣減餘額");
                await Task.Delay(100);
                return context.Completed(new DeductBalanceLog() { Price = 100 });
            }
        }

     

          扣減庫存 Activity

        public class DeductStockActivity : IActivity<DeductStockModel, DeductStockLog>
        {
            private readonly ILogger<DeductStockActivity> logger;
            public DeductStockActivity(ILogger<DeductStockActivity> logger)
            {
                this.logger = logger;
            }
            public async Task<CompensationResult> Compensate(CompensateContext<DeductStockLog> context)
            {
                var log = context.Log;
                logger.LogInformation("還原庫存");
                return context.Compensated();
            }
    
            public async Task<ExecutionResult> Execute(ExecuteContext<DeductStockModel> context)
            {
                var argument = context.Arguments;
                logger.LogInformation("扣減庫存");
                await Task.Delay(100);
                return context.Completed(new DeductStockLog() { ProductId = argument.ProductId, Amount = 1 });
            }
        }
    

           生成訂單 Execute Activity

        public class CreateOrderActivity : IExecuteActivity<CreateOrderModel>
        {
            private readonly ILogger<CreateOrderActivity> logger;
            public CreateOrderActivity(ILogger<CreateOrderActivity> logger)
            {
                this.logger = logger;
            }
            public async Task<ExecutionResult> Execute(ExecuteContext<CreateOrderModel> context)
            {
                logger.LogInformation("創建訂單");
                await Task.Delay(100);
                //throw new CommonActivityExecuteFaildException("當日訂單已達到上限");
                return context.CompletedWithVariables(new CreateOrderResult { OrderId="111122",Message="創建訂單成功" });
            }
        }
    

      組裝 以上 Activity 生成一個 Routing Slip,這是一個有序的組合,扣減庫存=》扣減餘額=》生成訂單

                var builder = new RoutingSlipBuilder(NewId.NextGuid());
    builder.AddActivity("DeductStock", new Uri($"{configuration["RabbitmqConfig:HostUri"]}/DeductStock_execute"), new DeductStockModel { ProductId = request.Message.ProductId }); builder.AddActivity("DeductBalance", new Uri($"{configuration["RabbitmqConfig:HostUri"]}/DeductBalance_execute"), new DeductBalanceModel { CustomerId = request.Message.CustomerId, Price = request.Message.Price }); builder.AddActivity("CreateOrder", new Uri($"{configuration["RabbitmqConfig:HostUri"]}/CreateOrder_execute"), new CreateOrderModel { Price = request.Message.Price, CustomerId = request.Message.CustomerId, ProductId = request.Message.ProductId });
    var routingSlip = builder.Build();

      執行 Routing Slip

    await bus.Execute(routingSlip);
    

      

          這裡是沒有任何返回值的,所有activity都是 異步執行,雖然所有的activity可以執行完成或者由於某個Activity執行出錯而全部回退。(其實這裡有一種更壞的情況就是 Compensate 出錯,默認情況下 Masstransit 只會發送一個回退錯誤的消息,後面講到創建訂單的時候我會把它塞到錯誤隊列里,這樣我們可以通過修改 Compensate bug后重新導入到正常隊列來修正數據),這個功能完全滿足不了 創建訂單這個需求,執行 await bus.Execute(routingSlip) 后我們完全不知道訂單到底創建成功,還是由於庫存或餘額不足而失敗了(異步)。

         還好 routing slip 在執行過程中產生很多消息,比如 RoutingSlipCompleted ,RoutingSlipCompensationFailed ,RoutingSlipActivityCompleted,RoutingSlipActivityFaulted 等,具體文檔,我們可以訂閱這些事件,再結合Request/Response 實現 創建訂單的功能。

    實現創建訂單(庫存滿足+餘額滿足)長流程

    創建訂單 command 

        /// <summary>
        /// 長流程 分佈式事務
        /// </summary>
        public class CreateOrderCommand
        {
            public string ProductId { get; set; }
            public string CustomerId { get; set; }
            public int Price { get; set; }
        }

      事務第一步,扣減庫存相關 代碼

      public class DeductStockActivity : IActivity<DeductStockModel, DeductStockLog>
        {
            private readonly ILogger<DeductStockActivity> logger;
            public DeductStockActivity(ILogger<DeductStockActivity> logger)
            {
                this.logger = logger;
            }
            public async Task<CompensationResult> Compensate(CompensateContext<DeductStockLog> context)
            {
                var log = context.Log;
                logger.LogInformation("還原庫存");
                return context.Compensated();
            }
    
            public async Task<ExecutionResult> Execute(ExecuteContext<DeductStockModel> context)
            {
                var argument = context.Arguments;
                logger.LogInformation("扣減庫存");
                await Task.Delay(100);
                return context.Completed(new DeductStockLog() { ProductId = argument.ProductId, Amount = 1 });
            }
        }
        public class DeductStockModel
        {
            public string ProductId { get; set; }
        }
        public class DeductStockLog
        {
            public string ProductId { get; set; }
            public int Amount { get; set; }
        }

     事務第二步,扣減餘額相關代碼

    public class DeductBalanceActivity : IActivity<DeductBalanceModel, DeductBalanceLog>
        {
            private readonly ILogger<DeductBalanceActivity> logger;
            public DeductBalanceActivity(ILogger<DeductBalanceActivity> logger)
            {
                this.logger = logger;
            }
            public async Task<CompensationResult> Compensate(CompensateContext<DeductBalanceLog> context)
            {
                logger.LogInformation("還原餘額");
                var log = context.Log;
                //throw new ArgumentException("some things were wrong");
                return context.Compensated();
            }
    
            public async Task<ExecutionResult> Execute(ExecuteContext<DeductBalanceModel> context)
            {
    
                logger.LogInformation("扣減餘額");
                await Task.Delay(100);
                return context.Completed(new DeductBalanceLog() { Price = 100 });
            }
        }
        public class DeductBalanceModel
        {
            public string CustomerId { get; set; }
            public int Price { get; set; }
        }
        public class DeductBalanceLog
        {
            public int Price { get; set; }
        }

     事務第三步,創建訂單相關代碼

     public class CreateOrderActivity : IExecuteActivity<CreateOrderModel>
        {
            private readonly ILogger<CreateOrderActivity> logger;
            public CreateOrderActivity(ILogger<CreateOrderActivity> logger)
            {
                this.logger = logger;
            }
            public async Task<ExecutionResult> Execute(ExecuteContext<CreateOrderModel> context)
            {
                logger.LogInformation("創建訂單");
                await Task.Delay(100);
                //throw new CommonActivityExecuteFaildException("當日訂單已達到上限");
                return context.CompletedWithVariables(new CreateOrderResult { OrderId="111122",Message="創建訂單成功" });
            }
        }
        public class CreateOrderModel
        {
            public string ProductId { get; set; }
            public string CustomerId { get; set; }
            public int Price { get; set; }
        }
        public class CreateOrderResult
        {
            public string OrderId { get; set; }
            public string Message { get; set; }
        }

       我通過 消費 創建訂單 request,獲取 request 的 response 地址與 RequestId,這兩個值 返回 response 時需要用到,我把這些信息存到 RoutingSlip中,並且訂閱 RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted | RoutingSlipEvents.CompensationFailed 三種事件,當這三種消息出現時 我會根據 事件類別 和RoutingSlip中 之前加入的 (response 地址與 RequestId)生成 Response ,整個過程大概就是這麼個意思,沒理解可以看demo。這裏由於每一個事物所需要用到的 RoutingSlip + Request/Response 步驟都類似 可以抽象一下(模板方法),把Activity 的組裝 延遲到派生類去解決,這個代理類Masstransit有 ,但是官方沒有顧及到 CompensationFailed 的情況,所以我乾脆自己再寫一個。

        public abstract class RoutingSlipDefaultRequestProxy<TRequest> :
            IConsumer<TRequest>
            where TRequest : class
        {
            public async Task Consume(ConsumeContext<TRequest> context)
            {
                var builder = new RoutingSlipBuilder(NewId.NextGuid());
    
                builder.AddSubscription(context.ReceiveContext.InputAddress, RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted | RoutingSlipEvents.CompensationFailed);
                
                builder.AddVariable("RequestId", context.RequestId);
                builder.AddVariable("ResponseAddress", context.ResponseAddress);
                builder.AddVariable("FaultAddress", context.FaultAddress);
                builder.AddVariable("Request", context.Message);
    
                await BuildRoutingSlip(builder, context);
    
                var routingSlip = builder.Build();
    
                await context.Execute(routingSlip).ConfigureAwait(false);
            }
    
            protected abstract Task BuildRoutingSlip(RoutingSlipBuilder builder, ConsumeContext<TRequest> request);
        }


     這個 是派生類 Routing slip 的拼裝過程 

        public class CreateOrderRequestProxy : RoutingSlipDefaultRequestProxy<CreateOrderCommand>
    
        {
            private readonly IConfiguration configuration;
            public CreateOrderRequestProxy(IConfiguration configuration)
            {
                this.configuration = configuration;
            }
            protected override Task BuildRoutingSlip(RoutingSlipBuilder builder, ConsumeContext<CreateOrderCommand> request)
            {
                builder.AddActivity("DeductStock", new Uri($"{configuration["RabbitmqConfig:HostUri"]}/DeductStock_execute"), new DeductStockModel { ProductId = request.Message.ProductId });
    
                builder.AddActivity("DeductBalance", new Uri($"{configuration["RabbitmqConfig:HostUri"]}/DeductBalance_execute"), new DeductBalanceModel { CustomerId = request.Message.CustomerId, Price = request.Message.Price });
    
                builder.AddActivity("CreateOrder", new Uri($"{configuration["RabbitmqConfig:HostUri"]}/CreateOrder_execute"), new CreateOrderModel { Price = request.Message.Price, CustomerId = request.Message.CustomerId, ProductId = request.Message.ProductId });
    
                return Task.CompletedTask;
            }
        }

      構造response 基類,主要是對三種情況做處理。

     

        public abstract class RoutingSlipDefaultResponseProxy<TRequest, TResponse, TFaultResponse> : IConsumer<RoutingSlipCompensationFailed>, IConsumer<RoutingSlipCompleted>,
            IConsumer<RoutingSlipFaulted>
            where TRequest : class
            where TResponse : class
            where TFaultResponse : class
        {
            public async Task Consume(ConsumeContext<RoutingSlipCompleted> context)
            {
                var request = context.Message.GetVariable<TRequest>("Request");
                var requestId = context.Message.GetVariable<Guid>("RequestId");
    
                Uri responseAddress = null;
                if (context.Message.Variables.ContainsKey("ResponseAddress"))
                    responseAddress = context.Message.GetVariable<Uri>("ResponseAddress");
    
                if (responseAddress == null)
                    throw new ArgumentException($"The response address could not be found for the faulted routing slip: {context.Message.TrackingNumber}");
    
                var endpoint = await context.GetResponseEndpoint<TResponse>(responseAddress, requestId).ConfigureAwait(false);
    
                var response = await CreateResponseMessage(context, request);
    
                await endpoint.Send(response).ConfigureAwait(false);
            }
    
            public async Task Consume(ConsumeContext<RoutingSlipFaulted> context)
            {
                var request = context.Message.GetVariable<TRequest>("Request");
                var requestId = context.Message.GetVariable<Guid>("RequestId");
    
                Uri faultAddress = null;
                if (context.Message.Variables.ContainsKey("FaultAddress"))
                    faultAddress = context.Message.GetVariable<Uri>("FaultAddress");
                if (faultAddress == null && context.Message.Variables.ContainsKey("ResponseAddress"))
                    faultAddress = context.Message.GetVariable<Uri>("ResponseAddress");
    
                if (faultAddress == null)
                    throw new ArgumentException($"The fault/response address could not be found for the faulted routing slip: {context.Message.TrackingNumber}");
    
                var endpoint = await context.GetFaultEndpoint<TResponse>(faultAddress, requestId).ConfigureAwait(false);
    
                var response = await CreateFaultedResponseMessage(context, request, requestId);
    
                await endpoint.Send(response).ConfigureAwait(false);
            }
            public async Task Consume(ConsumeContext<RoutingSlipCompensationFailed> context)
            {
                var request = context.Message.GetVariable<TRequest>("Request");
                var requestId = context.Message.GetVariable<Guid>("RequestId");
    
                Uri faultAddress = null;
                if (context.Message.Variables.ContainsKey("FaultAddress"))
                    faultAddress = context.Message.GetVariable<Uri>("FaultAddress");
                if (faultAddress == null && context.Message.Variables.ContainsKey("ResponseAddress"))
                    faultAddress = context.Message.GetVariable<Uri>("ResponseAddress");
    
                if (faultAddress == null)
                    throw new ArgumentException($"The fault/response address could not be found for the faulted routing slip: {context.Message.TrackingNumber}");
    
                var endpoint = await context.GetFaultEndpoint<TResponse>(faultAddress, requestId).ConfigureAwait(false);
    
                var response = await CreateCompensationFaultedResponseMessage(context, request, requestId);
    
                await endpoint.Send(response).ConfigureAwait(false);
            }
            protected abstract Task<TResponse> CreateResponseMessage(ConsumeContext<RoutingSlipCompleted> context, TRequest request);
    
            protected abstract Task<TFaultResponse> CreateFaultedResponseMessage(ConsumeContext<RoutingSlipFaulted> context, TRequest request, Guid requestId);
            protected abstract Task<TFaultResponse> CreateCompensationFaultedResponseMessage(ConsumeContext<RoutingSlipCompensationFailed> context, TRequest request, Guid requestId);
        }

     Response 派生類 ,這裏邏輯可以隨自己定義,我也是隨便寫了個 CommonResponse和一個業務錯誤拋錯(犧牲了一點性能)。

        public class CreateOrderResponseProxy :
                RoutingSlipDefaultResponseProxy<CreateOrderCommand, CommonCommandResponse<CreateOrderResult>, CommonCommandResponse<CreateOrderResult>>
        {
    
            protected override Task<CommonCommandResponse<CreateOrderResult>> CreateResponseMessage(ConsumeContext<RoutingSlipCompleted> context, CreateOrderCommand request)
            {
    
                return Task.FromResult(new CommonCommandResponse<CreateOrderResult>
                {
                    Status = 1,
                    Result = new CreateOrderResult
                    {
                        Message = context.Message.Variables.TryGetAndReturn(nameof(CreateOrderResult.Message))?.ToString(),
                        OrderId = context.Message.Variables.TryGetAndReturn(nameof(CreateOrderResult.OrderId))?.ToString(),
                    }
                });
            }
            protected override Task<CommonCommandResponse<CreateOrderResult>> CreateFaultedResponseMessage(ConsumeContext<RoutingSlipFaulted> context, CreateOrderCommand request, Guid requestId)
            {
                var commonActivityExecuteFaildException = context.Message.ActivityExceptions.FirstOrDefault(m => m.ExceptionInfo.ExceptionType == typeof(CommonActivityExecuteFaildException).FullName);
                if (commonActivityExecuteFaildException != null)
                {
                    return Task.FromResult(new CommonCommandResponse<CreateOrderResult>
                    {
                        Status = 2,
                        Message = commonActivityExecuteFaildException.ExceptionInfo.Message
                    });
                }
                // system error  log here
                return Task.FromResult(new CommonCommandResponse<CreateOrderResult>
                {
                    Status = 3,
                    Message = "System error"
                });
            }
    
            protected override Task<CommonCommandResponse<CreateOrderResult>> CreateCompensationFaultedResponseMessage(ConsumeContext<RoutingSlipCompensationFailed> context, CreateOrderCommand request, Guid requestId)
            {
                var exception = context.Message.ExceptionInfo;
                // lg here context.Message.ExceptionInfo
                return Task.FromResult(new CommonCommandResponse<CreateOrderResult>
                {
                    Status = 3,
                    Message = "System error"
                });           
            }
        }

    對於  CompensationFailed 的處理 通過 ActivityCompensateErrorTransportFilter 實現 發送到錯誤消息隊列,後續通過prometheus + rabbitmq-exporter + alertmanager 觸發告警 通知相關人員處理。

      public class ActivityCompensateErrorTransportFilter<TActivity, TLog> : IFilter<CompensateActivityContext<TActivity, TLog>>
            where TActivity : class, ICompensateActivity<TLog>
            where TLog : class
        {
            public void Probe(ProbeContext context)
            {
                context.CreateFilterScope("moveFault");
            }
    
            public async Task Send(CompensateActivityContext<TActivity, TLog> context, IPipe<CompensateActivityContext<TActivity, TLog>> next)
            {
                try
                {
                    await next.Send(context).ConfigureAwait(false);
                }
                catch(Exception ex)
                {
                    if (!context.TryGetPayload(out IErrorTransport transport))
                        throw new TransportException(context.ReceiveContext.InputAddress, $"The {nameof(IErrorTransport)} was not available on the {nameof(ReceiveContext)}.");
                    var exceptionReceiveContext = new RescueExceptionReceiveContext(context.ReceiveContext, ex);
                    await transport.Send(exceptionReceiveContext);
                }
            }
        }

    註冊 filter 

        public class RoutingSlipCompensateErrorSpecification<TActivity, TLog> : IPipeSpecification<CompensateActivityContext<TActivity, TLog>>
            where TActivity : class, ICompensateActivity<TLog>
            where TLog : class
        {
            public void Apply(IPipeBuilder<CompensateActivityContext<TActivity, TLog>> builder)
            {
                builder.AddFilter(new ActivityCompensateErrorTransportFilter<TActivity, TLog>());
            }
    
            public IEnumerable<ValidationResult> Validate()
            {
               yield return this.Success("success");
            }
        }
    
    
                cfg.ReceiveEndpoint("DeductStock_compensate", ep =>
                {
                    ep.PrefetchCount = 100;
                    ep.CompensateActivityHost<DeductStockActivity, DeductStockLog>(context.Container, conf =>
                     {
                         conf.AddPipeSpecification(new RoutingSlipCompensateErrorSpecification<DeductStockActivity, DeductStockLog>());
                     });
    
                });

     

    實現創建產品(創建完成+添加庫存)

    實現了 創建訂單的功能,整個流程其實是同步的,我在想能不能實現最為簡單的最終一致性 比如 創建一個產品 ,然後異步生成它的庫存 ,我發現是可以的,因為我們可以監聽到每一個Execute Activity 的完成事件,並且把出錯時的信息通過 filter 塞到 錯誤隊列中。

    這裏的代碼就不貼了,詳情請看 demo

     

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

    【其他文章推薦】

    新北清潔公司,居家、辦公、裝潢細清專業服務

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

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

    ※超省錢租車方案

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

  • 看準中國市場 LG 化學在南京建電動車電池工廠

    韓國 LG 化學今(10)日表示,2015 年將在中國建電動車電池工廠,此舉是在押注身為全球最大汽車市場的中國需求將持續增加。

    LG 化學稱,這座工廠設在中國南京,將滿足上汽集團等中國汽車製造商和通用汽車 (GM) 等全球性企業的需求,工廠耗資數億美元,預計 2020 年綜合營收將達到 1 兆韓元 ( 約 9.899 億美元)。

    本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※為什麼 USB CONNECTOR 是電子產業重要的元件?

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

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

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

    新北清潔公司,居家、辦公、裝潢細清專業服務

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

  • 雷諾與法國富豪攜手 搶進電動車市場

    法國雷諾集團(Renault SA)與法國富豪波洛黑(Vincent Bollore)決定合作,共同製造電動車,瞄準零排放汽車日益強勁的需求,及正於各地興起的環保汽車租用潮流。   雷諾汽車將於 2015 下半年開始生產波洛黑的電動車款「藍車(Bluecar)」,但並未透露生產目標。該車款自 2011 年起,被使用於巴黎名為「Autolib」的電動車租賃共享計畫中,此計畫同時也於里昂及波爾多等地運行。   此外,雷諾汽車與波洛黑亦宣布,將成立一策略聯盟,其中波洛黑擁有 70% 股權,雷諾股權則達 30%。該聯盟將於法國及歐洲其他地方提供車輛共享服務。另外並進行研究,以協助雷諾製造一款 3 人座、使用波洛黑所生產電池的電動車。

    本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理
    【其他文章推薦】

    USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

    台北網頁設計公司這麼多該如何選擇?

    ※智慧手機時代的來臨,RWD網頁設計為架站首選

    ※評比南投搬家公司費用收費行情懶人包大公開

    ※幫你省時又省力,新北清潔一流服務好口碑

    ※回頭車貨運收費標準

  • ASP.NET Core Blazor Webassembly 之 漸進式應用(PWA)

    ASP.NET Core Blazor Webassembly 之 漸進式應用(PWA)

    Blazor支持漸進式應用開發也就是PWA。使用PWA模式可以使得web應用有原生應用般的體驗。

    什麼是PWA

    PWA應用是指那些使用指定技術和標準模式來開發的web應用,這將同時賦予它們web應用和原生應用的特性。
    例如,web應用更加易於發現——相比於安裝應用,訪問一個網站顯然更加容易和迅速,並且你可以通過一個鏈接來分享web應用。
    在另一方面,原生應用與操作系統可以更加完美的整合,也因此為用戶提供了無縫的用戶體驗。你可以通過安裝應用使得它在離線的狀態下也可以運行,並且相較於使用瀏覽器訪問,用戶也更喜歡通過點擊主頁上的圖標來訪問它們喜愛的應用。
    PWA賦予了我們創建同時擁有以上兩種優勢的應用的能力。
    這並不是一個新概念——這樣的想法在過去已經在web平台上通過許多方法出現了多次。漸進式增強和響應式設計已經可以讓我們構建對移動端友好的網站。在多年以前的Firefox OS的生態系統中離線運行和安裝web應用已經成為了可能。
    PWAs, 不但如此,更是提供了所有的甚至是更多的特性,來讓web更加優秀。

    引用自MDN

    說人話就是PWA可以讓你的web程序跟一般應用一樣運行,有桌面圖標,能離線,沒有瀏覽器地址欄,一切看起來想個普通的程序/APP。

    新建Blazor PWA程序

    使用VS新建一個Blazor程序,選擇Webassembly模式,勾選支持PWA。

    支持PWA的Blazor程序主要是多了幾個東西:

    1. manifest.json
    2. service-worker.js

    manifest.json

    manifest.json是個清單文件,當程序被安裝到設備上的時候會讀取裏面的信息,名稱是什麼,圖標是什麼,什麼語言等等。

    {
      "name": "BlazorPWA",
      "short_name": "BlazorPWA",
      "start_url": "./",
      "display": "standalone",
      "background_color": "#ffffff",
      "theme_color": "#03173d",
      "icons": [
        {
          "src": "icon-512.png",
          "type": "image/png",
          "sizes": "512x512"
        }
      ]
    }
    
    

    service-worker.js

    service-worker用來跑一些後台任務。它跟瀏覽器主進程是隔離的,也就是說跟原來的JavaScript運行時是分開,當然了它不會阻塞頁面。我們可以用它來完成一些功能,比如對所有的fetch/xhr請求進行過濾,哪些請求走緩存,哪些不走緩存;比如在後台偷偷給你拉一些數據緩存起來。

    // Caution! Be sure you understand the caveats before publishing an application with
    // offline support. See https://aka.ms/blazor-offline-considerations
    
    self.importScripts('./service-worker-assets.js');
    self.addEventListener('install', event => event.waitUntil(onInstall(event)));
    self.addEventListener('activate', event => event.waitUntil(onActivate(event)));
    self.addEventListener('fetch', event => event.respondWith(onFetch(event)));
    
    const cacheNamePrefix = 'offline-cache-';
    const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`;
    const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/ ];
    const offlineAssetsExclude = [ /^service-worker\.js$/ ];
    
    async function onInstall(event) {
        console.info('Service worker: Install');
    
        // Fetch and cache all matching items from the assets manifest
        const assetsRequests = self.assetsManifest.assets
            .filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url)))
            .filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url)))
            .map(asset => new Request(asset.url, { integrity: asset.hash }));
        await caches.open(cacheName).then(cache => cache.addAll(assetsRequests));
    }
    
    async function onActivate(event) {
        console.info('Service worker: Activate');
    
        // Delete unused caches
        const cacheKeys = await caches.keys();
        await Promise.all(cacheKeys
            .filter(key => key.startsWith(cacheNamePrefix) && key !== cacheName)
            .map(key => caches.delete(key)));
    }
    
    async function onFetch(event) {
        let cachedResponse = null;
        if (event.request.method === 'GET') {
            // For all navigation requests, try to serve index.html from cache
            // If you need some URLs to be server-rendered, edit the following check to exclude those URLs
            const shouldServeIndexHtml = event.request.mode === 'navigate';
    
            const request = shouldServeIndexHtml ? 'index.html' : event.request;
            const cache = await caches.open(cacheName);
            cachedResponse = await cache.match(request);
        }
    
        return cachedResponse || fetch(event.request);
    }
    
    

    項目里有2個service-worker.js文件,一個是開發時候的沒邏輯,還有一個是發布時候的有一些緩存的邏輯。

    運行一下

    如果是PWA程序,在瀏覽器地址欄有個+號一樣的圖標,點擊可以把程序安裝到本地。

    安裝完了會在桌面生成一個圖標,打開會是一個沒有瀏覽器地址欄的界面。

    這樣一個PWA程序已經可以運行了。

    離線運行

    如果只是這樣,僅僅是沒有瀏覽器地址欄,那PWA也太沒什麼吸引力了。個人覺得PWA最大的魅力就是可以離線運行,在沒有網絡的情況下依然可以運行,這樣才像一個原生編寫的程序。

    修改service-worker

    離線的原理也很簡單,就是請求的數據都緩存起來,一般是緩存Get請求,比如各種頁面圖片等。

    // In development, always fetch from the network and do not enable offline support.
    // This is because caching would make development more difficult (changes would not
    // be reflected on the first load after each change).
    
    self.addEventListener('fetch', event => event.respondWith(onFetch(event)));
    self.addEventListener('install', event => event.waitUntil(onInstall(event)));
    
    async function onInstall(event) {
        console.info('Service worker: Install');
    }
    
    
    async function onFetch(event) {
        let cachedResponse = null;
        const cache = await caches.open('blazor_pwa');
        if (event.request.method === 'GET') {
            const request = event.request;
            cachedResponse = await caches.match(request);
            if (cachedResponse) {
                return cachedResponse;
            }
            var resp = await fetch(event.request)
            cache.put(event.request, resp.clone());
            return resp;
        }
    
        return fetch(event.request);
    }
    

    修改一下sevice-worker.js,把GET請求全部緩存起來。這裏為了演示圖方便,其實情況顯然不會這麼簡單粗暴。為了能緩存頁面,顯然必須先在線運行成功一次。

    模擬離線

    當我們修改完上面的js,然後在線正常一次后,可以看到所有GET請求的資源都被緩存起來了。

    我們可以用chrome來模擬離線情況:

    選擇offline模式,然後刷新我們的頁面,如果依然可以正常運行則表示可以離線運行。

    總結

    使用Blazor可以快速的開發PWA應用。利用PWA跟Blazor Webassembly的特性,可以開發出類似桌面的應用程序。或許這是跨平台桌面應用開發除了electron的又一種方案吧。

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
    【其他文章推薦】

    USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

    台北網頁設計公司這麼多該如何選擇?

    ※智慧手機時代的來臨,RWD網頁設計為架站首選

    ※評比南投搬家公司費用收費行情懶人包大公開

    ※幫你省時又省力,新北清潔一流服務好口碑

    ※回頭車貨運收費標準

  • 使用Kubernetes、K3s和Traefik2進行本地開發

    使用Kubernetes、K3s和Traefik2進行本地開發

    作者簡介

    Vyacheslav,擁有運維和項目管理經驗的軟件工程師

    這篇文章將承接我此前搭建的本地Docker開發環境,具體步驟已經放在在以下網址:

    https://github.com/Voronenko/traefik2-compose-template

    除了經典的docker化的項目之外,我還有其他的Kubernetes項目。儘管Kubernetes已經成為容器編排的事實標準,但是不得不承認Kubernetes是一個既消耗資源又消耗金錢的平台。由於我並不經常需要外部集群,因此我使用輕量級K3s發行版來進行Kubernetes本地開發。

    K3s是為IoT和邊緣計算而構建的經過認證的Kubernetes發行版之一,還能夠按產品規模部署到VM。

    我使用K3s的方式是這樣的:在我的工作筆記本上本地安裝K3s,儘管有時我需要在本地部署較重的測試工作負載,為此,我準備了兩個神器——兩個運行ESXi的外部Intel NUCs。

    默認情況下,K3s安裝Traefik 1.x作為ingress,如果你對此十分滿意,那麼無需往下繼續閱讀了。

    在我的場景中,我同時會牽涉到好幾個項目,特別是經典的docker和docker swarm,因此我經常遇到在獨立模式下部署Traefik的情況。

    因此,本文其餘部分將深入介紹如何將外部traefik2配置為K3s集群的ingress。

    安裝Kubernetes K3s系列集群

    你可以按照常規方式使用命令curl -sfL https://get.k3s.io | sh -安裝K3s,或者你可以使用輕量實用程序k3sup安裝(https://github.com/alexellis/k3sup)。具體步驟在之前的文章介紹過。

    與我們的設置不同的是,我們使用命令--no-deploy traefik專門安裝了不帶traefik組件的K3s。

    export CLUSTER_MASTER=192.168.3.100
    export CLUSTER_DEPLOY_USER=slavko
    k3sup install --ip $CLUSTER_MASTER --user $CLUSTER_DEPLOY_USER --k3s-extra-args '--no-deploy traefik'
    

    執行后,你將獲得使用kubectl所需的連接詳細信息。安裝K3s后,你可以快速檢查是否可以看到節點。

    # Test your cluster with - export path to k3s cluster kubeconfig:
    export KUBECONFIG=/home/slavko/kubeconfig
    kubectl get node -o wide
    

    注:這裏沒有固定的安裝模式,你甚至可以使用docker-compose自行啟動它。

    server:
      image: rancher/k3s:v0.8.0
      command: server --disable-agent --no-deploy traefik
      environment:
        - K3S_CLUSTER_SECRET=somethingtotallyrandom
        - K3S_KUBECONFIG_OUTPUT=/output/kubeconfig.yaml
        - K3S_KUBECONFIG_MODE=666
      volumes:
        # k3s will generate a kubeconfig.yaml in this directory. This volume is mounted
        # on your host, so you can then 'export KUBECONFIG=/somewhere/on/your/host/out/kubeconfig.yaml',
        # in order for your kubectl commands to work.
        - /somewhere/on/your/host/out:/output
        # This directory is where you put all the (yaml) configuration files of
        # the Kubernetes resources.
        - /somewhere/on/your/host/in:/var/lib/rancher/k3s/server/manifests
      ports:
        - 6443:6443
    
    node:
      image: rancher/k3s:v0.8.0
      privileged: true
      links:
        - server
      environment:
        - K3S_URL=https://server:6443
        - K3S_CLUSTER_SECRET=somethingtotallyrandom
      volumes:
        # this is where you would place a alternative traefik image (saved as a .tar file with
        # 'docker save'), if you want to use it, instead of the traefik:v2.0 image.
        - /sowewhere/on/your/host/custom-image:/var/lib/rancher/k3s/agent/images
    

    配置Traefik 2,與Kubernetes一起使用

    在文章開頭提到的鏈接中,我已經在我的系統中安裝了Traefik 2,並根據該鏈接內容,服務於一些需求。現在是時候配置Traefik 2 Kubernetes後端了。

    Traefik 2使用CRD(自定義資源定義)來完成這一點。定義的最新示例可以在以下鏈接中找到,但這些示例僅適用於Traefik 2也作為Kubernetes工作負載的一部分執行的情況:

    https://docs.traefik.io/reference/dynamic-configuration/kubernetes-crd/

    對於外部Traefik 2,我們僅需要以下描述的定義子集。

    我們引入一系列自定義資源定義,以允許我們來描述我們的Kubernetes服務將會如何暴露到外部,traefik-crd.yaml

    apiVersion: apiextensions.k8s.io/v1beta1
    kind: CustomResourceDefinition
    metadata:
      name: ingressroutes.traefik.containo.us
    
    spec:
      group: traefik.containo.us
      version: v1alpha1
      names:
        kind: IngressRoute
        plural: ingressroutes
        singular: ingressroute
      scope: Namespaced
    
    ---
    apiVersion: apiextensions.k8s.io/v1beta1
    kind: CustomResourceDefinition
    metadata:
      name: ingressroutetcps.traefik.containo.us
    
    spec:
      group: traefik.containo.us
      version: v1alpha1
      names:
        kind: IngressRouteTCP
        plural: ingressroutetcps
        singular: ingressroutetcp
      scope: Namespaced
    
    ---
    apiVersion: apiextensions.k8s.io/v1beta1
    kind: CustomResourceDefinition
    metadata:
      name: middlewares.traefik.containo.us
    
    spec:
      group: traefik.containo.us
      version: v1alpha1
      names:
        kind: Middleware
        plural: middlewares
        singular: middleware
      scope: Namespaced
    
    ---
    apiVersion: apiextensions.k8s.io/v1beta1
    kind: CustomResourceDefinition
    metadata:
      name: tlsoptions.traefik.containo.us
    
    spec:
      group: traefik.containo.us
      version: v1alpha1
      names:
        kind: TLSOption
        plural: tlsoptions
        singular: tlsoption
      scope: Namespaced
    
    ---
    apiVersion: apiextensions.k8s.io/v1beta1
    kind: CustomResourceDefinition
    metadata:
      name: traefikservices.traefik.containo.us
    
    spec:
      group: traefik.containo.us
      version: v1alpha1
      names:
        kind: TraefikService
        plural: traefikservices
        singular: traefikservice
      scope: Namespaced  
    

    同時,我們需要集群角色traefik-ingress-controller,以提供對服務、端點和secret的只讀訪問權限以及自定義的traefik.containo.us組,traefik-clusterrole.yaml

    kind: ClusterRole
    apiVersion: rbac.authorization.k8s.io/v1beta1
    metadata:
      name: traefik-ingress-controller
    
    rules:
      - apiGroups:
          - ""
        resources:
          - services
          - endpoints
          - secrets
        verbs:
          - get
          - list
          - watch
      - apiGroups:
          - extensions
        resources:
          - ingresses
        verbs:
          - get
          - list
          - watch
      - apiGroups:
          - extensions
        resources:
          - ingresses/status
        verbs:
          - update
      - apiGroups:
          - traefik.containo.us
        resources:
          - middlewares
        verbs:
          - get
          - list
          - watch
      - apiGroups:
          - traefik.containo.us
        resources:
          - ingressroutes
        verbs:
          - get
          - list
          - watch
      - apiGroups:
          - traefik.containo.us
        resources:
          - ingressroutetcps
        verbs:
          - get
          - list
          - watch
      - apiGroups:
          - traefik.containo.us
        resources:
          - tlsoptions
        verbs:
          - get
          - list
          - watch
      - apiGroups:
          - traefik.containo.us
        resources:
          - traefikservices
        verbs:
          - get
          - list
          - watch
    

    最後,我們需要系統服務賬號traefik-ingress-controller與之前創建的集群角色traefik-ingress-controller相關聯。

    ---
    kind: ServiceAccount
    apiVersion: v1
    metadata:
      namespace: kube-system
      name: traefik-ingress-controller
    
    ---
    kind: ClusterRoleBinding
    apiVersion: rbac.authorization.k8s.io/v1beta1
    metadata:
      name: traefik-ingress-controller
    
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: ClusterRole
      name: traefik-ingress-controller
    subjects:
      - kind: ServiceAccount
        name: traefik-ingress-controller
        namespace: kube-system
    

    我們應用以上資源之後:

    apply:
      kubectl apply -f traefik-crd.yaml
      kubectl apply -f traefik-clusterrole.yaml
      kubectl apply -f traefik-service-account.yaml
    

    我們已經準備好開始調整Traefik 2

    將Traefik 2指向K3s集群

    根據Traefik文檔的建議,當Traefik部署到Kubernetes中時,它將讀取環境變量KUBERNETES_SERVICE_HOST和KUBERNETES_SERVICE_PORT或KUBECONFIG來構造端點。

    /var/run/secrets/kubernetes.io/serviceaccount/token中查找訪問token,而SSL CA證書將在/var/run/secrets/kubernetes.io/serviceaccount/ca.crt.中查找。當部署到Kubernetes內部時,兩者都會自動提供掛載。

    當無法找到環境變量時,Traefik會嘗試使用external-cluster客戶端連接到Kubernetes API server。這一情況下,需要設置endpoint。具體來說,可以將其設置為kubectl代理使用的URL,以使用相關的kubeconfig授予的身份驗證和授權連接到Kubernetes集群。

    Traefik 2可以使用任何受支持的配置類型來靜態配置-toml、yaml或命令行交換。

    [providers.kubernetesCRD]
      endpoint = "http://localhost:8080"
      token = "mytoken"
    
    providers:
      kubernetesCRD:
        endpoint = "http://localhost:8080"
        token = "mytoken"
        # ...
    
    --providers.kubernetescrd.endpoint=http://localhost:8080 
    --providers.kubernetescrd.token=mytoken
    

    第一次運行時,如果你在外部有Traefik,很有可能沒有traefik-ingress-controller訪問token來指定mytoken。那麼,你需要執行以下命令:

    # Check all possible clusters, as your .KUBECONFIG may have multiple contexts:
    kubectl config view -o jsonpath='{"Cluster name\tServer\n"}{range .clusters[*]}{.name}{"\t"}{.cluster.server}{"\n"}{end}'
    
    # Output kind of
    # Alias tip: k config view -o jsonpath='{"Cluster name\tServer\n"}{range .clusters[*]}{.name}{"\t"}{.cluster.server}{"\n"}{end}'
    # Cluster name  Server
    # default  https://127.0.0.1:6443
    
    # You are interested in: "default", if you did not name it differently
    
    # Select name of cluster you want to interact with from above output:
    export CLUSTER_NAME="default"
    
    # Point to the API server referring the cluster name
    export APISERVER=$(kubectl config view -o jsonpath="{.clusters[?(@.name==\"$CLUSTER_NAME\")].cluster.server}")
    # usually https://127.0.0.1:6443
    
    # Gets the token value
    export TOKEN=$(kubectl get secrets -o jsonpath="{.items[?(@.metadata.annotations['kubernetes\.io/service-account\.name']=='traefik-ingress-controller')].data.token}" --namespace kube-system|base64 --decode)
    
    # Explore the API with TOKEN
    

    如果成功了,你應該收到以下響應:

    {
      "kind": "APIVersions",
      "versions": [
        "v1"
      ],
      "serverAddressByClientCIDRs": [
        {
          "clientCIDR": "0.0.0.0/0",
          "serverAddress": "192.168.3.100:6443"
        }
      ]
    

    以及一些事實,如token:

    eyJhbGciOiJSUzI1NiIsImtpZCI6IjBUeTQyNm5nakVWbW5PaTRRbDhucGlPeWhlTHhxTXZjUDJsRmNacURjVnMifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJ0cmFlZmlrLWluZ3Jlc3MtY29udHJvbGxlci10b2tlbi12emM3diIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUiOiJ0cmFlZmlrLWluZ3Jlc3MtY29udHJvbGxlciIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6ImQ5NTc3ZTkxLTdlNjQtNGMwNi1iZDgyLWNkZTk0OWM4MTI1MSIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDprdWJlLXN5c3RlbTp0cmFlZmlrLWluZ3Jlc3MtY29udHJvbGxlciJ9.Mk8EBS4soO8uX-uSnV3o4qZKR6Iw6bgeSmPhHbJ2fjuqFgLnLh4ggxa-N9AqmCsEWiYjSi5oKAu986UEC-_kGQh3xaCYsUwlkM8147fsnwCbomSeGIct14JztVL9F8JwoDH6T0BOEjn-J9uY8-fUKYL_Y7uTrilhFapuILPsj_bFfgIeOOapRD0XshKBQV9Qzg8URxyQyfzl68ilm1Q13h3jLj8CFE2RlgEUFk8TqYH4T4fhfpvV-gNdmKJGODsJDI1hOuWUtBaH_ce9w6woC9K88O3FLKVi7fbvlDFrFoJ2iVZbrRALPjoFN92VA7a6R3pXUbKebTI3aUJiXyfXRQ
    

    根據上次響應的API server的外部地址:https://192.168.3.100:6443

    同樣,提供的token中沒有任何特殊之處:這是JWT的token,你可以使用https://jwt.io/#debugger-io,檢查它的內容。

    {
      "alg": "RS256",
      "kid": "0Ty426ngjEVmnOi4Ql8npiOyheLxqMvcP2lFcZqDcVs"
    }
    {
      "iss": "kubernetes/serviceaccount",
      "kubernetes.io/serviceaccount/namespace": "kube-system",
      "kubernetes.io/serviceaccount/secret.name": "traefik-ingress-controller-token-vzc7v",
      "kubernetes.io/serviceaccount/service-account.name": "traefik-ingress-controller",
      "kubernetes.io/serviceaccount/service-account.uid": "d9577e91-7e64-4c06-bd82-cde949c81251",
      "sub": "system:serviceaccount:kube-system:traefik-ingress-controller"
    }
    

    正確的配置非常重要,因此請確保對APISERVER的兩個調用均返回合理的響應。

    export APISERVER=YOURAPISERVER
    export TOKEN=YOURTOKEN
    
    curl -X GET $APISERVER/api --header "Authorization: Bearer $TOKEN" --insecure
    
    curl -X GET $APISERVER/api/v1/endpoints --header "Authorization: Bearer $TOKEN" --insecure
    

    創建其他訪問token

    控制器循環確保每個服務賬戶都有一個帶有API token的secret,可以像我們之前那樣被發現。

    此外,你還可以為一個服務賬戶創建額外的token,創建一個ServiceAccountToken類型的secret,併為服務賬戶添加一個註釋,控制器會用生成的token來更新它。

    ---
    apiVersion: v1
    kind: Secret
    namespace: kube-system
    metadata:
      name: traefik-manual-token
      annotations:
        kubernetes.io/service-account.name: traefik-ingress-controller
    type: kubernetes.io/service-account-token
    
    # Any tokens for non-existent service accounts will be cleaned up by the token controller.
    
    # kubectl describe secrets/traefik-manual-token
    

    用以下命令創建:

    kubectl create -f ./traefik-service-account-secret.yaml
    kubectl describe secret traefik-manual-token
    

    刪除/無效:

    kubectl delete secret traefik-manual-token
    

    對外部traefik 2的更改構成定義

    我們需要在文章開頭給出的鏈接中獲得的traefik2配置進行哪些更改?

    https://github.com/Voronenko/traefik2-compose-template

    a) 我們在新文件夾kubernetes_data中存儲ca.crt文件,該文件用於驗證對Kubernetes授權的調用。這是可以在kubeconfig文件的clusters-> cluster-> certificate-authority-data下找到的證書。

    該volume將映射在/var/run/secrets/kubernetes.io/serviceaccount下以獲取官方Traefik 2鏡像

    volumes:
        ...
          - ./kubernetes_data:/var/run/secrets/kubernetes.io/serviceaccount
    

    b) 調整Traefik 2 kubernetescrd後端以提供3個參數:endpoint、證書路徑和token。請注意,作為外部Traefik作為docker容器,你需要指定正確的endpoint地址,並確保以安全的方式進行。

      - "--providers.kubernetescrd=true"
          - "--providers.kubernetescrd.endpoint=https://192.168.3.100:6443"
          - "--providers.kubernetescrd.certauthfilepath=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
          - "--providers.kubernetescrd.token=YOURTOKENWITHOUTANYQUOTES
    

    如果你都執行正確了,那麼你現在應該在Traefik UI上看到了一些希望。如果你沒有看到traefik,或者在運行Traefik時有問題,你可以查看之後的故障排除部分。

    現在是時候通過Trafik 2暴露一些Kubernetes服務了,以確保Traefik 2能夠作為ingress工作。讓我們來看經典案例whoami服務,whoami-service.yaml

    apiVersion: v1
    kind: Service
    metadata:
      name: whoami
    
    spec:
      ports:
        - protocol: TCP
          name: web
          port: 80
      selector:
        app: whoami
    
    ---
    kind: Deployment
    apiVersion: apps/v1
    metadata:
      namespace: default
      name: whoami
      labels:
        app: whoami
    
    spec:
      replicas: 2
      selector:
        matchLabels:
          app: whoami
      template:
        metadata:
          labels:
            app: whoami
        spec:
          containers:
            - name: whoami
              image: containous/whoami
              ports:
                - name: web
                  containerPort: 80
    
    

    並且以http或https的方式暴露它,whoami.k.voronenko.net全限定域名下的whoami-ingress-route.yaml

    apiVersion: traefik.containo.us/v1alpha1
    kind: IngressRoute
    metadata:
      name: ingressroute-notls
      namespace: default
    spec:
      entryPoints:
        - web
      routes:
        - match: Host(`whoami.k.voronenko.net`)
          kind: Rule
          services:
            - name: whoami
              port: 80
    
    ---
    apiVersion: traefik.containo.us/v1alpha1
    kind: IngressRoute
    metadata:
      name: ingressroute-tls
      namespace: default
    spec:
      entryPoints:
        - websecure
      routes:
        - match: Host(`whoami.k.voronenko.net`)
          kind: Rule
          services:
            - name: whoami
              port: 80
      tls:
        certResolver: default
    

    然後應用它:

    kubectl apply -f whoami-service.yaml
      kubectl apply -f whoami-ingress-route.yaml
    

    應用后,你應該會在Traefik dashboard上看到一些希望,即KubernetesCRD後端。

    正如你所看到的,Traefik已經檢測到我們的K3s Kubernetes集群上運行的新工作負載,而且它與我們在同一個盒子上的經典Docker工作負載(如portainer)很好地共存。

    讓我們檢查一下Traefik 2是否將Traefik路由到了我們的Kubernetes工作負載:如你所見,你可以在http和https endpoint上成功地接觸到whoami工作負載,瀏覽器接受你的證書為可信任的“綠標籤”。

    我們的目標達到了!我們在本地筆記本上配置了Traefik 2。Traefik 2將你的docker或Kubernetes工作流暴露在http或https endpoint上。帶可選的 letsencrypt 的 Traefik 2 將負責 https。

    故障排查

    正如你所知,在配置過程可能存在多個問題,你可以考慮使用一些分析工具,如:

    https://github.com/Voronenko/dotfiles/blob/master/Makefile#L185

    我特別建議:

    a) VMWare octant:這是一個基於Web的功能強大的Kubernetes dashboard,你可以在上面使用你的kubeconfig

    b) Rakess:這是一個獨立工具也是一個kubectl插件,用於显示Kubernetes服務器資源的訪問矩陣(https://github.com/corneliusweig/rakkess)

    檢查系統賬戶的憑據

    rakkess --sa kube-system:traefik-ingress-controller
    

    c) kubectl

    檢查哪些角色與服務賬戶相關聯

    kubectl get clusterrolebindings -o json | jq -r '
      .items[] |
      select(
        .subjects // [] | .[] |
        [.kind,.namespace,.name] == ["ServiceAccount","kube-system","traefik-ingress-controller"]
      ) |
      .metadata.name'
    

    d) Traefik 文檔:例如kubernetescrd後端提供了更多配置開關的方式。

    --providers.kubernetescrd  (Default: "false")
            Enable Kubernetes backend with default settings.
        --providers.kubernetescrd.certauthfilepath  (Default: "")
            Kubernetes certificate authority file path (not needed for in-cluster client).
        --providers.kubernetescrd.disablepasshostheaders  (Default: "false")
            Kubernetes disable PassHost Headers.
        --providers.kubernetescrd.endpoint  (Default: "")
            Kubernetes server endpoint (required for external cluster client).
        --providers.kubernetescrd.ingressclass  (Default: "")
            Value of kubernetes.io/ingress.class annotation to watch for.
        --providers.kubernetescrd.labelselector  (Default: "")
            Kubernetes label selector to use.
        --providers.kubernetescrd.namespaces  (Default: "")
            Kubernetes namespaces.
        --providers.kubernetescrd.throttleduration  (Default: "0")
            Ingress refresh throttle duration
        --providers.kubernetescrd.token  (Default: "")
            Kubernetes bearer token (not needed for in-cluster client).
        --providers.kubernetesingress  (Default: "false")
            Enable Kubernetes backend with default settings.
        --providers.kubernetesingress.certauthfilepath  (Default: "")
            Kubernetes certificate authority file path (not needed for in-cluster client).
        --providers.kubernetesingress.disablepasshostheaders  (Default: "false")
            Kubernetes disable PassHost Headers.
        --providers.kubernetesingress.endpoint  (Default: "")
            Kubernetes server endpoint (required for external cluster client).
        --providers.kubernetesingress.ingressclass  (Default: "")
            Value of kubernetes.io/ingress.class annotation to watch for.
        --providers.kubernetesingress.ingressendpoint.hostname  (Default: "")
            Hostname used for Kubernetes Ingress endpoints.
        --providers.kubernetesingress.ingressendpoint.ip  (Default: "")
            IP used for Kubernetes Ingress endpoints.
        --providers.kubernetesingress.ingressendpoint.publishedservice  (Default: "")
            Published Kubernetes Service to copy status from.
        --providers.kubernetesingress.labelselector  (Default: "")
            Kubernetes Ingress label selector to use.
        --providers.kubernetesingress.namespaces  (Default: "")
            Kubernetes namespaces.
        --providers.kubernetesingress.throttleduration  (Default: "0")
            Ingress refresh throttle duration
        --providers.kubernetesingress.token  (Default: "")
            Kubernetes bearer token (not needed for in-cluster client).
    

    e) 確保Traefik有足夠的權限可以訪問apiserver endpoint

    如果你希望Traefik為你查詢信息:通過在配置中放置一些錯誤的apiserver地址,可以查看訪問的endpoint和查詢順序。有了這些知識和你的Traefik Kubernetes token,你就可以使用Traefik憑證檢查這些endpoint是否可以訪問。

    traefik_1    | E0421 12:30:12.624877       1 reflector.go:125] pkg/mod/k8s.io/client-go@v0.0.0-20190718183610-8e956561bbf5/tools/cache/reflector.go:98: Failed to list *v1.Endpoints: Get https://192.168.3.101:6443/api/v1/endpoints?limit=500&resourceVersion=0:
    traefik_1    | E0421 12:30:12.625341       1 reflector.go:125] pkg/mod/k8s.io/client-go@v0.0.0-20190718183610-8e956561bbf5/tools/cache/reflector.go:98: Failed to list *v1.Service: Get https://192.168.3.101:6443/api/v1/services?limit=500&resourceVersion=0:
    traefik_1    | E0421 12:30:12.625395       1 reflector.go:125] pkg/mod/k8s.io/client-go@v0.0.0-20190718183610-8e956561bbf5/tools/cache/reflector.go:98: Failed to list *v1beta1.Ingress: Get https://192.168.3.101:6443/apis/extensions/v1beta1/ingresses?limit=500&resourceVersion=0:
    traefik_1    | E0421 12:30:12.625449       1 reflector.go:125] pkg/mod/k8s.io/client-go@v0.0.0-20190718183610-8e956561bbf5/tools/cache/reflector.go:98: Failed to list *v1alpha1.Middleware: Get https://192.168.3.101:6443/apis/traefik.containo.us/v1alpha1/middlewares?limit=500&resourceVersion=0:
    traefik_1    | E0421 12:30:12.625492       1 reflector.go:125] pkg/mod/k8s.io/client-go@v0.0.0-20190718183610-8e956561bbf5/tools/cache/reflector.go:98: Failed to list *v1alpha1.IngressRoute: Get https://192.168.3.101:6443/apis/traefik.containo.us/v1alpha1/ingressroutes?limit=500&resourceVersion=0:
    traefik_1    | E0421 12:30:12.625531       1 reflector.go:125] pkg/mod/k8s.io/client-go@v0.0.0-20190718183610-8e956561bbf5/tools/cache/reflector.go:98: Failed to list *v1alpha1.TraefikService: Get https://192.168.3.101:6443/apis/traefik.containo.us/v1alpha1/traefikservices?limit=500&resourceVersion=0:
    traefik_1    | E0421 12:30:12.625572       1 reflector.go:125] pkg/mod/k8s.io/client-go@v0.0.0-20190718183610-8e956561bbf5/tools/cache/reflector.go:98: Failed to list *v1alpha1.TLSOption: Get https://192.168.3.101:6443/apis/traefik.containo.us/v1alpha1/tlsoptions?limit=500&resourceVersion=0:
    traefik_1    | E0421 12:30:12.625610       1 reflector.go:125] pkg/mod/k8s.io/client-go@v0.0.0-20190718183610-8e956561bbf5/tools/cache/reflector.go:98: Failed to list *v1alpha1.IngressRouteTCP: Get https://192.168.3.101:6443/apis/traefik.containo.us/v1alpha1/ingressroutetcps?limit=500&resourceVersion=0:
    

    f) 記錄K3s本身

    安裝腳本將自動檢測你的操作系統是使用systemd還是openrc並啟動服務。使用openrc運行時,將在/var/log/k3s.log中創建日誌。使用systemd運行時,將在/var/log/syslog中創建日誌,並使用journalctl -u k3s查看。

    在那裡,你可能會得到一些提示,例如:

    кві 21 15:42:44 u18d k3s[612]: E0421 15:42:44.936960     612 authentication.go:104] Unable to authenticate the request due to an error: invalid bearer token
    

    這將為你提供有關K8s Traefik起初使用時出現問題的線索,Enjoy your journey!

    相關代碼你可以在以下鏈接中找到:

    https://github.com/Voronenko/k3s-mini

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

    【其他文章推薦】

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

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

    南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

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

    ※超省錢租車方案

  • 厄瓜多總統與原住民領袖達協議 化解反撙節示威

    摘錄自2019年10月14日中央社報導

    莫雷諾為向國際貨幣基金貸款42億美元而取消燃油補貼後,國內物價迅速飆升,引發長達12天的示威,造成7人喪命。暴力衝突迫使莫雷諾將政府遷移至厄瓜多第2大城瓜亞基爾(Guayaquil),並嚴重影響石油業,能源部暫停超過2/3原油的配送。抗議人士甚至佔領亞馬遜雨林地區的3處石油設施。

    厄瓜多總統莫雷諾及原住民領袖瓦爾加斯13日達成協議,終結近2週來反對撙節措施的暴力抗議。政府是為為獲得國際貨幣基金(IMF)數十億美元貸款,而採取這些緊縮措施。

    法新社報導,聯合國官員代表宣讀的聯合聲明說,「根據這項協議,厄瓜多各地的群眾動員劃下句點,我們承諾會恢復國內和平」。聲明並表示,政府已撤回取消燃油補貼的命令。臉上塗著油彩、頭上頂著羽毛頭飾的瓦爾加斯證實:「這些適用於我們全國各地的措施已經取消了。」

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

    【其他文章推薦】

    ※帶您來了解什麼是 USB CONNECTOR  ?

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

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

    ※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

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