Skip to content

🌍 环境模块

环境模块负责管理 SCP 系统的核心配置,包括用户认证和接口地址设置。

📋 概述

环境模块提供两个核心环境变量的管理:

  • token: 用户认证令牌
  • scpEndpoint: 后端 API 接口地址

🚀 快速开始

基础配置

ts
// 设置用户认证token(建议在用户登录成功后设置)
scp.env.token.set('your_access_token');

// 配置后端API接口地址(根据部署环境调整)
scp.env.scpEndpoint.set('http://bcapi-dev1.e-tudou.com');

🔐 Token 配置

配置时机

用户认证 token 需要在用户登录成功后立即设置,确保后续 API 调用的身份验证。

登录集成示例

以下是完整的登录处理函数,展示了如何在登录成功后正确设置 token:

ts
/**
 * 用户登录处理函数
 * 
 * 参考文献:
 * 1. 后端 sa-token: https://sa-token.cc/v/v1.34.0/doc.html#/fun/token-info
 * 2. 前端 js-cookie: https://www.npmjs.com/package/js-cookie
 */
function onLogin() {
	if (activeKey.value === "account") {
		// 账号密码登录
		formAccountRef.value?.validate().then(async () => {
			loading.value = true;
			const formData = toRaw(formAccount);

			try {
				// 调用登录接口
				const { username, password } = formData;
				const apiParams = {
					name: username,
					client: CLIENT_ID,
					pwd: shuffleBase64Encode(md5(password))
				};
				
				const { msg, data } = await loginApi(apiParams);
				if (!data) {
					return message.error(msg);
				}
				
				// 构建用户信息
				let userInfo = {
					tenantId: data.principalInfo.tenantId,
					userId: data.principalInfo.userId,
					userName: data.principalInfo.userName
				};

				// 存储到本地存储
				storage.set({
					expires_time: data.tokenInfo.tokenTimeout / (60 * 60 * 24),
					token: data.tokenInfo.tokenValue,
					access_token: data.tokenInfo.tokenValue, // 兼容旧的 SAAS 应用
					userInfo: JSON.stringify(userInfo)
				});

				// 🔑 关键步骤:设置 SCP 环境变量中的 token
				window.scp.env.token.set(data.tokenInfo.tokenValue); 

				// 页面跳转
				pageJump();
			} finally {
				loading.value = false;
			}
		});
	} else if (activeKey.value === "captcha") {
		// 短信验证码登录
		formCaptchaRef.value?.validate().then(async () => {
			loading.value = true;
			const formData = toRaw(formCaptcha);

			try {
				// 调用短信登录接口
				const { mobile, captcha } = formData;
				const apiParams = {
					name: mobile,
					validCode: captcha
				};
				
				const { msg, data } = await loginBySmsApi(apiParams);
				if (!data) {
					return message.error(msg);
				}
				
				message.success(msg);
				
				// 构建用户信息
				let userInfo = {
					tenantId: data.principalInfo.tenantId,
					userId: data.principalInfo.userId,
					userName: data.principalInfo.userName
				};

				// 存储到本地存储
				storage.set({
					expires_time: data.tokenInfo.tokenTimeout / (60 * 60 * 24),
					token: data.tokenInfo.tokenValue,
					access_token: data.tokenInfo.tokenValue, // 兼容旧的 SAAS 应用
					userInfo: JSON.stringify(userInfo)
				});

				// 🔑 关键步骤:设置 SCP 环境变量中的 token
				window.scp.env.token.set(data.tokenInfo.tokenValue); 

				// 页面跳转
				pageJump();
			} finally {
				loading.value = false;
			}
		});
	}
}

🌐 接口地址配置

配置时机

scpEndpoint 需要在路由拦截器中设置,确保每次页面访问时都有正确的接口地址。

Vue 项目路由拦截器集成

ts
/**
 * 路由拦截器 - beforeEach
 * 在每次路由跳转前设置环境变量
 */
router.beforeEach(async (to, from, next) => {
	const cookie_token = storage.get("token");
	const cookie_userInfoStr = storage.get("userInfo");
	const authStore = AuthStore();

	// 🔑 关键步骤:从本地存储恢复 token
	window.scp.env.token.set(cookie_token); 

	// 🌐 关键步骤:设置 API 接口地址
	window.scp.env.scpEndpoint.set(import.meta.env.VITE_API_BASE_DOMAIN); 

	// 1. NProgress 开始
	ConditionalNProgress.start();

	// 2. 动态设置标题
	const title = import.meta.env.VITE_GLOB_APP_TITLE;
	document.title = to.meta.title
		? `${tt(generatePinyin(to.meta.title as string, undefined), to.meta.title as string)} - ${tt(
				generatePinyin(title, undefined),
				title
		  )}`
		: tt(generatePinyin(title, undefined), title);

	// 3. 判断是访问登录页,有 Token 就在当前页面,没有 Token 重置路由并放行到登录页
	if (to.path.toLocaleLowerCase() === LOGIN_URL) {
		if (cookie_token && cookie_userInfoStr) return next(from.fullPath);

		storage.clear();
		resetRouter();
		return next();
	}

	// 4. 白名单可以直接访问
	if (ROUTER_WHITE_LIST.includes(to.path)) {
		return next();
	}

	// 5. 判断是否有 Token,没有重定向到 login
	if (!cookie_token || !cookie_userInfoStr) return next({ path: LOGIN_URL, replace: true });

	// 6. 如果没有菜单列表,就重新请求菜单列表并添加动态路由
	authStore.setRouteName(to.name as string);
	if (!authStore.authMenuListGet.length) {
		await initDynamicRouter();
		return next({ ...to, replace: true });
	}

	// 7. 正常访问页面
	next();
});

React 项目环境变量配置

在 React 项目中,可以使用 React Router 和 useEffect 钩子来实现类似的功能:

文件名: src/router/AuthGuard.tsx

ts
import { useEffect, useState } from "react"; // 新增useState
import { useLocation } from "react-router";

import { storage } from "@pt/utils/modules/storage";

import { getLoginUrl, ROUTER_WHITE_LIST } from "@/config";

interface AuthGuardProps {
  children: React.ReactNode;
}

async function initConfig() {
  // @ts-ignore
  await scp.env.scpEndpoint.set(import.meta.env.VITE_API_BASE_DOMAIN); 
  // @ts-ignore
  await scp.env.token.set(storage.get("token")); 
}

/**
 * 路由守卫组件
 * 用于验证用户登录状态和权限
 */
export default function AuthGuard({ children }: AuthGuardProps) {
  const location = useLocation();
  // 新增加载状态:true=还在初始化/校验,false=已完成
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    let isCancelled = false;
    const checkAuth = async () => {
      try {
        const currentPath = location.pathname;
        const cookie_token = storage.get("token");
        const cookie_userInfoStr = storage.get("userInfo");

        console.log('[AuthGuard] 路由守卫检查:', {
          currentPath,
          hasToken: !!cookie_token,
          hasUserInfo: !!cookie_userInfoStr,
          token: cookie_token,
          userInfo: cookie_userInfoStr,
        });

        // 先执行初始化,确保scp.env配置完成
        await initConfig();

        if (isCancelled) return;

        // 白名单路由直接放行
        if (ROUTER_WHITE_LIST.includes(currentPath)) {
          console.log('[AuthGuard] 白名单路由,直接放行');
          setIsLoading(false); // 加载完成,允许渲染
          return;
        }

        // 无认证信息则跳转登录
        if (!cookie_token || !cookie_userInfoStr) {
          console.error('[AuthGuard] 缺少认证信息,准备跳转登录页');
          storage.clear();
          window.location.href = getLoginUrl();
          return;
        }

        // 认证通过,加载完成
        setIsLoading(false);
      } catch (error) {
        console.error('[AuthGuard] 认证检查/初始化失败:', error);
        if (!isCancelled) {
          storage.clear();
          window.location.href = getLoginUrl();
        }
      }
    };

    checkAuth();

    return () => {
      isCancelled = true;
      setIsLoading(true); // 卸载时重置加载状态
      console.log('[AuthGuard] 路由变化/组件卸载,终止后续操作');
    };
  }, [location.pathname]);

  // 关键:加载中不渲染子组件,避免提前读取未初始化的scp.env
  if (isLoading) {
    // 可替换为项目的加载组件,比如 <Spin />
    return <div>加载中...</div>;
  }

  return <>{children}</>;
}

📚 API 参考

Token 管理

scp.env.token.set(token: string)

设置用户认证令牌。

参数:

  • token (string): 用户认证

示例:

ts
// 设置 token
scp.env.token.set('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...');

// 获取当前 token
const currentToken = scp.env.token.get();

scp.env.token.get()

获取当前设置的用户认证令牌。

返回值:

  • string | null: 当前的认证令牌,如果未设置则返回 null

接口地址管理

scp.env.scpEndpoint.set(endpoint: string)

设置后端 API 接口地址。

参数:

  • endpoint (string): 后端 API 的基础地址

环境配置说明

根据不同的部署环境,需要配置对应的接口地址:

开发环境 (dev1):

bash
# .env.development
VITE_API_BASE_DOMAIN = "http://bcapi-dev1.e-tudou.com"

测试环境 (beta1):

bash
# .env.beta
VITE_API_BASE_DOMAIN = "https://bcapi-beta1.e-tudou.com"

生产环境 (prod):

bash
# .env.production
VITE_API_BASE_DOMAIN = "https://bcapi.i-tudou.com"

私有化环境:

bash
# .env.private
VITE_API_BASE_DOMAIN = "https://bcapi.tudoucloud.com"

⚠️ 注意: 其他环境服务地址请参考部署文档设置

代码示例:

ts
// 根据环境变量自动设置接口地址
const API_ENDPOINT = import.meta.env.VITE_API_BASE_DOMAIN;
scp.env.scpEndpoint.set(API_ENDPOINT);

// 手动设置不同环境
// 开发环境
scp.env.scpEndpoint.set('http://bcapi-dev1.e-tudou.com');

// 测试环境
scp.env.scpEndpoint.set('https://bcapi-beta1.e-tudou.com');

// 生产环境
scp.env.scpEndpoint.set('https://bcapi.i-tudou.com');

// 私有化环境
scp.env.scpEndpoint.set('https://bcapi.tudoucloud.com');

// 获取当前接口地址
const currentEndpoint = scp.env.scpEndpoint.get();

scp.env.scpEndpoint.get()

获取当前设置的接口地址。

返回值:

  • string | null: 当前的接口地址,如果未设置则返回 null

🔧 最佳实践

1. 环境变量管理

ts
// 推荐:使用环境变量管理不同环境的配置
const API_BASE_URL = import.meta.env.VITE_API_BASE_DOMAIN || 'http://localhost:3000';
scp.env.scpEndpoint.set(API_BASE_URL);

2. Token 生命周期管理

ts
// 登录时设置
function handleLogin(tokenData) {
  // 存储到本地
  storage.set('token', tokenData.tokenValue);
  
  // 设置到环境变量
  scp.env.token.set(tokenData.tokenValue);
}

// 登出时清理
function handleLogout() {
  // 清理本地存储
  storage.remove('token');
  
  // 清理环境变量
  scp.env.token.set(null);
}

3. 应用初始化

ts
// 应用启动时恢复环境变量
function initializeApp() {
  const savedToken = storage.get('token');
  const apiEndpoint = import.meta.env.VITE_API_BASE_DOMAIN;
  
  if (savedToken) {
    scp.env.token.set(savedToken);
  }
  
  if (apiEndpoint) {
    scp.env.scpEndpoint.set(apiEndpoint);
  }
}

⚠️ 注意事项

安全性

  • 不要在客户端代码中硬编码敏感信息
  • Token 应该通过安全的登录流程获取
  • 使用 HTTPS 传输敏感数据

错误处理

ts
try {
  scp.env.token.set(tokenValue);
} catch (error) {
  console.error('设置 token 失败:', error);
  // 处理错误逻辑
}

类型安全

ts
// 推荐:添加类型检查
function setToken(token: string | null) {
  if (typeof token === 'string' && token.length > 0) {
    scp.env.token.set(token);
  } else {
    console.warn('无效的 token 值');
  }
}

🐛 常见问题

Q: Token 设置后 API 调用仍然返回 401 错误?

A: 检查以下几点:

  1. 确认 token 格式正确
  2. 验证 token 是否已过期
  3. 检查接口地址是否正确设置
  4. 确认后端接口是否正常工作

Q: 页面刷新后 token 丢失?

A: 确保在路由拦截器中正确恢复 token:

ts
router.beforeEach((to, from, next) => {
  const savedToken = storage.get('token');
  if (savedToken) {
    scp.env.token.set(savedToken);
  }
  next();
});

Q: 如何在多个环境间切换?

A: 使用环境变量配置,根据不同环境创建对应的配置文件:

bash
# .env.development (开发环境)
VITE_API_BASE_DOMAIN="http://bcapi-dev1.e-tudou.com"

# .env.beta (测试环境)
VITE_API_BASE_DOMAIN="https://bcapi-beta1.e-tudou.com"

# .env.production (生产环境)
VITE_API_BASE_DOMAIN="https://bcapi.i-tudou.com"

# .env.private (私有化环境)
VITE_API_BASE_DOMAIN="https://bcapi.tudoucloud.com"

构建时 Vite 会根据 --mode 参数自动加载对应的环境配置:

bash
# 开发环境
npm run dev

# 测试环境构建
npm run build --mode beta

# 生产环境构建
npm run build --mode production

# 私有化环境构建
npm run build --mode private

📖 相关文档

Released under the MIT License.