模块化开发

277 阅读5分钟

模块化规范的出现

以往的开发方式用script标签手动去引入每个用到的模块,这使得维护起来特别不方便。于是后来模块化开发有了很大的发展,例如CommonJS和ES Modules

模块化规范之CommonJS

CommonJS是nodejs推出的一套模块化标准,nodejs中所有的模块化代码都需要遵循CommonJS规范

CommonJS特性:
  1. 一个文件就是一个模块
  2. 每个模块都有单独的作用域
  3. 通过module.exports导出成员
  4. 通过require函数载入模块

在浏览器端如果也使用这个规范的化会出现一些问题:

  • node的加载机制:CommonJS是以同步模式加载模块,因为node的执行机制是在启动时加载模块,执行过程当中是不需要加载的,只会使到用模块

  • 所以CommonJS在node端不会有问题,但是如果换到浏览器端使用CommonJS规范,必然导致效率低下,因为每一次页面加载都会导致大量的同步模式请求出现,所以在早期的模块化当中并没有选择Commonjs这个规范,而是专门为浏览器端结合浏览器的特点重新设计了一个规范,叫做AMD(asynchronous Module Definition),还推出了一个非常有名的库,叫Require.js,它实现了AMD这个规范,本身也是一个强大的模块加载器,但是也有缺点:

    • AMD使用起来相对复杂
    • 模块js文件请求频繁
    • 同期出现的还有淘宝推出的Sea.js+CMD
  • 前端模块化目前统一成了CommonJS和ES Modules这两个规范了

模块化规范之ES Modules

ES Modules是es6定义的一个最新的模块系统,刚开始推出的时候几乎所有的主流浏览器都是不支持此特性的。随着webpack等打包工具的流行,这一规范才逐渐开始普及。截至到目前,ES Modules已经是最主流的前端模块化方案了。想比于AMD, ES Modules在语言层面实现了模块化,并且逐渐得到各大浏览器的支持

ESM基本特性
  • 自动采用严格模式(严格模式下this是undefined,非严格模式下this指向window)
  • 每个ESM模块都是单独的私有作用域
  • ESM是通过CORS去请求外部JS模块的
  • ESM的script标签会延迟加载脚本(相当于加了defer属性)
ESM导出功能用法
  • 用法一:export后跟导出的内容(可以导出多种类型)

    // module.js
    // 导出变量
    export var name = 'foo module'// 导出函数
    export function hello () {
        console.log('hello')
    }
    ​
    // 导出类
    export class Person {
    ​
    }
    ​
    ​
    // 对应引入
    import { name, hello, Person } from './module.js'
  • 用法二:export单独使用(这种方式更常用)

    // module.js
    var name = "foo module"
    function hello () {
        console.log('Hello')
    }
    class Person {
        
    }
    ​
    export { name, hello, Person}
    ​
    ​
    // 也可以对导出内容重命名
    export {
        name as fooName,
        hello as fooHello,
        Person
    }
    ​
    ​
    // 对应导入
    import { name, hello, Person} from './modules.js'
    
  • export默认导出

    // module.js
    export default name
    ​
    ​
    // 对应导入方式(引入的名称可自定义)
    import suibian from './module.js'
    
ESM导入导出的注意事项:
  1. export { name, hello } 导出的不是对象字面量,是固定用法
  2. import { name, hello } 语法跟es6的解构很像,但不是解构,是固定用法
  3. export导出的并不是里面的值,而是引用(并不是重新拷贝了一份),因此外部导入的成员会受模块内部的一些修改的影响
  4. import导入的成员是只读的
ES Modules导入用法
  1. 导入模块时,from后面跟的是导入模块的路径,必须使用完整的文件名称,不可以省略扩展名;关于index.js也不可以省略(这两点跟commonjs是不同的,后期在使用模块化打包的时候可以省略了)。'./'也不可以省略,这点跟commonjs是相同的

  2. 如果只想执行一个模块,不想提取模块中的成员的话,写法如下:

    import {} from './module.js'// 或者
    import './module.js'
    
  3. 如果提取的成员特别多,可以使用*方式

    import * as mod from './module.js'
    console.log(mod)
    
  4. import 需要放在页面的最顶层

  5. 动态引入

    // 一些代码执行require('./module.js').then(res => {
        console.log(res)
    })
    
  6. 同时导入default和其他成员

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

一个页面如果想使用多个组件,需要单独引入多个组件写很多行

比较好的做法:在components文件夹新建index.js

export { Button } from './components/button.js'
export { Avator } from './compoennts/avator.js'
ESM 浏览器环境 Polyfill

截止到目前为止,iE和一些国产浏览器还没有支持ESM

有一款Polyfill可以在浏览器中直接去支持ES Module中绝大多数的特性,模块名字叫browser-es-module-loader,具体用法:

  • 针对于npm模块,我们可以通过unpkg.com这个网站提供的cdn服务去拿到它下面所有的js文件 ,这时在js引入两个文件:

  • 引入后会遇到一个新的问题,发现IE不支持Promise这个ES6的特性,因此还需要单独为iE引入一个Promise的Polyfill

    • 地址栏输入unpkg.com/promise-polyfill回车
    • 拿到路径,然后通过script标签引入
  • 如果在支持ESM的浏览器当中也引入以上Polyfill,就会被执行两次,解决方法:

ES Modules in Nodejs 支持情况

node8.5版本以后可以使用ESM特性

  • node --experimental-modules index.mjs(注:目前还在试验阶段)
ES Modules in Nodejs 与 Commonjs交互
  • ES Modules中可以导入Common JS模块
  • CommonJS中不能导入ES Modules模块
  • CommonJS始终只会导出一个默认成员
  • 注意import 不是解构导出对象
ES Modules in Node.js - Babel 兼容方案

对于早期的node版本(如8.0.0)可以使用babel进行兼容

  • yarn add @babel/node --dev,安装完可以在node_modules文件夹下的.bin文件夹下找到一个babel-node 的命令,此时可以通过yarn babel-node index.js运行

  • @babel/node需要依赖babel的核心模块,还有一些babel的预设插件

    • yarn add @babel/core @babel/preset-env --dev
  • 直接运行yarn babel-node index.js会报错,因为需要指定插件,之前装的preset-env是插件的集合,可以利用它完成编译过程

    • 方式一:yarn babel-node index.js --presets=@babel/preset-env

    • 方式二:项目根目录下添加一个.babelrc的文件,此文件是一个json格式的文件,添加以下内容,可以运行命令时不用每次添加--presets=@babel/preset-env

      {
          "presets": ["@babel/preset-env"]
      }