60行代码实现一个基于esbuild按需加载的loader(二)

简介: 设计整个插件的构建思路 就是 读取配置, 然后判断是否需要 按需加载, 如果需要 就利用的 babel 能力, 去做转化, 转换完成用 esbuild 去做编译就好了。

设计



整个插件的构建思路 就是 读取配置, 然后判断是否需要 按需加载, 如果需要 就利用的 babel 能力, 去做转化, 转换完成用 esbuild 去做编译就好了。



image.png



项目源码 都在这里, 如果你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)
  },
})


我们看下遍历的结果


image.png


我们核心就是 拿到当前 引用的组件名字  就是 「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 个


image.png

image.png


速度 大概快了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 地址 如下:


image.png


感兴趣的小伙伴, 可以 试一试 玩一玩, 欢迎👏🏻提bug 给我 职业 改bug 哈哈哈。后面还会分享的内容如下


  1. 如何打包一个 合格的 npm 包


  1. 基于storybook 的组件库 技术搭建


  1. vite  rollup webpack 打包之间的差异


  1. 基于canvas 的 react 序列帧动画 渲染器
相关文章
|
25天前
|
JavaScript 测试技术 UED
解决 Vue 项目中 Tree shaking 无法去除某些模块
【10月更文挑战第23天】解决 Vue 项目中 Tree shaking 无法去除某些模块的问题需要综合考虑多种因素,通过仔细分析、排查和优化,逐步提高 Tree shaking 的效果,为项目带来更好的性能和用户体验。同时,持续关注和学习相关技术的发展,不断探索新的解决方案,以适应不断变化的项目需求。
|
6月前
|
JavaScript
【第12期】Vue3+TypeScript+Vite中动态引入图片等静态资源
【第12期】Vue3+TypeScript+Vite中动态引入图片等静态资源 c
1245 0
|
6月前
|
前端开发 JavaScript Shell
stylus详解与引入
stylus详解与引入
|
前端开发 JavaScript Java
前端——使用RequireJS的r.js打包压缩模块
前端——使用RequireJS的r.js打包压缩模块
|
6月前
elementPlus引入
elementPlus引入
101 1
|
6月前
|
JavaScript 前端开发
vue项目打包后使用reverse-sourcemap反编译到源码(详解版)
vue项目打包后使用reverse-sourcemap反编译到源码(详解版)
824 0
|
Web App开发 前端开发 JavaScript
UMD 被淘汰了吗?不考虑的 UMD 的库如何在纯 UMD 前端项目中运行?
UMD 被淘汰了吗?不考虑的 UMD 的库如何在纯 UMD 前端项目中运行?
234 0
|
JavaScript CDN
vue_按需引入elment、echarts和路由懒加载,减少打包体积
vue_按需引入elment、echarts和路由懒加载,减少打包体积
206 0
Vue3+Vant3通过mock引入本地图片路径
Vue3+Vant3通过mock引入本地图片路径
288 0
下一篇
无影云桌面