vue 拖拽滚动

Vue实现简单的鼠标拖拽滚动效果

import Vue from 'vue'

Vue.directive('dragscroll', function (el) {
  el.onmousedown = function (ev) {
    const disX = ev.clientX
    const disY = ev.clientY
    const originalScrollLeft = el.scrollLeft
    const originalScrollTop = el.scrollTop
    const originalScrollBehavior = el.style['scroll-behavior']
    const originalPointerEvents = el.style['pointer-events']
    el.style['scroll-behavior'] = 'auto'
    // 鼠标移动事件是监听的整个document,这样可以使鼠标能够在元素外部移动的时候也能实现拖动
    document.onmousemove = function (ev) {
      ev.preventDefault()
      const distanceX = ev.clientX - disX
      const distanceY = ev.clientY - disY
      el.scrollTo(originalScrollLeft - distanceX, originalScrollTop - distanceY)
      // 由于我们的图片本身有点击效果,所以需要在鼠标拖动的时候将点击事件屏蔽掉
      el.style['pointer-events'] = 'none'
    }
    document.onmouseup = function () {
      document.onmousemove = null
      document.onmouseup = null
      el.style['scroll-behavior'] = originalScrollBehavior
      el.style['pointer-events'] = originalPointerEvents
    }
  }
})

现有库

https://vue-dragscroll.donfalcon.com/

单元测试 vue

模拟第三方库

import { mount, createLocalVue  } from '@vue/test-utils'
import Vuex from 'vuex'
import ElementUI from 'element-ui'
describe('HelloWorld.vue', () => {
  it('renders props.msg when passed', () => {
    const localVue = createLocalVue()
    localVue.use(Vuex)
    localVue.use(ElementUI)
    const msg = 'new message';
    const wrapper = shallowMount(HelloWorld, {
      // 做一个数据的传
      propsData: { msg },
      // 使用 store
      store,
      // 避免混入和安装插件而污染全局 Vue
      localVue,
    });
    expect(wrapper.text()).toMatch(msg);
  });
});

jest.config.js 设置

因为项目中有引用 element-ui 和 vue-awesome,需要被 babel 解析,排除掉这两个包,在 jest.config.js 中配置

transformIgnorePatterns: [
    'node_modules/(?!(element-ui|vue-awesome)/)'
 ],
哪些文件是测试文件
testMatch: ['\*\*/\_\_tests\_\_/\*\*/\*.js?(x)','\*\*/?(*.)(spec|test).js?(x)']
moduleNameMapper: {
		"^@/(.*)$": "<rootDir>/src/$1"
	}

因为很多测试组件的时候需要引入很多文件或包,所以就提出来 js 文件,类似 vue 的 main.js ,做入口的统一处理,创建 tests/unit/lib/before-test.js 【基本的都是在 main.js 中引入的或添加】


import element from '@/plugins/element'
import baseComponent from '@/plugins/base-component'
import registeSvgIcon from '@/plugins/registe-svg-icon'
import API from '@/request/api'
import axios from '@/request'
import utils from '@/utils'
jest.mock('axios')
export default (Vue) => {
  element(Vue)
  baseComponent(Vue)
  registeSvgIcon(Vue)
  Vue.prototype.$API = API
  Vue.prototype.axios = axios
  Vue.prototype.$util = utils
}

在其他测试文件引入 import ‘./lib/before-test’

请求测试

created() {
    UserApi.getUserInfo()
      .then((user) => {
        this.user = user;
      });
  },

// api 接口 ./src/apis/user.js
function getUserInfo() {
  return $http.get('/user');
}

export default {
  getUserInfo,
};

测试
// ./src/components/user-info/user-info.spec.js
import { shallowMount } from '@vue/test-utils';
import UserInfo from './user-info'; // 组件
import UserApi from '../../apis/user';

// mock 掉 user 模块
jest.mock('../../apis/user');

// 指定 getUserInfo 方法返回假数据
UserApi.getUserInfo.mockResolvedValue({
  name: 'olive',
  desc: 'software engineer',
});

describe('<user-info/>', () => {
  const wrapper = shallowMount(UserInfo);
  test('getUserInfo 有且只 call 了一次', () => {
    expect(UserApi.getUserInfo.mock.calls.length).toBe(1);
  });
  it('用户信息渲染正确', () => {
    expect(wrapper.find('.name').text()).toEqual('olive');
    expect(wrapper.find('.desc').text()).toEqual('software engineer');
  });
});

组件封装

form 组件高度封装

对于form 表单项一般有 基础组件(input, select 等), 业务组件 (需要获取数据,且比较复杂, 比如Tree 选择)

思路: 通用表单项样式统一,组件prop加上可渲染的组件来定义业务表单组件。

//vue ,xComponent 传入组件名, 通过异步组件实现
<SField
  label="覆盖机构" type="array" name="agencyIds" :enum="typePurposeList" xComponent="MultiAgencySelect"/>


//react 
<SField render={<MultiAgencySelect/>}/>
// 根据名称加载异步组件
<template>
  <component :is="asyncComponent" v-bind="$attrs" v-on="$listeners" />
</template>

<script>
const loadView = view => {
  // 路由懒加载
  return () => Promise.resolve(require(`@/components/business/${view}`).default);
};
// 动态异步组件
export default {
  name: 'AsyncComponent',
  props: {
    name: {
      type: String,
    },
  },
  data() {
    return {
      asyncComponent: this.render(),
    };
  },
  watch: {
    name() {
      this.asyncComponent = this.render();
    },
  },
  methods: {
    render() {
      return loadView(this.name);
    },
  },
};
</script>

vue

v-for 里有click 并没有用到事件代理, react 是合成事件并代理到 document上

 Promise.retry = async function(fn, num){
    return new Promise(function(resolve, reject){
       while(num>0){
           try{
                  const res = await fn
                  resolve(res)
                  num = 0
            } catch(e){
                  if(!num) reject(e)
            }
            num --
        }
    })
}

vue watch vs getDerivedStateFromProps + componentDidUpdate

vue 中的watch 监听 props, data, computed 的变化, 执行异步或者开销较大的操作

getDerivedStateFromProps

getDerivedStateFromProps 会在render 前调用,并且在初始挂载和后续更新中都会调用, 返回一个对象更新state, 返回null 不更新任何内容,

当state 的值 在任何时候都需要props 的时候 适用该方法

static getDerivedStateFromProps(nextprops, preState){
  if(nextProps.info !== preState.info){
   return {info: nextProps.info}

  }
}

componentDidUpdate(prevProps, prevState, snapshot)

componentDidUpdate()方法在组件更新后被调用。首次渲染不会执行此方法。当组件更新后,可以在此处操作DOM、执行setState或者执行异步请求操作。

  • 1)componentDidUpdate()的第三个参数snapshot参数来源于getSnapshotBeforeUpdate()生命周期的返回值。若没有实现getSnapshotBeforeUpdate(),此参数值为undefined。
  • 2)可以在componentDidUpdate()中直接调用setState(),但是它必需被包裹在一个条件语句里,否则会导致死循环。
  • 3)可以在componentDidUpdate()对更新前后的props进行比较,执行异步操作。
  • 4)如果shouldComponentUpdate()返回值为false,则不会调用componentDidUpdate()。

Vue 插件按需引入 之 babel-plugin-component

1. npm 安装 babel-plugin-component
.babelrc plugins 配置
[cc lang=”js”]
[
“component”,
{
“libraryName”: “element-ui”,
“styleLibraryName”: “theme-chalk”
}
][/cc]
所以如果在.babelrc文件配置过styleLibraryName属性的,不要在全局引入element的css样式了

main.js 里 按需引入
[cc lang=”js”]
import Vue from ‘vue’;
import {
Dialog,
Autocomplete,
Dropdown,

} from ‘element-ui’;
Vue.use(Pagination);
Vue.use(Dialog);
Vue.use(Autocomplete);[/cc]

vue组件从开发到发布

1. 大纲

想要搭建一个组件库,我们必须先要有一个大概的思路。

  1. 规划目录结构
  2. 配置项目以支持目录结构
  3. 编写组件
  4. 编写示例
  5. 配置使用库模式打包编译
  6. 发布到npm

2. 规划目录结构

1.创建项目

在指定目录中使用命令创建一个默认的项目,或者根据自己需要自己选择。

vue create myproject

2. 调整目录

.
...
|-- examples      // 原 src 目录,改成 examples 用作示例展示
|-- packages      // 新增 packages 用于编写存放组件
...

3. 配置项目以支持新的目录结构

  • src目录更名为examples导致项目无法运行
  • 新增packages目录,该目录未加入webpack编译

1 重新配置入口,修改配置中的 page 选项

新版 Vue CLI 支持使用 vue.config.js 中的pages选项构建一个多页面的应用。

module.exports = {
  // 修改 src 目录 为 examples 目录
  pages: {
    index: {
      entry: 'examples/main.js',
      template: 'public/index.html',
      filename: 'index.html'
    }
  }
}

支持对packages目录的处理,修改配置中的chainWebpack选项

module.exports = {
  // 修改 src 为 examples
  pages: {
    index: {
      entry: 'examples/main.js',
      template: 'public/index.html',
      filename: 'index.html'
    }
  },
  // 扩展 webpack 配置,使 packages 加入编译
  chainWebpack: config => {
    config.module
      .rule('js')
      .include
        .add('/packages')
        .end()
      .use('babel')
        .loader('babel-loader')
        .tap(options => {
          // 修改它的选项...
          return options
        })
  }
}

4 编写组件

创建一个新组件

  • 在packages目录下,所有的单个组件都以文件夹的形式存储,所有这里创建一个目录 color-picker/
  • 在color-picker/目录下创建src/目录存储组件源码
  • 在/color-picker/目录下创建index.js文件对外提供对组件的引用。

修改 /packages/color-picker/index.js 文件,对外提供引用。

# /packages/color-picker/index.js
// 导入组件,组件必须声明 name
import colorPicker from './src/color-picker.vue'

// 为组件提供 install 安装方法,供按需引入
colorPicker.install = function (Vue) {
  Vue.component(colorPicker.name, colorPicker)
}

// 默认导出组件
export default colorPicker

2. 整合所有的组件,对外导出,即一个完整的组件库

修改 package.json 文件,对整个组件库进行导出。

// 导入颜色选择器组件
import colorPicker from './color-picker'

// 存储组件列表
const components = [
  colorPicker
]

// 定义 install 方法,接收 Vue 作为参数。如果使用 use 注册插件,则所有的组件都将被注册
const install = function (Vue) {
  // 判断是否安装
  if (install.installed) return
  // 遍历注册全局组件
  components.map(component => Vue.component(component.name, component))
}

// 判断是否是直接引入文件
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}

export default {
  // 导出的对象必须具有 install,才能被 Vue.use() 方法安装
  install,
  // 以下是具体的组件列表
  colorPicker
}

5 编写示例

1 在示例中导入组件库

import Vue from 'vue'
import App from './App.vue'

// 导入组件库
import ColorPicker from './../packages/index'
// 注册组件库
Vue.use(ColorPicker)

Vue.config.productionTip = false

new Vue({
  render: h => h(App)
}).$mount('#app')

2 在示例中使用组件库中的组件

在上一步用使用Vue.use()全局注册后,即可在任意页面直接使用了,而不需另外引入。当然也可以按需引入。

<template>
	<colorPicker v-model="color" v-on:change="headleChangeColor"></colorPicker>
</template>

<script>
export default {
	data () {
		return {
			color: '#ff0000'
		}
	},
	methods: {
		headleChangeColor () {
			console.log('颜色改变')
		}
	}
}
</script>

发布到 npm,方便直接在项目中引用

到此为止我们一个完整的组件库已经开发完成了,接下来就是发布到npm以供后期使用。

1 package.json 中新增一条编译为库的命令

在库模式中,Vue是外置的,这意味着即使在代码中引入了 Vue,打包后的文件也是不包含Vue的。

https://cli.vuejs.org/zh/guide/build-targets.html#%E5%BA%93

以下我们在 scripts 中新增一条命令 npm run lib

  • –target: 构建目标,默认为应用模式。这里修改为 lib 启用库模式。
  • –dest: 输出目录,默认dist。这里我们改成 lib
  • [entry]: 最后一个参数为入口文件,默认为 src/App.vue。这里我们指定编译 package/ 组件库目录。
"scripts": {
	// ...
	"lib": "vue-cli-service build --target lib --name vcolorpicker --dest lib packages/index.js"
}

配置 package.json 文件中发布到 npm 的字段

  • name: 包名,该名字是唯一的。可在 npm 官网搜索名字,如果存在则需换个名字。
  • version: 版本号,每次发布至 npm 需要修改版本号,不能和历史版本号相同。
  • description: 描述。
  • main: 入口文件,该字段需指向我们最终编译后的包文件。 “lib/vcolorpicker.umd.min.js”,
  • keyword:关键字,以空格分离希望用户最终搜索的词。
  • author:作者
  • private:是否私有,需要修改为 false 才能发布到 npm
  • license: 开源协议

3 添加.npmignore文件,设置忽略发布文件

我们发布到 npm 中,只有编译后的 lib 目录、package.json、README.md才是需要被发布的。所以我们需要设置忽略目录和文件。

# 忽略目录
examples/
packages/
public/

# 忽略指定文件
vue.config.js
babel.config.js
*.map

4 登录到 npm

$ npm config set registry http://registry.npmjs.org 
$ npm login
$ npm publish

5 发布成功 使用

$ npm install vcolorpicker -S

# 在 main.js 引入并注册
import vcolorpicker from 'vcolorpicker'
Vue.use(vcolorpicker)

首屏优化

组件化分治思想:

  • 将各模块拆分为组件粒度
  • 将组件依赖的资源全部封装在组件内部进行调用

加载优先级

  • 优先加载首屏可见模块
  • 其余不可见模块懒加载,待可见或即将可见时加载

如何解决判断可见性问题?

从前我们都是通过监听滚动事件、resize 事件来判断模块是否可见,代码不仅繁琐,而且一不小心没有函数去抖就又可能导致严重的性能问题。

现在我们有了更好的选择—— IntersectionObserver API ,IntersectionObserver 允许你配置一个回调函数,每当 target ,元素和设备视口或者其他指定元素发生交集的时候该回调函数将会被执行。这个 API 的设计是异步的,而且保证你的回调执行次数是非常有限的,而且回调是会在主线程空闲时才执行,在性能方面表现更优,使用起来也更简单。

如何尽可能懒的条件渲染?

在解决了加载条件的判断之后,我们需要解决加载条件为假的情况下不去渲染、加载条件为真的时候才渲染的问题,这里的答案非常简单:使用 Vue.js 提供的 v-if 指令,就可以做到真正的惰性渲染。

如果可见后进行初始渲染,可见前如何显示?

用户体验比较差,最开始是白屏,然后突然又渲染出现内容
最致命的是我们判断可见性是需要一个目标来观察的,如果什么不都渲染,我们就无从观察。

这里引入一个骨架屏的概念,我们为真实的组件做一个在尺寸、样式上非常接近真实组件的组件,叫做骨架屏。

如何提升切换时的体验?

如何提升切换时的体验?
在真实组件开始渲染的时候,需要一定的时间和空间,时间指的是真实组件从创建到渲染的时间,包括请求接口、请求资源和渲染的时间,空间指的是页面布局中需要给真实组件留出刚好的位置,避免产生抖动。

这里我们可以使用 Vue.js 内置的 transition 组件自定义骨架组件和真实组件的进入和离开效果,通过合理的布局和定位,减少切换时的抖动,https://github.com/xunleif2e/vue-lazy-component

前端路由回退时,如果页面中还有资源在pending, 页面不会跟着立即跳转

资源型会 阻塞 回退, xhr fetch 不会阻塞;
解决
1. 通过replace 代替 回退
2. 路由变化时,
2.1watch:{
$route(to,from){
console.log(to.path);
}
},
2.2 window.addEventListener(‘popstate’, function() { // 监听回退按钮
console.log(‘我监听到了回退事件’); // 在回退时进行某种操作。
},false);