كيف تخلصت من حزمة الويب وكتبت المكون الإضافي babel لـ scss / sass transpile

خلفية



في إحدى ليالي السبت كنت جالسًا وأبحث عن طرق لإنشاء مجموعة أدوات واجهة المستخدم باستخدام حزمة الويب. أستخدم styleguidst كعرض توضيحي لمجموعة أدوات واجهة المستخدم. بالطبع ، webpack ذكي ويقوم بحشو جميع الملفات الموجودة في دليل العمل في حزمة واحدة ومن هناك يتحول كل شيء ويتحول.



لقد قمت بإنشاء ملف entry.js ، واستوردت جميع المكونات هناك ، ثم قمت بالتصدير من هناك. يبدو أن كل شيء على ما يرام.



import Button from 'components/Button'
import Dropdown from 'components/Dropdown '

export {
  Button,
  Dropdown 
}


وبعد تجميع كل هذا ، حصلت على output.js ، حيث ، كما هو متوقع ، كان كل شيء - كل المكونات الموجودة في الكومة في ملف واحد. هنا نشأ السؤال:

كيف يمكنني تجميع كل الأزرار والقوائم المنسدلة وما إلى ذلك بشكل منفصل ، والتي سيتم استيرادها في مشاريع أخرى؟

لكنني أريد أيضًا تحميله على npm كحزمة.



حسنًا ... دعنا نذهب بالترتيب.



مداخل متعددة



بالطبع ، الفكرة الأولى التي تتبادر إلى الذهن هي تحليل جميع المكونات في دليل العمل. اضطررت إلى استخدام google قليلاً حول تحليل الملفات ، لأنني نادرًا ما أعمل مع NodeJS. وجدت شيء مثل الكرة الأرضية .



سافرنا لكتابة إدخالات متعددة.



const { basename, join, resolve } = require("path");
const glob = require("glob");

const componentFileRegEx = /\.(j|t)s(x)?$/;
const sassFileRegEx = /\s[ac]ss$/;

const getComponentsEntries = (pattern) => {
  const entries = {};
  glob.sync(pattern).forEach(file => {
    const outFile = basename (file);
    const entryName = outFile.replace(componentFileRegEx, "");
    entries[entryName] = join(__dirname, file);
  })
  return entries;
}

module.exports = {
  entry: getComponentsEntries("./components/**/*.tsx"),
  output: {
    filename: "[name].js",
    path: resolve(__dirname, "build")
  },
  module: {
    rules: [
      {
        test: componentFileRegEx,
        loader: "babel-loader",
        exclude: /node_modules/
      },
      {
        test: sassFileRegEx,
        use: ["style-loader", "css-loader", "sass-loader"]
      }
    ]
  }
  resolve: {
    extensions: [".js", ".ts", ".tsx", ".jsx"],
    alias: {
      components: resolve(__dirname, "components")
    }
  }
}


منجز. نجمعها.



بعد الإنشاء ، تم العثور على ملفي Button.js و Dropdown.js في دليل الإنشاء - فلنلق نظرة على الداخل. داخل الترخيص يوجد رد فعل.إنتاج.مين.جس ، رمز مصغر يصعب قراءته ، والكثير من الهراء. حسنًا ، لنحاول استخدام الزر.



في الملف التجريبي للزر ، قم بتغيير الاستيراد للاستيراد من دليل البناء.



هذه هي الطريقة التي يبدو بها العرض التوضيحي البسيط للزر في styleguidist - Button.md



```javascript
import Button from '../../build/Button'
<Button></Button>
```


نذهب لإلقاء نظرة على زر IR ... في هذه المرحلة ، اختفت بالفعل فكرة والرغبة في التجميع من خلال webpack.



Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.









البحث عن مسار بناء آخر بدون حزمة الويب



نذهب للحصول على مساعدة بابل بدون حزمة ويب. نكتب نصًا برمجيًا في package.json ، نحدد ملف التكوين ، الامتدادات ، الدليل الذي توجد به المكونات ، الدليل حيث يتم البناء:



{
  //...package.json  -     
  scripts: {
    "build": "babel --config-file ./.babelrc --extensions '.jsx, .tsx' ./components --out-dir ./build"
  }
}


يركض:



npm run build


Voila ، لدينا الآن ملفان Button.js ، Dropdown.js في دليل الإنشاء ، يوجد داخل الملفات ملف vanilla js مصمم بشكل جيد + بعض polyfills و requre الوحيد ("styles.scss") . من الواضح أن هذا لن يعمل في العرض التوضيحي ، وقم بإزالة استيراد الأنماط (في تلك اللحظة كنت أقضم على أمل أن أجد مكونًا إضافيًا لـ scss transpile) ، وقم بتجميعه مرة أخرى.



بعد التجميع ، لا يزال لدينا بعض JS لطيفة. دعنا نحاول مرة أخرى دمج المكون المُجمع في styleguidist:



```javascript
import Button from '../../build/Button'
<Button></Button>
```


المترجمة - يعمل. فقط زر بدون أنماط.



نحن نبحث عن مكون إضافي لـ transpile scss / sass



نعم ، تجميع المكونات يعمل ، المكونات تعمل ، يمكنك البناء ، النشر في npm أو رابطة العمل الخاصة بك. مع ذلك ، فقط احفظ الأنماط ... حسنًا ، ستساعدنا Google مرة أخرى (لا).



لم يعطني البحث عن المكونات الإضافية في Google أي نتائج. يُنشئ أحد المكونات الإضافية سلسلة من الأنماط ، بينما الآخر لا يعمل على الإطلاق ، بل ويتطلب استيراد طريقة العرض: استيراد أنماط من "styles.scss"



كان الأمل الوحيد لهذا المكون الإضافي: babel-plugin-transform-scss-import-to-string، لكنه فقط يولد خطًا من الأنماط (آه ... سبق أن قلت أعلاه. اللعنة ...) ثم ساء كل شيء ، لقد وصلت إلى الصفحة 6 في Google (والساعة بالفعل 3 صباحًا). ولن تكون هناك خيارات معينة للعثور على شيء ما. نعم ، وليس هناك ما يفكر فيه - إما webpack + sass-loader ، الذي يفعل ذلك بشكل سيء وليس لحالتي ، أو شيئًا آخر. الأعصاب ... قررت أن آخذ استراحة وشرب الشاي ، ما زلت لا أريد النوم بينما كنت أقوم بإعداد الشاي ، ظلت فكرة كتابة مكون إضافي لـ scss / sass transpile تطرأ على رأسي أكثر فأكثر. بينما كان السكر يتحرك ، رنين الملعقة في رأسي من حين لآخر كان يردد: "اكتب بلاجين". حسنًا ، قررت ، سأكتب ملحقًا.



البرنامج المساعد غير موجود. نكتب أنفسنا



أخذتُ babel-plugin-transform-scss-import-to-string المذكورة أعلاه كأساس للمكوِّن الإضافي الخاص بي . لقد فهمت تمامًا أنه سيكون هناك الآن بواسير مع شجرة AST ، وحيل أخرى. حسنا دعنا نذهب.



نجري الاستعدادات الأولية. نحتاج إلى node-sass و path ، بالإضافة إلى خطوط منتظمة للملفات والامتدادات. الفكرة هي:



  • نحصل على مسار الملف بأنماط من سطر الاستيراد
  • تحليل الأنماط إلى السلسلة عبر node-sass (بفضل babel-plugin-transform-scss-import-to-string)
  • نقوم بإنشاء علامات نمط لكل عملية استيراد (يتم تشغيل المكون الإضافي babel في كل استيراد)
  • من الضروري تحديد النمط الذي تم إنشاؤه بطريقة ما ، حتى لا يتم إلقاء نفس الشيء على كل عطسة يتم إعادة تحميلها على الساخن. دعونا ندفع في بعض السمات (data-sass-component) مع قيمة الملف الحالي واسم ورقة الأنماط. سيكون هناك شيء مثل هذا:



          <style data-sass-component="Button_style">
             .button {
                display: flex;
             }
          </style>
    


من أجل تطوير البرنامج المساعد واختباره على المشروع ، على مستوى دليل المكونات ، قمت بإنشاء دليل babel-plugin-transform-scss ، و stuffed package.json هناك وحشو دليل lib هناك ، وقد رميت به index.js بالفعل.

ماذا ستكون vkurse - تكوين Babel يتسلق خلف المكون الإضافي ، والذي تم تحديده في التوجيه الرئيسي في package.json ، لهذا كان علي حشره.
نشير إلى:



{
  //...package.json   -     ,    main  
  main: "lib/index.js"
}


بعد ذلك ، ادفع المسار إلى المكوّن الإضافي في ملف التكوين babel (.babelrc):



{
  //  
  plugins: [
    "./babel-plugin-transform-scss"
    //    
  ]
}


الآن ، دعونا نحشر بعض السحر في index.js.



المرحلة الأولى هي التحقق من استيراد ملف scss أو sass ، والحصول على اسم الملفات المستوردة ، والحصول على اسم ملف js (المكون) نفسه ، ونقل سلسلة scss أو sass إلى css. قمنا بتقسيم WebStorm إلى npm ، ثم تشغيل build من خلال مصحح أخطاء ، وتعيين نقاط توقف ، وإلقاء نظرة على وسيطات المسار والحالة ، واستخراج أسماء الملفات ، ومعالجتها باستخدام اللعنات:



const { resolve, dirname, join } = require("path");
const { renderSync } = require("node-sass");

const regexps = {
  sassFile: /([A-Za-z0-9]+).s[ac]ss/g,
  sassExt: /\.s[ac]ss$/,
  currentFile: /([A-Za-z0-9]+).(t|j)s(x)/g,
  currentFileExt: /.(t|j)s(x)/g
};

function transformScss(babel) {
  const { types: t } = babel;
  return {
    name: "babel-plugin-transform-scss",
    visitor: {
      ImportDeclaration(path, state) {
        /**
         * ,     scss/sass   
         */
        if (!regexps.sassExt.test(path.node.source.value)) return;
        const sassFileNameMatch = path.node.source.value.match(
          regexps.sassFile
        );

        /**
         *    scss/sass    js 
         */
        const sassFileName = sassFileNameMatch[0].replace(regexps.sassExt, "");
        const file = this.filename.match(regexps.currentFile);
        const filename = `${file[0].replace(
          regexps.currentFileExt,
          ""
        )}_${sassFileName}`;

        /**
         *
         *     scss/sass ,    css
         */
        const scssFileDirectory = resolve(dirname(state.file.opts.filename));
        const fullScssFilePath = join(
          scssFileDirectory,
          path.node.source.value
        );
        const projectRoot = process.cwd();
        const nodeModulesPath = join(projectRoot, "node_modules");
        const sassDefaults = {
          file: fullScssFilePath,
          sourceMap: false,
          includePaths: [nodeModulesPath, scssFileDirectory, projectRoot]
        };
        const sassResult = renderSync({ ...sassDefaults, ...state.opts });
        const transpiledContent = sassResult.css.toString() || "";
        }
    }
}


نار. أول نجاح ، حصل على سطر css في transpiledContent. بعد ذلك ، أسوأ شيء - نحن نصعد إلى babeljs.io/docs/en/babel-types#api لواجهة برمجة التطبيقات على شجرة AST. نتسلق إلى astexplorer.net ونكتب الكود لدفع ورقة الأنماط إلى الرأس.



في astexplorer.net ، اكتب دالة استدعاء ذاتية سيتم استدعاؤها في مكان استيراد النمط:



(function(){
  const styles = "generated transpiledContent" // ".button {/n display: flex; /n}/n" 
  const fileName = "generated_attributeValue" //Button_style
  const element = document.querySelector("style[data-sass-component='fileName']")
  if(!element){
    const styleBlock = document.createElement("style")
    styleBlock.innerHTML = styles
    styleBlock.setAttribute("data-sass-component", fileName)
    document.head.appendChild(styleBlock)
  }
})()


في مستكشف AST ، انقر على الجانب الأيسر على الخطوط ، والإعلانات ، والحروف الحرفية ، - على اليمين في الشجرة ننظر إلى بنية الإعلانات ، ونصعد إلى babeljs.io/docs/en/babel-types#api باستخدام هذا الهيكل ، ودخن كل هذا وكتابة بديل.



بعد لحظات قليلة ... بعد



ساعات ونصف ، من خلال علامات التبويب من ast إلى api من نوع بابل ، ثم في الكود ، كتبت بديلاً لاستيراد scss / sass. لن أقوم بتحليل شجرة آست و api من نوع بابل بشكل منفصل ، سيكون هناك المزيد من الأحرف. أعرض النتيجة على الفور:



const { resolve, dirname, join } = require("path");
const { renderSync } = require("node-sass");

const regexps = {
  sassFile: /([A-Za-z0-9]+).s[ac]ss/g,
  sassExt: /\.s[ac]ss$/,
  currentFile: /([A-Za-z0-9]+).(t|j)s(x)/g,
  currentFileExt: /.(t|j)s(x)/g
};

function transformScss(babel) {
  const { types: t } = babel;
  return {
    name: "babel-plugin-transform-scss",
    visitor: {
      ImportDeclaration(path, state) {
        /**
         * ,     scss/sass   
         */
        if (!regexps.sassExt.test(path.node.source.value)) return;
        const sassFileNameMatch = path.node.source.value.match(
          regexps.sassFile
        );

        /**
         *    scss/sass    js 
         */
        const sassFileName = sassFileNameMatch[0].replace(regexps.sassExt, "");
        const file = this.filename.match(regexps.currentFile);
        const filename = `${file[0].replace(
          regexps.currentFileExt,
          ""
        )}_${sassFileName}`;

        /**
         *
         *     scss/sass ,    css
         */
        const scssFileDirectory = resolve(dirname(state.file.opts.filename));
        const fullScssFilePath = join(
          scssFileDirectory,
          path.node.source.value
        );
        const projectRoot = process.cwd();
        const nodeModulesPath = join(projectRoot, "node_modules");
        const sassDefaults = {
          file: fullScssFilePath,
          sourceMap: false,
          includePaths: [nodeModulesPath, scssFileDirectory, projectRoot]
        };
        const sassResult = renderSync({ ...sassDefaults, ...state.opts });
        const transpiledContent = sassResult.css.toString() || "";
        /**
         *  ,   AST Explorer     
         * replaceWith  path.
         */
        path.replaceWith(
          t.callExpression(
            t.functionExpression(
              t.identifier(""),
              [],
              t.blockStatement(
                [
                  t.variableDeclaration("const", [
                    t.variableDeclarator(
                      t.identifier("styles"),
                      t.stringLiteral(transpiledContent)
                    )
                  ]),
                  t.variableDeclaration("const", [
                    t.variableDeclarator(
                      t.identifier("fileName"),
                      t.stringLiteral(filename)
                    )
                  ]),
                  t.variableDeclaration("const", [
                    t.variableDeclarator(
                      t.identifier("element"),
                      t.callExpression(
                        t.memberExpression(
                          t.identifier("document"),
                          t.identifier("querySelector")
                        ),
                        [
                          t.stringLiteral(
                            `style[data-sass-component='${filename}']`
                          )
                        ]
                      )
                    )
                  ]),
                  t.ifStatement(
                    t.unaryExpression("!", t.identifier("element"), true),
                    t.blockStatement(
                      [
                        t.variableDeclaration("const", [
                          t.variableDeclarator(
                            t.identifier("styleBlock"),
                            t.callExpression(
                              t.memberExpression(
                                t.identifier("document"),
                                t.identifier("createElement")
                              ),
                              [t.stringLiteral("style")]
                            )
                          )
                        ]),
                        t.expressionStatement(
                          t.assignmentExpression(
                            "=",
                            t.memberExpression(
                              t.identifier("styleBlock"),
                              t.identifier("innerHTML")
                            ),
                            t.identifier("styles")
                          )
                        ),
                        t.expressionStatement(
                          t.callExpression(
                            t.memberExpression(
                              t.identifier("styleBlock"),
                              t.identifier("setAttribute")
                            ),
                            [
                              t.stringLiteral("data-sass-component"),
                              t.identifier("fileName")
                            ]
                          )
                        ),
                        t.expressionStatement(
                          t.callExpression(
                            t.memberExpression(
                              t.memberExpression(
                                t.identifier("document"),
                                t.identifier("head"),
                                false
                              ),
                              t.identifier("appendChild"),
                              false
                            ),
                            [t.identifier("styleBlock")]
                          )
                        )
                      ],
                      []
                    ),
                    null
                  )
                ],
                []
              ),
              false,
              false
            ),
            []
          )
        );
        }
    }
}


أفراح أخيرة



الصيحة !!! تم استبدال الاستيراد باستدعاء وظيفة تكدس النمط بهذا الزر في رأس المستند. وبعد ذلك فكرت ، ماذا لو بدأت هذا القارب بأكمله من خلال حزمة الويب ، قص لودر sass؟ هل ستعمل؟ حسنًا ، نحن جز ونفحص. أقوم بتشغيل التجميع باستخدام حزمة ويب ، في انتظار حدوث خطأ يجب أن أحدد محملًا لنوع الملف هذا ... ولكن لا يوجد خطأ ، يتم تجميع كل شيء. أفتح الصفحة ، وألقي نظرة ، والنمط عالق في رأس المستند. اتضح بشكل مثير للاهتمام ، لقد تخلصت أيضًا من 3 رافعات نمط (ابتسامة سعيدة جدًا).



إذا كنت مهتمًا بالمقال ، فيرجى دعمه بعلامة النجمة على جيثب .



أيضًا رابط لحزمة npm: www.npmjs.com/package/babel-plugin-transform-scss



ملاحظة: خارج المقالة ، تمت إضافة فحص لاستيراد النمط حسب النوعاستيراد الأنماط من "./styles.scss"



All Articles