回顾

在(一)中我们实现了组件的变量替换和列表渲染的功能。本篇将对其缺陷进行改进。

一、改进——组件的共有样式和私有样式

显而易见,现在主页面的css和组件调用的css是相互影响的,很多时候我们不希望组件内的样式影响到主页面,这时候该怎么办?

1.最简单的想法是写组件样式的时候避免影响主页面的样式。如:

原先是:

1
2
3
4
.m-t_tit{
text-decoration: underline;
color: #09f;
}

可以写成:

1
2
3
4
[data-render] .m-t_tit{
text-decoration: underline;
color: #09f;
}

并在节点上加上属性data-render,以此来保证样式的“私有性”

这个想法很好,但是也不是完全没有问题:每个组件都这样写会加大开发成本;不同组件要想不同名字,想名字什么的最讨厌了;一个页面多个地方引用这个组件会导致组件间的互相影响。

2.本着追求最低开发成本、尽可能机器代替人脑的想法(偷懒至上),我们将样式私有化的工作交给了代码,首先制定组件的css规范:
  • 有仅只能有一个style标签,该标签的样式能影响主页面(并且这里的样式不能带有变量,后文介绍)。写法如:
    1
    2
    3
    4
    5
    6
    <style>
    .m-t_tit {
    text-decoration: underline;
    color: #09f;
    }
    </style>

(无可厚非组件样式要有它的私有性,但也不能全面封杀,所以开了这么个口子)

  • 有仅只能由一个带”private”属性的style标签,该标签的样式只能影响该组件(这里的样式可以带变量)。写法如:
    1
    2
    3
    4
    5
    6
    <style private>
    .m-t_tit {
    text-decoration: underline;
    color: #09f;
    }
    </style>
3.接下来就是如何处理这些css的问题了。

3.1 首先提取公用style标签里的内容:用正则很简单

1
2
3
4
5
6
7
8
9
10
11
// 假设组件的内容为html
var reg = /<style>(([\s\S])*?)<\/style>/,
cssdata;

html = html.replace(reg, function (match, data) {
if (data) cssdata = data;

return ''
});

// cssdata为提取的css内容,此时html已除去公用css部分

3.2 然后是最为关键的处理私有style标签的内容。
首先写个随机生成属性的函数getRandomAttr,生成的属性就是该组件的标识属性,用于后续的操作。每次生成的属性是随机的,因此也避免了主页面多次调用同一组件时样式间存在变量时的互相影响。

1
2
3
4
5
6
7
8
// get random attribute
// @return {String} attribute
var getRandomAttr = function () {
return 'data-r' + Math.random().toString(36).substr(2, 8);
};

console.log(getRandomAttr()); // 'data-rebda2fa8'
console.log(getRandomAttr()); // 'data-rz1ryln4r'

然后我们提取组件代码里私有style标签的内容:老套路还是用正则

1
2
3
4
5
6
7
8
9
10
11
// 假设组件的内容为html
var reg = /<style\s+private>(([\s\S])*?)<\/style>/,
cssPrivatedata;

html = html.replace(reg, function (match, data) {
if (data) cssPrivatedata = data;

return ''
});

// cssPrivatedata为提取的css内容,此时html已除去公用css部分

最为关键的部分,为提取出的标签添加私有属性标签。一开始的思路是从”}”结束标识入手,将”}”替换为”} [组件标识属性] “并在开头也加上这个选择器然后结尾去掉这个选择器,但发现当出现逗号多元素选择器时这种方式就行不通了。最后决定写一个“扫描器”,从头到尾扫一遍样式字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// remove 2+ space and line break
// @return {String} worked string
var removeSpaceAndLinebreak = function (str) {
return str.replace(/(^\s*)|(\s*$)/g, '').replace(/\s\s/g, '').replace(/<\/?.+?>/g, '').replace(/[\r\n\t]/g, '');
};

// scaner
function Scaner () {
this.index = 0;
this.status = 'off';
}

Scaner.prototype = {
// end of scan
end: function (callback) {
this.index = 0;
this.status = 'off';

if (callback) callback();
},
// css scan
// @param {Object} config:
// {Functon} handleStyle: selector and content function;
// {Function} callback: callback
doCssScan: function (config) {
var THIS = this;
var result = {
'selector': '',
'content': ''
};
var cssdata = removeSpaceAndLinebreak(config.data),
_length = cssdata.length;

THIS.status = 'selector';
for (THIS.index = 0; THIS.index < _length; THIS.index++ ) {
var _data = cssdata[THIS.index],
_status = THIS.status;

if (_data === '{' || _data === '}') {
THIS.status = _status === 'selector' ? 'content' : 'selector';
} else if (!(!result[THIS.status] && _data == ' ')) result[THIS.status] += cssdata.charAt(THIS.index);

if (THIS.status !== _status && _status === 'content') {
if (config.handleStyle) config.handleStyle(result);

result.selector = '';
result.content = '';
}
}

THIS.end(config.callback);
}
};

其中,removeSpaceAndLinebreak的作用是去除多个空格(两个及以上)、去除tab、去除换行,以减少扫描for循环的次数;doCssScan方法给了三个口子,可用于选择器的回调、样式内容的回调以及最后扫描结束的回调。我们只需对于每次扫描器返回选择器的回调时,处理选择器即可:每个选择器的开头加上”[组件标识属性] “(最后有空格),选择器里的”,”可以放心替换为”, [组件标识属性] “(最后有空格)。如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 前面提取出的css为cssPrivatedata
var componentScaner = new Scaner();
var private = getRandomAttr(),
var result;

componentScaner.doCssScan({
data: cssPrivatedata,
handleSelector: function (data) {
var _data = ('[' + privateAttr + '] ' + data).replace(',', ',[' + privateAttr + '] ');
cssPrivatedata = cssPrivatedata.replace(data, _data);
},
callback: function () {
result = cssPrivatedata;
}
});

// 最后的result为处理后的私有css

如:

1
2
3
4
5
6
7
8
9
.m-table{
width: 100%;
}
.m-table th,.m-table td{
height: 40px;
line-height: 40px;
color: #f00;
font-weight: bold;
}

处理后:

1
2
3
4
5
6
7
8
9
[data-rg2covpi5] .m-table{
width: 100%;
}
[data-rg2covpi5] .m-table th,[data-rg2covpi5] .m-table td{
height: 40px;
line-height: 40px;
color: #f00;
font-weight: bold;
}

4.css都处理完了,那就很简单了,提取出的cssdata和cssPrivatedata生成两个style节点,处理后的html(没了css)生成一个节点,然后赛入到添加组件标识属性后的容器中,这里就不描述了。

二、改进——组件的内容的缓存

下面我们来处理第二个问题:当一个页面多次引用组件时(比放组件是产品展示,主页面是产品列表),目前的组件会导致每次渲染组件都在重复着发请求——解析组件——容器填充的过程,请求和解析(特别是刚才私有css的解析)的消耗可是伤不起的呀。好,改进

1.变量缓存$.Components,主页面设置这个变量专门缓存组件信息内容,每个组件的url作为key
1
2
3
4
5
6
7
8
9
10
11
$.Components = {};

// ...加载组件后
$.Components = {
'components/test.html': {
//...
},
'components/list.html': {
//...
}
}
2.将请求降到最少。第一个请求发起前,变量$.Components先记一条url内容,后续请求判断变量里是否有此url,有的话便不发请求,如
1
2
3
4
5
6
7
8
9
10
$.fn.renderComponent = function (options) {
if (!options || !options.url) return false;

if ($.Components[options.url]) //不发请求直接走缓存渲染
else {
$.Components[options.url] = {};

$.get//.....
}
}

为了保证第二次请求时能拿到组件缓存,最简单粗暴的做法就是将请求设为同步,async:true。但这种方法会影响后续其他代码的执行,not good not cool。那么请求还是走异步,然后我们给组件的缓存变量加上属性tasks,用于容纳第一次请求未返回时的组件请求任务,在请求返回之时再执行tasks任务队列。如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$.fn.renderComponent = function (options) {
if (!options || !options.url) return false;

var cache = $.Components[options.url];
if (cache) {
if (!cache.content) { // 假设缓存内容为content
var $t = $(this);
$.Components[options.url].tasks.push({ dom: $t, options: options });
} else //直接渲染
}
else {
$.Components[options.url] = {
task: []
};

$.get//.....
...
$.Components[options.url].content = content; // 假设缓存内容未content

var tasks = $.Components[options.url].tasks;
if (tasks && tasks.length) {
for (var i in tasks) {
tasks[i].dom.renderComponent(tasks[i].options);
}

$.Components[options.url].tasks = [];
}
}
}

这样操作就把相同组件的请求降到了最低——1个。

3.考虑缓存哪些内容。
  • 节点html:毋庸置疑,肯定需要缓存,并重新渲染;
  • js:考虑到dom事件的重复绑定和变量函数的重复定义并且组件主要用于展示等因素,js不予缓存,只在第一次请求回调时执行,因此dom事件建议写在html里;
  • css:共有style不予缓存,也仅在第一次请求回调时生成节点,私有style就比较麻烦了,因为里面可能有组件变量,那我们先考虑私有style的缓存和渲染方法

最先的想法是私有方法如果有组件变量时,组件的标识属性重新生成,这样组件间的私有style就没有影响了,但是这种做法并不完美,当组件调用得很多时,特别是私有style里含变量的样式很少而不含变量的样式很多时,这种渲染就会导致页面很臃肿。我们也知道,没有变量的样式在同一组件之间是可用通用的,这时候还重复添加样式的话显然是不合理的。所以思路也就明确了:
同一组件模板的组件标识属性相同,私有style里不含组件变量的样式在第一次请求回调时跟共有style一起生成节点,不做缓存,私有style里含组件变量的样式给予缓存,后续调用时填充新的组件变量再生成节点。

4.缓存结果。

扫描器扫描一遍css,提取公有属性以及不带变量的私有属性;
将带变量的私有css用数组缓存。

1
2
3
4
5
6
7
8
9
10
   var privateVarArr = []; // 存放css缓存数组
scaner.doCssScan({
data: cssdata,
handleStyle: (result) => {
// push 私有style
if ((Regs.variable.test(result.selector) || Regs.variable.test(result.content))) {
privateVarArr.push({ selector: _selector, content: result.content });
}
}
});

然后提取js,剩下的html便是缓存的html

三、结果:Demo

github
主页面节点,其中:

  • 主页面有个table用于判断组件的私有性和公有性;
  • 四个容器加载的都是同一个组件,加载方式有所区别
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    <!-- 主 -->
    <section class="m-container f-tc">
    <h1>Hello world</h1>
    <p>我不是组件,我是主页面。</p>
    </section>

    <!-- 看看是否受组件影响 -->
    <section class="f-tc">
    <table class="m-table">
    <thead>
    <tr>
    <th>title1</th>
    <th>title2</th>
    <th>title3</th>
    <th>title4</th>
    <th>title5</th>
    </tr>
    </thead>
    <tbody>
    <td>1</td>
    <td>2</td>
    <td>3</td>
    <td>4</td>
    <td>5</td>
    </tbody>
    </table>
    </section>

    <!-- 组件内容 -->
    <section class="g-mt50 f-tc j-renderCtn" r-url="components/test.html" r-value="{'name': 'test1', 'content': 'This is a component1.','list':[], 'ularr': [], 'btnColor': '#f0f'}">
    看什么,还没内容呢
    </section>
    <section class="g-mt50 f-tc j-renderCtn" r-url="components/test.html">
    看什么,还没内容呢
    </section>
    <section class="g-mt50 f-tc j-renderCtn" r-url="components/test.html">
    看什么,还没内容呢
    </section>
    <section class="g-mt50 f-tc j-renderCtn" r-value="_value3">
    看什么,还没内容呢
    </section>

而组件’test.html’中既有有私有样式也有公有样式,还有变量
结果:
第一屏
第二屏

先看样式,可以看到公有css已经影响到了主页面的table,而私有样式只影响了组件;并且带有变量的样式各组件也能正常显示。
css
主页面head里新增的标签可以看出,公有css和不带变量的私有css只增加了一个dom节点,带有变量的私有css根据不同组件间的随机字符串得以区分(可以看出,如果样式里没有变量的话,能减少部分dom操作)

再看看内容,可以看到不同组件所渲染的数据是不同的,组件之间没有影响

alert
再看看js部分,这里组件里的script标签里只定义了个提示函数ralert,事件触发则写在了节点里。各按钮的弹出框也符合预期。

最后来看请求和缓存
alert
四个容器只发了一个请求,说明缓存是有用的。

alert
缓存里,任务队列此时为空,说明都已正常执行;缓存内容我们缓存了组件标识、带变量的私有style数组,模板html。(当确定不再加载该组件时,建议清除改缓存)

对迄今为止的组件使用进行一个说明

1.options参数
字段名 格式 说明
url String (必)组件的请求路径(无值时取容器”r-url”属性)
value Object 渲染的变量(无值时取容器”r-value”属性,属性可为变量名或字符串对象)
callback Function 渲染成功后的回调函数
2.组件使用:
  • ‘{ { 变量名 } }’:组件变量,取不到值时替换为””;
  • ‘{ {r-“列表变量名”} }内容{ {r-end} }’:列表渲染;
  • <style>样式内容\</style>‘:能影响全局的样式,不能含变量,最多一个;
  • <style private>样式内容</style>‘:不能影响全局的私有样式,可含变量,最多一个;
  • <script>样式内容</script>‘:js内容,最多一个;
3.缓存内容:

变量:$.Components

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$.Components = {
'组件路径': {
attr: { // 组件标识
name: '组件名',
value: '标识随机字符串'
},
content: { // 缓存内容
cssArr: [私有变量css数组],
content: 'html模板缓存'
},
tasks: [任务队列数组],
ajaxError: 加载失败与否
}
}

本篇内容主要解决了组件样式私有性的问题,避免了组件与主页面、组件之间的样式冲突;通过缓存解决了同一组件多次调用时多次请求的问题。但是能否进一步改善呢,下篇将利用nodejs对其进一步改进。

谢谢~