了解小程序登陸之前,我們寫了解下小程序/公眾號登錄涉及到兩個最關鍵的用戶標識:
OpenId
是一個用戶對于一個小程序/公眾號的標識,開發者可以通過這個標識識別出用戶。UnionId
是一個用戶對于同主體微信小程序/公眾號/APP的標識,開發者需要在微信開放平臺下綁定相同賬號的主體。開發者可通過UnionId,實現多個小程序、公眾號、甚至APP 之間的數據互通了。wx.login
官方提供的登錄能力
wx.checkSession
校驗用戶當前的session_key是否有效
wx.authorize
提前向用戶發起授權請求
wx.getUserInfo
獲取用戶基本信息
以下從筆者接觸過的幾種登錄流程來做闡述:
直接復用現有系統的登錄體系,只需要在小程序端設計用戶名,密碼/驗證碼輸入頁面,便可以簡便的實現登錄,只需要保持良好的用戶體驗即可。
?提過,OpenId
是一個小程序對于一個用戶的標識,利用這一點我們可以輕松的實現一套基于小程序的用戶體系,值得一提的是這種用戶體系對用戶的打擾最低,可以實現靜默登錄。具體步驟如下:
小程序客戶端通過 wx.login
獲取 code
傳遞 code 向服務端,服務端拿到 code 調用微信登錄憑證校驗接口,微信服務器返回 openid
和會話密鑰 session_key
,此時開發者服務端便可以利用 openid
生成用戶入庫,再向小程序客戶端返回自定義登錄態
小程序客戶端緩存 (通過storage
)自定義登錄態(token),后續調用接口時攜帶該登錄態作為用戶身份標識即可
如果想實現多個小程序,公眾號,已有登錄系統的數據互通,可以通過獲取到用戶 unionid 的方式建立用戶體系。因為 unionid 在同一開放平臺下的所所有應用都是相同的,通過 unionid
建立的用戶體系即可實現全平臺數據的互通,更方便的接入原有的功能,那如何獲取 unionid
呢,有以下兩種方式:
如果戶關注了某個相同主體公眾號,或曾經在某個相同主體App、公眾號上進行過微信登錄授權,通過 wx.login
可以直接獲取 到 unionid
結合 wx.getUserInfo
和 這兩種方式引導用戶主動授權,主動授權后通過返回的信息和服務端交互 (這里有一步需要服務端解密數據的過程,很簡單,微信提供了示例代碼) 即可拿到
unionid
建立用戶體系, 然后由服務端返回登錄態,本地記錄即可實現登錄,附上微信提供的最佳實踐:
調用 wx.login 獲取 code,然后從微信后端換取到 session_key,用于解密 getUserInfo返回的敏感數據。
使用 wx.getSetting 獲取用戶的授權情況
獲取到用戶數據后可以進行展示或者發送給自己的后端。
unionid
形式的登錄體系,在以前(18年4月之前)是通過以下這種方式來實現,但后續微信做了調整(因為一進入小程序,主動彈起各種授權彈窗的這種形式,比較容易導致用戶流失),調整為必須使用按鈕引導用戶主動授權的方式,這次調整對開發者影響較大,開發者需要注意遵守微信的規則,并及時和業務方溝通業務形式,不要存在僥幸心理,以防造成小程序不過審等情況。 wx.login(獲取code) ===> wx.getUserInfo(用戶授權) ===> 獲取 unionid
復制代碼
因為小程序不存在 cookie
的概念, 登錄態必須緩存在本地,因此強烈建議為登錄態設置過期時間
值得一提的是如果需要支持風控安全校驗,多平臺登錄等功能,可能需要加入一些公共參數,例如platform,channel,deviceParam等參數。在和服務端確定方案時,作為前端同學應該及時提出這些合理的建議,設計合理的系統。
openid
, unionid
不要在接口中明文傳輸,這是一種危險的行為,同時也很不專業。
經常開發和使用小程序的同學對這個功能一定不陌生,這是一種常見的引流方式,一般同時會在圖片中附加一個小程序二維碼。
借助 canvas
元素,將需要導出的樣式首先在 canvas
畫布上繪制出來 (api基本和h5保持一致,但有輕微差異,使用時注意即可)
借助微信提供的 canvasToTempFilePath
導出圖片,最后再使用 saveImageToPhotosAlbum
(需要授權)保存圖片到本地
根據上述的原理來看,實現是很簡單的,只不過就是設計稿的提取,繪制即可,但是作為一個常用功能,每次都這樣寫一坨代碼豈不是非常的難受。那小程序如何設計一個通用的方法來幫助我們導出圖片呢?思路如下:
繪制出需要的樣式這一步是省略不掉的。但是我們可以封裝一個繪制庫,包含常見圖形的繪制,例如矩形,圓角矩形,圓, 扇形, 三角形, 文字,圖片減少繪制代碼,只需要提煉出樣式信息,便可以輕松的繪制,最后導出圖片存入相冊。筆者覺得以下這種方式繪制更為優雅清晰一些,其實也可以使用加入一個type參數來指定繪制類型,傳入的一個是樣式數組,實現繪制。
結合上一步的實現,如果對于同一類型的卡片有多次導出需求的場景,也可以使用自定義組件的方式,封裝同一類型的卡片為一個通用組件,在需要導出圖片功能的地方,引入該組件即可。
class CanvasKit {
constructor() {
}
drawImg(option = {}) {
...
return this
}
drawRect(option = {}) {
return this
}
drawText(option = {}) {
...
return this
}
static exportImg(option = {}) {
...
}
}
let drawer = new CanvasKit('canvasId').drawImg(styleObj1).drawText(styleObj2)
drawer.exportImg()
復制代碼
短鏈接
的方式來解決數據統計作為目前一種常用的分析用戶行為的方式,小程序端也是必不可少的。小程序采取的曝光,點擊數據埋點其實和h5原理是一樣的。但是埋點作為一個和業務邏輯不相關的需求,我們如果在每一個點擊事件,每一個生命周期加入各種埋點代碼,則會干擾正常的業務邏輯,和使代碼變的臃腫,筆者提供以下幾種思路來解決數據埋點:
小程序的代碼結構是,每一個 Page 中都有一個 Page 方法,接受一個包含生命周期函數,數據的 業務邏輯對象
包裝這層數據,借助小程序的底層邏輯實現頁面的業務邏輯。通過這個我們可以想到思路,對Page進行一次包裝,篡改它的生命周期和點擊事件,混入埋點代碼,不干擾業務邏輯,只要做一些簡單的配置即可埋點,簡單的代碼實現如下:
代碼僅供理解思路
page = function(params) {
let keys = params.keys()
keys.forEach(v => {
if (v === 'onLoad') {
params[v] = function(options) {
stat() //曝光埋點代碼
params[v].call(this, options)
}
}
else if (v.includes('click')) {
params[v] = funciton(event) {
let data = event.dataset.config
stat(data) // 點擊埋點
param[v].call(this)
}
}
})
}
復制代碼
這種思路不光適用于埋點,也可以用來作全局異常處理,請求的統一處理等場景。
對于特殊的一些業務,我們可以采取 接口埋點
,什么叫接口埋點呢?很多情況下,我們有的api并不是多處調用的,只會在某一個特定的頁面調用,通過這個思路我們可以分析出,該接口被請求,則這個行為被觸發了,則完全可以通過服務端日志得出埋點數據,但是這種方式局限性較大,而且屬于分析結果得出過程,可能存在誤差,但可以作為一種思路了解一下。
微信本身提供的數據分析能力,微信本身提供了常規分析和自定義分析兩種數據分析方式,在小程序后臺配置即可。借助小程序數據助手
這款小程序可以很方便的查看。
目前的前端開發過程,工程化是必不可少的一環,那小程序工程化都需要做些什么呢,先看下目前小程序開發當中存在哪些問題需要解決:
對于目前常用的工程化方案,webpack,rollup,parcel等來看,都常用與單頁應用的打包和處理,而小程序天生是 “多頁應用” 并且存在一些特定的配置。根據要解決的問題來看,無非是文件的編譯,修改,拷貝這些處理,對于這些需求,我們想到基于流的 gulp
非常的適合處理,并且相對于webpack配置多頁應用更加簡單。所以小程序工程化方案推薦使用 gulp
通過 gulp 的 task 實現:
上述實現起來其實并不是很難,但是這樣的話就是一份純粹的 gulp 構建腳本和 約定好的目錄而已,每次都有一個新的小程序都來拷貝這份腳本來處理嗎?顯然不合適,那如何真正的實現 小程序工程化
呢?
我們可能需要一個簡單的腳手架,腳手架需要支持的功能:
微信小程序的框架包含兩部分 View 視圖層、App Service邏輯層。View 層用來渲染頁面結構,AppService 層用來邏輯處理、數據請求、接口調用。
它們在兩個線程里運行。
它們在兩個線程里運行。
它們在兩個線程里運行。
視圖層和邏輯層通過系統層的 JSBridage 進行通信,邏輯層把數據變化通知到視圖層,觸發視圖層頁面更新,視圖層把觸發的事件通知到邏輯層進行業務處理。
補充
視圖層使用 WebView 渲染,iOS 中使用自帶 WKWebView,在 Android 使用騰訊的 x5 內核(基于 Blink)運行。
邏輯層使用在 iOS 中使用自帶的 JSCore 運行,在 Android 中使用騰訊的 x5 內核(基于 Blink)運行。
開發工具使用 nw.js 同時提供了視圖層和邏輯層的運行環境。
在 Mac下 使用 js-beautify 對微信開發工具 @v1.02.1808080代碼批量格式化:
cd /Applications/wechatwebdevtools.app/Contents/Resources/package.nw
find . -type f -name '*.js' -not -path "./node_modules/*" -not -path -exec js-beautify -r -s 2 -p -f '{}' \;
復制代碼
在 js/extensions/appservice/index.js
中找到:
267: function(a, b, c) {
const d = c(8),
e = c(227),
f = c(226),
g = c(228),
h = c(229),
i = c(230);
var j = window.__global.navigator.userAgent,
k = -1 !== j.indexOf('game');
k || i(), window.__global.getNewWeixinJSBridge = (a) => {
const {
invoke: b
} = f(a), {
publish: c
} = g(a), {
subscribe: d,
triggerSubscribeEvent: i
} = h(a), {
on: j,
triggerOnEvent: k
} = e(a);
return {
invoke: b,
publish: c,
subscribe: d,
on: j,
get __triggerOnEvent() {
return k
},
get __triggerSubscribeEvent() {
return i
}
}
}, window.WeixinJSBridge = window.__global.WeixinJSBridge = window.__global.getNewWeixinJSBridge('global'), window.__global.WeixinJSBridgeMap = {
__globalBridge: window.WeixinJSBridge
}, __devtoolsConfig.online && __devtoolsConfig.autoTest && setInterval(() => {
console.clear()
}, 1e4);
try {
var l = new window.__global.XMLHttpRequest;
l.responseType = 'text', l.open('GET', `http://${window.location.host}/calibration/${Date.now()}`, !0), l.send()
} catch (a) {}
}
復制代碼
在 js/extensions/gamenaitveview/index.js
中找到:
299: function(a, b, c) {
'use strict';
Object.defineProperty(b, '__esModule', {
value: !0
});
var d = c(242),
e = c(241),
f = c(243),
g = c(244);
window.WeixinJSBridge = {
on: d.a,
invoke: e.a,
publish: f.a,
subscribe: g.a
}
},
復制代碼
在 js/extensions/pageframe/index.js
中找到:
317: function(a, b, c) {
'use strict';
function d() {
window.WeixinJSBridge = {
on: e.a,
invoke: f.a,
publish: g.a,
subscribe: h.a
}, k.a.init();
let a = document.createEvent('UIEvent');
a.initEvent('WeixinJSBridgeReady', !1, !1), document.dispatchEvent(a), i.a.init()
}
Object.defineProperty(b, '__esModule', {
value: !0
});
var e = c(254),
f = c(253),
g = c(255),
h = c(256),
i = c(86),
j = c(257),
k = c.n(j);
'complete' === document.readyState ? d() : window.addEventListener('load', function() {
d()
})
},
復制代碼
我們都看到了 WeixinJSBridge 的定義。分別都有 on
、invoke
、publish
、subscribe
這個幾個關鍵方法。
拿 invoke
舉例,在 js/extensions/appservice/index.js
中發現這段代碼:
f (!r) p[b] = s, f.send({
command: 'APPSERVICE_INVOKE',
data: {
api: c,
args: e,
callbackID: b
}
});
復制代碼
在 js/extensions/pageframe/index.js
中發現這段代碼:
g[d] = c, e.a.send({
command: 'WEBVIEW_INVOKE',
data: {
api: a,
args: b,
callbackID: d
}
})
復制代碼
簡單的分析得知:字段 command
用來區分行為,invoke
用來調用 Native 的 Api。在不同的來源要使用不同的前綴。data
里面包含 Api 名,參數。另外 callbackID
指定接受回調的方法句柄。Appservice 和 Webview 使用的通信協議是一致的。
我們不能在代碼里使用 BOM 和 DOM 是因為根本沒有,另一方面也不希望 JS 代碼直接操作視圖。
在開發工具中 remote-helper.js
中找到了這樣的代碼:
const vm = require("vm");
const vmGlobal = {
require: undefined,
eval: undefined,
process: undefined,
setTimeout(...args) {
//...省略代碼
return timerCount;
},
clearTimeout(id) {
const timer = timers[id];
if (timer) {
clearTimeout(timer);
delete timers[id];
}
},
setInterval(...args) {
//...省略代碼
return timerCount;
},
clearInterval(id) {
const timer = timers[id];
if (timer) {
clearInterval(timer);
delete timers[id];
}
},
console: (() => {
//...省略代碼
return consoleClone;
})()
};
const jsVm = vm.createContext(vmGlobal);
// 省略大量代碼...
function loadCode(filePath, sourceURL, content) {
let ret;
try {
const script = typeof content === 'string' ? content : fs.readFileSync(filePath, 'utf-8').toString();
ret = vm.runInContext(script, jsVm, {
filename: sourceURL,
});
}
catch (e) {
// something went wrong in user code
console.error(e);
}
return ret;
}
復制代碼
這樣的分層設計顯然是有意為之的,它的中間層完全控制了程序對于界面進行的操作, 同時對于傳遞的數據和響應時間也能做到監控。一方面程序的行為受到了極大限制, 另一方面微信可以確保他們對于小程序內容和體驗有絕對的控制。
這樣的結構也說明了小程序的動畫和繪圖 API 被設計成生成一個最終對象而不是一步一步執行的樣子, 原因就是 Json 格式的數據傳遞和解析相比與原生 API 都是損耗不菲的,如果頻繁調用很可能損耗過多性能,進而影響用戶體驗。
1.動畫需要綁定在 data 上,而繪圖卻不用。你覺得是為什么呢?
var context = wx.createCanvasContext('firstCanvas')
context.setStrokeStyle("#00ff00")
context.setLineWidth(5)
context.rect(0, 0, 200, 200)
context.stroke()
context.setStrokeStyle("#ff0000")
context.setLineWidth(2)
context.moveTo(160, 100)
context.arc(100, 100, 60, 0, 2 * Math.PI, true)
context.moveTo(140, 100)
context.arc(100, 100, 40, 0, Math.PI, false)
context.moveTo(85, 80)
context.arc(80, 80, 5, 0, 2 * Math.PI, true)
context.moveTo(125, 80)
context.arc(120, 80, 5, 0, 2 * Math.PI, true)
context.stroke()
context.draw()
復制代碼
Page({
data: {
animationData: {}
},
onShow: function(){
var animation = wx.createAnimation({
duration: 1000,
timingFunction: 'ease',
})
this.animation = animation
animation.scale(2,2).rotate(45).step()
this.setData({
animationData:animation.export()
})
}
})
復制代碼
2.小程序的 Http Rquest 請求是不是用的瀏覽器 Fetch API?
知識點考察
wx.request
是不是遵循 fetch API 規范實現的呢?答案,顯然不是。因為沒有 Promise
WXML(WeiXin Markup Language)
Wxml編譯器:Wcc 把 Wxml文件 轉為 JS
執行方式:Wcc index.wxml
使用 Virtual DOM,進行局部更新
WXSS(WeiXin Style Sheets)
wxss編譯器:wcsc 把wxss文件轉化為 js
執行方式: wcsc index.wxss
親測包含但不限于如下內容:
建議 Css3 的特性都可以做一下嘗試。
rpx(responsive pixel): 可以根據屏幕寬度進行自適應。規定屏幕寬為 750rpx。公式:
const dsWidth = 750
export const screenHeightOfRpx = function () {
return 750 / env.screenWidth * env.screenHeight
}
export const rpxToPx = function (rpx) {
return env.screenWidth / 750 * rpx
}
export const pxToRpx = function (px) {
return 750 / env.screenWidth * px
}
復制代碼
設備 | rpx換算px (屏幕寬度/750) | px換算rpx (750/屏幕寬度) |
---|---|---|
iPhone5 | 1rpx = 0.42px | 1px = 2.34rpx |
iPhone6 | 1rpx = 0.5px | 1px = 2rpx |
iPhone6 Plus | 1rpx = 0.552px | 1px = 1.81rpx |
可以了解一下 pr2rpx-loader 這個庫。
使用 @import
語句可以導入外聯樣式表,@import
后跟需要導入的外聯樣式表的相對路徑,用 ;
表示語句結束。
靜態的樣式統一寫到 class 中。style 接收動態的樣式,在運行時會進行解析,請盡量避免將靜態的樣式寫進 style 中,以免影響渲染速度。
定義在 app.wxss 中的樣式為全局樣式,作用于每一個頁面。在 page 的 wxss 文件中定義的樣式為局部樣式,只作用在對應的頁面,并會覆蓋 app.wxss 中相同的選擇器。
截止20180810
小程序未來有計劃支持字體。參考微信公開課。
小程序開發與平時 Web開發類似,也可以使用字體圖標,但是 src:url()
無論本地還是遠程地址都不行,base64 值則都是可以顯示的。
將 ttf 文件轉換成 base64。打開這個平臺 transfonter.org/。點擊 Add fonts 按鈕,加載ttf格式的那個文件。將下邊的 base64 encode 改為 on。點擊 Convert 按鈕進行轉換,轉換后點擊 download 下載。
復制下載的壓縮文件中的 stylesheet.css 的內容到 font.wxss ,并且將 icomoon 中的 style.css 除了 @font-face 所有的代碼也復制到 font.wxss 并將i選擇器換成 .iconfont,最后:
<text class="iconfont icon-home" style="font-size:50px;color:red">text>
復制代碼
小程序提供了一系列組件用于開發業務功能,按照功能與HTML5的標簽進行對比如下:
小程序的組件基于Web Component標準
使用Polymer框架實現Web Component
目前Native實現的組件有
cavnas
video
map
textarea
Native組件層在 WebView 層之上。這目前帶來了一些問題:
cover-view
可以覆蓋 cavnas video 等,但是也有一下弊端,比如在 cavnas 上覆蓋 cover-view
,就會發現坐標系不統一處理麻煩截止20180810
包含但不限于:
小程序仍然使用 WebView 渲染,并非原生渲染。(部分原生)
服務端接口返回的頭無法執行,比如:Set-Cookie。
依賴瀏覽器環境的 JS 庫不能使用。
不能使用 npm,但是可以自搭構建工具或者使用 mpvue。(未來官方有計劃支持)
不能使用 ES7,可以自己用babel+webpack自搭或者使用 mpvue。
不支持使用自己的字體(未來官方計劃支持)。
可以用 base64 的方式來使用 iconfont。
小程序不能發朋友圈(可以通過保存圖片到本地,發圖片到朋友前。二維碼可以使用B接口)。
獲取二維碼/小程序接口的限制。
小程序推送只能使用“服務通知” 而且需要用戶主動觸發提交 formId,formId 只有7天有效期。(現在的做法是在每個頁面都放入form并且隱藏以此獲取更多的 formId。后端使用原則為:優先使用有效期最短的)
小程序大小限制 2M,分包總計不超過 8M
轉發(分享)小程序不能拿到成功結果,原來可以。鏈接(小游戲造的孽)
拿到相同的 unionId 必須綁在同一個開放平臺下。開放平臺綁定限制:
公眾號關聯小程序,鏈接
一個公眾號關聯的10個同主體小程序和3個非同主體小程序可以互相跳轉
品牌搜索不支持金融、醫療
小程序授權需要用戶主動點擊
小程序不提供測試 access_token
安卓系統下,小程序授權獲取用戶信息之后,刪除小程序再重新獲取,并重新授權,得到舊簽名,導致第一次授權失敗
開發者工具上,授權獲取用戶信息之后,如果清緩存選擇全部清除,則即使使用了wx.checkSession,并且在session_key有效期內,授權獲取用戶信息也會得到新的session_key
為了驗證小程序對HTTP的支持適配情況,我找了兩個服務器做測試,一個是網上搜索到支持HTTP2的服務器,一個是我本地起的一個HTTP2服務器。測試中所有請求方法均使用 wx.request
。
網上支持HTTP2的服務器:HTTPs://www.snel.com:443
在Chrome上查看該服務器為 HTTP2
在模擬器上請求該接口,請求頭
的HTTP版本為HTTP1.1,模擬器不支持HTTP2
由于小程序線上環境需要在項目管理里配置請求域名,而這個域名不是我們需要的請求域名,沒必要浪費一個域名位置,所以打開不驗證域名,TSL 等選項請求該接口,通過抓包工具表現與模擬器相同
由上可以看出,在真機與模擬器都不支持 HTTP2,但是都是成功請求的,并且 響應頭
里的 HTTP 版本都變成了HTTP1.1 版本,說明服務器對 HTTP1.1 做了兼容性適配。
本地新啟一個 node 服務器,返回 JSON 為請求的 HTTP 版本
如果服務器只支持 HTTP2,在模擬器請求時發生了一個 ALPN
協議的錯誤。并且提醒使用適配 HTTP1
當把服務器的 allowHTTP1
,設置為 true
,并在請求時處理相關相關請求參數后,模擬器能正常訪問接口,并打印出對應的 HTTP 請求版本
面試題:先授權獲取用戶信息再 login 會發生什么?
我們知道view部分是運行在webview上的,所以前端領域的大多數優化方式都有用。
我們知道view部分是運行在webview上的,所以前端領域的大多數優化方式都有用。
我們知道view部分是運行在webview上的,所以前端領域的大多數優化方式都有用。
代碼包的大小是最直接影響小程序加載啟動速度的因素。代碼包越大不僅下載速度時間長,業務代碼注入時間也會變長。所以最好的優化方式就是減少代碼包的大小。
小程序加載的三個階段的表示。
優化方式
首屏加載的體驗優化建議
在構建小程序分包項目時,構建會輸出一個或多個功能的分包,其中每個分包小程序必定含有一個主包,所謂的主包,即放置默認啟動頁面/TabBar 頁面,以及一些所有分包都需用到公共資源/JS 腳本,而分包則是根據開發者的配置進行劃分。
在小程序啟動時,默認會下載主包并啟動主包內頁面,如果用戶需要打開分包內某個頁面,客戶端會把對應分包下載下來,下載完成后再進行展示。
優點:
限制:
原生分包加載的配置 假設支持分包的小程序目錄結構如下:
├── app.js
├── app.json
├── app.wxss
├── packageA
│ └── pages
│ ├── cat
│ └── dog
├── packageB
│ └── pages
│ ├── apple
│ └── banana
├── pages
│ ├── index
│ └── logs
└── utils
復制代碼
開發者通過在 app.json subPackages 字段聲明項目分包結構:
{
"pages":[
"pages/index",
"pages/logs"
],
"subPackages": [
{
"root": "packageA",
"pages": [
"pages/cat",
"pages/dog"
]
}, {
"root": "packageB",
"pages": [
"pages/apple",
"pages/banana"
]
}
]
}
復制代碼
分包原則
引用原則
官方即將推出 分包預加載
獨立分包
每次 setData 的調用都是一次進程間通信過程,通信開銷與 setData 的數據量正相關。
setData 會引發視圖層頁面內容的更新,這一耗時操作一定時間中會阻塞用戶交互。
setData 是小程序開發使用最頻繁,也是最容易引發性能問題的。
避免不當使用 setData
避免不當使用onPageScroll
使用自定義組件
在需要頻繁更新的場景下,自定義組件的更新只在組件內部進行,不受頁面其他部分內容復雜性影響。
小程序的幾個頁面間,存在一些相同或是類似的區域,這時候可以把這些區域邏輯封裝成一個自定義組件,代碼就可以重用,或者對于比較獨立邏輯,也可以把它封裝成一個自定義組件,也就是微信去年發布的自定義組件,它讓代碼得到復用、減少代碼量,更方便模塊化,優化代碼架構組織,也使得模塊清晰,后期更好地維護,從而保證更好的性能。
但微信打算在原來的基礎上推出的自定義組件 2.0,它將擁有更高級的性能:
目前小程序開發的痛點是:開源組件要手動復制到項目,后續更新組件也需要手動操作。不久的將來,小程序將支持npm包管理,有了這