序言
目的
这将是一个系列文,动机是为了希望给公司及项目组其他的新人同学同事作为入门教材使用。
整个系列将从零开始构建一个基于完整工具链构建的angular应用。使用的工具包括且不仅限于:
- nodejs/npm
- gulp
- sass/scss
我们将完成什么
我们使用Angular做一个很简单的,包括登陆与一些基础增删改查的应用。
Vol.1 使用npm,bower,gulp构建最基本的angular开发环境
介绍
本文面向有一定基础的Angular开发者,如果您是第一次接触angular,我建议您先去看下大漠穷秋先生写的基础教程书籍或者通过使用yoeman进行构建,对angular的工程化编写有一个初步的认识。
本次使用的组件与相应演示代码
- github代码 https://github.com/akirapanda/angular-with-gulp
- npm
- bower
- angular
- angular-ui-router
- gulp
- gulp-connect
使用npm与bower进行第三方组件包的管理
什么是npm
npm是nodejs提供的包管理工具(package management),对于js应用的开发类似于java的mvn,ruby的gem。用于管理应用相关的第三方组件与工具,而不是“上古”时期,开发人自己下载解压缩放到指定目录,再通过<script>标签进行引入。
比如最常见的我们引入jquery,我们除了引入了文件还需要进行版本的控制。
<script src="https://code.jquery.com/jquery-2.2.3.min.js"></script>
而使用npm进行管理则是通过描述文件进行相关的版本控制管理,将第三方组件管理排除与应用代码之外,为工程化管理代码提供便利。
除此以外npm还提供了许多nodejs周边cli工具的包管理,比如nodejs自己本身。
什么是bower
bower是由twitter开发的一套客户端包管理工具。其思想与npm基本是一致的,为什么会有两套不同的管理体系一直是一个悬而未解的谜团。
通常来说客户端的包依赖我们会通过bower进行管理,而服务端(如果存在)则会使用npm进行管理依赖。
因为客户端的包大部分都是js形式的第三方组件包,而npm包又不少是需要进行本地编译的cli工具包。
不过最近已经开始有开始提倡所有第三方包依赖都通过npm进行管理的趋势了。
那么什么是gulp
gulp是js的又一种比较流行的、基于任务的构建工具。类似C语言的make,Java的ant、maven,Ruby的rake。
而gulp的插件机制又提供给了开发者许多遍历,使js存在一个“编译期“的概念。
这部分我们先不展开。
初始化npm和bower
开始前请确定您已经安装了最新版本的nodejs,我使用的版本是mac的v4.2.1
我们先全局安装一下bower
npm install -g bower
完成后,我们新建工程文件夹,并且命名为news。
其次我们依次输入初始化命令:
npm init
bower init
之后我们并会得到两个文件,package.json与bower.json,两个文件分别用于描述我们依赖的第三方包清单。
通过npm安装gulp
与安装bower一样,我们需要安装一下gulp,但有所区别的是,我们希望将来所有使用我们这份代码的人都会可以通过npm自动安装下gulp。则我们需要比刚才的命令多打一个后缀参数:
npm install gulp --save-dev //npm下载后会将相关组件信息记录与package.json
键入完命令后,npm会做两件事情:
- 下载gulp的最新版本至node_modules文件夹下
- 将gulp的组件记录插入至package.json
我们查看下最新的package.json其内容将会出现gulp的信息。
"devDependencies": {
"gulp": "^3.9.1"
}
通过bower下载angualrjs
与npm安装gulp一样,我们通过bower的cli命令来安装angular
bower install angular --save
- bower会下载angular最新版本至bower_components文件夹下,
- 相关信息记录与bower.json中
\\bower.json会出现angular信息
"dependencies": {
"angular": "^1.5.4"
}
至此,相当文件夹下的目录结构便是
-news
--node_modules
--bower_components
-package.json
-bower.json
进入angular环节
这次我们就会比较简单的做一个只有一页的angualr应用,我们首先在news目录下新建一个app目录用于存储所有我们自行编写应用代码。其目录结构为
-news
--app
--scripts //所有js代码
--styles //所有样式代码
-home.html //页面
-index.hmtl //入口页面
第一个应用
这次我们目的是做一个欢迎的页面,其访问的url为/home,用户在下面输入自己姓名的后,上面的欢迎信息会自动发生变化的小应用。
安装angular-ui-router
angular-ui-router是angular中一套比较常用的路由控制库。什么是路由控制呢?也就是把url当成状态入口与相关控制器、视图进行分发绑定的组件。
我们通过bower进行安装,
bower install angular-ui-router --save
编写我们的入口index.html
我们的目的是编写一个spa(单页应用),则index.html则负责在第一次的时候为用户访问整个系统提供入口,下载相关的依赖资源。
\\index.html
<!doctype html>
<html ng-app="app" >
<head>
<meta charset="utf-8">
<title></title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width">
</head>
<body >
<div ui-view>
</div>
<!-- 将bower下载关联的组件js引入-->
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/angular-ui-router/release/angular-ui-router.js"></script>
</body>
</html>
如何开发运行应用呢?
如果之前没有接触过web开发和nodejs的同学在这里多半会将整个工程放到apache的htdoc目录下进行访问测试了吧?
而知道http-server的同学在news目录下运行了http-server又发现,上文中的script引入bower组件的地址是不正确的,并且地址栏上多一个app的目录路径。
推荐的做法是使用gulp-connect来管理开发环境的服务器启动。
通过npm安装gulp-connect
我们一样通过npm来装gulp-connect组件
npm install gulp-connect --save-dev
然后我们新建一个gulpfile.js文件来定义开发服务器的启动任务:
\\gulpfile.js
'user strict';
var gulp = require('gulp'); //require node_modules中的gulp包
var path = require("path");
var connect = require("gulp-connect"); //require node_modules中的gulp-connect包
var ROOT_PATH = path.resolve(__dirname); //项目根目录
var APP_PATH = path.resolve(ROOT_PATH,"app"); //应用代码目录
gulp.task("connect",function(){
connect.server({
root: ["app"], //使用哪个目录作为启动的根目录
livereload:true, //实施加载,可理解为热部署
middleware: function(connect) {
return [connect().use('/bower_components', connect.static('bower_components'))]; //见下文
}
});
});
gulp.task("default",["connect"]); // gulp如缺省目标task则使用connect作为task
我们通过在gulp中新建了一个connect任务,使用connect.server来启动一个开发部署的http服务器进行开发,其中root,livereload参数都不难理解。那么middleware的目的是什么呢?
这里要再次拿我们代码目录拿出来说一下,现在我们的代码目录应该是这样的
-news
--node_modules
--bower_components
-package.json
-bower.json
--app <---http启动加载的文件根目录
--scripts
--styles
-index.html
-home.html
则当服务器启动后index.html,我们是无法访问到在http目录以外的bower_compments目录中的第三方组件包的。我们做middelware的动机就是让connect额外的将/bower_compmenets文件夹加载到我们的应用服务器的内容中,就好比我们复制了一个备份到/app/bower_compmenets文件夹中。
当我们现在在应用目录下键入gulp命令后,便可看到控制端有以下的提示信息
我们便可在浏览器输入地址127.0.0.1:8080访问到我们index.html文件,并且也可以正确的引入相关的angular组件。
编写Module
可能很多同学想到是先去写一个controller来实现我们预期的功能,但我们提倡的是around module的开发模式,即所有的controller,service等angular组件全部围绕着module进行组织。故我们这里先将全局的module编写。
\\ app/scripts/index.module.js
(function ()
{
'use strict';
angular
.module('app', [
'ui.router'
])
.config(routeConfig);
routeConfig.$inject = ['$stateProvider', '$urlRouterProvider','$locationProvider'];
function routeConfig($stateProvider, $urlRouterProvider, $locationProvider)
{
$stateProvider
.state('home', {
url: '/home',
templateUrl: 'home.html',
controller: 'IndexController',
controllerAs: 'vm'
});
}
})();
我们使用的这种写法有几个地方需要注意:
- 通过IIFE风格的编码,使代码在声明后立刻被运行;
(function(){
})()
- 只在xxx.module文件中对angular.module进行setter操作
angular
.module('app', [
'ui.router'
])
module('name',[]) 为setter,即声明一个新的module至angular上下文
- 通过module.config,controlle,service方法将关联组件加入对应的module中
angular
.module('app', [
'ui.router'
])
.config(routeConfig);
// 等价于angular.module('app').config(routeConfig)
- 将相关的实现函数单独放下而不是通过config(function(){})进行声明,增加可读性
- 通过$inject方法来控制注入,而不是通过参数对比注入
//一般我们的做法可能是
function routeConfig(['$stateProvider', '$urlRouterProvider', '$locationProvider'],$stateProvider, $urlRouterProvider, $locationProvider){
}
这样通过参数控制的注入,一般会有顺序问题,参数一多,每次找的问题的时候必须先要进行“排排坐吃果果”的比较。推荐使用$inject来控制注入
routeConfig.$inject = ['$stateProvider', '$urlRouterProvider','$locationProvider'];
在实际声明部分便需要需要再次声明注入的形式的
function routeConfig($stateProvider, $urlRouterProvider, $locationProvider)
{
}
通过 ui-router来创建一个/home的路由
我们通过ui-router的stateProvider来增加一个新的state home,其访问的url为/home
function routeConfig($stateProvider, $urlRouterProvider, $locationProvider)
{
$stateProvider
.state('home', {
url: '/home',
templateUrl: 'home.html',
controller: 'IndexController',
controllerAs: 'vm' //使用vm而不是$scope
});
}
而home.html的内容为,这里我们使用了controller as vm的方法来访问IndexController中的实例变量,而不是使用$scope。至于这是为了什么,可以阅读我之前的说明或者谷歌一些相关的讨论。
视图中我们将在h1便签部分显示controller中的name变量的值,并且将其绑定与input的文本输入框中,当用户填写新的文本值时,会出修改name的值,从而上面h1显示的值也会发生变化。
<h1>Hello {{vm.name}}</h1>
<label>input your name:</label>
<input ng-model="vm.name"></input>
那么对应的我们也要写一个IndexController
\\ app/scripts/index.controller.js
(function ()
{
'use strict';
angular
.module('app')
.controller('IndexController', IndexController);
function IndexController()
{
var vm = this; // 将this指针更名为vm
vm.name = "unknown"; //初始化页面的name 为 unknown
}
})();
在index.html中引入module与contoller
<body>
<div ui-view>
</div>
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/angular-ui-router/release/angular-ui-router.js"></script>
<script src="scripts/index.module.js"></script>
<script src="scripts/index.controller.js"></script>
</body>
访问 127.0.0.1:8080/#/home
这时便可看到页面显示了预期的效果,每当我们在文本框输入新的值上方的显示值则同步更新。这就是angular双向绑定的优势之处。
下一期我们会做什么
我们会使用angular中的service来封装一些外部的API编写一个显示当前天气信息的小应用。