不论是vue、还是react,还是其他的现代前端框架,无论他们打着什么样的旗号进行宣传,更快的构建速度、更加简洁的API、更加优秀的性能,更加的方便的维护等等。
他们的核心其实无外乎两件事:单项数据流和数据驱动。
单项数据流说的是:组件数据的流转从外到内,从父组件到子组件。
数据驱动说的是:组件的渲染依赖组件自身状态或着全局状态,当状态发生变化时,组件渲染的内容页跟着发生变化。
尽管vue、react已经基本上是开发者的必选框架,但是依然有一些它们无法覆盖到的场景。
比如我们需要开发一款能够兼容市面上几乎所有机型的app,为了节约成本,提高开发效率以及迭代效率,webapp必然是我们首选的技术。
而webapp又有很多基于vue、react的框架,它们虽然能够快速迭代,单兼容性不够好。
需要我们做很多对js的兼容性进行处理,比如用pollify ,用babel做降级处理,甚至我们做了这些处理之后,依然有很多不可控的bug出现。比如:页面白屏,依赖过多导致加载缓缓等等。
所以,对于一些特定webapp的开发,原生js、html、css 依然是最好的技术选型。
对于使用原生js进行webapp的开发,又很容易陷入一种堆叠代码的困境之中。
有时候当一个节面的业务过于复杂,需要根据不同的状态展示不同的UI,不同的UI又有不同的事件绑定,甚至组件之间还有一些耦合业务需要处理。
这时候,这个页面的js代码就会随着业务的迭代变的越来越臃肿,尤其是当多个人维护这个文件时,每个人的代码习惯都可能不一样,就会加速项目的不可维护性,进而增加项目出现生成事故的风险。
那么有没有一种可以借鉴vue、react的开发方式,使项目整体具有很好的维护性,又能够使用原生js增加项目整体兼容性的开发方式呢?
有,这里我提供一种思路供大家进行参考。
我的想法如下:
首先,使用原生js封装一个基类,实现组件化。
function BaseComponent(container, stateManager) {
this.container = container;
this.stateManager = stateManager;
this.dom = null;
this.children = [];
this.init();
}
BaseComponent.prototype.init = function () {
this.stateManager.subscribe(this);
// this.dom = this.createDOM();
this.render();
this.bindEvents();
};
BaseComponent.prototype.createDOM = function (template) {
// 使用模板字符串创建 DOM 元素
// const template = this.getTemplate();
const wrapper = document.createElement("div");
wrapper.innerHTML = template;
console.log("wrapper", wrapper, wrapper.firstChild);
return wrapper;
};
BaseComponent.prototype.onStateUpdate = function () {
this.render();
this.updateDynamicParts();
};
BaseComponent.prototype.render = function () {
// 抽象方法,由子类实现
// 清空容器
this.container.innerHTML = "";
// 渲染自身 DOM
if (this.dom) {
this.container.appendChild(this.dom);
}
// 递归渲染子组件
this.children.forEach((child) => {
child.render();
});
};
BaseComponent.prototype.updateDynamicParts = function () {
// 局部更新逻辑
};
BaseComponent.prototype.bindEvents = function () {
// 使用事件委托
// this.container.addEventListener("click", (e) => {
// e.preventDefault() || e.stopPropagation();
// this.handleEvent(e);
// });
// 确保只添加一个事件监听器
if (!BaseComponent.eventBound) {
document.body.addEventListener("click", (e) => {
e.preventDefault() || e.stopPropagation();
this.handleEvent(e);
});
BaseComponent.eventBound = true;
}
};
BaseComponent.prototype.handleEvent = function (e) {
// 事件分发逻辑
};
// 添加子组件的方法
BaseComponent.prototype.addChild = function (childComponent) {
this.children.push(childComponent);
childComponent.container = this.container; // 设置子组件的容器
childComponent.init(); // 初始化子组件
};
其次,使用js封装状态管理基类,实现状态管理,即数据驱动试图进行更新。
代码如下:
function StateManager(initialState) {
this.state = initialState || {};
this.subscribers = [];
}
StateManager.prototype.subscribe = function (callback) {
this.subscribers.push(callback);
};
StateManager.prototype.unsubscribe = function (callback) {
this.subscribers = this.subscribers.filter((cb) => cb !== callback);
};
StateManager.prototype.setState = function (newState) {
this.state = { ...this.state, ...newState };
this.notifySubscribers();
};
StateManager.prototype.getState = function () {
return this.state;
};
StateManager.prototype.notifySubscribers = function () {
this.subscribers.forEach((component) => component.onStateUpdate());
};
这样一来,基本上就实现了类似vue中的状态管理,以数据驱动的方式进行原生js的项目开发。
同时,可以实现组件化,只需要对BaseComponent基类进行扩展即可。
设想的项目整体架构如下:
src/
├── core/
│ ├── StateManager.js // 状态管理中心
├── components/
│ ├── BaseComponent.js // 组件基类
│ ├── order/
│ │ ├── index.js
│ │ └── style.css
│
├── utils/
│ ├── tools.js // 工具方法
│ └── api.js // api请求
├── views/
│ ├── index.html
└── app.js // 可以一些全局的东西
order/index.js的实现:
// 订单信息
function OrderInfo(container, stateManager) {
BaseComponent.call(this, container, stateManager);
this.init();
}
OrderInfo.prototype = Object.create(BaseComponent.prototype);
OrderInfo.prototype.constructor = OrderInfo;
OrderInfo.prototype.init = function () {
BaseComponent.prototype.init.call(this);
};
OrderInfo.prototype.render = function () {
const template = `<div class="cardOrderWrap flex">
<a href="javascript:;" class="OrderItem flex" data-tplName="js_order"><img src="../../../static/images/index_v2/order1.png" alt=""><span>原始订单</span></a>
<div class="cardLine"></div>
<a href="javascript:;" class="OrderItem flex" data-tplName="js_bill"><img src="../../../static/images/index_v2/order2.png" alt=""><span>原始账单</span></a>
</div>
`;
this.dom = this.createDOM(template);
this.container.replaceChildren(this.dom);
};
OrderInfo.prototype.handleEvent = function (e) {
console.log("state---order", this.stateManager);
let test = {
showAmount: 8000,
};
this.stateManager.setState({
...this.stateManager.state,
...test,
});
// e.preventDefault() || e.stopPropagation();
};
index.html中的js
// index.js
const stateManager = new StateManager({
showAmount: 7000,
});
let orderInfo = new OrderInfo(
document.getElementById("orderInfo"),
stateManager
);
let canloan = new CanLoan(document.getElementById("canlon"), stateManager);
这样一来,当orderInfo组件调用setState更新状态后,CanLoan组件如果引用了这个showAmount属性,则会触发组件的更新方法从而进行渲染,无需手动进行dom操作。
这种设计的优点如下:
- 可以让原生js开发产生使用现代框架类似的组件化和模块化。
- 基于发布订阅模式的状态更新,加入了状态管理机制,状态快照机制保证数据不可变。
- 组件职责分明、样式与结构分离、基类具有很好的扩展性。
可以大大提升开发效率和项目整体的可维护性。
这种设计的缺点如下:
- html文件中需要导入大量的js 文件,后续如果能实现按需加载,则可以解决到这个问题。
- 虽然组件基类中添加了递归渲染子组件的逻辑,但无法像vue那样直接嵌套使用,稍显麻烦。
关注我,带你解锁更多前端技能!
注:如果你需要建站、小程序之类,随时欢迎联系
公众号《前端那些年》
(完)
记得点赞、关注、评论、转发一下。