麦芽糖

麦芽糖 查看完整档案

深圳编辑广东工业大学  |  计算机科学与技术 编辑深圳观麦科技网络  |  前端工程师 编辑 www.liyatang.me/ 编辑
编辑

enjoy segmentfault

个人动态

麦芽糖 赞了文章 · 2019-01-11

跟帖"我对技术会议的一些看法"

今天看到勾三股四的文章我对技术会议的一些看法,
有些感想, 也想把自己感受到的写出来, 大致顺着勾股的文章做感想吧.

秋天的时候整理的吊牌... 这些年压下来已经挺多的了.

这些年参加的会议都是国内的, 之前是同城的聚会, 然后有一些其他城市的活动,
最远的一次跟着勾股去了台湾那边参加 ModernWeb, 体验也更特殊一些.

会议的意义

我觉得网络发达了真的对技术会议有不小的改变, 特别是视频的传播,
国内的开发者应该也感觉得到, Google Apple 那些大会我们都没机会去,
但是会议结束之后, 那些视频往往会被整理出来, 全世界的开发者都有机会看,
视频的好处, 可以暂停, 可以快进, 可以选择, 纯粹出于学技术的目的, 完全足够.
所以我会觉得更多是交流的意义, 面对面那种投入感, 信任感, 能进行更深入的交流.

当然, 分享也是挺重要的. 不做分享, 陌生人交流会尴尬, 不好进入话题.
熟人之间当然是很容易进入到相互熟悉的话题的, 因为铺垫比较充足,
但是陌生人之间要沟通, 首先要有一定的了解, 然后有一些共通的知识基础.
我当然也相信有些人口才好, 陌生也能聊起来, 但是我想, 更多人还是需要先相互有所了解.

我幸运的地方是, 我因为 React 认识了不少人, 所以在大会上能有熟人交流了,
但我最初去 D2 那样的会, 跟着五花肉问, 放眼望去总共就认不到几个人,
那种情况下交流的障碍带来的隔阂我也是能体会到.
但是她当时说的蛮有道理, 先要有些认识的人, 然后才能认识更多的人,
至少做些准备吧, 匆匆忙忙的机会本来也就不是那么多, 总不能不说话就结束了.

审稿

我觉得审稿有一定的必要, 之前有机会旁听过贺老给人审 review Qcon 试讲,
对于演讲质量和演讲技巧来说, 有不小帮助的, 主要是对演讲经验少的帮助更多一些,
程序员很多都在埋头做技术, 锻炼演讲技能的机会不是那么多,
我的经历, 如果不是 Teambition 当年有内部的分享, 我最开始的演讲底子就很欠缺.
这两年工作节奏忙这方面做的不够, 所以我能感觉到审稿和试讲会有不少帮助.

而且稿子的内容, 长短, 深度, 是否跟其他嘉宾有重叠, 我觉得也是主办方需要考虑的,
倒不是说要主办方控制讲师讲的是什么, 而是说根据观众群体水平调整一下侧重,
有时候大会上嘉宾内容之间深度会有明显的反差, 这时候事先通气总是能调整一些的.

会前预热

参会是交流的好机会, 但是很多次, 会发现等到认识了, 会议已经散场了,
如果说接下来晚饭或者第二天有活动, 大家还能一起继续交流交流,
不然的话, 各自就回到自己城市去了, 下一次碰面不知道什么时候.
所以我觉得, 会议开始之前进行预热, 很多时候能带来好处.
深圳 JSConf 那年, 就是提前有建群, 然后大家出车站机场就有在指路或者约饭的,
小圈子里当然建群还更早一些, 熟人可能更早约见面了.

我个人比较期待的话, 如果可以带着自己代码的 Demo 去交流, 效果会更好,
其实很多说的事情, 你在网上说, 或者看新闻说, 是相似的,
或者见面的时间那么短, 能说的事情反而可能不如在网上能展开的细节来得多,
但是 Demo 的话, 现场演示和交流还是比录视频去演示要好的,
随时能跟着对方的想法去调整, 去解释, 会更有效果. 也明白哪里更重要.

而且可能提早一段时间去讲师的微博 Twitter 上熟悉一下相关领域还更好一些,
会场要上去聊天的话, 对方近期做的一些事情当然了解一下是更好,
特别是相互之间做的事情有没有交集, 有没有项目方面搭上的机会...
我站到讲师的角度, 我也希望有人到我面前说事情之前, 微博上是打过招呼的.

交流工具

目前为止我看到的, 大陆的会议, 如果交流, 都是用的微信,
台湾那次比较邪乎, 他们用的 Gitter, 结果人真的少, 听不到什么八卦,
有微信其实蛮不错的, 主办方能很快收到反馈, 有什么问题, 都很快.

微信麻烦的就是刷屏了, 话题也很容易带偏, 几个人聊着很容易被大部队冲散了,
所以我一直有点期望能有个两个维度的聊天工具, 论坛帖子那样的,
聊着聊着能很容易走近小圈子, 这样有些技术问题能在时间线当中延续.

还有一个就是陌生人的话, 你很难把他微信跟他在技术社区的身份很快关联起来,
网上的话, 看看这人 GitHub 或者中文版 StackOverflow, 或者博客, 能看到水平,
技术会议上看着别人主页晒美食晒行程总觉得哪里不大对....

本地聚会和全国会议

参加大会, 有一个问题是, 一些新人去了, 其实很难找到机会跟有名气点的人聊天,
如果害羞, 话都没机会说, 如果没有知识储备, 聊两句就聊不下去了,
也不能指望对方一定是耐心的人, 毕竟一天时间另一个人还想找别人聊天呢,
对于新手来说, 更需要的是本地小规模的聚会, 甚至多接触经验相近的人.

相比各种大会, 我觉得地区性的小型聚会需要更多的人去组织,
我在上海, 参与过很多次朋友办的 Linux 用户组的分析, Elixir 社区的分享,
也有参与过比较少的 Ruby 社区的活动, 或者 FreeCodecamp 的活动,
上海这些活动应该说比较好, 相对其他城市加班少一些, 人口数量大.
但是小型的活动挺难延续的, 找场地, 找话题, 都要免费, 比较难凑起来.

之前同城的 Ruby 聚会, Elixir 聚会, 给我的感觉更好一些,
人少, 相互都能认识, 交流的时间也稳定一些, 也不像大会那么拘谨,
这种氛围对于经验少一些的人更合适一些了, 而且也不用去愁很贵的门票.

而大会的话, 听说是门票钱很多耗在了外国讲师的行程上面(没去确认过),
我也当过讲师, 能感觉到机票食宿方面如果折腾起来, 都挺耗钱的...
这对很多新人来说是吃亏的地方, 因为大老远请来了嘉宾, 你一句话都搭不上,
而且在会场认识的人多了, 能做的事情也就更多, 这是有点两极分化的.

英语

广州这次终于壮着胆子跟国外讲师套近乎了, 我口语也不够流利,
听 Sean 聊天给我的感觉, 他在国外各种分享认识了好多人,
所以他接触过很多的想法, 不同的技术氛围, 我们在国内很难碰到,
我是在讲师聚餐时候听到讲的, 所以听了总比平时碰到能听到的多,
当然相应的, 需要的知识储备, 不同的方面的东西, 也要求会多一些...

然后问题就是, 国内的话, 越是年纪大的人当中, 英语好的人越少,
这么一趟下来我更觉得英语重要了, 或者说对英语好的人是个机会, 万一突然红了呢!
说不定哪天开会的时候就要征志愿者给他们配翻译呢, 谁知道呢...
我觉得外国讲师应该也是会觉得国内生态有什么有意思的, 也想听听八卦,
就算不听八卦, 满街的中文饭馆还有路标总是需要人帮衬的.

圈子外的人

我有在参会的时候遇到过圈子外的人来听技术分享的情况, 当然这种少一些,
我是说, 换个角度可以想想, 除了新手, 还是有一些人需要我们给与一些帮助的,
那些人可能是对某项内容感兴趣的创业者, 可能冲着朋友或者讲师过来的,
技术会议内容晦涩, 如果能提供一些帮助, 我们可能包容多一些人参与进来.

而且某些情况, 等这一茬的程序员长大结婚了, 带着配偶带着小孩参会也是可能发生的事情,
这种, 说不定同城聚会不那么正式的场合会多一些,
这样其实还是要有心理准备要跟圈外的人甚至还没入行的人作交流.
我印象当中新人畏惧社交会多一些, 还没有进入圈子的人, 他们也需要多一些机会.

结尾

勾股说得对!

最后我想鼓励大家多学习英文,多出去走走。
查看原文

赞 8 收藏 0 评论 0

麦芽糖 赞了文章 · 2018-09-03

试用React语法的多端框架Taro问题汇总

Taro 是由京东 - 凹凸实验室打造的一套遵循 React 语法规范的多端统一开发框架。

我试用了有15天左右,总的来说,这是一款优秀的框架,尤其补充了目前市面上无法用 React 开发小程序的需求空缺,所以其优点就不多说了,大家可去其官方查看详细文档

下面说下我的试用感受,希望帮助后面使用Taro的同学少踩一些坑;因为能力有限,可能了解和认识会有一些不到位的地方,还望各路大佬不吝留言赐教

存在的问题

以下,是我在使用Taro过程中遇到的影响开发流程或体验的地方:

  1. 不支持source map,调试可通过debugger
  2. 不支持alias,所以项目里会有大片大片的 ../../../,不利于后期维护
  3. 全局请求的需求,官方未有最佳方案。理应app.tsx是最合适的地方,但是该组件的Provider组件内写的任何组件都会被Taro替换掉。我目前是通过在app.tsx里通过store.dispatch(action)发送全局异步请求
  4. redux/connect方法的mapStateToProps缺少第二个参数ownProps
  5. 组件嵌套时,taro生命周期与react生命周期执行顺序有差异,如图是Taro的生命周期执行顺序,可以看到componentDidMount跟React是相反的。图片描述
  6. 不可以使用 ... 拓展操作符给组件传递属性,<Comp {...props} />写法错误
  7. 属性不能传入 JSX 元素,<Content footer={<View />} />写法错误

展望

虽然存在以上种种问题,还是要感谢京东前端团队能够开源一款React语法的多端开发框架,让我们React粉儿能够用React开发小程序;以上有些问题我已提了PR,如Q4,并且维护人员很快将PR进行了merge,凹凸团队对这个项目的重视程度和责任心可见一斑,所以我相信,凹凸团队一定可以把Taro不断完善的,加油!

查看原文

赞 14 收藏 6 评论 8

麦芽糖 赞了文章 · 2018-09-03

试用React语法的多端框架Taro问题汇总

Taro 是由京东 - 凹凸实验室打造的一套遵循 React 语法规范的多端统一开发框架。

我试用了有15天左右,总的来说,这是一款优秀的框架,尤其补充了目前市面上无法用 React 开发小程序的需求空缺,所以其优点就不多说了,大家可去其官方查看详细文档

下面说下我的试用感受,希望帮助后面使用Taro的同学少踩一些坑;因为能力有限,可能了解和认识会有一些不到位的地方,还望各路大佬不吝留言赐教

存在的问题

以下,是我在使用Taro过程中遇到的影响开发流程或体验的地方:

  1. 不支持source map,调试可通过debugger
  2. 不支持alias,所以项目里会有大片大片的 ../../../,不利于后期维护
  3. 全局请求的需求,官方未有最佳方案。理应app.tsx是最合适的地方,但是该组件的Provider组件内写的任何组件都会被Taro替换掉。我目前是通过在app.tsx里通过store.dispatch(action)发送全局异步请求
  4. redux/connect方法的mapStateToProps缺少第二个参数ownProps
  5. 组件嵌套时,taro生命周期与react生命周期执行顺序有差异,如图是Taro的生命周期执行顺序,可以看到componentDidMount跟React是相反的。图片描述
  6. 不可以使用 ... 拓展操作符给组件传递属性,<Comp {...props} />写法错误
  7. 属性不能传入 JSX 元素,<Content footer={<View />} />写法错误

展望

虽然存在以上种种问题,还是要感谢京东前端团队能够开源一款React语法的多端开发框架,让我们React粉儿能够用React开发小程序;以上有些问题我已提了PR,如Q4,并且维护人员很快将PR进行了merge,凹凸团队对这个项目的重视程度和责任心可见一斑,所以我相信,凹凸团队一定可以把Taro不断完善的,加油!

查看原文

赞 14 收藏 6 评论 8

麦芽糖 赞了文章 · 2018-09-03

试用React语法的多端框架Taro问题汇总

Taro 是由京东 - 凹凸实验室打造的一套遵循 React 语法规范的多端统一开发框架。

我试用了有15天左右,总的来说,这是一款优秀的框架,尤其补充了目前市面上无法用 React 开发小程序的需求空缺,所以其优点就不多说了,大家可去其官方查看详细文档

下面说下我的试用感受,希望帮助后面使用Taro的同学少踩一些坑;因为能力有限,可能了解和认识会有一些不到位的地方,还望各路大佬不吝留言赐教

存在的问题

以下,是我在使用Taro过程中遇到的影响开发流程或体验的地方:

  1. 不支持source map,调试可通过debugger
  2. 不支持alias,所以项目里会有大片大片的 ../../../,不利于后期维护
  3. 全局请求的需求,官方未有最佳方案。理应app.tsx是最合适的地方,但是该组件的Provider组件内写的任何组件都会被Taro替换掉。我目前是通过在app.tsx里通过store.dispatch(action)发送全局异步请求
  4. redux/connect方法的mapStateToProps缺少第二个参数ownProps
  5. 组件嵌套时,taro生命周期与react生命周期执行顺序有差异,如图是Taro的生命周期执行顺序,可以看到componentDidMount跟React是相反的。图片描述
  6. 不可以使用 ... 拓展操作符给组件传递属性,<Comp {...props} />写法错误
  7. 属性不能传入 JSX 元素,<Content footer={<View />} />写法错误

展望

虽然存在以上种种问题,还是要感谢京东前端团队能够开源一款React语法的多端开发框架,让我们React粉儿能够用React开发小程序;以上有些问题我已提了PR,如Q4,并且维护人员很快将PR进行了merge,凹凸团队对这个项目的重视程度和责任心可见一斑,所以我相信,凹凸团队一定可以把Taro不断完善的,加油!

查看原文

赞 14 收藏 6 评论 8

麦芽糖 收藏了文章 · 2018-06-21

Taro 技术揭秘之taro-cli

文章同步于 Github/Blog

前言

Taro 是由凹凸实验室打造的一套遵循 React 语法规范的多端统一开发框架。

使用 Taro,我们可以只书写一套代码,再通过 Taro 的编译工具,将源代码分别编译出可以在不同端(微信小程序、H5、App 端等)运行的代码。实现 一次编写,多端运行。 关于 Taro 的更多详细的信息可以看官方的介绍文章 Taro - 多端开发框架 ,或者直接前往 GitHub 仓库 NervJS/taro 查看 Taro 文档及相关资料。

image

Taro 项目实现的功能强大,项目复杂而庞大,涉及到的方方面面(多端代码转换、组件、路由、状态管理、生命周期、端能力的实现与兼容等等)多,对于大多数人来说,想要深入理解其实现机制及原理,还是比较困难的。

Taro 技术揭秘系列文章将为你逐步揭开 Taro 强大的功能之后的神秘面纱,带领你深入 Taro 内部,了解 Taro 是怎样一步一步实现 一次编写,多端运行 的宏伟目标,同时也希望借此机会抛砖引玉,促进前端圈涌现出更多的,能够解决大家痛点的开源项目。

首先,我们将从负责 Taro 脚手架初始化和项目构建的的命令行工具,也就是 Taro 的入口:@tarojs/cli 开始。

taro-cli 包

taro 命令

taro-cli 包位于 Taro 工程的 packages 目录下,通过 npm install -g @tarojs/cli 全局安装后,将会生成一个taro 命令。主要负责项目初始化、编译、构建等。直接在命令行输入 taro ,会看到如下提示:

➜ taro
👽 Taro v0.0.63


  Usage: taro <command> [options]

  Options:

    -V, --version       output the version number
    -h, --help          output usage information

  Commands:

    init [projectName]  Init a project with default templete
    build               Build a project with options
    update              Update packages of taro
    help [cmd]          display help for [cmd]

在这里可以详细看看 taro 命令用法及作用。

包管理与发布

首先,我们需要了解 taro-cli 包与 taro 工程的关系。

将 Taro 工程 clone 下来之后,我们可以看到工程的目录结构如下,整体还是比较简单明了的。

.
├── CHANGELOG.md
├── LICENSE
├── README.md
├── build
├── docs
├── lerna-debug.log
├── lerna.json     // Lerna 配置文件
├── package.json
├── packages
│   ├── eslint-config-taro
│   ├── eslint-plugin-taro
│   ├── postcss-plugin-constparse
│   ├── postcss-pxtransform
│   ├── taro
│   ├── taro-async-await
│   ├── taro-cli
│   ├── taro-components
│   ├── taro-components-rn
│   ├── taro-h5
│   ├── taro-plugin-babel
│   ├── taro-plugin-csso
│   ├── taro-plugin-sass
│   ├── taro-plugin-uglifyjs
│   ├── taro-redux
│   ├── taro-redux-h5
│   ├── taro-rn
│   ├── taro-rn-runner
│   ├── taro-router
│   ├── taro-transformer-wx
│   ├── taro-weapp
│   └── taro-webpack-runner
└── yarn.lock

Taro 项目主要是由一系列 npm 包组成,位于工程的 packages 目录下。它的包管理方式和 Babel 项目一样,将整个项目作为一个 monorepo 来进行管理,并且同样使用了包管理工具 Lerna

Lerna 是一个用来优化托管在 git/npm 上的多 package 代码库的工作流的一个管理工具,可以让你在主项目下管理多个子项目,从而解决了多个包互相依赖,且发布时需要手动维护多个包的问题。

关于 Lerna 的更多介绍可以看官方文档 Lerna:A tool for managing JavaScript projects with multiple packages

packages 目录下十几个包中,最常用的项目初始化与构建的命令行工具 taro-cli 就是其中一个。在 Taro 工程根目录运行 lerna publish 命令之后,lerna.json 里面配置好的所有的包会被发布到 npm 上去。

目录结构

taro-cli 包的目录结构如下:

./
├── bin    // 命令行
│   ├── taro        // taro 命令
│   ├── taro-build        // taro build 命令
│   ├── taro-update        // taro update 命令
│   └── taro-init        // taro init 命令
├── package.json
├── node_modules
├── src
│   ├── build.js        // taro build 命令调用,根据 type 类型调用不同的脚本
│   ├── config
│   │   ├── babel.js    // Babel 配置
│   │   ├── babylon.js        // JavaScript 解析器 babylon 配置
│   │   ├── browser_list.js    // autoprefixer browsers 配置
│   │   ├── index.js        // 目录名及入口文件名相关配置
│   │   └── uglify.js
│   ├── creator.js
│   ├── h5.js        // 构建h5 平台代码
│   ├── project.js    // taro init 命令调用,初始化项目
│   ├── rn.js        // 构建React Native 平台代码
│   ├── util        // 一系列工具函数
│   │   ├── index.js
│   │   ├── npm.js
│   │   └── resolve_npm_files.js
│   └── weapp.js        // 构建小程序代码转换
├── templates        // 脚手架模版
│   └── default
│       ├── appjs
│       ├── config
│       │   ├── dev
│       │   ├── index
│       │   └── prod
│       ├── editorconfig
│       ├── eslintrc
│       ├── gitignore
│       ├── index.js    // 初始化文件及目录,copy模版等
│       ├── indexhtml
│       ├── npmrc
│       ├── pagejs
│       ├── pkg
│       └── scss
└── yarn-error.log

其中关键文件的作用已添加注释说明,大家可以先大概看看,有个初步印象。

通过上面的目录树可以看出,taro-cli 工程的文件并不算多,主要目录有:/bin/src/template,我已经在上面详细标注了主要的目录和文件的作用,至于具体的流程,咱们接下来再分析。

用到的核心库

  • tj/commander.jsNode.js 命令行接口全面的解决方案,灵感来自于 Ruby's commander。可以自动的解析命令和参数,合并多选项,处理短参等等,功能强大,上手简单。
  • jprichardson/node-fs-extra 在nodejs的fs基础上增加了一些新的方法,更好用,还可以拷贝模板。
  • chalk/chalk 可以用于控制终端输出字符串的样式。
  • SBoudrias/Inquirer.js NodeJs 命令行交互工具,通用的命令行用户界面集合,用于和用户进行交互。
  • sindresorhus/ora 加载中状态表示的时候一个loading怎么够,再在前面加个小圈圈转起来,成功了console一个success怎么够,前面还可以给他加个小钩钩,ora就是做这个的。
  • SBoudrias/mem-fs-editor 提供一系列API,方便操作模板文件。
  • shelljs/shelljs ShellJS 是Node.js 扩展,用于实现Unix shell 命令执行。
  • Node.js child_process 模块 用于新建子进程。子进程的运行结果储存在系统缓存之中(最大200KB),等到子进程运行结束以后,主进程再用回调函数读取子进程的运行结果。

taro init

taro init 命令主要的流程如下:

image

taro 命令入口

当我们全局安装 taro-cli 包之后,我们的命令行里就多了一个 taro 命令。

$ npm install -g @tarojs/cli

那么 taro 命令是怎样添加进去的呢,其原因在于 package.json 里面的 bin 字段;

"bin": {
    "taro": "bin/taro"
  },

上面代码指定,taro 命令对应的可执行文件为 bin/taro。npm 会寻找这个文件,在 [prefix]/bin 目录下建立符号链接。在上面的例子中,taro会建立符号链接 [prefix]/bin/taro。由于 [prefix]/bin 目录会在运行时加入系统的 PATH 变量,因此在运行 npm 时,就可以不带路径,直接通过命令来调用这些脚本。

关于prefix,可以通过npm config get prefix获取。

$ npm config get prefix
/usr/local

通过下列命令可以更加清晰的看到它们之间的符号链接:

$ ls -al `which taro`
lrwxr-xr-x  1 chengshuai  admin  40  6 15 10:51 /usr/local/bin/taro -> ../lib/node_modules/@tarojs/cli/bin/taro

taro 子命令

上面我们已经知道 taro-cli 包安装之后,taro 命令是怎么和 /bin/taro 文件相关联起来的, 那 taro init 和 taro build 又是怎样和对应的文件关联起来的呢?

命令关联与参数解析

这里就不得不提到一个有用的包:tj/commander.jsNode.js 命令行接口全面的解决方案,灵感来自于 Ruby's commander。可以自动的解析命令和参数,合并多选项,处理短参等等,功能强大,上手简单。具体的使用方法可以参见项目的 README。

更主要的,commander 支持 git 风格的子命令处理,可以根据子命令自动引导到以特定格式命名的命令执行文件,文件名的格式是 [command]-[subcommand],例如:

taro init => taro-init
taro build => taro-build

/bin/taro 文件内容不多,核心代码也就那几行 .command() 命令:

#! /usr/bin/env node

const program = require('commander')
const {getPkgVersion} = require('../src/util')

program
  .version(getPkgVersion())
  .usage('<command> [options]')
  .command('init [projectName]', 'Init a project with default templete')
  .command('build', 'Build a project with options')
  .command('update', 'Update packages of taro')
  .parse(process.argv)

command方法

用法:.command('init <path>', 'description')

command的 用法稍微复杂,原则上他可以接受三个参数,第一个为命令定义,第二个命令描述,第三个为命令辅助修饰对象。

  • 第一个参数中可以使用 <> 或者 [] 修饰命令参数
  • 第二个参数可选。

    • 当没有第二个参数时,commander.js 将返回 Command 对象,若有第二个参数,将返回原型对象。
    • 当带有第二个参数,并且没有显示调用 action(fn) 时,则将会使用子命令模式
    • 所谓子命令模式即,./pm./pm-install./pm-search等。这些子命令跟主命令在不同的文件中。
  • 第三个参数一般不用,它可以设置是否显示的使用子命令模式。
注意第一行#!/usr/bin/env node,有个关键词叫 Shebang,不了解的可以去搜搜看。

参数解析及与用户交互

前面提到过,commander 包可以自动解析命令和参数,在配置好命令之后,还能够自动生产 help(帮助) 命令和 version(版本查看) 命令。并且通过program.args便可以获取命令行的参数,然后再根据参数来调用不同的脚本。

但当我们运行 taro init 命令后,如下所示的命令行交互又是怎么实现的呢?

$ taro init taroDemo
Taro即将创建一个新项目!
Need help? Go and open issue: https://github.com/NervJS/taro/issues/new

Taro v0.0.50

? 请输入项目介绍!
? 请选择模板 默认模板

这里使用的是SBoudrias/Inquirer.js 来处理命令行交互。

用法其实很简单:

const inquirer = require('inquirer')  // npm i inquirer -D

if (typeof conf.description !== 'string') {
      prompts.push({
        type: 'input',
        name: 'description',
        message: '请输入项目介绍!'
      })
}

prompt()接受一个问题对象的数据,在用户与终端交互过程中,将用户的输入存放在一个答案对象中,然后返回一个Promise,通过then()获取到这个答案对象。so easy!

借此,新项目的名称、版本号、描述等信息可以直接通过终端交互插入到项目模板中,完善交互流程。

当然,交互的问题不仅限于此,可以根据自己项目的情况,添加更多的交互问题。inquirer.js强大的地方在于,支持很多种交互类型,除了简单的input,还有confirmlistpasswordcheckbox等,具体可以参见项目的工程README

此外,你还在执行异步操作的过程中,你还可以使用 sindresorhus/ora 来添加一下 loading 效果。使用chalk/chalk 给终端的输出添加各种样式。

模版文件操作

最后就是模版文件操作了,主要分为两大块:

  • 将输入的内容插入到模板中
  • 根据命令创建对应目录结构,copy 文件
  • 更新已存在文件内容

这些操作基本都是在 /template/index.js 文件里。

这里还用到了shelljs/shelljs 执行shell 脚本,如初始化 git git init,项目初始化之后安装依赖npm install等。

拷贝模板文件

拷贝模版文件主要是使用 jprichardson/node-fs-extracopyTpl()方法,此方法使用ejs模板语法,可以将输入的内容插入到模版的对应位置:

this.fs.copyTpl(
      project,
      path.join(projectPath, 'project.config.json',
      {description,projectName}
    );

更新已经存在的文件内容

更新已经存在的文件内容是很复杂的工作,最可靠的方法是把文件解析为AST,然后再编辑。一些流行的 AST parser 包括:

  • Cheerio:解析HTML
  • Babylon:解析JavaScript
  • 对于JSON文件,使用原生的JSON对象方法。

使用 Regex 解析一个代码文件是邪道,不要这么干,不要心存侥幸。

taro build

taro build 命令是整个 taro 项目的灵魂和核心,主要负责 多端代码编译(h5,小程序,React Native等)。

taro 命令的关联,参数解析等和 taro init 其实是一模一样的,那么最关键的代码转换部分是怎样实现的呢?

这个部分内容过于庞大,需要单独拉出来一篇讲。不过这里可以先简单提一下。

编译工作流与抽象语法树(AST)

Taro 的核心部分就是将代码编译成其他端(H5、小程序、React Native等)代码。一般来说,将一种结构化语言的代码编译成另一种类似的结构化语言的代码包括以下几个步骤:

image

首先是 parse,将代码 解析(Parse)抽象语法树(Abstract Syntex Tree),然后对 AST 进行 遍历(traverse)替换(replace)(这对于前端来说其实并不陌生,可以类比 DOM 树的操作),最后是 生成(generate),根据新的 AST 生成编译后的代码。

Babel 模块

Babel 是一个通用的多功能的 JavaScript 编译器,更确切地说是源码到源码的编译器,通常也叫做 转换编译器(transpiler)。 意思是说你为 Babel 提供一些 JavaScript 代码,Babel 更改这些代码,然后返回给你新生成的代码。

此外它还拥有众多模块可用于不同形式的 静态分析

静态分析是在不需要执行代码的前提下对代码进行分析的处理过程 (执行代码的同时进行代码分析即是动态分析)。 静态分析的目的是多种多样的, 它可用于语法检查,编译,代码高亮,代码转换,优化,压缩等等场景。

Babel 实际上是一组模块的集合,拥有庞大的生态。Taro 项目的代码编译部分就是基于 Babel 的以下模块实现的:

  • babylon Babylon 是 Babel 的解析器。最初是 从Acorn项目fork出来的。Acorn非常快,易于使用,并且针对非标准特性(以及那些未来的标准特性) 设计了一个基于插件的架构。
  • babel-traverse Babel Traverse(遍历)模块维护了整棵树的状态,并且负责替换、移除和添加节点。
  • babel-types Babel Types模块是一个用于 AST 节点的 Lodash 式工具库, 它包含了构造、验证以及变换 AST 节点的方法。 该工具库包含考虑周到的工具方法,对编写处理AST逻辑非常有用。
  • babel-generator Babel Generator模块是 Babel 的代码生成器,它读取AST并将其转换为代码和源码映射(sourcemaps)。
  • babel-template babel-template 是另一个虽然很小但却非常有用的模块。 它能让你编写字符串形式且带有占位符的代码来代替手动编码, 尤其是生成的大规模 AST的时候。 在计算机科学中,这种能力被称为准引用(quasiquotes)。

解析页面 config 配置

在业务代码编译成小程序的代码过程中,有一步是将页面入口 js 的 config 属性解析出来,并写入 *.json 文件,供小程序使用。那么这一步是怎么实现的呢,这里将这部分功能的关键代码抽取出来:

// 1. babel-traverse方法, 遍历和更新节点
traverse(ast, {  
    ClassProperty(astPath) { // 遍历类的属性声明
        const node = astPath.node
        if (node.key.name === 'config') { // 类的属性名为 config
            configObj = traverseObjectNode(node)
            astPath.remove() // 将该方法移除掉
        }
    }
})

// 2. 遍历,解析为 JSON 对象
function traverseObjectNode(node, obj) { 
    if (node.type === 'ClassProperty' || node.type === 'ObjectProperty') {
        const properties = node.value.properties
        obj = {}
        properties.forEach((p, index) => {
            obj[p.key.name] = traverseObjectNode(p.value)
        })
        return obj
    }
    if (node.type === 'ObjectExpression') {
        const properties = node.properties
        obj = {}
        properties.forEach((p, index) => {
            // const t = require('babel-types')  AST 节点的 Lodash 式工具库
            const key = t.isIdentifier(p.key) ? p.key.name : p.key.value
            obj[key] = traverseObjectNode(p.value)
        })
        return obj
    }
    if (node.type === 'ArrayExpression') {
        return node.elements.map(item => traverseObjectNode(item))
    }
    if (node.type === 'NullLiteral') {
        return null
    }
    return node.value
}

// 3. 写入对应目录的 *.json 文件
fs.writeFileSync(outputPageJSONPath, JSON.stringify(configObj, null, 2))

通过以上代码的注释,可以清晰的看到,通过以上三步,就可以将工程里面的 config 配置转换成小程序对应的 json 配置文件。

但是,哪怕仅仅是这一小块功能点,真正实现起来也没那么简单,你还需要考虑大量的真实业务场景及极端情况:

  • 应用入口app.js 和页面入口 index.js 的 config 是否得单独处理?
  • tabBar配置怎样转换且保证功能及交互一致?
  • 用户的配置信息有误怎样提示?

更多代码编译相关内容,还是放在下一篇吧。

总结

到此,taro-cli 的主要目录结构,命令调用,项目初始化方式等基本都捋完了,有兴趣的同学可以结合着工程的源代码自己跟一遍,应该不会太费劲。

taro-cli 目前是将模版放在工程里面的,每次更新模版都要同步更新脚手架。而 vue-cli 是将项目模板放在 git 上,运行的时候再根据用户交互下载不同的模板,经过模板引擎渲染出来,生成项目。这样将模板和脚手架分离,就可以各自维护,即使模板有变动,只需要上传最新的模板即可,而不需要用户去更新脚手架就可以生成最新的项目。 这个后期可以纳入优化的范畴。

下一篇文章,我们将一起进入 Taro 代码编译的世界。

查看原文

麦芽糖 赞了文章 · 2018-06-21

Taro 技术揭秘之taro-cli

文章同步于 Github/Blog

前言

Taro 是由凹凸实验室打造的一套遵循 React 语法规范的多端统一开发框架。

使用 Taro,我们可以只书写一套代码,再通过 Taro 的编译工具,将源代码分别编译出可以在不同端(微信小程序、H5、App 端等)运行的代码。实现 一次编写,多端运行。 关于 Taro 的更多详细的信息可以看官方的介绍文章 Taro - 多端开发框架 ,或者直接前往 GitHub 仓库 NervJS/taro 查看 Taro 文档及相关资料。

image

Taro 项目实现的功能强大,项目复杂而庞大,涉及到的方方面面(多端代码转换、组件、路由、状态管理、生命周期、端能力的实现与兼容等等)多,对于大多数人来说,想要深入理解其实现机制及原理,还是比较困难的。

Taro 技术揭秘系列文章将为你逐步揭开 Taro 强大的功能之后的神秘面纱,带领你深入 Taro 内部,了解 Taro 是怎样一步一步实现 一次编写,多端运行 的宏伟目标,同时也希望借此机会抛砖引玉,促进前端圈涌现出更多的,能够解决大家痛点的开源项目。

首先,我们将从负责 Taro 脚手架初始化和项目构建的的命令行工具,也就是 Taro 的入口:@tarojs/cli 开始。

taro-cli 包

taro 命令

taro-cli 包位于 Taro 工程的 packages 目录下,通过 npm install -g @tarojs/cli 全局安装后,将会生成一个taro 命令。主要负责项目初始化、编译、构建等。直接在命令行输入 taro ,会看到如下提示:

➜ taro
👽 Taro v0.0.63


  Usage: taro <command> [options]

  Options:

    -V, --version       output the version number
    -h, --help          output usage information

  Commands:

    init [projectName]  Init a project with default templete
    build               Build a project with options
    update              Update packages of taro
    help [cmd]          display help for [cmd]

在这里可以详细看看 taro 命令用法及作用。

包管理与发布

首先,我们需要了解 taro-cli 包与 taro 工程的关系。

将 Taro 工程 clone 下来之后,我们可以看到工程的目录结构如下,整体还是比较简单明了的。

.
├── CHANGELOG.md
├── LICENSE
├── README.md
├── build
├── docs
├── lerna-debug.log
├── lerna.json     // Lerna 配置文件
├── package.json
├── packages
│   ├── eslint-config-taro
│   ├── eslint-plugin-taro
│   ├── postcss-plugin-constparse
│   ├── postcss-pxtransform
│   ├── taro
│   ├── taro-async-await
│   ├── taro-cli
│   ├── taro-components
│   ├── taro-components-rn
│   ├── taro-h5
│   ├── taro-plugin-babel
│   ├── taro-plugin-csso
│   ├── taro-plugin-sass
│   ├── taro-plugin-uglifyjs
│   ├── taro-redux
│   ├── taro-redux-h5
│   ├── taro-rn
│   ├── taro-rn-runner
│   ├── taro-router
│   ├── taro-transformer-wx
│   ├── taro-weapp
│   └── taro-webpack-runner
└── yarn.lock

Taro 项目主要是由一系列 npm 包组成,位于工程的 packages 目录下。它的包管理方式和 Babel 项目一样,将整个项目作为一个 monorepo 来进行管理,并且同样使用了包管理工具 Lerna

Lerna 是一个用来优化托管在 git/npm 上的多 package 代码库的工作流的一个管理工具,可以让你在主项目下管理多个子项目,从而解决了多个包互相依赖,且发布时需要手动维护多个包的问题。

关于 Lerna 的更多介绍可以看官方文档 Lerna:A tool for managing JavaScript projects with multiple packages

packages 目录下十几个包中,最常用的项目初始化与构建的命令行工具 taro-cli 就是其中一个。在 Taro 工程根目录运行 lerna publish 命令之后,lerna.json 里面配置好的所有的包会被发布到 npm 上去。

目录结构

taro-cli 包的目录结构如下:

./
├── bin    // 命令行
│   ├── taro        // taro 命令
│   ├── taro-build        // taro build 命令
│   ├── taro-update        // taro update 命令
│   └── taro-init        // taro init 命令
├── package.json
├── node_modules
├── src
│   ├── build.js        // taro build 命令调用,根据 type 类型调用不同的脚本
│   ├── config
│   │   ├── babel.js    // Babel 配置
│   │   ├── babylon.js        // JavaScript 解析器 babylon 配置
│   │   ├── browser_list.js    // autoprefixer browsers 配置
│   │   ├── index.js        // 目录名及入口文件名相关配置
│   │   └── uglify.js
│   ├── creator.js
│   ├── h5.js        // 构建h5 平台代码
│   ├── project.js    // taro init 命令调用,初始化项目
│   ├── rn.js        // 构建React Native 平台代码
│   ├── util        // 一系列工具函数
│   │   ├── index.js
│   │   ├── npm.js
│   │   └── resolve_npm_files.js
│   └── weapp.js        // 构建小程序代码转换
├── templates        // 脚手架模版
│   └── default
│       ├── appjs
│       ├── config
│       │   ├── dev
│       │   ├── index
│       │   └── prod
│       ├── editorconfig
│       ├── eslintrc
│       ├── gitignore
│       ├── index.js    // 初始化文件及目录,copy模版等
│       ├── indexhtml
│       ├── npmrc
│       ├── pagejs
│       ├── pkg
│       └── scss
└── yarn-error.log

其中关键文件的作用已添加注释说明,大家可以先大概看看,有个初步印象。

通过上面的目录树可以看出,taro-cli 工程的文件并不算多,主要目录有:/bin/src/template,我已经在上面详细标注了主要的目录和文件的作用,至于具体的流程,咱们接下来再分析。

用到的核心库

  • tj/commander.jsNode.js 命令行接口全面的解决方案,灵感来自于 Ruby's commander。可以自动的解析命令和参数,合并多选项,处理短参等等,功能强大,上手简单。
  • jprichardson/node-fs-extra 在nodejs的fs基础上增加了一些新的方法,更好用,还可以拷贝模板。
  • chalk/chalk 可以用于控制终端输出字符串的样式。
  • SBoudrias/Inquirer.js NodeJs 命令行交互工具,通用的命令行用户界面集合,用于和用户进行交互。
  • sindresorhus/ora 加载中状态表示的时候一个loading怎么够,再在前面加个小圈圈转起来,成功了console一个success怎么够,前面还可以给他加个小钩钩,ora就是做这个的。
  • SBoudrias/mem-fs-editor 提供一系列API,方便操作模板文件。
  • shelljs/shelljs ShellJS 是Node.js 扩展,用于实现Unix shell 命令执行。
  • Node.js child_process 模块 用于新建子进程。子进程的运行结果储存在系统缓存之中(最大200KB),等到子进程运行结束以后,主进程再用回调函数读取子进程的运行结果。

taro init

taro init 命令主要的流程如下:

image

taro 命令入口

当我们全局安装 taro-cli 包之后,我们的命令行里就多了一个 taro 命令。

$ npm install -g @tarojs/cli

那么 taro 命令是怎样添加进去的呢,其原因在于 package.json 里面的 bin 字段;

"bin": {
    "taro": "bin/taro"
  },

上面代码指定,taro 命令对应的可执行文件为 bin/taro。npm 会寻找这个文件,在 [prefix]/bin 目录下建立符号链接。在上面的例子中,taro会建立符号链接 [prefix]/bin/taro。由于 [prefix]/bin 目录会在运行时加入系统的 PATH 变量,因此在运行 npm 时,就可以不带路径,直接通过命令来调用这些脚本。

关于prefix,可以通过npm config get prefix获取。

$ npm config get prefix
/usr/local

通过下列命令可以更加清晰的看到它们之间的符号链接:

$ ls -al `which taro`
lrwxr-xr-x  1 chengshuai  admin  40  6 15 10:51 /usr/local/bin/taro -> ../lib/node_modules/@tarojs/cli/bin/taro

taro 子命令

上面我们已经知道 taro-cli 包安装之后,taro 命令是怎么和 /bin/taro 文件相关联起来的, 那 taro init 和 taro build 又是怎样和对应的文件关联起来的呢?

命令关联与参数解析

这里就不得不提到一个有用的包:tj/commander.jsNode.js 命令行接口全面的解决方案,灵感来自于 Ruby's commander。可以自动的解析命令和参数,合并多选项,处理短参等等,功能强大,上手简单。具体的使用方法可以参见项目的 README。

更主要的,commander 支持 git 风格的子命令处理,可以根据子命令自动引导到以特定格式命名的命令执行文件,文件名的格式是 [command]-[subcommand],例如:

taro init => taro-init
taro build => taro-build

/bin/taro 文件内容不多,核心代码也就那几行 .command() 命令:

#! /usr/bin/env node

const program = require('commander')
const {getPkgVersion} = require('../src/util')

program
  .version(getPkgVersion())
  .usage('<command> [options]')
  .command('init [projectName]', 'Init a project with default templete')
  .command('build', 'Build a project with options')
  .command('update', 'Update packages of taro')
  .parse(process.argv)

command方法

用法:.command('init <path>', 'description')

command的 用法稍微复杂,原则上他可以接受三个参数,第一个为命令定义,第二个命令描述,第三个为命令辅助修饰对象。

  • 第一个参数中可以使用 <> 或者 [] 修饰命令参数
  • 第二个参数可选。

    • 当没有第二个参数时,commander.js 将返回 Command 对象,若有第二个参数,将返回原型对象。
    • 当带有第二个参数,并且没有显示调用 action(fn) 时,则将会使用子命令模式
    • 所谓子命令模式即,./pm./pm-install./pm-search等。这些子命令跟主命令在不同的文件中。
  • 第三个参数一般不用,它可以设置是否显示的使用子命令模式。
注意第一行#!/usr/bin/env node,有个关键词叫 Shebang,不了解的可以去搜搜看。

参数解析及与用户交互

前面提到过,commander 包可以自动解析命令和参数,在配置好命令之后,还能够自动生产 help(帮助) 命令和 version(版本查看) 命令。并且通过program.args便可以获取命令行的参数,然后再根据参数来调用不同的脚本。

但当我们运行 taro init 命令后,如下所示的命令行交互又是怎么实现的呢?

$ taro init taroDemo
Taro即将创建一个新项目!
Need help? Go and open issue: https://github.com/NervJS/taro/issues/new

Taro v0.0.50

? 请输入项目介绍!
? 请选择模板 默认模板

这里使用的是SBoudrias/Inquirer.js 来处理命令行交互。

用法其实很简单:

const inquirer = require('inquirer')  // npm i inquirer -D

if (typeof conf.description !== 'string') {
      prompts.push({
        type: 'input',
        name: 'description',
        message: '请输入项目介绍!'
      })
}

prompt()接受一个问题对象的数据,在用户与终端交互过程中,将用户的输入存放在一个答案对象中,然后返回一个Promise,通过then()获取到这个答案对象。so easy!

借此,新项目的名称、版本号、描述等信息可以直接通过终端交互插入到项目模板中,完善交互流程。

当然,交互的问题不仅限于此,可以根据自己项目的情况,添加更多的交互问题。inquirer.js强大的地方在于,支持很多种交互类型,除了简单的input,还有confirmlistpasswordcheckbox等,具体可以参见项目的工程README

此外,你还在执行异步操作的过程中,你还可以使用 sindresorhus/ora 来添加一下 loading 效果。使用chalk/chalk 给终端的输出添加各种样式。

模版文件操作

最后就是模版文件操作了,主要分为两大块:

  • 将输入的内容插入到模板中
  • 根据命令创建对应目录结构,copy 文件
  • 更新已存在文件内容

这些操作基本都是在 /template/index.js 文件里。

这里还用到了shelljs/shelljs 执行shell 脚本,如初始化 git git init,项目初始化之后安装依赖npm install等。

拷贝模板文件

拷贝模版文件主要是使用 jprichardson/node-fs-extracopyTpl()方法,此方法使用ejs模板语法,可以将输入的内容插入到模版的对应位置:

this.fs.copyTpl(
      project,
      path.join(projectPath, 'project.config.json',
      {description,projectName}
    );

更新已经存在的文件内容

更新已经存在的文件内容是很复杂的工作,最可靠的方法是把文件解析为AST,然后再编辑。一些流行的 AST parser 包括:

  • Cheerio:解析HTML
  • Babylon:解析JavaScript
  • 对于JSON文件,使用原生的JSON对象方法。

使用 Regex 解析一个代码文件是邪道,不要这么干,不要心存侥幸。

taro build

taro build 命令是整个 taro 项目的灵魂和核心,主要负责 多端代码编译(h5,小程序,React Native等)。

taro 命令的关联,参数解析等和 taro init 其实是一模一样的,那么最关键的代码转换部分是怎样实现的呢?

这个部分内容过于庞大,需要单独拉出来一篇讲。不过这里可以先简单提一下。

编译工作流与抽象语法树(AST)

Taro 的核心部分就是将代码编译成其他端(H5、小程序、React Native等)代码。一般来说,将一种结构化语言的代码编译成另一种类似的结构化语言的代码包括以下几个步骤:

image

首先是 parse,将代码 解析(Parse)抽象语法树(Abstract Syntex Tree),然后对 AST 进行 遍历(traverse)替换(replace)(这对于前端来说其实并不陌生,可以类比 DOM 树的操作),最后是 生成(generate),根据新的 AST 生成编译后的代码。

Babel 模块

Babel 是一个通用的多功能的 JavaScript 编译器,更确切地说是源码到源码的编译器,通常也叫做 转换编译器(transpiler)。 意思是说你为 Babel 提供一些 JavaScript 代码,Babel 更改这些代码,然后返回给你新生成的代码。

此外它还拥有众多模块可用于不同形式的 静态分析

静态分析是在不需要执行代码的前提下对代码进行分析的处理过程 (执行代码的同时进行代码分析即是动态分析)。 静态分析的目的是多种多样的, 它可用于语法检查,编译,代码高亮,代码转换,优化,压缩等等场景。

Babel 实际上是一组模块的集合,拥有庞大的生态。Taro 项目的代码编译部分就是基于 Babel 的以下模块实现的:

  • babylon Babylon 是 Babel 的解析器。最初是 从Acorn项目fork出来的。Acorn非常快,易于使用,并且针对非标准特性(以及那些未来的标准特性) 设计了一个基于插件的架构。
  • babel-traverse Babel Traverse(遍历)模块维护了整棵树的状态,并且负责替换、移除和添加节点。
  • babel-types Babel Types模块是一个用于 AST 节点的 Lodash 式工具库, 它包含了构造、验证以及变换 AST 节点的方法。 该工具库包含考虑周到的工具方法,对编写处理AST逻辑非常有用。
  • babel-generator Babel Generator模块是 Babel 的代码生成器,它读取AST并将其转换为代码和源码映射(sourcemaps)。
  • babel-template babel-template 是另一个虽然很小但却非常有用的模块。 它能让你编写字符串形式且带有占位符的代码来代替手动编码, 尤其是生成的大规模 AST的时候。 在计算机科学中,这种能力被称为准引用(quasiquotes)。

解析页面 config 配置

在业务代码编译成小程序的代码过程中,有一步是将页面入口 js 的 config 属性解析出来,并写入 *.json 文件,供小程序使用。那么这一步是怎么实现的呢,这里将这部分功能的关键代码抽取出来:

// 1. babel-traverse方法, 遍历和更新节点
traverse(ast, {  
    ClassProperty(astPath) { // 遍历类的属性声明
        const node = astPath.node
        if (node.key.name === 'config') { // 类的属性名为 config
            configObj = traverseObjectNode(node)
            astPath.remove() // 将该方法移除掉
        }
    }
})

// 2. 遍历,解析为 JSON 对象
function traverseObjectNode(node, obj) { 
    if (node.type === 'ClassProperty' || node.type === 'ObjectProperty') {
        const properties = node.value.properties
        obj = {}
        properties.forEach((p, index) => {
            obj[p.key.name] = traverseObjectNode(p.value)
        })
        return obj
    }
    if (node.type === 'ObjectExpression') {
        const properties = node.properties
        obj = {}
        properties.forEach((p, index) => {
            // const t = require('babel-types')  AST 节点的 Lodash 式工具库
            const key = t.isIdentifier(p.key) ? p.key.name : p.key.value
            obj[key] = traverseObjectNode(p.value)
        })
        return obj
    }
    if (node.type === 'ArrayExpression') {
        return node.elements.map(item => traverseObjectNode(item))
    }
    if (node.type === 'NullLiteral') {
        return null
    }
    return node.value
}

// 3. 写入对应目录的 *.json 文件
fs.writeFileSync(outputPageJSONPath, JSON.stringify(configObj, null, 2))

通过以上代码的注释,可以清晰的看到,通过以上三步,就可以将工程里面的 config 配置转换成小程序对应的 json 配置文件。

但是,哪怕仅仅是这一小块功能点,真正实现起来也没那么简单,你还需要考虑大量的真实业务场景及极端情况:

  • 应用入口app.js 和页面入口 index.js 的 config 是否得单独处理?
  • tabBar配置怎样转换且保证功能及交互一致?
  • 用户的配置信息有误怎样提示?

更多代码编译相关内容,还是放在下一篇吧。

总结

到此,taro-cli 的主要目录结构,命令调用,项目初始化方式等基本都捋完了,有兴趣的同学可以结合着工程的源代码自己跟一遍,应该不会太费劲。

taro-cli 目前是将模版放在工程里面的,每次更新模版都要同步更新脚手架。而 vue-cli 是将项目模板放在 git 上,运行的时候再根据用户交互下载不同的模板,经过模板引擎渲染出来,生成项目。这样将模板和脚手架分离,就可以各自维护,即使模板有变动,只需要上传最新的模板即可,而不需要用户去更新脚手架就可以生成最新的项目。 这个后期可以纳入优化的范畴。

下一篇文章,我们将一起进入 Taro 代码编译的世界。

查看原文

赞 48 收藏 34 评论 0

麦芽糖 关注了用户 · 2018-04-24

宝贝 @lueluelue_59fd7723a5676

邮箱804316947@qq.com

关注 19

麦芽糖 赞了文章 · 2018-04-17

淘宝新势力周H5性能优化实战

前言

淘宝新势力周(春上新)是命运石kimi链路(H5链路)第一次承接S级大促,面对S级大促内容丰富且复杂的页面需求,kimi链路遇到了一些性能问题,在未进行性能优化之前,搭建出来的页面,业务方普遍反馈页面卡顿严重,无法滑动。

因为时髦女装会场是反馈比较严重的页面之一,所以我以时髦女装会场为例子,介绍下这次性能优化的具体方案。时髦女装会场页面模块在18个左右,页面上的img标签数量在200左右,页面总长度 31208px,以iPhone6页面一屏736px为标准,总共能分为42.4屏左右。为什么我要特别把img标签写出来呢?因为这次的性能卡顿主要的原因是因为错误使用图片懒加载引起的。

通过performance图排查性能问题

现代的web性能优化离不开chrome devtool里performance的帮助,我们先来看一张未优化之前 performance的截图

未优化前

这张performance图我们主要看三个部分:第一个是最上面FPS红线的部分,红线代表着这段时间内未达到60FPS每帧;第二部分是Frames的耗时,勾选了Screenshots后我们能看到每帧的耗时;第三部分是下面函数耗时,我们能从函数耗时里分析出来到底是哪段代码block住了页面渲染,导致卡帧。

从上面的图可以看到最长的一帧耗时3.37秒,这导致FPS都快接近0了。

把函数耗时图拉大分析里面耗时最长的函数,可以看到耗时最长的函数是inview函数,这个函数是图片懒加载库里面检查当前图片是否在屏幕中间的函数。

图片懒加载库的基本逻辑是:当调用初始化函数时立即检查当前页面上所有未真正加载的图片,判断是否需要加载。当页面进行滑动时,重复检查所有图片的逻辑。

这次性能问题的原因和解决方案

卡顿掉帧的原因:这次搭建出来的页面使用的是外包同学开发的业务模块,在模块内部手动调用了lazyLoad初始函数,所以每初始化一个模块就会立即检查所有未加载图片,当页面上图片数量不断增长的时候,inview函数的耗时也不断增加,检查一个图片是否在页面的耗时是2ms~5ms,如果页面中有100个图片未加载当页面滑动时每一次检查会耗时200ms~500ms,如果检查是同步操作的话,掉帧几乎无法避免。

优化方案:之前的其他链路的优化方案是模块懒加载,然后lazyload统一调用,但是因为这次离上线时间较紧张,让外包返工改模块风险较大,于是有另外的一个优化方案:图片懒加载库的异步化,只要避免函数执行耗时过长阻塞渲染,就能避免卡帧,假设我们有100张图片,我们分多批次进行检查,避免一次检查所有图片阻塞渲染。另外针对模块初始化时频繁的检查所有图片的问题,我们给这段逻辑加上debounce函数和图片缓存队列。

优化的过程

优化1.0:

在我接手之前,有一版优化是将模块的渲染通过setTimeout函数改成异步的;这个优化是几乎没有效果的,优化后页面依然卡顿掉帧,因为这个优化并没有找到页面卡顿的原因。起码也应该将setTimeout改成RAF。当然模块的延时加载并不能解决卡顿问题,但是模块的懒加载能解决一部分问题。下面我们看一张使用模块懒加载后的performance图

模块懒加载后,一长条红色块已经变成了短条的红色块,但是因为模块内部单独使用图片懒加载导致频繁检查所有图片是否在可视范围内的问题还是没有得到解决,最长的一帧达到855ms,依然存在掉帧。

优化2.0:

图片懒加载异步版本:通过对图片懒加载库的改造,1、初始化时加上debound优化和图片缓存队列,2、分批检查图片。我们在看一下优化后的performance图

红色的条块也消失,看下面函数执行变的又长有尖,这是因为检查图片的操作变成异步分批了。

图片懒加载库改造时遇到的问题:

在将图片懒加载改造成异步的时候遇到了一个问题,就和Java多线程一样,很多时候异步我们也希望是有序的异步。

分批检查的有序是比较容易保证的,将图片分成多批,一批一批进行,再最后一批结束任务。但是问题出在分批检查和图片懒加载模块初始化存在交替运行的情况,而这两个任务都会改变一个变量。如果不能解决这个问题,就会出现图片有时候能正常加载,有时候加载不出来的情况。所以有说法是,大部分偶现的问题都是异步并行的问题。

解决这个问题的思路也比较常见,就是通过锁,当一个操作异步变量的任务开启,我们的锁自增1,完成异步任务时自减,图片懒加载库的图片缓存初始队列等到异步锁释放后再进行检查,否则存入缓存队列,等待下一帧再检查。

总结

优化过后,对应常见的机型基本能保证页面流畅不卡顿。chrome的performance图基本上和真机操作的情况保持一致,如果performance出现掉帧,那iPhone6s上和android上基本也会出现掉帧,但是iPhone7以上的机器却可能感受不明显。通过performance能够快速定位掉帧的问题,通过解决这些问题实质性的优化页面性能,而不是通过猜测进行无效优化。

查看原文

赞 37 收藏 54 评论 4

麦芽糖 提出了问题 · 2018-04-13

mongoose 关联模糊匹配查找怎么写

假设有表 Group User,且 Group 的 owner 关联 User

Group
_id
name
owner: { type: Schema.ObjectId, ref: 'User' }

User
_id
name

问: 搜索查找 Group 列表,根据 Group name 和 User name 来模糊查找,且有 count 的分页形式?


如果条件只有 Group name,我会写

var reg = new RegExp(searchText, i);

var q = {name: {$regex: reg}};

Group.count(q, function(err, count){
    Group.find(q).skip(skip).limit(limit).exec(function(err, groups){
        return res.status(200).json({
            count: count,
            groups: groups
        })
    })
})

关注 2 回答 0

麦芽糖 赞了文章 · 2018-03-30

React v16.3.0: New lifecycles and context API

几天前,我们写了一篇关于即将到来的对我们的传统生命周期方法的变更的文章,包括逐步迁移策略。在React 16.3.0中,我们添加了一些新的生命周期方法来帮助迁移。我们还引入了新的API,用于长时间请求的特性:一个官方的上下文API、一个ref转发API和一个更语意化的ref API。

请继续阅读,了解更多关于这个版本的信息。

官方认证的 Context API

多年来,React为Context提供了一个实验性的API。虽然它是一个强大的工具,但是由于API中固有的问题,它的使用是不受欢迎的,因此我们打算用一个更好的API来替代这实验性的API。

React 16.3引入了一个新的Context API,它更高效,同时支持静态类型检查和深度更新。

注意
旧的ContextAPI 将继续保留到React 16.x,所以您将有时间迁移。

下面是一个示例,说明如何使用新的上下文API注入“主题”:

## by 司徒正美
const ThemeContext = React.createContext('light');

class ThemeProvider extends React.Component {
  state = {theme: 'light'};

  render() {
    return (
      <ThemeContext.Provider value={this.state.theme}>
        {this.props.children}
      </ThemeContext.Provider>
    );
  }
}

class ThemedButton extends React.Component {
  render() {
    return (
      <ThemeContext.Consumer>
        {theme => <Button theme={theme} />}
      </ThemeContext.Consumer>
    );
  }
}

createRef API

以前,React提供了两种管理refs的方法:字符串ref API和回调ref API。尽管字符串ref API比较方便,但是它有几个缺点,所以我们的官方推荐是使用回调ref。

React 16.3为管理refs提供了一个新的方案,它为字符串ref提供了方便,并且没有任何缺点:

## by 司徒正美

class MyComponent extends React.Component {
  constructor(props) {
    super(props);

    this.inputRef = React.createRef();
  }

  render() {
    return <input type="text" ref={this.inputRef} />;
  }

  componentDidMount() {
    this.inputRef.current.focus();
  }
}
注意

除了新的createRef API外,回调refs将继续得到支持。

您不需要在组件中替换回调refs。它们稍微灵活一些,因此它们将继续作为一个高级特性。

forwardRef API

高阶组件(或HOCs)是在组件之间重用代码的常用方法。基于上面的主题上下文示例,我们可能会创建一个临时对象,将当前的“主题”作为一个属性注入:

## by 司徒正美

function withTheme(Component) {
  return function ThemedComponent(props) {
    return (
      <ThemeContext.Consumer>
        {theme => <Component {...props} theme={theme} />}
      </ThemeContext.Consumer>
    );
  };
}

我们可以使用上述特殊的方式将组件连接到主题上下文,而不必直接使用主题上下文。例如:

## by 司徒正美

class FancyButton extends React.Component {
  buttonRef = React.createRef();

  focus() {
    this.buttonRef.current.focus();
  }

  render() {
    const {label, theme, ...rest} = this.props;
    return (
      <button
        {...rest}
        className={`${theme}-button`}
        ref={this.buttonRef}>

        {label}
      </button>
    );
  }
}

const FancyThemedButton = withTheme(FancyButton);

// We can render FancyThemedButton as if it were a FancyButton
// It will automatically receive the current "theme",
// And the HOC will pass through our other props.
<FancyThemedButton
  label="Click me!"
  onClick={handleClick}
/>;

HOCs通常会将props传递给它们包装的组件。不幸的是,refs没有冲透进去。这意味着如果我们使用FancyThemedButton,我们就不能将ref添加到FancyButton中,因此我们无法调用focus()。

新的代理API通过提供一种方法来拦截一个ref,并将其转发为一个普通的props,从而解决了这个问题:

## by 司徒正美

function withTheme(Component) {
  // Note the second param "ref" provided by React.forwardRef.
  // We can attach this to Component directly.
  function ThemedComponent(props, ref) {
    return (
      <ThemeContext.Consumer>
        {theme => (
          <Component {...props} ref={ref} theme={theme} />
        )}
      </ThemeContext.Consumer>
    );
  }

  // These next lines are not necessary,
  // But they do give the component a better display name in DevTools,
  // e.g. "ForwardRef(withTheme(MyComponent))"
  const name = Component.displayName || Component.name;
  ThemedComponent.displayName = `withTheme(${name})`;

  // Tell React to pass the "ref" to ThemedComponent.
  return React.forwardRef(ThemedComponent);
}

const fancyButtonRef = React.createRef();

// fancyButtonRef will now point to FancyButton
<FancyThemedButton
  label="Click me!"
  onClick={handleClick}
  ref={fancyButtonRef}
/>;

组件生命周期钩子的变化

React的类组件API已经存在多年,几乎没有变化。但是,当我们为更高级的特性(例如错误边界和即将到来的异步渲染模式)添加支持时,我们以它本来没有打算的方式来扩展这个模型。

例如,在当前的API中,用一些非寻常的手段来阻止初始渲染是很容易的。在某种程度上,这是因为有太多的钩子来完成这项既定的任务,而且还不清楚哪一个是最好的。我们已经注意到错误处理的中断行为通常不会被考虑,并且可能导致内存泄漏(这也会影响即将到来的异步渲染模式)。当前的类组件API也使其他的工作变得复杂,比如我们的代码优化器(Prepack)的工作。

componentWillMount, componentWillReceiveProps, componentWillUpdate这些钩子很容易引发问题,并且也严重扰乱React的生命周期。基于这些原因,我们将废弃这些方法,以支持更好的替代方案。

我们认识到这一变化将影响许多现有的组件。因此,迁移路径将尽可能平缓,并提供迁移方案。(在Facebook,我们拥有5万多个React组件。我们也依赖于一个渐进的发布周期!

注意

弃用警告将在React16以后的版本中启用, 一直保留到17发布时。

即使在React17中,仍然可以使用它们,但是它们将添加“UNSAFE_”前缀,以表明它们可能导致问题。我们还准备了一个自动化的脚本,以便现有代码中重命名它们。

除了废弃不安全的生命周期钩子外,我们还增加了一些新的生命周期钩子:

getDerivedStateFromProps 用来componentWillReceiveProps。

getSnapshotBeforeUpdate,用在更新前从DOM中安全地读取属性。

StrictMode 组件

<StrictMode />是一种专门用于暴露潜在问题的工具。与<Fragment />一样,<StrictMode/>将 不会渲染到视图中。它能为其子组件激活额外的检查和警告。

注意

<StrictMode />检查只在开发模式下运行;它们不会影响生产构建。

虽然严格的模式不可能捕获所有的问题(例如某些类型的窜改),但它可以帮助很多人。如果您在严格的模式下看到警告,这些事情很可能会导致异步渲染的错误。

在16.3版本中,StrictMode帮助:

  1. 识别具有不安全生命周期钩子的组件。
  2. 关于遗留字符串ref API用法的警告。
  3. 检测意想不到的副作用
查看原文

赞 21 收藏 29 评论 6

认证与成就

  • 获得 97 次点赞
  • 获得 64 枚徽章 获得 3 枚金徽章, 获得 21 枚银徽章, 获得 40 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

注册于 2015-01-13
个人主页被 2.2k 人浏览