问大家一个问题,曾经的你是否也遇到过,一个项目中有好几个页面长得基本相同,但又差那么一点,想用 vue extends 继承它又不能按需继承html模板部分,恰好 B 页面需要用的 A 页面 80% 的模板,剩下的 20% 由 B 页面自定义,举个栗子:
我们假设这是两个页面,B页面比A页面多了个p标签,剩余的东西都一样,难道仅仅是因为这一个 p标签就要重新写一份模板吗?相信大部分伙伴解决方式是把公共部分抽成一个组件来用,这是一个好的做法。没错,但是来了,老板让你在 标题1、标题2下面分别插入一段内容,这会儿你是不是头大了?难道只能重写一份了吗?当然不是,来开始我们的填坑之路~(当你的业务能用插槽或者组件抽离的方式固然更好,以下内容仅针对当你项目达到一定体量,vue老三套难以处理的情况下采用)
准备以下工具包:
npm install --save node-html-parser
<template extend="./xxx.vue">
</template>
<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 标签需要具备以下属性:
参数 | 说明 | 类型 | 可选值 |
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页面:
A.vue:
<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或者界面或者同时需要添加固定功能,需要调整的时候,这一千个页面要怎么调?
作者:小小小小_柏
链接:https://juejin.cn/post/7347973138787467274
目开发中的函数抽离和复用
在实际的项目开发中,尽可能要做到让开发者易于理解和后期维护,那么,其中一个最重要的就是必须将重复使用的相同代码块或者是差异不明显的代码块抽离出来。在需要使用的地方传参调用执行。
这样做的好处其中一个是,尽可能减少代码模块的改动,当项目中有需要拓展或者修改的地方,那就不需要在一个大的函数体里面去修改,而是该对应的模块,只要确定好输入的参数和返回值就可以。另一个好处是易于项目的拓展,将通用逻辑抽离出来之后,如果有新增的方法,直接新命名一个新的函数体实现新的逻辑,旧的函数体也可以保留,保证目前线上代码的兼容性。这就是面向对象编程的开发思想。
下面是一个简单的例子:
这是一段数据上报相关的代码,在上报之前合并所需参数,之后判断当前环境再执行上报事件。
// index.vue
methods : {
pageReport(ags) {
let baseParams = {
'event_id': this.eventId,
'event_name': this.eventName,
'page_id': '123',
};
return this.report({ ...baseParams, ...args });
},
report(args) {
if (this.isApp) {
web.Connect.Report({
"data": args
});
} else {
web.report(args);
}
},
}
流程图为:
这一段代码本身是没有什么问题,但是后来想了一下,如果这个时候做拓展增加上报的场景,比如微信情况下,qq情况下,或者在上报的时候增加了一些其他逻辑,那么,report 这个函数的代码就会很臃肿,而且也不好理解。所以,可以先把客户端上报和web上报的函数单独给抽离出来。
所以,可以代码修改成这样:
// index.vue
methods : {
pageReport(args) {
let baseParams = {
'event_id': this.eventId,
'event_name': this.eventName,
'page_id': '123',
};
return this.reportDistribute({ ...baseParams, ...args });
},
reportDistribute(args) {
if (!this.isApp) return this.webReport(args);
this.appReport(args);
},
appReport(args) {
web.Connect.Report({
"data": args
});
},
webReport(ars) {
web.report(args)
},
}
流程图为:
由中间的一个函数做判断处理,这样至少可以先保证客户端上报和web上报这两个地方不论在逻辑上怎么去改,只要是不涉及到上报本身的逻辑修改,那么就只要修改 reportDistribute 这个函数就可以,可以理解为 appReport 和 webReport 这两个函数是底层执行代码,而 reportDistribute 这个就是逻辑层代码,负责判断处理调用。
这样抽离之后底层负责执行的模块就会和逻辑层分开,需要拓展的之后只需要增加对应的模块和定义好需要的数据,使用的时候再去调用既可以了,不会再改到逻辑层上的代码,同时,任意一个模块需要修改,那么只要修改对应模块就可以。
上面的代码目前只有两种情况,客户端内和Web情况下上报 。虽然是将底层代码抽离了出来,但如果这个地方如果增加了其他场景的上报比如微信下的,或者是qq下的又或者是微博下的,按照目前的写法,就是增加 if else,但是这样并不好理解,代码的圈复杂度也高,这个时候可以写一个配置表进行映射 ,代码如下:
// index.vue
methods : {
appReport(ars) {
// do something
},
webReport(ars) {
// do something
},
weixinReport(ars) {
// do something
},
getReportParams() {
let isApp = isApp ? '2' : '1';
if (+isApp === 1 && isWeixin === 1) {
isApp = 3
}
return isApp
},
reportDistribute(args, type) {
const config = {
1: this.webReport,
2: this.appReport,
3: this.weixinReport
}
config[type](args);
},
reportInit(args) {
return this.reportDistribute(args, this.getReportParams());
},
pageReport(args) {
let baseParams = {
'event_id': this.eventId,
'event_name': this.eventName,
'page_id': '123',
};
return this.reportInit({ ...baseParams, ...args });
},
}
流程图为:
这样不管再新增多少情况的上报,只要在 reportDistribute 里面增加一个配置,在 getReportParams 中多增加一个返回值,就可以完成配置,整一段代码清晰可维护。
接着,这一段代码负责底层逻辑的一些映射相关的配置,这些一般改动的是比较少的,那么可以在把这一些抽离出来单独放在一个 config 文件里面,而不是全都挤在一个 vue 页面的 methods 中。
首先,将映射配置相关和负责底层执行的函数抽离出来,放在一个page-config.js 文件中,再将接口export 出去。 代码如下:
第一个文件 page-config.js ,定义基本配置和映射表。
// page-config.js
function appReport(ars) {
alert(JSON.stringify(ars));
}
function webReport(ars) {
alert(JSON.stringify(ars));
}
function weixinReport(ars) {
alert(JSON.stringify(ars));
}
const baseParams = {
eventId: '1',
eventName: 'test-page',
pageId: 'index',
};
const pageConfig = {
appReport: appReport,
webReport: webReport,
weixinReport: weixinReport,
};
export { pageConfig, baseParams };
之后如果需要做拓展,直接在这个配置文件里面加内容即可。
再是第二个文件 index.vue 文件
// index.vue
import { pageConfig, baseParams } from './page-config';
export default {
methods: {
getReportParams() {
let isApp = this.isApp() ? 'appReport' : 'webReport';
if (+isApp === 'webReport' && this.isWeixin === true) {
isApp = 'weixinReport'
}
return isApp
},
// add function
pageGo(args) {
return this.eventDistribute('link', 'https://www.google.com');
},
eventDistribute(type, args) {
return pageConfig[type](args);
},
pageReport(args) {
return this.eventDistribute(this.getReportParams(), { ...baseParams, ...args });
},
}
}
函数抽离之后,这里以后的拓展需要改到的只有 getReportParams 函数,这里面可能会返回新的数值。
这里注意一点是,之前的 reportDistribute 函数改成了 eventDistribute,这样做的目的是更加的通用,因为一个项目不单单是只有上报事件,肯定还有其他功能逻辑,例如这时候需要再加一个网页跳转的事件,那么就可以向上面的代码一样 ,增加一个 pageGo调用 eventDistribute 。
eventDistribute 函数通用第一个参数区分执行的函数类型和第二个参数进行传参执行对应函数。
整理的流程图改为:
接下来,也可以再对 page-config 做一点优化,因为,上面举例的事件类型也只有两大类一个是页面跳转,另一个是上报事件。那么,要考虑再多其他类型的情况,也不太好记的请。所以,可以对底层的映射配置再做一点优化,比如,通过参数的名称进行对应的函数执行,代码如下:
methods : {
getReportParams() {
let isApp = this.isApp ? "report.appReport" : "report.webReport";
if (isApp === "report.webReport" && this.isWeixin) {
isApp = "report.weixinReport";
}
return isApp;
},
// add function
pageGo(args) {
return this.eventDistribute("go.link", "https://www.google.com");
},
eventDistribute(type, args) {
return pageConfig(type, args);
},
pageReport(args) {
return this.eventDistribute(this.getReportParams(), {
...baseParams,
...args,
});
},
},
把事件类型分类,再使用小数点隔开 report.appReport
// page-config.js
function appReport(ars) {
alert(JSON.stringify(ars));
}
function webReport(ars) {
alert(JSON.stringify(ars));
}
function weixinReport(ars) {
alert(JSON.stringify(ars));
}
function link(url) {
window.location.href = url;
}
const baseParams = {
eventId: '1',
eventName: 'test-page',
pageId: 'index',
};
const pageEvent = (name, args) => {
eval(name + "(" + JSON.stringify(args) + ")");
};
const pageFunConfig = {
'report': pageEvent,
'go': pageEvent
};
const pageConfig = (type, args) => {
const eventType = type.split('.')[0];
const eventName = type.split('.')[1];
pageFunConfig[eventType](eventName, args);
}
export { pageConfig, baseParams };
切割出函数名称之后执行函数。流程图如下:
上面的代码只是利用了上报这个行为做了一个例子,并不是说一定就是要这样写,更多的是一种将代码抽离达到多次服用和容易维护的目的。
如果小伙伴有更好的方法和建议,麻烦请在我的公众号留言,谢谢!
-----
利宝阁确实有点东西。
欢迎关注各位小伙伴我的公众号 @GavinUI
中1, 2, 3, 4 表示优先级
选择器 | 格式 | 优先级权重 |
!important | 10000 | |
内联样式 | 1000 | |
id 选择器 | #id | 100 |
类选择器 | #classname | 10 |
属性选择器 | a[ref=“eee”] | 10 |
伪类选择器 | li:last-child | 10 |
标签选择器 | div | 1 |
伪元素选择器 | li::after | 1 |
相邻兄弟选择器 | h1+p | 0 |
子选择器 | ul>li | 0 |
后代选择器 | li a | 0 |
通配符选择器 | * | 0 |
注意事项:
可继承:
字体系列 font-family font-weight font-size
文本系列 color text-align line-height
可见系列 如 visibility
由于属性太多,这里只列举常见的可继承的属性
属性值 | 作用 |
none | 元素不显示,并且会从文档流中移除。 |
block | 块类型。默认宽度为父元素宽度,可设置宽高,换行显示。 |
inline | 行内元素类型。默认宽度为内容宽度,不可设置宽高,同行显示。 |
inline-block | 默认宽度为内容宽度,可以设置宽高,同行显示。 |
list-item | 像块类型元素一样显示,并添加样式列表标记。 |
table | 此元素会作为块级表格来显示。 |
inherit | 规定应该从父元素继承 display 属性的值。 |
block:
*请认真填写需求信息,我们会在24小时内与您取得联系。