作者:葉小釵?
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先森】,回復關鍵詞 “小程序”,獲取更多資料,更多關鍵詞玩法期待你的探索~