單頁面應用特征
假設: 在一個 web 頁面中,有1個按鈕,點擊可跳轉到站內其他頁面。
多頁面應用: 點擊按鈕,會從新加載一個html資源,刷新整個頁面;
單頁面應用: 點擊按鈕,沒有新的html請求,只發生局部刷新,能營造出一種接近原生的體驗,如絲般順滑。
SPA 單頁面應用為什么可以幾乎無刷新呢?因為它的SP——single-page。在第一次進入應用時,即返回了唯一的html頁面和它的公共靜態資源,后續的所謂“跳轉”,都不再從服務端拿html文件,只是DOM的替換操作,是模(jia)擬(zhuang)的。
那么js又是怎么捕捉到組件切換的時機,并且無刷新變更瀏覽器url呢?靠hash和HTML5History。
hash 路由
特征
類似www.xiaoming.html#bar 就是哈希路由,當 # 后面的哈希值發生變化時,不會向服務器請求數據,可以通過 hashchange 事件來監聽到 URL 的變化,從而進行DOM操作來模擬頁面跳轉
不需要服務端配合
對 SEO 不友好
原理
hash
HTML5History 路由
特征
History 模式是 HTML5 新推出的功能,比之 hash 路由的方式直觀,長成類似這個樣子www.xiaoming.html/bar ,模擬頁面跳轉是通過 history.pushState(state, title, url) 來更新瀏覽器路由,路由變化時監聽 popstate 事件來操作DOM
需要后端配合,進行重定向
對 SEO 相對友好
原理
Html5 History
vue-router 源碼解讀
以 Vue 的路由vue-router為例,我們一起來擼一把它的源碼。
Tips:因為,本篇的重點在于講解單頁面路由的兩種模式,所以,下面只列舉了一些關鍵代碼,主要講解:
注冊插件
VueRouter的構造函數,區分路由模式
全局注冊組件
hash / HTML5History模式的 push 和監聽方法
transitionTo 方法
注冊插件
首先,作為一個插件,要有暴露一個install方法的自覺,給Vue爸爸去 use。
源碼的install.js文件中,定義了注冊安裝插件的方法install,給每個組件的鉤子函數混入方法,并在beforeCreate鉤子執行時初始化路由:
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
// 全文中以...來表示省略的方法
...
});
區分mode
然后,我們從index.js找到整個插件的基類 VueRouter,不難看出,它是在constructor中,根據不同mode 采用不同路由實例的。
...
import {install} from './install';
import {HashHistory} from './history/hash';
import {HTML5History} from './history/html5';
...
export default class VueRouter {
static install: () => void;
constructor (options: RouterOptions = {}) {
if (this.fallback) {
mode = 'hash'
}
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
}
}
全局注冊router-link組件
這個時候,我們也許會問:使用 vue-router 時, 常見的<router-link/>、 <router-view/>又是在哪里引入的呢?
回到install.js文件,它引入并全局注冊了 router-view、router-link組件:
import View from './components/view';
import Link from './components/link';
...
Vue.component('RouterView', View);
Vue.component('RouterLink', Link);
在 ./components/link.js 中,<router-link/>組件上默認綁定了click事件,點擊觸發handler方法進行相應的路由操作。
const handler = e => {
if (guardEvent(e)) {
if (this.replace) {
router.replace(location, noop)
} else {
router.push(location, noop)
}
}
};
就像最開始提到的,VueRouter構造函數中對不同mode初始化了不同模式的 History 實例,因而router.replace、router.push的方式也不盡相同。接下來,我們分別扒拉下這兩個模式的源碼。
hash模式
history/hash.js 文件中,定義了HashHistory 類,這貨繼承自 history/base.js 的 History 基類。
它的prototype上定義了push方法:在支持 HTML5History 模式的瀏覽器環境中(supportsPushState為 true),調用history.pushState來改變瀏覽器地址;其他瀏覽器環境中,則會直接用location.hash = path 來替換成新的 hash 地址。
其實最開始讀到這里是有些疑問的,既然已經是 hash 模式為何還要判斷supportsPushState?是為了支持scrollBehavior,history.pushState可以傳參key過去,這樣每個url歷史都有一個key,用 key 保存了每個路由的位置信息。
同時,原型上綁定的setupListeners 方法,負責監聽 hash 變更的時機:在支持 HTML5History 模式的瀏覽器環境中,監聽popstate事件;而其他瀏覽器中,則監聽hashchange。監聽到變化后,觸發handleRoutingEvent 方法,調用父類的transitionTo跳轉邏輯,進行 DOM 的替換操作。
import { pushState, replaceState, supportsPushState } from '../util/push-state'
...
export class HashHistory extends History {
setupListeners () {
...
const handleRoutingEvent = () => {
const current = this.current
if (!ensureSlash()) {
return
}
// transitionTo調用的父類History下的跳轉方法,跳轉后路徑會進行hash化
this.transitionTo(getHash(), route => {
if (supportsScroll) {
handleScroll(this.router, route, current, true)
}
if (!supportsPushState) {
replaceHash(route.fullPath)
}
})
}
const eventType = supportsPushState ? 'popstate' : 'hashchange'
window.addEventListener(
eventType,
handleRoutingEvent
)
this.listeners.push(() => {
window.removeEventListener(eventType, handleRoutingEvent)
})
}
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(
location,
route => {
pushHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
},
onAbort
)
}
}
...
// 處理傳入path成hash形式的URL
function getUrl (path) {
const href = window.location.href
const i = href.indexOf('#')
const base = i >= 0 ? href.slice(0, i) : href
return `${base}#${path}`
}
...
// 替換hash
function pushHash (path) {
if (supportsPushState) {
pushState(getUrl(path))
} else {
window.location.hash = path
}
}
// util/push-state.js文件中的方法
export const supportsPushState =
inBrowser &&
(function () {
const ua = window.navigator.userAgent
if (
(ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
ua.indexOf('Mobile Safari') !== -1 &&
ua.indexOf('Chrome') === -1 &&
ua.indexOf('Windows Phone') === -1
) {
return false
}
return window.history && typeof window.history.pushState === 'function'
})()
HTML5History模式
類似的,HTML5History 類定義在 history/html5.js 中。
定義push原型方法,調用history.pusheState修改瀏覽器的路徑。
與此同時,原型setupListeners 方法對popstate進行了事件監聽,適時做 DOM 替換。
import {pushState, replaceState, supportsPushState} from '../util/push-state';
...
export class HTML5History extends History {
setupListeners () {
const handleRoutingEvent = () => {
const current = this.current;
const location = getLocation(this.base);
if (this.current === START && location === this._startLocation) {
return
}
this.transitionTo(location, route => {
if (supportsScroll) {
handleScroll(router, route, current, true)
}
})
}
window.addEventListener('popstate', handleRoutingEvent)
this.listeners.push(() => {
window.removeEventListener('popstate', handleRoutingEvent)
})
}
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
pushState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
}
...
// util/push-state.js文件中的方法
export function pushState (url?: string, replace?: boolean) {
saveScrollPosition()
const history = window.history
try {
if (replace) {
const stateCopy = extend({}, history.state)
stateCopy.key = getStateKey()
history.replaceState(stateCopy, '', url)
} else {
history.pushState({ key: setStateKey(genStateKey()) }, '', url)
}
} catch (e) {
window.location[replace ? 'replace' : 'assign'](url)
}
}
transitionTo 處理路由變更邏輯
上面提到的兩種路由模式,都在監聽時觸發了this.transitionTo,這到底是個啥呢?它其實是定義在 history/base.js 基類上的原型方法,用來處理路由的變更邏輯。
先通過const route = this.router.match(location, this.current)對傳入的值與當前值進行對比,返回相應的路由對象;接著判斷新路由是否與當前路由相同,相同的話直接返回;不相同,則在this.confirmTransition中執行回調更新路由對象,并對視圖相關DOM進行替換操作。
export class History {
...
transitionTo (
location: RawLocation,
onComplete?: Function,
onAbort?: Function
) {
const route = this.router.match(location, this.current)
this.confirmTransition(
route,
() => {
const prev = this.current
this.updateRoute(route)
onComplete && onComplete(route)
this.ensureURL()
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
if (!this.ready) {
this.ready = true
this.readyCbs.forEach(cb => {
cb(route)
})
}
},
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務
JavaScript 語言中的 for 循環用于多次執行代碼塊,它是 JavaScript 中最常用的一個循環工具,還可用于數組的遍歷循環等。
我們為什么要使用 for 循環呢?打個比方,例如我們想要控制臺輸出1到1000之間的所有數字,如果單寫輸出語句,要寫1000句代碼,但是如果使用 for 循環,幾句代碼就能實現??傊?,使用 for 循環能夠讓我們寫代碼更方便快捷(當然啦,否則要它干嘛)。
for 循環語法
語法如下所示:
for(變量初始化; 條件表達式; 變量更新) {
// 條件表達式為true時執行的語句塊
}
變量初始化,表示代碼塊開始前執行。
條件表達式,定義運行循環代碼塊的條件。
變量更新,在循環代碼塊每次被執行之后再執行。
示例:
例如我們在一個HTML文件中,編寫如下代碼,實現計算1到100的總和:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JS_俠課島(9xkd.com)</title>
</head>
<body>
<script>
var result = 0;
for(var i = 1; i <= 100; i++) {
result = result + i;
}
alert(result);
</script>
</body>
</html>
在瀏覽器中打開這個文件,會彈出一個彈出層,彈出層中顯示的是1到100的總和:
上述代碼中,我們聲明了一個變量 result 并給它賦值為 0,表示初始的總和為 0 。
然后在 for 循環中三個語句:
變量初始化 i = 1,表示從 1 開始計算。
條件表達式 i <= 100,表示只要 i 小于等于 100 循環就會一直執行,當 i 大于 100 循環會停止。
變量更新 i++,之前我們學運算符的時候學過,這是遞增運算符 ++,表示為其操作數增加 1。
此時我們可以一點點來看這個 for 循環:
第一次循環: result = 0 + 1 // 此時result值為0, i的值為1
第二次循環: result = 1 + 2 // 此時result值為0+1,i的值為2
第三次循環: result = 3 + 3 // 此時result值為1+2,i的值為3
第四次循環: result = 6 + 4 // 此時result值為3+3,i的值為4
第五次循環: result = 10 + 5 // 此時result值為6+4,i的值為5
...
我們只需要搞清楚 for 循環中的執行原理,不需要手動來計算求和,只要寫好代碼,執行代碼后計算機會很快會告訴我們1到 100 的總和。
再補充一下,上述代碼中result = result + i,我們也可以寫成 result += i,這是我們之前學過的加賦值運算符,還記得嗎?
示例:
再來看一個例子,例如我們可以使用 for 循環來實現數組遍歷,首先定義一個數組 lst:
var lst = ["a", "b", "c", "d", "e"];
在寫 for 循環時,首先就是要搞清楚小括號里面的三個語句,因為我們可以通過數組中元素的下標索引來獲取元素的值,而數組的索引又是從 0 開始,所以變量初始化可以設置為i = 0。第二個條件表達式,因為數組中最后一個索引為 lst.length - 1,所以只要小于等于 lst.length - 1,循環就會一直執行。而i <= lst.length - 1 就相當于 i<lst.length。第三個變量更新,當循環每循環一次,索引值就加一,所以為 i++。
所以循環可以像下面這樣寫:
for(i = 0; i<lst.length; i++){
console.log(lst[i]); // 輸出數組中的元素值,從索引為0的值開始輸出,每次加1,一直到lst.length-1
}
輸出:
a
b
c
d
e
其實遍歷數組還有一種更好的方法,就是使用 for...in 循環語句來遍歷數組。
for...in 循環
for...in 循環主要用于遍歷數組或對象屬性,對數組或對象的屬性進行循環操作。for...in 循環中的代碼每執行一次,就會對數組的元素或者對象的屬性進行一次操作。
語法如下:
for (變量 in 對象) {
// 代碼塊
}
for 循環括號內的變量是用來指定變量,指定的可以是數組對象或者是對象屬性。
示例:
使用 for...in 循環遍歷我們定義好的 lst 數組:
var lst = ["a", "b", "c", "d", "e"];
for(var l in lst){
console.log(lst[l]);
}
輸出:
a
b
c
d
e
除了數組,for...in 循環還可以遍歷對象,例如我們遍歷 俠俠 的個人基本信息:
var object = {
姓名:'俠俠',
年齡:'22',
性別:'男',
出生日期:'1997-08-05',
職業:'程序員',
特長:'跳舞'
}
for(var i in object) {
console.log(i + ":" + object[i]);
}
輸出:
姓名: 俠俠
年齡: 22
性別: 男
出生日期: 1997-08-05
職業:程序員
特長:跳舞
動手小練習
請自定義一個長度為7的數組,然后通過 for 循環將數組中的元素遍歷出來。
求和:1~100的奇數和。
求和:1~100的偶數和。
使用對象定義一個人的個人信息(包括姓名、性別、年齡、出生日期、興趣愛好、職業、特長等),然后使用 for...in 循環將這些信息遍歷輸出。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務
表格需求
一般管理系統對表格會有以下需求
可以分頁(需要有分頁條)
可以多選(表格帶復選框)
頂部需要加一些操作按鈕(新增,刪除等等)
表格每行行尾有操作按鈕
表格行可以編輯
如下圖為一個示例表格
如果我們直接使用element-ui提供的組件的話,那么開發一個這樣的表格就需要使用到以下內容
需要使用表格的插槽功能,開發每一行的按鈕
需要通過樣式調整頂部按鈕,表格,分頁條的布局樣式
需要監聽分頁的事件然后去刷新表格數據
頂部按鈕或操作按鈕如果需要獲取表格數據,需要調用表格提供的api
對于有行編輯的需求,還需要通過插槽去渲染行編輯的內容,同時要控制行編輯的開關
不僅僅開發表格比較麻煩,而且還要考慮團隊協作,如果每個人實現表格的方式存在差別,那么可能后期的維護成本也會變得很高。那怎么辦呢?
項目安裝
安裝插件
在使用element-ui的項目中,可以通過以下命令進行安裝
npm install vue-elementui-table -S
在項目中使用
在main.js中添加以下代碼
import ZjTable from 'vue-element-table'
Vue.use(ZjTable)
然后即可像下文中的使用方式進行使用
表格配置
為了滿足團隊快速開發的需要,小編對上面提出來的需求進行了封裝,然后使用的時候,開發人員只需要配置一些JSON便可以完成以上功能的開發。
基礎配置
一個基礎的表格包含了數據和列信息,那么如何用封裝的表格去配置呢?
<template>
<zj-table
:columns="columns"
:data="data"
:pagination="false"
/>
</template>
<script>
export default {
data() {
return {
// 表格的列信息, 數組每一項代表一個字段,可以使用element 列屬性的所有屬性,以下僅為示例
columns: Object.freeze([
{
// 表頭顯示的文字
label: '姓名',
// 對應數據里面的字段
prop: 'name'
},
{
label: '性別',
prop: 'sex',
// 格式化表格,與element-ui 的表格屬性相同
formatter(row, column, cellValue) {
return cellValue === 1 ? '男' : '女'
}
},
{
label: '年齡',
prop: 'age'
}
]),
data: [
{
name: '子君',
sex: 1,
age: 18
}
]
}
}
}
</script>
通過上面的配置,就可以完成一個基礎表格的開發,完整代碼見 https://github.com/snowzijun/vue-element-table/blob/master/example/views/demo/base.vue,效果如下圖所示
表格默認會顯示復選框,也可以通過配置selectable屬性來關閉掉
添加分頁
簡單的表格用封裝之后的或未封裝的開發工作量區別并不大,我們繼續為表格添加上分頁
<template>
<!--
current-page.sync 表示頁碼, 添加上 .sync 在頁碼發生變化時自動同步頁碼
page-size.sync 每頁條數
total 總條數
height="auto" 配置height:auto, 表格高度會根據內容自動調整,如果不指定,表格將保持充滿父容器,同時表頭會固定,不跟隨滾動條滾動
@page-change 無論pageSize currentPage 哪一個變化,都會觸發這個事件
-->
<zj-table
v-loading="loading"
:columns="columns"
:data="data"
:current-page.sync="currentPage"
:page-size.sync="pageSize"
:total="total"
height="auto"
@page-change="$_handlePageChange"
/>
</template>
<script>
export default {
data() {
return {
columns: Object.freeze([
// 列字段與上例一樣,此處省略
]),
data: [],
// 當前頁碼
currentPage: 1,
// 每頁條數
pageSize: 10,
// 總條數
total: 0,
// 是否顯示loading
loading: false
}
},
created() {
this.loadData()
},
methods: {
// 加載表格數據
loadData() {
this.loading = true
setTimeout(() => {
// 假設總條數是40條
this.total = 40
const { currentPage, pageSize } = this
// 模擬數據請求獲取數據
this.data = new Array(pageSize).fill({}).map((item, index) => {
return {
name: `子君${currentPage + (index + 1) * 10}`,
sex: Math.random() > 0.5 ? 1 : 0,
age: Math.floor(Math.random() * 100)
}
})
this.loading = false
}, 1000)
},
$_handlePageChange() {
// 因為上面設置屬性指定了.sync,所以這兩個屬性會自動變化
console.log(this.pageSize, this.currentPage)
// 分頁發生變化,重新請求數據
this.loadData()
}
}
}
</script>
完整代碼請參考 https://github.com/snowzijun/vue-element-table/blob/master/example/views/demo/pagination.vue
通過封裝,表格將自帶分頁功能,通過上面代碼,實現效果如下所示,是不是變得簡單了一些。接下來我們繼續給表格添加按鈕
添加頂部按鈕
表格上面可能會有新增,刪除等等按鈕,怎么辦呢,接下來我們繼續通過配置去添加按鈕
<template>
<zj-table
:buttons="buttons"
/>
</template>
<script>
export default {
data() {
return {
buttons: Object.freeze([
{
// id 必須有而且是在當前按鈕數組里面是唯一的
id: 'add',
text: '新增',
type: 'primary',
icon: 'el-icon-circle-plus',
click: this.$_handleAdd
},
{
id: 'delete',
text: '刪除',
// rows 是表格選中的行,如果沒有選中行,則禁用刪除按鈕, disabled可以是一個boolean值或者函數
disabled: rows => !rows.length,
click: this.$_handleRemove
},
{
id: 'auth',
text: '這個按鈕根據權限顯示',
// 可以通過返回 true/false來控制按鈕是否顯示
before: (/** rows */) => {
return true
}
},
// 可以配置下拉按鈕哦
{
id: 'dropdown',
text: '下拉按鈕',
children: [
{
id: 'moveUp',
text: '上移',
icon: 'el-icon-arrow-up',
click: () => {
console.log('上移')
}
},
{
id: 'moveDown',
text: '下移',
icon: 'el-icon-arrow-down',
disabled: rows => !rows.length,
click: () => {
console.log('下移')
}
}
]
}
])
}
},
created() {},
methods: {
// 新增
$_handleAdd() {
this.$alert('點擊了新增按鈕')
},
// 頂部按鈕會自動將表格所選的行傳出來
$_handleRemove(rows) {
const ids = rows.map(({ id }) => id)
this.$alert(`要刪除的行id為${ids.join(',')}`)
},
// 關注作者公眾號
$_handleFollowAuthor() {}
}
}
</script>
表格頂部可以添加普通的按鈕,也可以添加下拉按鈕,同時還可以通過before來配置按鈕是否顯示,disabled來配置按鈕是否禁用,上面完整代碼見 https://github.com/snowzijun/vue-element-table/blob/master/example/views/demo/button.vue
通過上面的代碼就可以配置出下面的表格,是不是很簡單呢?
表格頂部可以有按鈕,行尾也是可以添加按鈕的,一起來看看
行操作按鈕
一般我們會將一些單行操作的按鈕放在行尾,比如編輯,下載等按鈕,那如何給行尾配置按鈕呢?
<template>
<zj-table
:columns="columns"
/>
</template>
<script>
export default {
data() {
return {
columns: Object.freeze([
{
// 可以指定列的寬度,與element-ui原生用法一致
width: 220,
label: '姓名',
prop: 'name'
},
// 行編輯按鈕,在表格末尾出現,自動鎖定右側
{
width: 180,
label: '操作',
// 通過 actions 指定行尾按鈕
actions: [
{
id: 'follow',
text: '關注作者',
click: this.$_handleFollowAuthor
},
{
id: 'edit',
text: '編輯',
// 可以通過before控制按鈕是否顯示,比如下面年齡四十歲的才會顯示編輯按鈕
before(row) {
return row.age < 40
},
click: this.$_handleEdit
},
{
id: 'delete',
text: '刪除',
icon: 'el-icon-delete',
disabled(row) {
return row.sex === 0
},
// 為了拿到this,這里需要用箭頭函數
click: () => {
this.$alert('女生被禁止刪除了')
}
}
]
}
])
}
},
methods: {
// 關注作者公眾號
$_handleFollowAuthor() {
console.log('微信搜索【前端有的玩】,這是對小編最大的支持')
},
/**
* row 這一行的數據
*/
$_handleEdit(row, column) {
this.$alert(`點擊了姓名為【${row.name}】的行上的按鈕`)
}
}
}
</script>
行操作按鈕會被凍結到表格最右側,不會跟隨滾動條滾動而滾動,上面完整代碼見, https://github.com/snowzijun/vue-element-table/blob/master/example/views/demo/button.vue
通過上面的代碼就可以完成以下效果
最后再來一起看看行編輯
行編輯
比如上例,我希望點擊行尾的編輯按鈕的時候,可以直接在行上面編輯用戶的姓名與性別,如何配置呢?
<template>
<zj-table
ref="table"
:columns="columns"
:data="data"
/>
</template>
<script>
export default {
data() {
return {
columns: Object.freeze([
{
label: '姓名',
prop: 'name',
editable: true,
field: {
componentType: 'input',
rules: [
{
required: true,
message: '請輸入姓名'
}
]
}
},
{
label: '性別',
prop: 'sex',
// 格式化表格,與element-ui 的表格屬性相同
formatter(row, column, cellValue) {
return cellValue === '1' ? '男' : '女'
},
editable: true,
field: {
componentType: 'select',
options: [
{
label: '男',
value: '1'
},
{
label: '女',
value: '0'
}
]
}
},
{
label: '年齡',
prop: 'age',
editable: true,
field: {
componentType: 'number'
}
},
{
label: '操作',
actions: [
{
id: 'edit',
text: '編輯',
// 如果當前行啟用了編輯,則不顯示編輯按鈕
before: row => {
return !this.editIds.includes(row.id)
},
click: this.$_handleEdit
},
{
id: 'save',
text: '保存',
// 如果當前行啟用了編輯,則顯示保存按鈕
before: row => {
return this.editIds.includes(row.id)
},
click: this.$_handleSave
}
]
}
]),
data: [
{
// 行編輯必須指定rowKey字段,默認是id,如果修改為其他字段,需要給表格指定row-key="字段名"
id: '0',
name: '子君',
sex: '1',
age: 18
},
{
// 行編輯必須指定rowKey字段,默認是id,如果修改為其他字段,需要給表格指定row-key="字段名"
id: '1',
name: '子君1',
sex: '0',
age: 18
}
],
editIds: []
}
},
methods: {
$_handleEdit(row) {
// 通過調用 startEditRow 可以開啟行編輯
this.$refs.table.startEditRow(row.id)
// 記錄開啟了行編輯的id
this.editIds.push(row.id)
},
$_handleSave(row) {
// 點擊保存的時候,通過endEditRow 結束行編輯
this.$refs.table.endEditRow(row.id, (valid, result, oldRow) => {
// 如果有表單驗證,則valid會返回是否驗證成功
if (valid) {
console.log('修改之后的數據', result)
console.log('原始數據', oldRow)
const index = this.editIds.findIndex(item => item === row.id)
this.editIds.splice(index, 1)
} else {
// 如果校驗失敗,則返回校驗的第一個輸入框的異常信息
console.log(result)
this.$message.error(result.message)
}
})
}
}
}
</script>
不需要使用插槽就可以完成行編輯,是不是很開心。上述完整代碼見 https://github.com/snowzijun/vue-element-table/blob/master/example/views/demo/row-edit.vue
效果如下圖所示:
其他功能
除了上面的功能之外,表格還可以配置其他許多功能,比如
可以指定字段為鏈接列,需要給列配置link屬性
可以通過插槽自定義頂部按鈕,行操作按鈕,行字段等
可以在按鈕區域右側通過插槽配置其他內容
其他等等
表格開發說明
通過上面的代碼示例,我們已經知道了封裝之后的表格可以完成哪些事情,接下來一起來看看表格是如何實現的。完整代碼見 https://github.com/snowzijun/vue-element-table/tree/master/src/components/zj-table
表格布局
整個表格是通過JSX來封裝的,因為JSX使用起來更加靈活。對于我們封裝的表格,我們從豎向可以分為三部分,分別是頂部按鈕區,中間表格區,底部分頁區,如何去實現三個區域的布局呢,核心代碼如下
render(h) {
// 按鈕區域
const toolbar = this.$_renderToolbar(h)
// 表格區域
const table = this.$_renderTable(h)
// 分頁區域
const page = this.$_renderPage(h)
return (
<div class="zj-table" style={{ height: this.tableContainerHeight }}>
{toolbar}
{table}
{page}
</div>
)
}
通過三個render函數分別渲染對應區域,然后將三個區域組合在一起。
渲染表格列
通過前文的講解,我們可以將表格的列分為以下幾種
常規列
行編輯列
操作按鈕列
插槽列
鏈接列(文檔后續完善)
嵌套列(文檔后續完善)
$_renderColumns(h, columns) {
// 整體是否排序
let sortable = this.sortable ? 'custom' : false
return columns
.filter(column => {
const { hidden } = column
if (hidden !== undefined) {
if (typeof hidden === 'function') {
return hidden({
columns,
column
})
}
return hidden
}
return true
})
.map(column => {
const {
useSlot = false,
// 如果存在操作按鈕,則actions為非空數組
actions = [],
// 是否可編輯列, 對于可編輯列需要動態啟用編輯
editable = false,
// 是否有嵌套列
nests,
// 是否可點擊
link = false
} = column
let newSortable = sortable
if (column.sortable !== undefined) {
newSortable = column.sortable ? 'custom' : false
}
column = {
...column,
sortable: newSortable
}
if (nests && nests.length) {
// 使用嵌套列
return this.$_renderNestColumn(h, column)
} else if (editable) {
// 使用編輯列
return this.$_renderEditColumn(h, column)
} else if (useSlot) {
// 使用插槽列
return this.$_renderSlotColumn(h, column)
} else if (actions && actions.length > 0) {
// 使用操作列
column.sortable = false
return this.$_renderActionColumn(h, column)
} else if (link) {
// 使用鏈接列
return this.$_renderLinkColumn(h, column)
} else {
// 使用默認列
return this.$_renderDefaultColumn(h, column)
}
})
},
行編輯列
當前表格行編輯支持input,select,datepicker,TimeSelect,InputNumber等組件,具體渲染代碼如下所示
// 編輯單元格
$_renderEditCell(h, field) {
const components = {
input: Input,
select: ZjSelect,
date: DatePicker,
time: TimeSelect,
number: InputNumber
}
const componentType = field.componentType
const component = components[componentType]
if (component) {
return this.$_renderField(h, field, component)
} else if (componentType === 'custom') {
// 如果自定義,可以通過component指定組件
return this.$_renderField(h, field, field.component)
}
return this.$_renderField(h, field, Input)
},
$_renderField(h, field, Component) {
// 編輯行的id字段
const { rowId, events = {}, nativeEvents = {} } = field
const getEvents = events => {
const newEvents = {}
Object.keys(events).forEach(key => {
const event = events[key]
newEvents[key] = (...rest) => {
const args = [
...rest,
{
rowId,
row: this.editRowsData[rowId],
value: this.editRowsData[rowId][field.prop]
}
]
return event(...args)
}
})
return newEvents
}
// 事件改寫
const newEvents = getEvents(events)
const newNativeEvents = getEvents(nativeEvents)
return (
<Component
size="small"
on={newEvents}
nativeOn={newNativeEvents}
v-model={this.editRowsData[rowId][field.prop]}
{...{
attrs: field,
props: field
}}
/>
)
}
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務
提交規范
AngularJS 在開發者文檔中關于 git commit 的指導說明,提到嚴格的 git commit 格式規范可以在瀏覽項目歷史的過程中看到更易讀的信息,并且能用 git commit 的信息直接生成 AngularJS 的 change log 。
commit messages 格式規范
commit messages 由 header 、body 、footer 組成。
header 又包含 type 、scope 、subject 。header 是必需的,不過其中的 scope 是可選的。
body 和 footer 可以省略。
<type>(<scope>): <subject>
// 空行
<BLANK LINE>
<body>
// 空行
<BLANK LINE>
<footer>
注:為了能在 github 以及各種 git 工具中看得更清晰,commit messages 的每一行都不要超過 100 個字符。
Header
Type
類型必須是以下幾種之一:
feat: 新功能
fix: bug 修復
docs: 僅修改文檔
style: 修改格式(空格,格式化,省略分號等),對代碼運行沒有影響
refactor: 重構(既不是修 bug ,也不是加功能)
build: 構建流程、外部依賴變更,比如升級 npm 包、修改 webpack 配置等
perf: 性能優化
test: 測試相關
chore: 對構建過程或輔助工具和庫(如文檔生成)的更改
ci: ci 相關的更改
除此之外,還有一個特殊的類型 revert ,如果當前提交是為了撤銷之前的某次提交,應該用 revert 開頭,后面加上被撤銷的提交的 header,在 body 中應該注明: This reverts commit <hash>. ,hash 指的就是將要被撤銷的 commit SHA 。
// 例如
revert: feat(user): add user type
This reverts commit ca16a365467e17915f0273392f4a13331b17617d.
Scope
scope 可以指定提交更改的影響范圍,這個視項目而定,當修改影響超過單個的 scope 時,可以指定為 * 。
Sbuject
subject 是指更改的簡潔描述,長度約定在 50 個字符以內,通常遵循以下幾個規范:
用動詞開頭,第一人稱現在時表述,例如:change 代替 changed 或 changes
第一個字母小寫
結尾不加句號(.)
Body
body 部分是對本地 commit 的詳細描述,可以分成多行。
跟 subject 類似,用動詞開頭,第一人稱現在時表述,例如:change 代替 changed 或 changes。
body 應該說明修改的原因和更改前后的行為對比。
Footer
footer 基本用在這兩種情況:
不兼容的改動( Breaking Changes ),通常用 BREAKING CHANGE: 開頭,后面跟一個空格或兩個換行符。剩余的部分就是用來說明這個變動的信息和遷移方法等。
關閉 Issue, github 關閉 Issue 的例子
// BREAKING CHANGE: 的例子
BREAKING CHANGE: isolate scope bindings definition has changed and
the inject option for the directive controller injection was removed.
To migrate the code follow the example below:
Before:
scope: {
myAttr: 'attribute',
myBind: 'bind',
myExpression: 'expression',
myEval: 'evaluate',
myAccessor: 'accessor'
}
After:
scope: {
myAttr: '@',
myBind: '@',
myExpression: '&',
// myEval - usually not useful, but in cases where the expression is assignable, you can use '='
myAccessor: '=' // in directive's template change myAccessor() to myAccessor
}
The removed `inject` wasn't generaly useful for directives so there should be no code using it.
// Closes Issue 例子
Closes #2314, #3421
完整的例子
例一: feat
feat($browser): onUrlChange event (popstate/hashchange/polling)
Added new event to $browser:
- forward popstate event if available
- forward hashchange event if popstate not available
- do polling when neither popstate nor hashchange available
Breaks $browser.onHashChange, which was removed (use onUrlChange instead)
例二: fix
fix($compile): couple of unit tests for IE9
Older IEs serialize html uppercased, but IE9 does not...
Would be better to expect case insensitive, unfortunately jasmine does
not allow to user regexps for throw expectations.
Closes #392
Breaks foo.bar api, foo.baz should be used instead
例三: style
style($location): add couple of missing semi colons
查看更多例子
規范 commit message 的好處
首行就是簡潔實用的關鍵信息,方便在 git history 中快速瀏覽
具有詳實的 body 和 footer ,可以清晰的看出某次提交的目的和影響
可以通過 type 過濾出想要查找的信息,也可以通過關鍵字快速查找相關提交
可以直接從 commit 生成 change log
// 列舉幾個常用的 log 參數
// 輸出 log 的首行
git log --pretty=oneline
// 只輸出首行的 commit 信息。不包含 hash 和 合并信息等
git log --pretty=format:%s
// 查找有關“更新菜單配置項”的提交
git log --grep="更新菜單配置項"
// 打印出 chenfangxu 的提交
git log --author=chenfangxu
// 紅色的短 hash,黃色的 ref , 綠色的相對時間
git log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr)%Creset'
用工具實現規范提交
上面介紹了規范提交的格式,如果讓各位同學在 git commit 的時候嚴格按照上面的規范來寫,首先心智是有負擔的,得記住不同的類型到底是用來定義什么的,subject 怎么寫,body 怎么寫,footer 要不要寫。其次,對人的規范大部分都是反人性的,所以很可能在過不了多久,就會有同學漸漸的不按照規范來寫。靠意志力來控制自己嚴格按照規范來寫是需要額外耗費一些精力的,把精力耗費在這種事情上面實在有些浪費。
用工具實現規范提交的方案,一種是在提交的時候就提示必填字段,另一種是在提交后校驗字段是否符合規范。這兩種在實際項目中都是很有必要的。
Commitizen
Zen-like commit messages for internet citizens. 嗯~~一種禪意
Commitizen 是一個幫助撰寫規范 commit message 的工具。他有一個命令行工具 cz-cli,接下來會把使用 Commitizen 分成幾個階段來介紹。
體驗 git cz
// 全局安裝 Commitizen
npm install -g commitizen
你的倉庫可能還不是對 Commitizen 友好的,此時運行 git cz 的效果跟 git commit 一樣,也就是沒有效果。 不過,可以執行 npx git-cz 來體驗。
如果想直接運行 git cz 實現語義化的提交,可以根據 streamich/git-cz 文檔中說的全局安裝 git cz。
// 全局安裝 git cz
npm install -g git-cz
除此之外還有一種更推薦的方式,就是讓你的倉庫對 Commitizen 友好。
Commitizen 友好
全局安裝 Commitizen 后,用 cz-conventional-changelog 適配器來初始化你的項目
// 初始化 cz-conventional-changelog 適配器
commitizen init cz-conventional-changelog --save-dev --save-exact
上面的初始化做了三件事:
安裝 cz-conventional-changelog 依賴
把依賴保存到 package.json 的 dependencies 或 devDependencies 中
在根目錄的 package.json 中 添加如下所示的 config.commitizen
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}
或者,在項目根目錄下新建一個 .czrc 文件,內容設置為
{
"path": "cz-conventional-changelog"
}
現在運行 git cz 效果如下:
cz-customizable 自定義中文配置
通過上面的截圖可以看到,提交的配置選項都是英文的,如果想改成中文的,可以使用 cz-customizable 適配器。
運行下面的命令,注意之前已經初始化過一次了,這次再初始化,需要加 --force 覆蓋
npm install cz-customizable --save-dev
commitizen init cz-customizable --save-dev --save-exact --force
現在 package.json 中 config.commitizen 字段為:
"config": {
"commitizen": {
"path": "./node_modules/cz-customizable"
}
}
cz-customizable 文檔中說明了查找配置文件的方式有三種,我們按照第一種,在項目根目錄創建一個 .cz-config.js 的文件。按照給出的示例 cz-config-EXAMPLE.js 編寫我們的 config。 commit-type 可以參考 conventional-commit-types 。
可以點擊查看我配置好的文件 qiqihaobenben/commitizen-git/.cz-config.js ,里面中詳細的注釋。
commitlint 校驗提交
Commitizen 文檔中開始就介紹到,Commitizen 可以在觸發 git commit 鉤子之前就能給出提示,但是也明確表示提交時對 commit messages 的校驗也是很有用的。畢竟即使用了 Commitzen,也是能繞過去,所以提交最后的校驗很重要。
commitlint 可以檢查 commit messages 是否符合常規提交格式,需要一份校驗配置,推薦 @commitlint/config-conventional 。
npm i --save-dev @commitlint/config-conventional @commitlint/cli
在項目根目錄創建 commitlint.config.js 文件并設置校驗規則:
module.exports = {
extends: ["@commitlint/config-conventional"],
// rules 里面可以設置一些自定義的校驗規則
rules: {},
};
在項目中安裝 husky ,并在項目根目錄新建 husky.config.js 文件,加入以下設置:
// 安裝 husky
npm install --save-dev husky
// husky.config.js 中加入以下代碼
module.exports = {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
}
注意:因為 @commitlint/config-conventional 校驗規則遵循 Angular 的規范, 所以我們在用 cz-customizable 自定義中文配置時, 是按照給出的符合 Angular 規范的示例 cz-config-EXAMPLE.js 編寫.cz-config.js 的。但是如果你自定義的 Commitizen 配置不符合 Angular 規范,可以使用 commitlint-config-cz 設置校驗規則。(推薦還是按照 Angular 規范進行 cz-customizable 自定義配置)
// 安裝 commitlint-config-cz
npm install commitlint-config-cz --save-dev
// commitlint.config.js 改為
module.exports = {
extends: [
'cz'
]
};
git commit 觸發 git cz
在提交的時候,我們都習慣了 git commit ,雖然換成 git cz 不難,但是如果讓開發者在 git commit 時無感知的觸發 git cz 肯定是更好的,
而且也能避免不熟悉項目的人直接 git commit 提交一些不符合規范的信息。
我們可以在 husky.config.js 中設置:
"hooks": {
"prepare-commit-msg": "exec < /dev/tty && git cz --hook || true",
}
注意: 在 window 系統,可能需要在 git base 中才能生效。
生成 CHANGELOG
standard-version
是一個使用 semver 和 conventional-commits 支持生成 CHANGELOG 進行版本控制的實用程序。
standard-version 不只是能生成 CHANGELOG , 還能根據 commit 的 type 來進行版本控制。
// 安裝 standard-verison
npm i --save-dev standard-version
// 在 package.json 中的 scripts 加入 standard-version
{
"scripts": {
"release": "standard-version"
}
}
示例項目
可以查看 commitizen-git ,里面歸納了快速配置 Commitizen 友好倉庫的步驟。
差不多三五分鐘就能搞定。
可以看一下配置完后,執行 git commit 的效果。
擴展
更復雜的自定義提示
cz-customizable 中自定義配置項通常情況是夠用的,
commitlint 中校驗的規則基本上也是夠用的,但是會有比較硬核的開發者會覺得還是不夠,還要更多。比如一些 prompt 更加自定義,
提交時詢問的 question 添加更多的邏輯,比如可以把一些重要的字段校驗提前到 Commitizen 中,或者添加更多自定義的校驗。
如果真想這么干,那就去 fork 一份 cz-conventional-changelog 或者 cz-customizable 來改,
或者直接自己寫一個 adapter。
Commitizen 友好徽章
如果把倉庫配置成了對 Commitizen 友好的話,可以在 README.md 中加上這個小徽章
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務
Web 安全是互聯網中不可或缺的一個領域,這個領域中誕生了大量的黑帽子與白帽子,他們都是安全領域的王者,在平時里,他們利用各種巧妙的技術互相博弈,時不時就會掀起一場 Web 安全浪潮,真可謂神仙打架,各顯神通。
本文從一個吃瓜群眾的角度,聊一聊 Web 安全的一些有趣故事。
安全世界觀
安全攻防案例
總結與思考
安全世界觀
在互聯網發展之初,IE 瀏覽器壟斷的時期,大家上網的目的都很單純,主要通過瀏覽器分享信息,獲取新聞。但隨著互聯網的不斷發展發展,一個網頁能做的事情越來越多,除了看新聞,我們還可以看視頻、玩游戲、購物、聊天等,這些功能都大大豐富了我們的生活。
隨著網頁功能的逐漸增多,就開始出現了一些黑帽子,他們試圖通過一些技術手段來牟取利益。在我小的時候,印象最深的就是木馬病毒,它可以監控你的鍵盤,將你在鍵盤上敲打的內容發送到黑客的機器上,黑客通過分析這些內容,很容易就能得到你的游戲賬號和密碼。
在這之后,就誕生出了一些殺毒軟件,致力于解決網絡上的各種病毒,隨著不斷地發展,殺毒軟件已經成為一臺電腦必不可少的軟件。
為什么會出現這樣的安全問題?
安全歸根到底是信任的問題,如果所有人都按照正常的流程去上網,不去謀取私利,也就沒有安全問題可談了。
安全的根本在于信任,但要讓所有人互相信任談何容易。在當前階段,我們可以做到:持續做好安全防護,讓漏洞越來越少,非法攻擊越來越困難,這樣就能逐漸減少黑帽子的數量,讓病毒制造者越來越少。
如何做好安全
要做好安全,首先得理解安全問題的屬性,前人通過無數實踐,最后將安全的屬性總結為安全三要素,分別為:機密性、完整性、可用性。
機密性
保護數據內容不被泄露。
通常使用加密的方法。
完整性
保護數據內容是完整的、沒有被篡改。
通常使用數字簽名的方法。
可用性
數據隨時都能夠使用。
通常是在防御 DOS。
有了安全 3 要素之后,我們就可以對安全問題進行評估了。
資產等級劃分
找出最重要的數據。
找出最重要數據的宿主空間,如:在數據庫里,那么數據庫就得重點防御。
找出數據庫的宿主空間,如:在一臺服務器上,那么這臺服務器就得做次等防御。
找出服務器的宿主空間,如:在 OSI 網絡層級上,那么在網絡層面就得做一般防御。
威脅分析
找出威脅(可能造成危害的來源)。
找出風險(可能出現的損失叫做風險)。
風險分析
采取多標準決策分析,即:風險 = 威脅等級 * 威脅可行性。
計算所有的威脅,將最終的風險進行排序,優先解決風險大的問題。
確認解決方案
找出不安全的實現方式,并確定解決方案。
解決方案不要改變商業需求的初衷。
解決方案需對用戶透明,不要改變用戶的習慣。
做好安全評估之后,我們就有了一份安全解決方案,后續的安全工作只需按照這個方案去做,就沒有任何問題。
安全的原則
有了安全解決方案之后,我們還可以制定一些安全原則,遵守原則做事,可以讓我們事半功倍。
黑名單、白名單原則
白名單方案指的是給安全的資源授權。
黑名單方案指的是禁用不安全的資源。
我們應該優先使用白名單方案,因為黑名單通常統計不完所有的不安全資源。
如:XSS 攻擊的方式非常多,可以通過 script、css、image 標簽等,盡管你將這些標簽都加入黑名單,也不能保證其他的標簽都沒有 XSS 的攻擊隱患。
最小權限原則
只授予必要的權限,不要過度授權,減少出錯機會。
如:普通權限的 Linux 用戶只能操作 ~ 文件夾下的目錄,如果有人想刪庫跑路,在執行 rm -rf / 時,就會提示無權限。
縱深防御原則
這條原則類似 木桶理論,安全水平往往取決于最短的那塊板。
即:不要留下短板,黑帽子們往往可以利用短板為突破口,挖掘更大的漏洞。
數據與代碼分離原則
當用戶數據被當成代碼執行時,混淆了數據和代碼的邊界,從而導致安全問題。
如:XSS 就是利用這一點去攻擊的。
不可預測性原則
這條原則是為了提高攻擊門檻,有效防止基于篡改、偽造的攻擊。
如:數據庫中使用 uuid 代替 number 型的自增主鍵,可以避免 id 被攻擊者猜到,從而進行批量操作。
token 也是利用不可預測性,攻擊者無法構造 token 也就無法進行攻擊。
有了這些安全原則,我們就可以開干了,接下來介紹幾個常見的攻防案例。
安全攻防案例
安全攻防的案例非常多,這里主要介紹幾個出鏡率比較高的安全問題。
客戶端攻擊
XSS 攻擊
CSRF 攻擊
點擊劫持
XSS 攻擊
XSS 攻擊的本質是將用戶數據當成了 HTML 代碼一部分來執行,從而混淆原本的語義,產生新的語義。
如圖所示,我們注冊了一個 <script>alert(document.cookie)</script> 的用戶名,所有能看到此用戶名字的頁面,都會彈出當前瀏覽器的 Cookie,如果代碼的邏輯是將 Cookie 發送到攻擊者的網站,攻擊者就能冒充當前用戶進行登錄了。
XSS 攻擊方式有很多,所有和用戶交互的地方,都有可能存在 XSS 攻擊。
例如:
所有 input 框。
window.location。
window.name。
document.referrer。
document.cookie。
localstorage。
...
由于頁面中與用戶交互的地方非常多,肯定還有一些 XSS 的攻擊方式沒有被發現,而一旦被黑帽子發現,就可能造成嚴重的影響,所以我們務必引起重視。
XSS 攻擊影響
被 XSS 攻擊成功后,攻擊者就可以獲取大量的用戶信息,例如:
識別用戶 UA。
識別用戶瀏覽器擴展。
識別用戶瀏覽過的網站。
通過 CSS 的 Visited 屬性。
獲取用戶真實的 IP。
通過 WebRTC 等。
盜取 Cookie
偽造用戶登錄,竊取用戶資料。
XSS 釣魚。
向頁面注入一個登錄彈窗,讓用戶認為是網站內的登錄彈窗(其實是釣魚網站的),一旦用戶登錄,賬號密碼就泄露給了釣魚網站。
XSS 攻擊防御
目前來說,XSS 已經得到了互聯網行業的重視,許多開發框架都內置了安全的 HTML 渲染方法。
我們也可以自定義進行一些安全配置。
配置 HTTP 中的 http-only 頭,讓前端 JS 不能操作 Cookie。
輸入檢查,在用戶提交數據時,使用 XssFilter 過濾掉不安全的數據。
輸出檢查,在頁面渲染的時候,過濾掉危險的數據。
CSRF 攻擊
CSRF(Cross-site request forgery)跨站請求偽造,是一種利用用戶身份,執行一些用戶非本意的操作。
如圖所示:
用戶先登錄了服務器 B,然后去訪問服務器 C。
服務器 C 通過惡意腳本,冒充 A 去調用服務器 B 上的某個功能,
對于服務器 B 來說,還以為這是 A 發起的請求,就當作正常請求處理了。
試想一下,如果 C 冒充 A 進行了一次轉賬,必定會造成大量的經濟損失。
CSRF 防御方式
防御 CSRF 主要有以下幾種方式:
驗證碼
每一次請求都要求用戶驗證,以確保請求真實可靠。
即:利用惡意腳本不能識別復雜的驗證碼的特點,保證每次請求都是合法的。
Referer 檢查
檢查發起請求的服務器,是否為目標服務器。
即:HTTP 請求中的 Referer 頭傳遞了當前請求的域名,如果此域名是非法服務器的域名,則需要禁止訪問。
Token
利用不可預測性原則,每一請求必須帶上一段隨機碼,這段隨機碼由正常用戶保存,黑帽子不知道隨機碼,也就無法冒充用戶進行請求了。
點擊劫持
點擊劫持是一種視覺欺騙的攻擊手段。攻擊者將需要攻擊的網站通過 iframe 嵌套的方式嵌入自己的網頁中,并將 iframe 設置為透明,在頁面中透出一個按鈕誘導用戶點擊。
就像一張圖片上面鋪了一層透明的紙一樣,你看到的是攻擊者的頁面,但是其實這個頁面只是在底部,而你真正點擊的是被攻擊者透明化的另一個網頁。
如果所示,當你點擊了頁面上的按鈕之后,本以為會...... ,而真正執行的操作是關注了某人的博客。
點擊劫持防御
由于點擊劫持主要通過 iframe,所以在防御時,主要基于 iframe 去做。
方案一:frame busting
正常網站使用 JS 腳本判斷是否被惡意網站嵌入,如:博客網站監測到被一個 iframe 打開,自動跳轉到正常的頁面即可。
if (self !== top) { // 跳回原頁面 top.location = self.location;}
方案二:使用 HTTP 中的 x-frame-options 頭,控制 iframe 的加載,它有 3 個值可選:
DENY,表示頁面不允許通過 iframe 的方式展示。
SAMEORIGIN,表示頁面可以在相同域名下通過 iframe 的方式展示。
ALLOW-FROM,表示頁面可以在指定來源的 iframe 中展示。
配置 iframe 的 sandbox 屬性
sandbox = "allow-same-origin" 則只能加載與主站同域的資源。
服務器端攻擊
服務器端的攻擊的方式也非常多,這里列舉幾個常見的。
SQL 注入攻擊
文件上傳漏洞
登錄認證攻擊
應用層拒絕服務攻擊
webServer 配置安全
SQL 注入攻擊
SQL 注入和 XSS 一樣,都是違背了數據和代碼分離原則導致的攻擊方式。
如圖所示,我們利用 SQL 注入,就能在不需要密碼的情況下,直接登錄管理員的賬號。
攻擊的前提是:后端只用了簡單的拼接 SQL 的方式去查詢數據。
# 拼接出來的 sql 如下:select * from user where username = 'admin' or 1=1 and password = 'xxx'# 無論密碼輸入什么,這條 sql 語句都能查詢到管理員的信息
除此之外,SQL 注入還有以下幾種方式:
使用 SQL 探測,猜數據庫表名,列名。
通過 MySQL 內置的 benchmark 探測數據庫字段。
如:一段偽代碼 select database as current if current[0]==='a',benchmark(10000,'猜對了') 如果表明猜對了,就延遲 10 s 并返回成功。
使用存儲過程執行系統命令
通過內置的方法或存儲過程執行 shell 腳本。
如:xp_cmdshell、sys_eval、sys_exec 等。
字符串截斷
如:MySQL 在處理超長的字符串時,會顯示警告,但會執行成功。
注冊一個 admin + 50 個空格的用戶,會觸發截斷,最終新增一個 admin 用戶,這樣就能擁有管理員權限了。
SQL 注入防御
防止 SQL 注入的最好的辦法就是,不要手動拼接 SQL 語句。
最佳方案,使用預編譯語句綁定變量
通常是指框架提供的拼接 SQL 變量的方法。
這樣的語義不會發生改變,變量始終被當成變量。
嚴格限制數據類型,如果注入了其他類型的數據,直接報錯,不允許執行。
使用安全的存儲過程和系統函數。
CRLF 注入
在注入攻擊中,換行符注入也是非常常見的一種攻擊方式。
如果在 HTTP 請求頭中注入 2 個換行符,會導致換行符后面的所有內容都被解析成請求實體部分。
攻擊者通常在 Set-Cookie 時,注入換行符,控制請求傳遞的內容。
文件上傳漏洞
上傳文件是網頁開發中的一個常見功能,如果不加處理,很容易就會造成攻擊。
如圖所示,攻擊者上傳了一個木馬文件,并且通過返回的 URL 進行訪問,就能控制服務器。
通常我們會控制上傳文件的后綴名,但也不能完全解決問題,攻擊者還可以通過以下方式進行攻擊:
偽造正常文件
將木馬文件偽裝成正常的后綴名進行上傳。
如果要避免這個問題,我們可以繼續判斷上傳文件的文件頭前 10 個字節。
Apache 解析方式是從后往前解析,直到找到一個認識的后綴名為止
如:上傳一個 abc.php.rar.rar.rar 能繞過后綴名檢查,但在執行時,被當成一個 php 文件進行執行。
IIS 會截斷分號進行解析
如:abc.asp;xx.png 能繞過后綴名檢查,但在執行時,被當成一個 asp 文件進行執行。
HTTP PUT 方法允許將文件上傳到指定位置
通過 HTTP MOVE 方法,還能修改上傳的文件名。
通過二者配合,就能先上傳一個正常的后綴名,然后改為一個惡意的后綴名。
PHP CGI 路徑問題
執行 http://abc.com/test.png/xxx.php 時,會把 test.png 當做 php 文件去解析。
如果用戶正好是把一段惡意的 php 腳本當做一張圖片進行上傳,就會觸發這個攻擊。
文件上傳漏洞防御
防御文件上傳漏洞,可以從以下幾點考慮:
將文件上傳的目錄設置為不可執行。
判斷文件類型
檢查 MIME Type,配置白名單。
檢查后綴名,配置白名單。
使用隨機數改寫文件名和文件路徑
上傳文件后,隨機修改文件名,讓攻擊者無法執行攻擊。
單獨設置文件服務器的域名
單獨做一個文件服務器,并使用單獨的域名,利用同源策略,規避客戶端攻擊。
通常做法是將靜態資源存放在 CDN 上。
登錄認證攻擊
登錄認證攻擊可以理解為一種破解登錄的方法。攻擊者通常采用以下幾種方式進行破解:
彩虹表
攻擊者通過搜集大量明文和 MD5 的對應關系,用于破解 MD5 密文找出原文。
對于彩虹表中的 MD5 密碼,我們可以加鹽,進行二次加密,避免被破解。
Session Fixation 攻擊
利用應用系統在服務器的 SessionID 固定不變機制,借助他人用相同的 SessionID 獲取認證和授權。
攻擊者登錄失敗后,后端返回了 SessionID,攻擊者將 SessionID 交給正常用戶去登錄,登錄成功后,攻擊者就能使用這個 SessionID 冒充正常用戶登錄了。
如果瀏覽器每一次登錄都刷新 SessionID 可以避免這個問題。
Session 保持攻擊
有些時候,后端出于用戶體驗考慮,只要這個用戶還活著,就不會讓這個用戶的 Session 失效。
攻擊者可以通過不停發起請求,可以讓這個 Session 一直活下去。
登錄認證防御方式
多因素認證
密碼作為第一道防御,但在密碼驗證成功后,我們還可以繼續驗證:動態口令,數字證書,短信驗證碼等,以保證用戶安全。
由于短信和網頁完全是 2 套獨立的系統,攻擊者很難獲取到短信驗證碼,也就無法進行攻擊。
除此之外,前端登錄認證還有多種方式,如果你對此感興趣,可以參考我之前寫的 前端登錄,這一篇就夠了。
應用層拒絕服務攻擊
應用層拒絕服務攻擊,又叫 DDOS 攻擊,它指的是利用大量的請求造成資源過載,導致服務器不可用。
通常有以下幾種 DDOS 攻擊方式:
SYN Flood 洪水攻擊
利用 HTTP 3 次握手機制,消耗服務器連接資源。
如:攻擊者發起大量的 HTTP 請求,但并不完成 3 次握手,而是只握手 2 次,這時服務器端會繼續等待直至超時。這時的服務器會一直忙于處理大量的垃圾請求,而無暇顧及正常請求。
Slowloris 攻擊
以非常低的速度發送 HTTP 請求頭,消耗服務器連接資源。
如:攻擊者發送大量 HTTP 請求,但每個請求頭都發的很慢,每隔 10s 發送一個字符,服務器為了等待數據,不得始終保持連接,這樣一來,服務器連接數很快就被占光了。
HTTP POST DOS
發送 HTTP 時,指定一個非常大的 Content-Length 然后以很長的間隔發送,消耗服務器連接資源。
CC 攻擊
針對一些非常消耗資源的頁面,不斷發起請求。
如:頁面中的某些頁面,需要后端做大量的運算,或者需要做非常耗時的數據庫查詢。在大量的請求下,服務器的 CPU、內存等資源可能就被占光了。
Server Limit DOS
通過 XSS 注入一段超長的 Cookie,導致超出 Web 服務器所能承受的 Request Header 長度,服務器端就會拒絕此服務。
ReDOS
針對一些缺陷的正則表達式,發起大量請求,耗光系統資源。
應用層拒絕服務攻擊防御
對于應用層拒絕服務攻擊,目前也沒有特別完美的解決方案,不過我們還是可以進行一些優化。
應用代碼做好性能優化
合理使用 Redis、Memcache 等緩存方案,減少 CPU 資源使用率。
網絡架構上做好優化
后端搭建負載均衡。
靜態資源使用 CDN 進行管理。
限制請求頻率
服務器計算所有 IP 地址的請求頻率,篩選出異常的 IP 進行禁用。
可以使用 LRU 算法,緩存前 1000 條請求的 IP,如果有 IP 請求頻率過高,就進行禁用。
其實,處理 DDOS 核心思路就是禁用不可信任的用戶,確保資源都是被正常的用戶所使用。
WebServer 配置安全
我們在部署 web 應用的時候,經常會用到 Nginx、Apache、IIS、Tomcat、Jboss 等 Web 服務器,這些服務器本身也存在一些安全隱患,如果配置不當,很容易收到攻擊。
在配置 Web 服務器時,可以參考以下幾點:
以用戶權限運行 Web 服務器
遵守最小權限原則,以最小權限身份運行 Web 服務器,限制被入侵后的權限。
刪除可視化后臺
運行 Tomcat、Jboss 等 Web 服務器時,默認會開啟一個可視化的運營后臺,運行在 8080 端口,并且第一次訪問是沒有認證的。
攻擊者可以利用可視化后臺,遠程加載一段 war 包或者上傳木馬文件,進行控制。
及時更新版本
主流的 Web 服務器,每隔一段時間就會修復一些漏洞,所以記得及時更新版本。
總結與思考
本文介紹了 Web 安全的基本概念,以及大量的攻防技巧,其實這只是 Web 安全中的冰山一角,如果你對此感興趣,不妨在安全領域繼續深耕學習,一定能看到更廣闊一片天。
對于一個開發者來說,我們應該在寫代碼時就將安全考慮其中,形成自己的一套安全開發體系,做到心中有安全,時時考慮安全,就能無形之中化解不法分子的攻擊。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務
CSS :before 選擇器
定義和說明
:before 選擇器向選定的元素前插入內容。
使用content 屬性來指定要插入的內容。
CSS :after 選擇器
定義和說明
:after 選擇器向選定的元素之后插入內容。
使用content 屬性來指定要插入的內容。
這兩個偽元素會在真正頁面元素之前和之后插入一個額外的元素,從技術角度上講,它們與下面的HTML標記是等效的。
1.偽類光圈
<div class="hover-circle">CSS</div>
.hover-circle {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
font-size: 3rem;
letter-spacing: 0.3rem;
font-weight: bold;
position: relative;
cursor: pointer;
color: #666;
}
.hover-circle::before {
width: 8.5rem;
height: 8.5rem;
border: 3px solid pink;
content: "";
border-radius: 50%;
position: absolute;
opacity: 0;
}
.hover-circle::after {
width: 7.2rem;
height: 7.2rem;
border: 6px solid pink;
content: "";
border-radius: 50%;
position: absolute;
opacity: 0;
}
.hover-circle:hover::before,
.hover-circle:hover::after {
animation-duration: 0.8s;
animation-delay: 0.2s;
animation: circle 0.8s;
}
@keyframes circle {
0% {
opacity: 0;
scale: 1;
}
25% {
opacity: 0.25;
}
50% {
opacity: 0.5;
scale: 1.03;
}
75% {
opacity: 0.75;
}
100% {
opacity: 1;
scale: 1.03;
}
}
2.偽類括號效果
<div class="hover-text">CSS</div>
.hover-text {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
font-size: 3rem;
letter-spacing: 0.3rem;
font-weight: bold;
position: relative;
cursor: pointer;
color: #666;
}
.hover-text::before {
content: "[";
position: absolute;
left: 0.8rem;
opacity: 0;
color: #999;
}
.hover-text::after {
content: "]";
position: absolute;
right: 0.8rem;
opacity: 0;
color: #999;
}
.hover-text:hover::before {
animation-duration: 0.8s;
animation-delay: 0.2s;
animation: hovertext1 0.8s;
}
.hover-text:hover::after {
animation-duration: 0.8s;
animation-delay: 0.2s;
animation: hovertext2 0.8s;
}
@keyframes hovertext1 {
0% {
opacity: 0;
left: 0.8rem;
}
100% {
opacity: 1;
left: 0.5rem;
}
}
@keyframes hovertext2 {
0% {
opacity: 0;
right: 0.8rem;
}
100% {
opacity: 1;
right: 0.5rem;
}
}
3.炫酷絲帶效果
雙邊絲帶
<div class="tc">
<div class="title1"><span>距離結束還有10天</span></div>
</div>
.title1 {
position: relative;
display: inline-block;
}
.title1 span {
position: relative;
z-index: 2;
display: inline-block;
padding: 0 15px;
height: 32px;
line-height: 32px;
background-color: #dc5947;
color: #fff;
font-size: 16px;
box-shadow: 0 10px 6px -9px rgba(0, 0, 0, 0.6);
}
.title1 span::before,
.title1 span::after {
position: absolute;
bottom: -6px;
border-width: 3px 5px;
border-style: solid;
content: "";
}
.title1 span::before {
left: 0;
border-color: #972f22 #972f22 transparent transparent;
}
.title1 span::after {
right: 0;
border-color: #972f22 transparent transparent #972f22;
}
.title1::before,
.title1::after {
position: absolute;
top: 6px;
content: "";
border-style: solid;
border-color: #dc5947;
}
.title1::before {
left: -32px;
border-width: 16px 26px 16px 16px;
border-left-color: transparent;
}
.title1::after {
right: -32px;
border-width: 16px 16px 16px 26px;
border-right-color: transparent;
}
右邊絲帶
<span class="title2">距離結束還有10天</span>
.title2 {
position: relative;
display: inline-block;
padding: 0 15px;
height: 32px;
line-height: 32px;
background-color: #dc5947;
color: #fff;
font-size: 16px;
}
.title2::before {
position: absolute;
top: -4px;
left: 0;
border-width: 2px 4px;
border-style: solid;
border-color: transparent #972f22 #972f22 transparent;
content: "";
}
.title2::after {
position: absolute;
top: 0;
right: -8px;
border-width: 16px 8px 16px 0;
border-style: solid;
border-color: #dc5947 transparent #dc5947 #dc5947;
content: "";
}
箭頭絲帶
<span class="title3">距離結束還有10天</span>
.title3 {
position: relative;
display: inline-block;
margin-right: 16px;
padding: 0 10px;
height: 32px;
line-height: 32px;
background-color: #dc5947;
color: #fff;
font-size: 16px;
}
.title3::before {
position: absolute;
top: 0;
left: -16px;
border-width: 16px 16px 16px 0;
border-style: solid;
border-color: transparent #dc5947 transparent transparent;
content: "";
}
.title3::after {
position: absolute;
top: 0;
right: -16px;
border-width: 16px 16px 16px 0;
border-style: solid;
border-color: #dc5947 transparent #dc5947 #dc5947;
content: "";
}
多個箭頭絲帶
<div class="mt30 pl16">
<span class="title3">距離結束還有10天</span>
<span class="title3 ml5">距離結束還有10天</span>
<span class="title3 ml5">距離結束還有10天</span>
</div>
.title4 {
width: 200px;
height: 140px;
position: absolute;
top: -8px;
left: -8px;
overflow: hidden;
}
.title4::before {
position: absolute;
left: 124px;
border-radius: 8px 8px 0 0;
width: 16px;
height: 8px;
background-color: #972f22;
content: "";
}
.title4::after {
position: absolute;
left: 0;
top: 124px;
border-radius: 0 8px 8px 0;
width: 8px;
height: 16px;
background-color: #972f22;
content: "";
}
.title4 span {
display: inline-block;
text-align: center;
width: 200px;
height: 40px;
line-height: 40px;
position: absolute;
top: 30px;
left: -50px;
z-index: 2;
overflow: hidden;
-ms-transform: rotate(-45deg);
-moz-transform: rotate(-45deg);
-webkit-transform: rotate(-45deg);
-o-transform: rotate(-45deg);
transform: rotate(-45deg);
border: 1px dashed #fff;
box-shadow: 0 0 0 3px #dc5947, 0 14px 7px -9px rgba(0, 0, 0, 0.6);
background-color: #dc5947;
color: #fff;
}
懸掛標簽
<div class="pr mt30" style="background-color: #eee; height: 200px;">
<div class="title4"><span>企業熱門動態</span></div>
<div class="title5"><span>企業熱門動態</span></div>
</div>
.title5 {
width: 140px;
height: 200px;
position: absolute;
top: -8px;
right: -8px;
overflow: hidden;
}
.title5::before {
position: absolute;
right: 124px;
border-radius: 8px 8px 0 0;
width: 16px;
height: 8px;
background-color: #972f22;
content: "";
}
.title5::after {
position: absolute;
right: 0;
top: 124px;
border-radius: 0 8px 8px 0;
width: 8px;
height: 16px;
background-color: #972f22;
content: "";
}
.title5 span {
display: inline-block;
text-align: center;
width: 200px;
height: 40px;
line-height: 40px;
position: absolute;
top: 30px;
right: -50px;
z-index: 2;
overflow: hidden;
-ms-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-webkit-transform: rotate(45deg);
-o-transform: rotate(45deg);
transform: rotate(45deg);
border: 1px dashed #fff;
box-shadow: 0 0 0 3px #dc5947, 0 14px 7px -9px rgba(0, 0, 0, 0.6);
background-color: #dc5947;
color: #fff;
}
4.幾何圖形
三角形
<div class="triangle"></div>
.triangle {
width: 0;
height: 0;
margin: 50px auto;
border-bottom: 100px solid #dc5947;
border-left: 50px solid transparent;
border-right: 50px solid transparent;
cursor: pointer;
transform: scale(1.2);
transition: 0.5s;
}
五角星
<div class="pentagram"></div>
.pentagram {
width: 0;
height: 0;
margin: 100px auto;
position: relative;
border-bottom: 70px solid #dc5947;
border-left: 100px solid transparent;
border-right: 100px solid transparent;
-webkit-transform: rotate(35deg);
-moz-transform: rotate(35deg);
-ms-transform: rotate(35deg);
-o-transform: rotate(35deg);
transform: rotate(35deg);
-webkit-transform: scale(1), rotate(35deg);
-moz-transform: scale(1), rotate(35deg);
-ms-transform: scale(1), rotate(35deg);
-o-transform: scale(1), rotate(35deg);
transform: scale(1), rotate(35deg);
}
.pentagram::after {
content: "";
width: 0;
height: 0;
border-bottom: 70px solid #dc5947;
border-left: 100px solid transparent;
border-right: 100px solid transparent;
-webkit-transform: rotate(-70deg);
-moz-transform: rotate(-70deg);
-ms-transform: rotate(-70deg);
-o-transform: rotate(-70deg);
transform: rotate(-70deg);
position: absolute;
top: 0px;
left: -100px;
}
.pentagram::before {
content: "";
width: 0;
height: 0;
border-bottom: 80px solid #dc5947;
border-left: 30px solid transparent;
border-right: 30px solid transparent;
-webkit-transform: rotate(-35deg);
-moz-transform: rotate(-35deg);
-ms-transform: rotate(-35deg);
-o-transform: rotate(-35deg);
transform: rotate(-35deg);
position: absolute;
top: -45px;
left: -60px;
}
5.水滴
<div class="drop"></div>
.drop::after {
content: "";
position: absolute;
width: 30px;
height: 20px;
border-radius: 50%;
background-color: #ace3ff;
margin: 100px auto;
top: -50px;
left: 25px;
box-shadow: 5px 12px 4px #ace3ff, -5px 11px 4px #ace3ff, 0px 14px 4px #4d576e;
-webkit-transform: rotate(35deg);
}
.drop::before {
content: "";
position: absolute;
width: 0px;
height: 0px;
border-style: solid;
border-width: 0 40px 50px 40px;
border-color: transparent transparent #ace3ff transparent;
top: -30px;
left: 10px;
}
.drop {
width: 100px;
height: 100px;
border-radius: 50%;
background-color: #ace3ff;
position: relative;
margin: 100px auto;
box-shadow: 0px 6px 0 #3f475a;
}
6 絢麗流動邊框
<div class="box-line1"></div>
.box-line2,
.box-line2::before,
.box-line2::after {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.box-line2 {
width: 200px;
height: 200px;
margin: auto;
color: #69ca62;
box-shadow: inset 0 0 0 1px rgba(105, 202, 98, 0.5);
}
.box-line2::before,
.box-line2::after {
content: "";
z-index: 99;
margin: -5%;
box-shadow: inset 0 0 0 2px;
animation: clipMe 8s linear infinite;
}
.box-line2::before {
animation-delay: -4s;
}
.box-line2:hover::after,
.box-line2:hover::before {
background-color: rgba(255, 0, 0, 0.3);
}
@keyframes clipMe {
0%,
100% {
clip: rect(0px, 220px, 2px, 0px);
}
25% {
clip: rect(0px, 2px, 220px, 0px);
}
50% {
clip: rect(218px, 220px, 220px, 0px);
}
75% {
clip: rect(0px, 220px, 220px, 218px);
}
}
@keyframes surround {
0%,
100% {
clip: rect(0px, 220px, 2px, 0px);
}
25% {
clip: rect(0px, 2px, 220px, 0px);
}
50% {
clip: rect(218px, 220px, 220px, 0px);
}
75% {
clip: rect(0px, 220px, 220px, 218px);
}
}
.box-line1:before,
.box-line1:after {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
content: "";
z-index: 99;
margin: -5%;
animation: surround linear infinite 8s;
box-shadow: inset 0 0 0 2px #69ca62;
}
.box-line1:before {
animation-delay: -4s;
}
.box-line1 {
border: 1px solid #69ca62;
position: absolute;
left: 500px;
top: 200px;
margin: auto;
width: 200px;
height: 200px;
margin: auto;
}
7.Tooltip提示
<div class="tip" data-tip="CSS偽類">CSS偽類</div>
.tip::after {
content: attr(data-tip);
display: none;
position: absolute;
padding: 5px 10px;
left: 15%;
bottom: 100%;
width: 150px;
margin-bottom: 12px;
transform: translateX(-50%);
font-size: 12px;
background: #000;
color: #fff;
cursor: default;
border-radius: 4px;
}
.tip::before {
content: " ";
position: absolute;
display: none;
left: 15%;
bottom: 100%;
transform: translateX(-50%);
margin-bottom: 3px;
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 9px solid #000;
}
.tip:hover::after,
.tip:hover::before {
display: block;
}
8.CSS 偽類盒子陰影
使用偽元素:before and :after制作出了完美驚艷的相片陰影效果。其中的技巧是使用絕對定位固定偽元素,然后給它們的z-index一個負值,以背景出現。
<div class="box effect2">
<h3>CSS 偽類盒子陰影</h3>
</div>
.effect2 {
position: relative;
}
.effect2::before, .effect2::after {
z-index: -1;
position: absolute;
content: "";
bottom: 15px;
left: 10px;
width: 50%;
top: 80%;
max-width: 300px;
background: #777;
-webkit-box-shadow: 0 15px 10px #777;
-moz-box-shadow: 0 15px 10px #777;
box-shadow: 0 15px 10px #777;
-webkit-transform: rotate(-3deg);
-moz-transform: rotate(-3deg);
-o-transform: rotate(-3deg);
-ms-transform: rotate(-3deg);
transform: rotate(-3deg);
}
.effect2::after {
-webkit-transform: rotate(3deg);
-moz-transform: rotate(3deg);
-o-transform: rotate(3deg);
-ms-transform: rotate(3deg);
transform: rotate(3deg);
right: 10px;
left: auto;
}
CSS Box 陰影效果
9.Tabs當前激活狀態
<div class="sm-box flex">
<div class="menu-tabs active">首頁</div>
<div class="menu-tabs">新聞</div>
<div class="menu-tabs">視頻</div>
<div class="menu-tabs">圖片</div>
</div>
.menu-tabs {
display: block;
padding: 0.25rem 1.5rem;
clear: both;
font-weight: 400;
color: #212529;
text-align: inherit;
white-space: nowrap;
background-color: transparent;
width: 50px;
border: 0;
height: 35px;
justify-content: center;
display: flex;
cursor: pointer;
}
.menu-tabs:hover {
color: #20a884;
position: relative;
}
.menu-tabs:hover:after {
position: absolute;
content: "";
border: 1px solid #20a884;
width: 3rem;
left: 0;
bottom: 0;
margin-left: 50%;
transform: translateX(-50%);
}
.active {
position: relative;
color: #20a884;
}
.flex {
display: flex;
}
.active::after {
position: absolute;
content: "";
border: 1px solid #20a884;
width: 3rem;
left: 0;
bottom: 0;
margin-left: 50%;
transform: translateX(-50%);
}
10.偽元素模糊背景
<div class="container">
<div class="overlay">
<h1>A blurred overlay</h1>
<p>... mask or whatever
<br>that is responsive and could be cross-browser compatible back to IE9</p>
</div>
</div>
.container {
width: 100%;
height: 100%;
margin: 0;
}
.container,
.overlay:before {
background: url(https://wow.techbrood.com/assets/landing.jpg) no-repeat fixed 0 0 / cover;
}
.container {
-webkit-box-align: center;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
}
.overlay {
max-height: 200px;
margin: 0 auto;
max-width: 768px;
padding: 50px;
position: relative;
color: white;
font-family: "Lato";
position: relative;
text-align: center;
z-index: 0;
}
.overlay:before {
content: "";
-webkit-filter: blur(100px);
filter: blur(100px);
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
z-index: -1;
}
11.藍湖文字
<span class="lanhu_text">
本站由叫我詹躲躲提供技術支持
</span>
.lanhu_text {
position: relative;
color: #2878ff;
}
.lanhu_text::before {
content: "";
width: 80px;
height: 20px;
position: absolute;
left: -86px;
top: 0;
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAABCAYAAABJwyn/AAAAjElEQVQoU22NSw7CQAxDX8ahICF2HIDTcf9d1c8kaDpthQSL6CmxHRuk8cZfMxqf6DGh+Y5uCxquaB7xdUCXilZHWvBorEiOaqAKrkRRUEmUOw283TKRRb9b4GnIEpWmGYrA237kDh1w6J5N7zzzZv13gtuvT7t++jefUTYmwvpk7v3fPaCzn//9LfsBvRpHnliu+xMAAAAASUVORK5CYII=) 0 no-repeat;
}
.lanhu_text::after {
content: "";
width: 80px;
height: 20px;
position: absolute;
right: -86px;
top: 0;
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAABCAYAAABJwyn/AAAAhElEQVQoU22OSw7CQAxDXzJDWbHiEFyRJQflFggJSjyDph9oC6snx45lO52rrKJqzIyNlkFUPv6vnv+3uW9vrPpBq77/uWFPAXnrnfYVQ16JiWq0stjvxKB9vDc/MeaXWo5cqDEFUkKpEuEoB3ollHvUZ9QV4rFD3R3d9ujwJK5HxAW9AStemAIOwCNlAAAAAElFTkSuQmCC) 100% no-repeat;
}
12 主要標題
<div class="first-title">服務項目</div>
.first-title {
position: relative;
color: #a98661;
font-weight: 400;
font-size: 30px;
text-align: center;
}
.first-title::before,
.first-title::after {
position: absolute;
content: "";
width: 110px;
border-bottom: 1px solid #a98661;
top: 50%;
transform: translateY(-50%);
}
.first-title::before {
left: 100px;
}
.first-title::after {
right: 100px;
}
13.鼠標浮層遮罩浮層
<div class="black-mask"></div>
.black-mask {
position: relative;
height: 100%;
width: 100%;
cursor: pointer;
}
.black-mask:hover {
transition-duration: 1s;
scale: 1.02;
}
.black-mask:hover:before {
object-fit: cover;
}
.black-mask:hover:after {
height: 100%;
opacity: 1;
transition-duration: 1s;
display: flex;
align-items: flex-end;
padding: 0 30px 15px;
}
.black-mask::before {
position: absolute;
content: "";
background: url(https://dcdn.it120.cc/2019/11/14/f17c5848-6d1f-4254-b3ba-64d3969d16b6.jpg) no-repeat;
background-size: 100% 100%;
width: 100%;
height: 100%;
}
.black-mask::after {
position: absolute;
content: "霧在微風的吹動下滾來滾去,像冰峰雪山,似蓬萊仙境,如海市蜃樓,使人覺得飄然欲仙。山河景色在霧的裝點下,變得更加美麗。遠處的七連山巍峨挺拔,它們仿佛成了神仙住的寶山,令人神往。近處池塘邊時時飄來霧氣,在初升陽光的照耀下,呈現出赤、橙、黃、綠、青、藍、紫七種色彩。......";
width: 90%;
height: 0%;
bottom: 0;
right: 0;
z-index: 32;
background: rgba(0, 0, 0, 0.3);
opacity: 1;
color: #fff;
opacity: 0;
padding: 0 30px 0;
}
14.絢麗光圈
<div class="aperture">光圈</div>
.aperture {
width: 136px;
height: 136px;
background-color: #dc5947;
border-radius: 50%;
line-height: 136px;
text-align: center;
color: #fff;
font-size: 24px;
cursor: pointer;
position: relative;
}
.aperture::before {
border: 3px dashed #a0ff80;
content: "";
width: 144px;
height: 144px;
position: absolute;
border-radius: 50%;
left: -8px;
top: -6px;
animation: clockwise 5s linear infinite;
}
@keyframes clockwise {
100% {
transform: rotate(360deg);
}
}
15.彩色流動邊框
<div class="rainbow"></div>
.rainbow {
position: relative;
z-index: 0;
width: 400px;
height: 300px;
border-radius: 10px;
overflow: hidden;
padding: 2rem;
}
.rainbow::before {
content: '';
position: absolute;
z-index: -2;
left: -50%;
top: -50%;
width: 200%;
height: 200%;
background-color: #399953;
background-repeat: no-repeat;
background-size: 50% 50%, 50% 50%;
background-position: 0 0, 100% 0, 100% 100%, 0 100%;
background-image: linear-gradient(#399953, #399953), linear-gradient(#fbb300, #fbb300), linear-gradient(#d53e33, #d53e33), linear-gradient(#377af5, #377af5);
-webkit-animation: rotate 4s linear infinite;
animation: rotate 4s linear infinite;
}
.rainbow::after {
content: '';
position: absolute;
z-index: -1;
left: 6px;
top: 6px;
width: calc(100% - 12px);
height: calc(100% - 12px);
background: white;
border-radius: 5px;
}
@keyframes rotate {
100% {
-webkit-transform: rotate(1turn);
transform: rotate(1turn);
}
}
16.炫酷偽類邊框
<div class="corner-button">CSS3</div>
.corner-button::before, .corner-button::after {
content: '';
position: absolute;
background: #2f2f2f;
z-index: 1;
transition: all 0.3s;
}
.corner-button::before {
width: calc(100% - 3rem);
height: calc(101% + 1rem);
top: -0.5rem;
left: 50%;
-webkit-transform: translateX(-50%);
transform: translateX(-50%);
}
.corner-button::after {
height: calc(100% - 3rem);
width: calc(101% + 1rem);
left: -0.5rem;
top: 50%;
-webkit-transform: translateY(-50%);
transform: translateY(-50%);
}
.corner-button:hover {
color: pink;
}
.corner-button {
font-family: 'Lato', sans-serif;
letter-spacing: .02rem;
cursor: pointer;
background: transparent;
border: 0.5rem solid currentColor;
padding: 1.5rem 2rem;
font-size: 2.2rem;
color: #06c17f;
position: relative;
transition: color 0.3s;
text-align: center;
margin: 5rem 12rem;
}
.corner-button:hover::after {
height: 0;
}
.corner-button:hover::before {
width: 0;
}
.bg-f2{
background: #2f2f2f;
}
17.偽類美化文字
<div class="beautify-font" data-text='躲躲'>躲躲</div>
<div class="beautify-font2" data-text='躲躲'>躲躲</div>
.beautify-font{
position: relative;
font-size: 12rem;
color: #0099CC
}
.beautify-font::before{
position: absolute;
font-size: 12rem;
color: #333;
content: attr(data-text);
white-space:nowrap;
width: 50%;
display: inline-block;
overflow: hidden;
transition:1s ease-in-out 0s;
}
.beautify-font2{
position: relative;
font-size: 6rem;
color: #0099CC
}
.beautify-font2::before{
position: absolute;
font-size: 6rem;
color: #333;
content: attr(data-text);
white-space:nowrap;
height: 50%;
display: inline-block;
overflow: hidden;
transition:1s ease-in-out 0s;
}
.beautify-font:hover::before{
width:0;
}
.beautify-font2:hover::before{
height: 0;
}
18.照片堆疊效果
只使用一張圖片來創造出一堆圖片疊摞在一起的效果,能做到嗎?當然,關鍵是要使用偽元素:before和:after來幫助呈現。把這些偽元素的z-index設置成負值,讓它們以背景方式起作用。
<div class="stackthree"><img src="./images/city.jpg"></div>
.stackthree::before {
background: #eff4de;
}
.stackthree, .stackthree::before, .stackthree::after {
border: 6px solid #fff;
height: 200px;
width: 200px;
-webkit-box-shadow: 2px 2px 5px rgba(0,0,0,0.3);
-moz-box-shadow: 2px 2px 5px rgba(0,0,0,0.3);
box-shadow: 2px 2px 5px rgba(0,0,0,0.3);
}
.stackthree::before {
top: 5px;
left: -15px;
z-index: -1;
-webkit-transform: rotate(-10deg);
-moz-transform: rotate(-10deg);
-o-transform: rotate(-10deg);
-ms-transform: rotate(-10deg);
transform: rotate(-10deg);
}
.stackthree::after {
top: -2px;
left: -10px;
-webkit-transform: rotate(-5deg);
-moz-transform: rotate(-5deg);
-o-transform: rotate(-5deg);
-ms-transform: rotate(-5deg);
transform: rotate(-5deg);
}
.stackthree::before, .stackthree::after {
background: #768590;
content: "";
position: absolute;
z-index: -1;
height: 0px\9;
width: 0px\9;
border: none\9;
}
.stackthree {
float: left;
position: relative;
margin: 50px;
}
為元素的兼容性
不論你使用單冒號還是雙冒號語法,瀏覽器都能識別。因為IE8只支持單冒號的語法,所以,如果你想兼容IE8,保險的做法是使用單冒號。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務
在最后的 阿寶哥有話說 環節,阿寶哥將介紹 WebSocket 與 HTTP 之間的關系、WebSocket 與長輪詢有什么區別、什么是 WebSocket 心跳及 Socket 是什么等內容。
下面我們進入正題,為了讓大家能夠更好地理解和掌握 WebSocket 技術,我們先來介紹一下什么是 WebSocket。
一、什么是 WebSocket
1.1 WebSocket 誕生背景
早期,很多網站為了實現推送技術,所用的技術都是輪詢。輪詢是指由瀏覽器每隔一段時間向服務器發出 HTTP 請求,然后服務器返回的數據給客戶端。常見的輪詢方式分為輪詢與長輪詢,它們的區別如下圖所示:
為了更加直觀感受輪詢與長輪詢之間的區別,我們來看一下具體的代碼:
這種傳統的模式帶來很明顯的缺點,即瀏覽器需要不斷的向服務器發出請求,然而 HTTP 請求與響應可能會包含較長的頭部,其中真正有效的數據可能只是很小的一部分,所以這樣會消耗很多帶寬資源。
比較新的輪詢技術是 Comet)。這種技術雖然可以實現雙向通信,但仍然需要反復發出請求。而且在 Comet 中普遍采用的 HTTP 長連接也會消耗服務器資源。
在這種情況下,HTML5 定義了 WebSocket 協議,能更好的節省服務器資源和帶寬,并且能夠更實時地進行通訊。Websocket 使用 ws 或 wss 的統一資源標志符(URI),其中 wss 表示使用了 TLS 的 Websocket。如:
ws://echo.websocket.org
wss://echo.websocket.org
WebSocket 與 HTTP 和 HTTPS 使用相同的 TCP 端口,可以繞過大多數防火墻的限制。默認情況下,WebSocket 協議使用 80 端口;若運行在 TLS 之上時,默認使用 443 端口。
1.2 WebSocket 簡介
WebSocket 是一種網絡傳輸協議,可在單個 TCP 連接上進行全雙工通信,位于 OSI 模型的應用層。WebSocket 協議在 2011 年由 IETF 標準化為 RFC 6455,后由 RFC 7936 補充規范。
WebSocket 使得客戶端和服務器之間的數據交換變得更加簡單,允許服務端主動向客戶端推送數據。在 WebSocket API 中,瀏覽器和服務器只需要完成一次握手,兩者之間就可以創建持久性的連接,并進行雙向數據傳輸。
介紹完輪詢和 WebSocket 的相關內容之后,接下來我們來看一下 XHR Polling 與 WebSocket 之間的區別:
1.3 WebSocket 優點
較少的控制開銷。在連接創建后,服務器和客戶端之間交換數據時,用于協議控制的數據包頭部相對較小。
更強的實時性。由于協議是全雙工的,所以服務器可以隨時主動給客戶端下發數據。相對于 HTTP 請求需要等待客戶端發起請求服務端才能響應,延遲明顯更少。
保持連接狀態。與 HTTP 不同的是,WebSocket 需要先創建連接,這就使得其成為一種有狀態的協議,之后通信時可以省略部分狀態信息。
更好的二進制支持。WebSocket 定義了二進制幀,相對 HTTP,可以更輕松地處理二進制內容。
可以支持擴展。WebSocket 定義了擴展,用戶可以擴展協議、實現部分自定義的子協議。
由于 WebSocket 擁有上述的優點,所以它被廣泛地應用在即時通信、實時音視頻、在線教育和游戲等領域。對于前端開發者來說,要想使用 WebSocket 提供的強大能力,就必須先掌握 WebSocket API,下面阿寶哥帶大家一起來認識一下 WebSocket API。
二、WebSocket API
在介紹 WebSocket API 之前,我們先來了解一下它的兼容性:
(圖片來源:https://caniuse.com/#search=W...)
從上圖可知,目前主流的 Web 瀏覽器都支持 WebSocket,所以我們可以在大多數項目中放心地使用它。
在瀏覽器中要使用 WebSocket 提供的能力,我們就必須先創建 WebSocket 對象,該對象提供了用于創建和管理 WebSocket 連接,以及可以通過該連接發送和接收數據的 API。
使用 WebSocket 構造函數,我們就能輕易地構造一個 WebSocket 對象。接下來我們將從 WebSocket 構造函數、WebSocket 對象的屬性、方法及 WebSocket 相關的事件四個方面來介紹 WebSocket API,首先我們從 WebSocket 的構造函數入手:
2.1 構造函數
WebSocket 構造函數的語法為:
const myWebSocket = new WebSocket(url [, protocols]);
相關參數說明如下:
url:表示連接的 URL,這是 WebSocket 服務器將響應的 URL。
protocols(可選):一個協議字符串或者一個包含協議字符串的數組。這些字符串用于指定子協議,這樣單個服務器可以實現多個 WebSocket 子協議。比如,你可能希望一臺服務器能夠根據指定的協議(protocol)處理不同類型的交互。如果不指定協議字符串,則假定為空字符串。
當嘗試連接的端口被阻止時,會拋出 SECURITY_ERR 異常。
2.2 屬性
WebSocket 對象包含以下屬性:
每個屬性的具體含義如下:
binaryType:使用二進制的數據類型連接。
bufferedAmount(只讀):未發送至服務器的字節數。
extensions(只讀):服務器選擇的擴展。
onclose:用于指定連接關閉后的回調函數。
onerror:用于指定連接失敗后的回調函數。
onmessage:用于指定當從服務器接受到信息時的回調函數。
onopen:用于指定連接成功后的回調函數。
protocol(只讀):用于返回服務器端選中的子協議的名字。
readyState(只讀):返回當前 WebSocket 的連接狀態,共有 4 種狀態:
CONNECTING — 正在連接中,對應的值為 0;
OPEN — 已經連接并且可以通訊,對應的值為 1;
CLOSING — 連接正在關閉,對應的值為 2;
CLOSED — 連接已關閉或者沒有連接成功,對應的值為 3。
url(只讀):返回值為當構造函數創建 WebSocket 實例對象時 URL 的絕對路徑。
2.3 方法
close([code[, reason]]):該方法用于關閉 WebSocket 連接,如果連接已經關閉,則此方法不執行任何操作。
send(data):該方法將需要通過 WebSocket 鏈接傳輸至服務器的數據排入隊列,并根據所需要傳輸的數據的大小來增加 bufferedAmount 的值 。若數據無法傳輸(比如數據需要緩存而緩沖區已滿)時,套接字會自行關閉。
2.4 事件
使用 addEventListener() 或將一個事件監聽器賦值給 WebSocket 對象的 oneventname 屬性,來監聽下面的事件。
close:當一個 WebSocket 連接被關閉時觸發,也可以通過 onclose 屬性來設置。
error:當一個 WebSocket 連接因錯誤而關閉時觸發,也可以通過 onerror 屬性來設置。
message:當通過 WebSocket 收到數據時觸發,也可以通過 onmessage 屬性來設置。
open:當一個 WebSocket 連接成功時觸發,也可以通過 onopen 屬性來設置。
介紹完 WebSocket API,我們來舉一個使用 WebSocket 發送普通文本的示例。
2.5 發送普通文本
在以上示例中,我們在頁面上創建了兩個 textarea,分別用于存放 待發送的數據 和 服務器返回的數據。當用戶輸入完待發送的文本之后,點擊 發送 按鈕時會把輸入的文本發送到服務端,而服務端成功接收到消息之后,會把收到的消息原封不動地回傳到客戶端。
// const socket = new WebSocket("ws://echo.websocket.org");
// const sendMsgContainer = document.querySelector("#sendMessage");
function send() {
const message = sendMsgContainer.value;
if (socket.readyState !== WebSocket.OPEN) {
console.log("連接未建立,還不能發送消息");
return;
}
if (message) socket.send(message);
}
當然客戶端接收到服務端返回的消息之后,會把對應的文本內容保存到 接收的數據 對應的 textarea 文本框中。
// const socket = new WebSocket("ws://echo.websocket.org");
// const receivedMsgContainer = document.querySelector("#receivedMessage");
socket.addEventListener("message", function (event) {
console.log("Message from server ", event.data);
receivedMsgContainer.value = event.data;
});
為了更加直觀地理解上述的數據交互過程,我們使用 Chrome 瀏覽器的開發者工具來看一下相應的過程:
以上示例對應的完整代碼如下所示:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WebSocket 發送普通文本示例</title>
<style>
.block {
flex: 1;
}
</style>
</head>
<body>
<h3>阿寶哥:WebSocket 發送普通文本示例</h3>
<div style="display: flex;">
<div class="block">
<p>即將發送的數據:<button onclick="send()">發送</button></p>
<textarea id="sendMessage" rows="5" cols="15"></textarea>
</div>
<div class="block">
<p>接收的數據:</p>
<textarea id="receivedMessage" rows="5" cols="15"></textarea>
</div>
</div>
<script>
const sendMsgContainer = document.querySelector("#sendMessage");
const receivedMsgContainer = document.querySelector("#receivedMessage");
const socket = new WebSocket("ws://echo.websocket.org");
// 監聽連接成功事件
socket.addEventListener("open", function (event) {
console.log("連接成功,可以開始通訊");
});
// 監聽消息
socket.addEventListener("message", function (event) {
console.log("Message from server ", event.data);
receivedMsgContainer.value = event.data;
});
function send() {
const message = sendMsgContainer.value;
if (socket.readyState !== WebSocket.OPEN) {
console.log("連接未建立,還不能發送消息");
return;
}
if (message) socket.send(message);
}
</script>
</body>
</html>
其實 WebSocket 除了支持發送普通的文本之外,它還支持發送二進制數據,比如 ArrayBuffer 對象、Blob 對象或者 ArrayBufferView 對象:
const socket = new WebSocket("ws://echo.websocket.org");
socket.onopen = function () {
// 發送UTF-8編碼的文本信息
socket.send("Hello Echo Server!");
// 發送UTF-8編碼的JSON數據
socket.send(JSON.stringify({ msg: "我是阿寶哥" }));
// 發送二進制ArrayBuffer
const buffer = new ArrayBuffer(128);
socket.send(buffer);
// 發送二進制ArrayBufferView
const intview = new Uint32Array(buffer);
socket.send(intview);
// 發送二進制Blob
const blob = new Blob([buffer]);
socket.send(blob);
};
以上代碼成功運行后,通過 Chrome 開發者工具,我們可以看到對應的數據交互過程:
下面阿寶哥以發送 Blob 對象為例,來介紹一下如何發送二進制數據。
Blob(Binary Large Object)表示二進制類型的大對象。在數據庫管理系統中,將二進制數據存儲為一個單一個體的集合。Blob 通常是影像、聲音或多媒體文件。在 JavaScript 中 Blob 類型的對象表示不可變的類似文件對象的原始數據。
對 Blob 感興趣的小伙伴,可以閱讀 “你不知道的 Blob” 這篇文章。
2.6 發送二進制數據
在以上示例中,我們在頁面上創建了兩個 textarea,分別用于存放 待發送的數據 和 服務器返回的數據。當用戶輸入完待發送的文本之后,點擊 發送 按鈕時,我們會先獲取輸入的文本并把文本包裝成 Blob 對象然后發送到服務端,而服務端成功接收到消息之后,會把收到的消息原封不動地回傳到客戶端。
當瀏覽器接收到新消息后,如果是文本數據,會自動將其轉換成 DOMString 對象,如果是二進制數據或 Blob 對象,會直接將其轉交給應用,由應用自身來根據返回的數據類型進行相應的處理。
數據發送代碼
// const socket = new WebSocket("ws://echo.websocket.org");
// const sendMsgContainer = document.querySelector("#sendMessage");
function send() {
const message = sendMsgContainer.value;
if (socket.readyState !== WebSocket.OPEN) {
console.log("連接未建立,還不能發送消息");
return;
}
const blob = new Blob([message], { type: "text/plain" });
if (message) socket.send(blob);
console.log(`未發送至服務器的字節數:${socket.bufferedAmount}`);
}
當然客戶端接收到服務端返回的消息之后,會判斷返回的數據類型,如果是 Blob 類型的話,會調用 Blob 對象的 text() 方法,獲取 Blob 對象中保存的 UTF-8 格式的內容,然后把對應的文本內容保存到 接收的數據 對應的 textarea 文本框中。
數據接收代碼
// const socket = new WebSocket("ws://echo.websocket.org");
// const receivedMsgContainer = document.querySelector("#receivedMessage");
socket.addEventListener("message", async function (event) {
console.log("Message from server ", event.data);
const receivedData = event.data;
if (receivedData instanceof Blob) {
receivedMsgContainer.value = await receivedData.text();
} else {
receivedMsgContainer.value = receivedData;
}
});
同樣,我們使用 Chrome 瀏覽器的開發者工具來看一下相應的過程:
通過上圖我們可以很明顯地看到,當使用發送 Blob 對象時,Data 欄位的信息顯示的是 Binary Message,而對于發送普通文本來說,Data 欄位的信息是直接顯示發送的文本消息。
以上示例對應的完整代碼如下所示:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WebSocket 發送二進制數據示例</title>
<style>
.block {
flex: 1;
}
</style>
</head>
<body>
<h3>阿寶哥:WebSocket 發送二進制數據示例</h3>
<div style="display: flex;">
<div class="block">
<p>待發送的數據:<button onclick="send()">發送</button></p>
<textarea id="sendMessage" rows="5" cols="15"></textarea>
</div>
<div class="block">
<p>接收的數據:</p>
<textarea id="receivedMessage" rows="5" cols="15"></textarea>
</div>
</div>
<script>
const sendMsgContainer = document.querySelector("#sendMessage");
const receivedMsgContainer = document.querySelector("#receivedMessage");
const socket = new WebSocket("ws://echo.websocket.org");
// 監聽連接成功事件
socket.addEventListener("open", function (event) {
console.log("連接成功,可以開始通訊");
});
// 監聽消息
socket.addEventListener("message", async function (event) {
console.log("Message from server ", event.data);
const receivedData = event.data;
if (receivedData instanceof Blob) {
receivedMsgContainer.value = await receivedData.text();
} else {
receivedMsgContainer.value = receivedData;
}
});
function send() {
const message = sendMsgContainer.value;
if (socket.readyState !== WebSocket.OPEN) {
console.log("連接未建立,還不能發送消息");
return;
}
const blob = new Blob([message], { type: "text/plain" });
if (message) socket.send(blob);
console.log(`未發送至服務器的字節數:${socket.bufferedAmount}`);
}
</script>
</body>
</html>
可能有一些小伙伴了解完 WebSocket API 之后,覺得還不夠過癮。下面阿寶哥將帶大家來實現一個支持發送普通文本的 WebSocket 服務器。
三、手寫 WebSocket 服務器
在介紹如何手寫 WebSocket 服務器前,我們需要了解一下 WebSocket 連接的生命周期。
從上圖可知,在使用 WebSocket 實現全雙工通信之前,客戶端與服務器之間需要先進行握手(Handshake),在完成握手之后才能開始進行數據的雙向通信。
握手是在通信電路創建之后,信息傳輸開始之前。握手用于達成參數,如信息傳輸率,字母表,奇偶校驗,中斷過程,和其他協議特性。 握手有助于不同結構的系統或設備在通信信道中連接,而不需要人為設置參數。
既然握手是 WebSocket 連接生命周期的第一個環節,接下來我們就先來分析 WebSocket 的握手協議。
3.1 握手協議
WebSocket 協議屬于應用層協議,它依賴于傳輸層的 TCP 協議。WebSocket 通過 HTTP/1.1 協議的 101 狀態碼進行握手。為了創建 WebSocket 連接,需要通過瀏覽器發出請求,之后服務器進行回應,這個過程通常稱為 “握手”(Handshaking)。
利用 HTTP 完成握手有幾個好處。首先,讓 WebSocket 與現有 HTTP 基礎設施兼容:使得 WebSocket 服務器可以運行在 80 和 443 端口上,這通常是對客戶端唯一開放的端口。其次,讓我們可以重用并擴展 HTTP 的 Upgrade 流,為其添加自定義的 WebSocket 首部,以完成協商。
下面我們以前面已經演示過的發送普通文本的例子為例,來具體分析一下握手過程。
3.1.1 客戶端請求
GET ws://echo.websocket.org/ HTTP/1.1
Host: echo.websocket.org
Origin: file://
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: Zx8rNEkBE4xnwifpuh8DHQ==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
備注:已忽略部分 HTTP 請求頭
字段說明
Connection 必須設置 Upgrade,表示客戶端希望連接升級。
Upgrade 字段必須設置 websocket,表示希望升級到 WebSocket 協議。
Sec-WebSocket-Version 表示支持的 WebSocket 版本。RFC6455 要求使用的版本是 13,之前草案的版本均應當棄用。
Sec-WebSocket-Key 是隨機的字符串,服務器端會用這些數據來構造出一個 SHA-1 的信息摘要。把 “Sec-WebSocket-Key” 加上一個特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后計算 SHA-1 摘要,之后進行 Base64 編碼,將結果做為 “Sec-WebSocket-Accept” 頭的值,返回給客戶端。如此操作,可以盡量避免普通 HTTP 請求被誤認為 WebSocket 協議。
Sec-WebSocket-Extensions 用于協商本次連接要使用的 WebSocket 擴展:客戶端發送支持的擴展,服務器通過返回相同的首部確認自己支持一個或多個擴展。
Origin 字段是可選的,通常用來表示在瀏覽器中發起此 WebSocket 連接所在的頁面,類似于 Referer。但是,與 Referer 不同的是,Origin 只包含了協議和主機名稱。
3.1.2 服務端響應
HTTP/1.1 101 Web Socket Protocol Handshake ①
Connection: Upgrade ②
Upgrade: websocket ③
Sec-WebSocket-Accept: 52Rg3vW4JQ1yWpkvFlsTsiezlqw= ④
備注:已忽略部分 HTTP 響應頭
① 101 響應碼確認升級到 WebSocket 協議。
② 設置 Connection 頭的值為 "Upgrade" 來指示這是一個升級請求。HTTP 協議提供了一種特殊的機制,這一機制允許將一個已建立的連接升級成新的、不相容的協議。
③ Upgrade 頭指定一項或多項協議名,按優先級排序,以逗號分隔。這里表示升級為 WebSocket 協議。
④ 簽名的鍵值驗證協議支持。
介紹完 WebSocket 的握手協議,接下來阿寶哥將使用 Node.js 來開發我們的 WebSocket 服務器。
3.2 實現握手功能
要開發一個 WebSocket 服務器,首先我們需要先實現握手功能,這里阿寶哥使用 Node.js 內置的 http 模塊來創建一個 HTTP 服務器,具體代碼如下所示:
const http = require("http");
const port = 8888;
const { generateAcceptValue } = require("./util");
const server = http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
res.end("大家好,我是阿寶哥。感謝你閱讀“你不知道的WebSocket”");
});
server.on("upgrade", function (req, socket) {
if (req.headers["upgrade"] !== "websocket") {
socket.end("HTTP/1.1 400 Bad Request");
return;
}
// 讀取客戶端提供的Sec-WebSocket-Key
const secWsKey = req.headers["sec-websocket-key"];
// 使用SHA-1算法生成Sec-WebSocket-Accept
const hash = generateAcceptValue(secWsKey);
// 設置HTTP響應頭
const responseHeaders = [
"HTTP/1.1 101 Web Socket Protocol Handshake",
"Upgrade: WebSocket",
"Connection: Upgrade",
`Sec-WebSocket-Accept: ${hash}`,
];
// 返回握手請求的響應信息
socket.write(responseHeaders.join("\r\n") + "\r\n\r\n");
});
server.listen(port, () =>
console.log(`Server running at http://localhost:${port}`)
);
在以上代碼中,我們首先引入了 http 模塊,然后通過調用該模塊的 createServer() 方法創建一個 HTTP 服務器,接著我們監聽 upgrade 事件,每次服務器響應升級請求時就會觸發該事件。由于我們的服務器只支持升級到 WebSocket 協議,所以如果客戶端請求升級的協議非 WebSocket 協議,我們將會返回 “400 Bad Request”。
當服務器接收到升級為 WebSocket 的握手請求時,會先從請求頭中獲取 “Sec-WebSocket-Key” 的值,然后把該值加上一個特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后計算 SHA-1 摘要,之后進行 Base64 編碼,將結果做為 “Sec-WebSocket-Accept” 頭的值,返回給客戶端。
上述的過程看起來好像有點繁瑣,其實利用 Node.js 內置的 crypto 模塊,幾行代碼就可以搞定了:
// util.js
const crypto = require("crypto");
const MAGIC_KEY = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
function generateAcceptValue(secWsKey) {
return crypto
.createHash("sha1")
.update(secWsKey + MAGIC_KEY, "utf8")
.digest("base64");
}
開發完握手功能之后,我們可以使用前面的示例來測試一下該功能。待服務器啟動之后,我們只要對 “發送普通文本” 示例,做簡單地調整,即把先前的 URL 地址替換成 ws://localhost:8888,就可以進行功能驗證。
感興趣的小伙們可以試試看,以下是阿寶哥本地運行后的結果:
從上圖可知,我們實現的握手功能已經可以正常工作了。那么握手有沒有可能失敗呢?答案是肯定的。比如網絡問題、服務器異?;?Sec-WebSocket-Accept 的值不正確。
下面阿寶哥修改一下 “Sec-WebSocket-Accept” 生成規則,比如修改 MAGIC_KEY 的值,然后重新驗證一下握手功能。此時,瀏覽器的控制臺會輸出以下異常信息:
WebSocket connection to 'ws://localhost:8888/' failed: Error during WebSocket handshake: Incorrect 'Sec-WebSocket-Accept' header value
如果你的 WebSocket 服務器要支持子協議的話,你可以參考以下代碼進行子協議的處理,阿寶哥就不繼續展開介紹了。
// 從請求頭中讀取子協議
const protocol = req.headers["sec-websocket-protocol"];
// 如果包含子協議,則解析子協議
const protocols = !protocol ? [] : protocol.split(",").map((s) => s.trim());
// 簡單起見,我們僅判斷是否含有JSON子協議
if (protocols.includes("json")) {
responseHeaders.push(`Sec-WebSocket-Protocol: json`);
}
好的,WebSocket 握手協議相關的內容基本已經介紹完了。下一步我們來介紹開發消息通信功能需要了解的一些基礎知識。
3.3 消息通信基礎
在 WebSocket 協議中,數據是通過一系列數據幀來進行傳輸的。為了避免由于網絡中介(例如一些攔截代理)或者一些安全問題,客戶端必須在它發送到服務器的所有幀中添加掩碼。服務端收到沒有添加掩碼的數據幀以后,必須立即關閉連接。
3.3.1 數據幀格式
要實現消息通信,我們就必須了解 WebSocket 數據幀的格式:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
可能有一些小伙伴看到上面的內容之后,就開始有點 “懵逼” 了。下面我們來結合實際的數據幀來進一步分析一下:
在上圖中,阿寶哥簡單分析了 “發送普通文本” 示例對應的數據幀格式。這里我們來進一步介紹一下 Payload length,因為在后面開發數據解析功能的時候,需要用到該知識點。
Payload length 表示以字節為單位的 “有效負載數據” 長度。它有以下幾種情形:
如果值為 0-125,那么就表示負載數據的長度。
如果是 126,那么接下來的 2 個字節解釋為 16 位的無符號整形作為負載數據的長度。
如果是 127,那么接下來的 8 個字節解釋為一個 64 位的無符號整形(最高位的 bit 必須為 0)作為負載數據的長度。
多字節長度量以網絡字節順序表示,有效負載長度是指 “擴展數據” + “應用數據” 的長度?!皵U展數據” 的長度可能為 0,那么有效負載長度就是 “應用數據” 的長度。
另外,除非協商過擴展,否則 “擴展數據” 長度為 0 字節。在握手協議中,任何擴展都必須指定 “擴展數據” 的長度,這個長度如何進行計算,以及這個擴展如何使用。如果存在擴展,那么這個 “擴展數據” 包含在總的有效負載長度中。
3.3.2 掩碼算法
掩碼字段是一個由客戶端隨機選擇的 32 位的值。掩碼值必須是不可被預測的。因此,掩碼必須來自強大的熵源(entropy),并且給定的掩碼不能讓服務器或者代理能夠很容易的預測到后續幀。掩碼的不可預測性對于預防惡意應用的作者在網上暴露相關的字節數據至關重要。
掩碼不影響數據荷載的長度,對數據進行掩碼操作和對數據進行反掩碼操作所涉及的步驟是相同的。掩碼、反掩碼操作都采用如下算法:
j = i MOD 4
transformed-octet-i = original-octet-i XOR masking-key-octet-j
original-octet-i:為原始數據的第 i 字節。
transformed-octet-i:為轉換后的數據的第 i 字節。
masking-key-octet-j:為 mask key 第 j 字節。
為了讓小伙伴們能夠更好的理解上面掩碼的計算過程,我們來對示例中 “我是阿寶哥” 數據進行掩碼操作。這里 “我是阿寶哥” 對應的 UTF-8 編碼如下所示:
E6 88 91 E6 98 AF E9 98 BF E5 AE 9D E5 93 A5
而對應的 Masking-Key 為 0x08f6efb1,根據上面的算法,我們可以這樣進行掩碼運算:
let uint8 = new Uint8Array([0xE6, 0x88, 0x91, 0xE6, 0x98, 0xAF, 0xE9, 0x98,
0xBF, 0xE5, 0xAE, 0x9D, 0xE5, 0x93, 0xA5]);
let maskingKey = new Uint8Array([0x08, 0xf6, 0xef, 0xb1]);
let maskedUint8 = new Uint8Array(uint8.length);
for (let i = 0, j = 0; i < uint8.length; i++, j = i % 4) {
maskedUint8[i] = uint8[i] ^ maskingKey[j];
}
console.log(Array.from(maskedUint8).map(num=>Number(num).toString(16)).join(' '));
以上代碼成功運行后,控制臺會輸出以下結果:
ee 7e 7e 57 90 59 6 29 b7 13 41 2c ed 65 4a
上述結果與 WireShark 中的 Masked payload 對應的值是一致的,具體如下圖所示:
在 WebSocket 協議中,數據掩碼的作用是增強協議的安全性。但數據掩碼并不是為了保護數據本身,因為算法本身是公開的,運算也不復雜。那么為什么還要引入數據掩碼呢?引入數據掩碼是為了防止早期版本的協議中存在的代理緩存污染攻擊等問題。
了解完 WebSocket 掩碼算法和數據掩碼的作用之后,我們再來介紹一下數據分片的概念。
3.3.3 數據分片
WebSocket 的每條消息可能被切分成多個數據幀。當 WebSocket 的接收方收到一個數據幀時,會根據 FIN 的值來判斷,是否已經收到消息的最后一個數據幀。
利用 FIN 和 Opcode,我們就可以跨幀發送消息。操作碼告訴了幀應該做什么。如果是 0x1,有效載荷就是文本。如果是 0x2,有效載荷就是二進制數據。但是,如果是 0x0,則該幀是一個延續幀。這意味著服務器應該將幀的有效負載連接到從該客戶機接收到的最后一個幀。
為了讓大家能夠更好地理解上述的內容,我們來看一個來自 MDN 上的示例:
Client: FIN=1, opcode=0x1, msg="hello"
Server: (process complete message immediately) Hi.
Client: FIN=0, opcode=0x1, msg="and a"
Server: (listening, new message containing text started)
Client: FIN=0, opcode=0x0, msg="happy new"
Server: (listening, payload concatenated to previous message)
Client: FIN=1, opcode=0x0, msg="year!"
Server: (process complete message) Happy new year to you too!
在以上示例中,客戶端向服務器發送了兩條消息。第一個消息在單個幀中發送,而第二個消息跨三個幀發送。
其中第一個消息是一個完整的消息(FIN=1 且 opcode != 0x0),因此服務器可以根據需要進行處理或響應。而第二個消息是文本消息(opcode=0x1)且 FIN=0,表示消息還沒發送完成,還有后續的數據幀。該消息的所有剩余部分都用延續幀(opcode=0x0)發送,消息的最終幀用 FIN=1 標記。
好的,簡單介紹了數據分片的相關內容。接下來,我們來開始實現消息通信功能。
3.4 實現消息通信功能
阿寶哥把實現消息通信功能,分解為消息解析與消息響應兩個子功能,下面我們分別來介紹如何實現這兩個子功能。
3.4.1 消息解析
利用消息通信基礎環節中介紹的相關知識,阿寶哥實現了一個 parseMessage 函數,用來解析客戶端傳過來的 WebSocket 數據幀。出于簡單考慮,這里只處理文本幀,具體代碼如下所示:
function parseMessage(buffer) {
// 第一個字節,包含了FIN位,opcode, 掩碼位
const firstByte = buffer.readUInt8(0);
// [FIN, RSV, RSV, RSV, OPCODE, OPCODE, OPCODE, OPCODE];
// 右移7位取首位,1位,表示是否是最后一幀數據
const isFinalFrame = Boolean((firstByte >>> 7) & 0x01);
console.log("isFIN: ", isFinalFrame);
// 取出操作碼,低四位
/**
* %x0:表示一個延續幀。當 Opcode 為 0 時,表示本次數據傳輸采用了數據分片,當前收到的數據幀為其中一個數據分片;
* %x1:表示這是一個文本幀(text frame);
* %x2:表示這是一個二進制幀(binary frame);
* %x3-7:保留的操作代碼,用于后續定義的非控制幀;
* %x8:表示連接斷開;
* %x9:表示這是一個心跳請求(ping);
* %xA:表示這是一個心跳響應(pong);
* %xB-F:保留的操作代碼,用于后續定義的控制幀。
*/
const opcode = firstByte & 0x0f;
if (opcode === 0x08) {
// 連接關閉
return;
}
if (opcode === 0x02) {
// 二進制幀
return;
}
if (opcode === 0x01) {
// 目前只處理文本幀
let offset = 1;
const secondByte = buffer.readUInt8(offset);
// MASK: 1位,表示是否使用了掩碼,在發送給服務端的數據幀里必須使用掩碼,而服務端返回時不需要掩碼
const useMask = Boolean((secondByte >>> 7) & 0x01);
console.log("use MASK: ", useMask);
const payloadLen = secondByte & 0x7f; // 低7位表示載荷字節長度
offset += 1;
// 四個字節的掩碼
let MASK = [];
// 如果這個值在0-125之間,則后面的4個字節(32位)就應該被直接識別成掩碼;
if (payloadLen <= 0x7d) {
// 載荷長度小于125
MASK = buffer.slice(offset, 4 + offset);
offset += 4;
console.log("payload length: ", payloadLen);
} else if (payloadLen === 0x7e) {
// 如果這個值是126,則后面兩個字節(16位)內容應該,被識別成一個16位的二進制數表示數據內容大小;
console.log("payload length: ", buffer.readInt16BE(offset));
// 長度是126, 則后面兩個字節作為payload length,32位的掩碼
MASK = buffer.slice(offset + 2, offset + 2 + 4);
offset += 6;
} else {
// 如果這個值是127,則后面的8個字節(64位)內容應該被識別成一個64位的二進制數表示數據內容大小
MASK = buffer.slice(offset + 8, offset + 8 + 4);
offset += 12;
}
// 開始讀取后面的payload,與掩碼計算,得到原來的字節內容
const newBuffer = [];
const dataBuffer = buffer.slice(offset);
for (let i = 0, j = 0; i < dataBuffer.length; i++, j = i % 4) {
const nextBuf = dataBuffer[i];
newBuffer.push(nextBuf ^ MASK[j]);
}
return Buffer.from(newBuffer).toString();
}
return "";
}
創建完 parseMessage 函數,我們來更新一下之前創建的 WebSocket 服務器:
server.on("upgrade", function (req, socket) {
socket.on("data", (buffer) => {
const message = parseMessage(buffer);
if (message) {
console.log("Message from client:" + message);
} else if (message === null) {
console.log("WebSocket connection closed by the client.");
}
});
if (req.headers["upgrade"] !== "websocket") {
socket.end("HTTP/1.1 400 Bad Request");
return;
}
// 省略已有代碼
});
更新完成之后,我們重新啟動服務器,然后繼續使用 “發送普通文本” 的示例來測試消息解析功能。以下發送 “我是阿寶哥” 文本消息后,WebSocket 服務器輸出的信息。
Server running at http://localhost:8888
isFIN: true
use MASK: true
payload length: 15
Message from client:我是阿寶哥
通過觀察以上的輸出信息,我們的 WebSocket 服務器已經可以成功解析客戶端發送包含普通文本的數據幀,下一步我們來實現消息響應的功能。
3.4.2 消息響應
要把數據返回給客戶端,我們的 WebSocket 服務器也得按照 WebSocket 數據幀的格式來封裝數據。與前面介紹的 parseMessage 函數一樣,阿寶哥也封裝了一個 constructReply 函數用來封裝返回的數據,該函數的具體代碼如下:
function constructReply(data) {
const json = JSON.stringify(data);
const jsonByteLength = Buffer.byteLength(json);
// 目前只支持小于65535字節的負載
const lengthByteCount = jsonByteLength < 126 ? 0 : 2;
const payloadLength = lengthByteCount === 0 ? jsonByteLength : 126;
const buffer = Buffer.alloc(2 + lengthByteCount + jsonByteLength);
// 設置數據幀首字節,設置opcode為1,表示文本幀
buffer.writeUInt8(0b10000001, 0);
buffer.writeUInt8(payloadLength, 1);
// 如果payloadLength為126,則后面兩個字節(16位)內容應該,被識別成一個16位的二進制數表示數據內容大小
let payloadOffset = 2;
if (lengthByteCount > 0) {
buffer.writeUInt16BE(jsonByteLength, 2);
payloadOffset += lengthByteCount;
}
// 把JSON數據寫入到Buffer緩沖區中
buffer.write(json, payloadOffset);
return buffer;
}
創建完 constructReply 函數,我們再來更新一下之前創建的 WebSocket 服務器:
server.on("upgrade", function (req, socket) {
socket.on("data", (buffer) => {
const message = parseMessage(buffer);
if (message) {
console.log("Message from client:" + message);
// 新增以下
使你的代碼可讀和可維護的快速提示。
有多少次,你打開一個舊的項目,發現混亂的代碼,當你添加一些新的東西時,很容易崩潰?我們都有過這樣的經歷。
為了減少難以讀懂的javascript的數量,我提供了以下示例。這些都是我過去所犯過的錯誤。
對具有多個返回值的函數使用數組解構
假設我們有一個返回多個值的函數。一種可能的實現是使用數組解構,如下所示:
const func = () => {
const a = 1;
const b = 2;
const c = 3;
const d = 4;
return [a,b,c,d];
}
const [a,b,c,d] = func();
console.log(a,b,c,d); // 1,2,3,4
盡管上面的方法很好用,但確實引入了一些復雜性。
當我們調用函數并將值分配給 a,b,c,d 時,我們需要注意返回數據的順序。這里的一個小錯誤可能會成為調試的噩夢。
此外,無法確切指定我們要從函數中獲取哪些值,如果我們只需要 c 和 d 怎么辦?
相反,我們可以使用對象解構。
const func = () => {
const a = 1;
const b = 2;
const c = 3;
const d = 4;
return {a,b,c,d};
}
const {c,d} = func();
現在,我們可以輕松地從函數中選擇所需的數據,這也為我們的代碼提供了未來的保障,允許我們在不破壞東西的情況下增加額外的返回變量。
不對函數參數使用對象分解
假設我們有一個函數,該函數將一個對象作為參數并對該對象的屬性執行一些操作。一種幼稚的方法可能看起來像這樣:
// 不推薦
function getDaysRemaining(subscription) {
const startDate = subscription.startDate;
const endDate = subscription.endDate;
return endDate - startDate;
}
上面的方法按預期工作,但是,我們創建了兩個不必要的臨時引用 startDate 和 endDate。
一種更好的實現是對 subscription 對象使用對象解構來在一行中獲取 startDate 和 endDate。
// 推薦
function getDaysRemaining(subscription) {
const { startDate, endDate } = subscription;
return startDate - endDate;
}
我們可以更進一步,直接對參數執行對象析構。
// 更好
function getDaysRemaining({ startDate, endDate }) {
return startDate - endDate;
}
更優雅,不是嗎?
在不使用擴展運算符的情況下復制數組
使用 for循環遍歷數組并將其元素復制到新數組是冗長且相當丑陋的。
可以以簡潔明了的方式使用擴展運算符來達到相同的效果。
const stuff = [1,2,3];
// 不推薦
const stuffCopyBad = []
for(let i = 0; i < stuff.length; i++){
stuffCopyBad[i] = stuff[i];
}
// 推薦
const stuffCopyGood = [...stuff];
使用var
使用 const 保證不能重新分配變量。這樣可以減少我們代碼中的錯誤,并使其更易于理解。
// 不推薦
var x = "badX";
var y = "baxY";
// 推薦
const x = "goodX";
const y = "goodX";
果你確實需要重新分配變量,請始終選擇 let 而不是 var。
這是因為 let 是塊作用域的,而 var 是函數作用域的。
塊作用域告訴我們,只能在定義它的代碼塊內部訪問變量,嘗試訪問塊外部的變量會給我們提供ReferenceError。
for(let i = 0; i < 10; i++){
//something
}
print(i) // ReferenceError: i is not defined
函數作用域告訴我們,只能在定義其的函數內部訪問變量。
for(var i = 0; i < 10; i++){
//something
}
console.log(i) // 10
let 和 const 都是塊范圍的。
不使用模板字面值
手動將字符串連接在一起相當麻煩,而且輸入時可能會造成混淆。這是一個例子:
// 不推薦
function printStartAndEndDate({ startDate, endDate }) {
console.log('StartDate:' + startDate + ',EndDate:' + endDate)
}
模板文字為我們提供了一種可讀且簡潔的語法,該語法支持字符串插值。
// 推薦
function printStartAndEndDate({ startDate, endDate }) {
console.log(`StartDate: ${startDate}, EndDate: ${endDate}`)
}
模板文字也提供了嵌入新行的簡便方法,你所需要做的就是照常按鍵盤上的Enter鍵。
// 兩行打印
function printStartAndEndDate({ startDate, endDate }) {
console.log(`StartDate: ${startDate}
EndDate: ${endDate}`)
}
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務
沒有 switch 就沒有復雜的代碼塊
switch很方便:給定一個表達式,我們可以檢查它是否與一堆case子句中的其他表達式匹配。 考慮以下示例:
const name = "Juliana";
switch (name) {
case "Juliana":
console.log("She's Juliana");
break;
case "Tom":
console.log("She's not Juliana");
break;
}
當 name 為“Juliana”時,我們將打印一條消息,并立即中斷退出該塊。 在switch函數內部時,直接在 case 塊使用 return,就可以省略break。
當沒有匹配項時,可以使用 default 選項:
const name = "Kris";
switch (name) {
case "Juliana":
console.log("She's Juliana");
break;
case "Tom":
console.log("She's not Juliana");
break;
default:
console.log("Sorry, no match");
}
switch在 Redux reducers 中也大量使用(盡管Redux Toolkit簡化了樣板),以避免產生大量的if。 考慮以下示例:
const LOGIN_SUCCESS = "LOGIN_SUCCESS";
const LOGIN_FAILED = "LOGIN_FAILED";
const authState = {
token: "",
error: "",
};
function authReducer(state = authState, action) {
switch (action.type) {
case LOGIN_SUCCESS:
return { ...state, token: action.payload };
case LOGIN_FAILED:
return { ...state, error: action.payload };
default:
return state;
}
}
這有什么問題嗎?幾乎沒有。但是有沒有更好的選擇呢?
從 Python 獲得的啟示
來自 Telmo 的這條 Tweet引起了我的注意。 他展示了兩種“switch”風格,其中一種非常接近Python中的模式。
Python 沒有開關,它給我們一個更好的替代方法。 首先讓我們將代碼從 JavaScript 移植到Python:
LOGIN_SUCCESS = "LOGIN_SUCCESS"
LOGIN_FAILED = "LOGIN_FAILED"
auth_state = {"token": "", "error": ""}
def auth_reducer(state=auth_state, action={}):
mapping = {
LOGIN_SUCCESS: {**state, "token": action["payload"]},
LOGIN_FAILED: {**state, "error": action["payload"]},
}
return mapping.get(action["type"], state)
在 Python 中,我們可以使用字典來模擬switch 。 dict.get() 可以用來表示 switch 的 default 語句。
當訪問不存在的key時,Python 會觸發一個 KeyError 錯誤:
>>> my_dict = {
"name": "John",
"city": "Rome",
"age": 44
}
>>> my_dict["not_here"]
# Output: KeyError: 'not_here'
.get()方法是一種更安全方法,因為它不會引發錯誤,并且可以為不存在的key指定默認值:
>>> my_dict = {
"name": "John",
"city": "Rome",
"age": 44
}
>>> my_dict.get("not_here", "not found")
# Output: 'not found'
因此,Pytho n中的這一行:
return mapping.get(action["type"], state)
等價于 JavaScript中的:
function authReducer(state = authState, action) {
...
default:
return state;
...
}
使用字典的方式替換 switch
再次思考前面的示例:
const LOGIN_SUCCESS = "LOGIN_SUCCESS";
const LOGIN_FAILED = "LOGIN_FAILED";
const authState = {
token: "",
error: "",
};
function authReducer(state = authState, action) {
switch (action.type) {
case LOGIN_SUCCESS:
return { ...state, token: action.payload };
case LOGIN_FAILED:
return { ...state, error: action.payload };
default:
return state;
}
}
如果不使用 switch 我們可以這樣做:
function authReducer(state = authState, action) {
const mapping = {
[LOGIN_SUCCESS]: { ...state, token: action.payload },
[LOGIN_FAILED]: { ...state, error: action.payload }
};
return mapping[action.type] || state;
}
這里我們使用 ES6 中的計算屬性,此處,mapping的屬性是根據兩個常量即時計算的:LOGIN_SUCCESS 和 LOGIN_FAILED。
屬性對應的值,我們這里使用的是對象解構,這里 ES9((ECMAScript 2018)) 出來的。
const mapping = {
[LOGIN_SUCCESS]: { ...state, token: action.payload },
[LOGIN_FAILED]: { ...state, error: action.payload }
}
你如何看待這種方法?它對 switch 來說可能還能一些限制,但對于 reducer 來說可能是一種更好的方案。
但是,此代碼的性能如何?
性能怎么樣?
switch 的性能優于字典的寫法。我們可以使用下面的事例測試一下:
console.time("sample");
for (let i = 0; i < 2000000; i++) {
const nextState = authReducer(authState, {
type: LOGIN_SUCCESS,
payload: "some_token"
});
}
console.timeEnd("sample");
測量它們十次左右,
for t in {1..10}; do node switch.js >> switch.txt;done
for t in {1..10}; do node map.js >> map.txt;done
clipboard.png
人才們的 【三連】 就是小智不斷分享的最大動力,如果本篇博客有任何錯誤和建議,歡迎人才們留言,最后,謝謝大家的觀看。
原文:https://codeburst.io/alternat...
代碼部署后可能存在的BUG沒法實時知道,事后為了解決這些BUG,花了大量的時間進行log 調試,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務
Vuex 是一個專為 Vue.js 應用程序開發的狀態管理模式。它采用集中式存儲管理應用的所有組件的狀態,并以相應的規則保證狀態以一種可預測的方式發生變化。Vuex 也集成到 Vue 的官方調試工具 devtools extension,提供了諸如零配置的 time-travel 調試、狀態快照導入導出等高級調試功能。采用了全局單例模式,將組件的共享狀態抽離出來管理,使得組件樹中每一個位置都可以獲取共享的狀態或者觸發行為。
那么什么是狀態呢?我把狀態理解為在沒有使用vuex時,在當前組件中data內需要共用的數據為狀態。
vuex使得狀態或行為成為了共享的狀態,所共享的狀態或行為可以在各個組件中都可以訪問到,省去了子父或子子之間傳遞變量,提高了開發效率。
當我們不使用vuex時,對于組件之間傳遞信息會較為麻煩。
App.vue文件中:
<template>
<div id="app">
<Fruits :fruitList="fruitList"/>
</div>
</template>
<script> import Goods from './components/Goods'; export default { name: 'App',
components:{
Fruits,
Goods
}, data(){
return{ goodList:[
{
name:'doll',
price:12 },
{ name:'glass',
price:10 }
],
}
}
}
</script>
<style>
</style>
Good.vue文件中:
<template>
<div class="hello">
<ul>
<li v-for="(good,index) in goodList" :key="index"> name:{{good.name}} number: {{good.number}} {{index}}
</li>
</ul>
</div>
</template>
<script> export default { props:['goodList'],
}
</script>
<style>
</style>
首先先創建一個js文件作為兩兄弟之間傳輸的紐扣,這里起名為msg.js
//創建并暴露vue import Vue from 'vue';
export default new Vue
兄弟組件Goods:
<template>
<div>
<button @click="deliver">點擊</button>
</div>
</template>
<script> import MSG from '../msg';
export default {
data(){ return{
msg:'hahah' }
},
methods:{
deliver() {
MSG.$emit('showMsg',this.msg)
}
}
}
</script>
<style>
</style>
兄弟組件Fruits:
<template>
<div>
<div class="fruit">
{{text}}
</div>
</div>
</template>
<script> import MSG from '../msg';
export default {
data(){ return{
text:'' }
},
created(){ this.getMsg()
},
methods:{
getMsg(){
MSG.$on('showMsg',(message)=>{ this.text = message
})
}
}
}
</script>
<style>
</style>
在App組件中的代碼:
點擊按鈕:
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務
藍藍設計的小編 http://www.syprn.cn