yujinjin / fans

这是一个app(android/iOS)项目,但页面视图全部都用的是html5页,没有使用app的原生页面。 前端h5是基于mui + vue2 + vue-router2 + es6 + webpack2 + vuex + signalR 的前端webApp单页项目框架,项目可以直接在PC上运行html5页面。 app打包技术是用HBuilder IDE工具一键打包成APP。

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

前言

这是一个app(android/iOS)项目,但页面视图全部都用的是html5页,没有使用app的原生页面,项目可以直接在PC上运行html5页面。与服务端的交互全部都是走web api接口方式。客户端的登录是JSON WEB TOKEN 认证(JSON Web Token(JWT)是什么鬼)。项目里有android Apk打包文件,可以直接下载安装点此链接下载

前端h5是基于mui + vue2 + vue-router2 + es6 + webpack2 + vuex + signalR的前端webApp单页项目框架。

app打包技术是用HBuilder IDE工具一键打包成APP,本项目使用了原生设备的的Storage和管理条码扫描。对于app的升级是html5资源在线升级更新,而不是整个APP更新。这些都是dcloud提供一整套技术解决方案。

本项目只是一个技术框架,对于项目中具体的业务的东西只会大概的说明一下。

说明: 可能有些朋友不知道signalR是什么东西,其实signalR就是让客户端(Web页面)和服务器端可以互相通知消息及调用方法的前端JS,当WebSockets可用时(即浏览器支持Html5)signalR使用WebSockets,当不支持时signalR将使用其它技术来保证达到相同效果。

  1. 前端UI的部分使用mui框架

  2. app打包技术使用HBuilder IDE工具

  3. 原生App对设备的调用

  4. 使用vue-router2实现单页路由

  5. 使用.vue文件进行页面功能组件化的开发

  6. 使用vuex管理webApp的数据状态

  7. 使用signalR实现客户端与服务端长时间通信

  8. 使用webpack2实现对模块打包、压缩、混淆,预处理,热加载。

  9. 自己实现了一套路由机制,但它只适用于app中使用h5的header。

吐槽: 我想吐槽一下webpack2的webpack.config.js中各个插件配置,当时配置了好几天,这个loader配置好了,另外一个又出问题了,而且网上关于webapck2 API太少了,都是靠摸索着前进,真的是好难配。那有人肯定问了为啥不用VUE官方提供的vue-cli创建项目,我想说兄弟呀那个vue-cli是针对webpack1的,我想用webpack2(不要问我为什么,我就是固执的想用),而且官方配置的JS太TM的不直接了,10个配置文件我想改一些配置得看半天。

安装

  • 下载HBuilder IDE开发工具,其实HBuilder是dcloud 把eclipse的改造成一个专门应用于app打包、多种语言支持:php、jsp、ruby、python、nodejs等web语言,less、coffee等编译型语言均支持的开发工具

  • 下载node.js,作为前端web的运行环境。我当前的node.js版本是6.9.2 npm版本是3.10.9

  • app打包完全是基于manifest.json配置文件,它主要是用来配置app的基本信息(版本号、appid等)、图标(app的应用图标)、sdk配置、模块权限配置、页面引用关系、代码视图,具体参看dcloud提供的文档

npm初始化

package.json内容如下
{
	"name": "Fans",
	"version": "1.0.0",
	"description": "粉丝煲",
	"main": "js/entrance.js",
	"keywords": "粉丝煲",
	"homepage": "",
	"bugs": {
		"url": "https://github.com/yujinjin/fans/issues",
		"email": "yujinjin9@126.com"
	},
	"author": {
		"name": "jinyu",
		"email": "yujinjin9@126.com",
		"url": "https://github.com/yujinjin"
	},
	"license": "MIT",
	"repository": {
		"type": "git",
		"url": "https://github.com/yujinjin/fans.git"
	},
	"scripts": {
		"R_DEV": "set NODE_RUN=1&&webpack-dev-server --progress --watch --inline --host=0.0.0.0  --port 8083",
		"B_DEV":"set NODE_ENV=dev&set NODE_RUN=0&webpack --progress --hide-modules",
		"lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs"
	},
	"dependencies": {
		"vue": "^2.1.8",
		"vue-resource": "^1.0.3",
		"vue-router": "^2.0.1",
		"vue-html-loader": "1.2.3",
    	"vue-loader": "10.0.0",
    	"vue-style-loader": "^1.0.0",
    	"vue-template-compiler": "^2.1.0"
	},
	"devDependencies": {
		"vuex": "^2.0.0",
		"autoprefixer": "^6.4.0",
	    "babel-core": "^6.0.0",
	    "babel-eslint": "^7.0.0",
	    "babel-loader": "^6.0.0",
	    "babel-plugin-transform-runtime": "^6.0.0",
	    "babel-preset-es2015": "^6.0.0",
	    "babel-preset-stage-2": "^6.0.0",
	    "babel-register": "^6.0.0",
	    "babel-polyfill": "^6.22.0",
		"cross-env": "^1.0.6",
		"css-loader": "^0.25.0",
		"less": "^2.7.1",
		"less-loader": "^2.2.3",
		"file-loader": "^0.9.0",
		"html-loader": "^0.4.4",
		"html-webpack-plugin": "^2.24.1",
		"jshint": "^2.9.4",
		"jshint-loader": "^0.8.3",
		"style-loader": "^0.13.1",
		"url-loader": "^0.5.7",
		"extract-text-webpack-plugin": "^2.0.0-beta.4",
		"webpack": "^2.1.0-beta.25",
		"webpack-dev-server": "^2.1.0-beta.10",
		"webpack-require-http": "^0.4.0"
	},
	
	"engines": {
		"node": ">=5.0.0",
		"npm": ">=3.3.6"
	}
}

开发环境依赖模块说明
vue                            //构建用户界面的
vue-resource                   //vue 的http ajax请求插件(本项目没有用它,暂时保留)
vue-router                     //vue 路由插件
vue-html-loader                //vue html加载器
vue-loader                     //vue加载器
vue-style-loader               //vue的样式加载器
vue-template-compiler          //vue的模板编译器
vuex                           //组件状态管理
autoprefixer                   //css  浏览器兼容性问题处理
babel-core                     //ES6  代码转换器
babel-eslint                   //ES6的代码检查
babel-loader                   //ES6  代码转换器,webpack插件
babel-plugin-transform-runtime //和polyfill类似,替换助手函数
babel-preset-es2015            //ES6  代码编译成现在浏览器支持的ES5
babel-preset-stage-2           //ES6  ES7要使用的语法阶段
babel-register                 //用于改写require命令,为它加上一个钩子。此后,每当使用require加载.js、.jsx、.es和.es6后缀名的文件,就会先用Babel进行转码。
babel-polyfill                 //Babel默认只转换新的JavaScript句法(syntax),而不转换新的API,babel-polyfill就是为当前环境提供一个垫片。解决一些浏览器不能识别的语法,比如:Promise
cross-env                      //解决跨平台设置NODE_ENV的问题
css-loader                     //css  生成
less                           //css  预处理器less
less-loader                    //css  预处理器less的webpack插件
file-loader                    //webpack的文件加载器,主要用于字体  将字体文件打包
html-loader                    //webpack的html加载器,主要用于html文件的加载
html-webpack-plugin            //html  文件编译
jshint                         //Js代码检查工具
jshint-loader                  //webpack的jshint加载器,主要用于Js代码检查工具
style-loader                   //webpack的style加载器,主要用于css  插入到style标签
url-loader                     //webpack的url加载器,主要用于图片加载及限制
extract-text-webpack-plugin    //把额外的数据内容加到编译好的文件中 (独立打包样式文件)
webpack                        //用来构建打包程序
webpack-dev-server             //开发环境下,设置代理服务器
webpack-require-http           //webapck打包环境下的requrire加载http文件的插件

项目目录说明

|-- build                               // webapck打包后的文件目录
|-- logo                                // 存放app的图表地址目录
|-- src                                 // 源码目录
|   |-- components                      // 存放公共组件的目录
|       |-- member-qrcode.vue           // 会员二维码公共组件
|       |-- ...                         // 其他公共组件
|   |-- css                             // 存放各种css文件目录
|       |-- app.css                     // app的公用样式文件 
|       |-- icons-extra.css             // icons的扩展字体样式 
|       |-- mui.css                     // mui框架css
|       |-- ...                         // 其他css
|   |-- fonts                           // 存放各种fonts文件目录
|       |-- ...                         // 其他fonts文件
|   |-- imgs                            // 存放各种图片文件目录
|       |-- test                        // 存放开发测试的图片文件目录
|           |-- ...                     // 其他测试图片文件
|       |-- ...                         // 其他图片文件
|   |-- js                              // 存放各种js文件目录
|       |-- components                  // 存放各种js组件的目录
|           |-- app-routers.js          // 站点路由插件(只做路由的操作,不涉及实际的业务处理)
|           |-- signalR.js              // signalR组件
|           |-- ...                     // 其他JS组件
|       |-- config                      // 存放打包各种环境的目录
|           |-- DEV.js                  // DEV环境配置文件
|           |-- GQC.js                  // GQC环境配置文件
|           |-- PRD.js                  // PRD环境配置文件
|           |-- ...                     // 其他环境配置文件
|       |-- lib                         // 第三方JS lib目录
|           |-- mui.js                  // mui插件
|           |-- ...                     // 其他第三方JS插件
|       |-- services                    // app自己的业务目录
|           |-- global-service.js       // APP 全局业务逻辑方法,主要处理登录、登出的业务逻辑
|       |-- store                       // vuex管理webApp的数据状态目录
|           |-- index.js                // app数据管理入口文件
|           |-- app-data.js             // app临时数据管理
|           |-- app-event.js            // app事件管理
|           |-- router-status.js        // app路由状态管理
|       |-- utils                       // app的存放工具
|           |-- directives.js           // vue 自定义指令文件
|           |-- log.js                  // app log日志
|           |-- update.js               // app在线更新
|           |-- utils.js                // app站点页面表单验证框架工具类
|           |-- ....                    // 其他工具JS文件
|   		|-- app.js                      // app配置以及其他方法
|   		|-- entrance.js                 // app程序入口文件,加载各种公共组件
|   		|-- routers.js                  // vue的路由配置文件
|   |-- json                      			// 测试的json数据存放目录
|   |-- less                      			// 存放各种less文件的目录
|   		|-- app.less                    // app基础样式,包含其他less文件的入口
|   		|-- ...                         // 其他less样式文件
|   |-- views                      			// 存放各种页面视图组件目录
|   		|-- error                       // 存放错误视图组件目录
|   				|-- 404.vue                 // 404页面视图
|   		|-- users                      	// 存放用户的视图组件目录
|   				|-- login.vue               // 登录页面
|   				|-- user-center.vue         // 用户中心页面
|   				|-- welcome.vue         		// 欢迎页面
|   				|-- ...         		// 其他视图页面
|   		|-- ...                     // 其他功能模块目录
|   		|-- app.vue                     // app页面入口文件
|   		|-- barcode.vue                 // barcode页面入口文件
|   		|-- home.vue                    // app首页面
|   |-- index.html                      // app的html模板页面
|-- unpackage                           // app编译包目录
|-- .babelrc                            // ES6语法编译配置
|-- .editorconfig                       // 编辑器编码规范配置
|-- .gitignore                          // git忽略文件
|-- index.html                          // webapp的首页加载文件
|-- manifest.json                       // 打包app的配置文件
|-- package.json                        // 配置项目相关信息,通过执行 npm init 命令创建
|-- webpack.config.js                   // webpack配置文件

上图

1. app首次启动-欢迎页面

image

2. 登录页

image

3. 首页

image

3. 1. 扫码核销

image

3. 2. 会员识别

image

3. 3. 素材列表
4. 我的集客(随便凑的一个页面)

image

4. 1. 会员列表
5. 个人中心

image

5. 1. 个人资料

image

5. 2. 我的收益
5. 3. 密码修改

image

5. 4. 消息列表

image

5. 5. 消息内容

image

运行程序

项目地址:(git clone

git clone https://github.com/yujinjin/fans.git

通过npm安装本地服务第三方依赖模块(需要已安装Node.js)

npm install

启动DEV服务(http://localhost:8083)

npm run R_DEV (window)
npm run MR_DEV (MAC)

打包发布DEV代码

npm run B_DEV (window)
npm run MB_DEV (MAC)

说明一下:由于要解决移动端iOS操作系统click事件延迟300ms问题,特意使用了tap事件来替代click事件。所以运行时最好是在浏览器中的手机模拟器中操作。

实现的功能

  • 用户登录
  • 首页
    • 扫码核销
    • 会员识别
    • 消息中心
    • 我的收益
    • 营销(素材列表、分享集客)
    • 集客排行榜
  • 我的集客
    • 会员管理
    • 集团集客排行榜
    • 本院集客排行榜
  • 个人中心
    • 员工信息
    • 我的收益
    • 密码修改
    • 消息中心(收益变化 等)
    • 用户注销

webpack.config.js 配置说明

const path = require('path'),
	webpack = require('webpack'),
	NODE_ENV = process.env.NODE_ENV || "DEV", //环境类型
	NODE_RUN = process.env.NODE_RUN || "0", //是否是运行
	ROOT_PATH = path.resolve(__dirname) + "\\",
	OUT_PATH = path.resolve(ROOT_PATH, 'build') + "\\",
	SERVER_PATH = process.env.SERVER || "./build/",// 服务路径
	ExtractTextPlugin = require("extract-text-webpack-plugin"),
	HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
	entry: {
		page: "./src/js/entrance.js", //[ROOT_PATH + "\\js\\entrance.js"],
		// 打包第三方库作为公共包
	    commons: ['vue', 'vue-router']
	},
	output: {
		path: NODE_RUN === "0" ? './build' : "/",//"./build",//"./build",//path.resolve(__dirname, './build'), //path.resolve(__dirname, './build'), //
		//publicPath路径就是你发布之后的路径,比如你想发布到你站点的/util/vue/build 目录下, 那么设置publicPath: "/util/vue/build/",此字段配置如果不正确,发布后资源定位不对,比如:css里面的精灵图路径错误
		publicPath: NODE_RUN === "0" ? "./build/": "/build/",//"build/",//SERVER_PATH, //process.env.CUSTOM ? "/git/WebApp/n-build/" : "/n-build/",
		filename: NODE_RUN === "0" ? "build.[hash].js" : "build.js",
		/*
	    	import()加载的文件会被分开打包, 我们称这个包为chunk, chunkFilename用来配置这个chunk输出的文件名.
	    	[id]: 编译时每个chunk会有一个id.
	    	[chunkhash]: 这个chunk的hash值, 文件发生变化时该值也会变. 文件名加上该值可以防止浏览器读取旧的缓存文件.
	    */
	    //chunkFilename: '[id].js?[chunkhash]',
	},
	externals:[require('webpack-require-http')],
	module: {
		rules: [{
          	test: /\.html$/,
          	use: [{
          		loader: 'html-loader',
          		options: {
            		//root: resolve(__dirname, 'src'),
            		attrs: ['img:src', 'link:href']
          		}
           }]
		}, {
            test: /\.js(x)*$/,
            exclude: /^node_modules$/,
            //loader: 'babel-loader'
            use: ['babel-loader']
       	}, {
			test: /\.vue$/,
			use: ['vue-loader']
			//loader: 'vue-loader'
//			options: {
//				loaders: {
//		            css: ExtractTextPlugin.extract({
//		              	loader: 'css-loader',
//		              	fallbackLoader: 'vue-style-loader' // <- this is a dep of vue-loader, so no need to explicitly install if using npm3
//		            })
//		        }
//			}
		}, 
//		{
//			test: /\.html$/,
//			loader: 'html-loader',
//			options: {
//            	/*
//	              	html-loader接受attrs参数, 表示什么标签的什么属性需要调用webpack的loader进行打包.
//	              	比如<img>标签的src属性, webpack会把<img>引用的图片打包, 然后src的属性值替换为打包后的路径.
//	              	使用什么loader代码, 同样是在module.rules定义中使用匹配的规则.
//	              	如果html-loader不指定attrs参数, 默认值是img:src, 意味着会默认打包<img>标签的图片.
//	              	这里我们加上<link>标签的href属性, 用来打包入口index.html引入的favicon.png文件.
//            	*/
//            	attrs: ['img:src', 'link:href']
//          }
//		},
		{
			test: /\.css$/,
			exclude: /^node_modules$/,
//			use:[{
//          	loader: 'style-loader'
//          },{
//          	loader: 'css-loader'
//          }]
			//use: ['style-loader', 'css-loader']
//			loader: ['style-loader', 'css-loader']
//			loader: `vue-style-loader!css-loader!autoprefixer-loader?{ browsers: ['last 100 versions'] }!`
			loader: ExtractTextPlugin.extract({
                fallbackLoader: "style-loader",
                loader: "css-loader",
                publicPath: "./"
            })
		}, {
            test: /\.less/,
            exclude: /^node_modules$/,
//          use:[{
//          	loader: 'style-loader'
//          },{
//          	loader: 'css-loader'
//          },{
//          	loader: 'less-loader'
//          }]
//                loader: ['style-loader', 'css-loader', 'less-loader']
//                loader: `vue-style-loader!css-loader!less-loader!autoprefixer-loader?{ browsers: ['last 100 versions'] }!less-loader`
            loader: ExtractTextPlugin.extract({
	          	fallbackLoader: 'style-loader',
	          	loader: "css-loader!less-loader",
                publicPath: "./"
	        })
        },{
	        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
	        use: [{
	        	loader: "url-loader",
	        	query: {
		          	limit: 10000,
		          	name: 'imgs/[name].[hash:7].[ext]'
	        	}
	        }]
	        //loader: 'url-loader',
//	        use: ['url-loader'],
//	        query: {
//	          	limit: 5000,
//	          	name: 'imgs/[name].[hash:7].[ext]'
//	        }
      	} ,{
	        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
	        use: [{
	        	loader: "url-loader",
	        	query: {
		          	limit: 5000,
		          	name: 'fonts/[name].[hash:7].[ext]'
	        	}
	        }]
	        //loader: 'url-loader',
//	        query: {
//	          	limit: 5000,
//	          	name: 'fonts/[name].[hash:7].[ext]'
//      	}
      	}
//      ,{
//			//!cssnext-loader 会导致压缩的时候动画命名被覆盖
//			test: /\.(png|jpg)$/,
//			exclude: /^node_modules$/,
//			//注意后面那个limit的参数,当你图片大小小于这个限制的时候,会自动启用base64编码图片
//			loader: 'url-loader?limit=8000&name=[name].[ext]'
//		}, {
//          test: /\.(eot|woff|svg|ttf|woff2|gif|appcache)(\?|$)/,
//          exclude: /^node_modules$/,
//          loader: 'file-loader?name=[name].[ext]'
//      }
		]
	},
	plugins:[
//		new ExtractTextPlugin({
//			fileName: NODE_RUN === "0" ? "style.[hash].css" : "style.css",
//			disable: false,
//			allChunks: true
//		}), //加上这个参数老是编译不通过不知道为什么
		new ExtractTextPlugin(NODE_RUN === "0" ? "style.[hash].css" : "style.css"),
		new HtmlWebpackPlugin({
			filename: "../index.html", //生成的html存放路径,相对于 path
			template: './src/index.html', //html模板路径
			favicon: "./src/imgs/goldfish.ico",
			inject: true, //允许插件修改哪些内容,包括head与body
			minify: { //压缩HTML文件
				removeComments: true, //移除HTML中的注释
				collapseWhitespace: false, //删除空白符与换行符
				//removeAttributeQuotes: true
			}
		}),
		/*
		      使用CommonsChunkPlugin插件来处理重复代码
		      因为vendor.js和index.js都引用了spa-history, 如果不处理的话, 两个文件里都会有spa-history包的代码,
      		我们用CommonsChunkPlugin插件来使共同引用的文件只打包进vendor.js
      	*/
		new webpack.optimize.CommonsChunkPlugin({
			name: "commons",
			filename: NODE_RUN === "0" ? "common.[hash].js" : "common.js",
			minChunks: function (module, count) {
		        // any required modules inside node_modules are extracted to vendor
		        return (module.resource && /\.js$/.test(module.resource) && module.resource.indexOf(path.join(__dirname, '../node_modules')) === 0);
		    }
		}),
//		new webpack.optimize.CommonsChunkPlugin({
//    		name: 'manifest',
//    		chunks: ['commons']
//  	}),
		//自动分析重用的模块并且打包成单独的文件
		new webpack.ProvidePlugin({
			//根据环境加载JS
			config: ROOT_PATH + "\\src\\js\\config\\" + NODE_ENV
		})
	],
	resolve: {
        extensions: ['.js', '.vue', '.jsx', '.less', '.scss', '.css'], //后缀名自动补全
    	//fallback: [path.join(__dirname, '../node_modules')], webpack2 不能有这个
//  	alias: {
//          hubs : 'http://www.dev.zmscrm.cn/signalr/hubs',//后续直接 require('AppStore') 即可
//      }
	},
	devServer: {
		historyApiFallback: true,//配置为true, 当访问的文件不存在时, 返回根目录下的index.html文件
		noInfo: true
	},
	performance: {
    	hints: false
  	},
//	vue: {
//		loaders: {
//			css:"vue-style-loader!css-loader?sourceMap",
//			less:"vue-style-loader!css-loader?sourceMap!less-loader?sourceMap"
//		},
//      postcss: [
//          require('autoprefixer')({
//              browsers: ['last 100 versions']
//          })
//      ]
//	},
	devtool: '#eval-source-map'
}
var fileSystem = require('fs');
//打包状态
if(NODE_RUN === "0") {
	module.exports.devtool = false;
	module.exports.plugins = (module.exports.plugins || []).concat([
//		new webpack.LoaderOptionsPlugin({
//		      	minimize: true
//		}), //加上这个编辑“url('data:image/svg+xml;charset=utf-8,<svg....”会报错
		new webpack.DefinePlugin({
			'process.env': {
				NODE_ENV: '"production"'
			}
		}),
		new webpack.optimize.UglifyJsPlugin({
			compress: {
		        warnings: false
		    },
		    output: {
		        comments: false
		    },
			sourceMap: false
		})
	]);
	//非开发环境下要清空 output 文件夹下的文件
	var dirArray = [];
	//递归删文件
	var clearOutPutDir = function(path) {
		if(fileSystem.existsSync(path)) {
			var dirList = fileSystem.readdirSync(path);
			dirList.forEach(function(fileName) {
				if(fileSystem.statSync(path + fileName).isDirectory()) {
					console.info("目录:" + path + fileName);
					// 目录
					dirArray.push(path + fileName);
					clearOutPutDir(path + fileName + "\\");
				} else {
					console.info("文件:" + path + fileName);
					fileSystem.unlinkSync(path + fileName);
				}
			});
		};
	}
	clearOutPutDir(OUT_PATH);
	for(var i = dirArray.length - 1, j = 0; i >= j; i--) {
		console.info(dirArray[i])
		fileSystem.rmdirSync(dirArray[i]);
	}
} else {
	console.info("run........................................");
	//本地运行状态把index.html中的href、src连接修改掉
	fileSystem.readFile("index.html", 'utf-8', function(err, data) {
	if(err) {
		console.log("error");
	} else {
		//将index.html里面的hash值清除掉
		var devhtml = data.replace(/((?:href|src)="[^"]+\.)(\w{20}\.)(js|css)/g, '$1$3');
		fileSystem.writeFileSync('index.html', devhtml);
	}
});
}

webApp技术框架说明

1. 入口(entrance.js)

webpack的入口加载文件,也是Webapp的初始化。主要做app SUI的初始化、VUE的初始化、webapp的常用JS加载。

import babelPolyfill from 'babel-polyfill'
import mui from "./lib/mui"
import app from './app'
import globalService from './services/global-service'
import log from './utils/log'
import utils from './utils/utils'
import directives from "./utils/directives"
import Vue from 'vue'
import Vuex from 'vuex'
import VueRouter from 'vue-router'
import routers from "./routers"
import vueApp from "../views/app"
import store from "./store/"
import jQuery from "./lib/jquery-1.12.4"

Object.assign(app.Config, config);
window.app = Object.assign({}, app, {log, utils, mui, globalService});
//signalR是基于jquery的,所以必须要把jQuery引进来,仅仅是用于signalR。太恶心了,其实我TM的真的不想这样...
window.jQuery = window.$ = jQuery;
const initVue = function(){
	Vue.use(Vuex);
	Vue.use(VueRouter);
	Object.keys(directives).forEach((key) => {
	    Vue.directive(key, directives[key]);
	});
	const [router, VueApp] = [routers.createRouter(VueRouter, store), Vue.extend(vueApp)];
	window.app.vueApp = new VueApp({ router, name: "app", store }).$mount('#app');
}
mui.init({
	swipeBack:false, //关闭右滑关闭功能(默认就是false)
	keyEventBind: {
		backbutton: true  //开启back按键监听(默认就是true)
	},
	statusBarBackground: "#1981D8" //设置状态栏颜色,仅iOS可用
});
if(mui.os.plus) {
	app.Config.isApp = true;
	mui.plusReady(function(){
		Object.assign(app.Config.device, {
			isAndroid : plus.os.name === "Android", //是否在安卓环境内
			isIOS : plus.os.name === "iOS", //是否在IOS环境内
			model: plus.device.model, //设备的型号
			imsi: plus.device.imsi, //设备的国际移动用户识别码 ,//Android - 2.2+ (支持): 如果设备没有插入SIM卡,则返回空数组。|iOS - 4.5+ (不支持): iOS设备不支持获取SIM卡信息,返回空数组。
			vendor: plus.device.vendor, // 设备的生产厂商
			uuid: plus.device.uuid, //设备的唯一标识
//			resolutionHeight: plus.screen.resolutionHeight * plus.screen.scale, //设备屏幕高度分辨率
//			resolutionWidth: plus.screen.resolutionWidtht * plus.screen.scale, //设备屏幕宽度分辨率,目前好像是空的
//			scale: plus.screen.scale, //逻辑分辨率与实际分辨率的比例
			version: plus.os.version, //系统版本信息
			osName: plus.os.name //系统的名称
		});
		app.Config.version = plus.runtime.version;
		app.Config.clientVersion = plus.runtime.innerVersion;
		initVue();
	});
} else {
	mui.ready(function() {
		initVue();
	});
}
2. app配置以及其他方法(app.js)
/**
 * 作者:yujinjin9@126.com
 * 时间:2016-03-03
 * 描述:app 核心框架
 */
const site = {
	Config: {
		resourecePath: "", //资源服务路径
		serverPath: "", //服务路径
		version: "", //app版本
		releaseTime: "", //发布时间
		isDebug: true, //是否是前端调试状态
		innerVersion : "999.999.999", // 获得当前终端的版本号
		startVersion: "0.1.1", //app启动动画版本号
		isInsideApp : false, // 是否在APP应用环境内
		isWeiXin : false, //是否在微信环境内
		isApp: false, //是否是在app内
		device: {
			isAndroid : false, //是否在安卓环境内
			isIOS : false, //是否在IOS环境内
			model: null, //设备的型号
			imsi: null, //设备的国际移动用户识别码 ,//Android - 2.2+ (支持): 如果设备没有插入SIM卡,则返回空数组。|iOS - 4.5+ (不支持): iOS设备不支持获取SIM卡信息,返回空数组。
			vendor: null, // 设备的生产厂商
			uuid: null, //设备的唯一标识
			resolutionHeight: null, //设备屏幕高度分辨率
			resolutionWidth: null, //设备屏幕宽度分辨率
			scale: null, //逻辑分辨率与实际分辨率的比例
			version: null, //系统版本信息
			osName: null, //系统的名称
		}//设备信息
	},

	initApp() {
		//获取当前环境
		if(window.navigator && window.navigator.userAgent) {
			var ua = window.navigator.userAgent.toLocaleLowerCase();
			site.Config.isWeiXin = (ua.match(/MicroMessenger/i) == 'micromessenger');
		}
	},

	//重写ajax定义的方法,主要用于自己业务逻辑上的处理
	ajax: function(options) {
		let _url = null;
		if(app.Config.isApp && plus.networkinfo.getCurrentType() === plus.networkinfo.CONNECTION_NONE){
			app.mui.toast('<a href="javascript:void(0);" style="text-decoration: underline;color: #FFF;" onclick="window.location.reload();">亲~网络连接不上,请检测网络。点此刷新重试</a>', {duration:'8000', type:'div'});
			return;
		}
		if(typeof(options) === "string"){
			_url = options;
		} else if(typeof(options) !== "object" || !options.url){
			app.mui.alert("Ajax 参数错误!");
			return;
		} else {
			_url = options.url;
			delete options.url;
		}
		var _default = {
			type: "POST",
            dataType: "json",
            headers:{'Content-Type':'application/json'},
            processData: true, //是否自动处理data数据
            async: true,
            timeout: 20000,
            auth: false, //是否验证当前API接口的登录权限
            authFailCallbackFun: null, // 验证失败的回调函数
            successFunData: true, //是否验证成功回调函数的数据
            showLoading: false //是否显示加载
		};
        var _options = app.mui.extend(true, {}, _default, options);
       	if(!_options.data) {
       		_options.data = {};
       	}
       	if(_options.type.toUpperCase() === 'GET'){
        	_url += '&rnd='+ (+new Date()+'');
        	_options.data = _options.data || {};
        } else if(!_url.match(/^(?:http|ftp|https):\/\//)){
        	//如果传的url含有 http://说明是个绝对路径,就不用拼了
            _url = app.Config.webapiDomain + _url;
        }
        if(_default.processData && typeof(_options.data) === "object" && _options.type.toUpperCase() === 'POST'){
        	_options.data = JSON.stringify(_options.data);
        }
        if(_options.auth === true && !app.globalService.isLogin()) {
        	if(typeof(_options.authFailCallbackFun) === "function"){
        		_options.authFailCallbackFun();
        	}
        	return;
        }
        _options.success = function(data, textStatus){
        	if(_options.showLoading === true){
            	//router.app.$emit('vHideLoad')
            }
        	var _data = data;
        	if(_options.successFunData === true){
        		if(data.success){
        			_data = data.result;
    			} else if(data.error && data.error.message) {
					app.mui.alert(data.error.message, "错误提示");
					return;
				} else {
					app.mui.toast("出错了!");
					return;
				}
        	}
        	if(typeof(options.success) === "function"){
        		options.success(_data);
        	}
        	if(typeof(options.complete) === "function"){
        		options.complete(data, textStatus);
        	}
        }
        _options.error = function(xhr, error){
        	if(_options.showLoading === true){
        		_options.showLoading = false;
            	//router.app.$emit('vHideLoad');
            }
            app.log.debug(xhr, error);
        	if(typeof(options.error) === "function"){
        		options.error(xhr, error);
        	} else if (xhr.response) {
        		var responseJSON = null;
        		try{
        			responseJSON = JSON.parse(xhr.response);
        		}catch(e){}
        		if(responseJSON && responseJSON.__abp && responseJSON.unAuthorizedRequest){
        			//app.mui.toast('<a href="javascript:void(0);" style="text-decoration: underline;color: #FFF;" onclick="window.location.reload();">亲~登录过期了。点此重新登录</a>', {duration: 8000, type:'div'});
        			app.mui.confirm("亲~登录过期了!", null, ['想再看看','去登录'], function(e){
        				if(e.index === 1){
        					app.vueApp.$router.push({ name: 'login' });
        				}
        			});
        		} else if(responseJSON && responseJSON.__abp && responseJSON.error && responseJSON.error.message){
        			app.mui.toast(responseJSON.error.message);
        		}
            } else {
                app.mui.toast('<a href="javascript:void(0);" style="text-decoration: underline;color: #FFF;" onclick="window.location.reload();">亲~服务出错了。点此刷新重试</a>', {duration: 8000, type:'div'});
            }
        	if(typeof(options.complete) === "function"){
        		options.complete(xhr, error);
        	}
        }
        _options.beforeSend = function(xhr){
        	//xhr.setRequestHeader("ClientVersion", app.Config.innerVersion);
        	var _token = app.globalService.getLoginUserInfo().token;
        	if(_token){
                xhr.setRequestHeader("Authorization", _token);
            }
            if(_options.showLoading === true){
            	//router.app.$emit('vShowLoad');
            }
            if(typeof(options.beforeSend) === "function"){
        		options.beforeSend(xhr);
        	}
        }
        app.mui.ajax(_url, _options);
	},
	
	//获取图片地址,如果地址带有 http://那么就认为是绝对地址,然后直接返回
    getResourceUrl: function(url){
    	if(url && url.match(/http:\/\//)){
            return url;
    	}
        //全站统一配置
        if(window.abp){
            return app.Config.imageDomain + url;
        }
        return "http://img.yaomaiche.com" + url;
    },

	//获取站点本地存储信息
	getSiteLocalStorage: function(){
		var _site_local_storage = app.utils.localStorage("siteLocalStorage");
		if(_site_local_storage) {
			try {
				_site_local_storage = JSON.parse(_site_local_storage);
			}catch(e){
				app.log.error(e);
			}
		}
		if(_site_local_storage == null || typeof(_site_local_storage) != "object"){
			_site_local_storage = {};
		}
		return _site_local_storage;
	},
};
site.initApp();
module.exports = site;
3. 单页路由配置(routers.js)
import globalService from './services/global-service'
import appRouters from "./components/app-routers"

export default {
	routes: [{
		path: '/', //首页
		name: "home",
        meta: { title: "首页" },
		component: require('../views/home.vue') //resolve => require(['../views/home.vue'], resolve)
	},{
		path: '/users/user-center', //用户中心
		name: "userCenter",
		meta: { title: "个人中心" },
		component: require('../views/users/user-center.vue') 
	},{
		path: '/users/my-message-list', //消息中心
		name: "myMessageList",
		meta: { title: "消息列表" },
		component: require('../views/users/my-message-list.vue') 
	},{
		path: '/users/message-details', //消息中心
		name: "messageDetails",
		meta: { title: "消息详情" },
		component: require('../views/users/message-details.vue') 
	},{
		path: '/users/user-info', //个人资料
		name: "userInfo",
		meta: { title: "个人资料" },
		component: require('../views/users/user-info.vue') 
	},{
		path: '/users/reset-password', //个人资料
		name: "resetPassword",
		meta: { title: "重设密码" },
		component: require('../views/users/reset-password.vue') 
	},{
		path: '/users/register', //注册
		name: "register",
		meta: {auth: false, title: "注册" },
		component: require('../views/users/register.vue') 
	},{
		path: '/users/login', //登录
		name: "login",
		meta: {auth: false, title: "登录" },
		component: require('../views/users/login.vue') 
	},{
		path: '/customerGather/my-customer-gathers',
		name: "myCustomerGathers",
		meta: {title: "我的集客" },
		component: require('../views/customerGather/my-customer-gathers.vue') 
	},{
		path: '/barcode', //二维码扫描
		name: "barcode",
		meta: {title: "二维码扫描" },
		component: require('../views/barcode.vue') 
	},{
		path: '/users/welcome', //登录
		name: "welcome",
		meta: {auth: false, title: "启动欢迎" },
		component: require('../views/users/welcome.vue') 
	},{
		path: '*', //未发现该页面
		name: "notFound",
		meta: {auth: false, title: "未发现该页面" },
		component: require('../views/error/404.vue') 
	}],
	
	//使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。 
	scrollBehavior(to, from, savedPosition) {
		if (savedPosition) {
	    	return savedPosition;
	  	} else {
	    	return { x: 0, y: 0 };
	  	}
	  	if (to.hash) {
    		return { selector: to.hash};
  		}
	},
	
	//创建路由
	createRouter(VueRouter, store){
		var _this = this;
		var router = new VueRouter({
			//路由列表
			routes: _this.routes,
			//使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。 
			scrollBehavior: _this.scrollBehavior,
			//hash: 使用 URL hash 值来作路由。支持所有浏览器,包括不支持 HTML5 History Api 的浏览器。
			//history: 依赖 HTML5 History API 和服务器配置。查看 HTML5 History 模式.
			//abstract: 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式。
			//mode: 'history',
			//应用的基路径。例如,如果整个单页应用服务在 /app/ 下,然后 base 就应该设为 "/app/"。
			base: "/",
			//全局配置 <router-link> 的默认『激活 class 类名』。参考 router-link.
			linkActiveClass: "router-link-active"
		});
		//const [_push, _go, _replace] = [router.push, router.go, router.replace];
		const {push, go, replace} = router;
		router.push = function(location) {
			console.info("........push");
			if(!store.state.routerStatus.direction){
				store.dispatch("updateDirection", "going");
			}
			push.call(this, location);
		}
		router.go = function(n) {
			console.info("........go");
			if(store.state.routerStatus.direction != "backing"){
				store.dispatch("updateDirection", "backing");
			}
			go.call(this, location);
		}
		router.replace = function(location) {
			console.info("........replace");
			if(store.state.routerStatus.direction != "replace"){
				store.dispatch("updateDirection", "replace");
			}
			replace.call(this, location);
		}
		router.beforeEach((to, from, next)=>_this.beforeEach(to, from, next, store));
		router.afterEach((router)=> _this.afterEach(router, store));
		return router;
	},

	//访问之前的函数
	beforeEach(to, from, next, store){
		console.info(to.name + "...................beforeEach");
		if(JSON.stringify(store.state.routerStatus.backConfig) !== "{}") {
			store.dispatch("resetBackConfig");
		}
		if(to.meta.auth !== false && !globalService.isLogin()){
			next({name: 'login', query: Object.assign({toName: to.name}, to.query)});
			return;
		}
		switch(to.name) {
			case 'home':
				store.dispatch("updateNavbarStatus",{isShowHead: false, isShowBack: false});
				appRouters.clear();
				break;
			case 'userCenter':
				store.dispatch("updateNavbarStatus",{isShowHead: false, isShowBack: false});
				appRouters.clear();
				break;
			case 'myCustomerGathers':
				store.dispatch("updateNavbarStatus",{isShowHead: false, isShowBack: false});
				appRouters.clear();
				break;
			case 'login':
				store.dispatch("updateNavbarStatus",{isShowBack: false, isShowHead: true, isShowFoot: false});
				appRouters.clear();
				break;
			case 'welcome':
				store.dispatch("updateNavbarStatus",{isShowBack: false, isShowHead: false, isShowFoot: false});
				appRouters.clear();
				break;
			case 'barcode':
				store.dispatch("updateTransition", null);
				store.dispatch("updateNavbarStatus",{isShowBack: false, isShowHead: false, isShowFoot: false});
				appRouters.clear();
				break;
			default:
				store.dispatch("updateNavbarStatus",{isShowFoot: false});
				break;
		}
//		if(site.globalService.isLogin() && "_login _reg _smslogin".indexOf(transition.to.name) != -1) {
//			next({path: '/login', query: { redirect: to.fullPath }});
//			return;
//		}
		// 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
		// next(false): 中断当前的导航。如果浏览器的 URL 改变了(可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。
		// next('/') 或者 next({ path: '/' }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。
		next();
		if(store.state.routerStatus.direction) {
			appRouters.push((store.state.routerStatus.direction == "going" || store.state.routerStatus.direction == "backing" || store.state.routerStatus.direction == "replace"), {
				name: to.name,
				query: to.query,
				url: window.location.href
			});
		} else {
			store.dispatch("updateDirection", appRouters.push(false, {name: to.name, query: to.query, url: window.location.href}) ? "going" : "backing");
		}
		console.info("...................next");
		store.dispatch("updateDirection", null);
	},
	
	//可以记录访问路径
	afterEach(router, store){
		console.info("...................afterEach");
		if(router.meta.title && router.meta.title != store.state.appData.navbarTitle){
			store.dispatch("updateNavbarTitle", router.meta.title);
		}
	}
}
4. 视图组件

存放app的视图组件的目录,目前有加载动画、二维码、弹窗3个组件。

image

5. JS组件

下面是目录JS组件文件的截图,主要是存放app的路由、弹窗、signalR组件

image

6. 环境打包配置

DEV、GQC、PRD、PRE、UAT环境的打包配置文件,比如DEV.js的配置文件如下:

/**
 * 作者:yujinjin9@126.com
 * 时间:2016-03-07
 * 描述:dev 外部接口配置文件
 */
module.exports = {
	//M站点的接口地址
	webapiDomain:'http://storeapi.dev.XXX.cn',
	//M站点的地址
	msiteDomain:'http://msite.dev.platform.XXX.com',
	//支付服务器地址
	payDomain:'http://pay.shdev.XXX.com',
	//获取资源服务器地址
	imageDomain:'http://img.shdev.ymc.com',
	//上传资源服务器地址
	resourceUploadUrl:'http://resource.shdev.XXX.com/Uploader',
	//无线H5服务器地址
	h5ServiceHost:'http://h5.private.XXX.app:7070',
	//公共服务站点地址
	commonDomain: "http://common.dev.ymc.com:8080"
	//UBT服务器地址
}
7. app自己的业务

目前app自己所特有的业务逻辑是global-service.js,是全局业务逻辑用于判断当前用户登录、站点本地存储等一些业务处理。具体代码如下:

/**
 * 作者:yujinjin9@126.com
 * 时间:2015-08-04
 * 描述:APP 全局业务逻辑
 */
export default{
	//判断当前用户信息是否登录
	isLogin() {
        return true;
    },
    
    //获取启动项标志
    getStartFlag(){
    	//如果不是app始终返回true
    	if(!app.Config.isApp) {
    		return true;
    	}
    	const _site_local_storage = app.getSiteLocalStorage();
    	if(_site_local_storage.startInfo) {
    		return (_site_local_storage.startInfo.flag === true && _site_local_storage.startInfo.version === app.Config.startVersion);
    	}
    	return false;
    },
    
    //设置启动项标志
    setStartFlag(flag){
    	const _site_local_storage = app.getSiteLocalStorage();
		if(_site_local_storage.startInfo == null || typeof(_site_local_storage.startInfo) != "object"){
			_site_local_storage.startInfo = {};
		}
		Object.assign(_site_local_storage.startInfo, {flag: flag, version: app.Config.startVersion});
		app.utils.localStorage("siteLocalStorage", JSON.stringify(_site_local_storage));
    },
    
    //获取用户登录的Token信息
    getLoginUserInfo(){
    	const [_currentTime, _userInfo] = [(new Date()).getTime(), app.getSiteLocalStorage().userInfo || {}];
    	if(_userInfo.expireTime && (_userInfo.expireTime - _currentTime) > 0) {
    		return _userInfo;
    	} else {
    		app.globalService.setUserInfo({});
    		return {};
    	}
    },
    
    //退出登录
    logOut(){
    	app.globalService.setUserInfo({});
    },
    
    //设置用户信息
    setUserInfo({tenancyName, token, usernameOrEmailAddress, expireTime = -1}) {
    	if(expireTime > 0) {
    		const _site_local_storage = app.getSiteLocalStorage();
			if(_site_local_storage.userInfo == null || typeof(_site_local_storage.userInfo) != "object"){
				_site_local_storage.userInfo = {};
			}
			expireTime = (new Date()).getTime() + (expireTime - 60) * 1000;
			Object.assign(_site_local_storage.userInfo, {tenancyName, token, usernameOrEmailAddress, expireTime, version: app.Config.innerVersion});
    		app.utils.localStorage("siteLocalStorage", JSON.stringify(_site_local_storage));
    	} else {
    		app.utils.localStorage("siteLocalStorage", "{}");
    	}
    },
    
    //app更新升级 TODO: 需要根据实际的业务数据调整 by yujinjin
    updateApp(){
    	//mui.os.plus && !mui.os.stream && mui.plusReady(update);
    	app.ajax({
    		url: "", //更新URL
    		data: {
    			"appid": plus.runtime.appid,
				"version": plus.runtime.version,
				"imei": plus.device.imei
    		},
    		success: function(data){
    			if (data.status) {
					plus.nativeUI.confirm(data.note, function(event) {
						if (0 == event.index) {
							plus.runtime.openURL(data.url);
						}
					}, data.title, ["立即更新", "取  消"]);
				}
    		}
    	});
    }
}
8. vuex管理webApp的数据状态

存放webApp的临时数据,目前有app业务数据、定义事件、路由状态信息,其index.js是数据管理的入口文件。

image

9. webapp的工具包

存放app的工具包,目前有自定义指令、日志、更新、常用方法。

image

10. webapp的页面视图

下面是目录视图文件的截图,主要是首页、登录、个人中心等页面视图。

image

app打包及原生JS接口调用

  • app打包技术是用HBuilder IDE工具通过manifest.json配置一键打包成android和iOS,具体教程参看dcloud提供的文档

  • 原生设备的接口教程参看dcloud提供的文档

  • app的更新,dcloud提供三种解决方案。目前采用第二种解决方案,第一种方案作为备用。

      1. 整包(apk/ipa)升级
      1. App资源在线升级更新(生成移动App资源升级包直接下载更新)
      1. App资源在线差量升级更新(差量升级包是针对某个历史版本到新版本的差量,所以对于升级服务器来讲需要保留所有历史版本,并且分别生成每个历史版本到新版本的差量升级包。) 其更新的JS的代码如下
// src/js/utils/update.js

/**
 * 作者:yujinjin9@126.com
 * 时间:2017-01-19
 * 描述:app在线更新
 */
module.exports = {
	//资源在线升级更新
	updateWgt(){
		plus.downloader.createDownload("http://demo.dcloud.net.cn/test/update/H5EF3C469.wgt", {filename:"_doc/update/"}, function(d,status){
			plus.nativeUI.showWaiting("下载wgt文件...");
	        if ( status == 200 ) { 
	            app.log.debug("下载wgt成功:"+ d.filename);
	            plus.nativeUI.showWaiting("安装wgt文件...");
			    plus.runtime.install(d.filename, {} ,function(){
			        plus.nativeUI.closeWaiting();
			        app.log.debug("安装wgt文件成功!");
			        plus.nativeUI.alert("应用资源更新完成!",function(){
			            plus.runtime.restart();
			        });
			    },function(e){
			        plus.nativeUI.closeWaiting();
			        app.log.debug("安装wgt文件失败["+e.code+"]:"+e.message);
			        plus.nativeUI.alert("安装wgt文件失败["+e.code+"]:"+e.message);
			    });
	        } else {
	            app.log.debug("下载wgt失败!");
	            plus.nativeUI.alert("下载wgt失败!");
	        }
	        plus.nativeUI.closeWaiting();
	    }).start();
	},
	//整包更新
	updateApk(){
		if(app.Config.device.isAndroid){
			plus.downloader.createDownload("", {filename:"_doc/update/"}, function(d,status){
				plus.nativeUI.showWaiting("下载app文件...");
		        if ( status == 200 ) { 
		            app.log.debug("下载app成功:"+ d.filename);
		            plus.nativeUI.showWaiting("安装app文件...");
				    plus.runtime.install(d.filename, {} ,function(){
				        plus.nativeUI.closeWaiting();
				        app.log.debug("安装app文件成功!");
				        plus.nativeUI.alert("应用资源更新完成!",function(){
				            plus.runtime.restart();
				        });
				    },function(e){
				        plus.nativeUI.closeWaiting();
				        app.log.debug("安装app文件失败["+e.code+"]:"+e.message);
				        plus.nativeUI.alert("安装app文件失败["+e.code+"]:"+e.message);
				    });
		        } else {
		            app.log.debug("下载wgt失败!");
		            plus.nativeUI.alert("下载wgt失败!");
		        }
		        plus.nativeUI.closeWaiting();
		    }).start();
		} else if(app.Config.device.isIOS){
			//iOS平台的ipa无法安装,此时需要跳转到appstore,提示用户自动点击升级更新,跳转到appstore的方法为打开应用的appstore地址
			var url='itms-apps://itunes.apple.com/cn/app/hello-h5+/id682211190?l=zh&mt=8';// HelloH5应用在appstore的地址
			plus.runtime.openURL(url);
		}
	}

}

最后

  • 如果喜欢一定要 star哈!!!(谢谢!!)

  • 如果有意见和问题 请在 lssues提出,我会在线解答。

About

这是一个app(android/iOS)项目,但页面视图全部都用的是html5页,没有使用app的原生页面。 前端h5是基于mui + vue2 + vue-router2 + es6 + webpack2 + vuex + signalR 的前端webApp单页项目框架,项目可以直接在PC上运行html5页面。 app打包技术是用HBuilder IDE工具一键打包成APP。


Languages

Language:JavaScript 69.5%Language:CSS 21.2%Language:Vue 9.0%Language:HTML 0.2%