用typescript擼個前端框架InDiv

全菜工程師發表於2018-10-12

有個同事跟我說:需求還是不夠多,都有時間造輪子了。。。

前言

這個輪子從18年4月22造到18年10月12日,本來就是看了一個文章講前端框架的路由實現原理之後,想試著擼一個路由試試,結果越寫越多,到最後就莫名其妙變成了個mvvm框架了。順便寫了個比較渣的文件和服務端渲染。。。

當前版本:v1.2.0
專案地址
文件
npm

image

InDiv簡介

名字其實是瞎起的,因為元件要被包在一個div裡,所以叫了InDiv
整個專案是用typescript寫的,真心說一句ts真優雅。
在思考怎麼寫的時候參照了大量ng react vue的架構與實踐,用自己能想到的最好的方法實現了一下,也算是對自己的鍛鍊了一番(其實在寫的時候,發現越寫越像ng,可能是我真的太喜歡angular了吧)。
之後還實現了一個服務端渲染的,但是有點簡陋。。。
此刻多麼想致敬下三大框架的開發者大佬們,造輪子不易

  1. 主要分為模組(NvModule),元件(Component),和服務。
  2. 模板使用字串模板,我自己定義了一些例如:nv-class,nv-repeat等指令,然後再模板中僅僅可以使用來自元件例項中state的值($.)和例項的方法(@),所以顯得比較醜陋(先造出來再說)。
  3. 暫時沒有指令和pipe。其實在字串模板的裡可以使用元件上帶有返回值的方法(nv-src="@buildSrc($.src)"),返回值會被渲染到模板中,也算是暫時沒有做出pipe的補充。
  4. 自帶路由,採用基於virtual DOM的非同步渲染,但是路由懶載入暫時還沒有。
  5. 模組負責匯入匯出元件,匯入其他模組和註冊服務。當前模組內的元件可以使用來自根模組和當前模組的任何服務及元件,也可以使用被匯入模組中匯出的元件
  6. 如果沒有特殊宣告,在任何模組中被宣告的服務將成為全域性單例,但元件或服務只能注入當前模組內的服務或來自根模組的服務;而在元件中被宣告的服務將跟元件例項走,每個元件例項都有一個獨立的服務例項。(其實是實現了個3級的注入器)。
  7. 元件實現了幾個生命週期,在ts裡可以通過implements型別,而在js裡只能手寫生命週期方法。
  8. 通過Object.defineProperty監聽state,任何直接更改 state的屬性 及 通過setState更改state 的操作都為同步操作,會引起當前元件的重新渲染;而在子元件中,通過呼叫props中父元件的方法去更改父元件state的時候,子元件不會立刻就得到更新後的props,因為渲染為非同步的,而且渲染之後才能得到propos
  9. 因為使用Object.defineProperty監聽state,所以無法監聽到state中陣列item的增加插入移除,所以如果想更改陣列結構請只用setState重置state中的該項。
  10. 在ts中實現了依靠constructor的引數型別當做令牌的依賴注入並通過@Injected宣告需要注入;在js中只實現了依靠靜態屬性injectTokens: string[]宣告字串當令牌的服務。
  11. 封了了axios作為http服務,並在Utils類中集合了一些我平時用的工具。

使用

元件

  1. 通過註解Component來提供後設資料,並宣告選擇器,模板,及元件providers
  2. 通過註解Injected來宣告下面的類需要注入服務
  3. 通過implements來實現生命週期鉤子函式
  4. 提供SetState, GetLocation, SetLocation等型別,在元件中可以使用ths.setState, this.getLocation, this.setLocation等內建方法來改變狀態、獲取路由狀態、設定路由狀態等
  5. 如果使用JavaScript開發,除了不能使用Injected宣告需要注入服務而通過類的靜態屬性injectTokens: string[]注入服務之外都差不多
import { Component, SetState, GetLocation, SetLocation, Injected, OnInit, RouteChange, OnDestory } from 'indiv';
import TestService from '../../service/test';

@Injected
@Component({
    selector: 'app-container-component',
    template: (`
        <div class="app-container" nv-class="$.showSideBar" nv-on:click="@changeShowSideBar()">
            <side-bar handle-side-bar="{@changeShowSideBar}" can-show-sidebar="{$.showSideBar}" ></side-bar>
            <div nv-repeat="let test in $.testList" nv-key="test.id">
                name:{{test.name}} id:{{test.id}}
                 <side-bar nv-key="test.id" handle-side-bar="{@changeShowSideBar}" can-show-sidebar="{$.showSideBar}" ></side-bar>
            </div>
            <router-render></router-render>
        </div>
    `),
    providers: [{
        provide: TestService,
        useClass: TestService,
    },
    // 也可以直接 TestService,TestService當做令牌
    ],
})
export default class AppContainerComponent implements OnInit, RouteChange, OnDestory {
    public state: {
        showSideBar: string;
        testList: {id: number;name: string;}[];
    }
    public setState: SetState;

    constructor(
        private testS: TestService,
    ) {
        this.subscribeToken = this.testS.subscribe(this.subscribe);
    }
    
    public nvOnInit() {
        this.state = {
            showSideBar: 'open',
            testList: [
                {
                    id:0,
                    name: 'dima'
                },
                {
                    id: 1,
                    name: 'xxx'
                }
            ]
        };
    }
    
    public nvRouteChange(lastRoute?: string, newRoute?: string): void {}
    
    public nvOnDestory() {
        this.subscribeToken.unsubscribe();
    }

    public changeShowSideBar() {
        if (this.state.showSideBar === 'open') {
            this.state.showSideBar = 'close';
        } else {
            // this.state.showSideBar = 'open'; 也可以用setState
            this.setState({showSideBar: 'close'});
        }
    }
} 
複製程式碼

服務與依賴注入

  1. angular的服務類似預設為全域性單例,但是可以在@Injectable({isSingletonMode: false})指定isSingletonModefalse,這樣該服務例項就不會在IOC容器內建立出來,每次注入都會重新通過工廠函式建立個新的服務例項
  2. 通過註解Injected,服務也能被注入其他服務
  3. 推薦使用rxjs來實現元件通訊
  4. 服務可以在元件,模組中宣告,但是有些不同
  5. 模仿了ng的實現
import { Subject, Subscription } from 'rxjs';
import { Injectable, Injected } from 'indiv';

@Injected
@Injectable()
export default class TestService {
  public data: number;
  public subject: Subject<any>;

  constructor(
    private testService2: TestService2
  ) {
    this.data = 1;
    this.subject = new Subject();
  }
  
  public subscribe(fun: (value: any) => void): Subscription {
    return this.subject.subscribe({
      next: fun,
    });
  }

  public update(value: any) {
    this.subject.next({
      next: value,
    });
  }

  public unsubscribe() {
    this.subject.subscribe();
  }
}
複製程式碼

模組

  1. 模組通過@NvModule裝飾器接收五個引數,宣告某些元件(component)、服務(service)屬於這個模組
  2. 模組可以匯出元件給其他模組用
  3. 整個應用需要一個根模組,並且如果不使用路由則組要在根模組定義bootstrap
import { NvModule } from 'indiv';

import AppContainerComponent from '../pages/app.container.component';
import TestService from '../service/test.service';
import TestService2 from '../service/test2.service';

@NvModule({
    imports: [], // 引入其他模組
    providers: [
        {
            provide: TestService,
            useClass: TestService,
        },
        TestService2,
    ],
    components: [
        AppContainerComponent,
    ],
    exports: [
        AppContainerComponent,
    ],
    bootstrap: AppContainerComponent, // 如果不適用路由需要在根模組宣告bootstrap的元件
})
export default class AppModule { }

import { InDiv } from 'indiv';
const inDiv = new InDiv();
inDiv.bootstrapModule(AppModule);
// inDiv.use(router); 使用路由
inDiv.init();
複製程式碼

生命週期鉤子

僅僅實現了下面這幾種,這裡又大量借鑑了react。除此之外class的setter getter也可以當做生命週期

      constructor()
      nvOnInit(): void;
      nvBeforeMount(): void;
      nvAfterMount(): void;
      nvHasRender(): void;
      nvOnDestory(): void;
      nvWatchState(oldState?: State): void;
      nvRouteChange(lastRoute?: string, newRoute?: string): void;
      nvReceiveProps(nextProps: State): void;
複製程式碼

虛擬DOM

通過將DOM結構轉化為VNode,並diff出差異並應用在真實DOM上,其實也類似react的diff演算法。

diff子元素

  1. 只diff同級子元素,禁止跨層級diff
  2. 優先匹配新舊VNode中tagName和key都相同的元素,並計算位置差異
  3. 舊VNode中的子元素如果沒有匹配上則放入移除佇列
  4. 新VNode中的子元素如果沒有匹配上,則找到它的位置放入插入佇列
  5. 匹配到的兩個新舊VNode如果不是InDiv自定義的元件元素,則開始diff兩個匹配元素的屬性事件等並繼續diff下一層子元素
  6. 匹配到的兩個新舊VNode如果是InDiv自定義的元件元素,則跳過匹配下一層子元素,將diff交給元件的compiler
  7. 最後在每個元件的內部統一update各個佇列,遵循先移除後插入再替換屬性等

to do

  1. 支援自定義指令
  2. 路由懶載入
  3. 使用Proxy代替Object.defineProperty或實現髒檢查取消stateprops
  4. @indiv/cli

最後

關於其他字串模板,http ,utils 路由等在文件 裡都有,文筆不好還請各位見諒(估計沒人能看懂)。
如果看不懂的話可以去看文件的原始碼,完全用indiv實現。(吹一波牛逼)
其實整個專案就是一時興起寫的,也沒有寫單元測試,估計bug不少。作為一個前端菜雞,還是在深知自己眾多不足以及明白好記性不如爛筆頭的道理下,多造輪子總歸不會錯的。
最後感謝各位大佬看到最後

相關文章