模块化 早期的 JavaScript 往往作为嵌入到 HTML 页面中的用于控制动画与简单的用户交互的脚本语言,我们习惯这样写:
1 2 3 4 5 <script type ="application/javascript" > </script >
所有的嵌入到网页内的 JavaScript 对象都会使用全局的 window
对象来存放未使用 var
定义的变量。这就会导致一个问题,那就是,最后调用的函数或变量取决于我们引入的先后顺序。
随着单页应用与富客户端的流行,不断增长的代码库也急需合理的代码分割与依赖管理的解决方案,这也就是我们在软件工程领域所熟悉的模块化(Modularity)
。
什么是模块化
简而言之,模块化就是将一个大的功能拆分为多个块,每一个块都是独立 的,你不需要去担心污染 全局变量,命名冲突 什么的。
模块化的好处 :
封装功能
封闭作用域
可能解决依赖问题
工作效率更高,重构方便
解决命名冲突
…
JS模块化方案
全局function模式 :将不同的功能封装成不同的全局函数
1 2 3 4 5 6 7 function m1 ( ) { } function m2 ( ) { }
问题 :污染全局命名空间,容易引起命名冲突或数据不安全,而且模块成员之间看不出直接关系。
namespace模式 :简单对象封装,减少了全局变量,解决命名冲突
1 2 3 4 5 6 7 8 9 10 11 12 13 14 let greeting = { helloInLang: { en: 'Hello world!' , es: '¡Hola mundo!' , ru: 'Привет мир!' }, sayHello: function (lang ) { return helloInLang[lang] } } greeting.helloInLang.en = 'hello' greeting.sayHello('en' )
问题 :数据不安全(外部可以直接修改模块内部的数据)。
IIFE模式 :匿名函数自调用(闭包),数据是私有的, 外部只能通过暴露的方法操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 (function (window ) { var helloInLang = { en: 'Hello world!' , es: '¡Hola mundo!' , ru: 'Привет мир!' } function sayHello ( ) { return helloInLang[lang] } function otherFun ( ) { console .log('otherFun()' ) } window .greeting = { sayHello } })(window )
1 2 3 4 5 6 7 8 // index.html文件 <script type ="text/javascript" src ="lib/greeting.js" > </script > <script type ="text/javascript" > greeting.sayHello('en' ) console .log(greeting.helloInLang) greeting.helloInLang = 'xxxx' greeting.sayHello('en' ) </script >
问题 :无法解决模块依赖问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 (function (window, $ ) { var helloInLang = { en: 'Hello world!' , es: '¡Hola mundo!' , ru: 'Привет мир!' } function sayHello ( ) { $('body' ).css('background' , 'red' ) return helloInLang[lang] } function otherFun ( ) { console .log('otherFun()' ) } window .greeting = { sayHello } })(window , jQuery)
1 2 3 4 5 6 7 // index.html文件 <script type ="text/javascript" src ="jquery-1.10.1.js" > </script > <script type ="text/javascript" src ="lib/greeting.js" > </script > <script type ="text/javascript" > greeting.sayHello('en' ) </script >
CommonJS :主要是应用在Nodejs服务端,属于动态同步加载 。
1 2 3 4 5 6 7 8 9 10 11 12 var helloInLang = { en: 'Hello world!' , es: '¡Hola mundo!' , ru: 'Привет мир!' } module .exports = { sayHello: function (lang ) { return helloInLang[lang] } }
1 2 3 4 5 const greeting = require ('./lib/greeting.js' )var phrase = greeting.sayHello('en' )document .write(phrase)
AMD && CMD :AMD是RequireJS
提出的,主要是依赖前置 。CMD是SeaJS
提出的,主要是就近依赖(只要用到才会导入),两者用法接近。属于异步加载 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 define(function ( ) { var helloInLang = { en: 'Hello world!' , es: '¡Hola mundo!' , ru: 'Привет мир!' } return { sayHello: function (lang ) { return helloInLang[lang] } } })
1 2 3 4 5 define(['./lib/greeting' ], function (greeting ) { var phrase = greeting.sayHello('en' ) document .write(phrase) })
UMD :因为AMD中无法使用CommonJS,所以出来了一个UMD,可在UMD中同时使用AMD和CommonJS
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 (function (define ) { define(function ( ) { var helloInLang = 'hello' return { sayHello: function (lang ) { return helloInLang[lang] } } }) }( typeof module === 'object' && module .exports && typeof define !=='function' ? function (factory ) { module .exports = factory() } : define ))
ESM(ES Module) :ES6新规范,JavaScript终于在语言标准的层面上,实现了模块功能,使得在编译时就能确定模块的依赖关系,以及其输入和输出的变量,属于静态加载(编译时加载) 。
1 2 3 4 5 6 7 8 9 10 const helloInLang = { en: 'Hello world!' , es: '¡Hola mundo!' , ru: 'Привет мир!' } export const sayHello = (lang ) => { return helloInLang[lang] }
1 2 3 4 import { sayHello } from './lib/greeting' sayHello('en' )
问题 :目前浏览器和Nodejs的支持程度都并不理想,需要使用额外的工具如Babel编译成ES5,才可以在浏览器和Nodejs中运行。
Webpack打包机制 由于模块化的原因,我们不得不处理不同模块的依赖关系。随着项目越来越庞大,这种关系会变得越来越难以维护。为了方便开发和维护,我们就会使用到打包工具webpack。
webpack可以根据模块的依赖关系进行静态分析,然后将这些模块按照指定的规则生成对应的静态资源。那么,它究竟是如何处理这些依赖关系的呢 ?
使用webpack打包 新建一个简单的项目,目录下包含这样几个文件:
1 2 3 4 5 src ├── big.js ├── helloWorld.js ├── index.js └── lazy.js
1 2 3 4 5 6 7 8 9 10 11 12 import helloWorld from './helloWorld' const node = document .createElement("div" ) node.innerHTML = helloWorld + 'loading...' import ( './lazy' ) .then(({ default : lazy } ) => { node.innerHTML = helloWorld + lazy }) document .body.appendChild(node)
1 2 3 4 5 6 import big from './big' const helloWorld = big('hello world!' )export default helloWorld
1 2 3 4 5 6 import big from './big' const lazy = big("lazy loaded!" )export default lazy
1 2 3 4 export default (val) => { return val && val.toUpperCase() }
Step1:划分模块,组成模块队列(queue) 从以上代码中我们可以观察到每个模块之间的引用关系:
1 2 3 4 5 6 7 8 9 10 11 12 * src/index.js (ESM) # ./helloWorld # (async) ./lazy - src/helloWorld.js - (async) src/lazy.js * src/helloWorld.js (ESM) # ./big - src/big.js * src/big.js * src/lazy.js (ESM) # ./big - src/big.js
使用webpack打包后的结果:
我们可以观察一下打包生成的main.js
里的内容,这里的代码比我们的源代码多了许多乱七八糟的东西,但是我们可以将它简化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 (function (modules ) { }) ({ "./src/big.js" : (function (module, __webpack_exports__, __webpack_require__ ) { }), "./src/index.js" : (function (module, __webpack_exports__, __webpack_require__ ) { }) });
简化后我们可以发现,main.js
里的内容本质上是一个立即执行函数,这个函数的参数便是我们模块代码(队列):
1 2 3 4 5 6 7 (function (modules ) { })({ [moduleId]: function ( ) { } })
Step2:每个模块import和export改写 由于在webpack中配置了concatenateModules: true
(作用域提升),index.js
与helloWorld.js
模块合并成了一个文件。查看模块-id键值
中的function内容可以发现,webpack对模块内的import与export进行了改写:
1 2 3 4 5 6 (function (__require__, exports ) { exports.default = (val ) => { return val && val.toUpperCase() } })
1 2 3 4 5 6 7 8 9 10 11 12 13 (function (__require__, exports ) { const big = __require__('./src/big.js' ) const helloWorld = big.default('hello world' ) const node = document .createElement("div" ) node.innerHTML = helloWorld + 'loading...' document .body.appendChild(node) })
1 2 3 4 5 6 (function (__require__, exports ) { const big = __require__('./src/big.js' ) const lazy = big.default('lazy loaded!' ) exports.default = lazy })
Step3:实现__require__和export逻辑 我们知道import需要具备以下功能:
执行目标模块代码
导出目标模块的export内容给外部使用
因此__require__
与export
需要具备以上功能,使代码能在单文件中相互引用并执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 !(function (modules ) { function __require__ (id ) { if (cache[id]) return cache[id].exports var module = { exports: {} } modules[id](__require__, module .exports, module ) cache[id] = module return module .exports } })({ })
Step4:把处理好的模块作为参数传进IIFE 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 !(function (modules ) { function __require__ (id ) { if (cache[id]) return cache[id].exports var module = { exports: {} } modules[id](__require__, module .exports, module ) cache[id] = module return module .exports } })({ './src/big.js' : (function (__require__, exports ) { exports.default = (val ) => { return val && val.toUpperCase() } }), './src/index.js' : (function (__require__, exports ) { const big = __require__('./src/big.js' ) const helloWorld = big.default('hello world' ) const node = document .createElement("div" ) node.innerHTML = helloWorld + 'loading...' document .body.appendChild(node) }) })
异步加载的模块 1 2 3 4 5 6 __require__.loadChunk('async' ) .then(__require__.bind(null , './src/lazy.js' )) .then(function (_ref ) { var lazy = _ref.default node.innerHTML = helloWorld + lazy })
1 2 3 4 5 6 7 8 9 10 11 let chunkResolves = {}__require__.loadChunk = function (chunkId ) { return new Promise (resolve => { chunkResolves[chunkId] = resolve let srcipt = document .createElement('script' ) script.src = 'src/' + chunkId + '.js' document .head.appendChild(script) }) }
1 2 3 4 5 6 7 8 window .webpackJsonpCallback ('async' , { './src/lazy.js' : (function (__require__, exports ) { const big = __require__('./src/big.js' ) const lazy = big.default('lazy loaded!' ) exports.default = lazy }) })
1 2 3 4 5 6 7 8 let chunkResolves = {}window .webpackJsonpCallback = function (chunkId, newModules ) { for (const id in newModules) { modules[id] = newModules[id] chunkResolves[chunkId]() } }
编写迷你打包程序 了解了webpack的打包步骤,我们可以模拟以上过程编写出完整的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 function createAsset (filename ) {}function createGraph (entry ) {}function bundle (graph ) {}const graph = createGraph('./src/index.js' )const result = bundle(graph)console .log(result)
所需依赖工具:
@babel/parser:js解析器,将文本代码转化成AST(语法树)
@babel/traverse:遍历AST寻找依赖关系
@babel/core的transformFromAst:将AST代码转化成浏览器所能识别的代码(ES5)
createAsset 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 function createAsset (filename ) { const content = fs.readFileSync(filename, 'utf-8' ) const ast = parser.parse(content, { sourceType: 'module' }) const dependencies = [] traverse(ast, { ImportDeclaration: ({ node } ) => { dependencies.push(node.source.value) } }) const id = ID++ const { code } = transformFromAst(ast, null , { presets: ['@babel/env' ], }) return { id, filename, dependencies, code } }
createGraph 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function createGraph (entry ) { const mainAsset = createAsset(entry) const queue = [mainAsset] for (const asset of queue) { asset.mapping = {} const dirname = path.dirname(asset.filename) asset.dependencies.forEach(relativePath => { const absolutePath = path.join(dirname, relativePath) + '.js' const child = createAsset(absolutePath) asset.mapping[relativePath] = child.id queue.push(child) }) } return queue }
bundle 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 function bundle (graph ) { let modules = '' graph.forEach(mod => { modules += `${mod.id} : [ function (require, module, exports) { ${mod.code} }, ${JSON .stringify(mod.mapping)} ],` }) const result = ` (function (modules) { function require(id) { const [fn, mapping] = modules[id] function localRequire (name) { return require(mapping[name]) } const module = { exports: {} } fn(localRequire, module, module.exports) return module.exports } require(0) })({ ${modules} }) ` return result }
项目地址:mini-pack
参考文章: