项目说明
模块化开发,是当下最重要的前端开发范式之一。模块化只是一个思想、一个理论。
笔记来源:拉勾教育 大前端高薪训练营
阅读建议:内容较多,建议通过左侧导航栏进行阅读
模块化过程
早期模块化完全依靠约定。
文件划分方式
缺点:
- 1,污染全局作用域;
- 2,命名冲突问题;
- 3,无法管理模块依赖关系。
命名空间方式
命名空间方式,将所需变量包裹进全局对象内。
IIFE
IIFE,自执行函数,将变量设为私有成员。
模块化规范
模块化规范,是指 模块化标准 + 模块加载器。
CommonJs 规范
CommonJs 是针对 NodeJs 的规范,是以同步模式加载模块,约定如下:
- 1,一个文件就是一个模块;
- 2,每个模块都有单独的作用域;
- 3,通过 module.exports 导出成员;
- 4,通过 require 函数载入模块。
AMD + Require.js
AMD(Asynchronous Module Definition),异步的模块定义规范,是针对浏览器的规范,它是通过 Require.js 进行实现的,Require.js 是一个非常强大的模块加载器。
缺点
- 1,AMD 使用起来相对复杂;
- 2,模块 JS 文件请求频繁
Require.js API
define()
每一个模块都要通过 define() 函数进行定义。
传递三个参数
-
第一个参数,是一个字符串,表示模块名,方便后期通过模块名进行使用;
-
第二个参数,是一个数组,用来声明这个模块的一些依赖项,此参数按需添加,可有可无;
-
第三个参数,是一个函数,函数的参数与前面数组中的依赖项一一对应,每一项分别为依赖项导出的成员,用来为当前模块提供私有空间,通过 return 向外部导出成员
语法示例如下:
define('module_name', [], function() { return ... })
require()
require() 函数,用来载入一个模块,内部会自动创建script标签,执行相应的代码。
传入两个参数
-
第一个参数,是一个数组,表示所引入模块的组合
-
第二个参数,是一个函数,函数的参数与前面数组中的模块一一对应,可以使用
模块名.xx
的方式访问模块中导出的成员语法示例如下:
require(['module_name', ...], function(module_name){ // module_name.xx })
CMD + Sea.js
CMD(Common Module Definition),通用的模块定义规范,类似 CommonJs 规范。Sea.js 是由淘宝推出的库,遵循CMD规范。
ES Modules
ES Modules 规范 是在 ECMAScript2015(ES6)中提出的 Module 模块标准。通过给 script 添加 type = module 的属性,就可以以 ES Module
的标准执行其中的 JS 代码。
基本特性
-
1,ESM 自动采用严格模式,忽略 ‘use strict’
<script type="module"> console.log(this); // undefined this在严格模式下,为 undefined </script>
-
2,每个 ES Module 都是运行在单独的私有作用域中
<script type="module"> var foo = 100 // 不会造成全局作用域污染问题 console.log(foo) // 100 </script> <script type="module"> console.log(foo) // Uncaught ReferenceError: foo is not defined </script>
-
3,ESM 是通过 CORS 的跨域请求方式请求外部 JS 模块的
<script type="module" src="https://umpkg.com/jquery@3.4.1/dist/jquery.min.js"> // 需要服务端支持 CORS ,否则会出现跨域问题 </script>
-
4,ESM 的 script 标签会延迟执行脚本,相当于 script 的 defer 属性
<script type="module" src="./demo.js"> // 等待网页的渲染过后,再去执行脚本,不会阻碍页面的显示 </script> <p>需要显示的内容</p>
导入和导出
导出 (export)
export 是在模块内去对外暴露接口。
导出语法
-
1,直接成员导出,变量、函数、类等都可以导出。
导出方式如下:
export var name = 'foo module' // 导出变量 export function hello () { } // 导出函数 export class Person { } // 导出类
-
2,成员集中导出,可以更加直观的看到向外部导出了哪些成员
导出方式如下:
var name = 'foo module' function hello () { } class Person { } export { name, hello, Person }
-
3,别名导出,使用 as 进行重命名
导出方式如下:
var name = 'foo module' function hello () { } class Person { } export { name as fooName, hello as fooHello, Person as fooPerson }
-
4,默认导出,设置某一个成员的别名是 default
导出方式如下:
var name = 'foo module' export { name as default } // <==> 推荐下方书写方式 export default name
导入 (import)
import 是在模块内导入其他模块所提供的接口。
导入语法
对应上面的导出语法。
-
1,对应 直接成员导出 和 成员集中导出
导入方式如下:
// .js 不可以省略,完整路径 import { name, hello, Person } from 'module_path'
-
2,对应 别名导出
导入方式如下(app.js):
import { fooName, fooHello, fooPerson } from 'module_path'
-
3,对应 默认导出
导入方式如下(app.js):
import { default as name } from 'module_path' // default 是关键字 // 简写为 import name from 'module_path'
-
4,导入模块时,模块路径的三种写法
模块路径如下(app.js):
// ./ 不可以省略,相对路径 import { name } from './module.js' // / 绝对路径 import { name } from '/04-import/module.js' // 完整 url 路径 import { name } from 'htto://localhost:3000//04-import/module.js'
-
5,加载模块,但不提取模块内的成员,一般用于导入一些不需要外部控制的子功能模块
导入示例如下(app.js):
import {} from './module.js' // 简写为 import './module.js'
-
6,提取模块导出的所有成员, 使用 as 将所有成员作为一个对象的属性
导入示例如下(app.js):
import * as mod from './module.js' console.log(mod.name, mod.age);
-
7,动态导入模块, 返回一个 Promise对象
导入示例如下(app.js):
import('./module.js').then(function (module) { console.log(module); })
-
8,导出时,同时导出命名成员和默认成员,如何导入
导入示例如下(app.js):
import { name, age, default as d } from './module.js' // 简写为, 默认成员的名字可以随意命名 import d, { name, age } from './module.js'
导出导入成员
当前 module.js 模块的导出成员,将直接作为 app.js 模块的导出成员使用,一般用于集中导出分散的子模块成员
-
1,index.js ,集中导出成员文件
export { Button } from './button.js' // 默认成员的导出,必须重命名,以别名的形式导出 export { default as Avatar} from './avatar.js'
-
2,button.js ,分散子模块,导出 Button 成员
export var Button = 'Button Component'
-
3,avatar.js ,分散子模块,导出 Avatar 默认成员
var Avatar = 'Avatar Component' export default Avatar
注意事项
-
1,export 单独使用时,
{}
是固定语法,导出的不是对象字面量代码示例如下:
var name = 'jack' var age = 18 export { name, age }
导入模块,
{}
固定语法,不是对象的解构代码示例如下:
import { name, age } from './module.js'
-
2,export default 组合使用时,
{}
代表导出的是对象字面量代码示例如下:
var name = 'jack' var age = 18 export default { name, age }
导入模块,不可以使用 {} 写法
代码示例如下:
// module_obj 自定义名字,最好和模块名保持一致 import module_obj from './module.js' // 访问导出的成员 console.log(module_obj.name, module_obj.age)
-
3,export 导出的是值的内存地址
代码示例如下(module.js):
var name = 'jack' var age = 18 export { name, age } // 导出的是值的内存地址 setTimeout(function () { name = 'ben' }, 1000)
代码示例如下(app.js):
import { name, age } from './module.js' console.log(name, age); // jack 18 setTimeout(function () { console.log(name, age); // ben 18 }, 1500)
-
4,导出的值是只读的,无法在模块外部修改成员
代码示例如下(app.js):
import { name, age } from './module.js' name = 'tom' // Uncaught TypeError: Assignment to constant variable.
Polyfill
解决浏览器的兼容性问题。
-
在 HTML 页面,手动引入
browser-es-module-loader
js 文件查找地址,如下:browser-es-module-loader
代码示例如下(app.js):
<!-- 有的IE版本不支持 Promise,因此需要引入 Promise Polyfill --> <script nomodule src="https://unpkg.com/promise-polyfill@8.2.0/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 Modules Loader, 读取代码,将不识别的特性交给 babel 进行转换 --> <script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>
nomodule 属性,表示只在不支持 ES Modules 的浏览器中运行,避免支持的浏览器多次运行 。
不建议在生产版本中使用,影响效率。
in Node.js
-
测试 ES Modules 在 node.js 环境的运行情况
文件后缀名,设置为 xxx.mjs
添加参数 – experimental-modules , 启动 ES Modules 的实验特性
$ node --experimental-modules xxx.mjs # node 8.5+
1)内置模块兼容了 ESM 的提取成员方式
引入方式如下(index.mjs):
// 方式一 import fs from 'fs' fs.writeFileSync('./foo.txt', 'es module working') // 代码执行成功 // 方式二 import { writeFileSync } from 'fs' writeFileSync('./bar.txt', 'es module working~') // 代码执行成功
2)第三方模块都是导出默认成员,不支持使用 {} 语法导入成员
引入方式如下(index.mjs):
import _ from 'lodash' console.log(_.camelCase('ES Module')); // 代码执行成功 // import { camelCase } from 'lodash' // console.log(camelCase('ES Module')); // SyntaxError:...
-
ES Modules 与 CommonJS 交互
1)ES Modules 中可以导入 CommonJS 模块
代码示例如下(commonJs.js):
module.exports = { foo: 'commonjs' } // exports 是 module.exports 的别名,二者是等价的 exports.foo = 'commonjs'
代码示例如下(es-modules.mjs):
import mod from './common.js' console.log(mod);
2)CommonJS 中不能导入 ES Modules 模块
代码示例如下(commonJs.js):
const mod = require('./es-module.mjs') console.log(mod); // Error [ERR_REQUIRE_ESM]: Must use import to load ES Module
代码示例如下(es-modules.mjs):
export const foo = 'es module export value'
3)CommonJS 始终只会导出一个默认成员,不能直接提取成员
代码示例如下(commonJs.js):
exports.foo = 'commonjs'
代码示例如下(es-modules.mjs):
import { foo } from './common.js' // SyntaxError:... console.log(foo);
4)注意 import 不是解构导出对象
-
ES Modules 与 CommonJs 的差异
ES Modules 中没有 CommonJs 中的那些模块全局成员了,如:
require(加载模块函数);
module(模块对象);
exports(导出对象别名);
__filename(当前文件的绝对路径);
__dirname(当前文件所在目录) -
node.js 新版本进一步支持
为了使项目中所有的js文件,都可以使用 ES Modules ,在 package.json 中添加属性 type 进行设置。
代码示例如下(package.json):
{ type: 'module' }
此时,无需再将 .js 改为 .mjs。但是要将 CommonJS 的 .js 改为 .cjs,保证兼容 CommonJS。
-
低版本 Node.js,使用 Babel 进行兼容
1)安装 babel 相关依赖模块
$ yarn add @babel/node @babel/core @babel/preset-env --dev
2)运行 ES Modules 的 JS 文件,需要添加特性转换的预设参数
$ yarn babel-node index.js --presets=@babel/preset-env
3)@babel/preset-env 只是一个插件集合,真正起作用的是
@babel/plugin-transform-modules-commonjs
插件$ yarn add @babel/plugin-transform-modules-commonjs --dev
4)若不想在执行命令时添加参数,可以配置 .babelrc 文件,这是 babel 的配置文件
代码示例如下(.babelrc):
{ "presets": ["@babel/preset-env"], "plugins": ["@babel/plugin-transform-modules-commonjs"] }
5)运行命令,进行测试
$ yarn babel-node index.js
总结
模块化的最佳实践:NodeJs 环境遵循 CommonJs,浏览器环境遵循 ES Modules 规范。