文档_Vue04_生产部署_单文件组件_单元测试_TypeScript支持_路由_状态管理_响应式原理_3大框架对比

Vue.js_官方文档学习笔记_Part_04

文档_Vue04_生产部署_单文件组件_单元测试_TypeScript支持_路由_状态管理_响应式原理_3大框架对比




生产环境部署

开启生产环境模式

开发环境下,Vue 会提供很多警告来帮你对付常见的错误与陷阱。

而在生产环境下,这些警告语句却没有用,反而会增加应用的体积。

此外,有些警告检查还有一些小的运行时开销,这在生产环境模式下是可以避免的。

不使用构建工具

如果用 Vue 完整独立版本,即直接用 <script> 元素引入 Vue 而不提前进行构建,

请记得在生产环境下使用压缩后的版本 (vue.min.js)。

两种版本都可以在安装指导中找到。

使用构建工具

当使用 webpack 或 Browserify 类似的构建工具时,

Vue 源码会根据 process.env.NODE_ENV决定是否启用生产环境模式,默认情况为开发环境模式。

在 webpack 与 Browserify 中都有方法来覆盖此变量,以启用 Vue 的生产环境模式,

同时在构建过程中警告语句也会被压缩工具去除。

这些所有 vue-cli 模板中都预先配置好了,但了解一下怎样配置会更好。


webpack

使用 webpack 的 DefinePlugin 来指定生产环境,以便在压缩时可以让 UglifyJS 自动删除警告代码块。

例如配置:

var webpack = require('webpack')

module.exports = {
  // ...
  plugins: [
    // ...
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: '"production"'
      }
    }),
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false
      }
    })
  ]
}

Browserify

  • 在运行打包命令时将 NODE_ENV 设置为 "production"。这等于告诉 vueify 避免引入热重载和开发相关的代码。
  • 对打包后的文件进行一次全局的 envify 转换。这使得压缩工具能清除调 Vue 源码中所有用环境变量条件包裹起来的警告语句。例如:
NODE_ENV=production browserify -g envify -e main.js | uglifyjs -c -m > build.js
  • 或者在 Gulp 中使用 envify

    // 使用 envify 的自定义模块来定制环境变量
    var envify = require('envify/custom')
    
    browserify(browserifyOptions)
      .transform(vueify)
      .transform(
        // 必填项,以处理 node_modules 里的文件
        { global: true },
        envify({ NODE_ENV: 'production' })
      )
      .bundle()
    
  • 或者配合 Grunt 和 grunt-browserify 使用 envify

    // 使用 envify 自定义模块指定环境变量
    var envify = require('envify/custom')
    
    browserify: {
      dist: {
        options: {
            // 该函数用来调整 grunt-browserify 的默认指令
            configure: b => b
            .transform('vueify')
            .transform(
                // 用来处理 `node_modules` 文件
              { global: true },
              envify({ NODE_ENV: 'production' })
            )
            .bundle()
        }
      }
    }
    

Rollup

使用 rollup-plugin-replace

const replace = require('rollup-plugin-replace')
rollup({
  // ...
  plugins: [
    replace({
      'process.env.NODE_ENV': JSON.stringify( 'production' )
    })
  ]
}).then(...)

模板预编译

当使用 DOM 内模板或 JavaScript 内的字符串模板时,模板会在运行时被编译为渲染函数。

通常情况下这个过程已经足够快了,但对性能敏感的应用还是最好避免这种用法。


预编译模板最简单的方式就是使用单文件组件——相关的构建设置会自动把预编译处理好,

所以构建好的代码已经包含了编译出来的渲染函数而不是原始的模板字符串。


如果你使用 webpack,并且喜欢分离 JavaScript 和模板文件,

你可以使用 vue-template-loader,它也可以在构建过程中把模板文件转换成为 JavaScript 渲染函数。

提取组件的 CSS

当使用单文件组件时,组件内的 CSS 会以 <style> 标签的方式通过 JavaScript 动态注入。

这有一些小小的运行时开销,如果你使用服务端渲染,这会导致一段“无样式内容闪烁 (fouc)”。

将所有组件的 CSS 提取到同一个文件可以避免这个问题,也会让 CSS 更好地进行压缩和缓存。

查阅这个构建工具各自的文档来了解更多:

  • webpack + vue-loader (vue-cli 的 webpack 模板已经预先配置好)
  • Browserify + vueify
  • Rollup + rollup-plugin-vue

跟踪运行时错误

如果在组件渲染时出现运行错误,错误将会被传递至全局 Vue.config.errorHandler 配置函数 (如果已设置)。

利用这个钩子函数来配合错误跟踪服务是个不错的主意。

比如 Sentry,它为 Vue 提供了官方集成




单文件组件

介绍

在很多 Vue 项目中,我们使用 Vue.component 来定义全局组件,

紧接着用 new Vue({ el: '#container '}) 在每个页面内指定一个容器元素。

这种方式在很多中小规模的项目中运作的很好,在这些项目里 JavaScript 只被用来加强特定的视图。

但当在更复杂的项目中,或者你的前端完全由 JavaScript 驱动的时候,下面这些缺点将变得非常明显:

  • 全局定义 (Global definitions) 强制要求每个 component 中的命名不得重复
  • 字符串模板 (String templates) 缺乏语法高亮,在 HTML 有多行的时候,需要用到丑陋的 \
  • 不支持 CSS (No CSS support) 意味着当 HTML 和 JavaScript 组件化时,CSS 明显被遗漏
  • 没有构建步骤 (No build step) 限制只能使用 HTML 和 ES5 JavaScript, 而不能使用预处理器,如 Pug (formerly Jade) 和 Babel

文件扩展名为 .vue 的 single-file components(单文件组件) 为以上所有问题提供了解决方法,

并且还可以使用 webpack 或 Browserify 等构建工具。

这是一个文件名为 Hello.vue 的简单实例:

文档_Vue04_生产部署_单文件组件_单元测试_TypeScript支持_路由_状态管理_响应式原理_3大框架对比

现在我们获得:

  • 完整语法高亮
  • CommonJS 模块
  • 组件作用域的 CSS

正如我们说过的,我们可以使用预处理器来构建简洁和功能更丰富的组件,

比如 Pug,Babel (with ES2015 modules),和 Stylus。

文档_Vue04_生产部署_单文件组件_单元测试_TypeScript支持_路由_状态管理_响应式原理_3大框架对比

这些特定的语言只是例子,

你可以只是简单地使用 Babel,TypeScript,SCSS,PostCSS - 或者其他任何能够帮助你提高生产力的预处理器。

如果搭配 vue-loader 使用 webpack,它也是把 CSS Modules 当作第一公民来对待的。

怎么看待关注点分离?

一个重要的事情值得注意,关注点分离不等于文件类型分离。

在现代 UI 开发中,我们已经发现:

相比于把代码库分离成三个大的层次并将其相互交织起来,把它们划分为松散耦合的组件再将其组合起来更合理一些。

在一个组件里,其模板、逻辑和样式是内部耦合的,并且把他们搭配在一起实际上使得组件更加内聚且更可维护。

即便你不喜欢单文件组件,你仍然可以把 JavaScript、CSS 分离成独立的文件然后做到热重载和预编译。

<!-- my-component.vue -->
<template>
  <div>This will be pre-compiled</div>
</template>
<script src="./my-component.js"></script>
<style src="./my-component.css"></style>

起步

例子沙箱

如果你希望深入了解并开始使用单文件组件,请来 CodeSandbox 看看这个简单的 todo 应用

针对刚接触 JavaScript 模块开发系统的用户

有了 .vue 组件,我们就进入了高级 JavaScript 应用领域。

如果你没有准备好的话,意味着还需要学会使用一些附加的工具:

  • Node Package Manager (NPM):阅读 Getting Started guide 直到 10: Uninstalling global packages章节。

  • Modern JavaScript with ES2015/16:阅读 Babel 的 Learn ES2015 guide。你不需要立刻记住每一个方法,但是你可以保留这个页面以便后期参考。

在你花一天时间了解这些资源之后,我们建议你参考 webpack 模板。

只要遵循指示,你就能很快地运行一个用到 .vue 组件,ES2015 和热重载 (hot-reloading) 的 Vue 项目!

想学习更多 webpack 的知识,请移步它们的官方文档以及 webpack learning academy


在 webpack 中,每个模块被打包到 bundle 之前都由一个相应的“loader”来转换,

Vue 也提供 vue-loader 插件来执行 .vue 单文件组件 的转换。

针对高级用户

无论你更钟情 webpack 或是 Browserify,我们为简单的和更复杂的项目都提供了一些文档模板。

我们建议浏览 github.com/vuejs-templates,找到你需要的部分,

然后参考 README 中的说明,使用 vue-cli 工具生成新的项目。


模板中使用 webpack,一个模块加载器加载多个模块然后构建成最终应用。

为了进一步了解 webpack,可以看 官方介绍视频。如果你有基础,可以看 在 Egghead.io 上的 webpack 进阶教程



单元测试

配置和工具

任何兼容基于模块的构建系统都可以正常使用,但如果你需要一个具体的建议,可以使用 Karma 进行自动化测试。

它有很多社区版的插件,包括对 Webpack 和 Browserify 的支持。


更多详细的安装步骤,请参考各项目的安装文档,

通过这些 Karma 配置的例子可以快速帮助你上手 (Webpack 配置,Browserify 配置)。

简单的断言

你不必为了可测性在组件中做任何特殊的操作,导出原始设置就可以了:

<template>
  <span>{{ message }}</span>
</template>

<script>
  export default {
    data () {
      return {
        message: 'hello!'
      }
    },
    created () {
      this.message = 'bye!'
    }
  }
</script>

然后随着 Vue 导入组件的选项,你可以使用许多常见的断言:

// 导入 Vue.js 和组件,进行测试
import Vue from 'vue'
import MyComponent from 'path/to/MyComponent.vue'

// 这里是一些 Jasmine 2.0 的测试,你也可以使用你喜欢的任何断言库或测试工具。

describe('MyComponent', () => {
  // 检查原始组件选项
  it('has a created hook', () => {
    expect(typeof MyComponent.created).toBe('function')
  })

  // 评估原始组件选项中的函数的结果
  it('sets the correct default data', () => {
    expect(typeof MyComponent.data).toBe('function')
    const defaultData = MyComponent.data()
    expect(defaultData.message).toBe('hello!')
  })

  // 检查 mount 中的组件实例
  it('correctly sets the message when created', () => {
    const vm = new Vue(MyComponent).$mount()
    expect(vm.message).toBe('bye!')
  })

  // 创建一个实例并检查渲染输出
  it('renders the correct message', () => {
    const Constructor = Vue.extend(MyComponent)
    const vm = new Constructor().$mount()
    expect(vm.$el.textContent).toBe('bye!')
  })
})

编写可被测试的组件

很多组件的渲染输出由它的 props 决定。

事实上,如果一个组件的渲染输出完全取决于它的 props,

那么它会让测试变得简单,就好像断言不同参数的纯函数的返回值。

看下面这个例子:

<template>
  <p>{{ msg }}</p>
</template>

<script>
  export default {
    props: ['msg']
  }
</script>

你可以在不同的 props 中,通过 propsData 选项断言它的渲染输出:

import Vue from 'vue'
import MyComponent from './MyComponent.vue'

// 挂载元素并返回已渲染的文本的工具函数
function getRenderedText (Component, propsData) {
  const Constructor = Vue.extend(Component)
  const vm = new Constructor({ propsData: propsData }).$mount()
  return vm.$el.textContent
}

describe('MyComponent', () => {
  it('renders correctly with different props', () => {
    expect(getRenderedText(MyComponent, {
      msg: 'Hello'
    })).toBe('Hello')

    expect(getRenderedText(MyComponent, {
      msg: 'Bye'
    })).toBe('Bye')
  })
})

断言异步更新

由于 Vue 进行 异步更新 DOM 的情况,一些依赖 DOM 更新结果的断言必须在 Vue.nextTick 回调中进行:

// 在状态更新后检查生成的 HTML
it('updates the rendered message when vm.message updates', done => {
  const vm = new Vue(MyComponent).$mount()
  vm.message = 'foo'

  // 在状态改变后和断言 DOM 更新前等待一刻
  Vue.nextTick(() => {
    expect(vm.$el.textContent).toBe('foo')
    done()
  })
})

我们计划做一个通用的测试工具集,让不同策略的渲染输出 (例如忽略子组件的基本渲染) 和断言变得更简单。

关于更深入的 Vue 单元测试的内容,请移步 vue-test-utils



TypeScript 支持

在 Vue 2.5.0 中,我们大大改进了类型声明以更好地使用默认的基于对象的 API。

同时此版本也引入了一些其它变化,需要开发者作出相应的升级。

阅读博客文章了解更多详情。

发布为 NPM 包的官方声明文件

静态类型系统能帮助你有效防止许多潜在的运行时错误,而且随着你的应用日渐丰满会更加显著。

这就是为什么 Vue 不仅仅为 Vue core 提供了针对 TypeScript 的官方类型声明

还为 Vue Router 和 Vuex 也提供了相应的声明文件。


而且,我们已经把它们发布到了 NPM,最新版本的 TypeScript 也知道该如何自己从 NPM 包里解析类型声明。

这意味着只要你成功地通过 NPM 安装了,就不再需要任何额外的工具辅助,即可在 Vue 中使用 TypeScript 了。


我们还计划在近期为 vue-cli 提供一个选项,来初始化一个立即可投入开发的 Vue + TypeScript 项目脚手架。

推荐配置

// tsconfig.json
{
  "compilerOptions": {
    // 与 Vue 的浏览器支持保持一致
    "target": "es5",
    // 这可以对 `this` 上的数据属性进行更严格的推断
    "strict": true,
    // 如果使用 webpack 2+ 或 rollup,可以利用 tree-shake:
    "module": "es2015",
    "moduleResolution": "node"
  }
}

注意你需要引入 strict: true (或者至少 noImplicitThis: true,这是 strict 模式的一部分) 

以利用组件方法中 this 的类型检查,否则它会始终被看作 any 类型。

参阅 TypeScript 编译器选项文档 (英) 了解更多。

开发工具链

要使用 TypeScript 开发 Vue 应用程序,我们强烈建议您使用 Visual Studio Code,它为 TypeScript 提供了极好的“开箱即用”支持。

如果你正在使用单文件组件 (SFC), 可以安装提供 SFC 支持以及其他更多实用功能的 Vetur 插件

WebStorm 同样为 TypeScript 和 Vue.js 提供了“开箱即用”的支持。

基本用法

要让 TypeScript 正确推断 Vue 组件选项中的类型,您需要使用 Vue.component 或 Vue.extend 定义组件:

import Vue from 'vue'
const Component = Vue.extend({
  // 类型推断已启用
})

const Component = {
  // 这里不会有类型推断,
  // 因为TypeScript不能确认这是Vue组件的选项
}

基于类的 Vue 组件

如果您在声明组件时更喜欢基于类的 API,则可以使用官方维护的 vue-class-component 装饰器:

import Vue from 'vue'
import Component from 'vue-class-component'

// @Component 修饰符注明了此类为一个 Vue 组件
@Component({
  // 所有的组件选项都可以放在这里
  template: '<button @click="onClick">Click!</button>'
})
export default class MyComponent extends Vue {
  // 初始数据可以直接声明为实例的属性
  message: string = 'Hello!'

  // 组件方法也可以直接声明为实例的方法
  onClick (): void {
    window.alert(this.message)
  }
}

增强类型以配合插件使用

插件可以增加 Vue 的全局/实例属性和组件选项。

在这些情况下,在 TypeScript 中制作插件需要类型声明。

庆幸的是,TypeScript 有一个特性来补充现有的类型,叫做模块补充 (module augmentation)

例如,声明一个 string 类型的实例属性 $myProperty

// 1. 确保在声明补充的类型之前导入 'vue'
import Vue from 'vue'

// 2. 定制一个文件,设置你想要补充的类型
//    在 types/vue.d.ts 里 Vue 有构造函数类型
declare module 'vue/types/vue' {
// 3. 声明为 Vue 补充的东西
  interface Vue {
    $myProperty: string
  }
}

在你的项目中包含了上述作为声明文件的代码之后 (像 my-property.d.ts),

你就可以在 Vue 实例上使用 $myProperty 了。

var vm = new Vue()
console.log(vm.$myProperty) // 将会顺利编译通过

你也可以声明额外的属性和组件选项:

import Vue from 'vue'

declare module 'vue/types/vue' {
  // 可以使用 `VueConstructor` 接口
  // 来声明全局属性
  interface VueConstructor {
    $myGlobal: string
  }
}

// ComponentOptions 声明于 types/options.d.ts 之中
declare module 'vue/types/options' {
  interface ComponentOptions<V extends Vue> {
    myOption?: string
  }
}

上述的声明允许下面的代码顺利编译通过:

// 全局属性
console.log(Vue.$myGlobal)

// 额外的组件选项
var vm = new Vue({
  myOption: 'Hello'
})

标注返回值

因为 Vue 的声明文件天生就具有循环性,TypeScript 可能在推断某个方法的类型的时候存在困难。

因此,你可能需要在 render 或 computed 里的方法上标注返回值。

import Vue, { VNode } from 'vue'

const Component = Vue.extend({
  data () {
    return {
      msg: 'Hello'
    }
  },
  methods: {
    // 需要标注有 `this` 参与运算的返回值类型
    greet (): string {
      return this.msg + ' world'
    }
  },
  computed: {
    // 需要标注
    greeting(): string {
      return this.greet() + '!'
    }
  },
  // `createElement` 是可推导的,但是 `render` 需要返回值类型
  render (createElement): VNode {
    return createElement('div', this.greeting)
  }
})

如果你发现类型推导或成员补齐不工作了,标注某个方法也许可以帮助你解决这个问题。

使用 --noImplicitAny 选项将会帮助你找到这些未标注的方法。



路由

官方路由

对于大多数单页面应用,都推荐使用官方支持的 vue-router 库

更多细节可以看 vue-router 文档

从零开始简单的路由

如果只需要非常简单的路由而不需要引入整个路由库,可以动态渲染一个页面级的组件像这样:

const NotFound = { template: '<p>Page not found</p>' }
const Home = { template: '<p>home page</p>' }
const About = { template: '<p>about page</p>' }

const routes = {
  '/': Home,
  '/about': About
}

new Vue({
  el: '#app',
  data: {
    currentRoute: window.location.pathname
  },
  computed: {
    ViewComponent () {
      return routes[this.currentRoute] || NotFound
    }
  },
  render (h) { return h(this.ViewComponent) }
})

结合 HTML5 History API,你可以建立一个非常基本但功能齐全的客户端路由器。

可以直接看实例应用

整合第三方路由

如果有非常喜欢的第三方路由,如 Page.js 或者 Director,整合很简单

这有个用了 Page.js 的复杂示例



状态管理

类 Flux 状态管理的官方实现

由于状态零散地分布在许多组件和组件之间的交互中,大型应用复杂度也经常逐渐增长。

为了解决这个问题,Vue 提供 vuex:我们有受到 Elm 启发的状态管理库。

vuex 甚至集成到 vue-devtools,无需配置即可进行时光旅行调试

React 的开发者请参考以下信息

如果你是来自 React 的开发者,你可能会对 Vuex 和 Redux 间的差异表示关注,Redux 是 React 生态环境中最流行的 Flux 实现。

Redux 事实上无法感知视图层,所以它能够轻松的通过一些简单绑定和 Vue 一起使用。

Vuex 区别在于它是一个专门为 Vue 应用所设计。

这使得它能够更好地和 Vue 进行整合,同时提供简洁的 API 和改善过的开发体验。

简单状态管理起步使用

经常被忽略的是,Vue 应用中原始数据对象的实际来源 - 当访问数据对象时,一个 Vue 实例只是简单的代理访问。

所以,如果你有一处需要被多个实例间共享的状态,可以简单地通过维护一份数据来实现共享:

const sourceOfTruth = {}

const vmA = new Vue({
  data: sourceOfTruth
})

const vmB = new Vue({
  data: sourceOfTruth
})

现在当 sourceOfTruth 发生变化,vmA 和 vmB 都将自动的更新引用它们的视图。

子组件们的每个实例也会通过 this.$root.$data 去访问。

现在我们有了唯一的实际来源,但是,调试将会变为噩梦。

任何时间,我们应用中的任何部分,在任何数据改变后,都不会留下变更过的记录。

为了解决这个问题,我们采用一个简单的 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 之前发生了什么。

此外,每个实例/组件仍然可以拥有和管理自己的私有状态:

var vmA = new Vue({
  data: {
    privateState: {},
    sharedState: store.state
  }
})

var vmB = new Vue({
  data: {
    privateState: {},
    sharedState: store.state
  }
})

文档_Vue04_生产部署_单文件组件_单元测试_TypeScript支持_路由_状态管理_响应式原理_3大框架对比

重要的是,注意你不应该在 action 中 替换原始的状态对象 - 组件和 store 需要引用同一个共享对象,mutation 才能够被观察

接着我们继续延伸约定,组件不允许直接修改属于 store 实例的 state,

而应执行 action 来分发 (dispatch) 事件通知 store 去改变,我们最终达成了 Flux 架构。


这样约定的好处是,我们能够记录所有 store 中发生的 state 改变,

同时实现能做到记录变更 (mutation)、保存状态快照、历史回滚/时光旅行的先进的调试工具。


说了一圈其实又回到了vuex,如果你已经读到这儿,或许可以去尝试一下!




服务端渲染

SSR 完全指南

在 2.3 发布后我们发布了一份完整的构建 Vue 服务端渲染应用的指南。

这份指南非常深入,适合已经熟悉 Vue, webpack 和 Node.js 开发的开发者阅读。

请移步 ssr.vuejs.org

Nuxt.js

从头搭建一个服务端渲染的应用是相当复杂的。

幸运的是,我们有一个优秀的社区项目 Nuxt.js 让这一切变得非常简单。

Nuxt 是一个基于 Vue 生态的更高层的框架,为开发服务端渲染的 Vue 应用提供了极其便利的开发体验。

更酷的是,你甚至可以用它来做为静态站生成器。推荐尝试。




深入响应式原理

现在是时候深入一下了!Vue 最独特的特性之一,是其非侵入性的响应式系统。

数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。

这使得状态管理非常简单直接,不过理解其工作原理同样重要,这样你可以回避一些常见的问题。

在这个章节,我们将进入一些 Vue 响应式系统的底层的细节。

如何追踪变化

当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,

Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。


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

这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。


这里需要注意的问题是浏览器控制台在打印数据对象时 getter/setter 的格式化并不同,

所以你可能需要安装 vue-devtools 来获取更加友好的检查接口。


每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,

之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。

文档_Vue04_生产部署_单文件组件_单元测试_TypeScript支持_路由_状态管理_响应式原理_3大框架对比

检测变化的注意事项

受现代 JavaScript 的限制 (以及废弃 Object.observe),Vue 不能检测到对象属性的添加或删除

由于 Vue 会在初始化实例时对属性执行 getter/setter 转化过程,

所以属性必须在 data 对象上存在才能让 Vue 转换它,这样才能让它是响应的。

例如:

var vm = new Vue({
  data:{
  a:1
  }
})

// `vm.a` 是响应的

vm.b = 2
// `vm.b` 是非响应的

Vue 不允许在已经创建的实例上动态添加新的根级响应式属性 (root-level reactive property)。

然而它可以使用 Vue.set(object, key, value) 方法将响应属性添加到嵌套的对象上:

Vue.set(vm.someObject, 'b', 2)

您还可以使用 vm.$set 实例方法,这也是全局 Vue.set 方法的别名:

this.$set(this.someObject,'b',2)

有时你想向已有对象上添加一些属性,例如使用 Object.assign() 或 _.extend() 方法来添加属性。

但是,添加到对象上的新属性不会触发更新。

在这种情况下可以创建一个新的对象,让它包含原对象的属性和新的属性:

// 代替 `Object.assign(this.someObject, { a: 1, b: 2 })`
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })

也有一些数组相关的问题,之前已经在列表渲染中讲过。

声明响应式属性

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

var vm = new Vue({
  data: {
    // 声明 message 为一个空值字符串
    message: ''
  },
  template: '<div>{{ message }}</div>'
})
// 之后设置 `message`
vm.message = 'Hello!'

如果你未在 data 选项中声明 message,Vue 将警告你渲染函数正在试图访问的属性不存在。


这样的限制在背后是有其技术原因的,它消除了在依赖项跟踪系统中的一类边界情况,

也使 Vue 实例在类型检查系统的帮助下运行的更高效。


而且在代码可维护性方面也有一点重要的考虑:

data 对象就像组件状态的概要,提前声明所有的响应式属性,

可以让组件代码在以后重新阅读或其他开发人员阅读时更易于被理解。

异步更新队列

可能你还没有注意到,Vue 异步执行 DOM 更新。

只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。

如果同一个 watcher 被多次触发,只会被推入到队列中一次。

这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。

然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。


Vue 在内部尝试对异步队列使用原生的 Promise.then 和 MessageChannel

如果执行环境不支持,会采用 setTimeout(fn, 0) 代替。


例如,当你设置 vm.someData = 'new value' ,该组件不会立即重新渲染。

当刷新队列时,组件会在事件循环队列清空时的下一个“tick”更新。

多数情况我们不需要关心这个过程,但是如果你想在 DOM 状态更新后做点什么,这就可能会有些棘手。

虽然 Vue.js 通常鼓励开发人员沿着“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们确实要这么做。

为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback) 。

这样回调函数在 DOM 更新完成后就会调用。

例如:

<div id="example">{{message}}</div>
var vm = new Vue({
  el: '#example',
  data: {
    message: '123'
  }
})
vm.message = 'new message' // 更改数据
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
  vm.$el.textContent === 'new message' // true
})

在组件内使用 vm.$nextTick() 实例方法特别方便,

因为它不需要全局 Vue ,并且回调函数中的 this 将自动绑定到当前的 Vue 实例上:

Vue.component('example', {
  template: '<span>{{ message }}</span>',
  data: function () {
    return {
      message: '没有更新'
    }
  },
  methods: {
    updateMessage: function () {
      this.message = '更新完成'
      console.log(this.$el.textContent) // => '没有更新'
      this.$nextTick(function () {
        console.log(this.$el.textContent) // => '更新完成'
      })
    }
  }
})




对比其他框架

这个页面无疑是最难编写的,但我们认为它也是非常重要的。

或许你曾遇到了一些问题并且已经用其他的框架解决了。

你来这里的目的是看看 Vue 是否有更好的解决方案。

这也是我们在此想要回答的。


客观来说,作为核心团队成员,显然我们会更偏爱 Vue,认为对于某些问题来讲用 Vue 解决会更好。

如果没有这点信念,我们也就不会整天为此忙活了。

但是在此,我们想尽可能地公平和准确地来描述一切。

其他的框架也有显著的优点,例如 React 庞大的生态系统,或者像是 Knockout 对浏览器的支持覆盖到了 IE6。

我们会尝试着把这些内容全部列出来。


我们也希望得到的帮助,来使文档保持最新状态,因为 JavaScript 的世界进步的太快。

如果你注意到一个不准确或似乎不太正确的地方,请提交问题让我们知道。

React

React 和 Vue 有许多相似之处,它们都有:

  • 使用 Virtual DOM
  • 提供了响应式 (Reactive) 和组件化 (Composable) 的视图组件。
  • 将注意力集中保持在核心库,而将其他功能如路由和全局状态管理交给相关的库。

由于有着众多的相似处,我们会用更多的时间在这一块进行比较。

这里我们不只保证技术内容的准确性,同时也兼顾了平衡的考量。

我们需要承认 React 比 Vue 更好的地方,比如更丰富的生态系统。


React 社区为我们准确进行平衡的考量提供了非常积极的帮助,特别感谢来自 React 团队的 Dan Abramov 。

他非常慷慨的花费时间来贡献专业知识来帮助我们完善这篇文档。

运行时性能

React 和 Vue 都是非常快的,所以速度并不是在它们之中做选择的决定性因素。

对于具体的数据表现,可以移步这个第三方 benchmark,它专注于渲染/更新非常简单的组件树的真实性能。


优化

在 React 应用中,当某个组件的状态发生变化时,它会以该组件为根,重新渲染整个组件子树。

如要避免不必要的子组件的重渲染,

你需要在所有可能的地方使用 PureComponent,或是手动实现 shouldComponentUpdate 方法。

同时你可能会需要使用不可变的数据结构来使得你的组件更容易被优化。


然而,使用 PureComponent 和 shouldComponentUpdate 时,

需要保证该组件的整个子树的渲染输出都是由该组件的 props 所决定的。

如果不符合这个情况,那么此类优化就会导致难以察觉的渲染结果不一致。

这使得 React 中的组件优化伴随着相当的心智负担。


在 Vue 应用中,组件的依赖是在渲染过程中自动追踪的,所以系统能精确知晓哪个组件确实需要被重渲染。

你可以理解为每一个组件都已经自动获得了 shouldComponentUpdate,并且没有上述的子树问题限制。

Vue 的这个特点使得开发者不再需要考虑此类优化,从而能够更好地专注于应用本身。

HTML & CSS

在 React 中,一切都是 JavaScript。

不仅仅是 HTML 可以用 JSX 来表达,现在的潮流也越来越多地将 CSS 也纳入到 JavaScript 中来处理。

这类方案有其优点,但也存在一些不是每个开发者都能接受的取舍。

Vue 的整体思想是拥抱经典的 Web 技术,并在其上进行扩展。

我们下面会详细分析一下。


JSX vs Templates

在 React 中,所有的组件的渲染功能都依靠 JSX。

JSX 是使用 XML 语法编写 JavaScript 的一种语法糖。

使用 JSX 的渲染函数有下面这些优势:

  • 你可以使用完整的编程语言 JavaScript 功能来构建你的视图页面。比如你可以使用临时变量、JS 自带的流程控制、以及直接引用当前 JS 作用域中的值等等。

  • 开发工具对 JSX 的支持相比于现有可用的其他 Vue 模板还是比较先进的 (比如,linting、类型检查、编辑器的自动完成)。

事实上 Vue 也提供了渲染函数,甚至支持 JSX

然而,我们默认推荐的还是模板。

任何合乎规范的 HTML 都是合法的 Vue 模板,这也带来了一些特有的优势:

  • 对于很多习惯了 HTML 的开发者来说,模板比起 JSX 读写起来更自然。这里当然有主观偏好的成分,但如果这种区别会导致开发效率的提升,那么它就有客观的价值存在。

  • 基于 HTML 的模板使得将已有的应用逐步迁移到 Vue 更为容易。

  • 这也使得设计师和新人开发者更容易理解和参与到项目中。

  • 你甚至可以使用其他模板预处理器,比如 Pug 来书写 Vue 的模板。


有些开发者认为模板意味着需要学习额外的 DSL (Domain-Specific Language 领域特定语言) 才能进行开发

——我们认为这种区别是比较肤浅的。

首先,JSX 并不是免费的——它是基于 JS 之上的一套额外语法,因此也有它自己的学习成本。

同时,正如同熟悉 JS 的人学习 JSX 会很容易一样,熟悉 HTML 的人学习 Vue 的模板语法也是很容易的。

最后,DSL 的存在使得我们可以让开发者用更少的代码做更多的事,

比如 v-on 的各种修饰符,在 JSX 中实现对应的功能会需要多得多的代码。


更抽象一点来看,我们可以把组件区分为两类:

一类是偏视图表现的 (presentational),一类则是偏逻辑的 (logical)。

我们推荐在前者中使用模板,在后者中使用 JSX 或渲染函数。

这两类组件的比例会根据应用类型的不同有所变化,但整体来说我们发现表现类的组件远远多于逻辑类组件。


组件作用域内的 CSS

除非你把组件分布在多个文件上 (例如 CSS Modules),

CSS 作用域在 React 中是通过 CSS-in-JS 的方案实现的 (比如 styled-componentsglamorous 和 emotion)。

这引入了一个新的面向组件的样式范例,它和普通的 CSS 撰写过程是有区别的。

另外,虽然在构建时将 CSS 提取到一个单独的样式表是支持的,

但 bundle 里通常还是需要一个运行时程序来让这些样式生效。

当你能够利用 JavaScript 灵活处理样式的同时,也需要权衡 bundle 的尺寸和运行时的开销。


如果你是一个 CSS-in-JS 的爱好者,许多主流的 CSS-in-JS 库也都支持 Vue (比如 styled-components-vue 和 vue-emotion)。

这里 React 和 Vue 主要的区别是,Vue 设置样式的默认方法是单文件组件里类似 style 的标签。


单文件组件让你可以在同一个文件里完全控制 CSS,将其作为组件代码的一部分。

<style scoped>
  @media (min-width: 250px) {
    .list-container:hover {
      background: orange;
    }
  }
</style>

这个可选 scoped 属性会自动添加一个唯一的属性 (比如 data-v-21e5b78) 为组件内 CSS 指定作用域,

编译的时候 .list-container:hover 会被编译成类似 .list-container[data-v-21e5b78]:hover

最后,Vue 的单文件组件里的样式设置是非常灵活的。

通过 vue-loader,你可以使用任意预处理器、后处理器,甚至深度集成 CSS Modules——全部都在 <style> 标签内。

规模

向上扩展

Vue 和 React 都提供了强大的路由来应对大型应用。

React 社区在状态管理方面非常有创新精神 (比如 Flux、Redux),

而这些状态管理模式甚至 Redux 本身也可以非常容易的集成在 Vue 应用中。


实际上,Vue 更进一步地采用了这种模式 (Vuex),更加深入集成 Vue 的状态管理解决方案 Vuex 相信能为你带来更好的开发体验。

两者另一个重要差异是,Vue 的路由库和状态管理库都是由官方维护支持且与核心库同步更新的。

React 则是选择把这些问题交给社区维护,因此创建了一个更分散的生态系统。

但相对的,React 的生态系统相比 Vue 更加繁荣。


最后,Vue 提供了 Vue-cli 脚手架,能让你非常容易地构建项目,包含了 WebpackBrowserify,甚至 no build system

React 在这方面也提供了 create-react-app,但是现在还存在一些局限性:

  • 它不允许在项目生成时进行任何配置,而 Vue 支持 Yeoman-like 定制。
  • 它只提供一个构建单页面应用的单一模板,而 Vue 提供了各种用途的模板。
  • 它不能用用户自建的模板构建项目,而自建模板对企业环境下预先建立协议是特别有用的。

而要注意的是这些限制是故意设计的,这有它的优势。

例如,如果你的项目需求非常简单,你就不需要自定义生成过程。

你能把它作为一个依赖来更新。如果阅读更多关于不同的设计理念


向下扩展

React 学习曲线陡峭,在你开始学 React 前,你需要知道 JSX 和 ES2015,因为许多示例用的是这些语法。

你需要学习构建系统,虽然你在技术上可以用 Babel 来实时编译代码,但是这并不推荐用于生产环境。


就像 Vue 向上扩展好比 React 一样,Vue 向下扩展后就类似于 jQuery。

你只要把如下标签放到页面就可以运行:(牛,赞)

<script src="https://cdn.jsdelivr.net/npm/vue"></script>

然后你就可以编写 Vue 代码并应用到生产中,你只要用 min 版 Vue 文件替换掉就不用担心其他的性能问题。

由于起步阶段不需学 JSX,ES2015 以及构建系统,所以开发者只需不到一天的时间阅读指南就可以建立简单的应用程序。

原生渲染

React Native 能使你用相同的组件模型编写有本地渲染能力的 APP (iOS 和 Android)。

能同时跨多平台开发,对开发者是非常棒的。


相应地,Vue 和 Weex 会进行官方合作,Weex 是阿里巴巴发起的跨平台用户界面开发框架,

同时也正在 Apache 基金会进行项目孵化,Weex 允许你使用 Vue 语法开发不仅仅可以运行在浏览器端,

还能被用于开发 iOS 和 Android 上的原生应用的组件。


现在,Weex 还在积极发展,成熟度也不能和 React Native 相抗衡。

但是,Weex 的发展是由世界上最大的电子商务企业的需求在驱动,

Vue 团队也会和 Weex 团队积极合作确保为开发者带来良好的开发体验。


另一个 Vue 的开发者们很快就会拥有的选项是 NativeScript,这是一个社区驱动的插件

MobX

Mobx 在 React 社区很流行,实际上在 Vue 也采用了几乎相同的反应系统。

在有限程度上,React + Mobx 也可以被认为是更繁琐的 Vue,

所以如果你习惯组合使用它们,那么选择 Vue 会更合理。

AngularJS (Angular 1)

Vue 的一些语法和 AngularJS 的很相似 (例如 v-if vs ng-if)。

因为 AngularJS 是 Vue 早期开发的灵感来源。

然而,AngularJS 中存在的许多问题,在 Vue 中已经得到解决。

复杂性

在 API 与设计两方面上 Vue.js 都比 AngularJS 简单得多,因此你可以快速地掌握它的全部特性并投入开发。

灵活性和模块化

Vue.js 是一个更加灵活开放的解决方案。
它允许你以希望的方式组织应用程序,而不是在任何时候都必须遵循 AngularJS 制定的规则,这让 Vue 能适用于各种项目。
我们知道把决定权交给你是非常必要的。
这也就是为什么我们提供 webpack template,让你可以用几分钟,去选择是否启用高级特性,
比如热模块加载、linting、CSS 提取等等。

数据绑定

AngularJS 使用双向绑定,Vue 在不同组件间强制使用单向数据流。

这使应用中的数据流更加清晰易懂。

指令与组件

在 Vue 中指令和组件分得更清晰。

指令只封装 DOM 操作,而组件代表一个自给自足的独立单元——有自己的视图和数据逻辑。

在 AngularJS 中,每件事都由指令来做,而组件只是一种特殊的指令。

运行时性能

Vue 有更好的性能,并且非常非常容易优化,因为它不使用脏检查。

在 AngularJS 中,当 watcher 越来越多时会变得越来越慢,因为作用域内的每一次变化,所有 watcher 都要重新计算。

并且,如果一些 watcher 触发另一个更新,脏检查循环 (digest cycle) 可能要运行多次。

AngularJS 用户常常要使用深奥的技术,以解决脏检查循环的问题。

有时没有简单的办法来优化有大量 watcher 的作用域。


Vue 则根本没有这个问题,因为它使用基于依赖追踪的观察系统并且异步队列更新,所有的数据变化都是独立触发,

除非它们之间有明确的依赖关系。


有意思的是,Angular 和 Vue 用相似的设计解决了一些 AngularJS 中存在的问题。

Angular (原本的 Angular 2)

我们将新的 Angular 独立开来讨论,因为它是一个和 AngularJS 完全不同的框架。

例如:它具有优秀的组件系统,并且许多实现已经完全重写,API 也完全改变了。

TypeScript

Angular 事实上必须用 TypeScript 来开发,因为它的文档和学习资源几乎全部是面向 TS 的。

TS 有很多好处——静态类型检查在大规模的应用中非常有用,

同时对于 Java 和 C# 背景的开发者也是非常提升开发效率的。


然而,并不是所有人都想用 TS——在中小型规模的项目中,引入 TS 可能并不会带来太多明显的优势。

在这些情况下,用 Vue 会是更好的选择,因为在不用 TS 的情况下使用 Angular 会很有挑战性。


最后,虽然 Vue 和 TS 的整合可能不如 Angular 那么深入,

我们也提供了官方的 类型声明 和 组件装饰器,并且知道有大量用户在生产环境中使用 Vue + TS 的组合。

我们也和微软的 TS / VSCode 团队进行着积极的合作,目标是为 Vue + TS 用户提供更好的类型检查和 IDE 开发体验。

运行时性能

这两个框架都很快,有非常类似的 benchmark 数据。

你可以浏览具体的数据做更细粒度的对比,不过速度应该不是决定性的因素。

体积

在体积方面,最近的 Angular 版本中在使用了 AOT 和 tree-shaking 技术后使得最终的代码体积减小了许多。

但即使如此,与使用了这些优化的 angular-cli 生成的默认项目尺寸 (~130kB) 相比,

一个包含了 Vuex + Vue Router 的 Vue 项目 (gzip 之后 30kB) 还是要小得多。

灵活性

Vue 相比于 Angular 更加灵活,Vue 官方提供了构建工具来协助你构建项目,但它并不限制你去如何组织你的应用代码。

有人可能喜欢有严格的代码组织规范,但也有开发者喜欢更灵活*的方式。

学习曲线

要学习 Vue,你只需要有良好的 HTML 和 JavaScript 基础。

有了这些基本的技能,你就可以非常快速地通过阅读 指南 投入开发。


Angular 的学习曲线是非常陡峭的——作为一个框架,它的 API 面积比起 Vue 要大得多,

你也因此需要理解更多的概念才能开始有效率地工作。

当然,Angular 本身的复杂度是因为它的设计目标就是只针对大型的复杂应用;

但不可否认的是,这也使得它对于经验不甚丰富的开发者相当的不友好。

Ember

Ember 是一个全能框架。它提供了大量的约定,一旦你熟悉了它们,开发会变得很高效。

不过,这也意味着学习曲线较高,而且并不灵活。

这意味着在框架和库 (加上一系列松散耦合的工具) 之间做权衡选择。

后者会更*,但是也要求你做更多架构上的决定。


也就是说,我们最好比较的是 Vue 内核和 Ember 的模板数据模型层:

  • Vue 在普通 JavaScript 对象上建立响应,提供自动化的计算属性。在 Ember 中需要将所有东西放在 Ember 对象内,并且手工为计算属性声明依赖。

  • Vue 的模板语法可以用全功能的 JavaScript 表达式,而 Handlebars 的语法和帮助函数相比来说非常受限。

  • 在性能上,Vue 比 Ember 好很多,即使是 Ember 2.x 的最新 Glimmer 引擎。Vue 能够自动批量更新,而 Ember 在性能敏感的场景时需要手动管理。

Knockout

Knockout 是 MVVM 领域内的先驱,并且追踪依赖。它的响应系统和 Vue 也很相似。

它在浏览器支持以及其他方面的表现也是让人印象深刻的。它最低能支持到 IE6,而 Vue 最低只能支持到 IE9。


随着时间的推移,Knockout 的发展已有所放缓,并且略显有点老旧了。

比如,它的组件系统缺少完备的生命周期事件方法,尽管这些在现在是非常常见的。

以及相比于 Vue 调用子组件的接口它的方法显得有点笨重。


如果你有兴趣研究,你还会发现二者在接口设计的理念上是不同的。

这可以通过各自创建的 simple Todo List 体现出来。

或许有点主观,但是很多人认为 Vue 的 API 接口更简单结构更优雅。

Polymer

Polymer 是另一个由谷歌赞助的项目,事实上也是 Vue 的一个灵感来源。

Vue 的组件可以粗略的类比于 Polymer 的自定义元素,并且两者具有相似的开发风格。


最大的不同之处在于,Polymer 是基于最新版的 Web Components 标准之上,

并且需要重量级的 polyfills 来帮助工作 (性能下降),浏览器本身并不支持这些功能。


相比而言,Vue 在支持到 IE9 的情况下并不需要依赖 polyfills 来工作。

在 Polymer 1.0 版本中,为了弥补性能,团队非常有限的使用数据绑定系统。

例如,在 Polymer 中唯一支持的表达式只有布尔值否定和单一的方法调用,它的 computed 方法的实现也并不是很灵活。

Polymer 自定义的元素是用 HTML 文件来创建的,这会限制使用 JavaScript/CSS (和被现代浏览器普遍支持的语言特性)。

相比之下,Vue 的单文件组件允许你非常容易的使用 ES2015 和你想用的 CSS 预编译处理器。


在部署生产环境时,Polymer 建议使用 HTML Imports 加载所有资源。

而这要求服务器和客户端都支持 Http 2.0 协议,并且浏览器实现了此标准。

这是否可行就取决于你的目标用户和部署环境了。

如果状况不佳,你必须用 Vulcanizer 工具来打包 Polymer 元素。


而在这方面,Vue 可以结合异步组件的特性和 Webpack 的代码分割特性来实现懒加载 (lazy-loaded)。

这同时确保了对旧浏览器的兼容且又能更快加载。


而 Vue 和 Web Component 标准进行深层次的整合也是完全可行的,

比如使用 Custom Elements、Shadow DOM 的样式封装。

然而在我们做出严肃的实现承诺之前,我们目前仍在等待相关标准成熟,进而再广泛应用于主流的浏览器中。

Riot

Riot 3.0 提供了一个类似于基于组件的开发模型 (在 Riot 中称之为 Tag),它提供了小巧精美的 API。

Riot 和 Vue 在设计理念上可能有许多相似处。


尽管相比 Riot ,Vue 要显得重一点,Vue 还是有很多显著优势的:

  • 更好的性能。Riot 使用了 遍历 DOM 树 而不是虚拟 DOM,但实际上用的还是脏检查机制,因此和 AngularJS 患有相同的性能问题。
  • 更多成熟工具的支持。Vue 提供官方支持 webpack 和 Browserify,而 Riot 是依靠社区来建立集成系统。


















未完待续,下一章节,つづく