模块化开发

309 阅读8分钟

模块化开发

模块化开发是当下最重要的前端开发范式之一, 模块化 只是一种思想

以下内容依照此目录结构:

1、模块化演变过程
    通过历史,理解模块化存在的价值,以及解决的问题
2、模块化规范
3、常用的模块化打包工具
4、基于模块化工具构建现代web应用
5、打包工具的优化技巧

模块化演变过程

从代码的/文件的划分方式上来看早起的模块化落地方式

  • 文件划分方式

    每个单独的文件目录就是单独的模块,模块没有单独的作用域空间,会污染全局作用域

  • 命名空间方式

    在文件划分基础上,将每个模块包装成全局对象的形式,此方式会减小冲突风险,但没有根本解决

  • IIFE 立即执行函数

    模块的所有对象,放入一个匿名函数中,将需要暴露给外部的对象挂载到全局对象上,实现了私有成员概念,确保了私有成员的安全

    以上三种是早期在没有工具和规范的情况下,对模块化的落地方式,已约定的方式

模块化规范的出现

为什么要出现模块化规范?

首先来看下为什么要出现模块化规范?

  • 不同的开发者有不同的编码习惯
  • 不同项目有不同的设置差异
  • 早期模块化加载都是手动通过script标签外链的形式,时间长了后维护量大增,难度居高

基于以上原因,我们需要模块化标准模块加载器来自动完成这些工作。

常用的模块化打包工具

到现在为止,出现过哪些模块化标准呢?模块标准分析如下:

  • CommonJs规范

    • 一个文件就是一个模块
    • 每个模块都有单独的作用域
    • 通过module.exports导出成员
    • 通过require函数载入模块
    • 以同步模式加载模块(不适合浏览器场景)
  • ADM规范(Asyncchronous Module Definition)

    • 异步模式加载
    • 相关库:require.js
    • 可以在define内部return一些成员,私有空间
    • require函数,自动加载模块,只加载模块,和define区别是define只定义
    • 自动创建script标签,并执行内部代码
    • 绝大数都支持此规范

    AMD规范约定:

    • 每个模块,必须以define函数去定义,具体代码参见 modular-evolution/stage-5
    • 函数接收3个参数,且形参顺序为依赖数组且值为依赖数组导出的成员
    • 参数依次为:模块名称、依赖数组、执行函数

    AMD缺点:

    • 使用起来相对复杂
    • 模块划分细致时,文件请求频繁,导致性能下降
  • sea.js

    可以按不同的先后依赖关系对 JavaScript 等文件的进行加载工作

    现在require兼容了此种方式

    现在已被淘汰

模块化场景

  • node.js

    在node平台对应使用 CommonJs 规范

  • web浏览器

    ES6+ 开始使用 ESM(ECMAScript Module)规范

ESM特性

作为新出的模块化标准规范,ESM 到底约定了哪些语法和特性?

如果通过工具/方案去解决运行环境中兼容性带来的问题?

特性:

  • 自动采用严格模式,忽略'use strict'这种文件头的形式
  • 每个ESM都是运行在单独的私有作用域当中

如两个script标签,内部都有foo,第二个无法获取第一个标签内foo,原因就是块级作用域。如此不用担心全局污染问题

  • ESM是通过cors方式请求外部的js模块的

如要支持,返回标头必须添加支持跨域标头,cors不支持文件的形式请求,必须是在serve的环境下访问

  • ESM的script标签会延迟执行脚本,等同与defer属性
  • 浏览器中script标签添加 type=module 属性,就可以以ESM的标准执行代码
  • ESM由导入导出功能:import、export

ESM文件使用export导出时,如果不指明default,则外部使用时必须明确其导出成员来使用,有相应default时可以直接使用整体来接收,示例:

 // 没有default
export const name = 'foo';
//使用时
import { name } from './module.js';

// 返回default
const name = 'foo1';
export default name; // 或 export default { name };
// 使用时
import ModuleData from './module.js';

注: export 导出default时,后面不可以使用var/let/const修饰符

注意事项:

1export 后边跟着的两个花括弧不是对象字面量,是固定语法

2export default 后边跟着的花括弧是对象字面量,因default后是具体内容

3import后边的花括弧内容,并不是解构,是固定用法

4export导出成员时,导出的是成员的一个引用,基础类型和引用类型返回的都是引用关系,基础数据类型不再是复制了

5、暴露出去的这个引用关系是只读属性,并不可修改(被导出来后变为了常量般的数据)

ESM导入用法

1、原生module中文件结尾不可以省略,路径必须以绝对或相对开头

import foo from './foo.js'

2、如果直接写模块名,则会被认为是第三方模块,或者直接使用cdn地址

import loadsh from 'loadsh'

import jQuery from 'https://XXXXXXXX'

3、import后如果不写任何key,则会只执行该模块,不会提取任何成员信息,可简写为直接import '模块路径'

import './foo.js' // 将只加载并执行该模块,但不会提取成员

4、如果要导入所有成员,则使用*,且必须使用as起一个别名

import * as foo from './foo.js'

5、import不能嵌套,不能动态导入模块,如果要动态导入某个模块,可通过使用import函数,如下:

    import('./module').then(function(module) {
        console.log('module', module);
    })

6、同时导出了命名成员和默认成员,则可以将default重命名即可

import { default as foo, name } from './foo.js'

ESM导出导入成员

  • import导入的结果直接作为export的导出成员
export { foo, name } from './foo.js'

export { default as foo, name } from './foo.js'

基于模块化工具构建现代web应用

ESM-浏览器环境 polyfill

使用此loader工具后,可以转换module代码,但会出现不负需求的地方:在支持module语法的浏览器上代码会执行两遍!what?

会执行两遍的原因是:在支持module语法的浏览器上,当加载了module内的内容后会执行其内容,这是一遍;loader工具会将代码加载并转换后再执行一遍,此为两遍。

那么有没有办法解决这种问题呢?答案是肯定的。

在通过script标签引入module时,标签加上 nomodule 属性,会告知浏览器,在支持module环境下不去加载polyfill文件,解决执行两遍问题~

但是此种polyfill方法适合开发者个人在本地测试使用,不建议在线上环境进行部署。如果要部署线上,还是建议将代码进行编译、打包后再整体部署。

ESM in Node.js

  • node环境内使用时文件扩展名需修改为mjs
  • 命令添加实验标识参数,跟上文件路径和名称,如:
node --experimental-modules index.mjs
  • Node内置的模块兼容了ESM的提取成员方式

    所有成员会单独导出一次,最后再整体导出一次

ESM 与 CommonJs 交互

//commonjs模块
module.exports = {
    foo: 'commonjs exports value'
}

// es module模块
import mod from './commonjs.js';
console.log(mod);

验证可行,但注意CommonJs中导出的只有默认成员,不可以提取成员

反之,在CommonJs中不能通过require载入模块,原生环境不支持

ESM与CommonJs差异

在CommonJs中不能使用 require、module、exports、__filename、__dirname

原因是这5个函数是commonjs导出的全局函数。虽然不可直接使用。但可通过方法将函数进行模拟,如下:

  • __filename
// 通过内置模块url的fileURLToPath方法获取到文件路径,commonjs是__filename
import { fileURLToPath } from 'url';
const currentMeta = import.meta.url;
const path = fileURLToPath(currentMeta);
console.log(path)
  • __dirname
// 可通过dirname获取当前模块的目录地址,commonjs是__dirname
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const currentMeta = import.meta.url;
const path = fileURLToPath(currentMeta);
const filename = dirname(path);
console.log(path)
console.log(filename)

ESM-node新版本进一步支持

node 在 12.10 版本下,在 package.json 里,"type" 设置为 "module" 的话,项目里的所有 js 后缀就不需要改写成 mjs,但这种情况下,CommonJS 的模块后缀就要改成 cjs

打包工具的优化技巧

ESM-babel兼容方案

使用babel时,需要安装相应的依赖(babel不会直接转移我们的代码,babel的是以插件的形式去实现相应的功能,转换一个特性需要安装一个相应的插件来实现,preset为特性插件集合):

yarn add @babel/node @babel/core @babel/preset-env --dev

安装完成后修改babel配置

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

如果不想使用这个集合。就需要自己一个个特性进行插件安装来转换

总结

模块化开发阶段细碎知识点十分多,也和实际工作中的细节点有很多体现,听和看只是知其然,工作中不断地用、不断遇到问题并解决后再结合理论,才会知其所以然