跳到主要内容

2 篇博文 含有标签「react」

查看所有标签

electron学习笔记

· 阅读需 5 分钟
XOne
小站站长

Webpack + Typescript

npm init electron-app@latest my-new-app -- --template=webpack-typescript

React代码需要,增加jsx支持

{
"compilerOptions": {
"jsx": "react-jsx"
}
}

npm 换成 pnpm 需要 install

pnpm install @electron-forge/plugin-base @electron-forge/maker-base @types/webpack --save-dev

路由

pnpm install react-router-dom

less

pnpm install less less-loader --save-dev

webpack.rules.ts

{
test: /\.less$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
mode: 'local',
localIdentName: '[name]__[local]___[hash:base64:5]',
},
importLoaders: 1,
},
},
'less-loader',
],
}

antd pro

pnpm install antd --save
pnpm install @ant-design/pro-components --save
pnpm install @ant-design/pro-card
pnpm install @ant-design/pro-descriptions
pnpm install @ant-design/pro-field
pnpm install @ant-design/pro-form
pnpm install @ant-design/pro-layout
pnpm install @ant-design/pro-list
pnpm install @ant-design/pro-provider
pnpm install @ant-design/pro-skeleton
pnpm install @ant-design/pro-table
pnpm install @ant-design/pro-utils

编译使用 babel-plugin-import 优化 antd 打包体积

pnpm install babel-plugin-import --save-dev
pnpm install @ant-design/icons --save

数据存储 electron-store

conf 是处理类型报错安装

pnpm install electron-store conf

tsconfig.json中模版默认是:"moduleResolution": "node" ,electron-forge似乎很难或者不支持配置"moduleResolution": "node16" 或者其他属性值,这里采用投机取巧的方式配置别名,修复electron-store的类型错误
tsconfig.json

{
"compilerOptions": {
"paths": {
"conf": ["node_modules/conf/dist/source"]
}
}
}

设置路径别名

tsconfig.json

{
"compilerOptions": {
"paths": {
"@/*": ["src/*"]
}
}
}

webpack.renderer.config.ts

{
resolve: {
alias: { '@': path.resolve(__dirname, './src') },
},
}

除了 tsconfig.json webpack.renderer.config.ts 配置别名外 还需要修复 eslint 提示

pnpm install eslint-import-resolver-typescript --save-dev

.eslintrc.json

"settings": {
"import/resolver": {
"typescript": {}
}
}

进程通信(‌IPC)‌

使用ipcMain和ipcRenderer模块:‌主进程通过ipcMain模块监听事件,‌渲染进程通过ipcRenderer模块发送事件,‌实现双向通信。‌
使用remote模块:‌允许在渲染进程中直接调用主进程中的模块和方法,‌实现简单的跨进程通信‌。
使用webContents模块:‌通过此模块可以直接向指定的渲染进程发送消息,‌实现通信‌。‌
使用进程之间的共享变量:‌通过建立全局变量或共享内存来实现跨进程通信‌。‌

使用ipcMain和ipcRenderer模块

渲染进程 -> 主进程
方式一:

// 渲染进程 发出事件
ipcRenderer.send("channel1", 1, 2, 3);
// 主进程 监听事件
ipcMain.on("channel1", (e, a, b, c) => {
console.log("监听到渲染进程的发出的事件(callback)", a, b, c);
})

方式二:

// 渲染进程 发出事件
ipcRenderer.invoke("channel2", 1, 2, 3);
// 主进程 监听事件
ipcMain.handle("channel2", (e, a, b, c) => {
console.log("监听到渲染进程的发出的事件(Promise)", a, b, c)
})

主进程 -> 渲染进程
方式一:

// 渲染进程 监听事件
ipcRenderer.on(channel, handler)
// 主进程 发出事件
mainWindow.webContents.send("channel3");

网络请求

pnpm install axios
// forge.config.ts 配置Content-Security-Policy 不是配置html中
const config: ForgeConfig = {
plugins: [
new WebpackPlugin({
devContentSecurityPolicy: "default-src 'self'; connect-src *; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline';",
devServer: {
//配置代理 解决跨域
proxy: {
'/login': {
target: 'http://localhost:8080', // 另一个API服务器地址
changeOrigin: true,
},
},
},
}),
],
};


//对应axios拓展文件 requestUtil.ts
const request = axios.create({
baseURL: 'http://localhost:4000',
});

requestUtil.ts 文件详细如下

import axios from 'axios';

const request = axios.create({
baseURL: 'http://localhost:4000',
timeout: 1000,
headers: {
'X-Custom-Header': 'foobar',
// 'Content-Type': 'application/json',
},
// withCredentials: true, //保持跨域请求的cookie信息
});

request.interceptors.request.use(
function (config) {
const token = localStorage.getItem('token');
config.headers.Authorization = token ? `Bearer ${token}` : '';
return config;
},
function (error) {
return Promise.reject(error);
}
);

request.interceptors.response.use(
function (response) {
// 对响应数据做一些处理
return response;
},
function (error) {
// 对响应错误做处理
return Promise.reject(error);
}
);

export default request;

umijs + qiankun

· 阅读需 10 分钟
XOne
小站站长
信息

本文档是记录站长为长沙一家医疗公司做技术升级改造,采用umijs + antd + qiankun 技术栈时遇到的一些问题,以及解决这些问题的方案。

🚀概述

提示

Umi,中文发音为「乌米」,是可扩展的企业级前端应用框架。Umi 以路由为基础,同时支持配置式路由和约定式路由,保证路由的功能完备,并以此进行功能扩展。然后配以生命周期完善的插件体系,覆盖从源码到构建产物的每个生命周期,支持各种功能扩展和业务需求。 🐳

静态配置在config/config.ts,运行时配置在src/app.tsx。config.ts开启对应的插件才会注入相应的依赖。如:

// 开启qiankun依赖
{
qiankun: {
master: {},
}
};

运行时接入qiankun配置

/**
* 乾坤配置 加载微应用数据
* @returns
*/
export function qiankun() {
const config: QiankunConfigType & { master: { prefetch: boolean } } = {
master: {
apps: [],
prefetch: false,//这里实际是有用的
},
}

fetchAppData().then((res) => {
if (res.data) {
const microData = res.data;
microData.map((item: AppAPI.AppItemType) => {
config.master.apps.push({
name: item.number,
entry: item.url || '',
})
})
}
})

return config;
}

父子应用通信,如果父子应用采用的都是umijs,是可以按官网的做法,比较简单的进行数据交互和响应式处理。然此次微应用是用vue写的,也考虑到后续接入的应用会有各种架构,所以采用的方案是更加通用的做法,并且由于业务的要求,进行了一些特殊处理

/**
* const { micro,kp } = useModel('@@qiankunStateForSlave');
* 父子应用通信 该函数的返回值将传递给子应用
* @returns
*/
export function useQiankunStateForSlave() {
//API函数
const [api, setApi] = useState({
...microUtil
})

//响应式API
const [respApi, setRespApi] = useState({})

//响应式数据
const [state, doSetState]: UseStateType<MicroGlobalStateType> = useState({
callback: {}
})

//回调函数
kpSetCallback({ useEffect, state });

// 实际给子应用调用修改state的方法
// 传参和实现可以自定义,子应用直接调用setState是不生效的,所以才需要这个额外的方法,这是一个坑
const setState = (state: any) => {
doSetState(state)
}

return {
micro: { api, respApi, state, setState },
kp: { setApi, setRespApi, doSetState }
}
}

在加载子应用的方案时,采用umijs提供的组件<MicroApp/>,相较于<MicroAppWithMemoHistory/> ,不需要指定url,这个和后续的一个坑有关。
代码过多,这里只贴出关键代码

const element = React.useMemo(() => {
let cachedElement = getCachedElement(url);

if (cachedElement) {
return cachedElement;
}

cachedElement = <MicroApp
settings={settings}
wrapperClassName={cns('microContainer')}
name={name}
url={url}
autoCaptureError
autoSetLoading
loader={loaderHadler}
errorBoundary={ErrorComponent}
{...microData}
loadingState
base={base}
pageContext={pageContext}
/>
saveElement(url, cachedElement);

return cachedElement;
}, [name, url, microData.data.userLayout.component]);

🚀开始踩坑

坑1 子应用找不到

子应用数据举例如下

const microList = [
{
name: 'app1',
entry: '//localhost:5556',
},
];

情况一:不要开启代理,找问题能找疯掉。跟踪到底层,你会发现load的地址是代理前的地址。跨域问题组件本身已处理,不需要我们考虑,虽然前端可能报的是跨域的错,但实际是加载地址错误。
情况二:Application died in status LOADING_SOURCE_CODE: You need to export the functional lifecycles in xxx entry

/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
console.log('react app bootstraped');
}

/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
ReactDOM.render(<App />, props.container ? props.container.querySelector('#root') : document.getElementById('root'));
}

/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount(props) {
ReactDOM.unmountComponentAtNode(
props.container ? props.container.querySelector('#root') : document.getElementById('root'),
);
}

/**
* 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
*/
export async function update(props) {
console.log('update props', props);
}

找不到这几个函数,原因可能是子应用的webpack配置问题

 output: {
library: `${name}-[name]`,
libraryTarget: 'umd', // 把微应用打包成 umd 库格式
jsonpFunction: `webpackJsonp_${name}`,
// chunkLoadingGlobal: `webpackJsonp_${name}`,
// globalObject: 'window',
}

其他原因可到官网看看:qiankun常见问题

坑2 父子应用通信,数据要区分局部的/全局的,全局的都是响应式的数据,响应式的数据要考虑如何提供对应的回调处理,或者说怎么通知子应用数据变更

//qiankun  fun state
const { kp } = useModel('@@qiankunStateForSlave');
//set函数
const { doSetState, setRespApi } = kp;

//简单数据
const microData = kpGetMicroData(initialState!, name, component);
//响应式函数
const respApi = kpGetMicroRespApi(setInitialState);

useEffect(() => {
//追加响应式数据
kpSetMicroState({ doSetState, initialState, antdToken });

//追加响应式函数
setRespApi((pre: any) => ({
...pre,
...respApi,
}))
}, [initialState!.currentUser!, antdToken])

子应用中我采用了callback方式

/**
* 加载
*/
export async function mount(props) {
//1.存储props为全局方便其他地方使用
await store.dispatch('micro/setMicroProps', props);

//事件
const onSettingsChange = (settings) => {
console.log('onSettingsChange settings', settings)
}

const onCurrentUserChange = (currentUser) => {
console.log('onCurrentUserChange currentUser', currentUser)
}

const onAntdTokenChange = (antdToken) => {
console.log('onAntdTokenChange antdToken', antdToken)
}

//2.设置回调函数
props.micro.setState((pre) => ({
...pre,
callback: {
onSettingsChange,
onCurrentUserChange,
onAntdTokenChange,
}
}))

//3.渲染目标页面
render(props);

//4.关闭加载动画
props.setLoading(false)
}

坑3 子应用的路由拦截导致无限循环加载

一般应用都会有路由拦截,判断用户是否登录,访问路径是否合法等。由于主应用传过来的path是属于主应用的路由规则的path,子应用的路由规则不一定和主应用一致,也不应该一致。比如我有2个应用,都有个路由叫/home,这在主应用无法区分,我的做法是每个应用前增加表示该应用的前缀,如:/app1/home
此时,子应用不该存在路由,因为主应用的路由已经可以告诉子应用该加载哪个页面了,直接load即可。

function render(props = {}) {
if (window.__POWERED_BY_QIANKUN__ && props.data.userLayout.component) {
const component = props.data.userLayout.component;
let path = component.replace('@/', './');
import(path + '/index.vue').then((module) => {
const container = module.default;
instance = createApp(MicroLayout);

instance
.use(store)
.use(Antd)
.use(Print)
.mount(props.container ? props.container.querySelector('#root') : '#root');
}).catch((error) => {
console.error('Failed to load component:', error);
});
} else {
instance = createApp(App);
instance
.use(store)
.use(router)
.use(Antd)
.use(Print)
.mount(props.container ? props.container.querySelector('#root') : '#root');
}
}

坑4 子应用使用了antd,主应用也是用antd,子应用弹窗样式丢失

一开始以为是样式冲突问题,在主应用配置了统一前缀config.ts

  antd: {
//设置class前缀 避免和微应用冲突
configProvider:{
prefixCls: 'kp-ant',
iconPrefixCls: 'kp-anticon',
}
}

事实上不是这个原因,但是这一步还是有必要的,可以避免antd组件的class冲突。
这里不得不提antd的弹窗会把元素追加到最外层的最下面,直接在qiankun包裹之外,这就是原因。解决方案是指定挂载的容器:

 <a-drawer :get-container="$el" 
:title="title"
:width="720"
:visible="visible"
:body-style="{ paddingBottom: '80px' }"
@close="onClose"
>...<a-drawer/>

不同的组件写法不同,可以看组件的官方文档

🚀最后

上述列举的都是笔者印象比较深刻的问题,且笔者在处理过程中,这些问题大多无法面向百度解决,至于其他问题一般都可在百度找到答案。
上述所有写法或解决方案,仅为笔者个人摸索出来,不代表权威的解决方案,仅供参考。