前段時間作了一個頁面,作的是我的雲盤的業務,操做功能上相似於百度網盤和windows文件管理。這個業務自己沒有稱得上是亮點的地方,可是當中有不少地方值得總結,不管是技術上仍是感悟上。javascript
個人感悟首先在產品上,做爲一名前端,要不斷地站在用戶的角度上去感覺它,必定有一些能夠作的更友好、更人性化的地方。好比在移動複製文件/文件夾的操做中,原來只能經過右鍵菜單操做,如今能夠經過鍵盤ctrl + vc/x/v,也能夠直接拖動(移動)。css
其次在本次編碼中,我有如下意識和習慣:html
而後,還有幾個感悟:前端
最後,還有一些收穫:java
接下來,我把全部相關的技術點整理在這裏,鞏固學習。清單以下:node
HTML5 增長了一批 Oberver API ,包括 MutationObserver, PerformanceObserver, IntersectionOberver, ResizeObserver 等,目的是針對一些目標進行監控。這些 API 中只有 MutationObserver (針對DOM結構的監控)進入了正式標準,PerformaneObserver 進入候選階段,IntersectionObserver 和 ResizeObserver 目前在草案階段。因此這裏講解一下 MutationObserver,它有一個構造函數 MutationObserver() 和 三個方法 disconnect()、observe()、takeRecords()python
MutationObserver(callback) 構造函數,返回一個監聽DOM變化的MutationObserver對象 回調函數:當指定的被監控DOM節點發生變更時執行。有兩個參數:第一個是 MutationRecord 對象數組,即描述全部被觸發改動的對象,詳細的變更信息存儲在這些對象中。第二個是當前調用該函數的 mutationObserver 對象 .observe(target, opinions) 開始監控DOM節點 target是被監控的DOM節點 opinions可選,是一個對象,屬性有: attributeFilter 要監控的DOM屬性,若無此屬性,默認監控全部屬性。無默認值 attributeOldValue 當被監控節點的屬性改動時,將次屬性置爲true將記錄任何有改動屬性的上一個值。無默認值 attributes 置爲true以觀察受監視元素的屬性值變動。默認值爲false characterData 置爲true以觀察受監視元素的屬性值變動。默認值爲false characterDataOldValue 置爲true以在文本在受監視節點上發生更改時記錄節點文本的先前值。 childList 置爲true以監視目標節點(若是subtree爲true,則包含子孫節點)添加或刪除新的子節點。默認值爲false。 subtree 置爲true以擴展監視範圍到目標節點下的整個子樹的全部節點。MutationObserverInit的其餘值都會做用於此子樹下的全部節點,而不只僅只做用於目標節點。默認值爲false。 .disconnect() 此方法告訴觀察者中止監控 .takeRecords() 此方法返回已檢測到但還沒有由觀察者的回調函數處理的全部匹配DOM更改的列表,使變動隊列保持爲空。 此方法最多見的使用場景是在斷開觀察者以前當即獲取全部未處理的更改記錄,以便在中止觀察者時能夠處理任何未處理的更改。
何爲 props 派生?好比如今有這樣的需求,子組件中來自父組件的 props 數據,並非直接使用,而是在其基礎上進行更改事後纔會使用,所以須要 props 變化時更新 state 的操做,能夠經過生命週期函數實現。react
在react16.4版本以前經過 componentWillReceiveProps 來實現,16.4以後還能夠經過 getDerivedStateFromProps 來實現。另外,在具體狀況下是否真的須要 props 派生、注意事項及可能出現的bug官網博客總結的很詳細nginx
滾動事件 onscroll,滾輪事件 onwheel。在PC端通常容易被認爲沒什麼區別,但仍是有些細微的差異。不管經過何種方式(鼠標滾輪、鍵盤方向鍵、觸摸板)滾動頁面,只要有滾動發生都會觸發滾動事件。而滾輪事件不管頁面有無發生滾動,只要滾輪被觸動,都會發生該事件。大部分時候只須要滾動事件便可,個別時候滾輪事件配合使用。好比想頁面已經滾動到底部,仍在滾動滑輪時,只發生滾輪事件不發生滾動事件,有這個需求能夠配合使用。注意,滾輪事件要使用onwheel,onmousewheel已被廢棄。
在封裝事件委託以前,有幾個問題須要明白:
進入React、Vue、Angular的前端組件化 + 前端工程化時代,咱們應該改變思惟,在開發中儘可能不要使用jQuery。你應該首選使用React提供的事件處理機制,儘可能不要使用原生JS處理事件。當你確認React的事件處理沒法知足你的需求、或者不方便實現時,可使用addEventListener()。雖然這裏封裝了 onDelegate(),但仍是建議你不在萬不得已的狀況下不要使用。
實現源碼:
import cloneDeep from "lodash/cloneDeep"; const throwError = (message) => { throw new Error(message) }; // 判斷是不是DOM元素 const isDOM = (obj) => typeof HTMLElement === 'object' ? obj instanceof HTMLElement : obj && typeof obj === 'object' && obj.nodeType === 1 && typeof obj.nodeName === 'string'; // 檢查selector的有效性 const checkSelector = (parent, selector) => { try{ parent.querySelector(selector); }catch (e) { return `參數 selector 無效` } }; // 參數檢測 const paramCheck = (type, events, parent, selector, func, data, reverseScope, capture) => { let baseMsg = `Document模塊 ${type}Delegate()方法調用錯誤:`; if (type === "on") { typeof events !== "string" && throwError(`${baseMsg}參數 events 必須是 string 類型,如今是${typeof events}!`); events.length === 0 && throwError(`${baseMsg}參數 events 不能爲空!`); !isDOM(parent) && throwError(`${baseMsg}參數 parent 必須是 DOM 元素!`); typeof selector !== "string" && throwError(`${baseMsg}參數 selector 必須是 string 類型,如今是${typeof selector}!`); let selectRes = checkSelector(parent, selector); // 檢測selector的有效性 typeof selectRes === "string" && throwError(`${baseMsg}${selectRes}!`); typeof func !== "function" && throwError(`${baseMsg}參數 func 必須是 function 類型,如今是${typeof func}!`); typeof reverseScope !== "boolean" && throwError(`${baseMsg}參數 reverseScope 必須是 boolean 類型,如今是${typeof reverseScope}!`); typeof capture !== "boolean" && throwError(`${baseMsg}參數 capture 必須是 boolean 類型,如今是${typeof capture}!`); Object.prototype.toString.call(data).slice(8, -1) !== "Object" && throwError(`${baseMsg}參數 data 必須是 object 類型!`); // 判斷data數據類型 }else if(type === "off") { typeof events !== "string" && throwError(`${baseMsg}參數 events 必須是 string 類型,如今是${typeof events}!`); events.length === 0 && throwError(`${baseMsg}參數 events 不能爲空!`); let selectRes = checkSelector(parent, selector); // 檢測selector的有效性 typeof selectRes === "string" && throwError(`${baseMsg}${selectRes}!`); !isDOM(parent) && throwError(`${baseMsg}參數 parent 必須是 DOM元素!`); typeof selector !== "string" && throwError(`${baseMsg}參數 selector 必須是 string 類型,如今是${typeof selector}!`); typeof func !== "function" && throwError(`${baseMsg}參數 func 必須是 function 類型,如今是${typeof func}!`); typeof reverseScope !== "boolean" && throwError(`${baseMsg}參數 reverseScope 必須是 boolean 類型,如今是${typeof reverseScope}!`); } }; let EventHandles = []; // 事件委託 const onDelegate = (events = "", parent, selector = "", func, data = {}, reverseScope = false, capture = false) => { data = cloneDeep(data); paramCheck("on", events, parent, selector, func, data, reverseScope, capture); // 參數檢測 const already = EventHandles.find(f => f.events === events && f.parent === parent && f.selector === selector && f.func === func && f.reverseScope === reverseScope); if(!already) { const handler = (e) => { let flag = false, target = e.target, selectList = Array.from(parent.querySelectorAll(selector)); while (target.tagName !== "BODY") { if (selectList.includes(target)) { let event = { delegateTarget: parent, currentTarget: target, data: data, originalEvent: e }; !reverseScope && func(event); flag = true; break; } target = target.parentNode ? target.parentNode : ""; } let event = { delegateTarget: parent, currentTarget: e.target, data, originalEvent: e }; reverseScope && !flag && func(event); }; parent.addEventListener(events, handler, capture); EventHandles.push({ events, parent, selector, func, reverseScope, handler }); } }; // 解除由onDelegate()綁定的事件監聽 const offDelegate = (events = "", parent, selector = "", func, reverseScope = false) => { paramCheck("off", events, parent, selector, func, {}, reverseScope); let hands = EventHandles.filter(f => f.events === events && f.parent === parent && f.selector === selector && f.func === func && f.reverseScope === reverseScope); hands.forEach(i => { parent.removeEventListener(events, i.handler); EventHandles.splice(EventHandles.indexOf(i), 1); }); }; export { onDelegate, offDelegate };
這個需求可能不太常見
export const convertImgUrlToBase64 = (url, outputFormat) => new Promise((resolve, reject) => { let img = document.createElement("img"); img.crossOrigin = 'Anonymous'; let canvas = document.createElement('CANVAS'); let ctx = canvas.getContext('2d'); img.src = url; img.addEventListener("load", () => { canvas.height = img.height; canvas.width = img.width; ctx.drawImage(img, 0, 0); let dataURL = canvas.toDataURL(outputFormat || 'image/png'); resolve(dataURL); canvas = null; }); });
這裏判斷瀏覽器外殼沒有太大的意義,重要的是判斷內核。
export const judgeBrowserType = () => { const agent = navigator.userAgent; let browser = "", version = "-1", ver; switch (true) { case agent.includes("Opera"): // Opera瀏覽器(非Chromium內核, 老版本) browser = "opera"; ver = agent.match(/Version\/([\d.]+)/)[1].split("."); version = `${ver[0]}.${ver[1]}`; break; case agent.includes("Trident") || agent.includes("MSIE"): // IE瀏覽器 或 IE內核 browser = "ie"; agent.includes("MSIE") && (ver = agent.match(/MSIE\/([\d.]+)/)[1].split(".")); !agent.includes("MSIE") && (ver = ["11", "0"]); version = `${ver[0]}.${ver[1]}`; break; case agent.includes("Edge"): // Edge瀏覽器 browser = "edge"; ver = agent.match(/Edge\/([\d.]+)/)[1].split("."); version = `${ver[0]}.${ver[1]}`; break; case agent.includes("Firefox"): // Firefox瀏覽器 browser = "firefox"; ver = agent.match(/Firefox\/([\d.]+)/)[1].split("."); version = `${ver[0]}.${ver[1]}`; break; case agent.includes("Gecko") && !agent.includes("like Gecko"): // 非Firefox的Gecko內核, 沒法判斷版本 browser = "firefox"; break; case agent.includes("Safari") && !agent.includes("Chrome"): // Safari瀏覽器 browser = "safari"; ver = agent.match(/Version\/([\d.]+)/)[1].split("."); version = `${ver[0]}.${ver[1]}`; break; case agent.includes("Chrome") && agent.includes("Safari"): // Google Chrome 或 Chromium內核 browser = "chrome"; ver = agent.match(/Chrome\/([\d.]+)/)[1].split("."); version = `${ver[0]}.${ver[1]}`; break; default: browser = "others"; break; } return { browser, version } };
各平臺的文件命名規則:
// turn === "turn"表示,不合規的字符會被修改爲下劃線(超出長度的字符被剪掉) const nameRule_compatible = (fullName, name, turn) => { let flag = true; let errorMsg = ""; const forbid = `?"/\\<>*|:`; fullName = fullName.trim(); name = name.trimLeft(); if(fullName.length > 255) { errorMsg = getLabel(513983, '名稱不得超過255個字符'); if (turn === "turn") { name = name.substring(0, 216); fullName = `${name}.${fullName.split(".").pop()}`; } } for(let i=0; i<forbid.length; i++) { if(name.includes(forbid[i])) { errorMsg = `${getLabel(513984, '文件名不能包含下列任何字符')}: \\ / : * ? " < > | `; if (turn === "turn") { let regExp = new RegExp(`\\${forbid[i]}`, "g"); name = name.replace(regExp, "_"); fullName = fullName.replace(regExp, "_"); } } } if(name[0] === ".") { errorMsg = getLabel(513985, '不能以 . 開頭'); if (turn === "turn") { fullName = "_" + fullName.substring(1); name = "_" + name.substring(1); } } if(errorMsg) { flag = false; message.warn(errorMsg); } return [flag, fullName, name] }; export { nameRule_compatible, }
這裏」組件 props 使用「指的是:當使用某個組件時,沒法直接接觸到其內部使用的某組件,而這時但願改變該某組件的 props 傳參。這裏有兩個方法,一是獲取目標組件的ref,能夠直接修改值;二是直接獲取目標組件的變量(或其父組件、祖先組件,能夠順着找到目標組件)來操做。須要指出的是,第二種方法具備破壞性,能夠在實在沒辦法的狀況下使用。
報錯「傳遞給系統調用的數據區域過小」。是因爲 IE 瀏覽器中對 href、src 這些屬性對 url 長度有限制,而 base64 通常都比較長。原理上講,先將 base64 轉 blob 再生成 url,但由 blob 生成 url 這部分操做(HTML標準)的結果,在IE下會報錯。怎麼解決呢?使用IE本身的API: window.navigator.msSaveOrOpenBlob(blob, fileName);
本業務中的功能是:拖拽文件圖標至文件夾中,完成文件移動的功能。此功能在開發中依照 HTML5 標準 API 編寫,基於最新的穩定版Google Chrome(78),並未發現任何兼容性問題。
解決方法:drop 事件中阻止默認行爲
IE11:在 dataTransfer.setData() 時,鍵不能自定義,只能是標準規定的如 Text
IE十、IE9:支持 HTML5 標準,但本人未做測試
IE9 如下:不支持標準 API
舊版本的 KTHML 內核:測試版本是16,遇到的問題與 IE11 相同,處理方式相同
新版本的 Chromium 內核:無兼容性問題
IE 內核:不要使用 dataTransfer 對象來傳遞數據,可使用共享的變量(如全局變量、store、類屬性this.xxx),須要該數據去維護
Chromium 內核:緣由在於 e.dataTransfer.setData() 中的 key (貌似須要使用自定義key)
若是子元素是 input,子元素 draggable=true,dragstart 事件阻止默認事件便可
若是子元素是普通元素,使用 mousedown/mouseup 事件 或 mouseenter/mouseleave事件 相互配合,改變父元素的 draggable 屬性
指望效果是在 dragenter 事件(進入目標元素時)改變背景顏色,dragleave 事件(離開目標元素時)恢復背景顏色。現實狀況是:進入目標元素後離開目標元素前不斷閃爍(屢次交替發生 dragenter/dragleave),而且時長沒法恢復背景顏色。
緣由:若是目標元素內部沒有子元素,不會出現上述異常。若是內部有多個子元素(及後代元素),那麼拖動元素在目標元素上通過子元素時會有上述異常。明明是在目標元素上綁定的這兩個事件,卻在其全部的後代元素上都會觸發(並不是冒泡)
解決方法:設置一個緩存變量(布爾值),標記當前是否進入/離開目標元素,排除子元素的干擾,便可。
只需將 dataTransfer 對象設置 DownloadURL 便可。
斷點續傳必然要分片上傳,前端將文件分片上傳,後端一個一個地接收分片並存儲,當所有接收完畢後再合併。所以,在分片上傳時,須要先後端協商好文件名、任務ID、分片個數、當前分片索引等信息。
分片上傳建議一個一個地上傳(串行上傳),當用戶暫停上傳時,當前正在上傳的分片中斷,下次繼續上傳時,今後分片開始上傳。
前端的核心問題是如何實現文件分片,後端的核心問題是如何將文件合併、什麼時候合併。
前端分片經過 HTML5 FILE API 的 slice() 方法,可將文件分片。後端在所有分片接收完畢時便可開始合併,合併思路:新建二進制文件,按順序讀取分片,將讀取的二進制流依次寫入新文件,正確命名特別是擴展名,便可完成合並。
前端實驗代碼:
function SliceUploadFile() { let fileObj = document.getElementById("file").files[0]; // js 獲取文件對象 const itemSize = 8 * 1024 * 1024; // 分片大小:8M const number = Math.ceil(fileObj.size / itemSize); // 分片數量 let prev = 0; for(let i=0; i<number; i++) { let start = prev; let end = start + itemSize; let blob = fileObj.slice(start, end); let msg = {type: "slice", name: fileObj.name, task: "fileTest", count: number, current: i}; // FormData 對象 var form = new FormData(); form.append("author", "xueba"); // 能夠增長表單數據 {#console.log("msg", msg);#} form.append("msg", JSON.stringify(msg)); form.append("file", blob);// 文件對象 // jQuery ajax $.ajax({ url: "/upload/", type: "POST", async: true, // 異步上傳 data: form, contentType: false, // 必須false纔會自動加上正確的Content-Type processData: false, // 必須false纔會避開jQuery對 formdata 的默認處理。XMLHttpRequest會對 formdata 進行正確的處理 xhr: function () { let xhr = $.ajaxSettings.xhr(); xhr.upload.addEventListener("progress", progressSFunction, false); xhr.upload.onloadstart = (e) => { progress[0] = { last_laoded: 0, last_time: e.timeStamp, }; console.log("開始上傳",progress); }; xhr.upload.onloadend = () => { delete progress[0]; console.log("結束上傳",progress); }; return xhr; }, success: function (data) { data = JSON.parse(data); data.forEach((i) => { console.log(i.code, i.file_url); }); }, error: function () { alert("aaa上傳失敗!"); }, }); prev = end } }
後端分片上傳代碼:
try: resList, fileList = [], request.FILES.getlist("file") msg = json.loads(request.POST.get("msg")) print(f"msg: {msg['type']}, count: {msg['count']}, current: {msg['current']}") dir_path = 'static/files/{0}/{1}/{2}'.format(time.strftime("%Y"), time.strftime("%m"), time.strftime("%d")) if os.path.exists(dir_path) is False: os.makedirs(dir_path) for file in fileList: filename = f"{msg['current']}_{msg['task']}" if msg['type'] == "slice" else file.name file_path = '%s/%s' % (dir_path, filename) file_url = '/%s/%s' % (dir_path, filename) res = {"code": 0, "file_url": ""} with open(file_path, 'wb') as f: if f == False: res['code'] = 1 for chunk in file.chunks(): # chunks()代替read(),若是文件很大,能夠保證不會拖慢系統內存 f.write(chunk) res['file_url'] = file_url resList.append(res) return HttpResponse(json.dumps(resList)) except: return HttpResponse("error")
後端分片合併代碼:
def mergeFiles(): "合併分片文件" path = "../static/files/2019/11/26" fileList = [file for file in os.listdir(path)] fileList.sort(key=lambda x: int(x.split("_")[0])) maxIndex = int(fileList[-1].split("_")[0]) mergeName = "企業應用-部署介紹和nginx安裝.mp4" with open(f"{path}/{mergeName}", "wb") as f: for fileName in fileList: print("正在合併", fileName) with open(f"{path}/{fileName}", "rb") as file: # f.write(file.read()) for line in file: f.write(line)
在整個系統中常見文件下載,下載自己的實現也很簡單,但下載若是有異常可能會致使前端頁面報錯、白屏、錯誤頁等問題,也就是提示不友好的問題。
通過思考,我認爲這須要先後端的配合才能夠作到友好提示,以下:
多行文本省略號,目前 CSS 沒有正式的標準方案,webkit 內核的瀏覽器(Chromium內核【Chrome、Edge、Opera、國產瀏覽器】、Firefox68+、Safari)有非標準的 CSS 方案能夠實現。可是對於低版本火狐、舊版Edge、IE、舊版Opera等,沒法只經過 CSS 實現。之前的處理辦法是 overflow: hidden,再設置 max-height、固定 width。雖然沒有省略號效果,可是也能看得過去。
如今的狀況不一樣了,系統的字體能夠隨時變化:「大」、「中」、「默認」。致使在 overflow: hidden 時,max-height 的值沒法固定,此方案行不通。所以,在這種狀況下,通過個人摸索找到了兩種方法:
這裏重點講第一種方法的 CSS(LESS) ,能夠作到在對應的系統字體下,3行之內沒有省略號,超過3行出現省略號:
LESS:
.WeaDoc-showName{ color: #333333; margin-top: 6px; letter-spacing: -0.08px; display: inline-block; position: relative; word-wrap: break-word; word-break: break-all; cursor: text; min-width: 25px; .textname{ position: relative; overflow: hidden; text-overflow: ellipsis; display: -moz-box; display: -webkit-box; -ms-box-orient: vertical; -moz-box-orient: vertical; -webkit-box-orient: vertical; -ms-line-clamp: 2; -moz-line-clamp: 2; -webkit-line-clamp: 2; } @font-size-list: 12, 14, 16; @font12-height: 38px; @font14-height: 44px; @font16-height: 51.2px; @gradient-color: white; @base-after-number: 7; .show-name-common(@height){ float: right; width: 100%; margin-left: -5px; max-height: @height + 1; } .show-before-common(@height){ content: ""; float: left; width: 5px; height: @height; } .show-after-common(@bottom, @fontSize, @backColor: white){ content: "..."; float: right; position: relative; bottom: ~"@{bottom}px"; left: 100%; width: 30px; font-size: ~"@{fontSize}px"; margin-left: -30px; padding-right: 5px; background: linear-gradient(to right, transparent, @backColor 45%, @backColor); box-sizing: content-box; text-align: right; transform: translateX(-4px); pointer-events: none; } .font-compatible-loop(1, 7); .font-compatible-loop(@i, @base) when (@i <= length(@font-size-list)) { @size: extract(@font-size-list, @i); @heightStr: "font@{size}-height"; .forLoopItem(@size, @@heightStr, @base); .font-compatible-loop(@i + 1, @base + 1); } .forLoopItem(@size, @height, @base){ &.text-@{size}{ height: @height; overflow: hidden; .textname{ .show-name-common(@height: @height); } &::before{ .show-before-common(@height: @height); } &::after{ .show-after-common(@bottom: @size + @base, @fontSize: @size); } } } }
變異後的CSS:
.WeaDoc-showName { color: #333333; margin-top: 6px; letter-spacing: -0.08px; display: inline-block; position: relative; word-wrap: break-word; word-break: break-all; cursor: text; min-width: 25px; } .WeaDoc-showName .textname { position: relative; overflow: hidden; text-overflow: ellipsis; display: -moz-box; display: -webkit-box; -ms-box-orient: vertical; -moz-box-orient: vertical; -webkit-box-orient: vertical; -ms-line-clamp: 2; -moz-line-clamp: 2; -webkit-line-clamp: 2; } .WeaDoc-showName.text-12 { height: 38px; overflow: hidden; } .WeaDoc-showName.text-12 .textname { float: right; width: 100%; margin-left: -5px; max-height: 39px; } .WeaDoc-showName.text-12::before { content: ""; float: left; width: 5px; height: 38px; } .WeaDoc-showName.text-12::after { content: "..."; float: right; position: relative; bottom: 19px; left: 100%; width: 30px; font-size: 12px; margin-left: -30px; padding-right: 5px; background: linear-gradient(to right, transparent, white 45%, white); box-sizing: content-box; text-align: right; transform: translateX(-4px); pointer-events: none; } .WeaDoc-showName.text-14 { height: 44px; overflow: hidden; } .WeaDoc-showName.text-14 .textname { float: right; width: 100%; margin-left: -5px; max-height: 45px; } .WeaDoc-showName.text-14::before { content: ""; float: left; width: 5px; height: 44px; } .WeaDoc-showName.text-14::after { content: "..."; float: right; position: relative; bottom: 22px; left: 100%; width: 30px; font-size: 14px; margin-left: -30px; padding-right: 5px; background: linear-gradient(to right, transparent, white 45%, white); box-sizing: content-box; text-align: right; transform: translateX(-4px); pointer-events: none; } .WeaDoc-showName.text-16 { height: 51.2px; overflow: hidden; } .WeaDoc-showName.text-16 .textname { float: right; width: 100%; margin-left: -5px; max-height: 52.2px; } .WeaDoc-showName.text-16::before { content: ""; float: left; width: 5px; height: 51.2px; } .WeaDoc-showName.text-16::after { content: "..."; float: right; position: relative; bottom: 25px; left: 100%; width: 30px; font-size: 16px; margin-left: -30px; padding-right: 5px; background: linear-gradient(to right, transparent, white 45%, white); box-sizing: content-box; text-align: right; transform: translateX(-4px); pointer-events: none; }
組件庫也屬於公共組件的範疇,在業務中被大量複用。通常在大公司中會有本身的一套組件庫供業務開發使用,注重通用性、便捷性。但對於一個龐大的系統而言,一套組件庫不能照顧到全部的邊邊角角,有些模塊須要定製本身的公共部分、有些部分可能只在這一個模塊中被複用。在這裏,我說的公共組件指的是這部分。
首先,你須要一個組件或者一些功能,卻並無現成的輪子。(確認組件庫中真的沒有這部分)
其次,你須要的這個組件,可能會在不少地方複用
首先要明白,在 React 中一個功能可能並不徹底由公共組件實現、也有多是公共組件與業務代碼相互配合實現(這樣的狀況不少),所以咱們在開發公共組件的時候要明白,如何權衡、如何劃分最合理
公共組件內部也要注意合理拆分(解耦),爲了代碼具有更好的擴展性、可維護性、可讀性,分爲三個維度的拆分:
狀態數據的管理:絕大部分的公共組件內部均可以使用 React 自身的 API 實現狀態管理(state、hooks),若是該公共組件過於龐大,內部過於複雜,可使用 mobx、redux 等狀態管理。不管哪一種方式,這些狀態數據都是隻供組件內部使用的,不能是外部使用的。
對於業務組件來講,最重要的是明確一個公共組件適合在何時用、該如何使用(掌握 API)
屬性:大多數的 React 組件功能經過調整屬性 props 便可實現,屬性的值能夠是全部類型 number/string/boolean/function/...... 。這裏又分爲幾種不一樣的方式
組件實例 ref :若是公共組件想對外提供一些方法以供調用,須要經過 ref 。這裏須要說明的是,你須要考慮哪些方法暴露出去、哪些方法不暴露出去。