挑战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 这可能并不是用户想要的。


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


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


源码归档

目录
相关文章
|
27天前
|
Dart 前端开发
【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
116 75
【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
|
2月前
|
开发框架 小程序 前端开发
圈子社交app前端+后端源码,uniapp社交兴趣圈子开发,框架php圈子小程序安装搭建
本文介绍了圈子社交APP的源码获取、分析与定制,PHP实现的圈子框架设计及代码编写,以及圈子小程序的安装搭建。涵盖环境配置、数据库设计、前后端开发与接口对接等内容,确保平台的安全性、性能和功能完整性。通过详细指导,帮助开发者快速搭建稳定可靠的圈子社交平台。
|
3月前
|
前端开发 JavaScript API
前端界的秘密武器:掌握这些框架,让你轻松秒杀99%的同行!
前端开发日新月异,掌握几个明星框架如React、Vue.js和Angular,不仅能让工作更得心应手,还能轻松超越同行。React以高效的虚拟DOM和组件化著称;Vue.js简洁易懂,灵活性高;Angular提供全面的解决方案,适合大型应用。此外,轻量级的Svelte也值得关注,其编译时处理设计提升了应用性能。掌握这些框架,结合深刻理解和灵活运用,助你在前端领域脱颖而出。
49 9
|
3月前
|
前端开发 JavaScript 开发者
颠覆传统:React框架如何引领前端开发的革命性变革
【10月更文挑战第32天】本文以问答形式探讨了React框架的特性和应用。React是一款由Facebook推出的JavaScript库,以其虚拟DOM机制和组件化设计,成为构建高性能单页面应用的理想选择。文章介绍了如何开始一个React项目、组件化思想的体现、性能优化方法、表单处理及路由实现等内容,帮助开发者更好地理解和使用React。
119 9
|
4月前
|
缓存 前端开发 JavaScript
前端serverless探索之组件单独部署时,利用rxjs实现业务状态与vue-react-angular等框架的响应式状态映射
本文深入探讨了如何将RxJS与Vue、React、Angular三大前端框架进行集成,通过抽象出辅助方法`useRx`和`pushPipe`,实现跨框架的状态管理。具体介绍了各框架的响应式机制,展示了如何将RxJS的Observable对象转化为框架的响应式数据,并通过示例代码演示了使用方法。此外,还讨论了全局状态源与WebComponent的部署优化,以及一些实践中的改进点。这些方法不仅简化了异步编程,还提升了代码的可读性和可维护性。
117 1
|
4月前
|
JavaScript 前端开发 测试技术
前端全栈之路Deno篇(五):如何快速创建 WebSocket 服务端应用 + 客户端应用 - 可能是2025最佳的Websocket全栈实时应用框架
本文介绍了如何使用Deno 2.0快速构建WebSocket全栈应用,包括服务端和客户端的创建。通过一个简单的代码示例,展示了Deno在WebSocket实现中的便捷与强大,无需额外依赖,即可轻松搭建具备基本功能的WebSocket应用。Deno 2.0被认为是最佳的WebSocket全栈应用JS运行时,适合全栈开发者学习和使用。
213 7
|
4月前
|
前端开发 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 应用程序。
190 2
|
4月前
|
前端开发 JavaScript API
前端的全栈之路Meteor篇(完):关于前后端分离及与各框架的对比,浅析分离之下的潜在耦合
本文探讨了Meteor.js这一全栈JavaScript框架的特点与优势,特别是在前后端分离架构中的应用。Meteor通过共享数据结构和简化全栈开发流程,实现了前后端的紧密协作。文章还对比了其他全栈框架,如Next.js、Nuxt.js等,分析了各自的优势与适用场景,最后讨论了通过定义文档归属者和用户专有数据集简化后端构建及端云数据同步的方法。
134 0
|
4月前
|
人工智能 前端开发 JavaScript
前端架构思考 :专注于多框架的并存可能并不是唯一的方向 — 探讨大模型时代前端的分层式微前端架构
随着前端技术的发展,微前端架构成为应对复杂大型应用的流行方案,允许多个团队使用不同技术栈并将其模块化集成。然而,这种设计在高交互性需求的应用中存在局限,如音视频处理、AI集成等。本文探讨了传统微前端架构的不足,并提出了一种新的分层式微前端架构,通过展示层与业务层的分离及基于功能的横向拆分,以更好地适应现代前端需求。
103 0
|
4月前
|
前端开发 JavaScript 开发者
qiankun(乾坤)微前端框架简介
qiankun(乾坤)微前端框架简介
478 1

热门文章

最新文章

  • 1
    【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 2
    【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 3
    【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
  • 4
    详解智能编码在前端研发的创新应用
  • 5
    巧用通义灵码,提升前端研发效率
  • 6
    智能编码在前端研发的创新应用
  • 7
    【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
  • 8
    【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
  • 9
    抛弃node和vscode,如何用记事本开发出一个完整的vue前端项目
  • 10
    大前端之前端开发接口测试工具postman的使用方法-简单get接口请求测试的使用方法-简单教学一看就会-以实际例子来说明-优雅草卓伊凡
  • 1
    以项目登录接口为例-大前端之开发postman请求接口带token的请求测试-前端开发必学之一-如果要学会联调接口而不是纯写静态前端页面-这个是必学-本文以优雅草蜻蜓Q系统API为实践来演示我们如何带token请求接口-优雅草卓伊凡
    29
  • 2
    大前端之前端开发接口测试工具postman的使用方法-简单get接口请求测试的使用方法-简单教学一看就会-以实际例子来说明-优雅草卓伊凡
    51
  • 3
    【2025优雅草开源计划进行中01】-针对web前端开发初学者使用-优雅草科技官网-纯静态页面html+css+JavaScript可直接下载使用-开源-首页为优雅草吴银满工程师原创-优雅草卓伊凡发布
    26
  • 4
    巧用通义灵码,提升前端研发效率
    93
  • 5
    【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
    141
  • 6
    详解智能编码在前端研发的创新应用
    96
  • 7
    智能编码在前端研发的创新应用
    83
  • 8
    【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
    37
  • 9
    【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
    121
  • 10
    【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
    75