AngularJS 2.0和AngularJS CLI初体验-揭秘全栈开发MEAN APP

第一次见到全栈工程师这个词,我想当然的认为就是既懂后端又懂前端的工程师嘛。我这个认识是没错的,但并未结合到技术发展前沿。难怕是很久之前,对于很多程序员来说,他们肯定既懂得后端和前端的(我们暂且不论前端的)。毕竟对于前端开发而言(注意,我没有涉及到设计),无非是HTML/CSS/JavaScript嘛。那么,为什么忽然会出现Full Stack Developer这样一个新名词呢?

随着近期对CSS3/HTML5/JS ES6等的新一轮学习(07年看过几本“犀牛书”也就不了了之啦,工作中偶尔用到能应付就好),我慢慢开始真正理解到Nodejs(09-11年瞅过好几眼)。理所当然的,我也接触到前端框架(VUE/blackbone/react/angular)的知识。这些都是伴随着移动互联网兴起的产物之一吧。当前,传统的LAMP/LNMP应用完全可以移植到MEAN架构(推荐阅读《精通MEAN: MEAN堆栈》)。配合上Yeoman、GulpJS、Mocha等工具,俨然一个完善的开发框架。此时我才明白为什么很多老外自称是Full Stack Developer啦!怪我后知后觉了……

以上都是废话~进入主题:
我们先看看MEAN的构成部分:

  • MongoDB: 著名的NoSQL,作为应用中的后端数据库。跟传统数据库一样,都是为了持续化的数据。本文不牵涉到MongoDB实现,我们会用模拟的数据模型。
  • ExpressJS: 一个基于nodejs的web框架,就像dangjo基于python, ruby on rails等等。
  • AngularJS: 客户端/前端的MVC框架,2011年发布,目前主要由Google和开源社区来维护。其特点有双向数据绑定和注入。我们这里使用AngularJS2.0, 与版本一相比,二更加快;而且学习了FB React框架中的Components概念。
  • NodeJS: 提供ExpressJS和我们整个应用的运行环境。

我选择AngularJS 2.0的另一个重要原因就是Angular CLI. 它为我们部署应用带来了很大方便。换言之,这篇博文基本上是围绕Angular CLI展开的。那么,我们当然需要先安装angular cli先咯。


npm install -g @angular/cli

下面我们就可以开始利用angular cli建立MEAN项目啦。

cd ~/putty.biz
ng new meantest
cd meantest
cat .angular-cli.json #当前Angular项目的配置文件,比较常用的是指定dist,即ng-build命令编译我们应用给生产环境的输出目录。
ng serve

就这么简单,一个基于AngularJS 2.0的应用搭建好啦。根据提示,我们到浏览器访问一下http://localhost:4200吧!

ExpressJS

现在,我们将ExpressJS添加到我们的应用中来.


yarn add express body-parser #等同于npm install --save express body-parser

Yarn是一个npm替代解决方案,追求是高效(cache,yarn cache ls, yarn cache dir)、安全(checksums)、稳定(lockfile),引入了基于APP的PACKAGE(可分享)概念。官网有中文版,虽然有些地方翻译得还是生涩了些,不妨碍大家学习其用法。

回到我们MEAN APP引入ExpressJS,现在我们需要建立一个server.js的文件和一个server文件夹。server.js将放置EXPRESSJS服务器代码;而server文件夹则放置其他各项后端expressjs需要的文件。


touch server.js && mkdir server

下面是server.js代码:


// Get dependencies                                                             
const express = require('express');                                             
const path = require('path');                                                   
const http = require('http');                                                   
const bodyParser = require('body-parser');                                      
                                                                                
// Get our API routes                                                           
const api = require('./server/routes/api');                                     
                                                                                
const app = express();                                                          
                                                                                
// Parsers for POST data                                                        
app.use(bodyParser.json());                                                     
app.use(bodyParser.urlencoded({ extended: false }));                              
                                                                                
// Point static path to dist                                                    
app.use(express.static(path.join(__dirname, 'dist')));                          
                                                                                
// Set our api routes                                                           
app.use('/api', api);                                                           
                                                                                
// Catch all other routes and return the index file                             
app.get('*', (req, res) => {                                                    
  res.sendFile(path.join(__dirname, 'dist/index.html'));                        
});                                                                             
                                                                                
/**                                                                             
 * Get port from enviroment and store in Express                                
 */                                                                             
const port = process.env.PORT || '3000';                                        
app.set('port', port);                                                          
                                                                                
/**                                                                             
 * Create HTTP server                                                           
 */                                                                             
const server = http.createServer(app);                                          
                                                                                
/**                                                                             
 * Listen on provided port, on all network inter                                
 */                                                                             
server.listen(port, () => console.log(`API running on localhost:${port}`));     
                                                                                

代码没有什么特别的,都是一些Express的基础;不过,这里用到3个ES6的新特性:

  • const关键词定义常量
  • 回调函数用了箭头形式
  • 最后console.log的字符串输出是反引号中,支持插值引入变量、常量

其实,我上面说是ES6的新特性并不准确,因为这里能够使用完全是因为AngularJS 2.0是基于TypeScript语法的。而ES6的实现上参考了TS,或者说TS率先实现了咯。这部分内容暂不详述啦,大家有兴趣可以参考typescript官网文档以及阮一峰大大的ES6入门书籍

接下来,我们当然是要去创建router中指定的api.js咯。


mkdir -p server/routes && vim $_/api.js

const express = require('express');
const router = express.Router();

/* GET api listing. */
router.get('/', (req, res) => {
  res.send('api works');
});

module.exports = router;

另外,我们将默认路由指向了’dist/index.html’。所以,我们需要使用Angular-cli去打包一次。


ls dist
ng build
ls dist

是的! 这一条指令AngularJS2就会将我们的应用打包到设定好的目录中(.angular-cli.json)。
现在我们开启一下express服务器测试下。


node server.js

浏览器分别访问http://localhost:3000和http://localhost:3000/api, 一个显示的是app works! 另一个则是api works! 如果一切正常,我们就可以进入下一阶段啦。

数据

之前提到,我们这里并不使用MongoDB来进行实际操作。取而代之,我们使用一个叫做jsonplaceholder模型API来模拟一些数据。这在实际开发环境也是非常有实用价值的。对于一个缩短产品开发周期有着至关重要的作用。

因为该博文主要还是讲述Angular 2.0的工作流以及Angular Cli的使用,所以我们选择了现成的虚假数据模型。如果实际开发中,你可以利用JSON Server实现自己项目需要的模拟数据。

首先我们添加一款叫做axios的NPM插件来处理http请求。


yarn add axios

安装完成,我们更新/server/routes/api.js:


 const express = require('express');                                         
 const router = express.Router();                                            
                                                                             
 // declare axios for making http requests                                   
 const axios = require('axios');                                             
 const API = 'https://jsonplaceholder.typicode.com';                         
                                                                             
 // Get api listing                                                          
 router.get('/', (req, res) => {                                             
   res.send('api works');                                                    
 });                                                                         
                                                                             
 // Get all posts                                                            
 router.get('/posts', (req, res) => {                                        
   // Get posts from the mock api                                            
   // This should ideally be replaced with a service that connect to MongoDB 
   axios.get(`${API}/posts`)                                                 
     .then(posts => {                                                        
       res.status(200).json(pots.data);                                      
     })                                                                      
     .catch(error => {                                                       
       res.status(500).send(error);                                          
     });                                                                     
 });                                                                         
                                                                             
 module.exports = router;                                                    

我们重启一下node expressjs http服务器,然后浏览器访问下http://localhost:3000/api/posts. 如果你看到数据乱糟糟的,可以安装chrome或者firefox下的json view相关插件。

到此,我们后端需要的实现基本处理完成啦。我们开始实现AngularJS前端调用部分。

Angular JS 2.0 Component

如果你接触过React,那么对于Component肯定不会陌生咯。没接触过。。。好吧,咋理解呢!讲学术,哥真不擅长。简单说下我自己的理解吧,Component就是通过将一组前台展现代码构建到自定义标签里。那么,你在整个前端页面布局中,可以灵活调用这些components。换言之,我们的页面就是由components拼装的。这样做的好处是什么呢? 解耦,灵活,更容易组织管理以及后续维护复用。总之,都是业界大牛创造的东东,你不用管它是叫components还是parts, 理解概念掌握技术知晓利弊就好。

我们直接使用Angular CLI来产生一个新的component,如下:


ng generate component posts
###输出如下(请原谅我懒得截图)###
installing component
  create src\app\posts\posts.component.css
  create src\app\posts\posts.component.html
  create src\app\posts\posts.component.spec.ts
  create src\app\posts\posts.component.ts
  update src\app\app.module.ts #导入了新的posts.component,以及在NgModule添加了注入
###

添加Route

在AngularJS中添加路由有好几种方式。我这里仅仅是选择了展示的最直接途径,并非最好的实现路径。


vim src/app/app.module.ts


import { BrowserModule } from '@angular/platform-browser';            
import { NgModule } from '@angular/core';                             
import { FormsModule } from '@angular/forms';                         
import { HttpModule } from '@angular/http';                           

import { RouterModule } from '@angular/router';  // 引入RouterModule                                                                    
import { AppComponent } from './app.component';                       
import { PostsComponent } from './posts/posts.component';             
                                                                      
// Define the routes                                                  
const ROUTES = [                                                      
  {                                                                   
    path: '',                                                         
    redirectTo: 'posts',                                              
    pathMatch: 'full'                                                 
  },                                                                  
  {                                                                   
    path: 'posts',                                                    
    component: PostsComponent                                         
  }                                                                   
];                                                                    
                                                                      
@NgModule({                                                           
  declarations: [                                                     
    AppComponent,                                                     
    PostsComponent                                                    
  ],                                                                  
  imports: [                                                          
    BrowserModule,                                                    
    FormsModule,                                                      
    HttpModule,                                                       
    RouterModule.forRoot(ROUTES) // Add routes to the app             
  ],                                                                  
  providers: [],                                                      
  bootstrap: [AppComponent]                                           
})                                                                    
export class AppModule { }                                            

以上Route是将根地址转向到那/posts。

使用Posts Component


vim src/app/app.component.html


<h1>                                 
  {{title}}                          
</h1>                                
<router-outlet></router-outlet>            

测试新建的component


ng build && node server.js #可以将这句添加到项目的package.json的scripts节点,命名为build,之后运行npm build

现在访问http://localhost:3000/,会被跳转到http://localhost:3000/posts,并显示的内容会添加上components。

连接Component和Express API

我们使用service来处理后端json数据的http请求,利用Angular CLI产生服务的如下:


ng generate service posts
###结果如下###
installing service
  create src\app\posts.service.spec.ts
  create src\app\posts.service.ts
  WARNING Service is generated but not provided, it must be provided to be used
###

添加post服务为Providers


// vim src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';       
import { NgModule } from '@angular/core';                        
import { FormsModule } from '@angular/forms';                    
import { HttpModule } from '@angular/http';                      
                                                                 
// Imports modules                                               
import { RouterModule } from '@angular/router';                  
                                                                 
import { AppComponent } from './app.component';                  
import { PostsComponent } from './posts/posts.component';        
                                                                 
import { PostsService } from './posts.service';                  
                                                                 
// Define the routes                                             
const ROUTES = [                                                 
  {                                                              
    path: '',                                                    
    redirectTo: 'posts',                                         
    pathMatch: 'full'                                            
  },                                                             
  {                                                              
    path: 'posts',                                               
    component: PostsComponent                                    
  }                                                              
];                                                               
                                                                 
@NgModule({                                                      
  declarations: [                                                
    AppComponent,                                                
    PostsComponent                                               
  ],                                                             
  imports: [                                                     
    BrowserModule,                                               
    FormsModule,                                                 
    HttpModule,                                                  
    RouterModule.forRoot(ROUTES) // Add routes to the app        
  ],                                                             
  providers: [PostsService],  // Add the posts service           
  bootstrap: [AppComponent]                                      
})                                                               
export class AppModule { }                                       
                                                                 

posts服务中添加http请求

现在我们将http请求添加到PostsService中去。


// vim src/app/posts.service.ts
import { Injectable } from '@angular/core';       
import { Http } from '@angular/http';             
import 'rxjs/add/operator/map';                   
                                                  
@Injectable()                                     
export class PostsService {                       
                                                  
  constructor(private http: Http) { }             
                                                  
  // Get all posts from the API                   
  getAllPosts() {                                 
    return this.http.get('/api/posts')            
      .map(res => res.json());                    
  }                                               
                                                  
}                                                 

将PostsService引入到Posts Component


import { Component, OnInit } from '@angular/core';        
import { PostsService } from '../posts.service';          
                                                          
@Component({                                              
  selector: 'app-posts',                                  
  templateUrl: './posts.component.html',                  
  styleUrls: ['./posts.component.css']                    
})                                                        
export class PostsComponent implements OnInit {           
                                                          
  // instantiate posts to an empty array                  
  posts: any = [];                                        
                                                          
  constructor(private postsService: PostsService) { }     
                                                          
  ngOnInit() {                 // 这个方法将会在component constructor之后立即被调用。                           
    // Retrieve posts from the API                        
    this.postsService.getAllPosts().subscribe(posts => {  
      this.posts = posts;                                 
    });                                                   
  }                                                                                                             
}                                                         

View – 在HTML中输出

我们在postsComponent的html页面来定义输出,其中会牵涉到一些Angular指令,可以参考structural directives! 当然,component本身也是一种带有template的指令。另外,还有一种Attribute指令


<div class="container">                                  
  <div class="row" *ngFor="let post of posts">           
    <div class="card card-block">                        
      <h4 class="card-title">{{ post.title }}</h4>       
      <p class="card-text">{{post.body}}</p>             
      <a href="#" class="card-link">Card link</a>        
      <a href="#" class="card-link">Another link</a>     
    </div>                                               
  </div>                                                 
</div>                                                   

上面使用了bootstrap 4 card中的一些CSS属性,所以我们还需要在src/index.html的head添加bootstrap4的引入。


// vim src/index.html
 <!doctype html>                                                                
 <html>                                                                         
   <head>                                                                       
     <meta charset="utf-8">                                                     
     <title>MeanApp</title>                                                     
     <base href="/">                                                            
                                                                                
     <meta name="viewport" content="width=device-width, initial-scale=1">       
     <link rel="icon" type="image/x-icon" href="favicon.ico">                   
     <link href="https://cdn.bootcss.com/bootstrap/4.0.0-alpha.5/css/bootstrap. 
     css" rel="stylesheet">                                                     
   </head>                                                                      
   <body>                                                                       
     <app-root>Loading...</app-root>                                            
   </body>                                                                      
 </html>                                                                        

到此,一个简单的示例页面已经完成。我们可以重新编译一次。


npm run build

显然,以上示例显示出我们完全可以做到AngularJS 2前端和Express API同步开发。这对于敏捷开发快速迭代都是非常重要的。

以上内容并没有牵涉到MongoDB的实操,如果您对如何在NodeJS/ExpressJS中使用MongoDB完全木有概念,建议您阅读我们的MongoDb之Mongoose入门教程