一篇文章講解“模型-模板布局之模板標簽方式”,本篇文章講解“模板-模板布局之動態方法布局”。
使用內置的layout方法可以更靈活的在程序中控制模板輸出的布局功能,尤其適用于局部需要布局或者關閉布局的情況,這種方式也不需要在配置文件中開啟layout_on。
1)基本使用
①在Index控制器中,新建dynamic方法
②index.html模板與layout.html布局模板的內容
index.html模板:
layout.html布局模板:
預覽:
2)使用其他的布局模板
如果當前輸出需要使用不同的布局模板,可以動態的指定布局模板名稱,例如:
①在dynamic方法中指定使用其他的布局模板
②在view/public/下新建lay.html布局模板
預覽:
3)動態關閉當前模板的布局功能
使用layout方法動態關閉當前模板的布局功能(這種用法可以配合第一種布局方式,例如全局配置已經開啟了布局,可以在某個頁面單獨關閉):
①在dynamic方法中動態關閉模板的布局功能
②開啟全局配置方式的模板布局
預覽:
注意:
1. 三種模板布局方式中,第一種和第三種是在程序中配置實現模板布局,第二種方式則是單純通過模板標簽在模板中使用布局。具體選擇什么方式,需要根據項目的實際情況來了。
2. 模板布局默認布局模板位置為“view/layout.html”
3. 默認替換的特定字符串為{__CONTENT__}
關注卓象程序員,定期發布技術文章
下一篇講解“模板-包含文件”
一篇文章講解“模型-原樣輸出+模板注釋”,本篇文章講解“模板-模板布局之全局配置方式”。
ThinkPHP的模板引擎內置了布局模板功能支持,可以方便的實現模板布局以及布局嵌套功能。
有三種布局模板的支持方式:
①全局配置方式
②模板標簽方式
③動態方法布局
這種方式僅需在項目配置文件中添加相關的布局模板配置,就可以簡單實現模板布局功能,比較適用于全站使用相同布局的情況,需要配置開啟layout_on參數(默認不開啟),并且設置布局入口文件名layout_name(默認為layout)。
1)開啟模板布局,在config/template.php文件中開啟模板布局,并設置布局入口文件名
2)新建Index控制器,并新建index方法,渲染index.html模板
預覽:
3)模板布局文件
在不開啟layout_on布局模板之前,會直接渲染application/index/view/index/index.html模板文件,開啟之后,首先會渲染application/index/view/layout.html模板,布局模板的寫法和其他模板的寫法類似,本身也可以支持所有的模板標簽以及包含文件,區別在于有一個特定的輸出替換變量{__CONTENT__},例如,下面是一個典型的layout.html模板的寫法:
讀取layout模板之后,會再解析index/index.html模板文件,并把解析后的內容替換到layout布局模板文件的{__CONTENT__}特定字符串。
①新建layout.html布局模板
②在index/index.html模板中添加要輸出的內容
注意:
1. index.html模板內容解析后會替換到layout.html布局模板的{__CONTENT__}字符串位置,因此index.html模板可以不加html的頭和底,layout.html中有頭和底,加也不會錯。
預覽:
4)修改特定替換字符串{__CONTENT__}的名字
默認在模板布局layout.html模板中{__CONTENT__}為替換字符串,也可以進行自定義。
在config/template.php文件中設置:
設置后重新訪問index控制器的index方法,預覽:
修改layout.html中的特定替換字符串:
預覽:
注意:
1. 一個布局模板同時只能有一個特定替換字符串。
2. 采用這種布局方式的情況下,一旦user/add.html模板文件或者layout.html布局模板文件發生修改,都會導致模板重新編譯。
5)修改布局模板位置
如果需要指定其他位置的布局模板,可以設置layout_name:
在view/public下新建layout.html模板:
預覽:
6)某些頁面不需要使用布局模板
如果某些頁面不需要使用布局模板功能,可以在模板文件開頭加上{__NOLAYOUT__}字符串。
①假設index.html模板不需要使用布局模板功能
在index.html模板開頭加{__NOLAYOUT__}
預覽:
關注卓象程序員,定期發布技術文章
下一篇講解“模板-模板布局之模板標簽方式”
言
當我們拿到一個 PC 端頁面的設計稿的時候,往往會發現頁面的布局并不是隨意的,而是遵循的一定的規律:行與行之間會以某種方式對齊。對于這樣的設計稿,我們可以使用柵格布局來實現。
早在 Bootstrap 一統江湖的時代,柵格布局的概念就已深入人心,整個布局就是一個二維結構,包括列和行, Bootstrap 會把屏幕分成 12 列,還提供了一些非常方便的 CSS 名讓我們來指定每列占的寬度百分比,并且還通過媒體查詢做了不同屏幕尺寸的適應。
element-ui 也實現了類似 Bootstrap 的柵格布局系統,那么基于 Vue 技術棧,它是如何實現的呢?
需求分析
和 Bootstrap 12 分欄不同的是,element-ui 目標是提供的是更細粒度的 24 分欄,迅速簡便地創建布局,寫法大致如下:
<el-row> <el-col>aaa</el-col> <el-col>bbb</el-col> </el-row> <el-row> ... </el-row> 復制代碼
這就是二維布局的雛形,我們會把每個列的內容寫在 <el-col></el-col> 之間,除此之外,我們還需要支持控制每個 <el-col> 所占的寬度自由組合布局;支持分欄之間存在間隔;支持偏移指定的欄數;支持分欄不同的對齊方式等。
了解了 element-ui Layout 布局組件的需求后,我們來分析它的設計和實現。
設計和實現
組件的渲染
回顧前面的例子,從寫法上看,我們需要設計 2 個組件,el-row 和 el-col 組件,分別代表行和列;從 Vue 的語法上看,這倆組件都要支持插槽(因為在自定義組件標簽內部的內容都分發到組件的 slot 中了);從 HTML 的渲染結果上看,我們希望模板會渲染成:
<div class="el-row"> <div class="el-col">aaa</div> <div class="el-col">bbb</div> </div> <div class="el-row"> ... </div> 復制代碼
想達到上述需求,組件的模板可以非常簡單。
el-row 組件模板代碼如下:
<div class="el-row"> <slot></slot> </div> 復制代碼
el-col 組件代碼如下:
<div class="el-col"> <slot></slot> </div> 復制代碼
這個時候,新需求來了,我希望 el-row 和 el-col 組件不僅能渲染成 div,還可以渲染成任意我想指定的標簽。 那么除了我們要支持一個 tag 的 prop 之外,僅用模板是難以實現了。
我們知道 Vue 的模板最終會編譯成 render 函數,Vue 的組件也支持直接手寫 render 函數,那這個需求用 render 函數實現就非常簡單了。
el-row 組件:
render(h) { return h(this.tag, { class: [ 'el-row', ] }, this.$slots.default); } 復制代碼
el-col 組件:
render(h) { return h(this.tag, { class: [ 'el-col', ] }, this.$slots.default); } 復制代碼
其中,tag 是定義在 props 中的,h 是 Vue 內部實現的 $createElement 函數,如果對 render 函數語法還不太懂的同學,建議去看 Vue 的官網文檔 render 函數部分。
了解了組件是如何渲染之后,我們來給 Layout 組件擴展一些 feature 。
分欄布局
Layout 布局的主要目標是支持 24 分欄,即一行能被切成 24 份,那么對于每一個 el-col ,我們想要知道它的占比,只需要指定它在 24 份中分配的份數即可。
于是我們給剛才的示例加上一些配置:
<el-row> <el-col :span="8">aaa</el-col> <el-col :span="16">bbb</el-col> </el-row> <el-row> ... </el-row> 復制代碼
來看第一行,第一列 aaa 占 8 份,第二列 bbb 占 16 份。總共寬度是 24 份,經過簡單的數學公式計算,aaa 占總寬度的 1/3,而 bbb 占總寬度的 2/3,進而推導出每一列指定 span 份就是占總寬度的 span/24。
默認情況下 div 的寬度是 100% 獨占一行的,為了讓多個 el-col 在一行顯示,我們只需要讓每個 el-col 的寬占一定的百分比,即實現了分欄效果。設置不同的寬度百分比只需要設置不同的 CSS 即可實現,比如當某列占 12 份的時候,那么它對應的 CSS 如下:
.el-col-12 { width: 50% } 復制代碼
為了滿足 24 種情況,element-ui 使用了 sass 的控制指令,配合基本的計算公式:
.el-col-0 { display: none; } @for $i from 0 through 24 { .el-col-#{$i} { width: (1 / 24 * $i * 100) * 1%; } } 復制代碼
所以當我們給 el-col 組件傳入了 span 屬性的時候,只需要給對應的節點渲染生成對應的 CSS 即可,于是我們可以擴展 render 函數:
render(h) { let classList = []; classList.push(`el-col-${this.span}`); return h(this.tag, { class: [ 'el-col', classList ] }, this.$slots.default); } 復制代碼
這樣只要指定 span 屬性的列就會添加 el-col-${span} 的樣式,實現了分欄布局的需求。
分欄間隔
對于柵格布局來說,列與列之間有一定間隔空隙是常見的需求,這個需求的作用域是行,所以我們應該給 el-row 組件添加一個 gutter 的配置,如下:
<el-row :gutter="20"> <el-col :span="8">aaa</el-col> <el-col :span="16">bbb</el-col> </el-row> <el-row> ... </el-row> 復制代碼
有了配置,接下來如何實現間隔呢?實際上非常簡單,想象一下,2 個列之間有 20 像素的間隔,如果我們每列各往一邊收縮 10 像素,是不是看上去就有 20 像素了呢。
先看一下 el-col 組件的實現:
computed: { gutter() { let parent = this.$parent; while (parent && parent.$options.componentName !== 'ElRow') { parent = parent.$parent; } return parent ? parent.gutter : 0; } }, render(h) { let classList = []; classList.push(`el-col-${this.span}`); let style = {}; if (this.gutter) { style.paddingLeft = this.gutter / 2 + 'px'; style.paddingRight = style.paddingLeft; } return h(this.tag, { class: [ 'el-col', classList ] }, this.$slots.default); } 復制代碼
這里使用了計算屬性去計算 gutter,其實是比較有趣的,它通過 $parent 往外層查找 el-row,獲取到組件的實例,然后獲取它的 gutter 屬性,這樣就建立了依賴關系,一旦 el-row 組件的 gutter 發生變化,這個計算屬性再次被訪問的時候就會重新計算,獲取到新的 gutter。
其實,想在子組件去獲取祖先節點的組件實例,我更推薦使用 provide/inject 的方式去把祖先節點的實例注入到子組件中,這樣子組件可以非常方便地拿到祖先節點的實例,比如我們在 el-row 組件編寫 provide:
provide() { return { row: this }; } 復制代碼
然后在 el-col 組件注入依賴:
inject: ['row'] 復制代碼
這樣在 el-col 組件中我們就可以通過 this.row 訪問到 el-row 組件實例了。
使用 provide/inject 的好處在于不論組件層次有多深,子孫組件可以方便地訪問祖先組件注入的依賴。當你在編寫組件庫的時候,遇到嵌套組件并且子組件需要訪問父組件實例的時候,避免直接使用 this.$parent,盡量使用 provide/inject,因為一旦你的組件嵌套關系發生變化,this.$parent 可能就不符合預期了,而 provide/inject 卻不受影響(只要祖先和子孫的關系不變)。
在 render 函數中,我們會根據 gutter 計算,給當前列添加了 paddingLeft 和 paddingRight 的樣式,值是 gutter 的一半,這樣就實現了間隔 gutter 的效果。
那么這里能否用 margin 呢,答案是不能,因為設置 margin 會占用外部的空間,導致每列的占用空間變大,會出現折行的情況。
render 過程也是有優化的空間,因為 style 是根據 gutter 計算的,那么我們可以把 style 定義成計算屬性,這樣只要 gutter 不變,那么 style 就可以直接拿計算屬性的緩存,而不用重新計算,對于 classList 部分,我們同樣可以使用計算屬性。組件 render 過程的一個原則就是能用計算屬性就用計算屬性。
再來看一下 el-row 組件的實現:
computed: { style() { const ret = {}; if (this.gutter) { ret.marginLeft = `-${this.gutter / 2}px`; ret.marginRight = ret.marginLeft; } return ret; } }, render(h) { return h(this.tag, { class: [ 'el-row', ], style: this.style }, this.$slots.default); } 復制代碼
由于我們是通過給每列添加左右 padding 的方式來實現列之間的間隔,那么對于第一列和最后一列,左邊和右邊也會多出來 gutter/2 大小的間隔,顯然是不符合預期的,所以我們可以通過設置左右負 margin 的方式填補左右的空白,這樣就完美實現了分欄間隔的效果。
偏移指定的欄數
如圖所示,我們也可以指定某列的偏移,由于作用域是列,我們應該給 el-col 組件添加一個 offset 的配置,如下:
<el-row :gutter="20"> <el-col :offset="8" :span="8">aaa</el-col> <el-col :span="8">bbb</el-col> </el-row> <el-row> ... </el-row> 復制代碼
直觀上我們應該用 margin 來實現偏移,并且 margin 也是支持百分比的,因此實現這個需求就變得簡單了。
我們繼續擴展 el-col 組件:
render(h) { let classList = []; classList.push(`el-col-${this.span}`); classList.push(`el-col-offset-${this.offset}`); let style = {}; if (this.gutter) { style.paddingLeft = this.gutter / 2 + 'px'; style.paddingRight = style.paddingLeft; } return h(this.tag, { class: [ 'el-col', classList ] }, this.$slots.default); } 復制代碼
其中 offset 是定義在 props 中的,我們根據傳入的 offset 生成對應的 CSS 添加到 DOM 中。element-ui 同樣使用了 sass 的控制指令,配合基本的計算公式來實現這些 CSS 的定義:
@for $i from 0 through 24 { .el-col-offset-#{$i} { margin-left: (1 / 24 * $i * 100) * 1%; } } 復制代碼
對于不同偏移的分欄數,會有對應的 margin 百分比,就很好地實現分欄偏移需求。
對齊方式
當一行分欄的總占比和沒有達到 24 的時候,我們是可以利用 flex 布局來對分欄做靈活的對齊。
對于不同的對齊方式 flex 布局提供了 justify-content 屬性,所以對于這個需求,我們可以對 flex 布局做一層封裝即可實現。
由于對齊方式的作用域是行,所以我們應該給 el-row 組件添加 type 和 justify 的配置,如下:
<el-row type="flex" justify="center"> <el-col :span="8">aaa</el-col> <el-col :span="8">bbb</el-col> </el-row> <el-row> ... </el-row> 復制代碼
由于我們是對 flex 布局的封裝,我們只需要根據傳入的這些 props 去生成對應的 CSS,在 CSS 中定義 flex 的布局屬性即可。
我們繼續擴展 el-row 組件:
render(h) { return h(this.tag, { class: [ 'el-row', this.justify !== 'start' ? `is-justify-${this.justify}` : '', { 'el-row--flex': this.type === 'flex' } ], style: this.style }, this.$slots.default); } 復制代碼
其中 type 和 justify 是定義在 props 中的,我們根據它們傳入的值生成對應的 CSS 添加到 DOM 中,接著我們需要定義對應的 CSS 樣式:
@include b(row) { position: relative; box-sizing: border-box; @include utils-clearfix; @include m(flex) { display: flex; &:before, &:after { display: none; } @include when(justify-center) { justify-content: center; } @include when(justify-end) { justify-content: flex-end; } @include when(justify-space-between) { justify-content: space-between; } @include when(justify-space-around) { justify-content: space-around; } } } 復制代碼
element-ui 在編寫 sass 的時候主要遵循的是 BEM 的命名規則,并且編寫了很多自定義 @mixin 來配合樣式名的定義。
這里我們來花點時間來學習一下它們,element-ui 的自定義 @mixin 定義在 pacakages/theme-chalk/src/mixins/ 目錄中,我并不會詳細解釋這里面的關鍵字,如果你對 sass 還不熟悉,我建議在學習這部分內容的時候配合 sass 的官網文檔看。
mixins/config.scss 中定義了一些全局變量:
$namespace: 'el'; $element-separator: '__'; $modifier-separator: '--'; $state-prefix: 'is-'; 復制代碼
mixins/mixins.scss 中定義了 BEM 的自定義 @mixin,先來看一下定義組件樣式的 @mixin b:
@mixin b($block) { $B: $namespace+'-'+$block !global; .#{$B} { @content; } } 復制代碼
這個 @mixin 很好理解,$B 是內部定義的變量,它的值通過 $namespace+'-'+$block 計算得到,注意這里有一個 !global 關鍵字,它表示把這個局部變量變成全局的,意味著你也可以在其它 @mixin 中引用它。
通過 @include 我們就可以去引用這個 @mixin,結合我們的 case 來看:
@include b(row) { // xxx content } 復制代碼
會編譯成:
.el-row { // xxx content } 復制代碼
再來看表示修飾符的 @mixin m:
@mixin m($modifier) { $selector: &; $currentSelector: ""; @each $unit in $modifier { $currentSelector: #{$currentSelector + & + $modifier-separator + $unit + ","}; } @at-root { #{$currentSelector} { @content; } } } 復制代碼
這里是允許傳入的 $modifier 有多個,所以內部用了 @each,& 表示父選擇器,$selector 和 $currentSelector 是內部定義的 2 個局部變量,結合我們的 case 來看:
@mixin b(row) { @include m(flex) { // xxx content } } 復制代碼
會編譯成:
.el-row--flex { // xxx content } 復制代碼
有同學可能會疑問,難道不是:
.el-row { .el-row--flex { // xxx content } } 復制代碼
其實并不是,因為我們在該 @mixin 的內部使用了 @at-root 指令,它會把樣式規則定義在根目錄下,而不是嵌套在其父選擇器下。
最后來看一下表示同級樣式的 @mixin when:
@mixin when($state) { @at-root { &.#{$state-prefix + $state} { @content; } } } 復制代碼
這個 @mixin 也很好理解,結合我們的 case 來看:
@mixin b(row) { @include m(flex) { @include when(justify-center) { justify-content: center; } } } 復制代碼
會編譯成:
.el-row--flex.is-justify-center { justify-content: center; } 復制代碼
關于 BEM 的 @mixin,常用的還有 @mixin e,用于定義組件內部一些子元素樣式的,感興趣的同學可以自行去看。
再回到我們的 el-row 組件的樣式,我們定義了幾種flex 布局的對齊方式,然后通過傳入不同的 justify 來生成對應的樣式,這樣我們就很好地實現了靈活對齊分欄的需求。
響應式布局
element-ui 參照了 Bootstrap 的響應式設計,預設了五個響應尺寸:xs、sm、md、lg 和 xl。
允許我們在不同的屏幕尺寸下,設置不同的分欄配置,由于作用域是列,所以我們應該給 el-col 組件添加 xs xs、sm、md、lg 和 xl 的配置,如下:
<el-row type="flex" justify="center"> <el-col :xs="8" :sm="6" :md="4" :lg="3" :xl="1">aaa</el-col> <el-col :xs="4" :sm="6" :md="8" :lg="9" :xl="11">bbb</el-col> <el-col :xs="4" :sm="6" :md="8" :lg="9" :xl="11">ccc</el-col> <el-col :xs="8" :sm="6" :md="4" :lg="3" :xl="1">ddd</el-col> </el-row> <el-row> ... </el-row> 復制代碼
同理,我們仍然是通過這些傳入的 props 去生成對應的 CSS,在 CSS 中利用媒體查詢去實現響應式。
我們繼續擴展 el-col 組件:
render(h) { let classList = []; classList.push(`el-col-${this.span}`); classList.push(`el-col-offset-${this.offset}`); ['xs', 'sm', 'md', 'lg', 'xl'].forEach(size => { classList.push(`el-col-${size}-${this[size]}`); }); let style = {}; if (this.gutter) { style.paddingLeft = this.gutter / 2 + 'px'; style.paddingRight = style.paddingLeft; } return h(this.tag, { class: [ 'el-col', classList ] }, this.$slots.default); } 復制代碼
其中,xs、sm、md、lg 和 xl 是定義在 props 中的,實際上 element-ui 源碼還允許傳入一個對象,可以配置 span 和 offset,但這部分代碼我就不介紹了,無非就是對對象的解析,添加對應的樣式。
我們來看一下對應的 CSS 樣式,以 xs 為例:
@include res(xs) { .el-col-xs-0 { display: none; } @for $i from 0 through 24 { .el-col-xs-#{$i} { width: (1 / 24 * $i * 100) * 1%; } .el-col-xs-offset-#{$i} { margin-left: (1 / 24 * $i * 100) * 1%; } } } 復制代碼
這里又定義了表示響應式的 @mixin res,我們來看一下它的實現:
@mixin res($key, $map: $--breakpoints) { // 循環斷點Map,如果存在則返回 @if map-has-key($map, $key) { @media only screen and #{inspect(map-get($map, $key))} { @content; } } @else { @warn "Undefeined points: `#{$map}`"; } } 復制代碼
這個 @mixns 主要是查看 $map 中是否有 $key,如果有的話則定義一條媒體查詢規則,如果沒有則拋出警告。
$map 參數的默認值是 $--breakpoints,定義在 pacakges/theme-chalk/src/common/var.scss 中:
$--sm: 768px !default; $--md: 992px !default; $--lg: 1200px !default; $--xl: 1920px !default; $--breakpoints: ( 'xs' : (max-width: $--sm - 1), 'sm' : (min-width: $--sm), 'md' : (min-width: $--md), 'lg' : (min-width: $--lg), 'xl' : (min-width: $--xl) ); 復制代碼
結合我們的 case 來看:
@include res(xs) { .el-col-xs-0 { display: none; } @for $i from 0 through 24 { .el-col-xs-#{$i} { width: (1 / 24 * $i * 100) * 1%; } .el-col-xs-offset-#{$i} { margin-left: (1 / 24 * $i * 100) * 1%; } } } 復制代碼
會編譯成:
@media only screen and (max-width: 767px) { .el-col-xs-0 { display: none; } .el-col-xs-1 { width: 4.16667% } .el-col-xs-offset-1 { margin-left: 4.16667% } // 后面循環的結果太長,就不貼了 } 復制代碼
其它尺寸內部的樣式定義規則也是類似,這樣我們就通過媒體查詢定義了各個屏幕尺寸下的樣式規則了。通過傳入 xs、sm 這些屬性的值不同,從而生成不同樣式,這樣在不同的屏幕尺寸下,可以做到分欄的占寬不同,很好地滿足了響應式需求。
基于斷點的隱藏類
Element 額外提供了一系列類名,用于在某些條件下隱藏元素,這些類名可以添加在任何 DOM 元素或自定義組件上。
我們可以通過引入單獨的 display.css:
import 'element-ui/lib/theme-chalk/display.css'; 復制代碼
它包含的類名及其含義如下:
我們來看一下它的實現,看一下 display.scss:
.hidden { @each $break-point-name, $value in $--breakpoints-spec { &-#{$break-point-name} { @include res($break-point-name, $--breakpoints-spec) { display: none !important; } } } } 復制代碼
實現很簡單,對 $--breakpoints-spec 遍歷,生成對應的 CSS 規則,$--breakpoints-spec 定義在 pacakges/theme-chalk/src/common/var.scss 中:
$--breakpoints-spec: ( 'xs-only' : (max-width: $--sm - 1), 'sm-and-up' : (min-width: $--sm), 'sm-only': "(min-width: #{$--sm}) and (max-width: #{$--md - 1})", 'sm-and-down': (max-width: $--md - 1), 'md-and-up' : (min-width: $--md), 'md-only': "(min-width: #{$--md}) and (max-width: #{$--lg - 1})", 'md-and-down': (max-width: $--lg - 1), 'lg-and-up' : (min-width: $--lg), 'lg-only': "(min-width: #{$--lg}) and (max-width: #{$--xl - 1})", 'lg-and-down': (max-width: $--xl - 1), 'xl-only' : (min-width: $--xl), ); 復制代碼
我們以 xs-only 為例,編譯后生成的 CSS 規則如下:
.hidden-xs-only { @media only screen and (max-width:767px) { display: none !important; } } 復制代碼
本質上還是利用媒體查詢定義了這些 CSS 規則,實現了在某些屏幕尺寸下隱藏的功能。
總結
其實 Layout 布局還支持了其它一些特性,我不一一列舉了,感興趣的同學可以自行去看。Layout 布局組件充分利用了數據驅動的思想,通過數據去生成對應的 CSS,本質上還是通過 CSS 滿足各種靈活的布局。
學習完這篇文章,你應該徹底弄懂 element-ui Layout 布局組件的實現原理,并且對 sass 的 @mixin 以及相關使用到的特性有所了解,對組件實現過程中可以優化的部分,應該有自己的思考。
把不會的東西學會了,那么你就進步了,如果你覺得這類文章有幫助,也歡迎把它推薦給你身邊的小伙伴。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。