目录
一、props配置项
1、安装依赖
昨天我们提到项目中的
node_modules文件夹,不需要在传输的时候传给别人,因为他这里都是一些第三方模块
当我们接收到别人的项目文件时,我们需要把这个文件需要的第三方模块进行安装(因为人家没给麻)
我们打开cmd窗口,进入项目路径内,然后执行下方命令
cnpm install
他就会根据项目的需求自行安装第三方模块( package.json 里面有记录)
ps:一些高版本的项目可能看不到node_modules文件夹,因为我们其实不会对内部文件操作,没什么影响。
2、做一个纯净的vue项目
-在router 的index.js 中删除about的路由
-删除所有小组件和about页面组件
-App.vue 只留
<template>
<div id="app">
<router-view/>
</div>
</template>
ps:''会自动匹配在路由中注册过的组件,因此这个不能删除
3、自定义属性之props配置项
我们通过自定义属性,实现父组件往子组件传递数据
子组件在接受数据的时候需要定义props属性用于接收数据
- 方式一:使用数组
只接收变量,不做处理
props:['name']
- 方式二:使用对象
属性认证,需要接收指定类型的数据
props: {name: Number}
- 方式三:使用对象,可以设置默认值和是否必填
props: {
name: {
type: String, //类型
required: true, //必要性(不传会在控制台窗口出现警告信息)
default: '老王' //默认值
}
}
二、mixin(混入)
mixin(混入)
功能:可以把多个组件共用的配置提取成一个混入对象
有两种使用方式:局部混入和全局混入
ps:如果在组件中定义,直接使用即可
前置操作
1 首先需要定义一个混入对象,新建一个mixin包,在他的下方新建index.js
2 在 index.js中写 代码(写组件中会用到的内容,data,methods...等配置项)
export const lqz = {
data() {
return {
age: 199
}
},
methods: {
showName() {
alert(this.name); // 没有this.name
},
},
mounted() {
console.log("你好啊!,页面挂在执行");
},
}
局部混入
局部混入在组件内导入使用
<template>
<div class="home">
<h1>混入的使用</h1>
<button @click="showName">点我,看名字</button>
<!-- 这里的showName方法和age变量,我们没有在当前组件定义,但是导入的混入对象中有定义,因此可以使用 -->
<h2>{{age}}</h2>
</div>
</template>
<script>
// 导入混入对象
import {lqz} from '@/mixin'
'这里用的是命名导入导出'
export default {
name: 'HomeView',
data() {
return {
name: '彭于晏'
}
},
mixins: [lqz]
'这里使用数组存储混入对象,因为可以导入多个混入对象使用'
}
</script>
全局混入
全局混入需要在main.js文件中导入,这样就可以在所有组件中被使用
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import {lqz} from '@/mixin'
// 这里是导入混入的文件
Vue.config.productionTip = false
Vue.mixin(lqz)
// Vue.mixin(lqz2)
// Vue.mixin(lqz3)
// 这里就相当于对导入的混入文件进行注册,可以注册多个不同的混入文件
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
三、插件
-
功能:用于增强Vue
-
本质:包含install方法的一个对象,install的第一个参数是Vue,第二个以后的参数是插件使用者传递的数据。
-
定义插件:plugins/index.js
可以做的事
1 了解,自定义指令(不了解没关系)
2 定义全局变量,以后在任何组件中都可以使用到,借助于Vue.prototype往里放 ,以后所有组件只要this.$ajax 就是axios对象
3 使用全局混入
4 自定义全局组件
main.js中注册的问题
index.js
export default {
install(vue,name) {
}
}
- 我们通过上面的学习可以知道组件需要在内部定义install方法,并且必须有一个vue参数,如果有别的自定义参数,在main.js中注册的时候也需要写上
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
Vue.config.productionTip = false
// 使用自定义插件
import plugin from '@/plugins'
Vue.use(plugin,'xxx')
// 这里的plugin就是传递给vue参数的,而后面的字符串'xxx'则是传递给name的
// Vue.use(plugin)
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
前置操作
新建包plugins,新建index.js,然后在js文件内编写代码
自定义指令(了解,不了解没关系)
index.js中编写自定义指令
这里在插件内编写了一个自定义指令模仿的是v-bind的功能
import Vue from "vue";
export default {
install(vue) {
console.log('执行了插件', vue)
// 可以做的事
// 1 了解,自定义指令(不了解没关系)
Vue.directive("fbind", {
//指令与元素成功绑定时(一上来)
bind(element, binding) {
element.value = binding.value;
},
//指令所在元素被插入页面时
inserted(element, binding) {
element.focus();
},
//指令所在的模板被重新解析时
update(element, binding) {
element.value = binding.value;
},
})
}
}
在main.js中导入并注册
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
Vue.config.productionTip = false
// 使用自定义插件
import plugin from '@/plugins'
Vue.use(plugin)
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
Index.vue
<template>
<div class="home">
<h1>插件的使用</h1>
<h1>使用自定义指令</h1>
<input type="text" v-fbind:value="v">
</div>
</template>
<script>
export default {
name: 'HomeView',
data() {
return {
v: 'xxx'
}
},
}
</script>
定义全局变量
在python中我们只需要在全局定义变量,就可以把他当成全局变量调用。
在vue中我们需要定义Vue.prototype之后,才可以定义全局变量
index.js
import Vue from "vue";
import axios from "axios";
export default {
install(vue) {
console.log('执行了插件', vue)
// 2 定义全局变量,以后在任何组件中都可以使用到,借助于Vue.prototype往里放 ,以后所有组件只要this.$ajax 就是axios对象
Vue.prototype.$name = '彭于晏'
Vue.prototype.$add = (a, b) => {
return a + b
}
Vue.prototype.$ajax=axios
}
}
这里同样需要去main.js中导入并注册,我就不重复贴代码了
Index.vue
<template>
<div class="home">
<h1>插件的使用</h1>
<h1>插件第二种应用,定义全局变量</h1>
<button @click="handlClick">点我发请求---发布成功</button>
</div>
</template>
<script>
export default {
name: 'HomeView',
data() {
return {
}
},
methods: {
handlClick() {
// this.$ajax('sss').then(res => {
//
// })
// console.log(this.$name)
console.log(this.$add(4, 5))
},
},
}
</script>
这里我们在handlClick方法中分别实现了用全局变量发送ajax请求以及打印全局变量和方法
使用全局混入
我们可以在插件中定义混入,因为插件会在main.js中注册所以就是全局混入
index.js
import Vue from "vue";
import axios from "axios";
export default {
install(vue) {
console.log('执行了插件', vue)
// 3 使用全局混入
Vue.mixin({
data() {
return {
name: '彭于晏',
age: 19,
};
},
});
}
}
这里同样需要去main.js中导入并注册,我就不重复贴代码了
Index.vue
<template>
<div class="home">
<h1>插件的使用</h1>
<h1>插件中使用全局混入</h1>
<button @click="haneldShowName">点我,弹出名字</button>
</div>
</template>
<script>
export default {
name: 'HomeView',
data() {
return {
v: 'xxx'
}
},
methods: {
haneldShowName(){
alert(this.name)
}
},
}
</script>
这里我们并没有在data中定义name属性,但是因为在插件中定义了全局混入,所以在点击按钮后触发了haneldShowName方法,会获取到混入中的name属性并弹出来
自定义全局组件(了解)
index.js
import Vue from "vue";
import axios from "axios";
export default {
install(vue) {
console.log('执行了插件', vue)
// 4 自定义全局组件
Vue.component('child',{
......
})
}
}
我们在插件中定义组件,就可以在所有的组件中直接调用,但是这样的情况很少。
四、elementui使用(重点)
第三方框架介绍
在vue上有很多的第三方框架,他们提供了很多的css样式,比较流行的有下面这些:
名称 | 介绍 | 官网 |
---|---|---|
Elementui | Elementui它是由饿了么前端团队推出的基于 Vue2.0封装的桌面端组件库(网页端样式用的多),提供PC 端组件,简化了常用组件的封装,降低开发难度。(已经终止维护了) | https://element.eleme.cn/#/zh-CN/component/installation |
elementui-plus | 第三方团队基于vue3.0对于Element UI的升级与适配。 | https://element-plus.gitee.io/zh-CN/ |
vant | Vant 是一个轻量、可靠的移动端组件库(手机app)。目前Vant官方提供了 Vue 2 版本、Vue 3 版本和微信小程序版本,并由社区团队维护 React 版本和支付宝小程序版本。 | https://vant-ui.github.io/vant/?#/zh-CN |
iview | 基于 Vue.js 3 的企业级 UI 组件库和中后台系统解决方案。(pc端用的多) | http://iviewui.com/ |
elementui的使用
ps:所有的操作都是可以参考官网的文档的
1、安装
这里我们用cnpm进行安装,更快嘛
cnpm i element-ui -S
2、在配置文件main.js中引入elementui
- 引入elementui有两种方式:完整引入和按需引入
- 完整引入就是直接把所有的组件全部引入进来,体积比较大
- 按需引入的流程参考官网文档,他就是需要用什么组件就引入什么组件,但是对于开发者来说操作起来麻烦
以后在咱们组件中直接使用elementui提供的全局组件即可
在 main.js 中写入以下内容:
import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
以上代码便完成了 Element 的引入。需要注意的是,样式文件需要单独引入。
3、组件的使用
去官网文档查找自己需要的组件,然后拿过来用即可
五、localStorage和sessionStorage
1、什么是 localStorage、sessionStorage
- 在 HTML5 中,新加入了一个 localStorage 特性,这个特性主要是用来作为本地存储来使用的,解决了 cookie 存储空间不足的问题(cookie 中每条 cookie 的存储空间为 4k)
- localStorage 中一般浏览器支持的是 5M 大小,这个在不同的浏览器中 localStorage 会有所不同。
- 对浏览器来说,使用 Web Storage 存储键值对比存储 Cookie 方式更直观,而且容量更大,它包含两种:localStorage 和 sessionStorage
sessionStorage(临时存储):为每一个数据源维持一个存储区域,在浏览器打开期间存在,关闭浏览器再打开就会重新加载
localStorage(长期存储):与 sessionStorage 一样,但是浏览器关闭后,数据依然会一直存在
2、在浏览器端存数据有什么用?
- 登录成功可以把token存在本地
- 可以存储数据(不登录加入购物车功能,迪卡侬存在了localStorage中)
- 组件间通信(可以实现跨组件通信)
3、localStorage
- localStorage是永久存储,除非清空缓存,手动删除,代码删除
上面我们提到迪卡侬把购物车数据存在了localStorage中,我们可以在浏览器中打开控制台界面查看
增删查操作
<template>
<div>
<h1>localStorage使用</h1>
<button @click="insertLocalStorage">写入localStorage</button>
<button @click="getLocalStorage">获取localStorage</button>
<button @click="deleteLocalStorage">删除localStorage</button>
</div>
</template>
<script>
export default {
name: 'HomeView',
data() {
return {
userInfo: {name: 'lqz', age: 19}
}
// 这里定义一些数据用于记录
},
methods: {
insertLocalStorage() {
// value 必须是字符串,如果是对象或数组 ,转成json格式字符串
// 这里就是往 localStorage中添加数据
localStorage.setItem('userinfo', JSON.stringify(this.userInfo))
},
getLocalStorage() {
// 取出json格式字符串,再转成对象
// 这里就是在 localStorage中查找数据
var res = localStorage.getItem('userinfo')
console.log(typeof res)
var res1 = JSON.parse(res)
console.log(typeof res1)
},
deleteLocalStorage() {
// 这里就是在 localStorage中删除数据
// localStorage.clear() // 清空全部
localStorage.removeItem('userinfo') // 只删除某个
},
}
}
</script>
4、sessionStorage
- 内部的数据,关闭浏览器,自动清理
增删查操作
类似localStorage
<template>
<div>
<h1>sessionStorage使用</h1>
<button @click="insertsessionStorage">写入sessionStorage使用</button>
<button @click="getsessionStorage">获取sessionStorage使用</button>
<button @click="deletesessionStorage">删除sessionStorage使用</button>
</div>
</template>
<script>
export default {
name: 'HomeView',
data() {
return {
userInfo: {name: 'lqz', age: 19}
}
//
},
methods: {
insertsessionStorage() {
// value 必须是字符串,如果是对象或数组 ,转成json格式字符串
sessionStorage.setItem('userinfo', JSON.stringify(this.userInfo))
},
getsessionStorage() {
// 取出json格式字符串,再转成对象
var res = sessionStorage.getItem('userinfo')
console.log(typeof res)
var res1 = JSON.parse(res)
console.log(typeof res1)
},
deletesessionStorage() {
// sessionStorage.clear()
sessionStorage.removeItem('userinfo') // 只删除某个
},
}
}
</script>
5、cookie
- 有过期时间,到过期时间自动清理
增删查操作
需要借助第三方组件vue-cookies
<template>
<div>
<h1>Cookie使用</h1>
<button @click="insertCookie">写入Cookie使用</button>
<button @click="getCookie">获取Cookie使用</button>
<button @click="deleteCookie">删除Cookie使用</button>
</div>
</template>
<script>
import cookies from 'vue-cookie'
// vue-cookies 和 vue-cookie 是俩不一样的,咱们用vue-cookies,过期时间是s
export default {
name: 'HomeView',
data() {
return {
userInfo: {name: 'lqz', age: 19}
}
//
},
methods: {
insertCookie() {
// vue 中操作cookie,需要借助于第三方,vue-cookie
// value 必须是字符串,如果是对象或数组 ,转成json格式字符串
cookies.set('userinfo', JSON.stringify(this.userInfo), 1)
},
getCookie() {
// 取出json格式字符串,再转成对象
console.log(cookies.get('userinfo'))
},
deleteCookie() {
cookies.delete('userinfo')
},
}
}
</script>
六、vue Router
- Router:官方提供的用来实现SPA 的vue 插件
- 实现了单页面应用,就是在一个index.html 中有页面跳转效果的
- 本质就是做路由控制
基础使用
1 创建vue项目时加入了Router,直接用即可
2 如果之前没装:
先下载
npm install vue-router --save
在项目中创建router包,写个index.js,把下方的代码copy过来
import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
// 以后添加路由,就是模仿上面的home写对象,导入vue文件,更改路由路径,然后写上对应的vue文件名称
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
// 这里跟我们创建时候的配置选项有关,有一个地方询问我们是否开始这个历史模式,还有一个配置是询问我们项目依赖在哪个位置
routes
// 这就是我们上面定义的存储路由信息的数组
})
export default router
// 这里我们把VueRouter产生的对象导出
main.js 也要导入注册
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
ps:路由需要有对应的vue视图组件
路由跳转的两种方式
介绍路由跳转之前,我们介绍一下会用到的标签
-<router-link> 跳转用
-<router-view/> 替换页面组件用
我们的‘’可以在App.vue中看到,之前也介绍过他的作用,这两个东西比较像,可以区分记忆
# 点击跳转路由两种方式
-js控制
this.$router.push('路径')
-标签控制
<router-link to="/home">
<button>点我跳转到home页面</button>
</router-link>
这里我就把登陆页面的代码放上来了,主要还是路由跳转的代码
1、js控制实现路由跳转
<template>
<div>
<button @click="goLogin">点我跳转到登录</button>
</div>
</template>
<script>
export default {
name: 'HomeView',
data() {
return {
obj: {name: 'login', query: {name: 'lqz'}, params: {id: 999}}
}
},
methods: {
goLogin() {
// js 控制的跳转 ,跳转到login页面
this.$router.push('/login/')
}
},
}
</script>
相关API
代码 | 作用 |
---|---|
this.$router.push(path): | 相当于点击路由链接(可以返回到当前路由界面) |
this.$router.replace(path): | 用新路由替换当前路由(不可以返回到当前路由界面) |
this.$router.back(): | 请求(返回)上一个记录路由 |
this.$router.go(-1): | 请求(返回)上一个记录路由 |
this.$router.go(1): | 请求下一个记录路由 |
2、标签控制实现路由跳转
<template>
<div>
<router-link :to="/login">
<button>点我跳转到登陆页面</button>
</router-link>
</div>
</template>
<script>
export default {
name: 'HomeView',
data() {
return {
obj: {name: 'login', query: {name: 'lqz'}, params: {id: 999}}
}
},
}
</script>
3、区分this.\(route this.\)router
#4 区分this.$route this.$router
-this.$router # new VueRouter对象,实例,可以实现路由的跳转
-this.$route # 是当前路由对象,内部有传入的参数
4、路由跳转,携带数据的两种方式
在路由跳转中有两种方式携带数据
-1 /course/?pk=1 带在路径中使用 ? 携带 数据
-2 /course/1/ 路径中分割的数据
第一种方式:/course/?pk=1
我们在登陆组件的内部通过vue的八大钩子可以查看上面介绍的route和router的区别
然后我们根据浏览器中打印出的对象的信息,可以知道怎么获取路由中携带的数据
<template>
<div>
<h1>登录页面</h1>
</div>
</template>
<script>
export default {
name: "Login",
created() {
console.log('route', this.$route)
console.log('router', this.$router)
// 从当前路由对象中取出携带过来的值 ? 后的值
// console.log(this.$route.query.pk)
// 从当前路由对象中取出携带过来的值 路径切出来的
// console.log(this.$route.params.id)
}
}
</script>
<style scoped>
</style>
第二种方式:/course/1/
第二种方式因为对路由做出了更改,所以我们需要去路由中重新定义
这里的id就是等下我们取值时候需要点的变量名
const routes = [
{
path: '/login/:id',
name: 'login',
component: LoginView
},
]
这里也可以用第一种方式中登陆界面的代码去控制台中打印对应的对象来查看
5、两种路由跳转方式,使用对象进行跳转方式(编程式路由导航)
我们也可以使用对象来携带参数
js控制实现路由跳转
- 第一种方式:/course/?pk=1(这样进行路由跳转,路由会变成这样)
this.$router.push({
name: 'login',
query: {
name: 'lqz',
age: 19
},
})
- 第二种方式:/course/1/ (这样进行路由跳转,路由会变成这样)
我们可以使用params来接收数据
this.$router.push({
name: 'login',
params: {
id: 88
}
})
标签控制实现路由跳转
我们可以在标签上把to这里设置成属性指令,让他传入一个对象,在对象中携带数据
规律也是跟上面js跳转一样的,携带了params的跳转需要在路由层更改路由,没有携带params的跳转会用?号在路由后面携带数据
<template>
<div>
<!-- <router-link to="/home">-->
<!-- <router-link :to="obj">-->
<router-link :to="{name: 'login', query: {name: 'lqz'}, params: {id: 999}}">
<button>点我跳转到home页面</button>
</router-link>
</div>
</template>
<script>
export default {
name: 'HomeView',
data() {
return {
obj: {name: 'login', query: {name: 'lqz'}, params: {id: 999}}
}
},
}
</script>
6、路由守卫
- 作用:对路由进行权限控制
- 分类:全局守卫、独享守卫、组件内守卫
全局守卫
全局守卫有两种
-前置路由守卫:在进路由前,执行代码(任意路由跳转都会触发它的执行)
-后置路由守卫:路由跳转走,执行代码
全局前置路由守卫
//全局前置路由守卫————初始化的时候被调用、每次路由切换之前被调用
router.beforeEach((to, from, next) => {
// to 是去哪,哪个路由对象
// from 是来自哪,是哪个路由对象 比如从 /--->/login
// next 是函数,如果加括号执行,就会真正的过去
console.log('前置路由守卫', to, from, next)
// next() // 真正跳转到 要去的路径
if (to.name == 'login') {
console.log('走了')
next()
} else if (res) {
next()
} else {
alert('您没有登录')
// 跳转到login--->没有解决---》你们搜一下如何解决
// console.log(this)
router.push({name: 'login', params: {id: 99}})
}
}
)
七、vuex
1.概念
在Vue中实现集中式状态(数据)管理的一个Vue插件,对vue应用中多个组件的共享状态进行集中式的管理(读/写),也是一种组件间通信的方式,且适用于任意组件间通信。
2.何时使用?
多个组件需要共享数据时
基本情况可以参考第二张图的流程讲解,中间的步骤是为了实现给后端发送请求等操作,所以才这样设计的。
基本情况可以参考第二张图的流程讲解,中间的步骤是为了实现给后端发送请求等操作,所以才这样设计的。
三个状态介绍
state:真正存数据的
mutations:修改state的地址 state.变量名,修改即可
actions:判断,跟后端交互,调用mutations的地方 context.commit
如何查看组件中显示state的变量
-我们可以在组件中显示state的变量
html中:
{{$store.state.变量名}}
js中:
this.$store.state.变量面
如何更改state中的值
-推荐按正常步骤---》this.$store.dispatch('actions中的方法',参数)---》actions中的方法调用 context.commit('mutations',参数)---》在mutations中直接修改state的值
-可以跨过任何一步(但最好按照流程编写)
this.$store.commit()
this.$store.state.变量名
3.搭建vuex环境
1、创建文件:src/store/index.js
//引入Vue核心库
import Vue from 'vue'
//引入Vuex
import Vuex from 'vuex'
//应用Vuex插件
Vue.use(Vuex)
//准备actions对象——响应组件中用户的动作
const actions = {}
//准备mutations对象——修改state中的数据
const mutations = {}
//准备state对象——保存具体的数据
const state = {}
//创建并暴露store
export default new Vuex.Store({
actions,
mutations,
state
})
store内部有一个方法,但是另外两个用的不多,不用管(也没讲解)
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
},
getters: {
},
mutations: {
},
actions: {
},
modules: {
}
})
2、在main.js
中创建store
配置项
......
//引入store
import store from './store'
......
//创建vm
new Vue({
el:'#app',
render: h => h(App),
store
...
})
3、举例
这里我们编写了一个购物车来举例
src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import axios from "axios";
Vue.use(Vuex)
export default new Vuex.Store({
state: {
num: 10,
good_num: 0
},
mutations: {
add(state, value) {
// state就是上面的的state
// value是传入的值
state.num = state.num + value
},
addShopping(state) {
state.good_num += 1
}
},
actions: {
add(context, value) {
console.log(context)
console.log(value)
context.commit('add', value) // 它会触发mutations里面add的执行
},
addShopping(context, id) {
// 发送ajax请求,把id携带后后端,把这个商品加入到购物车数据库
context.commit('addShopping')
}
}
})
这里记得去main.js中添加配置
Index.vue
<template>
<div>
<h1>vuex的使用</h1>
<!-- <button @click="add">点我自增1</button>-->
<!-- {{ $store.state.num }}-->
<ul>
<li v-for="item in goodList">
商品id:{{ item.id }}--->商品名:{{ item.name }}--->商品价格:{{ item.price }}--->
<button @click="addShopppingCart(item.id)">加入购物车</button>
</li>
</ul>
<hr>
<ShoppingCart></ShoppingCart>
</div>
</template>
<script>
import ShoppingCart from "@/little/ShoppingCart";
export default {
name: 'HomeView',
data() {
return {
goodList: [
{id: 1, name: '钢笔', price: 2},
{id: 2, name: '内衣', price: 244},
{id: 3, name: '秋衣', price: 222},
{id: 4, name: '秋裤', price: 23},
],
}
},
methods: {
add() {
// 直接改,没问题,但是不建议
// this.$store.state.num++
// 1 先触发actions的执行--->触发store中的actions中定义的add,并且把1传给value
this.$store.dispatch('add', 1)
},
addShopppingCart(id) {
// this.$store.dispatch('addShopping',id)
// this.$store.state.good_num+=1
this.$store.commit('addShopping')
}
},
components: {
ShoppingCart
}
}
</script>
标签:插件,Vue,name,vue,组件,import,vuex,路由
From: https://www.cnblogs.com/wxlxl/p/17146076.html