vue2-入门

vue官网:https://cn.vuejs.org/

框架由来

 

核心概念

vue的内部有一些方法,开头带$的是一些实用方法和属性,我们可以配置代码进入,这个过程叫注入,开头带_的是vue内部使用的成员,开发者不要使用它们

var vm = new Vue({
            el: "#app",
            data: {
                products: [
                    { name: "iphone", stock: 10 },
                    { name: "xiaomi", stock: 9 },
                    { name: 'huawei', stock: 8 },
                ]
            },
            computed: {
                total() {
                    return this.products.reduce((a, b) => a + b.stock, 0);
                }
            },
            methods: {
                changeStock(products, newStock) {
                    if (newStock < 0) {
                        newStock = 0;
                    }
                    products.stock = newStock//数据变化
                },
                remove(index) {
                    this.products.splice(index, 1);
                }
            },


        })

注入:

vue会将以下配置注入到vue实例:

  • data:和界面相关的数据
  • computed:通过已有数据计算得来的数据,将来详细讲解
  • methods:方法

模板中可以使用vue实例中的成员,但是要放到大胡子语法里面{ { }}

在控制台输入vm(上面定义vm为vue对象)可以看到vue的实例对象

注入完成之后,才会有数据响应式,即data里面的数据改变了,会影响界面

扫描二维码关注公众号,回复: 17186540 查看本文章

虚拟DOM树

直接操作真实的DOM会严重影响效率,触发浏览器的重排,回流,VUE使用虚拟dom(vnode)的方式来描述要渲染的内容,vnode是一个普通的js对象,用于描述界面上应该有什么。

vue模板不是真实的dom,它会被编译成虚拟dom

<div id="app">
        <h1>标题:商品管理系统</h1>
        <ul>
            <li v-for="(item, i) in products">
                商品名称:{ {item.name}}
                <button @click="changeStock(item,item.stock-1)">-</button>
                { {item.stock?item.stock:"无货"}}
                <button @click="changeStock(item,item.stock+1)">+</button>
                <button @click="remove(i)">删除</button>
            </li>
        </ul>
        <p>总库存:{ {total}}</p>
    </div>

这些内容会被编译成虚拟dom

编译过程中,将标签作为节点,然后可以改变内容

当数据发生改变时,将引发重新渲染,vue会比较新旧两颗树的差异,并把差异部分作用到真实dom tree中

vue通过以下逻辑生成vnode tree

通过vue实例方法render来生成,如果没有render方法,就使用模板(temple)将内容编译为vnode tree,如果没有手写temple,就将el配置的outerHTML作为模板.

虚拟节点数必须是单根的,如果不是单根只会识别第一个。

挂载

将生成的真实的dom树,放到某个元素的位置上,称作为挂载

挂载的方式:

1.通过el:css选择器,进行配置

2.通过vue实例.$mount('css选择器")进行配置

完整流程

实例被创建->注入->(编译生成虚拟节点树->挂载)(初次渲染时)->已挂载。

如果挂载后数据变动,页面会响应并且重新渲染,渲染方式如上。

小知识:v-bind:定于定义图片位置

例如:

<div id="app">
      <img :src="image" alt="" />
    </div>
 <script src="./vue.min.js"></script>
    <script>
      var vm = new Vue({
        data: {
          image:
            "https://dss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=2342083191,4139347491&fm=26&gp=0.jpg",
        },
      });
      vm.$mount("#app");
    </script>

v-for:循环数组;注意绑定key值,key值唯一且稳定

例如:

<!-- vue模板 -->
    <div id="app">
      <h1>系统名称:{ {title}}</h1>
      <ul>
        <li v-for="(item, i) in products" :key="item.id">
          { {item.name}}
          <button @click="changeStock(item, item.stock-1)">-</button>
          <span>{ {item.stock > 0 ? item.stock : "无货"}}</span>
          <button @click="item.stock++">+</button>
          <button @click="deleteProduct(i)">删除</button>
        </li>
      </ul>
      <p>总库存:{ {totalStock}}</p>
    </div>
    <script src="./vue.min.js"></script>
    <script>
      var vm = new Vue({
        // 配置对象
        el: "#app",
        data: {
          title: "库存管理系统",
          products: [
            { id: 1, name: "iphone", stock: 0 },
            { id: 2, name: "xiaomi", stock: 6 },
            { id: 3, name: "huawei", stock: 7 },
          ],
        },
        computed: {
          totalStock() {
            return this.products.reduce((a, b) => a + b.stock, 0);
          },
        },
        methods: {
          deleteProduct(i) {
            this.products.splice(i, 1);
          },
          changeStock(p, newStock) {
            if (newStock < 0) {
              newStock = 0;
            }
            p.stock = newStock;
          },
        },
      });

v-on:可简写为@:variate

绑定事件

指定参数 事件名

简写@

怎么降低复杂度,怎么降低重复的问题:

主要是靠组件来解决,组件出现的目的主要有两个:

1.降低整体复杂度,提升代码的可读性和可维护性

2.提升局部代码的可复用性

简称为细粒度的划分

绝大部分情况下,一个组件就是页面中的某个区域,组件包含该区域的

功能(js代码)

内容(模板代码)

样式(css代码)

要在组件中包含样式,需要构建工具的支撑

组件开发

创建组件

组件是根据一个普通的配置对象创建的,所以要开发一个组件,只需要写一个配置对象即可

该配置对象和vue实例的配置是几乎一样

//组件配置对象

var myComp = {

 data(){

 return {

 // ...

 }

 },

 template: `....`

}

值得注意的是,组件配置对象和vue实例有以下几点差异:

  • el
  • data必须是一个函数,该函数返回的对象作为数据
  • 由于没有el配置,组件的虚拟DOM树必须定义在templaterender

注册组件

注册组件分为两种方式,一种是全局注册,一种是局部注册

全局注册

一旦全局注册了一个组件,整个应用中任何地方都可以使用该组件

全局注册的方式是:

// 参数1:组件名称,将来在模板中使用组件时,会使用该名称

// 参数2:组件配置对象

// 该代码运行后,即可在模板中使用组件

Vue.component('my-comp', myComp)

在模板中,可以使用组件了

两种方式:

<my-comp /> <!-- 或 -->

<my-comp></my-comp>

但在一些工程化的大型项目中,很多组件都不需要全局使用。 比如一个登录组件,只有在登录的相关页面中使用,如果全局注册,将导致构建工具无法优化打包 因此,除非组件特别通用,否则不建议使用全局注册

局部注册

局部注册就是哪里需要组件就在哪里注册组件;

局部注册的方式是,在要使用组件的组件或实例中加入一个配置:

搭建// 这是另一个要使用my-comp的组件

var otherComp = {

 components:{

 // 属性名为组件名称,模板中将使用该名称

 // 属性值为组件配置对象

 "my-comp": myComp

 },

 template: `

 <div>

 <!-- 该组件的其他内容 -->

 <my-comp></my-comp>

 </div>

 `;

}

应用组件

在模板中使用组件特别简单,把组件名当作HTML元素名使用即可。

但要注意以下几点:

  1. 组件必须有结束

组件可以自结束,也可以用结束标记结束,但必须要有结束

下面的组件使用是错误的:

<my-comp>

  1. 组件的命名

无论你使用哪种方式注册组件,组件的命名需要遵循规范。

组件可以使用kebab-case 短横线命名法,也可以使用PascalCase 大驼峰命名法

下面两种命名均是可以的

var otherComp = {

 components:{

 "my-comp": myComp, // 方式1

 MyComp: myComp //方式2

 }

}

实际上,使用小驼峰命名法 camelCase也是可以识别的,只不过不符合官方要求的规范

使用PascalCase方式命名还有一个额外的好处,即可以在模板中使用两种组件名

var otherComp = {

 components:{

 MyComp: myComp

 }

}

模板中:

<!-- 可用 -->

<my-comp />

<MyComp />

因此,在使用组件时,为了方便,往往使用以下代码:

var MyComp = { //组件配置 } var OtherComp = { components:{ MyComp // ES6速写属性 } }

搭建工程

vue中文官网:

介绍 | Vue CLI

VUE-CLI

vue-cli是一个脚手架工具,用于搭建vue工程

它内部使用了webpack,并预置了诸多插件(plugin)和加载器(loader),以达到开箱即用的效果

除了基本的插件和加载器外,vue-cli还预置了:

  • babel
  • webpack-dev-server
  • eslint
  • postcss
  • less-loader

SFC

单文件组件,Single File Component,即一个文件就包含了一个组件所需的全部代码

<template> <!-- 组件模板代码 --> </template>

 <script> export default { // 组件配置 } </script>

 <style> /* 组件样式 */ </style>

预编译

vue-cli进行打包时,会直接把组件中的模板转换为render函数,这叫做模板预编译

这样做的好处在于:

  1. 运行时就不再需要编译模板了,提高了运行效率
  2. 打包结果中不再需要vue的编译代码,减少了打包体积

源代码->打包->运行

在app.vue中模板应写在script标签外方便预编译成render,否则打包结果中无法编译,而且增加打包体积

计算属性

面试题:计算属性和方法有什么区别?

计算属性本质上是包含getter和setter的方法 当获取计算属性时,实际上是在调用计算属性的getter方法。vue会收集计算属性的依赖,并缓存计算属性的返回结果。只有当依赖变化后才会重新进行计算。 方法没有缓存,每次调用方法都会导致重新执行。

 计算属性的getter和setter参数固定,getter没有参数,setter只有一个参数。而方法的参数不限。

由于有以上的这些区别,因此计算属性通常是根据已有数据得到其他数据,并在得到数据的过程中不建议使用异步、当前时间、随机数等副作用操作。(只有响应式数据变化,才会收到通知) 实际上,他们最重要的区别是含义上的区别。计算属性含义上也是一个数据,可以读取也可以赋值;方法含义上是一个操作,用于处理一些事情。

完整计算属性书写方法

computed: {

 propName: {

 get(){

 // getter

 },

 set(val){

 // setter

 }

 }

}

简写

computed: { propName(){ // getter } }

当在app.vue中的样式模块添加scoped属性,则子组件的根元素会受到父组件的类样式影响,并且子组件拥有自己的类样式

 组件事件

v-if 和 v-show

面试题:v-if 和 v-show 有什么区别?

v-if能够控制是否生成vnode,也就间接控制了是否生成对应的dom。当v-if为true时,会生成对应的vnode,并生成对应的dom元素;当其为false时,不会生成对应的vnode,自然不会生成任何的dom元素。

v-show始终会生成vnode,也就间接导致了始终生成dom。它只是控制dom的display属性,当v-show为true时,不做任何处理;当其为false时,生成的dom的display属性为none。

 使用v-if可以有效的减少树的节点和渲染量,但也会导致树的不稳定;而使用v-show可以保持树的稳定,但不能减少树的节点和渲染量。 因此,在实际开发中,显示状态变化频繁的情况下应该使用v-show,以保持树的稳定;显示状态变化较少时应该使用v-if,以减少树的节点和渲染量。

组件事件

抛出事件:子组件在某个时候发生了一件事,但自身无法处理,于是通过事件的方式通知父组件处理

事件参数:子组件抛出事件时,传递给父组件的数据

注册事件:父组件申明,当子组件发生某件事的时候,自身将做出一些处理

优化工程结构

如何使用组件

编写组件说明文档

./src/components/README.md

如何测试组件效果

快速原型开发 | Vue CLI

插槽

简单用法

<!-- message组件:一个弹窗消息 -->

<div class="message-container">

 <div class="content">

 <!-- slot是vue的内置组件 -->

内置组件,插槽,占位

 <slot></slot>

 </div>

 <button>确定</button>

 <button>关闭</button>

</div>

<!-- 父组件App -->

<Message>

 <div class="app-message">

 <p>App Message</p>

 <a href="">detail</a>

 </div>

</Message>

<!-- 最终的结果 -->

<div class="message-container">

 <div class="content">

 <div class="app-message">

 <p>App Message</p>

 <a href="">detail</a>

 </div>

 </div>

 <button>确定</button>

 <button>关闭</button>

</div>

父组件Message标签里的内容会替换掉message组件里,slot标签所占的位置

具名插槽

如果某个组件中需要父元素传递多个区域的内容,也就意味着需要提供多个插槽

为了避免冲突,就需要给不同的插槽赋予不同的名字

<!-- Layout 组件 -->

<div class="layout-container">

 <header>

 <!-- 我们希望把页头放这里,提供插槽,名为header -->

 <slot name="header"></slot>

 </header>

 <main>

 <!-- 我们希望把主要内容放这里,提供插槽,名为default -->

 <slot></slot>

 </main>

 <footer>

 <!-- 我们希望把页脚放这里,提供插槽,名为footer -->

 <slot name="footer"></slot>

 </footer>

</div>

<!-- 父组件App -->

<BaseLayout>

 <template v-slot:header>

 <h1>Here might be a page title</h1>

 </template>

 <template v-slot:default>

 <p>A paragraph for the main content.</p>

 <p>And another one.</p>

 <template v-slot:default>

 

 <template v-slot:footer>

 <p>Here's some contact info</p>

 </template>

</BaseLayout>

v-slot:name来定义传送插槽到哪里

如果是默认值可以不写template标签

路由

路由插件

npm i vue-router

路由插件的使用

import Vue from 'vue'

import VueRouter from 'vue-router'

Vue.use(VueRouter); // Vue.use(插件) 在Vue中安装插件

const router = new VueRouter({

 // 路由配置

})

new Vue({

 ...,

 router

})

基本使用

// 路由配置

const router = new VueRouter({

 routes: [ // 路由规则

 // 当匹配到路径 /foo 时,渲染 Foo 组件

 { path: '/foo', component: Foo },

 // 当匹配到路径 /bar 时,渲染 Bar 组件

 { path: '/bar', component: Bar }

 ]

})

<!-- App.vue -->

<div class="container">

 <div>

 <!-- 公共区域 -->

 </div>

 <div>

 <!-- 页面区域 -->

 <!-- vue-router 匹配到的组件会渲染到这里 -->

 <RouterView />

 </div>

</div>

导航

vue-router提供了全局的组件RouterLink,它的渲染结果是一个a元素

<RouterLink to="/blog">文章</RouterLink>

<!-- mode:hash 生成 -->

<a href="#/blog">文章</a>

<!-- mode:history 生成 -->

<!-- 为了避免刷新页面,vue-router实际上为它添加了点击事件,并阻止了默认行为,在事件内部使用hitory api更改路径 -->

<a href="/blog">文章</a>

激活状态

默认情况下,vue-router会用 当前路径 匹配 导航路径 :

  • 如果当前路径是以导航路径开头,则算作匹配,会为导航的a元素添加类名router-link-active
  • 如果当前路径完全等于导航路径,则算作精确匹配,会为导航的a元素添加类名router-link-exact-active
  • 例如,当前访问的路径是/blog,则:

导航路径 类名

/ router-link-active

/blog router-link-active router-link-exact-active

/about 无

/message 无

可以为组件RouterLink添加bool属性exact,将匹配规则改为:必须要精确匹配才能添加匹配类名router-link-active

例如,当前访问的路径是/blog,则:

导航路径 exact 类名

/ true 无

/blog false router-link-active router-link-exact-active

/about true 无

/message true 无

例如,当前访问的路径是/blog/detail/123,则:

导航路径 exact 类名

/ true 无

/blog false router-link-active

/about true 无

/message true 无

另外,可以通过active-class属性更改匹配的类名,通过exact-active-class更改精确匹配的类名

命名路由

使用命名路由可以解除系统与路径之间的耦合

// 路由配置

const router = new VueRouter({

 routes: [ // 路由规则

 // 当匹配到路径 /foo 时,渲染 Foo 组件

 { name:"foo", path: '/foo', component: Foo },

 // 当匹配到路径 /bar 时,渲染 Bar 组件

 { name:"bar", path: '/bar', component: Bar }

 ]

})

<!-- 向to属性传递路由信息对象 RouterLink会根据你传递的信息以及路由配置生成对应的路径 -->

<RouterLink :to="{ name:'foo' }">go to foo</RouterLink>

动态路由

我们希望下面的地址都能够匹配到Blog组件

  • /article,显示全部文章
  • /article/cate/1,显示分类id1的文章
  • /article/cate/3,显示分类id3的文章
  • ...

第一种情况很简单,只需要将一个固定的地址匹配到Blog组件即可

{ path: "/article", name: "Blog", component: Blog }

但后面的情况则不同:匹配到Blog组件的地址中,有一部分是动态变化的,则需要使用一种特殊的表达方式:

{ path: "/article/cate/:categoryId", name: "CategoryBlog", component: Blog }

在地址中使用:xxx,来表达这一部分的内容是变化的,在vue-router中,将变化的这一部分称之为params,可以在vue组件中通过this.$route.params来获取

// 访问 /article/cate/3 this.$route.params // { categoryId: "3" }

// 访问 /article/cate/1 this.$route.params // { categoryId: "1" }

动态路由的导航

<router-link to="/article/cate/3">to article of category 3</router-link>

<router-link :to="{

 name: 'CategoryBlog',

 params: {

 categoryId: 3

 }

}">to article of category 3</router-link>

编程式导航

除了使用<RouterLink>超链接导航外,vue-router还允许在代码中跳转页面

this.$router.push("跳转地址"); // 普通跳转

this.$router.push({ // 命名路由跳转

 name:"Blog"

})

this.$router.go(-1); // 回退。类似于 history.go

watch

利用watch配置,可以直接观察某个数据的变化,变化时可以做一些处理

export default {

 // ... 其他配置

 watch: {

 // 观察 this.$route 的变化,变化后,会调用该函数

 $route(newVal, oldVal){

 // newVal:this.$route 新的值,等同 this.$route

 // oldVal:this.$route 旧的值

 },

 // 完整写法

 $route: {

 handler(newVal, oldVal){},

 deep: false, // 是否监听该数据内部属性的变化,默认 false

 immediate: false // 是否立即执行一次 handler,默认 false

 }

 // 观察 this.$route.params 的变化,变化后,会调用该函数

 ["$route.params"](newVal, oldVal){

 // newVal:this.$route.params 新的值,等同 this.$route.params

 // oldVal:this.$route.params 旧的值

 },

 // 完整写法

 ["$route.params"]: {

 handler(newVal, oldVal){},

 deep: false, // 是否监听该数据内部属性的变化,默认 false

 immediate: false // 是否立即执行一次 handler,默认 false

 }

 }

}

弹出消息

使用css module

需要将样式文件命名为xxx.module.ooo

xxx为文件名

ooo为样式文件后缀名,可以是cssless

得到组件渲染的Dom

 /**

 获取某个组件渲染的Dom根元素

 */

function getComponentRootDom(comp, props){

 const vm = new Vue({

 render: h => h(comp, {props})

 })

 vm.$mount();

 return vm.$el;

}

扩展vue实例

Vue.prototype上添加的属性在组件实例和Vue构造函数当中都可以使用

Vue.prototype.sayHello在原型链上添加方法。

ref

<template>

 <div>

 <p ref="para">some paragraph</p>

 <ChildComp ref="comp" />

 <button @click="handleClick">查看所有引用</button>

 </div>

</template>

<script>

 import ChildComp from "./ChildComp"

 export default {

 components:{

 ChildComp

 },

 methods:{

 handleClick(){

 // 获取持有的所有引用

 console.log(this.$refs);

 /*

 {

 para: p元素(原生DOM),

 comp: ChildComp的组件实例

 }

 */

 }

 }

 }

</script>

通过ref可以直接操作dom元素,甚至可能直接改动子组件,这些都不符合vue的设计理念。

除非迫不得已,否则不要使用ref

获取远程数据

vue cli: https://cli.vuejs.org/zh/

axios: https://github.com/axios/axios

mockjs:http://mockjs.com/

意义

服务器从后台页面维护更新数据,前台页面从服务器获取最新数据。

开发环境有跨域问题

生产环境没有跨域问题

解决开发环境跨域问题

为什么要Mock数据

 

 组件的生命周期

实例被创建->注入->编译生成虚拟Dom树->挂载->已挂载(若数据变动,响应式渲染,重新生成虚拟dom树并且·比较差异,将差异应用到真实dom)->完成渲染

生命周期钩子函数

beforeCreate();created();beforeMount();mounted();beforeUpdate();updated();

beforeDestroy();destroyed()

加载远程数据

export default {

 data(){

 return {

 news: []

 }

 },

 async created(){

 this.news = await getNews();

 }

}

直接操作dom

export default {

 data(){

 return {

 containerWidth:0,

 containerHeight:0

 }

 },

 mounted(){

 this.containerWidth = this.$refs.container.clientWidth;

 this.containerHeight = this.$refs.container.containerHeight;

 }

}

启动和清除计时器

export default {

 data(){

 return {

 timer: null

 }

 },

 created(){

 this.timer = setInterval(()=>{

 ...

 }, 1000)

 },

 destroyed(){

 clearInterval(this.timer);

 }

}

自定义指令

定义指令

全局定义:

// 指令名称为:mydirec1

Vue.directive('mydirec1', {

 // 指令配置

})

// 指令名称为:mydirec2

Vue.directive('mydirec2', {

 // 指令配置

})

之后,所有的组件均可以使用mydirec1和mydirec2指令

<template>

 <!-- 某个组件代码 -->

 <div>

 <MyComp v-mydirec1="js表达式" />

 <div v-mydirec2="js表达式">

 ...

 </div>

 <img v-mydirec1="js表达式" />

 </div>

</template>

局部定义:

局部定义是指在某个组件中定义指令,和局部注册组件类似。

定义的指令仅在该组件中有效。

<template>

 <!-- 某个组件代码 -->

 <div>

 <MyComp v-mydirec1="js表达式" />

 <div v-mydirec2="js表达式">

 ...

 </div>

 <img v-mydirec1="js表达式" />

 </div>

</template>

<script>

export default {

 // 定义指令

 directives: {

 // 指令名称:mydirec1

 mydirec1: {

 // 指令配置

 },

 // 指令名称:mydirec2

 mydirec2: {

 // 指令配置

 }

 }

}

</script>

和局部注册组件一样,为了让指令更加通用,通常我们会把指令的配置提取到其他模块。

<template>

 <!-- 某个组件代码 -->

 <div>

 <MyComp v-mydirec1="js表达式" />

 <div v-mydirec2="js表达式">

 ...

 </div>

 <img v-mydirec1="js表达式" />

 </div>

</template>

<script>

 // 导入当前组件需要用到的指令配置对象

 import mydirec1 from "@/directives/mydirec1";

 import mydirec2 from "@/directives/mydirec2";

 export default {

 // 定义指令

 directives: {

 mydirec1,

 mydirec2

 }

 }

</script>

指令配置对象

没有配置的指令,就像没有配置的组件一样,毫无意义

vue支持在指令中配置一些钩子函数,在适当的时机,vue会调用这些钩子函数并传入适当的参数,以便开发者完成自己想做的事情。

常用的钩子函数:

// 指令配置对象

{

 bind(){

 // 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

 },

 inserted(){

 // 被绑定元素插入父节点时调用。

 },

 update(){

 // 所在组件的 VNode 更新时调用

 }

每个钩子函数在调用时,vue都会向其传递一些参数,其中最重要的是前两个参数

// 指令配置对象

{

 bind(el, binding){

 // el 是被绑定元素对应的真实DOM

 // binding 是一个对象,描述了指令中提供的信息

 }

}

binding对象

binding:一个对象,包含以下 property:

  • name:指令名,不包括 v- 前缀。
  • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2

配置简化

比较多的时候,在配置自定义指令时,我们都会配置两个钩子函数

{

 bind(el, bingding){

 

 },

 update(el, bingding){

 

 }

}

这样,在元素绑定和更新时,都能运行到钩子函数

如果这两个钩子函数实现的功能相同,可以直接把指令配置简化为一个单独的函数:

function(el, bingding){

 // 该函数会被同时设置到bind和update中

}

组件混入

// 抽离的公共代码const common = { data(){ return { a: 1, b: 2 }},created(){ console.log("common created");},computed:{ sum(){ return this.a + this.b; } }}

/**

  • 使用comp1,将会得到:
  • common created
  • comp1 created 1 2 3*/const comp1 = {mixins: [common] // 之所以是数组,是因为可以混入多个配置代码created(){console.log("comp1 created", this.a, this.b, this.sum);}}

组件通信

props

父组件到子组件的属性传递;一般配合v-for来使用,v-for循环属性内的内容。可以设置约束类型“type”;默认值“default:...",子组件没有权利更改。

event

子组件抛出事件到父组件

使用vue实例方法,$emit();

$refs

直接获取某一个组件,即父组件直接获取子组件的引用

router

通过路由,间接在不同组件之间通信

$listeners

获取从父组件传来的所有事件函数

v-model

v-model是一个语法糖,它是value属性和input事件的结合体

事件总线

两个页面之间没有任何引入和被引入关系的情况下能够保持组件通信

文章数据逻辑

有一个后台页面,其中有一个页面就是添加文章或编辑文章,会让我们写markdown,后台页面同时具有一些功能,能够将markdown转换为html,我们还得把markdown的目录提取出来形成一个TOC数组;

将markdown和html代码以及TOC数组一并提交到服务器上,服务器会将html和toc返回到前台页面,来进行展示

BlogDetail

该组件没有任何难度,根据「属性 - 文章对象」显示出文章信息即可

由于文章的内容属于原始html,因此需要使用v-html指令来设置

另外,文章的内容是不带样式的,因此需要选择一款markdowncss样式(见附件markdown.css

对于文章中脚本部分的样式,可以使用第三方库highlight.js中提供的样式

import "highlight.js/styles/github.css";

事件修饰符

针对dom节点的原生事件vue支持多种修饰符以简化代码

详见:事件修饰符、按键修饰符、系统修饰符

$listeners

$listenersvue的一个实例属性,它用于获取父组件传过来的所有事件函数

<!-- 父组件 --> <Child @event1="handleEvent1" @event2="handleEvent2" />

// 子组件 this.$listeners // { event1: handleEvent1, event2: handleEvent2 }

$emit$listeners通信的异同

相同点:均可实现子组件向父组件传递消息

差异点:

  • $emit更加符合单向数据流,子组件仅发出通知,由父组件监听做出改变;$listeners则是在子组件中直接使用了父组件的方法
  • 调试工具可以监听到子组件$emit的事件,但无法监听到$listeners中的方法调用。(想想为什么)
  • 由于$listeners中可以获得传递过来的方法,因此调用方法可以得到其返回值。但$emit仅仅是向父组件发出通知,无法知晓父组件处理的结果

对于上述中的第三点,可以在$emit中传递回调函数来解决

父组件:

<template>

 <Child @click="handleClick" />

</template>

<script>

 import Child from "./Child"

 export default {

 components:{

 Child

 },

 methods:{

 handleClick(data, callback){

 console.log(data); // 得到子组件事件中的数据

 setTimeout(()=>{

 callback(1); // 一段时间后,调用子组件传递的回调函数

 }, 3000)

 }

 }

 }

</script>

子组件:

<template>

<button @click="handleClick">

 click

 </button>

</template>

<script>

 export default {

 methods:{

 handleClick(){

 this.$emit("click", 123, (data)=>{

 console.log(data); // data为父组件处理完成后得到的数据

 })

 }

 }

 }

</script>

v-model

v-model指令实质是一个语法糖,它是value属性和input事件的结合体

<input :value="data" @input="data=$event.target.value" />

:value="data"绑定输入框的值,@input="data=$event.target.value"注册事件,当输入内容变化时更改数据

<!-- 等同于 -->

<input v-model="data" />

数据共享

在vue中遇到数据共享会有一下问题

如何保持数据的唯一性

如果数据不唯一,就会浪费大量内存资源,降低运行效率

如果数据不唯一,就会导致数据不统一,难以维护

如果某个组件改变了数据,如何让其他使用该数据的组件知道数据改变了

事件总线貌似可以解决该问题,但需要在组件中手动的维护监听,极其不方便,而且事件总线的目的在于「通知」,而不是「共享数据」

一种比较容易想到的方案,就是把所有的共享数据全部提升到根组件,然后通过属性不断下发,当某个组件需要修改数据时,又不断向上抛出事件,直到根组件完成对数据的修改。

这种方案的缺陷也非常明显:

  • 需要编写大量的代码层层下发数据,很多组件被迫拥有了自己根本不需要的数据
  • 需要编写大量的代码层层上抛事件,很多组件被迫注册了自己根本处理不了的事件

基于上面的问题,我们可以简单的设置一个独立的数据仓库。

  • 组件需要什么共享数据,可以自由的从仓库中获取,需要什么拿什么。
  • 组件可以自由的改变仓库中的数据,仓库的数据变化后,会自动通知用到对应数据的组件更新

要实现这一切,可以选择vuex

创建仓库

安装vuex后,可以通过下面的代码创建一个数据仓库,在大部分情况下,一个工程仅需创建一个数据仓库

import Vuex from "vue"; import Vue from "vue"; Vue.use(Vuex);
// 应用vuex插件
const store = new Vuex.Store({   
// 仓库的配置  
state: {
// 仓库的初始状态(数据)    
count: 0   } })
  export default store;

仓库创建好后,你可以使用store.state来访问仓库中的数据

如果希望在vue中方便的使用仓库数据,需要将vuex作为插件安装

// store.js
import Vuex from "vue";
import Vue from "vue";
Vue.use(Vuex); // 安装Vuex插件
const store = new Vuex({
  // 仓库的配置
  state: { // 仓库的初始状态(数据)
    count: 0
  }
})

export default store;

// main.js
import Vue from "vue";
import App from "./App.vue";
import store from "./store.js";
new Vue({
  store, // 向vue中注入仓库
  render: h => h(App)
}).$mount("#app");

之后,在vue组件中,可以通过实例的$store属性访问到仓库

Vuex会自动将配置的状态数据设置为响应式数据,当数据变化时,依赖该数据的组件会自动渲染。

数据变更

尽管可以利用数据响应式的特点直接变更数据,但这样的做法在大型项目中会遇到问题

如果有一天,你发现某个共享数据是错误的,而有一百多个组件都有可能变更过这块数据,你该如何知道是哪一步数据变更出现了问题?

为了能够更好的跟踪数据的变化,vuex强烈建议使用mutation来更改数据

const store = new Vuex({
  // 仓库的配置
  state: { // 仓库的初始状态(数据)
    count: 0
  },
  mutations: {
    /**
     * 每个mutation是一个方法,它描述了数据在某种场景下的变化
     * increase mutation描述了数据在增加时应该发生的变化
     * 参数state为当前的仓库数据
     */
    increase(state){
      state.count++;
    },
    decrease(state){
      state.count--;
    },
    /**
     * 求n次幂
     * 该mutation需要一个额外的参数来提供指数
     * 我们把让数据产生变化时的附加信息称之为负荷(负载) payload
     * payload可以是任何类型,数字、字符串、对象均可
     * 在该mutation中,我们约定payload为一个数字,表示指数
     */
    power(state, payload){
      state.count **= payload;
    }
  }
})

当我们有了mutation后,就不应该直接去改动仓库的数据了

而是通过store.commit方法提交一个mutation,具体做法是

store.commit("mutation的名字", payload);

现在,我们可以通过vue devtools观测到数据的变化了

**特别注意: **

  1. mutation中不得出现异步操作在实际开发的规范中,甚至要求不得有副作用操作

副作用操作包括:

    • 异步
    • 更改或读取外部环境的信息,例如localStorage、location、DOM
  1. 提交mutation是数据改变的唯一原因

异步处理

如果在vuex中要进行异步操作,需要使用action

const store = new Vuex({
  state: {
    count: 0
  },
  mutations: {
    increase(state){
      state.count++;
    },
    decrease(state){
      state.count--;
    },
    power(state, payload){
      state.count **= payload;
    }
  },
  actions: {
    /**
     * ctx: 类似于store的对象
     * payload: 本次异步操作的额外信息
     */
    asyncPower(ctx, payload){
      setTimeout(function(){
        ctx.commit("power", payload)
      }, 1000)
    }
  }
})

用户模块逻辑示意图

参考资料

vue

watch配置

Vue.prototype.$watch

vuex

mapState

getters

mapGetters

modules

watch

router

exact-path

导航前置守卫

导航守卫

打包优化

分析打包结果

由于vue-cli是利用webpack进行打包,我们仅需加入一个webpack插件webpack-bundle-analyzer即可分析打包结果

为了避免在开发环境中启动webpack-bundle-analyzer,我们仅需使用以下代码即可,创建一个webpack.config.js文件

const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")

 .BundleAnalyzerPlugin;

// vue.config.js

module.exports = {

 // 通过 configureWebpack 选项,可对 webpack 进行额外的配置

 // 该配置最终会和 vue-cli 的默认配置进行合并(webpack-merge)

 configureWebpack: {

 plugins: [new BundleAnalyzerPlugin()]

 },

};

优化公共库打包体积

使用CDN

CDN全称为Content Delivery Network,称之为内容分发网络

它的基本原理是:架设多台服务器,这些服务器定期从源站拿取资源保存本地,到让不同地域的用户能够通过访问最近的服务器获得资源

我们可以把项目中的所有静态资源都放到CDN上(收费),也可以利用现成免费的CDN获取公共库的资源

首先,我们需要告诉webpack不要对公共库进行打包

// vue.config.js

module.exports = {

 configureWebpack: {

 externals: {

 vue: "Vue",

 vuex: "Vuex",

 "vue-router": "VueRouter",

 }

 },

};

然后,在页面中手动加入cdn链接,这里使用bootcn

<body>

 <div id="app"></div>

 <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.12/vue.min.js"></script>

 <script src="https://cdn.bootcdn.net/ajax/libs/vuex/3.5.1/vuex.min.js"></script>

 <script src="https://cdn.bootcdn.net/ajax/libs/vue-router/3.4.7/vue-router.min.js"></script>

 <!-- built files will be auto injected -->

</body>

对于vuexvue-router,使用这种传统的方式引入的话会自动成为Vue的插件,因此需要去掉Vue.use(xxx)

我们可以使用下面的代码来进行兼容

// store.js

import Vue from "vue";

import Vuex from "vuex";

if(!window.Vuex){

 // 没有使用传统的方式引入Vuex

 Vue.use(Vuex);

}

// router.js

import VueRouter from "vue-router";

import Vue from "vue";

if(!window.VueRouter){

 // 没有使用传统的方式引入VueRouter

 Vue.use(VueRouter);

}

启用现代模式

为了兼容各种浏览器,vue-cli在内部使用了@babel/present-env对代码进行降级,你可以通过.browserlistrc配置来设置需要兼容的目标浏览器

这是一种比较偷懒的办法,因为对于那些使用现代浏览器的用户,它们也被迫使用了降级之后的代码,而降低的代码中包含了大量的polyfill,从而提升了包的体积

因此,我们希望提供两种打包结果:

  1. 降级后的包(大),提供给旧浏览器用户使用
  2. 未降级的包(小),提供给现代浏览器用户使用

除了应用webpack进行多次打包外,还可以利用vue-cli给我们提供的命令:

vue-cli-service build --modern

优化项目包体积

这里的项目包是指src目录中的打包结果

页面分包

默认情况下,vue-cli会利用webpacksrc目录中的所有代码打包成一个bundle

这样就导致访问一个页面时,需要加载所有页面的js代码

我们可以利用webpack动态import的支持,从而达到把不同页面的代码打包到不同文件中

// routes

export default [

 {

 name: "Home",

 path: "/",

 component: () => import(/* webpackChunkName: "home" */ "@/views/Home"),

 },

 {

 name: "About",

 path: "/about",

 component: () => import(/* webpackChunkName: "about" */"@/views/About"),

 }

];

优化首屏响应

首页白屏受很多因素的影响

vue页面需要通过js构建,因此在js下载到本地之前,页面上什么也没有

一个非常简单有效的办法,即在页面中先渲染一个小的加载中效果,等到js下载到本地并运行后,即会自动替换

<div id="app">

 <img src="loading.gif" />

</div>

猜你喜欢

转载自blog.csdn.net/Vince_13/article/details/132432968