Web跨平台应用脚本按需加载方案设计
闻者落泪,见者伤心。中年男人虚荣心的驱使下我数了数大概就5个朋友留言催更吧。够了,这5个朋友的催更是我写作的动力源泉!!!不出意外的话,近两个月都没得时间更新react源码相关的文章。原本还打算今年mini-react能挣个小 200 stars,但目前只有164个,离目标还有点距离。希望屏幕前的朋友抽个空给个star啥的开心一下。好了,废话不多说,说多不是我。下面开始今天的正文。
需求背景
最近在做一个跨平台的项目,期望web项目能跑在window端(基于electron实现)、安卓端以及web端,同时调用这些平台的API。web端采用react单页应用
架构设计
平时开发中,当我们遇到这种调用不同平台API的业务场景时,我们可以很容易这样实现:
class Demo extends React.Component {
// ...
componentDidMount() {
if (isElectron){
// 调用Electron的new BrowserWindow
} else {
// web端直接跳转新路由
}
}
// ...
handleClick = () => {
if (isElectron){
// 调用Electron的Dialog
} else {
// web端直接alert
}
// ...
}
// ...
}
这里面有几个问题:
- 代码充斥着大量的环境判断和分支,提高了代码维护难度
- 代码冗余,比如web端加载了electron端的代码,造成资源浪费
对于这个问题,腾讯IMWeb团队也给了一种方案,详情可以查看这篇文章。
一句话来说,就是根据不同平台打包不同的代码,再结合tree shaking。我个人认为这种方式不够优雅,同时增加了打包的复杂度,以及实现成本很高,同时需要服务端判断不同的平台返回对应平台的脚本。
因此,我采取了一种我认为较优雅的架构
方案实现
平台相关的API我都放在bridge下面,react端直接调用bridge的方法。
目录划分:
bridge
bridge 统一暴露各平台接口,并通过动态import的方式导入,这样能够做到动态加载不同的平台代码。
下面我以一个简单的例子来演示,假设我们点击按钮的时候,需要根据平台调用不同的能力,比如electron端我需要调用new Browser创建新的窗口展示页面,而在web端我只需要跳转新的路由。
传统的写法如下:
const { BrowserWindow } = window.require("@electron/remote")
function MyApp() {
return (
<button
onClick={async () => {
if(window.require){
// electron端
console.log('electron端接口:goToNewPage2')
const win = new BrowserWindow({
width: 200,
height: 200
})
win.loadURL('http://localhost:3000/dashboard')
} else {
// web端
console.log('web端接口:goToNewPageWeb')
alert('假装页面跳转成功')
}
}}
>
弹出窗口
</button>
);
}
为了不在react代码里耦合这些跨平台的API,因此我将这个逻辑统一抽象成goToNewPage,下面是具体的代码实现
bridge/index.js
export default function bridge() {
// 根据不同的平台动态加载不同的脚本,注意尽量做到各平台暴露出来的接口命名、出入参一致
return new Promise((resolve, reject) => {
// TODO: 暂时用window.require判断electron和web端,后续需要看下是否有更可靠的判断方法
if (!window.require) {
import(/* webpackChunkName: "web" */ './web').then(res => {
console.log('动态import.web...', res)
resolve(res)
})
} else {
import(/* webpackChunkName: "electron" */ './electron').then(res => {
console.log('动态import.electron...', res)
resolve(res)
})
}
})
}
web
web端的接口统一通过web/index.js暴露出来
bridge/web/index.js
export const goToNewPage = () => {
console.log('web端接口:goToNewPageWeb')
alert('假装页面跳转成功')
}
electron
electron端的接口统一通过electron/index.js暴露出来。
bridge/electron/index.js
const { BrowserWindow } = window.require("@electron/remote")
export const goToNewPage = () => {
console.log('electron端接口:goToNewPage2')
const win = new BrowserWindow({
width: 200,
height: 200
})
win.loadURL('http://localhost:3000/dashboard')
}
APP使用
然后在Button里面直接引用即可。
App.tsx
import React from "react";
import bridge from "./bridge";
function MyApp() {
return (
<button
onClick={async () => {
const { goToNewPage } = await bridge();
if(!goToNewPage)return // 如果方法不存在则不执行
goToNewPage();
}}
>
弹出窗口
</button>
);
}
export default MyApp;
小结
总的来说就是提供一个bridge层,在这一层暴露所有的平台相关的接口,然后在react中调用,这种方案的好处:
- 平台特异性的API不直接耦合在react业务代码中
- 能够根据不同平台动态加载不同的脚本
- 维护成本低,不需要在react业务中判断平台。修改electron端的代码完全不用担心影响到web端的代码
- 最重要的是实现成本也低,不需要根据不同平台打包不同的代码
缺点:
暴露的接口要尽可能保持同步,比如如果我们需要调用electron端的能力,但在web端不需要,那么为了保证API的一致性,就需要在web端要写个noop函数。比如我们为了调用electron的能力封装了一个electron的保存文件到本地指定目录的接口writeFile,那么在bridge/electron/index.js 我们需要这么导出
const { BrowserWindow } = window.require("@electron/remote")
const fs = window.require('fs').promises
export const goToNewPage = () => {
console.log('electron端接口:goToNewPage2')
const win = new BrowserWindow({
width: 200,
height: 200
})
win.loadURL('http://localhost:3000/dashboard')
}
export const writeFile = (path, content) => {
return fs.writeFile(path, content, { encoding: 'utf-8' })
}
然后在业务中可以这么使用:
import React from "react";
import bridge from "./bridge";
function MyApp() {
return (
<button
onClick={async () => {
const { writeFile } = await bridge();
writeFile();
}}
>
弹出窗口
</button>
);
}
但是很明显,我们在web端根本没有保存文件到指定目录的能力,因此web端可以不用实现这个需求。那这样通过bridge导出的api如果在electron端跑完全没问题,如果在web端就会报错,说writeFile不存在。因为web端没有暴露这个接口
解决这个问题也很简单,第一种方案,我们可以通过在web端导出一个noop函数,比如:
export const goToNewPage = () => {
console.log('web端接口:goToNewPageWeb')
alert('假装页面跳转成功')
}
const noop = () => { }
export const writeFile = noop
但这种方式不太可靠,万一调用方用到函数返回值就GG了。第二种方案就是,我们在使用到这种跨平台API时,可以判断一下,比如:
function MyApp() {
return (
<button
onClick={async () => {
const { writeFile } = await bridge();
if(!writeFile) return
writeFile();
}}
>
弹出窗口
</button>
);
}
是不是很简单,也很舒服
转载自:https://juejin.cn/post/7174782373765054519