多線程與高併發(一)多線程入門

一、基礎概念

多線程的學習從一些概念開始,進程和線程,併發與并行,同步與異步,高併發。

1.1 進程與線程

幾乎所有的操作系統都支持同時運行期多個任務,所有運行中的任務通常就是一個進程,進程是處於運行過程中的程序,進程是操作系統進行資源分配和調度的一個獨立單位。

進程有三個如下特徵:

  • 獨立性:進程是系統中獨立存在的實體,它可以擁有自己獨立的資源,每一個進程都擁有自己私有的地址空間。在沒有經過進程本身允許的情況下,一個用戶進程不可以直接訪問其他進程的地址空間。

  • 動態性:進程與程序的區別在於,程序只是一個靜態的指令集合,而進程是一個正在系統中活動的指令集合。在進程中加入了時間的概念,進程具有自己的生命周期和各種不同的狀態,這些概念在程序中部是不具備的。

  • 併發性:多個進程可以在單個處理器上併發執行,多個進程之間不會互相影響。

線程是進程的組成部分,一個進程可以擁有多個線程,而線程必須有一個父進程,線程可以有自己的堆棧、自己的程序計數器和自己的局部變量,但不擁有系統資源。比如使用QQ時,我們可以同事傳文件,發送圖片,聊天,這就是多個線程在進行。

線程可以完成一定的任務,線程能夠獨立運行的,它不知道有其他線程的存在,線程的執行是搶佔式的,當前線程隨時可能被掛起。

總之:一個程序運行后至少有一個進程,一個進程里可以有多個線程,但至少要有一個線程。

1.2 併發和并行

併發和并行是比較容易混淆的概念,他們都表示兩個或者多個任務一起執行,但併發側重多個任務交替執行,同一時刻只能有一條指令執行,但多個進程指令被快速輪換執行,使得在宏觀上具有多個進程同時執行的效果。而并行確實真正的同時執行,有多條指令在多個處理器上同時執行,并行的前提條件就是多核CPU。

1.3 同步和異步

同步和異步通常用來形容一次方法調用。同步方法調用一旦開始,調用者必須等到方法調用返回后,才能繼續後續的行為。異步方法調用更像一個消息傳遞,一旦開始,方法調用就會立即返回,調用者可以繼續後續的操作。

1.4 高併發

高併發一般是指在短時間內遇到大量操作請求,非常具有代表性的場景是秒殺活動與搶票,高併發是互聯網分佈式系統架構設計中必須考慮的因素之一,高併發相關常用的一些指標有響應時間(Response Time),吞吐量(Throughput),每秒查詢率QPS(Query Per Second),併發用戶數等。

多線程在這裏只是在同/異步角度上解決高併發問題的其中的一個方法手段,是在同一時刻利用計算機閑置資源的一種方式

1.5 多線程的好處

線程在程序中是獨立的、併發的執行流,擁有獨立的內存單元,多個線程共享父進程里的全部資源,線程共享的環境有進程的代碼段,進程的公有數據等,利用這些共享數據,線程很容易實現相互之間的通信,可以提高程序的運行效率。

多線程的好處主要有:

  • 進程之間不能共享內存,但線程之間共享內存非常容易。

  • 系統創建進程時需要給進程重新分配系統資源,但創建線程代價小得多,所以使用多線程實現多任務併發比多進程效率高

  • Java語言內置了多線程功能支持。

二、使用多線程

上面講了多線程的一些概念,都有些抽象,下面將學習如何使用多線程,創建多線程的方式有三種。

2.1 繼承Thread類創建

繼承Thread創建並啟動多線程有三個步驟:

  1. 定義類並繼承Thread,重寫run()方法,run()方法中為需要多線程執行的任務。

  2. 創建該類的實例,即創建了線程對象。

  3. 調用實例的start()方法啟動線程。

public class FirstThread extends Thread {

    private int i=0;
    public void run() {
        for (; i < 100; i++) {
            //獲取當前線程名稱
            System.out.println(this.getName() + " " + i);
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            //Thread的靜態方法currentThread,獲取當前線程
            System.out.println(Thread.currentThread().getName());
            if (i == 20) {
                //創建線程並啟動
                new FirstThread().start();
                new FirstThread().start();
            }

        }
    }
}

運行結果可以看到兩個線程的i並不是連續的,說明他們並不共享數據。

2.2 實現Runnable接口

實現Runnable接口創建並啟動多線程也有以下步驟:

  1. 定義類並繼承Runnable接口,重寫run()方法,run()方法中為需要多線程執行的任務。

  2. 創建該類的實例,並以此實例作為target為參數來創建Thread對象,這個Thread對象才是真正的多線程對象。

public class SecondThread implements Runnable {
    private int i = 0;
    
    @Override
    public void run() {
        for (; i < 100; i++) {
            //此時想要獲取到多線程對象,只能使用Thread.currentThread()方法
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            //Thread的靜態方法currentThread,獲取當前線程
            System.out.println(Thread.currentThread().getName());
            if (i == 20) {
                //創建線程並啟動
                SecondThread secondThread=new SecondThread();
                new Thread(secondThread,"線程一").start();
                new Thread(secondThread,"線程二").start();
            }

        }
    }
}

2.3 使用Callable和Future

Callable是Runnable的增加版,主要是接口中的call()方法可以有返回值,並且可以申明拋出異常,使用Callable創建的步驟如下:

  1. 定義類並繼承Callable接口,重寫call()方法,run()方法中為需要多線程執行的任務。

  2. 創建類實例,使用FutureTask來包裝對象實例,

  3. 使用FutureTask對象作為Thread的target來創建多線程,並啟動線程。

  4. 調用FutureTask對象的get()方法來獲取子線程結束后的返回值。

public class ThirdThread {

    public static void main(String[] args) {
        //使用lambda表達式
        FutureTask<Integer> task = new FutureTask<>((Callable<Integer>) () -> {
            int i = 0;
            for (; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + "的循環變量i的值:" + i);
            }
            return i;
        });
        for (int i = 0; i < 100; i++) {
            //Thread的靜態方法currentThread,獲取當前線程
            System.out.println(Thread.currentThread().getName());
            if (i == 20) {
                //創建線程並啟動
                new Thread(task, "有返回值的線程").start();
            }
        }
        try {
            System.out.println("線程的返回值:" + task.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

這裏使用了lambda表達式,不使用表達式的方式也很簡單,可以去源碼中查看。Callable與Runnable方式基本相同,只不過增加了返回值且可允許聲明拋出異常。

使用三種方式都可以創建線程,且方式也相對簡單,大體分為實現接口和實現Thread類兩種,這兩種都各有優缺點。

繼承接口實現:

  • 優點:除了繼承接口之外,還可以繼承其他類。這種方式多個線程共享一個target對象,可以處理用於共同資源的情況。
  • 缺點:編程稍微複雜一些,並且沒有直接獲取當前線程對象的方式,必須使用Thread.currentThread()方式。

基礎Thread類:

  • 優點:編程簡單

  • 缺點:不能繼承其他類

三、多線程的生命周期

線程狀態是線程中非常重要的一個概念,然而我看過很多資料,線程的狀態理解有很多種方式,很多人將其分為五個基本狀態:新建、就緒、運行、阻塞、死亡,但在狀態枚舉中並不是這五個狀態,我不知道是什麼原因(有大神可以解答更好),只能按照枚舉中的狀態根據自己的理解。

  1. 初始(NEW):新創建了一個線程對象,但還沒有調用start()方法,而且就算調用了改方法也不代表狀態立即改變。

  2. 運行(RUNNABLE):在運行的狀態肯定就處於RUNNABLE狀態。

  3. 阻塞(BLOCKED):表示線程阻塞,或者說線程已經被掛起了。

  4. 等待(WAITING):進入該狀態的線程需要等待其他線程做出一些特定動作(通知或中斷)。

  5. 超時等待(TIMED_WAITING):該狀態不同於WAITING,它可以在指定的時間后自行返回。

  6. 終止(TERMINATED):表示該線程已經執行完畢。

狀態流程圖如下:

理解:初始狀態很好理解,這個時候其實還不能被稱為一個線程,因為他還沒被啟動,當調用start()方法后,線程正式啟動,但是也不代表立即就改變了狀態。

運行狀態中其實包含兩種狀態,運行中(RUNING)就緒(READY)

就緒狀態表示你有資格運行,只要CPU還未調度到你,就處於就緒狀態,有幾個狀態會是線程狀態編程就緒狀態

  • 調用線程的start()方法。

  • 當前線程sleep()方法結束,其他線程join()結束,等待用戶輸入完畢,某個線程拿到對象鎖。

  • 當前線程時間片用完了,調用當前線程的yield()方法。

  • 鎖池裡的線程拿到對象鎖后。

運行中(RUNING)狀態比較好理解,線程調度程序選擇了當前線程作。

阻塞狀態是線程阻塞在進入synchronized關鍵字修飾的方法或代碼塊(獲取鎖)時的狀態。

等待狀態是指線程沒有被CPU分配執行時間,需要等待,這種等待是需要被显示的喚醒,否則會無限等待下去。

超時等待狀態是這現在沒有被CPU分配執行時間,需要等待,不過這種等待不需要被显示的喚醒,會設置一定的時間后zi懂喚醒。

死亡狀態也很好理解,說明線程方法被執行完成,或者出錯了,線程一旦進入這個狀態就代表徹底的結束

 

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

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

Git的使用 — 用git玩翻github,結尾有驚喜!有驚喜!有驚喜!林妙妙看了說:牛呲呼啦帶閃電 (三)(超詳解)

簡介

      上一篇主要講解的是Git安裝及配置,這一篇就詳細的從無到有的來用Git玩翻github。

一、什麼是Github

Github是全球最大的社交編程及代碼託管網站(https://github.com/)。

Github可以託管各種git庫,並提供一個web界面(用戶名.github.io/倉庫名)

二、Github和Git是什麼關係 

Git是版本控制軟件

Github是項目代碼託管的平台,藉助git來管理項目代碼

1、 使用Github

目的:藉助github託管項目代碼

2、基本概念

a、倉庫(Repository)

 倉庫的意思,即你的項目,你想在 GitHub 上開源一個項目,那就必須要新建一個 Repository ,如果你開源的項目多了,你就擁有了多個 Repositories 。

 倉庫用來存放項目代碼,每個項目對應一個倉庫,多個開源項目則有多個倉庫。

b、收藏(Star)

倉庫主頁star按鈕,意思為收藏項目的人數,收藏項目,方便下次查看,在 GitHub 上如果你有一個項目獲得100個star都算很不容易了!

【如何收藏】

 操作:打開對應項目主頁,點擊右上角  star 按鈕即可收藏

 情景:張三無意訪問到李四的開源項目感覺不錯並進行收藏

 

【如何查看自己得收藏】

 

c、複製項目(Fork)派生

這個不好翻譯,如果實在要翻譯我把他翻譯成分叉,什麼意思呢?你開源了一個項目,別人想在你這個項目的基礎上做些改進,然後應用到自己的項目中,這個時候他就可以 Fork 你的項目(打開項目主頁點擊右上角的fork按鈕即可),然後他的 GitHub 主頁上就多了一個項目,只不過這個項目是基於你的項目基礎(本質上是在原有項目的基礎上新建了一個分支),他就可以隨心所欲的去改進,但是絲毫不會影響原有項目的代碼與結構。

注意:該fork的項目時獨立存在的

比如:張三fork了李四的項目,相當於張三複制了李四的項目,所以自己也單獨有了一個一樣名稱的倉庫(注:該倉庫會聲明來自於李四,但是獨立存在)

d、發起請求(Pull Request)

發起請求,這個其實是基於 Fork 的,還是上面那個例子,如果別人在你基礎上做了改進,後來覺得改進的很不錯,應該要把這些改進讓更多的人收益,於是就想把自己的改進合併到原有項目里,這個時候他就可以發起一個 Pull Request(簡稱PR) ,原有項目創建人,也就是你,就可以收到這個請求,這個時候你會仔細review他的代碼,並且測試覺得OK了,就會接受他的PR,這個時候他做的改進原有項目就會擁有了。

e、關注(Watch)

這個也好理解就是觀察,如果你 Watch 了某個項目,那麼以後只要這個項目有任何更新,你都會第一時間收到關於這個項目的通知提醒。

f、問題(Issue)

發現代碼BUG,但是目前沒有成型代碼,需要討論時用; 問題的意思,舉個例子,就是你開源了一個項目,別人發現你的項目中有bug,或者哪些地方做的不夠好,他就可以給你提個 Issue ,即問題,提的問題多了,也就是 Issues ,然後你看到了這些問題就可以去逐個修復,修復ok了就可以一個個的 Close 掉。

g、Github主頁

賬號創建成功或點擊網址導航欄github圖標都可進入github主頁:該頁左側主要显示用戶動態以及關注用戶或關注倉庫的動態;右側显示所有的git庫

h、倉庫主頁

倉庫主頁主要显示項目的信息,如:項目代碼,版本,收藏/關注/fork情況等

i、個人主頁

個人信息:頭像,個人簡介,關注我的人,我關注的人,我關注的git庫,我的開源項目,我貢獻的開源項目等信息

3、註冊github賬號

官方網址:https://github.com

注意:

a、因為github在國外服務器所以訪問較慢或者無法訪問,需要FQ(***)

b、私有倉庫只能自己或者指定的朋友才有權限操作(私有倉庫是收費的)

c、新註冊的用戶必須驗證郵箱后才可以創建git庫倉庫

4、創建倉庫/創建新項目

說明:一個git庫(倉庫)對應一個開源項目。通過git管理git庫

a、創建倉庫

1)點擊【Start a project】創建一個倉庫

2)問題:點擊【Start a project】創建一個倉庫,后出現該頁面

2)原因:未驗證郵箱,點擊下圖框框中的鏈接進行驗證

 

3)點擊【resend】發送郵件驗證郵箱

 

 4)點擊【verify email address】驗證郵箱

   說明:驗證成功後會自動跳轉github主頁,重新點擊【Start a project】即可創建倉庫

 

5) 驗證郵箱后,點擊【Start a project】進入下圖界面

b、倉庫主頁說明

 

 

 注意:qq郵箱需要設置白名單才可以收到郵件

設置QQ郵箱白名單

1、打開QQ郵箱、點擊【設置】

2、點擊【反垃圾】

3、點擊【設置域名白名單】

4、在新頁面的input框中輸入【github.com】添加即可

5、倉庫管理

a、新建文件:倉庫主頁,點擊【create new file】創建倉庫文件

 

 

 

   

b、編輯文件:倉庫主頁,點擊【需要修改的文件】進入文件詳情頁

 

    

c、刪除文件

 

d、被刪除文件如何查看信息

答案:點擊commits按鈕查看

e、上傳文件

 

f、搜索倉庫文件:快捷鍵(t)

6、下載/檢出項目

7、Github Issues

作用:發現代碼BUG,但是目前沒有成型代碼,需要討論時用;或者使用開源項目出現問題時使用

情景:張三發現李四開源git庫,則發提交了一個issue;李四隔天登錄在github主頁看到通知並和張三交流,最後關閉issue

三、基本概念(實戰操作)

1、Github主頁

 

2、個人主頁

 

四、開源項目貢獻流程

1、新建Issue

提交使用問題或者建議或者想法

2、Pull Request

步驟:

a、 fork項目

b、 修改自己倉庫的項目代碼

c、 新建 pull request

d、 等待作者操作審核

五、下面就是驚喜:Github  Pages搭建網站

1、個人站點

訪問:https://用戶名.github.io

搭建步驟:

a、創建個人站點-》新建倉庫(倉庫名必須是【用戶名.github.io】)

 

b、在倉庫下新建index.html的文件即可

注意:a、Github Page僅支持靜態頁面

   b、倉庫裏面只能是.html文件

   c、個人主頁也可以設置主題

2、Project Pages 項目站點

訪問:https://用戶名.github.io/倉庫名

原理:gh-pages 用於構建和發布

搭建步驟

a、進入項目主頁,點擊settings

b、在settings頁面,點擊【Choose a theme 】來自動生成主題頁面

c、新建站點基礎信息設置

d、選擇主題

e、發布網頁(publish page)

六、小結

Clone和Fork的區別:

fork(派生):將別人的倉庫複製一份到自己的倉庫。
clone(克隆):將倉庫克隆到自己本地電腦中。

Fork的主要應用場景:
1.在A的倉庫中fork項目B (此時我們自己的github就有一個一模一樣的倉庫B,但是URL不同)
2.將我們修改的代碼push到自己github中的倉庫B中
3.pull request ,主人就會收到請求,並決定要不要接受你的代碼

【精選推薦文章】

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

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師"嚨底家"!!

kubeadm1.14.1 安裝Metrics Server

Metrics API

介紹Metrics-Server之前,必須要提一下Metrics API的概念

Metrics API相比於之前的監控採集方式(hepaster)是一種新的思路,官方希望核心指標的監控應該是穩定的,版本可控的,且可以直接被用戶訪問(例如通過使用 kubectl top 命令),或由集群中的控制器使用(如HPA),和其他的Kubernetes APIs一樣。

官方廢棄heapster項目,就是為了將核心資源監控作為一等公民對待,即像pod、service那樣直接通過api-server或者client直接訪問,不再是安裝一個hepater來匯聚且由heapster單獨管理。

假設每個pod和node我們收集10個指標,從k8s的1.6開始,支持5000節點,每個節點30個pod,假設採集粒度為1分鐘一次,則:

10 x 5000 x 30 / 60 = 25000 平均每分鐘2萬多個採集指標

因為k8s的api-server將所有的數據持久化到了etcd中,顯然k8s本身不能處理這種頻率的採集,而且這種監控數據變化快且都是臨時數據,因此需要有一個組件單獨處理他們,k8s版本只存放部分在內存中,於是metric-server的概念誕生了。

其實hepaster已經有暴露了api,但是用戶和Kubernetes的其他組件必須通過master proxy的方式才能訪問到,且heapster的接口不像api-server一樣,有完整的鑒權以及client集成。這個api現在還在alpha階段(18年8月),希望能到GA階段。類api-server風格的寫法:generic apiserver

有了Metrics Server組件,也採集到了該有的數據,也暴露了api,但因為api要統一,如何將請求到api-server的/apis/metrics請求轉發給Metrics Server呢,解決方案就是:kube-aggregator,在k8s的1.7中已經完成,之前Metrics Server一直沒有面世,就是耽誤在了kube-aggregator這一步。

kube-aggregator(聚合api)主要提供:

  • Provide an API for registering API servers.

  • Summarize discovery information from all the servers.

  • Proxy client requests to individual servers.

詳細設計文檔:參考鏈接

metric api的使用:

  • Metrics API 只可以查詢當前的度量數據,並不保存歷史數據

  • Metrics API URI 為 /apis/metrics.k8s.io/,在 k8s.io/metrics 維護

  • 必須部署 metrics-server 才能使用該 API,metrics-server 通過調用 Kubelet Summary API 獲取數據

如:

http://127.0.0.1:8001/apis/metrics.k8s.io/v1beta1/nodes
​
http://127.0.0.1:8001/apis/metrics.k8s.io/v1beta1/nodes/<node-name>
​
http://127.0.0.1:8001/apis/metrics.k8s.io/v1beta1/namespace/<namespace-name>/pods/<pod-name>

Metrics-Server

Metrics server定時從Kubelet的Summary API(類似/ap1/v1/nodes/nodename/stats/summary)採集指標信息,這些聚合過的數據將存儲在內存中,且以metric-api的形式暴露出去。

Metrics server復用了api-server的庫來實現自己的功能,比如鑒權、版本等,為了實現將數據存放在內存中嗎,去掉了默認的etcd存儲,引入了內存存儲(即實現Storage interface)。因為存放在內存中,因此監控數據是沒有持久化的,可以通過第三方存儲來拓展,這個和heapster是一致的。

 

Metrics server出現后,新的​Kubernetes 監控架構將變成上圖的樣子

  • 核心流程(黑色部分):這是 Kubernetes正常工作所需要的核心度量,從 Kubelet、cAdvisor 等獲取度量數據,再由metrics-server提供給 Dashboard、HPA 控制器等使用。

  • 監控流程(藍色部分):基於核心度量構建的監控流程,比如 Prometheus 可以從 metrics-server 獲取核心度量,從其他數據源(如 Node Exporter 等)獲取非核心度量,再基於它們構建監控告警系統。

官方地址:https://github.com/kubernetes-incubator/metrics-server

部署

mkdir metrics;cd metics
git clone https://github.com/kubernetes-incubator/metrics-server.git
cd metrics-server/deploy/1.8+/

 修改metrics-server-deployment.yaml,紅色command部分。

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: metrics-server
  namespace: kube-system
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: metrics-server
  namespace: kube-system
  labels:
    k8s-app: metrics-server
spec:
  selector:
    matchLabels:
      k8s-app: metrics-server
  template:
    metadata:
      name: metrics-server
      labels:
        k8s-app: metrics-server
    spec:
      serviceAccountName: metrics-server
      volumes:
      # mount in tmp so we can safely use from-scratch images and/or read-only containers
      - name: tmp-dir
        emptyDir: {}
      containers:
      - name: metrics-server
        image: k8s.gcr.io/metrics-server-amd64:v0.3.3
        command:
        - /metrics-server
        - --metric-resolution=30s
        - --kubelet-preferred-address-types=InternalIP,Hostname,InternalDNS,ExternalDNS,ExternalIP
        - --kubelet-insecure-tls
        imagePullPolicy: Always
        volumeMounts:
        - name: tmp-dir
          mountPath: /tmp

 創建

[root@cn-hongkong 1.8+]# kubectl apply -f .
clusterrole.rbac.authorization.k8s.io/system:aggregated-metrics-reader unchanged
clusterrolebinding.rbac.authorization.k8s.io/metrics-server:system:auth-delegator unchanged
rolebinding.rbac.authorization.k8s.io/metrics-server-auth-reader unchanged
apiservice.apiregistration.k8s.io/v1beta1.metrics.k8s.io unchanged
serviceaccount/metrics-server unchanged
deployment.extensions/metrics-server configured

 等待一會就可以看下集群的資源使用情況了!

 

【精選推薦文章】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

微信小程序入口場景的問題整理與相關解決方案

前言

最近一段時間都在做小程序。

雖然是第二次開發小程序,但是上次做小程序已經是一年前的事了,所以最終還是被坑得死去活來。

這次是從零開始開發一個小程序,其實除了一些莫名其妙的兼容性問題,大多數坑點都是在微信小程序的各個入口場景處。

所以這裏整理一下微信小程序的各個入口場景,以及從這些入口場景進入小程序會面臨的問題以及解決方案。

這裏只列出常用的幾種場景:

  • [簡單場景]啟動小程序並進入
  • [簡單場景]退出重進(啟動小程序后,退出小程序,再次進入小程序)
  • [簡單場景]退出重進首頁(啟動小程序后,退出小程序,通過掃二維碼再次進入小程序)
  • [複雜場景]啟動並進入指定頁面(從小程序的分享卡片或者微信發送的通知消息進入小程序)
  • [複雜場景]退出重進指定頁面(啟動小程序后,退出小程序,從小程序的分享卡片或者微信發送的通知消息進入小程序)

啟動小程序並進入

微信小程序的入口場景光微信提供的場景值就有幾十種,但是絕大多數都可以劃分為啟動小程序並進入。

這是最常用的一種進入小程序的方式,比如通過搜索進入或者點擊最近使用小程序的方式進入,都算是這種類型。

這一場景下,首先我們需要明白髮生了什麼:

下載小程序 => 啟動小程序 onLaunch事件觸發 => 加載首頁 onLoad事件觸發 => 首頁 onShow事件

然後在這個場景下,需要注意以下幾個問題:

  1. 這個場景下一般會涉及到登錄。
    所謂登錄,不一定是要在這個階段做,但是登錄信息的判斷這個階段是一定要做的。
    通常前端肯定是要將登錄的這些信息存儲在小程序的storage里,然後在onLaunch事件中判斷是否登錄,沒登錄就跳轉到登錄頁面,登錄了就跳轉到首頁。
    這裏的登錄判斷一定要放在onLaunch,而不要放在首頁的onLoad裏面,因為小程序啟動一定會進入onLaunch,而不一定會進入首頁的onLoad。
  2. 而登錄頁面在設計的時候最好要加上一個url參數,傳入登錄成功后跳轉到的頁面地址,而不是登錄之後始終跳轉到首頁,後面會講為什麼需要這麼做。
  3. onLaunch階段是否有發出請求,並在請求完成後進行了頁面跳轉,或者請求完成設置storage,並在onLoad頁面中使用?
    這種情況的出現,會導致在請求時間過長時,首頁的onLoad已經執行了,此時就會出現BUG。
    對於這個問題,有的人會用定時器去判斷是否完成這個操作,但是我的建議是盡量避免在onLaunch中進行這些操作。
    如果一定要有,那麼最好的方式就是做一個加載頁面去承載這些功能。
  4. 首頁數據的初始化,一般是放在onLoad中執行。當然總是有些特殊的需求是要放在onShow裏面的。
    關於onLoad和onShow,最常見的處理區別就在跳轉頁面時。
    當載入首頁時,先觸發onLoad,再觸發onShow。
    此時通過wx.navigateTo 的方式跳轉到頁面A,這個時候首頁並沒有被關閉,那麼從頁面A再返回首頁時,onLoad就不會觸發,但onShow會觸發。
    通常在加載數據時,一般會用到onLoad。
    但是如果說頁面A更新了數據,然後返回首頁時,首頁的相關數據也需要更新。
    那麼初始化數據就不能放在onLoad里,而需要放在onShow里。
    (當然還有一種方式是通過getCurrentPages的方式在頁面A中調用首頁的方法。但是這裏極不推薦這種方式,屬於某個頁面的事情一定要給這個頁面。最好不要將頁面間的職責通過這種方式打亂,容易引起代碼混亂,不易維護。)

退出重進(啟動小程序后,退出小程序,再次進入小程序)

這種場景實際上是對第一種場景的擴展。

而所謂的退出小程序不管你是點右上角的退出按鈕還是Home鍵直接切出都算是這類退出。

但是退出后再立即進入小程序的時候,依然會進入你退出小程序時所在的頁面,而不會觸發onLaunch,也不會觸發這個頁面的onLoad,不過onShow是肯定會觸發的。

這一場景下,首先我們需要明白髮生了什麼:

再次進入小程序 => 進入退出小程序時所在頁面 觸發onShow

在這個場景下,只需要注意onShow中是否有不可重複執行的操作。

例如onShow中會獲取用戶喜歡吃的食物,加載到頁面的列表中,在這種場景下,如果不清空之前的列表或者加個判斷的話,就會出現重複數據。

退出重進首頁(啟動小程序后,退出小程序,通過掃二維碼再次進入小程序)

這種場景實際上是對第二種場景的擴展。

我們通常給二維碼配置的是一個無參數的小程序首頁地址,當我們退出小程序,通過掃二維碼再次進入小程序時會進入首頁。

這一場景下,首先我們需要明白髮生了什麼:

再次進入小程序 => 進入退出小程序時所在頁面A 不觸發onShow => 觸發頁面A onHide => 觸發頁面A onUnload=> 進入首頁 onLoad => 首頁onShow

在這個場景下,除了需要注意第二種場景存在的問題,還需要注意頁面A的onHide事件中是否會觸發奇怪的操作,例如頁面跳轉。

啟動並進入指定頁面(從小程序的分享卡片或者微信發送的通知消息進入小程序)

這塊場景常見於邀請他人進入小程序,需要注意的是他們往往被賦予了更多的業務功能,也就往往增大了小程序的實現難度。

這一場景下,首先我們需要明白髮生了什麼:

下載小程序 => 啟動小程序 onLaunch事件觸發 => 加載指定頁面 onLoad事件觸發 =>指定頁面  onShow事件

這裏就可以看出,並不是進入小程序就一定會進入首頁的onLoad。

所以這就是為什麼之前強調不要將登錄判斷放在首頁的onLoad中,而一定要放在onLaunch里。

但是這裏又和掃二維碼不同,掃二維碼的鏈接一般都是指定的首頁。

而這裏通常跳轉到的是非首頁的頁面,而且可能還多了複雜的業務功能。

我們在需求分析和設計階段應該更多地考慮到這裏可能會引發的複雜問題,而盡量將此處的業務邏輯簡化,或者加大估時。

接下來,我們將根據業務從簡單到複雜,慢慢講解這個場景下可能存在的問題。

最簡單的邀請函(進入小程序首頁)

和第一種場景差不多,這裏略過

進階邀請函(進入小程序指定頁面,帶參數,需要根據參數初始化頁面)

這種情況下,需要考慮以下幾個問題:

  1. 首先在onLaunch階段會判斷是否登錄,沒登錄那麼就需要跳轉到登錄頁面,登錄頁面登錄之後,肯定要跳轉到這個頁面,而不是首頁。
    所以之前說過登錄頁面設計的時候需要傳入一個url參數,來明確登錄成功后跳轉到哪個頁面。
  2. 這種跳轉到指定頁面的情況通常都需要一個回到首頁的按鈕。
    就比如邀請某人查看一篇文章,點擊邀請卡片後會進入小程序內的文章詳情。
    一般在小程序內通常是通過點擊文章列表跳轉到文章詳情,那麼這個時候可以逐級返回到首頁。
    但是在點擊邀請函進入的情況是沒有返回功能的,此時如果沒有回到首頁功能,那麼用戶可能就永遠沒法回到首頁。
    (其實是可以的,但是小程序的的這個功能藏得比較深,不要指望所有用戶都那麼熱愛摸索)
  3. 這裏一定要特別注意第一種場景的第三個應該注意的問題,對於第一種場景而言那個問題因為啟動次數很多容易出現,但是在當前的場景下可能很容易被忽略掉。

涉及身份的邀請函(進入小程序指定頁面,帶參數,需要根據參數切換身份,更可能涉及到登錄)

為了更好地說明這種情況,我們來列舉一個場景。

如果有一個打車軟件,進入這個軟件後有兩種身份,一種是乘客,一種是司機。

用戶是司機,那麼看到的是頁面A或者選定了TabA,如果是乘客,那麼看到的是頁面B或者選定了TabB。

而且還有一個需求,用戶上次登陸時什麼身份,這次登陸也是什麼身份。

考慮到換手機的場景,那麼這個信息肯定是存儲在服務端的,所以進入小程序的時候會去請求服務端進行判斷。

現在我用司機的身份發了個單,微信給了個通知消息,我沒點開。然後切換到乘客的身份了,再去點擊通知消息,那麼我會以司機的身份去打開這個消息。

這個場景其實在業務上來看是很合理的,但是對於我們的程序實現來看,複雜度一下子就上來了。

  1. 首先我們確定一下這個請求身份信息的請求在哪個階段發出?
    onLaunch?
    那麼是不是需要在onLoad階段去獲取這個身份的信息然後給出不同的頁面?
    這樣一下子就會出現進階邀請函的第三個問題,而且還不僅僅是這一個問題,之後我們會講到。
    所以這個地方需要做一個專門的邀請加載頁面去處理這個事情。
  2. 分離出一個單獨的加載頁面之後,其實我們的工作會變的簡單清晰起來。
    因為我們只需要去做我們這個頁面所需要做的事情就行了。
    根據參數去獲取我們現在的身份,然後以這種身份跳轉到相應的頁面。
  3. 這裏還涉及到一個問題,那就是正常啟動而不是通過通知消息進入的時候,也需要去請求服務端獲取身份信息。
    我給的建議是一定要另外單獨建一個頁面去承載這個功能,而不要將這兩個加載頁面糅合到一起。
    裏面的頁面展示我們可以用組件化的方式去做,但是頁面的邏輯一點更要分開。
    因為這兩種情況真的很容易混雜,也是為了利於後面的維護工作。
  4. 正常啟動時的加載頁面也可以看情況糅合到首頁的onLoad裏面。
    但是如果有可能,還是希望放在單獨的頁面里。
    首頁往往功能很多,代碼量比較大,不要將本來可以分離出去的功能放進去。
    還是那句話,頁面的職責分開。

我這裏講的其實還是一個比較常見的功能,通常我們的業務也不一定像上面這樣簡單。

所以如果涉及到這方面的操作,在需求分析和設計的時候就應該考慮清楚。

如果等到功能開發的時候再去考慮這些事情,那麼等待你的一定是延期或者加班。

退出重進指定頁面(啟動小程序后,退出小程序,從小程序的分享卡片或者微信發送的通知消息進入小程序)

這種場景同樣是第四種場景的進階,但是如果你在第四種場景中使用了我所說的加載頁面,那麼接下來的問題會簡單很多。

這一場景下,首先我們需要明白髮生了什麼:

再次進入小程序 => 進入退出小程序時所在頁面A 不觸發onShow => 觸發頁面A onHide => 觸發頁面A onUnload => 進入邀請加載頁面onLoad => 加載頁面onShow

對於第四種場景中的打車小程序而言,如果按照我們先前所說沒有在onLaunch中獲取身份信息,而是放在了加載頁中,那麼現在什麼都不用改。

如果獲取身份信息的請求放在onLaunch中,現在又得在onLoad中加一道邏輯。

當然這裏還是得注意一個問題,對於這一類型的進入小程序的方式,比如從分享卡片進入和微信的通知消息進入。

即使他們所進入的頁面不同,但是他們都可以使用這個載入頁面去做判斷。

與正常啟動場景的載入頁面是不同的,他們本來就是同一種入口場景。

所以該共用的地方還是得共用,用不同的業務code判斷即可。

總結

總的來說,以上的幾種情況應該能涵蓋絕大多數小程序的入口場景。

整理的目的其實主要是為了做需求分析和設計時參考使用,以避免在考慮業務問題時漏過這些場景導致後期的工作計劃受到影響。

所謂加班和項目延期發布,大都是前期需求分析和設計考慮不周。

我們不可能考慮到所有的場景,但是應該盡善盡美。

謀定而後動,前事不忘後事之師,也算是PDCA了。

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

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

在 Ubuntu 開啟 GO 程序編譯之旅

本文將使用 putty 連接到一台阿里雲 Ubuntu 16.04 服務器,在其上安裝 go 語言的編譯環境,旨在呈現從安裝到“你好,世界!”涉及的方方面面,希望完成這個過程無須覓它處。

1. 安裝

方式一使用 apt-get

apt-get install golang-go

執行完成之後,會把 golang 安裝在這個位置:/usr/lib/go-1.6/,go 命令會在該目錄的 bin 子目錄下,同時,/usr/bin 下會有該命令的文件鏈接。

當然,也許你並不知道到底安裝在哪,可以通過以下命令找找觀察判斷一下。

# 找名字為 go 的文件
find / -name go

執行 /usr/bin/go version,結果如下,显示的版本號為 go1.6.2,版本比較低。

是不是想卸載?使用以下命令可以完成卸載,跟安裝一一對應。

apt-get --purge remove golang-go

方式二使用 wget

直接下載想要的版本進行安裝,一切皆在掌控之中。通過以下兩條命令,我們把 golang 安裝在 /usr/local/go 下。

# 下載
wget https://storage.googleapis.com/golang/go1.9.1.linux-amd64.tar.gz
# 解壓
tar -xzf go1.9.1.linux-amd64.tar.gz -C /usr/local

2. 設置環境變量

這裡會涉及到3個環境變量,分別是 PATH、GOROOT、GOPATH。
PATH,是為了讓 go 命令隨處可敲。
GOROOT,代表 golang 的根目錄,在設置PATH時可以用一下,如 export PATH=$GOROOT/bin。
GOPATH,特別重要,單獨做一節(2.2)來講。

2.1 設置

環境變量可以設置在不同的文件中。
etc/profile : 對所有用戶生效
~/.profile : 對當前用戶生效

配置在哪都行,能用到即可。在配置文件末尾加上以下文本。

export GOROOT=/usr/local/go
export GOPATH=/usr/goprojs
export PATH=$GOROOT/bin:$PATH:$GOPATH/bin

GOPATH、PATH 多個路徑,中間使用冒號分隔。
配置完成后,使用source ~/.profile 讓其立即生效。

2.2 GOPATH

GOPATH 是GO程序找依賴包的路徑。
其子目錄 src 中可放置各個包的源碼,編譯時會通過 GOPATH 去引用它們。
子目錄 bin 則是編譯之後的可執行文件,在PATH 里要加上各$GOPATH/bin 可以讓編譯的運行文件在執行搜索路徑範圍內方便執行。
子目錄 pkg,編譯包的中間文件,不太關心它。

GOPATH 的第一個路徑特別重要
使用 go get 下載的包都會安裝在第一個路徑,所以如果想讓公共包統一在某處,應該要為它單獨建立一個路徑作為GOPATH的第一個路徑,從而使得 go get 總去向那裡。實際項目最好另建路徑加入GOPATH,這樣即在引用範圍 go get 又影響不到。

附 go get 可帶參數:
|參數|描述|
|——|——|
| -v |显示操作流程的日誌及信息 |
| -u |僅下載丟失的包,不更新已存在的 |
| -d | 只下載,不安裝 |
| -insecure | 允許使用HTTP,而不一定要HTTPS |

3. 你好,世界!

3.1 編寫代碼

建立代碼文件。點此可以在線嘗鮮 GO 代碼

vi hello.go
// 輸入以下代碼保存
package main
import "fmt"

func main(){
    fmt.Println("Hello world!")
}

3.2 執行

直接在文件目錄執行以下命令運行。

go run hello.go
// 或者
go build hello.go
./hello

4. 附件

設置環境變量的配置文件,有網友總結:

/etc/profile,/etc/bashrc 是系統全局環境變量設定
~/.profile,~/.bashrc用戶家目錄下的私有環境變量設定
當登入系統時候獲得一個shell進程時,其讀取環境設定檔有三步
1).首先讀入的是全局環境變量設定檔/etc/profile,然後根據其內容讀取額外的設定的文檔,如
/etc/profile.d和/etc/inputrc
2).然後根據不同使用者帳號,去其家目錄讀取~/.bash_profile,如果這讀取不了就讀取~/.bash_login,這個也讀取不了才會讀取
~/.profile,這三個文檔設定基本上是一樣的,讀取有優先關係
3).然後在根據用戶帳號讀取~/.bashrc
~/.profile與~/.bashrc的區別
都具有個性化定製功能
~/.profile可以設定本用戶專有的路徑,環境變量,等,它只能登入的時候執行一次
~/.bashrc也是某用戶專有設定文檔,可以設定路徑,命令別名,每次shell script的執行都會使用它一次

【精選推薦文章】

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

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師"嚨底家"!!

【拆分版】Docker-compose構建Zookeeper集群管理Kafka集群

寫在前邊

在搭建Logstash多節點之前,想到就算先搭好Logstash啟動會因為日誌無法連接到Kafka Brokers而無限重試,所以這裏先構建下Zookeeper集群管理的Kafka集群。

眾所周知,Zookeeper是一個高效的分佈式協調中間件,可以提供配置信息管理、命名、分佈式同步(分佈式鎖)、集群管理、數據庫切換等服務。這裏主要用它的集群管理功能,它可以確保在網絡狀態不一致,選出一致的Master節點。它是Apache下的一個Java項目,隸屬於Hadroop系統,正如其名”動物管理員”,作為管理員的角色存在。

有興趣了解zookeeper的原理,可以學習Paxos協議與Zab協議。

ps: Hadroop系統下基本上所有的軟件都是動物命名的

在這裏,我們將使用Zookeeper來管理Kafka集群,Kafka是一種消息隊列(Message Queue)中間件,具有高併發、高吞吐量、容錯性強、可擴展等優點。在ELK日誌系統中使用Kafka作為數據的緩衝層,提高了系統的性能與穩定性。

正好今天通過翻看兩者官方的文檔與其Docker鏡像的文檔,終於搭建成功,遂記之分享諸君。鑒於水平有限,如有寫得不對的地方,歡迎大家指正。

本文搭建架構圖

說明:

Zookeeper搭建成集群后,提供命名服務與集群協調服務,Kafka的節點Broker通過domain與ip進行註冊到Zookeeper集群中,通過Zookeeper的協調能力,選出唯一的Leader節點,集群服務啟動並對外提供服務。

環境準備

  • GNU/Debian Stretch 9.9 linux-4.19
  • Docker 18.09.6
  • Docker-Compose 1.17.1

目錄結構

├── docker-kafka-cluster
│   ├── docker-kafka-cluster-down.sh
│   ├── docker-kafka-cluster-up.sh
│   ├── kafka-01
│   │   ├── docker-compose.yml
│   │   └── .env
│   ├── kafka-02
│   │   ├── docker-compose.yml
│   │   └── .env
│   ├── kafka-03
│   │   ├── docker-compose.yml
│   │   └── .env
│   └── kafka-manager
│       ├── docker-compose.yml
│       └── .env
└── docker-zookeeper-cluster
    ├── docker-zk-cluster-down.sh
    ├── docker-zk-cluster-up.sh
    ├── zk-01
    │   ├── docker-compose.yml
    │   └── .env
    ├── zk-02
    │   ├── docker-compose.yml
    │   └── .env
    └── zk-03
        ├── docker-compose.yml
        └── .env

docker-zookeeper-cluster源碼參見我的Git倉庫 https://github.com/hellxz/docker-zookeeper-cluster.git

docker-kafka-cluster源碼參見我的Git倉庫 https://github.com/hellxz/docker-kafka-cluster.git

各節點容器說明列表

Zookeeper集群

節點目錄名 容器名 client port follower port election port
zk-01 zk-01 2181 2888 3888
zk-02 zk-02 2182 2889 3889
zk-03 zk-03 2183 2890 3890

Kafka集群

節點目錄名 容器名 佔用端口
kafka-01 kafka-1 9092
kafka-02 kafka-2 9093
kafka-03 kafka-3 9094
kafka-manager kafka-manager 19000

各文件內容說明

Zookeeper部分

docker-zookeeper-cluster/zk-01目錄下的.env

.env配置文件為docker-compose.yml提供了多個zookeeper的發現服務節點列表

配置格式為 server.x=x節點主機ip:隨從端口:選舉端口;客戶端口 其中xZOO.MY.ID的數值,客戶端口前是;

# set args to docker-compose.yml by default
# set zookeeper servers, pattern is `server.x=ip:follower-port:election-port;client:port`,
# such as "server.1=192.168.1.1:2888:3888;2181 server.2=192.168.1.2:2888:3888;2181", 
# `x` is the `ZOO.MY.ID` in docker-compose.yml, multiple server separator by white space.
# now you can overide the ip for server.1 server.2 server.3, here demonstrate in one machine so ip same.
ZOO_SERVERS=server.1=10.2.114.110:2888:3888;2181 server.2=10.2.114.111:2889:3889;2182 server.3=10.2.114.112:2890:3890;2183

docker-zookeeper-cluster/zk-01目錄下的docker-compose.yml

version: '3'
services:
    zk-01:
        image: zookeeper:3.5.5
        restart: always
        container_name: zk-01
        ports:
            - 2181:2181 # client port
            - 2888:2888 # follower port
            - 3888:3888 # election port
        environment:
            ZOO_MY_ID: 1 # this zookeeper's id, and others zookeeper node distinguishing
            ZOO_SERVERS: ${ZOO_SERVERS} # zookeeper services list
        network_mode: "host"

Kafka部分

kafka-01目錄下的.env 為例

.env配置文件為docker-compose.yml提供了多個zookeeper的ip:client-port列表

# default env for kafka docker-compose.yml
# set zookeeper cluster, pattern is "zk1-host:port,zk2-host:port,zk3-host:port", use a comma as multiple servers separator.
ZOO_SERVERS=10.2.114.110:2181,10.2.114.111:2182,10.2.114.112:2183

kafka-01目錄下的docker-compose.yml,為docker-compse的配置文件

version: "3"
services:
    kafka-1:
        image: wurstmeister/kafka:2.12-2.1.1
        restart: always
        container_name: kafka-1
        environment:
            - KAFKA_BROKER_ID=1 #kafka的broker.id,區分不同broker
            - KAFKA_LISTENERS=PLAINTEXT://kafka1:9092 #綁定監聽9092端口
            - KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://kafka1:9092 #綁定發布訂閱的端口
            - KAFKA_ZOOKEEPER_CONNECT=${ZOO_SERVERS} #連接zookeeper的服務地址
            - KAFKA_MESSAGE_MAX_BYTES=2000000 #單條消息最大字節數
            #- KAFKA_CREATE_TOPICS=Topic1:1:3,Topic2:1:1:compact #創建broker時創建的topic:partition-num:replica-num[:clean.policy]
        network_mode: "host"

KAFKA_CREATE_TOPICS使用官方說明:Topic 1 will have 1 partition and 3 replicas, Topic 2 will have 1 partition, 1 replica and a cleanup.policy set to compact. 文檔地址:https://hub.docker.com/r/wurstmeister/kafka

Zookeeper集群使用

  1. 請確保所布署的 1~3 台服務器網絡可以ping通
  2. 確保第一台主機的2181\2888\3888端口未佔用,第二台主機的2182\2889\3889端口未佔用,第三台主機的2183\2890\3890端口未佔用
  3. 複製zk-01到第一台主機、複製zk-02到第二台主機、複製zk-03到第三台主機
  4. 修改zk-01\zk-02\zk-03目錄下的.env中的ZOO_SERVERS的值,按上述配置要求修改。修改完后的配置應該是集群內通用的,可以scp複製過去。
  5. 單台主機請為docker-zk-cluster-up.shdocker-zk-cluster-down.sh授執行權,使用它們進行up和down操作;多台主機請手動分別進入zk-0x目錄,執行docker-compose up -d以啟動,執行docker-compose down以關閉。

Kafka集群使用

  1. 使用前確保各主機可以互相ping通

  2. 確保zookeeper的服務列表與各對應的zookeeper的ip與客戶端口相同,如不同注意修改.env,集群中.env文件相同,可scp複製

  3. 確保zookeeper集群啟動

  4. 複製kafka-01到第一台主機、複製kafka-02到第二台主機、複製kafka-03到第三台主機

  5. 確保這幾台主機對應的佔用端口號不被佔用 kafka-01對應9092kafka-02對應9093kafka-03對應9094kafka-manager對應19000

  6. 分別對每一台kafka-0x所在的主機修改/etc/hosts,例

    10.2.114.110 kafka1
    10.2.114.111 kafka2
    10.2.114.112 kafka3

    其中每個主機只需要設置自己的主機上的host,比如我複製了kafka-01我就寫本機ip kafka1 ,依次類推.

  7. 單台主機部署kafka集群請為docker-kafka-cluster-up.shdocker-kafka-cluster-down.sh授執行權,不要移動目錄,通過這兩個shell腳本來啟動項目;多台主機請手動進入kafka-0x目錄下,執行docker-compose up -d以後台啟動,執行docker-compose down以移除容器

  8. 啟動腳本中沒有啟動kafka-manager,有需要請自行啟動。為了匹配kafka的版本,使用時設置2.1.1即可。

文中配置部分的ip因使用同一台主機做的測試,所以ip相同,為了防止誤解,在文中已經修改了ip,具體詳見:

  • docker-zookeeper-cluster源碼 https://github.com/hellxz/docker-zookeeper-cluster.git

  • docker-kafka-cluster源碼 https://github.com/hellxz/docker-kafka-cluster.git

本文系原創文章,謝絕轉載

【精選推薦文章】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

雙指針技巧匯總

我認為雙指針技巧還可以分為兩類,一類是「快慢指針」,另一類是「左右指針」。前者解決主要解決鏈表中的問題,比如典型的判定鏈表中是否包含環;後者主要解決數組(或者字符串)中的問題,比如二分查找。

一、快慢指針的常見算法

快慢指針一般都初始化指向鏈表的頭結點 head,前進時快指針 fast 在前,慢指針 slow 在後,巧妙解決一些鏈表中的問題。

1、判定鏈表中是否含有環

這應該屬於鏈表最基本的操作了,如果讀者已經知道這個技巧,可以跳過。

單鏈表的特點是每個節點只知道下一個節點,所以一個指針的話無法判斷鏈表中是否含有環的。

如果鏈表中不包含環,那麼這個指針最終會遇到空指針 null 表示鏈表到頭了,這還好說,可以判斷該鏈表不含環。

boolean hasCycle(ListNode head) {
    while (head != null)
        head = head.next;
    return false;
}

但是如果鏈表中含有環,那麼這個指針就會陷入死循環,因為環形數組中沒有 null 指針作為尾部節點。

經典解法就是用兩個指針,一個每次前進兩步,一個每次前進一步。如果不含有環,跑得快的那個指針最終會遇到 null,說明鏈表不含環;如果含有環,快指針最終會超慢指針一圈,和慢指針相遇,說明鏈表含有環。

boolean hasCycle(ListNode head) {
    ListNode fast, slow;
    fast = slow = head;
    while(fast != null && fast.next != null) {
        fast = fast.next.next;
        slow = slow.next;
        
        if (fast == slow)
            return true;
    }
    return false;
}

2、已知鏈表中含有環,返回這個環的起始位置

這個問題其實不困難,有點類似腦筋急轉彎,先直接看代碼:

ListNode detectCycle(ListNode head) {
    ListNode fast, slow;
    fast = slow = head;
    while (fast != null && fast.next != null) {
        fast = fast.next.next;
        slow = slow.next;
        if (fast == slow)
            break;
    }
    
    slow = head;
    while (slow != fast) {
        fast = fast.next;
        slow = slow.next;
    }
    return slow;
}

可以看到,當快慢指針相遇時,讓其中任一個指針重新指向頭節點,然後讓它倆以相同速度前進,再次相遇時所在的節點位置就是環開始的位置。這是為什麼呢?

第一次相遇時,假設慢指針 slow 走了 k 步,那麼快指針 fast 一定走了 2k 步,也就是說比 slow 多走了 k 步(也就是環的長度)。

設相遇點距環的起點的距離為 m,那麼環的起點距頭結點 head 的距離為 k – m,也就是說如果從 head 前進 k – m 步就能到達環起點。

巧的是,如果從相遇點繼續前進 k – m 步,也恰好到達環起點。

所以,只要我們把快慢指針中的任一個重新指向 head,然後兩個指針同速前進,k – m 步后就會相遇,相遇之處就是環的起點了。

3、尋找鏈表的中點

類似上面的思路,我們還可以讓快指針一次前進兩步,慢指針一次前進一步,當快指針到達鏈表盡頭時,慢指針就處於鏈表的中間位置。

ListNode slow, fast;
slow = fast = head;
while (fast != null && fast.next != null) {
    fast = fast.next.next;
    slow = slow.next;
}
// slow 就在中間位置
return slow;

當鏈表的長度是奇數時,slow 恰巧停在中點位置;如果長度是偶數,slow 最終的位置是中間偏右:

尋找鏈表中點的一個重要作用是對鏈表進行歸併排序。

回想數組的歸併排序:求中點索引遞歸地把數組二分,最後合併兩個有序數組。對於鏈表,合併兩個有序鏈表是很簡單的,難點就在於二分。

但是現在你學會了找到鏈表的中點,就能實現鏈表的二分了。關於歸併排序的具體內容本文就不具體展開了。

4、尋找鏈表的倒數第 k 個元素

我們的思路還是使用快慢指針,讓快指針先走 k 步,然後快慢指針開始同速前進。這樣當快指針走到鏈表末尾 null 時,慢指針所在的位置就是倒數第 k 個鏈表節點(為了簡化,假設 k 不會超過鏈表長度):

ListNode slow, fast;
slow = fast = head;
while (k-- > 0) 
    fast = fast.next;

while (fast != null) {
    slow = slow.next;
    fast = fast.next;
}
return slow;

二、左右指針的常用算法

左右指針在數組中實際是指兩個索引值,一般初始化為 left = 0, right = nums.length – 1 。

1、二分查找

前文 二分查找算法詳解 有詳細講解,這裏只寫最簡單的二分算法,旨在突出它的雙指針特性:

int binarySearch(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;
    while(left <= right) {
        int mid = (right + left) / 2;
        if (nums[mid] == target)
            return mid;
        else if (nums[mid] < target)
            left = mid + 1;
        else if (nums[mid] > target)
            right = mid - 1;
    }
}

2、兩數之和

直接看一道 LeetCode 題目吧:

只要數組有序,就應該想到雙指針技巧。這道題的解法有點類似二分查找,通過調節 left 和 right 可以調整 sum 的大小:

3、反轉數組

void reverse(int[] nums) {
    int left = 0;
    int right = nums.length - 1;
    while (left < right) {
        // swap(nums[left], nums[right])
        int temp = nums[left];
        nums[left] = nums[right];
        nums[right] = temp;
        left++;
        right--;
    }
}

4、滑動窗口算法

這也許是雙指針技巧的最高境界了,如果掌握了此算法,可以解決一大類子字符串匹配的問題,不過「滑動窗口」算法比上述的這些算法稍微複雜些。

幸運的是,這類算法是有框架模板的,下篇文章就準備講解「滑動窗口」算法模板,幫大家秒殺幾道 LeetCode 子串匹配的問題。

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

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

由老同事學習SAP所想到的

前段時間一位老同事在微信上跟我說他們公司正計劃導SAP系統,但整個IT中心幾乎無人使用過SAP,知道我在這行業幹了多年了,所以想問我怎麼開始學習。於是我約他今天出來聊聊,順便把手裡的SAP ECC EHP6版本的虛擬機拷給他自己先自學。 

他們公司一直都是在用九二年版的QAD系統(美國ERP廠商),跟之前我們同事的那家企業系統一致,非常古老的系統,不支持鼠標操作,基本上現在ERP系統該有的功能它都沒有,唯一好處的是開源可開發。公司老闆不知道從哪裡交流了一下,然後打算大刀闊斧大幹一場,改革目前信息化現狀,為將來業務擴展做信息化支撐。 

一直以來他都是做ERP行業,接觸過多個模塊,現在這個公司可能是因為體量小的原因,一個人幾乎全管了所有的模塊,業務能力很紮實,對企業的流程和供應鏈非常熟悉。看我給他演示了一下基礎的SAP操作和邏輯,一直驚呼SAP的強大。

 

 SAP的龐大複雜對於一個從來沒接觸到人來說門檻還是相當高的,這個門檻並不是看幾本PDF、看幾個視頻、上上培訓機構就能越過得了的,其中包含的後台邏輯配置和各種強關聯絕對會把一個人打蒙。想起前幾年碰到一個啥都不懂的信息化管理者,在ERP選型會議上跟演示系統的供應商要求在企業內部安裝一套空白的ERP試用,想想這真是一大笑柄。

 這持續枯燥乏味的學習過程絕對非常考驗一個人的毅力。想起十多年前,為了學習SAP,我從騰訊拍拍上花了600元買SAP ECC的安裝包,含視頻教程差不多三十多張DVD光盤,升級了老爺筆記本配置(酷睿雙核、4G內存、500G机械硬盤),安裝Windows Server,安裝Java,安裝MSSQL,安裝SAP,通宵安裝了十五六個小時才搞定,佔用硬盤空間220G,一開啟SAP服務整個電腦就得卡死半個小時,CPU直接100%,內存爆滿。

之後對着SAP GUI界面一臉懵逼,根本不知道怎麼下手。雖然我知道部分ERP的流程和功能,但我根本不知道怎麼弄。看購買回來的視頻也是一臉懵逼,因為系統裏面的組織配置跟視頻教程里根本就不一樣,真要操作起來困難重重,各種紅燈錯誤,這也不行那也不行,那種深深的絕望感至今歷歷在目。

 

後來跌跌撞撞學了一點ABAP開發,由於沒有實際的工作經歷,也只是懂個ABAP開發的一絲絲皮毛而已。那時候沒有SAP前輩先驅可以交流,沒有QQ群,連熱鬧一點的論壇都沒有,夜以繼日枯燥得學習才進步這麼點,支撐起我這份毅力恆心的大概就是“生存”壓力吧。一心想離開那時候的工作環境,不願被溫水煮死。

後來在廈門面試了一家正在實施SAP的企業,面試的主管給我出了一道SAP開發的題目,非常簡單的數據查詢我都沒能做出來,好在他們給了我機會讓我回去用自己的電腦做題。回去之後我狂惡補知識,當晚做題到凌晨,將源碼發郵件給那位主管,第二天早上接到他們複試的通知,於是第二輪面試的時候我也很幸運成功解決了ABAP的問題,就這樣開始跟SAP結緣了。

 

為了不讓主管失望,覺得我SAP技術是半桶水,那時候我瘋狂加班,下班回來也利用自己電腦的SAP狂學習,不停研究顧問開發的代碼,看到不熟悉的語法就記下來百度,做各種嘗試測試。恰好那時候公司要開發三支程序,顧問那邊報價十多萬台幣。於是我自告奮勇,跟主管說我來開發。然後就是瘋狂的查閱資料,查看SAP官方英文文檔,系統測試,順利得完成了任務。短短2個月就給公司省了十多萬的開發費用,且提前了一個月轉正。不得不說,不逼一下自己都不知道自己原來可以如此優秀。

再後來跳槽去做業務模塊做項目了,開始是做MM模塊,實施和運維過程中遇到過各種各樣的問題,也深深感受到了SAP的強大,後來又接觸了SD模塊,Basis模塊等。我覺得一個SAP顧問如果不精通一兩個模塊,其他模塊如果不熟悉的話,是很沒優勢的。這個過程中累積的各種筆記和實施運維實錄有五六百兆,上千篇文檔。

就這樣曲曲折折這麼些年,非常成功的項目也有,失敗的項目也有,見識到了形形色色的SAP顧問和關鍵用戶,這些都變成了自己非常寶貴的經驗。一個顧問如果沒有經歷過失敗的項目,那就是失敗的!

 

當然,之前兩年半的QAD運維並非全是沒用的,至少讓我懂得了部分業務,知道了如何敏捷高效開發(這點得感謝那時候的主管領導,至今讓我受益無窮,很遺憾現在絕大多數只是有開發的語法並沒有開發的思維觀念),也讓我明白系統固然重要但企業流程和業務分析能力更重要。我曾經不止一次說過考驗一個SAP顧問的能力並不在於他會多少事務代碼,知道後台表是什麼,不在於他知道SAP這個功能如何配置,而是他對業務的分析水平的高低以及需求溝通的能力大小,這才是一個資深的SAP顧問跟一個培訓機構培訓出來的人的區別。

很多人來信問我該如何入行SAP這個行業,每個人成長的道路不同,但我還是很忌諱培訓機構的,他們只會弄虛作假,投機取巧,教你如何在簡歷上謊報項目經驗,也只會教一些系統層級的東西,隨便甲方稍微面試一下就露馬腳了。我覺得時刻準備着,好好學習,找機會入職甲方或者乙方才是正道,別去花冤枉錢。

老同事如今也面臨“生存”壓力,我想他應該是有毅力堅持下去的,但能學到什麼程度就不知道了。不過他懂開發,懂業務,學起SAP應該可以輕鬆不少。要知道一個人能集業務分析、開發、項目管理、系統配置於一身,那真的不得了!

 

 

 

 

  本文作者 | SAP夢心

  聯繫方式 | 微信:W150112458(瘋狂的程序員)

  特別敬告 | 歡迎轉載,轉載請註明出處並保持原文不動,謝謝

 

 

【精選推薦文章】

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

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師"嚨底家"!!

使用React Hook后的一些體會

一、前言

距離React Hook發布已經有一段時間了,筆者在之前也一直在等待機會來嘗試一下Hook,這個嘗試不是像文檔中介紹的可以先在已有項目中的小組件和新組件上嘗試,而是嘗試用Hook的方式構建整個項目,正好新的存儲項目啟動了,需要一個新的基於web的B/S管理系統,機會來了。在項目未進入正式開發前的時間里,筆者和小夥伴們對官方的Hook和Dan以及其他優秀開發者的關於Hook的文檔和文章都過了至少一遍,當時的感覺就是:之前學的又沒用了,新的一套又來了。目前這個項目已經成功搭起來了,主要組件和業務已具規模,UT也對應完成了。是時候寫一下對Hook使用后的初步體會了,在這裏,筆者不會做太多太深入的Hook API和原理講解,因為很多其他優秀的文章可以已經講得足夠多了。再者因為雖然重構了項目,但代碼組織方式可能還不是最Hook的方式。本文內容大多為筆者認為使用Hook最需要明白的地方。

 

二、怎麼替代之前的生命周期方法?

這個問題在筆者粗略地過了一遍Hook的API后自然而然地產生了,因為畢竟大多數關注Hook新特性的開發者們,都是從生命周期的開發方式方式過來的,從 createClass 到ES2015的 class ,再到Hook。很少有人是從Hook出來才使用React的。這也就是說,大家在使用初期,都會首先用生命周期的思維模式來探究Hook的使用,就像我們對英語沒熟練使用之前,英文對話都是先在心裏準備出中文語句,在心裏翻譯出英文語句再說出來。筆者已有3年的生命周期方式的開發經驗,慣性的思維改變起來最為困難。

筆者在之前使用生命周期的方式開發組件時,使用最多的、對要實現業務最依賴的生命周期是 componentDidMount 、 componentWillReceiveProps 、 shouldComponentUpdate 。

對於 componentDidMount 的替代方式很簡單: useEffect(() => {/* code */}, []); ,使用 useEffect hook,依賴給空數組就行,空數組在這裏表示有依賴的存在,但依賴實際上又為空,會是這個hook在初次render完成的時候調用一次足矣。如果有需要在組件卸載的生命周期內 componentWillUnmount 乾的事情,只需要在 useEffect 內部返回一個函數,並在這個函數內部做這些事情即可。但要記住的時候,考慮到函數的Capture Value的特性,對值的獲取等情況與生命周期方法的表現並非完全一致。

對於 componentWillReceiveProps 這個生命周期。首先這裏說說筆者自己的歷史原因。在React16.3版本以後,生命周期API被大幅修改,16.4又在16.3上改了一把,為了後期的Async Render的出現,原有的 componentWillReceiveProps 被預先重命名為unsafe方法,並引入了 getDerivedStateFromPorps 的靜態方法,為了不重構項目,筆者把React和對應打包工具都停留在了16.2和適配16.2的版本。現有的Hook文檔也忽略了怎麼替代 componentWillReceiveProps 。其實這個生命周期的替代方式最為簡單,因為像 useEffect 、 useCallback 、 useMemo 等hook都可以指定依賴,當依賴變化后,回調函數會重新執行,或者返回一個根據依賴產生的新的函數,或者返回一個根據依賴產生的新的值。

對於 shouldComponentUpdate 來說,它和 componentWillReceiveProps 的替換方式其實差不多。說實話,筆者在項目中,至少是在目前跑在PC瀏覽器的項目中,不太經常使用這個生命周期。因為在目前的業務中,從redux導致的props更新基本都有數據變化進而導致有視圖更新的需要,可能從觸發父到子的prop更新的時候,會出現不太必要的沖渲染需要,這個時候可能需要這個生命周期對當前和歷史狀態進行判斷。也就是說,如果對於某個組件來說,差不多每次的props變化大概率可能是值真的變了,其實做比較是無意義的,因為比較也需要耗時,特別是數據量較大的情況。最後耗時去比較了,結果還是數據發生了變化,需要衝渲染,那麼這是很操蛋的。所有說不能濫用 shouldComponentUpdate ,真的要看業務情況而定,在PC上多幾次小範圍的無意義的重渲染對性能影響不是很大,但在移動端的影響就很大,所以得看時機情況來決定。

Hook帶來的改變,最重要的應該是在組織一個組件代碼的時候,在思維方式上的變化,這也是官方文章中有提到的:”忘記你已經學會的東西”,所以我們在熟悉Hook以後,在書寫組件邏輯的時候應該不要先考慮生命周期是怎麼實現這個業務的,再轉成Hook的實現,這樣一來,一是還停留在生命周期的方式上,二是即便實現了業務功能,可能也不是很Hook的最優方式。所以,是時候用Hook的方式來思考組件的設計了。

 

三、不要忘記依賴、不要打亂Hook的順序

先說Hook的順序,在很多文章中,都有介紹Hook的基本實現或模擬實現原理,筆者這裏不再多講,有興趣可以自行查看。總結來說就是,Hook實現的時候依賴於調用索引,當某個Hook在某一次渲染時因條件不滿足而未能被調用,就會造成調用索引的錯位,進而導致結果出錯。這是和Hook的實現方式有關的原因,只要記住Hook不能書寫在 if 等條件判斷語句內部即可。

對於某個hook的依賴來說,一定要記住寫,因為函數式組件是沒有 componentWillReceive 、 shouldComponentUpdate 生命周期的。任何在重渲染時,一個函數是否需要重新創建、一個值是否需要重新計算,都和依賴有關係,如果依賴變了,就需要計算,沒變就不需要計算,以節省重渲染的成本。這裏特別需要注意的是函數依賴,因為函數內部可能會使用到 state 和 props 。比如,當你在 useEffect 內部引用了某些 state 和 props ,你可能會很容易的查看到,但是不太容易查看到其內部調用的其他函數是否也用到了 state 和 props 。所以函數的依賴一定不要忘記寫。當然官方的CRA工具已經集成了ESlint配置,來幫我們檢測某個hook是否存在有遺漏的依賴沒有寫上。PS. 這裏我也推薦大家使用CRA進行項目初始化,並eject出配置文件,這樣可以按照我們的業務要求自定義修改配置,然後將一些框架代碼通過yeoman打包成generator,這樣我們就有了自己的種子項目生成器,當開新項目的時候,可以進行快速的初始化。

 

四、Cpature Value特性

捕獲值的這個特性並非函數式組件特有,它是函數特有的一種特性,函數的每一次調用,會產生一個屬於那一次調用的作用域,不同的作用域之前不受影響。筆者看過的有關Hook的文檔中,大多都引述過這個經典的例子:

function App (){
    const [count, setCount] = useState(0);
    
    function increateCount (){
        setCount(count + 1);
    }
    
    function showCount (){
        setTimeout(() => console.log(`你點擊了${count}次`), 3000);
    }
    
    return (
        <div>
            <p>點擊了{count}次</p>
            <button onClick={increateCount}>增加點擊次數</button>
            <button onClick={showCount}>显示點擊次數</button>
        </div>
    );
}

當我們點擊了一次”增加點擊次數”按鈕后,再點擊”显示點擊次數”按鈕,在大約3s后,我們可以看到點擊次數會在控制台輸上出來,在這之前我們再次點擊”增加點擊次數”按鈕。3s后,我們看到控制台上輸出的是1,而我們期望的是2。當你第一次接觸Hook的時候看到這個結果,你一定會大吃一驚,WTF?

可以驚,但不要慌,聽我細細道來:

1. 當App函數組件初次渲染完后,生成了第一個scope。在這個scope中, count 的值為0。

2. 我們第一次點擊”增加點擊次數”按鈕的時候,調用了 setCount 方法,並將 count 的值加1,觸發了重渲染,App組件函數因重渲染的需要而被重新調用,生成了第二個scope。在這個scope中,count為1。頁面也更新到最新的狀態,显示”點擊了1次”。

3. 緊接着我們點擊了”显示點擊次數”按鈕,將調用 showCount 方法,延遲3s后显示 count 的值。請注意這裏,我們這次操作是在第二次渲染生成的這個scope(第二個scope)中進行的,而在這個scope中, count 的值為1。

4. 在3s的異步宏任務還未被推進主線程執行之前,我們又再次點擊了”增加點擊次數”按鈕,再次調用了 setCount 方法,並加 count 的值再次加1,又觸發了重渲染,App組件函數因重渲染的需要而被重新調用,生成了第三個scope。在這個scope中,count為2。頁面也更新到最新的狀態,显示”點擊了2次”。

5. 3s到了以後,主線程也出於空閑狀態,之前壓入異步隊列的宏任務被推入主線程中執行,重要的地方來了,這個異步任務所處的作用域是屬於第二個scope,也就是說它會使用那一次渲染scope的 count 值,也就是1。而不是和界面最新的渲染結果2一樣。

當你使用類組件來實現這個小功能並進行相同操作的時候,在控制台得到的結果都不同,但是在界面上最終的結果是一致的。在類組件中,我們在是生命周期方法 componentDidMount 、 componentDidUpdate 通過 this.state 去獲取狀態,得到的一定是其最新的值。這就是最大的不同之處,也是讓初學者很困惑,很容易踩入坑中的地方,當然這個坑並不是說函數式組件和Hook設計上的問題,而是我們對其的不了解,進而導致使用上的錯誤和對結果的誤判,進而導致代碼出現BUG。

Capture Value這個特性在Hook的編碼中一定要記住,並且理解。

如果說想要跳出每個重渲染產生的scope會固化自己的狀態和值的特性,可以使用Hook API提供的 useRef hook,讓所有的渲染scope中的某個狀態,都指向一個統一的值的一個Key(API中採用current)。這個對象是引用傳遞的,ref的值記錄在這個Key中,我們並不直接改變這個對象本身,而是通過修改其的一個Key來修記錄的值。讓每次重渲染生成的scope都保持對同一個對象的引用,來跳出Cpature Value帶來的限制。

 

五、Hook的優勢

在Hook的官方文檔和一些文章中也提到了類組件的一些不好的地方,比如:HOC的多層嵌套,HOC和Render Props也不是太理想的復用代碼邏輯,有關狀態管理的邏輯代碼很難在組件之間復用、一個業務邏輯的實現代碼被放到了不同的生命周期內、ES2015與類有關語法和this指向等困擾初級開發者的問題等都有提到。還有像上一段落中提到的一些問題一樣。這些都是需要改革和推動的地方。

這裏筆者對HOC的多層嵌套確實覺得很噁心,因為筆者之前的項目就是這樣的,一旦進入開發者工具的React Dev Tool的Tab,犹如地獄般的connect、asyncLoad就出現了,你會發現每個和Redux有關的組件都有一個connect,做了代碼分割以後,異步加載的組件都有一個asyncLoad(雖然後面可以用原生的 lazy 和 suspense 替代),很多因使用HOC而帶來的負面影響,對強迫症患者來說這不可接受,只能不看了之。

而對於類組件生命周期的開發方式來說,一個業務邏輯的實現,需要多個生命周期的配合,也就是邏輯代碼會被放到多個生命周期內部,在一個組件比較稍微龐大和複雜以後,維護起來較為困難,有些時候可能會忘記修改某個地方,而採用Hook的方式來實現就比較好,可以完全封裝在一個自定hook內部,需要的組件引入這個hook即可,還可以做到邏輯的復用。比如這個簡單的需求:在頁面渲染完成后監聽一個瀏覽器網絡變化的事件,並給出對應提示,在組件卸載后,我們再移除這個監聽,通常使用生命周期的實現方式為:

class App (){
    browserOnline () {
        notify('瀏覽器網絡已恢復正常!');  
    }   

    browserOffline () {
        notify('瀏覽器發生網絡異常!');  
    }  

    componentDidMount (){
        window.addEventListener('online', this.browserOnline);
        window.addEventListener('offline', this.browserOffline);
    }  

    componentWillUnmount (){
        window.removeEventListener('online', this.browserOnline);
        window.removeEventListener('offline', this.browserOffline);
    }
}

使用Hook方式實現:

function useNetworkNotification (){
    const browserOnline = () => notify('瀏覽器網絡已恢復正常!');

    const browserOffline = () => notify('瀏覽器發生網絡異常!');

    useEffect(() => {
        window.addEventListener('online', browserOnline);
        window.addEventListener('offline', browserOffline);

        return () => {
            window.removeEventListener('online', browserOnline);
            window.removeEventListener('offline', browserOffline);
        };
    }, []);
}
function App (){
    useNetworkNotification();
}    

function AnotherComp (){
    useNetworkNotification();
}

所以,採用Hook實現的代碼不僅管理起來方便(無需將相關的代碼散布到不同的生命周期方法內),可以封裝成自定義的hook,便於邏輯的在不同組件間復用,組件在使用的時候也不需要關注其內部的實現方式。這僅僅是實現了一個很簡單功能的例子,如果項目變得更加複雜和難以維護,通過自定義Hook的方式來抽象邏輯有助於代碼的組織質量。

 

六、為啥會推動Hook

筆者認為上個段落中提到的函數式組件配合Hook相較於類組件配合生命周期方法是存在有一定優勢的。再者,React團隊最開始發布Hook的時候,其實是頂着很大的壓力的,因為這對於開發者來說實在就是以前的白學了,除了底層某些思想不變外,上層API全部變完。筆者最開始了解Hook后,最直接感受就是這東西是不是在給後面的Async Render填坑用的,為啥會這麼說呢?因為React的這種更新機制就是全部樹做Diff然後更新patch。而Vue是依賴收集方式的,數據變化后,哪些地方需要更新是明確的,所以更新是精準的。React的這種設計機制,就導致更新的成本很高,即便有虛擬樹,但是一旦應用很龐大以後,遍歷新舊虛擬樹做Diff也是很耗時的,並且沒有Async Render前,一旦開啟協調,就只能一條路走到底,代碼又不能控制JS引擎的函數調用棧,在主線程長時間運行腳本又不歸還控制權,會阻塞線程造成界面友好度下降,特別是當應用運行在移動端設備等性能不太強的計算機上時效果特別顯著。而基於Fiber的鏈表式樹結構可以模擬出函數調用棧,並能夠由代碼控制工作的開始和暫停,可以有效解決上述問題,但它會破壞原本完整的生命周期方式,因為一個協調的任務,可能會放在不同的線程空閑時間內去完成,進而導致一個生命周期可能會被調用多次,導致實際運行的結果並不像代碼書寫的那樣,這也是在16.3及以後版本將某些生命周期重命名為unsafe的原因。生命周期基本廢掉了,雖然後來引入了一些靜態方法用來解決一些問題,但存在感太低了,基本都屬於過度階段的產物。生命周期廢了,就需要有東西來替代,並支持Async Render的實現,Hook這種模式就是一個不錯的選擇。當然這可能並不全面,或者說的不絕對正確,但筆者認為是有這個原因的。

 

七、單元測試

筆者目前的項目對穩定性要求高,屬於LTS類型,不像創業型的互聯網項目,可能上線幾個月就下了,所以UT是必須的。筆者給新項目的模塊寫單元測試的時候,比較完好的支持Hook的Enzyme3.10版本在8天前才發布:(。從目前測試的體驗來看,相對於類組件時代確實有進步。在類組件時代,除了生命周期外,其他的一切基本都靠HOC來完成,這就造成了我們在測試的時候,必須套上HOC,而當測試組件業務邏輯的時候,又必須扒開之前套上的HOC,找到裏面的真實組件,再進行各種模擬和打樁操作。而函數式組件是沒有這個問題的,有Hook加持后,一切都是扁平化的,總之就是比之前好測了。有一點稍微麻煩點的就是:

1. 涉及到會觸發重渲染,會執行useEffect 和 useState 的操作,需要放入 react-dom/test-utils 的act 方法內,並且還需要注意源代碼是同步還是異步執行,並且在 act 方法執行后,需要執行wrapper的 update 來更新wrapper。遇到這類問題不難解決,到React、Enzyme的Github上搜對應issue即可。

2. 測試中,Capture Value的特性也會存在,所以有些之前緩存的東西,並不是最新的:(。

當然類組件時代也有好處,就是能夠訪問instance,但對於函數組件來說,無法從函數外面訪問函數作用域內的東西。

 

八、總結

就像官方團隊的文章中寫道的一樣:“如果你太不能夠接受Hook,我們還是能夠理解的,但請你至少不要去噴它,可以適當宣傳一下。”。我們還是可以大膽嘗試一下Hook的,至少現在2019年年中的時候,因為在這個時間點,一切有關Hook的支持和文檔應該都比去年年底甚至是年初的時候更加完善了,雖然可能還不是太完全,但至少官方還在繼續摸索,社區也很活躍,造輪子的人也很多。之前也有消息說Vue3.0大版本也會出Hook,哈哈,又是一片腥風血雨。總之,風口來了,能折騰的、喜歡折騰的就跟着風吹唄。入門簡單,但完全、徹底地掌握和熟練運用,還是需要時間的。

 

【精選推薦文章】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

[WPF自定義控件庫]使用TextBlockHighlightSource強化高亮的功能,以及使用TypeConverter簡化調用

1. 強化高亮的功能

上一篇文章介紹了使用附加屬性實現TextBlock的高亮功能,但也留下了問題:不能定義高亮(或者低亮)的顏色。為了解決這個問題,我創建了TextBlockHighlightSource這個類,比單純的字符串存儲更多的信息,這個類的定義如下:

相應地,附加屬性的類型也改變為這個類,並且屬性值改變事件改成這樣:

private static void OnHighlightTextChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
    var oldValue = (TextBlockHighlightSource)args.OldValue;
    var newValue = (TextBlockHighlightSource)args.NewValue;
    if (oldValue == newValue)
        return;

    void OnPropertyChanged(object sender,EventArgs e)
    {
        if (obj is TextBlock target)
        {
            MarkHighlight(target, newValue);
        }
    };

    if(oldValue!=null)
        newValue.PropertyChanged -= OnPropertyChanged;

    if (newValue != null)
        newValue.PropertyChanged += OnPropertyChanged;

    OnPropertyChanged(null, null);
}

MarkHighlight的關鍵代碼修改為這樣:

if (highlightSource.LowlightForeground != null)
    run.Foreground = highlightSource.LowlightForeground;

if (highlightSource.HighlightForeground != null)
    run.Foreground = highlightSource.HighlightForeground;

if (highlightSource.HighlightBackground != null)
    run.Background = highlightSource.HighlightBackground;

使用起來就是這樣:

<TextBlock Text="Git hub"
           TextWrapping="Wrap">
    <kino:TextBlockService.HighlightText>
        <kino:TextBlockHighlightSource Text="hub"
                                       LowlightForeground="Black"
                                       HighlightBackground="#FFF37D33" />
    </kino:TextBlockService.HighlightText>
</TextBlock>

2. 使用TypeConverter簡化調用

TextBlockHighlightSource提供了很多功能,但和直接使用字符串比起來,創建一個TextBlockHighlightSource要複雜多。為了可以簡化調用可以使用自定義的TypeConverter

首先來了解一下TypeConverter的概念。XAML本質上是XML,其中的屬性內容全部都是字符串。如果對應屬性的類型是XAML內置類型(即Boolea,Char,String,Decimal,Single,Double,Int16,Int32,Int64,TimeSpan,Uri,Byte,Array等類型),XAML解析器直接將字符串轉換成對應值賦給屬性;對於其它類型,XAML解析器需做更多工作。

<Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="*"/>
</Grid.RowDefinitions>

如上面這段XAML中的”Auto”和”*”,XAML解析器將其分別解析成GridLength.Auto和new GridLength(1, GridUnitType.Star)再賦值給Height,它相當於這段代碼:

grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });

為了完成這個工作,XAML解析器需要TypeConverter的協助。XAML解析器通過兩個步驟查找TypeConverter:
1. 檢查屬性聲明上的TypeConverterAttribute。
2. 如果屬性聲明中沒有TypeConverterAttribute,檢查類型聲明中的TypeConverterAttribute。

屬性聲明上TypeConverterAttribute的優先級高於類型聲明。如果以上兩步都找不到類型對應的TypeConverterAttribute,XAML解析器將會報錯:屬性”*”的值無效。找到TypeConverterAttribute指定的TypeConverter后,XAML解析器調用它的object ConvertFromString(string text)函數將字符串轉換成屬性的值。

WPF內置的TypeConverter十分十分多,但有時還是需要自定義TypeConverter,自定義TypeConverter的基本步驟如下:

  • 創建一個繼承自TypeConverter的類;
  • 重寫virtual bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType);
  • 重寫virtual bool CanConvertTo(ITypeDescriptorContext context, Type destinationType);
  • 重寫virtual object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value);
  • 重寫virtual object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType);
  • 使用TypeConverterAttribute 指示XAML解析器可用的TypeConverter;

到這裏我想TypeConverter的概念已經介紹得夠詳細了。回到本來話題,要簡化TextBlockHighlightSource的調用我創建了TextBlockHighlightSourceConverter這個類,它繼承自TypeConverter,裏面的關鍵代碼如下:

public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
    if (sourceType == typeof(string))
    {
        return true;
    }

    return base.CanConvertFrom(context, sourceType);
}

public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
    switch (value)
    {
        case null:
            throw GetConvertFromException(null);
        case string source:
            return new TextBlockHighlightSource { Text = value.ToString() };
    }

    return base.ConvertFrom(context, culture, value);
}

然後在TextBlockHighlightSource上使用TypeConverterAttribute:

[TypeConverter(typeof(TextBlockHighlightSourceConverter))]
public class TextBlockHighlightSource : FrameworkElement

這樣在XAML中TextBlockHighlightSource的調用方式就可以和使用字符串一樣簡單了。

<TextBlock Text="Github"
           kino:TextBlockService.HighlightText="hub" />

3. 使用Style

有沒有發現TextBlockHighlightSource繼承自FrameworkElement?這種奇特的寫法是為了讓TextBlockHighlightSource可以使用全局的Style。畢竟要在應用程序里統一Highlight的顏色還是全局樣式最好使,但作為附加屬性,TextBlockHighlightSource並不是VisualTree的一部分,它拿不到VisualTree上的Resources。最簡單的解決方案是讓TextBlockHighlightSource繼承自FrameworkElement,把它放到VisualTree里,用法如下:

<StackPanel>
    <FrameworkElement.Resources>
        <Style TargetType="kino:TextBlockHighlightSource">
            <Setter Property="LowlightForeground" Value="Blue"/>
        </Style>
    </FrameworkElement.Resources>
    <TextBox x:Name="FilterElement3"/>
    <kino:TextBlockHighlightSource Text="{Binding ElementName=FilterElement3,Path=Text}" 
                                   HighlightForeground="DarkBlue"
                                   HighlightBackground="Yellow"
                                   x:Name="TextBlockHighlightSource2"/>
    <TextBlock Text="A very powerful projector with special features for Internet usability, USB" 
               kino:TextBlockService.HighlightText="{Binding ElementName=TextBlockHighlightSource2}"
               TextWrapping="Wrap"/>
</StackPanel>

也許你會覺得這種寫法有些奇怪,畢竟我也覺得在View上放一個隱藏的元素真的很怪。其實在一萬二千年前微軟就已經有這種寫法,在DomainDataSource的文檔里就有用到:

<Grid x:Name="LayoutRoot" Background="White">  
    <Grid.RowDefinitions>
        <RowDefinition Height="25" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <riaControls:DomainDataSource x:Name="source" QueryName="GetProducts" AutoLoad="true">
        <riaControls:DomainDataSource.DomainContext>
            <domain:ProductDomainContext />
        </riaControls:DomainDataSource.DomainContext>   
        <riaControls:DomainDataSource.FilterDescriptors>
            <riaData:FilterDescriptorCollection LogicalOperator="And">
              <riaData:FilterDescriptor PropertyPath="Color" Operator="IsEqualTo" Value="Blue" />
              <riaData:FilterDescriptor PropertyPath="ListPrice" Operator="IsLessThanOrEqualTo">
                  <riaControls:ControlParameter 
                      ControlName="MaxPrice" 
                      PropertyName="SelectedItem.Content" 
                      RefreshEventName="SelectionChanged" />
              </riaData:FilterDescriptor>
            </riaData:FilterDescriptorCollection>
        </riaControls:DomainDataSource.FilterDescriptors>
    </riaControls:DomainDataSource>
    <ComboBox x:Name="MaxPrice" Grid.Row="0" Width="60" SelectedIndex="0">
        <ComboBoxItem Content="100" />
        <ComboBoxItem Content="500" />
        <ComboBoxItem Content="1000" />
    </ComboBox>
    <data:DataGrid Grid.Row="1" ItemsSource="{Binding Data, ElementName=source}" />
</Grid>

把DataSource放到View上這種做法可能是WinForm的祖傳家訓,結構可恥但有用。

4. 結語

寫這篇博客的時候我才發覺這個附加屬性還叫HighlightText好像不太好,但也懶得改了。

這篇文章介紹了使用TypeConverter簡化調用,以及繼承自FrameworkElement以便使用Style。

5. 參考

TypeConverter 類
TypeConverters 和 XAML
Type Converters for XAML Overview
TypeConverterAttribute Class
如何:實現類型轉換器

6. 源碼

TextBlock at master · DinoChan_Kino.Toolkit.Wpf

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

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