一个vue页面复用方案

前言

问大家一个问题,曾经的你是否也遇到过,一个项目中有好几个页面长得基本相同,但又差那么一点,想用 vue extends 继承它又不能按需继承html模板部分,恰好 B 页面需要用的 A 页面 80% 的模板,剩下的 20% 由 B 页面自定义,举个栗子:

我们假设这是两个页面,B页面比A页面多了个p标签,剩余的东西都一样,难道仅仅是因为这一个 p标签就要重新写一份模板吗?相信大部分伙伴解决方式是把公共部分抽成一个组件来用,这是一个好的做法。没错,但是来了,老板让你在 标题1、标题2下面分别插入一段内容,这会儿你是不是头大了?难道只能重写一份了吗?当然不是,来开始我们的填坑之路~当你的业务能用插槽或者组件抽离的方式固然更好,以下内容仅针对当你项目达到一定体量,vue老三套难以处理的情况下采用

准备工作

准备以下工具包:

  • node-html-parser: 将html生成dom树 官网
npm install --save node-html-parser

思路

  1. 子页面提供继承的父页面的路径,如下:
<template extend="./xxx.vue"> 
</template>
  1. 子页面需要通过一个自定义标签(假设是 extend)的方式,来决定如何拓展父页面,如下就应该是一个替换的操作,它最少应该具备拓展类型 type 与目标节点 target 属性。
<template extend="./xxx.vue"><div><extend type="replace" target="#div_1"><a>通过replace替换掉父页面下id为div_1的元素 </a></extend></div>
</template>

最终它生成的应该是除了 id 为 div_1元素被<a>通过replace替换掉父页面下id为div_1的元素 </a>替换掉之外,剩下的全部和xxx.vue一样的页面。

梳理需求点

子页面继承父页面既可以完全继承,也可以通过某种方式以父页面为基板,对其进行增、删、改。方便理解,我们先定义一个自定义标签 extend,子页面通过该标签对其继承的页面操刀动手术,为了实现一个比较完善的继承拓展,extend 标签需要具备以下属性:

Extend Attributes
参数说明类型可选值
type指定扩展类型stringinsert(插入)、replace(替换)、remove(移除)、append(向子集追加)
position指定插入的位置(仅在 type 取值 insert 时生效)stringbefore(目标前)、after(目标后)
指定插入的位置(仅在 type 取值 append 时生效,用于指定插入成为第几个子节点)number-
target指定扩展的目标string

实现需求

新建一个vue2的项目,项目结构如下:

我们的继承拓展通过自定义loader在编译的时候实现,进入到src/loader/index.js

const extend = require('./extend');
module.exports = function (source) {// 当前模块目录const resourcePath = this.resourcePath;// 合并const result = new extend(source, resourcePath).mergePage();// console.log('result :>> ', result);// 返回合并后的内容this.callback(null, result);
};

实现继承拓展主要逻辑代码:src/loader/extend.js

const parser = require('node-html-parser');
const fs = require('fs');
const pathFile = require('path');
/*** 通过node-html-parser解析页面文件重组模板* @param {String} source 页面内容* @param {String} resourcePath 页面目录* @returns {String} 重组后的文件内容*/
class Extend {constructor(source, resourcePath) {this.source = source;this.resourcePath = resourcePath;}// 合并页面mergePage() {// 通过node-html-parser解析模板文件const pageAst = parser.parse(this.source).removeWhitespace();// 获取template标签extend属性值const extendPath = pageAst.querySelector('template').getAttribute('extend');if (!extendPath) {return pageAst.toString();}// extendPath文件内容const extendContent = fs.readFileSync(pathFile.resolve(pathFile.dirname(this.resourcePath), extendPath), 'utf-8');// extendContent文件解析const extendAst = parser.parse(extendContent).removeWhitespace();// 获取页面文件标签为extend的元素const extendElements = pageAst.querySelectorAll('extend');extendElements.forEach((el) => {// 获取对应属性值const type = el.getAttribute('type');const target = el.getAttribute('target');const position = parseInt(el.getAttribute('position'));// 匹配模板符合target的元素let templateElements = extendAst.querySelectorAll(target);// type属性为insertif (type === 'insert') {templateElements.forEach((tel) => {// 通过position属性判断插入位置 默认为afterif (position === 'before') {el.childNodes.forEach((child) => {tel.insertAdjacentHTML('beforebegin', child.toString());});} else {el.childNodes.forEach((child) => {tel.insertAdjacentHTML('afterend', child.toString());});}});}// type属性为appendif (type === 'append') {templateElements.forEach((tel) => {const elNodes = el.childNodes;let tlNodes = tel.childNodes;const len = tlNodes.filter((node) => node.nodeType === 1 || node.nodeType === 3).length;// 未传position属性或不为数字、大于len、小于0时默认插入到最后if(isNaN(position) || position > len || position <= 0){elNodes.forEach((child) => {tel.insertAdjacentHTML('beforeend', child.toString());});}else {tlNodes =  [...tlNodes.slice(0, position-1), ...elNodes, ...tlNodes.slice(position-1)]tel.set_content(tlNodes);}});}// type属性为replaceif (type === 'replace') {templateElements.forEach((tel) => {tel.replaceWith(...el.childNodes);});}// type属性为removeif (type === 'remove') {templateElements.forEach((tel) => {tel.remove();});}});// 重组文件内容const template = extendAst.querySelector('template').toString();const script = pageAst.querySelector('script').toString();const style = extendAst.querySelector('style').toString() + pageAst.querySelector('style').toString() return`${template}${script}${style}`}}
module.exports = Extend;

好的,自定义loader已经编写完成,在vue.config.js里面配置好我们的loader

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({configureWebpack: {module: {rules: [{test: /\.vue$/,use: [{loader: require.resolve('./src/loader'),},],},],},},
})

接下来我们尝试编写A页面和B页面:

<template><div class="template"><div id="div_1" class="div">父页面的div_1</div><div id="div_2" class="div">父页面的div_2</div><div id="div_3" class="div">父页面的div_3</div><div id="div_4" class="div">父页面的div_4</div><div id="div_5" class="div">父页面的div_5</div><div id="div_6" class="div">父页面的div_6</div><div id="div_7" class="div">父页面的div_7</div><div id="div_8" class="div">父页面的div_8</div></div>
</template>
<script>
export default {name: 'COM_A',props: {msg: String}
}
</script>
<style scoped>
.div {color: #42b983;font-size: 1.5em;margin: 0.5em;padding: 0.5em;border: 2px solid #42b983; border-radius:  0.2em;
}
</style>

B.vue:

<template extend="./A.vue"><div><extend type="insert" target="#div_1" position="after"><div id="div_child" class="div">子页面的div_5</div></extend><extend type="append" target="#div_3" position="2"><a> 子页面通过append插入的超链接 </a></extend></div>
</template>
<script>
import A from './A.vue'
export default {name: 'COM_B',extends: A,//继承业务逻辑代码props: {msg: String}
}
</script>
<style scoped>
#div_child {color: #d68924;font-size: 1.5em;margin: 0.5em;padding: 0.5em;border: 2px solid #d68924;
}
a {color: blue;font-size: 0.7em;
}
</style>

我们在App.vue下引入B.vue

<template><div id="app"><B/></div>
</template>
<script>
import B from './components/B.vue'
export default {name: 'App',components: {B}
}
</script>
<style>
#app {font-family: Avenir, Helvetica, Arial, sans-serif;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;text-align: center;color: #2c3e50;margin-top: 60px;
}
</style>

当我们执行编译的时候,实际上B.vue的编译结果如下:

<template><div class="template"><div id="div_1" class="div">父页面的div_1</div><div id="div_child" class="div">子页面的div_5</div><div id="div_2" class="div">父页面的div_2</div><div id="div_3" class="div">父页面的div_3<a> 子页面通过append插入的超链接 </a></div><div id="div_4" class="div">父页面的div_4</div><div id="div_5" class="div">父页面的div_5</div><div id="div_6" class="div">父页面的div_6</div><div id="div_7" class="div">父页面的div_7</div><div id="div_8" class="div">父页面的div_8</div></div>
</template>
<script>
import A from './A.vue'
export default {name: 'COM_B',extends: A,//继承业务逻辑代码props: {msg: String}
}
</script>
<style scoped>
.div {color: #42b983;font-size: 1.5em;margin: 0.5em;padding: 0.5em;border: 2px solid #42b983;border-radius: 0.2em;
}
</style>
<style scoped>
#div_child {color: #d68924;font-size: 1.5em;margin: 0.5em;padding: 0.5em;border: 2px solid #d68924;
}a {color: blue;font-size: 0.7em;
}
</style>

注意我们在B.vue使用了extends继承了组件A,这里是为了能复用业务逻辑代码,最后我们运行代码,页面输出为:

image.png

结语

在真实的项目当中,我们遇到大量重复的页面但是又有小区别的页面,是可以通过这种方式减少我们的代码量,当然也许有更好的办法,也希望大伙能提出宝贵的建议。

最后引用一下 @XivLaw 老哥的评论:有很多人说通过cv就能解决,但是当你的业务有成千上万个页面是趋同,并且具有相同的基本功能,当界面需要统一调整或者需要进行ui统一管控的时候,cv就成了你的累赘了。 也有朋友说通过组件化和插槽解决,组件化是一个不错的方案,但是当成千上万个趋同的界面存在时,插槽并一定能覆盖所有的业务定制化。 使不使用这种方式,主要看你的业务。

直白一点说就是:我现在有一千个页面几乎一样,有的页面是头部多一点东西,有的是底部,有的是某个按钮旁边多一个按钮,有的是输入框之间多个输入框,ui或者界面或者同时需要添加固定功能,需要调整的时候,这一千个页面要怎么调?

仅供参考!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/45415.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【验收支撑】软件系统验收计划书(直接套用原件doc)

编写软件验收计划是软件开发过程中的一个关键步骤&#xff0c;其重要性体现在以下几个方面&#xff1a; 明确验收标准&#xff1a;软件验收计划详细列出了验收的标准、测试方法、测试环境等&#xff0c;确保所有相关人员对验收的期望和要求有清晰的认识。这有助于避免在验收阶段…

triton需要cuda11.8版本--so 安装个11.8的cuda

1.安装一个11.8的cuda 查看ubunto版本 lsb_release -a查看cpu架构 uname -m到官网得到相应的命令&#xff1a;CUDA Toolkit Archive | NVIDIA Developer 依次输入两条命令。如果没有没有sudo权限就将cuda安装路径修改在自己目录下&#xff1a; sh cuda_11.8.0_520.61.05_li…

论文AIGC率超标?一键降重快速搞定

如何有效降低AIGC论文的重复率&#xff0c;也就是我们说的aigc如何降重&#xff1f;AIGC疑似度过高确实是个比较愁人的问题。如果你用AI帮忙写了论文&#xff0c;就一定要在交稿之前做一下AIGC降重的检查。一般来说&#xff0c;如果论文的AIGC超过30%&#xff0c;很可能会被判定…

【EI征稿】第四届机器人、自动化与智能控制国际会议

【快速通道】 参会方式&#xff1a;担任会议committee成员、组建workshop 、参会报告、参会交流、审稿专家、投稿参会。 会议地点&#xff1a; 湖南 长沙 会议时间&#xff1a;12月6日-9日 会议检索&#xff1a;EI检索 会议官网&#xff1a;https://www.icraic.org/ 投稿链接&a…

华为910b推理Qwen1.5-72b

前情提要&#xff1a;华为910b部署训练推理大模型&#xff0c;本人之前并没有接触过&#xff0c;所以&#xff0c;写此文档进行记录。 &#xff08;注意&#xff1a;版本适配很重要&#xff01;&#xff01;不然就像我一样走了好多坑~~~&#xff09; 首先&#xff0c;看一张图…

大鲸鱼—docker 基本概念及安装使用

目录 一、docker前言 1.什么是Docker&#xff1f; 2.Docker的宗旨 3.容器的优点 4.Docker与虚拟机的区别 5.Docker核心概念 镜像 容器 仓库 6.为什么要用容器 7.容器越来越受欢迎的原因 8.容器在内核中支持2种重要技术 二、Docker安装 三、Docker 镜像操作 1.搜…

旷视AI开源新突破:上传照片即可生成表情包视频!

日前&#xff0c;旷视科技发布了一项新的开源AI人像视频生成框架——MegActor。该框架让用户只需输入一张静态肖像图片和一段视频&#xff08;如演讲、表情包、rap&#xff09;&#xff0c;便可生成一段表情丰富、动作一致的AI人像视频。生成的视频长度取决于输入的视频长度。与…

API接口详解及其在电子商务中的应用研究

目录 引言 一、API接口概述 1.1 API接口定义 1.2 API接口的作用 二、API接口分类 2.1 根据使用对象分类 2.2 根据协议和数据格式分类 三、API接口设计原则 四、API接口在电子商务中的应用 4.1 数据交换与集成 4.2 个性化推荐与营销 4.3 库存管理与订单处理 4.4 数…

C++ 数据结构探索:构建高效程序的基础

C 数据结构探索&#xff1a;构建高效程序的基础 在C编程的广阔领域中&#xff0c;数据结构是理解和实现高效、可维护程序的核心。数据结构是计算机存储、组织数据的方式&#xff0c;它们使得数据访问和修改操作更加高效。本文将带您走进C中几种常见且重要的数据结构&#xff0…

数据湖仓一体(一) 编译hudi

目录 一、大数据组件版本信息 二、数据湖仓架构 三、数据湖仓组件部署规划 四、编译hudi 一、大数据组件版本信息 hudi-0.14.1zookeeper-3.5.7seatunnel-2.3.4kafka_2.12-3.5.2hadoop-3.3.5mysql-5.7.28apache-hive-3.1.3spark-3.3.1flink-1.17.2apache-dolphinscheduler-3.1.9…

气膜仓储与传统仓储的成本优势对比—轻空间

随着物流和仓储需求的不断增长&#xff0c;企业对仓储设施的要求也日益提高。传统仓储设施虽然具有一定的优势&#xff0c;但在建设和运营成本上往往较高。近年来&#xff0c;气膜仓储作为一种新型仓储方式&#xff0c;以其独特的优势逐渐受到市场青睐。轻空间将详细探讨气膜仓…

spring boot实现短信验证码功能

1、到阿里云网站申请 https://market.aliyun.com/products/5700000 2/cmapi00046920.html2、配置文件&#xff0c;可申请测试 sms:app-code: xxxxxxxxxtemplate-id: xxxxxxx3、使用restTemplate用于第三方接口调用 package com.example.rsocketclient.config;import org.spr…

Altium Designer输出Gerber文件步骤

参考链接 技术指导&#xff1a;Altium Designer输出Gerber文件步骤 (jlc.com)https://www.jlc.com/portal/server_guide_10171.html 特此记录 anlog 2024年7月12日

springboot 程序运行一段时间后收不到redis订阅的消息

springboot 程序运行一段时间后收不到redis订阅的消息 问题描述 程序启动后redis.user.two主题正常是可以收到消息的&#xff0c;发一条收一条&#xff0c;但是隔一段时间后&#xff1b;就收不到消息了&#xff1b; 此时如果你手动调用发送另外一个消息订阅redis.user.two2&…

豆包AI智能助手:知识库整理与智能检索的双重应用

引言 抖音豆包&#xff0c;作为字节跳动开发的AI智能助手&#xff0c;不仅仅是一个简单的虚拟角色&#xff0c;而是集成了深度学习和自然语言处理技术的复杂系统&#xff0c;专门设计来增强用户在抖音平台上的体验。同时&#xff0c;豆包的应用示范了AI智能助手在知识库管理和…

华为HCIP Datacom H12-821 卷38

1.多选题 下面关于 BGP中的公认属性的描述&#xff0c;正确的是 A、公认必遵属性是所有BGP路由器都识别&#xff0c;且必须存在于Updata消息中心 B、BGP必须识别所有公认属性 C、公认属性分为公认必遵和可选过渡两种 D、公认任意属性是所有BGP造由器都可以识别&#xff0c…

FX110网:香港证监会对Yomaex等多家虚拟资产交易平台发出警告

近日&#xff0c;香港证券及期货事务监察委员会&#xff08;香港证监会&#xff0c;SFC&#xff09;对虚拟资产交易平台Yomaex发出警告&#xff0c;运营网址为yomaexd.com.该平台曾多次被FX110网发文曝光。相关阅读 《Yomaex平台“高额回报”是“高级陷阱”&#xff0c;公务员也…

Java中Timer定时器的使用

定时器Timer 概述&#xff1a;开发中&#xff0c;有时候我们会需要一些周期性的操作&#xff0c;每隔一段时间去做一件事&#xff0c;在Java中可以通过Timer定时器去实现。Timer是一种工具&#xff0c;线程用其安排以后在后台线程中执行的任务。可安排任务执行一次&#xff0c…

Oracle11g_RAC for vmware workstation 安装教程(on suse11)

一、前言 本文介绍在vmware workstation环境下&#xff0c;基于suse11sp1操作系统安装Oracle11g RACASM 数据库&#xff08;两节点&#xff09;。 1.1 RAC中的基本概念 安装ORACLE RACASM前&#xff0c;您可能需要事先简要的了解RAC&#xff0c;CRS&#xff0c;ASM的概念。 1.1…

easyExcel 不规则模板导入数据

文章目录 前言一、需求和效果二、难点和思路三、全部代码踩坑 前言 之前分享的 EasyExcel 批量导入并校验数据&#xff0c;仅支持规则excel&#xff0c;即首行表头&#xff0c;下面对应数据&#xff0c;无合并单元格情况。 本篇主要解决问题&#xff1a; 模板excel 表头不在首…