js模块化进程

108 阅读5分钟

最初把所有js都写在一个文件中,这样会出现代码冗长,全局变量污染,等一系列问题

前端开发者想代码模块化

1、一个函数就是一个模块

function m1 () { // ... } 
function m2 () { // ... }

缺点:污染了全局变量,无法保证不会与其它模块发生冲突,而且模块成员之间看不出直接关系。

2、一个对象是一个模块

var module1 = new Object({ 
    _sum: 0, 
    foo1: function () {}, 
    foo2: function () {} 
})

缺点:会暴露所有模块成员,内部的状态可能被改写。

3、立即执行函数为一个模块

  var module1 = (function() {
    var _sum = 0;
    var foo1 = function () {};
    var foo2 = function () {};
    return {
      foo1: foo1,
      foo2: foo2
    }
  })();

利用立即执行函数内的作用域已经闭包来实现模块功能,导出我们想要导出的成员。

此时外部代码就不能读取到_sum了:

console.log(module1._sum) // undefined

4、CommonJS规范

export require

  • 所有代码都运行在模块作用域,不会污染全局作用域;

  • 模块是同步加载的,即只有加载完成,才能执行后面的操作;

  • 模块在首次执行后就会缓存,再次加载只返回缓存结果,如果想要再次执行,可清除缓存;

  • CommonJS输出是值的拷贝(即,require返回的值是被输出的值的拷贝,模块内部的变化也不会影响这个值)。

webpack 中webpack文件是在nodejs中执行所以用require,加载image也是要经过webpack编译所以用require

5、AMD规范

commonjs的同步加载只适应服务端,因为从磁盘上读取文件快,前端文件要ajax请求会很慢,所以commonjs不适合前端

requirejs 执行的时候异步加载依赖,依赖加载完再执行需要依赖的代码

require define

6、CMD规范

seajs

require define

1、AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块

2、CMD推崇就近依赖,只有在用到某个模块的时候再去require

7、ES6 Module

每个模块都有自己的上下文,每个模块内部声明变量都是局部变量,不会污染全局作用域

每个模块只加载一次(是单例的),若再去加载相同目录下的文件,直接从内存中读取(前端路由懒加载,读取过文件就会放在内存里,下次进入这个路由,不会再去加载对用的组件文件,从内存中读取)

  • 输出使用export 由于代码是模块而不是脚本,因此所有声明都将作用于该模块,而不是在所有脚本和模块中全局可见

  • 输入使用import 打包的时候会tree-shaking掉没用到的函数

  • 可以使用export...from...这种写法来达到一个"中转"的效果

  • 输入的模块变量是不可重新赋值的,它只是个可读引用,不过却可以改写属性

  • export命令和import命令可以出现在模块的任何位置,只要处于模块顶层就可以。 如果处于块级作用域内,就会报错,这是因为处于条件代码块之中,就没法做静态优化了,违背了ES6模块的设计初衷。

  • import命令具有提升效果,会提升到整个模块的头部,首先执行。

JS引擎在运行一个模块时,有以下几个步骤:

  1. 解析:读取模块的源代码并检查语法错误;(没用到的变量会优化删掉吗)
  2. 加载:递归加载所有导入的模块
  3. 作用域:创建模块作用域,并使用模块中声明的主体(如函数)填充,包括从其他模块导入的主体;
  4. 执行每个新加载模块主体中的语句;

es6 动态加载

允许您仅在需要时动态加载模块,而不必预先加载所有模块,这存在明显的性能优势

这个新功能允许您将import()作为函数调用,将其作为参数传递给模块的路径。它返回一个 promise,它用一个模块对象来实现,让你可以访问该对象的导出

import('/modules/myModule.mjs') .then((module) => { // Do something with the module. });

Bable下的ES6模块转换

还有一点就是,如果你有使用过一些ES6的Babel的话,你会发现当使用export/import的时候,Babel也会把它转换为exports/require的形式。

commomjs和es6 module区别

  • CommonJS模块是运行时加载,ES6 Modules是编译时输出接口
  • CommonJS输出是值的拷贝;ES6 Modules输出的是值的引用,被输出模块的内部的改变会影响引用的改变
  • CommonJs导入的模块路径可以是一个表达式,因为它使用的是require()方法;而ES6 Modules只能是字符串
  • CommonJS this指向当前模块,ES6 Modules this指向undefined
  • 且ES6 Modules中没有这些顶层变量:argumentsrequiremoduleexports__filename__dirname

关于第一个差异,是因为CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

打包后在浏览器中运行的格式

es6module 还是 umd? caniuse.com/

如果要支持各种浏览器 要选择umd

默认情况下,Vite 的目标浏览器是指能够 支持原生 ESM script 标签 和 支持原生 ESM 动态导入 的。作为参考,Vite 使用这个 browserslist 作为查询标准:

defaults and supports es6-module and supports es6-module-dynamic-import, not opera > 0, not samsung > 0, not and_qq > 0

你也可以通过 build.target 配置项 指定构建目标,最低支持 es2015

请注意,默认情况下 Vite 只处理语法转译,且 默认不包含任何 polyfill。你可以前往 Polyfill.io 查看,这是一个基于用户浏览器 User-Agent 字符串自动生成 polyfill 包的服务。

传统浏览器可以通过插件 @vitejs/plugin-legacy 来支持,它将自动生成传统版本的 chunk 及与其相对应 ES 语言特性方面的 polyfill。兼容版的 chunk 只会在不支持原生 ESM 的浏览器中进行按需加载。

参考 一篇不是标题党的CommonJS和ES6模块规范讲解