StuQ《Node.js微信开发》

demo

  • 部署服务器
  • 代码解读
    • node的代码
      • 模型
      • 测试
      • api
    • 前端weui代码
      • index.html
      • example.js
      • 实现tab
      • 绑定实践
  • 微信配置说明
  • 总结

部署服务器

阿里云

ubuntu 14.10 LTS 64位

登录远端服务器

ssh root@ip

创建用户

  # sudo useradd -m -d /home/sang -s /bin/bash -c "the sang user" -U sang
  # passwd sang
  Enter new UNIX password:
  Retype new UNIX password:
  passwd: password updated successfully
  • useradd创建登录用户
  • passwd设置用户登录密码

赋予sudo权限

如果有必要使用sudu权限,请修改

  # sudo vi /etc/sudoers

复制root行改为sang即可

  # User privilege specification
  root    ALL=(ALL:ALL) ALL
  sang    ALL=(ALL:ALL) ALL

切换用户

  # su - sang
  $ ls
  $
  $ pwd
  /home/sang
  $

安装必备软件

安装git

如果上面没有复制给sang账户sudo权限,请切换到root账户操作

sudo apt-get update
sudo apt-get install git

安装nginx

sudo apt-get install nginx

开机启动

http://www.jianshu.com/p/2e03255cfabb)

sudo apt-get install sysv-rc-conf
sudo sysv-rc-conf nginx on

注意:Ubuntu系统中服务的运行级别

  • 0 系统停机状态
  • 1 单用户或系统维护状态
  • 2~5 多用户状态
  • 6 重新启动

准备工作目录

mkdir -p workspace/github
cd workspace/github

安装nodejs

安装nvm

$ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.0/install.sh | bash
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  7766  100  7766    0     0  28614      0 --:--:-- --:--:-- --:--:-- 28656
=> Downloading nvm as script to '/home/sang/.nvm'

=> Appending source string to /home/sang/.bashrc
=> Close and reopen your terminal to start using nvm
$ source ~/.bashrc
$ nvm

Node Version Manager

安装nodejs lts版本

$ nvm install 4
Downloading https://nodejs.org/dist/v4.3.2/node-v4.3.2-linux-x64.tar.xz...
######################################################################## 100.0%
Now using node v4.3.2 (npm v2.14.12)
Creating default alias: default -> 4 (-> v4.3.2)
$ node -v
v4.3.2

使之成为默认

$  nvm alias default 4.3
default -> 4.3 (-> v4.3.2)

确认npm版本

$ npm -v
2.14.12

只要大于2.9.1即可,如不是,请npm i -g npm@2.9.1

安装nrm

$ npm i -g nrm
npm WARN deprecated npmconf@0.1.16: this package has been reintegrated into npm and is now out of date with respect to npm
/home/sang/.nvm/versions/node/v4.3.2/bin/nrm -> /home/sang/.nvm/versions/node/v4.3.2/lib/node_modules/nrm/cli.js
nrm@0.3.0 /home/sang/.nvm/versions/node/v4.3.2/lib/node_modules/nrm
├── ini@1.3.4
├── only@0.0.2
├── extend@1.3.0
├── async@0.7.0
├── open@0.0.5
├── commander@2.9.0 (graceful-readlink@1.0.1)
├── npmconf@0.1.16 (inherits@2.0.1, osenv@0.0.3, ini@1.1.0, semver@2.3.2, mkdirp@0.3.5, once@1.3.3, nopt@2.2.1, config-chain@1.1.10)
├── node-echo@0.0.6 (jistype@0.0.3, mkdirp@0.3.5, coffee-script@1.7.1)
└── request@2.69.0 (aws-sign2@0.6.0, forever-agent@0.6.1, tunnel-agent@0.4.2, oauth-sign@0.8.1, is-typedarray@1.0.0, caseless@0.11.0, stringstream@0.0.5, isstream@0.1.2, json-stringify-safe@5.0.1, extend@3.0.0, tough-cookie@2.2.1, node-uuid@1.4.7, qs@6.0.2, combined-stream@1.0.5, form-data@1.0.0-rc3, mime-types@2.1.10, aws4@1.3.2, hawk@3.1.3, bl@1.0.3, http-signature@1.1.1, har-validator@2.0.6)

测速

$ nrm test

* npm ---- 274ms
  cnpm --- 6868ms
  taobao - 716ms
  edunpm - 5598ms
  eu ----- Fetch Error
  au ----- Fetch Error
  sl ----- 1234ms
  nj ----- 2228ms
  pt ----- Fetch Error

切换源

$ nrm use npm

   Registry has been set to: https://registry.npmjs.org/

部署nodejs应用

基础

  • git clone
  • npm i
  • pm2 start

修改nginx

cat /etc/nginx/sites-enabled/default


upstream backend_nodejs {
    server 127.0.0.1:3019 max_fails=0 fail_timeout=10s;
    #server 127.0.0.1:3001;
    keepalive 512;
}


server {
    listen 80 default_server;
    listen [::]:80 default_server ipv6only=on;

    #root /usr/share/nginx/html;
    root /home/sang/workspace/oschina/base2-wechat-jssdk/public;
    index index.html index.htm;

    # Make site accessible from http://localhost/
        server_name nodeonly.mengxiaoban.cn at35.com;
    client_max_body_size 16M;
      keepalive_timeout 10;

    location / {
        # First attempt to serve request as file, then
        # as directory, then fall back to displaying a 404.
        #try_files $uri $uri/ =404;
        # Uncomment to enable naxsi on this location
        # include /etc/nginx/naxsi.rules

    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_set_header X-NginX-Proxy true;
    proxy_redirect off;
        proxy_next_upstream error timeout http_500 http_502 http_503 http_504;
    proxy_set_header   Connection "";
    proxy_http_version 1.1;
    proxy_pass http://backend_nodejs;
    }
}

注意

  • upstream backend_nodejs定义的代理转发的api地址
  • location /下面的proxy_pass,从upstream里取
  • root下面放的是静态资源,比如express下的public目录

然后重启nginx即可

sudo nginx -s reload

了解MONGODB的部署

  • replset
  • shard

我写的《 mongodb运维之副本集实践》

https://cnodejs.org/topic/5590adbbebf9c92d17e734de

on ubuntu

https://docs.mongodb.org/manual/tutorial/install-mongodb-on-ubuntu/

代码部署

ssh免登陆

git clone git@git.oschina.net:i5ting/wechat-dev-with-nodejs.git
  • 调整nginx
  • pm2

node的代码

模型

  • 目录结构
  • 表间关系

User用户

  username: {// 真实姓名
    type: String
  },
  password       : String,
  unionid       : String,
  openid: {// from weixin openid
    type: String,
    required: true,
    index: {
      unique: true
    }
  },
  nickname      : String,// from weixin 昵称
  sex           : String,// from weixin 性别 0->女 1->男
  language      : String,// from weixin 语言
  city          : String,// from weixin 城市
  province      : String,// from weixin
  country       : String,// from weixin
  headimgurl    : String,// from weixin 头像路径
  privilege     : [],    // from weixin
  created_at    : {
    type: Date,
    "default": Date.now
  }

Course课程

  name: {// 课程名
    type: String
  },
  pic: {// 课程图片
    type: String,
    "default": "/images/logo.png"
  },
  desc: {// 描述
    type: String,
    "default": "这是StuQ的一门在线课程"
  },
  price: {// 价格
    type: Number
  },
  docent: {// 讲师
    type: String
  },
  content: {// 大纲
    type: String
  },
  owner_id: { //user_id
    type: Schema.ObjectId,
    index: true,
    required: true,
    ref: 'User'
  },
  created_at: {
    type: Date,
    "default": Date.now
  }

订单

  desc: {// 订单说明
    type: String
  },
  "user_id": { //user_id
    type: Schema.ObjectId,
    index: true,
    required: true,
    ref: 'User'
  },
  user_name: {// 用户名
    type: String
  },
  "course_id": { //user_id
    type: Schema.ObjectId,
    index: true,
    required: true,
    ref: 'Course'
  },
  course_name: {// 课程名
    type: String
  },
  created_at    : {
    type: Date,
    "default": Date.now
  }

问题

  • 如何查看我的课程?
  • 如果查看我购买的课程?

源码介绍的时候,穿插robo和dash用法

测试

user

var request = require('supertest');
var assert  = require('chai').assert;
var expect  = require('chai').expect;
require('chai').should();

require('../db')

var User = require('../app/models/user')

// 测试代码基本结构
describe('用户User', function(){
    before(function(d) {
    // runs before all tests in this block
    User.remove({"openid":"ss"},function(){
      d()
    })
  })
  after(function(){
    // runs after all tests in this block
    // User.remove({},function(err, user){
    // });
  })
  beforeEach(function(){
    // runs before each test in this block
  })
  afterEach(function(){
    // runs after each test in this block
  })

  describe('#save()', function(){
    this.timeout(30000);
    it('should return stuq when user save', function(done){
      User.create({"username":"stuq","password":"password", "openid":"ss"},function(err, user){
        if(err){
          console.log(err)
          expect(err).to.be.not.null;
          done();
        }

        expect(user.username).to.be.a('string');
        expect(user.username).to.equal('stuq');
        done();
      });
    })
  })
})

用户与课程

var request = require('supertest');
var assert  = require('chai').assert;
var expect  = require('chai').expect;
require('chai').should();

require('../db')

var User = require('../app/models/user')
var Course = require('../app/models/course')
var Order = require('../app/models/order')

var _user;

// 测试代码基本结构
describe('课程Course', function(){
    before(function(done) {
    // runs before all tests in this block
    User.removeAsync({"username":"stuq","password":"password", "openid":"ss"}).then(function(){
      return User.createAsync({"username":"stuq","password":"password", "openid":"ss"})
    }).then(function(user){
      _user = user;
      return  Course.removeAsync({"name":"Node.js微信开发"});
    }).then(function(){
      done();
    });
  })
  after(function(){
    // runs after all tests in this block
    // User.remove({},function(err, user){
    // });
  })
  beforeEach(function(){
    // runs before each test in this block
  })
  afterEach(function(){
    // runs after each test in this block
  })

  describe('#save()', function(){
    it('should return Node.js微信开发 when Course save', function(done){
      Course.create({
        "name":"Node.js微信开发","desc":"stuq在线课程", "docent":"桑世龙", owner_id: _user._id,
        desc:"通过学习Node.js基础和express,微信开发常用库,h5,最后达到学会Node.js开发的目的,该课程以实战为主,深入浅出"
      },function(err, c){
        if(err){
          console.log(err)
          expect(err).to.be.not.null;
          done();
        }

        expect(c.name).to.be.a('string');
        expect(c.name).to.equal('Node.js微信开发');
        done();
      });
    })
  })
})

三个表相关的订单

var request = require('supertest');
var assert  = require('chai').assert;
var expect  = require('chai').expect;
require('chai').should();

require('../db')

var User = require('../app/models/user')
var Order = require('../app/models/order')
var Course = require('../app/models/course')

var _user, _course;

// 测试代码基本结构
describe('订单Order', function(){
    before(function(done) {
    // runs before all tests in this block
    User.removeAsync({"openid":"ss1"}).then(function(){
      return  Course.removeAsync({"name":"Node.js微信开发1"});
    }).then(function(){
      User.create({"username":"stuq1","password":"password", "openid":"ss1"},function(err, user){
        _user = user;
        // console.log(err)
        // console.log(_user)
        return Course.create({"name":"Node.js微信开发1","desc":"stuq在线课程", "docent":"桑世龙", owner_id: _user._id},function(err1, c){
          // console.log(c)
          _course = c;

          done();
        });
      });
    })
  })
  after(function(){

  })
  beforeEach(function(){
    // runs before each test in this block
  })
  afterEach(function(){
    // runs after each test in this block
  })

  describe('#save()', function(){
    it('should return order when order save', function(done){
      Order.create({
        "desc":"a order"
        ,"user_id":_user._id
        , "user_name": _user.username
        ,course_id : _course._id
        ,course_name : _course.name
      },function(err, order){
        if(err){
          console.log(err)
          expect(err).to.be.not.null;
          done();
        }

        expect(order.desc).to.be.a('string');
        expect(order.desc).to.equal('a order');
        done();
      });
    })
  })
})

另外举例runkoa没有ci引发的血案

api

自动挂载路由

var mount         = require('mount-routes');

// simple
mount(app, __dirname + '/app/routes');

示例

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

var User = require('../../models/user')
var Course = require('../../models/course')
var Order = require('../../models/order')

router.get('/', function(req, res, next) {
  Course.find({},function(err, courses){
    res.json({
      status:{
        code:0,
        msg:'sucess'
      },
      data:courses
    });
  });
})

module.exports = router;

测试

curl http://127.0.0.1:3019/api/courses

或者postman

前端weui代码

weui v0.4.x新增了路由和tab等组件,问题还是挺多的

frontend目录随便配,目的就是为了让大家理解前后端分离

路由

定义骨架

    var router = new Router({
        container: '#container',
        enterTimeout: 250,
        leaveTimeout: 250
    });

然后


    // course
    var course = {
        url: '/course',
        className: 'panel',
        render: function () {
            // alert(getQueryStringByName('id'));
            return $('#tpl_course').html();
        },
        bind: function () {
            $('.pay_btn').on('click', function(){
              var id = getQueryStringByName('id');
              pay_h5(id);
            })
        }
    };

    // tabbar
    var tabbar = {
        url: '/home',
        className: 'tabbar',
        render: function () {
            var _t = this;
            setTimeout(function(){
              _t.bind()
            },100)
            return $('#tpl_tabbar').html();
        },
        bind: function () {
          $('.course_list').html(all_courses_html);
          $('.weui_tabbar_content').eq(0).show()
          $('.weui_tabbar_item').on('click', function () {
            $('.weui_tabbar_item').eq($('.weui_tabbar_item').index(this)).addClass('weui_bar_item_on').siblings().removeClass('weui_bar_item_on')
            $('.weui_tabbar_content').eq($('.weui_tabbar_item').index(this)).show().siblings().hide();
          });
        }
    };

    router.push(tabbar)
        .push(course)
        .setDefault('/home')
        .init();

example.js

根据query的参数名,取值

function getQueryStringByName(name){
  var result = location.hash.match(new RegExp("[\?\&]" + name+ "=([^\&]+)","i"));
  if(result == null || result.length < 1){
    return "";
  }
  return result[1];
}

ajax

var all_courses_html;
$.getJSON('/api/courses',function(res){
  // alert(res)
  var item_html = ""
  for(var i in res.data){
    console.log(i);
    var course = res.data[i];

    var item = "  <a href='#/course?id=" + course._id + "' class='weui_media_box weui_media_appmsg'>"
              +"    <div class='weui_media_hd'>"
              +"        <img class='weui_media_appmsg_thumb' src='" + course.pic + "' alt=''>"
              +"    </div>"
              +"    <div class='weui_media_bd'>"
              +"        <h4 class='weui_media_title'>" + course.name + "</h4>"
              +"        <p class='weui_media_desc'>" + course.desc + "</p>"
              +"    </div>"
              +"  </a>"

    item_html += item;
  }

  all_courses_html = "<div class='weui_panel_bd'> " + item_html + " </div><a class='weui_panel_ft' href='javascript:void(0);'>查看更多</a>"
  // alert(all);

  $('.course_list').html(all_courses_html);
})

这样首页是ok了,但是里面呢?

// tabbar
var tabbar = {
    url: '/home',
    className: 'tabbar',
    render: function () {
        var _t = this;
        setTimeout(function(){
          _t.bind()
        },100)
        return $('#tpl_tabbar').html();
    },
    bind: function () {
      $('.course_list').html(all_courses_html);
      $('.weui_tabbar_content').eq(0).show()
      $('.weui_tabbar_item').on('click', function () {
        $('.weui_tabbar_item').eq($('.weui_tabbar_item').index(this)).addClass('weui_bar_item_on').siblings().removeClass('weui_bar_item_on')
        $('.weui_tabbar_content').eq($('.weui_tabbar_item').index(this)).show().siblings().hide();
      });
    }
};
  • setTimeout
  • $('.course_list').html(all_courses_html);

实现tab

    <script type="text/html" id="tpl_tabbar">
        <div class="weui_tab">
            <div class="weui_tab_bd">
              <div class="weui_tabbar_content">
                <div class="hd">
                    <h1 class="page_title">StuQ课程</h1>
                    <p class="page_desc"> 提升你的IT职业技能最好的在线学习平台</p>
                </div>

                <div class="weui_panel weui_panel_access">
                    <div class="weui_panel_hd">课程列表</div>
                    <div class='course_list'></div>
                </div>

              </div>

              <div class="weui_tabbar_content">

              </div>

            </div>
            <div class="weui_tabbar">
                <a href="javascript:;" class="weui_tabbar_item">
                    <div class="weui_tabbar_icon">
                        <img src="./images/icon_nav_article.png" alt="">
                    </div>
                    <p class="weui_tabbar_label">课程</p>
                </a>
                <a href="javascript:;" class="weui_tabbar_item">
                    <div class="weui_tabbar_icon">
                        <img src="./images/icon_nav_cell.png" alt="">
                    </div>
                    <p class="weui_tabbar_label">我</p>
                </a>
            </div>
        </div>
    </script>

留意weui_tabbar_content

$('.weui_tabbar_content').eq(0).show()
$('.weui_tabbar_item').on('click', function () {
  $('.weui_tabbar_item').eq($('.weui_tabbar_item').index(this)).addClass('weui_bar_item_on').siblings().removeClass('weui_bar_item_on')
  $('.weui_tabbar_content').eq($('.weui_tabbar_item').index(this)).show().siblings().hide();
});

参见https://github.com/i5ting/i5ting.jquery.tab

绑定事件

  • on
  • live
  • bind

微信配置说明

Stuq微信开发测试账号信息

公众平台

https://mp.weixin.qq.com/

  • 微信名:StuQ课课程测试账号
  • 微信账户:zhuohang111@163.com
  • 密码:duanmu5061656
{
  "app_id": "wx1207227ce79d76c3",
  "app_secret": "b1693148b1b26318c9d8224a17ff0ee1"
}

微信支付

https://pay.weixin.qq.com

微信支付商户号 1299809901 商户平台登录帐号 1299809901@1299809901 商户平台登录密码 000090

安装操作证书

  • 安装安全控件
  • 安装操作证书(请事先联系海角,获取对应的短信验证码,输入短信验证码和验证码就自动安装完成了)

然后点击api安全

  • 有手机号授权找海角
  • 有域名ip地址指定找i5ting

总结

关于StuQ

软件公司招聘需要巨大,但入门难,技术发展过快(指数),而人的曲线成长较慢,现在的慕客形式又过于老旧,呆板,少互动,所以社群时代的在线教育,一定是专业的、互动的、深入浅出、共同成长,这些正是StuQ最擅长的方面,我个人特别看好StuQ这个品牌,真心推荐,如果不是股份绑定,我一定会加入StuQ

我的近况

  • 新书《更了不起的 Node 4:将下一代 Web 框架 Koa 进行到底》,预计2到3个月就能和大家见面
  • StuQ-Koa在线课程,准备招生

写给大家

  • 学不学在自己,会不会也在自己
  • 少抱怨、多思考、未来更美好
  • 闲时要有吃紧的心思
  • 一万个小时就能成为专家,难在坚持
  • 掌握了学习方法,以后职业不愁