搭建前端監控,如何采集異常數據?

大家好,我是若川。持續組織了近一年的源碼共讀活動,感興趣的可以?點此加我微信ruochuan12?參與,每周大家一起學習200行左右的源碼,共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》?包含20余篇源碼文章。歷史面試系列。另外:目前建有江西|湖南|湖北籍前端群,可加我微信進群。


前兩篇,我們介紹了為什么前端應該有監控系統,以及搭建前端監控的總體步驟,前端監控的 Why 和 What 想必你已經明白了。接下來我們解決 How 如何實現的問題。

如果不了解前端監控,建議先看前兩篇:

  • 為什么前端不能沒有監控系統?

  • 前端監控的總體搭建步驟

本篇我們介紹,前端如何采集數據,先從收集異常數據開始。

什么是異常數據?

異常數據,是指前端在操作頁面的過程中,觸發的執行異常或加載異常,此時瀏覽器會拋出來報錯信息。

比如說你的前端代碼用了個未聲明的變量,此時控制臺會打印出紅色錯誤,告訴你報錯原因。或者是接口請求出錯了,在網絡面板內也能查到異常情況,是請求發送的異常,還是接口響應的異常。

在我們實際的開發場景中,前端捕獲的異常主要是分兩個大類,接口異常前端異常,我們分別看下這兩大類異常怎么捕獲。

接口異常

接口異常一定是在請求的時候觸發。前端目前大部分的請求是用 axios 發起的,所以只要獲取 axios 可能發生的異常即可。

如果你用 Promise 的寫法,則用 .catch 捕獲:

axios.post('/test').then((res)?=>?{console.log(res);}).catch((err)?=>?{//?err?就是捕獲到的錯誤對象handleError(err);});

如果你用 async/await 的寫法,則用 try..catch.. 捕獲:

async?()?=>?{try?{let?res?=?await?axios.post('/test');console.log(res);}?catch?(err)?{//?err?就是捕獲到的錯誤對象handleError(err);}
};

當捕獲到異常之后,統一交給 handleError 函數處理,這個函數會將接收到的異常進行處理,并調用 上報接口 將異常數據傳到服務器,從而完成采集。

上面我們寫的異常捕獲,邏輯上是沒問題的,實操起來就會發現第一道坎:頁面這么多,難道每個請求都要包一層 catch 嗎?

是啊,如果我們是新開發一個項目,在開始的時候就規定每個請求要包一層 catch 也無可厚非,但是如果是在一個已有的規模還不小的項目中接入前端監控,這時候在每個頁面或每個請求 catch 顯然是不現實的。

所以,為了最大程度地降低接入成本,減少侵入性,我們是用第二種方案:在 axios 攔截器中捕獲異常

前端項目,為了統一處理請求,比如 401 的跳轉,或者全局錯誤提示,都會在全局寫一個 axios 實例,為這個實例添加攔截器,然后在其他頁面中直接導入這個實例使用,比如:

//?全局請求:src/request/axios.jsconst?instance?=?axios.create({baseURL:?'https://api.test.com'timeout:?15000,headers:?{'Content-Type':?'application/json',},
})export?default?instance

然后在具體的頁面中這樣發起請求:

// a 頁面:src/page/a.jsx
import?http?from?'@/src/request/axios.js';async?()?=>?{let?res?=?await?http.post('/test');console.log(res);
};

這樣的話,我們發現每個頁面的請求都會走全局 axios 實例,所以我們只需要在全局請求的位置捕獲異常即可,就不需要在每個頁面捕獲了,這樣接入成本會大大降低。

按照這個方案,接下來我們在 src/request/axios.js 這個文件中動手實施。

攔截器中捕獲異常

首先我們為 axios 添加響應攔截器:

//?響應攔截器
instance.interceptors.response.use((response)?=>?{return?response.data;},(error)?=>?{//?發生異常會走到這里if?(error.response)?{let?response?=?error.response;if?(response.status?>=?400)?{handleError(response);}}?else?{handleError(null);}return?Promise.reject(error);},
);

響應攔截器的第二個參數是在發生錯誤時執行的函數,參數就是異常。我們首先要判斷是否存在 error.response,存在就說明接口有響應,也就是接口通了,但是返回錯誤;不存在則說明接口沒通,請求一直掛起,多數是接口崩潰了。

如果有響應,首先獲取狀態碼,根據狀態碼來判斷什么時候需要收集異常。上面的判斷方式簡單粗暴,只要狀態碼大于 400 就視為一個異常,拿到響應數據,并執行上報邏輯。

如果沒有響應,可以看作是接口超時異常,調用異常處理函數時傳一個 null 即可。

前端異常

上面我們介紹了在 axios 攔截器中如何捕獲接口異常,這部分我們再介紹如何捕獲前端異常。

前端代碼捕獲異常,最常用的方式就是用 try..catch.. 了,任意同步代碼塊都可以放到 try 塊中,只要發生異常就會執行 catch:

try?{//?任意同步代碼
}?catch?(err)?{console.log(err);
}

上面說“任意同步代碼”而不是“任意代碼”,主要是普通的 Promise 寫法 try..catch.. 是捕獲不到的,只能用 .catch() 捕獲,如:

try?{Promise.reject(new?Error('出錯了')).catch((err)?=>?console.log('1:',?err));
}?catch?(err)?{console.log('2:',?err);
}

把這段代碼丟進瀏覽器,打印結果是:

1:?Error:?出錯了

很明顯只是 .catch 捕獲到了異常。不過與上面接口異常的邏輯一樣,這種方式處理當前頁面異常沒什么問題,但從整個應用來看,這樣捕獲異常侵入性強,接入成本高,所以我們的思路依然是全局捕獲。

全局捕獲 js 的異常也比較簡單,用 window.addEventLinstener('error') 即可:

//?js?錯誤捕獲
window.addEventListener('error',?(error)?=>?{//?error?就是js的異常
});

為啥不用 window.onerror ?

這里很多小伙伴有疑問,為什么不用 window.onerror 全局監聽呢?window.addEventLinstener('error')window.onerror 有什么區別呢?

首先這兩個函數功能基本一致,都可以全局捕獲 js 異常。但是有一類異常叫做 資源加載異常,就是在代碼中引用了不存在的圖片,js,css 等靜態資源導致的異常,比如:

const?loadCss?=?()=>?{let?link?=?document.createElement('link')link.type?=?'text/css'link.rel?=?'stylesheet'link.href?=?'https://baidu.com/15.css'document.getElementsByTagName('head')[10].append(link)
}
render()?{return?<div><img?src='./bbb.png'/><button?onClick={loadCss}>加載樣式<button/></div>
}

上述代碼中的 baidu.com/15.cssbbb.png 是不存在的,JS 執行到這里肯定會報一個資源找不到的錯誤。但是默認情況下,上面兩種 window 對象上的全局監聽函數都監聽不到這類異常。

因為資源加載的異常只會在當前元素觸發,異常不會冒泡到 window,因此監聽 window 上的異常是捕捉不到的。那怎么辦呢?

如果你熟悉 DOM 事件你就會明白,既然冒泡階段監聽不到,那么在捕獲階段一定能監聽到。

方法就是給 window.addEventListene 函數指定第三個參數,很簡單就是 true,表示該監聽函數會在捕獲階段執行,這樣就能監聽到資源加載異常了。

//?捕獲階段全局監聽
window.addEventListene('error',(error)?=>?{if?(error.target?!=?window)?{console.log(error.target.tagName,?error.target.src);}handleError(error);},true,
);

上述方式可以很輕松的監聽到圖片加載異常,這就是為什么更推薦 window.addEventListene 的原因。不過要記得,第三個參數設為 true,監聽事件捕獲,就可以全局捕獲到 JS 異常和資源加載異常。

需要特別注意,window.addEventListene 同樣不能捕獲 Promise 異常。不管是 Promise.then() 寫法還是 async/await 寫法,發生異常時都不能捕獲。

因此,我們還需要全局監聽一個 unhandledrejection 函數來捕獲未處理的 Promise 異常。

//?promise?錯誤捕獲
window.addEventListener('unhandledrejection',?(error)?=>?{//?打印異常原因console.log(error.reason);handleError(error);//?阻止控制臺打印error.preventDefault();
});

unhandledrejection 事件會在 Promise 發生異常并且沒有指定 catch 的時候觸發,相當于一個全局的 Promise 異常兜底方案。這個函數會捕捉到運行時意外發生的 Promise 異常,這對我們排錯非常有用。

默認情況下,Promise 發生異常且未被 catch 時,會在控制臺打印異常。如果我們想阻止異常打印,可以用上面的 error.preventDefault() 方法。

異常處理函數

前面我們在捕獲到異常時調用了一個異常處理函數 handleError,所有的異常和上報邏輯統一在這個函數內處理,接下來我們實現這個函數。

const?handleError?=?(error:?any,?type:?1?|?2)?{if(type?==?1)?{//?處理接口異常}if(type?==?2)?{//?處理前端異常}
}

為了區分異常類型,函數新加了第二個參數 type 表示當前異常屬于前端還是接口。在不同的場景中使用如下:

  • 處理前端異常:handleError(error, 1)

  • 處理接口異常:handleError(error, 2)

處理接口異常

處理接口異常,我們需要將拿到的 error 參數解析,然后取到需要的數據。接口異常一般需要的數據字段如下:

  • code:http 狀態碼

  • url:接口請求地址

  • method:接口請求方法

  • params:接口請求參數

  • error:接口報錯信息

這些字段都可以在 error 參數中獲取,方法如下:

const?handleError?=?(error:?any,?type:?1?|?2)?{if(type?==?1)?{//?此時的?error?響應,它的?config?字段中包含請求信息let?{?url,?method,?params,?data?}?=?error.configlet?err_data?=?{url,?method,params:?{?query:?params,?body:?data?},error:?error.data?.message?||?JSON.stringify(error.data),})}
}

config 對象中的 params 表示 GET 請求的 query 參數,data 表示 POST 請求的 body 參數,所以我在處理參數的時候,將這兩個參數合并為一個,用一個屬性 params 來表示。

params:?{?query:?params,?body:?data?}

還有一個 error 屬性表示錯誤信息,這個獲取方式要根據你的接口返回格式來拿。要避免獲取到接口可能返回的超長錯誤信息,多半是接口沒處理,這樣可能會導致寫入數據失敗,要提前與后臺規定好。

處理前端異常

前端異常異常大多數就是 js 異常,異常對應到 js 的 Error 對象,在處理之前,我們先看 Error 有哪幾種類型:

  • ReferenceError:引用錯誤

  • RangeError:超出有效范圍

  • TypeError:類型錯誤

  • URIError:URI 解析錯誤

這幾類異常的引用對象都是 Error,因此可以這樣獲取:

const?handleError?=?(error:?any,?type:?1?|?2)?{if(type?==?2)?{let?err_data?=?null//?監測?error?是否是標準類型if(error?instanceof?Error)?{let?{?name,?message?}?=?errorerr_data?=?{type:?name,error:?message}}?else?{err_data?=?{type:?'other',error:?JSON.strigify(error)}}}
}

上述判斷中,首先判斷異常是否是 Error 的實例。事實上絕大部分的代碼異常都是標準的 JS Error,但我們這里還是判斷一下,如果是的話直接獲取異常類型和異常信息,不是的話將異常類型設置為 other 即可。

我們隨便寫一個異常代碼,看一下捕獲的結果:

function?test()?{console.aaa('ccc');
}
test();

然后捕獲到的異常是這樣的:

const?handleError?=?(error:?any)?=>?{if?(error?instanceof?Error)?{let?{?name,?message?}?=?error;console.log(name,?message);//?打印結果:TypeError console.aaa is not a function}
};

獲取環境數據

獲取環境數據的意思是,不管是接口異常還是前端異常,除了異常本身的數據之外,我們還需要一些其他信息來幫助我們更快更準的定位到哪里出錯了。

這類數據我們稱之為 “環境數據”,就是觸發異常時所在的環境。比如是誰在哪個頁面的哪個地方觸發的錯誤,有了這些,我們就能馬上找到錯誤來源,再根據異常信息解決錯誤。

環境數據至少包括下面這些:

  • app:應用的名稱/標識

  • env:應用環境,一般是開發,測試,生產

  • version:應用的版本號

  • user_id:觸發異常的用戶 ID

  • user_name:觸發異常的用戶名

  • page_route:異常的頁面路由

  • page_title:異常的頁面名稱

appversion 都是應用配置,可以判斷異常出現在哪個應用的哪個版本。這兩個字段我建議直接獲取 package.json 下的 nameversion 屬性,在應用升級的時候,及時修改 version 版本號即可。

其余的字段,需要根據框架的配置獲取,下面我分別介紹在 Vue 和 React 中如何獲取。

在 Vue 中

在 Vue 中獲取用戶信息一般都是直接從 Vuex 里面拿,如果你的用戶信息沒有存到 Vuex 里,從 localStorage 里獲取也是一樣的。

如果在 Vuex 里,可以這樣實現:

import?store?from?'@/store';?//?vuex?導出目錄
let?user_info?=?store.state;
let?user_id?=?user_info.id;
let?user_name?=?user_info.name;

用戶信息存在狀態管理中,頁面路由信息一般是在 vue-router 中定義。前端的路由地址可以直接從 vue-router 中獲取,頁面名稱可以配置在 meta 中,如:

{path:?'/test',name:?'test',meta:?{title:?'測試頁面'},component:?()?=>?import('@/views/test/Index.vue')
},

這樣配置之后,獲取當前頁面路由和頁面名稱就簡單了:

window.vm?=?new?Vue({...})let?route?=?vm.$route
let?page_route?=?route.path
let?page_title?=?route.meta.title

最后一步,我們再獲取當前環境。當前環境用一個環境變量 VUE_APP_ENV 表示,有三個值:

  • dev:開發環境

  • test:測試環境

  • pro:生產環境

然后在根目錄下新建三個環境文件,寫入環境變量:

  • .env.development:VUE_APP_ENV=dev

  • .env.staging:VUE_APP_ENV=test

  • .env.production:VUE_APP_ENV=pro

現在獲取 env 環境時就可以這么獲取:

{env:?process.env.VUE_APP_ENV;
}

最后一步,執行打包時,傳入模式以匹配對應的環境文件:

#?測試環境打包
$?num?run?build?--mode?staging
#?生產環境打包
$?num?run?build?--mode?production

獲取到環境數據,再拼上異常數據,我們就準備好了數據等待上報了。

在 React 中

和 Vue 一樣,用戶信息可以直接從狀態管理里拿。因為 React 中沒有全局獲取當前旅游的快捷方式,所以頁面信息我也會放在狀態管理里面。我用的狀態管理是 Mobx,獲取方式如下:

import?{?TestStore?}?from?'@/stores';?//?mobx?導出目錄
let?{?user_info,?cur_path,?cur_page_title?}?=?TestStore;
//?用戶信息:user_info
//?頁面信息:cur_path,cur_page_title

這樣的話,就需要在每次切換頁面時,更新 mobx 里的路由信息,怎么做呢?

其實在根路由頁(一般是首頁)的 useEffect 中監聽即可:

import?{?useLocation?}?from?'react-router';
import?{?observer,?useLocalObservable?}?from?'mobx-react';
import?{?TestStore?}?from?'@/stores';export?default?observer(()?=>?{const?{?pathname,?search?}?=?useLocation();const?test_inst?=?useLocalObservable(()?=>?TestStore);useEffect(()?=>?{test_inst.setCurPath(pathname,?search);},?[pathname]);
});

獲取到用戶信息和頁面信息,接下來就是當前環境了。和 Vue 一樣通過 --mode 來指定模式,并加載相應的環境變量,只不過設置方法略有不同。大多數的 React 項目可能都是用 create-react-app 創建的,我們以此為例介紹怎么修改。

首先,打開 scripts/start.js 文件,這是執行 npm run start 時執行的文件,我們在開頭部分第 6 行加代碼:

process.env.REACT_APP_ENV?=?'dev';

沒錯,我們指定的環境變量就是 REACT_APP_ENV,因為只有 REACT_ 開頭的環境變量可被讀取。

然后再修改 scripts/build.js 文件的第 48 行,修改后如下:

if?(argv.length?>=?2?&&?argv[0]?==?'--mode')?{switch?(argv[1])?{case?'staging':process.env.REACT_APP_ENV?=?'test';break;case?'production':process.env.REACT_APP_ENV?=?'pro';break;default:}
}

此時獲取 env 環境時就可以這么獲取:

{env:?process.env.REACT_APP_ENV;
}

總結

經過前面一系列操作,我們已經比較全面的獲取到了異常數據,以及發生異常時到環境數據,接下來就是調用上報接口,將這些數據傳給后臺存起來,我們以后查找和追蹤就很方便了。

如果你也需要前端監控,不妨花上半個小時,按照文中介紹的方法收集一下異常數據,相信對你很有幫助。

1fb3e756dd13bf612b4ffbf75a29f54f.gif

·················?若川簡介?·················

你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》20余篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結,已經堅持寫了8年,點擊查看年度總結。
同時,最近組織了源碼共讀活動,幫助4000+前端人學會看源碼。公眾號愿景:幫助5年內前端人走向前列。

5d704efb45bb958696bec11f7bb90baf.png

掃碼加我微信 ruochuan12、拉你進源碼共讀

今日話題

目前建有江西|湖南|湖北?籍 前端群,想進群的可以加我微信 ruochuan12?進群。分享、收藏、點贊、在看我的文章就是對我最大的支持

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/274633.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/274633.shtml
英文地址,請注明出處:http://en.pswp.cn/news/274633.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

產品經理如何提高創造力_如何提高產品設計師的創造力

產品經理如何提高創造力When David Kelley, Bill Moggridge, and Mike Nuttall founded IDEO, a consulting firm that would become one of the most innovative companies of the late 90s, they brought a new perspective in product development.當大衛凱利(David Kelley)…

Github上8個很棒的Vue項目

大家好&#xff0c;我是若川。持續組織了近一年的源碼共讀活動&#xff0c;感興趣的可以 點此加我微信ruochuan12 參與&#xff0c;每周大家一起學習200行左右的源碼&#xff0c;共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》 包含20余篇源碼文章。歷史面試系列。…

域名解析文件hosts文件是什么?如何修改hosts文件?

如何修改hosts文件&#xff1f; hosts文件的位置&#xff1a;xp,2000等系統在 C:\windows\system32\drivers\etc 文件夾中找到Hosts文件并用記事本打開(Windows 9x/Me系統在C:\Windows文件夾中找)按照 ip地址 域名 的格式添加單獨的一行記錄。例如72.14.219.190 www.hbcms.net…

python 投資組合_成功投資組合的提示

python 投資組合Lately, I’ve had some free time during my job transition and have been reviewing a few of my friends’ design portfolios. Gradually, I found some common themes around the feedback I’ve given. And it occurred to me that others might find so…

Github上8個很棒的React項目

大家好&#xff0c;我是若川。持續組織了近一年的源碼共讀活動&#xff0c;感興趣的可以 點此加我微信ruochuan12 參與&#xff0c;每周大家一起學習200行左右的源碼&#xff0c;共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》 包含20余篇源碼文章。歷史面試系列。…

騰訊的筆試題一道

搜羅了一些騰訊的筆試題目 題目是這樣的&#xff1a;在如下8*6的矩陣中&#xff0c;請計算從A移動到B一共有多少種走法&#xff1f;要求每次只能向上揮著向右移動一格&#xff0c;并且不能經過P&#xff1b; B P …

屏幕廣播系統_如何設計系統,而不是屏幕

屏幕廣播系統重點 (Top highlight)Over the past several decades, rapid advances in technology have dramatically enhanced the digital customer experience and their expectations. In the face of these heightened customer expectations, the role of the Interactio…

Umi 4 發布啦

大家好&#xff0c;我是若川。持續組織了近一年的源碼共讀活動&#xff0c;感興趣的可以 點此加我微信ruochuan12 參與&#xff0c;每周大家一起學習200行左右的源碼&#xff0c;共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》 包含20余篇源碼文章。歷史面試系列。…

Win32匯編--加載菜單資源

基本上的窗口都會有一個菜單,現在就來看看Win32匯編中是如何加載菜單的: 1>在工程中添加新的菜單資源 2>雙擊新添加的菜單資源進行編輯 3>菜單欄:Make->Compile RC來編譯資源文件 4>導出資源中的ID號并寫到數據段的.const中 5>下面是完整的源代碼供參考:(工程…

Futura:從納粹主義到月球-甚至更遠

Reading the title of this article, the first thing that will come to mind for some is the funny expression of Buzz Lightyear — the Disney character — when he stretches his arms outwards and utters the famous phrase “To infinity and beyond!” before jump…

如何碎片化時間高效學習前端~

前端技術日新月異&#xff0c;發展迅速&#xff0c;作為一個與時俱進的前端工程師&#xff0c;需要不斷的學習。這里強烈推薦幾個前端開發工程師必備的優質公眾號&#xff0c;希望對你有所幫助。大家可以像我一樣&#xff0c;利用碎片時間閱讀這些公眾號的文章。前端從進階到入…

爬取淘寶定價需要多久時間_如何對設計工作進行定價—停止收??取時間并專注于價值

爬取淘寶定價需要多久時間Pricing creative work is a new concept for most freelancers who are starting their business. We are used to being paid for our time, either by an hourly wage or an annual salary. It makes it simple to quantify how much value we thin…

OEA 框架中集成的 RDLC 報表介紹

之前 OEA 一直用著一個 Delphi 開發的報表&#xff0c;所以兩年來我一直就想在 OEA 中構建一個純 .NET 的報表模塊&#xff0c;但是一想到要開發復雜的報表引擎和設計器就覺得麻煩。所以這事一直拖著。最近開始研究一些成熟的報表引擎&#xff0c;經過對比&#xff0c;還是發現…

昆蟲繁殖_“專為昆蟲而生” –好奇!

昆蟲繁殖重點 (Top highlight)The industry is changing towards a more agile approach and jacks of one trade can go extinct sooner than we think.該 行業正在發生變化 朝著更加靈活的方法和一個貿易的插Kong可以去滅絕快于我們的想法。 I’ve read a quote in a book r…

ECMAScript 2022 正式發布,有哪些新特性?

大家好&#xff0c;我是若川。持續組織了近一年的源碼共讀活動&#xff0c;感興趣的可以 點此加我微信ruochuan12 參與&#xff0c;每周大家一起學習200行左右的源碼&#xff0c;共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》 包含20余篇源碼文章。歷史面試系列。…

字母框如何影響UI內容的理解

What is your earliest memory of reading? Mine’s reading comics. I preferred films over books, I still do, but I seemed to have a fascination for comics. The experience of reading a comic, to me, was somewhere between watching a film and reading a novel, …

Vue2.7 本周發布?支持組合式 API、setup、css v-bind

大家好&#xff0c;我是若川。持續組織了近一年的源碼共讀活動&#xff0c;感興趣的可以 點此加我微信ruochuan12 參與&#xff0c;每周大家一起學習200行左右的源碼&#xff0c;共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》 包含20余篇源碼文章。歷史面試系列。…

linux中用戶忘記root的密碼--ubuntu版本

基于ubuntu操作系統的情況&#xff0c;當用戶忘記root密碼后&#xff0c; 在普通用戶登陸后 輸入sudu su root 之后系統要求輸入當前用戶的密碼&#xff0c;用戶輸入密碼后&#xff0c;就可以進入root的模式了 就可以操作任何任務。轉載于:https://www.cnblogs.com/zhengyn/arc…

馬上7月,誠邀新老朋友參加近5000人的源碼共讀活動!

大家好&#xff0c;我是若川。最近有不少新朋友關注我。誠邀各位新老讀者朋友參加源碼共讀活動。活動介紹可以點擊文末的閱讀原文。https://juejin.cn/post/7079706017579139102很多人關注我的公眾號是因為我寫了一系列源碼文章&#xff0c;想參與源碼共讀活動。雖然現在有近50…

hashmap 從頭到尾_如何從頭到尾設計一個簡單的復古徽標

hashmap 從頭到尾在紙上素描粗糙的概念 (Sketch rough concepts on paper) Start by sketching out a few ideas for your logo on paper. These don’t have to be detailed drawings. Instead, it’s about getting your ideas out quickly. In this early stage, you can ex…