分類
發燒車訊

基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(一)

系列文章

  1. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用 abp cli 搭建項目
  2. 基於 abp vNext 和 .NET Core 開發博客項目 – 給項目瘦身,讓它跑起來
  3. 基於 abp vNext 和 .NET Core 開發博客項目 – 完善與美化,Swagger登場
  4. 基於 abp vNext 和 .NET Core 開發博客項目 – 數據訪問和代碼優先
  5. 基於 abp vNext 和 .NET Core 開發博客項目 – 自定義倉儲之增刪改查
  6. 基於 abp vNext 和 .NET Core 開發博客項目 – 統一規範API,包裝返回模型
  7. 基於 abp vNext 和 .NET Core 開發博客項目 – 再說Swagger,分組、描述、小綠鎖
  8. 基於 abp vNext 和 .NET Core 開發博客項目 – 接入GitHub,用JWT保護你的API
  9. 基於 abp vNext 和 .NET Core 開發博客項目 – 異常處理和日誌記錄
  10. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用Redis緩存數據
  11. 基於 abp vNext 和 .NET Core 開發博客項目 – 集成Hangfire實現定時任務處理
  12. 基於 abp vNext 和 .NET Core 開發博客項目 – 用AutoMapper搞定對象映射
  13. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(一)
  14. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(二)
  15. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(三)
  16. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(一)
  17. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(二)
  18. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(三)
  19. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(四)
  20. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(五)

前言

從今天開始將使用 Blazor 完成博客的前端開發,如果你不了解 Blazor ,建議你還是去微軟官網學習學習基礎知識。本篇不做普及,因為這是實戰系列,重點是完成項目開發。

還有,在開始 Blazor 實戰之前,建議動手完成之前的系列文章,這樣更有連貫性,不至於懵圈。

因為我也是第一次使用 Blazor 開發項目,所以無法保證代碼的最優性,如果有不對的地方,或者有更好的做法,歡迎大家指正,謝謝。

接下來,我將現學現做帶來一個完整的博客項目,來吧,Just do it 。

我這裏選擇的是 Blazor WebAssembly,需要你有 .NET Core 3.1 的開發環境,並且你還要有 Visual Studio 2019 IDE。

給大家看看我的開發環境,終端工具是:Window Terminal ,配置一下用起來太爽了,五星強烈推薦。

搭建

Blazor WebAssembly 是一個單頁應用框架,可用它通過 .NET 生成交互式客戶端 Web 應用。 Blazor WebAssembly 使用開放的 Web 標準(沒有插件或代碼轉換),適用於移動瀏覽器等各種新式 Web 瀏覽器。

不啰嗦了,直接開干吧,在項目中新建 Blazor Web 應用。

然後將項目設置為啟動項目,Ctrl+F5 運行一下看看,官網默認示例我這裏也懶得說了,直接進入主題吧。

改造

我這裏使用的UI還是我目前博客的樣式,你可以選擇任意你喜歡的UI界面,這部分就隨意了,不是本實戰系列的重點,所以有關樣式這些東西我就直接 Ctrl CV 了。

替換下面css代碼到 wwwroot/css/app.css 中。

點擊查看代碼

*,
*:after,
*:before {
	-webkit-box-sizing: border-box;
	-moz-box-sizing: border-box;
	box-sizing: border-box;
}

html {
	line-height: 1.15;
	-webkit-text-size-adjust: 100%;
}

body {
	margin: 0;
}

h1 {
	font-size: 2em;
	margin: 0.67em 0;
}

hr {
	box-sizing: content-box;
	height: 0;
	overflow: visible;
}

pre {
	font-family: monospace, monospace;
	font-size: 1em;
}

a {
	background-color: transparent;
}

abbr[title] {
	border-bottom: none;
	text-decoration: underline;
	text-decoration: underline dotted;
}

b,
strong {
	font-weight: bolder;
}

code,
kbd,
samp {
	font-family: monospace, monospace;
	font-size: 1em;
}

small {
	font-size: 80%;
}

sub,
sup {
	font-size: 75%;
	line-height: 0;
	position: relative;
	vertical-align: baseline;
}

sub {
	bottom: -0.25em;
}

sup {
	top: -0.5em;
}

img {
	border-style: none;
}

button,
input,
optgroup,
select,
textarea {
	font-family: inherit;
	font-size: 100%;
	line-height: 1.15;
	margin: 0;
}

button,
input {
	overflow: visible;
}

button,
select {
	text-transform: none;
}

button,
[type="button"],
[type="reset"],
[type="submit"] {
	-webkit-appearance: button;
}

button::-moz-focus-inner,
    [type="button"]::-moz-focus-inner,
    [type="reset"]::-moz-focus-inner,
    [type="submit"]::-moz-focus-inner {
	border-style: none;
	padding: 0;
}

button:-moz-focusring,
    [type="button"]-moz-focusring,
    [type="reset"]-moz-focusring,
    [type="submit"]-moz-focusring {
	outline: 1px dotted ButtonText;
}

fieldset {
	padding: 0.35em 0.75em 0.625em;
}

legend {
	box-sizing: border-box;
	color: inherit;
	display: table;
	max-width: 100%;
	padding: 0;
	white-space: normal;
}

progress {
	vertical-align: baseline;
}

textarea {
	overflow: auto;
}

[type="checkbox"],
[type="radio"] {
	box-sizing: border-box;
	padding: 0;
}

[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
	height: auto;
}

[type="search"] {
	-webkit-appearance: textfield;
	outline-offset: -2px;
}

    [type="search"]::-webkit-search-decoration {
	-webkit-appearance: none;
}

::-webkit-file-upload-button {
	-webkit-appearance: button;
	font: inherit;
}

details {
	display: block;
}

summary {
	display: list-item;
}

template {
	display: none;
}

[hidden] {
	display: none;
}

@font-face {
	font-family: 'Fira Code Medium';
    src: url('https://static.meowv.com/fonts/FiraCode-Medium.woff2') format('woff2'), url("https://static.meowv.com/fonts/FiraCode-Medium.woff") format("woff");
    font-weight: 500;
    font-style: normal;
}

html {
	font-family: 'Fira Code Medium', Microsoft Yahei, monospace;
	overflow-x: hidden;
}

html::-webkit-scrollbar {
	width: 5px;
	height: 5px;
}

html::-webkit-scrollbar-thumb {
	height: 20px;
	background-color: #5A9600;
}

html::-webkit-scrollbar-thumb:hover {
	background-color: #5A9600;
}

body {
	font-size: 11pt;
	font-weight: normal;
	line-height: 2em;
	background-color: #fff;
	color: #161209;
	transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease, opacity 0.2s ease;
}

body:before {
	content: "";
	background-repeat: no-repeat;
	background-position: center;
	opacity: 0.05;
	position: fixed;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	z-index: -1;
}

body.dark-theme {
	background-color: #292a2d;
	color: #a9a9b3;
	transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease, opacity 0.2s ease;
}

a {
	color: #161209;
	text-decoration: none;
	transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease, opacity 0.2s ease;
	cursor: pointer;
}

a:hover {
	color: #5A9600;
	text-decoration: none;
	transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease, opacity 0.2s ease;
}

.dark-theme a {
	color: #a9a9b3;
	text-decoration: none;
	transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease, opacity 0.2s ease;
}

.dark-theme a:hover {
	color: #fff;
	text-decoration: none;
	transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease, opacity 0.2s ease;
}

.wrapper {
	display: flex;
	flex-direction: column;
	min-height: 100vh;
	width: 100%;
}

.navbar {
	height: 4rem;
	line-height: 4rem;
	width: 100%;
}

.navbar .container {
	width: auto;
	max-width: 1200px;
	text-align: center;
	margin: 0 auto;
	display: flex;
	justify-content: space-between;
}

.main {
	flex-grow: 1;
	flex-shrink: 0;
	flex-basis: auto;
}

.container {
	padding-left: 1em;
	padding-right: 1em;
}

.footer {
	width: 100%;
	text-align: center;
}


/*input css begin*/

* {
	-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}

input[type="checkbox"],
input[type="radio"] {
	display: none;
	width: 0;
	height: 0;
	visibility: hidden;
}

input[type="checkbox"]:checked + label:after {
	transition: all 0.3s ease-in;
}

input[type="checkbox"]:not(:checked) + label:after {
	transition: all 0.3s ease-out;
}

input[type="checkbox"]:checked + label,
    input[type="checkbox"]:not(:checked) + label {
	transition: all 0.3s ease-in-out;
}

input[type="checkbox"]:checked + label:before,
        input[type="checkbox"]:checked + label i:before,
        input[type="checkbox"]:not(:checked) + label i:before,
        input[type="checkbox"]:checked + label i:after,
        input[type="checkbox"]:not(:checked) + label i:after,
        input[type="checkbox"]:not(:checked) + label:before {
	transition: all 0.3s ease-in-out;
}

input[type="radio"]:checked + label:after,
    input[type="radio"]:not(:checked) + label:after {
	transition: all 0.3s ease-in-out;
}

.switch_default + label {
	background-color: #e6e6e6;
	border-radius: 7px;
	cursor: pointer;
	display: inline-block;
	height: 14px;
	position: relative;
	box-shadow: 0.2px 0.2px 1px 0.5px rgb(180, 180, 180);
	width: 30px;
}

.switch_default + label:after {
	background-color: #fff;
	border-radius: 50%;
	content: "";
	height: 12px;
	left: 1px;
	position: absolute;
	top: .5px;
	width: 12px;
	box-shadow: 0.2px 0.2px 1px 0.5px rgb(180, 180, 180);
}

.switch_default:checked + label {
	background-color: #1ABC9C;
	box-shadow: none;
}

.switch_default:checked + label:after {
	left: 17px;
}

@font-face {
	font-family: "iconfont";
    src: url('//at.alicdn.com/t/font_1313145_r9szngeugmj.eot?t=1566619028667'); /* IE9 */
    src: url('//at.alicdn.com/t/font_1313145_r9szngeugmj.eot?t=1566619028667#iefix') format('embedded-opentype'), /* IE6-IE8 */
    url('data:application/x-font-woff2;charset=utf-8;base64,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') format('woff2'), url('//at.alicdn.com/t/font_1313145_r9szngeugmj.woff?t=1566619028667') format('woff'), url('//at.alicdn.com/t/font_1313145_r9szngeugmj.ttf?t=1566619028667') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
    url('//at.alicdn.com/t/font_1313145_r9szngeugmj.svg?t=1566619028667#iconfont') format('svg'); /* iOS 4.1- */
}

.iconfont {
	font-family: "iconfont" !important;
	font-size: 16px;
	font-style: normal;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
}

.iconread:before {
	content: "\e742";
}

.iconweixin:before {
	content: "\e632";
}

.iconmanage:before {
	content: "\e610";
}

.iconapi:before {
	content: "\e668";
}

.iconcode:before {
	content: "\e654";
}

.icongithub:before {
	content: "\ea0a";
}

.iconnotes:before {
	content: "\e687";
}

.header-logo a {
	padding: 0;
}

.navbar .menu a {
	padding: 0 8px;
}

.navbar .menu .active {
	font-weight: 900;
	color: #161209;
}

.dark-theme .navbar .menu .active {
	color: #fff;
}

.navbar-header a:hover,
.navbar .menu a:hover {
	background-color: transparent;
}

header label {
	margin-left: 15px;
	position: relative;
	-webkit-transform: translateY(0.1em) translateX(0.5em);
}

.copyright {
	font-size: 14px;
}

.pagination {
	display: flex;
	flex-direction: row;
	justify-content: center;
	list-style: none;
	white-space: nowrap;
	width: 100%;
	padding-top: 2em;
}

.pagination a,
    .pagination span {
	-webkit-font-smoothing: antialiased;
	font-size: 12px;
	color: #bfbfbf;
	letter-spacing: 0.1em;
	font-weight: 700;
	padding: 5px 5px;
	text-decoration: none;
	transition: 0.3s;
}

.pagination .page-number {
	padding-bottom: 3px;
	margin: 0 20px;
	box-sizing: border-box;
	position: relative;
	display: inline;
}

.pagination .page-number.disabled {
	display: none;
}

.pagination .page-number:hover a {
	color: #000;
}

.dark-theme .pagination .page-number:hover a {
	color: #fff;
}

.pagination .page-number:before,
.pagination .page-number:after {
	position: absolute;
	content: "";
	width: 0;
	height: 1px;
	background: #000;
	transition: 0.3s;
	bottom: 0px;
}

.dark-theme .pagination .page-number:before,
.dark-theme .pagination .page-number:after {
	background: #fff;
}

.pagination .page-number:before .current,
.pagination .page-number:after .current {
	width: 100%;
}

.pagination .page-number:before {
	left: 50%;
}

.pagination .page-number:after {
	right: 50%;
}

.pagination .page-number:hover:before,
.pagination .page-number:hover:after {
	width: 50%;
}

.pagination .page-number.current {
	color: #000;
}

.dark-theme .pagination .page-number.current {
	color: #fff;
}

.pagination .page-number.current:before,
.pagination .page-number.current:after {
	width: 60%;
}

.intro {
	transform: translateY(20vh);
	text-align: center;
}

.intro .avatar {
	padding: 10px;
}

.intro .avatar img {
	width: 128px;
	height: auto;
	display: inline-block;
	-webkit-border-radius: 100%;
	border-radius: 100%;
	-webkit-box-shadow: 0 0 0 0.3618em rgba(0, 0, 0, 0.05);
	box-shadow: 0 0 0 0.3618em rgba(0, 0, 0, 0.05);
	margin: 0 auto;
	-webkit-transition: all ease 0.4s;
	-moz-transition: all ease 0.4s;
	-o-transition: all ease 0.4s;
	transition: all ease 0.4s;
	cursor: pointer;
}

.intro .avatar img:hover {
	position: relative;
	-webkit-transform: translateY(-0.75em);
	-moz-transform: translateY(-0.75em);
	-ms-transform: translateY(-0.75em);
	-o-transform: translateY(-0.75em);
	transform: translateY(-0.75em);
	cursor: pointer;
}

.nickname {
	font-size: 2em;
	font-weight: normal;
}

.links a {
	padding: 0 5px;
}

.links a:hover {
	background-color: transparent;
}

.links .iconfont {
	font-size: 2em;
}

.post-wrap {
	position: relative;
	width: 100%;
	max-width: 1024px;
	margin: 0 auto;
	padding-top: 2rem;
}

.archive-item-date {
	float: right;
	text-align: right;
	color: #a9a9b3;
}

.dark-theme .archive-item-date {
	color: #87878d;
}

.post-wrap .categories-card {
	margin: 0 auto;
	margin-top: 1em;
	display: flex;
	align-items: center;
	justify-content: space-between;
	flex-direction: row;
	flex-wrap: wrap;
	padding: 0 2.5em;
	line-height: 1.6em;
}

.post-wrap .categories-card .card-item {
	font-size: 14px;
	text-align: left;
	width: 50%;
	display: flex;
	align-items: flex-start;
	position: relative;
}

.post-wrap .categories-card .card-item .categories {
	overflow: hidden;
}

.categories h3 {
	display: inline-block;
}

.categories span {
	float: right;
	padding-right: 1em;
}

.categories .more-post-link {
	float: right;
}

.tag-cloud-tags {
	margin: 10px 0;
	padding-top: 2em;
}

.tag-cloud-tags a {
	display: inline-block;
	position: relative;
	margin: 5px 10px;
	word-wrap: break-word;
	transition-duration: 0.3s;
	transition-property: transform;
	transition-timing-function: ease-out;
}

.tag-cloud-tags a:active,
        .tag-cloud-tags a:focus,
        .tag-cloud-tags a:hover {
	color: #5A9600;
	transform: scale(1.1);
}

.dark-theme .tag-cloud-tags a:active,
.dark-theme .tag-cloud-tags a:focus,
.dark-theme .tag-cloud-tags a:hover {
	color: #fff;
}

.tag-cloud-tags a small {
	margin: 0 0.3em;
	color: #a9a9b3;
}

.dark-theme .tag-cloud-tags a small {
	color: #fff;
}

.page {
	padding-top: 0;
}

.page .post-content {
	margin: 0;
	padding-top: 0;
}

.post-wrap p {
	font-size: 1em;
	margin: 0.5em 0 0.5em 0;
}

.post-wrap .post-header h1 {
	margin: 0 !important;
}

.post-wrap .post-title {
	font-size: 2em;
	line-height: 1.5em;
}

.post-wrap .eror-tip {
	text-align: center;
	line-height: 1.5em;
	margin-top: 250px;
}

.post-wrap .post-meta {
	color: rgba(85, 85, 85, 0.529) !important;
}

.dark-theme .post-wrap .post-meta {
	color: #87878d !important;
}

.post-wrap .post-meta a {
	color: #000;
}

.dark-theme .post-wrap .post-meta a {
	color: #eee;
}

.post-wrap .post-meta a:hover {
	color: #5A9600;
}

.dark-theme .post-wrap .post-meta a:hover {
	color: #fff;
}

.post-content {
	padding-top: 2rem;
	text-align: justify;
}

.post-copyright {
	margin-top: 5rem;
	border-top: 1px solid #e8e8e8;
	border-bottom: 1px solid #e8e8e8;
}

.post-copyright a {
	color: #000;
}

.dark-theme .post-copyright a {
	color: #eee;
}

.post-copyright a:hover {
	color: #5A9600;
}

.dark-theme .post-copyright a:hover {
	color: #fff;
}

.post-copyright .copyright-item {
	margin: 5px 0;
}

.post-copyright .lincese {
	font-weight: bold;
}

.dark-theme .post-copyright {
	border-top: 1px solid #909196;
	border-bottom: 1px solid #909196;
}

.post-tags {
	padding: 1rem 0 1rem;
	display: flex;
	justify-content: space-between;
}

.post-nav:before,
.post-nav:after {
	content: " ";
	display: table;
}

.post-nav a.prev,
.post-nav a.next {
	font-weight: 600;
	font-size: 16px;
	transition-property: transform;
	transition-timing-function: ease-out;
	transition-duration: 0.3s;
}

.post-nav a.prev {
	float: left;
}

.post-nav a.prev::before {
	content: "<";
	margin-right: 0.5em;
}

.post-nav a.prev:hover {
	transform: translateX(-4px);
}

.post-nav a.next {
	float: right;
}

.post-nav a.next::after {
	content: ">";
	margin-left: 0.5em;
}

.post-nav a.next:hover {
	transform: translateX(4px);
}

.post-nav a.prev::before,
    .post-nav a.next::after {
	font-weight: bold;
}

.tag:not(:last-child) a::after {
	content: " / ";
}

@media only screen and (min-device-width: 320px) and (max-device-width: 1024px) {
	.main {
		padding-top: 40pt;
	}

	.navbar {
		display: none;
	}

	.navbar-mobile {
		display: block !important;
		position: fixed;
		width: 100%;
		z-index: 100;
		transition: all 0.6s ease 0s;
	}

	.navbar-mobile .container {
		padding: 0;
		margin: 0;
		line-height: 5.5em;
		background: #fff;
	}

	.navbar-mobile .container .navbar-header {
		display: flex;
		justify-content: space-between;
		align-items: center;
		width: 100%;
		padding-right: 1em;
		padding-left: 1em;
		box-sizing: border-box;
		position: relative;
	}

	.navbar-mobile .container .navbar-header .menu-toggle {
		cursor: pointer;
		line-height: 5.5em;
		padding: auto 2em;
	}

	.navbar-mobile .container .navbar-header .menu-toggle span {
		display: block;
		background: #000;
		width: 36px;
		height: 2px;
		-webkit-border-radius: 3px;
		-moz-border-radius: 3px;
		border-radius: 3px;
		-webkit-transition: 0.25s margin 0.25s, 0.25s transform;
		-moz-transition: 0.25s margin 0.25s, 0.25s transform;
		transition: 0.25s margin 0.25s, 0.25s transform;
	}

	.dark-theme .navbar-mobile .container .navbar-header .menu-toggle span {
		background: #a9a9b3;
	}

	.navbar-mobile .container .navbar-header .menu-toggle span:nth-child(1) {
		margin-bottom: 8px;
	}

	.navbar-mobile .container .navbar-header .menu-toggle span:nth-child(3) {
		margin-top: 8px;
	}

	.navbar-mobile .container .navbar-header .menu-toggle.active span {
		-webkit-transition: 0.25s margin, 0.25s transform 0.25s;
		-moz-transition: 0.25s margin, 0.25s transform 0.25s;
		transition: 0.25s margin, 0.25s transform 0.25s;
	}

	.navbar-mobile .container .navbar-header .menu-toggle.active span:nth-child(1) {
		-moz-transform: rotate(45deg) translate(4px, 6px);
		-ms-transform: rotate(45deg) translate(4px, 6px);
		-webkit-transform: rotate(45deg) translate(4px, 6px);
		transform: rotate(45deg) translate(4px, 6px);
	}

	.navbar-mobile .container .navbar-header .menu-toggle.active span:nth-child(2) {
		opacity: 0;
	}

	.navbar-mobile .container .navbar-header .menu-toggle.active span:nth-child(3) {
		-moz-transform: rotate(-45deg) translate(8px, -10px);
		-ms-transform: rotate(-45deg) translate(8px, -10px);
		-webkit-transform: rotate(-45deg) translate(8px, -10px);
		transform: rotate(-45deg) translate(8px, -10px);
	}

	.navbar-mobile .container .menu {
		text-align: center;
		background: #fff;
        /*border-top: 1px solid #000;*/
		padding-top: 1em;
		padding-bottom: 1em;
		display: none;
		box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1), 0px 4px 8px rgba(0, 0, 0, 0.1);
	}

	.navbar-mobile .container .menu a {
		display: inline-block;
		margin: 0 1em;
		line-height: 2.5em;
	}

	.navbar-mobile .container .menu.active {
		display: block;
		white-space: nowrap;
		box-sizing: border-box;
		overflow-x: auto;
	}

	.dark-theme .navbar-mobile .container .menu {
		background: #292a2d;
        /*border-top: 1px solid #87878d;*/
	}

	.dark-theme .navbar-mobile .container {
		background: #292a2d !important;
	}

	.archive {
		width: 90%;
	}

	.archive .archive-item .archive-item-date {
		display: none;
	}

	#dynamic-to-top {
		display: none !important;
	}

	.footer {
		height: 3rem;
		width: 100%;
		text-align: center;
		line-height: 1.5rem;
		padding-top: 2em;
	}

	.post-warp {
		padding-top: 6em;
	}

	.post-warp .archive-item-date {
		display: none;
	}

	.categories .categories-card .card-item {
		width: 100%;
		display: flex;
		min-height: 0;
	}

	.categories .categories-card .card-item .categories {
		overflow: hidden;
	}

	.signature-img {
		width: 100%;
	}

	.signature-box img {
		height: 100px !important;
	}

	.signature-img img {
		height: 100px;
	}

	.signature-action, .vip-action {
		width: 100%;
	}

	.signature-action select, .vip-action select {
		width: 100%;
	}

	.signature-action input, .vip-action input {
		width: 200px;
		float: left;
		margin-top: 5px;
	}

	.signature-action button, .vip-action button {
		width: 200px;
		margin-top: 5px;
	}

	.mta-box {
		width: 100% !important;
	}

	.mta-a ul li {
		width: 100% !important;
	}

	.navbar-mobile {
		display: none;
	}
}

@media only screen and (min-device-width: 768px) {
	.navbar-mobile {
		display: none;
	}
}

@media only screen and (min-width: 1024px) {
	.navbar-mobile {
		display: none;
	}
}

.loader {
	box-sizing: content-box;
	display: block;
	position: absolute;
	top: 50%;
	left: 50%;
	margin: 0;
	text-align: center;
	z-index: 1000;
	-webkit-transform: translateX(-50%) translateY(-50%);
	transform: translateX(-50%) translateY(-50%);
}

.loader:before {
	position: absolute;
	content: '';
	top: 0;
	left: 50%;
	width: 50px;
	height: 50px;
	margin: 0 0 0 -25px;
	border-radius: 50px;
	border: 4px solid rgba(0, 0, 0, .1);
}

.loader:after {
	position: absolute;
	content: '';
	top: 0;
	left: 50%;
	width: 50px;
	height: 50px;
	margin: 0 0 0 -25px;
	animation: loader .6s linear;
	animation-iteration-count: infinite;
	border-radius: 50px;
	border: 4px solid transparent;
	border-top-color: #767676;
	box-shadow: 0 0 0 1px transparent;
}

@keyframes loader {
	from {
		-webkit-transform: rotate(0deg);
		transform: rotate(0deg);
	}

	to {
		-webkit-transform: rotate(360deg);
		transform: rotate(360deg);
	}
}

.dark-theme .post-content {
	background: #292a2d !important;
	color: #eee !important;
}

.dark-theme .post-content p code, .dark-theme .post-content ul li code {
	background: #292a2d !important;
}

.apps {
	color: red;
	font-weight: bold;
}

.signature-box {
	margin-top: 100px;
	text-align: center;
}

.signature-box img {
	border: none;
	height: 145px;
	margin-bottom: 50px;
}

.signature-action select, .vip-action select {
	height: 30px;
}

.signature-action input, .vip-action input {
	height: 25px;
	padding-left: 5px;
}

.signature-action input:focus, .vip-action input:focus {
	outline: none;
}

.signature-action button, .vip-action button {
	width: 135px;
	height: 30px;
}

.tag-cloud-tags-extend {
	padding-top: 0;
}

.hidden {
	display: none;
}

.vip-action {
	text-align: center;
}

.imgbox {
	width: 70%;
	text-align: center;
	margin: 80px auto 0;
}

.imgbox img {
	max-width: 100%;
	max-height: 100%;
}

.girl-qrcode {
	text-align: center;
}

.girl-img {
	width: 20%;
}

.btnbox {
	text-align: center;
	margin-top: 20px;
}

.tab-box {
	margin: 0 auto;
	margin-top: 50px;
	width: 1150px;
}

.top-tab {
	font-weight: bold;
	float: left;
	margin-top: 5px;
}

.top-tab ul li {
	list-style: none;
}

.top-tab ul li a.archive {
	color: #5A9600;
}

.top-content {
	float: left;
}

.top-content ul li {
	list-style: none;
	height: 35px;
	line-height: 35px;
	width: 888px;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.mta-box {
	width: 75%;
	margin: 100px auto 0;
}

.mta-a {
	margin: 20px 68px 50px 20px;
}

.mta-a ul {
	width: auto;
	display: none;
}

.mta-a ul li {
	float: left;
	width: 25%;
	list-style: none;
}

.mta-a-item {
	margin: 0 10px;
	border: 1px solid #e1e1e1;
	background: #fff;
	min-height: 60px;
}

.mta-a-title {
	padding: 16px 16px 0;
	height: 20px;
	line-height: 20px;
}

.mta-a-value {
	height: 100%;
	font-size: 30px;
	height: 24px;
	margin: 20px 0 20px 30px;
}

.mta-date {
	text-align: right;
	padding-right: 5px;
}

.dark-theme .mta-a-item {
	border: 1px solid #a9a9b3;
	background: transparent;
}

.qrcode {
	width: 120px;
	z-index: 99999;
	opacity: 0.8;
	margin: 20px auto 0;
}

.qrcode img {
	width: 100%;
}

.soul {
	text-align: center;
	margin-top: 200px;
}

.soul-btn {
	background-color: #5a9600;
	border: 5px;
	color: white;
	padding: 15px 32px;
	text-align: center;
	text-decoration: none;
	display: inline-block;
	font-size: 16px;
	margin: 4px 2px;
	cursor: pointer;
	-webkit-transition-duration: 0.4s;
	transition-duration: 0.4s;
}

.soul-btn:hover {
	box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24),0 17px 50px 0 rgba(0,0,0,0.19);
}

在css中用到了開源的 FiraCode 字體,可以自己去下載,不過在css中我已經改為遠程地址了。

刪掉 wwwroot/sample-data、wwwroot/css/bootstrap、wwwroot/css/open-iconic 三個文件夾。

在wwwroot文件夾下,有一個index.html,這個是我們網站的入口,注意裏面有一對標籤:<app>Loading...</app>,這個標籤裏面的內容會在 wasm 加載完畢后自動清除掉,所以,一般可以用來做加載提示。

現在改造一下index.html,代碼如下:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <meta name="keywords" content="Meowv,qix,阿星Plus,個人博客">
    <meta name="description" content="阿星Plus的個人博客,用於發表原創文章,關注微信公眾號:『阿星Plus』了解更多。">
    <title>阿星Plus⭐⭐⭐</title>
    <base href="/" />
    <link href="css/app.css" rel="stylesheet" />
</head>

<body>
    <app>
        <div class="loader"></div>
    </app>
    <script src="_framework/blazor.webassembly.js"></script>
</body>

</html>

千萬注意,blazor.webassembly.js 這個js不可以刪除,會在項目打包后自動生成這個js文件。

然後我們一點一點完善,Program.cs默認就行暫時不需要做任何改動。

Pages文件夾內的Razor組件就是我們的具體頁面了,幹掉默認的Counter.razorFetchData.razor,留下Index.razor,當作我們的首頁。

Shared文件夾內可以放一些共享的組件,比如我們的模板MainLayout.razor就在裏面,它需要繼承LayoutComponentBase

所以現在可以來修改一下我們的模板內容。

以我博客的UI架構而言,博客分為了三個部分,頭部、尾部、內容。

@inherits LayoutComponentBase

<div class="wrapper">
    <Header />
    @Body
    <Footer />
</div>

@Body為固定語法,表示這裡是內容部分,其他的不變,只在這裏填充內容。

在Shared文件夾中添加兩個組件,頭部:Header.razor、尾部:Footer.razor

Header.razor的內容如下:

<!-- Header.razor -->
<header>
    <nav class="navbar">
        <div class="container">
            <div class="navbar-header header-logo">
                <NavLink class="menu-item" href="/" Match="NavLinkMatch.All">
                    阿星Plus
                </NavLink>
            </div>
            <div class="menu navbar-right">
                <NavLink class="menu-item" href="posts">Posts</NavLink>
                <NavLink class="menu-item" href="categories">Categories</NavLink>
                <NavLink class="menu-item" href="tags">Tags</NavLink>
                <NavLink class="menu-item apps" href="apps">Apps</NavLink>
                <input id="switch_default" type="checkbox" class="switch_default" />
                <label for="switch_default" class="toggleBtn"></label>
            </div>
        </div>
    </nav>
    <nav class="navbar-mobile">
        <div class="container">
            <div class="navbar-header">
                <div>
                    <NavLink class="menu-item" href="" Match="NavLinkMatch.All">阿星Plus</NavLink>
                    <NavLink >&nbsp;·&nbsp;Light</NavLink>
                </div>
                <div class="menu-toggle">&#9776; Menu</div>
            </div>
            <div class="menu">
                <NavLink class="menu-item" href="posts">Posts</NavLink>
                <NavLink class="menu-item" href="categories">Categories</NavLink>
                <NavLink class="menu-item" href="tags">Tags</NavLink>
                <NavLink class="menu-item apps" href="apps">Apps</NavLink>
            </div>
        </div>
    </nav>
</header>

可以看到有很多的NavLink組件,這是我將a標籤轉換后的內容,其實最終生成的也是我們熟悉的a標籤,不過他自然有獨特用處,看介紹:

創建導航鏈接時,請使用 NavLink 組件代替 HTML 超鏈接元素 (<a>)。 NavLink 組件的行為方式類似於 <a> 元素,但它根據其 href 是否與當前 URL 匹配來切換 active CSS 類。 active 類可幫助用戶了解所显示導航鏈接中的哪個頁面是活動頁面。

Footer.razor的內容如下:

<!-- Footer.razor -->
<footer id="footer" class="footer">
    <div class="copyright">
        <span>
            Powered by <a target="_blank" href="http://dot.net">.NET Core 3.1</a> and <a href="http://blazor.net/">Blazor</a> on Linux
        </span>
    </div>
</footer>

然後刪掉默認的多餘的組件:NavMenu.razorSurveyPrompt.razor

還有一個_Imports.razor,這個就是用來導入命名空間的,放在這裏面就相當於全局引用了。

現在去編輯我們的首頁Index.razor

@page "/"

<div class="main">
    <div class="container">
        <div class="intro">
            <div class="avatar">
                <a href="javascript:;"><img src="https://static.meowv.com/images/avatar.jpg"></a>
            </div>
            <div class="nickname">阿星Plus</div>
            <div class="description">
                <p>
                    生命不息,奮鬥不止
                    <br>Cease to struggle and you cease to live
                </p>
            </div>
            <div class="links">
                <NavLink class="link-item" title="Posts" href="posts">
                    <i class="iconfont iconread"></i>
                </NavLink>
                <NavLink target="_blank" class="link-item" title="Notes" href="https://notes.meowv.com/">
                    <i class="iconfont iconnotes"></i>
                </NavLink>
                <NavLink target="_blank" class="link-item" title="API" href="https://api.meowv.com/">
                    <i class="iconfont iconapi"></i>
                </NavLink>
                <NavLink class="link-item" title="Manage" href="/account/auth">
                    <i class="iconfont iconcode"></i>
                </NavLink>
                <NavLink target="_blank" class="link-item" title="Github" href="https://github.com/Meowv/">
                    <i class="iconfont icongithub"></i>
                </NavLink>
                <NavLink class="link-item weixin" title="掃碼關注微信公眾號:『阿星Plus』查看更多。">
                    <i class="iconfont iconweixin"></i>
                </NavLink>
                <div class="qrcode">
                    <img src="https://static.meowv.com/images/wx_qrcode.jpg" />
                </div>
            </div>
        </div>
    </div>
</div>

@page指令用於設置頁面路由地址,因為是首頁,所以直接給一個”/”就可以了。

至此項目算是搭建完成並且將其改造了一番,現在可以去運行一下看看效果了。

第一次打開或者強制刷新頁面會出現加載中的界面,我這裏就是一個小圈圈在那裡轉,當加載完畢后就會自動消失,什麼都不需要干,太方便了。

現在已經成功將首頁的显示搞定了,隨便點擊幾個按鈕試試,會輸出一個錯誤提示:Sorry, there’s nothing at this address,因為沒有找到這些路由,所以就…

默認的有點丑,並且這句提示當然也可以自定義的,現在來看最後的一個組件App.razor

<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="@typeof(MainLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

通過語義化的代碼不難理解,Found就是找到與之匹配的路由,然後調用模板MainLayoutNotFound就是沒有找到的情況下,使用MainLayout並且在@body輸出一句提示。

將這句錯誤提示做成一個公共的組件並且美化一下,在Shared文件夾下新建組件:ErrorTip.razor,內容如下:

<div class="main">
    <div class="post-wrap">
        <h2 class="eror-tip">Sorry, there's nothing at this address.</h2>
    </div>
</div>

使用組件也很簡單,在App.razor中刪掉默認的p標籤然後調用ErrorTip

...
    <NotFound>
        <LayoutView Layout="@typeof(MainLayout)">
            <ErrorTip />
        </LayoutView>
    </NotFound>
...

再看一下打開了不存在路由的頁面的錯誤提示吧。

哈哈哈,是不是好看許多,接下來會完成主題切換,菜單展開關閉等等功能,其實這些可以用JavaScript很方便的實現,但是既然用了 Blazor 開發,所以還是用 .NET 代碼實現吧。

本篇就先到這裏,未完待續…

開源地址:https://github.com/Meowv/Blog/tree/blog_tutorial

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

分類
發燒車訊

Python機器學習筆記:SVM(3)——證明SVM,Python機器學習筆記:SVM(1)——SVM概述,Python機器學習筆記:SVM(2)——SVM核函數,Python機器學習筆記:SVM(3)——證明SVM,Python機器學習筆記:SVM(4)——sklearn實現

  說實話,凡是涉及到要證明的東西(理論),一般都不好惹。絕大多數時候,看懂一個東西不難,但證明一個東西則需要點數學功底,進一步,證明一個東西也不是特別難,難的是從零開始發明這個東西的時候,則顯得艱難(因為任何時代,大部分人的研究所得都不過是基於前人的研究成果,前人所做的是開創性的工作,而這往往是最艱難最有價值的,他們被稱為真正的先驅。牛頓也曾說過,他不過是站在巨人的肩上,你,我更是如此)。

  正如陳希孺院士在他的著作《數理統計學簡史》的第四章,最小二乘法中所講:在科研上諸多觀念的革新和突破是有着很多的不易的,或者某個定理在某個時期由有個人點破了,現在的我們看來一切都是理所當然,但在一切沒有發現之前,可能許許多多的頂級學者畢其功於一役,耗盡一生,努力了幾十年最終也是無功而返。

  上一節我學習了SVM的核函數內容,下面繼續對SVM進行證明,具體的參考鏈接都在第一篇文章中,SVM四篇筆記鏈接為:

Python機器學習筆記:SVM(1)——SVM概述

Python機器學習筆記:SVM(2)——SVM核函數

Python機器學習筆記:SVM(3)——證明SVM

Python機器學習筆記:SVM(4)——sklearn實現

  話休絮煩,要證明一個東西先要弄清楚它的根基在哪裡,即構成它的基礎是哪些理論。OK,以下內容基本上都是上文沒有學習到的一些定理的證明,包括其背後的邏輯,來源背景等東西。

  本文包括內容:

  • 1,線性學習器中,主要闡述感知機算法
  • 2,非線性學習器中,主要闡述 Mercer定理
  • 3,損失函數
  • 4,最小二乘法
  • 5,SMO算法的推導

  同樣,在學習這些之前,我們再複習一下SVM,這裏使用(http://staff.ustc.edu.cn/~ketang/PPT/PRLec5.pdf)的PPT來學習。

熱身:SVM的整理

  這裏直接借用別人的PPT粘貼在這裏,讓自己再梳理一遍SVM。

熱身1,Hard Margin SVM

熱身2,Soft Margin SVM

熱身3,LS-SVM

1,線性學習器

1.1 感知機算法

  這個感知器算法是在1956年提出的,年代久遠,依然影響着當今,當然,可以肯定的是,此算法亦非最優,後續會有更詳盡闡述。不過,有一點,你必須清楚,這個算法是為了干什麼的:不斷的訓練試錯以期尋找一個合適的超平面。

   下面,舉個例子。如下圖所示,憑我們的直覺可以看出,圖中紅線是最優超平面,藍線則是根據感知機算法在不斷的訓練中,最終,若藍線能通過不斷的訓練移動到紅線位置上,則代表訓練成功。

   既然需要通過不斷的訓練以讓藍線最終成為最優分類超平面,那麼到底需要訓練多少次呢?

  Novikoff 定理告訴我們當間隔是正的時候感知機算法會在有限次數的迭代中收斂,也就是說 Novikoff 定理證明了感知機算法的收斂性,即能得到一個界,不至於無窮循環下去。

  Novikoff 定理:如果分類超平面存在,僅需要在序列 S 上迭代幾次,在界為 (2R / γ)2 的錯誤次數下就可以找到分類超平面,算法停止。

  這裏的 R = max1<=i<=l||Xi|| ,γ 為擴充間隔。根據誤分次數公式可知,迭代次數與對應於擴充(包括偏置)權重的訓練集的間隔有關。

  順便再解釋下這個所謂的擴充間隔 γ , γ 即為樣本到分類間隔的距離,即從 γ 引出的最大分類間隔。之前我們推導過的內容,如下:

   在給出幾何間隔的定義之前,咱們首先來看下,如上圖所示,對於一個點 x,令其垂直投影到超平面上的對應的為 x0,由於 w 是垂直於超平面的一個向量, γ 為樣本 x 到分類間隔的距離,我們有:

   同時有一點值得注意:感知機算法雖然可以通過簡單迭代對線性可分數據生成正確分類的超平面,但不是最優效果,那怎麼才能得到最優效果呢,就是前面博文說的尋找最大分類間隔超平面。此外,Novikoff定理的證明請參考:http://www.cs.columbia.edu/~mcollins/courses/6998-2012/notes/perc.converge.pdf

2,非線性學習器

2.1 Mercer定理

   Mercer定理:如果函數 K 是 Rn *Rn – R 上的映射(也就是從兩個 n 維向量映射到實數域)。那麼如果K是一個有效核函數(也稱為 Mercer 核函數),那麼當且僅當對於訓練樣例 { x(1), x(2), …  x(m)},其相應的核函數矩陣是對稱半正定的。

  Mercer定理表明為了證明K是有效的核函數,那麼我們不用去尋找 Φ ,而只需要在訓練集上求出各個 Kij,然後判斷矩陣K是否是半正定(使用左上角主子式大於等於零等方法)即可。

  要理解這個 Mercer定理,先要了解什麼是半正定矩陣,要了解什麼是半正定矩陣,先得知道什麼是正定矩陣(矩陣理論博大精深,關於矩陣推薦一本書《矩陣分析與應用》),然後關於Mercer定理的證明參考:http://ftp136343.host106.web522.com/a/biancheng/matlab/2013/0120/648.html

  其實,核函數在SVM的分類效果中起到了重要的作用,下面鏈接有個 tutorial可以看看:https://people.eecs.berkeley.edu/~bartlett/courses/281b-sp08/7.pdf

2.2 正定矩陣

  在百度百科,正定矩陣的定義如下:在線性代數里,正定矩陣(positive definite materix)有時會簡稱為正定陣。在線性代數中,正定矩陣的性質類似於複數中的正實數。與正定矩陣相對應的線性算子是對稱的正定雙線性形式。

  廣義的定義:設 M 為 n 階方陣,如果對任何非零向量 z,都有 zTMz > 0,其中 zT 表示 z 的轉置,就稱 M  為正定矩陣。

  狹義的定義:一個 n 階的實對稱矩陣 M 是正定的條件是當且僅當對所有的非零實係數向量 z,都有 zTMz > 0,其中 zT 表示 z 的轉置。

  正定矩陣的性質:

  • 1,正定矩陣的行列式恆為正
  • 2,實對稱矩陣 A 正定當且僅當 A 與單位矩陣合同
  • 3,若 A 是正定矩陣,則 A 的逆矩陣也是正定矩陣
  • 4,兩個正定矩陣的和是正定矩陣
  • 5,正實數域正定矩陣的乘積是正定矩陣

3,損失函數

  之前提到過“支持向量機(SVM)是 90 年代中期發展起來的基於統計學習理論的一種機器學習方法,通過尋找結構化風險最小來提高學習機泛化能力,實現經驗風險和置信範圍的最小化,從而達到在統計樣本量較少的情況下,亦能獲得良好統計規律的目的。”但初次看到的人可能不了解什麼是結構化風險,什麼又是經驗風險。要了解這兩個所謂的“風險”,還得從監督學習說起。

  監督學習實際上就是一個經驗風險或者結構風險函數的最優化問題。風險函數度量平均意義下模型預測的好壞,模型每一次預測的好壞用損失函數來度量。它從假設空間 F 中選擇模型 f 作為決策函數,對於給定的輸入 X,由 f(x) 給出相應的輸出 Y,這個輸出的預測值 f(X)與真實值 Y 可能一致也可能不一致,用一個損失函數來度量預測錯誤的程度。損失函數記為 L(Y, f(X))。

  常用損失函數有以下幾種(摘抄於《統計學習方法》):

  (1) 0-1 損失函數

  (2)平方損失函數

   (3)絕對損失函數

   (4)對數損失函數

   給定一個訓練數據集

   模型 f(X) 關於訓練數據集的平均損失稱為經驗風險,如下:

   關於如何選擇模型,監督學習有兩種策略:經驗風險最小化和結構風險最小化。

  經驗風險最小化的策略認為,經驗風險最小的模型就是最優的模型,則按照經驗風險最小化求最優模型就是求解如下的最優化問題:

  當樣本容量很小時,經驗風險最小化的策略容易產生過擬合的現象。結構風險最小化可以防止過擬合。結構風險是在經驗風險的基礎上加上表示模型複雜度的正則化項或懲罰項,結構風險定義如下:

   其中 J(f) 為模型的複雜度,模型 f 越複雜,J(f) 值就越大,模型越簡單,J(f) 值就越小,也就是說J(f)是對複雜模型的乘法。λ>=0 是係數,用以衡量經驗風險和模型複雜度。結構風險最小化的策略認為結構風險最小的模型是最優的模型,所以求最優的模型就是求解下面的最優化問題:

   這樣,簡單學習問題就變成了經驗風險或結構化風險函數的最優化問題。如上式最優化問題的轉換。

   這樣一來,SVM就有第二種理解,即最優化+損失最小。如網友所言:“可以從損失函數和優化算法角度看SVM,Boosting,LR等算法,可能會有不同收穫”。

  關於損失函數:可以看看張潼的這篇《Statistical behavior and consistency of classification methods based on convex risk minimization》。各種算法中常用的損失函數基本都具有fisher一致性,優化這些損失函數得到的分類器可以看作是后驗概率的“代理”。此外,張潼還有另外一篇論文《Statistical analysis of some multi-category large margin classification methods》,在多分類情況下margin loss的分析,這兩篇對Boosting和SVM使用的損失函數分析的很透徹。

  關於統計學習方法的問題,可以參考:https://people.eecs.berkeley.edu/~bartlett/courses/281b-sp08/7.pdf

4,最小二乘法

4.1  什麼是最小二乘法?

  下面引用《正態分佈的前世今生》里的內容稍微簡單闡述一下。

  我們口頭經常經常說:一般來說,平均來說。如平均來說,不吸煙的健康優於吸煙者,之所有要加“平均” 二字,是因為凡是皆有例外,總存在某個特別的人他吸煙但由於經常鍛煉所以他的健康狀況可能會優於他身邊不吸煙的盆友。而最小二乘的一個最簡單例子便是算術平均。

  最小二乘法(又稱最小平方法)是一種數學優化技術。它通過最小化誤差的平方和尋找數據的最佳函數匹配。利用最小二乘法可以簡便的求得未知的數據,並使得這些求得的數據與實際數據之間誤差的平方和為最小。用函數表示為:

   使誤差(所謂誤差,當然是觀察值與實際真實值的差量)平方和達到最小以尋求估計值的方法,就叫做最小二乘法,用最小二乘法得到的估計,叫做最小二乘估計。當然,取平方和作為目標函數只是眾多可取的方法之一。

  最小二乘法的一般形式可表示為:

   有效的最小二乘法是勒讓得在1805年發表的,基本思想就是認為測量中有誤差,所以所有方程的累積誤差為:

   我們求解出導致累積誤差最小的參數即可:

   勒讓得在論文中對最小二乘法的優良性做了幾點說明:

  • 最小二乘的誤差平方和最小,並在各個方程的誤差之間建立了一種平衡,從而防止某個極端誤差取得支配地位。
  • 計算中只需要求偏導后求解線性方程組,計算過程明確便捷
  • 最小二乘可以導出算術平均值作為估計

  對於最後一點,從統計學的角度來看是很重要的一個性質。推理如下:假設真值為 Θ ,x1,…..xn 為 n 次測量值,每次測量的誤差為 ei = xi – Θ,按最小二乘法,誤差累積為:

  求解 Θ 使 L(Θ) 達到最小,正好是算術平均 xhat,其公式如下:

   由於算術平均是一個歷經考驗的方法,而以上的推理說明,算術平均是最小二乘的一個特例,所以從另外一個角度說明了最小二乘方法的優良性,使我們對最小二乘法更加有信息。

  最小二乘法發布之後很快得到了大家的認可接受,並迅速的在數據分析實踐中被廣泛使用。不過歷史上又有人把最小二乘法的發明歸功於高斯,這又是怎麼一回事呢?高斯在 1809 年也發表了最小二乘法,並且聲稱自己已經使用了這個方法多年。高斯發明了小行星定位的數學方法,並在數據分析中使用最小二乘方法進行計算,準確的預測了穀神星的位置。

  說了這麼多,貌似與SVM沒啥關係,但是別著急,請繼續聽,本質上說,最小二乘法即是一種參數估計方法,說到參數估計,咱們從一元線性模型說起。

4.2 最小二乘法的解法

   什麼是一元線性模型呢?我們引用(https://blog.csdn.net/qll125596718/article/details/8248249)的內容,先來梳理一下幾個基本的概念:

  • 監督學習中,如果預測的變量是離散的,我們稱其為分類(如決策樹,支持向量機等),如果預測的變量是連續的,我們稱其為回歸。
  • 回歸分析中,如果只包括一個自變量和一個因變量,且二者的關係可用一條直線近似表示,這種回歸分析稱為一元線性回歸分析。
  • 如果回歸分析中包括兩個或兩個以上的自變量,且因變量和自變量之間是線性關係,則稱為多元線性回歸分析。
  • 對於二維空間線性是一條直線;對於三維空間線性是一個平面,對於多維空間線性是一個超平面。

  對於一元線性回歸模型,假設從總體中獲取了 n 組觀察值(X1, Y1),(X2, Y2),…(Xn, Yn)。對於平面中的這 n 個點,可以使用無數條曲線來擬合。要求樣本回歸函數盡可能好的擬合這組值。綜合起來看,這條直線處於樣本數據的中心位置最合理。

  選擇最佳擬合曲線的標準可以確定為:使總的擬合誤差(即總殘差)達到最小,有以下三個標準可以選擇:

  • 1,用“殘差和最小”確定直線位置是一個途徑。但是很快發現計算“殘差和” 存在相互抵消的問題。
  • 2,用“殘差絕對值和最小”確定直線位置也是一個途徑。但絕對值的計算比較麻煩。
  • 3,最小二乘法的原則是以“殘差平方和最小” 確定直線位置。用最小二乘法除了計算比較方便外,得到的估計量還具有優良特性。這種方法對異常值非常敏感。

  最常用的是普通最小二乘法(Ordinary Least Square, OLS ):所選擇的回歸模型應該使所有觀察值的殘差平方和達到最小,即採用平方損失函數。

   我們定義樣本回歸模型為:

   得到誤差 ei (ei為樣本)為:

   接着,定義平方損失函數 Q:

   則通過Q最小確定這條直線,即確定 β0hat,  β1hat, β0hat,  β1hat為變量,把它們看做是 Q 的函數,就變成了一個求極值的問題,可以通過求導數得到。

  求 Q 對兩個待估參數的偏導數:

   根據數學知識我們知道,函數的極值點為偏導為 0 的點,解得:

   這就是最小二乘法的解法,就是求得平方損失函數的極值點。自此,我們可以看到求解最小二乘法和求解SVM是何等相似,尤其是定義損失函數,而後通過偏導求極值。

5,SMO算法

  無論Hard Margin 或 Soft Margin SVM,我們均給出了SVM的對偶問題,但並沒有說明對偶問題怎麼求解。由於矩陣Q的規模和樣本數相等,當訓練樣本數很大的時候,這個矩陣的規模很大,求解二次規劃問題的經典算法會遇到性能問題,也就是說同時求解 n 個拉格朗日乘子涉及很多次迭代,計算開銷太大,所以一般採用 Sequential Minimal Optimization(SMO)算法。

  SMO算法的基本思想每次只更新兩個乘子,迭代獲得最終解

  上文中,我們提到了求解對偶問題的序列最小最優化 SMO 算法,但並未提到其具體解法。首先看下最後懸而未決的問題:

   等價於求解:

   1998年,Microsoft Research 的John C. Platt 在論文《Sequential  Minimal Optimization:A Fast Alogrithm for Training Support Vector Machines》中提出針對上述問題的解法:SMO算法,它很快便成為最快的二次規劃優化算法,特別是針對線性SVM和數據稀疏時性能更優。這個算法的思路是每次在優化變量中挑出兩個分量進行優化,而讓其他分量固定,這樣才能保證滿足等式約束條件,這是一種分治法的思想。

  接下來,我們便參考 John C.Platt 的文章(找不到了。。。)來看看 SMO的解法。

5.1 SMO算法的推導

  首先我們來定義特徵到結果的輸出函數:

   注:這個 u 與我們之前定義的 f(x) 實質上是一樣的。

   接着,重新定義下我們原始的優化問題,權當重新回顧,如下:

   求導得到:

   代入 u 的公式中,可得:

   通過引入拉格朗日乘子轉換為對偶問題后,得:

   注:這裏得到的 min 函數與我們之前的 max 函數實質上也是一樣,因為把符號變下,即由 min 轉換為 max 的問題,且 yi也與之前的 y(i) 等價,yj 亦如此。

  經過加入鬆弛變量后,模型修改為:

   從而最終我們的問題變為:

  下面要解決的問題是:在 αi = { α1, α2, α3,……, αn} 上求上述目標函數的最小值。為了求解這些乘子,每次從中任意抽取兩個乘子 α1 和 α2,然後固定 α1 和 α2 以外的乘子 {α3, α4,….αn},使得目標函數只是關於 α1 和 α2 的函數。這樣,不斷的從一堆乘子中任意抽取兩個求解,不斷地迭代求解子問題,最終達到求解原問題的目的。

  (注意:下面均使用兩個相同的表達式,是參考了兩個方法,並且這兩個方法均易於理解,可以說我先看第一個公式的文章,然後偶爾有次看到第二個公式的文章,發現也很好理解,所以粘貼在這裏,特地說明

  我們首先給出對於這兩個常量的優化問題(稱為子問題)的求解方法。假設選取的兩個分量為 αi, αj,其他分量都固定(即當做常數)。由於:

  所以對偶問題的子問題的目標函數可以表達為:

  (更普及一點,可以寫成下面這樣)

   其中C是一個常數,前面的二次項很容易計算出來,一次項要複雜一些,並且:

  這裏的變量 α* 為變量 a 在上一輪迭代后的值。上面的目標函數是一個兩變量的二次函數,我們可以直接給出最小值的解析解(公式解)。

   為了解決這個子問題,首要問題便是每次如何選取 α1 和 α2。實際上,其中一個乘子是違反 KKT條件最嚴重的,另外一個乘子則由另一個約束條件選取。

  根據KKT條件可以得到目標函數中 αi 取值的意義:

   這裏的 αi 還是拉格朗日乘子:

  • 1,對於第一種情況,表明 αi 是正常分類,在間隔邊界內部(我們知道正確分類的點 yi * f(xi) >= 0)
  • 2,對於第二種情況,表明了 αi 是支持向量,在間隔邊界上
  • 3,對於第三種情況,表明了 αi 是在兩條間隔邊界之間

  而最優解需要滿足KKT 條件,即上述三個條件都得滿足,以下幾種情況出現將會出現不滿足:

  • 1,yiui <=1,但是 αi < C 則不是不滿足的,而原本 αi = C
  • 2,yiui >=1,但是 αi > C 則不是不滿足的,而原本 αi = C
  • 3,yiui =1,但是 αi = 0 或者  αi = C 則不是不滿足的,而原本 0  < αi < C

  也就是說,如果存在不滿足 KKT 條件的 αi ,那麼需要更新這些 αi ,這是第一個約束條件。此外,更新的同時還要受到第二個約束條件的限制,即:

   因此,如果假設選擇的兩個乘子  α1 和 α2 ,他們在更新之前分別是  α1old 和 α2old,更新之後分別是  α1new 和 α2new,那麼更新前後的值需要滿足以下等式才能保證和為 0  的約束:

   其中,ξ 是常數,(上面兩個式子都一樣,只不過第二個更容易理解)。

  兩個因子不好同時求解,所以可選求第二個乘子 α2 的解(α2new),得到 α2 的解(α2new)之後,再利用 α2 的解(α2new)表示 α1 的解(α1new).

  為了求解 α2 的解(α2new),得先確定 α2new 的取值範圍。假設它的上下邊界分別為 H 和 L,那麼有:

   接下來,綜合下面兩個約束條件,求解 α2new 的取值範圍:

   由於 yi,  yj(也可以說為 y1  y2)的取值只能為 +1 或者 -1,那麼當他們異號,也就是當 y1 != y2 時,根據:

   可得:  α1old – α2old  = ξ   (  αi – αj  = ξ),它確定的可行域是一條斜率為1的直線段,因為αi αj 要滿足約束條件

  他們的可行域如下圖所示:

  上面兩條直線分別對應於 y1為 +1 和 -1 的情況。如果是上面那條直線,則 αj 的取值範圍為 [-ξ, C]。如果是下面的那條直線,則為 [0,C-ξ]。

  對於這兩種情況 αj 的下界和上界可以統一寫成如下形式:

  因為   αi – αj  = ξ ,所以又可以寫為:  L =  max (0, -ξ),  H = min(C, C-ξ)

  下邊界是直線和 x 軸交點的 x 坐標以及 0 的較大值;上邊界是直線和的交點的 x 坐標和 C 的較小值。

   再來看第二種情況,如果 yi  yj 同號,即當 y1 = y2 時,同樣根據:

   可得:  α1old + α2old  = ξ (  αi  +  αj  = ξ ),所以有:

 

   根據   αi  +  αj  = ξ  , 上式也可寫為:L =  max (0, ξ – C),  H = min(C, ξ)

  這種情況如下圖所示:

   如此,根據這兩個變量的等式約束條件( y1 和 y2 異號或者同號),可以消掉α2old ,可得出 α2new 的上下界分別為:

   回顧下第二個約束條件:

  下面我們來計算不考慮截斷時的函數極值。為了避免分 -1 和 +1 兩種情況,我們將上面的等式約束兩邊同時乘以 y1(第二種表達是乘以yi),可得:

   其中 α1 可以用 α2 表示,α1 = w – s*α2,從而我們把子問題的目標函數轉換為只含 α2 的問題:

   對 α2 求導(即對自變量求導),並令導數為零,可得:

   由於:

   化簡下:

   然後將:

   代入上式,可得:

   下面令(其中 Ei 表示預測值與真實值之差):

   然後上式兩邊同時除以 η ,得到一個關於單變量 α2 的解:

  在求得  αj 之後,根據等式約束條件我們就可以求得另外一個變量的值:

  目標函數的二階導數為 η,前面假設二階導數 η  > 0,從而保證目標函數是凸函數即開口向上的拋物線,有極小值。如果 η  < 0 或者 η  = 0,該怎麼處理?對於線性核或正定核函數,可以證明矩陣K的任意一個上述子問題對應的二階子矩陣半正定,因此必定有 η  >= 0。無論本次迭代時的初始值是多少,通過上面的子問題求解算法得到是在可行域里的最小值,因此每次求解更新這兩個變量的值之後,都能保證目標函數值小於或者等於初始值,即函數值下降。

   這個解沒有考慮其約束條件 0 <=  α2 <= C,即是未經剪輯時的解。

  然後考慮約束 0 <=  α2 <= C 可得到經過剪輯后的 α2new 的解析解為:

  (如果用αi,αj表示,則我們求的這個二次函數的最終極值點為:)

   求出了 α2new后,便可以求出α1new ,得:

  這三種情況下的二次函數極小值如下圖所示:

  上圖中第一種情況是拋物線的最小值點在 [L, H]中;第二種情況是拋物線的最小值點大於 H,被截斷為H;第三種情況是小於L,被截斷為L。

   那麼如何選擇乘子   α1 和 α2呢?

  1. 對於 α1 ,即第一個乘子,可以通過剛剛說的那3種不滿足 KKT的條件來找
  2. 而對於第二個乘子 α2 可以尋找滿足條件: max |Ei – Ej| 的乘子

  而 b 滿足下述條件:

   下面更新 b:

   且每次更新完兩個乘子的優化后,都需要再重新計算 b,及對應的 Ei值。

  最後更新所有的 αi,y 和 b,這樣模型就出來了,從而即可求出咱們開頭提出的分類函數:

   此外,這裡有一篇類似的文章,大家可以參考下(https://www.cnblogs.com/jerrylead/archive/2011/03/18/1988419.html)。

 5.2  SMO算法的步驟

  綜上,總結下SMO的主要步驟,如下:

   意思是:

  • 1,第一步:選取一對 αi 和 αj,選取方法使用啟髮式方法
  • 2,第二步:固定除αi 和 αj 之外的其他參數,確定W 極值條件下的 αi 和 αj 由 αi 表示

  假定在某一次迭代中,需要更新 x1,x2 對應的拉格朗日乘子 α1,α2,那麼這個小規模的二次規劃問題寫為:

   那麼在每次迭代中,如何更新乘子呢?引用下面地址(http://staff.ustc.edu.cn/~ketang/PPT/PRLec5.pdf)的兩張PPT說明下:

   知道了如何更新乘子,那麼選取哪些乘子進行更新呢?具體有以下兩個步驟:

  • 步驟一:先“掃描”所有乘子,把第一個違反KKT條件的作為更新對象,令為 a1
  • 步驟二:在所有不違反KKT條件的乘子中,選擇使 |E1 – E2|最大的 a2 進行更新,使得能最大限度增大目標函數的值(類似於梯度下降,此外 Ei = ui – yi,而 u = w*x – b ,求出來的 E 代表函數 ui 對輸入 xi 的預測值與真實輸出類標記 yi 之差)

  最後,每次更細完兩個乘子的優化后,都需要再重新計算 b,及對應的 Ei 值。

  綜上,SMO算法的基本思想是把 Vapnik 在 1982年提出的 Chunking方法推到極致,SMO算法每次迭代只選出兩個分量 ai 和 aj 進行調整,其他分量則保持固定不變,在得到 解 ai 和 aj 之後,再用 ai 和 aj 改進其他分量。與通常的分解算法比較,儘管它可能需要更多的迭代次數,但每次迭代的計算量比較小,所以該算法表現出較好的快速收斂性,且不需要存儲核函數,也沒有矩陣運算。

5.3  SMO算法的實現

  行文至此,我相信,SVM理解到了一定程度后,是的確能在腦海里從頭到尾推導出相關公式的,最初分類函數,最大化分類間隔,max1/||w||,min1/2||w||^2,凸二次規劃,拉格朗日函數,轉化為對偶問題,SMO算法,都為尋找一個最優解,一個最優分類平面。一步步梳理下來,為什麼這樣那樣,太多東西可以追究,最後實現。

  至於上文中將闡述的核函數則是為了更好的處理非線性可分的情況,而鬆弛變量則是為了糾正或約束少量“不安分”或脫離集體不好歸類的因子。

      台灣的林智仁教授寫了一個封裝SVM算法的libsvm庫,大家可以看看,此外這裏還有一份libsvm的註釋文檔。在這篇論文《fast training of support vector machines using sequential minimal optimization》中platt給出了SMO算法的邏輯代碼。

5.4  SMO算法的優缺點

  優點:

  • 可保證解的全局最優解,不存在陷入局部極小值的問題
  • 分類器複雜度由支撐向量的個數,而非特徵空間(或核函數)的維數決定,因此較少因維數災難發生過擬合線性

  缺點:

  1. 需要求解二次規劃問題,其規模與訓練模式量成正比,因此計算複雜度高,且存儲開銷大,不適用於需進行在線學習/訓練的大規模分類問題  

  這篇文章主要參考:https://mp.weixin.qq.com/s/ZFWJUazMbAqeoSIkXjuG5g

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

網頁設計最專業,超強功能平台可客製化

分類
發燒車訊

別再重複造輪子了,幾個值得應用到項目中的 Java 開源庫送給你

我是風箏,公眾號「古時的風箏」。文章會收錄在 JavaNewBee 中,更有 Java 後端知識圖譜,從小白到大牛要走的路都在裏面。公眾號回復『666』獲取高清大圖。

風箏我作為一個野路子開發者,直到遇見下面的這幾個工具庫,才知道之前重複造了不少輪子,而且輪子還不一定有人家的圓。相信跟我一樣,沒事兒造輪子的人還不在少數,有些人就是對造輪子感興趣,這個咱也無話可說,但是,比如我,我是造輪子之前不知道這世上已經有好用的輪子了,害,無知限制了我的想象力。

比如我們在拿到一個 List 集合之後,要對這個集合進行判空操作,以前我一直是這樣寫的:

List<String> list = getList();
if (list != null && list.size() > 0) {
    //do something
}

雖然這樣也沒什麼問題,但是,我懶啊,每次敲這麼多代碼,也挺累啊。有同學說,那你包裝成一個方法不就行了,每次調用個方法就 OK 啦。這不,同學,你就在造輪子了,已經有人幫你寫好了這樣類似的一系列方法了。

來讓我們認識認識這些輪子吧。

Java 8 Stream

Stream 不算是工具庫,但是通過 stream 提供的一系列方法,可以實現集合的過濾、分組、集合轉換等諸多操作。

例如下面的方法,實現列表元素根據某個字段去重的功能。

List<User> userList = new ArrayList();
//添加元素
userList =  userList.stream().filter(distinctByKey(user->user.getUserId())).collect(Collectors.toList());

private static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
      Map<Object,Boolean> seen = new ConcurrentHashMap<>();
      return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
}

apache commons

官方地址:http://commons.apache.org/

這不是一個庫,而是一系列的工具庫。

由於包含的庫過多,我就不一一列舉了,可以到官網一探。有集合處理的、數學計算的、IO 操作的等等,其中最常用的莫過於 Apache Commons Lang 和 Apache Commons Collections 這兩個。

Apache Commons Lang 包括一系列工具類,有字符串相關的、時間處理的、反射的、併發包的等等,Apache Commons Collections 專門用作集合處理。

下面舉幾個例子說明一下,更詳細的內容可以到官網查看文檔。

字符串判空操作

String s = "";
Boolean isEmpty = StringUtils.isEmpty(s);

獲取類的全名稱

ClassUtils.getName(ClassUtils.class);

判斷集合是否為空

Boolean isNotEmpty = CollectionUtils.isNotEmpty(list);

反射獲取某個類的所有 Field

Field[] fields = FieldUtils.getAllFields(User.class);

Google Guava

官方地址:https://github.com/google/guava

和 Apache Commons 有點兒類似,它也是包含了一系列的比如字符串、集合、反射、數學計算等的操作封裝,還可以用作 JVM 緩存。

舉幾個例子說明:

New 各種對象

List<String> list = Lists.newArrayList();
Set<String> set = Sets.newHashSet();
Map<String,Object> map = Maps.newConcurrentMap();

// 不可變集合
ImmutableList<String> immutableList = ImmutableList.of("1", "2", "3");

列錶轉符號分隔的字符串

List<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
list.add("3");
String result = Joiner.on("-").join(list);

> 1-2-3

求交集、並集、差集等

例如下面代碼求 set1 和 set2 的交集

Set<Integer> set1 = Sets.newHashSet(1, 2, 3, 4, 5, 6);
Set<Integer> set2 = Sets.newHashSet(1,2,3,4);
       
Sets.SetView<Integer> intersection = Sets.intersection(set1, set2);

Joda Time

官方地址:https://www.joda.org/joda-time/

一個日期、時間處理的工具庫。如果你不是經常做日期處理,那差不多每次需要的時候都需要查詢相關的 API,而有了工具類就不一樣了,只要一個 “.”,你想要的方法就出現了,而 Joda Time 就是一款好用的工具庫。

比如下面這個方法,計算到新年還有多少天。

public Days daysToNewYear(LocalDate fromDate) {
  LocalDate newYear = fromDate.plusYears(1).withDayOfYear(1);
  return Days.daysBetween(fromDate, newYear);
}

OkHttp3

官方地址:https://square.github.io/okhttp/

一個 HTTP 客戶端,使用簡單,性能良好,是時候放棄 HttpClient 了。

一個 get 請求:

OkHttpClient client = new OkHttpClient();

String run(String url) throws IOException {
  Request request = new Request.Builder()
      .url(url)
      .build();

  try (Response response = client.newCall(request).execute()) {
    return response.body().string();
  }
}

一個 post 請求:

public static final MediaType JSON
    = MediaType.get("application/json; charset=utf-8");

OkHttpClient client = new OkHttpClient();

String post(String url, String json) throws IOException {
  RequestBody body = RequestBody.create(json, JSON);
  Request request = new Request.Builder()
      .url(url)
      .post(body)
      .build();
  try (Response response = client.newCall(request).execute()) {
    return response.body().string();
  }
}

Json 系列

Jackson

Spring 默認的 Json 序列化工具,其實已經足夠用了。

Gson

Google 出品,功能齊全。

FastJson

阿里出品,算法良好,性能最優。

EasyExcel

官方地址:https://www.yuque.com/easyexcel/doc/easyexcel

阿里開源的 Excel 操作工具庫,可以看做是 Apache POI 的增強封裝版、優化版。

如果你的數據量很大,那用 EasyExcel 可以節省內存,提升效率,並且沒有併發風險。

如果你的 Excel 足夠複雜,那用 EasyExcel 會比你直接用 POI 少些很多代碼。

比如我實現了下面這個 Excel 動態導出,包括動態表頭、動態合併單元格的功能,只用了很少的代碼,如果是使用 POI 的話,那可能代碼量增加不止一倍啊。

TinyPinyin

官方地址:https://github.com/promeG/TinyPinyin

中文轉拼音,把你輸入的中文轉換成拼音。比如搜索功能要實現這樣的功能,輸入 “fengzheng” 搜索,會匹配到 “風箏”這個詞語,這就需要中文轉拼音了。

有的同學說了,這不是拼音轉英文嗎?當然不是在輸入“fengzheng”的時候轉換了,而是在包含“風箏”的這條記錄中有一個拼音的額外字段,這樣搜索的時候直接匹配拼音那個字段。

chinese_name pinyin_name
風箏 fengzheng

反射工具庫 – jOOR

官方地址:https://github.com/jOOQ/jOOR

它是 JDK 反射包的友好封裝,通過一系列簡單友好的鏈式操作實現反射調用。比如下面這個例子

public interface StringProxy {
  String substring(int beginIndex);
}

String substring = on("java.lang.String")
                    .create("Hello World")
                    .as(StringProxy.class)
                    .substring(6);    

簡單的代碼實現 JDK 動態代理,節省了不少代碼。

MyBatis-Plus

官方地址:https://mp.baomidou.com/

只要你的項目中有數據庫訪問,那你肯定用過或者至少聽說過 MyBatis ,但是如果你只用 MyBatis 需要針對每個DAO方法寫對應的 SQL Statement(也就是 mapper.xml 中的代碼塊),當然有一些自動生成的工具,MyBatis 就有它提供的 MyBatis Generator,比如我也稍做加工,做過一個web 版的 MyBatis Generator,開發效率是提高了,但是每個 mapper.xml 文件的代碼量很大,於是 MyBatis-Plus 就要出場了。

官網上對他的定義如下:

  1. 只做增強不做改變,引入它不會對現有工程產生影響,如絲般順滑。
  2. 只需簡單配置,即可快速進行 CRUD 操作,從而節省大量時間。
  3. 熱加載、代碼生成、分頁、性能分析等功能一應俱全。

最後,在配上 MybatisX IDEA 插件,也是可以了。

vjtools

官方地址:https://github.com/vipshop/vjtools

這是唯品會的開源工具包,這裏主要介紹其中的 vjkit 模塊,是關於文本,集合,併發等基礎功能的核心類庫。這個庫是我很早之前搜索日期操作的時候偶然發現的,我發現裏面日期處理的 API 相當全面而且很實用,還在我的項目中用過一段時間。

最後

好用的工具庫可以提高我們的開發效率,而且也是我們學習源碼的好去處,和其他的開源框架(比如 Spring、Dubbo)一樣,看看優秀的代碼是如何實現的。

如果你還知道什麼好用、強大的開源工具包,歡迎在留言區分享,好東西不能獨享,讓更多的人受益。

各位大佬,給個推薦,讓我奮發圖強

我是風箏,公眾號「古時的風箏」。一個兼具深度與廣度的程序員鼓勵師,一個本打算寫詩卻寫起了代碼的田園碼農!你可選擇現在就關注我,或者看看歷史文章再關注也不遲。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計最專業,超強功能平台可客製化

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※教你寫出一流的銷售文案?

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家公司費用怎麼算?

分類
發燒車訊

如何優雅地向公司提加薪

二哥,你好呀,我是你鐵杆粉絲,想向你請教一個問題。我是 2019 年 3 月份入職的,當時很菜,接手的是一個要離職同事的代碼,可把我害慘了,一邊推進度,一邊修 bug,7 月份一整個月都沒有在凌晨 3 點前睡過。幸好挺了過來,截止目前在公司待了一年零兩個月了,想找領導談薪水的問題,但不知道如何開口。

以上是讀者奔三十私信我的問題,很有代表性,我計劃着好好寫一篇文章來統一回復下,結果一拖再拖,拖了快一個月時間,真的非常非常抱歉。

我之所以拖,有兩個原因。第一個原因就是這個問題確實不太好回答,因為我自己親身經歷的漲薪就那麼幾次,並且我沒去過大廠,經驗不一定具備普適性;第二個原因就是,拖延症犯了,呵呵(戰術性)。

那接下來,就談談我自己僅有的幾次加薪情況吧,希望給小夥伴們一些參考。

我是大三就出去實習的,當時工資 1200 塊一個月,一起進來的二十多名新人都這個標準。這個不算是加薪,但起點大家要了解一下。

簡單介紹一下背景,可能一些新來的讀者不太清楚。

我是一名大專生,這沒什麼可恥的,真的。經常有一些讀者私信我說,“二哥,我學歷不好,大專生,畢業后被歧視怎麼辦?”

起點低,被歧視是正常的,要學會接受。人生下來就不是平等的,我們要做的就是自尊自愛。網絡上經常遇到一些噴子,留言噴我垃圾,我就回一句話:“我與你的區別就是,我越來越強大,而你,還是那個噴子,而已。”

大二結束后,我就去蘇州一家軟件園培訓了倆月,然後入職了一家日企(江蘇富士通,讀者群體里有沒有知道這家公司的?),當時軟件部剛剛成立,急需新鮮血液的融入,而我們這些廉價的勞動力正好是壓榨的對象。

實習結束后,回學校拿了畢業證(順帶和女朋友團聚了倆月時間),然後正式入職。但記不得當時具體的工資是多少了,四五千吧。

這次加薪我是沒有資本吭聲的,因為有一份工作就不錯了。況且這家公司發展的真不錯,項目部不斷擴大,剛開始一個部門,我回洛陽的時候已經四個部門了。

關鍵是,不缺項目啊,日方那邊源源不斷地供給着項目,最重要的是,資金。公司本來還有一片地,在鄧蔚路的綠寶廣場附近,修地鐵的時候賣了,然後在一個叫什麼區的地方買了幾棟樓,名字我忘記了,當時挺偏僻的。

女朋友畢業后,去蘇州過一段時間,我們在市區租了一間房子,每次上班我要先走一段路,搭公司的大巴去上班,要一個小時左右車程呢。

由於我是大專生,所以這次加薪,要比本科學歷的同事少三四百,具體数字同事之間也不太方便交底,反正是確實少一些,但差額並沒有很離譜。

正式入職一年後,我換了一個項目組, 改做 Flex 了。之前基本上是打雜,搞過 SQL、搞過 Ruby、搞過 Spring + Hibernate + Postgresql,基本上是哪需要就往哪插。

這一年時間里,我了解到公司的發展重心將會是 Flex,就私下里研究了整個框架的源碼,並且寫了一個內部聊天工具,供幾個老資格的同事聊魔獸世界用。這點我在之前的文章里提到過。

(心機 boy,有沒有?)

正是憑藉這個不起眼的工具,我被這幾個老資格的同事推薦給了後來的直屬領導,說我這個年輕人有技術,頭腦又靈活,是塊好材料。

新成立的項目組,加上翻譯,好像是二十三個人,不算小的一個團隊了。我帶五個新人編碼。

就這樣幹了半年,領導覺得我的表現無可挑剔,確實能攻堅,就主動找我提加薪。當時激動壞了,內心告訴自己,一定要多要點,超過那幾個一塊來的本科生。

然後我就順嘴說了一個數目,自己覺得不算少了,領導也二話沒說就欣然答應了。

結果,我特么天真了,當時要太少了。後來幾個同事透露說,我提的額度和他們差不多。麻蛋,白白錯過了一次拉開差距的機會啊。

事後諸葛亮一下,要提加薪,最好了解一下公司內部的行情(想辦法)。領導他自己那是有個標準的,如果你確實表現優異,要想盡辦法知道領導這個線是多少,然後再這個基線上往上浮動一些,尤其是領導主動過來問價的時候。

第二次加薪是我離開蘇州的前一個月,2013 年底。這次加薪是公司主動加的,因為之前的合同到期了。

我當時和之前的那個領導鬧翻了,被調換到了另外一個開發部,所以動了離開的心思。再加上女朋友已經回到了洛陽,確實到了該和蘇州說再見的時候了。

拿到第一筆獎金后,我提了離職。結果公司比我精明得多,年底的獎金是分批發的,況且第二筆獎金比第一筆獎金多得多。我顯然是沒機會拿第二筆了。

過年前,領導終於批了我的離職申請,手續辦完,公積金取了出來后,我就回洛陽了——帶着不舍,畢竟在蘇州生活了三年半,有感情了。

第三次加薪是我回到洛陽工作后的第二個月,2014 年 3 月份。當時實習工資只有 2500,實在是受不了,第二個月過了一周我就迫不及待地找領導申請轉正了。

公司是一家挂名北大青鳥的培訓機構,不過我不是做講師,而是在獨立運營的開發部——承接一些政府或者個人項目。

團隊小,而我的實力確實過硬,再加上和領導、人事之間的關係好,公司就破格給我轉正了,工資上漲了 2/3,並且繳納了五險一金,加上績效獎,每個月到手的工資超過了當時洛陽的平均房價。

為什麼敢提轉正,敢提漲薪,這裏很重要的一點就是,我在團隊的表現是獨一檔的,工作上沒有解決不掉的難題,還能給領導提出建設性的意見,關係處得非常好。

每天早上,我基本上是第一個到工作崗位的,因為辦公室鑰匙我拿着,不去早也不行啊——當然了,這件事是我主動請纓的。

(心機 boy,有沒有?)

因為是指紋打卡嘛,我打卡的時間還被人事在月度總結會議上表揚過一次。上班早,下班我也不甘示弱,領導啥時候走我就啥時候走。

所以,綜合工作表現,為人處事的表現,公司不給我轉正說不過去啊,對不對?

簡單總結一下,也算是對讀者奔三十的一些建議。如何優雅地向公司提加薪?必須得做好以下三點:

第一,工作表現確實沒得說,該抗的事得能抗下來。逼着領導過來找你加薪,前提是自己一定要了解公司內部的漲薪結構,不要少要,也不要獅子大開口。

第二,如果是小公司的話,和領導的關係走得近一些,和同事之間的關係處得好一些,不要背後捅刀子。這樣提加薪的時候,領導不為難。

第三,臉皮要厚,臉皮要厚,臉皮要厚,重要的事情說三遍。

如果覺得文章對你有點幫助,請微信搜索「 沉默王二 」第一時間閱讀。回復關鍵字「簡歷」更有一份技術大佬整理的優質簡歷模板,助你一臂之力。

本文已收錄 GitHub,傳送門~ ,裏面更有大廠面試完整考點,歡迎 Star。

我是沉默王二,一枚有顏值卻靠才華苟且的程序員。關注即可提升學習效率,別忘了三連啊,點贊、收藏、留言,我不挑,嘻嘻

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※超省錢租車方案

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

※推薦台中搬家公司優質服務,可到府估價

分類
發燒車訊

移動UI系列 – 簡單地使用半衰期算法來預測手勢的滑動方向與速度

前言

有一個問題, 給定一個物體的運動軌跡, 包含時間和坐標的數組, 如何使用這個數據來預測物體未來的運動走勢??

本文提供了一個很簡單的方式去實現這個算法. 效果夠用, 又簡單, 有一定的準確程度. 

 

緣由

以前做過的一些手機應用, 沒有做動畫的, 也沒做手勢. 這個做起來挺麻煩的. 

最近開始了新的手機項目(微信項目模板) , 基於 Blazor server side , 其組件化的方式可以很方便地把各種常用的通用的東西封裝成 組件/控件 

於是, 就打算讓這段時間辛苦一些, 一次過把動畫的部分補上, 讓以後的項目更加牛逼, 意思是讓客戶更加樂意地掏錢. 

 

問題

手勢的一個問題是力度/速度判斷, 劃得快, 劃得慢, 先快後慢, 先慢後快, 都得有一個合適的算法來得到一個最後速度. 

這個算法一定要滿足基本的需求後, 要盡量簡單, 要目測, 理論上沒有bug . 

這個時候, 半衰期算法就派上用場了. (不知道有沒有半衰期算法這玩意, 應該說是使用半衰期的原理)

 

原理

這是一個很簡單的權重計算, 有很多個不同時間點的坐標, 每個相鄰時間的兩個數據, 有距離差, 有時間差

關鍵是解決先慢後快, 先快後慢的數據的處理問題, 那麼我們只需要

把新的數據, 乘以更大的權重, 把老的數據, 乘以更小的權重, 最後得到這個距離權重的合成值, 就OK了. 

 

預覽效果:

注意, 因為gif的幀數不夠, 要更好效果還是複製代碼運行一遍. 

 

 

測試代碼:

<! DOCTYPE html > 
< html > 
< head > 
    < meta charset ="utf-8"  /> 
    < title > Half Life </ title > 
    < style > 
        html, body, canvas { width : 100% ; height : 100% ; margin : 0 ; padding : 0 ; box-sizing : border-box ; overflow : hidden ; }
    </ style > 
</ head > 
< body >

    < canvas ></ canvas >

    < script type ="text/javascript" >

        var CONST_HALF_LIFE_SPAN =  50 ;     //半衰期,設置得越小,就越看淡以前的速度
        var CONST_MAX_SAMPLES =  15 ;         //保留最後15個點的數據減少無意義的運算量,主要看瀏覽器觸發onmousemove的頻率來調整.

        var pts = [];
        document.onmousemove =  function (e) {

            //儲存數據
            pts.push({ time: Date.now(), x: e.clientX, y: e.clientY });
             if (pts.length > CONST_MAX_SAMPLES) pts.shift();

            //計算

            var xs =  0 , ys =  0 ;     //走過的路的長度. 
            var previtem = pts[ 0 ];
             for ( var index =  1 ; index < pts.length; index ++ ) {
                 var newitem = pts[index ];
                 var t = newitem.time - previtem.time;

                //讓這個數據衰減一次,衰減程度由時間差決定. 
                var halflifefactor = Math.pow( 0.5 , t / CONST_HALF_LIFE_SPAN);

                //注意,這裏沒有計算速度,而是直接用距離來預測將來要走過的距離.

                //走過的距離每一次都要衰減,每一段的路程都多次乘以各時間差的factor, 
                //原理是, 0.5 ^ (t1 + t2 + t3...)等於0.5 ^ t1 * 0.5 ^ t2 * 0.5 ^ t3 * ... 
                xs = xs * halflifefactor + newitem.x - previtem.x;
                ys = ys * halflifefactor + newitem.y - previtem.y;

                previtem = newitem;
            }

            //畫圖

            var CONST_EFFECT_FACTOR =  2 ;     //乘以一個因素來畫圖用,這裏數值可以充當'摩擦係數'大小的效果. 
            xs = Math.floor(xs * CONST_EFFECT_FACTOR);
            ys = Math.floor(ys * CONST_EFFECT_FACTOR);

            var x0 = e.clientX, y0 = e.clientY;

            var canvas = document.querySelector( " canvas " );
             var ctx = canvas.getContext( " 2d " );
             var w = canvas.width = canvas.offsetWidth;
             var h = canvas.height = canvas.offsetHeight;
            ctx.clearRect( 0 , 0 , w, h);
            ctx.lineWidth =  5 ;
            ctx.strokeStyle =  " blue " ;
            ctx.beginPath();
            ctx.moveTo(x0, y0);
            ctx.lineTo(x0 + xs, y0 + ys);
            ctx.closePath();
            ctx.stroke();

            console.log(xs, ys)
        }
    </ script >

</ body >

</ html >

 

簡單講解

可以看出, 代碼量非常少. 與其說這是一種”算法” , 不然說是一種”思路” 

 

代碼先是記錄每一點的數據, 然後放進 pts 數組 

在鼠標移動後, 使用pts 數組, 計算出每一點的x , y 的移動量, 讓每一段的移動量都使用 半衰期 的方式進行調整. 

最後得出的xs , ys ,是理論上“最近移動的距離.”

使用這個數值,作為“參考值” ,就可以用於更多的判斷. 

 

例如我最近做的一個簡單的 swipe 組件, 

手指滑動了1/4個屏幕,然後再加上理論的參考值,  一共超過了1/2個屏幕,就切換到下一個panel 

這個參考值很重要. 如果手指只是很慢地滑動, 那麼參考值就很小, 就不應該切換. 如果手指很快地滑動, 那麼就應該切換panel

 

可改良的方案

上面方案, 是計算’移動距離’ 的, 它有一個弊端, 無論劃得多快, 移動的總數是有上限的. 

這段代碼完全可以 除以t , 得到一個 速度值,

這更加合理, 但是注意速度的合成方式不能普通地累加. 

 

這就留給有興趣的網友自己嘗試了. 也不難. 畢竟本文傳播的是 半衰期 的思路. 不宜說太細. 

 

 

擴展思路

其實這種算法, 一直都有人用. 很奇怪的就是, 沒有看到什麼人專門寫文章介紹? 

半衰期除了計算運動軌跡, 還可以很好地去統計熱度. 

例如一篇文章, 一個視頻, 有不同時段的點擊數量. 每一天都不一樣. 

 

怎樣使用最少的儲存方式, 去儲存一個合理的熱度參考值? 

可行的方法是,儲存兩個數值 (就夠了) : 

articleRateValue 用於儲存熱度值

articleRateTime 用於儲存熱度時間

 

每一次點擊, 都使用這個公式儲存: 

articleRateValue = newclickcount + articleRateValue * POW(0.5, (NOW – articleRateTime) / HALF_LIFE_SPAN )

articleRateTime = NOW 

排序的時候麻煩點, 要實時的計算 articleRateValue * POW(0.5, (NOW – articleRateTime) / HALF_LIFE_SPAN ) 來得到每個文章的熱度值. 

(對於排序的問題, 還有兩個變通的做法, 如果以後有時間寫一篇文章細說這個熱度方案, 再詳細解說)

(最簡單的變通方法是, 每晚找服務器空閑的時候, 把表裡的數值都重新計算一次, 平時排序的時候不計算) 

 

注意這裡有一個 HALF_LIFE_SPAN 的概念. 如果 HALF_LIFE_SPAN 是一天, 那麼昨天的熱度就佔1/2 權重, 前天的就佔 1/4 權重, 大前天的佔1/8 

這樣按天算也有個弊端, 我想按週, 按月算那怎麼辦? 

再存兩組數值 :

weeklyRateValue

weeklyRateTime

monthlyRateValue

monthlyRateTime 

如此類推. 

 

這樣做的最大好處是, 無需詳細地紀錄每一次的點擊數據. 非常節省空間.  

缺點是, 每一組數據, 只適合一個半衰期時段的數值, 要儲存多個參考值得準備多組數據. 

 

最後

時間過得真快. 這段時間挖的坑太多, 在業餘時間內根本沒有時間填坑..

5月初的時候實現了BlazorCefApp , 到現在開了幾個有意義的坑, Blazor微信項目模板 算是一個. 

但是由於沒有時間寫博客, 有時只是偶爾把測試的視頻放到B站: https://space.bilibili.com/540073960 

有興趣用Blazor 來做微信項目或手機網頁項目的, 可以去了解一下. 當項目成熟後, 會發佈到github上. 

—-

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

※別再煩惱如何寫文案,掌握八大原則!

【其他文章推薦】

※回頭車貨運收費標準

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※推薦評價好的iphone維修中心

※教你寫出一流的銷售文案?

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

分類
發燒車訊

詳解 Seata Golang 客戶端 AT 模式及其使用

源碼seata-golang

概述

  我們知道 Seata Java Client 的 AT 模式,通過代理數據源,實現了對業務代碼無侵入的分佈式事務協調機制,將與 Transaction Coordinator (TC) 交互的邏輯、Commit 的邏輯、Rollback 的邏輯,隱藏在切面和代理數據源相應的代碼中,使開發者無感知。那如果這個方法,要用 Golang 來實現一遍,應該如何操作呢?關於這個問題,我想了很久,最初的設想是,對 database/sql 的 mysql driver 進行增強,在對包 github.com/go-sql-driver/mysql 研究了一段時間后,還是沒有頭緒,不知如何下手,最後轉而增強 database/sql 包。由於 AT 模式必須保證本地事務的正確處理,在具體業務開發時,首先要通過 db.Begin() 獲得一個 Tx 對象,然後再 tx.Exec() 執行數據庫操作,最後 tx.Commit() 提交或 tx.Rollback() 回滾。這種處理方式算是一個 Golang 數據庫事務處理的基本操作。 所以對 database/sql 的增強,我們重點關注這幾個方法 db.Begin()tx.Exec()tx.Commit()tx.Rollback

事務提交、回滾

  通過 Seata Java Client 的相關代碼,我們知道,在本地事務提交的時候,主要是將分支事務註冊到 TC 上,並將數據庫操作產生的 undoLog 一起寫入到 undoLog 表;本地事務回滾的時候,需要將分支事務(即本地事務)的執行狀態報告給 TC,使 TC 好知道是否通知參与全局事務的其他分支回滾。

func (tx *Tx) Commit() error {
        //註冊分支事務
	branchId,err := tx.register()
	if err != nil {
		return errors.WithStack(err)
	}
	tx.tx.Context.BranchId = branchId

	if tx.tx.Context.HasUndoLog() {
                //將 undoLog 寫入 undoLog 表
		err = manager.GetUndoLogManager().FlushUndoLogs(tx.tx)
		if err != nil {
			err1 := tx.report(false)
			if err1 != nil {
				return errors.WithStack(err1)
			}
			return errors.WithStack(err)
		}
		err = tx.tx.Commit()
		if err != nil {
			err1 := tx.report(false)
			if err1 != nil {
				return errors.WithStack(err1)
			}
			return errors.WithStack(err)
		}
	} else {
		return tx.tx.Commit()
	}
	if tx.reportSuccessEnable {
		tx.report(true)
	}
	tx.tx.Context.Reset()
	return nil
}

  db.Begin() 會產生一個 Tx 對象,tx.Exec() 會產生 undoLog,tx.Commit() 將 undoLog 刷到數據庫中。那麼 undoLog 保存到哪裡呢?答案是 TxContext 中。

type TxContext struct {
	*context.RootContext
	Xid string
	BranchId int64
	IsGlobalLockRequire bool

	LockKeysBuffer *model.Set
	SqlUndoItemsBuffer []*undo.SqlUndoLog
}

  Commit() 方法中的 tx.tx.Context,第一個 tx 是封裝的 Tx 對象,第二個 tx 是 database/sql 的 Tx,tx.tx.Context 則是 TxContext。UndoLogManager 則是操作 undoLog 的核心對象,處理 undoLog 的插入、刪除,並查詢出 undoLog 用於回滾。

func (tx *Tx) Rollback() error {
	err := tx.tx.Rollback()
	if tx.tx.Context.InGlobalTransaction() && tx.tx.Context.IsBranchRegistered() {
                // 報告 TC 分支事務執行失敗
		tx.report(false)
	}
	tx.tx.Context.Reset()
	return err
}

  通過上面的代碼呢,我們知道增強型 Tx 對象需要向 TC 註冊分支事務,並報告分支事務的執行狀態,相應代碼如下:

func (tx *Tx) register() (int64,error) {
	return dataSourceManager.BranchRegister(meta.BranchTypeAT,tx.tx.ResourceId,"",tx.tx.Context.Xid,
		nil,tx.tx.Context.BuildLockKeys())
}

func (tx *Tx) report(commitDone bool) error {
	retry := tx.reportRetryCount
	for retry > 0 {
		var err error
		if commitDone {
			err = dataSourceManager.BranchReport(meta.BranchTypeAT, tx.tx.Context.Xid, tx.tx.Context.BranchId,
				meta.BranchStatusPhaseoneDone,nil)
		} else {
			err = dataSourceManager.BranchReport(meta.BranchTypeAT, tx.tx.Context.Xid, tx.tx.Context.BranchId,
				meta.BranchStatusPhaseoneFailed,nil)
		}
		if err != nil {
			logging.Logger.Errorf("Failed to report [%d/%s] commit done [%t] Retry Countdown: %d",
				tx.tx.Context.BranchId,tx.tx.Context.Xid,commitDone,retry)
			retry = retry -1
			if retry == 0 {
				return errors.WithMessagef(err,"Failed to report branch status %t",commitDone)
			}
		}
	}
	return nil
}

  和 TC 進行通信的主要邏輯還是在 DataSourceManager 裏面。AT 模式涉及的兩個關鍵對象 DataSourceManager、UndoLogManager 就浮出水面。一個用於遠程 TC 交互,一個用於本地數據庫處理。

事務執行

func (tx *Tx) Exec(query string, args ...interface{}) (sql.Result, error) {
	var parser = p.New()
        // 解析業務 sql
	act,_ := parser.ParseOneStmt(query,"","")
	deleteStmt,isDelete := act.(*ast.DeleteStmt)
	if isDelete {
		executor := &DeleteExecutor{
			tx:            tx.tx,
			sqlRecognizer: mysql.NewMysqlDeleteRecognizer(query,deleteStmt),
			values:        args,
		}
		return executor.Execute()
	}

	insertStmt,isInsert := act.(*ast.InsertStmt)
	if isInsert {
		executor := &InsertExecutor{
			tx:            tx.tx,
			sqlRecognizer: mysql.NewMysqlInsertRecognizer(query,insertStmt),
			values:        args,
		}
		return executor.Execute()
	}

	updateStmt,isUpdate := act.(*ast.UpdateStmt)
	if isUpdate {
		executor := &UpdateExecutor{
			tx:            tx.tx,
			sqlRecognizer: mysql.NewMysqlUpdateRecognizer(query,updateStmt),
			values:        args,
		}
		return executor.Execute()
	}

	return tx.tx.Tx.Exec(query,args)
}

  執行業務 sql,並生成 undoLog 的關鍵,在於識別業務 sql 執行了什麼操作:插入?刪除?修改?這裏使用 tidb 的 sql parser 去解析業務 sql,再使用相應的執行器去執行業務 sql,生成 undoLog 保存在 Tx_Context 中。

事務開啟

  db.Begin() 返回增強型的 Tx 對象。

func (db *DB) Begin(ctx *context.RootContext) (*Tx,error) {
	tx,err := db.DB.Begin()
	if err != nil {
		return nil,err
	}
	proxyTx := &tx2.ProxyTx{
		Tx:         tx,
		DSN:        db.conf.DSN,
		ResourceId: db.GetResourceId(),
		Context:    tx2.NewTxContext(ctx),
	}
	return &Tx{
		tx: proxyTx,
		reportRetryCount: db.conf.ReportRetryCount,
		reportSuccessEnable: db.conf.ReportSuccessEnable,
	},nil
}

seata-golang at 模式的使用

sample 代碼

  • 首先執行 scripts 腳本,初始化數據庫
    如果之前沒有初始化過 seata 數據庫,先執行 seata-golang/scripts/server/db/mysql.sql 腳本
  • 修改 dsn 數據庫配置,修改下列文件:
seata-golang/tc/app/profiles/dev/config.yml
seata-golang/samples/at/product_svc/conf/client.yml
seata-golang/samples/at/product_svc/conf/client.yml
  • 將下列文件中的 configPath 修改為 client.yml 配置文件的路徑
seata-golang/samples/at/product_svc/main.go
seata-golang/samples/at/order_svc/main.go
seata-golang/samples/at/aggregation_svc/main.go
  • 依次運行 tc、order_svc、product_svc、aggragation_svc,訪問下列地址開始測試:
http://localhost:8003/createSoCommit
http://localhost:8003/createSoRollback

TC 啟動參考參与 Seata 社區到 go 與 Seata 的邂逅

seata-golang 後續安排

  接下來不打算再增加新的 feature。Java 版 Seata 畢竟發展了一年多時間,並且有很多社區成員一起維護,Go 版本目前主要是我在開發,時間不到2個月,現有的代碼,僅是完成了框架,還需要大量優化,改bug,後續的工作重心在於使 seata-golang 穩定運行,生產可用,希望對分佈式事務感興趣且對 Go 感興趣的同學一起加入進來,一起做些事情。進入微信群,請加我微信:scottlewis,備註進群。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!

分類
發燒車訊

Kubernetes日誌的6個最佳實踐

本文轉自Rancher Labs

Kubernetes可以幫助管理部署在Pod中的上百個容器的生命周期。它是高度分佈式的並且各個部分是動態的。一個已經實現的Kubernetes環境通常涉及帶有集群和節點的幾個系統,這些系統託管着幾百個容器,而這些容器不斷地基於工作負載啟動、毀滅。

當在Kubernetes中處理大量的容器化應用和工作負載時,主動進行監控和調試錯誤十分重要。在容器、節點或集群級別,這些錯誤都能在容器中看到。Kubernetes的日誌機制是一個十分重要的組件,可以用來管理和監控服務以及基礎設施。在Kubernetes中,日誌可以讓你跟蹤錯誤甚至可以調整託管應用程序的容器的性能。

配置stdout(標準輸出)和stderr(標準錯誤)數據流

圖片來源:kubernetes.io

第一步是理解日誌是如何生成的。通過Kubernetes,日誌會被發送到兩個數據流——stdout和stderr。這些數據流將寫入JSON文件,並且此過程由Kubernetes內部處理。你可以配置將哪個日誌發送到哪個數據流中。而一個最佳實踐的建議是將所有應用程序日誌都發送到stdout並且所有錯誤日誌都發送到stderr。

決定是否使用Sidecar模型

Kubernetes建議使用sidecar容器來收集日誌。在這一方法中,每個應用程序容器將有一個鄰近的“streaming容器”,該容器將會將所有日誌流傳輸到stdout和stderr。Sidecar模型可以幫助避免在節點級別公開日誌,並且它可以讓你控制容器級別的日誌。

然而,這一模型的問題是它能夠適用於小容量的日誌記錄,如果面對大規模的日誌記錄,可能會造成大量資源被佔用。因此,你需要為每個正在運行的應用程序容器單獨運行一個日誌容器。在Kubernetes文檔中,將sidecar模型形容為“幾乎沒有很大的開銷”。需要由你決定是否嘗試這一模型並在選擇它之前查看它所消耗的資源類型。

替代方法是使用日誌代理,該代理在節點級別收集日誌。這樣可以減少開銷,並確保安全地處理日誌。Fluentd已成為大規模聚合Kubernetes日誌的最佳選擇。它充當Kubernetes與你要使用Kubernetes日誌的任意數量的端點之間的橋樑。你也可以選擇像Rancher這樣的Kubernetes管理平台,在應用商店已經集成了Fluentd,無需從頭開始安裝配置。

確定Fluentd可以更好地匯總和路由日誌數據后,下一步就是確定如何存儲和分析日誌數據。

選擇日誌分析工具:EFK或專用日誌記錄

傳統上,對於以本地服務器為中心的系統,應用程序日誌存儲在系統中的日誌文件中。這些文件可以在定義的位置看到,也可以移動到中央服務器。但是對於Kubernetes,所有日誌都發送到磁盤上/var/log的JSON文件中。這種類型的日誌聚合併不安全,因為節點中的Pod可以是臨時的也可以是短暫的。刪除Pod時,日誌文件將丟失。如果你需要嘗試對部分日誌數據丟失進行故障排除時,這可能很難。

Kubernetes官方推薦使用兩個選項:將所有日誌發送到Elasticsearch,或使用你選擇的第三方日誌記錄工具。同樣,這裏存在一個潛在的選擇。採用Elasticsearch路線意味着你需要購買一個完整的堆棧,即EFK堆棧,包括Elasticsearch、Fluentd和Kibana。每個工具都有其自己的作用。如上所述,Fluentd可以聚合和路由日誌。Elasticsearch是分析原始日誌數據並提供可讀輸出的強大平台。Kibana是一種開源數據可視化工具,可以從你的日誌數據創建漂亮的定製dashboard。這是一個完全開源的堆棧,是使用Kubernetes進行日誌記錄的強大解決方案。

儘管如此,有些事情仍然需要牢記。Elasticsearch除了由名為Elastic的組織構建和維護,還有龐大的開源社區開發人員為其做貢獻。儘管經過大量的實踐檢驗,它可以快速、強大地處理大規模數據查詢,但在大規模操作時可能會出現一些問題。如果採用的是自我管理(Self-managed)的Elasticsearch,那麼需要有人了解如何構建大規模平台。

替代方案是使用基於雲的日誌分析工具來存儲和分析Kubernetes日誌。諸如Sumo Logic和Splunk等工具都是很好的例子。其中一些工具利用Fluentd來將日誌路由到他們平台,而另一些可能有它們自己的自定義日誌代理,該代理位於Kubernetes中的節點級別。這些工具的設置十分簡單,並且使用這些工具可以花費最少的時間從零搭建一個可以查看日誌的dashboard。

使用RBAC控制對日誌的訪問

在Kubernetes中身份驗證機制使用的是基於角色訪問控制(RBAC)以驗證一個用戶的訪問和系統權限。根據用戶是否具有特權(authorization.k8s.io/decision )並向用戶授予原因(authorization.k8s.io/reason ),對在操作期間生成的審核日誌進行註釋。默認情況下,審核日誌未激活。建議激活它以跟蹤身份驗證問題,並可以使用kubectl進行設置。

保持日誌格式一致

Kubernetes日誌由Kubernetes架構中不同的部分生成。這些聚合的日誌應該格式一致,以便諸如Fluentd或FluentBit的日誌聚合工具更易於處理它們。例如,當配置stdout和stderr或使用Fluentd分配標籤和元數據時,需要牢記這一點。這種結構化日誌提供給Elasticsearch之後,可以減少日誌分析期間的延遲。

在日誌收集守護進程上設置資源限制

由於生成了大量日誌,因此很難在集群級別上管理日誌。DaemonSet在Kubernetes中的使用方式與Linux類似。它在後台運行以執行特定任務。Fluentd和filebeat是Kubernetes支持的用於日誌收集的兩個守護程序。我們必須為每個守護程序設置資源限制,以便根據可用的系統資源來優化日誌文件的收集。

結 論

Kubernetes包含多個層和組件,因此對其進行良好地監控和跟蹤能夠讓我們在面對故障時從容不迫。Kubernetes鼓勵使用無縫集成的外部“Kubernetes原生”工具進行日誌記錄,從而使管理員更輕鬆地獲取日誌。文章中提到的實踐對於擁有一個健壯的日誌記錄體繫結構很重要,該體繫結構在任何情況下都可以正常工作。它們以優化的方式消耗計算資源,並保持Kubernetes環境的安全性和高性能。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

※回頭車貨運收費標準

分類
發燒車訊

8.38萬元起同級別最精緻的SUV車主怎麼說

但是就是動力不咋的,不符合主流的1。5T發動機的動力表現。這估計就是在你看得見摸得着的地方做的很好,但是內在就不好說了。不滿意的:點煙器和車載電腦的USB接口在扶手箱下面的一個小盒子里,使用極不方便,儲物空間太小,畢竟這個車子的尺寸比較小。

其實剛開始寫這篇文章我是拒絕的,因為H2s才剛上市,車主根本不好找,但是很多讀者都想進一步了解一下H2s,想知道車主的使用情況。沒辦法,編者只有硬着頭皮,各種找朋友問同學,費了好大的勁才找到了一些車主,下面一起來看看這些車主對他們H2s的評價。

長城汽車-哈弗H2s

指導價:8.38-10.28萬

哈弗H2s是在11月18日廣州車展上市的,憑藉更亮麗的外觀和更精緻的內飾,再加上新的動力系統贏得了很大的關注度。

車主:雙截棍

購買車型:2017款 藍標 1.5T 手動精英型

裸車購買價:8.78萬

最滿意的地方:外觀很好看,我買車最看重的是顏值,當時對H2s一見傾心。風琴式的中控台個人認為很不錯,有點天馬星空的感覺。不過就是不好打理。萬年不變的1.5T發動機提速較慢,不過畢竟是帶渦輪的發動機,速度上來使勁踩油門提速還是有一點的。我身高1.75米,體重150斤,膝蓋老是碰到車前面的塑料蓋,所以大長腿的慎買啊!

不滿意的地方:新車異味好大,異響暫時沒發現,空間比較小,車子沒有胎壓監測。

車主:馬小虎

購買車型:2017款 紅標 1.5T 手動精英型

裸車購買價:8.88萬

最滿意的地方:當初在紅藍標之間猶豫,後來覺得年輕么。就要選個霸氣的外觀,大嘴的紅標也許更適合自己,雖然紅標比藍標貴了1000塊錢,但是卻多了胎壓監測裝置、后視鏡電動摺疊,這兩個配置還是值1000塊錢的。H2s的整體配置挺高的,內飾做工也比較好。但是就是動力不咋的,不符合主流的1.5T發動機的動力表現。這估計就是在你看得見摸得着的地方做的很好,但是內在就不好說了。

不滿意的:點煙器和車載電腦的USB接口在扶手箱下面的一個小盒子里,使用極不方便,儲物空間太小,畢竟這個車子的尺寸比較小。車子跑了300公里,油耗9L,新車沒有太大的參考性。

車主:狗子

購買車型:2017款 紅標 1.5T 自動精英型

裸車購買價:9.88萬

最滿意的地方:價格很實惠,配置比較高,內飾很精緻,做工也很好。其中變速箱為格特拉克的7速濕式雙離合變速箱。十萬的車子用上了濕式雙離合,看來長城這次是真的下本了。以前備受吐槽的哈弗變速箱終於在H2s上得到了改善。這款變速箱反應速度很快,邏輯清晰,平順性很好,底盤調教很紮實,行駛質感很好,但是還是受到那個老舊的發動機的拖累,提速較差。如果換個1.5T發動機,體驗會好很多吧!

不滿意的:1.5T發動機該換了,座椅靠前了安全帶就跑到了後面,懸架有點硬,兒童座椅的接口處理的很粗糙。油耗目前不到9L。

總結:H2s用上了7速濕式雙離合變速箱,這一點值得表揚,同時外觀靚麗,配置較高,內飾精緻,行駛質感較好。但是1.5T發動機確實該換了。如果很在乎動力,那麼H2s會讓你失望的。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

分類
發燒車訊

國產SUV稱霸!最接地氣的銷量榜解析

期望非常大,如果又遇到黑它的人,請大家備好磚頭,你懂的。

哈弗H6的銷量如此火爆,下個月還能賣7萬輛嗎?期望非常大,如果又遇到黑它的人,請大家備好磚頭,你懂的。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

網頁設計最專業,超強功能平台可客製化

分類
發燒車訊

價錢低逼格不低,小弟車型這麼屌,叫大哥怎麼混?

由於全系的X1使用了UKL平台,橫置布局的發動機,再加上對車內空間的優化,最終的表現非常出色,加上X1的第二排座椅還可以前後移動和調節靠背角度,空間表現已經超越了其大哥X3。X1 xDrive25Li使用了代號為B48A20Ο0的高功率版2。

在中國的傳統家庭里,有一個非常有趣的現象,如果你有哥哥或者姐姐,一般情況下都不能比哥哥姐姐早結婚,父母要催婚,肯定是先從哥哥姐姐下手,如果弟弟妹妹先結婚了,哥哥姐姐會過上很慘的被催婚生活。

其實在很多領域都存在着這樣的現象,例如考量業績的銷售業,鄉鎮級分銷點的銷量比縣城級分銷點高,那就非常尷尬了。

但是在汽車行業,卻是截然不同,消費者非常樂意看到某車型超越自己的大哥,最典型的例子就是經常被冠以“小S”稱號的奔馳E級。

我們拿同樣是320 L的奔馳E級與S級進行對比,雖然兩者採用了同樣的設計語言,但是在車身尺寸上還是相差甚遠的,奔馳E 320 L 4MATIC的車身尺寸為:5065x1860x1482 mm,軸距:3079 mm,奔馳S 320 L 商務型的車身尺寸為:5250x1899x1494 mm,軸距:3165 mm。

奔馳E級雖然後排座椅的橫向空間和頭部空間不及S級,但是後排的腿部縱向空間表現幾乎一樣,非常出色。奔馳E 320 L 4MATIC指導價:62.98萬,奔馳S 320 L 商務型指導價:93.80萬,兩者同樣擁有后風擋遮陽簾和後排側遮陽簾,E 320L還多出了後排側隱私玻璃。

兩車的指導價雖然相差30.82萬,但是發動機同樣是3.0T雙渦輪增壓V6發動機,E 320 L 4MATIC更是擁有9AT和全時四驅系統,配置上比S 320 L 商務型多出了無鑰匙進入、電動/感應後備廂、方向盤/電動座椅/后視鏡記憶功能,還有自適應巡航、主動剎車等一系列高科技配置。

雖然奔馳E級和S級在氣場上還是有不少差距,但如果讓選擇,還是會選擇E級,因為是配置控,同時E 320的机械品質足以滿足絕大多數情景的需求。

接下來的這個對比更加有看點,寶馬X1 xDrive25Li 豪華型對比寶馬X3 sDrive20i,兩者的指導價分別是43.9萬和42.1萬,價格非常接近,重點是兩車的車身尺寸差距很小,寶馬X1的軸距也只是比X3短了30mm而已。

由於全系的X1使用了UKL平台,橫置布局的發動機,再加上對車內空間的優化,最終的表現非常出色,加上X1的第二排座椅還可以前後移動和調節靠背角度,空間表現已經超越了其大哥X3。

X1 xDrive25Li使用了代號為B48A20Ο0的高功率版2.0T發動機,X3 sDrive20i則是使用N20B20的低功率版2.0T發動機,X1的動力表現比X3要好出不少,而且X1 xDrive25Li是前置適時四驅,X3 sDrive20i只是前置后驅。

在配置方面,X1 xDrive25Li多出了無鑰匙進入系統、電動/感應後備廂方向盤換擋、HUD抬頭显示、GpS導航、藍牙、LED大燈、後排出風口、自動泊車、車道偏離預警等等,第二排座椅還可以前後移動和調節靠背角度。

與奔馳的E級和S級不同,X1 xDrive25Li和X3 sDrive20i在價格上相差無幾,X1使用了新的平台、新的動力總成、新的設計,可以說是把還沒換代的老X3打敗了,動力更強、配置更高。

林肯的MKC和MKX這兩款SUV的情況和X1、X3的情況有點相似,我們拿MKC的2017款 2.3T 四驅總統系列與MKX的2015款 2.0T 兩驅尊享版作對比,兩者的指導價分別是43.88萬和44.98萬。

先來進行配置對比MKC僅多出了方向盤加熱、前排座椅通風、後排座椅加熱、自適應遠近光、感應雨刷、自動泊車入位、自適應巡航、車道偏離預警和併線輔助,兩者在配置上的差異其實並不是很大,主要是因為最低配的MKX配置水平真心不低。

MKC和MKX的外觀內飾設計都非常相似,慶幸的是兩車都還沒有使用林肯MKZ的那個最新前臉設計,它們都很好的保留了林肯家族該有的美式設計美學,霸氣的中網和貫穿式的尾燈非常漂亮。

空間表現並不是它們的優勢,重點是MKC和MKX的內飾氛圍都非常豪華,僅看內飾的話,真的感覺不出MKC定位比MKX低,這就是消費者最喜歡的典型例子:花更少的錢得到更高級的視覺享受。

其實同品牌的雞頭鳳尾之選,還是有不少的,例如日產的軒逸和天籟,新款天籟的外觀設計真是佩服,相信有不少人把天籟硬生生看成軒逸,還有國內即將上市的寶馬5系,無論是設計元素還是各項配置,都在向著7系靠攏。

雖然說上面提到的在很多方面都向著大哥靠攏,但我們還是要理性對待,外觀內飾的設計,還有車輛的配置,這些都能夠做到互相媲美,但是在車輛的行駛品質,動態體驗方面,不同級別的車型還是存在着本質區別的,是否值得購買,就要看你注重車輛的是哪個方面了。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計最專業,超強功能平台可客製化

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※教你寫出一流的銷售文案?

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家公司費用怎麼算?