最近在業務中遇到了一個關於 多級下拉 需求,須要將後端樹狀數據顯示在 textarea
上,同時 textArea
中也能對數據進行處理,轉化爲能進行多級選擇樹狀數據。javascript
拿問卷星的多級下拉舉個例子,以下圖所示,用戶能夠在 textArea
框進行多級下拉的數據的編寫,第一行表明標題,餘下的每一行表明一個多級下拉框中各級的數據,各級數據之間使用 /
來進行分隔。css
數據編輯完成保存以後,咱們將樹狀數據用在移動端或者小程序端,這樣就完成了一個多級下拉的組件。html
今天這篇文章就簡單介紹一下這個工做流程,主要包括:前端
textarea
上展現的 value
值 ?textarea
中的數據 轉化爲 樹狀數據 ?npm
的組件?關於多級下拉的 數據展現 在這篇文章中不會作介紹,那麼接下來咱們就開始發車。java
這個組件是使用 React Hooks + TypeScript
來實現,衆所周知,Hooks
是 React
將來的趨勢,同時 TypeScript
也是 JavaScript
將來的趨勢,小弟恰好拿這個組件練練手。node
打包工具用的是 Rollup
,由於打包組件庫的 Rollup
比 Webpack
更受歡迎,Webpack
更適合打包複雜的大型項目。關於 Webpack
的學習,你們能夠參考筆者整理的 Webpack 學習文檔。react
項目結構以下所示:webpack
. ├── node_modules // 第三方的依賴 ├── example // 開發時預覽代碼 ├── public // 放置靜態資源文件夾 ├── src // 示例代碼目錄 ├── app.js // 測試項目 入口 js 文件 └── index.html // 測試項目 入口 html 文件 ├── yarn.lock // 測試項目 yarn lock 文件 └── package.json // 測試項目 依賴 ├── src // 組件源代碼目錄 ├── components // 輪子的目錄 ├──textarea // 項目內部使用的一個 textarea 組件 ├── index.less // 組件核心代碼樣式文件 └── textarea.tsx // 組件核心代碼 ├── types // typescripe 的接口定義 ├── utils // 工具函數目錄 ├── assets // 靜態資源目錄 ├── index.tsx // 項目入口文件 └── index.less // 項目入口樣式文件 ├── lib // 組件打包結果目錄 ├── test // 測試文件夾 ├── typings // 放置項目全局 ts 申明文件目錄 ├── .babelrc // babel 配置文件 ├── .eslintignore // eslintignore 配置文件 ├── .eslintrc.js // eslint 配置文件 ├── .gitignore // git上傳時忽略的文件 ├── api-extractor.json // 用於將多個 ts 聲明文件合成一個 ├── jest.config.js // 測試配置文件 ├── .npmignore // npm 上傳忽略文件 ├── README.md ├── tsconfig.eslint.json // ts 的 eslint 文件 ├── tsconfig.json // ts 的配置文件 ├── rollup.config.js // rollup 打包配置文件 ├── yarn.lock // yarn lock 文件 └── package.json // 當前整一個項目的依賴
對於項目中除了源碼之外的一些知識點,筆者就不細說了,好比Rollup
如何配置;api-extractor
如何將多個聲明文件生成一個等等,你們能夠自行查閱一波。
倉庫地址在此:多級下拉 textarea 組件git
Hooks
骨架代碼咱們使用的 React Hooks
來編寫這個組件,通常編寫組件以前咱們須要明確這個組件該支持哪些功能,即支持哪些 props
,在這個組件中暫時支持下面這些參數:github
<TreeTextArea treeTitle={title} // 多級下拉 標題數據 treeData={tree_value} // 樹狀數據 row={21} // textarea 的行數 showNumber // 是否展現左側 textarea 數字 shouleGetTreeData // 是否開啓 處理樹數據的功能 delimiter='/' // 以什麼符號切割 maxLevel={4} // 支持的最大級數 onChangeTreeData={ // 與 shouleGetTreeData 進行搭配使用,返回處理後的標題和樹狀數據 (treeTitle, treeData) => { console.log('---treeTitle---', treeTitle); console.log('---treeData---', treeData); } } defaultData={DEFAULT_TEXT} // 樹狀數據默認值 placeholder='請輸入標題,例:省份/城市/區縣/學校 浙江省/寧波市/江北區/學校1' />
咱們在 src/components/textarea.tsx
中進行相應代碼的編寫,其中包括接收相應的傳入相應的 props
值、剛進入頁面的時候去 初始化數據、監聽數據變化、獲取樹狀值 等操做:
const TreeTextArea = (props: Props): JSX.Element => { // 一系列 props 數據的接受 // ... const [__textAreaData, setTextAreaData] = useState(''); const [__flattenData, setFlattenData] = useState([]); // 數據初始化 useEffect(()=>{ if (isArray(__treeData) && isArray(__treeTitle)) { // ... const flattenData = flattenChainedData(__treeData); const textAreaData = getTextAreaData(flattenData, titles); setFlattenData(flattenData); setTextAreaData(textAreaData.join('\n')); } return ()=>{ // willUnMount } }, []) // 監聽數據變化 const onChange = (data: any): void => {} // 設置默認值 const getDefaultData = (): void => {} // 獲取樹狀值 const getTreeData = (e: any): void => { const { onChangeTreeData } = props; // ... if (onChangeTreeData) { onChangeTreeData(levelTitles, valueData); } } return ( <div className={styles.wrapper}> <NumberTextArea row={__row} value={__textAreaData} onChange={onChange} showNumber={__showNumber} placeholder={__placeholder} errCode={__errCode} errText={__errText} /> { // ... // 填充默認值、獲取樹狀值 代碼 } </div> ) }
咱們內部還封裝了一個 NumberTextArea
,在這個組件中我增長了 左側序號顯示、錯誤顯示 等邏輯,具體的代碼就不貼上來了,你們有興趣能夠參考源碼。
關於相關的 React Hooks
知識你們能夠自行查閱相關資料學習,筆者在這裏也不作介紹了。
接下來咱們來看一下組件中最核心的 多級下拉邏輯處理。
在總體骨架代碼搭建好以後,咱們就只需關注 textarea
處理數據的邏輯就好了。
首先咱們在 utils
目錄下新建 testData.ts
,模擬後端的 json
數據,以下圖所示:
咱們先從編輯開始提及,假如後端給了咱們要渲染的標題以及多級下拉的樹狀數據:
接着咱們但願經過一些處理將後端給的數據修改爲 textarea
中能夠展現的 value
值,相似於下面的字符串做爲 value
值:
這裏咱們須要作的是將 樹狀數據 進行 扁平化處理,給每一級的數據增長一個 title
屬性,這即是咱們須要在 textarea
每一行中所要展現的數據,相似以下的數據:
咱們須要將每一級的數據的都扁平化出來。
但這裏有一個問題好比 浙江省/寧波市 和 浙江省/寧波市/海曙區 這是兩個不一樣的數據,可是在 textarea
中其實不須要展現 浙江省/寧波市 這一個數據的,因此我在這裏作了一個判斷,若是這一個數據有孩子的話,就給他增長一個屬性 hasChildren
,讓咱們在獲取 textarea
的數據的時候作一下過濾就好了,不展現有屬性 hasChildren
的數據。
那麼咱們如何來扁平化數據呢?其實只要對樹狀數據作一下遞歸處理就好了。
/** * 將後端的 樹狀結構 數據 扁平化 * @param {Array} data : 後端 tree_node 數據 */ export const flattenChainedData = (data: any) => { let arr = []; forEach(data, (item) => { const childrens = item.children; const rootObj = createNewObj(item, item.value); if (childrens) { rootObj.hasChildren = true; } arr.push(rootObj); if (childrens) { // 遞歸得到全部扁平的數據 const dataNew = getChildFlattenData(childrens, item.value, 1); arr = concat(arr, dataNew); } }); return arr; }; /** * 遞歸得到 扁平數組 * @param {*} data : 要處理的數組 * @param {*} title : 前幾級 拼的 title * @param {*} level : 當前級數 */ const getChildFlattenData = (data, title, level) => { // 超過最大級數 if (level > MAX_LEVEL) return false; if (!data) return false; let arr = []; forEach(data, (item) => { const { children } = item; const rootObj = createNewObj(item, `${title}/${item.value}`); if (children) { rootObj.hasChildren = true; const childrenData = getChildFlattenData(children, `${title}/${item.value}`, level + 1); arr.push(rootObj); arr = concat(arr, childrenData); } else { arr.push(rootObj); } }); return arr; };
其中上面的 createNewObj
是爲扁平數據新增 value/title 屬性,返回新的對象,具體就不上代碼了。
轉化爲扁平數據以後,咱們就能夠將數據中的 title 屬性拿出來,組成 textarea 所需的數據便可:
/** * 將 扁平數據 轉化爲 textarea 中 value * @param {Array} flattenData : 扁平化數據 * @param {String} titles : textarea 第一行的 title */ export const getTextAreaData = (flattenData, titles) => { const newData = filter(flattenData, (item) => { return !item.hasChildren && item.status !== 2; }); const arr = []; arr.push(titles); forEach(newData, (item) => { arr.push(item.title); }); return arr; };
其中咱們過濾了 hasChildren
爲 true
,同時這裏以 status = 2
表示刪除的數據,也進行過濾, 這樣咱們即可以獲得以下圖所示的一個 textarea 數組:
接着咱們將這些數組經過 \n
換行符 join
起來就是咱們所需的 textarea
的 value
值了。
這裏是整個多級下拉邏輯中最核心的部分,咱們須要將用戶修改過的數據,與原來的數據進行關聯,生成新的樹狀數據。咱們分爲四個步驟來說解這一個步驟。
咱們會建立一個 數據處理類 treeTextAreaDataHandle
,並在 constructor
構造函數中傳入 扁平化數據、textarea
文本框的值 來初始化一個實例對象。以後咱們會在此類中完善咱們處理數據的一些屬性和方法。
class treeTextAreaDataHandle { // 扁平化數組 private flattenData: FlattenDataObj[]; // textarea 框 文本值 private textAreaTexts: string; constructor(options: treeTextAreaData) { const { flattenData, textAreaTexts } = options; this.flattenData = flattenData; this.textAreaTexts = textAreaTexts; } }
第一步咱們會生成用戶修改後的 textarea
初始映射數據,咱們會根據級數分別放在不一樣的數組中,舉個🌰:
咱們會根據用戶最後輸入完成的 textarea
值,來生成一組根據級數排布的對象數據,以下圖:
如上面這張圖會轉化爲以下面圖中的數據,這個數據會是咱們進行接下去三步操做的關鍵:
這裏須要注意的一個問題,有可能在某一級是有同名的值存在,這個時候咱們不能單純的就把這兩個值認爲是同一個值,而要去比較他們的爸爸是不是同樣的,以此類推,遞歸比較直到第一級,若是都是同樣的話,那麼他們纔是同一個值,不然就是不一樣的值。
舉個簡單例子,好比有兩個數據: 浙江省/寧波市/海曙區 和 江蘇省/無錫市/海曙區 這兩個值,雖然第三級中的 海曙區 名字是相同的,可是他們是兩個不一樣的值,他們應該被分配到兩個不一樣的 id,而且各自的 parent_id 也不同。
接下來上代碼,咱們新建一個實例方法 transDataFromText
,在這個方法中進行樹狀數據的轉化,並獲得最後的數據:
/** * 將 textarea 數據 轉化爲 後端所需的樹狀結構數據 * @param {Array} flattenData : 扁平數據 * @param {String} texts : textarea 的文本 */ public transDataFromText() { const texts = this.textAreaTexts; const arr = texts.split('\n'); // 去除標題 if (arr.length > 1) { arr.shift(); } // 賦值每一行文字爲數組 this.textAreaArr = arr; // 解析 TextArea 數據 爲 指定 層級映射數據 this.parserRootData(); // ... }
咱們在 parserRootData
這個方法中去生成修改後的初始映射:
/** * 將 textarea 數據 轉化爲 相應級數 的 數據 * @param {Array} textArr : textarea 的文本 轉化的數組 * @param {Number} handleLevel : 要處理的級數 */ private parserRootData() { // 每一行的 textArea 值 const textArr = this.textAreaArr; // 最大級數 const handleLevel = this.MAX_LEVEL; // 以什麼分隔符切割 const delimiter = this.delimiter; // 去重 每一級 textArea 值 const uniqueTextArr = uniq(textArr); // 映射數據存放對象 const namesArrObj: namesArrObj = {}; // 根據最大級數爲每一級建立一個數組 for (let i = 1; i <= handleLevel; i++) { namesArrObj[`${ROOT_ARR_PREFIX}_${i}`] = []; } // 遍歷 每一行的 textArea 值 forEach(uniqueTextArr, (item: string) => { // 切割 每一行 字符串,生成字符串 const itemArr = item.split(delimiter); // 根據最大級數往 namesArrObj 塞數據 for (let i = 1; i <= handleLevel; i++) { if ( !treeTextAreaDataHandle.sameParentNew(namesArrObj, itemArr, i) && itemArr[i - 1] ) { // 建立一個對應級數的對象,塞入對應的數組 const obj: parserItemObj = {}; obj.id = _id(); obj.value = itemArr[i - 1]; obj.level = i; // 獲取當前的級數的值,爸爸的 id const parentId = treeTextAreaDataHandle.getParentIdNew( namesArrObj, itemArr, i ); obj.parent_id = parentId; namesArrObj[`${ROOT_ARR_PREFIX}_${i}`].push(obj); } } }); // 保存到對象的 rootArrObj 屬性值中 this.rootArrObj = namesArrObj; }
上面最爲關鍵的一個方法就是靜態方法 sameParentNew
,做用是幫咱們遞歸判斷 兩個相同名稱的值是否真的相同。其實原理也很簡單,也是 遞歸 判斷他們各自的爸爸是否相同。具體代碼你們能夠參考源碼。
其次這裏還有用到相似:
id
的方法:_id()
parent_id
的靜態方法:getParentIdNew
到這裏咱們第一步生成 初始映射 數據就完成了,接下來咱們就須要結合後端提供給咱們的扁平數據 flattenData 來填充已存在的數據,同時篩選出新增的數據。
這一步咱們須要將後端給咱們的數據 flattenData
與咱們的初始映射數據進行比對。填充存在數據的屬性同時篩選出新增的數據,並給新增數據加上屬性 new = true
,最後塞到對應的對象對應級數組中去 existNamesArrObj
和 addNamesArrObj
。
舉個🌰
咱們新增了 浙江省/寧波市/高新區,咱們能夠在新增數據中的第三級中找到 高新區,由於 浙江省 和 寧波市 已經存在,他們不會被添加到新增的數組中去,只會在已存在的對象中被找到,而且會用後端給的 id
替換掉咱們以前生成映射數據是生成的 id
,以下圖:
存在數據:existNamesArrObj
新增數據:addNamesArrObj
這裏咱們還要注意的一個點是,咱們在進行數據篩選以前,須要將後端給的數據flattenData
數據中加上一個屬性root_id
,它的做用是幫咱們將 修改後數據 和以前 後端給的數據 進行關聯,好比上面咱們新增 高新區 這個例子,他的爸爸是已經存在的,他的id
是已經存在的 36178,可是新增的高新區的parent_id
是咱們在映射數據時生成的,這兩個確定不相等,咱們須要藉助root_id
來將這兩個數據聯繫起來。
接下來上代碼,咱們將這一波處理放到 handleExistData
方法中,
/** * 填充已有的數據,並篩選出新增的數據 * @param {*} TextAreaData : parserRootData() 處理的數據 * @param {*} newFlattenData : 扁平化數據 * @param {Number} handleLevel : 要處理的級數 */ private handleExistData() { const namesArrObj = this.rootArrObj; const newFlattenData = this.flattenData; const handleLevel = this.MAX_LEVEL; // 存在的數據 const existNamesArrObj = {}; // 新增的數據 const addNamesArrObj = {}; for (let i = 1; i <= handleLevel; i++) { addNamesArrObj[`${ADD_ARR_PREFIX}_${i}`] = []; existNamesArrObj[`${EXIST_ARR_PREFIX}_${i}`] = []; } // flatten 加上 parser 的 映射 id this.setMapIdForFlattenDataToRootData(); for (let i = 1; i <= handleLevel; i++) { // 獲取出事映射相應級數的數據 const curNamesArr = namesArrObj[`${ROOT_ARR_PREFIX}_${i}`]; forEach(curNamesArr, (item) => { // 設立一個標誌位 // 標誌這一級的數據是否存在 let flag = false; // 映射數據的屬性 const { value, parent_id, id } = item; // 新增數據 obj const addNewObj: addNewObj = { level: i, value, id, new: true, root_id: id, }; // 遍歷比較後端數據 與 映射數據 的 `value` 和 `level` // 來肯定他們映射數據是否存在 // 存在就 forEach(newFlattenData, (val) => { if (value === val.value) { if (val.level === i) { // level 等於 1 if (val.level === 1 && val.parent_id === 0) { const obj = { ...val }; existNamesArrObj[`${EXIST_ARR_PREFIX}_${i}`].push(obj); flag = true; } // level 大於 1 if (val.level !== 1 || val.parent_id !== 0) { if (this.isExistitem(val, parent_id, i)) { const obj = { ...val }; existNamesArrObj[`${EXIST_ARR_PREFIX}_${i}`].push(obj); flag = true; } } } } }); // 若是是新增數據 if (!flag) { // 塞入 addNamesArrObj addNamesArrObj[`${ADD_ARR_PREFIX}_${i}`].push(addNewObj); // 塞入 最新 扁平化數據 newFlattenData.push(addNewObj); } }); } // 將 existNamesArrObj 掛到類屬性 existNamesArrObj 上 this.existNamesArrObj = existNamesArrObj; this.addNamesArrObj = addNamesArrObj; }
上面的方法中還用到了一個比較重要的方法 isExistitem
方法,判斷當前級數據是否存在,其原理跟parserRootData
中用到的 sameParentNew
相似,也是去遞歸比較在他們的爸爸是不是相同的,直到找到不一樣的爸爸或者到第一級爲止,只不過這裏面比較的是 初始映射數據 和 後端扁平數據。
還有一個方法就是咱們上面講到的給 後端扁平數據 添加 root_id
的方法 setMapIdForFlattenDataToRootData
,具體代碼筆者不貼了,你們有興趣能夠自行查看。
處理完已存在數據,和新增數據,咱們還須要處理刪除的數據。
這裏若是需求中要出對數據進行排序的話,其實能夠吧 存在數據 和 新增數據 放在一個對象中,這樣每次有新增或者存在數據的時候都會從上都下依次塞入,如今筆者是吧 存在數據 和 新增數據 分開來了, 新增數據 默認都是在最後的。
通常來講,若是數據刪除了,前端還須要將數據傳給後端,告訴後端這條數據刪除了。因此咱們須要給刪除的數據中加上相應的 狀態值,這裏咱們加了 status = 2
,表明此條數據在前端已經被刪除了。
實現起來很簡單,由於咱們經過第二步已經獲得了 已經存在的數據,只須要拿它與最初的 後端提供的扁平數據 進行比較一波就能得出哪些數據被刪除了,篩選出來以後將他們將上相應的屬性便可。
好比咱們刪除了 江蘇省/無錫市/惠山區 這一行,實際上是刪除了 無錫市 和 惠山區 兩個數據,咱們能夠獲得以下結果:
接下來上代碼,咱們將篩選刪除數據的方法寫在 handleTagForDeleleByLevel
方法中,
/** * 根據標題 幾級 來獲取刪除的數據,並給刪除數據打上標籤,並返回刪除數據 * @param {*} handleDataArr : fillExistData() 處理的數據 * @param {*} newFlattenData : 扁平化數據 * @param {Number} handleLevel : 要處理的級數 */ private handleTagForDeleleByLevel = () => { const existNamesArrObj = this.existNamesArrObj; const handleLevel = this.MAX_LEVEL; // 存放 存在扁平數據 的數組 let existData = []; // 遍歷 存在數據對象 扁平化存在數據 for (let i = 1; i <= handleLevel; i++) { const curArray = existNamesArrObj[`${EXIST_ARR_PREFIX}_${i}`]; existData = concat(existData, curArray); } // 給刪除數據添加屬性 status = 2 const deleteData = this.addTagForDeleleData(existData); // 將 deleteData 掛到屬性 deleteData 上 this.deleteData = deleteData; };
咱們經過 addTagForDeleleData
方法來比較不一樣的值,並加上屬性 status=2
,在這裏咱們也可使用lodash
的 difference
方法來獲得兩個數組不一樣的值。
處理完上面三步以後,基本上就大公告成了,接下來就生成最終樹狀數據。
最後咱們就須要將 存在數據、新增數據、刪除數據 生成一個新的扁平化數組,由這個新扁平化數據生成咱們想要的樹狀數據。
好比咱們新增 浙江省/寧波市/高新區,刪除 江蘇省/無錫市/惠山區,最終會獲得新的扁平數據以下,咱們能夠看到 高新區 是新增的,惠山區 也加上了相應的 status=2
的屬性:
接着咱們就能夠根據這個扁平化數據,遞歸生成樹狀數據,以下圖:
接下來上代碼,首先 getLastFlattenData
方法,經過這個方法咱們能夠獲取到最新的扁平化數據:
/** * 生成最新的數據 * @param {*} existNamesArrObj : existNamesArrObj 已存在數據 * @param {*} addNamesArrObj : addNamesArrObj 新增數據 * @param {*} deleteData : addTagForDeleleByLevel() 獲得的刪除數據 * @param {Number} handleLevel : 要處理的級數 */ private getLastFlattenData() { const existNamesArrObj = this.existNamesArrObj; const newAddNamesArrObj = this.newAddNamesArrObj; const deleteData = this.deleteData; const handleLevel = this.MAX_LEVEL; let lastData = []; let AddLast = []; let ExistLast = []; // 遍歷 扁平化 存在和新增數據 for (let i = 1; i <= handleLevel; i++) { const curArrayExist = existNamesArrObj[`${EXIST_ARR_PREFIX}_${i}`]; const curArrayAdd = newAddNamesArrObj[`${HANDLE_ADD_ARR_PREFIX}_${i}`]; ExistLast = concat(ExistLast, curArrayExist); AddLast = concat(AddLast, curArrayAdd); } // 合併三種類型的數據 lastData = concat(lastData, ExistLast, AddLast, deleteData); // 將 lastData 掛到 類屬性 newDataLists 上 this.newDataLists = lastData; };
最後就是生成最終樹狀數據,原理就是從 parent_id
爲 0
開始進行 遞歸遍歷,直到遍歷完全部節點爲止,與此同時咱們須要在生成樹以前,刪除一些本來不須要的屬性,好比新增屬性 new
,映射關聯的 root_id
等。
/** * 刪除 以前 組裝 樹狀結構時 使用的 一些自定義屬性 * 後端不須要 * @param {Object} item : 每一項的 item */ static clearParamsInTreeData = (item) => { delete item.title; delete item.hasChildren; delete item.root_id; if (item.new) { delete item.new; delete item.id; delete item.parent_id; } }; /** * 遞歸 將扁平數據轉化爲 樹狀 結構數據 * 用於 transDataFromText * @param {Array} lists : 扁平數據 * @param {Number} parent_id : 爸爸的 id */ private getTreeDataBylists = (parent_id: number | string): any => { const lists = this.newDataLists; //遞歸,菜單 const tree = []; forEach(lists, (item) => { const newItemId = item.parent_id; if (parent_id === newItemId) { const childrenTree = this.getTreeDataBylists(item.id); if (isArray(childrenTree) && childrenTree.length > 0) { item.children = childrenTree; } else { item.children = null; } // 刪除沒必要要屬性 treeTextAreaDataHandle.clearParamsInTreeData(item); tree.push(item); } }); return tree; };
到這裏咱們變完成了對 textarea
的處理,最終的 transDataFromText
方法以下:
/** * 將 textarea 數據 轉化爲 後端所需的樹狀結構數據 * @param {Array} flattenData : 扁平數據 * @param {String} texts : textarea 的文本 */ public transDataFromText() { const texts = this.textAreaTexts; const arr = texts.split('\n'); if (arr.length > 1) { arr.shift(); } this.textAreaArr = arr; // 解析 TextArea 數據 爲 指定 層級映射數據 this.parserRootData(); // 填充已有數據 並 篩選新增數據 this.handleExistData(); // 處理新增數據 this.handleParamsInAddData(); // 獲取刪除數據 this.handleTagForDeleleByLevel(); // 獲取最新扁平數據 this.getLastFlattenData(); // 獲取最新樹狀數據 this.lastTreeData = this.getTreeDataBylists(0); return this.lastTreeData; }
咱們須要對一些錯誤進行處理,好比 用戶可能不會輸入標題、又或者 用戶輸入的標題大於了最大支持級數(固然在咱們項目中,這個最大支持級數用戶能夠本身來控制)、又或者 標題的級數與下面內容的級數不對應,這些都應該被歸爲錯誤列表中。
舉個例子,當用戶沒有輸入標題的時候,咱們應該提示其輸入標題,以下圖:
咱們新建一個方法 isEquelLevel
方法,來檢測用戶輸入的值是否符合規範,代碼其實也很簡單,咱們能夠取到最終的數據,遍歷數據中是否存在錯誤,存在錯誤就拋出相應的 錯誤碼 errorCode
和 錯誤信息 ERROR_INFO
,錯誤類型以下:
/** * 校驗信息 */ export const ERROR_INFO = { 1: '第一行標題不可爲空', 2: `第一行標題不可超過 ${MAX_LEVEL} 列`, 3: '標題和選擇項的層級數請保持一致', 4: `選擇項不可超過 ${MAX_LEVEL} 行`, 5: '請至少填寫一行選擇項', };
功能寫完以後,咱們須要測試一下組件的功能,能夠藉助使用 create-react-app
的 react-scripts
幫咱們快速啓動一個應用:
package.json
配置:如下是測試項目 package.json
文件:
{ "name": "example", "version": "0.0.0", "description": "", "license": "MIT", "private": true, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject", }, "author": "Darrell", "dependencies": { "lodash": "^4.17.15", "react": "link:../node_modules/react", "react-dom": "link:../node_modules/react-dom", "react-scripts": "^3.4.1" }, "browserslist": [ ">0.2%", "not dead", "not ie <= 11", "not op_mini all" ] }
這裏面須要注意的一個問題就是,這裏面的 react
和 react-dom
兩個依賴須要使用上一級根目錄 node_modules
下的依賴。
由於使用 Hooks
寫的插件會由於有多個 React
應用而報錯,以下圖:
致使這個問題的緣由主要是第一個 React
版本沒到 16.8
,或者第三個,在項目中有多個 React
引用。
至於第二個問題,Hooks
不符合規範基本上在咱們安裝了 eslint-plugin-react-hooks
插件以後就基本上能夠規避掉了。關於這個問題的更多信息你們能夠參考 這條 issure。
而後咱們進入 exmaple
安裝相應的依賴,直接運行 yarn start
就能夠將咱們的項目跑起來了。
咱們在 example
的 public
目錄下新建
index.html
:項目的模版文件,即負責項目顯示的 html
文件<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="theme-color" content="#000000"> <link rel="manifest" href="%PUBLIC_URL%/manifest.json"> <title>測試頁面</title> </head> <body> <noscript> You need to enable JavaScript to run this app. </noscript> <div id="root"></div> </body> </html>
manifest.json
:若是寫手機端的 h5app
,圖標、主題顏色等是在這個文件裏是設置的,在這裏咱們能夠隨意配置一波。同時在 src
目錄下新建:
index.js
:項目入口文件index.css
:入口文件的樣式import React from 'react' import ReactDOM from 'react-dom' import './index.css' import App from './App' ReactDOM.render(<App />, document.getElementById('root'))
App.js
:測試組件的文件import React, { Component } from 'react' // 測試數據 import { title, tree_value, DEFAULT_TEXT } from './testData' import TreeTextArea from 'darrell-tree-textarea' export default class App extends Component { render () { return ( <div className='App'> <TreeTextArea treeTitle={title} treeData={tree_value} row={21} showNumber shouleGetTreeData delimiter='/' maxLevel={4} onChangeTreeData={ (treeTitle, treeData) => { console.log('---treeTitle---', treeTitle); console.log('---treeData---', treeData); } } defaultData={DEFAULT_TEXT} placeholder='請輸入標題,例:省份/城市/區縣/學校 浙江省/寧波市/江北區/學校1' /> </div> ) } };
npm
以前的測試上面的測試文件是寫在咱們的組件項目中的。
可是通常在發包以前,咱們須要在其餘的項目裏面測試使用一下,這個時候咱們能夠藉助 npm link
。
npm link
,這句命令意思就是將組件引入到全局的 node_modules
。
npm link <package 名>
看個🌰:假設咱們使用 create-react-app
新建了一個項目 my-app
,咱們就能夠在此項目的根目錄下面,運行:
npm link @darrell/darrell-tree-textarea
這個時候咱們能夠在項目中有了咱們項目的依賴:
可是由於是項目的引用,因此這個依賴包含了咱們插件項目中的全部內容,包括 node_modules
,這裏會出現咱們上面提到的 Hooks
開發組件 Invalid hook call
這個錯誤,由於在咱們的依賴下有 @darrell/darrell-tree-textarea
下有 node_modules
文件下,在它下面有 React
依賴,同時在 my-app
下面的 node_modules
下也有 React
依賴,因此就會出現 多個 React
引用 這個問題。
這個問題在咱們發到 npm
上以後不會出現,由於在上傳到 npm
上的時候是不會把 node_modules
目錄傳上去的。
解決辦法有兩個:
@darrell/darrell-tree-textarea
下的 node_modules
,可是每次都須要從新安裝my-app
項目下,改一下配置文件,將全部的 react
引用指向同一個引用alias: { // ... 'react': path.resolve(__dirname, '../node_modules/react'), 'react-dom': path.resolve(__dirname, '../node_modules/react-dom'), },
關於如何發包你們能夠參考這篇文章:從零開始實現類 antd 分頁器(三):發佈npm,這篇文章中有詳細的介紹組件的測試和 npm
的發佈,在本篇文章中就不涉及了。
本文主要講了如何製做一個能處理 多級下拉樹狀數據 的 textarea
組件的編寫,總體來看仍是比較簡單,整個組件的難點應該是如何有效的 遞歸處理 數據:
children
。還有在組件測試那裏也折騰了蠻久的,由於碰到了 React Hooks
組件不能運行的問題,我曾一度覺得是 Hooks
的寫法有問題,後來沒想到是多個 React
引用出現的錯誤。
不過如今回過頭來思考這個問題,發現 React
的錯誤提醒其實作的很清楚,本身只要跟着這個錯誤提示一步一步就能把問題解決掉。
實不相瞞,想要個贊!