<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【Web技術】剖析前端異常及降級處理

          共 16533字,需瀏覽 34分鐘

           ·

          2021-08-20 21:12

          一、導讀

          “異常”一詞出自《后漢書.卷一.皇后紀上.光烈陰皇后紀》,表示非正常的,不同于平常的。在我們現實生活中同樣處處存在著異常,比如小縣城里的路燈年久失修...,上下班高峰期深圳的地鐵總是那么的擁擠...,人也總是時不時會生病等等; 由此可見,這個世界錯誤無處不在,這是一個基本的事實。

          而在計算機的世界中,異常指的是在程序運行過程中發(fā)生的異常事件,有些錯誤是由于外部環(huán)境導致的,有些錯誤是由于開發(fā)人員疏忽所導致的,有效的處理這些錯誤,保證計算機世界的正常運轉是我們開發(fā)人員必不可少的一環(huán)。

          二、背景

          隨著項目的不斷壯大,客戶的不斷接入,項目的穩(wěn)定性成為團隊的一大挑戰(zhàn)。

          當用戶或者團隊測試人員遇到問題時,大概率是直接丟給開發(fā)人員一張白屏頁面或錯誤UI的截圖,且該錯誤并不是必現時,讓前后端同學定位問題倍感頭痛。有沒有一種方式既能夠提升用戶體驗,又能夠幫助開發(fā)人員快速定位解決問題?

          本著“客戶就是上帝”的商業(yè)準則,為用戶創(chuàng)造良好的用戶體驗,是前端開發(fā)者職責之所在。當頁面發(fā)生錯誤的時候,相比于頁面崩潰或點不動,在適當的時機,以一種適當的方式去提醒用戶當前發(fā)生了什么,無疑是一種更友好的處理方式。

          項目中面臨下面幾種異常場景,需要處理:

          • 語法錯誤
          • 事件異常
          • HTTP請求異常
          • 靜態(tài)資源加載異常
          • Promise 異常
          • Iframe 異常
          • 頁面崩潰

          整體異常處理方案需要實現二方面的效果:

          1. 提升用戶體驗
          2. 上報監(jiān)控系統(tǒng),能及時早發(fā)現、定位、解決問題

          下面我們先從幾個異常場景出發(fā),逐步探討如何解決這些異常并給予更好的用戶體驗。

          三、錯誤類型

          在探討具體的解決方案之前,我們先來認識和熟悉一下前端的各種錯誤類型。

          ECMA-262規(guī)范定義的七種錯誤類型:

          • Error
          • EvalError
          • RangeError
          • ReferenceError
          • SyntaxError
          • TypeError
          • URIError

          Error

          Error是所有錯誤的基類,其他錯誤都繼承自該類型

          EvalError

          EvalError對象表示全局函數eval()中發(fā)生的錯誤。如果eval()中沒有錯誤,則不會拋出該錯誤。可以通過構造函數創(chuàng)建這個對象的實例

          image.png

          RangeError

          RangeError對象表示當一個值不在允許值的集合或范圍內時出現錯誤。

          image.png

          ReferenceError

          當引用不存在的變量時,該對象表示錯誤:

          image.png

          SyntaxError

          當JavaScript引擎在解析代碼時遇到不符合該語言語法的標記或標記順序時,將引發(fā)該異常:

          image.png

          TypeError

          傳遞給函數的操作數或實參與該操作符或函數期望的類型不兼容:

          image.png

          URIError

          當全局URI處理函數以錯誤的方式使用時:

          image.png

          四、處理和防范

          上文我們提到錯誤和異常無處不在,存在于各式各樣的應用場景中,那我們應該如何有效的攔截異常,將錯誤扼殺于搖籃之中,讓用戶無感呢?亦或者遇到致命錯誤時,進行降級處理?

          (1) try catch

          1.語法

          ECMA-262 第 3 版中引入了 try-catch作為 JavaScript 中處理異常的一種標準方式,基本的語法如下所示。

          try {
            // 可能會導致錯誤的代碼
          } catch (error) {
            // 在錯誤發(fā)生時怎么處理
          }
          復制代碼

          2.動機

          使用try...catch來捕獲異常,我歸納起來主要有兩個動機:

          1)是真真正正地想對可能發(fā)生錯誤的代碼進行異常捕獲;

          2)我想保證后面的代碼繼續(xù)運行。

          動機一沒什么好講的,在這里,我們講講動機二。假如我們有以下代碼:

          console.log(foo); //foo未定義
          console.log('I want running')
          復制代碼

          代碼一執(zhí)行,你猜怎么著?第一行語句報錯了,第二行語句的log也就沒打印出來。如果我們把代碼改成這樣:

          try{ 
              console.log(foo)
          }catch(e){
              console.log(e)
          }
          console.log('I want running');
          復制代碼

          以上代碼執(zhí)行之后,雖然還是報了個ReferenceError錯誤,但是后面的log卻能夠被執(zhí)行。

          從這個示例,我們可以看出,一旦前面的(同步)代碼出現了沒有被開發(fā)者捕獲的異常的話,那么后面的代碼就不會執(zhí)行了。所以,如果你希望當前可能出錯的代碼塊后續(xù)的代碼能夠正常運行的話,那么你就得使用try...catch來主動捕獲異常。

          擴展:

          實際上,出錯代碼是如何干擾后續(xù)代碼的執(zhí)行,是一個值得探討的主題。下面進行具體的探討。因為市面上瀏覽器眾多,對標準的實現也不太一致。所以,這里的結論僅僅是基于Chromev91.0.4472.114。探討過程中,我們涉及到兩組概念:同步代碼與異步代碼,代碼書寫期和代碼運行期。

          場景一:同步代碼(出錯) + 同步代碼

          1625024247(1).png

          可以看到,出錯的同步代碼后面的同步代碼不執(zhí)行了。

          場景二:同步代碼(出錯) + 異步代碼

          1625024396(1).png

          跟上面的情況一下,異步代碼也受到影響,也不執(zhí)行了。

          場景三:異步代碼(出錯) + 同步代碼

          image.png

          可以看到,異步代碼出錯,并不會影響后面同步代碼的執(zhí)行。

          場景四:異步代碼(出錯) + 異步代碼

          image.png

          出錯的異步代碼也不會影響后面異步代碼的執(zhí)行。

          如果只看場景一二三,很容易得出如下結論:在代碼運行期,同步代碼始終是先于異步代碼執(zhí)行的。如果先執(zhí)行的同步代碼沒有出錯的話,那么后面的代碼就會正常執(zhí)行,否則后面的代碼就不會執(zhí)行。但場景四卻打破了這個結論。我們不妨繼續(xù)看看場景五。

          場景五:異步代碼 + 同步代碼(出錯) + 異步代碼

          image.png

          看到了沒?同樣是異步代碼,按理說,代碼運行期,如果你是受出錯的同步代碼的影響的話,那你要么是兩個都不執(zhí)行,或者兩個都執(zhí)行啊?憑什么寫在出錯代碼代碼書寫期前面的異步代碼就能正常執(zhí)行,而寫在后面的就不執(zhí)行呢?經過驗證,在firefoxv75.0版本中也是同樣的表現。

          所以,到了這里,我們基本上可以得出這樣的結論:運行期,一先一后的兩個代碼中,出錯的一方代碼是如何影響另外一方代碼繼續(xù)執(zhí)行的問題中,跟異步代碼沒關系,只跟同步代碼有關系;跟代碼執(zhí)行期沒關系,只跟代碼書寫期有關系。

          說人話就是,異步代碼出錯與否都不會影響其他代碼繼續(xù)執(zhí)行。而出錯的同步代碼,如果它在代碼書寫期是寫在其他代碼之前,并且我們并沒有對它進行手動地去異常捕獲的話,那么它就會影響其他代碼(不論它是同步還是異步代碼)的繼續(xù)執(zhí)行。

          綜上所述,如果我們想要保證某塊可能出錯的同步代碼后面的代碼繼續(xù)執(zhí)行的話,那么我們必須對這塊同步代碼進行異常捕獲。

          3.范圍

          只能捕獲同步代碼所產生的運行時錯誤,對于語法錯誤和異步代碼所產生的錯誤是無能為力的。

          當遇到語法錯誤時:

          當遇到異步運行時錯誤時:

          (2) Promise.catch()

          1.語法

          const promise1 = new Promise((resolve, reject) => {
            throw 'Uh-oh!';
          });

          promise1.catch((error) => {
            console.error(error);
          });
          // expected output: Uh-oh!
          復制代碼

          2.動機

          用來捕獲promise代碼中的錯誤

          3.范圍

          使用Promise.prototype.catch()我們可以方便的捕獲到異常,現在我們來測試一下常見的語法錯誤、代碼錯誤以及異步錯誤。

          當遇到代碼錯誤時,可以捕獲:

          當遇到語法錯誤時,不能捕獲:

          當遇到異步運行時錯誤時,不能捕獲:

          1625033576(1).png

          (3) unhandledrejection

          1.用法

          unhandledrejection:當Promise 被 reject 且沒有 reject 處理器的時候,會觸發(fā) unhandledrejection 事件

          window.addEventListener("unhandledrejection"function(e){
            console.log(e);
          });
          復制代碼

          2.動機

          為了防止有漏掉的 Promise 異常,可以在全局增加一個對 unhandledrejection 的監(jiān)聽進行兜底,用來全局監(jiān)聽Uncaught Promise Error。

          3.范圍

              window.addEventListener("unhandledrejection"function (e) {
                console.log("捕獲到的promise異常:", e);
                e.preventDefault();
              });
              new Promise((res) => {
                console.log(a);
              });
              // 捕獲到的promise異常的: PromiseRejectionEvent
          復制代碼

          注意:此段代碼直接寫在控制臺是捕獲不到promise異常的,寫在html文件中可正常捕獲。

          (4) window.onerror

          1.用法

          當 JS 運行時錯誤發(fā)生時,window 會觸發(fā)一個 ErrorEvent 接口的 error 事件,并執(zhí)行 window.onerror()。

          window.onerror = function(message, source, lineno, colno, error) {
             console.log('捕獲到異常:',{message, source, lineno, colno, error});
          }
          復制代碼

          2.動機

          眾所周知,很多做錯誤監(jiān)控和上報的類庫就是基于這個特性來實現的,我們期待它能處理那些try...catch不能處理的錯誤。

          3.范圍

          根據MDN的說法,wondow.onerror能捕獲JavaScript運行時錯誤(包括語法錯誤)或一些資源錯誤。而在真正的測試過程中,wondow.onerror并不能捕獲語法錯誤。

          image.png

          經測試,window.onerror并不能捕獲語法錯誤和靜態(tài)資源的加載錯誤。同樣也不能捕獲異步代碼的錯誤,但是有一點值得注意的是,window.onerror能捕獲同樣是異步代碼的setTimeout和setInterval里面的錯誤。

          看來,寄予厚望的window.onerror并不是萬能的。

          (5) window.addEventListener

          1.用法

          window.addEventListener('error',(error)=>{console.log(error)})
          復制代碼

          2.動機

          當然是希望用他來兜住window.onerror和try catch的底,希望他能捕獲到異步錯誤和資源的加載錯誤。

          3.范圍

            <body>
              <img id="img" src="./fake.png" />
              <iframe id="iframe" src="./test4.html"></iframe>
            </body>
            <script>
              window.addEventListener(
                "error",
                function (error) {
                  console.log(error, "error");
                },
                true
              );
              setTimeout(() => {
                console.log(a);
              });
              new Promise((resolve, reject) => {
                console.log(a);
              });
              console.log(b)
              var f=e, //語法異常
            </script>
          復制代碼

          在此過程中,資源文件都是不存在的,我們發(fā)現window.addEventListener('error')依舊不能捕獲語法錯誤,Promise異常和iframe異常。

          對于語法錯誤我們可以在編譯過程中捕獲,,Promise異常已在上文中給出解決方案,現在還剩下iframe異常需要單獨處理了。

          (5) iframe異常

          1.用法

          window.frames[0].onerror = function (message, source, lineno, colno, error) {
              console.log('捕獲到 iframe 異常:',{message, source, lineno, colno, error});
              return true;
          };
          復制代碼

          2.動機

          用來專門捕獲iframe加載過程中的異常。

          3.范圍

          很遺憾,結果并不令人滿意,在實際的測試過程中,該方法未能捕獲到異常。

          (6) React中捕獲異常

          部分 UI 的 JavaScript 錯誤不應該導致整個應用崩潰,為了解決這個問題,React 16 引入了一個新的概念 —— 錯誤邊界。

          錯誤邊界是一種 React 組件,這種組件可以捕獲并打印發(fā)生在其子組件樹任何位置的 JavaScript 錯誤,并且,它會渲染出備用 UI,而不是渲染那些崩潰了的子組件樹。錯誤邊界在渲染期間、生命周期方法和整個組件樹的構造函數中捕獲錯誤。

          注意:錯誤邊界無法捕獲以下場景中產生的錯誤

          • 事件處理
          • 異步代碼(例如 setTimeout 或 requestAnimationFrame 回調函數)
          • 服務端渲染
          • 它自身拋出來的錯誤(并非它的子組件)

          如果一個 class 組件中定義了 static getDerivedStateFromError() 或 componentDidCatch() 這兩個生命周期方法中的任意一個(或兩個)時,那么它就變成一個錯誤邊界。當拋出錯誤后,請使用 static getDerivedStateFromError() 渲染備用 UI ,使用 componentDidCatch() 打印錯誤信息。

          class ErrorBoundary extends React.Component {
            constructor(props) {
              super(props);
              this.state = { hasError: false };
            }

            static getDerivedStateFromError(error) {
              // 更新 state 使下一次渲染能夠顯示降級后的 UI
              return { hasError: true };
            }

            componentDidCatch(error, errorInfo) {
              // 你同樣可以將錯誤日志上報給服務器
              logErrorToMyService(error, errorInfo);
            }

            render() {
              if (this.state.hasError) {
                // 你可以自定義降級后的 UI 并渲染
                return <h1>Something went wrong.</h1>;
              }

              return this.props.children; 
            }
          }
          復制代碼

          錯誤邊界的工作方式類似于 JavaScript 的 catch {},不同的地方在于錯誤邊界只針對 React 組件。只有 class 組件才可以成為錯誤邊界組件。大多數情況下, 你只需要聲明一次錯誤邊界組件, 并在整個應用中使用它。

          以上引用自React 官網。

          (7) Vue中捕獲異常

          Vue.config.errorHandler = function (err, vm, info) {
            // handle error
            // `info` 是 Vue 特定的錯誤信息,比如錯誤所在的生命周期鉤子
            // 只在 2.2.0+ 可用
          }
          復制代碼

          指定組件的渲染和觀察期間未捕獲錯誤的處理函數。這個處理函數被調用時,可獲取錯誤信息和 Vue 實例。

          • 從 2.2.0 起,這個鉤子也會捕獲組件生命周期鉤子里的錯誤。同樣的,當這個鉤子是 undefined 時,被捕獲的錯誤會通過 console.error 輸出而避免應用崩潰。
          • 從 2.4.0 起,這個鉤子也會捕獲 Vue 自定義事件處理函數內部的錯誤了。
          • 從 2.6.0 起,這個鉤子也會捕獲 v-on DOM 監(jiān)聽器內部拋出的錯誤。另外,如果任何被覆蓋的鉤子或處理函數返回一個 Promise 鏈 (例如 async 函數),則來自其 Promise 鏈的錯誤也會被處理。

          以上引用自Vue 官網。

          (8) http請求異常

          1.用法

          以axios為例,添加響應攔截器

          axios.interceptors.response.use(function (response) {
              // 對響應數據做點什么
              // response 是請求回來的數據
              return response;
            }, function (error) {
              // 對響應錯誤做點什么
              return Promise.reject(error)
            }
          )

          復制代碼

          2.動機

          用來專門捕獲HTTP請求異常

          五、項目實踐

          在提出了這么多的解決方案之后,相信大家對具體怎么用還是存在一些疑惑。那么接下來,我們真正的進入實踐階段吧!

          我們再次回顧一下我們需要解決的問題是什么?

          • 語法錯誤
          • 事件異常
          • HTTP請求異常
          • 靜態(tài)資源加載異常
          • Promise 異常
          • Iframe 異常
          • 頁面崩潰

          捕獲異常是我們的最終目標嗎?并不是,回到解決問題的背景下,相比于頁面崩潰或點不動,在適當的時機,以一種適當的方式去提醒用戶當前發(fā)生了什么,無疑是一種更友好的處理方式。

          結合到項目中,具體實踐起來有如下兩種方案:

          • 1.代碼中通過大量的try catch/Promise.catch來捕獲,捕獲不到的使用其他方式進行兜底
          • 2.通過框架提供的機制來做,再對不能捕獲的進行兜底

          方案一無疑不是很聰明的樣子...這意味著要去改大量的原有代碼,心智負擔成倍數增加。方案二則更加明智,通過在底層對錯誤進行統(tǒng)一處理,無需變更原有邏輯。

          到項目中,使用的是React框架,React正好提供了一種捕獲異常的機制(上文已提及)并做降級處理,但是細心的小伙伴發(fā)現了,react并不能捕獲如下四種錯誤:

          • 事件處理
          • 異步代碼(例如 setTimeout 或 requestAnimationFrame 回調函數)
          • 服務端渲染
          • 它自身拋出來的錯誤(并非它的子組件)

          對于第三點服務端渲染錯誤,項目中并沒有適用的場景,此次不做重點分析。我們重點分析第一點和第二點。

          我在這里先拋出幾個問題,大家先做短暫的思考:

          • 1.若事件處理和異步代碼的錯誤導致頁面crash,我們該如何預防?
          • 2.如何對ErrorBounary進行兜底?相比一個按鈕點擊無效,如何更加友好的提示用戶?

          先來看第一個問題,若事件處理和異步代碼的錯誤導致頁面崩潰:

          const Test = () => {
            const [data, setData] = useState([]);
            return (
              <div
                onClick={() => {
                  setData('');
                }}
              >
                {data.map((s) => s.i)}
              </div>
            );
          };
          復制代碼

          此段代碼在正常渲染期間是沒問題的,但在觸發(fā)了點擊事件之后會導致頁面異常白屏,如果在外面套上我們的ErrorBounday組件,情況會是怎么樣呢?

          答案是依然能夠捕獲到錯誤,并能夠對該組件進行降級處理!

          此時有些小伙伴已經察覺到了,錯誤邊界只要是在渲染期間都是可以捕獲錯誤的,無論首次渲染還是二次渲染。流程圖如下:

          image.png

          第一個問題原來根本就不是問題,這本身就是一個閉環(huán),不用我們解決!

          再來看看第二個問題:

          對于事件處理和異步代碼中不會導致頁面崩潰的代碼:

          const Test = () => {
            return (
              <button
                onClick={() => {
                  [].map((s) => s.a.b);
                }}
              >
                點擊
              </button>
            );
          };
          復制代碼

          button按鈕可正常點擊,但是該點擊事件的內部邏輯是有問題的,導致用戶點擊該按鈕本質是無效的。此時若不及時給與友好提示,用戶只會陷入抓狂中....

          那么有沒有辦法對ErrorBoundary進行兜底呢?即可以捕獲異步代碼或事件處理中的錯誤。

          上文提到的window.addEventListener('error')正好可以解決這個問題。理想狀態(tài)下:

          而真正的執(zhí)行順序確實這樣的:

          1625105438(1).png

          在真正執(zhí)行的過程中,window.addEventListener('error')是先于ErrorBoundary捕獲到錯誤的,這就導致當error事件捕獲到錯誤時,他并不知道該錯誤是否會導致頁面崩潰,不知道該給予怎樣的提示,到底是對頁面進行降級處理還是只做簡單的報錯提示?

          問題似乎就卡在這了....

          那能否通過一種有效的途徑告訴error事件:ErrorBoundary已經捕獲到了錯誤,你不需要處理!亦或者是ErrorBoundary未能捕獲到錯誤,這是一個異步錯誤/事件錯誤,但不會引起頁面崩潰,你只需要提示用戶!

          答案肯定是有的,比如建立一個nodeJs服務器,通過webSocket去通知,但是這樣做不僅麻煩,還會有一定的延遲。

          在筆者苦思冥想之際,在某個靜悄悄的夜晚,突然靈感一現。為什么我們非要按照他規(guī)定的順序執(zhí)行呢?我們能不能嘗試改變他的執(zhí)行順序,讓錯誤捕獲回到我們理想中的流程來呢?

          改變思路之后,我們再思考有什么能改變代碼執(zhí)行順序嗎?沒錯,異步事件!

                window.addEventListener('error'function (error) {
                  setTimeout(()=>{
                    console.log(error, 'error錯誤');
                  })
                });
          復制代碼

          當給error事件的回調函數加入setTimeout后,捕獲異常的流程為:

          image.png

          現在就可以通知error事件到底頁面崩潰了沒有,到底需不需要它的處理!上代碼:

          class ErrorBoundary extends React.Component {
            constructor(props) {
              super(props);
              this.state = { hasError: false };
            }

            static getDerivedStateFromError(error) {
              // 更新 state 使下一次渲染能夠顯示降級后的 UI
              return { hasError: true };
            }

            componentDidCatch(error, errorInfo) {
              // 你同樣可以將錯誤日志上報給服務
              logErrorToMyService(error, errorInfo);
              
              //告訴error事件 ErrorBoundary已處理異常
               localStorage.setItem("ErrorBoundary",true)
            }

            render() {
              if (this.state.hasError) {
                // 你可以自定義降級后的 UI 并渲染
                return <h1>Something went wrong.</h1>;
              }

              return this.props.children; 
            }
          }
          復制代碼
            window.addEventListener('error'function (error) {
                  setTimeout(() => {
                    //進來代表一定有錯誤 判斷ErrorBoundary中是否已處理異常
                    const flag = localStorage.getItem('ErrorBounary');
                    if (flag) {
                      //進入了ErrorBounary 錯誤已被處理 error事件不用處理該異常
                      localStorage.setItem('ErrorBounary'false); //重置狀態(tài)
                    } else {
                      //未進入ErrorBounary 代表此錯誤為異步錯誤/事件錯誤
                      logErrorToMyService(error, errorInfo);  // 你可以將錯誤日志上報給服務
                      //判斷具體錯誤類型
                      if (error.message.indexOf('TypeError')) {
                        alert('這是一個TypeError錯誤,請通知開發(fā)人員');
                      } else if (error.message.indexOf('SyntaxError')) {
                        alert('這是一個SyntaxError錯誤,請通知開發(fā)人員');
                      } else {
                        //在此次給與友好提示
                      }
                    }
                  });
                });
          復制代碼

          最后,通過我們的努力,當頁面崩潰時,及時進行降級處理;當頁面未崩潰,但有錯誤時,我們及時的告知用戶,并對錯誤進行上報,達到預期的效果。

          六、擴展

          1.設置采集率

          若是錯誤實在太多,比如有時候代碼進入死循環(huán),錯誤量過多導致服務器壓力大時,可酌情降低采集率。比如采集30%:

                if (Math.random() < 0.3) {
                  //上報錯誤
                  logErrorToMyService(error, errorInfo);
                }
          復制代碼

          2.提效

          解決上面這些問題后,大家難免會有疑問:那每一個組件都要去套一層ErrorBoundary組件,這工作量是不是有點大....而且有一些老代碼,嵌套的比較深,改起來心理負擔也會比較大。那有沒有辦法將其作為一個配置項,配置完之后,編譯時自動套上一層ErrorBoundary組件呢?這個我們下次在做探討!

          3.可配置

          能否將ErrorBoundary擴展成可傳入自定義UI的組件呢?這樣大家通過定制化UI,在不同的場景進行不同的降級處理。

          同樣,這一塊我們下次再討論!

          七、總結

          異常處理是高質量軟件開發(fā)中的一個基本部分,但是在許多情況下,它們會被忽略,或者是不正確的使用,而處理異常只是保證代碼流程不出錯,重定向到正確的程序流中去。

          本文從前端錯誤類型出發(fā),從try catch逐步揭開錯誤異常神秘的面紗,再通過一系列的操作對異常進行監(jiān)控和捕獲,最后達到提升用戶體驗,上報監(jiān)控系統(tǒng)的效果。

          八、思考

          • Promise.catch 和 try catch 捕獲異常有什么區(qū)別?
          • ErrorBounary內部如何實現?
          • 為什么unhandledrejection寫在控制臺是捕獲不到錯誤的?而寫在HTML文件中就可以捕獲到?
          • 服務端渲染錯誤如何捕獲?

          帶著這些思考,我們下次見~

          關于本文

          來源:縱有疾風起

          https://juejin.cn/post/6979564690787532814


          1. JavaScript 重溫系列(22篇全)
          2. ECMAScript 重溫系列(10篇全)
          3. JavaScript設計模式 重溫系列(9篇全)
          4. 正則 / 框架 / 算法等 重溫系列(16篇全)
          5. Webpack4 入門(上)|| Webpack4 入門(下)
          6. MobX 入門(上) ||  MobX 入門(下)
          7. 120+篇原創(chuàng)系列匯總

          回復“加群”與大佬們一起交流學習~

          點擊“閱讀原文”查看 120+ 篇原創(chuàng)文章

          瀏覽 23
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产欧美第一页 | 天天综合日本网 | 影音先锋成人av电影 | 69无码 | 蜜臀久久精品久久久久 |