From 32e6278b7436ce3b235cf6d68178f08ba80a8f44 Mon Sep 17 00:00:00 2001
From: aringlai <aringlai@webank.com>
Date: Thu, 4 Feb 2021 21:36:51 +0800
Subject: [PATCH] =?UTF-8?q?refactor:=20vuex=E6=8F=92=E4=BB=B6=E5=AE=9E?=
 =?UTF-8?q?=E7=8E=B0=E9=87=8D=E6=9E=84=EF=BC=8C=E5=A2=9E=E5=8A=A0=E7=B1=BB?=
 =?UTF-8?q?=E5=9E=8B=E5=AF=BC=E5=87=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/fes-plugin-vuex/package.json         |   3 +
 packages/fes-plugin-vuex/src/helper.js        | 160 ++++++++++++++++++
 packages/fes-plugin-vuex/src/index.js         |  79 ++++-----
 packages/fes-plugin-vuex/src/runtime/core.tpl |  26 +++
 .../fes-plugin-vuex/src/runtime/runtime.js    |   6 +
 .../fes-plugin-vuex/src/runtime/runtime.tpl   |  10 --
 packages/fes-template/.fes.js                 |   7 +-
 packages/fes-template/src/pages/index.vue     |  10 +-
 packages/fes-template/src/pages/store.vue     |  50 ++++++
 packages/fes-template/src/stores/foo/bar.js   |  23 +++
 packages/fes-template/src/stores/user.js      |  29 ++++
 11 files changed, 334 insertions(+), 69 deletions(-)
 create mode 100644 packages/fes-plugin-vuex/src/helper.js
 create mode 100644 packages/fes-plugin-vuex/src/runtime/core.tpl
 create mode 100644 packages/fes-plugin-vuex/src/runtime/runtime.js
 delete mode 100644 packages/fes-plugin-vuex/src/runtime/runtime.tpl
 create mode 100644 packages/fes-template/src/pages/store.vue
 create mode 100644 packages/fes-template/src/stores/foo/bar.js

diff --git a/packages/fes-plugin-vuex/package.json b/packages/fes-plugin-vuex/package.json
index 5074d0a7..bab1b6f0 100644
--- a/packages/fes-plugin-vuex/package.json
+++ b/packages/fes-plugin-vuex/package.json
@@ -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",
diff --git a/packages/fes-plugin-vuex/src/helper.js b/packages/fes-plugin-vuex/src/helper.js
new file mode 100644
index 00000000..9cec46dd
--- /dev/null
+++ b/packages/fes-plugin-vuex/src/helper.js
@@ -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模块的mutations、actions、getters类型
+ * @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)
+    };
+}
diff --git a/packages/fes-plugin-vuex/src/index.js b/packages/fes-plugin-vuex/src/index.js
index 22705455..7f92ffd6 100644
--- a/packages/fes-plugin-vuex/src/index.js
+++ b/packages/fes-plugin-vuex/src/index.js
@@ -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}`);
 };
diff --git a/packages/fes-plugin-vuex/src/runtime/core.tpl b/packages/fes-plugin-vuex/src/runtime/core.tpl
new file mode 100644
index 00000000..7366fc01
--- /dev/null
+++ b/packages/fes-plugin-vuex/src/runtime/core.tpl
@@ -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
+};
diff --git a/packages/fes-plugin-vuex/src/runtime/runtime.js b/packages/fes-plugin-vuex/src/runtime/runtime.js
new file mode 100644
index 00000000..d775ceb9
--- /dev/null
+++ b/packages/fes-plugin-vuex/src/runtime/runtime.js
@@ -0,0 +1,6 @@
+// eslint-disable-next-line import/extensions
+import { install } from './core';
+
+export function onAppCreated({ app }) {
+    install(app);
+}
diff --git a/packages/fes-plugin-vuex/src/runtime/runtime.tpl b/packages/fes-plugin-vuex/src/runtime/runtime.tpl
deleted file mode 100644
index bf0aec46..00000000
--- a/packages/fes-plugin-vuex/src/runtime/runtime.tpl
+++ /dev/null
@@ -1,10 +0,0 @@
-import { createStore } from 'vuex'
-{{{IMPORT_MODULES}}}
-{{{IMPORT_PLUGINS}}}
-
-export function onAppCreated({ app }) {
-    app.use(createStore({
-        modules: {{{MODULES}}},
-        plugins: {{{PLUGINS}}}
-    }))
-}
\ No newline at end of file
diff --git a/packages/fes-template/.fes.js b/packages/fes-template/.fes.js
index db77aa6f..5c5f5dee 100644
--- a/packages/fes-template/.fes.js
+++ b/packages/fes-template/.fes.js
@@ -9,7 +9,7 @@ export default {
     publicPath: '/',
     access: {
         roles: {
-            admin: ["/", "/onepiece"]
+            admin: ["/", "/onepiece", '/store']
         }
     },
     layout: {
@@ -20,6 +20,8 @@ export default {
             name: 'index'
         }, {
             name: 'onepiece'
+        }, {
+            name: 'store'
         }]
     },
     locale: {
@@ -30,5 +32,8 @@ export default {
     },
     enums: {
         status: [['0', '无效的'], ['1', '有效的']]
+    },
+    vuex: {
+        strict: true
     }
 };
diff --git a/packages/fes-template/src/pages/index.vue b/packages/fes-template/src/pages/index.vue
index 3a304634..bf40156a 100644
--- a/packages/fes-template/src/pages/index.vue
+++ b/packages/fes-template/src/pages/index.vue
@@ -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
 } from '@webank/fes';
@@ -65,8 +63,6 @@ export default {
             ]
         });
         console.log(roles);
-        const store = useStore();
-        console.log('store==>', store);
         onMounted(() => {
             console.log(router);
             setTimeout(() => {
@@ -86,9 +82,7 @@ export default {
             accessOnepicess,
             t: localI18n.t,
             enumsGet: enums.get,
-            roles,
-            count: computed(() => store.state.counter.count),
-            increment: () => store.commit('counter/increment')
+            roles
         };
     }
 };
diff --git a/packages/fes-template/src/pages/store.vue b/packages/fes-template/src/pages/store.vue
new file mode 100644
index 00000000..ef94e6e7
--- /dev/null
+++ b/packages/fes-template/src/pages/store.vue
@@ -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>
diff --git a/packages/fes-template/src/stores/foo/bar.js b/packages/fes-template/src/stores/foo/bar.js
new file mode 100644
index 00000000..78070ba3
--- /dev/null
+++ b/packages/fes-template/src/stores/foo/bar.js
@@ -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);
+        }
+    }
+};
diff --git a/packages/fes-template/src/stores/user.js b/packages/fes-template/src/stores/user.js
index e6ffcceb..337bf8f8 100644
--- a/packages/fes-template/src/stores/user.js
+++ b/packages/fes-template/src/stores/user.js
@@ -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() {}
+            }
         }
     }
 };