分類
發燒車訊

對於計算機相關專業我們在迷茫什麼

計算機相關專業初識–對於計算機相關專業我們在迷茫什麼

前言

由於種種原因,迫使我寫下這篇博客,我相信,初入計算機相關專業的萌新肯定很迷茫,我該學什麼,我該如何去學,我該如何學好等等問題纏繞心頭。有很多學弟學妹問我該如何去學計算機相關專業,作為過來人,我決定將我的所知所得寫下來,讓初入計算機相關專業的萌新的學習之路走得更順暢一些。

一、什麼是計算機

對於剛學習計算機相關專業的萌新來說,了解一下計算機的工作原理是十分必要的,但是在這裏我們不過多闡述,讓大家簡單了解一下就好。

讓我們先來看一下對於計算機名詞的解釋:

計算機(computer)俗稱電腦,是現代一種用於高速計算的电子計算機器,可以進行數值計算,又可以進行邏輯計算,還具有存儲記憶功能。是能夠按照程序運行,自動、高速處理海量數據的現代化智能电子設備。

划重點:

  • 我們注意到,計算機就是一種用於進行數值計算的現代化智能电子設備。需要理解的是為什麼是進行數值計算,在這裏,你會疑惑,為什麼是數值計算呢,我輸入的明明不是数字呀?這個問題很容易解釋清楚,因為計算機只是一種电子設備,它不具有人類獨立思考和不斷學習的能力,它的所有功能都是事先設定好的,所以當計算機面對輸入字符的時候,會將它統一按照ASCII(計算機編碼系統)規則轉換為數值“0”和“1”(二進制數值),所以,在計算機里,數據存儲都是用“0”和“1”(即二進制數值)來實現。

  • 還有一點值得注意,按照程序運行,那麼問題來了,程序是什麼?程序就是一組計算機能識別和執行的指令, 它以某些程序設計語言編寫,運行於某種目標結構體繫上 。舉個例子,程序就像是用英語(程序設計語言,例如c,c++)寫的文章,要讓一個懂的英語的人(編譯器,如C的編譯器gcc,這裏要注意編譯器和IDE的區別,通常IDE包含編譯器)同時也會閱讀這篇文章的人(結構體系)來閱讀、理解、標記這篇文章。

有學妹問過我,為什麼簡單的代碼,能實現豐富的效果。其實這取決於編譯器的強大能力。下面來簡單介紹一下,編輯器,編譯器,IDE(集成開發環境)的區別。

  • 編輯器:編輯器就是用來編輯的軟件,比如windows自帶的記事本就是一個編輯器, 記事本沒有語法高亮,不显示行號,當一段可執行代碼寫完后無法通過內置環境執行,必須手動輸入命令執行編譯等等一些弊端,所以很少有程序員會用記事本去寫代碼 , 寫代碼比較好用的編輯器軟件有vscode,vim,sublime,notepad++,emacs,atom等等 ,雖然編輯器原始功能不足,但是開發人員為了使編輯器更加友好,所以有很多內置插件可供使用,完全可以手動打造一個IDE。
  • 編譯器:簡單來說,編譯器就是將“一種語言(一般為高級語言,如c,c++,java,python等,計算機不可直接識別和執行)”翻譯為“另一種語言(一般為低級語言,低級語言即機器語言,機器語言是用二進制代碼錶示的計算機能直接識別和執行的一種機器指令的集合)”的程序。舉個例子,用Dev-C++寫好一段可執行"hello,world!"C語言代碼之後,我們要讓它在屏幕打印出來我們想要它輸出的"hello,world!",就需要通過gcc編譯器執行編譯后才能显示。其他語言同理。
  • IDE:集成開發環境,用於程序開發環境的應用程序,一般包含代碼編輯器編譯器調試器圖像用戶界面等工具。集成了代碼編寫程序分析程序編譯程序調試等功能。如 jetbrains 的用於Java開發的 IntelliJ IDEA 、用於JavaScript開發的WebStorm、用於Python開發Pycharm,微軟的 Visual Studio系列 ,IBM的Eclipse。

二、我們該學什麼

很多初入計算機相關專業的萌新,總是很迷茫,不知道自己該學什麼,通常是他們知道如何去學好學校開設的每一門課程,就是不知道自己該向哪些方向學習,這些方向指的是專業技能和就業方向,諸如web開發、Android/IOS開發、數據分析、人工智能、網絡安全、遊戲開發、軟件測試等等。有這種疑惑很正常,迷茫也是正常的,但我們總要讓自己了解自己所需,然後腳踏實地,一步一步去充實自己的能力。而我想做的也很簡單,就是幫助大家解除心裏的疑惑。那麼,我們開始進入正題。

1. 我們該如何選擇適合自己的方向

對於這個問題,其實是很難回答清楚的,因為每個人的興趣都不相同,所以就很難去站在自己的角度去回答疑問者的問題。但是,原理都是想通的,我相信我的經驗會幫助到你們。

  • 通常,學校每學期都會開設一門或多門語言(程序設計語言,下文同),那麼,喜歡一門語言,首先要愛上它的語言風格,諸如Java的嚴謹,Python的自由,總有一款適合你;其次,在學習語言的過程中,一定要了解它能幹什麼,市場環境如何,工作崗位多少等綜合因素,再決定要不要去深入這門語言,並且主攻自己感興趣的那個方向。

  • 對於學校沒有開設,但是自己又想學習的語言而言,該如何去選擇。首先,學校開設的語言基本是市場比較流行的語言,也符合市場需求,所以,完全可以在學校開設的語言中去選擇自己想要了解並學習的語言。此外,我們可以藉助 TIOBE ( TIOBE 編程社區指數是編程語言流行度的指標,該榜單每月更新一次,指數基於全球技術工程師、課程和第三方供應商的數量。包括流行的搜索引擎,如谷歌、必應、雅虎、維基百科、亞馬遜、YouTube 和百度都用於指數計算。 )去了解語言的流行程度,流行程度決定市場需求,以此來參考自己想要了解並學習的語言,在此附上2019年11月語言排名。

2. 主流編程語言主要應用場景

  • Java

    1. 企業級應用開發: 大到全國聯網的系統,小到中小企業的應用解決方案,Java都佔有極為重要的地位 。
    2. web後端開發: JSP+Servlet+JavaBean 是一種比較流行的開發模式。
    3. 移動領域:手機遊戲。
    4. Android App開發: android 開發只用到了JAVA的語法和JAVA SE的一小部分API。
  • C

    C語言是一門基礎語言,是其他一些語言的基礎,例如MATLAB,Object-C,Lua等.同時也是學習來比較難的語言,達到精通的程度沒有3-10年左右很難,C語言沒有比較完善的開發框架,是面向過程的一門語言,講究算法跟邏輯。

    1. 科研
    2. 服務器: 網絡核心設備,如路由器、交換機、防火牆。
    3. 操作系統:類unix系統(Linux/freebsd)
    4. 嵌入式開發: 在一個特定的硬件環境上開發與構建特定的可編程軟件系統的綜合技術。
    5. 自動化控制
  • Python

    1. 圖形處理
    2. 數學處理
    3. 文本處理
    4. 數據庫編程
    5. 網絡編程
    6. 多媒體應用
    7. pymo引擎: 運行於Symbian S60V3,Symbian S60V5,Symbian 3,Android,Windows,Linux,Mac Os,Maemo,MeeGo系統上的AVG遊戲引擎。
    8. 黑客編程
    9. 網絡安全
  • C++

    1. 遊戲開發
    2. 科學計算
    3. 網絡軟件
    4. 操作系統
    5. 設備驅動程序
    6. 移動設備
    7. 嵌入式開發
    8. 科研
    9. 編譯器
  • C#

    1. web後端開發
    2. 桌面軟件開發
    3. 人工智能
    4. 遊戲開發
  • JavaScript
    唯一能用於前後端開發的語言web前端開發
    1. web前端開發
    2. node web後端開發
    3. 操作系統
    4. 後台
    5. 桌面軟件開發
    6. 混合App
    7. 小程序
  • PHP

    1. web後端開發
    2. 桌面軟件開發
    3. 命令行腳本
  • SQL

    1. 操作數據庫
  • Swift

    1. 蘋果生態系統應用開發
  • Ruby

    1. web開發
  • R

    數據科學闖天下,左手Python右手R

    1. 機器學習
    2. 數據分析
    3. 科學計算
  • Go

    1. web後端開發
    2. 高性能服務器應用

3. 主流編程語言學習路徑(將持續更新,僅供參考)

  • JavaScript

4. 主流編程語言入門學習書籍推薦

語言 書籍
C 《嗨翻C語言》
C++ 《C++權威教程》
Java 《Java輕鬆學》
Python 《Python編程從入門到實戰》
JavaScript 《JavaScript入門經典》
PHP 《PHP編程實戰》
SQL 《SQL基礎教程》
Swift 《Swift編程權威指南》
Ruby 《Ruby從入門到精通》
R 《R語言實戰》
Go 《Go語言聖經》

5. 編程學習網站推薦

網站 網址
菜鳥教程
W3School
實驗樓
猿學
慕課網
SegmentFault
博客園
GitHub
掘金
學習數據科學
易百教程
看雲

三、總結

通篇寫完,感覺自己也重新學到了很多,學習就是一個反覆複習的過程,每次學習都能帶給自己不一樣的收穫。希望以上內容可以給初入計算機相關專業的萌新帶來一些幫助,後面我會不斷更新和優化本文,請大家持續關注。

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

【其他文章推薦】

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

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

分類
發燒車訊

2016上海國際汽車新能源及智慧技術展覽會

創新技術 智能演繹 展會日期:2016年6月28日-30日 展會地點:上海新國際博覽中心   觀眾數量:30,000人次(預計) 展覽週期:二年一屆,2016年首屆 同期展會:上海國際汽車零部件及相關服務展覽會   主辦單位: 中國國際貿易促進委員會上海市分會 中國國際貿易促進委員會汽車行業分會 中國汽車工程學會 上海市國際展覽有限公司   展位價格: 室內光地:650元/平方米(36平米起租) 標準展位:900元/平方米(9平米起租)   參展範圍: -節能汽車、純電動車,混合動力車,燃料電池車,輕型電動車,天然氣(液化氣)車,醇類及其他代用燃料車和節能汽車。 -電池、電機、電控等核心零部件和先進技術應用;輕量化材料、整車優化設計及混合動力等節能技術產品; 自動駕駛,汽車共用,模組化(集成化)構造,汽車車載資訊系統及智慧化設備;汽車相關後市場服務及產品。 -充電樁、充電機、配電櫃、充換電池及電池管理系統、停車場充電設施、智慧監控、充電站供電解決方案、充電站-智慧電網解決方案等。 -檢測,維修,監控,實驗,安全防護裝備及媒體等.   展會諮詢: 北京盛大超越國際展覽有限公司 連絡人:岳巍 先生 電話(微信):135 5286 5285 郵箱:     附:其他推薦展會   2016中國國際汽車新能源及技術應用展覽會 節能與新能源汽車產業發展規劃成果展覽會   展會時間:2016年10月13-16日 展會地點:北京•國家會議中心 展覽規模:30,000平方米(2015年) 觀眾數量:60,000人次(2015年) 展覽週期:每年一屆,2013年首屆 詳情請點擊  

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

【其他文章推薦】

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

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

分類
發燒車訊

6. SOFAJRaft源碼分析— 透過RheaKV看線性一致性讀

開篇

其實這篇文章我本來想在講完選舉的時候就開始講線性一致性讀的,但是感覺直接講沒頭沒尾的看起來比比較困難,所以就有了RheaKV的系列,這是RheaKV,終於可以講一下SOFAJRaft的線性一致性讀是怎麼做到了的。所謂線性一致性,一個簡單的例子是在 T1 的時間寫入一個值,那麼在 T1 之後讀一定能讀到這個值,不可能讀到 T1 之前的值。

其中部分內容參考SOFAJRaft文檔:

RheaKV讀取數據

RheaKV的讀取數據的入口是DefaultRheaKVStore的bGet。

DefaultRheaKVStore#bGet

public byte[] bGet(final String key) {
    return FutureHelper.get(get(key), this.futureTimeoutMillis);
}

bGet方法中會一直調用到DefaultRheaKVStore的一個get方法中:
DefaultRheaKVStore#get

private CompletableFuture<byte[]> get(final byte[] key, final boolean readOnlySafe,
                                      final CompletableFuture<byte[]> future, final boolean tryBatching) {
    //校驗started狀態
    checkState();
    Requires.requireNonNull(key, "key");
    if (tryBatching) {
        final GetBatching getBatching = readOnlySafe ? this.getBatchingOnlySafe : this.getBatching;
        if (getBatching != null && getBatching.apply(key, future)) {
            return future;
        }
    }
    internalGet(key, readOnlySafe, future, this.failoverRetries, null, this.onlyLeaderRead);
    return future;
}

get方法會根據傳入的參數來判斷是否採用批處理的方式來讀取數據,readOnlySafe表示是否開啟線程一致性讀,由於我們調用的是get方法,所以readOnlySafe和tryBatching都會返回true。
所以這裡會調用getBatchingOnlySafe的apply方法,將key和future傳入。
getBatchingOnlySafe是在我們初始化DefaultRheaKVStore的時候初始化的:
DefaultRheaKVStore#init

.....
this.getBatchingOnlySafe = new GetBatching(KeyEvent::new, "get_batching_only_safe",
        new GetBatchingHandler("get_only_safe", true));
.....

在初始化getBatchingOnlySafe的時候傳入的處理器是GetBatchingHandler。

然後我們回到getBatchingOnlySafe#apply中,看看這個方法做了什麼:

public boolean apply(final byte[] message, final CompletableFuture<byte[]> future) {
    //GetBatchingHandler
    return this.ringBuffer.tryPublishEvent((event, sequence) -> {
        event.reset();
        event.key = message;
        event.future = future;
    });
}

apply方法會向Disruptor發送一個事件進行異步處理,並把我們的key封裝到event的key中。getBatchingOnlySafe的處理器是GetBatchingHandler。

批量獲取數據

GetBatchingHandler#onEvent

public void onEvent(final KeyEvent event, final long sequence, final boolean endOfBatch) throws Exception {
    this.events.add(event);
    this.cachedBytes += event.key.length;
    final int size = this.events.size();
    //校驗一下數據量,沒有達到MaxReadBytes並且不是最後一個event,那麼直接返回
    if (!endOfBatch && size < batchingOpts.getBatchSize() && this.cachedBytes < batchingOpts.getMaxReadBytes()) {
        return;
    }

    if (size == 1) {
        reset();
        try {
            //如果只是一個get請求,那麼不需要進行批量處理
            get(event.key, this.readOnlySafe, event.future, false);
        } catch (final Throwable t) {
            exceptionally(t, event.future);
        }
    } else {
        //初始化一個剛剛好大小的集合
        final List<byte[]> keys = Lists.newArrayListWithCapacity(size);
        final CompletableFuture<byte[]>[] futures = new CompletableFuture[size];
        for (int i = 0; i < size; i++) {
            final KeyEvent e = this.events.get(i);
            keys.add(e.key);
            futures[i] = e.future;
        }
        //遍歷完events數據到entries之後,重置
        reset();
        try {
            multiGet(keys, this.readOnlySafe).whenComplete((result, throwable) -> {
                //異步回調處理數據
                if (throwable == null) {
                    for (int i = 0; i < futures.length; i++) {
                        final ByteArray realKey = ByteArray.wrap(keys.get(i));
                        futures[i].complete(result.get(realKey));
                    }
                    return;
                }
                exceptionally(throwable, futures);
            });
        } catch (final Throwable t) {
            exceptionally(t, futures);
        }
    }
}
}

onEvent方法首先會校驗一下當前的event數量有沒有達到閾值以及當前的event是不是Disruptor中最後一個event;然後會根據不同的events集合中的數量來走不同的實現,這裏做了一個優化,如果是只有一條數據那麼不會走批處理;最後將所有的key放入到keys集合中並調用multiGet進行批處理。

multiGet方法會調用internalMultiGet返回一個Future,從而實現異步的返回結果。
DefaultRheaKVStore#internalMultiGet

private FutureGroup<Map<ByteArray, byte[]>> internalMultiGet(final List<byte[]> keys, final boolean readOnlySafe,
                                                             final int retriesLeft, final Throwable lastCause) {
    //因為不同的key是存放在不同的region中的,所以一個region會對應多個key,封裝到map中
    final Map<Region, List<byte[]>> regionMap = this.pdClient
            .findRegionsByKeys(keys, ApiExceptionHelper.isInvalidEpoch(lastCause));
    //返回值
    final List<CompletableFuture<Map<ByteArray, byte[]>>> futures =
            Lists.newArrayListWithCapacity(regionMap.size());
    //lastCause傳入為null
    final Errors lastError = lastCause == null ? null : Errors.forException(lastCause);

    for (final Map.Entry<Region, List<byte[]>> entry : regionMap.entrySet()) {
        final Region region = entry.getKey();
        final List<byte[]> subKeys = entry.getValue();
        //重試次數減1,設置一個重試函數
        final RetryCallable<Map<ByteArray, byte[]>> retryCallable = retryCause -> internalMultiGet(subKeys,
                readOnlySafe, retriesLeft - 1, retryCause);
        final MapFailoverFuture<ByteArray, byte[]> future = new MapFailoverFuture<>(retriesLeft, retryCallable);
        //發送MultiGetRequest請求,獲取數據
        internalRegionMultiGet(region, subKeys, readOnlySafe, future, retriesLeft, lastError, this.onlyLeaderRead);
        futures.add(future);
    }
    return new FutureGroup<>(futures);
}

internalMultiGet里會根據key去組裝region,不同的key會對應不同的region,數據時存在region中的,所以要從不同的region中獲取數據,region和key是一對多的關係所以這裡會封裝成一個map。然後會遍歷regionMap,每個region所對應的數據作為一個批次調用到internalRegionMultiGet方法中,根據不同的情況獲取數據。

DefaultRheaKVStore#internalRegionMultiGet

private void internalRegionMultiGet(final Region region, final List<byte[]> subKeys, final boolean readOnlySafe,
                                    final CompletableFuture<Map<ByteArray, byte[]>> future, final int retriesLeft,
                                    final Errors lastCause, final boolean requireLeader) {
    //因為當前的是client,所以這裡會是null
    final RegionEngine regionEngine = getRegionEngine(region.getId(), requireLeader);
    // require leader on retry
    //設置重試函數
    final RetryRunner retryRunner = retryCause -> internalRegionMultiGet(region, subKeys, readOnlySafe, future,
            retriesLeft - 1, retryCause, true);
    final FailoverClosure<Map<ByteArray, byte[]>> closure = new FailoverClosureImpl<>(future,
            false, retriesLeft, retryRunner);
    if (regionEngine != null) {
        if (ensureOnValidEpoch(region, regionEngine, closure)) {
            //如果不是null,那麼會獲取rawKVStore,並從中獲取數據
            final RawKVStore rawKVStore = getRawKVStore(regionEngine);
            if (this.kvDispatcher == null) {
                rawKVStore.multiGet(subKeys, readOnlySafe, closure);
            } else {
                //如果是kvDispatcher不為空,那麼放入到kvDispatcher中異步執行
                this.kvDispatcher.execute(() -> rawKVStore.multiGet(subKeys, readOnlySafe, closure));
            }
        }
    } else {
        final MultiGetRequest request = new MultiGetRequest();
        request.setKeys(subKeys);
        request.setReadOnlySafe(readOnlySafe);
        request.setRegionId(region.getId());
        request.setRegionEpoch(region.getRegionEpoch());
        //調用rpc請求
        this.rheaKVRpcService.callAsyncWithRpc(request, closure, lastCause, requireLeader);
    }
}

因為我們這裡是client端調用internalRegionMultiGet方法的,所以是沒有設置regionEngine的,那麼會直接向server的當前region所對應的leader節點發送一個MultiGetRequest請求。

因為上面的這些方法基本上和put是一致的,我們已經在講過了,所以這裏不重複的講了。

server端處理MultiGetRequest請求

MultiGetRequest請求會被KVCommandProcessor所處理,KVCommandProcessor里會根據請求的magic方法返回值來判斷是用什麼方式來進行處理。我們這裡會調用到DefaultRegionKVService的handleMultiGetRequest方法中處理請求。

public void handleMultiGetRequest(final MultiGetRequest request,
                                  final RequestProcessClosure<BaseRequest, BaseResponse<?>> closure) {
    final MultiGetResponse response = new MultiGetResponse();
    response.setRegionId(getRegionId());
    response.setRegionEpoch(getRegionEpoch());
    try {
        KVParameterRequires.requireSameEpoch(request, getRegionEpoch());
        final List<byte[]> keys = KVParameterRequires.requireNonEmpty(request.getKeys(), "multiGet.keys");
        //調用MetricsRawKVStore的multiGet方法
        this.rawKVStore.multiGet(keys, request.isReadOnlySafe(), new BaseKVStoreClosure() {

            @SuppressWarnings("unchecked")
            @Override
            public void run(final Status status) {
                if (status.isOk()) {
                    response.setValue((Map<ByteArray, byte[]>) getData());
                } else {
                    setFailure(request, response, status, getError());
                }
                closure.sendResponse(response);
            }
        });
    } catch (final Throwable t) {
        LOG.error("Failed to handle: {}, {}.", request, StackTraceUtil.stackTrace(t));
        response.setError(Errors.forException(t));
        closure.sendResponse(response);
    }
}

handleMultiGetRequest方法會調用MetricsRawKVStore的multiGet方法來批量獲取數據。

MetricsRawKVStore#multiGet

public void multiGet(final List<byte[]> keys, final boolean readOnlySafe, final KVStoreClosure closure) {
    //實例化MetricsKVClosureAdapter對象
    final KVStoreClosure c = metricsAdapter(closure, MULTI_GET, keys.size(), 0);
    //調用RaftRawKVStore的multiGet方法
    this.rawKVStore.multiGet(keys, readOnlySafe, c);
}

multiGet方法會傳入一個MetricsKVClosureAdapter實例,通過這個實例實現異步回調response。然後調用RaftRawKVStore的multiGet方法。

RaftRawKVStore#multiGet

public void multiGet(final List<byte[]> keys, final boolean readOnlySafe, final KVStoreClosure closure) {
    if (!readOnlySafe) {
        this.kvStore.multiGet(keys, false, closure);
        return;
    }
    // KV 存儲實現線性一致讀
    // 調用 readIndex 方法,等待回調執行
    this.node.readIndex(BytesUtil.EMPTY_BYTES, new ReadIndexClosure() {

        @Override
        public void run(final Status status, final long index, final byte[] reqCtx) {
            //如果狀態返回成功,
            if (status.isOk()) {
                RaftRawKVStore.this.kvStore.multiGet(keys, true, closure);
                return;
            }
            //readIndex 讀取失敗嘗試應用鍵值讀操作申請任務於 Leader 節點的狀態機 KVStoreStateMachine
            RaftRawKVStore.this.readIndexExecutor.execute(() -> {
                if (isLeader()) {
                    LOG.warn("Fail to [multiGet] with 'ReadIndex': {}, try to applying to the state machine.",
                            status);
                    // If 'read index' read fails, try to applying to the state machine at the leader node
                    applyOperation(KVOperation.createMultiGet(keys), closure);
                } else {
                    LOG.warn("Fail to [multiGet] with 'ReadIndex': {}.", status);
                    // Client will retry to leader node
                    new KVClosureAdapter(closure, null).run(status);
                }
            });
        }
    });
}

multiGet調用node的readIndex方法進行一致性讀操作,並設置回調,如果返回成功那麼就直接調用RocksRawKVStore讀取數據,如果返回不是成功那麼申請任務於 Leader 節點的狀態機 KVStoreStateMachine。

線性一致性讀readIndex

所謂線性一致讀,一個簡單的例子是在 t1 的時刻我們寫入了一個值,那麼在 t1 之後,我們一定能讀到這個值,不可能讀到 t1 之前的舊值(想想 Java 中的 volatile 關鍵字,即線性一致讀就是在分佈式系統中實現 Java volatile 語義)。簡而言之是需要在分佈式環境中實現 Java volatile 語義效果,即當 Client 向集群發起寫操作的請求並且獲得成功響應之後,該寫操作的結果要對所有後來的讀請求可見。和 volatile 的區別在於 volatile 是實現線程之間的可見,而 SOFAJRaft 需要實現 Server 之間的可見。

SOFAJRaft提供的線性一致讀是基於 Raft 協議的 ReadIndex 實現用 ;Node#readIndex(byte [] requestContext, ReadIndexClosure done) 發起線性一致讀請求,當安全讀取時傳入的 Closure 將被調用,正常情況從狀態機中讀取數據返回給客戶端。

Node#readIndex

public void readIndex(final byte[] requestContext, final ReadIndexClosure done) {
    if (this.shutdownLatch != null) {
        //異步執行回調
        Utils.runClosureInThread(done, new Status(RaftError.ENODESHUTDOWN, "Node is shutting down."));
        throw new IllegalStateException("Node is shutting down");
    }
    Requires.requireNonNull(done, "Null closure");
    //EMPTY_BYTES
    this.readOnlyService.addRequest(requestContext, done);
}

readIndex會調用ReadOnlyServiceImpl#addRequest將requestContext和回調方法done傳入,requestContext傳入的是BytesUtil.EMPTY_BYTES
接着往下看

ReadOnlyServiceImpl#addRequest

public void addRequest(final byte[] reqCtx, final ReadIndexClosure closure) {
    if (this.shutdownLatch != null) {
        Utils.runClosureInThread(closure, new Status(RaftError.EHOSTDOWN, "Was stopped"));
        throw new IllegalStateException("Service already shutdown.");
    }
    try {
        EventTranslator<ReadIndexEvent> translator = (event, sequence) -> {
            event.done = closure;
            //EMPTY_BYTES
            event.requestContext = new Bytes(reqCtx);
            event.startTime = Utils.monotonicMs();
        };
        int retryTimes = 0;
        while (true) {
            //ReadIndexEventHandler
            if (this.readIndexQueue.tryPublishEvent(translator)) {
                break;
            } else {
                retryTimes++;
                if (retryTimes > MAX_ADD_REQUEST_RETRY_TIMES) {
                    Utils.runClosureInThread(closure,
                        new Status(RaftError.EBUSY, "Node is busy, has too many read-only requests."));
                    this.nodeMetrics.recordTimes("read-index-overload-times", 1);
                    LOG.warn("Node {} ReadOnlyServiceImpl readIndexQueue is overload.", this.node.getNodeId());
                    return;
                }
                ThreadHelper.onSpinWait();
            }
        }
    } catch (final Exception e) {
        Utils.runClosureInThread(closure, new Status(RaftError.EPERM, "Node is down."));
    }
}

addRequest方法里會將傳入的reqCtx和closure封裝成一個時間,傳入到readIndexQueue隊列中,事件發布成功後會交由ReadIndexEventHandler處理器處理,發布失敗會進行重試,最多重試3次。

ReadIndexEventHandler

private class ReadIndexEventHandler implements EventHandler<ReadIndexEvent> {
    // task list for batch
    private final List<ReadIndexEvent> events = new ArrayList<>(
                                                  ReadOnlyServiceImpl.this.raftOptions.getApplyBatch());

    @Override
    public void onEvent(final ReadIndexEvent newEvent, final long sequence, final boolean endOfBatch)
                                                                                                     throws Exception {
        if (newEvent.shutdownLatch != null) {
            executeReadIndexEvents(this.events);
            this.events.clear();
            newEvent.shutdownLatch.countDown();
            return;
        }

        this.events.add(newEvent);
        //批量執行
        if (this.events.size() >= ReadOnlyServiceImpl.this.raftOptions.getApplyBatch() || endOfBatch) {
            executeReadIndexEvents(this.events);
            this.events.clear();
        }
    }
}

ReadIndexEventHandler是ReadOnlyServiceImpl裏面的內部類,裏面有一個全局的events集合用來做事件的批處理,如果當前的event已經達到了32個或是整個Disruptor隊列里最後一個那麼會調用ReadOnlyServiceImpl的executeReadIndexEvents方法進行事件的批處理。

ReadOnlyServiceImpl#executeReadIndexEvents

private void executeReadIndexEvents(final List<ReadIndexEvent> events) {
    if (events.isEmpty()) {
        return;
    }
    //初始化ReadIndexRequest
    final ReadIndexRequest.Builder rb = ReadIndexRequest.newBuilder() //
        .setGroupId(this.node.getGroupId()) //
        .setServerId(this.node.getServerId().toString());

    final List<ReadIndexState> states = new ArrayList<>(events.size());

    for (final ReadIndexEvent event : events) {
        rb.addEntries(ZeroByteStringHelper.wrap(event.requestContext.get()));
        states.add(new ReadIndexState(event.requestContext, event.done, event.startTime));
    }
    final ReadIndexRequest request = rb.build();

    this.node.handleReadIndexRequest(request, new ReadIndexResponseClosure(states, request));
}

executeReadIndexEvents封裝好ReadIndexRequest請求和將ReadIndexState集合封裝到ReadIndexResponseClosure中,為後續的操作做裝備

NodeImpl#handleReadIndexRequest

public void handleReadIndexRequest(final ReadIndexRequest request, final RpcResponseClosure<ReadIndexResponse> done) {
    final long startMs = Utils.monotonicMs();
    this.readLock.lock();
    try {
        switch (this.state) {
            case STATE_LEADER:
                readLeader(request, ReadIndexResponse.newBuilder(), done);
                break;
            case STATE_FOLLOWER:
                readFollower(request, done);
                break;
            case STATE_TRANSFERRING:
                done.run(new Status(RaftError.EBUSY, "Is transferring leadership."));
                break;
            default:
                done.run(new Status(RaftError.EPERM, "Invalid state for readIndex: %s.", this.state));
                break;
        }
    } finally {
        this.readLock.unlock();
        this.metrics.recordLatency("handle-read-index", Utils.monotonicMs() - startMs);
        this.metrics.recordSize("handle-read-index-entries", request.getEntriesCount());
    }
}

因為線性一致讀在任何集群內的節點發起,並不需要強制要求放到 Leader 節點上,允許在 Follower 節點執行,因此大大降低 Leader 的讀取壓力。
當在Follower節點執行一致性讀的時候實際上Follower 節點調用 RpcService#readIndex(leaderId.getEndpoint(), newRequest, -1, closure) 方法向 Leader 發送 ReadIndex 請求,交由Leader節點實現一致性讀。所以我這裏主要介紹Leader的一致性讀。

繼續往下走調用NodeImpl的readLeader方法
NodeImpl#readLeader

private void readLeader(final ReadIndexRequest request, final ReadIndexResponse.Builder respBuilder,
                        final RpcResponseClosure<ReadIndexResponse> closure) {
    //1. 獲取集群節點中多數選票數是多少
    final int quorum = getQuorum();
    if (quorum <= 1) {
        // Only one peer, fast path.
        //如果集群中只有一個節點,那麼直接調用回調函數,返回成功
        respBuilder.setSuccess(true) //
                .setIndex(this.ballotBox.getLastCommittedIndex());
        closure.setResponse(respBuilder.build());
        closure.run(Status.OK());
        return;
    }

    final long lastCommittedIndex = this.ballotBox.getLastCommittedIndex();
    //2. 任期必須相等
    //日誌管理器 LogManager 基於投票箱 BallotBox 的 lastCommittedIndex 獲取任期檢查是否等於當前任期
    // 如果不等於當前任期表示此 Leader 節點未在其任期內提交任何日誌,需要拒絕只讀請求;
    if (this.logManager.getTerm(lastCommittedIndex) != this.currTerm) {
        // Reject read only request when this leader has not committed any log entry at its term
        closure
                .run(new Status(
                        RaftError.EAGAIN,
                        "ReadIndex request rejected because leader has not committed any log entry at its term, " +
                         "logIndex=%d, currTerm=%d.",
                        lastCommittedIndex, this.currTerm));
        return;
    }
    respBuilder.setIndex(lastCommittedIndex);

    if (request.getPeerId() != null) {
        // request from follower, check if the follower is in current conf.
        final PeerId peer = new PeerId();
        peer.parse(request.getServerId());
        //3. 來自 Follower 的請求需要檢查 Follower 是否在當前配置
        if (!this.conf.contains(peer)) {
            closure
                    .run(new Status(RaftError.EPERM, "Peer %s is not in current configuration: {}.", peer,
                     this.conf));
            return;
        }
    }

    ReadOnlyOption readOnlyOpt = this.raftOptions.getReadOnlyOptions();
    //4. 如果使用的是ReadOnlyLeaseBased,確認leader是否是在在租約有效時間內
    if (readOnlyOpt == ReadOnlyOption.ReadOnlyLeaseBased && !isLeaderLeaseValid()) {
        // If leader lease timeout, we must change option to ReadOnlySafe
        readOnlyOpt = ReadOnlyOption.ReadOnlySafe;
    }

    switch (readOnlyOpt) {
        //5
        case ReadOnlySafe:
            final List<PeerId> peers = this.conf.getConf().getPeers();
            Requires.requireTrue(peers != null && !peers.isEmpty(), "Empty peers");
            //設置心跳的響應回調函數
            final ReadIndexHeartbeatResponseClosure heartbeatDone = new ReadIndexHeartbeatResponseClosure(closure,
                    respBuilder, quorum, peers.size());
            // Send heartbeat requests to followers
            //向 Followers 節點發起一輪 Heartbeat,如果半數以上節點返回對應的
            // Heartbeat Response,那麼 Leader就能夠確定現在自己仍然是 Leader
            for (final PeerId peer : peers) {
                if (peer.equals(this.serverId)) {
                    continue;
                }
                this.replicatorGroup.sendHeartbeat(peer, heartbeatDone);
            }
            break;
        //6. 因為在租約期內不會發生選舉,確保 Leader 不會變化
        //所以直接返回回調結果
        case ReadOnlyLeaseBased:
            // Responses to followers and local node.
            respBuilder.setSuccess(true);
            closure.setResponse(respBuilder.build());
            closure.run(Status.OK());
            break;
    }
}
  1. 獲取集群節點中多數選票數是多少,即集群節點的1/2+1,如果當前的集群里只有一個節點,那麼直接返回成功,並調用回調方法
  2. 校驗 Raft 集群節點數量以及 lastCommittedIndex 所屬任期符合預期,那麼響應構造器設置其索引為投票箱 BallotBox 的 lastCommittedIndex
  3. 來自 Follower 的請求需要檢查 Follower 是否在當前配置,如果不在當前配置中直接調用回調方法設置異常
  4. 獲取 ReadIndex 請求級別 ReadOnlyOption 配置,ReadOnlyOption 參數默認值為 ReadOnlySafe。如果設置的是ReadOnlyLeaseBased,那麼會調用isLeaderLeaseValid檢查leader是否是在在租約有效時間內
  5. 配置為ReadOnlySafe 調用 Replicator#sendHeartbeat(rid, closure) 方法向 Followers 節點發送 Heartbeat 心跳請求,發送心跳成功執行 ReadIndexHeartbeatResponseClosure 心跳響應回調;ReadIndex 心跳響應回調檢查是否超過半數節點包括 Leader 節點自身投票贊成,半數以上節點返回客戶端Heartbeat 請求成功響應,即 applyIndex 超過 ReadIndex 說明已經同步到 ReadIndex 對應的 Log 能夠提供 Linearizable Read
  6. 配置為ReadOnlyLeaseBased,因為Leader 租約有效期間認為當前 Leader 是 Raft Group 內的唯一有效 Leader,所以忽略 ReadIndex 發送 Heartbeat 確認身份步驟,直接返回 Follower 節點和本地節點 Read 請求成功響應。Leader 節點繼續等待狀態機執行,直到 applyIndex 超過 ReadIndex 安全提供 Linearizable Read

無論是ReadOnlySafe還是ReadOnlyLeaseBased,最後發送成功響應都會調用ReadIndexResponseClosure的run方法。

ReadIndexResponseClosure#run

public void run(final Status status) {
    //fail
    //傳入的狀態不是ok,響應失敗
    if (!status.isOk()) {
        notifyFail(status);
        return;
    }
    final ReadIndexResponse readIndexResponse = getResponse();
    //Fail
    //response沒有響應成功,響應失敗
    if (!readIndexResponse.getSuccess()) {
        notifyFail(new Status(-1, "Fail to run ReadIndex task, maybe the leader stepped down."));
        return;
    }
    // Success
    //一致性讀成功
    final ReadIndexStatus readIndexStatus = new ReadIndexStatus(this.states, this.request,
        readIndexResponse.getIndex());
    for (final ReadIndexState state : this.states) {
        // Records current commit log index.
        //設置當前提交的index
        state.setIndex(readIndexResponse.getIndex());
    }

    boolean doUnlock = true;
    ReadOnlyServiceImpl.this.lock.lock();
    try {
        //校驗applyIndex 是否超過 ReadIndex
        if (readIndexStatus.isApplied(ReadOnlyServiceImpl.this.fsmCaller.getLastAppliedIndex())) {
            // Already applied, notify readIndex request.
            ReadOnlyServiceImpl.this.lock.unlock();
            doUnlock = false;
            //已經同步到 ReadIndex 對應的 Log 能夠提供 Linearizable Read
            notifySuccess(readIndexStatus);
        } else {
            // Not applied, add it to pending-notify cache.
            ReadOnlyServiceImpl.this.pendingNotifyStatus
                .computeIfAbsent(readIndexStatus.getIndex(), k -> new ArrayList<>(10)) //
                .add(readIndexStatus);
        }
    } finally {
        if (doUnlock) {
            ReadOnlyServiceImpl.this.lock.unlock();
        }
    }
}

Run方法首先會校驗一下是否需要響應失敗,如果響應成功,那麼會將所有封裝的ReadIndexState更新一下index,然後校驗一下applyIndex 是否超過 ReadIndex,超過了ReadIndex代表所有已經複製到多數派上的 Log(可視為寫操作)被視為安全的 Log,該 Log 所體現的數據就能對客戶端 Client 可見。

ReadOnlyServiceImpl#notifySuccess

private void notifySuccess(final ReadIndexStatus status) {
    final long nowMs = Utils.monotonicMs();
    final List<ReadIndexState> states = status.getStates();
    final int taskCount = states.size();
    for (int i = 0; i < taskCount; i++) {
        final ReadIndexState task = states.get(i);
        final ReadIndexClosure done = task.getDone(); // stack copy
        if (done != null) {
            this.nodeMetrics.recordLatency("read-index", nowMs - task.getStartTimeMs());
            done.setResult(task.getIndex(), task.getRequestContext().get());
            done.run(Status.OK());
        }
    }
}

如果是響應成功,那麼會調用notifySuccess方法,會將status里封裝的ReadIndexState集合遍歷一遍,調用當中的run方法。

這個run方法會調用到我們在multiGet中設置的run方法中
RaftRawKVStore#multiGet

public void multiGet(final List<byte[]> keys, final boolean readOnlySafe, final KVStoreClosure closure) {
    if (!readOnlySafe) {
        this.kvStore.multiGet(keys, false, closure);
        return;
    }
    // KV 存儲實現線性一致讀
    // 調用 readIndex 方法,等待回調執行
    this.node.readIndex(BytesUtil.EMPTY_BYTES, new ReadIndexClosure() {

        @Override
        public void run(final Status status, final long index, final byte[] reqCtx) {
            //如果狀態返回成功,
            if (status.isOk()) {
                RaftRawKVStore.this.kvStore.multiGet(keys, true, closure);
                return;
            }
            //readIndex 讀取失敗嘗試應用鍵值讀操作申請任務於 Leader 節點的狀態機 KVStoreStateMachine
            RaftRawKVStore.this.readIndexExecutor.execute(() -> {
                if (isLeader()) {
                    LOG.warn("Fail to [multiGet] with 'ReadIndex': {}, try to applying to the state machine.",
                            status);
                    // If 'read index' read fails, try to applying to the state machine at the leader node
                    applyOperation(KVOperation.createMultiGet(keys), closure);
                } else {
                    LOG.warn("Fail to [multiGet] with 'ReadIndex': {}.", status);
                    // Client will retry to leader node
                    new KVClosureAdapter(closure, null).run(status);
                }
            });
        }
    });

這個run方法會調用RaftRawKVStore的multiGet從RocksDB中直接獲取數據。

總結

我們這篇文章從RheaKVStore的客戶端get方法一直講到,RheaKVStore服務端使用JRaft實現線性一致性讀,並講解了線性一致性讀是怎麼實現的,通過這個例子大家應該對線性一致性讀有了一個相對不錯的理解了。

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

【其他文章推薦】

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

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

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

分類
發燒車訊

Webpack 4 Tree Shaking 終極優化指南

幾個月前,我的任務是將我們組的 Vue.js 項目構建配置升級到 Webpack 4。我們的主要目標之一是利用 tree-shaking 的優勢,即 Webpack 去掉了實際上並沒有使用的代碼來減少包的大小。現在,tree-shaking 的好處將根據你的代碼庫而有所不同。由於我們的幾個架構決策,我們從公司內部的其他庫中提取了大量代碼,而我們只使用了其中的一小部分。

我寫這篇文章是因為恰當地優化 Webpack 並不簡單。一開始我以為這是一種簡單的魔法,但後來我花了一個月的時間在網上搜索我遇到的一系列問題的答案。我希望通過這篇文章,其他人會更容易地處理類似問題。

先說好處

在討論技術細節之前,讓我先總結一下好處。不同的應用程序將看到不同程度的好處。主要的決定因素是應用程序中死代碼的數量。如果你沒有多少死代碼,那麼你就看不到 tree-shaking 的多少好處。我們項目里有很多死代碼。

在我們部門,最大的問題是共享庫的數量。從簡單的自定義組件庫,到企業標準組件庫,再到莫名其妙地塞到一個庫中的大量代碼。很多都是技術債務,但一個大問題是我們所有的應用程序都在導入所有這些庫,而實際上每個應用程序都只需要其中的一小部分

總的來說,一旦實現了 tree-shaking,我們的應用程序就會根據應用程序的不同,縮減率從25%到75%。平均縮減率為52%,主要是由這些龐大的共享庫驅動的,它們是小型應用程序中的主要代碼。

同樣,具體情況會有所不同,但是如果你覺得你打的包中可能有很多不需要的代碼,這就是如何消除它們的方法。

沒有示例代碼倉庫

對不住了各位老鐵,我做的項目是公司的財產,所以我不能分享代碼到 GitHub 倉庫了。但是,我將在本文中提供簡化的代碼示例來說明我的觀點。

因此,廢話少說,讓我們來看看如何編寫可實現 tree-shaking 的最佳 webpack 4 配置。

什麼是死代碼

很簡單:就是 Webpack 沒看到你使用的代碼。Webpack 跟蹤整個應用程序的 import/export 語句,因此,如果它看到導入的東西最終沒有被使用,它會認為那是“死代碼”,並會對其進行 tree-shaking 。

死代碼並不總是那麼明確的。下面是一些死代碼和“活”代碼的例子,希望能讓你更明白。請記住,在某些情況下,Webpack 會將某些東西視為死代碼,儘管它實際上並不是。請參閱《副作用》一節,了解如何處理。

// 導入並賦值給 JavaScript 對象,然後在下面的代碼中被用到
// 這會被看作“活”代碼,不會做 tree-shaking
import Stuff from './stuff';
doSomething(Stuff);
// 導入並賦值給 JavaScript 對象,但在接下來的代碼里沒有用到
// 這就會被當做“死”代碼,會被 tree-shaking
import Stuff from './stuff';
doSomething();
// 導入但沒有賦值給 JavaScript 對象,也沒有在代碼里用到
// 這會被當做“死”代碼,會被 tree-shaking
import './stuff';
doSomething();
// 導入整個庫,但是沒有賦值給 JavaScript 對象,也沒有在代碼里用到
// 非常奇怪,這竟然被當做“活”代碼,因為 Webpack 對庫的導入和本地代碼導入的處理方式不同。
import 'my-lib';
doSomething();

用支持 tree-shaking 的方式寫 import

在編寫支持 tree-shaking 的代碼時,導入方式非常重要。你應該避免將整個庫導入到單個 JavaScript 對象中。當你這樣做時,你是在告訴 Webpack 你需要整個庫, Webpack 就不會搖它。

以流行的庫 Lodash 為例。一次導入整個庫是一個很大的錯誤,但是導入單個的模塊要好得多。當然,Lodash 還需要其他的步驟來做 tree-shaking,但這是個很好的起點。

// 全部導入 (不支持 tree-shaking)
import _ from 'lodash';
// 具名導入(支持 tree-shaking)
import { debounce } from 'lodash';
// 直接導入具體的模塊 (支持 tree-shaking)
import debounce from 'lodash/lib/debounce';

基本的 Webpack 配置

使用 Webpack 進行 tree-shaking 的第一步是編寫 Webpack 配置文件。你可以對你的 webpack 做很多自定義配置,但是如果你想要對代碼進行 tree-shaking,就需要以下幾項。

首先,你必須處於生產模式。Webpack 只有在壓縮代碼的時候會 tree-shaking,而這隻會發生在生產模式中。

其次,必須將優化選項 “usedExports” 設置為true。這意味着 Webpack 將識別出它認為沒有被使用的代碼,並在最初的打包步驟中給它做標記。

最後,你需要使用一個支持刪除死代碼的壓縮器。這種壓縮器將識別出 Webpack 是如何標記它認為沒有被使用的代碼,並將其剝離。TerserPlugin 支持這個功能,推薦使用。

下面是 Webpack 開啟 tree-shaking 的基本配置:

// Base Webpack Config for Tree Shaking
const config = {
 mode: 'production',
 optimization: {
  usedExports: true,
  minimizer: [
   new TerserPlugin({...})
  ]
 }
};

有什麼副作用

僅僅因為 Webpack 看不到一段正在使用的代碼,並不意味着它可以安全地進行 tree-shaking。有些模塊導入,只要被引入,就會對應用程序產生重要的影響。一個很好的例子就是全局樣式表,或者設置全局配置的JavaScript 文件。

Webpack 認為這樣的文件有“副作用”。具有副作用的文件不應該做 tree-shaking,因為這將破壞整個應用程序。Webpack 的設計者清楚地認識到不知道哪些文件有副作用的情況下打包代碼的風險,因此默認地將所有代碼視為有副作用。這可以保護你免於刪除必要的文件,但這意味着 Webpack 的默認行為實際上是不進行 tree-shaking。

幸運的是,我們可以配置我們的項目,告訴 Webpack 它是沒有副作用的,可以進行 tree-shaking。

如何告訴 Webpack 你的代碼無副作用

package.json 有一個特殊的屬性 sideEffects,就是為此而存在的。它有三個可能的值:

true 是默認值,如果不指定其他值的話。這意味着所有的文件都有副作用,也就是沒有一個文件可以 tree-shaking。

false 告訴 Webpack 沒有文件有副作用,所有文件都可以 tree-shaking。

第三個值 […] 是文件路徑數組。它告訴 webpack,除了數組中包含的文件外,你的任何文件都沒有副作用。因此,除了指定的文件之外,其他文件都可以安全地進行 tree-shaking。

每個項目都必須將 sideEffects 屬性設置為 false 或文件路徑數組。在我公司的工作中,我們的基本應用程序和我提到的所有共享庫都需要正確配置 sideEffects 標誌。

下面是 sideEffects 標誌的一些代碼示例。儘管有 JavaScript 註釋,但這是 JSON 代碼:

// 所有文件都有副作用,全都不可 tree-shaking
{
 "sideEffects": true
}
// 沒有文件有副作用,全都可以 tree-shaking
{
 "sideEffects": false
}
// 只有這些文件有副作用,所有其他文件都可以 tree-shaking,但會保留這些文件
{
 "sideEffects": [
  "./src/file1.js",
  "./src/file2.js"
 ]
}

全局 CSS 與副作用

首先,讓我們在這個上下文中定義全局 CSS。全局 CSS 是直接導入到 JavaScript 文件中的樣式表(可以是CSS、SCSS等)。它沒有被轉換成 CSS 模塊或任何類似的東西。基本上,import 語句是這樣的:

// 導入全局 CSS
import './MyStylesheet.css';

因此,如果你做了上面提到的副作用更改,那麼在運行 webpack 構建時,你將立即注意到一個棘手的問題。以上述方式導入的任何樣式表現在都將從輸出中刪除。這是因為這樣的導入被 webpack 視為死代碼,並被刪除。

幸運的是,有一個簡單的解決方案可以解決這個問題。Webpack 使用它的模塊規則系統來控制各種類型文件的加載。每種文件類型的每個規則都有自己的 sideEffects 標誌。這會覆蓋之前為匹配規則的文件設置的所有 sideEffects 標誌。

所以,為了保留全局 CSS 文件,我們只需要設置這個特殊的 sideEffects 標誌為 true,就像這樣:

// 全局 CSS 副作用規則相關的 Webpack 配置
const config = {
 module: {
  rules: [
   {
    test: /regex/,
    use: [loaders],
    sideEffects: true
   }
  ]
 } 
};

Webpack 的所有模塊規則上都有這個屬性。處理全局樣式表的規則必須用上它,包括但不限於 CSS/SCSS/LESS/等等。

什麼是模塊,模塊為什麼重要

現在我們開始進入秘境。表面上看,編譯出正確的模塊類型似乎是一個簡單的步驟,但是正如下面幾節將要解釋的,這是一個會導致許多複雜問題的領域。這是我花了很長時間才弄明白的部分。

首先,我們需要了解一下模塊。多年來,JavaScript 已經發展出了在文件之間以“模塊”的形式有效導入/導出代碼的能力。有許多不同的 JavaScript 模塊標準已經存在了多年,但是為了本文的目的,我們將重點關注兩個標準。一個是 “commonjs”,另一個是 “es2015”。下面是它們的代碼形式:

// Commonjs
const stuff = require('./stuff');
module.exports = stuff;

// es2015 
import stuff from './stuff';
export default stuff;

默認情況下,Babel 假定我們使用 es2015 模塊編寫代碼,並轉換 JavaScript 代碼以使用 commonjs 模塊。這樣做是為了與服務器端 JavaScript 庫的廣泛兼容性,這些 JavaScript 庫通常構建在 NodeJS 之上(NodeJS 只支持 commonjs 模塊)。但是,Webpack 不支持使用 commonjs 模塊來完成 tree-shaking。

現在,有一些插件(如 common-shake-plugin)聲稱可以讓 Webpack 有能力對 commonjs 模塊進行 tree-shaking,但根據我的經驗,這些插件要麼不起作用,要麼在 es2015 模塊上運行時,對 tree-shaking 的影響微乎其微。我不推薦這些插件。

因此,為了進行 tree-shaking,我們需要將代碼編譯到 es2015 模塊。

es2015 模塊 Babel 配置

據我所知,Babel 不支持將其他模塊系統編譯成 es2015 模塊。但是,如果你是前端開發人員,那麼你可能已經在使用 es2015 模塊編寫代碼了,因為這是全面推薦的方法。

因此,為了讓我們編譯的代碼使用 es2015 模塊,我們需要做的就是告訴 babel 不要管它們。為了實現這一點,我們只需將以下內容添加到我們的 babel.config.js 中(在本文中,你會看到我更喜歡JavaScript 配置而不是 JSON 配置):

// es2015 模塊的基本 Babel 配置
const config = {
 presets: [
  [
   '[@babel/preset-env](http://twitter.com/babel/preset-env)',
   {
    modules: false
   }
  ]
 ]
};

modules 設置為 false,就是告訴 babel 不要編譯模塊代碼。這會讓 Babel 保留我們現有的 es2015 import/export 語句。

划重點:所有可需要 tree-shaking 的代碼必須以這種方式編譯。因此,如果你有要導入的庫,則必須將這些庫編譯為 es2015 模塊以便進行 tree-shaking 。如果它們被編譯為 commonjs,那麼它們就不能做 tree-shaking ,並且將會被打包進你的應用程序中。許多庫支持部分導入,lodash 就是一個很好的例子,它本身是 commonjs 模塊,但是它有一個 lodash-es 版本,用的是 es2015模塊。

此外,如果你在應用程序中使用內部庫,也必須使用 es2015 模塊編譯。為了減少應用程序包的大小,必須將所有這些內部庫修改為以這種方式編譯。

不好意思, Jest 罷工了

其他測試框架情況類似,我們用的是 Jest。

不管怎麼樣,如果你走到了這一步,你會發現 Jest 測試開始失敗了。你會像我當時一樣,看到日誌里出現各種奇怪的錯誤,慌的一批。別慌,我會帶你一步一步解決。

出現這個結果的原因很簡單:NodeJS。Jest 是基於 NodeJS 開發的,而 NodeJS 不支持 es2015 模塊。為此有一些方法可以配置 Node,但是在 jest 上行不通。因此,我們卡在這裏了:Webpack 需要 es2015 進行 tree shaking,但是 Jest 無法在這些模塊上執行測試。

就是為什麼我說進入了模塊系統的“秘境”。這是整個過程中耗費我最多時間來搞清楚的部分。建議你仔細閱讀這一節和後面幾節,因為我會給出解決方案。

解決方案有兩個主要部分。第一部分針對項目本身的代碼,也就是跑測試的代碼。這部分比較容易。第二部分針對庫代碼,也就是來自其他項目,被編譯成 es2015 模塊並引入到當前項目的代碼。這部分比較複雜。

解決項目本地 Jest 代碼

針對我們的問題,babel 有一個很有用的特性:環境選項。通過配置可以運行在不同環境。在這裏,開發和生產環境我們需要 es2015 模塊,而測試環境需要 commonjs 模塊。還好,Babel 配置起來非常容易:

// 分環境配置Babel 
const config = {
 env: {
  development: {
   presets: [
    [
     '[@babel/preset-env](http://twitter.com/babel/preset-env)',
     {
      modules: false
     }
    ]
   ]
  },
  production: {
   presets: [
    [
     '[@babel/preset-env](http://twitter.com/babel/preset-env)',
     {
      modules: false
     }
    ]
   ]
  },
  test: {
   presets: [
    [
     '[@babel/preset-env](http://twitter.com/babel/preset-env)',
     {
      modules: 'commonjs'
     }
    ]
   ],
   plugins: [
    'transform-es2015-modules-commonjs' // Not sure this is required, but I had added it anyway
   ]
  }
 }
};

設置好之後,所有的項目本地代碼能夠正常編譯,Jest 測試能運行了。但是,使用 es2015 模塊的第三方庫代碼依然不能運行。

解決 Jest 中的庫代碼

庫代碼運行出錯的原因非常明顯,看一眼node_modules 目錄就明白了。這裏的庫代碼用的是 es2015 模塊語法,為了進行 tree-shaking。這些庫已經採用這種方式編譯過了,因此當 Jest 在單元測試中試圖讀取這些代碼時,就炸了。注意到沒有,我們已經讓 Babel 在測試環境中啟用 commonjs 模塊了呀,為什麼對這些庫不起作用呢?這是因為,Jest (尤其是 babel-jest) 在跑測試之前編譯代碼的時候,默認忽略任何來自node_modules 的代碼。

這實際上是件好事。如果 Jest 需要重新編譯所有庫的話,將會大大增加測試處理時間。然而,雖然我們不想讓它重新編譯所有代碼,但我們希望它重新編譯使用 es2015 模塊的庫,這樣才能在單元測試里使用。

幸好,Jest 在它的配置中為我們提供了解決方案。我想說,這部分確實讓我想了很久,並且我感覺沒必要搞得這麼複雜,但這是我能想到的唯一解決方案。

配置 Jest 重新編譯庫代碼

// 重新編譯庫代碼的 Jest 配置 
const path = require('path');
const librariesToRecompile = [
 'Library1',
 'Library2'
].join('|');
const config = {
 transformIgnorePatterns: [
  `[\\\/]node_modules[\\\/](?!(${librariesToRecompile})).*$`
 ],
 transform: {
  '^.+\.jsx?$': path.resolve(__dirname, 'transformer.js')
 }
};

以上配置是 Jest 重新編譯你的庫所需要的。有兩個主要部分,我會一一解釋。

transformIgnorePatterns 是 Jest 配置的一個功能,它是一個正則字符串數組。任何匹配這些正則表達式的代碼,都不會被 babel-jest 重新編譯。默認是一個字符串“node_modules”。這就是為什麼Jest 不會重新編譯任何庫代碼。

當我們提供了自定義配置,就是告訴 Jest 重新編譯的時候如何忽略代碼。也就是為什麼你剛才看到的變態的正則表達式有一個負向先行斷言在裏面,目的是為了匹配除了庫以外的所有代碼。換句話說,我們告訴 Jest 忽略 node_modules 中除了指定庫之外的所有代碼。

這又一次證明了 JavaScript 配置比 JSON 配置要好,因為我可以輕鬆地通過字符串操作,往正則表達式里插入庫名字的數組拼接。

第二個是 transform 配置,他指向一個自定義的 babel-jest 轉換器。我不敢100%確定這個是必須的,但我還是加上了。設置它用於在重新編譯所有代碼時加載我們的 Babel 配置。

// Babel-Jest 轉換器
const babelJest = require('babel-jest');
const path = require('path');
const cwd = process.cwd();
const babelConfig = require(path.resolve(cwd, 'babel.config'));
module.exports = babelJest.createTransformer(babelConfig);

這些都配置好后,你在測試代碼應該又能跑了。記住了,任何使用庫的 es2015 模塊都需要這樣配置,不然測試代碼跑不動。

Npm/Yarn Link 就是魔鬼

接下來輪到另一個痛點了:鏈接庫。使用 npm/yarn 鏈接的過程就是創建一個指向本地項目目錄的符號鏈接。結果表明,Babel 在重新編譯通過這種方式鏈接的庫時,會拋出很多錯誤。我之所以花了這麼長時間才弄清楚 Jest 這檔子事兒,原因之一就是我一直通過這種方式鏈接我的庫,出現了一堆錯誤。

解決辦法就是:不要使用 npm/yarn link。用類似 “yalc” 這樣的工具,它可以連接本地項目,同時能模擬正常的 npm 安裝過程。它不但沒有 Babel 重編譯的問題,還能更好地處理傳遞性依賴。

針對特定庫的優化。

如果完成了以上所有步驟,你的應用基本上實現了比較健壯的 tree shaking。不過,為了進一步減少文件包大小,你還可以做一些事情。我會列舉一些特定庫的優化方法,但這絕對不是全部。它尤其能為我們提供靈感,做出一些更酷的事情。

MomentJS 是出了名的大體積庫。幸好它可以剔除多語言包來減少體積。在下面的代碼示例中,我排除了 momentjs 所有的多語言包,只保留了基本部分,體積明顯小了很多。

// 用 IgnorePlugin 移除多語言包
const { IgnorePlugin } from 'webpack';
const config = {
 plugins: [
  new IgnorePlugin(/^\.\/locale$/, /moment/)
 ]
};

Moment-Timezone 是 MomentJS 的老表,也是個大塊頭。它的體積基本上是一個帶有時區信息的超大 JSON 文件導致的。我發現只要保留本世紀的年份數據,就可以將體積縮小90%。這種情況需要用到一個特殊的 Webpack 插件。

// MomentTimezone Webpack Plugin
const MomentTimezoneDataPlugin = require('moment-timezone-data-webpack-plugin');
const config = {
 plugins: [
  new MomentTimezoneDataPlugin({
   startYear: 2018,
   endYear: 2100
  })
 ]
};

Lodash 是另一個導致文件包膨脹的大塊頭。幸好有一個替代包 Lodash-es,它被編譯成 es2015 模塊,並帶有 sideEffects 標誌。用它替換 Lodash 可以進一步縮減包的大小。

另外,Lodash-es,react-bootstrap 以及其他庫可以在 Babel transform imports 插件的幫助下實現瘦身。該插件從庫的 index.js 文件讀取 import 語句,並使其指向庫中特定文件。這樣就使 webpack 在解析模塊樹時更容易對庫做 tree shaking。下面的例子演示了它是如何工作的。

// Babel Transform Imports
// Babel config
const config = {
 plugins: [
  [
   'transform-imports',
   {
    'lodash-es': {
     transform: 'lodash/${member}',
     preventFullImport: true
    },
    'react-bootstrap': {
     transform: 'react-bootstrap/es/${member}', // The es folder contains es2015 module versions of the files
     preventFullImport: true
    }
   }
  ]
 ]
};
// 這些庫不再支持全量導入,否則會報錯
import _ from 'lodash-es';
// 具名導入依然支持
import { debounce } from 'loash-es';
// 不過這些具名導入會被babel編譯成這樣子
// import debounce from 'lodash-es/debounce';

總結

全文到此結束。這樣的優化可以極大地縮減打包后的大小。隨着前端架構開始有了新的方向(比如微前端),保持包大小最優化變得比以往更加重要。希望本文能給那些正在給應用程序做tree shaking的同學帶來一些幫助。

交流

歡迎掃碼關注微信公眾號“1024譯站”,為你奉上更多技術乾貨。

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

【其他文章推薦】

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

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

分類
發燒車訊

上汽、廣汽等十家車企聯合成立電動汽車產業聯盟

近日,上汽、一汽、東風、長安、廣汽、北汽、重汽、華晨、奇瑞、江淮這十家國內車企聯合成立了“電動汽車產業聯盟”,意在聯手攻克節能與新能源汽車在產業化過程中遇到的種種難關。

中國汽車工業協會秘書長董揚稱,聯盟是2015年7月11日成立的,銷售額排名居前十位的國內汽車企業集團“一把手”悉數參會,會上各方探討了電動汽車聯合行動的問題,並制定出針對十家企業的《電動汽車發展共同行動綱要》。

根據綱要內容,確定了“積極引領、聯合行動、突出重點、創新發展”十六字戰略方針,並且成立了T10電動汽車領導小組、電動汽車標準項目工作組,在統一標準方面著手研究,打算如具體工業專案一樣開展工作。

目前聯盟企業正在就關鍵零部件、關鍵總成聯合開發的問題進行調研,調研之後將形成統一規劃,今後的發展方式有可能是一部分技術難題大家共同攻關、投資生產,也有可能各自攻關,誰的成果好其他企業再去共用,儘量追求效益,聯合發展。

據悉,今後該聯盟還將統一制定新能源汽車的相關標準,比如對電池的規格、電機以及電控系統的統一標準,並且將考慮國內的資源情況,而不是像傳統汽車那樣照搬國際標準。

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

【其他文章推薦】

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

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

分類
發燒車訊

病毒如何從動物傳染給人類?

摘錄自2020年2月8日德國之聲中文網報導

2月7日,中國華南農業大學的學者公布了一項研究,指出瀕危動物穿山甲可能是新冠病毒的中間宿主,也就是在自然宿主蝙蝠與人類之間充當了橋梁。科學家發現,從穿山甲身上分離出來的病毒,其基因序列與當前新冠肺炎患者身上的病毒相似度高達99%。

美國得克薩斯大學的華裔病毒學教授項嚴(音)對德國之聲介紹說,許多病毒的自然宿主都是蝙蝠,這並不令人感到驚訝,因為蝙蝠種群數量龐大、分布廣泛。

得克薩斯大學的項教授認為,調查了1000餘個野生動物物種的華南農業大學最新研究,將中間宿主嫌疑指向穿山甲,這一結論是「可信的」。他說,盡管這項研究的論文還沒有正式發表,但是相關證據在去年10月的一篇論文中就可見端倪:來自廣州的幾名科學家,從馬來西亞走私到中國的幾只呈現病態的穿山甲中,發現了冠狀病毒的影蹤。

項教授表示,當前的新型冠狀病毒可能「如同最近一份論文所指出的那樣,是兩種非常類似的冠狀病毒的混合體。」「病毒很難直接從蝙蝠傳染到人,必須要經過中間宿主產生變異。」

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

【其他文章推薦】

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

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

分類
發燒車訊

白朗峰「冰海冰川」縮水中 馬克宏將加強保育

摘錄自2020年2月14日民視報導

西歐最高峰白朗峰每年吸引了約3萬名登山客,一年當中200到300天都有人要去挑戰,有些人甚至不顧天氣警報,並在山上留下不少垃圾。法國總統馬克宏打算進一步推動環保政策,對抗氣候變遷,第一步就從限制人流開始,來避免白朗峰一帶因為遊客過多、全球暖化而遭受嚴重破壞。

冰海冰川每年縮水8到10公尺,自1850年以來已經後退約2公里,專家認為,本世紀結束時多達八成的冰川恐永遠消失,相當驚人。馬克宏指出,冰川急速消退是全球暖化的證據,他誓言加強保育,包括在白朗峰設立保護區、限制訪客人數、提高亂丟垃圾的罰款,並提出其他更廣泛的措施來保護生物多樣性、對抗氣候危機。

其實氣候變遷不只表現在冰川受威脅,法國出現了百年來最暖冬天,溫度上升造成葡萄酒走味、有滑雪場因為雪量不足被迫暫時關閉,種種跡象都顯示各國必須盡快減少溫室氣體排放量,達到巴黎氣候協定中升溫幅度最好不超過1.5度的目標。

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

【其他文章推薦】

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

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

分類
發燒車訊

福特將在2020年之前投資45億美元開發純電動車

美國福特汽車公司將在2020年之前集中投資45億美元用於純電動汽車的開發。

這是迄今為止福特面向純電動車領域的最大一筆投資。該公司將首先在2016年下半年上市新 款純電動車“福克斯”,續航距離約160公里,可進行高速充電,30分鐘就可充滿電池的約80%。包括混合動力、插電式混動(PHV)、純電動產品在內, 計畫在2020年之前推出13款產品。

車輛投放對象市場除美國和歐洲外,還設想了今後純電動領域的市場有望擴大的臺灣、韓國、中國大陸等亞洲地區。尤其是中國大陸的大氣污染嚴重,不排放尾氣的純電動享受政府的優厚補貼。福特在2013年之前一直與豐田汽車共同開展面向皮卡的混合動力技術,但此次似乎準備獨自開發。

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

【其他文章推薦】

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

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

分類
發燒車訊

Asciinema:你的所有操作都將被錄製

如何實現類似於Jumpserver koko一樣的終端錄製回放功能呢?本文介紹一個神器

asciinema 是一款開源免費的終端錄製工具,它可以將命令行輸入輸出的任何內容加上時間保存在文件中,同時還提供方法在終端或者web瀏覽器中進行回放

asciinema的錄製和播放都是基於文本的,相比傳統的video有很多好處,例如錄製文件體積小,在播放的過程中可以暫停複製其中的文本內容等等

同時asciinema還提供了一個網站,你如果願意還可以將錄製的內容上傳至進行展示,也可以在這裏找到很多有趣的終端錄像

asciinema由以下三個子項目構成:

  1. asciinema:基於命令行的終端會話記錄器
  2. asciinema.org:提供API供上傳錄像和展示的網站
  3. javascript player:用於在web上播放錄像的js播放器

asciinema的安裝和使用都非常簡單,一起來看看吧

安裝

asciinema為python開發,可以直接通過apt-getyum或者pip進行安裝

# apt-get install asciinema

安裝完成后查看版本

# asciinema --version
asciinema 2.0.2

asciinema有v1和v2兩個版本,差異較大,咖啡君使用了v2,以下所有內容也基於v2演示

asciinema有5個參數,分別為錄製:rec,播放:play,以文件形式查看錄製內容:cat,上傳文件到asciinema.org網站:upload、asciinema.org賬號認證:auth,本文主要說明recplay的使用

錄製

# asciinema rec ops-coffee.cast

有幾個參數可以使用:

--stdin 表示啟用標準輸入錄製,意思是通常情況下linux輸入密碼類的信息都不會显示,如果開啟了這個選項,可以記錄鍵盤輸出的密碼,但這個功能官方似乎還沒有支持,加了后看不到效果

--append 添加錄製到已存在的文件中

--raw 保存原始STDOUT輸出,無需定時信息等

--overwrite 如果文件已存在,則覆蓋

-c 要記錄的命令,默認為$SHELL

-e 要捕獲的環境變量列表,默認為SHELL,TERM

-t 後跟数字,指定錄像的title

-i 後跟数字,設置錄製時記錄的最大空閑時間

-y 所有提示都輸入yes

-q 靜默模式,加了此參數在進入錄製或者退出錄製時都沒有提示

輸入exit或按ctrl+D組合鍵退出錄製

播放

# asciinema play ops-coffee.cast 

有兩個參數可以使用:

-s 後邊跟数字,表示用幾倍的速度來播放錄像

-i 後邊跟数字,表示在播放錄像時空閑時間的最大秒數

在播放的過程中你可以通過空格來控制暫停或播放,也可以通過ctrl+c組合鍵來退出播放,當你按空格鍵暫停時,可以通過.號來逐幀显示接下來要播放的內容

文件

asciinema推薦的文件後綴是.cast,當然linux是不關心文件後綴的,你用什麼都可以,推薦按規範使用.cast,文件內容大概如下

# cat ops-coffee.cast
{"version": 2, "width": 237, "height": 55, "timestamp": 1572646909, "env": {"SHELL": "/bin/bash", "TERM": "linux"}, "title": "ops-coffee"}
[0.010014, "o", "root@onlinegame:~# "]
[1.296458, "o", "exit"]
[1.976439, "o", "\r\n"]
[1.976532, "o", "exit\r\n"]

cast文件主要有兩部分組成,位於第一行的一個字典,這裏叫header

{
    "version": 2,
    "width": 237,
    "height": 55,
    "timestamp": 1572646909,
    "env": {
        "SHELL": "/bin/bash",
        "TERM": "linux"
    },
    "title": "ops-coffee"
}

header很簡單,字段的意思分別為:version版本,width和height分別表示錄製窗口的寬高,timestamp錄製開始的時間戳,env錄製時指定的-e參數設置,title錄製時指定的-t參數設置

接下來的都是固定格式的內容,實際上就是IO流信息

[0.010014, "o", "root@onlinegame:~# "]

每一行都是由三部分組成的一個列表

第一部分為一個浮點數,表示輸入輸出這一行內容所花的時間

第二部分似乎是一個固定的字符串,沒有找到說明做什麼用的

第三部分就是具體的輸入輸出的內容

這個文件格式設計還是非常優雅的,開頭header聲明,後邊具體內容,如果中途因為任何意外導致錄像終止,也不會丟失整個錄像,而且還可以append增加錄像,這在需要長時間暫停錄製時非常有用,更重要的是可以流式讀取,幾乎很少佔用內存,不需要把整個錄像文件都放在內存中,對長時間的錄製播放更友好

自動錄製審計日誌

如果你有經歷過嚴格的IT審計,或者有用到堡壘機,就會知道操作過程是需要記錄並加入審計的,如果你有因為不知道是誰操作了什麼導致了數據被刪而背鍋的經歷,就會知道對操作過程的記錄有多麼的重要,接下來以一個簡單的案例來介紹asciinema有什麼樣的實用價值

如果希望能夠將linux服務器上devuser的所有操作過程都記錄下來,以備後續審計使用,該如何實現呢?

非常簡單,只需要在devuser用戶的家目錄下添加.bash_profile文件即可,內容如下:

$ cat ~/.bash_profile 
export LC_ALL=en_US.UTF-8
/usr/local/bin/asciinema rec /tmp/$USER-$(date +%Y%m%d%H%M%S).log -q

添加export LC_ALL=en_US.UTF-8的原因是有可能系統會報錯asciinema needs a UTF-8 native locale to run. Check the output of locale command.

rec命令進行錄製時添加了-q參數,這樣在進入或者退出時都不會有任何關於asciinema的提示,使用簡單方便

這樣devuser用戶每次登陸就會自動開啟一個錄像,如果需要審計或檢查操作,只需要回放錄像就可以了

你可能會說history命令一樣可以記錄用戶操作,asciinema有什麼優勢呢?asciinema不僅可以記錄用戶的輸入,還可以記錄系統的輸出,也就是說history只能記錄執行的命令,而asciinema還可以記錄執行的結果,怎麼樣,是不是很方便,趕緊試試吧

相關文章推薦閱讀:

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

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

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

小三通物流營運型態?

※快速運回,大陸空運推薦?

分類
發燒車訊

阿拉伯世界首座核電廠 准運轉

摘錄自2020年2月18日聯合新聞網報導

阿拉伯聯合大公國17日宣布,已向阿拉伯世界第一座核電廠「巴拉卡核電廠」四個機組中的一個核發運轉執照,開啟歷史新頁。

巴拉卡核電廠坐落在波斯灣沿岸、首都阿布達比西方,原定2017年底服役,但數度延後,官方說是要符合安全標準和政府規範。阿布達比政府上月說,巴拉卡核電廠將在數月內運轉。

全球最大石油出口國沙烏地阿拉伯曾說,打算建造16個核電機組,但一直沒實現。巴拉卡核電廠由韓國電力公社主導的企業集團建造,耗資244億美元(約台幣7300億元)。如果四個機組都全速運轉,將能產生5600百萬瓦的電力,能滿足阿聯全國四分之一的電力需求。

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

【其他文章推薦】

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

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

小三通物流營運型態?

※快速運回,大陸空運推薦?