Angular提供了强大的样式管理机制,包括组件样式封装、CSS作用域隔离以及多种样式绑定方式。
Angular提供了三种视图封装模式,控制组件样式的应用范围。
默认模式
通过属性选择器实现样式封装
全局模式
样式添加到全局作用域
原生封装
使用浏览器原生Shadow DOM
// 组件中设置封装模式
import { Component, ViewEncapsulation } from '@angular/core';
// 模式1: Emulated (默认)
@Component({
selector: 'app-emulated',
templateUrl: './emulated.component.html',
styleUrls: ['./emulated.component.css'],
encapsulation: ViewEncapsulation.Emulated // 可省略,默认值
})
// 模式2: None (全局样式)
@Component({
selector: 'app-global',
templateUrl: './global.component.html',
styleUrls: ['./global.component.css'],
encapsulation: ViewEncapsulation.None
})
// 模式3: ShadowDom
@Component({
selector: 'app-shadow',
templateUrl: './shadow.component.html',
styleUrls: ['./shadow.component.css'],
encapsulation: ViewEncapsulation.ShadowDom
})
模拟样式封装,通过为组件元素添加唯一属性来限定样式作用域。
// emulated.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-emulated-demo',
template: `
<div class="container">
<h2>Emulated模式示例</h2>
<p class="content">这个样式只在本组件中有效</p>
<app-child></app-child>
</div>
`,
styles: [`
.container {
padding: 20px;
background-color: #f0f8ff;
border-radius: 8px;
}
.content {
color: #2c3e50;
font-size: 16px;
}
h2 {
color: #3498db;
border-bottom: 2px solid #3498db;
padding-bottom: 10px;
}
`]
})
export class EmulatedDemoComponent { }
Emulated模式生成的HTML结构:
<app-emulated-demo _ngcontent-abc="">
<div _ngcontent-abc="" class="container">
<h2 _ngcontent-abc="">Emulated模式示例</h2>
<p _ngcontent-abc="" class="content">这个样式只在本组件中有效</p>
<app-child _ngcontent-abc=""></app-child>
</div>
</app-emulated-demo>
生成的CSS样式:
.container[_ngcontent-abc] { ... }
.content[_ngcontent-abc] { ... }
h2[_ngcontent-abc] { ... }
禁用样式封装,组件样式将影响整个应用。
// none.component.ts
import { Component, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'app-none-demo',
template: `
<div class="global-container">
<h2>None模式示例</h2>
<p>这个样式会影响整个应用</p>
</div>
`,
styles: [`
/* 这些样式会添加到全局,可能影响其他组件 */
.global-container {
padding: 20px;
background-color: #fff3e0;
border: 2px dashed #ff9800;
}
h2 {
color: #ff5722; /* 可能影响其他h2元素 */
}
`],
encapsulation: ViewEncapsulation.None
})
export class NoneDemoComponent { }
使用浏览器原生的Shadow DOM技术实现样式隔离。
// shadow.component.ts
import { Component, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'app-shadow-demo',
template: `
<div class="shadow-container">
<h2>ShadowDom模式示例</h2>
<p>使用原生Shadow DOM隔离样式</p>
<slot></slot> <!-- Shadow DOM插槽 -->
</div>
`,
styles: [`
/* 这些样式被严格隔离在Shadow DOM内 */
.shadow-container {
padding: 20px;
background-color: #fff8e1;
border: 2px solid #ffb300;
}
h2 {
color: #ff8f00;
font-family: 'Arial', sans-serif;
}
p {
color: #5d4037;
}
`],
encapsulation: ViewEncapsulation.ShadowDom
})
export class ShadowDemoComponent { }
// 方式1: 使用styleUrls(推荐)
@Component({
selector: 'app-example',
templateUrl: './example.component.html',
styleUrls: ['./example.component.css', './additional.component.css']
})
// 方式2: 使用styles内联样式
@Component({
selector: 'app-inline',
template: `...`,
styles: [`
.container { padding: 20px; }
h2 { color: blue; }
`]
})
// 方式3: 使用模板内联样式(不推荐)
@Component({
selector: 'app-template',
template: `
<style>
.container { padding: 20px; }
</style>
<div class="container">...</div>
`
})
// 方式4: 使用link标签(仅None模式有效)
@Component({
selector: 'app-link',
template: `
<link rel="stylesheet" href="external.css">
<div>...</div>
`,
encapsulation: ViewEncapsulation.None
})
/* :host - 选择组件宿主元素 */
:host {
display: block;
border: 1px solid #ccc;
padding: 10px;
}
/* :host:hover - 宿主元素悬停状态 */
:host:hover {
background-color: #f5f5f5;
}
/* :host(.active) - 宿主元素有特定类时 */
:host(.active) {
border-color: #3498db;
background-color: #e3f2fd;
}
/* :host-context() - 根据祖先元素应用样式 */
:host-context(.dark-theme) .container {
background-color: #2c3e50;
color: white;
}
/* ::ng-deep(已弃用,但有时需要) */
:host ::ng-deep .child-component {
color: red; /* 穿透组件边界影响子组件 */
}
/* /deep/ 和 >>> 已被弃用,使用::ng-deep替代 */
// button.component.ts
import { Component, Input, HostBinding } from '@angular/core';
@Component({
selector: 'app-button',
template: `
<button class="btn" [disabled]="disabled">
<ng-content></ng-content>
</button>
`,
styles: [`
:host {
display: inline-block;
margin: 4px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
font-family: inherit;
}
:host(.primary) .btn {
background-color: #3498db;
color: white;
}
:host(.primary) .btn:hover:not([disabled]) {
background-color: #2980b9;
}
:host(.secondary) .btn {
background-color: #95a5a6;
color: white;
}
:host(.danger) .btn {
background-color: #e74c3c;
color: white;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
:host-context(.dark-theme) .btn {
background-color: #34495e;
color: #ecf0f1;
}
`]
})
export class ButtonComponent {
@Input() disabled = false;
@HostBinding('class.primary') @Input() primary = false;
@HostBinding('class.secondary') @Input() secondary = false;
@HostBinding('class.danger') @Input() danger = false;
}
[class.active]="isActive"
[style.color]="textColor"
[ngClass]="{'active': isActive}"
[ngStyle]="{'color': textColor}"
// style-bindings.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-style-bindings',
template: `
<div class="demo-container">
<h3>样式绑定示例</h3>
<!-- 1. Class绑定 -->
<div [class.active]="isActive"
[class.error]="hasError"
class="box">
Class绑定示例
</div>
<!-- 2. 多个Class绑定 -->
<div [class]="getClasses()" class="box">
动态Class
</div>
<!-- 3. NgClass -->
<div [ngClass]="classObject" class="box">
NgClass示例
</div>
<!-- 4. Style绑定 -->
<div [style.color]="textColor"
[style.font-size.px]="fontSize"
class="box">
Style绑定示例
</div>
<!-- 5. 多个Style绑定 -->
<div [style]="getStyles()" class="box">
动态Style
</div>
<!-- 6. NgStyle -->
<div [ngStyle]="styleObject" class="box">
NgStyle示例
</div>
<!-- 控制面板 -->
<div class="controls">
<button (click)="toggleActive()">切换Active</button>
<button (click)="toggleError()">切换Error</button>
<button (click)="changeColor()">改变颜色</button>
</div>
</div>
`,
styles: [`
.demo-container {
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
}
.box {
padding: 15px;
margin: 10px 0;
border: 1px solid #dee2e6;
border-radius: 4px;
transition: all 0.3s ease;
}
.active {
background-color: #d4edda !important;
border-color: #28a745 !important;
color: #155724;
}
.error {
background-color: #f8d7da !important;
border-color: #dc3545 !important;
color: #721c24;
}
.warning {
background-color: #fff3cd !important;
border-color: #ffc107 !important;
color: #856404;
}
.controls {
margin-top: 20px;
display: flex;
gap: 10px;
}
button {
padding: 8px 16px;
border: 1px solid #6c757d;
background: white;
border-radius: 4px;
cursor: pointer;
}
`]
})
export class StyleBindingsComponent {
isActive = true;
hasError = false;
textColor = '#3498db';
fontSize = 16;
// NgClass对象
classObject = {
'warning': true,
'active': false,
'custom-class': true
};
// NgStyle对象
styleObject = {
'background-color': '#e8f5e9',
'border': '2px solid #4caf50',
'padding': '10px',
'border-radius': '8px'
};
toggleActive(): void {
this.isActive = !this.isActive;
}
toggleError(): void {
this.hasError = !this.hasError;
}
changeColor(): void {
const colors = ['#3498db', '#e74c3c', '#2ecc71', '#f39c12', '#9b59b6'];
const randomColor = colors[Math.floor(Math.random() * colors.length)];
this.textColor = randomColor;
}
getClasses(): string {
const classes = ['custom-box'];
if (this.isActive) classes.push('active');
if (this.hasError) classes.push('error');
return classes.join(' ');
}
getStyles(): string {
return `color: ${this.textColor}; font-size: ${this.fontSize}px;`;
}
}
// Angular支持多种CSS预处理器
// 1. SCSS/Sass (最常用)
@Component({
selector: 'app-sass',
templateUrl: './sass.component.html',
styleUrls: ['./sass.component.scss']
})
// 2. Less
@Component({
selector: 'app-less',
templateUrl: './less.component.html',
styleUrls: ['./less.component.less']
})
// 3. Stylus
@Component({
selector: 'app-stylus',
templateUrl: './stylus.component.html',
styleUrls: ['./stylus.component.styl']
})
// sass.component.scss
// SCSS特性示例
@mixin button-style($color) {
background-color: $color;
color: white;
padding: 10px 20px;
border-radius: 4px;
&:hover {
background-color: darken($color, 10%);
}
}
.container {
padding: 20px;
.header {
font-size: 24px;
color: #333;
&--small {
font-size: 18px;
}
}
.btn {
@include button-style(#3498db);
&--success {
@include button-style(#2ecc71);
}
&--danger {
@include button-style(#e74c3c);
}
}
}
// 嵌套规则
:host {
display: block;
&.active {
border: 2px solid #3498db;
}
}
/* styles.css - 定义全局CSS变量 */
:root {
/* 主题颜色 */
--primary-color: #3498db;
--secondary-color: #2ecc71;
--danger-color: #e74c3c;
/* 间距系统 */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
/* 字体系统 */
--font-size-sm: 12px;
--font-size-md: 14px;
--font-size-lg: 18px;
/* 边框半径 */
--border-radius: 4px;
--border-radius-lg: 8px;
/* 阴影 */
--shadow-sm: 0 2px 4px rgba(0,0,0,0.1);
--shadow-md: 0 4px 8px rgba(0,0,0,0.1);
--shadow-lg: 0 8px 16px rgba(0,0,0,0.1);
}
/* 暗色主题 */
.dark-theme {
--primary-color: #2980b9;
--background-color: #2c3e50;
--text-color: #ecf0f1;
}
/* 组件中使用CSS变量 */
.button {
background-color: var(--primary-color);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--border-radius);
font-size: var(--font-size-md);
box-shadow: var(--shadow-sm);
&:hover {
box-shadow: var(--shadow-md);
}
}
// theme.service.ts - 主题管理服务
import { Injectable, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';
@Injectable({
providedIn: 'root'
})
export class ThemeService {
private currentTheme = 'light';
constructor(@Inject(DOCUMENT) private document: Document) {}
setTheme(theme: 'light' | 'dark'): void {
this.currentTheme = theme;
// 切换body上的主题类
const body = this.document.body;
body.classList.remove('light-theme', 'dark-theme');
body.classList.add(`${theme}-theme`);
// 保存到localStorage
localStorage.setItem('theme', theme);
}
toggleTheme(): void {
const newTheme = this.currentTheme === 'light' ? 'dark' : 'light';
this.setTheme(newTheme);
}
loadTheme(): void {
const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' || 'light';
this.setTheme(savedTheme);
}
getCurrentTheme(): string {
return this.currentTheme;
}
}
/* BEM (Block Element Modifier) 示例 */
.card { /* Block */ }
.card__header { /* Element */ }
.card__body { /* Element */ }
.card__footer { /* Element */ }
.card--highlighted { /* Modifier */ }
.card--disabled { /* Modifier */ }
.card__button { /* Element */ }
.card__button--primary { /* Modifier */ }
.card__button--large { /* Modifier */ }
/* Angular组件中的BEM */
:host(.card) { /* Block */ }
.header { /* Element: .card__header */ }
.body { /* Element: .card__body */ }
:host(.card--highlighted) { /* Modifier */ }
// 问题:如何样式化子组件内部元素?
// 解决方案1:使用::ng-deep(已弃用,但可用)
:host ::ng-deep .child-element {
color: red;
}
// 解决方案2:使用全局样式(None模式)
@Component({
encapsulation: ViewEncapsulation.None
})
// 解决方案3:通过属性或类传递样式
<app-child [customClass]="'my-class'"></app-child>
// 在子组件中:
<div [class]="customClass">...</div>
// 解决方案4:使用CSS变量
:host {
--child-color: red;
}
/* 子组件中使用 */
.child-element {
color: var(--child-color);
}
// 方式1:在angular.json中全局引入
"styles": [
"src/styles.css",
"node_modules/bootstrap/dist/css/bootstrap.min.css"
]
// 方式2:在组件中引入(仅None模式)
@Component({
encapsulation: ViewEncapsulation.None,
styles: [`
@import '~bootstrap/dist/css/bootstrap.min.css';
`]
})
// 方式3:使用样式加载器
npm install --save-dev style-loader css-loader
// webpack配置中添加loader
// 方式4:复制样式文件到assets
// 将第三方CSS复制到assets/css,然后在index.html中引入
/* 移动优先的响应式设计 */
.container {
width: 100%;
padding: 16px;
/* 平板设备 */
@media (min-width: 768px) {
padding: 24px;
max-width: 720px;
margin: 0 auto;
}
/* 桌面设备 */
@media (min-width: 992px) {
max-width: 960px;
}
/* 大桌面设备 */
@media (min-width: 1200px) {
max-width: 1140px;
}
}
/* 响应式工具类 */
.hidden-mobile {
@media (max-width: 767px) {
display: none !important;
}
}
.visible-mobile {
@media (min-width: 768px) {
display: none !important;
}
}
/* 响应式字体 */
:root {
--font-size-base: 16px;
@media (min-width: 768px) {
--font-size-base: 18px;
}
@media (min-width: 992px) {
--font-size-base: 20px;
}
}
body {
font-size: var(--font-size-base);
}
// style-test.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, ViewChild } from '@angular/core';
import { StyleTestComponent } from './style-test.component';
// 测试容器组件
@Component({
template: `
<app-style-test
[isActive]="isActive"
[textColor]="textColor"
></app-style-test>
`
})
class TestHostComponent {
@ViewChild(StyleTestComponent) childComponent!: StyleTestComponent;
isActive = false;
textColor = '#000000';
}
describe('StyleTestComponent', () => {
let fixture: ComponentFixture<TestHostComponent>;
let hostComponent: TestHostComponent;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [StyleTestComponent, TestHostComponent]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(TestHostComponent);
hostComponent = fixture.componentInstance;
fixture.detectChanges();
});
it('应该应用active类', () => {
hostComponent.isActive = true;
fixture.detectChanges();
const element = fixture.nativeElement.querySelector('.active');
expect(element).toBeTruthy();
});
it('应该正确设置文本颜色', () => {
hostComponent.textColor = '#ff0000';
fixture.detectChanges();
const element = fixture.nativeElement.querySelector('[style]');
expect(element.style.color).toBe('rgb(255, 0, 0)');
});
it('应该生成正确的CSS类名', () => {
const compiled = fixture.nativeElement;
const componentElement = compiled.querySelector('app-style-test');
// 检查是否生成了Angular的属性选择器
expect(componentElement.getAttribute('_ngcontent-c0')).toBeDefined();
});
});