• Angular-cli 构建应用的一些配置


    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中。比如我把routesmodules分离,这样组件将会在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:WebSite;
        index  index.html;
        ry_files $uri $uri/ /index.html?$query_string;
    }
    

    13. vue中习惯使用v-ifv-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);
      }
    
    }
    

    在控制台,我们可以看到:

    1

    第二次触发并没有被打印。是因为调用了取消订阅事件。将取消订阅事件注释掉,可以看到第二次触发打印:

    2

    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

  • 相关阅读:
    建立Azure Dev Ops持续集成和持续交付(CICD)(四、使用 Azure Pipelines 建立CICD)
    建立Azure Dev Ops持续集成和持续交付(CICD)(三、准备好Azure DevOps的帐号并上传代码)
    建立Azure Dev Ops持续集成和持续交付(CICD)(二、准备好ServicePrincipal并配置相应的权限)
    建立Azure Dev Ops持续集成和持续交付(CICD)(一、准备好Azure Web App资源)
    Jquery.Datatable 控件后端分页实例 (后台使用ashx、aspx-webmethod)
    c# 将object尝试转为指定对象
    c# 生成随机数
    http跳转https
    解决sqlserver数据库表空间不自动释放问题
    windowsSevice程序和topshelf程序创建服务对比
  • 原文地址:https://www.cnblogs.com/jehorn/p/8568109.html
Copyright © 2020-2023  润新知