GithubHelp home page GithubHelp logo

article's Introduction

Hi there 👋

  • 🌱 I’m a javascript developer
  • 😄 My life motto is start small

Liugq5713's GitHub stats

article's People

Contributors

liugq5713 avatar

Stargazers

 avatar

article's Issues

为管理复杂组件状态困扰?试试 vue 简单状态管理 Store 模式

在 vue 中,通信有几种形式:

  • 父子组件 emit/on
  • vuex **享 state
  • 跨组件 EventBus

文档中的提到的 Store 模式却鲜有人去使用讨论。笔者在研究 ElementUI的Table组件的代码组织方式,以及在自己 ElementUI 表单编辑项目中实践之后觉得其在复杂组件组织上非常有用,是一个被忽视的组件通信方法。

简单状态管理 store 模式

官方示例代码:

var store = {
  debug: true,
  state: {
    message: 'Hello!'
  },
  setMessageAction(newValue) {
    if (this.debug) console.log('setMessageAction triggered with', newValue)
    this.state.message = newValue
  },
  clearMessageAction() {
    if (this.debug) console.log('clearMessageAction triggered')
    this.state.message = ''
  }
}

官方介绍:所有 store 中 state 的改变,都放置在 store 自身的 action 中去管理。这种集中式状态管理能够被更容易地理解哪种类型的 mutation 将会发生,以及它们是如何被触发。当错误出现时,我们现在也会有一个 log 记录 bug 之前发生了什么。此外,每个实例/组件仍然可以拥有和管理自己的私有状态

数据流图

官方版的介绍过于简陋,不妨我们更进一步,学习一下 ElementUI 的 Table 组件是如何用 Store 组织一个复杂组件的

为什么需要 Store 模式

ElementUI 的 Table 组件,功能很多。该组件由父组件 Table.vue 和众多子组件 layout-observer,table-body,table-column,table-footer,table-header,table-layout 组成。看 ElementUI 文档就觉得 Table 组件复杂。

如果把子组件的事件都 emit 到父组件处理,那么父组件得接收多少事件。并且子组件部分功能会影响父组件的布局。并且 Table 的部分数据大多数子组件都需要,你要一个一个通过 Porp 传入吗?自顶向下的数据流动开发困难。不如把这些共享的数据放在一个地方,我们自然很容易想到 Vuex,但是 ElementUI 库引入 ElementUI 引入 Vuex,你觉得合适吗,并且数据共享仅仅是在 Table 组件里面,并不是全局的数据,因此采用 Store 模式再好不过了。

ElementUI 模仿了 Vuex 的使用方式。有兴趣的读者可以看一下 Table 组件中table-store.js

模仿 Vuex 的一个好处就是我后期如果项目大了,可以十分平滑的引入 Vuex,并且如果你熟悉 Vuex,使用 Store 模式没有任何认知成本

实践

笔者用 Store 模式改造了我之前的ElementUI 的表单在线编辑器,之前的主页面由表单元素资源区,表单属性编辑区,表单元素拖拽区,表单元素属性编辑区,JSON表单生成区,代码生成区。然而整个页面就维护表单对象,表单元素列表,表单元素属性这几个值,然而这些值在多个子组件里面都起了一定的作用,一开始没有集中处理,导致数据会意外变化,不知道是那个组件引起的。后使用Store模式集中处理之后,代码逻辑清楚很多

项目展示
这种方式其实就是把数据管理,数据更新的功能交给了 Store。如果你熟悉 Vuex 的话,应该很快能理解我在说什么

通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护

数据流

声明一个 Store 对象

const FormStore = function(form, initialState = {}) {
   // 将父组件的示例保存在Store里面
  if (!form) {
    throw new Error('Form is required.')
  }
  this.form = form

  this.states = { ... }
  // initialState 里面的值必须是 this.states声明过的,这样所有状态的变化应该都在store里面可以查找,并由store控制
  for (let prop in initialState) {
    if (initialState.hasOwnProperty(prop) && this.states.hasOwnProperty(prop)) {
      this.states[prop] = initialState[prop]
    }
  }
}

mutations

Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的   事件类型 (type)  和 一个   回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数

我们这里也模仿它,注意这里我们只放同步的代码,异步代码自己处理

FormStore.prototype.mutations = {
  setFormAttribute(states, formAttribute) {
    this.states = { ...states, formAttribute }
  },
  setFormItems(states, formItems) {
    this.states = { ...states, formItems }
  },
  setClickedIndex(states, clickedIndex) {
    this.states = { ...states, clickedIndex }
  },
  setFormItemToHandle(states, formItemToHandle) {
    this.states = { ...states, formItemToHandle }
  },
  setItemInFormItems(states, idx, formItem) {
    states.formItems.splice(idx, 1, formItem)
  },
  setFromItems(states, formItems) {
    this.states = { ...states, formItems }
  }
}

commit

复杂数据结构的父子组件的数据通信用emitv-on事件流容易混乱,尤其是对象嵌套对象的时候。采用 Store 模式,子组件和父组件之间有了 store 这个桥梁,通过 commit 来分发事件

在 commit 函数里面,打上一个console.log,事件的变化全部掌握在你的手里。就像使用 Vuex 一样

// 定义
FormStore.prototype.commit = function(name, ...args) {
  const mutations = this.mutations
  console.log('emit', name)
  if (mutations[name]) {
    // states 作为第一个参数
    mutations[name].apply(this, [this.states].concat(args))
  } else {
    throw new Error(`Action not found: ${name}`)
  }
}
// 分发事件
this.store.commit('setFormItemToHandle', val)

使用

在父组件的 data 里面创建 store,然后把 store 传入到各个子组件里面去。代码逻辑非常清楚

data() {
    const store = new FormStore(this);
    return {
      store
    };
},
computed: {
    form() {
      return this.store.states.formAttribute;
    }
},
methods: {
    genFormItem(val) {
        this.store.commit("setFormItemToHandle", val);
      }
    }
}

Store 模式 vs EventBus

Vuex 的优点即是 Store 模式的优点

  1. 易于调试与管理
  2. 和 EventBus 差不多的便捷,虽然做不到全局发事件,接受事件,但是如果有这种情况的话,为什么不试试 Vuex 呢
  3. 可局部应用,职责专一

EventBus 在代码量增多的情况下:

  1. 代码逻辑性极具下降,可阅读性变低
  2. 对于每一个 action 父组件都需要一个 on(或 dispatch)一个事件来处理
  3. 你将很难查找到每一个事件是从哪里触发,满篇都是业务逻辑

Store 模式 vs Vuex

有的时候,我们可能不知道是否该使用 Vuex,虽然 Redux 的作者 Dan Abramov 的话这么说:

Flux 架构就像眼镜:您自会知道什么时候需要它

但是我可能只是轻微的近视,不带眼镜也可以,但是看东西不太清楚,带上眼镜又感觉有点累赘,这个时候就需要我们的 Store 模式

Vuex 负责全局状态的管理,Store 模式负责局部状态的交流

Store 模式可以在你写一个大型组件的时候,单独在该组件中使用,不用数据都放在 Vuex 里面,作为多个子组件和父组件通信的桥梁使用

多人开发的时候,每个人负责的业务有单独的 Store 也不会互相影响

你甚至可以使用多个 Store 去组织你所有的代码

总结

模仿 Vuex,我们多了一种组织复杂组件或局部状态管理的新思路,在你写复杂的组件,又不想污染全局的 Vuex,又需要将状态在多个组件**享,则可以考虑一下 Store 模式,和 Vuex 一样方便,和 EventBus 一样轻量

既然采用了模仿 Vuex 的方式,代码风格就要贯彻到底,毕竟 Store 模式没有强力的约束,不能像 ElementUI 一样,代码里面还有直接修改 states 语句(逃

eg:
this.store.states.treeData = this.getTableTreeData(value);

体验我基于 Store 模式改造的 ElementUI 表单编辑器项目,记得点个小星星哦,查看项目地址

参考

设计一个vue组件【理论篇】

一个适用性良好的组件,一种是可配置项很多,另一种就是容易覆写,从而扩展功能

Vue 组件的 API 来自三部分——prop、事件和插槽:

  • prop 允许外部环境传递数据给组件
  • event 允许从组件内触发外部环境的副作用
  • slot 允许外部环境将额外的内容组合在组件中

prop

组件具有自身状态,当没有相关 porps 传入时,使用自身状态完成渲染和交互逻辑;当该组件被调用时,如果有相关 props 传入,那么将会交出控制权,由父组件控制其行为

仅一个值传入组件

  • 如果该组件设计上支持双向绑定,可使用v-model将该参数传入组件,减少记忆成本(毕竟 vue 官方的语法糖,不用白不用)
<my-component v-model="foo" />
  • 如果该组件可以独立运行,不依赖父组件时,还是给这个值起个名字吧
<component-no-sync :childNeed="foo" />

很多值需要传入组件

比如当一个组件有诸多配置项,且当没有传入配置项取用组件内部默认项的时候,我们原先的父组件写法:

<child-component :prop1="var1" :prop2="var2" :prop="var3" ... />

其实可以在父组件上直接使用v-bind={子组件props集合}

但是为了方便覆写子组件的内部配置项,不妨使用一个对象将配置收集到一起,但是这种做法不能利用 props 验证对象里面每个的值类型

<child-component v-model="text" :setting="{color:'bule'}" />

// 子组件内部读取配置,通过扩展运算符替换掉默认配置
const setting ={
  ...defaultSetting,
  ...this.setting
}

computed 属性

vue 的 computed 属性默认是只读的,你可以提供一个 setter。它可以优化我写组件的逻辑,适用于父组件处理的值和子组件处理的值是同一个的情况

<template>
  <el-select v-model="email">
    <el-option
      v-for="item in adminUserOptions"
      :key="item.email"
      :label="item.email"
      :value="item.email"
    />
  </el-select>
</template>
export default {
  props: {
    value: {}
  },
  computed: {
    email: {
      get() {
        return this.value
      },
      set(val) {
        this.$emit('input', val)
        this.$emit('change', val)
      }
    }
  }
}

灵活的 prop

我们常看到一些优秀的组件库,传入的值既可以是一个 String/Number,也可以是一个函数。

比如ElementUITable组件,当你想要显示树形数据的时候,必须传入row-key。看它的介绍就知道是有多灵活:

row-key的作用:行数据的 Key,用来优化 Table 的渲染;在使用 reserve-selection 功能与显示树形数据时,该属性是必填的。类型为 String 时,支持多层访问:user.info.id,但不支持 user.info[0].id,此种情况请使用 Function

处理 rowKey 生成 RowIdentity 的函数源码:

//https://github.com/ElemeFE/element/blob/dev/packages/table/src/util.js
export const getRowIdentity = (row, rowKey) => {
  if (!row) throw new Error('row is required when get row identity')
  // 行数据的key
  if (typeof rowKey === 'string') {
    if (rowKey.indexOf('.') < 0) {
      return row[rowKey]
    }
    // 支持多层访问:user.info.id
    let key = rowKey.split('.')
    let current = row
    for (let i = 0; i < key.length; i++) {
      current = current[key[i]]
    }
    return current
    // 通过函数自定义
    // 我处理过父和子id可能相同的情况,只好通过Function自定义
    // 不可以通过时间或者随机字符串生成ID
  } else if (typeof rowKey === 'function') {
    return rowKey.call(null, row)
  }
}

由于业务场景多变,组件的设计者很难考虑完全,不妨设计灵活的 prop,由开发者自行定义

事件

emit/on

读者肯定知道 emit/on 如何使用,我就简单说一下 vue 的 v-modelsync的语法糖,我们可以利用这些语法糖,帮助我们写出简洁的代码(父组件可以少写监听子组件的事件,比如你不用写@input

v-model

看一下下面的代码示例,就能懂这句话了。v-model 会忽略所有表单元素的 value、checked、selected 特性的初始值而总是将 Vue 实例的数据作为数据来源。你应该通过 JavaScript 在组件的 data 选项中声明初始值

<input v-model="searchText" />

<input
  v-bind:value="searchText"
  v-on:input="searchText = $event.target.value"
/>

// 当把v-model用在组件上

<custom-input
  v-bind:value="searchText"
  v-on:input="searchText = $event"
></custom-input>

为了让它正常工作,这个组件内的 <input> 必须:将其 value 特性绑定到一个名叫 value 的 prop 上在其 input 事件被触发时,将新的值通过自定义的 input 事件抛出,即this.$emit('input',changedValue)

自定义 v-model

为啥要自定义组件的 v-model 呢,因为数据不符合要求呗。你的输入值不可能总是 value ,你的事件不可能总是 input,具体详见文档

sync(双向绑定语法糖)

vue 真的是方便了开发者很多,站在开发者的角度考虑,很大的提升开发效率

以  update:myPropName  的模式触发事件取代双向绑定this.$emit('update:title', newTitle),具体详见文档

Function 通过 prop 传入

本来想放在 prop 部分的,但是个人觉得其实它和 emit/on 更有关系一点

有读者可能会问,为什么不能把子组件里面的事件 emit 出来,通过父组件处理?然后传入一个控制子组件的 prop 属性。

我想说的是,可以,但是这样真的很麻烦,子组件内部的状态却要依赖父组件传值。

该组件内部的状态,我们需要把它暴露出来嘛?我觉得不需要,组件内部的状态就让它处于组件内部

但是可以通过传入 function(你可以理解为一个钩子),参与组件状态变更的行为。比如很好用的拖拽库,Vue.Draggable控制元素是否被拖动的行为。

Vue.Draggable可以传入一个 move 方法,我们看一下它如何处理的。

onDragMove(evt, originalEvent) {
      const onMove = this.move;
      // 如果没有传入move,那么返回true,可以移动
      if (!onMove || !this.realList) {
        return true;
      }

      const relatedContext = this.getRelatedContextFromMoveEvent(evt);
      const draggedContext = this.context;
      const futureIndex = this.computeFutureIndex(relatedContext, evt);
      Object.assign(draggedContext, { futureIndex });
      const sendEvt = Object.assign({}, evt, {
        relatedContext,
        draggedContext
      });
      // 组件行为由传入的move函数控制
      return onMove(sendEvt, originalEvent);
}

这样做的好处,就是组件内部自由一套运行逻辑,但是我可以通过传入 function 来干预。我没有直接修改组件内部状态,而是通过函数(你可以称它为钩子)去触发,方便调试组件,使得组件行为具有可预测性

父组件直接操作子组件

很少有这样的*操作,但是由于数据和操作的复杂性,当数据结构复杂,嵌套过深的情况下,父组件很难对于子组件的数据的精细控制

因此,如果不得已而为之,请在文档里,把子组件可以调用的方法暴露出来,供使用者使用。使用这种组件比较麻烦,得去看文档,没有文档的只好去看源码

ElementUItree组件提供了很多方法,用于父组件去操作子组件。

eg:this.$refs.tree.setCheckedKeys([]);

插槽

HTML <slot> element 是 Web Components 技术的一部分,是自定义 web 组件的占位符,vue 里面的 slot 的灵感来自 Web Components 规范草案,具体见文档

默认插槽

能用默认插槽就不要使用具名插槽,我真的不想使用你这个组件的时候还去翻看你的插槽叫什么名字

之前我司一个网页模板 三个插槽,header,body,footer,我用的是真的难受,每次都记不得,看似三个单词都挺熟悉的,但是其实 head,content,foot 这些单词也都行啊,谁知道用啥(可能我老了吧,组件如果不是必要尽量不要让人有记忆成本)。

后备内容

就是给组件里面的插槽定义默认值,它只会在没有提供内容的时候被渲染。建议用上插槽就给它添加默认内容

封装他人组件

有些时候我们可能是对他人的组件进行封装,这里强烈推荐使用v-bind="$attrs" 和 v-on="$listeners"vm.$attrs 是一个属性,其包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)。这些未识别的属性可以通过 v-bind="$attrs" 传入内部组件。未识别的事件可通过v-on="$listeners"传入

举个例子,比如我创建了我的按钮组件myButton,封装了 element-ui 的 el-button 组件(其实什么事情都没做),在使用组件 <my-button />时,就可以直接在组件上使用 el-button 的属性,不被 prop 识别的属性会传入到 el-button 元素上去

<template>
  <div>
    <el-button v-bind="$attrs">导出</el-button>
  <div>
</template>
// 父组件使用
<my-button type='primary' size='mini'/>

组件命名

这里推荐遵循 vue 官方指南,值得一看

我们构建组件的时候通常会将其入口命名为 index.vue ,引入的时候,直接引入该组件的文件夹即可。

但是这样做会有一个问题,当你编辑多个组件的时候,所有的组件入口都叫做index.vue,容易糊涂

vscode 显然意识到了这个问题,所以当文件名相同的文件被打开时,它会在文件名旁边显示文件夹名

如何解决呢,我们可以把 index.js 当作一个单纯的入口,不承担任何逻辑。仅仅负责引入component-name-container以及export default component-name-container

my-app
└── src
        └── components
                └── component-name
                    ├── component-name.css
                    ├── component-name-container.vue
                    └── index.js

tips(个人喜好)

  • template,把一个<template> 元素当做不可见的包裹元素,并在上面使用 v-if。最终的渲染结果将不包含 <template> 元素
  • 能用 computed 计算属性的,尽量就不用 watch
  • 模板里面写太多 v-if 会让你的模板很难看,v-else-if尽量还是别用了吧。一长串的 if else,在模板里面看的很乱

你有什么写组件的独特技巧,不妨在评论区告诉我吧

与服务器交互-从 Form 表单到 Ajax

为什么 form 表单提交没有跨域问题,但 ajax 提交有跨域问题?

里面一个优秀的回答:所谓的跨域问题只是浏览器强加给 js 的规则而已,世界本无跨域限制。是浏览器强制不允许 js 访问别的域,但是浏览器却没有限制它自己。比如说 img 标签可以加载任何域的图片,script 可以加载任何域的 js。再比如说你不能从前端 js 去调淘宝的接口获取 ip 对应的城市地址信息,,但是你可以手敲在浏览器地址栏,直接打开。

form 表单向服务器提交数据

HTML  form 元素  表示了文档中的一个区域,这个区域包含有交互控制元件,用来向 web 服务器提交信息。

action 和 enctype这两个属性值得注意

action

一个处理这个 form 信息的程序所在的 URL。这个值可以被    或者    元素中的  formaction  属性覆盖。

enctype

当 method 为 post 时,enctype 的可能值为

  • application/x-www-form-urlencoded: 默认值
  • multipart/form-data: 如果你想上传文件(input 元素 type 属性设为 file),设置该属性
  • text/plain: (HTML5):仅用于 debug. 具体原因,笔者暂时看不懂:  不能可靠的被浏览器解释。不建议使用。

form 表单的提交会伴随着跳转到 action 中指定 的 url 链接。因此以前也有很多文章告诉你如何阻止跳转。但是原生的 form 表单用的少了,大多通过 ajax 向服务器提交数据。

构造 form 对象提交

FormData  接口提供了一种表示表单数据的键值对的构造方式,经过它的数据可以使用  XMLHttpRequest.send()  方法送出,本接口和此方法都相当简单直接。如果送出时的编码类型被设为  "multipart/form-data",它会使用和表单一样的格式。

笔者在做七牛云上传的时候,我需要把canvas生成的blob上传到七牛。我直接调用的 API,的时候一直报错content-type isn't multipart/form-data,估计要设置一下ontent-type
搜索一下七牛的官方文档,上面很简单的说:

开发者只要组装一个符合 HTML 文件上传表单规范(参考 RFC1867)的 HTTP 请求,并以 POST 方式向域名 upload.qiniup.com 发起这个请求,即可将指定文件上传到服务端。详细使用方法请参考表单上传 API

即,我们可以通过构造一个 Form 表单将文件上传服务端

ajax 向服务器提交数据

目前,笔者基本上全部使用 Ajax 向服务器提交数据,Ajax 优点很明显,局部刷新,用户体验好,对没有 SEO 需求的后台来说刚刚好。

JavaScript中 string 的方法 与 array的对比

前言

为什么写这篇文章,因为笔者在使用 string 方法的时候经常会搞混array 的方法

string method vs array method

都拥有的方法

  • concat()
  • includes()
  • indexOf()
  • lastIndexOf()
  • slice()

设计一个vue组件【实战】-写一个drawer抽屉组件

项目展示

项目展示

本文章以抽屉组件为例子,有不好或者不完善的地方,欢迎在评论区指出或者提 PR,项目地址项目 demo 地址

我们想象一下用户会如何使用我们的组件,它可能需要哪些自定义的功能,比如内容的宽度,控件的位置,抽屉的位置,控件样式自定义等等,可能的交互比如:点击控件/鼠标悬浮打开抽屉,点击抽屉外部收起抽屉等等,接着我们判断一下哪些是需要暴露给外部组件的,哪些是属于组件内部的状态,尽可能的做到这个组件职责单一,且遵循最少知识原则。从这些个角度出发,来编写我们的代码

设计

这个组件有个通用的名字,叫抽屉(Drawer),组件结构分为控件和内容两部分。如图:

           +-----------------------+
           |                       |
   +-------+                       |
   |       |                       |
   |       |                       |
   |       |       content         |
controls   |                       |
   |       |                       |
   |       |                       |
   |       |                       |
   +-------+                       |
           |                       |
           +-----------------------+

不以规矩,不成方圆。HTML 有语义化标签,CSS 有 BEM 规范,这些帮助我们写出结构清晰的 HTML 架构(ps:布局部分使用语义化标签还挺适合的,这种局部小组件还是 div 一把梭了)。组件 HTML 结构如下:

<div class="drawer-container">
  <div class="drawer">
    <div class="controls__container" ref="controls__container">
      <ul class="controls">
        <li>xxx</li>
      </ul>
    </div>
    <div class="content"></div>
  </div>
</div>

基本实现

我们拿贴在右侧的抽屉举例(实际代码与它不完全相同):

抽屉的展开收起

我们定义好抽屉的大小,并将其 postion 设置为 fixed,使用 top,right 属性,将其固定在右侧。因为抽屉默认是收起的,然后通过 translate 将其移除可视区。

.drawer {
  width: '300px';
  height: '100vh';
  position: fixed;
  top: 0;
  right: 0;
  transform: 'translate(100%,0)';
}

抽屉展开的代码也很简单,在通过 translate 将其移回来

.drawer__container--show .drawer {
  transform: translate(0);
}

显示控件

通过负值将控件从抽屉内容区移出来

.controls__container {
  position: absolute;
  left: -40px;
}

基本交互

处理抽屉的打开关闭

抽屉组件支持了 mouseover 和 click 事件,开发的时候,遇到一个比较麻烦的问题:当抽屉以 mouseover 触发,鼠标移到控件上的时候,抽屉会很鬼畜的打开收起打开收起。(因为鼠标在控件上,mouseover 事件不断的被触发,导致抽屉的打开和收起)

面对这种情况,我一开始就想到了防抖和节流。但其实直接拿来用是不适合的

防抖的原理:你尽管触发事件,但是我一定在事件触发 n 秒后才执行,如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行,总之,就是要等你触发完事件 n 秒内不再触发事件,我才执行。

防抖由于是在一个事件触发 n 秒之后才执行,导致组件有一种反应慢的感觉。

节流的原理:如果你持续触发事件,每隔一段时间,只执行一次事件。

其执行事件是异步的,那么当我打开抽屉,然后将鼠标移到抽屉外(移到抽屉外会关闭抽屉),因为抽屉的打开和关闭都是由show变量控制。如果使用节流,会导致异步执行打开抽屉的函数,导致抽屉关闭之后又开起。

节流一般是指事件在一段时间内执行。我们这里不妨换一种思路,对show值进行节流,你也可以把它理解成一种锁。那么当show值变化后,我们锁住show值,n 秒内不允许修改,n 秒后才可以修改。即控制住了抽屉不会在短时间内迅速开合。我们使用计算属性实现如下:

// this.lock 初始值为undefine
// 开闭抽屉的函数通过对lockedShow进行赋值,不会直接操作show
lockedShow: {
    get() {
      return this.show;
    },
    set(val) {
      if (this.lock) {
        return;
      } else {
        this.lock = setTimeout(() => {
        // 200毫秒之后解除锁
          this.lock = undefined;
        }, 200);
        this.show = val;
      }
    }
}

点击抽屉外部分收起

这里我们通过 Element.closest() 方法用来获取点击的祖先元素(Element.closest:匹配特定选择器且离当前元素最近的祖先元素,也可以是当前元素本身)。如果匹配不到,则返回 null。

closeSidebar(evt) {
  const parent = evt.target.closest(".drawer");
  // 点击抽屉以外部分,即匹配不到,parent值为null
  if (!parent) {
    this.show = false;
  }
}

全局监听点击事件

window.addEventListener('click', this.closeSidebar)

我一开始的做法是,组件挂载的时候,全局监听点击事件,组件销毁时移除点击事件。但我们可以做的更好,当 controls 被点击时,添加点击事件,收起抽屉的时候,移除点击事件。减少全局监听的 click 事件。

除了点击事件,我们也顺便支持一下 hover 的操作。鼠标移出收起的操作和点击抽屉外部分收起的代码相同。

通过e.type判断是点击事件还是鼠标移入事件。

 toggleDrawerShow(e) {
    if (e.type === "mouseover" && this.triggerEvent === "mouseover") {
      // do some thing
    }
    if (e.type === "click" && this.triggerEvent === "click") {
      // do some thing
    }
}

优化

控件的位置

使得控件完全贴合内容区,不会因为控件的内容变化,比如控件内容为 show 和 hidden,由于切换的时候,两个单词长度不一样,而使得控件显示不完全,或者脱离内容区。

这种情况我们可以使用 JavaScript 动态计算。因为经常用到,还是封装成一个函数吧。还是拿右侧抽屉举例子:

updateControlLayout() {
  // 获取控件的宽高
  const rect = this.$refs['controls'].getBoundingClientRect()
  if (this.position === 'right') {
    // 重新设置偏移量
    this.$refs['controls'].style['left'] = `-${rect.width}px`
  }
}

动画

主要是蒙层的显影,以及抽屉的开合。CSS动画贝塞尔曲线了解一下,笔者自己也了解不多,感兴趣可以自己去看。

transition: opacity 0.3s cubic-bezier(0.7, 0.3, 0.1, 1);

滚动条

当内容过长的时候,打开抽屉的时候,滚动条还在。因此我们需要在抽屉打开的时候打开滚动条。代码也很好写,给document.body添加overflow:hidden属性。

这里有一个小小的坑。原先的 css 是置于 scope 里面的,如果想要把这个属性添加到 body 上,是不成功的。把 scoped 去了即可。

<style>
.hidden_scoll_bar{
  overflow: hidden;
}
</style>

自定义

覆写控件样式

每个人都有自己独特的审美,不然也不会出现那么多的 UI 库了。作为一个组件的设计者,很难预设很多种样式让每一个使用组件的人都满意。不如把自己定义的控件作为插槽的后备内容,用户可以很方便的使用control的具名插槽覆写控件。

<li
  v-for="(control,idx) in controlItems"
  class="control"
  :class="'control-'+idx"
  :key="idx"
>
  <template v-if="show">
    // 提供用户自定义插槽所需要的信息(控件是否展示,控件的信息)
    <slot name="control" v-bind:drawer="{drawerShow:show,control}"
      >{{control.hidden}}</slot
    >
  </template>
  <template v-else>
    <slot name="control" v-bind:drawer="{drawerShow:show,control}"
      >{{control.show}}</slot
    >
  </template>
</li>

支持设置抽屉位置

因为抽屉支持在上下左右四个方向上放置,不同方向上定义的偏移方向都不同。因此需要定义不同的 css 类。通过传入的 position 值,利用 css 的级联特性应用样式

<div
  class="drawer__container"
  :class="[positionClass,{'drawer__container--show':show}]"
></div>
  data() {
    return {
      show: false,
      positionClass: this.position
    };
  },
// 定义右侧的drawer,其余方向上的同理
// 通过css的级联,对不同方向上的drawer添加不同的样式
.right .drawer {
  height: 100vh;
  width: 100%;
  transform: translate(100%, 0);
  top: 0;
  right: 0;
}

抽屉开启的钩子

抽屉组件内部的状态没有被暴露出去,用户可能有点击控件,不打开抽屉而去做其他事情的需求。因此我们需要提供一个钩子,通过 prop 将函数openDrawer传入,openDrawer控制是否抽屉被打开。

点击控件,开合抽屉的实现,利用了事件委托,将 click 事件,mouseover 事件直接挂载到了class=controls的 ul 元素上,为了方便识别目标li元素,给每一个 li 元素添加 :class="'control-'+idx"

<ul
  class="controls"
  @click="toggleDrawerShow"
  @mouseover="toggleDrawerShowByMouseover"
>
  <li
    v-for="(control,idx) in controlItems"
    class="control"
    :class="'control-'+idx"
    :key="idx"
  >
    <!-- xxx -->
  </li>
</ul>
// 开合抽屉的函数
openDrawerByControl(evt) {
  const onOpenDraw = this.openDrawer;
  if (!onOpenDraw) {
    this.lockedShow = true;
    return;
  }
// 获取到目标阶段指向的函数
  const target = evt.target;
//获取到代理事件的元素
  const currentTarget = evt.currentTarget;
  // 我们给openDraw传入target,currentTarget两个参数,具体由父组件决定onOpenDraw如何实现
  this.lockedShow = onOpenDraw(target, currentTarget);
}

父组件传入的函数如下,关于事件委托的知识感觉可以应用在这里,笔者做一个示例,让class='control-0'的元素不能点击。

我们使用 Element.matches 匹配.control-0类,其可以像 CSS 选择器做更加灵活的匹配。但因为 li 元素里面可能还有其他元素,所以需要不断向上寻找其父元素,直到匹配到我们事件委托的元素为止

openDrawer(target) {
  let shouldOpen = true;
   // 仅遍历到最外层
  while (!target.matches(".controls")) {
    // 判断是否匹配到我们所需要的元素上
    if (target.matches(".control-0")) {
      shouldOpen = false;
      break;
    } else {
      // 向上寻找
      target = target.parentNode;
    }
  }
  return shouldOpen;
}

总结

  • 用到了很多 Element 的方法(eg:closest,matches),平时很少接触
  • CSS 真难写,作为一个写后台的,不经常写 CSS 的表示好难,这里费了最多的功夫
  • 实践了自己之前写好一个组件的文章,知易行难,还需努力
  • 一开始自己可能很难想全组件需要什么配置,可以文档先行,先想好做什么怎么做

参考文章

【源码解读】vue `v-for`的`this._l`方法渲染列表

最近在阅读element源码的,但是element内部有很多this._l方法,element源码里面也找不到,查了一下,原来是vue的内部渲染列表的方法

源码位置,代码不长,可以一读

三个工具函数

isDef

isDef是isDefined的缩写,反过来就是isUndefined,反正就是看它是不是undefined

function isDef (v) {
  return v !== undefined && v !== null
}

isObject

isObject,主要区分原始值和对象

function isObject (obj) {
  return obj !== null && typeof obj === 'object'
}

hasSymbol

用来判断当前宿主环境是否支持原生 Symbol 和 Reflect.ownKeys。首先判断 Symbol 和 Reflect 是否存在,并使用 isNative 函数保证 Symbol 与 Reflect.ownKeys 全部是原生定义

var hasSymbol =
  typeof Symbol !== 'undefined' && isNative(Symbol) &&
  typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys);

/* 判断是否是内置方法 */
function isNative (Ctor) {
  return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}

renderList

src/core/instance/render-helpers/index.js 的installRenderHelpers方法中,renderList方法复制给了target._l ,即this._l = renderList

代码逻辑很清晰,分四种情况(你可以把val看作被v-for的那个值)

val 为 Array,或者 String

ret = new Array(val.length)
for (i = 0, l = val.length; i < l; i++) {
  ret[i] = render(val[i], i)
}

val 为 number

竟然还支持 number !!

ret = new Array(val)
for (i = 0; i < val; i++) {
    ret[i] = render(i + 1, i)
}

val 为 Object

  • 支持Symbol,且含有迭代器的情况

为啥要去判断一下有迭代器的情况,感觉不是多此一举么?理由在官方文档上写着:在遍历对象时,是按 Object.keys() 的结果遍历,但是不能保证它的结果在不同的 JavaScript 引擎下是一致的。所以为了一致性,你可以自定义一个迭代器。

Symbol.iterator 为每一个对象定义了默认的迭代器,内置类型中,Array,String,Map,Set,TypedArray而Object没有

所以为了能够使用迭代器,我们可以自己定义一个迭代器,示例代码:

const obj = {
  age: 1,
  name: 'liu',
  [Symbol.iterator]: function*() {
    let properties = Object.keys(this)
    for (let i of properties) {
      yield [i, this[i]]
    }
  }
}

const res = obj[Symbol.iterator]()
console.log('res', res.next())

所以,如果你有自定义列表顺序的需求的话,可以自定义一个迭代器,定义遍历的值的顺序

ret = []
const iterator: Iterator<any> = val[Symbol.iterator]()
let result = iterator.next()
while (!result.done) {
    ret.push(render(result.value, ret.length))
    result = iterator.next()
}
  • 不支持Symbol的情况

这种情况比较简单,通过Object.key生成对象的属性数组,然后遍历一下

keys = Object.keys(val)
ret = new Array(keys.length)
for (i = 0, l = keys.length; i < l; i++) {
    key = keys[i]
    ret[i] = render(val[key], key, i)
}

val 没有被定义的情况

返回一个空数组

if (!isDef(ret)) {
    ret = []
}

PS: 虽然我觉得这种异常情况应该置于最前,属于个人编码习惯,问题不大

总结

  • v-for 可以对数字,字符串进行遍历
  • 可以自定义对象的迭代器,实现自定义列表顺序,解决了按 Object.keys() 的结果遍历,不同js引擎下渲染顺序不一致的情况
  • TypeArray 是有迭代器的,即v-for可以渲染类数组
  • v-for 里面做了异常处理,所以当你传入了不属于Array,String,Number,Object的值时,v-for渲染一个空数组

vim


title: "Vim Tutorial"
date: 2020-08-30T11:11:25+08:00
draft: false
toc: true
images:
tags:

  • vim

基本知识

个人习惯配置

将大写键 映射为 ctrl , 退出 vim 通过 ctrl+[ 命令实现。

命令语法

每当在命令语法里看到{motion} 时,你也可以在这个地方使用文本对象,常见的例子包括 d{motion}、c{motion}和 y{motion}(更多命令,请参见表 2-1)。

文本对象

Vim 的文本对象分为两类:一类是操作分隔符的文本对象,如 i)、i" 和 it;另一类用于操作文本块,如单词、句子和段落。

Vim 的文本对象由两个字符组成,第一个字符永远是 i 或是 a。我们一般说,以 i 开头的文本对象会选择分隔符内部的文本,而以 a 开头的文本对象则会选择包括分隔符在内的整个文本。为了便于记忆,可以把 i 想成“inside”,而把 a 想成“around”或“all”

  • 操作符号+ 动作命令
    这一点感觉是 vim 操作上最大的优势之一,首先熟悉一下常用的文本对象
iw : 当前单词
aw : 当前单词及一个空格
ip : 当前段落
ap : 当前段落及一个空行
it : 这个对前端很有用,表示一个标签内的内容

以删除为例 d{motion},搭配动作命令我们可以快速做到很多操作例如

d2d  : 删除两行
daw  : 删除一个单词(包括空格)
dap  : 删除段落(包括空行)
dit  : 删除标签内的内容
dl   : 删除一个字符

移动

字符移动

h j k l,表示左下上右

单词移动

w 移动光标到下一个单词的词首
b 上一个单词的词首
e 移动到单词的末尾
ge 移动到上一个单词的末尾

^ $ 类似于正则,^ 是行首,$ 是行尾,因为行首,为了方便我把移动,我把移动到行首行尾,分为映射为 H ,L

相对屏幕移动

通过 c-f 向下翻页,c-b 向上翻页;c-e 逐行下滚,c-y 逐行上滚

输入 CTRL-G 显示当前编辑文件中当前光标所在行位置以及文件状态信息。

大写 G 跳转到文件底部,gg 跳转到文件开头

屏幕相对于光标进行移动

这种滚屏方式相对于翻页来讲,它的好处在于,你能够始终以当前光标位置做为参照,不会出现翻几次页后,发现自己迷失了方向。

  • zt scrolls to the top
  • zz scrolls to the middle
  • zb scrolls to the bottom

标记跳转移动

有的时候我们想在几个离得很远的函数相互间跳来跳去,我们可以使用 vim 的标记功能,m{字母}打标记,`{字母}进行跳转。

m ——创建标记
' ——移动到标记的文本行首
` ——移动到标记的光标位置
:marks ——列示所有标记
:delmarks ——删除指定标记
:delmarks! ——删除所有标记

基于历史记录跳转

ctrl+o: go to the older position

ctrl+i: go to the newer one

想查看详细,可以运行 :help jumplist

查找与替换

查找

  • 光标停留在要查找的单词上,按# 或*进行快速前后查找单词
  • f{字母} 快速移动到该字母的位置,然后用 ; 和 , 命令相应地正向或反向重复此跳转;
  • 命令行输入/{查找内容},可以用查找命令(以及 n / N 命令)跳转到匹配指定模式的地方。每次在可视模式中移动光标,都会改变高亮选区的边界, 搜索

替换

:s/vivian/sky/ 替换当前行第一个 vivian 为 sky
:s/vivian/sky/g 替换当前行所有 vivian 为 sky
gu	# 转小写
gU	# 转大写

编辑

缩略词

如果你有一些特定的名词很长,或者很难打,你就可以使用 vim 的缩略词

使用以下命令,将定义 ad 来代替 advertisement:

:abbreviate ad advertisement
// 简写成ab也是可以的
:ab ad advertisement

移除某个缩写 :unabbreviate ad

清除所有缩写: :abclear

撤销

撤销操作 u

撤销一个撤销 CTRL+R,我个人把这个操作符改为了 U

删除

x 代表删当前光标的所在的一个字符

更改类操作符的工作方式跟删除类是一致的。操作格式是:

c [number] motion,删除后进入编辑模式

d [number] motion 仅删除

大小写转化

进入可视模式,选择文本,然后直接按 gU,会把小写转大写,gu 把大写转小写。

另一种方式,在 normal 模式下,gUw 把后面的单词大写,guw把剩下的单词小写。gUiw 把光标所在的单词大写

vim 交换两行的位置 ddp

/foo, 通过 n 查找下一个,通过 n 查找上一个.

复制粘贴

方法一:按 v 进入可视模式,然后通过方向键选择,按 y 进行复制,按 p 进行粘贴

方法二:进行命令默认,输入y2,代表复制当前光标下的两行,然后按p进行粘贴

复制一个 vim 文件内的所有内容

:%y+

to yank all lines.

Explanation:

% to refer the next command to work on all the lines
y to yank those lines
- to copy to the system clipboard

回退操作

u : 撤销上一步操作
ctrl+r : 恢复上一步被撤销的操作

可视模式

vim 对于从列块可视模式切换到插入模式的命令也遵从类似的约定。i 命令和 a 命令都完成此切换,并分别把光标置于选区的开头和结尾。那 i 和 a 命令呢,它们在可视模式里干什么?在可视模式及操作符待决模式中,i 和 a 键沿用一个不同的约定。它们会被当作一个文本对象的组成部分,我们将在技巧 51 中深入探讨文本对象。如果你在列块可视模式里选中了一块区域,并且很奇怪为什么按 i 键没进入插入模式,那么换用 i 键试一下。

工作流

文件间切换

:bn下一个文件
:bp上一个文件

vim 里面的 leader 是什么

可以自定义的快捷键,一般定义为空格,很方便,逗号不太好,因为不太好按。

在 vim 里面执行 shell 命令

!<shell command>

刷新在 vi(m)中打开的文件的更改内容

:edit 可以缩写为 :e
:edit!用来强制重新加载当前文件(您将丢失所做 的修改)。

录制宏

  1. 在 normal 模式下,按 q 加一个字母开始录制。例如按下 qr,将该宏注册为 r。

进行任意的操作

  1. 按下 q 完成录制。

如何使用宏

@r , 使用 @@ 重复使用上一次的宏

分屏

:vsp : 垂直分屏 :sp 水平分屏

ctrl+w c 退出分屏

非常好用

vundle

自动运行

vim diff

把两个文本打开,然后在屏幕下分别运行:diffthis,即可比较两个文本。

neovim

使用 neovim 已经它的插件能很方便的给你带来代码补全等 IDE 的功能。

Vim for TypeScript and React in 2020

pip3 install --user neovim

常用插件

nerdtree

use to toggle nerdtree

:nerdtreefind : 打开当前文件目录,vim 快捷键是 <leader> e

let g:nerdtreequitonopen = 1 打开文件后自动关闭 nerdtree ,一般情况下建议不关闭,不关的话,感觉就很像 vscode 了。

  • 添加书签

有的文件目录很大,可以添加书签方便下次查找

in [nerdtree], you can toggle the display of bookmarks with b. when your cursor is on a file name there do :bookmark to add it to the bookmarks list.

this is nice when you’re working in a folder with 414 files and you only need to periodically work with 3 of them that are in deeply nested directories.

:b 命令展示所有的标签

  • menu

pressing m would open a menu below and you can select from a list of actions. 你可以进行额外的一些操作

vim 使用 prettier

surround vim 使用指南

markdown

在 vim 中使用 markdown 也是很方便的。

参考

vim-fugitive

http://tpope/vim-fugitive,通过命令 Gdiff 可以很方便查看文件有哪些改动。

其他问题

vim 换行

教程上说设置 set wrap 就可以了,但是没有效果。需要删除一些文件 rm -rf ~/.vimviews

vscode vim 插件

通过这个命令就可以到文件夹,查看文件树:

pane/window with normal vim commands: <c-w> + h.

参考

JavaScript中 string 的方法 与 regx的对比

前言

同样是因为容易搞混,所以需要梳理一下

可以传入正则表达式的string方法

  • split
  • replace
  • match : The match() method retrieves the result of matching a string against a regular expression.
  • matchAll
  • search

正则表达式

  • RegExp.prototype.test(): The test() method executes a search for a match between a regular expression and a specified string. Returns true or false.

  • RegExp.prototype.exec(): The exec() method executes a search for a match in a specified string. Returns a result array, or null.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.