一次失败的尝试,h5+Api 结合 react,webpack,同时生成android 、ios、h5端代码

1,071 阅读5分钟

vue 项目写多了,觉得不能一成不变,想去外面的世界看看。所以尝试了一把react开发,嗯~ o( ̄▽ ̄)o 就在想做一个webApp吧,脚手架也自己搭一个吧。然后脚手架搭建完,项目可以正式开始了,自己又出幺蛾子,为什么不能打包成App呢,之前接触过cordova平台打包App,这次决定用HBuilder h5+api 开发一个同时打包多页面App应用 和 SPA单页面应用。(小程序,哎 野心太大,但是实力不允许),在抹平平台差异后,可以愉快的写代码了,但是在我看了uni-app文档后,觉得自己写的好原始。哎。虽然写的喽了点,但是对HBuilder h5+api 有了一定的了解,在看uni-app 文档时可以在脑海里模拟它接口,功能的实现了。还有他封装的功能为了实现多平台对h5+api的简化。

项目地址:github.com/wangyaxinon…

由于是同时开发多页面应用和单页面应用,所以我再开发之前考虑到了如下问题:

  1. h5+api App多页面应用 和 react 单页面应用跳转问题,以及跳转页面所需的参数。
  2. h5+api App 支持离线应用,在离线状态如何获取上次有网的数据,以及离线提交。react webApp 不支持离线。
  3. 如何在不同的终端打包不同的代码(例如:在APP端打包扫码功能模块,在h5端不打包此模块代码),以及在react jsx中根据不同的平台,渲染不同的代码

开始解决上面提到的问题

  1. APP端页面跳转是通h5+api过创建一个Webview (plus.webview.create)窗口,并且设置创建的窗口显示 (plus.webview.show),在显示后的回调中设置上一个webview隐藏。但是在react 单页面应用中是使用react-router 。所以我封装了一个适配模式,在ios、android 平台打包app页面切换代码,在h5平台打包 react-router跳转页面封装的代码。 (process.env.platform 后面再说)
   if(process.env.platform==='ios' || process.env.platform==='Android'){
       var router = require(`./app.js`)
   }else{
       var router = require(`./web.js`)
   }
   router.default && (router = router.default)
   export default router;

app端跳转代码

import allRouter from "@/utils/route.js"
import utils from "@/utils/init.js"
var _openw;

export function push({path,titleViewOptions,AnimationType}){
    if(_openw || !path){return;}  // 防止快速点击
    if(path==="/login"){
        if(isLogin()){
            return;
        } 
    }
    if(path==='/' ||path==='/index' ){
        path = `index.html`
    }else if(path[0]==='/'){
        var pathArr = path.split('/');
        var newpath = pathArr[pathArr.length-1]
        path = `/pages/${newpath}.html`
    }
    
    utils.changePage(path)
    .then(()=>{
        _openw=null;
    })
   
}
export function go(num){
    utils.go()
}
function isLogin() {
    var userDetail = utils.getItem("userDetail");
    if(userDetail &&userDetail.token){
        return true;
    }else{
        return false;
    }
}


web端跳转代码

import { createHashHistory } from 'history'
var history = createHashHistory();
var push = function (data){
    console.log(arguments);
    return history.push(data.path)
}
var go = (num)=>{
    return history.go(num)
}
export {
    push,
    go
}  

2.h5+api App 支持离线应用,在离线状态如何获取上次有网的数据,以及离线提交。react webApp 不支持离线。解决方案: APP端离线的一些静态资源如 html css js img font 都是打包在应用内的可以直接离线访问,但是比如一个商品列表的数据是从后台请求过来的。在离线的情况下是肯定拿不到数据的。但是我们可以借助h5+apisqlite本地数据库实现此功能 )。原理是在初始化的时候创建一个表,第一次请求的时候将请求接口和数据插入表中,以后的每次请求都是跟新表中当前接口的数据。

var qs = require('qs')
import config  from "./config.js"
import SQLite from "@/platform/storage/app.js"

var types = {};
types['0'] = "未知";
types['1'] = "未连接网络";
types['2'] = "有线网络";
types['3'] = "WiFi网络";
types['4'] = "2G蜂窝网络";
types['5'] = "3G蜂窝网络";
types['6'] = "4G蜂窝网络";
  
function get(options){
    if(!options.url){
        return 
    }
    if(!options.type){
        options.type = 'get';
    }
    
    if(Object.prototype.toString.call(options.data)!=="[object String]"){
        options.data = qs.stringify(options.data)
    }
    
    return new Promise((resolve,reject)=>{
        var xhr = new plus.net.XMLHttpRequest();
        xhr.onreadystatechange = function () {
            if(xhr.readyState==4){
                if ( xhr.status == 200 ) {
                    resolve(xhr.responseText );
                } else {
                    reject(xhr.readyState );
                }
            }
        }
        

        xhr.open( options.type,  `${options.url}?${options.data}` );
        xhr.send();
    })
    
}
function post(options){
    if(!options.url){
        return 
    }
    if(Object.prototype.toString.call(options.data)!=="[object String]"){
        options.data = JSON.stringify(options.data)
    }
    return new Promise((resolve,reject)=>{
        var xhr = new plus.net.XMLHttpRequest();
        xhr.onreadystatechange = function () {
            if(xhr.readyState==4){
                if ( xhr.status == 200 ) {
                    resolve(xhr.responseText );
                } else {
                    reject(xhr.readyState );
                }
            }
        }
        xhr.open( options.type, `${options.url}?${options.data}` );
        xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
        xhr.send();
    })
}

export default function (options){
    options.url = config.baseUrl+options.url;
    var CurrentType = types[plus.networkinfo.getCurrentType()];
    options.cache = options.cache || true;
    //无网络时或者cache,读取数据库中上一次请求成功的数据
    if(CurrentType==='未知' || CurrentType==='未连接网络' && options.cache){
        return SQLite.selectSQL(`select * from database WHERE key = '${options.url}'`)
        .then((data)=>{
            var nowData;
            if(data && data.length){

                nowData = data[0].data;
            }
            try{
                nowData = JSON.parse(nowData)
            }catch{
            }
            return nowData || {};
        })
    }else{
        if(options.type==='get' || !options.type){
            return Promise.race([
                get(options),
                new Promise((resolve, reject) => {
                    setTimeout(() => reject('request timeout'), config.timeout ? config.timeout : 30 * 1000);
                })
            ]).then((res)=>{
                try {
                    res = JSON.parse(res);
                }
                catch(err) {
                }
                setsqLite(`UPDATE database SET data = '${JSON.stringify(res)}', date = '${new Date()/1} WHERE key = '${options.url}'`)
                return res;
            })
        }else{
            return Promise.race([
                post(options),
                new Promise((resolve, reject)=>{
                    setTimeout(() => reject('request timeout'), config.timeout ? config.timeout : 30 * 1000);
                })
            ]).then((res)=>{
                try {
                    res = JSON.parse(res);
                }
                catch(err) {
                }
                
                setsqLite({
                    res,
                    options
                })
                return res;
            })
        }
    }
    function setsqLite({options,res}) {
        SQLite.selectSQL(`select key from database WHERE key = '${options.url}'`)
        .then((data)=>{
            if(data && data.length){
                //跟新表中数据
                SQLite.executeSql(`UPDATE database SET data = '${JSON.stringify(res)}', time = '${new Date()/1}' WHERE key = '${options.url}'`)
            }else{
                //第一次请求数据
                SQLite.executeSql(`insert into database values('${options.url}','${JSON.stringify(res)}','${new Date()/1}')`)
            }
        })
       

    }
}

3.区分不同的平台,我是借助webpack 实现的,在package.json scripts 传入一个参数 --ios --wx --android --web,同时在根目录下的 config/webpack.config.base.js 文件中获取这些参数,在webpack.DefinePlugink中设置全局变量

"scripts": {
    "build-ios": "cross-env NODE_ENV=production webpack --ios --config configMulti/webpack.config.js",
    "build-Android": "cross-env NODE_ENV=production webpack --Android  --config configMulti/webpack.config.js",
    "build-web": "cross-env NODE_ENV=production webpack --web  --config configSPA/webpack.config.js",
    "build-wx": "cross-env NODE_ENV=production webpack --wx  --config configSPA/webpack.config.js",
    "dev-web": "cross-env NODE_ENV=development webpack-dev-server  --web --inline --host 0.0.0.0 --config configSPA/webpack.config.dev.js",
    "dev-ios": "cross-env NODE_ENV=development webpack --w  --ios --inline --host 0.0.0.0 --config configMulti/webpack.config.dev.js",
    "dev-Android": "cross-env NODE_ENV=development webpack-dev-server  --Android --inline --host 0.0.0.0 --config configMulti/webpack.config.dev.js",
    "dev-wx": "cross-env NODE_ENV=development webpack-dev-server  --wx --inline --host 0.0.0.0 --config configSPA/webpack.config.dev.js"
  },
//读取命令行传入的参数
var parms = process.argv;
var DefinePlugin = null
if(parms.includes('--ios')){
    DefinePlugin = {
        'process.env': {
            platform: '"ios"'
        }
    }
}
if(parms.includes('--Android')){
    DefinePlugin = {
        'process.env': {
            platform: '"Android"'
        }
    }
}
if(parms.includes('--wx')){
    DefinePlugin = {
        'process.env': {
            platform: '"wx"'
        }
    }
}
if(parms.includes('--web')){
    DefinePlugin = {
        'process.env': {
            platform: '"web"'
        }
    }
} 
// DefinePlugin.NODE_ENV = '"development"'
config.plugins.push(
    new webpack.DefinePlugin(DefinePlugin),
)
module.exports = config

项目的核心来了:

单页面还好,多页面视图的切换,底部的导航,顶部的titleNView 子视图的创建 这些调用的都是原生功能。所以我做了一个配置,不必每次都该源代码,按规则修改配置视图也跟着去变化。这些配置在初始化的时候去创建。

比如初始化页面

initSubPages() {
        if(routeConfig){
            for(var key in routeConfig){
                var children = routeConfig[key].children;
                var parentConfig = routeConfig[key];
                if(children && children.length){
                    //默认打开的第一个首页
                    if(key==='index'){
                        var self = plus.webview.currentWebview();
                        var titleNView = self.getTitleNView();
                        console.log('titleNView')
                        console.log(JSON.stringify(titleNView))
                        children.forEach((item,idx)=>{
                            var page = item.MultiPath;
                            var meta = item.meta || {};
                            if(!plus.webview.getWebviewById(page)){
                               
                                // 初始化第一个子页面
                                if(idx ==0 ){
                                    utils.setStatusBar(item);
                                    var sub = plus.webview.create( page, page, item.WebviewStyles,meta);
                                    // append到当前父webview
                                    self.append(sub);
                                    //添加第一个子页面进入栈
                                    utils.setItem('pagesList',[page])
                                }
                            }
                        })
                    }else{
                        //其他在需要显示的时候创建
                        // var parentPage = routeConfig[key].MultiPath;
                        // var parent = plus.webview.create( parentPage, parentPage);
                        // children.forEach((item)=>{
                        //     var page = item.MultiPath;
                        //     var meta = item.meta
                        //     if(!plus.webview.getWebviewById(page)){
                        //         var sub = plus.webview.create( page, page, utils.subPageStyle,meta);
                        //         // append到父webview
                        //         parent.append(sub);
                        //         // 初始化隐藏
                        //         sub.hide();
                               
                        //     }
                        // })
                    }
                }else{
                    //其他在需要显示的时候创建
                    // var parentPage = routeConfig[key].MultiPath;
                    // var parent = plus.webview.create( parentPage, parentPage);
                    // parent.hide();
                }
            }
        }
        
    },

初始化所有路由页面配置的底部按钮

//递归路由配置,创建原生底部导航
initAllTabBar() {
        if(routeConfig){
            drawAllNative(routeConfig);
        }
        function drawAllNative(routeConfig) {
            if(Object.prototype.toString.call(routeConfig)==="[object Object]"){
                for(var key in routeConfig){
                    var View = routeConfig[key].View;
                    if(View && View.length){
                        View.forEach((item,idx)=>{
                            var nowView = new plus.nativeObj.View(item.id, item.styles, item.tags);
                            var parentWebview = plus.webview.getWebviewById(routeConfig[key].MultiPath==='/index.html'?utils.indexId:routeConfig[key].MultiPath);
                            if(parentWebview){
                                parentWebview.append(nowView)
                            }else{
                                //未创建页面在切换时加载View
                            }
                        })
                    }
                    var children = routeConfig[key].children;
                    if(children && children.length){
                        drawAllNative(children);
                    }
                } 
            }else if(Object.prototype.toString.call(routeConfig)==="[object Array]"){
                routeConfig.forEach((item,idx)=>{
                    var View = item.View;
                    if(View && View.length){
                        View.forEach((item,idx)=>{
                            var nowView = new plus.nativeObj.View(item.id, item.styles, item.tags);
                            var parentWebview = plus.webview.getWebviewById(item.MultiPath);
                            if(parentWebview){
                                parentWebview.append(nowView)
                            }else{
                                //未创建页面在切换时加载View
                            }
                        })
                    }
                    var children = item.children;
                    if(children && children.length){
                        drawAllNative(children);
                    }
                })
            }    
        }
    },

h5+api切换页面

//切换页面
    changePage(targetPage) {
        
        return new Promise((resolve,reject)=>{
            var pagesList = utils.getItem('pagesList')
           
            var activePage =  pagesList[pagesList.length-1];
            if(targetPage===activePage){
                return;
            }
            
            if($.isEmptyObject(utils.MuLti)){
                utils.MuLti = getMuLtiConfig(routeConfig)
            }else{

            }
            var targetPageWebview = plus.webview.getWebviewById(targetPage)
            if(targetPageWebview){
                plus.webview.show(targetPage , (utils.MuLti[targetPage].AnimationTypeShow || 'auto'), 300,()=>{
                    hidePage()
                });
                console.log('已存在');
            }else{
                // plus.webview.open(targetPage, targetPage, {}, 'slide-in-right', 200);
                var nowConfig = utils.MuLti[targetPage];
                var meta = nowConfig.meta || {};
                console.log('parentPath :   '+nowConfig.parentPath)
                if(nowConfig.parentPath){
                    var parentView = plus.webview.getWebviewById(nowConfig.parentPath=="/index.html"?utils.indexId:nowConfig.parentPath);
                    var sub = plus.webview.create( nowConfig.MultiPath, nowConfig.MultiPath, nowConfig.WebviewStyles,meta);
                    // append到当前父webview
                    parentView.append(sub);
                    addNowPageView();
                   
                    plus.webview.show(sub, (nowConfig.AnimationTypeShow || 'auto'), 300,()=>{
                        hidePage()
                    });
                }else{
                   
                    var ws = plus.webview.create( targetPage, targetPage, nowConfig.WebviewStyles ,meta);
                    addNowPageView();
                    
                    plus.webview.show(ws, (nowConfig.AnimationTypeShow || 'auto'), 300,()=>{
                        hidePage()
                    });
                }
                
                console.log('初次创建');
            }
            utils.setStatusBar(utils.MuLti[targetPage]);
            function addNowPageView(){
                var nowConfig = utils.MuLti[targetPage];
                if(nowConfig.View && nowConfig.View.length){
                    nowConfig.View.forEach((item)=>{
                        var nowView = new plus.nativeObj.View(item.id, item.styles, item.tags);
                        var parentWebview = plus.webview.getWebviewById(nowConfig.MultiPath);
                        if(parentWebview){
                            parentWebview.append(nowView)
                        }
                    })
                }
            }
            
            //隐藏当前 除了第一个父窗口
            function hidePage() {
                
                resolve('success')
                var pagesList = utils.getItem('pagesList')
                if(utils.MuLti[targetPage] && utils.MuLti[targetPage].meta && utils.MuLti[targetPage].meta.ignore){
                    // activePage = pagesList[pagesList.length-1] //activePage = 上一次打开的页面
                }else{

                }
                pagesList.push(targetPage)
                utils.setItem('pagesList',pagesList)
                activePage = pagesList[pagesList.length-2] //activePage = 上一次打开的页面
                if(activePage !== plus.webview.getLaunchWebview().id) {
                    var AnimationTypeClose = utils.MuLti[activePage] ? utils.MuLti[activePage].AnimationTypeClose :null
                    if(utils.MuLti[activePage] && utils.MuLti[activePage].meta && utils.MuLti[activePage].meta.leaveClose) {
                        plus.webview.close(activePage,AnimationTypeClose || 'auto');
                    }else{
                        plus.webview.hide(activePage,AnimationTypeClose || 'auto');
                    }
                }
            }
        })
    },

写的废话有点多。