模块化开发
模块化开发是当下最重要的前端开发范式之一, 模块化 只是一种思想
以下内容依照此目录结构:
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修饰符
注意事项:
1、export 后边跟着的两个花括弧不是对象字面量,是固定语法
2、export default 后边跟着的花括弧是对象字面量,因default后是具体内容
3、import后边的花括弧内容,并不是解构,是固定用法
4、export导出成员时,导出的是成员的一个引用,基础类型和引用类型返回的都是引用关系,基础数据类型不再是复制了
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
- 2014才被提出,早期浏览器不支持,需要使用polyfill兼容,使用 browser-es-module-loader
使用此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"]
}
如果不想使用这个集合。就需要自己一个个特性进行插件安装来转换
总结
模块化开发阶段细碎知识点十分多,也和实际工作中的细节点有很多体现,听和看只是知其然,工作中不断地用、不断遇到问题并解决后再结合理论,才会知其所以然