ez-saga 18.0.5 → 18.0.7
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 +52 -1
- package/lib/redux/PromiseMiddleware.js +13 -13
- package/lib/redux/PromiseMiddleware.js.map +1 -1
- package/lib/redux/createApp.js +95 -78
- package/lib/redux/createApp.js.map +1 -1
- package/lib/vite/index.d.ts +11 -0
- package/lib/vite/index.js +59 -0
- package/lib/vite/index.js.map +1 -0
- package/lib/webpack/index.d.ts +9 -0
- package/lib/webpack/index.js +28 -0
- package/lib/webpack/index.js.map +1 -0
- package/lib/webpack/loader.d.ts +5 -0
- package/lib/webpack/loader.js +68 -0
- package/lib/webpack/loader.js.map +1 -0
- package/package.json +25 -22
- package/src/index.ts +1 -1
- package/src/redux/PromiseMiddleware.ts +17 -14
- package/src/redux/createApp.ts +121 -95
- package/src/redux/typeDeclare.ts +1 -6
- package/src/vite/index.ts +63 -0
- package/src/webpack/index.ts +31 -0
- package/src/webpack/loader.ts +72 -0
- package/tsconfig.json +2 -1
- package/src/global-shim.d.ts +0 -10
- package/src/index.js +0 -4
package/README.md
CHANGED
|
@@ -142,4 +142,55 @@ function stateMapProps(state, props) {
|
|
|
142
142
|
export default connect(stateMapProps)(View);
|
|
143
143
|
```
|
|
144
144
|
|
|
145
|
-
We can invoke reducers and effects using dispatch, and we can obtain the results returned by effects.
|
|
145
|
+
We can invoke reducers and effects using dispatch, and we can obtain the results returned by effects.
|
|
146
|
+
|
|
147
|
+
# Vite Plugin for HMR
|
|
148
|
+
|
|
149
|
+
`ez-saga` provides a built-in Vite plugin to support Hot Module Replacement (HMR) for models. This allows you to modify effects and reducers during development without reloading the page.
|
|
150
|
+
|
|
151
|
+
## Usage
|
|
152
|
+
|
|
153
|
+
1. Import the plugin in your `vite.config.ts`:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { defineConfig } from 'vite';
|
|
157
|
+
import ezSagaHmr from 'ez-saga/vite';
|
|
158
|
+
|
|
159
|
+
export default defineConfig({
|
|
160
|
+
plugins: [
|
|
161
|
+
ezSagaHmr(), // Add ez-saga HMR plugin
|
|
162
|
+
// other plugins...
|
|
163
|
+
]
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
2. That's it!
|
|
168
|
+
|
|
169
|
+
The plugin automatically detects your model files and injects hot update logic. When you modify a model file, `ez-saga` will:
|
|
170
|
+
- Cancel old saga tasks.
|
|
171
|
+
- Re-register the model reducers (hot-swap logic).
|
|
172
|
+
- Restart the effects.
|
|
173
|
+
|
|
174
|
+
**Note**: This requires your model files to be exported using `export default` and contain a `name` property.
|
|
175
|
+
|
|
176
|
+
# Webpack Plugin for HMR
|
|
177
|
+
|
|
178
|
+
For Webpack 5 users, `ez-saga` also provides a compatible HMR plugin.
|
|
179
|
+
|
|
180
|
+
## Usage
|
|
181
|
+
|
|
182
|
+
1. Import the plugin in your `webpack.config.js`:
|
|
183
|
+
|
|
184
|
+
```javascript
|
|
185
|
+
const EzSagaWebpackPlugin = require('ez-saga/webpack').default; // Notice the .default for CJS
|
|
186
|
+
|
|
187
|
+
module.exports = {
|
|
188
|
+
// ...
|
|
189
|
+
plugins: [
|
|
190
|
+
new EzSagaWebpackPlugin(),
|
|
191
|
+
// other plugins...
|
|
192
|
+
]
|
|
193
|
+
};
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
This plugin automatically configures the HMR loader for your model files.
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 判断是否为标准Action
|
|
3
|
+
*/
|
|
4
|
+
function isAction(action) {
|
|
5
|
+
return typeof action === 'object' && action !== null && 'type' in action;
|
|
6
|
+
}
|
|
1
7
|
/**
|
|
2
8
|
* 创建中间层
|
|
3
9
|
* @param registedModel 已注册model
|
|
@@ -6,22 +12,18 @@ function createPromiseMiddleware(registedModel) {
|
|
|
6
12
|
function isEffect(type) {
|
|
7
13
|
if (!type || typeof type !== 'string')
|
|
8
14
|
return false;
|
|
15
|
+
// 性能优化:快速排除不包含 '/' 的普通 Action
|
|
16
|
+
if (type.indexOf('/') === -1)
|
|
17
|
+
return false;
|
|
9
18
|
const [modelName, effect] = type.split('/');
|
|
10
19
|
const model = registedModel[modelName];
|
|
11
|
-
if (model) {
|
|
12
|
-
|
|
13
|
-
return true;
|
|
14
|
-
}
|
|
20
|
+
if (model && model.effects && model.effects[effect]) {
|
|
21
|
+
return true;
|
|
15
22
|
}
|
|
16
23
|
return false;
|
|
17
24
|
}
|
|
18
25
|
return (api) => (next) => (action) => {
|
|
19
|
-
|
|
20
|
-
if (action?.type !== undefined) {
|
|
21
|
-
const { type } = action;
|
|
22
|
-
exeEffect = isEffect(type);
|
|
23
|
-
}
|
|
24
|
-
if (exeEffect) {
|
|
26
|
+
if (isAction(action) && isEffect(action.type)) {
|
|
25
27
|
return new Promise((resolve, reject) => {
|
|
26
28
|
next({
|
|
27
29
|
_dy_resolve: resolve,
|
|
@@ -30,9 +32,7 @@ function createPromiseMiddleware(registedModel) {
|
|
|
30
32
|
});
|
|
31
33
|
});
|
|
32
34
|
}
|
|
33
|
-
|
|
34
|
-
return next(action);
|
|
35
|
-
}
|
|
35
|
+
return next(action);
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
export default createPromiseMiddleware;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PromiseMiddleware.js","sourceRoot":"","sources":["../../src/redux/PromiseMiddleware.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,SAAS,uBAAuB,CAAqB,aAA4B;IAC/E,SAAS,QAAQ,CAAC,IAAY;QAC5B,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"PromiseMiddleware.js","sourceRoot":"","sources":["../../src/redux/PromiseMiddleware.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,SAAS,QAAQ,CAAC,MAAe;IAC/B,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,IAAI,MAAM,CAAC;AAC3E,CAAC;AAED;;;GAGG;AACH,SAAS,uBAAuB,CAAqB,aAA4B;IAC/E,SAAS,QAAQ,CAAC,IAAY;QAC5B,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QAEpD,8BAA8B;QAC9B,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC;QAE3C,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC5C,MAAM,KAAK,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC;QACvC,IAAI,KAAK,IAAI,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YACpD,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO,CAAC,GAA0B,EAAE,EAAE,CAAC,CAAC,IAA8B,EAAE,EAAE,CAAC,CAAC,MAAe,EAAE,EAAE;QAC7F,IAAI,QAAQ,CAAC,MAAM,CAAC,IAAI,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;YAC9C,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;gBACrC,IAAI,CAAC;oBACH,WAAW,EAAE,OAAO;oBACpB,UAAU,EAAE,MAAM;oBAClB,GAAG,MAAM;iBACV,CAAC,CAAC;YACL,CAAC,CAAC,CAAC;QACL,CAAC;QACD,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC;IACtB,CAAC,CAAC;AACJ,CAAC;AAED,eAAe,uBAAuB,CAAC"}
|
package/lib/redux/createApp.js
CHANGED
|
@@ -1,28 +1,69 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createSlice, combineReducers } from '@reduxjs/toolkit';
|
|
1
|
+
import { configureStore, createSlice, combineReducers } from '@reduxjs/toolkit';
|
|
3
2
|
import createSagaMiddleware from 'redux-saga';
|
|
4
3
|
import { call, put, select, takeEvery, putResolve } from 'redux-saga/effects';
|
|
5
|
-
import win from 'global/window';
|
|
6
4
|
import saveState from './defaultReducer';
|
|
7
5
|
import createPromiseMiddleware from './PromiseMiddleware';
|
|
6
|
+
// 提取公共的 Effect 工具对象,避免在循环中重复创建
|
|
7
|
+
const opFun = { call, put, putResolve, select };
|
|
8
|
+
/**
|
|
9
|
+
* 统一处理 Effect 的 Generator 函数
|
|
10
|
+
* 提取到外部以减少闭包创建,提升性能
|
|
11
|
+
*/
|
|
12
|
+
function* handleEffect(modelName, effectFunc, tools, action) {
|
|
13
|
+
// 开始异步任务设置loading状态
|
|
14
|
+
yield putResolve({ type: `${modelName}/saveState`, payload: { loading: true } });
|
|
15
|
+
// 执行 Effect (支持 Generator 或 Promise)
|
|
16
|
+
const ret = yield call(effectFunc, action, tools);
|
|
17
|
+
// 结束异步任务关闭loading状态
|
|
18
|
+
yield putResolve({ type: `${modelName}/saveState`, payload: { loading: false } });
|
|
19
|
+
// 处理 PromiseMiddleware 的回调
|
|
20
|
+
if (action._dy_resolve) {
|
|
21
|
+
action._dy_resolve(ret);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
8
24
|
/**
|
|
9
25
|
* 获取注册model函数
|
|
10
|
-
* @param store redux store
|
|
11
|
-
* @param registedModel 已注册model, 对象, 属性为model名称, value为model
|
|
12
|
-
* @param allReducers 所有的reducers
|
|
13
|
-
* @param sagaMiddleware saga中间件
|
|
14
|
-
* @returns 返回function regist(model)
|
|
15
26
|
*/
|
|
16
|
-
function getRegistModelFunc(store, registedModel, allReducers, sagaMiddleware) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
function getRegistModelFunc(store, registedModel, allReducers, sagaMiddleware, runningEffects) {
|
|
28
|
+
// 提取的公共模型安装逻辑
|
|
29
|
+
function installModel(model) {
|
|
30
|
+
// 初始化模型属性
|
|
31
|
+
if (!model.state)
|
|
32
|
+
model.state = {};
|
|
33
|
+
model.initialState = model.state;
|
|
34
|
+
if (!model.reducers)
|
|
35
|
+
model.reducers = {};
|
|
36
|
+
// 注入默认的 saveState reducer
|
|
37
|
+
if (!model.reducers.saveState)
|
|
38
|
+
model.reducers.saveState = saveState;
|
|
39
|
+
if (!model.effects)
|
|
40
|
+
model.effects = {};
|
|
41
|
+
// 使用 Redux Toolkit 创建 Slice
|
|
42
|
+
const modelSlice = createSlice({
|
|
43
|
+
name: model.name,
|
|
44
|
+
initialState: model.initialState,
|
|
45
|
+
reducers: model.reducers
|
|
46
|
+
});
|
|
47
|
+
// 注册 Reducer
|
|
48
|
+
allReducers[model.name] = modelSlice.reducer;
|
|
49
|
+
registedModel[model.name] = model;
|
|
50
|
+
// 动态更新 Store 的 Reducers
|
|
51
|
+
const newReducer = combineReducers(allReducers);
|
|
52
|
+
store.replaceReducer(newReducer);
|
|
53
|
+
// 注册 Effects
|
|
54
|
+
runningEffects[model.name] = [];
|
|
55
|
+
for (const effectKey in model.effects) {
|
|
56
|
+
const type = `${model.name}/${effectKey}`;
|
|
57
|
+
console.log('[ez-saga] regist effect:', type);
|
|
58
|
+
const effectFunc = model.effects[effectKey];
|
|
59
|
+
const task = sagaMiddleware.run(function* () {
|
|
60
|
+
// 使用 takeEvery 的参数传递功能,将上下文传入 handleEffect
|
|
61
|
+
// handleEffect(modelName, effectFunc, tools, action)
|
|
62
|
+
yield takeEvery(type, handleEffect, model.name, effectFunc, opFun);
|
|
63
|
+
});
|
|
64
|
+
runningEffects[model.name].push(task);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
26
67
|
return function regist(reduxModel) {
|
|
27
68
|
const model = {
|
|
28
69
|
...reduxModel,
|
|
@@ -31,76 +72,52 @@ function getRegistModelFunc(store, registedModel, allReducers, sagaMiddleware) {
|
|
|
31
72
|
if (registedModel[model.name]) {
|
|
32
73
|
return;
|
|
33
74
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
const modelSlice = createSlice(model);
|
|
48
|
-
const reducer = modelSlice.reducer;
|
|
49
|
-
allReducers[model.name] = reducer;
|
|
50
|
-
registedModel[model.name] = model;
|
|
51
|
-
//获得一个新的reducer, 将所有的reducer整合成一个
|
|
52
|
-
let newReducer = combineReducers(allReducers);
|
|
53
|
-
store.replaceReducer(newReducer);
|
|
54
|
-
//注册effects
|
|
55
|
-
for (let effect in model.effects) {
|
|
56
|
-
let type = `${model.name}/${effect}`;
|
|
57
|
-
let execFun = model.effects[effect];
|
|
58
|
-
function* loading(opFun, action) {
|
|
59
|
-
// 开始异步任务设置loading状态
|
|
60
|
-
yield putResolve({ type: `${model.name}/saveState`, payload: { loading: true } });
|
|
61
|
-
let ret = yield call(execFun, action, opFun);
|
|
62
|
-
// 结束异步任务关闭loading状态
|
|
63
|
-
yield putResolve({ type: `${model.name}/saveState`, payload: { loading: false } });
|
|
64
|
-
if (action._dy_resolve) {
|
|
65
|
-
action._dy_resolve(ret);
|
|
75
|
+
// 安装模型
|
|
76
|
+
installModel(model);
|
|
77
|
+
// 开发环境:注册热更新事件监听
|
|
78
|
+
if (process.env.NODE_ENV !== 'production' && typeof window !== 'undefined') {
|
|
79
|
+
const eventName = `EZ_SAGA_UPDATE_${model.name}`;
|
|
80
|
+
const hmrHandler = (e) => {
|
|
81
|
+
const newModel = e.detail;
|
|
82
|
+
console.log('[ez-saga] Hot updating model:', newModel.name);
|
|
83
|
+
// 1. 取消旧任务
|
|
84
|
+
const tasks = runningEffects[model.name];
|
|
85
|
+
if (tasks && tasks.length) {
|
|
86
|
+
tasks.forEach(task => task.cancel());
|
|
66
87
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
sagaMiddleware.run(runEffect);
|
|
88
|
+
runningEffects[model.name] = [];
|
|
89
|
+
// 2. 安装新模型 (包含更新 Reducer 和重启 Effects)
|
|
90
|
+
installModel(newModel);
|
|
91
|
+
};
|
|
92
|
+
window.addEventListener(eventName, hmrHandler);
|
|
73
93
|
}
|
|
74
94
|
};
|
|
75
95
|
}
|
|
76
96
|
/** 创建store */
|
|
77
97
|
export default function create() {
|
|
78
|
-
//已经注册的reducer, key是名字, value是reducer
|
|
79
98
|
const allReducers = {};
|
|
80
|
-
//已注册model
|
|
81
99
|
const registedModel = {};
|
|
82
100
|
const sagaMiddleware = createSagaMiddleware();
|
|
83
101
|
const promiseMiddleware = createPromiseMiddleware(registedModel);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
regist
|
|
102
|
+
// 使用 configureStore 替代 createStore
|
|
103
|
+
// 自动集成 Redux DevTools,自动组合中间件
|
|
104
|
+
const store = configureStore({
|
|
105
|
+
reducer: saveState, // 初始 Reducer
|
|
106
|
+
middleware: (getDefaultMiddleware) => getDefaultMiddleware({
|
|
107
|
+
thunk: false, // 禁用默认的 thunk,因为我们使用 saga
|
|
108
|
+
serializableCheck: false, // 禁用序列化检查,因为 action 中可能包含回调函数 (_dy_resolve)
|
|
109
|
+
immutableCheck: false // 禁用不可变检查,提升开发环境性能
|
|
110
|
+
}).concat(promiseMiddleware, sagaMiddleware),
|
|
111
|
+
devTools: process.env.NODE_ENV !== 'production',
|
|
112
|
+
preloadedState: {}
|
|
113
|
+
});
|
|
114
|
+
// 存储运行中的 Effect 任务,用于热更新取消
|
|
115
|
+
const runningEffects = {};
|
|
116
|
+
const regist = getRegistModelFunc(store, registedModel, allReducers, sagaMiddleware, runningEffects);
|
|
117
|
+
return {
|
|
118
|
+
store,
|
|
119
|
+
sagaMiddleware,
|
|
120
|
+
regist
|
|
103
121
|
};
|
|
104
|
-
return app;
|
|
105
122
|
}
|
|
106
123
|
//# sourceMappingURL=createApp.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createApp.js","sourceRoot":"","sources":["../../src/redux/createApp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"createApp.js","sourceRoot":"","sources":["../../src/redux/createApp.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Plugin } from 'vite';
|
|
2
|
+
/**
|
|
3
|
+
* ez-saga Vite 插件
|
|
4
|
+
*
|
|
5
|
+
* 自动为 ez-saga model 文件注入 HMR 代码。
|
|
6
|
+
* 约定:
|
|
7
|
+
* 1. Model 文件必须使用 export default 导出对象
|
|
8
|
+
* 2. 导出的对象必须包含 'name' 属性 (作为 Model 的唯一标识)
|
|
9
|
+
* 3. 项目中必须将 app 实例挂载到 window.app
|
|
10
|
+
*/
|
|
11
|
+
export default function ezSagaHmr(): Plugin;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import MagicString from 'magic-string';
|
|
2
|
+
/**
|
|
3
|
+
* ez-saga Vite 插件
|
|
4
|
+
*
|
|
5
|
+
* 自动为 ez-saga model 文件注入 HMR 代码。
|
|
6
|
+
* 约定:
|
|
7
|
+
* 1. Model 文件必须使用 export default 导出对象
|
|
8
|
+
* 2. 导出的对象必须包含 'name' 属性 (作为 Model 的唯一标识)
|
|
9
|
+
* 3. 项目中必须将 app 实例挂载到 window.app
|
|
10
|
+
*/
|
|
11
|
+
export default function ezSagaHmr() {
|
|
12
|
+
return {
|
|
13
|
+
name: 'vite-plugin-ez-saga-hmr',
|
|
14
|
+
apply: 'serve', // 仅在开发环境(serve)模式下应用
|
|
15
|
+
transform(code, id) {
|
|
16
|
+
// 过滤掉 node_modules 和非 JS/TS 文件
|
|
17
|
+
if (id.includes('node_modules') || !/\.(js|ts|tsx|jsx)$/.test(id)) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
// 简单的特征检测:检查是否包含 ez-saga model 的关键属性
|
|
21
|
+
// 必须包含 name 属性,且通常包含 effects 或 reducers 或 state
|
|
22
|
+
// 宽容匹配:
|
|
23
|
+
// 1. export default ... (可能是对象字面量,也可能是变量)
|
|
24
|
+
// 2. name: ... (可能是字符串字面量,也可能是变量)
|
|
25
|
+
const hasDefaultExport = /export\s+default/.test(code);
|
|
26
|
+
const hasNameProp = /name\s*:/.test(code);
|
|
27
|
+
const hasModelKeywords = /(effects|reducers|state|initialState)\s*:\s*\{/.test(code);
|
|
28
|
+
if (hasDefaultExport && hasNameProp && hasModelKeywords) {
|
|
29
|
+
// console.log('ez-saga HMR: Injecting HMR code into', id);
|
|
30
|
+
// 注入 HMR 代码
|
|
31
|
+
// 使用 window 事件通信,解耦 app 实例引用
|
|
32
|
+
const hmrCode = `
|
|
33
|
+
if (import.meta.hot) {
|
|
34
|
+
import.meta.hot.accept((newModule) => {
|
|
35
|
+
if (newModule && newModule.default) {
|
|
36
|
+
const model = newModule.default;
|
|
37
|
+
if (model.name) {
|
|
38
|
+
// console.log('[ez-saga] Hot updating model:', model.name);
|
|
39
|
+
if (window && window.dispatchEvent) {
|
|
40
|
+
window.dispatchEvent(new CustomEvent('EZ_SAGA_UPDATE_' + model.name, {
|
|
41
|
+
detail: model
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
`;
|
|
49
|
+
const s = new MagicString(code);
|
|
50
|
+
s.append(hmrCode);
|
|
51
|
+
return {
|
|
52
|
+
code: s.toString(),
|
|
53
|
+
map: s.generateMap({ hires: true })
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/vite/index.ts"],"names":[],"mappings":"AACA,OAAO,WAAW,MAAM,cAAc,CAAC;AAEvC;;;;;;;;GAQG;AAEH,MAAM,CAAC,OAAO,UAAU,SAAS;IAC7B,OAAO;QACH,IAAI,EAAE,yBAAyB;QAC/B,KAAK,EAAE,OAAO,EAAE,qBAAqB;QACrC,SAAS,CAAC,IAAI,EAAE,EAAE;YACd,+BAA+B;YAC/B,IAAI,EAAE,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;gBAChE,OAAO;YACX,CAAC;YACD,qCAAqC;YACrC,gDAAgD;YAChD,QAAQ;YACR,0CAA0C;YAC1C,kCAAkC;YAClC,MAAM,gBAAgB,GAAG,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACvD,MAAM,WAAW,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC1C,MAAM,gBAAgB,GAAG,gDAAgD,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAErF,IAAI,gBAAgB,IAAI,WAAW,IAAI,gBAAgB,EAAE,CAAC;gBACtD,2DAA2D;gBAC3D,YAAY;gBACZ,6BAA6B;gBAC7B,MAAM,OAAO,GAAG;;;;;;;;;;;;;;;;CAgB/B,CAAC;gBACc,MAAM,CAAC,GAAG,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC;gBAChC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;gBAElB,OAAO;oBACH,IAAI,EAAE,CAAC,CAAC,QAAQ,EAAE;oBAClB,GAAG,EAAE,CAAC,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;iBACtC,CAAC;YACN,CAAC;QACL,CAAC;KACJ,CAAC;AACN,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
// ESM 环境下模拟 __dirname
|
|
4
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
5
|
+
const __dirname = path.dirname(__filename);
|
|
6
|
+
/**
|
|
7
|
+
* ez-saga Webpack Plugin (Webpack 5+)
|
|
8
|
+
*
|
|
9
|
+
* 自动配置 ez-saga-loader,实现 Model 热更新。
|
|
10
|
+
*/
|
|
11
|
+
export default class EzSagaWebpackPlugin {
|
|
12
|
+
apply(compiler) {
|
|
13
|
+
// 获取 loader 的绝对路径
|
|
14
|
+
const loaderPath = path.resolve(__dirname, './loader.js');
|
|
15
|
+
compiler.hooks.afterEnvironment.tap('EzSagaWebpackPlugin', () => {
|
|
16
|
+
// 注入 loader规则
|
|
17
|
+
compiler.options.module.rules.push({
|
|
18
|
+
test: /\.(js|ts|tsx|jsx)$/,
|
|
19
|
+
exclude: /node_modules/,
|
|
20
|
+
enforce: 'post', // 在编译后执行,避免影响 ts-loader 输入
|
|
21
|
+
use: [{
|
|
22
|
+
loader: loaderPath
|
|
23
|
+
}]
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/webpack/index.ts"],"names":[],"mappings":"AACA,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAEpC,sBAAsB;AACtB,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;AAE3C;;;;GAIG;AACH,MAAM,CAAC,OAAO,OAAO,mBAAmB;IACpC,KAAK,CAAC,QAAkB;QACpB,kBAAkB;QAClB,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;QAE1D,QAAQ,CAAC,KAAK,CAAC,gBAAgB,CAAC,GAAG,CAAC,qBAAqB,EAAE,GAAG,EAAE;YAC5D,cAAc;YACd,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC;gBAC/B,IAAI,EAAE,oBAAoB;gBAC1B,OAAO,EAAE,cAAc;gBACvB,OAAO,EAAE,MAAM,EAAE,2BAA2B;gBAC5C,GAAG,EAAE,CAAC;wBACF,MAAM,EAAE,UAAU;qBACrB,CAAC;aACL,CAAC,CAAC;QACP,CAAC,CAAC,CAAC;IACP,CAAC;CACJ"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import MagicString from 'magic-string';
|
|
2
|
+
/**
|
|
3
|
+
* ez-saga Webpack Loader
|
|
4
|
+
* 对应 src/vite/index.ts 的逻辑,用于 Webpack 场景
|
|
5
|
+
*/
|
|
6
|
+
export default function (source) {
|
|
7
|
+
// 简单的特征检测
|
|
8
|
+
const hasDefaultExport = /export\s+default/.test(source) || /exports\.default\s*=/.test(source) || /module\.exports\s*=/.test(source);
|
|
9
|
+
// name属性检查 (宽松匹配: name: "foo" 或 name = "foo")
|
|
10
|
+
const hasNameProp = /(name\s*[:=])/.test(source);
|
|
11
|
+
// 关键字检查 (Loose due to compilation)
|
|
12
|
+
const hasModelKeywords = /(effects|reducers|state|initialState)\s*[:=]\s*\{/.test(source);
|
|
13
|
+
if (hasDefaultExport && hasNameProp && hasModelKeywords) {
|
|
14
|
+
const callback = this.async();
|
|
15
|
+
const s = new MagicString(source);
|
|
16
|
+
// Regular expressions to detect export patterns
|
|
17
|
+
const esmRegex = /export\s+default/;
|
|
18
|
+
const cjsDefaultRegex = /exports\.default\s*=/;
|
|
19
|
+
const cjsModuleRegex = /module\.exports\s*=/;
|
|
20
|
+
let match;
|
|
21
|
+
const modelVarName = '__ez_saga_model_def';
|
|
22
|
+
if ((match = esmRegex.exec(source)) !== null) {
|
|
23
|
+
// ESM: export default { ... }
|
|
24
|
+
const start = match.index;
|
|
25
|
+
const end = start + match[0].length;
|
|
26
|
+
s.overwrite(start, end, `const ${modelVarName} =`);
|
|
27
|
+
s.append(`\nexport default ${modelVarName};\n`);
|
|
28
|
+
}
|
|
29
|
+
else if ((match = cjsDefaultRegex.exec(source)) !== null) {
|
|
30
|
+
// CJS: exports.default = { ... }
|
|
31
|
+
const start = match.index;
|
|
32
|
+
const end = start + match[0].length;
|
|
33
|
+
s.overwrite(start, end, `const ${modelVarName} =`);
|
|
34
|
+
s.append(`\nexports.default = ${modelVarName};\n`);
|
|
35
|
+
}
|
|
36
|
+
else if ((match = cjsModuleRegex.exec(source)) !== null) {
|
|
37
|
+
// CJS: module.exports = { ... }
|
|
38
|
+
const start = match.index;
|
|
39
|
+
const end = start + match[0].length;
|
|
40
|
+
s.overwrite(start, end, `const ${modelVarName} =`);
|
|
41
|
+
s.append(`\nmodule.exports = ${modelVarName};\n`);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
// 匹配失败
|
|
45
|
+
this.callback(null, source);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// 注入 HMR 逻辑
|
|
49
|
+
const hmrCode = `
|
|
50
|
+
if (module.hot) {
|
|
51
|
+
module.hot.accept();
|
|
52
|
+
if (${modelVarName} && ${modelVarName}.name) {
|
|
53
|
+
if (typeof window !== 'undefined' && window.dispatchEvent) {
|
|
54
|
+
window.dispatchEvent(new CustomEvent('EZ_SAGA_UPDATE_' + ${modelVarName}.name, {
|
|
55
|
+
detail: ${modelVarName}
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
`;
|
|
61
|
+
s.append(hmrCode);
|
|
62
|
+
// 禁用 SourceMap
|
|
63
|
+
callback(null, s.toString());
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
this.callback(null, source);
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.js","sourceRoot":"","sources":["../../src/webpack/loader.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ez-saga",
|
|
3
|
-
"version": "18.0.
|
|
3
|
+
"version": "18.0.7",
|
|
4
4
|
"description": "The ez-saga project is a project that imitates dva-js",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js",
|
|
9
|
+
"./vite": "./lib/vite/index.js",
|
|
10
|
+
"./webpack": "./lib/webpack/index.js",
|
|
11
|
+
"./package.json": "./package.json"
|
|
12
|
+
},
|
|
13
|
+
"typesVersions": {
|
|
14
|
+
"*": {
|
|
15
|
+
"vite": [
|
|
16
|
+
"lib/vite/index.d.ts"
|
|
17
|
+
],
|
|
18
|
+
"webpack": [
|
|
19
|
+
"lib/webpack/index.d.ts"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
},
|
|
7
23
|
"sideEffects": false,
|
|
8
24
|
"repository": {
|
|
9
25
|
"type": "git",
|
|
@@ -29,34 +45,22 @@
|
|
|
29
45
|
},
|
|
30
46
|
"dependencies": {
|
|
31
47
|
"@reduxjs/toolkit": "^2.11.2",
|
|
48
|
+
"magic-string": "^0.30.21",
|
|
32
49
|
"redux": "^5.0.1",
|
|
33
50
|
"redux-saga": "^1.4.2",
|
|
34
|
-
"
|
|
51
|
+
"tslib": "^2.8.1"
|
|
35
52
|
},
|
|
36
|
-
"peerDependencies": {},
|
|
37
53
|
"devDependencies": {
|
|
38
|
-
"@
|
|
39
|
-
"@babel/eslint-parser": "^7.23.3",
|
|
40
|
-
"@babel/plugin-proposal-decorators": "^7.23.7",
|
|
41
|
-
"@babel/plugin-syntax-flow": "^7.23.3",
|
|
42
|
-
"@babel/plugin-transform-runtime": "^7.23.7",
|
|
43
|
-
"@babel/preset-env": "^7.23.8",
|
|
44
|
-
"@babel/preset-flow": "^7.23.3",
|
|
45
|
-
"@babel/preset-typescript": "^7.23.3",
|
|
46
|
-
"@babel/runtime-corejs3": "^7.23.8",
|
|
47
|
-
"@babel/plugin-transform-class-properties": "^7.23.3",
|
|
48
|
-
"@babel/plugin-transform-private-methods": "^7.23.3",
|
|
49
|
-
"@babel/plugin-transform-private-property-in-object": "^7.23.4",
|
|
50
|
-
"cross-env": "^7.0.3",
|
|
51
|
-
"eslint": "^8.56.0",
|
|
52
|
-
"eslint-plugin-flowtype": "^8.0.3",
|
|
53
|
-
"eslint-plugin-import": "^2.29.1",
|
|
54
|
+
"@types/node": "^25.0.3",
|
|
54
55
|
"eslint-plugin-node": "^11.1.0",
|
|
55
56
|
"eslint-plugin-standard": "^4.1.0",
|
|
56
|
-
"
|
|
57
|
+
"rimraf": "^6.1.2",
|
|
58
|
+
"typescript": "^5.3.3",
|
|
59
|
+
"vite": "^7.3.0",
|
|
60
|
+
"webpack": "^5.104.1"
|
|
57
61
|
},
|
|
58
62
|
"engines": {
|
|
59
|
-
"node": ">=
|
|
63
|
+
"node": ">= 18.0.0",
|
|
60
64
|
"npm": ">= 6.0.0"
|
|
61
65
|
},
|
|
62
66
|
"browserslist": [
|
|
@@ -68,7 +72,6 @@
|
|
|
68
72
|
"files": [
|
|
69
73
|
"lib",
|
|
70
74
|
"src",
|
|
71
|
-
"index.js",
|
|
72
75
|
"tsconfig.json",
|
|
73
76
|
"typing.d.ts"
|
|
74
77
|
]
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import { Dispatch, Middleware, MiddlewareAPI, Action } from 'redux';
|
|
1
|
+
import { Dispatch, Middleware, MiddlewareAPI, Action, UnknownAction } from 'redux';
|
|
2
2
|
import { RegistedModel } from './typeDeclare';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* 判断是否为标准Action
|
|
6
|
+
*/
|
|
7
|
+
function isAction(action: unknown): action is Action<string> {
|
|
8
|
+
return typeof action === 'object' && action !== null && 'type' in action;
|
|
9
|
+
}
|
|
10
|
+
|
|
4
11
|
/**
|
|
5
12
|
* 创建中间层
|
|
6
13
|
* @param registedModel 已注册model
|
|
@@ -8,33 +15,29 @@ import { RegistedModel } from './typeDeclare';
|
|
|
8
15
|
function createPromiseMiddleware<D extends Dispatch>(registedModel: RegistedModel): Middleware<any, any, any> {
|
|
9
16
|
function isEffect(type: string) {
|
|
10
17
|
if (!type || typeof type !== 'string') return false;
|
|
18
|
+
|
|
19
|
+
// 性能优化:快速排除不包含 '/' 的普通 Action
|
|
20
|
+
if (type.indexOf('/') === -1) return false;
|
|
21
|
+
|
|
11
22
|
const [modelName, effect] = type.split('/');
|
|
12
23
|
const model = registedModel[modelName];
|
|
13
|
-
if (model) {
|
|
14
|
-
|
|
15
|
-
return true;
|
|
16
|
-
}
|
|
24
|
+
if (model && model.effects && model.effects[effect]) {
|
|
25
|
+
return true;
|
|
17
26
|
}
|
|
18
27
|
return false;
|
|
19
28
|
}
|
|
20
29
|
|
|
21
30
|
return (api: MiddlewareAPI<D, any>) => (next: (action: unknown) => any) => (action: unknown) => {
|
|
22
|
-
|
|
23
|
-
if ((action as Action)?.type !== undefined) {
|
|
24
|
-
const { type } = action as Action;
|
|
25
|
-
exeEffect = isEffect(type);
|
|
26
|
-
}
|
|
27
|
-
if (exeEffect) {
|
|
31
|
+
if (isAction(action) && isEffect(action.type)) {
|
|
28
32
|
return new Promise((resolve, reject) => {
|
|
29
33
|
next({
|
|
30
34
|
_dy_resolve: resolve,
|
|
31
35
|
_dy_reject: reject,
|
|
32
|
-
...action
|
|
36
|
+
...action,
|
|
33
37
|
});
|
|
34
38
|
});
|
|
35
|
-
} else {
|
|
36
|
-
return next(action);
|
|
37
39
|
}
|
|
40
|
+
return next(action);
|
|
38
41
|
};
|
|
39
42
|
}
|
|
40
43
|
|
package/src/redux/createApp.ts
CHANGED
|
@@ -1,126 +1,152 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import createSagaMiddleware, { SagaMiddleware } from 'redux-saga';
|
|
1
|
+
import { configureStore, createSlice, combineReducers } from '@reduxjs/toolkit';
|
|
2
|
+
import createSagaMiddleware, { SagaMiddleware, Task } from 'redux-saga';
|
|
4
3
|
import { call, put, select, takeEvery, putResolve } from 'redux-saga/effects';
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import { ReduxModel, ReduxApp, RegistedModel, ReduxSagaModel, EffectTool, PayloadAction } from './typeDeclare';
|
|
4
|
+
import { Reducer, Action, Store } from 'redux';
|
|
5
|
+
import { ReduxModel, ReduxApp, RegistedModel, ReduxSagaModel, EffectTool, PayloadAction, Effect } from './typeDeclare';
|
|
8
6
|
import saveState from './defaultReducer';
|
|
9
7
|
import createPromiseMiddleware from './PromiseMiddleware';
|
|
10
8
|
|
|
9
|
+
// 提取公共的 Effect 工具对象,避免在循环中重复创建
|
|
10
|
+
const opFun: EffectTool = { call, put, putResolve, select };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 统一处理 Effect 的 Generator 函数
|
|
14
|
+
* 提取到外部以减少闭包创建,提升性能
|
|
15
|
+
*/
|
|
16
|
+
function* handleEffect(
|
|
17
|
+
modelName: string,
|
|
18
|
+
effectFunc: Effect,
|
|
19
|
+
tools: EffectTool,
|
|
20
|
+
action: PayloadAction
|
|
21
|
+
) {
|
|
22
|
+
// 开始异步任务设置loading状态
|
|
23
|
+
yield putResolve({ type: `${modelName}/saveState`, payload: { loading: true } });
|
|
24
|
+
|
|
25
|
+
// 执行 Effect (支持 Generator 或 Promise)
|
|
26
|
+
const ret: any = yield call(effectFunc, action, tools);
|
|
27
|
+
|
|
28
|
+
// 结束异步任务关闭loading状态
|
|
29
|
+
yield putResolve({ type: `${modelName}/saveState`, payload: { loading: false } });
|
|
30
|
+
|
|
31
|
+
// 处理 PromiseMiddleware 的回调
|
|
32
|
+
if (action._dy_resolve) {
|
|
33
|
+
action._dy_resolve(ret);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
11
37
|
/**
|
|
12
38
|
* 获取注册model函数
|
|
13
|
-
* @param store redux store
|
|
14
|
-
* @param registedModel 已注册model, 对象, 属性为model名称, value为model
|
|
15
|
-
* @param allReducers 所有的reducers
|
|
16
|
-
* @param sagaMiddleware saga中间件
|
|
17
|
-
* @returns 返回function regist(model)
|
|
18
39
|
*/
|
|
19
|
-
function getRegistModelFunc(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
40
|
+
function getRegistModelFunc(
|
|
41
|
+
store: Store<any, Action<string>>,
|
|
42
|
+
registedModel: RegistedModel,
|
|
43
|
+
allReducers: { [x: string]: Reducer<any, any>; },
|
|
44
|
+
sagaMiddleware: SagaMiddleware<object>,
|
|
45
|
+
runningEffects: Record<string, Task[]>
|
|
46
|
+
): (model: ReduxModel) => void {
|
|
47
|
+
// 提取的公共模型安装逻辑
|
|
48
|
+
function installModel(model: ReduxSagaModel) {
|
|
49
|
+
// 初始化模型属性
|
|
50
|
+
if (!model.state) model.state = {};
|
|
51
|
+
model.initialState = model.state;
|
|
52
|
+
if (!model.reducers) model.reducers = {};
|
|
53
|
+
// 注入默认的 saveState reducer
|
|
54
|
+
if (!model.reducers.saveState) model.reducers.saveState = saveState;
|
|
55
|
+
if (!model.effects) model.effects = {};
|
|
56
|
+
|
|
57
|
+
// 使用 Redux Toolkit 创建 Slice
|
|
58
|
+
const modelSlice = createSlice({
|
|
59
|
+
name: model.name,
|
|
60
|
+
initialState: model.initialState,
|
|
61
|
+
reducers: model.reducers as any
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// 注册 Reducer
|
|
65
|
+
allReducers[model.name] = modelSlice.reducer;
|
|
66
|
+
registedModel[model.name] = model;
|
|
67
|
+
|
|
68
|
+
// 动态更新 Store 的 Reducers
|
|
69
|
+
const newReducer = combineReducers(allReducers);
|
|
70
|
+
store.replaceReducer(newReducer);
|
|
71
|
+
|
|
72
|
+
// 注册 Effects
|
|
73
|
+
runningEffects[model.name] = [];
|
|
74
|
+
for (const effectKey in model.effects) {
|
|
75
|
+
const type = `${model.name}/${effectKey}`;
|
|
76
|
+
console.log('[ez-saga] regist effect:', type);
|
|
77
|
+
const effectFunc = model.effects[effectKey];
|
|
78
|
+
const task = sagaMiddleware.run(function* () {
|
|
79
|
+
// 使用 takeEvery 的参数传递功能,将上下文传入 handleEffect
|
|
80
|
+
// handleEffect(modelName, effectFunc, tools, action)
|
|
81
|
+
yield takeEvery(type, handleEffect, model.name, effectFunc, opFun);
|
|
82
|
+
});
|
|
83
|
+
runningEffects[model.name].push(task);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
31
87
|
return function regist(reduxModel: ReduxModel): void {
|
|
32
88
|
const model = {
|
|
33
89
|
...reduxModel,
|
|
34
90
|
initialState: {}
|
|
35
91
|
} as ReduxSagaModel;
|
|
92
|
+
|
|
36
93
|
if (registedModel[model.name]) {
|
|
37
94
|
return;
|
|
38
95
|
}
|
|
39
|
-
if (!model.state) {
|
|
40
|
-
model.state = {};
|
|
41
|
-
}
|
|
42
|
-
model.initialState = model.state;
|
|
43
|
-
if (!model.reducers) {
|
|
44
|
-
model.reducers = {};
|
|
45
|
-
}
|
|
46
|
-
if (!model.reducers.saveState) {
|
|
47
|
-
model.reducers.saveState = saveState;
|
|
48
|
-
}
|
|
49
|
-
if (!model.effects) {
|
|
50
|
-
model.effects = {};
|
|
51
|
-
}
|
|
52
|
-
const modelSlice = createSlice(model);
|
|
53
|
-
const reducer = modelSlice.reducer;
|
|
54
|
-
allReducers[model.name] = reducer;
|
|
55
|
-
registedModel[model.name] = model;
|
|
56
|
-
//获得一个新的reducer, 将所有的reducer整合成一个
|
|
57
|
-
let newReducer = combineReducers(allReducers);
|
|
58
|
-
store.replaceReducer(newReducer);
|
|
59
|
-
//注册effects
|
|
60
|
-
for (let effect in model.effects) {
|
|
61
|
-
let type: string = `${model.name}/${effect}`;
|
|
62
|
-
let execFun = model.effects[effect];
|
|
63
|
-
function* loading(opFun: EffectTool, action: PayloadAction) {
|
|
64
|
-
// 开始异步任务设置loading状态
|
|
65
|
-
yield putResolve({ type: `${model.name}/saveState`, payload: { loading: true } });
|
|
66
|
-
let ret = yield call(execFun, action, opFun);
|
|
67
|
-
// 结束异步任务关闭loading状态
|
|
68
|
-
yield putResolve({ type: `${model.name}/saveState`, payload: { loading: false } });
|
|
69
|
-
if (action._dy_resolve) {
|
|
70
|
-
action._dy_resolve(ret);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
96
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
yield takeEvery(type, loading, { call, put, putResolve, select });
|
|
77
|
-
}
|
|
97
|
+
// 安装模型
|
|
98
|
+
installModel(model);
|
|
78
99
|
|
|
79
|
-
|
|
100
|
+
// 开发环境:注册热更新事件监听
|
|
101
|
+
if (process.env.NODE_ENV !== 'production' && typeof window !== 'undefined') {
|
|
102
|
+
const eventName = `EZ_SAGA_UPDATE_${model.name}`;
|
|
103
|
+
const hmrHandler = (e: any) => {
|
|
104
|
+
const newModel = e.detail as ReduxSagaModel;
|
|
105
|
+
console.log('[ez-saga] Hot updating model:', newModel.name);
|
|
106
|
+
// 1. 取消旧任务
|
|
107
|
+
const tasks = runningEffects[model.name];
|
|
108
|
+
if (tasks && tasks.length) {
|
|
109
|
+
tasks.forEach(task => task.cancel());
|
|
110
|
+
}
|
|
111
|
+
runningEffects[model.name] = [];
|
|
112
|
+
// 2. 安装新模型 (包含更新 Reducer 和重启 Effects)
|
|
113
|
+
installModel(newModel);
|
|
114
|
+
};
|
|
115
|
+
window.addEventListener(eventName, hmrHandler);
|
|
80
116
|
}
|
|
81
117
|
};
|
|
82
118
|
}
|
|
83
119
|
|
|
84
|
-
|
|
85
120
|
/** 创建store */
|
|
86
121
|
export default function create(): ReduxApp {
|
|
87
|
-
//已经注册的reducer, key是名字, value是reducer
|
|
88
122
|
const allReducers = {};
|
|
89
|
-
//已注册model
|
|
90
123
|
const registedModel: RegistedModel = {};
|
|
91
124
|
|
|
92
125
|
const sagaMiddleware = createSagaMiddleware();
|
|
93
|
-
|
|
94
126
|
const promiseMiddleware = createPromiseMiddleware(registedModel);
|
|
95
127
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
store: store,
|
|
120
|
-
/** saga中间件 */
|
|
121
|
-
sagaMiddleware: sagaMiddleware,
|
|
122
|
-
/** model注册函数 */
|
|
123
|
-
regist: regist
|
|
128
|
+
// 使用 configureStore 替代 createStore
|
|
129
|
+
// 自动集成 Redux DevTools,自动组合中间件
|
|
130
|
+
const store = configureStore({
|
|
131
|
+
reducer: saveState, // 初始 Reducer
|
|
132
|
+
middleware: (getDefaultMiddleware) =>
|
|
133
|
+
getDefaultMiddleware({
|
|
134
|
+
thunk: false, // 禁用默认的 thunk,因为我们使用 saga
|
|
135
|
+
serializableCheck: false, // 禁用序列化检查,因为 action 中可能包含回调函数 (_dy_resolve)
|
|
136
|
+
immutableCheck: false // 禁用不可变检查,提升开发环境性能
|
|
137
|
+
}).concat(promiseMiddleware, sagaMiddleware),
|
|
138
|
+
devTools: process.env.NODE_ENV !== 'production',
|
|
139
|
+
preloadedState: {}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// 存储运行中的 Effect 任务,用于热更新取消
|
|
143
|
+
const runningEffects: Record<string, Task[]> = {};
|
|
144
|
+
|
|
145
|
+
const regist = getRegistModelFunc(store, registedModel, allReducers, sagaMiddleware, runningEffects);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
store,
|
|
149
|
+
sagaMiddleware,
|
|
150
|
+
regist
|
|
124
151
|
};
|
|
125
|
-
return app;
|
|
126
152
|
}
|
package/src/redux/typeDeclare.ts
CHANGED
|
@@ -20,11 +20,6 @@ declare module 'redux' {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
// /** 定义AsyncDispatch, 使其兼容redux promiseMiddleware中间件修改返回结果的情况 */
|
|
24
|
-
// export interface AsyncDispatch {
|
|
25
|
-
// <R = any>(action: PayloadAction, ...extraArgs: any[]): Promise<R>;
|
|
26
|
-
// }
|
|
27
|
-
|
|
28
23
|
/** 工具 */
|
|
29
24
|
export interface EffectTool {
|
|
30
25
|
/** 调用异步函数, 并获得该异步函数的结果 */
|
|
@@ -74,7 +69,7 @@ export interface RegistedModel {
|
|
|
74
69
|
export interface ReduxApp {
|
|
75
70
|
store: Store<any, ReduxAction>,
|
|
76
71
|
sagaMiddleware: SagaMiddleware<object>;
|
|
77
|
-
regist: (model: ReduxModel) => void
|
|
72
|
+
regist: (model: ReduxModel) => void;
|
|
78
73
|
}
|
|
79
74
|
|
|
80
75
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Plugin } from 'vite';
|
|
2
|
+
import MagicString from 'magic-string';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ez-saga Vite 插件
|
|
6
|
+
*
|
|
7
|
+
* 自动为 ez-saga model 文件注入 HMR 代码。
|
|
8
|
+
* 约定:
|
|
9
|
+
* 1. Model 文件必须使用 export default 导出对象
|
|
10
|
+
* 2. 导出的对象必须包含 'name' 属性 (作为 Model 的唯一标识)
|
|
11
|
+
* 3. 项目中必须将 app 实例挂载到 window.app
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export default function ezSagaHmr(): Plugin {
|
|
15
|
+
return {
|
|
16
|
+
name: 'vite-plugin-ez-saga-hmr',
|
|
17
|
+
apply: 'serve', // 仅在开发环境(serve)模式下应用
|
|
18
|
+
transform(code, id) {
|
|
19
|
+
// 过滤掉 node_modules 和非 JS/TS 文件
|
|
20
|
+
if (id.includes('node_modules') || !/\.(js|ts|tsx|jsx)$/.test(id)) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
// 简单的特征检测:检查是否包含 ez-saga model 的关键属性
|
|
24
|
+
// 必须包含 name 属性,且通常包含 effects 或 reducers 或 state
|
|
25
|
+
// 宽容匹配:
|
|
26
|
+
// 1. export default ... (可能是对象字面量,也可能是变量)
|
|
27
|
+
// 2. name: ... (可能是字符串字面量,也可能是变量)
|
|
28
|
+
const hasDefaultExport = /export\s+default/.test(code);
|
|
29
|
+
const hasNameProp = /name\s*:/.test(code);
|
|
30
|
+
const hasModelKeywords = /(effects|reducers|state|initialState)\s*:\s*\{/.test(code);
|
|
31
|
+
|
|
32
|
+
if (hasDefaultExport && hasNameProp && hasModelKeywords) {
|
|
33
|
+
// console.log('ez-saga HMR: Injecting HMR code into', id);
|
|
34
|
+
// 注入 HMR 代码
|
|
35
|
+
// 使用 window 事件通信,解耦 app 实例引用
|
|
36
|
+
const hmrCode = `
|
|
37
|
+
if (import.meta.hot) {
|
|
38
|
+
import.meta.hot.accept((newModule) => {
|
|
39
|
+
if (newModule && newModule.default) {
|
|
40
|
+
const model = newModule.default;
|
|
41
|
+
if (model.name) {
|
|
42
|
+
// console.log('[ez-saga] Hot updating model:', model.name);
|
|
43
|
+
if (window && window.dispatchEvent) {
|
|
44
|
+
window.dispatchEvent(new CustomEvent('EZ_SAGA_UPDATE_' + model.name, {
|
|
45
|
+
detail: model
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
`;
|
|
53
|
+
const s = new MagicString(code);
|
|
54
|
+
s.append(hmrCode);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
code: s.toString(),
|
|
58
|
+
map: s.generateMap({ hires: true })
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Compiler } from 'webpack';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
// ESM 环境下模拟 __dirname
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* ez-saga Webpack Plugin (Webpack 5+)
|
|
11
|
+
*
|
|
12
|
+
* 自动配置 ez-saga-loader,实现 Model 热更新。
|
|
13
|
+
*/
|
|
14
|
+
export default class EzSagaWebpackPlugin {
|
|
15
|
+
apply(compiler: Compiler) {
|
|
16
|
+
// 获取 loader 的绝对路径
|
|
17
|
+
const loaderPath = path.resolve(__dirname, './loader.js');
|
|
18
|
+
|
|
19
|
+
compiler.hooks.afterEnvironment.tap('EzSagaWebpackPlugin', () => {
|
|
20
|
+
// 注入 loader规则
|
|
21
|
+
compiler.options.module.rules.push({
|
|
22
|
+
test: /\.(js|ts|tsx|jsx)$/,
|
|
23
|
+
exclude: /node_modules/,
|
|
24
|
+
enforce: 'post', // 在编译后执行,避免影响 ts-loader 输入
|
|
25
|
+
use: [{
|
|
26
|
+
loader: loaderPath
|
|
27
|
+
}]
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import MagicString from 'magic-string';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ez-saga Webpack Loader
|
|
5
|
+
* 对应 src/vite/index.ts 的逻辑,用于 Webpack 场景
|
|
6
|
+
*/
|
|
7
|
+
export default function (this: any, source: string) {
|
|
8
|
+
// 简单的特征检测
|
|
9
|
+
const hasDefaultExport = /export\s+default/.test(source) || /exports\.default\s*=/.test(source) || /module\.exports\s*=/.test(source);
|
|
10
|
+
// name属性检查 (宽松匹配: name: "foo" 或 name = "foo")
|
|
11
|
+
const hasNameProp = /(name\s*[:=])/.test(source);
|
|
12
|
+
// 关键字检查 (Loose due to compilation)
|
|
13
|
+
const hasModelKeywords = /(effects|reducers|state|initialState)\s*[:=]\s*\{/.test(source);
|
|
14
|
+
|
|
15
|
+
if (hasDefaultExport && hasNameProp && hasModelKeywords) {
|
|
16
|
+
const callback = this.async();
|
|
17
|
+
const s = new MagicString(source);
|
|
18
|
+
|
|
19
|
+
// Regular expressions to detect export patterns
|
|
20
|
+
const esmRegex = /export\s+default/;
|
|
21
|
+
const cjsDefaultRegex = /exports\.default\s*=/;
|
|
22
|
+
const cjsModuleRegex = /module\.exports\s*=/;
|
|
23
|
+
|
|
24
|
+
let match;
|
|
25
|
+
const modelVarName = '__ez_saga_model_def';
|
|
26
|
+
|
|
27
|
+
if ((match = esmRegex.exec(source)) !== null) {
|
|
28
|
+
// ESM: export default { ... }
|
|
29
|
+
const start = match.index;
|
|
30
|
+
const end = start + match[0].length;
|
|
31
|
+
s.overwrite(start, end, `const ${modelVarName} =`);
|
|
32
|
+
s.append(`\nexport default ${modelVarName};\n`);
|
|
33
|
+
} else if ((match = cjsDefaultRegex.exec(source)) !== null) {
|
|
34
|
+
// CJS: exports.default = { ... }
|
|
35
|
+
const start = match.index;
|
|
36
|
+
const end = start + match[0].length;
|
|
37
|
+
s.overwrite(start, end, `const ${modelVarName} =`);
|
|
38
|
+
s.append(`\nexports.default = ${modelVarName};\n`);
|
|
39
|
+
} else if ((match = cjsModuleRegex.exec(source)) !== null) {
|
|
40
|
+
// CJS: module.exports = { ... }
|
|
41
|
+
const start = match.index;
|
|
42
|
+
const end = start + match[0].length;
|
|
43
|
+
s.overwrite(start, end, `const ${modelVarName} =`);
|
|
44
|
+
s.append(`\nmodule.exports = ${modelVarName};\n`);
|
|
45
|
+
} else {
|
|
46
|
+
// 匹配失败
|
|
47
|
+
this.callback(null, source);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 注入 HMR 逻辑
|
|
52
|
+
const hmrCode = `
|
|
53
|
+
if (module.hot) {
|
|
54
|
+
module.hot.accept();
|
|
55
|
+
if (${modelVarName} && ${modelVarName}.name) {
|
|
56
|
+
if (typeof window !== 'undefined' && window.dispatchEvent) {
|
|
57
|
+
window.dispatchEvent(new CustomEvent('EZ_SAGA_UPDATE_' + ${modelVarName}.name, {
|
|
58
|
+
detail: ${modelVarName}
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
`;
|
|
64
|
+
s.append(hmrCode);
|
|
65
|
+
|
|
66
|
+
// 禁用 SourceMap
|
|
67
|
+
callback(null, s.toString());
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
this.callback(null, source);
|
|
72
|
+
}
|
package/tsconfig.json
CHANGED
package/src/global-shim.d.ts
DELETED
package/src/index.js
DELETED