开发中使用Gulp + Browserify对于前端的多页面应用进行工程模块化,发现VS Code无法识别tpl文件,就找了VS Code 支持的方式,并记录下来。
打开 文件 =》首选项 =》设置,并输入associations:

加入上面的划线部分设置即可。
开发中使用Gulp + Browserify对于前端的多页面应用进行工程模块化,发现VS Code无法识别tpl文件,就找了VS Code 支持的方式,并记录下来。
打开 文件 =》首选项 =》设置,并输入associations:
加入上面的划线部分设置即可。
本文是源仓库里的”AMD”文档的一份拷贝,放在这里是用来维护历史链接。文中任何与源仓库里的文档不一致之处,以源仓库里的文档为准。
异步模块定义规范(AMD)制定了定义模块的规则,这样模块和模块的依赖可以被异步加载。这和浏览器的异步加载模块的环境刚好适应(浏览器同步加载模块会导致性能、可用性、调试和跨域访问等问题)。
此AMD与科技公司AMD 及其制造的AMD处理器无关。
本规范只定义了一个函数 “define”,它是全局变量。函数的描述为:
define(id?, dependencies?, factory);
第一个参数,id,是个字符串。它指的是定义中模块的名字,这个参数是可选的。如果没有提供该参数,模块的名字应该默认为模块加载器请求的指定脚本的名字。如果提供了该参数,模块名必须是“顶级”的和绝对的(不允许相对名字)。
模块名用来唯一标识定义中模块,它们同样在依赖数组中使用。AMD的模块名规范是CommonJS模块名规范的超集。引用如下:
上文引用的CommonJS模块id属性常被用于JavaScript模块。
相对模块名解析示例:
"a/b/c"
请求 "../d"
, 则解析为"a/d"
"a/b/c"
请求 "./e"
, 则解析为"a/b/e"
如果AMD的实现支持加载器插件(Loader-Plugins),则”!”符号用于分隔加载器插件模块名和插件资源名。由于插件资源名可以非常自由地命名,大多数字符都允许在插件资源名使用。
(译注:关于Loader-Plugins)
第二个参数,dependencies,是个定义中模块所依赖模块的数组。依赖模块必须根据模块的工厂方法优先级执行,并且执行的结果应该按照依赖数组中的位置顺序以参数的形式传入(定义中模块的)工厂方法中。
依赖的模块名如果是相对的,应该解析为相对定义中的模块。换句话来说,相对名解析为相对于模块的名字,并非相对于寻找该模块的名字的路径。
本规范定义了三种特殊的依赖关键字。如果”require”,”exports”, 或 “module”出现在依赖列表中,参数应该按照CommonJS模块规范自由变量去解析。
依赖参数是可选的,如果忽略此参数,它应该默认为[“require”, “exports”, “module”]。然而,如果工厂方法的形参个数小于3,加载器会选择以函数指定的参数个数调用工厂方法。
第三个参数,factory,为模块初始化要执行的函数或对象。如果为函数,它应该只被执行一次。如果是对象,此对象应该为模块的输出值。
如果工厂方法返回一个值(对象,函数,或任意强制类型转换为true的值),应该为设置为模块的输出值。
如果依赖性参数被忽略,模块加载器可以选择扫描工厂方法中的require语句以获得依赖性(字面量形为require(“module-id”))。第一个参数必须字面量为require从而使此机制正常工作。
在某些情况下,因为脚本大小的限制或函数不支持toString方法(Opera Mobile是已知的不支持函数的toString方法),模块加载器可以选择扫描不扫描依赖性。
如果有依赖参数,模块加载器不应该在工厂方法中扫描依赖性。
为了清晰的标识全局函数(为浏览器加载script必须的)遵从AMD编程接口,任何全局函数应该有一个”amd”的属性,它的值为一个对象。这样可以防止与现有的定义了define函数但不遵从AMD编程接口的代码相冲突。
当前,define.amd对象的属性没有包含在本规范中。实现本规范的作者,可以用它通知超出本规范编程接口基本实现的额外能力。
define.amd的存在表明函数遵循本规范。如果有另外一个版本的编程接口,那么应该定义另外一个属性,如define.amd2,表明实现只遵循该版本的编程接口。
一个如何定义同一个环境中允许多次加载同一个版本的模块的实现:
define.amd = { multiversion: true };
最简短的定义:
define.amd = {};
在一个脚本中可以使用多次define调用。这些define调用的顺序不应该是重要的。早一些的模块定义中所指定的依赖,可以在同一脚本中晚一些定义。模块加载器负责延迟加载未解决的依赖,直到全部脚本加载完毕,防止没必要的请求。
创建一个名为”alpha”的模块,使用了require,exports,和名为”beta”的模块:
define("alpha", ["require", "exports", "beta"], function (require, exports, beta) { exports.verb = function() { return beta.verb(); //Or: return require("beta").verb(); } });
一个返回对象的匿名模块:
define(["alpha"], function (alpha) { return { verb: function(){ return alpha.verb() + 2; } }; });
一个没有依赖性的模块可以直接定义对象:
define({ add: function(x, y){ return x + y; } });
一个使用了简单CommonJS转换的模块定义:
define(function (require, exports, module) { var a = require('a'), b = require('b'); exports.action = function () {}; });
本规范保留全局变量”define”以用来实现本规范。包额外信息异步定义编程接口是为将来的CommonJS API保留的。模块加载器不应在此函数添加额外的方法或属性。
本规范保留全局变量”require”被模块加载器使用。模块加载器可以在合适的情况下自由地使用该全局变量。它可以使用这个变量或添加任何属性以完成模块加载器的特定功能。它同样也可以选择完全不使用”require”。
为了使静态分析工具(如build工具)可以正常工作,推荐使用字面上形如的’define(…)’。
一个关于本API的wiki开始在CommonJS wiki中创建了,作为中转的格式,模块中转。但是为了包含模块定义接口,随着时间而不断改变。在CommonJS列表中关于推荐本API作为模块定义API尚未达成一致。本API被转移到它自己的wiki和讨论组中。
AMD可以作为CommonJS模块一个中转的版本只要CommonJS没有被用来同步的require调用。使用同步require调用的CommonJS代码可以被转换为使用回调风格的AMD模块加载器。
JavaScript前期的代码还不是很复杂,我们可以通过代码简单实现,但随着项目越来越大、程序所包含的功能越来越多、开发人员原来越多的时候,我们就需要一个统一的模块化规范来编写代码,以求我们的代码的可读性、可维护性更高,同时也方便其他人来使用我们的实现。
模块化开发是一种管理方式,是一种生产方式,一种解决问题的方案,一个模块就是实现特定功能的文件,有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块。
随着需求的不断增加,前端的模块化技术也是一直处于不断演进的状态。
将不同的功能封装成不同的全局函数,这会导致Global被污染了, 很容易引起命名冲突。
function sum(a,b){
return parseInt(a) + parseInt(b);
}
function reduce(a,b){
return parseInt(a) - parseInt(b);
}
通过将参数、方法挂载在对象上,实现的简单的模块化隔离。
var MYNAMESPACE = MYNAMESPACE || {};
MYNAMESPACE.person = function(name) {
this.name = name;
};
MYNAMESPACE.person.prototype.getName = function() {
return this.name;
};
// 使用方法
var p = new MYNAMESPACE.person("doc");
p.getName(); /
IIFE: Immediately Invoked Function Expression,意为立即调用的函数表达式,也就是说,声明函数的同时立即调用这个函数。我看可以通过自执行函数实现数据的私有化隔离(函数上下文)。匿名函数自身不污染全局环境,同时为内部变量提供作用于环境空间,且提供闭包环境,可以做闭包想做的事情。
var a = 2;
(function IIFE(global){
var a = 3;
console.log(a); // 3
console.log(global.a); // 2
})(window);
console.log(a); // 2
2009年发布,Node 应用由模块组成,采用 CommonJS 模块规范。commonJS规范加载是同步的,也就是说,加载完成才执行后面的操作。CommonJS遵循Modules/1.0(http://wiki.commonjs.org/wiki/Modules/1.0)规范,该规范首次提出了JS的模块化实现应该遵循的规则(nodejs)。规范指出:
该规范保留了以下未指明的互操作性要点:
遵循commonjs规范的代码:
//math.js
exports.add = function() {
var sum = 0, i = 0, args = arguments, l = args.length;
while (i < l) {
sum += args[i++];
}
return sum;
};
//increment.js
var add = require('math').add;
exports.increment = function(val) {
return add(val, 1);
};
//program.js
var inc = require('increment').increment;
var a = 1;
inc(a); // 2
Modules/1.0规范源于服务端,无法直接用于浏览器端,原因表现为:
基于Modules/1.0的基础上,commonJs社区讨论关于浏览器端的模块化规范的时候,内部发生了比较大的分歧,分裂出了三个主张,渐渐的形成三个不同的派别:
这一波人认为,在现有基础上进行改进即可满足浏览器端的需要,既然浏览器端需要function包装,需要异步加载,那么新增一个方案,能把现有模块转化为适合浏览器端的就行了。基于这个主张,制定了Modules/Transport(http://wiki.commonjs.org/wiki/Modules/Transport)规范,提出了先通过工具把现有模块转化为复合浏览器上使用的模块,然后再使用的方案。
AMD是”Asynchronous Module Definition”的缩写,意思就是”异步模块定义”。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。他们认为浏览器与服务器环境差别太大,不能沿用旧的模块标准。既然浏览器必须异步加载代码,那么模块在定义的时候就必须指明所依赖的模块,然后把本模块的代码写在回调函数里。模块的加载也是通过下载-回调这样的过程来进行,这个思想就是AMD的基础,由于“革新派”与“保皇派”的思想无法达成一致,最终从CommonJs中分裂了出去,独立制定了浏览器端的js模块化规范AMD(Asynchronous Module Definition)(https://github.com/amdjs/amdjs-api/wiki/AMD)
// 规定采用require语句加载模块,但是不同于CommonJS,它要求两个参数
require([module], callback);
// 在定义模块的时候需要使用define函数定义:
define(id?, dependencies?, factory);
这一波人有点像“中间派”,既不想丢掉旧的规范,也不想像AMD那样推到重来。他们认为,Modules/1.0固然不适合浏览器,但它里面的一些理念还是很好的,(如通过require来声明依赖),新的规范应该兼容这些,AMD规范也有它好的地方(例如模块的预先加载以及通过return可以暴漏任意类型的数据,而不是像commonjs那样exports只能为object),也应采纳。最终他们制定了一个Modules/Wrappings(http://wiki.commonjs.org/wiki/Modules/Wrappings)规范,此规范指出了一个模块应该如何“包装”,包含以下内容:
为了支持一个模块同时兼容AMD和CommonJs规范,适用于 同时支持浏览器端和服务端引用的第三方库,提出了 UMD规范。UMD是一个时代的产物,当各个环境最终实现ES harmony的统一的规范后,它也将退出历史舞台。
// 定义一个模块
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['jquery', 'underscore'], factory);
} else if (typeof exports === 'object') {
// Node, CommonJS之类的
module.exports = factory(require('jquery'), require('underscore'));
} else {
// 浏览器全局变量(root 即 window)
root.returnExports = factory(root.jQuery, root._);
}
}(this, function ($, _) {
// 方法
function a(){}; // 私有方法,因为它没被返回 (见下面)
function b(){}; // 公共方法,因为被返回了
function c(){}; // 公共方法,因为被返回了
// 暴露公共方法
return {
b: b,
c: c
}
}));
CMD 是 “Common Module Definition”的缩写,意思是通用模块规范。CMD专门用于浏览器端, 模块的加载是异步的 ,模块使用时才会加载执行。在 CMD 规范中,一个模块就是一个文件。规范地址: https://github.com/cmdjs/specification/blob/master/draft/module.md
// 定义一个模块
define(function(require, exports, module) {
....
})
//sea.js:
define(function(require, exports, module) {
var mod_A = require("dep_A");
var mod_B = require("dep_B");
var mod_C = require("dep_C");
});
ES6是ECMA的为JavaScript制定的第6个版本的标准,标准委员会最终决定,标准在每年的 6 月份正式发布一次,作为当年的正式版本。ECMAscript 2015 是在2015年6月份发布的ES6的第一个版本。
// file lib/greeting.js 定义一个模块
const helloInLang = {
en: 'Hello world!',
es: '¡Hola mundo!',
ru: 'Привет мир!'
};
// 对外输出
export const greeting = {
sayHello: function (lang) {
return helloInLang[lang];
}
};
// file hello.js 引入一个模块
import { greeting } from "./lib/greeting";
const phrase = greeting.sayHello("en");
document.write(phrase);
webpack 自己实现了一套模块机制,无论是 CommonJS 模块的 require 语法还是 ES6 模块的 import 语法,都能够被解析并转换成指定环境的可运行代码。随着webpack打包工具的流行,ES6语法广泛手中,后来的开发者对于 AMD CMD的感知越来越少。
在 Sea.js 中,所有 JavaScript 模块都遵循 CMD(Common Module Definition) 模块定义规范。该规范明确了模块的基本书写格式和基本交互规则。
在 CMD 规范中,一个模块就是一个文件。代码的书写格式如下:
define(factory);
Function
define
是一个全局函数,用来定义模块。
define(factory)
define
接受 factory
参数,factory
可以是一个函数,也可以是一个对象或字符串。
factory
为对象、字符串时,表示模块的接口就是该对象、字符串。比如可以如下定义一个 JSON 数据模块:
define({ "foo": "bar" });
也可以通过字符串定义模板模块:
define('I am a template. My name is {{name}}.');
factory
为函数时,表示是模块的构造方法。执行该构造方法,可以得到模块向外提供的接口。factory
方法在执行时,默认会传入三个参数:require
、exports
和 module
:
define(function(require, exports, module) {
// 模块代码
});
define(id?, deps?, factory)
define
也可以接受两个以上参数。字符串 id
表示模块标识,数组 deps
是模块依赖。比如:
define('hello', ['jquery'], function(require, exports, module) {
// 模块代码
});
id
和 deps
参数可以省略。省略时,可以通过构建工具自动生成。
注意:带 id
和 deps
参数的 define
用法不属于 CMD 规范,而属于 Modules/Transport 规范。
Object
一个空对象,可用来判定当前页面是否有 CMD 模块加载器:
if (typeof define === "function" && define.cmd) {
// 有 Sea.js 等 CMD 模块加载器存在
}
Function
require
是 factory
函数的第一个参数。
require(id)
require
是一个方法,接受 模块标识 作为唯一参数,用来获取其他模块提供的接口。
define(function(require, exports) {
// 获取模块 a 的接口
var a = require('./a');
// 调用模块 a 的方法
a.doSomething();
});
注意:在开发时,require
的书写需要遵循一些 简单约定。
require.async(id, callback?)
require.async
方法用来在模块内部异步加载模块,并在加载完成后执行指定回调。callback
参数可选。
define(function(require, exports, module) {
// 异步加载一个模块,在加载完成时,执行回调
require.async('./b', function(b) {
b.doSomething();
});
// 异步加载多个模块,在加载完成时,执行回调
require.async(['./c', './d'], function(c, d) {
c.doSomething();
d.doSomething();
});
});
注意:require
是同步往下执行,require.async
则是异步回调执行。require.async
一般用来加载可延迟异步加载的模块。
require.resolve(id)
使用模块系统内部的路径解析机制来解析并返回模块路径。该函数不会加载模块,只返回解析后的绝对路径。
define(function(require, exports) {
console.log(require.resolve('./b'));
// ==> http://example.com/path/to/b.js
});
这可以用来获取模块路径,一般用在插件环境或需动态拼接模块路径的场景下。
Object
exports
是一个对象,用来向外提供模块接口。
define(function(require, exports) {
// 对外提供 foo 属性
exports.foo = 'bar';
// 对外提供 doSomething 方法
exports.doSomething = function() {};
});
除了给 exports
对象增加成员,还可以使用 return
直接向外提供接口。
define(function(require) {
// 通过 return 直接提供接口
return {
foo: 'bar',
doSomething: function() {}
};
});
如果 return
语句是模块中的唯一代码,还可简化为:
define({
foo: 'bar',
doSomething: function() {}
});
上面这种格式特别适合定义 JSONP 模块。
特别注意:下面这种写法是错误的!
define(function(require, exports) {
// 错误用法!!!
exports = {
foo: 'bar',
doSomething: function() {}
};
});
正确的写法是用 return
或者给 module.exports
赋值:
define(function(require, exports, module) {
// 正确写法
module.exports = {
foo: 'bar',
doSomething: function() {}
};
});
提示:exports
仅仅是 module.exports
的一个引用。在 factory
内部给 exports
重新赋值时,并不会改变 module.exports
的值。因此给 exports
赋值是无效的,不能用来更改模块接口。
Object
module
是一个对象,上面存储了与当前模块相关联的一些属性和方法。
String
模块的唯一标识。
define('id', [], function(require, exports, module) {
// 模块代码
});
上面代码中,define
的第一个参数就是模块标识。
String
根据模块系统的路径解析规则得到的模块绝对路径。
define(function(require, exports, module) {
console.log(module.uri);
// ==> http://example.com/path/to/this/file.js
});
一般情况下(没有在 define
中手写 id
参数时),module.id
的值就是 module.uri
,两者完全相同。
Array
dependencies
是一个数组,表示当前模块的依赖。
Object
当前模块对外提供的接口。
传给 factory
构造方法的 exports
参数是 module.exports
对象的一个引用。只通过 exports
参数来提供接口,有时无法满足开发者的所有需求。 比如当模块的接口是某个类的实例时,需要通过 module.exports
来实现:
define(function(require, exports, module) {
// exports 是 module.exports 的一个引用
console.log(module.exports === exports); // true
// 重新给 module.exports 赋值
module.exports = new SomeClass();
// exports 不再等于 module.exports
console.log(module.exports === exports); // false
});
注意:对 module.exports
的赋值需要同步执行,不能放在回调函数里。下面这样是不行的:
// x.js
define(function(require, exports, module) {
// 错误用法
setTimeout(function() {
module.exports = { a: "hello" };
}, 0);
});
在 y.js 里有调用到上面的 x.js:
// y.js
define(function(require, exports, module) {
var x = require('./x');
// 无法立刻得到模块 x 的属性 a
console.log(x.a); // undefined
});
这就是 CMD 模块定义规范的所有内容。经常使用的 API 只有 define
, require
, require.async
, exports
, module.exports
这五个。其他 API 有个印象就好,在需要时再来查文档,不用刻意去记。
与 RequireJS 的 AMD 规范相比,CMD 规范尽量保持简单,并与 CommonJS 和 Node.js 的 Modules 规范保持了很大的兼容性。通过 CMD 规范书写的模块,可以很容易在 Node.js 中运行
一、出现原因
ES5 的对象属性名都是字符串,这容易造成属性名的冲突。为了解决这一问题,在ES6中便新增了一种新的原始数据类型Symbol
,它表示独一无二的值。前面我们说过JavaScript的六种数据类型分别是:
undefined, null, Boolean, String, Number, Object
加上现在说的Symbol,现在有七种了。Symbol实际上是一种唯一的标识符,可以作为对象的唯一属性名,这样就不会被覆盖了。
Symbol 值通过Symbol
函数生成,于是对象的属性名便有两种类型,一种是原来的字符串,另外就是现在说的Symbol类型。
let a = Symbol()
typeof a // symbol
console.log(a) // Symbol()
let b = Symbol('b')
typeof b // "symbol"
console.log(b) // Symbol(b)
注意:Symbol
函数前不能使用new
命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。实际上它是一种类似于字符串的数据类型。
二、特性
1、相同参数的Symbol函数返回的值是不相同的
let a1 = Symbol('a')
let a2 = Symbol('a')
console.log(a1 == a2,a1,a2) // false Symbol(a) Symbol(a)
这是因为Symbol
函数的参数只是对当前 Symbol 值的描述,因此相同参数的Symbol
函数的返回值是不相等的。
2、Symbol值不能与其他类型的数据进行运算,但是Symbol值可以显式转为字符串,Symbol值也可以转为布尔值
Symbol() + 1 // Uncaught TypeError: Cannot convert a Symbol value to a number
'this is ' + Symbol('a') // Uncaught TypeError: Cannot convert a Symbol value to a string
let str = Symbol('str')
console.log(String(str)) // "Symbol(str)"
Boolean(str) // true
3、Symbol的值作为对象的属性名,由于Symbol值的唯一性,因此不会覆盖属性名
let attr = Symbol()
// 第一种写法
let obj = {}
obj[attr] = 'Hello!'
console.log(obj,obj[attr]) // {Symbol(): "Hello!"}
// 第二种写法
let obj = { [attr] : 'hello!' }
console.log(obj,obj[attr]) // {Symbol(): "Hello!"}
// 第三种写法
let obj = {}
Object.defineProperty(obj, attr, {value: 'hello!'})
console.log(obj,obj[attr]) // {Symbol(): "Hello!"}
4、Symbol值作为对象属性名时,不能用点运算符
let attr = Symbol()
const obj = {}
obj.attr = 'hello!'
console.log(obj[attr], obj['attr']) // undefined "hello!"
因为点运算符后面总是字符串,所以不会读取attr作为标识名所指代的那个值,导致obj 的属性名实际上是一个字符串,而不是一个 Symbol 值。所以在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。
let attr = Symbol()
let obj = {
[attr]: function(param){
console.log(param)
}
}
obj[attr](1234)
5、Symbol类型还可以用于定义一组常量,保证这组常量的值都是不相等的
let greet = {}
greet.levels = {
DEBUG: Symbol('debug'),
INFO: Symbol('info'),
WARN: Symbol('warn')
}
console.log(greet.levels.DEBUG, 'debug message'); // Symbol(debug) "debug message"
console.log(greet.levels.INFO, 'info message'); // Symbol(info) "info message"
注意Symbol 值作为属性名时,该属性还是公开属性,不是私有属性。
三、Symbol 作为属性名的遍历
1、Object.getOwnPropertySymbols() 遍历属性名
当symbol作为属性名时,遍历属性名时,不能从for...in
、for...of
循环中取出,也不会被Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
返回。
此时需要用Object.getOwnPropertySymbols()方法获取指定对象的所有 Symbol 属性名,该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
const attr1 = Symbol('attr1')
const attr2 = Symbol('attr2')
let obj = {
[attr1]:'hello1',
[attr2]:'hello2',
}
const res = Object.getOwnPropertySymbols(obj)
console.log(res) // [Symbol(attr1), Symbol(attr2)]
2、Reflect.ownKeys() 返回属性名
Reflect.ownKeys()
方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。
let obj = {
a:1,
b:[1,2],
[Symbol('c')]:'str'
}
Reflect.ownKeys(obj) // ["a", "b", Symbol(c)]
四、Symbol 相关函数使用
1、Symbol.for(),Sysmbol.keyFor()
当我们需要使用相同的Sysmbol值时,Symbol.for()就可以生成相同的值。它需要传一个字符串参数,然后他会先搜索是否存在以该参数作为名称的Symbol的值,如果有则返回这个Symbol的值,否则创建该字符串为名称的Symbol值并将其注册到全局。
let obj1 = Symbol.for('a')
let obj2 = Symbol.for('a')
console.log(obj1 === obj2) // true
Symbol.for()
与Symbol()
它们都会生成新的Symbol值,区别是Symbol.for()被登记在全局环境中供搜索而Symbol()则不会。
console.log(Symbol.for('a') === Symbol.for('a')) // true
console.log(Symbol('a') == Symbol('a')) // false
Symbol.keyFor()
方法返回一个已登记的 Symbol 类型值的key
let obj1 = Symbol.for('a')
console.log(Symbol.keyFor(obj1)) // a
let obj2 = Symbol('a')
console.log(Symbol.keyFor(obj2)) // undefined
Symbol.for()
为Symbol值登记的名字,是全局环境的,不管有没有在全局环境运行。
Broswerify 是一个前端管理依赖的工具,通过它可以在浏览器环境下像nodejs一样遵循commonjs规范的模块化编程。
浏览器没有定义require方法,但是Node.js有。使用Browserify可以编写使用require的代码,就像在Node中使用它一样。
Browserify从entry着手,对源代码进行抽象语法树分析,从而获得整个项目的依赖关系图,最后将整个项目打包成一个JavaScript文件。特点如下:
module.exports.add = function(a,b) {
return a + b;
}
module.exports.reduce = function(a,b){
return a-b;
}
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Browserify 测试</title>
</head>
<body align="center">
<h3>Browserify 测试</h3>
<form style="text-align: left;margin-left: 40%;">
<input name="a" type="input" placeholder="请输入第一个操作数"/>
<input name="b" type="input" placeholder="请输入第二个操作数"/>
<div style="margin-top: 20px;">合计:<span id="result"></span></div>
<div style="margin-top: 20px;">
<button id="add" type="button">加法</button>
<button id="reduce" type="button" style="margin-left: 30px;">减法</button>
</div>
</form>
<script src="./bundle.js" ></script> <!--打包生成文件 -->
</body>
</html>
var { add } = require('./add');
var { reduce } = require('./reduce');
// 加法
document.getElementById('add').onclick = function () {
const a = document.getElementsByName('a')[0].value;
const b = document.getElementsByName('b')[0].value;
document.getElementById('result').innerHTML = add(a,b);
}
// 减法
document.getElementById('reduce').onclick = function () {
const a = document.getElementsByName('a')[0].value;
const b = document.getElementsByName('b')[0].value;
document.getElementById('result').innerHTML = reduce(a,b);
}
$ npm install -g browserify
$ browserify main.js > bundle.js
前端技术随着日益成熟的开发框架(React、Vue、Angular)的流行,如何去管理、构建前端项目也就显得尤其重要。目前市面上较为常用的构建工具就是Webpack。本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包工具。当 webpack 处理应用程序时,它会在内部构建一个 依赖图(dependency graph),此依赖图会映射项目所需的每个模块,并生成一个或多个 bundle。除了webpack以外,前端其实还有很多比较优秀的构建工具,适用于不同的开发场景。如:Gulp、Grunt、FIS、Rollup、Parcel、Yeoman6、Athena、WeFlow、Cooking等。
构建工具在开发中是一个很特殊的存在,它不影响代码逻辑、业务,当大多情况下是不可或缺的角色。从定义上看:构建工具是一个把源代码生成可执行应用程序的过程自动化的程序,如gcc将.c生成为.a、.o文件、javac将.java文件生成为.class文件、gradle将项目工程生成为.apk文件等等诸如此类。如上所说的gcc、javac、gradle都是相对流程的一个构建工具。
了解了什么是构建工具后,接下来需要了解的是既然构架工具并不对结果负责,那么我们为什么要使用构建工具呢?其实理由很现实,就是减负,减轻开发者负担,让开发这不需要将过多的精力用于管理包、包版本、压缩等与业务不相干的工作,而且当项目越来越大,人力也无法负担如此巨大的工作。
说了构建工具的重要性,接下来需要说的是构建工具具体能帮我们做哪些工作:
在前端中构建工具对应的具体需要帮我们实现的如下:
说完了构建工具的功能,接下来需要了解在前端开发中具体有哪些构建工具,我们可以通过工具的类型,将构建工具分为:
常见的而模块化打包类型构建工具如:webpack、Rollup、Browserify。任务流类型的有:Gulp、Grunt。集合型的主要包括:Yeoman、FIS、jdf、Athena、cooking、weflow。就目前来看用的较多的是属于第一、第二类型的构建工具。
Webpack 是一个打包模块化 JavaScript 的工具,在 Webpack 里一切文件皆模块,通过 Loader 转换文件,通过 Plugin 注入钩子,最后输出由多个模块组合成的文件。Webpack 专注于构建模块化项目。
一切文件:JavaScript、CSS、SCSS、图片、模板,在 Webpack 眼中都是一个个模块,这样的好处是能清晰的描述出各个模块之间的依赖关系,以方便 Webpack 对模块进行组合和打包。 经过 Webpack 的处理,最终会输出浏览器能使用的静态资源。Webpack的优点是:
– 模块化处理项目- Plugin 扩展- 使用场景不仅限于 Web 开发- 社区活跃,能为大多数场景找到已有的开源扩展
Grunt的出现早于Gulp,Gulp是后起之秀。他们的本质都是通过 JavaScript 语法实现了shell script 命令的一些功能。 Grunt是一个任务执行者,有大量现成的插件封装了常见的任务,也能管理任务之间的依赖关系,自动化执行依赖的任务,每个任务的具体执行代码和依赖关系写在配置文件 Gruntfile.js 里,如下配置文件:
// Gruntfile.js
module.exports = function(grunt) {
grunt.initConfig({
// js格式检查任务
jshint: {
src: 'src/test.js'
}
// 代码压缩打包任务
uglify: {}
});
// 导入任务插件
grunt.loadnpmTasks('grunt-contrib-uglify');
// 注册自定义任务, 如果有多个任务可以添加到数组中
grunt.regusterTask('default', ['jshint'])
Gulp
Gulp吸取了Grunt的优点,拥有更简便的写法,通过流(Stream)的概念来简化多任务之间的配置和输出,让任务更加简洁和容易上手。通过配置gulpfile.js文件来实现,一个简单的gulpfile.js配置如下:
// gulpfile.js
var gulp = require('gulp');
var jshint = require('gulp-jshint');
var uglify = require('gulp-uglify');
// 代码检查任务 gulp 采取了pipe 方法,用流的方法直接往下传递
gulp.task('lint', function() {
return gulp.src('src/test.js')
.pipe(jshint())
.pipe(jshint.reporter('default'));
});
// 压缩代码任务
gulp.task('compress', function() {
return gulp.src('src/test.js')
.pipe(uglify())
.pipe(gulp.dest('build'));
});
// 将代码检查和压缩组合,新建一个任务
gulp.task('default', ['lint', 'compress']);
本文转载自https://www.jianshu.com/p/a36fb3d3885f,如有侵权,请联系删除
看下面的代码:
const Demo = ()=> {
const [n,setN] = React.useState(0)
return <div>
{n}
<button onClick={()=> {setN(n+1)}}>点我+1</button>
</div>
}
const rootElement = document.getElementById("app")
ReactDOM.render(<Demo />, rootElement);
首次渲染:render()——得到Demo组件——调用Demo组件——得到虚拟div——创建真实的div
点击button后:调用setN(n+1)——再次render()——得到Demo组件——调用Demo组件——得到虚拟div——使用DOM Diff对比——更新真实的div
往后一次重复第二次的操作
state:每个组件都应该有自己的数据state,
setN:setN一定会修改数据state,将n+1存入state,然后重新render(),再次渲染
useState:useState肯定会从state读取n的最新值
let _state
const myUseState = (initState) => {
_state = _state ===undefined ? initState : _state
const setState = (newValue =>{
_state = newValue
render()
})
return [_state,setState]
}
const render =()=>{
ReactDOM.render(<Demo />, rootElement);
}
const Demo = ()=> {
const [n,setN] = myUseState(0)
console.log('n变了');
console.log(`n的值是: ${n}`);
return <div>{n}
<button onClick={()=> {setN(n+1)}}>点我+1</button>
</div>
}
const rootElement = document.getElementById("app")
ReactDOM.render(<Demo />, rootElement);
多点几次,打印的结果看下图
image.png
打印的结果也确实对,却又一个很大的问题:如果一个组件用了两个useState怎么办?
let _state
const myUseState = (initState) => {
_state = _state ===undefined ? initState : _state
const setState = (newValue =>{
_state = newValue
render()
})
return [_state,setState]
}
const render =()=>{
ReactDOM.render(<Demo />, rootElement);
}
const Demo = ()=> {
const [n,setN] = myUseState(0)
const [m,setM] = myUseState(0)
return <div>{n}
<button onClick={()=> {setN(n+1)}}>点我+1</button>
<br/>
{m}
<button onClick={()=> {setM(m+1)}}>点我+1</button>
</div>
}
const rootElement = document.getElementById("app")
ReactDOM.render(<Demo />, rootElement);
把上面的代码运行,你会发现,无论你点击哪个button,n和m都会+1,
现在我们来对它改进,_state不能直接让它等于一个值,而是让它等于很多值,只有对象和数组能做到,而对象不太合适,因为_state = {n: 0,m: 0}
,useState()
并不知道变量叫n
还是m
,只能把它做成数组,比如:_state = [0,0]
(前面0是n,后面0是m)
let _state = []
let index = 0
const myUseState = (initState) => {
const currentIndex = index
_state[currentIndex] = _state[currentIndex] ===undefined ? initState : _state[currentIndex]
const setState = (newValue =>{
_state[currentIndex] = newValue
console.log(_state);
render()
})
index += 1
return [_state[currentIndex],setState]
}
const render =()=>{
index = 0
ReactDOM.render(<Demo />, rootElement);
}
const Demo = ()=> {
const [n,setN] = myUseState(0)
const [m,setM] = myUseState(0)
return <div>{n}
<button onClick={()=> {setN(n+1)}}>点我+1</button>
<br/>
{m}
<button onClick={()=> {setM(m+1)}}>点我+1</button>
</div>
}
const rootElement = document.getElementById("app")
ReactDOM.render(<Demo />, rootElement);
最终代码就是上面的代码,用log可以打出_state
的值,我们分别点击button让它们+1,看log出的值
image.png
这样,我们自己写的_state
是不是就和React的一模一样?
通过实现简单的useState,能让我们更好地理解useState
JS中,给我们提供两种延时操作的内置方法setTimeout()和setInterval()。setTimeout和setInterval方法都是挂载在javascript的window对象下,通过两个参数控制,第一个参数控制运行的表达式或方法,第二个参数表示延时的时间,时间单位为毫秒级。
const id = setTimeout(()=>{
// 延时200毫秒后执行的代码段
}, 200);
clearTimeout(id); // 取消setTimeOut
const intervalId = setInterval(()=>{
// 每隔200毫秒后执行的代码段
}, 200);
clearInterval(intervalId); //取消指定的循环
setInterval与seTimeout不同的是,setTimeou定时执行一次结束,setInterval是循环间隔第二个参数时长执行参数一的表达式或方法。
在 web 前端领域,单元测试通常包括:对某个 JS 的方法进行测试,对某个组件进行测试。除了单元测试,前端经常会有端到端测试。相对于端到端测试来说,单元测试编写更复杂。但是完整的单元测试的样例能够覆盖更多端到端测试覆盖不到的点,对于前端代码通常比较关键的模块可以通过添加单元测试来规避后续修改或者重构带来的风险。单元测试样例的编写过程也有助于进一步审视模块的功能。
单元测试适用于功能不会经常改动的工具方法模块和一些基础的公共组件,对于会经常在快速迭代中更新的业务组件和功能模块端到端的测试会更适合,但这并不是说不需要写单元测试。这其实是一个投入和产出比的一个权衡,编写单元测试可能会需要频繁的更新测试样例,对于部分业务尤其是中后台的应用来说成本是偏高的。
在 React 诞生之前,前端的单元测试往往只能针对于一些纯粹的 JS 模块。由于对浏览器环境的依赖,很难去做涉及到 dom 操作的模块的单元测试。但是对于前端来说,大部分代码其实都是 UI 组件,这就导致长期以来前端的代码甚至一些开源的被应用得很广泛的 UI 组件库都缺乏完整的单元测试。
但是 React 的诞生伴随着虚拟 dom 被发明,这使得前端组件的测试变得更方便了。虚拟 dom 使得一个组件可以脱离真实的浏览器环境模拟 dom 的相关操作。我们可以通过测试虚拟 dom 的表现是否正常来测试组件的逻辑,让编写组件的测试能够脱离对浏览器 dom 环境的依赖。
在 umi 中,内置了 jest 作为单元测试的库,接下来我们会介绍如何使用 jest 对 JS 方法或者组件进行测试。
umi内置了jest测试。执行umi test 会匹配所有 .test.js 结尾的文件运行。通常我们约定把测试的代码统一放到test文件夹下,当然你也可以按照你的习惯组织,比如可以和测试对应的模块放到一起。
非umi环境下安装参考上次分享的内容,内容在我的博客中:
http://xuxiao.wang/detail/单元测试-Jest入门1
test('测试值', () => {
expect(2 + 2).toBe(4);
});
测试一个对象的值是否相等
test('测试对象', () => {
const data = {one: 1};
data['two'] = 2;
expect(data).toEqual({one: 1, two: 2});
});
test(‘匹配字符串', () => {
expect('team').not.toMatch(/I/);
});
使用正则匹配一个字符串。测试用例匹配不包含字母I
const shoppingList = [ 'diapers', 'kleenex', 'trash bags', 'paper towels', 'beer', ];
test('匹配数组中是否包含某个元素', () => {
expect(shoppingList).toContain('beer’);
expect(new Set(shoppingList)).toContain('beer’);
});
使用正则匹配一个字符串。测试用例匹配不包含字母I
expect(compileAndroidCode).toThrow();
compileAndroidCode 是一段可能会抛出异常的方法,如果抛出异常则匹配