Vue相关参考教程(alpha)

源码解析(v2.5.17-beta.0)

准备工作

认识 Flow

Flowfacebook出品的JavaScript静态类型检查工具,它与Typescript不同的是,它可以部分引入,不需要完全重构整个项目,所以对于一个已有一定规模的项目来说,迁移成本更小,也更加可行。除此之外,Flow可以提供实时增量的反馈,通过运行Flow server不需要在每次更改项目的时候完全从头运行类型检查,提高运行效率。可以简单总结为:对于新项目,可以考虑使用TypeScript或者Flow,对于已有一定规模的项目则建议使用Flow进行较小成本的逐步迁移来引入类型检查。Vue.js的源码利用了Flow做了静态类型检查,所以了解Flow有助于我们阅读源码。

为什么用 Flow

JavaScript是动态类型语言,它的灵活性有目共睹,但是过于灵活的副作用是很容易就写出非常隐蔽的隐患代码,在编译期甚至运行时看上去都不会报错,但是可能会发生各种各样奇怪的和难以解决的bug。

类型检查是当前动态类型语言的发展趋势,所谓类型检查,就是在编译期尽早发现(由类型错误引起的)bug,又不影响代码运行(不需要运行时动态检查类型),使编写JavaScript具有和编写Java等强类型语言相近的体验。

项目越复杂就越需要通过工具的手段来保证项目的维护性和增强代码的可读性。Vue.js在做2.0重构的时候,在ES2015的基础上,除了ESLint保证代码风格之外,也引入了Flow做静态类型检查。之所以选择Flow,主要是因为BabelESLint都有对应的Flow插件以支持语法,可以完全沿用现有的构建配置,非常小成本的改动就可以拥有静态类型检查的能力。

Flow常用方法

通常类型检查分成 2 种方式:

  • 类型推断:通过变量的使用上下文来推断出变量类型,然后根据这些推断来检查类型。
  • 类型注释:事先注释好我们期待的类型,Flow 会基于这些注释来判断。

类型判断

它不需要任何代码修改即可进行类型检查,最小化开发者的工作量。它不会强制你改变开发习惯,因为它会自动推断出变量的类型。这就是所谓的类型推断,Flow 最重要的特性之一。

通过一个简单例子说明一下:

/*@flow*/``

function split(str) {
  return str.split(' ')
}

split(11)

Flow 检查上述代码后会报错,因为函数split期待的参数是字符串,而我们输入了数字。

类型注释

如上所述,类型推断是Flow最有用的特性之一,不需要编写类型注释就能获取有用的反馈。但在某些特定的场景下,添加类型注释可以提供更好更明确的检查依据。

考虑如下代码:

/*@flow*/

function add(x, y){
  return x + y
}

add('Hello', 11)

Flow检查上述代码时检查不出任何错误,因为从语法层面考虑,+既可以用在字符串上,也可以用在数字上,我们并没有明确指出add()的参数必须为数字。在这种情况下,我们可以借助类型注释来指明期望的类型。类型注释是以冒号:开头,可以在函数参数,返回值,变量声明中使用。如果我们在上段代码中添加类型注释,就会变成如下:

/*@flow*/

function add(x: number, y: number): number {
  return x + y
}

add('Hello', 11)

现在Flow就能检查出错误,因为函数参数的期待类型为数字,而我们提供了字符串。上面的例子是针对函数的类型注释。接下来我们来看看Flow能支持的一些常见的类型注释:

  1. 数组
/*@flow*/

var arr: Array<number> = [1, 2, 3]

arr.push('Hello')

数组类型注释的格式是Array<T>T表示数组中每项的数据类型。在上述代码中,arr是每项均为数字的数组。如果我们给这个数组添加了一个字符串,Flow能检查出错误。

  1. 类和对象
/*@flow*/

class Bar {
  x: string;           // x 是字符串
  y: string | number;  // y 可以是字符串或者数字
  z: boolean;

  constructor(x: string, y: string | number) {
    this.x = x
    this.y = y
    this.z = false
  }
}

var bar: Bar = new Bar('hello', 4)

var obj: { a: string, b: number, c: Array<string>, d: Bar } = {
  a: 'hello',
  b: 11,
  c: ['hello', 'world'],
  d: new Bar('hello', 3)
}

类的类型注释格式如上,可以对类自身的属性做类型检查,也可以对构造函数的参数做类型检查。这里需要注意的是,属性y的类型中间用|做间隔,表示y的类型即可以是字符串也可以是数字。

对象的注释类型类似于类,需要指定对象属性的类型。

  1. Null

若想任意类型T可以为null或者undefined,只需类似如下写成?T的格式即可。

/*@flow*/

var foo: ?string = null

此时,foo可以为字符串,也可以为null

Flow 在 Vue.js 源码中的应用

有时候我们想引用第三方库,或者自定义一些类型,但Flow并不认识,因此检查的时候会报错。为了解决这类问题,Flow提出了一个libdef的概念,可以用来识别这些第三方库或者是自定义类型,而Vue.js也利用了这一特性。

Vue.js的主目录下有.flowconfig文件, 它是Flow的配置文件。这其中的[libs]部分用来描述包含指定库定义的目录,默认是名为flow-typed的目录。

这里[libs]配置的是flow,表示指定的库定义都在flow文件夹内。我们打开这个目录,会发现文件如下:

flow
├── compiler.js        # 编译相关
├── component.js       # 组件数据结构
├── global-api.js      # Global API 结构
├── modules.js         # 第三方库定义
├── options.js         # 选项相关
├── ssr.js             # 服务端渲染相关
├── vnode.js           # 虚拟 node 相关

可以看到,Vue.js有很多自定义类型的定义,在阅读源码的时候,如果遇到某个类型并想了解它完整的数据结构的时候,可以回来翻阅这些数据结构的定义。

小结

通过对Flow的认识,有助于我们阅读Vue的源码,并且这种静态类型检查的方式非常有利于大型项目源码的开发和维护。

源码目录结构

Vue.js的源码都在src目录下,其目录结构如下:

src
├── compiler        # 编译相关 
├── core            # 核心代码 
├── platforms       # 不同平台的支持
├── server          # 服务端渲染
├── sfc             # .vue 文件解析
├── shared          # 共享代码

compiler

compiler目录包含Vue.js所有编译相关的代码。它包括把模板解析成ast语法树ast语法树优化代码生成等功能。

编译的工作可以在构建时做(借助 webpack、vue-loader 等辅助插件);也可以在运行时做,使用包含构建功能的 Vue.js。显然,编译是一项耗性能的工作,所以更推荐前者——构建时编译。

core

core目录包含了Vue.js的核心代码,包括内置组件全局API封装Vue实例化观察者虚拟DOM工具函数等等。

platform

Vue.js是一个跨平台的MVVM框架,它可以跑在web上,也可以配合weex跑在natvie客户端上。platformVue.js的入口,2个目录代表2个主要入口,分别打包成运行在web上和weex上的Vue.js。我这里只会分析web入口打包后的Vue.js

server

Vue.js 2.0支持了服务端渲染,所有服务端渲染相关的逻辑都在这个目录下。注意:这部分代码是跑在服务端的Node.js,不要和跑在浏览器端的Vue.js混为一谈。

服务端渲染主要的工作是把组件渲染为服务器端的HTML字符串,将它们直接发送到浏览器,最后将静态标记"混合"为客户端上完全交互的应用程序。

sfc

通常我们开发Vue.js都会借助webpack构建, 然后通过.vue单文件来编写组件。这个目录下的代码逻辑会把.vue文件内容解析成一个JavaScript的对象。

shared

Vue.js会定义一些工具方法,这里定义的工具方法都是会被浏览器端的Vue.js和服务端的Vue.js所共享的。

小结

Vue.js的目录设计可以看到,作者把功能模块拆分的非常清楚,相关的逻辑放在一个独立的目录下维护,并且把复用的代码也抽成一个独立目录。这样的目录设计让代码的阅读性和可维护性都变强,是非常值得学习和推敲的。

源码构建

Vue.js 源码是基于Rollup构建的,它的构建相关配置都在scripts目录下。Rollup是一款用来es6模块打包代码的构建工具(支持css和js打包)。当我们使用ES6模块编写应用或者库时,它可以打包成一个单独文件提供浏览器和Node.js来使用

构建脚本

通常一个基于NPM托管的项目都会有一个package.json文件,它是对项目的描述文件,它的内容实际上是一个标准的JSON对象。 我们通常会配置script字段作为NPM的执行脚本,Vue.js源码构建的脚本如下:

{
  "script": {
    "build": "node scripts/build.js",
    "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
    "build:weex": "npm run build --weex"
  }
}

这里总共有3条命令,作用都是构建Vue.js,后面2条是在第一条命令的基础上,添加一些环境参数。当在命令行运行npm run build的时候,实际上就会执行node scripts/build.js,接下来我们来看看它实际是怎么构建的。

构建过程

我们对于构建过程分析是基于源码的,先打开构建的入口 JS 文件,在scripts/build.js中:

let builds = require('./config').getAllBuilds()

// filter builds via command line arg
if (process.argv[2]) {
  const filters = process.argv[2].split(',')
  builds = builds.filter(b => {
    return filters.some(f => b.output.file.indexOf(f) > -1 || b._name.indexOf(f) > -1)
  })
} else {
  // filter out weex builds by default
  builds = builds.filter(b => {
    return b.output.file.indexOf('weex') === -1
  })
}

build(builds)

这段代码逻辑非常简单,先从配置文件读取配置,再通过命令行参数对构建配置做过滤,这样就可以构建出不同用途的Vue.js了。接下来我们看一下配置文件,在scripts/config.js中:

const builds = {
  // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
  'web-runtime-cjs': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.common.js'),
    format: 'cjs',
    banner
  },
  // Runtime+compiler CommonJS build (CommonJS)
  'web-full-cjs': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.common.js'),
    format: 'cjs',
    alias: { he: './entity-decoder' },
    banner
  },
  // Runtime only (ES Modules). Used by bundlers that support ES Modules,
  // e.g. Rollup & Webpack 2
  'web-runtime-esm': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.esm.js'),
    format: 'es',
    banner
  },
  // Runtime+compiler CommonJS build (ES Modules)
  'web-full-esm': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.esm.js'),
    format: 'es',
    alias: { he: './entity-decoder' },
    banner
  },
  // runtime-only build (Browser)
  'web-runtime-dev': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.js'),
    format: 'umd',
    env: 'development',
    banner
  },
  // runtime-only production build (Browser)
  'web-runtime-prod': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.min.js'),
    format: 'umd',
    env: 'production',
    banner
  },
  // Runtime+compiler development build (Browser)
  'web-full-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.js'),
    format: 'umd',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },
  // Runtime+compiler production build  (Browser)
  'web-full-prod': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.min.js'),
    format: 'umd',
    env: 'production',
    alias: { he: './entity-decoder' },
    banner
  },
  // ...
}

这里列举了一些Vue.js构建的配置,关于还有一些服务端渲染webpack插件以及weex的打包配置就不列举了。

对于单个配置,它是遵循Rollup的构建规则的。其中entry属性表示构建的入口JS文件地址,dest属性表示构建后的JS文件地址。format属性表示构建的格式,cjs表示构建出来的文件遵循CommonJS规范,es表示构建出来的文件遵循ES Module规范。 umd表示构建出来的文件遵循UMD规范。

web-runtime-cjs配置为例,它的entryresolve('web/entry-runtime.js'),先来看一下resolve函数的定义:

源码目录:scripts/config.js

const aliases = require('./alias')
const resolve = p => {
  const base = p.split('/')[0]
  if (aliases[base]) {
    return path.resolve(aliases[base], p.slice(base.length + 1))
  } else {
    return path.resolve(__dirname, '../', p)
  }
}

这里的resolve函数实现非常简单,它先把resolve函数传入的参数p通过/做了分割成数组,然后取数组第一个元素设置为base。在我们这个例子中,参数pweb/entry-runtime.js,那么base则为webbase并不是实际的路径,它的真实路径借助了别名的配置,我们来看一下别名配置的代码,在scripts/alias中:

const path = require('path')

module.exports = {
  vue: path.resolve(__dirname, '../src/platforms/web/entry-runtime-with-compiler'),
  compiler: path.resolve(__dirname, '../src/compiler'),
  core: path.resolve(__dirname, '../src/core'),
  shared: path.resolve(__dirname, '../src/shared'),
  web: path.resolve(__dirname, '../src/platforms/web'),
  weex: path.resolve(__dirname, '../src/platforms/weex'),
  server: path.resolve(__dirname, '../src/server'),
  entries: path.resolve(__dirname, '../src/entries'),
  sfc: path.resolve(__dirname, '../src/sfc')
}

很显然,这里web对应的真实的路径是path.resolve(__dirname, '../src/platforms/web'),这个路径就找到了Vue.js源码的web目录。然后resolve函数通过path.resolve(aliases[base], p.slice(base.length + 1))找到了最终路径,它就是Vue.js源码web目录下的entry-runtime.js。因此,web-runtime-cjs配置对应的入口文件就找到了。

它经过Rollup的构建打包后,最终会在dist目录下生成vue.runtime.common.js

Runtime Only VS Runtime+Compiler

通常我们利用vue-cli去初始化我们的Vue.js项目的时候会询问我们用Runtime Only版本的还是Runtime+Compiler版本。下面我们来对比这两个版本。

  • Runtime Only

我们在使用Runtime Only版本的Vue.js的时候,通常需要借助如webpackvue-loader工具把.vue文件编译成JavaScript,因为是在编译阶段做的,所以它只包含运行时的Vue.js代码,因此代码体积也会更轻量。

  • Runtime + Compiler

我们如果没有对代码做预编译,但又使用了Vuetemplate属性并传入一个字符串,则需要在客户端编译模板,如下所示:

// 需要编译器的版本
new Vue({
  template: '<div>{{ hi }}</div>'
})

// 这种情况不需要
new Vue({
  render (h) {
    return h('div', this.hi)
  }
})

因为在Vue.js 2.0中,最终渲染都是通过render函数,如果写template属性,则需要编译成render函数,那么这个编译过程会发生运行时,所以需要带有编译器的版本。很显然,这个编译过程对性能会有一定损耗,所以通常我们更推荐使用Runtime-OnlyVue.js

小结

通过这一节的分析,我们可以了解到Vue.js的构建打包过程,也知道了不同作用和功能的Vue.js它们对应的入口以及最终编译生成的JS文件。

从入口开始

我们之前提到过Vue.js构建过程,在web应用下,尽管在实际开发过程中我们会用Runtime Only版本开发比较多,但为了分析Vue的编译过程,我们还是重点分析Runtime + Compiler 的 Vue.js的源码:

它的入口是src/platforms/web/entry-runtime-with-compiler.js

/* @flow */

import config from 'core/config'
import { warn, cached } from 'core/util/index'
import { mark, measure } from 'core/util/perf'

import Vue from './runtime/index'
import { query } from './util/index'
import { compileToFunctions } from './compiler/index'
import { shouldDecodeNewlines, shouldDecodeNewlinesForHref } from './util/compat'

const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)
}

/**
 * Get outerHTML of elements, taking care
 * of SVG elements in IE as well.
 */
function getOuterHTML (el: Element): string {
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}

Vue.compile = compileToFunctions

export default Vue

那么,当我们的代码执行import Vue from 'vue'的时候,就是从这个入口执行代码来初始化Vue

Vue 的入口

在这个入口JS的上方我们可以找到Vue的来源:import Vue from './runtime/index',我们先来看一下这块儿的实现,它定义在src/platforms/web/runtime/index.js中:

/* @flow */

import Vue from 'core/index'
import config from 'core/config'
import { extend, noop } from 'shared/util'
import { mountComponent } from 'core/instance/lifecycle'
import { devtools, inBrowser, isChrome } from 'core/util/index'

import {
  query,
  mustUseProp,
  isReservedTag,
  isReservedAttr,
  getTagNamespace,
  isUnknownElement
} from 'web/util/index'

import { patch } from './patch'
import platformDirectives from './directives/index'
import platformComponents from './components/index'

// install platform specific utils
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement

// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop

// public mount method
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

// devtools global hook
/* istanbul ignore next */
if (inBrowser) {
  setTimeout(() => {
    if (config.devtools) {
      if (devtools) {
        devtools.emit('init', Vue)
      } else if (
        process.env.NODE_ENV !== 'production' &&
        process.env.NODE_ENV !== 'test' &&
        isChrome
      ) {
        console[console.info ? 'info' : 'log'](
          'Download the Vue Devtools extension for a better development experience:\n' +
          'https://github.com/vuejs/vue-devtools'
        )
      }
    }
    if (process.env.NODE_ENV !== 'production' &&
      process.env.NODE_ENV !== 'test' &&
      config.productionTip !== false &&
      typeof console !== 'undefined'
    ) {
      console[console.info ? 'info' : 'log'](
        `You are running Vue in development mode.\n` +
        `Make sure to turn on production mode when deploying for production.\n` +
        `See more tips at https://vuejs.org/guide/deployment.html`
      )
    }
  }, 0)
}

export default Vue

这里关键的代码是import Vue from 'core/index',之后的逻辑都是对Vue这个对象做一些扩展,可以先不用看,我们来看一下真正初始化Vue的地方,在src/core/index.js中:

import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'

initGlobalAPI(Vue)

Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
})

Object.defineProperty(Vue.prototype, '$ssrContext', {
  get () {
    /* istanbul ignore next */
    return this.$vnode && this.$vnode.ssrContext
  }
})

// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
  value: FunctionalRenderContext
})

Vue.version = '__VERSION__'

export default Vue

这里有2处关键的代码,import Vue from './instance/index'initGlobalAPI(Vue),初始化全局Vue API(我们稍后介绍),我们先来看第一部分,在src/core/instance/index.js中:

  • Vue 的定义
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

在这里,我们终于看到了Vue的庐山真面目了,它实际上就是一个用Function实现的类,我们只能通过new Vue去实例化它。

这里有一点需要特别说明下,这里为何Vue不用ES6Class去实现呢?我们往后看这里有很多xxxMixin的函数调用,并把Vue当参数传入,它们的功能都是给Vueprototype上扩展一些方法(这里具体的细节会在之后的文章介绍,这里不展开),Vue按功能把这些扩展分散到多个模块中去实现,而不是在一个模块里实现所有,这种方式是用Class难以实现的。这么做的好处是非常方便代码的维护和管理,这种编程技巧也非常值得我们去学习。

  • initGlobalAPI

Vue.js在整个初始化过程中,除了给它的原型prototype上扩展方法,还会给Vue这个对象本身扩展全局的静态方法,它的定义在src/core/global-api/index.js中:

export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  const configDef = {}
  configDef.get = () => config
  if (process.env.NODE_ENV !== 'production') {
    configDef.set = () => {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
  Object.defineProperty(Vue, 'config', configDef)

  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }

  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue

  extend(Vue.options.components, builtInComponents)

  initUse(Vue)
  initMixin(Vue)
  initExtend(Vue)
  initAssetRegisters(Vue)
}

小结

这里就是在Vue上扩展的一些全局方法的定义,Vue官网中关于全局API都可以在这里找到,这里不会介绍细节,会在之后的章节我们具体介绍到某个API的时候会详细介绍。有一点要注意的是,Vue.util暴露的方法最好不要依赖,因为它可能经常会发生变化,是不稳定的。

数据驱动

Vue.js一个核心思想是数据驱动。所谓数据驱动,是指视图是由数据驱动生成的,我们对视图的修改,不会直接操作DOM,而是通过修改数据。它相比我们传统的前端开发,如使用jQuery等前端库直接修改DOM,大大简化了代码量。特别是当交互复杂的时候,只关心数据的修改会让代码的逻辑变的非常清晰,因为DOM变成了数据的映射,我们所有的逻辑都是对数据的修改,而不用碰触DOM,这样的代码非常利于维护。

Vue.js中我们可以采用简洁的模板语法来声明式的将数据渲染为DOM

<div id="app">
  {{ message }}
</div>
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

最终它会在页面上渲染出Hello Vue。接下来,我们会从源码角度来分析Vue是如何实现的,分析过程会以主线代码为主,重要的分支逻辑会放在之后单独分析。数据驱动还有一部分是数据更新驱动视图变化,这一块内容我们也会在之后的章节分析,这一章我们的目标是弄清楚模板和数据如何渲染成最终的 DOM。

new Vue 发生了什么

从入口代码开始分析,我们先来分析new Vue背后发生了哪些事情。我们都知道,new关键字在Javascript语言中代表实例化是一个对象,而Vue实际上是一个类,类在Javascript中是用Function来实现的,来看一下源码,在src/core/instance/index.js中:

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

可以看到Vue只能通过new关键字初始化,然后会调用this._init方法, 该方法在src/core/instance/init.js中定义:

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }

Vue初始化主要就干了几件事情:合并配置初始化生命周期初始化事件中心初始化渲染初始化 datapropscomputedwatcher 等等。

小结

Vue的初始化逻辑写的非常清楚,把不同的功能逻辑拆成一些单独的函数执行,让主线逻辑一目了然,这样的编程思想是非常值得借鉴和学习的。在初始化的最后,检测到如果有el属性,则调用vm.$mount方法挂载vm,挂载的目标就是把模板渲染成最终的DOM

实例挂载的实现

Vue中我们是通过$mount实例方法去挂载vm的,$mount方法在多个文件中都有定义,如src/platform/web/entry-runtime-with-compiler.jssrc/platform/web/runtime/index.jssrc/platform/weex/runtime/index.js。因为$mount这个方法的实现是和平台、构建方式都相关的。接下来我们重点分析带compiler版本的$mount实现,因为抛开webpackvue-loader,我们在纯前端浏览器环境分析Vue的工作原理,有助于我们对原理理解的深入。

compiler版本的$mount实现非常有意思,先来看一下src/platform/web/entry-runtime-with-compiler.js文件中定义:

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)
}

/**
 * Get outerHTML of elements, taking care
 * of SVG elements in IE as well.
 */
function getOuterHTML (el: Element): string {
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}

Vue.compile = compileToFunctions

export default Vue

这段代码首先缓存了原型上的$mount方法,再重新定义该方法,我们先来分析这段代码。首先,它对el做了限制,Vue不能挂载在bodyhtml这样的根节点上。接下来的是很关键的逻辑 —— 如果没有定义render方法,则会把el或者template字符串转换成render方法。这里我们要牢记,在Vue 2.0版本中,所有Vue的组件的渲染最终都需要render方法,无论我们是用单文件.vue方式开发组件,还是写了el或者template属性,最终都会转换成render方法,那么这个过程是Vue的一个“在线编译”的过程,它是调用compileToFunctions方法实现的,编译过程我们之后会介绍。最后,调用原先原型上的$mount方法挂载。

原先原型上的$mount方法在src/platform/web/runtime/index.js中定义,之所以这么设计完全是为了复用,因为它是可以被runtime only版本的Vue直接使用的。

// public mount method
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

$mount方法支持传入2个参数,第一个是el,它表示挂载的元素,可以是字符串,也可以是DOM对象,如果是字符串在浏览器环境下会调用query方法转换成DOM对象的。第二个参数是和服务端渲染相关,在浏览器环境下我们不需要传第二个参数。$mount方法实际上会去调用mountComponent方法,这个方法定义在src/core/instance/lifecycle.js文件中:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

从上面的代码可以看到,mountComponent核心就是先调用vm._render方法先生成虚拟Node,再实例化一个渲染Watcher,在它的回调函数中会调用updateComponent方法,最终调用vm._update更新DOMWatcher在这里起到两个作用,一个是初始化的时候会执行回调函数,另一个是当vm实例中的监测的数据发生变化的时候执行回调函数,这块儿我们会在之后的章节中介绍。函数最后判断为根节点的时候设置vm._isMountedtrue,表示这个实例已经挂载了,同时执行mounted钩子函数。这里注意 vm.$vnode表示Vue实例的父虚拟Node,所以它为Null则表示当前是根Vue的实例。

小结

mountComponent方法的逻辑也是非常清晰的,它会完成整个渲染工作,接下来我们要重点分析其中的细节,也就是最核心的2个方法:vm._rendervm._update

render

Vue_render方法是实例的一个私有方法,它用来把实例渲染成一个虚拟Node。它的定义在src/core/instance/render.js文件中:

Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options

    // reset _rendered flag on slots for duplicate slot check
    if (process.env.NODE_ENV !== 'production') {
      for (const key in vm.$slots) {
        // $flow-disable-line
        vm.$slots[key]._rendered = false
      }
    }

    if (_parentVnode) {
      vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject
    }

    // set parent vnode. this allows render functions to have access
    // to the data on the placeholder node.
    vm.$vnode = _parentVnode
    // render self
    let vnode
    try {
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`)
      // return error render result,
      // or previous vnode to prevent render error causing blank component
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production') {
        if (vm.$options.renderError) {
          try {
            vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
          } catch (e) {
            handleError(e, vm, `renderError`)
            vnode = vm._vnode
          }
        } else {
          vnode = vm._vnode
        }
      } else {
        vnode = vm._vnode
      }
    }
    // return empty vnode in case the render function errored out
    if (!(vnode instanceof VNode)) {
      if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
        warn(
          'Multiple root nodes returned from render function. Render function ' +
          'should return a single root node.',
          vm
        )
      }
      vnode = createEmptyVNode()
    }
    // set parent
    vnode.parent = _parentVnode
    return vnode
  }

这段代码最关键的是render方法的调用,我们在平时的开发工作中手写render方法的场景比较少,而写的比较多的是template模板,在之前的mounted方法的实现中,会把template编译成render方法,但这个编译过程是非常复杂的。

编译

之前我们分析过模板到真实DOM渲染的过程,中间有一个环节是把模板编译成render函数,这个过程我们把它称作编译。

虽然我们可以直接为组件编写render函数,但是编写template模板更加直观,也更符合我们的开发习惯。

Vue.js提供了2个版本,一个是Runtime + Compiler的,一个是Runtime only的,前者是包含编译代码的,可以把编译过程放在运行时做,后者是不包含编译代码的,需要借助webpackvue-loader事先把模板编译成render函数。

slot

Vue 的组件提供了一个非常有用的特性 —— slot 插槽,它让组件的实现变的更加灵活。我们平时在开发组件库的时候,为了让组件更加灵活可定制,经常用插槽的方式让用户可以自定义内容。插槽分为普通插槽和作用域插槽,它们可以解决不同的场景,但它是怎么实现的呢,下面我们就从源码的角度来分析插槽的实现原理。

普通插槽

为了更加直观,我们还是通过一个例子来分析插槽的实现:

let AppLayout = {
  template: '<div class="container">' +
  '<header><slot name="header"></slot></header>' +
  '<main><slot>默认内容</slot></main>' +
  '<footer><slot name="footer"></slot></footer>' +
  '</div>'
}

let vm = new Vue({
  el: '#app',
  template: '<div>' +
  '<app-layout>' +
  '<h1 slot="header">{{title}}</h1>' +
  '<p>{{msg}}</p>' +
  '<p slot="footer">{{desc}}</p>' +
  '</app-layout>' +
  '</div>',
  data() {
    return {
      title: '我是标题',
      msg: '我是内容',
      desc: '其它信息'
    }
  },
  components: {
    AppLayout
  }
})

这里我们定义了 AppLayout 子组件,它内部定义了 3 个插槽,2 个为具名插槽,一个 name 为 header,一个 name 为 footer,还有一个没有定义 name 的是默认插槽。 <slot></slot> 之间填写的内容为默认内容。我们的父组件注册和引用了 AppLayout 的组件,并在组件内部定义了一些元素,用来替换插槽,那么它最终生成的 DOM 如下:

<div>
  <div class="container">
    <header><h1>我是标题</h1></header>
    <main><p>我是内容</p></main>
    <footer><p>其它信息</p></footer>
  </div>
</div>

编译

还是先从编译说起,我们知道编译是发生在调用vm.$mount的时候,所以编译的顺序是先编译父组件,再编译子组件。

首先编译父组件,在parse阶段,会执行processSlot处理 slot,它的定义在src/compiler/parser/index.js中:

基础

Vue.prototype

很多人经常搞混 Vue 中的全局变量和原型属性。其中 Vue.prototype 不是全局变量,而是原型属性。实例中 this.aVue.prototype.a 而来。如果需要设置全局变量,在 main.js 中,Vue 实例化的代码里添加:

new Vue({
  ...
  data() {
    return {
      ...,
      a: 1
      ...
    };
  },
  ...
});

其他所有组件中通过 this.$root.a可访问此变量。

Class 与 Style 绑定

  • 数组语法

一个静态一个动态::class="['wrapper', { mobile : isMobile }]"

双向绑定

目前几种主流的mvc(vm)框架都实现了单向数据绑定,而我所理解的双向数据绑定无非就是在单向绑定的基础上给可输入元素(input、textare等)添加了change(input)事件,来动态修改model和 view,并没有多高深。所以无需太过介怀是实现的单向或双向绑定。

实现数据绑定的做法有大致如下几种:

1、发布者-订阅者模式(backbone.js)

2、脏值检查(angular.js)

3、数据劫持(vue.js)

发布者-订阅者模式: 一般通过sub,pub的方式实现数据和视图的绑定监听,更新数据方式通常做法是vm.set('property', value),这里有篇文章讲的比较详细,有兴趣可点这里

这种方式现在毕竟太low了,我们更希望通过vm.property = value这种方式更新数据,同时自动更新视图,于是有了下面两种方式

脏值检查: angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,最简单的方式就是通过setInterval()定时轮询检测数据变动,当然Google不会这么low,angular只有在指定的事件触发时进入脏值检测,大致如下:

1、DOM事件,譬如用户输入文本,点击按钮等。( ng-click )

2、XHR响应事件 ( $http )

3、浏览器Location变更事件 ( $location )

4、Timer事件( $timeout , $interval )

5、执行 $digest() 或 $apply()

数据劫持:vue.js则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的settergetter,在数据变动时发布消息给订阅者,触发相应的监听回调。

思路整理

已经了解到vue是通过数据劫持的方式来做数据绑定的,其中最核心的方法便是通过Object.defineProperty()来实现对属性的劫持,达到监听数据变动的目的,无疑这个方法是本文中最重要、最基础的内容之一。

参考:剖析Vue原理&实现双向绑定MVVM

声明式编程与命令式编程

命令式编程:命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现。

声明式编程:告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。

比如react和vue,我们只负责想要什么数据、事件等等,具体怎么渲染那是react和vue的事情,如果用jquery命令式渲染,你需要知道如何增加,删除、更改、插入等等,渲染工作需要自己完成!

Vue2.0 中,“渐进式框架”和“自底向上增量开发的设计”这两个概念是什么?

Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。

渐进式代表的含义是:主张最少。

Vue2.0 中,“渐进式框架”和“自底向上增量开发的设计”这两个概念是什么?

Vue slot

slot 实际上是一个抽象元素,有点类似template,设计思想有点类似面向对象中的多态,用于组件中某一项需要单独定义,那么就应该使用solt。核心概念是:组件当中某一项,可能是一个元素,也可能只是一个文本。。。。

举例说明下: 项目中需要一个模态框,包括成功和失败两种情况,其中该模态框有文案和背景图片差异,那么模态框可以看作一个组件,而文案和背景图片就可以用slot。

Vue属性绑定

Vue2.0里属性绑定数据已经弃用了 Mustache 而改用 v-bind。为什么会弃用?大致如下原因:

<img src = {{img_url}} />

html都是给浏览器引擎解析的, 这种在html标签里使用Mustache进行属性绑定数据的,在 vue 处理 img_url 之前,浏览器会得到一个错误的 src 值,即{ {img_url}},所以浏览器会发送一个错误的 GET 请求,用 v-bind ,

<img v-bind:src = 'img_url'>
// 或
<img :src = 'img_url'> 

对于 v-bind:src 和 :src 浏览器根本不认识,一个没有 src 的 img,浏览器是不会发出请求的,因此 Vue2.0 统一了接口,去除了不能通用的 Mustache {{}}

Vue编写插件

插件和组件关系就是:插件可以封装组件,组件可以暴露数据给插件。

vue插件的编写方法一般分为4类:

  1. 添加全局方法或属性
export default {
  install(Vue, options) {
    Vue.$myName = '劳卜';
    Vue.myGlobalMethod = function () {  // 1. 添加全局方法或属性,如:  vue-custom-element
      // 逻辑...
    }
  }
}

在install方法中,我们直接在Vue实例上声明了$myName属性并进行了赋值,当该插件注册后只要存在Vue实例的地方你都可以获取到Vue.$myName的值,因为其直接绑定在了Vue实例上。

  1. 添加全局资源
export default {
    install(Vue, options) {
        Vue.directive('focus', {
            bind: function() {},

            // 当绑定元素插入到 DOM 中。
            inserted: function(el, binding, vnode, oldVnode) {

                // 聚焦元素
                el.focus();
            },

            update: function() {},
            componentUpdated: function() {},
            unbind: function() {}
        });
    },
}

添加全局资源包含了添加全局的指令/过滤器/过渡等,上方代码我们通过Vue.directive()添加了一个全局指令v-focus,其主要包含了5种方法,其中inserted代表当绑定元素插入到 DOM 中执行,而el.focus()代表聚焦绑定的元素,这样如果我们在一个input输入框上绑定该指令就会自动进行focus聚焦。

  1. 添加全局mixin方法
export default {
    install(Vue, options) {
        Vue.mixin({
            methods: {
                greetingFn() {
                    console.log('greeting');
                }
            }
        });
    },
}

mixin代表混合的意思,我们可以全局注册一个混合,其会影响注册之后创建的每个 Vue 实例,上方代码注册后会在每个组件实例中添加greetingFn方法,在单文件组件中可以直接通过this.greetingFn()调用。当然如果实例中存在同名方法,则mixin方法中创建的会被覆盖,同时mixin对象中的钩子将在组件自身钩子之前调用。

  1. 添加实例方法
export default {
    install(Vue, options) {
        Vue.prototype.$myName = '劳卜';
        Vue.prototype.showMyName = value => {
            console.log(value);
        };
    },
}

添加实例方法是最常用的一种方法,其直接绑定在vue的原型链上,我们可以回想一下 JS 里的类的概念。实例方法可以在组件内部,通过this.$myMethod来调用。

  1. 插件封装组件

上方4点只讲解了插件自身的4中编写方法,并没有涉及组件的内容,如果我们要在组件的基础上编写插件,我们可以使用Vue.extend(component)来进行,可以见下方loading插件实例。

Vue跨域解决方法

vue项目中,前端与后台进行数据请求或者提交的时候,如果后台没有设置跨域,前端本地调试代码的时候就会报“No 'Access-Control-Allow-Origin' header is present on the requested resource.”这种跨域错误。

  • 一、CORS

后台设置:

header('Access-Control-Allow-Origin:*');//允许所有来源访问 
header('Access-Control-Allow-Method:POST,GET');//允许访问的方式 
  • 二、使用JQuery提供的jsonp (注:vue中引入jquery,自行百度)
methods: { 
  getData () { 
    var self = this 
    $.ajax({ 
      url: 'http://f.apiplus.cn/bj11x5.json', 
      type: 'GET', 
      dataType: 'JSONP', 
      success: function (res) { 
        self.data = res.data.slice(0, 3) 
        self.opencode = res.data[0].opencode.split(',') 
      } 
    }) 
  } 
} 
  • 三、使用http-proxy-middleware 代理解决(项目使用vue-cli脚手架搭建)

例如请求的url:“http://f.apiplus.cn/bj11x5.json”

1、打开config/index.js,在proxyTable中添写如下代码:

proxyTable: { 
  '/api': {  //使用"/api"来代替"http://f.apiplus.c" 
    target: 'http://f.apiplus.cn', //源地址 
    changeOrigin: true, //改变源 
    pathRewrite: { 
      '^/api': 'http://f.apiplus.cn' //路径重写 
      } 
  } 
}

2、使用axios请求数据时直接使用“/api”:

getData () { 
 axios.get('/api/bj11x5.json', function (res) { 
   console.log(res) 
})

通过这种方法去解决跨域,打包部署时还按这种方法会出问题。解决方法如下:

let serverUrl = '/api/'  //本地调试时 
// let serverUrl = 'http://f.apiplus.cn/'  //打包部署上线时 
export default { 
  dataUrl: serverUrl + 'bj11x5.json' 
}

vue.js中箭头函数this的指向

所有的生命周期钩子自动绑定this上下文到实例中,因此你可以访问数据,对属性和方法进行运算。这意味着 你不能使用箭头函数来定义一个生命周期方法 (例如created: () => this.fetchTodos())。这是因为箭头函数绑定了父上下文,因此this与你期待的Vue实例不同,this.fetchTodos的行为未定义。

而生命周期钩子内部的箭头函数相当于匿名函数,并且简化了函数定义。看上去是匿名函数的一种简写,但实际上,箭头函数和匿名函数有个明显的区别:箭头函数内部的this是词法作用域,由上下文确定。此时this在箭头函数中已经按照词法作用域绑定了。很明显,使用箭头函数之后,箭头函数指向的函数内部的this已经绑定了外部的vue实例了。

vue脚手架vue-cli起的webpack项目,用localhost可以正常访问,切换到ip就提示无法访问此网站

修改config/index.js,将host: 'localhost'改为host:’0.0.0.0’

路由

Vue路由参数

{ path: '/' },
// 参数用":"表示
{ path: '/params/:foo/:bar' },
// 可以通过添加“?”来选择参数(可有可无)
{ path: '/optional-params/:foo?' },
// 参数后面可以有一个ReEX模式,如果ID是所有的数字,这个路由将匹配。
{ path: '/params-with-regex/:id(\\d+)' },
// 星号可以匹配任何东西
{ path: '/asterisk/*' },
// 通过用括号包装并添加“?”来使路径的一部分可选。
{ path: '/optional-group/(foo/)?bar' }

vue-router 源码分析-整体流程

vue-router 源码分析-整体流程

Nuxt.js

开发环境是否允许自动打开浏览器

参考:Auto open browser when run dev

1、方法一(推荐)

// package.json update
{
  "config": {
    "nuxt": {
      "host": "127.0.0.1",
      "port": "8888"
    }
  }
}

// nuxt.config.js
const opn = require('opn')

module.exports = {
  hooks: {
    listen(server, { host, port }) {
      opn(`http://${host}:${port}`)
    }
  }
}

2、方法二

// package.json update
{
  "scripts": {
    "dev": "node scripts/server.js"
  }
}

// scripts/server.js
const opn = require('opn');
const { Nuxt, Builder } = require('nuxt');
const app = require('express')();
const port = process.env.PORT || 8888;

// 传入配置初始化 Nuxt.js 实例(config and init)
const config = require('../nuxt.config.js');

const nuxt = new Nuxt(config);
app.use(nuxt.render);

// 在开发模式下进行编译(Compile in the development mode)
if (config.dev) {
  new Builder(nuxt).build()
  .catch((error) => {
    console.log(error);
    process.exit(1);
  })
}

// 监听指定端口(Listen on port)
app.listen(port, 'localhost', function () {
  console.log('成功开启'+ port +'端口');
  var url = 'http://localhost:' + port;
  console.log('> 服务器运行于 ' + url + '\n');
  opn(url);
})

element-ui

当水平折叠收起菜单时,需要手动引入Tooltip,也就是说NavMenu依赖于Tooltip

上次更新: 2018-7-16 18:10:21