前言
前端的模块化开发经过长时间的演变现在已经趋于稳定,但是模块化的演变过程对于前端开发者的学习来说也是十分重要的,所以我总结了模块化的演变过程和当下最流行的模块化方案的基本使用,希望大家能有所收获
模块化演变过程
-
基于文件的划分模块的方式 <body>
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
method1()
name = 'foo'
</script>
</body>
var name = 'module-a'
function method1 () {
console.log(name + '#method1')
}
function method2 () {
console.log(name + '#method2')
}
命名冲突,污染全局作用域,模块成员可以被修改,无法管理模块依赖关系; -
命名空间方式 每个模块只暴露一个全局对象,所有模块成员都挂载到这个对象中 <body>
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
moduleA.method1()
moduleB.method1()
moduleA.name = 'foo'
</script>
</body>
var moduleA = {
name: 'module-a',
method1: function () {
console.log(this.name + '#method1')
},
method2: function () {
console.log(this.name + '#method2')
}
}
通过命名空间的方式会减小命名冲突的可能,但是模块成员仍然可以被修改,仍然无法管理模块依赖关系; -
IIFE 具体做法就是将每个模块成员都放在一个函数提供的私有作用域中。 有了私有成员的概念,私有成员只能在模块成员内通过闭包的形式访问。 对于管理模块依赖关系,可以利用自执行函数的参数声明去传入依赖的模块,这样可以很清晰的看见,当前模块依赖了哪些模块。 <body>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
moduleA.method1()
moduleB.method1()
console.log(moduleA.name)
</script>
</body>
;(function ($) {
var name = 'module-a'
function method1 () {
console.log(name + '#method1')
$('body').animate({ margin: '200px' })
}
function method2 () {
console.log(name + '#method2')
}
window.moduleA = {
method1: method1,
method2: method2
}
})(jQuery)
模块化规范的出现
commonJS是以同步模块加载模块,node是在启动的时候加载模块,而在使用的时候不需要去加载模块,所以在node环境中不会有问题,但是在浏览器端必然会引起效率低下,每次页面加载都会导致大量的同步请求出现;
所以在浏览器端,并没有选择commonJS规范,而是专门为浏览器端重新设计了规范AMD(异步的模块定义规范),同期推出了Require.js,它实现AMD规范,同时它还是强大的模块加载器;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Modular evolution stage 5</title>
</head>
<body>
<h1>模块化规范的出现</h1>
<h2>Require.js 提供了 AMD 模块化规范,以及一个自动化模块加载器</h2>
<script src="lib/require.js" data-main="main"></script>
</body>
</html>
require.config({
paths: {
jquery: './lib/jquery'
}
})
require(['./modules/module1', 'jquery'], (module1) => {
console.log(module1)
module1.start()
})
define(['jquery', './module2'], function ($, module2) {
return {
start: function () {
$('body').animate({ margin: '200px' })
module2()
}
}
})
但是AMD使用起来相对复杂,模块JS文件请求频繁,一个模块就会请求一次,个人认为AMD只能算是前端模块化发展的一步,是一种妥协的方案,同期淘宝推出了Sea.js + CMD,设计的想法是希望能与CommonJS规范类似,减轻开发者的负担,算是一个重复的轮子;
define(function (require, exports, module) {
var $ = require('jquery')
module.exports = function () {
console.log('module 2~')
$('body').append('<p>module2</p>')
}
})
模块化的最佳实践
CommonJS in Node.js
ES Modules in Browers
ES Modules基本特性
- 通过给 script 添加 type = module 的属性,就可以以 ES Module 的标准执行其中的 JS 代码了
- ESM 自动采用严格模式,忽略 ‘use strict’
- 每个 ES Module 都是运行在单独的私有作用域中
- ESM 是通过 CORS 的方式请求外部 JS 模块的,请求的地址必须支持CORS
- ESM 的 script 标签会延迟执行脚本,等同于defer属性,异步加载,加载完成以后等待DOM树构建好,构建完成以后在执行;
ES Modules 导出
var name = 'foo module'
function hello () {
console.log('hello')
}
class Person {}
export { name as newName, hello as default, Person }
import { newName, default as hello, Person } from './module.js'
console.log(name, hello, Person)
ES Modules导入导出的注意事项
-
export{}的语法与对象的简写 var obj = { name, age }不相同 -
导入成员并不是复制一个副本,而是直接导入模块成员的引用地址,也就是说 import 得到的变量与 export 导入的变量在内存中是同一块空间,一旦模块中成员修改了,这里也会同时修改 -
导入模块成员变量是只读的,name = ‘tom’ // 报错 -
但是需要注意如果导入的是一个对象,对象的属性读写不受影响,会修改原模块的值,name.xxx = ‘xxx’ // 正常
ES Module导入用法
-
路径的.js后缀不可以省略,使用webpack可以省略 // import { name } from ‘./module.js’ -
路径的index.js不可以省略,使用webpack可以省略 // import { lowercase } from ‘./utils/index.js’ -
可以使用绝对路径,相对路径,URL // import { name } from ‘http://localhost:3000/04-import/module.js’ -
当导入的成员比较多的时候,// import * as mod from ‘./module.js’ -
不可以导入的路径为一个变量,也不可以出现在if判断中,如果有这样的需求,可以使用全局Import函数动态导入 import('./module.js').then(function (module) {
console.log(module.default)
console.log(module.name)
})
-
可以同时导入默认成员和其他成员 // import abc, { name, age } from ‘./module.js’
ES Module导入导出对象
当一个js要导入的模块特别多的时候,导入的语句会越来越多,那么可以用一个文件来集中管理,只需要从这个文件来导入就行了
export { foo, bar } from './module.js'
export { default as Button } from './button.js'
export { Avatar } from './avatar.js'
import { Button, Avatar } from './components/index.js'
这样做有它的好处,但是缺点是引入了所有的模块,但是我们使用的不一定是所有的模块,如果有副作用那么文件打包的时候就不会被tree shaking掉;
ES Module浏览器环境 Polyfill
低版本的IE浏览器不兼容ES Module
script有个nomodule属性,当不支持ES Module时运行
<body>
<script nomodule src="https://unpkg.com/promise-polyfill@8.1.3/dist/polyfill.min.js"></script>
<script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
<script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>
<script type="module">
import { foo } from './module.js'
console.log(foo)
</script>
</body>
上述只限于在开发环境下使用,对于线上环境应该将这些兼容性代码提前编译出来;
ES Module在Node.js下支持情况
要在Node.js中使用ES Module规范有如下步骤
-
将.js文件后缀名改成.mjs后缀名 注意: 也可以通过在package.json中添加"type":“module”,这样就不需要添加.mjs的后缀名了,但是如果这样加的话,对于使用CommonJS规范的文件需要将后缀名改为.cjs(好无语!) -
命令行运行文件时添加参数node --exprimental-modules index.mjs (实测不加也没关系0-0)
import { foo, bar } from './module.mjs'
console.log(foo, bar)
import fs from 'fs'
fs.writeFileSync('./foo.txt', 'es module working')
import { writeFileSync } from 'fs'
writeFileSync('./bar.txt', 'es module working')
import _ from 'lodash'
_.camelCase('ES Module')
ES Module与CommonJS交互
-
ES Module 中可以导入 CommonJS 模块 -
CommonJS 中不能导入ES Module 模块 -
CommonJS始终只会导出一个默认对象 注意: 实测有个特殊情况,当CommonJS规范文件(只使用一个)exports.foo = xxx导出时,在ES Module规范文件中可以使用import { foo } from './commonjs.js’获取到导出的foo
exports.foo = '111'
import name, { foo } from './commonjs.js'
console.log(foo, name)
-
注意import不是结构导出对象
ES Module与CommonJS差异
console.log(require)
console.log(module)
console.log(exports)
console.log(__filename)
console.log(__dirname)
__filename 和 __dirname 通过 import 对象的 meta 属性获取
import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
console.log(__filename)
console.log(__dirname)
console.log(import.meta.url)
__filename (绝对路径文件名): C:\Users\Administrator\Desktop\codes\es-module-in-node\03-differences\esm.mjs
import.meta.url: file:///C:/Users/Administrator/Desktop/codes/es-module-in-node/03-differences/esm.mjs
__dirname(绝对路径目录名): C:\Users\Administrator\Desktop\codes\es-module-in-node\03-differences
使用Babel兼容方案
当我们在低版本的node环境中运行ES Module时,有很多兼容性的问题,这个时候我们需要依靠babel;
yarn add @babel/node @babel/core @babel/preset-env --dev
{
"plugins": [
"@babel/plugin-transform-modules-commonjs"
]
}
npx babel-node index.js --presets=@babel/preset-env
如果写了配置文件则后面的参数可以不需要
npx babel-node index.js
|