Knockout Mvc Compoment FrameSet With Typescript
框架简介
以下简称为 kot。kot是一套基于knockout jquery bootstrap(Metronic) typescript 的可快速推广的完整前端框架。
框架优势
框架文件结构
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、业务文件结构
4、单元测试文件结构
框架基础
登录原理
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 响应,来生成菜单和面包屑导航。
对应的模板在 /Views/ Shared/ PartialViews/ _Left.cshtml 中。
js控件
接口
interface WebUtilAjaxOption extends JQueryAjaxSettings {
encodeHtml(str: string): string;
WebUtil.encodeHtml("<div></div>"); 返回string类型。
stopEvent(event: any): void;
parseDate(value: string): Date;
var str="2015-09-10 18:30"; var date = WebUtil.parseDate(str);
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;
一个在分页状态下获取连续序号的方法。这里$parent 是knockout上下文对象。$index是knockout循环上下文中的索引。
<tbody data-bind="foreach:{data:dataList,as:'item'}">
declare var WebUtil: WebUtilStatic;
alert(msg: string, callback?: () => void, msgtitle?: string): void;
2)sh.alert("操作完成!",function(){console.log("数据操作完成") });
3)sh.alert("操作完成!",function(){console.log("数据操作完成") },"DaTree提示");
confirm(msg: string, yescallback: () => void, nocallback?: () => void, msgtitle?:
string): void;包含确定取消按钮的提示框:title 默认为"系统提示"
1)sh.confirm("确定要这么做吗?",function(){ console.log("点击了确认") });
2)sh.confirm("确定要这么做吗?",function(){ console.log("点击了确认") },function(){console.log("点击了取消")});
3) sh.confirm("确定要这么做吗?",function(){ console.log("点击了确认") },function(){console.log("点击了取消")},"DaTree提示自定义标题");
4) 不要取消事件可以传null
sh.confirm("确定要这么做吗?",function(){ console.log("点击了确认") },null,"DaTree提示自定义标题");
declare var sh: shStatic;
interface KnockoutStatic {
/**
* 注册控件通用方法
* @param controlName
* @param viewModel
* @param templateUrl
* @returns {}
*/
RegisterControl(controlName: string, viewModel: any, templateUrl: string): void;
1)通用注册控件方法
//注册控件
ko.RegisterControl("priceconfigcontrol", PriceConfigControlViewModel, formatUrl("/UserControls/PackageConfig/PriceConfigControl/PriceConfigControlView.html"));
2)registerControl方法会造成很多次异步html请求,正在想办法解决。
formatCurrency
/**
* 将数字转换为 格式化后的金钱字符串
* @param num
*/
declare function formatCurrency(num: number): string;
- 使用场景一般是在html绑定一个金钱格式的数字时用的。
<div class="col-md-3 ">
<p class="form-control-static">
套餐必选成本合计:<!--ko text:formatCurrency(priceConfigModel().RequiredCostTotalPrice())--><!--/ko-->元
</p>
</div>
formatUrl
全局处理虚拟目录的方法。在layout上实现。
/**
* 处理虚拟目录格式化地址的方法 在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;
比如当前网站是在虚拟目录下
http://www.baidu.com/myWebSite/
那么在调用url时 需要加上 formatUrl();
$.ajax({
url:formatUrl("/package/getpackage")
data:{}
});
这里 formatUrl返回为 /myWebSite/package/getpackage
Guid
/**
* 定义一个Guid接口
*/
interface GuidStatic {
Empty: string;
}
/**
* 定义一个Guid静态类
*/
declare var Guid: GuidStatic;
Guid在js中没有默认值,有的时候后台参数需要 反序列化一个 Id类型,所以添加了一个Guid默认值
下面为demo
constructor(model?) {
this.Id = ko.observable(model && model.Id != null ? model.Id : Guid.Empty);
}
string.format
/**
* 定义String静态方法
*/
interface StringConstructor {
format: (...args: any[]) => string;
}
这里与C#中静态调用稍有不同。
C#中 : string str=string.Format("今天是{0}国庆节","10月1号");
js中: var str="今天是{0}国庆节".format("10月1号");
Date通用
简单例子: var date1=new Date();
date1.format("yyyy-mm-dd HH:mm");
/**
* 时间通用处理
*/
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
一般用于字典类型的数据处理
Demo
/**
* 选区类型字典
*/
categoryAreaDic: KnockoutObservableArray<KeyValuePair2<string, string>>;
api
/**
* 键值对
*/
interface KeyValuePair2<TKey, TValue> {
Key: TKey;
Value: TValue;
}
/**
* 键值对参数对象
*/
interface IKeyVaulePair {
Key: any;
Value: any;
}
/**
* 键值对委托方法
*/
declare var KeyValuePair2: (obj: IKeyVaulePair) => void;
EventBus事件总线
/**
* 事件总线接口
*/
interface EventBusStatic {
/**
* 注册事件
* @param option
* @returns {}
*/
registerEvent(option: EventBusOption): EventBusStatic;
/**
* 调用事件
* @param eventID
* @param args 参数列表
* @returns void
*/
callEvent(eventID: string, ...args: Array<any>): void;
/**
* 打印事件列表
* @returns {}
*/
print(): Array<EventBusOption>;
/**
* 删除事件
* @param eventID
* @returns {}
*/
removeEvent: (eventID: string) => void;
/**
* 验证事件是否存在
* @param eventID
* @returns true存在
*/
eventExist: (eventID: string) => boolean;
}
/**
* 事件总线对象
*/
declare var EventBus: EventBusStatic;
demo
var obj1 = { name: "123", say: function (title, msg) { debugger; alert(title + ":" + msg + "name:" + this.name); } };
EventBus.registerEvent({ eventHandler: obj1, eventName: "say2", eventId: "s1", eventBody: obj1.say });
EventBus.registerEvent({ eventHandler: obj1, eventName: "say2", eventId: "s2", eventBody: obj1.say });
EventBus.registerEvent({ eventHandler: obj1, eventName: "say2", eventId: "s3", eventBody: obj1.say });
EventBus.callEvent("s2", "标题", "消息");
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() {
}
},
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中声明。
Demo见viewModel。
viewModel规范
下图是一个ts版的demo
viewModel 由4部分组成
- 属性:包含页面所有需要的数据源,临时属性,搜索条件属性等
- 构造:构造中会初始化所有属性声明、字典数据、页面init方法、验证控件、模态框等。
- 方法:方法声明的原则为数据交互使用,所有请求controler获取或设置数据的方法(WebUtil.ajax)都写在方法里。
- 事件:页面所有元素的事件绑定usercontrol回调,如搜索按钮点击,下拉框等。
属性上都应有注释,如果有依赖关系 可以用 region 扩起来。
下面是方法的例子,这些方法都用于数据交互。(ts中不写返回类型默认为 void)
事件一定要注明用途,控件回调的事件也在这个区域声明。
下面有一个事件叫 eventShowRelation 显示关联套餐弹窗,其中有一行代码,this.modalRelationPackage.show();
这行代码 调用的就是弹窗控件的方法(详见,js控件下的 jquery.sh.popups.js的api)。
控件回调事件写法
控件初始化绑定方法方法。
Knockout的组件可以实现自定义html标签的功能。
Knockout 控件库
PagingControl
用于knockout通用分页。
文件结构
paging.css 控件样式表, PagingControlViewModel,主要逻辑文件,PagingControlView.html控件绑定的html模板。
例子
首先文件引用,在引用了 jquery.js、knockout.js、loading.js、popups.js、webutil.js、的基础上,引用如下文件。
viewModel 构造里面其实还有一些参数不过被我删除了,一般都是字典的传入,对控件本身没影响。
文件引用
<!--paging control-->
<link href="~/UserControls/PagingControl/paging.css" rel="stylesheet" />
<script src="~/UserControls/PagingControl/PagingControlModel.js"></script>
<!--main viewModel 页面主viewModel文件-->
<script src="~/ViewModels/PackageConfig/PackageConfigListViewModel.js"></script>
<script>
$(function () {
ko.applyBindings(new PackageListViewModel());
});
</script>
viewModel
首先是模型,不必关注具体有什么,你要在列表上显示哪些列就写什么属性就可以了。
然后主viewModel声明 一个 KnockoutObservableArray 类型的属性。
/**
* 列表数据源
*/
listPackage: KnockoutObservableArray<PackageListModel>;
在构造中实现声明。
this. listPackage = ko.observableArray<PackageListModel>([]).extend({
paging: {
pageIndex: 1,
pageSize: 10,
callback() {
self.fnGetData();
}
}
});
注意后面的 extend 是扩展 这个 obarray的。
几个必要的分页参数 pageIndex pageSize,主要是这个callback,这个callback的作用就是当你点击页码时执行的方法。
原理就是在点击页码后执行callback里的 fnGetData();方法来刷新 listPackage这个数据源(koarray,数据源更新页面html也会自动更新。)
Ajax Data 参数中事一些搜索用的条件用于获取数据用。分页控件主要还是关注 pageIndex和pageSize
/**
* 获取套餐列表数据源
*/
fnGetData() {
var self = this;
WebUtil.ajax({
url: formatUrl("/PackageConfig/GetPackageList"),
data: {
pageIndex: self.dataList.pageIndex(),
pageSize: self.dataList.pageSize(),
keywords: self.keywords()
},
type: "post",
dataType: "json",
success(data) {
if (data) {
//设置总条数 需要后台返回当前这个查询有多少条数据 以便控件计算页码
self.listPackage.SetPageTotal(data.RowCount);
//清空之前的缓存
self. listPackage.removeAll();
//循环添加
for (var item of data.Datas) {
//记得一定不要忘记 new koModel
self.dataList.push(new PackageListModel(item));
}
}
}
});
}
Html绑定
我把完整的结构粘贴下来,实际上只有2行高亮显示的代码是用于pagingcontrol。
思路就是一个table 下的tbody 中 循环列出模型中的项。加一个分页控件的绑定。
(注意代码中 <!—ko *** --><!— /ko--> )是无容器绑定
<div class="table-responsive">
<table class="table table-bordered table-advance">
<thead>
<tr>
<th>
序号
</th>
<th>
模板名称
</th>
<th>
总面积(㎡)
</th>
<th>
<select class="" data-bind="options:packageTypesDic,optionsText:'Value',optionsValue:'Key',optionsCaption:'套餐类型',value:packageType,event:{change:eventSearch}"></select>
</th>
<th>
<select class="" data-bind="options:packageStatesDic,optionsText:'Value',optionsValue:'Key',optionsCaption:'状态',value:packageState,event:{change:eventSearch}"></select>
</th>
<th>
创建时间
</th>
<th>
下架工艺/商品
</th>
<th>
排序
</th>
<th>
竞品标签
</th>
<th>
套餐类别
</th>
<th>
套餐模式
</th>
<th style="width: 100px;">
操作
</th>
</tr>
</thead>
<tbody data-bind="foreach:{data:listPackage,as:'item'}">
<tr>
<td>
<!--通用获取序号方法-->
<p class="form-control-static"><!--ko text:WebUtil.getNum($index(),$parent.dataList.pageIndex(),$parent.dataList.pageSize())--><!--/ko--></p>
</td>
<td>
<p class="form-control-static"> <!--ko text:item.Name--><!--/ko--></p>
</td>
<td>
<p class="form-control-static"> <!--ko text:formatCurrency(item.Area())--><!--/ko--></p>
</td>
<td>
<p class="form-control-static"><!--ko text:item.PackageType--><!--/ko--></p>
</td>
<td>
<p class="form-control-static"> <!--ko text:item.Status--><!--/ko--></p>
</td>
<td>
<p class="form-control-static"> <!--ko text:item.CreateDate--><!--/ko--></p>
</td>
<td>
<p class="form-control-static"><!--ko text:item.OffShelfCount--><!--/ko--></p>
</td>
<td style="width: 90px;">
<input type="text" style="width: 100%;" class="{required:true,integer:true,min:1,max:999999999}" data-bind="value:item.Sort,event:{change:function(){$parent.eventSaveSort($element,item.Id());}}" value="" />
</td>
<td>
<div class="colortag" data-bind="style:{'background-color':'#'+item.Color()}"></div>
</td>
<td>
<p class="form-control-static"><!--ko text:item.ModelType--><!--/ko--></p>
</td>
<td>
<p class="form-control-static"><!--ko text:item.Mode--><!--/ko--></p>
</td>
<td>
<div class="btn-group btn-group-solid btn-group-sm" style="position: absolute;">
<button type="button" class="btn blue dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
<i class="fa fa-ellipsis-horizontal"></i> 操作 <i class="fa fa-angle-down"></i>
</button>
<ul class="dropdown-menu pull-right">
<li>
<!-- #region if-->
<!-- ko if:item.StatusId()==1 -->
<a href="javascript:;" data-bind="click:function(){$parent.eventOffShelf(item.Id())}">
下架
</a>
<!--/ko-->
<!-- #endregion endif-->
<!-- #region if -->
<!-- ko if:item.StatusId()!=1 -->
<a href="javascript:;" data-bind="click:function(){$parent.eventOnShelf(item.Id())}">
上架
</a>
<!--/ko-->
<!-- #endregion endif-->
</li>
<li>
<a href="javascript:;" data-bind="attr:{href:formatUrl('/PackageConfig/Edit?packageId='+item.Id()+'&sitemap='+encodeURI('套餐研发~套餐设计器~编辑套餐'))}">
编辑
</a>
</li>
<li>
<a href="javascript:;" data-bind="click:function(){$parent.eventShowCopy(item)}">
复制
</a>
</li>
<!-- #region if -->
<!-- ko if:item.StatusId()==0-->
<li>
<a href="javascript:;" data-bind="click:function(){$parent.eventDelPackage(item.Id())}">
删除
</a>
</li>
<!--/ko-->
<!-- #endregion endif-->
<li>
<a data-bind="attr:{href:formatUrl('/PackageConfig/TagManage?packageId='+item.Id()+'&sitemap='+encodeURI('套餐研发~套餐设计器~标签管理'))}">
标签管理
</a>
</li>
<li>
<a href="javascript:;" data-bind="click:function(){$parent.eventShowRelation(item.Id(),item.Name())}">
套餐关联
</a>
</li>
</ul>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<pagingcontrol params="{paging:listPackage}"></pagingcontrol>
环境搭建
依赖包:
Install-Package jquery.TypeScript.DefinitelyTyped -Version 1.9.9
Install-Package jQuery -Version 1.11.1
Install-Package linq.TypeScript.DefinitelyTyped
Install-Package linq.js -Version 2.2.0.2
Install-Package knockout.TypeScript.DefinitelyTyped
Install-Package knockoutjs -Version 3.4.0
依赖插件:
vs2015及以上*
https://www.tslang.cn/index.html#download-links * 下载 对应版本
http://www.vswebessentials.com/ (常用工具)
resharper 10 以上(智能提示)
WebCompiler(less编译)
问题汇总
- 如果是 固定标签之内的组件绑定 需要使用无容器方式,不能使用自定义标签方式。
- 组件绑定时在params上传递的值必须是监听属性(也就是说productId,切记不能 使用productId())这样做在productId更新时,组件会被重置。
如果有一天你跟断点事遇到一个component被无限初始化的时候,先看一下是不是传入参数有问题。
- 组件支持套用,可以组件套组件,今天遇到了一个情况(外层组件被初始化2次,而内部组件初始化不走),后来仔细检查,发现内层组件的register时传入的viewModel是外层组件的,这是个低级错误。
写组件时不要忘记检查 组件名,viewModel和模板路径。
更新日志
2017-04-06 09:02:43 添加环境搭建教程,添加项目问题汇总,添加更新日志记录。
2017-04-19 10:57:17 问题汇总更新了2个问题。WebUtil添加事件总线机制,component之间的交互在也不用来回传递事件了。