前言
开发前端这么久,虽然每天面对各种各样的需求,但是大部分情况下是在做重复性事情,而且是高密度的重复性的劳动,而且每天加班做这么多事情,对自己没有任何提升,鉴于此,我就一直在琢磨,能不能把这些重复性的工作,交给程序去处理,如果能够总结这些工作内容的共同规律,编写成为一套程序,交给程序去处理,那么我们就可以解放自己的时间,让自己可以做更加有意义的事情,而不是,每天像个机器,重复的做那些类似事情。
机缘巧合下,我来到了极客大学前端训练营,学习组件化时,我感觉这个体系和我原来的开发方式不一样了,他就是将共同规律封装成为一个个的模块来实现复用的。我感觉前端就应该是这样来开发的。下面我将分享组件化体系的一些内容,这只是我自己体会的一部分,如果有什么不完善的地方,还请大佬拍砖指正。
组件体系
组件概括
我们的页面是由一个个的标签组成的,但是这些标签有一定的局限性,有时候我们会想,要是可以自定以标签的行为和特征就好了,三大框架已经有不少自定义的组件,能满足大部分的需求,但是如果遇到一些自己定制化的需求,就满足不了了,还是得自己制作自己自定义的组件。
我理解的组件体系是:根据自己的需求,将对应的标签组合起来,形成一个独立的模块,这个模块有自己的属性和行为特征,而组件体系就是多个这样的组件,可以互相结合,就像搭积木一样,拼接搭配完成我们想要的城堡。
组件构成
生命周期
组件有自己的生命周期,创建(create),挂载(mounted),更新(update), 卸载(unmounted):
export default class Text {
constructor(text){
this[PROPERTY_SYMBOL] = Object.create(null);
this[ATTRIBUTE_SYMBOL] = Object.create(null);
this[EVENT_SYMBOL] = Object.create(null);
this[STATE_SYMBOL] = Object.create(null);
this.created(text);
}
created(text){
this.root = document.createElement("span");
this.root.style.whiteSpace = 'normal';
this.root.style.fontSize = '30px';
this.root.innerText = text;
}
mounted(){
}
unmounted(){
}
update(){
}
- create钩子用来创建自己的结构体系,并执行一些初始化操作。
- mouted钩子用来执行一些挂载dom时的行为操作,比如渲染样式。
- update钩子用来更新数据并重新渲染页面,当"观察者模式"监听到数据发生变化时。
- unmounted钩子用来执行卸载组件时的一些行为,比如清除没用的数据结构,释放内存空间等。
私有属性
组件有自己的一些独有属性,不希望被外界篡改,以破坏扰乱组件的行为特征。我们有必要加一些保护措施,来防止被干扰:
const PROPERTY_SYMBOL = Symbol("property");
const ATTRIBUTE_SYMBOL = Symbol("attribute");
const EVENT_SYMBOL = Symbol("event");
const STATE_SYMBOL = Symbol("state");
export default class Text {
constructor(text){
this[PROPERTY_SYMBOL] = Object.create(null);
this[ATTRIBUTE_SYMBOL] = Object.create(null);
this[EVENT_SYMBOL] = Object.create(null);
this[STATE_SYMBOL] = Object.create(null);
this.created(text);
}
es6引入了symbol数据结构,帮我们解决了这个问题,上面我用Symbol定义了这些私有属性,这样就可以放心的使用这些属性,而不用担心被别人篡改了。
get,set
取值和赋值会用到get和set方法,我们可以自定义get和set方法,用来拦截默认行为,定制一些自己的行为:
get width(){
return this[PROPERTY_SYMBOL].width;
}
set width(value){
this.root.style.width = value;
this.update();
return this[PROPERTY_SYMBOL].width = value;
}
get urls(){
return this[PROPERTY_SYMBOL].urls;
}
set urls(value){
return this[PROPERTY_SYMBOL].urls = value;
}
这里我们可以利用set来实现一个“发布-订阅模式”,来实现,当数据变化时,页面的及时更新功能。
attribute
当我们为我们的组件标签设置一些属性时,我们可以自定义setAttribute函数,拦截默认的setAttribute函数,来设置一些定制行为:
setAttribute(name, value){
if(name == "style") {
this.root.setAttribute('style',value);
this.root.style.width = "100%";
this.root.style.height = "100%";
this.root.style.display = "inline-block";
this.root.style.verticalAlign = "top";
}
return this[ATTRIBUTE_SYMBOL][name] = value;
}
react把attribute和property同步了,我们这儿attribute可以用来记录标签上的一些属性。当然也可以做一些定制化的操作。比如如果是以“on-” 开头的,就可以添加对应的监听事件。
property
我们的property属性是用来记录js操作的一些状态,来辅助整个代码逻辑的:
if(!this[PROPERTY_SYMBOL]['isDone']){
this.triggerEvent('scrollToBottom', '加载更多......');
this.root.appendChild(this.placeHolder);
this[PROPERTY_SYMBOL]['isDone'] = true;
setTimeout(() => {
this[PROPERTY_SYMBOL]['isDone'] = false;
window.getJSON("../data.json").then( data => {
this.root.removeChild(this.placeHolder);
this.render([data[1], data[2]]).appendTo(this.root);
this.triggerEvent('scrollToBottom', '人家也是有底线的!');
this.root.appendChild(this.placeHolder);
}).catch(
err => {
console.log(err);
return err;
}
)
}, 2000);
}
上面代码中,用property的isDone属性,来控制是不是要执行相应的逻辑。
组件的大部分功能都是通过attribute和property来完成的,这两个属性至关重要。
事件机制
我们可以自定义事件机制,如下:
addEventListener(type, listener){
if(!this[EVENT_SYMBOL][type])
this[EVENT_SYMBOL][type] = new Set;
this[EVENT_SYMBOL][type].add(listener);
}
removeEventListener(type, listener){
if(!this[EVENT_SYMBOL][type])
return;
this[EVENT_SYMBOL][type].delete(listener);
}
triggerEvent(type, ...args){
if(!this[EVENT_SYMBOL][type])
return;
for(let event of this[EVENT_SYMBOL][type])
event.call(this, ...args);
}
如上所示,我们可以添加事件队列,删除事件队列中的事件,触发事件队列中的事件。
由于篇幅有限,组件的构成就讲这么多吧。
组件的样式处理
组件的样式可以用js的style书写,尤其是对于核心构成样式,这是必要的,但是对于常规的修饰样式,都要拿js来写,想想就觉得繁琐,所以写了一个loader,来引入和处理css,使得可以使用css文件,代码如下:
css loader 处理 (依赖了css)
var css = require('css');
module.exports = function(source, map){
let filename = this.resourcePath.match(/([^\/ | \\ ]+)\.css$/)[1];
var classname = filename.replace(
/^[A-Z]/,
l => l.toLowerCase()
).replace(
/[A-Z]/,
l => '-' + l.toLowerCase()
);
var obj = css.parse(source);
let jsObj = {};
for(var rule of obj.stylesheet.rules) {
if(rule.type !== "rule")
continue;
rule.selectors = rule.selectors.map( selector =>{
if(selector.match(/filename/)){
selector = selector.replace(/filename/, l => {
return classname;
});
return selector;
}
return '.' + classname + ' ' + selector;
});
}
return "export default " + JSON.stringify(css.stringify(obj));
}
如上所示:引入了css-loader来处理css内容并转化为字符,导出给引用者,
- var obj = css.parse(source)这段代码就是转化css内容的
- let filename = this.resourcePath.match(/([^\/ | \\ ]+).css$/)[1];这是用来获取css文件名,注意**[^\/ | \\ ]**,由于不同的操作系统resourcePath所获得的斜杠不同,所以这儿用了兼容性的写法。
- filename.replace方法将驼峰式的文件名转换为了css常用的 key-code 中划线 方式的名称,赋值给了className.
- for循环了把css文件里所有的类名,都追加到了以该文件名转换后的className类名后了,保证了css样式的局部性,不会影响全局样式。
- 注意循环里的filename,这个是用来处理根元素样式的,这个会直接返回className.
- 最后return "export default" + + JSON.stringify(css.stringify(obj)),将导出的内容转换为字符串,return 出去,方便引用者调用。
css部分
.filename.root {
width: 100%;
height: 110px;
}
.container {
width: 100%;
height: 100%;
}
.left-img-box {
width: 110px;
height: 110px;
border-radius: 50%;
display: inline-block;
position: absolute;
left: 35px;
background-color: rgba(0, 0, 0, 0.03);
}
注意:这个filename是给根元素添加的样式。
在浏览器里面效果如下:

引入添加部分
import css from './ListHead.css';
if(!window.LIST_VIEW_STYLE_ELEMENT){
let styleElement = document.createElement('style');
styleElement.innerHTML = css;
document.getElementsByTagName('head')[0].appendChild(styleElement);
window.LIST_VIEW_STYLE_ELEMENT = true;
}
export default class ListHead{
constructor(config){
this[PROPERTY_SYMBOL] = Object.create(null);
this[ATTRIBUTE_SYMBOL] = Object.create(null);
this[EVENT_SYMBOL] = Object.create(null);
this[STATE_SYMBOL] = Object.create(null);
this[PROPERTY_SYMBOL].urls = config.urls;
this[STATE_SYMBOL].position = 0;
this.created();
}
created() {
this.root = document.createElement('div');
this.root.classList.add('list-head');
this.root.classList.add('root');
this.container = document.createElement('div');
this.container.classList.add('container');
this.leftImgBox = document.createElement('div');
this.leftImgBox.classList.add('left-img-box');
this.render();
}
如上所示:引入了css导出的内容,当然导出的内容是由loader处理的。并将导出的内容追加到style里面,并追加到document.head里了。
- 注意create函数里的calsslist添加类的方式,根节点会添加文件名转换为 key-code 方式的名称的类,还会添加一个名为root的类。
组件应用部分
组件应用使用了babel-loader处理,将组件导出的模块可以向标签一样使用。 package.json相关依赖配置如下:
"devDependencies": {
"@babel/cli": "^7.6.0",
"@babel/core": "^7.6.0",
"@babel/plugin-transform-react-jsx-source": "^7.5.0",
"@babel/preset-env": "^7.6.0",
"babel-loader": "^8.0.6",
"babel-plugin-transform-react-jsx": "^6.24.1",
"css": "^2.2.4",
"css-loader": "^3.2.0",
"to-string-loader": "^1.1.5",
"file-loader": "^4.2.0",
"url-loader": "^2.2.0",
"webpack": "^4.39.3"
},
"dependencies": {
"@babel/polyfill": "^7.6.0",
"babel-preset-spritejsx": "^1.0.7",
"webpack-cli": "^3.3.8",
"webpack-dev-server": "^3.8.0"
}
webpack.config.js配置如下:
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: [['babel-plugin-transform-react-jsx', {pragma:"create"}]]
}
}
},
如上:js文件,将被 “babel-plugin-transform-react-jsx” 转换处理,支持jsx语法。对应的处理函数为"create",这个名字是可以根据喜好更改的。
组件实际应运如下:
import {create} from "./create.js";
import Tabview from "./tabView.js";
import Div from "./div.js";
import Carousel from "./carousel.js";
import ListView from "./ListView.js";
import ListHead from "./ListHead.js";
import ListFrame from "./ListFrame.js";
var c = <Tabview>
<Div tab-title="推介"
placeHolder="加载更多......"
on-scrollToBottom={loadMore} >
<Carousel >
</Carousel>
<div>
</div>
<ListFrame data={ obj } ></ListFrame>
</Div>
<Div tab-title="有趣的店" placeHolder="load more..."
on-scrollToBottom={loadMore} >
</Div>
<Div tab-title="品牌新店" >
<div>
<ListHead ></ListHead>
</div>
<ListView ></ListView>
</Div>
</Tabview>;
c.appendTo(document.body);
如上:组件被引入之后,以标签的方式引用,还可以嵌套使用,这就实现了我开头所说的自定以标签,将一个模块,用原生的一些标签组合起来,形成了一个新的自定以标签,及组件,整个业务都可以用这些组件以对应的方式组合来完成,如同搭积木一般,自有拼接,组合成我们想要的样子。
每个组件初始化时,都会调用create初始化,create初始化代码如下:
import Text from './text.js';
import Wrapper from './wrapper.js';
let urls = [
"https://gw.alicdn.com/imgextra/i3/1618197344/O1CN01ojvFXL247bENVZDBI_!!1618197344-0-beehive-scenes.jpg_790x10000Q75.jpg_.webp",
"https://gw.alicdn.com/imgextra/i4/1114322414/TB2Jq3lxlmWBuNkSndVXXcsApXa_!!1114322414-2-beehive-scenes.png_790x10000.jpg_.webp",
"https://gw.alicdn.com/imgextra/i1/3402647387/TB2Mr64rQCWBuNjy0FaXXXUlXXa_!!3402647387-0-beehive-scenes.jpg_790x10000Q75.jpg_.webp",
"https://gw.alicdn.com/imgextra/i3/1618197344/O1CN01ojvFXL247bENVZDBI_!!1618197344-0-beehive-scenes.jpg_790x10000Q75.jpg_.webp"
];
let config = {
urls: urls
}
export function create(Class, attributes, ...children){
var object;
if(typeof Class === 'string')
object = new Wrapper(Class);
else
object = new Class(config);
for(let name in attributes){
if(name.match(/^on-([\s\S]+)$/)){
object.addEventListener(RegExp.$1, attributes[name]);
}else{
object.setAttribute(name, attributes[name]);
}
}
for(let child of children){
if(child instanceof Array){
for(let c of child){
if(typeof(c) === "string"){
object.appendChild(new Text(c));
}else {
object.appendChild(c);
}
}
}else if(typeof(child) === "object"){
object.appendChild(child);
} else {
object.appendChild(new Text(child.toString()));
}
}
return object;
}
上面代码中,引入了 wrapper 和 text,用来处理原始标签和文本内容。
- 首先通过class判断标签是自定义的,还是原始的,如果是原始的,那么就用wrapper包装class处理。
- 通过attributes参数,处理attributes,如上代码,如果是以on-开头的,那么就调用addEventListener添加事件方法,否则调用setAttribute方法。
- 对children的处理,如果children的元素是字符串,那么调用Text组件包装处理。
wrapper.js 代码如下:
const PROPERTY_SYMBOL = Symbol("property");
const ATTRIBUTE_SYMBOL = Symbol("attribute");
const EVENT_SYMBOL = Symbol("event");
const STATE_SYMBOL = Symbol("state");
export default class Wrapper {
constructor(type){
this[PROPERTY_SYMBOL] = Object.create(null);
this[ATTRIBUTE_SYMBOL] = Object.create(null);
this[EVENT_SYMBOL] = Object.create(null);
this[STATE_SYMBOL] = Object.create(null);
this[PROPERTY_SYMBOL].children = [];
this.root = document.createElement(type);
}
appendTo(element){
element.appendChild(this.root);
}
get width(){
return this[PROPERTY_SYMBOL].width;
}
set width(value){
return this[PROPERTY_SYMBOL].width = value;
}
get urls(){
return this[PROPERTY_SYMBOL].urls;
}
set urls(value){
return this[PROPERTY_SYMBOL].urls = value;
}
get children(){
return this[PROPERTY_SYMBOL].children;
}
appendChild(child){
this.children.push(child);
child.appendTo(this.root);
}
getAttribute(name){
return this.root.getAttribute(name);
}
setAttribute(name, value){
this.root.setAttribute(name, value);
}
addEventListener(type, listener){
this.root.addEventListener(...arguments);
}
removeEventListener(type, listener){
this.root.removeEventListener(...arguments);
}
}
如上:和其他的组件其实是一样的,
- this.root = document.createElement(type); 注意这儿只创建一个根元素,并且传进来什么标签,就创建什么。
text.js代码如下:
const PROPERTY_SYMBOL = Symbol("property");
const ATTRIBUTE_SYMBOL = Symbol("attribute");
const EVENT_SYMBOL = Symbol("event");
const STATE_SYMBOL = Symbol("state");
export default class Text {
constructor(text){
this[PROPERTY_SYMBOL] = Object.create(null);
this[ATTRIBUTE_SYMBOL] = Object.create(null);
this[EVENT_SYMBOL] = Object.create(null);
this[STATE_SYMBOL] = Object.create(null);
this.created(text);
}
appendTo(element){
element.appendChild(this.root);
this.mounted();
}
created(text){
this.root = document.createElement("span");
this.root.style.whiteSpace = 'normal';
this.root.style.fontSize = '30px';
this.root.innerText = text;
}
mounted(){
}
unmounted(){
}
update(){
}
log(){
console.log("width:", this.width);
}
get width(){
return this[PROPERTY_SYMBOL].width;
}
set width(value){
return this[PROPERTY_SYMBOL].width = value;
}
get urls(){
return this[PROPERTY_SYMBOL].urls;
}
set urls(value){
return this[PROPERTY_SYMBOL].urls = value;
}
appendChild(child){
child.appendTo(this.root);
}
getAttribute(name){
return this[ATTRIBUTE_SYMBOL][name]
}
setAttribute(name, value){
if(name == "style") {
this.root.setAttribute('style',value);
this.root.style.width = "100%";
this.root.style.height = "100%";
this.root.style.display = "inline-block";
this.root.style.verticalAlign = "top";
}
return this[ATTRIBUTE_SYMBOL][name] = value;
}
addEventListener(type, listener){
if(!this[EVENT_SYMBOL][type])
this[EVENT_SYMBOL][type] = new Set;
this[EVENT_SYMBOL][type].add(listener);
}
removeEventListener(type, listener){
if(!this[EVENT_SYMBOL][type])
return;
this[EVENT_SYMBOL][type].delete(listener);
}
triggerEvent(type){
for(let event of this[EVENT_SYMBOL][type])
event.call(this);
}
}
他和wrapper.js 的主要区别在于,这儿的根元素始终是span标签,用来存放文本内容。
本文从组件的应运场景,组件的组成,以及组件的样式和使用来介绍组件。希望对大家能起到帮助,如果有什么纰漏还请留言讨论指正,欢迎大家一起来讨论。