一直負(fù)責(zé)公司的一個小程序項目開發(fā),到目前為止,一期版本也算完成的差不多了,覺得也是時候從技術(shù)的角度對項目作一個小結(jié)了,記錄踩的一些坑和一些自己覺得的最佳實踐吧!關(guān)于項目工 ...
一直負(fù)責(zé)公司的一個小程序項目開發(fā),到目前為止,一期版本也算完成的差不多了,覺得也是時候從技術(shù)的角度對項目作一個小結(jié)了,記錄踩的一些坑和一些自己覺得的最佳實踐吧!
小程序運行時,會把所有的源代碼下載到本地。之后小程序每次運行就像App一樣,幾乎(除cgi數(shù)據(jù),網(wǎng)絡(luò)圖片)全是本地文件IO,而沒有網(wǎng)絡(luò)下載,這也是小程序快的主要原因之一。另外,小程序自帶了ES6編譯轉(zhuǎn)換,css3樣式補全,所以我們基本不需要做任何工程化的事情,因為我們根本不需要合并,打包。JS代碼規(guī)范,是我們所做的唯一與工程化相關(guān)的事了。以下是我們的eslint配置:
//.eslintrc.js
module.exports = {
"env": {
"browser": true,
"node": true,
"commonjs": true,
"es6": true
},
"globals": {
"App": true,
"wx": true,
"Page": true,
"getApp": true,
"getCurrentPages": true
},
"extends": "eslint:recommended",
"parserOptions": {
"sourceType": "module",
},
"rules": {
// enable additional rules
// 強制使用一致的縮進
// "indent": ["error", 2],
// 要求加上分號
"semi": ["error", "always"],
// override default options for rules from base configurations
// 禁止在條件語句中出現(xiàn)賦值操作符
"no-cond-assign": ["error", "always"],
// disable rules from base configurations
"no-console": "off",
"no-debugger": 0,
}
}
在小程序官方提供的IDE中,編輯與調(diào)試在兩個Tab中,切換起來實在麻煩;另外小程序開發(fā)工具對Emmet (Zen Coding)不支持...;再加上習(xí)慣了自己的開發(fā)工具,要一下切到小程序開發(fā)工具上,真是不適應(yīng);所以我在開發(fā)時,小程序開發(fā)工具僅用于效果預(yù)覽與調(diào)試,而真正的代碼編輯還是使用了自己習(xí)慣的IDE,配合雙顯示器,開發(fā)體驗與開發(fā)H5基本一致。
如果不使用小程序開發(fā)工具做代碼編輯器,要讓.wxml、.wxss支持語法高亮,只需要將.wxml文件設(shè)置為html文件類型,而.wxss文件設(shè)置為css文件類型。由于不同編輯器設(shè)置文件類型的方法不一樣,google一下就知道了。
先用管理員賬號上傳小程序,然后在管理平臺上指定此版本為體驗版,使用擁有體驗權(quán)限的微信號掃碼就可以體驗了。這里有注意:
把小程序api定義wx.d.ts放到項目目錄中,在編碼時,就會有很酷的代碼提示
從小程官方文檔:工具->細(xì)節(jié)點中,我們可以知道,Promise在ios9中不支持,那么我們使用promise時就需要polyfill。 關(guān)于wx.request最大并發(fā)數(shù)為5的限制問題在官網(wǎng)有提及(地址),但我測試時,沒有發(fā)現(xiàn)有這個限制,為了保險起見,我們還是做了相應(yīng)處理。
/**
* utils/app.js
* 1. 增加promise支持
* 2. 突破wx.request最大并發(fā)數(shù)是5的限制
*/
import { Promise, } from "./promise";
import Helper from "./helper";
// 突破 request 的最大并發(fā)數(shù)是 5的限制
// refer https://mp.weixin.qq.com/debug/wxadoc/dev/api/network-request.html#wxrequestobject
let RequestMQ = {
map: {},
mq: [],
running: [],
MAX_REQUEST: 5,
push(param) {
param.t = +new Date();
while ((this.mq.indexOf(param.t) > -1 || this.running.indexOf(param.t) > -1)) {
param.t += Math.random() * 10 >> 0;
}
this.mq.push(param.t);
this.map[param.t] = param;
},
next() {
let me = this;
if (this.mq.length === 0)
return;
if (this.running.length < this.MAX_REQUEST - 1) {
let newone = this.mq.shift();
let obj = this.map[newone];
let oldComplete = obj.complete;
obj.complete = (...args) => {
me.running.splice(me.running.indexOf(obj.t), 1);
delete me.map[obj.t];
oldComplete && oldComplete.apply(obj, args);
me.next();
};
this.running.push(obj.t);
return wx.request_bak(obj);
}
},
request(obj) {
let me = this;
obj = obj || {};
obj = (typeof(obj) === "string") ? { url: obj, } : obj;
this.push(obj);
return this.next();
},
};
function hackRequest() {
wx["request_bak"] = wx["request"];
Object.defineProperty(wx, "request", {
get() {
return (obj) => {
obj = obj || {};
obj = (typeof(obj) === "string") ? { url: obj, } : obj;
return new Promise((resolve, reject) => {
obj.success = resolve;
obj.fail = (res) => {
if (res && res.errMsg) {
reject(new Error(res.errMsg));
} else {
reject(res);
}
};
RequestMQ.request(obj);
});
};
},
});
}
// 增加promsie支持
function addPromise() {
let noPromiseMethods = {
stopRecord: true,
pauseVoice: true,
stopVoice: true,
pauseBackgroundAudio: true,
stopBackgroundAudio: true,
showNavigationBarLoading: true,
hideNavigationBarLoading: true,
createAnimation: true,
createContext: true,
createCanvasContext: true,
hideKeyboard: true,
stopPullDownRefresh: true,
};
Object.keys(wx).forEach((key) => {
if (!noPromiseMethods[key] && key.substr(0, 2) !== "on" && key !== "request" && !(/\w+Sync$/.test(key))) {
wx[key + "_bak"] = wx[key];
Object.defineProperty(wx, key, {
get() {
return (obj) => {
obj = obj || {};
//obj = (typeof(obj) === 'string') ? {url: obj} : obj;
return new Promise((resolve, reject) => {
obj.success = resolve;
obj.fail = (res) => {
if (res && res.errMsg) {
reject(new Error(res.errMsg));
} else {
reject(res);
}
};
wx[key + "_bak"](obj);
});
};
},
});
}
});
}
export default function createApp(config) {
addPromise();
hackRequest();
let helper = Helper.$extend({}, Helper, {
Promise,
});
return Helper.$extend({}, config, {
helper,
});
}
// app.js啟動小程序
import createApp from "./utils/app";
App(createApp({
data: {},
Events: {},
onLaunch() {
// console.log(wx.login());
// Do something initial when launch.
},
});
遇到這個問題,多半是https版本或證書有問題,找后臺或運維解決。
weui-wxss 是官方提供的一些常用組件,可以根據(jù)情況是否使用。這里主要想說的是,從github下載weui-wxss源碼后,要使用dist作為小程序項目根目錄。在預(yù)覽組件效果時,來回切換項目十分麻煩,這時候我推薦 wept 這個瀏覽器環(huán)境的小程序運行工具來幫幫助我們預(yù)覽。
頁面樣式,要使用Page這個元素元素器,則不是.page class選擇器。如:
/*所有頁面初始設(shè)置*/
page {
color: #333;
height: 100%;
font-size: 28rpx;
line-height: 1.5;
background-color: #f2f2f2;
}
下拉刷新最好不要在全局開啟,而是在具體的頁面開啟。另外在具體頁面只能配置window下面的屬性,所以不需要再寫window。下面的配置是**此頁面的下拉刷新和設(shè)置頁面標(biāo)題:
{
"enablePullDownRefresh": true,
"navigationBarTitleText": "小程序"
}
/*引用樣式*/
@import '/components/loading/loading.wxss';
<!--引用wxml-->
<import src="/components/loadmore/loadmore.wxml" />
<!--wxml中引用圖片-->
<image class="icon" src="/images/category.png" mode="aspectFill" />
小程序提供了wx.previewImage方法來預(yù)覽圖片,所有不需要再實現(xiàn)圖片查看器
wx.previewImage({
urls: this.data.swiper.imgUrls
});
app.json中pages選項的第一個頁面即小程序的入口頁面。因此把當(dāng)前開發(fā)頁面配置成第一個頁面,可以方便我們預(yù)覽。
指定頁面path一定要使用“/”開頭:
wx.navigateTo({
url: '/pages/goods/search/search'
});
<navigator url="/pages/goods/detail/detail?gid={{goods[0].id}}" hover-class="weui-cell_active">
<template is="goodsListItem" data="{{goods: goods[0]}}"></template>
</navigator>
block標(biāo)簽在官方文檔中沒有怎么提及,剛開始時甚至都不知道有這個標(biāo)簽。由于
<!--循環(huán)列表-->
<block wx:for="{{history}}" wx:for-item="item" wx:key="*this">
<view class="search__history-list-item g-wto" catchtap="clickSearch" data-key="{{item}}">{{item}}</view>
</block>
<!--條件選擇-->
<block wx:if="{{isOrder}}">
<!--...-->
</block>
<block wx:else >
<!--...-->
</block>
先看使用方法:
<!--template使用-->
<!--/components/nodata/nodata.wxml中定義nodata template-->
<template name="nodata">
<view class="c-no-data" hidden="{{hidden}}">
<view class="content">
<image class="icon" src="{{icon}}" mode="widthFix" />
<view class="label">{{msg || '沒有數(shù)據(jù)'}}</view>
</view>
</view>
</template>
<!--template使用-->
<import src="/components/nodata/nodata.wxml" />
<template is="nodata" data="{{icon:'/images/empty2.png', hidden: empty, msg: '數(shù)據(jù)為空'}}"></template>
<!--include使用-->
<view class="p-search">
<include src="/components/search/search.wxml" />
</view>
Page在啟動時,要求傳入一個配置對象。這個配置對象的某些屬性會在頁面具體的生命周期中執(zhí)行,比如onLoad, onShow...等。
// 官方頁面注冊
Page({
data: {
text: "This is page data."
},
onLoad: function(options) {
// Do some initialize when page load.
},
onReady: function() {
// Do something when page ready.
},
onShow: function() {
// Do something when page show.
},
onHide: function() {
// Do something when page hide.
},
onUnload: function() {
// Do something when page close.
},
onPullDownRefresh: function() {
// Do something when pull down.
},
onReachBottom: function() {
// Do something when page reach bottom.
},
onShareAppMessage: function () {
// return custom share data when user share.
},
// Event handler.
viewTap: function() {
this.setData({
text: 'Set some data for updating view.'
})
},
customData: {
hi: 'MINA'
}
});
如果我們抽象一些公共mixin,則頁面的注冊就會像下面的樣子:
import { $extend } from '../../../utils/helper';
import Search from '../../../components/search/search';
import { SEARCH_CACHE_KEY } from '../../../config/index';
Page($extend({
onLoad() {
this.init({
cacheKey: SEARCH_CACHE_KEY,
cgi: queryOrders,
isOrder: true
});
}
}, Search));
var appInstance = getApp()
// 讀
console.log(appInstance.globalData) // I am global data
// 寫
appInstance.newKey = 'new value';
綁定方式
小程序事件綁定有bind或catch兩種開頭,然后跟上事件的類型,如bindtap, catchtouchstart。區(qū)別是:bind事件綁定不會阻止冒泡事件向上冒泡,catch事件綁定可以阻止冒泡事件向上冒泡。建議使用catch綁定事件。
<view class="search-head">
<view class="search-head__input">
<icon type="search" size="15" class="icon"></icon>
<icon type="clear" hidden="{{!showClear}}" size="15" class="clear" catchtap="clearKeyword"></icon>
<input id="input" class="search-head__input-input" type="text"
placeholder="搜索"
placeholder-class="search-head__input-ph" value="{{keyword}}" focus="{{true}}"
bindinput="keywordInput"
bindconfirm="doSearch" />
</view>
<view class="search-head__cancel" catchtap="goHome">取消</view>
</view>
dataset
<block wx:for="{{filters}}" wx:key="{{filter.name}}" wx:for-item="filter" wx:for-index="idxi">
<view class="m-detail__size">
<view class="label m-detail__size-label">{{filter.name}}</view>
<view class="m-detail__size-wrap">
<block wx:for="{{filter.value}}" wx:key="*this" wx:for-item="item" wx:for-index="idxj">
<block wx:if="{{item.enable}}">
<view class="m-detail__size-item {{item.selected ? 'selected' : ''}}"
data-target="{{item}}"
data-i="{{idxi}}"
data-j="{{idxj}}"
data-selected="{{item.selected}}"
data-enable="{{item.enable}}"
catchtap="doFilter">{{item.value}}</view>
</block>
</block>
</view>
</view>
</block>
doFilter(e) {
let target = e.target.dataset.target;
let selected = e.target.dataset.selected;
let enable = e.target.dataset.enable;
let i = ~~(e.target.dataset.i);
let j = ~~(e.target.dataset.j);
let value = target.value;
//...
}
以下的dataset寫法都會報錯,與常見的mvvm中傳值還是有區(qū)別:
data-j="{{idxj: idxj}}"
data-j="{{idxj, idxi}}"
rpx是小程序提供的一種新的尺寸單位,相比于px,rpx具有更好的兼容性。
js中:只能通過"import RefresherPlugin from '../../plugins/refresher';"這種方式引用,不能省略".."
wxss和wxml中:可以使用/root/path/file.ext的方法引用文件,如
/*引用樣式*/
@import '/components/loading/loading.wxss';
<!--引用wxml-->
<import src="/components/loadmore/loadmore.wxml" />
<!--wxml中引用圖片-->
<image class="icon" src="/images/category.png" mode="aspectFill" />
工作日 8:30-12:00 14:30-18:00
周六及部分節(jié)假日提供值班服務(wù)