Angular 18+ 高级教程 – 大杂烩
前言
本篇记入一些 Angular 的小东西。
Angular 废弃 API 列表
Docs – Deprecated APIs and features
Using Tailwind CSS with Angular
依照这个教程做可以了:Install Tailwind CSS with Angular
postcss 和 autoprefixer 即便不安装也可以跑,但 tailwindcss 一定要。
原因是 Angular CLI 里面是有安装了 postcss 和 autoprefixer 的

而 tailwindcss 是 under peerDependencies

如果有 tailwind.config.js 但是没用 yarn install tailwindcss,那是会 warning 的

DomSanitizer
DomSanitizer 是 Angular built-in 的消毒器。
DomSanitizer Provider
它是一个 Root Level Provider,用法非常简单。
export class AppComponent {
constructor() {
const domSanitizer = inject(DomSanitizer);
// 1. 里面包含了一些不安全的东西,e.g. script, style, template
const unsafeHtml = `
<h1>Hello World</h1>
<script>alert('abc')</script>
<style>*{}</style>
<template>0</template>
<p>Lorem ipsum dolor sit amet.</p>
`;
// 2. 使用 domSanitizer.sanitize 对 unsafeHtml 消毒
const safeHtml = domSanitizer.sanitize(SecurityContext.HTML, unsafeHtml);
console.log('unsafe', unsafeHtml);
console.log('safe', safeHtml);
}
}
效果

sanitize 会把不安全的东西消除掉,比如 <script>, <style>, <template> 等等。
不只是 HTML 可以消毒,还有其它的:
Style (v10.0 之后就废弃了,现在已经不消毒 style 了)

我没有考古出相关信息。
Script
domSanitizer.sanitize(SecurityContext.SCRIPT, unsafeScript)
尝试消毒 unsafe script 会直接报错。

URL & Resource URL
首先要懂得区分 URL 和 Resource URL

比如 <img src> 就是 URL,<iframe src> 则是 Resource URL。

URL 可以消毒,Resource URL 不消毒直接会报错。
DomSanitizer used by Angular Internal
App HTML
<div [innerHTML]="unsafeHtml"></div>
App 组件
export class AppComponent {
unsafeHtml = `
<h1>Hello World</h1>
<script>alert('abc')</script>
<style>*{}</style>
<template>0</template>
<p>Lorem ipsum dolor sit amet.</p>
`;
}
run compilation
yarn run ngc -p tsconfig.json
App Definition

在做 binding [innerHtml] 时,Angular 会使用 ɵɵsanitizeHtml 函数。
ɵɵsanitizeHtml 函数的源码在 sanitization.ts。

除了 HTML 还有其它的也会使用消毒,我就不一一列出来了。
提醒:如果我们自己使用 Renderer2.setProperty(el, 'innerHTML', 'raw html'),它内部不会自动替我们消毒哦,我们需要先消毒了 'raw html' 才传进去。
bypassSecurityTrust
上面有提到 sanitize 不一定会消毒成功变成 safe value,有时候它会直接报错 (也没有检查哦),提醒我们不安全而已。
所以即便我们给的 string 是安全的也没用,它依然会直接报错。这时就需要 bypass。
export class AppComponent {
constructor() {
const domSanitizer = inject(DomSanitizer);
const unsafeScript = '';
const safeScript = domSanitizer.sanitize(SecurityContext.SCRIPT, unsafeScript);
console.log(safeScript);
}
}
直接报错,即使 unsafeScript 只是 empty string。

这种情况下就需要用 bypassSecurityTrust 方法。
export class AppComponent {
constructor() {
const domSanitizer = inject(DomSanitizer);
const unsafeScript = '';
const bypassScript = domSanitizer.bypassSecurityTrustScript(unsafeScript);
console.log('bypassScript', bypassScript);
const safeScript = domSanitizer.sanitize(SecurityContext.SCRIPT, bypassScript);
console.log('safeScript', safeScript);
}
}
效果

HTML,Style,Script,URL,Resource URL 都有对应的 bypass 方法

提醒:bypass 就是 skip 掉消毒,它不是 white list 的概念哦,没用一半一半的。相关 Github Issue – Extensible Sanitizer。
Renderer2 和 inject(DOCUMENT)
Angular 项目通常是运行在游览器上的,但如果项目有需要做 server-side rendering (SSR),那 Angular 会运行在服务端环境 (比如 Node.js)。
这两个环境有两大特点:
服务端没有 BOM 和 DOM
比如 document, window 这些在游览器环境才存在
服务端只负责渲染,没有 event listener
游览器才能交互,才能有事件监听
假如我们的 Angular 项目要支持两个环境,首先在渲染阶段,我们要刻意避开使用任何游览器独有的特性,比如 document 和 window。
什么叫渲染阶段呢?基本上除了 event handle 以外,Constructor, PreOrderHooks, ContentHooks, ViewHooks 都属于渲染阶段。
所以在 constructor, OnInit, AfterContentInit, AfterViewInit 这些方法里面,我们都不可以使用 document, window 这些。
AfterRenderHooks 则不同,它在 server-side rendering 时是不被调用的,所以 afterNextRender, afterRender 函数里可以使用 document, window 这些。
Add class to body through Renderer2 and DOCUMENT
绝大部分的 DOM 操作,我们都可以通过 Angular MVVM way 去解决,比如 Template Binding Syntax。
但 Angular 的范围始终局限在 <app-root /> 里面,如果我们想给 body element 添加一个 class 该怎么做到呢?(要支持 server-side rendering)
解决方法是使用 Renderer2 和 DOCUMENT 代理
export class AppComponent {
constructor() {
const renderer = inject(Renderer2);
const document = inject(DOCUMENT);
renderer.addClass(document.body, 'dark-theme');
}
}
在游览器环境下,inject(DOCUMENT) 拿到的是游览器的 document 对象。

在服务端环境下,inject(DOCUMENT) 拿到的是由服务端创建出来的 document 对象。

parseDocument 和 createHtmlDocument 是创建 Document 对象的方法。它底层是用 domino (一个基于 Mozilla's dom.js 的库) 来实现的。
我没有研究过服务端 document 和游览器 document 具体有没有区别,但我感觉它们肯定是不一样的,虽然它们 interface 一样,这部分等以后我研究了 Angular Server-side rendering 再补上呗。
那我们可以不可以直接 document.body.classList.add('dark-theme') 添加 class?
我觉得是可以的,但是更安全的做法是使用 Renderer2。
Renderer2 是 Angular 封装的渲染 Service,但凡我们需要 manipulation DOM,不管是 for render 还是 for add event listener 都尽量使用 Renderer2 就对了。
我们看个例子,体会一下使用 Renderer2 和直接操作 DOM 的区别
renderer.listen(this.inputElementRef().nativeElement, 'keydown.enter', () => console.log('enter'));
看到吗,它可以监听 keydown.enter 事件,这个是 Angular 扩展的,如果我们用原生 element.addEventListener 就做不到这个。
提醒:不管是用原生 element.addEventListener 或者 renderer.listen,我们都需要自己 remove event listener。只有用 Template Binding Syntax 监听的事件才会在组件销毁时自动 remove event listener。
题外话:Angular Material 在 2017 年 Angular 使用 domino 之后就不再使用 Renderer2 了,他们直接 DOM manupulation (相关 issue)。但最近 (2024 年 6 月) 有一个 RFC 正在讨论 optional domino...,显然直接 DOM Manipulation 还是比较不顺风水的。
No provider for _Renderer2
inject(Renderer2) 必须在组件内才有效,在 Service 会报错。
相关源码在 api.ts

原因是 Service 通常是 Root Provider,inject 时使用的是 Root Injector,而 Renderer2 必须使用 NodeInjector 才能 inject 到。
因为 Renderer2 是从当前 LView 里取出来的,它依赖当前 LView。
每当创建组件 LView 时都会创建 renderer


在游览器环境,rendererFactory 是 DomRendererFactory2

getOrCreateRenderer 方法

它会依据组件 ViewEncapsulation 创建出不同的 Renderer2。
组件默认是 ViewEncapsulation.Emulated,它对应的是 EmulatedEncapsulationDomRenderer2。这个 renderer 继承自 NoneEncapsulationDomRenderer

NoneEncapsulationDomRenderer 又继承自 DefaultDomRenderer2

可以看到主要是对 styles 做了一些 override 而已,绝大部分功能是 DefaultDomRenderer2 实现的。
如果我们想在 Service 里使用 Renderer2 做一些简单的 DOM 操作 (e.g. createElement),那我们可以 inject Renderer2
@Injectable({
providedIn: 'root',
})
export class TestService {
constructor() {
const rendererFactory = inject(RendererFactory2);
const renderer = rendererFactory.createRenderer(null, null);
const div = renderer.createElement('div');
}
}
参数传入 2 个 null,它会返回 DefaultDomRenderer2

总结
不需要支持 Server-side rendering 的话,我们可以随意使用 DOM 和 BOM。
要支持 Server-side rendering 的话,在渲染阶段,我们要避开 DOM 和 BOM。
inject(DOCUMENT) 可以让我们在 Server-side rendering 时使用 document 对象,这个 document 是用 domino 生成的,通常用它来 query element (比如 document.body)。
Renderer2 不仅仅可以用于 Server-side rendering,它也适用于游览器环境,inject(DOCUMENT) 负责 "read",那 Renderer2 就是负责 "write"。
它可以 addClass, setAttribute, appendChild, listen 等等
Dynamic Add Event Listener (renderer.listen)

要实现上面这个交互体验,我们需要监听 mouse down 事件,然后监听 document mouse move 事件和 document mouse up 事件。
Angular template binding syntax 没办法实现动态添加 event listener。
所以我们只能直接操作 DOM 了。
首先有一个 button,然后 query 它出来
<button #button>click me</button>
export class AppComponent {
button = viewChild.required<string, ElementRef<HTMLElement>>('button', { read: ElementRef });
}
然后 add event listener
export class AppComponent {
button = viewChild.required<string, ElementRef<HTMLElement>>('button', { read: ElementRef });
constructor() {
// after next render 需要用到 injector,所以先提取出来
const injector = inject(Injector);
// DOM 操作通常放到 after next render,一来是因为 SSR 不需要执行
// 二来是因为此时 query button 才准备好
afterNextRender(() => {
// 使用 renderer 做事件监听会比较好,因为可以使用 Angular 对事件的扩展功能,比如 (keydown.enter) 语法
// 当然你嫌麻烦要用原生的也完全可以
const renderer = injector.get(Renderer2);
// 虽然 button 是 signal 但其实它不可能变更了,所以这里也不需要顾虑这个,直接用就可以了
const buttonElement = this.button().nativeElement;
const mouseDown$ = fromRendererEvent(renderer, buttonElement, 'mousedown');
const mouseUp$ = fromRendererEvent(renderer, document, 'mouseup');
const mouseMove$ = mouseDown$.pipe(
switchMap(() => fromRendererEvent(renderer, document, 'mousemove').pipe(takeUntil(mouseUp$))),
);
const destroyRef = injector.get(DestroyRef);
// 要记得在组件销毁时 remove event listener 哦
mouseMove$.pipe(takeUntilDestroyed(destroyRef)).subscribe(() => console.log('moving'));
});
}
}
// convert Angular renderer.listen to RxJS Observable
export function fromRendererEvent<T>(renderer: Renderer2, target: unknown, eventName: string): Observable<T> {
return fromEventPattern(
handler => renderer.listen(target, eventName, handler),
(_handler, removeEventListenerFn) => removeEventListenerFn(),
);
}
效果

PLATFORM_ID, isPlatformBrowser, isPlatformServer
Angular 支持 SSR (Server-side rendering),也就是说我们的程序有可能会运行在 server-side (e.g. Node.js),不一定是 browser。
如果运行在 server-side,那就不能调用 browser 的 BOM。所以,我们的程序需要有能力区分这两种环境,并做出相应的处理。
PLATFORM_ID token, isPlatformBrowser 函数, isPlatformServer 函数,这些变是 Angular 提供给我们用于区分当前执行环境的。
export class AppComponent {
constructor() {
const platformId = inject(PLATFORM_ID); // 'browser'
const isRunningOnBrowser = isPlatformBrowser(platformId); // true
const isRunningOnServer = isPlatformServer(platformId); // false
}
}
用法非常简单,注入 PLATFORM_ID token,它会拿到一个 string,然后把这个 value 传给 isPlatformBrowser 函数或者 isPlatformServer 函数,它们会返回 boolean。
forwardRef
forwardRef 是一个函数,它是用来解决循环引用问题的。
问题说明
我们有两个组件 -- Parent 和 Child
Parent 组件 JS import 了 Child 组件,因为 Template 要输出 <app-child /> 需要再 @Component imports
import { Component } from '@angular/core';
import { ChildComponent } from "./child.component";
@Component({
selector: 'app-parent',
standalone: true,
template: `
<p>parent works!</p>
<app-child />
`,
imports: [ChildComponent]
})
export class ParentComponent {}
同时 Child 组件也 JS imports 了 Parent 组件,因为要 inject(ParentComponent)
import { Component, inject } from '@angular/core';
import { ParentComponent } from './parent.component';
@Component({
selector: 'app-child',
standalone: true,
imports: [],
template: `
<p>
child works!
</p>
`,
})
export class ChildComponent {
constructor() {
console.log(inject(ParentComponent, { optional: true }))
}
}
站 JS import 的角度,这两个 module 已经循环引用了,但不要紧,因为循环引用不代表会坏掉,要看最终获取依赖的时机,只要在获取之前它被赋值了就可以。
好,我们继续。
App 组件输出这两个组件
import { Component } from '@angular/core';
import { ParentComponent } from './parent.component';
import { ChildComponent } from './child.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [ParentComponent, ChildComponent],
template: `
<app-parent />
<app-child />
`,
})
export class AppComponent {}
效果

正常输出
现在我们动一点手脚,把 App 组件 import Parent Child 的顺序替换一下。
// before
// import { ParentComponent } from './parent.component';
// import { ChildComponent } from './child.component'; // after
import { ChildComponent } from './child.component';
import { ParentComponent } from './parent.component';
效果

直接报错ERROR TypeError: Cannot read properties of undefined (reading 'ɵcmp')
原理说明
为什么会这样呢?其实原理很简单,我们来看看它的代码。
yarn run ngc -p tsconfig.json
app.component.js import 了 child.component.js

child.component.js 又马上 import 了 parent.component.js

parent.component.js

问题出现在这里,由于是循环引用,ChildComponent 一开始会是 undefined,而 ParentComponent static 是立刻执行的,遇上 undefined 被放进去 dependencies array 里了。
这就导致了 Cannot read properties of undefined (reading 'ɵcmp'),这个 undefined 指的就是 ChildComponent。
那为什么 import 顺序调换它又没有问题呢?

因为它们使用依赖的时机不同,Parent 使用 Child 是在 class static 也就是立刻执行,而 Child 使用 Parent 是在 constructor 里,它不是立刻执行的,是等到 new Child 时才执行,所以就不会有问题。
破解之法
循环引用的破解之法就是压后执行时机,而 forwardRef 就是干这件事的。
我们在 Parent 组件使用 Child 的地方 wrap 上一层 forwardRef
@Component({
selector: 'app-parent',
standalone: true,
template: `
<p>parent works!</p>
<app-child />
`,
imports: [forwardRef(() => ChildComponent)] // 加上 forwardRef
})
export class ParentComponent {}
compile 之后

原本是直接把 ChildComponent (undefined) 放进 dependencies array,加了 forwardRef 后,dependencies 变成了一个函数,调用函数后才会把 ChildComponent 放进 array,这就是所谓的压后执行。
当 Angular 执行这个函数时,ChildComponent 已经不再是 undefined 了,也就不会再报错了。
forwardRef 不仅仅可以用在 imports,很多地方都可以用,比如 inject(forwardRef(() => ParentComponent)) 等等,
但凡遇到循环引用,试试 forwardRef 就对了。
Environment Configuration
参考:
Docs – Configuring application environments
YouTube – Angular Environmental Variables and Configuration
项目开发通常会分几个阶段 (比如 development,staging / UAT,production 等等)。
不同阶段会部署在不同环境,不同环境又需要不同配置,于是就有了 Environment Configuration 概念。
Create environment files
首先创建 environment folder by Angular CLI
ng generate environments
它会创建 1 个 environments folder 和 2 个 environment files。

environment.ts 代表 production 环境配置,environment.development.ts 代表 development 环境配置。
里面是一个空对象

我们可以添加一些常用的配置,比如服务器地址,
这是 development 环境的配置,服务器地址是 localhost 本地

这是 production 环境的配置,服务器地址是一个域名

Use environment config in App
在 App 直接 import environment.ts 就可以了。

上面 import 的是 enviroment.ts,按理说它应该指的是 production 配置,但其实它是动态的,它会依据 Angular build 时选择的环境而决定是使用 development 配置还是 production 配置。
其关键在 angular.json

这段表示在 build developement 的时候,CLI 会做一个 file replacement 的动作,把 environment.ts 换成 environment.development.ts,
所以即便我们 import 的是 environment.ts 但最终拿到的却是 environment.development.ts 的内容。
Add staging environment
我们尝试添加多一个 staging 环境配置。
创建 environment.staging.ts

然后添加 staging configuration 到 angular.json


"staging": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all",
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.staging.ts"
}
]
},
接着 build staging
ng build --configuration=staging
效果

在最终的 main.js 里,可以看到 host 使用的是 staging 的配置。
Use specify environment configuration
我们也可以直接 import 指定的环境配置。

import environment.ts 是动态的,它会依据 build config 得到不同环境配置。
import environment.development / staging 则不会,它就是拿指定的环境配置。
如果我们想搞多一个 environment.production.ts 拿指定的 production 配置也可以。
总之 Angular 仅仅只是做了一个 file replacement 而已,并没有什么黑魔法。
Get environment configuration from server
Angular 这套 Environment 机制发生在编译期,跟 server 扯不上关系。
所以,如果想 ajax get configuration from server,那我们需要完全自己实现。
isDevMode
如果我们只是想单纯的判断是不是在 development (非 production) 环境下,不一定要搞 environment.ts,Angular 提供了一个快捷的方法 -- isDevMode 函数。
import { ChangeDetectionStrategy, Component, isDevMode } from '@angular/core';
export class AppComponent {
constructor() {
if (isDevMode()) {
console.log('keatkeat123'); // only run in development environment
}
}
}
这个 isDevMode 是 Angular 官方推荐使用的,但是 Angular Material 却没有 follow。
原因是它不够智能,在 production 环境下,上面这段 code 是多余的,理应被自动移除,
但是 Angular CLI 并没有把它移除,相关 Github Issue – Provide the ability to remove code needed only in dev mode from the production bundle
那 Angular Material 是怎么做的呢?
首先是 declare 一个 global type ngDevMode variable

dev-mode-types.d.ts
declare const ngDevMode: object | null;
放到 src 里面
注:如果是 Library 需要配置 tsconfig.lib.json

然后像这样使用它

typeof ngDevMode === 'undefined' || ngDevMode
这个判断方式其实和 isDevMode 函数内部大同小异。

经过测试,使用 ngDevMode,Angular CLI 会在非 development 环境下删除其下无用的代码,
而使用 Angular 的 isDevMode 函数的话,Angular CLI 不会删除其下代码。
所以,目前 follow Angular Material 才是正确的,用 ngDevMode 不要用 isDevMode。
另外:Angular CLI 是依靠 "ngDevMode" 来识别的,如果我们给它 wrap 一个函数,那 CLI 就识别不出来了。
ngDevMode 源码逛一逛
ngDevMode 主要源码在 ng_dev_mode.ts

它的类型是 null 或者 NgDevModePerfCounters 对象。
NgDevModePerfCounters 是 Angular 用于存 debug 资料的对象。
虽然类型没有声明它可能是 undefined,但是它并没有初始值,所以 runtime 时是有可能出现 undefined 情况的。
那它具体什么时候被赋值呢?
非常非常早,比如在 ɵɵdefineComponent 函数的第一句。


赋值的逻辑是这样

判断 locationString (这个 location 指的是 WorkerGlobalScope),如果是 ngDevMode=false 那就表示 "不是 development 环境",那就不可以赋值对象,要赋值 false。
相反就赋值 NgDevModePerfCounters 对象。
这个 WorkerGlobalScope 我不熟,但是我猜它和 CLI 可能有点关系。
下图是 Angular CLI 的一段源码

当开启 optimization 时,ngDevMode 会被设置成 false,其它情况下 CLI 不会去设置 ngDevMode。
综上,我们得出几个结论:
ngDevMode 初始时是 undefined
在非常早的时候 (比如 ɵɵdefineComponent),它就会被赋值了。
如果 CLI 没有给 ngDevMode false 那么就表示是 development 环境,赋值 NgDevModePerfCounters 对象。
如果 CLI 有给 ngDevMode false,那么就表示不是 development 环境,赋值 false。
对于我们使用者来说,绝大部分情况下 if (ngDevMode) 就 ok 了,不需要
typeof ngDevMode === 'undefined' || !!ngDevMode
第一,我们不太可能在它还没有赋值就需要判断,非常罕见。
第二,即便我们真的需要判断也没辙,因为 undefined 并不代表是 development 环境,它只是还不知道而已。
第三,Angular 也没有公开 initNgDevMode 函数,所以我们想提早给它赋值也不行。
所以 Angular Material 的逻辑是,当 ngDevMode 未赋值,把它当作是 development 环境来处理 (这个规则不一定适用于每个情况)
至于 !!ngDevMode 只是把 NgDevModePerfCounters 对象强转成 boolean,这只是代码风格而已,不是必须的。
ngDevMode 在 Angular 的类型定义是 null | NgDevModePerfCounters
ngDevMode 在 Angular Material 的类型定义是 null | object
看源码后,类型 runtime 应该是 NgDevModePerfCounters | false | undefined,没有看到赋值 null 的情况。
目录
想查看目录,请移步 Angular 18+ 高级教程 – 目录
Angular 18+ 高级教程 – 大杂烩的更多相关文章
- Siki_Unity_2-9_C#高级教程(未完)
Unity 2-9 C#高级教程 任务1:字符串和正则表达式任务1-1&1-2:字符串类string System.String类(string为别名) 注:string创建的字符串是不可变的 ...
- Pandas之:Pandas高级教程以铁达尼号真实数据为例
Pandas之:Pandas高级教程以铁达尼号真实数据为例 目录 简介 读写文件 DF的选择 选择列数据 选择行数据 同时选择行和列 使用plots作图 使用现有的列创建新的列 进行统计 DF重组 简 ...
- ios cocopods 安装使用及高级教程
CocoaPods简介 每种语言发展到一个阶段,就会出现相应的依赖管理工具,例如Java语言的Maven,nodejs的npm.随着iOS开发者的增多,业界也出现了为iOS程序提供依赖管理的工具,它的 ...
- 【读书笔记】.Net并行编程高级教程(二)-- 任务并行
前面一篇提到例子都是数据并行,但这并不是并行化的唯一形式,在.Net4之前,必须要创建多个线程或者线程池来利用多核技术.现在只需要使用新的Task实例就可以通过更简单的代码解决命令式任务并行问题. 1 ...
- 【读书笔记】.Net并行编程高级教程--Parallel
一直觉得自己对并发了解不够深入,特别是看了<代码整洁之道>觉得自己有必要好好学学并发编程,因为性能也是衡量代码整洁的一大标准.而且在<失控>这本书中也多次提到并发,不管是计算机 ...
- 分享25个新鲜出炉的 Photoshop 高级教程
网络上众多优秀的 Photoshop 实例教程是提高 Photoshop 技能的最佳学习途径.今天,我向大家分享25个新鲜出炉的 Photoshop 高级教程,提高你的设计技巧,制作时尚的图片效果.这 ...
- 展讯NAND Flash高级教程【转】
转自:http://wenku.baidu.com/view/d236e6727fd5360cba1adb9e.html 展讯NAND Flash高级教程
- Net并行编程高级教程--Parallel
Net并行编程高级教程--Parallel 一直觉得自己对并发了解不够深入,特别是看了<代码整洁之道>觉得自己有必要好好学学并发编程,因为性能也是衡量代码整洁的一大标准.而且在<失控 ...
- [转帖]tar高级教程:增量备份、定时备份、网络备份
tar高级教程:增量备份.定时备份.网络备份 作者: lesca 分类: Tutorials, Ubuntu 发布时间: 2012-03-01 11:42 ė浏览 27,065 次 61条评论 一.概 ...
- Angular CLI 使用教程指南参考
Angular CLI 使用教程指南参考 Angular CLI 现在虽然可以正常使用但仍然处于测试阶段. Angular CLI 依赖 Node 4 和 NPM 3 或更高版本. 安装 要安装Ang ...
随机推荐
- Linux 中 WIFI 和热点的使用
之前一直在 ubuntu 的图形界面中使用,突然需要在 ARM 板上打开热点,一时给弄蒙了,在此记录一下 一.网卡命令 显示所有网络信息 sudo ip link show 关闭或打开网络 sudo ...
- 解决方案 | Chrome/Edge 总是自动修改我的pdf默认打开方式
1.问题描述 最近我的pdf文件总是被chrome打开(如图1),而且点击属性,更改别的pdf阅读器也不管用(如图2),此时的chrome就像个流氓软件一样. 图1 被chrome劫持 图2 点击属性 ...
- VUE系列之性能优化--懒加载
一.懒加载的基本概念 懒加载是一种按需加载技术,即在用户需要时才加载相应的资源,而不是在页面初始加载时一次性加载所有资源.这样可以减少页面初始加载的资源量,提高页面加载速度和用户体验. 二.Vue 中 ...
- 云端IDE如何重定义开发体验
豆包 MarsCode 是一个集成了AI功能的编程助手和云端IDE,旨在提高开发效率和质量.它支持多种编程语言和IDE,提供智能代码补全.代码解释.单元测试生成和问题修复等功能,同时具备AI对话视图和 ...
- vue codemirror sql编辑器功能 可自定义提醒(关键字,库名,表名),高亮,主题
工作中再一次需要开发sql编辑器,优化上篇文章内容 https://www.cnblogs.com/Lu-Lu/p/14388888.html 本次功能是tab页打开多个sql编辑器,效果图: 安装: ...
- WPF控件库
Welcome to CookPopularControl 介绍 CookPopularControl是支持.NetFramework4.6.1与.Net5.0的WPF控件库,其中参考了一些资料,目前 ...
- SQL Server 清除一个数据库下所有表数据,保留表结构
用法:在需要清空数据的数据库创建并执行存储过程,该存储过程并不会影响其他数据库 请小心使用这些脚本,确保在生产环境之前备份您的数据库.️ 存储过程: CREATE PROCEDURE ClearAll ...
- 第九讲: MySQL为什么有时候会选错索引?
第九讲: MySQL为什么有时候会选错索引? 前面我们介绍过索引,你已经知道了在 MySQL 中一张表其实是可以支持多个索引的. 但是,你写 SQL 语句的时候,并没有主动指定使用哪个索引.也 ...
- 【SQL】 牛客网SQL训练Part3 较难难度
获取当前薪水第二多的员工的emp_no以及其对应的薪水salary 请你查找薪水排名第二多的员工编号emp_no.薪水salary.last_name以及first_name,不能使用order by ...
- 支持国际学术资源开放(版权费用 Open Access),支持SCI-HUB,向Sci-hub致敬
在去年多次向中国红字会捐款后再次决定向公益事业捐款,这次的捐款对象是SCI-HUB,可以说这是我们这种弱势的无大单位庇佑的散researcher的必备工具,多年来一直在使用,这次突然看到有支付宝捐款的 ...