本文主要详细介绍了,如何使用 webpack,打包多套不同主题的解决方案以及实践中所遇到的问题及解决方案。
起因
首先,简单的介绍一下什么是多主题,所谓多套主题/配色,就是我们很常见的换肤功能。换肤简单来说就是更换 css
。这是一个老生常谈的问题,具体实践请参考less 换肤功能实践。本文不在赘述。
一般实现多主题的样式文件,我们都会借用 gulp、grunt这种构建工具进行构建。但是,这样做有一个巨大的问题,就是非常不方便。我们既然已经使用了 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
。
1 2 3 4
| import './less/index.less'; + import './less/themes/green.less'; + import './less/themes/red.less'; + import './less/themes/yellow.less';
|
然后编译一下,你发现所有的样式都混在一起了。
参照文档,我们需要多次声明 ExtractTextPlugin
,以达到把不同的主题输出到不同文件的目的。这里我使用的是, loader
的 include
和 exclude
参数。在默认样式中将其他样式排除,然后每一个主题的样式,分别打包自己的样式。
最终代码的改动如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
| 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 };
|
做出以上改动之后,就可以正常的输出样式文件了。
详细的代码改动在这里,并且有详细的注释。
不让 css 注入 html
这样做之后,虽然 webpack
可以正常的编译样式文件了,但是有一个致命的问题。让我们看看现在的<head/>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <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
排除它就好了。
1 2 3 4 5
| // style import './less/index.less'; - import './less/themes/green.less'; - import './less/themes/red.less'; - import './less/themes/yellow.less';
|
1 2 3
| import './less/themes/green.less'; import './less/themes/red.less'; import './less/themes/yellow.less';
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| entry: { app: './src/index.js', + themes: './src/themes.js' }, // //... 省略没用的代码 //
new HtmlwebpackPlugin({ title: 'webpack 多主题打包演示', template: 'src/index.html', inject: true, + excludeChunks: ['themes'] })
|
但是这时候,发现多了一个 themes.bundle.js
文件。所以需要删除掉。修改 build
脚本。
1 2
| "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
。可以处理html
。HtmlWebpackHandleCssInjectPlugin.js(支持webpack4
和其他 webpack
版本)。
然后这样使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| + 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]
的值。
这个方法,既不需要任何工具,也不需要删除什么。非常完美。更改记录,完整代码
这两种方法我个人比较倾向于方法三。由于 plugin 的代码比较简单,就不做 publish 了。需要的欢迎自取。
本文章所涉及的源码。方法二和方法三在不同的分支,点击查看最终效果
最后感谢@xiyuyizhi提供的宝贵思路。
本文纯属原创,如有错误欢迎指正。