漫漫前端路之JS基础——JS模块化篇

133 阅读11分钟

模块化

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

为什么要有模块化?

  • 避免命名冲突,减少命名空间污染
  • 降低复杂度
  • 提高解耦性,按需加载,部署方便,一个项目中不需要的模块js代码不需要引入
  • 更高的复用性
  • 高可维护性(理想)

模块化的缺点

  • 请求过多(1个文件拆成10个)
  • 依赖模糊
  • 难以维护(互相依赖,引入js的顺序不能更换,否则会报错)

模块的组成

  • 数据--->内部的属性
  • 操作数据的行为--->内部的函数

模块化的进化过程

  • 全局function模式 :
// module.js
let msg = ''
function foo () {
  console.log(msg)
}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>01_全局function模式</title>
</head>
<body>
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
  msg = '1'
  foo()
</script>
</body>
</html>

编码: 全局变量/函数,缺点:

  1. 耦合度高,关联性强,不方便维护
  2. 功能点不明确
  3. 污染全局命名空间, 容易引起命名冲突/数据不安全,。
  • namespace模式 :
let obj = {
   msg:'module2',
   foo() {
      console.log(this.msg)
   }
}
<script type="text/javascript">
  obj.foo();
  obj.msg = 'NBA'
  obj.foo();//NBA
</script>

编码: 将数据/行为封装到对象中,解决了命名冲突(减少了全局变量),缺点:数据不安全(外部可以直接修改模块内部的数据)

  • IIFE模式:立即调用函数表达式--->匿名函数自调用
(function(window){
   let msg = 'module3'
   function foo (){
     console.log(msg)
   }
   window.myModule={foo}
})(window)
<script type="text/javascript">
  msg = '';
  console.log(msg);//''相当于在window添加一个属性msg=''
  foo();//不能直接调用
  myModule.foo()
  //myModule.otherFun()  //myModule.otherFun is not a function
  console.log(myModule.data) //undefined 不能访问模块内部数据
  myModule.data = 'xxxx' //不是修改的模块内部的data
  myModule.foo() //没有改变
</script>

编码: 将数据和行为封装到一个函数内部, 通过给window添加属性来向外暴露接口,函数是js唯一的local scope。

  • IIFE增强模式:
(function(window,$){
   let msg = 'module4'
   function foo (){
     console.log(msg)
     $('body').css('background', 'red')
   }
   window.module4={foo};
})(window,jQuery)
<script type="text/javascript" src="jquery-1.10.1.js"></script>
<script type="text/javascript" src="module4.js"></script>
<script type="text/javascript">
  module4();
</script>

引入依赖: 通过函数形参来引入依赖模块。这就是模块模式,也是现代模块实现的基石。

模块化规范

CommonJS

规范说明

  • 每个文件都可以当作一个模块;
  • 在服务器端:模块加载时运行时同步加载的;【同步带来的后果是等待造成堵塞】
  • 在浏览器端:模块需要提前编译打包处理;运行的是打包生成的js, 运行时不存在需要再从远程引入依赖模块

基本语法

  • 暴露模块:
exports.xxx = value(任意数据类型)
module.exports = value

暴露的本质都是exports对象

  • 引入模块:
var module = require('模块名/模块相对路径')
// 模块名为第三方模块,模块相对路径为自定义模块

实现

  1. Node.js : 服务器端
  • 下载安装node.js
  • 创建项目结构
  |-modules
    |-module1.js
    |-module2.js
    |-module3.js
  |-app.js
  |-package.json // 当前文件夹的终端输入npm init可以自动创建
    {
      "name": "commonJS-node",
      "version": "1.0.0"
    }
  • 下载第三方模块
npm install uniq --save // 在package添加依赖
  • 模块化编码
// module1.js
module.exports = {
  foo() {
    console.log('moudle1 foo()')
  }
}
// module2.js
module.exports = function () {
  console.log('module2()')
}
// module3.js
exports.foo = function () {
  console.log('module3 foo()')
}

exports.bar = function () {
  console.log('module3 bar()')
}
exports.arr = [2,3,1,1,2,3];
// app.js
"use strict";
// 第三方模块引入要放在前面
let uniq = require('uniq')

//引用模块
let module1 = require('./modules/module1')
let module2 = require('./modules/module2')
let module3 = require('./modules/module3')

let fs = require('fs')

//使用模块
module1.foo()
module2()
module3.foo()
module3.bar()

console.log(uniq(module3.arr))

fs.readFile('app.js', function (error, data) {
  console.log(data.toString())
})
  • 通过node运行app.js 命令: node app.js / 工具: 右键-->运行
  1. Browserify : 浏览器端,也称为js的打包工具(ES6也用)
  • 创建项目结构
  |-js
    |-dist //打包生成文件的目录
    |-src //源码所在的目录
      |-module1.js
      |-module2.js
      |-module3.js
      |-app.js //应用主源文件
  |-index.html // 不同于服务端是用Node运行app.js,浏览器端必须用html才能运行文件
  |-package.json // npm init / 手动创建
    {
      "name": "browserify-test",
      "version": "1.0.0"
    }
  • 下载browserify 全局: npm install browserify -g and 局部: npm install browserify --save-dev(当前包只在开发环境运行)【browserify要求两个都要执行】
// package.json 
    {
      "name": "browserify-test",
      "version": "1.0.0",
      "devDependencies":{
        "browserify": "^14.5.0" // 开发依赖(开发调试)
      }
      "dependencies:"{
        "uniq":"^1.0.1" // 生产环境依赖(线上)
      }
    }   
  • 定义模块代码
// module1.js
module.exports = {
  foo() {
    console.log('moudle1 foo()')
  }
}
// module2.js
module.exports = function () {
  console.log('module2()')
}
// module3.js
exports.foo = function () {
  console.log('module3 foo()')
}
exports.bar = function () {
  console.log('module3 bar()')
}
// app.js (应用的主js)
let uniq = require('uniq')
//引用模块
let module1 = require('./module1')
let module2 = require('./module2')
let module3 = require('./module3')
//使用模块
module1.foo()
module2()
module3.foo()
module3.bar()
console.log(uniq([1, 3, 1, 4, 3]))
  • 打包处理js:
browserify js/src/app.js -o js/dist/build.js
  • 页面使用引入:
<script type="text/javascript" src="js/dist/build.js"></script> 

AMD

规范说明

专门用于浏览器端,模块加载时异步的

基本语法

  • 暴露模块
  1. 定义没有依赖的模块
define(function(){
  return 模块
})
  1. 定义有依赖的模块
define(['module1','module2'],function(m1,m2){
  return 模块
})
  • 引入模块
require(['module1','module2'],function(m1,m2){
  使用m1/m2
})

实现

  1. NoAMD
  • 创建项目结构
  |-js
    |-alert.js
    |-dataService.js
  |-app.js 
  |-test.html 
  • 定义模块代码
// dataService.js
(function (window) {
  let msg = 'atguigu.com'

  function getMsg() {
    return msg.toUpperCase()
  }

  window.dataService = {getMsg}
})(window)
// alert.js
(function (window, dataService) {
  let name = 'Tom'

  function showMsg() {
    alert(dataService.getMsg() + ', ' + name)
  }

  window.alerter = {showMsg}
})(window, dataService)
// app.js
(function (alerter) {
  alerter.showMsg()
})(alerter)
// test.html
<script type='text/javascript' src='js/modules/dataService.js'></script>
<script type='text/javascript' src='js/modules/alerter.js'></script>
<script type="text/javascript" src="js/main.js"></script>
// 请求多,依赖关系难维护
  1. AMD
  • 下载require.js / 第三方模块, 将require.js / 第三方模块导入项目: js/libs/require.js and js/libs/jquery-1.10.1.js 官网: www.requirejs.cn/
  • 创建项目结构
  |-js
    |-libs
      |-jquery-1.10.1.js
      |-angular.js
      |-require.js
    |-modules
      |-alerter.js
      |-dataService.js
    |-main.js
  |-index.html
  • 定义模块代码
// dataService.js 没有依赖的模块
define(function () {
  let msg = 'atguigu.com'
  function getMsg() {
    return msg.toUpperCase()
  }
  return {getMsg}
})
// alerter.js 有依赖的模块
define(['dataService', 'jquery'], function (dataService, $) {
  let name = 'Tom2'
  function showMsg() {
    $('body').css('background', 'gray')
    alert(dataService.getMsg() + ', ' + name)
  }
  return {showMsg}
})
// main.js
  (function () {
    //配置
    requirejs.config({
      //基本路径
      baseUrl: "js/",//如果未设置,就是基于main.js
      //模块标识名与模块路径映射,这样在模块互相引用时才能找的到
      paths: {
        "alerter": "modules/alerter", // or "./modules/alerter"
        "dataService": "modules/dataService",
         //库模块
        'jquery': 'libs/jquery-1.10.1',
        'angular': 'libs/angular'
      }
    })
    //配置不兼容AMD的模块
    shim: {
      angular: {
        exports: 'angular'
      }

    }
  })

  //引入模块使用
  require(['alerter', 'angular'], function (alerter, angular) {
    alerter.showMsg()
    console.log(angular);
  })
<script data-main="js/main.js" src="js/libs/require.js"></script>

CMD

规范说明

专门用于浏览器端,模块加载时异步的,模块使用时才会加载

基本语法

  • 暴露模块
  1. 定义没有依赖的模块
define(function(require,exports,module){
  exports.xxx = value;
  module.exports = value
})
  1. 定义有依赖的模块
define(function(require,exports,module){
  // 引入依赖模块(同步)
  var module2 = require('./module2);
  // 引入依赖模块(异步)
  require.async('./module3',function(m3){
  
  })
  exports.xxx = value;
  module.exports = value
})
  • 引入模块
define(function(require){
  var m1 = require('./module1')
  var m1 = require('./module4')
  m1.show();
  m4.show();
})

实现

  1. 下载sea.js, 并将sea.js导入项目: js/libs/sea.js 官网: seajs.org/,github : github.com/seajs/seajs

  2. 创建项目结构

  |-js
    |-libs
      |-sea.js
    |-modules
      |-module1.js
      |-module2.js
      |-module3.js
      |-module4.js
      |-main.js
  |-index.html
  1. 定义sea.js的模块代码
// module1.js
define(function (require, exports, module) {
  //内部变量数据
  var data = 'atguigu.com'
  //内部函数
  function show() {
    console.log('module1 show() ' + data)
  }
  //向外暴露
  exports.show = show
})
// module2.js
define(function (require, exports, module) {
  module.exports = {
    msg: 'I Will Back'
  }
})
// module3.js
define(function (require, exports, module) {
  const API_KEY = 'abc123'
  exports.API_KEY = API_KEY
})
// module4.js
define(function (require, exports, module) {
  //引入依赖模块(同步)
  var module2 = require('./module2')
  function show() {
    console.log('module4 show() ' + module2.msg)
  }
  exports.show = show
  //引入依赖模块(异步)
  require.async('./module3', function (m3) {
    console.log('异步引入依赖模块3  ' + m3.API_KEY)
  })
})
// main.js 
define(function (require) {
  var m1 = require('./module1')
  var m4 = require('./module4')
  m1.show()
  m4.show()
})
// index.html
  <script type="text/javascript" src="js/libs/sea.js"></script>
  <script type="text/javascript">
    seajs.use('./js/modules/main')
  </script>

ES6

说明

依赖模块需要编译打包处理

语法

  • 暴露:export
  • 引入:import

实现(浏览器端)

  • 使用Babel将ES6转为ES5
  • 使用Browserify编译打包JS

ES6-Babel-Browserify使用教程

  1. 创建项目结构
  |-js
    |src
      |-module1.js
      |-module2.js
      |-module3.js
      |-main.js
  |-index.html
  |-.babelrc(不要写json)
  |-package.json
  1. 定义package.json文件
  {
    "name" : "es6-babel-browserify",
    "version" : "1.0.0"
  }
  1. 安装babel-cli, babel-preset-es2015和browserify
npm install babel-cli browserify -g
npm install babel-preset-es2015 --save-dev
  1. 定义.babelrc文件 rc = run control运行时需要控制的文件
{
 "presets": ["es2015"] //"presets": ["es2015", "react"] 告诉babel要转换es6和jsx的语法
}
  1. 编码
  • js/src/module1.js
export function foo() {
  console.log('module1 foo()');
}
export let bar = function () {
  console.log('module1 bar()');
}
export const DATA_ARR = [1, 3, 5, 1]
  • js/src/module2.js
let data = 'module2 data'
function fun1() {
  console.log('module2 fun1() ' + data);
}
function fun2() {
  console.log('module2 fun2() ' + data);
}
export {fun1, fun2}
  • js/src/module3.js
export default {
  name: 'Tom',
  setName: function (name) {
    this.name = name
  }
}
  • js/src/app.js
import {foo, bar} from './module1'
import {DATA_ARR} from './module1'
import {fun1, fun2} from './module2'
import person from './module3'
// import $ from 'jquery'
// $('body').css('background', 'red')
foo()
bar()
console.log(DATA_ARR);
fun1()
fun2()
person.setName('JACK')
console.log(person.name);
  1. 编译
  • 使用Babel将ES6编译为ES5代码(但包含CommonJS语法) : babel js/src -d js/lib
  • 使用Browserify编译js : browserify js/lib/app.js -o js/lib/bundle.js
  1. 页面中引入测试
  <script type="text/javascript" src="js/lib/bundle.js"></script>
  1. 引入第三方模块(jQuery) 1). 下载jQuery模块:
npm install jquery@1 --save 2). 在app.js中引入并使用
import $ from 'jquery'
$('body').css('background', 'red')

模块化规范大总结

CommonJSAMDCMDES6
引用模块requirerequirerequireimport
暴露接口module.exportsexportsdefine函数返回值 returnexportsexport
加载方式运行时加载,同步加载并行加载,提前执行,异步加载并行加载,按需执行,异步加载编译时加载,异步加载
实现模块规范NodeJSRequireJSSeaJS原生JS
适用服务器浏览器浏览器服务器/浏览器
CommonJSAMDCMDES6
引用模块requirerequirerequireimport
暴露接口module.exportsexportsdefine函数返回值 returnexportsexport
加载方式运行时加载,同步加载并行加载,提前执行,异步加载并行加载,按需执行,异步加载编译时加载,异步加载
实现模块规范NodeJSRequireJSSeaJS原生JS
适用服务器浏览器浏览器服务器/浏览器

CommonJs 和 ES6规范的区别

  1. 写法上
const fs = require('fs')
exports.fs = fs
module.exports = fs
import fs from 'fs'
import {default as fs} from 'fs'
import * as fs from 'fs'
import {readFile} from 'fs'
import {readFile as read} from 'fs'
import fs, {readFile} from 'fs'

export default fs
export const fs
export function readFile
export {readFile, read}
export * from 'fs'
  1. 输入值
  • require输入的变量,基本类型数据是赋值,引用类型为浅拷贝,可修改
  • import输入的变量都是只读的,如果输入 a 是一个对象,允许改写对象属性
import {a} from './xxx.js'

a = {}; // Syntax Error : 'a' is read-only;

a.foo = 'hello'; // 合法操作
  1. 执行顺序
  • require:不具有提升效果,到底加载哪一个模块,只有运行时才知道
const path = './' + fileName;
const myModual = require(path);
  • import:具有提升效果,会提升到整个模块的头部,首先执行。import的执行早于foo的调用。本质就是import命令是编译阶段执行的,在代码运行之前。
foo();
import { foo } from 'my_module';
  • import()函数,支持动态加载模块,其是运行时加载,还有运行到那一块,才会加载模块,可用于按需加载条件加载动态的模块路径等。
// 按需加载
button.addEventListener('click', event => {
  import('./dialogBox.js')
  .then({export1, export2} => {   // export1和export2都是dialogBox.js的输出接口,解构获得
    // do something...
  })
  .catch(error => {})
});

// 条件加载
if (condition) {
  import('moduleA').then(...);
} else {
  import('moduleB').then(...);
}


// 动态的模块路径
import(f()).then(...);    // 根据函数f的返回结果,加载不同的模块。
  1. 使用的表达式和变量
  • require可以使用表达式和变量
let a = require('./a.js')
a.add()

let b = require('./b.js')
b.getSum()
  • import静态执行,不能使用表达式和变量,因为这些都是只有在运行时才能得到结果的语法结构。
// 报错
import { 'f' + 'oo' } from 'my_module';

// 报错
let module = 'my_module';
import { foo } from module;

// 报错
if (x === 1) {
  import { foo } from 'module1';
} else {
  import { foo } from 'module2';
}

本质区别

1、浏览器在不做任何处理时,默认是不支持import和require; 2、babel会将ES6模块规范转化成Commonjs规范; 3、webpack、gulp以及其他构建工具会对Commonjs进行处理,使之支持浏览器环境browserify。有三个重大差异:

  1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  3. CommonJS 模块的require()同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。 其中,导致第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
  • CommonJS运行时加载 只能在运行时确定模块的依赖关系,以及输入和输出的变量,一个模块就是一个对象,输入时必须查找对象属性。
// CommonJS模块
let { stat, exists, readfile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
  • ES6 :编译时加载 或者静态加载 ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。可以在编译时就完成模块加载,引用时只加载需要的方法,其他方法不加载。效率要比 CommonJS 模块的加载方式高。
import { stat, exists, readFile } from 'fs';

开发环境依赖 vs 生产环境依赖

  1. 前端普通项目:package.json里面的dependencies(--save)和devDependencies(--save-dev)里面含有的包没啥区别,即使都放在devDependencies都没有关系。
  • 因为实际上部署的时候部署的页面也只是webpack打包生产出来的产品(打包出的产物html css js跟是什么依赖无关的,webpack打包是从入口文件开始分析完代码中所有的依赖后开始打的),所以依赖包放在dependencies和devDependencies没有关系。
  • 将所有依赖包放入devDependencies可能会破坏某些在服务器上进行初始build的部署脚本。因此,将所有依赖包放到dependencies中会更容易些。【有些自动构建工程build脚本会加上--production的参数,这个时候不会拉devDependencies的包,导致build流程异常】
  1. Node应用服务,因为这时dependencies和devDependencies实际上是作为运行时(runtime)部署的,所以要尽量严格区分dependencies和devDependencies,项目本身就是运行态,如果部署上线不会希望把开发态的一些工具,例如测试框架等,带到线上。

  2. 开发npm包,若开发A,A中依赖了B,如果B在dependencies的话就会一起被下载到A的node_modules,如果是开发的话,就不会被下载。因此,如果是一些比如webpack等开发依赖,可以放到devDependencies,这样就能够减少他人使用A时下载的包大小。