ez-saga 18.0.6 → 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/createApp.js +38 -11
- 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 +21 -2
- package/src/redux/createApp.ts +45 -15
- package/src/redux/typeDeclare.ts +1 -1
- 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/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.
|
package/lib/redux/createApp.js
CHANGED
|
@@ -24,15 +24,9 @@ function* handleEffect(modelName, effectFunc, tools, action) {
|
|
|
24
24
|
/**
|
|
25
25
|
* 获取注册model函数
|
|
26
26
|
*/
|
|
27
|
-
function getRegistModelFunc(store, registedModel, allReducers, sagaMiddleware) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
...reduxModel,
|
|
31
|
-
initialState: {}
|
|
32
|
-
};
|
|
33
|
-
if (registedModel[model.name]) {
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
27
|
+
function getRegistModelFunc(store, registedModel, allReducers, sagaMiddleware, runningEffects) {
|
|
28
|
+
// 提取的公共模型安装逻辑
|
|
29
|
+
function installModel(model) {
|
|
36
30
|
// 初始化模型属性
|
|
37
31
|
if (!model.state)
|
|
38
32
|
model.state = {};
|
|
@@ -57,14 +51,45 @@ function getRegistModelFunc(store, registedModel, allReducers, sagaMiddleware) {
|
|
|
57
51
|
const newReducer = combineReducers(allReducers);
|
|
58
52
|
store.replaceReducer(newReducer);
|
|
59
53
|
// 注册 Effects
|
|
54
|
+
runningEffects[model.name] = [];
|
|
60
55
|
for (const effectKey in model.effects) {
|
|
61
56
|
const type = `${model.name}/${effectKey}`;
|
|
57
|
+
console.log('[ez-saga] regist effect:', type);
|
|
62
58
|
const effectFunc = model.effects[effectKey];
|
|
63
|
-
sagaMiddleware.run(function* () {
|
|
59
|
+
const task = sagaMiddleware.run(function* () {
|
|
64
60
|
// 使用 takeEvery 的参数传递功能,将上下文传入 handleEffect
|
|
65
61
|
// handleEffect(modelName, effectFunc, tools, action)
|
|
66
62
|
yield takeEvery(type, handleEffect, model.name, effectFunc, opFun);
|
|
67
63
|
});
|
|
64
|
+
runningEffects[model.name].push(task);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return function regist(reduxModel) {
|
|
68
|
+
const model = {
|
|
69
|
+
...reduxModel,
|
|
70
|
+
initialState: {}
|
|
71
|
+
};
|
|
72
|
+
if (registedModel[model.name]) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
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());
|
|
87
|
+
}
|
|
88
|
+
runningEffects[model.name] = [];
|
|
89
|
+
// 2. 安装新模型 (包含更新 Reducer 和重启 Effects)
|
|
90
|
+
installModel(newModel);
|
|
91
|
+
};
|
|
92
|
+
window.addEventListener(eventName, hmrHandler);
|
|
68
93
|
}
|
|
69
94
|
};
|
|
70
95
|
}
|
|
@@ -86,7 +111,9 @@ export default function create() {
|
|
|
86
111
|
devTools: process.env.NODE_ENV !== 'production',
|
|
87
112
|
preloadedState: {}
|
|
88
113
|
});
|
|
89
|
-
|
|
114
|
+
// 存储运行中的 Effect 任务,用于热更新取消
|
|
115
|
+
const runningEffects = {};
|
|
116
|
+
const regist = getRegistModelFunc(store, registedModel, allReducers, sagaMiddleware, runningEffects);
|
|
90
117
|
return {
|
|
91
118
|
store,
|
|
92
119
|
sagaMiddleware,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createApp.js","sourceRoot":"","sources":["../../src/redux/createApp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAChF,OAAO,
|
|
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,6 +45,7 @@
|
|
|
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"
|
|
@@ -38,7 +55,9 @@
|
|
|
38
55
|
"eslint-plugin-node": "^11.1.0",
|
|
39
56
|
"eslint-plugin-standard": "^4.1.0",
|
|
40
57
|
"rimraf": "^6.1.2",
|
|
41
|
-
"typescript": "^5.3.3"
|
|
58
|
+
"typescript": "^5.3.3",
|
|
59
|
+
"vite": "^7.3.0",
|
|
60
|
+
"webpack": "^5.104.1"
|
|
42
61
|
},
|
|
43
62
|
"engines": {
|
|
44
63
|
"node": ">= 18.0.0",
|
package/src/redux/createApp.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { configureStore, createSlice, combineReducers } from '@reduxjs/toolkit';
|
|
2
|
-
import createSagaMiddleware, { SagaMiddleware } from 'redux-saga';
|
|
2
|
+
import createSagaMiddleware, { SagaMiddleware, Task } from 'redux-saga';
|
|
3
3
|
import { call, put, select, takeEvery, putResolve } from 'redux-saga/effects';
|
|
4
4
|
import { Reducer, Action, Store } from 'redux';
|
|
5
5
|
import { ReduxModel, ReduxApp, RegistedModel, ReduxSagaModel, EffectTool, PayloadAction, Effect } from './typeDeclare';
|
|
@@ -41,18 +41,11 @@ function getRegistModelFunc(
|
|
|
41
41
|
store: Store<any, Action<string>>,
|
|
42
42
|
registedModel: RegistedModel,
|
|
43
43
|
allReducers: { [x: string]: Reducer<any, any>; },
|
|
44
|
-
sagaMiddleware: SagaMiddleware<object
|
|
44
|
+
sagaMiddleware: SagaMiddleware<object>,
|
|
45
|
+
runningEffects: Record<string, Task[]>
|
|
45
46
|
): (model: ReduxModel) => void {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
...reduxModel,
|
|
49
|
-
initialState: {}
|
|
50
|
-
} as ReduxSagaModel;
|
|
51
|
-
|
|
52
|
-
if (registedModel[model.name]) {
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
|
|
47
|
+
// 提取的公共模型安装逻辑
|
|
48
|
+
function installModel(model: ReduxSagaModel) {
|
|
56
49
|
// 初始化模型属性
|
|
57
50
|
if (!model.state) model.state = {};
|
|
58
51
|
model.initialState = model.state;
|
|
@@ -77,15 +70,49 @@ function getRegistModelFunc(
|
|
|
77
70
|
store.replaceReducer(newReducer);
|
|
78
71
|
|
|
79
72
|
// 注册 Effects
|
|
73
|
+
runningEffects[model.name] = [];
|
|
80
74
|
for (const effectKey in model.effects) {
|
|
81
75
|
const type = `${model.name}/${effectKey}`;
|
|
76
|
+
console.log('[ez-saga] regist effect:', type);
|
|
82
77
|
const effectFunc = model.effects[effectKey];
|
|
83
|
-
|
|
84
|
-
sagaMiddleware.run(function* () {
|
|
78
|
+
const task = sagaMiddleware.run(function* () {
|
|
85
79
|
// 使用 takeEvery 的参数传递功能,将上下文传入 handleEffect
|
|
86
80
|
// handleEffect(modelName, effectFunc, tools, action)
|
|
87
81
|
yield takeEvery(type, handleEffect, model.name, effectFunc, opFun);
|
|
88
82
|
});
|
|
83
|
+
runningEffects[model.name].push(task);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return function regist(reduxModel: ReduxModel): void {
|
|
88
|
+
const model = {
|
|
89
|
+
...reduxModel,
|
|
90
|
+
initialState: {}
|
|
91
|
+
} as ReduxSagaModel;
|
|
92
|
+
|
|
93
|
+
if (registedModel[model.name]) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 安装模型
|
|
98
|
+
installModel(model);
|
|
99
|
+
|
|
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);
|
|
89
116
|
}
|
|
90
117
|
};
|
|
91
118
|
}
|
|
@@ -112,7 +139,10 @@ export default function create(): ReduxApp {
|
|
|
112
139
|
preloadedState: {}
|
|
113
140
|
});
|
|
114
141
|
|
|
115
|
-
|
|
142
|
+
// 存储运行中的 Effect 任务,用于热更新取消
|
|
143
|
+
const runningEffects: Record<string, Task[]> = {};
|
|
144
|
+
|
|
145
|
+
const regist = getRegistModelFunc(store, registedModel, allReducers, sagaMiddleware, runningEffects);
|
|
116
146
|
|
|
117
147
|
return {
|
|
118
148
|
store,
|
package/src/redux/typeDeclare.ts
CHANGED
|
@@ -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
|
+
}
|