首頁

JavaScript中的for循環(huán)

seo達(dá)人

JavaScript 語言中的 for 循環(huán)用于多次執(zhí)行代碼塊,它是 JavaScript 中最常用的一個循環(huán)工具,還可用于數(shù)組的遍歷循環(huán)等。


我們?yōu)槭裁匆褂?for 循環(huán)呢?打個比方,例如我們想要控制臺輸出1到1000之間的所有數(shù)字,如果單寫輸出語句,要寫1000句代碼,但是如果使用 for 循環(huán),幾句代碼就能實現(xiàn)。總之,使用 for 循環(huán)能夠讓我們寫代碼更方便快捷(當(dāng)然啦,否則要它干嘛)。


for 循環(huán)語法

語法如下所示:


for(變量初始化; 條件表達(dá)式; 變量更新) {

   // 條件表達(dá)式為true時執(zhí)行的語句塊

}

變量初始化,表示代碼塊開始前執(zhí)行。

條件表達(dá)式,定義運行循環(huán)代碼塊的條件。

變量更新,在循環(huán)代碼塊每次被執(zhí)行之后再執(zhí)行。

示例:

例如我們在一個HTML文件中,編寫如下代碼,實現(xiàn)計算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 循環(huán)中三個語句:


變量初始化 i = 1,表示從 1 開始計算。

條件表達(dá)式 i <= 100,表示只要 i 小于等于 100 循環(huán)就會一直執(zhí)行,當(dāng) i 大于 100 循環(huán)會停止。

變量更新 i++,之前我們學(xué)運算符的時候?qū)W過,這是遞增運算符 ++,表示為其操作數(shù)增加 1。

此時我們可以一點點來看這個 for 循環(huán):


第一次循環(huán): result = 0 + 1   // 此時result值為0,  i的值為1

第二次循環(huán): result = 1 + 2   // 此時result值為0+1,i的值為2

第三次循環(huán): result = 3 + 3   // 此時result值為1+2,i的值為3

第四次循環(huán): result = 6 + 4   // 此時result值為3+3,i的值為4

第五次循環(huán): result = 10 + 5  // 此時result值為6+4,i的值為5

...

我們只需要搞清楚 for 循環(huán)中的執(zhí)行原理,不需要手動來計算求和,只要寫好代碼,執(zhí)行代碼后計算機(jī)會很快會告訴我們1到 100 的總和。


再補(bǔ)充一下,上述代碼中result = result + i,我們也可以寫成 result += i,這是我們之前學(xué)過的加賦值運算符,還記得嗎?


示例:

再來看一個例子,例如我們可以使用 for 循環(huán)來實現(xiàn)數(shù)組遍歷,首先定義一個數(shù)組 lst:


var lst = ["a", "b", "c", "d", "e"];

在寫 for 循環(huán)時,首先就是要搞清楚小括號里面的三個語句,因為我們可以通過數(shù)組中元素的下標(biāo)索引來獲取元素的值,而數(shù)組的索引又是從 0 開始,所以變量初始化可以設(shè)置為i = 0。第二個條件表達(dá)式,因為數(shù)組中最后一個索引為 lst.length - 1,所以只要小于等于 lst.length - 1,循環(huán)就會一直執(zhí)行。而i <= lst.length - 1 就相當(dāng)于 i<lst.length。第三個變量更新,當(dāng)循環(huán)每循環(huán)一次,索引值就加一,所以為 i++。


所以循環(huán)可以像下面這樣寫:


for(i = 0; i<lst.length; i++){

   console.log(lst[i]);  // 輸出數(shù)組中的元素值,從索引為0的值開始輸出,每次加1,一直到lst.length-1

}

輸出:


a

b

c

d

e

其實遍歷數(shù)組還有一種更好的方法,就是使用 for...in 循環(huán)語句來遍歷數(shù)組。


for...in 循環(huán)

for...in 循環(huán)主要用于遍歷數(shù)組或?qū)ο髮傩?,對?shù)組或?qū)ο蟮膶傩赃M(jìn)行循環(huán)操作。for...in 循環(huán)中的代碼每執(zhí)行一次,就會對數(shù)組的元素或者對象的屬性進(jìn)行一次操作。


語法如下:


for (變量 in 對象) {

   // 代碼塊

}

for 循環(huán)括號內(nèi)的變量是用來指定變量,指定的可以是數(shù)組對象或者是對象屬性。


示例:

使用 for...in 循環(huán)遍歷我們定義好的 lst 數(shù)組:


var lst = ["a", "b", "c", "d", "e"];

for(var l in lst){

   console.log(lst[l]);

}

輸出:


a

b

c

d

e

除了數(shù)組,for...in 循環(huán)還可以遍歷對象,例如我們遍歷 俠俠 的個人基本信息:


var object = {

   姓名:'俠俠',

   年齡:'22',

   性別:'男',

   出生日期:'1997-08-05',

   職業(yè):'程序員',

   特長:'跳舞'

}


for(var i in object) {

   console.log(i + ":" + object[i]);

}

輸出:


姓名: 俠俠

年齡: 22

性別: 男

出生日期: 1997-08-05

職業(yè):程序員

特長:跳舞

動手小練習(xí)

請自定義一個長度為7的數(shù)組,然后通過 for 循環(huán)將數(shù)組中的元素遍歷出來。

求和:1~100的奇數(shù)和。

求和:1~100的偶數(shù)和。

使用對象定義一個人的個人信息(包括姓名、性別、年齡、出生日期、興趣愛好、職業(yè)、特長等),然后使用 for...in 循環(huán)將這些信息遍歷輸出。

藍(lán)藍(lán)設(shè)計www.bouu.cn )是一家專注而深入的界面設(shè)計公司,為期望卓越的國內(nèi)外企業(yè)提供卓越的UI界面設(shè)計、BS界面設(shè)計 、 cs界面設(shè)計 、 ipad界面設(shè)計 、 包裝設(shè)計 、 圖標(biāo)定制 、 用戶體驗 、交互設(shè)計、 網(wǎng)站建設(shè) 、平面設(shè)計服務(wù)

封裝element-ui表格

seo達(dá)人

表格需求

一般管理系統(tǒng)對表格會有以下需求


可以分頁(需要有分頁條)

可以多選(表格帶復(fù)選框)

頂部需要加一些操作按鈕(新增,刪除等等)

表格每行行尾有操作按鈕

表格行可以編輯

如下圖為一個示例表格




如果我們直接使用element-ui提供的組件的話,那么開發(fā)一個這樣的表格就需要使用到以下內(nèi)容


需要使用表格的插槽功能,開發(fā)每一行的按鈕

需要通過樣式調(diào)整頂部按鈕,表格,分頁條的布局樣式

需要監(jiān)聽分頁的事件然后去刷新表格數(shù)據(jù)

頂部按鈕或操作按鈕如果需要獲取表格數(shù)據(jù),需要調(diào)用表格提供的api

對于有行編輯的需求,還需要通過插槽去渲染行編輯的內(nèi)容,同時要控制行編輯的開關(guān)

不僅僅開發(fā)表格比較麻煩,而且還要考慮團(tuán)隊協(xié)作,如果每個人實現(xiàn)表格的方式存在差別,那么可能后期的維護(hù)成本也會變得很高。那怎么辦呢?


項目安裝

安裝插件

在使用element-ui的項目中,可以通過以下命令進(jìn)行安裝


npm install vue-elementui-table -S

在項目中使用

在main.js中添加以下代碼


import ZjTable from 'vue-element-table'


Vue.use(ZjTable)

然后即可像下文中的使用方式進(jìn)行使用


表格配置

為了滿足團(tuán)隊快速開發(fā)的需要,小編對上面提出來的需求進(jìn)行了封裝,然后使用的時候,開發(fā)人員只需要配置一些JSON便可以完成以上功能的開發(fā)。


基礎(chǔ)配置

一個基礎(chǔ)的表格包含了數(shù)據(jù)和列信息,那么如何用封裝的表格去配置呢?


<template>

 <zj-table

   :columns="columns"

   :data="data"

   :pagination="false"

 />

</template>

<script>

export default {

 data() {

   return {

     // 表格的列信息, 數(shù)組每一項代表一個字段,可以使用element 列屬性的所有屬性,以下僅為示例

     columns: Object.freeze([

       {

         // 表頭顯示的文字

         label: '姓名',

         // 對應(yīng)數(shù)據(jù)里面的字段

         prop: 'name'

       },

       {

         label: '性別',

         prop: 'sex',

         // 格式化表格,與element-ui 的表格屬性相同

         formatter(row, column, cellValue) {

           return cellValue === 1 ? '男' : '女'

         }

       },

       {

         label: '年齡',

         prop: 'age'

       }

     ]),

     data: [

       {

         name: '子君',

         sex: 1,

         age: 18

       }

     ]

   }

 }

}

</script>

通過上面的配置,就可以完成一個基礎(chǔ)表格的開發(fā),完整代碼見 https://github.com/snowzijun/vue-element-table/blob/master/example/views/demo/base.vue,效果如下圖所示




表格默認(rèn)會顯示復(fù)選框,也可以通過配置selectable屬性來關(guān)閉掉


添加分頁

簡單的表格用封裝之后的或未封裝的開發(fā)工作量區(qū)別并不大,我們繼續(xù)為表格添加上分頁


<template>

   <!--

   current-page.sync 表示頁碼, 添加上 .sync 在頁碼發(fā)生變化時自動同步頁碼

   page-size.sync 每頁條數(shù)

   total  總條數(shù)

   height="auto" 配置height:auto, 表格高度會根據(jù)內(nèi)容自動調(diào)整,如果不指定,表格將保持充滿父容器,同時表頭會固定,不跟隨滾動條滾動

   @page-change 無論pageSize currentPage 哪一個變化,都會觸發(fā)這個事件

 -->

 <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: [],

     // 當(dāng)前頁碼

     currentPage: 1,

     // 每頁條數(shù)

     pageSize: 10,

     // 總條數(shù)

     total: 0,

     // 是否顯示loading

     loading: false

   }

 },

 created() {

   this.loadData()

 },

 methods: {

   // 加載表格數(shù)據(jù)

   loadData() {

     this.loading = true

     setTimeout(() => {

       // 假設(shè)總條數(shù)是40條

       this.total = 40

       const { currentPage, pageSize } = this

       // 模擬數(shù)據(jù)請求獲取數(shù)據(jù)

       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() {

     // 因為上面設(shè)置屬性指定了.sync,所以這兩個屬性會自動變化

     console.log(this.pageSize, this.currentPage)

     // 分頁發(fā)生變化,重新請求數(shù)據(jù)

     this.loadData()

   }

 }

}

</script>

完整代碼請參考 https://github.com/snowzijun/vue-element-table/blob/master/example/views/demo/pagination.vue


通過封裝,表格將自帶分頁功能,通過上面代碼,實現(xiàn)效果如下所示,是不是變得簡單了一些。接下來我們繼續(xù)給表格添加按鈕




添加頂部按鈕

表格上面可能會有新增,刪除等等按鈕,怎么辦呢,接下來我們繼續(xù)通過配置去添加按鈕


<template>

 <zj-table

   :buttons="buttons"

 />

</template>

<script>

export default {

 data() {

   return {

     buttons: Object.freeze([

       {

         // id 必須有而且是在當(dāng)前按鈕數(shù)組里面是唯一的

         id: 'add',

         text: '新增',

         type: 'primary',

         icon: 'el-icon-circle-plus',

         click: this.$_handleAdd

       },

       {

         id: 'delete',

         text: '刪除',

         // rows 是表格選中的行,如果沒有選中行,則禁用刪除按鈕, disabled可以是一個boolean值或者函數(shù)

         disabled: rows => !rows.length,

         click: this.$_handleRemove

       },

       {

         id: 'auth',

         text: '這個按鈕根據(jù)權(quán)限顯示',

         // 可以通過返回 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(',')}`)

   },

   // 關(guān)注作者公眾號

   $_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'

       },

       // 行編輯按鈕,在表格末尾出現(xiàn),自動鎖定右側(cè)

       {

         width: 180,

         label: '操作',

         // 通過 actions 指定行尾按鈕

         actions: [

           {

             id: 'follow',

             text: '關(guān)注作者',

             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,這里需要用箭頭函數(shù)

             click: () => {

               this.$alert('女生被禁止刪除了')

             }

           }

         ]

       }

     ])

   }

 },

 methods: {

   // 關(guān)注作者公眾號

   $_handleFollowAuthor() {

           console.log('微信搜索【前端有的玩】,這是對小編最大的支持')

   },

   /**

    * row 這一行的數(shù)據(jù)

    */

   $_handleEdit(row, column) {

     this.$alert(`點擊了姓名為【${row.name}】的行上的按鈕`)

   }

 }

}

</script>

行操作按鈕會被凍結(jié)到表格最右側(cè),不會跟隨滾動條滾動而滾動,上面完整代碼見, 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: '編輯',

             // 如果當(dāng)前行啟用了編輯,則不顯示編輯按鈕

             before: row => {

               return !this.editIds.includes(row.id)

             },

             click: this.$_handleEdit

           },

           {

             id: 'save',

             text: '保存',

             // 如果當(dāng)前行啟用了編輯,則顯示保存按鈕

             before: row => {

               return this.editIds.includes(row.id)

             },

             click: this.$_handleSave

           }

         ]

       }

     ]),

     data: [

       {

         // 行編輯必須指定rowKey字段,默認(rèn)是id,如果修改為其他字段,需要給表格指定row-key="字段名"

         id: '0',

         name: '子君',

         sex: '1',

         age: 18

       },

       {

         // 行編輯必須指定rowKey字段,默認(rèn)是id,如果修改為其他字段,需要給表格指定row-key="字段名"

         id: '1',

         name: '子君1',

         sex: '0',

         age: 18

       }

     ],

     editIds: []

   }

 },

 methods: {

   $_handleEdit(row) {

     // 通過調(diào)用 startEditRow 可以開啟行編輯

     this.$refs.table.startEditRow(row.id)

     // 記錄開啟了行編輯的id

     this.editIds.push(row.id)

   },

   $_handleSave(row) {

     // 點擊保存的時候,通過endEditRow 結(jié)束行編輯

     this.$refs.table.endEditRow(row.id, (valid, result, oldRow) => {

       // 如果有表單驗證,則valid會返回是否驗證成功

       if (valid) {

         console.log('修改之后的數(shù)據(jù)', result)

         console.log('原始數(shù)據(jù)', 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屬性

可以通過插槽自定義頂部按鈕,行操作按鈕,行字段等

可以在按鈕區(qū)域右側(cè)通過插槽配置其他內(nèi)容

其他等等

表格開發(fā)說明

通過上面的代碼示例,我們已經(jīng)知道了封裝之后的表格可以完成哪些事情,接下來一起來看看表格是如何實現(xiàn)的。完整代碼見 https://github.com/snowzijun/vue-element-table/tree/master/src/components/zj-table


表格布局

整個表格是通過JSX來封裝的,因為JSX使用起來更加靈活。對于我們封裝的表格,我們從豎向可以分為三部分,分別是頂部按鈕區(qū),中間表格區(qū),底部分頁區(qū),如何去實現(xiàn)三個區(qū)域的布局呢,核心代碼如下


render(h) {

   // 按鈕區(qū)域

   const toolbar = this.$_renderToolbar(h)

   // 表格區(qū)域

   const table = this.$_renderTable(h)

   // 分頁區(qū)域

   const page = this.$_renderPage(h)


   return (

     <div class="zj-table" style={{ height: this.tableContainerHeight }}>

       {toolbar}

       {table}

       {page}

     </div>

   )

 }

通過三個render函數(shù)分別渲染對應(yīng)區(qū)域,然后將三個區(qū)域組合在一起。


渲染表格列

通過前文的講解,我們可以將表格的列分為以下幾種


常規(guī)列

行編輯列

操作按鈕列

插槽列

鏈接列(文檔后續(xù)完善)

嵌套列(文檔后續(xù)完善)

   $_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為非空數(shù)組

           actions = [],

           // 是否可編輯列, 對于可編輯列需要動態(tài)啟用編輯

           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 {

           // 使用默認(rèn)列

           return this.$_renderDefaultColumn(h, column)

         }

       })

   },

行編輯列

當(dāng)前表格行編輯支持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

         }}

       />

     )

   }

藍(lán)藍(lán)設(shè)計www.bouu.cn )是一家專注而深入的界面設(shè)計公司,為期望卓越的國內(nèi)外企業(yè)提供卓越的UI界面設(shè)計、BS界面設(shè)計 、 cs界面設(shè)計 、 ipad界面設(shè)計 、 包裝設(shè)計 、 圖標(biāo)定制 、 用戶體驗 、交互設(shè)計、 網(wǎng)站建設(shè) 平面設(shè)計服務(wù)


UI 配色方法

資深UI設(shè)計者

配色,設(shè)計師的世紀(jì)難題。從平面到屏幕,CMYK 到 RGB,墨點到像素,色彩越來越豐富,形式越來越復(fù)雜。UI 的發(fā)展從擬物的繁瑣細(xì)節(jié)中掙脫出來,卻在色彩的展現(xiàn)中放飛了自我。

零售業(yè)有個有趣的研究成果 —— 「七秒鐘定律」:人們在挑選商品和服務(wù)時 ,只需要 7 秒鐘就可以確定是否感興趣,而在這短暫的 7 秒鐘內(nèi),色彩的作用占到了 67%。

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

要在小小的手機(jī)屏幕中加入這么多顏色,并保持其中的聯(lián)系和邏輯,著實不易。如果你還對配色一無所知,完全不知道配色應(yīng)該怎么入手,那么你需要了解一下,我?guī)啄杲?jīng)驗總結(jié)的配色思路。

拾色器中的黃金三分法

無論我們用 PS、AI,還是 Sketch、XD、Figma,和色彩打交道最多的地方就是拾色器窗口,我們來看看這些案例:

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

雖然是不同的應(yīng)用,但是這個拾色器的用法大同小異,但是,很多新人并沒有搞懂拾色器的正確應(yīng)用邏輯。

很多人知道,UI 的色彩使用 RGB 模式,但是拾色器主要的選色原理遵循的是 HSB 模式的邏輯,也就是色相(H)、飽和度(S)、明度(B)。

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

HSB 是色彩科學(xué)中對所有顏色屬性的理論劃分,是種概念上的定義,可以用來解釋任何色彩,也就是說可以和 RGB 和 CMYK 相互轉(zhuǎn)化,且 HSB 的選色邏輯更清晰、簡潔、干練。

因為一個正確的選色過程,是先確定出色相,然后再在這個色相維度下選出明度和飽和度,所以我們首先要關(guān)注色相選擇條。

細(xì)心的同學(xué)應(yīng)該已經(jīng)發(fā)現(xiàn)了,它們的首尾都是紅色,那是因為色相的序列是一個首尾相接的環(huán)形模式,所以它實際上就是色環(huán)的柱狀展示圖,應(yīng)用起來和色環(huán)沒有實際區(qū)別。

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

接下來就要說到重點,飽和度和明度選擇區(qū),我自己使用的習(xí)慣,是將這個選擇區(qū)通過黃金三分法的方式切割成等比的 9 個區(qū)域,然后明確它們在 UI 中的對應(yīng)情緒和應(yīng)用場景。

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

在過去的大量分析中,互聯(lián)網(wǎng)產(chǎn)品的主色和重要輔助色都會往右上角聚集,一些次要、不可點的色彩聚集在中上方,而文字背景色則聚集在左側(cè),無人區(qū)則是我們重點避開的對象。

下面我們分析幾個案例,看看它們在這個選擇區(qū)中的色彩分布情況:

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

大家也可以自己拿一些主流的應(yīng)用做截圖,然后把它們的 UI 元素色彩排列到拾色面板中,就會得到基本一致的結(jié)果。牢記這9個區(qū)域的場景劃分,可以幫助我們非常、安全地完成 UI 配色。

UI配色中的色彩選擇

在眾多的 UI 設(shè)計規(guī)范中,色彩部分的介紹,都必然包含三種類型,分別是:

  • 主色:應(yīng)用的核心色彩,品牌色
  • 輔色:豐富頁面視覺和傳達(dá)效果的次要顏色
  • 中性:沒有色相的文字、背景用色

1. 主色的選擇

主色是一個應(yīng)用的最核心的色彩,品牌的象征色,比如想到餓了么的藍(lán)色、微信的綠色、京東的紅色、淘寶的橙色。

確定主色,并沒有大家想象的那么復(fù)雜,它的要點在于——你想讓用戶感受到哪種情緒,然后通過情緒關(guān)聯(lián)一個大致的色彩范圍,再進(jìn)行微調(diào)。

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

在今天的互聯(lián)網(wǎng)產(chǎn)品中,主色的應(yīng)用選擇范圍在拾色器區(qū)域的右上角,前面已經(jīng)有解釋了。這和平面設(shè)計中的用色有非常大的不同。

移動端產(chǎn)品要在一個方寸大的空間中爭奪用戶的注意力,引起用戶的興趣,那么低飽和清淡的色調(diào)是無法實現(xiàn)這個目標(biāo)的,所以今天主色飽和度越來越,比如我們之前整理的一篇總結(jié):

為什么支付寶要換 Logo 顏色?分析下目前 Logo 的主色趨勢

再加上屏幕的 RGB 顯示特性,高對比度,高動態(tài)范圍的特質(zhì)能給用戶提供更好的觀感。所以選擇主色最安全的做法,就是在確定色相類型后,在右上方區(qū)域選出合適的色值。

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

2. 輔助色的選擇

輔助色是豐富應(yīng)用中的次要色彩,它會包含一到若干個和主色不同的色彩,除了品牌傳達(dá)外,具有更強(qiáng)的實用性。

前面我們提到過色環(huán),這里就要派上用場了。我們知道色環(huán)是個色彩序列首尾相連的環(huán)形模型,它蘊(yùn)含一個最樸素的原則,即兩個顏色在這個環(huán)形中角度越大,那么視覺差異性越大,對比越強(qiáng),比如下圖的展示:

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

這些配色的模式是不能閉著眼隨便挑的,它們僅僅作為一個色彩對比度的判斷標(biāo)準(zhǔn)。而真正輔助色的選擇,是根據(jù)實際場景的功能決定的。

比如通知、提醒、取消用大紅色,確認(rèn)、同意用綠色或者藍(lán)色,收藏、打分、評價用橙黃色。都是已經(jīng)在用戶心智中建立了標(biāo)準(zhǔn)的用色類型,跟著常規(guī)方法來做,是沒有其它思路的情況下最簡單、最安全的輔助色選擇方式。

沒有標(biāo)準(zhǔn)元素用色的情況下,再考慮應(yīng)用色環(huán)的 「角度原則」,越需要被突出的顏色,可以在色環(huán)中離主色越遠(yuǎn),越不需要被突出的則越近。

比如下方攜程的案例,主色在藍(lán)色的情況下,支付、保險金標(biāo)簽這些需要被重點突出的色彩,使用了主色的互補(bǔ)色, 讓我們一眼就能看見并產(chǎn)生強(qiáng)烈的操作欲望。

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

3. 中性色的選擇

中性色,是頁面中文字、背景用到的顏色,它們承擔(dān)起最基本的層次表現(xiàn)、便于閱讀的重任。多數(shù)新手覺得中性色無關(guān)緊要,實際情況恰恰相反。

主色輔助色決定了界面視覺是否出彩,而中性色的應(yīng)用直接決定了頁面能不能正常使用。如果看過比較多的原型案例,就應(yīng)該明白,即使只有黑白灰的狀態(tài)下,我們理解這些頁面和進(jìn)行使用也不會有絲毫的障礙。

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

中性色的配置,就是制定一個由深到淺的灰度階梯,應(yīng)用在對應(yīng)權(quán)重的元素上,色彩輕重的主要判斷依據(jù)是 HSB 中的 B(明度) 值。

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

中性色雖然指的是無個性,但并不是只能用純灰色,常見的一種做法,就是為中性色添加適量的藍(lán)色飽和度,提升觀看體驗(滿足RGB的某種特性)。

這種做法,顏色越淺的時候飽和度應(yīng)用色值就越低,將這個規(guī)律在拾色器中進(jìn)行表現(xiàn),那么我們就可以得到一個 L 型曲線,我稱它為 「中性曲線」。

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

掌握對于主色、輔助色、中性色的選擇方法,那么對于UI配色的奧義來說,你已經(jīng)掌握了一半,接下來就要了解更具體的實踐思路了。

配色方式的四象限

配色并不是只有色彩的色值本身,大家一直在研究各種色彩心理學(xué)和理論,卻很少關(guān)心它們?nèi)绾螒?yīng)用,如何配置。

所以,我根據(jù)主色和輔助色應(yīng)用總結(jié)了一個配色的四象限表格,再分別看看它們對應(yīng)的案例:

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

1. 主色占比大,色彩豐富度高

主色會作為頂部標(biāo)題欄或其它重要模塊中的背景色,進(jìn)行大面積應(yīng)用,加深用戶對品牌的認(rèn)知和辨識度。而產(chǎn)品中又包含了大量功能和服務(wù),需要用豐富的色彩來進(jìn)行暗示,吸引用戶關(guān)注。

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

2. 主色占比大,色彩豐富度低

這類配色中,主色的應(yīng)用占比也大,出現(xiàn)頻率高,鮮有其它顏色出現(xiàn)。比較適用于圖片內(nèi)容豐富的題材中,或者是相對正式、品牌感強(qiáng)的應(yīng)用。

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

3. 主色占比小,色彩豐富度高

這是多數(shù)主流應(yīng)用的趨勢,降低主色占比,留出更多的空間給內(nèi)容模塊的展示上,突出自身帶有的服務(wù)和功能。

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

4. 主色占比小,色彩豐富度低

通常,會應(yīng)用這種方式是因為產(chǎn)品的服務(wù)是相對單一,但也需要用戶投入注意力的應(yīng)用,設(shè)計師就會盡力避免給予用戶過多的干擾。

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

每次在進(jìn)行配色的時候,我們都需要對自己要應(yīng)用哪種配色應(yīng)用方式做出規(guī)劃,然后再動手執(zhí)行。有了這個目標(biāo),后面在整個項目的設(shè)計中的配色步驟就是水到渠成的事情了。

配色流程演示

在實踐前,我們要簡單講講一個應(yīng)用或者界面的標(biāo)準(zhǔn)配色的流程了,流程順序如下:

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

具體應(yīng)該怎么使用這套流程,我們用一個圖蟲APP改版的案例來做演示,首先從四象限中的第一個主色占比高、色彩豐富度高的類型做起。

1. 配色流程演示

原型是 UI 設(shè)計的基本藝能了,在開始具體設(shè)計、配色前,搭建頁面的框架原型是一個必備的條件,下面,是我們已經(jīng)準(zhǔn)備好的原型案例。

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

然后,我們確定以橙色作為應(yīng)用主色,并在拾色器中進(jìn)行確認(rèn)。

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

有了主色,就可以對頁面進(jìn)行色彩填充和圖片填充了。既然我們主色是占比大的,那么首先可以用到的就是頂部標(biāo)題欄的背景色了,以及底部 Tabbar 中的選中色,大按鈕色等。

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

接著,我們開始整理中性色的使用,選擇新的顏色來填充文字和背景,清晰的表現(xiàn)模塊層級,文字信息的權(quán)重。

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

最后,就是添加輔助色和其它色彩的元素了,這個步驟建議都是放在最后一步操作。因為色彩越豐富,越難控制,容易讓整個畫面顯得雜亂無序,所以先完成基礎(chǔ)搭建,可以更好的幫助我們判斷彩色的使用是否合理。

下面,我們使用彩色的金剛區(qū)圖標(biāo),然后將用戶關(guān)注、認(rèn)證用戶、標(biāo)簽等元素使用其它色彩,來豐富頁面的色彩內(nèi)容。

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

2. 其他配色類型應(yīng)用

根據(jù)第一個方案,我們可以再用這個原型來實現(xiàn)其余的三個方案的配色。

比如下面的主色占比大,但是色彩豐富度低的。因為已經(jīng)不太適用其它輔助色,所以主色填充上我們不再填充頂部導(dǎo)航欄的背景,而是將更多元素應(yīng)用主色,減少輔助色數(shù)量。

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

然后是主色占比小,色彩豐富度高的方案,進(jìn)一步降低主色應(yīng)用的比例,然后在金剛區(qū)、標(biāo)簽等處使用較為豐富的配色。

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

最后,就是主色占比小,色彩豐富性也低的方案,那么使用中性色的元素就開始增多,只保留最核心的一些元素使用主色,和極少的輔助色。

10年經(jīng)驗的資深設(shè)計師,總結(jié)了這份 UI 配色終極奧義

根據(jù)四種不同的配色方案,我們就可以得到四種不同的配色結(jié)果和風(fēng)格,在每次設(shè)計開始實施前,我們都可以根據(jù)這種做法來做嘗試,并選出自己滿意的方案。

要再次強(qiáng)調(diào),UI 配色是極其強(qiáng)調(diào)形式的應(yīng)用科學(xué),最后做的往往會和一開始想的效果有極大出入,所以需要我們有幾個備選方案,可以隨時進(jìn)行調(diào)整,并選出合理的那個。

總結(jié)

以上是我們關(guān)于配色有關(guān)知識點的分享,希望可以幫助大家提升對 UI 配色的認(rèn)識。

文章來源:優(yōu)設(shè)    作者:超人的電話亭

藍(lán)藍(lán)設(shè)計www.bouu.cn )是一家專注而深入的界面設(shè)計公司,為期望卓越的國內(nèi)外企業(yè)提供卓越的UI界面設(shè)計、BS界面設(shè)計 、 cs界面設(shè)計 、 ipad界面設(shè)計 、 包裝設(shè)計 、 圖標(biāo)定制 、 用戶體驗 、交互設(shè)計、 網(wǎng)站建設(shè) 、平面設(shè)計服務(wù)

讓你的 commit 更有價值

seo達(dá)人

提交規(guī)范

AngularJS 在開發(fā)者文檔中關(guān)于 git commit 的指導(dǎo)說明,提到嚴(yán)格的 git commit 格式規(guī)范可以在瀏覽項目歷史的過程中看到更易讀的信息,并且能用 git commit 的信息直接生成 AngularJS 的 change log 。


commit messages 格式規(guī)范

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 修復(fù)

docs: 僅修改文檔

style: 修改格式(空格,格式化,省略分號等),對代碼運行沒有影響

refactor: 重構(gòu)(既不是修 bug ,也不是加功能)

build: 構(gòu)建流程、外部依賴變更,比如升級 npm 包、修改 webpack 配置等

perf: 性能優(yōu)化

test: 測試相關(guān)

chore: 對構(gòu)建過程或輔助工具和庫(如文檔生成)的更改

ci: ci 相關(guān)的更改

除此之外,還有一個特殊的類型 revert ,如果當(dāng)前提交是為了撤銷之前的某次提交,應(yīng)該用 revert 開頭,后面加上被撤銷的提交的 header,在 body 中應(yīng)該注明: This reverts commit <hash>. ,hash 指的就是將要被撤銷的 commit SHA 。


// 例如


revert: feat(user): add user type


This reverts commit ca16a365467e17915f0273392f4a13331b17617d.

Scope

scope 可以指定提交更改的影響范圍,這個視項目而定,當(dāng)修改影響超過單個的 scope 時,可以指定為 * 。


Sbuject

subject 是指更改的簡潔描述,長度約定在 50 個字符以內(nèi),通常遵循以下幾個規(guī)范:


用動詞開頭,第一人稱現(xiàn)在時表述,例如:change 代替 changed 或 changes

第一個字母小寫

結(jié)尾不加句號(.)

Body

body 部分是對本地 commit 的詳細(xì)描述,可以分成多行。


跟 subject 類似,用動詞開頭,第一人稱現(xiàn)在時表述,例如:change 代替 changed 或 changes。


body 應(yīng)該說明修改的原因和更改前后的行為對比。


Footer

footer 基本用在這兩種情況:


不兼容的改動( Breaking Changes ),通常用 BREAKING CHANGE: 開頭,后面跟一個空格或兩個換行符。剩余的部分就是用來說明這個變動的信息和遷移方法等。

關(guān)閉 Issue, github 關(guān)閉 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

查看更多例子

規(guī)范 commit message 的好處

首行就是簡潔實用的關(guān)鍵信息,方便在 git history 中快速瀏覽

具有詳實的 body 和 footer ,可以清晰的看出某次提交的目的和影響

可以通過 type 過濾出想要查找的信息,也可以通過關(guān)鍵字快速查找相關(guān)提交

可以直接從 commit 生成 change log

// 列舉幾個常用的 log 參數(shù)


// 輸出 log 的首行

git log --pretty=oneline


// 只輸出首行的 commit 信息。不包含 hash 和 合并信息等

git log --pretty=format:%s


// 查找有關(guān)“更新菜單配置項”的提交

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'

用工具實現(xiàn)規(guī)范提交

上面介紹了規(guī)范提交的格式,如果讓各位同學(xué)在 git commit 的時候嚴(yán)格按照上面的規(guī)范來寫,首先心智是有負(fù)擔(dān)的,得記住不同的類型到底是用來定義什么的,subject 怎么寫,body 怎么寫,footer 要不要寫。其次,對人的規(guī)范大部分都是反人性的,所以很可能在過不了多久,就會有同學(xué)漸漸的不按照規(guī)范來寫??恳庵玖砜刂谱约簢?yán)格按照規(guī)范來寫是需要額外耗費一些精力的,把精力耗費在這種事情上面實在有些浪費。


用工具實現(xiàn)規(guī)范提交的方案,一種是在提交的時候就提示必填字段,另一種是在提交后校驗字段是否符合規(guī)范。這兩種在實際項目中都是很有必要的。


Commitizen

Zen-like commit messages for internet citizens. 嗯~~一種禪意

Commitizen 是一個幫助撰寫規(guī)范 commit message 的工具。他有一個命令行工具 cz-cli,接下來會把使用 Commitizen 分成幾個階段來介紹。


體驗 git cz

// 全局安裝 Commitizen

npm install -g commitizen

你的倉庫可能還不是對 Commitizen 友好的,此時運行 git cz 的效果跟 git commit 一樣,也就是沒有效果。 不過,可以執(zhí)行 npx git-cz 來體驗。


如果想直接運行 git cz 實現(xiàn)語義化的提交,可以根據(jù) 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 文件,內(nèi)容設(shè)置為


{

 "path": "cz-conventional-changelog"

}

現(xiàn)在運行 git cz 效果如下:




cz-customizable 自定義中文配置

通過上面的截圖可以看到,提交的配置選項都是英文的,如果想改成中文的,可以使用 cz-customizable 適配器。


運行下面的命令,注意之前已經(jīng)初始化過一次了,這次再初始化,需要加 --force 覆蓋


npm install cz-customizable --save-dev


commitizen init cz-customizable --save-dev --save-exact --force

現(xiàn)在 package.json 中 config.commitizen 字段為:


"config": {

   "commitizen": {

     "path": "./node_modules/cz-customizable"

   }

 }

cz-customizable 文檔中說明了查找配置文件的方式有三種,我們按照第一種,在項目根目錄創(chuàng)建一個 .cz-config.js 的文件。按照給出的示例 cz-config-EXAMPLE.js 編寫我們的 config。 commit-type 可以參考 conventional-commit-types 。


可以點擊查看我配置好的文件 qiqihaobenben/commitizen-git/.cz-config.js ,里面中詳細(xì)的注釋。


commitlint 校驗提交

Commitizen 文檔中開始就介紹到,Commitizen 可以在觸發(fā) git commit 鉤子之前就能給出提示,但是也明確表示提交時對 commit messages 的校驗也是很有用的。畢竟即使用了 Commitzen,也是能繞過去,所以提交最后的校驗很重要。


commitlint 可以檢查 commit messages 是否符合常規(guī)提交格式,需要一份校驗配置,推薦 @commitlint/config-conventional 。


npm i --save-dev @commitlint/config-conventional @commitlint/cli

在項目根目錄創(chuàng)建 commitlint.config.js 文件并設(shè)置校驗規(guī)則:


module.exports = {

 extends: ["@commitlint/config-conventional"],

 // rules 里面可以設(shè)置一些自定義的校驗規(guī)則

 rules: {},

};

在項目中安裝 husky ,并在項目根目錄新建 husky.config.js 文件,加入以下設(shè)置:


// 安裝 husky

npm install --save-dev husky



// husky.config.js 中加入以下代碼

module.exports = {

 "hooks": {

   "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"

 }

}

注意:因為 @commitlint/config-conventional 校驗規(guī)則遵循 Angular 的規(guī)范, 所以我們在用 cz-customizable 自定義中文配置時, 是按照給出的符合 Angular 規(guī)范的示例 cz-config-EXAMPLE.js 編寫.cz-config.js 的。但是如果你自定義的 Commitizen 配置不符合 Angular 規(guī)范,可以使用 commitlint-config-cz 設(shè)置校驗規(guī)則。(推薦還是按照 Angular 規(guī)范進(jìn)行 cz-customizable 自定義配置)

// 安裝 commitlint-config-cz

npm install commitlint-config-cz --save-dev



// commitlint.config.js 改為

module.exports = {

 extends: [

   'cz'

 ]

};

git commit 觸發(fā) git cz

在提交的時候,我們都習(xí)慣了 git commit ,雖然換成 git cz 不難,但是如果讓開發(fā)者在 git commit 時無感知的觸發(fā) git cz 肯定是更好的,

而且也能避免不熟悉項目的人直接 git commit 提交一些不符合規(guī)范的信息。


我們可以在 husky.config.js 中設(shè)置:


"hooks": {

 "prepare-commit-msg": "exec < /dev/tty && git cz --hook || true",

}

注意: 在 window 系統(tǒng),可能需要在 git base 中才能生效。

生成 CHANGELOG

standard-version

是一個使用 semver 和 conventional-commits 支持生成 CHANGELOG 進(jìn)行版本控制的實用程序。

standard-version 不只是能生成 CHANGELOG , 還能根據(jù) commit 的 type 來進(jìn)行版本控制。


// 安裝 standard-verison

npm i --save-dev standard-version


// 在 package.json 中的 scripts 加入 standard-version

{

 "scripts": {

   "release": "standard-version"

 }

}

示例項目

可以查看 commitizen-git ,里面歸納了快速配置 Commitizen 友好倉庫的步驟。

差不多三五分鐘就能搞定。


可以看一下配置完后,執(zhí)行 git commit 的效果。




擴(kuò)展

更復(fù)雜的自定義提示

cz-customizable 中自定義配置項通常情況是夠用的,

commitlint 中校驗的規(guī)則基本上也是夠用的,但是會有比較硬核的開發(fā)者會覺得還是不夠,還要更多。比如一些 prompt 更加自定義,

提交時詢問的 question 添加更多的邏輯,比如可以把一些重要的字段校驗提前到 Commitizen 中,或者添加更多自定義的校驗。


如果真想這么干,那就去 fork 一份 cz-conventional-changelog 或者 cz-customizable 來改,

或者直接自己寫一個 adapter。


Commitizen 友好徽章

如果把倉庫配置成了對 Commitizen 友好的話,可以在 README.md 中加上這個小徽章

藍(lán)藍(lán)設(shè)計www.bouu.cn )是一家專注而深入的界面設(shè)計公司,為期望卓越的國內(nèi)外企業(yè)提供卓越的UI界面設(shè)計、BS界面設(shè)計 、 cs界面設(shè)計 、 ipad界面設(shè)計 、 包裝設(shè)計 、 圖標(biāo)定制 、 用戶體驗 、交互設(shè)計、 網(wǎng)站建設(shè) 平面設(shè)計服務(wù)

一文讀懂 Web 安全

seo達(dá)人

Web 安全是互聯(lián)網(wǎng)中不可或缺的一個領(lǐng)域,這個領(lǐng)域中誕生了大量的黑帽子與白帽子,他們都是安全領(lǐng)域的王者,在平時里,他們利用各種巧妙的技術(shù)互相博弈,時不時就會掀起一場 Web 安全浪潮,真可謂神仙打架,各顯神通。


本文從一個吃瓜群眾的角度,聊一聊 Web 安全的一些有趣故事。


安全世界觀

安全攻防案例

總結(jié)與思考

安全世界觀

在互聯(lián)網(wǎng)發(fā)展之初,IE 瀏覽器壟斷的時期,大家上網(wǎng)的目的都很單純,主要通過瀏覽器分享信息,獲取新聞。但隨著互聯(lián)網(wǎng)的不斷發(fā)展發(fā)展,一個網(wǎng)頁能做的事情越來越多,除了看新聞,我們還可以看視頻、玩游戲、購物、聊天等,這些功能都大大豐富了我們的生活。


隨著網(wǎng)頁功能的逐漸增多,就開始出現(xiàn)了一些黑帽子,他們試圖通過一些技術(shù)手段來牟取利益。在我小的時候,印象最深的就是木馬病毒,它可以監(jiān)控你的鍵盤,將你在鍵盤上敲打的內(nèi)容發(fā)送到黑客的機(jī)器上,黑客通過分析這些內(nèi)容,很容易就能得到你的游戲賬號和密碼。


在這之后,就誕生出了一些殺毒軟件,致力于解決網(wǎng)絡(luò)上的各種病毒,隨著不斷地發(fā)展,殺毒軟件已經(jīng)成為一臺電腦必不可少的軟件。


為什么會出現(xiàn)這樣的安全問題?

安全歸根到底是信任的問題,如果所有人都按照正常的流程去上網(wǎng),不去謀取私利,也就沒有安全問題可談了。


安全的根本在于信任,但要讓所有人互相信任談何容易。在當(dāng)前階段,我們可以做到:持續(xù)做好安全防護(hù),讓漏洞越來越少,非法攻擊越來越困難,這樣就能逐漸減少黑帽子的數(shù)量,讓病毒制造者越來越少。


如何做好安全

要做好安全,首先得理解安全問題的屬性,前人通過無數(shù)實踐,最后將安全的屬性總結(jié)為安全三要素,分別為:機(jī)密性、完整性、可用性。


機(jī)密性


保護(hù)數(shù)據(jù)內(nèi)容不被泄露。

通常使用加密的方法。

完整性


保護(hù)數(shù)據(jù)內(nèi)容是完整的、沒有被篡改。

通常使用數(shù)字簽名的方法。

可用性


數(shù)據(jù)隨時都能夠使用。

通常是在防御 DOS。

有了安全 3 要素之后,我們就可以對安全問題進(jìn)行評估了。


資產(chǎn)等級劃分


找出最重要的數(shù)據(jù)。

找出最重要數(shù)據(jù)的宿主空間,如:在數(shù)據(jù)庫里,那么數(shù)據(jù)庫就得重點防御。

找出數(shù)據(jù)庫的宿主空間,如:在一臺服務(wù)器上,那么這臺服務(wù)器就得做次等防御。

找出服務(wù)器的宿主空間,如:在 OSI 網(wǎng)絡(luò)層級上,那么在網(wǎng)絡(luò)層面就得做一般防御。

威脅分析


找出威脅(可能造成危害的來源)。

找出風(fēng)險(可能出現(xiàn)的損失叫做風(fēng)險)。

風(fēng)險分析


采取多標(biāo)準(zhǔn)決策分析,即:風(fēng)險 = 威脅等級 * 威脅可行性。

計算所有的威脅,將最終的風(fēng)險進(jìn)行排序,優(yōu)先解決風(fēng)險大的問題。

確認(rèn)解決方案


找出不安全的實現(xiàn)方式,并確定解決方案。

解決方案不要改變商業(yè)需求的初衷。

解決方案需對用戶透明,不要改變用戶的習(xí)慣。

做好安全評估之后,我們就有了一份安全解決方案,后續(xù)的安全工作只需按照這個方案去做,就沒有任何問題。


安全的原則

有了安全解決方案之后,我們還可以制定一些安全原則,遵守原則做事,可以讓我們事半功倍。


黑名單、白名單原則


白名單方案指的是給安全的資源授權(quán)。

黑名單方案指的是禁用不安全的資源。

我們應(yīng)該優(yōu)先使用白名單方案,因為黑名單通常統(tǒng)計不完所有的不安全資源。

如:XSS 攻擊的方式非常多,可以通過 script、css、image 標(biāo)簽等,盡管你將這些標(biāo)簽都加入黑名單,也不能保證其他的標(biāo)簽都沒有 XSS 的攻擊隱患。

最小權(quán)限原則


只授予必要的權(quán)限,不要過度授權(quán),減少出錯機(jī)會。

如:普通權(quán)限的 Linux 用戶只能操作 ~ 文件夾下的目錄,如果有人想刪庫跑路,在執(zhí)行 rm -rf / 時,就會提示無權(quán)限。

縱深防御原則


這條原則類似 木桶理論,安全水平往往取決于最短的那塊板。

即:不要留下短板,黑帽子們往往可以利用短板為突破口,挖掘更大的漏洞。

數(shù)據(jù)與代碼分離原則


當(dāng)用戶數(shù)據(jù)被當(dāng)成代碼執(zhí)行時,混淆了數(shù)據(jù)和代碼的邊界,從而導(dǎo)致安全問題。

如:XSS 就是利用這一點去攻擊的。

不可預(yù)測性原則


這條原則是為了提高攻擊門檻,有效防止基于篡改、偽造的攻擊。

如:數(shù)據(jù)庫中使用 uuid 代替 number 型的自增主鍵,可以避免 id 被攻擊者猜到,從而進(jìn)行批量操作。

token 也是利用不可預(yù)測性,攻擊者無法構(gòu)造 token 也就無法進(jìn)行攻擊。

有了這些安全原則,我們就可以開干了,接下來介紹幾個常見的攻防案例。


安全攻防案例

安全攻防的案例非常多,這里主要介紹幾個出鏡率比較高的安全問題。


客戶端攻擊

XSS 攻擊

CSRF 攻擊

點擊劫持

XSS 攻擊

XSS 攻擊的本質(zhì)是將用戶數(shù)據(jù)當(dāng)成了 HTML 代碼一部分來執(zhí)行,從而混淆原本的語義,產(chǎn)生新的語義。




如圖所示,我們注冊了一個 <script>alert(document.cookie)</script> 的用戶名,所有能看到此用戶名字的頁面,都會彈出當(dāng)前瀏覽器的 Cookie,如果代碼的邏輯是將 Cookie 發(fā)送到攻擊者的網(wǎng)站,攻擊者就能冒充當(dāng)前用戶進(jìn)行登錄了。


XSS 攻擊方式有很多,所有和用戶交互的地方,都有可能存在 XSS 攻擊。


例如:


所有 input 框。

window.location。

window.name。

document.referrer。

document.cookie。

localstorage。

...

由于頁面中與用戶交互的地方非常多,肯定還有一些 XSS 的攻擊方式?jīng)]有被發(fā)現(xiàn),而一旦被黑帽子發(fā)現(xiàn),就可能造成嚴(yán)重的影響,所以我們務(wù)必引起重視。


XSS 攻擊影響

被 XSS 攻擊成功后,攻擊者就可以獲取大量的用戶信息,例如:


識別用戶 UA。

識別用戶瀏覽器擴(kuò)展。

識別用戶瀏覽過的網(wǎng)站。


通過 CSS 的 Visited 屬性。

獲取用戶真實的 IP。


通過 WebRTC 等。

盜取 Cookie


偽造用戶登錄,竊取用戶資料。

XSS 釣魚。


向頁面注入一個登錄彈窗,讓用戶認(rèn)為是網(wǎng)站內(nèi)的登錄彈窗(其實是釣魚網(wǎng)站的),一旦用戶登錄,賬號密碼就泄露給了釣魚網(wǎng)站。

XSS 攻擊防御

目前來說,XSS 已經(jīng)得到了互聯(lián)網(wǎng)行業(yè)的重視,許多開發(fā)框架都內(nèi)置了安全的 HTML 渲染方法。


我們也可以自定義進(jìn)行一些安全配置。


配置 HTTP 中的 http-only 頭,讓前端 JS 不能操作 Cookie。

輸入檢查,在用戶提交數(shù)據(jù)時,使用 XssFilter 過濾掉不安全的數(shù)據(jù)。

輸出檢查,在頁面渲染的時候,過濾掉危險的數(shù)據(jù)。

CSRF 攻擊

CSRF(Cross-site request forgery)跨站請求偽造,是一種利用用戶身份,執(zhí)行一些用戶非本意的操作。




如圖所示:


用戶先登錄了服務(wù)器 B,然后去訪問服務(wù)器 C。

服務(wù)器 C 通過惡意腳本,冒充 A 去調(diào)用服務(wù)器 B 上的某個功能,

對于服務(wù)器 B 來說,還以為這是 A 發(fā)起的請求,就當(dāng)作正常請求處理了。

試想一下,如果 C 冒充 A 進(jìn)行了一次轉(zhuǎn)賬,必定會造成大量的經(jīng)濟(jì)損失。


CSRF 防御方式

防御 CSRF 主要有以下幾種方式:


驗證碼


每一次請求都要求用戶驗證,以確保請求真實可靠。

即:利用惡意腳本不能識別復(fù)雜的驗證碼的特點,保證每次請求都是合法的。

Referer 檢查


檢查發(fā)起請求的服務(wù)器,是否為目標(biāo)服務(wù)器。

即:HTTP 請求中的 Referer 頭傳遞了當(dāng)前請求的域名,如果此域名是非法服務(wù)器的域名,則需要禁止訪問。

Token


利用不可預(yù)測性原則,每一請求必須帶上一段隨機(jī)碼,這段隨機(jī)碼由正常用戶保存,黑帽子不知道隨機(jī)碼,也就無法冒充用戶進(jìn)行請求了。

點擊劫持

點擊劫持是一種視覺欺騙的攻擊手段。攻擊者將需要攻擊的網(wǎng)站通過 iframe 嵌套的方式嵌入自己的網(wǎng)頁中,并將 iframe 設(shè)置為透明,在頁面中透出一個按鈕誘導(dǎo)用戶點擊。


就像一張圖片上面鋪了一層透明的紙一樣,你看到的是攻擊者的頁面,但是其實這個頁面只是在底部,而你真正點擊的是被攻擊者透明化的另一個網(wǎng)頁。




如果所示,當(dāng)你點擊了頁面上的按鈕之后,本以為會...... ,而真正執(zhí)行的操作是關(guān)注了某人的博客。


點擊劫持防御

由于點擊劫持主要通過 iframe,所以在防御時,主要基于 iframe 去做。


方案一:frame busting


正常網(wǎng)站使用 JS 腳本判斷是否被惡意網(wǎng)站嵌入,如:博客網(wǎng)站監(jiān)測到被一個 iframe 打開,自動跳轉(zhuǎn)到正常的頁面即可。

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" 則只能加載與主站同域的資源。

服務(wù)器端攻擊

服務(wù)器端的攻擊的方式也非常多,這里列舉幾個常見的。


SQL 注入攻擊

文件上傳漏洞

登錄認(rèn)證攻擊

應(yīng)用層拒絕服務(wù)攻擊

webServer 配置安全

SQL 注入攻擊

SQL 注入和 XSS 一樣,都是違背了數(shù)據(jù)和代碼分離原則導(dǎo)致的攻擊方式。


如圖所示,我們利用 SQL 注入,就能在不需要密碼的情況下,直接登錄管理員的賬號。




攻擊的前提是:后端只用了簡單的拼接 SQL 的方式去查詢數(shù)據(jù)。


# 拼接出來的 sql 如下:select * from user where username = 'admin' or 1=1 and password = 'xxx'# 無論密碼輸入什么,這條 sql 語句都能查詢到管理員的信息

除此之外,SQL 注入還有以下幾種方式:


使用 SQL 探測,猜數(shù)據(jù)庫表名,列名。


通過 MySQL 內(nèi)置的 benchmark 探測數(shù)據(jù)庫字段。

如:一段偽代碼 select database as current if current[0]==='a',benchmark(10000,'猜對了') 如果表明猜對了,就延遲 10 s 并返回成功。

使用存儲過程執(zhí)行系統(tǒng)命令


通過內(nèi)置的方法或存儲過程執(zhí)行 shell 腳本。

如:xp_cmdshell、sys_eval、sys_exec 等。

字符串截斷


如:MySQL 在處理超長的字符串時,會顯示警告,但會執(zhí)行成功。

注冊一個 admin + 50 個空格的用戶,會觸發(fā)截斷,最終新增一個 admin 用戶,這樣就能擁有管理員權(quán)限了。

SQL 注入防御

防止 SQL 注入的最好的辦法就是,不要手動拼接 SQL 語句。


最佳方案,使用預(yù)編譯語句綁定變量


通常是指框架提供的拼接 SQL 變量的方法。

這樣的語義不會發(fā)生改變,變量始終被當(dāng)成變量。

嚴(yán)格限制數(shù)據(jù)類型,如果注入了其他類型的數(shù)據(jù),直接報錯,不允許執(zhí)行。

使用安全的存儲過程和系統(tǒng)函數(shù)。

CRLF 注入

在注入攻擊中,換行符注入也是非常常見的一種攻擊方式。


如果在 HTTP 請求頭中注入 2 個換行符,會導(dǎo)致?lián)Q行符后面的所有內(nèi)容都被解析成請求實體部分。

攻擊者通常在 Set-Cookie 時,注入換行符,控制請求傳遞的內(nèi)容。

文件上傳漏洞

上傳文件是網(wǎng)頁開發(fā)中的一個常見功能,如果不加處理,很容易就會造成攻擊。




如圖所示,攻擊者上傳了一個木馬文件,并且通過返回的 URL 進(jìn)行訪問,就能控制服務(wù)器。


通常我們會控制上傳文件的后綴名,但也不能完全解決問題,攻擊者還可以通過以下方式進(jìn)行攻擊:


偽造正常文件


將木馬文件偽裝成正常的后綴名進(jìn)行上傳。

如果要避免這個問題,我們可以繼續(xù)判斷上傳文件的文件頭前 10 個字節(jié)。

Apache 解析方式是從后往前解析,直到找到一個認(rèn)識的后綴名為止


如:上傳一個 abc.php.rar.rar.rar 能繞過后綴名檢查,但在執(zhí)行時,被當(dāng)成一個 php 文件進(jìn)行執(zhí)行。

IIS 會截斷分號進(jìn)行解析


如:abc.asp;xx.png 能繞過后綴名檢查,但在執(zhí)行時,被當(dāng)成一個 asp 文件進(jìn)行執(zhí)行。

HTTP PUT 方法允許將文件上傳到指定位置


通過 HTTP MOVE 方法,還能修改上傳的文件名。

通過二者配合,就能先上傳一個正常的后綴名,然后改為一個惡意的后綴名。

PHP CGI 路徑問題


執(zhí)行 http://abc.com/test.png/xxx.php 時,會把 test.png 當(dāng)做 php 文件去解析。

如果用戶正好是把一段惡意的 php 腳本當(dāng)做一張圖片進(jìn)行上傳,就會觸發(fā)這個攻擊。

文件上傳漏洞防御

防御文件上傳漏洞,可以從以下幾點考慮:


將文件上傳的目錄設(shè)置為不可執(zhí)行。

判斷文件類型


檢查 MIME Type,配置白名單。

檢查后綴名,配置白名單。

使用隨機(jī)數(shù)改寫文件名和文件路徑


上傳文件后,隨機(jī)修改文件名,讓攻擊者無法執(zhí)行攻擊。

單獨設(shè)置文件服務(wù)器的域名


單獨做一個文件服務(wù)器,并使用單獨的域名,利用同源策略,規(guī)避客戶端攻擊。

通常做法是將靜態(tài)資源存放在 CDN 上。

登錄認(rèn)證攻擊

登錄認(rèn)證攻擊可以理解為一種破解登錄的方法。攻擊者通常采用以下幾種方式進(jìn)行破解:


彩虹表


攻擊者通過搜集大量明文和 MD5 的對應(yīng)關(guān)系,用于破解 MD5 密文找出原文。

對于彩虹表中的 MD5 密碼,我們可以加鹽,進(jìn)行二次加密,避免被破解。

Session Fixation 攻擊


利用應(yīng)用系統(tǒng)在服務(wù)器的 SessionID 固定不變機(jī)制,借助他人用相同的 SessionID 獲取認(rèn)證和授權(quán)。

攻擊者登錄失敗后,后端返回了 SessionID,攻擊者將 SessionID 交給正常用戶去登錄,登錄成功后,攻擊者就能使用這個 SessionID 冒充正常用戶登錄了。

如果瀏覽器每一次登錄都刷新 SessionID 可以避免這個問題。

Session 保持攻擊


有些時候,后端出于用戶體驗考慮,只要這個用戶還活著,就不會讓這個用戶的 Session 失效。

攻擊者可以通過不停發(fā)起請求,可以讓這個 Session 一直活下去。

登錄認(rèn)證防御方式

多因素認(rèn)證


密碼作為第一道防御,但在密碼驗證成功后,我們還可以繼續(xù)驗證:動態(tài)口令,數(shù)字證書,短信驗證碼等,以保證用戶安全。

由于短信和網(wǎng)頁完全是 2 套獨立的系統(tǒng),攻擊者很難獲取到短信驗證碼,也就無法進(jìn)行攻擊。

除此之外,前端登錄認(rèn)證還有多種方式,如果你對此感興趣,可以參考我之前寫的 前端登錄,這一篇就夠了。


應(yīng)用層拒絕服務(wù)攻擊

應(yīng)用層拒絕服務(wù)攻擊,又叫 DDOS 攻擊,它指的是利用大量的請求造成資源過載,導(dǎo)致服務(wù)器不可用。




通常有以下幾種 DDOS 攻擊方式:


SYN Flood 洪水攻擊


利用 HTTP 3 次握手機(jī)制,消耗服務(wù)器連接資源。

如:攻擊者發(fā)起大量的 HTTP 請求,但并不完成 3 次握手,而是只握手 2 次,這時服務(wù)器端會繼續(xù)等待直至超時。這時的服務(wù)器會一直忙于處理大量的垃圾請求,而無暇顧及正常請求。

Slowloris 攻擊


以非常低的速度發(fā)送 HTTP 請求頭,消耗服務(wù)器連接資源。

如:攻擊者發(fā)送大量 HTTP 請求,但每個請求頭都發(fā)的很慢,每隔 10s 發(fā)送一個字符,服務(wù)器為了等待數(shù)據(jù),不得始終保持連接,這樣一來,服務(wù)器連接數(shù)很快就被占光了。

HTTP POST DOS


發(fā)送 HTTP 時,指定一個非常大的 Content-Length 然后以很長的間隔發(fā)送,消耗服務(wù)器連接資源。

CC 攻擊


針對一些非常消耗資源的頁面,不斷發(fā)起請求。

如:頁面中的某些頁面,需要后端做大量的運算,或者需要做非常耗時的數(shù)據(jù)庫查詢。在大量的請求下,服務(wù)器的 CPU、內(nèi)存等資源可能就被占光了。

Server Limit DOS


通過 XSS 注入一段超長的 Cookie,導(dǎo)致超出 Web 服務(wù)器所能承受的 Request Header 長度,服務(wù)器端就會拒絕此服務(wù)。

ReDOS


針對一些缺陷的正則表達(dá)式,發(fā)起大量請求,耗光系統(tǒng)資源。

應(yīng)用層拒絕服務(wù)攻擊防御

對于應(yīng)用層拒絕服務(wù)攻擊,目前也沒有特別完美的解決方案,不過我們還是可以進(jìn)行一些優(yōu)化。


應(yīng)用代碼做好性能優(yōu)化


合理使用 Redis、Memcache 等緩存方案,減少 CPU 資源使用率。

網(wǎng)絡(luò)架構(gòu)上做好優(yōu)化


后端搭建負(fù)載均衡。

靜態(tài)資源使用 CDN 進(jìn)行管理。

限制請求頻率


服務(wù)器計算所有 IP 地址的請求頻率,篩選出異常的 IP 進(jìn)行禁用。

可以使用 LRU 算法,緩存前 1000 條請求的 IP,如果有 IP 請求頻率過高,就進(jìn)行禁用。

其實,處理 DDOS 核心思路就是禁用不可信任的用戶,確保資源都是被正常的用戶所使用。


WebServer 配置安全

我們在部署 web 應(yīng)用的時候,經(jīng)常會用到 Nginx、Apache、IIS、Tomcat、Jboss 等 Web 服務(wù)器,這些服務(wù)器本身也存在一些安全隱患,如果配置不當(dāng),很容易收到攻擊。


在配置 Web 服務(wù)器時,可以參考以下幾點:


以用戶權(quán)限運行 Web 服務(wù)器


遵守最小權(quán)限原則,以最小權(quán)限身份運行 Web 服務(wù)器,限制被入侵后的權(quán)限。

刪除可視化后臺


運行 Tomcat、Jboss 等 Web 服務(wù)器時,默認(rèn)會開啟一個可視化的運營后臺,運行在 8080 端口,并且第一次訪問是沒有認(rèn)證的。

攻擊者可以利用可視化后臺,遠(yuǎn)程加載一段 war 包或者上傳木馬文件,進(jìn)行控制。

及時更新版本


主流的 Web 服務(wù)器,每隔一段時間就會修復(fù)一些漏洞,所以記得及時更新版本。

總結(jié)與思考

本文介紹了 Web 安全的基本概念,以及大量的攻防技巧,其實這只是 Web 安全中的冰山一角,如果你對此感興趣,不妨在安全領(lǐng)域繼續(xù)深耕學(xué)習(xí),一定能看到更廣闊一片天。


對于一個開發(fā)者來說,我們應(yīng)該在寫代碼時就將安全考慮其中,形成自己的一套安全開發(fā)體系,做到心中有安全,時時考慮安全,就能無形之中化解不法分子的攻擊。

藍(lán)藍(lán)設(shè)計www.bouu.cn )是一家專注而深入的界面設(shè)計公司,為期望卓越的國內(nèi)外企業(yè)提供卓越的UI界面設(shè)計、BS界面設(shè)計 、 cs界面設(shè)計 、 ipad界面設(shè)計 、 包裝設(shè)計 、 圖標(biāo)定制 、 用戶體驗 、交互設(shè)計、 網(wǎng)站建設(shè) 、平面設(shè)計服務(wù)

關(guān)于醫(yī)療產(chǎn)品經(jīng)理,這7件事你需要知道

資深UI設(shè)計者

編輯導(dǎo)讀:隨著醫(yī)療行業(yè)與互聯(lián)網(wǎng)的聯(lián)系日益緊密,醫(yī)療行業(yè)對產(chǎn)品經(jīng)理的需求也越來越迫切。在這個特殊行業(yè)中,醫(yī)療產(chǎn)品經(jīng)理需要具備哪些能力,應(yīng)該如何工作,創(chuàng)造哪些價值?本文圍繞醫(yī)療產(chǎn)品經(jīng)理展開,從七個方面展開介紹分析,希望對你有幫助。

越來越多的醫(yī)療機(jī)構(gòu)開始考慮設(shè)置醫(yī)療產(chǎn)品經(jīng)理這個崗位,但是對于產(chǎn)品經(jīng)理具體應(yīng)該做什么工作,可能產(chǎn)生何等價值,以及如何招聘到合適的人才,和這個角色在組織內(nèi)部如何開展工作,都有很多的困惑。今天我們就簡單聊聊這個話題。

總的來說醫(yī)療產(chǎn)品經(jīng)理還是個非常新,甚至可以說有一些超前的職能,傳統(tǒng)FMCG和互聯(lián)網(wǎng)行業(yè)的產(chǎn)品經(jīng)理對應(yīng)的工作內(nèi)容和思考方式并不能簡單照搬過來使用。我們需要清空過去在這些行業(yè)積累的認(rèn)知,從醫(yī)療經(jīng)營的原點出發(fā),從下面7個方面思考:

  1. 醫(yī)療產(chǎn)品經(jīng)理的價值何在
  2. 醫(yī)療產(chǎn)品的設(shè)計邏輯
  3. 醫(yī)療產(chǎn)品經(jīng)理的職責(zé)
  4. 好的產(chǎn)品經(jīng)理應(yīng)該具備的技能
  5. 產(chǎn)品經(jīng)理需要和哪些部門溝通
  6. 與產(chǎn)品經(jīng)理相關(guān)的組織架構(gòu)
  7. 醫(yī)療產(chǎn)品經(jīng)理的招聘和培養(yǎng)

一、醫(yī)療產(chǎn)品經(jīng)理的價值何在

產(chǎn)品經(jīng)理就是足球場上的中場大將,起到承上啟下,功放轉(zhuǎn)換的樞紐,具體說有三大作用:

  1. 進(jìn)攻的發(fā)起
  2. 防守的第一道屏障
  3. 三條線的串聯(lián)

什么是進(jìn)攻?進(jìn)攻就是嘗試主動去占據(jù)一塊領(lǐng)地。

對營利性醫(yī)療機(jī)構(gòu)來說最常見的情況有4種:

  1. 地域上,新開了一個診所,或者一家醫(yī)院,要進(jìn)攻這個網(wǎng)點覆蓋的人群。
  2. 專業(yè)上,新增了一個科別,可以多覆蓋若干種疾病人群,包括自己存量病人,也包括切割存量市場競品的份額。
  3. 設(shè)備上,新裝備了一種設(shè)備,可以開展之前不具備的檢查、治療和手術(shù)能力。
  4. 還有一種是階段性行動,在沒有新的項目情況下,為了擴(kuò)大客戶群,采取一定的促銷行動,最典型的如雙十一洗牙9.9。

那么,又何謂防守呢?簡單來說,就是對應(yīng)上述各種進(jìn)攻的應(yīng)對。

十年前私立醫(yī)院還不算很多,也還沒有那么多連鎖診所品牌的時候,事實上大家主要都在忙著跑馬圈地,短兵相接的攻防其實并不多?,F(xiàn)在隨著市場參與者的倍增,慢慢開始出現(xiàn)了小區(qū)域內(nèi)的半正面PK,并且我預(yù)計在未來兩年內(nèi)可能會出現(xiàn)直接內(nèi)部指名道姓對標(biāo)的戰(zhàn)斗。

足球場上的三條線是進(jìn)攻、防守和中場,這里我們所說的三條線,大體對應(yīng)的是:市場營銷、醫(yī)療質(zhì)量和行政職能三大板塊。產(chǎn)品經(jīng)理的重要價值就是能夠打通這三條線的隔閡,把整個醫(yī)院的資源凝結(jié)成有效的市場成果。

二、醫(yī)療產(chǎn)品的設(shè)計邏輯

醫(yī)療產(chǎn)品不等于,不等于,不等于“打包套餐”!這個概念請務(wù)必建立起來。

首先要厘定什么是醫(yī)療產(chǎn)品。

可以用“三個明確”來界定之:

  1. 明確人:由專業(yè)醫(yī)務(wù)人員實施的某種行為
  2. 明確物:有標(biāo)準(zhǔn)的資源配比、服務(wù)項目
  3. 明確錢:有公開的定價

醫(yī)療產(chǎn)品開發(fā)的邏輯的源頭就在于評估一種醫(yī)療服務(wù)是否吻合這三個明確,因此不是所有的醫(yī)療服務(wù)都可能變成“產(chǎn)品”。

比如我們醫(yī)院口腔科有一個非常棒的醫(yī)生,專注于牙齒美容,我們稱之為“微笑設(shè)計”。這種設(shè)計是完全量體裁衣的,我們市場團(tuán)隊對他的關(guān)注點就主要在故事性的傳播,而不是試圖將這種高度個性化、動態(tài)化的醫(yī)療行為產(chǎn)品化。

簡單來說,對于符合三個明確的醫(yī)療服務(wù),我們對其進(jìn)行產(chǎn)品化的“化”,是一系列有序的動作:

  • 確定需求
  • 自我評估
  • 調(diào)配資源
  • 制定服務(wù)
  • 設(shè)定價格
  • 內(nèi)部培訓(xùn)

囿于篇幅,這里我們就不展開詳述了。

三、醫(yī)療產(chǎn)品經(jīng)理的職責(zé)所系

理想中的醫(yī)療產(chǎn)品經(jīng)理對下面4件事情負(fù)責(zé):

  1. 開發(fā)新品
  2. 發(fā)起促銷
  3. 產(chǎn)品監(jiān)測
  4. 競品追蹤

很容易看出來,這4種不同的職責(zé)恰好也就對應(yīng)了攻防轉(zhuǎn)換的價值所在。其中促銷是一個容易被忽視和輕視的事情,“不就是打折然后發(fā)個微信(十年前是發(fā)短信)推文嘛”——絕對不是這樣,促銷是一門大學(xué)問,打折、捆綁、買贈、兌獎、積分凡此種種。不僅花樣很多,更重要的是背后的深層次的思考,是“為什么”。

另外,目前的醫(yī)療機(jī)構(gòu)基本上也沒有人比較認(rèn)真、成體系地做競品追蹤。這樣會失去潛在市場機(jī)會,非??上?。

四、好的產(chǎn)品經(jīng)理應(yīng)該具備的技能

我認(rèn)為一名出色的醫(yī)療產(chǎn)品經(jīng)理應(yīng)該在下面四個方面都具備一定的能力,沒有特別的短板:

  1. 學(xué)習(xí)能力
  2. 同理心
  3. 數(shù)據(jù)敏感性
  4. 表達(dá)能力

特別就同理心和表達(dá)能力簡單闡述。同理心,即換位思考,用現(xiàn)在更流行的話說,是場景意識。能否準(zhǔn)確地設(shè)置出用戶的場景,體會到用戶的感受,會直接決定產(chǎn)品帶給客戶的體驗,進(jìn)而一系列結(jié)果:定價、毛利、傳播ROI、客戶口碑,口碑帶來的新客增長,等等不一而足。

而表達(dá)能力則是決定這個產(chǎn)品經(jīng)理是否能實現(xiàn)“串聯(lián)三條線”價值的決定性因素。醫(yī)院是一個觀念高度保守,流程高度復(fù)雜的行業(yè),很多人雄心勃勃地進(jìn)來,最終死在“搞不定那些人”上。因此,優(yōu)秀的表達(dá)能力,包括書面和口頭表達(dá)能力,是遴選醫(yī)療產(chǎn)品經(jīng)理必須考量的重要因素之一。

五、產(chǎn)品經(jīng)理需要和哪些部門溝通

我不是危言聳聽:產(chǎn)品經(jīng)理幾乎要和醫(yī)院里所有部門打交道。

常見的如下面這些:

  • 醫(yī)療
  • 護(hù)理
  • 財務(wù)
  • 客服
  • 呼叫中心
  • 新媒體
  • 地推/線下活動
  • 推薦:獨立的數(shù)據(jù)部門

醫(yī)療、護(hù)理和財務(wù)對于產(chǎn)品工作的重要性相信無需贅言。后面幾個呢?

試想,你精心設(shè)計的賣點,是你自己拉著每一個潛在客戶去吆喝么?當(dāng)然不是,客服要幫你介紹,新媒體要幫你寫文章、畫插圖。他們是不是要吃透你的意思?如果涉及填表、兌獎,要不要和客服商量流程?遇到產(chǎn)品的技術(shù)較為復(fù)雜,需要不需要策劃一些活動幫助客戶直觀理解其價值?最后,當(dāng)潛在用戶感興趣而打電話給呼叫中心的時候,接線員是否已經(jīng)被你提前武裝好,能充分回答各種提問了?

至于獨立的數(shù)據(jù)部門,是我的一個強(qiáng)力推薦。傳統(tǒng)上由財務(wù)和病案提供的數(shù)據(jù),更多聚焦于“既往”而很少關(guān)注“開來”。如果不由同時懂得醫(yī)療業(yè)務(wù)和有商業(yè)經(jīng)驗的數(shù)據(jù)部門處理,很難直接推動運營的改善和提升。

六、與產(chǎn)品經(jīng)理相關(guān)的組織架構(gòu)

很多人問我,產(chǎn)品經(jīng)理屬于市場營銷部門嗎?難道不是屬于運營部,或者醫(yī)療企劃之類的部門嗎?

別忘了,市場營銷最基本的范式——4P中第一個P就是產(chǎn)品,Product。只要你所在的醫(yī)療機(jī)構(gòu)設(shè)置有相對完整的市場部門,就應(yīng)該在其中設(shè)置產(chǎn)品經(jīng)理崗位?;蛘叻催^來說,如果你準(zhǔn)備建制產(chǎn)品經(jīng)理崗位,從一開始就應(yīng)該將其設(shè)置在市場部內(nèi),并從頭開始考慮這個職位所需要的上下游角色和他們之間的銜接。

如前面所分析的,產(chǎn)品經(jīng)理的后端,一定要有提供數(shù)據(jù)支持的部門,前端一定要有專業(yè)的傳播團(tuán)隊,這樣才能實現(xiàn)產(chǎn)品的潛力。橫向上,產(chǎn)品經(jīng)理和他的上級,一定要高度重視與客戶服務(wù)團(tuán)隊的緊密合作。

七、醫(yī)療產(chǎn)品經(jīng)理的招聘和培養(yǎng)

老實說,現(xiàn)在幾乎沒有多少現(xiàn)成的、成熟的醫(yī)療產(chǎn)品經(jīng)理。因為營利性醫(yī)療行業(yè)太新了,而產(chǎn)品經(jīng)理這個崗位在這個行業(yè)又是近一兩年剛剛興起的角色。

從招聘角度來說,我建議不要拘泥于候選人必須有醫(yī)療背景。我就沒念過醫(yī)學(xué)院,十一年前入行,就這樣摸著石頭過河,也多多少少做過一些還不錯的產(chǎn)品,有過幾個“爆款”的心得。相對來說,我更看重候選人是否有完整的商業(yè)思考邏輯能力。

換一個角度來說,還可以在醫(yī)院內(nèi)部挖掘有潛力的人才,從臨床部門轉(zhuǎn)型為產(chǎn)品經(jīng)理。最關(guān)鍵的環(huán)節(jié)在于這個人是否有足夠強(qiáng)烈的興趣。世上無難事只怕有心人,有醫(yī)學(xué)院背景的人才,只要對產(chǎn)品工作發(fā)自內(nèi)心感興趣,就有很大的轉(zhuǎn)型成功概率。

文章來源:人人都是產(chǎn)品經(jīng)理    作者:易亮 

藍(lán)藍(lán)設(shè)計www.bouu.cn )是一家專注而深入的界面設(shè)計公司,為期望卓越的國內(nèi)外企業(yè)提供卓越的UI界面設(shè)計、BS界面設(shè)計 、 cs界面設(shè)計 、 ipad界面設(shè)計 、 包裝設(shè)計 、 圖標(biāo)定制 、 用戶體驗 、交互設(shè)計、 網(wǎng)站建設(shè) 、平面設(shè)計服務(wù)

如何做好Banner設(shè)計?

資深UI設(shè)計者

「空間陳列法」是說先構(gòu)建一個空間,然后將主體元素用合適的形式陳列出來。

這是隨著手機(jī)興起而真正流行起來的一招,因為PC時代都是寬大的橫屏設(shè)計,適合展現(xiàn)視野開闊的大場面,像大漠、海邊等等,而「空間陳列」作為小場景,在PC端就顯得不大氣,因此使用較少;而手機(jī)端卻剛好相反,瘦長豎屏就適合表現(xiàn)長焦特寫的小場景,像微距下的花鳥魚蟲等等,這時「空間陳列」就用的恰到好處。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

如圖所示,同樣的產(chǎn)品展示圖,在PC端就顯得單薄,版面空缺,整體不飽滿;而在手機(jī)端則用的剛剛好,確實這種長焦特寫、微距放大的陳列小場景就是手機(jī)屏的最愛,所以在手機(jī)時代,空間陳列圖才會呈現(xiàn)井噴式增長。

其實合成、三維和攝影都可以實現(xiàn)「空間陳列」,但本書還是以合成為主,而合成的難點就在于如何將產(chǎn)品和空間進(jìn)行自然融合,不能有違和感。

若想合的天衣無縫,第一步就是要做到「透視準(zhǔn)確」,而透視作為構(gòu)圖中的重要知識點,可以說是無處不在,在前面的章節(jié)里也多次提及。我們只有掌握透視的變化規(guī)律,才能準(zhǔn)確表現(xiàn)出元素的空間關(guān)系,如果透視不對,那空間將會失真,下面就來詳細(xì)講講這個理性知識點——透視。

焦點透視

日常生活中,當(dāng)我們看周圍事物時,會有遠(yuǎn)近、高低、長短、寬窄等不同,這是由于距離、方位等差異在視覺中呈現(xiàn)的不同反映,這種現(xiàn)象就是透視。透視學(xué)的出現(xiàn)可以幫我們非??茖W(xué)的表現(xiàn)各種空間感和立體感,它廣泛用于繪畫、建筑、環(huán)藝、設(shè)計等諸多領(lǐng)域,而常見的透視共3類:空氣透視、散點透視和焦點透視,這3類的側(cè)重點各有不同。

空氣透視又稱「色彩透視」,由于空氣介質(zhì)的存在(雨、雪、霧、煙等),使人們看到近處景物比遠(yuǎn)處的輪廓更清晰、色彩更飽滿的視覺現(xiàn)象,例如下方海報中的「煙雨蒙蒙」,這種近實遠(yuǎn)虛感就是典型的空氣透視,隨著鏡頭拉遠(yuǎn),山川也變得越來越模糊。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

散點透視則是中國畫特有的一種透視類型,例如下方的《清明上河圖》就是這類透視的代表作,在這五米長的畫卷中,很難找出畫家的具體觀察位置,好似在移動中作畫,每到一處畫一部分,最后拼接起來,這種視點不斷移動的畫法就是散點透視,散點透視適合表現(xiàn)景色的波瀾壯闊,重在寫意,體現(xiàn)一種氣勢和意境。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

而焦點透視才是本文重點,它是透視學(xué)中的核心理論,也是西方繪畫所遵循的透視原則,最早研究透視就是從這里開始,如果散點透視是「寫意」,那焦點透視則「寫實」,一切都以客觀還原為準(zhǔn)。

例如名畫《最后的晚餐》,所有視線都匯聚一點(稱為滅點),營造出一種立體空間感,這些就像自己身處畫面中央所看到的逼真景象。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

而我們在設(shè)計中常說的透視也都指「焦點透視」,這是我們需要掌握的重中之重。記得高中學(xué)習(xí)素描時,老師就說畫靜物要「近大遠(yuǎn)小」,其實就是對焦點透視最為形象的描述,例如草地上的奶牛,離我們越近就越大,越遠(yuǎn)則越小,正是這種近大遠(yuǎn)小的透視變化才使場景有了空間和層次。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

觀察視角

在介紹焦點透視前,我們先說說透視中一個很重要的影響因素——觀察視角。視角即指人眼(稱為視點)在觀察事物時視線之間所形成的角度。

如下圖所示,其實就是人眼觀看角度的變化,常見有3種:當(dāng)我們平視前方時就是「平視視角」;仰頭看時則是「仰視視角」;低頭看時便是「俯視視角」。

其中平視時人眼和物體形成的假想連線稱為「水平視線」,這是判斷視角高低的參考線。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

當(dāng)我們將產(chǎn)品放入空間時,就要根據(jù)陳列形式選擇合適視角,從下方的示意圖中能看到,三種視角給人的感受都不相同:

  • 平視有種方方正正感,給人一種非常自然的觀察感受,雖然中規(guī)中矩但視覺舒服;
  • 仰視則能體現(xiàn)產(chǎn)品的高大和氣勢,用來烘托價值感;
  • 而俯視最接近我們?nèi)粘?醋烂嫘∥锲返囊暯?,很真實也很親切,同時還凸顯了產(chǎn)品的立體感。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

而上圖的仰視和俯視都屬于小視角,即產(chǎn)品視線和水平視線的夾角較小,這是設(shè)計中的常見視角,大概就是把頭微微抬起或低下時看到的場景,這時畫面最自然也最舒服。反之若視角過大,即頭抬的很高或壓的很低時,這時產(chǎn)品的形變就很夸張,顯得刻意、不舒服。

說完3種視角,現(xiàn)在正式講解焦點透視,一般根據(jù)物體滅點的數(shù)量不同,焦點透視又分3種:平行透視(一點透視)、成角透視(兩點透視)和斜角透視(三點透視),它們都有各自的透視效果和適用范圍,但若鋪開講會很復(fù)雜,因此下面就結(jié)合「空間陳列」進(jìn)行介紹。

1. 平行透視

用立方體簡單說明,就是有一面與畫面平行,這時物體的厚度邊線若向內(nèi)延伸,最后都會匯聚到1個點上,因此又稱「一點透視」。這是最簡單也最易掌握的一種透視形式,其中匯聚點稱為「滅點」,而滅點所在的那條線則是「視平線」,即與人眼等高的一條水平線。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

再來看看平行透視在生活中的場景呈現(xiàn),如圖所示,將各種景物進(jìn)行前后連線并延伸,最后都是匯到一點才消失。平行透視適合表現(xiàn)場景縱深,給人一種正式感和平和感。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

電商中的產(chǎn)品展示也一樣,例如下方示意圖中,不管哪種視角,產(chǎn)品和立方體都是正對觀眾,讓人覺得擺放角度正正好。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

總體來說平行透視只有1個滅點,變化并不多,視覺表現(xiàn)單一,沒有太多的空間變化,基本就是從正面來表現(xiàn)整個場景,因此上手簡單,只要確保前后連線都匯聚一點即可,這樣畫面各元素也會顯得整齊。但有時這種正視會讓畫面缺少層次感,顯得很平,此時可嘗試俯視視角或者強(qiáng)化背景的空間縱深。

下面展示平行透視在3種視角下的應(yīng)用案例,注意觀察不同視角下的產(chǎn)品呈現(xiàn)和透視變化,雖然微妙,但每種視角確實給人不一樣的視覺感受。

平行平視

當(dāng)畫面為平行透視和平視視角時,這時的觀察位置很正。如下圖所示,空間和產(chǎn)品都顯得有些平整,雖然場景的立體感較弱,但視覺舒服協(xié)調(diào),表現(xiàn)起來也相對簡單。注意平視的「視平線」基本位于主體元素的中心處,即是說人眼此時正對前方物體的中心,這樣才會有平視效果。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

平行仰視

當(dāng)畫面為仰視時,一般視角都不會太大,微微仰視即可,這樣視覺才舒服。如下圖所示,其實和平視比起來,小角度仰視的透視變化并不明顯,沒有夸張形變,但依然能體現(xiàn)空間和產(chǎn)品的高大。此時「視平線」位于主體中心靠下的位置,這時人眼明顯是從下往上看。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

平行俯視

如果覺得畫面的層次感和立體感不夠,那就嘗試下俯視視角,如下圖所示,由于俯視時我們能同時看到物體的頂面和正面,這樣就能表現(xiàn)物體的厚度,立體感也明顯增強(qiáng)。而畫面的「視平線」則位于主體中心靠上的位置,這時人眼就是從上往下看,但同樣屬于小角度俯視。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

2. 正俯視

平行透視的俯視還有一種特殊情形——正俯視,即視角為90°的俯視,這時我們是從物體的正上方低頭往下看,如下圖所示,當(dāng)產(chǎn)品平放桌面時,正俯視能清晰看到產(chǎn)品的全貌。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

在空間陳列中這是一種常見視角,上手簡單,展現(xiàn)清晰。例如下方案例中,俯視下的產(chǎn)品擺放非常靈活,根據(jù)構(gòu)圖需求可以工整 ① 也可以隨意 ② ,并且產(chǎn)品多以正面展現(xiàn)為主,整體直觀、舒服。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

以上都是產(chǎn)品的陳列案例,其實正俯視有時也會用于場景呈現(xiàn)中,如下圖所示,視點位于場景的正上方,有點類似無人機(jī)的俯瞰拍攝,這種看似「刁鉆」的視角能給畫面帶來獨特的戲劇效果,令人印象深刻。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

空間平行透視

除了3種視角,這里還要介紹一種平行透視的常見形式——空間平行透視,這種形式即畫面的正前方有個類似方盒的縱深空間,而人物或產(chǎn)品就放置在空間里。

如圖所示,該形式也有視角的3種變化,但為了確保視覺的自然舒服,仰視和俯視也都是小角度的上下擺動,所以產(chǎn)品的透視變化并不明顯,場景呈現(xiàn)也沒有很夸張。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

為何要將該形式單獨列出?因為它非常適合手機(jī)端的豎屏構(gòu)圖。

手機(jī)不像PC,無法通過寬屏來表現(xiàn)視野開闊的大場面,手機(jī)屏更適合長焦特寫的小場景,但這樣有時就會顯得左右擁擠不透氣,這時「空間平行透視」就剛好取長補(bǔ)短,通過「深度」刻畫將狹窄空間的縱深感體現(xiàn)出來,最終使觀者視線在前后維度上有了延伸和舒展。

該形式正是利用平行透視的縱深性,才緩解了手機(jī)屏的擁擠感。下面分視角列舉案例,要注意不管平視、仰視還是俯視,空間里的所有元素最后都要匯聚一點,這樣透視才合理,縱深效果也最好。

3. 空間平視

3種視角中,空間平視最常見,因為這種方方正正的空間展示最適合手機(jī)的豎屏構(gòu)圖,看著最舒服,上手也簡單,易于搭建。在平視下,由于沒有視角的高低變化,空間基本位于人眼的正前方,無任何偏移,擺放角度非常正,構(gòu)圖給人一種穩(wěn)定感,元素也沒有夸張形變,組合方便,真實自然。

對于「空間平行透視」,「深度」刻畫很關(guān)鍵,我們要根據(jù)版面構(gòu)圖選擇合適的深淺。例如下方案例中:① ② 的深度淺,空間相對封閉,適合展現(xiàn)小空間,給人溫馨感和趣味性;而 ③ 的深度深,空間開闊,適合展現(xiàn)大空間,這樣能讓視線更舒展,畫面更透氣。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

4. 空間仰視

在仰視下,我們是抬頭向上看,這時空間顯得高大,給人強(qiáng)烈的氣勢感。如下圖所示,視平線貼近地面,像是我們蹲著向上看,這種仰望視角,建筑和人物都很高大,再加上強(qiáng)烈的縱深感,雖然空間左右依然狹窄,但上下和前后維度卻變的非常開闊,畫面通透。

空間仰視能渲染氛圍,提升場景的戲劇效果,突出視覺沖擊力,但要注意仰視視角不能太大,否則夸張的仰視效果反而給人一種壓抑感。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

5. 空間俯視

在俯視下,視平線處于空間中心的上方,類似我們站在高處往下看,如下圖所示,這時空間顯得立體,遠(yuǎn)近的各類元素也能得到清晰展現(xiàn),層次分明。

俯視適合展現(xiàn)空間的全局觀,也讓各物體有著豐富的體積感。除非有特殊的構(gòu)圖需求,不然和仰視一樣,俯視視角不能過大,否則俯視就變成俯瞰,會產(chǎn)生遙遠(yuǎn)的距離感,空間也壓縮的厲害,進(jìn)而導(dǎo)致形變和失真。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

成角透視

還是拿立方體舉例,就是物體與畫面形成一定夾角,這時物體的所有邊線分別向各自方向進(jìn)行延伸,最后會在視平線上形成一左一右2個滅點,因此又稱「兩點透視」。這類透視最接近我們?nèi)粘5挠^察角度,即是說大部分時候,我們看到的物體都屬于成角透視。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

雖然成角透視只比平行透視多了1個滅點,但2個滅點的位置卻很靈活,這樣空間的透視變化也更加豐富。

例如下方是我們經(jīng)常看到的景象,雖然都是典型的成角透視,但 ① 是2個滅點都在畫面外,這時建筑給人的感覺結(jié)構(gòu)平穩(wěn),立體感強(qiáng),側(cè)重寫實;而 ② 則是1個滅點在畫面內(nèi),另1個在畫面外,這時空間右側(cè)的透視形變較大,產(chǎn)生縱深感,整個場景更有張力和沖擊。

其實還有第3種情形是2個滅點都在畫面內(nèi),但由于空間會產(chǎn)生夸張形變和失真,因此總體少見,不再舉例說明。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

再看看成角透視下的產(chǎn)品展示效果,如下圖所示,不同于單面展示為主的平行透視,成角透視則以展示物體的兩面為主,這樣立體感更強(qiáng),構(gòu)圖也更穩(wěn)定。注意在成角透視中,畫面所有的豎向邊線都是平行,不會產(chǎn)生滅點。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

相比平行透視,成角透視在表現(xiàn)上更復(fù)雜一些,一般都以2滅點在畫面外的情形為主,這時透視最舒服。注意要想畫面只產(chǎn)生2個滅點,那當(dāng)中的所有元素都需排列整齊,這也是成角透視的常用做法,此時畫面會顯得整齊統(tǒng)一。下面列舉3種視角下的電商案例,其中以仰視和俯視最為常見。

1. 成角平視

平視下的成角透視相對少見,因為使用成角透視就是為了凸顯物體的立體感,但平視由于視角很正,恰恰就會顯得立體感較弱,這時2種效果會有矛盾,影響場景的協(xié)調(diào)性。例如下方的2個案例中,產(chǎn)品看著就有些平整,和方形盒子以及立方體稍顯沖突,但整體視覺真實平和,沒有形變。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

2. 成角仰視

仰視下的成角透視就顯得剛剛好,如下圖所示,所有元素均用2個立面展示,加上透視形變,這時空間的立體感強(qiáng),產(chǎn)品和立方體也都有明顯的體積感,視覺平穩(wěn)、飽滿,而且還能體現(xiàn)產(chǎn)品形象的高大,凸顯價值感。

注意2個案例中,視平線上都只有2個滅點,這是因為產(chǎn)品和立方體的排列都很整齊,反之若無序排列,就會產(chǎn)生多個滅點,這樣畫面會顯凌亂,視覺不舒服,所以在表現(xiàn)成角透視時,盡量確保所有元素都能整齊排列。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

3. 成角俯視

在成角透視中,俯視視角最常見。因為該視角下的物體可以展現(xiàn)3個面,能進(jìn)一步強(qiáng)化元素的立體感和方位感,如圖所示,物體的空間關(guān)系明顯,層次分明,構(gòu)圖也平穩(wěn)。

一般成角俯視適合小場景陳列(若是大場景則垂直方向會發(fā)生嚴(yán)重形變,這就是后面要講的「斜角透視」),剛好這是手機(jī)屏的擅長,小空間配上小角度俯視會給人一種親切感,類似長焦鏡頭的特寫畫面,很好的拉近了觀者的心理距離,因此屬于手機(jī)端的常用構(gòu)圖。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

空間成角透視

成角透視的優(yōu)勢在于畫面立體、穩(wěn)定和寫實,這些優(yōu)勢剛好適合空間的立體呈現(xiàn),如下圖所示,成角透視即可用于室內(nèi)塑造 ① 也可用于外形搭建 ② ,類似我們站在空間側(cè)面看整體,此時空間立體、飽滿,結(jié)構(gòu)有張力。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

但由于成角透視都是在畫面兩端形成滅點,所以該透視下的空間更適合橫構(gòu)圖,但手機(jī)屏卻是豎構(gòu)圖,對于橫向擁擠的豎長屏,成角透視就會有些施展不開,左右狹窄,無法像橫版那樣開闊的展現(xiàn)空間,也沒有平行透視那樣看著規(guī)整,因此使用較少。

斜角透視

物體與畫面存在一定夾角,并且在2點透視的基礎(chǔ)上,再加入了高度變化,這樣垂直方向的連線會向上或者向下匯聚,最終畫面形成3個滅點,又稱「三點透視」。相比成角透視,斜角透視其實就是讓本沒有交集的豎線有了交集,這樣垂直方向就有了強(qiáng)烈的匯聚感。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

斜角透視的形變夸張,常用于大型物體的仰視或高處俯瞰,類似廣角拍攝。

如下圖所示,該透視能表現(xiàn)出建筑或空間的宏大感,并且越宏大透視就越強(qiáng)烈。這時畫面的夸張構(gòu)圖會顯得觀者渺小,給人一種壓迫感,也讓場景有著極強(qiáng)沖擊力,同時帶來了更加刺激的視覺感受。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

其實只要觀者在場景中顯得很小,這時看到的畫面就會產(chǎn)生斜角透視,例如當(dāng)我們仰望高樓時,相對高樓而言,渺小的我們就會看到斜角透視。

但如果產(chǎn)品展示采用斜角透視時,就會有一種強(qiáng)烈的不真實感,因為相對產(chǎn)品來說,我們并不渺小,所以日常是不會看到這樣的場景,這種場景更像是「昆蟲視角」,如圖所示,斜角透視下,雖然畫面不真實,但會有種特別的戲劇效果。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

另外斜角透視沒有平視視角,因為平視物體的豎向邊線依然平行,不會在垂直方向產(chǎn)生第3個滅點,因此仍屬于成角透視??傊挥性诖蠼嵌妊鲆暬蚋┮暣笮臀矬w時,才會看到斜角透視。

這種形變強(qiáng)烈的夸張透視雖然生活中相對少見,但電商中用的還真不少。還記得我們在第2章講創(chuàng)意方法時提過的「獨特視角」嗎?其中一個方向就是使用斜角透視。

這種透視可以體現(xiàn)物體的巨大(仰視)或者場景的宏大(俯視),正是這樣一種不真實也不自然的視覺感受,反倒給人一種強(qiáng)烈的氣勢和沖擊,畫面極具張力的構(gòu)圖往往能脫穎而出,并在第一時間抓人眼球,吸引注意,所以成角透視特別適合大促主題的場景搭建和氛圍營造,下面分視角舉例。

斜角仰視

視能讓物體顯得高大,而斜角仰視則讓物體顯得「巨大」。如圖所示,2個案例中的產(chǎn)品都十分「巨大」,通過這樣一種「刁鉆」視角和夸張手法渲染出了產(chǎn)品氣勢,使產(chǎn)品顯得分量十足,同時也提升了視覺沖擊力,整個場景都變的大氣。其中 ① 由于版面有限,有個滅點沒有標(biāo)示出來。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

斜角俯視

當(dāng)我們站在一個很高的地方俯瞰周圍,或者用無人機(jī)在高空航拍,這時看到的景象就是斜角俯視。如圖所示,盡管豎構(gòu)圖并不適合展現(xiàn)遼闊的大場面,但在斜角俯視的幫助下,2個案例依然體現(xiàn)了場景的宏大,視覺沖擊強(qiáng),這種居高臨下感使人視野開闊。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

平行斜角透視

斜角透視中還有一種特殊情形——平行斜角透視,就是當(dāng)空間或產(chǎn)品的一面與畫面平行時,此時不管透視多強(qiáng)烈,畫面也只有2個滅點。

如圖所示,當(dāng)立方體的一面正對視點時,側(cè)面便從2個主立面減為了1個,這時除了垂直方向的1個滅點外,原本視平線上的2個也就成了1個,雖然只剩2滅點,但本質(zhì)仍屬于透視強(qiáng)烈的斜角透視。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

既然是與畫面平行,那和平行透視有何區(qū)別?

下面看張空間對比圖,能看到2者的形變差異還是相當(dāng)大:左圖為平行透視,像是我們在看一個小方盒,親切、自然、真實;而右圖則是平行斜角透視,更像是我們在仰望一個巨大空間的入口,充滿戲劇性,并有壓迫感。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

其實對于手機(jī)端,「平行斜角透視」才是斜角透視中的常用形式,因為它的擺放角度很正,這種正面觀察很適合手機(jī)端的豎屏構(gòu)圖,而且前后的縱深刻畫也能緩解版面的左右擁擠,另外畫面縱向的匯聚感還能迅速吸引注意,給人一種巨大沖擊力和強(qiáng)烈氛圍感,其中仰視比俯視更加常見。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

平行斜角仰視

近幾年這類透視越來越多見,因為它既適合豎構(gòu)圖,也能提升畫面的形式感和表現(xiàn)力,非常利于促銷主題的氛圍打造。

如圖所示,整個畫面就像是我們站在宏大場景的正中央仰望時看到的景象,各元素擺放很正,雖有壓迫感,但空間和產(chǎn)品都顯得非常高大,張力十足,能讓用戶牢牢聚焦,同時也產(chǎn)生了強(qiáng)烈沖擊力,更易在用戶腦海中形成記憶。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

平行斜角俯視

在這類透視下我們會感覺自己擁有高高在上的「上帝視角」,元素擺放同樣很正,視野遼闊,場景宏大。但過大的俯視視角會對場景進(jìn)行一定壓縮,再加上俯瞰產(chǎn)生的遙遠(yuǎn)距離感,這樣就顯得元素有些「小氣」,無法體現(xiàn)仰視下的高大,因此畫面的刺激感沒那么強(qiáng)烈,總體相對少見。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

3. 小結(jié)

以上便是焦點透視的3種類型,我們再來回顧下各自的使用情形,如下圖所示,這是一張3種透視的轉(zhuǎn)換示意圖,都是仰視視角,旁邊小人則是觀察者的大小示意:

  • 當(dāng)立方體的一面正對觀察者時,就是「平行透視」,這時除了物體厚度的邊線會匯聚1點,其余邊線均無交集;
  • 而當(dāng)立方體旋轉(zhuǎn)一個角度,任何一面都不正對觀察者時,就是「成角透視」,這時橫向邊線會向各自方向匯聚成2點,豎向邊線則無交集;
  • 此時若將立方體變的巨大,大到需要仰望,就是「斜角透視」,這時在2點基礎(chǔ)上,本無交集的豎向邊線將匯成1個新點。

希望通過這張示意圖能幫大家更好理解什么時候該用哪種透視,總之小場景搭建一般以「平行透視」和「成角透視」為主,而恢弘的大場景則以「斜角透視」為主。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

當(dāng)然現(xiàn)實里的透視遠(yuǎn)不會這么單一,根據(jù)物體不同的擺放位置以及不同的觀測距離,很多時候同一畫面也會存在多種透視,例如平行透視和成角透視就經(jīng)常組合出現(xiàn)。

電商設(shè)計也一樣,例如下方案例中,整個空間是平行透視,而里面的盒子則是成角透視,這時視平線上會有3個滅點,其實若產(chǎn)品的擺放再凌亂些,還會出現(xiàn)更多滅點,但這種無序組合會讓空間塑造變的復(fù)雜,看著也不規(guī)整,因此并不推薦。

但要注意不管畫面的透視多復(fù)雜,當(dāng)中的視平線卻只能有1條,并且無論水平方向有多少個滅點,最后也都會落在視平線上。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

除了以上3種透視外,其實還有4點透視(超廣角透視)、5點透視(魚眼透視)等等,但都過于復(fù)雜,用的也很少,這里就不做展開。對于電商里的「空間陳列」,3種透視已夠用,它是我們塑造空間的基礎(chǔ),如果一開始的透視錯了,即便配色、光影做的再出彩那也無用。

總有人說透視難掌握,其實只要我們在生活中多觀察、多留意身邊物體的透視變化和規(guī)律,及時總結(jié),那這種理性還原就不難做到。當(dāng)然設(shè)計終歸還是理性與感性的雙重表達(dá),所以透視雖要遵循,但切忌生搬硬套,視覺協(xié)調(diào)即可。

講完了焦點透視,我們知道了空間塑造的基本原則,下面就來說說都有哪些陳列場景。

陳列場景

相對PC來說,手機(jī)端受屏幕所限,陳列場景其實沒那么復(fù)雜,核心是要先構(gòu)建一個空間,然后讓產(chǎn)品以合適的視角及透視在空間里呈現(xiàn)出來,而這個空間場景則要和主題氛圍、產(chǎn)品氣質(zhì)都高度匹配。

一般來說,手機(jī)端常用的陳列場景有4類:盒子陳列、臺面陳列、自然陳列以及舞臺陳列,選擇哪類則要看哪個場景對產(chǎn)品的烘托效果最好。

1. 產(chǎn)品組合

在介紹每個場景前,我先說說關(guān)于產(chǎn)品組合的2原則,因為很多時候在空間擺放的產(chǎn)品數(shù)量較多,這時它們的組合形式就變的尤其重要,稍不注意就會顯得畫面雜亂無章,不夠協(xié)調(diào),而且凌亂的擺放也會降低產(chǎn)品的品質(zhì)感,缺少吸引力。關(guān)于組合原則,核心有2點:大小合理以及三角構(gòu)圖。

大小合理

如果將多個產(chǎn)品擺一起,則要確保它們之間的相對大小符合現(xiàn)實中的真實差異,現(xiàn)實中尺寸大的產(chǎn)品就要相對大些,而尺寸小的產(chǎn)品就要相對小一點,這樣才會真實并經(jīng)得起推敲。

而有些設(shè)計師在組合產(chǎn)品時,也不管大小的真實差異,放進(jìn)版面后就很隨意的放大或縮小,最后出來的組合要不就大小一樣,要不就比例失真,這些都會給用戶一種強(qiáng)烈的不協(xié)調(diào)感和不真實感。

下面再看一組對比圖,明顯大小一樣的左圖會有不適感,也缺少層次;而大小合理的右圖則更有美感也更舒服。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

其實大小合理的最終目的是希望整體結(jié)構(gòu)錯落有致,就像右圖一樣,這種有高有低的組合才能體現(xiàn)韻律感和結(jié)構(gòu)美。所以如果可以選擇,那我們就選擇一些尺寸差異較大的產(chǎn)品,盡量避免出現(xiàn)大小差不多的情況。

當(dāng)然如果必須陳列大小一樣的產(chǎn)品,那也可以通過透視或者輔助元素來改善,例如空間里的近大遠(yuǎn)小、立方體加高都能改變高度一樣的情形。

三角構(gòu)圖

當(dāng)我們選好不同大小的產(chǎn)品后,就要注意它們的組合形式,千萬不能亂堆一氣,不同的擺放會形成不同結(jié)構(gòu),而每種結(jié)構(gòu)又會給人不同感受。我們在「圖形分割」中講過「正三角」具有很強(qiáng)的穩(wěn)定性,因此當(dāng)產(chǎn)品采用正三角構(gòu)圖時,會讓人覺得版面平穩(wěn)、視覺舒服。

如圖所示,所謂三角構(gòu)圖,其實就是將尺寸大的產(chǎn)品放中間,而尺寸小的產(chǎn)品放兩邊,這樣不但構(gòu)圖穩(wěn)定,而且畫面也有節(jié)奏和變化??梢哉f「三角構(gòu)圖」就是空間陳列里最常用的構(gòu)圖方式,而本節(jié)展示的所有案例中,大部分也都是三角構(gòu)圖。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

明確了產(chǎn)品的組合原則,知道了如何陳列才舒服,下面就正式講講空間陳列的4類場景。

2. 場景

盒子陳列

「盒子陳列」就是在盒子里面放產(chǎn)品,而盒子多以禮盒為主,使用場景和主題相對單一,基本用于送禮之意的專題頁。創(chuàng)意雖然普通,但卻不易出錯,是種相對安全的表現(xiàn)方式。當(dāng)然若能在盒里加些小心思,畫面也會很出彩,像我之前看過一個新年Banner,就是禮盒里裝著一個大家庭在吃年夜飯的溫馨場景,這樣的新組合便讓人眼前一亮。

另外若能提升禮盒刻畫的精致度,那畫面也會有不錯的設(shè)計感。而盒子外形也不只有方形,常用的還有圓形和異形。觀察視角則以小角度俯視居多,因為這個視角最接近我們?nèi)粘?炊Y盒的真實情形,盒內(nèi)產(chǎn)品在俯視下能看的一清二楚,展現(xiàn)也立體。

盒子陳列的難點在于當(dāng)盒內(nèi)要擺放很多產(chǎn)品時,如何能讓產(chǎn)品真實、自然的呈現(xiàn),這需要我們既注意擺放的合理性,也能準(zhǔn)確表現(xiàn)透視,還要刻畫出產(chǎn)品的明暗變化,總之只有把握好產(chǎn)品的空間感、立體感以及光影感,畫面才會舒服協(xié)調(diào)。

方形盒是最常用的盒子類型,畢竟也是生活中最常用的禮盒外形,結(jié)構(gòu)感強(qiáng)而且易表現(xiàn)。如下圖所示,禮盒都是成角透視,且左右滅點都在畫面之外,這樣結(jié)構(gòu)最穩(wěn)定,立體感也強(qiáng)。注意盒里的產(chǎn)品呈現(xiàn),特別是俯視視角下,產(chǎn)品越多越要注意它們是否協(xié)調(diào)統(tǒng)一,透視、光影等細(xì)節(jié)一個都不能少,把控不到位就會顯得凌亂,畫面別扭。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

圓形盒比較少見,因為和立方體比起來,圓柱體的透視沒那么強(qiáng)烈,結(jié)構(gòu)感偏弱,但圓潤的外形能使畫面變的柔和,給人一種親和力和溫馨感,如圖所示,由于圓形盒沒有明顯塊面,所以不管透視還是光影,刻畫起來都相對簡單。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

異形盒是指外形為不規(guī)則形狀的盒子,總體也很少見,但易出彩。形狀用的好便能打破「盒子陳列」的常規(guī)感,使畫面變的新穎有創(chuàng)意。

例如下方案例中,不管是心形、貓頭輪廓還是圣誕樹,都能成為畫面焦點并引人注意。另外盒子呈現(xiàn)均用了「正俯視」視角,其實除了小角度俯視外,這種視角也很常見,因為該視角下的產(chǎn)品陳列清晰完整,盒子外形也能直觀顯示,的展現(xiàn)了其外形的特別之處。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

臺面陳列

在空間陳列的4類場景中,「臺面陳列」用的最多,適用范圍也最廣,可以說電商中的大部分主題和產(chǎn)品都能使用,算是一種真正白搭的表現(xiàn)方式。

「臺面陳列」就是在空間里搭建一個「臺面」,然后在上面放置相關(guān)產(chǎn)品。由于該手法還是以放大特寫的小場景為主,元素形變不能太大,因此畫面常用平行透視和成角透視,視角則很靈活,跟著構(gòu)圖走,3種(平視、俯視、仰視)都有?!概_面陳列」上手簡單,場景多變,其中的關(guān)鍵元素——臺面需要根據(jù)主題、場景進(jìn)行靈活變化,常見有2類:桌面和幾何體。

桌面很好理解,就是桌子頂部的陳列面,所有產(chǎn)品都放置其上。由于桌子是家里常見家具之一,因此桌面陳列往往能傳遞一種「家」的溫暖和溫馨。

可能有人覺得「桌面」形式有些單一,其實遠(yuǎn)沒那么簡單,我們不要固化思維,要能靈活變化桌子的樣式和裝飾,例如方桌還是圓桌?木桌還是大理石桌?光面還是鋪桌布?這些都是可變量,再加上視角和周圍環(huán)境的變化,總之形式可以很豐富。

如圖所示,桌面陳列尤其適合各類美食的組合呈現(xiàn),這時整個場景貼近生活,頗有帶入感。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

就是將各種幾何形體作為陳列產(chǎn)品的臺面,幾何體相對抽象,表現(xiàn)場景更多元,因此比具象「桌面」更加常用。

如圖所示,幾何體一般都是組合出現(xiàn),特別適合多產(chǎn)品陳列,簡約大方,能烘托出產(chǎn)品的品質(zhì)感。同時通過高高低低的大小排列也能表現(xiàn)出畫面的結(jié)構(gòu)美以及層次感,總之是一種 「上手簡單、易出效果」的表現(xiàn)方式。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

常用幾何體有「立方體」和「圓柱體」,它們適合陳列,高度調(diào)節(jié)方便,使用靈活。

如下圖所示,整體表現(xiàn)并不復(fù)雜,就是將各種產(chǎn)品放在幾何體上。但作為畫面的核心元素,這時幾何體的形態(tài)、排列、視角和透視就變的非常重要,我們要根據(jù)創(chuàng)意需求和產(chǎn)品氣質(zhì)選擇最合適的展現(xiàn)方式,而這些展現(xiàn)本身就有不錯的形式感。

幾何體陳列既能營造空間關(guān)系和簡約氣質(zhì),也能讓用戶聚焦產(chǎn)品本身,因為它的外形簡單,不搶產(chǎn)品,不像一些復(fù)雜元素或場景,雖然視覺豐富但最后卻讓產(chǎn)品淹沒其中,這樣就本末倒置。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

自然陳列

「自然陳列」需要先創(chuàng)建一個合適的自然環(huán)境,然后再將產(chǎn)品以合適方式融入其中。相對其他場景,自然環(huán)境顯然復(fù)雜一些,呈現(xiàn)手法常以「合成」和「插畫」為主。因為產(chǎn)品都是實物拍攝,為了風(fēng)格統(tǒng)一,自然環(huán)境會偏向?qū)憣嶏L(fēng)格,這樣2者結(jié)合才協(xié)調(diào)。

從下方案例能看到,「自然陳列」常用于季節(jié)感受或者產(chǎn)地溯源等主題,畫面通過「自然場景」?fàn)I造出天然健康的綠色氛圍。而場景中的元素繁多,呈現(xiàn)復(fù)雜,這就需要我們具備優(yōu)秀的整合能力。

對于「自然」塑造,視角以平視和小角度俯視居多,但畫面由于沒有太多的幾何型物體,所以透視沒那么嚴(yán)謹(jǐn),核心是注意近、中、遠(yuǎn)景的層次區(qū)分,還有光影的合理添加。如果這些把控不到位,就很可能出現(xiàn)場景雜亂、缺少層次、沒有帶入感的粗糙畫面,最終淪為各種素材的亂堆一氣。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

以上列舉的都是以花草樹木為主的自然環(huán)境,確實綠色場景在「自然陳列」中用的也是最多,但除了綠色場景外,有時也會用到其他環(huán)境。如下圖所示,像海底 ① 、沙灘 ② 、海面 ③ 、冰山 ④ 也是適合陳列的自然場景,特別是夏季主題會經(jīng)常用到。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

舞臺陳列

最后一類「舞臺陳列」常用于大促主題的氣氛營造,這類場景不挑類目,任何產(chǎn)品放在「舞臺」上,燈光一打,色彩再斑斕些,都能營造出熱鬧的促銷氛圍。

如圖所示,舞臺外形以圓形居多,因為圓形的透視感較弱,構(gòu)圖靈活,而且也符合大家對舞臺的第一印象。舞臺視角則很靈活,3種均很常見,核心是和產(chǎn)品視角保持一致。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

關(guān)于「舞臺」塑造,還有2處需要注意的地方:

舞臺外形除了最常用的「圓形」外,還有半圓形、方形和六邊形等等;舞臺造型也可以很豐富,并不局限于常規(guī)的表演舞臺,各種造型都可嘗試,例如上方左四圖就是現(xiàn)代感十足的三維舞臺,總之我們要根據(jù)創(chuàng)意和風(fēng)格塑造相匹配的陳列舞臺。

另外就是燈光運用,可以說這是「舞臺陳列」和其他場景的最大區(qū)別,但燈光也不是越多越好,太多反而顯得眼花繚亂,其實能渲染出絢爛氣氛即可。而有光就有影,在燈光照射下,產(chǎn)品一定要有準(zhǔn)確的光影呼應(yīng),這樣才不會顯得突兀。例如上方案例中,仔細(xì)觀察燈光下的產(chǎn)品呈現(xiàn),能看到產(chǎn)品表面都產(chǎn)生了被燈光照射后的色彩、明暗等變化,這些細(xì)節(jié)刻畫才讓畫面更真實,融合更自然。

3. 實戰(zhàn)案例

本次案例會用膠原蛋白口服液作主體元素,然后用這4類場景(盒子、臺面、自然、舞臺)來設(shè)計4張不同視角、不同風(fēng)格的Banner,讓大家看看在不同場景下,如何將產(chǎn)品融入其中。先展示案例會用到的3種產(chǎn)品視角, 下方案例會根據(jù)不同的場景視角選擇對應(yīng)素材。

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

如何做好Banner設(shè)計?先掌握這個高手都會的空間陳列法!

總結(jié)

「空間陳列法」的內(nèi)容量挺大,主要分成了「焦點透視」和「陳列場景」2大部分來介紹,其中焦點透視是立體「空間」的塑造基礎(chǔ);而陳列場景則是產(chǎn)品「陳列」的具體環(huán)境。

常用場景有4類:盒子陳列、臺面陳列、自然陳列和舞臺陳列,每種陳列都有各自適用的主題和氛圍:「盒子」常用于溫馨的送禮主題;「臺面」則能根據(jù)不同主題靈活應(yīng)變,屬于百搭場景;而「自然」則適合季節(jié)或者溯源主題,體現(xiàn)天然清新感;最后的「舞臺」則用于氛圍濃烈的大促主題。不管哪種場景,都要確保產(chǎn)品和空間的視角、透視相一致,這樣場景才會真實協(xié)調(diào)。另外多產(chǎn)品陳列時,還要注意它們之間的大小比例以及擺放結(jié)構(gòu),其中三角結(jié)構(gòu)最常用??傊谑謾C(jī)時代,「空間陳列」是一種真正適合小屏豎構(gòu)圖的表現(xiàn)方式。

文章來源:優(yōu)設(shè)    作者:賢輩

藍(lán)藍(lán)設(shè)計www.bouu.cn )是一家專注而深入的界面設(shè)計公司,為期望卓越的國內(nèi)外企業(yè)提供卓越的UI界面設(shè)計、BS界面設(shè)計 、 cs界面設(shè)計 、 ipad界面設(shè)計 、 包裝設(shè)計 、 圖標(biāo)定制 、 用戶體驗 、交互設(shè)計、 網(wǎng)站建設(shè) 、平面設(shè)計服務(wù)

手機(jī)appUI界面設(shè)計賞析(四)

前端達(dá)人

與傳統(tǒng)PC桌面不同,手機(jī)屏幕的尺寸更加小巧操作,方式也已觸控為主,APP界面設(shè)計不但要保證APP功能的完整性和合理性,又要保證APP的功能性和實用性,在保證其擁有流暢的操作感受的同時,滿足人們的審美需求。

接下來為大家介紹幾款手機(jī)appui界面設(shè)計

微信圖片_20200721175459.jpg

微信圖片_20200721175502.jpg

微信圖片_20200721175510.jpg

微信圖片_20200721175514.jpg

微信圖片_20200721175540.jpg

微信圖片_20200721175544.jpg

微信圖片_20200721175548.jpg

微信圖片_20200721175624.png

微信圖片_20200721175631.jpg

微信圖片_20200721175635.jpg

微信圖片_20200721175639.jpg

微信圖片_20200727222354.png

微信圖片_20200727222406.jpg

微信圖片_20200727222412.png

微信圖片_20200727222421.jpg

微信圖片_20200727222431.jpg


   --手機(jī)appUI設(shè)計--


(以上圖片均來源于網(wǎng)絡(luò))



  藍(lán)藍(lán)設(shè)計www.bouu.cn )是一家專注而深入的界面設(shè)計公司,為期望卓越的國內(nèi)外企業(yè)提供卓越的UI界面設(shè)計BS界面設(shè)計 、 cs界面設(shè)計 、 ipad界面設(shè)計 、 包裝設(shè)計 、 圖標(biāo)定制 、 用戶體驗 、交互設(shè)計、 網(wǎng)站建設(shè) 平面設(shè)計服務(wù)



  更多精彩文章:

       手機(jī)appUI界面設(shè)計賞析(一)

       手機(jī)appUI界面設(shè)計賞析(二)

       手機(jī)appUI界面設(shè)計賞析(三)



巧用偽元素before和after制作絢麗效果

seo達(dá)人

CSS :before 選擇器

定義和說明

:before 選擇器向選定的元素前插入內(nèi)容。

使用content 屬性來指定要插入的內(nèi)容。


CSS :after 選擇器

定義和說明

:after 選擇器向選定的元素之后插入內(nèi)容。

使用content 屬性來指定要插入的內(nèi)容。


這兩個偽元素會在真正頁面元素之前和之后插入一個額外的元素,從技術(shù)角度上講,它們與下面的HTML標(biāo)記是等效的。


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>距離結(jié)束還有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">距離結(jié)束還有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">距離結(jié)束還有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">距離結(jié)束還有10天</span>

   <span class="title3 ml5">距離結(jié)束還有10天</span>

   <span class="title3 ml5">距離結(jié)束還有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;

}

懸掛標(biāo)簽



<div class="pr mt30" style="background-color: #eee; height: 200px;">

   <div class="title4"><span>企業(yè)熱門動態(tài)</span></div>

   <div class="title5"><span>企業(yè)熱門動態(tài)</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一個負(fù)值,以背景出現(xiàn)。






<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當(dāng)前激活狀態(tài)



   <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.藍(lán)湖文字



<span class="lanhu_text">

    本站由叫我詹躲躲提供技術(shù)支持

</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 主要標(biāo)題



<div class="first-title">服務(wù)項目</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.鼠標(biāo)浮層遮罩浮層



<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: "霧在微風(fēng)的吹動下滾來滾去,像冰峰雪山,似蓬萊仙境,如海市蜃樓,使人覺得飄然欲仙。山河景色在霧的裝點下,變得更加美麗。遠(yuǎn)處的七連山巍峨挺拔,它們仿佛成了神仙住的寶山,令人神往。近處池塘邊時時飄來霧氣,在初升陽光的照耀下,呈現(xiàn)出赤、橙、黃、綠、青、藍(lán)、紫七種色彩。......";

 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.照片堆疊效果

只使用一張圖片來創(chuàng)造出一堆圖片疊摞在一起的效果,能做到嗎?當(dāng)然,關(guān)鍵是要使用偽元素:before和:after來幫助呈現(xiàn)。把這些偽元素的z-index設(shè)置成負(fù)值,讓它們以背景方式起作用。




<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,保險的做法是使用單冒號。

藍(lán)藍(lán)設(shè)計www.bouu.cn )是一家專注而深入的界面設(shè)計公司,為期望卓越的國內(nèi)外企業(yè)提供卓越的UI界面設(shè)計、BS界面設(shè)計 、 cs界面設(shè)計 、 ipad界面設(shè)計 、 包裝設(shè)計 、 圖標(biāo)定制 、 用戶體驗 、交互設(shè)計、 網(wǎng)站建設(shè) 平面設(shè)計服務(wù)

你不知道的 WebSocket

seo達(dá)人

在最后的 阿寶哥有話說 環(huán)節(jié),阿寶哥將介紹 WebSocket 與 HTTP 之間的關(guān)系、WebSocket 與長輪詢有什么區(qū)別、什么是 WebSocket 心跳及 Socket 是什么等內(nèi)容。


下面我們進(jìn)入正題,為了讓大家能夠更好地理解和掌握 WebSocket 技術(shù),我們先來介紹一下什么是 WebSocket。


一、什么是 WebSocket

1.1 WebSocket 誕生背景

早期,很多網(wǎng)站為了實現(xiàn)推送技術(shù),所用的技術(shù)都是輪詢。輪詢是指由瀏覽器每隔一段時間向服務(wù)器發(fā)出 HTTP 請求,然后服務(wù)器返回的數(shù)據(jù)給客戶端。常見的輪詢方式分為輪詢與長輪詢,它們的區(qū)別如下圖所示:




為了更加直觀感受輪詢與長輪詢之間的區(qū)別,我們來看一下具體的代碼:




這種傳統(tǒng)的模式帶來很明顯的缺點,即瀏覽器需要不斷的向服務(wù)器發(fā)出請求,然而 HTTP 請求與響應(yīng)可能會包含較長的頭部,其中真正有效的數(shù)據(jù)可能只是很小的一部分,所以這樣會消耗很多帶寬資源。


比較新的輪詢技術(shù)是 Comet)。這種技術(shù)雖然可以實現(xiàn)雙向通信,但仍然需要反復(fù)發(fā)出請求。而且在 Comet 中普遍采用的 HTTP 長連接也會消耗服務(wù)器資源。


在這種情況下,HTML5 定義了 WebSocket 協(xié)議,能更好的節(jié)省服務(wù)器資源和帶寬,并且能夠更實時地進(jìn)行通訊。Websocket 使用 ws 或 wss 的統(tǒng)一資源標(biāo)志符(URI),其中 wss 表示使用了 TLS 的 Websocket。如:


ws://echo.websocket.org

wss://echo.websocket.org

WebSocket 與 HTTP 和 HTTPS 使用相同的 TCP 端口,可以繞過大多數(shù)防火墻的限制。默認(rèn)情況下,WebSocket 協(xié)議使用 80 端口;若運行在 TLS 之上時,默認(rèn)使用 443 端口。


1.2 WebSocket 簡介

WebSocket 是一種網(wǎng)絡(luò)傳輸協(xié)議,可在單個 TCP 連接上進(jìn)行全雙工通信,位于 OSI 模型的應(yīng)用層。WebSocket 協(xié)議在 2011 年由 IETF 標(biāo)準(zhǔn)化為 RFC 6455,后由 RFC 7936 補(bǔ)充規(guī)范。


WebSocket 使得客戶端和服務(wù)器之間的數(shù)據(jù)交換變得更加簡單,允許服務(wù)端主動向客戶端推送數(shù)據(jù)。在 WebSocket API 中,瀏覽器和服務(wù)器只需要完成一次握手,兩者之間就可以創(chuàng)建持久性的連接,并進(jìn)行雙向數(shù)據(jù)傳輸。


介紹完輪詢和 WebSocket 的相關(guān)內(nèi)容之后,接下來我們來看一下 XHR Polling 與 WebSocket 之間的區(qū)別:




1.3 WebSocket 優(yōu)點

較少的控制開銷。在連接創(chuàng)建后,服務(wù)器和客戶端之間交換數(shù)據(jù)時,用于協(xié)議控制的數(shù)據(jù)包頭部相對較小。

更強(qiáng)的實時性。由于協(xié)議是全雙工的,所以服務(wù)器可以隨時主動給客戶端下發(fā)數(shù)據(jù)。相對于 HTTP 請求需要等待客戶端發(fā)起請求服務(wù)端才能響應(yīng),延遲明顯更少。

保持連接狀態(tài)。與 HTTP 不同的是,WebSocket 需要先創(chuàng)建連接,這就使得其成為一種有狀態(tài)的協(xié)議,之后通信時可以省略部分狀態(tài)信息。

更好的二進(jìn)制支持。WebSocket 定義了二進(jìn)制幀,相對 HTTP,可以更輕松地處理二進(jìn)制內(nèi)容。

可以支持?jǐn)U展。WebSocket 定義了擴(kuò)展,用戶可以擴(kuò)展協(xié)議、實現(xiàn)部分自定義的子協(xié)議。

由于 WebSocket 擁有上述的優(yōu)點,所以它被廣泛地應(yīng)用在即時通信、實時音視頻、在線教育和游戲等領(lǐng)域。對于前端開發(fā)者來說,要想使用 WebSocket 提供的強(qiáng)大能力,就必須先掌握 WebSocket API,下面阿寶哥帶大家一起來認(rèn)識一下 WebSocket API。


二、WebSocket API

在介紹 WebSocket API 之前,我們先來了解一下它的兼容性:




(圖片來源:https://caniuse.com/#search=W...)


從上圖可知,目前主流的 Web 瀏覽器都支持 WebSocket,所以我們可以在大多數(shù)項目中放心地使用它。


在瀏覽器中要使用 WebSocket 提供的能力,我們就必須先創(chuàng)建 WebSocket 對象,該對象提供了用于創(chuàng)建和管理 WebSocket 連接,以及可以通過該連接發(fā)送和接收數(shù)據(jù)的 API。


使用 WebSocket 構(gòu)造函數(shù),我們就能輕易地構(gòu)造一個 WebSocket 對象。接下來我們將從 WebSocket 構(gòu)造函數(shù)、WebSocket 對象的屬性、方法及 WebSocket 相關(guān)的事件四個方面來介紹 WebSocket API,首先我們從 WebSocket 的構(gòu)造函數(shù)入手:


2.1 構(gòu)造函數(shù)

WebSocket 構(gòu)造函數(shù)的語法為:


const myWebSocket = new WebSocket(url [, protocols]);

相關(guān)參數(shù)說明如下:


url:表示連接的 URL,這是 WebSocket 服務(wù)器將響應(yīng)的 URL。

protocols(可選):一個協(xié)議字符串或者一個包含協(xié)議字符串的數(shù)組。這些字符串用于指定子協(xié)議,這樣單個服務(wù)器可以實現(xiàn)多個 WebSocket 子協(xié)議。比如,你可能希望一臺服務(wù)器能夠根據(jù)指定的協(xié)議(protocol)處理不同類型的交互。如果不指定協(xié)議字符串,則假定為空字符串。

當(dāng)嘗試連接的端口被阻止時,會拋出 SECURITY_ERR 異常。


2.2 屬性

WebSocket 對象包含以下屬性:




每個屬性的具體含義如下:


binaryType:使用二進(jìn)制的數(shù)據(jù)類型連接。

bufferedAmount(只讀):未發(fā)送至服務(wù)器的字節(jié)數(shù)。

extensions(只讀):服務(wù)器選擇的擴(kuò)展。

onclose:用于指定連接關(guān)閉后的回調(diào)函數(shù)。

onerror:用于指定連接失敗后的回調(diào)函數(shù)。

onmessage:用于指定當(dāng)從服務(wù)器接受到信息時的回調(diào)函數(shù)。

onopen:用于指定連接成功后的回調(diào)函數(shù)。

protocol(只讀):用于返回服務(wù)器端選中的子協(xié)議的名字。

readyState(只讀):返回當(dāng)前 WebSocket 的連接狀態(tài),共有 4 種狀態(tài):


CONNECTING — 正在連接中,對應(yīng)的值為 0;

OPEN — 已經(jīng)連接并且可以通訊,對應(yīng)的值為 1;

CLOSING — 連接正在關(guān)閉,對應(yīng)的值為 2;

CLOSED — 連接已關(guān)閉或者沒有連接成功,對應(yīng)的值為 3。

url(只讀):返回值為當(dāng)構(gòu)造函數(shù)創(chuàng)建 WebSocket 實例對象時 URL 的絕對路徑。

2.3 方法

close([code[, reason]]):該方法用于關(guān)閉 WebSocket 連接,如果連接已經(jīng)關(guān)閉,則此方法不執(zhí)行任何操作。

send(data):該方法將需要通過 WebSocket 鏈接傳輸至服務(wù)器的數(shù)據(jù)排入隊列,并根據(jù)所需要傳輸?shù)臄?shù)據(jù)的大小來增加 bufferedAmount 的值 。若數(shù)據(jù)無法傳輸(比如數(shù)據(jù)需要緩存而緩沖區(qū)已滿)時,套接字會自行關(guān)閉。

2.4 事件

使用 addEventListener() 或?qū)⒁粋€事件監(jiān)聽器賦值給 WebSocket 對象的 oneventname 屬性,來監(jiān)聽下面的事件。


close:當(dāng)一個 WebSocket 連接被關(guān)閉時觸發(fā),也可以通過 onclose 屬性來設(shè)置。

error:當(dāng)一個 WebSocket 連接因錯誤而關(guān)閉時觸發(fā),也可以通過 onerror 屬性來設(shè)置。

message:當(dāng)通過 WebSocket 收到數(shù)據(jù)時觸發(fā),也可以通過 onmessage 屬性來設(shè)置。

open:當(dāng)一個 WebSocket 連接成功時觸發(fā),也可以通過 onopen 屬性來設(shè)置。

介紹完 WebSocket API,我們來舉一個使用 WebSocket 發(fā)送普通文本的示例。


2.5 發(fā)送普通文本



在以上示例中,我們在頁面上創(chuàng)建了兩個 textarea,分別用于存放 待發(fā)送的數(shù)據(jù) 和 服務(wù)器返回的數(shù)據(jù)。當(dāng)用戶輸入完待發(fā)送的文本之后,點擊 發(fā)送 按鈕時會把輸入的文本發(fā)送到服務(wù)端,而服務(wù)端成功接收到消息之后,會把收到的消息原封不動地回傳到客戶端。


// 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("連接未建立,還不能發(fā)送消息");

   return;

 }

 if (message) socket.send(message);

}

當(dāng)然客戶端接收到服務(wù)端返回的消息之后,會把對應(yīng)的文本內(nèi)容保存到 接收的數(shù)據(jù) 對應(yīng)的 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;

});

為了更加直觀地理解上述的數(shù)據(jù)交互過程,我們使用 Chrome 瀏覽器的開發(fā)者工具來看一下相應(yīng)的過程:




以上示例對應(yīng)的完整代碼如下所示:


<!DOCTYPE html>

<html>

 <head>

   <meta charset="UTF-8" />

   <meta name="viewport" content="width=device-width, initial-scale=1.0" />

   <title>WebSocket 發(fā)送普通文本示例</title>

   <style>

     .block {

       flex: 1;

     }

   </style>

 </head>

 <body>

   <h3>阿寶哥:WebSocket 發(fā)送普通文本示例</h3>

   <div style="display: flex;">

     <div class="block">

       <p>即將發(fā)送的數(shù)據(jù):<button onclick="send()">發(fā)送</button></p>

       <textarea id="sendMessage" rows="5" cols="15"></textarea>

     </div>

     <div class="block">

       <p>接收的數(shù)據(jù):</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");


     // 監(jiān)聽連接成功事件

     socket.addEventListener("open", function (event) {

       console.log("連接成功,可以開始通訊");

     });


     // 監(jiān)聽消息

     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("連接未建立,還不能發(fā)送消息");

         return;

       }

       if (message) socket.send(message);

     }

   </script>

 </body>

</html>

其實 WebSocket 除了支持發(fā)送普通的文本之外,它還支持發(fā)送二進(jìn)制數(shù)據(jù),比如 ArrayBuffer 對象、Blob 對象或者 ArrayBufferView 對象:


const socket = new WebSocket("ws://echo.websocket.org");

socket.onopen = function () {

 // 發(fā)送UTF-8編碼的文本信息

 socket.send("Hello Echo Server!");

 // 發(fā)送UTF-8編碼的JSON數(shù)據(jù)

 socket.send(JSON.stringify({ msg: "我是阿寶哥" }));

 

 // 發(fā)送二進(jìn)制ArrayBuffer

 const buffer = new ArrayBuffer(128);

 socket.send(buffer);

 

 // 發(fā)送二進(jìn)制ArrayBufferView

 const intview = new Uint32Array(buffer);

 socket.send(intview);


 // 發(fā)送二進(jìn)制Blob

 const blob = new Blob([buffer]);

 socket.send(blob);

};

以上代碼成功運行后,通過 Chrome 開發(fā)者工具,我們可以看到對應(yīng)的數(shù)據(jù)交互過程:




下面阿寶哥以發(fā)送 Blob 對象為例,來介紹一下如何發(fā)送二進(jìn)制數(shù)據(jù)。


Blob(Binary Large Object)表示二進(jìn)制類型的大對象。在數(shù)據(jù)庫管理系統(tǒng)中,將二進(jìn)制數(shù)據(jù)存儲為一個單一個體的集合。Blob 通常是影像、聲音或多媒體文件。在 JavaScript 中 Blob 類型的對象表示不可變的類似文件對象的原始數(shù)據(jù)。

對 Blob 感興趣的小伙伴,可以閱讀 “你不知道的 Blob” 這篇文章。


2.6 發(fā)送二進(jìn)制數(shù)據(jù)



在以上示例中,我們在頁面上創(chuàng)建了兩個 textarea,分別用于存放 待發(fā)送的數(shù)據(jù) 和 服務(wù)器返回的數(shù)據(jù)。當(dāng)用戶輸入完待發(fā)送的文本之后,點擊 發(fā)送 按鈕時,我們會先獲取輸入的文本并把文本包裝成 Blob 對象然后發(fā)送到服務(wù)端,而服務(wù)端成功接收到消息之后,會把收到的消息原封不動地回傳到客戶端。


當(dāng)瀏覽器接收到新消息后,如果是文本數(shù)據(jù),會自動將其轉(zhuǎn)換成 DOMString 對象,如果是二進(jìn)制數(shù)據(jù)或 Blob 對象,會直接將其轉(zhuǎn)交給應(yīng)用,由應(yīng)用自身來根據(jù)返回的數(shù)據(jù)類型進(jìn)行相應(yīng)的處理。


數(shù)據(jù)發(fā)送代碼


// 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("連接未建立,還不能發(fā)送消息");

   return;

 }

 const blob = new Blob([message], { type: "text/plain" });

 if (message) socket.send(blob);

 console.log(`未發(fā)送至服務(wù)器的字節(jié)數(shù):${socket.bufferedAmount}`);

}

當(dāng)然客戶端接收到服務(wù)端返回的消息之后,會判斷返回的數(shù)據(jù)類型,如果是 Blob 類型的話,會調(diào)用 Blob 對象的 text() 方法,獲取 Blob 對象中保存的 UTF-8 格式的內(nèi)容,然后把對應(yīng)的文本內(nèi)容保存到 接收的數(shù)據(jù) 對應(yīng)的 textarea 文本框中。


數(shù)據(jù)接收代碼


// 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 瀏覽器的開發(fā)者工具來看一下相應(yīng)的過程:




通過上圖我們可以很明顯地看到,當(dāng)使用發(fā)送 Blob 對象時,Data 欄位的信息顯示的是 Binary Message,而對于發(fā)送普通文本來說,Data 欄位的信息是直接顯示發(fā)送的文本消息。


以上示例對應(yīng)的完整代碼如下所示:


<!DOCTYPE html>

<html>

 <head>

   <meta charset="UTF-8" />

   <meta name="viewport" content="width=device-width, initial-scale=1.0" />

   <title>WebSocket 發(fā)送二進(jìn)制數(shù)據(jù)示例</title>

   <style>

     .block {

       flex: 1;

     }

   </style>

 </head>

 <body>

   <h3>阿寶哥:WebSocket 發(fā)送二進(jìn)制數(shù)據(jù)示例</h3>

   <div style="display: flex;">

     <div class="block">

       <p>待發(fā)送的數(shù)據(jù):<button onclick="send()">發(fā)送</button></p>

       <textarea id="sendMessage" rows="5" cols="15"></textarea>

     </div>

     <div class="block">

       <p>接收的數(shù)據(jù):</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");


     // 監(jiān)聽連接成功事件

     socket.addEventListener("open", function (event) {

       console.log("連接成功,可以開始通訊");

     });


     // 監(jiān)聽消息

     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("連接未建立,還不能發(fā)送消息");

         return;

       }

       const blob = new Blob([message], { type: "text/plain" });

       if (message) socket.send(blob);

       console.log(`未發(fā)送至服務(wù)器的字節(jié)數(shù):${socket.bufferedAmount}`);

     }

   </script>

 </body>

</html>

可能有一些小伙伴了解完 WebSocket API 之后,覺得還不夠過癮。下面阿寶哥將帶大家來實現(xiàn)一個支持發(fā)送普通文本的 WebSocket 服務(wù)器。


三、手寫 WebSocket 服務(wù)器

在介紹如何手寫 WebSocket 服務(wù)器前,我們需要了解一下 WebSocket 連接的生命周期。




從上圖可知,在使用 WebSocket 實現(xiàn)全雙工通信之前,客戶端與服務(wù)器之間需要先進(jìn)行握手(Handshake),在完成握手之后才能開始進(jìn)行數(shù)據(jù)的雙向通信。


握手是在通信電路創(chuàng)建之后,信息傳輸開始之前。握手用于達(dá)成參數(shù),如信息傳輸率,字母表,奇偶校驗,中斷過程,和其他協(xié)議特性。 握手有助于不同結(jié)構(gòu)的系統(tǒng)或設(shè)備在通信信道中連接,而不需要人為設(shè)置參數(shù)。


既然握手是 WebSocket 連接生命周期的第一個環(huán)節(jié),接下來我們就先來分析 WebSocket 的握手協(xié)議。


3.1 握手協(xié)議

WebSocket 協(xié)議屬于應(yīng)用層協(xié)議,它依賴于傳輸層的 TCP 協(xié)議。WebSocket 通過 HTTP/1.1 協(xié)議的 101 狀態(tài)碼進(jìn)行握手。為了創(chuàng)建 WebSocket 連接,需要通過瀏覽器發(fā)出請求,之后服務(wù)器進(jìn)行回應(yīng),這個過程通常稱為 “握手”(Handshaking)。


利用 HTTP 完成握手有幾個好處。首先,讓 WebSocket 與現(xiàn)有 HTTP 基礎(chǔ)設(shè)施兼容:使得 WebSocket 服務(wù)器可以運行在 80 和 443 端口上,這通常是對客戶端唯一開放的端口。其次,讓我們可以重用并擴(kuò)展 HTTP 的 Upgrade 流,為其添加自定義的 WebSocket 首部,以完成協(xié)商。


下面我們以前面已經(jīng)演示過的發(fā)送普通文本的例子為例,來具體分析一下握手過程。


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 必須設(shè)置 Upgrade,表示客戶端希望連接升級。

Upgrade 字段必須設(shè)置 websocket,表示希望升級到 WebSocket 協(xié)議。

Sec-WebSocket-Version 表示支持的 WebSocket 版本。RFC6455 要求使用的版本是 13,之前草案的版本均應(yīng)當(dāng)棄用。

Sec-WebSocket-Key 是隨機(jī)的字符串,服務(wù)器端會用這些數(shù)據(jù)來構(gòu)造出一個 SHA-1 的信息摘要。把 “Sec-WebSocket-Key” 加上一個特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后計算 SHA-1 摘要,之后進(jìn)行 Base64 編碼,將結(jié)果做為 “Sec-WebSocket-Accept” 頭的值,返回給客戶端。如此操作,可以盡量避免普通 HTTP 請求被誤認(rèn)為 WebSocket 協(xié)議。

Sec-WebSocket-Extensions 用于協(xié)商本次連接要使用的 WebSocket 擴(kuò)展:客戶端發(fā)送支持的擴(kuò)展,服務(wù)器通過返回相同的首部確認(rèn)自己支持一個或多個擴(kuò)展。

Origin 字段是可選的,通常用來表示在瀏覽器中發(fā)起此 WebSocket 連接所在的頁面,類似于 Referer。但是,與 Referer 不同的是,Origin 只包含了協(xié)議和主機(jī)名稱。

3.1.2 服務(wù)端響應(yīng)

HTTP/1.1 101 Web Socket Protocol Handshake ①

Connection: Upgrade ②

Upgrade: websocket ③

Sec-WebSocket-Accept: 52Rg3vW4JQ1yWpkvFlsTsiezlqw= ④

備注:已忽略部分 HTTP 響應(yīng)頭

① 101 響應(yīng)碼確認(rèn)升級到 WebSocket 協(xié)議。

② 設(shè)置 Connection 頭的值為 "Upgrade" 來指示這是一個升級請求。HTTP 協(xié)議提供了一種特殊的機(jī)制,這一機(jī)制允許將一個已建立的連接升級成新的、不相容的協(xié)議。

③ Upgrade 頭指定一項或多項協(xié)議名,按優(yōu)先級排序,以逗號分隔。這里表示升級為 WebSocket 協(xié)議。

④ 簽名的鍵值驗證協(xié)議支持。

介紹完 WebSocket 的握手協(xié)議,接下來阿寶哥將使用 Node.js 來開發(fā)我們的 WebSocket 服務(wù)器。


3.2 實現(xiàn)握手功能

要開發(fā)一個 WebSocket 服務(wù)器,首先我們需要先實現(xiàn)握手功能,這里阿寶哥使用 Node.js 內(nèi)置的 http 模塊來創(chuàng)建一個 HTTP 服務(wù)器,具體代碼如下所示:


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);

 // 設(shè)置HTTP響應(yīng)頭

 const responseHeaders = [

   "HTTP/1.1 101 Web Socket Protocol Handshake",

   "Upgrade: WebSocket",

   "Connection: Upgrade",

   `Sec-WebSocket-Accept: ${hash}`,

 ];

 // 返回握手請求的響應(yīng)信息

 socket.write(responseHeaders.join("\r\n") + "\r\n\r\n");

});


server.listen(port, () =>

 console.log(`Server running at http://localhost:${port}`)

);

在以上代碼中,我們首先引入了 http 模塊,然后通過調(diào)用該模塊的 createServer() 方法創(chuàng)建一個 HTTP 服務(wù)器,接著我們監(jiān)聽 upgrade 事件,每次服務(wù)器響應(yīng)升級請求時就會觸發(fā)該事件。由于我們的服務(wù)器只支持升級到 WebSocket 協(xié)議,所以如果客戶端請求升級的協(xié)議非 WebSocket 協(xié)議,我們將會返回 “400 Bad Request”。


當(dāng)服務(wù)器接收到升級為 WebSocket 的握手請求時,會先從請求頭中獲取 “Sec-WebSocket-Key” 的值,然后把該值加上一個特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后計算 SHA-1 摘要,之后進(jìn)行 Base64 編碼,將結(jié)果做為 “Sec-WebSocket-Accept” 頭的值,返回給客戶端。


上述的過程看起來好像有點繁瑣,其實利用 Node.js 內(nèi)置的 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");

}

開發(fā)完握手功能之后,我們可以使用前面的示例來測試一下該功能。待服務(wù)器啟動之后,我們只要對 “發(fā)送普通文本” 示例,做簡單地調(diào)整,即把先前的 URL 地址替換成 ws://localhost:8888,就可以進(jìn)行功能驗證。


感興趣的小伙們可以試試看,以下是阿寶哥本地運行后的結(jié)果:




從上圖可知,我們實現(xiàn)的握手功能已經(jīng)可以正常工作了。那么握手有沒有可能失敗呢?答案是肯定的。比如網(wǎng)絡(luò)問題、服務(wù)器異?;?Sec-WebSocket-Accept 的值不正確。


下面阿寶哥修改一下 “Sec-WebSocket-Accept” 生成規(guī)則,比如修改 MAGIC_KEY 的值,然后重新驗證一下握手功能。此時,瀏覽器的控制臺會輸出以下異常信息:


WebSocket connection to 'ws://localhost:8888/' failed: Error during WebSocket handshake: Incorrect 'Sec-WebSocket-Accept' header value

如果你的 WebSocket 服務(wù)器要支持子協(xié)議的話,你可以參考以下代碼進(jìn)行子協(xié)議的處理,阿寶哥就不繼續(xù)展開介紹了。


// 從請求頭中讀取子協(xié)議

const protocol = req.headers["sec-websocket-protocol"];

// 如果包含子協(xié)議,則解析子協(xié)議

const protocols = !protocol ? [] : protocol.split(",").map((s) => s.trim());


// 簡單起見,我們僅判斷是否含有JSON子協(xié)議

if (protocols.includes("json")) {

 responseHeaders.push(`Sec-WebSocket-Protocol: json`);

}

好的,WebSocket 握手協(xié)議相關(guān)的內(nèi)容基本已經(jīng)介紹完了。下一步我們來介紹開發(fā)消息通信功能需要了解的一些基礎(chǔ)知識。


3.3 消息通信基礎(chǔ)

在 WebSocket 協(xié)議中,數(shù)據(jù)是通過一系列數(shù)據(jù)幀來進(jìn)行傳輸?shù)摹榱吮苊庥捎诰W(wǎng)絡(luò)中介(例如一些攔截代理)或者一些安全問題,客戶端必須在它發(fā)送到服務(wù)器的所有幀中添加掩碼。服務(wù)端收到?jīng)]有添加掩碼的數(shù)據(jù)幀以后,必須立即關(guān)閉連接。


3.3.1 數(shù)據(jù)幀格式

要實現(xiàn)消息通信,我們就必須了解 WebSocket 數(shù)據(jù)幀的格式:


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 ...                |

+---------------------------------------------------------------+

可能有一些小伙伴看到上面的內(nèi)容之后,就開始有點 “懵逼” 了。下面我們來結(jié)合實際的數(shù)據(jù)幀來進(jìn)一步分析一下:




在上圖中,阿寶哥簡單分析了 “發(fā)送普通文本” 示例對應(yīng)的數(shù)據(jù)幀格式。這里我們來進(jìn)一步介紹一下 Payload length,因為在后面開發(fā)數(shù)據(jù)解析功能的時候,需要用到該知識點。


Payload length 表示以字節(jié)為單位的 “有效負(fù)載數(shù)據(jù)” 長度。它有以下幾種情形:


如果值為 0-125,那么就表示負(fù)載數(shù)據(jù)的長度。

如果是 126,那么接下來的 2 個字節(jié)解釋為 16 位的無符號整形作為負(fù)載數(shù)據(jù)的長度。

如果是 127,那么接下來的 8 個字節(jié)解釋為一個 64 位的無符號整形(最高位的 bit 必須為 0)作為負(fù)載數(shù)據(jù)的長度。

多字節(jié)長度量以網(wǎng)絡(luò)字節(jié)順序表示,有效負(fù)載長度是指 “擴(kuò)展數(shù)據(jù)” + “應(yīng)用數(shù)據(jù)” 的長度?!皵U(kuò)展數(shù)據(jù)” 的長度可能為 0,那么有效負(fù)載長度就是 “應(yīng)用數(shù)據(jù)” 的長度。


另外,除非協(xié)商過擴(kuò)展,否則 “擴(kuò)展數(shù)據(jù)” 長度為 0 字節(jié)。在握手協(xié)議中,任何擴(kuò)展都必須指定 “擴(kuò)展數(shù)據(jù)” 的長度,這個長度如何進(jìn)行計算,以及這個擴(kuò)展如何使用。如果存在擴(kuò)展,那么這個 “擴(kuò)展數(shù)據(jù)” 包含在總的有效負(fù)載長度中。


3.3.2 掩碼算法

掩碼字段是一個由客戶端隨機(jī)選擇的 32 位的值。掩碼值必須是不可被預(yù)測的。因此,掩碼必須來自強(qiáng)大的熵源(entropy),并且給定的掩碼不能讓服務(wù)器或者代理能夠很容易的預(yù)測到后續(xù)幀。掩碼的不可預(yù)測性對于預(yù)防惡意應(yīng)用的作者在網(wǎng)上暴露相關(guān)的字節(jié)數(shù)據(jù)至關(guān)重要。


掩碼不影響數(shù)據(jù)荷載的長度,對數(shù)據(jù)進(jìn)行掩碼操作和對數(shù)據(jù)進(jìn)行反掩碼操作所涉及的步驟是相同的。掩碼、反掩碼操作都采用如下算法:


j = i MOD 4

transformed-octet-i = original-octet-i XOR masking-key-octet-j

original-octet-i:為原始數(shù)據(jù)的第 i 字節(jié)。

transformed-octet-i:為轉(zhuǎn)換后的數(shù)據(jù)的第 i 字節(jié)。

masking-key-octet-j:為 mask key 第 j 字節(jié)。

為了讓小伙伴們能夠更好的理解上面掩碼的計算過程,我們來對示例中 “我是阿寶哥” 數(shù)據(jù)進(jìn)行掩碼操作。這里 “我是阿寶哥” 對應(yīng)的 UTF-8 編碼如下所示:


E6 88 91 E6 98 AF E9 98 BF E5 AE 9D E5 93 A5

而對應(yīng)的 Masking-Key 為 0x08f6efb1,根據(jù)上面的算法,我們可以這樣進(jìn)行掩碼運算:


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(' '));

以上代碼成功運行后,控制臺會輸出以下結(jié)果:


ee 7e 7e 57 90 59 6 29 b7 13 41 2c ed 65 4a

上述結(jié)果與 WireShark 中的 Masked payload 對應(yīng)的值是一致的,具體如下圖所示:




在 WebSocket 協(xié)議中,數(shù)據(jù)掩碼的作用是增強(qiáng)協(xié)議的安全性。但數(shù)據(jù)掩碼并不是為了保護(hù)數(shù)據(jù)本身,因為算法本身是公開的,運算也不復(fù)雜。那么為什么還要引入數(shù)據(jù)掩碼呢?引入數(shù)據(jù)掩碼是為了防止早期版本的協(xié)議中存在的代理緩存污染攻擊等問題。


了解完 WebSocket 掩碼算法和數(shù)據(jù)掩碼的作用之后,我們再來介紹一下數(shù)據(jù)分片的概念。


3.3.3 數(shù)據(jù)分片

WebSocket 的每條消息可能被切分成多個數(shù)據(jù)幀。當(dāng) WebSocket 的接收方收到一個數(shù)據(jù)幀時,會根據(jù) FIN 的值來判斷,是否已經(jīng)收到消息的最后一個數(shù)據(jù)幀。


利用 FIN 和 Opcode,我們就可以跨幀發(fā)送消息。操作碼告訴了幀應(yīng)該做什么。如果是 0x1,有效載荷就是文本。如果是 0x2,有效載荷就是二進(jìn)制數(shù)據(jù)。但是,如果是 0x0,則該幀是一個延續(xù)幀。這意味著服務(wù)器應(yīng)該將幀的有效負(fù)載連接到從該客戶機(jī)接收到的最后一個幀。


為了讓大家能夠更好地理解上述的內(nèi)容,我們來看一個來自 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!

在以上示例中,客戶端向服務(wù)器發(fā)送了兩條消息。第一個消息在單個幀中發(fā)送,而第二個消息跨三個幀發(fā)送。


其中第一個消息是一個完整的消息(FIN=1 且 opcode != 0x0),因此服務(wù)器可以根據(jù)需要進(jìn)行處理或響應(yīng)。而第二個消息是文本消息(opcode=0x1)且 FIN=0,表示消息還沒發(fā)送完成,還有后續(xù)的數(shù)據(jù)幀。該消息的所有剩余部分都用延續(xù)幀(opcode=0x0)發(fā)送,消息的最終幀用 FIN=1 標(biāo)記。


好的,簡單介紹了數(shù)據(jù)分片的相關(guān)內(nèi)容。接下來,我們來開始實現(xiàn)消息通信功能。


3.4 實現(xiàn)消息通信功能

阿寶哥把實現(xiàn)消息通信功能,分解為消息解析與消息響應(yīng)兩個子功能,下面我們分別來介紹如何實現(xiàn)這兩個子功能。


3.4.1 消息解析

利用消息通信基礎(chǔ)環(huán)節(jié)中介紹的相關(guān)知識,阿寶哥實現(xiàn)了一個 parseMessage 函數(shù),用來解析客戶端傳過來的 WebSocket 數(shù)據(jù)幀。出于簡單考慮,這里只處理文本幀,具體代碼如下所示:


function parseMessage(buffer) {

 // 第一個字節(jié),包含了FIN位,opcode, 掩碼位

 const firstByte = buffer.readUInt8(0);

 // [FIN, RSV, RSV, RSV, OPCODE, OPCODE, OPCODE, OPCODE];

 // 右移7位取首位,1位,表示是否是最后一幀數(shù)據(jù)

 const isFinalFrame = Boolean((firstByte >>> 7) & 0x01);

 console.log("isFIN: ", isFinalFrame);

 // 取出操作碼,低四位

 /**

  * %x0:表示一個延續(xù)幀。當(dāng) Opcode 為 0 時,表示本次數(shù)據(jù)傳輸采用了數(shù)據(jù)分片,當(dāng)前收到的數(shù)據(jù)幀為其中一個數(shù)據(jù)分片;

  * %x1:表示這是一個文本幀(text frame);

  * %x2:表示這是一個二進(jìn)制幀(binary frame);

  * %x3-7:保留的操作代碼,用于后續(xù)定義的非控制幀;

  * %x8:表示連接斷開;

  * %x9:表示這是一個心跳請求(ping);

  * %xA:表示這是一個心跳響應(yīng)(pong);

  * %xB-F:保留的操作代碼,用于后續(xù)定義的控制幀。

  */

 const opcode = firstByte & 0x0f;

 if (opcode === 0x08) {

   // 連接關(guān)閉

   return;

 }

 if (opcode === 0x02) {

   // 二進(jìn)制幀

   return;

 }

 if (opcode === 0x01) {

   // 目前只處理文本幀

   let offset = 1;

   const secondByte = buffer.readUInt8(offset);

   // MASK: 1位,表示是否使用了掩碼,在發(fā)送給服務(wù)端的數(shù)據(jù)幀里必須使用掩碼,而服務(wù)端返回時不需要掩碼

   const useMask = Boolean((secondByte >>> 7) & 0x01);

   console.log("use MASK: ", useMask);

   const payloadLen = secondByte & 0x7f; // 低7位表示載荷字節(jié)長度

   offset += 1;

   // 四個字節(jié)的掩碼

   let MASK = [];

   // 如果這個值在0-125之間,則后面的4個字節(jié)(32位)就應(yīng)該被直接識別成掩碼;

   if (payloadLen <= 0x7d) {

     // 載荷長度小于125

     MASK = buffer.slice(offset, 4 + offset);

     offset += 4;

     console.log("payload length: ", payloadLen);

   } else if (payloadLen === 0x7e) {

     // 如果這個值是126,則后面兩個字節(jié)(16位)內(nèi)容應(yīng)該,被識別成一個16位的二進(jìn)制數(shù)表示數(shù)據(jù)內(nèi)容大小;

     console.log("payload length: ", buffer.readInt16BE(offset));

     // 長度是126, 則后面兩個字節(jié)作為payload length,32位的掩碼

     MASK = buffer.slice(offset + 2, offset + 2 + 4);

     offset += 6;

   } else {

     // 如果這個值是127,則后面的8個字節(jié)(64位)內(nèi)容應(yīng)該被識別成一個64位的二進(jìn)制數(shù)表示數(shù)據(jù)內(nèi)容大小

     MASK = buffer.slice(offset + 8, offset + 8 + 4);

     offset += 12;

   }

   // 開始讀取后面的payload,與掩碼計算,得到原來的字節(jié)內(nèi)容

   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 "";

}

創(chuàng)建完 parseMessage 函數(shù),我們來更新一下之前創(chuàng)建的 WebSocket 服務(wù)器:


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;

 }

 // 省略已有代碼

});

更新完成之后,我們重新啟動服務(wù)器,然后繼續(xù)使用 “發(fā)送普通文本” 的示例來測試消息解析功能。以下發(fā)送 “我是阿寶哥” 文本消息后,WebSocket 服務(wù)器輸出的信息。


Server running at http://localhost:8888

isFIN:  true

use MASK:  true

payload length:  15

Message from client:我是阿寶哥

通過觀察以上的輸出信息,我們的 WebSocket 服務(wù)器已經(jīng)可以成功解析客戶端發(fā)送包含普通文本的數(shù)據(jù)幀,下一步我們來實現(xiàn)消息響應(yīng)的功能。


3.4.2 消息響應(yīng)

要把數(shù)據(jù)返回給客戶端,我們的 WebSocket 服務(wù)器也得按照 WebSocket 數(shù)據(jù)幀的格式來封裝數(shù)據(jù)。與前面介紹的 parseMessage 函數(shù)一樣,阿寶哥也封裝了一個 constructReply 函數(shù)用來封裝返回的數(shù)據(jù),該函數(shù)的具體代碼如下:


function constructReply(data) {

 const json = JSON.stringify(data);

 const jsonByteLength = Buffer.byteLength(json);

 // 目前只支持小于65535字節(jié)的負(fù)載

 const lengthByteCount = jsonByteLength < 126 ? 0 : 2;

 const payloadLength = lengthByteCount === 0 ? jsonByteLength : 126;

 const buffer = Buffer.alloc(2 + lengthByteCount + jsonByteLength);

 // 設(shè)置數(shù)據(jù)幀首字節(jié),設(shè)置opcode為1,表示文本幀

 buffer.writeUInt8(0b10000001, 0);

 buffer.writeUInt8(payloadLength, 1);

 // 如果payloadLength為126,則后面兩個字節(jié)(16位)內(nèi)容應(yīng)該,被識別成一個16位的二進(jìn)制數(shù)表示數(shù)據(jù)內(nèi)容大小

 let payloadOffset = 2;

 if (lengthByteCount > 0) {

   buffer.writeUInt16BE(jsonByteLength, 2);

   payloadOffset += lengthByteCount;

 }

 // 把JSON數(shù)據(jù)寫入到Buffer緩沖區(qū)中

 buffer.write(json, payloadOffset);

 return buffer;

}

創(chuàng)建完 constructReply 函數(shù),我們再來更新一下之前創(chuàng)建的 WebSocket 服務(wù)器:


server.on("upgrade", function (req, socket) {

 socket.on("data", (buffer) => {

   const message = parseMessage(buffer);

   if (message) {

     console.log("Message from client:" + message);

     // 新增以下

日歷

鏈接

個人資料

存檔