Vue.js学习总结

作者: 酥枫 | 来源:发表于2018-10-14 13:23 被阅读41次
    • Vue不支持IE8以及以下版本。
    • 想要使用Vue的话可以通过直接下载vue.js,放置到项目中写好路径就可以,或者直接通过CDN引入https://cdn.jsdelivr.net/npm/vue/dist/vue.js,当然,在构建大型应用时推荐使用NPM安装(目前还没用到,用到再说)
    • Vue是一个MVVM框架
    • 一个简单的例子:

    html:

    <div id="app1">
      {{ message }}
    </div>
    

    js:

    var app=new Vue({
        el:"#app1",
        data:{
            message:'Hello world'
        }
    });
    

    上面的代码就相当于把id为app1的div元素和一个Vue实例绑定了起来(通过传入Vue的对象的el属性来和html元素绑定)。这时候Vue将数据和DOM建立了关联,是响应式的,比如可以通过打开页面之后打开页面的控制台,手动修改app1.message就可以看到页面上自动更新了。

    上面html中的这种绑定方式是使用“Mustache”语法 (双大括号) 的文本插值(Mustache就是胡子的意思)

    • v-bind是一个指令,可以单向绑定DOM和数据,说是单向其实就是说数据变了之后DOM的value值和显示也会变化,但是DOM的value变了之后(例如在input元素中),数据不会改变。例子:

    html:

    <div id="app2">
      <span v-bind:title="message">
        鼠标悬停几秒钟查看此处动态绑定的提示信息!
      </span>
    </div>
    

    js:

    var app2 = new Vue({
      el: '#app2',
      data: {
        message: 'This is a message'
      }
    });
    

    这样当鼠标悬停在span元素上时,就会看到这段message。在控制台中修改app2.message之后也会立刻在DOM上反映出来。

    • 创建Vue实例是通过Vue(构造)函数来创建的,通过new关键字,Vue构造函数接受一个参数,该参数是一个对象,称为选项对象,可以直接在创建时候写一个对象,也可以先创建好一个对象再传入,即下面两种方式均可:
    var vm = new Vue({
      el: '#some-id',
      data: {
        message: 'This is a message'
      }
    });
    
    var obj = {
        el: '#some-id',
        data: {
            message: 'This is a message'
        }
    }
    var vm = new Vue(obj);
    

    上面两种方式均可,不过若是第二种方式,需要对象中的属性名(key)与标准的选项对象中的属性名一样,即名字必须为eldatamethods等。

    • 选项对象中的属性分别为:
      • el
        绑定DOM元素,是一个字符串类型,格式为'#id'
      • data
        用于存放属性、数据等,也是一个对象,数据都存在data对象内部
      • methods
        用于存放方法,DOM事件处理函数,是一个对象,函数方法存在对象内部
      • computed
        存放计算属性,所谓计算属性,就是逻辑比较多的属性,是一个对象,计算属性存放在computed对象内部,同时,计算属性本身是一个函数,函数内部是计算的逻辑。
      • watch
        侦听器,存放要侦听的数据,是一个对象,属性的名字(key)为要侦听的数据,属性的值为一个函数,即数据发生变化时要执行的回调函数。
      • 等等

    上面这些属性,都会被初始化为Vue实例的属性,并且是绑定的是响应式的,即:

    // 我们的数据对象
    var data = { a: 1 }
    
    // 该对象被加入到一个 Vue 实例中
    var vm = new Vue({
      data: data
    })
    
    // 获得这个实例上的属性
    // 返回源数据中对应的字段
    vm.a === data.a // => true
    
    // 设置属性也会影响到原始数据
    vm.a = 2
    data.a // => 2
    
    // ……反之亦然
    data.a = 3
    vm.a // => 3
    

    值得注意的是只有当实例被创建时 data 中存在的属性才是响应式的。也就是说如果你添加一个新的属性,比如:

    vm.b = 'hi'
    

    那么对 b 的改动将不会触发任何视图的更新。如果你知道你会在晚些时候需要一个属性,但是一开始它为空或不存在,那么你仅需要设置一些初始值,如:

    data: {
      newTodoText: '',
      visitCount: 0,
      hideCompletedTodos: false,
      todos: [],
      error: null
    }
    

    在使用Object.freeze()冻结传入的选项对象之后,Vue的响应系统无法再追踪变化。

    当然,如果要想访问选项对象的属性,Vue提供了前缀$

    var data = { a: 1 }
    var vm = new Vue({
      el: '#example',
      data: data
    })
    
    vm.$data === data // => true
    vm.$el === document.getElementById('example') // => true
    
    // $watch 是一个实例方法
    vm.$watch('a', function (newValue, oldValue) {
      // 这个回调将在 `vm.a` 改变后调用
    })
    
    • Vue的模板语法:
      • 常规的Mustache语法,例如插入到pspan标签内
      • 如果想要插入原始html,则需要使用v-html指令
      • 如果想要为DOM元素绑定属性(或者称为特性,例如src、style、class或者其他自定义的特性),那么就不能用Mustache语法了,需要用v-bind指令,如:<div v-bind:title="myTitle"></div>v-bind的特殊之处在于,使用了v-bind之后,后面的双引号中的,就不再是一个字符串了,即这个例子中的div元素的title属性的值不是"myTitle"这个字符串,而是对应的Vue实例(假设名字是vm)中的myTitle这个变量的值,即div的title值为vm.myTitle。所以,如果单纯的想传一个字符串给元素的属性(特性),就不要加v-bind或者前面加:,这点在组件的prop传值的时候也是这样的。
      • Vue的模板语法除了可以插入常规字符串之类的,还可以插入JavaScript表达式,如:

    html:

    {{ number + 1 }}
    
    {{ ok ? 'YES' : 'NO' }}
    
    {{ message.split('').reverse().join('') }}
    
    <div v-bind:id="'list-' + id"></div>
    

    这些表达式会在所属 Vue 实例的数据作用域下作为 JavaScript 被解析。有个限制就是,每个绑定都只能包含单个表达式,所以下面的例子都不会生效:

    html:

    <!-- 这是语句,不是表达式 -->
    {{ var a = 1 }}
    
    <!-- 流控制也不会生效,请使用三元表达式 -->
    {{ if (ok) { return message } }}
    
    • Vue指令都是带有v-前缀的特殊特性。常见指令有以下这些:
      • v-bind
        用于绑定,上面说过了,还有就是组件的prop传值时用。
      • v-on
        用于绑定DOM事件。
      • v-if
        条件判断,条件渲染
      • v-for
      • v-model

    上面的指令中,v-bindv-on可以被简写,如下:

    html:

    <!-- 完整语法 -->
    <a v-bind:href="url">...</a>
    
    <!-- 缩写 -->
    <a :href="url">...</a>
    
    <!-- 完整语法 -->
    <a v-on:click="doSomething">...</a>
    
    <!-- 缩写 -->
    <a @click="doSomething">...</a>
    
    • 计算属性,函数返回一个值:

    html:

    <div id="example">
      <p>Original message: "{{ message }}"</p>
      <p>Computed reversed message: "{{ reversedMessage }}"</p>
    </div>
    

    js:

    var vm = new Vue({
      el: '#example',
      data: {
        message: 'Hello'
      },
      computed: {
        // 计算属性的 getter
        reversedMessage: function () {
          // `this` 指向 vm 实例
          return this.message.split('').reverse().join('')
        }
      }
    })
    

    运行结果为:

    Original message: "Hello"

    Computed reversed message: "olleH"

    计算属性也可以用vm.reversedMessagge来访问,并且它的值始终依赖于vm.message,后者改变前者也响应式地改变。

    使用计算属性的地方也可以通过调用方法来实现,例如:

    html:

    <p>Reversed message: "{{ reversedMessage() }}"</p>
    

    js:

    methods: {
      reversedMessage: function () {
        return this.message.split('').reverse().join('')
      }
    }
    

    我们可以将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是完全相同的。然而,不同的是计算属性是基于它们的依赖进行缓存的。计算属性只有在它的相关依赖发生改变时才会重新求值。这就意味着只要 message还没有发生改变,多次访问 reversedMessage 计算属性会立即返回之前的计算结果,而不必再次执行函数。相比之下,每当触发重新渲染时,调用方法将总会再次执行函数。

    我们为什么需要缓存?假设我们有一个性能开销比较大的计算属性 A,它需要遍历一个巨大的数组并做大量的计算。然后我们可能有其他的计算属性依赖于 A 。如果没有缓存,我们将不可避免的多次执行 A 的 getter!如果你不希望有缓存,请用方法来替代。

    计算属性处理getter也可以设置setter:

    // ...
    computed: {
      fullName: {
        // getter
        get: function () {
          return this.firstName + ' ' + this.lastName
        },
        // setter
        set: function (newValue) {
          var names = newValue.split(' ')
          this.firstName = names[0]
          this.lastName = names[names.length - 1]
        }
      }
    }
    // ...
    
    • 侦听器的使用方式为:
    new Vue({
        el:'#id',
        data:{
            str:'some message'
        },
        watch:{
            str:function(oldStr,newStr){
                //...
            }
        }
    });
    
    • class绑定
      绑定DOM元素的class就是前面说到的用v-bind,但是在绑定classstyle时,可以传入对象和数组:

    html:

    <div class="static"
         v-bind:class="{ active: isActive, 'text-danger': hasError }">
    </div>
    

    js:

    data: {
      isActive: false,
      hasError: true
    }
    

    在上面的例子中,activetext-danger这两个class是否存在取决于对应的Vue实例中的属性isActivehasError的真值,同时在Vue中动态的class可以和普通的class并存,所以前面可以还有一个class,所以上面的渲染结果就是:

    html:

    <div class="static text-danger"></div>
    

    class绑定的数据对象不一定非要内联在模板中,也可以直接就是一个对象,还可以是一个计算属性:

    html:

    <div v-bind:class="classObject"></div>
    

    js:

    data: {
      classObject: {
        active: true,
        'text-danger': false
      }
    }
    

    除此之外,还可以是一个数组(甚至数组中还可以内嵌对象模板):

    html:

    <div v-bind:class="[{ active: isActive }, errorClass]"></div>
    

    js:

    data: {
      isActive: true,
      errorClass: 'text-danger'
    }
    

    渲染为:

    html:

    <div class="active text-danger"></div>
    

    如果是在自定义组件上,这些类会被添加到该组件的根元素上,这个元素上已经存在的类不会被覆盖:
    js:

    Vue.component('my-component', {
      template: '<p class="foo bar">Hi</p>'
    })
    

    html:

    <my-component class="baz boo" v-bind:class="{ active: isActive }"></my-component>
    

    如果isActive的真值为真,则最终被渲染为:

    html:

    <p class="foo bar baz boo active">Hi</p>
    
    • 绑定内联样式
      绑定CSS样式时,也是用上面提到的v-bind指令,然后让DOM元素的style属性(特性)等于一个对象或者数组即可:

    html:

    <div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
    

    js:

    data: {
      activeColor: 'red',
      fontSize: 30
    }
    

    或者直接绑定到一个样式对象通常更好,这会让模板更清晰:

    html:

    <div v-bind:style="styleObject"></div>
    

    js:

    data: {
      styleObject: {
        color: 'red',
        fontSize: '13px'
      }
    }
    

    当然,也可以绑定到一个数组,数组的元素为多个样式对象。

    • v-if
      条件渲染,用法为<h1 v-if="ok">Yes</h1>,表示这个DOM元素对于的Vue实例(假设为vm)中的属性vm.ok为真时渲染h1这个元素。与v-if配合使用的经常有v-elsev-else-if。如果想要用v-if一条指令条件渲染多个元素,那么应该用一个<template>元素包裹起来,最终渲染结果不包括<template>元素:

    html:

    <template v-if="ok">
      <h1>Title</h1>
      <p>Paragraph 1</p>
      <p>Paragraph 2</p>
    </template>
    
    • v-if条件渲染时,Vue会高效地渲染元素,会复用一些已有的元素,而不是从头开始渲染:

    html:

    <template v-if="loginType === 'username'">
      <label>Username</label>
      <input placeholder="Enter your username">
    </template>
    <template v-else>
      <label>Email</label>
      <input placeholder="Enter your email address">
    </template>
    

    这样的话在切换loginType时,不会清除用户已经输入的内容,因为这两个模板使用了相同的元素,<input>不会被替换掉除了placeholder。如果想Vue不要复用,则需要为相同的元素添加不同的key属性来表示这两个元素是完全独立的:

    html:

    <template v-if="loginType === 'username'">
      <label>Username</label>
      <input placeholder="Enter your username" key="username-input">
    </template>
    <template v-else>
      <label>Email</label>
      <input placeholder="Enter your email address" key="email-input">
    </template>
    

    当然了,上面的label还是复用了,因为没有给它添加key属性。

    • v-show功能看起来和v-if差不多,也是可以根据条件来做一些判断,它是根据条件展示元素,而不是渲染元素,和v-if不同的是,v-show的元素始终会被渲染并被保留在DOM中,v-show只是简单地切换元素的CSS属性dispaly。而且v-show也不支持templatev-else
    • v-if是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。

    v-if也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

    相比之下,v-show就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于CSS进行切换。

    一般来说,v-if有更高的切换开销,而v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用v-show 较好;如果在运行时条件很少改变,则使用v-if较好。

    • v-ifv-for一起使用时,v-for的优先级更高。
    • 对于选择框元素来说(select包围着option),select元素的值(value)是选中的optionvalue,如果option没有value属性,那么option的值就是option开始和结束标签之间的值
    • 在事件处理时,子组件抛出的事件的名称只能是kebab-case的(其实是建议用kebab-case),而不能是camelCased的,因为HTML是大小写不敏感的,所以在DOM模板中使用camelCase会自动全部转换为小写,所以监听不到在组件定义中抛出的事件。
    • 在组件上使用v-model时,子组件抛出的事件不能使用自定义的名称
    • 有些 HTML 元素,诸如<ul><ol><table><select>,对于哪些元素可以出现在其内部是有严格限制的。而有些元素,诸如<li><tr><option>,只能出现在其它某些特定的元素内部。

    这会导致我们使用这些有约束条件的元素时遇到一些问题。例如:

    <table>
      <blog-post-row></blog-post-row>
    </table>
    

    这个自定义组件<blog-post-row>会被作为无效的内容提升到外部,并导致最终渲染结果出错。幸好这个特殊的is特性给了我们一个变通的办法:

    <table>
      <tr is="blog-post-row"></tr>
    </table>
    
    • 在对组件传prop值时,如果传入的值是数组、数字、布尔值或者对象这些,都需要用v-bind来传,因为使用了v-bind之后,后面的双引号中就不再是一个字符串,而是一个JavaScript表达式,这点在上面也提到过。

    • 自定义组件的v-model

    • 组件的prop在js代码中可以使用驼峰式命名,然后在对应的HTML传值的时候要使用kebab case。而在组件的抛出事件中,事件名称始终建议使用kebab case,不论是在js中还是在html中。

    • [x] 非prop的属性(inheritAttrs: false$attrs合用)

      非prop的属性就是组件没有相应的prop定义,但是却传给了该组件的那些特性,例如:

      <component-a
      prop-a="xxx"
      class="yyy"
      style="color:#fff;"
      ></component-a>
      

      在上面的例子中,除了classstyle会被合并到组件模板中(根元素上)对应的classstyle,模板中(根元素上)的propA会被覆盖掉。如果不想被覆盖掉可以在组件定义中添加inheritAttrs: false选项来禁用特性继承。同时inheritAttrs: false可以和$attrs合用,来指定将非prop特性传给模板中的那个元素而不必担心模板根元素是哪个。详见非prop的特性

    • vm.$attrs:包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用。

    • [x] 将原生事件绑定到组件

      使用.native修饰符可以将原生DOM事件直接添加到组件上,而不用添加到组件模板上再抛出,例如:

      <base-input v-on:focus.native="onFocus"></base-input>
      

      这个时候focus事件会被添加到base-input组件模板的根元素上。详见将原生事件绑定到组件

    • [x] 作用域插槽

      对于作用域插槽可以理解为子组件可以向父组件传递参数,在父组件中使用template标签,这个标签有个scope属性(在Vue2.5之后不建议用这个属性了,换成了slot-scope),这个属性可以获得子组件中的slot的prop,是一个对象,可以被重新命名,即scope="aNewName",之后就可以通过aNewName来访问子组件的prop了。注意,如果插槽是具名插槽,那么父组件中要想获得插槽prop,那么就必须指明slot,如slot="slot1";如果插槽是匿名的,那么就可以直接使用slot-scope属性来获取prop。

    • [ ] 异步组件

    • [ ] 处理边界情况

    • 直接在HTML标签中添加v-on绑定原生事件的时候,$event可以用来访问原生DOM事件,如

      html:

      <a href="" @click="handleClick('禁止打开',$event)">some text</a>
      

      js:

      methods:{
          handlleClick:function(message,event){
              event.preventDefault();
              window.alert(message);
          }
      }
      

      通过$emit子组件抛出事件,$event则代表传递的参数。

      简单来说,就是方法名中传入$event表示原生dom事件(虽然在实际开发中发现,就算不传$event参数,也可以在方法内部访问到dom事件event),在$emit抛出事件时,父组件(在事件处理函数不是一个方法的情况下)可以通过$event访问$emit传入的第一个参数($emit('my-event',0.1,0.2),类似于这样,父组件直接通过v-on来监听,在事件处理不是一个方法的情况下,通过$event可以访问到0.1这个参数)

    • 组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件(即抛出的事件名为input),但是可以在定义组件时添加一个model选项来更改prop名和event名

    • Vue.extend()接收一个包含组件选项的对象的参数,相当于是创建了Vue的一个带有一些参数的子类,可以用new关键字来创建一个组件实例,之后可以手动挂载到DOM元素上

    • Vue在组件中定义的组件data选项中的数据不是响应式更新的

    • 可以给事件中心(eventHub或者称为bus)添加data、methods、computed等选项来扩展bus示例,这些都是可以共用的。

    • $event详解

      <button @click="funcA">click here</button>
      
      methods:{
          funcA(e){
              console.log(e);//可以访问到event事件
          }
      }
      

      @click绑定一个方法(不加括号)调用时,可以直接在funcA中访问到event事件

      <button @click="funcA('stringstring')">click here</button>
      
      methods:{
          funcA(message){
              console.log(message);
              console.log(e);//访问不到event事件
          }
      }
      

      但是像下面这样:

      <button @click="funcA('stringstring')">click here</button>
      
      methods:{
          funcA(message){
              console.log(message);
              console.log(event);//可以访问到event事件
          }
      }
      

      或者这样:

      <button @click="funcA('stringstring',$event)">click here</button>
      
      methods:{
          funcA(message,e){
              console.log(message);
              console.log(e);//也可以访问到event事件
          }
      }
      
      1. 子组件通过$emit抛出事件时,除去第一个参数(因为第一个参数是抛出的事件的名字,例如my-click),剩下的参数都可以在父组件的对应方法中的参数列表接收到。如果父组件的事件处理函数是一个表达式,那么就只能通过$event访问到子组件通过$emit抛出的参数中的第一个值。
    • 可以直接在vue实例绑定的div(或者其他元素)中写html代码,也可以不在html中写,写在vue实例的template选项中或者通过render函数渲染(以前一直以为只要vue组件才能有templaterender,才知道vue实例也可以),这样也能起到相同的效果,在template或者render中,也可以使用组件,和在html中使用一样。写在html中和写在vue实例中唯一的区别就是,写在vue实例中的模板或者render,最终会替换掉vue实例绑定的那个div(或者其他元素),而不是像往常一样会成为div的子节点

    • Vue中书写模板的几种方法:

      1. 直接写在组件的template标签中,用" ` ` "包围或者用''包围。缺点是书写不方便
      2. 在html文件中写,用<script type="text/x-template" id="xxx"></script>或者<template id="xxx"></template>包围模板,之后在组件的template选项中引入id就行,类似于el选项。缺点是组件模板会直接暴露在网页源代码中
      3. 内联模板。缺点是作用域混乱,难以理解。
      4. 写在单独的html文件中,之后在组件中通过import引入。缺点是需要webpack和babel的支持,无法直接使用
      5. 单文件组件,即.vue文件。缺点也是需要webpack和babel等的支持,但是适合较大的项目

      结论就是,小的项目(不需要使用打包工具时)建议直接写在template选项中,但是在稍微大点的项目中建议使用第4点和第5点的方法,特别是第5点中的单文件组件。

    • Vue的构建方式分为两种,一种是独立构建,一种是运行时构建,这两种构建的区别如下:

      • 独立构建:拥有完整的模版编译功能和运行时调用功能,即拥有模板编译器
      • 运行时构建:只拥有完整的运行时调用功能,即不含模板编译器

      上面的话的意思就是说,独立构建包含模板编译器,可以将template选项编译成render函数,render函数是渲染的关键。基于此,使用运行时构建时,不能出现template选项,index.html中也不要出现模板或者是通过vue-router渲染的route-view,因为此时没有模板编译器。但是有一种情况除外:即webpack+vue-loader情况下单文件组件中出现template是可以的。所以运行时构建只能在.vue文件中使用模板或者在需要使用模板的地方使用render函数。在使用vue-cli生成项目时,会提醒使用哪种构建模式,npm包默认导出的是运行时构建,如果需要使用独立构建,需要在webpack中配置alias,如下:

      resolve:{
          // ...
          alias:{
              'vue$':'vue/dist/vue.js'
          }
          // ...
      }
      

      值得注意的是,通过script标签引入的vue是独立构建方式,通过npm install安装后使用,默认使用的是运行时构建
      以下是官方话术:

      1. 独立构建包括编译和支持 template 选项。 它也依赖于浏览器的接口的存在,所以你不能使用它来为服务器端渲染。
      2. 运行时构建不包括模板编译,不支持 template 选项。运行时构建,可以用 render 选项,但它只在单文件组件中起作用,因为单文件组件的模板是在构建时预编译到 render 函数中,运行时构建只有独立构建大小的 30%,只有 16Kb min+gzip 大小。
    • Vuex中的mapStatemapGettersmapMutations,在传入一个数组时,需要映射的计算属性的名称与stategetters的子节点名称(或者方法名称与mutations的子节点名)相同,这时数组的元素都是字符串,如下:

      computed: mapState([
          // 映射 this.count 为 store.state.count
          'count'
      ])
      

      在传入一个对象时,可以给stategetters中的属性另外起个别名,或者写成计算属性的形式:

       computed: mapState({
          // 箭头函数可使代码更简练
          myCount: state => state.count,
          
          // 传字符串参数 'count' 等同于 `state => state.count`,相当于给count起了个别名叫countAlias
          countAlias: 'count',
          
          // 为了能够使用 `this` 获取局部状态,必须使用常规函数
          countPlusLocalState (state) {
              return state.count + this.localCount
          }
      })
      
    • 如果想要给getter(或者计算属性也可以?)传参,那么就要让getter返回一个函数

    • 通过commitdispach调用mutationsactions中的方法时传值只能传一个,即第一个参数是方法名,第二个参数是要传的值,想要多传几个值的话需要将param全部写到一个对象里当做第二个参数传进去,因为第三个参数是带命名空间的模块内访问全局内容时的options,是一个Object,例如commit('someMutation', null, { root: true })

    • 将store切割成模块之后,导入模块a,那么模块a内的gettersmutationsactions都会自动添加到store实例的gettersmutationsactions中,而模块a的state,则是变成了一个对象,放在store的state中,如下:

      {
          "count": 0,
          "todos": [{"id": 1,"done": false},{"id": 2,"done": true }],
          "a": {
              "countA": 0,
              "anotherState":"xxx"
          }
      }
      
    • 在严格模式中(这里指的是Vuex的严格模式,而不是JavaScript的严格模式),因为不能直接修改state中的值,所以无法正常地使用v-model来绑定返回了state中属性的计算属性,解决方法就是手动实现v-model或者给要绑定是计算属性添加一个setter函数

    相关文章

      网友评论

        本文标题:Vue.js学习总结

        本文链接:https://www.haomeiwen.com/subject/cpvzaftx.html