最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Vue3实践SOLID五大设计原则

    正文概述 掘金(码农阿焦)   2021-02-26   496

    本篇内容在国外的一篇博客的基础上修改的,基于Vue3 + JavaScript实现,使用腾讯前端AlloyTeam的代码规范对演示代码进行校验,Git提交规范使用开源工具husky来验证。本文涉及的代码均以上传到GitHubGitee中。文章中有不正确的地方,请大家不吝赐教,批评指正。

    • GitHub访问地址
    • Gitee访问地址

    1. SOLID原则

    SOLID原则是面向对象编程和面向对象设计的5大基本原则,分别代表的是:

    • SSRP,单一职责原则
    • OOCP,开放封闭原则
    • LLSP,里氏替换原则
    • IISP,接口隔离原则
    • DDIP,接口隔离原则

    下面对5大基本原则做一个简单的描述,如果之前有了解,可以跳转到第2章,了解如何在Vue3中一步步实践这5大原则。

    1.1 单一职责原则

    单一职责原则规定:一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中,即定义有且仅有一个原因使类变更。

    反例:A类负责两个不同的职责:职责B和职责C。当由于职责B需求发生了改变而修改A类时,有可能会导致职责C不能正常运行了,这就说明职责B和职责C被耦合在A类。

    1.2 开放封闭原则

    开放封闭原则规定:一个实体应该对扩展开放,对修改是封闭的,也就说说,如果想对一个已完成或者已运行的实体添加新的功能,那么应该通过扩展的方式来实现对实体的变化,而不应该通过对现有的实体进行修改来实现变化。

    1.3 里氏替换原则

    里氏替换原则规定:一个对象出现的地方,都可以使用子类来替换,并且不会导致程序的错误,它的意义在于:子类可以扩展父类的功能,但是不能修改父类原有的功能。

    1.4 接口隔离原则

    接口隔离原则规定:客户端不应该被迫实现一些他们不需要的接口,而应该把胖接口中的方法分组,然后使用多个接口来替代它,确保每个接口服务于一个模块。

    1.5 依赖倒置原则

    依赖倒置原则规定:抽象不应该依赖于细节,而细节应该依赖于抽象,也就说,应该针对抽象(接口)编程,而不是针对实现细节编程。

    依赖倒置原则和开放封闭原则的关系:开放封闭原则是面向对象设计原则的基础,而依赖倒置原则是实现开放封闭原则的一个基础。

    2 初始Vue3项目

    这里使用vite来初始化Vue3项目,这里不介绍vite是什么,可移步vite官网查看。

    $ npm create @vitejs/app --template vue
    

    2.1 配置AlloyTeam代码规范检测

    这里以VSCode编辑器为例,介绍如何在Vue3项目中配置AlloyTeam代码规范检测。

    1. VSCode需要安装的插件ESLintPrettier - Code formatterVeturAuto Close Tag(可选)、Auto Rename Tag(可选)和Debugger for Chrome(可选)
    2. VSCode配置:使用组合键Ctrl+Shift+P打开Command Palette,选择Preference: Open Settings (JSON),在文件中配置一下内容:
    {
      "eslint.validate": ["javascript", "vue"],
      "editor.codeActionsOnSave": {
        "source.fixAll": true,
        "source.fixAll.eslint": true
      },
      "files.eol": "\n",
      "editor.formatOnSave": true,
      "editor.defaultFormatter": "esbenp.prettier-vscode",
    }
    
    1. 项目安装依赖:package.json文件内容如下,可直接替换devDependencies中的内容,然后使用npm install安装依赖
    {
        "name": "vite-project",
        "version": "0.0.0",
        "scripts": {
            "dev": "vite",
            "build": "vite build",
            "serve": "vite preview"
        },
        "dependencies": {
            "vue": "^3.0.5"
        },
        "devDependencies": {
            "@types/eslint": "^7.2.6",
            "@vitejs/plugin-vue": "^1.1.4",
            "@vue/compiler-sfc": "^3.0.5",
            "babel-eslint": "^10.1.0",
            "eslint": "^7.20.0",
            "eslint-config-alloy": "^3.10.0",
            "eslint-config-prettier": "^8.0.0",
            "eslint-plugin-vue": "^7.6.0",
            "prettier": "^2.2.1",
            "vite": "^2.0.1",
            "vue-eslint-parser": "^7.5.0"
        }
    }
    
    1. Vue3项目根目录下增加两个文件:.prettierrc.js.eslintrc.js
    • .eslintrc.js
    module.exports = {
        extends: ['alloy', 'alloy/vue'],
        env: {},
        globals: {},
        rules: {
            'vue/component-tags-order': [
                'error',
                {
                    order: [['template', 'script'], 'style'],
                },
            ],
        },
    };
    
    • .prettierrc.js
    module.exports = {
        // 一行最多 120 字符
        printWidth: 120,
        // 使用 4 个空格缩进
        tabWidth: 4,
        // 不使用缩进符,而使用空格
        useTabs: false,
        // 行尾需要有分号
        semi: true,
        // 使用单引号
        singleQuote: true,
        // 对象的 key 仅在必要时用引号
        quoteProps: 'as-needed',
        // jsx 不使用单引号,而使用双引号
        jsxSingleQuote: false,
        // 末尾需要有逗号
        trailingComma: 'all',
        // 大括号内的首尾需要空格
        bracketSpacing: true,
        // jsx 标签的反尖括号需要换行
        jsxBracketSameLine: false,
        // 箭头函数,只有一个参数的时候,也需要括号
        arrowParens: 'always',
        // 每个文件格式化的范围是文件的全部内容
        rangeStart: 0,
        rangeEnd: Infinity,
        // 不需要写文件开头的 @prettier
        requirePragma: false,
        // 不需要自动在文件开头插入 @prettier
        insertPragma: false,
        // 使用默认的折行标准
        proseWrap: 'preserve',
        // 根据显示样式决定 html 要不要折行
        htmlWhitespaceSensitivity: 'css',
        // vue 文件中的 script 和 style 内不用缩进
        vueIndentScriptAndStyle: false,
        // 换行符使用 lf
        endOfLine: 'lf',
        // 格式化嵌入的内容
        embeddedLanguageFormatting: 'auto',
    };
    
    1. 通过以上配置,在保存文件时,会自动检测相关代码,并按照约定的格式格则自动调整。

    2.2 配置Git提交规范验证工具:husky

    1. 安装依赖项:npm i -D husky
    2. 修改package.json:在devDependencies后添加husky,具体内容如下:
    {
        "name": "vite-project",
        "version": "0.0.0",
        "scripts": {
            "dev": "vite",
            "build": "vite build",
            "serve": "vite preview"
        },
        "dependencies": {
            "vue": "^3.0.5"
        },
        "devDependencies": {
            "@types/eslint": "^7.2.6",
            "@vitejs/plugin-vue": "^1.1.4",
            "@vue/compiler-sfc": "^3.0.5",
            "babel-eslint": "^10.1.0",
            "eslint": "^7.20.0",
            "eslint-config-alloy": "^3.10.0",
            "eslint-config-prettier": "^8.0.0",
            "eslint-plugin-vue": "^7.6.0",
            "husky": "^5.1.1",
            "prettier": "^2.2.1",
            "vite": "^2.0.1",
            "vue-eslint-parser": "^7.5.0"
        },
        "husky": {
            "hooks": {
                "commit-msg": "node script/verify-commit.js"
            }
        }
    }
    
    1. 在项目根目录下创建script文件夹,并在其中创建verify-commit.js文件
    const msgPath = process.env.HUSKY_GIT_PARAMS;
    const msg = require('fs').readFileSync(msgPath, 'utf-8').trim();
    
    // 提前定义好 commit message 的格式,如果不符合格式就退出程序。
    const commitRE = /^(feat|fix|docs|style|refactor|perf|test|workflow|build|ci|chore|release|workflow)(\(.+\))?: .{1,50}/;
    
    if (!commitRE.test(msg)) {
        console.error(`
            不合法的 commit 消息格式。
            请查看 git commit 提交规范:https://github.com/woai3c/Front-end-articles/blob/master/git%20commit%20style.md
        `);
    
        process.exit(1);
    }
    

    2.3 实现代码事件列表

    1. 删除components文件夹
    2. 增加views文件夹,并在其中创建Home.vue文件
    <script>
    import { onMounted, ref } from 'vue';
    export default {
        name: 'Home',
        setup() {
            let todos = ref([]);
    
            const fetchTodos = () => {
                fetch('https://jsonplaceholder.typicode.com/todos/')
                    .then((response) => response.json())
                    .then((response) => {
                        todos.value = response;
                    });
            };
            onMounted(() => {
                fetchTodos();
            });
    
            return {
                todos,
            };
        },
    };
    </script>
    
    <template>
        <div>
            <header class="header">
                <nav class="header-nav"></nav>
                <div class="container">
                    <h1>我的代办事项</h1>
                </div>
            </header>
            <main>
                <div class="container">
                    <div class="todo-list">
                        <div v-for="{ id, title, completed } in todos" :key="id" class="todo-list__task">
                            <span :class="{ 'todo-list__task--completed': completed }">
                                {{ title }}
                            </span>
                        </div>
                    </div>
                </div>
            </main>
        </div>
    </template>
    
    <style lang="scss">
    .header {
        width: 100%;
        &-nav {
            background: teal;
            width: 100%;
            height: 56px;
        }
    }
    .container {
        padding: 1.5rem;
    }
    .todo-list {
        display: flex;
        flex-wrap: wrap;
        justify-content: center;
        align-items: stretch;
        &__task {
            width: 24%;
            padding: 1.5rem;
            margin: 0.5%;
            text-align: left;
            color: #4169e1;
            box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
            transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
            &:hover {
                box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
            }
            &--completed {
                color: #2e8b57;
                text-decoration: line-through;
            }
        }
    }
    </style>
    
    1. 修改App.vue
    <script>
    export default {};
    </script>
    
    <template>
        <div id="app">
            <router-view />
        </div>
    </template>
    
    1. src目录下新建router.js
    import { createRouter, createWebHistory } from 'vue-router';
    
    import Home from './views/Home.vue';
    
    const routerHistory = createWebHistory();
    const router = createRouter({
        history: routerHistory,
        routes: [
            {
                path: '/',
                name: 'home',
                component: Home,
            },
        ],
    });
    
    export default router;
    
    1. 修改main.js
    import { createApp } from 'vue';
    import App from './App.vue';
    import router from './router';
    
    const app = createApp(App);
    
    app.use(router);
    app.mount('#app');
    
    1. 实现效果:
    $ npm run dev
    

    Vue3实践SOLID五大设计原则

    3 SRP改造

    在上面的示例中,前面页面的工作都是在Home.vue这个文件中实现,包括:header barnav bartodo list等。如果将SRP原则应用于当前现有的项目中:每个组件都应该只有一个改变的理由。很明显,当前的Home.vue组件不能满足SRP理由,因为它有多个可以改变的理由:

    • 使用axios替换fetch
    • 根据需求添加其他元素,例如:sidebarimg
    • 改变列表的展示形式
    • ...

    当整个系统功能越来越多,系统越来越庞大的时候,我们可能对Home.vue文件失去控制,因此,尝试使用SRP原则对其进行改造。

    3.1 抽取访问RESTful接口的内容

    在项目根目录创建services文件夹,然后在该文件夹中创建api.js文件,内容如下:

    export class Api {
        constructor(url) {
            this.baseUrl = 'https://jsonplaceholder.typicode.com/';
            this.url = url;
        }
    
        async fetch() {
            const response = await fetch(`${this.baseUrl}``${this.url}`);
            return await response.json();
        }
    }
    

    3.2 拆分页面组件

    headerHome.vue中拆分出来,在components文件夹中创建MyHeader.vue文件:

    <script>
    export default {
        name: 'MyHeader',
        props: ['listName'],
    };
    </script>
    
    <template>
        <header class="header">
            <nav class="header-nav" />
            <div class="container">
                <h1>{{$props.listName}}</h1>
            </div>
        </header>
    </template>
    
    <style lang="scss">
    .header {
        width: 100%;
        &-nav {
            background: teal;
            width: 100%;
            height: 56px;
        }
    }
    </style>
    

    todo-list也从Home.vue中拆分出来,在components文件夹中创建MyTodoList.vue文件:

    <script>
    export default {
        name: 'MyTodoList',
        props: ['todos']
    }
    </script>
    
    <template>
        <div class="container">
            <div class="todo-list">
                <div v-for="{ id, title, completed } in $props.todos" :key="id" class="todo-list__task">
                    <span :class="{ 'todo-list__task--completed': completed }">
                        {{ title }}
                    </span>
                </div>
            </div>
        </div>
    </template>
    
    <style lang="scss">
    .todo-list {
        display: flex;
        flex-wrap: wrap;
        justify-content: center;
        align-items: stretch;
        &__task {
            width: 24%;
            padding: 1.5rem;
            margin: 0.5%;
            text-align: left;
            color: #4169e1;
            box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
            transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
            &:hover {
                box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
            }
            &--completed {
                color: #2e8b57;
                text-decoration: line-through;
            }
        }
    }
    </style>
    

    3.3 修改Home.vue

    修改Home.vue文件,引入刚刚拆分的组件和API接口:

    <script>
    import { onMounted, ref } from 'vue';
    import MyHeader from '../components/MyHeader.vue';
    import MyTodoList from '../components/MyTodoList.vue';
    import { Api } from '../services/api'
    export default {
        name: 'Home',
        components: {
            MyHeader,
            MyTodoList
        },
        setup() {
            let todos = ref([]);
            const fetchTodos = async () => {
                const api = new Api('todos')
                return await api.fetch()
            };
            onMounted(async () => {
                todos.value = await fetchTodos();
            });
            return {
                todos,
            };
        },
    };
    </script>
    
    <template>
        <div>
            <MyHeader :listName="代办事项列表"/>
            <main>
                <MyTodoList :todos="todos"/>
            </main>
        </div>
    </template>
    
    <style lang="scss">
    .container {
        padding: 1.5rem;
    }
    </style>
    

    通过以上步骤,使演示示例基本满足SRP原则的规定,即:

    1. api.js文件对应着RESTful接口的调用
    2. MyHeader.vue文件对应着header
    3. MyTodoList.vue文件对应着todo-list代办事项列表

    4 OCP改造

    OCP原则规定:对扩展开放,对修改关闭。会看下当前示例,现在展示具体的代办事项使用Card的形式来展示的,那么如果想用Row的方式来展示,就要修改现有的MyTodoList.vue文件,这显然与不符合OCP原则。具体的改造方法如下:

    4.1 拆分Card表示形式

    MyTodoList.vue文件中涉及的Card表现形式的相关代码抽离出来,添加到具体的文件中。在components文件夹中新建文件MyTodoCard.vue文件,内容如下:

    <script>
    export default {
        name: 'MyTodoCard',
        props: ['todo']
    }
    </script>
    
    <template>
        <div class="todo-list__task">
            <span :class="{ 'todo-list__task--completed': $props.todo.completed }">
                {{ $props.todo.title }}
            </span>
        </div>
    </template>
    
    <style lang="scss" scoped>
    .todo-list {
      &__task {
        width: 24%;
        padding: 1.5rem;
        margin: 0.5%;
        text-align: left;
        color: #4169e1;
        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
        transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
        &:hover {
          box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25),
            0 10px 10px rgba(0, 0, 0, 0.22);
        }
        &--completed {
          color: #2e8b57;
          text-decoration: line-through;
        }
      }
    }
    </style>
    

    4.2 修改MyTodoList

    修改MyTodoList.vue文件,主要是使用slot插槽的形式替换之前具体的Card表现形式,并移除多余的CSS代码:

    <script>
    export default {
        name: 'MyTodoList'
    }
    </script>
    
    <template>
        <div class="container">
            <div class="todo-list">
                <slot/>
            </div>
        </div>
    </template>
    
    <style lang="scss">
    .todo-list {
        display: flex;
        flex-wrap: wrap;
        justify-content: center;
        align-items: stretch;
    }
    </style>
    

    4.3 修改Home.vue

    修改Home.vue文件,引入MyTodoCar.vue文件,MyTodoList元素中引入MyTodoCar,为代办事项设置对应的表示形式:

    <script>
    import { onMounted, ref } from 'vue';
    import MyHeader from '../components/MyHeader.vue';
    import MyTodoList from '../components/MyTodoList.vue';
    import MyTodoCard from '../components/MyTodoCard.vue';
    import { AxiosApi } from '../services/AxiosApi';
    
    export default {
        name: 'Home',
        components: {
            MyHeader,
            MyTodoList,
            MyTodoCard
        },
        setup() {
            let todos = ref([]);
    
            const fetchTodos = async () => {
                const api = new AxiosApi()
                return await api.fetch('todos')
            };
            onMounted(async () => {
                todos.value = await fetchTodos();
            });
    
            return {
                todos,
            };
        },
    };
    </script>
    
    <template>
        <div>
            <MyHeader listName="代办事项列表"/>
            <main>
                <MyTodoList>
                    <!-- <MyTodoCard v-for="todo in todos" :key="todo.id" :todo="todo"/> -->
                    <MyTodoRow v-for="todo in todos" :key="todo.id" :todo="todo"/>
                </MyTodoList>
            </main>
        </div>
    </template>
    
    <style lang="scss">
    .container {
        padding: 1.5rem;
    }
    
    </style>
    

    4.4 新增Row.vue文件

    增加一种新的代办事项的展示形式,来验证经过上述修改是否能满足OCP原则的规定。

    1. components文件夹中创建MyTodoRow.vue文件
    <script>
    export default {
        name: 'MyTodoRow',
        props: ['todo']
    }
    </script>
    
    <template>
        <div class="todo-list__row">
            <span>{{$props.todo.id}}.</span>
            <span :class="{ 'todo-list__row--completed': $props.todo.completed }">
                {{$props.todo.title}}
            </span>
        </div>
    </template>
    
    <style lang="scss">
    .todo-list {
      &__row {
        width: 100%;
        text-align: left;
        color: #4169e1;
        &--completed {
          text-decoration: line-through;
          color: #2e8b57;
        }
      }
    }
    </style>
    
    1. Home.vue文件中使用MyTodoRow替换MyTodoCard
    <script>
    import { onMounted, ref } from 'vue';
    import MyHeader from '../components/MyHeader.vue';
    import MyTodoList from '../components/MyTodoList.vue';
    // import MyTodoCard from '../components/MyTodoCard.vue';
    import MyTodoRow from '../components/MyTodoRow.vue';
    import { Api } from '../services/api';
    
    export default {
        name: 'Home',
        components: {
            MyHeader,
            MyTodoList,
            // MyTodoCard,
            MyTodoRow
        },
        setup() {
            let todos = ref([]);
    
            const fetchTodos = async () => {
                const api = new Api('todos')
                return await api.fetch()
            };
            onMounted(async () => {
                todos.value = await fetchTodos();
            });
    
            return {
                todos,
            };
        },
    };
    </script>
    
    <template>
        <div>
            <MyHeader listName="代办事项列表"/>
            <main>
                <MyTodoList>
                    <!-- <MyTodoCard v-for="todo in todos" :key="todo.id" :todo="todo"/> -->
                    <MyTodoRow v-for="todo in todos" :key="todo.id" :todo="todo"/>
                </MyTodoList>
            </main>
        </div>
    </template>
    
    <style lang="scss">
    .container {
        padding: 1.5rem;
    }
    </style>
    

    Vue3实践SOLID五大设计原则

    可以看到,修改代办事项的表现形式,就需要增加一个.vue文件来实现具体的表现形式,然后在Home.vue文件中做简单的修改,并且MyTodoList.vue文件保持不变。到这里,基本实现OCP原则。

    5 LSP改造

    LSP原则规定:当对父类进行扩展时,能够使用子类的对象来替换之前使用的父类的对象,从而不会破坏客户机的代码。具体到当前演示示例该如何实现呢?考虑这种情况:现在基于axios来从服务器获取todo-list,并且不修改Home.vue的文件,这种情况该怎么实现呢?

    5.1 修改API传参方式

    将传递URL具体地址的入口由new Api()转为fetch函数,即:api.js的内容:

    export class Api {
        constructor() {
            this.baseUrl = 'https://jsonplaceholder.typicode.com/';
        }
    
        async fetch(url) {
            const response = await fetch(`${this.baseUrl}${url}`);
            return await response.json();
        }
    }
    

    Home.vue也做相应的修改,主要是用来适配api.js的修改,修改后的setup的内容为:

       setup() {
            let todos = ref([]);
    
            const fetchTodos = async () => {
                const api = new Api('todos');
                return await api.fetch();
            };
            onMounted(async () => {
                todos.value = await fetchTodos();
            });
    
            return {
                todos,
            };
        },
    

    5.2 添加基类BaseApi

    services文件夹中创建文件BaseApi.js作为API的基类,主要存储URL中的服务器信息,并使用fetch的方式来获取服务器返回数据,内容如下:

    export class BaseApi {
        constructor() {
            this.baseUrl = 'https://jsonplaceholder.typicode.com/';
        }
    
        async fetch(url) {
            const response = await fetch(`${this.baseUrl}${url}`);
            return await response.json();
        }
    }
    

    5.3 扩展基类使用axios查询RESTful接口

    services文件夹中创建文件AxiosApi.js,使用axios来查询RESTful接口。

    import axios from 'axios';
    import { BaseApi } from './BaseApi.js';
    
    export class AxiosApi extends BaseApi {
        async fetch(url) {
            const { data } = await axios.get(`${this.baseUrl}${url}`);
            return data;
        }
    }
    

    5.4 修改api.js

    接下来,需要修改api.js文件,用来调用BaseApi.js或者AxiosApi.js

    import { BaseApi } from './BaseApi';
    
    export class Api {
        constructor() {
            this.apiProvider = new BaseApi();
        }
    
        async fetch(url) {
            return await this.apiProvider.fetch(url);
        }
    }
    

    如果想替换成axios的方式来获取服务器数据,只需要将引入import { AxiosApi } from './AxiosApi',并修改apiProvider的实例化方式即可。这样我们就完成参照LSP规则对演示示例的改造。

    6 ISP改造

    ISP规则应用在组件情形时,表示:不应该强迫组件依赖于它们不使用的属性或者方法。来回顾之前的代码,主要观察对于MyTodoRowMyTodoCard的传参方式,是把整个todo对象传递给了这两个组件,但是对象中的userId属性在这两个组件中都没有用到,并且MyTodoCard没有使用到id这个属性。可见,当前的演示示例违反了ISP原则。

    解决这个问题的两种方法:

    1. todo实例切分成更小的实例
    2. 向组件传递它需要的数据

    这里采用第2种解决方法。分别修改MyTodoCard.vueMyTodoRow.vue文件,将需要使用的数据属性添加到props中,然后修改Home.vue文件,修改向其传递数据的方式。

    • MyTodoCard.vue
    <script>
    export default {
        name: 'MyTodoRow',
        props: ['id', 'completed', 'title'],
    };
    </script>
    
    <template>
        <div class="todo-list__row">
            <span>{{ $props.id }}.</span>
            <span :class="{ 'todo-list__row--completed': $props.completed }">
                {{ $props.title }}
            </span>
        </div>
    </template>
    
    <style lang="scss">
    .todo-list {
        &__row {
            width: 100%;
            text-align: left;
            color: #4169e1;
            &--completed {
                text-decoration: line-through;
                color: #2e8b57;
            }
        }
    }
    </style>
    
    • MyTodoCard.vue
    <script>
    export default {
        name: 'MyTodoCard',
        props: ['completed', 'title'],
    };
    </script>
    
    <template>
        <div class="todo-list__task">
            <span :class="{ 'todo-list__task--completed': $props.completed }">
                {{ $props.title }}
            </span>
        </div>
    </template>
    
    <style lang="scss" scoped>
    .todo-list {
        &__task {
            width: 24%;
            padding: 1.5rem;
            margin: 0.5%;
            text-align: left;
            color: #4169e1;
            box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
            transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
            &:hover {
                box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
            }
            &--completed {
                color: #2e8b57;
                text-decoration: line-through;
            }
        }
    }
    </style>
    
    • Home.vue
    <script>
    import { onMounted, ref } from 'vue';
    import MyHeader from '../components/MyHeader.vue';
    import MyTodoList from '../components/MyTodoList.vue';
    import MyTodoCard from '../components/MyTodoCard.vue';
    // import MyTodoRow from '../components/MyTodoRow.vue';
    import { Api } from '../services/api.js';
    
    export default {
        name: 'Home',
        components: {
            MyHeader,
            MyTodoList,
            MyTodoCard,
            // MyTodoRow,
        },
        setup() {
            let todos = ref([]);
    
            const fetchTodos = async () => {
                const api = new Api();
                return await api.fetch('todos');
            };
            onMounted(async () => {
                todos.value = await fetchTodos();
            });
    
            return {
                todos,
            };
        },
    };
    </script>
    
    <template>
        <div>
            <MyHeader listName="代办事项列表" />
            <main>
                <MyTodoList>
                    <MyTodoCard v-for="todo in todos" :key="todo.id" : :completed="todo.completed" />
                    <!-- <MyTodoRow
                        v-for="todo in todos"
                        :id="todo.id"
                        :key="todo.id"
                        :
                        :completed="todo.completed"
                    /> -->
                </MyTodoList>
            </main>
        </div>
    </template>
    
    <style lang="scss">
    .container {
        padding: 1.5rem;
    }
    </style>
    

    7 DIP改造

    DIP规则规定:高级类(组件)不能依赖低级类(组件),他们两个都应该依赖抽象,抽象不能依赖细节,而是细节应该依赖抽象。其中:

    • 低级类用来实现基础的操作,例如:与API对接;
    • 高级类包含了复杂的业务逻辑,这些逻辑用来指导低级类进行某些操作。

    参照DSP规则,修改api相关的类文件。

    作者原文中使用TypeScript实现的DIP规则。JavaScript中没有接口这个功能,但是可以使用模块封装来实现类似的行为,例如:导出类或者方法。这里使用导出类的方法来实现DIP规则。

    1. 修改BaseApi.js文件,实现类似接口的功能
    export class BaseApi {
        constructor() {
            this.baseUrl = 'https://jsonplaceholder.typicode.com/';
        }
    
        async fetch(url) {
            throw new Error('没有具体的实现方法');
        }
    }
    
    1. services文件夹创建FetchApi.js文件,继承BaseApi.js方法,使用fetch方法来实现获取数据
    import { BaseApi } from './BaseApi.js';
    
    export class FetchApi extends BaseApi {
        async fetch(url) {
            const response = await fetch(`${this.baseUrl}${url}`);
            return await response.json();
        }
    }
    
    1. 修改api.js文件,在这个文件中,可以切换AxiosApi.jsFetchApi.js文件的引用,调用不同的方式来获取服务器数据
    import { AxiosApi } from './AxiosApi';
    
    export class Api {
        constructor() {
            this.apiProvider = new AxiosApi();
        }
    
        async fetch(url) {
            return await this.apiProvider.fetch(url);
        }
    }
    

    8 参考

    • 设计模式之SOLID原则
    • 前端工程(二):统一规范
    • How to avoid SOLID principles violations in Vue. JS application
    • Decoupling code in JavaScript with the Dependency Inversion Principle

    起源地下载网 » Vue3实践SOLID五大设计原则

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    除了单一跟开闭,其他三个都有点抽象了
    回复(0)

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元