https://zhuanlan.zhihu.com/p/87424183推荐读一下这篇文章。 讲 vs code 和 Basecamp 是怎么保障发布的。
下面是我拷贝过来的原文
直到最近我读了 Basecamp 的产品方法论 Shape Up: Stop Running in Circles and Ship Work that Matters 。这份文档介绍了 Basecamp 是如何保证,一个想法通过层层把关,在一个自由的工作环境中,最终准时变成产品发布出去的。
## 发布
重要的事情最先说,整个文档的核心目标就是发布。这些年大环境的熏陶下,大家都明白了 “Ship it”,重要的是把产品发布出来,因为一个产品没有问世见到客户,一切都是空谈。“Ship it” 很难,努努力总是可以达到的,没达到的团队就死了。
相比之下,更难的则是产品发布后,持续的发布迭代。为了确保每个 release 计划的新功能都能够发布出去,Basecamp 的第一个准则就是小步快跑,六个礼拜一个 release ,把 deadline 作为严格标准,其他事情都得为 deadline 做让步。
六个礼拜的时间是 Basecamp 实践后找到的最合适他们的产品发布周期。VS Code 也差不多,通常一个 release 持续四个礼拜或者五个礼拜。
我深有体会。 分享一下我的做法。
1. 先写好需求文档, 写完需求文档以后。
2. 严格按照需求文档开发,只能修 bug ,砍需求 。 不能加需求
3. 实现好一会自己测试一下, 把 bug 记录
4. 把 bug 都需求,不要加需求。
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}`);
}
}
}
```
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
开始真正对接服务器了。
第一版做的简单一点,在页面加载的时候异步合并一下服务器的数据,合并后把数据上传到服务器。
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 , 让软件可离线使用