「前端工程化系列-模块化技术」

665 阅读5分钟

本文章内容涉及前端模块化发展简史、CommonJS、AMD、CMD规范,以及现在的ESModule,了解这些浏览器和服务器通用的模块化方案的原理,以及后续更文的npm包管理、webpack打包编译工具,我们将能够以全新视角一举窥探前端工程化的关键技术。

前端模块化的定义

模块化,其实就是将程序按规则拆分为可按需导入、可复用的模块,通过模块依赖关系可构建成完整的程序。体现的是高内聚低耦合的设计原则。

前端模块化的简史
1. 全局function模式:将不同功能封装成不同的全局函数
// 即function声明都是挂在window下 (浏览器环境) 
// 封装的全局函数可能散落在多个js文件、<script>标签
funtion dateFn(){  
  return new Date()  
}  
  
function timeFn(date){  
  return date.getTime()  
}  
  
function demoFn(){  
  return {}  
}  
  
const date = dateFn();  
const time = timeFn(date)

缺陷:容易引发全局命名空间冲突,而且模块成员之间看不出直接关系

2. 全局namespace模式:通过统一规范namespace
// 减少全局变量 解决命名冲突
window.__Module__ = {
  data: 100,  
  dateFn(){
    return new Date()  
  },  
  timeFn(date){  
    return date.getTime()  
  },  
  demoFn(){
    return {}  
  }  
}  
  
const module = window.__Module__  
consr date = module.dateFn()

// 100
console.log(module.data)

module.data = 200
// 200
console.log(module.data)

缺陷:由于外部业务代码可以直接修改模块内部数据状态,引发数据安全风险

3. 立即执行函数IIFE模式:通过IIFE产生独立词法作用域
// 独立作用域 避免被外部直接访问模块内部
(function(window){
  let data = 100

  funtion dateFn(){  
    return new Date()  
  }  
    
  function timeFn(date){  
    return date.getTime()  
  }  
    
  function setData(val){  
    data = val
  }

  window.__Module__ = {
    data,
    dateFn,
    timeFn,
    setData,
  }
})(window)

const module = window.__Module__  
consr date = module.dateFn()

// 100
console.log(module.data)

module.setData(200)
// 200
console.log(module.data)

module.data = 300
// 200
console.log(module.data)

缺陷:无法解决模块间相互依赖的问题

4.立即执行函数IIFE模式增强:自定义依赖

假定上面的模块为moduleA.js 定义下面的moduleB.js依赖它

(function(window, moduleA){
  function getData(){
    return moduleA.data
  }

  window.__Modules__ = {
    moduleA,
    getData,
  }
})(window, window.__Module__)

const module = window.__Modules__
module.moduleA.setData(250)

缺陷:依赖为参数传递,强耦合,可维护性低

前端模块化的规范
1. CommonJS模块规范简介
Node.js模块系统中,每个文件都被视为独立的模块。拥有自己的模块作用域,不会污染全局作用域。

模块在服务器端运行时同步加载方式,在浏览器端提前编译打包处理方式。

模块可多次加载,第一次加载时会运行模块,模块输出结果会被缓存,再次加载时,会从缓存结果中直接读取模块输出结果。

模块加载的顺序,按照其在代码中出现的顺序。

通过require加载模块,exportsmodule.exports导出模块。

模块导出的变量值是值拷贝,类似立即执行函数IIFE方案中的内部变量。

// mod.js
var val = 250
function setVal(v) {
  val = v
}
module.exports = {
  val,
  setVal,
}

//biz.js
const mod = require('./mod')
mod.setVal(120)
2. CommonJS模块加载机制

CommonJS模块导出的变量值是值拷贝,即使业务引入该模块变量值,通过模块暴露的接口修改内部变量值也不会影响业务的变量值。

//biz.js
const mod = require('./mod')

// 250
console.log(mod.val)

mod.setVal(110)
// 250
console.log(mod.val)
3. AMD与CMD模块规范简介
AMD规范采用非同步加载模块,允许指定回调函数(针对commonjs同步而诞生的规范)。

Node主要用于服务器端,模块文件通常都位于本地硬盘,加载速度比较快,所以适用CommonJS的这种同步加载方案。

但是浏览器环境下,模块需要网络请求从服务端加载模块,所以适用于异步加载,一般采用AMD规范。

require.jsAMD方案的一个具体实现库。

//定义模块
define(function(){
   return {}
})

//定义模块 依赖其它模块
define(['module1', 'module2'], function(m1, m2){
   return {}
})

//引入使用模块
require(['module1', 'module2'], function(m1, m2){
    //使用m1 m2
})
CMD模块方案整合了CommonJSAMD的优点,异步加载,使用时才会加载模块执行。

Sea.jsCMD方案的一个实现库。

// 定义模块
define(function(require, exports, module){
  exports.val = value
  module.exports = value
})

// 定义模块
define(function(require, exports, module){
  // 引入依赖模块(同步)
  var m2 = require('./m2')
  // 引入依赖模块(异步)
  require.async('./m3', function (m3) {
    console.log(m3)
  })
  // 导出模块
  exports.val = value
})

// 引用模块
define(function (require) {
  var m1 = require('./m1')
  var m4 = require('./m4')
  m1.xx()
  m4.xx()
})

AMD与CMD也仅是社区过渡方案,如今node环境下推行社区规范CommonJS,浏览器环境下推行ECMA规范ESModule。

4. ESModule模块规范简介

ESModule模块方案,在编译时通过静态代码分析就可确定模块的依赖关系、输入输出,同时还支持动态加载方式。通过export、export default、import、import()关键字声明和引用。

// 第1种方式 mod.js
export const val = 250;

export const getVal = () => {
    return val
}

// 第2种方式 mod.js
const val = 250;

const getVal = () => {
    return val
}

export { val, getVal }

// 第1、2种方式 模块引用
import { val, getVal } from './mod.js'
console.log(val)

// 第3种方式 mod.js
const request = (url) => {
    return fetch(url)
}
export default request

// 第3种方式 模块引用 可自定导入名称
import req from './mod.js'
req('/api/v1').then(res => {})
// 异步加载
import('/js/mod.js').then(mod => { 
    console.log(mod) 
})

// 动态加载 即运行时通过资源路径按需异步加载
fetch('/api/js/mod/v1').then(res => {
    if (res.data.url) {
        import(res.data.url).then(mod => {
            console.log(mod)
        })
    }
})

5. npm、webpack简介

npm是一个js软件包中心仓,开发者可分享、引用、管理npm包。简单的说,npm包导出暴露的接口是符合模块化规范标准,这使得我们可引用这些npm包,安装添加至自己的项目依赖(package.json),在业务代码import、require使用它们,从而提高开发效率。通过工程化包的封装,实现代码的通用、复用能力。

webpack是一个用于现代JavaScript应用程序的静态模块打包工具。它处理应用代码时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph),然后将项目中所需的每一个模块组合成一个或多个bundles。它可通过loader和plugins拓展处理模块的能力,比如可对模块源代码进行读写、转换等一系列操作。