Vue项目经验总结(持续更新中...)

14,698 阅读13分钟

前言

本文讲述了毕业实习和正式工作1年以来,使用Vue开发项目的一些个人经历和想法,仅是个人总结,如有不合理的地方,欢迎吐槽。以下是本文的大概内容。

1.Vue项目搭建

1.1从VueCli2到VueCli3

一开始实习接触Vue的脚手架是VueCli2版本,学习的webpack配置也都Cli2的,后来公司使用的是Cli3,所以有一个学习和适应的过程。
VueCli2和VueCli3的差别大概体现在:

  • 创建项目

3.0:vue create。
2.0:vue init webpack 

  • 启动项目

3.0启动npm run serve
2.0启动npm run dev

  • 项目配置途径

2.0 config、build文件夹中进行项目的webpack、多环境和打包等配置
3.0 项目结构比2.0要简洁,缺少了build和confilg文件,可自行创建与package.json同级的 vue.config.js 文件,进行配置。
主要的常用配置整理如下:

// vue.config.js 基本配置方法
module.exports = {
  // 项目部署的基础路径
  // 我们默认假设你的应用将会部署在域名的根部,
  // 比如 https://www.my-app.com/
  // 如果你的应用时部署在一个子路径下,那么你需要在这里
  // 指定子路径。比如,如果你的应用部署在
  // https://www.foobar.com/my-app/
  // 那么将这个值改为 `/my-app/`
  // 基本路径 baseURL已经过时
  publicPath: './',
  
	// 打包项目时构建的文件目录,用法与webpack本身的output.path一致
  outputDir: 'dist', 
  
  // 静态资源目录 (js, css, img, fonts)
  assetsDir: 'assets',
  
  // eslint-loader 是否在保存的时候检查,编译不规范时,设为true在命令行中警告,若设为error则不仅警告,并且编译失败
  lintOnSave: true,
  
  // 调整内部的 webpack 配置。查阅 https://github.com/vuejs/vue-docs-zh-cn/blob/master/vue-cli/webpack.md
  chainWebpack: () => {},
  configureWebpack: () => {},
  
  // vue-loader 配置项 https://vue-loader.vuejs.org/en/options.html
   vueLoader: {},
  
  // 生产环境是否生成 sourceMap 文件,默认true,若不需要生产环境的sourceMap,可以设置为false,加速生产环境的构建
  productionSourceMap: true,
  
  // css相关配置
  css: {
   // 是否使用css分离插件 ExtractTextPlugin,采用独立样式文件载入,不采用<style>方式内联至html文件中
   extract: true,
   // 是否在构建样式地图,false将提高构建速度
   sourceMap: false,
   // css预设器配置项
   loaderOptions: {},
   // 启用 CSS modules for all css / pre-processor files.
   // 这个选项不会影响 `*.vue` 文件
   modules: false
  },
  
  // 在生产环境下为 Babel 和 TypeScript 使用 `thread-loader`
  // 在多核机器下会默认开启。
  parallel: require('os').cpus().length > 1,
  
  // 是否启用dll See https://github.com/vuejs/vue-cli/blob/dev/docs/cli-service.md#dll-mode
  dll: false,
  
  // PWA 插件相关配置 see https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa
  pwa: {},
  
  // webpack-dev-server 相关配置
  devServer: {
   open: process.platform === 'darwin',
   host: '0.0.0.0',//如果是真机测试,就使用这个IP
   port: 1234,
   https: false,
   hotOnly: false,
   proxy: null, // 设置代理
   // proxy: {
   //     '/api': {
   //         target: '<url>',
   //         ws: true,
   //         changOrigin: true
   //     }
   // },
   before: app => {}
  },
  // 第三方插件配置
  pluginOptions: {
   // ...
  }
 }

1.2 Axios二次封装

axios二次封装的目的主要是三个方面:

  • 接口的请求拦截处理(配置处理、拦截重复请求)
  • 接口的响应拦截处理
  • API方法的封装复用

1.2.1 接口请求拦截处理

1.2.1.1 可配置项

在进行接口请求拦截进行配置处理的时候,针对以下参数,可以灵活配置。

参数意义例子
url用于请求的服务器 URLurl: '/user'
method创建请求时使用的方法method: 'get'
baseURL自动加在 url 前面,除非 url 是一个绝对 URL,通过设置一个 baseURL 便于为 axios 实例的方法传递相对 URLbaseURL: 'some-domain.com/api/'
transformRequest允许在向服务器发送前,修改请求数据
 // 只能用在 'PUT', 'POST' 和 'PATCH' 这几个请求方法
 // 后面数组中的函数必须返回一个字符串,或 ArrayBuffer,或 Stream
transformRequest: [function (data) {
   // 对 data 进行任意转换处理
   return data;
 }],
headers即将被发送的自定义请求头 headers: {'X-Requested-With': 'XMLHttpRequest'},
params即将与请求一起发送的 URL 参数params: {
   ID: 12345
},
paramsSerializer负责 params 序列化的函数(e.g. www.npmjs.com/package/qs, api.jquery.com/jquery.para…)paramsSerializer: function(params) {
   return Qs.stringify(params, {arrayFormat: 'brackets'})
 }
data data 是作为请求主体被发送的数据
  只适用于这些请求方法 'PUT', 'POST', 和 'PATCH'
  在没有设置 transformRequest 时,必须是以下类型之一:
- string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
- 浏览器专属:FormData, File, Blob
- Node 专属: Stream
data: {
   firstName: 'Fred'
 }
timeout指定请求超时的毫秒数(0 表示无超时时间)timeout: 1000
adapter允许自定义处理请求,以使测试更轻松,返回一个 promise 并应用一个有效的响应adapter: function (config) {
   /* ... */
 },
auth表示应该使用 HTTP 基础验证,并提供凭据,这将设置一个 Authorization 头,覆写掉现有的任意使用 headers 设置的自定义 Authorizationauth: {
   username: 'janedoe',
   password: 's00pers3cret'
 },
responseType服务器响应的数据类型,可以是 'arraybuffer', 'blob', 'document', 'json', 'text', 'stream'responseType: 'json', // 默认的
xsrfCookieName用作 xsrf token 的值的cookie的名称xsrfCookieName: 'XSRF-TOKEN'
xsrfHeaderName承载 xsrf token 的值的 HTTP 头的名称xsrfHeaderName: 'X-XSRF-TOKEN', // 默认的
onUploadProgress允许为上传处理进度事件onUploadProgress: function (progressEvent) {
   // 对原生进度事件的处理
 },
onDownloadProgress允许为下载处理进度事件 onDownloadProgress: function (progressEvent) {
   // 对原生进度事件的处理
 },
maxContentLength定义允许的响应内容的最大尺寸maxContentLength: 2000
validateStatus定义对于给定的HTTP 响应状态码是 resolve 或 reject  promise 。如果 validateStatus 返回 true (或者设置为 nullundefined),promise 将被 resolve; 否则,promise 将被 rejectevalidateStatus: function (status) {
   return status >= 200 && status < 300; // 默认的
 },
maxRedirects定义在 node.js 中 follow 的最大重定向数目maxRedirects: 5, // 默认的
httpAgenthttpAgenthttpsAgent 分别在 node.js 中用于定义在执行 http 和 https 时使用的自定义代理。 httpAgent: new http.Agent({ keepAlive: true }),
 httpsAgent: new https.Agent({ keepAlive: true }),
proxy定义代理服务器的主机名称和端口proxy: {
   host: '127.0.0.1',
   port: 9000,
   auth: : {
     username: 'mikeymike',
     password: 'rapunz3l'
   }
 },
cancelToken指定用于取消请求的 cancel tokencancelToken: new CancelToken(function (cancel) {
 })

1.2.1.2 拦截重复请求

在网速较慢的情况下,容易出现用户多次点击而重复请求使得页面抖动的问题,用户体验不好,因此进行拦截重复请求的处理。思路是:
创建请求队列 ---->

-----拦截处理------
标识即将发送的请求---->
判断即将发送的请求与队列中的请求是否相同---->
若相同则执行当前请求的取消方法,并从请求队列中删除---->
创建即将请求的取消方法,放入队列中

拦截处理

request.interceptors.request.use(
  config => {
    // 拦截重复请求(即当前正在进行的相同请求)
    const requestData = getRequestIdentify(config, true); // 标识请求
    removePending(requestData, true);// 取消重复请求
    config.cancelToken = new CancelToken((c) => { // 创建当前请求的取消方法
      pending[requestData] = c;
    });
    return config;
  }, error => {
  	return Promise.reject(error)
	})


标识请求

const getRequestIdentify = (config, isReuest = false) => {
  let url = config.url;
  if (isReuest) {
    url = config.baseURL + config.url.substring(1, config.url.length);
  }
  return config.method === 'get' ? encodeURIComponent(url + JSON.stringify(config.params)) : encodeURIComponent(config.url + JSON.stringify(config.data));
};


取消重复请求

const pending = {};
const CancelToken = axios.CancelToken;
const removePending = (key, isRequest = false) => {
  if (pending[key] && isRequest) {
    pending[key]('取消重复请求');
  }
  delete pending[key];
};

1.2.2 接口响应拦截处理

接口响应的拦截主要是对接口返回的数据进行提取、封装使用,以及对请求异常进行统一配置处理。

request.interceptors.response.use(
  response => {
    const data = response.data || {};
    return data;
  },
  error => {
    const code = error.response.status;
    if (code) {
      let msg = '';
      switch (code) {
        case 400:
          msg = '请求错误';
          break;
        case 401:
          msg = '未授权,请登录';
          break;
        case 403:
          msg = '拒绝访问';
          break;
        case 404:
          msg = `请求${error.response.config.url}出现404错误`;
          break;
        case 408:
          msg = '请求超时';
          break;
        case 500:
          msg = '服务器内部错误';
          break;
        case 501:
          msg = '服务未实现';
          break;
        case 502:
          msg = '网关错误';
          break;
        case 503:
          msg = '服务不可用';
          break;
        case 504:
          msg = '网关超时';
          break;
        case 505:
          msg = 'HTTP版本不受支持';
          break;
      }
      Message.error(msg);
    }
    return Promise.reject(error);
  }
)

1.2.3 API方法封装

单独封装接口请求方法, GET方法的参数为params,POST方法的参数为data。

// api.js
import request from '@/utils/request';

export function APIPostMethod(data) { // 自定义接口方法
  return request({
    url: '/url1',
    method: 'post',
    data
  });
}
export function APIGetMethod(params) { // 自定义接口方法
  return request({
    url: '/url2',
    method: 'get',
    params
  });
}

在业务中调用API方法

import { APIGetMethod, APIPostMethod } from '@/utils/request';
const params = {}
APIGetMethod(params).then(res => {
//...
//对数据处理
})

1.3跨域处理

简单来说,为了防止XSS和CSFR攻击,浏览器的同源策略限制带来了前后端分离开发时的跨域问题。即当请求与响应不在同一个协议+域名+端口下,就不会被浏览器允许。但是同源策略只是浏览器的一种策略,不是HTTP协议的一部分,因此服务端调用HTTP接口只是使用HTTP协议,而不会通过浏览器,更不会执行JS脚本,所以不会触发同源策略机制,不存在跨域问题。针对这个特点,可以从前端配置代理服务器入手。
尝试过两种解决跨域的方法:
1.Node.js中间件代理
在vue-cli中,里用node+webpack+webpack-dev-server代理接口跨域。

//vue.config.js
const config = {
  // ...
  devServer: {
    hot: true,
    open: true,
    host: '127.0.0.1',
    // host: '0.0.0.0',//如果是真机测试,就使用这个IP
    port: 8899,
    https: false,
    hotOnly: false,
    proxy: {
      '/': {
        target: 'http://xxx.xxx.xx.xx:xxxx/',
        logLevel: 'debug',
        ws: false,
        changOrigin: true
      }
    }
  }
}
module.exports = config;

2.Nginx反向代理
通过nginx配置一个代理服务器,域名与本地域名A一致,但端口不同,反向代理访问对方B域名下的接口。

#proxy服务器
server {
        listen       8088;
        server_name  _;
        client_max_body_size 50m;

        location / {
            root   html/dist;
            try_files $uri $uri/ /index.html;
            index  /index.html;

            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "Upgrade";
        }
        location ^~/api {
            proxy_pass	http://xxx.xxx.xx.xx:xxxx/api;
            proxy_redirect default;
        }
}


前端在启动项目的时候,需要把项目proxy代理对应的8088端口上(感觉这种方式有点多余,但是同事用这种方式,咱不敢说,所以个人这边是用的第一种)。

Nginx的方式更适合用于线上部署解决跨域使用,开发环境下,使用vue-cli中的devserve既方便又快捷。


2.Vue技巧

2.1 懒加载

懒加载也叫延迟加载,使组件进行异步加载。目的是延迟非必要资源的加载,减少页面加载的时间,从而优化页面的性能。

2.1.1 路由懒加载

export default new Router({
  routes:[
    {
     path: '/test',
     name: 'test',
      //懒加载
     component: resolve => require(['../page/test.vue'], resolve),
    },
  ]
})

在路由懒加载下,代码根据路由被拆分为不同的代码块,在切换进入相应的路由时,才对对应的代码块进行加载,加载更加高效了。
image.png

2.1.2 组件懒加载

components: {
  UpdateModal: resolve => { require(['./UpdateNormalTaskModal'], resolve); }
},

在路由懒加载的前提下,进行组件懒加载的对比实验。
未使用组件懒加载:
image.png
整个页面为一个js,大小为718KB,加载耗时166ms。
在使用组件懒加载的时候:
image.png
整个页面被拆分为三个js(53.js),其中懒加载的两个组件,各自一个js(78.js、91.js),可看出来,53.js的文件大小变小,加载速度变快,将一个js拆分为多个进行并行加载,可以提高加载的效率,从而提升性能。

2.2按需引入

按需加载一般用于在使用第三方库的时候,为了避免第三方库过大,而造成的对首屏加载带来的过大的压力。
以VantUI按需加载为例

  • 安装babel-plugin-import
    npm i babel-plugin-import -D
  • 在babel.config.js中配置plugins(插件)
module.exports = {
  plugins: [
    ['import', {
      libraryName: 'vant',
      libraryDirectory: 'es',
      style: true
    }, 'vant']
  ],
  presets: [
    '@vue/app'
  ]
};
  • 在要用到三方组件的vue文件中引入使用
import { Swipe, SwipeItem, ImagePreview } from 'vant';
export default {
  components: {
    vanSwipe: Swipe,
    vanSwipeItem: SwipeItem,
  }
}

2.3 JS中使用本地图片资源

在vue的v-bind语法中使用到本地资源时,路径是相对本地文件夹的相对路径,打包时无法解析。

2.3.1.静态资源读取

将图片资源放在static静态资源文件夹下,在使用src时,直接访问根目录下的资源
比如图片放在public目录下,路径直接写为'/img/case/gkjg/7.jpg'
image.png

2.3.2导入资源

在data中采用require的方式,将图片资源导入,然后使用imgUrl变量。

data(){
  return {
		imgUrl:require("../assets/test.png")
  }
}

2.4 keep-alive

<keep-alive></keep-alive>包含的组件会被缓存下来,不进行再次渲染DOM,从而节省性能,切换内容时会出发activated和deactivated两个生命周期钩子函数,被缓存的组件会保留当前组件的状态。

2.4.1路由页面缓存

利用router的meta字段

//...router.js
export default new Router({
  routes: [
    {
      path: '/',
      name: 'Hello',
      component: Hello,
      meta: {
        keepAlive: false // 不需要缓存
      }
    },
    {
      path: '/page1',
      name: 'Page1',
      component: Page1,
      meta: {
        keepAlive: true // 需要被缓存
      }
    }
  ]
})
 <keep-alive> 
    <router-view v-if="$route.meta.keepAlive"></router-view> 
 </keep-alive> 
 <router-view v-if="!$route.meta.keepAlive"></router-view> 

2.4.2组件缓存

<keep-alive include="test-keep-alive">
  <!-- 将缓存name为test-keep-alive的组件 -->
  <component></component>
</keep-alive>

<keep-alive include="a,b">
  <!-- 将缓存name为a或者b的组件,结合动态组件使用 -->
  <component :is="view"></component>
</keep-alive>

<!-- 使用正则表达式,需使用v-bind -->
<keep-alive :include="/a|b/">
  <component :is="view"></component>
</keep-alive>

<keep-alive exclude="test-keep-alive">
  <!-- 将不缓存name为test-keep-alive的组件 -->
  <component></component>
</keep-alive>

2.4.3 结合berforeRouteEnter

结合路由beforeRouteLeave(to, from, next)的钩子,设置to.meta.keepAlive 来指定目的页面是否进行keepAlive。

export default {
    data() {
        return {};
    },
    methods: {},
    beforeRouteLeave(to, from, next) {
         // 设置下一个路由的 meta
        to.meta.keepAlive = true;  // 当前页面跳转到下一个页面时,让目的页面缓存,不刷新
        next();
    }
};


恰当使用keep-alive,结合activated和deactivated两个钩子函数,将不需要更新的内容缓存下来,将需要更新的内容放在两个钩子中去处理,这样可以减少不必要的http请求和DOM重复渲染,提升了不少性能

2.5自定义v-model指令

2.5.1 寻常的v-model

  • 应用场景

有的状态在子组件和父组件中都会被修改,同时,子父组件都会在这个状态变化的同时,去写一些业务逻辑,如果用props和事件监听的方式,写下来组件的代码里会很长很丑,所以我个人特别喜欢用v-model来实现子父组件的双向绑定!非常方便!

  • v-model的作用

v-model常用于input这样的表单组件,用于实现表单值的双向绑定,意思就是从外界可以传入一个值给表单,当你改变表单的值时,外界的这个值同时也会被改变,因此他们的影响时双向的。

  • v-model本质
    v-model的本质实际上是一个语法糖,比如在input元素中
<!--v-model是语法糖-->
<Input v-model="username">
<!--默认等效于下⾯面这⾏行行-->
<Input :value="username" @input="username=$event">

2.5.2自定义v-model

既然v-model是一个语法糖,那么实际上的props$emit是不能少的,但是v-model指向的propd是哪一个,对应的$emit事件是哪一个,是需要我们进行声明的。
以一个demo为例:

// 父组件中对demo组件的调用
<demo v-model="currentData" :other="otherProps"></demo>

父组件里绑定一个值

// demo.vue
<template>
  <div>
    <button @click="cEvent">点击</button>
    {{ current }}
  </div>
</template>

<script>
export default {
  props: {
  	current: {
      type: String
    },
    other: {
      type: String
    }
  },
  watch:{
  	current(newVal,oldVal) {
    	...doSomething...
    }
  },
  model: {
    prop: 'current',
    event: 'event'
  },
  methods: {
    cEvent() {
      this.$emit('event', '改变啦');
    }
  }
};
</script>
  

子组件中声明修改model的默认行为,指定v-model的传入是props中的current, 当要改变current的值时,$emit一个event事件通知父组件修改current的值,实现了v-model的双向绑定。
其实上述行为等同于以下使用常规props$emit 的写法

// 父组件中对demo组件的调用

<template>
  <div>
    <demo :current"currentData" :other="otherProps" @event="changeCurrent"></demo>
  </div>
</template>

<script>
export default {
  data() {
    return {
      currentData: '初始值'
    }
  },
  methods: {
    changeCurrent(val) {
      this.changeCurrent = val
      ...doSomething...
    }
  }
};
</script>
// demo.vue
<template>
  <div>
    <button @click="cEvent">点击</button>
    {{ current }}
  </div>
</template>

<script>
export default {
  props: {
  	current: {
      type: String
    },
    other: {
      type: String
    }
  },
  watch:{
  	current(newVal,oldVal) {
    	...doSomething...
    }
  }
  methods: {
    cEvent() {
      this.$emit('event', '改变啦');
    }
  }
};
</script>

2.6 动态组件+组件的自动化注册

场景:

  • 需要动态地加载某个文件夹的vue文件
  • 文件很多,不想单个手动import

2.6.1自动化注册

// 此段代码直接写在script标签里
var templatePage = {};
var nameList = [];
function initPage() {
  /*
  	require.context()的参数中,不能含有变量
		因此不能动态导入,只能一次性导入之后,动态引用
  */
  const requireComponent = require.context(
    // 其组件目录的相对路径
    '../views/template',
    // 是否查询其子目录
    true,
    // 匹配基础组件文件名的正则表达式
    /.vue$/
  );
  requireComponent.keys().forEach(fileName => {
    var names = fileName
      .split('/')[1] + '-' + fileName
      .split('/')[2].split('.')[0];
    const componentConfig = requireComponent(fileName);
    templatePage[names] = componentConfig.default || componentConfig;
    nameList.push(names);
  });
}
initPage();
components: {
  ...templatePage
},

此方法还可以用来自动导入图片资源

2.6.2 动态组件

通过is匹配组件,因此这些组件必须要有name。

// template中
<component
  :is="comp"
  v-for="comp in fileList"
  :id="comp"
  :key="comp"
  class="content__item">
</component>
computed: {
    fileList() {
      return nameList
        .filter(v => v.split('-')[0] === this.$route.params.templateId);
    }
  },

2.7封装自定义指令

官方文档
按钮级粒度的权限控制,使用自定义指令v-permission来实现。
该指令通过传递进来的权限数组和当前用户角色数组过滤
如果用户拥有要求的权限,可以看到,否则删除指令挂钩的dom元素

// permission.js

export default {
  // el - 挂载dom
  // binding- v-per="[]" {value: []}
	inserted(el,binding) {
    // 获得传入的值
  	const {value: permissionRoles} = binding;
    //...业务逻辑
    // 获取用户角色
    const roles = stroe.getters.roles;
    // 合法性判断
    if(permissionRoles && permissionRoles instanceof Array
			permissionRoles.length > 0) {
      const hasPermission = roles.some(role => {
        return permissionRoles.includes(role)
      });
      if(!hasPermission) {
        el.parentNode && el.parentNode.removeChild(el);
      }
    } else {
    	throw new Error('需要指定数组类型的权限');
    }
  }
}
//main.js
import permission from '@../permission;

//注册指令
Vue.directive('permission',permission);

2.8 装饰器

Decorator 的语法还没有通过提案,所以项目中很少用
blog.csdn.net/weixin_3423…
juejin.cn/post/685651…

2.9 上传文件的进度条

利用axios的onUploadProgress和onDownloadProgress可分别获得上传和下载的进度,配置方法和配置contentType之类的config是一样的。

onUploadProgress: function (progressEvent) {
    // Do whatever you want with the native progress event
}
    // `onDownloadProgress` allows handling of progress events for downloads
    // browser only
onDownloadProgress: function (progressEvent) {
    // Do whatever you want with the native progress event
}

3.Element-UI踩坑

公司后台管理系统UI库选的是Element,期间踩过挺多坑,但是忙于业务疏于整理,记不清了,之后这一块会慢慢积累,然后记录上去。

3.1Dialog

3.1.1生命周期(Dialog作为子组件的场景下)

子父组件的在生命周期种加载渲染的顺序: 父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted

因此在el-dialog在父组件created之后,紧接着也进入了created、mounted周期,无论dialog是否被打开,出现在界面里。
为了能在打开时执行获取数据的方法,绕了很多弯弯。
1.v-if控制渲染法
在父组件里,对el-dialog进行v-if控制,每次显示都会重新渲染。然后就可以在created生命周期里执行你想执行的方法。
image.png
2.watch监听法
在el-dialog组件里,监听控制显示隐藏的变量,当变量为true时,代表dialog显示,执行你执行的方法。
image.png
3.open事件回调法
别提了,之前一直没发现el-dialog有open这个回调函数! 才会有了上面两种方法,使用oepn回调,在回调中执行你的方法就完事了!

3.1.2 可拖拽

将element-ui的dialog组件修改为可以拖拽的,用户可以根据实际需求拖拽决定dialog的位置。
实际实现参考了 大佬网友 的代码,根据实际需求,取消了边界判断
具体实现如下:

  • 创建directives.js文件
import Vue from 'vue';

// v-dialogDrag: 弹窗拖拽
Vue.directive('dialogDrag', {
  bind(el, binding, vnode, oldVnode) {
    // 获取拖拽内容头部
    const dialogHeaderEl = el.querySelector('.el-dialog__header');
    // 获取拖拽内容整体 这个rrc-dialog是我自己封装的组件 如果使用element的组件应写成.el-dialog
    const dragDom = el.querySelector('.el-dialog');
    dialogHeaderEl.style.cursor = 'move';

    // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
    const sty = dragDom.currentStyle || window.getComputedStyle(dragDom, null);

    // 鼠标按下事件
    dialogHeaderEl.onmousedown = (e) => {
      e.stopPropagation();
      // 鼠标按下,计算当前元素距离可视区的距离 (鼠标点击位置距离可视窗口的距离)
      const disX = e.clientX - dialogHeaderEl.offsetLeft;
      const disY = e.clientY - dialogHeaderEl.offsetTop;

      // 获取到的值带px 正则匹配替换
      let styL, styT;

      // 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
      if (sty.left.includes('%')) {
        styL = +document.body.clientWidth * (+sty.left.replace(/\%/g, '') / 100);
        styT = +document.body.clientHeight * (+sty.top.replace(/\%/g, '') / 100);
      } else {
        styL = +sty.left.replace(/\px/g, '');
        styT = +sty.top.replace(/\px/g, '');
      }

      // 鼠标拖拽事件
      document.onmousemove = function(e) {
        e.stopPropagation();
        // 通过事件委托,计算移动的距离 (开始拖拽至结束拖拽的距离)
        const l = e.clientX - disX;
        const t = e.clientY - disY;

        let finallyL = l + styL;
        let finallyT = t + styT;

        // 边界值判定 注意clientWidth scrollWidth区别 要减去之前的top left值
        // dragDom.offsetParent表示弹窗阴影部分
        if (finallyL < 0) {
          // finallyL = 0;
        } else if (finallyL > dragDom.offsetParent.clientWidth - dragDom.clientWidth - dragDom.offsetParent.offsetLeft) {
          finallyL = dragDom.offsetParent.clientWidth - dragDom.clientWidth - dragDom.offsetParent.offsetLeft;
        }

        if (finallyT < 0) {
          // finallyT = 0;
        } else if (finallyT > dragDom.offsetParent.clientHeight - dragDom.clientHeight - dragDom.offsetParent.offsetTop) {
          finallyT = dragDom.offsetParent.clientHeight - dragDom.clientHeight - dragDom.offsetParent.offsetTop;
        }

        // 移动当前元素
        dragDom.style.left = `${finallyL}px`;
        dragDom.style.top = `${finallyT}px`;

        // 将此时的位置传出去
        // binding.value({x:e.pageX,y:e.pageY})
      };

      document.onmouseup = function(e) {
        e.stopPropagation();
        document.onmousemove = null;
        document.onmouseup = null;
      };
    };
  }
});

  • 在main.js中引入
import './utils/directives';
  • 在需要拖拽的dialog标签中使用v-dialogDrag指令,即可实现拖拽
<el-dialog v-dialogDrag custom-class="enter-dialog" title="""></el-dialog>

3.2表格reserve-selection跨页选择

el-table中的reserve-selection属性,可以实现分页切换后,依然保留上一页勾选的数据(进行跨页选择)
image.png

注:
1.设置row-key,这个row-key一定要是每条数据唯一的标识。
2.当涉及到表格内容的切换,不希望被保存勾选,但是没有进行重新渲染时,一定要记住清除勾选,避免发生误操作。

3.3自定义样式

3.3.1 样式穿透

在vue中使用element的组件时,通常使用在组件文件中,每个组件为了防止命名带来的样式污染,通常会使用scoped。在这样的情况下,如果要对elment的组件进行自定义,可以采用三种办法:

<style scoped>
/* 外层>>>穿透 */
.my-dialog >>> .el-dialog {
 background: #ff0;
}
/* /deep/穿透 */
/deep/ .el-dialog{
 background: #ff0;
}
</style>

/* 单独一个不使用scoped的style用来设置第三方样式的修改*/
/* 全局css配合外层样式限制 */
<style>
.my-dialog .el-dialog{
  background: #ff0;
}
</style>


通过调试可以看出,都达到了只在组件内部修改elment组件样式的效果,不会影响到全局的element组件样式:

  • 在scoped下,外层>>>穿透,在外层加上了[data-v-***]唯一标识符
  • /deep/穿透,直接在el组件前加上了标识符
  • 单独设置一个全局的标签,并在外层套一个样式进行局部限制的方法,没有什么特别的,但是也实现了预期的效果。
  • 三个的优先级: >>>穿透 > 全局配合外层样式 > /deep/


![image.png](https://cdn.nlark.com/yuque/0/2020/png/200737/1586402040845-b2dfbbc8-9708-4c75-9f93-9f09e1544242.png#align=left&display=inline&height=319&name=image.png&originHeight=319&originWidth=383&size=43503&status=done&style=none&width=383)

注:

  •  /deep/是chrome自己的标准,在其他浏览器上可能会无效
  • <<< 穿透在有些预处理器上无法解析

> 所以我目前用的还是第三种(单独设置style标签),虽然代码略丑,但是还是比较稳的方法

3.3.2特殊的下拉框样式

坑:
在自定义包含下拉框的组件样式时,比如下拉选择el-select,下拉框的部分是脱离组件文件的,直接与最外部的App.vue平级,这意味着,任何的外层样式都包裹不住这个下拉框。因此不存在穿透了,无论怎么穿透都是穿不出来的。
image.png
官方文档:image.png
在propper-class下进行样式的修改,这时候只能使用不带scoped的style实现,为了各个页面的下拉框不影响,propper-class的类名一定要具有唯一标识性。
拥有propper-class属性的组件大概有:image.pngimage.png

3.4 Image

3.4.1 加载时默认的白色背景

加载时,图片会有一个默认的白色背景,加载的一瞬间很难捕捉是哪个css在作妖,所以只好去源码里看。
// node_modules/element-ui/packages/image/src/main.vue
image.pngimage.png
看到了是el-image__placeholder这个class在作妖。这时在自己的组件里就可以重写样式,改成自己想要的颜色啦!!