<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>

          一行代碼,搞定瀏覽器數(shù)據(jù)庫(kù) IndexedDB

          共 15014字,需瀏覽 31分鐘

           ·

          2021-03-11 08:44


          作者 | 星塵starx 

          來(lái)源 | https://juejin.cn/post/6918705632757415950
          前言

          2021 年,如果你的前端應(yīng)用,需要在瀏覽器上保存數(shù)據(jù),有三個(gè)主流方案可以選擇:

          • Cookie:上古時(shí)代就已存在,但能應(yīng)用的業(yè)務(wù)場(chǎng)景比較有限
          • LocalStorage:使用簡(jiǎn)單靈活,但是容量只有 10Mb,且儲(chǔ)存 JS 對(duì)象存在問(wèn)題
          • IndexedDB:算得上真正意義上的數(shù)據(jù)庫(kù),功能強(qiáng)大,但坑異常多,使用麻煩,古老的 API 設(shè)計(jì)放在現(xiàn)代前端工程中總有種格格不入的感覺(jué)

          我在大三的時(shí)候,曾經(jīng)用 IndexedDB 寫過(guò)一個(gè)背單詞 App,當(dāng)時(shí)就有把 IndexedDB 封裝一遍的想法,但是由于學(xué)業(yè)緊張,后來(lái)就擱置了

          最近,我終于有了空閑時(shí)間,于是撿起了當(dāng)年的想法,開(kāi)始嘗試用 TypeScript 把 IndexedDB 封裝一遍,把坑一個(gè)個(gè)填上,做成一個(gè)開(kāi)發(fā)者友好的庫(kù),并開(kāi)源出來(lái),上傳至 npm

          拍腦袋后,我決定把這個(gè)項(xiàng)目命名為 GoDB.js

          GoDB.js

          GoDB 的出現(xiàn),讓你即使你不了解瀏覽器數(shù)據(jù)庫(kù) IndexedDB,也能把它用的行云流水,從而把關(guān)注點(diǎn)放到業(yè)務(wù)上面去

          畢竟要用好 IndexedDB,你需要翻無(wú)數(shù)遍 MDN,而 GoDB 替你吃透了 MDN,從而讓你把 IndexedDB 用的更好的同時(shí),操作還更簡(jiǎn)單了

          當(dāng)前項(xiàng)目處于 Alpha 階段(版本 0.4.x),意味著之后隨時(shí)可能會(huì)有 breaking changes,在正式版(1.0.0 及以后)發(fā)布之前,不要把這個(gè)項(xiàng)目用到任何嚴(yán)肅的場(chǎng)景下

          安裝

          首先需要安裝,這里默認(rèn)你使用了 webpack、gulp 等打包工具,或在 vue、react 等項(xiàng)目中

          npm install godb

          在第一個(gè)正式版發(fā)布后,還會(huì)提供 CDN 的引入方式,敬請(qǐng)期待~

          簡(jiǎn)單上手

          操作非常簡(jiǎn)單,增、刪、改、查各只需要一行代碼:

          import GoDB from 'godb';

          const testDB = new GoDB('testDB');
          const user = testDB.table('user');

          const data = { name'luke'age22 };

          user.add(data) // 增
            .then(luke => user.get(luke.id)) // 查
            .then(luke => user.put({ ...luke, age23 })) // 改
            .then(luke => user.delete(luke.id)); // 刪

          • Table.get()Table.add() 和 Table.put() 都返回完整數(shù)據(jù)
          • Table.delete() 不返回?cái)?shù)據(jù)(返回 undefined

          需要注意的就是,put(obj) 方法中的 obj 需要包含 id,否則就等價(jià)于 add(obj)

          上面的 demo 中,get() 得到的 luke 對(duì)象包含 id,因此是修改操作

          之后會(huì)引入一個(gè) update 方法來(lái)改進(jìn)這個(gè)問(wèn)題

          也可以一次性添加多條數(shù)據(jù)

          const data = [
              { name'luke'age22 },
              { name'elaine'age23 }
          ];

          user.addMany(data)
            .then(() => user.consoleTable());

          Table.consoleTable()

          這里用了一個(gè) Table.consoleTable() 的方法

          雖然 chrome 開(kāi)發(fā)者工具內(nèi)就能看到表內(nèi)所有數(shù)據(jù),但這個(gè)方法好處是可以在需要的時(shí)候打印出數(shù)據(jù),方便 debug

          注意:這個(gè)方法是異步的,因?yàn)樾枰跀?shù)據(jù)庫(kù)里把數(shù)據(jù)庫(kù)取出來(lái);異步意味著緊接在它后面的代碼,可能會(huì)在打印出結(jié)果之前執(zhí)行,如果不希望出現(xiàn)這種情況,使用 await 或 Promise.then 即可

          addMany(data) 方法:

          • 嚴(yán)格按照 data 的順序添加
          • 返回 id 的數(shù)組,與 data 順序一致

          之所以單獨(dú)寫個(gè) addMany,而不在 add 里加一個(gè)判斷數(shù)組的邏輯,是因?yàn)橛脩粝胍?,可能就是添加一個(gè)數(shù)組到數(shù)據(jù)庫(kù)中

          注意:addMany 和 add 不要同步調(diào)用,如果在 addMany 正在執(zhí)行時(shí)調(diào)用 add,可能會(huì)導(dǎo)致數(shù)據(jù)庫(kù)里的順序不符合預(yù)期,請(qǐng)?jiān)?nbsp;addMany 的回調(diào)完成后再調(diào)用 add(未來(lái)可能會(huì)引入一個(gè)隊(duì)列來(lái)修復(fù)這個(gè)問(wèn)題)

          Table.find()

          如果你想在數(shù)據(jù)庫(kù)中查找數(shù)據(jù),還可以使用 Table.find() 方法:

          const data = [
              { name'luke'age22 },
              { name'elaine'age23 }
          ];

          user.addMany(data)
            .then(() => {
              user.find((item) => {
                return item.age > 22;
              }).then((data) => {
                console.log(data)
              })
              // { name: 'elaine', age: 23 }
            });

          Table.find(fn)接受一個(gè)函數(shù)fn作為參數(shù),這個(gè)函數(shù)的返回值應(yīng)當(dāng)為truefalse

          用法其實(shí)和 JS 數(shù)組方法 Array.find() 如出一轍

          這個(gè)方法在內(nèi)部會(huì)從頭遍歷整個(gè)表(使用 IndexedDB 的 Cursor),然后把每一次的結(jié)果放進(jìn) fn 執(zhí)行,如果 fn 的返回值為 true(內(nèi)部使用 if(fn()) 判斷),就返回當(dāng)前的結(jié)果,停止遍歷

          這個(gè)方法只會(huì)返回第一個(gè)滿足條件的值,如果需要返回所有滿足條件的值,請(qǐng)使用 Table.findAll(),用法與 Table.find() 一致,但是會(huì)返回一個(gè)數(shù)組,包含所有滿足條件的值

          Schema

          如果你希望數(shù)據(jù)庫(kù)的結(jié)構(gòu)更嚴(yán)格一點(diǎn),也可以添加 schema

          GoDB 會(huì)根據(jù) schema 建立 IndexedDB 數(shù)據(jù)庫(kù)索引,給字段添加特性

          import GoDB from 'godb';

          // 定義數(shù)據(jù)庫(kù)結(jié)構(gòu)
          const schema = {
              // user 表:
              user: {
                  // user 表的字段:
                  name: {
                      typeString,
                      uniquetrue // 指定 name 字段在表里唯一
                  },
                  ageNumber
              }
          }

          const testDB = new GoDB('testDB', schema);
          const user = testDB.table('user');

          const data = {
              name'luke'
              age22
          };

          user.add(data) // 沒(méi)問(wèn)題
            .then(() => user.get({ name'luke' })) // 定義schema后,就可以用id以外的字段獲取數(shù)據(jù)
            .then(luke => user.add(luke)) // 報(bào)錯(cuò),name 重復(fù)了

          如上面的例子,指定了 schema 后

          • 定義了 schema,因此 get() 和 delete() 中可以使用 id 以外的字段搜索了,否則只能傳入 id
          • 指定了 user.name 這一項(xiàng)是唯一的,因此無(wú)法添加重復(fù)的 name

          當(dāng)然,你也可以在 table 那定義 schema

          const testDB = new GoDB('testDB');
          const user = testDB.table('user', {
              name: {
                  typeString,
                  uniquetrue
              },
              ageNumber
          });

          但這種方式的缺點(diǎn)是,如果定義 table 發(fā)生在連接數(shù)據(jù)庫(kù)之后,GoDB 會(huì)先發(fā)起一個(gè) IDBVersionChange 的事件,導(dǎo)致 IndexedDB 數(shù)據(jù)庫(kù)版本升級(jí),此時(shí)如果有不止一個(gè) GoDB 實(shí)例連接了同樣的數(shù)據(jù)庫(kù),版本升級(jí)將會(huì)被 block,導(dǎo)致建表失敗

          要避免這個(gè)問(wèn)題倒是很簡(jiǎn)單,把所有獲取 table 的操作緊接在 new GoDB() 之后(保證這兩操作是同步而非異步執(zhí)行的)就可以,這樣可以確保所有 table 都在連接完成之前獲取到(JS 的事件循環(huán)特性)

          當(dāng)然,最佳實(shí)踐還是在連接數(shù)據(jù)庫(kù)時(shí)就定義好所有的 schema,這樣 GoDB 會(huì)在應(yīng)用初始化時(shí)就建立好所有的數(shù)據(jù)庫(kù)和表,并建立字段的索引

          Table.get() 與 Table.find() 區(qū)別

          get() 使用數(shù)據(jù)庫(kù)索引搜索,性能更高,但是需要定義 schema,才能使用 id 以外的索引進(jìn)行搜索

          而 find() 利用函數(shù)判斷遍歷全表,使用上更靈活,但是性能相對(duì)沒(méi)有 get() 好

          關(guān)于 schema:

          部分同學(xué)或許會(huì)發(fā)現(xiàn),上面定義 schema 的方式有點(diǎn)眼熟,沒(méi)錯(cuò),正是參考了 mongoose

          • 定義數(shù)據(jù)庫(kù)的字段時(shí),可以只指明數(shù)據(jù)類型,如上面的 age: Number
          • 也可以使用一個(gè)對(duì)象,里面除了定義數(shù)據(jù)類型 type,也指明這個(gè)字段是不是唯一的(unique: true),之后會(huì)添加更多可選屬性,如用來(lái)指定字段默認(rèn)值的 default,和指向別的表的索引 ref

          不定義 schema 時(shí),GoDB 使用起來(lái)就像 MongoDB 一樣,可以靈活添加數(shù)據(jù);區(qū)別是 Mongodb 中,每條數(shù)據(jù)的唯一標(biāo)識(shí)符是 _id,而 GoDB 是 id

          雖然這樣做的問(wèn)題是,用戶使用不規(guī)范的話(如每次添加的數(shù)據(jù)結(jié)構(gòu)都不一樣),久而久之可能會(huì)使得數(shù)據(jù)庫(kù)的字段特別多,維護(hù)和使用起來(lái)非常麻煩

          定義 schema 后,你將無(wú)法給 schema 內(nèi)沒(méi)有的字段添加數(shù)據(jù)(在寫文檔的時(shí)候還沒(méi)有實(shí)現(xiàn)這個(gè)功能,即使 schema 不符合也能加,下個(gè)版本會(huì)安排上)

          因此推薦在項(xiàng)目中,首先定義好 schema,這樣不管是維護(hù)性上,還是性能上,都要更勝一籌

          使用 await

          由于 GoDB 的 API 都是 Promise 的,因此在很多場(chǎng)景下可以使用 await,使代碼更簡(jiǎn)潔,同時(shí)拓寬使用場(chǎng)景(await 可以很方便用在循環(huán)內(nèi),而 Promise.then 很難)

          import GoDB from 'godb';

          const db = new GoDB('testDB', schema);
          const user = db.table('user', {
              name: {
                typeString,
                uniquetrue
              },
              ageNumber
          });

          crud();

          async function crud() {
            // 增:
            await user.addMany([
              { name'luke'age22 },
              { name'elaine'age23 }
            ]);

            console.log('add user: luke');
            await user.consoleTable(); // await 非必須,這里為了防止打印順序出錯(cuò)

            // 查:
            const luke = await user.get({ name'luke' });

            // 改:
            luke.age = 23;
            await user.put(luke);

            console.log('update: set luke.age to 23');
            await user.consoleTable();

            // 刪:
            await user.delete({ name'luke' });

            console.log('delete user: luke');
            await user.consoleTable();

          }


          上面這段 demo,會(huì)在控制臺(tái)打印出下面的內(nèi)容:

          19 個(gè)接私活平臺(tái)匯總升級(jí)版,你有技術(shù)就有錢

          API 設(shè)計(jì)

          因?yàn)椤高B接數(shù)據(jù)庫(kù)」和「連接表」這兩個(gè)操作是異步的,在設(shè)計(jì)之初,曾經(jīng)有兩個(gè) API 方案,區(qū)別在于:要不要把這兩個(gè)操作,做為異步 API 提供給用戶

          這里討論的不是「API 如何命名」這樣的細(xì)節(jié),而是「API 的使用方式」,因?yàn)檫@會(huì)直接影響到用戶使用 GoDB 時(shí)的業(yè)務(wù)代碼編寫方式

          以連接數(shù)據(jù)庫(kù) -> 添加一條數(shù)據(jù)的過(guò)程為例

          設(shè)計(jì)一:提供異步特性

          GitHub 上大多數(shù)開(kāi)源的 IndexedDB 封裝庫(kù)都是這么做的

          import GoDB from 'godb';

          // 連接數(shù)據(jù)庫(kù)是異步的
          GoDB.open('testDB')
              .then(testDB => testDB.table('user')) // 連接表也需要異步
              .then(user => {
                  user.add({
                      name'luke',
                      age22
                  });
              });
          });

          這樣的優(yōu)點(diǎn)是,工作流程一目了然,畢竟對(duì)數(shù)據(jù)庫(kù)的操作,要放在連接數(shù)據(jù)庫(kù)之后

          但是,這種設(shè)計(jì)不適合工程化的前端項(xiàng)目!

          因?yàn)?,所有增刪改查等操作,都需要用戶,手動(dòng)放到連接完成的異步回調(diào)之后,否則無(wú)法知道操作時(shí)有沒(méi)有連上數(shù)據(jù)庫(kù)和表

          導(dǎo)致每次需要操作數(shù)據(jù)庫(kù)時(shí),都要先打開(kāi)數(shù)據(jù)庫(kù)一遍數(shù)據(jù)庫(kù),才能繼續(xù)

          即使你預(yù)先定義一個(gè)全局的連接,你在之后想要使用它時(shí),如果不包一層 Promise,是無(wú)法確定數(shù)據(jù)庫(kù)和表,在使用時(shí)有沒(méi)有連接上的

          以 Vue 為例,如果你在全局環(huán)境(比如 Vuex)定義了一個(gè)連接:

          import GoDB from 'godb';

          new Vuex.Store({
            state: {
              godbawait GoDB.open('testDB'// 不加 await 返回的就是 Promise 了
            }
          });

          這樣,在 Vue 的任何一個(gè)組件中,我們都能訪問(wèn)到 GoDB 實(shí)例

          問(wèn)題來(lái)了,在你的組件中,如果你想在組件初始化時(shí),比如 created 和 mounted 這樣的鉤子函數(shù)中(React 中就是 ComponentDidMount),去訪問(wèn)數(shù)據(jù)庫(kù):

          new Vue({
             mounted() {
                 const godb = this.$store.state.godb; // 從全局環(huán)境取出連接
                 godb.table('user')
                     .then(user => {
                         user.add({
                             name'luke',
                             age22
                         }); // user is undefined!
                     });
             }
          });

          你會(huì)發(fā)現(xiàn),如果這個(gè)組件在 App 初始化時(shí)就被加載,在組件 mounted 函數(shù)觸發(fā)時(shí),本地?cái)?shù)據(jù)庫(kù)可能根本就沒(méi)有連接上!(連接數(shù)據(jù)庫(kù)這樣的操作,最典型的執(zhí)行場(chǎng)景就是在組件加載時(shí))

          解決辦法是,在每一個(gè)需要操作數(shù)據(jù)庫(kù)的地方,都定義一個(gè)連接:

          import GoDB from 'godb';

          new Vue({
              mounted() {
                  GoDB.open('testDB')
                    .then(testDB => testDB.table('user'))
                    .then(user => {
                        user.add({
                            name'luke',
                            age22
                        });
                    });
              }
          });

          這樣不僅代碼又臭又長(zhǎng),性能低下(每次操作都需要先連接),在需要連接本地?cái)?shù)據(jù)庫(kù)的組件多了后,維護(hù)起來(lái)更是一場(chǎng)噩夢(mèng)

          簡(jiǎn)而言之,就是這個(gè)方案,在工程化前端的不同組件中,需要在每次操作之前,都連一遍數(shù)據(jù)庫(kù),否則無(wú)法確保組件加載時(shí),已經(jīng)連接上了 IndexedDB

          設(shè)計(jì)二:隱藏連接的異步特性

          我最終采用了這個(gè)方案,對(duì)開(kāi)發(fā)者而言,甚至感覺(jué)不到「連接數(shù)據(jù)庫(kù)」和「連接表」這兩個(gè)操作是異步的

          const testDB = new GoDB('testDB');
          const user = testDB.table('user');

          user.add({
              name'luke',
              age22
          })
            .then(luke => console.log(luke));

          這樣使用上非常自然,開(kāi)發(fā)者并不需要關(guān)心操作時(shí)有沒(méi)有連上數(shù)據(jù)庫(kù)和表,只需要在操作后的回調(diào)內(nèi)寫好自己的邏輯就可以

          但是,這個(gè)方案的缺點(diǎn)就是開(kāi)發(fā)起來(lái)比較麻煩(嘿嘿,麻煩自己,方便用戶)

          因?yàn)?nbsp;new Codb('testDB') 內(nèi)部的連接數(shù)據(jù)庫(kù)的操作,實(shí)際上是異步的(因?yàn)?IndexedDB 的原生 API 就是異步的設(shè)計(jì))

          在連接數(shù)據(jù)庫(kù)的操作發(fā)出去后,即使還沒(méi)連接上,下面的 testDB.table('user') 和 user.add() 也會(huì)先開(kāi)始執(zhí)行

          也就是說(shuō),之后的「獲取 user 表」 和 「添加一條數(shù)據(jù)」實(shí)際上會(huì)先于「連上數(shù)據(jù)庫(kù)」這個(gè)過(guò)程執(zhí)行,如果實(shí)現(xiàn)該 API 設(shè)計(jì)時(shí)未處理這個(gè)問(wèn)題,上面的示例代碼肯定會(huì)報(bào)錯(cuò)

          而要處理這個(gè)問(wèn)題,我用到了下面兩個(gè)方法:

          • 在每次需要連上數(shù)據(jù)庫(kù)的操作中(比如 add()),先拿到數(shù)據(jù)庫(kù)的連接,再進(jìn)行操作
          • 使用隊(duì)列 Queue,在還未連接時(shí),把需要連接數(shù)據(jù)庫(kù)的操作放進(jìn)隊(duì)列,等連接完成,再執(zhí)行該隊(duì)列

          具體而言,就是

          • 在 GoDB 的 class 中定義一個(gè) getDB(callback),用來(lái)獲取 IndexedDB 連接實(shí)例
          • 增刪改查中,都調(diào)用 getDB,在 callback 獲取到 IndexedDB 的連接實(shí)例后再進(jìn)行操作
          • getDB 中使用一個(gè)隊(duì)列,如果數(shù)據(jù)庫(kù)還沒(méi)連接上,就把 callback 放進(jìn)隊(duì)列,在連接上后,執(zhí)行這個(gè)隊(duì)列中的函數(shù)
          • 連接完成時(shí),直接把 IndexedDB 連接實(shí)例傳進(jìn) callback 執(zhí)行即可

          在調(diào)用 getDB 時(shí),可能有三種狀態(tài)(其實(shí)還有個(gè)數(shù)據(jù)庫(kù)已關(guān)閉的狀態(tài),這里不討論):

          1. 剛初始化,未發(fā)起和 IndexedDB 的連接
          2. 正在連接 IndexedDB,但還未連上
          3. 已經(jīng)連上,此時(shí)已經(jīng)有 IndexedDB 的連接實(shí)例

          第一種狀態(tài)只在第一次執(zhí)行 getDB 時(shí)觸發(fā),因?yàn)橐坏﹪L試建立連接就進(jìn)入下一個(gè)狀態(tài)了;第一次執(zhí)行被我放到了 GoDB 類的構(gòu)造函數(shù)中

          第三種狀態(tài)時(shí),也就是已經(jīng)連上數(shù)據(jù)庫(kù)后,直接把連接實(shí)例傳進(jìn) callback 執(zhí)行即可

          關(guān)鍵是處理第二種狀態(tài),此時(shí)正在連接數(shù)據(jù)庫(kù),但還未連上,無(wú)法進(jìn)行增刪改查:

          const testDB = new GoDB('testDB');
          const user = testDB.table('user');

          user.add({ name'luke' }); // 此時(shí)數(shù)據(jù)庫(kù)正在連接,還未連上
          user.add({ name'elaine' }); // 此時(shí)數(shù)據(jù)庫(kù)正在連接,還未連上

          testDB.onOpened = () => { // 數(shù)據(jù)庫(kù)連接成功的回調(diào)
              user.add({ name'lucas' }); // 此時(shí)已連接
          }

          上面的例子,頭兩個(gè) add 執(zhí)行時(shí)其實(shí)數(shù)據(jù)庫(kù)并未連接上

          那要如何操作,才能保證正常添加,并且 luke 和 elaine 在 lucas 進(jìn)入數(shù)據(jù)庫(kù)的順序,和代碼順序一致呢?

          答案是使用隊(duì)列 Queue,把數(shù)據(jù)庫(kù)還未連上時(shí)的 add 操作加進(jìn)隊(duì)列,在連接成功時(shí),按先進(jìn)先出的順序執(zhí)行

          這樣,用戶就不需要關(guān)心,操作時(shí)數(shù)據(jù)庫(kù)是否已經(jīng)連上了(只需要關(guān)注異步回調(diào)即可),GoDB 幫你在幕后做好了這一切

          注意之所以使用 callback 而不是 Promise,是因?yàn)?JS 中的回調(diào)既可以是異步的,也可以是同步的

          而連接成功,已經(jīng)有連接實(shí)例后,直接同步返回連接實(shí)例更好,沒(méi)必要再使用異步

          還是以 Vue 為例,如果我們?cè)?Vuex(全局變量)中添加連接實(shí)例:

          import GoDB from 'godb';

          new Vuex.Store({
              state: {
                  godbnew GoDB('testDB')
              }
          });

          這樣,在所有組件中,我們都可以使用同一個(gè)連接實(shí)例:

          new Vue({
              computed: {
                  // 把全局實(shí)例變?yōu)榻M件屬性
                  godb() {
                      return this.$store.state.godb;
                  }
              },
              mounted() {
                  this.godb.table('user').add({
                      name'luke',
                      age22
                  })
                    .then(luke => console.log(luke));
              }
          });

          總結(jié)這個(gè)方案的優(yōu)點(diǎn):

          • 性能更高(可以全局共享一個(gè)連接實(shí)例)
          • 代碼更簡(jiǎn)潔
          • 最關(guān)鍵的,心智負(fù)擔(dān)低了很多 -- 直接操作即可,無(wú)需關(guān)注數(shù)據(jù)庫(kù)的連接狀態(tài)

          缺點(diǎn):對(duì) GoDB.js 的開(kāi)發(fā)更麻煩,不是簡(jiǎn)單把 IndexedDB 封裝一層 Promise 就行

          因此,我最終采用了這個(gè)方案,畢竟麻煩我一個(gè),方便你我他,優(yōu)點(diǎn)遠(yuǎn)遠(yuǎn)蓋過(guò)了缺點(diǎn)

          如果對(duì)實(shí)現(xiàn)好奇的話,可以去閱讀源碼,當(dāng)前只是實(shí)現(xiàn)了基本的 CRUD,源碼暫時(shí)還不復(fù)雜

          總結(jié)

          如果想了解更多,可以訪問(wèn)項(xiàng)目官網(wǎng)

          godb-js.github.io

          如果你有任何建議或意見(jiàn),請(qǐng)?jiān)谠u(píng)論區(qū)留言,我會(huì)認(rèn)證讀每一個(gè)反饋



          往期推薦
          19 個(gè)接私活平臺(tái)匯總升級(jí)版,你有技術(shù)就有錢
          水貨CTO,將熊熊一窩
          職場(chǎng)異性相處PPT!網(wǎng)友:你倒是給我分配個(gè)女同事?。?/a>
          如何使錯(cuò)誤日志更加方便排查問(wèn)題
          Spring Boot 2.x基礎(chǔ)教程:使用MongoDB

          如果你喜歡本文,歡迎關(guān)注我,訂閱更多精彩內(nèi)容
          關(guān)注我回復(fù)「加群」,加入Spring技術(shù)交流群

          點(diǎn)擊領(lǐng)?。篔ava基礎(chǔ)核心知識(shí)大總結(jié).pdf


          喜歡的這里報(bào)道

          ↘↘↘

          瀏覽 37
          點(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>
                  五月色影音先锋 | 北条麻妃高清无码视频 | 黄色电影第一页 | 欧美性爱娱乐网 | 肏屄网视频 |