模块化简史
// 松耦合拓展// 这种方式使得可以在不同的文件中以相同结构共同实现一个功能块,且不用考虑在引入这些文件时候的顺序问题。// 缺点是没办法重写你的一些属性或者函数,也不能在初始化的时候就是用module的属性。var module = (function(my){// ...return my})(module || {})// 紧耦合拓展(没有传默认参数)// 加载顺序不再自由,但是可以重载var module = (function(my){var old = my.someOldFuncmy.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')// 定义Modulefunction Module(id){this.id = idthis.filename = idthis.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] = modulemodule.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.jsrequire(["helper/sum"], function(sum) {console.log('sum', sum)});// helper/sum.jsdefine(['helper/num_1', 'helper/num_2'], (num1, num2) => {return {sum: num1.num + num2.num}})// helper/multi.jsdefine(() => {return function (data) {return data * 2}})// helper/num_1.jsdefine(() => {return {num: 1}})// helper/num_2.jsdefine(['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] = resulthandlers.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 = falsebreak}}}// 加载依赖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 = namedeps = []name = null}if (Array.isArray(name) && typeof deps === 'function') {cb = depsdeps = namename = null}if (typeof name === 'string' && typeof deps === 'function') {cb = depsdeps = []}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

