挑战21天手写前端框架 day12 云谦:动态生成的入口文件,框架才有了意义

简介: 挑战21天手写前端框架 day12 云谦:动态生成的入口文件,框架才有了意义

image.png


标题来自云谦的星球

阅读本文需要 5 分钟,编写本文耗时 1 小时


极简的生命周期

  1. 获取应用元数据
  2. 获取路由配置(约定式路由)
  3. 动态生成应用主入口文件
  4. 动态生成 HTML
  5. 执行构建

昨天我们已经完成了 “获取应用元数据” 和 “获取路由配置(约定式路由)”,今天我们就直接进入主题,看看两个动态生成流程是如何实现的。



动态生成应用主入口文件

首先我们重新指定我们的项目主入口,因为是临时生成的,因此我们将它放到 absTmpPath 临时目录中。

packages/malita/src/constants.ts

- export const DEFAULT_ENTRY_POINT = 'malita.tsx';
+ export const DEFAULT_ENTRY_POINT = 'src/index.tsx';
复制代码


其实我们的需求是非常清晰的,就是通过之前获取到的路由数据,生成我们现在的入口文件,即 examples/app/src/index.tsx

我们需要写一个工具,将 routes 配置,转换成真实可用的代码。

[
    path: '/',
    element: '/malita/examples/app/src/layouts/index'
    routes: [{
            path: '/',
            element: '/malita/examples/app/src/pages/home'
        },
        {
            path: '/users',
            element: '/malita/examples/app/src/pages/users'
        }
    ]
]
复制代码


转换为

import React from 'react';
import ReactDOM from 'react-dom/client';
import { HashRouter, Routes, Route, } from 'react-router-dom';
import KeepAliveLayout from '@malitajs/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));
复制代码


仔细观察,其实我们需要关注的仅仅是与组件相关的这几行代码的动态生成。

import Layout from './layouts/index';
import Hello from './pages/home';
import Users from './pages/users';
<Route path='/' element={<Layout />}>
    <Route path="/" element={<Hello />} />
    <Route path="/users" element={<Users />} />
</Route>
复制代码


只要根据配置生成对应的字符串即可。由于 esbuild 不转换 ast,所以我们这里做了一个简化,给导入页面随便写一个名字。

import A1 from './layouts/index';
复制代码


简单是实现如下:

let count = 1;
const getRouteStr = (routes: IRoute[]) => {
    let routesStr = '';
    let importStr = '';
    routes.forEach(route => {
        count += 1;
        importStr += `import A${count} from '${route.element}';\n`;
        routesStr += `\n<Route path='${route.path}' element={<A${count} />}>`;
        if (route.routes) {
            const { routesStr: rs, importStr: is } = getRouteStr(route.routes);
            routesStr += rs;
            importStr += is;
        }
        routesStr += '</Route>\n';
    })
    return { routesStr, importStr };
}
复制代码


最终我们就可以得到,整个文件的字符串如下:

const { routesStr, importStr } = getRouteStr(routes);
        const content = `
import React from 'react';
import ReactDOM from 'react-dom/client';
import { HashRouter, Routes, Route, } from 'react-router-dom';
import KeepAliveLayout from '@malitajs/keepalive';
${importStr}
const App = () => {
    return (
        <KeepAliveLayout keepalive={[/./]}>
            <HashRouter>
                <Routes>
                    ${routesStr}
                </Routes>
            </HashRouter>
        </KeepAliveLayout>
    );
}
const root = ReactDOM.createRoot(document.getElementById('malita'));
root.render(React.createElement(App));
    `;
复制代码


然后将字符串写到对应文件中即可writeFileSync(appData.paths.absEntryPath, content, 'utf-8');


指的注意的是当目标文件的所在文件夹不存在的时候,是无法完成文件写入的,所以我们可以先创建目标文件所在的文件夹。这个在微生成器的实现部分,是一个非常需要注意的地方。

import { mkdir, writeFileSync } from 'fs';
let count = 1;
const getRouteStr = (routes: IRoute[]) => {}
export const generateEntry = ({ appData, routes }: { appData: AppData; routes: IRoute[] }) => {
    return new Promise((resolve, rejects) => {
        count = 0;
        const { routesStr, importStr } = getRouteStr(routes);
        const content = `
import React from 'react';
import ReactDOM from 'react-dom/client';
import { HashRouter, Routes, Route, } from 'react-router-dom';
import KeepAliveLayout from '@malitajs/keepalive';
${importStr}
const App = () => {
    return (
        <KeepAliveLayout keepalive={[/./]}>
            <HashRouter>
                <Routes>
                    ${routesStr}
                </Routes>
            </HashRouter>
        </KeepAliveLayout>
    );
}
const root = ReactDOM.createRoot(document.getElementById('malita'));
root.render(React.createElement(App));
    `;
        try {
            mkdir(path.dirname(appData.paths.absEntryPath), { recursive: true }, (err) => {
                if (err) {
                    rejects(err)
                }
                writeFileSync(appData.paths.absEntryPath, content, 'utf-8');
                resolve({})
            });
        } catch (error) {
            rejects({})
        }
    })
}
复制代码



动态生成 HTML

还是一样的思路,动态生成,我们还是依照模版分析。我们要从之前获取到的数据,生成我们需要的 html 字符串。

app.get('/', (_req, res) => {
        res.set('Content-Type', 'text/html');
        res.send(`<!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <title>Malita</title>
        </head>
        <body>
            <div id="malita">
                <span>loading...</span>
            </div>
            <script src="/${DEFAULT_OUTDIR}/index.js"></script>
            <script src="/malita/client.js"></script>
        </body>
        </html>`);
    });
复制代码


这里我们取 package.json 中的 name 作为页面的 title然后,修改引入 js 的真实路径,并将 html 写到根路径的 index.html

import { mkdir, writeFileSync } from 'fs';
import path from 'path';
import type { AppData } from './appData';
import { DEFAULT_FRAMEWORK_NAME, DEFAULT_OUTDIR } from './constants';
export const generateHtml = ({ appData }: { appData: AppData; }) => {
    return new Promise((resolve, rejects) => {
        const content = `
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <title>${appData.pkg.name ?? 'Malita'}</title>
        </head>
        <body>
            <div id="malita">
                <span>loading...</span>
            </div>
            <script src="/${DEFAULT_OUTDIR}/${DEFAULT_FRAMEWORK_NAME}.js"></script>
            <script src="/malita/client.js"></script>
        </body>
        </html>`;
        try {
            const htmlPath = path.resolve(appData.paths.absOutputPath, 'index.html')
            mkdir(path.dirname(htmlPath), { recursive: true }, (err) => {
                if (err) {
                    rejects(err)
                }
                writeFileSync(htmlPath, content, 'utf-8');
                resolve({})
            });
        } catch (error) {
            rejects({})
        }
    })
}
复制代码



执行构建

单独看这几个生命周期,会觉得它们相互之间的关联性并不是太强烈,会有带着一个的疑问:为什么要这么写,为什么要生成这个文件?这我们在构建环节就会将这几个数据和临时文件串联到一起。

malitaServe.listen(port, async () => {
        console.log(`App listening at http://${DEFAULT_HOST}:${port}`);
        try {
            // 生命周期
            // 获取项目元信息 
            const appData = await getAppData({
                cwd
            });
            // 获取 routes 配置
            const routes = await getRoutes({ appData });
            // 生成项目主入口
            await generateEntry({ appData, routes });
            // 生成 Html
            await generateHtml({ appData });
            // 执行构建
            await build({
                // 没修改的配置,这里简略了,不是删除了哦
                outdir: appData.paths.absOutputPath,
                entryPoints: [appData.paths.absEntryPath],
            });
        } catch (e) {
            console.log(e);
            process.exit(1);
        }
    });
复制代码


我们使用 esbuild 将新的项目主入口,构建到产物路径中,因此我们要同步的修改,我们的 html 获取方法。

const output = path.resolve(cwd, DEFAULT_OUTDIR);
    app.get('/', (_req, res, next) => {
        res.set('Content-Type', 'text/html');
        const htmlPath = path.join(output, 'index.html');
        if (fs.existsSync(htmlPath)) {
            fs.createReadStream(htmlPath).on('error', next).pipe(res);
        } else {
            next();
        }
    });
复制代码

判断 html 是否生成成功,再返回 html。

fs.createReadStream(htmlPath).on('error', next).pipe(res); 来自辟殊 (pshu)



新的问题产生了

我们将应用主入口从项目中,移到了框架中,当前的问题就是我们之前的 配置,被写死到了,生成的临时文件中。

我们的页面 title,也是我们默认的取的 package.json 中的 name 这可能并不是用户想要的。


因此这些数据应该从项目传到框架中,因此就出现了“用户配置”需求。这内容我们会在明天实现。


感谢阅读,今天的内容主要是补充了昨天缺漏的实现。对于实现,也仅仅是实现我们此刻的需求和场景。但正是因为简单,反而更适合新手阅读。不知道这样的编写方式,你是觉得适合你呢,还是太简单啰嗦了呢?我期待你的反馈。


源码归档

目录
相关文章
|
10天前
|
前端开发 JavaScript 开发者
颠覆传统:React框架如何引领前端开发的革命性变革
【10月更文挑战第32天】本文以问答形式探讨了React框架的特性和应用。React是一款由Facebook推出的JavaScript库,以其虚拟DOM机制和组件化设计,成为构建高性能单页面应用的理想选择。文章介绍了如何开始一个React项目、组件化思想的体现、性能优化方法、表单处理及路由实现等内容,帮助开发者更好地理解和使用React。
36 9
|
2月前
|
前端开发
前端引入字体文件
文章介绍了如何在前端项目中引入字体文件,并展示了具体的HTML和CSS代码示例,包括如何使用`@font-face`规则来定义字体和在页面中应用自定义字体。
67 1
前端引入字体文件
|
24天前
|
人工智能 前端开发 JavaScript
前端架构思考 :专注于多框架的并存可能并不是唯一的方向 — 探讨大模型时代前端的分层式微前端架构
随着前端技术的发展,微前端架构成为应对复杂大型应用的流行方案,允许多个团队使用不同技术栈并将其模块化集成。然而,这种设计在高交互性需求的应用中存在局限,如音视频处理、AI集成等。本文探讨了传统微前端架构的不足,并提出了一种新的分层式微前端架构,通过展示层与业务层的分离及基于功能的横向拆分,以更好地适应现代前端需求。
|
4天前
|
前端开发 JavaScript API
前端界的秘密武器:掌握这些框架,让你轻松秒杀99%的同行!
前端开发日新月异,掌握几个明星框架如React、Vue.js和Angular,不仅能让工作更得心应手,还能轻松超越同行。React以高效的虚拟DOM和组件化著称;Vue.js简洁易懂,灵活性高;Angular提供全面的解决方案,适合大型应用。此外,轻量级的Svelte也值得关注,其编译时处理设计提升了应用性能。掌握这些框架,结合深刻理解和灵活运用,助你在前端领域脱颖而出。
17 9
|
1月前
|
JavaScript 前端开发 API
Vue.js:现代前端开发的强大框架
【10月更文挑战第11天】Vue.js:现代前端开发的强大框架
65 41
|
16天前
|
前端开发 JavaScript
Bootstrap Web 前端 UI 框架
Bootstrap 是快速开发 Web 应用程序的前端工具包。
30 3
|
22天前
|
JavaScript 前端开发 测试技术
前端全栈之路Deno篇(五):如何快速创建 WebSocket 服务端应用 + 客户端应用 - 可能是2025最佳的Websocket全栈实时应用框架
本文介绍了如何使用Deno 2.0快速构建WebSocket全栈应用,包括服务端和客户端的创建。通过一个简单的代码示例,展示了Deno在WebSocket实现中的便捷与强大,无需额外依赖,即可轻松搭建具备基本功能的WebSocket应用。Deno 2.0被认为是最佳的WebSocket全栈应用JS运行时,适合全栈开发者学习和使用。
|
22天前
|
缓存 前端开发 JavaScript
前端serverless探索之组件单独部署时,利用rxjs实现业务状态与vue-react-angular等框架的响应式状态映射
本文深入探讨了如何将RxJS与Vue、React、Angular三大前端框架进行集成,通过抽象出辅助方法`useRx`和`pushPipe`,实现跨框架的状态管理。具体介绍了各框架的响应式机制,展示了如何将RxJS的Observable对象转化为框架的响应式数据,并通过示例代码演示了使用方法。此外,还讨论了全局状态源与WebComponent的部署优化,以及一些实践中的改进点。这些方法不仅简化了异步编程,还提升了代码的可读性和可维护性。
|
23天前
|
前端开发 JavaScript 中间件
前端全栈之路Deno篇(四):Deno2.0如何快速创建http一个 restfulapi/静态文件托管应用及oak框架介绍
Deno 是由 Node.js 创始人 Ryan Dahl 开发的新一代 JavaScript 和 TypeScript 运行时,旨在解决 Node.js 的设计缺陷,具备更强的安全性和内置的 TypeScript 支持。本文介绍了如何使用 Deno 内置的 `Deno.serve` 快速创建 HTTP 服务,并详细讲解了 Oak 框架的安装和使用方法,包括中间件、路由和静态文件服务等功能。Deno 和 Oak 的结合使得创建 RESTful API 变得高效且简便,非常适合快速开发和部署现代 Web 应用程序。
|
28天前
|
前端开发 JavaScript 开发者
qiankun(乾坤)微前端框架简介
qiankun(乾坤)微前端框架简介
79 1