扩展你的前端知识库,毫无废话!

又是持续学习的一天!这篇文章给大家分享前端中一些原理性问题,以及开发中一些功能的实现,分别为:

  • Form表单字段校验如何实现
  • Vue 项目中 data-v-xxx 的生成
  • Vue项目打包后产生的 JS 请求
  • 远程加载 .vue 组件并使用
  • render函数实现一个菜单下拉框

一、Form表单字段校验如何实现

每个前端开发者肯定都接触过 Form 表单组件,组件库都会有这个组件,平时用得也非常多,但是我们很容易忽略它的一些功能是怎么实现的,比如表单校验是如何实现的?为啥每次输入的时候,就能马上触发到表单的校验呢?这些都是值得我们去深究的问题,只有这样才能不断提升开发能力!

下面以一个例子来研究表单校验功能的实现:

js 复制代码
<template>
  <div>
    <a-form :rules="rules" :data="formData">
      <a-form-item field="name">
        <a-input v-model="formData.name" />
      </a-form-item>
    </test-form>
  </div>
</template>

<script lang="ts" setup>
import { reactive } from 'vue'
import AForm from './Form.vue';
import AFormItem from './Form-Item.vue'
import AInput from './Input.vue';

// 规则
const rules = {
  name: {
    required: true
  }
}
// 数据
const formData = reactive({
  name: '哈哈哈'
})
</script>

其实校验的核心功能就是三个东西

  • 表单规则:在 a-form 中
  • 表单字段:在 a-form-item 中
  • 表单的值:在 a-input 中

我们可以通过表单字段去获取到实时的表单值,接着去表单规则中去匹配,就可以获取到校验结果了;只有将这三个东西结合起来,才能做到校验,他们的关系如下图:

大致分为几步:

  • 1、Form 将 rules、validate 传给 From-Item
  • 2、Form-Item 将 field、onChange 函数传给 Input
  • 3、Input 的 value 改变时触发 validate、onChange 函数去执行校验,并且决定错误提示的展示

为了传值方便,这里主要采用 provide、inject 的方式(Vue3 中组件之间传值的方式)

1. 具体实现------Form

Form 组件的作用:执行校验函数的主要逻辑,根据传入的 field 判断该字段的校验是否通过,然后将校验结果存到 errorMap 中。

html 复制代码
<template>
  <form>
    <slot></slot>
  </form>
</template>

<script lang="ts" setup>
import { provide, reactive } from 'vue'
const props = defineProps<{ rules: any; data: any }>();
// 字段有多个,所以需要维护一个错误表
const errorMap = reactive<any>({})

// 校验函数
const validateFn = (field: string): Promise<void> => {
  return new Promise((resolve, reject) => {
    const { rules, data } = props;
    const ruleItem = rules[field]
    const dataItem = data[field]
    if (ruleItem.required && dataItem === '') {
      return reject()
    }
    resolve()
  });
};

// 执行校验
const validate = (field: string) => {
  validateFn(field).then(() => {
    errorMap[field] = false
  }).catch(() => {
    errorMap[field] = true
  })
}


// 注入
provide('a-form', {
  validate,
  getErrorMap: () => errorMap
})
</script>

2. 具体实现------From-Item

From-Item 组件的作用:提供 onChange 方法以及需要校验的字段 field,当 validate 执行完后获取校验的结果,然后决定是否展示错误提示

js 复制代码
<template>
  <div>
    <slot></slot>
    <div style="color: red" v-if="data.showError">字段必填</div>
  </div>
</template>

<script lang="ts" setup>
import { provide, inject, reactive } from 'vue';
const props = defineProps<{ field: string }>();
const AForm = inject<{ validate: (field: string) => Promise<any>; getErrorMap: any }>('a-form');
const data = reactive({
  showError: false,
});

const onChange = () => {
  setTimeout(() => {
    if (AForm) {
      const showError = AForm.getErrorMap()[props.field]
      // 决定展示不展示错误提示
      data.showError =showError
    }
  })
}

// 注入
provide('a-form-item', {
  getField: () => props.field,
  onChange
});
</script>

3. 具体实现------Input

Input 组件的作用:当输入框的值 value 发生改变时,依次触发 validate、onChange 函数去执行校验

js 复制代码
<template>
  <input @input="onChange" :value="data.inputValue" />
</template>

<script lang="ts" setup>
import { reactive, watch, inject } from 'vue';

const props = defineProps<{ modelValue: string }>();
const emits = defineEmits(['update:modelValue']);

// 接收注入
const AForm = inject<{ validate: (field: string) => Promise<any>; getErrorMap: any }>(
  'a-form',
);
const AFormItem = inject<{ getField: () => string; onChange: () => void }>('a-form-item')

// 内部维护 value
const data = reactive({
  inputValue: props.modelValue,
});

watch(
  () => props.modelValue,
  v => {
    data.inputValue = v;
  },
);

// value change 时,执行 validate、onChange
const onChange = (e: Event) => {
  emits('update:modelValue', (e.target as HTMLInputElement).value);
  if (AForm && AFormItem) {
    AForm.validate(AFormItem.getField())
    AFormItem.onChange()
  }
};
</script>

二、Vue 项目中 data-v-xxx 的生成

先来看一个问题:

大意是说 Vue scoped 的 data-v-xxx 是根据文件相对路径计算的,如果微前端的两个 Vue 子项目采用相同的路径结构,那么算出来的 data-v-xxx 是一样的,可能会导致样式冲突。

  • webpack + vue-loader 对 vue2 的处理
js 复制代码
  const shortFilePath = path
    .relative(rootContext || process.cwd(), filename)
    .replace(/^(..[/])+/, '').replace(//g, '/')
  
  const id = hash(
    isProduction
      ? shortFilePath + '\n' + source.replace(/\r\n/g, '\n')
      : shortFilePath
  )
  • vite + @vitejs/plugin-vue 对 vue3 的处理
js 复制代码
import path from "node:path";
import { createHash } from "node:crypto";
import slash from "slash";

function getHash(text) {
  return createHash("sha256").update(text).digest("hex").substring(0, 8);
}

// 获取文件相对路径
const normalizedPath = slash(path.normalize(path.relative(root, filename)));
// 计算 ID
descriptor.id = getHash(normalizedPath + (isProduction ? source : ""));

可以发现,不管是 vue-loader 还是 @vitejs/plugin-vue,data 属性 ID 的生成机制都是一样的,即:

  • 开发环境下会根据文件相对路径 生成唯一 ID,比如 vite 中 src/App.vue 固定生成 7a7a37b1
  • 生产环境下会根据文件相对路径+文件内容共同生成唯一 ID

为什么开发环境和生产环境的 ID 计算方式不一样?

首先,开发环境下最好不要加入文件内容进行 hash 计算,因为 hash 计算是耗时的,内容越多耗时越长,而且还会频繁变动节点样式,徒增成本。

那生产环境为什么还要加入文件内容计算 hash 呢?如果 ID 与文件内容无关,就可以实现稳定的 data 属性,对于 E2E 测试用例,就可以直接使用 data 属性进行元素寻址。

三、Vue项目打包后产生的 JS 请求

如果我们关注 Vue 项目的打包,会发现在打包后产生若干 js 请求文件,如 app.jschunk-vendors.js ,在这里深入探讨 Vue 项目打包后产生的文件都是什么。

注:Vue 打包后和 Vue 项目直接本地运行后产生的 js 请求文件一致,为了方便展示,这里采用本地启动项目后产生的文件进行探讨。

1. 基础请求文件

我们创建一个最基础的 Vue 项目,不引入路由、Vuex 及其他任何第三方 JS,运行项目,我们可以发现 Vue 产生了两个请求文件:

name status type size time
chunk-vendors.js 200 script 392 kB 67 ms
app.js 200 script 15.5 kB 70 ms

2. app.js 文件

我们在 app.vue 中添加大量文字,重新运行项目,可以发现,app.js 的大小发生了改变,但 chunk-vendors.js 的大小并没有发生改变:

结论:打包后,app.js 就是我们的业务层代码,如 main.js 里面的内容、各个 Vue 组件里面的内容

3. chunk-vendors.js 文件

我们在 app.vue 中安装并引入 moment.js:

js 复制代码
<script>
import moment from "moment"
moment()
export default {
  name: "App",
};
</script>

重新运行项目,我们可以发现 chunk-vendors.js 的尺寸几乎翻倍,但 app.js 的大小几乎没变:

结论:打包后,chunk-vendors.js 集成的是 node_modules 里面用到的 js 内容

4. 路由请求文件

以一个企业级项目为例,我们在项目中定义一个路由页面。

js 复制代码
const headquarters = () => import("@/views/headquarters/index.vue");
export default [
  {
    path: "/",
    name: "headquarters",
    component: headquarters,
    meta: {
      keepAlive: false,
      title: "总部展示大屏",
      name: "总部展示大屏",
    },
  },
];

启动项目http://localhost:8080/demo/,我们可以看到页面请求了3个JS文件

注:链接中的 demo 是项目的 baseUrl

它们的作用如下:

请求文件 作用 资源尺寸 请求时间
0.js headquarters路由内容 (prefetch cache) 60 ms
app.js main.js等非路由页面内容 893 kB 12 ms
chunk-vendors.js node_modules内容 37.9 MB 427ms

我们再增加一个路由

js 复制代码
  {
    path: "/data-analysis",
    name: "dataAnalysis",
    component: () => import("@/views/data-analysis/index.vue"),
    meta: {
      keepAlive: false,
      title: "数据分析",
      name: "数据分析",
    },
  },

重新输入链接 http://localhost:8080/demo ,你可能会疑惑,明明添加了路由,但是为什么请求文件还是 0.js?

实际上,因为我们没有访问对应路由链接,输入包含新路由的链接,就可以看到新路由文件的请求,这里的 1.js 就是 dataAnalysis 组件内容:

四、远程加载 .vue 组件并使用

平时咱们都是在项目写好组件使用,那么如何从服务端加载 .vue 文件用并在项目中跑起来呢? 大致分以下三步:

  1. http 请求远程文件
  2. 将文本组件转换为 vue 响应式组件
  3. component :is 渲染

1. vue3-sfc-loader

Vue3/Vue2 单文件组件加载器,在运行时从 html/js 动态加载 .vue 文件,不需要 node.js 环境,不需要 webpack 构建步骤,这里以 Vue2 为例:

Vue2 的使用路径在 /dist 目录下:

js 复制代码
import { loadModule } from 'vue3-sfc-loader/dist/vue2-sfc-loader.js'

2. 封装动态远程加载组件

使用 is 动态渲染加载的组件,同时使用透传属性 v-bind,事件响应处理 v-on="$listeners"

js 复制代码
<template>
  <div class="async-component">
    <component :is="remote" v-if="remote" v-bind="$attrs" v-on="$listeners" />
  </div>
</template>

动态加载:

  • moduleCache 将 Vue 对象传下去(强制性)
  • getFile 获取远程文件逻辑,此处使用原生 fetch API 请求
  • addStyle 创建并添加样式
js 复制代码
const com = await loadModule(url, {
    moduleCache: {
      vue: Vue,
    },
    // 获取文件
    async getFile (url) {
      const res = await fetch(url)
      if (!res.ok) {
        throw Object.assign(new Error(`${res.statusText}  ${url}`), { res })
      }
      return {
        getContentData: asBinary => (asBinary ? res.arrayBuffer() : res.text()),
      }
    },
    // 添加样式
    addStyle (textContent) {
      const style = Object.assign(document.createElement('style'), { textContent })
      const ref = document.head.getElementsByTagName('style')[0] || null
      document.head.insertBefore(style, ref)
    },
  })

五、render函数实现一个菜单下拉框

1. 下拉框组件

html 复制代码
// Select.vue
<template>
  <div class="select-wrap">
    <span>福利商城</span>
    <span>Saas平台</span>
    <span>活动定制</span>
  </div>
</template>

2. 渲染组件

我们要将这个组件渲染在网页上,操作应该是这样的:当鼠标移动到产品服务时,将下拉框组件作为一个组件实例渲染在页面的合适位置,要知道组件渲染的位置,我们必须知道父组件的 dom 位置,我们通过 ref 来获取父组件的 dom 信息:

js 复制代码
// App.vue
<div ref="select">
  <span class="name">产品服务</span> 
</div>
<script setup >
import { ref } from "vue"
import Select from "./Select.vue"
import { createVNode, h, render, VNode } from 'vue'

const select = ref()

// 在 Vue3 中,渲染一个 Vonde,核心逻辑如下:
function createDom(){
  const left = select.value.offsetLeft + "px"
  const width = select.value.getBoundingClientRect().left + "px"
  const props = {
    width,
    left,
  }
  //1、创造包裹虚拟节点的div元素
  const container = document.createElement('div');
  //2、创造虚拟节点
  let vm = createVNode(Select,props)
  //3、将虚拟节点创造成真实DOM
  render (vm, container)
  //4、将渲染的结果放到body下
  document.body.appendChild(container.firstElementChild) 
}
</script>

其中,prop 是传递给 Select 组件的距离参数,在组件内设置即可

3. 销毁组件

销毁组件,我们可以使用 render 渲染一个空对象即可:

js 复制代码
render (vm, container)

如果需要子组件来销毁自身,我们可以使用父子传值

js 复制代码
<template>
  <div class="select-wrap" @mouseleave="beforeUnload">
    <span>福利商城</span>
    <span>Saas平台</span>
    <span>活动定制</span>
  </div>
</template>
<script   setup>
const emit = defineEmits(['destroy'])
function beforeUnload(){
 emit('destroy')
}
</script>

父组件里,我们需要在 props 中添加一个 onDestroy 函数

js 复制代码
function createDom(){
  const left = select.value.offsetLeft + "px"
  const width = select.value.getBoundingClientRect().left + "px"
  const props = {
    width,
    left,
    onDestroy: () => {
      render(null, container)
    },
  }
  //1、创造包裹虚拟节点的div元素
  const container = document.createElement('div');
  //2、创造虚拟节点
  let vm = createVNode(Select,props)
  //3、将虚拟节点创造成真实DOM
  render (vm, container)
  //4、将渲染的结果放到body下
  document.body.appendChild(container.firstElementChild) 
}
相关推荐
吃杠碰小鸡8 分钟前
commitlint校验git提交信息
前端
天天进步201528 分钟前
Vue+Springboot用Websocket实现协同编辑
vue.js·spring boot·websocket
虾球xz38 分钟前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇44 分钟前
HTML常用表格与标签
前端·html
疯狂的沙粒1 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员1 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐1 小时前
前端图像处理(一)
前端
程序猿阿伟1 小时前
《智能指针频繁创建销毁:程序性能的“隐形杀手”》
java·开发语言·前端
疯狂的沙粒1 小时前
对 TypeScript 中函数如何更好的理解及使用?与 JavaScript 函数有哪些区别?
前端·javascript·typescript
瑞雨溪1 小时前
AJAX的基本使用
前端·javascript·ajax