vue 项目写多了,觉得不能一成不变,想去外面的世界看看。所以尝试了一把react开发,嗯~ o( ̄▽ ̄)o 就在想做一个webApp吧,脚手架也自己搭一个吧。然后脚手架搭建完,项目可以正式开始了,自己又出幺蛾子,为什么不能打包成App呢,之前接触过cordova平台打包App,这次决定用HBuilder h5+api 开发一个同时打包多页面App应用 和 SPA单页面应用。(小程序,哎 野心太大,但是实力不允许),在抹平平台差异后,可以愉快的写代码了,但是在我看了uni-app文档后,觉得自己写的好原始。哎。虽然写的喽了点,但是对HBuilder h5+api 有了一定的了解,在看uni-app 文档时可以在脑海里模拟它接口,功能的实现了。还有他封装的功能为了实现多平台对h5+api的简化。
由于是同时开发多页面应用和单页面应用,所以我再开发之前考虑到了如下问题:
- h5+api App多页面应用 和 react 单页面应用跳转问题,以及跳转页面所需的参数。
- h5+api App 支持离线应用,在离线状态如何获取上次有网的数据,以及离线提交。react webApp 不支持离线。
- 如何在不同的终端打包不同的代码(例如:在APP端打包扫码功能模块,在h5端不打包此模块代码),以及在react jsx中根据不同的平台,渲染不同的代码
开始解决上面提到的问题
- 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+api (sqlite本地数据库实现此功能 )。原理是在初始化的时候创建一个表,第一次请求的时候将请求接口和数据插入表中,以后的每次请求都是跟新表中当前接口的数据。
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');
}
}
}
})
},
复制代码
写的废话有点多。