Better

Ethan的博客,欢迎访问交流

Vue进阶

哈哈,其实也没那么进阶,就是对于基础的一个扩充吧

发展

vue 2.x

  • 数据绑定 MVVM 模式
  • vue 单文件开发方式,起初 jsx 支持不好,自创 vue 方式
  • render 方法
    • vue2 采用虚拟 dom
    • 数据变动,自动调用 render
    • 会逐层遍历 template 结构,渲染数据

API重点

  • 生命周期方法
  • computed
    • 有变动自动调用
    • 缓存功能
    • 不用调用,直接写函数名即可

vue 使用 jsx 与使用 vue 文件

  • jsx操作能力更强,因为你可以使用js的所有能力,而不是通过v-if等指令,vue2开始支持
  • vue 结构更清晰,使用更方便

项目结构

通常使用vue-cli生成的项目骨架是比较简单的,我们通常开发位于src文件夹下,src如何划分才能使得项目简介明了,可维护,而且方便我们直接定位代码呢。我比较赞同下面这种

├── App.vue             # 项目如何组件
├── api/service         # 存放接口相关文件
├── assets              # 存放项目资源文件,比如图片
├── components          # 存放通用组件
├── directive           # 存放全局自定义指令
├── filters             # 存放全局过滤器
├── main.js             # 项目入口文件
├── mock                # mock数据
├── router              # 路由
├── store               # 状态管理
├── styles              # 样式文件
├── utils               # 存放工具函数
└── views/pages         # 存放视图类组件

如果项目很大,可以在各自文件夹下进一步细分,但外层的结构基本满足绝大部分需求了,规范的目录结构可以很好的规范化你的开发习惯,代码分工明确,便于维护。

混入

混入(mixins)

  • 混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项。
  • 当组件和混入对象含有同名选项时,这些选项将以恰当的方式混合。
    • 数据对象在内部会进行浅合并 (一层属性深度),在和组件的数据发生冲突时以组件数据优先。
    • 同名钩子函数将混合为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。
    • 值为对象的选项,例如 methods, components 和 directives,将被混合为同一个对象。两个对象键名冲突时,取组件对象的键值对
  • 全局混入(Vue.mixin),注意使用! 一旦使用全局混入对象,将会影响到 所有 之后创建的 Vue 实例

自定义指令

除了内置的指令外,Vue 也允许注册自定义指令。在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。

我们可以通过 Vue.directive(id, [definition]) 全局注册指令,可以通过在组件中接受一个 directives 的选项来注册局部指令。

Vue.directive("helloworld", {
  bind: function (el, binding) {
    el.innerHTML = "<h1>"+ binding.value +"</h1>"
  }
})

指令有bind、inserted、update、componentUpdated 和 unbind 5个钩子函数,对应钩子函数的参数 (即 el、binding、vnode 和 oldVnode)。深入了解前往官网自定义指令

过滤器

Vue.js 允许你自定义过滤器,可被用于一些常见的文本格式化。过滤器可以用在两个地方:双花括号插值和 v-bind 表达式,过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”符号指示,帮助我们处理数据转换相关的工作。

// 注册
Vue.filter('my-filter', function (value) {
  // 返回处理后的值
})
// 你也可以在组件选项中定义本地的过滤器 filters

过滤器函数总接收表达式的值 (之前的操作链的结果) 作为第一个参数。如果需要多个参数,则如下使用

{{ message | filterA('arg1', arg2) }}

过滤器可以串联使用

Vue 内置的过滤器也有必要了解下,就可以避免重复实现已经存在的过滤器啦

  • capitalize:首字母大写
  • uppercase:全部大写
  • lowercase:全部小写
  • currency:输出金钱以及小数点,第一个参数表示货币符号,第二个参数表示小数位
  • pluralize
  • debounce:需要在@中使用,默认值: 300,包装处理器,让它延迟执行 x ms, 默认延迟 300ms。包装后的处理器在调用之后至少将延迟 x ms, 如果在延迟结束前再次调用,延迟时长重置为 x ms。
  • limitBy:在v-for中使用,第一个参数表示取得数量,第二个表示偏移量
  • filterBy:第一个参数:{String | Function} 表示搜索的字符串,第二个参数:in(可选,指定搜寻位置),第三个参数:{String|Array}
  • orderBy:需在v-for(即数组)里面使用,第一个参数:{String | Array | Function} 需要搜索的字符串,第二个参数: {String} 可选参数 order 决定结果升序(order >= 0)或降序(order < 0),默认是升序

编译

Vue 推荐在绝大多数情况下使用 template 来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力,这就是 render 函数,它比 template 更接近编译器。

这玩意有点类似 angular 的 compile,在你通过代码 append html 片段或者 dom 元素时,如果使用了 vue 语法,那么 vue 此时时不会认的,需要 compile 一下。

var tpl = Vue.compile('<h1>{{ msg }}</h1>')
new Vue({
  el: "#app",
  data () {
    return {
      msg: "Hello Vue.js!"
    }
  },
  render: tpl.render
})

事件总线

如果是多个组件或者不同父组件的组件间通信,这时我们可以借助创建空的 Vue 实例作为事件总线,通过发布订阅模式进行数据传递。

var bus = new Vue()

// 组件中使用 bus 订阅发布事件
bus.$emit(eventName, payload)
bus.$on(eventName, function (payload) {

})

更方便的做法可以将bus挂在到Vue原型上, 这样在组件中就可以随意使用了

Vue.prototype.bus = new Vue()

渲染函数 & JSX

在 Vue 中,我们通常使用如下方式定义组件模版

  • 单元件组件中的 template 标签
  • 通过 template 属性指定模版字符串

webpack 在处理.vue 文件时,会对其执行一个转换。在转换过程中,CSS 被从组件中提取出来,并放到单独的文件中,剩余部分则被转换为 JavaScript。template 标签内容会转换为 template 属性内容

当模版编译器遇到 template 属性时,它将 template 属性提取出来,并将它的内容编译为 JavaScript,然后将一个渲染函数添加到组件对象中。这个渲染函数将返回已转换为 JavaScript 的 template 属性的内容。渲染函数还允许我们在 Vue 中使用 JSX!

render(h) {
  return h(
    'div',
    { class: 'mood' },
    this.todayIsSunny ? 'Makes me happy' : 'Eh! Doesn't bother me'
  )
}

我们不需要等待 Vue 为我们添加渲染函数,我们可以自定义渲染函数,而且它应该优先于 template 属性。

无渲染组件中的 slot

render(h) {
    return h('div', this.$slots.default)
}

其中 render 函数中的 createElement 方法有三个参数。第一个参数为外层标签名,第二个为外层标签的属性对象,第三个为外层标签中的内容,同时第三个参数除了字符串,还可以传入 VNode 的数组。VNode 就是 vue 中的节点。同时你需要的知道是 createElement 返回新的 VNode 节点,因此结果可以作为第三个参数,如此就带来灵活的可组合性

通过 this.$slots.default 获取所有插入到组件内默认 slot 的节点

通过 slot 可以自定义组件的一些内容和功能,但是它并不允许我们暴露子组件的任何内容。此时我们需要 scoped slot。

render(h) {
    return this.$scopedSlots.default({
        on: this.currentState,
        setOn: this.setOn,
        setOff: this.setOff,
        toggle: this.toggle,
    })
}

此时在组件外部就可以通过 slot-scope 得到对应的属性和函数,需要注意的是,不能直接作用在组件上,需要在组件子元素上,比如自定义 toggle 组件

<toggle>
    <div slot-scope="{ on, setOn, setOff }" class="container"></div>
</toggle>

插件开发

在实际开发中,我们需要引入各种模块来实现功能,或者给全局的 Vue 对象添加一些全局功能。这些都可以通过插件帮助我们完成,我们也可以开发自己的插件。

Vuejs 插件范围没有限制,一般有以下几种:

  • 添加全局方法或者属性,直接挂在 Vue 对象上
  • 添加全局资源:指令、过滤器等
  • 通过全局 mixin 方法添加一些组件选项,如:vue-router
  • 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。
  • 一个库,提供自己的 API,同时提供上面提到的一个或多个功能,如:vue-router

创建自己的 Vue 插件需要一个公开方法 install,这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的配置对象,书写方式如下:

const MyPlugin = {}
MyPlugin.install = function(Vue, options) {
  // 1. 添加全局方法或属性
  Vue.myGlobalProp = 'yugasun';
  Vue.myGlobalMethod = function() {
    // 逻辑
  };
  // 2. 添加全局资源:
  Vue.directive('my-directive', {
    bind(el, binding, vnode, oldVnode) {
      // 逻辑
    }
  });
  // 3. 注入通用方法或属性给组件
  Vue.mixin({
    data(){
      return {
        // 通用数据,
      };
    },
    created() {
      // 逻辑
    },
  });
  // 4. 添加实例方法
  Vue.prototype.$myMethod = function(methodOptions) {
    // 逻辑
  };
};
export default MyPlugin

插件使用起来很简单,只需要引入,然后通过调用 Vue.use() 方法就行了

import Vue from 'vue';
import MyPlugin from './MyPlugin';
Vue.use(MyPlugin, {someOption: true});

路由深入

原理

什么是路由?简单的说,路由是根据不同的 url 地址展示不同的内容或页面

使用场景?前端路由更多用在单页应用上, 也就是SPA, 因为单页应用, 基本上都是前后端分离的, 后端自然也就不会给前端提供路由。

前端的路由和后端的路由在实现技术上不一样,但是原理都是一样的。在 HTML5 的 history API 出现之前,前端的路由都是通过 hash 来实现的,hash 能兼容低版本的浏览器。

两种实现前端路由的方式

  1. HTML5 History 两个新增的API:history.pushState 和 history.replaceState,两个 API 都会操作浏览器的历史记录,而不会引起页面的刷新。
  2. 在 HTML5 的 history API 出现之前,前端的路由都是通过 hash 来实现的,hash 能兼容低版本的浏览器。Hash就是url 中看到 # ,我们需要一个根据监听哈希变化触发的事件( hashchange) 事件。我们用 window.location 处理哈希的改变时不会重新渲染页面,而是当作新页面加到历史记录中,这样我们跳转页面就可以在 hashchange 事件中注册 ajax 从而改变页面内容。Web 服务并不会解析 hash,也就是说 # 后的内容 Web 服务都会自动忽略,但是 JavaScript 是可以通过 window.location.hash 读取到的,读取到路径加以解析之后就可以响应不同路径的逻辑处理。

优点

  1. 从性能和用户体验的层面来比较的话,后端路由每次访问一个新页面的时候都要向服务器发送请求,然后服务器再响应请求,这个过程肯定会有延迟。而前端路由在访问一个新页面的时候仅仅是变换了一下路径而已,没有了网络延迟,对于用户体验来说会有相当大的提升。
  2. 在某些场合中,用ajax请求,可以让页面无刷新,页面变了但Url没有变化,用户就不能复制到想要的地址,用前端路由做单页面网页就很好的解决了这个问题

缺点

  1. 使用浏览器的前进,后退键的时候会重新发送请求,没有合理地利用缓存。

实现

知道的原理后,自己实现一个简易版的路由就并不复杂了,比如在 Vue 中我们可以怎么做呢?

  1. 定义路由关系,路径 -> 组件
  2. 在路由跳转处,想办法通知路径改变,寻找合适组件
    • hash方式,window.location.hash 实现跳转
    • HTML5 history API pushState 方式
  3. 自定义渲染组件,渲染函数 render 渲染目标组件
    render(createElement) {
     // 此处必须使用 default 属性作为参数
     return createElement(this.ViewComponent.default);
    },
    

生产环境部署

注意事项

  1. 开启生产环境模式
  2. 模版预编译,使用DOM内模版或JavaScript内字符串模版时,会在运行时编译为渲染函数,对性能敏感最好避免这种做法
    • 预编译模板最简单的方式就是使用单文件组件——相关的构建设置会自动把预编译处理好
    • 在webpack中,可以使用vue-template-loader,它也可以在构建过程中把模板文件转换成为 JavaScript 渲染函数。
  3. 提取组件的 CSS
    • 当使用单文件组件时,组件内的 CSS 会以 <style> 标签的方式通过 JavaScript 动态注入,会有运行时开销
    • 将所有组件的 CSS 提取到同一个文件可以避免这个问题,也会让 CSS 更好地进行压缩和缓存。
  4. 跟踪运行时错误,错误将会被传递至全局 Vue.config.errorHandler 配置函数,可以用来实现错误上报

深入响应式原理

当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。

Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是为什么 Vue 不支持 IE8 以及更低版本浏览器。

受现代 JavaScript 的限制 (以及废弃 Object.observe),Vue 不能检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化过程,所以属性必须在 data 对象上存在才能让 Vue 转换它,这样才能让它是响应的。

Vue 不允许在已经创建的实例上动态添加新的根级响应式属性 (root-level reactive property)。然而它可以使用 Vue.set(object, key, value) 方法将响应属性添加到嵌套的对象上。在组件内容可以直接使用this.$set,他是全局Vue.set的别名。

具体使用限制

  • Vue 不能检测以下变动的数组:
    • 当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue
    • 当你修改数组的长度时,例如:vm.items.length = newLength
  • Vue 不能检测对象属性的添加或删除

有时你可能需要为已有对象赋予多个新属性,比如使用 Object.assign() 或 _.extend()。在这种情况下,你应该用两个对象的属性创建一个新的对象。所以,如果你想添加新的响应式属性,不要像这样:

Object.assign(vm.userProfile, {
  age: 27,
  favoriteColor: 'Vue Green'
})

你应该这样做:

vm.userProfile = Object.assign({}, vm.userProfile, {
  age: 27,
  favoriteColor: 'Vue Green'
})

由于 Vue 不允许动态添加根级响应式属性,所以你必须在初始化实例前声明根级响应式属性,哪怕只是一个空值。

这样的限制在背后是有其技术原因的,它消除了在依赖项跟踪系统中的一类边界情况,也使 Vue 实例在类型检查系统的帮助下运行的更高效。而且在代码可维护性方面也有一点重要的考虑:data 对象就像组件状态的概要,提前声明所有的响应式属性,可以让组件代码在以后重新阅读或其他开发人员阅读时更易于被理解。

在组件开发时,我们也有可能需要属性在不同函数之间传递,但是并不需要显示在页面上,通常我们可以声明在created()钩子函数中。

异步更新队列:Vue是异步执行DOM更新的,只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。

Vue.js 通常鼓励开发人员沿着“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们确实要这么做。为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback) 。这样回调函数在 DOM 更新完成后就会调用。

在组件内使用 vm.$nextTick() 实例方法特别方便,因为它不需要全局 Vue ,并且回调函数中的 this 将自动绑定到当前的 Vue 实例上

Refer



留言