<dfn id="hx5t3"><strike id="hx5t3"><em id="hx5t3"></em></strike></dfn>

    <thead id="hx5t3"></thead><nobr id="hx5t3"><font id="hx5t3"><rp id="hx5t3"></rp></font></nobr>

    <listing id="hx5t3"></listing>

    <var id="hx5t3"></var>
    <big id="hx5t3"></big>

      
      

      <output id="hx5t3"><ruby id="hx5t3"></ruby></output>
      <menuitem id="hx5t3"><dfn id="hx5t3"></dfn></menuitem>

      <big id="hx5t3"></big>

        公子

        公子 查看完整檔案

        北京編輯中國地質大學(北京)  |  構造地質學 編輯奇舞團  |  前端工程師 編輯 zh.eming.li 編輯
        編輯
        • 額米那個陀佛,無量那個天尊!
        • SF啥時候出注銷功能啊

        個人動態

        公子 發布了文章 · 3月13日

        一道面試題讓你更加了解事件隊列

        今天在群里聊天,突然有人放出了一道面試題。經過群里一番討論,最終解題思路慢慢完善起來,我這里就整理一下群內解題的思路。

        該題定義了一個同步函數對傳入的數組進行遍歷乘二操作,同時每執行一次就會給 executeCount 累加。最終我們需要實現一個 batcher 函數,使用其對該同步函數包裝后,實現每次調用依舊返回預期的二倍結果,同時還需要保證 executeCount 執行次數為1。

        let executeCount = 0
        const fn = nums => {
          executeCount++
          return nums.map(x => x * 2)
        }
        
        const batcher = f => {
          // todo 實現 batcher 函數
        }
        
        const batchedFn = batcher(fn);
        
        const main = async () => {
          const [r1, r2, r3] = await Promise.all([
            batchedFn([1,2,3]),
            batchedFn([4,5]),
            batchedFn([7,8,9])
          ]);
        
          //滿足以下 test case
          assert(r1).tobe([2, 4, 6])
          assert(r2).tobe([8, 10])
          assert(r3).tobe([14, 16, 18])
          assert(executeCount).tobe(1)
        }

        抖機靈解法

        拿到題目的第一時間,我就想到了抖機靈的方法。直接面向用例編程,執行完之后重置下 executeCount 就好了。

        const batcher = f => {
          return nums => {
            try { return f(nums) } finally { executeCount = 1 }
          }
        }

        當然除非你不在乎這次面試,否則一般不建議你用這種抖機靈的方法回答面試官(不要問我為什么知道)。由于 executeCount 的值和 fn() 函數的調用次數呈正相關,所以這道理也就換成了我們需要實現 batcher() 方法返回新的包裝函數,該函數會被調用多次,但最終只會執行一次 fn() 函數。

        setTimeout 解法

        由于題干中使用了 Promise.all(),我們自然而然想到使用異步去解決。也就是每次調用的時候會把所以的傳參存下來,直到最后的時候再執行 fn() 返回對應的結果。問題在于什么時候觸發開始執行呢?自然而然我們想到了類似 debounce 的方式使用 setTimeout 增加延遲時間。

        const batcher = f => {
          let nums = [];
          const p = new Promise(resolve => setTimeout(_ => resolve(f(nums)), 100));
        
          return arr => {
            let start = nums.length;
            nums = nums.concat(arr);
            let end = nums.length;
            return p.then(ret => ret.slice(start, end));
          };
        };

        這里的難點在于預先定義了一個 Promise 在 100ms 之后才會 resolve。返回的函數本質只是將參數推入到 nums 數組中,待 100ms 后觸發 resolve 返回統一執行 fn() 后的結果并獲取對應于當前調用的結果片段。

        后來有群友反饋,實際上不用定義 100ms 直接 0ms 也是可以的。由于 setTimeout 是在 UI 渲染結束之后才會執行的宏任務,所以理論上來說 setTimeout() 的最小間隔值無法設置為 0。它的最小值和瀏覽器的刷新頻率有關系,根據 MDN 描述,它的最小值一般為 4ms。所以理論上它設置 0ms 和 100ms 效果是差不多的,都類似于 debounce 的效果。

        Promise 解法

        那么如何能實現延遲 0ms 執行呢?我們知道除了宏任務之外 JS 還有微任務,微任務隊列是在 JS 主線程執行完成之后立即執行的事件隊列。Promise 的回調就會存儲在微任務隊列中。于是我們將 setTimeout 修改成了 Promise.resolve(),最終發現也是可以實現同樣的效果。

        const batcher = f => {
          let nums = [];
          const p = Promise.resolve().then(_ => f(nums));
        
          return arr => {
            let start = nums.length;
            nums = nums.concat(arr);
            let end = nums.length;
            return p.then(ret => ret.slice(start, end));
          };
        };

        由于 Promise 的微任務隊列效果將 _ => f(nums) 推入微任務隊列,待主線程的三次 batcherFn() 調用都執行完成之后才會執行。之后 p 的狀態變為 fulfilled 后繼續完成最終 slice 的操作。

        后記

        最終分析下來,其實這道理的本質就是要通過某些方法將 fn() 函數的執行后置到主線程執行完畢,至于是使用宏任務還是微任務隊列,就看具體的需求了。除了 setTimeout() 之外,還有 setInterval(), requestAnimationFrame() 都是宏任務隊列。而微任務隊列里除了有 Promise 之外,還有 MutationObserver。關于宏任務和微任務隊列相關的,感興趣的可以看看《微任務、宏任務與Event-Loop》這篇文章。

        查看原文

        贊 2 收藏 0 評論 0

        公子 發布了文章 · 2月4日

        不用備案也能支持微信自定義分享

        我們知道,在微信中打開網頁,使用右上角的 ... 分享給朋友/朋友圈,是可以使用 JS SDK 自定義分享卡片文案的。為了讓分享內容能夠更好的受到監管,從早期會自動讀取網頁內第一張大圖到后期使用 JS SDK 自定義分享,再到后期需要做域名綁定關聯,自定義分享卡片內容的流程變的越來越復雜。

        目前如果你的網站想要增加微信自定義分享文案的支持,需要準備以下兩件事情:

        1. 確保你的網站域名已備案,并被添加到了一個已認證的公眾號的“JS安全域名”中。
        2. 提供服務端支持,用于與微信交互獲取 access token 和 jsapi ticket 并計算獲得最終的 signature,用于在前端調用微信 JS SDK 時進行校驗。

        具體的流程可以參見微信開放文檔??梢钥吹揭雽崿F自定義分享文案,除了開發流程之外,你還需要域名備案和公眾號認證,這兩個做過的人肯定知道會有多頭疼了。而最蛋疼的是,我的博客域名后綴 .org 目前是不支持備案的,難道就沒有辦法了嗎?

        wxhermit

        為了能讓未備案網站也能自定義分享文案,我開發了 wxhermit 這個項目。它的原理非常簡單,基本就是在已備案域名下 <iframe> 嵌套展示未備案域名,并通過 postMessage 通信,將自定義分享的文案傳遞到父頁面。最終實現了任意網站分享自定義的需求。

        當然它的本質還是使用基于最開始的備案域名網站進行分享。由于微信分享需要提供已認證公眾號并綁定已備案域名,一個已認證公眾號只能綁定至多 5 個安全域名,條件頗為苛刻。針對 5 個以上的域名,部分域名無法備案的情況,要自定義微信分享的文案就非常麻煩。本方案比較好的迂回解決該問題。

        如何使用

        在服務端使用 Docker 啟動服務。其中 WECHAT_IDWECHAT_SECRET 是在微信公眾號后臺開發-基本配置中獲取的“開發者ID”和“開發者密碼”。而 ALLOW_HOST_LIST 是為了避免服務被濫用,允許開發者配置允許使用內嵌服務的網站??梢允褂枚禾柶唇佣鄠€域名,例如 imnerd.org,eming.li。不在該列表中的域名會直接跳轉會源地址。

        docker run 
          -e WECHAT_ID=<WECHAT_ID> 
          -e WECHAT_SECRET=<WECHAT_SECRET> 
          -e ALLOW_HOST_LIST=<ALLOW_HOST_LIST> 
          -p 8360:8360
          lizheming/wxhermit

        而對于需要使用該服務的網站,需要在頁面中增加以下代碼用于自定義分享文案。其中 wxhermit 是固定值,其它的為自定義文案內容。

        <script>
        if (window.parent !== window) {
          window.parent.postMessage({ 
            type: 'wxhermit',
            title: '自定義分享的標題',
            desc: '自定義分享的描述',
            imgUrl: '自定義分享的封面圖'
          }, '*');
        }
        </script>

        配置好后就可以在微信使用 <domain>/?url=<url> 來訪問了,其中 <domain> 是你的已綁定的安全域名,<url> 則是在 ALLOW_HOST_LIST 中配置的可使用域名下的網址。

        后記

        通過代理的形式很好的解決了我未備案域名需要自定義分享的問題。為了能讓體驗更自然,我在我的網站中增加了在微信中自動跳轉至該嵌套頁面的邏輯。

        <script>
        if(/micromessenger/i.test(navigator.userAgent) && window.parent === window) {
            location. + encodeURIComponent(location.href);
        }
        </script>

        不過它的缺點也很明顯,本質相當于將所有的域名都掛靠在某個安全域名之下。所以在微信下拉顯示網站地址的時候都還是顯示該安全域名。而且子域如果出現內容問題的話風險也全部在該安全域名上,所以建議是 ALLOW_HOST_LIST 配置個人可控域名。

        除了我的這種方案之外,也有配置 <meta> 信息通過 Safari 調用系統的分享功能設置封面圖和文案的方式,以及通過 QQ 瀏覽器分享自動獲取頁面第一張大圖的形式自定義分享卡片。不過它們在可定制和確定性上都要稍微弱一些,可以根據實際情況選擇使用。

        查看原文

        贊 2 收藏 1 評論 0

        公子 發布了文章 · 1月24日

        React Server Component 可能并沒有那么香

        前段時間 React 團隊發布了一項用于解決 React 頁面在多接口請求下的性能問題的解決方案 React Server Components。當然該方案目前還在草案階段,官方也只是發了視頻和一個示例 demo 來說明這個草案。

        Server Components

        官方在視頻和 RFC 中說明了產生這個方案的主要原因是因為大量的 React 組件依賴數據請求才能做渲染。如果每個組件自己去請求數據的話會出現子組件要等父組件數據請求完成渲染子組件的時候才會開始去請求子組件的數據,也就是官方所謂的 WaterFall 數據請求隊列的問題。而將數據請求放在一起請求又非常不便于維護。

        既然組件需要數據才能渲染,那為什么接口不直接返回渲染后的組件呢?所以他們提出了 Server Components 的解決方案。我們暫且不管這其中的邏輯有沒有道理,先來看看該方案的大體流程是怎樣的。

        方案的大概就是將 React 組件拆分成 Server 組件(.server.tsx)和 Client 組件(.client.tsx)兩種類型。其中 Server 組件會在服務端直接渲染并返回。與 SSR 的區別是 Server Components 返回的是序列化的組件數據,而不是最終的 HTML。

        可能帶來的問題

        通過接口將組件和組件的數據一并返回的方式帶來了打包體積的優勢,但是它真的能像 React Hooks 一樣香嗎?我覺得并不然。

        接口返回

        常規做法里前端 JS 中加載組件,接口返回組件需要的數據。而 React Server Components 中則是將二者合二為一,雖然在打包體積上有所優化,但是明顯是把這體積轉義到了接口返回中。特別是在類似列表這種有分頁的請求中,這種劣勢會更明顯。明明組件只需要在初始的時候進行加載,但是因為被融合進接口里了,每次接口都會返回冗余的組件結構,這樣也不知道是好還是不好??赡芎罄m需要優化一下接口二次返回只返回數據會比較好。

        服務器成本問題

        這里所說的服務器成本有很多,首先是機器本身的成本。將客戶端渲染行為遷移到服務端時候勢必會增加服務端的壓力,用戶量上來之后這塊的成本是成量級的在增加的。關于這個問題,官方提供的回復是隨著服務器的成本降低勢必 Server Components 帶來的優勢會抵消這塊的劣勢。

        Question: This might become more expensive for applications. In the search demo, finding those search results plus rendering them on the server is a more expensive operation than just an API call sent from the client.

        Reply: We are moving some of the rendering to the server–so it's true that your server will be doing more work than before. But server costs are constantly going down, and far more powerful than most consumer devices. I think React Server Components will give you the ability to make that tradeoff and choose where you best want the work to be done, on a per component basis. And that's not something that's easily possible today.
        via: 《RFC: React Server Components》

        不過以目前我所在的業務情況來看,服務器的成本還是非常貴的,為了降低成本大家紛紛將邏輯下發到邊緣計算甚至是客戶端處理。一方面是為了節省成本,另一方面也是為了降低壓力加快處理。

        除了機器本身的成本之外,請求的成本也會增加。畢竟除了數據請求之外還要處理組件渲染,而且這塊作為組件耦合不好進行拆分。相比較常規方案,使用 JS 文件加載組件到客戶端,接口單純返回數據,這塊的時間成本增加了非常多。特別是常規方案中 JS 文件加載完之后是在瀏覽器中緩存的,后續的成本非常小。

        體積問題可能還好,但是請求時間增加了這個可能就非常致命了。

        心智負擔

        這點在 RFC 中也有說明。由于 Server Components 中無法使用 useState, useReduce, useEffect, DOM API 等方法,勢必這會給使用者帶來大量的心智負擔。雖然官方說會使用工具讓開發者做到無感,且會提供運行時報錯,但是我相信光是想什么時候需要寫 Server Componet 什么時候需要寫 Client Component 就已經腦殼疼了吧,更別提還有個 Shared Component 了。

        另外還有就是增加了跨端的流程之后,調試的成本也會變的非常高。別說很多人沒有服務端的經驗,就算是有相關經驗的同學可能也沒辦法很好的在服務端進行快速定位。關于這個問題官方提供的說法是可以依賴內部的錯誤監控和日志服務。

        回歸問題的本質

        讓我們回歸到問題的本質,React Server Component 的目的其實是為了解決接口請求分散在各組件中帶來的子組件的數據請求需要等待父組件請求完成渲染子組件時才能開始請求的數據請求隊列問題。那么除了 Server Component 之外沒有其它的解決方案了嗎?其實不然。

        import React, {useState, useEffect} from 'react';
        import ReactDOM from 'react-dom';
        
        function App() {
          const [data, setData] = useState([]);
          useEffect(() => {
            fetchData.then(setData);
          }, []);
          
          return (
            <div>
              {!data.length ? 'loading' : null}
              <Child data={data} />
            </div>
          );
        }
        
        function Child({data}) {
          const [childData, setData] = useState([]);
          useEffect(() => {
            fetchChildData.then(setData);
          }, []);
          
          if(!data.length) {
            return null;
          }
          
          return (
            <div>{data.length + childData.length}</div>
          );
        }
        
        ReactDOM.render(<App />, document.querySelector('#root'));

        如示例代碼所示,只要加載組件,但是在無數據情況下不返回 DOM 也是可以做到子組件的數據先請求而無需等待的。當然這種需要認為的在寫法上進行優化,但我也仍然認為比大費周章的去做 Server Component 要好很多。

        至于 Server Component 帶來的打包體積優化這個問題,我覺得 RFC 里面的評論說的非常的好?!北绕?83KB(gzip 后大概是 20KB)打包體積,我覺得在項目中為了格式化日期使用一個 83KB 的庫這才是更大的問題?!?/p>

        Removing a 83KB (20KB gzip) library isn't a big deal, I would say the bigger problem here is that you're using a 83KB library to format dates.
        via: 《RFC: React Server Component》

        實際上官方列舉的兩點關于日期處理以及 Markdown 格式處理的庫,可以看到都是針對于數據進行處理的需求。針對這種情況如果覺得這塊的體積非?!辟F“的話完全是可以讓服務端將格式化后的數據返回,這樣豈不是更小成本的解決了這個問題?

        后記

        看完 《RFC: React Server Component》 中所有的討論,大部分人對 Server Component 還是持不贊成的態度的,認為它可能并沒有像 React Hooks 那樣解決業務中的實際痛點。就目前暴露的提案,我個人也覺得 Server Component 是弊大于利的。目前就期望官方如果要實現的話能解耦實現,不要影響未使用 Server Component 的 React 用戶打包體積。

        當然該提案我覺得不是沒有好處,它最大的好處我個人認為是帶來了 React 組件序列化的官方標準。為多端、多機、多語言之間實現 React 組件交流提供了基礎?;谶@套序列化方案,我們可以實現組件緩存存儲,多機器并發渲染組件等。至于多語言實現也是在 RFC 討論中大家比較關心的問題,通過這套序列化標準讓其它語言去實現 React 組件也不是沒有可能。

        查看原文

        贊 6 收藏 4 評論 0

        公子 發布了文章 · 2020-12-27

        靜態博客如何高性能插入評論

        ?? 前言

        我們知道,靜態博客由于不帶有動態功能,所以針對評論這種動態需求比較大眾的做法就是使用第三方評論系統。第三方評論的本質其實就是使用 JS 去調取第三方服務接口獲取評論后動態渲染到頁面中。雖然它很好的解決了這個問題,但是由于需要請求接口,在體驗上遠比動態博客的直出效果要差很多。所以當我把博客從動態博客 Typecho 遷移到靜態博客 Hugo 上來時,就一直在思考這個問題。直到我看到了 Hugo 的 getJSON 方法,發現原來靜態博客也是能夠像動態博客一樣直出評論的。

        大部分的靜態博客的原理是解析存儲內容的文件夾,使用一些模板語言遍歷數據生成一堆 HTML 文件。而 Hugo 除了解析 Markdown 內容之外,還支持額外的數據獲取方法 getJSON。由于有了 getJSON 方法的出現,我們可以實現在博客編譯構建過程中動態的去獲取評論接口數據,將其渲染到頁面中,實現評論數據的直出效果。關于 getJSON 的更多介紹,可以查看 Hugo 文檔數據模板一節。

        ?? 方案

        高性能方案基本思路是在需要評論數據的地方通過 getJSON 方法調用接口獲取評論數據并進行模板渲染。當評論更新的時候,我們需要觸發重新構建。實現這個方案依賴三個關鍵要素:

        1. 構建過程支持調取接口獲取數據
        2. 評論服務提供 HTTP 接口返回數據
        3. 博客部署服務支持鉤子觸發重新構建

        我的博客使用的是 Hugo 靜態博客系統,如上文所說通過 getJSON 即可解決第一個問題。而我的評論服務使用的是自研的 Waline 評論系統,它提供了評論數、評論列表、最近評論等基礎接口滿足我們的數據獲取需求。并且 Waline 提供了豐富的鉤子功能,支持在評論發布的時候觸發自第一方法。我的博客部署在 Vercel 上,它提供了 Deploy Hooks 功能,通過 URL 即可觸發重新構建。也就是說我只要在 Waline 評論發布的鉤子中調用 Vercel 的鉤子 URL 觸發重新構建即可解決第三個問題。

        ?? 實現

        我的博客上有三處地方和評論有關,分別是首頁側邊欄的最近評論,文章標題下方的評論數,以及文章詳情頁底部的評論列表展示。

        ?? 最近評論

        Waline 最近評論接口:文檔

        {{ $walineURL := .Site.Params.comment.waline.serverURL }}
        <h2 class="widget-title ">最近回復</h2>
        <ul class="widget-list recentcomments">
          {{ $resp := getJSON $walineURL "/comment?type=recent" }}
          {{ range $resp }}
          <li class="recentcomments">
            <a href="{{.Site.BaseURL}}{{ .url }}">{{ .nick }}</a>:{{ .comment | safeHTML | truncate 22 }}
          </li>
          {{ end }}
        </ul>

        ?? 文章評論數

        Waline 獲取文章對應的評論數接口:文檔

        {{ $walineURL := .Site.Params.comment.waline.serverURL }}
        {{ $count := getJSON $walineURL "/comment?type=count&url=/" .Slug ".html" }}
        <a href="{{ .Permalink }}#comments" title="{{ .Title }}">
          <i class="fas fa-comment mr-1"></i>
          <span>{{- if gt $resp 0}}{{$resp}} 條評論{{else}}暫無評論{{end -}}</span>
        </a>

        ?? 評論列表

        評論列表由于有分頁的存在,不像最近評論和評論數一樣簡單的調用接口即可。先獲取評論數,發現有評論時先獲取第一頁的評論,主要是用來獲取總共有多少頁評論。之后再從第二頁開始循環獲取評論數據。最終將獲取到的數據全部存到 {{$scratch.Get "comments"}} 數組中,使用模板語法渲染該數組數據即可。

        {{$baseUrl := .Site.Params.comment.waline.serverURL}}
        {{$slug := .Slug}}
        {{$count := getJSON $baseUrl "/comment?type=count&url=/" $slug ".html" }}
        {{$scratch := newScratch}}
        {{$scratch.Add "comments" slice}}
        
        {{if gt $count 0}}
          {{$comments := getJSON $baseUrl "/comment?path=/" $slug ".html&page=1&pageSize=100"}}
          {{range $cmt := $comments.data}}
            {{$scratch.Add "comments" $cmt}}
          {{end}}
        
          {{$totalPages := $comments.totalPages}}
          {{if gt $totalPages 1}}
            {{range $page := seq 2 $totalPages}}
              {{$comments := getJSON $baseUrl "/comment?path=/" $slug ".html&pageSize=100&page=" $page}}
              {{range $cmt := $comments.data}}
                {{$scratch.Add "comments" $cmt}}
              {{end}}
            {{end}}
          {{end}}
        {{end}}
        
        <div class="vcards">
          {{range $cmt := $scratch.Get "comments"}}
          <div class="vcard" id={{$cmt.objectId}}>
            <img class="vimg" data-original="https://gravatar.loli.net/avatar/{{$cmt.mail}}?d=mp">
            <div class="vh">
              <div class="vhead">
                <a class="vnick" rel="nofollow" href="{{$cmt.link}}" target="_blank">{{$cmt.nick}}</a>
                <span class="vsys">{{$cmt.browser}}</span>
                <span class="vsys">{{$cmt.os}}</span>
              </div>
              <div class="vmeta">
                <span class="vtime">{{dateFormat $cmt.insertedAt "2006-01-02 03:04:05"}}</span>
                <span class="vat">回復</span>
              </div>
              <div class="vcontent" data-expand="查看更多...">
                {{$cmt.comment | safeHTML}}
              </div>
              <div class="vreply-wrapper"></div>
              <div class="vquote">
                {{range $cmt := $cmt.children}}
                <div class="vh" id="{{$cmt.objectId}}">
                  <div class="vhead">
                    <a class="vnick" rel="nofollow" href="{{$cmt.link}}" target="_blank">{{$cmt.nick}}</a>
                    <span class="vsys">{{$cmt.browser}}</span>
                    <span class="vsys">{{$cmt.os}}</span>
                  </div>
                  <div class="vmeta">
                    <span class="vtime">{{dateFormat $cmt.insertedAt "2006-01-02 03:04:05"}}</span>
                    <span class="vat">回復</span>
                  </div>
                  <div class="vcontent" data-expand="查看更多...">
                    {{$cmt.comment | safeHTML}}
                  </div>
                  <div class="vreply-wrapper"></div>
                </div>
                {{end}}
              </div>
            </div>
          </div>
          {{end}}
        </div>

        ?? 構建觸發

        Waline 在評論發布、更新和刪除階段都支持自定義鉤子,在鉤子中觸發 Vercel 的構建鉤子即可完成發布評論重新構建的流程。

        按照如下內容修改服務端部署的 index.js 文件,查看文檔了解全部的 Waline 鉤子。

        const Waline = require('@waline/vercel');
        const https = require('https');
        const buildTrigger = _ => https.get('https://api.vercel.com/v1/integrations/deploy/xxxxx');
        
        module.exports = Waline({
          async postSave(comment) {
            if(comment.status !== 'approved') {
              return;
            }
            buildTrigger();
          },
          async postUpdate() {
            buildTrigger();
          },
          async postDelete() {
            buildTrigger();
          }
        });

        ?? 后記

        通過以上操作,就能在不損失用戶體驗的情況下實現評論數據的動態支持了。有些人可能會擔心是否會在構建階段造成超多的接口請求。這里大可不用擔心,Hugo 自己會在構建的時候做接口的緩存,同 URL 的接口調用會走緩存數據而不會重新調用。

        除了用戶體驗之外,由于只會在構建的時候觸發數據的獲取,針對有調用次數配額的第三方評論服務也能節省額度。當然,理論上構建次數是遠小于訪問次數的,所以額度節省的結論是能成立的。如果說你的構建次數要比訪問次數還要大的話,那這種方法就無法節省額度了。

        當然這種方式也會有帶來些問題,主要是評論的更新沒那么快。好在 Hugo 的構建速度非???,一兩分鐘的時間也能接受。而針對用戶評論的發布,則可以通過評論發布后先假插入緩解該問題。

        查看原文

        贊 1 收藏 0 評論 0

        公子 贊了文章 · 2020-12-17

        基于 ThinkJS 的云開發體驗

        背景

        ThinkJS 是一款企業級的 Node.js Web 開發框架,致力于集成項目最佳實踐,規范項目讓企業級團隊開發變得更加簡單,更加高效。 它基于 Koa 2.0 開發,兼容 Koa 的所有 Middleware。 內核小巧,支持 Adapter, Extend 等多種插件擴展方式,框架內的大部分功能也是通過這些方式進行擴展的。 性能優異,支持 TypeScript。

        云開發 CloudBase 是云原生一體化應用研發平臺為開發者提供高可用、自動彈性擴縮的后端云服務,包含計算、存儲、托管等能力,可用于云端一體化開發多種端應用(小程序、公眾號、Web 應用、Flutter 客戶端等),幫助開發者統一構建和管理后端服務和云資源,避免了應用開發過程中繁瑣的服務器搭建及運維,開發者可以專注于業務邏輯的實現,開發門檻更低,效率更高。

        在云開發中使用ThinkJS

        其實在云開發中使用 ThinkJS 和我們日常使用大同小異,除了啟動文件需要按照云開發的要求修改一下以外,內部的業務邏輯基本不需要改動。 我們可以使用云開發的 CLI 工具快速的初始化一個適配云開發的 ThinkJS 項目。 其中 thinkjs-app 是你的項目文件夾名稱。

        tcb new thinkjs-app thinkjs-starter

        初始化完畢進入項目目錄后執行 npm install 安裝好依賴,就可以通過 npm start 啟動開發環境了。

        如果一切正常,你可以通過訪問 http://127.0.0.1:8360 看到經典的 ThinkJS 的初始化界面了。

        之后我們就可以愉快的進行項目的開發了。 當項目開發完畢之后,直接執行如下命令,CLI 工具就會將我們的代碼部署到云函數上啦!

        由此可見,通過云開發,我們的開發部署流程變的更加簡單了。打通流程之后具體就是業務邏輯的開發了,關于更多 ThinkJS 和云開發的開發指南可參見官方文檔 。

        除了上述的云函數,云開發還提供了云數據庫和云存儲服務方便我們的項目快速接入相關服務。 數據庫這塊是比較經典的 NoSQL 的數據庫操作,使用云開發的 SDK 可以非常方便的進行操作。

        const cloudbase = require(‘@cloudbase/node-sdk’);
        
        const app = cloudbase.init({…});
        
        const db = app.database();
        
        const _ = db.command;
        
        let userInstance = db.collection(‘User’);
        
        userInstance = userInstance.where({create_time: _.gt(new Date(2020, 0, 1))});
        
        const users = await userInstance.get();

        大家可以直接通過在 ThinkJS 中調用 SDK 的方法實現數據庫的查詢操作。

        需要注意是:ThinkJS 中模型的操作都是單例的,模型實例創建后之后的所有實例操作都會對當前實例有影響。 而云開發的 SDK 是純函數形式的,因此每次操作完都會返回被操作后的對象,不會對源對象進行修改,所以我們需要通過變量覆蓋的形式獲取最新的操作結果。

        總結

        云開發作為基于 Serverless 的應用開發平臺,不僅能方便的將我們的項目無服務器化,同時也提供了其它業務常用服務的快速接入,讓我們的項目開發更加便捷。

        產品介紹

        云開發(Tencent CloudBase,TCB)是騰訊云提供的云原生一體化開發環境和工具平臺,為開發者提供高可用、自動彈性擴縮的后端云服務,包含計算、存儲、托管等serverless化能力,可用于云端一體化開發多種端應用(小程序,公眾號,Web 應用,Flutter 客戶端等),幫助開發者統一構建和管理后端服務和云資源,避免了應用開發過程中繁瑣的服務器搭建及運維,開發者可以專注于業務邏輯的實現,開發門檻更低,效率更高。
        開通云開發:https://console.cloud.tencent.com/tcb?tdl_anchor=techsite
        產品文檔:https://cloud.tencent.com/product/tcb?from=12763
        技術文檔:https://cloudbase.net?from=10004
        技術交流加Q群:601134960
        最新資訊關注微信公眾號【騰訊云云開發】

        查看原文

        贊 2 收藏 1 評論 0

        公子 發布了文章 · 2020-11-16

        基于 Serverless 的 Valine 可能并沒有那么香

        Valine 是一款樣式精美,部署簡單的評論系統, 第一次接觸便被它精美的樣式,無服務端的特性給吸引了。它最大的特色是基于 LeanCloud 直接在前端進行數據庫操作而無需服務端,極大的縮減了部署流程,僅需要在靜態頁引入 Valine SDK 即可。

        ?????? 初識 Valine

        以下是 Valine 官網提供的快速部署腳本,其中 appIdappKey 是你在 LeanCloud 上創建應用后對應的應用密鑰。也正是基于這對密鑰,Valine 在內部調用了 LeanCloud SDK 進行數據的獲取,最終將數據渲染在 #vcomments 這個 DOM 上。這便是 Valine 的大概原理。

        <head>
          ..
          <script data-original='//unpkg.com/valine/dist/Valine.min.js'></script>
          ...
        </head>
        <body>
          ...
          <div id="vcomments"></div>
          <script>
            new Valine({
              el: '#vcomments',
              appId: 'Your appId',
              appKey: 'Your appKey'
            })
          </script>
        </body>

        有同學可能會有疑問了,appIdappKey 都直接寫在前端了,那豈不是誰都可以修改數據了?這就需要牽扯到 LeanCloud 的數據安全問題了,官方專門寫了篇文檔《數據和安全》 來說明這個問題。簡單的理解就是針對數據設置用戶的讀寫權限,確保正確的人對數據有且僅有正確的權限來保證數據的安全。

        乍聽一下,保證用戶數據只讀的話,感覺還是挺安全的??墒聦嵳娴娜绱嗣?,讓我們繼續來看看。

        ???♂? Valine 的問題

        ?? 閱讀統計篡改

        Valien 1.2.0 增加了文章閱讀統計的功能,用戶訪問頁面就會在后臺 Counter 表中根據 url 記錄訪問次數。由于每次訪問頁面都需要更新數據,所以在權限上必須設置成可寫,才能進行后續的字段更新。這樣就造成了一個問題,實際上該條數據是可以被更新成任意值的。感興趣的同學可以打開 https://valine.js.org/visitor... 官網頁面后進入控制臺輸入以下代碼試試。試完了記得把數改回去哈~

        const counter = new AV.Query('Counter');
        const resp = await counter.equalTo('url', '/visitor.html').find();
        resp[0].set('time', -100001).save();
        location.reload();

        可以看到該頁面的訪問統計被設置成了 -100000 了。這個問題唯一值得慶幸的是 time 字段的值是 Number 類型的,其它的值都無法插入。如果是字符串類型的話就是一個 XSS 漏洞了。

        該問題有一個解決辦法,就是不使用次數累加的存儲方式。更改為每次訪問都存儲一條只讀的訪問記錄,讀取的時候使用 count() 方法進行統計。這樣所有數據都是只讀的,就不存在篡改的問題了。這種解決方案唯一的問題就是數據量會比較大,對查詢會造成一定壓力。當然如果是在基于原數據不變的情況下,只能是增加一層服務端來做修改權限的隔離了。

        ?? XSS 安全

        從很早的版本開始就有用戶報告了 Valine 的 XSS 問題,社區也在使用各種方法在修復這些問題。包括增加驗證碼,前端XSS過濾等方式。不過后來作者才明白,前端的一切驗證都只能防君子,所以把驗證碼之類的限制去除了。

        現有的邏輯里,前端發布評論的時候會將 Markdown 轉換成 HTML 然后走一下前端的一個 XSS 過濾方法最后提交到 LeanCloud 中。從 LeanCloud 中拿到數據之后因為是 HTML 直接插入進行顯示即可。很明顯,這個流程是存在問題的。只要直接提交的是 HTML 而且拿到 HTML 之后直接進行展示的話,XSS 從根本上是無法根除的。

        那有沒有根本的解決辦法?其實是有的。針對存儲型的 XSS 攻擊,我們可以使用轉義編碼進行解決。只要效仿早前 BBCode 的做法,提交到數據庫的是 Markdown 內容。前端讀取到內容對所有 HTML 進行編碼后再進行 Markdown 轉換后展示。

        function encodeForHTML(str){
          return ('' + str)
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')    
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#x27;')
            .replace(/\//g, '&#x2F;');
        };

        由于 Serverless 攻擊者是可以直達存儲階段,所以數據存儲之前的一切防范是無效的,只能在讀取展示過程處理。由于所有的 HTML 轉義后無法解析,Markdown 相當于我們根據自定義的語法解析成 HTML,保證轉換后的 HTML 沒有被插入的機會。

        不過這個方法存在一個問題,那就是對老數據存在不兼容。因為這相當于修改了存儲和展示的規則,而之前一直存儲的都是 HTML 內容,修復后之前的數據將無法展示 HTML 樣式。而為了能在存儲的還是 HTML 情況下規避 XSS 安全問題,唯一的辦法就是增加服務端中間層。存儲階段增加一道閥門,將轉義階段提前至存儲階段,保證新老數據的通用。

        ?? 隱私泄露

        說完了存儲的問題,我們再來看看讀取的問題。攻擊者除了可以直達存儲,也可以直達讀取,當一個數據庫的字段開放了讀取權限后,相當于該字段的內容對攻擊者是透明的。

        在評論數據中,有兩個字段是用戶比較敏感的數據,分別是 IP 和郵箱。燈大甚至專門寫了一篇文章來批判該問題 《請馬上停止使用Valine.js評論系統,除非它修復了用戶隱私泄露問題》。甚至掘金社區在早期使用 LeanCloud 的時候也暴出過泄露用戶手機號的安全問題。

        為了規避這個問題,Valine 作者增加了 recordIP 配置用來設置是否允許記錄用戶 IP。由于是 Serverless,目前能想到的也只是不存儲的方式解決了。不過該配置項會存在一個問題,就是該配置項的配置權在網站,隱私的問題是評論者遇到的,也就是說評論者是無權管理自己的隱私的。

        除了這個矛盾點之外,還有就是郵箱的問題。郵箱本質上只需要返回 md5 用來獲取 Gravatar 頭像即可。但是由于無服務端的限制,只能返回原始內容由前端計算。而郵箱我們又需要獲取到原始值,方便做評論回復郵件通知功能。所以我們也不能不存儲,或者存儲 md5 后的值。

        該問題的解決方案只能是增加一層服務端,通過服務端過濾敏感信息解決這個問題。

        ?? Waline!

        基于以上原因,我們發現只有增加一層服務端中間層才能很好的解決 Valine 的安全問題,所以 Waline 橫空出世了!Waline 與 Valine 最大的不同就是增加了服務端中間層,解決 Valine 暴露出來的安全問題。同時基于服務端的特性,提供了郵件通知、微信通知、評論后臺管理、LeanCloud, MySQL, MongoDB, SQLite, PostgreSQL 多存儲服務支持等諸多特性。不僅如此,Waline 默認使用 Vercel 部署,實現完全免費部署!

        Waline 最初的目標僅僅是為 Valine 增加上服務端中間層。但是由于作者不知為何從 1.4.0 版本開始只推送編譯后的文件到 Github 倉庫中,源文件停止更新。導致我只能連帶前端也實現一遍。當然前端的很多代碼和邏輯為了和 Valine 的配置保持一致都有參考 Valine,甚至在名字上,我也是從 Valine 上衍生的,讓大家能明白這個項目是 Valine 的衍生版。

        ?? 后記

        Serverless 的概念火了非常多年,但技術沒有銀彈,我們在看到它的優點的同時,也要正視它所帶來的問題。而 Serverless 自己可能也意識到了這個問題,從早期的無服務端慢慢轉向了無服務器,更偏向 BaaS 了。不過由于 Valine 沒有開放源代碼,所以上面說的一些問題和解決方法只能等待作者自己發現這件事了。

        查看原文

        贊 9 收藏 2 評論 0

        公子 發布了文章 · 2020-11-09

        Hugo 之旅

        之前寫了篇文章《博客遷移至 Hugo》,提了下使用 Typecho 多年后越發感受到運維的成本之高后,將博客遷移到了靜態博客程序 Hugo 下。使用 Vercel + Github 可以免費搭建高性能博客,綁定域名還能自動幫忙創建 SSL 證書。當然偷懶的話也可以直接使用默認分配的二級域名。

        搭建

        創建 Hugo 博客

        點擊上面的按鈕快速抵達創建頁面,未登錄的會需要登錄,這塊直接使用 Github 登錄即可。登錄后第一步會讓你選擇 Vercel 賬號,這里直接選擇 Personal Account 即可。之后會讓你輸入倉庫名稱,Vercel 會自動幫你創建并初始化該倉庫。如果你的倉庫不想讓其它人看的話,這里可以勾選 Private Git Repository 創建私有倉庫。

        下一步這塊會讓你輸入 Vercel 中項目的名稱和一些配置。這里需要注意一下,官方提供的默認 Hugo 編譯命令會把草稿文章也生成出來。需要在 BUILD COMMAND 那打開 OVERRIDE 按鈕后輸入 hugo --gc 進行覆蓋。

        稍等片刻之后,你就可以看到飄著滿屏的彩帶慶祝你創建博客成功叻!點擊 Visit 按鈕你就可以看到你的博客的樣子了。

        由于是靜態博客,你所有的文章都會存儲在你剛才新建的倉庫中。你可以選擇將倉庫下來修改后提交,也可以利用 Github 的在線編輯功能在線修改提交。提交之后 Vercel 會自動觸發更新,重新構建并更新你的博客。

        配置

        默認每次提交 Vercel 構建完成之后都會把構建后的地址評論在你的 Github 提交下。你可以通過設置關閉該功能。另外默認 Vercel 指定的 Hugo 版本比較老了,在 Markdown 編譯過程中會發生一些異常的行為。我們可以通過配置指定最新的 Hugo 版本進行編譯。在項目根目錄下新建 vercel.json 文件,并加入以下內容。

        {
          "github": {
            "silent": true
          },
          "build": {
            "env": {
              "HUGO_VERSION": "0.78.1"
            }
          }
        }

        Hugo 博客本身的配置都在 config.toml 下。默認情況下 Hugo 生成的 URL 都是 /posts/hello-world/ 這種格式,不過之前做動態博客為了做偽靜態,一般都將路由設置成了 /hello-world.html 這種格式。這種時候就需要在 config.toml 中增加 uglyurls 配置。

        uglyurls = true
        [permalinks]
          posts = ":slug"

        后面的 permalinks 配置主要是用來去除文章的 /posts 前綴的。除此之外,默認的配置文章中的 HTML 是會被轉義的,對于我這種偶爾會在 Markdown 中寫 HTML 的人來說操作有點多余。這時候可以在配置中定義它不轉義。

        [markup.goldmark.renderer]
          unsafe= true

        域名

        使用 Vercel 搭建的網站,它會默認提供一個 *.vercel.app 的二級域名,你可以直接使用這個域名訪問網站。如果你想要綁定自己的域名,也可以在后臺設置。進入網站后選擇自己的項目,選擇 Settings - Domains 進入域名配置界面,在輸入框中添加自己的域名。它會提示你需要給域名增加 A 記錄或者是 CNAME 解析。按照提示添加后后臺會自動檢測是否生效。

        生效的時間視 DNS 服務器的生效時間而定,我這邊使用 DNSPod 還挺快的,大概 30 秒之內就生效了。生效后 Vercel 會自動幫我們申請配置 SSL 證書,我們全然不用操心證書的問題。等待片刻之后我們就能直接使用新域名進行訪問了。

        圖片

        默認所有的靜態資源都放在 static/ 目錄下。你可以將圖片放在該目錄下,例如 static/hello-world.jpg。在文章中則直接使用 /hello-world.jpg 地址引用即可。

        由于 Github 僅有單文件小于 100M 的限制,Vercel 會將所有的資源部署到自己服務器上。所以使用倉庫存儲的方式會非常方便和安全,而且還不損失速度。唯一美中不足的是,由于是 HTTP 路徑,在本地編寫文章的時候會不方便。如果是使用 Typora 的話可以點擊標簽欄 格式 - 圖像 - 設置圖片根目錄 將目錄指定到 static/ 目錄解決。VSCode 的話暫時沒有倒騰出來。

        其實比較好的方案是建議大家創建一個文件夾,將該篇文章和它所用到的圖片都歸置到一塊。使用類似組件化的思路管理內容,會更加方便后續的修改。

        除了使用本地存儲之外,你也可以選擇使用第三方的存儲服務。免費服務的話可以試試 又拍云 提供的聯盟計劃,按照要求申請下即可。其他家的存儲服務要么免費時間有限,要么有部分收費功能。第三方存儲的好處在于帶有 CDN 加速,在速度上會很方便。但目前 Vercel 的速度我覺得還挺不錯的,所以最終還是選擇了直接存倉庫。

        評論

        使用靜態博客之后,評論則只能選擇第三方評論系統了。鑒于國情,國內的第三方評論服務都已名存實亡。目前比較知名的 Disqus , HyperComments 以及 來必力 都是國外的服務。HyperComments 是付費的服務就不多做討論,Disqus 是老牌服務,提供了強大的 API 對開發者較為友好。來必力則是對國內用戶友好,集成了很多國內的社交賬號登錄。

        除了專門提供評論的第三方服務之外,也有一部分是選擇自建評論服務。其中比較知名的是 ValineGitalk 以及 Staticman,isso。Staticman 提供了強大的 API 但是缺少界面,isso 則是需要服務器部署,偏離了我使用靜態博客的初衷就是不想維護服務器的目的,所以這兩者都不多做討論。Gitalk 是使用 Github issue 進行評論數據存儲的評論腳本,適合純技術博客和極客使用。Valine 則是基于LeanCloud Serverless 云存儲進行評論數據存儲的評論腳本,同時它還帶有漂亮的外觀,適合各類人群使用。

        博客站點上與評論相關的地方一般有三個地方:

        1. 首頁顯示最近評論
        2. 文章下顯示當前文章評論數
        3. 文章底部顯示該文章的評論列表以及輸入框

        最開始我選擇了 Disqus 作為本站的評論系統。但是它有個問題國內正常無法訪問。后來我發現 Hugo 的模板中是可以使用 getJSON 方法調取接口的。而 Vercel 的機器本身就在海外,那我實際上可以在博客編譯階段就獲取到輸入寫到頁面中。最近評論和評論數的顯示還是比較簡單的,而評論列表的顯示則需要折騰一下了。不過 Disqus API 的使用頻率是 1000 次/小時,當短時間部署頻繁的話可能會有超過的風險。而且發布評論的接口沒辦法走該邏輯,還是需要一個代理服務。

        最終讓我決定不使用 Disqus 的原因是它的評論沒辦法讓評論者輸入網站地址,評論列表中的評論者昵稱點擊也無法跳轉到用戶輸入的網站中。這對于有著大量的老式評論數據的我來說是不太能接受的。所以基于這些種種原因,我最后又將評論遷移到了 Valine 下。

        我對 Valine 的主要問題在于兩點:

        1. 早前使用 Valine 的時候發現了大量的 XSS 漏洞
        2. 荒野無燈大大反饋的“泄露用戶隱私問題

        以上兩個是非常重要的安全問題,不過我看 XSS 的問題作者已經修復,實際上對用戶輸入的所有 HTML 內容進行轉義可以規避該問題。而燈大反饋的 IP 泄露的問題,作者也通過不記錄 IP 來規避該問題。這兩個問題造成的原因其實還是因為 Serverless 。因為我想直接在編譯的時候將相關數據靜態化,所以我勢必是需要增加一個服務端來幫我進行接口的封裝,最后也能規避這些問題。

        不過 Valine 不知因為什么原因不開放源碼,只在 Github 提供編譯后代碼,這點除了讓我比較無語之外,改造工作也比較慢。本來我只需要將它 UI 中調用 LeanCloud API 的邏輯替換成我的接口邏輯即可。但是因為沒有源碼,只能自己重新制作 UI 了。

        更新:基于 Valine 衍生的帶后端評論系統已經完成 https://github.com/lizheming/... 已切換至該評論系統。2020/11/08

        搜索

        缺少服務端之后,靜態博客的搜索功能也無法自己完成。目前比較知名的是第三方搜索服務 Agolia,提供了免費的文章索引和搜索的功能。除了使用第三方服務之外,還發現有一種比較簡單的做法就是將所有數據生成到一個文件中,前端下載該文件進行搜索結果展示。這里頭比較知名的是 fuse,它是一個 JS 模塊,將輸入傳入之后它能幫我們快速的匹配到命中結果。

        目前本站使用的是后一種方法,該方法的優點是不依賴第三方服務,自己就能完成搜索功能。缺點是第一次搜索的時候需要下載完整的數據,對文章比較多的網站用戶鴨梨會比較大。使用第二種方法第一步是需要先創建全量的數據索引,通過以下配置告知 Hugo 編譯時創建 index.json 索引文件。

        [outputs]
          home = ["HTML", "RSS", "JSON"]

        當然我們還需要為新的數據編寫生成的模板文件,在 layouts/ 下增加 index.json 文件,并加入以下內容:

        {{- $.Scratch.Add "index" slice -}}
        {{- range .Site.RegularPages -}}
            {{- $.Scratch.Add "index" (dict "title" .Title "contents" .Plain  "summary" .Summary "permalink" .Permalink "date" (.Date.Format "2006年01月02日")) -}}
        {{- end -}}
        {{- $.Scratch.Get "index" | jsonify -}}

        其中 dict后的字典可以添加多個字段,視你 JS 渲染腳本中需要的字段而定。完成這些之后 Hugo 編譯就會生成 index.json 文件了。

        最后我們需要增加一個搜索頁面,該頁面會先加載 index.json 然后使用 fuse 進行數據查找,最后渲染成 HTML 輸出搜索結果。具體的代碼可以直接查看源碼 https://imnerd.org/search.htm... 參考本站的搜索頁。

        //以下為示例代碼
        let fuse;
        async function search(text) {
          if(!fuse) {
            const indexData = await fetch('/index.json', {method: 'GET'}).then(resp => resp.json());
            fuse = new Fuse(indexData, {...});
          }
          const result = fuse.search(text);
          renderHTML(result, '#app');
        }

        其它

        Hugo 靜態博客能提供給我們發揮的空間非常多,比如說我參考 屈屈的博客 增加了一個博客文章發布 >180 天的話,就在文章詳情頁黃條提醒:

        提醒:本文最后更新于 313 天前,文中所描述的信息可能已發生改變,請謹慎使用。

        正常來說由于我沒有服務端,所以只能使用前端來計算當前時間與發布時間的時間差。但其實轉念一想,編譯時也是可以拿到時間差的,只是這個時間差無法隨著時間的變化而變化。不過我只要每天編譯一次,就能解決變化的問題了。在 Vercel 項目 Settings - Git - Deploy Hooks 中你可以輸入 Hook 名稱和觸發分支創建一個 Hook URL。只要訪問該 URL 就可以觸發 Vercel 更新博客。

        {{ if eq .Type "posts" -}}
            {{ $ageDays := div (sub now.Unix .Date.Unix) 86400 }}
            {{ if gt $ageDays 180 }}
                <p class="expired-tips"> 提醒:本文最后更新于&nbsp;{{$ageDays}}&nbsp;天前,文中所描述的信息可能已發生改變,請謹慎使用。</p>
            {{ end }}
        {{ end }}

        而在 Vercel 的 Marketplace 中,有一個 EasyCron 的服務提供了定時任務的功能。我在上面設置了每天零點訪問 Hook URL 觸發博客更新,就這樣解決每天需要更新時間的問題。

        另外屈屈博客中的“查看本文Markdown版本”也是個不錯的功能,在 Hugo 中也可以實現,本質是在編譯的時候順便生成一份 .md 的 Markdown 文件即可。

        首先我們需要在 layouts/ 下增加 single.md 文件,表示的是當文章頁(Single)需要導出 .md 的數據的時候使用該模板。模板內容不用填寫,為空即可。然后在 config.toml 中新增 .md 文件類型,并告知 Hugo 文章需要增加編譯 Markdown 文件類型。

        [mediaTypes]
          [mediaTypes."text/plain"]
            suffixes = ["md"]
        
        [outputFormats.MarkDown]
          mediaType = "text/plain"
          isPlainText = true
          isHTML = false
        
        [outputs]
          page = ["HTML", "MarkDown"]
        查看原文

        贊 3 收藏 1 評論 0

        公子 發布了文章 · 2020-11-02

        常見登錄鑒權方案

        編者注:今天我們分享的是盧士杰同學整理的網站常用鑒權方案的實現原理與實現以及他們的適用場景,幫助大家在業務中做合適的選擇。

        背景

        說起鑒權大家應該都很熟悉,不過作為前端開發來講,鑒權的流程大頭都在后端小哥那邊,本文的目的就是為了讓大家了解一下常見的鑒權的方式和原理。

        認知:HTTP 是一個無狀態協議,所以客戶端每次發出請求時,下一次請求無法得知上一次請求所包含的狀態數據。

        一、HTTP Auth Authentication

        簡介

        HTTP 提供一個用于權限控制和認證的通用框架。最常用的HTTP認證方案是HTTP Basic Authentication

        鑒權流程

        加解密過程

        // Authorization 加密過程
        let email = "postmail@test.com"
        let password = "12345678"
        let auth = `${email}:${password}`
        const buf = Buffer.from(auth, 'ascii');
        console.info(buf.toString('base64')); // cG9zdG1haWxAdGVzdC5jb206MTIzNDU2Nzg=
        
        // Authorization 解密過程
        const buf = Buffer.from(authorization.split(' ')[1] || ''),  'base64');
        const user = buf.toString('ascii').split(':');

        其他 HTTP 認證

        通用 HTTP 身份驗證框架有多個驗證方案使用。不同的驗證方案會在安全強度上有所不同。

        IANA 維護了一系列的驗證方案,除此之外還有其他類型的驗證方案由虛擬主機服務提供,例如 Amazon AWS ,常見的驗證方案包括:

        • Basic (查看 RFC 7617, Base64 編碼憑證. 詳情請參閱下文.),
        • Bearer (查看 RFC 6750, bearer 令牌通過OAuth 2.0保護資源),
        • Digest (查看 RFC 7616, 只有 md5 散列 在Firefox中支持, 查看 bug 472823 用于SHA加密支持),
        • HOBA (查看 RFC 7486 (草案), HTTP Origin-Bound 認證, 基于數字簽名),
        • Mutual (查看 draft-ietf-httpauth-mutual),
        • AWS4-HMAC-SHA256 (查看 AWS docs)

        二、Cookie + Session

        注冊流程

        思考:為什么要在密碼里加點“鹽”?

        鑒權流程

        Session 存儲

        最常用的 Session 存儲方式是 KV 存儲,如Redis,在分布式、API 支持、性能方面都是比較好的,除此之外還有 mysql、file 存儲。

        如果服務是分布式的,使用 file 存儲,多個服務間存在同步 session 的問題;高并發情況下錯誤讀寫鎖的控制。

        Session Refresh

        我們上面提到的流程中,缺少 Session 的刷新的環節,我們不能在用戶登錄之后經過一個 expires 時間就把用戶踢出去,如果在 Session 有效期間用戶一直在操作,這時候 expires 時間就應該刷新。

        以 Koa 為例,刷新 Session 的機制也比較簡單:
        開發一個 middleware(默認情況下所有請求都會經過該 middleware),如果校驗 Session 有效,就更新 Session 的 expires: 當前時間+過期時間。

        優化:

        1. 頻繁更新 session 會影響性能,可以在 session 快過期的時候再更新過期時間。
        2. 如果某個用戶一直在操作,同一個 sessionID 可能會長期有效,如果相關 cookie 泄露,可能導致比較大的風險,可以在生成 sessionID 的同時生成一個 refreshID,在 sessionID 過期之后使用 refreshID 請求服務端生成新的 sessionID(這個方案需要前端判斷 sessionID 失效,并攜帶 refreshID 發請求)。

        單設備登錄

        有些情況下,只允許一個帳號在一個端下登錄,如果換了一個端,需要把之前登錄的端踢下線(默認情況下,同一個帳號可以在不同的端下同時登錄的)。

        這時候可以借助一個服務保存用戶唯一標識和 sessionId 值的對應關系,如果同一個用戶,但 sessionId 不一樣,則不允許登錄或者把之前的踢下線(刪除舊 session )。

        三、JWT

        簡介

        JSON Web Token (JWT)是一個開放標準(RFC 7519),它定義了一種緊湊的、自包含的方式,用于作為JSON對象在各方之間安全地傳輸信息。該信息可以被驗證和信任,因為它是數字簽名的。

        JWT 組成

        JWT 由三部分組成,分別是 header(頭部),payload(載荷),signature(簽證) 這三部分以小數點連接起來。

        例如使用名為 jwt-token 的cookie來存儲 JWT 例如:

        jwt-token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoibHVzaGlqaWUiLCJpYXQiOjE1MzI1OTUyNTUsImV4cCI6MTUzMjU5NTI3MH0.WZ9_poToN9llFFUfkswcpTljRDjF4JfZcmqYS0JcKO8;

        使用.分割值可以得到三部分組成元素,按照順序分別為:

        • header

          • 值:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
          • Base64 解碼: {"alg": "HS256", "type": "JWT"}
        • payload

          • 值:eyJuYW1lIjoibHVzaGlqaWUiLCJpYXQiOjE1MzI1OTUyNTUsImV4cCI6MTUzMjU5NTI3MH0
          • Base64 解碼:

            {
              "name": "lushijie", 
              "iat": 1532595255, // 發布時間
              "exp": 1532595270 // 過期時間
            }
        • signature

          • 值:WZ9_poToN9llFFUfkswcpTljRDjF4JfZcmqYS0JcKO8
          • 解碼:

            const headerEncode = base64Encode(header);
            const payloadEncode = base64Encode(payload);
            let signature = HMACSHA256(headerEncode + '.' + payloadEncode, '密鑰');

        鑒權流程

        Token 校驗

        對于驗證一個 JWT 是否有效也是比較簡單的,服務端根據前面介紹的計算方法計算出 signature,和要校驗的JWT中的 signature 部分進行對比就可以了,如果 signature 部分相等則是一個有效的 JWT。

        Token Refresh

        為了減少 JWT Token 泄露風險,一般有效期會設置的比較短。 這樣就會存在 JWT Token 過期的情況,我們不可能讓用戶頻繁去登錄獲取新的 JWT Token。

        解決方案:

        可以同時生成 JWT Token 與 Refresh Token,其中 Refresh Roken 的有效時間長于 JWT Token,這樣當 JWT Token 過期之后,使用 Refresh Token 獲取新的 JWT Token 與 Refresh Token,其中 Refresh Token 只能使用一次。

        四、OAuth

        簡介

        有時候,我們登錄某個網站,但我們又不想注冊該網站的賬號,這時我們可以使用第三方賬號登錄,比如 github、微博、微信、QQ等。

        開放授權(OAuth)是一個開放標準,允許用戶讓第三方應用訪問該用戶在某一網站上存儲的私密的資源(如照片,視頻,聯系人列表),而無需將用戶名和密碼提供給第三方應用。

        OAuth允許用戶提供一個令牌,而不是用戶名和密碼來訪問他們存放在特定服務提供者的數據。每一個令牌授權一個特定的網站(例如,視頻編輯網站)在特定的時段(例如,接下來的2小時內)內訪問特定的資源(例如僅僅是某一相冊中的視頻)。這樣,OAuth讓用戶可以授權第三方網站訪問他們存儲在另外服務提供者的某些特定信息,而非所有內容。

        OAuth是OpenID的一個補充,但是完全不同的服務。

        —— 摘自 維基百科

        授權流程

        名詞解釋:

        • Third-party application:第三方應用程序又稱"客戶端"(client),比如打開知乎,使用第三方登錄,選擇 Github 登錄,這時候知乎就是客戶端。
        • Resource Owner:資源所有者,本文中又稱"用戶"(user),即登錄用戶。
        • Authorization server:認證服務器,即 Github 專門用來處理認證的服務器。
        • Resource server:資源服務器,即 Github 存放用戶生成的資源的服務器。它與認證服務器,可以是同一臺服務器,也可以是不同的服務器。

        • A. A網站讓用戶跳轉到 GitHub,請求授權碼;GitHub 要求用戶登錄,然后詢問“知乎網站要求獲得 xx 權限,你是否同意?”;
        • B. 用戶同意,GitHub 就會重定向回 A 網站,同時發回一個授權碼;
        • C. A 網站使用授權碼,向 GitHub 請求令牌;
        • D. GitHub 返回令牌;
        • E. A 網站使用令牌,向 GitHub 請求用戶數據;

        其他授權模式

        授權碼模式(authorization code)是功能最完整、流程最嚴密的授權模式。除了我們上面所說的授權碼模式,其實還有其他授權模式:

        1. 簡化模式(Implicit grant type)
          有些 Web 應用是純前端應用,沒有后端。這時就不能用上面的方式了,必須將令牌儲存在前端。RFC 6749 就規定了第二種方式,允許直接向前端頒發令牌。這種方式沒有授權碼這個中間步驟
        2. 密碼模式(Resource Owner Password Credentials Grant)
          如果你高度信任某個應用,RFC 6749 也允許用戶把用戶名和密碼,直接告訴該應用。該應用就使用你的密碼,申請令牌
        3. 客戶端模式(Client Credentials Grant)
          適用于沒有前端的命令行應用,即在命令行下請求令牌

        關于這些模式詳細請見:OAuth2.0 的四種方式

        單點登錄

        單點登錄(Single Sign On, SSO),即:單一標記(單點)登錄。例如:QQ,我在QQ空間登錄一次,我可以去訪問QQ產品的其他服務:QQ郵箱、騰訊新聞等,都能保證你的賬戶保持登錄狀態。

        延伸閱讀:

        五、總結對比

        沒有最好,只有最合適?。?!

        • HTTP Auth Authentication:

          • 梳理總結:
            通用 HTTP 身份驗證框架有多個驗證方案使用。不同的驗證方案會在安全強度上有所不同。HTTP Auth Authentication 是最常用的 HTTP認證方案,為了減少泄露風險一般要求 HTTPS 協議。
          • 適用場景:
            一般多被用在內部安全性要求不高的的系統上,如路由器網頁管理接口
          • 問題:

            1. 請求上攜帶驗證信息,容易被嗅探到
            2. 無法注銷
        • Cookie + Session:

          • 梳理總結:

            • 服務端存儲 session ,客戶端存儲 cookie,其中 cookie 保存的為 sessionID
            • 可以靈活 revoke 權限,更新信息后可以方便的同步 session 中相應內容
            • 分布式 session 一般使用 redis(或其他KV) 存儲
          • 使用場景:
            適合傳統系統獨立鑒權
        • JWT:

          • 梳理總結:

            • 服務器不再需要存儲 session,服務器認證鑒權業務可以方便擴展
            • JWT 并不依賴 cookie,也可以使用 header 傳遞
            • 為減少盜用,要使用 HTTPS 協議傳輸
          • 適用場景:

            • 適合做簡單的 RESTful API 認證
            • 適合一次性驗證,例如注冊激活鏈接
          • 問題:

            1. 使用過程中無法廢棄某個 token,有效期內 token 一直有效
            2. payload 信息更新時,已下發的 token 無法同步
        • OAuth:

          • 梳理總結:

            • OAuth是一個開放標準,允許用戶授權第三方應用訪問他們存儲在另外的服務提供者上的信息,而不需要將用戶名和密碼提供給第三方移動應用或分享他們數據的所有內容。
            • GitHub OAuth 文檔 Identifying and authorizing users for GitHub Apps
          • 適用場景:
            OAuth 分為下面四種模式

            1. 簡化模式,不安全,適用于純靜態頁面應用
            2. 授權碼模式,功能最完整、流程最嚴密的授權模式,通常使用在公網的開放平臺中
            3. 密碼模式,一般在內部系統中使用,調用者是以用戶為單位。
            4. 客戶端模式,一般在內部系統之間的 API 調用。兩個平臺之間調用,以平臺為單位。
        查看原文

        贊 2 收藏 1 評論 0

        公子 發布了文章 · 2020-10-31

        博客遷移至HUGO

        我的博客從09年開始到現在已經度過了十一個年頭,雖然更新的頻率變慢了,但也還是一直在堅持輸出力求能夠幫助到大家。本站最早使用過 ASP 的 PJBlog 到之后大名鼎鼎的 WordPress,之后在 2011年 的時候遷到了 Typecho 一直到現在。

        因為多年的數據和主題修改沉淀,我一直懶得更換程序,甚至是自己開發的 Node.js 博客系統 Firekylin 也沒有激發我把主站遷移過去。不過隨著網絡環境的不穩定性,我也在慢慢尋找一些比較靠譜的備份和快速遷移的方式來降低博客的運維成本。

        數據庫和Docker

        現在最方便的部署方式就是容器了,所以我第一個想到的就是將博客容器化。因為 Typecho 可能會有圖片上傳等用戶數據產生,所以比較合理的鏡像方式是為 Typecho 程序制作鏡像,同時將用戶數據掛載到本地。

        數據庫則選擇購買了阿里云專門的數據庫服務,降低數據庫這塊的人工維護成本。這樣維護的成本就變成了只要定時備份用戶上傳文件即可。但是隨之又帶來了新的問題。

        數據庫服務貴不好用

        首當其沖的就是數據庫服務的成本問題,最低配版套餐都要好幾百,基本等同于一臺主機的價錢。人工運維成本是降低了,但是金錢運維成本直接加倍!而且跨主機商只能使用外網連接,雖然都是同地區服務,但是連接意外的慢。導致主站打開需要好幾秒,大大降低了用戶體驗。

        機器性能要求高

        然后容器對機器的性能也有一定要求。我一直在使用 1核2G 的低配機器維護著我的網站。由于我維護的站點比較多,使用上容器之后發現機器扛不住。換成了 2核4G 之后好一點了,但是金錢運維成本再次加倍!

        鏡像構建困難

        最后其實是容器構建這塊的成本。因為手上很多的老站都是使用 PHP 開發的,之前只是單純的用這些程序?,F在為了制作鏡像,需要去看程序的基礎依賴。

        另外還有一些老程序是破解版的,不太適合放 Docker Hub 上,尋找一個可以讓我托管私有鏡像的地方也廢了我不少時間。之前一直沒關注過這塊,很早之前知道的幾個做容器服務的要么不做了,要么就是改成收費了。后來發現其實 Github 就能直接托管鏡像,這才方便了點。

        總的來說容器的雖然降低了遷移的成本,但是目前看來性價比不高,想要更好的服務勢必要投入更多的資金才行,對我來說有點舍本逐末。

        靜態部署

        同時為了降低我的其它站的運維成本,考慮之后決定將一些不使用的站進行靜態博客處理。保留了存檔的功能,同時降低了維護成本。主要是現在有很多的免費服務支持靜態網站部署。比如我之前在用的 Vercel,很好的提供了對靜態博客的支持。在挑選程序的時候發現 Hugo 上我現在在用的主題已經有人實現了 TwentyFourteen,這個主題是我很早之前從 WordPress 的默認主題上遷移過來的。性能非常好。遂就使用它將其他的一些站進行了遷移。

        只需要在 Vercel 上使用 Import Template 的形式創建項目即可。輕輕一點它便會自動幫你創建 Github 倉庫,分配域名,添加證書,最后自動部署上線。然后我再把倉庫克隆下來,把導出的舊數據挪進倉庫提高一下即可。Vercel 會自動觸發更新的流程。

        不管是人力成本,金錢成本,還有數據備份成本上來看,這種方案都達到了最低方案。特別在 Github 給個人開放了無限的私有倉庫之后,你連數據隱秘性都不需要考慮了。所以在遷移了四五個老站之后,我看著生下來的主站,決定一不做二不休一塊遷移得了。

        后記

        關于如何進行遷移我之后再單獨寫一篇文章來講。遷移之后運維成本直接消失,不僅構建快而且頁面訪問速度也快,一個字香!之前 Typecho 的 Markdown 格式一直不支持表格語法,換了之后沒有這些問題了。不過也不是說沒有缺點,之前其實用 Typecho 的后臺寫文章非常方便,現在換成 VSCode 之后在快捷方式上沒有網頁的好用,之后再考慮做個單獨的發布頁吧。

        查看原文

        贊 0 收藏 0 評論 5

        公子 發布了文章 · 2020-10-26

        Drone 自定義 UI

        Drone 是一款開源的 CI/CD 工具,基于容器提供了強大的插件系統。多年前我有寫過《基于Docker的CI工具——Drone》中有詳細的介紹它的優點。Drone 采用的是 Server/Agent 架構,Server 端用來處理請求派發任務給 Agent,最終在 Agent 上執行任務。

        Drone 整體是使用 Golang 寫的,drone/drone-ui 是它的前端頁面倉庫,采用 Vue.js 進行開發(很早之前是使用 React 進行開發的)。前后端分離的項目,比較正常的中間會使用 NGINX 之類的 Web Server 進行橋接,用戶通過 Web Server 訪問前端頁面,然后頁面在訪問 Web Server 反代后的接口。不過 Drone Server 端直接是使用的 Golang 自己起的服務,而 Golang 又是一種需要編譯的語言。為了能讓 Server 編譯后還是單文件,作者特地寫了一款工具 bradrydzewski/togo 用來將靜態資源編譯成 Golang 代碼。編譯出來的結果本質就是文件路由和內容的哈希表,可以在官方倉庫中一窺究竟。

        將編譯后生成的 Golang 文件提交到倉庫之后,就可以在 Server 中使用模塊的形式將其加載進來,剩下的就是在 Server 中根據路由獲取內容返回了。這種做法在開發上會比較麻煩,不過對使用的人來說倒是方便很多了。不過由于靜態資源被編譯進了執行文件中,所以我們如果要自定義前端界面的話,就需要按照這個流程重新構建編譯 Server 執行文件了。

        構建前端模塊

        首先我們需要針對 drone/drone-ui 原始倉庫進行 Fork,在新的倉庫中根據你們的需求進行前端代碼的修改。在 RADME 中介紹了如何在開發環境中進行開發。如果改動不大的話,可以在每次 Drone 官方發布版本的時候根據上游倉庫提交 Pull Request 進行需求合并。執行 npm run build 會在 dist/files 目錄生成最終需要的前端靜態資源。

        前端資源備好之后需要安裝 bradrydzewski/togo 將靜態資源嵌到 Golang 代碼中。如果沒有安裝 Golang 的話需要先安裝 Golang。另外 Golang 的全局 bin 目錄需要配置到 PATH 環境變量中,否則編譯時會提示找不到該命令。

        go get github.com/bradrydzewski/togo
        cd dist
        go generate dist.go
        注:go generate 是利用注釋快速執行腳本的一種方式。本質上是執行了 dist.go 文件中的 togo http -package dist -output dist_gen.go 這條命令。

        最后將編譯生成的 dist_gen.go 文件添加到倉庫中提交,完成前端模塊的構建。接下來我們需要重新構建 Server 執行文件。

        構建執行文件

        Server 執行文件的倉庫是在 drone/drone,我們需要找到依賴了 github.com/drone/drone-ui 模塊的文件,并將其替換成我們 Fork 的新倉庫地址 xxx.com/xxx/drone-ui。主要有 ./handler/web/{logout,pages,web}.go 三個文件需要被替換。

        go get -v -insecure xxx.com/xxx/drone-ui
        sed -i '' 's/github.com\/drone\/drone-ui/xxx.com\/xxx\/drone-ui/' ./handler/web/{logout,pages,web}.go

        注: 針對這種場景,Golang 官方的模塊管理中其實是支持 replace 方式用來將 A 模塊替換成 B 模塊的,不過我當時沒有實驗成功,就還是使用了 sed 的方式。

        go mod edit -replace=github.com/drone/drone-ui=xxx.com/xxx/drone-ui

        之后我們就可以執行 go build 對其進行構建了。我們并沒有對該項目進行修改,只是針對它依賴的前端模塊進行處理。所以我的想法是當 drone-ui 倉庫發生變更的之后,執行 CI 流水線將 Server 倉庫克隆下來修改后執行鏡像構建并上傳到鏡像倉庫中。

        CI 執行當然是選擇 Drone 啦,用 Drone 去構建 Drone 聽起來就很酷!默認 Drone 會把當前倉庫克隆下來,但實際上我們不需要克隆當前倉庫,當前倉庫是被主倉庫依賴的模塊。我們真正需要下載的是 drone/drone 主倉庫。

        clone: 
          disable: true
        
        steps:
        - name: clone
          image: alpine/git
          commands:
          - git clone https://github.com/drone/drone.git .
          - git checkout ${DRONE_TAG}
        
        trigger:
          event:
          -tag

        在 Drone 的配置中,設置 disable: false 即可實現不克隆當前倉庫。然后自己在單獨增加 git clone 的步驟。我們將倉庫克隆到當前目錄中,并根據當前 git tag 的版本號切換 Server 倉庫的版本。這樣保證最后編譯出來的鏡像同版本號和上游不會有其它差異。

        - name: build
          image: golang:1.14.4
          commands:
          - go get -v -insecure xxx.com/xxx/drone-ui
          - sed -i '' 's/github.com\\/drone\\/drone-ui/xxx.com\\/xxx\\/drone-ui/' ./handler/web/{logout,pages,web}.go
          - sh scripts/build.sh
          environment:
            GOARCH: amd64
            GOOS: linux

        接下來這段構建命令除了增加前端模塊依賴替換之外,其它的都是從上游 Server 倉庫 中搬運過來的。上游構建中還有 ARM, ARM64 架構版本的構建,由于我這里并不需要,就不增加構建時間了。

        之后我們再像官方一樣,增加 Docker 鏡像構建上傳的步驟即可完成最終鏡像的創建。使用的時候使用該鏡像即可。

        后記

        同樣是使用 Drone 搭建,官方針對 Github 搭建的 https://cloud.drone.io 在未登錄的情況下還會自帶一個登錄頁。原理是 Server 服務在 pages.go 中判斷接入域名為 "cloud.drone.io" 的話會展示位于 handler/web/landingpage/index.html 的靜態頁。如果有門戶頁的需求的話可以針對這些文件進行對應的修改。

        查看原文

        贊 1 收藏 0 評論 0

        認證與成就

        • 獲得 2687 次點贊
        • 獲得 106 枚徽章 獲得 8 枚金徽章, 獲得 46 枚銀徽章, 獲得 52 枚銅徽章

        擅長技能
        編輯

        開源項目 & 著作
        編輯

        • Firekylin

          A Simple & Fast Node.js Blogging Platform Base On ThinkJS & React & ES2015+

        • Animaris

          使用 ThinkJS + MongoDB + React + Antd 開發的移動端 WebView 接口文檔系統

        • Pharos

          A Simple Front End monitor system Base On ThinkJS 3 & ReactJS & ES2015+.

        • Ionic實戰

          基于AngularJS的移動混合應用開發

        • Waline

          基于 Valine 衍生的簡潔、安全的評論系統

        注冊于 2011-09-21
        個人主頁被 38.6k 人瀏覽

        一本到在线是免费观看_亚洲2020天天堂在线观看_国产欧美亚洲精品第一页_最好看的2018中文字幕