react-native-hox 0.0.1-beta → 0.0.1
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 +149 -76
- package/dist/index.d.mts +34 -37
- package/dist/index.d.ts +34 -37
- package/dist/index.js +146 -151
- package/dist/index.mjs +143 -151
- package/package.json +19 -5
- package/README.en.md +0 -36
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import
|
|
1
|
+
import useSyncExternalStoreExports from 'use-sync-external-store/shim/with-selector';
|
|
2
2
|
|
|
3
3
|
// src/createModel.ts
|
|
4
4
|
|
|
5
5
|
// src/config.ts
|
|
6
|
+
var cachedDefaultWebStorage;
|
|
7
|
+
var hasCachedDefaultWebStorage = false;
|
|
6
8
|
function getDefaultWebStorage() {
|
|
9
|
+
if (hasCachedDefaultWebStorage) {
|
|
10
|
+
return cachedDefaultWebStorage;
|
|
11
|
+
}
|
|
12
|
+
hasCachedDefaultWebStorage = true;
|
|
7
13
|
try {
|
|
8
14
|
const anyGlobal = globalThis;
|
|
9
15
|
const storage = anyGlobal == null ? void 0 : anyGlobal.localStorage;
|
|
@@ -13,57 +19,32 @@ function getDefaultWebStorage() {
|
|
|
13
19
|
const testKey = "__react_native_hox_test__";
|
|
14
20
|
storage.setItem(testKey, "1");
|
|
15
21
|
storage.removeItem(testKey);
|
|
16
|
-
|
|
22
|
+
cachedDefaultWebStorage = {
|
|
17
23
|
getItem: (key) => storage.getItem(key),
|
|
18
24
|
setItem: (key, value) => storage.setItem(key, value),
|
|
19
25
|
removeItem: (key) => storage.removeItem(key)
|
|
20
26
|
};
|
|
27
|
+
return cachedDefaultWebStorage;
|
|
21
28
|
} catch {
|
|
29
|
+
cachedDefaultWebStorage = void 0;
|
|
22
30
|
return void 0;
|
|
23
31
|
}
|
|
24
32
|
}
|
|
25
|
-
var config = {
|
|
26
|
-
storage: getDefaultWebStorage()
|
|
27
|
-
};
|
|
28
|
-
var setup = (options) => {
|
|
29
|
-
Object.assign(config, options);
|
|
30
|
-
};
|
|
31
|
-
var getConfig = () => config;
|
|
32
33
|
|
|
33
34
|
// src/vanilla.ts
|
|
34
35
|
var createStore = (initialState) => {
|
|
35
36
|
let state = initialState;
|
|
36
37
|
const listeners = /* @__PURE__ */ new Set();
|
|
37
|
-
const
|
|
38
|
-
const setState = (partial, replace = false) => {
|
|
38
|
+
const setState = (partial, replace) => {
|
|
39
39
|
const nextState = typeof partial === "function" ? partial(state) : partial;
|
|
40
|
-
if (Object.is(nextState, state)) {
|
|
41
|
-
|
|
40
|
+
if (!Object.is(nextState, state)) {
|
|
41
|
+
const previousState = state;
|
|
42
|
+
state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);
|
|
43
|
+
listeners.forEach((listener) => listener(state, previousState));
|
|
42
44
|
}
|
|
43
|
-
const previousState = state;
|
|
44
|
-
const isNextObject = typeof nextState === "object" && nextState !== null;
|
|
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);
|
|
63
|
-
}
|
|
64
|
-
const currentListeners = new Set(listeners);
|
|
65
|
-
currentListeners.forEach((listener) => listener(state, previousState));
|
|
66
45
|
};
|
|
46
|
+
const getState = () => state;
|
|
47
|
+
const getInitialState = () => initialState;
|
|
67
48
|
const subscribe = (listener) => {
|
|
68
49
|
listeners.add(listener);
|
|
69
50
|
return () => listeners.delete(listener);
|
|
@@ -71,11 +52,11 @@ var createStore = (initialState) => {
|
|
|
71
52
|
const destroy = () => {
|
|
72
53
|
listeners.clear();
|
|
73
54
|
};
|
|
74
|
-
return { getState, setState, subscribe, destroy };
|
|
55
|
+
return { getState, getInitialState, setState, subscribe, destroy };
|
|
75
56
|
};
|
|
76
57
|
|
|
77
58
|
// src/utils.ts
|
|
78
|
-
|
|
59
|
+
function shallow(objA, objB) {
|
|
79
60
|
if (Object.is(objA, objB)) {
|
|
80
61
|
return true;
|
|
81
62
|
}
|
|
@@ -93,7 +74,7 @@ var shallow = (objA, objB) => {
|
|
|
93
74
|
}
|
|
94
75
|
}
|
|
95
76
|
return true;
|
|
96
|
-
}
|
|
77
|
+
}
|
|
97
78
|
function debounce(func, wait) {
|
|
98
79
|
let timeout;
|
|
99
80
|
let lastArgs;
|
|
@@ -105,6 +86,7 @@ function debounce(func, wait) {
|
|
|
105
86
|
timeout = setTimeout(() => {
|
|
106
87
|
func.apply(context, args);
|
|
107
88
|
lastArgs = void 0;
|
|
89
|
+
timeout = void 0;
|
|
108
90
|
}, wait);
|
|
109
91
|
};
|
|
110
92
|
debounced.flush = () => {
|
|
@@ -112,162 +94,172 @@ function debounce(func, wait) {
|
|
|
112
94
|
clearTimeout(timeout);
|
|
113
95
|
func.apply(context, lastArgs);
|
|
114
96
|
lastArgs = void 0;
|
|
97
|
+
timeout = void 0;
|
|
115
98
|
}
|
|
116
99
|
};
|
|
117
100
|
debounced.cancel = () => {
|
|
118
101
|
clearTimeout(timeout);
|
|
119
102
|
lastArgs = void 0;
|
|
103
|
+
timeout = void 0;
|
|
120
104
|
};
|
|
121
105
|
return debounced;
|
|
122
106
|
}
|
|
123
107
|
|
|
124
108
|
// src/createModel.ts
|
|
109
|
+
var { useSyncExternalStoreWithSelector } = useSyncExternalStoreExports;
|
|
110
|
+
var identity = (v) => v;
|
|
111
|
+
function isInvalidHookCallError(error) {
|
|
112
|
+
return error instanceof Error && /Invalid hook call|Hooks can only be called/i.test(error.message);
|
|
113
|
+
}
|
|
125
114
|
function createModel(initialState, options) {
|
|
115
|
+
var _a;
|
|
126
116
|
const initialValue = typeof initialState === "function" ? initialState() : initialState;
|
|
127
117
|
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", {
|
|
118
|
+
const logger = (_a = options == null ? void 0 : options.logger) != null ? _a : console;
|
|
119
|
+
setupModelSideEffects(store, options, logger);
|
|
120
|
+
let isDestroyed = false;
|
|
121
|
+
const setState = (partial, replace) => {
|
|
122
|
+
var _a2;
|
|
123
|
+
if (isDestroyed && ((_a2 = options == null ? void 0 : options.strict) == null ? void 0 : _a2.forbidSetStateAfterDestroy)) {
|
|
124
|
+
const error = new Error("[react-native-hox] model \u5DF2\u9500\u6BC1\uFF0C\u65E0\u6CD5\u518D\u8C03\u7528 setState\u3002");
|
|
125
|
+
logger.error(error);
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
store.setState(partial, replace);
|
|
129
|
+
};
|
|
130
|
+
const destroy = () => {
|
|
131
|
+
isDestroyed = true;
|
|
132
|
+
store.destroy();
|
|
133
|
+
};
|
|
134
|
+
const reset = () => store.setState(initialValue, true);
|
|
135
|
+
const subscribe = createSubscribe(store);
|
|
136
|
+
const data = {
|
|
137
|
+
getState: store.getState,
|
|
138
|
+
setState,
|
|
139
|
+
subscribe,
|
|
140
|
+
destroy,
|
|
141
|
+
reset
|
|
142
|
+
};
|
|
143
|
+
Object.defineProperty(data, "state", {
|
|
167
144
|
get: () => store.getState()
|
|
168
145
|
});
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
146
|
+
const hook = createModelHook(store);
|
|
147
|
+
const model = {
|
|
148
|
+
getState: hook,
|
|
149
|
+
useState: hook,
|
|
150
|
+
use: hook,
|
|
151
|
+
// Add 'use' alias
|
|
152
|
+
setState,
|
|
153
|
+
reset,
|
|
154
|
+
destroy,
|
|
155
|
+
data
|
|
173
156
|
};
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
157
|
+
const modelFn = function(selector, equalityFn) {
|
|
158
|
+
return hook(selector, equalityFn);
|
|
159
|
+
};
|
|
160
|
+
Object.assign(modelFn, model);
|
|
161
|
+
return modelFn;
|
|
162
|
+
}
|
|
163
|
+
function createSubscribe(store) {
|
|
164
|
+
return ((arg1, arg2, arg3) => {
|
|
178
165
|
var _a;
|
|
179
166
|
if (typeof arg1 === "function" && typeof arg2 !== "function") {
|
|
180
|
-
return
|
|
167
|
+
return store.subscribe(arg1);
|
|
181
168
|
}
|
|
182
169
|
const selector = arg1;
|
|
183
170
|
const listener = arg2;
|
|
184
171
|
const options = arg3 != null ? arg3 : {};
|
|
185
172
|
const equalityFn = (_a = options.equalityFn) != null ? _a : Object.is;
|
|
186
|
-
let
|
|
173
|
+
let lastSelected = selector(store.getState());
|
|
187
174
|
if (options.fireImmediately) {
|
|
188
|
-
listener(
|
|
175
|
+
listener(lastSelected, lastSelected);
|
|
189
176
|
}
|
|
190
|
-
return
|
|
177
|
+
return store.subscribe((nextState) => {
|
|
191
178
|
const nextSelected = selector(nextState);
|
|
192
|
-
|
|
193
|
-
if (equalityFn(prevSelectedFromPrevState, nextSelected)) {
|
|
179
|
+
if (equalityFn(lastSelected, nextSelected)) {
|
|
194
180
|
return;
|
|
195
181
|
}
|
|
196
|
-
prevSelected =
|
|
197
|
-
|
|
182
|
+
const prevSelected = lastSelected;
|
|
183
|
+
lastSelected = nextSelected;
|
|
184
|
+
listener(nextSelected, prevSelected);
|
|
198
185
|
});
|
|
199
|
-
};
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function createModelHook(store) {
|
|
189
|
+
const useModel = ((selector, equalityFn = shallow) => {
|
|
190
|
+
try {
|
|
191
|
+
return useSyncExternalStoreWithSelector(
|
|
192
|
+
store.subscribe,
|
|
193
|
+
store.getState,
|
|
194
|
+
store.getInitialState,
|
|
195
|
+
selector != null ? selector : identity,
|
|
196
|
+
equalityFn
|
|
197
|
+
);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
if (isInvalidHookCallError(error)) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
"[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"
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
});
|
|
200
207
|
return useModel;
|
|
201
208
|
}
|
|
202
|
-
function setupModelSideEffects(store, options) {
|
|
203
|
-
var _a, _b, _c;
|
|
204
|
-
const { storage } = getConfig();
|
|
209
|
+
function setupModelSideEffects(store, options, logger) {
|
|
210
|
+
var _a, _b, _c, _d;
|
|
205
211
|
const persist = typeof (options == null ? void 0 : options.persist) === "string" ? { key: options.persist } : options == null ? void 0 : options.persist;
|
|
206
212
|
const persistKey = persist == null ? void 0 : persist.key;
|
|
207
|
-
const
|
|
213
|
+
const storage = (_a = persist == null ? void 0 : persist.storage) != null ? _a : getDefaultWebStorage();
|
|
208
214
|
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
215
|
return;
|
|
215
216
|
}
|
|
216
|
-
const beforePersist = (
|
|
217
|
-
const beforeRecover = (
|
|
218
|
-
const debounceMs = (
|
|
217
|
+
const beforePersist = (_b = persist == null ? void 0 : persist.beforePersist) != null ? _b : ((s) => s);
|
|
218
|
+
const beforeRecover = (_c = persist == null ? void 0 : persist.beforeRecover) != null ? _c : ((v) => v);
|
|
219
|
+
const debounceMs = (_d = persist == null ? void 0 : persist.debounce) != null ? _d : 100;
|
|
219
220
|
if (!storage) {
|
|
220
|
-
|
|
221
|
-
"[react-native-hox] \
|
|
221
|
+
logger.warn(
|
|
222
|
+
"[react-native-hox] persist \u5DF2\u5F00\u542F\u4F46 storage \u4E0D\u53EF\u7528\uFF1AWeb \u9700\u8981 localStorage \u53EF\u7528\uFF1BReact Native/Node \u8BF7\u5728 persist.storage \u4F20\u5165 AsyncStorage/MMKV \u7B49\u3002"
|
|
222
223
|
);
|
|
224
|
+
return;
|
|
223
225
|
}
|
|
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;
|
|
226
|
+
let isDestroyed = false;
|
|
227
|
+
Promise.resolve().then(() => storage.getItem(persistKey)).then((val) => {
|
|
228
|
+
if (isDestroyed) return;
|
|
229
|
+
if (!val) return;
|
|
230
|
+
try {
|
|
231
|
+
const parsed = JSON.parse(val);
|
|
232
|
+
const recovered = beforeRecover(parsed);
|
|
233
|
+
store.setState(recovered, false);
|
|
234
|
+
} catch (e) {
|
|
235
|
+
logger.error("[react-native-hox] Failed to parse stored value:", e);
|
|
236
|
+
}
|
|
237
|
+
}).catch((e) => {
|
|
238
|
+
logger.error("[react-native-hox] Recover failed:", e);
|
|
239
|
+
});
|
|
240
|
+
const save = debounce((state) => {
|
|
241
|
+
if (isDestroyed) return;
|
|
258
242
|
try {
|
|
259
243
|
const prepared = beforePersist(state);
|
|
260
|
-
|
|
244
|
+
const result = storage.setItem(persistKey, JSON.stringify(prepared));
|
|
245
|
+
Promise.resolve(result).catch((e) => {
|
|
246
|
+
logger.error("[react-native-hox] Save failed:", e);
|
|
247
|
+
});
|
|
261
248
|
} catch (e) {
|
|
262
|
-
|
|
249
|
+
logger.error("[react-native-hox] Save failed:", e);
|
|
263
250
|
}
|
|
264
251
|
}, debounceMs);
|
|
265
|
-
store.subscribe((state) => {
|
|
266
|
-
save(state
|
|
267
|
-
if (channel && !suppressBroadcast) {
|
|
268
|
-
channel.postMessage({ __hox: true, source: instanceId, state });
|
|
269
|
-
}
|
|
252
|
+
const unsubscribe = store.subscribe((state) => {
|
|
253
|
+
save(state);
|
|
270
254
|
});
|
|
255
|
+
const originalDestroy = store.destroy;
|
|
256
|
+
store.destroy = () => {
|
|
257
|
+
unsubscribe();
|
|
258
|
+
save.flush();
|
|
259
|
+
isDestroyed = true;
|
|
260
|
+
save.cancel();
|
|
261
|
+
originalDestroy();
|
|
262
|
+
};
|
|
271
263
|
}
|
|
272
264
|
|
|
273
|
-
export { createModel,
|
|
265
|
+
export { createModel, shallow };
|
package/package.json
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-hox",
|
|
3
|
-
"version": "0.0.1
|
|
3
|
+
"version": "0.0.1",
|
|
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,14 +39,14 @@
|
|
|
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
|
+
"use-sync-external-store": "^1.6.0"
|
|
49
|
+
},
|
|
37
50
|
"peerDependencies": {
|
|
38
51
|
"react": ">=16.8",
|
|
39
52
|
"react-native": ">=0.64"
|
|
@@ -43,6 +56,7 @@
|
|
|
43
56
|
"@types/react": "^19.2.8",
|
|
44
57
|
"@types/react-native": "^0.72.8",
|
|
45
58
|
"@types/react-test-renderer": "^19.1.0",
|
|
59
|
+
"@types/use-sync-external-store": "^1.5.0",
|
|
46
60
|
"jest": "^30.2.0",
|
|
47
61
|
"react": "^19.2.0",
|
|
48
62
|
"react-test-renderer": "^19.2.3",
|
|
@@ -50,4 +64,4 @@
|
|
|
50
64
|
"tsup": "^8.5.0",
|
|
51
65
|
"typescript": "^5.9.3"
|
|
52
66
|
}
|
|
53
|
-
}
|
|
67
|
+
}
|
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/)
|