Vue3实践SOLID五大设计原则

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

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

image-20210224140216138

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>

image-20210225094748530

可以看到,修改代办事项的表现形式,就需要增加一个.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" :title="todo.title" :completed="todo.completed" />
                <!-- <MyTodoRow
                    v-for="todo in todos"
                    :id="todo.id"
                    :key="todo.id"
                    :title="todo.title"
                    :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 参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值