整合营销服务商

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

免费咨询热线:

5个不常提及的HTML技巧

021年你需要知道的HTML标签和属性

Web开发人员都在广泛的使用HTML。无论你使用什么框架或者选择哪个后端语言,框架在变,但是HTML始终如一。尽管被广泛使用,但还是有一些标签或者属性是大部分开发者不熟知的。虽然现在有很多的模版引擎供我们使用,但是乐字节教育的老师和我们说还是需要尽可能的熟练掌握HTML内容,就像CSS一样。

在我看来,最好尽可能使用HTML特性来实现我们的功能,而不是使用JavaScript实现相同的功能,尽管我承认编写HTML可能会是重复的和无聊的。

尽管许多开发人员每天都在使用HTML,但他们并没有尝试改进自己的项目,也没有真正利用HTML的一些鲜为人知的特性。

下面这5个通过HTML标签/属性实现的功能我觉得需要了解一下:

图片懒加载

图片懒加载可以帮助提升网站的性能和响应能力。图片懒加载可以避免立即加载那些不在屏幕中立即显示的图片素材,当用户滚动临近图片时再去开始加载。

换言之,当用户滚动到图片出现时再进行加载,否则不加载。这就降低了屏幕内容展示过程中的图片素材的请求数量,提升了站点性能。

往往我们都是通过javascript来实现的,通过监听页面滚动事件来确定加载对应的资源。但是,在不完全考虑兼容性的场景下,我们其实可以直接通过HTML来直接实现。

注:本篇的提到的标签和属性的兼容性需要大家根据实际场景来选取是否使用

可以通过为图片文件添加loading="lazy"的属性来实现:


输入提示

当用户在进行输入搜索功能时,如果能够给出有效的提示,这会大大提升用户体验。输入建议和自动完成功能现在到处可见,我们可以使用Javascript添加输入建议,方法是在输入框上设置事件侦听器,然后将搜索到的关键词与预定义的建议相匹配。

其实,HTML也是能够让我们来实现预定义输入建议功能的,通过<datalist>标签来实现。需要注意的是,使用时这个标签的id属性需要和input元素的list属性一致。


Picture标签

你是否遇到过在不同场景或者不同尺寸的设备上面的时候,图片展示适配问题呢?我想大家都遇到过。

针对只有一个尺寸的图片素材的时候,我们往往可以通过CSS的object-fit属性来进行裁切适配。但是有些时候需要针对不同的分辨率来显示不同尺寸的图片的场景的时候,我们是否可以直接通过HTML来实现呢?

HTML提供了<picture>标签,允许我们来添加多张图片资源,并且根据不同的分辨率需求来展示不同的图片。


我们可以定义不同区间的最小分辨率来确定图片素材,这个标签的使用有些类似<audio>和<video>标签。

Base URL

当我们的页面有大量的锚点跳转或者静态资源加载时,并且这些跳转或者资源都在统一的域名的场景时,我们可以通过<base>标签来简化这个处理。

例如,我们有一个列表需要跳转到微博的不同大V的主页,我们就可以通过设置来简化跳转路径


<base>标记必须具有href和target属性。

页面重定向(刷新)

当我们希望实现一段时间后或者是立即重定向到另一个页面的功能时,我们可以直接通过HTML来实现。

我们经常会遇到有些站点会有这样一个功能,“5s后页面将跳转”。这个交互可以嵌入到HTML中,直接通过<meta>标签,设置http-equiv="refresh"来实现


这里content属性指定了重定向发生的秒数。值得一提的是,尽管谷歌声称这种形式的重定向和其他的重定向方式一样可用,但是使用这种类型的重定向其实并不是那么的优雅,往往会显得很突兀。

因此,最好在某些特殊的情况下使用它,比如在长时间用户不活动之后再重定向到目标页面。

后记

HTML和CSS是非常强大的,哪怕我们仅仅使用这两种技术也能创建出一些奇妙的网站。虽然它们的使用量很大很普遍,还是有很多的开发者并没有真正的深入了解他们,还有很多的内容需要我们深入的去学习和理解,实践,有很多的技巧等待着我们去发现。

文章转载至乐字节

最后给大家推荐几个b站超详细的Java自学课:

Servlet入门教程BV1D5411373E

Vue、Vuejs教程,BV19V41177od

SpringBoot+Vue项目实战BV1o64y117qQ

iframe元素

创建包含另外一个文档的内联框架(即行内框架)。

CSS3规范,视口单位主要包括以下4个:

1、vw : 1vw 等于视口宽度的1%;

2、vh : 1vh 等于视口高度的1%;

3、vmin : 选取vw和vh中最小的那个;

4、vmax : 选取vw和vh中最大的那个;

100%高度和宽度:

body {
    margin: 0; /* Reset default margin */
}
iframe {
    display: block;  /* iframes are inline by default */
    background: #fff;
    border: none;   /* Reset default border */
    height: 100vh;   /* Viewport-relative units */
    width: 100vw;
}

参考代码

HTML中嵌入iframe

<!DOCTYPE html>
<html lang="zh-CN">
<head>
	<title>XXXX调查表</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
	<style type="text/css">
		body html{
			margin:0;
			padding:0;
		}
		#app{
			margin: 0 auto;
			width: 600px;
		}
		iframe {
			display: block; 
			background: #FFF;
			border: none; 
			width:100vw;  /* Viewport-relative units */
			height:100vh;
			width:100%;
		}
	</style>
</head>
<body>
	<div id="app">
		<div>
			<button type="button"><span>返回</span></button>
			<button type="button" onclick="print()"><span>打印</span></button>
		</div>
		<h2>公司部门调查表</h2>
		<iframe src="C:/Users/dd/Desktop/003.html" scrolling="no"></iframe>
	</div>
</html>

被嵌入页面:

<!doctype html>
<html lang="zh-CN">
<head>
	<title>XXXX调查表</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
	<script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script>
	<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
	<link href="https://cdn.bootcdn.net/ajax/libs/antd/4.18.2/antd.variable.css" rel="stylesheet">
	<!--
		https://v1-cn.vuejs.org/guide/forms.html
	-->
</head>
<body>
	<div id="app">
		<p><h2>公司部门调查表</h2></p>
		<form v-model="form" v-on:submit.prevent="saved()" >
			<fieldset>
				<legend>职员信息</legend>
				<ol>
					<li>
						<label>姓名: </label>
						<input v-model="form.name" type="text" name="name" autofocus/>
					</li>
					<li>
						<label>年龄: </label>
						<input v-model="form.age" type="number" name="age" />
					</li>
					<li>
						<label>性别:</label>
						<input v-model="form.sex" type="radio" name="sex" value="未知" />未知 
						<input v-model="form.sex" type="radio" name="sex" value="男" />男 
						<input v-model="form.sex" type="radio" name="sex" value="女" />女
					</li>
					<li>
						<label>籍贯:</label>
						<select v-model="form.nativePlace">
							<optgroup label="山西省">
								<option>太原市</option>
							</optgroup>
							<optgroup label="北京">
								<option>北京市</option>
							</optgroup>
						</select>
					</li>
					<li>
						<label>爱好:</label>
						<input v-model='form.hobbys' type="checkbox" name="hobby" value="爬山" />爬山 
						<input v-model='form.hobbys' type="checkbox" name="hobby" value="涉水" />涉水 
						<input v-model='form.hobbys' type="checkbox" name="hobby" value="下棋" />下棋 
						<input v-model='form.hobbys' type="checkbox" name="hobby" value="游戏" />游戏 
					</li>
					<li>
						<label>所在部门:</label>
						<select v-model="form.department">
							<option v-for="(item, index) in form.departments" :key="item">{{item}}</option>
						</select>
					</li>
					<li>
						<label>职位:</label>
						<input v-model="form.title" type="text" name="title" />
					</li>
					<li>
						<label>提交建议:</label>
						<input v-model="form.advise" type="file" name="form.advise" />
					</li>
					<li>
						<label>填表日期:</label>
						<input v-model="form.fillDate" type="date" name="fillDate">
					</li>
				</ol>
			</fieldset>
			<fieldset>
				<legend>反馈意见</legend>
				<ol>
					<li>
						<label>你对公司目前的发展表示:</label>
						<input v-model="form.development" type="radio" name="development" value="满意"  /> 满意  
						<input v-model="form.development" type="radio" name="development" value="不满意"  /> 不满意  
						<input v-model="form.development" type="radio" name="development" value="一般"  /> 一般
					</li>
					<li>
						<label>你对公司的任职岗位表示:</label>
						<input v-model="form.jobIdentification" type="radio" name="jobIdentification" value="认同"  /> 认同  
						<input v-model="form.jobIdentification" type="radio" name="jobIdentification" value="不认同"  /> 不认同  
						<input v-model="form.jobIdentification" type="radio" name="jobIdentification" value="一般"  /> 一般
					</li>
					<li>
						<label>你对公司的期望: </label> <br />
						<textarea v-model="form.expect" rows="4" name="neme" cols="50"></textarea>
					</li>
				</ol>
			</fieldset>
			<input type="submit" value="提交">
			<input type="reset" value="重置">
		</form>
	</div>
	<script>
		var now = new Date();
		var year = now.getFullYear();
		var month = now.getMonth()+1;
		var day = now.getDay();
		// 
		var app = new Vue({
			el: '#app',
			data() {
				return {
					form: {
						name:"孔子",
						age: 2000,
						sex: "男",
						nativePlace: "北京市",
						hobbys: ["爬山","下棋"],
						departments: ["财务部","法务部","加盟部","商务部","技术部"],
						department: "技术部",
						title: "工程师",
						fillDate: year + "-" + month + "-" + day,
						development: "一般",
						jobIdentification: "不认同",
						expect: "一切都美好~"
					}
				}
			},
			methods:{
				saved(){
					alert(JSON.stringify(this.form));
					axios.post('http://192.168.1.116:8080/data/post',this.form,{
						headers: { "token": "token123" }
					}).then(res => {
						console.log("result", JSON.stringify(res.data));
					})
				}
				
			}
		});
	</script>
</html>

VUE中嵌入:

不管的后端还是前端表单的使用率是极高的,但风格和团队之间个人的编写风格各异,尤其的多条件,多规则,大量表单页面,维护和保持统一编写就变的比较困难,之前使用vue2版本的框架的时候基于之前低代码开发的经验编写了一套 基于json描述生成的 表单组件,vue版本升级到vue3之后自然也需要一套与之相匹配的,简单易用的表单组件。

凡事都有前置条件,因为业务的需要,后台系统使用微前端的qiankun框架,但比较可惜的是vite尚不被qiankun支持,当然也有一些 相应的插件可以解决这个问题,但考虑到其非官方产品,企业从稳定性出发排除了vite

所以下面我讲到的 组件是基于 vue3+typscript+webpack+elementulplus来进行二次封装实现的。


组件的目标

我们要做的组件就是为了简洁,可维护,可扩展 ,使用者不需要了解组件的内部实现,仅需要按照组件的配置要求进行配置即可实现功能,当然我们要尽可能的满足需求,但觉不是100%满足,其实一个优秀的组件能够完成业务的80%的需求已经非常了不起了。在进行通用组件设计的时候切记不可大而全,如果一个组件使用的学习成本较高,基本也失去了封装的意义。当然框架除外,比如vue

我的的目标是:

  1. 通过配置 json 并把json做为 props 传递给组件,
  2. 组件自动生成基于elementUIplus的 表单
  3. 组件包含elementUiPlus实现的所有表单功能
  4. 组件支持部分自定义组件的插入,比如某个input框下面需要加一个相应的文字说明或者其他什么的
  5. 组件支持所有elemnetUIPlus提供的属性 比如 size、disabled lable...
  6. 组件表单的提交和情况
  7. 组件的初始化或反显的实现
  8. 组件提交方法的自定义
  9. 内置基于业务的表单判断逻辑,比如 手机号,身份证,地址,上传,等或其他一些基于企业内部的逻辑,通过特定参数直接进行实现
  10. 样式的灵活配置。


组件的结构设计

我们的设计结构是

module:

module是我们组成表单的一个个的最小的自定义组件单元,。其实modle也可以不存在,直接在render函数里使用 elementUIplus里定义好的组件,这又是另一个话题了,这里我们使用自定义组件也是为了方便自定义和扩展。

data:

data是一种json格式的数据结构,用来对模块进行描述,(低代码平台的实现本质上也是如此。通过拖拽或其他方式对数据进行拼装然后展示)

render:

render 层是 vue3框架提供的 render函数,结合h()实现VirtualDom的的渲染,因为是h()是创建虚拟 DOM 节点 (vnode)的方法,因此不存在性能问题,在实现上和正常写法没有区别。

view:

view 做为展示层 及我们展示的页面。

data层 json的数据格式设计

elementuiplus 的form组件数据层面需要传递两组数据(需要提交的数据和校验规则)

数据和规则分离并不合理,所以在这里我们在json里把 需要校验的数据和校验规则放在一组里。

{
        blockName: "Input",
        attrsData: {
            type: "text",
            field: "name",
            label: "姓名",
            placeholder: "Please input name",
            maxlength: 20,
            minlength: 1,
            showWordLimit: true,
            clearable: true,
            disabled: false,
            inputStyle: { color: "red"
            },
            initValue:'666'
        },
        rule: [
            {
                required: true,
                message: "Please input Activity name",
                trigger: "blur",
            },
            { min: 3, max: 5, message: "Length should be 3 to 5", trigger: "blur"
            },
        ]
    }

数据有三部分,即 blockName(组件名称),attrsData(组件属性的描述),rule(校验规则)


module层组件的实现

<template>
    <el-input
        :placeholder="placeholder"
        :maxlength="maxlength"
        :minlength="minlength"
        :clearable="clearable"
        :require="require"
        :name="name"
        :formatter="formatter"
        :show-password="showPassword"
        :parser="parser"
        :prefix-icon="prefixIcon"
        :input-Style="inputStyle"
        v-model="value"
    />
</template>

<script lang="ts">
import { useGetAttrsHook } from "../hooks/common.hook";
export default {
    setup(props, context) {
        const {
            clearValue,
            test,
            value,
            placeholder,
            maxlength,
            minlength,
            clearable,
            formatter,
            showPassword,
            parser,
            require,
            options,
            name,
            field,
            clear,
            prefixIcon,
            activeText,
            inactiveText,
        } = useGetAttrsHook(props, context);
        return {
            clearValue,
            test,
            value,
        };
    },
};
</script>


以上面的代码为例,在组件里我们使用elemnetUIplus提供的 表单组件。并把相应的 属性进行了绑定,除了value其他属性都只需要进行正常的 attrs 传递即可,value则需要实现数据的双向绑定,并在我们点击父组件的提交按钮时传递给父组件进行提交,所以在这里就需要做一些特别的处理。来实现数据双向传递,注意是双向传递。如果不自定义组件也可以在 h函数渲染的时候直接使用 elementUIplus提供的组件,省去这一步。但双向绑定需要想办法实现,并且灵活性也差一些。不能在组件里定义一些自己的逻辑和样式。


数据的双向传递实现方案:

我们知道组件上使用v-model实现组件上的双向绑定,如果是常规写法自然没有问题,但很遗憾我们的render 是不走寻常路的.先看下vue3的文档的实现方式

https://cn.vuejs.org/guide/components/v-model.html

通过文档我们知道 v-model 实际上就是一个语法糖,他的本来面目是这样的

<CustomInput

:modelValue="searchText"

@update:modelValue="newValue=>searchText = newValue"

/>

对于我们来说这仍然是常规写法,因为在h()里面这样是行不通的。那么在h()中的写法应该是这样的

h(ASwitch, {
    modelValue: this.value,
    ["onUpdate:modelValue"]: (data) => {
      this.value = data;
    },
});

如果监听的是一个原生的input这个写法是生效的,但可惜在我写的的组件上并不好使。之所以在本文中写出来是给大家一个参考或者各位有人发现我的错误之处,可以有实现。

那么还有什么方式实现数据的双向传递呢,那就是 子组件调用父组件方法,父组件调用子组件方法,并互相进行数据传递了。

那么在vue3 中我们只能这样去进行实现了,我们先谈下子组件调用父组件的方法。

https://cn.vuejs.org/guide/components/events.html

<createForm
  :data="fromData"
  :ruleForm="ruleForm"
  @setData="setData"
  ref="childComp"
  :reset="reset.type"
/>

父组件通过@绑定一个方法到子组件,子组件通过使用 defineEmits()宏来声明它要触发的事件,然后通过emit('方法名',入参)来实现父组件的调用。明白了这个概念就可以了,因为之后我们需要在h()上来实现这样的一个功能。

还有就是我们需要另一个功能就是父组件传递一个指令给子组件,子组件监听到命令后,执行自己的一个方法,用来清空自己的 value 也就是组件上传递的属性 :reset="reset.type" 所有子组件如果监听到父组件这个值的变化就清空value。

其实除了传递指令还有另一个方法就是父组件调用子组件的内的清除value的方法,但vue3父组件掉子组件的方法,因为涉及到ref的绑定 如果在h函数里实现比较麻烦,我没有实现。有兴趣的同学可以尝试一下。


vue3渲染函数(h函数)的变化:

vue3 h函数的写法与vue2 有很大不同,在学习 render函数的实现之前我们需要对 h函数的变化有所了解。

  • h 现在全局导入,而不是作为参数传递给渲染函数
  • 渲染函数参数更改为在有状态组件和函数组件之间更加一致
  • vnode 现在有一个扁平的 prop 结构


vue2 props 格式化语法

{

class: ['button', 'is-outlined'],

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

attrs: { id: 'submit' },

domProps: { innerHTML: '' },

on: { click: submitForm },

key: 'submit-button'

}



vue3 props格式化语法

{

class: ['button', 'is-outlined'],

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

//属性不需要放在 attrs domProps on这些字段下了。

id: 'submit',

innerHTML: '',

onClick: submitForm,

key: 'submit-button'

}

我们可以看到vue3 渲染函数的属性更加的扁平化,一些属性不需要在包含在对应的字段里了。我个人觉的之前的更加严谨。仅个人观点。

render函数的实现:

了解了渲染函数的变化我们来看下渲染方法的实现 ,先看代码。


import {h,ref} from 'vue'
import CreateBlock from './createBlock';
import { ElFormItem} from 'element-plus';
const renderConfig =(h, data,func,reset)=> {
    if (!data) { return console.error('no data'); }
    return h(ElFormItem, { label: data.attrsData.label, prop: data.attrsData.field }, [h(CreateBlock, {
        ...data, clear: reset,['onSetData']:(data) => { 
            func(data)
    } })])
}


export default {
    props:{
        data: {
            type: Array,
            default() {
                return [];
            },
        },
        reset: {
            type: Number,
            default() { 
                return 0
            }
        }
    },
    setup(props, context) {
        // const list = props.data
        const blockFunc = (data) => { 
            // console.log('传入的data',data)
            context.emit('setData', data)
        }
        return {
            blockFunc
        }
    },
    render(cxt) { 
        const list = cxt.data
        const reset = cxt.reset
        const childNodes = list.map((n) => renderConfig(h, n,cxt.blockFunc,reset));
            const result = h('div',childNodes);
            return result;
    }
}

createFrom.ts 做了三件事,1,接受到了父组件传递过来的数据,2,根据传过来的json遍历渲染 子组件

3.把接受到的属性,方法,和 组件名称 传递给子组件


CreateBlock 会根据json的blackName返回相应的组件,所以我们渲染处理的组件不顺序不会有问题

setup 函数里的context.emit('setData', data) 用来实现子组件向父组件传递数据

renderConfig 函数里的 clear: reset 属性则是父组件向子组件传递清除命令的


整体逻辑如下;

1.render 接收到 json数据 然后循环调用 renderConfig 函数

2.renderConfig函数 给每个子组件外层包裹了 ElFormItem 并给ElFormItem提供了相应的属性如 label,prop

3.renderConfig函数把json遍历到的每条数据都通过 props 传递给了createBlock 同时给每个子组件绑定了setdata函数,注意这里要用onsetData


createBlock函数子组件渲染的实现:

import { h,toRefs } from 'vue'
export default {
    setup(props, context) { 
        const newMap: Array<object> = [];
        try {
            const elObj = require.context("../module", true, /\.vue$/);
            elObj.keys().forEach((key) => {
                const blockName = key.split("/")[1].split('.')[0];
                const el = elObj(key).default;
                if (!el) return;
                newMap[blockName] = el;
            });
        } catch (error) {
            console.log(error);
        }
        const block = newMap[context.attrs.blockName + 'Block'];
        const attrsData = { ...context.attrs.attrsData };
        const func = (data) => {
            context.emit('SetData', data)
        }
        const getAttrs = ()=>{ 
            return { ...context.attrs }
        }
        return {
            block,
            attrsData,
            func,
            getAttrs
        }
    },
    render(cxt) { 
        const { clear } = cxt.getAttrs()
        return h(cxt.block, { ...cxt.attrsData,clear,['onsetData']: cxt.func})
    }
}

可以看到我们使用 require.context("../module", true, /\.vue$/); 来获取 module文件夹下的所以组件,并按文件名称转换成数组和对应的引用地址,存放在newMap变量里。然后使用 context.attrs.blockName获取 父组件传递过来的属性 “blockName” 找到我们需要的子组件宣传出来。是不是很简单。


子组件的实现有很多通用方法 common.hook.ts

import {
    toRefs,
    toRef,
    ref,
    useAttrs,
    reactive,
    watch,
    onMounted,
    watchEffect,
} from "vue";

/* eslint-disable */

export const useGetAttrsHook = (props, context) => { 
    
            const {
                placeholder,
                maxlength,
                minlength,
                clearable,
                formatter,
                showPassword,
                parser,
                require,
                options,
                name,
                field,
                clear,
                prefixIcon,
                initValue,
                activeText,
                inactiveText,
            } = context.attrs;
    
            let value = ref(initValue);
            console.log("value---555", options);
            watchEffect(() => {
                let data = {};
                if (typeof field === "string") {
                    data[field] = value.value;
                }
                context.emit("setData", data);
            });
            watch(
                () => props.clear,
                (newValue, oldValue) => {
                    console.log("oldValue, newValue--", newValue, oldValue);
                    if (newValue == 1) {
                        clearValue();
                    }
                }
            );
            const test = ref("ok");
            const clearValue = () => {
                value.value = "";
            };
            return {
                clearValue,
                test,
                value,
                placeholder,
                maxlength,
                minlength,
                clearable,
                formatter,
                showPassword,
                parser,
                require,
                options,
                name,
                field,
                clear,
                initValue,
                prefixIcon,
                activeText,
                inactiveText
            };
    
}

/* eslint-enable */


最后一起看下实现的效果

最后

目前暴露的数据和接口比较多,还做进一步的封装和优化。