写了个 babel-plugin,我收获了不止一点

2,108 阅读2分钟

图片 babel

一、问题引出

我们知道 React 中一切皆组件,公共组件一般都会在 src 目录下新建 components 目录来存放。那么问题来了:如果组件拆分得细而多,那么文件数量自然而然就多了起来,我们在使用时可能会看见如下场景:

1.png

一大堆的文件是不是看着头就大!试想我们可不可以像 antd 组件一样:

import {
    ReimburseDetail,
    ContactUs,
    HoverTips,
    CustomModal,
    ReimburseStatus
} from 'src-components'

这么使用呢?哎,还真有办法!

二、解决问题

方案一

  1. components 目录下新建一个 index.js ,引入目录下所有文件,然后导出。
    /**
    * './module' 要读取的目录
    * true 是否读取子目录
    * /\.js$/ 匹配后缀为'.js'的文件
    */
    const files = require.context('./module', true, /\.js$/)
    const modules = files.keys().reduce((modules, path) => {
        // './app.js' => 'app'
        const name = path.replace(/^\.\/|.js$/g, '')
        modules[name] = files(path).default
        return modules
    }, {})
    export default modules
    
  2. 试试效果:可以满足要求,但打包体积会变大,未使用的组件也参与了打包。

方案二

  1. 我们试试手动实现一个 babel-plugin

  2. 思路:我们知道引入单一文件时不存在所谓的按需加载,当一个文件暴露的出口文件多时,那么这个文件就不是单一文件了,一旦文件被引用,该文件暴露的文件就全部会引入并参与打包,这显然不是我们需要的。那么我们是不是可以做点什么呢?假如我引入一个不存在的包,然后导出想要的组件:

    • 显然不可能会导入所有的包
    • 显然不可能会生效,编译会报错

    基于这个思路,我们可以手动修改它默认的编译规则,以达到我们想要的目的。我们需要拦截所有使用了我们自定义的包名文件,对传入的组件名提取出来,修改编译规则,让其单独引入传入的包名的组件参与单独引入打包,就可以达到我们想要的目的。

  3. 来吧,上手干。参照 链接这篇文章 查看 babel-plugin api 就开搞:

    // .babelrc
    {
        "plugins": [
            [
                "./src/utils/my-plugin-import", {
                    "libraryName": "src-components",
                    "alias": "@/components"
                }
            ],
            ...
        ]
    }
    
    // src/utils/my-plugin-import.js
    const toLine = name => {
        const str = name.replace(/([A-Z])/g, "-$1").toLowerCase()
        return str.split('-')[0] ? str : str.slice(1)
    }
    
    module.exports = function ({ types: t }) {
        return {
            visitor: {
                ImportDeclaration(path, source) {
                    const { opts: { libraryName, alias } } = source
                    if (!t.isStringLiteral(path.node.source, { value: libraryName })) {
                        return
                    }
                    const newImports = path.node.specifiers.map(item => {
                        const str = toLine(item.local.name)
    
                        return t.importDeclaration(
                            [t.importDefaultSpecifier(item.local)],
                            t.stringLiteral(`${alias}/${str}`)
                        )
                    })
    
                    path.replaceWithMultiple(newImports)
                }
            }
        }
    }
    
    • .babelrc 使用上我们的插件,babel-loader 会在编译的时候执行我们的自定义 js
    • 配置我们的自定义包名 src-components(引用的时候使用)
    • 配置我们需要加载的组件的路径 alias
    • 映射规则转换:大写驼峰 - 中划线命名方式
    • 最终路径就是:alias + name
    • 恭喜你,到这里就完成了

三、上手体验

修改组件引入方式,保存 ,编译正常,无任何毛病;爽歪歪,再 build 构建试试,无完全没问题。

四、回顾与思考

这种方式确实给我们带来了极大便利,优化了大量代码,简洁易读。这样看来,我们的代码是不是都可以和 antd 写法相媲美了,美滋滋。

等等 antd 是怎么实现按需引入的呢?噢噢,原来使用了 babel-plugin-import,让我们看看 .babelrc

// .babelrc
[
    "import",
    {
        "libraryName": "antd",
        "libraryDirectory": "es",
        "style": true
    }
]

哇咔咔,这是不是惊奇的相似?只不过别人还做了额外的功能就是导包的时候,再引入对应的css罢了。

至此,就到文末了。我们从发现问题到解决问题,从写插件再到发觉按需引入原理,收获还真不止一点点哦!