<address id="ttjl9"></address>

      <noframes id="ttjl9"><address id="ttjl9"><nobr id="ttjl9"></nobr></address>
      <form id="ttjl9"></form>
        <em id="ttjl9"><span id="ttjl9"></span></em>
        <address id="ttjl9"></address>

          <noframes id="ttjl9"><form id="ttjl9"></form>

          首頁

          Web安全之CSRF實例解析

          seo達人

          前言

          文章首次發表在 個人博客


          之前寫過一篇 web安全之XSS實例解析,是通過舉的幾個簡單例子講解的,同樣通過簡單得例子來理解和學習CSRF,有小伙伴問實際開發中有沒有遇到過XSS和CSRF,答案是有遇到過,不過被測試同學發現了,還有安全掃描發現了可能的問題,這兩篇文章就是簡化了一下當時實際遇到的問題。


          CSRF

          跨站請求偽造(Cross Site Request Forgery),是指黑客誘導用戶打開黑客的網站,在黑客的網站中,利用用戶的登陸狀態發起的跨站請求。CSRF攻擊就是利用了用戶的登陸狀態,并通過第三方的站點來做一個壞事。


          要完成一次CSRF攻擊,受害者依次完成兩個步驟:


          登錄受信任網站A,并在本地生成Cookie

          在不登出A的情況,訪問危險網站B

          CSRF攻擊


          在a.com登陸后種下cookie, 然后有個支付的頁面,支付頁面有個誘導點擊的按鈕或者圖片,第三方網站域名為 b.com,中的頁面請求 a.com的接口,b.com 其實拿不到cookie,請求 a.com會把Cookie自動帶上(因為Cookie種在 a.com域下)。這就是為什么在服務端要判斷請求的來源,及限制跨域(只允許信任的域名訪問),然后除了這些還有一些方法來防止 CSRF 攻擊,下面會通過幾個簡單的例子來詳細介紹 CSRF 攻擊的表現及如何防御。


          下面會通過一個例子來講解 CSRF 攻擊的表現是什么樣子的。

          實現的例子:

          在前后端同域的情況下,前后端的域名都為 http://127.0.0.1:3200, 第三方網站的域名為 http://127.0.0.1:3100,釣魚網站頁面為 http://127.0.0.1:3100/bad.html。


          平時自己寫例子中會用到下面這兩個工具,非常方便好用:

          http-server: 是基于node.js的HTTP 服務器,它最大的好處就是:可以使用任意一個目錄成為服務器的目錄,完全拋開后端的沉重工程,直接運行想要的js代碼;

          nodemon: nodemon是一種工具,通過在檢測到目錄中的文件更改時自動重新啟動節點應用程序來幫助開發基于node.js的應用程序

          前端頁面: client.html


          <!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>CSRF-demo</title>

             <style>

                 .wrap {

                     height: 500px;

                     width: 300px;

                     border: 1px solid #ccc;

                     padding: 20px;

                     margin-bottom: 20px;

                 }

                 input {

                     width: 300px;

                 }

                 .payInfo {

                     display: none;

                 }

                 .money {

                     font-size: 16px;

                 }

             </style>

          </head>


          <body>

             <div class="wrap">

                 <div class="loginInfo">

                     <h3>登陸</h3>

                     <input type="text" placeholder="用戶名" class="userName">

                     <br>

                     <input type="password" placeholder="密碼" class="password">

                     <br>

                     <br>

                     <button class="btn">登陸</button>

                 </div>

                 

                 

                 <div class="payInfo">

                     <h3>轉賬信息</h3>

                     <p >當前賬戶余額為 <span class="money">0</span>元</p>

                     <!-- <input type="text" placeholder="收款方" class="account"> -->

                     <button class="pay">支付10元</button>

                     <br>

                     <br>

                     <a target="_blank">

                         聽說點擊這個鏈接的人都賺大錢了,你還不來看一下么

                     </a>

                 </div>

             </div>

          </body>

          <script>

             const btn = document.querySelector('.btn');

             const loginInfo = document.querySelector('.loginInfo');

             const payInfo = document.querySelector('.payInfo');

             const money = document.querySelector('.money');

             let currentName = '';

             // 第一次進入判斷是否已經登陸

             Fetch('http://127.0.0.1:3200/isLogin', 'POST', {})

             .then((res) => {

                 if(res.data) {

                     payInfo.style.display = "block"

                     loginInfo.style.display = 'none';

                     Fetch('http://127.0.0.1:3200/pay', 'POST', {userName: currentName, money: 0})

                     .then((res) => {

                         money.innerHTML = res.data.money;

                     })

                 } else {

                     payInfo.style.display = "none"

                     loginInfo.style.display = 'block';

                 }

                 

             })

             // 點擊登陸

             btn.onclick = function () {

                 var userName = document.querySelector('.userName').value;

                 currentName = userName;

                 var password = document.querySelector('.password').value;

                 Fetch('http://127.0.0.1:3200/login', 'POST', {userName, password})

                 .then((res) => {

                     payInfo.style.display = "block";

                     loginInfo.style.display = 'none';

                     money.innerHTML = res.data.money;

                 })

             }

             // 點擊支付10元

             const pay = document.querySelector('.pay');

             pay.onclick = function () {

                 Fetch('http://127.0.0.1:3200/pay', 'POST', {userName: currentName, money: 10})

                 .then((res) => {

                     console.log(res);

                     money.innerHTML = res.data.money;

                 })

             }

             // 封裝的請求方法

             function Fetch(url, method = 'POST', data) {

                 return new Promise((resolve, reject) => {

                     let options = {};

                     if (method !== 'GET') {

                         options = {

                             headers: {

                                 'Content-Type': 'application/json',

                             },

                             body: JSON.stringify(data),

                         }

                     }

                     fetch(url, {

                         mode: 'cors', // no-cors, cors, *same-origin

                         method,

                         ...options,

                         credentials: 'include',

                     }).then((res) => {

                         return res.json();

                     }).then(res => {

                         resolve(res);

                     }).catch(err => {

                         reject(err);

                     });

                 })

             }

             

          </script>


          </html>

          實現一個簡單的支付功能:


          會首先判斷有沒有登錄,如果已經登陸過,就直接展示轉賬信息,未登錄,展示登陸信息

          登陸完成之后,會展示轉賬信息,點擊支付,可以實現金額的扣減

          后端服務: server.js


          const Koa = require("koa");

          const app = new Koa();

          const route = require('koa-route');

          const bodyParser = require('koa-bodyparser');

          const cors = require('@koa/cors');

          const KoaStatic = require('koa-static');


          let currentUserName = '';


          // 使用  koa-static  使得前后端都在同一個服務下

          app.use(KoaStatic(__dirname));


          app.use(bodyParser()); // 處理post請求的參數


          // 初始金額為 1000

          let money = 1000;


          // 調用登陸的接口

          const login = ctx => {

             const req = ctx.request.body;

             const userName = req.userName;

             currentUserName = userName;

             // 簡單設置一個cookie

             ctx.cookies.set(

                 'name',

                 userName,

                 {

                   domain: '127.0.0.1', // 寫cookie所在的域名

                   path: '/',       // 寫cookie所在的路徑

                   maxAge: 10 * 60 * 1000, // cookie有效時長

                   expires: new Date('2021-02-15'),  // cookie失效時間

                   overwrite: false,  // 是否允許重寫

                   SameSite: 'None',

                 }

               )

             ctx.response.body = {

                 data: {

                     money,

                 },

                 msg: '登陸成功'

             };

          }

          // 調用支付的接口

          const pay = ctx => {

             if(ctx.method === 'GET') {

                 money = money - Number(ctx.request.query.money);

             } else {

                 money = money - Number(ctx.request.body.money);

             }

             ctx.set('Access-Control-Allow-Credentials', 'true');

             // 根據有沒有 cookie 來簡單判斷是否登錄

             if(ctx.cookies.get('name')){

                 ctx.response.body = {

                     data: {

                         money: money,

                     },

                     msg: '支付成功'

                 };

             }else{

                 ctx.body = '未登錄';

             }

          }


          // 判斷是否登陸

          const isLogin = ctx => {

             ctx.set('Access-Control-Allow-Credentials', 'true');


             if(ctx.cookies.get('name')){

                 ctx.response.body = {

                     data: true,

                     msg: '登陸成功'

                 };


             }else{

                 ctx.response.body = {

                     data: false,

                     msg: '未登錄'

                 };

             }

          }

          // 處理 options 請求

          app.use((ctx, next)=> {

             const headers = ctx.request.headers;

             if(ctx.method === 'OPTIONS') {

                 ctx.set('Access-Control-Allow-Origin', headers.origin);

                 ctx.set('Access-Control-Allow-Headers', 'Content-Type');

                 ctx.set('Access-Control-Allow-Credentials', 'true');

                 ctx.status = 204;

             } else {

                 next();

             }

          })


          app.use(cors());

          app.use(route.post('/login', login));

          app.use(route.post('/pay', pay));

          app.use(route.get('/pay', pay));

          app.use(route.post('/isLogin', isLogin));


          app.listen(3200, () => {

             console.log('啟動成功');

          });

          執行 nodemon server.js,訪問頁面 http://127.0.0.1:3200/client.html


          CSRF-demo


          登陸完成之后,可以看到Cookie是種到 http://127.0.0.1:3200 這個域下面的。


          第三方頁面 bad.html


          <!DOCTYPE html>

          <html lang="en">

          <head>

             <meta charset="UTF-8">

             <meta name="viewport" content="width=device-width, initial-scale=1.0">

             <title>第三方網站</title>

          </head>

          <body>

             <div>

                 哈哈,小樣兒,哪有賺大錢的方法,還是踏實努力工作吧!

                 <!-- form 表單的提交會伴隨著跳轉到action中指定 的url 鏈接,為了阻止這一行為,可以通過設置一個隱藏的iframe 頁面,并將form 的target 屬性指向這個iframe,當前頁面iframe則不會刷新頁面 -->

                 <form action="http://127.0.0.1:3200/pay" method="POST" class="form" target="targetIfr" style="display: none">

                     <input type="text" name="userName" value="xiaoming">

                     <input type="text" name="money" value="100">

                 </form>

                 <iframe name="targetIfr" style="display:none"></iframe>

             </div>

          </body>

          <script>

             document.querySelector('.form').submit();

          </script>

          </html>

          使用 HTTP-server 起一個 本地端口為 3100的服務,就可以通過 http://127.0.0.1:3100/bad.html 這個鏈接來訪問,CSRF攻擊需要做的就是在正常的頁面上誘導用戶點擊鏈接進入這個頁面

          CSRF-DEMO


          點擊誘導鏈接,跳轉到第三方的頁面,第三方頁面自動發了一個扣款的請求,所以在回到正常頁面的時候,刷新,發現錢變少了。

          我們可以看到在第三方頁面調用 http://127.0.0.1:3200/pay 這個接口的時候,Cookie自動加在了請求頭上,這就是為什么 http://127.0.0.1:3100/bad.html 這個頁面拿不到 Cookie,但是卻能正常請求 http://127.0.0.1:3200/pay 這個接口的原因。


          CSRF攻擊大致可以分為三種情況,自動發起Get請求, 自動發起POST請求,引導用戶點擊鏈接。下面會分別對上面例子進行簡單的改造來說明這三種情況


          自動發起Get請求

          在上面的 bad.html中,我們把代碼改成下面這樣


          <!DOCTYPE html>

          <html>

           <body>

             <img src="http://127.0.0.1:3200/payMoney?money=1000">

           </body>

          </html>

          當用戶訪問含有這個img的頁面后,瀏覽器會自動向自動發起 img 的資源請求,如果服務器沒有對該請求做判斷的話,那么會認為這是一個正常的鏈接。


          自動發起POST請求

          上面例子中演示的就是這種情況。


          <body>

             <div>

                 哈哈,小樣兒,哪有賺大錢的方法,還是踏實努力工作吧!

                 <!-- form 表單的提交會伴隨著跳轉到action中指定 的url 鏈接,為了阻止這一行為,可以通過設置一個隱藏的iframe 頁面,并將form 的target 屬性指向這個iframe,當前頁面iframe則不會刷新頁面 -->

                 <form action="http://127.0.0.1:3200/pay" method="POST" class="form" target="targetIfr">

                     <input type="text" name="userName" value="xiaoming">

                     <input type="text" name="money" value="100">

                 </form>

                 <iframe name="targetIfr" style="display:none"></iframe>

             </div>

          </body>

          <script>

             document.querySelector('.form').submit();

          </script>

          上面這段代碼中構建了一個隱藏的表單,表單的內容就是自動發起支付的接口請求。當用戶打開該頁面時,這個表單會被自動執行提交。當表單被提交之后,服務器就會執行轉賬操作。因此使用構建自動提交表單這種方式,就可以自動實現跨站點 POST 數據提交。


          引導用戶點擊鏈接

          誘惑用戶點擊鏈接跳轉到黑客自己的網站,示例代碼如圖所示


          <a >聽說點擊這個鏈接的人都賺大錢了,你還不來看一下么</a>

          用戶點擊這個地址就會跳到黑客的網站,黑客的網站可能會自動發送一些請求,比如上面提到的自動發起Get或Post請求。


          如何防御CSRF

          利用cookie的SameSite

          SameSite有3個值: Strict, Lax和None


          Strict。瀏覽器會完全禁止第三方cookie。比如a.com的頁面中訪問 b.com 的資源,那么a.com中的cookie不會被發送到 b.com服務器,只有從b.com的站點去請求b.com的資源,才會帶上這些Cookie

          Lax。相對寬松一些,在跨站點的情況下,從第三方站點鏈接打開和從第三方站點提交 Get方式的表單這兩種方式都會攜帶Cookie。但如果在第三方站點中使用POST方法或者通過 img、Iframe等標簽加載的URL,這些場景都不會攜帶Cookie。

          None。任何情況下都會發送 Cookie數據

          我們可以根據實際情況將一些關鍵的Cookie設置 Stirct或者 Lax模式,這樣在跨站點請求的時候,這些關鍵的Cookie就不會被發送到服務器,從而使得CSRF攻擊失敗。


          驗證請求的來源點

          由于CSRF攻擊大多來自第三方站點,可以在服務器端驗證請求來源的站點,禁止第三方站點的請求。

          可以通過HTTP請求頭中的 Referer和Origin屬性。


          HTTP請求頭


          但是這種 Referer和Origin屬性是可以被偽造的,碰上黑客高手,這種判斷就是不安全的了。


          CSRF Token

          最開始瀏覽器向服務器發起請求時,服務器生成一個CSRF Token。CSRF Token其實就是服務器生成的字符串,然后將該字符串種植到返回的頁面中(可以通過Cookie)

          瀏覽器之后再發起請求的時候,需要帶上頁面中的 CSRF Token(在request中要帶上之前獲取到的Token,比如 x-csrf-token:xxxx), 然后服務器會驗證該Token是否合法。第三方網站發出去的請求是無法獲取到 CSRF Token的值的。

          其他知識點補充

          1. 第三方cookie

          Cookie是種在服務端的域名下的,比如客戶端域名是 a.com,服務端的域名是 b.com, Cookie是種在 b.com域名下的,在 Chrome的 Application下是看到的是 a.com下面的Cookie,是沒有的,之后,在a.com下發送b.com的接口請求會自動帶上Cookie(因為Cookie是種在b.com下的)


          2. 簡單請求和復雜請求

          復雜請求需要處理option請求。


          之前寫過一篇特別詳細的文章 CORS原理及@koa/cors源碼解析,有空可以看一下。


          3. Fetch的 credentials 參數

          如果沒有配置credential 這個參數,fetch是不會發送Cookie的


          credential的參數如下


          include:不論是不是跨域的請求,總是發送請求資源域在本地的Cookies、HTTP Basic anthentication等驗證信息

          same-origin:只有當URL與響應腳本同源才發送 cookies、 HTTP Basic authentication 等驗證信息

          omit: 從不發送cookies.

          平常寫一些簡單的例子,從很多細節問題上也能補充自己的一些知識盲點。

          寫一個腳本,將所有js文件后綴批量改成ts后綴

          seo達人

          做項目的時候準備把js項目重構成ts項目,需要把文件后綴改成ts,一個bat腳本搞定,命令如下:

          @echo off
          
          rem 正在搜索...
          
          for /f "delims=" %%i in ('dir /b /a-d /s "*.js"') do ren "%%i" "%%~ni.ts" rem 搜索完畢 @pause

          把腳本放到根目錄下,雙擊運行完就可以了

          六個好用的程序員開發在線工具

          seo達人

          網上可以找到前端開發社區貢獻的大量工具,這篇文章列出了我最喜歡的一些工具,這些工具給我的工作帶來了許多便利。


          1. EnjoyCSS


          老實說,雖然我做過許多前端開發,但我并不擅長 CSS。當我陷入困境時,EnjoyCSS 是我的大救星。EnjoyCSS 提供了一個簡單的交互界面,幫助我設計元素,然后自動輸出相應的 CSS 代碼。




          EnjoyCSS 可以輸出 CSS、LESS、SCSS 代碼,并支持指定需要支持哪些瀏覽器及其版本。開發簡單頁面時用起來比較方便,但不太適合復雜一點的前端項目(這類項目往往需要引入 CSS 框架)。

          2. Prettier Playground


          Prettier 是一個代碼格式化工具,支持格式化 JavaScript 代碼(包括 ES2017、JSX、Angular、Vue、Flow、TypeScript 等)。Prettier 會移除代碼原本的樣式,替換為遵循最佳實踐的標準化、一致的樣式。IDE 大多支持 Prettier 工具,不過 Prettier 也有在線版本,讓你可以在瀏覽器里格式化代碼。




          如果工作電腦不在手邊,使用移動端設備或者臨時借用別人的電腦查看代碼時,Prettier Playground 非常好用。相比在 IDE 或編輯器下使用 Prettier,個人更推薦通過 git pre-commit hook 配置 Prettier:hook 可以保證整個團隊使用統一的配置,免去各自分別配置 IDE 或編輯器的麻煩。如果是老項目,hook 還可以設置只格式化有改動的單個文件甚至有改動的代碼段,避免在 IDE 或編輯器下使用 Prettier 時不小心格式了大量代碼,淹沒了 commit 的主要改動,讓 review 代碼變得十分痛苦。

          3. Postman


          Postman 一直在我的開發工具箱里,測試后端 API 接口時非常好用。GET、POST、DELETE、OPTIONS、PUT 這些方法都支持。毫無疑問,你應該使用這個工具。




          Postman 之外,Insomnia 也是很流行的 REST API 測試工具,亮點是支持 GraphQL。不過 Postman 從 去年夏天發布的 v7.2 起也支持了 GraphQL。

          4. StackBlitz


          Chidume Nnamdi 盛贊這是每個用戶最喜歡的在線 IDE。StackBlitz 將大家最喜歡、最常用的 IDE Visual Studio Code 搬進了瀏覽器。


          StackBlitz 支持一鍵配置 Angular、React、Ionic、TypeScript、RxJS、Svelte 等 JavaScript 框架,也就是說,只需幾秒你就可以開始寫代碼了。


          我覺得這個在線 IDE 很有用,特別是可以在線嘗試一些樣例代碼或者庫,否則僅僅嘗試一些新特性就需要花很多時間在新項目初始化配置上。有了 StackBlitz,無需在本地從頭搭建環境,花上幾分鐘就可以試用一個 NPM 包。很棒,不是嗎?




          微軟官方其實也提供了在線版本的 VSCode,可以在瀏覽器內使用 VSCode,并且支持開發 Node.js 項目(基于 Azure)。不過 StackBlitz 更專注于優化前端開發體驗,界面更加直觀一點,也推出了 beta 版本的 Node.js 支持(基于 GCP,需要填表申請)。

          5. Bit.dev


          軟件開發的基本原則之一就是代碼復用。代碼復用減少了開發量,讓你不用從頭開發組件。


          這正是 Bit.dev 做的事,分享可重用的組件和片段,降低開發量,加速開發進程。


          除了公開分享,它還支持在團隊分享,讓團隊協作更方便。


          正如 Bit.dev 的口號「組件即設計體系。協同開發更好的組件。」所言,Bit.dev 可以用來創建設計體系,允許團隊內的開發者和設計師一起協作,從頭搭建一套設計體系。


          Bit.dev 目前支持 React、Vue、Angular、Node 及其他 JavaScript 框架。




          在 Bit.dev 上不僅可以搜索組件,還可以直接查看組件的依賴,瀏覽組件的代碼,甚至在線編輯代碼并查看預覽效果!選好組件后可以通過 Bit.dev 的命令行工具 bit 在本地項目引入組件,也可以通過 npm、yarn 引入組件。

          6. CanIUse


          CanIUse是非常好用的在線工具,可以方便地查看各大瀏覽器對某個特性的支持程度。


          我過去經常碰到自己開發的應用的一些功能在其他瀏覽器下不支持的情況。比如我的作品集項目使用的某個特性在 Safari 下不支持,直到項目上線幾個月后我才意識到。這些經驗教訓讓我意識到需要檢查瀏覽器兼容性。


          我們來看一個例子吧。哪些瀏覽器支持 WebP 圖像格式?




          如你所見,Safari 和 IE 目前不支持 WebP。這意味著需要為不兼容的瀏覽器提供回退選項,比如:


          <picture>

          CanIUse 還可以在命令行下使用,例如,在命令行下查看 WebP 圖像格式的瀏覽器兼容性:caniuse webp(運行命令前需要事先通過 npm install -g caniuse-cmd安裝命令行工具。


          10 個超有用的 JavaScript 技巧

          seo達人

          方法參數的驗證

          JavaScript 允許你設置參數的默認值。通過這種方法,可以通過一個巧妙的技巧來驗證你的方法參數。


          const isRequired = () => { throw new Error('param is required'); };

          const print = (num = isRequired()) => { console.log(`printing ${num}`) };

          print(2);//printing 2

          print()// error

          print(null)//printing null

          非常整潔,不是嗎?


          格式化 json 代碼

          你可能對 JSON.stringify 非常熟悉。但是你是否知道可以用 stringify 進行格式化輸出?實際上這很簡單。


          stringify 方法需要三個輸入。 value,replacer 和 space。后兩個是可選參數。這就是為什么我們以前沒有注意過它們。要對 json 進行縮進,必須使用 space 參數。


          console.log(JSON.stringify({name:"John",Age:23},null,'\t'));

          >>>

          {

          "name": "John",

          "Age": 23

          }

          從數組中獲取唯一值

          要從數組中獲取唯一值,我們需要使用 filter 方法來過濾出重復值。但是有了新的 Set 對象,事情就變得非常順利和容易了。


          let uniqueArray = [...new Set([1, 2, 3, 3, 3, "school", "school", 'ball', false, false, true, true])];

          >>> [1, 2, 3, "school", "ball", false, true]

          從數組中刪除虛值(Falsy Value)

          在某些情況下,你可能想從數組中刪除虛值。虛值是 JavaScript 的 Boolean 上下文中被認定為為 false 的值。 JavaScript 中只有六個虛值,它們是:


          undefined

          null

          NaN

          0

          "" (空字符串)

          false

          濾除這些虛值的最簡單方法是使用以下函數。


          myArray.filter(Boolean);

          如果要對數組進行一些修改,然后過濾新數組,可以嘗試這樣的操作。請記住,原始的 myArray 會保持不變。


          myArray

             .map(item => {

                 // Do your changes and return the new item

             })

             .filter(Boolean);

          合并多個對象

          假設我有幾個需要合并的對象,那么這是我的首選方法。


          const user = {

              name: 'John Ludwig',

              gender: 'Male'

          };

          const college = {

              primary: 'Mani Primary School',

              secondary: 'Lass Secondary School'

          };

          const skills = {

             programming: 'Extreme',

             swimming: 'Average',

             sleeping: 'Pro'

          };

          const summary = {...user, ...college, ...skills};

          這三個點在 JavaScript 中也稱為展開運算符。你可以在這里學習更多用法。


          對數字數組進行排序

          JavaScript 數組有內置的 sort 方法。默認情況下 sort 方法把數組元素轉換為字符串,并對其進行字典排序。在對數字數組進行排序時,這有可能會導致一些問題。所以下面是解決這類問題的簡單解決方案。


          [0,10,4,9,123,54,1].sort((a,b) => a-b);

          >>> [0, 1, 4, 9, 10, 54, 123]

          這里提供了一個將數字數組中的兩個元素與 sort 方法進行比較的函數。這個函數可幫助我們接收正確的輸出。


          Disable Right Click

          禁用右鍵

          你可能想要阻止用戶在你的網頁上單擊鼠標右鍵。


          <body oncontextmenu="return false">

             <div></div>

          </body>

          這段簡單的代碼將為你的用戶禁用右鍵單擊。


          使用別名進行解構

          解構賦值語法是一種 JavaScript 表達式,可以將數組中的值或對象的值或屬性分配給變量。解構賦值能讓我們用更簡短的語法進行多個變量的賦值。


          const object = { number: 10 };


          // Grabbing number

          const { number } = object;


          // Grabbing number and renaming it as otherNumber

          const { number: otherNumber } = object;

          console.log(otherNumber); //10

          獲取數組中的最后一項

          可以通過對 splice 方法的參數傳入負整數,來數獲取組末尾的元素。


          let array = [0, 1, 2, 3, 4, 5, 6, 7]

          console.log(array.slice(-1));

          >>>[7]

          console.log(array.slice(-2));

          >>>[6, 7]

          console.log(array.slice(-3));

          >>>[5, 6, 7]

          等待 Promise 完成

          在某些情況下,你可能會需要等待多個 promise 結束。可以用 Promise.all 來并行運行我們的 promise。


          const PromiseArray = [

             Promise.resolve(100),

             Promise.reject(null),

             Promise.resolve("Data release"),

             Promise.reject(new Error('Something went wrong'))];


          Promise.all(PromiseArray)

           .then(data => console.log('all resolved! here are the resolve values:', data))

           .catch(err => console.log('got rejected! reason:', err))

          關于 Promise.all 的主要注意事項是,當一個 Promise 拒絕時,該方法將引發錯誤。這意味著你的代碼不會等到你所有的 promise 都完成。


          如果你想等到所有 promise 都完成后,無論它們被拒絕還是被解決,都可以使用 Promise.allSettled。此方法在 ES2020 的最終版本得到支持。


          const PromiseArray = [

             Promise.resolve(100),

             Promise.reject(null),

             Promise.resolve("Data release"),

             Promise.reject(new Error('Something went wrong'))];


          Promise.allSettled(PromiseArray).then(res =>{

          console.log(res);

          }).catch(err => console.log(err));


          //[

          //{status: "fulfilled", value: 100},

          //{status: "rejected", reason: null},

          //{status: "fulfilled", value: "Data release"},

          //{status: "rejected", reason: Error: Something went wrong ...}

          //]

          即使某些 promise 被拒絕,Promise.allSettled 也會從你所有的 promise 中返回結果。

          你所不知道的XML

          前端達人

          一、XML:

          XML(Extensible Markup Language 可擴展標記語言),XML是一個以文本來描述數據的文檔。

          1. 示例:

          <?xml version="1.0" encoding="UTF-8"?>
          <people>
              <person personid="E01">
                  <name>Tony</name>
                  <address>10 Downing Street, London, UK</address>
                  <tel>(061) 98765</tel>
                  <fax>(061) 98765</fax>
                  <email>tony@everywhere.com</email>
              </person>
              <person personid="E02">
                  <name>Bill</name>
                  <address>White House, USA</address>
                  <tel>(001) 6400 98765</tel>
                  <fax>(001) 6400 98765</fax>
                  <email>bill@everywhere.com</email>
              </person>
          </people>
          

          2. 用途:

          (1)充當顯示數據(以XML充當顯示層)

          (2)存儲數據(存儲層)的功能

          (3)以XML描述數據,并在聯系服務器與系統的其余部分之間傳遞。(傳輸數據的一樣格式)

          從某種角度講,XML是數據封裝和消息傳遞技術。

          3.解析XML:
          3.1 :使用SAX解析XML

          3.1.1 什么是SAX:

          SAX是Simple API for XML的縮寫
          SAX 是讀取和操作 XML 數據更快速、更輕量的方法。SAX 允許您在讀取文檔時處理它,從而不必等待整個文檔被存儲之后才采取操作。它不涉及 DOM 所必需的開銷和概念跳躍。 SAX API是一個基于事件的API ,適用于處理數據流,即隨著數據的流動而依次處理數據。SAX API 在其解析您的文檔時發生一定事件的時候會通知您。在您對其響應時,您不作保存的數據將會被拋棄。

          3.1.2 SAX解析XML方式:

          SAX API中主要有四種處理事件的接口,它們分別是ContentHandler,DTDHandler, EntityResolver 和 ErrorHandler 。實際上只要繼承DefaultHandler 類就可以,DefaultHandler實現了這四個事件處理器接口,然后提供了每個抽象方法的默認實現。
          // 創建SAX解析器工廠對象
          SAXParserFactory spf = SAXParserFactory.newInstance();
          // 使用解析器工廠創建解析器實例
          SAXParser saxParser = spf.newSAXParser();
          // 創建SAX解析器要使用的事件偵聽器對象
          PersonHandler handler = 
                                   new PersonHandler();
          // 開始解析文件
          saxParser.parse(
                      new File(fileName), handler);
          


          3.2. DOM解析XML:

          DOM:Document Object Model(文檔對象模型)
          DOM的特性:
          定義一組 Java 接口,基于對象,與語言和平臺無關將 XML 文檔表示為樹,在內存中解析和存儲 XML 文檔,允許隨機訪問文檔的不同部分。

          DOM解析XML
          DOM的優點,由于樹在內存中是持久的,因此可以修改后更新。它還可以在任何時候在樹中上下導航,API使用起來也較簡單。 

          DocumentBuilderFactory builder = DocumentBuilderFactory.newInstance();
          DocumentBuilder db = builder.newDocumentBuilder();
          db.parse("person.xml");
          NodeList node_person = doc.getElementsByTagName("person");
          

           3.3. JDOM解析XML:

          JDOM是兩位著名的 Java 開發人員兼作者,Brett Mclaughlin 和 Jason Hunter 的創作成果, 2000 年初在類似于Apache協議的許可下,JDOM作為一個開放源代碼項目正式開始研發了。

          JDOM 簡化了與 XML 的交互并且比使用 DOM 實現更快,JDOM 與 DOM 主要有兩方面不同。首先,JDOM 僅使用具體類而不使用接口。這在某些方面簡化了 API,但是也限制了靈活性。第二,API 大量使用了 Collections 類,簡化了那些已經熟悉這些類的 Java 開發者的使用。
           

          解析步驟:
          (1)SAXBuilder sax = new SAXBuilder();
          (2)Document doc = sax.build(….);
          (3)Element el = doc.getRootElement();(4)List list = el.getChildren();
          (5)遍歷內容
          


          3.4. DOM4J解析XML:

          dom4j是一個非常非常優秀的Java XML API,具有性能優異、功能強大和極端易用使用的特點,同時它也是一個開放源代碼的軟件,可以在SourceForge上找到它。在對主流的Java XML API進行的性能、功能和易用性的評測,dom4j無論在那個方面都是非常出色的。如今你可以看到越來越多的Java軟件都在使用dom4j來讀寫XML,特別值得一提的是連Sun的JAXM也在用dom4j。這是必須使用的jar包, Hibernate用它來讀寫配置文件。
          解析步驟:
          (1)SAXReader sax = new SAXReader();
          (2)Document doc = sax.read(Thread.currentThread().getContextClassLoader()
                    .getResourceAsStream("person.xml"));
          (3)Element root = doc.getRootElement();
          (4)Iterator iterator = root.elementIterator();
          (5)遍歷迭代器
          


          4.各種解析方法比較:
          JDOM 和 DOM 在性能測試時表現不佳,在測試 10M 文檔時內存溢出。
          SAX表現較好,這要依賴于它特定的解析方式。一個 SAX 檢測即將到來的XML流,但并沒有載入到內存(當然當XML流被讀入時,會有部分文檔暫時隱藏在內存中。DOM4J是這場測試的獲勝者,目前許多開源項目中大量采用 DOM4J,例如大名鼎鼎的 Hibernate 也用 DOM4J 來讀取 XML 配置文件。
          xstream 實現XML的轉換


          5.案例:

          public class Person {
              private String personid;
              private String name;
              private String address;
              private String tel;
              private String fax;
              private String email;
          
              @Override
              public String toString() {
                  return "Person{" +
                          "personid='" + personid + '\'' +
                          ", name='" + name + '\'' +
                          ", address='" + address + '\'' +
                          ", tel='" + tel + '\'' +
                          ", fax='" + fax + '\'' +
                          ", email='" + email + '\'' +
                          '}';
              }
          
              public String getPersonid() {
                  return personid;
              }
          
              public void setPersonid(String personid) {
                  this.personid = personid;
              }
          
              public String getName() {
                  return name;
              }
          
              public void setName(String name) {
                  this.name = name;
              }
          
              public String getAddress() {
                  return address;
              }
          
              public void setAddress(String address) {
                  this.address = address;
              }
          
              public String getTel() {
                  return tel;
              }
          
              public void setTel(String tel) {
                  this.tel = tel;
              }
          
              public String getFax() {
                  return fax;
              }
          
              public void setFax(String fax) {
                  this.fax = fax;
              }
          
              public String getEmail() {
                  return email;
              }
          
              public void setEmail(String email) {
                  this.email = email;
              }
          }
          



          <?xml version="1.0" encoding="UTF-8"?>
          <people>
              <person personid="E01">
                  <name>Tony Blair</name>
                  <address>10 Downing Street, London, UK</address>
                  <tel>(061) 98765</tel>
                  <fax>(061) 98765</fax>
                  <email>blair@everywhere.com</email>
              </person>
              <person personid="E02">
                  <name>Bill Clinton</name>
                  <address>White House, USA</address>
                  <tel>(001) 6400 98765</tel>
                  <fax>(001) 6400 98765</fax>
                  <email>bill@everywhere.com</email>
              </person>
          </people>
          


          import org.xml.sax.Attributes;
          import org.xml.sax.SAXException;
          import org.xml.sax.helpers.DefaultHandler;
          
          import java.util.ArrayList;
          import java.util.List;
          
          /**
           * Created by Hu Guanzhong
           * SAX解析的特點:
           * 1、基于事件驅動
           * 2、順序讀取,速度快
           * 3、不能任意讀取節點(靈活性差)
           * 4、解析時占用的內存小
           * 5、SAX更適用于在性能要求更高的設備上使用(Android開發中)
           *
           */
          public class PersonHandler extends DefaultHandler{
              private List<Person> persons = null;
              private Person p;//當前正在解析的person
              private String tag;//用于記錄當前正在解析的標簽名
          
              public List<Person> getPersons() {
                  return persons;
              }
          
              //開始解析文檔時調用
              @Override
              public void startDocument() throws SAXException {
                  super.startDocument();
                  persons = new ArrayList<>();
                  System.out.println("開始解析文檔...");
              }
          
              //在XML文檔解析結束時調用
              @Override
              public void endDocument() throws SAXException {
                  super.endDocument();
                  System.out.println("解析文檔結束.");
              }
          
              /**
               * 解析開始元素時調用
               * @param uri 命名空間
               * @param localName 不帶前綴的標簽名
               * @param qName 帶前綴的標簽名
               * @param attributes 當前標簽的屬性集合
               * @throws SAXException
               */
              @Override
              public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
                  super.startElement(uri, localName, qName, attributes);
                  if ("person".equals(qName)){
                      p = new Person();
                      String personid = attributes.getValue("personid");
                      p.setPersonid(personid);
                  }
                  tag = qName;
                  System.out.println("startElement--"+qName);
              }
          
              //解析結束元素時調用
              @Override
              public void endElement(String uri, String localName, String qName) throws SAXException {
                  super.endElement(uri, localName, qName);
                  if ("person".equals(qName)) {
                      persons.add(p);
                  }
                  tag = null;
                  System.out.println("endElement--"+qName);
              }
          
              //解析文本內容時調用
              @Override
              public void characters(char[] ch, int start, int length) throws SAXException {
                  super.characters(ch, start, length);
                  if (tag != null) {
                      if ("name".equals(tag)) {
                          p.setName(new String(ch,start,length));
                      }else if("address".equals(tag)){
                          p.setAddress(new String(ch,start,length));
                      }else if("tel".equals(tag)){
                          p.setTel(new String(ch,start,length));
                      }else if("fax".equals(tag)){
                          p.setFax(new String(ch,start,length));
                      }else if("email".equals(tag)){
                          p.setEmail(new String(ch,start,length));
                      }
                      System.out.println(ch);
                  }
              }
          }
          



          public class XMLDemo {
          
              /**
               * 使用第三方xstream組件實現XML的解析與生成
               */
              @Test
              public void xStream(){
                  Person p = new Person();
                  p.setPersonid("1212");
                  p.setAddress("北京");
                  p.setEmail("vince@163.com");
                  p.setFax("6768789798");
                  p.setTel("13838389438");
                  p.setName("38");
          
                  XStream xStream = new XStream(new Xpp3Driver());
                  xStream.alias("person",Person.class);
                  xStream.useAttributeFor(Person.class,"personid");
                  String xml = xStream.toXML(p);
                  System.out.println(xml);
          
                  //解析XML
                  Person person = (Person)xStream.fromXML(xml);
                  System.out.println(person);
              }
          
              /**
               * 從XML文件中讀取對象
               */
              @Test
              public void xmlDecoder() throws FileNotFoundException {
                  BufferedInputStream in = new BufferedInputStream(new FileInputStream("test.xml"));
                  XMLDecoder decoder = new XMLDecoder(in);
                  Person p = (Person)decoder.readObject();
                  System.out.println(p);
              }
              /**
               * 把對象轉成XML文件寫入
               */
              @Test
              public void xmlEncoder() throws FileNotFoundException {
                  BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("test.xml"));
                  XMLEncoder xmlEncoder = new XMLEncoder(bos);
                  Person p = new Person();
                  p.setPersonid("1212");
                  p.setAddress("北京");
                  p.setEmail("vince@163.com");
                  p.setFax("6768789798");
                  p.setTel("13838389438");
                  p.setName("38");
                  xmlEncoder.writeObject(p);
                  xmlEncoder.close();
              }
          
              /**
               * DOM4J解析XML
               * 基于樹型結構,第三方組件
               * 解析速度快,效率更高,使用的JAVA中的迭代器實現數據讀取,在WEB框架中使用較多(Hibernate)
               *
               */
              @Test
              public void dom4jParseXML() throws DocumentException {
                  //1 創建DOM4J的解析器對象
                  SAXReader reader = new SAXReader();
                  InputStream is = Thread.currentThread().getContextClassLoader()
                          .getResourceAsStream("com/vince/xml/person.xml");
                  org.dom4j.Document doc = reader.read(is);
                  org.dom4j.Element rootElement = doc.getRootElement();
                  Iterator<org.dom4j.Element> iterator = rootElement.elementIterator();
                  ArrayList<Person> persons = new ArrayList<>();
                  Person p = null;
                  while(iterator.hasNext()){
                      p = new Person();
                      org.dom4j.Element e = iterator.next();
                      p.setPersonid(e.attributeValue("personid"));
                      Iterator<org.dom4j.Element> iterator1 = e.elementIterator();
                      while(iterator1.hasNext()){
                          org.dom4j.Element next = iterator1.next();
                          String tag = next.getName();
                          if("name".equals(tag)){
                              p.setName(next.getText());
                          }else if("address".equals(tag)){
                              p.setAddress(next.getText());
                          }else if("tel".equals(tag)){
                              p.setTel(next.getText());
                          }else if("fax".equals(tag)){
                              p.setFax(next.getText());
                          }else if("email".equals(tag)){
                              p.setEmail(next.getText());
                          }
                      }
                      persons.add(p);
                  }
                  System.out.println("結果:");
                  System.out.println(Arrays.toString(persons.toArray()));
              }
          
              /**
               * JDOM解析 XML
               * 1、與DOM類似基于樹型結構,
               * 2、與DOM的區別:
               * (1)第三方開源的組件
               * (2)實現使用JAVA的Collection接口
               * (3)效率比DOM更快
               */
              @Test
              public void jdomParseXML() throws JDOMException, IOException {
                  //創建JDOM解析器
                  SAXBuilder builder = new SAXBuilder();
                  InputStream is = Thread.currentThread().getContextClassLoader()
                          .getResourceAsStream("com/vince/xml/person.xml");
                  org.jdom2.Document build = builder.build(is);
                  Element rootElement = build.getRootElement();
                  List<Person> list = new ArrayList<>();
                  Person person = null;
                  List<Element> children = rootElement.getChildren();
                  for(Element element: children){
                      person = new Person();
                      String personid = element.getAttributeValue("personid");
                      person.setPersonid(personid);
                      List<Element> children1 = element.getChildren();
                      for (Element e: children1){
                          String tag = e.getName();
                          if("name".equals(tag)){
                              person.setName(e.getText());
                          }else if("address".equals(tag)){
                              person.setAddress(e.getText());
                          }else if("tel".equals(tag)){
                              person.setTel(e.getText());
                          }else if("fax".equals(tag)){
                              person.setFax(e.getText());
                          }else if("email".equals(tag)){
                              person.setEmail(e.getText());
                          }
                      }
                      list.add(person);
                  }
                  System.out.println("結果:");
                  System.out.println(Arrays.toString(list.toArray()));
              }
          
              /**
               * DOM解析XML
               * 1、基于樹型結構,通過解析器一次性把文檔加載到內存中,所以會比較占用內存,可以隨機訪問
               * 更加靈活,更適合在WEB開發中使用
               */
              @Test
              public void domParseXML() throws ParserConfigurationException, IOException, SAXException {
                  //1、創建一個DOM解析器工廠對象
                  DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
                  //2、通過工廠對象創建解析器對象
                  DocumentBuilder documentBuilder = factory.newDocumentBuilder();
                  //3、解析文檔
                  InputStream is = Thread.currentThread().getContextClassLoader()
                          .getResourceAsStream("com/vince/xml/person.xml");
                  //此代碼完成后,整個XML文檔已經被加載到內存中,以樹狀形式存儲
                  Document doc = documentBuilder.parse(is);
                  //4、從內存中讀取數據
          
                  //獲取節點名稱為person的所有節點,返回節點集合
                  NodeList personNodeList = doc.getElementsByTagName("person");
                  ArrayList<Person> persons = new ArrayList<>();
                  Person p = null;
                  //此循環會迭代兩次
                  for (int i=0;i<personNodeList.getLength();i++){
                      Node personNode = personNodeList.item(i);
                      p = new Person();
                      //獲取節點的屬性值
                      String personid = personNode.getAttributes().getNamedItem("personid").getNodeValue();
                      p.setPersonid(personid);
                      //獲取當前節點的所有子節點
                      NodeList childNodes = personNode.getChildNodes();
                      for (int j = 0;j<childNodes.getLength();j++){
                          Node item = childNodes.item(j);
                          String nodeName = item.getNodeName();
                          if ("name".equals(nodeName)) {
                              p.setName(item.getFirstChild().getNodeValue());
                          }else if("address".equals(nodeName)){
                              p.setAddress(item.getFirstChild().getNodeValue());
                          }else if("tel".equals(nodeName)){
                              p.setTel(item.getFirstChild().getNodeValue());
                          }else if("fax".equals(nodeName)){
                              p.setFax(item.getFirstChild().getNodeValue());
                          }else if("email".equals(nodeName)){
                              p.setEmail(item.getFirstChild().getNodeValue());
                          }
                      }
                      persons.add(p);
                  }
                  System.out.println("結果:");
                  System.out.println(Arrays.toString(persons.toArray()));
              }
          
              /**
               * SAX解析的特點:
               * 1、基于事件驅動
               * 2、順序讀取,速度快
               * 3、不能任意讀取節點(靈活性差)
               * 4、解析時占用的內存小
               * 5、SAX更適用于在性能要求更高的設備上使用(Android開發中)
               * @throws ParserConfigurationException
               * @throws SAXException
               * @throws IOException
               */
              @Test
              public void saxParseXML() throws ParserConfigurationException, SAXException, IOException {
                  //1、創建一個SAX解析器工廠對象
                  SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
                  //2、通過工廠對象創建SAX解析器
                  SAXParser saxParser = saxParserFactory.newSAXParser();
                  //3、創建一個數據處理器(需要我們自己來編寫)
                  PersonHandler personHandler = new PersonHandler();
                  //4、開始解析
                  InputStream is = Thread.currentThread().getContextClassLoader()
                          .getResourceAsStream("com/vince/xml/person.xml");
                  saxParser.parse(is,personHandler);
                  List<Person> persons = personHandler.getPersons();
                  for (Person p:persons){
                      System.out.println(p);
                  }
              }
          }
          

          修復一個因為 scrollbar 占據空間導致的 bug

          seo達人

          背景

          這一個因為滾動條占據空間引起的bug, 查了一下資料, 最后也解決了,順便研究一下這個屬性, 做一下總結,分享給大家看看。


          正文

          昨天, 測試提了個問題, 現象是一個輸入框的聚焦提示偏了, 讓我修一下, 如下圖:


          image.png


          起初認為是紅框提示位置不對, 就去找代碼看:


          <Input

           // ...

           onFocus={() => setFocusedInputName('guidePrice')}

           onBlur={() => setFocusedInputName('')}

          />


          <Table

           data-focused-column={focusedInputName}

           // ...

          />

          代碼上沒有什么問題, 不是手動設置的,而且, 在我和另一個同事, 還有PM的PC上都是OK的:


          image.png


          初步判斷是,紅框位置結算有差異, 差異大小大概是17px, 但是這個差異是怎么產生的呢?


          就去測試小哥的PC上看, 注意到一個細節, 在我PC上, 滾動條是懸浮的:

          image.png


          在他PC上, 滾動條是占空間的:


          image.png


          在他電腦上, 手動把原本的 overscroll-y: scroll 改成 overscroll-y: overlay 問題就結局了。


          由此判定是: 滾動條占據空間 引起的bug。


          overscroll-y: overlay

          CSS屬性 overflow, 定義當一個元素的內容太大而無法適應塊級格式化上下文的時候該做什么。它是 overflow-x 和overflow-y的 簡寫屬性 。

          /* 默認值。內容不會被修剪,會呈現在元素框之外 */

          overflow: visible;


          /* 內容會被修剪,并且其余內容不可見 */

          overflow: hidden;


          /* 內容會被修剪,瀏覽器會顯示滾動條以便查看其余內容 */

          overflow: scroll;


          /* 由瀏覽器定奪,如果內容被修剪,就會顯示滾動條 */

          overflow: auto;


          /* 規定從父元素繼承overflow屬性的值 */

          overflow: inherit;

          官方描述:

          overlay  行為與 auto 相同,但滾動條繪制在內容之上而不是占用空間。 僅在基于 WebKit(例如,Safari)和基于Blink的(例如,Chrome或Opera)瀏覽器中受支持。

          表現:

          html {

           overflow-y: overlay;

          }

          兼容性

          沒有在caniuse上找到這個屬性的兼容性, 也有人提這個問題:


          image.png


          問題場景以及解決辦法

          1. 外部容器的滾動條

          這里的外部容器指的是html, 直接加在最外層:


          html {

           overflow-y: scroll;

          }

          手動加上這個特性, 不論什么時候都有滾動寬度占據空間。


          缺點: 沒有滾動的時候也會有個滾動條, 不太美觀。


          優點: 方便, 沒有兼容性的問題。


          2. 外部容器絕對定位法

          用絕對定位,保證了body的寬度一直保持完整空間:


          html {

           overflow-y: scroll; // 兼容ie8,不支持:root, vw

          }


          :root {

           overflow-y: auto;

           overflow-x: hidden;

          }


          :root body {

           position: absolute;

          }


          body {

           width: 100vw;

           overflow: hidden;

          }

          3. 內部容器做兼容


          .wrapper {

             overflow-y: scroll; // fallback

             overflow-y: overlay;

          }

          總結

          個人推薦還是用 overlay, 然后使用scroll 做為兜底。


          內容就這么多, 希望對大家有所啟發。


          文章如有錯誤, 請在留言區指正, 謝謝。

          將 Gatsby 項目遷移到 TypeScript

          seo達人

          之前花了些時間將gatsby-theme-gitbook遷移到 Typescript,以獲得在 VSCode 中更好的編程體驗.

          整體差不多已經完成遷移,剩下將 Gatsby 的 API 文件也遷移到 TS,這里可以看到 gatsby#21995 官方也在將核心代碼庫遷移到 Typescript,準備等待官方將核心代碼庫遷移完成,在遷移 API 文件.


          這篇文章用XYShaoKang/gatsby-project-config,演示如何將 gatsby 遷移到 TypeScript,希望能幫到同樣想要在 Gatsby 中使用 TS 的同學.


          遷移步驟:


          TS 配置

          配置 ESLint 支持 TS

          完善 GraphQL 類型提示

          初始化項目

          gatsby new gatsby-migrate-to-typescript XYShaoKang/gatsby-project-config

          cd gatsby-migrate-to-typescript

          yarn develop

          TS 配置

          安裝typescript

          添加typescript.json配置文件

          修改 js 文件為 tsx

          補全 TS 聲明定義

          安裝typescript

          yarn add -D typescript

          添加配置文件tsconfig.json

          // https://www.typescriptlang.org/v2/docs/handbook/tsconfig-json.html

          {

           "compilerOptions": {

             "target": "esnext", // 編譯生成的目標 es 版本,可以根據需要設置

             "module": "esnext", // 編譯生成的目標模塊系統

             "lib": ["dom", "es2015", "es2017"], // 配置需要包含的運行環境的類型定義

             "jsx": "react", // 配置 .tsx 文件的輸出模式

             "strict": true, // 開啟嚴格模式

             "esModuleInterop": true, // 兼容 CommonJS 和 ES Module

             "moduleResolution": "node", // 配置模塊的解析規則,支持 node 模塊解析規則

             "noUnusedLocals": true, // 報告未使用的局部變量的錯誤

             "noUnusedParameters": true, // 報告有關函數中未使用參數的錯誤

             "experimentalDecorators": true, // 啟用裝飾器

             "emitDecoratorMetadata": true, // 支持裝飾器上生成元數據,用來進行反射之類的操作

             "noEmit": true, // 不輸出 js,源映射或聲明之類的文件,單純用來檢查錯誤

             "skipLibCheck": true // 跳過聲明文件的類型檢查,只會檢查已引用的部分

           },

           "exclude": ["./node_modules", "./public", "./.cache"], // 解析時,應該跳過的路晉

           "include": ["src"] // 定義包含的路徑,定義在其中的聲明文件都會被解析進 vscode 的智能提示

          }

          將index.js改成index.tsx,重新啟動服務,查看效果.


          其實 Gatsby 內置了支持 TS,不用其他配置,只要把index.js改成index.tsx就可以直接運行.添加 TS 依賴是為了顯示管理 TS,而tsconfig.json也是這個目的,當我們有需要新的特性以及自定義配置時,可以手動添加.

          補全 TS 聲明定義

          打開index.tsx,VSCode 會報兩個錯誤,一個是找不到styled-components的聲明文件,這個可以通過安裝@types/styled-components來解決.

          另外一個錯誤綁定元素“data”隱式具有“any”類型。,這個錯誤是因為我們在tsconfig.json中指定了"strict": true,這會開啟嚴格的類型檢查,可以通過關閉這個選項來解決,只是我們用 TS 就是要用它的類型檢查的,所以正確的做法是給data定義類型.

          下面來一一修復錯誤.


          安裝styled-components的聲明文件


          yarn add -D @types/styled-components

          修改index.tsx


          import React, { FC } from 'react'

          import styled from 'styled-components'

          import { graphql } from 'gatsby'

          import { HomeQuery } from './__generated__/HomeQuery'


          const Title = styled.h1`

           font-size: 1.5em;

           margin: 0;

           padding: 0.5em 0;

           color: palevioletred;

           background: papayawhip;

          `


          const Content = styled.div`

           margin-top: 0.5em;

          `


          interface PageQuery {

           data: {

             allMarkdownRemark: {

               edges: Array<{

                 node: {

                   frontmatter: {

                     title: string

                   }

                   excerpt: string

                 }

               }>

             }

           }

          }


          const Home: FC<PageQuery> = ({ data }) => {

           const node = data.allMarkdownRemark.edges[0].node


           const title = node.frontmatter?.title

           const excerpt = node.excerpt


           return (

             <>

               <Title>{title}</Title>

               <Content>{excerpt}</Content>

             </>

           )

          }


          export default Home


          export const query = graphql`

           query HomeQuery {

             allMarkdownRemark {

               edges {

                 node {

                   frontmatter {

                     title

                   }

                   excerpt

                 }

               }

             }

           }

          `

          這時候會出現一個新的錯誤,在excerpt: string處提示Parsing error: Unexpected token,這是因為 ESLint 還無法識別 TS 的語法,下面來配置 ESLint 支持 TS.


          配置 ESLint 支持 TypeScript

          安裝依賴


          yarn add -D @typescript-eslint/parser @typescript-eslint/eslint-plugin

          配置.eslintrc.js


          module.exports = {

           parser: `@typescript-eslint/parser`, // 將解析器從`babel-eslint`替換成`@typescript-eslint/parser`,用以解析 TS 代碼

           extends: [

             `google`,

             `eslint:recommended`,

             `plugin:@typescript-eslint/recommended`, // 使用 @typescript-eslint/eslint-plugin 推薦配置

             `plugin:react/recommended`,

             `prettier/@typescript-eslint`, // 禁用 @typescript-eslint/eslint-plugin 中與 prettier 沖突的規則

             `plugin:prettier/recommended`,

           ],

           plugins: [

             `@typescript-eslint`, // 處理 TS 語法規則

             `react`,

             `filenames`,

           ],

           // ...

          }

          在.vscode/settings.json中添加配置,讓VSCode使用ESLint擴展格式化ts和tsx文件


          // .vscode/settings.json

          {

           "eslint.format.enable": true,

           "[javascript]": {

             "editor.defaultFormatter": "dbaeumer.vscode-eslint"

           },

           "[javascriptreact]": {

             "editor.defaultFormatter": "dbaeumer.vscode-eslint"

           },

           "[typescript]": {

             "editor.defaultFormatter": "dbaeumer.vscode-eslint"

           },

           "[typescriptreact]": {

             "editor.defaultFormatter": "dbaeumer.vscode-eslint"

           }

          }

          完善 GraphQL 類型提示

          // index.tsx

          import React, { FC } from 'react'

          // ...

          interface PageQuery {

           data: {

             allMarkdownRemark: {

               edges: Array<{

                 node: {

                   frontmatter: {

                     title: string

                   }

                   excerpt: string

                 }

               }>

             }

           }

          }


          const Home: FC<PageQuery> = ({ data }) => {

           // ...

          }


          export default Home


          export const query = graphql`

           query HomeQuery {

             allMarkdownRemark {

               edges {

                 node {

                   frontmatter {

                     title

                   }

                   excerpt

                 }

               }

             }

           }

          `

          我們看看index.tsx文件,會發現PropTypes和query結構非常類似,在Gatsby運行時,會把query查詢的結果作為組件prop.data傳入組件,而PropTypes是用來約束prop存在的.所以其實PropTypes就是根據query寫出來的.


          如果有依據query自動生成PropTypes的功能就太棒了.

          另外一個問題是在query中編寫GraphQL查詢時,并沒有類型約束,也沒有智能提示.


          總結以下需要完善的體驗包括:


          GraphQL 查詢編寫時的智能提示,以及錯誤檢查

          能夠從 GraphQL 查詢生成對應的 TypeScript 類型.這樣能保證類型的唯一事實來源,并消除 TS 中冗余的類型聲明.畢竟如果經常需要手動更新兩處類型,會更容易出錯,而且也并不能保證手動定義類型的正確性.

          實現方式:


          通過生成架構文件,配合Apollo GraphQL for VS Code插件,實現智能提示,以及錯誤檢查

          通過graphql-code-generator或者apollo生成 TS 類型定義文件

          如果自己去配置的話,是挺耗費時間的,需要去了解graphql-code-generator的使用,以及Apollo的架構等知識.

          不過好在社區中已經有對應的 Gatsby 插件集成了上述工具可以直接使用,能讓我們不用去深究對應知識的情況下,達到優化 GraphQL 編程的體驗.

          嘗試過以下兩個插件能解決上述問題,可以任選其一使用


          gatsby-plugin-codegen

          gatsby-plugin-typegen

          另外還有一款插件gatsby-plugin-graphql-codegen也可以生成 TS 類型,不過配置略麻煩,并且上述兩個插件都可以滿足我現在的需求,所以沒有去嘗試,感興趣的可以嘗試一下.


          注意點:


          Apollo不支持匿名查詢,需要使用命名查詢

          第一次生成,需要運行Gatsby之后才能生成類型文件

          整個項目內不能有相同命名的查詢,不然會因為名字有沖突而生成失敗

          下面是具體操作


          安裝vscode-apollo擴展

          在 VSCode 中按 Ctrl + P ( MAC 下: Cmd + P) 輸入以下命令,按回車安裝


          ext install apollographql.vscode-apollo

          方式一: 使用gatsby-plugin-codegen

          gatsby-plugin-codegen默認會生成apollo.config.js和schema.json,配合vscode-apollo擴展,可以提供GraphQL的類型約束和智能提示.

          另外會自動根據query中的GraphQL查詢,生成 TS 類型,放在對應的tsx文件同級目錄下的__generated__文件夾,使用時只需要引入即可.

          如果需要在運行時自動生成 TS 類型,需要添加watch: true配置.


          安裝gatsby-plugin-codegen


          yarn add gatsby-plugin-codegen

          配置gatsby-config.js


          // gatsby-config.js

          module.exports = {

           plugins: [

             // ...

             {

               resolve: `gatsby-plugin-codegen`,

               options: {

                 watch: true,

               },

             },

           ],

          }

          重新運行開發服務生成類型文件


          yarn develop

          如果出現以下錯誤,一般是因為沒有為查詢命名的緣故,給查詢添加命名即可,另外配置正確的話,打開對應的文件,有匿名查詢,編輯器會有錯誤提示.


          fix-anonymous-operations.png


          這個命名之后會作為生成的類型名.


          修改index.tsx以使用生成的類型


          gatsby-plugin-codegen插件會更具查詢生成對應的查詢名稱的類型,保存在對應tsx文件同級的__generated__目錄下.


          import { HomeQuery } from './__generated__/HomeQuery' // 引入自動生成的類型

          // ...


          // interface PageQuery {

          //   data: {

          //     allMarkdownRemark: {

          //       edges: Array<{

          //         node: {

          //           frontmatter: {

          //             title: string

          //           }

          //           excerpt: string

          //         }

          //       }>

          //     }

          //   }

          // }


          interface PageQuery {

           data: HomeQuery // 替換之前手寫的類型

          }


          // ...

          將自動生成的文件添加到.gitignore中


          apollo.config.js,schema.json,__generated__能通過運行時生成,所以可以添加到.gitignore中,不用提交到 git 中.當然如果有需要也可以選擇提交到 git 中.

          # Generated types by gatsby-plugin-codegen

          __generated__

          apollo.config.js

          schema.json

          方式二: 使用gatsby-plugin-typegen

          gatsby-plugin-typegen通過配置生成gatsby-schema.graphql和gatsby-plugin-documents.graphql配合手動創建的apollo.config.js提供GraphQL的類型約束和智能提示.

          根據GraphQL查詢生成gatsby-types.d.ts,生成的類型放在命名空間GatsbyTypes下,使用時通過GatsbyTypes.HomeQueryQuery來引入,HomeQueryQuery是由對應的命名查詢生成


          安裝gatsby-plugin-typegen


          yarn add gatsby-plugin-typegen

          配置


          // gatsby-config.js

          module.exports = {

           plugins: [

             // ...

             {

               resolve: `gatsby-plugin-typegen`,

               options: {

                 outputPath: `src/__generated__/gatsby-types.d.ts`,

                 emitSchema: {

                   'src/__generated__/gatsby-schema.graphql': true,

                 },

                 emitPluginDocuments: {

                   'src/__generated__/gatsby-plugin-documents.graphql': true,

                 },

               },

             },

           ],

          }

          //apollo.config.js

          module.exports = {

           client: {

             tagName: `graphql`,

             includes: [

               `./src/**/*.{ts,tsx}`,

               `./src/__generated__/gatsby-plugin-documents.graphql`,

             ],

             service: {

               name: `GatsbyJS`,

               localSchemaFile: `./src/__generated__/gatsby-schema.graphql`,

             },

           },

          }

          重新運行開發服務生成類型文件


          yarn develop

          修改index.tsx以使用生成的類型


          gatsby-plugin-codegen插件會更具查詢生成對應的查詢名稱的類型,保存在對應tsx文件同級的__generated__目錄下.


          // ...


          // interface PageQuery {

          //   data: {

          //     allMarkdownRemark: {

          //       edges: Array<{

          //         node: {

          //           frontmatter: {

          //             title: string

          //           }

          //           excerpt: string

          //         }

          //       }>

          //     }

          //   }

          // }


          interface PageQuery {

           data: GatsbyTypes.HomeQueryQuery // 替換之前手寫的類型

          }


          // ...

          將自動生成的文件添加到.gitignore中


          __generated__能通過運行時生成,所以可以添加到.gitignore中,不用提交到 git 中.當然如果有需要也可以選擇提交到 git 中.

          # Generated types by gatsby-plugin-codegen

          __generated__

          有趣的Canvas,你值得擁有!

          seo達人

          Canvas 是 HTML5 提供的一個用于展示繪圖效果的標簽. Canvas 原意為畫布, 在 HTML 頁面中用于展示繪圖效果. 最早 Canvas 是蘋果提出的一個方案, 今天已經在大多數瀏覽器中實現。


          canvas 的使用領域


          游戲

          大數據可視化數據

          banner 廣告

          多媒體

          模擬仿真

          遠程操作

          圖形編輯

          判斷瀏覽器是否支持 canvas 標簽


          var canvas = document.getElementById('canvas')

          if (canvas.getContext) {

          console.log('你的瀏覽器支持Canvas!')

          } else {

          console.log('你的瀏覽器不支持Canvas!')

          }

          canvas 的基本用法

          1、使用 canvas 標簽, 即可在頁面中開辟一格區域,可以設置其寬高,寬高為 300 和 150


          <canvas></canvas>

          2、獲取 dom 元素 canvas


          canvas 本身不能繪圖. 是使用 JavaScript 來完成繪圖. canvas 對象提供了各種繪圖用的 api。


          var cas = document.querySelector('canvas')

          3、通過 cas 獲取上下文對象(畫布對象!)


          var ctx = cas.getContext('2d')

          4、通過 ctx 開始畫畫(設置起點 設置終點 連線-描邊 )


          ctx.moveTo(10, 10)

          ctx.lineTo(100, 100)

          ctx.stroke()

          繪制線條

          設置開始位置: context.moveTo( x, y )

          設置終點位置: context.lineTo( x, y )

          描邊繪制: context.stroke()

          填充繪制: context.fill()

          閉合路徑: context.closePath()

          canvas 還可以設置線條的相關屬性,如下:


          CanvasRenderingContext2D.lineWidth 設置線寬.

          CanvasRenderingContext2D.strokeStyle 設置線條顏色.

          CanvasRenderingContext2D.lineCap 設置線末端類型,'butt'( 默認 ), 'round', 'square'.

          CanvasRenderingContext2D.lineJoin 設置相交線的拐點, 'miter'(默認),'round', 'bevel',

          CanvasRenderingContext2D.getLineDash() 獲得線段樣式數組.

          CanvasRenderingContext2D.setLineDash() 設置線段樣式.

          CanvasRenderingContext2D.lineDashOffset 繪制線段偏移量.

          封裝一個畫矩形的方法


          function myRect(ctxTmp, x, y, w, h) {

          ctxTmp.moveTo(x, y)

          ctxTmp.lineTo(x + w, y)

          ctxTmp.lineTo(x + w, y + h)

          ctxTmp.lineTo(x, y + h)

          ctxTmp.lineTo(x, y)

          ctxTmp.stroke()

          }


          var cas = document.querySelector('canvas')

          var ctx = cas.getContext('2d')

          myRect(ctx, 50, 50, 200, 200)

          繪制矩形

          fillRect( x , y , width , height) 填充以(x,y)為起點寬高分別為 width、height 的矩形 默認為黑色

          stokeRect( x , y , width , height) 繪制一個空心以(x,y)為起點寬高分別為 width、height 的矩形

          clearRect( x, y , width , height ) 清除以(x,y)為起點寬高分別為 width、height 的矩形 為透明

          繪制圓弧

          繪制圓弧的方法有


          CanvasRenderingContext2D.arc()

          CanvasRenderingContext2D.arcTo()

          6 個參數: x,y(圓心的坐標),半徑,起始的弧度(不是角度 deg),結束的弧度,(bool 設置方向 ! )


          var cas = document.querySelector('canvas')

          var ctx = cas.getContext('2d')


          ctx.arc(100, 100, 100, 0, degToArc(360))

          ctx.stroke()


          // 角度轉弧度

          function degToArc(num) {

          return (Math.PI / 180) * num

          }

          繪制扇形


          var cas = document.querySelector('canvas')

          var ctx = cas.getContext('2d')


          ctx.arc(300, 300, 200, degToArc(125), degToArc(300))


          // 自動連回原點

          ctx.closePath()

          ctx.stroke()


          function degToArc(num) {

          return (Math.PI / 180) * num

          }

          制作畫筆

          聲明一個變量作為標識

          鼠標按下的時候,記錄起點位置

          鼠標移動的時候,開始描繪并連線

          鼠標抬起的時候,關閉開關

          點擊查看效果圖


          var cas = document.querySelector('canvas')

          var ctx = cas.getContext('2d')


          var isDraw = false

          // 鼠標按下事件

          cas.addEventListener('mousedown', function () {

          isDraw = true

          ctx.beginPath()

          })


          // 鼠標移動事件

          cas.addEventListener('mousemove', function (e) {

          if (!isDraw) {

          // 沒有按下

          return

          }

          // 獲取相對于容器內的坐標

          var x = e.offsetX

          var y = e.offsetY

          ctx.lineTo(x, y)

          ctx.stroke()

          })


          cas.addEventListener('mouseup', function () {

          // 關閉開關了!

          isDraw = false

          })

          手動涂擦

          原理和畫布相似,只不過用的是clearRect()方法。


          點擊查看效果圖


          var cas = document.querySelector('canvas')

          var ctx = cas.getContext('2d')


          ctx.fillRect(0, 0, 600, 600)


          // 開關

          var isClear = false


          cas.addEventListener('mousedown', function () {

          isClear = true

          })


          cas.addEventListener('mousemove', function (e) {

          if (!isClear) {

          return

          }

          var x = e.offsetX

          var y = e.offsetY

          var w = 20

          var h = 20

          ctx.clearRect(x, y, w, h)

          })


          cas.addEventListener('mouseup', function () {

          isClear = false

          })

          刮刮樂

          首先需要設置獎品和畫布,將畫布置于圖片上方蓋住,

          隨機設置生成獎品。

          當手觸摸移動的時候,可以擦除部分畫布,露出獎品區。

          點擊查看效果圖


          <div>

          <img src="./images/2.jpg" alt="" />

          <canvas width="600" height="600"></canvas>

          </div>

          css


          img {

          width: 600px;

          height: 600px;

          position: absolute;

          top: 10%;

          left: 30%;

          }


          canvas {

          width: 600px;

          height: 600px;

          position: absolute;

          top: 10%;

          left: 30%;

          border: 1px solid #000;

          }

          js


          var cas = document.querySelector('canvas')

          var ctx = cas.getContext('2d')

          var img = document.querySelector('img')

          // 加一個遮罩層

          ctx.fillStyle = '#ccc'

          ctx.fillRect(0, 0, cas.width, cas.height)

          setImgUrl()

          // 開關

          var isClear = false

          cas.addEventListener('mousedown', function () {

          isClear = true

          })

          cas.addEventListener('mousemove', function (e) {

          if (!isClear) {

          return

          }

          var x = e.offsetX

          var y = e.offsetY

          ctx.clearRect(x, y, 30, 30)

          })

          cas.addEventListener('mouseup', function () {

          isClear = false

          })


          function setImgUrl() {

          var arr = ['./images/1.jpg', './images/2.jpg', './images/3.jpg', './images/4.jpg']

          // 0-3

          var random = Math.round(Math.random() * 3)

          img.src = arr[random]

          }

          更多demo,請查看 github.com/Michael-lzg…


          v-if 和 v-show的區別

          前端達人

          簡單來說,v-if 的初始化較快,但切換代價高;v-show 初始化慢,但切換成本低

          1.共同點

          都是動態顯示DOM元素

          2.區別

          (1)手段:
          v-if是動態的向DOM樹內添加或者刪除DOM元素;
          v-show是通過設置DOM元素的display樣式屬性控制顯隱;
          (2)編譯過程:
          v-if切換有一個局部編譯/卸載的過程,切換過程中合適地銷毀和重建內部的事件監聽和子組件;
          v-show只是簡單的基于css切換;
          (3)編譯條件:
          v-if是惰性的,如果初始條件為假,則什么也不做;只有在條件第一次變為真時才開始局部編譯(編譯被緩存?編譯被緩存后,然后再切換的時候進行局部卸載);
          v-show是在任何條件下(首次條件是否為真)都被編譯,然后被緩存,而且DOM元素保留;
          (4)性能消耗:
          v-if有更高的切換消耗;
          v-show有更高的初始渲染消耗;
          (5)使用場景:
          v-if適合運營條件不大可能改變;
          v-show適合頻繁切換。



          新版vue-router的hooks用法

          seo達人

          雖然Vue 3還沒有正式發布,但是熱愛新技術的我早已按捺不住自己的內心,開始嘗試在小項目中使用它了。


          根據這篇《今日凌晨Vue3 beta版震撼發布,竟然公開支持腳手架項目!》我搭建了一個Vue 3的腳手架項目,用這種方式搭建的腳手架項目不僅僅只有vue是新版的,就連vue-router、vuex都是的。


          給大家截一下package.json的圖:




          可以看到vue-router和vuex都已經開啟4.0時代啦!


          不過其實我并沒有去了解過vue-router 4.0的新用法什么的,因為我覺得它不像vue 3.0都已經進行到beta的版本不會有特別大的變動。


          而vue-router 4.0還是alpha的階段,所以我認為現在去學習它有些為時尚早。但卻就是它!差點釀成了一場慘劇。


          舊版vue + vue-router的使用方式

          假如你在路由里面定義了一個動態參數通常都會這么寫:


          {

             path: '/:id'

          }

          然后用編程式導航的時候通常會這樣去寫:


          this.$router.push('/123')

          在組件中是這樣獲取這個參數的:


          this.$route.params.id

          我以為的新版vue + vue-router的使用方式

          由于vue 3.0的Composition API中沒有this了,所以我想到了通過獲取組件實例的方式來獲取$route:


          import { defineComponent, getCurrentInstance } from 'vue'


          export default defineComponent((props, context) => {

             const { ctx } = getCurrentInstance()

             

             console.log(ctx.$route)

          })

          沒想到打印出來的居然是undefined!

          這是咋回事呢?

          于是我又打印了一遍ctx(ctx是當前組件上下文):




          沒有&dollar;的那些字段是我在組件中自己定義的變量,帶&dollar;的這些就是vue內置的了,找了半天發現沒有&dollar;route了,只剩下了一個&dollar;router,估計vue-router 4.0把當前路由信息都轉移到$router里面去了。


          帶著猜想,我點開了&dollar;router:




          currentRoute! 看名字的話感覺應該就是它了!于是乎我:


          import { defineComponent, getCurrentInstance } from 'vue'


          export default defineComponent((props, context) => {

             const { ctx } = getCurrentInstance()

             

             console.log(ctx.$router.currentRoute.value.params.id)

          })

          果然獲取到了!好開心!


          實際的新版vue + vue-router用法

          在接下來的過程中我用ctx.&dollar;router代替了原來的this.&dollar;router、用ctx.&dollar;router.currentRoute.value代替了原先的this.&dollar;route。


          盡管在接下來的進度中并沒有出現任何的bug,程序一直都是按照我所設想的那樣去運行的。


          但在項目打包后卻出現了意想不到的bug:在跳轉路由的時候報了一個在undefined上面沒有push的錯誤。


          奇了怪了,在開發階段程序都沒有任何的報錯怎么一打包就不行了呢?根據我多年的開發經驗,我很快就定位到了是vue-router的錯誤。


          難道這樣寫是錯的嗎?可是我打印了ctx,它里面明明有一個&dollar;router、&dollar;router里面明明就有currentRoute、currentRoute里面明明就有一個value、value里面明明就有params、params里面我一點開明明就看到了傳過來的參數啊:




          估計可能是vue-router的bug,果然alpha階段的產物不靠譜,我開始后悔使用新版的vue腳手架項目了。


          vue-router里的hooks

          不過這時我突然靈光一現,vue 3不是受到了react hooks的啟發才產生了Composition API的嗎?


          那么估計vue-router肯定也會受到react-router的啟發了!


          還好我學過react,果然技多不壓身啊!估計里面肯定是有一個useXxx,就像這樣:


          import { useXxx } from 'vue-router'

          那么應該是use什么呢?按理來說應該會盡量的和以前的API保持一定的聯系,我猜應該是useRoute和useRouter吧!


          為了驗證我的想法,我打開了node_modules找到了vue-router的源碼:




          果不其然,在第2454和第2455行我發現它導出了useRoute和useRouter,那么就是它了:


          import { defineComponent } from 'vue'

          import { useRoute, useRouter } from 'vue-router'


          export default defineComponent(_ => {

             const route = useRoute()

             const router = useRouter()


             console.log(route.params.id)

             router.push('/xxx/xxx')

          })

          使用這種方式不但可以成功跳轉路由,也同樣可以獲取到路由傳過來的參數,這次再打包試了一下,果然就沒有之前的那個報錯了。


          結語

          估計以后的vue全家桶要開啟全民hooks的時代了,在翻看源碼的同時我發現他們把一些示例都寫在了vue-router/playground文件夾下了,在里面我發現了一些有趣的用法。


          如果有時間的話我會仔細研究一下然后出一篇更加深入的文章給大家,當然如果已經有小伙伴等不及我出新文章的話可以直接進入vue-router-next的github地址:


          https://github.com/vuejs/vue-router-next

          它的示例都放在了playground這個文件夾下,期待你們研究明白后出一篇更加深入的文章!

          日歷

          鏈接

          個人資料

          藍藍設計的小編 http://www.syprn.cn

          存檔

          亚洲va欧美va天堂v国产综合