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 序列帧动画 渲染器
相关文章
|
Web App开发 缓存 移动开发
V8 JS AOT化的探索与实践
JS 语言的动态性非常优秀,其弱类型等语言特性也使得一线业务开发者更容易上手,但这也导致 JS 每一次运行前都要重复编译,使得 JS 的执行性能不理想;虽然之前 UC 内核有做过 Code Cache 方案,但支持的场景不够完整,与原生 Native 的技术方案比,尤其是首次启动场景(如各类大促活动等)还是有比较大的差距。为了能尽可能做到与 Native 对标,缩小性能差距,同时让业务开发者无感,我们开发了 JS AOT 功能。本分享将结合目前集团内自有业务形态,以及 JS 在 Web 中的执行过程,介绍JS AOT是如何设计和实现的,以及能给业务带来哪些收益。本篇分享来自阿里巴巴的喻世江在第
2317 0
V8 JS AOT化的探索与实践
|
1月前
|
中间件 API
Next.js 实战 (八):使用 Lodash 打包构建产生的“坑”?
这篇文章介绍了作者在使用Nextjs15进行项目开发时遇到的部署问题。在部署过程中,作者遇到了打包构建时的一系列报错,报错内容涉及动态代码评估在Edge运行时不被允许等问题。经过一天的尝试和调整,作者最终删除了lodash-es库,并将radash的部分源码复制到本地,解决了打包报错的问题。文章最后提供了项目的线上预览地址,并欢迎读者留言讨论更好的解决方案。
41 10
|
9月前
|
JavaScript
【第12期】Vue3+TypeScript+Vite中动态引入图片等静态资源
【第12期】Vue3+TypeScript+Vite中动态引入图片等静态资源 c
1395 0
|
7月前
|
JavaScript 前端开发
前端 JS 经典:Vite 打包优化
前端 JS 经典:Vite 打包优化
316 0
|
9月前
|
前端开发 JavaScript Shell
stylus详解与引入
stylus详解与引入
|
前端开发 JavaScript Java
前端——使用RequireJS的r.js打包压缩模块
前端——使用RequireJS的r.js打包压缩模块
|
Web App开发 前端开发 JavaScript
UMD 被淘汰了吗?不考虑的 UMD 的库如何在纯 UMD 前端项目中运行?
UMD 被淘汰了吗?不考虑的 UMD 的库如何在纯 UMD 前端项目中运行?
259 0
|
JavaScript CDN
vue_按需引入elment、echarts和路由懒加载,减少打包体积
vue_按需引入elment、echarts和路由懒加载,减少打包体积
230 0
|
JavaScript 前端开发 UED
如何实现一个基于Vue.js的图片懒加载插件
在Web开发中,图片懒加载是一个常用的技术,可以减少页面首次加载时的请求数量,提高页面的响应速度,降低带宽使用。在Vue.js框架中,我们可以很容易地实现一个基于Vue.js的图片懒加载插件。
164 0

热门文章

最新文章