模块化
从本文你将理解到
- 什么是模块化
- 模块化的进化史
- 当下罕用的模块化标准 CommonJS,ES Module
- ES Module 个性
- ES Module 应用 in Browsers , Node.js
- ES Modules in Node.js – 与 CommonJS 交互
- ES Modules in Node.js – 与 CommonJS 差别
模块化
- 前端开发范式 是一种思维
- 依据性能不同将代码划分不同模块,从而进步开发效率,升高保护老本
模块化的进化史
stage- 1 文件划分形式
<body>
<h1> 模块化演变(第一阶段)</h1>
<h2> 基于文件的划分模块的形式 </h2>
<p>
具体做法就是将每个性能及其相干状态数据各自独自放到不同的文件中,约定每个文件就是一个独立的模块,应用某个模块就是将这个模块引入到页面中,而后间接调用模块中的成员(变量 / 函数)</p>
<p>
毛病非常显著:所有模块都间接在全局工作,没有公有空间,所有成员都能够在模块内部被拜访或者批改,而且模块一段多了过后,容易产生命名抵触,另外无奈治理模块与模块之间的依赖关系
</p>
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
// 命名抵触
method1()
// 模块成员能够被批改
name = 'foo'
</script>
</body>
module-a.js
var name = 'module-a'
function method1 () {console.log(name + '#method1')
}
function method2 () {console.log(name + '#method2')
}
stage- 2 命名空间形式
<body>
<h1> 模块化演变(第二阶段)</h1>
<h2> 每个模块只裸露一个全局对象,所有模块成员都挂载到这个对象中 </h2>
<p>
具体做法就是在第一阶段的根底上,通过将每个模块「包裹」为一个全局对象的模式实现,有点相似于为模块内的成员增加了「命名空间」的感觉。</p>
<p>
通过「命名空间」减小了命名抵触的可能,然而同样没有公有空间,所有模块成员也能够在模块内部被拜访或者批改,而且也无奈治理模块之间的依赖关系。</p>
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
moduleA.method1()
moduleB.method1()
// 模块成员能够被批改
moduleA.name = 'foo'
</script>
</body>****
module-a.js
var moduleA = {
name: 'module-a',
method1: function () {console.log(this.name + '#method1')
},
method2: function () {console.log(this.name + '#method2')
}
}
stage-3 IIFE 形式
<body>
<h1> 模块化演变(第三阶段)</h1>
<h2> 应用立刻执行函数表达式(IIFE:Immediately-Invoked Function Expression)为模块提供公有空间 </h2>
<p>
具体做法就是将每个模块成员都放在一个函数提供的公有作用域中,对于须要裸露给内部的成员,通过挂在到全局对象上的形式实现
</p>
<p>
有了公有成员的概念,公有成员只能在模块成员内通过闭包的模式拜访。能够通过 IIFE 向模块外部传参
</p>
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
moduleA.method1()
moduleB.method1()
// 模块公有成员无法访问
console.log(moduleA.name) // => undefined
</script>
</body>
module-a.js
;(function () {
var name = 'module-a'
function method1 () {console.log(name + '#method1')
}
function method2 () {console.log(name + '#method2')
}
window.moduleA = {
method1: method1,
method2: method2
}
})()
stage-4 AMD 标准
<body>
<h1> 模块化标准的呈现 </h1>
<h2>Require.js 提供了 AMD 模块化标准,以及一个自动化模块加载器 </h2>
<script src="lib/require.js" data-main="main"></script>
</body>
module1.js
// 因为 jQuery 中定义的是一个名为 jquery 的 AMD 模块
// 所以应用时必须通过 'jquery' 这个名称获取这个模块
// 然而 jQuery.js 并不在同级目录下,所以须要指定门路
// define API 参数:模块名,依赖项(可选参数),函数(函数参数与依赖项对应)为以后模块提供公有空间
define('module1', ['jquery', './module2'], function ($, module2) {
return { // 公有空间向外导出成员通过 return
start: function () {$('body').animate({margin: '200px'})
module2()}
}
})
module2.js
// 兼容 CMD 标准(相似 CommonJS 标准)define(function (require, exports, module) {
// 通过 require 引入依赖
var $ = require('jquery')
// 通过 exports 或者 module.exports 对外裸露成员
module.exports = function () {console.log('module 2~')
$('body').append('<p>module2</p>')
}
})
以后模块化局势
模块化标准 CommonJS,ES Module
-
CommonJS 标准
- nodejs 提出的一套规范
- nodejs 代码必须遵循 CommonJS 标准
- 一个文件就是一个模块
- 每个模块都有独自的作用域
- 通过 module.exports 导出成员
- 通过 require 函数载入模块
- CommonJS 是以同步模式加载模块
- node 端:node 的执行机制是在启动时加载模块。执行过程中不须要加载,只会应用到模块,因而 node 环境下应用 commonjs 没有问题
- 浏览器端:必然导致效率低下,每次页面加载都会导致大量同步模式申请呈现,因而晚期前端模块化中并没有应用 commonjs 标准,而是依据浏览器特点设计了专门用于浏览器端的标准 AMD(Asynchronous Module Definition),和库 Require.js(实现了 AMD 标准),Require 也是个十分弱小的模块加载器
- 目前绝大多数第三方库都反对 AMD 标准,但应用起来绝对简单,另外如果我的项目模块划分过细,那么同一个页面对 js 文件的申请次数就会特地多,从而导致页面效率低下
- 同期呈现了淘宝 Sea.js 库 +CMD 标准,当然起初也被 Require.js 兼容了
ESModule 个性
- 装置
yarn global add serve
- 执行
serve ./src/xxx
查看文件 -
给 script 增加
type=module
属性,就能够以 ESModule 的规范执行其中的 JS 代码<script type="module"> console.log('this is es module'); </script>
相比于一般 script 标签,esm 的特点
-
主动采纳严格模式,疏忽 ‘use strict’
<script type="module"> console.log(this); //undefined </script>
-
每个 ES Module 都是运行在独自的公有作用域中
<script type="module"> var foo = 100; console.log(foo); </script> <script type="module"> console.log(foo); // 报错 </script>
- ESM 是通过 CORS 的形式申请内部 js 模块的
意味着如果申请的 js 不在同源目录下。申请的服务端地址,它在响应的响应头中须要提供无效的 CORS 标头,也就是申请地址须要反对 CORS
// 申请胜利
<script type="module" src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script>
// 申请失败
<script type="module" src="https://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
- ESM 的 script 标签会提早执行脚本 等同于 defer 属性
默认按执行程序,script 立刻执行,页面的渲染会期待脚本执行后再持续渲染
增加 type 后,执行程序并不等于引入程序
<script type="module">
alert("Hello")
</script>
<p> 须要显示的内容 </p>
ESM 导入和导出
每个模块领有公有作用域,因而内部无法访问
通过 export,import 进行模块裸露和载入
export,import
-
export 能够导出变量,函数,类
export var foo = {} export var fn = function(){} export class Person{}
-
独自应用 export, 更直观形容导出成员
var foo = {} var fn = function(){} export {foo,fn}
-
导出重命名
var foo = {} var fn = function(){} export {foo as baz,fn} // 引入时通过 baz 引入
-
重命名为 default, 默认导出
var foo = {} var fn = function(){} export {foo as default,fn} // 默认导出必须重命名 import {default as baz} from ''
-
默认导出写法 2
export default {foo, fn} // 接管对象成员 import mod from '' | mod.fn 应用
导出注意事项
-
export {}不是字面量对象,import 引入的也不是解构,都是固定语法, 因而
export foo
也是不被容许的,要应用export var foo
, 而export default {}
默认导出,导出的是字面量对象# a.js var foo = {} var fn = function(){} export {foo,fn} ... # b.js import {foo,fn} from ''
-
通过 export 导出的不是值,而是值的地址,内部取值会受到外部值批改的影响
# a.js export {foo,fn} setTimeout(function(){foo="ben"},1000) ... # b.js import {foo,fn} from '' console.log(foo) setTimeout(function(){console.log(foo) },1500)
-
内部导入的成员属于只读成员(常量),无奈批改
#b.js import {foo,fn} from '' console.log(foo) foo = "bau" // 报错
导入注意事项
import xx from './module.js'
门路名称必须残缺不能省略,省略会报错import xx from '/xx/module.js'
导入外部文件应用相对路径或者绝对路径,’./’ 不能省略,否则会认为是加载模块import xx from 'http://localhost:3000/xx/module.js'
能够应用残缺的 url 拜访import {} from './module.js'
只会执行模块,不会提取成员import './module.js'
下面能够简写成 import ‘ 模块门路 ’,导入一些不须要管制的子功能模块十分好用import * as mod from './module.js'
当导出内容很多,用 * as mod 提取进去,通过 mod.xx 应用成员import()
动静导入模块
应用动静导入模块解决
// 无奈用变量
var modulePath = "./module.js"
import {name} from modulePath;
// 无奈条件判断, 只能放在最顶层作用域
if(true){import {name} from './module.js'
}
// 全局的 import 函数,用来动静导入模块, 返回 promise
import('./module.js').then(function(module){console.log(module); // 导入的模块对象通过参数拿到
})
import title,{foo,fn} from './module.js'
对于导出的默认成员能够将其提取进去,也能够写成import {foo,fn, default as title} from './module.js'
导入导出成员
export {name, age} from ''
所有导入成员作为以后模块的导出成员, 那么导入的成员无法访问
# index.js 临时文件
import {Button} from './xxx'
import {Avatar} from './xxx1'
export {Button,Avatar}
// 改写成
export {Button} from './xxx'
export {Avatar} from './xxx1'
ESM in Browsers
ie 下用 Polyfill, 增加 CDN 减速,解决 esm 用不了的问题
Polyfill 是一块代码(通常是 Web 上的 JavaScript),用来为旧浏览器提供它没有原生反对的较新的性能, 能够了解为是一种兼容计划, 参考地址
配置运行环境
装置yarn global add browser-sync
应用browser-sync . --files **/*.js
或者 vscode 装置个 live Server
启用
// 通过 unpkg 网站提供的 cdn 服务拿到 js 文件
// https://unpkg.com/browse/browser-es-module-loader@0.4.1/dist/
// https://unpkg.com/browse/promise-polyfill@8.2.0/dist/
<body>
<!-- nomodule 布尔值 不反对模块化的浏览器会执行,不然 chrome 执行两次,只适宜开发阶段 -->
<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>
ESM in Node.js
nodejs 中应用 ESM 须要批改文件名从 .js
=> .mjs
node 启动文件应用 node --experimental-modules xxx.mjs
案例
# module.mjs
export const foo = 'hello'
export const bar = 'world'
# index.mjs
import {foo, bar} from "./module.mjs";
console.log(foo,bar);
//node 中应用 esm 须要批改文件名为 .mjs
/**
* node --experimental-modules index.mjs
(node:9696) ExperimentalWarning: The ESM module loader is experimental.
hello world
*
*/
// 通过 ESM 载入原生模块
import fs from "fs";
fs.writeFileSync('./foo.txt','es module working')
// 零碎内置模块,能够通过这种形式导入,内置模块兼容了 ESM 的提取成员形式
import {writeFileSync} from "fs";
writeFileSync('./bar.txt','es module working~ bar')
// 通过 ESM 载入第三方模块
import _ from "lodash" // 须要 yarn add loadsh
console.log(_.camelCase('ES MODULE'))
// 不反对,因为第三方模块都是导出默认成员,只能用默认导入
// import {camelCase} from "lodash";
// console.log(camelCase('ES Module'));
ESM 与 CommonJS 交互
# commonjs.js
//CommonJS 模块始终只会导出一个默认成员
// 这也就意味着只能通过 import 载入默认成员形式引入
module.exports = {foo: 'commonjs exports value foo'}
exports.baz = 'commonjs exports value baz'
console.log("---------------------------------------");
// node --experimental-modules commonjs.js
// 不能在 CommonJS 模块中通过 require 载入 ESM
const mod = require('./es-module.mjs')
console.log(mod);// 报错
# es-module.mjs
// node --experimental-modules es-module.mjs
//ESM 中能够导入 Commonjs 模块
import mod from "./commonjs.js";
console.log(mod); //ok
// 不能间接提取成员,留神 import 不是解构导出对象
import {baz} from "./commonjs.js";
console.log(baz); // 报错
import baz from './commonjs.js' //ok
console.log("---------------------------------------------");
export const foo = 'es module export value'
总结
- ES Modules 中能够导入 CommonJS 模块
- CommonJS 中不能导入 ES Modules 模块
- CommonJS 始终只会导出一个默认成员
- 留神 import 不是解构导出对象
ESM 与 CommonJS 差别
运行环境nodemon --experimental-modules xxx.mjs
在 commonjs 中的对象在 ESM 中会报错,解决办法是用其余办法代替,报错办法有
# cjs.js
// 加载模块函数
console.log(require);
// 模块对象
console.log(module);
// 导出对象别名
console.log(exports);
// 以后文件的绝对路径
console.log(__filename);
// 以后文件所在目录
console.log(__dirname);
# esm.mjs
// 前三个能够应用 export import 代替
// 后两个
import {fileURLToPath} from "url";
const __filename = fileURLToPath(import.meta.url)
console.log(__filename); //d:\Users\admin\Desktop\part2-2\differences\esm.mjs
import {dirname} from 'path'
const __dirname = dirname(__filename)
console.log(__dirname); //d:\Users\admin\Desktop\part2-2\differences
ESM in Nodejs 进一步反对
将 package.json 中增加 type = 'module'
, 这样不必批改.js 为.mjs
了
# package.json
{"type":"module"}
// 运行命令 `nodemon --experimental-modules xxx.js`
如果增加了 type 的我的项目中还想应用 CommonJS
# common.js
const path = require('path')
console.log(path.join(__dirname,'foo'))
会报错,解决办法将 commonjs 文件批改为 common.cjs
ESM in Nodejs Babel 兼容计划
晚期 node 版本 8.0.0,通过 babel 来让 node 运行 esm
装置 babel-mode yarn add @babel/node @babel/core @babel/preset-env --dev
babel 是基于插件机制去实现的,外围 core 和 preset-env 并不会转换代码,具体转换个性通过插件
一个插件转化一个个性,preset-env 是插件汇合,这个汇合蕴含了 js 规范中所有新个性
理论帮咱们转换的是汇合里的插件
能够通过yarn babel-node index.js --presets=@babel/preset-env
或者放入配置文件.babelrc
json 格式文件
{"presets":["@babel/preset-env"]
}
执行yarn babel-node index.js
或者移除汇合应用插件
通过 yarn remove @babel/preset-env
yarn add @babel/plugin-transform-modules-commonjs --dev
{
"plugins":["@babel/plugin-tranform-modules-commonjs"]
}