什么是编译器?
compiler也叫编译器,是一种电脑程序,它会将用某种编程语言写成的源代码,转换成另一种编程语言。
从维基百科的定义来看,编译器就是个将当前语言转为其他语言的过程,回到babel上,它所做的事就是语法糖之类的转换,比如ES6/ES7/JSX转为ES5或者其他指定版本,因此称之为compiler也是正确的,换言之,像我们平时开发过程中所谓的其他工具,如:
- Less/Saas
- TypeScript/coffeeScript
- Eslint
词法分析(Lexical Analysis)
将文本分割成一个个的"token",例如:init、main、init、x、;、x、=、3、;、}等等。同时它可以去掉一些注释、空格、回车等等无效字符;
语法分析(Syntactic Analysis)
我们日常所说的编译原理就是将一种语言转换为另一种语言。编译原理被称为形式语言,它是一类无需知道太多语言背景、无歧义的语言。而自然语言通常难以处理,主要是因为难以识别语言中哪些是名词哪些是动词哪些是形容词。例如:"进口汽车"这句话,"进口"到底是动词还是形容词?所以我们要解析一门语言,前提是这门语言有严格的语法规定的语言,而定义语言的语法规格称为文法。
代码转换(Transformation)
在得到AST后,我们一般会先将AST转为另一种AST,目的是生成更符合预期的AST,这一步称为代码转换。
代码转换的优势:主要是产生工程上的意义
- 易移植:与机器无关,所以它作为中间语言可以为生成多种不同型号的目标机器码服务;
- 机器无关优化:对中间码进行机器无关优化,利于提高代码质量;
- 层次清晰:将AST映射成中间代码表示,再映射成目标代码的工作分层进行,使编译算法更加清晰 ;
代码生成 (Code Generation)
在实际的代码处理过程中,可能会递归的分析(recursive)我们最终生成的AST,然后对于每种type都有个对应的函数处理,当然,这可能是最简单的做法。总之,我们的目标代码会在这一步输出,对于我们的目标语言,它就是HTML了。
完整链路
input => tokenizer => tokens; // 词法分析
tokens => parser => ast; // 语法分析,生成AST
ast => transformer => newAst; // 中间层代码转换
newAst => generator => output; // 生成目标代码
npm package.json 的属性
npm包版本管理机制
npm包 中的模块版本都需要遵循 SemVer规范------由 Github 起草的一个具有指导意义的,统一的版本号表示规则。实际上就是 Semantic Version(语义化版本)的缩写。
SemVer规范官网: semver.org/
标准版本
SemVer规范的标准版本号采用 X.Y.Z 的格式,其中 X、Y 和 Z 为非负的整数,且禁止在数字前方补零。X 是主版本号、Y 是次版本号、而 Z 为修订号。每个元素必须以数值来递增。
- 主版本号(major):当你做了不兼容的API 修改;
- 次版本号(minor):当你做了向下兼容的功能性新增;
- 修订号(patch):当你做了向下兼容的问题修正;
例如:1.9.1 -> 1.10.0 -> 1.11.0
先行版本
当某个版本改动比较大、并非稳定而且可能无法满足预期的兼容性需求时,你可能要先发布一个先行版本。
先行版本号可以加到"主版本号.次版本号.修订号"的后面,先加上一个连接号再加上一连串以句点分隔的标识符和版本编译信息。
- 内部版本(alpha);
- 公测版本(beta);
- 正式版本的候选版本rc: 即 Release candiate;
发布版本
在修改 npm 包某些功能后通常需要发布一个新的版本,我们通常的做法是直接去修改 package.json 到指定版本。如果操作失误,很容易造成版本号混乱,我们可以借助符合 Semver 规范的命令来完成这一操作:
- npm version patch : 升级修订版本号
- npm version minor : 升级次版本号
- npm version major : 升级主版本号
前端模块化规范
CommonJS
Node 应用由模块组成,采用 CommonJS 模块规范。每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。在服务器端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理。
- 所有代码都运行在模块作用域,不会污染全局作用域;
- 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存;
- 模块加载的顺序,按照其在代码中出现的顺序,可以动态加载,代码发生在运行时;
- 导入的值是拷贝的,可以修改拷贝值,不会引起变量污染。
基本语法
- 暴露模块:module.exports = value或exports.xxx = value
- 引入模块:require(xxx),如果是第三方模块,xxx为模块名;如果是自定义模块,xxx为模块文件路径
此处我们有个疑问:CommonJS暴露的模块到底是什么? CommonJS规范规定,每个模块内部,module变量代表当前模块 。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。
模块的加载机制
CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。这点与ES6模块化有重大差异(下文会介绍),请看下面这个例子:
javascript
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
上面代码输出内部变量counter和改写这个变量的内部方法incCounter。
javascript
// main.js
var counter = require('./lib').counter;
var incCounter = require('./lib').incCounter;
console.log(counter); // 3
incCounter();
console.log(counter); // 3
上面代码说明,counter输出以后,lib.js模块内部的变化就影响不到counter了。这是因为counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。
AMD(Asynchronous Module Definition)
CommonJS规范加载模块是同步 的,也就是说,只有加载完成,才能执行后面的操作。AMD规范则是非同步加载模块 ,允许指定回调函数。由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。此外AMD规范比CommonJS规范在浏览器端实现要来着早。
定义暴露模块:
javascript
//定义没有依赖的模块
define(function(){
return 模块
})
//定义有依赖的模块
define(['module1', 'module2'], function(m1, m2){
return 模块
})
引入使用模块:
javascript
require(['module1', 'module2'], function(m1, m2){
使用m1/m2
})
AMD实现
通过比较是否实用AMD,来说明使用AMD实际使用的效果。
未使用AMD规范
javascript
// dataService.js文件
(function (window) {
let msg = 'www.xianzao.com'
function getMsg() {
return msg.toUpperCase()
}
window.dataService = {getMsg}
})(window)
// alerter.js文件
(function (window, dataService) {
let name = 'xianzao'
function showMsg() {
alert(dataService.getMsg() + ', ' + name)
}
window.alerter = {showMsg}
})(window, dataService)
// main.js文件
(function (alerter) {
alerter.showMsg()
})(alerter)
// index.html文件
<div><h1>Modular Demo 1: 未使用AMD(require.js)</h1></div>
<script type="text/javascript" src="js/modules/dataService.js"></script>
<script type="text/javascript" src="js/modules/alerter.js"></script>
<script type="text/javascript" src="js/main.js"></script>
最后得到如下结果:
javascript
'WWW.XIANZAO.COM', 'xianzao'
这种方式缺点很明显:首先会发送多个请求,其次引入的js文件顺序不能搞错,否则会报错
使用require.js
RequireJS是一个工具库,主要用于客户端的模块管理。它的模块管理遵守AMD规范,RequireJS的基本思想是,通过define方法,将代码定义为模块;通过require方法,实现代码的模块加载。
接下来介绍AMD规范在浏览器实现的步骤:
- 下载require.js, 并引入
- 官网: http://www.requirejs.cn/
- github : https://github.com/requirejs/requirejs
然后将require.js导入项目: js/libs/require.js
- 创建项目结构
javascript
|-js
|-libs
|-require.js
|-modules
|-alerter.js
|-dataService.js
|-main.js
|-index.html
- 定义require.js的模块代码
javascript
// dataService.js文件
// 定义没有依赖的模块
define(function() {
let msg = 'www.xianzao.com'
function getMsg() {
return msg.toUpperCase()
}
return { getMsg } // 暴露模块
})
//alerter.js文件
// 定义有依赖的模块
define(['dataService'], function(dataService) {
let name = 'xianzao'
function showMsg() {
alert(dataService.getMsg() + ', ' + name)
}
// 暴露模块
return { showMsg }
})
// main.js文件
(function() {
require.config({
baseUrl: 'js/', //基本路径 出发点在根目录下
paths: {
//映射: 模块标识名: 路径
alerter: './modules/alerter', //此处不能写成alerter.js,会报错
dataService: './modules/dataService'
}
})
require(['alerter'], function(alerter) {
alerter.showMsg()
})
})()
// index.html文件
<!DOCTYPE html>
<html>
<head>
<title>Modular Demo</title>
</head>
<body>
<!-- 引入require.js并指定js主文件的入口 -->
<script data-main="js/main" src="js/libs/require.js"></script>
</body>
</html>
- 页面引入require.js模块
在index.html引入
javascript
<script data-main="js/main" src="js/libs/require.js"></script>
- 引入第三方库
javascript
// alerter.js文件
define(['dataService', 'jquery'], function(dataService, $) {
let name = 'Tom'
function showMsg() {
alert(dataService.getMsg() + ', ' + name)
}
$('body').css('background', 'green')
// 暴露模块
return { showMsg }
})
javascript
// main.js文件
(function() {
require.config({
baseUrl: 'js/', //基本路径 出发点在根目录下
paths: {
//自定义模块
alerter: './modules/alerter', //此处不能写成alerter.js,会报错
dataService: './modules/dataService',
// 第三方库模块
jquery: './libs/jquery-1.10.1' //注意:写成jQuery会报错
}
})
require(['alerter'], function(alerter) {
alerter.showMsg()
})
})()
上例是在alerter.js文件中引入jQuery第三方库,main.js文件也要有相应的路径配置。
总结
通过两者的比较,可以得出AMD模块定义的方法非常清晰,不会污染全局环境,能够清楚地显示依赖关系。AMD模式可以用于浏览器环境,并且允许非同步加载模块,也可以根据需要动态加载模块。
javascript
//定义没有依赖的模块
define(function(require, exports, module){
exports.xxx = value
module.exports = value
})
javascript
//定义有依赖的模块
define(function(require, exports, module){
//引入依赖模块(同步)
var module2 = require('./module2')
//引入依赖模块(异步)
require.async('./module3', function (m3) {
})
//暴露模块
exports.xxx = value
})
javascript
// 引入使用的模块
define(function (require) {
var m1 = require('./module1')
var m4 = require('./module4')
m1.show()
m4.show()
})
CMD(Common Module Definition)
CMD规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。CMD规范整合了CommonJS和AMD规范的特点。在 Sea.js 中,所有 JavaScript 模块都遵循 CMD模块定义规范。
CMD实现
- 下载sea.js, 并引入
- 官网: http://seajs.org/
- github : https://github.com/seajs/seajs
然后将sea.js导入项目:js/libs/sea.js
- 创建项目结构
javascript
|-js
|-libs
|-sea.js
|-modules
|-module1.js
|-module2.js
|-module3.js
|-module4.js
|-main.js
|-index.html
- 定义sea.js的模块代码
javascript
// module1.js文件
define(function (require, exports, module) {
//内部变量数据
var data = 'xianzao.com'
//内部函数
function show() {
console.log('module1 show() ' + data)
}
//向外暴露
exports.show = show
})
// module2.js文件
define(function (require, exports, module) {
module.exports = {
msg: 'I am xianzao'
}
})
// module3.js文件
define(function(require, exports, module) {
const API_KEY = 'abc123'
exports.API_KEY = API_KEY
})
// module4.js文件
define(function (require, exports, module) {
//引入依赖模块(同步)
var module2 = require('./module2')
function show() {
console.log('module4 show() ' + module2.msg)
}
exports.show = show
//引入依赖模块(异步)
require.async('./module3', function (m3) {
console.log('异步引入依赖模块3 ' + m3.API_KEY)
})
})
// main.js文件
define(function (require) {
var m1 = require('./module1')
var m4 = require('./module4')
m1.show()
m4.show()
})
- 在index.html中引入
javascript
<script type="text/javascript" src="js/libs/sea.js"></script>
<script type="text/javascript">
seajs.use('./js/modules/main')
</script>
最后得到结果如下:
javascript
module1 show(), xianzao
module4 show() I am xianzao
异步引入依赖模块3 abc123
AMD与CMD区别
javascript
// AMD
define(['Module1'], function (module1) {
var result1 = module1.exec();
return {
result1: result1,
}
});
// CMD
define(function (requie, exports, module) {
//依赖就近书写
var module1 = require('Module1');
var result1 = module1.exec();
module.exports = {
result1: result1,
}
});
从上面的代码比较中我们可以得出AMD规范和CMD规范的区别
- 对依赖的处理:
- AMD推崇依赖前置,即通过依赖数组的方式提前声明当前模块的依赖;
- CMD推崇依赖就近,在编程需要用到的时候通过调用require方法动态引入;
- 在本模块的对外输出:
- AMD推崇通过返回值的方式对外输出;
- CMD推崇通过给module.exports赋值的方式对外输出;
特性 | CommonJS | AMD | CMD |
---|---|---|---|
加载方式 | 同步加载 | 异步加载 | 异步加载(延迟加载依赖) |
模块导出 | module.exports / exports |
define |
define |
依赖管理 | 无需显式声明依赖 | 必须声明依赖 | 延迟声明依赖,按需加载依赖 |
执行时机 | 模块首次加载时立即执行 | 模块加载时执行 | 模块执行时,依赖被延迟加载 |
使用环境 | 主要用于 Node.js 环境 | 主要用于浏览器端 | 主要用于浏览器端 |
模块缓存 | 有(第一次加载后缓存) | 无(每次加载模块都重新执行) | 有(延迟加载,模块执行时缓存) |