对“数据服务子系统”前端项目的理解

619 阅读8分钟

一. 目录结构

  • .editorconfig
    帮助开发人员定义和维护编程风格,有些编辑器不用安装插件,会自动识别.editorconfig文件,然后会按文件中的规范设置编程风格

  • .env.development
    开发环境加载的配置文件

  • .env.production

生产环境加载的配置文件

  • jsconfig.json 表明该目录是 JavaScript 项目的根目录,webpack别名配置好以后,如果想要在vscold中可以正确提示路径,就需要配置jsconfig.json文件

  • postcss.congif.js
    利用JS插件来对CSS进行转换的工具,可以写正常的CSS,也可以结合LESS或者SASS一起编写

  • prettierrc.json prettier格式化代码时的配置文件

  • babel.config.js 整个项目中babel的配置文件

  • vue.config.js webpack的的配置文件,webpack在真正打包构建之前,会先读取这个文件,从而基于给定的配置,对项目进行打包

工程化选型

代码规范

代码规范一直是软件开发人员一直讨论的的话题,几乎所有工程师在开发过程中都会遇到,并思考过这一问题。随着前端应用的复杂和庞大,前端工程都已全面推广代码规范化,大多数团队在项目开发过程中使用eslint和Prettier的相互配合,以此来规范代码。在格式化代码方面, Prettier 确实和 ESLint 有重叠,但两者侧重点不同:ESLint 主要工作就是检查代码质量并给出提示,它所能提供的格式化功能很有限;而 Prettier 在格式化代码方面具有更大优势。

eslint

安装

// 项目中安装
npm install eslint --save-dev
// or
yarn add eslint --dev

// 全局安装
npm install eslint -g
// or
yarn add eslint -g

安装完了eslint,如果我们想让我们在写代码的同时就知道自己代码的问题,可以在vsode中安装以下eslint插件。

1650939061(1).png

配置

安装成功后,.eslintrc.js文件中添加自定义配置项

使用

在写项目时,如果不符合eslint中配置的规范,则会在写代码时就看到报错

prettier

安装

vscode中安装如下prettier插件

image.png

配置

在项目根目录生成prettier配置文件

使用

打开设置,找到Format选项,选择Prettier作为格式化拓展插件,如下: 1650946731(1).png

当然此设置也可直接在setting.json中添加如下代码: "editor.defaultFormatter": "esbenp.prettier-vscode"

vscode插件和npm包区别

以eslint为例,vscode插件和npm包的区别:

eslint插件和eslint的npm包是相辅相成的,vscode中的eslint插件,需要项目里或者全局安装eslint包,插件首先调用的,就是当前项目目录下的eslint包,如果没有,就会去查找有没有全局安装的eslint包。插件检查代码时,还是在调用eslint包来进行检查,然后将报错反馈给vscode,这样我们才能在vscode编辑器里,直接看到代码里的红色波浪线等错误提示; 如果只安装了eslint npm包,没有安装eslint插件,那在vscode中,代码不会有红色波浪线等警告提示,我们要进行代码质量检查的话,需要在终端里运行项目来查看代码报错情况。

vscode配置自动保存

配置好了eslint和prettier,我们需要在保存的时候,代码按eslint格式进行修复,按照prettier自动格式化,可在setting.json文件中加入如下代码:

 //代码按照eslint进行修复
 "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true // 每次保存的时候将代码按eslint格式进行修复
  },
//代码按照prettier进行保存格式化
{
  "prettier.eslintIntegration": true, // prettier 集成eslint
  "editor.formatOnSave": true // 保存自动格式化
}

注意:以上配置能生效是在eslint与prettier没有冲突的情况下才是可行的,否则代码校验会出现异常。
原因是:在VSCode中配置了保存自动格式化后,eslint中将代码进行格式化后,会重新被prettier再次格式化。因此最终的格式化效果是Prettier提供的,然而我们代码校验使用的是ESLint,因此往往会出现冲突,最常见的冲突就是:eslint fix后变成单引号,保存(prettier)自动格式化后变成双引号,导致代码校验异常。

解决eslint与prettier的冲突

我们知道eslint和prettier中有好多的规则,我们也无法保证在eslint和prettier中配置的规则是没有冲突的,如果有冲突的话会一直报错,这个时候我们可以用[eslint-config-prettier]和[eslint-plugin-prettier]来解决,他们的功能如下所示:

  • 使用eslint-config-prettier禁用掉ESLint中与Prettier冲突的规则,关闭eslint中与prettier相互冲突的规则。
  • 使用eslint-plugin-prettier添加ESLint的Prettier功能的实现,是赋予eslintprettier格式化代码的能力。

项目中的实现

安装:

npm install eslint-plugin-prettier eslint-config-prettier

有了这两个库,在稍微修改一下eslint配置即可:

module.exports = {
  plugins: ['prettier'], // eslint-plugin-prettier的缩写
  extends: [
    'prettier', // eslint-config-prettier的缩写
  ],
  rules: {
    'prettier/prettier': 'error'
  }
}
  1. extends: ['prettier']: 通过 eslint-config-prettier 关闭eslintprettier 相冲突的规则。
  2. plugins: ['prettier']: 加载 eslint-plugin-prettier,赋予 eslintprettier 格式化文档的功能
  3. 'prettier/prettier': 'error': 让代码文件中不符合prettier格式化规则的都标记为错误,结合vscode-eslint插件便可以看到这些错误被标记为红色,当运行eslint --fix 命令时,将自动修复这些错误。

至此, prettiereslint 便可以无冲突协作,保存时候也能自动修复并格式化代码了。

注意:

  1. {extends: ['eslint:recommended','prettier', // 必须放最后 ],}
  2. 如果修改了.prettierrc的配置选项,会发现 eslint 和 prettier又冲突了,这是因为vscode插件缓存没有及时更新,重启下vscode即可。
  3. 以下两种写法一样
// .eslintrc    
{      
    "plugins": ["prettier"],      
    "rules": {        
        "prettier/prettier": "error"      
    }    
}

将上面两个步骤和在一起就是下面的配置,也是官方的推荐配置

// .eslintrc
{
  "extends": ["plugin:prettier/recommended"]
}

第三方组件库的封装

为什么要封装组件

我们知道现在前端已经有了好多ui组件库,每个库都有自己的特点,但是有些情况下这些库的有些组件可能满足不了我们的需求,这个时候可以对这些组件进行二次封装。

优点

  1. 便于切换UI库
    当自己的软件版本升级时,可能会出现我们使用的这个库不能满足新的需求,需要更换第三方库的情况,这时,如果使用了二次封装,那么库的更改和自己编写的应用层就可以脱离,只需要改写二次封装就可以实现。

  2. 封装后的组件更符合我们的项目需求

  3. 灵活,维护成本低
    比如一个image组件, 我们需要统一在图片加载失败的时候展示的特定图,每次使用组件都加一遍, 麻烦耗时,关键是维护成本高,当需要更新这个加载出错的图片时, 得再次一个个去找到使用该组件的地方修改。

  4. 当需求必须对第三方库添加额外功能时,可以添加在二次封装层,避免了对第三方库本身的修改,减少了潜在危险。
    所以我觉得大部分组件还是自己封装来的更为方便和灵活一些。

组件要怎么封装

对于封装组件有一个大原则就是我们应该尽量保持原有组件的接口,除了我们需要封装的功能外,我们不应该改变原有组件的接口,即保持原有组件提供的接口如props,events,slots等不变。为了实现这一原则我们就需要将新组件的接口与旧组件的接口一一对应, 这个时候v-bind="attrs"v-on="$listener"就会发挥重要的作用,它们可以使得封装后的组件“继承”原组件的几乎所有 v-bind 属性和 v-on 事件,且用法和作用与在原组件一样。

关键技术点介绍

1. v-bind="$attrs"

v-bind="$attrs"的妙用是在创建更高级别的组件,在封装第三方组件时,可以自动将在父作用域中使用的v-bind的属性自动绑定,并向下传入被封装的使用了v-bind="$attrs"的组件。 vue官网中是这样介绍的

包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用。

意思就是:$attrs 可以收集父组件中的所有传过来的属性除了那些在组件中没有通过 props 定义的。

比如有组件App.vue,Father.vue,Sun.vue,我们把App.vue组件当做爷组件、而对应Father.vue就是父组件,同时sun.vue就是孙子组件。也就是爷、父、子的这种祖孙关系组件。我们在这样的结构中去实现爷组件到孙子组件中的数据传递。其实,爷组件传递给孙组件的逻辑流程就是,通过爷组件首先传递给父组件,当然父组件不在props中接收,那么爷组件传递给父组件的数据就会存放到父组件的$attrs对象中里面了,然后,再通过v-bind="$attrs",再把这个$attr传递给孙组件,在孙组件中使用props就能接收到$attrs中的数据了,这样就实现了,祖孙之间的数据传递。

App.vue

<template>
  <div id="app">
    我是爷爷组件
    <div>孙子组件传递过来的数据:{{msg}}</div>
    <father
      :aaa="aaa"
      :bbb="bbb"
      :ccc="ccc"
      :ddd="ddd"
      @fromSun="fromSun"
    ></father>
  </div>
</template>

<script>
import father from './views/Father.vue'
export default {
  components: {
    father
  },
  data () {
    return {
      msg: '',
      aaa: 'aaa',
      bbb: 'bbb',
      ccc: 'ccc',
      ddd: 'ddd'
    }
  },
  methods: {
    fromSun (payload) {
      this.msg = payload
    }
  }
}
</script>

Father.vue

<template>
<div class="fatherClass">
  我是父组件
  <h2>$attrs:{{$attrs}}</h2>
  <h2>{{ aaa }}</h2>
  <h2>{{ $attrs.bbb}}</h2>
  <h2>{{ $attrs.ccc}}</h2>
  <h2>{{ $attrs.ddd}}</h2>
  <sun v-bind="$attrs" v-on="$listeners"></sun>

</div>

</template>

<script>
import Sun from './Sun.vue'
export default {
inheritAttrs: false,
components: { Sun },
props: {
  aaa: {
    type: String,
    default: ''
  }
},
mounted () {
  console.log('fu组件实例', this.$attrs)
}
}
</script>

Sun.vue

<template>
  <div class="sunClass">
    我是孙子组件
    <h2>接收爷组件数据:-->{{ bbb }}</h2>
    <h2>接收爷组件数据:-->{{ ccc }}</h2>
    <h2>接收爷组件数据:-->{{ ddd }}</h2>
    <button @click="fun">点击向爷爷组件传递数据</button>
  </div>
</template>
<script>
export default {
  // $attrs一般搭配interitAttrs 一块使用
  // inheritAttrs: false, // 默认会继承在html标签上传递过来的数据
  /*
    孙子组件通过props,就能接收到父组件传递过来的$attrs了,就能拿到里面的数据了,也就是:
    爷传父、父传子。即:祖孙之间的数据传递。
  */
  data () {
    return {
    }
  },
  props: {
    bbb: {
      type: String,
      default: ''
    },
    ccc: {
      type: String,
      default: ''
    },
    ddd: {
      type: String,
      default: ''
    }
  },
  methods: {
    fun () {
      this.$emit('fromSun', '我是孙子组件的数据')
    }
  }
}
</script>

子组件须加上v-bind=“$attrs”,孙组件才能接收上数据。另外 不想继承所有父组件的内容,同时也不在组件根元素dom上显示属性就加上inheritAttrs: false,为true时是继承所有父组件的值,与data同级别。
注意: 不想传递给孙组件某个属性,需要子组件用props接收。

默认情况下父作用域的不被认作 props 的 attribute 绑定 (attribute bindings) 将会“回退”且作为普通的 HTML attribute 应用在子组件的根元素上。当撰写包裹一个目标元素或另一个组件的组件时,这可能不会总是符合预期行为。通过设置 inheritAttrs 到 false,这些默认行为将会被去掉。 如下图所示:

1651035027(1).png

如果不设置时,如下图所示:

1651035121(1).png

2. v-on="$listener

vue官网中是这么介绍的:

包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件——在创建更高层次的组件时非常有用。

组件之间的关系也如上所示,运行结果如下图所示:点击孙子组件中的按钮,可以触发到爷爷组件中的事件并向爷爷组件传递数据。

1651034983(1).png 总结:子组件触发的事件,都会在父组件的$listeners中,在父组件中就可以通过v-on=$listeners的方式继续向上一层组件传递

3.插槽

3.1 匿名插槽

匿名插槽就是一个没有name属性的插槽。父组件中除了具名包裹内容外,其他内容全部插入到匿名插槽中。 匿名插槽虽然没有name属性,但出口会带有一个隐含的名字“default”。单独使用匿名插槽时,父组件中可以不标注default,但使用多个插槽时,最好以名字区分插槽。

<!-- 子组件 -->
<template>
  <div class="child">
    <h3>子组件:匿名插槽</h3>
    <slot></slot>
  </div>
</template>

<!-- 父组件 -->
<template>  
  <div class="parent">    
    <child>      
      <template>        
        <p>插入匿名插槽</p>      
      </template>    
    </child>  
  </div>
</template>

3.2 具名插槽
具名插槽就是一个有name属性的插槽。

<!-- 子组件 -->
<template>
  <div class="child">
    <h3>子组件:具名插槽</h3>
    <slot name="child"></slot>
  </div>
</template>

<!-- 父组件 -->
<template>  
  <div class="parent">    
    <child>      
      <template v-slot:child>         
        <p>插入具名插槽</p>      
      </template> 
    </child>  
  </div>
</template>

<!--父组件也可写为 -->
<template>  
  <div class="parent">    
    <child>      
      <p slot="child">插入具名插槽</p>      
    </child>  
  </div>
</template>

具名插槽在vue2.6之后也可以用#'插槽名代替'

v-slot和slot的用法还是有区别的,v-slot只能用于template和组件,而slot属性可以加在任意标签上。 3.3 作用域插槽
匿名插槽和具名插槽仅仅只能把Dom插入到封装好的组件中,而不能获取组件中的数据。

比如我们需要在父组件中操作子组件中的数据,这时可以通过在子组件中通过v-bind绑定需要暴露出去的数据,然后要获取子组件中的数据,就要使用作用域插槽。

<!-- 子组件 -->
<template>
  <div class="child">
    <h3>子组件:作用域插槽</h3>
    <slot name="user" :user="user"></slot>
  </div>
</template>
<script>
export default {
  data() {
    return {
      user: {
        name: 'aaa'
      }
    }
  }
}
</script>

<!-- 父组件 -->
<template>  
  <div class="parent">    
    <child>      
      <template #user="{ user }">        
        <p>插入作用域插槽 {{ user.name }}</p>
      </template>    
    </child>  
  </div>
</template>
  
<!-- 父组件也可写为 -->
<template>  
  <div class="parent">    
    <child>      
      <template slot="user" slot-scope="user">        
        <p>插入作用域插槽{{ user.name }}/p>
      </template>    
    </child>  
  </div>
</template>

组件封装注意事项

  • 注释:不管是公共组件还是其它组件,注释这个习惯的问题还是强调一下,写的注释要具有可读性,最好有注释规范。
  • 命名:公共组件命名一般和他人形成统一的规约,一般对于第三方组件的封装,命名要和第三方组件有一定的联系,这样使用起来会比较方便。
  • 尽量避免不改变原有组件的props,events和slots.