前言
本文章示例地址: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. 标签含义
-
<%
存放js语句 -
%>
一般结束标签 -
-%>
删除紧随其后的换行符 -
_%>
删除后面的空格符 -
<%_
删除前面的空格符 -
<%=
输出数据到模板 -
<%-
输出非转义的数据到模板 -
<%#
注释标签,不执行、不输出内容 -
<%%
输出字符串 '<%' -
%%>
输出字符串 '%>'
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运行文件,可看到交互界面
空格选择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点:
-
inquirer安装的版本为8.0.0
-
使用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'],