用typescript撸个前端框架InDiv

栏目: CSS · 发布时间: 6年前

内容简介:这个轮子从18年4月22造到18年10月12日,本来就是看了一个文章讲前端框架的路由实现原理之后,想试着撸一个路由试试,结果越写越多,到最后就莫名其妙变成了个mvvm框架了。顺便写了个比较渣的文档和服务端渲染。。。名字其实是瞎起的,因为组件要被包在一个div里,所以叫了整个项目是用

这个轮子从18年4月22造到18年10月12日,本来就是看了一个文章讲前端框架的路由实现原理之后,想试着撸一个路由试试,结果越写越多,到最后就莫名其妙变成了个mvvm框架了。顺便写了个比较渣的文档和服务端渲染。。。

当前版本:v1.2.0

项目地址

文档

npm
用typescript撸个前端框架InDiv

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();
  }
}
复制代码

模块

@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子元素

  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不少。作为一个前端菜鸡,还是在深知自己众多不足以及明白好记性不如烂笔头的道理下,多造轮子总归不会错的。

最后感谢各位大佬看到最后


以上所述就是小编给大家介绍的《用typescript撸个前端框架InDiv》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Web标准之道

Web标准之道

阿一、棕熊、李战、丁学 / 人民邮电出版社 / 2009-8 / 35.00元

《Web标准之道:博客园精华集》由博客园知名博主联手打造,涉及Web标准、HTML/CSS、JavaScript、SEO优化等诸多领域,内容新颖,观点独特,妙语连珠。《Web标准之道:博客园精华集》并不是一本由代码和技巧堆积而成的集合,更多的是探讨了Web设计中若干理念和心得,其中多为经验之谈。无论对于从事Web前端设计的人士,还是对于那些从事Web后端编程的技术人员,《Web标准之道:博客园精华......一起来看看 《Web标准之道》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

SHA 加密
SHA 加密

SHA 加密工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具