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

InDiv簡介
名字其實是瞎起的,因為元件要被包在一個div裡,所以叫了 InDiv
。
整個專案是用 typescript
寫的,真心說一句ts真優雅。
在思考怎麼寫的時候參照了大量ng react vue的架構與實踐,用自己能想到的最好的方法實現了一下,也算是對自己的鍛鍊了一番(其實在寫的時候,發現越寫越像ng,可能是我真的太喜歡angular了吧)。
之後還實現了一個 服務端渲染 的,但是有點簡陋。。。
此刻多麼想致敬下三大框架的開發者大佬們,造輪子不易
- 主要分為模組(NvModule),元件(Component),和服務。
- 模板使用字串模板,我自己定義了一些例如:
nv-class
,nv-repeat
等指令,然後再模板中僅僅可以使用來自元件例項中state的值($.
)和例項的方法(@
),所以顯得比較醜陋(先造出來再說)。 - 暫時沒有指令和pipe。其實在字串模板的裡可以使用元件上帶有返回值的方法(
nv-src="@buildSrc($.src)"
),返回值會被渲染到模板中,也算是暫時沒有做出pipe的補充。 - 自帶路由,採用基於virtual DOM的非同步渲染,但是路由懶載入暫時還沒有。
- 模組負責匯入匯出元件,匯入其他模組和註冊服務。當前模組內的元件可以使用來 自根模組和當前模組 的任何服務及元件,也可以使用 被匯入模組中匯出的元件 。
- 如果沒有特殊宣告,在任何模組中被宣告的服務將成為 全域性單例 ,但元件或服務只能注入當前模組內的服務或來自根模組的服務;而在元件中被宣告的服務將跟元件例項走,每個元件例項都有一個 獨立的服務例項 。(其實是實現了個3級的注入器)。
- 元件實現了幾個生命週期,在ts裡可以通過
implements
型別,而在js裡只能手寫生命週期方法。 - 通過
Object.defineProperty
監聽state
,任何直接更改state
的屬性 及 通過setState
更改state
的操作都為同步操作,會引起當前元件的重新渲染;而在子元件中,通過呼叫props
中父元件的方法去更改父元件state
的時候,子元件不會立刻就得到更新後的props
,因為渲染為非同步的,而且渲染之後才能得到propos
。 - 因為使用
Object.defineProperty
監聽state
,所以無法監聽到state中陣列item的增加插入移除,所以如果想更改陣列結構請只用setState
重置state
中的該項。 - 在ts中實現了依靠
constructor
的引數型別當做令牌的依賴注入並通過@Injected
宣告需要注入;在js中只實現了依靠靜態屬性injectTokens: string[]
宣告字串當令牌的服務。 - 封了了
axios
作為http服務,並在Utils類中集合了一些我平時用的工具。
使用
元件
- 通過註解
Component
來提供元資料,並宣告選擇器,模板,及元件providers - 通過註解
Injected
來宣告下面的類需要注入服務 - 通過
implements
來實現生命週期鉤子函式 - 提供
SetState, GetLocation, SetLocation
等型別,在元件中可以使用ths.setState, this.getLocation, this.setLocation
等內建方法來改變狀態、獲取路由狀態、設定路由狀態等 - 如果使用
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'}); } } } 複製程式碼
服務與依賴注入
- 跟
angular
的服務類似預設為全域性單例,但是可以在@Injectable({isSingletonMode: false})
指定isSingletonMode
為false
,這樣該服務例項就不會在IOC/">IOC容器內創建出來,每次注入都會重新通過工廠函式建立個新的服務例項 - 通過註解
Injected
,服務也能被注入其他服務 - 推薦使用rxjs來實現元件通訊
- 服務可以在元件,模組中宣告,但是有些不同
- 模仿了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(); } } 複製程式碼
模組
@NvModule 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子元素
- 只diff同級子元素,禁止跨層級diff
- 優先匹配新舊VNode中tagName和key都相同的元素,並計算位置差異
- 舊VNode中的子元素如果沒有匹配上則放入移除佇列
- 新VNode中的子元素如果沒有匹配上,則找到它的位置放入插入佇列
- 匹配到的兩個新舊VNode如果不是InDiv自定義的元件元素,則開始diff兩個匹配元素的屬性事件等並繼續diff下一層子元素
- 匹配到的兩個新舊VNode如果是InDiv自定義的元件元素,則跳過匹配下一層子元素,將diff交給元件的compiler
- 最後在每個元件的內部統一update各個佇列,遵循先移除後插入再替換屬性等
to do
- 支援自定義指令
- 路由懶載入
- 使用
Proxy
代替Object.defineProperty
或實現髒檢查取消state
和props
- @indiv/cli
最後
關於其他字串模板,http ,utils 路由等在文件 裡都有,文筆不好還請各位見諒(估計沒人能看懂)。
如果看不懂的話可以去看 文件的原始碼 ,完全用indiv實現。(吹一波牛逼)
其實整個專案就是一時興起寫的,也沒有寫單元測試,估計bug不少。作為一個前端菜雞,還是在深知自己眾多不足以及明白好記性不如爛筆頭的道理下,多造輪子總歸不會錯的。
最後感謝各位大佬看到最後