JavaScript手寫(xiě)一個(gè)前端存儲(chǔ)工具庫(kù)
在項(xiàng)目開(kāi)發(fā)的過(guò)程中,為了減少提高性能,減少請(qǐng)求,開(kāi)發(fā)者往往需要將一些不易改變的數(shù)據(jù)放入本地緩存中。如把用戶使用的模板數(shù)據(jù)放入 localStorage 或者 IndexedDB。代碼往往如下書(shū)寫(xiě)。
// 這里將數(shù)據(jù)放入內(nèi)存中
let templatesCache = null;
// 用戶id,用于多賬號(hào)系統(tǒng)
const userId: string = '1';
const getTemplates = ({
refresh = false
} = {
refresh: false
}) => {
// 不需要立即刷新,走存儲(chǔ)
if (!refresh) {
// 內(nèi)存中有數(shù)據(jù),直接使用內(nèi)存中數(shù)據(jù)
if (templatesCache) {
return Promise.resolve(templatesCache)
}
const key = `templates.${userId}`
// 從 localStorage 中獲取數(shù)據(jù)
const templateJSONStr = localStroage.getItem(key)
if (templateJSONStr) {
try {
templatesCache = JSON.parse(templateJSONStr);
return Promise.resolve(templatesCache)
} catch () {
// 解析失敗,清除 storage 中數(shù)據(jù)
localStroage.removeItem(key)
}
}
}
// 進(jìn)行服務(wù)端掉用獲取數(shù)據(jù)
return api.get('xxx').then(res => {
templatesCache = cloneDeep(res)
// 存入 本地緩存
localStroage.setItem(key, JSON.stringify(templatesCache))
return res
})
};可以看到,代碼非常冗余,同時(shí)這里的代碼還沒(méi)有處理數(shù)據(jù)版本、過(guò)期時(shí)間以及數(shù)據(jù)寫(xiě)入等功能。如果再把這些功能點(diǎn)加入,代碼將會(huì)更加復(fù)雜,不易維護(hù)。
于是個(gè)人寫(xiě)了一個(gè)小工具 storage-tools 來(lái)處理這個(gè)問(wèn)題。
使用 storage-tools 緩存數(shù)據(jù)
該庫(kù)默認(rèn)使用 localStorage 作為數(shù)據(jù)源,開(kāi)發(fā)者從庫(kù)中獲取 StorageHelper 工具類。
import { StorageHelper } from "storage-tools";
// 當(dāng)前用戶 id
const userId = "1";
// 構(gòu)建模版 store
// 構(gòu)建時(shí)候就會(huì)獲取 localStorage 中的數(shù)據(jù)放入內(nèi)存
const templatesStore = new StorageHelper({
// 多賬號(hào)用戶使用 key
storageKey: `templates.${userId}`,
// 當(dāng)前數(shù)據(jù)版本號(hào),可以從后端獲取并傳入
version: 1,
// 超時(shí)時(shí)間,單位為 秒
timeout: 60 * 60 * 24,
});
// 從內(nèi)存中獲取數(shù)據(jù)
const templates = templatesStore.getData();
// 沒(méi)有數(shù)據(jù),表明數(shù)據(jù)過(guò)期或者沒(méi)有存儲(chǔ)過(guò)
if (templates === null) {
api.get("xxx").then((val) => {
// 存儲(chǔ)數(shù)據(jù)到內(nèi)存中去,之后的 getData 都可以獲取到數(shù)據(jù)
store.setData(val);
// 閑暇時(shí)間將當(dāng)前內(nèi)存數(shù)據(jù)存儲(chǔ)到 localStorage 中
requestIdleCallback(() => {
// 期間內(nèi)可以多次掉用 setData
store.commit();
});
});
}StorageHelper 工具類支持了其他緩存源,代碼如下:
import { IndexedDBAdaptor, StorageAdaptor, StorageHelper } from "storage-tools";
// 當(dāng)前用戶 id
const userId = "1";
const sessionStorageStore = new StorageHelper({
// 配置同上
storageKey: `templates.${userId}`,
version: 1,
timeout: 60 * 60 * 24,
// 適配器,傳入 sessionStorage
adapter: sessionStorage,
});
const indexedDBStore = new StorageHelper({
storageKey: `templates.${userId}`,
version: 1,
timeout: 60 * 60 * 24,
// 適配器,傳入 IndexedDBAdaptor
adapter: new IndexedDBAdaptor({
dbName: "userInfo",
storeName: "templates",
}),
});
// IndexedDB 只能異步構(gòu)建,所以現(xiàn)在只能等待獲取構(gòu)建獲取完成
indexedDBStore.whenReady().then(() => {
// 準(zhǔn)備完成后,我們就可以 getData 和 setData 了
const data = indexedDBStore.getData();
// 其余代碼
});
// 只需要有 setItem 和 getItem 就可以構(gòu)建 adaptor
class MemoryAdaptor implements StorageAdaptor {
readonly cache = new Map();
// 獲取 map 中數(shù)據(jù)
getItem(key: string) {
return this.cache.get(key);
}
setItem(key: string, value: string) {
this.cache.set(key, value);
}
}
const memoryStore = new StorageHelper({
// 配置同上
storageKey: `templates.${userId}`,
version: 1,
timeout: 60 * 60 * 24,
// 適配器,傳入攜帶 getItem 和 setItem 對(duì)象
adapter: new MemoryAdaptor(),
});當(dāng)然了,我們還可以繼承 StorageHelper 構(gòu)建業(yè)務(wù)類。
// 也可以基于 StorageHelper 構(gòu)建業(yè)務(wù)類
class TemplatesStorage extends StorageHelper {
// 傳入 userId 以及 版本
constructor(userId: number, version: number) {
super({
storageKey: `templates.${userId}`,
// 如果需要運(yùn)行時(shí)候更新,則可以動(dòng)態(tài)傳遞
version,
timeout: 60 * 60 * 24,
});
}
// TemplatesStorage 實(shí)例
static instance: TemplatesStorage;
// 如果需要版本信息的話,
static version: number = 0;
static getStoreInstance() {
// 獲取版本信息
return getTemplatesVersion().then((newVersion) => {
// 沒(méi)有構(gòu)建實(shí)例或者版本信息不相等,直接重新構(gòu)建
if (
newVersion !== TemplatesStorage.version || !TemplatesStorage.instance
) {
TemplatesStorage.instance = new TemplatesStorage("1", newVersion);
TemplatesStorage.version = newVersion;
}
return TemplatesStorage.instance;
});
}
/**
* 獲取模板緩存和 api 請(qǐng)求結(jié)合
*/
getTemplates() {
const data = super.getData();
if (data) {
return Promise.resolve(data);
}
return api.get("xxx").then((val) => {
this.setTemplates(val);
return super.getData();
});
}
/**
* 保存數(shù)據(jù)到內(nèi)存后提交到數(shù)據(jù)源
*/
setTemplats(templates: any[]) {
super.setData(templates);
super.commit();
}
}
/**
* 獲取模版信息函數(shù)
*/
const getTemplates = () => {
return TemplatesStorage.getStoreInstance().then((instance) => {
return instance.getTemplates();
});
};針對(duì)于某些特定列表順序需求,我們還可以構(gòu)建 ListStorageHelper。
import { ListStorageHelper, MemoryAdaptor } from "../src";
// 當(dāng)前用戶 id
const userId = "1";
const store = new ListStorageHelper({
storageKey: `templates.${userId}`,
version: 1,
// 設(shè)置唯一鍵 key,默認(rèn)為 'id'
key: "searchVal",
// 列表存儲(chǔ)最大數(shù)據(jù)量,默認(rèn)為 10
maxCount: 100,
// 修改數(shù)據(jù)后是否移動(dòng)到最前面,默認(rèn)為 true
isMoveTopWhenModified: true,
// 添加數(shù)據(jù)后是否是最前面, 默認(rèn)為 true
isUnshiftWhenAdded: true,
});
store.setItem({ searchVal: "new game" });
store.getData();
// [{
// searchVal: 'new game'
// }]
store.setItem({ searchVal: "new game2" });
store.getData();
// 會(huì)插入最前面
// [{
// searchVal: 'new game2'
// }, {
// searchVal: 'new game'
// }]
store.setItem({ searchVal: "new game" });
store.getData();
// 會(huì)更新到最前面
// [{
// searchVal: 'new game'
// }, {
// searchVal: 'new game2'
// }]
// 提交到 localStorage
store.commit();storage-tools 項(xiàng)目演進(jìn)
任何項(xiàng)目都不是一觸而就的,下面是關(guān)于 storage-tools 庫(kù)的編寫(xiě)思路。希望能對(duì)大家有一些幫助。
StorageHelper 支持 localStorage 存儲(chǔ)
項(xiàng)目的第一步就是支持本地儲(chǔ)存 localStorage 的存取。
// 獲取從 1970 年 1 月 1 日 00:00:00 UTC 到用戶機(jī)器時(shí)間的秒數(shù)
// 后續(xù)有需求也會(huì)向外提供時(shí)間函數(shù)配置,可以結(jié)合 sync-time 庫(kù)一起使用
const getCurrentSecond = () => parseInt(`${new Date().getTime() / 1000}`);
// 獲取當(dāng)前空數(shù)據(jù)
const getEmptyDataStore = (version: number): DataStore<any> => {
const currentSecond = getCurrentSecond();
return {
// 當(dāng)前數(shù)據(jù)的創(chuàng)建時(shí)間
createdOn: currentSecond,
// 當(dāng)前數(shù)據(jù)的修改時(shí)間
modifiedOn: currentSecond,
// 當(dāng)前數(shù)據(jù)的版本
version,
// 數(shù)據(jù),空數(shù)據(jù)為 null
data: null,
};
};
class StorageHelper<T> {
// 存儲(chǔ)的 key
private readonly storageKey: string;
// 存儲(chǔ)的版本信息
private readonly version: number;
// 內(nèi)存中數(shù)據(jù),方便隨時(shí)讀寫(xiě)
store: DataStore<T> | null = null;
constructor({ storageKey, version }) {
this.storageKey = storageKey;
this.version = version || 1;
this.load();
}
load() {
const result: string | null = localStorage.getItem(this.storageKey);
// 初始化內(nèi)存信息數(shù)據(jù)
this.initStore(result);
}
private initStore(storeStr: string | null) {
// localStorage 沒(méi)有數(shù)據(jù),直接構(gòu)建 空數(shù)據(jù)放入 store
if (!storeStr) {
this.store = getEmptyDataStore(this.version);
return;
}
let store: DataStore<T> | null = null;
try {
// 開(kāi)始解析 json 字符串
store = JSON.parse(storeStr);
// 沒(méi)有數(shù)據(jù)或者 store 沒(méi)有 data 屬性直接構(gòu)建空數(shù)據(jù)
if (!store || !("data" in store)) {
store = getEmptyDataStore(this.version);
} else if (store.version !== this.version) {
// 版本不一致直接升級(jí)
store = this.upgrade(store);
}
} catch (_e) {
// 解析失敗了,構(gòu)建空的數(shù)據(jù)
store = getEmptyDataStore(this.version);
}
this.store = store || getEmptyDataStore(this.version);
}
setData(data: T) {
if (!this.store) {
return;
}
this.store.data = data;
}
getData(): T | null {
if (!this.store) {
return null;
}
return this.store?.data;
}
commit() {
// 獲取內(nèi)存中的 store
const store = this.store || getEmptyDataStore(this.version);
store.version = this.version;
const now = getCurrentSecond();
if (!store.createdOn) {
store.createdOn = now;
}
store.modifiedOn = now;
// 存儲(chǔ)數(shù)據(jù)到 localStorage
localStorage.setItem(this.storageKey, JSON.stringify(store));
}
/**
* 獲取內(nèi)存中 store 的信息
* 如 modifiedOn createdOn version 等信息
*/
get(key: DataStoreInfo) {
return this.store?.[key];
}
upgrade(store: DataStore<T>): DataStore<T> {
// 獲取當(dāng)前的秒數(shù)
const now = getCurrentSecond();
// 看起來(lái)很像 getEmptyDataStore 代碼,但實(shí)際上是不同的業(yè)務(wù)
// 不應(yīng)該因?yàn)榇a相似而合并,不利于后期擴(kuò)展
return {
// 只獲取之前的創(chuàng)建時(shí)間,如果沒(méi)有使用當(dāng)前的時(shí)間
createdOn: store?.createdOn || now,
modifiedOn: now,
version: this.version,
data: null,
};
}
}StorageHelper 添加超時(shí)機(jī)制
添加超時(shí)機(jī)制很簡(jiǎn)單,只需要在 getData 的時(shí)候檢查一下數(shù)據(jù)即可。
class StorageHelper<T> {
// 其他代碼 ...
// 超時(shí)時(shí)間,默認(rèn)為 -1,即不超時(shí)
private readonly timeout: number = -1;
constructor({ storageKey, version, timeout }: StorageHelperParams) {
// 傳入的數(shù)據(jù)是數(shù)字類型,且大于 0,就設(shè)定超時(shí)時(shí)間
if (typeof timeout === "number" && timeout > 0) {
this.timeout = timeout;
}
}
getData(): T | null {
if (!this.store) {
return null;
}
// 如果小于 0 就沒(méi)有超時(shí)時(shí)間,直接返回?cái)?shù)據(jù),事實(shí)上不可能小于0
if (this.timeout < 0) {
return this.store?.data;
}
// 修改時(shí)間加超時(shí)時(shí)間大于當(dāng)前時(shí)間,則表示沒(méi)有超時(shí)
// 注意,每次 commit 都會(huì)更新 modifiedOn
if (getCurrentSecond() < (this.store?.modifiedOn || 0) + this.timeout) {
return this.store?.data;
}
// 版本信息在最開(kāi)始時(shí)候處理過(guò)了,此處直接返回 null
return null;
}
}StorageHelper 添加其他存儲(chǔ)適配
此時(shí)我們可以添加其他數(shù)據(jù)源適配,方便開(kāi)發(fā)者自定義 storage。
/**
* 適配器接口,存在 getItem 以及 setItem
*/
interface StorageAdaptor {
getItem: (key: string) => string | Promise<string> | null;
setItem: (key: string, value: string) => void;
}
class StorageHelper<T> {
// 其他代碼 ...
// 非瀏覽器環(huán)境不具備 localStorage,所以不在此處直接構(gòu)造
readonly adapter: StorageAdaptor;
constructor({ storageKey, version, adapter, timeout }: StorageHelperParams) {
// 此處沒(méi)有傳遞 adapter 就會(huì)使用 localStorage
// adapter 對(duì)象必須有 getItem 和 setItem
// 此處沒(méi)有進(jìn)一步判斷 getItem 是否為函數(shù)以及 localStorage 是否存在
// 沒(méi)有辦法限制住所有的異常
this.adapter = adapter && "getItem" in adapter && "setItem" in adapter
? adapter
: localStorage;
this.load();
}
load() {
// 此處改為 this.adapter
const result: Promise<string> | string | null = this.adapter.getItem(
this.storageKey,
);
}
commit() {
// 此處改為 this.adapter
this.adapter.setItem(this.storageKey, JSON.stringify(store));
}
}StorageHelper 添加異步獲取
如有些數(shù)據(jù)源需要異步構(gòu)建并獲取數(shù)據(jù),例如 IndexedDB 。這里我們先建立一個(gè) IndexedDBAdaptor 類。
import { StorageAdaptor } from "../utils";
// 把 indexedDB 的回調(diào)改為 Promise
function promisifyRequest<T = undefined>(
request: IDBRequest<T> | IDBTransaction,
): Promise<T> {
return new Promise<T>((resolve, reject) => {
// @ts-ignore
request.oncomplete = request.onsuccess = () => resolve(request.result);
// @ts-ignore
request.onabort = request.onerror = () => reject(request.error);
});
}
/**
* 創(chuàng)建并返回 indexedDB 的句柄
*/
const createStore = (
dbName: string,
storeName: string,
upgradeInfo: IndexedDBUpgradeInfo = {},
): UseStore => {
const request = indexedDB.open(dbName);
/**
* 創(chuàng)建或者升級(jí)時(shí)候會(huì)調(diào)用 onupgradeneeded
*/
request.onupgradeneeded = () => {
const { result: store } = request;
if (!store.objectStoreNames.contains(storeName)) {
const { options = {}, indexList = [] } = upgradeInfo;
// 基于 配置項(xiàng)生成 store
const store = request.result.createObjectStore(storeName, { ...options });
// 建立索引
indexList.forEach((index) => {
store.createIndex(index.name, index.keyPath, index.options);
});
}
};
const dbp = promisifyRequest(request);
return (txMode, callback) =>
dbp.then((db) =>
callback(db.transaction(storeName, txMode).objectStore(storeName))
);
};
export class IndexedDBAdaptor implements StorageAdaptor {
private readonly store: UseStore;
constructor({ dbName, storeName, upgradeInfo }: IndexedDBAdaptorParams) {
this.store = createStore(dbName, storeName, upgradeInfo);
}
/**
* 獲取數(shù)據(jù)
*/
getItem(key: string): Promise<string> {
return this.store("readonly", (store) => promisifyRequest(store.get(key)));
}
/**
* 設(shè)置數(shù)據(jù)
*/
setItem(key: string, value: string) {
return this.store("readwrite", (store) => {
store.put(value, key);
return promisifyRequest(store.transaction);
});
}
}對(duì) StorageHelper 類做如下改造
type CreateDeferredPromise = <TValue>() => CreateDeferredPromiseResult<TValue>;
// 劫持一個(gè) Promise 方便使用
export const createDeferredPromise: CreateDeferredPromise = <T>() => {
let resolve!: (value: T | PromiseLike<T>) => void;
let reject!: (reason?: any) => void;
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return {
currentPromise: promise,
resolve,
reject,
};
};
export class StorageHelper<T> {
// 是否準(zhǔn)備好了
ready: CreateDeferredPromiseResult<boolean> = createDeferredPromise<
boolean
>();
constructor({ storageKey, version, adapter, timeout }: StorageHelperParams) {
this.load();
}
load() {
const result: Promise<string> | string | null = this.adapter.getItem(
this.storageKey,
);
// 檢查一下當(dāng)前的結(jié)果是否是 Promise 對(duì)象
if (isPromise(result)) {
result
.then((res) => {
this.initStore(res);
// 準(zhǔn)備好了
this.ready.resolve(true);
})
.catch(() => {
this.initStore(null);
// 準(zhǔn)備好了
this.ready.resolve(true);
});
} else {
// 不是 Promise 直接構(gòu)建 store
this.initStore(result);
// 準(zhǔn)備好了
this.ready.resolve(true);
}
}
// 詢問(wèn)是否做好準(zhǔn)備
whenReady() {
return this.ready.currentPromise;
}
}如此,我們就完成了 StorageHelper 全部代碼。
列表輔助類 ListStorageHelper
ListStorageHelper 基于 StorageHelper 構(gòu)建,方便特定業(yè)務(wù)使用。
// 數(shù)組最大數(shù)量
const STORE_MAX_COUNT: number = 10;
export class ListStorageHelper<T> extends StorageHelper<T[]> {
// 主鍵,默認(rèn)為 id
readonly key: string = "id";
// 存儲(chǔ)最大數(shù)量,默認(rèn)為 10
readonly maxCount: number = STORE_MAX_COUNT;
// 是否添加在最前面
readonly isUnshiftWhenAdded: boolean = true;
// 修改后是否放入最前面
readonly isMoveTopWhenModified: boolean = true;
constructor({
maxCount,
key,
isMoveTopWhenModified = true,
isUnshiftWhenAdded = true,
storageKey,
version,
adapter,
timeout,
}: ListStorageHelperParams) {
super({ storageKey, version, adapter, timeout });
this.key = key || "id";
// 設(shè)置配置項(xiàng)
if (typeof maxCount === "number" && maxCount > 0) {
this.maxCount = maxCount;
}
if (typeof isMoveTopWhenModified === "boolean") {
this.isMoveTopWhenModified = isMoveTopWhenModified;
}
if (typeof this.isUnshiftWhenAdded === "boolean") {
this.isUnshiftWhenAdded = isUnshiftWhenAdded;
}
}
load() {
super.load();
// 沒(méi)有數(shù)據(jù),設(shè)定為空數(shù)組方便統(tǒng)一
if (!this.store!.data) {
this.store!.data = [];
}
}
getData = (): T[] => {
const items = super.getData() || [];
// 檢查數(shù)據(jù)長(zhǎng)度并移除超過(guò)的數(shù)據(jù)
this.checkThenRemoveItem(items);
return items;
};
setItem(item: T) {
if (!this.store) {
throw new Error("Please complete the loading load first");
}
const items = this.getData();
// 利用 key 去查找存在數(shù)據(jù)索引
const index = items.findIndex(
(x: any) => x[this.key] === (item as any)[this.key],
);
// 當(dāng)前有數(shù)據(jù),是更新
if (index > -1) {
const current = { ...items[index], ...item };
// 更新移動(dòng)數(shù)組數(shù)據(jù)
if (this.isMoveTopWhenModified) {
items.splice(index, 1);
items.unshift(current);
} else {
items[index] = current;
}
} else {
// 添加
this.isUnshiftWhenAdded ? items.unshift(item) : items.push(item);
}
// 檢查并移除數(shù)據(jù)
this.checkThenRemoveItem(items);
}
removeItem(key: string | number) {
if (!this.store) {
throw new Error("Please complete the loading load first");
}
const items = this.getData();
const index = items.findIndex((x: any) => x[this.key] === key);
// 移除數(shù)據(jù)
if (index > -1) {
items.splice(index, 1);
}
}
setItems(items: T[]) {
if (!this.store) {
return;
}
this.checkThenRemoveItem(items);
// 批量設(shè)置數(shù)據(jù)
this.store.data = items || [];
}
/**
* 多添加一個(gè)方法 getItems,等同于 getData 方法
*/
getItems() {
if (!this.store) {
return null;
}
return this.getData();
}
checkThenRemoveItem = (items: T[]) => {
if (items.length <= this.maxCount) {
return;
}
items.splice(this.maxCount, items.length - this.maxCount);
};
}該類繼承了 StorageHelper,我們依舊可以直接調(diào)用 commit 提交數(shù)據(jù)。如此我們就不需要維護(hù)復(fù)雜的 storage 存取邏輯了。
代碼都在 storage-tools 中,歡迎各位提交 issue 以及 pr。
到此這篇關(guān)于JavaScript手寫(xiě)一個(gè)前端存儲(chǔ)工具庫(kù)的文章就介紹到這了,更多相關(guān)JavaScript前端存儲(chǔ)工具庫(kù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JS實(shí)現(xiàn)屏蔽網(wǎng)頁(yè)右鍵復(fù)制及ctrl+c復(fù)制的方法【2種方法】
這篇文章主要介紹了JS實(shí)現(xiàn)屏蔽網(wǎng)頁(yè)右鍵復(fù)制及ctrl+c復(fù)制的方法,結(jié)合實(shí)例形式分析了2種比較常用的屏蔽復(fù)制功能的技巧,需要的朋友可以參考下2016-09-09
Bootstrap實(shí)現(xiàn)漸變頂部固定自適應(yīng)導(dǎo)航欄
這篇文章給大家介紹了Bootstrap實(shí)現(xiàn)漸變頂部固定自適應(yīng)導(dǎo)航欄,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-01-01
js將鍵值對(duì)字符串轉(zhuǎn)為json字符串的方法
下面小編就為大家分享一篇js將鍵值對(duì)字符串轉(zhuǎn)為json字符串的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-03-03
在JavaScript中調(diào)用Java類和接口的方法
這篇文章主要講述如何在JavaScript腳本語(yǔ)言中調(diào)用Java類和接口,對(duì)大家的學(xué)習(xí)和工作有一定的參考借鑒價(jià)值,有需要的朋友們下面來(lái)一起看看吧。2016-09-09
javascript實(shí)現(xiàn)移動(dòng)端觸屏拖拽功能
這篇文章主要為大家詳細(xì)介紹了javascript實(shí)現(xiàn)移動(dòng)端觸屏拖拽功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-07-07
javascript 禁用IE工具欄,導(dǎo)航欄等等實(shí)現(xiàn)代碼
在處理問(wèn)題時(shí)候遇到的,就順便記錄與大家一起分享下,感興趣的朋友可以參考下哈,希望可以幫助到你2013-04-04

