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