挑战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”,我们会在下一天完成。


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


源码归档



目录
相关文章
|
5天前
|
缓存 监控 前端开发
【Flutter 前端技术开发专栏】Flutter 应用的启动优化策略
【4月更文挑战第30天】本文探讨了Flutter应用启动优化策略,包括理解启动过程、资源加载优化、减少初始化工作、界面布局简化、异步初始化、预加载关键数据、性能监控分析以及案例和未来优化方向。通过这些方法,可以缩短启动时间,提升用户体验。使用Flutter DevTools等工具可助于识别和解决性能瓶颈,实现持续优化。
【Flutter 前端技术开发专栏】Flutter 应用的启动优化策略
|
5天前
|
Dart 前端开发 开发工具
【Flutter前端技术开发专栏】Flutter入门指南:搭建开发环境与第一个应用
【4月更文挑战第30天】本文介绍了Flutter SDK的安装和配置过程,以及如何创建并运行第一个Flutter应用。首先确保安装了Dart SDK和Flutter SDK,支持macOS、Linux和Windows。安装完成后,设置环境变量,然后通过`flutter doctor`验证安装。接着,使用`flutter create`命令创建新项目,进入项目目录并运行`flutter run`启动应用。在`main.dart`中修改代码以自定义应用。Flutter支持热重载和DevTools调试。本文为Flutter初学者提供了快速入门的指导。
【Flutter前端技术开发专栏】Flutter入门指南:搭建开发环境与第一个应用
|
23小时前
|
前端开发 Java Go
从前端到后端:构建现代化Web应用的技术演进
本文探讨了从前端到后端的技术演进,介绍了前端、后端以及多种编程语言,如Java、Python、C、PHP和Go,以及数据库在构建现代化Web应用中的应用。通过深入剖析各个技术领域的发展和应用,读者将对构建高效、可扩展、安全的Web应用有更深入的理解。
|
3天前
|
前端开发 Java Go
从前端到后端:构建现代化Web应用的技术实践
本文将介绍如何通过前端和后端技术相结合,构建现代化Web应用的技术实践。我们将探讨前端开发、后端架构以及多种编程语言(如Java、Python、C、PHP、Go)在构建高效、可扩展的Web应用中的应用。
|
4天前
|
前端开发
前端路由机制实现hash-history
前端路由机制实现hash-history
8 1
|
4天前
|
前端开发 JavaScript 测试技术
第八章(应用场景篇) 中大型项目的解构:从单体应用到微前端
第八章(应用场景篇) 中大型项目的解构:从单体应用到微前端
|
4天前
|
Web App开发 前端开发 JavaScript
构建跨浏览器兼容的前端应用:技术实践与挑战
【5月更文挑战第16天】构建跨浏览器兼容的前端应用是应对浏览器差异和多样性的挑战。使用现代框架(如React、Vue)能自动转换代码,编写可移植的Web标准代码,结合浏览器兼容性测试工具和Polyfill解决旧浏览器支持问题。关注浏览器更新,应对性能、API差异和样式问题,采用渐进增强、条件判断和CSS Reset策略确保应用在各种浏览器上运行良好。
|
5天前
|
前端开发 JavaScript Java
浅谈企业级前端应用中的组件概念和具体的应用
浅谈企业级前端应用中的组件概念和具体的应用
10 1
|
5天前
|
JSON 前端开发 JavaScript
快照测试在前端自动化测试中的应用
在前端自动化测试中,快照测试常用于检验组件渲染与布局。
|
5天前
|
缓存 移动开发 前端开发
【专栏:HTML与CSS前端技术趋势篇】HTML与CSS在PWA(Progressive Web Apps)中的应用
【4月更文挑战第30天】PWA(Progressive Web Apps)结合现代Web技术,提供接近原生应用的体验。HTML在PWA中构建页面结构和内容,响应式设计、语义化标签、Manifest文件和离线页面的创建都离不开HTML。CSS则用于定制主题样式、实现动画效果、响应式布局和管理字体图标。两者协同工作,保证PWA在不同设备和网络环境下的快速、可靠和一致性体验。随着前端技术进步,HTML与CSS在PWA中的应用将更广泛。