30分鐘熟悉微信小程序

作者:葉小釵?

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


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

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

所以說,其實微信小程序是一套超級Hybrid的解決方案,現在看來,小程序應該是應用場景最廣,也最為復雜的解決方案了。

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

微信小程序是給各個公司開發者接入的,其他公司平臺多是給自己業務團隊使用,這一根本區別,就造就了我們看到的很多小程序不一樣的特性:

小程序定義了自己的標簽語言WXML

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

小程序提供了一套前端框架包括對應Native API

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

只要了解到這些區別就會知道為什么小程序會這么設計:

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

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

另一方面,經過之前的學習,我這邊明確可以得出一個感受:

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

標簽的展示只與data有關聯,和js是隔離的,沒有辦法在標簽中調用js的方法

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


this.setData({'wxml': `

??<my-component>

??<view>動態插入的節點</view>

??</my-component>

`});


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

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

project

├── pages

|?? ├── index

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

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

|?? |?? ├── index.wxml??index 頁面結構

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

|?? └── log

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

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

|?????? ├── log.js??????log 頁面結構

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

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

├── app.json????????????小程序公共設置

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


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

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

所有的小程序基本都最后都被打成上面的結構

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

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

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

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

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

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

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

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

所有的小程序基本都最后都被打成上面的結構

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

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

WAConsole.js 框架JS庫,控制臺

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

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

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

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

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

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

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

開發工具+調試工具

開發基本模型(開發基本標準WXML、WXSS、JS、JSON)

完善的構建(對業務方透明)

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

監控統計邏輯

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

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

工程部門為業務開發者封裝最小化開發環境,最優為瀏覽器,確實不行便為其提供一個類似瀏覽器的調試環境

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

稍微大點的公司,稍微寬裕的團隊,還會同步做很多后續的性能監控、錯誤日志工作,如此形成一套文檔->開發->調試->構建->發布->監控、分析 為一套完善的技術體系

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

微信小程序的執行流程

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

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

"pages":[

??"pages/index/index",

??"pages/list/list",

??"pages/logs/logs"

],


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

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

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

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

WXML&CSS編譯形成的JS View實例化結束,準備結束時向業務線程發送通知

業務線程中的JS Page部分同步完成實例化結束,這個時候接收到View線程部分的等待數據通知,將初始化data數據發送給View

View線程接到數據,開始渲染頁面,渲染結束執行通知Page觸發onReady事件

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

__appServiceSDK__.invokeWebviewMethod({

????name: "appDataChange",

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

????????complete: n

????}),

????webviewIds: [t]

})


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

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

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

模擬實現

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

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

<view>

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

??<my-component inner-text="組件數據"></my-component>

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

</view>

Page({

??data: {

????pageData: '頁面數據'

??},

??onLoad: function () {

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

??},

})

<!-- 這是自定義組件的內部WXML結構 -->

<view class="inner">

??{{innerText}}

</view>

<slot></slot>

Component({

??properties: {

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

????innerText: {

??????type: String,

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

????}

??},

??data: {

????// 這里是一些組件內部數據

????someData: {}

??},

??methods: {

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

????customMethod: function () { }

??}

})

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

我們需要做的就是讓這段代碼運行起來,而這里的目錄是我們最終看見的目錄,真實運行的時候可能不是這個樣,運行之前項目會通過我們的工程構建,變成可以直接運行的代碼,而我這里思考的可以運行的代碼事實上是一個模塊,所以我們這里從最終結果反推、分拆到開發結構目錄,我們首先將所有代碼放到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傳入的初始數據或者setData產生的數據

??????this.data = {

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

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

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

??????};


??????this.labelMap = {

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

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

??????};


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

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

????}


????/*

??????傳入一個節點,解析出一個節點,并且將節點中的數據以初始化數據改變

??????并且將其中包含{{}}標志的節點信息記錄下來

????*/

????_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) {

????????//這里暫時只處理屬性和值兩種情況,多了就復雜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需要處理則需要存下來標志

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

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


????????????//存儲所有會用到的節點,以便后面動態更新

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

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

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

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

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

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


??????????}

????????}

??????}


??????return {

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

????????map: map

??????}


????}


????//遍歷一個節點的所有子節點,如果有子節點繼續遍歷到沒有為止

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


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

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

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

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


??????//先將該根節點插入到上一個節點中

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


??????//處理map數據,這里的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);

??????}


????}


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

????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

????}


??????//拆分目標形成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;


??????//開始遍歷節點,處理

??????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

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

??????????}

????????}


????????//這里暫時只處理屬性和值兩種情況,多了就復雜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需要處理則需要存下來標志

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

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


????????????//存儲所有會用到的節點,以便后面動態更新

????????????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>


這段代碼,非常簡單:

① 設置了一段模板,甚至,我們這里根本不關系其格式化狀態,直接寫成一行方便處理

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

② 然后我們將這段模板轉為node節點(這里可以不用zepto,但是模擬實現怎么簡單怎么來吧),然后遍歷處理所有節點,我們就可以處理我們的數據了,最終形成了這個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>


③ 與此同時,我們存儲了一個對象,這個對象包含所有與之相關的節點:

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

//View為模塊的實現,主要用于解析目標生產node

class View {

??constructor(template) {

????this.template = template;


????//由控制器page傳入的初始數據或者setData產生的數據

????this.data = {};


????this.labelMap = {

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

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

????};


????this.nodes = {};

????this.root = {};

??}


??setInitData(data) {

????this.data = data;

??}


??//數據便會引起的重新渲染

??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]);

??????????}

????????}

??????}

????}

??}

??/*

????傳入一個節點,解析出一個節點,并且將節點中的數據以初始化數據改變

????并且將其中包含{{}}標志的節點信息記錄下來

??*/

??_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) {

??????//這里暫時只處理屬性和值兩種情況,多了就復雜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需要處理則需要存下來標志

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

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


??????????//存儲所有會用到的節點,以便后面動態更新

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

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

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

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

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

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


????????}

??????}

????}


????return {

??????node: n,

??????map: map

????}


??}


??//遍歷一個節點的所有子節點,如果有子節點繼續遍歷到沒有為止

??_runAllNode(node, map, root) {


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

????let _map = nodeInfo.map;

????let n = nodeInfo.node;

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


????//先將該根節點插入到上一個節點中

????root.appendChild(n);


????//處理map數據,這里的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);

????}


??}


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

??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導致的改變

渲染和再次渲染工作

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

//這個為js羅杰部分實現,后續會釋放工廠方法

class PageClass {

??//構造函數,傳入對象

??constructor(opts) {


????//必須擁有的參數

????this.data = {};

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

??}


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

??setView(view) {

????this.view = view;

??}


??//核心方法,設置數據后會引發頁面刷新

??setData(data) {

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


????//只影響改變的數據

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

??}


??render() {

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

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


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

??}


}


現在輪著我們實際調用方,Page方法出場了:

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


于是,我們可以看到頁面的變化,由開始的初始化頁面到執行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羅杰部分實現,后續會釋放工廠方法

class PageClass {

??//構造函數,傳入對象

??constructor(opts) {


????//必須擁有的參數

????this.data = {};

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

??}


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

??setView(view) {

????this.view = view;

??}


??//核心方法,設置數據后會引發頁面刷新

??setData(data) {

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


????//只影響改變的數據

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

??}


??render() {

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

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


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

??}


}


//View為模塊的實現,主要用于解析目標生產node

class View {

??constructor(template) {

????this.template = template;


????//由控制器page傳入的初始數據或者setData產生的數據

????this.data = {};


????this.labelMap = {

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

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

????};


????this.nodes = {};

????this.root = {};

??}


??setInitData(data) {

????this.data = data;

??}


??//數據便會引起的重新渲染

??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]);

??????????}

????????}

??????}

????}

??}

??/*

????傳入一個節點,解析出一個節點,并且將節點中的數據以初始化數據改變

????并且將其中包含{{}}標志的節點信息記錄下來

??*/

??_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) {

??????//這里暫時只處理屬性和值兩種情況,多了就復雜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需要處理則需要存下來標志

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

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


??????????//存儲所有會用到的節點,以便后面動態更新

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

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

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

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

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

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


????????}

??????}

????}


????return {

??????node: n,

??????map: map

????}


??}


??//遍歷一個節點的所有子節點,如果有子節點繼續遍歷到沒有為止

??_runAllNode(node, map, root) {


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

????let _map = nodeInfo.map;

????let n = nodeInfo.node;

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


????//先將該根節點插入到上一個節點中

????root.appendChild(n);


????//處理map數據,這里的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);

????}


??}


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

??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>


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

這段代碼可以是最終打包構建形成的代碼,但是我這里的完成度只有百分之一,后續需要大量的構建相關介入

這篇文章目的還是接受開發基礎,而本章模擬實現太過復雜,如果篇幅大了會主旨不清

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

如果繼續實現,這里馬上要遇到組件處理、事件模型、分文件構建等高端知識,時間會拉得很長

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,106評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,441評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,211評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,736評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,475評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,834評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,829評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,009評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,559評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,306評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,516評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,038評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,728評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,132評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,443評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,249評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,484評論 2 379

推薦閱讀更多精彩內容