refactor(plugin-model): 使用@vueuse/core实现模型状态共享

重构模型插件,利用@vueuse/core的createSharedComposable实现模型状态共享
移除默认容器模板,改为使用JSX实现的getRootContainer组件
优化路由初始化逻辑,将beforeRender处理移至根容器组件
This commit is contained in:
harrywan 2025-09-11 10:25:11 +08:00
parent c1fa59fcf8
commit 6db74bfbbc
12 changed files with 173 additions and 82 deletions

View File

@ -30,6 +30,7 @@
"@fesjs/plugin-sass": "workspace:*",
"@fesjs/plugin-swc": "workspace:*",
"@fesjs/plugin-watermark": "workspace:*",
"@vueuse/core": "13.9.0",
"core-js": "^3.45.1",
"pinia": "^3.0.3",
"vue": "^3.5.21"

View File

@ -3,6 +3,6 @@ import { ref } from 'vue';
export default function user() {
const count = ref(1);
return {
count
count,
};
}

View File

@ -11,8 +11,8 @@
</config>
<script>
import { onMounted, ref } from 'vue';
import { MonacoEditor, useLayout } from '@fesjs/fes';
import { onMounted, ref } from 'vue';
export default {
components: {

View File

@ -8,7 +8,7 @@
</template>
<script setup>
import { defineRouteMeta, useRouter } from '@fesjs/fes';
import { defineRouteMeta, useModel, useRouter } from '@fesjs/fes';
import { FButton } from '@fesjs/fes-design';
defineRouteMeta({
@ -16,7 +16,9 @@ defineRouteMeta({
title: '$test.test',
});
console.log('123123'.replaceAll('123', '234'));
const initialState = useModel('@@initialState');
console.log(initialState);
const router = useRouter();
function go() {

View File

@ -31,6 +31,7 @@
},
"peerDependencies": {
"@fesjs/fes": "^3.1.12",
"@vueuse/core": "^13.0.0",
"vue": "^3.5.21"
},
"dependencies": {

View File

@ -1,4 +1,6 @@
import { createSharedComposable } from '@vueuse/core';
{{{userImports}}}
{{{extraImports}}}
@ -12,18 +14,40 @@ export const models = {
};
const cache = new Map();
/**
* 使用createSharedComposable包装后的共享模型对象
* 每个模型都会被包装成可共享的组合式函数
*/
const sharedModels = {}
export const useModel = (name) => {
const modelFunc = models[name];
if (modelFunc === undefined) {
throw new Error('[plugin-model]: useModel, name is undefined.');
// 为每个模型创建共享的组合式函数
Object.keys(models).forEach(key => {
/**
* 使用createSharedComposable包装模型
* key作为唯一标识符确保同一模型在不同组件间共享状态
*/
sharedModels[key] = createSharedComposable(models[key], `model:${key}`)
})
/**
* 使用模型的Hook函数
* 提供统一的模型访问接口,支持状态共享和生命周期管理
* @param {string} name - 模型名称,必须是已注册的模型
* @returns {any} 共享的模型实例,多个组件调用同一模型时返回相同的状态实例
*/
export function useModel(name) {
// 检查模型是否存在于注册列表中
if (!(name in sharedModels)) {
// 提供详细的错误信息,帮助开发者调试
const availableModels = Object.keys(sharedModels).join(', ')
throw new Error(
`模型 "${name}" 不存在。\n` +
`可用的模型有: ${availableModels}\n` +
`请检查模型名称是否正确,或确认模型文件是否已正确导出。`
)
}
if (typeof modelFunc !== 'function') {
throw new Error('[plugin-model]: useModel is not a function.');
}
if (!cache.has(name)) {
cache.set(name, modelFunc());
}
return cache.get(name);
};
// 返回共享的模型实例
// createSharedComposable确保同一模型在不同组件间共享状态
return sharedModels[name]()
}

View File

@ -1,3 +0,0 @@
<template>
<router-view></router-view>
</template>

View File

@ -6,7 +6,7 @@ import { plugin } from './core/plugin';
import './core/pluginRegister';
import { ApplyPluginsType } from '{{{ runtimePath }}}';
import { getRoutes } from './core/routes/routes';
import DefaultContainer from './defaultContainer.vue';
import getRootContainer from './getRootContainer';
{{{ imports }}}
@ -16,17 +16,8 @@ import DefaultContainer from './defaultContainer.vue';
const renderClient = (opts = {}) => {
const { plugin, routes, rootElement } = opts;
const rootContainer = plugin.applyPlugins({
type: ApplyPluginsType.modify,
key: 'rootContainer',
initialValue: DefaultContainer,
args: {
routes: routes,
plugin: plugin
}
});
const app = createApp(rootContainer);
const app = createApp(getRootContainer(routes, plugin));
plugin.applyPlugins({
key: 'onAppCreated',

View File

@ -0,0 +1,82 @@
import { defineComponent, onBeforeMount, ref, provide, } from 'vue'
import { useRouter, RouterView } from 'vue-router'
import { plugin } from './core/plugin';
import { updateInitialState } from './initialState';
import { ApplyPluginsType } from '{{{ runtimePath }}}';
export const DefaultContainer = defineComponent({
name: 'DefaultContainer',
setup() {
return () => {
return <RouterView></RouterView>
}
}
})
export default function getRootContainer(_routes, _plugin) {
return defineComponent({
name: 'RootContainer',
setup(props) {
const RootContainer = plugin.applyPlugins({
type: ApplyPluginsType.modify,
key: 'rootContainer',
initialValue: DefaultContainer,
args: {
routes: _routes,
plugin: _plugin
}
});
const beforeRenderConfig = plugin.applyPlugins({
key: "beforeRender",
type: ApplyPluginsType.modify,
initialValue: {
loading: null,
action: null
},
});
if (typeof beforeRenderConfig.action !== "function") {
return () => <RootContainer {...props} />
}
const router = useRouter();
const isLoading = ref(false);
onBeforeMount(async () => {
let isInit = false
router.beforeEach(async (to, from, next) => {
if (isInit) {
return next()
}
try {
isInit = true
isLoading.value = true;
const _initialState = await beforeRenderConfig.action({ router });
updateInitialState(_initialState);
next();
} catch (e) {
console.error(`[fes] beforeRender执行出现异常:`);
console.error(e);
next(false);
} finally {
isLoading.value = false;
}
})
plugin.applyPlugins({
key: 'onRouterCreated',
type: ApplyPluginsType.event,
args: { router },
});
})
return () => {
if (isLoading.value) {
return <beforeRenderConfig.loading {...props} />
}
return <RootContainer {...props} />
}
},
})
};

View File

@ -73,15 +73,14 @@ export default function (api) {
}),
});
const defaultContainerName = 'defaultContainer';
api.writeTmpFile({
path: `${defaultContainerName}.vue`,
content: readFileSync(join(__dirname, `./${defaultContainerName}.tpl`), 'utf-8'),
});
api.writeTmpFile({
path: `initialState.js`,
content: Mustache.render(readFileSync(join(__dirname, `./initialState.tpl`), 'utf-8')),
});
api.writeTmpFile({
path: `getRootContainer.jsx`,
content: Mustache.render(readFileSync(join(__dirname, `./getRootContainer.jsx.tpl`), 'utf-8'), { runtimePath }),
});
});
}

View File

@ -1,7 +1,5 @@
import { createApp } from 'vue';
import { createRouter as createVueRouter, {{{ CREATE_HISTORY }}}, ApplyPluginsType } from '{{{ runtimePath }}}';
import { plugin } from '../plugin';
import { updateInitialState } from '../../initialState';
const ROUTER_BASE = '{{{ routerBase }}}';
let router = null;
@ -37,47 +35,6 @@ export const createRouter = (routes) => {
routes: route.routes
});
let isInit = false
router.beforeEach(async (to, from, next) => {
if(isInit){
return next()
}
isInit = true
const beforeRenderConfig = plugin.applyPlugins({
key: "beforeRender",
type: ApplyPluginsType.modify,
initialValue: {
loading: null,
action: null
},
});
if (typeof beforeRenderConfig.action !== "function") {
return next();
}
const rootElement = document.createElement('div');
document.body.appendChild(rootElement)
const app = createApp(beforeRenderConfig.loading);
app.mount(rootElement);
try {
const initialState = await beforeRenderConfig.action({router, history});
updateInitialState(initialState || {})
next();
} catch(e){
next(false);
console.error(`[fes] beforeRender执行出现异常`);
console.error(e);
}
app.unmount();
app._container.innerHTML = '';
document.body.removeChild(rootElement);
})
plugin.applyPlugins({
key: 'onRouterCreated',
type: ApplyPluginsType.event,
args: { router, history },
});
return router;
};

37
pnpm-lock.yaml generated
View File

@ -423,6 +423,9 @@ importers:
'@fesjs/plugin-watermark':
specifier: workspace:*
version: link:../plugin-watermark
'@vueuse/core':
specifier: 13.9.0
version: 13.9.0(vue@3.5.21(typescript@5.9.2))
core-js:
specifier: ^3.45.1
version: 3.45.1
@ -633,6 +636,9 @@ importers:
'@fesjs/utils':
specifier: workspace:*
version: link:../utils
'@vueuse/core':
specifier: ^10.9.0
version: 10.11.1(vue@3.5.21(typescript@5.9.2))
vue:
specifier: ^3.5.21
version: 3.5.21(typescript@5.9.2)
@ -3165,6 +3171,9 @@ packages:
'@types/web-bluetooth@0.0.20':
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
'@types/web-bluetooth@0.0.21':
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
'@types/ws@8.18.1':
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
@ -3339,18 +3348,31 @@ packages:
'@vueuse/core@10.11.1':
resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==}
'@vueuse/core@13.9.0':
resolution: {integrity: sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==}
peerDependencies:
vue: ^3.5.0
'@vueuse/core@9.13.0':
resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==}
'@vueuse/metadata@10.11.1':
resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==}
'@vueuse/metadata@13.9.0':
resolution: {integrity: sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==}
'@vueuse/metadata@9.13.0':
resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==}
'@vueuse/shared@10.11.1':
resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==}
'@vueuse/shared@13.9.0':
resolution: {integrity: sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==}
peerDependencies:
vue: ^3.5.0
'@vueuse/shared@9.13.0':
resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==}
@ -10718,6 +10740,8 @@ snapshots:
'@types/web-bluetooth@0.0.20': {}
'@types/web-bluetooth@0.0.21': {}
'@types/ws@8.18.1':
dependencies:
'@types/node': 24.3.0
@ -10990,6 +11014,13 @@ snapshots:
- '@vue/composition-api'
- vue
'@vueuse/core@13.9.0(vue@3.5.21(typescript@5.9.2))':
dependencies:
'@types/web-bluetooth': 0.0.21
'@vueuse/metadata': 13.9.0
'@vueuse/shared': 13.9.0(vue@3.5.21(typescript@5.9.2))
vue: 3.5.21(typescript@5.9.2)
'@vueuse/core@9.13.0(vue@3.5.21(typescript@5.9.2))':
dependencies:
'@types/web-bluetooth': 0.0.16
@ -11002,6 +11033,8 @@ snapshots:
'@vueuse/metadata@10.11.1': {}
'@vueuse/metadata@13.9.0': {}
'@vueuse/metadata@9.13.0': {}
'@vueuse/shared@10.11.1(vue@3.5.21(typescript@5.9.2))':
@ -11011,6 +11044,10 @@ snapshots:
- '@vue/composition-api'
- vue
'@vueuse/shared@13.9.0(vue@3.5.21(typescript@5.9.2))':
dependencies:
vue: 3.5.21(typescript@5.9.2)
'@vueuse/shared@9.13.0(vue@3.5.21(typescript@5.9.2))':
dependencies:
vue-demi: 0.14.10(vue@3.5.21(typescript@5.9.2))