整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          CSS酷炫跑馬燈等待時加載效果

          CSS酷炫跑馬燈等待時加載效果

          個視頻用CSS來寫一個酷炫的跑馬燈加載等待的效果。

          ·來看一下代碼:<span style="--:html結構非常簡單,加載的這些小圓點等一下就通過這些width的尾元素來寫,一共有二十個,每個里面都定義了一個變量i。當然這一堆東西是可以通過JS來生成的,也非常簡單,大家可以自己試著去改一下。

          ·樣式現在寫了一些基本的樣式,其它樣式重新來寫。<span style="首先是加載的這個區域,給它一個相對定位,大小是120乘以120。<div class="loader"然后寫一下這些span。

          ·現在還沒有什么東西,因為這些小圓點還沒有寫,就一個背景,這些小圓點通過width的尾元素來寫,大小給15像素就可以了,給個背景顏色還有圓角。現在這些小圓點是重疊在一起了,可以把它旋轉開。

          ·width進行一個load,用一下計算的函數,用定義好的這個變量乘以18度。然后給這些小圓點加一些陰影,一共五層陰影非常簡單,就給它疊加起來。

          ·接下來就是動畫的效果。首先先讓背景顏色可以不斷的切換起來,給它綁定一個動畫,來寫一下這個動畫,非常簡單。width:1通過濾鏡去調整它的色相就可以了。position:relativ開始在0度的位置,把色相的角度剛好轉一圈,現在背景顏色就已經有變化了。

          ·最后就是怎么樣讓這些小圓點有一個跑馬燈的加載效果。這里很簡單,再來寫一個動畫,對它進行縮放,一開始保持原來的大小,然后到百分之八十一,直到后面就讓它消失,把這個動畫綁定在偽元素上面。

          現在這些小圓點也有動畫了,但是它們是整體同時出現同時消失的,沒有那種跑馬燈的加載效果。其實也非常簡單,只要讓這些小圓點的動畫起始時間不一樣就可以了,也就是給它加一個動畫的延遲。這里同樣要計算一下,用定義好的變量i每個都乘以0.1秒。

          來看一下最終的效果,沒有問題,這種跑馬燈的加載效果就完成了。

          這個視頻就到這里,感謝大家的收看。

          于最近的工作原因對圖標有了更加全方位的認知,雖然之前寫過《如何繪制功能圖標基礎篇?》《如何系統的學習功能圖標?》這兩篇文章,但里面還是缺少了理論依據和系統做圖標的思維。通過不斷在學習的過程中不斷有了新的認知,希望和大家一起分享。

          圖標設計原則

          1. 表意準確

          功能圖標的第一原則是表意準確,要讓用戶看到圖標第一時間就能理解它的含義。同時,功能圖標還具有通用性,符合所有的用戶的使用習慣,不要試圖去改變用戶日積月累沉淀下來的記憶。

          微信底部的Tab欄,已經很多年沒換過了,由于微信用戶群體龐大機構復雜,牽一發而動全身,誰也不敢隨便的改變用戶多年積累的認知記憶。可能從美觀角度還有很大發揮的空間,但是用戶更多的會認為,熟悉的就是最好的。

          而愛心圖標在用戶的認知里更多的是喜歡,當朋友在微信朋友圈發了照片或更新動態,點擊愛心來表達自己的喜歡。

          2. 可預見性

          預見性是指人對事物發展的預判和前瞻,而人對功能圖標預見性的強弱取決于用戶對該圖標的認知強弱,當我們把繪制好的圖標放入頁面時我們要考慮用戶是否可以很快的找到該圖標?

          當用戶找到該圖標時,用戶是否會很快的理解圖標代表什么意思?當用戶在點擊圖標前是否已經大約預測到點擊該圖標后的界面大體樣式或內容?

          根據上圖的icon我們可以預測這是一款音樂類app的圖標,因為圖標有有明顯的音符和音樂播放按鈕等。

          根據上圖的icon我們可以預測這是一款購物電商類app的圖標,因為圖標中有分類查找和購物車圖標。

          上圖中當前顯示頁面為店鋪頁面,當我們看到客服圖標時能大體的想象得到點擊客服圖標會跳轉到聊天工具的頁面,這就是圖標的可預見性。

          3. 統一性

          (1)大小的統一

          圖標的主流尺寸有16×16, 24×24, 32×32, 48×48, 64×64, 96×96, 128×128, 192×192, 256×256, 512×512,1024×1024……

          (2)偶數規則

          元素周期表中相鄰的兩元素,原子序數為偶數的,其在地殼中的平均含量常大于奇數元素的含量。對于同一元素而言,質量數為偶數的同位素,在地殼中的平均含量大于相鄰奇數同位素的平均含量。

          這是人們根據分析的實際數據,經驗歸納而得出的元素和同位素在地殼中的分布規則之一,稱為偶數規則。

          在UI界面設計對于偶數原則基本保持一致態度。

          在圖標設計中主要就是兩種聲音,4的倍數和8的倍數?48之間的爭斗不僅體現在圖標尺寸的規范上也體現在珊格系統的規范制定中。

          那么怎么根據強有力的依據去決定到底是用4的倍數還是8的倍數呢?換言之就是到底用ios的規范還是用Material design的規范?

          (3)ios的規范4的倍數

          iPhone上最小的點擊區域,官方推薦是44px×44px。

          為什么ios的規范4的倍數?因為蘋果改變了游戲的規則,以前大家一起玩耍的時候都用px物理像素(physical pixel)來定義大小的尺寸,突然蘋果推出retina屏幕改變了普通屏幕的物理尺寸。在不同的屏幕上(普通屏幕 vs retina屏幕),css像素所呈現的大小(物理尺寸)是一致的,不同的是1個css像素所對應的物理像素個數是不一致的。

          在普通屏幕下,1個css像素 對應 1個物理像素(1:1);在retina 屏幕下,1個css像素對應 4個物理像素(1:4)。

          (4)Material design的規范8點網格

          Material design建立8點為一個單位的網格,所有的元素尺寸都是8的倍數。有些屏幕會很難調整適應這個系統,比如iPhone6開始的375×667的尺寸,但是解決方法也很簡單。

          保持填充和空隙(padding & margin)的尺寸統一遵循規則,剩余的空間可以用塊狀的元素來填充。有一些元素的尺寸是奇數的也沒關系,只要他們能讓整體遵守這套規則就好。

          (5)數字8拆解分析

          • 加減法:2+2+2+2=8;2+3+3=8;2+6=8;3+5=8;4+4=8
          • 乘除法:2×4=8
          • 次方:2的3次方等于8
          • 比例關系:2/8=1/4;3/8;4/8=1/2;5/8;6/8=3/4

          (6)黃金螺旋線/斐波那契數列

          斐波那契數列(FibonacciSequence)數列是這樣一個數列1、1、2、3、5、8……

          在數學上,斐波那契數列是以遞歸的方法來定義:

          F0=1

          F1=1

          Fn=F(n-1)+F(n-2)

          (n>=2,n∈N*)

          為什么谷歌的Material design和Ant design都把8點一個單位的網格,根據我上面的一些數學方法的推理,斐波那契數列中數字1/2/3/5/8占了很大的比重。

          舉個列子2+6=8,可以繼續拆解成1+3+1+3=8,但是2:6=1:3;同理 2×4=8,但是2:4=1:2,里面細拆數字都符合斐波那契數列,符合斐波那契數列意味著就符合了黃金分割比。

          最后得出的結論就是8的倍數為主,4的倍數為輔;除非你設計的app只需要適配ios系統可以使用4的倍數,當既要適配ios系統又要適配安卓系統時且沒有設計兩套界面分別適配ios跟安卓時選擇8的倍數是做好的選擇。

          (7)顏色統一

          圖標在選取顏色的時候盡量不要超過4種顏色,且每個圖標的配色需要根據對應的行業背景進行配色,利用色彩心理學比如紅色可使用在美食餐飲上,橙色用在美食上多指甜美,綠的代表食物多指健康綠色產品等

          (8)風格統一

          風格已經在《如何系統的學習功能圖標?》歸納的很全了,直角圖標和圓角圖標基礎上適當添加一種符合的圖標風格;最好不要出現兩種風格相加,很容易亂,也不夠簡潔,主次不明。

          在整個產品或者系統中,可以適當使用2到3種風格不同的圖標就行差異化對待。

          (9)圖標設計規范

          圓角規范:外圓角半徑-線的粗細=內圓角半徑

          外圓角半徑大小:圓角半徑是整個圖標大小的十分之一左右

          (10)圖標的物理平衡和視覺平衡

          為什么我們再同樣的大小區域去繪制正方形、圓形、三角形,雖然符合了統一的物理大小規范,但是從視覺上看上去卻很不均衡?

          關于這一點Material design給出了很好的解決辦法規范化的去繪制圖標。

          正方形18dp*18dp ; 圓形直徑20dp大小的規范

          垂直矩形20dp*16dp ; 水平矩形16dp*20dp

          通過Google系統圖標規范繪制出來的圖標可以達到視覺平衡

          打破規則:當視覺平衡和物理平衡發生沖突時,我們應該優先選擇視覺平衡。上圖中是微信的界面圖標,仔細觀察我們發現通訊錄的圖標已經超出物理規定的大小,但是整個圖標在界面中是可以達到視覺平衡的。

          所以我們在繪制的過程中可以打破規則,這也是每個優秀的設計師應該具備的。

          (11)圖標網格系統

          在主流的圖標繪制中,線性圖標的粗細大小有1px、2px、3px。所以我們在建立圖標網格系統是使用了8的倍數,上面已經通過對數字8拆解中得知8的倍數非常適合1px、2px、3px粗細大小的。

          在二倍圖下使用48*48px的尺寸大小,在一倍圖下使用了24*24px的尺寸大小來繪制圖標。

          空間的呼吸感:在繪制圖標時,我們不但要確定圖標的大小,還要考慮圖標的內呼吸感,就是所謂的正負形,圖標的負空間也有規則,Material design內呼吸感以2px為基準進行繪制的。

          通過字體字重感受線性圖標粗細:

          字體字重從細到粗會給人輕盈到沉穩的感覺,無論中文還是西文,文字越細其可讀性越強,文字越粗其視認性越高。

          通過列舉線性圖標的粗細大小有1px、2px、3px、4px。可以看到圖標粗細變化給人的視覺感受也是不一樣的,具體使用多大取決于界面內容,最好的方法就是通過對比來驗證那個粗細更適合當前界面。

          關于2倍尺寸下使用3px,在3倍尺寸下會變成4.5px,會出現0.5px的問題。這方面的技術已經可以實現了,當然最好使用svg矢量格式。比如上圖的愛心圖標,弧線肯定是存在小數點問題,所以使用svg矢量格式是最好的選擇。

          (12)怎么畫一條0.5px的邊

          比較了在高清屏上畫0.5px的幾種方法——可以通過直接設置寬高border為0.5px、設置box-shadow的垂直方向的偏移量為0.5px、借助線性漸變linear-gradient、使用transform: scaleY(0.5)的方法,使用SVG的方法。

          最后發現SVG的方法兼容性和效果都是最好的,所以在viewport是1的情況下,可以使用SVG畫0.5px,而如果viewport的縮放比例不是1的話,那么直接畫1px即可。

          更詳細的請參考鏈接:怎么畫一條0.5px的邊。

          4. 層次明確

          圖標具有可點擊性和標識性。可點擊性就會有點擊前、點擊時、點擊后三種狀態,主流底部標簽欄會在點擊前使用線性圖標,點擊時和點擊后使用面性圖標;也有使用顏色來區分。

          5. 延展性

          圖標應該具有很強的延展性,好的圖標可以直接當應用圖標或者logo來使用,好的圖標還可以通過點線面動效變化做下拉加載動畫。

          圖標的功能分類

          按圖標功能還可以細分為動作圖標、警示圖標、內容圖標、設備圖標、文件圖標、編輯圖標、導航圖標、通知圖標、社交圖標、切換圖標等……

          為什么我們在設計圖標的時候很少去系統的這樣去區分,更多的原因可能我們做的C端產品,圖標種類和數量相對較少,當我們接觸到B端產品,由于B端產品的業務復雜程度對應的圖標數量也隨之增加,為了更好的管理圖標需要更加詳細的設置分類。

          圖標的命名規范

          關于圖標的命名為什么要用英語正規化?因為我們用的整個系統都是基于英語開發的,設計師的業務下游主要是前端工程師,如果我們不能規范的命名每個圖標肯定會增加前端的工作量,如何提高合作效率應該也是設計師用戶體驗的范疇。英語差的打開谷歌翻譯基本沒任何問題的。

          切圖命名以模塊為前綴,如:模塊_類別_功能_狀態.png

          • 模塊:登陸頁面(login) 公共(common) 需求a(need) 需求b(demand) 發現(discover) 消息(message) 我(me)
          • 類別:導航欄(nav) 菜單欄(tab) 按鈕(btn) 圖標(icon) 背景圖片(bg) 默認圖片(def)
          • 功能:菜單(menu) 返回(back) 關閉(close) 編輯(eidt) 消息(message) 刪除(delete)
          • 狀態:選中(selected) 不可點(disabled) 按下(pressed) 正常(normal)

          圖標的制作上線

          在app產品中,以美團app為例整個產品中圖標使用了多種風格,首頁金剛區圖標作為首頁流量分發的重要分支,在視覺設計要吸引用戶的眼球做的更艷麗一點,而在標簽欄導航圖標和內頁的功能圖標需要設計的更加簡潔。

          我們在繪制圖標的時候首頁金剛區復雜的圖標單獨繪制一套,其他系統需要繪制線面兩種風格,為了更好的適配頁面,方面以后更好的使用我們在Sketch中使用Symbol系統的制作圖標。

          以愛心圖標為例,我們使用Symbol繪制線面兩套圖標,關于圖標的配色可以添加黑白灰+主色,可以有警示的橙色/成功的綠色/刪除的紅色等,后續復制添加也很方便。

          建立圖標庫和顏色庫,每次有新增的圖標和新增的顏色,只需再新增一個Symbol就可以很好的管理自己產品中的圖標庫了。

          團隊協作:

          目前團隊協作按照圖標功能分類上傳到sketch的插件craft,方便團隊其他成員一起使用。

          設計的下游——前端開發:

          為了很好的方便前端開發工作,我們需要根據上面的規范進行命名自己的圖標。

          前端開發主流做法就是把圖標變成一個字體,上傳到團隊共享的icon網站,通過輸出svg矢量格式的圖標,讓前端開發工程師直接調用。

          國內主流的平臺是iconfont,如果sketch制作的圖標,導出svg格式在illustrator軟件里面重新安裝1024的尺寸進行繪制,上傳到項目中。

          所有的路徑都需要擴展成面性圖標,多色圖標不支持后期代碼修改顏色,單色圖標后期可支持自定義圖標顏色。

          總結

          我們在設計的過程中,隨之對設計的認知水平提高,我們的知識體系也在不斷的完善,這時候就需要我們對了解的知識進行深挖,多問自己為什么?了解背后的邏輯。這樣才會更加深刻。

          #參考文獻#

          • Material Design規范:https://material.io
          • 怎么畫一條0.5px的邊:https://segmentfault.com/a/1190000013998884

          #相關文章#

          如何系統學習功能圖標

          作者:水手哥,公眾號:水手哥學設計

          本文由 @水手哥 原創發布于人人都是產品經理。未經許可,禁止轉載

          題圖來自 Unsplash ,基于 CC0 協議

          文件上傳

          前景提要

          在工作中,經常會遇到上傳文件的功能,但是當文件體積大時,如果使用把該文件直接在一個請求體中提交,會出現一些問題,以nginx為例:

          • 其默認允許1MB以內的文件
          • 超過1MB的文件,需要設置client_max_body_size放開體積限制

          但是這樣會存在一個問題,就是如果上傳的文件體積很大,就會出現一些問題,最明顯的問題是:

          服務器的存儲和網絡帶寬壓力都會非常大

          當服務器、產品、用戶忍不了時,就需要對大文件上傳進行優化。

          1、大文件切片上傳

          邏輯梗概

          • 將大文件分割成多個文件塊
          • 逐個上傳文件塊
          • 服務端將文件塊順序合并成完整文件

          優勢分析

          1. 減輕服務器壓力:如果一次性上傳大文件,服務器的存儲和網絡帶寬壓力都會非常大,而通過切片,可以將這些壓力分散到多個小文件中,減輕服務器的壓力。
          2. 斷點續傳、錯誤重試:因為大文件被肢解了,如果因為一些原因中斷、錯誤了,已經上傳的部分就不用再重新上傳了,只需要把后續的傳上就好了。

          前端部分

          1.1 切文件(前端)

          1.2 判定切片是否完成上傳完成(前端)

          • 客戶端記錄切片的上傳狀態,只需要上傳未成功的切片

          1.3 斷點、錯誤續傳(前端)

          • 客戶端上傳文件時,記錄已上傳的切片位置
          • 下次上傳時,根據記錄的位置,繼續上傳

          后端部分

          1.1 收切片、存切片

          • 將相關切片保存在目標文件夾

          1.2 合并切片

          • 服務端根據切片的順序,將切片合并成完整文件

          1.3 文件是否存在校驗

          • 服務端根據文件Hash值、文件名,校驗該文件是否已經上傳

          代碼實現

          1、搭建基礎項目

          服務器(基于express)

          const express=require('express')
          const app=express()
          app.listen(3000, ()=> {
              console.log('服務已運行:http://localhost:3000');
          })
          


          前端

          基礎頁面

          <!DOCTYPE html>
          <html lang="en">
          <head>
              <meta charset="UTF-8">
              <meta name="viewport" content="width=device-width, initial-scale=1.0">
              <title>Document</title>
              <style>
                  input{
                      display: block;
                      margin: 10px 0;
                  }
              </style>
          </head>
          <body>
              <input type="file" id="file">
              <input type="button" id="upload" value="上傳">
              <input type="button" id="continue" value="繼續上傳">
          </body>
          </html>
          


          引入資源

          <script type="module" src="./spark-md5.js"></script>
          <script type="module" src="./operate.js"></script>
          


          operate.js

          // 獲取文件域
          const fileEle=document.querySelector("#file");
          const uploadButton=document.querySelector("#upload");
          const continueButton=document.querySelector("#continue");
          uploadButton.addEventListener("click", async ()=> {
              console.log("點擊了上傳按鈕")
          })
          continueButton.addEventListener('click', async ()=> {
              console.log("點擊了繼續上傳按鈕")
          })
          


          3、靜態資源托管(server)

          app.use(express.static('static'))
          


          4、上傳接口

          搭建上傳接口(server)

          使用body-parser中間價解析請求體

          // 導入中間件
          const bodyParser=require('body-parser')
          // 使用中間件
          // 處理URL編碼格式的數據
          app.use(bodyParser.urlencoded({ extended: false })); 
          // 處理JSON格式的數據
          app.use(bodyParser.json()); 
          


          上傳接口

          app.post('/upload', (req, res)=> {
              res.send({
                  msg: '上傳成功',
                  success: true
              })
          })
          


          測試接口(前端)

          // 單個文件上傳
          const uploadHandler=async (file)=> {
              fetch('http://localhost:3000/upload', {
                  method: "POST",
                  headers: {
                      'Content-Type': 'application/json',
                  },
                  body: JSON.stringify({
                      fileName: '大文件',
                  }),
              })
          }
          uploadButton.addEventListener("click", async (e)=> {
              uploadHandler()
          })
          


          5、文件上傳接口存儲文件(server)

          使用multer中間件處理上傳文件

          設置uploadFiles文件夾為文件存儲路徑

          const multer=require('multer')
          const storage=multer.diskStorage({
              destination: function (req, file, cb) {
                  cb(null, './uploadFiles');
              },
          });
          const upload=multer({
              storage
          })
          
          app.post('/upload', upload.single('file'), (req, res)=> {
              
          })
          


          測試

          // 單個文件上傳
          const uploadHandler=async (file)=> {
              let fd=new FormData();
              fd.append('file', file);
              fetch('http://localhost:3000/upload', {
                  method: "POST",
                  body: fd
              })
          }
          uploadButton.addEventListener("click", async ()=> {
              let file=fileEle.files[0];
              uploadHandler(file)
          })
          


          6、文件切片

          注意

          假設切片大小為1M 保存切片順序(為了合成大文件時正確性) 上傳狀態(為了斷點續傳、前端顯示進度條)

          // 使用單獨常量保存預設切片大小 1MB
          const chunkSize=1024 * 1024 * 1; 
          // 文件切片
          const createChunks=(file)=> {
              // 接受一個文件對象,要把這個文件對象切片,返回一個切片數組
              const chunks=[];
              // 文件大小.slice(開始位置,結束位置)
              let start=0;
              let index=0;
              while (start < file.size) {
                  let curChunk=file.slice(start, start + chunkSize);
                  chunks.push({
                      file: curChunk,
                      uploaded: false,
                      chunkIndex: index,
                  });
                  index++;
                  start +=chunkSize;
              }
              return chunks;
          }
          


          測試文件切片函數

          // 存儲當前文件所有切片
          let chunks=[];
          uploadButton.addEventListener("click", async ()=> {
              let file=fileEle.files[0];
              chunks=createChunks(file);
              console.log(chunks);
          })
          


          注意:將來要把這些切片全部都上傳到服務器,并且最后需要把這些切片合并成一個文件,且要做出文件秒傳功能,需要保留當前文件的hash值和文件名,以辨別文件和合并文件。

          在頁面中引入spark-md5.js

          <script type="module" src="./spark-md5.js"></script>
          


          獲取文件Hash值

          const getHash=(file)=> {
              return new Promise((resolve)=> {
                  const fileReader=new FileReader();
                  fileReader.readAsArrayBuffer(file);
                  fileReader.onload=function (e) {
                      let fileMd5=SparkMD5.ArrayBuffer.hash(e.target.result);
                      resolve(fileMd5);
                  }
              });
          }
          


          把文件的hash值保存在切片信息中

          // 文件hash值
          let fileHash="";
          // 文件名
          let fileName="";
          // 創建切片數組
          const createChunks=(file)=> {
              // 接受一個文件對象,要把這個文件對象切片,返回一個切片數組
              const chunks=[];
              // 文件大小.slice(開始位置,結束位置)
              let start=0;
              let index=0;
              while (start < file.size) {
                  let curChunk=file.slice(start, start + chunkSize);
                  chunks.push({
                      file: curChunk,
                      uploaded: false,
                      fileHash: fileHash,
                      chunkIndex: index,
                  });
                  index++;
                  start +=chunkSize;
              }
              return chunks;
          }
          // 上傳執行函數
          const uploadFile=async(file)=> {
              // 設置文件名
              fileName=file.name;
              // 獲取文件hash值
              fileHash=await getHash(file);
              chunks=createChunks(file);
              console.log(chunks);
          }
          


          7、上傳邏輯修改

          前端部分

          單個文件上傳函數修改:

          插入文件名、文件Hash值、切片索引

          上傳成功之后修改狀態標識(可用于斷點續傳、上傳進度回顯)

          // 單個文件上傳
          const uploadHandler=(chunk)=> {
              return new Promise(async (resolve, reject)=> {
                  try {
                      let fd=new FormData();
                      fd.append('file', chunk.file);
                      fd.append('fileHash', chunk.fileHash);
                      fd.append('chunkIndex', chunk.chunkIndex);
                      let result=await fetch('http://localhost:3000/upload', {
                          method: 'POST',
                          body: fd
                      }).then(res=> res.json());
                      chunk.uploaded=true;
                      resolve(result)
                  } catch (err) {
                      reject(err)
                  }
              })
          }
          


          批量上傳切片

          限制并發數量(減輕服務器壓力)

          // 批量上傳切片
          const uploadChunks=(chunks, maxRequest=6)=> {
              return new Promise((resolve, reject)=> {
                  if (chunks.length==0) {
                      resolve([]);
                  }
                  let requestSliceArr=[]
                  let start=0;
                  while (start < chunks.length) {
                      requestSliceArr.push(chunks.slice(start, start + maxRequest))
                      start +=maxRequest;
                  }
                  let index=0;
                  let requestReaults=[];
                  let requestErrReaults=[];
          
                  const request=async ()=> {
                      if (index > requestSliceArr.length - 1) {
                          resolve(requestReaults)
                          return;
                      }
                      let sliceChunks=requestSliceArr[index];
                      Promise.all(
                          sliceChunks.map(chunk=> uploadHandler(chunk))
                      ).then((res)=> {
                          requestReaults.push(...(Array.isArray(res) ? res : []))
                          index++;
                          request()
                      }).catch((err)=> {
                          requestErrReaults.push(...(Array.isArray(err) ? err : []))
                          reject(requestErrReaults)
                      })
                  }
                  request()
              })
          }
          


          抽離上傳操作

          // 文件上傳
          const uploadFile=async (file)=> {
              // 設置文件名
              fileName=file.name;
              // 獲取文件hash值
              fileHash=await getHash(file);
              // 獲取切片
              chunks=createChunks(file);
              try {
                  await uploadChunks(chunks)
              } catch (err) {
                  return {
                      mag: "文件上傳錯誤",
                      success: false
                  }
              }
          }
          


          后端部分

          修改上傳接口,增加功能

          使用一個文件Hash值同名的文件夾保存所有切片

          這里使用了node內置模塊path處理路徑

          使用fs-extra第三方模塊處理文件操作

          const path=require('path')
          const fse=require('fs-extra')
          app.post('/upload', upload.single('file'), (req, res)=> {
              const { fileHash, chunkIndex }=req.body;
              // 上傳文件臨時目錄文件夾
              let tempFileDir=path.resolve('uploadFiles', fileHash);
              // 如果當前文件的臨時文件夾不存在,則創建該文件夾
              if (!fse.pathExistsSync(tempFileDir)) {
                  fse.mkdirSync(tempFileDir)
              }
              // 如果無臨時文件夾或不存在該切片,則將用戶上傳的切片移到臨時文件夾里
              // 如果有臨時文件夾并存在該切片,則刪除用戶上傳的切片(因為用不到了)
              // 目標切片位置
              const tempChunkPath=path.resolve(tempFileDir, chunkIndex);
              // 當前切片位置(multer默認保存的位置)
              let currentChunkPath=path.resolve(req.file.path);
              if (!fse.existsSync(tempChunkPath)) {
                  fse.moveSync(currentChunkPath, tempChunkPath)
              } else {
                  fse.removeSync(currentChunkPath)
              }
              res.send({
                  msg: '上傳成功',
                  success: true
              })
          })
          


          8、合并文件

          編寫合并接口(server)

          合并成的文件名為 文件哈希值.文件擴展名

          所以需要傳入文件Hash值、文件名

          app.get('/merge', async (req, res)=> {
              const { fileHash, fileName }=req.query;
              res.send({
                  msg: `Hash:${fileHash},文件名:${fileName}`,
                  success: true
              });
          })
          


          請求合并接口(前端)

          封裝合并請求函數

          // 合并分片請求
          const mergeRequest=(fileHash, fileName)=> {
              return fetch(`http://localhost:3000/merge?fileHash=${fileHash}&fileName=${fileName}`, {
                  method: "GET",
              }).then(res=> res.json());
          };
          


          在切片上傳完成后,調用合并接口

          // 文件上傳
          const uploadFile=async (file)=> {
              // 設置文件名
              fileName=file.name;
              // 獲取文件hash值
              fileHash=await getHash(file);
              // 獲取切片
              chunks=createChunks(file);
              try {
                  await uploadChunks(chunks)
                  await mergeRequest(fileHash, fileName)
              } catch (err) {
                  return {
                      mag: "文件上傳錯誤",
                      success: false
                  }
              }
          }
          


          合并接口邏輯

          1、根據文件Hash值,找到所有切片

          app.get('/merge', async (req, res)=> {
              const { fileHash, fileName }=req.query;
              // 最終合并的文件路徑
              const filePath=path.resolve('uploadFiles', fileHash + path.extname(fileName));
              // 臨時文件夾路徑
              let tempFileDir=path.resolve('uploadFiles', fileHash);
              // 讀取臨時文件夾,獲取所有切片
              const chunkPaths=fse.readdirSync(tempFileDir);
              console.log('chunkPaths:', chunkPaths);
              res.send({
                  msg: "合并成功",
                  success: true
              });
          })
          


          合并接口邏輯

          2、遍歷獲取所有切片路徑數組,根據路徑找到切片,合并成一個文件,刪除原有文件夾

          app.get('/merge', async (req, res)=> {
              const { fileHash, fileName }=req.query;
              // 最終合并的文件路徑
              const filePath=path.resolve('uploadFiles', fileHash + path.extname(fileName));
              // 臨時文件夾路徑
              let tempFileDir=path.resolve('uploadFiles', fileHash);
          
              // 讀取臨時文件夾,獲取所有切片
              const chunkPaths=fse.readdirSync(tempFileDir);
          
              console.log('chunkPaths:', chunkPaths);
          
              // 將切片追加到文件中
              let mergeTasks=[];
              for (let index=0; index < chunkPaths.length; index++) {
                  mergeTasks.push(new Promise((resolve)=> {
                      // 當前遍歷的切片路徑
                      const chunkPath=path.resolve(tempFileDir, index + '');
                      // 將當前遍歷的切片切片追加到文件中
                      fse.appendFileSync(filePath, fse.readFileSync(chunkPath));
                      // 刪除當前遍歷的切片
                      fse.unlinkSync(chunkPath);
                      resolve();
                  }))
              }
              await Promise.all(mergeTasks);
              // 等待所有切片追加到文件后,刪除臨時文件夾
              fse.removeSync(tempFileDir);
              res.send({
                  msg: "合并成功",
                  success: true
              });
          })
          


          10、斷點續傳

          封裝continueUpload方法

          在continueUpload方法中,只上傳 uploaded 為true的切片

          修改后此功能對用戶來說即是黑盒,用戶只需要重復調用continueUpload方法即可

          // 文件上傳
          const continueUpload=async (file)=> {
              if(chunks.length==0 || !fileHash || !fileName){
                  return;
              }
              try {
                  await uploadChunks(chunks.filter(chunk=> !chunk.uploaded))
                  await mergeRequest(fileHash, fileName)
              } catch (err) {
                  return {
                      mag: "文件上傳錯誤",
                      success: false
                  }
              }
          }
          


          2、大文件秒傳

          邏輯梗概

          • 客戶端上傳文件時,先提交文件的哈希值,
          • 服務端根據哈希值查詢文件是否已經上傳,如果已上傳,則直接返回已上傳狀態
          • 客戶端收到已上傳狀態后,直接跳過上傳過程

          優勢分析

          • 提高上傳效率:秒傳可以提高上傳效率,因為文件已經在上傳過程中被上傳過了,直接返回已上傳狀態,省要再次上傳,提高效率。

          代碼實現

          校驗接口,校驗是否已經存在目標文件

          邏輯:根據文件Hash值和文件名組成 “文件Hash.文件擴展名” ,以保證文件名唯一

          app.get('/verify', (req, res)=> {
              const { fileHash, fileName }=req.query;
              const filePath=path.resolve('uploadFiles', fileHash + path.extname(fileName));
              const exitFile=fse.pathExistsSync(filePath);
              res.send({
                  exitFile
              })
          })
          


          校驗函數

          // 校驗文件、文件分片是否存在
          const verify=(fileHash, fileName)=> {
              return fetch(`http://localhost:3000/verify?fileHash=${fileHash}&fileName=${fileName}`, {
                  method: "GET",
              }).then(res=> res.json());
          };
          
          // 文件上傳
          const uploadFile=async (file)=> {
              // 設置文件名
              fileName=file.name;
              // 獲取文件hash值
              fileHash=await getHash(file);
              // 校驗是否已經上傳該文件
              let { exitFile }=await verify(fileHash, fileName);
              if (exitFile) {
                  return {
                      mag: "文件已上傳",
                      success: true
                  }
              }
              // 獲取切片
              chunks=createChunks(file);
              try {
                  await uploadChunks(chunks.filter(chunk=> !chunk.uploaded))
                  await mergeRequest(fileHash, fileName)
              } catch (err) {
                  return {
                      mag: "文件上傳錯誤",
                      success: false
                  }
              }
          }
          


          3、提取為公共方法

          封裝

          編寫 bigFileUpload.js 文件,暴露uploadFile和continueUpload

          // bigFileUpload.js
          export default {
              uploadFile,
              continueUpload
          }
          


          使用

          導入資源并調用

          import bigUpload from './bigFileUpload.js'
          uploadButton.addEventListener("click", async ()=> {
              let file=fileEle.files[0];
              bigUpload.uploadFile(file)
          })
          continueButton.addEventListener('click', async ()=> {
              bigUpload.continueUpload()
          })
          


          4、可優化

          前端:

          封裝形式可優化,采用類的方式封裝,以保證數據的獨立性、可定制性

          切片Hash的計算可以通過抽樣切片的方式來進行

          ...

          后端:

          文件Hash校驗可增加用戶ip地址以保證文件唯一性

          待合并項可定時刪除

          ...

          歡迎大家補充!


          作者:JSNoob
          鏈接:https://juejin.cn/post/7323883238896058387


          主站蜘蛛池模板: 国产在线精品一区二区三区直播| 精品一区二区三区无码免费直播| 亚洲码一区二区三区| 中文字幕精品一区二区| 日韩一区在线视频| 麻豆精品人妻一区二区三区蜜桃 | 国产激情精品一区二区三区| 亚洲一区二区三区高清视频| 久久亚洲AV午夜福利精品一区| 日韩一区二区精品观看| 在线观看精品一区| 日韩一区二区三区在线| 极品尤物一区二区三区| 亚洲一区在线视频| 亚洲一区在线视频| 伊人色综合视频一区二区三区| 久久一本一区二区三区| 色欲AV蜜臀一区二区三区| 另类一区二区三区| 无码人妻AV免费一区二区三区| 国产免费无码一区二区| 亚洲AV无码一区东京热| 久久精品一区二区三区日韩| 久久青草精品一区二区三区| 无码日韩精品一区二区免费暖暖| 国产AV午夜精品一区二区三区| 亚洲熟女少妇一区二区| 亚洲国产一区在线| 国内精品一区二区三区东京| 精品人妻中文av一区二区三区 | 成人免费视频一区二区| 国产精品视频第一区二区三区| 无码aⅴ精品一区二区三区浪潮| 日韩视频免费一区二区三区| 在线观看国产一区亚洲bd| 国产一区二区三区在线免费观看| 国产主播一区二区三区| 国产在线观看一区精品| 一区二区在线观看视频| 欧美日本精品一区二区三区| 免费观看日本污污ww网站一区|