H5嵌入小程序适配方案

时间过去了两个多月,2024已经到来,又老了一岁。头发也掉了好多。在这两个月时间里都忙着写页面,感觉时间过去得很快。没有以前那么轻松了。也不是遇到了什么难点技术,而是接手了一个很烂得项目。能有多烂,一个页面发起六次同一个请求得存在,不得已又要重构页面。最近呢,在做webapp,h5,小程序,钉钉得适配,都是用一套代码。可以说都是h5链接得形式引入。无非显示东西不一样,这个不一样得东西,一般都是头部,跟下面得导航栏显示,功能得适配。不同手机机型得适配,也无非两种机型,安卓,IOS,线上问题得调试,还有屏幕尺寸得适配。不得不说这些真的掉头发。这篇主要是h5嵌入小程序,以及app适配方案。

说明

本文是针对是把h5应用嵌入其它平台得流程。不是单独taro发布一个小程序,单独打包成app得流程,是从一个主平台直接进入一个子系统得嵌入适配方案。所以描述得时候需要注意理解,我们是做了单点登录得。直接是使用一套 "build:h5": "taro build --type h5",进行得方案。当然不同得单独做登录得时候无法就是taro去获取当前得环境是weapp还是h5。我们得单点登录采用得方案是凭借在url里面得环境判断也是通过url方式。不是cooike跨域共享。这是因为平台很早就有的了。所以需要注意理解。不过这篇主要讲得是h5嵌入小程序

一、h5嵌入小程序适配流程

1、H5项目编译。基于自己的app项目,仍编译成h5环境的代码,就不存在启动weapp的情况了。换句话说,就是都用h5页面了。

2、照文档进行适配。根据本文第二点和第三点所述,代码层面做评估适配开发。

3、选择性使用sdk。如项目中不涉及文件类(上传下载)操作,适配工作可至本文第三点第4点即可;如涉及,请按照三.5描述进行sdk引入适配。

4、发布更新测试调试。同app,项目正常部署后,生成的h5地址就是最终通过平台小程序嵌入的h5地址,更新完成后,进行集成联调;但有一点和app不同,h5地址必须是映射过的,否则小程序集成环境无法正常访问。

二、 页面结构适配

1.导航栏:

思想上,app是需要导航栏得,小程序是自带固定得导航栏得。当是小程序时去除头部导航栏,顶部栏不能有任何东西,小程序的webview固定了导航栏,只允许修改基础配置,背景色,标题等。记得每个页面都要改,建议添加全局的判断方法.。所以这时候封装导航栏时,这时候就要考虑环境了。

2.页面内容得适配:

总体思想,采用媒体查询为基准,大得调整(有些页面在一些尺寸得屏幕需要另外得展示),页面区间采用flex布局。

因为我们使用得是taro,在 Taro 中尺寸单位建议使用 px、 百分比 %,Taro 默认会对所有

单位进行转换。在 Taro 中书写尺寸按照 1:1 的关系来进行书写,即从设计稿上量的长度 100px,那么尺寸书写就是 100px,当转成微信小程序的时候,尺寸将默认转换为 100rpx,当转成 H5 时将默认转换为以 rem 为单位的值。

3.路由切换:

尽量采用路由跳转,为什么内,如果采用tabs进行切换时候,如果时app返回操作还能进行处理,如果是微信小程序,上面返回就直接让你退出了。为此我们还进行了重构了首页页面。同时也要考虑手机物理按键得返回。所以尽量采用路由跳转方式。

4.js操作像素时:

当我们用js进行像素操作时,对此我们需要封装一个方法进行转化,特殊处理只用关心px。

js 复制代码
export const transformToRemOrRpx = function(size: number): Promise<string> {
    return new Promise((resolve, reject) => {
        Taro.getSystemInfo({
            success: res => {
                const width = res.windowWidth;
                const pixelRatio = 750 / width;
                let transSize = "0";
                if (process.env.TARO_ENV === 'weapp') {
                    transSize = size * pixelRatio + "rpx";
                } else { 
                    let baseSize = width >= 640 ? 40 : width <= 320 ? 20 : width / 320 * 20;
                    transSize = size / baseSize + "rem";
                }
                resolve(transSize);
            },
            fail: err => {
                reject(err);
            }
        })
    })
}

三、功能实现:

1. 编译

当前就不存在编译成weapp的情况了,都是h5,所以需要从跳转的地址栏上区分是小程序的h5还是app的h5,需要通过环境进行判断。以及根据这个判断来做适配。

注意:获取地址栏参数需要用原生location方式取,taro自带的api有可能会取不到,如果你想偷懒,本文也提供了获取地址栏参数的办法。

bash 复制代码
//解析url参数
export function parseUrlSearch<T>(): T {
    if (!window.location) { return {} as any }
    let href = window.location.href;
    if (href.includes('#') && href.includes('?')) {
        let queryIndex = href.indexOf('?')
        let symbolIndex = href.indexOf('#')
        let symbolList = href.split('#')
        symbolList = symbolList.filter(o => o.includes('?') ? o : false)
        let moreQuery = symbolList[1] && symbolList[1].slice(symbolList[1].indexOf('?')) || '';
        if (queryIndex < symbolIndex) {
            let hrefString = href.slice(0, href.indexOf('#'))
            href = hrefString + moreQuery.replace('?', '&')
        }
    }
    let qindex = href.indexOf("?");
    let queryParams = href.substr(qindex + 1);
    let queryParamsArr = queryParams.split("&");
    let queryParamsObj = {};
    queryParamsArr.map((v) => {
        let tmp = v.split("=");
        let value: string = tmp.length === 2 ? decodeURIComponent(tmp[1]) : '';
        if (value.includes("[") && value.includes("]")) {
            queryParamsObj[tmp[0]] = JSON.parse(decodeURIComponent(value));
        } else {
            queryParamsObj[tmp[0]] = decodeURIComponent(tmp[1])
        }
    });

    return queryParamsObj as any;
}

2. 数据获取(缓存)

小程序的h5,即webview场景下微信不允许使用缓存,需要适配;另外taro提供的存储api无法支持跨页面存储。小程序的h5所需全局数据,必须全部采用从地址栏传参方式获取,拿不到。

3. Token校验

微信嵌h5环境下,系统要求去除token校验,或者采用调组件库内app.ts中提供的token更新接口方式进行token校验更新。因为小程序的webview里头注入基础平台的js文件会有很多问题,最主要的是域名校验和资源路径问题,当前解决起来成本比较高,所以先去除。

4. taro全局路径配置(接口根路径、打包静态资源路径)

bash 复制代码
const path = require("path");
const config = {
   projectName: 'myApp',
   date: '2024-1-20',
   designWidth: 750,
   deviceRatio: {
      640: 2.34 / 2,
      750: 1,
      828: 1.81 / 2
   },
   sourceRoot: 'src',
   outputRoot: `dist/${process.env.TARO_ENV}`,//多端开发时必须使用******important*******
   plugins: [],
   defineConstants: {},
   copy: {
      patterns: [
         {
            from: '',
            to: ''
         },
      ],
      options: {}
   },
   framework: 'react',
   mini: {
      postcss: {
         pxtransform: {
            enable: true,
            config: {}
         },
         url: {
            enable: true,
            config: {
               limit: 1024 // 设定转换尺寸上限
            }
         },
         cssModules: {
            enable: true, // 默认为 false,如需使用 css modules 功能,则设为 true
            config: {
               namingPattern: 'module', // 转换模式,取值为 global/module
               generateScopedName: '[name]__[local]___[hash:base64:5]'
            }
         }
      }
   },
   h5: {
      publicPath: '/',
      staticDirectory: 'static',
      esnextModules: ['taro-ui'],//应用taroUI的时候必须设置******important*******
      postcss: {
         autoprefixer: {
            enable: true,
            config: {}
         },
         cssModules: {
            enable: true, // 默认为 false,如需使用 css modules 功能,则设为 true
            config: {
               namingPattern: 'module', // 转换模式,取值为 global/module
               generateScopedName: '[name]__[local]___[hash:base64:5]'
            }
         }
      }
   },
   alias: {
      '@/pages': path.resolve(__dirname, '..', 'src/pages'),
   }
}

module.exports = function (merge) {
   if (process.env.NODE_ENV === 'development') {
      return merge({}, config, require('./dev'))
   }
   return merge({}, config, require('./prod'))
}

5. 复杂业务(预览/选择图片等)sdk引入

由于微信环境下的h5需要使用微信提供的weixin-sdk,才能使用微信允许的功能,微信提供的接口如下图(附网址):

附网站:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html

四、常见的问题:

1. 子系统具体的页面标题怎么确定?

跳转过去默认是模块名称作为标题。如果需要自定义导航栏标题,建议使用document.title设置就行。

2. 涉及到sdk的功能如何进行调试?

真机调试。
1.调式接口,查看接口。点击小虫子就可以查看子应用得一个请求情况。


2.真机调试查看功能是否适配,采用局域网得方式直接连接本地跑的应用。

3. sdk不支持预览文件操作?

封装一个preiewFile这个函数的主要目的是在微信小程序内预览特定路径的文件。

这是它如何工作的:

1.首先,函数接受一个路径作为参数,并从该路径中提取出文件的类型。

2.如果文件类型是图片(png,jpeg,jpg),那么使用wx.previewImage预览该图片。

3.如果不是图片,那么函数会调用wx.downloadFile来下载该文件。在下载过程中,会显示一个加载提示。下载成功后,如果文件类型是文本文件(txt),那么就使用 wx.getFileSystemManager().readFile 读取文件内容,并通过模态对话框显示出来。

4.如果文件类型不是文本文件,那么就尝试使用 wx.openDocument 打开该文件。如果打开过程中出现错误,错误详细信息将被打印到控制台。

整个函数的主要功能就是为了提供一个预览文件的方案,包括图片预览,文本内容阅读以及开启文档。这样的功能在某些需要预览文件内容的场景中会非常有用。

js 复制代码
const wx = require('weixin-js-sdk')
   function previewFile(path) {
      // const path = files[index][this.data.downLoadAttr]
      const fileNameArr = name.split('.')
      const fileType = fileNameArr[fileNameArr.length-1]
      if(fileType == 'png' || fileType == 'jpeg' || fileType == 'jpg'){
         wx.previewImage({
            current: path, // 当前显示图片的http链接
            urls: [path] // 需要预览的图片http链接列表
         })
         return
      }
      let fs = wx.getFileSystemManager();
      wx.showLoading({ title : '文件加载中' });
      wx.downloadFile({
         url: path,//文件路径
',      //文件路径
         header: {},
         success: function (res) {
            wx.hideLoading();
            var filePath = res.tempFilePath;
            if(fileType == 'txt'){
               fs.readFile({
                  filePath:filePath,
                  encoding:'utf8',
                  complete(res){
                     // console.log(res.data);
                     wx.showModal({
                        title: '文件内容',
                        content: res.data,
                        showCancel: false,
                        confirmText: '确定',
                        success: (result) => {
                           if(result.confirm){

                           }
                        },
                        fail: ()=>{},
                        complete: ()=>{}
                     });
                  }
               })
            }else{
               wx.openDocument({
                  filePath: filePath,
                  showMenu : true,
                  // fileType: type,   //文件类型
                  success: function (res) {
                  },
                  fail: function (res) {
                     // wx.showToast({ title: '文件打开失败', icon : 'none' })
                     console.table(res);
                  },
                  complete: function (res) {
                  }
               })
            }
         },
         fail: function (res) {
            wx.hideLoading();
            console.log('文件下载失败:',path)
            wx.showToast({ title: '文件加载失败', icon : 'none' })
            console.table(res)
         },
         complete: function (res) {
         },

      })
   }

对于下载

简单粗暴的解决方式是:复制链接让用户自行去浏览器下载。可以借助clipboard插件。

4. sdk不支持文件选择操作?

当然,也不能使用taro提供的chooseMessageFile,因为微信的webview没开放给你用。我封装了原生input的实现封装方法chooseFIle,可以直接copy使用。

1.这函数,它用于在Web和非Web环境(如微信小程序)中选择文件。它以函数形式暴露,可以从外部调用,并传入一个对象参数,这个参数包括count(选择文件的数量),success(成功的回调函数),fail(失败的回调函数)和complete(完成的回调函数)。

2.函数首先进行参数校验,例如检查传入的 count 是否为数字。如果校验失败,它将调用失败回调函数并返回一个Rejected Promise。如果一切正常,我们会进行环境判断。

3.如果环境是Taro的WEB环境,那么将在页面上创建一个input元素,并设置它为file类型,以供用户选择文件。选择的文件信息会被添加到返回的结果对象中,然后调用成功回调和完成回调。

4.对于非Web环境,它使用Taro的 chooseMessageFile 方法来选择文件,这个函数通常被用于在微信小程序中选择聊天文件。

这个函数的主要目的是提供一个通用的文件选择方案,无论在Web环境还是非Web环境,都可以以相同的方式来选择文件。

js 复制代码
import Taro from '@tarojs/taro';
//选择文件
export const chooseFile = function (option) {
    const {count = 1, success,url, fail, complete} = option
    const res: any = {
        errMsg: 'chooseFile:ok',
        tempFilePaths: [],
        tempFiles: []
    }
    if (count && typeof count !== 'number') {
        res.errMsg = 'error,typeof count is error',
            console.error(res.errMsg)
        typeof fail === 'function' && fail(res)
        typeof complete === 'function' && complete(res)
        return Promise.reject(res)
    }

    if (Taro.getEnv() === Taro.ENV_TYPE.WEB) {
        const fileId = "MyUtilsChooseFile"
        let taroChooseImageId: any = document.getElementById(fileId)
        if (!taroChooseImageId) {
            let htmlInputElement: any = document.createElement("input");
            htmlInputElement.type = "file"
            htmlInputElement.setAttribute('id', fileId)
            htmlInputElement.setAttribute('style', 'position: fixed; top: -4000px; left: -3000px; z-index: -300;')
            if (count > 1) {
                htmlInputElement.setAttribute('multiple', 'multiple')
            }
            document.body.appendChild(htmlInputElement)
            taroChooseImageId = document.getElementById(fileId)
        } else {
            if (count > 1) {
                taroChooseImageId.setAttribute('multiple', 'multiple')
            } else {
                taroChooseImageId.removeAttribute("multiple")
            }
        }

        taroChooseImageId.onchange = (e) => {
            // console.log("onchange",e)
            let arr = [...e.target.files]
            arr = arr.splice(0, count)
            arr && arr.forEach(item => {
                let blob = new Blob([item], {
                    type: e.target.files[0].type
                });
                let url = URL.createObjectURL(blob);
                res.tempFilePaths.push(url)
                // res.tempFiles.push({path: url, size: item.size, type: item.type, originalFileObj: item})
                console.log("选择item", item)
                res.tempFiles.push({
                    path: url,
                    size: item.size,
                    type: item.type,
                    name: item.name,
                    lastModified: item.lastModified,
                    lastModifiedDate: item.lastModifiedDate,
                    webkitRelativePath: item.webkitRelativePath
                })
            })
            typeof success === "function" && success(res)
            typeof complete === "function" && complete(res)

            e.target.value = ''
            // document.body.removeChild(taroChooseImageId)
        }
        //触发选择
        taroChooseImageId.click()

    } else {
        Taro.chooseMessageFile({
            count: count,
            type: 'file',
            success: function (res) {
                // console.log("选择文件",res)
                success ? success(res) : ""
            }
        })
    }
}

使用

js 复制代码
chooseFile({
  count:2,
  success(res){
    console.log(res)
  }
})

5. sdk不支持文件上传操作?

用Taro自带的Taro.uploadFile(option)实现,示例如下:

js 复制代码
chooseFile({
  count:2,
  success(res){
    console.log(res)
    Taro.uploadFile({
      url:url
      filePath:res.tempFiles[0].path,
      name:res.tempFiles[0].name,
      formData: {},
      success (res){
        const data = res.data
        //do something
      }
    })
  } 	
})

6. sdk不支持小程序跳转?如何实现

先进行环境的判断

bash 复制代码
// 判断当前环境是否为小程序
const ua = navigator.userAgent.toLowerCase();
if (ua.match(/MicroMessenger/i) == 'micromessenger') {
    wx.miniProgram.getEnv((res) => {
        if (res.miniprogram) {
            console.log('在小程序内');
        } else {
            console.log('不在小程序内');
        }
    });
} else {
    console.log('不在微信浏览器内');
}

// 小程序跳转方法
wx.miniProgram.navigateTo({
   url:'/pages/index/index',        // 指定跳转至小程序页面的路径
   success: (res) => {
	  console.log(res);   // 页面跳转成功的回调函数
   },
   fail: (err) => {
	  console.log(err);   // 页面跳转失败的回调函数
   }
});

// 通过链接与小程序通讯传参
// 静态参数传输
wx.miniProgram.navigateTo({
   url:'/pages/index/index?id=1', // id:所需参数
   success: (res) => {
	  console.log(res);   // 页面跳转成功的回调函数
   },
   fail: (err) => {
	  console.log(err);   // 页面跳转失败的回调函数
   }
});

// 动态参数传输
let id = 1;
wx.miniProgram.navigateTo({
   url:'/pages/index/index?id=' + id, // id:所需参数(动态参数需放在引号外小程序才可识别)
   success: (res) => {
	  console.log(res);   // 页面跳转成功的回调函数
   },
   fail: (err) => {
	  console.log(err);   // 页面跳转失败的回调函数
   }
});

如果不再小程序内跳转会报错。

解决思想:
首先,webview内嵌h5,从h5页面是不可以直接使用wx.miniProgram.navigateTo 方法跳转到其他小程序,所以具体思路应该是小程序A => webview内嵌 h5 => 从h5调回到小程序A的中转页面 => 在小程序A中 使用wx.navigateToMiniProgram 方法跳转到 小程序B

1.我们采用自定义的navigateToMiniProgramForWebview函数来触发从当前小程序跳转到另一个小程序。这个函数接收两个参数,wx对象和包含跳转信息的params对象。

2.wx对象是由微信API提供的一个核心对象,带有微信小程序的接口。params对象包含了必须的跳转信息,比如目标小程序的appId,路径(path),额外的数据(extraData),以及小程序的版本.

3.函数体中的第一步是创建一个新的对象 passParams,它复制了params的所有属性,然后把extraData属性转换成 JSON 字符串,并且添加了一个新的属性:type,在这个例子中其值为:'navigateToMiniProgram'。

4.接下来,将这个passParams对象通过一个叫formatParams的函数转换成一个URL查询字符串,和路径组合成一个完整的URL。在这个例子中,这个URL指向小程序的一个特定页面:webviewMiddlePage,并带有跳转信息作为查询参数。

5.最后一步是通过 wx.miniProgram.navigateTo 接口, 使用这个URL进行导航。这将触发在当前小程序中打开webviewMiddlePage 页面,然后这个页面会解析URL中的参数,并根据跳转信息完成从当前小程序到目标小程序的跳转。

注意,这个函数中使用了两次跳转。
第一次是在当前小程序内部,跳转到 webviewMiddlePage页面。
第二次跳转是在 webviewMiddlePage 页面中,完成从当前小程序到目标小程序的跳转。通过这种方式设计,使得跳转过程可以携带更复杂的数据,同时绕过微信小程序navigateToMiniProgram 方法对传递数据量的限制。

js 复制代码
  const navigateToMiniProgramForWebview =(wx, params:any = {})=> {
    const passParams = {
      ...params,
      extraData:JSON.stringify(params.extraData || {}),
      type: 'navigateToMiniProgram'
    }
    const url = `/pages/webviewMiddlePage/index?${formatParams(passParams)}`//跳回我们小程序自定义得页面请求打开另外一个小程序
    wx?.miniProgram?.navigateTo({url})
  }
  navigateToMiniProgramForWebview(wx, {
     appId: '',//必传 跳转小程序唯一标识
     path: ``,//必传 跳转后初始页面路径
          //develop(开发版),trial(体验版),release(正式版 )
    envVersion: 'release',//必传 跳转小程序的版本
  })
    
 function formatParams(obj: any){
  let arr: string[] = []
  Object.keys(obj).forEach(key => {
    if(obj[key] && typeof obj[key] === 'string') {
      arr.push(`${key}=${encodeURIComponent(obj[key])}`)
    }
  })
  return arr.join('&')
}

如果是app或者h5直接window.open

7.IOS日期显示Invalid date

注意:iso 这样 new Date('2021-02-11') 返回的就是Invaild Date。

new Date('2021/02/11') '/'才是正常的

五、让人哭笑不得得代码

1.发起了六次请求。我挺搞不懂他是怎么写得。然后我就重构了页面。


2.人生无语,查看订单后,不能保存id嘛,又进行一次查询。然后在删除。然后我又要重构头就是怎么秃得


是不是很有意思

六、对于多端开发

小程序得每年认证,对于企业来说将会采用更多得h5页面去嵌入一个总的小程序平台,而不是采用单独得开发发布,因为如果认证几个小程序或者十几个小程序,企业觉得这样得费用不是很值得,更青睐于h5一套嵌入,webapp也是如此,钉钉也是如此,其它得也是。、taro也可以适配鸿蒙系统得开发。uniapp 是一种基于 Vue.js 的跨平台开发框架。而Taro 是一种基于 React 的多端开发框架。taro跟uniapp,taro跟react框架搭配,uniapp跟Vue搭配。两个框架我都学习使用。感受就是语法不一样,解决思路还是一样得。做多了项目后,会发现,其实框架只是实现需求得一种工具,更重要得是解决问题得思想跟经验。就像我搭档一样,他工作三年多了,他说他之前是使用vue开发得,对小程序,跟webapp也不是很熟。虽然我之前做过,但是跟他一起工作给我得一种感觉是他上手快。解决思路很清晰,只是语法切换而已,能搞快速得定位问题,并参考学习。

uniapp搭建学习

项目基于 uniapp + Vue3 + vite + pinia + ts + axios 项目模板快速开发,适用于小程序开发 h5。

项目使用了 vite 作为开发工具,支持 esm、cjs、iife 三种打包方式,支持按需加载,支持热更新。

github:https://github.com/wskang12138/aniapp-learn

声明:这个框架不是我搭建得,我也只是demo下来学习,改改就成我得了,哈哈哈哈哈哈。

七、总结

对于多端得适配,四种情况:顶部导航栏得适配,页面样式得适配,功能得适配,其它。
对于顶部导航栏得适配,这个简单,判断环境做好封装显示就好了。页面样式得适配,媒体查询为基准,flex为区间布局。功能得适配:得翻阅文档学习了。

参考:https://github.com/uileader/touchwx

相关推荐
xptwop26 分钟前
05-ES6
前端·javascript·es6
每天开心30 分钟前
噜噜旅游App(3)——打造个性化用户中心:AI生成头像与交互设计
前端·前端框架
Heo31 分钟前
调用通义千问大模型实现流式对话
前端·javascript·后端
前端小巷子1 小时前
深入 npm 模块安装机制
前端·javascript·面试
深职第一突破口喜羊羊2 小时前
记一次electron开发插件市场遇到的问题
javascript·electron
cypking2 小时前
electron中IPC 渲染进程与主进程通信方法解析
前端·javascript·electron
西陵2 小时前
Nx带来极致的前端开发体验——借助playground开发提效
前端·javascript·架构
江城开朗的豌豆3 小时前
Element UI动态组件样式修改小妙招,轻松拿捏!
前端·javascript·vue.js
float_六七3 小时前
JavaScript:现代Web开发的核心动力
开发语言·前端·javascript
zhaoyang03013 小时前
vue3笔记(2)自用
前端·javascript·笔记