序
随着web项目越来越复杂,排除掉外部引入的代码,开发的代码动辄上千过万行,甚至更多。按照最初的开发经验,我们把代码全部放在一个文件上,除了代码多以外,还有命名容易被覆盖,冲突等问题。 从管理上我们会选择把它分开若干个js,分成各种部分,每个js管理某些部分或功能。不同文件中的代码可能由不同的多个人开发,如果把变量都放在全局上,可能会造成变量被篡改,函数名冲突等问题。
通常我们把每个分出来的部分,叫做模块,模块的编写每个团队都有自己的规范,为了更好的管理,进而引申出模块化开发,即把整个项目分成若干个模块,分发给不同的人,每个模块处理某些功能,最后通过协定把它们凑合在一起,最终形成了一个项目。
需求
然而web的模块化并不那么容易,如果单纯的使用js来处理逻辑,我们可以利用灵活的js特性,通过闭包或者面对对象方式解耦来处理,如果涉及到了html和css,我们需要考虑更多问题。
-
html和css作为字符串的方式引入到js中,可能会导致调试的不便;
-
把html单独抽出来作为一个片段,需要部署到服务器中,文件直接打开无法获取;
-
将html放入页面中,注册事件以及引用其它对象,一不小心就会造成内存泄漏,内存的合理使用在webapp中是非常重要的;
-
css和html文件一样,是通过片段方式的引入,还是通过全局引入,如果以片段方式的引入,在切换页面的时候,移除样式,如果样式中包含全局样式,可能会照成页面抖动或布局重绘。
整体思想
通过以上讨论,以下列的方式进行统一规定
-
纯js的模块,如果有必要的会必须要有dispose方法,用来作为删除不需要的内存引用。然后在index.html引入该文件,或用的时候异步引入。
简单案例代码1
var m1 = { list: [], doMethod: function () { for (var i = 0; i < 1000; i++) this.list.push(i); }, // list除了可以通过dispose()来删除,还可以直接通过m1.list.length = 0来输出 dispose: function () { this.list.length = 0; } };
简单案例代码2
var m2 = (function () { var list = []; var doMethod = function () { for (var i = 0; i < 1000; i++) list.push(i); } return { doMethod: doMethod, // list内的内容只能m2.dispose()来删除,不然永远滞留在内存在, 从而保护私有变量 dispose: function () { list.length = 0; } } })();
-
如果带有html的片段,首先以单独html片段方式引入,由于ajax获取html片段是需要短暂的时间,在特殊情况下可以酌情以字符串的形式嵌入,对于dom的使用和事件的引用,要在模块销毁之前进行分离引用。
简单案例代码
var m3 = (function () { var domList = {}, eventList = {}, parentDom = null; // 加载html片段到dom中 var addDom = function (dom, bk) { ajax.fetch("temp.html", function (text) { parentDom = dom; dom.innerHTML = text; bk(); }) } // 绑定事件,缓存dom var attachDomEvent = function (key, type, eventFn) { domList[key] = domList[key] || parentDom.querySelector(key); if (eventList[key]) eventList[key].push(eventFn); else eventList[key] = [eventFn]; domList[key].addEventListener(type, eventFn); } // 清除引用,待系统回收内存 var dispose = function () { // 清除事件 for (var key in domList) { if (eventList[key]) { for (var i = 0; i < eventList[key].length; i++) domList.removeEventListener(domList[key], eventList[key][i]) } } // 移除dom parentDom.innerHTML = ""; // 移除引用 domList = null; eventList = null; } // 暴露外部使用 return { addDom: addDom, attachDomEvent: attachDomEvent, dispose: dispose } })();
-
如果是css的样式,那就以全局的方式在最开始的时候在index.html引入多个css文件,利用css的特性进行模块筛选。
在index.html中引入 part1.css, part2.css。如果开发的人很多,可以引入更多, 通过后代选择器进行样式书写。
part1.css简单案例
.part1 container { margin: 0 ; }
part2.css简单案例
.part2 container { margin: 20px; }
框架中的使用
-
纯js的模块引入及定义
简单案例说明,假设在/public/services/mk.js
// 引入模块依赖,避免循环引用和自引用 // strTool, otherTool就是其它定义的模块,在内部就可以使用 App.require(["strTool", "otherTool"], function (strTool, otherTool) { // 其它定义和事务处理 // 定义模块, 第一个参数为模块名,第二个为模块的对象 App.define("mk1", {}); })
-
带有html的模块使用,主要用于Page对象,Component对象
简单代码案例
// 通过引用外部模块,如果不引用直接传入一个回调函数 App.require(["mapTool"], function (mapTool) { var app = App.getCurrent(); // 可以存储私有对象和私有处理方案代码 // 定义page1的Page对象 app.definePage("page1", { render: function (next) { this.fetch("./page1.html", function (text) { // 将html模板传入,可以用来渲染页面 next(text); }) }, // 缓存dom和绑定事件。 getDomObj: function () { this.attachDom("button", "confirmBtn") .attachEvent("confirmBtn", this.confirmHandler, false); }, // 引入资源初始化,init方法在html放在页面,并且缓存dom和绑定事件后触发 init: function () { // 初始化 mapTool.init(); }, // 事件回调 confirmHandler: function (ev) { // 原生事件ev, this.domList缓存dom console.log(this.domList.confirmBtn === ev.target) }, dispose: function () { // 外部资源销毁 mapTool.dispose(); } }); })
页面中的事件会通过切换页面内部进行解绑,dom缓存会在恰当时机进行删除。
-
css的引入,与传统的方案一致
对于1,2两点,可能会有点困惑,传统的方式也能很好的工作,为什么还要进行一次封装呢,主要由以下原因:
- 随着业务发展,模块可以是上百个,按照传统方式也会由上百个全局变量。因此对于复杂的引用会产生管理困难;
- 为了按需引用,将每个模块分成通过名称引用,此时需要某个模块的时候,可以判断是否存在,如果不存在,去加载js文件,并且注册。从而提高了资源的利用率
通过以上方案,只要对模块进行统一的注册,内部的一个对象进行管理,使得最终易于管理和使用。
简单案例
为了更好的理解将上一章节的内容代码拷贝下来,并作以下修改
-
查看index.js, home.js, static.js中的代码改为引入模块的方式, 如下:
(function () {})();
修改为
App.require(function () {});
-
在services文件夹中添加两个文件,这里注意,要求services类的模块的文件名和定义的模块名称必须是一致。如果不一致需要手动设置模块配置项。
添加/public/services/strTool.js:注意模块名和文件名要一致
App.require(function () { App.define("strTool", { scriptName: "https://webapi.amap.com/maps/modules?v=1.4.15&key=82c79f1397d8cf32f8e2c7f97f814a3c&vrs=1599633849065" }); });
添加/public/services/mapTool.js:
App.require(["strTool"], function (strTool) { var map = null; var obj = { initMap: function (dom, lng, lat) { map = new AMap.Map(dom, { center: [lng, lat] }); }, dispose: function () { map.destroy(); map = null; } }; if (typeof AMap === "undefined") { var script = document.createElement("script"); script.src = strTool.scriptName; script.onload = function () { App.define("mapTool", obj); } script.onerror = function () { App.define("mapTool", { init: function () { alert("js引入失败"); } }) } document.head.appendChild(script); } else { App.define("mapTool", obj); } })
-
修改second.js代码
App.require(["mapTool"], function (mapTool) { var app = App.getCurrent(); app.definePage("second", { render: function (next) { this.fetch("./second.html", next); }, getDomObj: function () { this.attachDom("button", "btn") .attachDom("div", "div") .attachEvent("btn", "click", this.clickHandler, false); }, init: function () { app.staticPage.header = "次页"; app.staticPage.footer = "从首页跳转到次页"; // 初始化地图 mapTool.initMap(this.domList.div, 120, 27.7); }, clickHandler: function (ev) { history.back(); }, dispose: function () { // 销毁地图 mapTool.dispose(); } }) });
-
查看效果(主要针对移动端,可以通过手机端查看或者用浏览器模拟移动端)将代码放在可以访问的服务器或本地服务上,启动服务,通过浏览器访问,如下地址
结语
主要探索了如何管理复杂项目的构建方法,然后介绍了模块化方法,引出了框架层如何实现按需引入模块从而提高页面加载速度。分模块开发可以让开发更专注,效率更高。
推广
底层框架开源地址:https://gitee.com/string-for-100w/string