选择JS框架的规则

TL; DR

  • 本文不讨论TOP-3列表中的JS框架。
  • 在非TOP-3 JS框架上进行开发时,您必须解决的技术问题比开发开始时预期的多一个数量级
  • 这个故事是根据真实的事件。

故事始于一个小型项目,该项目最初是基于ribs.js和marionette.js库开发的。 当然,这些都是伟大的库,这些库开始了开发单页应用程序的历史。 但是到那时,它们更具有历史意义而不是实际价值。 我只提到一个事实,要显示一个简单的表格,必须创建:1)一个带有模型描述的模块,2)一个带有集合描述的模块,3)一个带有视图模型定义的模块,4)一个带有视图集合定义的模块,4)一个表格行模板5)表模板; 6)控制器模块。 在一个小型应用程序中大约有10个实体-在最初的阶段,您已经有五十多个小型模块。 这仅仅是开始。 但是,现在还不行。

在申请六个月后的某个时候,它仍然没有出现在搜索结果中。 将prerender.io添加到项目中(然后使用phantom.js引擎)虽然有所帮助,但效果并不理想。 云开始在应用程序之上和我之上聚集,此后,我意识到我需要非常快速有效地执行某件事,最好是在今天。 我设定的目标是:切换到服务器渲染。 使用骨架.js和marionettejs,这几乎是不可能的。 无论如何,在Spike Brehm(同构/通用应用程序的思想的作者)的指导下开发的骨架.js上的rendr项目在github.com上收集了58个贡献者和4,184个赞,但该项目在2015年停止,显然不打算进行一日闪电战。 我开始寻找替代方案。 我没有立即考虑使用TOP-3 JS框架,因为我没有足够的时间进行开发。 经过短暂的搜索,我发现了快速增长的JS框架riot.js github.com/riot/riot ,该框架目前在github.com上有13,704个赞,并且,正如我希望的那样,它很可能已经达到第一个位置(但是没有发生)。

是的,我通过使用riot.js将应用程序转移到服务器渲染中来解决了这个问题(虽然不是在同一天,而是在接下来的两天)。 在发生这种情况的同一天,该网站移至搜索结果的前十页。 一周之内,我转到了第一页,从那开始好几年了。

这结束了成功的故事,并开始了失败的历史。 下一个项目要复杂得多。 的确,有一个积极的一点是99%的应用程序屏幕都位于用户的个人帐户中,因此不需要服务器渲染。 受到使用riot.js的首次成功经验的启发,我开始提倡巩固成功并在前端应用riot.js的想法。 然后,在我看来,终于找到了一个解决方案,它结合了riot.js文档所承诺的简单性和功能性。 我错了!

当需要为HTML布局设计器提供所有必要的开发工具时,我遇到的第一个问题。 特别是,我们需要用于代码编辑器的插件,这是一个可以放置组件并立即观察结果的引擎,包括组件的热过载(热重载)。 所有这些都必须以准备在不久的将来工业运行的形式给出,而所有这些都没有。 结果,应用程序的布局开始于传统的模板引擎之一,结果导致了将HTML文档转换为riot.js组件的繁琐阶段。

但是,在此阶段出现的主要问题甚至与布局从模板格式到riot.js组件的转换无关。 突然发现riot.js给出了关于模板编译错误以及运行时错误的完全无信息的消息(对于riot.js的所有版本,直到4.0版(已完全重新设计)都是如此。 不仅没有发生错误的行的信息,甚至没有发生错误的文件或组件的名称的信息。 可以连续几个小时搜索错误,但仍然找不到。 然后,我必须将所有更改回滚到上一个工作状态。

以下是路由问题。 riot.js中的路由几乎是开箱即用的github.com/riot/route-至少来自同一位开发人员。 这使他希望手术无故障。 但是在某些时候,我注意到某些页面的负载超乎预期。 也就是说,一次转换可以在单页应用程序模式下进行,而另一次相同的转换会使整个HTML文档超载,就像在使用经典Web应用程序时一样。 当然,在这种情况下,如果内部状态尚未保存在服务器上,则会丢失。 (目前,该库的开发已停止,并且不与riot.js 4.0一起使用。)

系统中唯一按预期工作的组件是最小的类似流量的状态管理器github.com/jimsparkman/RiotControl 。 的确,要使用此组件,必须指定和取消状态更改的侦听器的次数比我们希望的要多得多。

本文的初衷是这样的:以我们自己对riot.js框架的经验为例,展示开发人员必须解决的任务,开发人员决定(决定)在JS框架上开发应用程序,而不是从TOP-3列表中进行开发。 但是,在准备过程中,我决定从内存中的riot.js文档中刷新一些页面,因此我了解到发行了新版本的riot.js 4.0,该版本完全(从头开始)进行了重新设计,可以在riot开发人员文章中找到。 js在medium.com上: medium.com/@gianluca.guarini/every-revolution-begins-with-a-riot-js-first-6c6a4b090ee 。 从这篇文章中,我了解到,所有困扰我的主要问题以及我将在本文中谈到的所有问题都已消除。 具体来说,在riot.js 4.0版中:

  • 编译器被完全重写(或者更确切地说,它首先被编写是因为之前引擎曾经使用过正则表达式)-这尤其会影响错误消息的信息内容
  • 除了服务器渲染之外,在客户端上还添加了hydr-这最终使我们能够开始编写通用应用程序而无需进行双重渲染(由于较早版本中缺少hydr()函数,因此第一次在服务器上以及就在客户端上)
  • 添加了用于热重载组件的插件github.com/riot/hot-reload
  • 和许多其他有用的更改

这不是广告,只是背景。 实际上,就使用建议而言,我的结论将非常模棱两可,并且该文章通常不专门针对特定的框架或库,而是针对开发过程中需要解决的任务。

不幸的是,riot.js开发人员所做的工作尚未得到社区的正确评估。 例如,自服务器开发库开发以来已经过去了六个月的服务器渲染库github.com/riot/ssrgithub.com上收集了三个共同贡献者和三个喜欢的人(并非所有人都由贡献者提供,尽管其中一个与贡献者相同)。

因此,在演出过程中,他改变了文章的方向,而不是回忆录,他尝试重新走遍了,他有了更多的知识和经验,拥有了更高级的图书馆版本和无限的空闲时间。

所以我们开始。 例如,实现了github.com/gothinkster/realworld应用程序。 在Habré上多次讨论了这个项目。 对于那些不熟悉他的人,我将简要描述他的想法。 该项目的开发人员借助不同的编程语言和框架(或者没有框架)可以解决相同的问题:开发博客引擎,其功能类似于media.com的简化版本。 这是我们每天必须开发的实际应用程序的复杂性与todo.app之间的折衷,todo.app并不总是让我们真正欣赏使用库或框架的工作。 该项目受到开发人员的尊重。 为了支持上述观点,我可以说Rich Rich(sveltejs的主要开发者)甚至有一个实现github.com/sveltejs/realworld

开发环境


当然,您已经准备好进行战斗,但是请考虑周围的开发人员。 在这种情况下,请关注您的同事在哪个开发环境中工作的问题。 如果没有要使用的主要开发环境的插件和框架的程序代码编辑器,则不太可能获得支持。 例如,我使用Atom编辑器进行开发。 对他来说,有一个防暴标签插件github.com/riot/syntax-highlight/tree/legacy ,最近三年没有更新。 在同一个存储库中,有一个Sublime github.com/riot/syntax-highlight插件-它是最新的,并支持riot.js 4.0的当前版本。

但是,riot.js组件是HTML文档的有效片段,其中JS代码包含在script元素的主体中。 因此,如果您为* .riot扩展名添加html文档类型,则一切正常。 当然,这是一个强制性的决定,因为否则将不可能在这里继续下去。

我们在文本编辑器中突出显示了语法,现在我们需要更多高级功能,这是我们习惯于从eslint获得的功能。 在我们的例子中,组件的JS代码包含在script元素的主体中,我希望找到并找到一个插件来从HTML文档github.com/BenoitZugmeyer/eslint-plugin-html中提取JS代码。 之后,我的eslint配置开始如下所示:

{ "parser": "babel-eslint", "plugins": [ "html" ], "settings": { "html/html-extensions": [".html", ".riot"] }, "env": { "browser": true, "node": true, "es6": true }, "extends": "standard", "globals": { "Atomics": "readonly", "SharedArrayBuffer": "readonly" }, "parserOptions": { "ecmaVersion": 2018, "sourceType": "module" }, "rules": { } } 

在选择JS框架时,开发人员开始考虑的不是语法突出显示和eslint插件。 同时,如果没有这些工具,您可能会由于项目的“良好”原因而遭到同事及其大规模外逃的反对。 尽管唯一且真正有效的理由是,如果没有完整的开发人员库,他们会感到不舒服。 对于riot.js,该问题已通过Columbus方法解决。 从某种意义上说,实际上没有riot.js的插件,但是由于riot.js模板语法的特殊性(看起来像是常规HTML文档的一部分),我们使用用于处理HTML文档的工具涵盖了99%的必要功能。

除了编写和验证我们刚刚检查过的代码的方法外,开发人员的工具还包括用于快速组装和调试项目,在更改项目时在Web浏览器中热重装组件和模块的工具。 我们将在下一部分中考虑这一部分。

项目组装


我们设法习惯了构建项目时所需的大多数功能,甚至不再考虑可能存在的差异。 但事实可能并非如此。 而且,如果您选择了一个新的JS框架,则建议首先确保一切正常。 例如,正如我已经提到的,在较旧版本的riot.js上进行开发时,最大的问题是缺少编译错误消息和有关发生此错误的组件的运行时信息。 同样重要的是编译速度。 通常,为了加快编译速度,在正确构建的框架中,仅重新编译更改的部分,因此,对组件文本更改的反应时间最小。 好吧,如果在不完全重新加载网络浏览器中的页面的情况下支持热重新加载组件,那将非常好。

因此,我将尝试列出清单,分析项目的构建工具时需要特别注意的事项:

1.存在开发模式和正在运行的应用程序
在开发人员模式下:
2.有关项目编译错误的信息性消息(源文件的名称,源文件中的行号,错误说明)
3.信息性的运行时错误消息(源文件的名称,源文件中的行号,错误说明)
4.快速重新组装修改后的模块
5.浏览器中组件的热过载
在操作模式下:
6.文件名中存在版本控制(例如4a8ee185040ac59496a2.main.js)
7.小模块在一个或多个模块中的布局(块)
8.使用动态导入将代码分成块

在riot.js版本4.0中,出现了github.com/riot/webpack-loader模块,该模块完全对应于给定的清单。 我不会列出程序集配置的所有功能。 我唯一要注意的是,在所考虑的项目中,我使用用于express.js的模块:webpack-dev-middleware和webpack-hot-middleware,它们使您从排版时就可以立即在功能齐全的服务器上工作。 这尤其允许开发通用/同构Web应用程序。 我提请您注意模块热过载模块仅对Web浏览器有效的事实。 同时,在服务器端呈现的组件保持不变。 因此,有必要听其更改,并在正确的时间删除服务器缓存的所有代码并加载更改后的模块的代码。 如何执行此操作很长一段时间,所以我只提供一个实现的链接: github.com/apapacy/realworld-riotjs-effector-universal-hot/blob/master/src/dev_server.js

路由选择


稍微解释一下Leo Tolstoy,我们可以说JS框架的所有引擎彼此相似,而与之相连的所有路由都以各自的方式工作。 我经常将路由器的条件分类分为两种:声明式和命令式。 现在,我将尝试找出这种分类的合理性。

让我们来回顾一下历史。 在Internet诞生之初,URL / URI与服务器上托管的文件的名称匹配。 我们一次翻阅几页历史,我们将了解Model 2(MVC)体系结构的问世。 在这种体系结构中,出现了一个前端控制器,它执行路由功能。 我想知道谁首先决定从前端控制器中选择一个单独的块中的路由功能,然后再将请求发送到许多控制器之一,但尚未找到答案。 看来他们立即开始做这一切。

也就是说,路由确定了应该在服务器上执行的操作以及(将通过控制器间接地)由控制器生成的视图。 将路由传输到客户端(Web浏览器)时,已经形成了应用程序中路由功能的构想。 而那些主要关注路由决定动作的事实的人,开发了命令式路由,而那些注意到路由最终决定应向用户显示的视图的人,则开发了声明式路由。

也就是说,当从服务器传输到客户端以进行路由时,它们“挂起”了服务器路由的两个功能(选择动作和选择视图)。 此外,还出现了新的任务-这是通过单页应用程序进行导航,而没有完全重新加载HTML文档,处理访问历史等等。 为了说明这一点,我将摘录一个大型框架路由器的文档摘录:

...使创建SPA应用程序变得容易。 包括以下功能

  • 嵌套路线/视图
  • 模块化路由器配置
  • 访问路由参数,查询,通配符
  • 查看基于Vue.js的过渡动画
  • 便捷的导航控制
  • 自动为链接添加活动CSS类
  • HTML5历史记录模式或哈希,在IE9中具有自动切换功能
  • 自定义滚动行为

在此选项中,路由功能显然过载了,需要在客户端重新考虑其任务。 我开始寻找适合自己任务的解决方案。 作为主要标准,我考虑了该路由:

  1. 对于通用/同构Web应用程序,应在Web客户端和Web服务器端均相同;
  2. 应该与任何(包括我选择的)框架一起使用,也可以没有框架。

我找到了一个这样的库,它是github.com/kriasoft/universal-router 。 如果我们简要描述此库的概念,它将配置路由,这些路由在输入时采用一串URL,并在输出时调用异步函数,该函数将解析后的URL作为实际参数传递。 老实说,我想问:这就是全部吗? 那么每个人应该如何处理呢? 然后,我在medium.com medium.com/@ippei.tanaka/universal-router-history-react-97ec79464573上找到了一篇文章,其中提出了一个相当不错的选择,但可能是重写了push()history方法,但没有我需要从代码中删除的代码。 结果,客户端上路由器的操作大致如下定义:

 const routes = new UniversalRouter([ { path: '/sign-in', action: () => ({ page: 'login', data: { action: 'sign-in' } }) }, { path: '/sign-up', action: () => ({ page: 'login', data: { action: 'sign-up' } }) }, { path: '/', action: (req) => ({ page: 'home', data: { req, action: 'home' } }) }, { path: '/page/:page', action: (req) => ({ page: 'home', data: { req, action: 'home' } }) }, { path: '/feed', action: (req) => ({ page: 'home', data: { req, action: 'feed' } }) }, { path: '/feed/page/:page', action: (req) => ({ page: 'home', data: { req, action: 'feed' } }) }, ... { path: '(.*)', action: () => ({ page: 'notFound', data: { action: 'not-found' } }) } ]) const root = getRootComponent() const history = createBrowserHistory() const render = async (location) => { const route = await router.resolve(location) const component = await import(`./riot/pages/${route.page}.riot`) riot.register(route.page, component.default || component) root.update(route, root) } history.listen(render) 

现在,任何对history.push()的调用都会启动路由。 要在应用程序内部导航,您还需要创建一个包装标准HTML元素a(锚)的组件,不要忘记取消其默认行为:

 <navigation-link href={ props.href } onclick={ action }> <slot/> <script> import history from '../history' export default { action (e) { e.preventDefault() history.push(this.props.href) if (this.props.onclick) { this.props.onclick.call(this, e) } e.stopPropagation() } } </script> </navigation-link> 

应用状态管理


最初,我在项目中包含了mobx库。 一切都按预期进行。 只是它与任务不完全相符-我在本文开头进行的研究。 所以我切换到github.com/zerobias/effector 。 这是一个非常强大的项目。 它提供了100%的redux功能(仅没有大的开销)和100%的mobx功能(尽管在这种情况下,有必要编写更多代码,但与没有装饰器的mobx相比,编码要少得多)

商店的描述如下所示:

 import { createStore, createEvent } from 'effector' import { request } from '../agent' import { parseError } from '../utils' export default class ProfileStore { get store () { return this.profileStore.getState() } constructor () { this.success = createEvent() this.error = createEvent() this.updateError = createEvent() this.init = createEvent() this.profileStore = createStore(null) .on(this.init, (state, store) => ({ ...store })) .on(this.success, (state, data) => ({ data })) .on(this.error, (state, error) => ({ error })) .on(this.updateError, (state, error) => ({ ...state, error })) } getProfile ({ req, author }) { return request(req, { method: 'get', url: `/profiles/${decodeURIComponent(author)}` }).then( response => this.success(response.data.profile), error => this.error(parseError(error)) ) } follow ({ author, method }) { return request(undefined, { method, url: `/profiles/${author}/follow` }).then( response => this.success(response.data.profile), error => this.error(parseError(error)) ) } } 

该库使用成熟的reducer(在effector.js文档中称为它们),许多人都缺乏mobx,但是与redux相比,其编码工作量要少得多。 但是最主要的不是。 在获得了100%的redux和mobx功能之后,我仅使用了effector.js固有功能的十分之一。 从中我们可以得出结论,在复杂项目中使用它可以显着丰富开发人员的资金。

测试中


待办

结论


至此,工作完成。 结果显示在github.com/apapacy/realworld-riotjs-effector-universal-hot存储库中,以及本文在Habré上。
演示站点,现为realworld-riot-effector-universal-hot-pnujtmugam.now.sh

最后,我将分享我对开发的印象。 在riot.js 4.0版上进行开发非常方便。 比起React,许多构造更容易编写。 在下班后和周末,没有狂热就花了整整两个星期。 但是……一个很小但是……奇迹没有再发生。 React中的服务器渲染速度提高了20-30倍。 公司再次获胜。 但是,测试了两个有趣的路由和状态管理器库。

apapacy@gmail.com
六月17,2019

Source: https://habr.com/ru/post/zh-CN456448/


All Articles