مرحبا يا هبر!
منذ وقت ليس ببعيد ، أدركت أن العمل مع CSS في جميع تطبيقاتي يمثل ألمًا للمطور والمستخدم.
تحت الخفض هي مشاكلي ، حفنة من الرموز الغريبة والمزالق في طريق العمل مع الأنماط بشكل صحيح.
مشكلة CSS
في مشروعات React و Vue التي قمت بها ، كان النهج تجاه الأنماط هو نفسه تقريبًا. يتم تجميع المشروع عن طريق webpack ، ويتم استيراد ملف CSS واحد من نقطة الدخول الرئيسية. يستورد هذا الملف بداخله بقية ملفات CSS التي تستخدم BEM لتسمية الفئات.
styles/ indes.css blocks/ apps-banner.css smart-list.css ...
هل هذا مألوف؟ لقد استخدمت هذا التنفيذ في كل مكان تقريبًا. وكان كل شيء على ما يرام حتى نما أحد المواقع إلى هذه الحالة لدرجة أن مشاكل الأنماط بدأت تهيج عيني بشكل كبير.
1. مشكلة إعادة التحميل الساخنةحدث استيراد أنماط من بعضها البعض من خلال المكون الإضافي postcss أو محمل القلم.
الصيد هو هذا:
عندما نحل عمليات الاستيراد من خلال المكون الإضافي postcss أو stylus-loader ، يكون الناتج ملف CSS كبير. الآن ، حتى مع تغيير طفيف في أحد أوراق الأنماط ، ستتم معالجة جميع ملفات CSS مرة أخرى.
إنها تقتل بالفعل سرعة إعادة التحميل الساخنة: تستغرق حوالي 4 ثوانٍ لمعالجة ~ 950 كيلوبايت من ملفات القلم.
ملاحظة حول css-loaderإذا تم حل استيراد ملفات CSS من خلال أداة تحميل css ، فلن تكون هذه المشكلة قد نشأت:
يقوم css-loader بتحويل CSS إلى JavaScript. سوف يحل محل كل واردات النمط مع الطلب. ثم لن يؤثر تغيير ملف CSS واحد على الملفات الأخرى وسيتم إعادة التحميل السريع بسرعة.
لمحمل CSS
@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كل هذه المشاكل جعلتني مرتبًا ، وقررت أخيرًا إنهاءها وتحقيق النتيجة المثالية.
اختيار القرار
للتخلص من جميع المشاكل المذكورة أعلاه ، وجدت حلين: CSS In JS (مكونات ذات أنماط) ووحدات CSS.
لم أر عيوبًا خطيرة في هذه الحلول ، ولكن في النهاية وقع اختياري على وحدات CSS لعدة أسباب:
- يمكنك وضع CSS في ملف منفصل للتخزين المؤقت المنفصل لـ JS و CSS.
- المزيد من الخيارات لأنماط التلدين.
- من الشائع العمل مع ملفات CSS.
يتم الاختيار ، لقد حان الوقت لبدء الطهي!
الإعداد الأساسي
تكوين حزمة الويب قليلاً. أضف محمل css وقم بتمكين وحدات 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 على مكتبة رائعة يمكن تحميلها لهذا الغرض. يسمح لك بإنشاء مكون يقوم بتنزيل الملف الذي نحتاجه ديناميكيًا ، إذا لزم الأمر.
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 حتى الآن ويتم إدراجه كنمط باستخدام محمل النمط. لكن لماذا لا نقطع الأنماط في ملف منفصل؟
سنقوم بإنشاء ملف CSS الخاص بنا لكل من الملف الرئيسي ولكل من الأجزاء.
قم بتثبيت البرنامج المساعد mini-css-extract-plugin واستحضاره باستخدام تكوين حزمة الويب:
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
يبدو أنه بعد كل بناء جديد قمنا بإبطال ذاكرة التخزين المؤقت. كيف ذلك؟
تكمن المشكلة في أن حزمة الويب لا تضمن الترتيب الذي تتم فيه معالجة الملفات. بمعنى ، ستتم معالجة ملفات CSS بترتيب غير متوقع ، وسيتم إنشاء أسماء مصغرة مختلفة لنفس اسم الفئة مع تجميعات مختلفة.
للتغلب على هذه المشكلة ، دعنا نحفظ البيانات حول أسماء الفئات المولدة بين التجميعات. قم بتحديث ملف getScopedName.js قليلاً:
const incstr = require('incstr');
لا يهم حقًا تنفيذ ملف generatorHelpers.js ، ولكن إذا كنت مهتمًا ، فإليك عملي:
مولد كهرباء Helper.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-response-css-modules في ذلك. في وقت الترجمة ، فإنه:
- سوف تجد استيراد CSS في الملف.
- سيفتح ملف CSS هذا ويغير أسماء فئات CSS تمامًا كما يفعل css-loader.
- سيعثر على عقد JSX مع سمة styleName.
- يستبدل أسماء الفئات المحلية من styleName بأسماء عالمية.
قم بإعداد هذا البرنامج المساعد. لنلعب مع تكوين بابل:
تحديث ملفات 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" } }
يبدو أن المتغير لا يزال موجودًا ، على الرغم من أنه لا يستخدم في أي مكان. لماذا حدث هذا؟
تدعم حزمة الويب عدة أنواع من الهياكل المعيارية ، والأكثر شيوعًا هي ES2015 (استيراد) و commonJS (مطلوب).
تدعم وحدات ES2015 ، على عكس CommonJS ،
اهتزاز الأشجار بسبب هيكلها الثابت.
لكن كلاً من محمل css ومحمِّل استخراج mini-css يستخدمان بناء جملة شائع JS لتصدير أسماء الفئات ، لذلك لا يتم حذف البيانات التي تم تصديرها من البنية.
سنكتب محملنا الصغير ونحذف البيانات الإضافية في وضع الإنتاج:
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) {}
يمكنك الزفير بارتياح ، كل شيء يعمل.
فشل في حذف متغير تعيين الفئةفي البداية ، بدا لي أن الأمر الأكثر وضوحًا هو استخدام حزمة
اللودر الموجودة بالفعل.
لكن كل شيء اتضح أنه ليس بهذه البساطة:
export default function() { return '// empty (null-loader)'; } export function pitch() { return '// empty (null-loader)'; }
كما ترون ، بالإضافة إلى الوظيفة الرئيسية ، يقوم برنامج null-loader أيضًا بتصدير وظيفة الملعب. لقد علمت
من الوثائق أن طرق العرض التقديمي تُسمى في وقت سابق عن غيرها ويمكن أن تلغي جميع اللوادر اللاحقة إذا أعادت بعض البيانات من هذه الطريقة.
مع المحمل الفارغ ، يبدأ تسلسل إنتاج CSS ليبدو كما يلي:
- يتم استدعاء طريقة الملعب للودر الفارغ ، والتي تُرجع سلسلة فارغة.
- نظرًا لأن طريقة العرض التقديمي للقيمة تعود ، لا يتم استدعاء جميع اللوادر اللاحقة.
لم أعد أرى الحلول وقررت عمل اللودر الخاص بي.
استخدم مع Vue.jsإذا كان لديك Vue.js واحد فقط في متناول يدك ، ولكنك تريد حقًا ضغط أسماء الصفوف وإزالة متغير وقت التشغيل ، فعندئذ لدي اختراق كبير!
كل ما نحتاجه هو مكونان إضافيان: وحدات babel-plugin-convert-vue-jsx و babel-plugin-response-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>
بسببك ، سيتحول العنوان إلى اللون الأحمر ، وسيترك المصغر عبر الإنترنت ترتيب إعلان النمط الصحيح وسيتحول إلى اللون الأخضر. بالطبع ، أنت تعرف أنه لن يكون هناك تقاطع مطلقًا مع المكون 1_العنوان والمكون 2_العنوان ، لأنهما في مكونات مختلفة. ولكن كيف نقول هذا إلى المصغر؟
بعد البحث في الوثائق ، وجدت القدرة على تحديد السياق لاستخدام الفئات فقط مع
csso . وليس لديه حل مناسب لحزمة الويب خارج الصندوق. للذهاب أبعد من ذلك ، نحن بحاجة إلى دراجة صغيرة.
تحتاج إلى دمج أسماء الفئات لكل مكون في صفائف منفصلة وإعطائها داخل csso. قبل ذلك بقليل ، أنشأنا أسماء فئات مصغرة وفقًا لهذا النمط: "[ComponId] _ [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 ٪ أقل في المتوسط.
سيؤدي ذلك إلى تسريع عملية تحميل الموقع ويقلل من وقت تحليل الأساليب ، الأمر الذي لن يؤثر فقط على المستخدمين ، ولكن أيضًا على الرؤساء التنفيذيين.
نمت المقالة بشكل كبير ، لكني سعيد لأن شخصًا ما كان قادرًا على التمرير حتى النهاية. شكرا على وقتك!
المواد المستخدمة