react-native-hox 0.0.1-beta → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +188 -88
- package/dist/index.d.mts +33 -37
- package/dist/index.d.ts +33 -37
- package/dist/index.js +146 -170
- package/dist/index.mjs +142 -170
- package/package.json +21 -6
- package/README.en.md +0 -36
package/dist/index.mjs
CHANGED
|
@@ -1,69 +1,30 @@
|
|
|
1
|
-
import
|
|
1
|
+
import useSyncExternalStoreExports from 'use-sync-external-store/shim/with-selector';
|
|
2
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
2
3
|
|
|
3
4
|
// src/createModel.ts
|
|
4
5
|
|
|
5
|
-
// src/
|
|
6
|
-
function
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const storage = anyGlobal == null ? void 0 : anyGlobal.localStorage;
|
|
10
|
-
if (!storage) {
|
|
11
|
-
return void 0;
|
|
12
|
-
}
|
|
13
|
-
const testKey = "__react_native_hox_test__";
|
|
14
|
-
storage.setItem(testKey, "1");
|
|
15
|
-
storage.removeItem(testKey);
|
|
16
|
-
return {
|
|
17
|
-
getItem: (key) => storage.getItem(key),
|
|
18
|
-
setItem: (key, value) => storage.setItem(key, value),
|
|
19
|
-
removeItem: (key) => storage.removeItem(key)
|
|
20
|
-
};
|
|
21
|
-
} catch {
|
|
22
|
-
return void 0;
|
|
6
|
+
// src/vanilla.ts
|
|
7
|
+
function isPlainObject(value) {
|
|
8
|
+
if (Object.prototype.toString.call(value) !== "[object Object]") {
|
|
9
|
+
return false;
|
|
23
10
|
}
|
|
11
|
+
const proto = Object.getPrototypeOf(value);
|
|
12
|
+
return proto === Object.prototype || proto === null;
|
|
24
13
|
}
|
|
25
|
-
var config = {
|
|
26
|
-
storage: getDefaultWebStorage()
|
|
27
|
-
};
|
|
28
|
-
var setup = (options) => {
|
|
29
|
-
Object.assign(config, options);
|
|
30
|
-
};
|
|
31
|
-
var getConfig = () => config;
|
|
32
|
-
|
|
33
|
-
// src/vanilla.ts
|
|
34
14
|
var createStore = (initialState) => {
|
|
35
15
|
let state = initialState;
|
|
36
16
|
const listeners = /* @__PURE__ */ new Set();
|
|
37
|
-
const
|
|
38
|
-
const setState = (partial, replace = false) => {
|
|
17
|
+
const setState = (partial, replace) => {
|
|
39
18
|
const nextState = typeof partial === "function" ? partial(state) : partial;
|
|
40
|
-
if (Object.is(nextState, state)) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const isPrevObject = typeof previousState === "object" && previousState !== null;
|
|
46
|
-
const shouldReplace = replace || !isNextObject || !isPrevObject;
|
|
47
|
-
if (shouldReplace) {
|
|
48
|
-
state = nextState;
|
|
49
|
-
} else {
|
|
50
|
-
const patch = nextState;
|
|
51
|
-
const prevObj = previousState;
|
|
52
|
-
let changed = false;
|
|
53
|
-
for (const [key, value] of Object.entries(patch)) {
|
|
54
|
-
if (!Object.is(prevObj[key], value)) {
|
|
55
|
-
changed = true;
|
|
56
|
-
break;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
if (!changed) {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
state = Object.assign({}, previousState, nextState);
|
|
19
|
+
if (!Object.is(nextState, state)) {
|
|
20
|
+
const previousState = state;
|
|
21
|
+
const shouldReplace = replace != null ? replace : !isPlainObject(state) || !isPlainObject(nextState);
|
|
22
|
+
state = shouldReplace ? nextState : Object.assign({}, state, nextState);
|
|
23
|
+
listeners.forEach((listener) => listener(state, previousState));
|
|
63
24
|
}
|
|
64
|
-
const currentListeners = new Set(listeners);
|
|
65
|
-
currentListeners.forEach((listener) => listener(state, previousState));
|
|
66
25
|
};
|
|
26
|
+
const getState = () => state;
|
|
27
|
+
const getInitialState = () => initialState;
|
|
67
28
|
const subscribe = (listener) => {
|
|
68
29
|
listeners.add(listener);
|
|
69
30
|
return () => listeners.delete(listener);
|
|
@@ -71,11 +32,11 @@ var createStore = (initialState) => {
|
|
|
71
32
|
const destroy = () => {
|
|
72
33
|
listeners.clear();
|
|
73
34
|
};
|
|
74
|
-
return { getState, setState, subscribe, destroy };
|
|
35
|
+
return { getState, getInitialState, setState, subscribe, destroy };
|
|
75
36
|
};
|
|
76
37
|
|
|
77
38
|
// src/utils.ts
|
|
78
|
-
|
|
39
|
+
function shallow(objA, objB) {
|
|
79
40
|
if (Object.is(objA, objB)) {
|
|
80
41
|
return true;
|
|
81
42
|
}
|
|
@@ -93,7 +54,7 @@ var shallow = (objA, objB) => {
|
|
|
93
54
|
}
|
|
94
55
|
}
|
|
95
56
|
return true;
|
|
96
|
-
}
|
|
57
|
+
}
|
|
97
58
|
function debounce(func, wait) {
|
|
98
59
|
let timeout;
|
|
99
60
|
let lastArgs;
|
|
@@ -105,6 +66,7 @@ function debounce(func, wait) {
|
|
|
105
66
|
timeout = setTimeout(() => {
|
|
106
67
|
func.apply(context, args);
|
|
107
68
|
lastArgs = void 0;
|
|
69
|
+
timeout = void 0;
|
|
108
70
|
}, wait);
|
|
109
71
|
};
|
|
110
72
|
debounced.flush = () => {
|
|
@@ -112,162 +74,172 @@ function debounce(func, wait) {
|
|
|
112
74
|
clearTimeout(timeout);
|
|
113
75
|
func.apply(context, lastArgs);
|
|
114
76
|
lastArgs = void 0;
|
|
77
|
+
timeout = void 0;
|
|
115
78
|
}
|
|
116
79
|
};
|
|
117
80
|
debounced.cancel = () => {
|
|
118
81
|
clearTimeout(timeout);
|
|
119
82
|
lastArgs = void 0;
|
|
83
|
+
timeout = void 0;
|
|
120
84
|
};
|
|
121
85
|
return debounced;
|
|
122
86
|
}
|
|
123
87
|
|
|
124
88
|
// src/createModel.ts
|
|
89
|
+
var { useSyncExternalStoreWithSelector } = useSyncExternalStoreExports;
|
|
90
|
+
var identity = (v) => v;
|
|
91
|
+
function isInvalidHookCallError(error) {
|
|
92
|
+
return error instanceof Error && /Invalid hook call|Hooks can only be called/i.test(error.message);
|
|
93
|
+
}
|
|
125
94
|
function createModel(initialState, options) {
|
|
95
|
+
var _a;
|
|
126
96
|
const initialValue = typeof initialState === "function" ? initialState() : initialState;
|
|
127
97
|
const store = createStore(initialValue);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (equalityRef.current(lastSelectionRef.current, nextSelection)) {
|
|
155
|
-
lastStateRef.current = state;
|
|
156
|
-
return lastSelectionRef.current;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
hasSelectionRef.current = true;
|
|
160
|
-
lastStateRef.current = state;
|
|
161
|
-
lastSelectionRef.current = nextSelection;
|
|
162
|
-
return nextSelection;
|
|
163
|
-
};
|
|
164
|
-
return useSyncExternalStore(subscribe, getSelection, getSelection);
|
|
165
|
-
});
|
|
166
|
-
Object.defineProperty(useModel, "data", {
|
|
98
|
+
const logger = (_a = options == null ? void 0 : options.logger) != null ? _a : console;
|
|
99
|
+
setupModelSideEffects(store, options, logger);
|
|
100
|
+
let isDestroyed = false;
|
|
101
|
+
const setState = (partial, replace) => {
|
|
102
|
+
var _a2;
|
|
103
|
+
if (isDestroyed && ((_a2 = options == null ? void 0 : options.strict) == null ? void 0 : _a2.forbidSetStateAfterDestroy)) {
|
|
104
|
+
const error = new Error("[react-native-hox] model \u5DF2\u9500\u6BC1\uFF0C\u65E0\u6CD5\u518D\u8C03\u7528 setState\u3002");
|
|
105
|
+
logger.error(error);
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
store.setState(partial, replace);
|
|
109
|
+
};
|
|
110
|
+
const destroy = () => {
|
|
111
|
+
isDestroyed = true;
|
|
112
|
+
store.destroy();
|
|
113
|
+
};
|
|
114
|
+
const reset = () => store.setState(initialValue, true);
|
|
115
|
+
const subscribe = createSubscribe(store);
|
|
116
|
+
const data = {
|
|
117
|
+
getState: store.getState,
|
|
118
|
+
setState,
|
|
119
|
+
subscribe,
|
|
120
|
+
destroy,
|
|
121
|
+
reset
|
|
122
|
+
};
|
|
123
|
+
Object.defineProperty(data, "state", {
|
|
167
124
|
get: () => store.getState()
|
|
168
125
|
});
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
126
|
+
const hook = createModelHook(store);
|
|
127
|
+
const model = {
|
|
128
|
+
getState: hook,
|
|
129
|
+
useState: hook,
|
|
130
|
+
use: hook,
|
|
131
|
+
// Add 'use' alias
|
|
132
|
+
setState,
|
|
133
|
+
reset,
|
|
134
|
+
destroy,
|
|
135
|
+
data
|
|
136
|
+
};
|
|
137
|
+
const modelFn = function(selector, equalityFn) {
|
|
138
|
+
return hook(selector, equalityFn);
|
|
173
139
|
};
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
140
|
+
Object.assign(modelFn, model);
|
|
141
|
+
return modelFn;
|
|
142
|
+
}
|
|
143
|
+
function createSubscribe(store) {
|
|
144
|
+
return ((arg1, arg2, arg3) => {
|
|
178
145
|
var _a;
|
|
179
146
|
if (typeof arg1 === "function" && typeof arg2 !== "function") {
|
|
180
|
-
return
|
|
147
|
+
return store.subscribe(arg1);
|
|
181
148
|
}
|
|
182
149
|
const selector = arg1;
|
|
183
150
|
const listener = arg2;
|
|
184
151
|
const options = arg3 != null ? arg3 : {};
|
|
185
152
|
const equalityFn = (_a = options.equalityFn) != null ? _a : Object.is;
|
|
186
|
-
let
|
|
153
|
+
let lastSelected = selector(store.getState());
|
|
187
154
|
if (options.fireImmediately) {
|
|
188
|
-
listener(
|
|
155
|
+
listener(lastSelected, lastSelected);
|
|
189
156
|
}
|
|
190
|
-
return
|
|
157
|
+
return store.subscribe((nextState) => {
|
|
191
158
|
const nextSelected = selector(nextState);
|
|
192
|
-
|
|
193
|
-
if (equalityFn(prevSelectedFromPrevState, nextSelected)) {
|
|
159
|
+
if (equalityFn(lastSelected, nextSelected)) {
|
|
194
160
|
return;
|
|
195
161
|
}
|
|
196
|
-
prevSelected =
|
|
197
|
-
|
|
162
|
+
const prevSelected = lastSelected;
|
|
163
|
+
lastSelected = nextSelected;
|
|
164
|
+
listener(nextSelected, prevSelected);
|
|
198
165
|
});
|
|
199
|
-
};
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
function createModelHook(store) {
|
|
169
|
+
const useModel = ((selector, equalityFn = shallow) => {
|
|
170
|
+
try {
|
|
171
|
+
return useSyncExternalStoreWithSelector(
|
|
172
|
+
store.subscribe,
|
|
173
|
+
store.getState,
|
|
174
|
+
store.getInitialState,
|
|
175
|
+
selector != null ? selector : identity,
|
|
176
|
+
equalityFn
|
|
177
|
+
);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (isInvalidHookCallError(error)) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
"[react-native-hox] \u4F60\u6B63\u5728\u7EC4\u4EF6\u5916\u8C03\u7528 Hook \u8BFB\u53D6\u72B6\u6001\uFF1B\u8BF7\u5728\u7EC4\u4EF6\u5185\u4F7F\u7528 `model()` / `model.use()` / `model.getState()` / `model.useState()`\uFF0C\u7EC4\u4EF6\u5916\u8BF7\u4F7F\u7528 `model.data.getState()` / `model.data.state`\u3002"
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
});
|
|
200
187
|
return useModel;
|
|
201
188
|
}
|
|
202
|
-
function setupModelSideEffects(store, options) {
|
|
203
|
-
var _a, _b, _c;
|
|
204
|
-
const { storage } = getConfig();
|
|
189
|
+
function setupModelSideEffects(store, options, logger) {
|
|
190
|
+
var _a, _b, _c, _d;
|
|
205
191
|
const persist = typeof (options == null ? void 0 : options.persist) === "string" ? { key: options.persist } : options == null ? void 0 : options.persist;
|
|
206
192
|
const persistKey = persist == null ? void 0 : persist.key;
|
|
207
|
-
const
|
|
193
|
+
const storage = (_a = persist == null ? void 0 : persist.storage) != null ? _a : AsyncStorage;
|
|
208
194
|
if (!persistKey) {
|
|
209
|
-
if (syncEnabled) {
|
|
210
|
-
console.warn(
|
|
211
|
-
"[react-native-hox] `sync` \u9700\u8981 `persist` \u63D0\u4F9B key \u624D\u80FD\u5EFA\u7ACB\u901A\u9053"
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
195
|
return;
|
|
215
196
|
}
|
|
216
|
-
const beforePersist = (
|
|
217
|
-
const beforeRecover = (
|
|
218
|
-
const debounceMs = (
|
|
197
|
+
const beforePersist = (_b = persist == null ? void 0 : persist.beforePersist) != null ? _b : ((s) => s);
|
|
198
|
+
const beforeRecover = (_c = persist == null ? void 0 : persist.beforeRecover) != null ? _c : ((v) => v);
|
|
199
|
+
const debounceMs = (_d = persist == null ? void 0 : persist.debounce) != null ? _d : 100;
|
|
219
200
|
if (!storage) {
|
|
220
|
-
|
|
221
|
-
"[react-native-hox] \
|
|
201
|
+
logger.warn(
|
|
202
|
+
"[react-native-hox] persist \u5DF2\u5F00\u542F\u4F46 storage \u4E0D\u53EF\u7528\uFF1A\u8BF7\u5728 persist.storage \u4F20\u5165 AsyncStorage/MMKV \u7B49\u3002"
|
|
222
203
|
);
|
|
204
|
+
return;
|
|
223
205
|
}
|
|
224
|
-
let
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
if (syncEnabled && typeof BroadcastChannel !== "undefined") {
|
|
243
|
-
channel = new BroadcastChannel(`hox-sync-${persistKey}`);
|
|
244
|
-
channel.onmessage = (event) => {
|
|
245
|
-
const payload = event.data;
|
|
246
|
-
const nextState = (payload == null ? void 0 : payload.__hox) ? payload.state : payload;
|
|
247
|
-
const source = (payload == null ? void 0 : payload.__hox) ? payload.source : void 0;
|
|
248
|
-
if (source && source === instanceId) {
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
suppressBroadcast = true;
|
|
252
|
-
store.setState(nextState, true);
|
|
253
|
-
suppressBroadcast = false;
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
const save = debounce((state, s) => {
|
|
257
|
-
if (!s) return;
|
|
206
|
+
let isDestroyed = false;
|
|
207
|
+
Promise.resolve().then(() => storage.getItem(persistKey)).then((val) => {
|
|
208
|
+
if (isDestroyed) return;
|
|
209
|
+
if (!val) return;
|
|
210
|
+
try {
|
|
211
|
+
const parsed = JSON.parse(val);
|
|
212
|
+
const recovered = beforeRecover(parsed);
|
|
213
|
+
store.setState(recovered, false);
|
|
214
|
+
} catch (e) {
|
|
215
|
+
logger.error("[react-native-hox] Failed to parse stored value:", e);
|
|
216
|
+
}
|
|
217
|
+
}).catch((e) => {
|
|
218
|
+
logger.error("[react-native-hox] Recover failed:", e);
|
|
219
|
+
});
|
|
220
|
+
const save = debounce((state) => {
|
|
221
|
+
if (isDestroyed) return;
|
|
258
222
|
try {
|
|
259
223
|
const prepared = beforePersist(state);
|
|
260
|
-
|
|
224
|
+
const result = storage.setItem(persistKey, JSON.stringify(prepared));
|
|
225
|
+
Promise.resolve(result).catch((e) => {
|
|
226
|
+
logger.error("[react-native-hox] Save failed:", e);
|
|
227
|
+
});
|
|
261
228
|
} catch (e) {
|
|
262
|
-
|
|
229
|
+
logger.error("[react-native-hox] Save failed:", e);
|
|
263
230
|
}
|
|
264
231
|
}, debounceMs);
|
|
265
|
-
store.subscribe((state) => {
|
|
266
|
-
save(state
|
|
267
|
-
if (channel && !suppressBroadcast) {
|
|
268
|
-
channel.postMessage({ __hox: true, source: instanceId, state });
|
|
269
|
-
}
|
|
232
|
+
const unsubscribe = store.subscribe((state) => {
|
|
233
|
+
save(state);
|
|
270
234
|
});
|
|
235
|
+
const originalDestroy = store.destroy;
|
|
236
|
+
store.destroy = () => {
|
|
237
|
+
unsubscribe();
|
|
238
|
+
save.flush();
|
|
239
|
+
isDestroyed = true;
|
|
240
|
+
save.cancel();
|
|
241
|
+
originalDestroy();
|
|
242
|
+
};
|
|
271
243
|
}
|
|
272
244
|
|
|
273
|
-
export { createModel,
|
|
245
|
+
export { createModel, shallow };
|
package/package.json
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-hox",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "一个轻量级、类型安全、零心智负担的 React Native 状态管理解决方案。",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react-native",
|
|
7
|
+
"state-management",
|
|
8
|
+
"store",
|
|
9
|
+
"global-state",
|
|
10
|
+
"typescript",
|
|
11
|
+
"hooks",
|
|
12
|
+
"hox"
|
|
13
|
+
],
|
|
5
14
|
"repository": {
|
|
6
15
|
"type": "git",
|
|
7
16
|
"url": "https://gitee.com/ws18250840411/react-native-hox.git"
|
|
8
17
|
},
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://gitee.com/ws18250840411/react-native-hox/issues"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://gitee.com/ws18250840411/react-native-hox#readme",
|
|
9
22
|
"license": "ISC",
|
|
10
23
|
"author": "wangws",
|
|
11
24
|
"sideEffects": false,
|
|
@@ -26,23 +39,25 @@
|
|
|
26
39
|
"dist",
|
|
27
40
|
"README.md"
|
|
28
41
|
],
|
|
29
|
-
"publishConfig": {
|
|
30
|
-
"tag": "beta"
|
|
31
|
-
},
|
|
32
42
|
"scripts": {
|
|
33
43
|
"test": "jest",
|
|
34
44
|
"build": "tsup",
|
|
35
45
|
"prepack": "npm run build"
|
|
36
46
|
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@react-native-async-storage/async-storage": "^2.2.0",
|
|
49
|
+
"use-sync-external-store": "^1.6.0"
|
|
50
|
+
},
|
|
37
51
|
"peerDependencies": {
|
|
38
52
|
"react": ">=16.8",
|
|
39
|
-
"react-native": ">=0.
|
|
53
|
+
"react-native": ">=0.65"
|
|
40
54
|
},
|
|
41
55
|
"devDependencies": {
|
|
42
56
|
"@types/jest": "^30.0.0",
|
|
43
57
|
"@types/react": "^19.2.8",
|
|
44
58
|
"@types/react-native": "^0.72.8",
|
|
45
59
|
"@types/react-test-renderer": "^19.1.0",
|
|
60
|
+
"@types/use-sync-external-store": "^1.5.0",
|
|
46
61
|
"jest": "^30.2.0",
|
|
47
62
|
"react": "^19.2.0",
|
|
48
63
|
"react-test-renderer": "^19.2.3",
|
|
@@ -50,4 +65,4 @@
|
|
|
50
65
|
"tsup": "^8.5.0",
|
|
51
66
|
"typescript": "^5.9.3"
|
|
52
67
|
}
|
|
53
|
-
}
|
|
68
|
+
}
|
package/README.en.md
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# react-native-hox
|
|
2
|
-
|
|
3
|
-
#### Description
|
|
4
|
-
{**When you're done, you can delete the content in this README and update the file with details for others getting started with your repository**}
|
|
5
|
-
|
|
6
|
-
#### Software Architecture
|
|
7
|
-
Software architecture description
|
|
8
|
-
|
|
9
|
-
#### Installation
|
|
10
|
-
|
|
11
|
-
1. xxxx
|
|
12
|
-
2. xxxx
|
|
13
|
-
3. xxxx
|
|
14
|
-
|
|
15
|
-
#### Instructions
|
|
16
|
-
|
|
17
|
-
1. xxxx
|
|
18
|
-
2. xxxx
|
|
19
|
-
3. xxxx
|
|
20
|
-
|
|
21
|
-
#### Contribution
|
|
22
|
-
|
|
23
|
-
1. Fork the repository
|
|
24
|
-
2. Create Feat_xxx branch
|
|
25
|
-
3. Commit your code
|
|
26
|
-
4. Create Pull Request
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
#### Gitee Feature
|
|
30
|
-
|
|
31
|
-
1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
|
|
32
|
-
2. Gitee blog [blog.gitee.com](https://blog.gitee.com)
|
|
33
|
-
3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
|
|
34
|
-
4. The most valuable open source project [GVP](https://gitee.com/gvp)
|
|
35
|
-
5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
|
|
36
|
-
6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
|