挑战21天手写前端框架 day10 手撕 esbuild 插件开发完成对 style 的支撑

简介: 挑战21天手写前端框架 day10 手撕 esbuild 插件开发完成对 style 的支撑

image.png


阅读本文需要 30 分钟,编写本文耗时 4 小时


最佳实践的目录结构

我们现在的页面还都写在 src/index.tsx 中,但是当我们项目变得复杂之后,仅用一个文件来管理整个项目显然是不行的,因此,我们借鉴一下 umi 的最佳实践将我们的页面按以下的目录结构重新调整一下。这是当前我们需要用到的文件目录,后续我们会不断的扩展我们需要的目录结构。

.
├── dist
├── src
│   ├── layout
│   │   ├── inedx.tsx
│   │   └── index.less
│   └── pages
│       ├── index.less
│       └── index.tsx
├── node_modules
├── package.json
├── tsconfig.json
└── typings.d.ts
复制代码


dist 目录

执行 malita build 后,产物默认会存放在这里。


/src 目录

layouts/index.tsx

约定式路由时的全局布局文件,后续我们会默认用他来包裹我们的路由。比如,你的路由是:

[
  { path: '/', component: './pages/index' },
  { path: '/users', component: './pages/users' },
]
复制代码


从组件角度可以简单的理解为如下关系:

<layout>
  <page>1</page>
  <page>2</page>
</layout>
复制代码


pages 目录

所有路由组件存放在这里。使用约定式路由时,约定 pages 下所有的 (j|t)sx? 文件即路由。使用约定式路由,意味着不需要维护,可怕的路由配置文件。约定式路由的实现我们会在明天完成。


调整当前的文件

将我们当前的 src/index.tsx 进行拆解。比如将 Layout 的内容存放到 src/layouts/index.tsx

import React from 'react';
import { useLocation } from 'react-router-dom';
import { Page, Content, Header } from '@alita/flow';
import { useKeepOutlets } from '@malita/keepalive';
const Layout = () => {
    const { pathname } = useLocation();
    const element = useKeepOutlets();
    return (
        <Page>
            <Header>当前路由: {pathname}</Header>
            <Content>
                {element}
            </Content>
        </Page>
    )
}
export default Layout;
复制代码


Hello 放到首页 src/pages/home/tsx

import React, { useState } from 'react';
import { Link } from 'react-router-dom';
const Hello = () => {
    const [text, setText] = React.useState('Hello Malita!');
    const [count, setCount] = useState(0);
    return (
        <>
            <p
                onClick={() => {
                    setText('Hi!');
                }}
            > {text} </p>
            <p>{count}</p>
            <p><button onClick={() => setCount(count => count + 1)}> Click Me! Add!</button></p>
            <Link to='/users'>go to Users</Link>
        </>);
};
export default Hello;
复制代码


同理整理 UsersMe 页面,然后修改项目主入口 src/index.tsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import { HashRouter, Routes, Route, } from 'react-router-dom';
import KeepAliveLayout from '@malita/keepalive';
import Layout from './layouts/index';
import Hello from './pages/home';
import Users from './pages/users';
const App = () => {
    return (
        <KeepAliveLayout keepalive={[/./]}>
            <HashRouter>
                <Routes>
                    <Route path='/' element={<Layout />}>
                        <Route path="/" element={<Hello />} />
                        <Route path="/users" element={<Users />} />
                    </Route>
                </Routes>
            </HashRouter>
        </KeepAliveLayout>
    );
}
const root = ReactDOM.createRoot(document.getElementById('malita'));
root.render(React.createElement(App));
复制代码

这样修改完之后,我们的整个项目的结构就会变得很清晰。这样开发人员就可以快速的上手开发,比如,维护一个老项目的时候,有个 bug 出现在 home 路由,你就知道,你只需要修改 home.tsx 而不用到处找文件。


支持 css

给我们的页面加一点简单的样式。我们这里加两个 css 文件。

src/layouts/index.css

.malita-layout {
  font-size: 24px;
}
复制代码


src/pages/home.css

.malita-home {
  font-size: 24px;
}
复制代码


分别在 layout 和 home 页面中 import "./index.css"import "./home.css";

然后在页面中使用它们。随便用,你喜欢加哪就加哪,比如

<p className='malita-home'>{count}</p>
复制代码


执行 pnpm dev,发现在 www 目录下新增了一个 index.css

/* src/layouts/index.css */
.malita-layout {
  font-size: 24px;
}
/* src/pages/home.css */
.malita-home {
  font-size: 32px;
  background: blue;
}
复制代码


此时我们只要将 index.css 文件挂载到 html 上就可以加载支持 css 了

<link href="/${DEFAULT_OUTDIR}/index.css" rel="stylesheet"></link>
复制代码

image.png

从效果上来看,我们的项目已经支持 css 了。但是,从感觉上不像那么一回事,有没有一种更加智能的东西能够帮助我们实现这个功能呢?


esbuild 插件编写

因为 esbuild 比较新,生态还是比较少的,所以遇到一些需求,我个人更倾向于自己实现插件。

esbuild 的插件 api 还是比较简单的,只有 onResolve 和 onLoad,一个用来处理路径相关的问题,一个用来处理加载数据。


onResolve

可以用它来处理路径相关的需求。

onResolve({ filter: filter }, async (args) => {
    return {
        path,
    };
});
复制代码


filter 表示路径的过滤条件,在 onLoad 中也是一样的用法,比如你要处理所有的 md 文件,你可以用 filter: /\.md$/,比如给所有的 md 文件添加前缀就是

onResolve({ filter: /\.md$/ }, async (args) => {
    // 只是演示,给这个路径加前缀没什么实际作用
    const path = `prefix/${args.path}`;
    return {
        path,
    };
});
复制代码


onResolve 还有一个使用的用法,是在 return 的时候指定 namespace,默认的namespace 一般是 file。你可以通过指定 namespace 把文件归类,这样在 onLoad 中可以针对这些文件做特殊处理。

比如官网中的例子:

onResolve({ filter: /^env$/ }, args => ({
    path: args.path,
    namespace: 'env-ns',
}))
onLoad({ filter: /.*/, namespace: 'env-ns' }, () => ({
    contents: JSON.stringify(process.env),
    loader: 'json',
}))
复制代码


这样你就可以在项目代码中使用 env,哪怕实际上并不存在这个模块和文件。

import { PATH } from 'env'
console.log(`PATH is ${PATH}`)
复制代码


onLoad

可以用它来处理内容相关的需求。一般是对文件内容有修改的时候,会用到它。 比如上面的例子,就是把本来是不存在内容,指定为 JSON.stringify(process.env) 所以就算本来这个文件不存在,在项目中也可以正常使用。

我们可以用它来实现一些 esbuild 官方还不支持的 loader,比如处理 less 文件,使用 postcss 等。


这里用 less 做个演示:

onLoad({ filter: /\.less$/ }, async (args) => {
    let content = await fs.readFile(args.path, 'utf-8');
    const dir = path.dirname(args.path);
    const filename = path.basename(args.path);
    const result = await less.render(content, {
        filename,
        rootpath: dir,
        paths: [...(lessOptions.paths || []), dir],
    });
    return {
        contents: result.css,
        loader: 'css',
        resolveDir: dir,
    };
});
复制代码


因为 esbuild 不能识别 less 文件,所以上面的例子中我们用 less.render 将 less 文件转换成 esbuild 能够识别的 css 文件。

当然 less 的实际处理要比上述的复杂的多,这里只是演示。


实现 esbuild 的 styles 插件

使用 onResolve 匹配所有 .css 路径的文件,将它的命名空间改到 style-stub 上。

onResolve({ filter: /\.css$/, namespace: 'file' }, (args) => {
      return { path: args.path, namespace: 'style-stub' };
    });
复制代码


使用 onLoad 修改 style-stub 中的返回内容

onLoad({ filter: /.*/, namespace: 'style-stub' }, async (args) => ({
    contents: `
  import { injectStyle } from "__style_helper__"
  import css from ${JSON.stringify(args.path)}
  injectStyle(css)
`,
}));
复制代码


这里又 import 两个文件,一个是 __style_helper__,另一个是它的原始路径。 因为我们项目根本就没有 __style_helper__ 文件,所以我们需要给这个路径加载的时候返回我们需要的代码。这就是我前面说过的“无中生有”。

还是想用 onResolve 匹配路径,将它指向 style-helper

onResolve(
    { filter: /^__style_helper__$/, namespace: 'style-stub' },
    (args) => ({
        path: args.path,
        namespace: 'style-helper',
        sideEffects: false,
    }),
);
复制代码


然后使用 onLoad 返回我们需要的代码工具类,值得注意的是 onLoad 就是构建的最后一环了,所以我们需要返回正确的 es5 语法。

onLoad({ filter: /.*/, namespace: 'style-helper' }, async () => ({
    contents: `
  export function injectStyle(text) {
    if (typeof document !== 'undefined') {
      var style = document.createElement('style')
      var node = document.createTextNode(text)
      style.appendChild(node)
      document.head.appendChild(style)
    }
  }
`,
}));
复制代码


接下来我们就来处理,它的另一个 import 它原始路径的逻辑处理。根据我们上面提到的 esbuild 的插件编写,遇到路径处理,都用 onResolve

onResolve({ filter: /\.css$/, namespace: 'style-stub' }, (args) => {
    return { path: args.path, namespace: 'style-content' };
});
复制代码


然后我们返回项目中真实的 css 文件,这时候就可以用上 esbuild 对 css 进行构建了。

onLoad(
    {
        filter: /.*/,
        namespace: 'style-content',
    },
    async (args) => {
        const { errors, warnings, outputFiles } = await esbuild.build(
            {
                entryPoints: [args.path],
                logLevel: 'silent',
                bundle: true,
                write: false,
                charset: 'utf8',
                minify: true,
                loader: {
                    '.svg': 'dataurl',
                    '.ttf': 'dataurl',
                },
            }
        );
        return {
            errors,
            warnings,
            contents: outputFiles![0].text,
            loader: 'text',
        };
    },
);
复制代码


整个过程非常的巧妙,逻辑简述的话,大概如下

import "./home.css"
复制代码


会被转换成

// import css from "./css"
var css = ".malita-home { font-size: 32px;background: blue;}";
function injectStyle(text) {
    if (typeof document !== 'undefined') {
        var style = document.createElement('style')
        var node = document.createTextNode(text)
        style.appendChild(node)
        document.head.appendChild(style)
    }
}
injectStyle(css); 
复制代码


这样只要我们加载了 js ,就会自动挂载我们用到的 css 文件了。

上述的实现,是为了将如何编写 esbuild 插件将清除,里面有很多实现细节被我忽略了,真正的实现在 @umijs/bundler-esbuild 中,感兴趣的朋友,可以进一步去阅读 umi 的源码。


明天我们会完成约定式路由的实现,到此 malita 就会成为一个真正的框架了。感谢关注,感谢阅读。

今天的内容是比较新的 esbuild 的插件开发,相信熟悉的朋友不是很多。希望这篇文章能够帮到你们。


最近的系列文章收到了一些朋友的反馈,也感谢朋友们指出文章中出现的错误,手误和概念缺失,我都会一一改正。

还有朋友推荐我将标题改成《umi 核心开发人员带你21天手写前端框架》,关注的人会更多。哈哈哈,我没好意思放。


希望能够得到更多朋友的互动,我希望将这东西写成一个玩具,在写它的时候,能够学到更多的东西。


源码归档

目录
相关文章
|
前端开发
前端学习笔记202303学习笔记第五天-style节点的基本使用
前端学习笔记202303学习笔记第五天-style节点的基本使用
190 0
|
前端开发 开发者
前端祖传三件套HTML的常用属性之style
HTML是前端开发的基础知识之一,而style属性是其中一个常用的属性。在HTML中,style属性可以帮助我们为元素设置样式,使网页呈现出更好的视觉效果。本文将介绍style属性的定义、用法以及注意事项,帮助广大前端开发者更好地掌握style属性。
1021 0
|
JavaScript 前端开发
前端-vue基础21-style样式处理
前端-vue基础21-style样式处理
213 0
前端-vue基础21-style样式处理
|
存储 人工智能 前端开发
前端大模型应用笔记(三):Vue3+Antdv+transformers+本地模型实现浏览器端侧增强搜索
本文介绍了一个纯前端实现的增强列表搜索应用,通过使用Transformer模型,实现了更智能的搜索功能,如使用“番茄”可以搜索到“西红柿”。项目基于Vue3和Ant Design Vue,使用了Xenova的bge-base-zh-v1.5模型。文章详细介绍了从环境搭建、数据准备到具体实现的全过程,并展示了实际效果和待改进点。
1249 14
|
JavaScript 前端开发 程序员
前端学习笔记——node.js
前端学习笔记——node.js
455 0
|
SpringCloudAlibaba JavaScript 前端开发
谷粒商城笔记+踩坑(2)——分布式组件、前端基础,nacos+feign+gateway+ES6+vue脚手架
分布式组件、nacos注册配置中心、openfegin远程调用、网关gateway、ES6脚本语言规范、vue、elementUI
谷粒商城笔记+踩坑(2)——分布式组件、前端基础,nacos+feign+gateway+ES6+vue脚手架
|
人工智能 自然语言处理 运维
前端大模型应用笔记(一):两个指令反过来说大模型就理解不了啦?或许该让第三者插足啦 -通过引入中间LLM预处理用户输入以提高多任务处理能力
本文探讨了在多任务处理场景下,自然语言指令解析的困境及解决方案。通过增加一个LLM解析层,将复杂的指令拆解为多个明确的步骤,明确操作类型与对象识别,处理任务依赖关系,并将自然语言转化为具体的工具命令,从而提高指令解析的准确性和执行效率。
622 6
|
机器学习/深度学习 弹性计算 自然语言处理
前端大模型应用笔记(二):最新llama3.2小参数版本1B的古董机测试 - 支持128K上下文,表现优异,和移动端更配
llama3.1支持128K上下文,6万字+输入,适用于多种场景。模型能力超出预期,但处理中文时需加中英翻译。测试显示,其英文支持较好,中文则需改进。llama3.2 1B参数量小,适合移动端和资源受限环境,可在阿里云2vCPU和4G ECS上运行。
855 1
|
前端开发 算法 测试技术
前端大模型应用笔记(五):大模型基础能力大比拼-计数篇-通义千文 vs 文心一言 vs 智谱 vs 讯飞vsGPT
本文对比测试了通义千文、文心一言、智谱和讯飞等多个国产大模型在处理基础计数问题上的表现,特别是通过链式推理(COT)提示的效果。结果显示,GPTo1-mini、文心一言3.5和讯飞4.0Ultra在首轮测试中表现优秀,而其他模型在COT提示后也能显著提升正确率,唯有讯飞4.0-Lite表现不佳。测试强调了COT在提升模型逻辑推理能力中的重要性,并指出免费版本中智谱GLM较为可靠。
821 0
前端大模型应用笔记(五):大模型基础能力大比拼-计数篇-通义千文 vs 文心一言 vs 智谱 vs 讯飞vsGPT

热门文章

最新文章

  • 1
    前端如何存储数据:Cookie、LocalStorage 与 SessionStorage 全面解析
    799
  • 2
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(九):强势分析Animation动画各类参数;从播放时间、播放方式、播放次数、播放方向、播放状态等多个方面,完全了解CSS3 Animation
    358
  • 3
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(八):学习transition过渡属性;本文学习property模拟、duration过渡时间指定、delay时间延迟 等多个参数
    279
  • 4
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(七):学习ransform属性;本文学习 rotate旋转、scale缩放、skew扭曲、tanslate移动、matrix矩阵 多个参数
    245
  • 5
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(六):全方面分析css的Flex布局,从纵、横两个坐标开始进行居中、两端等元素分布模式;刨析元素间隔、排序模式等
    368
  • 6
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(五):背景属性;float浮动和position定位;详细分析相对、绝对、固定三种定位方式;使用浮动并清除浮动副作用
    524
  • 7
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(四):元素盒子模型;详细分析边框属性、盒子外边距
    380
  • 8
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(三):元素继承关系、层叠样式规则、字体属性、文本属性;针对字体和文本作样式修改
    168
  • 9
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(二):CSS伪类:UI伪类、结构化伪类;通过伪类获得子元素的第n个元素;创建一个伪元素展示在页面中;获得最后一个元素;处理聚焦元素的样式
    359
  • 10
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(一):CSS发展史;CSS样式表的引入;CSS选择器使用,附带案例介绍
    321