Skip to content

2 组件

约 1831 个字 437 行代码 2 张图片 预计阅读时间 12 分钟

1 生命周期

  • 生命周期函数中 this 指向 Vue 实例对象

生命周期节点(都是钩子函数):

  • 挂载
    • beforeCreate 阶段,初始化生命周期函数与事件,但数据代理还未开始 (不能访问 data & methods)
    • created 数据监测与数据代理执行完毕(可以访问 data & methods 了)
    • beforeMount 开始解析模版,在内存中生成虚拟 DOM(插值语法等指令尚未替换显示) 此时所有针对 DOM 的操作均不生效
    • mounted 真实 DOM 完成挂载
  • 更新:Model -> View 更新
    • beforeUpdate 新数据 + 旧页面
    • updated 新数据 + 新页面
  • 销毁 - vm.$destroy()
    • beforeDestroy 所有 data、methods、指令均可用(数据能更新,但页面不会) 一般在此阶段关闭定时器、取消订阅消息、解绑自定义事件
    • destroyed vm被销毁,界面 DOM 保持最终的静止形态,和组件 vm 失去联系

挂载

一个不用 transition,硬用 Vue 实现透明度变化的 Sample

%%z注意这个骚气的插值形式%%
<h2 :style="{opacity}"> hello world </h2>
<button @click="stop">Stop</button>
new Vue({
    data: {
        opacity: 1
    },
    methods: {
        stop() {
            this.$destroy();           // 销毁Vue实例,阻止继续变换 opacity
            // 此时还没有关闭 timer
        }
    },
    mounted() { // 挂载完毕 - 解析完成并把初始真实DOM放入页面
        this.timer = setInterval(() => {
            this.opacity -= 0.01;
            if(this.opacity <= 0) this.opacity = 1;
        }, 16)
    },
    beforeDestroy() { // 自杀 or 他杀 都会调用该函数
        clearInterval(this.timer); // 清除定时器,停止透明度变换
    }
})

另外根节点可以啥都不写,内容用 template 进行填充

<div id="root">
</div>
new Vue({
    el: '#root',
    // template 中只能有一个标签(用 div 进行整体性包裹)-> root 将被替换
    template: `
    <div>
        <h2> n = {{n}} </h2>
        <button @click="n++"> n+1 </button>
    </div>
    `
})

2 组件

实现应用中 局部 功能的 代码 & 资源 的集合

  • 组件的本质是由Vue.extend()生成的构造函数 VueComponent
  • 解析组件标签时,Vue 会自动创建该组件的实例对象
  • 在组件配置项中,this 指向 VueComponent

内置关系

  • func.protoype (显式原型)与 entity.__proto__ (隐式原型)均指向同一个“原型对象”

    • VueComponent.prototype.__proto__ === Vue.prototype

      -> 组件原型对象的隐式原型是Vue实例的原型,即 Vue 实例为组件兜底

    • vc 也可以用 vm 上的 数据 & 方法

  • 我们一般通过“显示原型”往里面塞东西,“隐式原型“会自己从里面捞东西

2.1 非单文件组件

一个文件中包含多个组件 不能单独为组件定义样式

  1. 创建组件
    // 创建组件 - 一次一个
    const school = Vue.extend({
        // 不用写 el -> 因为插在别人下面,由宿主的 vm 决定你晚上住哪里
        name: 'xuexiao', // 在开发者工具中呈现的名字(应用时以注册名为主) 
        template: `
        <div>
            <h2>学生姓名:{{name}}</h2>
            <h3>学生年龄:{{age}}</h3>
        </div>
        `,
        // data 必须使用函数式定义 -> 每次使用都会拿到一个新的对象
        data() {
            return {
                name: 'SeaBee',
                age: 12
            }
        }
    })
    
  2. 注册组件
    // 局部注册 - 在使用者的 Vue 实例中进行注册
    new Vue({
        el: '#root',
        components: {
            school: school, // 使用名:创建时的组件名,使用名不应与标签名重复
            'my-school': school // 多单词组件名
            // 其实也可以用 MySchool - 但是需要 Cli
        }
    })
    // 全局注册
    Vue.component('使用名', 创建时的组件名)
    
  3. 使用组件
    // 使用 <使用名> 作为标签,决定组件插入的位置
    <school></school>
    

2.2 组件嵌套

用 school 组件包裹 student 组件(在父组件中注册子组件)

const school = Vue.extend({
    template: `
    <div>
        学生姓名:{{name}}
        <student></student> // 在school的模版中使用
    </div>`,
    data() {
        return { name: 'SeaBee' }
    },
    components: {
        student student
    }
})

2.3 单文件组件

.vue 文件 - 只包含一个组件,一般采用大驼峰命名法 MyVue.vue

可以通过 webpack / vue-cli 转变为 js 文件

  • 定义单文件组件
    <template>
        // 组件结构
    </template>
    
    <script>
        // 组件交互代码 - 数据 & 方法 ...
        // 组件需要暴露才能使用,一般使用“默认暴露”
        export const school = Vue.extend({}) // 分别暴露
        // at end
        export {school};                     // 统一暴露
        export default school;               // 默认暴露
        // 优化为以下形式
        export default {                     // 会默认调用 Vue.extend
            name: '与文件名一致',
            other_options
        };
    </script>
    
    <style>
        // 组件样式
    </style>
    
  • 必须存在的组件 app.vue
    // 用于汇总所有的组件 - 引入所有一级子组件
    <template>
        <div>
            <School></School>
            <Student></Student>
        </div>
    </template>
    
    <script>
        // 引入组件
        import School from './School';   // name - 相对路径
        import Student from './Student'; 
        export default {
            name: 'App',
            components: { // 注册组件
                School,   // 简写,等同于 School:School
                Student
            }
        }
    </script>
    
  • 汇总 main.js
    import App from './App.vue';
    new Vue({
        el: '#root',
        components: { App } // 仅注册 root 组件
    })
    // 随后在 index.html 引入该文件(<body>最下方),添加 <App></App>
    // 或者在本文件添加配置项:template: `<App></App>`
    // 记得还要引入 Vue.js 本身 orz
    

2.4 slot 插槽

默认插槽

  1. 定义插槽

    @ component.vue
    <template>
        <div class="container">
            <h2>这是原本的 Title</h2>
            <slot>如果使用者未传入数据,就显示这句话</slot>
        </div>
    </template>
    

  2. 使用插槽

    @ user.vue
    <template>
        <div>
            <h1>这是父组件</h1>
            <comp>
                <img src="url">
            </comp>
        </div>
    </template>
    slot 内元素的样式可以在 user 中进行定义
    

使用者的 <comp> 中有内容:

  • 有插槽:插槽替换为传入内容
  • 无插槽:传入内容无意义

具名插槽

用于区分多个插槽

@ component.vue
<template>
    <slot name="center">如果使用者未传入数据,就显示这句话</slot>
    <slot name="footer">如果使用者未传入数据,就显示这句话</slot>
</template>
@ user.vue
<template>
    <comp>
        // 使用 slot 属性指定嵌入的插槽
        <img slot="center" src="url">
        // 多个标签挤一个 slot:可以一个个加 name
        <a slot="footer" href="#">我放一个</a>
        <a slot="footer" href="#">甚至两个!</a>
        // 但一般用一个 div/template 包一下 - 方便布局
        <div slot="footer">
            <a href="#">我放一个</a>
            <a href="#">甚至两个!</a>
        </div>
    </comp>
</template>

作用域插槽

不同的 tab 使用同一套数据进行渲染 —— 我们把数据绑给插槽实例

@ component.vue
<template>
    <slot :g="games" msg="hello">如果使用者未传入数据,就显示这句话</slot>
    // 实际上 games 被塞给了使用者定义的 scope 中
</template>
<template>
    <comp> // 想要拿到数据必须使用 template 进行包裹!
        <template scope="s_name"> // 命名作用域(数据在作用域上)
            <h2>{{s_name.msg}}</h2>
            <ul>
                <li v:for="(g,idx) in s_name.games" :key="idx">{{g.name}}</li>
            </ul>
        </template>
    </comp>
    <comp> // 想要拿到数据必须使用 template 进行包裹!
        <template scope="s_name"> // 命名作用域(数据在作用域上)
            <ol>
                <li v:for="(g,idx) in s_name.games" :key="idx">{{g.name}}</li>
            </ol>
        </template>
    </comp>
</template>

3 路由

此处通过 vue-router 插件进行实现

  • 路由是一组 key-value 的对应关系,需要“路由器”进行管理:
    • key = 路径
    • value = function / component
  • 目的:实现单页面(SPA)应用 —— 维护路径,但不刷新整个页面
  • 分类
    • 前端路由 val -> component - 路径改变就显示对应组件
    • 后端路由 val -> function - 服务器收到请求时根据路径匹配处理函数

3.1 基本使用

标签式导航

  1. 安装 vue-router - npm install vue-router@3 (和 vuex 一样的版本对应关系)
  2. 导入并应用
    // @ main.js
    import VueRouter from 'vue-router';    // 引入插件
    import router from './router/index.js' // 引入路由器
    
    Vue.use(VueRouter);
    
    new Vue({
        router: router // 配置路由器
    })
    
  3. src/router/index.js 创建 Router
    import VueRouter from 'vue-router'
    // 引入组件
    import About from '../components/About'
    import Home from '../components/Home'
    
    export default new VueRouter({ // 创建并暴露路由器
        routes: [
            {
                name: 'gunayu',   // 命名路由
                path: '/about',   // 匹配的路径
                component: About  // 满足条件时显示的组件
            },
            {
                path: '/home',
                component: Home
            }
        ]
    })
    
  4. 「标签式导航」将路由与指定组件的点击事件绑定(不能用 <a> 哩)
    <router-link active-class="active" to="/about">To About</router-link>
    - active-class 是在路径被激活时,Vue 动态塞给标签的类名
    
  5. 指定变换组件的呈现位置 <router-view>
    <div>
        <router-view></router-view> - 放一对空标签就行
    </div>
    

编程式导航

  • 事件绑定在 @click
    // 如果需要使用插值语法之类的,需要调用时传参 - @click=pushShow(m)
    
    // push
    pushShow(m) {
        this.$router.push({
            name: 'Xiangqing',
            query: {
                id: m.id,
                title: m.title
            }
        })
    }
    
    // replace
    this.$router.replace({options})
    
    // 回退
    this.$router.back()
    // 前进
    this.$router.forward()
    // + 前进 | - 后退 N 步
    this.$router.go(N)
    

3.2 注意点

  • 组件分类 我们一般把组件分成 路由组件 & 一般组件 两类,前者通过路由规则进行匹配 一般在 src/pages 下放置路由组件,在 src/components 下放置一般组件
  • 没有显示的路由组件被「销毁」了 路由组件会被频繁的 挂载/销毁
  • 每个组件都有自己的 $route 属性,储存自己的路由信息(每个组件不一样)
  • 整个应用只有一个 Router,可以通过 $router 属性获取

3.3 嵌套路由

在最外层的展示区进行一个疯狂套娃

这里演示在 Home 里面套娃 News & Message

  1. 配置嵌套路由
    export default new VueRouter({
        routes: [
            {
                path: "/home", // 一级路由需要加斜杠
                component: Home,
                children: [
                    {
                        path: 'news', // >= 二级路由不用加斜杠
                        component: News
                    },
                    {
                        path: 'message',
                        component: Message
                    }
                ]
            }
        ]
    })
    
  2. 指定二级路由在 Home 组件中的呈现位置
    <template>
        <div class="home">
            <router-view></router-view>
        </div>
    </template>
    
  3. 修改超链接
    <router-link active-class="acitve" to="/home/news">News</router-link>
    - to 中配置的地址需要声明所有的前缀
    

3.4 路由传参

Query 参数

  • 传递参数

    // 字符串写法
    1. 使用 :to 实现 v-bind
    2. 双引号套模版字符串
    3. 需要动态的地方用 ${name}
    <router-link active-class="acitve" :to="`/home/news/detail?id=${m.id}`">{{m.title}}</router-link>
    
    // 对象写法
    <router-link :to="{
        path: '/home/news/detail',
        query: {
            id: m.id,
            title: m.title
        }
    }">
        {{m.title}}
    </router-link>
    
    // 如果对多级路由进行命名的话,可以通过 name 进行跳转(必须使用对象方式)
    <router-link :to="{name: 'xiangqing'}">{{m.title}}</router-link>
    

  • 接收参数 - 通过 this.$route.query.name 使用插值语法

Params 参数

  1. 在 router 中配置占位符

    export default new VueRouter({
        routes: [
            {
                name: 'Xiangqing',
                path: 'detail/:id/:title', // 首个参数为id,随后为title
                component: Detail
            }
        ]
    })
    

  2. 通过 URL 传递参数

    // 字符串形式
    <vue-router :to="`/home/message/detail/${m.id}/${m.title}`">
        Msg66
    </vue-router>
    
    // 对象形式 - 必须使用 name 而非 path
    <vue-router :to="{
        name: 'Xiangqing',
        params: {
            id: m.id,
            title: m.title
        }
    ">
        Msg66
    </vue-router>
    

  3. 通过 this.$route.params.name 获取对应参数

使用「编程式」路由进行跳转但「参数不变」时,会抛出NavigationDuplicated 错误

「声明式」导航不会出现该问题 => vue-router 底层完成了相关处理

=> 因为返回的 Promise 需要返回 res / err 回调,传入相关回调即可解决

this.$router.push({
    name: 'search',
    params: {keyWord: this.keyWord}
}, ()=>{}, ()=>{})
// 可以在 err 中捕获到异常,但是不显示「治标不治本」
// => 在别的组件中调用时还是报错

在组件中:

  • this 指向组件本身,一个 VueComponent 实例
  • this.$router 是一个 VueRouter 实例,由入口文件注册路由时添加
  • push 是 VueRouter 原型对象上的方法
    • 在实例中不存在,借用原型方法
    • 上下文为 VueRouter 的一个实例

下面重写 push 方法:

// @ router.js
// 备份原有的 push 方法
let originPush = VueRouter.prototypr.push
// 重写 push & replace
VueRouter.prototype.push = function(location, resolve, reject) {
    if(resolve && reject) { // 提供了两个回调函数
    // 调用原始 push 函数,篡改上下文
        originPush.call(this, location, resolve, reject)
    } else { // 手工补全缺失的回调
        originPush.call(this, location, ()=>{}, ()=>{})
    }
}
// 对 replace 的处理类似

3.5 props 配置

简化插值语法内容

  1. 在 Router 中配置 props 属性

    // 哪一层路由接收数据,就在哪一层配置 props
    export default new VueRouter({
        routes: [
            {
                path: '',
                component: Comp,
                // 1. 值为 Ob,所有键值对将以 props 形式传递给对应组件
                props: { // 传递的是固定数据
                    a: 1,
                    b: 'hello'
                },
                // 2. 布尔值配置,true - 将所有 params 以 props 形式传给组件
                props: true,
                // 3. 函数配置 - 兼容 query & params
                props($route) {
                    return {
                        id: $route.query.id, 
                        title: $route.params.title,
                    }
                }
            }
        ]
    })
    

  2. 在对应组件中接收

    export default {
        props: ['a', 'b']
        // 随后就可以直接通过 a / b 来接收传入的属性值
    }
    

小结

  • 路由穿参的对象写法能否使用 path + params 形式?

    不能,params 必须结合 name 使用(即便已经占位)

  • 如何指定 params 可传可不传(配置中已经进行占位)?

    要求传递但跑路 => URL 出现问题

    可在对应占位符后添加 ? 表示可选,如 /search/:keyWord?

  • 如何解决 params 参数为 空串 的情况?

    默认情况下会导致 URL 出现问题

    使用 undefined 解决:keyWord: this.keyWord || undefined

    由于空串将被识别为 false,此处将采用 undefined 作为参数值

    => 实际上看起来要配合占位符中的 ? 进行使用

  • 路由组件能否传递 props 参数?

    可以,而且有三种写法:

    1. 布尔值写法

      router 中配置 props: true

      • 未在接收方 props 中声明的 params 参数将出现在 $attrs

      • 反之,出现在接收方的 props 中,可以直接通过变量名进行访问

    2. 对象写法

      // @ router.js
      {
          props: { // 额外向路由组件传递参数
              prop_a: 1,
              prop_b: 'Tom'
          }
      }
      

    3. 函数写法

      可将 query & params 均以 props 形式传递给路由组件

      // @ router.js
      {
          props: ($route) => {
              return {
                  keyWord: $route.params.keyWord,
                  k: $route.query.k
              }
          }
      }
      

3.6 浏览历史记录

  • 每次点击 vue-router 都会触发 push 操作,将目标 URL 压到浏览器的历史记录栈中
  • 采用 replace 操作将使得目标URL替换掉当前栈顶的记录<vue-router replace></vue-router>

3.7 缓存路由组件

让切走的组件不被销毁 —— 在 container 中操作

<keep-alive include="News"> -> 仅保持 News 不被销毁
    「组件名」- 组件中 name 属性的值
    不指定 include,则所有可能的子组件都被缓存
    <router-view></router-view>
</keep-alive>

缓存多个时,可以使用以下写法:
:include=['News', 'Message']

3.8 俩新的钩子

// 因为 keep-alive 后组件不会销毁 => timer 一直在,so
activated() {
    this.timer = setInterval(() => {
        do sth;
    }, 1000)
},
deactivated() {
    clearInterval(this.timer)
}

3.9 路由守卫

满足一定条件才能进行路由跳转

全局前置

在 router 暴露之前添加路由守卫

const router = new VueRouter({}) // 不要直接默认暴露
// 添加全局前置路由守卫 -> 每次发生路由跳转前都会调用
router.beforeEach((to, from, next) => { // 不手动处理 next,则所有人都被阻断
    if(to.path === '' || to.name === '') {     // 跳转目标受限
        if(condition) next() // 符合条件,允许跳转
        else alert('无权限查看!')
    } else {
        next()
    }
})
export default router

全局后置

前置写法得把每一个 path / name 都给列出来 —— 我人先裂开

我们尝试在路由配置中设置一个分量用于标识当前路径是否需要权限校验

=> 最后这个信息被塞在了 meta

const router = new VueRouter({
    routes: [
        {
            path: '/home',
            name: 'jia',
            component: Home,
            meta: {
                isAuth: true // undifined -> false 不用写就行
            }
        }
    ]
})
// 前置守卫(优化版本)
router.beforeEach((to, from, next) => {
    if(to.meta.isAuth) {
        if(ok) next()
    } else {
        next()
    }
})
// 后置守卫 - 切换之后执行,已经跳完了所以没有 next
router.afterEach((to, from) => {
    // 可以联动式修改页面标题 —— 也塞在 meta 里
    to.title = to.meta.title || 'total name'
})

独享路由守卫

// 只限制某个路由 - 只有前置
{
    beforeEnter: (to, from, next) => {} // to == me
}

组件内路由守卫

component.vue 中进行配置

export default {
    // 直接在 template 中引入组件标签时不算
    beforeRouteEnter(to, from, next) { // 通过路由规则进入(了)组件时
        next() // 不写就没法进入组件orz
    },
    beforeRouteLeav(to, from, next) { // 通过路由规则离开组件前
        next() // 不写就没有办法跑路
    }
}

工作模式

  • 哈希模式(默认)
    • # 后的内容作为 hash 值,而非请求路径存在
    • 兼容性好,刷新能行(hash值不会被作为文件路径)
    • URL 通过第三方手机 app 分享可能会被标记为不合法
  • history 模式
    • 需要在 router 中手动配置 mode: 'history'
    • 路径后没有 #,所有都当路径
    • 兼容性差 —— 因为不走网络请求,一刷新就报废(被当成文件路径了)