我如何抛弃Webpack并编写用于SSS / SASS Transpile的babel-plugin

背景



一个星期六晚上,我坐在那里,寻找使用webpack构建UI-Kit的方法。作为UI-kit演示,我使用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")
    }
  }
}


做完了 我们收集。



构建之后,有2个Button.js文件,Dropdown.js进入了构建目录-让我们看看内部。许可证中包含react.production.min.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.









寻找没有webpack的另一个构建路径



我们向没有webpack的通天塔寻求帮助。我们在package.json中编写一个脚本,指定配置文件,扩展名,组件所在的目录,构建的目录:



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


跑:



npm run build


瞧,我们在build目录中有2个文件Button.js,Dropdown.js,文件中有一个设计精美的vanilla js +一些polyfills和一个孤独的要求(“ styles.scss”)显然,这在演示中将不起作用,删除样式的导入(那时候我很希望能找到用于scss transpile的插件),然后再次收集它。



组装后,我们还有一些不错的JS。让我们再次尝试将组装的组件集成到styleguidist中:



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


编译-有效。只有一个没有样式的按钮。



我们正在寻找用于Transsile Scss / Sass的插件



是的,组件的组装工作正常,组件正在工作,您可以在npm中构建,发布或自己的工作关系。不过,只需保存样式即可。好的,Google会再次帮助我们(否)。



谷歌搜索插件没有给我带来任何结果。一个插件从样式生成一个字符串,另一个插件根本不起作用,甚至需要导入视图:从“ styles.scss”导入样式



唯一的希望是此插件:babel-plugin-transform-scss-import-to-string,但是它只是根据样式生成一个字符串(啊...我在上面说过。该死的...)。然后一切都变得更糟了,我到达了Google的第6页(现在已经是凌晨3点了)。并且没有找到特定内容的特定选择。是的,没有什么可考虑的-webpack + sass-loader很讨厌这样做,而不是针对我的情况,或者其他。神经...我决定休息一下,喝点茶,我还是不想睡觉。在我泡茶的时候,为scss / sass transpile编写插件的想法越来越多地涌入我的脑海。当糖在搅动时,偶尔在我头上的汤匙响起,回响道:“写明信。” 好的,我决定写一个插件。



找不到插件。我们写自己



我将上面提到babel-plugin-transform-scss-import-to-string作为我的plugin基础我完全理解,现在会有带有AST树的痔疮和其他技巧。好吧走吧



我们做准备。我们需要节点资源和路径,以及文件和扩展名的常规行。这个想法是这样的:



  • 我们从导入行获取具有样式的文件路径
  • 通过node-sass将样式解析为字符串(感谢babel-plugin-transform-scss-import-to-string)
  • 我们为每个导入创建样式标签(每次导入都会启动babel插件)
  • 有必要以某种方式标识所创建的样式,以免在每次热装打喷嚏时都扔相同的东西。让我们用当前文件的值和样式表的名称向它推送一些属性(data-sass-component)。会有这样的事情:



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


为了开发插件并在项目上对其进行测试,在components目录的级别上,我创建了一个babel-plugin-transform-scss目录,在其中填充package.json并在其中填充lib目录,并且我已经将index.js放入其中。

您将是什么-Babel配置位于package.json的main指令中指定的插件后面,为此,我不得不将其塞入。
我们指出:



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


然后,将插件的路径推入babel配置(.babelrc):



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


现在,让我们将一些魔法塞入index.js。



第一步是检查scss或sass文件的导入,获取导入文件的名称,获取js文件(组件)本身的名称,然后将scss或sass字符串传输到css。我们将WebStorm切入npm,通过调试器运行构建,设置断点,查看路径和状态参数并获取文件名,并使用curses处理它们:



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() || "";
        }
    }
}


火。第一次成功,在transpiledContent中获得了CSS行。接下来,最糟糕的事情-我们进入AST树上的API的babeljs.io/docs/en/babel-types#api。我们进入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中,吸取所有内容并编写替换内容。



过了一会儿...



1-1.5小时后,从ast到babel-types api遍历选项卡,然后进入代码,我编写了scss / sass导入的替代项。我不会分别解析ast树和babel类型的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
            ),
            []
          )
        );
        }
    }
}


最后的欢乐



哇!导入被对函数的调用所代替,该函数将该按钮的样式推入文档的头部。然后我想,如果我通过Webpack开始整个皮划艇,修剪Sass-Loader,该怎么办?能行吗 好吧,我们割草并检查。我使用webpack启动程序集,等待错误,我必须为此文件类型定义一个加载器...但是没有错误,所有内容都已组装好。我打开页面,外观和样式都停留在文档的头部。有趣的是,我还摆脱了3种样式的装载机(非常开心的笑容)。



如果您对本文感兴趣,请在github上用星号支持



也是npm软件包的链接:www.npmjs.com/package/babel-plugin-transform-scss



注意:在本文之外,添加了按类型导入样式的检查从'./styles.scss'导入样式



All Articles