Blog
Blog
PHODAL

查看作者 Phodal Huang

在前端应用中,动画是一个常见的场景。在使用了一系列的动画库之后,终于需要自己来实现一个动画了。这次的动画则是基于 Angular 框架。我的场景是一个类似于添加购物车的动画。在这个场景里,需要两个动画,一个是购物车数量的增加动画,一个则是折叠页面元素的动画。 在实现的过程上,我采用了两种不同的 Angular 动画的方式: - 使用 TypeScript 控制动画 - 使用 @Component 中的 animations Angular 动画基础 --- 如 Angular 官网中的示例那样,要在 Angular 应用中添加动画是比较简单的一件事——前提是我们懂得添加的法则。如下是官网的示例: ``` @Component({ selector: 'app-hero-list-basic', template: `
  • {{hero.name}}
`, styleUrls: ['./hero-list.component.css'], animations: [ trigger('heroState', [ state('inactive', style({ backgroundColor: '#eee', transform: 'scale(1)' })), state('active', style({ backgroundColor: '#cfd8dc', transform: 'scale(1.1)' })), transition('inactive => active', animate('100ms ease-in')), transition('active => inactive', animate('100ms ease-out')) ]) ] }) ``` 要使用动画,需要在模板中使用 ``[@heroState]``语法,这里的 ``heroState`` 对应着 ``@Component`` 中的 ``heroState`` 相关的动画。 - 在这个 ``trigger`` 中,我们定义了 ``inactive`` 和 ``active`` 两个不同的 ``state``。即当模板中的 ``hero.state`` 发生变化的时候,我们就会找到对应的 ``state`` 的样式等等的内容。 - 在这个 ``trigger`` 中,我们还定义了两个 ``transition``,即当我们的 ``state`` 从 ``inactive => active`` 或者 ``active => inactive`` 时,我们就会执行后面的动画。 原理上,大概就是这么多了。然后,我就开始了我的动画之旅。 购物车数量增加动画 --- 对于我的场景来说,要添加这个动画并不难。无非就是上一个值淡出,新的值淡入: ``` trigger('count', [ transition('void => current', [ animate( '400ms 150ms', keyframes([ style({ opacity: 0.6, transform: 'translateY(0)', offset: 0 }), style({ opacity: 0.3, transform: 'translateY(-15px)', offset: 0.5 }), style({ opacity: 0, transform: 'translateY(-30px)', offset: 1 }) ]) ) ]), transition('void => last', [ animate( 250, keyframes([ style({ opacity: 0, transform: 'translateY(100%)', offset: 0 }), style({ opacity: 0.3, transform: 'translateY(15px)', offset: 0.5 }), style({ opacity: 0.8, transform: 'translateY(0)', offset: 1.0 }) ]) ) ]) ]) ``` 代码就是这么简单,这里用到了关键帧 ``keyframes``,来进行一些简单的动画转换。 页面缩放动画 --- 随后,我需要做的就是对页面的元素进行缩放等效果,这个时候就需要用到 AnimationBuilder 来实现了: ``` const myAnimation = this.animationBuilder.build([ animate( 1000, keyframes([ style({ opacity: 0.8, transform: 'scale(0.8)', offset: 0.3 }), style({ opacity: 0.3, transform: 'scale(0.3)', offset: 0.5 }), style({ opacity: 0.2, transform: 'scale(0.2) translate(12000px, 8000px)', offset: 1 }) ]) ) ]); const player = myAnimation.create(forkFormComponent); player.play(); player.onDone(() => { const nativeElement = this.cartContainer.nativeElement; nativeElement.removeChild(nativeElement.childNodes[0]); this.renderer.setStyle(nativeElement, 'display', 'none'); }); ``` 在那之前,我先复制了页面元素: ``` const formElement = this.formElement.nativeElement; const forkFormComponent = this.cartContainer.nativeElement; forkFormComponent.appendChild(formElement.cloneNode(true)); this.renderer.setStyle(forkFormComponent, 'display', 'block'); this.renderer.setStyle(forkFormComponent, 'position', 'absolute'); this.renderer.setStyle(forkFormComponent, 'top', '-300px'); this.renderer.setStyle(forkFormComponent, 'left', '0'); ``` 这样一来,就能复制页面的 DOM,然后实现缩放效果了。
> “我只是修改了一个 if 条件,没有想到它导致持续集成失败,部署也因此失败,还影响了 QA 测试其它功能。” Web 应用的质量提升,是一个非常有意思的话题。我们明知道有一系列的手段可以提升代码质量,但是限于多种原因,我们并不会去做。在我工作的第一个项目里,由于大家都是年轻人(Junior Consultant),我们实施了一系列的基础措施,来提升应用质量,诸如写测试、追求测试覆盖率、运行预提交脚本等等。 在最近的这个项目里,我们面临着类似的问题——需要提升项目的代码质量。于是,便想写一篇文章介绍一个相关的内容。这篇文章大致可以分为这几个部分: - Web 应用的质量问题 - 使用测试提升质量 - 使用 Lint 和 Git Hooks 检测代码 - 如何防范危险提交 那么,让我们继续回到老生常谈的 “Web 应用的质量问题”。 Web 应用的质量问题 --- Web 应用通常面临着上线和质量之间的博弈——只要不影响用户体验,小的 Bug 往往对于项目来说可以 “容忍” 的。这样一来说可以早些上线,实现用户价值。除此还有其开发的影响:一个是敏捷方式的开发周期,一个则是可以多次上线。 故而,Web 开发与一些特殊领域及行业的软件开发不同,在这些特殊的行业里,一个开发成本上亿的软件,可能只会运行、部署一次,不会有第二次机会,如原子弹的控制系统。还有一些类型的案例,就是智能汽车上的自动驾驶系统,稍有不慎就是车损人忙。相当于 Web 应用虽然更新困难,可它们还是能远程更新的。但是在这些系统上,它们就更追求系统的质量,而不是开发速度。Web 应用部署失败可以回滚,虽然会带来一定的钱力损失,但是极少带来生命危险。 因此在**质量**和**速度**方面,在 Web 开发上因此保持着一个微妙的平衡。 可软件开发不仅仅只有质量和速度的问题,还有一个产品问题——即,能做出符合用户需求的产品。于是,就变成了**质量-速度-需求**,一个更复杂的平衡。为了交付出更符合用户需求的产品,就不得不经常做一些需求变更。而取决于这些变更的时间,它往往会影响到代码质量和开发速度——实现一个需求的时间越短,那么其测试的时间越短,Bug 出现的可能性就更高。过去我遇到过,今晚上线,下午临时改需求。可想而知,测试人员是没有时间测试的。 在这个时候,持续集成只能显式地告诉我们,我们的测试挂了,我们的某些功能 broken 了,我们不应该部署这个新版本。然而并不是持续集成出问题了,我们就不能部署,我们仍然还是能部署的。 Blabla,那么问题来了,最有效的方式呢? 使用测试提高质量 --- 用于保证这个项目的质量,在代码提交之后,会经过一系列的测试: - 单元测试 - 自动化 UI 测试 - 开发人员手动进行集成测试 - 测试人员进行 3~4 轮的测试 如果只是宏观来看一个项目的测试的话,那么在一个敏捷项目里,测试可以分为这么几个阶段: - Dev (开发)环境的 Desk Check,主要用于演示功能是否和需求一致。 - QA (测试)环境的测试,用于进行一些常规的测试。 - ST (System Test)环境的测试,常用于与第三方系统联调。当出现第三方系统的时候,就需要该环境来集成。 - UAT(User Acceptance Test,用户接受测试)环境的测试,通常用由业务方的代码来验收产品是否符合需求。 如果一个 Web 应用能经过这么一系列的测试,那么它的质量在一定程度上是得到保障的。所以,开发人员如果不想活得太久,就可以 “不负责任” 地直接把功能扔给测试人员。笑~ 可是,在有一些公司时吧,Bug 率可是会影响绩效的,又或者是有这么多 Bug 看上去不那么专业,等等 blabla。 言而总之,总而言之,开发人员自己写测试会更友好一些。按照测试金字塔理论来说,我们需要三种类型的测试: - 单元测试,用于保证我们的基础函数是正常、正确工作的。 - 服务测试,不仅仅自身的服务,也会测试第三方依赖服务。 - UI 测试,模仿用户操作行为的测试。 对于一个前端项目来说,我们通常只需要两种:``单元测试`` 和 ``E2E 测试``。实际上,理论上应该还有 ``UI 组件的测试``,但是一般而言,我们在选用 UI 组件的时候,会考虑到组件的稳定性。 可是在多数国内公司里,写测试往往是不可能的。退而求次,我们就需要一种更简单而友好的方式,来做这样的事情。 使用 Lint 和 Git Hooks 检测代码 --- 在代码提交之前,我们还可以进行一些常见的操作: - 静态代码分析(lint),用于进行静态代码分析,常见的如 Lint4j、TSLint、ESLint。 - 运行测试,为了不影响持续集成,我们需要在代码提交之前进行测试。 现代的编辑器(使用相应插件)、IDE 可以提高很好的技术手段,在开发的过程中静态代码分析,并随时提高建议。如 Intellij IDEA 和 WebStorm 就会根据 TSLint,来提醒开发者 TypeScript 代码的一些规范问题。 这些分析工具主要进行一些代码上的分析,如《全栈应用开发:精益实践》一书所说,一般会进行如下一系列的风格检测: - 规范函数名及变量 - 代码格式规范 - 限制语言特性 - 函数行数限制 - 多重嵌套限制 - 未使用代码 - 等等 而这些规范,如果没有强制,那就是个游戏。于是,我们通常会依赖于 Git Hooks 来做这样的事。对于一个使用 Git 来管理源码的项目来说,Git Hooks 可以做这么一些事情,可以在 ``.git/hooks`` 目录下查看: ``` applypatch-msg post-merge pre-auto-gc prepare-commit-msg commit-msg post-receive pre-commit push-to-checkout post-applypatch post-rewrite pre-push update post-checkout post-update pre-rebase post-commit pre-applypatch pre-receive ``` 一般而言,我们只会在两个阶段做相应的事情: - ``pre-commit``,预本地提交。通常会在该提交之前,进行一些语法和 lint 的检测。 - ``pre-push``,预远程提交。通常会在该提交之前,运行一些测试。 于是,在我们的这个前端项目里,我们就又写了这两个 ``scripts``。对应的实现如下: ``` { "precommit": "lint-staged", "prepush": "ng test && ng build --prod" } ``` 在 ``precommit`` 时,我们配合 ``lint-staged`` 和 ``prettier`` 来进行代码格式化: ``` "lint-staged": { "src/app/*.{css,scss}": [ "stylelint --syntax=scss", "prettier --parser --write", "git add" ], "{src,test}/**/*.ts": [ "prettier --write --single-quote", "git add" ] } ``` 事实上,使用 ``ng lint --fix`` 也是一个不错的方式。 随后,我们在 push 代码之前,即 ``prepush``,进行了测试及 Angular 的构建 production 的脚本。由于单元测试运行得相当的快,它可以在几分钟内完成,快速对问题做出响应。而不是等到持续集成出问题时,再去修复。 但是 Git 提高了这一种的种选项,也提供了一个 ``--no-verify`` 的参数。它可以让开发者不需要进行上面的验证,就能提交代码。 我们往往无法阻止别人做这样的事情,特别是当出现多个团队协作的时候。 难以防范的危险提交 --- 原本,我想将标题取为 “有风险的提交”,但是我觉得危险的提交更为可靠。 常见的有要去吃饭了、要下班了、要开会了等等,临走前提交了一下代码。功能可能本身没有问题,但是它 block 后续的一系列行为。 当然了出现不可坑的因素,如地震、火灾等的时候,就不需要考虑这些事情了。 只是有了这些规范和实践,可以帮助我们开发出更稳定的 Web 应用。 结论 --- 开发速度和质量,是一个难以平衡的天平。在不同的时间里,我们应该做不同的技术决策。

就我而言,一年里我也没写出几篇让自己满意的文章。因为写一篇好的技术文章真的很难。

很遗憾的是,在今日的我看来,仍旧是两年前的那个观点:全栈是未来。这次并不是因为大公司全职业,小公司全栈,而是技术的门槛越来越低——人工智能 API 化、后端无服务器、跨平台应用。 在过去的一年里,我的主业仍然是前端开发。而如大家所见,我在这些日子里,也尝试了不同的技术领域移动开发,如 React Native 乃至于原生的 Android 开发,又或者是使用 Serverless 开发的后台应用。尽管仍遇到一些大大小小的挑战,这样一来,也有一些新的收获。不免还是觉得自己还是幸运的,不是在四年的工作里重复一年的工作经验。 很遗憾的是,在今日的我看来,仍旧是两年前的那个观点:全栈是未来。这次并不是因为大公司全职业,小公司全栈,而是技术的门槛越来越低——人工智能 API 化、后端无服务器、跨平台应用。 跨平台移动应用 --- 跨平台,一种特别有意思的技术,它在底层上个隔离了不同系统,因此也在某种程度降低了开发难度。 也因此能支撑越来越多的开发人员,更容易走上全栈的道路。它可以是使用同一种语言,如使用 JavaScript 可以开发前端、后端、移动端的应用;它也可以是使用某一特定的技术栈,如 React 来开发不同类型的应用。 过去,我们可以使用 jQuery 来开发前端 UI 界面,使用 jQuery Mobile 来开发混合移动应用。后来,当这样的移动应用性能不行时,又出现了 React Native、NativeScript、Flutter 等等。但是不论是使用哪种跨平台技术,它都意味着在这个领域,技术的门槛已经越来越低了——当然了,位于底层的开发人员,仍然有着相当的门槛。对于一般的跨平台移动应用,仍然面临着不少的兼容性、安装量和崩溃问题。 更进一步的,还有最近两年大火的微信小程序——它可以让开发人员,快速的开发**轻量级**的跨平台应用。 后端无服务器 --- 对于中小公司来说,小程序可以说是一种非常 MVP 的方式。 同样的,适用于后端快速开发的 Serverless 技术,也是一种快速的验证方式。而,它与一般的 MVP 不同的是:它可以自动扩容,能快速满足大量用户访问的需求。 Serverless 技术见证的是 DevOps 技术的不断提升,运维的门槛不断的降低。从过去的手动部署,到各种类型的自动部署,再到各式各样的虚拟化技术的使用。直到今天,使用 Serverless 技术,就可以直接将函数部署到服务器上,直接运行。 又诸如适用于小程序的 Wafer2,也是一个符合国情的,其是基于腾讯云强大的 IaaS 能力搭建了一个 PaaS 小程序解决方案,用户只需要开通,即可使用开发者工具上传、部署、调试小程序后端代码,无需了解服务器运维、数据库部署搭建即可使用。 有兴趣的读者可以访问:[https://serverless.ink](https://serverless.ink) 了解更多 Serverless 技术相关的实践。 人工智能 API 化 --- 在上次,我使用 Serverless + 小程序开发 “代码协作” 应用的时候,引入了对图片进行文本识别,以实现代码识别的功能。计算机视觉作为人工智能的一部分,它在使用上与传统 API 并没有太大的区别。并且,在这一领域已经有相当多成熟的开源软件,如 Tensorflow。 而,我在使用 AWS Rekognition 作为图片识别引擎时,我并不需要过多关注底层的细节。而当我在实现一些简单的推荐系统时,我也只需要在现有的算法上做一些微调。而随着 AI 的 API 不断的丰富,也就同上面的服务端与大前端一样,门槛会不断的降低。这时,如果使用自己的服务器和别人的服务没有太大的区别,那么使用别人的服务反而早期成本更低。 值得注意的是,人工智能时代**最值钱的不是算法,而是数据**。 结论 --- 尽管多种领域开发的门槛越来越低,但是在关键时候还是要掌握自己的核心技术。 末了,顺便推荐一下我去年写的《全栈应用开发:精益实践》

结束了五一国庆的八天假期后,便开始着手为微信小程序《代码协作》制作一个图片识别代码的功能。这个需求的主要来源是,在有的公司、组织、团队上,代码是不能直接拷贝出来的。但是,拍照是允许的。为了协作方便,一般会拍照在微信群里讨论。对于我而言,因此这样一个真实需求的存在,我便想试试能不能做这样的一个功能。

之前做过一个 Angular 下的路由重用,即记录 SPA 应用的上一个页面的内容。最开始的时候,并没有考虑 Lazyload,因此实现起来就比较简单: ``` export class SimpleReuseStrategy implements RouteReuseStrategy { public static handlers: { [key: string]: DetachedRouteHandle } = {}; public shouldDetach(route: ActivatedRouteSnapshot): boolean { return true; } public store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void { SimpleReuseStrategy.handlers[route.routeConfig.path] = handle; } public shouldAttach(route: ActivatedRouteSnapshot): boolean { return !!route.routeConfig && !!SimpleReuseStrategy.handlers[route.routeConfig.path]; } public retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle { if (!route.routeConfig) { return null; } return SimpleReuseStrategy.handlers[route.routeConfig.path]; } public shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { return future.routeConfig === curr.routeConfig; } } ``` 后来项目中添加了 Lazyload,因此便添加了一个 reusePath 的参数来控制是否缓存,最后实现如下: ``` export class SimpleReuseStrategy implements RouteReuseStrategy { public static handlers: { [key: string]: DetachedRouteHandle } = {}; public shouldDetach(route: ActivatedRouteSnapshot): boolean { if (!!route.data && !!route.data.reusePath) { return true; } return false; } /** 当路由离开时会触发。按reusePath作为key存储路由快照&组件当前实例对象 */ public store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void { SimpleReuseStrategy.handlers[route.data.reusePath] = handle; } /** 若 path 在缓存中有的都认为允许还原路由 */ public shouldAttach(route: ActivatedRouteSnapshot): boolean { if (!!route.data.reusePath && !!SimpleReuseStrategy.handlers[route.data.reusePath]) { return true; } return false; } /** 从缓存中获取快照,若无则返回nul */ public retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle { if (!!route.data && !!route.data.reusePath) { return SimpleReuseStrategy.handlers[route.data.reusePath]; } return null; } /** 进入路由触发,判断是否同一路由 */ public shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { if (future.data && curr.data && future.data.reusePath && curr.data.reusePath) { return future.data.reusePath === curr.data.reusePath; } return false; } } ``` 为了保存路由,需要单独额外的 在 Routing 里 添加一个 key : ``` const routes: Routes = [ { data: { reusePath: 'hello/hello' }, path: 'hello/hello', loadChildren: './hello/hello.module#CheckinModule' } ] ```

最近几天,博客所使用的 EC2 服务器一直不稳定。最开始,我以为是又双双叕叕叕敠被攻击了。

前几天,在使用 Serverless 技术做一个微信小程序,期间遇到一个需要用户登录的接口。于是,便使用 Serverless 简单的写了一个中转 API。

想来,我已经用 TypeScript 已经有一段时间了,它可以算得上是前端领域的一门 “平淡生活” 的语言。

平淡生活,我的意思是:生活可以从此多一点乐趣——毕竟 bug 少了一些。

还在担心后端 API 变更对前端的影响?快来使用 mest

Feeds

RSS / Atom

最近文章

关于作者

Phodal Huang

Engineer, Consultant, Writer, Designer

ThoughtWorks 技术专家

工程师 / 咨询师 / 作家 / 设计学徒

开源深度爱好者

出版有《前端架构:从入门到微前端》、《自己动手设计物联网》、《全栈应用开发:精益实践》

联系我: h@phodal.com

微信公众号: 最新技术分享

存档

分类

标签

作者