可视化按需启动路由

464 阅读5分钟

前言

本文章示例地址:cwjbjy/vite-react-ts-seed at feature/ejs (github.com)

一、EJS

EJS(Embedded JavaScript Templates)是一种简单而灵活的模板引擎,用于将数据动态渲染到网页上。

1. 安装

yarn add ejs -D

2. 基本语法

EJS使用尖括号加百分号的标记来执行JavaScript代码和插值。以下是EJS的基本语法和标签示例:

<% // 执行JavaScript代码 %>
<%= // 输出变量的值 %>
<%- // 输出原始HTML代码 %>
<%# // 注释 %>

3. 标签含义

  1. <% 存放js语句

  2. %> 一般结束标签

  3. -%> 删除紧随其后的换行符

  4. _%> 删除后面的空格符

  5. <%_ 删除前面的空格符

  6. <%= 输出数据到模板

  7. <%- 输出非转义的数据到模板

  8. <%# 注释标签,不执行、不输出内容

  9. <%% 输出字符串 '<%'

  10. %%> 输出字符串 '%>'

4. ejs.render

将数据插入到HTML中

ejs.render(str, data, options);

str:要渲染的字符串模板

data:模板中要使用的数据

options:额外的配置项

示例:根据字符串模板输出转义后的内容

//新建ejs.js
import ejs from 'ejs';

const html = ejs.render(
  `<% for(var i=0; i<num; i++ ) { -%>
    <h2><%= user.name %></h2>
    <h2><%= user.age %></h2>
<% } %>`,
  {
    user: {
      name: 'zhangsan',
      age: 22,
    },
    num: 2,
  },
  { rmWhitespace: true }, //删除所有可安全删除的空白
);

console.log(html); 

使用VScode的插件Run Code跑一下,输出结果

<h2>zhangsan</h2>
<h2>22</h2>
<h2>zhangsan</h2>
<h2>22</h2>

5. ejs.renderFile

用于加载 EJS 模板文件。相比于ejs.render,ejs.renderFile提高了字符串模板的复用

ejs.renderFile(filename, data, options, function(err, str){
  // str => Rendered HTML string
});

当想复用字符串模板时,可将其定义在一个专门的文件中,新建template.ejs

<% for(var i=0; i<num; i++ ) { -%>
  <h2><%= user.name %></h2>
  <h2><%= user.age %></h2>
<% } %>

修改ejs.js

import path from 'path';
import { fileURLToPath } from 'url';

import ejs from 'ejs';

const __filenameNew = fileURLToPath(import.meta.url);

const __dirname = path.dirname(__filenameNew);

const data = {
  user: {
    name: 'zhangsan',
    age: 22,
  },
  num: 2,
};

const getContentTemplate = async () => {
  try {
    const html = await ejs.renderFile(path.resolve(__dirname, './template.ejs'), data, {
      rmWhitespace: true,
      async: true,
    });
    console.log(html);
  } catch {
    console.log('error rendering template');
  }
};

getContentTemplate();

使用Run Code跑一下,输出结果

<h2>zhangsan</h2>
<h2>22</h2>
<h2>zhangsan</h2>
<h2>22</h2>

二、inquirer

常见的交互式命令行用户界面的集合

1. 安装

yarn add inquirer -D

目前最新版本为9.2.12,使用 ES module 方式引入。如果想使用 Commonjs 方式引入,需安装版本8

2. 基本使用

新建index.js文件

import inquirer from 'inquirer';

inquirer
  .prompt({
    type: 'checkbox',
    name: 'modules',
    message: '请选择启动的模块, 点击上下键选择, 按空格键确认(可以多选), 回车运行。',
    pageSize: 15,
    choices: ['react', 'vue', 'angler'].map((item) => {
      return {
        name: item,
        value: item,
      };
    }),
  })
  .then((answers) => {
    console.log(answers);
  })
  .catch((error) => {
    console.log(error);
  });

在终端中输入 node index.js运行文件,可看到交互界面

1718334254987.png

空格选择react之后,回车确认。可看到打印出{ modules: [ 'react' ] }

三、按需启动路由

1. 路由拆分

之前的路由结构都是定义在router/routes.tsx一个文件里。

对react-router-dom不熟的,可参考这篇文章:react-router-dom(6.23.1)最新最全指南

1. components/lazyImportComponent.tsx

import { Suspense, LazyExoticComponent } from 'react';

const LazyImportComponent = (props: { lazyChildren: LazyExoticComponent<() => JSX.Element> }) => {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <props.lazyChildren />
    </Suspense>
  );
};

export default LazyImportComponent;

2. router/routes.tsx

import { lazy } from 'react';

import LazyImportComponent from '@/components/lazyImportComponent';

const routes = [
  {
    path: '/login',
    element: <LazyImportComponent lazyChildren={lazy(() => import('../pages/login'))} />,
  },
  {
    element: <LazyImportComponent lazyChildren={lazy(() => import('../pages/home'))} />,
    children: [
      {
        path: '/',
        element: <LazyImportComponent lazyChildren={lazy(() => import('../pages/user'))} />,
      },
      {
        path: '/manage',
        element: <LazyImportComponent lazyChildren={lazy(() => import('../pages/manage'))} />,
      },
      {
        path: '/file',
        element: <LazyImportComponent lazyChildren={lazy(() => import('../pages/file'))} />,
      },
    ],
  },
  {
    path: '*',
    element: <div>404</div>,
  },
];

export default routes;

现在对路由进行拆分,将嵌套路由分别拆分到单独的文件中

3. 新建router/manage.tsx

import { lazy } from 'react';

import LazyImportComponent from '@/components/lazyImportComponent';

const manage = [
  {
    path: '/manage',
    element: <LazyImportComponent lazyChildren={lazy(() => import('../pages/manage'))} />,
  },
];

export default manage;

4. 新建router/file.tsx

import { lazy } from 'react';

import LazyImportComponent from '@/components/lazyImportComponent';

const file = [
  {
    path: '/file',
    element: <LazyImportComponent lazyChildren={lazy(() => import('../pages/file'))} />,
  },
];

export default file;

5. 修改router/routes.tsx

import { lazy } from 'react';

import LazyImportComponent from '@/components/lazyImportComponent';

import file from './file';
import manage from './manage';

const routes = [
  {
    path: '/login',
    element: <LazyImportComponent lazyChildren={lazy(() => import('../pages/login'))} />,
  },
  {
    element: <LazyImportComponent lazyChildren={lazy(() => import('../pages/home'))} />,
    children: [
      {
        path: '/',
        element: <LazyImportComponent lazyChildren={lazy(() => import('../pages/user'))} />,
      },
      ...file,
      ...manage,
    ],
  },
  {
    path: '*',
    element: <div>404</div>,
  },
];

export default routes;

将需要按需加载的路由,先按import方式导入,而非直接定义在路由配置表中

2. ejs模板

在项目根目录新建scripts/dev/router.config.template.ejs

import { lazy } from 'react';

import LazyImportComponent from '@/components/lazyImportComponent';

<%_ chooseModules.forEach(function(item){%>
    import <%=item %> from '<%=deelRouteName(item) %>';
  <% }) _%>


  const routes = [
  {
    path: '/login',
    element: <LazyImportComponent lazyChildren={lazy(() => import('../pages/login'))} />,
  },
  {
    element: <LazyImportComponent lazyChildren={lazy(() => import('../pages/home'))} />,
    children: [
      {
        path: '/',
        element: <LazyImportComponent lazyChildren={lazy(() => import('../pages/user'))} />,
      },
      <%_ chooseModules.forEach(function(item){%>
        ...<%=item %>,
        <% }) %>
    ],
  },
  {
    path: '*',
    element: <div>404</div>,
  },
];

export default routes;

通过两个forEach循环,分别写入import语句和扩展运算符

3. vite版

1. 安装ejs,inquirer

yarn add ejs -D
yarn add inquirer -D

2. 新建scripts/dev/config.js

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const __filenameNew = fileURLToPath(import.meta.url);

const __dirname = path.dirname(__filenameNew);

// 开发环境 router 文件路径
export const routerPath = '../../src/router/dev.routerConfig.tsx';

// 实际业务中的所有模块
export const routerModuleConfig = fs
  .readdirSync(path.resolve(__dirname, '../../src/router'))
  .map((item) => item.replace(/(.*)\.[jt]sx?$/, '$1'))
  .filter((file) => !['index', 'routes', 'dev.routerConfig'].includes(file));

routerPath:开发环境要生成的路由文件路径

routerModuleConfig:读取router文件夹下的文件名,并剔除index(路由出口),routes(生产环境路由定义),dev.routerConfig(开发环境路由定义)

3. 新建scripts/dev/index.js

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

import ejs from 'ejs';
import inquirer from 'inquirer';

import { routerPath, routerModuleConfig } from './config.js';

const __filenameNew = fileURLToPath(import.meta.url);

const __dirname = path.dirname(__filenameNew);

//选中的模块
const chooseModules = [];

function deelRouteName(name) {
  const preRoute = './';
  return preRoute + name;
}

const getContentTemplate = async () => {
  const html = await ejs.renderFile(
    path.resolve(__dirname, 'router.config.template.ejs'),
    { chooseModules, deelRouteName },
    { async: true },
  );
  fs.writeFileSync(path.resolve(__dirname, routerPath), html);
};

function promptModule() {
  inquirer
    .prompt({
      type: 'checkbox',
      name: 'modules',
      message:
        '请选择启动的模块, 点击上下键选择, 按空格键确认(可以多选), 回车运行。注意: 直接敲击回车会全量编译, 速度较慢。',
      pageSize: 20,
      choices: routerModuleConfig.map((item) => {
        return {
          name: item,
          value: item,
        };
      }),
    })
    .then((answers) => {
      if (answers.modules.length === 0) {
        chooseModules.push(...routerModuleConfig);
      } else {
        chooseModules.push(...answers.modules);
      }
      getContentTemplate();
    });
}

promptModule();

promptModule:通过交互式页面得到需要加载的路由

getContentTemplate:将生成的chooseModules数组和deelRouteName方法传入ejs模板中得到html字符串,通过fs.writeFileSync将文件内容写入到dev.routerConfig.tsx

4. vite-plugin-filter-replace

使用vite-plugin-filter-replace可根据规则全局替换模块,例如在router/index.ts路由入口文件中,在生产环境下通过import routes from './routes';导入配置的路由。在开发环境下,需将'./routes'替换为我们新生成的dev.routerConfig.tsx

1. 安装

yarn add vite-plugin-filter-replace -D

2. 修改package.json,在scripts中增加启动参数

"dev:m": "node ./scripts/dev/index.js && vite -- --moduleLoad",

3. 修改vite.config.ts

//新增
import replace from 'vite-plugin-filter-replace';

export default () => {
  return defineConfig({
    plugins: [
     //...新增
      replace(
        [
          {
            filter: /\.ts$/,
            replace: {
              from: './routes',
              to: './dev.routerConfig.tsx',
            },
          },
        ],
        {
          apply(config, { command }) {
            // 开发环境,并且包含启动参数--moduleLoad
            return command === 'serve' && process.argv.slice(3)?.join() === '--moduleLoad';
          },
        },
      ),
    ],
  });
};

运行yarn dev:m可查看效果

4. webpack版

配置webpack思路类似,需注意2点:

  1. inquirer安装的版本为8.0.0

  2. 使用webpack内置的NormalModuleReplacementPlugin实现vite中的vite-plugin-filter-replace功能

5. 配置

1. 配置.gitignore

因为dev.routerConfig.tsx仅用于开发环境,且每个人的启动路由都不一样,因此需配置.gitignore。

/src/router/dev.routerConfig.tsx

2. 配置.eslintrc.cjs

由于dev.routerConfig.tsx由ejs生成,代码风格方面会存在问题,还需配置.eslintrc.cjs(eslint版本为8)

//在ignorePatterns数组中,新增src/router/dev.routerConfig.tsx
ignorePatterns: ['src/router/dev.routerConfig.tsx'],