一个vue页面复用方案

07-14 1495阅读

前言

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

一个vue页面复用方案

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

准备工作

准备以下工具包:

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

    思路

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

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

    梳理需求点

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

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

    实现需求

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

    一个vue页面复用方案我们的继承拓展通过自定义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  {
                                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页面:

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

    B.vue:

      
        
          子页面的div_5
        
        
           子页面通过append插入的超链接 
        
      
    
    
    import A from './A.vue'
    export default {
      name: 'COM_B',
      extends: A,//继承业务逻辑代码
      props: {
        msg: String
      }
    }
    
    
    #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;
    }
    
    

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

      
        
      
    
    
    import B from './components/B.vue'
    export default {
      name: 'App',
      components: {
        B
      }
    }
    
    
    #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;
    }
    
    

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

      
        父页面的div_1
        子页面的div_5
        父页面的div_2
        
          父页面的div_3
           子页面通过append插入的超链接 
        
        父页面的div_4
        父页面的div_5
        父页面的div_6
        父页面的div_7
        父页面的div_8
      
    
    
    import A from './A.vue'
    export default {
      name: 'COM_B',
      extends: A,//继承业务逻辑代码
      props: {
        msg: String
      }
    }
    
    
    .div {
      color: #42b983;
      font-size: 1.5em;
      margin: 0.5em;
      padding: 0.5em;
      border: 2px solid #42b983;
      border-radius: 0.2em;
    }
    
    
    #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;
    }
    
    

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

    一个vue页面复用方案

    结语

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

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

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

    仅供参考!!!

VPS购买请点击我

文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

目录[+]