前端模块化的进化史,及最终形态ES Modules

模块化的演变过程

stage1-文件划分方式

将每个功能和状态数据存放在不同的文件中,约定每个文件就是独立的模块,使用时将每个文件在html中同锅script引入

缺点:所有模块都在全局范围工作,污染全局作用域,出现命名冲突,模块成员可以被修改,无法管理模块依赖关系等问题

stage2-命名空间方式

将每个功能和状态数据存放在不同的文件中,约定每个文件只暴露一个全局对象,所有的模块成员都挂载到这个全局对象下

缺点:模块成员依然可以被修改,无法管理模块依赖关系等问题

stage3-IIFE立即执行函数

将每个功能和状态数据存放在不同的文件中,文件中的模块成员都放在一个IIFE函数私有作用域中,需要暴露的成员都挂载到全局对象下

模块化规范

CommonJS规范

以同步的方式加载模块,在启动时加载模块,执行时不会加载模块,在浏览器端使用会出现页面加载效率低下,每次加载都会有大量的加载请求,所以一般用在node环境下

  • 一个文件就是一个模块
  • 每个模块都有单独的作用域
  • 通过module.exports导出成员
  • 通过require函数载入模块
AMD(Asynchronous Module Definition)规范

第一个参数是模块名称,第二参数是模块依赖项,第三个参数为一个函数,函数的每个参数为依赖项导出的成员

// 定义模块
define('module1', [
  'jquery',
  'module2'
], function($, module2) {
  'use strict'
  return {
    start: function () {
      $('body').text('text')
      module2()
    }
  }
})

// 载入模块
require(['./module1'], function (module1) {
  module1.start()
})
  • AMD生态较完善,但是使用起来相对复杂
  • 模块JS文件请求频繁,页面效率低下
CMD(Common Module Definition)规范
define(function (require, exports, module) {
  'use strict'
  // 通过require引入依赖
  var $ = require('jquery') 
  // 通过exports或者module.exports对外暴露成员
  module.exports = function () {
    console.log('module2')
    $('body').append('<p>module2</p>')
  }
})
ES Modules规范

浏览器中使用的主流模块化规范,其基本特性为

  • 自动采用严格模式,忽略’use strict’
  • 每个ESM模块都是单独的私有作用域
  • ESM是通过CROS去请求外部JS模块的
  • ESM的script标签会延迟执行脚本

html中的引入方式

<script type="module">
  console.log(this)
</script>
ES Modules 导入导出
  • 基本使用方式
// module.js
const foo = 'es modules'
export {
  foo
}

// app.js
import { foo } from './module.js'
console.log(foo)
  • 重命名导出模块
// module.js
const foo = 'es modules'
export {
  foo as bar
}

// app.js
import { bar } from './module.js'
console.log(bar)
  • 当导出为default命名时
// module.js
const foo = 'es modules'
export {
  foo as default
}

// app.js
import { default as bar } from './module.js'
console.log(bar)
  • 默认导出
// module.js
const foo = 'es modules'
export default foo

// app.js
// 默认导出时可以取除开关键词外的名称
import a from './module.js'
console.log(a)

导入导出的注意事项

  • export导出的不是对象和对象字面量,而是一种固定的写法
  • export default导出的是对象
  • import不是对export的对象解构,而是一种固定用法
  • export导出的不是新的对象或值,而是导出的对象或值的内存引用地址
  • export导出的引用地址是只读地址,是一个常量,在外部不能被修改
ES Modules 导入用法
// 原生的ES Modules不能省略扩展名
// import { name } from './module'
import { name } from './module.js'
import { name } from '/04/module.js'
import { name } from 'http://localhost:3000/04/module.js'

// 原生的ES Modules不能省略index.js
// import { foo } from './utils'
import { foo } from './utils/index.js'

// 原生的ES Modules不能省略根目录路径,否则会被认为是在导入node_modules模块
// import { foo } from 'module.js'

// 只加载执行某个模块,而不提取模块的任何成员
import {} from './module.js'
import './module.js'

// 导入多个成员
import * as mod from './module.js'
console.log(mod)
console.log(mod.foo)

// import不能from一个变量,也不能嵌套在判断里,必须出现在代码最顶层
// const modulePath = './module.js'
// import { name } form modulePath

// if (true) {
//   import { name } from './module.js'
// }

// 动态加载模块的正确方式
import('./module.js').then(module => {
  console.log(module)
})

// 当同时导出了默认成员和具名成员时的导入
export { name. age }
export default 'default export'

import { name, age, default as title } from './module.js'
import title, { name, age } from './module.js'
ES Modules 直接导出对其他模块的引用

此方法可用在index集中导出中,比如components中集中在index中导出

export { Button } form './button.js'
浏览器环境polyfill兼容不支持ES Modules的浏览器

polyfill的实现原理是利用babel实时的解析ES6代码,来到达低版本浏览器支持ES6,但是此方式只适合开发阶段测试,因为太耗费性能,生产环境不适合此方式,生产环境需要走gulp或webpack等打包

<!-- nomodule属性是为了防止支持es6的浏览器执行两次es6代码,nomodule属性只会在不支持es6的浏览器中运行 -->
<script nomodule src="https://unpkg.com/promise-polyfill@8.1.3/dist/polyfill.min.js"></script>
<script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
<script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>
<script type="module">
  import { foo } from './module.js'
  console.log(foo)
</script>
ES Modules 在Node.js中使用

基本使用

  • 文件扩展名需从.js修改成.mjs
  • 或在package.json中设置"type": "module"后依然可以使用.js作为扩展名,但是此时CommonJS规范的文件扩展名需修改为.cjs

module.mjs

export const foo = 'hello'
export const bar = 'modules'

work-in-node.mjs

import { foo, bar } from './module.mjs'
console.log(foo, bar)

import fs from 'fs'
fs.writeFileSync('./foo.txt', 'es module working')

启动node命令的方式

$ node --experimental-modules work-in-node.mjs
// 不支持第三方模块的具名成员导入,第三方的默认成员可以导入,内置模块具名成员可以导入
// import { camelCase } from 'lodash'
import _ from 'lodash'
import { writeFileSync } from 'fs'

也可以使用babel来让ESM运行在CommonJS中

$ yarn add @babel/node @babel/core @bable/preset-env --dev
$ yarn babel-node work-in-node.js --presets-@babel/preset-env

或者在项目中的.babelrc中设置后使用

{
  "presets": ["@babel/preset-env"]
}
$ yarn babel-node work-in-node.js

与CommonJS模块交互

  • ES Modules中可以导入CommonJS模块
  • CommonJS中不能导入ES Modules模块
  • CommonJS始终只会导出一个默认成员
  • 注意import不是解构导出对象

common.js

module.exports = {
  foo: 'commonjs'
}

// exports.foo = 'commonjs'

es-module.mjs

import mod from './common.js'
console.log(mod)

与CommonJS的差异

  • ESM中没有CommonJS模块全局成员
  • require可以用import实现
  • exports可以用export实现
  • 其他方法需要模拟,例如模拟__dirname和__filename
import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
console.log(__filename)
const __dirname = dirname(__filename)
console.log(__dirname)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值