-->

Webpack + postcss + themes + hot reloading

2019-05-02 16:16发布

问题:

I want to allow users to select themes on my website, which they will do through a dropdown this. I am currently having trouble figuring out how to support this feature though, without writing dumb code.

I currently use css-modules along with postcss to make my life easier. Webpack is used to bundle everything together. I utilize hot reloading during development, during which I use the style-loader instead.

The full configuration looks like this

{
  test: /\.css$/,
  loader: !isDev
    ? ExtractTextPlugin.extract('style-loader', 'css-loader?sourcemap&minimize&modules&importLoaders=1&localIdentName=[name]-[local]-[hash:base64:5]!postcss-loader')
    : 'style-loader!css-loader?sourcemap&minimize&modules&importLoaders=1&localIdentName=[name]-[local]-[hash:base64:5]!postcss-loader'
},

Solution 1

Place a classname on the body tag to determine what theme is loaded. This is then used to select the correct theme from inside each components css. This is quite tedious and something I'd like to avoid, but it would look something like this:

.myComponent {
  margin: 10px;
  width: 100px;
}

:global(.theme1) {
  .myComponent {
    background: $theme1_myComponent_background;
  }
}

:global(.theme2) {
  .myComponent {
    background: $theme2_myComponent_background;
  }
}

This is quickly going to turn into something hard to extend and unmaintainable which is why I would like to avoid it.

Solution 2

Generate pre-themed static bundles for the css. When the user requests /main.css, a handler on the server determines which theme to send (perhaps query string) and sends them theme1-main.css or theme2-main.css or something along those lines. The problem there is I don't know how to support the dev environment.

The good thing about this is that my css files don't need any extra and hard to maintain logic:

.myComponent {
  margin: 10px;
  width: 100px;
  background: $myComponent_background;
}

Solution 3

Use native css variables. These can then be updated during runtime which would reflect the already loaded css and work gracefully across both production and development environments. My components would also look like (or similar to) solution 2. The problem here is that they are not very widely supported natively, and I'm unsure if there are any polyfills worth looking into that will do this type of thing for me.


All in all, these are my thoughts. I have not arrived at any definitive conclusion yet and would appreciate all feedback on how I can solve this problem. Perhaps I'm thinking about it all wrong and there is a much better way of doing what I want done.

Thanks in advance!

回答1:

Here is a solution using SASS. It's basically your solution 1 but easier to write and maintain. It seams that you're not using SASS but since it's the best solution I could find for my personal use, I thought it worth sharing...

Here is the SCSS code (CodePen here):

// Define theme variables

$themes: (
  default: ( // default theme
    bg-color: white,
    text-color: black,
  ),
  dark: (
    bg-color: black,
    text-color: white,
  ),
  colorful: (
    bg-color: green,
    text-color: orange,
  )
);

// This is how you use your themes

.example{
  padding: 10px;
  @include themify {
    background: theme-get(bg-color);
    color: theme-get(text-color);
  }
}

It requires this to work:

@mixin themify() {
  // Iterate over the themes
  @each $theme-name, $theme in $themes {
    $current-theme: $theme !global;
    @if $theme-name == 'default' {
      @content;
    } @else {
      .theme-#{$theme-name} & {
        @content;
      }
    }
  }
}

@function theme-get($key, $theme: $current-theme) {
  $ret: map-get($theme, $key);
  @if not $ret {
    @error 'Your theme doesn\'t have a value for `#{$key}`.';    
  }
  @return $ret;
}

Resulting CSS:

.example {
  padding: 10px;
  background: white;
  color: black;
}
.theme-dark .example {
  background: black;
  color: white;
}
.theme-colorful .example {
  background: green;
  color: orange;
}

This is heavily inspired by:

  • https://www.sitepoint.com/sass-theming-neverending-story/
  • https://jaicab.com/sass-vary/


回答2:

One possible approach could be as follow

  1. Not to use style-loader because it inserts css automatically, but do it manually when we need and what we need.

for example:

const css1 = require("raw!./App1.css");
const css2 = require("raw!./App2.css");

(we need raw-loader here)

  1. then we just insert required style to document.

for example:

const style = document.createElement("link");
style.type = "stylesheet/text-css";
style.rel = "stylesheet";
style.href=`data:text/css,${css1}`;
// style.href=`data:text/css,${css2}`;
style.id = "theming";

document.querySelector("head").appendChild(style);
  1. then you can change your theme when you need

just put another style to href:

style.href=`data:text/css,${css2}`

or let's do same via React:

import React, { Component } from 'react';
import logo from './logo.svg';

class App extends Component {
  constructor(props) {
    super(props);

    this.cssThemes = [];
    this.cssThemes.push(require("./App.css"));
    this.cssThemes.push(require("./App1.css"));
    this.cssThemes.push(require("./App2.css"));
    this.cssThemes.push(require("./App3.css"));

    this.state = { 
      themeInd: 0,
    }
  }
  render() {
    return (
      <div >
      <style>
        {this.cssThemes[this.state.themeInd]}
      </style>
        <div className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h2>Welcome to React</h2>
        </div>
        <p >
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
        <button 
          onClick={() => this.setState({themeInd: this.state.themeInd+1})}
        >
          Change theme
        </button>
      </div>
    );
  }
}

export default App;

it'll work if you set the raw-loader to handle css file in webpack.config

{
  test: /\.css$/,
  loader: 'raw'
},