模块化开发

模块化概述

模块化只是一个思想,并不包含具体的实现

模块化的演变过程

stage1:文件划分方式

最原始的文件划分,具体的做法就是将每个功能以及相关的一些状态数据单独存放到不同的文件当中,约定每一个文件就是一个独立的模块,然后将模块引入到页面当中,一个srcipt标签对应一个模块,在代码当中直接调用模块当中的全局成员,成员有可能是变量也有可能是函数,缺点:1. 污染全局作用域,2.命令冲突问题,3.无法管理模块之间的依赖关系。完全依靠约定。
index.html
module-a.js
module-b.js

stage2:命名空间方式

约定每一个模块只暴露出一个全局的对象,所有的模块成员都挂载到这个全局对象上面。具体的做法:在第一个阶段及基础之上,通过将每一个模块包裹成一个全局对象的方式去实现,有点类似为模块内成员添加命名空间的感觉。优点:减小命名冲突的可能。缺点:任然没有私有空间,木块成员仍然可以在外部被访问,被修改,模块之间的依赖关系也没有得到解决
index.html
module-a.js
module-b.js

stage3:IIFE(立即执行函数)

具体做法:将模块成员放在一个函数提供的私有作用域当中,对于需要暴露给外部的成员,可以通过怪载到全局对象上的方式去实现。优点:实现了私有成员的概念,也就是说在模块内部的成员只能通过闭包的方式去访问,而在外部是没有办法去使用的,确保了私有变量的安全
index.html
module-a.js
module-b.js

stage4:IIFE参数作为依赖声明

具体做法就是在第三阶段的基础上,利用立即执行函数的参数传递模块依赖项。这使得每一个模块之间的关系变得更加明显。
module-a.js
以上的几个阶段是早期在没有工具和规范的情况下,通过约定的方式对模块化的实现.。

模块化规范的出现
  • CommonJS规范
    nodejs当中提出的一套标准,nodejs内置的模块系统
    • 一个文件就是一个模块
    • 每个模块都有单独的作用域
    • 通过module.exports导出成员
    • 通过require函数载入模块
      CommonJS是以同步模式加载模块,因为node的执行机制是在启动时加载模块,在执行过程当中是不需要去加载的,只会去使用到模块。但是换到浏览器端,会导致效率低下,因为每一次页面加载都会有大量的同步请求出现。所以在早期结合浏览器的特点专门为浏览器设置了规范:AMD异步模块定义规范(同期推出require.js模块加载器,实现了AMD规范)。
    AMD
    在AMD规范当中,约定每一个模块都必须要通过define这个函数去定义,define函数有三个参数,第一个参数:模块名,第二个参数:数组(用来声明模块的依赖项),第三个参数:函数,函数的参数与前面的依赖项一一对应,每一项分别为依赖项模块导出的成员,这个函数的作用是为当前的模块提供一个私有的空间,在模块中需要像外部导出成员可以通过return的方式去实现。
    require.js中提供了一个require函数,去用来加载模块,和define不同的是,require只是用来加载模块,define是用来定义模块的。requirejs需要加载一个模块时,内部会自动的创建一个script标签去发送对应脚本文件的请求,并且执行相应的模块代码
    目前绝大多数第三方库都支持AMD规范,生态较好,但使用起来相对复杂,模块js文件请求频繁
    Sea.js+CMD
    淘宝提供的一个模块化规范,类似CommonJS,使用上和requirejs差不多。
    index.html
    AMD requirejs
    CMD
模块化标准规范

模块化标准规范

ES Modules

基本特性
  • 自动采用严格模式,忽略’use strict’
  • 每个ESM模块都是单独的私有作用域
  • ESM是通过CORS去请求外部JS模块的。也就意味着请求的JS模块如果不在同源地址下面的话,那就需求请求的服务端地址响应的响应头当中必须要提供有效的CORS的标头。
  • ESM的script标签会延迟执行脚本,相当于加了defer
    es modules基本特性
导入和导出

导出

var name = 'foo module'
function hello () {
  console.log('hello')
}
class Person {}
// export { name, hello, Person }
// 导出重命名
export {
  // name as default,
  hello as fooHello
}

导入

// 导入重命名
// import { default as fooName } from './module.js'
// console.log(fooName)
import { name, hello, Person } from './module.js'
console.log(name, hello, Person)

注:
1. ES Module 中 { } 是固定语法,就是直接提取模块导出成员并不是对象字面量。如果需要导出对象字面量可以使用export default,但在CommonJS 中是先将模块整体导入为一个对象,然后从对象中结构出需要的成员
2. 导入的成员并不是一个复制的副本而是直接导入模块成员的引用地址,也就是说import得到的变量与export导入的变量在内存中是同一块空间,一旦模块中成员修改了,这里也会同时修改
3. 导入的模块成员变量是只读的,但是需要注意如果导入的是一个对象,对象的属性读写不受影响

导入用法
  1. import在导入模块时,from后面是导入路径,必须是完整的文件名称,不能省略.js扩展名,这个和CommonJS有区别的。还有在CommonJS当中可以通过载入目录的方式去载入目录下的index.js文件,但在ES Module中不能省略index.js路径,必要要写全。对于文件路径名称,在后期使用打包工具去打包模块时,可以省略扩展名和省略index.js这种默认文件的操作
  2. 在使用相对路径时,“./”在网页开发中是可以省略掉的,但在import的时候不可以省略,因为省略掉./,import会认为是在加载第三方模块,与CommonJS相同
  3. 可以使用完整的url加载模块
    import { name } from 'http://localhost:3000/04-import/module.js'
    
  4. 如果只是要执行某个模块而并不需要提取这个模块中的成员,可以保持import后面的{}为空
    import {} from './module.js'
    import './module.js' // 简写的语法
    
  5. 可以使用*把这个模块中所有的成员全部提取出来,通过as的方式将提取出来的成员全部放到一个对象当中,那么提取出来的每一个成员都会作为这个对象的属性出现
    import * as mod from './module.js'
    console.log(mod)
    
  6. import关键词只能出现在最外侧作用域,不可以被嵌套在if或函数当中,且导入模块的路径需要在开发阶段明确,不支持动态改变。
  7. 遇到上面的情况,可以使用Es module中提供的全局的import函数去动态导入,返回一个promise
import('./module.js').then(function (module) {
  console.log(module)
})

8.如果想要同时导入命名成员和默认成员,有两种方法

import { name, age, default as title } from './module.js'
import abc, { name, age } from './module.js'
导出导入成员

将导入的结果直接作为当前模块的导出成员

export { foo, bar } from './module.js'

这样所有的导入成员将直接作为当前模块的导出成员,在当前作用域当中就不再可以访问这些成员了。一般可以在index中使用,可以将某个目录下散落的模块组织到一起导出。

浏览器环境 Polyfill兼容方案

在使用ES Module的时候需要考虑浏览器的兼容问题, 可以借助一些编译工具在开发阶段将es6的代码编译成es5的方式。
Polyfill可以在浏览器当中直接去支持ES Module当中绝大多数的特性。
es-module-loader的工作原理:
将浏览器当中不识别的es module去交给babel转换,然后对于需要import进来的文件,在通过ajax的方式去请求,把请求回来的代码再去通过babel转换,从而去支持es module。
但在支持es module的浏览器中代码会被执行两次,这个可以借助于script标签的新属性nomodule,添加了这个属性,浏览器就只会在不支持es module的浏览器当中工作
只适合在开发阶段使用,不适合在生产阶段使用,因为他都是在运行阶段动态解析脚本,效率低,在生产阶段还是要预先编译的。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>ES Module 浏览器环境 Polyfill</title>
</head>
<body>
  <!-- 转换promise -->
  <script nomodule src="https://unpkg.com/promise-polyfill@8.1.3/dist/polyfill.min.js"></script>
  <!-- babel即时运行的浏览器版本 -->
  <script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
  <!-- es-module-loader 通过es module把代码读出来,然后交给babel去转换,从而让代码正常工作 -->
  <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>
</body>
</html>

在nodejs中使用

在node8.5版本过后,内部支持es module,仅是实验特性

  1. 将文件扩展名从.js修改为.mjs
  2. 启动node的时候需要加上–experimental-modules 文件名node --experimental-modules index.mjs这个参数代表启用一个es module的实验特性
// 第一,将文件的扩展名由 .js 改为 .mjs;
// 第二,启动时需要额外添加 `--experimental-modules` 参数;
import { foo, bar } from './module.mjs'
console.log(foo, bar)
// 此时我们也可以通过 esm 加载内置模块了
import fs from 'fs'
fs.writeFileSync('./foo.txt', 'es module working')
// 也可以直接提取模块内的成员,内置模块兼容了 ESM 的提取成员方式
import { writeFileSync } from 'fs'
writeFileSync('./bar.txt', 'es module working')
// 对于第三方的 NPM 模块也可以通过 esm 加载
import _ from 'lodash'
_.camelCase('ES Module')
// 不支持,因为第三方模块都是导出默认成员
// import { camelCase } from 'lodash'
// console.log(camelCase('ES Module'))
与CommonJS交互
  1. ES Modules中可以导入CommonJS模块
  2. CommonJs中不能导入ES Modules模块
  3. CommonJs始终只会导出一个默认成员
  4. 注意impot不是结构导出对象,他只是一个固定的用法,去提取模块当中的命名成员、
    commonjs
    es-modules
在nodejs中使用esm和CommonJs的差异
// 加载模块函数
console.log(require)

// 模块对象
console.log(module)

// 导出对象别名
console.log(exports)

// 当前文件的绝对路径
console.log(__filename)

// 当前文件所在目录
console.log(__dirname)

ESM中没有CommonJS中的那些模块全局成员了。原因是:这五个成员实际上是CommonJS把模块包装成一个函数过后通过参数提供进来的成员,那我们现在使用的是esm,他的加载方式出现了变化,所以不再提供这几个成员了。
require, module, exports 通过 import 和 export 代替
__filename 和 __dirname 通过 import 对象的 meta 属性获取const currentUrl = import.meta.url // 当前工作的文件url地址

// 通过 url 模块的 fileURLToPath 方法转换为路径
import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
console.log(__filename)
console.log(__dirname)
在nodejs新版本中对esm的支持

在node 12.10.0之后,可以在package.json中将type的值设置为module之后,在这个项目下所有的文件默认就会以esm的方式工作了,扩展名就可以从mjs变为js了
package.json
common.cjs

Babel兼容方案
  1. 安装npm i @babel/node @babel/core @babel/preset-env -D
  2. 在package.json中的scripts注册一下babel-node
  3. 然后npm run babel-node index.js --presets=@babel/preset-env
    不能直接运行npm run babel-node index.js,因为babel是基于插件机制去实现的,他的核心模块并不会去转换代码,具体要转换代码是通过插件实现的,也就是说我们需要一个插件去转换代码中的一个特性。preset-env是插件的集合,包含了最新的js标准当中所有的新特性。
    如果不想每一次手动传入–presets=@babel/preset-env,可以将它方式配置文件当中。在项目下边添加一个.babelrc的文件,这是一个json格式的文件
    .babelrc
    {
      "presets": ["@babel/preset-env"]
    }
    
    这样就可以直接通过npm run babel-node index.js去运行,不用添加参数了。
    也可以单独使用一个插件去转换esm
    npm i @babel/plugin-transform-modules-commonjs -D
    
    然后修改配置文件.babelrc
    {
      "plugins": [
        "@babel/plugin-transform-modules-commonjs"
      ]
    }
    

以上就是模块化开发的全部内容啦~ 之前对于esm和CommonJS概念还是比较模糊的,通过这次学习清晰了不少。继续加油吧~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值