挑战21天手写前端框架 day11 应用元数据与约定式路由

简介: 挑战21天手写前端框架 day11 应用元数据与约定式路由


阅读本文需要 7 分钟,编写本文耗时 2 小时



框架生命周期

生命周期这个词,相信对于现代前端开发人员来说是相当常见的一个词,比如在早期学习 React 的时候,就需要掌握 React 组件的生命周期,每个生命周期做了什么,什么事情在什么生命周期勾子中调用,这些都是基本技能了。但是对于框架的生命周期,对于做业务交付的朋友来说,其实关注度不高。但是对于框架开发者或者框架生态共建者,框架的生命周期确实一个非常重要的信息点。比如 umi 中的插件化开发,本质上就是在一个插件中响应不同的生命周期函数。


因为我们这次的目的是掌握前端框架开发基本技能,并不是要用这个框架去支撑什么业务开发。因此,我们的生命周期会用到什么就加什么。一定让读者掌握为什么要增加某个生命周期环节。


在这个框架的“用户配置”的设计上也会沿用上述的理念。用到什么加什么,而不是在一开始就设计一个很完善的配置和扩展能力。



极简的生命周期

根据我们前几天的开发工作和框架抽离工作,我们可以整理出当前我们需要的极简的生命周期。这只是在我们当前的实现中,做的一个整理和规划。但是却可以让 malita 变成一个真正的框架。云谦说的:动态生成的入口文件,框架才有了意义。


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


因为我们的每一步在后续扩展的时候,都可能会涉及到延时和等待,因此我们将每一个生命周期都写成 Promise


这样我们就可以在代码中,使用 async 和 await 来控制整体的构建流程,保证我们的框架完全按照我们设计的生命周期执行。

比如:

malitaServe.listen(port, async () => {
    // 生命周期
    // 获取项目元信息 
    const appData = await getAppData();
});
复制代码
export const getAppData = ()=>{
    return new Promise((resolve: (value: AppData) => void, rejects)=>{
        const appData = {}
        resolve(appData);
    }
}
复制代码



获取应用元数据

应用元数据的目的就是把后续所有生命周期中需要用到初始的信息提前获取到,使得后续不同的构建流程能用相同的数据,比如构建路径和端口号这种信息,我们都可以放到应用元数据中。


其实可以放的数据非常的丰富。像 Umi 4 就通过 api.appData 收集各种项目数据,从配置、路由、package.json、tsconfig.json、npmClient 到数据流、国际化、antd 用了哪个版本、react 和 react-dom 的版本等,应有尽有,这对于插件开发者会非常实用,也适用于有统计需求的场景。

不过我们当前的框架还用不到这么多的信息,我们只需要用到一些基础路径和 pkg 信息。



元数据 paths

存放一些项目相关的路径。


cwd

当前项目路径,也就是命令执行的路径,比如你在项目根目录下执行 malita dev 那 cwd 就是项目根目录了。

const app = express();
复制代码


absSrcPath

项目 src 目录的绝对路径,由于我们要求用户的项目都是按照最佳实践组织的,所以要求这些文件都存在。我们的 src 路径可以作为主路径,后续如果需要用到一些其他的约定的路径,都可以用这个主路径去拼接。

const absSrcPath = path.resolve(cwd, 'src');
复制代码


absPagesPath

pages 目录绝对路径,约定的路由文件存放路径。

const absPagesPath = path.resolve(absSrcPath, 'pages');
复制代码


absNodeModulesPath

node_modules 目录绝对路径

const absNodeModulesPath = path.resolve(cwd, 'node_modules');
复制代码


absTmpPath

临时目录绝对路径,因为我们临时生成的文件不需要被 git 管理,所以放到了 node_modules 路径下,这样就不用在 .gitignore 文件中处理了。

import { DEFAULT_TEMPLATE, } from './constants';
const absTmpPath = path.resolve(absNodeModulesPath, DEFAULT_TEMPLATE);
复制代码


absEntryPath

主入口文件的绝对路径,因为我们的主入口文件是动态创建的,所以放到了临时目录下。

import { DEFAULT_ENTRY_POINT, } from './constants';
const absEntryPath = path.resolve(absTmpPath, DEFAULT_ENTRY_POINT);
复制代码


absOutputPath

输出目录绝对路径。

import { DEFAULT_OUTDIR, } from './constants';
const absOutputPath = path.resolve(cwd, DEFAULT_OUTDIR);
复制代码



pkg 应用信息

存放着当前项目的 package.json 里的信息,这一次我们会使用到 name 作为页面的标题。

const pkg = require(path.resolve(cwd, 'package.json'));
复制代码



实现

import path from 'path';
import { DEFAULT_ENTRY_POINT, DEFAULT_OUTDIR, DEFAULT_TEMPLATE, } from './constants';
interface Options {
    cwd: string;
}
export interface AppData {
    paths: {
        cwd: string;
        absSrcPath: string;
        absPagesPath: string;
        absTmpPath: string;
        absOutputPath: string;
        absEntryPath: string;
        absNodeModulesPath: string;
    },
    pkg: any,
}
export const getAppData = ({
    cwd
}: Options) => {
    return new Promise((resolve: (value: AppData) => void, rejects) => {
        const absSrcPath = path.resolve(cwd, 'src');
        const absPagesPath = path.resolve(absSrcPath, 'pages');
        const absNodeModulesPath = path.resolve(cwd, 'node_modules');
        const absTmpPath = path.resolve(absNodeModulesPath, DEFAULT_TEMPLATE);
        const absEntryPath = path.resolve(absTmpPath, DEFAULT_ENTRY_POINT);
        const absOutputPath = path.resolve(cwd, DEFAULT_OUTDIR);
        const paths = {
            cwd,
            absSrcPath,
            absPagesPath,
            absTmpPath,
            absOutputPath,
            absEntryPath,
            absNodeModulesPath
        }
        const pkg = require(path.resolve(cwd, 'package.json'));
        resolve({ paths, pkg })
    });
}
复制代码



获取路由配置(约定式路由)

absPagesPath 路径下查找路由文件,然后生成路由配置信息。想法很简单,先找出所有的文件,然后筛选出所有的 tsx 文件,用文件名作为路由,生成路由配置信息。通过找到约定式的全局 layout ,把它当作根路由,嵌套上面生成的路由配置信息,最终返回。


获取文件信息

首先我们找到所有的文件,判断如果路径不存在,则返回一个空的路由配置信息。然后判断文件是不是是不是包含 /\.tsx?$/,如果不包含,就过滤掉。这里我们对约定式做了简化,没对嵌套的情况还有 pages 目录下存放非路由文件等情况作处理,是为了让我们的思路保持清晰,后续在实现前端数据流的时候我们会完善这个方法。

const getFiles = (root: string) => {
    if (!existsSync(root)) return [];
    return readdirSync(root).filter((file) => {
        const absFile = path.join(root, file);
        const fileStat = statSync(absFile);
        const isFile = fileStat.isFile();
        if (isFile) {
            if (!/\.tsx?$/.test(file)) return false;
        }
        return true;
    });;
}
复制代码


将文件信息转成路由配置

然后将我们获取到的文件信息转换成正确路由配置信息,因为只有一层路径,所以实现起来非常简单,只要获取到文件名就行了。然后我们之前将 home 页面作为首页,所以这里加了一个处理,后续我们会改成默认 index 和支持用户配置。

const filesToRoutes = (files: string[], pagesPath: string): IRoute[] => {
    return files.map(i => {
        let pagePath = path.basename(i, path.extname(i));
        const element = path.resolve(pagesPath, pagePath);
        if (pagePath === 'home') pagePath = '';
        return {
            path: `/${pagePath}`,
            element,
        }
    });
}
复制代码


获取约定 layout

我们约定了 absSrcPath 路径下面的 layouts/index.tsx 作为我们的全局 layout,因此我们使用 absSrcPath 拼接出正确的路径。其实这个也可以放到应用元数据中,这里演示一下后面的其他约定路径是如何使用 absSrcPath 的。

const layoutPath = path.resolve(appData.paths.absSrcPath, DEFAULT_GLOBAL_LAYOUTS);
复制代码


实现

export const getRoutes = ({ appData }: { appData: AppData }) => {
    return new Promise((resolve: (value: IRoute[]) => void) => {
        const files = getFiles(appData.paths.absPagesPath);
        const routes = filesToRoutes(files, appData.paths.absPagesPath);
        const layoutPath = path.resolve(appData.paths.absSrcPath, DEFAULT_GLOBAL_LAYOUTS);
        // 如果不存在全局 layout 那就直接返回路由配置信息
        if (!existsSync(layoutPath)) {
            resolve(routes);
        } else {
            resolve([{
                path: '/',
                element: layoutPath.replace(path.extname(layoutPath), ''),
                routes: routes
            }]);
        }
    })
}
复制代码



调试一下生命周期流程是否正确

当前项目结构

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


执行生命周期

malitaServe.listen(port, async () => {
    // 生命周期
    // 获取项目元信息  
    const appData = await getAppData({
        cwd
    });
    console.log(appData)
    // 获取 routes 配置
    const routes = await getRoutes({ appData });
    console.log(JSON.stringify(routes))
});
复制代码


appData 展示

{
  paths: {
    cwd: '/malita/examples/app',
    absSrcPath: '/malita/examples/app/src',
    absPagesPath: '/malita/examples/app/src/pages',
    absTmpPath: '/malita/examples/app/node_modules/.malita',
    absOutputPath: '/malita/examples/app/dist',
    absEntryPath: '/malita/examples/app/node_modules/.malita/malita.tsx',
    absNodeModulesPath: '/malita/examples/app/node_modules'
  },
  pkg: {
    name: '@examples/app',
    version: '1.0.0',
    description: '',
    main: 'index.js',
    scripts: {
      build: 'pnpm esbuild src/** --bundle --outdir=www',
      dev: 'malita dev',
      serve: 'cd dist && serve'
    },
    keywords: [],
    author: '',
    dependencies: {
      '@alita/flow': '^3.0.0-beta.5',
      malita: 'workspace:*',
      '@malita/keepalive': 'workspace:*',
      react: '^18.0.0',
      'react-dom': '^18.0.0',
      'react-router': '^6.3.0',
      'react-router-dom': '^6.3.0'
    },
    devDependencies: { serve: '^13.0.2' }
  }
}
复制代码


getFiles 展示

通过 getFiles 获取到文件组织树信息。

[ 'home.tsx', 'users.tsx' ]
复制代码


filesToRoutes 展示

通过 filesToRoutes 将上面的信息转成下面的路由配置信息。

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


由于篇幅问题,今天就只完成了两个生命周期,还有 “动态生成应用主入口文件” 和 “动态生成 HTML”,我们会在下一天完成。


如果你对我的有所期待,请给我点赞和关注。感谢感谢。


源码归档



目录
相关文章
|
15天前
|
前端开发 JavaScript 安全
前端性能调优:HTTP/2与HTTPS在Web加速中的应用
【10月更文挑战第27天】本文介绍了HTTP/2和HTTPS在前端性能调优中的应用。通过多路复用、服务器推送和头部压缩等特性,HTTP/2显著提升了Web性能。同时,HTTPS确保了数据传输的安全性。文章提供了示例代码,展示了如何使用Node.js创建一个HTTP/2服务器。
29 3
|
16天前
|
Rust 前端开发 JavaScript
前端性能革命:WebAssembly在高性能计算中的应用探索
【10月更文挑战第26天】随着Web应用功能的日益复杂,传统JavaScript解释执行模式逐渐成为性能瓶颈。WebAssembly(Wasm)应运而生,作为一种二进制代码格式,支持C/C++、Rust等语言编写的代码在浏览器中高效运行。Wasm不仅提升了应用的执行速度,还具备跨平台兼容性和安全性,显著改善了Web应用的响应速度和用户体验。
31 4
|
15天前
|
前端开发 数据管理 测试技术
前端自动化测试:Jest与Cypress的实战应用与最佳实践
【10月更文挑战第27天】本文介绍了前端自动化测试中Jest和Cypress的实战应用与最佳实践。Jest适合React应用的单元测试和快照测试,Cypress则擅长端到端测试,模拟用户交互。通过结合使用这两种工具,可以有效提升代码质量和开发效率。最佳实践包括单元测试与集成测试结合、快照测试、并行执行、代码覆盖率分析、测试环境管理和测试数据管理。
31 2
|
16天前
|
前端开发 安全 应用服务中间件
前端性能调优:HTTP/2与HTTPS在Web加速中的应用
【10月更文挑战第26天】随着互联网的快速发展,前端性能调优成为开发者的重要任务。本文探讨了HTTP/2与HTTPS在前端性能优化中的应用,介绍了二进制分帧、多路复用和服务器推送等特性,并通过Nginx配置示例展示了如何启用HTTP/2和HTTPS,以提升Web应用的性能和安全性。
17 3
|
16天前
|
前端开发 JavaScript 数据可视化
前端自动化测试:Jest与Cypress的实战应用与最佳实践
【10月更文挑战第26天】前端自动化测试在现代软件开发中至关重要,Jest和Cypress分别是单元测试和端到端测试的流行工具。本文通过解答一系列问题,介绍Jest与Cypress的实战应用与最佳实践,帮助开发者提高测试效率和代码质量。
27 2
|
16天前
|
前端开发 JavaScript API
前端框架新探索:Svelte在构建高性能Web应用中的优势
【10月更文挑战第26天】近年来,前端技术飞速发展,Svelte凭借独特的编译时优化和简洁的API设计,成为构建高性能Web应用的优选。本文介绍Svelte的特点和优势,包括编译而非虚拟DOM、组件化开发、状态管理及响应式更新机制,并通过示例代码展示其使用方法。
32 2
|
17天前
|
前端开发 JavaScript 开发者
“揭秘React Hooks的神秘面纱:如何掌握这些改变游戏规则的超能力以打造无敌前端应用”
【10月更文挑战第25天】React Hooks 自 2018 年推出以来,已成为 React 功能组件的重要组成部分。本文全面解析了 React Hooks 的核心概念,包括 `useState` 和 `useEffect` 的使用方法,并提供了最佳实践,如避免过度使用 Hooks、保持 Hooks 调用顺序一致、使用 `useReducer` 管理复杂状态逻辑、自定义 Hooks 封装复用逻辑等,帮助开发者更高效地使用 Hooks,构建健壮且易于维护的 React 应用。
28 2
|
22天前
|
JavaScript 前端开发 测试技术
前端全栈之路Deno篇(五):如何快速创建 WebSocket 服务端应用 + 客户端应用 - 可能是2025最佳的Websocket全栈实时应用框架
本文介绍了如何使用Deno 2.0快速构建WebSocket全栈应用,包括服务端和客户端的创建。通过一个简单的代码示例,展示了Deno在WebSocket实现中的便捷与强大,无需额外依赖,即可轻松搭建具备基本功能的WebSocket应用。Deno 2.0被认为是最佳的WebSocket全栈应用JS运行时,适合全栈开发者学习和使用。
|
18天前
|
前端开发 API UED
深入理解微前端架构:构建灵活、高效的前端应用
【10月更文挑战第23天】微前端架构是一种将前端应用分解为多个小型、独立、可复用的服务的方法。每个服务独立开发和部署,但共同提供一致的用户体验。本文探讨了微前端架构的核心概念、优势及实施方法,包括定义服务边界、建立通信机制、共享UI组件库和版本控制等。通过实际案例和职业心得,帮助读者更好地理解和应用微前端架构。
|
22天前
|
人工智能 资源调度 数据可视化
【AI应用落地实战】智能文档处理本地部署——可视化文档解析前端TextIn ParseX实践
2024长沙·中国1024程序员节以“智能应用新生态”为主题,吸引了众多技术大咖。合合信息展示了“智能文档处理百宝箱”的三大工具:可视化文档解析前端TextIn ParseX、向量化acge-embedding模型和文档解析测评工具markdown_tester,助力智能文档处理与知识管理。