用户可切换的自定义主题与Vue.js

时间:2017-10-13 13:17:08

标签: vue.js themes vuejs2

我有一个VueJS应用程序,它将带有许多不同的主题(至少20个左右)。每个主题样式表不仅会更改颜色和字体大小等内容,还会更改某些元素的位置和布局。

我希望用户能够动态切换这些主题。因此,在运行时,用户将能够打开“选项”菜单并从下拉列表中进行选择。

在VueJS中拥有许多动态用户可选主题的最简洁方法是什么?

我想过几种方法,例如:

  • 动态插入<link><style>标记。虽然这可能有用,但我并不认为它特别“干净”,如果我从AJAX加载,那么我经常会看到FOUC
  • 只需通过计算属性更改Vue类绑定。类似于为每个组件中的每个受支持主题设置if-else链。我并不特别喜欢这个解决方案,因为每当我稍后添加新主题时,我制作的每个组件都需要更新。

在React中,我认为有一个插件或具有<ThemeProvider>组件的东西,其中添加主题就像包装它一样简单,即<ThemeProvider theme={themeProp}><MyComponent></ThemeProvider>,并且该主题中的所有样式都将适用于该组件和所有子组件。

VueJS有类似的东西,还是有办法实现它?

5 个答案:

答案 0 :(得分:12)

我会承认我对这个有一些乐趣。此解决方案在Vue上不依赖,但可以通过 Vue轻松使用它。我们走了!

我的目标是创造一个特别干净的&#34;动态插入<link>样式表,不应该导致FOUC

我创建了一个类(在技术上,它是一个构造函数,但你知道我的意思)叫ThemeHelper,它的工作原理如下:

  • myThemeHelper.add(themeName, href)将使用hrefstylesheet.disabled = true(网址)预加载样式表,并为其命名(仅用于跟踪它)。这会返回Promise,在调用样式表onload时会解析为CSSStyleSheet
  • myThemeHelper.theme = "<theme name>"(setter)选择要应用的主题。禁用上一个主题,并启用给定主题。切换很快发生,因为样式表已由.add预先加载。
  • myThemeHelper.theme(getter)返回当前主题名称。

班级本身是33行。我制作了一个在一些Bootswatch主题之间切换的片段,因为那些CSS文件非常大(100Kb +)。

&#13;
&#13;
const ThemeHelper = function() {
 
  const preloadTheme = (href) => {
    let link = document.createElement('link');
    link.rel = "stylesheet";
    link.href = href;
    document.head.appendChild(link);
    
    return new Promise((resolve, reject) => {
      link.onload = e => {
        const sheet = e.target.sheet;
        sheet.disabled = true;
        resolve(sheet);
      };
      link.onerror = reject;
    });
  };
  
  const selectTheme = (themes, name) => {
    if (name && !themes[name]) {
      throw new Error(`"${name}" has not been defined as a theme.`); 
    }
    Object.keys(themes).forEach(n => themes[n].disabled = (n !== name));
  }
  
  let themes = {};

  return {
    add(name, href) { return preloadTheme(href).then(s => themes[name] = s) },
    set theme(name) { selectTheme(themes, name) },
    get theme() { return Object.keys(themes).find(n => !themes[n].disabled) }
  };
};

const themes = {
  flatly: "https://bootswatch.com/4/flatly/bootstrap.min.css",
  materia: "https://bootswatch.com/4/materia/bootstrap.min.css",
  solar: "https://bootswatch.com/4/solar/bootstrap.min.css"
};

const themeHelper = new ThemeHelper();

let added = Object.keys(themes).map(n => themeHelper.add(n, themes[n]));

Promise.all(added).then(sheets => {
  console.log(`${sheets.length} themes loaded`);
  themeHelper.theme = "materia";
});
&#13;
<h3>Click a button to select a theme</h3>

<button 
  class="btn btn-primary" 
  onclick="themeHelper.theme='materia'">Paper theme
  </button>
  
<button 
  class="btn btn-primary" 
  onclick="themeHelper.theme='flatly'">Flatly theme
</button>

<button 
  class="btn btn-primary" 
  onclick="themeHelper.theme='solar'">Solar theme
</button>
&#13;
&#13;
&#13;

不难说我是关于ES6的全部(也许我过度使用了const:)

就Vue而言,你可以制作一个包裹<select>的组件:

&#13;
&#13;
const ThemeHelper = function() {
 
  const preloadTheme = (href) => {
    let link = document.createElement('link');
    link.rel = "stylesheet";
    link.href = href;
    document.head.appendChild(link);
    
    return new Promise((resolve, reject) => {
      link.onload = e => {
        const sheet = e.target.sheet;
        sheet.disabled = true;
        resolve(sheet);
      };
      link.onerror = reject;
    });
  };
  
  const selectTheme = (themes, name) => {
    if (name && !themes[name]) {
      throw new Error(`"${name}" has not been defined as a theme.`); 
    }
    Object.keys(themes).forEach(n => themes[n].disabled = (n !== name));
  }
  
  let themes = {};

  return {
    add(name, href) { return preloadTheme(href).then(s => themes[name] = s) },
    set theme(name) { selectTheme(themes, name) },
    get theme() { return Object.keys(themes).find(n => !themes[n].disabled) }
  };
};

let app = new Vue({
  el: '#app',
  data() {
    return {
      themes: {
        flatly: "https://bootswatch.com/4/flatly/bootstrap.min.css",
        materia: "https://bootswatch.com/4/materia/bootstrap.min.css",
        solar: "https://bootswatch.com/4/solar/bootstrap.min.css"
      },
      themeHelper: new ThemeHelper(),
      loading: true,
    }
  },
  created() {
    // add/load themes
    let added = Object.keys(this.themes).map(name => {
      return this.themeHelper.add(name, this.themes[name]);
    });

    Promise.all(added).then(sheets => {
      console.log(`${sheets.length} themes loaded`);
      this.loading = false;
      this.themeHelper.theme = "flatly";
    });
  }
});
&#13;
<script src="https://unpkg.com/vue@2.5.2/dist/vue.js"></script>

<div id="app">
  <p v-if="loading">loading...</p>

  <select v-model="themeHelper.theme">
    <option v-for="(href, name) of themes" v-bind:value="name">
      {{ name }}
    </option>
  </select>
  <span>Selected: {{ themeHelper.theme }}</span>
</div>

<hr>

<h3>Select a theme above</h3>
<button class="btn btn-primary">A Button</button>
&#13;
&#13;
&#13;

我希望这对你有用,因为它对我来说很有趣!

答案 1 :(得分:0)

一种非常简单且有效的方法:只需动态更改身体的css类。

答案 2 :(得分:0)

首先,我要感谢ContinuousLoad的启发性代码段。 制作自己的主题选择器对我很有帮助。 我只是想提供一些反馈,并分享对原始代码的更改,特别是在功能preloadTheme中。最大的更改是在初始加载后删除了onload()事件侦听器,因为至少在Firefox下,每次更改link.disabled值时它将重新运行。 希望能有所帮助:)

const ThemeHelper = function() {
  const preloadTheme = href => {
    let link = document.createElement('link');
    link.rel = 'stylesheet';
    link.disabled = false;
    link.href = href;

    return new Promise((resolve, reject) => {
      link.onload = function() {
        // Remove the onload() event listener after initial load, because some
        // browsers (like Firefox) could call onload() later again when changing
        // the link.disabled value.
        link.onload = null;
        link.disabled = true;
        resolve(link);
      };
      link.onerror = event => {
        link.onerror = null;
        reject(event);
      };
      document.head.appendChild(link);
    });
  };

  const selectTheme = (themes, name) => {
    if (name && !themes[name]) {
      throw new Error(`"${name}" has not been defined as a theme.`);
    }
    Object.keys(themes).forEach(n => {
      if (n !== name && !themes[n].disabled) themes[n].disabled = true;
    });
    if (themes[name].disabled) themes[name].disabled = false;
  };

  let themes = {};

  return {
    add(name, href) {
      return preloadTheme(href).then(s => (themes[name] = s));
    },
    set theme(name) {
      selectTheme(themes, name);
    },
    get theme() {
      return Object.keys(themes).find(n => !themes[n].disabled);
    }
  };
};

let app = new Vue({
  el: '#app',
  data() {
    return {
      themes: {
        flatly: 'https://bootswatch.com/4/flatly/bootstrap.min.css',
        materia: 'https://bootswatch.com/4/materia/bootstrap.min.css',
        solar: 'https://bootswatch.com/4/solar/bootstrap.min.css'
      },
      themeHelper: new ThemeHelper(),
      loading: true
    };
  },
  created() {
    // add/load themes
    let added = Object.keys(this.themes).map(name => {
      return this.themeHelper.add(name, this.themes[name]);
    });

    Promise.all(added).then(sheets => {
      console.log(`${sheets.length} themes loaded`);
      this.loading = false;
      this.themeHelper.theme = 'flatly';
    });
  }
});
<script src="https://unpkg.com/vue@2.5.2/dist/vue.js"></script>

<div id="app">
  <p v-if="loading">loading...</p>

  <select v-model="themeHelper.theme">
    <option v-for="(href, name) of themes" v-bind:value="name">
      {{ name }}
    </option>
  </select>
  <span>Selected: {{ themeHelper.theme }}</span>
</div>

<hr>

<h3>Select a theme above</h3>
<button class="btn btn-primary">A Button</button>

答案 3 :(得分:0)

答案 4 :(得分:0)

今天,我发现了解决此问题的最简单方法,它甚至可以与SCSS一起使用(无需为每个主题使用单独的CSS,如果您的主题基于一个库并且您只想定义更改,则这一点很重要) ,但需要

  1. 为每个主题制作一个.scss / .css文件
  2. 使它们在src文件夹中的某个位置可用,例如src / bootstrap-themes / dark.scss
  3. 例如,将带有条件的.scss导入App.vue中的created:
if (Vue.$cookies.get('darkmode') === 'true') {
     import('../bootstrap-themes/dark.scss');
     this.nightmode = true;
} else {
     import('../bootstrap-themes/light.scss');
     this.nightmode = false;
}

当用户登陆页面时,我阅读了cookie,看看它们在上次离开并加载正确的scss时是否启用了夜间模式

当他们使用开关更改主题时,将调用此方法,该方法将保存cookie并重新加载页面,然后页面将读取cookie并加载正确的scss

setTheme(nightmode) {
     this.$cookies.set("darkmode", nightmode, "7d")
     this.$router.go()
}