侧边栏壁纸
博主头像
MicroMatrix博主等级

曲则全,枉则直,洼则盈,敝则新,少则得,多则惑。是以圣人抱一为天下式。不自见,故明;不自是,故彰;不自伐,故有功;不自矜,故长。夫唯不争,故天下莫能与之争。古之所谓“曲则全”者,岂虚言哉!诚全而归之。

  • 累计撰写 80 篇文章
  • 累计创建 21 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

基于esbuild搭建组件开发框架

蜗牛
2023-09-15 / 0 评论 / 0 点赞 / 8 阅读 / 18006 字 / 正在检测是否收录...

前言

在日常的前端开发中,经常需要开发一些组件。通常我们是基于某个特定的框架来开发,例如vue,react等等。对于页面的样式组件来说,没有什么太多的计较。但是如果开发一个画布工具或者一个音乐播放器的组件,那么这个组件必然会有很多功能,而且对于vue2/vue3,react版本,你可能每个都要开发一遍。那么web-component的开发理念就非常适合目前的需求了。但是通常的web-cmponent的开发中,对于JS我们可以很好的管理,拆分功能。但是对于dom样式的编写就极其不方便了。

什么是web-component开发理念

Web Component 是一套不同的技术,允许你创建可重用的定制元素(它们的功能封装在你的代码之外)并且在你的 web 应用中使用它们。它由三项主要技术组成:

  • 自定义元素(Custom Element):一组 JavaScript API,允许你定义 custom elements 及其行为,然后可以在你的用户界面中按照需要使用它们。
  • 影子DOM(Shadow DOM):一组 JavaScript API,用于将封装的“影子”DOM 树附加到元素(与主文档 DOM 分开呈现)并控制其关联的功能。通过这种方式,你可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
  • HTML模板(HTML Template):<template><slot> 元素使你可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。

Web Component 的优点是它是原生的,不需要加载任何外部库或框架,而且兼容性也越来越好。

那么这里非常难维护的是dom,我们在一个项目无法单独的写dom界面然后导入给控制的函数中使用。但是react的jsx的写法却能通过编译成JS然后使用,这就给了很方便的编写dom的方法,但是很多的打包工具需要配置jsx,所以这里我记录下我用esbuild搭建的一个开发项目。

搭建esbuild+jsx+typescript项目

首先说明下esbuild只是个打包工具,所以他除了打包之外可以说没有任何功能,直接编写.jsx 的文件他也无法识别。所以得给他配置插件,然后插件里让他识别jsx再转化成react的语法,然后reac给他编译成js文件,这样我们才能正常使用。这应该也是大部分的打包工具编写插件的基本思路吧。

  1. 第一步新建一个空文件夹

  2. 然后执行pnpm init,如果你没有安装pnpm的话去搜索下如何安装

  3. 新建一个pnpm-workspace.yaml文件,里面写入

    # 用来搭建monorepo管理项目
    packages:
      - "src/*"
    
  4. 执行下面的命令

    # 这3个插件是打包工具的核心
    pnpm install typescript minimist esbuild  -w -D
    
    # 这2个是转化jsx文件变成react-jsx
    pnpm install @babel/core @babel/plugin-transform-react-jsx -w -D
    
    # 这2个是预处理tsx中含有的ts语法。可以和上面的合在一起
    pnpm install --save-dev @babel/preset-env @babel/preset-typescript -w -D 
    
    # 这4个是React的插件,主要是编写的时候不报错,还有就是直接预览
    pnpm install react react-dom @types/react @types/react-dom
    
  5. 创建tsconfig.json 这里主要的一些就是识别@还有打包的一些参数

    {
    	"compilerOptions": {
    		"outDir": "dist", //输出目录
    		"sourceMap": true, //采用sourceMap
    		"target": "es2016", //目标语法
    		"module": "esnext", //模块格式
    		"moduleResolution": "node", //模块解析方式
    		"strict": true, //严格模式
    		"resolveJsonModule": true, //解析JSON模块
    		"esModuleInterop": true, //允许es6语法引入commonjs模块
    		"jsx": "react", //js不转译
    		"lib": ["esnext", "dom"], //支持的类库esnext及dom
    		"noEmit": true,
    		"allowImportingTsExtensions": true,
    		"forceConsistentCasingInFileNames": true,
    		"noImplicitAny": false,
    		"baseUrl": ".",
    		"paths": {
    			"@/*": ["src/*"]
    		}
    	},
    	"include": [
    		"src/**/*.ts",
    		"src/**/*.d.ts",
    		"src/**/*.tsx",
    		"src/**/*.jsx",
    		"src/**/*.css"
    	],
    	"exclude": []
    }
    
  6. 在根目录建立scripts/dev.js 写入下面的内容。都做了注释,具体可查看esbuild的文档

    // 这里用到了之前安装的minimist以及esbuild模块
    const args = require("minimist")(process.argv.slice(2)) // node scripts/dev.js reactivity -f global
    const { context } = require("esbuild");
    // console.log(args)
    const { resolve } = require('path');// node 内置模块
    
    
    const format = args.f || 'global';// 打包的格式
    
    // iife 立即执行函数 (function(){})();
    // cjs node中的模块 module.exports
    // esm 浏览器中的esModule模块 import
    const outputFormat = format.startsWith("global") ? 'iife' : format == "cjs" ? "cjs" : "esm";
    
    // 查看插件
    const watchBuild = () => ({
      name: "music-player",
      setup(build) {
        let count = 0;
        build.onEnd((result) => {
          if (count++ === 0) console.log("first build~~~~~");
          else console.log("subsequent build");
        });
      },
    });
    
    // jsx 转换插件
    const jsxTransform = () => ({
      name: "jsx-transform",
      setup(build) {
        const fs = require("fs");
        const babel = require("@babel/core");
        const plugin = require("@babel/plugin-transform-react-jsx").default(
          {},
          { runtime: "automatic" }
        );
        const presetEnv = require('@babel/preset-env');
        const presetTypescript = require('@babel/preset-typescript');
    
    
        build.onLoad({ filter: /\.[j|t]sx$/ }, async (args) => {
          const jsx = await fs.promises.readFile(args.path, "utf8");
          // 这里后面的预处理就处理了ts语法,不然不认识
          const result = babel.transformSync(jsx, { plugins: [plugin], presets: [presetEnv, presetTypescript], filename: args.path });
          return { contents: result.code };
        });
      },
    });
    
    const cssPlugin = require("esbuild-sass-plugin")
    //esbuild
    //天生就支持ts
    context({
      entryPoints: [resolve(__dirname, `../src/main.ts`)],
      outfile: 'dist/MusicPlayer.js', //输出的文件
      bundle: true, //把所有包全部打包到一起
      sourcemap: true,
      format: outputFormat, //输出格式
      globalName: "MusicPlayer", //打包全局名,上次在package.json中自定义的名字
      platform: format === "cjs" ? "node" : "browser",//项目运行的平台
      plugins: [watchBuild(), jsxTransform(), cssPlugin.sassPlugin()],
      jsxFactory: "h",
      jsxFragment: "Fragment",
      loader: {
        ".tsx": 'ts',
        ".jsx": 'js',
        ".js": 'js',
        ".ts": "ts",
        '.scss': 'css'
      },
      "treeShaking": true
    }).then((ctx) => {
      ctx.serve({
        servedir: '.',
        port: 8002,
      })
      return ctx
    }).then(ctx => ctx.watch())
    
  7. 这里顺带提供下build.js 的打包文件。注意我下面的target:es2015 表明打包成es5语法

    // 这里用到了之前安装的minimist以及esbuild模块
    const args = require("minimist")(process.argv.slice(2)); // node scripts/dev.js reactivity -f global
    const { context } = require("esbuild");
    // console.log(args)
    const { resolve } = require("path"); // node 内置模块
    
    const format = args.f || "global"; // 打包的格式
    
    // iife 立即执行函数 (function(){})();
    // cjs node中的模块 module.exports
    // esm 浏览器中的esModule模块 import
    const outputFormat = format.startsWith("global")
      ? "iife"
      : format == "cjs"
        ? "cjs"
        : "esm";
    
    const watchBuild = () => ({
      name: "draw-board",
      setup(build) {
        let count = 0;
        build.onEnd((result) => {
          console.log("打包完成")
          process.exit(0)
        });
      },
    });
    const jsxTransform = () => ({
      name: "jsx-transform",
      setup(build) {
        const fs = require("fs");
        const babel = require("@babel/core");
        const plugin = require("@babel/plugin-transform-react-jsx").default(
          {},
          { runtime: "automatic" }
        );
        const presetEnv = require('@babel/preset-env');
        const presetTypescript = require('@babel/preset-typescript');
    
    
        build.onLoad({ filter: /\.[j|t]sx$/ }, async (args) => {
          const jsx = await fs.promises.readFile(args.path, "utf8");
    
          const result = babel.transformSync(jsx, { plugins: [plugin], presets: [presetEnv, presetTypescript], filename: args.path });
          return { contents: result.code };
        });
      },
    });
    
    const fs = require("fs");
    let idPrefix = 'icon'
    const svgTitle = /<svg([^>+].*?)>/
    const clearHeightWidth = /(width|height)="([^>+].*?)"/g
    const hasViewBox = /(viewBox="[^>+].*?")/g
    const clearReturn = /(\r)|(\n)/g
    const findSvgFile = async (dir) => {
      const content = await fs.promises.readFile(dir, "utf8")
      const fileName = dir.replace(/^.*[\/]/, "").replace(/\.[^.]*$/, "")
      const svg = content.toString().replace(clearReturn, '').replace(svgTitle, ($1, $2) => {
        let width = '0'
        let height = '0'
        let content = $2.replace(
          clearHeightWidth,
          (s1, s2, s3) => {
            if (s2 === 'width') {
              width = s3
            } else if (s2 === 'height') {
              height = s3
            }
            return ''
          }
        )
        if (!hasViewBox.test($2)) {
          content += `viewBox="0 0 ${width} ${height}"`
        }
        return `<symbol id="${idPrefix}-${fileName}" ${content}>`
      }).replace('</svg>', '</symbol>')
      return { svg, fileName }
    }
    
    const svgBuilder = () => ({
      name: "svg-builder",
      setup(build) {
        build.onLoad({ filter: /\.svg$/ }, async (args) => {
          const path = args.path;
          let { svg, fileName } = await findSvgFile(path)
          let content = `export default  \`${svg}\`;`;
          return { contents: content }
        });
      },
    });
    
    const cssPlugin = require("esbuild-sass-plugin");
    //esbuild
    //天生就支持ts
    context({
      entryPoints: [resolve(__dirname, `../src/main.ts`)],
      outfile: "dist/DrawBoard.js", //输出的文件
      bundle: true, //把所有包全部打包到一起
      sourcemap: true,
      format: outputFormat, //输出格式
      globalName: "DrawBoard", //打包全局名,上次在package.json中自定义的名字
      platform: format === "cjs" ? "node" : "browser", //项目运行的平台
      plugins: [watchBuild(), jsxTransform(), cssPlugin.sassPlugin(), svgBuilder(),],
      jsxFactory: "h",
      jsxFragment: "Fragment",
      loader: {
        ".tsx": "tsx",
        ".jsx": "jsx",
        ".js": "js",
        ".ts": "ts",
        ".scss": "css",
        ".svg": "js"
      },
      treeShaking: true,
      target: 'es2015'
    })
      .then((ctx) =>
        ctx.watch()
      )
    
  8. 修改package.json文件 加入下面的内容,主要是打包的参数,还有esbuild对于jsx解析的配置,还有启动的时候的配置文件指向

    {
    ....
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "dev": "node scripts/dev.js reactivity -f global"
      },
    "buildOptions": {
        "name": "VueReactivity",
        "formats": [
          "global",
          "cjs",
          "esm-bundler"
        ]
      },
      "jsxFactory": "h",
      "jsxFragmentFactory": "Fragment"
    }
    
  9. 大部分的配置搭建都完毕了,现在新建src/main.ts src/main.tsx index.html

    main.ts中写一些代码

    import "./main.tsx"
    console.log(123)
    

    main.tsx中写入一些页面布局,语法的话和react一模一样

    // main.tsx
    import React from "react";
    import reactDom from "react-dom/client";
    function App(){
    	return <div>hello world</div>
    }
    export const root = reactDom.createRoot(document.getElementById("app")!);
    root.render(<App />);
    

    index.html的内容就引入我们打包出的js和css

    <!DOCTYPE html>
    <html lang="en">
    	<head>
    		<meta charset="UTF-8" />
    		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
    		<title>音乐播放器</title>
    		<script type="module" src="./dist/MusicPlayer.js"></script>
    		<link href="./dist/MusicPlayer.css" rel="stylesheet" />
    	</head>
    	<body>
    		<div id="app"></div>
    	</body>
    </html>
    

编写SVG组件和给esbuild编写SVG插件

现在可以很好的编写dom组件了,但是在日常开发中会有很多图标的使用,而这些图标大部分都是svg的格式,那么我就要非常优雅的使用svg的图标了。这里使用的思路依旧和我之前的文章前端项目优雅使用svg 的思路是一样的,那么我们的主要问题是如何给esbuild编写插件了。

  1. src/assets/icon文件夹里面新建index.ts文件,然后放入2张svg图标文件caret-left-fill.svgcaret-right-fill.svg 。这里

    // index.ts
    import prev from "./caret-left-fill.svg";
    import next from "./caret-right-fill.svg";
    let res = [prev, next];
    
    export const svgContent = ` <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position: absolute; width: 0; height: 0">${res.join(
    	""
    )}</svg>`;
    

    ⚠️注意:这里会在import提示找不到xx模块。需要在src下面新建一个svg.d.ts

    // svg.d.ts
    declare module "*.svg" {
    	export const template: any;
    	export default template;
    }
    
  2. 在esbuild配置文件中配置的入口文件里面写入如下代码,引入我们的svg中的index.ts,让接下来的esbuild捕获解析步骤能监控svg文件。

    // main.ts
    import { svgContent } from "@/assets/icon";
    
    const stringToHTML = function (str) {
    	var parser = new DOMParser();
    	var doc = parser.parseFromString(str, "text/html");
    	return doc.body.firstElementChild;
    };
    let svgUrl = stringToHTML(svgContent);
    
    let app = document.getElementById("app");
    app ? true : (app = document.createElement("div"));
    app.setAttribute("id", "app");
    
    svgUrl ? body.appendChild(svgUrl) : false;
    
  3. srcipt/dev.js(esbuld配置文件)中加入下面一个插件。

    const fs = require("fs");
    let idPrefix = 'icon'
    const svgTitle = /<svg([^>+].*?)>/
    const clearHeightWidth = /(width|height)="([^>+].*?)"/g
    const hasViewBox = /(viewBox="[^>+].*?")/g
    const clearReturn = /(\r)|(\n)/g
    const findSvgFile = async (dir) => {
      const content = await fs.promises.readFile(dir, "utf8")
      const fileName = dir.replace(/^.*[\/]/, "").replace(/\.[^.]*$/, "")
      const svg = content.toString().replace(clearReturn, '').replace(svgTitle, ($1, $2) => {
        let width = '0'
        let height = '0'
        let content = $2.replace(
          clearHeightWidth,
          (s1, s2, s3) => {
            if (s2 === 'width') {
              width = s3
            } else if (s2 === 'height') {
              height = s3
            }
            return ''
          }
        )
        if (!hasViewBox.test($2)) {
          content += `viewBox="0 0 ${width} ${height}"`
        }
        return `<symbol id="${idPrefix}-${fileName}" ${content}>`
      }).replace('</svg>', '</symbol>')
      return { svg, fileName }
    }
    
    const svgBuilder = () => ({
      name: "svg-builder",
      setup(build) {
        build.onLoad({ filter: /\.svg$/ }, async (args) => {
          const path = args.path;
          let { svg, fileName } = await findSvgFile(path)
          let content = `export default  \`${svg}\`;`;
          return { contents: content }
        });
      },
    });
    

    然后在plugins中加入svgBuilder()

  4. 编写SVG组件SvgIcon.tsx

    import React, { useMemo } from "react";
    
    function SvgIcon(props) {
    	const iconName = useMemo(() => {
    		return `#icon-${props.iconClass}`;
    	}, [props.iconClass]);
    
    	const svgClass = useMemo(() => {
    		if (props.className) {
    			return "svg-icon " + props.className;
    		} else {
    			return "svg-icon";
    		}
    	}, [props.className]);
    	return (
    		<svg className={svgClass} aria-hidden="true">
    			<use xlinkHref={iconName} />
    		</svg>
    	);
    }
    
    export default SvgIcon;
    

    使用的话就直接引入组件,下面是示范代码

    import React from "react";
    import SvgIcon from "@/components/SvgIcon";
    
    export function Index() {
    	return (
    		<div className={"audio-player-wrapper"}>
    			<SvgIcon iconClass={"caret-left-fill"} className={undefined}></SvgIcon>
    		</div>
    	);
    }
    

安装scss解析插件

pnpm install esbuild-sass-plugin -w -D

然后在esbuild的配置文件scripts/dev.js中的plugins加入cssPlugin.sassPlugin()

0

评论区