🌽 模块化:把复杂的系统分解到多个模块以方便编码
在模块化编程中,开发者将程序分解成离散功能块(discrete chunks of functionality),并称之为模块。
每个模块具有比完整程序更小的接触面,使得校验、调试、测试轻而易举。 精心编写的模块提供了可靠的抽象和封装界限,使得应用程序中每个模块都具有条理清楚的设计和明确的目的。
过去代码组织方式,会出现的问题:
实现模块化能实现的优势:
module1.js
// 数据let data1 = 'module one data';// 操作数据的函数function foo() {console.log(`foo() ${data1}`);}function bar() {console.log(`bar() ${data1}`);}
module2.js
let data2 = 'module two data';function foo() {//与模块1中的函数冲突了console.log(`foo() ${data2}`);}
test.html
<!-- 同步引入,若函数冲突,则后面覆盖前面 --><script type="text/javascript" src="module1.js"></script><script type="text/javascript" src="module2.js"></script><script type="text/javascript">foo(); // foo() module two databar(); // bar() module one data</script>
说明:
module1.js
let moduleOne = {data: 'module one data',foo() {console.log(`foo() ${this.data}`);},bar() {console.log(`bar() ${this.data}`);},};
module2.js
let moduleTwo = {data: 'module two data',foo() {console.log(`foo() ${this.data}`);},bar() {console.log(`bar() ${this.data}`);},};
test.html
<script type="text/javascript" src="module1.js"></script><script type="text/javascript" src="module2.js"></script><script type="text/javascript">moduleOne.foo(); //foo() module one datamoduleOne.bar(); //bar() module one datamoduleTwo.foo(); //foo() module two datamoduleTwo.bar(); //bar() module two datamoduleOne.data = 'update data'; //能直接修改模块内部的数据moduleOne.foo(); //foo() update data</script>
说明:
data
都不是全局变量了,而是对象的某一个属性)module1.js
(function(window) {// 数据let data = 'IIFE module data';//操作数据的函数function foo() {// 用于暴露的函数console.log(`foo() ${data}`);}function bar() {// 用于暴露的函数console.log(`bar() ${data}`);otherFun(); //内部调用}function otherFun() {// 内部私有的函数console.log('privateFunction go otherFun()');}// 暴露 foo 函数和 bar 函数window.module = { foo, bar };})(window);
test.html
<script type="text/javascript" src="module1.js"></script><script type="text/javascript">module.foo(); // foo() IIFE module datamodule.bar(); // bar() IIFE module data privateFunction go otherFun()// module.otherFun() //报错,module.otherFun is not a functionconsole.log(module.data); // undefined 因为我暴露的 module 对象中无 datamodule.data = 'xxxx'; // 不是修改的模块内部的 data,而是在 module 新增 data 属性module.foo(); // 验证内部的 data 没有改变 还是会输出 foo() IIFE module data</script>
说明:
引入 jQuery 到项目中
module1.js
(function(window, $) {//数据let data = 'IIFE Strong module data';//操作数据的函数function foo() {//用于暴露的函数console.log(`foo() ${data}`);$('body').css('background', 'red');}function bar() {//用于暴露的函数console.log(`bar() ${data}`);otherFun(); //内部调用}function otherFun() {//内部私有的函数console.log('privateFunction go otherFun()');}//暴露foo函数和bar函数window.moduleOne = { foo, bar };})(window, jQuery);
test.html
<!--引入的js必须有一定顺序--><script type="text/javascript" src="jquery-1.10.1.js"></script><script type="text/javascript" src="module1.js"></script><script type="text/javascript">moduleOne.foo(); //foo() IIFE Strong module data 而且页面背景会变色</script>
说明:
<script type="text/javascript" src="module1.js"></script><script type="text/javascript" src="module2.js"></script><script type="text/javascript" src="module3.js"></script><script type="text/javascript" src="module4.js"></script>
<script>
标签就是一次请求CommonJS 是服务器端模块的规范,Node.js 就是采用了这个规范。但目前也可用于浏览器端,需要使用 Browserify 进行提前编译打包。
CommonJS 模块化的引入方式使用 require
,暴露的方式使用 module.exports
或 exports
。
特点
彻底说明白
module.exports
和exports
的区别:
在 Node.js 中,module
是一个全局变量,类似于在浏览器端的 Window 也是一个全局变量一样的道理。
module.exports
初始的时候置为空对象,exports
也指向这个空对象。
内部代码实现:
var module = {id: 'xxxx',exports: {},};var exports = module.exports;// exports 是对 module.exports 的引用// 也就是 exports 现在指向的内存地址和 module.exports 指向的内存地址是一样的
上面的代码可以看出我们平常使用的 exports
是对 module.exports
的一个引用,两者都是指向同一个对象。
用一句话来说明就是,模块的 require
(引入)能看到的只有 module.exports
这个对象,它是看不到 exports
对象的,而我们在编写模块时用到的 exports
对象实际上只是对 module.exports
的引用。
exports = module.exports;
我们可以使用 exports.a = ‘xxx’
或 exports.b = function(){}
添加方法或属性,本质上它也添加在 module.exports
所指向的对象身上。
但是你不能直接 exports = { a: 'xxx'}
,这就将 exports
重新指向新的对象,它和 module.exports
就不是指向同一个对象,也就让两者失去了关系,而 Node.js 中 require
能看到的是 module.exports
指向的对象。
因此,我们一般都会直接使用:
module.exports;
再举例说明两者区别:
function foo() {console.log('foo');}function bar() {console.log('bar');}
想要将这两个函数暴露出去,可以直接使用exports
exports.foo = foo;exports.bar = bar;
也可以对 module.exports
赋值
module.exports = {foo: foo,bar: bar,};
但是不能直接对 exports
赋值
// 错误exports = {foo: foo,bar: bar,};
因为这样做仅仅改变了exports
的引用,而不改变 module.exports
。
总结
特点:同步加载,有缓存
用法:关键在于引入和暴露
require(url)
(url
为路径参数)./
或者 ../
开头exports
module.exports
主要是在服务器端使用的,但是也能在浏览器端运行,需要借助 Browserify 进行编译。
CommonJS 规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。由于 NodeJS 主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,所以同步加载没有问题。但是如果是浏览器端,同步加载很容易阻塞,这时候 AMD 规范就出来了。AMD 规范则是非同步加载模块,允许指定回调函数。故浏览器端一般会使用 AMD 规范。
AMD 是 require.js 在推广过程中对模块定义的规范化产出 。
特点:
用法:
return
define(['模块名'], function (模块暴露内容) {})
require(['模块名'], function (模块暴露内容) {})
require
定义异步模块requirejs.config({})
配置使用的模块路径requirejs(['模块名'], function (模块暴露内容) {})
<script>
标签<script data-main='app.js' src='require.js'></script>
AMD(通用模块定义)主要是在浏览器使用的。
CMD 是根据 CommonJS 和 AMD 基础上提出的。
CMD(通用模块定义)和 AMD(异步模块定)是比较相似的。
require.js
遵循的是 AMD(异步模块定义)规范,sea.js 遵循的是 CMD (通用模块定义)规范。
特点:
用法:
define(function (require, exports, module) {})
require()
require.async(['模块名'], function(模块暴露内容) {})
exports
module.exports
<script>
标签<script src='sea.js'></script>
<script>seajs.use('app.js')</script>
sea.js
和 require.js
一样主要在浏览器中使用。其实这两个一般都很少使用。用的比较多的是 commonjs
和马上要介绍的 ES6 模块化。
特点:
动态引入(按需加载),没有缓存
用法:
引入模块使用 import
import {模块暴露的内容} from '模块路径'
import * as m1 from './module1'
import 模块暴露的内容 from '模块路径'
。默认暴露,暴露任意数据类型,暴露什么数据类型,接收什么数据类型。暴露模块使用 export
主要是用在浏览器,服务器端也使用。但是现在浏览器和服务器均不支持 ES6 的模块化语法,所以要借助工具来编译运行
commonjs
)ES6 的模块自动采用严格模式,不管你是否有在模块头部加上 'use strict'
。
严格模式主要有以下限制:
with
语句delete prop
,会报错,只能删除属性 delete global[prop]
eval
不会在它的外层作用域引入变量eval
和 arguments
不能被重新赋值arguments
不会自动反映函数参数的变化arguments.callee
arguments.caller
this
指向全局对象fn.caller
和 fn.arguments
获取函数调用的堆栈protected
、static
和 interface
)其中,尤其需要注意 this
的限制。ES6 模块之中,顶层的 this
指向 undefined
,即不应该在顶层代码使用 this
。
既然说到模块化,其实我更想说说模块化与组件化。这两个概念在前端领域已经十分普遍。
先有模块化后有组件化。组件化是建立在模块化思想上的一次演进,一个变种。所以,我们会在软件工程体系中看过一句话:模块化是组件化的基石。
组件化和模块化的思想都是 分而治之 的思想。但还是有细小的区分,他们的侧重点有所不同。
组件化更加倾向于 UI 层面上,是一个可以独立展示内容的「积木」,比如一个页面的头部组件,包含结构 HTML、样式 CSS、逻辑 JS、以及静态资源图片组合一个集合体。一个页面是由众多组件组成的,就像由众多「积木」搭成的「城堡」一样; 模块化更加倾向于功能或者数据的封装,一般由几个组件或单个组件构成的带有一定功能的集合体;
引用一下 @张云龙 对组件化的理解:
就如上图的这个 title
组件,包含了结构 HTML、样式 CSS、逻辑 JavaScript、以及静态资源图片,往往组件的组成就是以上四个方面。这个 header
文件夹我们可以拿到其他项目中使用,它具有可以独立展示内容的特点。
结合前面提到的模块化开发,整个前端项目可以划分为这么几种开发概念:
名称 | 说明 | 举例 |
---|---|---|
JS 模块 | 独立算法和数据单元 | 浏览器环境检测(detect)、网络请求(ajax)、应用配置(config)、DOM 操作(dom)、工具函数(utils)以及组件里的 JS 单元 |
CSS 模块 | 独立的功能性样式单元 | 栅格系统(grid)、字体图标(icon-fonts)、动画样式(animate)以及组件里的 CSS 单元 |
页面 | 前端这种 GUI 软件的的界面状态,是 UI 组件的容器 | 首页(index)、列表页(list)、用户管理(user) |
应用 | 整个项目或整个站点被称之为应用,由多个页面组成 |
那么它们之间的关系如下图所示,一个应用由多个下图的页面组成。一个页面由多个组件组合。组件中可依赖 JS 模块。
所以,前端开发现在不仅仅只是别人说的「画画页面实现点效果」的职位,它是实现软件的图形用户界面(Graphical User Interface,简称 GUI),是一名软件工程师。现在前端开发都是基于模块化和组件化的开发,可以说算是工程化的项目了。从单页面(SPA)的应用就可以看出 JavaScript 大大改善了 Web 应用的用户体验。从谷歌提出 PWA(Progressive Web Apps)就可以看出前端在领域的成长。
不仅仅如此,多终端也已经成为时下以及未来的一个必然趋势,移动端、PC 端、触摸屏、智能设备、物联网等等,相信前端在跨端的领域下肯定会有更好的解决方案。
但是,如果从整个软件工程来看,我们就会意识到一个惨痛的事实:前端工程师在整个系统工程中的地位太低了。前端是处于系统软件的上游(用户入口),因此没有其他系统会来调取前端系统的服务。而后端它在软件开发中处于下游,后端一方面要为前端提供接口服务,一方面要向中后台以及数据层索取服务,对接层次会更多,地位也就更高了。由此导致,感觉每次需求评估前端往往是最后一道坎,因为上游依托下游,就只能是下游先行了,整体上就会感觉前端对业务的参与度太低了。
参考资料: