整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

从零开始学Vue-vue模板和组件

我们在前面的章节中已经了解了如何在屏幕上以文本内容的形式获得输出。在本文中我们将学习如何在屏幕上以HTML模板的形式获得输出。我们先看一段代码好来帮助我们理解。

//index.html
<!DOCTYPE html>
<html lang="en">
 <head>
 <meta charset="UTF-8" />
 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 <meta http-equiv="X-UA-Compatible" content="ie=edge" />
 <title>vue模板和组件</title>
 </head>
 <body>
 <div id="vue_det">
 <h1>姓名 : {{ name }}</h1>
 <div>{{ htmlcontent }}</div>
 </div>
 <script src="https://cdn.jsdelivr.net/npm/vue"></script>
 <script type="text/javascript" src="js/app.js"></script>
 </body>
</html>
//js/app.js+
var vm = new Vue({
 el: '#vue_det',
 data: {
 name: "孙悟空",
 htmlcontent: "<div><h1>Vue Js Template</h1></div>"
 }
})

使用live-server启动项目得到如下结果

因为我们使用了插值,也就是双括号,我们在浏览器中就真实的显示了html内容,这显然和我们想要渲染html是不同的,我们希望它在浏览器能显示html渲染的内容。

v-html

为了解决上面的问题,我们不得不适用v-html指令,只要我们将v-html属性分配给html元素,vue就会知道将其作为html内容输出,我们尝试下:

<div id="vue_det">
 <h1>姓名 : {{ name }}</h1>
 <div v-html="htmlcontent"></div>
 </div>

得到了如下结果

从浏览器调试可以看出

与app.js中填写的html字符串表现得相同

属性分配

我们已经了解了如何将HTML模板添加到DOM。现在,我们将实现如何向现有的HTML元素添加属性。想象一下,我们在HTML文件中有一个图像标记,我们想要分配src属性,举例,直接看代码

 <div id="vue_det">
 <h1>姓名 : {{ name }}</h1>
 <div v-html="htmlcontent"></div>
 <img src="" width="300" height="250" />
 </div>

img标签的src是空的,我们将src放到js的数据对象中

var vm = new Vue({
 el: '#vue_det',
 data: {
 name: "孙悟空",
 htmlcontent: "<div><h1>Vue Js Template</h1></div>",
 imgsrc: './img/img.jpg'
 }
})

然后我们修改index.html

<img src="{{ imgsrc }}" width="300" height="250" />

结果如下

好像哪里不对,其实不是,在vue中,我们绑定属性用v-bind指令

 <img v-bind:src="imgsrc" width="300" height="250" />

可以看到我们浏览器中渲染的html

组件

Vue组件是VueJS的重要功能之一,可以创建自定义元素,可以在HTML中重复使用。让我们使用一个示例并创建一个组件。

//index.html
<!DOCTYPE html>
<html lang="en">
 <head>
 <meta charset="UTF-8" />
 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 <meta http-equiv="X-UA-Compatible" content="ie=edge" />
 <title>vue模板和组件</title>
 </head>
 <body>
 <div id="component_test">
 <testcomponent></testcomponent>
 </div>
 <div id="component_test1">
 <testcomponent></testcomponent>
 </div>
 <script src="https://cdn.jsdelivr.net/npm/vue"></script>
 <script type="text/javascript" src="js/app.js"></script>
 </body>
</html>


//js/app.js
Vue.component('testcomponent', {
 template: '<div><h1>This is coming from component</h1></div>'
});
var vm = new Vue({
 el: '#component_test'
});
var vm1 = new Vue({
 el: '#component_test1'
});

我们创建了两个div,id分别是component_test和component_test1,在app.js中我们创建了两个Vue实例,外加一个Vue组件,要想创建组件,它的语法是

Vue.component('nameofthecomponent',{ // options});

创建组件后,组件的名称将成为自定义元素,并且可以在创建的Vue实例元素中使用相同的名称,在app.js文件中创建的组件中,我们添加了一个模板,我们已为其分配了HTML代码。这是一种注册全局组件的方法,可以将其作为任何vue实例的一部分,我们发现这时候浏览器变成了

组件被赋予自定义元素标记,即<testcomponent> </ testcomponent>。但是,当我们在浏览器中检查相同内容时,我们发现结果没有自定义的元素,如以下屏幕截图所示。

我们也可以将组件作为vue实例的一部分

var vm = new Vue({
 el: '#component_test',
 components:{
 'testcomponent': {
 template : '<div><h1>This is coming from component</h1></div>'
 }
 }
});

这是本地注册组件,组件只是vue实例的一部分。到目前为止我们已经基本组件的实现。现在我们来继续扩展。

// js/app.js
Vue.component('testcomponent', {
 template: '<div v-on:mouseover = "changename()" v-on:mouseout = "originalname();"><h1>Custom Component created by <span id = "name">{{name}}</span></h1></div>',
 data: function () {
 return {
 name: "tom"
 }
 },
 methods: {
 changename: function () {
 this.name = "bob";
 },
 originalname: function () {
 this.name = "tom";
 }
 }
});
var vm = new Vue({
 el: '#component_test'
});
var vm1 = new Vue({
 el: '#component_test1'
});

在上面的app.js文件中,我们添加了一个函数,它返回一个对象。该对象具有name属性,该属性被赋值为'tom'。尽管这里data是函数,我们也可以像直接在Vue实例中使用其属性,此外这里还添加了两个函数,在changename中,我们更改name属性,在originalname中我们将其重置为原始名称,有关事件我们后面在讨论,这段代码的结果是:

因为分配了mouseover和mouseout事件,当鼠标悬停在tom上时,会将tom改成bob

动态组件

使用关键字<component> </ component>创建动态组件,并使用属性绑定,如下

<component v-bind:is = "view"></component>
//index.html
<!DOCTYPE html>
<html lang="en">
 <head>
 <meta charset="UTF-8" />
 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 <meta http-equiv="X-UA-Compatible" content="ie=edge" />
 <title>vue模板和组件</title>
 </head>
 <body>
 <div id="databinding">
 <component v-bind:is="view"></component>
 </div>
 <script src="https://cdn.jsdelivr.net/npm/vue"></script>
 <script type="text/javascript" src="js/app.js"></script>
 </body>
</html>
//app.js
var vm = new Vue({
 el: "#databinding",
 data: {
 view: "component1"
 },
 components: {
 component1: {
 template:
 '<div><span style = "font-size:25;color:red;">Dynamic Component</span></div>'
 }
 }
});

浏览器已显示值:

提:已创建vue项目,未创建请参考 https://www.toutiao.com/article/7398100974524449330/

步骤 1:在项目目录下,安装 Element UI(Element UI 是一个基于 Vue.js 的组件库,它提供了一套为开发者设计和实现用户界面的解决方案。Element UI 提供了大量预设计的组件,如按钮、输入框、选择器等,这可以帮助开发者快速构建应用程序界面。

Element ui的手册网站: https://element-plus.org/zh-CN/guide/installation.html )

操作:在vscode中打开项目根目录,按ctrl+~键打开终端,在终端中输入npm install element-plus --save

步骤2:在 main.js 中引入 Element Plus 和相关的样式(此方式是全局引入即将Element所有的组件引入):

import { createApp } from 'vue';

import App from './App.vue';

import router from './router'; // 导入路由

import ElementPlus from 'element-plus'; // 导入 Element Plus

import 'element-plus/dist/index.css'; // 导入 Element Plus 的 CSS 样式

// 创建 Vue 应用实例

const app = createApp(App);

// 使用路由

app.use(router);

// 使用 Element Plus 插件

app.use(ElementPlus);

// 挂载应用

app.mount('#app');

步骤3: 使用 Element Plus 组件

打开网站的“组件”界面,在左侧选择要添加的组件,如:按钮;在右侧出现各种样式的按钮,点击样式右下角的“<>”显示出源代码,复制源代进行调用。

实操:我们可以在新建一个dome.vue页面,使用一个按钮组件:

(1)创建新页面,选中views右击点击“新建文件”在文件中输入“dome.vue

(2)选择按钮样式,这里我选择success按钮,复制相对应的代码<el-button type="success">Success</el-button>

(3)将代码添加到页面中

<template>

<el-button type="success">Success</el-button>

</template>

<script setup>


</script>

<style>

/* 这里可以添加样式 */

</style>

文分享自华为云社区《DTSE Tech Talk | 6个实例带你解读TinyVue 组件库跨框架技术-云社区-华为云》,作者: 华为云社区精选。

在DTSE Tech Talk 《 手把手教你实现mini版TinyVue组件库 》的主题直播中,华为云前端开发DTSE技术布道师阿健老师给开发者们展开了组件库跨框架的讨论,同时针对TinyVue组件库的关键技术进行了剖析,并通过项目实战演示了一份源码编译出2个不同Vue 框架的组件。最后针对框架间的差异,也给出了相应的技术方案,帮助开发者们实战完成组件库跨框架。

直播链接:https://bbs.huaweicloud.com/live/DTT_live/202404171630.html

一、手把手带你实现mini 版 TinyVue

当前实现组件库的跨框架技术,是提升Web页面开发效率与应用灵活性的重要手段。本次直播的实战环节,用300行代码模拟了 TinyVue 组件库的跨框架实现,开发者可以在mini 版组件库中,复现跨框架及多端适配两大功能。同时通过本期的实操环节,也给开发者呈现一个明确且详尽的实现流程,协助大家更好的理解并掌握跨框架技术并运用到实际工作中。

具体源码可参考: https://github.com/opentiny/mini-tiny-vue

二、为什么要实现组件库跨框架呢?

目前,Vue拥有Vue2和Vue3两大主要分支,它们在开发上并不兼容。Vue2还可以进一步细分为2.6及之前的版本和Vue2.7这两个小分支,其中Vue2.7作为2.6与Vue3之间的过渡版本,在开发上起着桥梁作用。

对于现有项目来讲,如果迁移到Vue3,难免存在API及组件功能不同步的情况,因此迁移过程将存在一定的成本及风险。而在当前的Vue生态中,诸如Antdesign和Element等知名组件库都推出了支持Vue2和Vue3的组件。然而这些官网文档和API却并不通用,这意味着实际上是提供了两个独立的组件库来实现跨框架支持的。

作为致力于实现跨框架的TinyVue组件库,旨在实现跨不同版本的Vue框架兼容性,其独特之处在于采用单份源代码策略,通过智能编译技术,能够同时生成适用于Vue 2.6、2.7版本以及Vue3版本的组件包。这意味着开发者只需维护同一个官方网站,并提供一套标准化的API接口,即可满足多版本Vue用户的需求。这种设计有效地减少了TinyVue组件库的维护成本和未来技术迁移的风险。

三、关键技术剖析

首先以一个button组件为例,组件的左上部分是模板,作为组件的入口,它集成了适配层、renderless逻辑以及theme样式(此处暂不涉及theme部分)。值得注意的是,组件内部并未包含任何逻辑代码,所有逻辑均被抽离至renderless中,这里可以按照下图所示观察其调用关系。

  • 从vue文件(即组件的入口文件)开始,引入了适配层中的setup函数和无状态的renderless函数。setup函数的调用过程中,将包含状态的props和context,以及无状态的纯函数renderless一并传入。
  • 然后进入setup函数内部,适配层中的tools函数会构造一个对象,用于抹平框架之间的差异,并将该对象传递给renderless函数。这样,在renderless函数中,可以放心地引用该对象,而无需担心组件是在vue2还是vue3环境下运行。
  • 接下来调用纯函数renderless。它为每个组件构造一个与当前组件相关联的state和api,这些都是有状态的值。随后,这些状态值被返回给适配层。
  • 最后适配层将这些状态值传递给模板进行绑定。具体而言,state被绑定到模板的数据值上,而api则被绑定到模板的事件上。

整体来看,调用过程就像一个管道,数据从模板开始流动,经过逻辑处理,再流回到模板上。在这个过程中,它流经的适配层巧妙地抹平了框架之间的差异,正是TinyVue跨框架的精妙所在。

四、如何解决框架差异统一,实现跨框架?

1、框架间的差异是什么?

Vue3是一次全新的框架升级,所以它的语法以及内部实现,都发生了很大的变化,这些是在开发跨框架组件库时必须考虑的问题。而在长期的跨框架组件库的开发中,可能会遇到众多的框架差异,具体可以将这些差异归结为2大类:

(1)框架对外差异,直接影响到模板的开发以及某些语法。例如:

  • 模板语法差异
  • 生命周期名称变化
  • 移除了事件修饰符、过滤器、消息订阅
  • v-model 语法糖差异
  • 指令,动画组件的差异

(2)框架内部差异,主要是Vue runtime层面的实现差异。在开发跨框架组件过程中,需要访问组件内部某些变量时可能会遇到,例如:

  • 组件实例的差异
  • Vnode结构的差异
  • 移除了$children, $scopedSlots等

2、 框架差异及应对方案

(1)响应式函数引入包差异:

在Vue 2.6 中引入响应函数

import { reactive, ref, watch, ... } from '@vue/composition-api'

在Vue 3 中引入响应函数

import { reactive, ref, watch, ... } from 'vue'

解决方案:通过在适配层暴露一个hooks变量,统一响应式函数的访问,代码如下

// adapter/vue2/index.js

import * as hooks from '@vue/composition-api'
// adapter/vue3/index.js

import * as hooks from 'vue'
// adapter/index.js

export { hooks }

(2)VNode和 h 函数的差异:

在Vue 2.6中,渲染函数的 VNode 参数结构

{

  staticClass: 'button',

  class: { 'is-outlined': isOutlined },

  staticStyle: { color: '#34495E' },

  style: { backgroundColor: buttonColor },

  attrs: { id: 'submit' },

  domProps: { innerHTML: '' },

  on: { click: submitForm },

  key: 'submit-button'

}

在Vue 3 中,渲染函数的 VNode 参数结构是扁平的

{

  class: ['button', { 'is-outlined': isOutlined }],

  style: [{ color: '#34495E' }, { backgroundColor: buttonColor }],

  id: 'submit',

  innerHTML: '',

  onClick: submitForm,

  key: 'submit-button'

}

解决方案:通过在适配层暴露一个h函数,让Vue3框架也能支持Vue2的参数格式。这样就能统一h 函数的用法,同时让在Vue2时期开发的组件在Vue3框架下兼容运行。

// adapter/vue2/index.js

const h = hooks.h
// adapter/vue3/index.js 

const h = (component, propsData, childData) => {

  // 代码有省略...... 

  let props = {}

  let children = childData
  if (propsData && typeof propsData === 'object' && !Array.isArray(propsData)) {

    props = parseProps(propsData)

    propsData.scopedSlots && (children = propsData.scopedSlots)

  } else if (typeof propsData === 'string' || Array.isArray(propsData)) {

    childData = propsData

  }

  return hooks.h(component, props, children)

}
// adapter/index.js

export { h }

(3)v-model的差异:

在Vue 2.6中,在组件上使用 v-model 相当于绑定 value 属性和 input 事件

  <ChildComponent v-model="pageTitle" />

  <!-- 会编译为: -->

  <ChildComponent :value="pageTitle" @input="pageTitle = $event" />

在Vue 3 中,v-model 相当于绑定了 modelValue 属性和 update:modelValue 事件

  <ChildComponent v-model="pageTitle" />

  <!-- 会编译为: -->

  <ChildComponent :modelValue="pageTitle" @update:modelValue="pageTitle = $event" />

解决方案:通过Vue2中声明 model的option 选项,来自定义Vue2框架下v-model 的默认绑定 prop 和 event 。

defineComponent({

  model: {

    prop: 'modelValue', // 默认值为 value

    event: 'update:modelValue' // 默认值为 input

  },

  props: {

    modelValue: String

  } // ...

})

(4)slots的差异:

在Vue 2.6中,有普通插槽 slots 和 作用域插槽 scopedSlots

// 普通插槽为对象,可以直接使用

this.$slots.mySlot
// 作用域插槽为函数,要按函数来调用

this.$scopedSlots.header()

在Vue 3 中,统一为 slots 函数的形式

// 将所有 scopedSlots 替换为 slots

this.$slots.header()
// 将原有 slots 改为函数调用方式

this.$slots.mySlot()

解决方案:通过构建一个vm.$slots属性, 来统一2个框架中,访问slots的访问。

// adapter/vue2/index.js

  Object.defineProperties(vm, {

     // ......

    $slots: { get: () => instance.proxy.$scopedSlots },

    $scopedSlots: { get: () => instance.proxy.$scopedSlots },

  })
   // adapter/vue3/index.js

   Object.defineProperties(vm, {

    // ......

    $slots: { get: () => instance.slots },

    $scopedSlots: { get: () => instance.slots },

  })

我们在vm下,还暴露了许多框架runtime层面上的组件属性,用于抹平跨Vue框架的差异。在开发跨框架组件时,要使用vm来访问组件,避免直接访问组件的instance。

// 创建一个Vue2 运行时的兼容 vm 对象

const createVm = (vm, _instance) => {

  const instance = _instance.proxy
  Object.defineProperties(vm, {

    $attrs: { get: () => instance.$attrs },

    $listeners: { get: () => instance.$listeners },

    $el: { get: () => instance.$el },

    $parent: { get: () => instance.$parent },

    $children: { get: () => instance.$children },

    $nextTick: { get: () => hooks.nextTick },

    $on: { get: () => instance.$on.bind(instance) },

    $once: { get: () => instance.$once.bind(instance) },

    $off: { get: () => instance.$off.bind(instance) },

    $refs: { get: () => instance.$refs },

    $slots: { get: () => instance.$scopedSlots },

    $scopedSlots: { get: () => instance.$scopedSlots },

    $set: { get: () => instance.$set }

  })
  return vm

}

// 创建一个Vue3 运行时的兼容 vm 对象

const createVm = (vm, instance) => {
  Object.defineProperties(vm, {

    $attrs: { get: () => $attrs },

    $listeners: { get: () => $listeners },

    $el: { get: () => instance.vnode.el },

    $parent: { get: () => instance.parent },

    $children:{get:()=>genChild(instance.subTree)},

    $nextTick: { get: () => hooks.nextTick },

    $on: { get: () => $emitter.on },

    $once: { get: () => $emitter.once },

    $off: { get: () => $emitter.off },

    $refs: { get: () => instance.refs },

    $slots: { get: () => instance.slots },

    $scopedSlots: { get: () => instance.slots },

    $set: { get: () => $set }

  })
  return vm

}

(5)指令的差异:

Vue3的指令生命周期的名称变化了, 但指令的参数基本不变

解决方案:在开发指令对象时,通过补齐指令周期,让指令对象同时支持Vue2 和 Vue3

(6)动画类型的差异:

解决方案:在全局的动画类名文件中,同时补齐2个框架下的类名,让动画类同时支持Vue2 和 Vue3的Transition组件

// 此处同时写了 -enter \  -enter-from 的类名,所以它同时支持vue2,vue3的 Transition 组件。

.fade-in-linear-enter,

.fade-in-linear-enter-from,

.fade-in-linear-leave-to {

  opacity: 0;

}

在构建TinyVue跨框架组件库的过程中,团队集中攻克了多个Vue框架间的关键差异点,其中六项尤为突出且具有代表性。

开发TinyVue跨框架组件库时,面对Vue2与Vue3的重要区别,我们确立了两个核心原则:一是“求同去异”,即在编写组件时选用两框架都支持的通用语法,如因Vue2不支持多根节点组件而统一采用单根节点设计;二是“兼容并包”,通过构建适配层隐藏框架间的差异,提供统一接口,无需开发者手动判断框架版本,这样他们可以更专注于逻辑开发。在指令对象和动画类名等细节方面,同样贯彻这一简化差异、广泛兼容的理念。

关注#华为云开发者联盟# 点击下方,第一时间了解华为云新鲜技术~

华为云博客_大数据博客_AI博客_云计算博客_开发者中心-华为云