30分鐘熟悉微信小程序

作者:葉小釵?

www.cnblogs.com/yexiaochai/p/9431816.html


首先我們來一言以蔽之,什么是微信小程序?PS:這個問題問得好像有些扯:)

小程序是一個不需要下載安裝就可使用的應(yīng)用,它實現(xiàn)了應(yīng)用觸手可及的夢想,用戶掃一掃或者搜一下即可打開應(yīng)用。也體現(xiàn)了用完即走的理念,用戶不用關(guān)心是否安裝太多應(yīng)用的問題。應(yīng)用將無處不在,隨時可用,但又無需安裝卸載。從字面上看小程序具有類似Web應(yīng)用的熱部署能力,在功能上又接近于原生APP。

所以說,其實微信小程序是一套超級Hybrid的解決方案,現(xiàn)在看來,小程序應(yīng)該是應(yīng)用場景最廣,也最為復(fù)雜的解決方案了。

很多公司都會有自己的Hybrid平臺,我這里了解到比較不錯的是攜程的Hybrid平臺、阿里的Weex、百度的糯米,但是從應(yīng)用場景來說都沒有微信來得豐富,這里根本的區(qū)別是:

微信小程序是給各個公司開發(fā)者接入的,其他公司平臺多是給自己業(yè)務(wù)團(tuán)隊使用,這一根本區(qū)別,就造就了我們看到的很多小程序不一樣的特性:

小程序定義了自己的標(biāo)簽語言WXML

小程序定義了自己的樣式語言WXSS

小程序提供了一套前端框架包括對應(yīng)Native API

禁用瀏覽器Dom API(這個區(qū)別,會影響我們的代碼方式)

只要了解到這些區(qū)別就會知道為什么小程序會這么設(shè)計:

因為小程序是給各個公司的開發(fā)做的,其他公司的Hybrid方案是給公司業(yè)務(wù)團(tuán)隊用的,一般擁有Hybrid平臺的公司實力都不錯。但是開發(fā)小程序的公司實力良莠不齊,所以小程序要做絕對的限制,最大程度的保證框架層(小程序團(tuán)隊)對程序的控制。因為畢竟程序運行在微信這種體量的APP中

之前我也有一個疑惑為什么微信小程序會設(shè)計自己的標(biāo)簽語言,也在知乎看到各種各樣的回答,但是如果出于設(shè)計層面以及應(yīng)用層面考慮的話:這樣會有更好的控制,而且我后面發(fā)現(xiàn)微信小程序事實上依舊使用的是webview做渲染(這個與我之前認(rèn)為微信是NativeUI是向左的),但是如果我們使用的微信限制下面的標(biāo)簽,這個是有限的標(biāo)簽,后期想要換成NativeUI會變得更加輕易:

另一方面,經(jīng)過之前的學(xué)習(xí),我這邊明確可以得出一個感受:

小程序的頁面核心是標(biāo)簽,標(biāo)簽是不可控制的(我暫時沒用到j(luò)s操作元素的方法),只能按照微信給的玩法玩,標(biāo)簽控制顯示是我們的view

標(biāo)簽的展示只與data有關(guān)聯(lián),和js是隔離的,沒有辦法在標(biāo)簽中調(diào)用js的方法

而我們的js的唯一工作便是根據(jù)業(yè)務(wù)改變data,重新引發(fā)頁面渲染,以后別想操作DOM,別想操作Window對象了,改變開發(fā)方式,改變開發(fā)方式,改變開發(fā)方式!


this.setData({'wxml': `

??<my-component>

??<view>動態(tài)插入的節(jié)點</view>

??</my-component>

`});


然后可以看到這個是一個MVC模型

每個頁面的目錄是這個樣子的:

project

├── pages

|?? ├── index

|?? |?? ├── index.json??index 頁面配置

|?? |?? ├── index.js????index 頁面邏輯

|?? |?? ├── index.wxml??index 頁面結(jié)構(gòu)

|?? |?? └── index.wxss??index 頁面樣式表

|?? └── log

|?????? ├── log.json????log 頁面配置

|?????? ├── log.wxml????log 頁面邏輯

|?????? ├── log.js??????log 頁面結(jié)構(gòu)

|?????? └── log.wxss????log 頁面樣式表

├── app.js??????????????小程序邏輯

├── app.json????????????小程序公共設(shè)置

└── app.wxss????????????小程序公共樣式表


每個組件的目錄也大概是這個樣子的,大同小異,但是入口是Page層。

小程序打包后的結(jié)構(gòu)(這里就真的不懂了,引用:小程序底層框架實現(xiàn)原理解析):

所有的小程序基本都最后都被打成上面的結(jié)構(gòu)

1、WAService.js 框架JS庫,提供邏輯層基礎(chǔ)的API能力

2、WAWebview.js 框架JS庫,提供視圖層基礎(chǔ)的API能力

3、WAConsole.js 框架JS庫,控制臺

4、app-config.js 小程序完整的配置,包含我們通過app.json里的所有配置,綜合了默認(rèn)配置型

5、app-service.js 我們自己的JS代碼,全部打包到這個文件

6、page-frame.html 小程序視圖的模板文件,所有的頁面都使用此加載渲染,且所有的WXML都拆解為JS實現(xiàn)打包到這里

7、pages 所有的頁面,這個不是我們之前的wxml文件了,主要是處理WXSS轉(zhuǎn)換,使用js插入到header區(qū)域

從設(shè)計的角度上說,小程序采用的組件化開發(fā)的方案,除了頁面級別的標(biāo)簽,后面全部是組件,而組件中的標(biāo)簽view、data、js的關(guān)系應(yīng)該是與page是一致的,這個也是我們平時建議的開發(fā)方式,將一根頁面拆分成一個個小的業(yè)務(wù)組件或者UI組件:

所有的小程序基本都最后都被打成上面的結(jié)構(gòu)

WAService.js 框架JS庫,提供邏輯層基礎(chǔ)的API能力

WAWebview.js 框架JS庫,提供視圖層基礎(chǔ)的API能力

WAConsole.js 框架JS庫,控制臺

app-config.js 小程序完整的配置,包含我們通過app.json里的所有配置,綜合了默認(rèn)配置型

app-service.js 我們自己的JS代碼,全部打包到這個文件

page-frame.html 小程序視圖的模板文件,所有的頁面都使用此加載渲染,且所有的WXML都拆解為JS實現(xiàn)打包到這里

pages 所有的頁面,這個不是我們之前的wxml文件了,主要是處理WXSS轉(zhuǎn)換,使用js插入到header區(qū)域

從設(shè)計的角度上說,小程序采用的組件化開發(fā)的方案,除了頁面級別的標(biāo)簽,后面全部是組件,而組件中的標(biāo)簽view、data、js的關(guān)系應(yīng)該是與page是一致的,這個也是我們平時建議的開發(fā)方式,將一根頁面拆分成一個個小的業(yè)務(wù)組件或者UI組件:

從我寫業(yè)務(wù)代碼過程中,覺得整體來說還是比較順暢的,小程序是有自己一套完整的前端框架的,并且釋放給業(yè)務(wù)代碼的主要就是page,而page只能使用標(biāo)簽和組件,所以說框架的對業(yè)務(wù)的控制力度很好。

最后我們從工程角度來看微信小程序的架構(gòu)就更加完美了,小程序從三個方面考慮了業(yè)務(wù)者的感受:

開發(fā)工具+調(diào)試工具

開發(fā)基本模型(開發(fā)基本標(biāo)準(zhǔn)WXML、WXSS、JS、JSON)

完善的構(gòu)建(對業(yè)務(wù)方透明)

自動化上傳離線包(對業(yè)務(wù)費透明離線包邏輯)

監(jiān)控統(tǒng)計邏輯

所以,微信小程序從架構(gòu)上和使用場景來說是很令人驚艷的,至少驚艷了我……所以我們接下來在開發(fā)層面對他進(jìn)行更加深入的剖析,我們這邊最近一直在做基礎(chǔ)服務(wù),這一切都是為了完善技術(shù)體系,這里對于前端來說便是我們需要做一個Hybrid體系,如果做App,React Native也是不錯的選擇,但是一定要有完善的分層:

底層框架解決開發(fā)效率,將復(fù)雜的部分做成一個黑匣子,給頁面開發(fā)展示的只是固定的三板斧,固定的模式下開發(fā)即可

工程部門為業(yè)務(wù)開發(fā)者封裝最小化開發(fā)環(huán)境,最優(yōu)為瀏覽器,確實不行便為其提供一個類似瀏覽器的調(diào)試環(huán)境

如此一來,業(yè)務(wù)便能快速迭代,因為業(yè)務(wù)開發(fā)者寫的代碼大同小異,所以底層框架配合工程團(tuán)隊(一般是同一個團(tuán)隊),便可以在底層做掉很多效率性能問題。

稍微大點的公司,稍微寬裕的團(tuán)隊,還會同步做很多后續(xù)的性能監(jiān)控、錯誤日志工作,如此形成一套文檔->開發(fā)->調(diào)試->構(gòu)建->發(fā)布->監(jiān)控、分析 為一套完善的技術(shù)體系

如果形成了這么一套體系,那么后續(xù)就算是內(nèi)部框架更改、技術(shù)革新,也是在這個體系上改造,這塊微信小程序是做的非常好的。但很可惜,很多其他公司團(tuán)隊只會在這個路徑上做一部分,后面由于種種原因不在深入,有可能是感覺沒價值,而最恐怖的行為是,自己的體系沒形成就貿(mào)然的換基礎(chǔ)框架,戒之慎之??!好了閑話少說,我們繼續(xù)接下來的學(xué)習(xí)。

微信小程序的執(zhí)行流程

微信小程序為了對業(yè)務(wù)方有更強的控制,App層做的工作很有限,我后面寫demo的時候根本沒有用到app.js,所以我這里認(rèn)為app.js只是完成了一個路由以及初始化相關(guān)的工作,這個是我們看得到的,我們看不到的是底層框架會根據(jù)app.json的配置將所有頁面js都準(zhǔn)備好。

我這里要表達(dá)的是,我們這里配置了我們所有的路由:

"pages":[

??"pages/index/index",

??"pages/list/list",

??"pages/logs/logs"

],


微信小程序一旦載入,會開3個webview,裝載3個頁面的邏輯,完成基本的實例化工作,只顯示首頁!這個是小程序為了優(yōu)化頁面打開速度所做的工作,也勢必會浪費一些資源,所以到底是全部打開或者預(yù)加載幾個,詳細(xì)底層Native會根據(jù)實際情況動態(tài)變化,我們也可以看到,從業(yè)務(wù)層面來說,要了解小程序的執(zhí)行流程,其實只要能了解Page的流程就好了,關(guān)于Page生命周期,除了釋放出來的API:onLoad -> onShow -> onReady -> onHide等,官方還出了一張圖進(jìn)行說明:

Native層在載入小程序時候,起了兩個線程一個的view Thread一個是AppService Thread,我這邊理解下來應(yīng)該就是程序邏輯執(zhí)行與頁面渲染分離,小程序的視圖層目前使用 WebView 作為渲染載體,而邏輯層是由獨立的 JavascriptCore 作為運行環(huán)境。在架構(gòu)上,WebView 和 JavascriptCore 都是獨立的模塊,并不具備數(shù)據(jù)直接共享的通道。當(dāng)前,視圖層和邏輯層的數(shù)據(jù)傳輸,實際上通過兩邊提供的 evaluateJavascript 所實現(xiàn)。即用戶傳輸?shù)臄?shù)據(jù),需要將其轉(zhuǎn)換為字符串形式傳遞,同時把轉(zhuǎn)換后的數(shù)據(jù)內(nèi)容拼接成一份 JS 腳本,再通過執(zhí)行 JS 腳本的形式傳遞到兩邊獨立環(huán)境。而 evaluateJavascript 的執(zhí)行會受很多方面的影響,數(shù)據(jù)到達(dá)視圖層并不是實時的。

因為之前我認(rèn)為頁面是使用NativeUI做渲染跟Webview沒撒關(guān)系,便覺得這個圖有問題,但是后面實際代碼看到了熟悉的shadow-dom以及Android可以看到哪部分是Web的,其實小程序主體還是使用的瀏覽器渲染的方式,還是webview裝載HTML和CSS的邏輯,最后我發(fā)現(xiàn)這張圖是沒有問題的,有問題的是我的理解,哈哈,這里我們重新解析這張圖:

WXML先會被編譯成JS文件,引入數(shù)據(jù)后在WebView中渲染,這里可以認(rèn)為微信載入小程序時同時初始化了兩個線程,分別執(zhí)行彼此邏輯:

WXML&CSS編譯形成的JS View實例化結(jié)束,準(zhǔn)備結(jié)束時向業(yè)務(wù)線程發(fā)送通知

業(yè)務(wù)線程中的JS Page部分同步完成實例化結(jié)束,這個時候接收到View線程部分的等待數(shù)據(jù)通知,將初始化data數(shù)據(jù)發(fā)送給View

View線程接到數(shù)據(jù),開始渲染頁面,渲染結(jié)束執(zhí)行通知Page觸發(fā)onReady事件

這里翻開源碼,可以看到,應(yīng)該是全局控制器完成的Page實例化,完成后便會執(zhí)行onLoad事件,但是在執(zhí)行前會往頁面發(fā)通知:

__appServiceSDK__.invokeWebviewMethod({

????name: "appDataChange",

????args: o({}, e, {

????????complete: n

????}),

????webviewIds: [t]

})


真實的邏輯是這樣的,全局控制器會完成頁面實例化,這個是根據(jù)app.json中來的,全部完成實例化存儲起來然后選擇第一個page實例執(zhí)行一些邏輯,然后通知view線程,即將執(zhí)行onLoad事件,因為view線程和業(yè)務(wù)線程是兩個線程,所以不會造成阻塞,view線程根據(jù)初始數(shù)據(jù)完成渲染,而業(yè)務(wù)線程繼續(xù)后續(xù)邏輯,執(zhí)行onLoad,如果onLoad中有setData,那么會進(jìn)入隊列繼續(xù)通知view線程更新。

所以我個人感覺微信官網(wǎng)那張圖不太清晰,我這里重新畫了一個圖:

再引用一張其他地方的圖:

模擬實現(xiàn)

都這個時候了,不來個簡單的小程序框架實現(xiàn)好像有點不對,我們做小程序?qū)崿F(xiàn)的主要原因是想做到一端代碼三端運行:web、小程序、Hybrid甚至Servce端

我們這里沒有可能實現(xiàn)太復(fù)雜的功能,這里想的是就實現(xiàn)一個基本的頁面展示帶一個最基本的標(biāo)簽即可,只做Page一塊的簡單實現(xiàn),讓大家能了解到小程序可能的實現(xiàn),以及如何將小程序直接轉(zhuǎn)為H5的可能走法

<view>

??<!-- 以下是對一個自定義組件的引用 -->

??<my-component inner-text="組件數(shù)據(jù)"></my-component>

??<view>{{pageData}}</view>

</view>

Page({

??data: {

????pageData: '頁面數(shù)據(jù)'

??},

??onLoad: function () {

????console.log('onLoad')

??},

})

<!-- 這是自定義組件的內(nèi)部WXML結(jié)構(gòu) -->

<view class="inner">

??{{innerText}}

</view>

<slot></slot>

Component({

??properties: {

????// 這里定義了innerText屬性,屬性值可以在組件使用時指定

????innerText: {

??????type: String,

??????value: 'default value',

????}

??},

??data: {

????// 這里是一些組件內(nèi)部數(shù)據(jù)

????someData: {}

??},

??methods: {

????// 這里是一個自定義方法

????customMethod: function () { }

??}

})

我們直接將小程序這些代碼拷貝一份到我們的目錄:

我們需要做的就是讓這段代碼運行起來,而這里的目錄是我們最終看見的目錄,真實運行的時候可能不是這個樣,運行之前項目會通過我們的工程構(gòu)建,變成可以直接運行的代碼,而我這里思考的可以運行的代碼事實上是一個模塊,所以我們這里從最終結(jié)果反推、分拆到開發(fā)結(jié)構(gòu)目錄,我們首先將所有代碼放到index.html,可能是這樣的:

<!DOCTYPE html>

<html lang="en">

<head>

??<meta charset="UTF-8">

??<title>Title</title>

</head>

<body>


<script type="text/javascript" src="libs/zepto.js" ></script>

<script type="text/javascript">


??class View {

????constructor(opts) {

??????this.template = '<view>{{pageShow}}</view><view class="ddd" is-show="{{pageShow}}" >{{pageShow}}<view class="c1">{{pageData}}</view></view>';


??????//由控制器page傳入的初始數(shù)據(jù)或者setData產(chǎn)生的數(shù)據(jù)

??????this.data = {

????????pageShow: 'pageshow',

????????pageData: 'pageData',

????????pageShow1: 'pageShow1'

??????};


??????this.labelMap = {

????????'view': 'div',

????????'#text': 'span'

??????};


??????this.nodes = {};

??????this.nodeInfo = {};

????}


????/*

??????傳入一個節(jié)點,解析出一個節(jié)點,并且將節(jié)點中的數(shù)據(jù)以初始化數(shù)據(jù)改變

??????并且將其中包含{{}}標(biāo)志的節(jié)點信息記錄下來

????*/

????_handlerNode (node) {


??????let reg = /{{([sS]+?)}}/;

??????let result, name, value, n, map = {};

??????let attrs , i, len, attr;


??????name = node.nodeName;

??????attrs = node.attributes;

??????value = node.nodeValue;

??????n = document.createElement(this.labelMap[name.toLowerCase()] || name);


??????//說明是文本,需要記錄下來了

??????if(node.nodeType === 3) {

????????n.innerText =??this.data[value] || '';


????????result =??reg.exec(value);

????????if(result) {

??????????n.innerText =??this.data[result[1]] || '';


??????????if(!map[result[1]]) map[result[1]] = [];

??????????map[result[1]].push({

????????????type: 'text',

????????????node: n

??????????});

????????}

??????}


??????if(attrs) {

????????//這里暫時只處理屬性和值兩種情況,多了就復(fù)雜10倍了

????????for (i = 0, len = attrs.length; i < len; i++) {

??????????attr = attrs[i];

??????????result = reg.exec(attr.value);


??????????n.setAttribute(attr.name, attr.value);

??????????//如果有node需要處理則需要存下來標(biāo)志

??????????if (result) {

????????????n.setAttribute(attr.name, this.data[result[1]] || '');


????????????//存儲所有會用到的節(jié)點,以便后面動態(tài)更新

????????????if (!map[result[1]]) map[result[1]] = [];

????????????map[result[1]].push({

??????????????type: 'attr',

??????????????name: attr.name,

??????????????node: n

????????????});


??????????}

????????}

??????}


??????return {

????????node: n,

????????map: map

??????}


????}


????//遍歷一個節(jié)點的所有子節(jié)點,如果有子節(jié)點繼續(xù)遍歷到?jīng)]有為止

????_runAllNode(node, map, root) {


??????let nodeInfo = this._handlerNode(node);

??????let _map = nodeInfo.map;

??????let n = nodeInfo.node;

??????let k, i, len, children = node.childNodes;


??????//先將該根節(jié)點插入到上一個節(jié)點中

??????root.appendChild(n);


??????//處理map數(shù)據(jù),這里的map是根對象,最初的map

??????for(k in _map) {

????????if(map[k]) {

??????????map[k].push(_map[k]);

????????} else {

??????????map[k] = _map[k];

????????}

??????}


??????for(i = 0, len = children.length; i < len; i++) {

????????this._runAllNode(children[i], map, n);

??????}


????}


????//處理每個節(jié)點,翻譯為頁面識別的節(jié)點,并且將需要操作的節(jié)點記錄

????splitTemplate () {

??????let nodes = $(this.template);

??????let map = {}, root = document.createElement('div');

??????let i, len;


??????for(i = 0, len = nodes.length; i < len; i++) {

????????this._runAllNode(nodes[i], map, root);

??????}


??????window.map = map;

??????return root

????}


??????//拆分目標(biāo)形成node,這個方法過長,真實項目需要拆分

????splitTemplate1 () {

??????let template = this.template;

??????let node = $(this.template)[0];

??????let map = {}, n, name, root = document.createElement('div');

??????let isEnd = false, index = 0, result;


??????let attrs, i, len, attr;

??????let reg = /{{([sS]+?)}}/;


??????window.map = map;


??????//開始遍歷節(jié)點,處理

??????while (!isEnd) {

????????name = node.localName;

????????attrs = node.attributes;

????????value = node.nodeValue;

????????n = document.createElement(this.labelMap[name] || name);


????????//說明是文本,需要記錄下來了

????????if(node.nodeType === 3) {

??????????n.innerText =??this.data[value] || '';


??????????result =??reg.exec(value);

??????????if(result) {

????????????n.innerText =??this.data[value] || '';


????????????if(!map[value]) map[value] = [];

????????????map[value].push({

??????????????type: 'text',

??????????????node: n

????????????});

??????????}

????????}


????????//這里暫時只處理屬性和值兩種情況,多了就復(fù)雜10倍了

????????for(i = 0, len = attrs.length; i < len; i++) {

??????????attr = attrs[i];

??????????result =??reg.exec(attr.value);


??????????n.setAttribute(attr.name, attr.value);

??????????//如果有node需要處理則需要存下來標(biāo)志

??????????if(result) {

????????????n.setAttribute(attr.name, this.data[result[1]] || '');


????????????//存儲所有會用到的節(jié)點,以便后面動態(tài)更新

????????????if(!map[result[1]]) map[result[1]] = [];

????????????map[result[1]].push({

??????????????type: 'attr',

??????????????name: attr.name,

??????????????node: n

????????????});


??????????}

????????}


debugger


????????if(index === 0) root.appendChild(n);

????????isEnd = true;

????????index++;


??????}


??????return root;



??????console.log(node)

????}


??}


??let view = new View();


??document.body.appendChild(window.node)


</script>

</body>

</html>


這段代碼,非常簡單:

① 設(shè)置了一段模板,甚至,我們這里根本不關(guān)系其格式化狀態(tài),直接寫成一行方便處理

this.template = '<view>{{pageShow}}</view><view class="ddd" is-show="{{pageShow}}" >{{pageShow}}<view class="c1">{{pageData}}</view></view>';

② 然后我們將這段模板轉(zhuǎn)為node節(jié)點(這里可以不用zepto,但是模擬實現(xiàn)怎么簡單怎么來吧),然后遍歷處理所有節(jié)點,我們就可以處理我們的數(shù)據(jù)了,最終形成了這個html:

<div><div><span>ffsd</span></div><div class="ddd" is-show="pageshow"><span>pageshow</span><div class="c1"><span>pageData</span></div></div></div>


③ 與此同時,我們存儲了一個對象,這個對象包含所有與之相關(guān)的節(jié)點:

這個對象是所有setData會影響到node的一個映射表,后面調(diào)用setData的時候,便可以直接操作對應(yīng)的數(shù)據(jù)了,這里我們分拆我們代碼,形成了幾個關(guān)鍵部分,首先是View類,這個對應(yīng)我們的模板,是核心類:

//View為模塊的實現(xiàn),主要用于解析目標(biāo)生產(chǎn)node

class View {

??constructor(template) {

????this.template = template;


????//由控制器page傳入的初始數(shù)據(jù)或者setData產(chǎn)生的數(shù)據(jù)

????this.data = {};


????this.labelMap = {

??????'view': 'div',

??????'#text': 'span'

????};


????this.nodes = {};

????this.root = {};

??}


??setInitData(data) {

????this.data = data;

??}


??//數(shù)據(jù)便會引起的重新渲染

??reRender(data, allData) {

????this.data = allData;

????let k, v, i, len, j, len2, v2;


????//開始重新渲染邏輯,尋找所有保存了的node

????for(k in data) {

??????if(!this.nodes[k]) continue;

??????for(i = 0, len = this.nodes[k].length; i < len; i++) {

????????for(j = 0; j < this.nodes[k][i].length; j++) {

??????????v = this.nodes[k][i][j];

??????????if(v.type === 'text') {

????????????v.node.innerText = data[k];

??????????} else if(v.type === 'attr') {

????????????v.node.setAttribute(v.name, data[k]);

??????????}

????????}

??????}

????}

??}

??/*

????傳入一個節(jié)點,解析出一個節(jié)點,并且將節(jié)點中的數(shù)據(jù)以初始化數(shù)據(jù)改變

????并且將其中包含{{}}標(biāo)志的節(jié)點信息記錄下來

??*/

??_handlerNode (node) {


????let reg = /{{([sS]+?)}}/;

????let result, name, value, n, map = {};

????let attrs , i, len, attr;


????name = node.nodeName;

????attrs = node.attributes;

????value = node.nodeValue;

????n = document.createElement(this.labelMap[name.toLowerCase()] || name);


????//說明是文本,需要記錄下來了

????if(node.nodeType === 3) {

??????n.innerText =??this.data[value] || '';


??????result =??reg.exec(value);

??????if(result) {

????????n.innerText =??this.data[result[1]] || '';


????????if(!map[result[1]]) map[result[1]] = [];

????????map[result[1]].push({

??????????type: 'text',

??????????node: n

????????});

??????}

????}


????if(attrs) {

??????//這里暫時只處理屬性和值兩種情況,多了就復(fù)雜10倍了

??????for (i = 0, len = attrs.length; i < len; i++) {

????????attr = attrs[i];

????????result = reg.exec(attr.value);


????????n.setAttribute(attr.name, attr.value);

????????//如果有node需要處理則需要存下來標(biāo)志

????????if (result) {

??????????n.setAttribute(attr.name, this.data[result[1]] || '');


??????????//存儲所有會用到的節(jié)點,以便后面動態(tài)更新

??????????if (!map[result[1]]) map[result[1]] = [];

??????????map[result[1]].push({

????????????type: 'attr',

????????????name: attr.name,

????????????node: n

??????????});


????????}

??????}

????}


????return {

??????node: n,

??????map: map

????}


??}


??//遍歷一個節(jié)點的所有子節(jié)點,如果有子節(jié)點繼續(xù)遍歷到?jīng)]有為止

??_runAllNode(node, map, root) {


????let nodeInfo = this._handlerNode(node);

????let _map = nodeInfo.map;

????let n = nodeInfo.node;

????let k, i, len, children = node.childNodes;


????//先將該根節(jié)點插入到上一個節(jié)點中

????root.appendChild(n);


????//處理map數(shù)據(jù),這里的map是根對象,最初的map

????for(k in _map) {

??????if(!map[k]) map[k] = [];

??????map[k].push(_map[k]);

????}


????for(i = 0, len = children.length; i < len; i++) {

??????this._runAllNode(children[i], map, n);

????}


??}


??//處理每個節(jié)點,翻譯為頁面識別的節(jié)點,并且將需要操作的節(jié)點記錄

??splitTemplate () {

????let nodes = $(this.template);

????let map = {}, root = document.createElement('div');

????let i, len;


????for(i = 0, len = nodes.length; i < len; i++) {

??????this._runAllNode(nodes[i], map, root);

????}


????this.nodes = map;

????this.root = root;

??}


??render() {

????let i, len;

????this.splitTemplate();

????for(i = 0, len = this.root.childNodes.length; i< len; i++)

??????document.body.appendChild(this.root.childNodes[0]);

??}


}


這個類主要完成的工作是:

接受傳入的template字符串(直接由index.wxml讀出)

解析template模板,生成字符串和兼職與node映射表,方便后期setData導(dǎo)致的改變

渲染和再次渲染工作

然后就是我們的Page類的實現(xiàn)了,這里反而比較簡單(當(dāng)然這里的實現(xiàn)是不完善的):

//這個為js羅杰部分實現(xiàn),后續(xù)會釋放工廠方法

class PageClass {

??//構(gòu)造函數(shù),傳入對象

??constructor(opts) {


????//必須擁有的參數(shù)

????this.data = {};

????Object.assign(this, opts);

??}


??//核心方法,每個Page對象需要一個模板實例

??setView(view) {

????this.view = view;

??}


??//核心方法,設(shè)置數(shù)據(jù)后會引發(fā)頁面刷新

??setData(data) {

????Object.assign(this.data, data);


????//只影響改變的數(shù)據(jù)

????this.view.reRender(data, this.data)

??}


??render() {

????this.view.setInitData(this.data);

????this.view.render();


????if(this.onLoad) this.onLoad();

??}


}


現(xiàn)在輪著我們實際調(diào)用方,Page方法出場了:

function Page (data) {

??let page = new PageClass(data);

??return page;

}


基本上什么都沒有干的感覺,調(diào)用層代碼這樣寫:

function main() {

??let view = new View('<view>{{pageShow}}</view><view class="ddd" is-show="{{pageShow}}" >{{pageShow}}<view class="c1">{{pageData}}</view></view>');

??let page = Page({

????data: {

??????pageShow: 'pageshow',

??????pageData: 'pageData',

??????pageShow1: 'pageShow1'

????},

????onLoad: function () {

??????this.setData({

????????pageShow: '我是pageShow啊'

??????});

????}

??});


??page.setView(view);

??page.render();

}


main();


于是,我們可以看到頁面的變化,由開始的初始化頁面到執(zhí)行onLoad時候的變化:


這里是最終完整的代碼:


<!DOCTYPE html>

<html lang="en">

<head>

??<meta charset="UTF-8">

??<title>Title</title>

</head>

<body>


<script type="text/javascript" src="libs/zepto.js" ></script>

<script type="text/javascript">


//這個為js羅杰部分實現(xiàn),后續(xù)會釋放工廠方法

class PageClass {

??//構(gòu)造函數(shù),傳入對象

??constructor(opts) {


????//必須擁有的參數(shù)

????this.data = {};

????Object.assign(this, opts);

??}


??//核心方法,每個Page對象需要一個模板實例

??setView(view) {

????this.view = view;

??}


??//核心方法,設(shè)置數(shù)據(jù)后會引發(fā)頁面刷新

??setData(data) {

????Object.assign(this.data, data);


????//只影響改變的數(shù)據(jù)

????this.view.reRender(data, this.data)

??}


??render() {

????this.view.setInitData(this.data);

????this.view.render();


????if(this.onLoad) this.onLoad();

??}


}


//View為模塊的實現(xiàn),主要用于解析目標(biāo)生產(chǎn)node

class View {

??constructor(template) {

????this.template = template;


????//由控制器page傳入的初始數(shù)據(jù)或者setData產(chǎn)生的數(shù)據(jù)

????this.data = {};


????this.labelMap = {

??????'view': 'div',

??????'#text': 'span'

????};


????this.nodes = {};

????this.root = {};

??}


??setInitData(data) {

????this.data = data;

??}


??//數(shù)據(jù)便會引起的重新渲染

??reRender(data, allData) {

????this.data = allData;

????let k, v, i, len, j, len2, v2;


????//開始重新渲染邏輯,尋找所有保存了的node

????for(k in data) {

??????if(!this.nodes[k]) continue;

??????for(i = 0, len = this.nodes[k].length; i < len; i++) {

????????for(j = 0; j < this.nodes[k][i].length; j++) {

??????????v = this.nodes[k][i][j];

??????????if(v.type === 'text') {

????????????v.node.innerText = data[k];

??????????} else if(v.type === 'attr') {

????????????v.node.setAttribute(v.name, data[k]);

??????????}

????????}

??????}

????}

??}

??/*

????傳入一個節(jié)點,解析出一個節(jié)點,并且將節(jié)點中的數(shù)據(jù)以初始化數(shù)據(jù)改變

????并且將其中包含{{}}標(biāo)志的節(jié)點信息記錄下來

??*/

??_handlerNode (node) {


????let reg = /{{([sS]+?)}}/;

????let result, name, value, n, map = {};

????let attrs , i, len, attr;


????name = node.nodeName;

????attrs = node.attributes;

????value = node.nodeValue;

????n = document.createElement(this.labelMap[name.toLowerCase()] || name);


????//說明是文本,需要記錄下來了

????if(node.nodeType === 3) {

??????n.innerText =??this.data[value] || '';


??????result =??reg.exec(value);

??????if(result) {

????????n.innerText =??this.data[result[1]] || '';


????????if(!map[result[1]]) map[result[1]] = [];

????????map[result[1]].push({

??????????type: 'text',

??????????node: n

????????});

??????}

????}


????if(attrs) {

??????//這里暫時只處理屬性和值兩種情況,多了就復(fù)雜10倍了

??????for (i = 0, len = attrs.length; i < len; i++) {

????????attr = attrs[i];

????????result = reg.exec(attr.value);


????????n.setAttribute(attr.name, attr.value);

????????//如果有node需要處理則需要存下來標(biāo)志

????????if (result) {

??????????n.setAttribute(attr.name, this.data[result[1]] || '');


??????????//存儲所有會用到的節(jié)點,以便后面動態(tài)更新

??????????if (!map[result[1]]) map[result[1]] = [];

??????????map[result[1]].push({

????????????type: 'attr',

????????????name: attr.name,

????????????node: n

??????????});


????????}

??????}

????}


????return {

??????node: n,

??????map: map

????}


??}


??//遍歷一個節(jié)點的所有子節(jié)點,如果有子節(jié)點繼續(xù)遍歷到?jīng)]有為止

??_runAllNode(node, map, root) {


????let nodeInfo = this._handlerNode(node);

????let _map = nodeInfo.map;

????let n = nodeInfo.node;

????let k, i, len, children = node.childNodes;


????//先將該根節(jié)點插入到上一個節(jié)點中

????root.appendChild(n);


????//處理map數(shù)據(jù),這里的map是根對象,最初的map

????for(k in _map) {

??????if(!map[k]) map[k] = [];

??????map[k].push(_map[k]);

????}


????for(i = 0, len = children.length; i < len; i++) {

??????this._runAllNode(children[i], map, n);

????}


??}


??//處理每個節(jié)點,翻譯為頁面識別的節(jié)點,并且將需要操作的節(jié)點記錄

??splitTemplate () {

????let nodes = $(this.template);

????let map = {}, root = document.createElement('div');

????let i, len;


????for(i = 0, len = nodes.length; i < len; i++) {

??????this._runAllNode(nodes[i], map, root);

????}


????this.nodes = map;

????this.root = root;

??}


??render() {

????let i, len;

????this.splitTemplate();

????for(i = 0, len = this.root.childNodes.length; i< len; i++)

??????document.body.appendChild(this.root.childNodes[0]);

??}


}


function Page (data) {

??let page = new PageClass(data);

??return page;

}


function main() {

??let view = new View('<view>{{pageShow}}</view><view class="ddd" is-show="{{pageShow}}" >{{pageShow}}<view class="c1">{{pageData}}</view></view>');

??let page = Page({

????data: {

??????pageShow: 'pageshow',

??????pageData: 'pageData',

??????pageShow1: 'pageShow1'

????},

????onLoad: function () {

??????this.setData({

????????pageShow: '我是pageShow啊'

??????});

????}

??});


??page.setView(view);

??page.render();

}


main();


</script>

</body>

</html>


我們簡單的模擬便先到此結(jié)束,這里結(jié)束的比較倉促有一些原因:

這段代碼可以是最終打包構(gòu)建形成的代碼,但是我這里的完成度只有百分之一,后續(xù)需要大量的構(gòu)建相關(guān)介入

這篇文章目的還是接受開發(fā)基礎(chǔ),而本章模擬實現(xiàn)太過復(fù)雜,如果篇幅大了會主旨不清

這個是最重要的點,我一時也寫不出來啊?。。?,所以各位等下個長篇,小程序前端框架模擬實現(xiàn)吧

如果繼續(xù)實現(xiàn),這里馬上要遇到組件處理、事件模型、分文件構(gòu)建等高端知識,時間會拉得很長

感興趣的小伙伴,可以關(guān)注公眾號【grain先森】,回復(fù)關(guān)鍵詞 “小程序”,獲取更多資料,更多關(guān)鍵詞玩法期待你的探索~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容