Merge pull request #41 from aringlai/vue3

refactor: vuex插件实现重构,增加类型导出
This commit is contained in:
harrywan 2021-02-05 17:04:18 +08:00 committed by GitHub
commit a8afd7976c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 334 additions and 69 deletions

View File

@ -26,6 +26,9 @@
"publishConfig": {
"access": "public"
},
"dependencies": {
"@umijs/utils": "3.3.3"
},
"peerDependencies": {
"@webank/fes": "^2.0.0-alpha.0",
"vue": "3.0.5",

View File

@ -0,0 +1,160 @@
import { parser } from '@umijs/utils';
import { readdirSync, readFileSync, statSync } from 'fs';
import { join } from 'path';
/**
* 获取文件夹所有JS文件路径
* @param {string} dir
*/
function getDirFilePaths(dir) {
const dirs = readdirSync(dir);
let pathList = [];
for (const name of dirs) {
const path = join(dir, name);
const info = statSync(path);
if (info.isDirectory()) {
pathList = pathList.concat(getDirFilePaths(path));
} else if (path.endsWith('.js')) {
pathList.push(path);
}
}
return pathList;
}
/**
* 路径转驼峰
* @param {*} path
*/
function pathToHump(path, root) {
return path.replace(root, '')
.replace('.js', '')
.replace(/(\/|\.|-|_)\S/g, text => text[1].toUpperCase())
.replace(/\S/, text => text.toLowerCase());
}
/**
* 获取vuex模块的mutationsactionsgetters类型
* @param {*} ast
* @param {*} name
*/
function getModelTypes(ast, name, namespace = '') {
const types = {
mutations: {},
actions: {},
getters: {}
};
let namespaced = false;
if (ast.type !== 'ObjectExpression') return types;
ast.properties.forEach((node) => {
if (node.key.name === 'namespaced' && node.value.value) {
namespaced = true;
return;
}
if (Object.keys(types).includes(node.key.name)) {
let type = types[node.key.name];
if (namespaced) {
type = types[node.key.name][name];
if (!type) {
// eslint-disable-next-line no-multi-assign
type = types[node.key.name][name] = {};
}
}
node.value.properties.forEach((prop) => {
const key = prop.key && prop.key.name;
if (key) {
type[key] = `${namespace}${namespaced ? `${name}/` : ''}${key}`;
}
});
return;
}
if (node.key.name === 'modules') {
node.value.properties.forEach((prop) => {
const subTypes = getModelTypes(prop.value, prop.key.name, `${namespace}${namespaced ? `${name}/` : ''}`);
Object.keys(types).forEach((key) => {
if (namespaced) {
types[key][name] = {
...subTypes[key],
...types[key][name]
};
} else {
types[key] = {
...subTypes[key],
...types[key]
};
}
});
});
}
});
return types;
}
/**
* 解析模块
* @param {*} paths
* @param {*} root
*/
function parseModel(paths = [], root) {
const modules = [];
const importModules = [];
let MUTATION_TYPES = {};
let ACTION_TYPES = {};
let GETTER_TYPES = {};
paths.forEach((path) => {
const moduleName = pathToHump(path, root);
importModules.push(`import ${moduleName} from '${path}'`);
modules.push(moduleName);
const content = readFileSync(path).toString('utf-8');
let ast = parser.parse(content, {
sourceType: 'module',
plugins: ['jsx', 'typescript']
});
ast = ast.program.body.filter(body => body.type === 'ExportDefaultDeclaration')[0];
if (ast) {
const { mutations, actions, getters } = getModelTypes(ast.declaration, moduleName);
MUTATION_TYPES = {
...mutations,
...MUTATION_TYPES
};
ACTION_TYPES = {
...actions,
...ACTION_TYPES
};
GETTER_TYPES = {
...getters,
...GETTER_TYPES
};
}
});
return {
modules, importModules, MUTATION_TYPES, ACTION_TYPES, GETTER_TYPES
};
}
function parsePlugin(paths = [], root) {
const plugins = [];
const importPlugins = [];
paths.forEach((path) => {
const moduleName = pathToHump(path, root);
importPlugins.push(`import ${moduleName} from '${path}'`);
plugins.push(moduleName);
});
return { plugins, importPlugins };
}
export function parseStore(root) {
const paths = getDirFilePaths(root);
const modelPaths = [];
const pluginPaths = [];
paths.forEach((path) => {
if (path.indexOf('plugin') > -1) {
pluginPaths.push(path);
} else {
modelPaths.push(path);
}
});
return {
...parsePlugin(pluginPaths, root),
...parseModel(modelPaths, root)
};
}

View File

@ -1,5 +1,6 @@
import { readdirSync, readFileSync, statSync } from 'fs';
import { readFileSync } from 'fs';
import { join } from 'path';
import { parseStore } from './helper';
const namespace = 'plugin-vuex';
@ -9,66 +10,36 @@ export default (api) => {
utils: { Mustache }
} = api;
/**
* 获取文件夹所有JS文件路径
* @param {string} dir
*/
function getDirFilePaths(dir) {
const dirs = readdirSync(dir);
let pathList = [];
for (const name of dirs) {
const path = join(dir, name);
const info = statSync(path);
if (info.isDirectory()) {
pathList = pathList.concat(getDirFilePaths(path));
} else if (path.endsWith('.js')) {
pathList.push(path);
}
api.describe({
key: 'vuex',
config: {
schema(joi) {
return joi.object();
},
onChange: api.ConfigChangeType.regenerateTmpFiles
}
return pathList;
}
/**
* 解析vuex模块及插件文件
* @param {Array<string>} pathList 文件路径
* @param {string} root
*/
function parseStore(pathList, root) {
const store = {
modules: [],
plugins: [],
importModules: [],
importPlugins: []
};
for (const path of pathList) {
const moduleName = path.replace(root, '').replace('.js', '').replace(/(\/|\.|-|_)\S/g, text => text[1].toUpperCase());
if (path.indexOf('plugin') > -1) {
store.importPlugins.push(`import ${moduleName} from '${path}'`);
store.plugins.push(moduleName);
} else {
store.importModules.push(`import ${moduleName} from '${path}'`);
store.modules.push(`${moduleName}`);
}
}
return store;
}
});
const absCoreFilePath = join(namespace, 'core.js');
const absRuntimeFilePath = join(namespace, 'runtime.js');
api.onGenerateFiles(() => {
const root = join(paths.absSrcPath, 'stores');
const storePaths = getDirFilePaths(root);
const store = parseStore(storePaths, join(root, '/'));
const root = join(paths.absSrcPath, api.config.singular ? 'store' : 'stores');
const store = parseStore(root);
const vuexConfig = api.config.vuex || {};
// 文件写出
api.writeTmpFile({
path: absRuntimeFilePath,
path: absCoreFilePath,
content: Mustache.render(
readFileSync(join(__dirname, 'runtime/runtime.tpl'), 'utf-8'),
readFileSync(join(__dirname, 'runtime/core.tpl'), 'utf-8'),
{
IMPORT_MODULES: store.importModules.join('\n'),
IMPORT_PLUGINS: store.importPlugins.join('\n'),
MODULES: `{ ${store.modules.join(', ')} }`,
PLUGINS: `[${store.plugins.join(', ')}]`
PLUGINS: `[${store.plugins.join(', ')}]`,
MUTATION_TYPES: JSON.stringify(store.MUTATION_TYPES),
ACTION_TYPES: JSON.stringify(store.ACTION_TYPES),
GETTER_TYPES: JSON.stringify(store.GETTER_TYPES),
VUEX_CONFIG: JSON.stringify(vuexConfig)
}
)
});
@ -79,5 +50,13 @@ export default (api) => {
ignore: ['.tpl']
});
});
api.addPluginExports(() => [
{
specifiers: ['MUTATION_TYPES', 'ACTION_TYPES', 'GETTER_TYPES'],
source: absCoreFilePath
}
]);
api.addRuntimePlugin(() => `@@/${absRuntimeFilePath}`);
};

View File

@ -0,0 +1,26 @@
import { createStore } from 'vuex';
{{{IMPORT_MODULES}}};
{{{IMPORT_PLUGINS}}};
const modules = {{{MODULES}}};
const MUTATION_TYPES = {{{MUTATION_TYPES}}};
const ACTION_TYPES = {{{ACTION_TYPES}}};
const GETTER_TYPES = {{{GETTER_TYPES}}};
const conifg = {{{VUEX_CONFIG}}};
const install = function (app) {
app.use(createStore({
modules: modules,
plugins: {{{PLUGINS}}},
strict: conifg.strict,
devtools: conifg.devtools
}));
}
export {
install,
MUTATION_TYPES,
ACTION_TYPES,
GETTER_TYPES
};

View File

@ -0,0 +1,6 @@
// eslint-disable-next-line import/extensions
import { install } from './core';
export function onAppCreated({ app }) {
install(app);
}

View File

@ -1,10 +0,0 @@
import { createStore } from 'vuex'
{{{IMPORT_MODULES}}}
{{{IMPORT_PLUGINS}}}
export function onAppCreated({ app }) {
app.use(createStore({
modules: {{{MODULES}}},
plugins: {{{PLUGINS}}}
}))
}

View File

@ -9,7 +9,7 @@ export default {
publicPath: '/',
access: {
roles: {
admin: ["/", "/onepiece"]
admin: ["/", "/onepiece", '/store']
}
},
mock: {
@ -29,6 +29,8 @@ export default {
name: 'index'
}, {
name: 'onepiece'
}, {
name: 'store'
}]
},
locale: {
@ -39,5 +41,8 @@ export default {
},
enums: {
status: [['0', '无效的'], ['1', '有效的']]
},
vuex: {
strict: true
}
};

View File

@ -9,7 +9,6 @@
<div v-for="item in enumsGet('status')" :key="item.key">{{item.value}}{{item.key}}</div>
<div v-for="item in roles" :key="item.key">{{item.name}}{{item.disabled}}</div>
<div>{{enumsGet('roles', '2', { dir: 'eName' })}}</div>
<h4>Vuex <button @click="increment">click me{{count}}</button></h4>
</div>
</template>
<config>
@ -19,8 +18,7 @@
}
</config>
<script>
import { ref, onMounted, computed } from 'vue';
import { useStore } from 'vuex';
import { ref, onMounted } from 'vue';
import {
access, useAccess, useRouter, useI18n, locale, enums, request
} from '@webank/fes';
@ -65,8 +63,6 @@ export default {
]
});
console.log(roles);
const store = useStore();
console.log('store==>', store);
onMounted(() => {
console.log(router);
setTimeout(() => {
@ -105,9 +101,7 @@ export default {
accessOnepicess,
t: localI18n.t,
enumsGet: enums.get,
roles,
count: computed(() => store.state.counter.count),
increment: () => store.commit('counter/increment')
roles
};
}
};

View File

@ -0,0 +1,50 @@
<template>
<div class="haizekuo">
<h4>Vuex</h4>
<div><button @click="increment">click me{{doubleCount}}</button></div>
<div><button :disabled="disabled" @click="login">async login</button></div>
<div><button @click="fooBarIncrement">foo/bar{{fooBarDoubleCount}}</button></div>
<div>{{address}}</div>
</div>
</template>
<config>
{
"name": "store",
"title": "vuex测试"
}
</config>
<script>
import { computed, ref } from 'vue';
import { useStore } from 'vuex';
import { MUTATION_TYPES, GETTER_TYPES, ACTION_TYPES } from '@webank/fes';
export default {
setup() {
const store = useStore();
console.log('store==>', store);
const disabled = ref(false);
return {
address: computed(() => store.getters[GETTER_TYPES.user.address]),
doubleCount: computed(() => store.getters[GETTER_TYPES.counter.doubleCount]),
disabled,
increment: () => store.commit(MUTATION_TYPES.counter.increment),
login: () => {
disabled.value = true;
store.dispatch(ACTION_TYPES.user.login).then((res) => {
// eslint-disable-next-line no-alert
window.alert(res);
disabled.value = false;
});
},
fooBarIncrement: () => store.commit(MUTATION_TYPES.fooBar.increment),
fooBarDoubleCount: computed(() => store.getters[GETTER_TYPES.fooBar.doubleCount])
};
}
};
</script>
<style scoped>
.haizekuo {
/* background: url('../images/icon.png'); */
}
</style>

View File

@ -0,0 +1,23 @@
export default {
namespaced: true,
state: () => ({
count: 0
}),
mutations: {
increment(state) {
state.count++;
}
},
getters: {
doubleCount(state) {
return state.count * 2;
}
},
actions: {
asyncIncrement({ commit }) {
setTimeout(() => {
commit('increment');
}, 2000);
}
}
};

View File

@ -20,6 +20,35 @@ export default {
setTimeout(() => {
commit('increment');
}, 2000);
},
login() {
return new Promise((reslove) => {
setTimeout(() => {
console.log('login');
reslove('OK');
}, 1000);
});
}
},
modules: {
address: {
state: () => ({
province: '广东省',
city: '深圳市',
zone: '南山区'
}),
getters: {
address(state) {
return state.province + state.city + state.zone;
}
}
},
posts: {
namespaced: true,
state: () => ({}),
mutations: {
doSomething() {}
}
}
}
};