前言
跟风微前端,看了一圈源码,觉得微前端并不适合公司目前的项目,例如css会有多次加载的问题等,而且我们也不会有juqery的库,所以将html解析成字符串动态插入并执行的路子在我这并非最优解,现在花点时间做一个共用的项目
github
https://github.com/757566833/react-vue
库的结构
以react为主,vue辅助,layout也是react写的
预览
项目期望
支持前后端分离,自带前端prod服务器,方便集成websocket等
关键字
react vue antd webpack umd
设计思路
main项目 作为整个项目的layout
react项目 负责react部分
vue项目 负责vue部分
main
main 主要负责外层的layout 需要的数据是menu当点击menu的时候,如果有tab,就跳到tab上;如果没有,就新出一个tab
渲染的逻辑和销毁的 也都在layout里面
react
任何react的页面,都在这里面,为了可以集成于一个项目,采用umd打包的方式
vue
任何vue的页面,都在这里面,为了可以集成于一个项目,采用umd打包的方式
如何渲染(这里和市面上的微前端不一样)
当打开一个tab时 ,会带出来一个element 并根据url给这个element 定义id 保证这个 id 的唯一性
当加载任意 react/vue的时候,就执行会reactdom.render这类的函数,加载了哪个页面的js,就挂载到哪个id上
子项目为何使用umd?
由于我们对系统进行优化,鉴于浏览器缓存机制,必然使用script标签等引入外部文件,这就导致了多次打开关闭 重复引入的问题,如何设计缓冲层来进行优化呢,大体上有三层,react/vue项目有自己的assets.json,指定了当前浏览器需要的js/css 叫什么,然后读取指定的 js/css文件,将js文件加载后由于umd的关系,直接new一个对象挂载在windows上,以后所有的重新打开tab,都直接取缓存的,不再去线上,css暂时没想到好的办法 目前是加一个id判断是否已经加载
这中间的关系 assets.json相当于索引
1.windows.modules里面找(自己定义的object) 2.找不到去assets.json里面找(webpack插件生成的文件索引) 3.根据浏览器自己的特性 是从disk中读取还是http请求服务器
1.前端,2.redis,3.数据库 = 1.内存读取object进行渲染 2.浏览器的srcipt标签缓存优化 3.真正从服务器拉文件
前后端的设计模式 是通用的
接下来就是又臭又长的开发环境搭建,不想关注可以直接跳到正式开始项目
整个项目需要的库
解析ts用的babel而不是ts-loader/awesome-typescript-loader
// 这里不会细说webpack 如果用开源脚手架例如umi等 需要了解下webpack内容
// 前两个是核心 第三个是热更新服务器 第四个是区分webpack mode 用的merge工具,具体内容请看webpack官方文档(不要看中文版本)
yarn add webpack webpack-cli webpack-dev-server webpack-merge --dev
// babel全家桶和react等,具体查看babel官网,还有很多插件可用
yarn add @babel/core @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators @babel/preset-env @babel/preset-react @babel/preset-typescript --dev
// webpack插件和loader 有些插件不是必须安装 例如clean可以用rm命令代替 cross-env在非多人合作下也没什么用 error-overlay 在此项目也会失效 自己酌情处理 在笔者写这个文档的时候 vue-loader刚好更新到16 改版有点大
yarn add assets-webpack-plugin babel-loader clean-webpack-plugin cross-env css-loader error-overlay-webpack-plugin file-loader fork-ts-checker-webpack-plugin html-webpack-plugin less less-loader mini-css-extract-plugin node-sass sass-loader vue-loader@15 webpack-bundle-analyzer url-loader --dev
// react 热更新插件
yarn add @hot-loader/react-dom react-hot-loader --dev
// eslint 自己酌情安装
yarn add eslint --dev
// 最主要的库
yarn add typescript react immutable react-dom react-redux react-router react-router-dom styled-components redux vue vue-class-component vue-property-decorator vue-template-compiler --dev
// ui库 vue的省略了
yarn add antd react-resizable --dev
// prod 下服务器的库(koa)
yarn add koa koa-router koa-send koa2-cors
// 补一下types
yarn add @types/assets-webpack-plugin @types/html-webpack-plugin @types/koa @types/koa-router @types/koa-send @types/koa2-cors @types/mini-css-extract-plugin @types/react @types/react-dom @types/react-hot-loader @types/react-redux @types/react-resizable @types/react-router-dom @types/styled-components @types/webpack-bundle-analyzer @types/webpack-dev-server @types/webpack-merge --dev
// ts 运行环境
yarn add ts-node --dev
最终我的package.json
{
"devDependencies": {
"@babel/core": "^7.10.3",
"@babel/plugin-proposal-class-properties": "^7.10.1",
"@babel/plugin-proposal-decorators": "^7.10.3",
"@babel/preset-env": "^7.10.3",
"@babel/preset-react": "^7.10.1",
"@babel/preset-typescript": "^7.10.1",
"@hot-loader/react-dom": "^16.13.0",
"@types/assets-webpack-plugin": "^3.9.0",
"@types/html-webpack-plugin": "^3.2.3",
"@types/koa": "^2.11.3",
"@types/koa-router": "^7.4.1",
"@types/koa-send": "^4.1.2",
"@types/koa2-cors": "^2.0.1",
"@types/mini-css-extract-plugin": "^0.9.1",
"@types/react": "^16.9.41",
"@types/react-dom": "^16.9.8",
"@types/react-hot-loader": "^4.1.1",
"@types/react-redux": "^7.1.9",
"@types/react-resizable": "^1.7.2",
"@types/react-router-dom": "^5.1.5",
"@types/styled-components": "^5.1.0",
"@types/webpack-bundle-analyzer": "^3.8.0",
"@types/webpack-dev-server": "^3.11.0",
"@types/webpack-merge": "^4.1.5",
"@typescript-eslint/eslint-plugin": "^3.4.0",
"@typescript-eslint/parser": "^3.4.0",
"antd": "^4.3.5",
"assets-webpack-plugin": "^5.0.2",
"babel-loader": "^8.1.0",
"clean-webpack-plugin": "^3.0.0",
"cross-env": "^7.0.2",
"css-loader": "^3.6.0",
"error-overlay-webpack-plugin": "^0.4.1",
"eslint": "^7.3.1",
"eslint-config-google": "^0.14.0",
"eslint-plugin-react": "^7.20.0",
"eslint-plugin-vue": "^6.2.2",
"file-loader": "^6.0.0",
"fork-ts-checker-webpack-plugin": "^5.0.5",
"html-webpack-plugin": "^4.3.0",
"immutable": "^4.0.0-rc.12",
"less": "^3.11.3",
"less-loader": "^6.1.2",
"mini-css-extract-plugin": "^0.9.0",
"node-sass": "^4.14.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-hot-loader": "^4.12.21",
"react-redux": "^7.2.0",
"react-resizable": "^1.10.1",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"redux": "^4.0.5",
"sass-loader": "^8.0.2",
"styled-components": "^5.1.1",
"ts-node": "^8.10.2",
"typescript": "^3.9.5",
"vue": "^2.6.11",
"vue-class-component": "^7.2.3",
"vue-loader": "15",
"vue-property-decorator": "^9.0.0",
"vue-template-compiler": "^2.6.11",
"webpack": "^4.43.0",
"webpack-bundle-analyzer": "^3.8.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0",
"webpack-merge": "^4.2.2"
},
"dependencies": {
"koa": "^2.13.0",
"koa-router": "^9.0.1",
"koa-send": "^5.0.0",
"koa2-cors": "^2.0.6"
}
}
eslint
// 这个会有命令提示,自己选就好了,最后会提示你缺的依赖,最后提示我缺的依赖我没用自动安装,手动用yarn安装的,因为自动安装调用的npm命令。注意,eslint有一些和ts兼容不是很好
npx eslint --init
// 添加官方推荐的eslint
yarn add eslint-plugin-react-hooks --dev
最终我的eslint
我不熟vue 需要自己加
env:
browser: true
es2020: true
node: true
extends:
- "eslint:recommended"
- "plugin:react/recommended"
- google
parser: "@typescript-eslint/parser"
parserOptions:
ecmaFeatures:
jsx: true
ecmaVersion: 11
sourceType: module
plugins:
- react
- "@typescript-eslint"
- "react-hooks"
rules:
no-unused-vars: "off"
no-prototype-builtins: "off"
react-hooks/rules-of-hooks: "error"
react-hooks/exhaustive-deps: "warn"
react/jsx-uses-react: "error"
react/jsx-uses-vars: "error"
no-undef: "off"
object-curly-spacing: ["error", "always"]
react/prop-types: 0
max-len: ["error", { "code": 120 }]
typescript
npx typescript --init
最终我的tsconfig
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
"jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
"sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
// "outDir": "./", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
"downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
"noUnusedLocals": true, /* Report errors on unused locals. */
"noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"baseUrl": "./", /* Base directory to resolve non-absolute module names. */
"paths": {
"@/*":["./src/*"],
}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
}
}
代码组成
layout 为main项目,页面本身为r和v项目main 和r 用react
v用vue
项目结构
项目结构未必合理,仅仅是一个demo 根目录下新建src
|-- src
|-- asset 静态文件
|-- global 全局文件例如方便moment全局设置等
|-- components 项目通用组件
|-- config 项目的一些配置
|-- http 封装http请求,例如fetch axios等
|-- layouts main的具体内容
|-- menu layout 左侧的menu
|-- pages
|--react react的内容
|--vue vue的内容
|--redux 实际上没什么用 在这里仅仅main用到了
|--services 各种接口
|--util 工具
|--react.tsx
|--vue.ts
webpack
这里我项目和其他的不一样 antd 不再使用按需加载,因为已有的项目使用了antd的全部组件
跟目录下新建webpack文件夹 webpack文件夹下 新建 main react vue三个文件夹
三个文件夹下分别新建webpack.common.ts webpack.dev.ts webpack.prod.ts
main下额外新建一个 template.html
main 的common
import path from 'path';
// 生成html的插件
import HtmlWebpackPlugin from 'html-webpack-plugin';
// 把css拆出来的插件
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import webpack from 'webpack';
const config: webpack.Configuration = {
entry: {
main: './src/layouts/index.tsx',
},
module: {
rules: [
{
test: /\.(tsx|ts)?$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
'@babel/preset-typescript',
'@babel/preset-react',
],
plugins: [
['@babel/plugin-proposal-decorators', { legacy: true }],
['@babel/plugin-proposal-class-properties', { loose: true }],
],
},
},
],
},
{
test: /\.css$/,
use: [{
loader: MiniCssExtractPlugin.loader,
},
{
loader: 'css-loader',
},
],
exclude: /node_modules/,
},
{
test: /\.less$/,
use: [{
loader: MiniCssExtractPlugin.loader,
},
{
loader: 'css-loader',
},
{
loader: 'less-loader',
},
],
},
{
test: /\.scss$/,
use: [{
loader: MiniCssExtractPlugin.loader,
}, {
loader: 'css-loader',
}, {
loader: 'sass-loader',
}],
exclude: /node_modules/,
},
{
test: /\.(jpg|jpeg|png|svg|gif|woff|woff2|otf|ttf)?$/,
loader: 'url-loader',
options: {
limit: 8192,
publicPath: '/',
name: 'img/[name].[hash:7].[ext]',
},
},
],
},
resolve: {
// 自动后缀
extensions: ['.tsx', '.ts'],
// 软连接
alias: {
'@': path.resolve('src'),
},
},
plugins: [
// 生成html
new HtmlWebpackPlugin({
title: 'test',
template: path.resolve(__dirname, 'template.html'),
}),
// 拆css
new MiniCssExtractPlugin({
filename: 'main/[name].[contenthash].css',
}),
// 检查类型
new ForkTsCheckerWebpackPlugin(),
],
};
export default config;
main 的dev
import path from 'path';
import webpack from 'webpack';
import merge from 'webpack-merge';
import common from './webpack.common';
// 因为是babel转译的ts 现在需要个插件检查类型
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
const config: webpack.Configuration = merge(common, {
mode: 'development',
devtool: 'eval-source-map',
output: {
path: path.resolve(__dirname, '..', '..', 'dist'),
filename: 'main/app.js',
publicPath: '/',
},
devServer: {
// spa必备
historyApiFallback: { index: '/' },
contentBase: path.join(__dirname, '..', '..', 'dist'),
host: '127.0.0.1',
hot: true,
port: 7000,
// 这个的作用是让webpack安静点
stats: 'errors-warnings',
publicPath: '/',
},
plugins: [
// 热更插件
new webpack.HotModuleReplacementPlugin(),
// 命名空间 也是热更用的
new webpack.NamedModulesPlugin(),
// 检查类型
new ForkTsCheckerWebpackPlugin(),
// 全局变量 区分环境
new webpack.DefinePlugin({
ENV_MODE: JSON.stringify('development'),
}),
],
// 热更必备
resolve: {
alias: {
'react-dom': '@hot-loader/react-dom',
},
},
});
// 覆盖掉common的配置,加入热更的babel
const config2 = merge.smart(config, {
module: {
rules: [{
test: /\.(tsx|ts)?$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
cacheDirectory: true,
babelrc: false,
presets: [
'@babel/preset-env',
'@babel/preset-typescript',
'@babel/preset-react',
],
plugins: [
['@babel/plugin-proposal-decorators', { legacy: true }],
['@babel/plugin-proposal-class-properties', { loose: true }],
'react-hot-loader/babel',
],
},
},
],
}],
},
});
export default config2;
main 的prod
import path from 'path';
import webpack from 'webpack';
// 分析打包
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
import merge from 'webpack-merge';
import common from './webpack.common';
import { CleanWebpackPlugin } from 'clean-webpack-plugin';
const config: webpack.Configuration = merge(common, {
mode: 'production',
devtool: 'source-map',
externals: {
'react': 'React',
'react-dom': 'ReactDOM',
// router用umd引入有些问题 具体可以去github issues 看
// 'react-router': 'ReactRouter',
// 'react-router-dom': 'ReactRouterDOM',
'antd': 'antd',
},
output: {
// 改成了chunk命名,避免出现0123这种
filename: 'main/[name].[chunkhash].js',
path: path.resolve(__dirname, '..', '..', 'dist'),
},
plugins: [
new BundleAnalyzerPlugin(),
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: ['main/**/*'],
}),
new webpack.DefinePlugin({
ENV_MODE: JSON.stringify('production'),
}),
],
});
export default config;
react的webpack common
// 采取约定式,pages下面的react里面所有的tsx文件(不包含某些关键字)全部为入口文件
...
const getEntry = (url: string) => {
if (url.includes('component') ||
url.includes('hooks') ||
url.includes('services') ||
url.includes('http')
) {
return;
}
const list = fs.readdirSync(url);
for (const iterator of list) {
if (iterator.includes('.tsx')) {
entry[`/${url}/${iterator}`
.replace('/index.tsx', '')
.replace('.tsx', '')
.replace('src/pages/react/', '')
.replace(/\//g, '')] = `./${url}/${iterator}`;
} else if (!iterator.includes('.')) {
getEntry(`${url}/${iterator}`);
}
}
};
const rootpath = path.join('src', 'pages', 'react');
...
其余的不再赘述 demo里有
正式开始项目
1.静态服务配置文件,这个和业务没啥关系,指定了不同环境下,服务器在哪
src/config/index.tsx
后缀ts tsx都可以,这个文件是main调用
// react 在7001端口 vue在7002
export const cssPrefix = 'egu-layout-';
export const modulePath = {
r: {
development: '//127.0.0.1:7001',
production: '',
},
v: {
development: '//127.0.0.1:7002',
production: '',
},
};
2. main开始
1. src/layouts/index.tsx
// 简单的react 入门 以后的入门级代码不再展示
import React from 'react';
import ReactDOM from 'react-dom';
import {
BrowserRouter as Router,
Route,
} from 'react-router-dom';
import store from '@/redux/store';
import { Provider } from 'react-redux';
import Base from './base';
import '@/global/react/global.scss';
import '@/global/react/global';
// antd 限定了中文
import { ConfigProvider } from 'antd';
import zhCN from 'antd/es/locale/zh_CN';
const Layout: React.FC = () => {
return <Provider store={store}>
<ConfigProvider locale={zhCN}>
{/* base模块 */}
<Base />
</ConfigProvider>
</Provider>;
};
const AppRouter: React.FC = () => {
return (
<Router>
<Route path="*" component={Layout} />
</Router>
);
};
const App: React.FC = () => {
return <>
<AppRouter />
</>;
};
ReactDOM.render(<App />, document.getElementById('root'));
2. src/menu/interface.ts (仅为demo)
数据类型不是通用的,此数据类型仅为demo,随意按自己想要的实现
// menu的item
export interface IMenuBean {
title: string;
path: string;
module?: 'r' | 'v'
type?: EMenuType;
authority?: string;
children?: IMenuBean[];
}
// menu item的类型
export enum EMenuType {
SubMenu = 'SubMenu',
Item = 'Item',
NoMenu = 'NoMenu',
Header = 'Header'
}
// 这个模块属于react还是vue
export enum module {
react = 'r',
vue = 'v'
}
3. src/menu/index.ts
import { IMenuBean } from './interface';
import basics from './basics';
import mall from './mall';
import assets from './assets';
import finance from './finance';
import config from './config';
const menu: IMenuBean[] = [
basics, // 运营
mall, // 业务
finance, // 财务
assets, // 资产
config, // 设置
];
export default menu;
4.src/menu/basics.ts
import { IMenuBean, EMenuType, module } from './interface';
const basics: IMenuBean = {
title: 'menu1',
type: EMenuType.Header,
path: '/basics/',
authority: 'menu1',
module: module.react,
children: [
{
title: 'submenu1',
type: EMenuType.SubMenu,
path: '/basics/enterprise',
authority: 'pc-op-enterprise',
children: [
{
title: 'menu11',
type: EMenuType.Item,
path: '/basics/enterprise/authentication',
authority: 'menu11',
},
{
title: 'menu12',
type: EMenuType.Item,
path: '/basics/enterprise/menu',
authority: 'menu12',
},
],
},
{
title: 'submenu2',
type: EMenuType.SubMenu,
path: '/basics/test',
authority: 'pc-op-account',
children: [
{
title: 'menu21',
type: EMenuType.Item,
path: '/basics/test/test1',
module: module.vue,
authority: 'menu21',
},
],
},
],
};
export default basics;
其余的看源码
5. src/services/index.ts
// 模拟权限接口
export const getWebAuthority:()=>Promise<{[key:string]:boolean}> = async ()=>{
return {
menu1:true,
submenu1:true,
menu11:true,
submenu2:true,
menu21:true
}
}
- src/layouts/base/index.tsx
// 不再展示基础代码 github里面有
...
const Base: React.FC = () => {
...
// 具体基础结构 和antd demo没什么区别
return (
<>
<Spin
size='large'
spinning={loading}
className={loading ?
'egu-layout-loading max-HW' : 'disappear'
}
wrapperClassName={loading ?
'egu-layout-loading-wrapper max-HW' :
'disappear'
}>
<div className='max_HW' />
</Spin>
<Layout
className={!loading ?'egu-layout' :'disappear'}
style={{ height: '100vh' }}
>
<Sider>
<div className='egu-layout-logo flex' />
<MenuMemo />
</Sider>
<Layout className={'site-layout'}>
<HeaderMemo />
<Content>
{/* 内容部分主要是这个index */}
<Index />
</Content>
</Layout>
</Layout>
</>
);
};
export default Base;
6. src/layouts/index/index.tsx
...
// 根据url获取当前有没有这个tab
const getTab = (url: string): IMenuBean | undefined => {
if (url == '/') {
return undefined;
}
let result: IMenuBean | undefined = undefined;
let stop = false;
let index = 0;
let _menu: IMenuBean[] | undefined = menu;
while (!stop) {
if (!_menu || !_menu[index]) {
return;
}
if (url == _menu[index].path && EMenuType.Item != _menu[index].type) {
return;
} else if (url.startsWith(_menu[index].path) &&
_menu[index].type == EMenuType.Item) {
result = _menu[index];
stop = true;
} else if (url.startsWith(_menu[index].path)) {
_menu = _menu[index].children;
index = 0;
} else {
index++;
}
}
return result;
};
const Index: React.FC = () => {
const dispatch = useDispatch();
const [tabs, setTabs] = useState<{
title: string, key: string, id: string
}[]>([]);
// 这边自己记录了一个history 方便自己用 看有没有用途 如果没有用途就可以去掉
const history = useRef<{url:string}[]>([]);
const [activeKey, setActiveKey] = useState<null | string>(null);
const lastTab = useRef('');
const url = useRouteMatch().url;
// url变化时究竟是打开 还是跳转
useEffect(() => {
const activeTab = getTab(url);
let hastab = false;
for (const iterator of tabs) {
if (iterator.key == activeTab?.path) {
hastab = true;
break;
}
}
if (activeTab) {
setActiveKey(activeTab.path);
lastTab.current = activeTab.path;
if (!hastab) {
setTabs(
[...tabs,
{
title: activeTab.title,
key: activeTab.path,
id: activeTab.path,
},
],
);
}
}
}, [dispatch, history, tabs, url]);
const routerHistory = useHistory();
const tabsClick = (url: string) => {
routerHistory.push(url);
};
const pushHistory=useCallback(()=>{
history.current.push({ url: url });
}, [url]);
useEffect(()=>{
pushHistory();
}, [pushHistory, url]);
const edit = (
targetKey: React.MouseEvent | React.KeyboardEvent | string,
action: 'add' | 'remove',
) => {
console.log(targetKey, action);
if (action == 'remove') {
let index: null | number = null;
console.log(tabs, targetKey);
tabs.forEach((item, i) => {
if (item.key == targetKey) {
index = i;
return;
}
});
console.log(index);
if (index != null) {
if (activeKey == targetKey) {
const reHistory = history.current.reverse();
reHistory.shift();
console.log('yes', reHistory);
for (const iterator of reHistory) {
let findLast = false;
for (const tab of tabs) {
if (tab.key==iterator.url) {
routerHistory.push(iterator.url);
findLast = true;
break;
}
}
if (findLast) {
break;
}
}
// routerHistory.push(history[]);
}
const newtabs = [...tabs.slice(0, index), ...tabs.slice(index + 1)];
console.log(newtabs);
setTabs(newtabs);
if (window.jsmodules && typeof targetKey == 'string') {
window.jsmodules[targetKey.replace(/\//g, '')].unRender();
}
}
}
};
const showKey = activeKey ? activeKey : lastTab.current;
return (
<div className='egu-saas-layout-content'>
<Tabs
hideAdd={true}
type="editable-card"
activeKey={showKey}
onTabClick={tabsClick}
onEdit={edit}
className='egu-saas-layout-content-tabs flex'
>
{tabs.map((pane) => (
<TabPane
forceRender={true}
tab={pane.title}
key={pane.key}
className={`
egu-saas-layout-content-tabs-tabpane
${activeKey == pane.key ?
'egu-saas-layout-content-tabs-tabpane-height-max' :
''}`
}
>
{/* 最终渲染用的组件 */}
<Body id={pane.id} />
</TabPane>
))}
</Tabs>
</div>
);
};
export default Index;
7. (核心内容) src/layouts/components/body/index.tsx
import React, { useEffect, useState, useCallback } from 'react';
import './styles.scss';
import { Spin } from 'antd';
import { Http } from '@/http';
import { useRouteMatch } from 'react-router';
import { useSelector } from 'react-redux';
import { modulePath } from '@/config';
import { IState } from '@/redux/state';
import styled from 'styled-components';
const Padding = styled.div`
height:100%;
padding:12px
`;
const Body: React.FC<{ id: string }> = React.memo((props) => {
console.log('body');
const url = useRouteMatch().url;
const moduleMap = useSelector(useCallback(
(storeData:IState) => storeData.moduleMap
, []),
);
const [loading, setLoading] = useState(false);
const reRender =useCallback( () => {
// 判断是为了迎合ts 理论上走到这个函数一定会有 jsmodules
if (window.jsmodules) {
try {
window.jsmodules[props.id.replace(/\//g, '')].render();
} catch (error) {
console.log(error);
}
}
}, [props.id]);
const getComponent = useCallback(()=>{
const module = new window.JSComponent(props.id.replace(/\//g, ''));
if (window.jsmodules) {
window.jsmodules[props.id.replace(/\//g, '')] = module;
} else {
window.jsmodules = {};
window.jsmodules[props.id.replace(/\//g, '')] = module;
}
setLoading(false);
reRender();
}, [props.id, reRender]);
const getComponetUrl =useCallback( (json:{[key:string]:any}, type:'r'|'v') => {
console.log('getComponetUrl', json, props.id.replace(/\//g, ''));
// 理论上走到这里必然会有asset
const moduleJson = json[props.id.replace(/\//g, '')];
if (moduleJson) {
setLoading(true);
if (moduleJson.js) {
const modules = document.createElement('script');
// 创建script标签;
modules.type = 'text/javascript';
modules.src = `${modulePath[type][ENV_MODE]}${moduleJson.js}`;
modules.onload = getComponent; // 引入url;
document.body.appendChild(modules);
}
if (moduleJson.css) {
if (!document.getElementById(`${props.id.replace(/\//g, '')}css`)) {
const link = document.createElement('link');
link.type = 'text/css';
link.rel = 'stylesheet';
link.href = `${modulePath[type][ENV_MODE]}${moduleJson.css}`;
link.id = `${props.id.replace(/\//g, '')}css`;
document.body.appendChild(link);
}
}
}
}, [getComponent, props.id]);
const getAsset = useCallback(async (type:'r'|'v') => {
const modoleJson = await Http.getStatic(type);
const json:{ [key: string]: any } = modoleJson?.text||{};
switch (type) {
case 'r':
window.reactAssets = json;
break;
case 'v':
window.vueAssets = json;
break;
default:
break;
}
getComponetUrl(json, type);
}, [getComponetUrl]);
useEffect(() => {
// 如果有静态文件的目录 如果有module的实体 且实体内有当然的模块实体
console.log('useEffect', moduleMap);
if (moduleMap) {
const type = moduleMap[url];
const jsmodules = window.jsmodules;
const vueAssets = window.vueAssets;
const reactAssets = window.reactAssets;
if (
(type=='r'&&reactAssets||type=='v'&&vueAssets)&&
jsmodules&&
jsmodules[props.id.replace(/\//g, '')]
) {
reRender();
} else if (type=='r'&&reactAssets) {
// 如果只有静态文件
getComponetUrl(reactAssets, type);
} else if (type=='v'&&vueAssets) {
getComponetUrl(vueAssets, type);
} else {
// 如果啥也没有
console.log('getAsset');
getAsset(type);
}
}
}, [props.id, moduleMap, getComponetUrl, getAsset, reRender, url]);
// window.sys
return (
<div className='egu-saas-layout-body'>
<Spin spinning={loading} >
<Padding>
<div className='egu-saas-layout-root' id={props.id.replace(/\//g, '')} />
</Padding>
</Spin>
</div>
);
}, () => true);
Body.displayName = 'Body';
export default Body;
8. typings.d.ts 我们在windows上定义了一些东西,在上一步应该会有提示 补一下声明
declare module '*.css';
declare module '*.less';
declare module '*.png';
interface Window {
jsmodules?: { [key: string]: any };
reactAssets?:{ [key: string]: any },
vueAssets?:{ [key: string]: any },
httpApi: string;
JSComponent: any;
store: any
}
declare const ENV_MODE: 'development' | 'production';
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}
3. react 开始
1.react主函数
// 随便写个文件
// src/pages/react/basics/enterprise/authentication/index.tsx
import React from 'react';
import { reactHOC } from '@/util/react';
const Index: React.FC= () => {
return (
<div>react </div>
);
};
export const JSComponent = reactHOC(Index);
2. reacthoc
上一个文件如果直接导出被reactdom.render 渲染一下 就可以直接用了,但是我们不仅满足于此,我们希望在main层调用的时候,由main来管理一个页面的生命周期,具体代码详见上文的 核心代码
// src/util/react.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/es/locale/zh_CN';
interface component{
}
/**
*
* @param {React.FC} ReactModule react每个模块的工厂
* @return {component} 工具类 工厂模式
*/
export const reactHOC = (ReactModule: React.FC):component => {
return class JSComponent {
id: string
timmer: NodeJS.Timeout | null | number
/**
* Create a point.
* @param {string} id 字符串 标示着唯一值
*/
constructor(id: string) {
this.id = id;
this.timmer = null;
}
/**
* 最终的渲染函数
*/
renderDom() {
if (document.getElementById(this.id)) {
ReactDOM.render(
<ConfigProvider locale={zhCN}>
<ReactModule />
</ConfigProvider>
, document.getElementById(this.id));
}
}
/**
* 渲染逻辑层,主要是为了兼容 浏览器空闲时间的api 否则会造成某些情况下不渲染的bug,猜测和requestIdleCallback相关
*/
render() {
this.renderDom();
this.timmer = setInterval(() => {
this.renderDom();
if (document.getElementById(this.id)?.childElementCount != 0) {
if (this.timmer) {
clearInterval(Number(this.timmer));
this.timmer = null;
}
}
}, 200);
}
/**
* 卸载函数
*/
unRender() {
const dom = document.getElementById(this.id);
if (dom) {
ReactDOM.unmountComponentAtNode(dom);
}
}
};
};
4. vue
1. vue主函数 这就略过了 本身拿的也是vue脚手架的demo页面
2 vuehoc
// src/util/vue.ts
import Vue, { VueConstructor } from 'vue';
import CombinedVueInstance from 'vue/types';
export const vueHOC = (vue:VueConstructor)=>{
return class JSComponent {
id: string
timmer: NodeJS.Timeout | null | number
vue:CombinedVueInstance
/**
* Create a point.
* @param {string} id 字符串 标示着唯一值
*/
constructor(id: string) {
this.id = id;
this.timmer = null;
this.vue = new Vue({
render: (h) => h(vue),
});
}
/**
* 最终的渲染函数
*/
renderDom() {
if (window.document.querySelector(`#${this.id}`)) {
this.vue.$mount(`#${this.id}`);
}
}
/**
* 渲染逻辑层,主要是为了兼容 浏览器空闲时间的api 否则会造成某些情况下不渲染的bug
*/
render() {
this.renderDom();
this.timmer = setInterval(() => {
this.renderDom();
if (window.document.querySelector(`#${this.id}`)?.childElementCount != 0) {
if (this.timmer) {
clearInterval(Number(this.timmer));
this.timmer = null;
}
}
}, 200);
}
/**
* 卸载函数
*/
unRender() {
console.log('vue unRender');
this.vue.$destroy();
}
};
};
这里面react 官方推荐的 查找dom方法 react 是getelementbyid ,而vue实现的则是用document.querySelector
开发和打包
1.命令
// package.json
...
"scripts": {
"start": "webpack-dev-server --config webpack/main/webpack.dev.ts",
"build": "webpack --config webpack/main/webpack.prod.ts",
"start:react": "webpack-dev-server --config webpack/react/webpack.dev.ts",
"build:react": "webpack --config webpack/react/webpack.prod.ts",
"start:vue": "webpack-dev-server --config webpack/vue/webpack.dev.ts",
"build:vue": "webpack --config webpack/vue/webpack.prod.ts"
}
2.开发模式
// main
yarn start
// react
yarn run start:react
// vue
yarn run start:vue
3.生产模式
// main
yarn run build
// react
yarn run build:react
// vue
yarn run build:vue
4.先后端分离
到此 其实就结束了,因为我们打包出来了静态文件
当然前后端分离的模式还有待商榷,如果你满足于做一个纯粹的前端,你大可只把静态文件提供到nginx下
如果你不满足于前端,还需要webnsocket,mongodb,redis,等前端全家桶,甚至为了实现serverless而打好基础,你都应该掌握nodejs
// 新建main.js
const path = require('path');
const Koa = require('koa');
const send = require('koa-send');
const Router = require('koa-router');
const cors = require('koa2-cors');
const app = new Koa();
const router = new Router();
app.use(cors({
origin: "*",
exposeHeaders: ["WWW-Authenticate", "Server-Authorization"],
maxAge: 50000,
credentials: true,
allowMethods: ["GET", "PUT", "POST", "PATCH", "DELETE", "HEAD", "OPTIONS"],
allowHeaders: ["Content-Type", "Authorization", "Accept"],
}));
// router.get()
router.get(/(.*?)/g, async (ctx) => {
const url = ctx.path;
if (url.includes('.')) {
await send(ctx, ctx.path, {
root: path.join(__dirname, 'dist'),
maxAge: 365 * 24 * 60 * 60 * 1000
});
} else {
await send(ctx, './index.html', {
root: path.join(__dirname, 'dist'),
maxAge: 0
});
}
})
app.use(router.routes()).use(router.allowedMethods())
app.listen(3000);
启动命令
node main.js
你仍旧可以选择pm2 docker 等工具 来从不同维度管理你的服务
这样 同样的方式用nginx反向代理到服务,所有的header头等,均由前端自己控制
未完成
- 未完成的部分例如antd的moment比较大进一步减小体积
- react-router 仍旧有些bug