likes
comments
collection
share

Web跨平台应用脚本按需加载方案设计

作者站长头像
站长
· 阅读数 19

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。我个人认为这种方式不够优雅,同时增加了打包的复杂度,以及实现成本很高,同时需要服务端判断不同的平台返回对应平台的脚本。

因此,我采取了一种我认为较优雅的架构

方案实现

Web跨平台应用脚本按需加载方案设计

平台相关的API我都放在bridge下面,react端直接调用bridge的方法。

目录划分:

Web跨平台应用脚本按需加载方案设计

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
评论
请登录