egret游戏引擎入门(二)

今天分享一下游戏入口类的编写。
首先游戏中默认的入口为Main.ts,但是因为是游戏入口,我只想在这里看到游戏加载后进入哪个页面,不想看见乱七八糟的游戏加载等事件。所以在这里将游戏入口拆分成两个部分,一个是入口的基类BaseMain,一个是入口类Main。这个地方是借鉴的别人的思想。

游戏入口基类:BaseMain

先说一下基类吧,对于像我这样的不是计算机专业的,也没学过java这种语言的人来说,可能不知道什么是基类。百度的解释是:通过继承机制,可以利用已有的数据类型来定义新的数据类型。在本游戏中,游戏入口基类最要承担游戏资源的加载、egret自带的素材解析、egret自带的主题解析、以及加载完资源后的操作。

资源加载RES

游戏资源加载的作用是在游戏开始时预加载资源
其实egret自动生成的demo对这方面的解释也比较清晰,从每一个方法名来看就可以看出,这个方法的作用。
RES.ResourceEvent.CONFIG_COMPLETE是资源配置文件加载完成;

RES.addEventListener(RES.ResourceEvent.CONFIG_COMPLETE,this.onConfigComplete,this);

RES.ResourceEvent.GROUP_COMPLETE是资源组加载完成,之前调研的createjs也有类似的资源组。这里需要判断加载完什么资源之后要执行的方法。比如我在这里就有当loading资源组加载完成后,加载preload资源组,并且显示loading页面。

RES.addEventListener(RES.ResourceEvent.GROUP_COMPLETE,this.onResourceLoadComplete,this);

RES.ResourceEvent.GROUP_PROGRESS是资源组加载中,这里一般会做一个判断,如果不是loading资源组,就显示loading页面,并且显示所有游戏资源的加载情况(进度条);

RES.addEventListener(RES.ResourceEvent.GROUP_PROGRESS,this.onResourceProgress,this);

RES.ResourceEvent.GROUP_LOAD_ERROR是资源组加载出错;

RES.addEventListener(RES.ResourceEvent.GROUP_LOAD_ERROR,this.onResourceLoadError,this);

RES.ResourceEvent.ITEM_LOAD_ERROR是某个资源加载出错。

RES.addEventListener(RES.ResourceEvent.ITEM_LOAD_ERROR,this.onItemLoadError,this);

RES.loadGroup(groupname)是开始加载某个资源组。

RES.loadGroup("loading");

这里的private是这个类的私有属性方法,外部无法调用。就像

function _alert(){
    alert(1);
}

因为js中没有私有、公共这个概念,有时候就会使用” _ “来约定,该属性或方法是私有的,外部不要调用。有addEventListener就有removeEventListener,也就是移除事件监听。因为游戏对性能要求比较高,不用了的监听需要及时移除,省的监听多了造成游戏卡顿。
之前也看到过单独编写资源加载类的,但是自己看了下,没有采用。

其他方法

如打开socket连接、发送第一条请求数据、注入egret定义的素材解析器、添加游戏图层(GameLayerManager)等都会在游戏的基类中编写。
最终贴一下BaseMain的代码吧,因为也是从头开始接触这个,所以注释什么的写的也比较全面。

namespace base {

    export class BaseMain extends eui.UILayer {
        private loadingView: LoadingView; //加载页面

        public constructor() {
            super();
            this.addEventListener(egret.Event.ADDED_TO_STAGE, this._onAddToStage, this);
        }
        private _onAddToStage() {
            SocketManager.getInstance().connection(); //开始websocket连接

            this.stage.registerImplementation('eui.IAssetAdapter',new AssetAdapter()); //注入自定义的素材解析器
            var theme = new eui.Theme('resource/default.thm.json', this.stage);

            this.addChild(GameLayerManager.gameLayer()); //添加游戏图层

            RES.addEventListener(RES.ResourceEvent.CONFIG_COMPLETE,this.onConfigComplete,this); //加载资源配置文件
            RES.loadConfig('resource/default.res.json', 'resource/');
        }

        private onConfigComplete(event: RES.ResourceEvent): void {
            RES.removeEventListener(RES.ResourceEvent.CONFIG_COMPLETE,this.onConfigComplete,this); //移除监听事件            
            //代表什么时候加载资源,完成、预加载等
            RES.addEventListener(RES.ResourceEvent.GROUP_COMPLETE,this.onResourceLoadComplete,this); //加载完成            
            RES.addEventListener(RES.ResourceEvent.GROUP_PROGRESS,this.onResourceProgress,this); //加载进行中
            RES.addEventListener(RES.ResourceEvent.GROUP_LOAD_ERROR,this.onResourceLoadError,this); //加载组错误
            RES.addEventListener(RES.ResourceEvent.ITEM_LOAD_ERROR,this.onItemLoadError,this); //加载单个文件错误
            RES.loadGroup("loading");
        }

        private onResourceProgress(event: RES.ResourceEvent): void {
            if(event.groupName != 'loading') { //如果不是加载loading页面则显示进度条
                this.loadingView.createLoadingBar(event.itemsLoaded, event.itemsTotal);
            }
        }

        private onResourceLoadError(event: RES.ResourceEvent): void {
            console.error('资源[' + event.groupName + ']:加载失败');
            this.onResourceLoadComplete(event);            
        }

        private onItemLoadError(event: RES.ResourceEvent): void {
            console.error('Url:' + event.resItem.url + '加载失败');
        }

        private onResourceLoadComplete(event: RES.ResourceEvent): void {
            console.warn('资源[' + event.groupName + ']:加载完成');
            this.addChild(ViewManager.getInstance()); //添加场景切换控制

            if(event.groupName == 'loading') { //如果加载完成的是loading资源组
                this.loadingView = new LoadingView();
                GameLayerManager.gameLayer().loadLayer.addChild(this.loadingView);
                RES.loadGroup('preload');
            }else if(event.groupName == 'preload') { //如果加载完成的是preload资源组
                SoundManager.getInstance(); //预先加载声音,放到声音预加载之后再执行
                if(GamePlayer.playerInfo.isNew) { //判断是否是新用户
                    RES.loadGroup('guide');
                }else {
                    let timer = setInterval(() => {
                        if(SocketManager.getInstance().isConnection) {
                            clearInterval(timer);
                            this.start();
                            GameLayerManager.gameLayer().loadLayer.removeChild(this.loadingView);                            
                        }                 
                    },100)
                }

            }else if(event.groupName == 'guide') { //如果是新用户 加载guide资源组
                if(SocketManager.getInstance().isConnection && GamePlayer.playerInfo.isNew) {
                    this.firstStart();
                    GameLayerManager.gameLayer().loadLayer.removeChild(this.loadingView);
                }else if(SocketManager.getInstance().isConnection) {
                    GameLayerManager.gameLayer().loadLayer.removeChild(this.loadingView);                    
                    this.start();
                }
            }
            let timer = setInterval(() => {
                if(SocketManager.getInstance().isConnection) {
                    clearInterval(timer);
                    SocketManager.getInstance().sendData(GlobalData.Cmd.gameEnter);
                }
            },100);
        }

        public start(): void {} //走main.ts
        public firstStart(): void {} //走main.ts
    }


    class AssetAdapter implements eui.IAssetAdapter {
        /**
         * 解析素材
         * @param source 待解析的新素材标识符
         * @param compFunc 解析完成回调函数,示例:callBack(content:any,source:string):void;
         * @param thisObject callBack的 this 引用
         */
        public getAsset(source: string, compFunc:Function, thisObject: any): void {
            function onGetRes(data: any): void {
                compFunc.call(thisObject, data, source);
            }
            if (RES.hasRes(source)) {
                let data = RES.getRes(source);
                if (data) {
                    onGetRes(data);
                }
                else {
                    RES.getResAsync(source, onGetRes, this);
                }
            }
            else {
                RES.getResByUrl(source, onGetRes, this, RES.ResourceItem.TYPE_IMAGE);
            }
        }
    }

    class ThemeAdapter implements eui.IThemeAdapter {
        /**
         * 解析主题
         * @param url 待解析的主题url
         * @param compFunc 解析完成回调函数,示例:compFunc(e:egret.Event):void;
         * @param errorFunc 解析失败回调函数,示例:errorFunc():void;
         * @param thisObject 回调的this引用
         */
        public getTheme(url:string,compFunc:Function,errorFunc:Function,thisObject:any):void {
            function onGetRes(e:string):void {
                compFunc.call(thisObject, e);
            }
            function onError(e:RES.ResourceEvent):void {
                if(e.resItem.url == url) {
                    RES.removeEventListener(RES.ResourceEvent.ITEM_LOAD_ERROR, onError, null);
                    errorFunc.call(thisObject);
                }
            }
            RES.addEventListener(RES.ResourceEvent.ITEM_LOAD_ERROR, onError, null);
            RES.getResByUrl(url, onGetRes, this, RES.ResourceItem.TYPE_TEXT);
        }
    }

}

入口类:Main

游戏入口类继承自BaseMain。
刚刚的游戏入口类中,在preload资源组以及guide资源组加载完成之后,分别调用start跟firstStart方法。但是在基类中,这两个方法没有方法体。那么如何调用呢?
在入口类Main中,有两个公共方法,一个是start方法,一个是firstStart方法。因为这个类是继承自BaseMain的,所以在游戏一开始运行时会先执行基类的方法,等到资源加载完成要调用相应的start跟firstStart方法时,就会先调用当前入口类Main中的方法。如果Main中没有该方法,就会执行基类也就是BaseMain中的方法。
这个没有什么好说的,直接贴一下代码:

class Main extends base.BaseMain {

    public constructor() {
        super();
    }

    public firstStart(): void { //新玩家
        var changeEvent = new ChangeSceneEvent(ChangeSceneEvent.CHANGE_SCENE_EVENT);
        changeEvent.eventType = FirstGuideView.FIRST_GUIDE_VIEW;
        changeEvent.obj = this;
        ViewManager.getInstance().dispatchEvent(changeEvent);
        SoundManager.getInstance().playSoundBg(); //播放背景音乐
    }

    public start(): void { //老玩家
        var changeEvent = new ChangeSceneEvent(ChangeSceneEvent.CHANGE_SCENE_EVENT);
        changeEvent.eventType = GameMainView.GAME_MAIN_VIEW;
        changeEvent.obj = this;
        ViewManager.getInstance().dispatchEvent(changeEvent);
        SoundManager.getInstance().playSoundBg(); //播放背景音乐
    }

    private end(): void {

    }

}

下次会分享一下关于声音控制的方法。

egret游戏引擎入门(一)

最初调研egret其实也是迫不得已。因为当时公司突然有了做游戏的需求,然而全公司上下竟然没有一个有开发游戏的经验。然后需求被boss接下来之后分配给了我,然后说可以看一下createjs。
经过一个月的调研+demo制作之后发现,英文的API也能凑合看一看,但是大部分功能还是自己各种尝试。但是因为社区活跃度太低了,加了还几个群,网上各种找资料,没弄懂的依旧没弄懂。打个比方来说屏幕的适配问题,搞了三四天依旧没有搞定。
在这期间,闲着没事儿的时候就会看网上各种关于web游戏引擎的评价、对比。最终选定egret还是因为觉得egret有一整套的工具。比如:ResDepot可以可视化管理游戏资源的,Texture Merger将小图合成大图,ios/android-support等等。

在正式开发之前,也看过好多好多教程、代码,自己写的demo也从最开始的流水式函数式从上到下垒代码,到第五遍demo的时候变成了个人认为比较符合MVC设计结构的代码。
写这一系列文章也是想让那些没有游戏开发经验,但是有这个需求的小白前端开发者们快速的趟过我走过的那些坑。至于egret的API官网上都有,可以直接参照。当然在游戏开发方面我也是小白,这些都是自己看过好多代码好多教程总结出来的,如有问题希望各位大大们多多指教。

游戏的框架:

game_MVC

Model:

先说一下Model吧,游戏所有的数据包括新手引导的数据、游戏的设置、客户端维护的玩家对象、pk赛游戏中客户端维护的其他玩家对象组以及后端返回数据的操作都会作为Model部分。每一个都有对应的Class或者namespace(module)。

View:

因为采用了egret的eui,所以这里的View像ps中的一个一个的图层那样。每一层都会显示不同的内容。比如:场景层SceneLayer,游戏内的各个场景会放置在这一层中进行展示,这一层也是最为重要的一层;弹窗层PanelLayer,一个游戏肯定会有很多类似弹窗、弹框这种的提示信息或模块,这个时候就需要将这些东西放置到弹窗层中进行统一控制;遮罩层MaskLayer及效果层EffectLayer是用来存放遮罩及游戏动效的;加载层LoadLayer只有在游戏最开始加载的时候会用到,进入游戏大厅之后就会销毁。当然也可以当做场景放置在SceneLayer中。

Controller:

最后再说一下Controller。本游戏主要通过声音、通信、图层、流程、场景等几个纬度来进行Controller的设计的。声音控制SoundManager,用于游戏内声音、音效的控制;通信控制SocketManager以及附属的HandleSocket(用于解析protobuf的)来控制游戏的连接以及数据传输的解析;图层控制GameLayerManager,用于控制游戏的各个图层的层级关系,如PanelLayer高于SceneLayer,也可以理解为脱离文档留之后的z-index;场景控制ViewManager,用于控制游戏场景之间的切换;流程控制FlowManager,用在接收后端信息后执行对应的操作。

最后:egret引擎是采用typescript开发的,在调研egret的同时,顺便也学习了一下typescript。所以感觉这个游戏学习上手成本很高,但是也多会了一些平常写页面接触不到的。关于typescript的内容我会令外整理,本教程只分享egret引擎相关的内容。
因为是网络游戏,考虑到信息传输的速度问题,后端决定采用谷歌的二进制传输协议protobuf。protobuf是另外一个前端同事调研的,对于这块儿我也不是很了解,但是这里面会有很多坑,而且调试起来非常的不方便。其实感觉用json也可以,哈哈。


下次会分享关于游戏入口类的编写。

nodejs定时抓取服务器文件并生成页面

最初觉得公司前端做的非产品提的需求的demo没有很好的利用起来,于是想能不能将前端组服务器中的所有demo自动抓取出来,并放置到一个页面中。这样一是有利于前端demo管理,二是其他部门用的时候找起来也比较方便。

遍历文件夹

首先就是要遍历文件夹,获取所有文件夹里面的文件信息

function geFileList(path) {
    var filesList = [];
    readFile(path,filesList);
    return filesList;
}

遍历读取文件

然后就需要将第一步中获取的文件信息读取出来

function readFile(path, filesList) {
    files = fs.readdirSync(path);//需要用到同步读取
    files.forEach(walk);
    function walk(file)
    {
        states = fs.statSync(path+'/'+file);
        if(states.isDirectory())
        {
            readFile(path+'/'+file,filesList);
        }
        else
        {
            //创建一个对象保存信息
            var obj = new Object();
            obj.size = states.size;//文件大小,以字节为单位
            obj.name = file;//文件名
            obj.path = path+'/'+file; //文件绝对路径
            filesList.push(obj);
        }
    }
}

生成的filesList为一个包含服务器下所有文件的数组,其中数组中的每一个元素都是一个对象,包含name、size、path属性。

匹配特定文件并生成包含title跟path的数组

var arr = [],str = '';
var regTitle = /<title>(.*?)<\/title>/i; //匹配文件的title
for(var i=0,len=filesList.length;i<len;i++){
    var item = filesList[i];
    if(item.name.indexOf("index.html") != -1){ //如果包含index.html
        var data = fs.readFileSync(item.path);
        var title = '"'+regTitle.exec(data)[1] +'"';
        arr.push(title);
        var url = '"www.zhangshuang.top/project' + item.path.slice(2) +'"';
        arr.push(url);
    }
}
str = 'arr = ['+arr+']';

写入文件

将内容用utf-8的格式写入文件,后续会有调用

function writeFile(fileName,data) {
    fs.writeFile(fileName,data,'utf-8',complete);
    function complete() {
        console.log("文件生成成功");
    }
}
writeFile("selfMotion_data.js",str); //第一个为自定义的生成的文件名,第二个为写入的数据

定时任务脚本

定时任务要感谢公司运维的小伙伴,之前在网上找了很多试了就是不行,结果是因为没有加执行的路径,现将shell脚本也贴出来:

/*bianli.sh*/

#!/bin/bash
cd /opt/node_server/public/project
/usr/bin/node /opt/node_server/public/project/bianli.js

记得一开始的cd进入项目目录一定要加上,要不然就不会执行。

启动定时任务

通过vim /etc/crontab命令进入并编辑定时任务。

 */1 * * * *     root   /bin/sh /opt/node_server/public/project/bianli.sh

前面部分是定时任务的频率,最后部分是要执行的脚本。

github地址

nodejs简易爬虫实现

通过nodejs抓取特定页面的特定内容,并且写入文件中。
demo很简单,注释也很完善,很好理解。

var request = require('request'); //请求模块,
var cheerio = require('cheerio'); //相当于jQuery
var fs = require('fs'); //nodejs的文件模块File System

function getHtml(url) {
    request(url, function (error, response, body) {
        if (!error && response.statusCode == 200) {
            var $ = cheerio.load(body,{ //加载html
                decodeEntities:false //禁止转码
            });
            $("div.arcBody>p").each(function () { //这个地方需要根据目标页面修改
                var content = $(this).text(); //获取抓取的信息
                fs.appendFileSync('yikedou.txt',content); //这里只是简单的添加进了txt文件中,需要样式的,直接加入标签在写入文件即可。
            });
            var prevArcLink = $("div.prevNextArc>span#prevArcLink>a").attr("href"); //获取地址
            var realPrevArcLink = site + prevArcLink; //因为该网站的地址不是带域名的绝对路径,所以这里要将网站的域名加上,应具体问题具体修改
            getHtml(realPrevArcLink); //递归,将真实路径传入,递归抓取
            // fs.appendFileSync('yikedou.txt',cnt)
        } else{
            console.log(error,response.statusCode);
        }
    });
}

var site = "http://www.yikedou.com"; //目标页面的域名
var firstUrl = 'http://www.yikedou.com/wenzi/201510/48767.html'; //第一次要抓取的页面

getHtml(firstUrl); //执行

github地址

将ipad扩展为第二屏幕

将ipad扩展为第二屏幕

前几天将家里的ipad翻出来,想看看能不能作为电脑的扩展屏来使用。然后就上网找各种资料破解之类的。试了好多种办法,现在将我找到的办法分享出来,亲测可用:

前提:
  1. 需要一个ipad,我的是ipad air2。
  2. ios端需要安装idisplay(pp助手搜索idisplay,下载即可)。
  3. window也需要安装idisplay
过程:
  1. 在ipad上安装好下载的idisplay。
  2. 在电脑上安装idisplay,虽然是英文但不影响,选择好安装磁盘,一路next。
  3. 保证你的电脑和ipad连接在同一路由器上(如果是笔记本可以创建一个无线热点,让ipad接入,如果不会自己百度一下)。
  4. 在windows上运行idisplay,(用管理员身份运行),此时在桌面右下角的任务栏上会出现一个灰色的图标。
  5. 在ipad上运行idisplay,会自动搜索出你的电脑,选择即可。
  6. 准备工作完成。
用法:

用法:在电脑上将窗口拖向屏幕右侧,奇迹出现了!这个窗口会在ipad上出现!用鼠标即可在ipad上操作,也可触摸操作!相信大家一看就会。
教程到此结束,此功能操作简单,新颖,值得尝试。大家别忘了顶一下,谢谢

分享几个关于时间的操作

分享几个与时间有关的操作

  1. 获取当前时间的毫秒数:
    new Date().getTime();
    +new Date();
  2. 确定是不是在某个时间段:
function isInTime(now) {
    const hours = new Date(now).getHours();
    if(hours >= 9 && hours <= 15) {
        return true;
    }else {
        return false;
    }
}

因为公司是做人工智能投顾的,与股票相关,所以这里时间段直接写的9–15。如果想改变的话可以改成将时间段也传入,并做相应修改。

function isInTime(now, before = 9, after = 15) {
    const hours = new Date(now).getHours();
    if(hours >= before && hours <= after) {
        return true;
    }else {
        return false;
    }
}

3.日期之间的运算,并格式化:

function dateFormat(m, d, numDate, symbol = ":") {
    var date = null;
    if(!numDate) { //如果没有传入numDate
        date = +new Date(); //获取当前时间的毫秒数
        date = date + d * 24 * 60 * 60 * 1000; //将date赋值为当前时间毫秒数+传入天数(d)点击毫秒数
        date = new Date(date); //再将其转换为标准时间。

        var yy = date.getFullYear();
        var mm = date.getMonth();
        var dd = date.getDate();
        if(mm === 0 && m === -1) {
            mm = 11;
            yy = yy - 1;
        }else {
            mm = mm + m;
        }
    }else { //如果传入了numDate(毫秒数),将其转变为制定格式的时间
        date = new Date(numDate);
        var yy = date.getFullYear();
        var mm = date.getMonth();
        var dd = date.getDate();
    }
    return yy + symbol + (mm + 1) + symbol + dd;
}

nodejs安装及常用快捷键

安装nvm,nodejs版本管理工具

参考:https://github.com/creationix/nvm
参考:http://www.imooc.com/article/14617
参考:centOs中安装nodejs
1. 通过curl安装nvm
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.32.1/install.sh | bash
2. 设置环境变量
export NVM_DIR="$HOME/.nvm"
3. load nvm
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"

4. nvm命令行操作
1. 查看已经安装的nodejs版本
nvm list / nvm ls
2. 查看nvm版本
nvm --version
3. 切换版本,仅限下载过的node版本
nvm use <version>例:nvm use v4.7.0
4. 设定默认node版本
nvm alias default <version>例:nvm alias default v4.7.0

##

通过nvm安装nodejs

  1. 安装6.x版本
    nvm install 6
  2. 安装4.x版本
    nvm install 4
  3. 查看node所有版本
    nvm ls-remote
  4. 安装位置
    ~/.nvm/versions/node
    /Users/zhangshuang/.nvm/versions/node
    ##

npm淘宝镜像 cnpm

  1. 安装:
    npm install -g cnpm --registry=https://registry.npm.taobao.org
  2. 使用:
    cnpm install -g <packageName>例:cnpm install -g nodemon

  3. 设置:npm配置信息
    npm config list

  4. 设置:npm registry 设置镜像路径
    npm config set registry "https://registry.npmjs.org/"

设置别名

alias设置别名
1. 原因:简化命令行提交代码的复杂度
2. 方法:vim ~/.bashrc进入该文件
3. 新增:alias <name>='<yourcode>'例:alias myweb='ssh -p 3000 root@192.169.21.141'
4. 生效:source ~/.bashrc
5. 使用:myweb + Enter
6. 如果未生效,创建~/.bash_profile,该文件是用户登录终端时自动执行的文件,一般此文件中会调用.bashrc,在其中写入source ~/.bashrc
##

pm2 与 nodemon

nodemon,开发环境使用,检测script文件变化,自动重启服务器
  1. 安装:
    npm install -g nodemon
  2. 运行:
    nodemon <appName>例:nodemon app.js
  3. 更改默认端口
    nodemon <appName> localhost 8080
pm2,生产环境使用,服务器崩溃自动重启
  1. 安装:
    npm install -g pm2
  2. 运行
    pm2 start <appName>例:pm2 start server.js
    使用多核心pm2 start <appName> -i max
  3. 查看已启动的应用列表
    pm2 list
  4. 查看应用详情
    pm2 show <appName>例:pm2 show server.js
  5. 退出pm2
    pm2 kill
  6. 查看控制台输出
    pm2 logs <appName?>输入app名可查看单个输出
  7. 关闭某个进程
    pm2 stop <id>例:pm2 stop 0
  8. 重启应用
    pm2 restart <appName>例:pm2 restart server.js
  9. 重载应用
    pm2 reload <appName>例:pm2 reload server.js
  10. 停止应用
    pm2 stop <appName>例:pm2 stop server.js
  11. 将应用从pm2中删除
    pm2 delete <appName>例:pm2 delete server.js
  12. 命名应用
    pm2 start server.js --name game
  13. 复合运用,命名、最多核心运行
    pm2 start server.js --name game -i max
    pm2 start server.js --name localGameServer -i max --watch
  14. 监控
    pm2 monit

参考:PM2实用入门指南
参考:生产环境部署pm2

##

express web开发框架

  1. 安装express:
    sudo npm install -g express
  2. 安装express命令行工具:
    sudo npm install -g express-generator
  3. 启动:
    npm start
    ##

node

  1. 运行进程 node
  2. 退出进程 process.exit()
  3. 查看进程pid global.process.pid
  4. 设置作者 npm config get init-author-name

highcharts绘制环形配比图

highcharts绘制k线的常用配置

之前看的都是中文文档,绘制环形配比图这个需求采用highcharts,想挑战一下自己,看一下英文的文档,结果苦逼的挨个敲出来试看是什么东西…

中文的api也有很多,现在将自己的代码贴出来,省的以后哪天想用的时候还要再过一遍,里面基本都写了注释,普通的需求基本都满足了。之后再分享一篇关于k线的配置。

效果图

$("#container").highcharts({
    chart: { //图表整体设置
        type: "pie" //图表样式为饼图
    },
    title: { //图表标题设置
        text: '<span style="color:#333333;font-size:.33rem;">' + ismDuring + '天</span><br/>',
        verticalAlign: "top",
        align: "left",
        x: 0.92 * FS,
        y: 1.34 * FS
    },
    subtitle: { //图表副标题设置
        text: '<span style="font-size:.24rem;text-align:center;color:#979797;">运作期</span>',
        verticalAlign: "top",
        align: "left",
        x: 0.92 * FS,
        y: 1.78 * FS
    },
    plotOptions: { //图表
        pie: {
            center: [0.9 * FS, 1 * FS],
            allowPointSelect: false,
            showInLegend: true, //是否显示图例
            ignoreHiddenPoint: true,
            size: 2.5 * FS, //饼图的大小
            innerSize: 2.16 * FS, //圆环的大小
            colors: [ //圆环颜色,如果数据多于颜色的个数,则颜色会重复出现
                "#df4935", "#f38f28", "#e9da59", "#73d84b", "#3adfd2", "#20acff", "#275caa", "#8d84f0"
            ],
            dataLabels: {
                enabled: false //图表区块的解释,小黑线伸出来的部分
            },
            point: {
                events: {
                    legendItemClick: function() {
                        return false; //图表的legend是否可以点击,如果可以 点击则取消
                    }
                }
            }
        },
        series: {
            allowPointSelect: true, //click的时候能否移出来
            enableMouseTracking: true //hover到饼上面不显示外宽
        }
    },
    legend: { //图例
        layout: "vertical", //图例布局: 垂直排列,
        verticalAlign: "middle",
        align: "right",
        y: 0 * FS,
        x: 0 * FS,
        labelFormatter: function() { //图例显示的内容
            return '<span style="width:1.55rem;display:inline-block;font-size:.18rem;color:#666666;font-weight:100;">' + this.name + '</span><span style="color:#666666;font-weight:100;font-size:.18rem;">' + this.percentage.toFixed(2) + '%</span>'
        },
        useHTML: true,
        itemWidth: 3.5 * FS,
        symbolWidth: 0.16 * FS,
        symbolHeight: 0.16 * FS,
        rtl: false //图表是否在右边
    },
    tooltip: { //提示框
        enabled: false,
        headerFormat: '<span style="font-size:11px">{series.name}</span><br>',
        pointFormat: '<span style="color:{point.color}">{point.name}</span>: <b>{point.y:.2f}%</b><br/>'
    },
    credits: {
        enabled: false
    },
    series: [ //数据
        {
            name: "推荐策略",
            colorByPoint: true,
            data: filterJson(data)
        }
    ]
});

整理几个移动端的兼容性问题

还是前几天谈的移动端问题,这次说一下项目中遇到的几个移动端兼容性问题

一. 华为荣耀3C的原生浏览器问题:

同一行内,存在靠左与靠右的元素时,都必须加上浮动,否则,右浮元素不右浮,影响后续排版。

<div class="forcast-title">
    <span class="fl">上一交易日预报结果</span>
    <span class="fr gray">2016-03-04</span>
</div>
<div class="forcast-cnt ofh gray cb"></div>

两个行内元素span元素都要浮动才行。

二. ios safari时间显示问题。

之前设置new Date日期格式的时候,在ios中的safari中发现显示效果与其他浏览器不一致,找了多方面原因,结果是把“-”换成“/”就可以解决了。new Date("year-month-day");只识别new Date("year/month/day");

三. 关于长按识别二维码问题。

安卓设备微信中识别没问题,结果到了ios中识别不了。最终发现是将position设置为fixed,ios的微信中长按会不起作用。要想在ios的微信中实现长按识别二维码,只需确保二维码图片和包住图片的div都没有设置position:fiexd即可。

四. 移动端最小识别rem问题。

移动端相对来说兼容性不是什么问题,但是实际项目中发现,如果采用rem: .01rem;有些机型如:魅蓝note、iphone5等机型的webview中显示不出来。华为荣耀有的设置border的时候,有时0.03rem可以,有时不可以,暂时不明所以。所以还是建议,像border这种属性,移动端可以设置为1px。

五. 点击跳转到新页面会有类似点击穿透效果。

部分机型(三星note2、htc one …)在app的webview中出现了点击链接跳转到下一页面后,如果要跳转页面在当前页面的按钮位置有a链接的话,会出现,点击当前页面链接,直接也点击了下一页吗的链接。
可能描述的不是很清楚,更形象点说。A页面有个链接a,B页面也有个链接b,并且b的x、y属性含于a的x、y属性。那么会出现点击a链接,直接也点击了b链接。
解决办法,直接在另一个页面写了个maskView遮罩层,setTimeout100毫秒解决的。可能有点low,但是当时上线较急,就想到了这个办法。

六. 让hybird app中的webview页面看起来更像native。

web相较于native有一个好处就是版本更新比较方便,不会出现native应用商店审核乱七八糟的时间,直接点击app的某个按钮,使用webview打开页面,该页面随意替换都非常方便。最常用的就是活动页面,定期的活动只需要前端开发人员维护即可。但是假如用户长按屏幕的时候出现选中文本的场景,看起来不那么想native,如果有解决这个问题的需求,可使用:

Element {
    -webkit-user-select: none;
    -moz-user-select: none;
    -khtml-user-select: none;
    user-select: none;
}

其中Element为任意元素,或者直接将其写入cssReset文件中。

七. 移动端300ms延迟。

300ms尚可接受,不过因为300ms产生的问题,我们必须要解决。300ms导致用户体验并不是很好,解决这个问题,我们一般在移动端用tap事件来取代click事件。
推荐两个js,一个是fastclick,一个是tap.js
关于300ms延迟,具体请看

八. 移动端 HTML5 audio autoplay 失效问题

这个不是 BUG,由于自动播放网页中的音频或视频,会给用户带来一些困扰或者不必要的流量消耗,所以苹果系统和安卓系统通常都会禁止自动播放和使用 JS 的触发播放,必须由用户来触发才可以播放。
解决方法思路:先通过用户 touchstart 触碰,触发播放并暂停(音频开始加载,后面用 JS 再操作就没问题了)。
解决代码:

document.addEventListener('touchstart', function () {
    document.getElementsByTagName('audio')[0].play();
    document.getElementsByTagName('audio')[0].pause();
});
九. 根据屏幕大小加载不同js

之前话k线用到highcharts的时候,在iphone5上发现显示不是很理想,用过highchars的api设置也没有解决一次设定适应多个屏幕。于是只能用过这种方式来进行多屏幕的适配。

<script>
    var browserWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
    var script = document.createElement("script");
    if(browserWidth <= 380) {
        script.src = "js/common_small.js"
    }else{
        script.src = "js/common.js"
    }
    document.head.appendChild(script);
</script>

十. 屏幕旋转的事件和样式

事件名:window.orientation,取值:正负90表示横屏模式,0和180表示竖屏模式。

window.onorientationchange = function(){
    switch(window.orientation){
        case -90:
        case 90:
        alert("横屏:" + window.orientation);
        case 0:
        case 180:
        alert("竖屏:" + window.orientation);
        break;
    }
}  

有些地方发现屏幕旋转之后,样式适配不完美,所以根据上面的orientation事件,增加了监测到设备屏幕旋转自动刷新的功能:

function hengshuping(){
    window.location.reload();
}
window.addEventListener("onorientationchange" in window ? "orientationchange" : "resize", hengshuping, false);
十一. background-size问题:

华为p8,魅族自带浏览器background-size显示有问题。

十二. flex布局问题:

iphone5s、魅族魅蓝note不支持(具体是自带浏览器还是什么浏览器忘了,有需要的可以自己尝试一下)

十三. input输入框的问题

最开始在input输入框中,也会习惯性的写上lineheight(其实可以不写),当使用placeholder的时候自己测试也没有问题。但当测试用魅族mx4自带浏览器测试的时候发现,在input输入框中,输入光标不会上下居中。当时也感觉很奇怪,然而将lineheight删掉就可以了,input中的所有元素就是默认居中的。

通过Html网页调用本地安卓app程序代码

前段时间写一些移动端的项目,正好项目中遇到与native交互的需求,特此将其整理下来:

一. 通过html页面打开Android本地的app
  1. 首先在编写一个简单的html页面
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Insert title here</title>
    </head>
    <body>
        <a href="m://my.com/">打开app</a><br/>
    </body>
</html>
  1. 在Android本地app的配置
    在AndroidManifest的清单文件里的intent-filte中加入如下元素:
<intent-filter>
<action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data
                    android:host="my.com"
                    android:scheme="m" />
</intent-filter>

然后使用“手机浏览器”或者“webview”的方式打开这个本地的html网页,点击“打开APP”即可成功开启本地的指定的app

二、如何通过这个方法获取网页带过来的数据

只能打开就没什么意思了,最重要的是,我们要传递数据,那么怎么去传递数据呢?
我们可以使用上述的方法,把一些数据传给本地app,那么首先我们更改一下网页,代码修改后:

<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Insert title here</title>
    </head>
    <body>
        <a href="m://my.com/?arg0=0&arg1=1">打开app</a><br/>
    </body>
</html>

(1).假如你是通过浏览器打开这个网页的,那么获取数据的方式为:

    uri = getIntent().getData();  String test1= uri.getQueryParameter("arg0");  String test2= uri.getQueryParameter("arg1");

(2)如果使用webview访问该网页,获取数据的操作为:

webView.setWebViewClient(new WebViewClient(){
  @Override
  public boolean shouldOverrideUrlLoading(WebView view, String url) {
      Uri uri=Uri.parse(url);
          if(uri.getScheme().equals("m")&&uri.getHost().equals("my.com")){
              String arg0=uri.getQueryParameter("arg0");
              String arg1=uri.getQueryParameter("arg1");

          }else{
              view.loadUrl(url);
          }
      return true;
  }
});