IndexedDB All In One

https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API

https://developer.mozilla.org/zh-CN/docs/Web/API/IDBObjectStore

Indexed Database API 2.0

W3C Recommendation, 30 January 2018

https://www.w3.org/TR/IndexedDB/

blogs

https://javascript.ruanyifeng.com/bom/indexeddb.html#indexeddb-对象

open / create



let log = console.log;

const openRequest = window.indexedDB.open("idb", 1);
let db; openRequest.onupgradeneeded = function (e) {
log("DB Upgrading...", e);
// db = e.target.result;
} openRequest.onsuccess = function (e) {
log("Open DB Success!", e);
db = openRequest.result;
log(`DB =`, db);
}; openRequest.onerror = function (e) {
log("Open DB Error", e);
};

delete DB


let log = console.log; const DBDeleteRequest = window.indexedDB.deleteDatabase("idb"); DBDeleteRequest.onerror = function (e) {
log("Delete DB Error", e);
}; DBDeleteRequest.onsuccess = function (e) {
log("Delete DB Success", e);
};




IDBObjectStore 对象

https://wangdoc.com/javascript/bom/indexeddb.html#idbobjectstore-对象


// DBOpenRequest. // IDBDatabase // IDBTransaction // IDBObjectStore // DBRequest. let IDBTransaction = db.transaction(['test'], 'readonly'); let IDBObjectStore = IDBTransaction.objectStore('test'); objectStore.add(value, key)
objectStore.put(item, key) objectStore.clear()
objectStore.delete(Key) IDBObjectStore.count(key) objectStore.getKey(key)
objectStore.get(key) // 获取所有记录
objectStore.getAll() // 获取所有符合指定主键或 IDBKeyRange 的记录
objectStore.getAll(query) // 指定获取记录的数量
objectStore.getAll(query, count) // 获取所有记录的主键
objectStore.getAllKeys() // 获取所有符合条件的主键
objectStore.getAllKeys(query) // 指定获取主键的数量
objectStore.getAllKeys(query, count) objectStore.index(name) objectStore.createIndex(indexName, keyPath, objectParameters) objectStore.deleteIndex(indexName) IDBObjectStore.openCursor() IDBObjectStore.openKeyCursor() /* IDBObjectStore.indexNames:返回一个类似数组的对象(DOMStringList),包含了当前对象仓库的所有索引。 IDBObjectStore.keyPath:返回当前对象仓库的主键。 IDBObjectStore.name:返回当前对象仓库的名称。 IDBObjectStore.transaction:返回当前对象仓库所属的事务对象。 IDBObjectStore.autoIncrement:布尔值,表示主键是否会自动递增。 */

let t = db.transaction(['test'], 'readonly');
let store = t.objectStore('test'); let cursor = store.openCursor(); cursor.onsuccess = function (event) {
let res = event.target.result;
if (res) {
console.log('Key', res.key);
console.dir('Data', res.value);
res.continue();
}
}
















Web Workers

https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API

https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Functions_and_classes_available_to_workers

demo


"use strict"; /**
*
* @author xgqfrms
* @license MIT
* @copyright xgqfrms
*
* @description IndexedDB
* @augments
* @example
* @link
*
*/ // const IndexedDB = (datas = [], debug = false) => {
// let result = ``;
// // do something...
// return result;
// }; // export default IndexedDB; // export {
// IndexedDB,
// }; let log = console.log; // 主线程
const url = `https://cdn.xgqfrms.xyz/json/xgqfrms.json`;
const css = `
color: #f0f;
font-size: 23px;
`;
let worker = new Worker("./idb-worker.js");
// 发送数据
worker.postMessage(url);
// 接收数据
worker.onmessage = function(e) {
log(`\n%c worker 线程 e = `, css, e);
let json = e.data || {};
log(`%c worker 线程 data = `, css, JSON.stringify(json, null, 4));
} setTimeout(() => {
// 完成任务后, 关掉 Worker
worker.terminate();
}, 1000 * 100);

"use strict"; /**
*
* @author xgqfrms
* @license MIT
* @copyright xgqfrms
*
* @description IndexedDBWorker
* @augments
* @example
* @link
*
*/ let log = console.log; log(`hello world!`); // worker 线程 const css = `
color: #0ff;
font-size: 23px;
`; const css_fd = `
color: #0f0;
font-size: 23px;
`; // callback function
onmessage = function (e){
console.log(`\n%c 主线程 e = `, css, e);
// evt.data 接收数据
let url = e.data;
// fetch data
const datas = [];
// decodeURI()
console.log(`%c 主线程 url = `, css, `"${url}"`);
fetch(url)
.then(res => res.json())
.then(
(json) => {
// console.log(`fetched json = \n`, JSON.stringify(json, null, 4));
let userInfos = json.user;
console.log(`%c userInfos = `, css_fd, JSON.stringify(userInfos, null, 4));
datas.push(userInfos);
postMessage(userInfos);
// 发送数据
}
)
.catch(err => log(`fetch error = \n`, err));
} // const IndexedDBWorker = (datas = [], debug = false) => {
// let result = ``;
// // do something...
// return result;
// }; // export default IndexedDBWorker; // export {
// IndexedDBWorker,
// };

self & this


"use strict"; /**
*
* @author xgqfrms
* @license MIT
* @copyright xgqfrms
*
* @description IndexedDBWorker
* @augments
* @example
* @link
*
*/ let log = console.log; // worker 线程 const css = `
color: #0ff;
font-size: 23px;
`; const css_fd = `
color: #0f0;
font-size: 23px;
`; // self 代表子线程自身,即子线程的全局对象 log(`worker self =`, self);
// DedicatedWorkerGlobalScope let name = self.name;
log(`worker name =`, name); // BAD
// self.addEventListener(`meassge`, function (e) {
// log(`self.addEventListener("meassge", function (e) {});`);
// log(`\n%c 主线程 e = `, css, e);
// // evt.data 接收数据
// let url = e.data;
// log(`%c 主线程 url = `, css, `"${url}"`);
// }); // BAD
// self.addEventListener(`meassge`, (e) => {
// log(`self.addEventListener("meassge", (e) => {});`);
// log(`\n%c 主线程 e = `, css, e);
// // evt.data 接收数据
// let url = e.data;
// log(`%c 主线程 url = `, css, `"${url}"`);
// }); // 写法一 OK
// this.addEventListener("message", function (e) {
// log(`this.addEventListener("message", function (e) {});`);
// log(`\n%c 主线程 e = `, css, e);
// // evt.data 接收数据
// let url = e.data;
// log(`%c 主线程 url = `, css, `"${url}"`);
// }, false); // 写法二 OK
// addEventListener("message", function (e) {
// log(`addEventListener("message", function (e) {});`);
// log(`\n%c 主线程 e = `, css, e);
// // evt.data 接收数据
// let url = e.data;
// log(`%c 主线程 url = `, css, `"${url}"`);
// }, false); // OK
self.onmessage = function (e){
log(`self.onmessage = function (e) {});`);
log(`\n%c 主线程 e = `, css, e);
// evt.data 接收数据
let url = e.data;
log(`%c 主线程 url = `, css, `"${url}"`);
}; // OK
// onmessage = function (e){
// log(`onmessage = function (e) {});`);
// log(`\n%c 主线程 e = `, css, e);
// // evt.data 接收数据
// let url = e.data;
// log(`%c 主线程 url = `, css, `"${url}"`);
// }; // Worker 线程
// self.close(); // callback function
// onmessage = function (e){
// console.log(`\n%c 主线程 e = `, css, e);
// // evt.data 接收数据
// let url = e.data;
// // fetch data
// const datas = [];
// // decodeURI()
// console.log(`%c 主线程 url = `, css, `"${url}"`);
// fetch(url)
// .then(res => res.json())
// .then(
// (json) => {
// // console.log(`fetched json = \n`, JSON.stringify(json, null, 4));
// let userInfos = json.user;
// console.log(`%c userInfos = `, css_fd, JSON.stringify(userInfos, null, 4));
// datas.push(userInfos);
// postMessage(userInfos);
// // 发送数据
// }
// )
// .catch(err => log(`fetch error = \n`, err));
// }; // onmessage = (e) => {
// let log = console.log;
// log(`e =`, e); // // // TODO ??? WebSockets header & token
// const url = `ws://10.1.64.142:8080/chat/?token=00`;
// // const url = `ws://10.1.64.142:8080/chat/?token=03977977-6ac7-4336-b799-90358d351b5c`; // let ws = new WebSocket(url); // ws.onopen = function(e) {
// log(`已经建立连接 open`, ws.readyState);
// log(`e = `, e);
// subscribe();
// }; // ws.onerror = function(e) {
// log(`连接异常 error`, ws.readyState);
// log(`e = `, e);
// }; // ws.onmessage = function(res) {
// log(`A 收到消息 message`, ws.readyState);
// let data = res.data;
// let origin = res.origin;
// // log(`res & e = `, res);
// data = JSON.parse(data);
// // log(`typeof(data) = `, typeof(data));
// log(`res.data = `, JSON.stringify(data, null, 4));
// log(`res.origin = `, origin);
// // postMessage(data);
// }; // ws.onclose = function(e) {
// log(`已经关闭连接 close`, ws.readyState);
// log(`e = `, e);
// }; // const subscribe = () => {
// let msg = {
// Action: "4",
// SendUserID: "6845488",
// ReciveUserID: "6845422",
// SerialNumber: "消息序列",
// LastReciveMsgId: null,
// LastReciveMsgIds: [{
// K: "6845422",
// V: null,
// }],// 最后一条消息
// AllUser: true, // 所有用户, boolean
// };
// log(`A 订阅所有用户消息`);
// let str_msg = JSON.stringify(msg);
// ws.send(str_msg);
// }; // let flag = setInterval(() => {
// let uid = new Date().getTime();
// // let msg = {
// // Action: "1",
// // SendUserID: "6845488",
// // ReciveUserID: "6845422",
// // SerialNumber: "消息序列",
// // Info: "消息内容 A",
// // MsgID: uid,
// // MsgType: "1", // 消息类型(1-文本、2-图片、3-表情、4-视频、5-音频)
// // };
// let msg = {
// Action: "1",
// SendUserID: "6845488",
// ReciveUserID: "6845422",
// SerialNumber: `A 消息序列 ${uid}`,
// Info: "消息内容 A",
// MsgID: uid,
// MsgType: 1,
// };
// log(`A 发送消息:`, JSON.stringify(msg, null, 4));
// let str_msg = JSON.stringify(msg);
// ws.send(str_msg);
// postMessage(msg);
// }, 3000); // setTimeout(() => {
// clearInterval(flag);
// }, 60 * 1000);
// }; // const IndexedDBWorker = (datas = [], debug = false) => {
// let result = ``;
// // do something...
// return result;
// }; // export default IndexedDBWorker; // export {
// IndexedDBWorker,
// };

bug

Failed to execute 'importScripts' on 'WorkerGlobalScope':


"use strict"; /**
*
* @author xgqfrms
* @license MIT
* @copyright xgqfrms
*
* @description IndexedDBWorker
* @augments
* @example
* @link
*
*/ let log = console.log; // worker 线程
let name = self.name;
log(`worker name =`, name); // importScripts("./set-red-point.js");
// self.importScripts("./set-red-point.js");
// self.importScripts("set-red-point.js"); onmessage = (e) => {
let log = console.log;
const css = `
color: #0ff;
font-size: 23px;
`; const css_fd = `
color: #0f0;
font-size: 23px;
`;
console.log(`\n%c 主线程 e = `, css, e);
let URI = e.data;
// fetch data
const datas = [];
// decodeURI()
// console.log(`%c 主线程 url = `, css, `"${URI}"`);
log(`e =`, e);
// TODO ??? WebSockets header & token
// const url = `${URI}/?token=00`;
const url = `ws://10.1.64.142:8080/chat/?token=00`;
console.log(`ws url = `, url);
// const url = `ws://10.1.64.142:8080/chat/?token=03977977-6ac7-4336-b799-90358d351b5c`;
let ws = new WebSocket(url);
// window.WS = ws;
// window.ws = ws;
// let ws = window.WS;
// let ws = Vue.prototype.$WS;
// log(`WS =`, window.WS); ws.onopen = function(e) {
log(`已经建立连接 open`, ws.readyState);
log(`e = `, e);
fetchList();
IndexDBDemo();
// subscribe();
// autoSendText();
}; ws.onerror = function(e) {
log(`onerror ws.readyState`, ws.readyState);
log(`连接异常 error`, ws.readyState);
log(`e = `, e);
}; ws.onmessage = function(res) {
log(`onmessage ws.readyState`, ws.readyState);
log(`A 收到消息 message`);
let data = res.data;
let origin = res.origin;
// log(`res & e = `, res);
data = JSON.parse(data);
// log(`typeof(data) = `, typeof(data));
log(`res.data = `, JSON.stringify(data, null, 4));
log(`res.origin = `, origin);
// postMessage(data);
}; ws.onclose = function(e) {
log(`onclose ws.readyState`, ws.readyState);
log(`已经关闭连接 close`, ws.readyState);
log(`e = `, e);
}; const fetchList = () => {
let msg = {
Action: "12",
};
log(`A 消息列表1`);
let str_msg = JSON.stringify(msg);
log(`A 消息列表`, str_msg);
ws.send(str_msg);
}; const subscribe = () => {
let msg = {
Action: "4",
SendUserID: "6845488",
ReciveUserID: "6845422",
SerialNumber: "消息序列",
LastReciveMsgId: null,
LastReciveMsgIds: [{
K: "6845422",
V: null,
}],// 最后一条消息
AllUser: true, // 所有用户, boolean
};
log(`A 订阅所有用户消息`);
let str_msg = JSON.stringify(msg);
ws.send(str_msg);
}; const autoSendText = () => {
let flag = setInterval(() => {
let uid = new Date().getTime();
// let msg = {
// Action: "1",
// SendUserID: "6845488",
// ReciveUserID: "6845422",
// SerialNumber: "消息序列",
// Info: "消息内容 A",
// MsgID: uid,
// MsgType: "1", // 消息类型(1-文本、2-图片、3-表情、4-视频、5-音频)
// };
let msg = {
Action: "1",
SendUserID: "6845488",
ReciveUserID: "6845422",
SerialNumber: `A 消息序列 ${uid}`,
Info: "消息内容 A",
MsgID: uid,
MsgType: 1,
};
log(`A 发送消息:`, JSON.stringify(msg, null, 4));
let str_msg = JSON.stringify(msg);
ws.send(str_msg);
postMessage(msg);
}, 3000); setTimeout(() => {
clearInterval(flag);
}, 60 * 1000);
};
}; const IndexDBDemo = ( debug = false) => {
log(`indexeddb created`)
// let indexedDB = window.indexedDB;
let dbVersion = 1;
// step 01, open
let idb_connect = indexedDB.open("test_db", dbVersion);
// IDBOpenDBRequest & {onblocked: null, onupgradeneeded: null, source: null, transaction: null, readyState: "pending", …} // idb_connect.onsuccess = function(e) {
// let log = console.log;
// let db = idb_connect.result;
// setTimeout(() => {
// // index & looks up
// let tx = db.transaction("books", "readonly");
// let opened_store = tx.objectStore("books");
// let index = opened_store.index("by_title");
// // query
// let request = index.get("Bedrock Nights");
// request.onsuccess = function() {
// let matching = request.result;
// if (matching !== undefined) {
// let {
// isbn,
// title,
// author,
// } = matching;
// log(`A match was found.`, isbn, title, author);
// } else {
// log(`No match was found.`);
// }
// };
// }, 3000);
// };
idb_connect.onsuccess = function(e) {
let readyState = idb_connect.readyState;
console.log("Success creating/accessing IndexedDB database", readyState);
let idb = idb_connect.result;
// step 02, idb
idb.onerror = function(e) {
let name = idb.name;
let version = idb.version;
let objectStoreNames = idb.objectStoreNames;
let length = objectStoreNames.length;
let error = idb.error;
console.log("Error creating/accessing IndexedDB", error);
};
idb.onabort = function(e) {
let name = idb.name;
let version = idb.version;
let objectStoreNames = idb.objectStoreNames;
let length = objectStoreNames.length;
console.log("onabort & IndexedDB", name, version);
};
idb.onversionchange = function(e) {
let name = idb.name;
let version = idb.version;
let objectStoreNames = idb.objectStoreNames;
let length = objectStoreNames.length;
console.log("onversionchange & IndexedDB", name, version);
};
idb.onclose = function(e) {
let name = idb.name;
let version = idb.version;
let objectStoreNames = idb.objectStoreNames;
let length = objectStoreNames.length;
console.log("onclose & IndexedDB", name, version);
};
}; idb_connect.onerror = function(e) {
let error = idb_connect.error;
console.log("Error creating/accessing IndexedDB database", error);
}; idb_connect.onblocked = function(e) {
let source = idb_connect.source;
console.log("onblocked & IndexedDB database", source, transaction);
}; // idb_connect.onupgradeneeded = function(e) {
// let source = idb_connect.source;
// let transaction = idb_connect.transaction;
// console.log("onupgradeneeded & IndexedDB database", source, transaction);
// };
// connect upgradeneeded
idb_connect.onupgradeneeded = function(e) {
let {
// name,
// version,
// objectStoreNames,
// error,
source,
transaction,
} = idb_connect;
// let source = idb_connect.source;
// let transaction = idb_connect.transaction;
log("onupgradeneeded & IndexedDB database", source, transaction);
// The database did not previously exist, so create object stores and indexes.
let db = idb_connect.result;
let store = db.createObjectStore("books", {keyPath: "isbn"});
// create index
let titleIndex = store.createIndex("by_title", "title", {unique: true});
let authorIndex = store.createIndex("by_author", "author");
// Populate with initial data.
store.put({
title: "Quarry Memories",
author: "Fred",
isbn: 123456,
});
store.put({
title: "Water Buffaloes",
author: "Fred",
isbn: 234567,
});
store.put({
title: "Bedrock Nights",
author: "Barney",
isbn: 345678,
});
setTimeout(() => {
// transaction & update
let tx = db.transaction("books", "readwrite");
let opened_store = tx.objectStore("books");
opened_store.put({
title: "Quarry Memories",
author: "Fred",
isbn: 123456,
});
opened_store.put({
title: "Water Buffaloes",
author: "Fred",
isbn: 234567,
});
opened_store.put({
title: "Bedrock Nights",
author: "Barney",
isbn: 345678,
});
tx.oncomplete = function() {
log(`All requests have succeeded and the transaction has committed.`);
};
}, 3000);
setTimeout(() => {
// index & looks up
let tx = db.transaction("books", "readonly");
let opened_store = tx.objectStore("books");
let index = opened_store.index("by_title");
// query
let request = index.get("Bedrock Nights");
request.onsuccess = function() {
let matching = request.result;
if (matching !== undefined) {
let {
isbn,
title,
author,
} = matching;
log(`A match was found.`, isbn, title, author);
} else {
log(`No match was found.`);
}
};
}, 3000);
};
return idb_connect;
}; // importScripts("./set-red-point.js");
// self.importScripts("./set-red-point.js"); // Worker 线程
// setTimeout(() => {
// log(`\n%c 关掉 worker 线程`, css);
// // 完成任务后, 关掉 Worker
// // worker.terminate();
// self.close();
// }, 1000 * 10); // const IndexedDBWorker = (datas = [], debug = false) => {
// let result = ``;
// // do something...
// return result;
// }; // export default IndexedDBWorker; // export {
// IndexedDBWorker,
// };

refs



xgqfrms 2012-2020

www.cnblogs.com 发布文章使用:只允许注册用户才可以访问!


IndexedDB All In One的更多相关文章

  1. IndexedDB(本地存储)

    var students = [{ id: 1001, name: "Byron", age: 24 }, { id: 1002, name: "Frank", ...

  2. HTML5存储之 indexedDB

    IndexeDB是HTML5 重要的一部分,它是一种轻量级的NOSQL数据库.对创建具有丰富本地存储数据的数据密集型的离线HTML5 Web 应用程序很有用. IndexedDB是为了能够在客户端存储 ...

  3. Notes:indexedDB使用

    indexedDB是浏览器端保存结构化数据的一种数据库,类似于mysql,oracle等数据库,但indexedDB使用对象存储数据,而不是用表. indexedDB是全局的宿主对象,使用window ...

  4. HTML5本地存储——IndexedDB(一:基本使用)

    在HTML5本地存储——Web SQL Database提到过Web SQL Database实际上已经被废弃,而HTML5的支持的本地存储实际上变成了 Web Storage(Local Stora ...

  5. IndexedDB参考资料网址

    IndexedDB:浏览器里内置的数据库, Web骇客 http://www.webhek.com/indexeddb/ 前端的数据库:IndexedDB入门(很全面) http://web.jobb ...

  6. HTML5本地存储——IndexedDB(二:索引)

    在HTML5本地存储——IndexedDB(一:基本使用)中介绍了关于IndexedDB的基本使用方法,很不过瘾,这篇我们来看看indexedDB的杀器——索引. 熟悉数据库的同学都知道索引的一个好处 ...

  7. js IndexedDB:浏览器端数据库的demo实例

    IndexedDB具有以下特点. (1)键值对储存. IndexedDB内部采用对象仓库(object store)存放数据.所有类型的数据都可以直接存入,包括JavaScript对象.在对象仓库中, ...

  8. Web数据持久化存储IndexedDB(不常用)

    IndexedDB是在浏览器中保存结构化数据的一种数据库,为了替换WebSQL(标准已废弃,但被广泛支持)而出现.IndexedDB使用NoSQL的形式来操作数据库,保存和读取是JavaScript对 ...

  9. [转]使用 HTML5 IndexedDB API

    本地数据持久性提高了 Web 应用程序可访问性和移动应用程序响应能力 索引数据库 (IndexedDB) API(作为 HTML5 的一部分)对创建具有丰富本地存储数据的数据密集型的离线 HTML5 ...

  10. HTML5 indexedDB数据库的入门学习(二)

    上一篇关于indexedDB的学习笔记主要写了indexedDB数据库的基本操作—增删改查:但是为什么我们要用indexedDB呢?为什么indexedDB受到了开发者们的青睐呢?最主要的就是inde ...

随机推荐

  1. http2 http1 对比

    RFC 7540 - Hypertext Transfer Protocol Version 2 (HTTP/2) https://tools.ietf.org/html/rfc7540#page-4 ...

  2. TypeScript基本类型

    类型注解 作用:相当于强类型语言中的类型声明 语法:(变量/函数):type 数据类型 新建src/datatype.ts,里面定义各种类型的数据 原始类型: let bool: boolean = ...

  3. JAD 反编译

    自动拆装箱 对于基本类型和包装类型之间的转换,通过xxxValue()和valueOf()两个方法完成自动拆装箱,使用jad进行反编译可以看到该过程: public class Demo { publ ...

  4. 通过动态构建Expression Select表达式并创建动态类型来控制Property可见性

    通过动态构建Expression Select表达式并创建动态类型来控制Property可见性 项目中经常遇到的一个场景,根据当前登录用户权限,仅返回权限内可见的内容.参考了很多开源框架,更多的是在V ...

  5. 一:优化Docker中的Spring Boot应用:单层镜像方法

    优化Docker中的Spring Boot应用:单层镜像方法 1.Docker关键概念 2.镜像层内容很重要 3.镜像层影响部署 4.Docker中的Spring Boot应用 5.单层方法 5.1 ...

  6. python----类,面向对象(封装、继承、多态)(属性,方法)

    什么是对象? 对象是内存中专门用来存储数据的一块区域 对象中可以存放各种数据(数字.代码等) 对象由三部分组成(1,对象标识(id)2,对象类型(type)3,对象的值(value)) 面向对象编程是 ...

  7. JavaWeb-tomcat安装(Unsupported major.minor version 51.0/startup.bat闪退)

    JavaWeb-tomcat安装(Unsupported major.minor version 51.0) 一 启动startup.bat 出错i 今天安装tomcat出错,折腾了一下午,收获了许多 ...

  8. redis分布式锁的这些坑,我怀疑你是假的开发

    摘要:用锁遇到过哪些问题? 一.白话分布式 什么是分布式,用最简单的话来说,就是为了较低单个服务器的压力,将功能分布在不同的机器上面:就比如: 本来一个程序员可以完成一个项目:需求->设计-&g ...

  9. 力扣643.子数组最大平均数I-C语言实现

    题目 给定 n 个整数,找出平均数最大且长度为 k 的连续子数组,并输出该最大平均数. 示例: 输入:[1,12,-5,-6,50,3], k = 4 输出:12.75 解释:最大平均数 (12-5- ...

  10. 初窥 Python 的 import 机制

    本文适合有 Python 基础的小伙伴进阶学习 作者:pwwang 一.前言 本文基于开源项目: https://github.com/pwwang/python-import-system 补充扩展 ...