lxjwlt's blog

JS模块的BEM命名实践

在项目开发中我多次遇到由于命名冲突导致页面样式混乱,甚至跨模块获取同名元素导致功能失效的情况。

对于这种情况,我之前的 一篇文章 中介绍过如何使用BEM命名法来解决命名冲突的问题。

当项目中引入了BEM命名后,不仅CSS代码,JS代码也应该做相应的调整,以下是我个人实践的总结,如果你也在项目中使用BEM命名,希望本文对你有所帮助。

更好阅读体验>>

JS统一管理模块标识

当我们提起BEM命名,我们知道一个元素的名称由三部分组成:

BEM = block + element + modifier = 模块名 + 元素名 + 修改器

假设我们有张联系页面,使用BEM命名的模板如下:

<div class="contact-page">
    <div class="contact-page_header"></div>
</div>

联系页面的模块标识为contact-page,所以该页面的所有子元素的名称都以contact-page为前缀来命名。

考虑到JS中需要通过模块标识来获取页面元素,而且HTML中手工编写模块标识影响可读性,不利于维护,所以我们将模块标识交由JS来统一管理,而HTML中的模块标识则使用JS模板引擎进行动态插入。

这里我们使用的是Ext.XTemplate引擎:

var $ = require('jquery'),
    R_Xtpl = require('xtpl');

var C = function (wrap, options) {
    this.modName = 'contact-page';

    this.$elem = $((new R_Xtpl(require('./index.html'))).apply({
        modName: this.modName
    }));

    $wrap.html(this.$elem);
};

在模板中,我们用modName变量代替模块标识:

<div class="{modName}">
    <div class="{modName}_header"></div>
</div>

这样,阅读HTML时我们只需要关注页面元素名(BEM的E)即可,可读性更好,而且我们能够在JS获取元素。

模块元素的选择器

模块标识存储在this.modName中,所以按照BEM命名,我们可以编写以下函数来获取元素的选择器:

C.prototype.selector = function (selector) {
    return this.modName + '_' + selector;
};

联系页面的header元素的选择器,我们可以这么获取:

'.' + this.selector('header'); // .contact-page_header

目前的selector函数不好的地方在于,每次获取元素选择器都要进行字符串的结合,如果点号(.)或井号(#)写在元素名中就好了,比如this.selector('.header'),我们对selector函数进行改进:

C.prototype.selector = function (selector) {
    var self = this,
        reg = /^(\.|#)?/;

    return selector.replace(reg, function (match, $1) {
        $1 = $1 || '';
        return $1 + self.modName + '_';
    });
};

这样我们就可以根据不同的场景分别获取元素的“名称”或“选择器”:

// .contact-page_header元素有类名contact-page_header
$(this.selector('.header')).hasClass(this.selector('header')); // true

获取模块元素

我们已经有了selector方法来获取元素选择器,那么接下来我们可以轻易的实现获取模块元素的接口:

C.prototype.elem = function (selector) {
    return selector ? 
        this.$elem.find(this.selector(selector)) : this.$elem;
};

以下用elem方法获取联系页面的头部元素:

this.elem('.header');

引入了VueJS之后

引入了VueJS之后,我们彻底抛弃了模板引擎库Ext.Xtemplate,我们使用Vue来作为视图层。

var $ = require('jquery'),
    R_Vue = require('Vue');

var C = function (wrap, options) {
    this.modName = 'contact-page';

    this.initVm(wrap);
};

C.prototype.initVm = function (wrap) {
    $elem = $(require('./index.tpl'));

    this.vm = new R_Vue ({
        el: $elem[0],
        data: {
            modName: this.modName
        }
    });

    $(wrap).html($elem);
};

module.exports = C;

引入vue之后我们不再使用类名来获取元素了,因为Vue提供了元素钩子,用来给元素建立索引,我们通过Vue实例的$els访问这些元素。

所以我们的模板可以改为:

<div class="">
    <div class="_header" v-el:header></div>
    <!-- ... -->
</div>

(注:类名中保留modName的写法是为了CSS中编写样式)

上面我们使用v-el语法为header元素建立了索引。

获取元素的方法改写为:

C.prototype.elem = function (selector) {
    selector = selector.replace(/-([a-zA-Z])/g, function (match, $1) {
        return $1.toUpperCase();
    });
    return this.vm.$els[selector];
};

获取header元素:

this.elem('header');

不使用CSS选择器(类名或ID)获取元素的好处在于: * CSS获取元素和JS获取元素的方式彻底分隔开,互不影响(记得有这么个规定:使用js-前缀的类名获取元素,就是为了达到这个目的) * 元素已经缓存在Vue实例中,我们不需要手动获取元素,也不需要手动进行缓存,由Vue进行统一的模板管理、元素缓存以及元素的注销

VueJS大法好啊!