分類
發燒車訊

如何給HTML標籤中的文本設置修飾線

text-decoration屬性介紹

  • text-decoration屬性是用來設置文本修飾線呢,text-decoration屬性一共有4個值。

text-decoration屬性值說明表

作用
none 去掉文本修飾線
underline 設置下劃線
overline 設置上劃線
line-through 設置刪除線

HTML標籤自帶修飾線

  • 在開始實踐text-decoration屬性之前,筆者先給大家普及下HTML中的標籤自帶修飾線如:u標籤s標籤,若有不全大家可以在下面評論中告訴筆者,畢竟筆者也是前端的一個小白,希望和大家相互交流,互幫互助,共同進步。

u標籤

  • 下面讓我們進入u標籤的實踐,u標籤自帶的是文本下劃線。
  • 代碼塊

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>設置文本修飾線</title>
  
</head>
<body>
    <u>成功不是擊敗別人,而是改變自己</u>
</body>
</html>
  • 結果圖

  • 注意:u標籤也可以配合HTML中的其他標籤使用,舉例:將u標籤嵌套到h1標籤中使用。

  • 代碼塊

<h1><u>成功不是擊敗別人,而是改變自己</u></h1>

s標籤

  • 下面讓我們進入s標籤的實踐,s標籤自帶的是文本刪除線。
  • 代碼塊

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>設置文本修飾線</title>
  
</head>
<body>
    <s>成功不是擊敗別人,而是改變自己</s>
</body>
</html>
  • 結果圖

  • 注意:s標籤也可以嵌套,和u標籤一致,筆者就不過多的介紹了。

none去除修飾線

  • 讓我們進入text-decoration屬性的none值實踐,實踐內容如:筆者將HTML頁面中的s標籤自帶的刪除線給去除掉。

  • 代碼塊

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>設置文本修飾線</title>
    <style>
        s{
            text-decoration: none;
        }
    </style>
</head>
<body>
    <s>成功不是擊敗別人,而是改變自己</s>
</body>
</html>
  • 結果圖

  • 注意:u標籤、s標籤、包括text-decoration屬性值的所有的修飾線都可以去掉哦。

underline設置下劃線

  • 讓我們進入text-decoration屬性的underline值實踐,實踐內容如:筆者將HTML頁面中的h2標籤中的文本設置一個下劃線。
  • 代碼塊

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>設置文本修飾線</title>
    <style>
        h2{
            text-decoration: underline;
        }
    </style>
</head>
<body>
    <h2>成功不是擊敗別人,而是改變自己</h2>
</body>
</html>
  • 結果圖

overline設置上劃線

  • 讓我們進入text-decoration屬性的overline值實踐,實踐內容如:筆者將HTML頁面中的h2標籤中的文本設置一個上劃線。

  • 代碼塊

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>設置文本修飾線</title>
    <style>
        h2{
            text-decoration: overline;
        }
    </style>
</head>
<body>
    <h2>成功不是擊敗別人,而是改變自己</h2>
</body>
</html>
  • 結果圖

line-through設置刪除線

  • 讓我們進入text-decoration屬性的line-through值實踐,實踐內容如:筆者將HTML頁面中的h2標籤中的文本設置一個刪除線。

  • 代碼塊

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>設置文本修飾線</title>
    <style>
        h2{
            text-decoration: line-through;
        }
    </style>
</head>
<body>
    <h2>成功不是擊敗別人,而是改變自己</h2>
</body>
</html>
  • 結果圖

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

【其他文章推薦】

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

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

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

南投搬家前需注意的眉眉角角,別等搬了再說!

分類
發燒車訊

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實現線性一致性讀,並講解了線性一致性讀是怎麼實現的,通過這個例子大家應該對線性一致性讀有了一個相對不錯的理解了。

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

【其他文章推薦】

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

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

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

分類
發燒車訊

【集合系列】- 紅黑樹實現分析

一、故事的起因

JDK1.8最重要的就是引入了紅黑樹的設計(當衝突的鏈表長度超過8個的時候),為什麼要這樣設計呢?好處就是避免在最極端的情況下衝突鏈表變得很長很長,在查詢的時候,效率會非常慢。

  • 紅黑樹查詢:其訪問性能近似於折半查找,時間複雜度O(logn);
  • 鏈表查詢:這種情況下,需要遍歷全部元素才行,時間複雜度O(n);

本文主要是講解紅黑樹的實現,只有充分理解了紅黑樹,對於後面的分析才會更加順利。

簡單的說,紅黑樹是一種近似平衡的二叉查找樹,其主要的優點就是“平衡“,即左右子樹高度幾乎一致,以此來防止樹退化為鏈表,通過這種方式來保障查找的時間複雜度為log(n)。

關於紅黑樹的內容,網上給出的內容非常多,主要有以下幾個特性:

  • 1、每個節點要麼是紅色,要麼是黑色,但根節點永遠是黑色的;
  • 2、每個紅色節點的兩個子節點一定都是黑色;
  • 3、紅色節點不能連續(也即是,紅色節點的孩子和父親都不能是紅色);
  • 4、從任一節點到其子樹中每個恭弘=叶 恭弘子節點的路徑都包含相同數量的黑色節點;
  • 5、所有的恭弘=叶 恭弘節點都是是黑色的(注意這裏說恭弘=叶 恭弘子節點其實是上圖中的 NIL 節點);

在樹的結構發生改變時(插入或者刪除操作),往往會破壞上述條件3或條件4,需要通過調整使得查找樹重新滿足紅黑樹的條件。

二、調整方式

上面已經說到當樹的結構發生改變時,紅黑樹的條件可能被破壞,需要通過調整使得查找樹重新滿足紅黑樹的條件。

調整可以分為兩類:一類是顏色調整,即改變某個節點的顏色,這種比較簡單,直接將節點顏色進行轉換即可;另一類是結構調整,改變檢索樹的結構關係。結構調整主要包含兩個基本操作:左旋(Rotate Left)右旋(RotateRight)

2.1、左旋

左旋的過程是將p的右子樹繞p逆時針旋轉,使得p的右子樹成為p的父親,同時修改相關節點的引用,使左子樹的深度加1,右子樹的深度減1,通過這種做法來調整樹的穩定性。過程如下:

以jdk1.8為例,打開HashMap的源碼部分,紅黑樹內部類TreeNode屬性分析:

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        //指向父節點的指針
        TreeNode<K,V> parent;
        //指向左孩子的指針
        TreeNode<K,V> left;
        //指向右孩子的指針
        TreeNode<K,V> right;
        //前驅指針,跟next屬性相反的指向
        TreeNode<K,V> prev;
        //是否為紅色節點
        boolean red;
        ......
}

左旋方法rotateLeft如下:

/*
 * 左旋邏輯
 */
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
                                              TreeNode<K,V> p) {
            //root:表示根節點
            //p:表示要調整的節點
            //r:表示p的右節點
            //pp:表示p的parent節點
            //rl:表示p的右孩子的左孩子節點
            TreeNode<K,V> r, pp, rl;
            //r判斷,如果r為空則旋轉沒有意義
            if (p != null && (r = p.right) != null) {
                //多個等號的連接操作從右往左看,設置rl的父親為p
                if ((rl = p.right = r.left) != null)
                    rl.parent = p;
                //判斷p的父親,為空,為根節點,根節點的話就設置為黑色
                if ((pp = r.parent = p.parent) == null)
                    (root = r).red = false;
                //判斷p節點是左兒子還是右兒子
                else if (pp.left == p)
                    pp.left = r;
                else
                    pp.right = r;
                r.left = p;
                p.parent = r;
            }
            return root;
}

2.2、右旋

了解了左旋轉之後,相應的就會有右旋,邏輯基本也是一樣,只是方向變了。右旋的過程是將p的左子樹繞p順時針旋轉,使得p的左子樹成為p的父親,同時修改相關節點的引用,使右子樹的深度加1,左子樹的深度減1,通過這種做法來調整樹的穩定性。實現過程如下:

同樣的,右旋方法rotateRight如下:

/*
 * 右旋邏輯
 */
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
                                               TreeNode<K,V> p) {
            //root:表示根節點
            //p:表示要調整的節點
            //l:表示p的左節點
            //pp:表示p的parent節點
            //lr:表示p的左孩子的右孩子節點
            TreeNode<K,V> l, pp, lr;
            //l判斷,如果l為空則旋轉沒有意義
            if (p != null && (l = p.left) != null) {
                //多個等號的連接操作從右往左看,設置lr的父親為p
                if ((lr = p.left = l.right) != null)
                    lr.parent = p;
                //判斷p的父親,為空,為根節點,根節點的話就設置為黑色
                if ((pp = l.parent = p.parent) == null)
                    (root = l).red = false;
                //判斷p節點是右兒子還是左兒子
                else if (pp.right == p)
                    pp.right = l;
                else
                    pp.left = l;
                l.right = p;
                p.parent = l;
            }
            return root;
}

三、操作示例介紹

3.1、插入調整過程圖解

3.2、刪除調整過程圖解

3.3、查詢過程圖解

四、總結

至此,紅黑樹的實現就基本完成了,關於紅黑樹的結構,有很多種情況,情況也比較複雜,但是整體調整流程,基本都是先調整結構然後調整顏色,直到最後滿足紅黑樹特性要求為止。整篇文章,如果有理解不當之處,歡迎指正!

五、參考

1、
2、

作者:炸雞可樂
出處:

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

【其他文章推薦】

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

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

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

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

分類
發燒車訊

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地圖已可更新顯示潭子電動車充電站設置地點!!

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

分類
發燒車訊

上海推出新能源汽車充電樁綜合保險

近期,上海保險業與國家電網積極合作,推出充電樁綜合保險產品。該產品包括充電樁財產保險和充電樁用電安全責任保險。充電樁財產保險保額最高1萬元,充電樁用電安全責任保險保額3萬元。

合作初期,上海相關險企採取贈送方式,建立專項資金池,向新能源車主贈送推廣充電樁保險。同時也在國家電網營業廳設點,向預報樁客戶宣傳介紹充電樁綜合保險的相關產品和服務。

據瞭解,2015年7月1日起,上海正式實施《上海市電動汽車充電基礎設施建設管理暫行規定》。7月1日後,上海市民必須提供政府備案的充電服務機構出具的“充電設施已裝證明”,才能在銷售企業辦理購買新能源汽車手續,必須安裝好充電樁後才能獲得國家和地方政府補貼、並申請到新能源車免費牌照。此外,《規定》也明確充電設施所有權人應當承擔充電設施維修更新養護及侵害第三者權益責任。

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

【其他文章推薦】

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

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

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

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

分類
發燒車訊

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

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

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

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

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

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

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

【其他文章推薦】

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

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

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

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

分類
發燒車訊

日問周刊 | 全棧面試匯總 | 第二期

勤學如春起之苗,不見其增,日有所長;輟學如磨刀之石,不見其損,日有所虧。

我在 github 上新建了一個倉庫 ,每天至少一個問題。有關全棧,graphql,devops,微服務以及軟技能,促進職業成長,歡迎交流。

以諸葛武侯的誡子書與君共勉

夫君子之行,靜以修身,儉以養德。非澹泊無以明志,非寧靜無以致遠。夫學須靜也,才須學也,非學無以廣才,非志無以成學。淫慢則不能勵精,險躁則不能治性。年與時馳,意與日去,遂成枯落,多不接世,悲守窮廬,將復何及!

【Q037】linux 有哪些發行版,你最喜歡哪一個

原文鏈接,歡迎討論:

開放問題,不過你至少得知道一個發行版…

【Q036】http 狀態碼中 301,302和307有什麼區別

原文鏈接,歡迎討論:

  • 301,Moved Permanently。永久重定向,該操作比較危險,需要謹慎操作:如果設置了301,但是一段時間后又想取消,但是瀏覽器中已經有了緩存,還是會重定向。
  • 302,Fount。臨時重定向,但是會在重定向的時候改變 method: 把 POST 改成 GET,於是有了 307
  • 307,Temporary Redirect。臨時重定向,在重定向時不會改變 method

【Q035】http 常見的狀態碼有哪些

原文鏈接,歡迎討論:

【Q034】如何實現一個 loading 動畫

原文鏈接,歡迎討論:

【Q033】如何對接口進行限流]

原文鏈接,歡迎討論:

一般採用漏桶算法:

  1. 漏桶初始為空
  2. API 調用是在往漏桶里注水
  3. 漏桶會以一定速率出水
  4. 水滿時 API 拒絕調用

可以使用 redis 的計數器實現

  1. 計數器初始為空
  2. API 調用計數器增加
  3. 給計數器設置過期時間,隔段時間清零,視為一定速率出水
  4. 計數器達到上限時,拒絕調用

當然,這隻是大致思路,這時會有兩個問題要注意

  1. 最壞情況下的限流是額定限流速率的2倍
  2. 條件競爭問題

不過實際實現時注意以下就好了(話說一般也是調用現成的三方庫做限流…),可以參考我以前的文章

【Q032】js 中什麼是 softbind,如何實現

原文鏈接,歡迎討論:

【Q031】js 中如何實現 bind

原文鏈接,歡迎討論:

最簡單的 bind 一行就可以實現,而在實際面試過程中也不會考察你太多的邊界條件

Function.prototype.fakeBind = function(obj) {
  return (...args) => this.apply(obj, args)
}

測試一下

function f (arg) {
  console.log(this.a, arg)
}

// output: 3, 4
f.bind({ a: 3 })(4)

// output: 3, 4
f.fakeBind({ a: 3 })(4)

【Q030】linux 中如何打印所有網絡接口

原文鏈接,歡迎討論:

ifconfig

ifconfig 是最簡單最常用,但是打印信息太多了

$ ifconfig

netstat

netstatip 也挺好用,特別是它們還可以打印路由表

$ netstat -i

ip

$ ip link

$ ip addr

【Q029】websocket 如何向特定的用戶組推送消息

redis 處維護一個對象,記錄每個 group 所對應的 connections/sockets

{
  'Class:201901': [student1Socket, student2Socket]
}

當 client 剛連入 server 時,便加入某個特定的組,或者叫 room,比如 student01,剛開始連入 server,可能要加入 room:Student:01Class:201901Group:10086

$ who

$ last

一圖勝千言

使用 jsonb_pretty 函數,示例如下

> select jsonb_pretty('{"a": {"b": 4}}'::jsonb)
+----------------+
| jsonb_pretty   |
|----------------|
| {              |
|     "a": {     |
|         "b": 4 |
|     }          |
| }              |
+----------------+
SELECT 1
Time: 0.018s

一個簡單的 Promise 的粗糙實現,關鍵點在於

  1. pending 時, thenable 函數由一個隊列維護
  2. 當狀態變為 resolved(fulfilled) 時,隊列中所有 thenable 函數執行
  3. resolved 時, thenable 函數直接執行

rejected 狀態同理

class Prom {
  static resolve (value) {
    if (value && value.then) {
      return value 
    }
    return new Prom(resolve => resolve(value))
  }

  constructor (fn) {
    this.value = undefined
    this.reason = undefined
    this.status = 'PENDING'

    // 維護一個 resolve/pending 的函數隊列
    this.resolveFns = []
    this.rejectFns = []

    const resolve = (value) => {
      // 注意此處的 setTimeout
      setTimeout(() => {
        this.status = 'RESOLVED'
        this.value = value
        this.resolveFns.forEach(({ fn, resolve: res, reject: rej }) => res(fn(value)))
      })
    }

    const reject = (e) => {
      setTimeout(() => {
        this.status = 'REJECTED'
        this.reason = e
        this.rejectFns.forEach(({ fn, resolve: res, reject: rej }) => rej(fn(e)))
      })
    }

    fn(resolve, reject)
  }


  then (fn) {
    if (this.status === 'RESOLVED') {
      const result = fn(this.value)
      // 需要返回一個 Promise
      // 如果狀態為 resolved,直接執行
      return Prom.resolve(result)
    }
    if (this.status === 'PENDING') {
      // 也是返回一個 Promise
      return new Prom((resolve, reject) => {
        // 推進隊列中,resolved 后統一執行
        this.resolveFns.push({ fn, resolve, reject }) 
      })
    }
  }

  catch (fn) {
    if (this.status === 'REJECTED') {
      const result = fn(this.value)
      return Prom.resolve(result)
    }
    if (this.status === 'PENDING') {
      return new Prom((resolve, reject) => {
        this.rejectFns.push({ fn, resolve, reject }) 
      })
    }
  }
}

Prom.resolve(10).then(o => o * 10).then(o => o + 10).then(o => {
  console.log(o)
})

return new Prom((resolve, reject) => reject('Error')).catch(e => {
  console.log('Error', e)
})

首參不一樣,直接上 API

React.cloneElement(
  element,
  [props],
  [...children]
)

React.createElement(
  type,
  [props],
  [...children]
)

它一般可以使用第三方庫 來實現,源碼很簡單,可以讀一讀

主要有兩個要點

  1. 選中
  2. 複製

選中

選中主要利用了

選中的代碼如下

const selection = window.getSelection();
const range = document.createRange();

range.selectNodeContents(element);
selection.removeAllRanges();
selection.addRange(range);

selectedText = selection.toString();

取消選中的代碼如下

window.getSelection().removeAllRanges();

它有現成的第三方庫可以使用:

複製

複製就比較簡單了,execCommand

document.exec('copy')

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

【其他文章推薦】

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

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

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

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

分類
發燒車訊

科學警訊:熱浪使熊蜂瀕臨滅絕 連帶影響農糧產量

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

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

【其他文章推薦】

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

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

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

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

分類
發燒車訊

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

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

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

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

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

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

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

【其他文章推薦】

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

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

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

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

分類
發燒車訊

斷電惹民怨 南非拚能源轉型

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

南非總統拉瑪佛沙日前表示,大量依賴煤炭的南非將會轉型使用更多再生能源,減少嚴重衝擊重建經濟努力的斷電現象。不過,他也警告,近期可能會有更多斷電發生。

南非民眾對今年盛夏分區斷電感到憤怒,投資人也感到擔憂。根據南非能源部,南非約77%電力依賴燃煤火力發電,部分民眾對於官員將停電歸咎於「濕煤」覺得傻眼。

拉瑪佛沙警告,停電將會持續,電力公司Eskom正在進行必要的改變,包括拖延已久的維修。他說,「在未來幾個月,隨著Eskom努力恢復其運營能力,我們將採取措施,從根本上改變我國能源生產軌跡」。

南非政府的解決辦法之一是,允許商業和工業用戶自行發電,並且允許地方政府向獨立發電商購買電力。南非也將向現有的風力和太陽能發電廠購買更多能源。

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

【其他文章推薦】

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

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

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

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