Module
CRUST Principles
- Consistent: ES6 API design
Array.XXX(fn). - Resilient: jQuery sizzle API design
$(element)/$(selector)/$(selector, context). - Unambiguous.
- Simple: Simple
fetchAPI design. - Tiny: Tiny surface areas.
Namespace Module Pattern
Namespace Module Constructor
- 命名空间.
- 依赖模式.
- 私有属性/特权方法.
- 初始化模式.
- 揭示模式: 公共接口.
- 即时函数模式.
APP.namespace = function (namespaceString) {
let parts = namespaceString.split('.')
let parent = APP
let i
// strip redundant leading global
if (parts[0] === 'APP') {
// remove leading global
parts = parts.slice(1)
}
for (i = 0; i < parts.length; i += 1) {
// create a property if it doesn't exist
if (typeof parent[parts[i]] === 'undefined')
parent[parts[i]] = {}
// 关键: 向内嵌套
parent = parent[parts[i]]
}
// 返回最内层模块名
return parent
}
// assign returned value to a local var
const module2 = APP.namespace('APP.modules.module2')
const truthy = module2 === APP.modules.module2 // true
// skip initial `APP`
APP.namespace('modules.module51')
// long namespace
APP.namespace('once.upon.a.time.there.was.this.long.nested.property')
Namespace Module Usage
通过传参匿名函数, 创建命名空间, 进行模块包裹:
const app = {}
;(function (exports) {
;(function (exports) {
const api = {
moduleExists: function test() {
return true
},
}
// 闭包式继承,扩展exports对象为api对象
$.extend(exports, api)
})(typeof exports === 'undefined' ? window : exports)
// 将api对象绑定至app对象上
})(app)
// global object
const APP = {}
// constructors
APP.Parent = function () {}
APP.Child = function () {}
// a variable
APP.some_var = 1
// an object container
APP.modules = {}
// nested objects
APP.modules.module1 = {}
APP.modules.module1.data = { a: 1, b: 2 }
APP.modules.module2 = {}
// 命名空间模式
APP.namespace('APP.utilities.array')
// 形参: 导入全局变量
APP.utilities.array = (function (app, global) {
// 依赖模式
const uObj = app.utilities.object
const uLang = app.utilities.lang
// 私有属性
const arrStr = '[object Array]'
const toStr = Object.prototype.toString
// 私有方法
const inArray = function (haystack, needle) {
for (let i = 0, max = haystack.length; i < max; i += 1) {
if (haystack[i] === needle)
return i
}
return -1
}
const isArray = function (a) {
return toStr.call(a) === arrayString
}
// 初始化模式:
// 初始化代码, 只执行一次.
// 揭示公共接口.
return {
isArray,
indexOf: inArray,
}
})(APP, this)
Sandbox Module Pattern
Sandbox Module Constructor
- 私有属性绑定至 this/prototype.
- 特权方法绑定至 modules/prototype.
function Sandbox(...args) {
// the last argument is the callback
const callback = args.pop()
// modules can be passed as an array or as individual parameters
let modules = args[0] && typeof args[0] === 'string' ? args : args[0]
// make sure the function is called
// as a constructor
if (!(this instanceof Sandbox))
return new Sandbox(modules, callback)
// add properties to `this` as needed:
this.a = 1
this.b = 2
// now add modules to the core `this` object
// no modules or "*" both mean "use all modules"
if (!modules || modules === '*') {
modules = []
for (const i in Sandbox.modules) {
if (Object.prototype.hasOwnProperty.call(Sandbox.modules, i))
modules.push(i)
}
}
// initialize the required modules
for (let i = 0; i < modules.length; i += 1)
Sandbox.modules[modules[i]](this)
// call the callback
callback(this)
}
// any prototype properties as needed
Sandbox.prototype = {
name: 'My Application',
version: '1.0',
getName() {
return this.name
},
}
静态属性: 使用添加的方法/模块:
Sandbox.modules = {}
Sandbox.modules.dom = function (box) {
box.getElement = function () {}
box.getStyle = function () {}
box.foo = 'bar'
}
Sandbox.modules.event = function (box) {
// access to the Sandbox prototype if needed:
// box.constructor.prototype.m = "mmm";
box.attachEvent = function () {}
box.detachEvent = function () {}
}
Sandbox.modules.ajax = function (box) {
box.makeRequest = function () {}
box.getResponse = function () {}
}
Sandbox Module Usage
Sandbox(['ajax', 'event'], (box) => {
// console.log(box);
})
Sandbox('*', (box) => {
// console.log(box);
})
Sandbox((box) => {
// console.log(box);
})
Sandbox('dom', 'event', (box) => {
// work with dom and event
Sandbox('ajax', (box) => {
// another "box" object
// this "box" is not the same as
// the "box" outside this function
// ...
// done with Ajax
})
// no trace of Ajax module here
})
CommonJS Pattern
- 无论一个模块在
require()中被引用多少次, 模块永远是单例, 只会被加载一次. - 模块第一次加载后会被缓存, 后续加载会取得缓存的模块.
- 模块加载是模块系统执行的同步操作,
require()可以位于条件语句中.
require.cache = Object.create(null)
// Construct 'require', 'module' and 'exports':
function require(moduleId) {
if (!(moduleId in require.cache)) {
const code = readFile(moduleId)
const module = { exports: {} }
require.cache[moduleId] = module
// Bind code to module.exports:
const wrapper = new Function('require, exports, module', code)
wrapper(require, module.exports, module)
}
return require.cache[moduleId].exports
}
AMD Pattern
Asynchronous module definition:
// ID 为 'moduleA' 的模块定义:
// moduleA 依赖 moduleB.
// moduleB 会异步加载.
define('moduleA', ['moduleB'], (moduleB) => {
return {
stuff: moduleB.doStuff(),
}
})
define('moduleA', ['require', 'exports'], (require, exports) => {
const moduleB = require('moduleB')
if (condition) {
const moduleC = require('moduleC')
}
exports.stuff = moduleB.doStuff()
})
UMD Pattern
Universal module definition:
- 判断是否支持 AMD (define), 存在则使用 AMD 方式加载模块.
- 判断是否支持 Node.js 的模块 (exports), 存在则使用 Node.js 模块模式.
/**
* UMD Boilerplate.
*/
;(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define([], () => {
return factory(root)
})
} else if (typeof exports === 'object') {
module.exports = factory(root)
} else {
root.myPlugin = factory(root)
}
})(
typeof globalThis !== 'undefined'
? globalThis
: typeof window !== 'undefined'
? window
: this,
(window) => {
'use strict'
// Module code goes here...
return {}
}
)
ES6 Module
ES6 Module Features
- Singleton:
- 模块是单例.
- 模块只能加载一次: 同一个模块无论在一个页面中被加载多少次, 也不管它是如何加载的, 实际上都只会加载一次.
- Imports:
- 模块可以请求加载其他模块.
- 模块支持循环依赖.
StaticandRead-onlyimports.
- Exports:
- 模块可以定义公共接口.
- 其他模块可以基于这个公共接口观察和交互.
- Local Scope:
- 模块不共享全局命名空间.
- 模块顶级
this的值是undefined(传统脚本中是window). - 模块中的
var声明不会添加到window对象.
- Async:
- 模块在浏览器中是异步加载和执行的.
- 模块代码只在加载后执行.
- 解析到
<script type="module">标签后会立即下载模块文件, 但执行会延迟到 HTML 文档解析完成 (<script defer>).
- Strict:
- 模块代码默认在严格模式下执行.
- Static:
StaticandRead-onlyimports: 模块是静态结构.- Imported module is
Pre-parsed: imported modules get run first, code which imports module gets executed after. - Imported module is
Read-only: code which imports module cannot modify imported module, only module which exports them can change its value.
- Imported module is
- Static analysis.
- Tree shaking.
- Compact bundling.
- Faster imports lookup.
<!-- 支持模块的浏览器会执行这段脚本 -->
<!-- 不支持模块的浏览器不会执行这段脚本 -->
<script type="module" src="module.js"></script>
<!-- 支持模块的浏览器不会执行这段脚本 -->
<!-- 不支持模块的浏览器会执行这段脚本 -->
<script nomodule src="script.js"></script>
ES6 Module Syntax
import * as Bar from './bar.js' // Object.freeze(Bar)
import module from './module.js'
import { lastName as surname } from './profile.js'
import './foo.js' // Load effects
export const firstName = 'Michael'
export const lastName = 'Jackson'
export const year = 1958
export function foo() {}
export function* bar() {}
export class Foo {}
// profile.js
const firstName = 'Michael'
const lastName = 'Jackson'
const year = 1958
export { firstName, lastName, year }
export { default as Article } from './Article'
// 接口改名
export { foo as myFoo } from 'node:module'
// 整体输出
export * from 'utils'
ES6 Module Imports
Import meta import.meta:
// index.mjs
import './index2.mjs?someURLInfo=5'
// index2.mjs
new URL(import.meta.url).searchParams.get('someURLInfo') // 5
const urlOfData = new URL('data.txt', import.meta.url)
Import assertion:
import data from './data.json' assert { type: 'json' }
console.log(data)
Import map importmap:
<script type="importmap">
{
"imports": {
"ms": "https://cdn.skypack.dev/ms"
"lodash": "https://cdn.skypack.dev/lodash",
"lodash": "https://cdn.skypack.dev/lodash/",
}
}
</script>
<script type="module">
import get from 'lodash/get.js'
import lodash from 'lodash'
import('lodash').then((_) => {})
</script>
Imports Order
- Polyfills:
import 'reflect-metadata';. - Node builtin modules:
import fs from 'node:fs';. - External modules:
import { motion } from 'framer-motion';. - Internal modules:
import { UserService } from 'src/services/userService';. - Parent directory modules:
import foo from '../foo'; import qux from '../../foo/qux';. - Same/Sibling directory modules:
import bar from './bar'; import baz from './bar/baz';.
ES6 Module Exports
- CommonJS 模块是运行时加载, ES6 模块是编译时输出接口.
- CommonJS 是单个值导出, ES6 Module 可以导出多个.
- CommonJS 是动态语法可以写在判断里, ES6 Module 是静态语法只能写在顶层.
- CommonJS 的
this是当前模块, ES6 Module 的this是undefined. - CommonJS 模块输出的是一个值的拷贝,
ES6 模块
export分多种情况:export default xxx输出value:defaultThingandanotherDefaultThingshows ES6 export default value,export xxx输出reference:importedThingandmodule.thingshows ES6 export live reference,Destructuringbehavior create a brand new value.- function/class special case:
export default function/class thing() {}; // function/class expressionsexport live reference,function/class thing() {}; export default thing; // function/class statementsexport default value.
Export default value:
// module.js
let thing = 'initial'
export { thing }
export default thing
setTimeout(() => {
thing = 'changed'
}, 500)
// main.js
import { default as defaultThing, thing } from './module.js'
import anotherDefaultThing from './module.js'
setTimeout(() => {
console.log(thing) // "changed"
console.log(defaultThing) // "initial"
console.log(anotherDefaultThing) // "initial"
}, 1000)
Export live reference:
// module.js
export let thing = 'initial'
setTimeout(() => {
thing = 'changed'
}, 500)
// main.js
import { thing as importedThing } from './module.js'
const module = await import('./module.js')
let { thing } = await import('./module.js') // Destructuring behavior
setTimeout(() => {
console.log(importedThing) // "changed"
console.log(module.thing) // "changed"
console.log(thing) // "initial"
}, 1000)
To sum up:
// Live reference:
import { thing } from './module.js'
import { thing as otherName } from './module.js'
// Current value:
const { thing } = await import('./module.js')
// Live reference:
export { thing }
export { thing as otherName }
export { thing as default }
export default function thing() {}
// Current value:
export default thing
export default 'hello!'