模块化简史
// 松耦合拓展
// 这种方式使得可以在不同的文件中以相同结构共同实现一个功能块,且不用考虑在引入这些文件时候的顺序问题。
// 缺点是没办法重写你的一些属性或者函数,也不能在初始化的时候就是用module的属性。
var module = (function(my){
// ...
return my
})(module || {})
// 紧耦合拓展(没有传默认参数)
// 加载顺序不再自由,但是可以重载
var module = (function(my){
var old = my.someOldFunc
my.someOldFunc = function(){
// 重载方法,依然可通过old调用旧的方法...
}
return my
})(module)
commonjs
CommonJS标准囊括了JavaScript需要在服务端运行所必备的基础能力,比如: 模块化 IO操作 二进制字符串 进程管理 Web网关接口(JSGI)。
- 文件即模块 文件所有代码都运行在独立的作用域,因此不会污染全局空间。
- 模块可以被多次引用 加载。在第一次被加载时, 会被缓存,之后都从缓存种直接读取结果。
- 加载某个模块, 就是引入该模块的 module.exports属性
- module.exports 属性 输出的是 值的拷贝 ,一旦这个值被输出 模块内发生变化不会影响到输出的值
- 模块加载顺序按照代码引入的顺序。
- 注意 module.exports 和 exports的区别
CommonJS规范代码如何在浏览器端实现,其实就是实现 module.exports 和 require方法
let module = {}
module.exports = {}
(function(module, exports) {
// ...
}(module, module.exports))
手写CommonJS
浅谈前端模块化
const path = require('path')
const fs = require('fs')
const vm = require('vm')
// 定义Module
function Module(id){
this.id = id
this.filename = id
this.exports = {}
this.loaded = false
}
// 定义拓展与解析规则
Module._extensions = Object.create(null)
Module._extensions['.json'] = function(module){
return Module.exports = JSON.parse(fs.readFileSync(module.filename, 'utf8'))
}
Module._extensions['.js'] = function(module){
Module._compile(moudle)
}
// 包装函数
Module.wrap = function(script) {
return Module.wrapper[0] + script + Module.wrapper[1];
};
Module.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];
// 编译执行
Module._compile = function(module){
const content = fs.readFileSync(module.filename, 'utf8'), filename = module.filename;
const wrapper = Module.wrap(content)
const compiledWrapper = vm.runInThisContext(wrapper, {
filename: filename,
lineOffset: 0,
displayErrors: true,
})
const result = compiledWrapper.call(module.exports, module.exports, require, module, filename, dirname);
return result
}
// 缓存
Module._cache = Object.create(null)
Module.prototype.load = function(filename){
let extname = path.extname(filename)
Module._extensions[extname](this);
this.loaded = true;
}
// 加载
Module._load = function(filename) {
const cacheModule = Module._cache[filename]
if(cacheModule){
return cacheModule.exports
}
let module = new Module(filename)
Module._cache[filename] = module
module.load(filename)
return module.exports
}
// 简单的路径解析
Module._resolveFilename = function(path) {
let p = path.resolve(path)
if(!/\.\w+$/.test(p)){
let arr = Object.keys(Module._extensions)
arr.forEach(item => {
let file = `${p}${item}`
try{
fs.accessSync(file)
return file
}catch(e){
// ...
}
})
}else{
return p
}
}
// require 函数
function require(path){
const filename = Module._resolveFilename(path)
return Module._load(filename)
}
AMD
通过define方法 将代码定义为 模块。
通过require方法 实现代码模块加载。
记手写requirejs的思考过程
文件是怎么被加载的?
浏览器加载js文件只有一种方法:通过script标签。所谓文件加载,也即是构建script标签加到页面上。
demo-example
// text.html
// <script data-main="scripts/main" src="scripts/require.js"></script>
// main.js
require(["helper/sum"], function(sum) {
console.log('sum', sum)
});
// helper/sum.js
define(['helper/num_1', 'helper/num_2'], (num1, num2) => {
return {
sum: num1.num + num2.num
}
})
// helper/multi.js
define(() => {
return function (data) {
return data * 2
}
})
// helper/num_1.js
define(() => {
return {
num: 1
}
})
// helper/num_2.js
define(['helper/multi'], (multi) => {
return {
num: multi(3)
}
})
var require;
var define;
(function () {
var executedModule = {}; // key 为已执行的模块名称,value为执行后的值
var handlers = []; // 待依赖完成而执行的模块
var depsToName = {}; // 路径和名称对应表,key为路径,value为模块名
function checkIsAllLoaded(handle) {
if (!handle.deps.length) return true // 没有依赖
return handle.deps.every(dep => {
const moduleName = depsToName[dep]
return !!executedModule[moduleName] === true
})
}
function getArgsFromDepend(handle) {
const args = []
handle.deps.forEach(re => {
const moduleName = depsToName[re]
if (executedModule[moduleName]) {
args.push(executedModule[moduleName])
}
})
return args
}
function runHandles() {
handlers.forEach((handle, index) => {
const isDependLoaded = checkIsAllLoaded(handle)
if (isDependLoaded) {
const arg = getArgsFromDepend(handle)
var result = handle.fn(...arg)
executedModule[handle.name] = result
handlers.splice(index, 1)
runHandles()
}
})
}
function addNameToModule(urlName) {
for (let i = 0; i < handlers.length; i++) {
if (handlers[i].isLoading) {
if (!handlers[i].name) { //如果没有定义define名字
handlers[i].name = urlName
}
depsToName[urlName] = handlers[i].name // 将depUrl与moduleName关联起来
handlers[i].isLoading = false
break
}
}
}
// 加载依赖
function loadRequireModule(deps) {
for (let i = 0; i < deps.length; i++) {
const url = deps[i]
let script = document.createElement('script')
script.src = 'scripts/' + url + '.js'
script.setAttribute('data-name', url)
document.body.appendChild(script)
script.onload = (data) => {
const moduleName = data.target.getAttribute('data-name')
addNameToModule(moduleName) // 添加名字
runHandles() // 执行回调
}
}
}
require = (deps, cb) => {
if (typeof deps === 'function' && cb === undefined) {
cb = deps;
url = []
}
if (!Array.isArray(deps)) {
throw 'first argument is not array'
}
if (typeof cb !== 'function') {
throw 'second argument must be a function'
}
handlers.push({
name: 'main',
deps: deps,
fn: cb
})
loadRequireModule(deps)
}
// require define 都只是添加函数到 handlers 并执行以来下载
define = (name, deps, cb) => {
if (typeof name === 'function') {
cb = name
deps = []
name = null
}
if (Array.isArray(name) && typeof deps === 'function') {
cb = deps
deps = name
name = null
}
if (typeof name === 'string' && typeof deps === 'function') {
cb = deps
deps = []
}
handlers.push({
name: name,
deps: deps,
fn: cb,
isLoading: true // 这个字符标识的是当前正在加载的路径,用于为其添加名字时的定位
})
loadRequireModule(deps)
}
function startMain() {
const scripts = document.querySelectorAll('script');
for (let i = 0; i < scripts.length; i++) {
const script = scripts[i]
const attr = script.getAttribute("data-main");
if (attr) {
const mainScript = document.createElement('script')
mainScript.src = attr + '.js'
window.onload = () => {
document.body.appendChild(mainScript)
}
}
}
}
startMain()
})()
执行流程总结
- 获取html上 script -> scripts ->循环匹配获取 data-main attr = script.getAttribute(‘data-main’)-> 创建script加载 attr
- attr 其实就是 require([a,b],function(){}) 加载依赖 -> handlers.push({ name:’main’,deps:deps,fn:cb})
loadRequireModule(deps) 循环deps 创建script加载依赖。
- script.setAttribute(‘data-name’,url)
- script.onload moduleName = getAttribute(‘data-name’) 也就是 define 定义的模块
- define handlers.push({ name:name,deps:deps,fn:cb,isLoading:true}) -> loadRequire(deps)
- addNameToModule(moduleName) 循环 handlers 通过isLoading判断当前加载的模块 depsToName[urlName] = handlers[i].name handlers[i].isLoading = false
- runHandlers() 循环 handlers
- checkIsAllLoaded(handle)
- getArgsFromDepend(handle)
cmd
AMD和CMD的两个主要区别如下:
AMD需要异步加载模块,而CMD在require依赖的时候,可以通过同步的形式(require) 也可以通过异步的形式(require.async)
- CMD 遵循依赖就近的原则,AMD遵循依赖前置原则。也就是说 AMD中,我们需要把模块所需要的依赖都提前在依赖数组种声明。而在CMD中 我们只需要在具体代码逻辑内,使用逻辑前,把依赖的模块require进来。
umd
UMD 允许在环境中同时使用 AMD 与CommonJS规范,相当于一个整合。该模式的 核心思想 在于利用 立即执行函数根据环境来判断需要的参数类别(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD 规范
define(['b'], factory);
} else if (typeof module === 'object' && module.exports) {
// 类 Node 环境,并不支持完全严格的 CommonJS 规范
// 但是属于 CommonJS-like 环境,支持 module.exports 用法
module.exports = factory(require('b'));
} else {
// 浏览器环境
root.returnExports = factory(root.b);
}
}(this, function (b) {
// 返回值作为 export 内容
return {};
}));
es6module
ES模块的设计思想是尽量 静态化。 这样能保证在编译时就确定模块之间的依赖关系,每个模块的输入和输出变量也也都是确定的。CommonJS和AMD 无法保证前置即确定这些内容,只能在运行时确定。
CommonJS模块输出的是一个值的拷贝
ES模块输出的是值的引用。ES模块化为什么要设计成静态的
通过静态分析,我们能够分析出导入的依赖。如果导入的模块没有被使用,我们便通过 tree shaking等手段减少代码体积,从而提升运行性能。 这个就是基于ESM实现 tree shaking的基础。tree shaking