哈Ha!
不久前,我意识到在所有应用程序中使用CSS会对开发人员和用户造成痛苦。
我要解决的问题是,在正确使用样式的过程中出现了一堆奇怪的代码和陷阱。
CSS问题
在我所做的React和Vue项目中,样式的方法大致相同。 该项目由webpack组装,从主入口点导入一个CSS文件。 此文件将使用BEM命名类的其余CSS文件导入其内部。
styles/ indes.css blocks/ apps-banner.css smart-list.css ...
熟悉吗? 我几乎在所有地方都使用了此实现。 一切都很好,直到其中一个站点发展到一种状态问题,使我的眼睛开始大为恼火。
1.热装问题相互之间导入样式是通过postcss插件或手写笔加载器进行的。
要注意的是:
当我们通过postcss或stylus-loader插件解决导入问题时,输出为一个大CSS文件。 现在,即使其中一个样式表稍有更改,所有CSS文件也将被再次处理。
它确实杀死了热重装的速度:处理大约950 KB的手写笔文件大约需要4秒钟。
关于CSS加载器的注意事项如果通过css-loader解决了导入CSS文件的问题,则不会出现这样的问题:
css-loader将CSS变成JavaScript。 它将用require替换所有样式导入。 然后,更改一个CSS文件不会影响其他文件,并且热重新加载会很快发生。
到css-loader
@import './test.css'; html, body { margin: 0; padding: 0; width: 100%; height: 100%; } body { background-color: red; }
之后
2.代码拆分问题从单独的文件夹加载样式时,我们不知道每种样式的使用上下文。 使用这种方法,无法将CSS分成多个部分并根据需要加载它们。
3.很棒的CSS类名每个BEM类名称如下所示:block-name__element-name。 如此长的名称会严重影响最终的CSS文件大小:例如,在Habr网站上,CSS类名占样式文件大小的36%。
Google意识到了这个问题,并且长期以来一直在其所有项目中使用名称最小化:
一块google.com所有这些问题使我井然有序,我最终决定结束这些问题,并取得理想的结果。
决策选择
为了摆脱上述所有问题,我找到了两个解决方案:JS中的CSS(样式组件)和CSS模块。
我没有看到这些解决方案中的严重缺陷,但最终由于以下几个原因,我选择使用CSS模块:
- 您可以将CSS放在单独的文件中,以分别缓存JS和CSS。
- 烧结样式的更多选项。
- 使用CSS文件更为常见。
做出了选择,是时候开始做饭了!
基本设定
稍微配置一下webpack。 添加css-loader并在其中启用CSS模块:
module.exports = { module: { rules: [ { test: /\.css$/, use: [ 'style-loader', { loader: 'css-loader', options: { modules: true, } }, ], }, ], }, };
现在,我们将CSS文件传播到包含组件的文件夹中。 在每个组件内部,我们导入必要的样式。
project/ components/ CoolComponent/ index.js index.css
.contentWrapper { padding: 8px 16px; background-color: rgba(45, 45, 45, .3); } .title { font-size: 14px; font-weight: bold; } .text { font-size: 12px; }
import React from 'react'; import styles from './index.css'; export default ({ text }) => ( <div className={styles.contentWrapper}> <div className={styles.title}> Weird title </div> <div className={styles.text}> {text} </div> </div> );
既然我们已经破坏了CSS文件,热重装将只处理对一个文件的更改。 问题1解决了,干杯!
将CSS分成大块
当一个项目有很多页面,而客户端只需要其中一个页面时,将所有数据抽出是没有意义的。 React为此提供了一个很棒的react-loadable库。 它允许您创建一个组件,该组件可以根据需要动态下载我们需要的文件。
import Loadable from 'react-loadable'; import Loading from 'path/to/Loading'; export default Loadable({ loader: () => import('path/to/CoolComponent'), loading: Loading, });
Webpack将把CoolComponent组件转换为一个单独的JS文件(块),该文件将在呈现AsyncCoolComponent时下载。
同时,CoolComponent包含自己的样式。 到目前为止,CSS都以JS字符串形式存在于其中,并使用style-loader作为样式插入。 但是为什么不将样式切成单独的文件呢?
我们将为主文件和每个块创建自己的CSS文件。
安装mini-css-extract-plugin并与webpack配置结合使用:
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const isDev = process.env.NODE_ENV === 'development'; module.exports = { module: { rules: [ { test: /\.css$/, use: [ (isDev ? 'style-loader' : MiniCssExtractPlugin.loader), { loader: 'css-loader', options: { modules: true, }, }, ], }, ], }, plugins: [ ...(isDev ? [] : [ new MiniCssExtractPlugin({ filename: '[name].[contenthash].css', chunkFilename: '[name].[contenthash].css', }), ]), ], };
仅此而已! 让我们以生产模式组装项目,打开浏览器并查看网络选项卡:
// GET /main.aff4f72df3711744eabe.css GET /main.43ed5fc03ceb844eab53.js // CoolComponent , JS CSS GET /CoolComponent.3eaa4773dca4fffe0956.css GET /CoolComponent.2462bbdbafd820781fae.js
问题2已经结束。
我们缩小CSS类
Css-loader在内部更改类名,并返回一个变量,该变量具有本地类名到全局的映射。
完成基本设置后,css-loader会根据文件的名称和位置生成一个长哈希。
在浏览器中,我们的CoolComponent现在如下所示:
<div class="rs2inRqijrGnbl0txTQ8v"> <div class="_2AU-QBWt5K2v7J1vRT0hgn"> Weird title </div> <div class="_1DaTAH8Hgn0BQ4H13yRwQ0"> Lorem ipsum dolor sit amet consectetur. </div> </div>
当然,这对我们来说还不够。
在开发过程中,必须有名称来查找原始样式。 在生产模式下,应最小化类名。
Css-loader允许您通过选项localIdentName和getLocalIdent自定义类名称的更改。 在开发模式下,我们将设置描述性的localIdentName-'[path] _ [name] _ [local]',在生产模式下,我们将创建一个将类名最小化的函数:
const getScopedName = require('path/to/getScopedName'); const isDev = process.env.NODE_ENV === 'development'; module.exports = { module: { rules: [ { test: /\.css$/, use: [ (isDev ? 'style-loader' : MiniCssExtractPlugin.loader), { loader: 'css-loader', options: { modules: true, ...(isDev ? { localIdentName: '[path]_[name]_[local]', } : { getLocalIdent: (context, localIdentName, localName) => ( getScopedName(localName, context.resourcePath) ), }), }, }, ], }, ], }, };
在这里,我们正在开发漂亮的视觉名称:
<div class="src-components-ErrorNotification-_index_content-wrapper"> <div class="src-components-ErrorNotification-_index_title"> Weird title </div> <div class="src-components-ErrorNotification-_index_text"> Lorem ipsum dolor sit amet consectetur. </div> </div>
并在生产中减少班级:
<div class="e_f"> <div class="e_g"> Weird title </div> <div class="e_h"> Lorem ipsum dolor sit amet consectetur. </div> </div>
第三个问题被克服。
删除不必要的缓存失效
使用上述的类最小化技术,尝试多次构建项目。 注意文件缓存:
/* */ app.bf70bcf8d769b1a17df1.js app.db3d0bd894d38d036117.css /* */ app.1f296b75295ada5a7223.js app.eb2519491a5121158bd2.css
似乎在每个新构建之后,我们都使缓存无效。 怎么会这样
问题在于webpack无法保证文件的处理顺序。 也就是说,CSS文件将以不可预测的顺序处理,对于具有不同程序集的相同类名称,将生成不同的最小名称。
为了克服这个问题,让我们在程序集之间保存有关生成的类名的数据。 稍微更新getScopedName.js文件:
const incstr = require('incstr');
generatorHelpers.js文件的实现并不重要,但是如果您有兴趣,这里是我的:
generatorHelpers.js const fs = require('fs'); const path = require('path'); const getGeneratorDataPath = generatorIdentifier => ( path.resolve(__dirname, `meta/${generatorIdentifier}.json`) ); const getGeneratorData = (generatorIdentifier) => { const path = getGeneratorDataPath(generatorIdentifier); if (fs.existsSync(path)) { return require(path); } return {}; }; const saveGeneratorData = (generatorIdentifier, uniqIds) => { const path = getGeneratorDataPath(generatorIdentifier); const data = JSON.stringify(uniqIds, null, 2); fs.writeFileSync(path, data, 'utf-8'); }; module.exports = { getGeneratorData, saveGeneratorData, };
各个版本之间的缓存已相同,一切都很好。 另外一点对我们有利!
删除运行时变量
由于我决定做出更好的决定,因此最好通过类映射来删除此变量,因为我们在编译阶段拥有所有必需的数据。
Babel-plugin-react-css-modules将对此提供帮助。 在编译时,它:
- 将在文件中找到CSS导入。
- 它将打开此CSS文件,并像css-loader一样更改CSS类名称。
- 它将找到具有styleName属性的JSX节点。
- 将styleName中的本地类名称替换为全局名称。
设置此插件。 让我们玩babel配置:
更新我们的JSX文件:
import React from 'react'; import './index.css'; export default ({ text }) => ( <div styleName="content-wrapper"> <div styleName="title"> Weird title </div> <div styleName="text"> {text} </div> </div> );
因此,我们不再使用显示样式名称的变量,而现在还没有它!
...还是在那里?
我们将收集项目并研究资源:
function(e, t, n) { e.exports = { "content-wrapper": "e_f", title: "e_g", text: "e_h" } }
尽管没有在任何地方使用它,但看起来该变量仍然存在。 为什么会这样呢?
该webpack支持多种类型的模块化结构,其中最受欢迎的是ES2015(导入)和commonJS(必需)。
ES2015模块与commonJS不同,由于它们的静态结构,它们支持
摇树 。
但是css-loader和mini-css-extract-plugin加载器都使用commonJS语法导出类名,因此不会从构建中删除导出的数据。
我们将编写我们的小型加载器,并在生产模式下删除多余的数据:
const path = require('path'); const resolve = relativePath => path.resolve(__dirname, relativePath); const isDev = process.env.NODE_ENV === 'development'; module.exports = { module: { rules: [ { test: /\.css$/, use: [ ...(isDev ? ['style-loader'] : [ resolve('path/to/webpack-loaders/nullLoader'), MiniCssExtractPlugin.loader, ]), { loader: 'css-loader', }, ], }, ], }, };
再次检查汇编文件:
function(e, t, n) {}
您可以松一口气,一切正常。
删除类映射变量失败最初,对我来说,使用已经存在的
null加载程序包似乎最为明显。
但事实证明并非如此简单:
export default function() { return '// empty (null-loader)'; } export function pitch() { return '// empty (null-loader)'; }
如您所见,除了主函数外,空加载器还导出了pitch函数。 我
从文档中了解到,音调方法比其他方法更早被调用,并且如果它们从此方法返回一些数据,则可以取消所有后续的加载器。
使用null-loader,CSS生成序列开始如下所示:
- 调用null-loader的pitch方法,该方法返回一个空字符串。
- 由于pitch方法返回值,因此不会调用所有后续加载程序。
我再也没有看到解决方案,因此决定自己制造装载机。
与Vue.js一起使用如果您只有一个Vue.js触手可及,但真的想压缩类名并删除运行时变量,那么我有个不错的选择!
我们需要的是两个插件:babel-plugin-transform-vue-jsx和babel-plugin-react-css-modules。 我们将需要第一个在渲染函数中编写JSX,第二个,正如您已经知道的,需要在编译阶段生成名称。
module.exports = { plugins: [ 'transform-vue-jsx', ['react-css-modules', {
import './index.css'; const TextComponent = { render(h) { return( <div styleName="text"> Lorem ipsum dolor. </div> ); }, mounted() { console.log('I\'m mounted!'); }, }; export default TextComponent;
充分压缩CSS
假设以下CSS出现在项目中:
.component1__title { color: red; } .component2__title { color: green; } .component2__title_red { color: red; }
您是CSS缩小器。 您会如何挤压它?
我认为您的答案是这样的:
.component2__title{color:green} .component2__title_red, .component1__title{color:red}
现在,我们将检查常用的缩小器会执行的操作。 将我们的代码放在一些
在线压缩器中 :
.component1__title{color:red} .component2__title{color:green} .component2__title_red{color:red}
他为什么不能呢?
缩小器担心由于样式声明顺序的更改,某些东西会损坏。 例如,如果项目具有以下代码:
<div class="component1__title component2__title">Some weird title</div>
由于您的原因,标题将变为红色,并且在线缩小器将保留正确的样式声明顺序,并且将变为绿色。 当然,您知道component1__title和component2__title永远不会交集,因为它们位于不同的组件中。 但是,如何对矿工说呢?
搜索文档后,我发现可以指定仅用于
csso的类的上下文。 而且他没有开箱即用的Webpack便捷解决方案。 要走得更远,我们需要一辆小型自行车。
您需要将每个组件的类名组合到单独的数组中,并在csso中提供它。 早些时候,我们根据这种模式生成了最小化的类名:“ [componentId] _ [classNameId]”。 因此,可以通过名称的第一部分简单地组合类名称!
系好安全带并编写插件:
const cssoLoader = require('path/to/cssoLoader'); module.exports = { plugins: [ new cssoLoader(), ], };
const csso = require('csso'); const RawSource = require('webpack-sources/lib/RawSource'); const getScopes = require('./helpers/getScopes'); const isCssFilename = filename => /\.css$/.test(filename); module.exports = class cssoPlugin { apply(compiler) { compiler.hooks.compilation.tap('csso-plugin', (compilation) => { compilation.hooks.optimizeChunkAssets.tapAsync('csso-plugin', (chunks, callback) => { chunks.forEach((chunk) => {
const csso = require('csso'); const getComponentId = (className) => { const tokens = className.split('_');
并不是那么困难,对吧? 通常,此缩小还会将CSS压缩3-6%。
值得吗?
当然可以
在我的应用程序中,终于出现了快速的热重装,CSS开始分裂成碎片,平均重量减轻了40%。
这将加快网站的加载速度,并减少解析样式的时间,这不仅会影响用户,还会影响CEO。
这篇文章已经有了很大的增长,但是我很高兴有人能够将其滚动到最后。 感谢您的宝贵时间!
所用材料