前言
问大家一个问题,曾经的你是否也遇到过,一个项目中有好几个页面长得基本相同,但又差那么一点,想用 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
复制
思路
- 子页面提供继承的父页面的路径,如下:
<template extend="./xxx.vue"> </template>
复制
- 子页面需要通过一个自定义标签(假设是
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 | 指定扩展类型 | string | insert(插入)、replace(替换)、remove(移除)、append(向子集追加) |
position | 指定插入的位置(仅在 type 取值 insert 时生效) | string | before(目标前)、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,这里是为了能复用业务逻辑代码,最后我们运行代码,页面输出为:
结语
在真实的项目当中,我们遇到大量重复的页面但是又有小区别的页面,是可以通过这种方式减少我们的代码量,当然也许有更好的办法,也希望大伙能提出宝贵的建议。
最后引用一下 @XivLaw 老哥的评论:有很多人说通过cv就能解决,但是当你的业务有成千上万个页面是趋同,并且具有相同的基本功能,当界面需要统一调整或者需要进行ui统一管控的时候,cv就成了你的累赘了。 也有朋友说通过组件化和插槽解决,组件化是一个不错的方案,但是当成千上万个趋同的界面存在时,插槽并一定能覆盖所有的业务定制化。 使不使用这种方式,主要看你的业务。
直白一点说就是:我现在有一千个页面几乎一样,有的页面是头部多一点东西,有的是底部,有的是某个按钮旁边多一个按钮,有的是输入框之间多个输入框,ui或者界面或者同时需要添加固定功能,需要调整的时候,这一千个页面要怎么调?
仅供参考!!!