模块化是工程化的基础:只有能将代码模块化,拆分为合理单元,才能使其具备调度整合的能 力,才有架构和工程一说。
使用模块化的好处:
解决命名冲突
提供复用性
提高代码可维护性
到底什么是模块化?
简单来说就是,对于 一个复杂的应用程序,与其将所有代码一股脑儿地放在一个文件中,不如按照一定的语法,遵循确定的规则(规范)将其拆分到几个互相独立的文件中 。 这些文件应该具有原子特性,也就是说,其内部完成共同的或类似的逻辑,通过对外暴露一些数据或调用方法,与外部完成整合 。 这样一来,每个文件彼此独立,开发者更容易开发和维护代码,模块之间又能够互相调用和通信,这是现代化开发的基本模式 。
其实,不论是我们的日常生活还是其他科学领域,都离不开模块化的概念,它主要体现了可复 用性、可组合性 、 中心化 、 独立性等原则 。 在模块化的基础上结合工程化,又可以衍生出很多概念和话题,如基千模块化的 treeshaking技 术、模块循环加载的处理等 。 不过不要着急 , 我们先来看一下前端模块化的发展历程 。
在早期,实现模块化最常见的手段就是通过立即执行函数(IIFE) ,构造一个私有作用域,再通过闭包(从某种角度上看,闭包简直就是一个天生解决数据访问性问题的方案),将需要对外暴露的数据和接口输出。我们称之为IIFE 模式
const module = (function(){
// ... 声明各种变量、函数都不会污染全局作用域
var foo = 'bar'
var fn1 = function (){
// ...
}
var fn2 = function (){
// ...
}
return {fn1, fn2}
})()
我们在调用 module 时,如果想要访问没暴露的变量 foo,是访问不到具体数据的。
了解了这种模式,我们就可以在此基础上结合顶层 window 对象进行实现模块化的初级功能。
(function(window){
var data = 'data'
function foo(){
console.log(`foo executing, data is ${data}`)
}
function bar(){
data = 'modified data'
console.log(`bar executing, data is now${data}`)
}
window.module1 = {foo, bar}
})(window)
数据 data 完全做到了私有,外界无法修改 data 值。 那么如何访问 data 呢?这时就需要模块内部设计并暴露相关接口。上述代码只需要调用模块 module! 暴露给外界 (window) 的函数即可:module1.foo()。修改 data值的途径,也只能由模块 moduleI 提供:module1.bar()。
进一步思考,如果 module} 依赖外部模块 module2(jQuery),该怎么办?
(function(window, $){
var data = 'data'
function foo(){
console.log(`foo executing, data is ${data}`)
console.log($)
}
function bar(){
data = 'modified data'
console.log(`bar executing, data is now${data}`)
}
window.module1 = {foo, bar}
})(window, jQuery)
事实上,这就是现代模块化方案的基石。至此,我们经历了模块化的第一阶段: “假“模块化 时代。这种实现极具阿 Q 精神,它并不是语言原生层面上的实现,而是开发者利用语言,借助 JavaScript 特性,对类似的功能进行了模拟,为后续方案打开了大门。
CommonJS 规范最早是 Node 独有的规范,目前也仍然广泛使用,比如在 Webpack 中就能见到它。浏览器中使用需要用到Browserify
解析。 Node 在实现中并非完全按照规范实现,而是对模块规范进行了一定的取舍,同时也增加了少许自身需要的特性。 CommonJS 对模块的定义十分简单,主要分为模块引用
、模块定义
和模块标识
3 个部分。
1. 模块引用 在 CommonJS 规范中,存在require()
方法,这个方法接受模块标识,以此引入一个模块的 API 到当前上下文中。var math = require('math');
2. 模块定义 在模块中,对应引入的功能,上下文提供了exports
对象用于导出当前模块的方法或者变量,并且它是唯一导出的出口。
module
对象,它代表模块自身,而exports
是module
的属性。exports
对象上作为属性即可定义导出的方式。加载某个模块,其实就是引入该模块的module.exports
属性。module.exports
属性输出的是值的拷贝,一旦这个值被输出 ,模块内再发生变化也不会影响 到输出的值 。 // a.js
module.exports = {
a: 1
}
// or
exports.a = 1
// b.js
var module = require('./a.js')
module.a // -> log 1
// 文件即模块,文件内的所有代码都运行在独立的作用域中,因此不会污染全局空间
// 这里其实就是包装了一层立即执行函数
module.exports
和exports
很容易混淆,可点击展开查看内部大致实现。 var module = {
id: 'xxxx', // 我总得知道怎么去找到它吧
exports: {} // exports 就是个空对象
}
// 这行代码是为什么 exports 和 module.exports 用法相似的原因
var exports = module.exports
var load = function (module) {
// 导出的东西
var a = 1
module.exports = a
return module.exports
};
// 当 require 的时候去找到独特的 id,然后将要使用的东西用立即执行函数包装下,over
重要的是 module 这里,module 是 Node 独有的一个变量
另外虽然两者用法相似,但是不能对 exports
直接赋值,不会有任何效果。
因为
var exports = module.exports
这句代码表明了exports
和module.exports
享有相同地址,通过改变对象的属性值会对两者都起效,但是如果直接对exports
赋值就会导致两者不再指向同一个内存地址,修改并不会对最终返回的module.exports
起效。
3.模块标识 模块标识其实就是传递给require()
方法的参数,它必须是符合小驼峰命名的字符串,或者以.
、..
开头的相对路径,或者绝对路径。它可以没有文件名后缀.js
。模块的定义十分简单,接口也十分简洁。它的意义在于将类聚的方法和变量等限定在私有的作用域中,同时支持引入和导出功能以顺畅地连接上下游依赖。
AMD 和 CMD
目前这两种实现方式已经过时,只需要了解这两者是如何使用的即可
AMD:
AMD 规范是 CommonJS 模块规范的一个延伸,它的全称是 Asynchronous Module Definition,即“异步模块定义”。按照该标准加载模块时是异步的,这种标准是完全适用于浏览器的 。
define(id?,dependencies?,factory);
// 模块 id 和 依赖 是可选的,与 Node 模块相似的地方在于 factory 的内容就是实际代码的内容
下面的代码定义了一个简单的模块:
define(['./a', './b'], function(a, b) {
var exports = {};
// 加载模块完毕可以使用
a.do()
b.do()
exports.sayHelloFromA = function() {
alert('hello from module:' + a.id)
};
return exports;
})
不同之处在于 AMD 模块需要用define
来明确定义一个模块,而在 Node 实现中是隐式包装的。
它们的目的是进行作用域隔离,仅在需要的时候被引入,避免掉过去那种通过全局变量或者全局命名空间的方式,以免变量污染和不小心被修改。另一个区别则是内容需要通过返回的方式实现导出。
CMD:
CMD 规范由国内的玉伯提出,与 AMD 规范的主要区别在于定义模块和依赖引入的部分。
也就是说,在 AMD 中,我们需要把模块所需要的依赖都提前声明在依赖数组中,然后通过形参传递依赖到模块内容中:
define(['dep1','dep2'], function(dep1, dep2){
return function() {};
});
而 CMD 中,支持动态引入。将 require
、exports
和module
通过形参传递给模块,然后在具体代码逻辑内,在使用依赖模块前,随时调用require()
引入依赖的模块即可 。
define(function(require, exports, module){
// 加载模块
// 可以把 require 写在函数体的任意地方实现延迟加载
var a = require('./a')
a.doSomething()
})
在有 Babel 的情况下,我们可以直接使用 ES6 原生实现的模块化方案 ES Module,最后也会编译成require/exports
// file1.js
export function a() {}
export function b() {}
// file2.js
export default function() {}
// 引入模块
import {a, b} from './file1.js'
import XXX from './file2.js'
ES模块化导出有 export 和 export default 两种。两种导出模块的方式不同,在另一个模块中引用的方式也不一样。这里建议减少使用 export default 导出。
CommonJS 和 ES Module 的区别?
ES 模块为什么要设计成静态的?
将 ES 块设计成静态的, 一个明显的优势是:通过静态分析,我们能够分析出导入的依赖。 如果导入的模块没有被使用,我们便可以通过 tree shaking 等手段减少代码体积,进而提升运行性能。这就是基于 ESM 实现 tree shaking 的基础 。
这么说可能过于笼统,下面从设计的角度分析这种规范的利弊 。 静态性需要规范去强制保证,因此 ES 模块规范不像 CommonJS 规范那样灵活,其静态性会带来如下一些限制。
这样的限制在语言层面带来的便利之一是,我们可以通过分析作用域,得出代码中变量所属的作用域及它们之间的引用关系,进而可以推导出变量和导入依赖变量之间的引用关系,在没有明显引用时,可以对代码进行去冗余 。
Babel
babel-handbook/plugin-handbook.md at master · jamiebuilds/babel-handbook
babel 本质就是编译器,它的转译过程分为三个阶段:
如何维护大型项目的 z-index,如何维护 CSS 选择器和样式之间的冲突 ?
CSS Modules 是指:项目中的所有 class 名默认都是局部起作用的。 其实, CSS Modules 并不是一个官方规范,更不是浏览器的机制 。 因为它依赖我们的项目构建过程,因此基于它的实现往往需要借助 webpack或其他构建工具,将 class 名唯一化,从而使其只在局部起作用。
.style_test_1923235023 { color: red; }
<div class="style_test_1923235023">This is a test.</div>
其中, class 名是动态生成的,在整个项目中这个名字是唯一的。通过命名规范的唯一性,达到了避免样式冲突的目的。不过,这样的解决方案似乎有一个问题:如何实现样式复用?
因为生成了全局唯一的 class 名,所以我们如何像传统方式那样实现样式复用呢?
从原理上想 ,全局唯一的 class 名是在构建过程中生成的,所以如果能够在构建过程中进行标识,表示该 class 将被复用,就可以解决问题了。
这样的方式需要依靠composes
关键字实现:
.common {
color: red;
}
.title {
composes: common;
font-size: 24px;
}
使用 composes 关键字在 title 中关联了 common 样式
<div
class="_style_title_09082423 _style_commin_23230082">
This is a test.
</div>
div 的 class 中加入了 _style_commin_23230082,这样就实现了样式复用
前端路由实现起来其实很简单,本质就是监听 URL 的变化,然后匹配路由规则,显示相应的页面,并且无须刷新。目前单页面使用的路由就只有两种实现方式。
www.test.com/##/
就是 Hash URL
,当 ##
后面的哈希值发生变化时,不会向服务器请求数据,可以通过 hashchange
事件来监听到 URL
的变化,从而进行跳转页面。
History
模式是 HTML5
新推出的功能,比之 Hash URL
更加美观。
随若业务复杂度的直线上升,前端项目不管是从代码量上,还是从依赖关系上都呈爆炸式增长。同时,由于团队中一般不止有一个业务项目,所以“多个项目之间如何配合”、“如何维护相互关系”、“公司自己的公共库版本如何管理”,这些问题随着业务扩展纷纷浮出水面。 一名合格的高级前端工程师,必需能在宏观上妥善处理这些问题 。
这就是项目代码在组织上的不同哲学: 一种倡导分而治之, 一种倡导集中管理 。 究竟是把鸡蛋 放在同一个篮子里,还是倡导多元化,这就要根据团队的风格及面临的实际场景进行选型了 。
multirepo 存在以下问题:
而 monorepo 缺点也非常明显,具体如下:
资料