模块化开发

166 阅读4分钟

模块化的演进过程

  1. 文件划分:一个文件就是一个模块,通过script标签引入。但是可以在外部直接访问到文件中的变量,没有一个独立的私有空间,导致外部可以直接修改模块内部成员,污染全局作用域。模块多了之后,带来命名冲突问题,也无法管理模块依赖关系
  2. 命名空间方式:约定每个模块暴露一个全局对象,所有的模块成员都挂载到这个全局对象下。
var module1 = {
    name: 'module1',
    method1: function(){
        alert(11)
    },
    method2: function(){
        alert(22)
    },
}

这样可以减少命名冲突的问题,但是仍然没有私有空间,外部仍可以访问并修改模块内部成员,模块间的依赖关系也没有得到解决。

  1. IIFE:通过立即执行函数为模块提供私有空间。
;(function(){
    var name = 'module2';
    function method1(){
        alert(33)
    };
    function method2(){
        alert(44)
    };
    window.module2 = {
        method1: method1,
        method2: method2,
    }
})()

这样在外部只能访问到暴露出来的method1method2name等私有成员不能访问。

模块化规范的出现

以上都是通过约定的方式来实现模块化的代码组织,在不同的开发者实施时会有细微的差别。为了统一不用的开发者和代码之间的差异,需要一系列的标准去规范。

  1. CommonJS 规范:在nodejs中提出的规范,以同步模式加载模块

    • 一个文件就是一个模块
    • 每个模块都有单独的作用域
    • 通过module.exports导出成员
    • 通过require函数载入模块
  2. ES Modules 规范:在浏览器环境规范

    ECMAScript 2015(ES6)

ES Modules 特性

目前浏览器已大部分支持该特性。通过给script标签添加type=module的属性,就可以以ES Modules的标准执行其中的JS代码。

<script type='module'>
    alert('this is module')
</script>

特性:

  • 自动采用严格模式,忽略'use strict'

    例如,打印this,在module属性下,this为undefined,否则this指向全局对象。

  • 每个ESM模块都是运行在单独的私有作用域中

    <script type='module'>
        var foo = 'foo';
        console.log(foo); // foo
    </script>
    <script type='module'>
        console.log(foo); // error: foo is not defined
    </script>
  • EMS通过CORS的方式请求外部JS模块

    如果请求的src地址服务端不支持CORS,则会提示跨域的错误。

  • EMS的script标签会自动延迟执行脚本

    会等待网页的执行渲染完成之后,再执行脚本。类似在script标签上添加来defer属性。

ES Modules 的导入和导出

// module.js
var foo = 'es module'
export { foo };

// app.js
import { foo } from './app.js';
console.log(foo); // es module

可以导出一个对象,里面是要暴露出的成员,可以通过as对导出的变量重命名:

// module.js
export {
    foo as fooname,
    fn as foofn,
}

// app.js
import { fooname, foofn } from './module.js'

可以默认导出一个成员,导入时可以直接引入并修改变量名:

// module.js
export default name;

//app.js
import name from './module.js';
// or
import fooname from './module.js';

注意:export {}import {}是固定语法,并不是导出的一个对象或对象的解构;导入的变量是对导出变量的址引用,模块中值的改变会影响引入模块的改变;导入模块不能对导出变量的重新赋值。

ES Modules 导入导出

直接在文件中将导入的成员导出: export { foo } from './modules.js' 使用场景:需要将所有的模块整合到一个模块中统一导出时。 比如:components 下的ButtonAvatar 模块

// index.js
import { Button } from './Button.js';
import { Avatar } from './Avatar.js';

export { Button, Avatar };

// 直接导出方式,在当前模块是无法使用的。此时index文件是作为一个整合桥梁。
export { Button } from './Button.js';
export { Avatar } from './Avatar.js';

如果直接导出的成员是默认成员,则需要对default重命名

export { default as Button } from './Button.js';

ES Modules in Browers 中 polyfill兼容方案

浏览器如果不识别ES6,可以通过babel将代码转为浏览器可识别的ES5代码。

ES Modules in NodeJS

与CommonJS 模块交互

  • 不能在CommonJS模块中通过require载入ES Modules
  • ES Modules模块中可以导入CommonJS模块
  • CommonJS始终只会导出一个默认成员,import不是解构导出对象
modules.exports = {
    name: '张三',
}
// or
exports.name = '李四'

与CommonJS 模块的差异

// commonjs
console.log(require);
console.log(module);
consoloe.log(__filename);
console.log(__dirname);

// esm 中没有CommonJS中的那些模块全局成员
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url)
consoloe.log(__filename);

const __dirname = dirname(__filename)
console.log(__dirname);

模块文件名,新版本node支持ESM

module.cjs为commonJS,module.mjs为ESM

Babel兼容

使用Babel的预设新特性可以帮助在低版本的node环境下实现ESM兼容

  1. 安装依赖

yarn add @babel/node @babel/core @babel/preset-env preset-env是插件的集合

  1. 运行

yarn babel-node index.js --presets=@babel/preset-env

  1. 配置文件.babelrc
{
    "presets": ["@babel/preset-env"]
}

运行:yarn babel-node index.js

  1. 使用插件完成转换

yarn add @babel/plugin-transform-modules-commonjs

  1. 配置文件.babelrc
{
    "plugins":  "@babel/plugin-transform-modules-commonjs"
}