{"action":"created","issue":{"url":"https://api.github.com/repos/amfe/article/issues/15","labels_url":"https://api.github.com/repos/amfe/article/issues/15/labels{/name}","comments_url":"https://api.github.com/repos/amfe/article/issues/15/comments","events_url":"https://api.github.com/repos/amfe/article/issues/15/events","html_url":"https://github.com/amfe/article/issues/15","id":117567563,"number":15,"title":"对无线电商动态化方案的思考（三）","user":{"login":"Jinjiang","id":206848,"avatar_url":"https://avatars.githubusercontent.com/u/206848?v=3","gravatar_id":"","url":"https://api.github.com/users/Jinjiang","html_url":"https://github.com/Jinjiang","followers_url":"https://api.github.com/users/Jinjiang/followers","following_url":"https://api.github.com/users/Jinjiang/following{/other_user}","gists_url":"https://api.github.com/users/Jinjiang/gists{/gist_id}","starred_url":"https://api.github.com/users/Jinjiang/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Jinjiang/subscriptions","organizations_url":"https://api.github.com/users/Jinjiang/orgs","repos_url":"https://api.github.com/users/Jinjiang/repos","events_url":"https://api.github.com/users/Jinjiang/events{/privacy}","received_events_url":"https://api.github.com/users/Jinjiang/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":4,"created_at":"2015-11-18T11:29:28Z","updated_at":"2015-11-18T13:33:30Z","closed_at":null,"body":"之前两篇我们分别谈到了对[无线电商动态化](https://github.com/amfe/article/issues/13)的理解，以及我们自己的技术方案：[Weex](https://github.com/amfe/article/issues/14)，今天我们分享一些其中的技术细节\r\n\r\n## data-binding\r\n\r\n首先要介绍一下我们是怎么做到数据绑定的，首先，我们对上层撰写的代码遵循了传统的 mustache 书写习惯。即：\r\n\r\n```html\r\n<template>\r\n  <text style=\"color: {{mainColor}}\">{{title}}</text>\r\n</template>\r\n<script>\r\n  module.exports = {\r\n    mainColor: '#FF9900',\r\n    title: 'Hello Weex!'\r\n  }\r\n</script>\r\n```\r\n\r\n我们的 transformer 对这样的语法的解析思路非常简单直接，即：\r\n\r\n- 不含 mustache 语法的值，直接做为最终返回值\r\n- 含 mustache 语法的值 (如 `{{xxx}}`)，转换为：`function () {return xxx}`\r\n\r\n所以上面的模板最终会被转换成：\r\n\r\n```javascript\r\n{\r\n  type: 'text',\r\n  style: {\r\n    color: function () {\r\n      return this.mainColor\r\n    }\r\n  },\r\n  content: function () {\r\n    return this.title\r\n  }\r\n}\r\n```\r\n\r\n在客户端的 JavaScript 引擎中，我们会进行这样的判断：\r\n\r\n```javascript\r\n// parent, key, value, updater, data\r\nif (typeof value === 'function') {\r\n  updater = value\r\n  parent._bindKey(data, null, key, updater)\r\n}\r\nelse {\r\n  data[key] = value\r\n}\r\n```\r\n\r\n只要是函数类型，就进行数据绑定，否则一次性赋值，简单易懂易用。\r\n\r\n这样在相应的数据发生改写的时候，界面结构和样式会通过 `_bindKey` 和 `updater` 自动触发更新。\r\n\r\n关于如何在 JavaScript 上实现数据监听，也算是一个很重要的细节，不过市面上已经有大把优秀的实现案例了，我们也没有在这个环节上做特别的事情，所以不做赘述。(p.s. 在 Weex 的 JS Framework 中，我们复用了 Vue.js 的数据监听 (`observer/*.js`) 和依赖收集 (`watcher.js`) 的代码实现，在此特别要感谢一下！)\r\n\r\n## transformer\r\n\r\n在 transformer 中，我们主要的工作就是对 HTML、CSS、JavaScript 代码进行解析和重组。这里我们用到了三个非常重要的库：\r\n\r\n- HTML 解析工具：[htmlparser](https://www.npmjs.org/package/htmlparser)\r\n- CSS 解析工具：[cssom](https://www.npmjs.org/package/cssom)\r\n- JavaScript 解析工具：[uglify-js](https://www.npmjs.org/package/uglify-js)\r\n\r\n用 htmlparser 可以把 HTML 转换成 JSON\r\n\r\n```javascript\r\nvar fragment\r\nvar handler = new htmlparser.DefaultHandler(function (err, data){\r\n  fragment = data\r\n})\r\nvar parser = new htmlparser.Parser(handler)\r\nparser.parseComplete(content)\r\n...\r\n```\r\n\r\n用 cssom 可以把 CSS 转换成对象供二次处理\r\n\r\n```javascript\r\nvar css = cssom.parse(content)\r\nvar rules = css.cssRules\r\n\r\nrules.forEach(function (rule) {\r\n  rule.selectorText\r\n  rule.style\r\n  ...\r\n})\r\n```\r\n\r\n用 uglify-js 可以把 JavaScript 代码进行细节解析处理\r\n\r\n```javascript\r\n// 遍历语法树\r\nnew uglifyjs.TreeWalker(function(node, descend) {...})\r\n...\r\n// 格式化代码\r\nuglifyjs.parse(code)\r\n...\r\n```\r\n\r\n而且因为有了 transformer，**我们可以把传统 mvvm 需要在客户端甚至 DOM 上完成的模板解析、数据绑定语法解析等工作提前处理完毕**。所以免去了客户端运行时现解析模板源文件的负担。更酷的是，**因为模板解析是不依赖真实 DOM 的，所以我们可以大大方方的把语法设计成 `<img src=\"{{xxx}}\">` 而不必担心任何副作用**。而**高级的表达式、过滤器等数据绑定语法也都可以在 transformer 这一层提前处理好**，这样在撰写体验持续增强的情况下，运行时并不会产生额外的负担——这都要归功于我们引入了这一层 transformer\r\n\r\n## debugger tools\r\n\r\n我们为 native 界面调试设计了贴心的远程调试工具，主要解决三个痛点问题：\r\n\r\n1. 调试 JavaScript 代码，任意设置断点 debug\r\n2. 运行命令行代码 (Console) 对程序做实时的判断\r\n3. 渲染结构的树形审查\r\n\r\n解决问题的办法是：\r\n\r\n<img width=\"1024\" alt=\"devtools\" src=\"https://cloud.githubusercontent.com/assets/206848/11239823/4cd7dbf2-8e29-11e5-8bc1-a4d854269d55.PNG\">\r\n\r\n图：远程调试工具工作原理\r\n\r\n1. 客户端设置一个开关，可以把 JS Bridge 对接到一个远程的 websocket 服务器，而不是对接到本地的 JavaScript 引擎\r\n2. 本地准备一个网页，其中运行了完整的 JavaScript 引擎的代码，并且也可以链接到一个远程的 websocket 服务器，这样客户端的 native 层和本地网页里的 JavaScript 引擎就串联起来了\r\n3. 原本通过 JS Bridge 的双向通信内容可以被 websocket 连接记录下来\r\n4. JavaScript 引擎里的所有代码都可以通过本地浏览器的开发者工具进行 debug 和 console 控制\r\n5. 开发一个简易的 Chrome Devtools Extension，可以得到 Weex 实例的界面结构，并以目录树的方式呈现出来。\r\n\r\n这样，一个客户端开关，一个 websocket 服务器，一个本地的 JavaScript 引擎页面，一个开发者工具扩展，我们就实现了 Weex 的远程调试。\r\n\r\n<img width=\"625\" alt=\"devtools-screenshot\" src=\"https://cloud.githubusercontent.com/assets/206848/11239840/6564c7d4-8e29-11e5-97e1-3d6d6af34e2f.png\">\r\n\r\n图：开发者工具的审查元素功能扩展\r\n\r\n## unit test 和 ci 实践\r\n\r\nWeex 的 JavaScript 引擎作为一个相对底层的项目，品控需要做到非常严格和极致，否则一个小小的失误在客户端长期运行之后都有可能带来灾难性的后果。\r\n\r\n本次 Weex 的开发当中，我们认真实践了基于 mocha、chai 和 sinon 的单元测试，每个源代码的文件夹都放了一个名叫 `__test__` 的文件夹，里面放了这个目录下所有同名的 JavaScript 文件，每个文件的内容都是当前文件夹内对应文件的测试用例。\r\n\r\n<img width=\"833\" alt=\"_2015-11-18_19_08_07\" src=\"https://cloud.githubusercontent.com/assets/206848/11239851/70271528-8e29-11e5-8531-da38d0f471c2.png\">\r\n<img width=\"832\" alt=\"_2015-11-18_19_08_17\" src=\"https://cloud.githubusercontent.com/assets/206848/11239855/73dc9562-8e29-11e5-81e8-79e5089c41fa.png\">\r\n\r\n图：所有的源文件都在同级目录下有一个 `__test__` 文件夹放相应的测试代码\r\n\r\n在项目中后期，我们还引入了集团内部的一个 ci 系统，每次开发新功能，先开一个分支，然后写测试用例，最后进行代码实现知道跑通所有的 test case，搞定之后发起 merge request，ci 系统会自动在线上运行所有的回归测试，再次验证其正确性和各项指标。\r\n\r\n<img width=\"821\" alt=\"network_graph_-_weapp-plus___js-framework___gitlab\" src=\"https://cloud.githubusercontent.com/assets/206848/11239904/c2f5100c-8e29-11e5-849a-ebd3a742a507.png\">\r\n\r\n图：项目分支管理\r\n\r\n<img width=\"652\" alt=\"cise\" src=\"https://cloud.githubusercontent.com/assets/206848/11239913/c993a8e2-8e29-11e5-8c42-2f180483e44e.png\">\r\n\r\n图：CI 实践\r\n\r\n其实有关项目品控的话题还有很多，我们也在逐渐实践当中。\r\n\r\n## isomorphic (同构)\r\n\r\n我们已经在内部版本实现了简单的服务端渲染，有了这个东西会怎样呢？\r\n\r\n一旦我们可以在服务端直接渲染出界面的 JSON 结构，客户端就可以绕过 JavaScript 解析过程，直接根据 JSON 结构把界面渲染出来。与其对应的逻辑控制稍后也会初始化好，并和界面效果最终保持同步，但在整个过程中，首屏加载的时间会进一步缩短。而且，更令人兴奋的是，如果当前界面刚好没有交互逻辑，甚至后期的 JavaScript 也不需要参与了，这是一条更短的链路！\r\n\r\n## 反哺 HTML5\r\n\r\n通过对 Weex 技术方案的探索，我们把组件化开发、transformer 机制、同构等理念反哺到 HTML5 开发，会有什么样的惊喜呢？\r\n\r\n那就是一个极简的针对无线前端的 HTML5 MVVM 库：我暂时取名叫做 v.js\r\n\r\nv.js 的名字来自著名的 MVVM 库 Vue.js，我希望这个库更适合移动端，目前它可以具备所有 MVVM 的核心功能，通过 transformer 提前解析模板，运行时更快速，体积是 Vue.js 的 1/3，而且支持服务端的同构。这些都是基于移动端现状的二次改进，目前还在细节构思和研发当中。希望不久的将来可以跟大家见面。\r\n\r\n```html\r\n<template>\r\n  <div>\r\n    <img src=\"http://www.taobao.com/favicon.ico\" width=\"32\" height=\"32\" onclick=\"foo\"></img>\r\n    <span style=\"color: red;\">Hello{{name}}!</span>\r\n  </div>\r\n</template>\r\n\r\n<style>\r\n  .title {color: red;}\r\n</style>\r\n\r\n<script>\r\n  module.exports = {\r\n    data: {\r\n      title: 'World'\r\n    },\r\n    methods: {\r\n      foo: function (e) {\r\n        this.title = 'V'\r\n      }\r\n    }\r\n  }\r\n</script>\r\n```\r\n\r\n基于 v.js 的源文件\r\n\r\n<img width=\"416\" alt=\"vjs-demo\" src=\"https://cloud.githubusercontent.com/assets/206848/11239963/0ef79dd0-8e2a-11e5-8d24-308dd7a32c63.png\">\r\n\r\nv.js 的展示效果\r\n\r\n```json\r\n{\r\n  \"ref\":\"0\",\"type\":\"root\",\"attr\":{},\"style\":{},\r\n  \"children\":[\r\n    {\"ref\":\"1\",\"type\":\"div\",\"attr\":{},\"style\":{},\"component\":\"...\",\r\n      \"children\":[\r\n        {\"ref\":\"2\",\"type\":\"image\",\"attr\":{\"height\":\"32\",\"width\":\"32\",\"src\":\"http://www.baidu.com/favicon.ico\"},\"style\":{},\"event\":[\"click\"]},\r\n        {\"ref\":\"3\",\"type\":\"span\",\"attr\":{\"value\":\"HelloWorld!\"},\"style\":{\"color\":\"red\"}}\r\n      ]\r\n    }\r\n  ]}\r\n```\r\n\r\n同构的 JSON 数据，方便 native 快速渲染\r\n\r\n```html\r\n<root ref=\"0\">\r\n  <div ref=\"1\">\r\n    <image ref=\"2\" height=\"32\" width=\"32\" src=\"http://www.baidu.com/favicon.ico\" onclick=\"$fire\"></image>\r\n    <span ref=\"3\" value=\"HelloWorld!\" style=\"color: red\"></span>\r\n  </div>\r\n</root>\r\n```\r\n\r\n同构的 HTML 代码，方便浏览器快速渲染\r\n\r\n篇幅有限，写了三篇还是觉得不够，<s>期待和大家更多的交流</s>**我晚上再针对大家频繁问及或感兴趣的问题做一个整理**。\r\n\r\n阿里无线前端团队更多精彩的内容还在后面排队，我这边对无线电商动态化方案的思考就先写到这里了：）"},"comment":{"url":"https://api.github.com/repos/amfe/article/issues/comments/157714220","html_url":"https://github.com/amfe/article/issues/15#issuecomment-157714220","issue_url":"https://api.github.com/repos/amfe/article/issues/15","id":157714220,"user":{"login":"yelingfeng","id":1932350,"avatar_url":"https://avatars.githubusercontent.com/u/1932350?v=3","gravatar_id":"","url":"https://api.github.com/users/yelingfeng","html_url":"https://github.com/yelingfeng","followers_url":"https://api.github.com/users/yelingfeng/followers","following_url":"https://api.github.com/users/yelingfeng/following{/other_user}","gists_url":"https://api.github.com/users/yelingfeng/gists{/gist_id}","starred_url":"https://api.github.com/users/yelingfeng/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yelingfeng/subscriptions","organizations_url":"https://api.github.com/users/yelingfeng/orgs","repos_url":"https://api.github.com/users/yelingfeng/repos","events_url":"https://api.github.com/users/yelingfeng/events{/privacy}","received_events_url":"https://api.github.com/users/yelingfeng/received_events","type":"User","site_admin":false},"created_at":"2015-11-18T13:33:30Z","updated_at":"2015-11-18T13:33:30Z","body":"持续关注"}}