Saber Frontend Framework

repository·master·Indexed 42 days ago

https://github.com/chillzhuang/saber

A Vue3 and Element-Plus based front-end framework for the SpringBlade microservice development platform. Version 5.0.1 introduces a migration from the Options API to the Composition API with TypeScript, including the removal of global mixins and the introduction of updated utility functions like validateNull and findColumn.

Tokens
17.3K
Snippets
62
Records
83
Agent score
54%

What's inside Saber

  1. Overview of SpringBlade Microservice Development Platform

    master

    SpringBlade is a microservice development platform utilizing a decoupled front-end and back-end architecture.

    Key Features:

    • Front-end: Uses the Saber framework (based on Vue3 and Element-Plus).
    • Back-end: Built on the Spring Cloud ecosystem. Core components are encapsulated in BladeTool, which is available in Maven Central for direct integration to reduce project bloat.
    • Stability & Control: Integrates Sentinel for traffic control, circuit breaking, and system load protection.
    • Service Discovery & Config: Uses Nacos for registration and configuration management.
    • Multi-tenancy: Includes a simplified, highly extensible SaaS multi-tenant system.
    • Security: Features a custom multi-terminal authentication system (inspired by OAuth2) and a Secure module using JWT for token authentication, with support for Redis-based fine-grained control.
  2. Breaking Changes: Removal of Global Mixins

    master

    Global mixins that previously injected helper methods onto the component instance (this.xxx) have been removed. You must now explicitly import these utilities from their respective modules.

    Old Access (via this.)New Access (via import)
    this.validData(x, def)import { validData } from '@/utils/util'
    this.findObject(column, prop)import { findColumn } from '@/utils/util'
    this.validatenull(x)import { validateNull } from '@/utils/validate'

    Warning: In files that still use the Options API (like src/page/), accessing these via this. will result in undefined and cause runtime is not a function errors. You must update these to use explicit imports and direct function calls.

  3. SpringBlade Project Structure

    master

    The project follows a clear microservice development pattern with organized package responsibilities:

    SpringBlade
    ├── src/main/java/org/springblade
    │   ├── Application.java -- Startup class
    │   ├── common -- Common modules (cache, config, constant, launch, tool)
    │   ├── core -- Core modules (log extensions, secure extensions)
    │   └── modules -- Business modules (auth, desk, develop, resource, system)
    ├── src/main/resources
    │   ├── application.yml -- Main configuration
    │   ├── application-dev.yml -- Development environment
    │   ├── application-test.yml -- Test environment
    │   ├── application-prod.yml -- Production environment
    │   └── config -- Other configuration files
    ├── doc -- Documentation directory
    │   ├── script -- Startup scripts
    │   └── sql -- Database scripts
    └── pom.xml -- Maven configuration
  4. Avoid runtime path resolution errors during TS migration

    master

    When converting infrastructure files from .js to .ts, code that uses import.meta.glob to look up modules by file extension will fail. For example, if a utility looks for ../api/user.js but the file is now ../api/user.ts, it will return undefined and cause a runtime TypeError.

    Fix: Update the lookup logic to check for both .ts and .js extensions to maintain backward compatibility and support the new structure.

    // Correct way to pick modules to support both .ts and .js
    const pickModule = (modules, path) => modules[`${path}.ts`] || modules[`${path}.js`]
    
    let optionObj = pickModule(import.meta.glob(`../option/**/**`), `../option/${option.name}`)
    let apiObj = pickModule(import.meta.glob(`../api/**/**`), `../api/${option.name}`)
  5. Choose between ref and reactive for state declaration

    master

    When declaring state in the Composition API, follow these rules to ensure reactivity and compatibility with Avue components:

    General Rules

    • Use ref for: Entity lists, forms, loading states, and selection sets.
    • Use reactive for: Avue option objects (do not use generic parameters; use interfaces to type the variables instead).

    Pagination (page) Selection Rule

    The choice depends on how the page object is bound in the template:

    Template BindingDeclarationReason
    v-model:page="page" (Two-way)Must be refAvue emits update:page, which performs a whole-object reassignment. reactive will lose reactivity.
    :page="page" (One-way)reactive is fineThe object is passed as read-only; no reassignment occurs.

    Note: In the current codebase, most list pages use :page (one-way) + reactive, but specific pages like topmenu.vue and reportlist.vue use v-model:page and require ref.

  6. Accessing Global Properties in `<script setup>`

    master

    While global properties attached via app.config.globalProperties (like website, $dayjs, or getScreen) are still directly accessible in Vue templates, they are not available via this inside <script setup>. You must import them directly.

    Template Usage (Unchanged)Script Setup Replacement
    websiteimport website from '@/config/website'
    $dayjsimport dayjs from 'dayjs'
    getScreenimport { getScreen } from '@/utils/util'
    axios (via window.axios)import request from '@/axios'
  7. Implement `@on-load` callbacks and total updates

    master

    When using the @on-load callback, avoid naming the callback parameters the same as your top-level state (e.g., page) to prevent variable shadowing.

    Best Practice: Rename the callback parameters (e.g., to pageData or pageParam). Use the parameters to read pagination values, but use the top-level page object to write back the total count.

    Example

    const onLoad = (pageData: { currentPage: number; pageSize: number }, params: Partial<DictEntity> = {}) => {
      loading.value = true;
      getList(pageData.currentPage, pageData.pageSize, Object.assign(params, query.value)).then(res => {
        // Write back to the top-level 'page' object
        page.total = res.data.data.total; 
        data.value = res.data.data.records;
        loading.value = false;
      });
    };
  8. Migrate to Vue 3 Composition API and TypeScript

    master

    When upgrading Saber frontend pages, transition from the Options API to <script setup lang="ts">. This involves replacing data(), computed, and methods with top-level const declarations using ref or reactive, and replacing this references with direct variable access (using .value for ref).

    Key Mapping Summary

    Options APIComposition API + TS
    data() { return { form: {} } }const form = ref<XxxForm>({})
    data() { return { page: {...} } }const page = reactive({...}) or ref({...}) (see Pagination Rules)
    computed: { xxx() {} }const xxx = computed(() => ...)
    ...mapGetters(['permission'])const store = useStore(); const permission = computed(() => store.getters.permission)
    methods: { fn() {} }const fn = () => {}
    this.xxx (state)xxx.value (ref) / xxx.field (reactive)
    this.$refs.crudcrudRef.value (requires ref="crudRef" in template)
    this.$messageimport { ElMessage } from 'element-plus'
    mounted() {}onMounted(() => {}) (import from vue)
    watch: { 'form.x'() {} }watch(() => form.value.x, () => {})
    // Example of a migrated component structure
    import { ref, reactive, computed } from 'vue';
    import { useStore } from 'vuex';
    import { ElMessage } from 'element-plus';
    import { validData, findColumn } from '@/utils/util';
    
    interface DictEntity { id: string; code: string; dictValue: string; parentId: string; }
    type DictForm = Partial<DictEntity>;
    
    const store = useStore();
    const permission = computed(() => store.getters.permission);
    
    const form = ref<DictForm>({});
    const selectionList = ref<DictEntity[]>([]);
    const loading = ref(true);
    const query = ref<Partial<DictEntity>>({});
    const page = reactive({ pageSize: 10, currentPage: 1, total: 0 });
    const option = reactive({ column: [] });
    const crudRef = ref();
    
    const permissionList = computed(() => ({
      addBtn: validData(permission.value.dict_add, false),
    }));
    
    const handleAdd = (row: DictEntity) => { crudRef.value.rowAdd(); };
  9. Saber Frontend Migration Overview (Vue 3 + TS)

    master

    The Saber frontend is undergoing a multi-stage paradigm shift from Options API to <script setup lang="ts"> Composition API with lightweight TypeScript.

    Key Migration Goals:

    • Target: All business pages in src/views/.
    • Paradigm: Use Composition API and explicit TypeScript types.
    • Constraints:
      • No Options API: Do not use data(), methods, computed options, mapGetters, or this.$xxx.
      • No explicit any: Use proper type annotations.
      • Explicit Imports: Even though auto-import is configured, you must explicitly import Vue and Vuex APIs to ensure IDE and type service compatibility (as dts:false is set).
    • Note on Tooling: The build process (pnpm build) remains a pure esbuild transpilation. Type checking is introduced in later stages via vue-tsc and pnpm run type-check.
  10. Migrate infrastructure layer to TypeScript (Phase 3)

    master

    After migrating views to TypeScript, the infrastructure layer (api/, utils/, store/, router/, lang/, config/, and entry files) must be converted to .ts to ensure type safety across the entire call chain.

    Key Migration Rules:

    • Do not change business logic: Only add type annotations where the compiler explicitly errors. If the code 'looks wrong' but the compiler is silent, do not touch it.
    • Handle specific TS errors as follows:
      • TS2554 (Too few arguments): Mark parameters as optional (e.g., tenantId?: string).
      • TS2339 (Property does not exist on type): Create an interface for parameter bags (e.g., StoreParams, LoginPayload).
      • TS2794 (resolve() without arguments): Add void generic to the Promise (e.g., new Promise<void>).
      • TS2339 (Browser private APIs): Add global declarations to env.d.ts (e.g., Document.webkitIsFullScreen).
      • Dynamic imports: Use generics with import.meta.glob<T>().
    • Structural rewrites required for:
      • router/avue-router.ts: Change plugin from a 'function object' to an 'object literal containing an install method'.
      • utils/store.ts (getStore): Split the variable into raw and obj to avoid type incompatibility between raw strings and parsed JSON.
      • utils/util.ts (findByValue): Use direct return statements in branches to align output paths.
      • router/page/index.ts & router/views/index.ts: Explicitly annotate routes as const routes: RouteRecordRaw[].
    // Example: Handling TS2554 by marking parameters optional
    getDeptTree(tenantId?: string)
    
    // Example: Handling TS2794 with Promise<void>
    new Promise<void>((resolve, reject) => { ... })
  11. Configure TypeScript Type-Checking Gate

    master

    In the second stage of the Saber 5.0 upgrade, a dedicated type-checking mechanism is introduced to ensure code quality without affecting the build process (which uses esbuild for pure transpilation).

    Key Changes

    • tsconfig.json: A new configuration dedicated to type-checking. The build process (pnpm build) does not read these check flags, decoupling checking from building.
    • Removal of jsconfig.json: To avoid parsing ambiguity, tsconfig.json now handles all path aliases (e.g., ~, components, styles, utils).
    • New Command: pnpm run type-check executes vue-tsc --noEmit. This command requires zero errors across the entire repository to pass.

    JS Coexistence Strategy

    To support existing .js files in api/, utils/, store/, etc., use the following configuration in tsconfig.json:

    • allowJs: true: Allows .js files to be referenced by TS and participate in type inference.
    • checkJs: false: Ensures .js files themselves are not checked for errors.

    Note: JSDoc in .js files is used for signature inference. For optional parameters, use the square bracket syntax: @param {number} [defaultValue].

    pnpm run type-check
  12. Migrate Vue components to Composition API + TypeScript

    master

    The core migration pattern for Saber 5.0 involves moving from the Options API to the Vue 3 Composition API with TypeScript (<script setup lang="ts">).

    Key changes include:

    • Replacing data(), methods, and computed with ref, reactive, and computed from vue.
    • Using useStore from vuex instead of this.$store.
    • Explicitly importing utility functions (e.g., import { validateNull } from '@/utils/validate') instead of relying on global mixins.
    • Defining interfaces for entity and form data to leverage TypeScript type safety.
    • Using ElMessage and ElMessageBox from element-plus for UI feedback.
    <script setup lang="ts'>
    import { ref, reactive, computed } from 'vue';
    import { useStore } from 'vuex';
    import { ElMessage, ElMessageBox } from 'element-plus';
    import { validData } from '@/utils/util';
    import { getList, add, update, remove } from '@/api/xxx/xxx';
    
    interface XxxEntity { id: string; /* ... */ }
    type XxxForm = Partial<XxxEntity>;
    
    const store = useStore();
    const permission = computed(() => store.getters.permission);
    
    const form = ref<XxxForm>({});
    const data = ref<XxxEntity[]>([]);
    const loading = ref(true);
    const page = reactive({ pageSize: 10, currentPage: 1, total: 0 });
    
    // Example permission check using validData
    const permissionList = computed(() => ({
      addBtn: validData(permission.value.xxx_add, false),
      editBtn: validData(permission.value.xxx_edit, false),
    }));
    
    const onLoad = (pageData: { currentPage: number; pageSize: number }, params: Partial<XxxEntity> = {}) => {
      loading.value = true;
      getList(pageData.currentPage, pageData.pageSize, Object.assign(params, query.value)).then(res => {
        page.total = res.data.data.total;
        data.value = res.data.data.records;
        loading.value = false;
      });
    };
    </script>