近兩萬字小程式攻略釋出了

yck發表於2018-09-05
近兩萬字小程式攻略釋出了

該內容由銀科控股融匯研發部曹俊及其團隊授權提供。該團隊擁有 10 多名小程式開發,深耕小程式領域,總結出了本篇優質長文。同時本篇內容也已經合併入我的 開源專案 中,目前專案內容包含了 JS、網路、瀏覽器相關、效能優化、安全、框架、Git、資料結構、演算法等內容,無論是基礎還是進階,亦或是原始碼解讀,你都能在本圖譜中得到滿意的答案,希望這個面試圖譜能夠幫助到大家更好的準備面試。

小程式-登入

unionid和openid

瞭解小程式登陸之前,我們寫了解下小程式/公眾號登入涉及到兩個最關鍵的使用者標識:

  • OpenId 是一個使用者對於一個小程式/公眾號的標識,開發者可以通過這個標識識別出使用者。
  • UnionId 是一個使用者對於同主體微信小程式/公眾號/APP的標識,開發者需要在微信開放平臺下繫結相同賬號的主體。開發者可通過UnionId,實現多個小程式、公眾號、甚至APP 之間的資料互通了。

關鍵Api

登入流程設計

以下從筆者接觸過的幾種登入流程來做闡述:

利用現有登入體系

直接複用現有系統的登入體系,只需要在小程式端設計使用者名稱,密碼/驗證碼輸入頁面,便可以簡便的實現登入,只需要保持良好的使用者體驗即可。

利用OpenId 建立使用者體系

?提過,OpenId 是一個小程式對於一個使用者的標識,利用這一點我們可以輕鬆的實現一套基於小程式的使用者體系,值得一提的是這種使用者體系對使用者的打擾最低,可以實現靜默登入。具體步驟如下:

  1. 小程式客戶端通過 wx.login 獲取 code

  2. 傳遞 code 向服務端,服務端拿到 code 呼叫微信登入憑證校驗介面,微信伺服器返回 openid 和會話金鑰 session_key ,此時開發者服務端便可以利用 openid 生成使用者入庫,再向小程式客戶端返回自定義登入態

  3. 小程式客戶端快取 (通過storage)自定義登入態(token),後續呼叫介面時攜帶該登入態作為使用者身份標識即可

利用 Unionid 建立使用者體系

如果想實現多個小程式,公眾號,已有登入系統的資料互通,可以通過獲取到使用者 unionid 的方式建立使用者體系。因為 unionid 在同一開放平臺下的所所有應用都是相同的,通過 unionid 建立的使用者體系即可實現全平臺資料的互通,更方便的接入原有的功能,那如何獲取 unionid 呢,有以下兩種方式:

  1. 如果戶關注了某個相同主體公眾號,或曾經在某個相同主體App、公眾號上進行過微信登入授權,通過 wx.login 可以直接獲取 到 unionid

  2. 結合 wx.getUserInfo<button open-type="getUserInfo"><button/> 這兩種方式引導使用者主動授權,主動授權後通過返回的資訊和服務端互動 (這裡有一步需要服務端解密資料的過程,很簡單,微信提供了示例程式碼) 即可拿到 unionid 建立使用者體系, 然後由服務端返回登入態,本地記錄即可實現登入,附上微信提供的最佳實踐:

    • 呼叫 wx.login 獲取 code,然後從微信後端換取到 session_key,用於解密 getUserInfo返回的敏感資料。

    • 使用 wx.getSetting 獲取使用者的授權情況

      • 如果使用者已經授權,直接呼叫 API wx.getUserInfo 獲取使用者最新的資訊;
      • 使用者未授權,在介面中顯示一個按鈕提示使用者登入,當使用者點選並授權後就獲取到使用者的最新資訊。
    • 獲取到使用者資料後可以進行展示或者傳送給自己的後端。

注意事項

  1. 需要獲取 unionid 形式的登入體系,在以前(18年4月之前)是通過以下這種方式來實現,但後續微信做了調整(因為一進入小程式,主動彈起各種授權彈窗的這種形式,比較容易導致使用者流失),調整為必須使用按鈕引導使用者主動授權的方式,這次調整對開發者影響較大,開發者需要注意遵守微信的規則,並及時和業務方溝通業務形式,不要存在僥倖心理,以防造成小程式不過審等情況。
   wx.login(獲取code) ===> wx.getUserInfo(使用者授權) ===> 獲取 unionid
複製程式碼
  1. 因為小程式不存在 cookie 的概念, 登入態必須快取在本地,因此強烈建議為登入態設定過期時間

  2. 值得一提的是如果需要支援風控安全校驗,多平臺登入等功能,可能需要加入一些公共引數,例如platform,channel,deviceParam等引數。在和服務端確定方案時,作為前端同學應該及時提出這些合理的建議,設計合理的系統。

  3. openidunionid 不要在介面中明文傳輸,這是一種危險的行為,同時也很不專業。

小程式-圖片匯出

經常開發和使用小程式的同學對這個功能一定不陌生,這是一種常見的引流方式,一般同時會在圖片中附加一個小程式二維碼。

基本原理

  1. 藉助 canvas 元素,將需要匯出的樣式首先在 canvas 畫布上繪製出來 (api基本和h5保持一致,但有輕微差異,使用時注意即可)

  2. 藉助微信提供的 canvasToTempFilePath 匯出圖片,最後再使用 saveImageToPhotosAlbum (需要授權)儲存圖片到本地

如何優雅實現

根據上述的原理來看,實現是很簡單的,只不過就是設計稿的提取,繪製即可,但是作為一個常用功能,每次都這樣寫一坨程式碼豈不是非常的難受。那小程式如何設計一個通用的方法來幫助我們匯出圖片呢?思路如下:

  1. 繪製出需要的樣式這一步是省略不掉的。但是我們可以封裝一個繪製庫,包含常見圖形的繪製,例如矩形,圓角矩形,圓, 扇形, 三角形, 文字,圖片減少繪製程式碼,只需要提煉出樣式資訊,便可以輕鬆的繪製,最後匯出圖片存入相簿。筆者覺得以下這種方式繪製更為優雅清晰一些,其實也可以使用加入一個type引數來指定繪製型別,傳入的一個是樣式陣列,實現繪製。

  2. 結合上一步的實現,如果對於同一型別的卡片有多次匯出需求的場景,也可以使用自定義元件的方式,封裝同一型別的卡片為一個通用元件,在需要匯出圖片功能的地方,引入該元件即可。

    
  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()

複製程式碼

注意事項

  1. 小程式中無法繪製網路圖片到canvas上,需要通過downLoadFile 先下載圖片到本地臨時檔案才可以繪製
  2. 通常需要繪製二維碼到匯出的圖片上,有一種方式匯出二維碼時,需要攜帶的引數必須做編碼,而且有具體的長度(32可見字元)限制,可以藉助服務端生成 短連結 的方式來解決

小程式-資料統計

資料統計作為目前一種常用的分析使用者行為的方式,小程式端也是必不可少的。小程式採取的曝光,點選資料埋點其實和h5原理是一樣的。但是埋點作為一個和業務邏輯不相關的需求,我們如果在每一個點選事件,每一個生命週期加入各種埋點程式碼,則會干擾正常的業務邏輯,和使程式碼變的臃腫,筆者提供以下幾種思路來解決資料埋點:

設計一個埋點sdk

小程式的程式碼結構是,每一個 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並不是多處呼叫的,只會在某一個特定的頁面呼叫,通過這個思路我們可以分析出,該介面被請求,則這個行為被觸發了,則完全可以通過服務端日誌得出埋點資料,但是這種方式侷限性較大,而且屬於分析結果得出過程,可能存在誤差,但可以作為一種思路瞭解一下。

微信自定義資料分析

微信本身提供的資料分析能力,微信本身提供了常規分析和自定義分析兩種資料分析方式,在小程式後臺配置即可。藉助小程式資料助手這款小程式可以很方便的檢視。

小程式-工程化

工程化做什麼

目前的前端開發過程,工程化是必不可少的一環,那小程式工程化都需要做些什麼呢,先看下目前小程式開發當中存在哪些問題需要解決:

  1. 不支援 css預編譯器,作為一種主流的 css解決方案,不論是 less,sass,stylus 都可以提升css效率
  2. 不支援引入npm包 (這一條,從微信公開課中聽聞,微信準備支援)
  3. 不支援ES7等後續的js特性,好用的async await等特性都無法使用
  4. 不支援引入外部字型檔案,只支援base64
  5. 沒有 eslint 等程式碼檢查工具

方案選型

對於目前常用的工程化方案,webpack,rollup,parcel等來看,都常用與單頁應用的打包和處理,而小程式天生是 “多頁應用” 並且存在一些特定的配置。根據要解決的問題來看,無非是檔案的編譯,修改,拷貝這些處理,對於這些需求,我們想到基於流的 gulp非常的適合處理,並且相對於webpack配置多頁應用更加簡單。所以小程式工程化方案推薦使用 gulp

具體開發思路

通過 gulp 的 task 實現:

  1. 實時編譯 less 檔案至相應目錄
  2. 引入支援async,await的執行時檔案
  3. 編譯字型檔案為base64 並生成相應css檔案,方便使用
  4. 依賴分析哪些地方引用了npm包,將npm包打成一個檔案,拷貝至相應目錄
  5. 檢查程式碼規範

上述實現起來其實並不是很難,但是這樣的話就是一份純粹的 gulp 構建指令碼和 約定好的目錄而已,每次都有一個新的小程式都來拷貝這份指令碼來處理嗎?顯然不合適,那如何真正的實現 小程式工程化 呢? 我們可能需要一個簡單的腳手架,腳手架需要支援的功能:

  1. 支援新建專案,建立Page,建立Component
  2. 支援內建構建指令碼
  3. 支援釋出小程式,也可以想辦法接入Jenkins等工具做持續整合 (小程式持續整合後面會提) ...

小程式架構

architecture

微信小程式的框架包含兩部分 View 檢視層、App Service邏輯層。View 層用來渲染頁面結構,AppService 層用來邏輯處理、資料請求、介面呼叫。

它們在兩個執行緒裡執行。

它們在兩個執行緒裡執行。

它們在兩個執行緒裡執行。

檢視層和邏輯層通過系統層的 JSBridage 進行通訊,邏輯層把資料變化通知到檢視層,觸發檢視層頁面更新,檢視層把觸發的事件通知到邏輯層進行業務處理。

補充

one-context

檢視層使用 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 的定義。分別都有 oninvokepublishsubscribe 這個幾個關鍵方法。

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 都是損耗不菲的,如果頻繁呼叫很可能損耗過多效能,進而影響使用者體驗。

下載小程式完整包

download

App Service - Life Cylce

lifecycle

面試題

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?

知識點考察

  • 知道 Request 是由 Native 實現的
  • JSCore 是不帶 Http Request、Websocket、Storage等功能的,那是 Webkit 帶的
  • 小程式的 wx.request 是不是遵循 fetch API 規範實現的呢?答案,顯然不是。因為沒有 Promise

View - WXML

WXML(WeiXin Markup Language)

  • 支援資料繫結
  • 支援邏輯算術、運算
  • 支援模板、引用
  • 支援新增事件(bindtap)

WXML

Wxml編譯器:Wcc 把 Wxml檔案 轉為 JS

執行方式:Wcc index.wxml

使用 Virtual DOM,進行區域性更新

View - WXSS

WXSS(WeiXin Style Sheets)

WXSS

wxss編譯器:wcsc 把wxss檔案轉化為 js

執行方式: wcsc index.wxss

支援大部分CSS特性

親測包含但不限於如下內容:

  • Transition
  • Animation
    • Keyframes
  • border-radius
  • calc()
  • 選擇器,除了官方文件列出的,其實還支援
    • element>element
    • element+element
    • element element
    • element:first-letter
    • element:first-line
    • element:first-child
    • element:last-child
    • element~element
    • element:first-of-type
    • element:last-of-type
    • element:only-of-type
    • element:only-child
    • element:nth-child(n)
    • element:nth-last-child(n)
    • element:nth-of-type(n)
    • element:nth-last-of-type(n)
    • :root
    • element:empty
    • :not(element)
  • iconfont

建議 Css3 的特性都可以做一下嘗試。

尺寸單位 rpx

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 中相同的選擇器。

iconfont

截止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>
複製程式碼

View - Component

小程式提供了一系列元件用於開發業務功能,按照功能與HTML5的標籤進行對比如下:

Component

小程式的元件基於Web Component標準

使用Polymer框架實現Web Component

View - Native Component

目前Native實現的元件有

  • cavnas

  • video

  • map

  • textarea

    Native Component

Native元件層在 WebView 層之上。這目前帶來了一些問題:

  • Native 實現的元件會遮擋其他元件
  • WebView 渲染出來的檢視在滾動時,Native 實現的元件需要更新位置,這會帶來效能問題,在安卓機器上比較明顯
  • 小程式原生元件 cover-view 可以覆蓋 cavnas video 等,但是也有一下弊端,比如在 cavnas 上覆蓋 cover-view,就會發現座標系不統一處理麻煩

目前小程式的問題或限制

截止20180810

包含但不限於:

  • 小程式仍然使用 WebView 渲染,並非原生渲染。(部分原生)

  • 服務端介面返回的頭無法執行,比如:Set-Cookie。

  • 依賴瀏覽器環境的 JS 庫不能使用。

  • 不能使用 npm,但是可以自搭構建工具或者使用 mpvue。(未來官方有計劃支援)

  • 不能使用 ES7,可以自己用babel+webpack自搭或者使用 mpvue。

  • 不支援使用自己的字型(未來官方計劃支援)。

  • 可以用 base64 的方式來使用 iconfont。

  • 小程式不能發朋友圈(可以通過儲存圖片到本地,發圖片到朋友前。二維碼可以使用B介面)。

  • 獲取二維碼/小程式介面的限制。

    • B 介面 scene 最大32個可見字元。
    • AC 介面總共生成的碼數量限制為 100,000,請謹慎呼叫。
    • 真機掃描二維碼只能跳轉到線上版本,所以測試環境下只可通過開發者工具的通過二維碼編譯進行除錯。
    • 沒有釋出到線上版本的小程式頁面路徑會導致生成二維碼失敗,需要先將新增了頁面的小程式釋出到線上版本。
  • 小程式推送只能使用“服務通知” 而且需要使用者主動觸發提交 formId,formId 只有7天有效期。(現在的做法是在每個頁面都放入form並且隱藏以此獲取更多的 formId。後端使用原則為:優先使用有效期最短的)

  • 小程式大小限制 2M,分包總計不超過 8M

  • 轉發(分享)小程式不能拿到成功結果,原來可以。連結(小遊戲造的孽)

  • 拿到相同的 unionId 必須綁在同一個開放平臺下。開放平臺繫結限制:

    • 50個移動應用
    • 10個網站
    • 50個同主體公眾號
    • 5個不同主體公眾號
    • 50個同主體小程式
    • 5個不同主體小程式
  • 公眾號關聯小程式,連結

    • 所有公眾號都可以關聯小程式。
    • 一個公眾號可關聯10個同主體的小程式,3個不同主體的小程式。
    • 一個小程式可關聯500個公眾號。
    • 公眾號一個月可新增關聯小程式13次,小程式一個月可新增關聯500次。
  • 一個公眾號關聯的10個同主體小程式和3個非同主體小程式可以互相跳轉

  • 品牌搜尋不支援金融、醫療

  • 小程式授權需要使用者主動點選

  • 小程式不提供測試 access_token

  • 安卓系統下,小程式授權獲取使用者資訊之後,刪除小程式再重新獲取,並重新授權,得到舊簽名,導致第一次授權失敗

  • 開發者工具上,授權獲取使用者資訊之後,如果清快取選擇全部清除,則即使使用了wx.checkSession,並且在session_key有效期內,授權獲取使用者資訊也會得到新的session_key

小程式HTTP2支援情況

HTTP2支援情況:模擬器與真機均不支援

為了驗證小程式對HTTP的支援適配情況,我找了兩個伺服器做測試,一個是網上搜尋到支援HTTP2的伺服器,一個是我本地起的一個HTTP2伺服器。測試中所有請求方法均使用 wx.request

  1. 網上支援HTTP2的伺服器:HTTPs://www.snel.com:443

  2. 在Chrome上檢視該伺服器為 HTTP2

    WechatIMG11

  3. 在模擬器上請求該介面,請求頭的HTTP版本為HTTP1.1,模擬器不支援HTTP2

    WechatIMG12

  4. 由於小程式線上環境需要在專案管理裡配置請求域名,而這個域名不是我們需要的請求域名,沒必要浪費一個域名位置,所以開啟不驗證域名,TSL 等選項請求該介面,通過抓包工具表現與模擬器相同

    WechatIMG14

HTTP2伺服器需要對小程式做相容性適配

由上可以看出,在真機與模擬器都不支援 HTTP2,但是都是成功請求的,並且 響應頭 裡的 HTTP 版本都變成了HTTP1.1 版本,說明伺服器對 HTTP1.1 做了相容性適配。

  1. 本地新啟一個 node 伺服器,返回 JSON 為請求的 HTTP 版本

    WechatIMG16

  2. 如果伺服器只支援 HTTP2,在模擬器請求時發生了一個 ALPN 協議的錯誤。並且提醒使用適配 HTTP1

    WechatIMG8

  3. 當把伺服器的 allowHTTP1,設定為 true,並在請求時處理相關相關請求引數後,模擬器能正常訪問介面,並列印出對應的 HTTP 請求版本

    WechatIMG15

授權獲取使用者資訊流程

近兩萬字小程式攻略釋出了
  • session_key 有有效期,有效期並沒有被告知開發者,只知道使用者越頻繁使用小程式,session_key 有效期越長
  • 在呼叫 wx.login 時會直接更新 session_key,導致舊 session_key 失效
  • 小程式內先呼叫 wx.checkSession 檢查登入態,並保證沒有過期的 session_key 不會被更新,再呼叫 wx.login 獲取 code。接著使用者授權小程式獲取使用者資訊,小程式拿到加密後的使用者資料,把加密資料和 code 傳給後端服務。後端通過 code 拿到 session_key 並解密資料,將解密後的使用者資訊返回給小程式

面試題:先授權獲取使用者資訊再 login 會發生什麼?

近兩萬字小程式攻略釋出了 近兩萬字小程式攻略釋出了
  • 使用者授權時,開放平臺使用舊的 session_key 對使用者資訊進行加密。呼叫 wx.login 重新登入,會重新整理 session_key,這時後端服務從開放平臺獲取到新 session_key,但是無法對老 session_key 加密過的資料解密,使用者資訊獲取失敗
  • 在使用者資訊授權之前先呼叫 wx.checkSession 呢?wx.checkSession 檢查登入態,並且保證 wx.login 不會重新整理 session_key,從而讓後端服務正確解密資料。但是這裡存在一個問題,如果小程式較長時間不用導致 session_key 過期,則 wx.login 必定會重新生成 session_key,從而再一次導致使用者資訊解密失敗。

效能優化

我們知道view部分是執行在webview上的,所以前端領域的大多數優化方式都有用。

我們知道view部分是執行在webview上的,所以前端領域的大多數優化方式都有用。

我們知道view部分是執行在webview上的,所以前端領域的大多數優化方式都有用。

載入優化

preload

程式碼包的大小是最直接影響小程式載入啟動速度的因素。程式碼包越大不僅下載速度時間長,業務程式碼注入時間也會變長。所以最好的優化方式就是減少程式碼包的大小。

load-time-series

小程式載入的三個階段的表示。

優化方式

  • 程式碼壓縮。
  • 及時清理無用程式碼和資原始檔。
  • 減少程式碼包中的圖片等資原始檔的大小和數量。
  • 分包載入。

首屏載入的體驗優化建議

  • 提前請求: 非同步資料請求不需要等待頁面渲染完成。
  • 利用快取: 利用 storage API 對非同步請求資料進行快取,二次啟動時先利用快取資料渲染頁面,在進行後臺更新。
  • 避免白屏:先展示頁面骨架頁和基礎內容。
  • 及時反饋:即時地對需要使用者等待的互動操作給出反饋,避免使用者以為小程式無響應。

使用分包載入優化

sub-package

在構建小程式分包專案時,構建會輸出一個或多個功能的分包,其中每個分包小程式必定含有一個主包,所謂的主包,即放置預設啟動頁面/TabBar 頁面,以及一些所有分包都需用到公共資源/JS 指令碼,而分包則是根據開發者的配置進行劃分。

在小程式啟動時,預設會下載主包並啟動主包內頁面,如果使用者需要開啟分包內某個頁面,客戶端會把對應分包下載下來,下載完成後再進行展示。

優點:

  • 對開發者而言,能使小程式有更大的程式碼體積,承載更多的功能與服務
  • 對使用者而言,可以更快地開啟小程式,同時在不影響啟動速度前提下使用更多功能

限制:

  • 整個小程式所有分包大小不超過 8M
  • 單個分包/主包大小不能超過 2M

原生分包載入的配置 假設支援分包的小程式目錄結構如下:

├── 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"
      ]
    }
  ]
}

複製程式碼

分包原則

  • 宣告 subPackages 後,將按 subPackages 配置路徑進行打包,subPackages 配置路徑外的目錄將被打包到 app(主包) 中
  • app(主包)也可以有自己的 pages(即最外層的 pages 欄位
  • subPackage 的根目錄不能是另外一個 subPackage 內的子目錄
  • 首頁的 TAB 頁面必須在 app(主包)內

引用原則

  • packageA 無法 require packageB JS 檔案,但可以 require app、自己 package 內的 JS 檔案
  • packageA 無法 import packageB 的 template,但可以 require app、自己 package 內的 template
  • packageA 無法使用 packageB 的資源,但可以使用 app、自己 package 內的資源

官方即將推出 分包預載入

preload-sub-package

獨立分包

single-sub-package

渲染效能優化

render

  • 每次 setData 的呼叫都是一次程式間通訊過程,通訊開銷與 setData 的資料量正相關。

  • setData 會引發檢視層頁面內容的更新,這一耗時操作一定時間中會阻塞使用者互動。

  • setData 是小程式開發使用最頻繁,也是最容易引發效能問題的。

避免不當使用 setData

  • 使用 data 在方法間共享資料,可能增加 setData 傳輸的資料量。。data 應僅包括與頁面渲染相關的資料。
  • 使用 setData 傳輸大量資料,**通訊耗時與資料正相關,頁面更新延遲可能造成頁面更新開銷增加。**僅傳輸頁面中發生變化的資料,使用 setData 的特殊 key 實現區域性更新。
  • 短時間內頻繁呼叫 setData,**操作卡頓,互動延遲,阻塞通訊,頁面渲染延遲。**避免不必要的 setData,對連續的setData呼叫進行合併。
  • 在後臺頁面進行 setData,**搶佔前臺頁面的渲染資源。**頁面切入後臺後的 setData 呼叫,延遲到頁面重新展示時執行。

one-context

避免不當使用onPageScroll

  • 只在有必要的時候監聽 pageScroll 事件。不監聽,則不會派發。
  • 避免在 onPageScroll 中執行復雜邏輯
  • 避免在 onPageScroll 中頻繁呼叫 setData
  • 避免滑動時頻繁查詢節點資訊(SelectQuery)用以判斷是否顯示,部分場景建議使用節點佈局橡膠狀態監聽(inersectionObserver)替代

使用自定義元件

在需要頻繁更新的場景下,自定義元件的更新只在元件內部進行,不受頁面其他部分內容複雜性影響。

官方小程式技術能力規劃

自定義元件2.0

小程式的幾個頁面間,存在一些相同或是類似的區域,這時候可以把這些區域邏輯封裝成一個自定義元件,程式碼就可以重用,或者對於比較獨立邏輯,也可以把它封裝成一個自定義元件,也就是微信去年釋出的自定義元件,它讓程式碼得到複用、減少程式碼量,更方便模組化,優化程式碼架構組織,也使得模組清晰,後期更好地維護,從而保證更好的效能。

但微信打算在原來的基礎上推出的自定義元件 2.0,它將擁有更高階的效能:

  • usingComponents 計劃支援全域性定義和萬用字元定義:這意味著不用在每個頁面反覆定義,可以批量匯入目錄下的所有自定義元件
  • 計劃支援類似 Computed 和 watch 的功能,它能使程式碼邏輯更清晰
  • 計劃支援 Component 構造器外掛,在例項化一個自定義元件的時候,允許你在構造器的這個階段,加入一些邏輯,方便進行一些擴充套件,甚至是可以擴充套件成 Vue 的語法

npm支援

目前小程式開發的痛點是:開源元件要手動複製到專案,後續更新元件也需要手動操作。不久的將來,小程式將支援npm包管理,有了這個之後,想要引入一些開源的專案就變得很簡單了,只要在專案裡面宣告,然後用簡單的命令安裝,就可以使用了。

官方自定義元件

微信小程式團隊表示,他們在考慮推出一些官方自定義元件,為什麼不內建到基礎庫裡呢?因為內建元件要提供給開發者,這個元件一定是開發者很難實現或者是無法實現的一個能力。所以他們更傾向於封裝成自定義元件,想基於這些內建元件裡,封裝一些比較常見的、互動邏輯比較複雜的元件給大家使用,讓大家更容易開發。類似彈幕元件,開發者就不用關注彈幕怎麼飄,可以節省開發者的開發成本。

同時,他們也想給開發者提供一些規範和一些模板,讓開發者設計出好用的自定義元件,更好地被大家去使用。

新增體驗評分

當小程式載入太慢時,可能會導致使用者的流失,而小程式的開發者可能會面臨著不知道如何定位問題或不知如何解決問題的困境。

為此,小程式即將推出一個體驗評分的功能,這是為了幫助開發者可以檢查出小程式有一些什麼體驗不好的地方,也會同時給出一份優化的指引建議。

原生元件同層渲染

小程式在最初的技術選型時,引入了原生元件的概念,因為原生元件可以使小程式的能力更加豐富,比如地圖、音視訊的能力,但是原生元件是由客戶端原生渲染的,導致了原生元件的層級是最高的,開發者很容易遇到開啟除錯的問題,發現視訊元件擋在了 vConsole 上。

為了解決這個問題,當時微信做了一個過渡的方案:cover-view。cover-view可以覆蓋在原生元件之上,這一套方案解決了大部分的需求場景。比如說視訊元件上很多的按鈕、標題甚至還有動畫的彈幕,這些都是用 cover-view 去實現的,但它還是沒有完全解決原生元件的開發體驗問題,因為 cover-view 有一些限制:

  • 無法與其他元件混在一起渲染
  • 沒有完整的觸控事件
  • cover-view 對樣式的表現有差異
  • cover-view 對樣式的支援度不夠高

因此微信決定將用同層渲染取代 cover-view,它能像普通元件一樣使用,原生元件的層級不再是最高,而是和其他的非原生元件在同一層級渲染,可完全由 z-index 控制,可完全支援觸控事件。

微信表示,同層渲染在 iOS 平臺小程式上已經開始內測,會很快開放給開發者,Android 平臺已經取得突破性進展,目前正在做一輪封裝的工作,開放指日可待。

wepy vs mpvue

資料流管理

相比傳統的小程式框架,這個一直是我們作為資深開發者比較期望去解決的,在 Web 開發中,隨著 Flux、Redux、Vuex 等多個資料流工具出現,我們也期望在業務複雜的小程式中使用。

  • WePY 預設支援 Redux,在腳手架生成專案的時候可以內建

  • Mpvue 作為 Vue 的移植版本,當然支援 Vuex,同樣在腳手架生成專案的時候可以內建

元件化

如果你和我們一樣,經歷了從無到有的小程式業務開發,建議閱讀【小程式的元件化開發】章節,進行官方語法的元件庫開發(從基礎庫 1.6.3 開始,官方提供了元件化解決方案)。

  • WePY 類似 Vue 實現了單檔案元件,最大的差別是檔案字尾 .wpy,只是寫法上會有差異,具體可以檢視【主流框架使用案例 1:WePY】章節,學習起來有一定成本,不過也會很快適應:
export default class Index extends wepy.page {}
複製程式碼
  • Mpvue 作為 Vue 的移植版本,支援單檔案元件,template、script 和 style 都在一個 .vue 檔案中,和 vue 的寫法類似,所以對 Vue 開發熟悉的同學會比較適應。

工程化

所有的小程式開發依賴官方提供的開發者工具。開發者工具簡單直觀,對除錯小程式很有幫助,現在也支援騰訊雲(目前我們還沒有使用,但是對新的一些開發者還是有幫助的),可以申請測試報告檢視小程式在真實的移動裝置上執行效能和執行效果,但是它本身沒有類似前端工程化中的概念和工具。

  • wepy 內建了構建,通過 wepy init 命令初始化專案,大致流程如下:
  • wepy-cli 會判斷模版是在遠端倉庫還是在本地,如果在本地則會立即跳到第 3 步,反之繼續進行。
  • 會從遠端倉庫下載模版,並儲存到本地。
  • 詢問開發者 Project name 等問題,依據開發者的回答,建立專案。
  • mpvue 沿用了 vue 中推崇的 webpack 作為構建工具,但同時提供了一些自己的外掛以及配置檔案的一些修改,比如:
  • 不再需要 html-webpack-plugin
  • 基於 webpack-dev-middleware 修改成 webpack-dev-middleware-hard-disk
  • 最大的變化是基於 webpack-loader 修改成 mpvue-loader
  • 但是配置方式還是類似,分環境配置檔案,最終都會編譯成小程式支援的目錄結構和檔案字尾。

綜合比較

對比\框架 微信小程式 mpvue wepy
語法規範 小程式開發規範 vue.js 類vue.js
標籤集合 小程式 htm l + 小程式 小程式
樣式規範 wxss sass,less,postcss sass,less,styus
元件化 無元件化機制 vue規範 自定義元件規範
多段複用 不可複用 支援h5 支援h5
自動構建 無自動構建 webpack 框架內建
上手成本 全新學習 vue 學習 vue 和 wepy
資料管理 不支援 vuex redux

選型的個人看法

先說結論:選擇 mpvue。

wepy vs mpvue。

理由:

工程化 原生開發因為不帶工程化,諸如NPM包(未來會引入)、ES7、圖片壓縮、PostCss、pug、ESLint等等不能用。如果自己要搭工程化,不如直接使用wepy或mpvue。mpvue和wepy都可以和小程式原生開發混寫。參考mpvue-echart參考wepy。 而問題在於wepy沒有引入webpack(wepy@2.0.x依然沒有引入),以上說的這些東西都要造輪子(作者造或自己造)。沒有引入 Webpack 是一個重大的硬傷。社群維護的成熟 Webpack 顯然更穩定,輪子更多。

維護 wepy 也是社群維護的,是官方的?其實 wepy 的主要開發者只有作者一人,附上一個contrubutors連結。另外被官方招安了也是後來的事,再說騰訊要有精力幫著一起維護好 wepy,為什麼不花精力在小程式原生開發上呢?再來看看 mpvue,是美團一個前端小組維護的。

學習成本 Vue 的學習曲線比較平緩。mpvue 是 Vue的子集。所以 mpvue 的學習成本會低於 wepy。尤其是之前技術棧有學過用過 Vue 的。

未來規劃 mpvue 已經支援 web 和小程式。因為 mpvue 基於AST,所以未來可以支援支付寶小程式和快應用。他們也是有這樣的規劃。

請在需求池下面自己找

mpvue-feature

兩者都有各自的坑。但是我覺得有一些wepy的坑是沒法容忍的。比如repeat組建裡面用computed得到的列表全是同一套資料而且1.x是沒法解決的。 wepy和mpvue我都開發過完整小程式的體驗下,我覺得wepy的坑更多,而且wepy有些坑礙於架構設計沒辦法解決。

mpvue

Vue.js 小程式版, fork 自 vuejs/vue@2.4.1,保留了 vue runtime 能力,新增了小程式平臺的支援。 mpvue 是一個使用 Vue.js 開發小程式的前端框架。框架基於 Vue.js 核心,mpvue 修改了 Vue.js 的 runtime 和 compiler 實現,使其可以執行在小程式環境中,從而為小程式開發引入了整套 Vue.js 開發體驗。

框架原理

兩個大方向

  • 通過mpvue提供 mp 的 runtime 適配小程式
  • 通過mpvue-loader產出微信小程式所需要的檔案結構和模組內容。

七個具體問題

要了解 mpvue 原理必然要了解 Vue 原理,這是大前提。但是要講清楚 Vue 原理需要花費大量的篇幅,不如參考learnVue

現在假設您對 Vue 原理有個大概的瞭解。

由於 Vue 使用了 Virtual DOM,所以 Virtual DOM 可以在任何支援 JavaScript 語言的平臺上操作,譬如說目前 Vue 支援瀏覽器平臺或 weex,也可以是 mp(小程式)。那麼最後 Virtual DOM 如何對映到真實的 DOM 節點上呢?vue為平臺做了一層適配層,瀏覽器平臺見 runtime/node-ops.js、weex平臺見runtime/node-ops.js,小程式見runtime/node-ops.js。不同平臺之間通過適配層對外提供相同的介面,Virtual DOM進行操作Real DOM節點的時候,只需要呼叫這些適配層的介面即可,而內部實現則不需要關心,它會根據平臺的改變而改變。

所以思路肯定是往增加一個 mp 平臺的 runtime 方向走。但問題是小程式不能操作 DOM,所以 mp 下的node-ops.js 裡面的實現都是直接 return obj

新 Virtual DOM 和舊 Virtual DOM 之間需要做一個 patch,找出 diff。patch完了之後的 diff 怎麼更新檢視,也就是如何給這些 DOM 加入 attr、class、style 等 DOM 屬性呢? Vue 中有 nextTick 的概念用以更新檢視,mpvue這塊對於小程式的 setData 應該怎麼處理呢?

另外個問題在於小程式的 Virtual DOM 怎麼生成?也就是怎麼將 template 編譯成render function。這當中還涉及到執行時-編譯器-vs-只包含執行時,顯然如果要提高效能、減少包大小、輸出 wxml、mpvue 也要提供預編譯的能力。因為要預輸出 wxml 且沒法動態改變 DOM,所以動態元件,自定義 render,和<script type="text/x-template"> 字串模版等都不支援(參考)。

另外還有一些其他問題,最後總結一下

  • 1.如何預編譯生成render function
  • 2.如何預編譯生成 wxml,wxss,wxs
  • 3.如何 patch 出 diff
  • 4.如何更新檢視
  • 5.如何建立小程式事件代理機制,在事件代理函式中觸發與之對應的vue元件事件響應
  • 6.如何建立vue例項與小程式 Page 例項關聯
  • 7.如何建立小程式和vue生命週期對映關係,能在小程式生命週期中觸發vue生命週期

platform/mp的目錄結構

.
├── compiler //解決問題1,mpvue-template-compiler原始碼部分
├── runtime //解決問題3 4 5 6 7
├── util //工具方法
├── entry-compiler.js //mpvue-template-compiler的入口。package.json相關命令會自動生成mpvue-template-compiler這個package。
├── entry-runtime.js //對外提供Vue物件,當然是mpvue
└── join-code-in-build.js //編譯出SDK時的修復
複製程式碼

後面的內容逐步解答這幾個問題,也就弄明白了原理

mpvue-loader

mpvue-loadervue-loader 的一個擴充套件延伸版,類似於超集的關係,除了vue-loader 本身所具備的能力之外,它還會利用mpvue-template-compiler生成render function

  • entry

它會從 webpack 的配置中的 entry 開始,分析依賴模組,並分別打包。在entry 中 app 屬性及其內容會被打包為微信小程式所需要的 app.js/app.json/app.wxss,其餘的會生成對應的頁面page.js/page.json/page.wxml/page.wxss,如示例的 entry 將會生成如下這些檔案,檔案內容下文慢慢講來:

// webpack.config.js
{
    // ...
    entry: {
        app: resolve('./src/main.js'),               // app 欄位被識別為 app 型別
        index: resolve('./src/pages/index/main.js'),   // 其餘欄位被識別為 page 型別
        'news/home': resolve('./src/pages/news/home/index.js')
    }
}

// 產出檔案的結構
.
├── app.js
├── app.json
├──· app.wxss
├── components
│   ├── card$74bfae61.wxml
│   ├── index$023eef02.wxml
│   └── news$0699930b.wxml
├── news
│   ├── home.js
│   ├── home.wxml
│   └── home.wxss
├── pages
│   └── index
│       ├── index.js
│       ├── index.wxml
│       └── index.wxss
└── static
    ├── css
    │   ├── app.wxss
    │   ├── index.wxss
    │   └── news
    │       └── home.wxss
    └── js
        ├── app.js
        ├── index.js
        ├── manifest.js
        ├── news
        │   └── home.js
        └── vendor.js
複製程式碼
  • wxml 每一個 .vue 的元件都會被生成為一個 wxml 規範的 template,然後通過 wxml 規範的 import 語法來達到一個複用,同時元件如果涉及到 props 的 data 資料,我們也會做相應的處理,舉個實際的例子:
<template>
    <div class="my-component" @click="test">
        <h1>{{msg}}</h1>
        <other-component :msg="msg"></other-component>
    </div>
</template>
<script>
import otherComponent from './otherComponent.vue'

export default {
  components: { otherComponent },
  data () {
    return { msg: 'Hello Vue.js!' }
  },
  methods: {
    test() {}
  }
}
</script>
複製程式碼

這樣一個 Vue 的元件的模版部分會生成相應的 wxml

<import src="components/other-component$hash.wxml" />
<template name="component$hash">
    <view class="my-component" bindtap="handleProxy">
        <view class="_h1">{{msg}}</view>
        <template is="other-component$hash" wx:if="{{ $c[0] }}" data="{{ ...$c[0] }}"></template>
    </view>
</template>
複製程式碼

可能已經注意到了 other-component(:msg="msg") 被轉化成了 。mpvue 在執行時會從根元件開始把所有的元件例項資料合併成一個樹形的資料,然後通過 setData 到 appData,$c是 $children 的縮寫。至於那個 0 則是我們的 compiler 處理過後的一個標記,會為每一個子元件打一個特定的不重複的標記。 樹形資料結構如下:

// 這兒資料結構是一個陣列,index 是動態的
{
  $child: {
    '0'{
      // ... root data
      $child: {
        '0': {
          // ... data
          msg: 'Hello Vue.js!',
          $child: {
            // ...data
          }
        }
      }
    }
  }
}
複製程式碼
  • wxss

這個部分的處理同 web 的處理差異不大,唯一不同在於通過配置生成 .css 為 .wxss ,其中的對於 css 的若干處理,在 postcss-mpvue-wxss 和 px2rpx-loader 這兩部分的文件中又詳細的介紹。

app.json/page.json 1.1.1 以上

推薦和小程式一樣,將 app.json/page.json 放到頁面入口處,使用 copy-webpack-plugin copy 到對應的生成位置。

1.1.1 以下

這部分內容來源於 app 和 page 的 entry 檔案,通常習慣是 main.js,你需要在你的入口檔案中 export default { config: {} },這才能被我們的 loader 識別為這是一個配置,需要寫成 json 檔案。

import Vue from 'vue';
import App from './app';

const vueApp = new Vue(App);
vueApp.$mount();

// 這個是我們約定的額外的配置
export default {
    // 這個欄位下的資料會被填充到 app.json / page.json
    config: {
        pages: ['static/calendar/calendar', '^pages/list/list'], // Will be filled in webpack
        window: {
            backgroundTextStyle: 'light',
            navigationBarBackgroundColor: '#455A73',
            navigationBarTitleText: '美團汽車票',
            navigationBarTextStyle: '#fff'
        }
    }
};
複製程式碼

同時,這個時候,我們會根據 entry 的頁面資料,自動填充到 app.json 中的 pages 欄位。 pages 欄位也是可以自定義的,約定帶有 ^ 符號開頭的頁面,會放到陣列的最前面。

style scoped 在 vue-loader 中對 style scoped 的處理方式是給每個樣式加一個 attr 來標記 module-id,然後在 css 中也給每條 rule 後新增 [module-id],最終可以形成一個 css 的“作用域空間”。

在微信小程式中目前是不支援 attr 選擇器的,所以我們做了一點改動,把 attr 上的 [module-id] 直接寫到了 class 裡,如下:

<!-- .vue -->
<template>
    <div class="container">
        // ...
    </div>
</template>
<style scoped>
    .container {
        color: red;
    }
</style>

<!-- vue-loader -->
<template>
    <div class="container" data-v-23e58823>
        // ...
    </div>
</template>
<style scoped>
    .container[data-v-23e58823] {
        color: red;
    }
</style>

<!-- mpvue-loader -->
<template>
    <div class="container data-v-23e58823">
        // ...
    </div>
</template>
<style scoped>
    .container.data-v-23e58823 {
        color: red;
    }
</style>
複製程式碼
  • compiler

生產出的內容是:

(function(module, __webpack_exports__, __webpack_require__) {

"use strict";
// mpvue-template-compiler會利用AST預編譯生成一個render function用以生成Virtual DOM。
var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
  // _c建立虛擬節點,參考https://github.com/Meituan-Dianping/mpvue/blob/master/packages/mpvue/index.js#L3606
  // 以及https://github.com/Meituan-Dianping/mpvue/blob/master/packages/mpvue/index.js#L3680
  return _c('div', {
    staticClass: "my-component"
  }, [_c('h1', [_vm._v(_vm._s(_vm.msg))]), _vm._v(" "), _c('other-component', {
    attrs: {
      "msg": _vm.msg,
      "mpcomid": '0'
    }
  })], 1)
}

// staticRenderFns的作用是靜態渲染,在更新時不會進行patch,優化效能。而staticRenderFns是個空陣列。
var staticRenderFns = []
render._withStripped = true
var esExports = { render: render, staticRenderFns: staticRenderFns }
/* harmony default export */ __webpack_exports__["a"] = (esExports);
if (false) {
  module.hot.accept()
  if (module.hot.data) {
     require("vue-hot-reload-api").rerender("data-v-54ad9125", esExports)
  }
}

/***/ })
複製程式碼

compiler

compiler相關,也就是template預編譯這塊,可以參考《聊聊Vue的template編譯》來搞明白。原理是一樣的。

mpvue自己實現了export { compile, compileToFunctions, compileToWxml }(連結)其中compileToWxml是用來生成wxml,具體程式碼在這

另外mpvue是不需要提供執行時-編譯器的,雖然理論上是能夠做到的。因為小程式不能操作DOM,即便提供了執行時-編譯器也產生不了介面。

詳細講解compile過程:

1.將vue檔案解析成模板物件

// mpvue-loader/lib/loader.js
var parts = parse(content, fileName, this.sourceMap)
複製程式碼

假如vue檔案原始碼如下:

<template>
  <view class="container-bg">
    <view class="home-container">
      <home-quotation-view v-for="(item, index) in lists" :key="index" :reason="item.reason" :stockList="item.list" @itemViewClicked="itemViewClicked" />
    </view>
  </view>
</template>

<script lang="js">
import homeQuotationView from '@/components/homeQuotationView'
import topListApi from '@/api/topListApi'

export default {
  data () {
    return {
      lists: []
    }
  },
  components: {
    homeQuotationView
  },
  methods: {
    async loadRankList () {
      let {data} = await topListApi.rankList()
      if (data) {
        this.dateTime = data.dt
        this.lists = data.rankList.filter((item) => {
          return !!item
        })
      }
    },
    itemViewClicked (quotationItem) {
      wx.navigateTo({
        url: `/pages/topListDetail/main?item=${JSON.stringify(quotationItem)}`
      })
    }
  },
  onShow () {
    this.loadRankList()
  }
}
</script>

<style lang="stylus" scoped>
  .container-bg
    width 100%
    height 100%
    background-color #F2F4FA

  .home-container
    width 100%
    height 100%
    overflow-x hidden

</style>
複製程式碼

呼叫parse(content, fileName, this.sourceMap) 函式得到的結果大致如下:

{
  template: {
    type: 'template',
    content: '\n<view class="container-bg">\n  <view class="home-container">\n    <home-quotation-view v-for="(item, index) in lists" :key="index" :reason="item.reason" :stockList="item.list" @itemViewClicked="itemViewClicked" />\n  </view>\n</view>\n',
    start: 10,
    attrs: {},
    end: 251
  },
  script: {
    type: 'script',
    content: '\n\n\n\n\n\n\n\n\nimport homeQuotationView from \'@/components/homeQuotationView\'\nimport topListApi from \'@/api/topListApi\'\n\nexport default {\n  data () {\n    return {\n      lists: []\n    }\n  },\n  components: {\n    homeQuotationView\n  },\n  methods: {\n    async loadRankList () {\n      let {data} = await topListApi.rankList()\n      if (data) {\n        this.dateTime = data.dt\n        this.lists = data.rankList.filter((item) => {\n          return !!item\n        })\n      }\n    },\n    itemViewClicked (quotationItem) {\n      wx.navigateTo({\n        url: `/pages/topListDetail/main?item=${JSON.stringify(quotationItem)}`\n      })\n    }\n  },\n  onShow () {\n    this.loadRankList()\n  }\n}\n',
    start: 282,
    attrs: {
      lang: 'js'
    },
    lang: 'js',
    end: 946,
    ...
  },
  styles: [{
    type: 'style',
    content: '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n.container-bg\n  width 100%\n  height 100%\n  background-color #F2F4FA\n\n.home-container\n  width 100%\n  height 100%\n  overflow-x hidden\n\n',
    start: 985,
    attrs: [Object],
    lang: 'stylus',
    scoped: true,
    end: 1135,
    ...
  }],
  customBlocks: []
}
複製程式碼

2.呼叫mpvue-loader/lib/template-compiler/index.js匯出的介面並傳入上面得到的html模板:

var templateCompilerPath = normalize.lib('template-compiler/index')
...
var defaultLoaders = {
  html: templateCompilerPath + templateCompilerOptions,
  css: options.extractCSS
    ? getCSSExtractLoader()
    : styleLoaderPath + '!' + 'css-loader' + cssLoaderOptions,
  js: hasBuble ? ('buble-loader' + bubleOptions) : hasBabel ? babelLoaderOptions : ''
}

// check if there are custom loaders specified via
// webpack config, otherwise use defaults
var loaders = Object.assign({}, defaultLoaders, options.loaders)
複製程式碼
  1. 呼叫mpvue/packages/mpvue-template-compiler/build.js的compile介面:
// mpvue-loader/lib/template-compiler/index.js
var compiled = compile(html, compilerOptions)
複製程式碼

compile方法生產下面的ast(Abstract Syntax Tree)模板,render函式和staticRenderFns

{
  ast: {
    type: 1,
    tag: 'view',
    attrsList: [],
    attrsMap: {
      class: 'container-bg'
    },
    parent: undefined,
    children: [{
      type: 1,
      tag: 'view',
      attrsList: [],
      attrsMap: {
        class: 'home-container'
      },
      parent: {
        type: 1,
        tag: 'view',
        attrsList: [],
        attrsMap: {
          class: 'container-bg'
        },
        parent: undefined,
        children: [
          [Circular]
        ],
        plain: false,
        staticClass: '"container-bg"',
        static: false,
        staticRoot: false
      },
      children: [{
        type: 1,
        tag: 'home-quotation-view',
        attrsList: [{
          name: ':reason',
          value: 'item.reason'
        }, {
          name: ':stockList',
          value: 'item.list'
        }, {
          name: '@itemViewClicked',
          value: 'itemViewClicked'
        }],
        attrsMap: {
          'v-for': '(item, index) in lists',
          ':key': 'index',
          ':reason': 'item.reason',
          ':stockList': 'item.list',
          '@itemViewClicked': 'itemViewClicked',
          'data-eventid': '{{\'0-\'+index}}',
          'data-comkey': '{{$k}}'
        },
        parent: [Circular],
        children: [],
        for: 'lists',
        alias: 'item',
        iterator1: 'index',
        key: 'index',
        plain: false,
        hasBindings: true,
        attrs: [{
          name: 'reason',
          value: 'item.reason'
        }, {
          name: 'stockList',
          value: 'item.list'
        }, {
          name: 'eventid',
          value: '\'0-\'+index'
        }, {
          name: 'mpcomid',
          value: '\'0-\'+index'
        }],
        events: {
          itemViewClicked: {
            value: 'itemViewClicked',
            modifiers: undefined
          }
        },
        eventid: '\'0-\'+index',
        mpcomid: '\'0-\'+index',
        static: false,
        staticRoot: false,
        forProcessed: true
      }],
      plain: false,
      staticClass: '"home-container"',
      static: false,
      staticRoot: false
    }],
    plain: false,
    staticClass: '"container-bg"',
    static: false,
    staticRoot: false
  },
  render: 'with(this){return _c(\'view\',{staticClass:"container-bg"},[_c(\'view\',{staticClass:"home-container"},_l((lists),function(item,index){return _c(\'home-quotation-view\',{key:index,attrs:{"reason":item.reason,"stockList":item.list,"eventid":\'0-\'+index,"mpcomid":\'0-\'+index},on:{"itemViewClicked":itemViewClicked}})}))])}',
  staticRenderFns: [],
  errors: [],
  tips: []
}
複製程式碼

其中的render函式執行的結果是返回VNode物件,其實render函式應該長下面這樣:

(function() {
  with(this){
    return _c('div',{   //建立一個 div 元素
      attrs:{"id":"app"}  //div 新增屬性 id
      },[
        _m(0),  //靜態節點 header,此處對應 staticRenderFns 陣列索引為 0 的 render 函式
        _v(" "), //空的文字節點
        (message) //三元表示式,判斷 message 是否存在
         //如果存在,建立 p 元素,元素裡面有文字,值為 toString(message)
        ?_c('p',[_v("\n    "+_s(message)+"\n  ")])
        //如果不存在,建立 p 元素,元素裡面有文字,值為 No message. 
        :_c('p',[_v("\n    No message.\n  ")])
      ]
    )
  }
})
複製程式碼

其中的_c就是vue物件的createElement方法 (建立元素),_mrenderStatic(渲染靜態節點),_vcreateTextVNode(建立文字dom),_stoString (轉換為字串)

// src/core/instance/render.js
export function initRender (vm: Component) {
  ...
  // bind the createElement fn to this instance
  // so that we get proper render context inside it.
  // args order: tag, data, children, normalizationType, alwaysNormalize
  // internal version is used by render functions compiled from templates
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
  ...
}

...
Vue.prototype._s = toString
...
Vue.prototype._m = renderStatic
...
Vue.prototype._v = createTextVNode
...
複製程式碼
  1. 呼叫compileWxml方法生產wxml模板,這個方法最終會呼叫 mpvue/packages/mpvue-template-compiler/build.js的compileToWxml方法將第一步compile出來的模板轉成小程式的wxml模板
// mpvue-loader/lib/template-compiler/index.js
compileToWxml.call(this, compiled, html)
複製程式碼

以上解答了問題1、2

runtime

目錄結構

.
├── events.js //解答問題5
├── index.js //入口提供Vue物件,以及$mount,和各種初始化
├── liefcycle //解答問題6、7
├── node-ops.js //操作真實DOM的相關實現,因為小程式不能操作DOM,所以這裡都是直接返回
├── patch.js //解答問題3
└── render.js //解答問題4
複製程式碼

patch.js

和vue使用的createPatchFunction保持一致,任然是舊樹和新樹進行patch產出diff,但是多了一行this.$updateDataToMP()用以更新。

render.js

兩個核心的方法initDataToMPupdateDataToMP

initDataToMP收集vm上的data,然後呼叫小程式Page示例的setData渲染。

updateDataToMP在每次patch,也就是依賴收集發現資料改變時更新(參考patch.js程式碼),這部分一樣會使用nextTick和佇列。最終使用了節流閥throttleSetData。50毫秒用來控制頻率以解決頻繁修改Data,會造成大量傳輸Data資料而導致的效能問題。

其中collectVmData最終也是用到了formatVmData。尤其要注意的是一句註釋:

getVmData 這兒獲取當前元件內的所有資料,包含 props、computed 的資料

我們又知道,service到view是兩個執行緒間通訊,如果Data含有大量資料,增加了傳輸資料量,加大了傳輸成本,將會造成效能下降。

events.js

正如官網所說的,這裡使用eventTypeMap做了各事件的隱射

import { getComKey, eventTypeMap } from '../util/index'
複製程式碼
// 用於小程式的 event type 到 web 的 event
export const eventTypeMap = {
  tap: ['tap', 'click'],
  touchstart: ['touchstart'],
  touchmove: ['touchmove'],
  touchcancel: ['touchcancel'],
  touchend: ['touchend'],
  longtap: ['longtap'],
  input: ['input'],
  blur: ['change', 'blur'],
  submit: ['submit'],
  focus: ['focus'],
  scrolltoupper: ['scrolltoupper'],
  scrolltolower: ['scrolltolower'],
  scroll: ['scroll']
}
複製程式碼

使用了handleProxyWithVue方法來代理小程式事件到vue事件。

另外看下作者自己對這部分的思路

事件代理機制:使用者互動觸發的資料更新通過事件代理機制完成。在 Vue.js 程式碼中,事件響應函式對應到元件的 method, Vue.js 自動維護了上下文環境。然而在小程式中並沒有類似的機制,又因為 Vue.js 執行環境中維護著一份實時的虛擬 DOM,這與小程式的檢視層完全對應,我們思考,在小程式元件節點上觸發事件後,只要找到虛擬 DOM 上對應的節點,觸發對應的事件不就完成了麼;另一方面,Vue.js 事件響應如果觸發了資料更新,其生命週期函式更新將自動觸發,在此函式上同步更新小程式資料,資料同步也就實現了。

getHandle這個方法應該就是作者思路當中所說的:找到對應節點,然後找到handle。

lifecycle.js

initMP方法中,自己建立小程式的App、Page。實現生命週期相關方法,使用callHook代理相容小程式App、Page的生命週期。

官方文件生命週期中說到了:

同 vue,不同的是我們會在小程式 onReady 後,再去觸發 vue mounted 生命週期

這部分檢視,onReady之後才會執行next,這個next回撥最終是vue的mountComponent。可以在index.js中看到。這部分程式碼也就是解決了"小程式生命週期中觸發vue生命週期"。

export function initMP (mpType, next) {
  // ...
    global.Page({
      // 生命週期函式--監聽頁面初次渲染完成
      onReady () {
        mp.status = 'ready'

        callHook(rootVueVM, 'onReady')
        next()
      },
    })
  // ...
}
複製程式碼

在小程式onShow時,使用$nextTick去第一次渲染資料,參考上面提到的render.js。

export function initMP (mpType, next) {
  // ...
  global.Page({
    // 生命週期函式--監聽頁面顯示
    onShow () {
      mp.page = this
      mp.status = 'show'
      callHook(rootVueVM, 'onShow')

      // 只有頁面需要 setData
      rootVueVM.$nextTick(() => {
        rootVueVM._initDataToMP()
      })
    },
  })
  // ...
}
複製程式碼

在mpvue-loader生成template時,比如點選事件@click會變成bindtap="handleProxy",事件繫結全都會使用handleProxy這個方法。

可以檢視上面mpvue-loader回顧一下。

最終handleProxy呼叫的是event.js中的handleProxyWithVue

export function initMP (mpType, next) {
  // ...
    global.Page({
      handleProxy (e) {
        return rootVueVM.$handleProxyWithVue(e)
      },
    })
  // ...
}
複製程式碼

index.js

最後index.js就負責各種初始化和mount。

Class和Style為什麼暫不支援元件

原因:目前的元件是使用小程式的 template 標籤實現的,給元件指定的class和style是掛載在template標籤上,而template 標籤不支援 class 及 style 屬性。

解決方案: 在自定義元件上繫結class或style到一個props屬性上。

 // 元件ComponentA.vue
 <template>
  <div class="container" :class="pClass">
    ...
  </div>
</template>
複製程式碼
<script>
    export default {
    props: {
      pClass: {
        type: String,
        default: ''
      }
    }
  }
</script>
複製程式碼
<!--PageB.vue-->
<template>
    <component-a :pClass="cusComponentAClass"  />
</template>
複製程式碼
<script>
data () {
    return {
      cusComponentAClass: 'a-class b-class'
    }
  }
</script>
複製程式碼
<style lang="stylus" scoped>
  .a-class
    border red solid 2rpx
  .b-class
    margin-right 20rpx
</style>
複製程式碼

但是這樣會有問題就是style加上scoped之後,編譯模板生成的程式碼是下面這樣的:

 .a-class.data-v-8f1d914e {
   border: #f00 solid 2rpx;
 }
 .b-class.data-v-8f1d914e {
   margin-right 20rpx
 }
複製程式碼

所以想要這些元件的class生效就不能使用scoped的style,改成下面這樣,最好自己給a-class和b-class加字首以防其他的檔案引用這些樣式:

 <style lang="stylus">
  .a-class
    border red solid 2rpx
  .b-class
    margin-right 20rpx
</style>

<style lang="stylus" scoped>
  .other-class
    border red solid 2rpx
    
   ...
</style>
複製程式碼
  • 在定義元件上繫結style屬性到一個props屬性上:
 <!--P元件ComponentA.vue-->
 <template>
  <div class="container" :style="pStyle">
    ...
  </div>
</template>
複製程式碼
<script>
  export default {
    props: {
      pStyle: {
        type: String,
        default: ''
      }
    }
  }
</script>
複製程式碼
<!--PageB.vue-->
<template>
    <component-a :pStyle="cusComponentAStyle"  />
</template>
複製程式碼
<script>
const cusComponentAStyle = 'border:red solid 2rpx; margin-right:20rpx;'
data () {
    return {
      cusComponentAStyle
    }
  }
</script>
複製程式碼
<style lang="stylus" scoped>
  ...
</style>
複製程式碼

也可以通過定義styleObject,然後通過工具函式轉化為styleString,如下所示:

const bstyle = {
  border: 'red solid 2rpx',
  'margin-right': '20rpx'
}
let arr = []
for (let [key, value] of Object.entries(bstyle)) {
  arr.push(`${key}: ${value}`)
}

const cusComponentAStyle = arr.join('; ')
複製程式碼
  • 當然自定義元件確定只會改變某個css樣式,通過pros傳入單個樣式的值,然後通過:style繫結肯定沒問題:
<!--元件ComponentA.vue-->
 <template>
  <div class="container" :style="{'background-color': backgroundColor}">
    ...
  </div>
</template>
複製程式碼
<script>
    export default {
    props: {
      backgroundColor: {
        type: String,
        default: 'yellow'
      }
    }
  }
</script>
複製程式碼
<!-- PageB.vue -->
<template>
    <component-a backgroundColor="red"  />
</template>
複製程式碼

分包載入

package.json修改

  • 升級: "mpvue-loader": "^1.1.2-rc.4" "webpack-mpvue-asset-plugin": "^0.1.1"
  • 新增: "relative": "^3.0.2"

注意事項

  • 1.1.2-rc.5 修復 slot 檔案路徑生成錯誤的問題
  • 1.1.x 版本還不是很穩定,對穩定性要求較高的專案建議暫時使用 1.0.x 版本

移動src/main.js中config相關內容到同級目錄下main.json(新建)中

export default {
  // config: {...} 需要移動
}

複製程式碼

to

{
 "pages": [
   "pages/index/main",
   "pages/logs/main"
  ],
  "subPackages": [
    {
      "root": "pages/packageA",
     "pages": [
       "counter/main"
     ]
   }
 ],
 "window": {...}
}
複製程式碼

webpack 配置配合升級指南

  • 本次升級意在調整生成檔案目錄結構,對依賴的檔案由原來的寫死絕對路徑該改為相對路徑
  • mpvue-loader@1.1.2-rc.4 依賴 webpack-mpvue-asset-plugin@0.1.0 做依賴資源引用
  • 之前寫在 main.js 中的 config 資訊,需要在 main.js 同級目錄下新建 main.json 檔案,使用 webapck-copy-plugin copy 到 build 目錄下
  • app.json 中引用的圖片不會自動 copy 到 dist 目錄下 json 配置檔案是由 webapck-copy-plugin copy 過去的,不會處理依賴,可以將圖片放到根目錄下 static 目錄下,使用 webapck-copy-plugin copy 過去

build/webpack.base.conf.js

+var CopyWebpackPlugin = require('copy-webpack-plugin')
+var relative = require('relative')

 function resolve (dir) {
   return path.join(__dirname, '..', dir)
 }

-function getEntry (rootSrc, pattern) {
-  var files = glob.sync(path.resolve(rootSrc, pattern))
-  return files.reduce((res, file) => {
-    var info = path.parse(file)
-    var key = info.dir.slice(rootSrc.length + 1) + '/' + info.name
-    res[key] = path.resolve(file)
-    return res
-  }, {})
+function getEntry (rootSrc) {
+  var map = {};
+  glob.sync(rootSrc + '/pages/**/main.js')
+  .forEach(file => {
+    var key = relative(rootSrc, file).replace('.js', '');
+    map[key] = file;
+  })
+   return map;
 }

   plugins: [
-    new MpvuePlugin()
+    new MpvuePlugin(),
+    new CopyWebpackPlugin([{
+      from: '**/*.json',
+      to: 'app.json'
+    }], {
+      context: 'src/'
+    }),
+    new CopyWebpackPlugin([ // 處理 main.json 裡面引用的圖片,不要放程式碼中引用的圖片
+      {
+        from: path.resolve(__dirname, '../static'),
+        to: path.resolve(__dirname, '../dist/static'),
+        ignore: ['.*']
+      }
+    ])
   ]
 }
複製程式碼

build/webpack.dev.conf.js

module.exports = merge(baseWebpackConfig, {
   devtool: '#source-map',
   output: {
     path: config.build.assetsRoot,
-    filename: utils.assetsPath('js/[name].js'),
-    chunkFilename: utils.assetsPath('js/[id].js')
+    filename: utils.assetsPath('[name].js'),
+    chunkFilename: utils.assetsPath('[id].js')
   },
   plugins: [
     new webpack.DefinePlugin({
    module.exports = merge(baseWebpackConfig, {
     // copy from ./webpack.prod.conf.js
     // extract css into its own file
     new ExtractTextPlugin({
-      filename: utils.assetsPath('css/[name].wxss')
+      filename: utils.assetsPath('[name].wxss')
     }),
    module.exports = merge(baseWebpackConfig, {
       }
     }),
     new webpack.optimize.CommonsChunkPlugin({
-      name: 'vendor',
+      name: 'common/vendor',
       minChunks: function (module, count) {
         // any required modules inside node_modules are extracted to vendor
         return (
        module.exports = merge(baseWebpackConfig, {
       }
     }),
     new webpack.optimize.CommonsChunkPlugin({
-      name: 'manifest',
-      chunks: ['vendor']
+      name: 'common/manifest',
+      chunks: ['common/vendor']
     }),
-    // copy custom static assets
-    new CopyWebpackPlugin([
-      {
-        from: path.resolve(__dirname, '../static'),
-        to: config.build.assetsSubDirectory,
-        ignore: ['.*']
-      }
-    ]),

複製程式碼

build/webpack.prod.conf.js


    var webpackConfig = merge(baseWebpackConfig, {
   devtool: config.build.productionSourceMap ? '#source-map' : false,
   output: {
     path: config.build.assetsRoot,
-    filename: utils.assetsPath('js/[name].js'),
-    chunkFilename: utils.assetsPath('js/[id].js')
+    filename: utils.assetsPath('[name].js'),
+    chunkFilename: utils.assetsPath('[id].js')
   },
   plugins: [
    var webpackConfig = merge(baseWebpackConfig, {
     }),
     // extract css into its own file
     new ExtractTextPlugin({
-      // filename: utils.assetsPath('css/[name].[contenthash].css')
-      filename: utils.assetsPath('css/[name].wxss')
+      // filename: utils.assetsPath('[name].[contenthash].css')
+      filename: utils.assetsPath('[name].wxss')
     }),
     // Compress extracted CSS. We are using this plugin so that possible
     // duplicated CSS from different components can be deduped.
    var webpackConfig = merge(baseWebpackConfig, {
     new webpack.HashedModuleIdsPlugin(),
     // split vendor js into its own file
     new webpack.optimize.CommonsChunkPlugin({
-      name: 'vendor',
+      name: 'common/vendor',
       minChunks: function (module, count) {
         // any required modules inside node_modules are extracted to vendor
         return (
     var webpackConfig = merge(baseWebpackConfig, {
     // extract webpack runtime and module manifest to its own file in order to
     // prevent vendor hash from being updated whenever app bundle is updated
     new webpack.optimize.CommonsChunkPlugin({
-      name: 'manifest',
-      chunks: ['vendor']
-    }),
+      name: 'common/manifest',
+      chunks: ['common/vendor']
+    })
-    // copy custom static assets
-    new CopyWebpackPlugin([
-      {
-        from: path.resolve(__dirname, '../static'),
-        to: config.build.assetsSubDirectory,
-        ignore: ['.*']
-      }
-    ])
   ]
 })
複製程式碼

config/index.js


module.exports = {
     env: require('./prod.env'),
     index: path.resolve(__dirname, '../dist/index.html'),
     assetsRoot: path.resolve(__dirname, '../dist'),
-    assetsSubDirectory: 'static', // 不將資源聚合放在 static 目錄下
+    assetsSubDirectory: '',
     assetsPublicPath: '/',
     productionSourceMap: false,
     // Gzip off by default as many popular static hosts such as
@@ -26,7 +26,7 @@ module.exports = {
     port: 8080,
     // 在小程式開發者工具中不需要自動開啟瀏覽器
     autoOpenBrowser: false,
-    assetsSubDirectory: 'static', // 不將資源聚合放在 static 目錄下
+    assetsSubDirectory: '',
     assetsPublicPath: '/',
     proxyTable: {},
     // CSS Sourcemaps off by default because relative paths are "buggy"

複製程式碼

參考連結

以上內容部分來自:

如果你想學習到更多的前端知識、面試技巧或者一些我個人的感悟,可以關注我的公眾號一起學習

近兩萬字小程式攻略釋出了

相關文章