2013尤雨溪在Google基于Angular开发,起初命名为Seed,同年12月更名为Vue-0.6.0.

3.0版本基于TypeScript,框架基于MVVM模型. SPA(Single Page Application);

一个vue文件就相当一个小组件(里面包含了template+css+js)!
data --> Virtual DOM --> Real DOM

install

  npm config set registry=http://registry.npm.taobao.org  # 全局更改源
  npm install -g typescript;
  npm install -g @vue/cli  # cnpm(国内的npm)有可能会遇到一些诡异问题;
  vue upgrade --next; vue -V

  # 目录生成
  vue create demo; vue ui;

basic

  • reactive内部使用 window.Proxy + window.Reflect 来实现响应式
  • ref内部还是使用 Object.defineProperty 来实现响应式
  • vue2: methods do not use ‘=> function’, because ’this’ will become window instead of vm

project tree

  build   # webpack
  config  # 端口等配置
  dist    # npm run build
  node_modules  # npm项目依赖模块
  public
    - index.html  # SPA
  src
    - api
    - assets  # language
    - components  # 基础的通常是view依赖的组件
    - views    # 通过路由来动态切换的组件. 亦可称为'pages'
    - router
        index.js
    - hooks
        usePoint.js  # setup()中一个独立的单元(数据定义+操作+生命周期)
    - store    # vuex四元组
        index.js  # 导出store的地方
        actions.js  # 根级别的action
        mutations.js
      - modules
          cart.js
    - App.vue  # 根组件,被main.js引入
    - main.js  # entry file. render App.vue to public/index.html!
    - index.html
    - theme.less  # 覆盖vant中的主题变量,避免重启
  static
    js/img/css
  .env  # 在所有环境中被载入; 假如这里面有变量: XX; 访问: process.env.XX
  .env.local/dev/sit/pro  # .env.dev.local(不应该加入git) > .env.local > .env.dev > .env
  .editorconfig
  babel.config.js  # ES6 --> ES5
  package-lock.json  # 包版本控制(锁定版本号)
  package.json
  webpack.config.js
  vue.config.js

component & template

绑定即绑定到Vue中的变量.方便动态变更数据<–>UI! DOM Template. Warning: vue2 Only allows one root node!

keyfunction
v-prevue will omit this element. improved efficiency!
v-once仅执行第一次插值,后面数据变化不再变动; <span v-once>{{msg}}</span>
v-htmlraw html; <span v-html="rawHtml"></span>; for safe reason, do not use this directive in form!
v-text实际上通常使用插值表达式(无法用在属性上) {{}} 来替代该指令!
v-show是否展示(非真移除,通过display属性); <h1 v-show="ok">xx</h1>
v-cloak斗篷/披风/遮盖物; 该属性的元素配合style标签内如下形式可解决闪烁问题: [v-cloak]{display:none}

demos

vue高亮有bug,以html替代之

<!--
* template: 会隐含的包含一个根标签<fragment>(但是渲染时又去掉了,vue2则必须将多个放在一个div里面)
-->
<template>
  <!-- 引用某个组件,在下面的script-component声明过了 -->
  <HelloWorld msg="" />
  <!-- 注意:插值表达式无法用在属性key(只能用在内容节点)! 可以是一些简单的js表达式! -->
  {{5+5}}
  <!-- nb些,可计算一个表达式! -->
  {{`rgb(${r},${g},${b})`}}
  <!-- if var.b undefined but var defined. this will return empty string! -->
  {{ ok ? 'yes': 'no' }}
  <!-- 注意:if等流控制是不行的 -->
  {{ message | msg.split().reverse().join() }}

  <!-- 条件(真的添加删除元素),仅能用在1个元素上 -->
  <p v-if="show">xx</p>
  <!-- ref: 方便vue通过'this.$refs.ref1'成员来引用我(DOM对象)! ref也可用在子组件身上(方便父亲调用子组件中的方法)! -->
  <p v-else-if ref="ref1"></p>

  <!-- 多个元素需使用template包裹 -->
  <template v-else="var == 'A'"><h1></1><p></p></template>
  <!-- 也可省略index(再次插入不能打乱原有的顺序); key用来记录每一项的状态(必须为数字或字符串且唯一),放置vue优化乱掉 -->
  <template v-for="(site, index) in sites" :key="user.id">
    <li>{{site.title}}</li>
  </template>
  <!-- 迭代对象的属性,也可以添加键名或索引: (v, key, index) in object -->
  <li v-for="v in object">{{v}}</li>
  <!-- 循环数字 -->
  <li v-for="n in 8">{{n}}</li>

<!-- binding - the value is considered as a expression -->
  <!-- 给属性key添加绑定"单向数据绑定"! 注意:双引号中的时纯js表达式,9就是数字类型.若想使用字符类型需单独加上单引号!!但不加bind时,他们的属性值都是字符串! -->
  <button :status="disabled">xx</button>
  <!-- var is a vue variable defined in 'data'; value can also be a function! -->
  <div v-bind:title="'box-' + var"></div>
  <!-- binding to a css value(must be an object or an array)! supports computed! -->
  <!-- css value expression often be defined as a variable in vm(ViewModel) -->
  <div :style="{backgroundColor: `rgb(${r},${g},${b})`;}"></div>
  <input type.trim="text" v-model="msg.name">  <!-- collects the default attribute 'value' to msg.name -->
  <input type="nubmer" v-model.number="msg.age">  <!-- warning: v-model always collect the value as a string by default. type's 'number' limits the input! -->
  <input type="radio" v-model="sex" value="male">  <!-- specify an additional value attribute(radio doesn't have value attribute by default) -->
  <input type="radio" v-model="sex" value="female">
  <input type="checkbox" v-model="hobby" value="eat">  <!-- vue variable 'hobby' would be an array -->
  <input type="checkbox" v-model="hobby" value="play">
  <input type="checkbox" v-model="agree">  <!-- 'agree' would be a bool. collects the default attribute 'checked' -->
  <textarea v-model.lazy="msg.info"></textarea>  <!-- only update 'info' when lose the focus! -->
  <select v-model="province">
    <option value="">请选择</option>
    <option :value="1">北京</option>  <!-- colon makes the "1" be a number or consider using 'v-model.number' -->
  </select>

<!-- events -->
  <!-- vue always treat component's event as customed event, so if you wanna use primitive event(vue2 only): '@click.native="cb"' -->
  <Son @update:evt1="cb"></Son>  <!-- 自定义事件(组件销毁后自动失效). 接收子组件emit传递过来的值; 也可配合once.. -->
  <!-- keydown/mousedown/mouseover/submit; delete esc space up down left right -->
  <!-- these key should combine with keydown: tab alt ctrl meta shift -->
  <a v-on:keyup.enter="keyEvent">
  <!--
    可不传参,省略小括号; $event是固定写法,表当前触发事件的对象(无自定义参数时可不写)! 可直接对变量进行简单操作(无需methods).
  * @click.stop:  默认click会传播到父标签(冒泡,前提时父标签也绑定了),阻止这种传递(==e.stopPropagation())
  * @click.self:  不接收冒泡,也不会向外传播. 仅当直接点击(可能要排除子控件范围)了自己(event.target==self),才会触发!
  * @click.once:  仅会触发一次(后续点击不会触发了)
  * @click.capture:  最外层先捕获(需要外层添加capture,方可优先捕获)
  * @click.prevent:  阻止link或submit的跳转(当然也可在响应函数中使用e.preventDefault()来实现)
  -->
  <a @click="doSth(2, $event)">..</a>
  <form @submit.prevent="add"></form> <!-- 防止页面刷新且绑定提交 -->
  <!--
  * @blur:  失去焦点
  * @scroll:  rsp cb then scroll the scrollbar. Same as wheel!
  * @wheel.passive:  // async execute cb then scroll the mouse wheel!
  -->

<!-- component: the micro-vm -->
  <!-- component is the set of local codes and resources -->
  <keep-alive include="comp1,compdo2">  <!-- avoid destroy; cache comp1 and comp2 -->
    <component :is="指定哪个组件名称就显示谁"></component>
  </keep-alive>

<!-- slot -->
  <!-- 定义插槽: 将当前组件中的数据'x'传递给外面的插槽使用者(如组件标签的内容),方便使用者使用本组件中定义好的数据! -->
  <slot name='slotName' :param="x" :user="bindingVar">
    <p>This is the default content</p>
  </slot>
  <!-- 使用插槽: warning: v-slot command(#) can only be used in template/component! -->
  <Left>
    <template #slotName="paramValueobj">  <!-- #slotName='{ msg, user }'  '#slotName'  '#default':means anonymous slot -->
      <!-- obj: { msg: 'x', user: {} }; -->
    </template>
  </Left>
</template>

<script>
import HelloWorld from './components/hello_world.vue'
export default { // Vue will call Vue.extend({}) automatically
name: 'MyComp1',  // the name of the current component. This name will be used in keep-alive and debug
el: '#div1',  // this component will replace the div1 element in index.html
components: {
  // register another component; can be used in template node by '<HelloWorld>xxx<HelloWorld>' or '<hello-world></hello-world>'
  // HelloWorld is an independent instance of the component! when use it in template vue will call 'new VueComponent(options)'
  // VueComponent.prototype.__proto__ = Vue.prototype --> supports using Vue members
  HelloWorld  // vm.children[0]
}
</script>

common scripts

import {ref, reactive, provide, toRef, toRefs, computed, watch, watchEffect, onBeforeMount} from 'vue'  // vue3
import HelloWorld from './components/hello-world.vue'  // webpack会提升最外层所有的import到上方!
const Child = defineAsyncComponent(() => import('./components/Child'))  // 动态引入,不用等待最慢的人(配合Suspence)
import Vue from 'vue'
import TheComponent from './components/my-comp1.vue'
Vue.use(Vant)
Vue.prototype.$http = axios  // 很少用
Vue.http.options.root = 'xxx'
Vue.http.options.emulateJSON = true
Vue.config.keyCodes.f2 = 113  // custom global keycode
const app = Vue.createApp({}) // app is the light weight vm
app.directive('focus', {
  mounted(el) { // 被绑定的元素挂载到DOM中时
    el.focus(); // 聚焦元素
  }
})
app.component('my-comp1', TheComponent)  // usage: <my-comp1 self-def-prop=""></my-comp1>; consider use 'app.component(TheComponent.name, TheComponent)'
app.mount('#app')

// reactive
const p = new Proxy(obj, { // 拦截对象中任意属性的变化
  get(target, propName){ // target:当前被代理的对象obj!
    return Reflect.get(target,propName)  // 操作任意对象的属性
  },
  set(target, propName, value){ // 修改或增加属性
    Reflect.set(target, propName, value)
  }
  deleteProperty(target, propName){ // 删除属性
    return Reflect.deleteProperty(target, propName)  // true/false
  }
})

props(vue2+vue3)

// usage: <MyComp1 :prop-one="x" undefinedProp="This prop will be omitted" />
// <==> VueComponent.$attrs.propOne; 类比 $slots 也会去捡漏
props: {
  propOne: { // warning: 'propOne' is readonly! 'this.propOne++' will cause a warning! reserved keys: 'key, ref'
    default: 2,   // 未指定时,初始值; 若是Object则必须通过一个function来返回{};
    type: [Number, String], // 限制其接收值的类型. 也可以是一个自定义对象类型Object(可改变对象的成员,但都不建议去修改prop属性)
    required: true, // even a default value is specified, missing this prop will still causing a warning in the console!
    validator(value) { // validate the prop value
      return true
    }
  },
  // usage: <MyComp1 :prop1="var" />
  // usage: <MyComp1 v-model:prop1="var" />: bi-directional data binding(always associated with a customized event)
  // if you don't declare it in props, then it will appear in $this.attrs.prop2
  prop1: var || []
},
emits: [
  'evt1'  // context.emit('evt1', xx)
],

setup(vue3)

// 于'beforeCreate()'前执行; 这里面不要使用'this'
// setup() includes: beforeCreate() + created()
setup(props, context){
  context.attrs  // props没有定义但调用时传递了,则出现在此处!
  context.slots  // slot
  context.emit('evt1', xx)
  // 这里面有些逻辑可能很复杂,可以单独组合成独立的模块. 然后在这里引入使用! -- hooks
  // isRef isReactive isReadonly isProxy(响应式的obj经过readOnly修饰后还是proxy的)
  let age = ref(2)  // 指定一个响应式变量的初始值. 使用时:'age.value=5'; 如果修饰的是个对象,内部还是会调用reactive! isRef/isReactive/isProxy/isReadonly;
  let obj = reactive({name:''})  // 适用于对象或数组(后续动态追加的属性也自动具有响应式,不想?使用:obj.car=markRaw({})); shallowReactive:仅处理对象第1层属性的响应式!
  let point = savePoint()  // 调用hooks中的某个模块
  let x = toRef(obj.jb, 'jb_attr_name')  // 使对象的属性具有响应式(参数1必须是个对象,无需.value)! toRefs(obj.jb):把jb第1层的所有属性都toRef!
  let y = toRaw(obj)   // 去掉reactive修饰的响应式(与UI剥离关系)
  obj = readonly(obj)  // 用别人的响应式数据,但是保证不修改它; shallowReadonly(obj):使响应式变量的第1层不可修改
  
  /* MVVM */
  // warning: attention the recursive call problem! pay attention to the caller's variable and the template's variable!!
  function say(n, e) { // <button @click=clickEvent(2, $event)></button>
    e.target.style.backgroundColor = 'blue'  // .target表触发事件的事件源!如是一个按钮.
    e.preventDefault()  // 阻止事件的默认行为(如,点击跳转)
    obj.name = '1'
    debugger  // script中代码的方式打断点
  },
  context.emit('evt1', 2)  // evt1在'emits'属性中声明可能会好点

  /* computed */
  obj.fullName = computed({ // vue3 defines a computed member. defaults readonly: ()=>{}
    get(){
      return obj.firstName + obj.LastName
    },
    set(value){
    }
  })

  /* watch */
  watch([obj, obj1], (newValue, oldValue)=>{ // newValues:[objNewValue, obj1NewValue...]; 刚触发时,oldValue可能是空数组[]!
    // 注意: 仅能监视'ref/reactive/array'; 能够深度监视(且关不掉,vue2则默认关闭深度监视)
    // 注意: reactive修饰的对象拿不到oldValue! 若要监视ref修饰的对象,则需要obj.value(非对象的ref无需)!
  })
  watch(()=>obj.attr1, (newValue, oldValue)=>{
    // 仅监视其中的某个属性(此时oldValue好用了); 如果要监视多个属性,则第一个参数写成数组!
    // 注意: 如果attr1本身是个obj,则必须加上{deep:true}才能监视到obj.attr1.xx的变化(即又回落到了vue2)!
  }, {
    immediate: true  // 没有修改初始化值的时候自动进来一次
  })
  watchEffect(()=>{ // 这里面用到的变量都将被监视. 另外,还附加了'immediate=true'
    const x = age.value
    const y = obj.job.salary  // 很聪明:其他层次或属性的变化并不会进来!
  })

  return {
    obj,  // {{obj}}
    say,
    ...x  // 解包对象x的所有有属性,一个个的共享给template
  }
}

hooks & lifecycle

// usePoint.js
import {reactive, onMounted, onBeforeUnmount} from 'vue'
export function savePoint() {
  let point = reactive({
    x: 0, y: 0
  })
  function savePoint(event) {
    point.x = event.pageX  // 鼠标坐标
    point.y = event.pageY
  }
  /* lifecycle */
  on[Before]Mount(()=>{ // 都需要import; v-if时就伴随着组件的挂载/卸载
    // before时,模板还没有被渲染,对外表现还是{{x}}.故此时不要使用dom元素!
    // mounted后,dom已经被挂载到了view,此时可以操作dom元素.此时长会做一些初始化操作:
    // setInterval(), network requests, subscribes, binding events
    window.addEventListener('click', savePoint)
  })
  on[Before]Update(()=>{
    // The page is old but the data is new; view is not sync with the data yet!
  })
  on[Before]Unmount(()=>{
    // 一般都是在onBeforeUnmount()里面做一些收尾:
    // 仍可使用所有的 datas + methods,但修改data并不会影响view了!
    // clearInterval() + cancel subscribes + unbind events
    this.$bus.$off('update:evt1')
    window.removeEventListener('click', savePoint)
  })
  return point  // 一般会在setup中被使用
}

// component lifecycle
activated() {
  this.timer = setInterval(()=>{})
}
deactivated() { // 如tab路由切换
  clearInterval(this.timer)
}
beforeRouteEnter(to, from, next) { // 通过路由规则(以下不算:直接引入/嵌入组件)进入当前组件时
  next()
}
beforeRouteLeave(to, from, next) { // 通过路由规则离开当前组件时
  next()
}
}

custom referrence & provide

setup(props, context){
  function myRef(value, extraDefParam) { // let x = myRef('value')
    let timer
    return customRef((track, trigger)=>{
      return {
        get(){ // 只要有地方使用了myRef修饰的对象,如v-model="x"或{{x}}
          track()  // 追踪value后续数据的变化(默认仅保留第1次的值); 与set中的trigger对应(不调用则不会响应trigger)!
          return value
        },
        set(newValue){ // 有人通过UI改了{{x}}
          clearTimeout(timer)
          timer = setTimeout(()=>{
            value = newValue
            trigger()  // 通知vue重新解析模板(再次触发上面的get-track)
          })
        }
      }
    })
  }
  /* provide */
  provide( // 给子孙组件传递数据
    'car': obj
  )
  let n = inject('car')  // 使用父辈数据.注意:是响应式的数据!
}

style

  npm install -D less
  npm install animate.css
import Vant from 'vant'  // put these import sentences to <script> section
import 'animate.css'
// scoped: this style only applied to the current component(automatically add 'data-v-xxxx' attribute to every element) == Rule 'A'
// App doesn't use scoped tag, use for storing public styles
<style lang="scss" scoped>
  :deep(.xxx) { /* break the rule 'A'. apply current css to child's element */
    transition: 1s linear;  /* 使用下面的transition(A) */
  }
  .hello-enter, .hello-leave-to {
    transform: translateX(-100%);
  }
  .hello-enter-to, .hello-leave {
    transform: translateX(0);
  }
  .hello-enter-active, .hello-leave-active {
    transform: translateX(0);
  }
</style>

vue2

data

// data proxier. This would be a heavy procedure!
// data and its operating method should stay in the same place!
data() { // == vm._data;
  return {
    // warning: Vue does not add reactive getter/setter to array's item!
    // Vue agents and listens all the modified members(push,pop,shift,unshift,splice,sort,reverse) of an array
    // If a member of an array is an object, it will become the react object!
    // member like lists will be proxied by Object.defineProperty(vm, 'lists', {get(){};set(){}}); vue3 use Proxy instead of defineProperty
    lists: [ // vm.lists == vm._data.lists
      {name:'leiz', age:'26'},
      {name:'dabao', age:'5'}
    ],
    styleObject: { // css value expression
      fontSize: '2px'  // can be used as prop's value--> :prop1="obj.name" :class="obj"
    },
    myPropOne: this.propOne  // prop has the highest priority(occupied the name: 'propOne'), here is the trick to store and change the prop!
  }
},

methods

beforeCreate(){
}
xx(n, e){
  this.$refs.btn   // access the DOM element by its attribute 'ref="btn"'; can also get the component instance(not only the DOM)!
  this.$nextTick(()=>{})  // 延迟执行回调(当dom渲染完毕后)
  if ('newAttr' in obj) { // obj.hasOwnProperty('newAttr')
    obj.newAttr = value  // 第1次已经注册过了,故此处无需再向Vue注册
  } else {
    this.$set(obj, 'newAttr', value)  // vue2:动态给obj添加一个响应式的变量(注册给了vue,以便监听该变量,建立其与View的联系); $delete()
    this.hobby[0] = ''  // vue2也监听不到数组中元素的变更(不仅仅使对象动态新增或删除的属性)
  }
  e.keycode  // enter is 13
  e.target.value = '';  // 此处的target一般表示input
  // 箭头函数会继承外层函数的this指向;若直接function(){},则这里面的this会被理解成window!
  this.intervalId = setInterval(() => {
    var start = this.msg.substring(0, 1)
    var end = this.msg.substring(1)
    // VM实例会监听自己身上 data 中所有数据的改变: 只要数据一发生变化,就会自动把最新的数据从data上同步到页面中去
    this.msg = end + start
  }, 400)
}
emit1(v) { // child should listening: <Son @update:evt1="onEvt1"></Son>
  // this is the parent logic -->
  // bind ref1 to an event(not using <Child @evt1=""/>)
  this.$refs.ref1.$on/$once('evt1', (){ // this cb use for receiving data from child
    // warning: this stands for the exact component that bind to this event. so, normally you should write this cb outside(or use => function)!
  })
  this.$bus.$on/$once('evt1', (obj){ // global event bus: suitable for communication between a small number of components
    this.info = {...this.info, ...obj}  // 仅修改info中改变的项(跳过那些undefined项)
  }
  // this is the child logic -->
  // customized events 'evt1' used for notifying msg from child to parent or siblings
  // params will pass to parent
  this.$emit('update:evt1', {'key': this.prop1})
  this.$bus.$emit('update:evt1', this.prop1)  // global event bus
  // this is the child(which is binded to the specific event) logic, unbind an event
  this.$off('evt1')  // unbind multiple events: ['evt1', 'evt2']; empty param means unbind all events!
}

computed

// do not embedded async task(setTimeout e.g.)
// usually used in searching filter
computed: {
  rgb(r,g,b){ // like macro, usage: {{ rgb }}
    return `rgb(${this.r}, ${this.g}, ${this.b})`
  },
  fullName(){
    return this.firstName + this.lastName
  },
  isAll(){ // combined with 'v-model:checked=isAll'. get/set's logical are different
    get(){
      return this.doneTotal == this.total && this.total > 0
    }
    set(value){ // value: bool
      // change all the associated datas
    }
  }
}

watch

watch: { // supports async task(setTimeout e.g.)
  name(newName, oldName) { // 若input则敲击一个字变化一次.可使用lazy修饰v-model!
    setTimeout(()=>{ // warning: ensure using '=>'(doesn't have this, so use the outer this instead 'window'--the explorer)!
      this.getList()
    })
  },
  username: {
    // 若input则敲击一个字变化一次.可使用lazy修饰v-model!
    handler: function(new, old){
      localStorage.setItem('todos', JSON.stringify(new))
    },
    immediate: true, // 进入时先触发一次
    deep: true  // watch this object's attributes change also
  }
}

directive

// called when this derective's template changed!
// directive must use this form: 'x-xx-xx'
// 'this' inside directive becomes 'window'!
directives: { // usage: <p v-focus="'red'"></p>
  v-focus: { // directive name
    bind(e, binding) { // 1. the directive is binding to the calling element by vue
      e.style.color = binding.value  // red
    }
    mounted/inserted(e, binding) { // 2. inserting to DOM
      e.focus()  // <input v-focus>
    },
    update(element, binding) { // 3. when DOM updated; always has the same body as bind, so can be abbreviated: 'v-focus'(){}
      element.value = binding.value
      element.focus()
    }
  }
}

misc

transition, teleport …

<template>
  <transition-group name="hello" appear>  // for single element, just use '<transition>'
    <h1 v-show="show" key="1">hello</h1>  // key is required
    <h1 v-show="show" key="2">hello</h1>
  </transition>

  <teleport to="body">  // 可以不显示在当前组件里面,而是跑到主页面的body中,越级了
  </teleport>
  <Suspence>  // 配合动态引入,占位; 此时setup就可以返回异步对象了
    <template v-slot:default>
    </template>
    <template v-slot:fallback>
    </template>
  </Suspence>
</template>

filters

filters: { // vue3被剔除了
  capitalize(str, arg1, arg2){ // 自定义参数总是在前一个输入项后面
  }
}

vue.config.js

const path = require('path')
const themePath = path.join(__dirname, './src/theme.less')
moduel.exports = {
  css: {
    loaderOptions: {
      less: {
        modifyVars: {
          hack: `true; @import "@{themePath}"`
        }
      }
    }
  },
  devServer: {
    proxy: {
      '/api/get_users': {
        target: '<host>',  // 转发到哪里
        pathRewrite: {  // rewrite path
          '^/api': ''
        }
      }
    }
  }
}