设计
整个插件的构建思路 就是 读取配置, 然后判断是否需要 按需加载, 如果需要 就利用的 babel 能力, 去做转化, 转换完成用 esbuild 去做编译就好了。
项目源码 都在这里, 如果你loader 都不是特别会的, 建议学习下loader, 然后再去看这边文章
按需加载
import { Button } from 'antd'; // 👇🏻 👇🏻 👇🏻 👇🏻 👇🏻 👇🏻 👇🏻 👇🏻 👇🏻 👇🏻 // import 'antd/lib/button/style/css'; import Button from 'antd/lib/button';
按需加载就是 比如 我项目 中引用到了 「antd」 的组件库, 但是我打包的时候, 不可能把antd 的所有组件库都打包进去的, 不利于 treeShaking, 所以我们就自己手动 转成 下面这种格式, 引用到 具体文件, 但是 有同学就会说, 我如果自己手动改, 那也可以哇, 但是对于一些大型项目, 你一个个改, 不累嘛, 然后再团队合作过程中,你能确保每一个同学有这种意识嘛, 每个人的理解都不一样, 所以借助工程化的能力, 统一操作就OK了。这就是工程化的意义, 定义统一的规范, 然后几乎看起来每个人写的代码都是一样的。
loader 的转化本质是传入字符串 , 在吐出 字符串的过程, 方便 下一个loader 继续操作, 所以 loader 一般都是 倒序执行。
比如是 loader 传入的是下面这些 字符串
import React, { useEffect, useState } from 'react' import { Button } from 'antd' function Home() { return <div>我是home 页</div> }
我们要将其进行改变要进行 下面几个步骤, 第一步
「借助 babel 的 接口 生成 AST 节点」
import parser from "@babel/parser"; const ast = parser.parse(source, { sourceType: 'module', plugins: ['jsx', 'typescript'] })
第二步 traverse 遍历 ast 节点 的 import 部分
import traverse from '@babel/traverse' traverse(res, { ImportDeclaration: function (path) { console.error(path.node, '/n') console.error(path.node.specifiers) }, })
我们看下遍历的结果
我们核心就是 拿到当前 引用的组件名字 就是 「Button」
所以就是替换当前的 同时增加 一条 css 的 引入 节点
let node = path.node let specifiers = node.specifiers const { library, customName, customStyle } = opts if (library == node.source.value && !types.isImportDeclaration(specifiers[0])) { let newImport: any[] = [] specifiers.forEach((specifier) => { newImport.push( types.importDeclaration( [types.importDefaultSpecifier(specifier.local as types.Identifier)], types.stringLiteral(customName(specifier.local.name)), ), ) // 增加一条新的节点 addSideEffect(path, `${customStyle(specifier.local.name)}`) }) // 替换当前 节点 path.replaceWithMultiple(newImport) }
这里的 我们将节点的名字 回调出来, 由外层决定,怎么定义 引用路径 类似于这样 每一个库的默认文件可能不一样
const opts = { library: 'antd', customName: (name: string) => { return `antd/lib/${name}` }, customStyle: (name: string) => { return `antd/lib/${name}/style` }, }
编译
编译解决完毕, 然后利用 esbuild 的 transform 去编译对应的 source
// 1. 获取异步回调函数 this.cacheable && this.cacheable() const callback = this.async() const { code, map } = await transform(source, transformOptions) callback(null, code, map && JSON.parse(map))
loader 的接口类型, 和 esbuild 保持一致, 只是踢出了 sourcemap 和 sourcefile 两个 属性 同时保持了。加了一些默认值。整体的流程到这里就结束了, 其实整体来说还是比较简单的。
构建效率
我在大型react 进行了试验, 看看这东西 到底能优化多少, 没使用 esbuild-import-loader 去打包 整体的 速度。大概模块在800 个
速度 大概快了2 倍就光是 ts 和 tsx 层面的打包,所以说整体的构建效率 还是 比较明显的 。说一下遇到的坑吧, esbuild 作为新兴的前端构建工具, 就是 esbuild 的 tsx loader 用来 编译 ts 某些语法 文件 会报错。比如:
const requestInstance = createFetch(remote) return <T>({ path, params, method, headers, ...config }: RequestConfig) => requestInstance<ResponseBody<T>>(path, { params, method, headers, ...config }) }
这也是最常见的问题 比如 无法在tsx加载器中对箭头函数表达式使用泛型类型参数,如「<T>(=>{})」 , tsx加载器不是ts加载器的超集。它们是两种不同的部分不兼容的语法。例如,字符序列
<a>1</a>/g
使用ts加载器解析为
<a>(1 < (/a>/g))
,使用tsx加载器则解析为
(<a>1</a>) / g
解决方案 就是 tsx 和 ts 匹配分开来 如下面 这样配置, 这样就可以解决了。
{ test: /\.tsx/, use: [ { loader: 'esbuild-import-loader', options: { loader: 'tsx', target: 'es2015', libraryName: 'xxx-desigin', customName: (name: string) => { return `xx-desigin/es/${name}/index.js` }, customStyle: (name: string) => { return `xx-desigin/es/${name}/style/css.js` }, }, }, ], exclude: /node_modules/, }, { test: /\.ts/, use: [ { loader: 'esbuild-import-loader', options: { loader: 'ts', target: 'es2015', libraryName: 'cat-desigin', customName: (name: string) => { return `xxx-desigin/es/${name}/index.js` }, customStyle: (name: string) => { return `xxx-desigin/es/${name}/style/css.js` }, }, }, ], exclude: /node_modules/, },
总结
目前代码已经开源到 这个 github 上, 同时 也发了 npm 包 github 地址 如下:
感兴趣的小伙伴, 可以 试一试 玩一玩, 欢迎👏🏻提bug 给我 职业 改bug 哈哈哈。后面还会分享的内容如下
- 如何打包一个 合格的 npm 包
- 基于storybook 的组件库 技术搭建
- vite rollup webpack 打包之间的差异
- 基于canvas 的 react 序列帧动画 渲染器