仅需一篇,搞定前端模块化

【稿件】前言

在JavaScript发展初期就是为了实现简单的页面交互逻辑,寥寥数语即可;如今CPU、浏览器性能得到了极大的提升,很多页面逻辑迁移到了客户端(表单验证等),随着web2.0时代的到来,Ajax技术得到广泛应用,jQuery等前端库层出不穷,前端代码日益膨胀,此时在JS方面就会考虑使用模块化规范去管理。

我们提供的服务有:成都网站设计、成都做网站、微信公众号开发、网站优化、网站认证、吉首ssl等。为数千家企事业单位解决了网站和推广的问题。提供周到的售前咨询和贴心的售后服务,是有科学管理、有技术的吉首网站制作公司

本文内容主要有理解模块化,为什么要模块化,模块化的优缺点以及模块化规范,并且介绍下开发中***的CommonJS, AMD, ES6、CMD规范。本文试图站在小白的角度,用通俗易懂的笔调介绍这些枯燥无味的概念,希望诸君阅读后,对模块化编程有个全新的认识和理解!

如需本文的源代码,请猛戳源代码 。

一、模块化的理解

1.什么是模块?

  • 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起
  • 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信

2.模块化的进化过程

  • 全局function模式 : 将不同的功能封装成不同的全局函数
    • 编码: 将不同的功能封装成不同的全局函数
    • 问题: 污染全局命名空间, 容易引起命名冲突或数据不安全,而且模块成员之间看不出直接关系 
 
 
 
 
  1. function m1(){  
  2. //...  
  3. }  
  4. function m2(){  
  5. //...  
  6. }  
  • namespace模式 : 简单对象封装
    • 作用: 减少了全局变量,解决命名冲突
    • 问题: 数据不安全(外部可以直接修改模块内部的数据) 
 
 
 
 
  1. let myModule = {  
  2. data: 'www.baidu.com',  
  3. foo() {  
  4. console.log(`foo() ${this.data}`)  
  5. },  
  6. bar() {  
  7. console.log(`bar() ${this.data}`)  
  8. }  
  9. myModule.data = 'other data' //能直接修改模块内部的数据
  10. myModule.foo() // foo() other data

这样的写法会暴露所有模块成员,内部状态可以被外部改写。

  • IIFE模式:匿名函数自调用(闭包)
    • 作用: 数据是私有的, 外部只能通过暴露的方法操作
    • 编码: 将数据和行为封装到一个函数内部, 通过给window添加属性来向外暴露接口
    • 问题: 如果当前这个模块依赖另一个模块怎么办? 
 
 
 
 
  1. // index.html文件 
  2.  
  3.  
  4.     myModule.foo() 
  5.     myModule.bar() 
  6.     console.log(myModule.data) //undefined 不能访问模块内部数据 
  7.     myModule.data = 'xxxx' //不是修改的模块内部的data 
  8.     myModule.foo() //没有改变 
  9.   
 
 
 
 
  1. // module.js文件 
  2. (function(window) { 
  3.   let data = 'www.baidu.com' 
  4.   //操作数据的函数 
  5.   function foo() { 
  6.     //用于暴露有函数 
  7.     console.log(`foo() ${data}`) 
  8.   } 
  9.   function bar() { 
  10.     //用于暴露有函数 
  11.     console.log(`bar() ${data}`) 
  12.     otherFun() //内部调用 
  13.   } 
  14.   function otherFun() { 
  15.     //内部私有的函数 
  16.     console.log('otherFun()') 
  17.   } 
  18.   //暴露行为 
  19.   window.myModule = { foo, bar } //ES6写法 
  20. })(window)  

***得到的结果:

  • IIFE模式增强 : 引入依赖

这就是现代模块实现的基石。

 
 
 
 
  1. // module.js文件 
  2. (function(window, $) { 
  3.   let data = 'www.baidu.com' 
  4.   //操作数据的函数 
  5.   function foo() { 
  6.     //用于暴露有函数 
  7.     console.log(`foo() ${data}`) 
  8.     $('body').css('background', 'red') 
  9.   } 
  10.   function bar() { 
  11.     //用于暴露有函数 
  12.     console.log(`bar() ${data}`) 
  13.     otherFun() //内部调用 
  14.   } 
  15.   function otherFun() { 
  16.     //内部私有的函数 
  17.     console.log('otherFun()') 
  18.   } 
  19.   //暴露行为 
  20.   window.myModule = { foo, bar } 
  21. })(window, jQuery)  
 
 
 
 
  1. // index.html文件 
  2.   
  3.   
  4.   
  5.   
  6.    myModule.foo() 
  7.    

上例子通过jquery方法将页面的背景颜色改成红色,所以必须先引入jQuery库,就把这个库当作参数传入。这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显。

3. 模块化的好处

  • 避免命名冲突(减少命名空间污染)
  • 更好的分离, 按需加载
  • 更高复用性
  • 高可维护性

4. 引入 

  •  
  •  
  • ***得到如下结果:

    这种方式缺点很明显:首先会发送多个请求,其次引入的js文件顺序不能搞错,否则会报错!

    • 使用require.js

    RequireJS是一个工具库,主要用于客户端的模块管理。它的模块管理遵守AMD规范,RequireJS的基本思想是,通过define方法,将代码定义为模块;通过require方法,实现代码的模块加载。

    接下来介绍AMD规范在浏览器实现的步骤:

    ①下载require.js, 并引入

    • 官网: http://www.requirejs.cn/
    • github : https://github.com/requirejs/requirejs

    然后将require.js导入项目: js/libs/require.js

    ②创建项目结构

     
     
     
     
    1. |-js   
    2.     |-libs   
    3.       |-require.js   
    4.     |-modules   
    5.       |-alerter.js   
    6.       |-dataService.js   
    7.     |-main.js   
    8.   |-index.html 

    ③定义require.js的模块代码

     
     
     
     
    1. // dataService.js文件 
    2. // 定义没有依赖的模块 
    3. define(function() { 
    4.   let msg = 'www.baidu.com' 
    5.   function getMsg() { 
    6.     return msg.toUpperCase() 
    7.   } 
    8.   return { getMsg } // 暴露模块 
    9. })  
     
     
     
     
    1. //alerter.js文件 
    2. // 定义有依赖的模块 
    3. define(['dataService'], function(dataService) { 
    4.   let name = 'Tom' 
    5.   function showMsg() { 
    6.     alert(dataService.getMsg() + ', ' + name) 
    7.   } 
    8.   // 暴露模块 
    9.   return { showMsg } 
    10. })  
     
     
     
     
    1. // main.js文件 
    2. (function() { 
    3.   require.config({ 
    4.     baseUrl: 'js/', //基本路径 出发点在根目录下 
    5.     paths: { 
    6.       //映射: 模块标识名: 路径 
    7.       alerter: './modules/alerter', //此处不能写成alerter.js,会报错 
    8.       dataService: './modules/dataService' 
    9.     } 
    10.   }) 
    11.   require(['alerter'], function(alerter) { 
    12.     alerter.showMsg() 
    13.   }) 
    14. })()  
     
     
     
     
    1. // index.html文件 
    2.  
    3.  
    4.    
    5.     Modular Demo 
    6.    
    7.    
    8.      
    9.      
    10.    
    11.  

    ④页面引入require.js模块:

    在index.html引入

    此外在项目中如何引入第三方库?只需在上面代码的基础稍作修改:

     
     
     
     
    1. // alerter.js文件 
    2. define(['dataService', 'jquery'], function(dataService, $) { 
    3.   let name = 'Tom' 
    4.   function showMsg() { 
    5.     alert(dataService.getMsg() + ', ' + name) 
    6.   } 
    7.   $('body').css('background', 'green') 
    8.   // 暴露模块 
    9.   return { showMsg } 
    10. })  
     
     
     
     
    1. // main.js文件 
    2. (function() { 
    3.   require.config({ 
    4.     baseUrl: 'js/', //基本路径 出发点在根目录下 
    5.     paths: { 
    6.       //自定义模块 
    7.       alerter: './modules/alerter', //此处不能写成alerter.js,会报错 
    8.       dataService: './modules/dataService', 
    9.       // 第三方库模块 
    10.       jquery: './libs/jquery-1.10.1' //注意:写成jQuery会报错 
    11.     } 
    12.   }) 
    13.   require(['alerter'], function(alerter) { 
    14.     alerter.showMsg() 
    15.   }) 
    16. })() 

    上例是在alerter.js文件中引入jQuery第三方库,main.js文件也要有相应的路径配置。

    小结:通过两者的比较,可以得出AMD模块定义的方法非常清晰,不会污染全局环境,能够清楚地显示依赖关系。AMD模式可以用于浏览器环境,并且允许非同步加载模块,也可以根据需要动态加载模块。

    3.CMD

    CMD规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。CMD规范整合了CommonJS和AMD规范的特点。在 Sea.js 中,所有 JavaScript 模块都遵循 CMD模块定义规范。

    (1)CMD规范基本语法

    定义暴露模块:

     
     
     
     
    1. //定义没有依赖的模块 
    2. define(function(require, exports, module){ 
    3.   exports.xxx = value 
    4.   module.exports = value 
    5. })  
     
     
     
     
    1. //定义有依赖的模块 
    2. define(function(require, exports, module){ 
    3.   //引入依赖模块(同步) 
    4.   var module2 = require('./module2') 
    5.   //引入依赖模块(异步) 
    6.     require.async('./module3', function (m3) { 
    7.     }) 
    8.   //暴露模块 
    9.   exports.xxx = value 
    10. }) 

    引入使用模块:

     
     
     
     
    1. define(function (require) { 
    2.   var m1 = require('./module1') 
    3.   var m4 = require('./module4') 
    4.   m1.show() 
    5.   m4.show() 
    6. }) 

    (2)sea.js简单使用教程

    ①下载sea.js, 并引入

    • 官网: http://seajs.org/
    • github : https://github.com/seajs/seajs

    然后将sea.js导入项目: js/libs/sea.js

    ②创建项目结构

     
     
     
     
    1. |-js  
    2.    |-libs   
    3.      |-sea.js   
    4.    |-modules   
    5.      |-module1.js   
    6.      |-module2.js   
    7.      |-module3.js   
    8.      |-module4.js   
    9.      |-main.js   
    10.  |-index.html  

    ③定义sea.js的模块代码

     
     
     
     
    1. // module1.js文件 
    2. define(function (require, exports, module) { 
    3.   //内部变量数据 
    4.   var data = 'atguigu.com' 
    5.   //内部函数 
    6.   function show() { 
    7.     console.log('module1 show() ' + data) 
    8.   } 
    9.   //向外暴露 
    10.   exports.show = show 
    11. }) 
     
     
     
     
    1. // module2.js文件 
    2. define(function (require, exports, module) { 
    3.   module.exports = { 
    4.     msg: 'I Will Back' 
    5.   } 
    6. }) 
     
     
     
     
    1. // module3.js文件 
    2. define(function(require, exports, module) { 
    3.   const API_KEY = 'abc123' 
    4.   exports.API_KEY = API_KEY 
    5. }) 
     
     
     
     
    1. // module4.js文件 
    2. define(function (require, exports, module) { 
    3.   //引入依赖模块(同步) 
    4.   var module2 = require('./module2') 
    5.   function show() { 
    6.     console.log('module4 show() ' + module2.msg) 
    7.   } 
    8.   exports.show = show 
    9.   //引入依赖模块(异步) 
    10.   require.async('./module3', function (m3) { 
    11.     console.log('异步引入依赖模块3  ' + m3.API_KEY) 
    12.   }) 
    13. }) 
     
     
     
     
    1. // main.js文件 
    2. define(function (require) { 
    3.   var m1 = require('./module1') 
    4.   var m4 = require('./module4') 
    5.   m1.show() 
    6.   m4.show() 
    7. }) 

    ④在index.html中引入

     
     
     
     
    1.  
    2.  
    3.   seajs.use('./js/modules/main') 
    4.  

    ***得到结果如下:

    4.ES6模块化

    ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

    (1)ES6模块化语法

    export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

     
     
     
     
    1. /** 定义模块 math.js **/ 
    2. var basicNum = 0; 
    3. var add = function (a, b) { 
    4.     return a + b; 
    5. }; 
    6. export { basicNum, add }; 
    7. /** 引用模块 **/ 
    8. import { basicNum, add } from './math'; 
    9. function test(ele) { 
    10.     ele.textContent = add(99 + basicNum); 

    如上例所示,使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。

     
     
     
     
    1. // export-default.js 
    2. export default function () { 
    3.   console.log('foo'); 
     
     
     
     
    1. // import-default.js 
    2. import customName from './export-default'; 
    3. customName(); // 'foo' 

    模块默认输出, 其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。

    (2)ES6 模块与 CommonJS 模块的差异

    它们有两个重大差异:

    ① CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。

    ② CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

    第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

    下面重点解释***个差异,我们还是举上面那个CommonJS模块的加载机制例子:

     
     
     
     
    1. // lib.js 
    2. export let counter = 3; 
    3. export function incCounter() { 
    4.   counter++; 
    5. // main.js 
    6. import { counter, incCounter } from './lib'; 
    7. console.log(counter); // 3 
    8. incCounter(); 
    9. console.log(counter); // 4 

    ES6 模块的运行机制与 CommonJS 不一样。ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

    (3) ES6-Babel-Browserify使用教程

    简单来说就一句话:使用Babel将ES6编译为ES5代码,使用Browserify编译打包js。

    ①定义package.json文件

     
     
     
     
    1. {   
    2.     "name" : "es6-babel-browserify",   
    3.     "version" : "1.0.0"   
    4.   }  

    ②安装babel-cli, babel-preset-es2015和browserify

    • npm install babel-cli browserify -g
      • npm install babel-preset-es2015 --save-dev
      • preset 预设(将es6转换成es5的所有插件打包) 

    ③定义.babelrc文件

     
     
     
     
    1.     "presets": ["es2015"] 
    2.   } 

    ④定义模块代码

     
     
     
     
    1. //module1.js文件 
    2. // 分别暴露 
    3. export function foo() { 
    4.   console.log('foo() module1') 
    5. export function bar() { 
    6.   console.log('bar() module1') 
     
     
     
     
    1. //module2.js文件 
    2. // 统一暴露 
    3. function fun1() { 
    4.   console.log('fun1() module2') 
    5. function fun2() { 
    6.   console.log('fun2() module2') 
    7. export { fun1, fun2 } 
     
     
     
     
    1. //module3.js文件 
    2. // 默认暴露 可以暴露任意数据类项,暴露什么数据,接收到就是什么数据 
    3. export default () => { 
    4.   console.log('默认暴露') 
     
     
     
     
    1. // app.js文件 
    2. import { foo, bar } from './module1' 
    3. import { fun1, fun2 } from './module2' 
    4. import module3 from './module3' 
    5. foo() 
    6. bar() 
    7. fun1() 
    8. fun2() 
    9. module3() 

    ⑤ 编译并在index.html中引入

    • 使用Babel将ES6编译为ES5代码(但包含CommonJS语法) : babel js/src -d js/lib
    • 使用Browserify编译js : browserify js/lib/app.js -o js/lib/bundle.js 

    然后在index.html文件中引入:

     
     
     
     
    1.   

    ***得到如下结果:

    此外第三方库(以jQuery为例)如何引入呢?

    首先安装依赖npm install jquery@1

    然后在app.js文件中引入:

     
     
     
     
    1. //app.js文件 
    2. import { foo, bar } from './module1' 
    3. import { fun1, fun2 } from './module2' 
    4. import module3 from './module3' 
    5. import $ from 'jquery' 
    6.  
    7. foo() 
    8. bar() 
    9. fun1() 
    10. fun2() 
    11. module3() 
    12. $('body').css('background', 'green') 

    三、总结

    • CommonJS规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD CMD解决方案。
    • AMD规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅。
    • CMD规范与AMD规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在Node.js中运行。不过,依赖SPM 打包,模块的加载逻辑偏重
    • ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

    参考文章 

    • 前端模块化开发那点历史
    • CommonJS,AMD,CMD区别
    • AMD 和 CMD 的区别有哪些?
    • Javascript模块化编程
    • Javascript标准参考教程
    • CMD 模块定义规范
    • 理解CommonJS、AMD、CMD三种规范

    作者:浪里行舟,慕课网认证作者,前端爱好者,立志往全栈工程师发展,从事前端一年多,目前技术栈有vue全家桶、ES6以及less等,乐于分享,最近一年写了五六十篇原创技术文章,得到诸多好评!

    【原创稿件,合作站点转载请注明原文作者和出处为.com】

    标题名称:仅需一篇,搞定前端模块化
    链接URL:http://www.36103.cn/qtweb/news33/37983.html

    网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

    广告

    声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联