[TOC]

背景

目前主要的平台分类:

  1. web平台/H5平台
  2. App平台(Android、IOS)
  3. 小程序平台(微信、QQ、支付宝、百度、头条)
  4. 桌面应用平台(Windows、Mac、Linux)

目前主要的跨平台开发技术:

  1. uni-app(基于vue,跨H5平台、App平台、小程序平台、web平台)
  2. Taro(支持react及vue,跨H5平台、App平台、小程序平台)
  3. electron(跨桌面应用平台)
  4. Flutter(跨App平台)
  5. React Native(跨App平台)

如何实现全平台开发?

经过一段时间思考后,萌生了一个想法,能否用uni-app与electron实现全平台开发?

uni-app本身跨端技术已经成熟,缺失的是跨桌面应用,而electron主打跨桌面应用技术,两者相结合做出类似桌面版H5或者叫桌面版小程序应该不成问题。

实现过程

一、搭建uni-app脚手架

uni-app项目有两种创建方式,HBuilderX和vue-cli命令行,HBuilderX方式不够灵活,因此决定使用vue-cli命令行方式搭建脚手架。参考:通过vue-cli命令行创建项目

开始前请确保电脑上已经安装了node和vue-cli。

  1. 创建uni-app

    vue create -p dcloudio/uni-preset-vue 项目名称
    

    1.1.png

  2. 选择项目模板
    我们只需要一个空项目,所以选择默认模板即可,然后等待模板下载完成。
    1.2.png
    1.3.png

  3. 进入项目目录,安装样式预处理器,uni-app项目推荐安装scss

    npm install -D node-sass sass-loader
    

    1.4.png

  4. 安装uView,uView是uni-app生态中优秀的UI框架

    1. 使用npm形式安装(也可以选择下载后放入项目)参考:npm安装方式配置

      npm install uview-ui
      
    2. 引入uView主JS库
      在项目根目录中的main.js中,引入并使用uView的JS库,注意这两行要放在import Vue之后

      // main.js
      import uView from "uview-ui";
      Vue.use(uView);
      
    3. 引入uView的全局SCSS主题文件
      在项目根目录的uni.scss中引入此文件

      /* uni.scss */
      @import 'uview-ui/theme.scss';
      
    4. 引入uView基础样式
      在App.vue中首行的位置引入,注意给style标签加入lang=”scss”属性

      <style lang="scss">
      /* 注意要写在第一行,同时给style标签加入lang="scss"属性 */
      @import "uview-ui/index.scss";
      </style>
      
    5. 配置easycom组件模式(easycom可以在页面直接使用组件,无需通过import引入组件)
      此配置需要在项目根目录的pages.json中配置,参考:easycom
      ```javascript // pages.json { “easycom”: { “^u-(.*)”: “uview-ui/components/u-$1/u-$1.vue” },

    // 此为本身已有的内容 “pages”: [

    // ......
    

    ] } ```

  1. 到此,脚手架的基础配置就完成了,运行
    1.5.png
    npm run serve
    

此脚手架仅用于示例,一个完整的脚手架至少还应该包括eslint、请求库、运行环境配置、vue.config配置、工具库等

二、配置Electron环境

在基于vue的项目中我们可以使用vue-cli-plugin-electron-builder插件很方便地配置electron环境,插件内集成了调试和打包,只需简单配置,就可以在各个桌面平台打包。(装包时需要科学上网,否则调试插件会安装不了,影响调试)

  1. 安装vue-cli-plugin-electron-builder
    vue-cli-plugin-electron-builder是vue-cli插件,我们需要使用vue-cli安装。
    在vue-cli3及以后版本中,我们可以启动图形化界面安装插件:
    启动图形化界面后选择对应项目,点击插件-添加插件
    1.6.png
    在搜索框中输入vue-cli-plugin-electron-builder并搜索,找到对应插件,点击安装
    1.7.png
    安装成功后,会跳转到配置插件,让我们选择electron版本,高版本容易出错,所以我们可以选择11.0.0版本,然后点击完成安装
    1.8.png
    安装完成后我们可以在项目中看到在package.json中添加了相关配置,包括启动脚本,入口文件,依赖包等
    1.9.png
    1.10.png
    1.11.png

    vue ui
    
  2. 运行项目
    看到下面的画面,就说明安装成功了
    1.12.png

    npm run electron:serve
    

    需要注意的是,第一次运行时会安装Vue Devtools,如果出现Vue Devtools failed to install :Error:net::ERR_CONNECTION_TIMED_OUT错误,可以忽略,在多次尝试仍然失败后,项目可以正常启动,但是,在控制台中不会显示Vue Devtools,要解决这个问题,建议科学上网。

  3. 适配uni-app
    我们可以在background.js中看到,electron默认启动的窗口大小为800x600,我们修改为375x667即可
    1.13.png
    1.14.png

    三、OCR识别

脚手架已经搭建完成,需要实战项目检验,本文以实现OCR识别功能为例,验证想法,体现跨平台魅力

要实现OCR功能,需要有相关接口,常用的有百度OCR,腾讯OCR,不过免费版本都有数量限制,所以我找了个相关平台免费的接口来实现

  1. src/pages/index下新建upload.vue组件,并写入以下代码 ```javascript

> u-upload是跨平台组件库uview中的上传组件

2.  在`src/pages/index`下的index.vue文件中写入以下代码<br />使用electron运行项目可以看到<br />![1.15.png](https://cdn.nlark.com/yuque/0/2021/png/1847559/1628732121840-e3cd1837-a80a-4912-99e6-a36a352933d3.png#clientId=ue3074002-eab5-4&from=drop&id=u5ce00de5&margin=%5Bobject%20Object%5D&name=1.15.png&originHeight=248&originWidth=398&originalType=binary&ratio=1&size=10954&status=done&style=none&taskId=ud5076854-fdd0-45c0-966c-ecc32206f36)<br />选择一张图片,OCR识别结果如下<br />![1.17.png](https://cdn.nlark.com/yuque/0/2021/png/1847559/1628732137070-3a49279e-510e-4ba0-be78-63c2fb2edbc4.png#clientId=ue3074002-eab5-4&from=drop&id=u2db19b6e&margin=%5Bobject%20Object%5D&name=1.17.png&originHeight=119&originWidth=840&originalType=binary&ratio=1&size=25324&status=done&style=none&taskId=u0a16017e-d441-4bb8-ad2a-b301dc0e91f)<br />![1.16.png](https://cdn.nlark.com/yuque/0/2021/png/1847559/1628732143563-2dd61897-02f9-44a2-af3f-bb91fd06ae38.png#clientId=ue3074002-eab5-4&from=drop&id=u9b6c7a11&margin=%5Bobject%20Object%5D&name=1.16.png&originHeight=392&originWidth=397&originalType=binary&ratio=1&size=63136&status=done&style=none&taskId=u72ca7006-83b7-471f-8572-bae0c0424a8)<br />点击复制会把识别结果复制到剪贴板 
```javascript
<template>
  <view class="ocr">
    <upload :dis_type="dis_type" @success="uploadSuccess" @remove="uploadRemove" />
    <view class="textlist">
      <view class="textlist_item" v-for="(item, index) in resultList" :key="index">
        <u-badge type="error" :absolute="true" :offset="[10, 10]" :overflow-count="999" :count="index + 1"></u-badge>
        <view v-for="(it, ix) in item.words_result" :key="ix">{{ it.words }}</view>
        <u-button class="copy" type="primary" shape="circle" size="mini" @click="copyText(item)">复制</u-button>
      </view>
    </view>
  </view>
</template>

<script>
import upload from './upload.vue';
export default {
  components: { upload },
  data: () => ({
    dis_type: 1,
    resultList: [],
    copyValue: ''
  }),
  computed: {},
  watch: {},
  methods: {
    uploadSuccess(data) {
      this.resultList.push(data.data);
    },
    uploadRemove(index) {
      this.resultList.splice(index, 1);
    },
    copyText(item) {
      this.copyValue = '';
      item.words_result.forEach(v => {
        this.copyValue += `${v.words} `;
      });
      // #ifdef H5
        const oInput = document.createElement('input');
        oInput.value = this.copyValue;
        document.body.appendChild(oInput);
        oInput.select(); // 选择对象
        document.execCommand('Copy'); // 执行浏览器复制命令
        oInput.className = 'oInput';
        oInput.style.display = 'none';
        uni.showToast({
          title: '复制成功',
          duration: 2000,
          mask: true
        });
      // #endif
      // #ifndef H5
        uni.setClipboardData({
          data: this.copyValue,
          success: () => {
            uni.showToast({
              title: '复制成功',
              duration: 2000,
              mask: true
            });
          }
        });
      // #endif
    }
  },
  // 页面周期函数--监听页面加载
  onLoad(options) {},
  // 页面处理函数--监听用户下拉动作
  onPullDownRefresh() {
    uni.stopPullDownRefresh();
  }
};
</script>

<style lang="scss" scoped>
.ocr {
  padding-top: 30rpx;

  .textlist {
    padding: 10rpx;

    &_item {
      position: relative;
      border: 1rpx solid #eee;
      padding: 20rpx;
      margin-bottom: 30rpx;
      .copy {
        margin-top: 20rpx;
      }
    }
  }
}
</style>
  1. 跨域问题
    如果你按照上面的步骤成功运行了项目,你会发现并没有这么顺利,请求会报错,不会返回识别结果,这是因为接口跨域了,在小程序平台、APP平台不存在这个问题,web平台、H5平台如何解决跨域问题不在赘述,在electron中,我们可以在background.js中加入webSecurity: false解决
    // background.js
    async function createWindow() {
    // Create the browser window.
    const win = new BrowserWindow({
    width: 375,
    height: 667,
    webPreferences: {
      // Use pluginOptions.nodeIntegration, leave this alone
      // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-    integration for more info
      nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
      contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION,
      webSecurity: false // 解决跨域问题
    }
    })
    ...
    }
    

四、全平台运行

  1. 桌面应用平台

    npm run electron:serve
    
  2. web平台/H5平台

    npm run serve 
    or
    npm run dev:h5
    
  3. 小程序平台 ```bash // 微信小程序 npm run dev:mp-weixin

// 支付宝小程序 npm run dev:mp-alipay

// 头条小程序 npm run dev:mp-toutiao


4.  App平台<br />App平台运行相对复杂,需要借助HBuilderX运行到真机或虚拟机上,在HBuilderX中打开项目,在菜单栏选择运行-运行到手机或模拟器,选择对应设备运行<br />![1.18.png](https://cdn.nlark.com/yuque/0/2021/png/1847559/1628732272604-d31ef91a-3c68-4e90-9265-ea094eebad8f.png#clientId=ue3074002-eab5-4&from=drop&id=uc1e5713b&margin=%5Bobject%20Object%5D&name=1.18.png&originHeight=387&originWidth=876&originalType=binary&ratio=1&size=149551&status=done&style=none&taskId=ub3f6cc34-1534-4ab8-afca-219b055d764)<br />安卓模拟器效果<br />![1.20.png](https://cdn.nlark.com/yuque/0/2021/png/1847559/1628732280169-765c0b34-67f3-4895-aa24-5c753eca515a.png#clientId=ue3074002-eab5-4&from=drop&id=u17b7a750&margin=%5Bobject%20Object%5D&name=1.20.png&originHeight=692&originWidth=962&originalType=binary&ratio=1&size=40277&status=done&style=none&taskId=u17a20df5-9f25-49fd-8af8-1deada67f28)  
> 在小程序平台和App平台上传图片可以选择相册或者拍照

5.  全平台集成运行<br />其实所有平台都可以在HBuilderX中运行,在HBuilderX运行菜单中,可以运行到浏览器、各家小程序、手机和模拟器,运行到终端菜单就是执行npm命令,特别的是HBuilderX内置浏览器不存在跨域问题,更方便开发<br />![1.19.png](https://cdn.nlark.com/yuque/0/2021/png/1847559/1628732300924-df168e78-dfc2-4da3-a429-9a14b9bf81ae.png#clientId=ue3074002-eab5-4&from=drop&id=u270d2b26&margin=%5Bobject%20Object%5D&name=1.19.png&originHeight=385&originWidth=891&originalType=binary&ratio=1&size=180652&status=done&style=none&taskId=u93fa20c6-177a-4a39-88f4-2648c3aea84) 
<a name="fdfad493"></a>
#### 五、全平台打包

1.  桌面应用平台
```bash
npm run electron:build
  1. web平台/H5平台

    npm run build
    or
    npm run build:h5
    
  2. 小程序平台 ```bash // 微信小程序 npm run build:mp-weixin

// 支付宝小程序 npm run build:mp-alipay

// 头条小程序 npm run build:mp-toutiao ```

  1. App平台
    App平台打包同样也相对复杂,需要借助HBuilderX打包成App应用,在HBuilderX中打开项目,在菜单栏选择发行-原生App-云打包,在打开的弹窗中选择需要的平台,并完成相关配置,然后点击打包按钮,项目打包配置检查通过后,会在云端打包,打包完成后,在HBuilderX的终端中可以看到应用下载位置
    1.21.png
    1.22.png
    1.23.png
  2. 全平台集成打包
    同全平台集成运行一样,在HBuilderX运行菜单中,运行到终端菜单就是执行npm命令,选择对应打包命令即可打包(App平台除外)
    1.24.png

    六、效果展示

  3. 桌面应用平台 | Windows (win 10) | Mac | Linux (unbutu) | | —- | —- | —- | | win.png | mac.png | linux.png |

  4. web平台/H5平台
    h5.png

  5. 小程序平台
    | 微信小程序 | QQ小程序 | 头条小程序 | | —- | —- | —- | | weixin.png | qq.png | toutiao.png |

  6. App平台
    | Android(android 10) | IOS(ios 12) | | —- | —- | | android.jpg | iphone.png |

常见问题

  1. 如何跨端兼容?使用无法跨平台的API?
    uni-app中可以使用条件编译,将代码编译到不同平台。参考:条件编译
  2. 按照步骤创建项目,安装完插件后不能正常启动?
    查看自己的node版本,node版本不能太高,尽可能小于14.x
  3. HBuilderX运行项目报错?
    请根据报错信息在官方论坛查找解决方案。论坛
  4. 解决跨域问题?
    跨域限制是浏览器行为,web平台/H5平台有此问题,示例使用的是HBuilderX中内置的浏览器,不存在跨域问题,常见的跨域解决方案参考:跨域问题解决方案