V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX  ›  hamsterbase  ›  全部回复第 6 页 / 共 18 页
回复总数  348
1 ... 2  3  4  5  6  7  8  9  10  11 ... 18  
12. 经过 2 天 6 小时的开发。 一个本地优先的极简卡片笔记就开发完了。(只有最核心功能)

可以访问 https://memo-talk-web.onrender.com 体验

注意: 项目还没正式发布,只能拿来玩。,不要在这里储存任何数据。

1. 纯 web 实现, 压缩后不到 173 k 。
2. 支持离线编辑,再次联网后自动同步。
3. 两台设备输入相同的账户密码。 刷新页面刷新后,他们会自动同步到同一个状态。
4. 所有数据在本地加密,服务器不储存明文数据。
11. 服务器问题修复后,开始对接。 核心的云同步逻辑非常简单


const sdk = new FileAPISDK(url);

// 生成加密密钥
// userToken 用来标记用户身份,也是服务器文件夹的名字
// encryptionKey 是文件加密密码,encryptionKey 不会发送到服务
const { userToken, encryptionKey } = generateKeys(username, passowrd);

// 获取服务器数据库列表
const files: string[] = await sdk.getList(userToken);

const mergedFiles: string[] = [];

// 把服务器上的文件和本地合并,合并后加入等待删除列表
for (const file of files) {
try {
const remoteData = await sdk.getFile(userToken, file);
const database = decryptData(remoteData, encryptionKey);
memoTalkCore.merge(database);
mergedFiles.push(file);
} catch (error) {
console.log(error);
}
}

// 获取本地最新数据,加密
const currentData = encryptData(memoTalkCore.encode(), encryptionKey);
const fileName = sha256(currentData);

// 把加密后的文档存到服务器
await sdk.createFile(userToken, fileName, currentData);

// 因为数据已经合并到本地了,所以把服务器的删掉
for (const mergedFile of mergedFiles) {
try {
await sdk.deleteFile(userToken, mergedFile);
} catch (error) {
console.log(error);
}
}
}
14. 发现服务器忘记配置跨域了。

q:koajs 如何开启跨区请求。
```
class FileAPISDK {
constructor(baseUrl = "http://localhost:9999") {
this.baseUrl = baseUrl;
}

async getList(folder) {
const response = await fetch(`${this.baseUrl}/public/api/v1/folder`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ folder }),
});

this._handleErrors(response);
return await response.json();
}

async getFile(folder, file) {
const response = await fetch(`${this.baseUrl}/public/api/v1/file`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ folder, file }),
});

this._handleErrors(response);
return await response.text();
}

async deleteFile(folder, file) {
const response = await fetch(`${this.baseUrl}/public/api/v1/file`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ folder, file }),
});

this._handleErrors(response);
return await response.text();
}

async createFile(folder, file, content) {
const response = await fetch(`${this.baseUrl}/public/api/v1/file`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ folder, file, content }),
});

this._handleErrors(response);
return await response.text();
}

_handleErrors(response) {
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}`);
}
}
}
```
13: POE 一天一次的 gpt4 体验卡

帮我基于下面的代码,写一个 js sdk 出来。


https://github.com/hamsterbase/memo-talk/blob/main/server/index.ts
12:帮我写一个 sha256 的函数。 基于 ts . crypto-js. 输入字符串,输出 hex
11: 帮上面的 encryptedData ,decryptedData 写测试。 随机的 key 和 data 。 加密和解密后数据不变
10. 帮我编写一个 aes 加密函数. 输入 key 和 data, 返回加密后的数据. 输入 data 和 key , 返回解密后的数据

基于 crypto-js 和 ts
9 。 开始编写服务器需要的一些工具函数


Q:
帮我用 crypto-js 编写函数。

输入账户,密码。 生成两个 key

第一个 key sha256(memo:sha256(username):sha256(password):talk);
第二 key 。sha256(memo:password:talk

用 ts 编写

第一个 key 名字为 userToken , 第二个 key 名字为 加密密钥(用英文)
8. 简单更新了一下首页的 UI , 然后把储存从 localstorage 换到了 IndexedDB
开始真正对接服务器了。

第一版做的简单一点,在页面加载的时候异步合并一下服务器的数据,合并后把数据上传到服务器。
7. 因为是极简的稍后读工具,所以在不校验账户密码。 只需要确保账户密码非空就行了

简单完成了账户密码的保存功能。 未来会加上警告,用户协议。 目前暂时先不加.

因为 antdmobile 是在 2019 年之后出的,chatgpt 估计不认识,就手写了

https://github.com/hamsterbase/memo-talk-web/commit/c5559845d27cdeba278c9969330c474e26db9ac6
6. 使用 vite 的多页面模式开发。 初始化一下 settings 页面。 配置一下 vite 的构建配置


https://github.com/hamsterbase/memo-talk-web/commit/d7024d70c6c63cca47aa550175504aa95e22f3eb
5. 逻辑和测试都让 chatgpt 写好了, 看了一下测试似乎没问题

他给的是在 IndexedDBSettingService 构造函数里调用 openDB, 我改成了每次 set , get 的时候初始化,似乎没问题。

https://github.com/hamsterbase/memo-talk-web/commit/31cc5a97a31d69f015f6ecfd089fb3147c1db7dd
4. 本来准备在 vite 里跑 mocha ,试了一下发现不太行,准备换一条路。 用假的 indexedDB 测试。 测试继续在 vitest 里跑。
3. 对 chatgpt 写的代码不太放心,准备写一个单元测试

Q: 帮我用 mocha 写单元测试

```ts
export interface ISettingService {
get<V extends SettingsValue>(key: string, defaultValue: V): Promise<V>;

set(key: string, value: SettingsValue): Promise<void>;

readConfig<T extends Record<string, SettingsValue>>(
defaultValue: T
): Promise<T>;
}
```
好的,我可以帮您实现一个基于 IndexedDB 的 ISettingService 。以下是实现代码示例:

typescript
Copy
export interface ConfigDatabase {
settings: Record<string, SettingsValue>;
}

export class IndexedDBSettingService implements ISettingService {
private readonly dbName: string = "my-settings-db";
private readonly dbVersion: number = 1;
private readonly storeName: string = "settings";
private db: IDBDatabase;

constructor() {
this.openDB();
}

private async openDB(): Promise<void> {
return new Promise<void>((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.dbVersion);

request.onerror = (event) => {
console.error("Error opening database", event);
reject(event);
};

request.onsuccess = (event) => {
this.db = request.result;
resolve();
};

request.onupgradeneeded = (event) => {
const db = request.result;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName);
}
};
});
}

public async get<V extends SettingsValue>(
key: string,
defaultValue: V
): Promise<V> {
return new Promise<V>((resolve, reject) => {
const transaction = this.db.transaction(this.storeName, "readonly");
const objectStore = transaction.objectStore(this.storeName);

const request = objectStore.get(key);

request.onerror = (event) => {
console.error(`Error getting value for key "${key}"`, event);
reject(event);
};

request.onsuccess = (event) => {
const value = request.result !== undefined ? request.result : defaultValue;
resolve(value);
};
});
}

public async set(key: string, value: SettingsValue): Promise<void> {
return new Promise<void>((resolve, reject) => {
const transaction = this.db.transaction(this.storeName, "readwrite");
const objectStore = transaction.objectStore(this.storeName);

const request = objectStore.put(value, key);

request.onerror = (event) => {
console.error(`Error setting value for key "${key}"`, event);
reject(event);
};

request.onsuccess = (event) => {
resolve();
};
});
}

public async readConfig<T extends Record<StorageKeys, SettingsValue>>(
defaultValue: T
): Promise<T> {
const config: T = {} as T;

// Read all values from the database and merge with default values
for (const key in defaultValue) {
if (defaultValue.hasOwnProperty(key)) {
const value = await this.get(key, defaultValue[key]);
config[key as keyof T] = value as T[keyof T];
}
}

return config;
}

public async clearAll(): Promise<void> {
return new Promise<void>((resolve, reject) => {
const transaction = this.db.transaction(this.storeName, "readwrite");
const objectStore = transaction.objectStore(this.storeName);

const request = objectStore.clear();

request.onerror = (event) => {
console.error("Error clearing settings", event);
reject(event);
};

request.onsuccess = (event) => {
resolve();
};
});
}
}
在这个实现中,我们创建了一个名为 my-settings-db 的 IndexedDB 数据库,并创建了一个名为 settings 的对象存储。在 get 和 set 方法中,我们分别使用事务来读取和写入数据。在 readConfig 方法中,我们遍历默认配置对象中的所有键,并使用 get 方法来获取每个键的值。最后,我们还为 IndexedDBSettingService 类添加了一个清除所有设置的 clearAll 方法。

请注意,由于 IndexedDB 是异步的,因此所有方法都返回一个 Promise 。此外,如果 IndexedDB 连接失败或出现错误,这些方法将会拒绝 Promise 并输出错误信息。
2. localstorage 存储量有限,准备基于 IndexedDB 实现本地存储


Q: 帮我基于 IndexedDB 实现一个 ISettingService 吧,我要把所有的配置都存在 IndexedDB 里面。

export type SettingsValue = string | boolean;

export enum StorageKeys {
backend = "backend",
"backend.hamsterbase.entrypoint" = "backend.hamsterbase.entrypoint",
"backend.hamsterbase.token" = "backend.hamsterbase.token",
"backend.notion.token" = "backend.notion.token",
"backend.notion.databaseId" = "backend.notion.databaseId",
"backend.notion.license" = "backend.notion.license",
autoOn = "autoOn",
autoOnBlockList = "autoOnBlockList",
}

export interface ISettingService {
_serviceBrand: undefined;

get<V extends SettingsValue>(key: string, defaultValue: V): Promise<V>;

set(key: string, value: SettingsValue): Promise<void>;

readConfig<T extends Record<StorageKeys, SettingsValue>>(
defaultValue: T
): Promise<T>;
}
1. 今天的目标: 发布 1.0 !!!

1. 接入 antd mobile ,重写 UI
2. 接入后端,实现登录功能.
1. 服务器不储存任何账户密码。只储存用户的 token ,token 由前端生成。
2. 把 token 作为文件夹名
3. token 的生成规则:sha256( `${sha256(user)} ${sha256(password)}`)
4. 文件储存之前使用 aes 加密。 加密 为 sha256(password)
3. 接入 PWA , 让软件可离线使用
@HulkLee 开始开始。~~~
2023-06-09 19:41:18 +08:00
回复了 mengyanshou 创建的主题 分享创造 完全本地运行的 VS Code For Android(Code Server),使用 Flutter
太厉害了。 我也准备用类似方案跑我的项目,在 react native 里跑 nodejs 。实现一套代码支持全部平台
1 ... 2  3  4  5  6  7  8  9  10  11 ... 18  
关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   701 人在线   最高记录 6679   ·     Select Language
创意工作者们的社区
World is powered by solitude
VERSION: 3.9.8.5 · 17ms · UTC 22:55 · PVG 06:55 · LAX 14:55 · JFK 17:55
Developed with CodeLauncher
♥ Do have faith in what you're doing.