Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How To Include CSS Before App Load in Angular + Webpack + Heroku Application

I currently have an Angular (v4.0.1) application using webpack and deployed to Heroku. I have a loading spinner set up to display on the page while the app is loading. Currently I have it set up so that it works locally, but for some reason when I deploy to heroku the loading spinner (or rather more specifically the CSS to spin the loading spinner) doesn't appear to be pulled in.

I've tried a number of possible fixes but I'm having a hard time figuring out what I need to change to get this to work in production and everything I've found on stackoverflow seems to only work locally. I should also clarify that all of my css files within the app (as in the component styles that load once the app has loaded) work fine, its just the one css file that I have included in the index.html specifically for the loading spinner that needs to be available before the Angular app loads.

My file structure (simplified):

.
+-- config/
+-- src/
|   +-- app/
|   +-- assets/
|      +-- icons/
|          +-- loading-spinner.svg
|      +-- stylesheets/
|          +--- loading-spinner.css
|    +-- vendor/
|    +-- index.html
|    +-- main.ts
|    +-- polyfills.ts
|    +-- tsconfig.json
+-- package.json
+-- server.js

My index.html

<!DOCTYPE html>
<html>
  <head>
    <base href="/">
    <title>Stylist Suite 2.0</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="src/assets/stylesheets/loading-spinner.css">
  </head>
  <body>
    <ss-app>
      <div class="loading-spinner ss-loading"></div>
    </ss-app>
  </body>
</html>

The loading-spinner.css file:

/* --- Loading Spinner - Needed Before App Loads ---*/

.loading-spinner {
  width: 42px;
  height: 44px;

  background: url("../icons/loading-spinner.svg") no-repeat;
  margin: 0 auto;
  animation: spin 2.5s linear infinite;

  -webkit-animation: spin 2.5s linear infinite;
  -webkit-transform: translateZ(0);
  -ms-transform: translateZ(0);
  transform: translateZ(0);
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

.loading-spinner .ss-loading {
  position: fixed;
  top:50%;
  left:50%;
  margin-left:-21px;
  margin-top: -22px;
  align-self: center;
  justify-self: center;
}

My webpack.common.js (located under config/)

var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var helpers = require('./helpers');

module.exports = {
  entry: {
    'polyfills': './src/polyfills.ts',
    'vendor': './src/vendor/vendor.ts',
    'app': './src/main.ts'
  },

  resolve: {
    extensions: ['.ts', '.js']
  },

  devtool: 'source-map',

  module: {
    rules: [
      {
        test: /\.html$/,
        loader: 'html-loader'
      },
      {
        test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/,
        loader: 'file-loader?name=assets/[name].[hash].[ext]'
      },
      {
        test: /\.css$/,
        exclude: helpers.root('src', 'app'),
        loader: ExtractTextPlugin.extract({ loader: 'style-loader', use: 'css-loader?sourceMap' })
      },
      {
         test: /\.css$/,
         include: helpers.root('src', 'app'),
         loader: 'raw-loader'
      },
      {
        test: /\.css$/,
        exclude: helpers.root('src', 'assets'),
        loader: ExtractTextPlugin.extract({ loader: 'style-loader', use: 'css-loader?sourceMap' })
      },
      {
        test: /\.css$/,
        include: helpers.root('src', 'assets'),
        loader: 'raw-loader'
      },
      {
        test: /\.scss$/,
        exclude: /node_modules/,
        loaders: ['to-string-loader', 'style-loader', 'css-loader', 'resolve-url-loader', 'sass-loader?sourceMap']
      },
      {
        test: /\.ts$/,
        loaders: [
          {
            loader: 'awesome-typescript-loader',
            options: { configFileName: helpers.root('src', 'tsconfig.json') }
          }, 'angular2-template-loader'
        ]
      }
    ]
  },

  plugins: [
    // Workaround for angular/angular#11580
    new webpack.ContextReplacementPlugin(
      // The (\\|\/) piece accounts for path separators in *nix and Windows
      /angular(\\|\/)core(\\|\/)(esm(\\|\/)src|src)(\\|\/)linker/,
          helpers.root('./src'), // location of your src
      {} // a map of your routes
    ),

    new webpack.optimize.CommonsChunkPlugin({
      name: ['app', 'vendor', 'polyfills']
    }),

    new HtmlWebpackPlugin({
      template: 'src/index.html'
    })
  ]
};

My webpack.dev.js (located under config/)

var webpack = require('webpack');
var webpackMerge = require('webpack-merge');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var commonConfig = require('./webpack.common.js');
var helpers = require('./helpers');

module.exports = webpackMerge(commonConfig, {
  devtool: 'cheap-module-eval-source-map',

  output: {
    path: helpers.root('dist'),
    publicPath: 'http://localhost:4200/',
    filename: '[name].js',
    chunkFilename: '[id].chunk.js'
  },

  plugins: [
    new ExtractTextPlugin('[name].css'),
    new webpack.DefinePlugin({
      'process.env.API_APPLICATION_ID': JSON.stringify(""),
      'process.env.REDIRECT_URL': JSON.stringify("http://localhost:4200/login"),
      'process.env.API_BASE_URL': JSON.stringify("http://localhost:3000"),
      'process.env.SITE_URL': JSON.stringify("http://localhost:3000")
    })
  ],

  devServer: {
    historyApiFallback: true,
    stats: 'minimal'
  }
});

My webpack.prod.js (located under config/)

var webpack = require('webpack');
var webpackMerge = require('webpack-merge');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var commonConfig = require('./webpack.common.js');
var helpers = require('./helpers');

const ENV = process.env.NODE_ENV = process.env.ENV = 'production';

module.exports = webpackMerge(commonConfig, {
  devtool: 'source-map',

  output: {
    path: helpers.root('dist'),
    publicPath: '/',
    filename: '[name].[hash].js',
    chunkFilename: '[id].[hash].chunk.js'
  },

  plugins: [
    new webpack.NoEmitOnErrorsPlugin(),
    new webpack.optimize.UglifyJsPlugin({ // https://github.com/angular/angular/issues/10618
      mangle: {
        keep_fnames: true
      }
    }),
    new ExtractTextPlugin('[name].[hash].css'),
    new webpack.DefinePlugin({
      'process.env.API_APPLICATION_ID': JSON.stringify(process.env.API_APPLICATION_ID),
      'process.env.REDIRECT_URL': JSON.stringify(process.env.REDIRECT_URL),
      'process.env.API_BASE_URL': JSON.stringify(process.env.API_BASE_URL),
      'process.env.SITE_URL': JSON.stringify(process.env.REDIRECT_URL)
    }),
    new webpack.LoaderOptionsPlugin({
      htmlLoader: {
        minimize: false // workaround for ng2
      }
    })
  ]
});

Again, all of my assets (other stylesheets, images under the icons folder) load fine, once the app has loaded, because they are being handled properly by the various css and file loaders in my webpack.common.js. I am assuming the issue has to do with my webpack.prod.js file because all of this works perfectly locally but everything I try seems to not fix the issue at all, the loading-spinner.css file is not being loaded at all. Any advice would be appreciated.

like image 626
NColey Avatar asked Oct 17 '22 14:10

NColey


1 Answers

From your configuration it looks like you just followed the Angular docs on webpack verbatim. In those docs, they make use of a global stylesheet src/assets/css/styles.css. They imported this in the AppComponent

import '../assets/css/styles.css';

@Component({
})
export class AppComponent { }

What this will do is cause the ExtractTextPlugin to extract the text from the (in this case css) file that you import or require, put it in a styles.css file, and add that file as a <link>. The output file name has no correlation to the actual filename. styles.css just happens to be the name configured for the ExtractTextPlugin.

If you check the dist folder, and check out the index.html, you will see that this styles.[hash].css file is added as a <link>.

That being said, you should do the same with the loader css. Just import it into the AppComponent. This will cause the loader css content into the styles.[hash].css. If you want to have the loader css separate from the other global styles, think again, as it doesn't really matter. The browser doesn't start rendering until all the style sheets are loaded anyway.

The reason it works for the dev server, is because the dev sever works a little differently. It's configured to add a public path for the server to it can server up files from a location.

In this case, the assets are accessible from the server. But when you build for production, these assets are not transferred. They need to be imported somewhere, as this is how webpack works for the most port. With the import, webpack knows that it's a module that should be build for distribution.

You also don't need to add the loader css to the index.html manually, as you have done. It will work both for dev and production, just by importing it. Also you don't need to import the svg anywhere, as webpack will already detect it in the loader css url, and transfer it in the production build

like image 143
Paul Samsotha Avatar answered Nov 01 '22 10:11

Paul Samsotha