├── .babelrc ├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── public └── resources │ └── loading.css ├── publish.js ├── script └── generate-themes.js ├── src ├── App.js ├── index.html ├── index.js └── less │ ├── index.less │ └── themes │ └── .gitkeep ├── static ├── fina-build.png ├── final-screenshoots.gif ├── first-build.png ├── mix-css.png ├── pallete.jpg ├── the-second-build.png └── themes.png ├── themes.config.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties", 8 | "@babel/plugin-transform-proto-to-assign", 9 | "@babel/plugin-transform-runtime", 10 | "react-hot-loader/babel", 11 | "@babel/plugin-syntax-dynamic-import", 12 | "@babel/plugin-syntax-import-meta", 13 | "@babel/plugin-proposal-json-strings", 14 | [ 15 | "@babel/plugin-proposal-decorators", 16 | { 17 | "legacy": true 18 | } 19 | ], 20 | "@babel/plugin-proposal-function-sent", 21 | "@babel/plugin-proposal-export-namespace-from", 22 | "@babel/plugin-proposal-numeric-separator", 23 | "@babel/plugin-proposal-throw-expressions", 24 | "@babel/plugin-proposal-export-default-from", 25 | "@babel/plugin-proposal-logical-assignment-operators", 26 | "@babel/plugin-proposal-optional-chaining", 27 | [ 28 | "@babel/plugin-proposal-pipeline-operator", 29 | { 30 | "proposal": "minimal" 31 | } 32 | ], 33 | "@babel/plugin-proposal-nullish-coalescing-operator", 34 | "@babel/plugin-proposal-do-expressions", 35 | "@babel/plugin-proposal-function-bind" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_style = space 9 | indent_size = 2 10 | trim_trailing_whitespace = true 11 | insert_final_newline = false 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pids 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | lib-cov 12 | coverage 13 | .nyc_output 14 | .grunt 15 | bower_components 16 | .lock-wscript 17 | build/Release 18 | node_modules/ 19 | jspm_packages/ 20 | typings/ 21 | .npm 22 | .eslintcache 23 | .node_repl_history 24 | *.tgz 25 | .yarn-integrity 26 | .env 27 | .next 28 | /dist/ 29 | # generate by script 30 | /src/less/themes/*.less 31 | /src/themes.js 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Godfery 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webpack-mutiple-theme-bundle-css-demo 2 | 本文主要详细介绍了,如何使用 [webpack][webpack],打包多套不同主题的解决方案以及实践中所遇到的问题及解决方案。 3 | 4 | > 如果你只是想快速编译多套主题,请直接使用 [webpack-multiple-themes-compile][webpack-multiple-themes-compile] 库。 5 | 6 | 7 | ## 起因 8 | 首先,简单的介绍一下什么是多主题,所谓多套主题/配色,就是我们很常见的换肤功能。换肤简单来说就是更换 `css`。这是一个老生常谈的问题,具体实践请参考[less换肤功能实践][less-multiple-color-theme-realize]。本文不在赘述。 9 | 一般实现多主题的样式文件,我们都会借用 [gulp][gulp]、[grunt][grunt]这种构建工具进行构建。但是,这样做有一个巨大的问题,就是非常不方便。我们既然已经使用了 `webpack` 进行打包,又为什么还要使用其他的构建工具呢? 10 | 另外,还有一个巨大的弊端就是使用其他构建工具构建的 css ,是没办法支持提供的 [scope][scope]功能的。这非常致命。所以到底该如何使用 webpack 进行构建呢? 11 | 12 | ## 大致思路 13 | 新建一些 `.less`文件,,使用 `webpack` 读取 `themes`目录中的样式文件,编译后输出 `.css`。并且首次加载时只引用默认主题文件,其他的可以到切换的时候再引入。 14 | 所以只需要解决解决编译多套 css 输出的问题和不让 css 注入 html的问题就好了。 15 | 16 | ## 解决编译多套 css 输出的问题 17 | - 建立一个[初始化的项目][initial-project],这个项目以`react`项目为例,预编译语言使用的是`less`。你可以随着自己的喜好进行任意选择。[初始配置][init-webpack-config]。然后再`less`文件夹下,新建一个`themes`目录,和多个 `.less`。 18 | ![目录结构](/static/themes.png) 19 | 建好之后,把所有的 文件引入 `index.js`中,`webpack`就会帮你把他们编译输出到一起了。一般情况下,[extract-text-webpack-plugin][extract-text-webpack-plugin] 可以帮我们把样式文件抽出来,但是会帮我们把他们都放在同一个文件中。 20 | 修改`index.js`。 21 | ```diff 22 | import './less/index.less'; 23 | + import './less/themes/green.less'; 24 | + import './less/themes/red.less'; 25 | + import './less/themes/yellow.less'; 26 | ``` 27 | 然后编译一下,你发现所有的样式都混在一起了。 28 | ![混在一起的样式](/static/mix-css.png) 29 | 参照文档,我们需要多次声明 `ExtractTextPlugin`,以达到把不同的主题输出到不同文件的目的。这里我使用的是, `loader` 的 `include` 和 `exclude`参数。在默认样式中将其他样式排除,然后每一个主题的样式,分别打包自己的样式。 30 | 最终代码的改动如下: 31 | ```diff 32 | const path = require('path'); 33 | + const fs = require('fs'); 34 | const webpack = require('webpack'); 35 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 36 | const HtmlwebpackPlugin = require('html-webpack-plugin'); 37 | 38 | const { STYLE_DEBUG } = process.env; 39 | + // 主题路径 40 | + const THEME_PATH = './src/less/themes'; 41 | 42 | const extractLess = new ExtractTextPlugin('style.[hash].css'); 43 | 44 | + const styleLoaders = [{ loader: 'css-loader' }, { loader: 'less-loader' }]; 45 | 46 | + const resolveToThemeStaticPath = fileName => path.resolve(THEME_PATH, fileName); 47 | + const themeFileNameSet = fs.readdirSync(path.resolve(THEME_PATH)); 48 | + const themePaths = themeFileNameSet.map(resolveToThemeStaticPath); 49 | + const getThemeName = fileName => `theme-${path.basename(fileName, path.extname(fileName))}`; 50 | 51 | + // 全部 ExtractLessS 的集合 52 | + const themesExtractLessSet = themeFileNameSet.map(fileName => new ExtractTextPlugin(`${getThemeName(fileName)}.css`)) 53 | + // 主题 Loader 的集合 54 | + const themeLoaderSet = themeFileNameSet.map((fileName, index) => { 55 | + return { 56 | + test: /\.(less|css)$/, 57 | + include: resolveToThemeStaticPath(fileName), 58 | + loader: themesExtractLessSet[index].extract({ 59 | + use: styleLoaders 60 | + }) 61 | + } 62 | + }); 63 | 64 | 65 | // 66 | //..... 这里省略了 67 | // 68 | 69 | module: { 70 | rules: [ 71 | { 72 | test: /\.js$/, 73 | use: [ 74 | 'transform-loader?brfs', // Use browserify transforms as webpack-loader. 75 | 'babel-loader?babelrc' 76 | ], 77 | exclude: /node_modules/ 78 | }, 79 | { 80 | test: /\.(less|css)$/, 81 | exclude: themePaths, 82 | loader: extractLess.extract({ 83 | - use: [ 84 | - { 85 | - loader: 'css-loader', 86 | - }, { 87 | - loader: 'less-loader' 88 | - } 89 | - ], 90 | + use: styleLoaders, 91 | // use style-loader in development 92 | fallback: 'style-loader?{attrs:{prop: "value"}}' 93 | }) 94 | }, 95 | { 96 | test: /\.html$/, 97 | use: [ 98 | { 99 | loader: 'html-loader' 100 | } 101 | ] 102 | }, 103 | + ...themeLoaderSet 104 | ] 105 | }, 106 | plugins: [ 107 | extractLess, 108 | + ...themesExtractLessSet, 109 | new webpack.NamedModulesPlugin(), 110 | new HtmlwebpackPlugin({ 111 | title: 'webpack 多主题打包演示', 112 | template: 'src/index.html', 113 | inject: true 114 | }) 115 | ], 116 | devtool: STYLE_DEBUG === 'SOURCE' && 'source-map' 117 | }; 118 | ``` 119 | 120 | 做出以上改动之后,就可以正常的输出样式文件了。 121 | ![第一次构建](/static/first-build.png) 122 | 123 | [详细的代码改动][git-commit-extract-css]在这里,并且有详细的注释。 124 | 125 | ## 不让 css 注入 html 126 | 这样做之后,虽然 `webpack` 可以正常的编译样式文件了,但是有一个致命的问题。让我们看看现在的`` 127 | ```html 128 | 129 | 130 | webpack 多主题打包演示页面 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | ``` 139 | 我们发现不仅注入了`style.css`同时注入了所有的`theme.css`。这显然不是我们想要的。所以有什么办法把多余的主题去掉呢? 140 | ### 方法一(不推荐) 141 | 用`node`写一个脚本,读取`html`,然后移除。这样又与我们最开始的初衷相违背,还是借助了其他的外力。 142 | ### 方法二 143 | [extract-text-webpack-plugin][extract-text-webpack-plugin] 提供了一个 `excludeChunks`方法,可以用来排除 `entry` 。所以我们可以把所有的样式文件放入,`themes.js` 中然后 在 entry 中添加 `themes`。再使用`excludeChunks`排除它就好了。 144 | - 删除 `index.js` 中的样式引用。 145 | ```diff 146 | // style 147 | import './less/index.less'; 148 | - import './less/themes/green.less'; 149 | - import './less/themes/red.less'; 150 | - import './less/themes/yellow.less'; 151 | ``` 152 | - 创建`themes.js` 153 | ```javascript 154 | import './less/themes/green.less'; 155 | import './less/themes/red.less'; 156 | import './less/themes/yellow.less'; 157 | ``` 158 | - 修改 `webpack.config.js` 159 | ```diff 160 | entry: { 161 | app: './src/index.js', 162 | + themes: './src/themes.js' 163 | }, 164 | // 165 | //... 省略没用的代码 166 | // 167 | 168 | new HtmlwebpackPlugin({ 169 | title: 'webpack 多主题打包演示', 170 | template: 'src/index.html', 171 | inject: true, 172 | + excludeChunks: ['themes'] 173 | }) 174 | ``` 175 | ![使用 excludeChunks方式构建](/static/first-build.png) 176 | 但是这时候,发现多了一个 `themes.bundle.js`文件。所以需要删除掉。修改 `build`脚本。 177 | ```diff 178 | "build": "rm -rf dist && NODE_ENV=production webpack --mode production --progress && cp -R public/* ./dist/ " 179 | "build": "rm -rf dist && NODE_ENV=production webpack --mode production --progress && cp -R public/* ./dist/ && && rm -rf dist/themes.bundle.js" 180 | ``` 181 | 这样就大功告成了。[更改记录][exclude-css-file],[完整代码](https://github.com/hiyangguo/webpack-mutiple-theme-bundle-css-demo) 182 | 183 | ### 方法三 184 | 但是,加了句 `rm -rf`,还是感觉有点不爽。所以在仔细的阅读了[extract-text-webpack-plugin][extract-text-webpack-plugin]文档后,我发现他提供了一个钩子函数`html-webpack-plugin-after-html-processing`。可以处理`html`。[HtmlWebpackHandleCssInjectPlugin.js](html-webpack-handle-css-inject-plugin-js)(**支持`webpack4`和其他 `webpack` 版本**)。 185 | 然后这样使用: 186 | ```diff 187 | + const HtmlWebpackHandleCssInjectPlugin = require('./HtmlWebpackHandleCssInjectPlugin'); 188 | //... 省略没用的代码 189 | plugins: [ 190 | extractLess, 191 | // 将所有的 themesExtractLess 加入 plugin 192 | ...themesExtractLessSet, 193 | new webpack.NamedModulesPlugin(), 194 | new HtmlwebpackPlugin({ 195 | title: 'webpack 多主题打包演示', 196 | template: 'src/index.html', 197 | inject: true 198 | + }), 199 | + new HtmlWebpackHandleCssInjectPlugin({ 200 | + filter: (filePath) => { 201 | + return filePath.includes('style'); 202 | + } 203 | + }) 204 | + ], 205 | ``` 206 | `filter` 函数`Array.filer`用法一直。参数`filePath`参数给出的就是`link`标签中`[href]`的值。 207 | 这个方法,既不需要任何工具,也不需要删除什么。非常完美。[更改记录][use-plugin],[完整代码](https://github.com/hiyangguo/webpack-mutiple-theme-bundle-css-demo/tree/plugin) 208 | ![使用 hook方式构建](/static/fina-build.png) 209 | 210 | 211 | 这两种方法我个人比较倾向于方法三。由于 plugin 的代码比较简单,就不做 publish 了。需要的欢迎自取。 212 | 本文章所涉及的[源码][source]。[方法二](#方法二)和[方法三](#方法三)在不同的分支,[点击查看最终效果][final]。 213 | 214 | ![最终效果截屏](/static/final-screenshoots.gif) 215 | 216 | 最后感谢[@xiyuyizhi][xiyuyizhi]提供的[宝贵思路](https://github.com/xiyuyizhi/notes/blob/master/js/theme.md)。 217 | 本文纯属原创,如有错误欢迎指正。 218 | 219 | ## 优化与改进 220 | 上面方法存在一个比较严重的问题,就是需要在 `themes` 文件夹下手动建立多个主题文件。这样做一方面比较难维护,另一方面也会多很多的冗余。所以这里写了一个[脚本][generate-themes-script],读取配置文件,并生成多个`theme.less`。 221 | 222 | ## 最终实现 223 | 最终写了一个[webpack-multiple-themes-compile][webpack-multiple-themes-compile]库,来完成上面所有的操作。只需要简单的几行配置! 224 | 225 | 226 | [webpack]:https://webpack.js.org/ 227 | [less-multiple-color-theme-realize]:https://hiyangguo.github.io/2017/03/21/less-multiple-color-theme-realize/ 228 | [gulp]:https://www.gulpjs.com.cn/ 229 | [grunt]:https://gruntjs.com/ 230 | [scope]:https://www.npmjs.com/package/css-loader#scope 231 | [xiyuyizhi]:https://github.com/xiyuyizhi 232 | [webpack构建下换肤功能的实现思路]:https://juejin.im/post/5a1b8a4df265da430f31d03e 233 | [initial-project]:https://github.com/hiyangguo/webpack-mutiple-theme-bundle-css-demo/tree/v0.0.1 234 | [init-webpack-config]:https://github.com/hiyangguo/webpack-mutiple-theme-bundle-css-demo/blob/v0.0.1/webpack.config.js 235 | [extract-text-webpack-plugin]:https://www.npmjs.com/package/extract-text-webpack-plugin 236 | [git-commit-extract-css]:https://github.com/hiyangguo/webpack-mutiple-theme-bundle-css-demo/commit/1509f94524626331731893253aec2d2ce6936911 237 | [html-webpack-handle-css-inject-plugin-js]:https://gist.github.com/hiyangguo/41b9d5fb9a164b1043ff1e5fbb7cdceb#file-htmlwebpackhandlecssinjectplugin-js 238 | [source]:https://github.com/hiyangguo/webpack-mutiple-theme-bundle-css-demo 239 | [exclude-css-file]:https://github.com/hiyangguo/webpack-mutiple-theme-bundle-css-demo/commit/90c1f24af526cd6d48bf6b095500e4ffa5c7f0e6 240 | [use-plugin]:https://github.com/hiyangguo/webpack-mutiple-theme-bundle-css-demo/commit/2688cead3298b65a4e5871ead1d261b008b545a8 241 | [final]:https://hiyangguo.github.io/webpack-mutiple-theme-bundle-css-demo/ 242 | [generate-themes-script]:https://github.com/hiyangguo/webpack-mutiple-theme-bundle-css-demo/blob/master/script/generate-themes.js 243 | [webpack-multiple-themes-compile]:https://github.com/rsuite/webpack-multiple-themes-compile/blob/master/README_zh.md 244 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-mutiple-theme-bundle-css-demo", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "A webpack mutiple theme bundle css demo.", 6 | "main": "index.js", 7 | "scripts": { 8 | "generate-themes": "rm -rf src/less/themes/*.less && node script/generate-themes.js", 9 | "dev": "npm run generate-themes && NODE_ENV=dev webpack-dev-server --mode development --progress --hot", 10 | "build": "npm run generate-themes && rm -rf dist && NODE_ENV=production webpack --mode production --progress && cp -R public/* ./dist/ && rm -rf dist/themes.bundle.js", 11 | "publish": "npm run build && node publish.js" 12 | }, 13 | "author": "Godfery ", 14 | "license": "MIT", 15 | "dependencies": { 16 | "@babel/core": "^7.0.0", 17 | "@babel/plugin-proposal-class-properties": "^7.0.0", 18 | "@babel/plugin-proposal-decorators": "^7.0.0", 19 | "@babel/plugin-proposal-do-expressions": "^7.0.0", 20 | "@babel/plugin-proposal-export-default-from": "^7.0.0", 21 | "@babel/plugin-proposal-export-namespace-from": "^7.0.0", 22 | "@babel/plugin-proposal-function-bind": "^7.0.0", 23 | "@babel/plugin-proposal-function-sent": "^7.0.0", 24 | "@babel/plugin-proposal-json-strings": "^7.0.0", 25 | "@babel/plugin-proposal-logical-assignment-operators": "^7.0.0", 26 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.0.0", 27 | "@babel/plugin-proposal-numeric-separator": "^7.0.0", 28 | "@babel/plugin-proposal-optional-chaining": "^7.0.0", 29 | "@babel/plugin-proposal-pipeline-operator": "^7.0.0", 30 | "@babel/plugin-proposal-throw-expressions": "^7.0.0", 31 | "@babel/plugin-syntax-dynamic-import": "^7.0.0", 32 | "@babel/plugin-syntax-import-meta": "^7.0.0", 33 | "@babel/plugin-transform-proto-to-assign": "^7.0.0", 34 | "@babel/plugin-transform-runtime": "^7.0.0", 35 | "@babel/preset-env": "^7.0.0", 36 | "@babel/preset-react": "^7.0.0", 37 | "@babel/runtime": "^7.1.2", 38 | "babel-loader": "^8.0.0", 39 | "babel-plugin-import": "^1.6.7", 40 | "brfs": "^1.6.1", 41 | "css-loader": "^0.28.11", 42 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 43 | "gh-pages": "^2.0.1", 44 | "html-webpack-plugin": "^3.0.6", 45 | "less": "^3.8.1", 46 | "less-loader": "^4.1.0", 47 | "lodash": "^4.17.10", 48 | "react": "^16.2.0", 49 | "react-dom": "^16.3.2", 50 | "react-hot-loader": "^4.0.0", 51 | "style-loader": "^0.20.3", 52 | "webpack": "^4.5.0", 53 | "webpack-bundle-analyzer": "^2.11.1", 54 | "webpack-cli": "^3.1.2", 55 | "webpack-dev-server": "^3.1.10" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /public/resources/loading.css: -------------------------------------------------------------------------------- 1 | body.body-loading{background:#2196F3}.sk-cube-grid{width:40px;height:40px;top:50%;position:absolute;left:50%;margin-top:-20px;margin-left:-20px}.sk-cube-grid .sk-cube{width:33%;height:33%;background-color:#fff;float:left;-webkit-animation:sk-cubeGridScaleDelay 1.3s infinite ease-in-out;animation:sk-cubeGridScaleDelay 1.3s infinite ease-in-out}.sk-cube-grid .sk-cube1{-webkit-animation-delay:.2s;animation-delay:.2s}.sk-cube-grid .sk-cube2{-webkit-animation-delay:.3s;animation-delay:.3s}.sk-cube-grid .sk-cube3{-webkit-animation-delay:.4s;animation-delay:.4s}.sk-cube-grid .sk-cube4{-webkit-animation-delay:.1s;animation-delay:.1s}.sk-cube-grid .sk-cube5{-webkit-animation-delay:.2s;animation-delay:.2s}.sk-cube-grid .sk-cube6{-webkit-animation-delay:.3s;animation-delay:.3s}.sk-cube-grid .sk-cube7{-webkit-animation-delay:0s;animation-delay:0s}.sk-cube-grid .sk-cube8{-webkit-animation-delay:.1s;animation-delay:.1s}.sk-cube-grid .sk-cube9{-webkit-animation-delay:.2s;animation-delay:.2s}@-webkit-keyframes sk-cubeGridScaleDelay{0%,100%,70%{-webkit-transform:scale3D(1,1,1);transform:scale3D(1,1,1)}35%{-webkit-transform:scale3D(0,0,1);transform:scale3D(0,0,1)}}@keyframes sk-cubeGridScaleDelay{0%,100%,70%{-webkit-transform:scale3D(1,1,1);transform:scale3D(1,1,1)}35%{-webkit-transform:scale3D(0,0,1);transform:scale3D(0,0,1)}} -------------------------------------------------------------------------------- /publish.js: -------------------------------------------------------------------------------- 1 | var ghpages = require('gh-pages'); 2 | console.log('Publish...'); 3 | ghpages.publish('dist', function(err) { 4 | err && console.log(err); 5 | !err && console.log('Published'); 6 | }); -------------------------------------------------------------------------------- /script/generate-themes.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const { join } = require('path'); 3 | const { writeFileSync } = require('fs'); 4 | const OUTPUT_DIR = '../src/less/themes'; 5 | const THEMES_CONFIG = require('../themes.config'); 6 | const CONTENT = `@import "../index";` 7 | const HEADER = '// Generate by Script, Config file is themes.config.js'; 8 | 9 | const write = _.partial(writeFileSync, _, _, 'utf8'); 10 | 11 | const getModifyVariablesContent = (variableConfig) => { 12 | const declaredContent = _.map(_.entries(variableConfig), ([key, value]) => `@${key}:${value};`); 13 | return declaredContent.join('\r'); 14 | }; 15 | 16 | _.forEach(_.entries(THEMES_CONFIG), ([theme, config]) => { 17 | const fileName = `${theme}.less`; 18 | const modifyVariablesContent = getModifyVariablesContent(config); 19 | const content = `${CONTENT} 20 | 21 | ${HEADER} 22 | ${modifyVariablesContent}`; 23 | const outPutFilePath = join(__dirname, OUTPUT_DIR, fileName); 24 | let flag = true; 25 | try { 26 | write(outPutFilePath, content); 27 | } catch (e) { 28 | flag = false; 29 | } 30 | console.log(`Generate ${outPutFilePath} ${flag ? 'Succeed' : 'Failed'}.`); 31 | }); 32 | 33 | const jsContent = _.map(_.keys(THEMES_CONFIG), theme => `import './less/themes/${theme}.less';`).join('\r'); 34 | 35 | write(join(__dirname, '../src/themes.js'), `${HEADER} 36 | ${jsContent}`); 37 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { hot } from 'react-hot-loader'; 3 | 4 | 5 | class App extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | themes: process.env.themes, 10 | currentLink: null 11 | } 12 | } 13 | 14 | remove = el => el && el.parentNode.removeChild(el) 15 | 16 | changeTheme = (theme) => { 17 | const { currentLink } = this.state; 18 | if (theme === (currentLink && currentLink.dataset.theme)) { 19 | return; 20 | } 21 | const link = document.createElement('link'); 22 | link.rel = 'stylesheet'; 23 | link.href = `./theme-${theme}.css`; 24 | link.dataset.theme = theme; 25 | document.head.appendChild(link); 26 | link.onload = () => { 27 | this.removeTheme(); 28 | this.setState({ 29 | currentLink: link 30 | }); 31 | } 32 | } 33 | 34 | removeTheme = () => { 35 | const { currentLink } = this.state; 36 | this.remove(currentLink); 37 | } 38 | 39 | resetTheme = () => { 40 | this.removeTheme(); 41 | this.setState({ 42 | currentLink: null 43 | }); 44 | } 45 | 46 | render() { 47 | const { themes } = this.state; 48 | return ( 49 |
50 |

点击按钮切换主题

51 | 52 | { 53 | themes.map(theme => ( 54 | 55 | )) 56 | } 57 |
58 | ); 59 | } 60 | } 61 | 62 | export default hot(module)(App); 63 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | webpack 多主题打包演示页面 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | // style 5 | import './less/index.less'; 6 | 7 | import App from './App'; 8 | 9 | const hotRender = Component => { 10 | ReactDOM.render(, document.getElementById('root')); 11 | }; 12 | 13 | document.body.className = ''; 14 | hotRender(App); 15 | -------------------------------------------------------------------------------- /src/less/index.less: -------------------------------------------------------------------------------- 1 | @base-color: #03a9f4; 2 | 3 | body { 4 | background-color: @base-color; 5 | transition: background-color .3s linear; 6 | } -------------------------------------------------------------------------------- /src/less/themes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiyangguo/webpack-mutiple-theme-bundle-css-demo/44f93a6a4a53e4dafec70143b455ce4e1965a2cf/src/less/themes/.gitkeep -------------------------------------------------------------------------------- /static/fina-build.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiyangguo/webpack-mutiple-theme-bundle-css-demo/44f93a6a4a53e4dafec70143b455ce4e1965a2cf/static/fina-build.png -------------------------------------------------------------------------------- /static/final-screenshoots.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiyangguo/webpack-mutiple-theme-bundle-css-demo/44f93a6a4a53e4dafec70143b455ce4e1965a2cf/static/final-screenshoots.gif -------------------------------------------------------------------------------- /static/first-build.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiyangguo/webpack-mutiple-theme-bundle-css-demo/44f93a6a4a53e4dafec70143b455ce4e1965a2cf/static/first-build.png -------------------------------------------------------------------------------- /static/mix-css.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiyangguo/webpack-mutiple-theme-bundle-css-demo/44f93a6a4a53e4dafec70143b455ce4e1965a2cf/static/mix-css.png -------------------------------------------------------------------------------- /static/pallete.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiyangguo/webpack-mutiple-theme-bundle-css-demo/44f93a6a4a53e4dafec70143b455ce4e1965a2cf/static/pallete.jpg -------------------------------------------------------------------------------- /static/the-second-build.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiyangguo/webpack-mutiple-theme-bundle-css-demo/44f93a6a4a53e4dafec70143b455ce4e1965a2cf/static/the-second-build.png -------------------------------------------------------------------------------- /static/themes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiyangguo/webpack-mutiple-theme-bundle-css-demo/44f93a6a4a53e4dafec70143b455ce4e1965a2cf/static/themes.png -------------------------------------------------------------------------------- /themes.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | green: { 3 | 'base-color': '#4caf50' 4 | }, 5 | red: { 6 | 'base-color': '#F44336' 7 | }, 8 | yellow: { 9 | 'base-color': '#ffeb3b' 10 | }, 11 | purple: { 12 | 'base-color': '#673ab7' 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const webpack = require('webpack'); 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 5 | const HtmlwebpackPlugin = require('html-webpack-plugin'); 6 | 7 | const { NODE_ENV } = process.env; 8 | 9 | const isDev = NODE_ENV === 'dev'; 10 | 11 | // 主题路径 12 | const THEME_PATH = './src/less/themes'; 13 | 14 | const extractLess = new ExtractTextPlugin('style.[hash].css'); 15 | 16 | const styleLoaders = [{ loader: 'css-loader' }, { loader: 'less-loader' }]; 17 | 18 | const resolveToThemeStaticPath = fileName => path.resolve(THEME_PATH, fileName); 19 | const themeFileNameSet = fs.readdirSync(path.resolve(THEME_PATH)).filter(fileName => /\.less/.test(fileName)); 20 | const themePaths = themeFileNameSet.map(resolveToThemeStaticPath); 21 | const getThemeName = fileName => `theme-${path.basename(fileName, path.extname(fileName))}`; 22 | 23 | // 全部 ExtractLessS 的集合 24 | const themesExtractLessSet = themeFileNameSet.map(fileName => new ExtractTextPlugin(`${getThemeName(fileName)}.css`)) 25 | // 主题 Loader 的集合 26 | const themeLoaderSet = themeFileNameSet.map((fileName, index) => { 27 | return { 28 | test: /\.(less|css)$/, 29 | include: resolveToThemeStaticPath(fileName), 30 | loader: themesExtractLessSet[index].extract({ 31 | use: styleLoaders 32 | }) 33 | } 34 | }); 35 | 36 | 37 | module.exports = { 38 | devServer: { 39 | contentBase: path.join(__dirname, 'public'), 40 | disableHostCheck: true, 41 | historyApiFallback: true, 42 | compress: true, 43 | host: '0.0.0.0', 44 | port: 3201 45 | }, 46 | entry: { 47 | app: './src/index.js', 48 | themes: './src/themes.js' 49 | }, 50 | output: { 51 | filename: '[name].bundle.js?[hash]', 52 | path: path.resolve(__dirname, 'dist'), 53 | publicPath: isDev ? '/' : './' 54 | }, 55 | module: { 56 | rules: [ 57 | { 58 | test: /\.js$/, 59 | use: ['babel-loader?babelrc'], 60 | exclude: /node_modules/ 61 | }, 62 | { 63 | test: /\.(less|css)$/, 64 | exclude: themePaths, 65 | loader: extractLess.extract({ 66 | use: styleLoaders, 67 | // use style-loader in development 68 | fallback: 'style-loader?{attrs:{prop: "value"}}' 69 | }) 70 | }, 71 | // 将Loader 的集合,加入 rules 72 | ...themeLoaderSet 73 | ] 74 | }, 75 | plugins: [ 76 | extractLess, 77 | // 将所有的 themesExtractLess 加入 plugin 78 | ...themesExtractLessSet, 79 | new webpack.NamedModulesPlugin(), 80 | new HtmlwebpackPlugin({ 81 | title: 'webpack 多主题打包演示', 82 | template: 'src/index.html', 83 | inject: true, 84 | excludeChunks: ['themes'] 85 | }), 86 | new webpack.DefinePlugin({ 87 | 'process.env.themes': JSON.stringify(themeFileNameSet.map(fileName => fileName.replace('.less', ''))) 88 | }) 89 | ] 90 | }; 91 | --------------------------------------------------------------------------------