隨着前端應用日益複雜,微前端架構已成為解決單體前端應用痛點的有效方案。本文將深入探討如何在PHP全棧應用中實現微前端架構,包括模塊化設計、組件共享、狀態管理和部署策略。
微前端基礎架構
主應用集成
// src/Controller/MicroFrontendController.php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class MicroFrontendController
{
private HttpClientInterface $httpClient;
private string $assetManifestUrl;
public function __construct(
HttpClientInterface $httpClient,
string $assetManifestUrl
) {
$this->httpClient = $httpClient;
$this->assetManifestUrl = $assetManifestUrl;
}
/**
* @Route("/dashboard", name="micro_frontend_dashboard")
*/
public function dashboard(): Response
{
// 1. 獲取微前端資產清單
$manifest = $this->fetchAssetManifest();
// 2. 渲染主模板並注入微前端配置
return $this->render('micro_frontend/dashboard.html.twig', [
'mfConfig' => [
'dashboard' => [
'js' => $manifest['dashboard.js'],
'css' => $manifest['dashboard.css'],
'mountPoint' => '#dashboard-container'
]
]
]);
}
private function fetchAssetManifest(): array
{
try {
$response = $this->httpClient->request(
'GET',
$this->assetManifestUrl
);
return $response->toArray();
} catch (\Exception $e) {
throw new \RuntimeException(
'Failed to fetch micro frontend asset manifest',
0,
$e
);
}
}
}
前端主框架實現
// assets/js/micro-frontend-loader.js
class MicroFrontendLoader {
constructor(config) {
this.config = config;
this.loadedUris = new Set();
this.mountedApps = new Map();
}
async loadApp(name) {
const appConfig = this.config[name];
if (!appConfig) {
throw new Error(`Micro frontend config not found for ${name}`);
}
// 加載CSS
if (appConfig.css && !this.loadedUris.has(appConfig.css)) {
await this.loadCss(appConfig.css);
this.loadedUris.add(appConfig.css);
}
// 加載JS
if (appConfig.js && !this.loadedUris.has(appConfig.js)) {
await this.loadScript(appConfig.js);
this.loadedUris.add(appConfig.js);
}
return window[`mount${name.charAt(0).toUpperCase() + name.slice(1)}`];
}
async mountApp(name, props = {}) {
if (this.mountedApps.has(name)) {
return this.mountedApps.get(name);
}
const mountFn = await this.loadApp(name);
const mountPoint = document.querySelector(this.config[name].mountPoint);
if (!mountPoint) {
throw new Error(`Mount point not found for ${name}`);
}
const unmountFn = mountFn(mountPoint, props);
this.mountedApps.set(name, unmountFn);
return unmountFn;
}
unmountApp(name) {
if (this.mountedApps.has(name)) {
this.mountedApps.get(name)();
this.mountedApps.delete(name);
}
}
loadScript(src) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
script.onload = resolve;
script.onerror = () => reject(new Error(`Failed to load script: ${src}`));
document.head.appendChild(script);
});
}
loadCss(href) {
return new Promise((resolve, reject) => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = href;
link.onload = resolve;
link.onerror = () => reject(new Error(`Failed to load CSS: ${href}`));
document.head.appendChild(link);
});
}
}
// 初始化加載器
const loader = new MicroFrontendLoader(window.mfConfig);
// 導出為全局對象
window.MicroFrontendLoader = loader;
微前端模塊開發
獨立構建的微前端
// dashboard-mf/src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import DashboardApp from './DashboardApp';
// 掛載函數
export function mountDashboard(container, props) {
ReactDOM.render(
<DashboardApp {...props} />,
container
);
// 返回卸載函數
return () => {
ReactDOM.unmountComponentAtNode(container);
};
}
// 開發環境獨立運行
if (process.env.NODE_ENV === 'development') {
const devContainer = document.getElementById('root');
if (devContainer) {
mountDashboard(devContainer, {
apiUrl: 'http://localhost:8000/api',
user: { id: 1, name: 'Developer' }
});
}
}
Webpack配置示例
// dashboard-mf/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
entry: './src/index.js',
output: {
publicPath: 'http://localhost:3001/',
filename: '[name].[contenthash].js'
},
module: {
rules: [
{
test: /\.jsx?$/,
loader: 'babel-loader',
exclude: /node_modules/,
options: {
presets: ['@babel/preset-react']
}
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
plugins: [
new ModuleFederationPlugin({
name: 'dashboard',
filename: 'remoteEntry.js',
exposes: {
'./DashboardApp': './src/DashboardApp'
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
'react-router-dom': { singleton: true }
}
}),
new HtmlWebpackPlugin({
template: './public/index.html'
})
],
devServer: {
port: 3001,
historyApiFallback: true,
headers: {
'Access-Control-Allow-Origin': '*'
}
}
};
PHP與微前端通信
// src/Service/MicroFrontendDataProvider.php
namespace App\Service;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Psr\Cache\CacheItemInterface;
class MicroFrontendDataProvider
{
private HttpClientInterface $httpClient;
private FilesystemAdapter $cache;
private string $apiBaseUrl;
public function __construct(
HttpClientInterface $httpClient,
string $apiBaseUrl
) {
$this->httpClient = $httpClient;
$this->cache = new FilesystemAdapter();
$this->apiBaseUrl = rtrim($apiBaseUrl, '/');
}
public function getDashboardData(int $userId): array
{
$cacheKey = "dashboard_data_{$userId}";
$cacheItem = $this->cache->getItem($cacheKey);
if ($cacheItem->isHit()) {
return $cacheItem->get();
}
$data = $this->fetchDashboardData($userId);
$cacheItem->set($data)->expiresAfter(300); // 5分鐘緩存
$this->cache->save($cacheItem);
return $data;
}
private function fetchDashboardData(int $userId): array
{
try {
$response = $this->httpClient->request(
'GET',
"{$this->apiBaseUrl}/dashboard/{$userId}"
);
return $response->toArray();
} catch (\Exception $e) {
throw new \RuntimeException(
'Failed to fetch dashboard data',
0,
$e
);
}
}
public function invalidateCache(int $userId): void
{
$this->cache->deleteItem("dashboard_data_{$userId}");
}
}
共享組件與狀態管理
跨微前端共享組件
// shared-components/src/Button.jsx
import React from 'react';
import PropTypes from 'prop-types';
import './Button.css';
export function Button({
children,
primary = false,
size = 'medium',
onClick,
...props
}) {
const mode = primary ? 'btn-primary' : 'btn-secondary';
return (
<button
type="button"
className={`btn btn-${size} ${mode}`}
onClick={onClick}
{...props}
>
{children}
</button>
);
}
Button.propTypes = {
children: PropTypes.node.isRequired,
primary: PropTypes.bool,
size: PropTypes.oneOf(['small', 'medium', 'large']),
onClick: PropTypes.func
};
// shared-components/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
output: {
publicPath: 'http://localhost:3003/'
},
plugins: [
new ModuleFederationPlugin({
name: 'sharedComponents',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/Button',
'./Card': './src/Card',
'./Modal': './src/Modal'
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
'prop-types': { singleton: true }
}
})
]
};
全局狀態管理
// shared-state/src/store.js
import { createStore } from 'redux';
const initialState = {
user: null,
notifications: [],
preferences: {
theme: 'light',
locale: 'en-US'
}
};
function reducer(state = initialState, action) {
switch (action.type) {
case 'SET_USER':
return { ...state, user: action.payload };
case 'ADD_NOTIFICATION':
return {
...state,
notifications: [...state.notifications, action.payload]
};
case 'UPDATE_PREFERENCES':
return {
...state,
preferences: { ...state.preferences, ...action.payload }
};
default:
return state;
}
}
const store = createStore(reducer);
// 訂閲函數用於跨微前端同步狀態
export function subscribeToStore(callback) {
let currentState = store.getState();
const handleChange = () => {
const nextState = store.getState();
if (nextState !== currentState) {
currentState = nextState;
callback(currentState);
}
};
const unsubscribe = store.subscribe(handleChange);
return unsubscribe;
}
export function getState() {
return store.getState();
}
export function dispatch(action) {
store.dispatch(action);
}
// 在微前端中集成
export function connectToStore(Component) {
return function WrappedComponent(props) {
const [state, setState] = React.useState(getState());
React.useEffect(() => {
return subscribeToStore(setState);
}, []);
return <Component {...props} {...state} dispatch={dispatch} />;
};
}
路由與導航
前端路由集成
// shell-app/src/App.jsx
import React from 'react';
import { BrowserRouter, Route, Switch, Link } from 'react-router-dom';
import { MicroFrontend } from './MicroFrontend';
const DashboardPage = ({ history }) => (
<MicroFrontend
host="http://localhost:3001"
name="dashboard"
history={history}
props={{ apiUrl: '/api' }}
/>
);
const ProfilePage = ({ history }) => (
<MicroFrontend
host="http://localhost:3002"
name="profile"
history={history}
props={{ apiUrl: '/api' }}
/>
);
export default function App() {
return (
<BrowserRouter>
<div className="app-shell">
<nav className="app-nav">
<Link to="/">Dashboard</Link>
<Link to="/profile">Profile</Link>
</nav>
<Switch>
<Route exact path="/" component={DashboardPage} />
<Route path="/profile" component={ProfilePage} />
</Switch>
</div>
</BrowserRouter>
);
}
// shell-app/src/MicroFrontend.jsx
import React, { useEffect, useRef } from 'react';
export function MicroFrontend({ host, name, history, props }) {
const containerRef = useRef(null);
useEffect(() => {
const scriptId = `micro-frontend-script-${name}`;
const renderMicroFrontend = () => {
window[`mount${name}`](containerRef.current, {
...props,
history
});
};
if (document.getElementById(scriptId)) {
renderMicroFrontend();
return;
}
fetch(`${host}/asset-manifest.json`)
.then(res => res.json())
.then(manifest => {
const script = document.createElement('script');
script.id = scriptId;
script.src = `${host}${manifest['main.js']}`;
script.onload = renderMicroFrontend;
document.head.appendChild(script);
});
return () => {
window[`unmount${name}`] && window[`unmount${name}`]();
};
}, [host, name, props, history]);
return <div ref={containerRef} />;
}
後端路由代理
// src/Controller/RouterController.php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class RouterController
{
private HttpClientInterface $httpClient;
private array $microFrontendConfig;
public function __construct(
HttpClientInterface $httpClient,
array $microFrontendConfig
) {
$this->httpClient = $httpClient;
$this->microFrontendConfig = $microFrontendConfig;
}
/**
* @Route("/{path}", name="micro_frontend_router", requirements={"path"="^(?!api|_profiler).+"})
*/
public function route(Request $request, string $path = ''): Response
{
$route = $path ?: 'dashboard';
if (!isset($this->microFrontendConfig[$route])) {
throw $this->createNotFoundException(
"Micro frontend route {$route} not found"
);
}
$config = $this->microFrontendConfig[$route];
try {
$response = $this->httpClient->request(
'GET',
$config['server'] . '/' . ltrim($path, '/')
);
return new Response(
$response->getContent(),
$response->getStatusCode(),
['Content-Type' => 'text/html']
);
} catch (\Exception $e) {
return new Response(
$this->renderView('error/micro_frontend.html.twig', [
'route' => $route
]),
Response::HTTP_BAD_GATEWAY
);
}
}
/**
* @Route("/api/micro-frontends", name="micro_frontend_config")
*/
public function getConfig(): JsonResponse
{
return $this->json(array_map(
fn($config) => [
'js' => $config['js'],
'css' => $config['css']
],
$this->microFrontendConfig
));
}
}
部署策略與優化
靜態資源版本控制
// src/Service/AssetVersionStrategy.php
namespace App\Service;
use Symfony\Component\Asset\VersionStrategy\VersionStrategyInterface;
class AssetVersionStrategy implements VersionStrategyInterface
{
private string $manifestPath;
private ?array $manifest = null;
private string $format;
public function __construct(
string $manifestPath,
string $format = '%s?%s'
) {
$this->manifestPath = $manifestPath;
$this->format = $format;
}
public function getVersion(string $path): string
{
return $this->getManifestHash($path) ?: '';
}
public function applyVersion(string $path): string
{
$version = $this->getVersion($path);
if ('' === $version) {
return $path;
}
return sprintf($this->format, $path, $version);
}
private function getManifestHash(string $path): ?string
{
if (null === $this->manifest) {
if (!file_exists($this->manifestPath)) {
return null;
}
$this->manifest = json_decode(
file_get_contents($this->manifestPath),
true
);
}
return $this->manifest[$path] ?? null;
}
}
Nginx配置優化
# /etc/nginx/conf.d/micro-frontend.conf
server {
listen 80;
server_name app.example.com;
root /var/www/html/public;
location / {
try_files $uri /index.php$is_args$args;
}
# 微前端靜態資源
location ~ ^/assets/(dashboard|profile|settings)/ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# API路由
location /api {
try_files $uri /index.php$is_args$args;
}
location ~ ^/index\.php(/|$) {
fastcgi_pass php-fpm:9000;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
internal;
}
location ~ \.php$ {
return 404;
}
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
}
CI/CD管道配置
# .github/workflows/micro-frontend.yml
name: Micro Frontend CI/CD
on:
push:
branches: [ main ]
paths:
- 'dashboard-mf/**'
- 'profile-mf/**'
- 'shared-components/**'
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
mf: ['dashboard-mf', 'profile-mf', 'shared-components']
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: 16
- name: Install dependencies
working-directory: ./${{ matrix.mf }}
run: npm install
- name: Build
working-directory: ./${{ matrix.mf }}
run: npm run build
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.mf }}-assets
path: ${{ matrix.mf }}/dist/
retention-days: 1
deploy:
needs: build
runs-on: ubuntu-latest
environment: production
steps:
- name: Download artifacts
uses: actions/download-artifact@v3
with:
path: artifacts
- name: Deploy to S3
uses: jakejarvis/s3-sync-action@v0.5.1
with:
args: --acl public-read --delete
env:
AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: 'us-east-1'
SOURCE_DIR: 'artifacts'
- name: Update asset manifest
run: |
aws s3 cp s3://${{ secrets.AWS_S3_BUCKET }}/asset-manifest.json asset-manifest.json || echo '{}' > asset-manifest.json
for mf in dashboard-mf profile-mf shared-components; do
jq -s '.[0] * .[1]' asset-manifest.json artifacts/$mf/dist/asset-manifest.json > merged.json
mv merged.json asset-manifest.json
done
aws s3 cp asset-manifest.json s3://${{ secrets.AWS_S3_BUCKET }}/asset-manifest.json --acl public-read
結語
本文深入探討了PHP全棧應用中的微前端架構實踐,包括:
- 微前端基礎架構:主應用集成與模塊加載機制
- 獨立模塊開發:組件封裝與獨立構建部署
- 共享資源管理:組件庫與狀態管理方案
- 路由導航方案:前後端路由協同
- 部署優化策略:靜態資源管理與CI/CD管道
這些技術實踐為PHP全棧開發帶來以下優勢:
- 增量升級:逐步替換舊功能而不影響整體應用
- 獨立部署:各功能模塊可獨立開發與發佈
- 技術異構:不同模塊可採用不同前端技術棧
- 團隊自治:各團隊可專注於特定業務領域
實施建議:
- 漸進式採用:從非核心功能開始引入微前端
- 統一規範:制定組件接口與通信標準
- 性能監控:關注資源加載與運行時性能
- 開發體驗:建立本地開發協作機制
- 測試策略:完善集成測試與契約測試
通過合理應用微前端架構,PHP全棧應用可以兼具單體應用的簡單性和分佈式系統的靈活性,為複雜業務場景提供可持續的解決方案。