Angular-cli 構建應用
的一些配置
標簽(空格分隔): Angular
- 直接使用
ng build --prod --build-optimizer --base-href=/
來發布 base-href
可以設置服務器上的某個子路徑,使用ng build --base-href=/my/path/
- 如果打包靜態文件(js和css)不放在和index.html同一路徑下,可以在
.angular-cli.json
配置文件apps
屬性下增加deployUrl
,等同於webpack的publicPath
。
如遇刷新找不到頁面(404)的情況,需要在服務器配置重定向到index.html。以nginx為例,可以在location添加try_files $uri $uri/ /index.html?$query_string;
來重定向到index.html。
如果碰到 *ngIf
*ngFor
用不了得情況,比如拋出 Property binding ngForOf not used by any directive on an embedded template. Make sure that the property name is spelled correctly and all directives are listed in the "@NgModule.declarations".的錯誤,通常是因為沒有importCommonModule
,而且這個import必須在組件被引用的module
中。比如我把routes
和modules
分離,這樣組件將會在xx-routing.module.ts
中被import,那么這個CommonModule
就得在xx-routing.module.ts
中被import,在xx.module.ts
引用是不行的。
首次項目實踐問題記錄
1. node版本升級(v6.x.x -> v8.11.1)后,原來項目ng serve
拋出錯誤:Node Sass could not find a binding for your current environment。
此時需要執行npm rebuild node-sass
來解決。參見stackoverflow。
2. 想要在整個應用初始化時候,在路由導航之前就請求數據,可以通過APP_INITIALIZER
實現。
app.module.ts
export function loadToken(tokenService: InitDataService) {
return () => tokenService.tokenAndTime();
}
providers: [
...
{
provide: APP_INITIALIZER,
useFactory: loadToken,
deps: [InitDataService],
multi: true
},
...
],
- *值得一提的是,目前只實現了同步數據獲取,如果異步,並不能在路由渲染完畢之前獲取完成。有待研究。 *
3. 關於querySelector()
選擇器,默認是返回Element
,這時候就不能在其后用.style
了。
需要將選擇到的Element轉為HTMLElement(參見):
let overlay = <HTMLElement>document.querySelector(`#${this.ID}`);
overlay.style.display = 'none';
4. 關於angualr的HttpClient,post請求默認將body中的數據序列化為json,如果后台只接收urlencoded格式的數據,就不能直接傳對象了:
private init() {
this.url = apiData.ServiceUrl + this.path;
const datas: Datas = {
ClientType: apiData.ClientType,
Token: this.tokenDatasService.token
};
Object.assign(datas, this._datas);
// 將參數對象序列化為 [key1]=[value1]&[key2]=[value2]的字符串
let params = new HttpParams();
if (!this.isGet) {
datas.Timespan = this.tokenDatasService.timespanFormat;
}
for (let key in datas) {
params = params.set(key, datas[key]);
}
if (this.isGet) {
this.datas = { params: params };
} else {
this.datas = params;
}
}
5. 如果想要使組件樣式可以應用到子組件,可以通過
@Component({
encapsulation: ViewEncapsulation.None,
...
})
這時樣式將不再局限於當前組件。
6. 如果想要當前根路徑(子根路徑)導航到未匹配路由時,比如設置404,可以在路由數組的末尾添加
const ROUTES: Routes = [
...
{ path: '**', component: NotFoundComponent}
];
7. 關於polyfills.ts
之前沒有取消注釋這個文件中的引用,在IE下打開發現報錯,取消注釋第一塊引用后,發現所有瀏覽器都出現自定義DI拋出錯誤Uncaught Error: Can't resolve all parameters for ApiService: (?). at syntaxError (compiler.es5.js:1694) ...。
google了半天都是說沒寫@Injectable()
或者少@或者(),然而檢查了半天並不是。最后在GitHub的一個Issues中找到了答案,需要取消注釋import 'core-js/es7/reflect';
即可解決。原因暫且未去探究。
8. 關於再ng中使用canvas
使用@ViewChild('[name]') canvasRef: ElementRef
來選擇canvas畫布。
9. 關於資源路徑,使用絕對路徑,比如css中獲取logo圖片:
background: url("/assets/img/shared/logo.png") no-repeat center/100%;
10. 父子路由可以通過服務來通信。
父級提供服務支持(providers),父級在constructor方法中訂閱(subscribe),子路由在ngOnInit方法或者其他自定義事件中賦值(next)。
11. 通過方括號綁定的routerLink屬性,值是異步獲取的(Observable)。這時候subscribe的時候拋出 ExpressionChangedAfterItHasBeenCheckedError 。
可以通過setTimeout([callback], 0)異步處理結果實現參見GitHub Issues :
this.accountService.titles$.subscribe(titles => setTimeout(() => {
this.title = titles.title;
this.titleLink = titles.titleLink.link;
this.titleLinkName = titles.titleLink.name;
}, 0));
12. 在開發環境(ng serve)中,各個路由刷新頁面正常顯示,但是打包部署到服務器后,在子路由中刷新頁面會出現404。可以通過配置服務器來修復這一問題 :
以nginx為例:
location / {
root C:\Web\Site;
index index.html;
ry_files $uri $uri/ /index.html?$query_string;
}
13. vue中習慣使用v-if
和v-else
,ng中也有這樣的模板語法:
注意必須使用ng-template
。
<h2 class="nick-name" *ngIf="isLogin; else notLogin">{{ userInfo.Name }}</h2>
<ng-template #notLogin>
<a href="javascript: void(0);" class="nick-name">立即登錄</a>
</ng-template>
14. 使用Subject
實現組件之間的通信
ionic中有一個Events
服務,可以通過publish
發布事件,在其他組件中subscribe
事件。
在Angular項目中,我們可以通過Subject
來創建一個服務實現類似的效果。類同本文(# 10)所述。
- 首先創建一個公共服務:
import {Injectable} from '@angular/core';
import {Datas} from '../models/datas.model';
import {Subject} from 'rxjs/Subject';
import {Observable} from 'rxjs/Observable';
import {Subscriber} from 'rxjs/Subscriber';
@Injectable()
export class EventsService {
private events: Datas = {};
public eventsName = [];
constructor() { }
/**
* 發布
* @param {string} topic 事件名稱
* @param {Datas} params 參數(對象)
*/
public publish(topic: string, params: Datas = {}) {
const event = this.getEvent(topic);
Object.assign(params, { EVENT_TOPIC_NAME: topic });
event.next(params);
}
/**
* 訂閱事件
* @param {string} topic 事件名稱
* @return {Observable}
*/
public subscribe(topic: string) {
return this.getEvent(topic).asObservable();
}
/**
* 取消訂閱事件
* @param {Subscriber} subscriber 訂閱事件對象
*/
public unsubscribe(subscriber: Subscriber<any>) {
subscriber.unsubscribe();
}
private getEvent(topic: string) {
this.eventsName.push(topic);
this.eventsName = Array.from(new Set(this.eventsName));
let _event;
for (const i in this.events) {
// 判斷是否已有事件
if (this.events.hasOwnProperty(i) && i === topic) {
_event = this.events[i];
break;
}
}
if (!_event) {
// 沒有事件 創建一個
_event = new Subject<Datas>();
const eventObj = { [topic]: _event };
Object.assign(this.events, eventObj);
}
return _event;
}
}
- 然后在某組件中訂閱事件
...
constructor(private eventsService: EventsService) { }
ngOnInit() {
const a = this.eventsService.subscribe('setHeader').subscribe(v => {
console.log(v);
// 取消訂閱
this.eventsService.unsubscribe(a);
});
}
...
- 在某組件中發布事件(觸發或許更為貼切)
...
export class IndexComponent implements OnInit {
constructor(private eventsService: EventsService) { }
ngOnInit() {
// 第一次觸發
this.eventsService.publish('setHeader', { a: 1, b: 2 });
setTimeout(() => {
// 第二次觸發
this.eventsService.publish('setHeader', { c: 3 });
}, 5000);
}
}
在控制台,我們可以看到:
第二次觸發並沒有被打印。是因為調用了取消訂閱事件。將取消訂閱事件注釋掉,可以看到第二次觸發打印:
15. 監聽路由跳轉
經常會用到路由跳轉后執行一些操作。通過Route
來進行操作。
import {NavigationEnd, Router} from '@angular/router';
...
constructor(private router: Router) { }
...
// 導航
navWatch() {
this.router.events.subscribe(e => {
if (e instanceof NavigationEnd) {
// TODO 路由跳轉完畢
}
});
}
...
16. 為組件添加事件
使用@Output() [eventName] = new EventEmitter<T>();
,然后在組件內部通過this[eventName].emit([params])
來觸發事件、傳遞參數。組件外部通過圓括號<my-component (eventName)="watchEvent($event)"></my-component>
。其中$event
就是傳遞過來的參數。
17. 監聽宿主事件
可以通過宿主監聽器@HostListener([event]: string, [args]: string[])
來操作。
比如監聽window滾動事件:
...
@HostListener('window:scroll', [])
onWindowScroll() {
// TODO 滾動事件
// this.scrollEvent().subscribe(obj => {
// this.scrollStyle(obj.offset, obj.direction);
// });
}
...
18. 自定義表單驗證器
如何實現兩次輸入密碼一致(兩個輸入框值相等)的自定義驗證器。
19. 給元素綁定data-*
等屬性
直接使用方括號你會發現拋出錯誤。這時候可以加個attr
來解決:
<img [attr.data-src]="value">
20. 關於css3 rem
的使用
我們習慣使用 html { font-size: 62.5%; }
來作為根大小(10px),但是Chrome並不支持12px以下的大小,這將導致Chrome與其他瀏覽器顯示不同。
搜索解決方案。
- 設置
body { font-size: 1.4em; }
,經試驗不起作用(至少在我的項目中)。 - 使用
-webkit-transform: scale(.8, .8);
,不是很滿意。 - 使用
html { font-size: 625%; }
,相當於100px。
我更偏向於第三種。
如果想要手動配置webpack來打包項目:(非必要)
使用ng new my-app
初始化的項目並不包含webpack配置文件,需要ng eject
命令來加入webpack.config.js
配置文件。
注意此時不能再用 ng build 之類的命令了,開發環境是npm start
,打包命令是npm run build
。
這時候webpack缺少一些原來的配置。
1. uglifyjs-webpack-plugin
js壓縮插件
將js文件壓縮,減小打包后文件的體積。
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
...
new UglifyJsPlugin({
"test": /\.js$/i,
"extractComments": false,
"sourceMap": true,
"cache": false,
"parallel": false,
"uglifyOptions": {
"output": {
"ascii_only": true,
"comments": false
},
"ecma": 5,
"warnings": false,
"ie8": false,
"mangle": {
properties: {
regex: /^my_[^_]{1}/,
reserved: ["$", "_"]
}
},
"compress": {}
}
})
2. compression-webpack-plugin
生成gzip文件插件
進一步減小打包文件體積。
const CompressionWebpackPlugin = require('compression-webpack-plugin');
...
new CompressionWebpackPlugin()
這個需要服務器開啟gzip on;
,以nginx為例,需要為服務器進行以下配置:
conf/nginx.conf:
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
# 開啟gzip
gzip on;
gzip_static on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_comp_level 2;
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
gzip_vary on;
gzip_disable "MSIE [1-6]\.";
server {
listen 8088;
server_name localhost;
location / {
root website/angular;
index index.html;
}
}
}
3. clean-webpack-plugin
清除打包文件工具
每次npm run build
后都會生成新的打包文件(文件名添加hash),這個插件可以在打包后刪除之前舊的文件。
const CleanWebpackPlugin = require('clean-webpack-plugin');
...
new CleanWebpackPlugin(['dist'], {
root: projectRoot,
verbose: true,
dry: false
})
4. CopyWebpackPlugin 配置修改
src/assets/
文件夾下的靜態資源以及favicon.ico
文件也需要打包,這時需要修改一下自動生成的配置代碼:
new CopyWebpackPlugin([
{
"context": "src",
"to": "assets/",
"from": "assets"
},
{
"context": "src",
"to": "",
"from": {
"glob": "favicon.ico",
"dot": true
}
}
], {
"ignore": [
".gitkeep",
"**/.DS_Store",
"**/Thumbs.db"
],
"debug": "warning"
}),
5. Extract Text Plugin 的使用(存在問題)
如果需要分離css單獨打包,可以使用 extract-text-webpack-plugin
。
可能會有解決方案,暫時不做深入探究。還是推薦直接使用ng-cli。
注意,分離css后,angular的特殊選擇器將失效,比如:host {}
選擇器,使用正常的css方法實現來替代。
注意,樣式的引用就需要通過import './xx.scss';
的方式來引用樣式文件,否則會拋出Expected 'styles' to be an array of strings.
的錯誤。
也有通過"use": ['to-string-loader'].concat(ExtractTextPlugin.extract(<options>))
的方法來實現。
因為不通過@Component({ styleUrls: '' })
的方式,樣式的scope作用將消失。
webpack.config.js
:
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const extractCSS = new ExtractTextPlugin('[name].[contenthash:8].css');
const extractSCSS = new ExtractTextPlugin('[name].[contenthash:8].css');
module.exports = {
...
"entry": {
...
"styles": [
"./src/app.scss"
]
},
"module": {
"rules": [
{
"test": /\.css$/,
"use": extractCSS.extract({
"fallback": "style-loader",
"use": [
{
"loader": "css-loader",
"options": {
"sourceMap": false,
"import": false
}
},
{
"loader": "postcss-loader",
"options": {
"ident": "postcss",
"plugins": postcssPlugins,
"sourceMap": false
}
}]
})
},
{
"test": /\.scss$|\.sass$/,
"use": extractSCSS.extract({
"fallback": "style-loader",
"use": [
{
"loader": "css-loader",
"options": {
"sourceMap": false,
"import": false
}
},
{
"loader": "postcss-loader",
"options": {
"ident": "postcss",
"plugins": postcssPlugins,
"sourceMap": false
}
},
{
"loader": "sass-loader",
"options": {
"sourceMap": false,
"precision": 8,
"includePaths": []
}
}]
})
},
],
"plugins": [
...
extractCSS,
extractSCSS
]
}
...
}
app.component.ts
import './app.component.scss';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
6. publicPath
- webpack 有一個
publicPath
屬性,可以設置資源引用路徑,需要寫在output
屬性下:
module.exports = {
...
"output": {
"publicPath": '/',
"path": path.join(process.cwd(), "dist"),
"filename": "[name].bundle.[chunkhash:8].js",
"chunkFilename": "[id].chunk.[chunkhash:8].js",
"crossOriginLoading": false
},
...
}
如果使用ng-cli,可以在apps
屬性下設置deployUrl
,等同於publicPath。
我的環境
Angular CLI: 1.6.7 (e)
Node: 8.11.1
OS: win32 x64
Angular: 5.2.3
demo源碼
參考文章: angular-cli issues | style-loader issues | stackoverflow | copy-webpack-plugin拷貝資源插件等
The end... Last updated by: Jehorn, Sep 17, 2018, 04:29 PM