ng-alain
转载请注明原出处,勘误请发送电子邮件
1.什么是 ng-alain?
ng-alain 是来自中国作者 卡色 开源出的一个企业级中后台前端/设计解决方案脚手架,秉承 Ant Design 的设计价值观,让 Angular
快速落地于企业生产实践中的一个高集成性框架。
其中提供了非常丰富的功能诸如
- Dynamic Form
- Antv/Echarts
- Acl
- Auth
- Theme
- Cache
- Mock
- Util (DeepCopy/TreeToArray 等等)
假如你对 Angular 有一些使用基础,那么使用起来会相当的得心应手。
假如你对 Angular 只有一些略微了解,那么也可以通过 ng-alain
提供的 CLI
快速的建立起一个可运行 Project
用来快速上手。
2.ng-alain 解析
- 生成一个 ng-alain 项目首先需要安装
AngularCli
npm install @angular/cli@12.2.0
- 其次使用
ng new my-project --style less --routing
创建名为my-project
的纯Angular
项目 - 然后使用
ng-alain
的CLI
生成项目 ng add ng-alain
- 最后使用
npm run start
启动项目 - 或者直接前往预览地址
3.ng-alain 项目
“没有比浏览 angular.json 配置文件更能快速了解一个 angular 项目的方法了” -詹姆斯高斯林
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"ng-alain": {
"projectType": "application",
"root": "",
"sourceRoot": "src",
"prefix": "app",
"schematics": {
"@schematics/angular:component": {
"style": "less"
},
"@schematics/angular:application": {
"strict": true
}
},
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist",
"index": "src/index.html",
"main": "src/main.ts",
"tsConfig": "tsconfig.app.json",
"polyfills": "src/polyfills.ts",
"assets": ["src/assets", "src/favicon.ico"],
"styles": ["src/styles.less"],
"scripts": [],
"allowedCommonJsDependencies": ["ajv", "ajv-formats"]
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"outputHashing": "all",
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "6mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "ng-alain:build",
"proxyConfig": "proxy.conf.json"
},
"configurations": {
"production": {
"browserTarget": "ng-alain:build:production"
},
"development": {
"browserTarget": "ng-alain:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "ng-alain:build"
}
},
....
我们可以清晰的从 angular.json 中看出
- build 部分
projects
中仅仅含有一个类型为application
的项目ng-alain
builder
编译器使用了默认的@angular-devkit/build-angular:browser
,当然还有许多其他的编译器,可以自行搜索使用。builder
中的options
指定了一些入口文件- 引入了
CommanJS
:ajv
,ajv-formats
,ajv
是JSON Schema
的一种验证器 build
中的configuration
会使用env.prod.ts
替换env.ts
outputHashing
打包文件带有哈希值- serve 部分
- serve 对应的命令为
ng serve
,使用的builder
为@angular-devkit/build-angular:dev-server
,我们可以在 @angular-devikt 找到这个包,有一些配置参数和说明,刚刚 build 时使用的 browser,还有 app-shell 呦
找到了入口文件 index.html
<app-root></app-root>
<div class="preloader">
<div class="cs-loader">
<div class="cs-loader-inner">
<label> ●</label>
<label> ●</label>
<label> ●</label>
<label> ●</label>
<label> ●</label>
<label> ●</label>
</div>
</div>
</div>
比较重要的一部分
<app-root>
,这里是对app.component.ts
的直接引用- 下面的
div
中使用class
和css
做了一个加载动画,可以理解成遮罩层,利用最先渲染index.html
,使用css
将整个webapp
遮盖住 - index.html 结束
找到了入口文件 main.ts
preloaderFinished();
第九行调用了 preloaderFinished
,这个函数是在 alain
的另一个基础项目 delon
中以基础包的形式出现的
export function preloaderFinished(): void {
const body = document.querySelector('body')!;
const preloader = document.querySelector('.preloader')!;
body.style.overflow = 'hidden';
function remove(): void {
// preloader value null when running --hmr
if (!preloader) return;
preloader.addEventListener('transitionend', () => {
preloader.className = 'preloader-hidden';
});
preloader.className += ' preloader-hidden-add preloader-hidden-add-active';
}
(window as NzSafeAny).appBootstrap = () => {
setTimeout(() => {
remove();
body.style.overflow = '';
}, 100);
};
}
-
写了之前说的遮罩层如何删除
-
赋予 window 对象一个
appBootstrap
函数,用来进行调用remove
删除掉index.html
中的遮罩层if (environment.production) {
enableProdMode();
}
如果环境为 prod
模式那么开启 Prod
模式
platformBrowserDynamic()
.bootstrapModule(AppModule, {
defaultEncapsulation: ViewEncapsulation.Emulated,
preserveWhitespaces: false
})
.then(res => {
const win = window as NzSafeAny;
if (win && win.appBootstrap) {
win.appBootstrap();
}
return res;
})
.catch(err => console.error(err));
上面为 main.ts
中最后的内容
- 引导
AppModule
启动 - 启动完毕后调用
win
中存下的remove
函数,删除遮罩层
@import '~@delon/theme/system/index';
@import '~@delon/abc/index';
@import '~@delon/chart/index';
@import '~@delon/theme/layout-default/style/index';
@import '~@delon/theme/layout-blank/style/index';
@import './styles/index';
@import './styles/theme';
在 style.less
中引入了一些 less
变量,以供全局 css
使用,这些 less
变量也是 @delon/theme
包中出现的,后续可能会看到
至此,除了 assets
静态资源目录,其余从 angular.json
找到的入口的启动过程梳理完毕,分别
- 提供了模版
- 提供了静态资源
- 提供了 JS 启动模块
- 提供了全局 CSS
3.1 项目配置
接下来看被 bootstrap
的 AppModule
const LANG = {
abbr: 'zh',
ng: ngLang,
zorro: zorroLang,
date: dateLang,
delon: delonLang
};
registerLocaleData(LANG.ng, LANG.abbr);
const LANG_PROVIDES = [
{ provide: LOCALE_ID, useValue: LANG.abbr },
{ provide: NZ_I18N, useValue: LANG.zorro },
{ provide: NZ_DATE_LOCALE, useValue: LANG.date },
{ provide: DELON_LOCALE, useValue: LANG.delon }
];
前面主要是本地化/i18n 的一些配置,通过 provide
键值对的方式注入到包里,使得第三方库使用 useValue
提供的值
const I18NSERVICE_PROVIDES = [{ provide: ALAIN_I18N_TOKEN, useClass: I18NService, multi: false }];
当然除了使用 useValue
的方式,还可以使用 useClass
export const ALAIN_I18N_TOKEN = new InjectionToken<AlainI18NService>('alainI18nToken', {
providedIn: 'root',
factory: () => new AlainI18NServiceFake()
});
上文的 ALAIN_I18N_TOKEN
在 @delon/theme
中,接收一个 AlainI18nService类型
的 class
I18nService
继承自 AlainI18nBaseService
,而 AlainI18nBaseService
又实现了 AlainI18NService
接口,所以类型匹配,这样我们就可以把自己的 class
传入第三方包中,第三方包注入 class
时调用的 function
就可以来自外部实际项目了
const GLOBAL_THIRD_MODULES: Array<Type<any>> = [BidiModule]
引入了一个左右布局切换的 Module,在 cdk
里,cdk
是 @angular
提供的一个工具包
const INTERCEPTOR_PROVIDES = [
{ provide: HTTP_INTERCEPTORS, useClass: SimpleInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: DefaultInterceptor, multi: true }
];
再往下就是这两个拦截器
Simple
: 验证 tokenDefault
: 处理错误信息
export function StartupServiceFactory(startupService: StartupService): () => Observable<void> {
return () => startupService.load();
}
const APPINIT_PROVIDES = [
StartupService,
{
provide: APP_INITIALIZER,
useFactory: StartupServiceFactory,
deps: [StartupService],
multi: true
}
];
重点,这里我称为 实际引导项目启动
的一个函数,这里作为整个项目声明周期的 初始化
周期,使用了 Angular
内置的 provide
: APP_INITIALIZER
并返回了一个 Promise
函数 load
@Injectable()
export class StartupService {
constructor(
iconSrv: NzIconService,
private menuService: MenuService,
@Inject(ALAIN_I18N_TOKEN) private i18n: I18NService,
private settingService: SettingsService,
private aclService: ACLService,
private titleService: TitleService,
private httpClient: HttpClient
) {
iconSrv.addIcon(...ICONS_AUTO, ...ICONS);
}
load(): Observable<void> {
const defaultLang = this.i18n.defaultLang;
return zip(this.i18n.loadLangData(defaultLang), this.httpClient.get('assets/tmp/app-data.json')).pipe(
// 接收其他拦截器后产生的异常消息
catchError(res => {
console.warn(`StartupService.load: Network request failed`, res);
return [];
}),
map(([langData, appData]: [Record<string, string>, NzSafeAny]) => {
// setting language data
this.i18n.use(defaultLang, langData);
// 应用信息:包括站点名、描述、年份
this.settingService.setApp(appData.app);
// 用户信息:包括姓名、头像、邮箱地址
this.settingService.setUser(appData.user);
// ACL:设置权限为全量
this.aclService.setFull(true);
// 初始化菜单
this.menuService.add(appData.menu);
// 设置页面标题的后缀
this.titleService.default = '';
this.titleService.suffix = appData.app.name;
})
);
}
}
以上代码初始化了整体项目必备的数据
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
HttpClientModule,
GlobalConfigModule.forRoot(),
CoreModule,
SharedModule,
LayoutModule,
RoutesModule,
STWidgetModule,
NzNotificationModule,
...GLOBAL_THIRD_MODULES,
...FORM_MODULES
],
providers: [...LANG_PROVIDES, ...INTERCEPTOR_PROVIDES, ...I18NSERVICE_PROVIDES, ...APPINIT_PROVIDES],
bootstrap: [AppComponent]
})
export class AppModule {}
回到 app.module
中,imports
一些模块,providers
一些服务,
其中对于整体项目比较重要的模块有
GlobalConfigModule.forRoot()
全局配置模块,主要配置alain
中的一些组件功能/认证/等各方面的内容LayoutModule
在用户端的一个布局模块SharedModule
共享模块RoutesModule
总体路由模块,比如/app/v1
路径,要渲染哪个模块的那个组件,就是这个模块决定的,在这个项目里,也称为业务模块
CoreModule
core 模块,刚才提到的拦截器和 i18n 和startup
就在这个模块中
4.alain 项目组成
直接看 package.json
吧,就不对照模块了,刚才涉及到的代码有很多都来自以下这些 npm
包
"ng-zorro-antd": "^12.0.1", 阿里ant组件库
"@delon/abc": "^12.1.0", delon包,也是最核心的包
"@delon/acl": "^12.1.0",
"@delon/auth": "^12.1.0",
"@delon/cache": "^12.1.0",
"@delon/chart": "^12.1.0",
"@delon/form": "^12.1.0",
"@delon/mock": "^12.1.0",
"@delon/theme": "^12.1.0",
"@delon/util": "^12.1.0",
"ngx-tinymce": "^12.0.0", 两个编辑器包
"ngx-ueditor": "^12.0.0",
"screenfull": "^5.1.0",
"ajv": "^8.6.2" jsonSchematicValidate包
"ng-alain": "^12.1.0", 脚手架包
"ng-alain-plugin-theme": "^12.0.0", 主题生成插件
"ng-alain-sts": "^0.0.1", 构建 Swagger API 转换为列表、编辑页的命令行工具
5.delon
delon 的项目地址位于: https://github.com/ng-alain/delon
按照惯例先看 angular.json
"delon": {
"root": "packages",
"projectType": "library",
"prefix": "",
"architect": {
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"fix": true,
"lintFilePatterns": [
"packages/**/*.ts",
"packages/**/*.html"
]
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "packages/test.ts",
"karmaConfig": "packages/karma.conf.js",
"polyfills": "packages/polyfills.ts",
"tsConfig": "packages/tsconfig.spec.json",
"scripts": ["node_modules/@antv/g2/dist/g2.min.js", "node_modules/@antv/data-set/dist/data-set.js"],
"codeCoverageExclude": ["schematics/**", "packages/testing/**"]
}
}
}
}
可以看到这里 delon
的跟路径为 packages
,应用类行为 package
,并不是刚才的 application
,说明它并不是一个应用,而是一个最终为 npm 包
形式的项目,所以自然没有 serve
启动项的配置
但是我们找到了入口 root
配置项配置的是 packages
5.1 delon/packages
5.2 delon build
5.3 delon schematics
5.4 sts
5.5 plugin-theme
plugin-theme 主要是用来生成主题配置的一个插件
plugin-theme 的项目在 https://github.com/ng-alain/plugin-theme.git
const cli = meow({
help: `
Usage
ng-alain-plugin-theme
Example
ng-alain-plugin-theme -t=themeCss -c=ng-alain.json
Options
-t, --type Can be set 'themeCss', 'colorLess'
-c, --config A filepath of NG-ALAIN config script
-d, --debug Debug mode
`,
flags: {
type: {
type: 'string',
default: 'themeCss',
alias: 't',
},
config: {
type: 'string',
default: 'ng-alain.json',
alias: 'c',
},
debug: {
type: 'boolean',
default: false,
alias: 'd',
},
},
});
这里使用了基于 nodejs 的 meow
包,构建了一个 CLI
工具,所谓 CLI
即为 Command Line Interface
,其实就是命令行,这里给出了三个配置项
- -t 文件类型,less/css
- -c 自定义主题 config,详情见 ng-alalin 项目内的 ng-alain.json,也可以在主题切换中找到使用方式
- -d 是否开启 debug 模式
let config: { theme: Config; colorLess: Config; [key: string]: Config };
传入 JSON 的格式
[key:string]:Config
意思是接口的任何属性,都要是 Config 格式
try {
const configFile = resolve(process.cwd(), cli.flags.config);
if (existsSync(configFile)) {
config = getJSON(configFile);
} else {
console.error(`The config file '${cli.flags.config}' will not found`);
process.exit(1);
}
} catch (err) {
console.error('Invalid config file', err);
process.exit(1);
}
读取 cli 传入的 文件路径
然后转换成 JSON,赋值给 config
if (cli.flags.type === 'themeCss') {
buildThemeCSS(config.theme);
} else if (cli.flags.type === 'colorLess') {
genColorLess(config.colorLess);
} else {
throw new Error(`Invalid type, can be set themeCss or colorLess value`);
}
假如 type 是 css
那么走 buildThemeCSS
函数
假如 type 是 less
那么走 genColorLess
函数
export async function genColorLess(config: ColorLessConfig): Promise<void> {
config = fixConfig(config);
if (existsSync(config.outputFilePath!)) {
unlinkSync(config.outputFilePath!);
}
await generateTheme(config);
}
genColorLess 简单的处理 config 传入的 ng-alalin.json
转换成的对象,然后调用了 generateTheme
来生成主题,最后输出到项目的目录中,具体通过 json
来生成 theme
的函数规则较为复杂,概括一下就是 拿json传入的配置,对文件内容自动进行@import,替换等操作
,最后生成目标文件 less,写入原项目中
5.6 scripts
5.7 site
5.8 changelog
5.9 publish
5.10 release
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于