如何用angularjs 构建脚手架构建管理后台

3448人阅读
ng+bootstrap可以做出很漂亮的管理系统出来,可以付费购买,下文会提供一个免费的,要讲解如何从0到1把ng前端结构搭出来是很漫长的教程,本文仅仅介绍一下这个免费后台模版的结构,然后重点讲解如何改写这个结构。
开始阅读之前,假设读者已经ng入门并且对于,有一定了解。
下载下来以后我们可以挂载到IIS里面看看这个模版的运行效果
1) 后台结构
其中我们需要关注的如下:
index.html:
js: 存放所有业务逻辑代码
js/app.js:定义ng需要加载的模块
js/main.js:定义ng的全局配置信息
js/config.router.js:ng的路由器
tpl:存放所有页面模版
tpl/blocks:页面框架(头、尾、侧边栏…)
vendor:存放所有第三方模块
2) 改写结构,接管路由
结构简图如下
这里接管的原则是尽量不改动原始结构,首先在根目录创建我们自己的目录结构.
& mkdir admin
& mkdir admin\blocks
& copy js\main.js admin\
编写页面框架模版,我们也可以直接从tpl目录复制过来再做修改。简单起见,我们在模版里面直接使用了中文,这样会导致页面乱码,解决方法:html文件用utf-8编码格式存储。
class="aside-wrap"&
class="navi-wrap"&
ui-nav class="navi" ng-include="'admin/blocks/nav.html'"&&
class="navbar-header {{app.settings.navbarHeaderColor}}&
class="pull-right visible-xs dk" ui-toggle-class="show" data-target=".navbar-collapse"&
class="glyphicon glyphicon-cog"&&
class="pull-right visible-xs" ui-toggle-class="off-screen" data-target=".app-aside" ui-scroll="app"&
class="glyphicon glyphicon-align-justify"&&
href="#/" class="navbar-brand text-lt"&
class="fa fa-btc"&&
src="img/logo.png" alt="." class="hide"&
class="hidden-folded m-l-xs"&{{app.name}}&
class="collapse pos-rlt navbar-collapse box-shadow {{app.settings.navbarCollapseColor}}&
class="nav navbar-nav hidden-xs"&
href class="btn no-shadow navbar-btn" ng-click="app.settings.asideFolded = !app.settings.asideFolded"&
class="fa {{app.settings.asideFolded ? 'fa-indent' : 'fa-dedent'}}&&
class="nav navbar-nav navbar-right"&
class="hidden-xs"&
ui-fullscreen&&
class="dropdown" dropdown&
href class="dropdown-toggle clear" dropdown-toggle&
class="thumb-sm avatar pull-right m-t-n-sm m-b-n-sm m-l-sm"&
src="img/a0.jpg" alt="..."&
class="on md b-white bottom"&&
class="hidden-sm hidden-md"&&
class="caret"&&
class="dropdown-menu animated fadeInRight w"&
href="#"&Logout&
class="nav"&
class="hidden-folded padder m-t m-b-sm text-muted text-xs"&
translate="导航"&导航&
data-ng-include=" 'admin/blocks/header.html' " class="app-header navbar"&
data-ng-include=" 'admin/blocks/aside.html' " class="app-aside hidden-xs {{app.settings.asideColor}}&
class="app-content"&
ui-butterbar&&
href class="off-screen-toggle hide" ui-toggle-class="off-screen" data-target=".app-aside" &&
ncy-breadcrumb&&
class="app-content-body fade-in-up" ui-view&&
class="app-footer wrapper b-t bg-light"&
class="pull-right"&{{app.version}}
href ui-scroll="app" class="m-l-sm text-muted"& class="fa fa-long-arrow-up"&&&&
& 2016 Copyright.
data-ng-include=" 'tpl/blocks/settings.html' " class="settings panel panel-default"&
给我们的首页创建一个空白模版admin/dashboard.html
写我们新的路由
'use strict';
function ($rootScope,
$stateParams) {
$rootScope.$state = $state;
$rootScope.$stateParams = $stateParams;
function ($stateProvider,
$urlRouterProvider) {
$urlRouterProvider
.otherwise('/app/dashboard');
$stateProvider
.state('app', {
abstract: true,
url: '/app',
templateUrl: 'admin/app.html',
.state('app.dashboard', {
url: '/dashboard',
templateUrl: 'admin/dashboard.html',
修改入口,注释或删除掉对原config.router.js、main.js的引用,换成我们的控制接管
src="admin/router.js"&&
src="admin/main.js"&&
到此我们就得到了一套可自由扩展的前端框架
3) 接下来我们基于BasicAuth加入系统的用户验证功能。
这里我们采用按功能模块来建立子目录,区别于原模版框架(原框架是按文件类型区分子目录,例如脚本放在js/里面,模版放在tpl里面)。
首先启动Restful服务器,然后我们为服务器配置一个全局变量,代码里面的host需要修改成服务器真实地址
// admin/main.js
$scope.app = {
host: "http://172.17.9.92:8000",
name: 'Angulr',
创建认证功能模块目录auth
& mkdir admin\auth\
编写controller(控制器)
app.controller('LoadingController',function($scope,$resource,$state){
var $com = $resource($scope.app.host + "/auth/info/?");
$com.get(function(){
$state.go('app.dashboard');
},function(){
$state.go('auth.login');
app.controller('LoginController',function($scope,$state,$http,$resource,Base64){
$scope.login = function(){
$scope.authError = ""
var authdata = Base64.encode($scope.user.username + ':' + $scope.user.password);
$http.mon['Authorization'] = 'Basic ' +
var $com = $resource($scope.app.host + "/auth/info/?");
$com.get(function(){
$state.go('app.dashboard');
},function(){
$scope.authError = "服务器登录错误"
app.factory('Base64',function(){
var keyStr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/=';
encode: function (input) {
var output = "";
var chr1, chr2, chr3 = "";
var enc1, enc2, enc3, enc4 = "";
var i = 0;
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 && 2;
enc2 = ((chr1 & 3) && 4) | (chr2 && 4);
enc3 = ((chr2 & 15) && 2) | (chr3 && 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
output = output +
keyStr.charAt(enc1) +
keyStr.charAt(enc2) +
keyStr.charAt(enc3) +
keyStr.charAt(enc4);
chr1 = chr2 = chr3 = "";
enc1 = enc2 = enc3 = enc4 = "";
} while (i & input.length);
decode: function (input) {
var output = "";
var chr1, chr2, chr3 = "";
var enc1, enc2, enc3, enc4 = "";
var i = 0;
var base64test = /[^A-Za-z0-9\+\/\=]/g;
if (base64test.exec(input)) {
window.alert("There were invalid base64 characters in the input text.\n" +
"Valid base64 characters are A-Z, a-z, 0-9, '+', '/',and '='\n" +
"Expect errors in decoding.");
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
enc1 = keyStr.indexOf(input.charAt(i++));
enc2 = keyStr.indexOf(input.charAt(i++));
enc3 = keyStr.indexOf(input.charAt(i++));
enc4 = keyStr.indexOf(input.charAt(i++));
chr1 = (enc1 && 2) | (enc2 && 4);
chr2 = ((enc2 & 15) && 4) | (enc3 && 2);
chr3 = ((enc3 & 3) && 6) | enc4;
output = output + String.fromCharCode(chr1);
if (enc3 != 64) {
output = output + String.fromCharCode(chr2);
if (enc4 != 64) {
output = output + String.fromCharCode(chr3);
chr1 = chr2 = chr3 = "";
enc1 = enc2 = enc3 = enc4 = "";
} while (i & input.length);
编写template(模版)
class="form-group" ng-controller="LoadingController"&
class="col-md-12 text-center"&
class="glyphicon glyphicon-refresh glyphicon-refresh-animate"&&&&loading...
class="container w-xxl w-auto-xs" ng-controller="LoginController"&
href class="navbar-brand block m-t"&{{app.name}}&
class="m-b-lg"&
class="wrapper text-center"&
&Sign in to get in touch&
name="form" class="form-validation"&
class="list-group list-group-sm"&
class="list-group-item"&
type="text" placeholder="username" class="form-control no-border" ng-model="user.username" required&
class="list-group-item"&
type="password" placeholder="password" class="form-control no-border" ng-model="user.password" required&
type="submit" class="btn btn-lg btn-primary btn-block" ng-click="login()" ng-disabled='form.$invalid'&Log in&
class="line line-dashed"&&
class="text-danger wrapper text-center" ng-show="authError"&
{{authError}}
// admin/router.js
$urlRouterProvider
.otherwise('/auth/loading');
$stateProvider
.state('auth',{
abstract: true,
url:'/auth',
template: '&div ui-view class="fade-in"&&/div&',
resolve: {
deps: ['$ocLazyLoad',
function( $ocLazyLoad ){
return $ocLazyLoad.load('admin/auth/ctrl.js');
.state('auth.loading',{
url:'/loading',
templateUrl:'admin/auth/loading.html',
.state('auth.login',{
url:'/login',
templateUrl:'admin/auth/login.html',
代码挺多,就不逐行解释了,最核心的就是:
用Base64加密[用户名:密码],在请求头加入 Authorization: Basic [加密串]
var authdata = Base64.encode(username + ":" + password);
$http.mon['Authorization'] = 'Basic ' +
改完以后重新访问,将实现流程图中的home-&loading-&login-&dashboard。
事情还没完,我们再重新访问,又会重复这个流程,并不会自动登录,这是因为BasicAuth的特性是在每一次web请求的时候都需要加入Authorization请求头才行。所以我们还要做点工作:1.登录成功以后将authdata存在本地,2.给全局http请求统一加入这个请求头
// admin/auth/ctrl.js
app.controller('LoginController',function($scope,$state,$http,$resource,Base64,$localStorage)
$com.get(function(){
$localStorage.auth =
$state.go('app.dashboard');
},function(){
$scope.authError = "服务器登录错误"
function ($rootScope,
$stateParams,$localStorage,$http) {
$http.mon['Authorization'] = 'Basic ' + $localStorage.
$rootScope.$state = $state;
$rootScope.$stateParams = $stateParams;
重新刷新首页,页面将实现自动登录了,但是事情还没完,进入系统以后,虽然每次Web请求我们都加入了BasicAuth的请求头,但是如果服务器端做了帐号修改,一样会产生401的错误,产生的结果就是客户端点什么操作都不会有反应,我们应该在全局来拦截401,引导客户端跳转到重新登录的界面:
app.config(function ($httpProvider) {
$httpProvider.interceptors.push('AuthInterceptor');
app.factory('AuthInterceptor', function ($rootScope, $q,$location) {
responseError: function (response) {
if(response.status==401)
$location.url('/auth/login');
return $q.reject(response);
大功即将告成,还有最后一步,有了登录必然有登出,BasicAuth协议本身是没有登出概念的,我们这里做的登出,就是删除本地那个保存的authdata。
// admin/main.js
angular.module('app')
.controller('AppCtrl', ['$scope', '$translate', '$localStorage', '$window', '$state','$http',
$translate,
$localStorage,
$window ,$state,$http) {
$scope.logout = function(){
$localStorage.auth =
$http.mon['Authorization'] = "Basic";
$state.go("auth.login");
function isSmartDevice( $window ){...}
class="dropdown-menu animated fadeInRight w"&
ng-click="logout()"&Logout&
锦上添花,标准的后台系统一般会在页面右上角显示登录用户的帐号信息,我们定义的协议/auth/info/是会把这些信息带下来的,我们来补全一下这个功能:
app.controller('LoadingController',function($scope,$resource,$state,$localStorage){
var $com = $resource($scope.app.host + "/auth/info/?");
$com.get(function(data){
$scope.session_user = $localStorage.user =
$state.go('app.dashboard');
app.controller('LoginController',function($scope,$state,$http,$resource,Base64,$localStorage){
$scope.login = function(){
$scope.authError = ""
var authdata = Base64.encode($scope.user.username + ':' + $scope.user.password);
$http.mon['Authorization'] = 'Basic ' +
var $com = $resource($scope.app.host + "/auth/info/?");
$com.get(function(data){
$scope.session_user = $localStorage.user =
$localStorage.auth =
$state.go('app.dashboard');
},function(){
$scope.authError = "服务器登录错误"
// admin/main.js
$scope.session_user = $localStorage.
$scope.logout = function(){...}
&!--admin/blocks/header.html--&
&span class="hidden-sm hidden-md"&{{session_user.username}}&/span& &b class="caret"&&/b&
创建news目录
& mkdir admin\news\
增加news导航
class="nav"&
class="hidden-folded padder m-t m-b-sm text-muted text-xs"&
translate="导航"&导航&
ui-sref-active="active"&
ui-sref="app.news.list"&
class="glyphicon glyphicon-book icon text-info-dker"&&
class="font-bold"&新闻管理&
书写控制器
'use strict';
app.controller('ListController', function($scope, $resource,$stateParams,$modal,$state) {
$scope.query = function(page,filter){
var $com = $resource($scope.app.host + "/news/?page=:page&search=:filter",{page:'@page',filter:'@filter'});
if(!page){
page=parseInt(page);
$com.get({page:page,filter:filter},function(data){
data.page_index =
data.pages = [];
var N = 5;
var s = Math.floor(page/N)*N;
if(s==page)s-=N;
var e = Math.min(data.page_count,s+N-1)
for(var i=s;i&=e;i++)
data.pages.push(i)
$scope.data =
$scope.search_context =
$scope.search = function(){
$state.go('app.news.list',{search:$scope.search_context});
var selected = false;
$scope.selectAll = function(){
selected = !
angular.forEach($scope.data.results,function(item){
item.selected =
$scope.exec = function(){
if($scope.operate=="1"){
var ids = [];
angular.forEach($scope.data.results,function(item){
if(item.selected){
ids.push(item.id);
if(ids.length&0){
var modalInstance = $modal.open({
templateUrl: 'admin/confirm.html',
controller: 'ConfirmController',
size:'sm',
modalInstance.result.then(function () {
var $com = $resource($scope.app.host + "/news/deletes/?");
$com.delete({'ids':ids.join(',')},function(){
$state.go('app.news.list');
$scope.query($stateParams.page,$stateParams.search);
app.controller('ConfirmController', ['$scope', '$modalInstance', function($scope, $modalInstance){
$scope.ok = function () {
$modalInstance.close();
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
app.controller('DetailController', function($rootScope,$scope, $resource, $stateParams,$state) {
$scope.edit_mode = !!$stateParams.
if($scope.edit_mode){
var $com = $resource($scope.app.host + "/news/:id/?",{id:'@id'});
var resp = $com.get({id:$stateParams.id},function(data){
$scope.data =
$scope.data = {};
$scope.submit = function(){
if($scope.edit_mode){
var $com = $resource($scope.app.host + "/news/:id/?",{id:'@id'},{
'update': { method:'PUT' },
$com.update({id:$stateParams.id},$scope.data,function(data){
$state.go($rootScope.previousState,$rootScope.previousStateParams);
var $com = $resource($scope.app.host + "/news/?");
$com.save($scope.data,function(data){
$state.go('app.news.list');
$scope.delete = function(){
var $com = $resource($scope.app.host + "/news/:id/?",{id:'@id'});
$com.delete({id:$stateParams.id},function(){
$state.go('app.news.list');
书写列表模版
class="wrapper-md" ng-controller="ListController"&
class="panel panel-default"&
class="panel-heading"&
class="nav nav-pills pull-right"&
style=" padding-top:4 padding-right:4px"& class="btn m-b-xs btn-sm btn-primary btn-addon" ui-sref="app.news.create()"& class="fa fa-plus"&&新增&&
class="row wrapper"&
class="col-sm-5 m-b-xs"&
class="input-sm form-control w-sm inline v-middle" ng-model="operate" ng-init="operate=0"&
value="0"&---&
value="1"&删除所选记录&
class="btn btn-sm btn-default" ng-click="exec()"&执行&
class="col-sm-4"&
class="col-sm-3"&
class="input-group"&
type="text" class="input-sm form-control" placeholder="Search" ng-model="search_context"&
class="input-group-btn"&
class="btn btn-sm btn-default" ng-click="search()" type="button"&Go!&
class="table-responsive" ng-if="data.total_count&0"&
class="table table-striped b-t b-light"&
style="width:20"&
class="i-checks m-b-none"&
type="checkbox" ng-click="selectAll()"&&&
&创建时间&
style="width:30"&&
ng-repeat="data in data.results"&
& class="i-checks m-b-none"& type="checkbox" ng-model="data.selected"&&&&&
&{{data.title}}&
&{{data.create_time|date:'yyyy-MM-dd HH:mm:ss Z'}}&
ui-sref="app.news.detail({id:data.id})" class="active"& class="fa fa-edit"&&&
class="panel-footer"&
class="row"&
class="col-sm-8 text-left"&
class="text-muted inline m-t-sm m-b-sm"&{{data.total_count}}条记录&
ng-if="data.page_count&1" class="col-sm-4 text-right text-center-xs"&
class="pagination pagination-sm m-t-none m-b-none"&
ng-class="{disabled:!data.previous}"& ui-sref="app.news.list({page:data.page_index-1,search:search_context})"& class="fa fa-chevron-left"&&&&
ng-repeat="page in data.pages" ng-class="{active:page==data.page_index}"& ui-sref="app.news.list({page:page,search:search_context})"&{{page}}&&
ng-class="{disabled:!data.next}"& ui-sref="app.news.list({page:data.page_index+1,search:search_context})"& class="fa fa-chevron-right"&&&&
书写详情、新增的模版,两者是可以复用一个模版的
ng-controller="DetailController"&
class="wrapper-md" &
class="panel panel-default"&
class="form-horizontal ng-pristine ng-valid ng-valid-date ng-valid-required ng-valid-parse ng-valid-date-disabled" ng-submit="submit()"&
class="panel-body"&
class="form-group"&
class="col-sm-2 control-label"&标题&
class="col-sm-10"&
type="text" class="form-control" ng-model="data.title" required&
class="line line-dashed b-b line-lg pull-in"&&
class="form-group"&
class="col-sm-2 control-label"&内容&
class="col-sm-10"&
class="form-control" rows="6" ng-model="data.content"&&
class="panel-footer text-right bg-light lter"&
type="button" ng-click="delete()" ng-if="edit_mode" class="btn btn-danger pull-left" value="删除"/&
type="submit" class="btn btn-success" value="提交"/&
书写删除确认框模版
class="modal-header"&
&确认删除?&
class="modal-footer"&
class="btn btn-default" ng-click="cancel()"&Cancel&
class="btn btn-primary" ng-click="ok()"&OK&
添加路由,注意run里面增加了事件监听,后文详细说
// admin/router.js
function (...) {
$rootScope.$on('$stateChangeSuccess', function(event, to, toParams, from, fromParams) {
$rootScope.previousState =
$rootScope.previousStateParams = fromP
.state('app.news', {
abstract: true,
url: '/news',
template: '&div ui-view class="fade-in"&&/div&',
resolve: {
deps: ['$ocLazyLoad',
function( $ocLazyLoad ){
return $ocLazyLoad.load('admin/news/ctrl.js');
.state('app.news.list', {
url: '/list?page&search',
templateUrl: 'admin/news/list.html',
.state('app.news.detail', {
url: '/detail/{id}',
templateUrl: 'admin/news/detail.html',
.state('app.news.create', {
url: '/create',
templateUrl: 'admin/news/detail.html',
我们这里搜索和分页都是采用URL跳转的方式(#/news/list/?search=str&page=int),这样能保证刷新页面的时候能停留在之前的页面结果上,ng默认的页面跳转是不保留前一个页面状态的(链接和参数),如果我们跳转到第2页,编辑,再返回,是会回到第1页去,为了比较好的用户体验所以我们有了如下代码
监听全局页面跳转信号($statChangeSuccess),将参数保存下来
// admin/router.js
$rootScope.$on('$stateChangeSuccess', function(event, to, toParams, from, fromParams) {
$rootScope.previousState =
$rootScope.previousStateParams = fromP
详情页编辑完成返回时读取参数跳转
// admin/news/ctrls.js
app.controller('DetailController',...){
$com.update({id:$stateParams.id},$scope.data,function(data){
$state.go($rootScope.previousState,$rootScope.previousStateParams);
完整的后台少不了导航
我们这里选用github上面一个写得挺棒的ng导航插件,求简,我们直接下载,由于我们是给ng装插件,所以建议放到vendor/里面去,接下来的改动也是针对原模版框架。
&!-- index.html --&
&script src="vendor/angular/angular-breadcrumb/angular-breadcrumb.min.js"&&/script&
// js/app.js
angular.module('app', [
'ncy-angular-breadcrumb',
配置扩展,让导航能支持HTML(具体就是能显示回首页的图标)
// js/config.js
app.config(function($breadcrumbProvider) {
$breadcrumbProvider.setOptions({
templateUrl: 'tpl/blocks/breadcrumb.html'
扩展的导航模版,写法参考插件官方文档
class="breadcrumb bg-white b-a"&
ng-repeat="step in steps | limitTo:(steps.length-1)"&
href="{{step.ncyBreadcrumbLink}} ng-bind-html="step.ncyBreadcrumbLabel"&&
ng-repeat="step in steps | limitTo:-1" class="active"&
ng-bind-html="step.ncyBreadcrumbLabel"&&
配置路由,加入导航
// admin/router.js
.state('app.dashboard', {
ncyBreadcrumb: {
label: '&i class="fa fa-home"&&/i& 首页'
.state('app.news.list', {
ncyBreadcrumb: {
parent:'app.dashboard',
label: '新闻列表',
.state('app.news.detail', {
ncyBreadcrumb: {
parent:'app.news.list',
label: '编辑',
.state('app.news.create', {
ncyBreadcrumb: {
parent:'app.news.list',
label: '新增',
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:67539次
排名:千里之外
原创:19篇
评论:22条
(3)(2)(1)(10)(2)(1)我的web开发最强组合:Play1+angularjs+bootstrap ++ (idea + l - 为程序员服务
我的web开发最强组合:Play1+angularjs+bootstrap ++ (idea + l
初稿,包含play1+angularjs+bootstrap+idea+livereload介绍
添加了haxejs部分
删除haxejs,添加typescript部分,并修改了在play1中使用angularjs的方式
暂时不再使用typescript,因为相关工具不成熟
首先说明我开发web的情况:
前后端全部自己搞定
网站类型多为传统多页面程序
注重开发效率
Javascritp能力不强
美术细胞很少
由于每个人情况不同,选择技术及方案的重点也不同,所以内容仅供参考。对于我来说,这一套不论开发效率还是开发感受都是很好的。
一、编辑器
我选用的是idea Ultimate,它除了超强的java编辑功能之外,还提供了功能完善的play1和play2插件,angularjs插件,javascript/css编辑功能,让人开发时事半功倍。这里截几个图说明一下:
模板标签的提示与补全
href属性中,可提示controller及action
routes文件中的提示
html中play标签的高亮和格式化
从action中快速跳转到view
点击红框后自动打开:
因为我不使用play2,所以未实际测试过。不过play2支持作为idea12的卖点之一,其支持程度应该不会低于play1。详情可参看idea官网上的介绍。
对于play2中scala模板的支持应该是很多人想要的
Angularjs支持
html属性提示
Bootstrap支持
idea没有对bootstrap有特别的支持,不过因为它本身对css的支持比较好,所以也可以方便的得到提示
Javascript支持
idea ultimate对javascript的支持非常强大,是我用过的编辑器中,对js支持最好的。不论是js文件,还是在html中嵌入的js代码,高亮、格式化、查错、提示等功能,都是一流
idea ultimate对css的支持也是非常强大
需要注意的是,上面提示的各功能,基本上都是idea ultimate才提供的(idea社区版中基本上没有上述功能)。如果你经济能力足够并且喜欢idea,不妨购买license对它进行支持;否则的话,自行google解决。
二、Live Reload
LiveReload是指当我们修改了项目中的文件时,浏览器会自动刷新,显示修改之后的效果。
在开发网站时,这个功能非常有用。想想你面前开着两个显示器,中间这个是编辑器,旁边的那个是浏览器。每当修改了java/html/css/js代码时,手不离键盘、光标不离编辑区,浏览器就自动刷新了!只需要眼睛轻轻一瞟,脖子都不用动,就能看到修改之后的效果,这种感觉何等舒畅!钛合金的F5都不需要了!
这种方式对于web框架有要求,首先是修改文件后不需要重启服务器。像纯html/php/rails都天生支持,java中某些框架支持,而play1的支持相当优秀。不论修改html/css/javascript,还是java源代码,甚至是配置文件,都不用重启,直接刷新浏览器了。刷新时间大约为1秒到4秒。
另一点是:因为livereload检查到文件修改后,只会触发浏览器刷新一次,所以要保证一次刷新就可以看到修改后的效果。某些框架利用tomcat/jetty的自动重启,无法很好的配合livereload。因为还没有重启完时,livereload就刷新了,取得的可能还是修改前的页面,必须手动刷新多次才能确定看到的是修改后的效果。对于这种情况,livereload几乎没用。而play在刷新过程中,会阻塞http请求,可以保证一次刷新就拿到修改之后的页面。
Livereload的官网是,它支持mac/linux/windows,同时还有chrome/firefox的浏览器插件。它对windows的支持比较差,很容易崩溃,而且是收费的。所以我们只需要用它的浏览器插件就可以了(免费的),然后再找一个免费的替代器换掉服务器端。
我选择的是:,它是一个python程序,以命令行方式启动,可以跟livereload的浏览器插件通信,效果不错。注意最好从github中下载源代码安装,因为通过pip或easy_install安装的版本有点旧,使用过程中有问题。(不清楚现在是否已经更新)
使用如下:
cd myproject
livereload
然后启用浏览器的livereload插件即可。
来段gif演示一下(注意每次刷新都是我按了ctrl+s后自动触发的):
三、Why play, and why play1
参考我写的另一个日志:
四、Why angularjs
曾经有一段时间,我对前端javascript框架很感兴趣,试用了很多,比如backbone/knockout/knockback/angularjs/…(以及一大堆已经忘了名字的),其中有两个让我印象深刻。一个是backbone,一个是angularjs。
Backbone的优点在于学习成本很低,与jquery的思路接近,偏向于底层及手动控制。采用backbone的项目比较多,资料也比较多,社区也比较大,还有一些基于backbone发展起来的高一级的框架(如)。这个bbb我没有用过,只是在js群中有人说使用它之后,开发效率比之前纯backbone有很大的提高,所以这里提一下。对于技术较普通的团队来说,使用backbone可能会比较保险一些。
而angularjs则是一个让我感到惊艳的框架,相对于同类无数个mv**框架,它的优势达到了数量级。如果用几个词来形容它,应该是:学习成本高,开发效率高,写代码时思路流畅。
它拥有双向绑定、directive、直接改写html标签等特性,使用它你可以对现有的html标签进行改进和增强,甚至还可以重写一套完全属于自己的html标签。也许你会觉得像“双向绑定”这种烂大街的特性有什么值得拿出来说的,关键在于anguarljs的设计非常统一,各个功能搭配得很流畅、一气呵成。这种感觉就像是eclipse与idea在操作上的区别:idea虽然有很多功能eclipse也提供了,但是用起来总不像idea中那么流畅 — 不论编辑什么类型的文件,在idea中都可以使用非常类似的操作,得到非常类似的界面反馈。
Angularjs的学习成本比较高,主要原因是其设计思路与我们以前写jquery代码时有很大的不同,不能套用。Angularjs的核心思想就是“复用”,它的“复用”体现在"directive"上。Directive既是angularjs的核心,也是它的重点、难点和杀手级特性。简单的说,directive可以是一个自定义的html标签、或者属性、或者注释,它的背后是一些函数,可以对所在的html标签进行增强,比如修改html的dom内容,增强它的功能(如用第三方js插件包装它)。
编写Directive比较复杂,需要理解它的内部原理才能定义出自己的directive。在掌握它以前,以前一些很简单的事情可能都没办法做,容易让人沮丧。比如在使用jquery时,经常会这样操作:
$("#mydiv").dialog();
但这种写法在使用angularjs的html页面中,是无法使用的。你必须把它写成一个directive(比如ui-dialog),然后在它的postLink()方法中,对传入的element元素操作:
element.dialog()
如果不理解postLink的各参数以及它是如何被angularjs使用的话,很难写出来。所以在使用angularjs的前期,很容易被卡住。
在学习angularjs时,一定要细读官网提供的develop guide (),把各章节读懂,知道angularjs的内部运行原理。千万别按jquery的方式学习,光看示例是绝对不够的。
Angularjs的另一个杀手级特性,就是把流程控制、事件绑定等代码,直接写在html标签上。这其实就是前面所说的directive的使用方式。先看一段代码:
在这段html代码中,你可以看到那些位于html标签上由蓝色背影标出来的内容,都是angularjs提供的directive。有的是绑定事件(如ng-click,ng-submit),有的是控制流程(如ng-repeat)。这种方式我非常喜欢,简单直接,可读性又很好。当然有人不喜欢这种方式,认为html就应该干干净净,应该把这些东西分享到javascript中,就像下面这样:
$("form").submit(function() { ... });
其实对于这种情况,angularjs也有相似的做法,即为该form定义一个directive,比如my-add-form,然后把那些逻辑代码放到它里面:
&form my-add-form&...&/form& // js code
module.directive("myAddForm", function() {
// the logic
不过这种方式对于一个不那么通用的逻辑来说有点重。所以我们通常还是采用在html标签上写控制,在controller中写逻辑的方式来做,通过合理的分配,在可读性与方便性之间取得平衡。
直接在标签上绑定事情处理函数,可以减少大量的命名,减少无谓代码,而且阅读起来更直观。想当初看backbone代码时,发现有二分之一的代码,都是通地css selector来获取元素,再将其某个事件与某个函数绑定起来。这样的代码一旦写完,html就不敢随便动了。因为若更改了html结构、id或者css class,这边的js代码都可能无法正常执行。
熟悉angularjs以后,会发现实现前端效果时,开发效率很高。在写html的同时,基本上就可以把大部分的交互效果写出来。同时,angularjs以model为中心,在编码时只需要考虑model。当改变了model的内容时,view就会自动更新,这可以让我们需要关注的东西更少。使用了angularjs后,你会发现html标签的表现力变强了,以前需要一些js插件实现的功能(比如简单的tab、tree等),使用angularjs几行代码就可以实现,而且所有的东西都是可定制的。
比如一个tab:
&a ng-click="tab=1"&Tab1&/a&
&a ng-click="tab=2"&Tab2&/a&
&a ng-click="tab=3"&Tab3&/a&
&/div&&div ng-show="tab==1"&This is tab1&/div&
&div ng-show="tab==2"&This is tab2&/div&
&div ng-show="tab==3"&This is tab3&/div&
比如一个tree
&script type="ng/template" id="'node.html'"&
{{node.name}}
&li ng-repeat="node in node.children" ng-include="'node.html'"&&/li&
&/script&&ul&
&li ng-repeat="node in rootNodes" ng-include="'node.html'"&&/li&
我在做某个网站后台时,开始打算用angularjs,后来感觉功能比较简单,就直接采用jquery,少引用一个库。开始还好,很快就发现只要增加一点复杂的功能时,所花的时间就大大增加。如果同样的功能使用angularjs来写,会非常简单。
五、单页面程序以及前后端交互
一般认为前端mv**框架适合于单页面程序,无刷新、局部更新的那种。前后端完全分离,之间以restful api交互,使用json交换数据。在前端做好router,当点击了某个按钮需要展示新内容时,直接由前端获取并显示另一个局部html页面,同时调用某个restful api获取json数据填充。这种程序,通常前端的功能比较复杂,而对后端要求较少。采用这类mv**框架,前端程序员们可以充分发挥自己的才智,完全使用javascript/css/html来实现功能。而对于后台,只需知道restful api接口即可。这是前端mv**推荐的方式,也是目前来说比较好的方式。其特点是“以前端js框架为主,后端为辅”。
Anguarljs对于这种方式,有着非常好的支持。它除了提供前端router外,还提供了一些与后台交互的service,如与ajax相关的$http,与restful相关的$resource;对于cookie与本地存储支持也很好,基本上使用angularjs就可以把程序做完。后台可以使用各种语言、各种框架来提供restful api。比如,我尝试过couchdb这个数据库,它直接在数据库层面提供了restful api作为外界操作数据库的接口,angularjs与它配合起来,连服务端程序都不用了。
在开发android程序时,我也尝试过将phonegap与angularjs结合起来,直接使用angularjs来实现程序。与后台之间通过restful api交互。最后虽然因为性能要求改用了android原生方式,但对于普通的安卓或ios应用来说,这种方式是一种很好的选择,开发效率很高。
六、传统多页面程序
对于我来说,大部分的网站还是传统多页面的。比如一个信息管理系统的后台。这种情况下能否使用angularjs呢?
最开始的时候,我想采用单页面的方式来做,按照前面所说的流程。但是很快遇到了不少麻烦:
页面跳转:这种网站页面很多,在前端定义比较麻烦,因为需要写长长的url,容易出错,检查也不方便。而使用play的模板引擎,可以直接写@{Controller.action(params)},play会自动把它变为url,并且会检查是否有拼写错误,非常方便。
权限:页面上某些按钮的显示与隐藏,取决于当前用户的角色。所以每次显示某个新页面时,都需要后台传过来一些json数据,例如:{ buttons: [{show: true}, {edit: false}]},来告诉前台显示或隐藏哪些按钮。有的时候这个操作非常繁琐。
多次请求:当显示一个新页面时,可能需要多次请求。首先html模板一次,然后取json数据一次(或多次)。这样给人的感觉就有点慢。虽然可通过一些手段(如缓存html模板,为每一个请求返回一个大的合并过的json数据),但由于模板显示的时间与取得json数据的时间之间总有一些间隔,有时候还是会让人觉得不太流畅,卡。
这几个问题我想了很久也没好办法,甚至打算放弃angularjs,还是采用以前完全由服务端生成页面的方式来做。好在最后改变了思路,“以后端框架为主,前端为辅”,找到了比较好的办法。
使用play模板的继承、包含功能
我没有像angularjs推荐的那样,采用静态的html,而继续使用play模板引擎,因为它有两大好处:
在服务器端装配好最终的html页面
利用play的模板引擎,我们可以把模板分开为多个文件,使用#{extends/}和#{include/}等标签,来继承及包含另一个页面。这个过程是在服务器端完成的,发给前端的是一个单独的html页面。它可以减少html请求的次数,并且文件组织起来也很灵活。
但如果我们用静态html来做,就有麻烦。如果将文件分为多个,使用ng-include包含,则它会产生一个新的html请求。这个请求将会在主页面处理完之后才请求,所以会有比较明显的延迟。另外,并且当页面分块较多时,很不方便。比如一个页面是“品”字型结构,顶端可以改变下面的页面,左边又能改变右边的页面,则在angularjs中不好实现,因为angularjs不支持嵌套的ng-view,只能用ng-include模拟,而这个过程是很繁琐的,并且可能产生更多的延迟性的请求。
可生成调用地址
使用前端mvc框架的另一个不方便的地方,就是配置routes很麻烦,这是全手动的过程。当页面很多并且有嵌套时,更加痛苦。但如果我们使用Play模板系统,则访问一个html页面还是得通过Play的action,这样就可以在服务器端生成一个js文件,找到这些入口,把它们对应的url算出来,传给前端直接用。前端要访问一个页面,只需要这样写:
&li ng-repeat="user in users"&&a ng-href="http://freewind.me/blog//{{Users.show(user.id)}}"&{{user.name}}&/a&&/li&
注意其中的User.show()这个方法,它实际上是由后台生成的。它到底对应哪个url,在前台是完全不用操心的。哪怕以后url改了,但也丝毫不会影响页面中的代码,因为url是自动生成的。
如果不使用Play的模板系统,很难做到这些。另外,由于我们仅用到最基本的功能,所以可以考虑采用性能更好的模板系统来代替Play的模板引擎,比如green同学的。
后端生成Js文件供前端调用
Angularjs在前台需要调用后台的方法时,往往通过一些restful api接口。由于restful api的意义在于良好、稳定的结构,让前后台解耦,但对于我来说,这点意义不大。
所以我可以利用Play的目录结构,让Play根据action的信息,生成一个jsRoutes文件,把每个action的get/post的调用方式,以及生成url的功能都包含进去。前台的angularjs要跳转页面,或者调用一个ajax方法,都只需要调用其中的一个函数即可,不需要关心url是什么。这里在下面将详细说明。
这种方式让事情变得简单多了,不追求无刷新,而追求开发效率与写代码时的舒适性。
七、关于angularjs的$resource
在angularjs中提供了一个service叫$resource:,它可以通过一个url和一些参数,定义一个resouce对象。通过调用该对象的某些方法,可以与后台交互,并且直接得到经过它处理之后的数据。使用的感觉有点像我们在后端常用的dao。
这个$resource服务对于“单页面,以restful api交互”的情况比较合适。它要求所给出来的url可以按restful api的方式调用,正好满足适合这种情况。
但对于“传统多页面程序”不好用,特别是那种信息管理系统。因为它们的url形式并不重要,是否restful也不重要,只要提供get/post两种方式,能把参数传过去就行了。如果在这里使用$resource,按它的规定来套,需要花很多心思来设计url,非常痛苦。我在这里卡了很长时间,因为我想不明白,为什么这个$resource看起来很好,但用起来就是不对劲呢。最后终于想通,原来我的情况不需要restful api。
最后我的做法是,在服务端把各action收集起来,生成一个js文件,在这个文件里把action以js函数的方式暴露出来,供angularjs直接调用(内部使用了angularjs提供的$http服务,而没有用$resource)。在本例中,这个js文件中定义了一个叫JsRoutes的object供使用。
Angularjs调用它的方式是这样的:
function Ctrl($scope, JsRoutes) {
$scope.submit = function() {
JsRoutes.Users.create.post({
username: username,
password: password
}, function(res) {
alert("ok");
这样,当我需要向后台传user相关的数据时,直接调用预定义的JsRoutes.Users.create即可,而不需要关注它对应的url到底是什么。
这个js文件的代码是这样的:
angular.module('JsRoutes', []).factory('JsRoutes', function ($http) {
var defaultErrorHandler = function (data, status, headers, config) {
alert('Sorry, server responses ' + status + ' error: ' + data);
// angular post json by default, change it to key value pairs
var keyValuesTransformFn = function (d) {
return jQuery.param(d);
var commonHandler = function (method, url, params, data, success, error, config) {
config = config || {};
config.method = config.method ||
config.params = config.params ||
config.data = config.data ||
config.url = config.url ||
config.timeout = config.timeout || 120 * 1000;
var postType = config.postType || 'form';
if (postType === 'form') {
config.transformRequest = keyValuesTransformFn;
config.headers = config.headers || {};
config.headers['Content-Type'] = 'application/x-www-form- charset=UTF-8';
// config.headers['Accept'] = "application/json, text/html, text/plain, */*";
$http(config).success(success).error(error || defaultErrorHandler);
var jsRoutesHandler = function (path) {
get: function (params, success, error, config) {
commonHandler('get', path, params, {}, success, error, config);
post: function (data, success, error, config) {
commonHandler('post', path, {}, data, success, error, config);
link: function(params) {
return path + "?" + jQuery.param(params);
#{list actions.keySet(), as: 'module', separator: ','}
#{if module}
${module} : {
#{list actions.get(module).keySet(), as: 'controller', separator: ','}
${controller} : {
#{list actions.get(module).get(controller), as: 'item', separator: ','}
${item.action}: jsRoutesHandler('${item.path}')
#{list actions.get(module).keySet(), as: 'controller', separator: ','}
${controller} : {
#{list actions.get(module).get(controller), as: 'item', separator: ','}
${item.action}: jsRoutesHandler('${item.path}')
注意其中的get/post/link三处,这是前台调用它们的关键方法。(现在使用typescript后,还可以考虑生成typescript的声明文件)
采用这种方式后,在前端js代码中完全不需要跟url打交道,因为它们都是在服务器端根据routes文件生成的。以后url有什么变化,js这里不需要修改一行代码。
另外需要注意的是,angularjs默认会向服务端发送json格式的数据,而play对key-value形式的数据处理的比较好,所以我就把它默认值改为了'application/x-www-form-urlencoded'
八、Angularjs中如何集成第三方js插件
有时候需要用一些第三方插件,比如datepicker,slider,或者tree等。以前的做法是直接通过jquery取得某个元素,然后调用某个方法即可。但在angularjs中,不能直接这么写,必须写在directive中。
有一个叫augular-ui的项目:,已经集成了一些常用的插件(来自jqueryui),很方便。但如果还是需要自己定义,该怎么做呢?
基本的思路就是,创建一个directive,把调用jquery插件的代码放在它里面。这里以jqueryui的slider为例:
样子如上,其示例在:
在jquery中,它的调用方式是这样的:
&div id="slider"&&/div&$("#slider" ).slider({
value: 10,
而在angularjs中,我们需要定义一个directive,假设是ui-slider:
app.directive('slider', function () {
require: '?ngModel',
restrict: 'A',
link: function (scope, element, attrs, ngModel) {
opts = angular.extend({}, scope.$eval(attrs.slider));
var slider = element.slider({
min: opts.min || 0,
max: opts.max || 100,
step: opts.step || 10,
value: attrs.ngModel && scope.$eval(attrs.ngModel) || 50,
slide: function (event, ui) {
if (ngModel) {
scope.$apply(function () {
ngModel.$setViewValue(ui.value);
scope.$watch(attrs.ngModel, function (v) {
slider.slider({
在html中的调用方式是:
&div slider="{min:0,max:500,step:5}" ng-model="row.width"&&/div&
在directive中的代码比jquery的多了不少,不过主要是增加了与双向绑定有关的两个函数。例如在"slide: function(event, ui)“中,是把sider的值传回给某个model(本例中为row.width)。在后面的scope.$watch中,是把model的值传给slider。这样当拖动slider上的刻度时,row.witdh的值会自动改变;或者改变了row.width的值之后,slider的刻度也会自动变化。
基本的思路就是这样,更详细的需要看相关文档。与angularjs相关的就讲到这里,在这里你可以看到很多angularjs的可执行的例子:,可以直接感受。
最后需要补充的两点:
Angularjs的社区气氛很好。其google group人气很旺,有问题很快就能得到详细的回复
Angularjs对IE6/7支持不好。如果一定要支持ie6/7,可考虑backbone或其它框架。
九、Bootstrap
Bootstrap是像我们这样缺少艺术细胞的程序员的福音。只需要记一些css class和某些js component的用法,就可以做出看起来比较美观、专业的页面效果出来。虽然过不了多久,人们也许就会对它出现审美疲劳,但好在它的社区已经形成,已经有不少基于它的模板出来,相信以后会有更多更美观的bootstrap theme可供选择:
十、Why HaxeJs
我的Js能力不强,主要是因为觉得Js里陷阱太多,总是觉得不安。所以我一直想找一个有静态类型、功能强大、能生成js并且生成的代码体识较小的语言,来代替它。经过多次尝试,终于让我找到了。
它就是HaxeJs。参看:
HaxeJs对于我来说,意义是很大的,因为Javascript是我长久以来心中的痛。不想用,但经常又不能不用,所以我尽量把逻辑写在后台,前台通过Ajax调用。但对于页面效果,不写JavaScript是不行的,所以我总为这犯愁。使用了Angularjs后,虽然对于javascript的要求大大降低,但当逻辑复杂的时候,上百行的js代码还是让我很不安。
但现在有了haxejs,我可以在一种类型安全的环境中写js代码,感觉就安全不一样了。以后我可以把更多的逻辑放在前台由js实现,或者使用haxe来写后台(比如nodejs/php等),感觉脚下的路一下子变宽了。
经过几天的试用,我
十、Why Typescript
在haxejs之后,又尝试了由微软推出的typescript。它的语言特性没有haxejs那么强,但是正好避开了haxejs与angularjs之间不匹配的那几个问题:
Typescript的思路还是javascript,并且兼容js语法,angularjs再侵入html也不怕
Typescript中允许使用$作为变量名
Typescript的类型功能基本可用,并且可下载到Angular的声明文件
详细内容可参考我的另一个文章:
经过几天的试用,Typescript倒在了工具和语言本身还不够成熟:
等到idea能支持直接在编辑器中实时检查错误时,再使用。
原文地址:, 感谢原作者分享。
您可能感兴趣的代码

我要回帖

更多关于 angularjs 构建 的文章

 

随机推荐