NodeJS+Angular+MongodbWeb開發

1. 項目簡介

??本項目提供瞭實現一個購物車的實際例子,允許你添加、刪除物品,經歷結賬過程,查看訂單等,這個例子可以讓你瞭解在結賬過程中,如何利用AngularJS在視圖之間進行切換。

??本項目創建的購物車提供瞭所需的大部分功能,但是省略瞭諸如身份驗證和錯誤處理等細節,隻是單純地設計到與購物車有關的東西,其餘一概省略。

?例如本例子采用一個userid(用戶ID)被硬編碼為customerA的用戶,所以不太可能直接用於生產,除非你在此基礎上花費不小的功夫繼續擴展。


2. 基本流程

本實例的一般邏輯流程如下:

這裡寫圖片描述


3. 用到的庫

express:作為項目的主web服務器 body-parser:為post請求提供JSON正文支持 ejs:用於呈現HTML模板 mongodb:用於訪問MongoDB數據庫 mongoose:用於提供結構化的數據模型 AngularJs庫


4. 項目的目錄結構


5. 定義模式

需要一個顧客模型作為購物車的容器,需要被放入購物車的產品,結賬時需要賬單信息和發貨信息,下訂單時,需要存儲訂單。
基於以上,設計模式。

(1)定義地址模式
地址模式是通用的,既可以用在發貨信息,也可以用在賬單信息,包含標準地址信息。

(2)定義賬單模式
包含標準信用卡數據(信用卡類型、賬戶名、賬戶號、過期時間、地址)

(3)定義產品模式
包含name、imagefile、description、price、instock(存貨數量)

(4)定義數量模式
存儲產品的數量

(5)定義訂單模式
包含訂購的五品、發貨信息、賬單信息

(6)定義顧客模式
包括標準的身份信息
//cart_model.js

var mongoose=require('mongoose');
var Schema=mongoose.Schema;

//定義地址模式
var AddressSchema=new Schema({
    name:String,
    address:String,
    city:String,
    state:String,
    zip:String
},{_id:false});

mongoose.model('Address',AddressSchema);

//定義賬單模式
var BillingSchema=new Schema({
    cardtype:{type:String,enum:['Visa','MasterCard','Amex']},
    name:String,
    number:String,
    expiremonth:Number,
    expireyear:Number,
    address:[AddressSchema]
},{_id:false});

mongoose.model('Billing',BillingSchema);

//定義產品模式
var ProductSchema=new Schema({
    name:String,
    imagefile:String,
    description:String,
    price:Number,
    instock:Number
});

mongoose.model('Product',ProductSchema);

//定義產品數量模式
var ProductQuantitySchema=new Schema({
    quantity:Number,
    product:[ProductSchema]
},{_id:false});

mongoose.model('ProductQuantity',ProductQuantitySchema);

//定義訂單模式
var OrderSchema=new Schema({
    userid:String,
    items:[ProductQuantitySchema],
    shipping:[AddressSchema],
    billing:[BillingSchema],
    status:{type:Date,default:Date.now}
});

mongoose.model('Order',OrderSchema);

//定義顧客模式
var Customer=new Schema({
    userid:{type:String,unique:true,required:true},
    shipping:[AddressSchema],
    billing:[BillingSchema],
    cart:[ProductQuantitySchema]
});

mongoose.model('Customer',Customer);

6. 創建購物車服務器

代碼很簡單,如下:

var express=require('express');
var bodyParser=require('body-parser');
var mongoose=require('mongoose');
var url='mongodb://localhost:27017/cart';
var db=mongoose.connect(url);
mongoose.Promise = global.Promise;
require('./models/cart_model.js');

var app=express();
app.engine('.html',require('ejs').__express);
app.set('views',__dirname+'/views');
app.set('view engine','html');

app.use(bodyParser.urlencoded({extended:false}));
require('./cart_routes')(app);

app.listen(3000);

console.log('App running...');

7. 實現路由,以支持產品、購物車和訂單請求

商品、訂單和顧客的路由

//cart_routes.js

var express=require('express');
module.exports=function(app){
    //加載控制器
    var customers=require('./controllers/customers_controller');
    var products=require('./controllers/product_controller');
    var orders=require('./controllers/orders_controller');

    //配置中間件
    app.use('/static',express.static('./static'));
    app.use('/images',express.static('./images'));
    app.use('/lib',express.static('./lib'));

    //配置路由
    app.get('/',function(req,res){
        res.render('shopping');
    });

    //商品
    app.get('/products/get',products.getProducts);

    //訂單
    app.get('/orders/get',orders.getOrders);
    app.post('/orders/add',orders.addOrder);

    //顧客
    app.get('customers/get',customers.getCustomers);
    app.post('customers/update/shipping',customers.updateShipping);
    app.post('customers/update/billing',customers.updateBilling);
    app.post('customers/update/cart',customers.updateCart);
};

8. 實現基於模型的控制器路由

(1)實現產品模型控制器

getProduct():根據在查詢中包含的productId 查找單個產品

getProducts():查找所有產品

如果查詢成功,則返回JSON字符串,請求時便則返回404錯誤。

//product_controller.js

var mongoose=require('mongoose');
var Product=mongoose.model('Product');


exports.getProduct=function(req,res){
    Product.findOne({_id:req.query.productId})
        .exec(function(err,product){
            if(err){
                throw err;
            }
            if(!product){
                res.json(404,{msg:'Product Not Found!'});
            }else{
                res.json(product);
            }
        });
};

exports.getProducts=function(req,res){
    Product.find()
        .exec(function(err,products){
            if(err){
                throw err;
            }
            if(!products){
                res.json(404,{msg:'Products Not Found!'});
            }else{
                res.json(products);
            }
        });
};

(2)實現訂單模型控制器

getOrder():根據查詢中包含的orderId 查找一個訂單

getOrders():查找屬於當前用戶的所有訂單

addOrder():通過獲取POST請求的 updateShipping updateBilling OrderItems 三個參數建立一個新的Order對象。
??????如果訂單成功保存,則將購物車cart清空。

在這個例子中, userid是硬編碼的customerA。

//orders_controller.js

var mongoose=require('mongoose');
var Customer=mongoose.model('Customer');
var Order=mongoose.model('Order');
var Address=mongoose.model('Address');
var Billing=mongoose.model('Billing');


//根據訂單編號查找訂單order
exports.getOrder=function(req,res){
    Order.findOne({_id:req.query.orderId})
        .exec(function(err,order){
            if(err){
                throw err;
            }
            if(!order){
                res.json(404,{msg:'Order Not Found!'});
            }else{
                res.json(order);
            }
        });
};

//查找當前用戶名下所屬的所有訂單orders
exports.getOrders=function(req,res){
    Order.find({userid:'customerA'})
        .exec(function(err,orders){
            if(err){
                throw err;
            }
            if(!orders){
                res.json(404,{msg:'Orders Not Found!'});
            }else{
                res.json(orders);
            }
        });
};

//
exports.getOrder=function(req,res){
    Order.findOne({_id:req.query.orderId})
        .exec(function(err,order){
            if(err){
                throw err;
            }
            if(!order){
                res.json(404,{msg:'Order Not Found!'});
            }else{
                res.json(order);
            }
        });
};

//添加訂單信息
exports.addOrder=function(req,res){
    var orderShipping=new Address(req.body.updateShipping); 
    var orderBilling=new Billing(req.body.updateBilling);   
    var orderItems=req.body.orderItems;

    var newOrder=new Order({
        userid:'customerA',
        items:orderItems,
        shipping:orderShipping,
        billing:orderBilling,
    });

    newOrder.save(function(err,results){
        if(err){
            res.json(500,'Failed to save Order!');
        }else{
            //保存到訂單成功後,清空購物車
            Customer.update({userid:'customerA'},{$set:{cart:[]}})
                .exec(function(err,results){
                    if(err||results<1){
                        res.json(404,{msg:'Failed to update Cart!'});
                    }else{
                        res.json({msg:'Order Saved!'});
                    }
                });
        }
    });
};

(3)實現顧客模型控制器

getCustomer():查找當前顧客的有關信息

updateShipping() 通過Post請求的updateShipping參數創建新的Address對象,然後使用一個update() 方法來使用新的發貨數據來更新Customer 對象

updateBilling():通過Post請求的updateBilling參數創建新的Billing對象,然後使用一個update() 方法來使用新的賬單數據來更新Customer 對象

updateCart()通過Post請求的updateCart參數來更新Customer對象的cart字段。

//customers_controller.js

var mongoose=require('mongoose');
var Customer=mongoose.model('Customer');
var Address=mongoose.model('Address');
var Billing=mongoose.model('Billing');

//查找當前顧客的有關信息
exports.getCustomer=function(req,res){
    Customer.findOne({userid:'customerA'})
        .exec(function(err,customer){
            if(err){
                throw err;
            }
            if(!customer){
                res.json(404,{msg:'Customer Not Found!'});
            }else{
                res.json(customer);
            }
        });
};

//更新發貨信息
exports.updateShipping=function(req,res){
    var newShipping=new Address(req.body.updateShipping);
    Customer.update({userid:'customerA'},{$set:{shipping:[newShipping.toObject()]}})
        .exec(function(err,results){
            if(err||results<1){
                res.json(404,{msg:'Failed to update Shipping!'});
            }else{
                res.json({msg:'Customer Shipping Updated!'});
            }
        });
};

//更新支付信息
exports.updateBilling=function(req,res){
    //你可以在此驗證信用卡信息,並在信用卡無效時取消結賬
    var newBilling=new Billing(req.body.updateBilling);
    Customer.update({userid:'customerA'},{$set:{billing:[newBilling.toObject()]}})
        .exec(function(err,results){
            if(err||results<1){
                res.json(404,{msg:'Failed to update Billing!'});
            }else{
                res.json({msg:'Customer Billing Updated!'});
            }
        });
};

//更新購物車
exports.updateCart=function(req,res){
    Customer.update({userid:'customerA'},{$set:{cart:req.body.updateCart}})
        .exec(function(err,results){
            if(err||results<1){
                res.json(404,{msg:'Failed to update Cart!'});
            }else{
                res.json({msg:'Customer Cart Updated!'});
            }
        });
};

9. 實現購物車和結賬視圖

shopping.html主視圖以及購物車(cart)、發貨(shipping)、賬單(billing)、復核(review)和訂單(orders)頁面的不同的局部視圖。

(1) 實現購物視圖

該視圖是購物應用程序的主視圖,註冊瞭Angular應用程序myapp,並通過shoppingController 控制器進行控制。




My Store

 

<script src=”/static/js/cart_app.js”></script><script src=”/static/js/angular.min.js”></script>

效果如下:

這裡寫圖片描述

(2) 實現所有產品列表視圖

向用戶提供可供選擇的產品列表
使用productsController控制器上的ng-repeat 列出產品
當點擊圖片元素時,setProduct()函數在控制器中被調用,該函數設置當前$scope.product值,並把$scope.content值更改為product.html



{{product.name}} {{product.price | currency}}

(3) 實現產品詳細頁面視圖

這裡使用訪問$scope.product 值的AngularJS表達式來顯示產品信息,Add to Cart 按鈕把當前產品product._id 發送到控制器的addToCart() 函數。

//product.html

{{product.name}}

{{product.description}}

{{product.price | currency}}

{{product.instock}}

Add To Cart

(4) 實現購物車視圖

一旦用戶單擊瞭Add To Cart 按鈕,物品就被添加到購物車中,並且視圖更改為購物車視圖。



(5) 實現發貨視圖

當用戶單擊購物車中的Checkout按鈕時,就顯示發貨視圖,允許用戶在此輸入發貨信息

(6) 實現賬單視圖

當用戶在發貨視圖單擊Continue to Billing 按鈕時,顯示賬單視圖,允許用戶輸入賬單信息。



(7) 實現復核視圖

當用戶單擊賬單視圖中的Verify Billing按鈕時,顯示復核按鈕,用戶可以在此查看訂單,包括發貨和賬單信息。
當顧客點擊Make Purchase按鈕時,這些信息被發送到服務器,並創建一個新的訂單對象,該視圖切換到訂單視圖。



(8) 實現訂單視圖

當訂單完成時,用戶將看到訂單視圖,顯示用戶完成的購買。




10. 實現AngularJS模塊和控制器

完成視圖後,你需要實現AngularJS控制器代碼來支持它們。

可以分解成以下步驟:

初始化購物車作用域
$scope.months 和 $scope.years 數組填充信用卡表單,$scope.content 決定瞭在視圖中呈現哪個AngularJS部件,它被初始化為products.html,這樣用戶就可以開始購物。

實現輔助函數
setContent() 函數設置設置$scope.content值,用以改變視圖
cartTotal() 函數遍歷用戶購物車中的產品,更新$scope.shipping,並返回一個隨後在購物車和復核視圖中使用的金額。

將物品添加到購物車
當用戶點擊Add To Cart按鈕時,調用addToCart()函數,此函數首先遍歷customer.cart中的物品,如果發現已經存在,則增加此物品數量,否則將該物品添加到customer.cart數組
一旦$scope.customer被更新,將對/customer/update/cart路由發出一個$http POST請求,來更新購物車,通過這種方式,持久化購物車,即使用戶關閉瀏覽器或者離開購物頁面也將存在,如果成功該視圖切換到cart.html頁面,否則出現一個警告窗口。

從購物車刪除商品
當用戶單擊刪除Remove按鈕時,調用deleteFromCart() 函數,遍歷在customer.cart中的物品,如果找到則采用array.slice(index,1)方法數組中刪除該物品。
一旦物品被從$scope.customer.cart刪除,對/customer/update/cart路由執行一個$http POST請求,來更新購物車,通過這種方式,持久化購物車,即使用戶關閉瀏覽器或者離開購物頁面也將存在,如果成功該視圖切換到cart.html頁面,否則出現一個警告窗口。

結賬
當用戶在購物車視圖單擊Checkout按鈕時,調用checkout() 函數,發送帶有{updatedCart:$scope.customer.cart} 參數的$http POST請求來更新購物車。

設置發貨信息
當用戶在購物車視圖單擊Continue to Billing按鈕時,調用setShipping() 函數,對/customer/update/shipping路由執行一個$http POST請求,在post方法中包括參數{updatedShipping:$scopr.customer.shiiping[0]},如果請求成功,切換到billing.html視圖,否則出現警告窗口。

驗證賬單
當用戶在發貨視圖單擊Verify Billing按鈕時,調用verifyBilling() 函數,信用卡信息可以在這一刻在服務器上進行驗證,對/customer/update/billing路由執行一個$http POST請求,如果請求成功,切換到review.html視圖,否則出現警告窗口。

執行購買
當用戶在賬單視圖單擊Make Purchase按鈕時,調用makePurchase() 函數,對/orders/add路由執行一個$http POST請求,在post方法中包括orderBilling orderShipping orderItems 參數,如果請求成功,則清空購物車,$scope.customer.cart被初始化為[],新的Order文檔將會已經在數據庫中創建,因此執行另一個請求,執行/order/get 路由,獲取訂單的完整列表,然後切換到orders.html視圖。

//cart_app.js

var app = angular.module('myApp', []);
app.controller('shoppingController', ['$scope', '$http', '$window', 
                              function($scope, $http, $window) {
    $scope.months = [1,2,3,4,5,6,7,8,9,10,11,12];
    $scope.years = [2014,2015,2016,2017,2018,2019,2020];
    $scope.content = '/static/products.html';
    $http.get('/products/get')
     .success(function(data, status, headers, config) {
        $scope.products = data;
        $scope.product = data[0];
      })
      .error(function(data, status, headers, config) {
        $scope.products = [];
      });
    $http.get('/customers/get')
     .success(function(data, status, headers, config) {
       $scope.customer = data;
      })
     .error(function(data, status, headers, config) {
       $scope.customer = [];
     });
    $http.get('/orders/get')
    .success(function(data, status, headers, config) {
       $scope.orders = data;
     })
     .error(function(data, status, headers, config) {
       $scope.orders = [];
     });
    $scope.setContent = function(filename){
      $scope.content = '/static/'+ filename;
    };
    $scope.setProduct = function(productId){
      $scope.product = this.product;
      $scope.content = '/static/product.html';
    };
    $scope.cartTotal = function(){
      var total = 0;
      for(var i=0; i<$scope.customer.cart.length; i++){
        var item = $scope.customer.cart[i];
        total += item.quantity * item.product[0].price;
      }
      $scope.shipping = total*.05;
      return total+$scope.shipping;
    };
    $scope.addToCart = function(productId){
      var found = false;
      for(var i=0; i<$scope.customer.cart.length; i++){
        var item = $scope.customer.cart[i];
        if (item.product[0]._id == productId){
          item.quantity += 1;
          found = true;
        }
      }
      if (!found){
        $scope.customer.cart.push({quantity: 1, 
                                   product: [this.product]});
      }
      $http.post('/customers/update/cart', 
                 { updatedCart: $scope.customer.cart })
       .success(function(data, status, headers, config) {
         $scope.content = '/static/cart.html';
       })
       .error(function(data, status, headers, config) {
         $window.alert(data);
       });
    };
    $scope.deleteFromCart = function(productId){
      for(var i=0; i<$scope.customer.cart.length; i++){
        var item = $scope.customer.cart[i];
        if (item.product[0]._id == productId){
          $scope.customer.cart.splice(i,1);
          break;
        }
      }
      $http.post('/customers/update/cart', 
                 { updatedCart: $scope.customer.cart })
       .success(function(data, status, headers, config) {
         $scope.content = '/static/cart.html';
       })
       .error(function(data, status, headers, config) {
         $window.alert(data);
       });
    };
    $scope.checkout = function(){
      $http.post('/customers/update/cart', 
                 { updatedCart: $scope.customer.cart })
       .success(function(data, status, headers, config) {
         $scope.content = '/static/shipping.html';
       })
       .error(function(data, status, headers, config) {
         $window.alert(data);
       });
    };
    $scope.setShipping = function(){
      $http.post('/customers/update/shipping', 
          { updatedShipping :$scope.customer.shipping[0] })
        .success(function(data, status, headers, config) {
          $scope.content = '/static/billing.html';
        })
        .error(function(data, status, headers, config) {
          $window.alert(data);
        });
    };
    $scope.verifyBilling = function(ccv){
      $scope.ccv = ccv;
      $http.post('/customers/update/billing', 
          { updatedBilling: $scope.customer.billing[0], ccv: ccv})
        .success(function(data, status, headers, config) {
          $scope.content = '/static/review.html';
        })
        .error(function(data, status, headers, config) {
          $window.alert(data);
        });
    };
    $scope.makePurchase = function(){
      $http.post('/orders/add', 
          { orderBilling: $scope.customer.billing[0],
            orderShipping: $scope.customer.shipping[0],
            orderItems: $scope.customer.cart })
        .success(function(data, status, headers, config) {
          $scope.customer.cart = [];
          $http.get('/orders/get')
          .success(function(data, status, headers, config) {
             $scope.orders = data;
             $scope.content = '/static/orders.html';
           })
           .error(function(data, status, headers, config) {
             $scope.orders = [];
           });
        })
        .error(function(data, status, headers, config) {
          $window.alert(data);
        });
    };
  }]);

11. 初始化應用程序

本程序已經完成 ,你需要在數據庫中創建初始的Customer Order Product 文檔,有幾種不同的方法可以做到這一點,以下是使用一個基本的NodeJs腳本完成。

首先執行清理,清楚顧客、訂單和產品的集合,然後創建一個Customer 文檔和一個Order 文檔,之後增加瞭一些Product 文檔,並且向Customer 文檔的購物車和Order 文檔的物品中添加瞭Product 文檔。

//cart_init.js

var mongoose = require('mongoose');
var db = mongoose.connect('mongodb://localhost:27017/cart');
require('./models/cart_model.js');
var Address = mongoose.model('Address');
var Billing = mongoose.model('Billing');
var Product = mongoose.model('Product');
var ProductQuantity = mongoose.model('ProductQuantity');
var Order = mongoose.model('Order');
var Customer = mongoose.model('Customer');
function addProduct(customer, order, name, imagefile, 
                    price, description, instock){
  var product = new Product({name:name, imagefile:imagefile, 
                             price:price, description:description, 
                             instock:instock});
  product.save(function(err, results){
    order.items.push(new ProductQuantity({quantity: 1, 
                                          product: [product]}));
    order.save();
    customer.save();
    console.log("Product " + name + " Saved.");
  });
}
Product.remove().exec(function(){
  Order.remove().exec(function(){
    Customer.remove().exec(function(){
      var shipping = new Address({
        name: 'Customer A',
        address: 'Somewhere',
        city: 'My Town',
        state: 'CA',
        zip: '55555'
      });
      var billing = new Billing({
        cardtype: 'Visa',
        name: 'Customer A',
        number: '1234567890',
        expiremonth: 1,
        expireyear: 2020,
        address: shipping
      });
      var customer = new Customer({
        userid: 'customerA',
        shipping: shipping,
        billing: billing,
        cart: []
      });
      customer.save(function(err, result){
        var order = new Order({
          userid: customer.userid,
          items: [],
          shipping: customer.shipping,
          billing: customer.billing
        });
        order.save(function(err, result){
          addProduct(customer, order, 'Delicate Arch Print', 
              'arch.jpg', 12.34, 
              'View of the breathtaking Delicate Arch in Utah', 
              Math.floor((Math.random()*10)+1));
          addProduct(customer, order, 'Volcano Print', 
              'volcano.jpg', 45.45, 
              'View of a tropical lake backset by a volcano', 
              Math.floor((Math.random()*10)+1));
          addProduct(customer, order, 'Tikal Structure Print', 
              'pyramid.jpg', 38.52, 
              'Look at the amazing architecture of early America.', 
              Math.floor((Math.random()*10)+1));
          addProduct(customer, order, 'Glacial Lake Print', 
              'lake.jpg', 77.45, 
              'Vivid color, crystal clear water from glacial runoff.', 
              Math.floor((Math.random()*10)+1));
        });
      });      
    });
  });
});

發佈留言