在Angular1.5中,增加了一個Component方法,並且定義了組件的若干生命周期hook,在代碼規范中也是推崇組件化開發,但是很遺憾的是,CSS模塊化組件化的問題並沒有得到解決,大部分項目的打包方式還是將所有CSS打包成一個完整的CSS文件,然后插入到html中,這樣做的壞處顯而易見,如果團隊沒有良好的CSS代碼規范,很容易引起CSS的沖突,本文使用CSS Modules來解決Angular1.X中存在的CSS 沖突問題。
為了便於讀者查看並動手操作,我將所有的代碼打包成了一個庫,首先在本地clone這個庫
git clone https://github.com/myzhibie/ng1-css-modules-demo.git
接着安裝所需要的依賴
npm install
上述過程如果成功,就可以運行了
gulp serve
在瀏覽器中查看結果http://localhost:3000
首先查看整個項目目錄
client文件夾表示客戶端代碼,common目錄下是一些公用組件,components目錄下是非公用的業務組件,generator目錄下是生成組件的模板文件,webpack.config.js是項目基礎的webpack配置文件,根據開發環境會執行添加到dev或者production配置當中去。
CSS Modules是什么,怎么用
關於CSS Modules是什么,這里不多說,只闡述如何在項目中使用,好處也是顯而易見的,就是徹底隔離了組件的CSS和全局的CSS,防止沖突。如果需要對CSS Modules有更深的概念上的認識,請查看官網。
配置webpack中的css-loader,啟用css-modules.
在項目根目錄下webpack.config.js中,查看如下代碼
1 module.exports = { 2 devtool: 'sourcemap', 3 entry: {}, 4 module: { 5 loaders: [ 6 { test: /\.js$/, exclude: [/app\/lib/, /node_modules/], loader: 'ng-annotate!babel' }, 7 { test: /\.html$/, loader: 'raw' }, 8 { test: /\.scss$/, loader: 'style!css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!sass' }, 9 { test: /\.css$/, loader: 'style!css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]' } 10 ] 11 }, 12 // resolve:{ 13 // modulesDirectories:[bootstrapPath] 14 // }, 15 plugins: [ 16 // Injects bundles in your index.html instead of wiring all manually. 17 // It also adds hash to all injected assets so we don't have problems 18 // with cache purging during deployment. 19 new HtmlWebpackPlugin({ 20 template: 'client/index.html', 21 inject: 'body', 22 hash: true 23 }), 24 25 // Automatically move all modules defined outside of application directory to vendor bundle. 26 // If you are using more complicated project structure, consider to specify common chunks manually. 27 new webpack.optimize.CommonsChunkPlugin({ 28 name: 'vendor', 29 minChunks: function (module, count) { 30 return module.resource && module.resource.indexOf(path.resolve(__dirname, 'client')) === -1; 31 } 32 }) 33 ] 34 };
注意查看第8和第9行,對於css和scss文件的處理,使用css-loader並啟用了css-modules,importLoaders=1代碼該文件被CSS-loader處理之后還會被Style-loader再次處理一次,localIdentName指定了scope css名字生成的規則,這里是[name]代表組件所在目錄名,[local]表示本來定義的css名稱,[hash:base64:5]代表由css-loader生成的哈希值。這樣定義之后,每次遇到css或者sass文件,css-loader都會按照我們指定的方式將組件的css名稱改名並引用到html中。
添加Scoped css(Sass)
所謂的scoped css,就是指局部css,在我們的項目中可以理解為單個組件自己的css,不與全局css相混淆。在client/app/components目錄下,選定attrs組件,添加attrs.scss,內容如下:
1 //local scope css 定義 2 .attrs{ 3 color:red; 4 } 5 .header{ 6 color:green; 7 }
接着在attrs.controller.js中引入這個sass文件,如下:
1 import styles from "./attrs.scss"; 2 class AttrsController { 3 constructor() { 4 this.name = 'Directives利用attrs通信'; 5 this.styles=styles; 6 } 7 } 8 9 export default AttrsController;
第一行import引入進來我們剛才寫的css,然后將它賦值給該controller的一個屬性叫styles,接着在html中引用我們定義的styles,如下:
1 <navbar></navbar> 2 <div> 3 <h2 class="{{$ctrl.styles.header}}">{{ $ctrl.name }}</h2> 4 <directive-b directive-a></directive-b> 5 </div>
注意第3行,我們直接使用controller的styles對象加上我們定義的類名(header)來使用綁定類名,結果如下:
可以看到css-loader將我們定義的類名改成了我們指定的模式,並在html中引用了該類,可以查看我們引入的styles對象如下:
基本原理就是css-loader按照我們指定的模式修改每個組件的類名,並在我們import的時候將其打包到js中的一個對象,這個對象的key值是我們定義的原始類名,鍵值是修改后的類名。
通過查看原始的HTML代碼,我們可以發現,css-loader最后將改名后的每個組件的css,全部以<style>的方式插入到html的header中,便於頁面的引用,如下
以上就是CSS Modules的基本原理
驗證是否存在CSS沖突
前面我們在attrs組件中添加了一個.header類,值為color:green,接着我們以同樣方式在about組件中添加一個.header類,值為 color: red;結果如下:
attrs組件
about組件
可以看到盡管我們定義的原始類名都是.header,但是被css-loader修改后的類名是不一樣的,所以在每個組件下定義的類即使同名也是不會發生沖突的,這就是scoped css的原理。
global css
我們實現了scoped css,又會問如果我想給全局添加一css怎么辦呢?其實很簡單,global css的實現引入方式和scoped css並沒有區別,只是在類的定義上有區別,如下,在app目錄下定義一個app.scss文件,內容如下:
1 body { 2 font-size: 20px; 3 } 4 5 :global { 6 .index { 7 display:inline-block; 8 margin-top: 20px; 9 } 10 }
注意第五行,使用:global定義的就是一個全局類,在項目任何地方都可以使用,比如我們在about組件中定義了一個directive,就可以在這個directive中使用index類,如下:
1 function aDirective(homeService,$rootScope) { 2 "ngInject"; 3 return { 4 restrict: 'E', 5 template: `name:<input type='text' ng-model='showValue' class='about'> 6 <br><button ng-click="addName()" class="index">addName</button>`, 7 link: (scope, element, attrs) => { 8 scope.addName = () => { 9 if(scope.showValue){ 10 $rootScope.$broadcast('addName',scope.showValue); 11 } 12 } 13 } 14 }; 15 } 16 17 export default aDirective;
注意第六行,就是直接指定類名為index,結果如下:
在css modules中使用sass嵌套定義類名
對於大型項目來講,很少有直接使用css的,通常都是使用一些CSS預處理器,比如說Sass,在Sass中,通常類之間可以嵌套定義,配合CSS Modules,也是能夠靈活使用,比如在about組件中,定義about.scss嵌套類名如下:
1 //嵌套定義,只需要寫類名即可,不需要嵌套調用 2 .sec { 3 .about { 4 display: inline-block; 5 margin-bottom: 10px; 6 } 7 } 8 9 .header { 10 color: red; 11 }
我們定義了一個.sec類,並且在該類下定義了一個嵌套的about類,在html中使用如下:
1 <navbar></navbar> 2 <h2 class="{{$ctrl.styles.header}}">{{ $ctrl.name }}</h2> 3 <section class="{{$ctrl.styles.sec}}"> 4 <event-adirective class="{{$ctrl.styles.about}}"></event-adirective> 5 <event-bdirective></event-bdirective> 6 </section>
注意第4行,我們並沒有在使用about類的時候,在前面加上sec,這說明CSS Modules 在處理styles對象的時候認為所有類的類名都是平級關系,盡管它們在定義的時候是嵌套定義的,但是要讓這些嵌套類起作用,必須按照嵌套的結構來添加類,結果如下:
使用CSS Modules的多個css文件合並功能
由於CSS Modules的主要思想是使用js來處理CSS,那么對於不同的CSS我們可以將其合並成一個styles對象,例如在ctrls組件中,定義兩個sass文件,
a.scss
1 //多個scss文件合並 2 .aCtrls{ 3 color: purple; 4 }
ctrls.scss
.ctrls { button { display: inline-block; margin: 20px; } }
在ctrls.controller.js中進行合並
1 let styles={}; 2 import aStyle from './a.scss'; 3 import bStyle from './ctrls.scss'; 4 Object.assign(styles,aStyle,bStyle); 5 class CtrlsController { 6 constructor() { 7 this.name = 'directive通過Controllers通信'; 8 this.styles=styles; 9 } 10 } 11 12 export default CtrlsController;
在1到4行,通過引入不同的styles文件,並將它們合並為一個對象,然后在ctrls.html中使用
1 <navbar></navbar> 2 <div> 3 <h1 class='{{$ctrl.styles.aCtrls}}'>{{ $ctrl.name }}</h1> 4 <a-ctrl-directive> 5 <div> 6 <div> 7 <b-ctrl-directive class='{{$ctrl.styles.ctrls}}'></b-ctrl-directive> 8 </div> 9 </div> 10 </a-ctrl-directive> 11 12 </div>
和使用一個sass文件定義的在使用上完全沒有任何區別。
以上就是CSS Modules在Angular1.X項目中的使用,我們還可以結合PostCSS讓它在功能上更加強大,進一步使用可以參考這里