一个vue页面复用方案

news2024/9/24 23:31:53

前言

问大家一个问题,曾经的你是否也遇到过,一个项目中有好几个页面长得基本相同,但又差那么一点,想用 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属性为insert
            if (type === 'insert') {
                templateElements.forEach((tel) => {
                    // 通过position属性判断插入位置 默认为after
                    if (position === 'before') {
                        el.childNodes.forEach((child) => {
                            tel.insertAdjacentHTML('beforebegin', child.toString());
                        });
                    } else {
                        el.childNodes.forEach((child) => {
                            tel.insertAdjacentHTML('afterend', child.toString());
                        });
                    }
                });
            }
            // type属性为append
            if (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属性为replace
            if (type === 'replace') {
                templateElements.forEach((tel) => {
                    tel.replaceWith(...el.childNodes);
                });
            }
            // type属性为remove
            if (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.coloradmin.cn/o/1920716.html

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

相关文章

在Anaconda环境中安装TensorFlow+启动jupyter notebook

1.打开cmd&#xff0c;输入C:\Users\xy>conda create -n tensorflow python3.7 这是在环境中创建了一个名为tensorflow的环境&#xff0c;具体会显示以下信息&#xff1a; C:\Users\xy>conda create -n tensorflow python3.7 Retrieving notices: ...working... done Co…

等保从哪些方面进行测评

等保&#xff0c;全名叫做信息安全等级保护&#xff0c;顾名思义就是指根据信息系统在国家安全、社会稳定、经济秩序和公共利益方便的中重要程度以及风险威胁、安全需求、安全成本等因素&#xff0c;将其划分不同的安全保护等级并采取相应等级的安全保护技术、管理措施、以保障…

Python面试宝典第11题:最长连续序列

题目 给定一个未排序的整数数组 nums &#xff0c;找出数字连续的最长序列&#xff08;不要求序列元素在原数组中连续&#xff09;的长度。请你设计并实现时间复杂度为 O(n) 的算法解决此问题。 示例 1&#xff1a; 输入&#xff1a;nums [100,4,200,1,3,2] 输出&#xff1a;…

前端JS特效第30集:jQuery焦点图插件edslider

jQuery焦点图插件edslider&#xff0c;先来看看效果&#xff1a; 部分核心的代码如下(全部代码在文章末尾)&#xff1a; <!DOCTYPE html> <html lang"zh"> <head> <meta charset"UTF-8"> <meta http-equiv"X-UA-Compatib…

台湾精锐APEX伺服行星减速机发热原因及解决方案

在实际运行过程中台湾精锐APEX伺服行星减速机常常会遇到发热的问题&#xff0c;这不仅影响减速机的正常运转&#xff0c;还可能缩短其使用寿命&#xff0c;甚至引发安全事故。因此&#xff0c;了解APEX伺服行星减速机发热的原因及相应的解决方案&#xff0c;对于保障生产线的稳…

C语言——基础框架、变量、运算符

基础框架&#xff1a; #include<stdio.h> //编译预处理指令int main() //程序的入口主函数main { //程序&#xff08;函数、功能&#xff09;结束标志return 0; //程序退出前返回给调用者&#xff08;操作系统&#xff09;的值…

MySQL实战45讲学习笔记(持续更新ing……)

文章目录 一、基础架构&#xff1a;一条SQL查询语句是如何执行的&#xff1f;概览连接器查询缓存分析器优化器执行器 二、日志系统&#xff1a;一条SQL更新语句是如何执行的&#xff1f;redo logbinlog两阶段提交 一、基础架构&#xff1a;一条SQL查询语句是如何执行的&#xf…

深度学习DeepLearning多元线性回归 学习笔记

文章目录 多维特征变量与术语公式多元线性回归正规方程法Mean normalizationZ-score normalization设置合适的学习率Feature engineering 多维特征 变量与术语 列属性xj属性数n x ⃗ \vec{x} x (i)行向量某个值 x ⃗ j i \vec{x}_j^i x ji​上行下列均值μ标准化标准差σsigm…

无线速度传感器

对高中物理实验中的速度测量方法进行改进&#xff0c;利用安装在小车上的无线光电门来测量小车运动过程中的速度&#xff0c;即满足了精度的要求&#xff0c;又可以研究物体的运动过程。无线光电门和数据接收器间采用蓝牙无线传输的方式&#xff0c;电脑端的软件使用Flash来制作…

vant-app中加的custom-class为啥审查元素时看不到自定义类名

如下图&#xff1a; 我们发现在左侧审查元素时确实看不到&#xff0c;但是在右侧是可以看到&#xff0c;而且样式是生效的。 是不是微信开发者工具的bug?

SQL基础-DQL 小结

SQL基础-DQL 小结 学习目标&#xff1a;学习内容&#xff1a;SELECTFROMWHEREGROUP BYHAVINGORDER BY运算符ASC 和 DESC 总结 学习目标&#xff1a; 1.理解DQL&#xff08;Data Query Language&#xff09;的基本概念和作用。 2.掌握SQL查询的基本语法结构&#xff0c;包括SEL…

微软子公司Xandr遭隐私诉讼,或面临巨额罚款

近日&#xff0c;欧洲隐私权倡导组织noyb对微软子公司Xandr提起了诉讼&#xff0c;指控其透明度不足&#xff0c;侵犯了欧盟公民的数据访问权。据指控&#xff0c;Xandr的行为涉嫌违反《通用数据保护条例》&#xff08;GFPR&#xff09;&#xff0c;因其处理信息并创建用于微目…

C#开发:VS2022中配置TFS(Team Foundation Server)和使用

第一步&#xff0c;点出团队资源管理器 第二步&#xff0c;输入服务器地址 第三步&#xff0c;输入配置地址和账密&#xff08;问管理员&#xff09; 输入配置地址&#xff1a;$/xxxx 输入工作区地址&#xff1a;本地随便一个路径 第四步&#xff0c;获取最新代码 第五步&#…

空调元件的介绍

保险丝管 1、保险丝管在电脑板上用FC1.2&#xff08;FUSE&#xff09;表示&#xff0c;主要用于起过电流保护。 2、故障现象&#xff1a;整机无电不工作 3、检测方法&#xff1a; 目测观察保险丝是否熔断&#xff0c;如是应更换&#xff1b; 4、注意事项&#xff1a; 如果电…

Python酷库之旅-第三方库Pandas(018)

目录 一、用法精讲 44、pandas.crosstab函数 44-1、语法 44-2、参数 44-3、功能 44-4、返回值 44-5、说明 44-6、用法 44-6-1、数据准备 44-6-2、代码示例 44-6-3、结果输出 45、pandas.cut函数 45-1、语法 45-2、参数 45-3、功能 45-4、返回值 45-5、说明 4…

开启新纪元!被AI驱动的游戏世界,提升游戏体验

随着人工智能的高速发展&#xff0c;人工智能逐渐应用到了生活中的方方面面&#xff0c;人工智能在游戏中也有诸多应用&#xff0c;在游戏里领域扮演了相当重要的角色。游戏AI是伴随着电子游戏而出现的&#xff0c;在早期的游戏中就出现了对抗类AI角色&#xff0c;后来逐渐出现…

服务器数据恢复—开盘修复raid5阵列硬盘故障的数据恢复案例

服务器存储数据恢复环境&#xff1a; 某品牌P2000存储&#xff0c;存储中有一组由8块硬盘&#xff08;包含一块热备盘&#xff09;组建的raid5阵列。上层部署VMWARE ESX虚拟化平台。 服务器存储故障&#xff1a; 存储在运行过程中有两块硬盘指示灯亮黄色。经过运维人员的初步检…

Sentinel 学习笔记

Sentinel 学习笔记 作者&#xff1a;王珂 邮箱&#xff1a;49186456qq.com 文章目录 Sentinel 学习笔记[TOC] 前言一、基础概念二、Sentinel控制台2.1 安装控制台2.2 簇点链路2.3 请求限流2.4 线程隔离2.5 服务降级2.6 服务熔断 三、Sentinel客户端3.1 原始Jar包客户端3.2 Sp…

【Windows】XMedia Recode(免费的专业视频格式转换软件)及同类型软件介绍

今天给大家介绍的这款软件叫XMedia Recode&#xff0c;这是一款免费的专业视频格式转换软件。有需要的朋友可以了解一下哦。 软件介绍 XMedia Recode 是一款功能强大的免费视频转换和音频转换软件&#xff0c;它支持多种格式的视频和音频文件转换&#xff0c;以及简单的编辑…

收银系统源码-商品套餐功能视频介绍

千呼新零售2.0系统是零售行业连锁店一体化收银系统&#xff0c;包括线下收银线上商城连锁店管理ERP管理商品管理供应商管理会员营销等功能为一体&#xff0c;线上线下数据全部打通。 适用于商超、便利店、水果、生鲜、母婴、服装、零食、百货、宠物等连锁店使用。 详细介绍请…