V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
southSu
V2EX  ›  分享发现

小程序项目填坑之加薪 6k~

  •  
  •   southSu · 2018-11-08 07:59:54 +08:00 · 2607 次点击
    这是一个创建于 2199 天前的主题,其中的信息可能已经有所发展或是发生改变。

    本文由 @IT·平头哥联盟-首席填坑官∙苏南分享

    大家好,这里是@IT·平头哥联盟,我是首席填坑官——苏南(South·Su),今天要给大家分享的是最近公司做的一个小程序项目,过程中的一些好的总结和遇到的坑,希望能给其他攻城狮带来些许便利,更希望能像标题所说,做完老板给你加薪自古深情留不住,唯有套路得人心

    今天是中秋节的第一天,假日的清晨莫名的醒的特别早,不知道为什么,也许是因为,昨晚公司上线的项目回来的路上,发现了个小 bug,心里有些忐忑吧,一会偷偷先改了,让领导发现这个月绩效就没了~~~~

    蜜獾(huan),绰号 "平头哥" 被称为最无所畏惧的动物

    ​  以上纯为扯淡,现在开始一本正经的装逼,请系好安全带,中间过程有可能会开车,请注意安全!!!!!

    最近这个项目跟团队小伙伴沟通在众多框架中最后选择了wepy,没有直接用原生的,小程序原生就……,大家都懂的,用wepy框架,给自己带来了便利,也带来了不少坑,但纵是如此,我还是怀着:“纵你虐我千百遍,我仍待你如初恋”的心态去认真把项目做好。

    toast 组件

    • toast组件,大家都知道,官方的 api wx.showToast 是满足不了我们的需求的,因为它只支持 "success", "loading"两种状态,同时“ title 文本最多显示 7 个汉字长度”,这是官方原话,有图有真相哦,样式巨丑~
    wx.showToast({
      title: '成功',
      icon: 'success',
      duration: 2000
    })
    wx.showModal({
      title: '提示',
      content: '这是一个模态弹窗',
      success: function(res) {
        if (res.confirm) {
          console.log('用户点击确定')
        } else if (res.cancel) {
          console.log('用户点击取消')
        }
      }
    })
    
    

    wx.showModal的 content 的文字是不会居中的(现在不确定有没有扩展,可以设置),依稀记得有一次因为问题差点跟产品经理吵起来,让文字居中,我说最少要两小时,当时产品就炸了,什么鬼???让文字居中一下要两小时??两小时??两小时??呵呵~走了,后来就下决定自己封装了一个属于自己的 toast 组件,以下为部分核心代码:

    <template lang="wxml">
        
        <view class="ui-toast  {{ className }}" hidden="{{ !visible }}">
            <view class="ui-toast_bd">
                <icon wx:if="{{ options.icon}}" type="{{ options.icon }}" size="40" color="{{ options.color }}" class="ui-toast_icon" />
                <view class="ui-toast_text">{{ options.text }}</view>
            </view>
        </view>
    </template>
    
    <script>
        import wepy from 'wepy';
        const __timer__ =1900;
        //方法以 : __XX__ 命名,因并入其他组件中后,可能引起方法名重复,首席填坑官∙苏南的专栏,梅斌的专栏
        class Toast extends wepy.component {
                
            /**
             * 默认数据
             */
            data={
                 list:[
                    {
                        type: `success`,
                        icon: `success`,
                        className: `ui-toast-success`,
                    },
                    {
                        type: `cancel`,
                        icon: `cancel`,
                        className: `ui-toast-cancel`,
                    },
                    {
                        type: `forbidden`,
                        icon: `warn`,
                        className: `ui-toast-forbidden`,
                    },
                    {
                        type: `text`,
                        icon: ``,
                        className: `ui-toast-text`,
                    },
                ],
                timer:null,
                scope: `$ui.toast`, 
                animateCss:'animateCss',
                className:'',
                visible:!1,
                options:{
                    type: ``, 
                    timer: __timer__, 
                    color: `#fff`, 
                    text: `已完成`, 
                }
            }
            /**
             * 默认参数
             */
            __setDefaults__() {
                return {
                    type: `success`, 
                    timer: __timer__, 
                    color: `#fff`, 
                    text: `已完成`, 
                    success() {}, 
                }
            }
            /**
             * 设置元素显示
             */
            __setVisible__(className = `ui-animate-fade-in`) {
                this.className = `${this.animateCss} ${className}`;
                this.visible = !0;
                this.$apply();
            }
    
            /**
             * 设置元素隐藏
             */
            __setHidden__(className = `ui-animate-fade-out`, timer = 300) {
                this.className = `${this.animateCss} ${className}`;
                this.$apply();
                setTimeout(() => {
                    this.visible = !1;
                    this.$apply();
                }, timer)
            }
            /**
             * 显示 toast 组件,首席填坑官∙苏南的专栏,梅斌的专栏
             * @param {Object} opts 配置项
             * @param {String} opts.type 提示类型
             * @param {Number} opts.timer 提示延迟时间
             * @param {String} opts.color 图标颜色
             * @param {String} opts.text 提示文本
             * @param {Function} opts.success 关闭后的回调函数
             */
            __show__(opts = {}) {
                let options = Object.assign({}, this.__setDefaults__(), opts)
                const TOAST_TYPES = this.list;
                TOAST_TYPES.forEach((value, key) => {
                    if (value.type === opts.type) {
                        options.icon = value.icon;
                        options.className = value.className
                    }
                })
                this.options = options;
                if(!this.options.text){
                    return ;
                };
                clearTimeout(this.timer);
                this.__setVisible__();
                this.$apply();
                this.timer = setTimeout(() => {
                    this.__setHidden__()
                    options.success&&options.success();
                }, options.timer);
    
            }
            __info__(args=[]){
                let [ message, callback, duration ]  = args;
                this.__show__({
                    type: 'text',
                    timer: (duration||__timer__),
                    color: '#fff',
                    text: message,
                    success: () => {callback&&callback()}
                });
            }
            __success__(args=[]){
                let [ message, callback, duration ]  = args;
                this.__show__({
                    type: 'success',
                    timer: (duration||__timer__),
                    color: '#fff',
                    text: message,
                    success: () => {callback&&callback()}
                });
            }
            __warning__(args){
                let [ message, callback, duration ]  = args;
                this.__show__({
                    type: 'forbidden',
                    timer: (duration||__timer__),
                    color: '#fff',
                    text: message,
                    success: () => {callback&&callback()}
                });
            }
            __error__(args){
                let [ message, callback, duration ]  = args;
                this.__show__({
                    type: 'cancel',
                    timer: (duration||__timer__),
                    color: '#fff',
                    text: message,
                    success: () => {callback&&callback()}
                });
            }
            __showLoading__(options){
                wx.showLoading({
                    title: (options&&options.title||"加载中"),
                });
            }
            __hideLoading__(){
                wx.hideLoading();
            }
            onLoad(){
                this.$apply()
            }
        }
    
        export default Toast;
    </script>
    

    调用示例:

    <template>
        <view class="demo-page">
            <Toast />
            <Modals />
        </view>
    </template>
    
    <script>
        import wepy from 'wepy'
        import Toast from '../components/ui/Toast'
        import Modals from '../components/ui/Modals'
        import {fetchJson} from '../utils/fetch';
    
        export default class Index extends wepy.page {
            config = {
                navigationBarBackgroundColor: "#0ECE8D",
          navigationBarTextStyle:"white",
                navigationBarTitleText: ''
            }
            components = {
                Toast: Toast,
                Modals: Modals
            }
            methods = {
                tapToast(){
                    this.$invoke("Toast","__success__",[`您已提交成功,感谢您的支持与配合`]);
                }   
            }
        }
    </script>
    

    @IT·平头哥联盟主要分享前端、测试 等领域的积累

    Storage (数据存储)

    Storage (存储)在前端我们存储的方式,cookielocalStoragesessionStorage等这些,特性就不一一说明了,小程序里大家都知道,数据存储只能调用 wx.setStorage、wx.setStorageSync,相当于 h5 的localStorage,而 localStorage是不会过期的,这个大家都知道,而且在很多的面试中,面试官都会问到这个问题,怎么让 localStorage 像cookie一样,只存两小时、两天、甚至只存两分钟呢?今天带你解惑,让你在职场面试中又减少一个难题,这也是我们项目中一直在用的方式,小程序中也同样实用:

    class storage {
    
      constructor(props) {
        this.props = props || {}
        this.source =  wx||this.props.source;
    
      }
    
      get(key) {
        const data = this.source,
              timeout = (data.getStorageSync(`${key}__expires__`)||0)
    
        // 过期失效
        if (Date.now() >= timeout) {
          this.remove(key)
          return;
        }
        const value = data.getStorageSync(key)
        return value
      }
    
      // 设置缓存
      // timeout:过期时间(分钟)
      set(key, value, timeout) {
        let data = this.source
        let _timeout = timeout||120;
        data.setStorageSync(key,(value));
        data.setStorageSync(`${key}__expires__`,(Date.now() + 1000*60*_timeout));
     
        return value;
      }
    
      remove(key) {
        let data = this.source
            data.removeStorageSync(key)
            data.removeStorageSync(`${key}__expires__`)
        return undefined;
      }
    }
    module.exports = new storage();
    
    

    其实很简单,大家看了之后就都 “哦,原来还可以这样” 懂了,只是一时没想到而已,就是个小技巧,每次在存储的时候同时也存入一个时效时间戳,而在获取数据前,先与当前时间比较,如果小于当前时间则过期了,直接返回空的数据。

    本文由 @IT·平头哥联盟-首席填坑官∙苏南分享

    接口 API 维护

    • **接口 API 的维护,**在没有nodejs之前,前端好像一直都在为处理不同环境(dev、test、uat、prd)下调用对应的API而烦恼,做的更多的就是用域名来进行判断,当然也有些高级一点的做法,后端在页面渲染的时候,存一个变量到cookie里或者在页面输出一个全局的 api 变量(建立在没有前后端分离的基础上),到了小程序同样也是如此,每次都要手动改环境,那么一个项目可能有不同的业务,要调用不同域名 api,又有不同的环境区分,怎么维护会比较好呢??
    env/dev.js
    //本地环境
    module.exports = {
        wabApi:{
            host:"https://dev-ali.southsu.com/XX/api/**",
        },
        questionApi:{
            host:"https://dev-ali.bin.com/question/api/**/question",
        },
        mockApi:{
            host:"https://easy.com/mock/594635**c/miniPrograms"
        },
        inWelApi: {
            host: "https://dev.**.com/Wab/api/escene/v2"   
        }
    };
    
    
     
    import dev from './env/dev'; //本地或开发
    import uat from './env/pre'; //体验环境
    import prd from './env/prd'; //线上
    
    
    var ENV = "prd"; //'dev | uat | prd';
    let _base_ = {
      dev,
      uat,
      prd
    }[ENV];
    var config = {
      ENV,
      baseAPI:{..._base_, env : ENV },
      appID:"wx*****b625e", //公司账号(指数) appid
      isAuthorization:true,
      'logId': 'gVDSMH****HAas4qSSOTb-gzGzoHsz',
      'logKey': 'pxFOg****Jn3JyjOVr',
      questionnaireNo:'z**Insu' // 问卷调查编号
    };
    export const __DEBUG__ = (ENV!="prd");
    export default  config;
    
    
    请求调用 api 处理的示例
    
    import wepy from 'wepy'
    import _login_ from './login';
    import config,{__DEBUG__} from './config';
    import 'wepy-async-function';
    export const  fetchJson = (options)=>{
        /*
         *  请求前的公共数据处理
         * @ param {String}     url 请求的地址
         * @ param {String}     Type 请求类型
         * @ param {String}     sessionId 用户 userToken
         * @ param {Boolean}    openLoad 开启加载提示,默认开启,true-开,false-关
         * @ param {function} StaticToast 静态提示方法 ,详细说明请参考 components/ui/Toast
         * @ param {Object}     header 重置请求头
         * @ param {Boolean}    isMandatory 是否强制用户授权,获取用户信息
        */
    
        StaticToast = getCurrentPages()[getCurrentPages().length - 1];
        let { url,openLoad=true, type, data={},header={}, ...others } = options||{};
        let sessionId = (Storage.get(__login__.server+'_userToken')||"");
        /*Start */
           
            var regExp = /\/(.*?)\//,
            hostkey = url.match(regExp)[1];
        let baseUrl = config.baseAPI[hostkey].host;
        url = url.replace(regExp, '/');
    
        /*End */
    
        __DEBUG__&&console.log('#--baseUrl:', baseUrl);
        __DEBUG__&&console.log('#--请求地址:', `${baseUrl}${url}`);
        __DEBUG__&&console.log('----------分割线---------------');
        openLoad&&StaticToast.__showLoading__();
        return new Promise((resolve, reject) => {
            return wepy.request({
                url:`${baseUrl}${url}`,
                method:(type || 'POST'),
                data,
                header:{
                    "t9oken":sessionId,
                    'content-type': 'application/json',
                    // 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
                    ...header
                },
                success:(res)=>{
                    StaticToast.__hideLoading__();
                    return resolve(resHandler(res,options));
                },
                error:(err,status)=>{
                    StaticToast.__hideLoading__();
                    return reject(errorHandler(err,options,err.statusCode));
                }
            });
        })
        
    

    业务调用示例:

    fetchJson({
    	type:"post",
    	// url:"/mockApi/service/XXX", 最后请求得到的地址是 https://easy.com/mock/594635**c/miniPrograms/service/XXX (域名不同环境不一样,在 config 里的 ENV baseAPI 控制)
    	data:{
    		name:"苏南"
    	},
    	success:res=>{
    		console.log("大家好,我是苏南",res)
    	}
    })
    

    填坑时间了

    填坑时间了wepy框架中每个组件内的生命周期回调 onload,只要是引入了组件,不管你视图有没有渲染,他都会执行,导致某些业务逻辑用不上它的时候也执行了产生异常(当然为个锅< 小程序 >肯定说我不背~^~ ),详细看链接:https://github.com/Tencent/wepy/issues/975https://honeybadger8.github.io/blog/ ,不知道后面有没有人解决。

    rich-text 组件

    • **rich-text,**小程序的一个组件,虽然有那么一点点用处,但又不得不说到底要它何用啊?其它的我就忍了,a标签,a标签啊,属性没有,那还要它何用啊??你都不要我跳转,我还要用你吗? b、i、span、em ……哪个我不能用?不知道设计这个组件的人是不是脑被驴踢了(愿老天保佑,我在这骂他,可千万别被看到了,哈哈~),又是业务需求后台配置的内容有链接,没办法,来吧,搞吧,往死里搞吧,一切的推脱都是你技术 low 的借口(你看,你看,别人的怎么可以跳转啊,别人怎么做到的?给我一刀,我能把产品砍成渣),所以有了后面的填坑:

    曾想仗剑天涯 后来 BUG 太多就没去了

    
    <template>
    	<view class="test-page">
    			<button @tap="cutting">点击解析 html</button>
    			<view wx:if="{{result.length>0}}" class="parse-list">
    					<view  class="parse-view" wx:for="{{result}}" wx:key="unique" wx:for-index="index" wx:for-item="items">
    							<block wx:if="{{items.children&&items.children.length}}">
    									<block wx:for="{{items.children}}" wx:for-item="child" >
    											<text wx:if="{{child.type == 'link'}}" class="parse-link" @tap="goToTap({{child.link}})">{{child.value}}</text>
    											<text class="parse-text" wx:else>{{child.value}}</text>
    									</block>
    							</block>
    							<text class="parse-text" wx:else>{{items.value}}</text>
    					</view>
    			</view>
    			<Toast />
    			<Modals />
    	</view>
    </template>
    
    <script>
    	import wepy from 'wepy'
    	import { connect } from 'wepy-redux'
    	import Toast from '../components/ui/Toast'
    	import Modals from '../components/ui/Modals'
    	import {fetchJson} from '../utils/fetch';
    	import Storage from "../utils/storage";
    
    	function wxHtmlParse(htmlStr=''){
    		if(!htmlStr){
    				return []
    		};
    		const httpExp  =/( http:\/\/|https:\/\/)((\w|=|\?|\.|\/|\&|-)+)/g;//提取网址正则
    		const aExp=/<a.[^>]*?>([(^<a|\s\S)]*?)<\/a>/ig; //a 标签分割正则
    		let cuttingArr = htmlStr.split(/[\n]/);
    		let result = [];
    		//有 a 标签的 html 处理
    		let itemParse = (itemHtml='')=>{
    				let itemCutting = itemHtml.split(aExp)||[];
    				let itemResult = [];
    				for(var i = 0;i<itemCutting.length;i++){
    						let _html = itemCutting[i];
    						if(_html!==''){
    								let itemData = {
    										value:_html,
    										type:'text',
    										class:"parse-text"
    								};
    								let matchTag = itemHtml.match(aExp)||[]; //再次匹配有 a 标签的
    								if(matchTag.length){
    										let itemIndex = matchTag.findIndex((k,v)=>(k.indexOf(_html)!==-1));
    										if(itemIndex>=0){
    												let link = matchTag[itemIndex].match( httpExp)[0];
    												itemData.type = 'link';
    												itemData.link = link;
    												itemData.class = "parse-link";
    										};
    								};
    								itemResult.push(itemData)
    						}
    				};
    				return itemResult;
    		};
    		cuttingArr.map((k,v)=>{
    				let itemData = {type : "view",class:"parse-view"};
    				let isATag = k.match(aExp);
    				if(isATag){
    						itemData.children = itemParse(k);
    				}else{
    						itemData.value = k;
    
    				};
    				result.push(itemData);
    				return k;
    		}) ;
    		return result;
    	};
    	export default class Index extends wepy.page {
    		config = {
    			navigationBarBackgroundColor: "#0ECE8D",
    			navigationBarTextStyle:"white",
    				navigationBarTitleText: '小程序解析数据中的 a 标签'
    		}
    		components = {
    			Toast: Toast,
    			Modals: Modals
    		}
    		data = {
    			html:'大家好,我是苏南( South·Su ),\n 职业:@IT·平头哥联盟-首席填坑官,\n 身高:176cm,\n 性别:男,\n 性取向:女,\n 公司:目前就职于由腾讯、阿里、平安三巨头合资的一家互联网金融公司深圳分公司某事业部、,\n 简介:宝剑锋从磨砺出 梅花香自苦寒来,认真做自己,乐于分享,希望能尽绵薄之力 助其他同学少走一些弯路!,gitHub: https://github.com/meibin08/,\n 兴趣:跑步、羽毛球、爬山、音乐、看书、分享自己的微薄知识帮助他人……,\n 其他:想了解更多吗?可以加入<a href="https://honeybadger8.github.io/blog/#/">386485473 交流群</a>,也可以给我电话<a href="https://github.com/meibin08/">134XX852xx5</a> ,开玩笑啦',
    			result:[]
    		}
    		methods = {
    			cutting(e){
    				this.result = wxHtmlParse(this.html);
    				console.log(`result`,this.result);
    				this.$apply();
    			},
    				
    		}
    			
    	}
    </script>
    

    PS完整示例源码 来啦~,觉得不错记得 Star、StarWatch 哦,感谢!

    今天的分享就到这里,写了蛮久,最近才在开始尝试写博客,新手上路中,如果文章中有不对之处,烦请各位大神斧正。如果你觉得这篇文章对你有所帮助,请记得点赞哦~,想了解更多?那就请关注下方的 公众号,有惊喜哦。

    宝剑锋从磨砺出,梅花香自苦寒来,做有温度的攻城狮,公众号:honeyBadger8

    更多文章:

    作者:苏南 - 首席填坑官
    交流群:912594095,公众号:honeyBadger8
    原文链接: https://blog.csdn.net/weixin_43254766/article/details/82811714
    本文原创,著作权归作者所有。商业转载请联系@IT·平头哥联盟获得授权,非商业转载请注明原链接及出处。

    7 条回复    2018-11-11 22:28:02 +08:00
    southSu
        1
    southSu  
    OP
       2018-11-08 08:01:22 +08:00
    如果可以,可以陪你千年不老,
    千年只想眷顾你倾城一笑;
    如果愿意,愿意陪你永世不离,
    永世只愿留恋你青丝白衣。——我是苏南,给您道一声早安,加油!
    SKull4
        2
    SKull4  
       2018-11-08 09:47:35 +08:00
    话说能加 6K 么
    southSu
        3
    southSu  
    OP
       2018-11-08 11:47:46 +08:00
    @SKull4
    加到 6k,了解一下,怕不怕~
    QiuXX
        4
    QiuXX  
       2018-11-08 13:11:30 +08:00
    当我看到 southSu 的时候我就觉得不简单。。。。。。。。。。。。。果然苏南哈哈哈哈哈哈哈哈哈哈
    Variazioni
        5
    Variazioni  
       2018-11-09 09:38:34 +08:00
    学习了。。最近刚好想做一个小程序。。需求设计都有。。就差技术了。。哈哈
    southSu
        6
    southSu  
    OP
       2018-11-11 22:27:09 +08:00
    @Variazioni
    情之为伤,苦了多少人,
    煞了多少忆,情之为悲,
    冷了多少清,落了多少思,
    纵使飞蛾扑火却依然义无反顾。——我是苏南,感谢支持,双十一快乐!!
    southSu
        7
    southSu  
    OP
       2018-11-11 22:28:02 +08:00
    @Variazioni

    每一个人都应该用努力去惊艳时光,
    生活不会亏待任何一个努力的人!——大佬晚上好,让大佬见笑了!
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3687 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 04:28 · PVG 12:28 · LAX 20:28 · JFK 23:28
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.