内容简介:这个轮子从18年4月22造到18年10月12日,本来就是看了一个文章讲前端框架的路由实现原理之后,想试着撸一个路由试试,结果越写越多,到最后就莫名其妙变成了个mvvm框架了。顺便写了个比较渣的文档和服务端渲染。。。名字其实是瞎起的,因为组件要被包在一个div里,所以叫了整个项目是用
这个轮子从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容器内创建出来,每次注入都会重新通过工厂函数创建个新的服务实例 - 通过注解
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不少。作为一个前端菜鸡,还是在深知自己众多不足以及明白好记性不如烂笔头的道理下,多造轮子总归不会错的。
最后感谢各位大佬看到最后
以上所述就是小编给大家介绍的《用typescript撸个前端框架InDiv》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。