├── .gitignore ├── README.md ├── demos ├── README.md ├── angular-demo │ ├── angular.1.4.8.min.js │ └── index.html ├── backbone-demo │ ├── index.html │ └── js │ │ ├── backbone.js │ │ ├── jquery.js │ │ ├── main.js │ │ └── underscore.js ├── eslint-demo │ ├── index.js │ └── package.json ├── express-demo │ ├── app1.js │ ├── app2.js │ ├── app3.js │ ├── app4.js │ └── package.json ├── jsx-demo │ ├── babel.min.js │ ├── index.html │ ├── react-dom.js │ └── react.js ├── mobx-demo │ ├── .babelrc │ ├── .eslintignore │ ├── .eslintrc │ ├── .gitignore │ ├── .jshintignore │ ├── .jshintrc │ ├── app │ │ ├── index.html │ │ ├── main.css │ │ ├── main.jsx │ │ └── store.js │ ├── browser-demo │ │ ├── babel.min.js │ │ ├── index.html │ │ ├── mobx-react.js │ │ ├── mobx.umd.js │ │ ├── react-15.1.0.js │ │ └── react-dom-15.1.0.js │ ├── favicon.ico │ ├── package.json │ ├── webpack.config.js │ └── webpack.production.config.js ├── mocha-demo │ ├── add.js │ └── package.json ├── nightmare-demo │ ├── babel.min.js │ ├── index.html │ ├── package.json │ ├── react-dom.js │ ├── react.js │ ├── server.js │ ├── taobao.test.js │ └── test.js ├── react-component-demo │ ├── babel.min.js │ ├── index1.html │ ├── index2.html │ ├── index3.html │ ├── index4.html │ ├── react-dom.js │ └── react.js ├── react-lifecycle-demo │ ├── babel.min.js │ ├── index.html │ ├── jquery.js │ ├── react-dom.js │ └── react.js ├── recharts-demo │ ├── Recharts.min.js │ ├── babel.min.js │ ├── index.html │ ├── react-dom-server.min.js │ ├── react-dom.js │ └── react-with-addons.min.js ├── redux-demo │ ├── .babelrc │ ├── .eslintignore │ ├── .eslintrc │ ├── .gitignore │ ├── .jshintignore │ ├── .jshintrc │ ├── app │ │ ├── App.js │ │ ├── index.html │ │ ├── main.css │ │ ├── main.jsx │ │ ├── myComponent.js │ │ └── reducer.js │ ├── favicon.ico │ ├── package.json │ ├── webpack.config.js │ └── webpack.production.config.js ├── rest-api-demo │ ├── db.json │ └── package.json ├── simple-app-demo │ ├── app.js │ ├── bundle.js │ ├── index.html │ └── package.json └── vue-demo │ ├── app1.js │ ├── app2.js │ ├── app3.js │ ├── index1.html │ ├── index2.html │ ├── index3.html │ └── vue.min.js ├── docs ├── engineering.md ├── history.md ├── images │ ├── angular-demo.png │ ├── angular.png │ ├── angular.svg │ ├── architecture-new.png │ ├── architecture-old.png │ ├── architecture-redux.png │ ├── backbone-demo.png │ ├── backbone-model-view.png │ ├── backbone-model-view.svg │ ├── backbone-routing.png │ ├── backbone-routing.svg │ ├── backbone.png │ ├── eslint.png │ ├── express.png │ ├── flow.png │ ├── frontend.png │ ├── functional-test.jpg │ ├── jsx.png │ ├── laravel-mvc.png │ ├── mocha.png │ ├── mvvm.png │ ├── node-logo.png │ ├── react-bootstrap.png │ ├── react-component-state.png │ ├── react-logo.jpg │ ├── react-logo.png │ ├── redux-architecture.png │ ├── redux.png │ ├── redux.svg │ ├── travis-ci.png │ ├── vue-demo.png │ ├── vue-logo.png │ └── web20.gif ├── node.md ├── preparation.md └── react.md └── ppt ├── README.md ├── demo.pdf ├── engineering.pdf ├── history.pdf ├── intro.pdf ├── node.pdf └── react.pdf /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | instructor.md 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 全栈工程师培训材料,帮助学习者掌握 JavaScript 全栈开发的基本知识,承担简单 Web 应用的前后端开发。 2 | 3 | 一共四讲,适合两天的训练营。 4 | 5 | ## 学员要求 6 | 7 | 本培训不是零基础的,要求学员具备互联网开发的基本知识。 8 | 9 | - HTML 10 | - JavaScript 11 | - CSS 12 | - 命令行 13 | 14 | 以上技术,只要懂得基本语法和如何使用即可。 15 | 16 | ## 环境准备 17 | 18 | 参加培训之前,请按照[文档](docs/preparation.md)安装软件,做好环境准备。 19 | 20 | ## 课堂练习 21 | 22 | 培训时,需要完成[课堂练习](demos)。 23 | 24 | ## 第一讲:[前端开发的历史和趋势](./docs/history.md) 25 | 26 | 1. 前端开发的历史演变 27 | 2. 前端 MVC 框架的兴起 28 | 3. 前后端分离 29 | 4. 全栈工程师 30 | 5. 前端开发的未来 31 | 32 | ## 第二讲:[React 技术栈](./docs/react.md) 33 | 34 | 1. React 的基本用法 35 | 2. React 应用的架构 36 | 37 | ## 第三讲:[Node 应用开发](./docs/node.md) 38 | 39 | 1. Node 的基本用法 40 | 2. Restful API 41 | 3. Express 框架搭建 Web 应用 42 | 43 | ## 第四讲:[前端工程简介](./docs/engineering.md) 44 | 45 | 1. 持续集成 46 | 1. 静态代码检查 47 | 1. 单元测试 48 | 1. 功能测试 49 | 1. 持续集成服务 Travis CI 50 | 51 | ## License 52 | 53 | GPL v3 54 | -------------------------------------------------------------------------------- /demos/README.md: -------------------------------------------------------------------------------- 1 | # 课堂练习的操作指导 2 | 3 | ## 目录 4 | 5 | - 前端开发的历史和趋势 6 | - [Backbone](#backbone) 7 | - [Angular](#angular) 8 | - [Vue](#vue) 9 | - React 技术栈 10 | - [JSX](#jsx) 11 | - [React 组件语法](#react-组件语法) 12 | - [React 组件的参数](#react-组件的参数) 13 | - [React 组件的状态](#react-组件的状态) 14 | - [React 组件实战](#react-组件实战) 15 | - [React 组件的生命周期](#react-组件的生命周期) 16 | - [ReCharts](#recharts) 17 | - [MobX](#mobx) 18 | - [Redux](#redux) 19 | - Node 开发 20 | - [Simple App](#simple-app) 21 | - [REST API](#rest-api) 22 | - [Express](#express) 23 | - 前端工程简介 24 | - [ESLint](#eslint) 25 | - [Mocha](#mocha) 26 | - [Nightmare](#nightmare) 27 | - [Travis CI](#travis-ci) 28 | 29 | ## Backbone 30 | 31 | ### 实验目的 32 | 33 | 1. 理解前端框架的路由组件(`router`)的作用 34 | 35 | ### 操作步骤 36 | 37 | 1. 浏览器打开[`demos/backbone-demo/index.html`](./backbone-demo/index.html) 38 | 1. 点击页面上的链接,注意浏览器 URL 的变化 39 | 1. 仔细查看[`js/main.js`](./backbone-demo/js/main.js)的源码,看懂 Router 组件的使用方式 40 | 41 | ## Angular 42 | 43 | ### 实验目的 44 | 45 | 1. 理解 Angular 的双向绑定机制 46 | 47 | ### 操作步骤 48 | 49 | 1. 浏览器打开[`demos/angular-demo/index.html`](./angular-demo/index.html) 50 | 1. 在输入框填入内容,注意页面变化 51 | 1. 查看[`index.html`](./angular-demo/index.html)的源码,理解 Angular 对 HTML 标签的增强 52 | 53 | ## Vue 54 | 55 | ### 实验目的 56 | 57 | 1. 理解 Vue 的模板与数据的双向绑定 58 | 59 | ### 操作步骤 60 | 61 | 1. 浏览器打开[`demos/vue-demo/index1.html`](./vue-demo/index1.html) 62 | 1. 在输入框填入内容,注意页面变化 63 | 1. 查看[`app1.js`](./vue-demo/app1.js),理解 Vue 组件的基本写法 64 | 65 | ### 注意事项 66 | 67 | 1. [`index2.html`](./vue-demo/index2.html)是一个稍微复杂的例子,模板如何绑定数据对象的一个字段。 68 | 2. [`index3.html`](./vue-demo/index3.html)是事件绑定模板的例子。 69 | 70 | ## JSX 71 | 72 | ### 实验目的 73 | 74 | 1. 掌握 JSX 基本语法 75 | 76 | ### 操作步骤 77 | 78 | 1. 浏览器打开`demos/jsx-demo/index.html`,仔细查看源码。 79 | 80 | ### 注意事项 81 | 82 | 1. `ReactDOM.render`方法接受两个参数:一个虚拟 DOM 节点和一个真实 DOM 节点,作用是将虚拟 DOM 挂载到真实 DOM。 83 | 84 | ### 练习 85 | 86 | 1. 修改源码,将显示文字变为 “Hello React!”。 87 | 88 | ## React 组件语法 89 | 90 | ### 实验目的 91 | 92 | 1. 掌握 React 组件的基本写法 93 | 94 | ### 操作步骤 95 | 96 | 1. 浏览器打开`demos/react-component-demo/index1.html`,仔细查看源码。 97 | 98 | ### 注意事项 99 | 100 | 1. `class MyTitle extends React.Component`是 ES6 语法,表示自定义一个`MyTitle`类,该类继承了基类`React.Component`的所有属性和方法。 101 | 1. React 规定,自定义组件的第一个字母必须大写,比如`MyTitle`不能写成`myTitle`,以便与内置的原生类相区分。 102 | 1. 每个组件都必须有`render`方法,定义输出的样式。 103 | 1. ``表示生成一个组件类的实例,每个实例一定要有闭合标签,写成``也可。 104 | 105 | ## React 组件的参数 106 | 107 | ### 实验目的 108 | 109 | 1. 学会向 React 组件传参数 110 | 111 | ### 操作步骤 112 | 113 | 1. 浏览器打开`demos/react-component-demo/index2.html`,仔细查看源码。 114 | 115 | ### 注意事项 116 | 117 | 1. 组件内部通过`this.props`对象获取参数。 118 | 119 | ### 练习 120 | 121 | 1. 将组件的颜色,从红色(`red`)换成黄色(`yellow`)。 122 | 123 | ## React 组件的状态 124 | 125 | ### 实验目的 126 | 127 | 1. 学会通过状态变动,引发组件的重新渲染。 128 | 129 | ### 操作步骤 130 | 131 | 1. 浏览器打开`demos/react-component-demo/index3.html`,仔细查看源码。 132 | 133 | ### 注意事项 134 | 135 | ```javascript 136 | class MyTitle extends React.Component { 137 | constructor(...args) { 138 | super(...args); 139 | this.state = { 140 | name: '访问者' 141 | }; 142 | } 143 | // ... 144 | ``` 145 | 146 | `constructor`是组件的构造函数,会在创建实例时自动调用。`...args`表示组件参数,`super(...args)`是 ES6 规定的写法。`this.state`对象用来存放内部状态,这里是定义初始状态。 147 | 148 | ```html 149 |
150 | 154 |

你好,{this.state.name}

155 |
; 156 | ``` 157 | 158 | `this.state.name`表示读取`this.state`的`name`属性。每当输入框有变动,就会自动调用`onChange`指定的监听函数,这里是`this.handleChange`,`.bind(this)`表示该方法内部的`this`,绑定当前组件。 159 | 160 | ```javascript 161 | handleChange(e) { 162 | let name = e.target.value; 163 | this.setState({ 164 | name: name 165 | }); 166 | } 167 | ``` 168 | 169 | `this.setState`方法用来重置`this.state`,每次调用这个方法,就会引发组件的重新渲染。 170 | 171 | ## React 组件实战 172 | 173 | ### 实验目的 174 | 175 | 1. 学会自己写简单的 React 组件。 176 | 177 | ### 操作步骤 178 | 179 | 1. 浏览器打开`demos/react-component-demo/index4.html`。 180 | 1. 点击`Hello World`,看看会发生什么。 181 | 182 | ### 练习 183 | 184 | 1. 修改源码,使得点击`Hello World`后,会显示当前的日期,比如`Hello 2016年1月1日`。 185 | 186 | 2. 请在上一步练习的基础上,进一步修改。现在`Hello World`点击一次,会改变内容,再点击就不会有反应了。请将其改成,再点击一次变回原样。 187 | 188 | ### 提示 189 | 190 | 练习一、下面的代码可以得到当前日期。 191 | 192 | ```javascript 193 | var d = new Date(); 194 | d.getFullYear() // 当前年份 195 | d.getMonth() + 1 // 当前月份 196 | d.getDate() // 当前是每个月的几号 197 | ``` 198 | 199 | 练习二、可以在`this.state`里面设置一个开关变量`isClicked`。 200 | 201 | ```javascript 202 | this.state = { 203 | text: 'World', 204 | isClicked: false 205 | }; 206 | ``` 207 | 208 | 然后,在`this.handleClick`方法里面,做一个`toggle`效果。 209 | 210 | ```javascript 211 | let isClicked = !this.state.isClicked; 212 | this.setState({ 213 | isClicked: isClicked, 214 | text: isClicked ? 'Clicked' : 'World' 215 | }); 216 | ``` 217 | 218 | ## React 组件的生命周期 219 | 220 | ### 实验目的 221 | 222 | 1. 掌握钩子方法的基本用法 223 | 1. 掌握组件如何通过 Ajax 请求获取数据,并对数据进行处理 224 | 225 | ### 操作步骤 226 | 227 | 1. 打开`demos/react-lifecycle-demo/index.html`,仔细查看源码。 228 | 229 | ### 注意事项 230 | 231 | ```javascript 232 | componentDidMount() { 233 | const url = '...'; 234 | $.getJSON(url) 235 | .done() 236 | .fail(); 237 | } 238 | ``` 239 | 240 | - `componentDidMount`方法在组件加载后执行,只执行一次。本例在这个方法里向服务器请求数据,操作结束前,组件都显示`Loading`。 241 | - `$.getJSON`方法用于向服务器请求 JSON 数据。本例的数据从 Github API 获取,可以打开源码里面的链接,看看原始的数据结构。 242 | 243 | ### 练习 244 | 245 | 1. 本例的 JSON 数据是 Github 上面最受欢迎的 JavaScript 项目。请在网页上显示一个列表,列出这些项目。 246 | 247 | ### 提示 248 | 249 | (1) `this.state.loading`记录数据加载是否结束。只要数据请求没有结束,`this.state.loading`就一直是`true`,网页上显示`loading`。 250 | 251 | (2) `this.state.error`保存数据请求失败时的错误信息。如果请求失败,`this.state.error`就是返回的错误对象,网页上显示报错信息。 252 | 253 | (3) `this.state.data`保存从服务器获取的数据。如果请求成功,可以先用`console.log`方法,将它在控制台里打印出来,看看数据结构。 254 | 255 | ```javascript 256 | render() { 257 | // 加一行打印命令,看看数据结构 258 | console.log(this.state.data); 259 | return { 260 | // ... 261 | ``` 262 | 263 | (4) `this.state.data`里面的`this.state.data.items`应该是一个数组,保存着每个项目的具体信息。可以使用`forEach`方法进行遍历处理。 264 | 265 | ```javascript 266 | var projects = this.state.data.items; 267 | var results = []; 268 | projects.forEach(p => { 269 | var item =
  • {p.name}
  • ; 270 | results.push(item); 271 | }); 272 | ``` 273 | 274 | (5)然后,将上一步的`results`插入网页即可。 275 | 276 | ```javascript 277 |
    278 | 279 |
    280 | ``` 281 | 282 | ## ReCharts 283 | 284 | ### 实验目的 285 | 286 | 1. 了解如何使用第三方组件库。 287 | 288 | ### 操作步骤 289 | 290 | 1. 浏览器打开`demos/recharts-demo/index.html`,查看效果。 291 | 292 | ## MobX 293 | 294 | ### 实验目的 295 | 296 | 1. 理解 MobX 框架 297 | 298 | ### 操作步骤 299 | 300 | (1)浏览器打开`demos/mobx-demo/browser-demo/index.html`,仔细查看源码 301 | 302 | (2) 命令行进入`demos/mobx-demo/`目录,执行如下的命令。 303 | 304 | ```bash 305 | $ npm install 306 | $ npm start 307 | ``` 308 | 309 | (3) 打开浏览器,访问 http://localhost:8080 ,查看结果,并仔细研究`app/`目录下面的代码。 310 | 311 | ### 注意事项 312 | 313 | ```javascript 314 | @observer 315 | class App extends React.Component { 316 | render() { 317 | // ... 318 | } 319 | } 320 | ``` 321 | 322 | `@observer`是一种新的语法,叫做“装饰器”,表示对整个类的行为进行修改,即将`App`类作为参数传入`observer`函数。这里的意思是,整个`App`类都是一个“观察者”,观察`store`的变化,只要一有变化,立刻重新渲染。 323 | 324 | 数据保存在`Store`里面。`Store`的属性分成两种:被观察的属性(`@observable`),和自动计算得到的属性`@computed`。 325 | 326 | ```javascript 327 | class Store { 328 | @observable name = 'Bartek'; 329 | @computed get decorated() { 330 | return `${this.name} is awesome!`; 331 | } 332 | } 333 | ``` 334 | 335 | `Store`的变化由用户引发。组件观察到`Store`的变化,自动重新渲染。 336 | 337 | ```javascript 338 |

    339 | {this.props.store.decorated} 340 |

    341 | 345 | this.props.store.name = event.currentTarget.value 346 | } 347 | /> 348 | ``` 349 | 350 | ## Redux 351 | 352 | ### 实验目的 353 | 354 | 1. 理解 Redux 架构 355 | 356 | ### 操作步骤 357 | 358 | (1) 命令行下进入`demos/redux-demo`目录,执行如下的命令。 359 | 360 | ```bash 361 | $ npm install 362 | $ npm start 363 | ``` 364 | 365 | (2)打开浏览器,访问 http://localhost:8080 ,查看结果,并仔细研究代码。 366 | 367 | ### 注意事项 368 | 369 | (1) Redux 要求 UI 的渲染组件都是纯组件,即不包含任何状态(`this.state`)的组件。 370 | 371 | ```javascript 372 |
    373 |

    {this.props.text}

    374 | 378 |
    379 | ``` 380 | 381 | (2) 进行数据处理、并包含状态的组件,称为“容器组件”。Redux 使用`connect`方法,自动生成 UI 组件对应的“容器组件”。 382 | 383 | ```javascript、 384 | // MyComponent 是纯的 UI 组件 385 | const App = connect( 386 | mapStateToProps, 387 | mapDispatchToProps 388 | )(MyComponent); 389 | ``` 390 | 391 | (3) `mapStateToProps`函数返回一个对象,表示一种映射关系,将 UI 组件的参数映射到`state`。 392 | 393 | ```javascript 394 | function mapStateToProps(state) { 395 | return { 396 | text: state.text, 397 | name: state.name 398 | }; 399 | } 400 | ``` 401 | 402 | (4) `mapDispatchToProps`函数也是返回一个对象,表示一种映射关系,但定义的是哪些用户的操作应该当作`Action`,传给`Store`。 403 | 404 | ```javascript 405 | function mapDispatchToProps(dispatch) { 406 | return { 407 | onChange: (e) => dispatch({ 408 | type: 'change', 409 | payload: e.target.value 410 | }) 411 | } 412 | } 413 | ``` 414 | 415 | (5) `reducer`函数用来接收`action`,算出新的`state`。 416 | 417 | ```javascript 418 | function reducer(state = { 419 | text: '你好,访问者', 420 | name: '访问者' 421 | }, action) { 422 | switch (action.type) { 423 | case 'change': 424 | return { 425 | name: action.payload, 426 | text: '你好,' + action.payload 427 | }; 428 | } 429 | } 430 | ``` 431 | 432 | `Store`由 Redux 提供的`createStore`方法生成,该方法接受`reducer`作为参数。 433 | 434 | ```javascript 435 | const store = createStore(reducer); 436 | 437 | ReactDOM.render( 438 | 439 | 440 | , 441 | document.body.appendChild(document.createElement('div')) 442 | ); 443 | ``` 444 | 445 | 为了把`Store`传入组件,必须使用 Redux 提供的`Provider`组件在应用的最外面,包裹一层。 446 | 447 | ## Simple App 448 | 449 | ### 实验目的 450 | 451 | 1. 学会使用 Node 编写简单的前端应用。 452 | 453 | ### 操作步骤 454 | 455 | (1)新建一个目录 456 | 457 | ```bash 458 | $ mkdir simple-app-demo 459 | $ cd simple-app-demo 460 | ``` 461 | 462 | (2)在该目录下,新建一个`package.json`文件。 463 | 464 | ```bash 465 | $ npm init -y 466 | ``` 467 | 468 | `package.json`是项目的配置文件。 469 | 470 | (3)安装`jquery`、`webpack`、`webpack-cli`这三个模块。 471 | 472 | ```bash 473 | $ npm install -S jquery 474 | $ npm install -S webpack webpack-cli 475 | ``` 476 | 477 | 打开`package.json`文件,会发现`jquery`、`webpack`和`webpack-cli`都加入了`dependencies`字段,并且带有版本号。 478 | 479 | (4)在项目根目录下,新建一个网页文件`index.html`。 480 | 481 | ```html 482 | 483 | 484 |

    Hello World

    485 | 486 | 487 | 488 | ``` 489 | 490 | (5)在项目根目录下,新建一个脚本文件`app.js`。 491 | 492 | ```javascript 493 | const $ = require('jquery'); 494 | $('h1').css({ color: 'red'}); 495 | ``` 496 | 497 | 上面代码中,`require`方法是 Node 特有的模块加载命令。 498 | 499 | (6)打开`package.json`,在`scripts`字段里面,添加一行。 500 | 501 | ```javascript 502 | "scripts": { 503 | "build": "webpack --mode production ./app.js -o ./bundle.js", 504 | "test": "...." 505 | }, 506 | ``` 507 | 508 | (7) 在项目根目录下,执行下面的命令,将脚本打包。 509 | 510 | ```bash 511 | $ npm run build 512 | ``` 513 | 514 | 执行完成,可以发现项目根目录下,新生成了一个文件`bundle.js`。 515 | 516 | (8)浏览器打开`index.html`,可以发现`Hello World`变成了红色。 517 | 518 | ### 练习 519 | 520 | 1. 修改样式,将标题变为蓝色,然后重新编译生成打包文件。 521 | 522 | ## REST API 523 | 524 | ### 实验目的 525 | 526 | 1. 熟悉 REST API 的基本用法 527 | 528 | ### 操作步骤 529 | 530 | (1) 命令行进入`demos/rest-api-demo`目录,执行下面的命令。 531 | 532 | ```bash 533 | $ npm install -S json-server 534 | ``` 535 | 536 | (2) 在项目根目录下,新建一个 JSON 文件`db.json`。 537 | 538 | ```javascript 539 | { 540 | "posts": [ 541 | { "id": 1, "title": "json-server", "author": "typicode" } 542 | ], 543 | "comments": [ 544 | { "id": 1, "body": "some comment", "postId": 1 } 545 | ], 546 | "profile": { "name": "typicode" } 547 | } 548 | ``` 549 | 550 | (3) 打开`package.json`,在`scripts`字段添加一行。 551 | 552 | ```javascript 553 | "scripts": { 554 | "server": "json-server db.json", 555 | "test": "..." 556 | }, 557 | ``` 558 | 559 | (4) 命令行下执行下面的命令,启动服务。 560 | 561 | ```bash 562 | $ npm run server 563 | ``` 564 | 565 | (5)打开 Chrome 浏览器的 Postman 应用。依次向`http://127.0.0.1:3000/posts`、`http://127.0.0.1:3000/posts/1`发出`GET`请求,查看结果。 566 | 567 | (6)向`http://127.0.0.1:3000/comments`发出`POST`请求。注意,数据体`Body`要选择`x-www-form-urlencoded`编码,然后依次添加下面两个字段。 568 | 569 | ```javascript 570 | body: "hello world" 571 | postId: 1 572 | ``` 573 | 574 | 发出该请求后,再向`http://127.0.0.1:3000/comments`发出`GET`请求,查看结果。 575 | 576 | (7) 向`http://127.0.0.1:3000/comments/2`发出`PUT`请求,数据体`Body`要选择`x-www-form-urlencoded`编码,然后添加下面的字段。 577 | 578 | ```javascript 579 | body: "hello react" 580 | ``` 581 | 582 | 发出该请求后,再向`http://127.0.0.1:3000/comments`发出`GET`请求,查看结果。 583 | 584 | (8)向`http://127.0.0.1:3000/comments/2`发出`delete`请求。 585 | 586 | 发出该请求后,再向`http://127.0.0.1:3000/comments`发出`GET`请求,查看结果。 587 | 588 | ## Express 589 | 590 | ### 实验目的 591 | 592 | 1. 学会 Express 搭建 Web 应用的基本用法。 593 | 594 | ### 操作步骤 595 | 596 | (1)进入`demos/express-demo`目录,命令行执行下面的命令,安装依赖。 597 | 598 | ```bash 599 | $ cd demos/express-demo 600 | $ npm install 601 | ``` 602 | 603 | (2)打开`app1.js`,尝试看懂这个脚本。 604 | 605 | ```javascript 606 | var express = require('express'); 607 | var app = express(); 608 | ``` 609 | 610 | 上面代码调用`express`,生成一个 Web 应用的实例。 611 | 612 | ```javascript 613 | var router = express.Router(); 614 | 615 | router.get('/', function(req, res) { 616 | res.send('

    Hello World

    '); 617 | }); 618 | 619 | app.use('/home', router); 620 | ``` 621 | 622 | 上面代码新建了一个路由对象,该对象指定访问根路由(`/`)时,返回`Hello World`。然后,将该路由加载在`/home`路径,也就是说,访问`/home`会返回`Hello World`。 623 | 624 | `router.get`方法的第二个参数是一个回调函数,当符合指定路由的请求进来,会被这个函数处理。该函数的两个参数,`req`和`res`都是Express 内置的对象,分别表示用户的请求和 Web 服务器的回应。`res.send`方法就表示服务器回应所送出的内容。 625 | 626 | ```javascript 627 | var port = process.env.PORT || 8080; 628 | 629 | app.listen(port); 630 | console.log('Magic happens on port ' + port); 631 | ``` 632 | 633 | 上面代码指定了外部访问的端口,如果环境变量没有指定,则端口默认为`8080`。最后两行是启动应用,并输出一行提示文字。 634 | 635 | (3)在命令行下,启动这个应用。 636 | 637 | ```bash 638 | $ node app1.js 639 | ``` 640 | 641 | 浏览器访问`localhost:8080/home`,看看是否输出`Hello World`。 642 | 643 | 然后,命令行下按 Ctrl + C,退出这个进程。 644 | 645 | (4)通过环境变量,自定义启动端口。 646 | 647 | 假定我们指定必须启动在`7070`端口,命令行可以这样操作。 648 | 649 | ```bash 650 | # Linux & Mac 651 | $ PORT=7070 node app1.js 652 | 653 | # windows cmd / (git cmd) 654 | $ set PORT=7070 655 | $ node app1.js 656 | 657 | # windows powershell 658 | $ $env:PORT=7070 659 | $ node app1.js 660 | ``` 661 | 662 | 浏览器就可以访问`localhost:7070/home`了。 663 | 664 | 然后,命令行下按 Ctrl + C,退出这个进程。 665 | 666 | 思考题:Node 应用能否直接在`80`端口启动? 667 | 668 | (5)打开`app2.js`,查看新增的那个路由。 669 | 670 | ```javascript 671 | router.get('/:name', function(req, res) { 672 | res.send('

    Hello ' + req.params.name + '

    '); 673 | }); 674 | ``` 675 | 676 | 上面代码新增了一个路由,这个路由的路径是一个命名参数`:name`,可以从`req.params.name`拿到这个传入的参数。 677 | 678 | 在命令行下,启动这个应用。 679 | 680 | ```bash 681 | $ node app2.js 682 | ``` 683 | 684 | 浏览器访问`localhost:8080/home/张三`,看看是否输出`Hello 张三`。 685 | 686 | 然后,命令行下按 Ctrl + C,退出这个进程。 687 | 688 | (6)打开`app3.js`,先查看页面头部新增的两行代码。 689 | 690 | ```javascript 691 | var express = require('express'); 692 | var app = express(); 693 | 694 | // 新增代码... 695 | var bodyParser = require('body-parser'); 696 | app.use(bodyParser.urlencoded({ extended: true })); 697 | 698 | // ... 699 | ``` 700 | 701 | 上面代码中,`body-parser`模块的作用,是对`POST`、`PUT`、`DELETE`等 HTTP 方法的数据体进行解析。`app.use`用来将这个模块加载到当前应用。有了这两句,就可以处理`POST`、`PUT`、`DELETE`等请求了。 702 | 703 | 下面查看新增的那个路由。 704 | 705 | ```javascript 706 | router.post('/', function (req, res) { 707 | var name = req.body.name; 708 | res.json({message: 'Hello ' + name}); 709 | }); 710 | ``` 711 | 712 | 上面代码表示,如果收到了`/`路径(实际上是`/home`路径)的`POST`请求,先从数据体拿到`name`字段,然后返回一段 JSON 信息。 713 | 714 | 在命令行下,启动这个应用。 715 | 716 | ```bash 717 | $ node app3.js 718 | ``` 719 | 720 | 然后,在 Chrome 浏览器的 Postman 插件里面,向`http://127.0.0.1:8080/home`发出一个`POST`请求。数据体的编码方法设为`x-www-form-urlencoded`,里面设置一个`name`字段,值可以随便取,假定设为`Alice`。也就是说,发出这样一个请求。 721 | 722 | ``` 723 | POST /home HTTP/1.1 724 | Host: 127.0.0.1:8080 725 | Content-Type: application/x-www-form-urlencoded 726 | 727 | name=Alice 728 | ``` 729 | 730 | 如果一切正常,服务器会返回一段 JSON 信息。 731 | 732 | ```javascript 733 | { 734 | "message": "Hello Alice" 735 | } 736 | ``` 737 | 738 | (7)打开`app4.js`,查看在所有路由之前新增的那个函数。 739 | 740 | ```javascript 741 | var router = express.Router(); 742 | 743 | // 新增的代码 744 | router.use(function(req, res, next) { 745 | console.log('There is a requesting.'); 746 | next(); 747 | }); 748 | 749 | router.get('/', function(req, res) { 750 | // ... 751 | ``` 752 | 753 | `router.use`的作用是加载一个函数。这个函数被称为中间件,作用是在请求被路由匹配之前,先进行一些处理。上面这个中间件起到 logging 的作用,每收到一个请求,就在命令行输出一条记录。请特别注意,这个函数内部的`next()`,它代表下一个中间件,表示将处理过的请求传递给下一个中间件。这个例子只有一个中间件,就进入路由匹配处理(实际上,`bodyparser`、`router`本质都是中间件,整个 Express 的设计哲学就是不断对 HTTP 请求加工,然后返回一个 HTTP 回应)。 754 | 755 | ### 练习 756 | 757 | 1. 请增加一个中间件,服务器每次收到用户请求,会在服务器的控制台打印出收到请求的时间。 758 | 759 | 2. URL 的查询字符串,比如`localhost:8080?name=Alice`里面的`name`,可以用`req.query.name`拿到。请修改一个路由,使之可以收到查询字符串,然后输出`'Hello ' + req.query.name`。 760 | 761 | ## ESLint 762 | 763 | ### 实验目的 764 | 765 | 1. 学会使用 ESLint 进行代码检查。 766 | 767 | ### 操作步骤 768 | 769 | (1)进入`demos/eslint-demo`目录,安装 ESLint。 770 | 771 | ```bash 772 | $ cd demos/eslint-demo 773 | $ npm install eslint --save-dev 774 | ``` 775 | 776 | (2)通常,我们会使用别人已经写好的代码检查规则,这里使用的是 Airbnb 公司的规则。所以,还要安装 ESLint 这个规则模块。 777 | 778 | ```bash 779 | $ npm install eslint-plugin-import eslint-config-airbnb-base --save-dev 780 | ``` 781 | 782 | 上面代码中,`eslint-plugin-import`是运行这个规则集必须的,所以也要一起安装。 783 | 784 | (3)ESLint 的配置文件是`.eslintrc.json`,放置在项目的根目录下面。新建这个文件,在里面指定使用 Airbnb 的规则。 785 | 786 | ```javascript 787 | { 788 | "extends": "airbnb-base" 789 | } 790 | ``` 791 | 792 | (4)打开项目的`package.json`,在`scripts`字段里面添加三个脚本。 793 | 794 | ```javascript 795 | { 796 | // ... 797 | "scripts" : { 798 | "test": "echo \"Error: no test specified\" && exit 1", 799 | "lint": "eslint **/*.js", 800 | "lint-html": "eslint **/*.js -f html -o ./reports/lint-results.html", 801 | "lint-fix": "eslint --fix **/*.js" 802 | }, 803 | // ... 804 | } 805 | ``` 806 | 807 | 除了原有的`test`脚本,上面代码新定义了三个脚本,它们的作用如下。 808 | 809 | - `lint`:检查所有`js`文件的代码 810 | - `lint-html`:将检查结果写入一个网页文件`./reports/lint-results.html` 811 | - `lint-fix`:自动修正某些不规范的代码 812 | 813 | (5)运行静态检查命令。 814 | 815 | ```bash 816 | $ npm run lint 817 | 818 | 1:5 error Unexpected var, use let or const instead no-var 819 | 2:5 warning Unexpected console statement no-console 820 | 821 | ✖ 2 problems (1 error, 1 warning) 822 | ``` 823 | 824 | 正常情况下,该命令会从`index.js`脚本里面,检查出来两个错误:一个是不应该使用`var`命令,另一个是不应该在生产环境使用`console.log`方法。 825 | 826 | (6)修正错误。 827 | 828 | ```bash 829 | $ npm run lint-fix 830 | ``` 831 | 832 | 运行上面的命令以后,再查看`index.js`,可以看到`var x = 1;`被自动改成了`const x = 1;`。这样就消除了一个错误,但是还留下一个错误。 833 | 834 | (7)修改规则。 835 | 836 | 由于我们想要允许使用`console.log`方法,因此可以修改`.eslintrc.json`,改变`no-console`规则。请将`.eslintrc.json`改成下面的样子。 837 | 838 | ```javascript 839 | { 840 | "extends": "airbnb-base", 841 | 842 | "rules": { 843 | "no-console": "off" 844 | } 845 | } 846 | ``` 847 | 848 | 再运行`npm run lint`,就不会报错了。 849 | 850 | ```bash 851 | $ npm run lint 852 | ``` 853 | 854 | ## Mocha 855 | 856 | ### 实验目的 857 | 858 | 1. 学会使用 Mocha 进行单元测试。 859 | 860 | ### 操作步骤 861 | 862 | (1) 进入`demos/mocha-demo`目录,安装 Mocha 和 Chai。 863 | 864 | ```bash 865 | $ cd demos/mocha-demo 866 | $ npm install -D mocha 867 | $ npm install -D chai 868 | ``` 869 | 870 | (2)打开`add.js`文件,查看源码,我们要测试的就是这个脚本。 871 | 872 | ```javascript 873 | function add(x, y) { 874 | return x + y; 875 | } 876 | 877 | module.exports = add; 878 | ``` 879 | 880 | (3)编写一个测试脚本`add.test.js`。 881 | 882 | ```javascript 883 | var add = require('./add.js'); 884 | var expect = require('chai').expect; 885 | 886 | describe('加法函数的测试', function() { 887 | it('1 加 1 应该等于 2', function() { 888 | expect(add(1, 1)).to.be.equal(2); 889 | }); 890 | }); 891 | ``` 892 | 893 | 测试脚本与所要测试的源码脚本同名,但是后缀名为`.test.js`(表示测试)或者`.spec.js`(表示规格)。比如,`add.js`的测试脚本名字就是`add.test.js`。 894 | 895 | 测试脚本里面应该包括一个或多个`describe`块,每个`describe`块应该包括一个或多个`it`块。 896 | 897 | `describe`块称为"测试套件"(test suite),表示一组相关的测试。它是一个函数,第一个参数是测试套件的名称("加法函数的测试"),第二个参数是一个实际执行的函数。 898 | 899 | `it`块称为"测试用例"(test case),表示一个单独的测试,是测试的最小单位。它也是一个函数,第一个参数是测试用例的名称("1 加 1 应该等于 2"),第二个参数是一个实际执行的函数。 900 | 901 | 上面的测试脚本里面,有一句断言。 902 | 903 | ```javascript 904 | expect(add(1, 1)).to.be.equal(2); 905 | ``` 906 | 907 | 所谓"断言",就是判断源码的实际执行结果与预期结果是否一致,如果不一致就抛出一个错误。上面这句断言的意思是,调用`add(1, 1)`,结果应该等于`2`。 908 | 909 | 所有的测试用例(`it`块)都应该含有一句或多句的断言。它是编写测试用例的关键。断言功能由断言库来实现,Mocha本身不带断言库,所以必须先引入断言库。 910 | 911 | ```javascript 912 | var expect = require('chai').expect; 913 | ``` 914 | 915 | 断言库有很多种,Mocha并不限制使用哪一种。上面代码引入的断言库是`chai`,并且指定使用它的`expect`断言风格。 916 | 917 | (4)打开`package.json`文件,改写`scripts`字段的`test`脚本。 918 | 919 | ```javascript 920 | "scripts": { 921 | "test": "echo \"Error: no test specified\" && exit 1" 922 | }, 923 | 924 | // 改成 925 | 926 | "scripts": { 927 | "test": "mocha *.test.js" 928 | }, 929 | ``` 930 | 931 | (5)命令行下,执行下面的命令,运行测试用例。 932 | 933 | ```bash 934 | $ npm test 935 | ``` 936 | 937 | 正常情况下,命令行会有提示,表示测试用例已经通过了。 938 | 939 | ### 练习 940 | 941 | 1. 请在`add.test.js`里面添加一个测试用例,测试`3`加上`-3`,`add`函数应该返回`0`。 942 | 943 | ## Nightmare 944 | 945 | ### 实验目的 946 | 947 | 1. 学会使用 Nightmare 完成功能测试。 948 | 949 | ### 操作步骤 950 | 951 | (1)进入`./demos/nightmare-demo`目录,安装依赖。 952 | 953 | ```bash 954 | $ cd demos/nightmare-demo 955 | 956 | # Linux & Mac 957 | $ env ELECTRON_MIRROR=https://npm.taobao.org/mirrors/electron/ npm install 958 | 959 | # Windows 960 | $ set ELECTRON_MIRROR=https://npm.taobao.org/mirrors/electron/ 961 | $ npm install 962 | ``` 963 | 964 | 注意,Nightmare 会先安装 Electron,而 Electron 的安装需要下载境外的包,有时会连不上,导致安装失败。所以,这里先设置了环境变量,指定使用国内的 Electron 源,然后才执行安装命令。 965 | 966 | (2)查看一下浏览器自动化脚本`taobao.test.js`。 967 | 968 | ```javascript 969 | var Nightmare = require('nightmare'); 970 | var nightmare = Nightmare({ show: true }); 971 | ``` 972 | 973 | 上面代码表示新建一个 Nightmare 实例,并且运行功能中,自动打开浏览器窗口。 974 | 975 | ```javascript 976 | nightmare 977 | .goto('https://www.taobao.com/') 978 | .type('#q', '电视机') 979 | .click('form[action*="/search"] [type=submit]') 980 | .wait('#spulist-grid') 981 | .evaluate(function () { 982 | return document.querySelector('#spulist-grid .grid-item .info-cont') 983 | .textContent.trim(); 984 | }) 985 | .end() 986 | ``` 987 | 988 | 上面代码表示,打开淘宝首页,在搜索框键入`电视机`,点击“搜索”按钮,等待`#spulist-grid`元素出现,在页面内注入(`evaluate`)代码,将执行结果返回。 989 | 990 | ```javascript 991 | .then(function (result) { 992 | console.log(result); 993 | }) 994 | .catch(function (error) { 995 | console.error('Search failed:', error); 996 | }); 997 | ``` 998 | 999 | Nightmare 会返回一个 Promise 对象,`then`方法指定操作成功的回调函数,`catch`方法指定操作失败的回调函数。 1000 | 1001 | (3)命令行下运行这个示例脚本。 1002 | 1003 | ```bash 1004 | $ node taobao.test.js 1005 | ``` 1006 | 1007 | 正常情况下,运行结束后,命令行会显示淘宝“电视机”搜索结果的第一项。 1008 | 1009 | (4)浏览器打开`index.html`文件,这是 React 练习时做过的一个例子,点击`Hello World`,标题会变成`Hello Clicked`。我们就要编写测试脚本,测试这个功能。 1010 | 1011 | (5)打开测试脚本`test.js`。 1012 | 1013 | ```javascript 1014 | var Nightmare = require('nightmare'); 1015 | var expect = require('chai').expect; 1016 | var fork = require('child_process').fork; 1017 | 1018 | describe('test index.html', function() { 1019 | var child; 1020 | 1021 | before(function (done) { 1022 | child = fork('./server.js'); 1023 | child.on('message', function (msg) { 1024 | if (msg === 'listening') { 1025 | done(); 1026 | } 1027 | }); 1028 | }); 1029 | 1030 | after(function () { 1031 | child.kill(); 1032 | }); 1033 | ``` 1034 | 1035 | 上面代码中,`before`和`after`是 Mocha 提供的两个钩子方法,分别在所有测试开始前和结束后运行。这里,我们在`before`方法里面,新建一个子进程,用来启动 HTTP 服务器;测试结束后,再杀掉这个子进程。 1036 | 1037 | 注意,`before`方法的参数是一个函数,它接受`done`作为参数。`done`是 Mocha 提供的一个函数,用来表示异步操作完成。如果不调用`done`,Mocha 就会认为异步操作没有结束,一直停在这一步,不往下执行,从而导致超时错误。 1038 | 1039 | 子进程脚本`server.js`的代码非常简单,只有四行。 1040 | 1041 | ```javascript 1042 | var httpServer = require('http-server'); 1043 | var server = httpServer.createServer(); 1044 | server.listen(8080); 1045 | process.send('listening'); 1046 | ``` 1047 | 1048 | 上面代码中,我们在`8080`端口启动 HTTP 服务器,然后向父进程发消息,表示启动完成。 1049 | 1050 | (6)真正的自动化测试脚本如下。 1051 | 1052 | ```javascript 1053 | it('点击后标题改变', function(done) { 1054 | var nightmare = Nightmare({ show: true }); 1055 | nightmare 1056 | .goto('http://127.0.0.1:8080/index.html') 1057 | .click('h1') 1058 | .wait(1000) 1059 | .evaluate(function () { 1060 | return document.querySelector('h1').textContent; 1061 | }) 1062 | .end() 1063 | .then(function(text) { 1064 | expect(text).to.equal('Hello Clicked'); 1065 | done(); 1066 | }) 1067 | }); 1068 | ``` 1069 | 1070 | 上面代码中,首先打开网页,点击`h1`元素,然后等待 1 秒钟,注入脚本获取`h1`元素的文本内容。接着,在`then`方法里面,做一个断言,判断获取的文本是否正确。 1071 | 1072 | (7)运行这个测试脚本。 1073 | 1074 | ```bash 1075 | $ npm test 1076 | ``` 1077 | 1078 | 如果一切正常,命令行下会显示测试通过。 1079 | 1080 | ### 练习 1081 | 1082 | 1. 请写一个测试用例,验证`

    `的字体颜色是红色。(提示:可以使用`Window.getComputedStyle()`方法,获取元素的最终样式。) 1083 | 1084 | ## Travis CI 1085 | 1086 | ### 实验目的 1087 | 1088 | 1. 了解持续集成的做法,学会使用 Travis CI。 1089 | 1090 | ### 操作步骤 1091 | 1092 | (1)注册 [Github](https://github.com) 的账户。如果你已经注册过,跳过这一步。 1093 | 1094 | (2)访问这个代码库[`github.com/ruanyf/travis-ci-demo`](https://github.com/ruanyf/travis-ci-demo),点击右上角的`Fork`按钮,将它克隆到你自己的空间里面。 1095 | 1096 | (3)将你`fork`的代码库,克隆到本地。注意,要将下面网址之中的`[your_username]`改成你的 Github 用户名。 1097 | 1098 | ```bash 1099 | // Linux & Mac 1100 | $ git clone git@github.com:[your_username]/travis-ci-demo.git 1101 | 1102 | // Windows 1103 | $ git clone https://github.com:[your_username]/travis-ci-demo 1104 | ``` 1105 | 1106 | (4)使用你的 Github 账户,登录 [Travis CI](https://travis-ci.org/auth) 的首页。然后,访问 [Profile](https://travis-ci.org/profile) 页面,选定`travis-ci-demo`代码库运行自动构建。 1107 | 1108 | (5)回到命令行,进入你本地的`travis-ci-demo`目录,切换到`demo01`分支。 1109 | 1110 | ```bash 1111 | $ cd travis-ci-demo 1112 | $ git checkout demo01 1113 | ``` 1114 | 1115 | 项目根目录下面有一个`.travis.yml`文件,这是 Travis CI 的配置文件。如果没有这个文件,就不会触发 Travis CI 的自动构建。打开看一下。 1116 | 1117 | ```bash 1118 | language: node_js 1119 | node_js: 1120 | - "node" 1121 | ``` 1122 | 1123 | 上面代码指定,使用 Node 完成构建,版本是最新的稳定版。 1124 | 1125 | 指定 Node 的版本号也是可以的。 1126 | 1127 | ```javascript 1128 | language: node_js 1129 | node_js: 1130 | - "4.1" 1131 | ``` 1132 | 1133 | 上面代码指定使用 Node 4.1 版。 1134 | 1135 | (6)Travis CI 默认依次执行以下九个脚本。 1136 | 1137 | - `before_install` 1138 | - `install` 1139 | - `before_script` 1140 | - `script` 1141 | - `after_success` 或者 `after_failure` 1142 | - `after_script` 1143 | - `before_deploy`(可选) 1144 | - `deploy`(可选) 1145 | - `after_deploy`(可选) 1146 | 1147 | 用户需要用到哪个脚本,就需要提供该脚本的内容。 1148 | 1149 | 对于 Node 项目,以下两个脚本有默认值,可以不用自己设定。 1150 | 1151 | ```javascript 1152 | "install": "npm install", 1153 | "script": "npm test" 1154 | ``` 1155 | 1156 | (7)打开当前分支的`package.json`,可以发现它的`test`脚本是一个`lint`命令。 1157 | 1158 | ```javascript 1159 | "scripts": { 1160 | "test": "jshint hello.js" 1161 | }, 1162 | ``` 1163 | 1164 | (8)在项目根目录下,新建一个新文件`NewUser.txt`,内容是你的用户名。提交这个文件,就会触发 Travis CI 的自动构建。 1165 | 1166 | ```bash 1167 | $ git add -A 1168 | $ git commit -m 'Testing Travis CI' 1169 | $ git push 1170 | ``` 1171 | 1172 | (9)等到 Travis CI 完成自动构建,到页面上[检查](https://travis-ci.org/repositories)构建结果。 1173 | 1174 | (10)切换到`demo02`分支,打开`package.json`,可以看到`test`脚本,现在需要完成两步操作了。 1175 | 1176 | ```javascript 1177 | "scripts": { 1178 | "lint": "jshint hello.js hello.test.js", 1179 | "test": "npm run lint && mocha hello.test.js" 1180 | }, 1181 | ``` 1182 | 1183 | (11)重复上面第 8 步和第 9 步。 1184 | 1185 | ### 练习 1186 | 1187 | 1. 修改`hello.js`,让其输出`Hello Node`。并修改测试用例`hello.test.js`,使之能够通过 Travis CI 的自动构建。 1188 | -------------------------------------------------------------------------------- /demos/angular-demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
    7 |

    姓名 :

    8 |

    你好,{{name}}

    9 |
    10 | 11 | 12 | -------------------------------------------------------------------------------- /demos/backbone-demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Backbone Demo 6 | 7 | 8 | 9 |

    Backbone Routing Demo

    10 | 11 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /demos/backbone-demo/js/backbone.js: -------------------------------------------------------------------------------- 1 | // Backbone.js 0.9.2 2 | 3 | // (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. 4 | // Backbone may be freely distributed under the MIT license. 5 | // For all details and documentation: 6 | // http://backbonejs.org 7 | (function(){var l=this,y=l.Backbone,z=Array.prototype.slice,A=Array.prototype.splice,g;g="undefined"!==typeof exports?exports:l.Backbone={};g.VERSION="0.9.2";var f=l._;!f&&"undefined"!==typeof require&&(f=require("underscore"));var i=l.jQuery||l.Zepto||l.ender;g.setDomLibrary=function(a){i=a};g.noConflict=function(){l.Backbone=y;return this};g.emulateHTTP=!1;g.emulateJSON=!1;var p=/\s+/,k=g.Events={on:function(a,b,c){var d,e,f,g,j;if(!b)return this;a=a.split(p);for(d=this._callbacks||(this._callbacks= 8 | {});e=a.shift();)f=(j=d[e])?j.tail:{},f.next=g={},f.context=c,f.callback=b,d[e]={tail:g,next:j?j.next:f};return this},off:function(a,b,c){var d,e,h,g,j,q;if(e=this._callbacks){if(!a&&!b&&!c)return delete this._callbacks,this;for(a=a?a.split(p):f.keys(e);d=a.shift();)if(h=e[d],delete e[d],h&&(b||c))for(g=h.tail;(h=h.next)!==g;)if(j=h.callback,q=h.context,b&&j!==b||c&&q!==c)this.on(d,j,q);return this}},trigger:function(a){var b,c,d,e,f,g;if(!(d=this._callbacks))return this;f=d.all;a=a.split(p);for(g= 9 | z.call(arguments,1);b=a.shift();){if(c=d[b])for(e=c.tail;(c=c.next)!==e;)c.callback.apply(c.context||this,g);if(c=f){e=c.tail;for(b=[b].concat(g);(c=c.next)!==e;)c.callback.apply(c.context||this,b)}}return this}};k.bind=k.on;k.unbind=k.off;var o=g.Model=function(a,b){var c;a||(a={});b&&b.parse&&(a=this.parse(a));if(c=n(this,"defaults"))a=f.extend({},c,a);b&&b.collection&&(this.collection=b.collection);this.attributes={};this._escapedAttributes={};this.cid=f.uniqueId("c");this.changed={};this._silent= 10 | {};this._pending={};this.set(a,{silent:!0});this.changed={};this._silent={};this._pending={};this._previousAttributes=f.clone(this.attributes);this.initialize.apply(this,arguments)};f.extend(o.prototype,k,{changed:null,_silent:null,_pending:null,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.get(a);return this._escapedAttributes[a]=f.escape(null== 11 | b?"":""+b)},has:function(a){return null!=this.get(a)},set:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c||(c={});if(!d)return this;d instanceof o&&(d=d.attributes);if(c.unset)for(e in d)d[e]=void 0;if(!this._validate(d,c))return!1;this.idAttribute in d&&(this.id=d[this.idAttribute]);var b=c.changes={},h=this.attributes,g=this._escapedAttributes,j=this._previousAttributes||{};for(e in d){a=d[e];if(!f.isEqual(h[e],a)||c.unset&&f.has(h,e))delete g[e],(c.silent?this._silent: 12 | b)[e]=!0;c.unset?delete h[e]:h[e]=a;!f.isEqual(j[e],a)||f.has(h,e)!=f.has(j,e)?(this.changed[e]=a,c.silent||(this._pending[e]=!0)):(delete this.changed[e],delete this._pending[e])}c.silent||this.change(c);return this},unset:function(a,b){(b||(b={})).unset=!0;return this.set(a,null,b)},clear:function(a){(a||(a={})).unset=!0;return this.set(f.clone(this.attributes),a)},fetch:function(a){var a=a?f.clone(a):{},b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return!1;c&&c(b,d)}; 13 | a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},save:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c=c?f.clone(c):{};if(c.wait){if(!this._validate(d,c))return!1;e=f.clone(this.attributes)}a=f.extend({},c,{silent:!0});if(d&&!this.set(d,c.wait?a:c))return!1;var h=this,i=c.success;c.success=function(a,b,e){b=h.parse(a,e);if(c.wait){delete c.wait;b=f.extend(d||{},b)}if(!h.set(b,c))return false;i?i(h,a):h.trigger("sync",h,a,c)};c.error=g.wrapError(c.error, 14 | h,c);b=this.isNew()?"create":"update";b=(this.sync||g.sync).call(this,b,this,c);c.wait&&this.set(e,a);return b},destroy:function(a){var a=a?f.clone(a):{},b=this,c=a.success,d=function(){b.trigger("destroy",b,b.collection,a)};if(this.isNew())return d(),!1;a.success=function(e){a.wait&&d();c?c(b,e):b.trigger("sync",b,e,a)};a.error=g.wrapError(a.error,b,a);var e=(this.sync||g.sync).call(this,"delete",this,a);a.wait||d();return e},url:function(){var a=n(this,"urlRoot")||n(this.collection,"url")||t(); 15 | return this.isNew()?a:a+("/"==a.charAt(a.length-1)?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return null==this.id},change:function(a){a||(a={});var b=this._changing;this._changing=!0;for(var c in this._silent)this._pending[c]=!0;var d=f.extend({},a.changes,this._silent);this._silent={};for(c in d)this.trigger("change:"+c,this,this.get(c),a);if(b)return this;for(;!f.isEmpty(this._pending);){this._pending= 16 | {};this.trigger("change",this,a);for(c in this.changed)!this._pending[c]&&!this._silent[c]&&delete this.changed[c];this._previousAttributes=f.clone(this.attributes)}this._changing=!1;return this},hasChanged:function(a){return!arguments.length?!f.isEmpty(this.changed):f.has(this.changed,a)},changedAttributes:function(a){if(!a)return this.hasChanged()?f.clone(this.changed):!1;var b,c=!1,d=this._previousAttributes,e;for(e in a)if(!f.isEqual(d[e],b=a[e]))(c||(c={}))[e]=b;return c},previous:function(a){return!arguments.length|| 17 | !this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},isValid:function(){return!this.validate(this.attributes)},_validate:function(a,b){if(b.silent||!this.validate)return!0;var a=f.extend({},this.attributes,a),c=this.validate(a,b);if(!c)return!0;b&&b.error?b.error(this,c,b):this.trigger("error",this,c,b);return!1}});var r=g.Collection=function(a,b){b||(b={});b.model&&(this.model=b.model);b.comparator&&(this.comparator=b.comparator); 18 | this._reset();this.initialize.apply(this,arguments);a&&this.reset(a,{silent:!0,parse:b.parse})};f.extend(r.prototype,k,{model:o,initialize:function(){},toJSON:function(a){return this.map(function(b){return b.toJSON(a)})},add:function(a,b){var c,d,e,g,i,j={},k={},l=[];b||(b={});a=f.isArray(a)?a.slice():[a];c=0;for(d=a.length;c=b))this.iframe=i('