Knockout Mvc Compoment FrameSet
框架文件结构
- 网站(表现层),mvc主要作用视图展示。
- 模型(Model),主要作用承载视图数据结构,网站前后台数据交互模型。
- 业务(Business),业务逻辑层(含Ibusiness),视图接口定义,从服务过来的Dto通过Business转换成Model。
- 单元测试(test),Controller中方法的单元测试。
1、网站文件结构
App_Data
App_Start
BundleConfig.cs mvc压缩css和js的配置文件。
FilterConfig.cs mvc过滤器配置,站点地图权限登录菜单管理的入口。
ExceptionFilter.cs 网站程序异常处理过滤器。
MvcMenuFilter.cs 框架权限验证和菜单处理主要程序。
WebSiteExcetpion.cs 自定义网站错误类型。用于抛出登信息丢失等自定义业务异常。
assest
网站主要引用的资源文件(样式,常用组件等)。现应用的是metronic_v4.1.0。
Controller
DictionaryFiles
Filters
Mvc过滤器文件夹,里面可以自定义一些过滤器,自定义的过滤器需要在App_Start下的FilterConfig.cs中注册。
Scripts
UserControls
Views
Mvc 视图文件夹,母版页在 Shared 为 _Layout.cshtml
BaseHttpApplication.cs
定义一些 HttpModule的自定义方法,目前在框架应用中,主要用于处理通用上传和下载。
Global.asax
SiteInfo.cs
Web.config
2、模型文件结构
3、业务文件结构
框架基础
2、【网站过滤器验证】 (MvcMenuFilter.cs 需要在FilterConfig中注册)
每一个action请求时会判断当前Session中是否有登录信息。
异步请求,则通过throw new WebSiteException ,错误码为1000.
所有网站异常都会被ExceptionFilter.cs捕获。
ExceptionFilter 可以记录错误日志,日志路径 在网站跟目录的Log文件夹下。
public
static
Dictionary<string, string> ErrorDictionary = new
Dictionary<string, string>()
如果抛出的是WebSiteException则会通过字典方式相应。
这个抛出的结果会被框架通WebUtil.js响应。(后面会详细说明)
-
菜单生成原理
获取当前系统编号后,可以通过系统编号拿到该用户当前登录系统的菜单,并转换成List<LoginMenuInfo>这个数组。
这个数组会通过ViewBag.treeData (json格式)传递到页面。
页面接收到数据时会通过Menu.js 响应,来生成菜单和面包屑导航。
接口
interface WebUtilAjaxOption extends JQueryAjaxSettings {
encodeHtml(str: string): string;
stopEvent(event: any): void;
parseDate(value: string): Date;
getQueryString(name: string): string;
ajax(option: WebUtilAjaxOption);
ajax中定义了一个错误的Handler 其中包含一些通用的自定义错误码
sh.alert("您的登录已失效,请重新登录。", function () {
location.href = "/Account/Login";
sh.alert("您的企业信息丢失,请重新选择企业。", function () {
location.href = formatUrl("/Home/EnterpriseSet");
sh.alert("您已在当前企业离职。", function () {
location.href = formatUrl("/Home/EnterpriseSet");
是以用WebUtil.ajax调用后台方法时如后台程序抛出异常,则会判断错误码,
如果错误码中的ErrorCode 符合通用Handler时则会调用Halder的方法
如果不包含errorCode但是包含 ErrorMessage 则是wcf接口抛出的通用异常,会直接弹出提示。
果即不包含错误码也不包含ErrorMessage则是404之类的调用异常。
抛出 调用ajax异常时 则是前端controller或者business出错,如果直接提示的错误信息则是wcf服务异常。
console.log(xhr.responseText);
var errorJson = $.parseJSON(xhr.responseText);
if (errorJson.errorCode != null) {
var errorFun = errorHandler[errorJson.errorCode];
var firstMsgJson = errorJson.errorMessage.match(/\{[^{}]+\}/)[0];
var serviceError = $.parseJSON(firstMsgJson);
sh.alert(serviceError.ErrorMessage);
sh.alert("调用ajax异常,请查看程序日志:" + errorJson.errorMessage);
getNum(index: number, pageIndex: number, pageSize: number): number;
declare
var WebUtil: WebUtilStatic;
alert(msg: string, callback?: () => void, msgtitle?: string): void;
confirm(msg: string, yescallback: () => void, nocallback?: () => void, msgtitle?:
}
declare
var sh: shStatic;
interface KnockoutStatic {
/**
* 注册控件通用方法
* @param controlName
* @param viewModel
* @param templateUrl
* @returns {}
*/
RegisterControl(controlName: string, viewModel: any, templateUrl: string): void;
}
registerControl方法会造成很多次异步html请求,正在想办法解决。
formatCurrency
/**
* 将数字转换为 格式化后的金钱字符串
* @param num
*/
declare
function formatCurrency(num: number): string;
formatUrl
/**
* 处理虚拟目录格式化地址的方法 在Layout上实现
* var appRoot = "@Request.ApplicationPath";
* if (!appRoot) {
* throw new Error("请设置全局变量.");
* }
*
* function formatUrl(url) {
* if (url == null) {
* return url;
* }
* if (window.appRoot && window.appRoot != ‘/‘ && url.indexOf("/") == 0) {
* if (url.indexOf(appRoot + "/") != 0) {
* url = appRoot + url;
* }
* }
* return url;
* }
* @param url
*/
declare
function formatUrl(url: string): string;
Guid
/**
* 定义一个Guid接口
*/
interface GuidStatic {
Empty: string;
}
/**
* 定义一个Guid静态类
*/
declare
var Guid: GuidStatic;
/**
* 定义String静态方法
*/
interface StringConstructor {
format: (...args: any[]) => string;
}
Date通用
/**
* 时间通用处理
*/
interface Date {
/**
* 格式化时间
* @param format
* @returns {}
*/
format(format: string): string;
/**
* 添加年
* @param value
* @returns {}
*/
addYear(value: number): Date;
/**
* 添加月
* @param value
* @returns {}
*/
addMonth(value: number): Date;
/**
* 添加天
* @param value
* @returns {}
*/
addDays(value: number): Date;
/**
* 添加小时
* @param value
* @returns {}
*/
addHours(value: number): Date;
/**
* 添加分
* @param value
* @returns {}
*/
addMinutes(value: number): Date;
/**
* 获取今天
* @param value
* @returns {}
*/
getToday(): Date;
}
KnockoutPaging扩展
/**
* 为kopaging 插件做的扩展
*/
interface KnockoutObservableArrayFunctions<T> {
/**
* 扩展了ko paging之后才有的属性
*/
pageIndex: KnockoutObservable<number>;
/**
* 扩展了ko paging之后才有的属性
*/
pageSize: KnockoutObservable<number>;
/**
* 扩展了ko paging之后才有的属性
*/
callback: () => void;
/**
* 设置数据总条数
* @param count
* @returns {}
*/
SetPageTotal: (count: number) => void;
}
KeyValuePair
/**
* 键值对
*/
interface KeyValuePair2<TKey, TValue> {
Key: TKey;
Value: TValue;
}
/**
* 键值对参数对象
*/
interface IKeyVaulePair {
Key: any;
Value: any;
}
/**
* 键值对委托方法
*/
declare
var KeyValuePair2: (obj: IKeyVaulePair) => void;
-
loading.js
接口
显示等待框方法
/**
* loading插件接口
*/
interface loadingStatic {
/**
* 打卡loading
* @param text 显示的文字
* @returns $loading
*/
open(text?: string): JQuery;
/**
* 关闭等待框
* @returns $loading
*/
close(): JQuery;
/**
* 一定要在 dom ready之前调用,否则无效。
* @param url loading 图片的路径 默认为 imgs/loading.gif
* @returns $loading
*/
setImageUrl(url: string): JQuery;
}
declare
var loading: loadingStatic;
例子
/*
* 开启 loading.open();
* 关闭 loading.close()
* 设置loading图片 loading.setImageUrl("/Content/Images/loading1.gif");
-
jquery.sh.popups.js
接口
/**
* 弹窗组件参数
*/
interface popupsOptions {
listeners?: {
show?: () => void;
hide?: () => void;
};
width?: number;
}
/**
* 弹窗组件对象
*/
interface popups {
show: () => popups;
hide: () => void;
}
interface JQuery {
/**
* 弹窗插件
* @param options
* @returns {}
*/
popModal(options?: popupsOptions): popups;
}
例子
/*
var modalChooseBuilding = $("#modalChooseBuilding").popModal({
listeners: {
show:function() {
},
hide:function() {
}
},
width: 1200
});
modalChooseBuilding.show(); 显示弹窗
modalChooseBuilding.hide(); 关闭弹窗
*<div id="modalChooseBuilding" class="pop-modal">
<div class="modal-header">
<h4 class="modal-title">标题</h4>
</div>
<div class="modal-body">
</div>
<div class="modal-footer">
<button class="btn btn-primary" >确定</button>
<button type="button" class="btn btn-default btn_enter" onclick="modalChooseBuilding.hide();">关闭</button>
</div>
</div>
*/
-
simpleValidate.js
接口
///
<reference path="../../jquery/jquery.d.ts" />
/**
* 参数接口
*/
interface SimpleValidateOption {
/**
* 成功的样式
*/
successClass?: string;
/**
* 失败的样式
*/
errorClass?: string;
/**
* 失败元素的样式
*/
errorMessageClass?: string;
/**
* 远程验证元素呈现的样式
*/
remoteClass?: string;
}
/**
* simpleValidate 静态方法
*/
interface SimpleValidate {
/**
* 添加规则
* @param rule
* @param message
* @returns void
*/
addRuleMessage(rule: any, message: any);
/**
* 初始化方法
* @param element
* @param options
* @returns {}
*/
init(element: JQuery, options?: SimpleValidateOption);
/**
* 重置方法
* @param element
* @returns {}
*/
reset(element: JQuery);
}
interface JQueryStatic {
/**
* 初始化全局JQuery静态变量
*/
simpleValidate: SimpleValidate;
}
interface JQuery {
/**
* 验证方法
* @param options
* @returns {}
*/
simpleValidate(options?: SimpleValidateOption): JQuery;
}
例子
初始化验证方法 $.simpleValidate.init($("#modalCopySpaceScheme"));
重置 $.simpleValidate.reset($("#modalCopySpaceScheme"));
验证
if (!$("#modalCopySpaceScheme").simpleValidate()) {
return;
}
-
jquery.sh.webuploader.js
通用上传控件
Api接口
interface ShUploaderServerFile {
responseVal: string;
name: string;
ext: string;
}
interface ShUploaderOption {
//对应错误处理时使用的提示信息
errorMessage: any;
//生成的input所使用的NAME
inputName: string;
//此处设为flash时会只支持flash方式,不启用HTML5方式
runtimeOrder?: string;
//服务器回传数据中代表文件的字段名
responseVal: string;
//上传控件备注名称
info?: string;
//文件上传路径(接口地址)
server: string;
//预览上传后文件的根目录或接口地址
previewURL: string;
//MD5秒传设置,为真时会把体积大小超过md5SizeLimit的文件向md5URL发送文件信息并根据结果绝定是不是需要上传文件
md5Check: boolean;
//秒传验证的url
md5URL?: string;
//文件上传域,即在回传POST(GET)的内容中,哪个参数名包含文件
fileVal: string,
//falsh插件路径,初始化插件时需配置此参数,否则FLASH插件会失效
swf: string;
//可以上传文件的总数量限制,默认为1
fileNumLimit: number;
//是否显示可以上传文件的总数量限制文本 默认为 true 显示
isShowfileNumLimit?:boolean;
//单个文件大小限制(此处默认为10M)
fileSingleSizeLimit: number;
//插件总计可以上传多少字节的文件(100M)
fileSizeLimit: number;
//根据服务器回传值创建预览文件服务端地址的URL方法
createFileUrl: (responseVal: string) => string;
//自动开始上传
auto: boolean;
//文件上传方式 false为常规方式,true为启用二进制流
sendAsBinary: boolean;
//[默认值:false] 是否要分片处理大文件上传。
chunked?: boolean;
// [可选] [默认值:5242880] 如果要分片,分多大一片? 默认大小为5M.
chunkSize?: number;
// [可选] [默认值:2] 如果某个分片由于网络问题出错,允许自动重传多少次?
chunkRetry?: number;
//并发上传,默认就让一次传一个 多个需要服务支持
threads: number;
//图片模式
imageMode: boolean;
//支持拖拽模式
dndMode: boolean;
//支持剪切板粘贴
pasteMode: boolean;
//事件处理
listeners: {
//文件上传成功
uploadSuccess: (file: any, response: any) => void;
//文件上传错误
error: (msg: string) => void;
//整体上传完成
complate: () => void;
//结束事件 此处添加上传结束的回调处理函数
finished: () => void;
//此处放置开始上传时调用的事件
startUploader: () => void;
//删除文件事件
removeUploadedFile: (file: any) => void;
};
//允许的文件类型
accept: {
title: string;
extensions: string;
mimeTypes: string;
};
//随上传文件一起回传的参数
formData: any;
//把已存在的文件显示出来,用于在编辑状态下显示已存 的文件
serverFiles: Array<ShUploaderServerFile>;
}
/**
* 上传插件
*/
interface ShUploader {
//控件销毁方法
destroy: () => void;
}
interface JQueryStatic {
/**
* 初始化全局JQuery静态变量
*/
sh: {
uploader: ShUploader;
};
}
interface JQuery {
shUploader(options?: ShUploaderOption): ShUploader;
}
例子
//上传控件配置1
this.upload1 = $("#file_uploaer_1").shUploader({
//对应错误处理时使用的提示信息
errorMessage: {
"Q_EXCEED_NUM_LIMIT": "只能上传999张图片",
"Q_EXCEED_SIZE_LIMIT": "请上传2M以下的图片",
"Q_TYPE_DENIED": "上传图片格式为: gif jpg png",
"F_DUPLICATE": "您选择了重复的文件",
"F_EXCEED_SIZE": "请上传2M以下的图片"
},
//runtimeOrder: ‘flash‘, //此处设为flash时会只支持flash方式,不启用HTML5方式
inputName: "sh_uploader_val", //生成的input所使用的NAME
responseVal: "revisionId", //服务器回传数据中代表文件的字段名
info: ‘上传控件1‘,
server: formatUrl("/Uploads"), //文件上传路径(接口地址)
previewURL: formatUrl("/Files/R"), //预览上传后文件的根目录或接口地址
//MD5秒传设置,为真时会把体积大小超过md5SizeLimit的文件向md5URL发送文件信息并根据结果绝定是不是需要上传文件
md5Check: false,
md5URL: formatUrl("/CheckRepeat"),
fileVal: ‘file‘, //文件上传域,即在回传POST(GET)的内容中,哪个参数名包含文件
swf: formatUrl("/Scripts/SH.Plugin/uploader/webuploader-0.1.5/Uploader.swf"), //falsh插件路径,初始化插件时需配置此参数,否则FLASH插件会失效
fileNumLimit: 999, //可以上传文件的总数量限制,默认为1
fileSingleSizeLimit: 2 * 1048576, //单个文件大小限制(此处默认为10M)
fileSizeLimit: 10000 * 10485764, //插件总计可以上传多少字节的文件(100M)
//根据服务器回传值创建预览文件服务端地址的URL方法
createFileUrl(responseVal) {
return
this.previewURL + "/" + responseVal;
},
auto: true, //自动开始上传
sendAsBinary: true, //文件上传方式 false为常规方式,true为启用二进制流
chunked: true, //[默认值:false] 是否要分片处理大文件上传。
chunkSize: 1048576, // [可选] [默认值:5242880] 如果要分片,分多大一片? 默认大小为5M.
chunkRetry: 2, // [可选] [默认值:2] 如果某个分片由于网络问题出错,允许自动重传多少次?
threads: 1, //并发上传,默认就让一次传一个\
//图片模式
imageMode: true,
//支持拖拽模式
dndMode: true,
//支持剪切板粘贴
pasteMode: true,
//事件处理
listeners: {
uploadSuccess(file, response) {
file.filePath = response.data.revisionId;
//self.EdittingPlan().Spaces()[index].SpaceImages.push(response.data.revisionId);
self.EdittingPlan().FirstImage(response.data.revisionId);
},
error(msg) {
sh.alert(msg);
},
complate() {
//此处添加上传成功的回调处理函数
},
//结速事件
finished() {
//此处添加上传结束的回调处理函数
loading.close();
},
startUploader() {
//此处放置开始上传时调用的事件
loading.open("文件上传中...");
},
removeUploadedFile(file) {
self.EdittingPlan().FirstImage("");
}
},
//允许的文件类型
accept: {
title: ‘Images‘,
extensions: ‘gif,jpg,png‘,
mimeTypes: ‘image/*‘
},
//随上传文件一起回传的参数
formData: {},
//把已存在的文件显示出来,用于在编辑状态下显示已存 的文件
serverFiles: (() => {
var result = [];
if (this.EdittingPlan().FirstImage() !== "") {
result.push({
responseVal: this.EdittingPlan().FirstImage(),
name: "",
ext: "jpg"
});
}
return result;
})()
});
-
Menu.js
通过viewbag 中取过来的数据来初始化菜单
例子
需要引用 jquery tmpl.js
<script>
$(document).ready(function () {
initSitemap(@Html.Raw(ViewBag.treeData),@Html.Raw(ViewBag.SiteMapKeys));
});
</script>
<script
id="one"
type="text/x-jquery-tmpl">
<li
data-mapdata="${Name}">
<a
href="javascript:;">
<i
class="${Icon}"></i>
<span
class="title">
${Name}
</span>
<span
class="selected"></span>
<span
class="arrow"></span>
</a>
{{if
ChildMenuInfos!=null
&&
ChildMenuInfos.length>0}}
<ul
class="sub-menu">
{{tmpl(ChildMenuInfos)
‘#tow‘}}
</ul>
{{/if}}
</li>
</script>
<script
id="maptree"
type="text/x-jquery-tmpl">
<li> <a
href="javascript:;">
${$data}</a> </li>
<i
class="fa fa-angle-right"></i>
</script>
<script
id="tow"
type="text/x-jquery-tmpl">
<li
data-mapdata="${Name}">
<a
href="${formatUrl(Url)}">
<i
class="${Icon}"></i>
${Name}
</a>
{{if
ChildMenuInfos!=null
&&
ChildMenuInfos.length>0}}
<ul
class="sub-menu">
{{tmpl(ChildMenuInfos)
‘#tow‘}}
</ul>
{{/if}}
</li>
</script>
<!--站点地图容器-->
<ul
id="menuwarp"
class="page-sidebar-menu"
data-keep-expanded="false"
data-auto-scroll="true"
data-slide-speed="200"></ul>
-
linq.js
框架 外部引用非必须组件
可以在js中做 lambda 查询
例子
具体用法请 linq.d.ts;
Enumerable.From(this.SpaceSchemesItems()).Where(x => x.IsDefault()).Sum(x => x.TotalCostPrice());
前端规范
-
typescript/javascript规范
TSModel规范
- Model属性声明,要与后台模型一致(需要提交到后台反序列化的部分一定要一致。)
- 计算属性声明,要确定返回值类型,在ts模式下有时候需要强制标识
this.DefaultCount = ko.computed<number>(() => {
return <number>Enumerable.From(this.SpaceSchemesItems()).Count(d => d.IsDefault());
}, this);
- 开头字母大写,与后台模型属性名想对应.
- 构造函数要添加可空any类型参数。
- KnockoutModel模型绑定中枚举类型想绑定checked需要使用string类型。
下面附上标准demo
/**
* 套餐列表模型
*/
class PackageListModel {
/**
* 空间类型集合
*/
SpaceTypes: KnockoutObservableArray<string>;
/**
* 创建日期
*/
CreateDate: KnockoutObservable<string>;
/**
* 下架商品数量
*/
OffShelfCount: KnockoutObservable<number>;
/**
* 排序
*/
Sort: KnockoutObservable<number>;
/**
* 颜色
*/
Color: KnockoutObservable<string>;
/**
* id
*/
Id: KnockoutObservable<string>;
/**
* 状态枚举描述
*/
Status: KnockoutObservable<string>;
/**
* 状态枚举id
*/
StatusId: KnockoutObservable<number>;
/**
* 套餐类型 套餐/造型
*/
ModelType: KnockoutObservable<string>;
/**
* 套餐模式 基础/成品/基础+成品
*/
Mode: KnockoutObservable<string>;
constructor(model?: any) {
this.SpaceTypes = ko.observableArray([]);
if (model && model.SpaceTypes != null) {
for (var item of model.SpaceTypes) {
this.SpaceTypes.push(item);
}
}
this.CreateDate = ko.observable(model && model.CreateDate != null ? model.CreateDate : "");
this.OffShelfCount = ko.observable(model && model.OffShelfCount != null ? model.OffShelfCount : 0);
this.Sort = ko.observable(model && model.Sort != null ? model.Sort : 0);
this.Color = ko.observable(model && model.Color != null ? model.Color : "");
this.Id = ko.observable(model && model.Id != null ? model.Id : Guid.Empty);
this.Status = ko.observable<string>(model && model.Status != null ? model.Status : "未上架");
this.StatusId = ko.observable<number>(model && model.StatusId != null ? model.StatusId : 0);
}
}
命名规则
1)搜索关键字:小s开头
sPackName,sKeywords,sBilPack。
2)字典:如枚举状态等dic开头
dicShellStatus
3)普通属性,私有变量:驼峰命名
packName
4)列表集合数据源属性:list开头
listPacks
5)临时用缓存属性,如编辑中商品等:temp开头,一定要备注用途
/*
* 编辑商品弹窗绑定商品数据源对象
*/
tempProduct
6)弹窗:modal开头
modalCopySpaceScheme
viewModel中也如此声明
//复用选区弹窗对象
modalCopySpaceScheme: popups;
<div
id="modalCopySpaceScheme"
class="pop-modal">
<div
class="modal-header">
<h4
class="modal-title">选区复用</h4>
</div>
<div
class="modal-body">
</div>
<div
class="modal-footer">
<a
href="javascript:;"
class="btn blue"
data-bind="click:function(){eventConfirmCopy();}">确定</a>
<button
type="button"
class="btn btn-default btn_enter"
data-bind="click:modalCopySpaceScheme.hide">取消</button>
</div>
</div>
Typescript书写规范
1)属性与构造相对应:ts中声明的属性是抽象的,需要在构造中实例化。
class PackageListModel {
/**
* 空间类型集合
*/
SpaceTypes: KnockoutObservableArray<string>;
constructor(model?: any) {
this.SpaceTypes=ko.observableArray([]);
}
2)this作用域:无法区分this作用域时。
var self=this;
-
html/cshtml规范
html书写规范遵循语义化的标准写法
比如声明一个按钮<button class="btn btn-default">确定</button>
尽量不要写 <a href="javascript:;" class="btn btn-default">确定</a>
1) 容器布局:遵循bootstrap的标签套用原则,不应有多余标签。
所有内容都应放在row下的col里。
2) 标签页:在Metronic的布局标准下,标签页的容器应为row和col。
代码详见Metronic模版。
3) 页面html行数较多时要添加region标签。Ctrl+k,s
4) Layout布局容器应在.container 里
5)搜索框应用panel包裹
3、 HTML元素命名规范
标签 |
命名 |
<input type="text" /> |
txtName |
<select></select> |
selProjectState |
<texarta></texarea> |
textProductDesc |
<label></lable> |
lbPrice |
<div></div> |
divProjectFile |
<span></span> |
spSKUPro |
标签页 |
tabUserManage |
模态框 |
modalAddUser |
遮罩层 |
dialogLoading |
-
controller business model 规范
- Controller 负责相应页面请求,给页面传入字典和接收页面返回值的作用。
2)Bussiness调用服务接口,简单逻辑处理,模型转换等。
-
knockout 组件规范
组件 在项目中被命名为UserControl
- userControl编码规则详见viewmodel,他们的声明方式类似,传入参数上只有prarms。
- userControl最下面一行需要调用控件注册,并指定模版路径。
以下是通用注册方法,在webutil中声明。
-
viewModel规范
下图是一个ts版的demo
viewModel 由4部分组成
- 属性:包含页面所有需要的数据源,临时属性,搜索条件属性等
- 构造:构造中会初始化所有属性声明、字典数据、页面init方法、验证控件、模态框等。
- 方法:方法声明的原则为数据交互使用,所有请求controler获取或设置数据的方法(WebUtil.ajax)都写在方法里。
- 事件:页面所有元素的事件绑定usercontrol回调,如搜索按钮点击,下拉框等。
属性上都应有注释,如果有依赖关系 可以用 region 扩起来。
下面是方法的例子,这些方法都用于数据交互。(ts中不写返回类型默认为 void)
事件一定要注明用途,控件回调的事件也在这个区域声明
控件回调事件写法