文章内容输出来源:拉勾教育大前端高薪训练营一期
我算是一个工作年限比较久的前端了,刚毕业时从事销售工作,后来在2015年想要从事脚踏实地的工作,便选择转行互联网。当时转行时我和我同学选择了不同的道路,她选择参加ios培训而我选择自学。刚入行时因为没有经验,所以拿着月薪1.5K的薪水做着很繁杂的工作,前端、php、测试、产品、需求都ok。但没有一个是我最擅长。忙碌的生活让我逐渐意识到我或许走的不会很长久,我在2017年惊醒便放弃了天津的2K薪资来到了北京。北京公司是个小公司,倒是成长很快但技术方面没有什么提升。在2018年进入目前这家公司之后,因为机缘巧合的原因带了小团队,但我越发恐慌,我好想并不能带给她们什么,也不能带领她们走向某一个方向,当我觉得自己’技’不配位的时候恰巧看到了拉勾的宣传,莫名的缘分让我成为了拉勾教育大前端高薪训练营一期的学员。
成为学员之后,刚开始还蛮焦虑的。早已习惯懒散的生活和规律的生活因为加入了拉勾而变得异常充实和快乐,每次看视频、听直播、看群内分享的时候就可以感知到原来自己太局限,我一个工作5年经验的人竟然还在做初级工程师的工作。以上种种让我很羡慕班里那些工作一年的小伙伴,她们的前途一片光明。虽然我也是。
从加入大前端高薪训练营到现在快3个月了,我对拉勾的课程质量绝对是非常满意了。之前也在其他网站陆续买过一些课程,从我的认知中拉勾是最全面的、最细致的一家。很多工作中我们不曾深入了解的工具、概念、实践,老师都会在课程中带领我们实践一次,真乃实践出真知呀。课程体系划分合理、课程内容充实且全面、老师知识储备超级丰富、讲解够细致,我想一定是经过了很长时间的打磨才会有如此多的好评。
授课的主讲老师是汪磊,我github唯一关注的粉丝(其实说明我太菜了),我这几年的工作经历也算见过一些大场面,但还是被汪老师的博学多识、幽默帅气所吸引。之前还特意关注过汪老师的微博,发现汪老师一直从事互联网教育培训方向,想来他的经验、学识、学习能力之所以如此优秀也是有原因的。持续深耕方可有今天的细致与宽广。
我们的班主任,一个细心且温柔且负责任的美少女,其实很多时候我都要作业要不明天再写吧(原谅我经常想偷懒),但老师常常会按时敲门来督促我们学习。我有想过,如果没人监督没人督促,我可能1个月都无法坚持。我的班主任总是晚上2点睡早晨6-7点起床,或许是我太不给力了。这些都是很细碎的点,但也正因为有她的陪伴、支持、鼓励,我才发现我现在可以逐渐摆脱之前不好的习惯,如果每天不学习我甚至会觉得她能感知到。现在真的变成了每天都要学习、每天都要记笔记的好习惯。
我们的助教老师熊熊老师和小北老师,常常在深夜或很早的时候就会回复我们的问题,每次的作业批改绝对是一丝不苟,我为什么会有这样的感触?因为有一次我的作业漏写了一道题老师还很认真的评价及解答_。
总体来说,对主讲老师汪磊,心存敬意,因为尊敬也让我更加认真的对待每一节视频、每一次直播。对班主任,心存感恩,希望自己更自律可以让她早点休息。对助教老师心存感激与敬佩,他们对待工作的态度也是我这个职场老人应该学习的。
最后,再来说一下故事开头中我那位女同学的发展之路,在我持续3年拿月薪2K的时候,她早已月薪20K。其实我想说,成长有很多条路,真正走过(浪费)了这几年之后我才发现我在最开始入行的时候应该选择最快捷的那条路。不过现在的我已经在路上了,希望不会太晚。同时也希望各位小伙伴可以找到适合自己的学习方法。
以下这篇博客内容会比较多,建议大家重点关注前言、CommonJs、ES Module即可。后续还会有系列文章,欢迎大家多多捧场~
模块化是一种思想,是一种解决问题的思路。
随着业务的发展我们的系统可能越来越复杂,那我们如何保证在复杂系统中代码可以方便维护、功能可以复用呢?模块化思想可以解决这个问题呀,可以将我们的复杂系统分解为可管理的模块、每个模块完成一个特定的子功能,所有的模块按某种方法组装起来,成为一个整体,完成整个系统所要求的功能。
我们
模块化只是一种思想
早期在没有工具和规范的情况下对模块化的落地方式:
以上模块化方式都是通过scrip手动t引入模块。模块的引入与否取决于人也就是不受代码控制,如果我们引入了模块但未使用、使用了但模块被删除等都会对程序产生影响。
如何通过代码自动加载模块?需要一个模块化标准和自动加载模块的基础库
CommonJs,AMD,CMD,ES Module这四种
首先登场的是commonJS,commonJS是一套规范、它约定实现模块化要遵守的规范,它是nodejs在服务端遵守的规范。
简单的描述就是:
每个文件就是一个模块,每个模块都有一个module对象,代表当前模块。它有以下属性:
module.exports 表示模块对外输出的值。
我们新建一个文件aa.js,此文件内容为
console.log(module)
然后执行node aa.js 可以看到打印的结果如下:
我们在上一节也说过module对象下有一个exports属性,表示模块对外输出的值,这样别的模块就可以引入此值。它输出的是一个对象。
// 创建文件aa.js
let a = '模块内的变量a'
function add(x,y){
console.log('模块内的求和函数')
return x+y
}
module.exports.a = a
module.exports.add = add
// 或者简写为
module.exports = {
a,
add:add,
d: function(x,y){
return x
}
}
利用 module.exports.name = 值,可以导出,导入的时候是一个利用moduleName.name即可接收
// 创建文件bb.js,导入aa.js
let moduleAA = require('./aa.js')
console.log(moduleAA.a)
console.log(moduleAA.add(3,4))
执行node bb可看到效果如下:
我发觉,如果是变量常量对象(不包括函数)这一类型的,都需要先定义再导出,函数可以先定义再导出也可以导出的时候直接定义。如下:
module.exports.c = function(x,y){
return x+y
}
为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部都有了一行隐形的命令:let exports = module.exports。 所以我们在对外输出或暴露的时候就看可以利用exports.name = xx进行暴露。
上述示例中暴露全可以简写为:
let a = '模块内的变量a'
function add(x,y){
console.log('模块内的求和函数')
return x+y
}
exports.a = a
exports.add = add
exports.c = function(x,y){
return x+y
}
简单理解exports可以说与module.exports指向同一内存地址,属于浅拷贝过程,任何一方的修改都会导致另一方可以感知到变化。最终导出的时候将内存地址里的值导出即可。
但有一点需要注意,我们不能使用直接赋值的形式,这样会切断exports的指向。
如下:
exports = {
name: '呼呼',
age: 18
}
或
exports = function(x){
return x
}
此时已经切断了两者的联系,并不能被导出。真正被导出的还是module.exports指向的内存地址。
所以我们建议使用module.exports而不用exports,避免出错
require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错。
默认加载.js 文件,所以我们在加载模块时可以省略.js后缀
根据参数的不同格式,require命令去不同路径寻找模块文件。
如果参数字符串以“/”开头,则表示加载的是一个位于绝对路径的模块文件。比如,require(’/home/marco/foo.js’)将加载/home/marco/foo.js。
如果参数字符串以“./”开头,则表示加载的是一个位于相对路径(跟当前执行脚本的位置相比)的模块文件。比如,require(’./circle’)将加载当前脚本同一目录的circle.js。
如果参数字符串不以“./“或”/“开头,则表示加载的是一个默认提供的核心模块(位于Node的系统安装目录中),或者一个位于各级node_modules目录的已安装模块(全局安装或局部安装)。
如果参数字符串不以“./“或”/“开头,而且是一个路径,比如require(‘example-module/path/to/file’),则将先找到example-module的位置,然后再以它为参数,找到后续路径。
如果指定的模块文件没有发现,Node会尝试为文件名添加.js、.json、.node后,再去搜索。.js件会以文本格式的JavaScript脚本文件解析,.json文件会以JSON格式的文本文件解析,.node文件会以编译后的二进制文件解析。
如果想得到require命令加载的确切文件名,使用require.resolve()方法。
我们之前看module输出的时候有一个path属性,表示模块加载顺序。
如果我们在bb.js中输出module会发现path的顺序为:
paths: [
‘D:\webStudy\vue-router\node_modules’,
‘D:\webStudy\node_modules’,
‘D:\node_modules’
]
假设我们引入了一个三方模块require(‘vue’),则它的查找顺序为先从"D:/webStudy/vue-router/node_modules"中查找,如果找不到向上一层到 “D:/webStudy/node_modules” 查找,如果还找不到则会从’D:\node_modules’查找。这样设计的目的是,使得不同的模块可以将所依赖的模块本地化。
第一次加载某个模块时,Node会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的module.exports属性。
所有缓存的模块保存在require.cache之中,如果想删除模块的缓存,可以像下面这样写。
// 删除指定模块的缓存
delete require.cache[moduleName];
// 删除所有模块的缓存
Object.keys(require.cache).forEach(function(key) {
delete require.cache[key];
})
注意,缓存是根据绝对路径识别模块的,如果同样的模块名,但是保存在不同的路径,require命令还是会重新加载该模块。
假设我们的b引入了a暴露的数据或函数,模块内部的变化就影响不到这个值。可以理解为两者是深拷贝,也就是相互独立。
所以我们会发现一个模块被多次引入并不会被执行多次。
commonjs是一个文件就是一个模块,每个模块是如何做到作用域相互独立的呢?事实上每个文件都是很一个函数,我们的代码逻辑都是在函数内部。
如何验证呢?
我们可以打印arguments,只有函数内部才有的参数列表,如下图:
这个函数有5个参数:
(function (exports, require, module,__filename, __dirname) {
});
commonJS是以同步的方式加载模块,node是在启动时加载模块,在执行过程中不会再次进行加载。但如果在浏览器端使用commonJS、页面加载都会导致大量的同步请求出现,性能比较低下。故而为浏览器重新设计了一个规范AMD,也就是异步模块定义规范,其中require.js库实现了AMD。
AMD(Asynchronous Module Definition)异步模块定义,它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
主要作用有2个:
define(id?, dependencies?, factory);
// 定义模块math.js
define(function (){
var add = function (x,y){
return x+y;
};
return {
add: add
};
});
// main.js引入math模块
require(['math'], function (math){
alert(math.add(1,1));
});
require(
['jquery', 'underscore','backbone'],
function ($, _, Backbone){
// some code here
});
require()函数接受两个参数。第一个参数是一个数组,表示所依赖的模块,上例就是[‘moduleA’, ‘moduleB’, ‘moduleC’],即主模块依赖这三个模块;第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用。加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块。
require()异步加载moduleA,moduleB和moduleC,浏览器不会失去响应;它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。
CMD(Common Module Definition)通用模块定义,对应SeaJS,是阿里玉伯团队首先提出的概念和设计。跟requireJS解决同样问题,只是运行机制不同。
define(id?, deps?, factory)
define(function(require, exports, module) {
// 通过 require 引入依赖
var otherModule = require('./otherModule');
// 通过 exports 对外提供接口
exports.myModule = function () {};
// 或者通过 module.exports 提供整个接口
module.exports = function () {};
})
define(function(require, exports, module) {
// 通过 require 引入依赖
var math = require(‘math’);
return math.add(1,2)
})
- | AMD | CMD |
---|---|---|
原理 | define(id ?,dependencies ?,factory)定义了一个单独的函数“define”。id为要定义的模块。依赖通过dependencies传入factory是一个工厂参数的对象,指定模块的导出值。 | CMD规范与AMD类似,并尽量保持简单,define(id?, deps?, function(require, exports, module){}) |
优点 | 特别适用于浏览器环境的异步加载 ,且可以并行加载。依赖前置,提前执行。 定义模块时就能清楚的声明所要依赖的模块 | 依赖就近,延迟执行。 按需加载,需要用到时再require |
缺点 | 开发成本较高,模块定义方式的语义交为难理解,不是很符合通过的模块化思维方式 | 依赖SPM打包,模块的加载主观逻辑交重。 |
体现 | require.js | sea.js |
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代现有的 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
通过给页面中的script添加一个type=module属性,就可以以ES Module的标准来执行该模块了。
ES Module特性:
//也就是需要我们引入网址支持跨域访问,百度不支持跨域,所以引入外部资源时会报跨域错误
ES Module主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。下面是一个 JS 文件,里面使用export命令输出变量。
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
或者
let firstName = 'Michael'
let lastName = 'Jackson'
let year = 1958
export {
firstName,
// es6新语法,对象中key与value相同时可简写即key:key=》key
lastName,
year
}
function f1(){
console.log('f1')
}
function f1(){
console.log('f2')
}
export {f1,f2}
function f1(){
console.log('f1')
}
function f1(){
console.log('f2')
}
export {
f1 as add,
f2 as hh,
f2 as hell
}
需要注意的有2点:
导出方式一:
export let firstName = 'Michael'
export lastName = 'Jackson'
export function f1(){
console.log('f1')
}
或者采用导出方式二
let firstName = 'Michael'
let lastName = 'Jackson'
function f1(){
console.log('f1')
}
export {
firstName,
lastName,
f1
}
但如果我们先定义变量、常量、函数、class,然后再export 变量名/常量名/函数名/class名则会报错,原因在于export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。let firstName = 'Michael'
export firstName
结果会报错
使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。
import { firstName, lastName } from './profile.js';
import { firstName as xing } from './index.js'
import {a} from './xxx.js'
a = {}; // Syntax Error : 'a' is read-only;
a.foo = 'hello'; // 合法操作
from后可以是绝对路径、相对路径、三方模块,.js可以省略
import命令不能出现在块级作用域中,只能出现在模块顶层就行。
function foo() {
import { aa } from './aa.js' //报错
}
foo()
console.log(aa)
import { aa } from './aa.js'
import { aa } from './aa.js'
import { aa } from './aa.js'
但如果是同一个模块中的不同输入,是都会被执行的。
import { aa } from './aa.js'
import { bb } from './aa.js'
等同于
import { aa, bb } from './aa.js'
假设aa.js中有aa和bb两个方法导出,可以逐一列出导入也可以利用*
import * as module from './aa.js'
从前面的例子可以看出,使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。
为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。
// a.js
export default function(){
console.log('a')
}
//b.js
加载该模块时,import命令可以为该匿名函数指定任意名字
import a from './a.js'
import _, { each, forEach } from 'lodash';
如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起。
export { foo, bar } from ‘my_module’;
// 可以简单理解为
import { foo, bar } from ‘my_module’;
export { foo, bar };
目前的最佳实践为:
服务器开发遵守 commonjs 规范
浏览器开发遵守 es module
AMD\CMD不再是主流,可以不关注。
因为内容比较多,所以后面还会再有一篇博客进行对比分析、外加讲解如何兼容使用。欢迎继续关注,也欢迎有不同意见的小伙伴提出建议。微信lm13821687665