matianming

matianming 查看完整档案

杭州编辑  |  填写毕业院校阿里巴巴  |  前端开发专家 编辑 github.com/alex-mm/blog/blob/master/README.md 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

matianming 收藏了文章 · 2018-05-23

页面可视化搭建工具前生今世

原文地址: https://github.com/CntChen/cn...

背景

引子

页面可视化搭建, 是一个历久弥新的话题. 更广义上讲, 页面是 GUI 的一部分, GUI 的拖拉生成在各种开发工具上很常见, 如 Android Studio, Xcode, Visual Studio 等. 前端页面早在十几年前就能用 Dreamweaver, Frontpage 等工具可视化搭建出来.

Dreamweaver 操作页面示例:

dreamweaver_demo

但是现在已经很少人使用 Dreamweaver 了, 其主要原因是页面承载的内容已经和页面源码分离, 由后端接口返回再渲染到页面, 静态页面网站无法承载大量的动态内容.

Dreamweaver 死了, 但是页面可视化搭建工具依然广泛需要和使用, 所以这个话题依然值得探讨.

文章内容

  • 页面构成和页面组件化.
  • 页面可视化搭建工具的必要性.
  • 页面可视化搭建工具的区分维度.
  • 业界的实践实例.

页面

页面是 HTML / DOM

页面可视化搭建的操作对象是页面. 页面是一份 HTML 文档, 不管是静态页面还是动态渲染出来的页面, 在页面上看到的内容, 都是 HTML 文档的一部分.

对 HTML 文档的实例化和操作, 通过文档对象模型(DOM)来实现, 也可以说页面是一个 DOM. 本文没有严格区分 HTML 和 DOM 这两个概念, 以下行文都用 HTML 这个概念.

HTML 使用一种树形结构来表示页面, 树的每个节点为一个页面元素或文本节点, 一个页面元素可以包含多个页面元素节点或文本节点. 页面元素通常称为标签, 页面元素类型由 HTML 规范定义.

HTML 结构示例:

pic_htmltree

https://www.w3schools.com/js/...

页面是 HTML Tree + Data

从前端开发的角度, 可以认为页面是由 HTML TreeData 组成, HTML Tree 是页面元素的树形结构, Data 是页面元素的属性或文本节点. 下图中蓝色框所示的节点可以认为是数据.

pic_htmltree_data

为什么从前端开发角度会说页面是 HTML Tree + Data? 举一个常见场景来说明: 在开发新页面时, 我们是可以复制已有页面(好吧, 我就是这样的前端工程师), 然后只修改页面 HTML, 或者只修改数据, 或同时修改 HTML 和数据, 从而完成新页面的开发.

静态页面和动态逻辑页面

上一节说页面的由 HTML TreeData 组成, 讨论的是静态页面.

what_is_static_page

浏览器请求静态页面, 网络返回的 HTML 源码就是页面渲染完成后的 HTML. 静态页面的源码和页面渲染结果一致:

static_page_render_result

当下, 前端页面更多的是有动态逻辑的页面, 在页面中引入和使用动态脚本(Javascript)对页面进行修改和控制.

what_is_dynamic_page

浏览器请求动态逻辑页面, 网络返回的 HTML 源码与页面渲染完成后的 HTML 有差异. 动态逻辑页面的源码和渲染结果有差异:

dynamic_logic_page_render_result

页面组件化

页面渲染后是一棵 HTML 元素构成的树, 页面的可编辑粒度为 HTML 规范定义的 HTML 元素.
使用 Web Components 组合 HTML 元素, 实现了功能封装和可复用的页面组件. 在流行的前端框架中, 都提供了组件化的功能, 从前端框架的视角看, 页面是由组件树组成. 这些组件内部维护自身的 HTML 元素结构、样式和功能逻辑, 并通过组件的 props 获取外部传入的数据, 实现了功能封装和复用.

Vue 组件树示例:

vue_components

https://vuejs.org/v2/guide/#C...

并没有讨论 CSS

在以上的章节中, 我们并没有讨论决定页面样式的 CSS. 因为借助 Javascript 的动态逻辑, CSS 可以归入到 Data 的范围: 通过对页面元素 style attribute 的修改, 或将 CSS 属性动态添加到 <style> 标签中, 可以实现对页面元素样式的修改.

页面可视化搭建

有了对页面组成的认知基础,可以对页面可视化搭建有更多的讨论: 页面可视化搭建是什么? 为什么需要?

是什么

如前文所阐述, 动态逻辑页面分解为 HTML Tree, DataDynamic Logic. 前端开发工程师开发前端页面的过程, 本质上是用编程工具(IDE)对页面的 HTML Tree, DataDynamic Logic 进行增删和修改.

页面可视化搭建, 是用可视化交互的方式对页面的 HTML Tree, DataDynamic Logic 进行增删和修改, 从而实现页面的生成. 页面可视化搭建工具是实现页面可视化编辑的软件工具.

用页面可视化搭建工具来搭建页面与前端工程师在页面上搬砖, 都是搭建页面, 区别在于实现页面搭建的方式. 做个简单对比:

差异点编程开发页面可视化搭建页面
技能要求需要编程基础可以没有编程基础
操作方式在代码编辑器中编写代码在可视化搭建工具中拖拉/填表/编写代码

为什么需要

任何工具的存在都是更高效地解决问题. 页面可视化搭建工具, 用于解决页面生成的效率问题.
可能前端工程师会觉得最有效率的页面生成方式是打代码, 但有搭建页面需求的不只是前端工程师. 而可视化页面搭建工具, 恰恰是面向"就缺一个前端工程师"的人员, 用于提升他们生成页面的效率.
我们可以从一些使用场景来窥探页面可视化搭建工具的应用场合.

页面小白做 H5

页面小白不需要任何页面相关的知识, 不需要了解 HTML/JS/CSS 这些概念, 只要像使用 Word 一样在 H5 制作工具上操作, 就可以做出一个挺漂亮的页面. H5 制作工具很多, 其中 百度H5 做很好不错.

如: 小陈女票要生日了, 小陈为女票做了一个有创意的生日祝福页面:

baidu_h5

营销活动页面搭建

大多数互联网公司需要做许多的活动页面来承载运营业务. 运营活动页面的特点是: 页面功能大同小异、需求急、时间紧、下线快、研发性很比低. 前端工程师无法持续开发无穷无尽的活动页面, 需要采用活动页面可视化搭建工具, 由运营人员/产品人员直接生成活动页面. 研发人员的工作转变为提供满足活动页面业务需要的活动模板.

如: 抽奖活动页面的可视化搭建:

activity_demo

中后台系统开发

在公司内部, 需要做许多的中后台支持系统, 这些系统的管理端一般用 web 页面承载. 那么问题来了, 中后台系统的前端工程, 怎么保障可用性、可维护性和页面呈现一致性? 这些系统与后台逻辑强关联, 一般由后台开发人员开发; 后台开发人员写代码逻辑是没有问题的, 但是其前端开发能力相对较弱. 所以需要增强他们开发前端页面的能力, 前端开发能力由前端服务化提供.

前端服务化的第一种方式是提供一套组件库, 如 饿了么的 Element.
组件库一般由前端开发人员封装成模板工程, 模板工程提供公共样式和函数库, 并对编写的代码做校验和约束, 一定程度上降低了前端开发难度, 统一后台人员代码风格. 此时后台开发人员的开发方式为: 在代码中用组件拼凑页面, 然后写代码逻辑.

前端服务化的第二种方式, 是提供页面可视化组装系统, 这个系统输出组装后的前端工程源码. 这样的系统比提供组件库和模板工程的方式走得更远: 通过可视化生成模板工程, 后台开发人员不需要在代码中拼凑前端页面, 不需要关注前端组件, 只需要编写代码逻辑.
这种方式可以参考阿里的 ice.

阿里 ice 示例:

iceworks_demo

前端服务化的终极方式, 是直接提供一个开发的 IDE, 将动态逻辑的书写也在 IDE 中完成.
美团外卖前端可视化界面组装平台 —— 乐高, 前端服务化——页面搭建工具的死与生.

美团乐高示例:

lego_demo

前端服务化

更加广泛来说, 为页面小白/运营人员/产品人员提供的页面可视化生成工具, 也是赋予以上人员前端开发的能力. 所以页面可视化搭建, 本质上是前端服务化的一部分. 前端服务化总结, 可以看百度的 前端即服务-通向零成本开发之路.

页面可视化搭建工具区分维度

有了前文对页面的基础认知, 终于进入了本文的正题 -- 页面可视化搭建工具.
前面已经零星讨论过页面可视化搭建工具的定义, 再总结一下: 页面可视化搭建, 是指用可视化交互的方式(对比编写代码的方式), 实现页面的修改或生成; 页面可视化搭建工具, 增强了使用者的前端开发能力, 提升了使用者修改或生成页面的效率.

思考一个更具体的问题: 当我们讨论页面可视化搭建工具时, 怎么进行描述和讨论? 换个角度提问题: 可以从什么维度对页面可视化搭建工具进行描述和区分?

页面可视化搭建工具的区分维度包括:

  • 系统功能
  • 面向客群
  • 编辑自由度

下文会对页面可视化搭建工具的区分维度做介绍, 并会对每个区分维度提供示例(这些示例不会展开讨论, 且在不同维度下会多次使用同个示例).

系统功能

页面可视化搭建工具的系统功能是指该工具在解决特定页面可视化搭建问题上提供的核心能力.
页面是由HTML Tree, DataDynamic Logic 三部分组成, 一个页面可视化搭建工具提供的能力是编辑页面组成部分之一或多部分. 对基于组件的页面, 其可编辑单元为组件, 此时采用 Component Tree 概念取代 HTML Tree.

system_function_category

HTML Tree 编辑

这类页面搭建工具专注于可视化地编辑页面 HTML Tree 部分, 一般可以对页面做自由度较高的编辑.
其关键功能在于高自由度: 几乎可以编辑页面可见的所有元素, 能自由修改页面结构、页面元素样式和页面数据, 采用类似 Word, Photoshop 的可视化编辑方式.
这类工具一般只适用于生成逻辑比较简单的页面, 其中原因后续会讲.
常说的 H5 制作工具就是指这类工具.

如: 百度H5iH5

Component Tree 编辑

这类页面搭建工具针对组件化的页面, 主要实现 Component Tree 的可视化编辑. 其核心功能在于页面布局设计: 在 UI 组件列表中选择合适的组件, 通过拖拉的方式将组件嵌入到页面中, 生成带布局和样式的页面.

如: ice 阿里飞冰、vue-layout

vue-layout 示例:

vue_layout_demo

https://jaweii.github.io/Vue-...

页面 Data 编辑

这类页面搭建工具专注于可视化地编辑页面的 Data 部分, 如图片URL、按钮文本、按钮跳转链接等.
这类搭建工具主要针对 HTML Tree 比较固定、能承载复杂业务逻辑的页面. HTML Tree 固定的常见方式是页面组件化, 只需修改页面组件的 Data 就能快速地生成页面.
其核心功能在于快速搭建承载业务逻辑的页面.
通常营销活动页面就采用这种方式来可视化搭建.

如: 阿里云凤蝶、开源的 pipeline

阿里云凤蝶示例:

yunfengdie_demo

Dynamic Logic 编辑

这类页面搭建工具支持在界面上输入逻辑代码, 实现页面 Dynamic Logic 编辑, 如后台接口请求逻辑, 业务判断逻辑等.
这些逻辑代码需要有合适的插入点, 一般在事件钩子中提供插入点, 如页面 onload、网络请求状态变更、按钮事件、数据变更等.
做到可以支持编辑 Dynamic Logic 是超牛逼的事情, 这类工具对页面的理解最深入, 对开发者的技术能力、前端架构能力和开发能力都要求很高.

如: 前端服务化——页面搭建工具的死与生

系统功能组合

还有其他系统功能的组合, 可以综合上面的典型类别来做讨论.

面向客群

页面可视化搭建工具的面向客群是指工具的使用客群. 不同的使用客群, 其对页面技术的认知程度、搭建页面的诉求有所不同, 所以可以从工具的面向客群来区分不同工具.

custom_category

前端小白

前端小白是不具有前端知识的人群, 他们对页面可视化搭建工具的诉求是交互性越高越好. 最适合他们的工具是像 Word, Powerpoint, Photoshop 等具有丰富交互功能, 且所见即所得的页面搭建工具.
同时他们也不关心页面最后用什么方式托管到互联网上, 页面编辑完成后要帮他们在公网上托管页面, 并提供页面链接, 方便前端小白将页面发给自己的女朋友.

如页面界的 Photoshop:

ih5_demo

https://www.ih5.cn

运营/产品

运营、产品人员没有开发人员页面开发、逻辑编程的能力, 他们的诉求是可以快速搭建活动、产品页面. 活动、产品页面是承载着业务逻辑的: 如包含领取优惠券功能、背景音乐播放功能、产品购买功能等. 运营、产品对页面可视化搭建的另一个诉求是“快速”: 一天好几个活动, 怎么快怎么来.
面向运营、产品的可视化搭建工具, 需要将页面的逻辑功能封装在页面区块内, 支持通过点击来选择区块, 然后在表单中编辑区块所需数据, 只对页面进行少量编辑就完成业务页面搭建.
如领取优惠券的页面, 运营、产品只要在表单中填入优惠券的 ID, 然后就快速生成领取该优惠券的页面, 不需要关心优惠券在页面上如何展示和被领取的具体逻辑.

如, 开源项目 pipeline:

demo

中后台开发人员

中后台开发人员具有逻辑编程能力, 但其前端开发能力比较弱. 中后台开发人员的诉求是, 在开发中后台系统的 Web 管理端时, 不需要进行重度的前端页面结构和样式开发, 可以专注在逻辑和数据处理上.
这要求页面可视化搭建工具提供页面搭建的区块, 对区块进行可视化组合来输出一个基本的前端页面; 并在页面搭建工具上提供业务逻辑编写的输入点, 或将基本前端页面源码导出到 IDE 中供中后台开发人员进行业务逻辑的开发.

如: ice 阿里飞冰

前端工程师

要啥页面可视化搭建工具, 抓起键盘就开始干.

just_do_it

编辑自由度

页面可视化搭建工具的编辑自由度, 是指页面可编辑单元的粒度. 前端页面的可编辑单元为 HTML 元素; 从前端页面组件化的角度, 页面可编辑单元为组件.
不同的编辑自由度的选择, 是可视化搭建工具在不同业务场景下编辑自由度与编辑效率的平衡.

degree_of_freedom_category

编辑自由度为 HTML 元素(左)与自由度为组件(右)的示例:

edit_free_degree

编辑自由度为 HTML 元素

编辑自由度为 HTML 元素的页面搭建工具有以下特点: 可编辑的元素丰富、页面结构灵活、可视化编辑效率较低、业务逻辑封装度较低.
这类工具的可编辑单元为 HTML 元素, 可以编辑元素的文本、样式和行为, 可编辑的元素较丰富; 并且可以组合各种 HTML 元素到页面中, 生成的页面结构灵活; 从生成页面的角度, 编辑出一个页面需要从基本的 HTML 元素开始搭建, 可视化编辑的工作量较大; 一个业务功能的实现, 通常需要渲染多个 HTML 元素, 而这类工具可以自由增删业务所需的 HTML 元素, 这导致无法固定地承载业务功能, 所以这类编辑工具生成的页面, 业务逻辑封装程度较低.

如: iH5vvveb

vvveb 示例:
vvveb_demo

http://www.vvveb.com/vvvebjs/...

编辑自由度为前端框架组件

编辑自由度为前端框架组件的页面搭建工具有以下特点: 可编辑的元素依赖搭建工具提供的组件, 可视化编辑效率较高、业务逻辑封装度较高.
这类工具的最小可编辑单元为前端框架的组件, 这些组件需要开发并导入到页面可视化搭建工具中; 组件的渲染结果包含了多个 HTML 元素, 所以从生成页面的角度, 编辑出一个页面只需要组合组件, 可以较快速完成页面的生成; 组件本身承载了特定的业务功能, 所以这类编辑器生成的页面, 业务逻辑封装程度较高.

如: Vue-Layout

vue-layout 示例:

vue_layout_demo

不嵌套的前端框架组件

移动端的页面, 常用的布局策略是: 宽度铺满, 高度滚动. 如果前端框架组件都设置为铺满宽度, 页面展示时组件只需在浏览器垂直方向上顺序排列, 则组件组合时候不需要嵌套, 所有组件互为兄弟节点. 这种铺满宽度的组件, 非常适合搭建移动端页面的场景: 在承载页面逻辑的同时, 使得页面的编辑更加简单, 使用者只需要处理组件的顺序, 不需要处理组件的嵌套. 组件的嵌套需要重点解决组件数据流和组件布局适配.

如: 阿里云凤蝶pipeline

pipeline 示例:
pipeline_demo

https://page-pipepline.github...

理想的页面可视化搭建框架

页面可视化搭建工具, 需要对页面做一些定义和约定, 在可视化搭建时遵循工具定义和约定来编辑页面. 更全面讨论页面可视化搭建工具时, 不只是关注工具本身的功能, 还需要关注工具的依赖和约束, 如页面可视化搭建工具的组件化方式、模板组织方式、编辑功能实现方式等. 从工具开发的角度说, 页面可视化搭建工具是需要架构设计的, 不同工具的区分, 其实是不同的页面可视化搭建框架间的差异.

在互联网公司中, 广泛运用页面可视化搭建工具来支持运营活动页面的生成, 本章我们只探讨运营页面搭建工具的理想框架.

页面可视化搭建框架的核心是实现页面的可视化编辑. 运营页面搭建工具声明配置数据并提供配置表单, 通过对配置表单的数据填充, 实现基于模板的页面生成. 如图所示:

visual_edit

可视化编辑

配置数据

对页面的可编辑部分, 需要准确描述可编辑部分所需的配置数据; 配置数据是异构的, 不同页面、不同区块的配置数据各不相同. 所以需要对不同页面、不同区块定义各自配置数据的数据结构和字段类型.
最理想的配置数据格式为 JSON, 因为其格式灵活, 前端友好; 最理想的配置数据描述是 JSON Schema, 因为其支持表单动态生成和数据校验.

编辑表单生成

采用 JSON Schema, 容易生成配置表单, 只要按照 JSON Schema 对 JSON 数据的描述, 可以动态渲染出配置表单. 并且可以采用 JSON Schema 对编辑后的数据做格式校验, 避免编辑错误.

如配置表单自动生成工具 json-editor:

json_editor_demo

组件化

组件是对 HTML 元素、元素布局和样式、业务逻辑的封装, 通过组件化的方式, 将页面的搭建转化为对组件的组合, 大大减低了运营页面生成的编辑工作量, 实现承载业务逻辑的运营页面的快速搭建.

如 pipeline 的页面组件化:

component_demo

模板

模板是带有默认数据的页面; 对于组件化的页面, 模板是从组件库中选取部分组件, 并带有各个组件的默认数据.
采用模板生成页面, 只需对模板进行少量编辑即可实现页面快速生成.

与编辑系统解偶

编辑系统和组件解偶,组件只需要遵循编辑系统的组织约定, 其具体开发过程和承载的逻辑与编辑系统无关, 支持自由拓展页面组件.
编辑系统与模板采用的前端框架解偶, 在遵循编辑系统约定下, 可以选择不同的前端框架.

理想的运营页面可视化搭建框架

  • 采用 JSON Schema 声明配置数据, 配置表单自动生成.
  • 采用组件化和页面模板实现页面生成效率的提升.
  • 编辑系统与组件、前端框架、模板解耦.
  • 在遵循编辑系统约定下, 前端框架可以自由选择, 组件可以自由拓展.

页面可视化搭建工具举例

列举一些页面可视化搭工具, 并附带少量点评.

阿里云凤蝶

移动建站平台
  • 支持页面 Data 编辑, 面向运营/产品人员, 编辑自由度为无嵌套的组件.
  • 目前制作运营、活动页面功能上最好的工具.
  • 提供页面搭建的模板, 并支持自定义模板.
  • 配置表单基于 Schema 生成, 配置表单操作功能完善.

ice 阿里飞冰

飞冰 - 让前端开发简单而友好
  • 支持 Component Tree 编辑, 面向中后台开发人员, 编辑自由度为无嵌套的组件.
  • 使用"物料-区块", 非前端开发人员可以快速搭建出可用、符合规范的页面.
  • 页面以源码方式输出.
  • 前端服务化的一种方式.

百度H5

创意,绝不雷同
  • 支持 HTML Tree 编辑, 面向前端小白, 编辑自由度为 HTML 元素.
  • 做 H5 的好工具, 功能上很强大, 对动画的编辑功能做到细致.

美团外卖前端可视化界面组装平台 —— 乐高

  • 支持 Dynamic Logic 编辑, 面向中后台开发人员, 编辑自由度为可嵌套的组件.
  • 前端服务化的一种方式.
  • 在美团内部支持了许多业务页面, 没有公网服务, 了解该系统只能通过其介绍文章.

esview

Drag vue dynamic components to build your page,generate vue code.

开源项目, 模仿美团点评的乐高.

  • 完整的可视化页面搭建框架, 面向中后台开发人员.
  • 页面布局结果看起来比较乱, 自定义组件写法比较诡异; 没有融合业务逻辑, 不支持在框架中写页面的代码逻辑.

gaea-editor

Design websites in your browser

开源项目.

  • 支持 Component Tree 编辑, 面向中后台开发人员, 编辑自由度为可嵌套的组件.
  • 页面的拖拉生成, 实现得很完整.
  • 用于页面设计, 所以偏向页面元素的样式控制.
  • 技术文章对可视化搭建工具的理解深刻: 可视化在线编辑器架构设计.

Vue-Layout

基于UI组件的Vue可视化布局、生成.vue代码的工具。

开源项目.

  • 支持 Component Tree 编辑, 面向中后台开发人员, 编辑自由度为可嵌套的组件.
  • 工具的使用体验效果不错.

gen

根据接口生成页面,减少重复性工作
  • 开源项目, 用起来感觉不错.
  • 系统中有好几个概念, 开始比较难上手.

其他

业界实践

列举一些业界在页面可视化搭工具上的实践, 并附带少量点评.

前端服务化——页面搭建工具的死与生

  • 支持 Dynamic Logic 的页面可视化搭建 IDE.
  • 讲解了页面可视化搭建框架支持 Dynamic Logic 的可行性和设计架构.
  • 作者在前端框架和 IDE 方面写了好几篇文章, 很深刻.

腾讯IMWeb: 积木系统,将运营系统做到极致

2015年的文章! 完全说到点上.

  • 简单易用的、可视化的可编辑页面.
  • 通用的、简便地组件接入机制.
  • 组件:开发过程和系统无关, 逻辑和系统无关.

美团外卖前端可视化界面组装平台 —— 乐高

  • 把系统架构将得很清楚, 有借鉴意义.
  • 对页面组成做了分析, 阐述了可视化配置的原理.

前端即服务-通向零成本开发之路

百度的前端服务化实践, 都在这一篇.

可视化在线编辑器架构设计

  • 可视化在线编辑器属于前端开发引擎, 前端进入了前端工业时代.
  • 深入讨论了组件数据流.

百度外卖如何做到前端开发配置化

  • PPT 将原理和架构讲得很清楚.
  • 使用流程图很清晰.
  • 项目开源了 -- block, 试用起来功能比较简陋.

转转运营活动高效开发有哪些秘诀

基于组件的页面生成系统-魔方, 采用 npm 管理组件.

QQ会员: 如何保证H5页面高质量低成本快速生成

内部 ET 平台, 包含活动管理的其他功能.

esview -- 这可能是目前最好的vue代码生成工具

总结

  • 页面由 HTML Tree, Data, Dynamic Login 组成.
  • 页面可视化搭建工具用于提升各类人员的页面搭建效率.
  • 页面可视化搭建其实是前端服务化的方式.
  • 页面可视化搭建工具需要平衡自由度和效率.
  • 组件和模板是页面可视化搭建框架的核心.

全文结束, 本文对页面可视化搭建思考和讨论可能还不够完整, 欢迎讨论和补充.

后记: 终于写完了, 历时估计一个月! 写这篇文章的初衷是给我造的页面可视化搭建框架 -- pipeline 写背景, 但思考的点比较多, 所以就独立写了一篇文章. Pipeline 基本对标阿里的云凤蝶, 已经开源, 相关文章还在撰写中. 赶紧点击 Demo 体验吧.

EOF

查看原文

matianming 回答了问题 · 2017-06-13

sublime eslintautofix的文件配置谁知道啊,windows的。。。

关注 2 回答 1

matianming 发布了文章 · 2017-01-23

Icon 进化史

“南方古猿”之 png sprite

看到上面这张图,相信好多资深前端会感到很亲切。

早期为了减少资源的请求,人们想到了将小的 png 图片合并到一张图上,然后根据 background-position 来显示不同的图片。

早期还有靠人肉来测量坐标,随着构建工具的发展,我们可以用一些插件,如 grunt-spritesmithgulp.spritesmith 等。它可以帮助我们自动合成,并生成好 css, 位置都计算好的。

那么使用 png 图片这种方式它的优点就是兼容性好。但是一旦开发多了,它的不便变体现出来了,换颜色、改大小、透明度、多倍屏等等。

所以对于这种方式我们只能缅怀。

“能人”之 Iconfont

于是人们又想出了用字体文件取代图片文件:Iconfont。

虽然早期制作或寻找合适字体比较麻烦,但随着各种字体库的网站出现,如: http://www.iconfont.cn ,那都不是事了。再加上 css 的自定义字体的兼容性非常好,Iconfont 迅速开始流行起来。以这个站为例,大概看下使用方法:

  1. 在 Iconfont 中创建自己的项目,将需要使用的图标添加到自己的项目中。

  2. 复制出 Unicode 或 Font class

  3. 全部代码如下

@font-face {
  font-family: 'iconfont';  /* project id 38792 */
  src: url('//at.alicdn.com/t/font_1444792316_9706304.eot');
  src: url('//at.alicdn.com/t/font_1444792316_9706304.eot?#iefix') format('embedded-opentype'),
  url('//at.alicdn.com/t/font_1444792316_9706304.woff') format('woff'),
  url('//at.alicdn.com/t/font_1444792316_9706304.ttf') format('truetype'),
  url('//at.alicdn.com/t/font_1444792316_9706304.svg#iconfont') format('svg');
}
.iconfont {
  font-family:"iconfont" !important;
  font-size:16px;
  font-style:normal;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.icon-tishi:before { content: "\e600"; }
<i class="iconfont icon-tishi"></i>

这里有demo

在实际开发中,我们会把常用的一些图标封装成组件,直接使用。像这样

<i class="kuma-icon kuma-icon-success"></i>

Iconfont 用起来挺方便的,而且兼容性也十分的好,大小、颜色可随意改变。

但它仍有缺陷:

  1. 字体需要加载资源

  2. 有时候可能会出现锯齿

  3. 只能被渲染成单色或者css3的渐变色

所以我们要继续进化。

“直立人”之 svg symbol

使用 svg ,这里所谓的进化并不是 svg 本身的进化,因为 svg 并不晚于 Iconfont。只是环境(兼容性)的原因导致它无用武之地。就像最近火的一塌糊涂的 AI, 其实最早在 1956 年就提出了。随着外界因素的进化,IE6/7/8 的淘汰, android 4.x 的开始,svg 的机会变到来了。先看下兼容性:
image

svg 的使用方式:

  • 保存成文件

    • 需要请求加载资源

  • inline 方式

    • 在 html 一坨坨,很麻烦

  • symbol

    • 适合我们做组件

The <symbol> element is used to define graphical template objects which can be instantiated by a <use> element.

通过 <symbol> 定义的 svg 模板,我们可以使用 <use> 来加载它。

<svg width="120" height="140">
<!-- symbol definition  NEVER draw -->
<symbol id="sym01" viewBox="0 0 150 110">
  <circle cx="50" cy="50" r="40" stroke-width="8"
      stroke="red" fill="red"/>
  <circle cx="90" cy="60" r="40" stroke-width="8"
      stroke="green" fill="white"/>
</symbol>

<!-- actual drawing by "use" element -->
<use xlink:href="#sym01"
     x="0" y="0" width="100" height="50"/>
</svg>

那么 <symbol> 是怎么来的呢?

同样,在这个构建工具十分发达的时刻。
最开始我们使用了 gulp-svg-symbols,它可以将指定目录中的 svg 自动合并到一个 svg 文件中,文件里包括了所有 icon 的 symbol 模板,然后再将这个模板将其隐藏放到 index.html 中。

但是这个库有个坑点是它依赖了一个 Unicode 的包,这个包在国内安装炒鸡慢,于是我们弃用了它,使用了gulp-svgstore

按照这种方式我们成功的开发一 salt-icon 这个组件,里面包括了一些常用的 icon。使用方式:

    <Icon name="success" className="icon-success"/>

这样我们在 mobile 端用 svg 替代了 Iconfont,解决了上述 Iconfont 提到的问题。

但是很快我们就发现,在 index.html 中引入那一坨 symbol 模板是极其恶心的。

随着 webpack 打包的成熟,各种 loader,我们将那一坨 symbol 模板直接打包成一个 salt-icon-source.js 文件,在这个文件中将其 append 到 body 上。

同时发现了上述提到的 iconfont 网站也支持直接导出 symbol 文件。

import React from 'react';
import ReactDOM from 'react-dom';
import IconSource from './svg/salt-icon-symbols.svg';


const WRAPPER_ID = '__SaltIconSymbols__';
const doc = document;
let wrapper = doc.getElementById(WRAPPER_ID);
if (!wrapper) {
  wrapper = doc.createElement('div');
  wrapper.id = WRAPPER_ID;
  wrapper.style.display = 'none';
  doc.body.appendChild(wrapper);
  ReactDOM.render(<IconSource />, wrapper);
}

这样虽然解决了引入模板的那个问题,但是后面又发现的 symbol 在安卓 4.3.x 下无法显示,也就是说 symbol 的兼容性并没有直接使用 svg 好。

然后我们通过使用一个叫 svg4everybody 的库,解决了上述兼容性问题。(它的原理是如果发现不支持 symbol 的,它会通过 xlink:href 拿到 svg 的资源,然后动态创建一个 svg,插入到当前位置)

虽然解决了兼容性的问题,但是我们深深的感觉到了这种方式的不优雅。

讲的这里,可能会有人会有疑问,既然已经有 svg-react-loader 了,为什么不直接 import svg 文件?

业务中使用的图片当然可以直接 import 加载,但一些通过的图标我们希望是能统一起来,做出组件,更方便的使用。

而且我们组件中还会对 svg 处理了它事件不能冒泡的问题,也就是在某些低版本的安卓机上,svg 图标是无法点击的。解决方案有两种:

  • 贴膜,不过这样会导致多一层结构嵌套

  • 去掉事件

svg {
  pointer-events: none;
}

不过,这个问题可以给我带来启示,‘既然已经有 svg-react-loader 了’,那么 svg-loader 里做了什么呢?symbol 的方式或许真的可以淘汰了。

“智人”之 svg

看下 svg-react-loader 的实现
首先有一个模板

  render () {
    const props = this.props;
      return (
        <svg {...props}>
          <%= innerXml %>
        </svg>
      );
    }

然后有一个 sanitize.js ,会对 svg 做一些处理,加上标准的 xml namespace, 把 React 特有的属性 class / for 转化为 className / htmlFor, 把属性名转化为驼峰。

最后根据模板生成这样一段代码

var React = require('react');

module.exports = React.createClass({

    displayName: "Test",

    getDefaultProps () {
        return {"width":"1024","height":"1024","viewBox":"0 0 1024 1024","version":"1.1","xmlns":"http://www.w3.org/2000/svg"};
    },

    render () {
        var props = this.props;

        return <svg {...props}>
          <path d="M512.002047... fill="#272636"/>
        </svg>;
    }
});"

这样的代码我们就可以直接在 react 中直接使用了。

所以我们的组件借助这样的思想,完全弃用了 symbol 模式。

我们先扫描对应的 svg 文件,将其按上面的思路生成一个个单独的 js 文件。
在组件层面可以再封装一层,统一引入,提供一个通用的调用方式,和上面一样。

    <Icon name="success" className="icon-success"/>

更好的是你也可以单独引用每一个文件,减小使用体积。

最后我们憧憬一下,随着 react 15.x 的发布,react 对 svg 的支持越来越好了,随着 IE 8 也即将被遗弃,我们的 PC 端也有望从 Iconfont 转换到 svg 了。

查看原文

赞 8 收藏 13 评论 0

matianming 赞了文章 · 2017-01-23

一个靠谱的前端开源项目需要什么?

0. 前言

写前端代码一段时间之后,你可能会萌生做一个开源项目的想法,一方面将自己的好点子分享出去让更多的人受益,另一方面也可以在社区贡献的环境下学到更多的东西从而快速成长。但是开源项目也有开源项目的玩法,一些可能没有注意的点,也许会让你的好点子和许多人失之交臂,在这里笔者以自身经验出发,聊一聊笔者心目中的靠谱的 Github 前端开源项目应该具有什么。当然我们讨论的只是一个项目至少需要什么才是靠谱的。真的想要做好一个项目,需要的比这里要讲的多得多。

限于篇幅,本文很多点都是点到为止,文章中有大量链接供同学进一步了解掌握相关知识。

1. 文档

文档是你的潜在用户了解项目的第一道窗口,文档书写的好坏直接决定了项目的易用性和用户粘性。

1.1 README

我们首先要提的是 README 文档,这是每个开源项目所必须的,README 文档会默认展示在项目首页,可以算作是整个项目的门面。一个靠谱的 README 应该包含以下几部分:

  • 言简意赅的项目介绍:你的项目解决了什么核心问题,有哪些令人心动的特性。
  • 简单的安装和使用指导:用户如何安装你的项目,又如何使用在自己的项目中,你应该想办法让这部分尽量简单,降低接受成本。
  • API 和配置说明:也许你的项目十分强大,支持很多特性,你需要告诉用户如何通过配置和 API 来完成这些事情,在这中间,也许你可以插入一些简单的 demo,帮助用户理解,这部分的好坏直接决定了项目的易用性。
随着你的项目日趋复杂,也许 README 的篇幅已经不能够承载你的 API 说明,这时在项目中单独建立一个 doc 文件夹用来存放文档,也是很多人的选择。
  • 如何贡献:开源的一个很重要的目的是为了让社区中的更多人来帮助你完善你的创意,你需要告诉他们你有哪些部分是他们可以帮的上忙的,你是如何对待 issues 和 pull requests( 后文称 pr) 的。
  • 如何与作者取得联系:有些合作和洽谈可能无法承载在 issue 上,告诉用户除了 issues,还有什么途径可以与作者取得联系。
  • 开源许可协议:既然是一个开源项目,那么开源许可协议必然是不可少的,你的开源许可是 MIT、BSD 还是 ISC、Apache?当然你也需要了解这些开源许可的意义,这里推荐一篇知乎问答

1.2 CONTRIBUTING

上面我们也提到了如何贡献的问题,当贡献需要了解的东西越来越多的时候,我们也习惯于把它单独抽成一份 CONTRIBUTING.md。在贡献文档中应该详细地告诉贡献者,哪些功能点是需要贡献者参与的,如何提 issue 以及 pr。

1.3 LICENSE

除了在 README 中提到遵循的开源协议外,一个靠谱的开源项目还会将该开源协议的内容文档放在自己的项目下方。

2. 代码风格

关于代码风格,每个人可能都会有自己的偏好,这些偏好中有好的,也有坏的,一些不好的代码风格会让其他开发者感到不快,减少了大家对于代码的关注,好在强大的开源社区中同样也有人开源了代码风格规范,这些代码规范都经过了激烈的讨论和充分的修改,为大多数人所认可,需要注意的是代码风格并不只是缩进、空格这一类的事情,它更多地是一种代码习惯,比如何时使用 const,何时使用 let,是否有声明但未使用的变量等等,这些习惯并不影响代码的功能,却可以很大程度上决定代码的可维护性、易读性和降低犯错的机会,同时也让其他开发者对你的项目刮目相看。

2.1 代码风格推荐

2.1.1 Airbnb: https://github.com/airbnb/jav...

Airbnb 的 js 规范应该是目前受认可度最高的代码规范之一,目前在 Github 上已经累加了 36700+ 颗星,包含的领域非常广泛,包括线下时髦的 ES6 和 React JSX 等等,笔者推荐。

2.1.2 idiomatic.js: https://github.com/rwaldron/i...

这是一份有着很长历史的,由一群经验丰富的 js 工程师维护的代码风格规范,同时也十分通俗易懂,另外他也有简体中文的版本。

2.1.3 jQuery:https://contribute.jquery.org...

jQuery 所倡导和使用的代码规范,大量的 jQuery 组件根据这一规范书写,如果你读过 jQuery 的源码,你喜欢他的风格,或者你正在开发一款 jQuery 的插件,那这也是一个不错的选择。

2.1.4 Google:https://google.github.io/styl...

谷歌倡导的代码风格,自 2012 年推出以后已经有很多谷歌的项目遵循这份规范进行编码,喜欢谷歌风格的朋友可以使用。

2.2 LINT

看到这里有人会有疑问,规范虽然好,可是条目太多了,我虽然可以学习,但是全都记住难度太高了。不用担心,你的痛点也是大家的痛点,早已经有工具帮助我们来解决这一问题,而且更棒的是他可以无缝地与你的 IDE 相结合。

在这里我们要推荐 ESLint 这款工具。在不久之前,你还有另一个选择 JSCS,但在最近,JSCS 团队选择与 ESLint 团队进行合并,专注于一款产品 ESLint 的开发,两大大牛团队的合体想必会带给 ESLint 更为强大的生命。

eslint & jscs
图1:JSCS 与 ESLint 已经合并

ESlint 提供了非常丰富的 IDE 集成工具,目前已经支持 Sublime Text 3, Vim, Emacs, Eclipse, TextMate 2, Atom, IDEA 和 Visual Studio Code。具体的插件工具请移步 ESlint 的集成文档。下面我们以 sublime 为例,简单讲一下如何使用这些插件:

  • 首先全局安装 ESLint

    npm install -g eslint
  • 接着通过 Package Control,安装 SublimeLinterSublimeLinter-contrib-eslint
  • 初始化 eslint 配置

    eslint --init

    这里会有一些人机交互,来选择 eslint 的风格,之后 eslint 就会在你的项目下添加对应的依赖,重启 sublime,就可以看到效果了。

lint 演示
图2:eslint sublime 插件演示

我们可以看到,linter 对于不符合规则的行和代码块会标红,将鼠标点击到对应标红的块,会显示报错的原因,比如上图中是因为 (global-require) 规则。有时仅通过提示的错误文案,无法帮你准确理解含义,这时我们还可以借助 eslint 的站点:http://eslint.org/docs/rules/ ,这里有更详细的讲解,你还可以直接搜索对应的规则。

3. 测试

靠人工来保证项目的维护总是不出差错是不靠谱的,人总有健忘和糊涂的时候,尤其是当项目越来越复杂时,一个人甚至可能无法全部了解项目的全部逻辑,这时我们就要依靠测试来保证项目的可靠性,这里的测试包括但不限于,单元功能测试,UI 测试,兼容性测试等等。

3.1 测试体系

一个测试体系大体应该包含以下三部分,这三者功能上互相独立,但合作完成一次测试:

  • 测试运行器(Test runner):主要负责测试的自动化,主要负责调用测试框架和测试代码,自动打开浏览器或者 js 环境,执行测试。
  • 测试框架(Testing Framework):测试框架规定了测试风格和测试的生命周期(lifeCircle hook)。
  • 断言库(Assertion library):每个测试单元是通过一句或者一组断言来组成的,每一句断言声明了对一种功能的描述。例如 expect(window.r).to.be(undefined);

3.2 Test runner

一个优秀的 runner 可以做很多事情,我们以 Google Angular 团队推出的 karma 为例,你可以指定在什么浏览器中,使用什么测试框架,进一步地完成测试框架的配置,以及其他非常多的定制。他的安装和初始化也非常简单:

# Install Karma:
$ npm install karma --save-dev

# Install plugins that your project needs:
$ npm install karma-jasmine karma-chrome-launcher --save-dev

初始化配置文件

$ karma init my.conf.js

Which testing framework do you want to use?
Press tab to list possible options. Enter to move to the next question.
> jasmine

...

Do you want Karma to watch all the files and run the tests on change?
Press tab to list possible options.
> yes

Config file generated at "/Users/vojta/Code/karma/my.conf.js".

然后就可以运行了

# Run Karma:
$ ./node_modules/karma/bin/karma start

当然这只是最初始化的配置,我们还没有写真正的测试,所以这里只是一个空架子。

3.3 测试框架

测试框架有很多:

对于一般的开发者来说,他们都能够满足日常的测试需求,所以主要看你的使用习惯,这里以 mocha 为例来给大家一个大概的印象。

首先安装 mocha

$ npm install -g mocha
$ mkdir test
$ $EDITOR test/test.js

接下来写你的 test

var assert = require('chai').assert;
describe('Array', function() {
  describe('#indexOf()', function () {
    it('should return -1 when the value is not present', function () {
      assert.equal(-1, [1,2,3].indexOf(5));
      assert.equal(-1, [1,2,3].indexOf(0));
    });
  });
});

运行 test,查看结果

mocha test/test.js

当然上面这个测试只能测试 node 下的 js 代码,如果你的代码是前端项目,那么就要借助 phantom/browser 等工具了,同时操作这么多东西很麻烦,这时 karma 的作用的就体现出来了,本文只是大体的介绍,因此不再铺开去讲,详细请查看 karma 的官方文档。

3.4 断言库

断言库也有很多的选择,其中比较有名气的有:

其中,chai 同时支持 BDD 和 TDD 两种测试模式,而 expect.js 具有 IE6+ 级别的浏览器兼容性。

3.4.1 测试模式

有人会问 BDD 和 TDD 都代表了什么?

TDD

TDD(Test-driven development),即 测试驱动开发。即先根据需求写测试,然后再写代码,使代码逐渐符合测试要求,不符合时重构代码满足。这种开发模式适合一些需求非常清晰明确,且不再变更的情况,在一般的开发中,我们还是 BDD 的模式使用的比较多。chai 提供的 assert 部分是经典的 TDD 风格,大致如下
var assert = require('chai').assert
  , foo = 'bar'
  , beverages = { tea: [ 'chai', 'matcha', 'oolong' ] };

assert.typeOf(foo, 'string'); // without optional message
assert.typeOf(foo, 'string', 'foo is a string'); // with optional message
assert.equal(foo, 'bar', 'foo equal `bar`');
assert.lengthOf(foo, 3, 'foo`s value has a length of 3');
assert.lengthOf(beverages.tea, 3, 'beverages has 3 types of tea');

BDD

BDD(Behavior-Driven development),即 行为驱动开发。

通常 BD测试提供了几个方法:

  • describe()
  • it()
  • before()
  • after()
  • beforeEach()
  • afterEach()

通过这些方法描述一种行为,当测试的表现满足行为的预期时,即表示测试通过。

4. 持续集成

持续集成,continuous integration (简称 ci),指的是频繁地(一天多次)将代码集成到主干。

为了保证这种快速迭代的开发方式不出差错,采取的核心措施:代码集成到主干之前,必须通过自动化测试。只要有一个测试用例失败,就不能集成。这种行为虽然不能消除 bug,但有效地帮助我们即时发现错误并改正。关于持续集成的详细流程,大家参考阮老师的 持续集成是什么?。本文则重点介绍已经介入 github,可以帮助到我们的继承工具。

接入到 github 的所有 CI 可以在 https://github.com/integrations 中查看。

4.1 CI: Travis CI/ CircleCI

两者都是接入 Github 的持续集成工具,其核心是通过一个脚本,在代码 commit 的时候自动运行继承脚本,完成测试、构建等任务。以 Travis CI 为例,首先在 github 的 Travis CI 页面将 Travis CI 的 hook 加入到你的项目中。

travis add project
图3:将 Travis CI/CircleCI 集成到你的项目中。

接着在你的项目目录下建立一个 .travis.yml,大致长成这个样子:

language: node_js

sudo: false

notification:
  email:
    - xxx@hotmail.com

node_js:
- 4.0.0

env:
  matrix:
  - TEST_TYPE=test
  - TEST_TYPE=coverage
  - TEST_TYPE=saucelabs

在不指定 .travis.yml 的情况下,travis 会自动执行 npm installnpm test 来进行集成测试。更多的配置可以参考官方的文档。集成通过在两个地方可以有所体现:

  • 一个是 ci 提供的 badge:build status
  • 一个是在 pr 提交时的自动 comment

4.2 跨浏览器集成测试:SAUCELABS & Browser Stack

这两个工具都是提供了多种浏览器环境,包括 pc 端和移动端,然后在多种环境下都是去运行测试脚本,来测试项目的浏览器兼容性。其中 SAUCELABS 对于开源项目完全免费,只需要走他的 Open Source Plan 即可,而 Browser Stack 虽然也提供了 Open Source 的免费计划,但比较麻烦,需要邮件联系,并且在项目 README 中提到其对项目的支持。手动配置这些集成还是比较麻烦的,一般我们都借助 karma 来集成,使用 karma 的 karma-saucelabs-launcherkarma-browserstack-launcher

saucelabs 也提供了很棒的 badge

Sauce Test Status

4.3 代码覆盖率集成 Coveralls

代码覆盖率可以在本地测试里集成,同样也可以在 CI 中集成,通过引入 Coveralls,他可以将你持续集成测试中得到的代码覆盖率结果做一个记录和保存,同时及时的反馈给用户。

Coveralls 也有两个地方可以体现:

  • 一个是 Coveralls 提供的 badge:Test Coverage
  • 一个是在 pr 提交时的自动 comment

coveralls
图4:coveralls 的自动 comment

5. 总结

碍于篇幅有限和行文的目的,文中提供的很多点,只是举了一些例子,点到为止,同时提供了链接,供大家日后仔细研究。作者希望通过此文,让读者了解到一个靠谱的前端开源项目应该具备的东西,这个靠谱不仅是为了让用户用的放心,更是为了开发者在开发时心中有谱。那具备了这些就代表了一个成功的开源项目吗?很遗憾,这只是通往成功的必备条件,一个成功的开源项目还需要开发者更多的经营,最重要的,是一份为用户解决痛点的初衷和持之以恒的决心。

最后

惯例地来宣传一下团队开源的 React PC 组件库 UXCore ,上面提到的点,在我们的组件开发工具中都有体现,欢迎大家一起讨论,也欢迎在我们的 SegmentFault 专题下进行提问讨论。

uxcore

本文作者 eternalsky,始发于团队微信公众号 猿猿相抱 和 segmentFault 专栏 UXCore,转载请保留作者信息。
查看原文

赞 12 收藏 90 评论 0

matianming 发布了文章 · 2016-09-18

ES6 + Webpack + React + Babel 如何在低版本浏览器上愉快的玩耍(下)

回顾

起因:

某天,某测试说:“这个页面在 IE8 下白屏,9也白。。”
某前端开发: 吭哧吭哧。。。一上午的时间就过去了,搞定了。
第二天,某测试说:“IE 又白了。。”
某前端开发: 嘿咻嘿咻。。。谁用的 Object.assign,出来我保证削不屎你。

上篇,我们主要抛出了两个问题,并给出了第一个问题的解决方案。

  1. SCRIPT5007: 无法获取属性 xxx 的值,对象为 null 或未定义,这种情况一般是组件继承后,无法继承到在构造函数里定义的属性或方法,同样类属性或方法也同样无法继承

  2. SCRIPT438: 对象不支持 xxx 属性或方法,这种情况一般是使用了 es6、es7 的高级语法,Object.assignObject.values 等,这种情况在移动端的一些 ‘神机’ 也一样会挂。

本篇将给出第二个问题的解决方案, 并对第一个问题的解决方案有了更新的进展。

文章略长,请耐心看~嘿嘿嘿~

image

正文开始

想要不支持该方法的浏览器支持,无非两种办法

  1. 局部引用,引入一个相同的方法代替,其缺点则是使用起来比较麻烦,每个用到的文件都要去引入。

  2. 全局实现,与之相反的方法是使用 polyfill ,其优点便是使用方便,缺点则是会全局污染,特别是实例方法,涉及到修改其 prototype ,不是你的类,你去修改它原型是不推荐的。

针对这两种办法,提供出以下几种方案,供大家参考

方案一:引入额外的库

拿最常用的 assign 来说,可以这样

import assign from 'object-assign';
assign({}, {});

其实这种也是我们之前的使用方式,缺点就是需要去找到对应的库,比如 Promise 我们可以使用 lie

另一方面一旦有人没有按照这个规则,而直接使用了 Object.assign,那这个人就可能被削。

方案二:全局引入 babel-polyfill

在项目的程序入口

import 'babel-polyfill';

babel 提供了这个 polyfill,有了它,你就可以尽情使用高级方法,包括 Object.values[].includesSetgeneratorPromise 等等。其底层依赖的是 core-js

但是这种方案显然有些暴力, polyfill 构建并 uglify 后的大小为 98k,gzip 后为32.6k,32k 对与移动端还是有点大的。

性能与使用是否方便自己权衡,比如离线包后或也可以接受。

方案三:手动引入 core-js

这个方案也稍微有些麻烦, core-js 里实现了大部分 e6、es7 的高级语法,具体列表可以去这里查看 https://github.com/babel/babe...

我先截取一部分做下参考

  Object: {
      assign: "object/assign",
      create: "object/create",
      defineProperties: "object/define-properties",
      defineProperty: "object/define-property",
      entries: "object/entries",
      freeze: "object/freeze",
      ...
  }

具体怎么使用呢?找到要使用的方法的值,如:assign 是 "object/assign",将其拼接至一个固定路径。

import assign from 'core-js/library/fn/object/assign'

import 'core-js/fn/object/assign'

这里包含上述所说的局部使用和全局实现的两种

直接引入 'core-js/fn/' 下的即为全局实现,你可以在程序入口引入你想使用的,这样相对于方案二避免了多余的库的引入

引入 'core-js/library/fn/' 下的即为局部使用,和方案一一样,只是省去了自己去寻找类库。

但是,实际使用,import 要写辣么长的路径,还是感觉有些麻烦。

方案四:使用 babel-plugin-transform-runtime

本文会重点介绍下这个插件

先看下如何使用

// without options
{
  "plugins": ["transform-runtime"]
}

// with options
{
  "plugins": [
    ["transform-runtime", {
     "helpers": false, // defaults to true; v6.12.0 (2016-07-27) 新增;
      "polyfill": true, // defaults to true
      "regenerator": true, // defaults to true
      // v6.15.0 (2016-08-31) 新增
      // defaults to "babel-runtime"
      // 可以这样配置
      // moduleName: path.dirname(require.resolve('babel-runtime/package'))
      "moduleName": "babel-runtime"
    }]
  ]
}

该插件会做三件事情

The runtime transformer plugin does three things:

  • Automatically requires babel-runtime/regenerator when you use generators/async functions.

  • Automatically requires babel-runtime/core-js and maps ES6 static methods (Object.assign) and built-ins (Promise).

  • Removes the inline babel helpers and uses the module babel-runtime/helpers instead.

  • 第一件,如果你想使用 generator , 有两个办法,一个就是引入 bable-polyfill 这个大家伙儿,另一个就是使用这个插件,否则你会看到这个错误

    Uncaught ReferenceError: regeneratorRuntime is not defined
  • 第二件,就是能帮助我们解决一些高级语法的问题,它会在构建时帮你自动引入,用到什么引什么。

但是它的缺陷是它只能帮我们引入静态方法和一些内建模块,如 Object.assignPromise 等。实例方法是不会做转换的,如 "foobar".includes("foo") ,官方提示在这里:

NOTE: Instance methods such as "foobar".includes("foo") will not work since that would require modification of existing builtins (Use babel-polyfill for that).

翻译一下就是,不要越俎代庖,不是你的东西你别乱碰,欠儿欠儿的。

image

所以这个方案不会像方案二那样随心所欲的使用,但其实也基本够用了。

没有的实例方法可以采用方案三委屈下。

个人还是比较推荐这两种合体的方案。

需要注意的一点是:

开启 polyfill 后,会与 export * from 'xx' 有冲突

请看构建后的代码:

...
/***/ },
/* 106 */
/***/ function(module, exports, __webpack_require__) {

    'use strict';
    // 这是什么鬼。
    import _Object$defineProperty from 'babel-runtime/core-js/object/define-property';
    import _Object$keys from 'babel-runtime/core-js/object/keys';
    Object.defineProperty(exports, "__esModule", {
      value: true
    });
    ...

截止 2016-09-10,官方尚未解决此 issue, 只有先避开 export * from 'xx' 这种写法。或在这里找答案。

  • 第三件,是会引入一些 helper 来代替每次都生成的通用函数,看个例子就明白了

原来构建好的代码每个模块都有类似这种代码:

function _classCallCheck(instance, Constructor)...

function _possibleConstructorReturn(self, call)...

function _inherits(subClass, superClass)...

开启 helper 后:

var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');

var _possibleConstructorReturn2 = require('babel-runtime/helpers/possibleConstructorReturn');

var _inherits2 = require('babel-runtime/helpers/inherits');

这样统一引用了 helper,去处了冗余,看起来也更优雅了。

在 v6.12.0 之前 helper 也是默认开启的,没有配置可改,其他的 ployfill regenerator 都是有配置可以设置的。也许是推荐你使用 helper 。

但是 v6.12.0 (2016-07-27) 增加了 helper 的配置。为什么呢?

我最开始用这个插件的时候也很诧异,按道理来说,去除了冗余代码,代码的体积应该变小才对,但实际测试却变大了,我测试时是未经 uglify 的代码从 18k 增加到了 78k,查看构建模块增加了将近 100 个 详情

原因是从 babel-runtime 里引入的 helper 依赖很多,全部都是兼容最底层的。比如 Object.createtypeof 这种方法全部被重写了。

后来 gaearon 大神都忍不了了,他测试的结果是增加了 5kB min+gzip 详情

于是有了 helper 这个配置项。

另外还有一点,如果开启了 helper 的话,你会发现之前引用的 babel-plugin-transform-proto-to-assign 就失效了,虽然他本来就不该被使用,后面会讲到。

所以目前看来这个 helper 不用也罢。

再说下 moduleName 这个参数是干什么的?

还记得开启 helper 后的代码吗

var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');

看下这个路径,如果是本地项目安装了 babel-runtime 是没问题的,但如果你是用的通用构建工具,比如 nowa,所有的构建依赖库都是在公共的地方,毕竟 babel 太太了。这里就会报错了。

Cannot resolve module babel-runtime/regenerator

gaearon 大神在写 create-react-app 时也发现了这个问题, 详情

虽然这个问题可以通过 webpack 的 resolve.root 来解决,但是 gaearon 大神看其不爽,觉得依赖 webpack 不够优雅,#3612 于是乎就有了 moduleName 这个参数,已发布 v6.15.0 (2016-08-31)。

放弃 loose 模式, 放弃 ie8

上篇中提到了开启了 loose 模式来解决低版本浏览器无法继承到在构造函数里定义的属性或方法。

我们是通过 babel-preset-es2015-ie 这个插件,主要是改写了 babel-plugin-transform-es2015-classes: {loose: true} 和添加了插件 babel-plugin-transform-proto-to-assign(解决类方法继承的问题)

babel-preset-es2015 v6.13.0 (2016-08-04) 时,presets 已经支持了参数配置,可以直接开启 loose 模式。

它内部会把开启一些插件的 loose 模式,不只是babel-plugin-transform-es2015-classes

{
  presets: [
    ["es2015", { "loose": true }]
  ]
}

这样我们就可以直接使用 babel-preset-es2015,至于 babel-plugin-transform-proto-to-assign 可以单独配置,也可不使用,因为类方法本来就不该被继承,要使用就直接 Parent.defaultProps 就可以了。

在上文中并没有提到开启 loose 模式的另一个原因是解决 ie8 下的两个 es3 属性名关键字的问题,因为上文测试均在 ie9 上,所以上述的方案也是停留在必须支持 ie8。

那么如果我们放弃了 ie8 ,看一看是不是会海阔天空。

babel-plugin-transform-es2015-classes v6.14.0 (2016-08-23) 一个 ‘大胡子哥’(原谅我不认识他) 修复了 __proto__ 这个问题 #3527 Fix class inheritance in IE <=10 without loose mode.
这样我们就可以在 ie9+ 上使用正常的 es6 模式了。

毕竟我们该向前看,loose 模式有点后退的赶脚。

这篇文章也表达了不推荐使用 loose 模式

Con: You risk getting problems later on, when you switch from transpiled ES6 to native ES6. That is rarely a risk worth taking.

当然,如果真的离不开 ie8,就针对 es3 关键字的问题引用两个插件即可

require('babel-plugin-transform-es3-member-expression-literals'),
require('babel-plugin-transform-es3-property-literals'),

我们再稍微看下‘大胡子哥’的修改,其实很简单,也很巧妙,看一行关键代码

// 修改后生成的代码多了一个 先取 `xxx.__proto__` 再使用 `Object.getPrototypeOf`
  var _this = _possibleConstructorReturn(this, (Test.__proto__ || Object.getPrototypeOf(Test)).call(this, props));

回顾下 inherits 方法的实现

function _inherits(subClass, superClass) {
    ...
    // 虽然 ie9/10 不支持 `__proto__`,这里只是作为了普通对象给予赋值,`Object.getPrototypeOf` 获取不到但可以直接 `.__proto__` 获取
  Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
  ...

如果你看懂了实现方式,不知道你有没有发现 babel-plugin-transform-proto-to-assign(解决类方法继承的问题)这个家伙真的不能用了

function _inherits(subClass, superClass) { 
  ...
  // 因为它会将 `__proto__` 转为 `_default` 
  Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : _defaults(subClass, superClass);
}

这样上述的修复就无效了。切记不能使用,还是那句话,类方法本来就不该被继承。

最后看下终极方案的通用配置

{
  plugins: [
    ["transform-runtime", {
      "helpers": false,
      "polyfill": true,
      "regenerator": true
    }],
    'add-module-exports',
    'transform-es3-member-expression-literals',
    'transform-es3-property-literals',
  ],
  "presets": [
    'react',
    'es2015',
    'stage-1'
  ],
}

更简单、完整的解决方案,请查看 nowa

感谢阅读。

参考链接

广告时间: 请献出你的小星星

查看原文

赞 25 收藏 56 评论 2

matianming 发布了文章 · 2016-09-18

ES6 + Webpack + React + Babel 如何在低版本浏览器上愉快的玩耍(上)

起因

某天,某测试说:“这个页面在 IE8 下白屏,9也白。。”

某前端开发: 吭哧吭哧。。。一上午的时间就过去了,搞定了。

第二天,某测试说:“IE 又白了。。”

某前端开发: 吭哧吭哧。。。谁用的 Object.assign,出来我保证削不屎你。

原谅我不禁又黑了一把 IE。

有人可能会想,都要淘汰了,还有什么好讲的?

也许几年后,确实没用了,但目前我们的系统还是要对 ie8+ 做兼容,因为确实还有个别用户,尽管他没朋友。。。

记录下本次在 IE 下踩得坑,让后面的同学能够不再在这上面浪费时间了。

经过

测试

首先,看下面代码(以下测试在 IE9)

class Test extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    return <div>{this.props.content}</div>;
  }
}

module.exports = Test;

这段代码跑的妥妥的,没什么问题。

一般来说,babel 在转换继承时,可能会出现兼容问题,那么,再看这一段

class Test extends React.Component {
  constructor(props) {
    super(props);
  }
  test() {
      console.log('test');
  }
  render() {
    return <div>{this.props.content}</div>;
  }
}

Test.defaultProps = {
  content: "测试"
};

class Test2 extends Test {
  constructor(props) {
    super(props);
    this.test();
  }
}

Test2.displayName = 'Test2';

module.exports = Test2;

这段代码同样也可以正常运行

也就是说在上述这两种情况下,不做任何处理(前提是已经加载了 es5-shim/es5-sham),在 IE9 下都可以正常运行。

然后我们再看下会跑挂的代码

class Test extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      test: 1,
    };
  }
  test() {
      console.log(this.state.value);
  }
  render() {
    return <div>{this.props.content}</div>;
  }
}

Test.defaultProps = {
  content: "测试"
};

class Test2 extends Test {
  constructor(props) {
    super(props);
    // SCRIPT5007: 无法获取属性 "value" 的值,对象为 null 或未定义
    this.test();
    
    // SCRIPT5007: 无法获取属性 "b" 的值,对象为 null 或未定义
    this.a = this.props.b;
  }
}
// undefined
console.log(Test2.defaultProps);

Test2.displayName = 'Test2';

module.exports = Test2;

这段代码在高级浏览器中是没问题的,在 IE9 中会出现注释所描述的问题

从这些问题分析,可得出3个结论

  1. 在构造函数里的定义的属性无法被继承

  2. 在构造函数里不能使用 this.props.xx

  3. 类属性或方法是无法被继承的

也就是说,只要规避了这三个条件的话,不做任何处理(前提是已经加载了 es5-shim/es5-sham),在 IE9 下都可以正常运行。

第二点,是完全可以避免的,切记在 constructor 直接使用 props.xxx, 不要再用 this.props.xxx

第三点,也是可以完全避免的,因为从理论上来说,类属性就不该被继承,如果想使用父类的类属性可以直接Test2.defaultProps = Test.defaultProps;

第一点,可避免,但无法完全避免

原因

第一点,有时是无法完全避免的,那么就要查询原因,才能找到解决方案

我们把 babel 转义后的代码放出来就能查出原因了

'use strict';

var _createClass = function () {
  ...
}();

function _classCallCheck(instance, Constructor) { 
  ...
}

function _possibleConstructorReturn(self, call) { 
  ...
  // 这个方法只是做了下判断,返回第一个或第二参数
  return call && (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { 
  ...; 
  // 这里的 _inherits 是通过将子类的原型[[prototype]]指向了父类,所以如果在高级浏览器下,子类的可以继承到类属性
  // 根本问题也是出在这里,IE9 下既没有 `setPrototypeOf` 也没有 `__proto__`
  if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 
}

var Test = function (_React$Component) {
  ...
  return Test;
}(React.Component);

Test.defaultProps = {
  content: "测试"
};

var Test2 = function (_Test) {
  _inherits(Test2, _Test);

  function Test2(props) {
    _classCallCheck(this, Test2);
     // 这里的 this 会通过 _possibleConstructorReturn,来获取父类构造函数里定义的属性
     // _possibleConstructorReturn 只是做了下判断,如果第二个参数得到了正确执行,则返回执行结果,否则返回第一个参数,也就是子类的 this
     // 也就是说问题出在 Object.getPrototypeOf 
     // 在 _inherits 中将子类的原型指向了父类, 这里通过 getPrototypeOf 来获取父类,其实就是 _Test
     // Object.getPrototypeOf 不能正确的执行,导致了子类无法继承到在构造函数里定义的属性或方法,也无法继承到类属性或方法
    var _this2 = _possibleConstructorReturn(this, Object.getPrototypeOf(Test2).call(this, props));

    _this2.test();
    console.log(_this2.props.children);
    return _this2;
  }

  return Test2;
}(Test);

console.log(Test2.defaultProps);

Test2.displayName = 'Test';

module.exports = Test2;

通过上述的代码注释,可以得出有两处问题需要解决

  1. 正确的获取父类(解决无法继承到在构造函数里定义的属性或方法)

  2. 正确的将子类的原型指向了父类(解决无法继承到类属性或方法)

解决方案

通过文档的查询,发现只要开启 es2015-classes 的 loose 模式即可解决第一个问题

loose 模式

Babel have two modes:

  • A normal mode follows the semantics of ECMAScript 6 as closely as possible.

  • A loose mode produces simpler ES5 code.

Babel 有两种模式:

  • 尽可能符合 ES6 语义的 normal 模式。

  • 提供更简单 ES5 代码的 loose 模式。

尽管官方是更推荐使用 normal 模式,但为了兼容 IE,我们目前也只能开启 loose 模式。

在 babel6 中,主要是通过 babel-preset-2015 这个插件,来进行转义的
我们看下 babel-preset-2015

 plugins: [
    require("babel-plugin-transform-es2015-template-literals"),
    require("babel-plugin-transform-es2015-literals"),
    require("babel-plugin-transform-es2015-function-name"),
    ...
    require("babel-plugin-transform-es2015-classes"),
    ...
    require("babel-plugin-transform-es2015-typeof-symbol"),
    require("babel-plugin-transform-es2015-modules-commonjs"),
    [require("babel-plugin-transform-regenerator"), { async: false, asyncGenerators: false }],
  ]

是一堆对应转义的插件,从命名上也可看出了大概,比如 babel-plugin-transform-es2015-classes 就是做类的转义的,也就是我们只需把它开启 loose 模式,即可解决我们的一个问题

[require('babel-plugin-transform-es2015-classes'), {loose: true}],

看下开启了 loose 模式的代码,你会发现它的确更接近 ES5

var Test = function (_React$Component) {
  ...
  // 这里是 ES5 的写法
  Test.prototype.test = function test() {
    console.log(this.state.value);
  };
  /* normal 模式是这样的
  {
    key: 'test',
    value: function test() {
      console.log(this.state.value);
    }
  }
  */
  return Test;
}(React.Component);

var Test2 = function (_Test) {
  _inherits(Test2, _Test);

  function Test2(props) {
    _classCallCheck(this, Test2);
    // 这里直接拿到了父类 _Test, 即解决了无法继承到在构造函数里定义的属性或方法
    var _this2 = _possibleConstructorReturn(this, _Test.call(this, props));

    _this2.test();
    return _this2;
  }

  return Test2;
}(Test);

我们可以通过去安装 babel-preset-es2015-loose, 这个插件来开启 loose 模式。

但从我们团队的 老司机 口中

image

得到了一个更好插件babel-preset-es2015-ie,看下这个插件的代码,发现它和原来的 babel-preset-2015 只有两行区别

[
  [require('babel-plugin-transform-es2015-classes'), {loose: true}],
  require('babel-plugin-transform-proto-to-assign'),
]

刚好解决我们上述碰到的两个问题

这个 babel-plugin-transform-proto-to-assign 插件会生成一个 _defaults 方法来处理原型

function _inherits(subClass, superClass) { 
  ...; 
  if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : _defaults(subClass, superClass);
}
function _defaults(obj, defaults) {
 var keys = Object.getOwnPropertyNames(defaults);
  for (var i = 0; i < keys.length; i++) {
   var key = keys[i]; 
   var value = Object.getOwnPropertyDescriptor(defaults, key);
    if (value && value.configurable && obj[key] === undefined) {
     Object.defineProperty(obj, key, value); 
     } 
   }
  return obj;
}

这个插件正确的将子类的原型指向了父类(解决无法继承到类属性或方法)

总结

本文讲述低版本浏览器报错的原因和解决方案

  • 一方面是提示下在构造函数里不要使用 this.props.xx

  • 另一方面也对继承的机制有了更好的理解

在这次项目中发现在低版本浏览器跑不起来的两点主要原因:

  1. SCRIPT5007: 无法获取属性 xxx 的值,对象为 null 或未定义,这种情况一般是组件继承后,无法继承到在构造函数里定义的属性或方法,同样类属性或方法也同样无法继承

  2. SCRIPT438: 对象不支持 xxx 属性或方法,这种情况一般是使用了 es6、es7 的高级语法,Object.assginObject.keys 等,这种情况在移动端的一些 ‘神机’ 也一样会挂。

第一点本文已经分析,预知第二点讲解请见下篇。

备注:下篇会主要介绍下如何让 用了 Object.assign 的那位同学可以继续用,又不会被削。

查看原文

赞 13 收藏 44 评论 0

认证与成就

  • 获得 47 次点赞
  • 获得 4 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

注册于 2016-09-18
个人主页被 979 人浏览