Dojo-Build-进阶

创建包一个包就是一部分代码,它用于表示一部分功能。可以按需异步、并行加载包。与不使用任何代码拆分技术的应用程序相比,合理分包的应用程序可以显著提高响应速度,需要请求的字节数更少,加载的时间更短。在处理大型应用程序时,这一点尤其重要,因为这类应用程序的大部分表现层逻辑在初始化时是不需要加载的。 Dojo 尝试使用路由和 outlet 智能地做出选择,自动将代码拆分为更小的包。通常各个包内的代码都是紧密相关的。这是构建系统内置的功能,可直接使用。但是,对于有特殊分包需求的用户,Dojo 还允许在 .dojorc 配置文件中显示定义包。 默认情况下,Dojo 应用程序只创建一个应用程序包。但是 @dojo/cli-build-app 提供了很多配置选项,这些选项可将应用程序拆分为较小的、可逐步加载的包。 使用路由自动分包默认情况下,Dojo 会基于应用程序的路由自动创建包。要做到这一点需要遵循以下几条规则。 src/routes.ts 必须默认导出路由配置信息部件所属的模块必须默认导出部件Outlet 的 render 函数必须使用内联函数src/routes.tsexport default [ { path: 'home', outlet: 'home', defaultRoute: true }, { path: 'about', outlet: 'about' }, { path: 'profile', outlet: 'profile' }];src/App.tsexport default class App extends WidgetBase { protected render() { return ( <div classes={[css.root]}> <Menu /> <div> <Outlet key="home" id="home" renderer={() => <Home />} /> <Outlet key="about" id="about" renderer={() => <About />} /> <Outlet key="profile" id="profile" renderer={() => <Profile username="Dojo User" />} /> </div> </div> ); }}将会为应用程序的每个顶级路由生成单独的包。在本例中,会生成一个应用程序的主包以及 src/Home、src/About 和 src/Profile 三个包。 ...

November 4, 2019 · 5 min · jiezi

Dojo-Build-简介

翻译自:https://github.com/dojo/frame... Dojo 提供了一套强大的命令行工具,让构建现代应用程序更加简单。 可以自动创建包(Bundle),可以使用 PWA 在本地缓存文件,可以在构建阶段渲染初始的 HTML 和 CSS,也可以使用 Dojo 的 CLI 工具和 .dojorc 配置文件按条件忽略一些代码。或者脱离(eject) Dojo 的构建工具,直接使用底层的构建工具以做到完全掌控。 功能描述Dojo CLI模块化的命令行工具,用于快速启动新的应用程序、创建部件和运行测试等。开发服务器开发时使用的本地 web 服务器,用于监听文件系统,当检测到变化时会自动重新构建。也支持 HTTPS 和设置代理。包(bundle)通过减少用户需要下载的内容和优化用户实际需要的应用程序交互时间(Time-to-Interactive)以提高用户体验。可以根据路由自动创建包,或者在配置文件中明确定义包。按条件纳入代码通过 .dojorc 配置文件可以静态方式关闭或打开使用 dojo/has 定义的功能。由于这些配置而无法访问到的代码分支会被自动忽略掉。这就很容易为特定目标(如 IE11 或 mobile)提供特定功能,而不会影响包的大小。PWA 支持渐进式 Web 应用程序通过缓存内容甚至脱机工作,创建更快、更可靠的用户体验。通过配置文件或者在代码中定义,dojo 很容易创建一个 service work,并将其构建为应用程序的一部分。构建时渲染在构建时渲染路由以生成初始的 HTML 和 CSS。在构建时渲染,Dojo 可以节省出初始渲染的成本,创建出一个响应性更高的应用程序,且不会引入额外的复杂性。基本用法Dojo 提供了一组 CLI 命令,辅助创建和构建应用程序。本指南假设已全局安装 @dojo/cli,且在项目中安装了 @dojo/cli-build-app 和 @dojo/cli-test-intern。如果项目是使用 @dojo/cli-create-app 初始化的,那么这些依赖应该已经存在。 构建Dojo 的 CLI 工具支持多种构建目标或 mode。在 dojo create app 为 package.json 生成的几个脚本(scripts)中可看到所有模式。 运行以下命令,创建一个为生产环境优化过的构建。 > dojo build --mode dist此次构建使用 dist 模式创建应用程序包,并将结果输出到 output/dist 目录中。 ...

November 3, 2019 · 1 min · jiezi

Dojo-进阶

官网 https://dojo.io 序言 - 构建企业级 web 应用程序在热衷敏捷交付的时代,鼓励将小功能点持续地交付给用户。软件行业开始青睐这种方式,因为它最大限度地降低风险,并最大限度地提高用户的参与度和满意度。 即使采用现代的交付方式,一些风险仍然不可避免。复杂性就是这样一种风险,对于成熟的应用程序而言,复杂性更成为一个重要的关注点。无论应用程序遵循什么样的系统架构,随着时间的推移,许多小功能聚集出一个庞大且令人畏惧的代码库,需要几个团队监督。 应用程序上线的时间越久,实现一个设计简洁的新功能的机会就越少。相反,更多的是在现有功能的基础上调整、修复 bug 或扩展。一个成功的应用程序——以及所包含的功能——大部分时间都花在维护上。 维护复杂的应用程序需要经过严格的训练。团队很容易陷入泥潭,将时间花在抱怨代码和同事上面,而不是向用户交付价值。要降低这种风险涉及很多方面,包括标准化、模式化、技术选型和工具等领域。 管理复杂性在软件交付的生命周期中,错误发现的越早越好。在开发阶段修复一个错误,比在交付环节修复错误,或者已给用户带来负面影响的上线阶段修复错误要快的多,成本也低得多。 强类型早期捕获错误的好方法是在应用程序的开发阶段支持强类型。如果应用程序的代码中显式指定了类型信息,就可以避免数据类型不匹配而导致的逻辑错误。编译器和静态类型检查程序可以验证类型信息,并当类型不匹配时让构建失败。只有修复了这些错误,软件才能从个人的工作空间进入到交付管道的后续环节。 Dojo 构建在 TypeScript 之上,提供了显式的类型和静态编译时的类型检查。使用 Dojo 构建的应用程序可以用到 TypeScript(而不是普通的 JavaScript)带来的优势。 当使用 Dojo CLI 构建应用程序时,应用程序的构建过程会默认包含 TypeScript 的编译阶段。开发人员从一开始就能编写出类型安全的应用程序。 模块化——单一职责原则理想情况下,一个组件应该足够小,以便它只实现单一职责。一个组件越简单、封装程度越高,则大量程序员长期理解和维护时就越容易。拥有庞大代码库的大型应用程序就是由大量的更小、更容易理解的组件组合而成的。 试图降低复杂度时,在单个组件中隔离职责有以下好处: 限制范围。假设组件维护一套一致的 API,则能在不影响外部用户使用的情况下更改内部实现。相反,组件的详细信息保存在定义模块的内部,这意味着其定义不会与其他组件的定义冲突,所以就不会与其他组件的命名约定重叠。简化测试需求,因为单元测试只需关注单一职责,而不是多个条件组合成的越来越多的应用程序逻辑。组件可以在多处重用,避免多处重复实现。修复错误时,只需专注于单个组件,而不是散落在各处的单独实例。对于 web 应用程序而言,隔离还能为终端用户带来额外的好处。应用程序可以划分为多个层,用户在给定的时间点只加载他们感兴趣的层。这减少了资源大小和网络传输需求,从而缩短了加载时间。 Dojo 应用程序的组件Index 网页HTML 页面是每个应用程序的基本内容,Dojo 应用程序也不例外。传统上,单个 index.html 文件既是应用程序的入口点,也是将应用程序的整体结构存入 DOM 的根容器。 Dojo 应用程序通常会注入到单个 DOM 元素中,默认情况下是注入到 document.body 中。这使得 Dojo 应用程序可以与页面中的其他内容共存——静态资源、传统的应用程序、甚至是另外一个 Dojo 应用程序。 部件Dojo 中的部件与 DOM 元素类似,是 Dojo 应用程序中封装的核心概念。正如传统网站是通过 DOM 元素逐层构建的一样,Dojo 应用程序则是通过部件逐层构建的。 部件可以描述一切,从单个界面元素(如 label 或 textbox)到更复杂的容器(如 form 表单、页面或者整个应用程序)。 ...

October 14, 2019 · 2 min · jiezi

Dojo-简介

官网 https://dojo.ioDojo 提供了一套设计现代 web 应用程序的完整解决方案,项目需要时也可以逐步的模块化。Dojo 框架可以随着应用程序的复杂性而扩展,可构建的内容从简单的预渲染站点一直到企业级的单页面 web 应用,包括跨多种设备的、接近本地 app 体验的渐进式 web 应用程序。 Dojo 提供了各种各样的框架组件、工具以及构建管道,它们协助解决许多端到端 web 应用程序的开发问题。 管理复杂的应用程序开发称为 Widget 的简单且模块化的组件,这些组件可通过多种方式组装,以实现日益复杂的需求。使用响应式的状态管理和数据流来连接部件,当应用程序的状态更改时,Dojo 框架就可以高效地渲染更新。使用集中的、面向命令的数据存储来管理高级的应用程序状态。允许用户使用声明式路由在单页面应用程序(SPA)内导航,并支持跟踪历史记录。通过功能切换检测来禁用处于开发阶段的功能——甚至在构建时删除未使用的模块,缩减应用程序的交付大小。编写适合在浏览器或服务器上运行的程序。创建高效的应用程序通过虚拟化 DOM(VDOM)声明部件结构,避免高昂的 DOM 操作和布局抖动。简化资源分层和绑定,缩减用户实际需要的应用程序交互时间(Time-to-Interactive)。当模块及其依赖跨多个绑定时,Dojo 框架能自动将 import 转换为延迟加载。创建全面的应用程序开发支持主题的部件和应用程序,从而将页面外观和页面功能隔离,并通过一种极其简单的方式在整个应用程序中实现外观一致。使用一套支持国际化(i18n)、可访问性(a11y)以及现成主题的 UI 部件使用国际化(i18n)框架支持多套区域设置,包括通过 Unicode CLDR 实现高级的消息格式化。创建可适配的应用程序开发渐进式 web 应用程序(PWA),支持与本地设备 APP 类似的功能,如离线使用、后台数据同步和推送通知。使用构建时渲染(BTR),提供可以与服务器端渲染(SSR)的应用程序媲美的预渲染功能,并且不需要托管到动态的 web 服务器上。创建完全不使用 JavaScript 的、真正的静态站点;或者借助 BTR 让应用程序实现更好的首次加载体验。利用先进的 web 技术,如 Web Animations、Intersection Observers 和 Resize Observers。Dojo 框架为用户在多种运行环境上使用最新功能提供了一致的应用程序体验。如果需要的话,需要定制的应用程序可以脱离 Dojo 的构建管道,转而使用自己的解决方案,并只使用框架提供的部分功能。加快开发使用简单的命令行界面(CLI)启动新项目,并持续的构建和验证。支持行业最佳实践且类型安全和稳健的构建管道,能立即提升开发人员的工作效率。快速构建与 Dojo 自带的部件库具有相同功能的自定义部件,包括自定义主题。

October 13, 2019 · 1 min · jiezi

Dojo 表单校验

tutorials/1015_form_validation/index.mdcommit 3e0f3ff1ed392163bc65e9cd015c4705cb9c586e{% section ‘first’ %}表单校验Overview本教程将介绍如何在示例应用程序的上下文中处理基本的表单校验。在 注入状态 教程中,我们已经介绍了处理表单数据;我们将在这些概念的基础上,在现有表单上添加校验状态和错误信息。本教程中,我们将构建一个支持动态的客户端校验和模拟的服务器端校验示例。前提你可以打开 codesandbox.io 上的教程 或者 下载 示例项目,然后运行 npm install。本教程假设你已经学习了 表单部件教程 和 状态管理教程。{% section %}创建存储表单错误的对象{% task ‘在应用程序上下文中添加表单错误。’ %}现在,错误对象应该对应存在于 WorkerForm.ts 和 ApplicationContext.ts 文件中的 WorkerFormData。这种错误配置有多种处理方式,一种情况是为单个 input 的多个校验步骤分别设置错误信息。现在我们将从最简单的情况开始,即为每个 input 添加布尔类型的 valid 和 invalid 状态。{% instruction ‘为 WorkerForm.ts 文件中创建一个 WorkerFormErrors 接口’ %}{% include_codefile ‘demo/finished/biz-e-corp/src/widgets/WorkerForm.ts’ lines:15-19 %}export interface WorkerFormErrors { firstName?: boolean; lastName?: boolean; email?: boolean;}将 WorkerFormErrors 中的属性定义为可选,这样我们就可以为 form 中的字段创建三种状态:未校验的、有效的和无效的。{% instruction ‘接下来将 formErrors 方法添加到 ApplicationContext 类中’ %}在练习中,完成以下三步:在 ApplicationContext 类中创建一个私有字段 _formErrors在 ApplicationContext 中为 _formErrors 创建一个 public 访问器更新 WorkerFormContainer.ts 文件中的 getProperties 函数,支持传入新的错误对象提示:查看 ApplicationContext 类中已有的 _formData 私有字段是如何使用的。可按照相同的流程添加 _formErrors 变量。确保 ApplicationContext.ts 中存在以下代码:// modify import to include WorkerFormErrorsimport { WorkerFormData, WorkerFormErrors } from ‘./widgets/WorkerForm’;// private fieldprivate _formErrors: WorkerFormErrors = {};// public getterget formErrors(): WorkerFormErrors { return this._formErrors;}WorkerFormContainer.ts 中修改后的 getProperties 函数:function getProperties(inject: ApplicationContext, properties: any) { const { formData, formErrors, formInput: onFormInput, submitForm: onFormSave } = inject; return { formData, formErrors, onFormInput: onFormInput.bind(inject), onFormSave: onFormSave.bind(inject) };}{% instruction ‘最后,修改 WorkerForm.ts 中的 WorkerFormProperties 来接收应用程序上下文传入的 formErrors 对象:’ %}export interface WorkerFormProperties { formData: WorkerFormData; formErrors: WorkerFormErrors; onFormInput: (data: Partial<WorkerFormData>) => void; onFormSave: () => void;}{% section %}为 form 表单输入框绑定校验{% task ‘在 onInput 中执行校验’ %}现在,我们已经可以在应用程序状态中存储表单错误,并将这些错误传给 form 表单部件。但 form 表单依然缺少真正的用户输入校验;为此,我们需要温习正则表达式并写一个基本的校验函数。{% instruction ‘在 ApplicationContext.ts 中创建一个私有方法 _validateInput’ %}跟已存在的 formInput 函数相似,应该为 _validateInput 传入 Partial 类型的 WorkerFormData 输入对象。校验函数应该返回一个 WorkerFormErrors 对象。示例应用程序中只展示了最基本的校验检查——示例中邮箱地址的正则表达式模式匹配简洁但有不够完备。你可以用更健壮的邮箱测试来代替,或者做其它修改,如检查第一个名字和最后一个名字的最小字符数。{% include_codefile ‘demo/finished/biz-e-corp/src/ApplicationContext.ts’ lines:32-50 %}private validateInput(input: Partial<WorkerFormData>): WorkerFormErrors { const errors: WorkerFormErrors = {}; // validate input for (let key in input) { switch (key) { case ‘firstName’: errors.firstName = !input.firstName; break; case ’lastName’: errors.lastName = !input.lastName; break; case ’email’: errors.email = !input.email || !input.email.match(/^[a-zA-Z0-9.!#$%&’*+/=?^`{|}~-]+@[a-zA-Z0-9-]+(?:.[a-zA-Z0-9-]+)*$/); } } return errors;}现在,我们将在每一个 onInput 事件中直接调用校验函数来测试它。将下面一行代码添加到 ApplicationContext.ts 中的 formInput 中:this._formErrors = deepAssign({}, this._formErrors, this._validateInput(input));{% instruction ‘更新 WorkerForm 的渲染方法来显示校验状态’ %}至此,WorkerForm 部件的 formErrors 属性中存着每个 form 字段的校验状态,每次调用 onInput 事件时都会更新校验状态。剩下的就是将 valid/invalid 属性传给所有输入部件。幸运的是,Dojo 的 TextInput 部件包含一个 invalid 属性,可用于在 DOM 节点上设置 aria-invalid 属性,并切换可视化样式类。WorkerForm.ts 中更新后的渲染方法,应该是将每个 form 字段部件的上 invalid 属性与 formErrors 对应上。我们也为 form 元素添加了一个 novalidate 属性来禁用原生浏览器校验。protected render() { const { formData: { firstName, lastName, email }, formErrors } = this.properties; return v(‘form’, { classes: this.theme(css.workerForm), novalidate: ’true’, onsubmit: this._onSubmit }, [ v(‘fieldset’, { classes: this.theme(css.nameField) }, [ v(’legend’, { classes: this.theme(css.nameLabel) }, [ ‘Name’ ]), w(TextInput, { key: ‘firstNameInput’, label:‘First Name’, labelHidden: true, placeholder: ‘Given name’, value: firstName, required: true, invalid: this.properties.formErrors.firstName, onInput: this.onFirstNameInput }), w(TextInput, { key: ’lastNameInput’, label: ‘Last Name’, labelHidden: true, placeholder: ‘Surname name’, value: lastName, required: true, invalid: this.properties.formErrors.lastName, onInput: this.onLastNameInput }) ]), w(TextInput, { label: ‘Email address’, type: ’email’, value: email, required: true, invalid: this.properties.formErrors.email, onInput: this.onEmailInput }), w(Button, {}, [ ‘Save’ ]) ]);}现在,当你在浏览器中查看应用程序时,每个表单字段的边框颜色会随着你键入的内容而变化。接下来我们将添加错误信息,并更新 onInput ,让检验只在第一次失去焦点(blur)事件后发生。{% section %}扩展 TextInput{% task ‘创建一个错误消息’ %}简单的将 form 字段的边框颜色设置为红色或绿色并不能告知用户更多信息——我们需要为无效状态添加一些错误消息文本。最基本要求,我们的错误文本必须与 form 中的 input 关联,可设置样式和可访问。一个包含错误信息的 form 表单字段看起来应该是这样的:v(‘div’, { classes: this.theme(css.inputWrapper) }, [ w(TextInput, { … aria: { describedBy: this._errorId }, onInput: this._onInput }), invalid === true ? v(‘span’, { id: this._errorId, classes: this.theme(css.error), ‘aria-live’: ‘polite’ }, [ ‘Please enter valid text for this field’ ]) : null])通过 aria-describeby 属性将错误消息与文本输入框关联,并使用 aria-live 属性来确保当它添加到 DOM 或发生变化后能被读取到。将输入框和错误信息包裹在一个 <div> 中,则在需要时可相对输入框来获取到错误信息的位置。{% instruction ‘扩展 TextInput,创建一个包含错误信息和 onValidate 方法的 ValidatedTextInput 部件’ %}为多个文本输入框重复创建相同的错误消息样板明显是十分啰嗦的,所以我们将扩展 TextInput。这还将让我们能够更好的控制何时校验,例如,也可以添加给 blur 事件。现在,只是创建一个 ValidatedTextInput 部件,它接收与 TextInput 相同的属性接口,但多了一个 errorMessage 字符串和 onValidate 方法。它应该返回与上面相同的节点结构。你也需要创建包含 error 和 inputWrapper 样式类的 validatedTextInput.m.css 文件,尽管我们会弃用本教程中添加的特定样式:.inputWrapper {}.error {}import { WidgetBase } from ‘@dojo/framework/widget-core/WidgetBase’;import { TypedTargetEvent } from ‘@dojo/framework/widget-core/interfaces’;import { v, w } from ‘@dojo/framework/widget-core/d’;import uuid from ‘@dojo/framework/core/uuid’;import { ThemedMixin, theme } from ‘@dojo/framework/widget-core/mixins/Themed’;import TextInput, { TextInputProperties } from ‘@dojo/widgets/text-input’;import * as css from ‘../styles/validatedTextInput.m.css’;export interface ValidatedTextInputProperties extends TextInputProperties { errorMessage?: string; onValidate?: (value: string) => void;}export const ValidatedTextInputBase = ThemedMixin(WidgetBase);@theme(css)export default class ValidatedTextInput extends ValidatedTextInputBase<ValidatedTextInputProperties> { private _errorId = uuid(); protected render() { const { disabled, label, maxLength, minLength, name, placeholder, readOnly, required, type = ’text’, value, invalid, errorMessage, onBlur, onInput } = this.properties; return v(‘div’, { classes: this.theme(css.inputWrapper) }, [ w(TextInput, { aria: { describedBy: this._errorId }, disabled, invalid, label, maxLength, minLength, name, placeholder, readOnly, required, type, value, onBlur, onInput }), invalid === true ? v(‘span’, { id: this._errorId, classes: this.theme(css.error), ‘aria-live’: ‘polite’ }, [ errorMessage ]) : null ]); }}你可能已注意到,我们创建的 ValidatedTextInput 包含一个 onValidate 属性,但我们还没有用到它。在接下来的几步中,这将变得非常重要,因为我们可以对何时校验做更多的控制。现在,只是把它当做一个占位符。{% instruction ‘在 WorkerForm 中使用 ValidatedTextInput’ %}现在 ValidatedTextInput 已存在,让我们在 WorkerForm 中导入它并替换掉 TextInput,并在其中写一些错误消息文本:Import 语句块{% include_codefile ‘demo/finished/biz-e-corp/src/widgets/WorkerForm.ts’ lines:1-7 %}import { WidgetBase } from ‘@dojo/framework/widget-core/WidgetBase’;import { TypedTargetEvent } from ‘@dojo/framework/widget-core/interfaces’;import { v, w } from ‘@dojo/framework/widget-core/d’;import { ThemedMixin, theme } from ‘@dojo/framework/widget-core/mixins/Themed’;import Button from ‘@dojo/widgets/button’;import ValidatedTextInput from ‘./ValidatedTextInput’;import * as css from ‘../styles/workerForm.m.css’;render() 方法内部{% include_codefile ‘demo/finished/biz-e-corp/src/widgets/WorkerForm.ts’ lines:72-108 %}v(‘fieldset’, { classes: this.theme(css.nameField) }, [ v(’legend’, { classes: this.theme(css.nameLabel) }, [ ‘Name’ ]), w(ValidatedTextInput, { key: ‘firstNameInput’, label: ‘First Name’, labelHidden: true, placeholder: ‘Given name’, value: firstName, required: true, onInput: this.onFirstNameInput, onValidate: this.onFirstNameValidate, invalid: formErrors.firstName, errorMessage: ‘First name is required’ }), w(ValidatedTextInput, { key: ’lastNameInput’, label: ‘Last Name’, labelHidden: true, placeholder: ‘Surname name’, value: lastName, required: true, onInput: this.onLastNameInput, onValidate: this.onLastNameValidate, invalid: formErrors.lastName, errorMessage: ‘Last name is required’ })]),w(ValidatedTextInput, { label: ‘Email address’, type: ’email’, value: email, required: true, onInput: this.onEmailInput, onValidate: this.onEmailValidate, invalid: formErrors.email, errorMessage: ‘Please enter a valid email address’}),{% task ‘创建从 onFormInput 中提取出来的 onFormValidate 方法’ %}{% instruction ‘传入 onFormValidate 方法来更新上下文’ %}现在校验逻辑毫不客气的躺在 ApplicationContext.ts 中的 formInput 中。现在我们将它抬到自己的 formValidate 函数中,并参考 onFormInput 模式,将 onFormValidate 传给 WorkerForm。这里有三个步骤:在 ApplicationContext.ts 中添加 formValidate 方法,并将 formInput 中更新 _formErrors 代码放到 formValidate 中:public formValidate(input: Partial<WorkerFormData>): void { this._formErrors = deepAssign({}, this._formErrors, this._validateInput(input)); this._invalidator();}public formInput(input: Partial<WorkerFormData>): void { this._formData = deepAssign({}, this._formData, input); this._invalidator();}更新 WorkerFormContainer,将 formValidate 传给 onFormValidate:function getProperties(inject: ApplicationContext, properties: any) { const { formData, formErrors, formInput: onFormInput, formValidate: onFormValidate, submitForm: onFormSave } = inject; return { formData, formErrors, onFormInput: onFormInput.bind(inject), onFormValidate: onFormValidate.bind(inject), onFormSave: onFormSave.bind(inject) };}在 WorkerForm 中先在 WorkerFormProperties 接口中添加 onFormValidate:export interface WorkerFormProperties { formData: WorkerFormData; formErrors: WorkerFormErrors; onFormInput: (data: Partial<WorkerFormData>) => void; onFormValidate: (data: Partial<WorkerFormData>) => void; onFormSave: () => void;}然后为每个 form 字段的校验创建内部方法,并将这些方法(如 onFirstNameValidate)传给每个 ValidatedTextInput 部件。这将使用与 onFormInput、onFirstNameInput、onLastNameInput 和 onEmailInput 相同的模式:protected onFirstNameValidate(firstName: string) { this.properties.onFormValidate({ firstName });}protected onLastNameValidate(lastName: string) { this.properties.onFormValidate({ lastName });}protected onEmailValidate(email: string) { this.properties.onFormValidate({ email });}{% instruction ‘在 ValidatedTextInput 中调用 onValidate’ %}你可能已注意到,当用户输入事件发生后,form 表单不再校验。这是因为我们已不在 ApplicationContext.ts 的 formInput 中处理校验,但我们还没有将校验添加到其它地方。要做到这一点,我们在 ValidateTextInput 中添加以下私有方法:private _onInput(value: string) { const { onInput, onValidate } = this.properties; onInput && onInput(value); onValidate && onValidate(value);}现在将它传给 TextInput,替换掉 this.properties.onInput:w(TextInput, { aria: { describedBy: this._errorId }, disabled, invalid, label, maxLength, minLength, name, placeholder, readOnly, required, type, value, onBlur, onInput: this._onInput})表单错误功能已恢复,并为无效字段添加了错误消息。{% section %}使用 blur 事件{% task ‘仅在第一次 blur 事件后开始校验’ %}现在只要用户开始在字段中输入就会显示校验信息,这是一种不友好的用户体验。在用户开始输入邮箱地址时就看到 “invalid email address” 是没有必要的,也容易分散注意力。更好的模式是将校验推迟到第一次 blur 事件之后,然后在 input 事件中开始更新校验信息。{% aside ‘Blur 事件’ %}当元素失去焦点后会触发 blur 事件。{% endaside %}现在已在 ValidatedTextInput 部件中调用了 onValidate,这是可以实现的。{% instruction ‘创建一个私有的 _onBlur 函数,它会调用 onValidate’ %}在 ValidatedTextInput.ts 文件中:private _onBlur(value: string) { const { onBlur, onValidate } = this.properties; onValidate && onValidate(value); onBlur && onBlur();}我们仅需在第一次 blur 事件之后使用这个函数,因为随后的校验交由 onInput 处理。下面的代码将根据输入框之前是否已校验过,来使用 this._onBlur 或 this.properties.onBlur:{% include_codefile ‘demo/finished/biz-e-corp/src/widgets/ValidatedTextInput.ts’ lines:50-67 %}w(TextInput, { aria: { describedBy: this._errorId }, disabled, invalid, label, maxLength, minLength, name, placeholder, readOnly, required, type, value, onBlur: typeof invalid === ‘undefined’ ? this._onBlur : onBlur, onInput: this._onInput}),现在只剩下修改 _onInput,如果字段已经有一个校验状态,则调用 onValidate:{% include_codefile ‘demo/finished/biz-e-corp/src/widgets/ValidatedTextInput.ts’ lines:24-31 %}private _onInput(value: string) { const { invalid, onInput, onValidate } = this.properties; onInput && onInput(value); if (typeof invalid !== ‘undefined’) { onValidate && onValidate(value); }}尝试输入一个邮箱地址来演示这些变化;它应该只在第一次离开 form 字段之后显示错误信息(或绿色边框),而在接下来的编辑中将立即触发校验。{% section %}在提交时校验{% task ‘创建一个模拟的服务器端校验,以处理提交的 form 表单’ %}到目前为止,我们的代码给用户提供了友好提示,但并不能防止我们将无效数据提交到我们的 worker 数组中。我们需要在 submitForm 操作中添加两个独立的检查:如果已存在的校验函数捕获到任何错误,则立即提交失败。执行额外检查(本示例中我们将检查邮箱唯一性)。这是我们在真正的应用程序中加入服务器端校验的地方。{% instruction ‘在 ApplicationContext.ts 中创建一个私有方法 _validateOnSubmit’ %}新增的 _validateOnSubmit 方法应该从对所有 _formData 运行已存在的输入校验开始,然后在存在任一错误后返回 false:private _validateOnSubmit(): boolean { const errors = this._validateInput(this._formData); this._formErrors = deepAssign({ firstName: true, lastName: true, email: true }, errors); if (this._formErrors.firstName || this._formErrors.lastName || this._formErrors.email) { console.error(‘Form contains errors’); return false; } return true;}接下来我们添加一个检查:假设每个工人的邮箱必须是唯一的,所以我们将在 _workerData 数组中测试输入的邮箱地址是否已存在。在现实中安全起见,这个检查运行在服务器端:{% include_codefile ‘demo/finished/biz-e-corp/src/ApplicationContext.ts’ lines:53-70 %}private _validateOnSubmit(): boolean { const errors = this._validateInput(this._formData); this._formErrors = deepAssign({ firstName: true, lastName: true, email: true }, errors); if (this._formErrors.firstName || this._formErrors.lastName || this._formErrors.email) { console.error(‘Form contains errors’); return false; } for (let worker of this._workerData) { if (worker.email === this._formData.email) { console.error(‘Email must be unique’); return false; } } return true;}修改完 ApplicationContext.ts 中的 submitForm 函数后,只有有效的工人数据才能提交成功。我们也需要在成功提交后清空 _formErrors 和 _formData:{% include_codefile ‘demo/finished/biz-e-corp/src/ApplicationContext.ts’ lines:82-92 %}public submitForm(): void { if (!this._validateOnSubmit()) { this._invalidator(); return; } this._workerData = [ …this._workerData, this._formData ]; this._formData = {}; this._formErrors = {}; this._invalidator();}{% section %}总结本教程不可能涵盖所有可能用例,但是存储、注入和显示校验状态的基本模式,为创建复杂的表单校验提供了坚实的基础。接下来将包含以下步骤:为传递给 WorkerForm 的对象配置错误信息创建一个 toast 来显示提交时的错误为一个 form 字段添加多个校验步骤你可以在 codesandbox.io 中打开完整示例或下载项目。{% section ’last’ %} ...

February 14, 2019 · 6 min · jiezi

Dojo 如何测试 widget

测试dojo/framework/src/testing/README.mdcommit 84e254725f41d60f624ab5ad38fe82e15b6348a2用于测试和断言 Dojo 部件期望的虚拟 DOM 和行为的简单 API。测试FeaturesharnessAPICustom Comparatorsselectorsharness.expectharness.expectPartialharness.triggerharness.getRenderAssertion TemplatesFeatures简单、熟悉和少量的 API重点测试 Dojo 虚拟 DOM 结构默认不需要 DOM全面支持函数式编程和 tsx 声明harness当使用 @dojo/framework/testing 时,harness 是最重要的 API,主要用于设置每一个测试并提供一个执行虚拟 DOM 断言和交互的上下文。目的在于当更新 properties 或 children 和失效部件时,镜像部件的核心行为,并且不需要任何特殊或自定义逻辑。APIharness(renderFunction: () => WNode, customComparators?: CustomComparator[]): Harness;renderFunction: 返回被测部件 WNode 的函数customComparators: 一组自定义比较器的描述符。每个描述符提供一个比较器函数,用于比较通过 selector 和 property 定位的 propertiesharness 函数返回的 Harness 对象提供了与被测部件交互的精简 API:Harnessexpect: 对被测部件完整的渲染结果执行断言expectPartial: 对被测部件的一部分渲染结果执行断言trigger: 用于在被测部件的节点上触发函数getRender: 根据提供的索引返回对应的渲染器使用 @dojo/framework/widget-core 中的 w() 函数设置一个待测试的部件简单且眼熟:class MyWidget extends WidgetBase<{ foo: string }> { protected render() { const { foo } = this.properties; return v(‘div’, { foo }, this.children); }}const h = harness(() => w(MyWidget, { foo: ‘bar’ }, [‘child’]));如下所示,harness 函数也支持 tsx 语法。README 文档中其余示例使用编程式的 w() API,在 unit tests 中可查看更多 tsx 示例。const h = harness(() => <MyWidget foo=“bar”>child</MyWidget>);renderFunction 是延迟执行的,所以可在断言之间包含额外的逻辑来操作部件的 properties 和 children。let foo = ‘bar’;const h = harness(() => { return w(MyWidget, { foo }, [ ‘child’ ]));};h.expect(/** assertion that includes bar /);// update the property that is passed to the widgetfoo = ‘foo’;h.expect(/ assertion that includes foo **/)Custom Comparators在某些情况下,我们在测试期间无法得知属性的确切值,所以需要使用自定义比较描述符。描述符中有一个定位要检查的虚拟节点的 selector,一个应用自定义比较的属性名和一个接收实际值并返回一个 boolean 类型断言结果的比较器函数。const compareId = { selector: ‘*’, // all nodes property: ‘id’, comparator: (value: any) => typeof value === ‘string’ // checks the property value is a string};const h = harness(() => w(MyWidget, {}), [compareId]);对于所有的断言,返回的 harness API 将只对 id 属性使用 comparator 进行测试,而不是标准的相等测试。selectorsharness API 支持 CSS style 选择器概念,来定位要断言和操作的虚拟 DOM 中的节点。查看 支持的选择器的完整列表 以了解更多信息。除了标准 API 之外还提供:支持将定位节点 key 属性简写为 @ 符号当使用标准的 . 来定位样式类时,使用 classes 属性而不是 class 属性harness.expect测试中最常见的需求是断言部件的 render 函数的输出结构。expect 接收一个返回被测部件期望的渲染结果的函数作为参数。APIexpect(expectedRenderFunction: () => DNode | DNode[], actualRenderFunction?: () => DNode | DNode[]);expectedRenderFunction: 返回查询节点期望的 DNode 结构的函数actualRenderFunction: 一个可选函数,返回被断言的实际 DNode 结构h.expect(() => v(‘div’, { key: ‘foo’ }, [w(Widget, { key: ‘child-widget’ }), ’text node’, v(‘span’, { classes: [‘class’] })]));expect 也可以接收第二个可选参数,返回要断言的渲染结果的函数。h.expect(() => v(‘div’, { key: ‘foo’ }), () => v(‘div’, { key: ‘foo’ }));如果实际的渲染输出和期望的渲染输出不同,就会抛出一个异常,并使用结构化的可视方法,用 (A) (实际值)和 (E) (期望值)指出所有不同点。断言的错误输出示例:v(“div”, { “classes”: [ “root”,(A) “other”(E) “another” ], “onclick”: “function”}, [ v(“span”, { “classes”: “span”, “id”: “random-id”, “key”: “label”, “onclick”: “function”, “style”: “width: 100px” }, [ “hello 0” ]) w(ChildWidget, { “id”: “random-id”, “key”: “widget” }) w(“registry-item”, { “id”: true, “key”: “registry” })])harness.expectPartialexpectPartial 根据 selector 断言部件的部分渲染输出。APIexpectPartial(selector: string, expectedRenderFunction: () => DNode | DNode[]);selector: 用于查找目标节点的选择器expectedRenderFunction: 返回查询节点期望的 DNode 结构的函数actualRenderFunction: 一个可选函数,返回被断言的实际 DNode 结构示例:h.expectPartial(’@child-widget’, () => w(Widget, { key: ‘child-widget’ }));harness.triggerharness.trigger() 在 selector 定位的节点上调用 name 指定的函数。interface FunctionalSelector { (node: VNode | WNode): undefined | Function;}trigger(selector: string, functionSelector: string | FunctionalSelector, …args: any[]): any;selector: 用于查找目标节点的选择器functionSelector: 要么是从节点的属性中找到的被调用的函数名,或者是从节点的属性中返回一个函数的函数选择器args: 为定位到的函数传入的参数如果有返回结果,则返回的是被触发函数的结果。用法示例:// calls the onclick function on the first node with a key of fooh.trigger(’@foo’, ‘onclick’);// calls the customFunction function on the first node with a key of bar with an argument of 100// and receives the result of the triggered functionconst result = h.trigger(’@bar’, ‘customFunction’, 100);functionalSelector 返回部件属性中的函数。函数也会被触发,与使用普通字符串 functionSelector 的方式相同。用法示例:假定有如下 VDOM 树结构,v(Toolbar, { key: ’toolbar’, buttons: [ { icon: ‘save’, onClick: () => this._onSave() }, { icon: ‘cancel’, onClick: () => this._onCancel() } ]});并且你想触发 save 按钮的 onClick 函数。h.trigger(’@buttons’, (renderResult: DNode<Toolbar>) => { return renderResult.properties.buttons[0].onClick;});harness.getRenderharness.getRender() 返回索引指定的渲染器,如果没有提供索引则返回最后一个渲染器。getRender(index?: number);index: 要返回的渲染器的索引用法示例:// Returns the result of the last renderconst render = h.getRender();// Returns the result of the render for the index providedh.getRender(1);Assertion Templates断言模板(assertion template)允许你构建一个传入 h.expect() 的期望渲染函数。断言模板背后的思想来自经常要断言整个渲染输出,并需要修改断言的某些部分。要使用断言模板,首先需要导入模块:import assertionTemplate from ‘@dojo/framework/testing/assertionTemplate’;然后,在你的测试中,你可以编写一个基本断言,它是部件的默认渲染状态:假定有以下部件:class NumberWidget extends WidgetBase<{ num?: number }> { protected render() { const { num } = this.properties; const message = num === undefined ? ’no number passed’ : the number ${num}; return v(‘div’, [v(‘span’, [message])]); }}基本断言如下所示:const baseAssertion = assertionTemplate(() => { return v(‘div’, [ v(‘span’, { ‘~key’: ‘message’ }, [ ’no number passed’ ]); ]);});在测试中这样写:it(‘should render no number passed when no number is passed as a property’, () => { const h = harness(() => w(NumberWidget, {})); h.expect(baseAssertion);});现在我们看看,为 NumberWidget 部件传入 num 属性后,如何测试数据结果:it(‘should render the number when a number is passed as a property’, () => { const numberAssertion = baseAssertion.setChildren(’~message’, [’the number 5’]); const h = harness(() => w(NumberWidget, { num: 5 })); h.expect(numberAssertion);});这里,我们使用 baseAssertion 的 setChildren() api,然后我们使用特殊的 ~ 选择器来定位 key 值为 ~message 的节点。~key 属性(使用 tsx 的模板中是 assertion-key)是断言模板的一个特殊属性,在断言时会被删除,因此在匹配渲染结构时不会显示出来。此功能允许你修饰断言模板,以便能简单的选择节点,而不需要扩展实际的部件渲染函数。一旦我们获取到 message 节点,我们就可以将其子节点设置为期望的 the number 5,然后在 h.expect 中使用生成的模板。需要注意的是,断言模板在设置值时总是返回一个新的断言模板,这可以确保你不会意外修改现有模板(可能导致其他测试失败),并允许你基于新模板,增量逐层构建出新的模板。断言模板具有以下 API:setChildren(selector: string, children: DNode[], type?: ‘prepend’ | ‘replace’ | ‘append’): AssertionTemplateResult;setProperty(selector: string, property: string, value: any): AssertionTemplateResult;getChildren(selector: string): DNode[];getProperty(selector: string, property: string): any; ...

February 11, 2019 · 3 min · jiezi

Dojo 路由

路由dojo/framework/src/routing/README.mdcommit b682b06ace25eea86d190e56dd81042565b35ed1Dojo 应用程序的路由路由FeaturesRoute 配置路径参数RouterHistory ManagersHash HistoryState HistoryMemory HistoryOutlet EventRouter Context InjectionOutletsGlobal Error OutletLinkActiveLinkFeatures部件(Widget)是 Dojo 应用程序的基本概念,因此 Dojo 路由提供了一组与应用程序中的部件直接集成的组件。这些组件能将部件注册到路由上,且不需要掌握路由相关的任何知识。Dojo 应用程序中的路由包括以下内容:Outlet 部件封装器指定 route 的 outlet key 和表现视图之间的映射关系Route 配置,用于在路径和 outlet key 之间建立映射关系Router 基于当前路径解析 RouteHistory 提供器负责向 Router 通知路径的更改Registry 将 Router 注入到部件系统中Route 配置RouteConfig 用于注册应用程序的路由,它定义了路由的 path、关联的 outlet 以及嵌套的子 RouteConfig。一个完整的路由就是由递归嵌套的 Route 构成的。路由配置示例:import { RouteConfig } from ‘@dojo/framework/routing/interfaces’;const config: RouteConfig[] = [ { path: ‘foo’, outlet: ‘root’, children: [ { path: ‘bar’, outlet: ‘bar’ }, { path: ‘baz’, outlet: ‘baz’, children: [ { path: ‘qux’, outlet: ‘qux’ } ] } ] }];此配置将注册以下路由和 outlet:RouteOutlet/fooroot/foo/barbar/foo/bazbaz/foo/baz/quxqux路径参数在 RouteConfig 的 path 属性中,在 path 的值中使用大括号可定义路径参数。 Parameters will match any segment and the value of that segment is made available to matching outlets via the mapParams Outlet options. The parameters provided to child outlets will include any parameters from matching parent routes.const config = [ { path: ‘foo/{foo}’, outlet: ‘foo’ }];具有路径参数的路由,可为每个路由指定默认的参数。当用没有指定参数的 outlet 生成一个链接,或者当前路由中不存在参数时,可使用这些默认参数值。const config = [ { path: ‘foo/{foo}’, outlet: ‘foo’, defaultParams: { foo: ‘bar’ } }];可使用可选的配属属性 defaultRoute 来设置默认路由,如果当前路由没有匹配到已注册的路由,就使用此路由。const config = [ { path: ‘foo/{foo}’, outlet: ‘foo’, defaultRoute: true }];RouterRouter 用于注册 route 配置,将 route 配置信息传入 Router 的构造函数即可:const router = new Router(config);会自动为 router 注册一个 HashHistory 历史管理器(history manager)。可在第二个参数中传入其他历史管理器。import { MemoryHistory } from ‘@dojo/framework/routing/MemoryHistory’;const router = new Router(config, { HistoryManager: MemoryHistory });使用应用程序路由配置创建路由后,需要让应用程序中的所有组件可使用这些路由。这是通过使用 @dojo/framework/widget-core/Registry 中的 Registry,定义一个将 invalidator 连接到 router 的 nav 事件的注入器,并返回 router 实例实现的。这里使用 key 来定义注入器,路由器的默认 key 值为 router。import { Registry } from ‘@dojo/framework/widget-core/Registry’;import { Injector } from ‘@dojo/framework/widget-core/Injector’;const registry = new Registry();// 假设我们有一个可用的 router 实例registry.defineInjector(‘router’, () => { router.on(’nav’, () => invalidator()); return () => router;};注意: 路由提供了 注册 router 的快捷方法。最后,为了让应用程序中的所有部件都能使用 registry,需要将其传给 vdom renderer 的 .mount() 方法。const r = renderer(() => v(App, {}));r.mount({ registry });History Managers路由自带三个历史管理器,用于监视和更改导航状态:HashHistory、StateHistory 和 MemoryHistory。默认使用 HashHistory,但是可在创建 Router 时传入不同的 HistoryManager。const router = new Router(config, { HistoryManager: MemoryHistory });Hash History基于哈希的管理器使用片段标识符(fragment identifier)来存储导航状态,是 @dojo/framework/routing 中的默认管理器。import { Router } from ‘@dojo/framework/routing/Router’;import { HashHistory } from ‘@dojo/framework/routing/history/HashHistory’;const router = new Router(config, { HistoryManager: HashHistory });历史管理器有 current、set(path: string) 和 prefix(path: string) 三个API。HashHistory 类假定全局对象是浏览器的 window 对象,但可以显式提供对象。管理器使用 window.location.hash 和 hashchange 事件的事件监听器。current 访问器返回当前路径,不带 # 前缀。State History基于状态的历史管理器使用浏览器的 history API:pushState() 和 replaceState(),来添加和修改历史纪录。状态历史管理器需要服务器端支持才能有效工作。Memory HistoryMemoryHistory 不依赖任何浏览器 API,而是保持其自身的内部路径状态。不要在生产应用程序中使用它,但在测试路由时却很有用。import { Router } from ‘@dojo/framework/routing/Router’;import { MemoryHistory } from ‘@dojo/framework/routing/history/MemoryHistory’;const router = new Router(config, { HistoryManager: MemoryHistory });Outlet Event当每次进入或离开 outlet 时,都会触发 router 实例的 outlet 事件。outlet 上下文、enter 或 exit 操作都会传给事件处理函数。router.on(‘outlet’, ({ outlet, action }) => { if (action === ’enter’) { if (outlet.id === ‘my-outlet’) { // do something, perhaps fetch data or set state } }});Router Context InjectionRouterInjector 模块导出一个帮助函数 registerRouterInjector,它组合了 Router 实例的实例化,注册 router 配置信息和为提供的 registry 定义注入器,然后返回 router 实例。import { Registry } from ‘@dojo/framework/widget-core/Registry’;import { registerRouterInjector } from ‘@dojo/framework/routing/RoutingInjector’;const registry = new Registry();const router = registerRouterInjector(config, registry);可使用 RouterInjectiorOptions 覆盖默认值:import { Registry } from ‘@dojo/framework/widget-core/Registry’;import { registerRouterInjector } from ‘@dojo/framework/routing/RoutingInjector’;import { MemoryHistory } from ‘./history/MemoryHistory’;const registry = new Registry();const history = new MemoryHistory();const router = registerRouterInjector(config, registry, { history, key: ‘custom-router-key’ });Outlets路由集成的一个基本概念是 outlet,它是与注册的应用程序 route 关联的唯一标识符。Outlet 是一个标准的 dojo 部件,可在应用程序的任何地方使用。Outlet 部件有一个精简的 API:id: 匹配时执行 renderer 的 outlet 标识。renderer: 当 outlet 匹配时调用的渲染函数。routerKey (可选): 在 registry 中定义路由时使用的 key - 默认为 router。接收渲染的 outlet 名称和一个 renderer 函数,当 outlet 匹配时,该函数返回要渲染的 DNode。render() { return v(‘div’, [ w(Outlet, { id: ‘my-outlet’, renderer: () => { return w(MyWidget, {}); }}) ])}可为 renderer 函数传入 MatchDetails 参数,该参数提供路由专有信息,用于确定要渲染的内容和计算传入部件的属性值。interface MatchDetails { /** * Query params from the matching route for the outlet / queryParams: Params; /* * Params from the matching route for the outlet / params: Params; /* * Match type of the route for the outlet, either index, partial or error / type: MatchType; /* * The router instance / router: RouterInterface; /* * Function returns true if the outlet match was an error type / isError(): boolean; /* * Function returns true if the outlet match was an index type */ isExact(): boolean;}render() { return v(‘div’, [ w(Outlet, { id: ‘my-outlet’, renderer: (matchDetails: MatchDetails) => { if (matchDetails.isError()) { return w(ErrorWidget, {}); } if (matchDetails.isExact()) { return w(IndexWidget, { id: matchDetails.params.id }); } return w(OtherWidget, { id: matchDetails.params.id }); }}) ])}Global Error Outlet只要注册了一个 error 匹配类型,就会自动为匹配的 outlet 添加一个全局 outlet,名为 errorOutlet。这个 outlet 用于为任何未知的路由渲染一个部件。render() { return w(Outlet, { id: ’errorOutlet’, renderer: (matchDetails: MatchDetails) => { return w(ErrorWidget, properties); } });}LinkLink 组件是对 DOM 节点 a 的封装,允许用户为创建的链接指定一个 outlet。也可以通过将 isOutlet 属性值设置为 false 来使用静态路由。如果生成的链接需要指定在 route 中不存在的路径或查询参数,可使用 params 属性传入。import { Link } from ‘@dojo/framework/routing/Link’;render() { return v(‘div’, [ w(Link, { to: ‘foo’, params: { foo: ‘bar’ }}, [ ‘Link Text’ ]), w(Link, { to: ‘#/static-route’, isOutlet: false, [ ‘Other Link Text’ ]) ]);}所有标准的 VNodeProperties 都可用于 Link 组件,因为最终是使用 @dojo/framework/widget-core 中的 v() 来创建一个 a 元素。ActiveLinkActiveLink 组件是对 Link 组件的封装,如果链接处于激活状态,则可以为 a 节点设置样式类。import { ActiveLink } from ‘@dojo/framework/routing/ActiveLink’;render() { return v(‘div’, [ w(ActiveLink, { to: ‘foo’, params: { foo: ‘bar’ }, activeClasses: [ ’link-active’ ]}, [ ‘Link Text’ ]) ]);} ...

February 5, 2019 · 4 min · jiezi

你的第一个 Dojo 应用程序

tutorials/001_static_content/index.mdcommit 3e0f3ff1ed392163bc65e9cd015c4705cb9c586e{% section ‘first’ %}你的第一个 Dojo 应用程序概述通过学习本教程,你将了解如何创建你的第一个 Dojo 程序,并使用它在浏览器上打印一段简短信息。前提你可以打开 codesandbox.io 上的教程 或者 下载 示例项目,然后运行 npm install。已全局安装 @dojo/cli 命令行工具。 参考 本地安装 dojo 文章了解更多信息。你也需要熟悉 TypeScript 语言, 因为 Dojo 是基于 TypeScript 语言开发的。{% section %}启动开发服务器{% task ‘构建并运行应用程序。’ %}在开始修改源代码之前,我们先启动开发服务器,如此就可以实时查看修改代码变化带来的变化。 在程序的根目录运行以下命令:dojo build –mode dev –watch memory –serve或者使用缩写参数:dojo build -m dev -w memory -s现在,打开浏览器并导航到 http://localhost:9999 查看当前应用程序。接下来,我们开始定制应用程序。{% section %}页面内容{% task ‘改变页面中显示的内容。’ %}为了开始定制应用程序,我们先删掉已有的内容。 需要调整两个地方。 第一行,index.html 文件中的 “Welcome to biz-e-core”。{% instruction ‘打开 src 文件夹中的 index.html 文件,删除 <p> 标签及其内容 “Welcome to biz-e-corp”。’ %}注意,页面已自动刷新。 这意味着我们不需要中断我们的工作,去刷新或重新构建程序,就可以实时查看调整后的效果。现在我们删掉 “Hello, Dojo World!” 信息。{% instruction ‘打开位于 /src 下的 main.ts。’ %}你将看到以下代码:import renderer from ‘@dojo/framework/widget-core/vdom’;import { v } from ‘@dojo/framework/widget-core/d’;const r = renderer(() => v(‘div’, [ ‘Hello, Dojo World!’ ]));r.mount({ domNode: document.querySelector(‘my-app’) as HTMLElement });可能现在看看不懂部分代码,但看完后面的教程后,你将逐步了解。 现在我们重点了解这行代码:v(‘div’, [ ‘Hello, Dojo World!’ ])v 函数指示 Dojo 创建一个 HTML 元素,这段代码创建一个 <div> 元素, 并在其中添加文字 “Hello, Dojo World!”。 我们接下来将构建一个页面,在这个页面中能查看 Biz-E 公司的员工列表, 所以我们将标签和消息修改为更合适的内容。{% instruction ‘使用 <h1> 标签替换掉 <div> 标签,并用 “Biz-E-Bodies” 替换掉 “Hello, Dojo World!”’ %}const r = renderer(() => v(‘h1’, [ ‘Biz-E-Bodies’ ]));现在,我们再回过头来看 v 函数。我们有意避免使用 document.createElement 来创建 DOM (Document Object Model) 元素。这是因为我们不会直接创建 DOM 元素。相反,我们用 TypeScript 创建一个视图(view)的表现层,然后 Dojo 能高效的将这个视图转换为 DOM 元素,并在页面中渲染出来。这个渲染技术就是所谓的 virtual DOM。传统的 web 程序,将 DOM 和 JavaScript 逻辑揉在一起,导致较复杂的应用程序的复杂度提高且效率低下。当构建具有大量的状态和数据更改的应用程序时, 虚拟 DOM 技术能极大简化程序逻辑并提高性能。虚拟 DOM 扮演中间人角色,处在应用程序逻辑和在页面中渲染真正 DOM 节点之间。Dojo 使用自研的虚拟 DOM 库,提供最高效的与视图中的 DOM 元素交互方式。虚拟 DOM 的另一个好处是它促使你编写出 reactive 风格的代码,而这种风格的代码会简化你的程序。在本教程的最后部分,我们将学习如何为虚拟 DOM 节点设置属性。{% section %}虚拟 DOM 属性{% task ‘为虚拟 DOM 节点设置属性。’ %}现在我们为早先创建的 HelloWorld.ts 文件中的 <h1> 元素增加属性。v 函数的第二个参数用于传入这些属性。{% instruction ‘更新 v 函数调用, 传入 title 属性。’ %}v(‘h1’, { title: ‘I am a title!’ }, [ ‘Biz-E-Bodies’ ]){% aside ‘虚拟 Dom 的 Properties 和 Attributes’ %}虚拟 DOM 系统自动将 string 类型的 properties 作为 attribute,其余的作为 properties 添加到 DOM 节点上。{% endaside %}注意,我们在 tag 和内容参数中间添加了一个参数。第二个参数可以往元素中设置任意 attributes 或 properties。这种使用 JavaScript 或 TypeScript 创建 DOM 元素的方法被称为 HyperScript,这种技术应用在很多虚拟 DOM 实现中。{% section %}总结恭喜!您在精通 Dojo 之旅中,迈出了坚实的一步。Dojo 应用程序的组件中,我们开始了解 Dojo 应用程序中的重要组件。你可以在 codesandbox.io 中打开完整示例或下载项目。{% section ’last’ %} ...

February 2, 2019 · 2 min · jiezi

本地安装 Dojo

tutorials/000_local_installation/index.mdcommit ef8cd9d90d326549aa3e6b43c2d4b78f846144d0本地安装 Dojo概述本教程介绍如何在本地安装 Dojo 环境。创建 Dojo 应用程序首先,我们需要创建一个 Dojo 项目。 Dojo 为创建应用程序提供强大和先进的工具。 它提供了一个高效的命令行工具,来简化安装过程, 使用以下命令安装此工具:npm install -g @dojo/cli该命令会安装 Dojo 命令行工具(@dojo/cli),它简化创建、测试和构建应用程序等相关的开发任务。 @dojo/cli 工具默认自带两个选项:ejet - 将一个项目与 @dojo/cli 工具断开,允许高阶用户自定义配置version - 显示 @dojo/cli 版本信息和已安装的命令运行 dojo 命令将显示所有可用的子命令,即使这些子命令还没有安装。当你尝试运行一个不可用的子命令时,CLI 会打印一条消息提示需要先安装此命令。在创建你的第一个 Dojo 应用程序前,请先全局安装 @dojo/cli-create-app 命令,该命令用于创建项目模板:npm install -g @dojo/cli-create-app一旦命令安装完成,请运行以下命令创建你的 Dojo 项目:dojo create app –name first-dojo-app{% aside ‘Dojo create arguments’ %}传入 dojo create app 的很多参数都有缩写版本。所以 dojo create app -n first-dojo-app 等价于 dojo create app –name first-dojo-app。{% endaside %}该命令将在新建的 “first-dojo-2-app” 文件夹下创建 Dojo 应用程序的基本项目结构,并预安装的所有依赖库。就是这么简单,此刻我们已成功创建了第一个基本的 Dojo 应用程序,并安装了它的依赖库。 现在让我们看看新建的应用程序能做什么! 第一步,我们使用另一个 @dojo/cli 命令。 您不需要自己动手去安装这个命令,它会随着其他依赖一起安装。 在终端切换到 first-dojo-app 文件夹,然后输入 dojo build –mode dev –watch –serve 命令:cd first-dojo-appdojo build –mode dev –watch –serve(或简写为 dojo build -m dev -w -s){% aside ‘Production build as a default’ %}@dojo/cli-build-app 命令默认使用 –mode dist 参数创建一个可用于生产环境的构建。提供的 –mode dev 参数将指示该命令创建一个对开发友好的构建,方便调试和不间断的开发。{% endaside %}此命令将触发 Dojo 工具使用 Webpack 构建和 transpile 项目, Webpack 是一款优秀的 JavaScript 源码优化工具。 –watch(或 -w) 标志用于监听变化,当有任何变化发生并保存到硬盘后,就立即重新构建项目;而 –serve (或 -s)标志会启动一个轻量级的 web 服务器,这样当我们修改一些源码后,可在浏览器中即时运行程序。为了查看应用程序的功能, 请打开一款主流的 web 浏览器(如最新版的 Chrome,Edge,Firefox,Internet Explore 或 Safari), 并导航到 http://localhost:9999。 您将看到一个简陋的页面,欢迎你访问新建的应用程序。 ...

February 1, 2019 · 1 min · jiezi