feat: 优化plugin-access和plugin-layout

1. access插件提供找不到页面时的处理器
2. 文件路径转路由路径优化
3. 提供按需加载插件配置dynamicImport
This commit is contained in:
万纯 2021-03-04 19:48:07 +08:00
parent 7460db344f
commit 14c00a8ffc
14 changed files with 144 additions and 59 deletions

View File

@ -73,7 +73,6 @@ export default {
locale.setLocale({ lang: 'en-US' }); locale.setLocale({ lang: 'en-US' });
locale.addLocale({ lang: 'ja-JP', messages: { test: 'テスト' } }); locale.addLocale({ lang: 'ja-JP', messages: { test: 'テスト' } });
console.log(locale.getAllLocales()); console.log(locale.getAllLocales());
access.addAccess('/onepiece1');
}, 2000); }, 2000);
setTimeout(() => { setTimeout(() => {
accessId.value = '11'; accessId.value = '11';

View File

@ -1,4 +1,4 @@
import { reactive, computed, inject } from "vue"; import { reactive, unref, computed, inject } from "vue";
import createDirective from "./createDirective"; import createDirective from "./createDirective";
import createComponent from "./createComponent"; import createComponent from "./createComponent";
@ -56,12 +56,9 @@ const setAccess = (accessIds) => {
state.currentAccessIds = accessIds; state.currentAccessIds = accessIds;
}; };
const addAccess = (accessId) => { const getAccess = () => {
if (typeof accessId !== 'string') { return state.currentAccessIds.slice(0)
throw new Error("[plugin-access]: argument to the addAccess() must be string"); }
}
state.currentAccessIds.push(accessId);
};
const _syncSetRoleId = (promise) => { const _syncSetRoleId = (promise) => {
rolePromiseList.push(promise); rolePromiseList.push(promise);
@ -116,12 +113,12 @@ const match = (path, accessIds) => {
return false; return false;
}; };
const hasLoading = () => { const isDataReady = () => {
return rolePromiseList.length || accessPromiseList.length; return rolePromiseList.length || accessPromiseList.length;
}; };
const hasAccess = async (path) => { const hasAccess = async (path) => {
if (!hasLoading()) { if (!isDataReady()) {
return match(path, getAllowAccessIds()); return match(path, getAllowAccessIds());
} }
await Promise.all(rolePromiseList.concat(accessPromiseList)); await Promise.all(rolePromiseList.concat(accessPromiseList));
@ -132,7 +129,7 @@ export const install = (app) => {
const allowPageIds = computed(getAllowAccessIds); const allowPageIds = computed(getAllowAccessIds);
const useAccess = (path) => { const useAccess = (path) => {
const result = computed(() => { const result = computed(() => {
return match(path, allowPageIds.value); return match(unref(path), allowPageIds.value);
}); });
return result; return result;
}; };
@ -143,10 +140,10 @@ export const install = (app) => {
export const access = { export const access = {
hasAccess, hasAccess,
hasLoading, isDataReady,
setRole, setRole,
setAccess, setAccess,
addAccess getAccess,
}; };
export const useAccess = (path) => { export const useAccess = (path) => {

View File

@ -3,6 +3,18 @@ import { access, install } from './core';
export function onRouterCreated({ router }) { export function onRouterCreated({ router }) {
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
const runtimeConfig = plugin.applyPlugins({
key: 'access',
type: ApplyPluginsType.modify,
initialValue: {}
});
if (to.matched.length === 0) {
if (runtimeConfig.noFoundHandler && typeof runtimeConfig.noFoundHandler === 'function') {
return runtimeConfig.noFoundHandler({
router, to, from, next
});
}
}
let path; let path;
if (to.matched.length === 1) { if (to.matched.length === 1) {
path = to.matched[0].path; path = to.matched[0].path;
@ -11,21 +23,14 @@ export function onRouterCreated({ router }) {
} }
const canRoute = await access.hasAccess(path); const canRoute = await access.hasAccess(path);
if (canRoute) { if (canRoute) {
next(); return next();
} else {
const runtimeConfig = plugin.applyPlugins({
key: 'access',
type: ApplyPluginsType.modify,
initialValue: {}
});
if (runtimeConfig.noAccessHandler && typeof runtimeConfig.noAccessHandler === 'function') {
runtimeConfig.noAccessHandler({
router, to, from, next
});
} else {
next(false);
}
} }
if (runtimeConfig.unAccessHandler && typeof runtimeConfig.unAccessHandler === 'function') {
return runtimeConfig.unAccessHandler({
router, to, from, next
});
}
next(false);
}); });
} }

View File

@ -7,29 +7,29 @@ if (!useAccess) {
); );
} }
const hasAccess = (item) => { export const hasAccessByMenuItem = (item) => {
let res; let res;
if (item.path && (!item.children || item.children.length === 0)) { if (item.path && (!item.children || item.children.length === 0)) {
res = useAccess(item.path); res = useAccess(item.path);
} else if (item.children && item.children.length > 0) { } else if (item.children && item.children.length > 0) {
res = computed(() => item.children.some(child => hasAccess(child))); res = computed(() => item.children.some(child => hasAccessByMenuItem(child)));
} }
return res; return res;
}; };
const addAcessTag = (arr) => { const _addAccessTag = (arr) => {
if (Array.isArray(arr)) { if (Array.isArray(arr)) {
arr.forEach((item) => { arr.forEach((item) => {
item.access = hasAccess(item); item.access = hasAccessByMenuItem(item);
if (item.children && item.children.length > 0) { if (item.children && item.children.length > 0) {
addAcessTag(item.children); _addAccessTag(item.children);
} }
}); });
} }
}; };
export default function (menus) { export const addAccessTag = (menus) => {
const originData = unref(menus); const originData = unref(menus);
addAcessTag(originData); _addAccessTag(originData);
return originData; return originData;
} };

View File

@ -1,11 +1,11 @@
<template> <template>
<a-menu <a-menu
:selectedKeys="selectedKeys" :selectedKeys="selectedKeys"
@click="onMenuClick"
:theme="theme" :theme="theme"
mode="inline" mode="inline"
@click="onMenuClick"
> >
<template v-for="(item, index) in menus" :key="index"> <template v-for="(item, index) in fixedMenus" :key="index">
<template v-if="item.access"> <template v-if="item.access">
<a-sub-menu v-if="item.children" :title="item.title"> <a-sub-menu v-if="item.children" :title="item.title">
<template <template
@ -52,7 +52,7 @@ import 'ant-design-vue/lib/menu/style';
import { import {
UserOutlined UserOutlined
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import addAccessTag from '../helpers/addAccessTag'; import { addAccessTag } from '../helpers/pluginAccess';
export default { export default {
components: { components: {
@ -93,7 +93,7 @@ export default {
const selectedKeys = computed(() => [route.path]); const selectedKeys = computed(() => [route.path]);
return { return {
selectedKeys, selectedKeys,
menus: fixedMenus, fixedMenus,
onMenuClick onMenuClick
}; };
} }

View File

@ -1,18 +1,18 @@
<template> <template>
<a-tabs <a-tabs
:activeKey="route.path" :activeKey="route.path"
@tabClick="switchPage"
class="layout-content-tabs" class="layout-content-tabs"
hide-add hide-add
type="editable-card" type="editable-card"
@tabClick="switchPage"
> >
<a-tab-pane v-for="page in pageList" :key="page.path" closable> <a-tab-pane v-for="page in pageList" :key="page.path" closable>
<template #tab> <template #tab>
{{page.name}} {{page.name}}
<ReloadOutlined <ReloadOutlined
v-show="route.path === page.path" v-show="route.path === page.path"
@click="reloadPage(page.path)"
class="layout-tabs-close-icon" class="layout-tabs-close-icon"
@click="reloadPage(page.path)"
/> />
</template> </template>
</a-tab-pane> </a-tab-pane>
@ -36,7 +36,7 @@
</a-tabs> </a-tabs>
<router-view v-slot="{ Component, route }"> <router-view v-slot="{ Component, route }">
<keep-alive> <keep-alive>
<component :key="getPageKey(route)" :is="Component" /> <component :is="Component" :key="getPageKey(route)" />
</keep-alive> </keep-alive>
</router-view> </router-view>
</template> </template>

View File

@ -42,6 +42,7 @@ export default function () {
require.resolve('./plugins/features/nodeModulesTransform'), require.resolve('./plugins/features/nodeModulesTransform'),
require.resolve('./plugins/features/vueLoader'), require.resolve('./plugins/features/vueLoader'),
require.resolve('./plugins/features/mock'), require.resolve('./plugins/features/mock'),
require.resolve('./plugins/features/dynamicImport'),
// misc // misc
require.resolve('./plugins/misc/route'), require.resolve('./plugins/misc/route'),

View File

@ -0,0 +1,12 @@
export default (api) => {
api.describe({
key: 'dynamicImport',
config: {
schema(joi) {
return joi.boolean();
}
},
default: false
});
};

View File

@ -57,14 +57,15 @@ const getRoutePath = function (parentRoutePath, fileName) {
if (fileName === 'index') { if (fileName === 'index') {
fileName = ''; fileName = '';
} }
let routePath = posix.join(parentRoutePath, fileName);
// /@id.vue -> /:id // /@id.vue -> /:id
routePath = routePath.replace(/@/g, ':'); if (fileName.startsWith('@')) {
// /*.vue -> * fileName = fileName.replace(/@/, ':');
if (routePath === '/*') {
routePath = '*';
} }
return routePath; // /*.vue -> :pathMatch(.*)
if (fileName.includes('*')) {
fileName = fileName.replace('*', ':pathMatch(.*)');
}
return posix.join(parentRoutePath, fileName);
}; };
// TODO 约定 layout 目录作为布局文件夹, // TODO 约定 layout 目录作为布局文件夹,
@ -140,7 +141,7 @@ const genRoutes = function (parentRoutes, path, parentRoutePath, config) {
* @param {*} routes * @param {*} routes
*/ */
const fix = function (routes) { const rank = function (routes) {
routes.forEach((item) => { routes.forEach((item) => {
const path = item.path; const path = item.path;
let arr = path.split('/'); let arr = path.split('/');
@ -150,9 +151,9 @@ const fix = function (routes) {
let count = 0; let count = 0;
arr.forEach((sonPath) => { arr.forEach((sonPath) => {
count += 4; count += 4;
if (sonPath.indexOf(':') !== -1) { if (sonPath.indexOf(':') !== -1 && sonPath.indexOf(':pathMatch(.*)') === -1) {
count += 2; count += 2;
} else if (sonPath.indexOf('*') !== -1) { } else if (sonPath.indexOf(':pathMatch(.*)') !== -1) {
count -= 1; count -= 1;
} else if (sonPath === '') { } else if (sonPath === '') {
count += 1; count += 1;
@ -162,7 +163,7 @@ const fix = function (routes) {
}); });
item.count = count; item.count = count;
if (item.children && item.children.length) { if (item.children && item.children.length) {
fix(item.children); rank(item.children);
} }
}); });
routes = routes.sort((a, b) => b.count - a.count); routes = routes.sort((a, b) => b.count - a.count);
@ -175,7 +176,7 @@ const getRoutes = function ({ config, absPagesPath }) {
const routes = []; const routes = [];
genRoutes(routes, absPagesPath, '/', config); genRoutes(routes, absPagesPath, '/', config);
fix(routes); rank(routes);
return routes; return routes;
}; };

View File

@ -12,7 +12,7 @@ export default {
publicPath: '/', publicPath: '/',
access: { access: {
roles: { roles: {
admin: ["/", "/onepiece", '/store'] admin: ["/"]
} }
}, },
request: { request: {
@ -52,5 +52,6 @@ export default {
}, },
vuex: { vuex: {
strict: true strict: true
} },
dynamicImport: true
}; };

View File

@ -1,13 +1,13 @@
import { access } from '@webank/fes'; import { access as accessApi } from '@webank/fes';
import PageLoading from '@/components/PageLoading'; import PageLoading from '@/components/PageLoading';
import UserCenter from '@/components/UserCenter'; import UserCenter from '@/components/UserCenter';
export const beforeRender = { export const beforeRender = {
loading: <PageLoading />, loading: <PageLoading />,
action() { action() {
const { setRole } = access; const { setRole } = accessApi;
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(() => { setTimeout(() => {
setRole('admin'); setRole('admin');
@ -22,3 +22,24 @@ export const beforeRender = {
export const layout = { export const layout = {
customHeader: <UserCenter /> customHeader: <UserCenter />
}; };
export const access = {
unAccessHandler({ to, next }) {
const accesssIds = accessApi.getAccess();
if (to.path === '/404') {
accessApi.setAccess(accesssIds.concat(['/404']));
return next('/404');
}
if (!accesssIds.includes('/403')) {
accessApi.setAccess(accesssIds.concat(['/403']));
}
next('/403');
},
noFoundHandler({ next }) {
const accesssIds = accessApi.getAccess();
if (!accesssIds.includes('/404')) {
accessApi.setAccess(accesssIds.concat(['/404']));
}
next('/404');
}
};

View File

@ -0,0 +1,25 @@
<template>
<a-result status="403" title="403" sub-title="Sorry, you are not authorized to access this page.">
<template #extra>
<a-button type="primary">上一页</a-button>
</template>
</a-result>
</template>
<config>
{
"layout": false
}
</config>
<script>
import Result from 'ant-design-vue/lib/result';
import 'ant-design-vue/lib/result/style';
import Button from 'ant-design-vue/lib/button';
import 'ant-design-vue/lib/button/style';
export default {
components: {
[Result.name]: Result,
[Button.name]: Button
}
};
</script>

View File

@ -0,0 +1,25 @@
<template>
<a-result status="404" title="404" sub-title="Sorry, the page you visited does not exist.">
<template #extra>
<a-button type="primary">上一页</a-button>
</template>
</a-result>
</template>
<config>
{
"layout": false
}
</config>
<script>
import Result from 'ant-design-vue/lib/result';
import 'ant-design-vue/lib/result/style';
import Button from 'ant-design-vue/lib/button';
import 'ant-design-vue/lib/button/style';
export default {
components: {
[Result.name]: Result,
[Button.name]: Button
}
};
</script>

View File

@ -69,12 +69,10 @@ export default {
locale.setLocale({ lang: 'en-US' }); locale.setLocale({ lang: 'en-US' });
locale.addLocale({ lang: 'ja-JP', messages: { test: 'テスト' } }); locale.addLocale({ lang: 'ja-JP', messages: { test: 'テスト' } });
console.log(locale.getAllLocales()); console.log(locale.getAllLocales());
access.addAccess('/onepiece1');
}, 2000); }, 2000);
setTimeout(() => { setTimeout(() => {
accessId.value = '11'; accessId.value = '11';
}, 4000); }, 4000);
// router.push('/onepiece');
console.log('测试 mock!!'); console.log('测试 mock!!');
request('/v2/file').then((data) => { request('/v2/file').then((data) => {