Taro微信小程序如何写一个loader实现全局导入公共组件,在所有页面命令式控制该组件
现在你只需要有一个全局Taro组件将路径传入到 publicComponents 假如你的全局组件是下面这样的,在下面组件中你使用到了三个参数,isShowPrompt, promptMessageTitle, promptMessage,然后你就可以在你的src/pages/home/index.tsx中使用,setIsShowPrompt, setPromptMessageTitle, set
需要新建下面两个文件
// config/index.ts 或 config/index.js
baseConfig:{
// ... 其他配置
plugins: [
[path.resolve(__dirname, './publicComponentsPlugin'), {}]
],
}
新建文件config\publicComponentsPlugin.ts
import type { IPluginContext } from '@tarojs/service';
import path from 'node:path'; // 新增path模块引入
export default (ctx: IPluginContext, pluginOpts) => {
ctx.modifyWebpackChain(({ chain }) => {
// 加载loader
const loaderPath = path.resolve('config/publicComponentsLoader.ts');
chain.module
.rule('componentInjector')
.test(/\.(js|ts|jsx|tsx)$/)
.exclude.add(/node_modules/).end()
.use('componentInjector')
.loader(loaderPath)
.end();
});
};
新建文件config\publicComponentsLoader.ts,该loader可能还有bug需要仔细查看使用
// 主要功能是注入全局组件,每次更改该文件需要重新编译,
import fs from 'node:fs';
import path, { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
// 公共组件相对于src的路径,默认将公共组件注入到所有src下的pages中,可以通过excludeFolderName排除要注入的文件夹
const publicComponents = [
'/src/components/MessagePrompt/index.tsx'
]
// 要排除注入的文件,
const excludeFolderName = ["init", "login"];
const resolvePath = (relativePath: string) => {
// 获取当前文件的 URL(如 file:///path/to/current-file.js)
const currentFileUrl = import.meta.url;
// 将文件 URL 转换为文件系统路径(如 /path/to/current-file.js)
const currentFilePath = fileURLToPath(currentFileUrl);
// 获取当前文件所在目录
const currentDir = dirname(currentFilePath);
// 解析相对路径为绝对路径
return path.resolve(currentDir, relativePath);
}
const extractComponentName = (path: string): string => {
// 使用正则表达式匹配路径中的组件名
const regex = /\/([^/]+)\/index\.(tsx|js|jsx)$/;
const match = path.match(regex);
if (match) {
return match[1];
} else {
return "matchError"
}
}
const removeComments = (code: string) => {
// 移除单行注释
code = code.replace(/\/\/.*$/gm, '');
// 移除多行注释
code = code.replace(/\/\*[\s\S]*?\*\//g, '');
return code;
}
const extractPropsNames = (componentCode: string): string[] => {
// 先移除代码中的注释
const codeWithoutComments = removeComments(componentCode);
const propsNames = new Set();
// 匹配解构赋值部分
const destructuringRegex = /\(({([^}]+)})\s*:?\s*Props\)/;
const destructuringMatch = codeWithoutComments.match(destructuringRegex);
if (destructuringMatch && destructuringMatch[2]) {
const destructuredProps = destructuringMatch[2].split(',');
destructuredProps.forEach(prop => {
prop = prop.trim();
if (prop) {
// 处理默认值的情况,提取 prop 名称
const propName = prop.split('=')[0].trim();
propsNames.add(propName);
}
});
}
// 匹配普通 props.xxx 形式
const normalRegex = /props\.([a-zA-Z_$][0-9a-zA-Z_$]*)/g;
let normalMatch;
while ((normalMatch = normalRegex.exec(codeWithoutComments)) !== null) {
propsNames.add(normalMatch[1]);
}
if (propsNames.size === 0) {
return []
} else {
return Array.from(propsNames) as string[];
}
}
const insertStatementIntoFunction = (str: string, useStateImport: string, insertStatement: string): string => {
if (!str) return '';
let functionBodyStartIndex = -1;
// 处理引用导出情况
if (str.includes('export default') && !str.includes('function') && !str.includes('=>')) {
const functionNameMatch = str.match(/export default (\w+);/);
if (functionNameMatch) {
const functionName = functionNameMatch[1];
const functionDefRegex = new RegExp(`function ${functionName}\\s*\\(`);
const functionDefMatch = str.match(functionDefRegex) as RegExpMatchArray & { index: number };;
if (functionDefMatch) {
const start = functionDefMatch.index + functionDefMatch[0].length;
for (let i = start; i < str.length; i++) {
if (str[i] === '{') {
functionBodyStartIndex = i + 1;
break;
}
}
}
}
}
// 处理普通函数导出
if (functionBodyStartIndex === -1) {
const normalFunctionRegex = /(?<!\/\/.*)function\s+(\w+)\s*\(/;
const normalFunctionMatch = str.match(normalFunctionRegex) as RegExpMatchArray & { index: number };
if (normalFunctionMatch) {
const start = normalFunctionMatch.index + normalFunctionMatch[0].length;
for (let i = start; i < str.length; i++) {
if (str[i] === '{') {
functionBodyStartIndex = i + 1;
break;
}
}
}
}
// 处理箭头函数导出
if (functionBodyStartIndex === -1) {
const arrowFunctionRegex = /(?<!\/\/.*)\(\s*\)\s*=>\s*{/;
const arrowFunctionMatch = str.match(arrowFunctionRegex) as RegExpMatchArray & { index: number };
if (arrowFunctionMatch) {
functionBodyStartIndex = arrowFunctionMatch.index + arrowFunctionMatch[0].length;
}
}
if (functionBodyStartIndex !== -1) {
// 判断是否有useState的导入
if (!str.includes('useState')) {
return [
useStateImport,
str.slice(0, functionBodyStartIndex),
insertStatement,
str.slice(functionBodyStartIndex)
].join('\n');
} else {
return [
str.slice(0, functionBodyStartIndex),
insertStatement,
str.slice(functionBodyStartIndex)
].join('\n');
}
}
return str;
}
export default function (source: string) {
if (publicComponents.length === 0) {
return source;
}
// 仅在页面文件中注入(假设页面都在src/pages目录下)
const pattern = /\\src\\pages\\.*\.tsx$/;
// 要注入的语句
let publicComponentsProps = {};
let importStatement = '';
// 没有导入的组件不会有this.resourcePath
for (let i = 0; i < publicComponents.length; i++) {
// 注入导入语句
const publicComponentsName = extractComponentName(publicComponents[i])
if (publicComponentsName) {
importStatement += `import ${publicComponentsName} from '@/components/${publicComponentsName}/index.tsx';\n`
}
const componentPath = resolvePath(`..${publicComponents[i]}`);
// 根据componentPath获取文件内容
const componentCode = fs.readFileSync(componentPath, 'utf8');
publicComponentsProps[publicComponentsName] = extractPropsNames(componentCode)
}
if (pattern.test(this.resourcePath)) {
// 排除文件夹
for (let i = 0; i < excludeFolderName.length; i++) {
if (this.resourcePath.includes(excludeFolderName[i])) {
return source;
}
}
// 在文件开头添加导入语句,并且使用该组件
source = importStatement + source;
// // 找到最后一个</View>标签的位置
const lastViewIndex = source.lastIndexOf('</View>');
// // 在</View>标签之前插入<MessagePrompt />
let publicComponentsUseStr = '';
let insertStatement = '';
for (const key in publicComponentsProps) {
for (let i = 0; i < publicComponentsProps[key].length; i++) {
if (i === 0) {
publicComponentsUseStr += `<${key} ${publicComponentsProps[key][i]}={${publicComponentsProps[key][i]}}`
} else if (i === publicComponentsProps[key].length - 1) {
publicComponentsUseStr += ` ${publicComponentsProps[key][i]}={${publicComponentsProps[key][i]}} />\n`
} else {
publicComponentsUseStr += ` ${publicComponentsProps[key][i]}={${publicComponentsProps[key][i]}} `
}
insertStatement += `const [${publicComponentsProps[key][i]}, set${publicComponentsProps[key][i].charAt(0).toUpperCase() + publicComponentsProps[key][i].slice(1)}] = useState();\n`
}
}
if (lastViewIndex !== -1) {
source = source.slice(0, lastViewIndex) + publicComponentsUseStr + source.slice(lastViewIndex);
}
// // 查找导出的函数体
const useStateImport = `import { useState } from 'react';`;
const newSource = insertStatementIntoFunction(source, useStateImport, insertStatement);
// 生成调试文件,查看编译后的代码是否正确方便调试
// fs.appendFileSync("你的绝对路径/config/path.txt", this.resourcePath + "\n");
// fs.appendFileSync("你的绝对路径/config/code.txt", newSource + "\n" + this.resourcePath + "--------------------------------------------" + "\n");
return newSource;
}
return source;
}
现在你只需要有一个全局Taro组件将路径传入到 publicComponents 假如你的全局组件是下面这样的,在下面组件中你使用到了三个参数,isShowPrompt , promptMessageTitle, promptMessage,然后你就可以在你的src/pages/home/index.tsx 中使用,setIsShowPrompt , setPromptMessageTitle, setPromptMessage,来设置该组件的值。
你的页面文件结构必须是这样的 src/pages/page1/index.tsx,src/pages/page2/index.tsx
你的组件文件结构必须是这样的 src/components/components1/index.tsx,src/components/components2/index.tsx,
import { View } from "@tarojs/components"
import Taro from "@tarojs/taro"
import "./index.less"
type Props = {
children: React.ReactNode
isShowPrompt: boolean
promptMessage: string
promptMessageTitle?: string
}
// 这是一个公共组件,只需要定义props的使用方式,在其使用到该组件的页面就可以这样使用该组件的props
// 例如 你使用到了 props.isShow 在父组件你可以使用 setIsShow(false) 来控制该组件的显示隐藏
export default ({ isShowPrompt, promptMessageTitle = "你有一条新消息", promptMessage }: Props) => {
// .... 其他函数以及功能
return (
<View onClick={goChart}>
<View className={`message-prompt ${isShowPrompt ? "show" : ''}`}>
<View className="font-bold">{promptMessageTitle}</View>
<View className="message">{promptMessage}</View>
</View>
</View>
)
}
,如果ts提示没有这几个函数需要手动在env.d.ts中定义这几个函数,如果你使用的是js就不用管了
declare function setMessagePrompt(params: boolean): void;
declare function setPromptMessageTitle(params: string): void;
declare function setPromptMessage(params: string): void;
declare function setIsShowPrompt(params: boolean): void;
什么原理?
原理很简单就是将该组件的导入语句和使用语句注入到所有pages/page1/index.tsx中,并将
const [isShowPrompt,setIsShowPrompt] = useState(); 语句注入到导出的函数体,所以才能在页面组件中直接使用setIsShowPrompt控制该组件的数据。如果你不想所有的页面都注入公共组件可以使用excludeFolderName 配置不想注入的页面的文件夹名称
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)