一篇关于 ExtJS 的推荐

This article isn’t available in English yet. Here’s the 简体中文 version for now.
English version’s here

前言

这是一篇推荐 ExtJS 框架的文章,ExtJS 是我最近来非常喜欢的前端框架,它是由 Sencha 公司开发维护的商业软件,授权比较贵,但是它同样提供了基于 GPL 协议的社区版。虽然通常社区版更新的比较慢,但是你可以使用它的绝大部分功能。

但是自 ExtJS 4.x 版本之后,国内的开发者使用的非常之少,相对的中文资料也非常少。究其原因据我猜测可能是因为授权问题、中文文档差一点、以及不太适合 to-C 应用。而 B 端客户是中国互联网公司不太重视的地方 —— 可能并没有像做 to-C 应用一样有高昂的利润。

为什么我要使用 ExtJS 而不是 Angular / Vue / React

设计

首先,因为 Angular / Vue / React 本身并不提供设计,所以使用它们开发的各种UI框架层出不穷,像是 Ant Design / Material - UI / ElementUI / IView 等等,设计理念越来越趋向同质化。但在 to-B 的应用中,我认为当前的设计语言方向并不太实用,充斥着大量的留白空间、硕大的内外边距、花哨但无用的特效等,让我在做 B 端应用的时候不敢苟同。

反观 ExtJS 一直是非常统一而且不断在进步,当前版本提供了 classicmordern 两个 toolkit,分别适用于桌面环境和移动环境,提供了几乎所有你能用到的 UI 组件,而且设计高度统一, classic toolkit 里面的 theme-classic 主题是永恒的经典,十几年过去仍不显过时 —— 我认为这是所有 ExtJS 官方主题里面为数不多的还算好看的主题了。

开发

诸多前端框架中,我认为 Angular (目前我还没有用它开发过任何实际上线的应用) 和 ExtJS 是最接近后端开发思想的,尤其是 ExtJS,后端人员理解起来相对其他的框架要好一点,可以以更加工程化的代码组织方式来进行开发,这对整个项目的结构是个非常好的事情。在前端越来越碎片化的今天,这可以有效的降低一些学习成本,提高项目的整体质量。

ExtJS 的提供了各种各样的组件、工具包,而且可以非常方便的与扩展和无侵入重写,你几乎不需要使用其他的第三方库就可以工作的非常好,不用担心各种版本问题、冲突问题等等。

文档

ExtJS 的 API 文档非常之详尽,对开发者非常友好,所有的配置、属性、方法、事件等等一目了然。 而且由于所有用到的组件出自自家,文档风格和内容也高度统一。 ExtJS 的文档

其他

然后就该说一下其他框架了,因为 AngularJS 我对 Angular 抱有非常大的成见,可能很多人都不记得这个框架了,它实际可以算是 Angular 的前身,在 Vue / React 还没出现之前它是最酷的前端框架,我曾经开发过一个开源项目 ONES ,它的前端就是基于 AngularJS 开发的,它让我见识了一个完全不一样的前端世界。但是后来 Google 放弃了它,这也是我不再维护这个项目的原因之一,它伤透了我的心。

可能是因为我的技术水平问题,在一个 Vue 2 + ElementUI 的前端中型项目中的表现并不是非常好。而对于React,我感觉唯一的原因就是我不太喜欢an JSX 的写法。

另外目前为止我还没有使用 Angular / React / Vue 做过非常复杂的应用,这可能也是我对他们的理解并没有那么深刻的原因。

用 ExtJS 做过的项目

这是一个比较典型的内部应用,前端涉及到了 ExtJS / Electron / Vue 等等很多技术,使用的是 ExtJS classic toolkit。通常我在做这种项目的时候喜欢做一些通用的视图,比如 Form / Grid / Bill / DetailView / QueryWindow 等等,这样可以有效的减少大量的同质开发工作,而且得益于 ExtJS 的类机制和Mixins机制,他们工作的非常好。 前端代码并不需要写很多,只需要后端定义好 Entity / Vo 就可以自动生成相应的 DataStructure 供前端使用,前端会根据不同的字段类型使用不同的控件,而且前端也可以进行相应的自定义调整。使用 Python 做了代码生成器,这样几乎无需写任何代码就可以完成一个模块基本的 CRUD 操作。

当然后端也有相应的通用控制器来进行请求的处理,通常只需要定义 Entity 和 Vo 的类成员变量就可以了,甚至如果你不需要 Vo 都可以不定义,JPA 会处理数据库结构同步,CommonRestHandler 会负责CRUD / 导入 / 导出 / 处理权限校验等等工作。

我认为这是一个非常好的方法,这有助于减少大量的同质代码,进行了新一层的抽象。

截图分享

jones screenshot jones screenshot 2

一些代码

Ext.define('yas.lib.base.Grid', {
requires: [
"Ext.grid.Panel",
"Ext.grid.filters.Filters"
],
extend: "Ext.grid.Panel",
autoDestroy: false,
columnLines: true,

    // enableLocking: true,

    // 默认自动加载数据
    autoLoad: true,
    
    border: false,
    plugins: {
        gridfilters: true
    },
    layout: "fit",
    scrollable: true,
    deferRowRender: true,

    initQueryParams: null,
    xtype: "basegrid",

    features: [{
        ftype: 'summary',
        dock: "bottom"
    }, {
        ftype: 'grouping',
        collapsible: false,
        groupHeaderTpl: ['<input class="group-checkbox" type="checkbox" /> {columnName}: {name}({children.length})'],
        enableNoGroups: true
    }],

    // enableLocking: true,

    // 按钮类型
    actionTypes: [
        "add", "edit", "delete", "-",
        "refresh", "-", "export", "import",
        "-", "trash", "restore"
    ],
    additionalActionTypes: [
        "->", "query"
    ],
    actionConfigs: {},
    
    scroll :true,
    viewConfig:{
        stripeRows:true,
        enableTextSelection:true,
        scrollable: true
    },
    
    selModel: {
        injectCheckbox: 0,
        mode: "SIMPLE",
        checkOnly: false,
        allowDeselect: true,
        type: "checkboxmodel"
    },

    bbar: {
        xtype: 'pagingtoolbar',
        displayInfo: true
    },

    contextMenu: null,

    listeners: {
        selectionchange: function(selectionModel, items) {
            let toolbar = this.down("toolbar"),
                selectedLength = items.length;
            if(!toolbar) {
                return;
            }

            for(let i in toolbar.items.items) {
                let toolbarBtn = toolbar.items.items[i];

                // 菜单
                if(toolbarBtn.menu && toolbarBtn.menu.items) {
                    for(let j in toolbarBtn.menu.items.items) {
                        let menuBtn = toolbarBtn.menu.items.items[j];
                        if(!menuBtn.multi) {
                            selectedLength !== 1 ? menuBtn.disable() : menuBtn.enable();
                        } else {
                            selectedLength <= 0 ? menuBtn.disable() : menuBtn.enable();
                        }
                    }

                    continue;
                }

                if(toolbarBtn.multi === undefined) {
                    continue;
                }


                if(!toolbarBtn.multi) {
                    selectedLength !== 1 ? toolbarBtn.disable() : toolbarBtn.enable();
                } else {
                    selectedLength <= 0 ? toolbarBtn.disable() : toolbarBtn.enable();
                }


            }

            let restoreBtn = toolbar.getComponent("restoreButton");
            if(restoreBtn) {
                this.trashStation && selectedLength > 0 ? restoreBtn.enable() : restoreBtn.disable();
            }

        },

        /**
         * 列变化时 保存到本地
         * @param ct
         * @param column
         */
        columnmove: function() {
            this.saveLayout();
        },
        columnresize: function() {
            this.saveLayout();
        },
        columnshow: function() {
            this.saveLayout();
        },
        columnhide: function() {
            this.saveLayout();
        },

        celldblclick: function( table, td, cellIndex, record, tr, rowIndex, e, eOpts) {
            try {
                let fieldName = table.getHeaderAtIndex(cellIndex).dataIndex;
                yas.lib.Plugin.execute("lib.base.grid.cellDblClick", fieldName, record, table, rowIndex, e, this);
            } catch(e) {}

        }
    },
    
    initComponent: function() {
        let me = this;

        if(this.apiAlias) {
            /**
             * 当前使用的API Class
             * @type {Object}
             */
            this.apiCls = this.apiCls || yas.lib.helper.Api.getApi(this.apiAlias);
            this.apiCls.component = this;
        }

        /**
         * 顶部工具栏
         * @type {*}
         */
        if(this.tbar !== false) {
            this.tbar = this.tbar || yas.lib.cv.GridActions.getActions(this.apiCls, this.definedTbar);
        }

        /**
         * 当前使用的 Store
         * @type {*}
         */
        this.store = this.store || Ext.create(
            yas.lib.helper.Helper.aliasToFullName(this.apiAlias, "store")
        );
        this.store.component = this;
        // this.store = this.store || Ext.StoreManager.lookup(this.apiAlias);
    
        /**
         * 未定义 columns 时尝试自动定义
         */
        this.columns = this.columns && this.columns.length > 0 ? this.columns : this.apiCls.getColumnsForGrid({
            grid: this
        });

        /**
         * 原始查询条件
         */
        if(this.initQueryParams) {
            this.getStore().setQueryParams(this.initQueryParams);
        }

        /**
         * 自动高度
         */
        // this.autoHeight && this.setHeight(Ext.getCmp("app_center").getHeight());
        // this.autoHeight && this.fitContainer();
    
        /**
         * 自动载入初始数据
         */
        this.autoLoad && this.getStore().load();

        this.callParent(arguments);

        if(typeof this.afterInitComponent === "function") {
            this.afterInitComponent();
        }
    },
    
    /**
     * 布局保存到本地
     */
    saveLayout: function() {
        let columns = this.getColumns(),
            cleared = {
                fieldsList: [],
                fieldConfig: {}
            },
            ignoreXtype = ["checkcolumn", "rownumberer"],
            gridName = this.gridName || this.apiAlias;

        Ext.Array.each(columns, function(column) {
            if(ignoreXtype.indexOf(column.xtype) >= 0 || !column.columnName) {
                return;
            }
            cleared.fieldsList.push(column.columnName);
            cleared.fieldConfig[column.columnName] = {
                width: column.width,
                hidden: column.hidden
            };
        });

        yas.lib.cv.SavedLayouts.save(gridName, cleared);

    }
});

最后

如果你在开发企业内部应用 / 开源项目 / 或者企业有能力负担相应的授权成本,而且你的应用更偏重于实用性而不是更华丽,那么我认为 ExtJS 是一个非常好的选择,尤其是作为全栈 Web 开发者,你可以尝试去使用它,而且如果你有任何问题,也随时欢迎和我聊聊!

还是那句老话:

所谓语言 / 框架 / 设计模式等都是实现的业务工具和手段,所以选择最适合的那个

相关资源

This site is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Comments