当前主流 JS 模块化方案
CommonJS 规范, Node.JS 实现的规范
AMD 规范, RequireJS 实现的规范
CMD 规范, SeaJS 实现的规范, SeaJS 与 RequireJS 实现原理有很多相似的地方 u ES Modules, 当前 JS 标准模块化方案
注意: cjs,amd,cmd, ES Modules 都是只规范, 所以可能对应有多种实现
下面就对各个模块化方案做简单说明
无模块化时代
一把梭
- <script src="jquery.js">
- </script>
- <script src="jquery_scroller.js">
- </script>
- <script src="main.js">
- </script>
- <script src="other1.js">
- </script>
- <script src="other2.js">
- </script>
- <script src="other3.js">
- </script>
无模块化时代的问题
污染全局作用域
不便于拆分逻辑, 维护成本高 • 依赖关系不明显
复用性差
CommonJS 规范
CommonJS 是由 node 实现的一套规范, 关于 CommonJS 的提出可参考 CommonJS 规范
require 源码解读可参考 require() 源码解读
模块包装相当于执行如下代码, compiledWrapper 是调用 node 封装的 V8 原生创建函数的方法返回的一个函数
- function compiledWrapper(exports, require, module, __filename, __dirname) {
- // 插入文件中的代码
- // 返回导出对象
- return module.exports
- }
- compiledWrapper.call(exports, exports, require, module, filename, dirname)
CommonJS 模块输出的是一个值的拷贝, 也就是说, 一旦输出一个值, 模块内部的变化就影响不到这个值
如下有两个文件, 执行命令 node index.JS, 会有什么结果?
lib.JS
- // lib.JS
- let counter = 3
- function incCounter() {
- counter++
- }
- module.exports = {
- counter,
- incCounter
- }
index.JS
- // index.JS
- const mod = require('./lib') // 此处输出值?
- console.log(mod.counter)
- mod.incCounter() // 此处输出值?
- console.log(mod.counter)
equire 命令第一次加载该脚本, 就会执行整个脚本, 然后在内存生成一个对象, 下次加载会直接从缓存中取数据
以下是一个循环引用的例子, 请问执行 node main.JS 后会输出什么?
a.JS
- // a.JS
- console.log('a starting')
- exports.done = false
- const b = require('./b.js')
- console.log('in a, b.done = %j', b.done)
- exports.done = true
- console.log('a done')
b.JS
- // b.JS
- console.log('b starting')
- exports.done = false
- const a = require('./a.js')
- console.log('in b, a.done = %j', a.done)
- exports.done = true
- console.log('b done')
main.JS
- // main.JS
- console.log('main starting')
- const a = require('./a.js')
- const b = require('./b.js')
- console.log('in main, a.done = %j, b.done = %j', a.done, b.done)
AMD 规范
AMD 是 Asynchronous Module Definition 的简写, 即异步模块定义
AMD 规范的完整定义可参考 https://github.com/amdjs/amdjs-api/wiki/AMD
RequireJS 是在浏览器中运行的, 所有一些基础库需要先配置, 以方便其他库调用, 可以理解为 CommonJS 中的 node_modules 下的包. 业务模块也可定义在其中, 可认为是路径别名. paths 中的路径不能包含扩展名.
- require.config({
- paths: {
- // 如果第一个加载失败就会加载第二个
- jQuery: ['lib/jquery.min', 'lib/jquery'],
- lodash: 'lib/lodash.min',
- main: './mian' // 入口文件
- }
- })
定义模块
- /**
- * 定义模块, 当依赖加载完成后执行回调
- * 回调可返回值, 返回值会被导出到外部使用
- * @param {String} id 模块名称, 可省略
- * @param {Array} dependencies 依赖的模块
- * @param {Function} factory 回调函数
- */
- define(id?, dependencies?, factory);
- define(['jquery'], function($) {
- $('body').CSS({ background: 'red' })
- // 导出 log 函数
- return (...args) => console.log('自定义 log', ...args)
- })
加载模块
- /**
- * 加载模块
- * @param {Array} deps 要加载的模块
- * @param {Function} callback 加载成功回调, 回调参数为加载模块导出对象
- * @param {Function} errback 加载失败回调
- */
- RequireJS(deps, callback, errback)
- require(['main'], log => {
- log('我成功加载了')
- // do something..., 也可以在这里继续 require 其他 JS 文件
- })
RequireJS 使用示例
目录结构
.
├── index.html
└── JS
├── lib
│ ├── jQuery.JS
│ ├── lodash.JS
│ └── require.JS
├── main.JS
└── time.JS
index.HTML
- <!DOCTYPE HTML>
- <HTML>
- <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>
- RequireJS-demo
- </title>
- </head>
- <body>
- <h1 id="time">
- </h1>
- <script src="./js/lib/require.js" data-main="./js/main.js">
- </script>
- </body>
- </HTML>
main.JS
- RequireJS.config({
- baseUrl: '/js/',
- paths: {
- jQuery: './lib/jquery',
- lodash: './lib/lodash'
- }
- })require(['jquery', './js/time.js'], ($, time) => {
- $('#time').text('TIME:' + time.getTime())
- setInterval(() => {
- $('#time').text('TIME:' + time.getTime())
- }, 1000)
- })
time.JS
- define(['jquery', 'lodash'], ($, _) => ({
- getTime() {
- const time = new Date()
- const year = time.getFullYear()
- const month = _.padStart(time.getMonth() + 1, 2, '0')
- const date = _.padStart(time.getDate(), 2, '0')
- const hour = _.padStart(time.getHours(), 2, '0')
- const minute = _.padStart(time.getMinutes(), 2, '0')
- const second = _.padStart(time.getSeconds(), 2, '0')
- return `${year}/${month}/${date} ${hour}:${minute}:${second}`
- }
- }))
CMD 规范
CMD 是 Common Module Definition 的简写, 即通用模块定义
CMD 规范的完整定义可参考 https://github.com/seajs/seajs/issues/242
CMD 的主要代表是 SeaJS.CMD 推崇依赖就近, AMD 推崇依赖前置. 即 AMD 在定义模块的时候就必须把依赖包含进来, CMD 是在使用的时候再 require 对应的依赖
当前主流的库对 CMD 支持不是很友好, 都需要额外的修改才能工作
AMD 与 CMD 写法对比如下
- // CMD
- // 代码写起来有同步 require 的感觉
- define((require, exports, module) => {
- const $ = require('jquery')
- $('title').text('hello')
- })
- // AMD
- // 明显的异步风格
- define(['jquery'], $ => {
- $('title').text('hello')
- })
SeaJS 中 require 书写约定
正确拼写 require
- // 错误!
- define(function(req) {
- // ...
- }) // 正确!
- define(function(require) {
- // ...
- })
使用直接量
- // 错误!
- require(myModule) // 错误!
- require('my-' + 'module') // 错误!
- require('MY-MODULE'.toLowerCase()) // 正确!
- require('my-module')
不要修改 require
- // 错误 - 重命名 "require"!
- var req = require,
- mod = req('./mod') // 错误 - 重定义 "require"!
- require = function() {} // 错误 - 重定义 "require" 为函数参数!
- function F(require) {} // 错误 - 在内嵌作用域内重定义了 "require"!
- function F() {
- var require = function() {}
- }
SeaJS 隐藏坑
如下代码输出 $ 为 null
- function func(require, exports, module) {
- const $ = require('jquery')
- console.log($)
- }
- func.toString = () => '() => {}'
- define(func)
SeaJS 对于 require 和 define 函数的特殊要求是由于, SeaJS 原理导致的, SeaJS 的执行流程大致如下
SeaJS 执行流程
SeaJS 使用示例
目录结构
.
├── index.HTML
└── JS
├── lib
│ ├── jQuery.JS
│ ├── lodash.JS
│ └── sea.JS
├── main.JS
└── time.JS
index.HTML
- <!DOCTYPE HTML>
- <HTML>
- <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>
- SeaJS-demo
- </title>
- </head>
- <body>
- <h1 id="time">
- </h1>
- <script src="./js/lib/sea.js" data-main="./js/main.js">
- </script>
- <script>
- SeaJS.config({
- base: '/js/',
- alias: {
- jQuery: './lib/jquery',
- lodash: './lib/lodash'
- }
- })
- // 加载入口模块
- SeaJS.use('./js/main.js')
- </script>
- </body>
- </HTML>
main.JS
- define((require, exports, module) => {
- const $ = require('jquery')
- const time = require('./time.js')
- $('#time').text('TIME:' + time.getTime())
- setInterval(() => {
- $('#time').text('TIME:' + time.getTime())
- }, 1000)
- })
time.JS
- define((require, exports, module) => {
- module.exports = {
- getTime() {
- const $ = require('jquery')
- const _ = require('lodash')
- const time = new Date()
- const year = time.getFullYear()
- const month = _.padStart(time.getMonth() + 1, 2, '0')
- const date = _.padStart(time.getDate(), 2, '0')
- const hour = _.padStart(time.getHours(), 2, '0')
- const minute = _.padStart(time.getMinutes(), 2, '0')
- const second = _.padStart(time.getSeconds(), 2, '0')
- return `${year}/${month}/${date} ${hour}:${minute}:${second}`
- }
- }
- })
- ES Modules
ES Modules 是 ECMAScript modules 的简写, 也可写为 ESM. ES Modules 是 JS 官方推出的标准
ES Modules 相比于其他模块规范是一个静态化的模块解决方案, 其他模块化方案都是运行时才能确定输出内容, 而 ES Modules 是编译时就确定了的. 其他模块化方案导入文件都是整个导入模块, 而 ES Modules 可以只导入需要的部分
ES Modules 会自动采用严格模式, 不需要像 ES5 一样在头部加上 "use strict"
ES Modules 可运行在服务端 (node) 和浏览器. 目前主流浏览器都已经支持 ES Modules,node 使用 ES Modules 需要在执行时加上 --experimental-modules, 且要求编写的 JS 文件必须以. mjs 为后缀
ES Modules 导出的是一个值得引用, 即在模块内改变了导出值, 那么下一次使用也会得到新的值
如下有两个文件, 执行命令
node --experimental-modules index.mjs
, 会有什么结果?
- lib.mjs
- // lib.mjs
- export let counter = 3
- export function incCounter() {
- counter++
- }
- index.mjs
- // index.mjs
- import * as mod from './lib'
- // 此处输出值?console.log(mod.counter)
- mod.incCounter()
- // 此处输出值?
- console.log(mod.counter)
循环引用
请问执行 node --experimental-modules main.mjs 后会输出什么内容
- a.mjs
- // a.mjs
- import {
- bar
- } from './b.mjs'
- console.log('a.mjs')
- console.log(bar)
- export let foo = 'foo'
- b.mjs
- // b.mjs
- import {
- foo
- } from './a.mjs'
- console.log('b.mjs')
- console.log(foo)
- export let bar = 'bar'
- main.mjs
- // main.mjs
- import './a.mjs'
循环依赖问题
在所有的模块规范中都存在循环依赖问题, 解决依赖循环的方式都相似, 几乎都采用惰性导入的方式来解决.
如下两个文件存在循环引用, 当执行 node --experimental-modules a.mjs 时, 会报错说 b 未定义, 这就是由于循环依赖导致的, 如果不使用 b 则不会报错, 修改方案如下. 其他的模块循环引用也可按照此方法进行修改.
CommonJS 也可以使用先导出自身, 再引入其他模块的方式尽心避免. 同时也可以把 require 放入到函数体中, 即在调用的时后才去加载依赖
循环依赖
相关链接
AMD 和 CMD 的区别有哪些?- 玉伯的回答 - 知乎
- https://github.com/seajs/seajs/issues/277
- https://github.com/seajs/seajs/issues/242
PPT 中示例源码: talk-is-cheap-show-me-the-code, 源码示例不完善, 仅供参考
来源: http://www.jianshu.com/p/419b5806622c