<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Node.js 結(jié)合 MongoDB 實(shí)現(xiàn)字段級(jí)自動(dòng)加密

          共 11332字,需瀏覽 23分鐘

           ·

          2022-06-07 14:14

          關(guān)注并將「趣談前端」設(shè)為星標(biāo)

          定期推送技術(shù)干貨/優(yōu)秀開(kāi)源/技術(shù)思維

          某些場(chǎng)景下,對(duì)于數(shù)據(jù)隱私會(huì)有較高的要求,例如,用戶系統(tǒng)的個(gè)人信息(身份證、手機(jī)號(hào))、醫(yī)患系統(tǒng)的患者信息等,怎么用技術(shù)手段安全的保護(hù)這些敏感數(shù)據(jù)是我們開(kāi)發(fā)人員需要考慮的問(wèn)題

          本篇文章,將介紹 MongoDB 的客戶端字段級(jí)加密功能,英文全稱為 Client-Side Field Level Encryption,在有些地方會(huì)看到簡(jiǎn)稱為 CSFLE,代表的是一個(gè)意思,下文有些地方也會(huì)這樣稱呼。

          該功能允許開(kāi)發(fā)人員將數(shù)據(jù)保存到 MongoDB 服務(wù)器之前選擇性的指定數(shù)據(jù)字段進(jìn)行加密,這些加密/解密操作都是事先在客戶端完成,與服務(wù)器通信時(shí)完全是加密的,最終只有配置了 CSFLE 客戶端才能讀取和寫(xiě)入敏感數(shù)據(jù)字段。

          文末列舉了幾個(gè)使用中的常見(jiàn)錯(cuò)誤原因,如有遇到類似錯(cuò)誤可以做為參考。

          環(huán)境要求

          MongoDB Server 選擇:MongoDB 客戶端字段級(jí)加密分為自動(dòng)加密、手動(dòng)加密兩種類型,自動(dòng)加密社區(qū)版是不支持的,需要 MongoDB Server 4.2 企業(yè)版 或 MongoDB Atlas,學(xué)習(xí)使用推薦 MongoDB Atlas,它是在云服務(wù)器中托管的 MongoDB 服務(wù)器,不需要安裝,且提供了免費(fèi)的入門套餐是夠我們學(xué)習(xí)使用了。

          驅(qū)動(dòng)兼容性:使用支持 CSFLE 功能的 Node.js MongoDB 驅(qū)動(dòng)程序,3.4+ 以上版本是支持的,快速入門。

          libmongocrypt:客戶端字段級(jí)加密依賴 libmongocrypt,它是 MongoDB 驅(qū)動(dòng)程序?qū)崿F(xiàn)客戶端加密/解密的核心組件,對(duì)應(yīng)的 Node.js NPM 包為 mongodb-client-encryption,需要注意這個(gè)包依賴于 libbson 和 libmongocrypt C 庫(kù),需要 C++ 工具鏈,但是做為 Node.js Addons 插件,其已經(jīng)利用 prebuild 在 CI 期間做了模塊的預(yù)先編譯,直接 npm i mongodb-client-encryption 安裝即可,如果網(wǎng)絡(luò)環(huán)境問(wèn)題鏈接不上 github.com 可能就很麻煩了需要手動(dòng)構(gòu)建、編譯,因?yàn)閷?duì)模塊的預(yù)先編譯是放在 Github 上的。

          mongocryptd:客戶端加密必須要 mongocryptd 進(jìn)程啟動(dòng)才能正常工作,剛開(kāi)始一直遇到一個(gè)問(wèn)題:MongoError: BSON field 'insert.jsonSchema' is an unknown field. This command may be meant for a mongocryptd process. 貌似就是因?yàn)?mongocryptd 進(jìn)程沒(méi)有啟動(dòng)導(dǎo)致的。在 MongoDB Server 企業(yè)版中包含 mongocryptd 這個(gè)組件的,解決辦法也很簡(jiǎn)單就是本機(jī)安裝下企業(yè)版,盡管我們這里使用的是 MongoDB Atlas 也要安裝的,安裝方法參考 docs.mongodb.com/manual/tutorial/install-mongodb-enterprise-on-os-x。

          項(xiàng)目準(zhǔn)備

          做一些初始化工作,安裝依賴、配置文件、創(chuàng)建一個(gè)常規(guī)的 MongoDB client。

          項(xiàng)目初始化

          mkdir?nodejs-mongodb-client-encryption
          cd?nodejs-mongodb-client-encryption
          npm?init
          npm?i?mongodb?mongodb-client-encryption?-S

          配置文件

          創(chuàng)建一個(gè) index.js 文件,核心代碼邏輯都在該文件編寫(xiě),

          //?index.js
          const?base64?=?require('uuid-base64');
          const?{?MongoClient,?Binary?}?=?require('mongodb');
          const?{?ClientEncryption?}?=?require('mongodb-client-encryption');
          const?fs?=?require('fs');

          //?配置
          const?config?=?{
          ??connectionString:?'${替換為自己的?MongoDB?鏈接字符串}',
          ??keyVaultDb:?'encryption',?//?encryption?表示密鑰保管數(shù)據(jù)庫(kù)
          ??keyVaultCollection:?'__keyVault',?//?__keyVault?表示集合
          ??keyVaultNamespace:?`encryption.__keyVault`,?//?密鑰庫(kù)命名空間
          ??keyAltNames:?'test-data-key',
          ??masterKeyPath:?'master-key.txt'
          }
          const?LOCAL_MASTER_KEY?=?fs.readFileSync(config.masterKeyPath);?//?讀取本地主密鑰
          const?kmsProviders?=?{?//?指定?KMS?提供程序設(shè)置
          ??local:?{
          ????key:?LOCAL_MASTER_KEY,
          ??},
          };

          創(chuàng)建常規(guī) Client

          /**
          ?*?獲取常規(guī)?Mongo?客戶端
          ?*?@param?{String}?connectionString
          ?*?@returns?
          ?*/
          function?getRegularClient(connectionString)?{
          ??const?client?=?new?MongoClient(connectionString,?{
          ????useNewUrlParser:?true,
          ????useUnifiedTopology:?true,
          ??});

          ??return?client.connect();
          }

          數(shù)據(jù)加密密鑰

          MongoDB 驅(qū)動(dòng)程序自動(dòng)加密/解密時(shí)需要訪問(wèn)事先創(chuàng)建的數(shù)據(jù)加密密鑰,而這個(gè)密鑰經(jīng)過(guò)程序的處理會(huì)存儲(chǔ)在密鑰保管數(shù)據(jù)庫(kù)的集合中,以下是創(chuàng)建一個(gè)數(shù)據(jù)加密密鑰的交互圖。

          創(chuàng)建主密鑰

          創(chuàng)建 MongoDB 數(shù)據(jù)加密密鑰還需要另外一個(gè)稱為 “主密鑰” 的密鑰進(jìn)行加密,下圖展示了創(chuàng)建主密鑰的流程:主密鑰的存儲(chǔ),生產(chǎn)環(huán)境 MongoDB 官方的推薦是使用密鑰管理服務(wù)(KMS):亞馬遜網(wǎng)絡(luò)服務(wù) KMS、Azure 密鑰保管庫(kù)、谷歌云平臺(tái)密鑰管理,更多內(nèi)容可閱讀 客戶端字段級(jí)加密:使用 KMS 存儲(chǔ)主密鑰。

          學(xué)習(xí)為目的,簡(jiǎn)單方便些可使用本地密鑰提供程序存儲(chǔ)主密鑰,這種方式不安全,不適合生產(chǎn)。

          創(chuàng)建一個(gè)腳本文件 create-master-key.js,生成一個(gè) 96 字節(jié)的密鑰文件,并寫(xiě)入到本地文件系統(tǒng)的 master-key.txt 文件中。

          //?create-master-key.js
          const?fs?=?require('fs');
          const?crypto?=?require('crypto');

          try?{
          ??fs.writeFileSync('master-key.txt',?crypto.randomBytes(96));
          }?catch?(err)?{
          ??console.error(err);
          }

          指定 KMS 程序配置

          客戶端使用如下配置發(fā)現(xiàn)主密鑰,local 表示的是使用本地主密鑰。

          const?LOCAL_MASTER_KEY?=?fs.readFileSync(config.masterKeyPath);?//?讀取本地主密鑰
          const?kmsProviders?=?{?//?指定?KMS?提供程序設(shè)置
          ??local:?{
          ????key:?LOCAL_MASTER_KEY,
          ??},
          };

          獲取或創(chuàng)建數(shù)據(jù)加密密鑰

          寫(xiě)一個(gè)函數(shù) getOrCreateDataKey 分別傳入創(chuàng)建的常規(guī) client、上面指定的 KMS 程序配置,該方法目的是獲取一個(gè)數(shù)據(jù)密鑰,如果不存在則創(chuàng)建,實(shí)現(xiàn)為以下幾個(gè)步驟:

          • 在密鑰保管庫(kù)集合的 keyAltNames 字段上先設(shè)置唯一索引,這里創(chuàng)建的是一個(gè)部分索引,符合條件的才會(huì)創(chuàng)建。
          • 檢查是否已創(chuàng)建數(shù)據(jù)加密密鑰,若創(chuàng)建則立即返回。
          • 若未創(chuàng)建數(shù)據(jù)加密密鑰,向指定的密鑰保管庫(kù)集合創(chuàng)建一條新的數(shù)據(jù)密鑰。
          /**
          ?*?獲取或創(chuàng)建數(shù)據(jù)加密密鑰
          ?*?如果已存在?dataKey?則返回,否則創(chuàng)建一條?dataKey
          ?*/

          async?function?getOrCreateDataKey(regularClient,?kmsProviders)?{
          ??//?在密鑰保管庫(kù)集合的?keyAltNames?字段上先設(shè)置索引
          ??await?regularClient
          ????.db(config.keyVaultDb)
          ????.collection(config.keyVaultCollection)
          ????.createIndex("keyAltNames",?{
          ??????unique:?true,
          ??????partialFilterExpression:?{
          ????????keyAltNames:?{
          ??????????$exists:?true
          ????????}
          ??????}
          ????});

          ??//?檢查是否已創(chuàng)建數(shù)據(jù)加密密鑰
          ??const?dataKeyInfo?=?await?regularClient
          ????.db(config.keyVaultDb)
          ????.collection(config.keyVaultCollection)
          ????.findOne({
          ??????keyAltNames:?{
          ????????$in:?[config.keyAltNames]
          ??????}
          ????});
          ??if?(dataKeyInfo)?{?//?存在立即返回
          ????return?dataKeyInfo['_id'].toString("base64");
          ??}

          ??//?創(chuàng)建一條新的數(shù)據(jù)密鑰
          ??const?encryption?=?new?ClientEncryption(regularClient,?{
          ????keyVaultNamespace:?config.keyVaultNamespace,
          ????kmsProviders,
          ??});
          ??const?dataKey?=?await?encryption.createDataKey('local',?{
          ????keyAltNames:?[config.keyAltNames]
          ??});
          ??return?dataKey.toString('base64');
          }

          驗(yàn)證數(shù)據(jù)加密密鑰是否成功創(chuàng)建

          調(diào)用編寫(xiě)好的方法,驗(yàn)證下數(shù)據(jù)加密密鑰是否創(chuàng)建成功。

          (async?()?=>?{
          ??let?regularClient;
          ??try?{
          ????//?創(chuàng)建常規(guī)?MongoDB?客戶端
          ????regularClient?=?await?getRegularClient(config.connectionString);
          ????//?獲取數(shù)據(jù)加密密鑰
          ????const?base64DataKeyId?=?await?getOrCreateDataKey(regularClient,?kmsProviders);
          ??}?catch?(err)?{
          ????console.error(err);
          ????regularClient.close();
          ??}
          })();

          我使用 Robo 3T 鏈接的 Atlas 集群,如果一切正常,你會(huì)看到在 encryption.__keyVault 集合中有如下一條密鑰記錄,_id 字段就是為我們需要的數(shù)據(jù)加密密鑰,使用 Base64 格式編碼。

          JSON Schema 定義

          Node.js 驅(qū)動(dòng)程序使用 JSON Schema 定義集合需要加密的字段,文檔類型定義使用 BSON 類型。

          • encryptMetadata.keyId:在根級(jí)別配置數(shù)據(jù)加密密鑰,properties 中的每個(gè)字段默認(rèn)都繼承該密鑰,除非特別指定,參考 docs.mongodb.com/manual/reference/security-client-side-automatic-json-schema/#encryptmetadata-schema-keyword。
          • algorithm:指定加密算法,AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic 為確定性加密算法,對(duì)讀取操作提供了更好的支持,安全系數(shù)相對(duì)沒(méi)有**隨機(jī)加密 **AEAD_AES_256_CBC_HMAC_SHA_512-Random 高,隨機(jī)加密算法每次執(zhí)行加密都會(huì)輸出不同的值。
          /**
          ?*?使用?JSON?Schema?定義集合需要加密的字段
          ?*?@param?{String}?base64DataKeyId
          ?*?@returns?
          ?*/

          function?getSchemaMap(base64DataKeyId)?{
          ??//?使用?JSON?Schema?指定加密字段
          ??const?userJsonSchema?=?{
          ????bsonType:?'object',
          ????encryptMetadata:?{
          ??????keyId:?[new?Binary(Buffer.from(base64DataKeyId,?'base64'),?4)]
          ????},
          ????properties:?{
          ??????phone:?{
          ????????encrypt:?{
          ??????????bsonType:?'string',
          ??????????algorithm:?'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'
          ????????}
          ??????},
          ??????password:?{
          ????????encrypt:?{
          ??????????bsonType:?'string',
          ??????????algorithm:?'AEAD_AES_256_CBC_HMAC_SHA_512-Random'
          ????????}
          ??????},
          ??????emergencyContact:?{
          ????????bsonType:?'object',
          ????????properties:?{
          ??????????phone:?{
          ????????????encrypt:?{
          ??????????????bsonType:?'string',
          ??????????????algorithm:?'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'
          ????????????}
          ??????????},
          ????????}
          ??????}
          ????}
          ??}

          ??//?將?JSON?模式映射到集合上
          ??const?schemaMap?=?{
          ????'test.users':?userJsonSchema
          ??};

          ??return?schemaMap;
          }

          CSFLE 客戶端驗(yàn)證讀寫(xiě)操作

          在有了數(shù)據(jù)加密密鑰、JSON Schema 之后可以創(chuàng)建一個(gè)支持 CSFLE 的 Mongo client,該客戶端和 MongDB 服務(wù)器交互,讀取/寫(xiě)入帶有加密字段的數(shù)據(jù)。

          讀寫(xiě)操作流程圖

          下圖展示了客戶端應(yīng)用程序和驅(qū)動(dòng)程序?yàn)閷?xiě)入字段級(jí)加密數(shù)據(jù)的一個(gè)步驟:下圖展示了客戶端應(yīng)用程序和驅(qū)動(dòng)程序?yàn)樽x取加密后字段進(jìn)行解密操作的一個(gè)過(guò)程:

          創(chuàng)建 CSFLE 客戶端

          創(chuàng)建 CSFLE 的 mongo client 與常規(guī) mongo client 相比較,需要多傳入 autoEncryption 對(duì)象,以下參數(shù)含義分別為:

          • keyVaultNamespace:存放數(shù)據(jù)加密密鑰的密鑰保管庫(kù)集合名稱。
          • kmsProviders:指定本地主密鑰。
          • schemaMap:需要加密字段的一些定義。
          function?getCSFLEClient(schemaMap,?kmsProviders)?{
          ??const?secureClient?=?new?MongoClient(config.connectionString,?{
          ????useNewUrlParser:?true,
          ????useUnifiedTopology:?true,
          ????monitorCommands:?true,
          ????autoEncryption:?{
          ??????bypassAutoEncryption:?true,
          ??????keyVaultNamespace:?config.keyVaultNamespace,
          ??????kmsProviders,
          ??????schemaMap
          ????}
          ??});

          ??return?secureClient.connect();
          }

          (async?()?=>?{
          ??try?{
          ????const?regularClient?=?await?getRegularClient(config.connectionString);
          ????const?base64DataKeyId?=?await?getOrCreateDataKey(regularClient,?kmsProviders);
          ????const?schemaMap?=?getSchemaMap(base64DataKeyId);
          ????const?csfleClient?=?await?getCSFLEClient(schemaMap,?kmsProviders);
          ????
          ??//?執(zhí)行讀寫(xiě)操作
          ??}?catch?(err)?{
          ????console.error(err);
          ??}
          })();

          執(zhí)行讀寫(xiě)操作驗(yàn)證

          在擁有 CSFLE 客戶端后,執(zhí)行一些讀寫(xiě)操作,創(chuàng)建一條用戶記錄,下面的代碼和我們常規(guī)的讀寫(xiě)操作沒(méi)什么區(qū)別,并且 phone 這個(gè)字段雖然是經(jīng)過(guò)加密的,我們?nèi)钥墒褂迷撟侄巫鰹樗饕?查找數(shù)據(jù)。

          (async?()?=>?{
          ??try?{
          ????//?...
          ????const?db?=?csfleClient.db('test');
          ????const?userColl?=?db.collection('users');
          ????const?doc?=?{
          ??????name:?'小張',
          ??????phone:?'18800030009',
          ??????password:?'123456',
          ??????emergencyContact:?{
          ????????name:?'小李',
          ????????phone:?'16600260023'
          ??????}
          ????};
          ????const?query?=?{?phone:?doc.phone?};
          ????await?userColl.updateOne(query,?{?$set:?doc?},?{?upsert:?true?});
          ????const?result?=?await?userColl.findOne(query);
          ????console.log(result);
          ??}?catch?(err)?{
          ????console.error(err);
          ??}
          })();

          當(dāng)成功插入一條記錄之后,在 Robo 3T 工具查詢?cè)摷希梢钥吹叫枰淖侄味家呀?jīng)做了加密,盡管我是一個(gè)管理員能夠查看數(shù)據(jù),也無(wú)法查看這些隱私數(shù)據(jù)。

          image.png

          只能通過(guò)程序正確的創(chuàng)建了 CSFLE 的客戶端才能讀取出解密后的數(shù)據(jù)。

          image.png

          幾個(gè)常見(jiàn)錯(cuò)誤

          文中示例測(cè)試時(shí)常見(jiàn)的幾個(gè)錯(cuò)誤,可以做為參考。

          認(rèn)證失敗

          遇到 Authentication failed 錯(cuò)誤,基本上都是連接字符串的賬號(hào)密碼或權(quán)限錯(cuò)誤,使用 MongoDB Atlas 的需要檢查下數(shù)據(jù)庫(kù)的訪問(wèn)權(quán)限配置

          image.png
          MongoServerError:?bad?auth?:?Authentication?failed.
          ??...
          ??ok:?0,
          ??code:?8000,
          ??codeName:?'AtlasError',
          ??[Symbol(errorLabels)]:?Set(0)?{}
          }

          創(chuàng)建加密客戶端鏈接失敗

          下面的報(bào)錯(cuò)很簡(jiǎn)單就是服務(wù)器鏈接不上。需要注意的是文中創(chuàng)建加密客戶端還會(huì)去鏈接本地安裝的 MongoDB 企業(yè)版 Server,在本地啟動(dòng) MongoDB 企業(yè)版 Server 時(shí)需要指定下端口 bin/mongod --dbpath data --logpath logs/mongo.log --port 27020

          MongoServerSelectionError:?connect?ECONNREFUSED?127.0.0.1:27020
          ????at?Timeout._onTimeout?(/Users/***********/nodejs-mongodb-client-encryption/node_modules/mongodb/lib/sdam/topology.js:318:38)
          ????at?listOnTimeout?(internal/timers.js:554:17)
          ????at?processTimers?(internal/timers.js:497:7)?{
          ??reason:?TopologyDescription?{
          ????type:?'Unknown',
          ????servers:?Map(1)?{?'localhost:27020'?=>?[ServerDescription]?},
          ????stale:?false,
          ????compatible:?true,
          ????heartbeatFrequencyMS:?10000,
          ????localThresholdMS:?15,
          ????logicalSessionTimeoutMinutes:?undefined
          ??},
          ??code:?undefined,
          ??[Symbol(errorLabels)]:?Set(0)?{}
          }

          mongocryptd 進(jìn)程注意事項(xiàng)

          在剛開(kāi)始的環(huán)境要求里有提到過(guò) mongocryptd 進(jìn)程,它會(huì)在這里檢查 JSON Schema 中定義的加密指令,也就是 getCSFLEClient() 傳入的 schemaMap 參數(shù),如果 mongocryptd 進(jìn)程沒(méi)有啟動(dòng),這里會(huì)一直報(bào)錯(cuò)。

          以下是我最開(kāi)始一直遇到的一個(gè)問(wèn)題,解決辦法很簡(jiǎn)單:

          • 第一步,本機(jī)安裝下企業(yè)版
          • 第二步,創(chuàng)建加密的 MongoDB 客戶端時(shí),鏈接參數(shù)要設(shè)置 autoEncryption.bypassAutoEncryption=true 會(huì)自動(dòng)生成 mongocryptd 進(jìn)程。
          writeError?occurred:?MongoError:?BSON?field?'insert.jsonSchema'?is?an?unknown?field.?This?command?may?be?meant?for?a?mongocryptd?process.
          ????at?MessageStream.messageHandler?(/Users/quzhenfei/Documents/study/node_modules/mongodb/lib/cmap/connection.js:268:20)
          ????at?MessageStream.emit?(events.js:314:20)
          ????at?processIncomingData?(/Users/quzhenfei/Documents/study/node_modules/mongodb/lib/cmap/message_stream.js:144:12)
          ????at?MessageStream._write?(/Users/quzhenfei/Documents/study/node_modules/mongodb/lib/cmap/message_stream.js:42:5)
          ????at?writeOrBuffer?(_stream_writable.js:352:12)
          ????at?MessageStream.Writable.write?(_stream_writable.js:303:10)
          ????at?TLSSocket.ondata?(_stream_readable.js:713:22)
          ????at?TLSSocket.emit?(events.js:314:20)
          ????at?addChunk?(_stream_readable.js:303:12)
          ????at?readableAddChunk?(_stream_readable.js:279:9)?{
          ??operationTime:?Timestamp?{?_bsontype:?'Timestamp',?low_:?1,?high_:?1632613160?},
          ??ok:?0,
          ??code:?4662500,
          ??codeName:?'Location4662500',
          ??'$clusterTime':?{
          ????clusterTime:?Timestamp?{?_bsontype:?'Timestamp',?low_:?1,?high_:?1632613160?},
          ????signature:?{?hash:?[Binary],?keyId:?[Long]?}
          ??}
          }

          總結(jié)

          MongoDB 提供的客戶端字段級(jí)自動(dòng)加密,對(duì)于有數(shù)據(jù)隱私需要加密保護(hù)的還是很方便的,在配置了 CSFLE 客戶端后應(yīng)用程序在讀寫(xiě)操作時(shí)和常規(guī)的客戶端讀寫(xiě)操作是沒(méi)有差別的,唯一的阻礙可能是僅企業(yè)版支持。

          文中我們將主密鑰存儲(chǔ)放在了本地的文件系統(tǒng)中,這在本地測(cè)試環(huán)境是可以的,但是生產(chǎn)環(huán)境不要用這種方式,因?yàn)槿魏文軌蛟L問(wèn)您本地文件系統(tǒng)主密鑰的人都可以讀取您的數(shù)據(jù)加密密鑰,建議放在更安全的地方,例如密鑰管理系統(tǒng)(KMS)。

          Reference

          • docs.mongodb.com/drivers/security/client-side-field-level-encryption-guide/#e.-perform-encrypted-read-write-operations
          • 基于 Mongo Shell 的手動(dòng)加密 https://www.modb.pro/db/100877
          • www.mongodb.com/community/forums/t/fle-mongoerror-bson-field-insert-jsonschema-is-an-unknown-field/5472/7
          • www.mongodb.com/developer/how-to/client-side-field-level-encryption-csfle-mongodb-node
          • mongodb.github.io/node-mongodb-native/3.4/reference/client-side-encryption/

          ?? 看完三件事

          如果你覺(jué)得這篇內(nèi)容對(duì)你挺有啟發(fā),我想邀請(qǐng)你幫我三個(gè)小忙:

          • 點(diǎn)個(gè)【在看】,或者分享轉(zhuǎn)發(fā),讓更多的人也能看到這篇內(nèi)容
          • 關(guān)注公眾號(hào)【趣談前端】,定期分享?工程化?/?可視化?/?低代碼?/?優(yōu)秀開(kāi)源





          從零搭建全棧可視化大屏制作平臺(tái)V6.Dooring

          從零設(shè)計(jì)可視化大屏搭建引擎

          Dooring可視化搭建平臺(tái)數(shù)據(jù)源設(shè)計(jì)剖析

          可視化搭建的一些思考和實(shí)踐

          基于Koa + React + TS從零開(kāi)發(fā)全棧文檔編輯器(進(jìn)階實(shí)戰(zhàn)




          點(diǎn)個(gè)在看你最好看

          瀏覽 56
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美在线a | 激情五月,五月婷婷 | 精品毛片一区二区三区 | 老牛AV国产性久久 | av在线小说 |