Giter VIP home page Giter VIP logo

webpack-mutiple-theme-bundle-css-demo's Introduction

webpack-mutiple-theme-bundle-css-demo

本文主要详细介绍了,如何使用 webpack,打包多套不同主题的解决方案以及实践中所遇到的问题及解决方案。

如果你只是想快速编译多套主题,请直接使用 webpack-multiple-themes-compile 库。

起因

首先,简单的介绍一下什么是多主题,所谓多套主题/配色,就是我们很常见的换肤功能。换肤简单来说就是更换 css。这是一个老生常谈的问题,具体实践请参考less换肤功能实践。本文不在赘述。 一般实现多主题的样式文件,我们都会借用 gulpgrunt这种构建工具进行构建。但是,这样做有一个巨大的问题,就是非常不方便。我们既然已经使用了 webpack 进行打包,又为什么还要使用其他的构建工具呢? 另外,还有一个巨大的弊端就是使用其他构建工具构建的 css ,是没办法支持提供的 scope功能的。这非常致命。所以到底该如何使用 webpack 进行构建呢?

大致思路

新建一些 <theme>.less文件,,使用 webpack 读取 themes目录中的样式文件,编译后输出 <theme>.css。并且首次加载时只引用默认主题文件,其他的可以到切换的时候再引入。 所以只需要解决解决编译多套 css 输出的问题和不让 css 注入 html的问题就好了。

解决编译多套 css 输出的问题

  • 建立一个初始化的项目,这个项目以react项目为例,预编译语言使用的是less。你可以随着自己的喜好进行任意选择。初始配置。然后再less文件夹下,新建一个themes目录,和多个 <theme>.less目录结构 建好之后,把所有的 文件引入 index.js中,webpack就会帮你把他们编译输出到一起了。一般情况下,extract-text-webpack-plugin 可以帮我们把样式文件抽出来,但是会帮我们把他们都放在同一个文件中。 修改index.js
import './less/index.less';
+ import './less/themes/green.less';
+ import './less/themes/red.less';
+ import './less/themes/yellow.less';

然后编译一下,你发现所有的样式都混在一起了。 混在一起的样式 参照文档,我们需要多次声明 ExtractTextPlugin,以达到把不同的主题输出到不同文件的目的。这里我使用的是, loaderincludeexclude参数。在默认样式中将其他样式排除,然后每一个主题的样式,分别打包自己的样式。 最终代码的改动如下:

const path = require('path');
+ const fs = require('fs');
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const HtmlwebpackPlugin = require('html-webpack-plugin');

const { STYLE_DEBUG } = process.env;
+ // 主题路径
+ const THEME_PATH = './src/less/themes';

const extractLess = new ExtractTextPlugin('style.[hash].css');

+ const styleLoaders = [{ loader: 'css-loader' }, { loader: 'less-loader' }];

+ const resolveToThemeStaticPath = fileName => path.resolve(THEME_PATH, fileName);
+ const themeFileNameSet = fs.readdirSync(path.resolve(THEME_PATH));
+ const themePaths = themeFileNameSet.map(resolveToThemeStaticPath);
+ const getThemeName = fileName => `theme-${path.basename(fileName, path.extname(fileName))}`;

+ // 全部 ExtractLessS 的集合
+ const themesExtractLessSet = themeFileNameSet.map(fileName => new ExtractTextPlugin(`${getThemeName(fileName)}.css`))
+ // 主题 Loader 的集合
+ const themeLoaderSet = themeFileNameSet.map((fileName, index) => {
+   return {
+     test: /\.(less|css)$/,
+     include: resolveToThemeStaticPath(fileName),
+     loader: themesExtractLessSet[index].extract({
+       use: styleLoaders
+     })
+   }
+ });


//
//..... 这里省略了
//

  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          'transform-loader?brfs', // Use browserify transforms as webpack-loader.
          'babel-loader?babelrc'
        ],
        exclude: /node_modules/
      },
      {
        test: /\.(less|css)$/,
        exclude: themePaths,
        loader: extractLess.extract({
-          use: [
-            {
-              loader: 'css-loader',
-            }, {
-              loader: 'less-loader'
-            }
-          ],  
+          use: styleLoaders,
          // use style-loader in development
          fallback: 'style-loader?{attrs:{prop: "value"}}'
        })
      },
      {
        test: /\.html$/,
        use: [
          {
            loader: 'html-loader'
          }
        ]
      },
+      ...themeLoaderSet
    ]
  },
  plugins: [
    extractLess,
+    ...themesExtractLessSet,
    new webpack.NamedModulesPlugin(),
    new HtmlwebpackPlugin({
      title: 'webpack 多主题打包演示',
      template: 'src/index.html',
      inject: true
    })
  ],
  devtool: STYLE_DEBUG === 'SOURCE' && 'source-map'
};

做出以上改动之后,就可以正常的输出样式文件了。 第一次构建

详细的代码改动在这里,并且有详细的注释。

不让 css 注入 html

这样做之后,虽然 webpack 可以正常的编译样式文件了,但是有一个致命的问题。让我们看看现在的<head/>

<head>
  <meta charset="UTF-8" >
  <title>webpack 多主题打包演示页面</title>
  <meta http-equiv="X-UA-Compatible" content="IE=edge" >
  <meta name="viewport" content="width=device-width, initial-scale=1.0" >
  <link rel="stylesheet" type="text/css" href="/resources/loading.css" >
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css" />
  <script type="text/javascript" src="//cdn.staticfile.org/babel-standalone/6.24.0/babel.min.js"></script>
  <script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=default|gated,Array.prototype.includes"></script>
<link href="/style.984c33e9f2d50d6db720.css" rel="stylesheet"><link href="/theme-green.css" rel="stylesheet"><link href="/theme-red.css" rel="stylesheet"><link href="/theme-yellow.css" rel="stylesheet"></head>

我们发现不仅注入了style.css同时注入了所有的theme.css。这显然不是我们想要的。所以有什么办法把多余的主题去掉呢?

方法一(不推荐)

node写一个脚本,读取html,然后移除。这样又与我们最开始的初衷相违背,还是借助了其他的外力。

方法二

extract-text-webpack-plugin 提供了一个 excludeChunks方法,可以用来排除 entry 。所以我们可以把所有的样式文件放入,themes.js 中然后 在 entry 中添加 themes。再使用excludeChunks排除它就好了。

  • 删除 index.js 中的样式引用。
// style
import './less/index.less';
- import './less/themes/green.less';
- import './less/themes/red.less';
- import './less/themes/yellow.less';
  • 创建themes.js
import './less/themes/green.less';
import './less/themes/red.less';
import './less/themes/yellow.less';
  • 修改 webpack.config.js
  entry: {
    app: './src/index.js',
+   themes: './src/themes.js'
  },
//
//... 省略没用的代码
//

new HtmlwebpackPlugin({
  title: 'webpack 多主题打包演示',
  template: 'src/index.html',
  inject: true,
+ excludeChunks: ['themes']
})

使用 excludeChunks方式构建 但是这时候,发现多了一个 themes.bundle.js文件。所以需要删除掉。修改 build脚本。

"build": "rm -rf dist && NODE_ENV=production webpack --mode production --progress && cp -R public/* ./dist/ "
"build": "rm -rf dist && NODE_ENV=production webpack --mode production --progress && cp -R public/* ./dist/ && && rm -rf dist/themes.bundle.js"

这样就大功告成了。更改记录完整代码

方法三

但是,加了句 rm -rf,还是感觉有点不爽。所以在仔细的阅读了extract-text-webpack-plugin文档后,我发现他提供了一个钩子函数html-webpack-plugin-after-html-processing。可以处理htmlHtmlWebpackHandleCssInjectPlugin.js支持webpack4和其他 webpack 版本)。 然后这样使用:

+ const HtmlWebpackHandleCssInjectPlugin = require('./HtmlWebpackHandleCssInjectPlugin');
//... 省略没用的代码
  plugins: [
    extractLess,
    // 将所有的 themesExtractLess 加入 plugin
    ...themesExtractLessSet,
    new webpack.NamedModulesPlugin(),
    new HtmlwebpackPlugin({
      title: 'webpack 多主题打包演示',
      template: 'src/index.html',
      inject: true
+    }),
+    new HtmlWebpackHandleCssInjectPlugin({
+      filter: (filePath) => {
+        return filePath.includes('style');
+      }
+    })
+  ],

filter 函数Array.filer用法一直。参数filePath参数给出的就是link标签中[href]的值。 这个方法,既不需要任何工具,也不需要删除什么。非常完美。更改记录,完整代码 使用 hook方式构建

这两种方法我个人比较倾向于方法三。由于 plugin 的代码比较简单,就不做 publish 了。需要的欢迎自取。 本文章所涉及的源码方法二方法三在不同的分支,点击查看最终效果

最终效果截屏

最后感谢@xiyuyizhi提供的宝贵思路。 本文纯属原创,如有错误欢迎指正。

优化与改进

上面方法存在一个比较严重的问题,就是需要在 themes 文件夹下手动建立多个主题文件。这样做一方面比较难维护,另一方面也会多很多的冗余。所以这里写了一个脚本,读取配置文件,并生成多个theme.less

最终实现

最终写了一个webpack-multiple-themes-compile库,来完成上面所有的操作。只需要简单的几行配置!

webpack-mutiple-theme-bundle-css-demo's People

Contributors

hiyangguo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

webpack-mutiple-theme-bundle-css-demo's Issues

博主,IView UI自定义主题如何使用

博主,这是我的webpack配置
我在我的工程src下建立一个themes文件夹,里面有index.js ,index.less, purple.less, red.less
目前来说不太明白您的参数里面该如何修改,谢谢。

'use strict'
const path = require('path')
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const multipleThemesCompile = require('webpack-multiple-themes-compile')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const TransferWebpackPlugin = require('transfer-webpack-plugin')
const InjectionHtmlPlugin = require('./../src/plugin/injection-html-plugin')
const {join, resolve} = require('path');

const env = require('../config/prod.env')

console.log("process.env.NODE_ENV : ",process.env.NODE_ENV)

const webpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true,
usePostCSS: true
})
},
devtool: config.build.productionSourceMap ? config.build.devtool : false,
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},
plugins: [
// http://vuejs.github.io/vue-loader/en/workflow/production.html
new webpack.DefinePlugin({
'process.env': env
}),
new UglifyJsPlugin({
uglifyOptions: {
compress: {
warnings: false
}
},
sourceMap: config.build.productionSourceMap,
parallel: true
}),
// extract css into its own file
new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css'),
// Setting the following option to false will not extract CSS from codesplit chunks.
// Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
// It's currently set to true because we are seeing that sourcemaps are included in the codesplit bundle as well when it's false,
// increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
allChunks: true,
}),
// Compress extracted CSS. We are using this plugin so that possible
// duplicated CSS from different components can be deduped.
new OptimizeCSSPlugin({
cssProcessorOptions: config.build.productionSourceMap
? { safe: true, map: { inline: false } }
: { safe: true }
}),
// generate dist index.html with correct asset hash for caching.
// you can customize output by editing /index.html
// see https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: config.build.index,
template: 'index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency',
excludeChunks: ['themes']
}),
// keep module.id stable when vendor modules does not change
new webpack.HashedModuleIdsPlugin(),
// enable scope hoisting
new webpack.optimize.ModuleConcatenationPlugin(),
// split vendor js into its own file
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks (module) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
}
}),
// extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
minChunks: Infinity
}),
// This instance extracts shared chunks from code splitted chunks and bundles them
// in a separate chunk, similar to the vendor chunk
// see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
new webpack.optimize.CommonsChunkPlugin({
name: 'app',
async: 'vendor-async',
children: true,
minChunks: 3
}),

// copy custom static assets
new CopyWebpackPlugin([
  {
    from: path.resolve(__dirname, '../static'),
    to: config.build.assetsSubDirectory,
    ignore: ['.*']
  }
]),
new InjectionHtmlPlugin({
  paths: ["../ssm/config/env.js"]
}),
new TransferWebpackPlugin([{
    from: 'aenv', to: 'config'
}], join(__dirname, './../src'))

]
})

if (config.build.productionGzip) {
const CompressionWebpackPlugin = require('compression-webpack-plugin')

webpackConfig.plugins.push(
new CompressionWebpackPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp(
'\.(' +
config.build.productionGzipExtensions.join('|') +
')$'
),
threshold: 10240,
minRatio: 0.8
})
)
}

if (config.build.bundleAnalyzerReport) {
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}

module.exports = merge(
webpackConfig,
multipleThemesCompile({
cacheDir: './src/themes',
themesConfig: {
purple: {
color: '#008000'
},
red: {
color: '#ffff00'
}
},
lessContent: 'body{color:@color}'
})
)

有几个问题想探讨一下

大佬 我现在想要达成目标是在打包时根据主题色的变量自动生成多个.css文件
看了你的demo觉得这种实现思路可取 但是有几个问题想要看看你有没有比较好的解决方案

  1. 现代的react项目一般.less文件都是分散于各个文件夹 这样你的这种实现思路就需要在每一个主题中手动引入每一个[component].less 这样显然工作量巨大并且不够优雅

  2. 当在react中使用cssModule时因为改变了文件的路径 导致类名会发生变化

目前我考虑的解决方案是在less-loader中通过控制modifyVars字段 引入不同的theme.json文件 来达到控制less变量的效果 但是出现的问题是同一个文件不能被less多次打包

关于ExtractTextPlugin 分离CSS问题

问题 : Git clone 项目后尝试代码分离,发现打包的主题 css 内容全部为空
复现 :install react-loadable, antd
步骤一
修改APP.JS
image

步骤一
webpack 添加
image

打包后theme-red.css 等css均为空, 自己研究半天都没发现原因 ,如果去掉步骤二, 虽然能看到主题存在,但是无法提炼nodemoudle 的vender.js ,希望能帮忙解答看一下

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.