块内部的变量和方法是私有的,只会向外暴露一些接口,通过接口与外部进行通信
背景
CommonJS的工作原理
当使用require(模块路径)导入一个模块时,node会做以下两件事情(不考虑模块缓存):
- 通过模块路径找到本机文件,并读取文件内容
- 将文件中的代码放入到一个函数环境中执行,并将执行后module.exports的值作为require函数的返回结果
正是这两个步骤,使得CommonJS在node端可以良好的被支持
但是,CommonJS是同步的,必须要等到加载完文件并执行完代码后才能继续向后执行,当想要把CommonJS放到浏览器端时,就遇到了一些挑战:
- 浏览器要加载JS文件,需要远程从服务器读取,而网络传输的效率远远低于node环境中读取本地文件的效率。由于CommonJS是同步的,这会极大的降低运行性能
- 如果需要读取JS文件内容并把它放入到一个环境中执行,需要浏览器厂商的支持,可是浏览器厂商不愿意提供支持,最大的原因是CommonJS属于社区标准,并非官方标准
基于以上两点原因,浏览器无法支持模块化
可这并不代表模块化不能在浏览器中实现
要在浏览器中实现模块化,只要能解决上面的两个问题就行了
解决办法其实很简单:
- 远程加载JS浪费了时间?做成异步即可,加载完成后调用一个回调就行了
- 模块中的代码需要放置到函数中执行?编写模块时,直接放函数中就行了
基于这种简单有效的思路,出现了AMD和CMD规范,有效的解决了浏览器模块化的问题,之后又提出了esm的模块标准
历史
- 一个一个引入 加载页面后去请求大量的script,浏览器有请求限制,几千个modules,HTTP2也无济于事
- 全写在一个文件里 作用域的问题,变量污染,全局变量
函数作用域/对象封装
- IIFE
IIFE不会污染全局变量,因为不用为函数命名
模块需要暴露的方法和变量,都可以挂载都window上 => 权衡全局变量污染问题,可以使用特殊符号避免
immediately invoked function expression 立即执行函数 IIFE可以看做是两部分构成,前半部分定义了一个函数表达式,后半部分的括号这是表示运行这个函数。
(function a(){
console.log('run a');
})();//最好加分号
var outerScope = 1;
const whatever = (function(dataWillUsedInside){
var outerScope = 0;
return {
someAttribute: 'youWantThis'
}
})(0);
// 1
console.log(outerScope);
问题:
1.不能修改某个文件,只能编译所有文件,包括没用过的
2.不能按需应用 只能引用全部的一坨
需求:
动态绑定
静态引入
模块的概念
- 将一个复杂程序的各个部分,按照一定的
规则(规范)封装不同的块(不同的文件),并组合在一起 - 块 内部的变量和方法是私有的,只会向外暴露一些接口,通过接口与外部进行通信
非模块化存在的问题
- 对全局变量的污染
- 各个js文件内部变量互相修改,即只存在全局作用域,没有函数作用域
- 各个模块如果存在依赖关系,依赖关系模糊,很难分清谁依赖谁,而依赖又必须前置
- 难以维护
1. CommonJS
一个单独的文件就是一个模块。Node.js为主要实践者,它有四个重要的环境变量为模块化的实现提供支持:module、exports、require、global。
require 命令用于输入其他模块提供的功能,module.exports命令用于规范模块的对外接口,输出的是一个值的拷贝,输出之后就不能改变了,会缓存起来。
模块以文件维度存在,并且在编译后缓存于内存中,通过require.cache可以查看模块缓存情况
- 路径分析
- 文件定位
- 编译执行
原来叫ServerJS
require
module.exports >exports
值拷贝,输出值之后模块内部变化不影响这个值
- 每个模块可以多次加载但是只会在第一次加载时运行,然后会被缓存供后续加载时使用,优先从缓存里加载
- 按照代码出现顺序同步加载
- CommonJS 模块中 require 引入模块的位置不同会对输出结果产生影响,并且会生成值的拷贝
- CommonJS 模块重复引入的模块并不会重复执行,再次获取模块只会获得之前获取到的模块的缓存
运行时加载:输入时先加载整个模块生成一个对象,然后在这个对象上读取方法
// CommonJS模块
let { stat, exists, readfile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
上面代码的实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
2.CMD/AMD
AMD推崇依赖前置,依赖也是提前就写好了,所以提前执行,提前加载, 因为是使用时用的require
CMD推崇依赖就近,依赖用的时候再require,延迟执行,用的时候在加载, 因为是定义时用的require
AMD
它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
语法
- 定义暴露模块
define([依赖模块名], function(依赖模块名){return 模块对象})
define(['dataService', 'jquery'], function (dataService, $) {
var name = 'xfzhang'
function showMsg() {
$('body').css({background : 'red'})
alert(name + ' '+dataService.getMsg())
}
return {showMsg}
})
- 引入模块
AMD也采用require()语句加载模块,但是不同于CommonJS,它要求两个参数:
require([module], callback);
- 第一个参数[module],是一个数组,里面的成员就是要加载的模块;
- 第二个参数callback,则是加载成功之后的回调函数。
/** AMD写法 **/
RequireJS
//b.js
define(function () { // ----------------- define(function(){...}) 定义一个模块
return 'string b'
})
//c.js
define(['./b.js'], function(res) { // --- define(['a'], function(res){...}) 定义一个有依赖的模块,c 依赖模块 b
return res + 'c'
});
//index.html
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://cdn.bootcss.com/require.js/2.3.6/require.min.js"></script>
</head>
<body>
<script>
require(['./requirejs/c.js'], function(res) { // 引入模块,并使用模块暴露的值,res 就是模块 c 暴露的值
console.log(res, 'res')
})
</script>
</body>
</html>
CMD
语法
这里以sea.js为例
- 定义
// 所有模块都通过 define 来定义
define(function(require, exports, module) {
// 通过 require 引入依赖
var $ = require('jquery');
var Spinning = require('./spinning');
// 通过 exports 对外提供接口
exports.doSomething = ...
// 或者通过 module.exports 提供整个接口
module.exports = ...
});
- 使用
seajs.use(['./a', './b'], function(a, b) {
a.init();
b.init();
});
/** CMD写法 **/
//定义没有依赖的模块
define(function(require, exports, module){
var value = 1
exports.xxx = value
module.exports = value
})
//定义有依赖的模块
define(function(require, exports, module){
var module2 = require('./module1') //引入依赖模块(同步)
require.async('./module2', function (m3) { //引入依赖模块(异步)
})
exports.xxx = value // 暴露模块,也可以用module.exports
})
// 引入使用模块
//定义没有依赖的模块
define(function(require, exports, module){
var value = 1
exports.xxx = value
module.exports = value
})
//定义有依赖的模块
define(function(require, exports, module){
var module2 = require('./module1') //引入依赖模块(同步)
require.async('./module2', function (m3) { //引入依赖模块(异步)
})
exports.xxx = value // 暴露模块,也可以用module.exports
})
// 引入使用模块
define(function (require) {
var a = require('./module1')
var b = require('./module2')
})
3.ESM
区别与以上三者需要在 进行时加载,ES6尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
官方比社区的优越之处,社区没办法自己加关键字,静态化设计思想,编译时确定依赖关系,不需要运行,前面都是运行
浏览器标准,兼容node
- CommonJS是运行时加载,因为只有运行时才能生成对象,从而得到对象,才可以访问对象的值
- ES6模块不是对象,而是通过exports显示输出的代码,通过import输入
CommonJS其实加载的是一个对象,这个对象只有在脚本运行时才会生成,而且只会生成一次,这个后面我们会具体解释。但是ES6模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成,这样我们就可以使用各种工具对JS模块进行依赖分析,优化代码。
与commonjs对比
- 磁盘里当作文本来扫描下
- 装载到内存中运行
因为JavaScript是没有编译动作的,这里的"编译时",个人倾向于称为"解析时",相对于"运行时"而言的。
下面的代码要求foo.js必须存在,因为在”解析时“要立刻读进来接续看看foo.js里面代码格式正确不正确,import不能出现在if/else里。
import foo from './foo.js'
如果是运行时,就可以像下面这样,编译时才不管foo.js存在不存在,只要这段代码格式没问题就好,在运行时才去读foo.js,没有就出错了。
if (someCondition) {
foo = require('./foo.js');
}
commonjs 模块在引入时就已经运行了,它是“运行时”加载的;但 es6 模块在引入时并不会立即执行,内核只是对其进行了引用(生成了一个只读引用),只有在真正用到时才会被执行,这就是“编译时”加载(引擎在编译代码时建立引用)。
ES6 模块与 CommonJS 模块完全不同
它们有三个重大差异。
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
- CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。
第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。
CommonJS 模块就是对象,输入时必须查找对象属性
// CommonJS模块
let { stat, exists, readfile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
上面代码的实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。
// ES6模块
import { stat, exists, readFile } from 'fs';
上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。
模块的整体加载,用星号(*)指定一个对象,所有输出值都加载在这个对象上面。
import * as circle from './circle'
引入default时候可以不加括号而且任意取名
本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。
import foo from './foo'
等同于
import { default as foo } from './foo'
ES6模块的好处
(1) 静态加载,编译时加载 ----- 效率较高,可以实现( 类型检查 )等只能靠( 静态分析 )实现的功能
(2) 不再需要( 对象 )作为( 命名空间 ),未来这些功能可以通过模块提供
区别
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
- ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令
import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
- 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
- 编译时加载: ES6 模块不是对象,而是通过
export命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”
注意,esm因为提升变量原因所以会在整个代码区域先执行,而commonjs在引入时执行
注意import语句与import()函数,import()函数类似于异步的require,支持动态加载模块 ,即运行时执行,什么时候运行到这一句,就会加载指定的模块,可以完全当成一个执行函数理解。import()加载模块成功以后,这个模块会作为一个对象,当作then方法的参数。
const utils = require('../utils/utils');替换写法
import * as utils from '../utils/utils';
4.webpack
只用import不用webpack有一点性能问题,因为在浏览器从上到下读取这个JS文件的时候,首先遇到import,然后去找这个包在哪,找到相应的路径,然后验证一下这些东西还能不能用,最后把这个文件读进来,然后继续在这个文件里重复这个步骤 — 一直到所有的依赖都读完了。需要注意的是,这一切都是在runtime完成的,也就是在加载你的网页的时候。
在实际开发中,浏览器模块很少被以“原始”形式进行使用。通常,我们会使用一些特殊工具,例如 Webpack,将它们打包在一起,然后部署到生产环境的服务器。
使用打包工具的一个好处是 —— 它们可以更好地控制模块的解析方式,允许我们使用裸模块和更多的功能,例如 CSS/HTML 模块等。
构建工具做以下这些事儿:
- 从一个打算放在 HTML 中的
<script type="module">“主”模块开始。 - 分析它的依赖:它的导入,以及它的导入的导入等。
- 使用所有模块构建一个文件(或者多个文件,这是可调的),并用打包函数(bundler function)替代原生的
import调用,以使其正常工作。还支持像 HTML/CSS 模块等“特殊”的模块类型。 - 在处理过程中,可能会应用其他转换和优化:
- 删除无法访问的代码。
- 删除未使用的导出(“tree-shaking”)。
- 删除特定于开发的像
console和debugger这样的语句。 - 可以使用 Babel 将前沿的现代的 JavaScript 语法转换为具有类似功能的旧的 JavaScript 语法。
- 压缩生成的文件(删除空格,用短的名字替换变量等)。
如果我们使用打包工具,那么脚本会被打包进一个单一文件(或者几个文件),在这些脚本中的 import/export 语句会被替换成特殊的打包函数(bundler function)。因此,最终打包好的脚本中不包含任何 import/export,它也不需要 type="module",我们可以将其放入常规的 <script>
参考
Babel + ts + rollup 编写一个适合2种规范格式的库
2021.4.12
es和cjs混用
一般应用npm包用2种方式,import和require。import适用于es6和es6以上语法,require使用于es6以下语法,像我们的项目中一般都是用import引入模块,因为项目已经支持了es6语法,不然就显得项目技术落后,需要升级了。
另外一种umd会被es取代,所以不再考虑
然而,import和require两种语法是不能混用的。import可以引用需要require引用的,反之则不能。
首先验证一下,的确es可以引用cjs打包的文件。但是有一点,如果要运行es6文件,需要babel.config.json + babel-node支持。其次,es可以加载cjs,只能加载cjs的default,不能加载其它的导出内容,如module.exports.a。
那么,如果我们要写一个适合2种规范格式的库,就可以写一个cjs,指定cjs默认导出default,同时供import和require两种方式加载。所以说,如果我们写的库只导出一个default,那是不必写es的。
ts项目里引用ts编译的es格式的包
诊断miauth库的导出,package.json是没有问题的,打包成cjs也是没有问题的。问题是用了ts + class + babel导致产出的class的写法有问题?提示default未定义。
- 通过一个新的ts项目,验证出原因,是 ts 的问题。
加入以下语法,以适配在TypeScript中适配以default引入cjs库
const midunAuth = require('./lib/midun-auth');
module.exports = midunAuth;
// Allow use of default import syntax in TypeScript
module.exports.default = midunAuth;
- 卡在 ts + babel + es modules,一直提示“can not use import statement outside a module”
2.1) @babel/preset-typescript是干什么的?
es6语法需要polyfill该怎么处理?
1)false,由引入者自行解决
2)entry,在入口模块导入polyfills,如import core-js、import regerator-runtime/runtime
3)usage,按需引入。
但是对于一个lib,应该按什么方式引入呢?
2.2) babel-node + @babel/preset-env + es module,是可以的。但是 + @babel/preset-typescript + ts + es module 却不行
babel-node + ts 运行时要再加上--extendsions ".ts,.tsx"才可以
zhuanlan.zhihu.com/p/102250469
- midun-auth-sdk最后收尾
2.1 去 console 和 注释代码,实现起来太难
2021.4.28
-
先发一个小米内部的包吧,
-
报错Cannot find module 'regenerator-runtime/runtime.js'
-
但是用相对路径,则没问题。把core-js改成depen也不行