💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
<h2 id="10.1">Underscore.js</h2> ## 概述 [Underscore.js](http://underscorejs.org/)是一个很精干的库,压缩后只有4KB。它提供了几十种函数式编程的方法,弥补了标准库的不足,大大方便了JavaScript的编程。MVC框架Backbone.js就将这个库作为自己的工具库。除了可以在浏览器环境使用,Underscore.js还可以用于Node.js。 Underscore.js定义了一个下划线(_)对象,函数库的所有方法都属于这个对象。这些方法大致上可以分成:集合(collection)、数组(array)、函数(function)、对象(object)和工具(utility)五大类。 ## 集合相关方法 Javascript语言的数据集合,包括两种结构:数组和对象。以下的方法同时适用于这两种结构。 ### 集合处理 数组处理指的是对数组元素进行加工。 **(1)map** map方法对集合的每个成员依次进行某种操作,将返回的值依次存入一个新的数组。 ```javascript _.map([1, 2, 3], function(num){ return num * 3; }); // [3, 6, 9] _.map({one : 1, two : 2, three : 3}, function(num, key){ return num * 3; }); // [3, 6, 9] ``` **(2)each** each方法与map类似,依次对数组所有元素进行某种操作,不返回任何值。 ```javascript _.each([1, 2, 3], alert); _.each({one : 1, two : 2, three : 3}, alert); ``` **(3)reduce** reduce方法依次对集合的每个成员进行某种操作,然后将操作结果累计在某一个初始值之上,全部操作结束之后,返回累计的值。该方法接受三个参数。第一个参数是被处理的集合,第二个参数是对每个成员进行操作的函数,第三个参数是累计用的变量。 ```javascript _.reduce([1, 2, 3], function(memo, num){ return memo + num; }, 0); // 6 ``` reduce方法的第二个参数是操作函数,它本身又接受两个参数,第一个是累计用的变量,第二个是集合每个成员的值。 **(4)reduceRight** reduceRight是逆向的reduce,表示从集合的最后一个元素向前进行处理。 ```javascript var list = [[0, 1], [2, 3], [4, 5]]; var flat = _.reduceRight(list, function(a, b) { return a.concat(b); }, []); // [4, 5, 2, 3, 0, 1] ``` **(5)shuffle** shuffle方法返回一个打乱次序的集合。 ```javascript _.shuffle([1, 2, 3, 4, 5, 6]); // [4, 1, 6, 3, 5, 2] ``` **(6)invoke** invoke方法对集合的每个成员执行指定的操作。 ```javascript _.invoke([[5, 1, 7], [3, 2, 1]], 'sort') // [[1, 5, 7], [1, 2, 3]] ``` **(7)sortBy** sortBy方法根据处理函数的返回值,返回一个排序后的集合,以升序排列。 ```javascript _.sortBy([1, 2, 3, 4, 5, 6], function(num){ return Math.sin(num); }); // [5, 4, 6, 3, 1, 2] ``` **(8)indexBy** indexBy方法返回一个对象,根据指定键名,对集合生成一个索引。 ```javascript var person = [{name: 'John', age: 40}, {name: 'larry', age: 50}, {name: 'curly', age: 60}]; _.indexBy(person, 'age'); // { "50": {name: 'larry', age: 50}, "60": {name: 'curly', age: 60} } ``` ### 集合特征 Underscore.js提供了一系列方法,判断数组元素的特征。这些方法都返回一个布尔值,表示是否满足条件。 **(1)every** every方法判断数组的所有元素是否都满足某个条件。如果都满足则返回true,否则返回false。 ```javascript _.every([1, 2, 3, 4, 5, 6], function(num){ return num % 2 == 0; }); // false ``` **(2)some** some方法则是只要有一个元素满足,就返回true,否则返回false。 ```javascript _.some([1, 2, 3, 4, 5, 6], function(num){ return num % 2 == 0; }); // true _.some([null, 0, 'yes', false]) // true ``` **(3)size** size方法返回集合的成员数量。 ```javascript _.size({one : 1, two : 2, three : 3}); // 3 ``` **(4)sample** sample方法用于从集合中随机取样。 ```javascript _.sample([1, 2, 3, 4, 5, 6]) // 4 ``` ### 集合过滤 Underscore.js提供了一系列方法,用于过滤数组,找到符合要求的成员。 **(1)filter** filter方法依次对集合的每个成员进行某种操作,只返回操作结果为true的成员。 ```javascript _.filter([1, 2, 3, 4, 5, 6], function(num){ return num % 2 == 0; }); // [2, 4, 6] ``` **(2)reject** reject方法只返回操作结果为false的成员。 ```javascript _.reject([1, 2, 3, 4, 5, 6], function(num){ return num % 2 == 0; }); // [1, 3, 5] ``` **(3)find** find方法依次对集合的每个成员进行某种操作,返回第一个操作结果为true的成员。如果所有成员的操作结果都为false,则返回undefined。 ```javascript _.find([1, 2, 3, 4, 5, 6], function(num){ return num % 2 == 0; }); // 2 ``` **(4)contains** contains方法表示如果某个值在数组内,则返回true,否则返回false。 ```javascript _.contains([1, 2, 3], 3); // true ``` **(5)countBy** countBy方法依次对集合的每个成员进行某种操作,将操作结果相同的成员算作一类,最后返回一个对象,表明每种操作结果对应的成员数量。 ```javascript _.countBy([1, 2, 3, 4, 5], function(num) { return num % 2 == 0 ? 'even' : 'odd'; }); // {odd: 3, even: 2} ``` **(6)where** where方法检查集合中的每个值,返回一个数组,其中的每个成员都包含指定的键值对。 ```javascript _.where(listOfPlays, {author: "Shakespeare", year: 1611}); // [{title: "Cymbeline", author: "Shakespeare", year: 1611}, // {title: "The Tempest", author: "Shakespeare", year: 1611}] ``` **(7)max,min** max方法返回集合中的最大值。如果提供一个处理函数,则该函数的返回值用作排名标准。 ```javascript var person = [{name: 'John', age: 40}, {name: 'larry', age: 50}, {name: 'curly', age: 60}]; _.max(person, function(per){ return per.age; }); // {name: 'curly', age: 60}; ``` min方法返回集合中的最小值。如果提供一个处理函数,则该函数的返回值用作排名标准。 ```javascript var numbers = [10, 5, 100, 2, 1000]; _.min(numbers) // 2 ``` ## 对象相关方法 **(1)toArray** toArray方法将对象转为数组,只包含对象成员的值。典型应用是将对类似数组的对象转为真正的数组。 ```javascript _.toArray({a:0,b:1,c:2}); // [0, 1, 2] ``` **(2)pluck** pluck方法将多个对象的某一个属性的值,提取成一个数组。 ```javascript var person = [{name: 'John', age: 40}, {name: 'larry', age: 50}, {name: 'curly', age: 60}]; _.pluck(person, 'name'); // ["moe", "larry", "curly"] ``` ## 与函数相关的方法 ### 绑定运行环境和参数 在不同的运行环境下,JavaScript函数内部的变量所在的上下文是不同的。这种特性会给程序带来不确定性,为了解决这个问题,Underscore.js提供了两个方法,用来给函数绑定上下文。 **(1)bind方法** 该方法绑定函数运行时的上下文,返回一个新函数。 ```javascript var o = { p: 2, m: function (){console.log(this.p);} }; o.m() // 2 _.bind(o.m,{p:1})() // 1 ``` 上面代码将o.m方法绑定到一个新的对象上面。 除了前两个参数以外,bind方法还可以接受更多参数,它们表示函数方法运行时所需的参数。 ```javascript var add = function(n1,n2,n3) { console.log(this.sum + n1 + n2 + n3); }; _.bind(add, {sum:1}, 1, 1, 1)() // 4 ``` 上面代码中bind方法有5个参数,最后那三个是给定add方法的运行参数,所以运行结果为4。 **(2)bindall方法** 该方法可以一次将多个方法,绑定在某个对象上面。 ```javascript var o = { p1 : '123', p2 : '456', m1 : function() { console.log(this.p1); }, m2 : function() { console.log(this.p2); }, }; _.bindAll(o, 'm1', 'm2'); ``` 上面代码一次性将两个方法(m1和m2)绑定在o对象上面。 **(3)partial方法** 除了绑定上下文,Underscore.js还允许绑定参数。partial方法将函数与某个参数绑定,然后作为一个新函数返回。 ```javascript var add = function(a, b) { return a + b; }; add5 = _.partial(add, 5); add5(10); // 15 ``` **(4)wrap方法** 该方法将一个函数作为参数,传入另一个函数,最终返回前者的一个新版本。 ```javascript var hello = function(name) { return "hello: " + name; }; hello = _.wrap(hello, function(func) { return "before, " + func("moe") + ", after"; }); hello(); // 'before, hello: moe, after' ``` 上面代码先定义hello函数,然后将hello传入一个匿名定义,返回一个新版本的hello函数。 **(5)compose方法** 该方法接受一系列函数作为参数,由后向前依次运行,上一个函数的运行结果,作为后一个函数的运行参数。也就是说,将f(g(),h())的形式转化为f(g(h()))。 ```javascript var greet = function(name){ return "hi: " + name; }; var exclaim = function(statement){ return statement + "!"; }; var welcome = _.compose(exclaim, greet); welcome('moe'); // 'hi: moe!' ``` 上面代码调用welcome时,先运行greet函数,再运行exclaim函数。并且,greet函数的运行结果是exclaim函数运行时的参数。 ### 函数运行控制 Underscore.js允许对函数运行行为进行控制。 **(1)memoize方法** 该方法缓存一个函数针对某个参数的运行结果。 ```javascript var fibonacci = _.memoize(function(n) { return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2); }); ``` **(2)delay方法** 该方法可以将函数推迟指定的时间再运行。 ```javascript var log = _.bind(console.log, console); _.delay(log, 1000, 'logged later'); // 'logged later' ``` 上面代码推迟1000毫秒,再运行console.log方法,并且指定参数为“logged later”。 **(3)defer方法** 该方法可以将函数推迟到待运行的任务数为0时再运行,类似于setTimeout推迟0秒运行的效果。 ```javascript _.defer(function(){ alert('deferred'); }); ``` **(4)throttle方法** 该方法返回一个函数的新版本。连续调用这个新版本的函数时,必须等待一定时间才会触发下一次执行。 ```javascript // 返回updatePosition函数的新版本 var throttled = _.throttle(updatePosition, 100); // 新版本的函数每过100毫秒才会触发一次 $(window).scroll(throttled); ``` **(5)debounce方法** 该方法返回的新函数有调用的时间限制,每次调用必须与上一次调用间隔一定的时间,否则就无效。它的典型应用是防止用户双击某个按钮,导致两次提交表单。 ```javascript $("button").on("click", _.debounce(submitForm, 1000, true)); ``` 上面代码表示click事件发生后,调用函数submitForm的新版本。该版本的两次运行时间,必须间隔1000毫秒以上,否则第二次调用无效。最后那个参数true,表示click事件发生后,立刻触发第一次submitForm函数,否则就是等1000毫秒再触发。 **(6)once方法** 该方法返回一个只能运行一次的新函数。该方法主要用于对象的初始化。 ```javascript var initialize = _.once(createApplication); initialize(); initialize(); // Application只被创造一次 ``` **(7)after方法** 该方法返回的新版本函数,只有在被调用一定次数后才会运行,主要用于确认一组操作全部完成后,再做出反应。 ```javascript var renderNotes = _.after(notes.length, render); _.each(notes, function(note) { note.asyncSave({success: renderNotes}); }); ``` 上面代码表示,函数renderNotes是函数render的新版本,只有调用notes.length次以后才会运行。所以,后面就可以放心地等到notes的每个成员都处理完,才会运行一次renderNotes。 ## 工具方法 ### 链式操作 Underscore.js允许将多个操作写成链式的形式。 ```javascript _.(users) .filter(function(user) { return user.name === name }) .sortBy(function(user) { return user.karma }) .first() .value() ``` ### template 该方法用于编译HTML模板。它接受三个参数。 ```javascript _.template(templateString, [data], [settings]) ``` 三个参数的含义如下: - templateString:模板字符串 - data:输入模板的数据 - settings:设置 **(1)templateString** 模板字符串templateString就是普通的HTML语言,其中的变量使用<%= … %>的形式插入;data对象负责提供变量的值。 ```javascript var txt = "<h2><%= word %></h2>"; _.template(txt, {word : "Hello World"}) // "<h2>Hello World</h2>" ``` 如果变量的值包含五个特殊字符(& < > " ' /),就需要用<%- ... %>转义。 ```javascript var txt = "<h2><%- word %></h2>"; _.template(txt, {word : "H & W"}) // <h2>H &amp; W</h2> ``` JavaScript命令可以采用<% … %>的形式插入。下面是判断语句的例子。 ```javascript var txt = "<% var i = 0; if (i<1){ %>" + "<%= word %>" + "<% } %>"; _.template(txt, {word : "Hello World"}) // Hello World ``` 常见的用法还有循环语句。 ```javascript var list = "<% _.each(people, function(name) { %> <li><%= name %></li> <% }); %>"; _.template(list, {people : ['moe', 'curly', 'larry']}); // "<li>moe</li><li>curly</li><li>larry</li>" ``` 如果template方法只有第一个参数templateString,省略第二个参数,那么会返回一个函数,以后可以向这个函数输入数据。 ```javascript var t1 = _.template("Hello <%=user%>!"); t1({ user: "<Jane>" }) // 'Hello <Jane>!' ``` ** (2)data ** templateString中的所有变量,在内部都是obj对象的属性,而obj对象就是指第二个参数data对象。下面两句语句是等同的。 ```javascript _.template("Hello <%=user%>!", { user: "<Jane>" }) _.template("Hello <%=obj.user%>!", { user: "<Jane>" }) ``` 如果要改变obj这个对象的名字,需要在第三个参数中设定。 ```javascript _.template("<%if (data.title) { %>Title: <%= title %><% } %>", null, { variable: "data" }); ``` 因为template在变量替换时,内部使用with语句,所以上面这样的做法,运行速度会比较快。 <h2 id="10.2">Modernizr</h2> ## 概述 随着HTML5和CSS3加入越来越多的模块,检查各种浏览器是否支持这些模块,成了一大难题。Modernizr就是用来解决这个问题的一个JavaScript库。 首先,从modernizr.com下载这个库。下载的时候,可以选择所需要的模块。然后,将它插入HTML页面的头部,放在head标签之中。 ```html <!DOCTYPE html> <html class="no-js" lang="en"> <head> <meta charset="utf-8"> <script src="js/modernizr.js"></script> </head> </html> ``` ## CSS的新增class 使用Modernizr以后,首先会把html元素的class替换掉。以chrome浏览器为例,新增的class大概是下面的样子。 ```html <html class="js no-touch postmessage history multiplebgs boxshadow opacity cssanimations csscolumns cssgradients csstransforms csstransitions fontface localstorage sessionstorage svg inlinesvg blobbuilder blob bloburls download formdata"> ``` IE 7则是这样: ```html <html class="js no-touch postmessage no-history no-multiplebgs no-boxshadow no-opacity no-cssanimations no-csscolumns no-cssgradients no-csstransforms no-csstransitions fontface localstorage sessionstorage no-svg no-inlinesvg wf-loading no-blobbuilder no-blob no-bloburls no-download no-formdata"> ``` 然后,就可以针对不同的CSS class,指定不同的样式。 ```css .button { background: #000; opacity: 0.75; } .no-opacity .button { background: #444; } ``` ## JavaScript侦测 除了提供新增的CSS class,Modernizr还提供JavaScript方法,用来侦测浏览器是否支持某个功能。 ```javascript Modernizr.cssgradients; //True in Chrome, False in IE7 Modernizr.fontface; //True in Chrome, True in IE7 Modernizr.geolocation; //True in Chrome, False in IE7 if (Modernizr.canvas){ // 支持canvas } else { // 不支持canvas } if (Modernizr.touch){ // 支持触摸屏 } else { // 不支持触摸屏 } ``` ## 加载器 Modernizr允许根据Javascript侦测的不同结果,加载不同的脚本文件。 ```javascript Modernizr.load({ test : Modernizr.localstorage, yep : 'localStorage.js', nope : 'alt-storageSystem.js', complete : function () { enableStorgeSaveUI();} }); ``` Modernizr.load方法用来加载脚本。它的属性如下: - test:用来测试浏览器是否支持某个属性。 - yep:如果浏览器支持该属性,加载的脚本。 - nope:如果浏览器不支持该属性,加载的脚本。 - complete:加载完成后,运行的JavaScript代码。 可以指定在支持某个功能的情况,所要加载的JavaScript脚本和CSS样式。 ```javascript Modernizr.load({ test : Modernizr.touch, yep : ['js/touch.js', 'css/touchStyles.css'] }); ``` <h2 id="10.3">Datejs</h2> ## 概述 Datejs是一个用来操作日期的库,官方网站为[datejs.com](http://www.datejs.com/)。 下载后插入网页,就可以使用。 ```html <script type="text/javascript" src="date.js"></script> ``` 官方还提供多种语言的版本,可以选择使用。 ```html // 美国版 <script type="text/javascript" src="date-en-US.js"></script> // 中国版 <script type="text/javascript" src="date-zh-CN.js"></script> ``` ## 方法 Datejs在原生的Date对象上面,定义了许多语义化的方法,可以方便地链式使用。 ### 日期信息 ```javascript Date.today() // 返回当天日期,时间定在这一天开始的00:00 Date.today().getDayName() // 今天是星期几 Date.today().is().friday() // 今天是否为星期五,返回true或者false Date.today().is().fri() // 等同于上一行 Date.today().is().november() // 今天是否为11月,返回true或者false Date.today().is().nov() // 等同于上一行 Date.today().isWeekday() // 今天是否为工作日(周一到周五) ``` ### 日期的变更 ```javascript Date.today().next().friday() // 下一个星期五 Date.today().last().monday() // 上一个星期一 new Date().next().march() // 下个三月份的今天 new Date().last().week() // 上星期的今天 Date.today().add(5).days() // 五天后 Date.friday() // 本周的星期五 Date.march() // 今年的三月 Date.january().first().monday() // 今年一月的第一个星期一 Date.dec().final().fri() // 今年12月的最后一个星期五 // 先将日期定在本月15日的下午4点30分,然后向后推90天 Date.today().set({ day: 15, hour: 16, minute: 30 }).add({ days: 90 }) (3).days().fromNow() // 三天后 (6).months().ago() // 6个月前 (12).weeks().fromNow() // 12个星期后 (30).days().after(Date.today()) // 30天后 ``` ### 日期的解析 ```javascript Date.parse('today') Date.parse('tomorrow') Date.parse('July 8') Date.parse('July 8th, 2007') Date.parse('July 8th, 2007, 10:30 PM') Date.parse('07.15.2007') ``` <h2 id="10.4">D3.js</h2> D3.js是一个用于网页作图、生成互动图形的JavaScript函数库。它提供一个d3对象,所有方法都通过这个对象调用。 ## 操作网页元素 D3提供了一系列操作网页元素的方法,很类似jQuery,也是先选中某个元素(select方法),然后对其进行某种操作。 ```javascript var body = d3.select("body"); var div = body.append("div"); div.html("Hello, world!"); ``` select方法用于选中一个元素,而selectAll方法用于选中一组元素。 ```javascript var section = d3.selectAll("section"); var div = section.append("div"); div.html("Hello, world!"); ``` 大部分D3的方法都返回D3对象的实例,这意味着可以采用链式写法。 ```javascript d3.select("body") .style("color", "black") .style("background-color", "white"); ``` 需要注意的是append方法返回一个新对象。 ```javascript d3.selectAll("section") .attr("class", "special") .append("div") .html("Hello, world!"); ``` ## 生成svg元素 D3作图需要svg元素,可以用JavaScript代码动态生成。 ```javascript var v = d3.select("#graph") .append("svg"); v.attr("width", 900).attr("height", 400); ``` ## 生成图形 ### 选中对象集 selectAll方法不仅可以选中现有的网页元素,还可以选中不存在的网页元素。 ```javascript d3.select(".chart") .selectAll("div"); ``` 上面代码表示,selectAll方法选中了.chart元素下面所有现有和将来可能出现的div元素。 ### 绑定数据 data方法用于对选中的结果集绑定数据。 ```javascript var data = [4, 8, 15, 16, 23, 42, 12]; d3.select(".chart") .selectAll("div") .data(data) .enter().append("div") .style("width", function(d) { return d * 10 + "px"; }) .text(function(d) { return d; }); ``` 上面代码中,enter方法和append方法表示由于此时div元素还不存在,必须根据数据的个数将它们创造出来。style方法和text方法的参数是函数,表示函数的运行结果就是设置网页元素的值。 上面代码的运行结果是生成一个条状图,但是没有对条状图的长度进行控制,下面采用scale.linear方法对数据长度进行设置。 ```javascript var data = [4, 8, 15, 16, 23, 42, 12]; var x = d3.scale.linear() .domain([0, d3.max(data)]) .range([0, 420]); d3.select(".chart") .selectAll("div") .data(data) .enter().append("div") .style("width", function(d) { return x(d) + "px"; }) .text(function(d) { return d; }); ``` ## 操作SVG图形 使用SVG图形生成条形图,首先是选中矢量图格式,然后每个数据值生成一个g元素(group),再在每个g元素内部生成一个rect元素和text元素。 ```javascript var width = 840, barHeight = 20; var x = d3.scale.linear() .domain([0, d3.max(dataArray)]) .range([0, width]); var chart = d3.select(".bar-chart-svg") .attr("width", width) .attr("height", barHeight * dataArray.length); var bar = chart.selectAll("g") .data(dataArray) .enter().append("g") .attr("transform", function(d, i) { return "translate(0," + i * barHeight + ")"; }); bar.append("rect") .attr("width", x) .attr("height", barHeight - 1); bar.append("text") .attr("x", function(d) { return x(d) - 3; }) .attr("y", barHeight / 2) .attr("dy", ".35em") .text(function(d) { return d; }); ``` ## 加载XML文件 ```javascript d3.xml('example', 'image/svg+xml', function (error, data) { if (error) { console.log('加载SVG文件出错!', error); } else { // 处理SVG文件 } }); ``` <h2 id="10.5">设计模式</h2> "设计模式"(Design Pattern)是针对编程中经常出现的、具有共性的问题,所提出的解决方法。著名的《设计模式》一书一共提出了23种模式。 ## Singleton Singleton模式指的是一个“类”只能创造一个实例。由于JavaScript语言没有类,单个对象可以直接生成,所以实际上,没有必要部署Singleton模式。但是,还是可以做到的。 ```javascript var someClass = { _singleton: null, getSingleton: function() { if (!this._singleton) { this._singleton = { // some code here } } return this._singleton; } }; var instance = someClass.getSingleton(); ``` 生成实例的时候,调用getSingleton方法。该方法首先检查_singleton属性是否有值,如果有值就返回这个属性,如果为空则生成新的实例,并赋值给_singleton属性,然后返回这个实例。这样就保证了生成的实例都是同一个对象。 为了保证实例不被改写,可以关闭它的写入开关。 ```javascript Object.defineProperty(namespace, "singleton", { writable: false, configurable: false, value: { ... } }); ``` 也可以考虑使用Object.preventExtensions()、Object.seal()、Object.freeze()等方法,限制对实例进行写操作。 <h2 id="10.6">排序算法</h2> 排序算法是将一系列的值按照顺序进行排列的方法。 ## 冒泡排序 ### 简介 冒泡排序(Bubble Sort)是最易懂的排序算法,但是效率较低,生产环境中很少使用。 它的基本思想是: 1. 依次比较相邻的两个数,如果不符合排序规则,则调换两个数的位置。这样一遍比较下来,能够保证最大(或最小)的数排在最后一位。 2. 再对最后一位以外的数组,重复前面的过程,直至全部排序完成。 由于每进行一次这个过程,在该次比较的最后一个位置上,正确的数会自己冒出来,就好像“冒泡”一样,这种算法因此得名。 以对数组[3, 2, 4, 5, 1] 进行从小到大排序为例,步骤如下: 1. 第一位的“3”与第二位的“2”进行比较,3大于2,互换位置,数组变成[2, 3, 4, 5, 1] 。 2. 第二位的“3”与第三位的“4”进行比较,3小于4,数组不变。 3. 第三位的“4”与第四位的“5”进行比较,4小于5,数组不变。 4. 第四位的“5”与第五位的“1”进行比较,5大于1,互换位置,数组变成[2, 3, 4, 1, 5] 。 第一轮排序完成,可以看到最后一位的5,已经是正确的数了。然后,再对剩下的数[2, 3, 4, 1] 重复这个过程,每一轮都会在本轮最后一位上出现正确的数。直至剩下最后一个位置,所有排序结束。 ### 算法实现 先定义一个交换函数,作用是交换两个位置的值。 ```javascript function swap(myArray, p1, p2){ var temp = myArray[p1]; myArray[p1] = myArray[p2]; myArray[p2] = temp; } ``` 然后定义主函数。 ```javascript function bubbleSort(myArray){ var len = myArray.length, i, j, stop; for (i=0; i < len; i++){ for (j=0, stop=len-1-i; j < stop; j++){ if (myArray[j] > myArray[j+1]){ swap(myArray, j, j+1); } } } return myArray; } ``` ## 选择排序 ### 简介 选择排序(Selection Sort)与冒泡排序类似,也是依次对相邻的数进行两两比较。不同之处在于,它不是每比较一次就调换位置,而是一轮比较完毕,找到最大值(或最小值)之后,将其放在正确的位置,其他数的位置不变。 以对数组[3, 2, 4, 5, 1] 进行从小到大排序为例,步骤如下: 1. 假定第一位的“3”是最小值。 2. 最小值“3”与第二位的“2”进行比较,2小于3,所以新的最小值是第二位的“2”。 3. 最小值“2”与第三位的“4”进行比较,2小于4,最小值不变。 4. 最小值“2”与第四位的“5”进行比较,2小于5,最小值不变。 5. 最小值“2”与第五位的“1”进行比较,1小于2,所以新的最小值是第五位的“1”。 6. 第五位的“1”与第一位的“3”互换位置,数组变为[1, 2, 4, 5, 3]。 这一轮比较结束后,最小值“1”已经排到正确的位置了,然后对剩下的[2, 4, 5, 3]重复上面的过程。每一轮排序都会将该轮的最小值排到正确的位置,直至剩下最后一个位置,所有排序结束。 ### 算法实现 先定义一个交换函数。 ```javascript function swap(myArray, p1, p2){ var temp = myArray[p1]; myArray[p1] = myArray[p2]; myArray[p2] = temp; } ``` 然后定义主函数。 ```javascript function selectionSort(myArray){ var len = myArray.length, min; for (i=0; i < len; i++){ // 将当前位置设为最小值 min = i; // 检查数组其余部分是否更小 for (j=i+1; j < len; j++){ if (myArray[j] < myArray[min]){ min = j; } } // 如果当前位置不是最小值,将其换为最小值 if (i != min){ swap(myArray, i, min); } } return myArray; } ``` ## 插入排序 ### 简介 插入排序(insertion sort)比前面两种排序方法都更有效率。它将数组分成“已排序”和“未排序”两部分,一开始的时候,“已排序”的部分只有一个元素,然后将它后面一个元素从“未排序”部分插入“已排序”部分,从而“已排序”部分增加一个元素,“未排序”部分减少一个元素。以此类推,完成全部排序。 以对数组[3, 2, 4, 5, 1] 进行从小到大排序为例,步骤如下: 1. 将数组分成[3]和[2, 4, 5, 1]两部分,前者是已排序的,后者是未排序的。 2. 取出未排序部分的第一个元素“2”,与已排序部分最后一个元素“3”比较,因为2小于3,所以2排在3前面,整个数组变成[2, 3]和[4, 5, 1]两部分。 3. 取出未排序部分的第一个元素“4”,与已排序部分最后一个元素“3”比较,因为4大于3,所以4排在3后面,整个数组变成[2, 3, 4]和[5, 1]两部分。 4. 取出未排序部分的第一个元素“5”,与已排序部分最后一个元素“4”比较,因为5大于4,所以5排在4后面,整个数组变成[2, 3, 4, 5]和[1]两部分。 5. 取出未排序部分的第一个元素“1”,与已排序部分最后一个元素“5”比较,因为1小于5,所以再与前一个元素“4”比较;因为1小于4,再与前一个元素“3”比较;因为1小于3,再与前一个元素“2”比较;因为小于1小于2,所以“1”排在2的前面,整个数组变成[1, 2, 3, 4, 5]。 ### 算法实现 算法的实现如下: ```javascript function insertionSort(myArray) { var len = myArray.length, // 数组的长度 value, // 当前比较的值 i, // 未排序部分的当前位置 j; // 已排序部分的当前位置 for (i=0; i < len; i++) { // 储存当前位置的值 value = myArray[i]; /* * 当已排序部分的当前元素大于value, * 就将当前元素向后移一位,再将前一位与value比较 */ for (j=i-1; j > -1 && myArray[j] > value; j--) { myArray[j+1] = myArray[j]; } myArray[j+1] = value; } return myArray; } ``` ## 合并排序 ### 简介 前面三种排序算法只有教学价值,因为效率低,很少实际使用。合并排序(Merge sort)则是一种被广泛使用的排序方法。 它的基本思想是,将两个已经排序的数组合并,要比从头开始排序所有元素来得快。因此,可以将数组拆开,分成n个只有一个元素的数组,然后不断地两两合并,直到全部排序完成。 以对数组[3, 2, 4, 5, 1] 进行从小到大排序为例,步骤如下: 1. 将数组分成[3, 2, 4]和[5, 1]两部分。 2. 将[3, 2, 4]分成[3, 2]和[4]两部分。 3. 将[3, 2]分成[3]和[2]两部分,然后合并成[2, 3]。 4. 将[2, 3]和[4]合并成[2, 3, 4]。 5. 将[5, 1]分成[5]和[1]两部分,然后合并成[1, 5]。 6. 将[2, 3, 4]和[1, 5]合并成[1, 2, 3, 4, 5]。 ### 算法实现 这里的关键是如何合并两个已经排序的数组。具体实现请看下面的函数。 ```javascript function merge(left, right){ var result = [], il = 0, ir = 0; while (il < left.length && ir < right.length){ if (left[il] < right[ir]){ result.push(left[il++]); } else { result.push(right[ir++]); } } return result.concat(left.slice(il)).concat(right.slice(ir)); } ``` 上面的merge函数,合并两个已经按升序排好序的数组。首先,比较两个数组的第一个元素,将其中较小的一个放入result数组;然后,将其中较大的一个与另一个数组的第二个元素进行比较,再将其中较小的一个放入result数组的第二个位置。以此类推,直到一个数组的所有元素都进入result数组为止,再将另一个数组剩下的元素接着result数组后面返回(使用concat方法)。 有了merge函数,就可以对任意数组排序了。基本方法是将数组不断地拆成两半,直到每一半只包含零个元素或一个元素为止,然后就用merge函数,将拆成两半的数组不断合并,直到合并成一整个排序完成的数组。 ```javascript function mergeSort(myArray){ if (myArray.length < 2) { return myArray; } var middle = Math.floor(myArray.length / 2), left = myArray.slice(0, middle), right = myArray.slice(middle); return merge(mergeSort(left), mergeSort(right)); } ``` 上面的代码有一个问题,就是返回的是一个全新的数组,会多占用空间。因此,修改上面的函数,使之在原地排序,不多占用空间。 ```javascript function mergeSort(myArray){ if (myArray.length < 2) { return myArray; } var middle = Math.floor(myArray.length / 2), left = myArray.slice(0, middle), right = myArray.slice(middle), params = merge(mergeSort(left), mergeSort(right)); // 在返回的数组头部,添加两个元素,第一个是0,第二个是返回的数组长度 params.unshift(0, myArray.length); // splice用来替换数组元素,它接受多个参数, // 第一个是开始替换的位置,第二个是需要替换的个数,后面就是所有新加入的元素。 // 因为splice不接受数组作为参数,所以采用apply的写法。 // 这一句的意思就是原来的myArray数组替换成排序后的myArray myArray.splice.apply(myArray, params); // 返回排序后的数组 return myArray; } ``` ## 快速排序 ### 简介 快速排序(quick sort)是公认最快的排序算法之一,有着广泛的应用。 它的基本思想很简单:先确定一个“支点”(pivot),将所有小于“支点”的值都放在该点的左侧,大于“支点”的值都放在该点的右侧,然后对左右两侧不断重复这个过程,直到所有排序完成。 具体做法是: 1. 确定“支点”(pivot)。虽然数组中任意一个值都能作为“支点”,但通常是取数组的中间值。 2. 建立两端的指针。左侧的指针指向数组的第一个元素,右侧的指针指向数组的最后一个元素。 3. 左侧指针的当前值与“支点”进行比较,如果小于“支点”则指针向后移动一位,否则指针停在原地。 4. 右侧指针的当前值与“支点”进行比较,如果大于“支点”则指针向前移动一位,否则指针停在原地。 5. 左侧指针的位置与右侧指针的位置进行比较,如果前者大于等于后者,则本次排序结束;否则,左侧指针的值与右侧指针的值相交换。 6. 对左右两侧重复第2至5步。 以对数组[3, 2, 4, 5, 1] 进行从小到大排序为例,步骤如下: 1. 选择中间值“4”作为“支点”。 2. 第一个元素3小于4,左侧指针向后移动一位;第二个元素2小于4,左侧指针向后移动一位;第三个元素4等于4,左侧指针停在这个位置(数组的第2位)。 3. 倒数第一个元素1小于4,右侧指针停在这个位置(数组的第4位)。 4. 左侧指针的位置(2)小于右侧指针的位置(4),两个位置的值互换,数组变成[3, 2, 1, 5, 4]。 5. 左侧指针向后移动一位,第四个元素5大于4,左侧指针停在这个位置(数组的第3位)。 6. 右侧指针向前移动一位,第四个元素5大于4,右侧指针移动向前移动一位,第三个元素1小于4,右侧指针停在这个位置(数组的第3位)。 7. 左侧指针的位置(3)大于右侧指针的位置(2),本次排序结束。 8. 对 [3, 2, 1]和[5, 4]两部分各自不断重复上述步骤,直到排序完成。 ### 算法实现 首先部署一个swap函数,用于互换两个位置的值。 ```javascript function swap(myArray, firstIndex, secondIndex){ var temp = myArray[firstIndex]; myArray[firstIndex] = myArray[secondIndex]; myArray[secondIndex] = temp; } ``` 然后,部署一个partition函数,用于完成一轮排序。 ```javascript function partition(myArray, left, right) { var pivot = myArray[Math.floor((right + left) / 2)], i = left, j = right; while (i <= j) { while (myArray[i] < pivot) { i++; } while (myArray[j] > pivot) { j--; } if (i <= j) { swap(myArray, i, j); i++; j--; } } return i; } ``` 接下来,就是递归上面的过程,完成整个排序。 ```javascript function quickSort(myArray, left, right) { if (myArray.length < 2) return myArray; left = (typeof left !== "number" ? 0 : left); right = (typeof right !== "number" ? myArray.length - 1 : right); var index = partition(myArray, left, right); if (left < index - 1) { quickSort(myArray, left, index - 1); } if (index < right) { quickSort(myArray, index, right); } return myArray; } ``` <h2 id="10.7">PhantomJS</h2> ## 概述 有时,我们需要浏览器处理网页,但并不需要浏览,比如生成网页的截图、抓取网页数据等操作。[PhantomJS](http://phantomjs.org/)的功能,就是提供一个浏览器环境的命令行接口,你可以把它看作一个“虚拟浏览器”,除了不能浏览,其他与正常浏览器一样。它的内核是WebKit引擎,不提供图形界面,只能在命令行下使用,我们可以用它完成一些特殊的用途。 PhantomJS是二进制程序,需要[安装](http://phantomjs.org/download.html)后使用。 ```bash $ npm install phantomjs -g ``` 使用下面的命令,查看是否安装成功。 ```bash $ phantomjs --version ``` ## REPL环境 phantomjs提供了一个完整的REPL环境,允许用户通过命令行与PhantomJS互动。键入phantomjs,就进入了该环境。 ```bash $ phantomjs ``` 这时会跳出一个phantom提示符,就可以输入Javascript命令了。 ```bash phantomjs> 1+2 3 phantomjs> function add(a,b) { return a+b; } undefined phantomjs> add(1,2) 3 ``` 按ctrl+c可以退出该环境。 下面,我们把上面的add()函数写成一个文件add.js文件。 ```javascript // add.js function add(a,b){ return a+b; } console.log(add(1,2)); phantom.exit(); ``` 上面的代码中,console.log()的作用是在终端窗口显示,phantom.exit()则表示退出phantomjs环境。一般来说,不管什么样的程序,exit这一行都不能少。 现在,运行该程序。 ```bash $ phantomjs add.js ``` 终端窗口就会显示结果为3。 下面是更多的例子。 ```javascript phantomjs> phantom.version { "major": 1, "minor": 5, "patch": 0 } phantomjs> console.log("phantom is awesome") phantom is awesome phantomjs> window.navigator { "cookieEnabled": true, "language": "en-GB", "productSub": "20030107", "product": "Gecko", // ... } ``` ## webpage模块 webpage模块是PhantomJS的核心模块,用于网页操作。 ```javascript var webPage = require('webpage'); var page = webPage.create(); ``` 上面代码表示加载PhantomJS的webpage模块,并创建一个实例。 下面是webpage实例的属性和方法介绍。 ### open() open方法用于打开具体的网页。 ```javascript var page = require('webpage').create(); page.open('http://slashdot.org', function (s) { console.log(s); phantom.exit(); }); ``` 上面代码中,open()方法,用于打开具体的网页。它接受两个参数。第一个参数是网页的网址,这里打开的是著名新闻网站[Slashdot](http://slashdot.org),第二个参数是回调函数,网页打开后该函数将会运行,它的参数是一个表示状态的字符串,如果打开成功就是success,否则就是fail。 注意,只要接收到服务器返回的结果,PhantomJS就会报告网页打开成功,而不管服务器是否返回404或500错误。 open方法默认使用GET方法,与服务器通信,但是也可以使用其他方法。 ```javascript var webPage = require('webpage'); var page = webPage.create(); var postBody = 'user=username&password=password'; page.open('http://www.google.com/', 'POST', postBody, function(status) { console.log('Status: ' + status); // Do other things here... }); ``` 上面代码中,使用POST方法向服务器发送数据。open方法的第二个参数用来指定HTTP方法,第三个参数用来指定该方法所要使用的数据。 open方法还允许提供配置对象,对HTTP请求进行更详细的配置。 ```javascript var webPage = require('webpage'); var page = webPage.create(); var settings = { operation: "POST", encoding: "utf8", headers: { "Content-Type": "application/json" }, data: JSON.stringify({ some: "data", another: ["custom", "data"] }) }; page.open('http://your.custom.api', settings, function(status) { console.log('Status: ' + status); // Do other things here... }); ``` ### evaluate() evaluate方法用于打开网页以后,在页面中执行JavaScript代码。 ```javascript var page = require('webpage').create(); page.open(url, function(status) { var title = page.evaluate(function() { return document.title; }); console.log('Page title is ' + title); phantom.exit(); }); ``` 网页内部的console语句,以及evaluate方法内部的console语句,默认不会显示在命令行。这时可以采用onConsoleMessage回调函数,上面的例子可以改写如下。 ```javascript var page = require('webpage').create(); page.onConsoleMessage = function(msg) { console.log('Page title is ' + msg); }; page.open(url, function(status) { page.evaluate(function() { console.log(document.title); }); phantom.exit(); }); ``` 上面代码中,evaluate方法内部有console语句,默认不会输出在命令行。这时,可以用onConsoleMessage方法监听这个事件,进行处理。 ### includeJs() includeJs方法用于页面加载外部脚本,加载结束后就调用指定的回调函数。 ```javascript var page = require('webpage').create(); page.open('http://www.sample.com', function() { page.includeJs("http://path/to/jquery.min.js", function() { page.evaluate(function() { $("button").click(); }); phantom.exit() }); }); ``` 上面的例子在页面中注入jQuery脚本,然后点击所有的按钮。需要注意的是,由于是异步加载,所以`phantom.exit()`语句要放在`page.includeJs()`方法的回调函数之中,否则页面会过早退出。 ### render() render方法用于将网页保存成图片,参数就是指定的文件名。该方法根据后缀名,将网页保存成不同的格式,目前支持PNG、GIF、JPEG和PDF。 ```javascript var webPage = require('webpage'); var page = webPage.create(); page.viewportSize = { width: 1920, height: 1080 }; page.open("http://www.google.com", function start(status) { page.render('google_home.jpeg', {format: 'jpeg', quality: '100'}); phantom.exit(); }); ``` 该方法还可以接受一个配置对象,format字段用于指定图片格式,quality字段用于指定图片质量,最小为0,最大为100。 ### viewportSize,zoomFactor viewportSize属性指定浏览器视口的大小,即网页加载的初始浏览器窗口大小。 ```javascript var webPage = require('webpage'); var page = webPage.create(); page.viewportSize = { width: 480, height: 800 }; ``` viewportSize的Height字段必须指定,不可省略。 zoomFactor属性用来指定渲染时(render方法和renderBase64方法)页面的放大系数,默认是1(即100%)。 ```javascript var webPage = require('webpage'); var page = webPage.create(); page.zoomFactor = 0.25; page.render('capture.png'); ``` ### onResourceRequested onResourceRequested属性用来指定一个回调函数,当页面请求一个资源时,会触发这个回调函数。它的第一个参数是HTTP请求的元数据对象,第二个参数是发出的网络请求对象。 HTTP请求包括以下字段。 - id:所请求资源的编号 - method:使用的HTTP方法 - url:所请求的资源 URL - time:一个包含请求时间的Date对象 - headers:HTTP头信息数组 网络请求对象包含以下方法。 - abort():终止当前的网络请求,这会导致调用onResourceError回调函数。 - changeUrl(newUrl):改变当前网络请求的URL。 - setHeader(key, value):设置HTTP头信息。 ```javascript var webPage = require('webpage'); var page = webPage.create(); page.onResourceRequested = function(requestData, networkRequest) { console.log('Request (#' + requestData.id + '): ' + JSON.stringify(requestData)); }; ``` ### onResourceReceived onResourceReceived属性用于指定一个回调函数,当网页收到所请求的资源时,就会执行该回调函数。它的参数就是服务器发来的HTTP回应的元数据对象,包括以下字段。 - id:所请求的资源编号 - url:所请求的资源的URL - time:包含HTTP回应时间的Date对象 - headers:HTTP头信息数组 - bodySize:解压缩后的收到的内容大小 - contentType:接到的内容种类 - redirectURL:重定向URL(如果有的话) - stage:对于多数据块的HTTP回应,头一个数据块为start,最后一个数据块为end。 - status:HTTP状态码,成功时为200。 - statusText:HTTP状态信息,比如OK。 如果HTTP回应非常大,分成多个数据块发送,onResourceReceived会在收到每个数据块时触发回调函数。 ```javascript var webPage = require('webpage'); var page = webPage.create(); page.onResourceReceived = function(response) { console.log('Response (#' + response.id + ', stage "' + response.stage + '"): ' + JSON.stringify(response)); }; ``` ## system模块 system模块可以加载操作系统变量,system.args就是参数数组。 ```javascript var page = require('webpage').create(), system = require('system'), t, address; // 如果命令行没有给出网址 if (system.args.length === 1) { console.log('Usage: page.js <some URL>'); phantom.exit(); } t = Date.now(); address = system.args[1]; page.open(address, function (status) { if (status !== 'success') { console.log('FAIL to load the address'); } else { t = Date.now() - t; console.log('Loading time ' + t + ' ms'); } phantom.exit(); }); ``` 使用方法如下: ```bash $ phantomjs page.js http://www.google.com ``` ## 应用 Phantomjs可以实现多种应用。 ### 过滤资源 处理页面的时候,有时不希望加载某些特定资源。这时,可以对URL进行匹配,一旦符合规则,就中断对资源的连接。 ```javascript page.onResourceRequested = function(requestData, request) { if ((/http:\/\/.+?\.css$/gi).test(requestData['url'])) { console.log('Skipping', requestData['url']); request.abort(); } }; ``` 上面代码一旦发现加载的资源是CSS文件,就会使用`request.abort`方法中断连接。 ### 截图 最简单的生成网页截图的方法如下。 ```javascript var page = require('webpage').create(); page.open('http://google.com', function () { page.render('google.png'); phantom.exit(); }); ``` page对象代表一个网页实例;open方法表示打开某个网址,它的第一个参数是目标网址,第二个参数是网页载入成功后,运行的回调函数;render方法则是渲染页面,然后以图片格式输出,该方法的参数就是输出的图片文件名。 除了简单截图以外,还可以设置各种截图参数。 ```javascript var page = require('webpage').create(); page.open('http://google.com', function () { page.zoomFactor = 0.25; console.log(page.renderBase64()); phantom.exit(); }); ``` zoomFactor表示将截图缩小至原图的25%大小;renderBase64方法则是表示将截图(PNG格式)编码成Base64格式的字符串输出。 下面的例子则是使用了更多参数。 ```javascript // page.js var page = require('webpage').create(); page.settings.userAgent = 'WebKit/534.46 Mobile/9A405 Safari/7534.48.3'; page.settings.viewportSize = { width: 400, height: 600 }; page.open('http://slashdot.org', function (status) { if (status !== 'success') { console.log('Unable to load!'); phantom.exit(); } else { var title = page.evaluate(function () { var posts = document.getElementsByClassName("article"); posts[0].style.backgroundColor = "#FFF"; return document.title; }); window.setTimeout(function () { page.clipRect = { top: 0, left: 0, width: 600, height: 700 }; page.render(title + "1.png"); page.clipRect = { left: 0, top: 600, width: 400, height: 600 }; page.render(title + '2.png'); phantom.exit(); }, 1000); } }); ``` 上面代码中的几个属性和方法解释如下: - settings.userAgent:指定HTTP请求的userAgent头信息,上面例子是手机浏览器的userAgent。 - settings.viewportSize:指定浏览器窗口的大小,这里是400x600。 - evaluate():用来在网页上运行Javascript代码。在这里,我们抓取第一条新闻,然后修改背景颜色,并返回该条新闻的标题。 - clipRect:用来指定网页截图的大小,这里的截图左上角从网页的(0. 0)坐标开始,宽600像素,高700像素。如果不指定这个值,就表示对整张网页截图。 - render():根据clipRect的范围,在当前目录下生成以第一条新闻的名字命名的截图。 ### 抓取图片 使用官方网站提供的[rasterize.js](https://github.com/ariya/phantomjs/blob/master/examples/rasterize.js),可以抓取网络上的图片,将起保存在本地。 ```javascript phantomjs rasterize.js http://ariya.github.com/svg/tiger.svg tiger.png ``` 使用[rasterize.js](https://github.com/ariya/phantomjs/blob/master/examples/rasterize.js),还可以将网页保存为pdf文件。 ```javascript phantomjs rasterize.js 'http://en.wikipedia.org/w/index.php?title=Jakarta&printable=yes' jakarta.pdf ``` ### 生成网页 phantomjs可以生成网页,使用content方法指定网页的HTML代码。 ```javascript var page = require('webpage').create(); page.viewportSize = { width: 400, height : 400 }; page.content = '<html><body><canvas id="surface"></canvas></body></html>'; phantom.exit(); ``` 官方网站有一个[例子](https://github.com/ariya/phantomjs/blob/master/examples/colorwheel.js),通过创造svg图片,然后截图保存成png文件。 ![](https://lh3.googleusercontent.com/-xSIzxPtJULw/TVzeP4NPMDI/AAAAAAAAB10/k-c8jB6I5Cg/s288/colorwheel.png) <h2 id="10.8">Bower:客户端库管理工具</h2> ## 概述 随着网页功能变得越来越复杂,同一张网页加载多个JavaScript函数库早已是家常便饭。开发者越来越需要一个工具,对浏览器端的各种库进行管理,比如搜索、自动安装\卸载、检查更新、确保依赖关系等等。Bower就是为了解决这个问题而诞生的针对浏览器端的库管理工具。 Bower基于node.js,所以安装之前,必须先确保已安装node.js。 ```bash $ sudo npm install bower --global ``` 运行上面的命令以后,Bower就已经安装在你的系统中了。运行帮助命令,查看Bower是否安装成功。 ```bash $ bower help ``` 下面的命令可以更新或卸载Bower。 ```bash # 更新 $ sudo npm update -g bower # 卸载 $ sudo npm uninstall --global bower ``` ## 常用操作 ### 项目初始化 在项目根目录下,运行下面的命令,进行初始化。 ```bash $ bower init ``` 通过回答几个问题,就会自动生成bower.json文件。这是项目的配置文件,下面是一个例子。 ```javascript { "name": "app-name", "version": "0.1.0", "main": ["path/to/app.html", "path/to/app.css", "path/to/app.js"], "ignore": [".jshintrc","**/*.txt"], "dependencies": { "sass-bootstrap": "~3.0.0", "modernizr": "~2.6.2", "jquery": "latests" }, "devDependencies": {"qunit": ">1.11.0"} } ``` 有了bower.json文件以后,就可以用bower install命令,一下子安装所有库。 ```bash $ bower install ``` bower.json文件存放在库的根目录下,它的作用是(1)保存项目的库信息,供项目安装时使用,(2)向Bower.com提交你的库,该网站会读取bower.json,列入在线索引。 ```bash $ bower register <my-package-name> <git-endpoint> # 实例:在 bower.com 登记jquery $ bower register jquery git://github.com/jquery/jquery ``` 注意,如果你的库与现有的库重名,就会提交失败。 ### 库的安装 bower install命令用于安装某个库,需要指明库的名字。 ```bash $ bower install backbone ``` Bower会使用库的名字,去在线索引中搜索该库的网址。某些情况下,如果一个库很新(或者你不想使用默认网址),可能需要我们手动指定该库的网址。 ```bash $ bower install git://github.com/documentcloud/backbone.git $ bower install http://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.0.0/backbone-min.js $ bower install ./some/path/relative/to/this/directory/backbone.js ``` 上面的命令说明,指定的网址可以是github地址、http网址、本地文件。 默认情况下,会安装该库的最新版本,但是也可以手动指定版本号。 ```bash $ bower install jquery-ui#1.10.1 ``` 上面的命令指定安装jquery-ui的1.10.1版。 如果某个库依赖另一个库,安装时默认将所依赖的库一起安装。比如,jquery-ui依赖jquery,安装时会连jquery一起安装。 安装后的库默认存放在项目的bower_components子目录,如果要指定其他位置,可在.bowerrc文件的directory属性设置。 ### 库的搜索和查看 bower search命令用于使用关键字,从在线索引中搜索相关库。 ```bash bower search jquery ``` 上面命令会得到下面这样的结果。 ```bash Search results: jquery git://github.com/components/jquery.git jquery-ui git://github.com/components/jqueryui jquery.cookie git://github.com/carhartl/jquery-cookie.git jquery-placeholder git://github.com/mathiasbynens/jquery-placeholder.git jquery-file-upload git://github.com/blueimp/jQuery-File-Upload.git jasmine-jquery git://github.com/velesin/jasmine-jquery jquery.ui git://github.com/jquery/jquery-ui.git ... ``` bower info命令用于查看某个库的详细信息。 ```bash bower info jquery-ui ``` 查看结果会列出该库的依赖关系(dependencies),以及可以得到的版本(Available versions)。 ### 库的更新和卸载 bower update用于更新一个库,将其更新为最新版本。 ```bash $ bower update jquery-ui ``` 如果不给出库名,则更新所有库。 bower uninstall命令用于卸载指定的库。 ```bash $ bower uninstall jquery-ui ``` 注意,默认情况下会连所依赖的库一起卸载。比如,jquery-ui依赖jquery,卸载时会连jquery一起卸载,除非还有别的库依赖jquery。 ### 列出所有库 bower list或bower ls命令,用于列出项目所使用的所有库。 ```bash Bower list Bower ls ``` ## 配置文件.bowerrc 项目根目录下(也可以放在用户的主目录下)的.bowerrc文件是Bower的配置文件,它大概像下面这样。 ```javascript { "directory" : "components", "json" : "bower.json", "endpoint" : "https://Bower.herokuapp.com", "searchpath" : "", "shorthand_resolver" : "" } ``` 其中的属性含义如下。 - directory:存放库文件的子目录名。 - json:描述各个库的json文件名。 - endpoint:在线索引的网址,用来搜索各种库。 - searchpath:一个数组,储存备选的在线索引网址。如果某个库在endpoint中找不到,则继续搜索该属性指定的网址,通常用于放置某些不公开的库。 - shorthand_resolver:定义各个库名称简写形式。 <h2 id="10.9">Grunt:任务自动管理工具</h2> 在Javascript的开发过程中,经常会遇到一些重复性的任务,比如合并文件、压缩代码、检查语法错误、将Sass代码转成CSS代码等等。通常,我们需要使用不同的工具,来完成不同的任务,既重复劳动又非常耗时。Grunt就是为了解决这个问题而发明的工具,可以帮助我们自动管理和运行各种任务。 简单说,Grunt是一个自动任务运行器,会按照预先设定的顺序自动运行一系列的任务。这可以简化工作流程,减轻重复性工作带来的负担。 ## 安装 Grunt基于Node.js,安装之前要先安装Node.js,然后运行下面的命令。 ```bash sudo npm install grunt-cli -g ``` grunt-cli表示安装的是grunt的命令行界面,参数g表示全局安装。 Grunt使用模块结构,除了安装命令行界面以外,还要根据需要安装相应的模块。这些模块应该采用局部安装,因为不同项目可能需要同一个模块的不同版本。 首先,在项目的根目录下,创建一个文本文件package.json,指定当前项目所需的模块。下面就是一个例子。 ```javascript { "name": "my-project-name", "version": "0.1.0", "author": "Your Name", "devDependencies": { "grunt": "0.x.x", "grunt-contrib-jshint": "*", "grunt-contrib-concat": "~0.1.1", "grunt-contrib-uglify": "~0.1.0", "grunt-contrib-watch": "~0.1.4" } } ``` 上面这个package.json文件中,除了注明项目的名称和版本以外,还在devDependencies属性中指定了项目依赖的grunt模块和版本:grunt核心模块为最新的0.x.x版,jshint插件为最新版本,concat插件不低于0.1.1版,uglify插件不低于0.1.0版,watch插件不低于0.1.4版。 然后,在项目的根目录下运行下面的命令,这些插件就会被自动安装在node_modules子目录。 ```bash npm install ``` 上面这种方法是针对已有package.json的情况。如果想要自动生成package.json文件,可以使用npm init命令,按照屏幕提示回答所需模块的名称和版本即可。 ```bash npm init ``` 如果已有的package.json文件不包括Grunt模块,可以在直接安装Grunt模块的时候,加上--save-dev参数,该模块就会自动被加入package.json文件。 ```bash npm install <module> --save-dev ``` 比如,对应上面package.json文件指定的模块,需要运行以下npm命令。 ```bash npm install grunt --save-dev npm install grunt-contrib-jshint --save-dev npm install grunt-contrib-concat --save-dev npm install grunt-contrib-uglify --save-dev npm install grunt-contrib-watch --save-dev ``` ## 命令脚本文件Gruntfile.js 模块安装完以后,下一步在项目的根目录下,新建脚本文件Gruntfile.js。它是grunt的配置文件,就好像package.json是npm的配置文件一样。Gruntfile.js就是一般的Node.js模块的写法。 ```javascript module.exports = function(grunt) { // 配置Grunt各种模块的参数 grunt.initConfig({ jshint: { /* jshint的参数 */ }, concat: { /* concat的参数 */ }, uglify: { /* uglify的参数 */ }, watch: { /* watch的参数 */ } }); // 从node_modules目录加载模块文件 grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-watch'); // 每行registerTask定义一个任务 grunt.registerTask('default', ['jshint', 'concat', 'uglify']); grunt.registerTask('check', ['jshint']); }; ``` 上面的代码用到了grunt代码的三个方法: - **grunt.initConfig**:定义各种模块的参数,每一个成员项对应一个同名模块。 - **grunt.loadNpmTasks**:加载完成任务所需的模块。 - **grunt.registerTask**:定义具体的任务。第一个参数为任务名,第二个参数是一个数组,表示该任务需要依次使用的模块。default任务名表示,如果直接输入grunt命令,后面不跟任何参数,这时所调用的模块(该例为jshint,concat和uglify);该例的check任务则表示使用jshint插件对代码进行语法检查。 上面的代码一共加载了四个模块:jshint(检查语法错误)、concat(合并文件)、uglify(压缩代码)和watch(自动执行)。接下来,有两种使用方法。 (1)命令行执行某个模块,比如 ```bash grunt jshint ``` 上面代码表示运行jshint模块。 (2)命令行执行某个任务。比如 ```bash grunt check ``` 上面代码表示运行check任务。如果运行成功,就会显示“Done, without errors.”。 如果没有给出任务名,只键入grunt,就表示执行默认的default任务。 ## Gruntfile.js实例:grunt-contrib-cssmin模块 下面通过cssmin模块,演示如何编写Gruntfile.js文件。cssmin模块的作用是最小化CSS文件。 首先,在项目的根目录下安装该模块。 ```bash npm install grunt-contrib-cssmin --save-dev ``` 然后,新建文件Gruntfile.js。 ```javascript module.exports = function(grunt) { grunt.initConfig({ cssmin: { minify: { expand: true, cwd: 'css/', src: ['*.css', '!*.min.css'], dest: 'css/', ext: '.min.css' }, combine: { files: { 'css/out.min.css': ['css/part1.min.css', 'css/part2.min.css'] } } } }); grunt.loadNpmTasks('grunt-contrib-cssmin'); grunt.registerTask('default', ['cssmin:minify','cssmin:combine']); }; ``` 下面详细解释上面代码中的三个方法,下面一个个来看。 **(1)grunt.loadNpmTasks** grunt.loadNpmTasks方法载入模块文件。 ```javascript grunt.loadNpmTasks('grunt-contrib-cssmin'); ``` 你需要使用几个模块,这里就要写几条grunt.loadNpmTasks语句,将各个模块一一加载。 如果加载模块很多,这部分会非常冗长。而且,还存在一个问题,就是凡是在这里加载的模块,也同时出现在package.json文件中。如果使用npm命令卸载模块以后,模块会自动从package.json文件中消失,但是必须手动从Gruntfile.js文件中清除,这样很不方便,一旦忘记,还会出现运行错误。这里有一个解决办法,就是安装load-grunt-tasks模块,然后在Gruntfile.js文件中,用下面的语句替代所有的grunt.loadNpmTasks语句。 ```javascript require('load-grunt-tasks')(grunt); ``` 这条语句的作用是自动分析package.json文件,自动加载所找到的grunt模块。 **(2)grunt.initConfig** grunt.initConfig方法用于模块配置,它接受一个对象作为参数。该对象的成员与使用的同名模块一一对应。由于我们要配置的是cssmin模块,所以里面有一个cssmin成员(属性)。 cssmin(属性)指向一个对象,该对象又包含多个成员。除了一些系统设定的成员(比如options),其他自定义的成员称为目标(target)。一个模块可以有多个目标(target),上面代码里面,cssmin模块共有两个目标,一个是“minify”,用于压缩css文件;另一个是“combine”,用于将多个css文件合并一个文件。 每个目标的具体设置,需要参考该模板的文档。就cssmin来讲,minify目标的参数具体含义如下: - **expand**:如果设为true,就表示下面文件名的占位符(即\*号)都要扩展成具体的文件名。 - **cwd**:需要处理的文件(input)所在的目录。 - **src**:表示需要处理的文件。如果采用数组形式,数组的每一项就是一个文件名,可以使用通配符。 - **dest**:表示处理后的文件名或所在目录。 - **ext**:表示处理后的文件后缀名。 除了上面这些参数,还有一些参数也是grunt所有模块通用的。 - **filter**:一个返回布尔值的函数,用于过滤文件名。只有返回值为true的文件,才会被grunt处理。 - **dot**:是否匹配以点号(.)开头的系统文件。 - **makeBase**:如果设置为true,就只匹配文件路径的最后一部分。比如,a?b可以匹配/xyz/123/acb,而不匹配/xyz/acb/123。 关于通配符,含义如下: - \*:匹配任意数量的字符,不包括/。 - ?:匹配单个字符,不包括/。 - \*\*:匹配任意数量的字符,包括/。 - {}:允许使用逗号分隔的列表,表示“or”(或)关系。 - !:用于模式的开头,表示只返回不匹配的情况。 比如,foo/\*.js匹配foo目录下面的文件名以.js结尾的文件,foo/\*\*/\*.js匹配foo目录和它的所有子目录下面的文件名以.js结尾的文件,!\*.css表示匹配所有后缀名不为“.css”的文件。 使用通配符设置src属性的更多例子: ```javascript {src: 'foo/th*.js'}grunt-contrib-uglify {src: 'foo/{a,b}*.js'} {src: ['foo/a*.js', 'foo/b*.js']} ``` 至于combine目标,就只有一个files参数,表示输出文件是css子目录下的out.min.css,输入文件则是css子目录下的part1.min.css和part2.min.css。 files参数的格式可以是一个对象,也可以是一个数组。 ```javascript files: { 'dest/b.js': ['src/bb.js', 'src/bbb.js'], 'dest/b1.js': ['src/bb1.js', 'src/bbb1.js'], }, // or files: [ {src: ['src/aa.js', 'src/aaa.js'], dest: 'dest/a.js'}, {src: ['src/aa1.js', 'src/aaa1.js'], dest: 'dest/a1.js'}, ], ``` 如果minify目标和combine目标的属性设置有重合的部分,可以另行定义一个与minify和combine平行的options属性。 ```javascript grunt.initConfig({ cssmin: { options: { /* ... */ }, minify: { /* ... */ }, combine: { /* ... */ } } }); ``` **(3)grunt.registerTask** grunt.registerTask方法定义如何调用具体的任务。“default”任务表示如果不提供参数,直接输入grunt命令,则先运行“cssmin:minify”,后运行“cssmin:combine”,即先压缩再合并。如果只执行压缩,或者只执行合并,则需要在grunt命令后面指明“模块名:目标名”。 ```bash grunt # 默认情况下,先压缩后合并 grunt cssmin:minify # 只压缩不合并 grunt css:combine # 只合并不压缩 ``` 如果不指明目标,只是指明模块,就表示将所有目标依次运行一遍。 ```bash grunt cssmin ``` ## 常用模块设置 grunt的[模块](http://gruntjs.com/plugins)已经超过了2000个,且还在快速增加。下面是一些常用的模块(按字母排序)。 - **grunt-contrib-clean**:删除文件。 - **grunt-contrib-compass**:使用compass编译sass文件。 - **grunt-contrib-concat**:合并文件。 - **grunt-contrib-copy**:复制文件。 - **grunt-contrib-cssmin**:压缩以及合并CSS文件。 - **grunt-contrib-imagemin**:图像压缩模块。 - **grunt-contrib-jshint**:检查JavaScript语法。 - **grunt-contrib-uglify**:压缩以及合并JavaScript文件。 - **grunt-contrib-watch**:监视文件变动,做出相应动作。 模块的前缀如果是grunt-contrib,就表示该模块由grunt开发团队维护;如果前缀是grunt(比如grunt-pakmanager),就表示由第三方开发者维护。 以下选几个模块,看看它们配置参数的写法,也就是说如何在grunt.initConfig方法中配置各个模块。 ### grunt-contrib-jshint jshint用来检查语法错误,比如分号的使用是否正确、有没有忘记写括号等等。它在grunt.initConfig方法里面的配置代码如下。 ```javascript jshint: { options: { eqeqeq: true, trailing: true }, files: ['Gruntfile.js', 'lib/**/*.js'] }, ``` 上面代码先指定jshint的[检查项目](http://www.jshint.com/docs/options/),eqeqeq表示要用严格相等运算符取代相等运算符,trailing表示行尾不得有多余的空格。然后,指定files属性,表示检查目标是Gruntfile.js文件,以及lib目录的所有子目录下面的JavaScript文件。 ### grunt-contrib-concat concat用来合并同类文件,它不仅可以合并JavaScript文件,还可以合并CSS文件。 ```javascript concat: { js: { src: ['lib/module1.js', 'lib/module2.js', 'lib/plugin.js'], dest: 'dist/script.js' } css: { src: ['style/normalize.css', 'style/base.css', 'style/theme.css'], dest: 'dist/screen.css' } }, ``` js目标用于合并JavaScript文件,css目标用语合并CSS文件。两者的src属性指定需要合并的文件(input),dest属性指定输出的目标文件(output)。 ### grunt-contrib-uglify uglify模块用来压缩代码,减小文件体积。 ```javascript uglify: { options: { banner: bannerContent, sourceMapRoot: '../', sourceMap: 'distrib/'+name+'.min.js.map', sourceMapUrl: name+'.min.js.map' }, target : { expand: true, cwd: 'js/origin', src : '*.js', dest : 'js/' } }, ``` 上面代码中的options属性指定压缩后文件的文件头,以及sourceMap设置;target目标指定输入和输出文件。 ### grunt-contrib-copy [copy模块](https://github.com/gruntjs/grunt-contrib-copy)用于复制文件与目录。 ```javascript copy: { main: { src: 'src/*', dest: 'dest/', }, }, ``` 上面代码将src子目录(只包含它下面的第一层文件和子目录),拷贝到dest子目录下面(即dest/src目录)。如果要更准确控制拷贝行为,比如只拷贝文件、不拷贝目录、不保持目录结构,可以写成下面这样: ```javascript copy: { main: { expand: true, cwd: 'src/', src: '**', dest: 'dest/', flatten: true, filter: 'isFile', }, }, ``` ### grunt-contrib-watch [watch模块](https://github.com/gruntjs/grunt-contrib-watch)用来在后台运行,监听指定事件,然后自动运行指定的任务。 ```javascript watch: { scripts: { files: '**/*.js', tasks: 'jshint', options: { livereload: true, }, }, css: { files: '**/*.sass', tasks: ['sass'], options: { livereload: true, }, }, }, ``` 设置好上面的代码,打开另一个进程,运行grunt watch。此后,任何的js代码变动,文件保存后就会自动运行jshint任务;任何sass文件变动,文件保存后就会自动运行sass任务。 需要注意的是,这两个任务的options参数之中,都设置了livereload,表示任务运行结束后,自动在浏览器中重载(reload)。这需要在浏览器中安装[livereload插件](http://livereload.com/)。安装后,livereload的默认端口为localhost:35729,但是也可以用livereload: 1337的形式重设端口(localhost:1337)。 ### 其他模块 下面是另外一些有用的模块。 **(1)grunt-contrib-clean** 该模块用于删除文件或目录。 ```javascript clean: { build: { src: ["path/to/dir/one", "path/to/dir/two"] } } ``` **(2)grunt-autoprefixer** 该模块用于为CSS语句加上浏览器前缀。 ```javascript autoprefixer: { build: { expand: true, cwd: 'build', src: [ '**/*.css' ], dest: 'build' } }, ``` **(3)grunt-contrib-connect** 该模块用于在本机运行一个Web Server。 ```javascript connect: { server: { options: { port: 4000, base: 'build', hostname: '*' } } } ``` connect模块会随着grunt运行结束而结束,为了使它一直处于运行状态,可以把它放在watch模块之前运行。因为watch模块需要手动中止,所以connect模块也就会一直运行。 **(4)grunt-htmlhint** 该模块用于检查HTML语法。 ```javascript htmlhint: { build: { options: { 'tag-pair': true, 'tagname-lowercase': true, 'attr-lowercase': true, 'attr-value-double-quotes': true, 'spec-char-escape': true, 'id-unique': true, 'head-script-disabled': true, }, src: ['index.html'] } } ``` 上面代码用于检查index.html文件:HTML标记是否配对、标记名和属性名是否小写、属性值是否包括在双引号之中、特殊字符是否转义、HTML元素的id属性是否为唯一值、head部分是否没有script标记。 **(5)grunt-contrib-sass模块** 该模块用于将SASS文件转为CSS文件。 ```javascript sass: { build: { options: { style: 'compressed' }, files: { 'build/css/master.css': 'assets/sass/master.scss' } } } ``` 上面代码指定输出文件为build/css/master.css,输入文件为assets/sass/master.scss。 **(6)grunt-markdown** 该模块用于将markdown文档转为HTML文档。 ```javascript markdown: { all: { files: [ { expand: true, src: '*.md', dest: 'docs/html/', ext: '.html' } ], options: { template: 'templates/index.html', } } }, ``` 上面代码指定将md后缀名的文件,转为docs/html/目录下的html文件。template属性指定转换时采用的模板,模板样式如下。 ```html <!DOCTYPE html> <html> <head> <title>Document</title> </head> <body> <div id="main" class="container"> <%=content%> </div> </body> </html> ``` <h2 id="10.10">RequireJS和AMD规范</h2> ## 概述 RequireJS是一个工具库,主要用于客户端的模块管理。它可以让客户端的代码分成一个个模块,实现异步或动态加载,从而提高代码的性能和可维护性。它的模块管理遵守[AMD规范](https://github.com/amdjs/amdjs-api/wiki/AMD)(Asynchronous Module Definition)。 RequireJS的基本思想是,通过define方法,将代码定义为模块;通过require方法,实现代码的模块加载。 首先,将require.js嵌入网页,然后就能在网页中进行模块化编程了。 ```javascript <script data-main="scripts/main" src="scripts/require.js"></script> ``` 上面代码的data-main属性不可省略,用于指定主代码所在的脚本文件,在上例中为scripts子目录下的main.js文件。用户自定义的代码就放在这个main.js文件中。 ### define方法:定义模块 define方法用于定义模块,RequireJS要求每个模块放在一个单独的文件里。 按照是否依赖其他模块,可以分成两种情况讨论。第一种情况是定义独立模块,即所定义的模块不依赖其他模块;第二种情况是定义非独立模块,即所定义的模块依赖于其他模块。 **(1)独立模块** 如果被定义的模块是一个独立模块,不需要依赖任何其他模块,可以直接用define方法生成。 ```javascript define({ method1: function() {}, method2: function() {}, }); ``` 上面代码生成了一个拥有method1、method2两个方法的模块。 另一种等价的写法是,把对象写成一个函数,该函数的返回值就是输出的模块。 ```javascript define(function () { return { method1: function() {}, method2: function() {}, }; }); ``` 后一种写法的自由度更高一点,可以在函数体内写一些模块初始化代码。 值得指出的是,define定义的模块可以返回任何值,不限于对象。 **(2)非独立模块** 如果被定义的模块需要依赖其他模块,则define方法必须采用下面的格式。 ```javascript define(['module1', 'module2'], function(m1, m2) { ... }); ``` define方法的第一个参数是一个数组,它的成员是当前模块所依赖的模块。比如,['module1', 'module2']表示我们定义的这个新模块依赖于module1模块和module2模块,只有先加载这两个模块,新模块才能正常运行。一般情况下,module1模块和module2模块指的是,当前目录下的module1.js文件和module2.js文件,等同于写成['./module1', './module2']。 define方法的第二个参数是一个函数,当前面数组的所有成员加载成功后,它将被调用。它的参数与数组的成员一一对应,比如function(m1, m2)就表示,这个函数的第一个参数m1对应module1模块,第二个参数m2对应module2模块。这个函数必须返回一个对象,供其他模块调用。 ```javascript define(['module1', 'module2'], function(m1, m2) { return { method: function() { m1.methodA(); m2.methodB(); } }; }); ``` 上面代码表示新模块返回一个对象,该对象的method方法就是外部调用的接口,menthod方法内部调用了m1模块的methodA方法和m2模块的methodB方法。 需要注意的是,回调函数必须返回一个对象,这个对象就是你定义的模块。 如果依赖的模块很多,参数与模块一一对应的写法非常麻烦。 ```javascript define( [ 'dep1', 'dep2', 'dep3', 'dep4', 'dep5', 'dep6', 'dep7', 'dep8'], function(dep1, dep2, dep3, dep4, dep5, dep6, dep7, dep8){ ... } ); ``` 为了避免像上面代码那样繁琐的写法,RequireJS提供一种更简单的写法。 ```javascript define( function (require) { var dep1 = require('dep1'), dep2 = require('dep2'), dep3 = require('dep3'), dep4 = require('dep4'), dep5 = require('dep5'), dep6 = require('dep6'), dep7 = require('dep7'), dep8 = require('dep8'); ... } }); ``` 下面是一个define实际运用的例子。 ```javascript define(['math', 'graph'], function ( math, graph ) { return { plot: function(x, y){ return graph.drawPie(math.randomGrid(x,y)); } } }; ); ``` 上面代码定义的模块依赖math和graph两个库,然后返回一个具有plot接口的对象。 另一个实际的例子是,通过判断浏览器是否为IE,而选择加载zepto或jQuery。 ```javascript define(('__proto__' in {} ? ['zepto'] : ['jquery']), function($) { return $; }); ``` 上面代码定义了一个中间模块,该模块先判断浏览器是否支持__proto__属性(除了IE,其他浏览器都支持),如果返回true,就加载zepto库,否则加载jQuery库。 ### require方法:调用模块 require方法用于调用模块。它的参数与define方法类似。 ```javascript require(['foo', 'bar'], function ( foo, bar ) { foo.doSomething(); }); ``` 上面方法表示加载foo和bar两个模块,当这两个模块都加载成功后,执行一个回调函数。该回调函数就用来完成具体的任务。 require方法的第一个参数,是一个表示依赖关系的数组。这个数组可以写得很灵活,请看下面的例子。 ```javascript require( [ window.JSON ? undefined : 'util/json2' ], function ( JSON ) { JSON = JSON || window.JSON; console.log( JSON.parse( '{ "JSON" : "HERE" }' ) ); }); ``` 上面代码加载JSON模块时,首先判断浏览器是否原生支持JSON对象。如果是的,则将undefined传入回调函数,否则加载util目录下的json2模块。 require方法也可以用在define方法内部。 ```javascript define(function (require) { var otherModule = require('otherModule'); }); ``` 下面的例子显示了如何动态加载模块。 ```javascript define(function ( require ) { var isReady = false, foobar; require(['foo', 'bar'], function (foo, bar) { isReady = true; foobar = foo() + bar(); }); return { isReady: isReady, foobar: foobar }; }); ``` 上面代码所定义的模块,内部加载了foo和bar两个模块,在没有加载完成前,isReady属性值为false,加载完成后就变成了true。因此,可以根据isReady属性的值,决定下一步的动作。 下面的例子是模块的输出结果是一个promise对象。 ```javascript define(['lib/Deferred'], function( Deferred ){ var defer = new Deferred(); require(['lib/templates/?index.html','lib/data/?stats'], function( template, data ){ defer.resolve({ template: template, data:data }); } ); return defer.promise(); }); ``` 上面代码的define方法返回一个promise对象,可以在该对象的then方法,指定下一步的动作。 如果服务器端采用JSONP模式,则可以直接在require中调用,方法是指定JSONP的callback参数为define。 ```javascript require( [ "http://someapi.com/foo?callback=define" ], function (data) { console.log(data); }); ``` require方法允许添加第三个参数,即错误处理的回调函数。 ```javascript require( [ "backbone" ], function ( Backbone ) { return Backbone.View.extend({ /* ... */ }); }, function (err) { // ... } ); ``` require方法的第三个参数,即处理错误的回调函数,接受一个error对象作为参数。 require对象还允许指定一个全局性的Error事件的监听函数。所有没有被上面的方法捕获的错误,都会被触发这个监听函数。 ```javascript requirejs.onError = function (err) { // ... }; ``` ### AMD模式小结 define和require这两个定义模块、调用模块的方法,合称为AMD模式。它的模块定义的方法非常清晰,不会污染全局环境,能够清楚地显示依赖关系。 AMD模式可以用于浏览器环境,并且允许非同步加载模块,也可以根据需要动态加载模块。 ## 配置require.js:config方法 require方法本身也是一个对象,它带有一个config方法,用来配置require.js运行参数。config方法接受一个对象作为参数。 ```javascript require.config({ paths: { jquery: [ '//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js', 'lib/jquery' ] } }); ``` config方法的参数对象有以下主要成员: **(1)paths** paths参数指定各个模块的位置。这个位置可以是同一个服务器上的相对位置,也可以是外部网址。可以为每个模块定义多个位置,如果第一个位置加载失败,则加载第二个位置,上面的示例就表示如果CDN加载失败,则加载服务器上的备用脚本。需要注意的是,指定本地文件路径时,可以省略文件最后的js后缀名。 ```javascript require(["jquery"], function($) { // ... }); ``` 上面代码加载jquery模块,因为jquery的路径已经在paths参数中定义了,所以就会到事先设定的位置下载。 **(2)baseUrl** baseUrl参数指定本地模块位置的基准目录,即本地模块的路径是相对于哪个目录的。该属性通常由require.js加载时的data-main属性指定。 **(3)shim** 有些库不是AMD兼容的,这时就需要指定shim属性的值。shim可以理解成“垫片”,用来帮助require.js加载非AMD规范的库。 ```javascript require.config({ paths: { "backbone": "vendor/backbone", "underscore": "vendor/underscore" }, shim: { "backbone": { deps: [ "underscore" ], exports: "Backbone" }, "underscore": { exports: "_" } } }); ``` 上面代码中的backbone和underscore就是非AMD规范的库。shim指定它们的依赖关系(backbone依赖于underscore),以及输出符号(backbone为“Backbone”,underscore为“_”)。 ## 插件 RequireJS允许使用插件,加载各种格式的数据。完整的插件清单可以查看[官方网站](https://github.com/jrburke/requirejs/wiki/Plugins)。 下面是插入文本数据所使用的text插件的例子。 ```javascript define([ 'backbone', 'text!templates.html' ], function( Backbone, template ){ // ... }); ``` 上面代码加载的第一个模块是backbone,第二个模块则是一个文本,用'text!'表示。该文本作为字符串,存放在回调函数的template变量中。 ## 优化器r.js RequireJS提供一个基于node.js的命令行工具r.js,用来压缩多个js文件。它的主要作用是将多个模块文件压缩合并成一个脚本文件,以减少网页的HTTP请求数。 第一步是安装r.js(假设已经安装了node.js)。 ```bash npm install -g requirejs ``` 然后,使用的时候,直接在命令行键入以下格式的命令。 ```bash node r.js -o <arguments> ``` &lt;argument&gt;表示命令运行时,所需要的一系列参数,比如像下面这样: ```bash node r.js -o baseUrl=. name=main out=main-built.js ``` 除了直接在命令行提供参数设置,也可以将参数写入一个文件,假定文件名为build.js。 ```javascript ({ baseUrl: ".", name: "main", out: "main-built.js" }) ``` 然后,在命令行下用r.js运行这个参数文件,就OK了,不需要其他步骤了。 ```bash node r.js -o build.js ``` 下面是一个参数文件的范例,假定位置就在根目录下,文件名为build.js。 ```javascript ({ appDir: './', baseUrl: './js', dir: './dist', modules: [ { name: 'main' } ], fileExclusionRegExp: /^(r|build)\.js$/, optimizeCss: 'standard', removeCombined: true, paths: { jquery: 'lib/jquery', underscore: 'lib/underscore', backbone: 'lib/backbone/backbone', backboneLocalstorage: 'lib/backbone/backbone.localStorage', text: 'lib/require/text' }, shim: { underscore: { exports: '_' }, backbone: { deps: [ 'underscore', 'jquery' ], exports: 'Backbone' }, backboneLocalstorage: { deps: ['backbone'], exports: 'Store' } } }) ``` 上面代码将多个模块压缩合并成一个main.js。 参数文件的主要成员解释如下: - **appDir**:项目目录,相对于参数文件的位置。 - **baseUrl**:js文件的位置。 - **dir**:输出目录。 - **modules**:一个包含对象的数组,每个对象就是一个要被优化的模块。 - **fileExclusionRegExp**:凡是匹配这个正则表达式的文件名,都不会被拷贝到输出目录。 - **optimizeCss**: 自动压缩CSS文件,可取的值包括“none”, “standard”, “standard.keepLines”, “standard.keepComments”, “standard.keepComments.keepLines”。 - **removeCombined**:如果为true,合并后的原文件将不保留在输出目录中。 - **paths**:各个模块的相对路径,可以省略js后缀名。 - **shim**:配置依赖性关系。如果某一个模块不是AMD模式定义的,就可以用shim属性指定模块的依赖性关系和输出值。 - **generateSourceMaps**:是否要生成source map文件。 更详细的解释可以参考[官方文档](https://github.com/jrburke/r.js/blob/master/build/example.build.js)。 运行优化命令后,可以前往dist目录查看优化后的文件。 下面是另一个build.js的例子。 ```javascript ({ mainConfigFile : "js/main.js", baseUrl: "js", removeCombined: true, findNestedDependencies: true, dir: "dist", modules: [ { name: "main", exclude: [ "infrastructure" ] }, { name: "infrastructure" } ] }) ``` 上面代码将模块文件压缩合并成两个文件,第一个是main.js(指定排除infrastructure.js),第二个则是infrastructure.js。 <h2 id="10.11">Lint 工具</h2> ## 概述 Lint工具用于检查代码的语法是否正确、风格是否符合要求。 JavaScript语言的最早的Lint工具,是Douglas Crockford开发的JSLint。由于该工具所有的语法规则,都是预设的,用户无法改变。所以,很快就有人抱怨,JSLint不是让你写成正确的JavaScript,而是让你像Douglas Crockford一样写JavaScript。 JSHint可以看作是JSLint的后继者,最大特定就是允许用户自定义自己的语法规则,写在项目根目录下面的`.jshintrc`文件。 JSLint和JSHint同时检查你的语法和风格。另一个工具JSCS则是只检查语法风格。 最新的工具ESLint不仅允许你自定义语法规则,还允许用户创造插件,改变默认的JavaScript语法,比如支持ES6和JSX的语法。 ## ESLint ### 基本用法 首先,安装ESLint。 ```bash $ npm i -g eslint ``` 其次,在项目根目录下面新建一个`.eslintrc`文件,里面定义了你的语法规则。 ```javascript { "rules": { "indent": 2, "no-unused-vars": 2, "no-alert": 1 }, "env": { "browser": true } } ``` 上面的`.eslintrc`文件是JSON格式,里面首先定义,这些规则只适用于浏览器环境。如果要定义,同时适用于浏览器环境和Node环境,可以写成下面这样。 ```javascript { "env": { "browser": true, "node": true } } ``` 然后,上面的`.eslintrc`文件定义了三条语法规则。每个语法规则后面,表示这个规则的级别。 - 0:关闭该条规则。 - 1:违反这条规则,会抛出一个警告。 - 2:违反这条规则,会抛出一个错误。 接下来,新建一个`index.js`文件。 ```javascript var unusued = 'I have no purpose!'; function greet() { var message = 'Hello, World!'; alert(message); } greet(); ``` 然后,运行ESLint检查该文件,结果如下。 ```bash $ eslint index.js index.js 1:5 error unusued is defined but never used no-unused-vars 5:5 warning Unexpected alert no-alert ✖ 2 problems (1 error, 1 warning) ``` 上面代码检查出两个问题,一个是定义了变量却没有使用,二是存在alert。 ### 预置规则 自己设置所有语法规则,是非常麻烦的。所以,ESLint提供了预设的语法样式,比较常用的Airbnb的语法规则。由于这个规则集涉及ES6,所以还需要安装Babel插件。 ```bash $ npm i -g babel-eslint eslint-config-airbnb ``` 安装完成后,在`.eslintrc`文件中注明,使用Airbnb语法规则。 ```bash { "extends": "eslint-config-airbnb" } ``` 你也可以用自己的规则,覆盖预设的语法规则。 ```javascript { "extends": "eslint-config-airbnb", "rules": { "no-var": 0, "no-alert": 0 } } ``` ### 语法规则 (1)indent indent规则设定行首的缩进,默认是四个空格。下面的几种写法,可以改变这个设置。 ```javascript // 缩进为4个空格(默认值) "indent": 2 // 缩进为2个空格 "indent": [2, 2] // 缩进为1个tab键 "indent": [2, "tab"] // 缩进为2个空格, // 同时,switch...case结构的case也必须缩进,默认是不打开的 "indent": [2, 2, {"SwitchCase": 1}] ``` (2)no-unused-vars 不允许声明了变量,却不使用。 ```javascript "no-unused-vars": [2, {"vars": "local", "args": "after-used"}] ``` 上面代码中,vars字段表示只检查局部变量,允许全局变量声明了却不使用;args字段表示函数的参数,只要求使用最后一个参数,前面的参数可以不使用。 (3)no-alert 不得使用alert、confirm和prompt。