对于前端来说, github
就是宝藏。做任何事情,一定要专业,很多知识都是可以找到的,尤其在前端,有很多很好的东西就摆在你的面前。好的组件源代码,好的设计模式,好的测试方案,好的代码结构,你都可以触手可及,所以不要觉得不会,其实 coding just api
,你需要掌握的是编程的思想和思维。
最近 ant design
彩蛋事件,这个彩蛋足够刺激,以至于大家反应这么强烈。足以说明 ant design
的受欢迎程度,按照土话说, ant design
以前的身份是:大家只爱不恨,但是现在的身份是:大家又爱又恨。想一下,有个故事是这样说的:
出了问题,该怎么解决,就怎么解决。但是逼还是要撕的,谁的锅谁背好。
故事是这样的:
比如平常在公司工作,同事或者其他人闯祸了,把你的代码 reset
掉了。这肯定波及到你的工作了,这个时候你会怎么做?你肯定不爽,肯定会BB。尤其遇到那种闯了祸,影响到了别人工作的还不主动背锅道歉,摆出一副你把代码找回来不就行了么的态度。遇到这种人你肯定就很不爽,要找这个人撕逼。毕竟你已经影响到我工作了,别一副好像锅不是自己的一样,锅你背好,我会解决掉你给我带来的问题,下次别再这样了。
而 ant design
,就好比上面闯祸的同事,波及到了大家,但是 ant
也主动认错了,锅也主动背了,也立刻给出了方案。
其实对于那些因为这个事情导致失业什么的,我个人认为还是比较难受的。但是对于那些说话比较激烈(难听)的人,也就是嘴上难听,有几个会因为前端框架而上升到很大的那种怨恨的,难听的目的无非就是隐式的鞭策 ant
团队。我想 ant
也意识到了,后面肯定不会再这样做类似这种事情了。
既然我们从一开始就选择了相信 ant design
,那我们就多一份包容,包容这一次 ant design
的犯错,不要因为一次犯错,就否定其全部。
其实你在公司里,也是这样的,你犯了错,影响到了很多同事,你意识到事情的严重性,你很难受,很后悔,你发现自己做了一件极其愚蠢的事情,你真的很想去弥补,但是时间不能倒退,岁月不能回流,你能做的就是保证下次不会再次犯错,你很想得到大家的原谅和信任。虽然你是真心认错的,希望大家可以像原来一样信任你,可是如果大家因为你一次错误,就在举止谈吐之间表现的不那么相信你了。那,此时你的心,也一定是极其的失落和灰冷吧。
所以我还是希望大家能继续对 ant design
保持信任,包容 ant design
一次,也是包容一次 偏右 这种为开源做出很大贡献的人。
其实,在生活中,有时候,我们会发现,包容不需要很多次的,一次包容就可以了。因为一次包容就可以让一件事情再也不会发生第二次。是不,啰啰嗦嗦了那么多,其实答案就在文字中。
好了,不胡诌个人看法了。下面进入正题,其实这次的文章也和 ant
彩蛋有点关系。因为有人说,谁让你不去阅读 npm
包源码的,可能很多人觉得阅读 npm
包的源码是一件很困难的事情,但是我要告诉你们, npm
包对前端来说就是一座宝藏。你可以从 npm
包中看到很多东西的真相,你可以看到全世界的最优秀的 npm
包的编程思想。
比如你可以看到他们的代码结构,他们的依赖关系,他们的代码交互方式,他们的代码编写规范,等等等等。那么现在,我就通过目前最火的多端统一框架 taro
来向大家展示,如何去分析一个通过 CLI
生成的 npm
包的代码。一片文章做不到太细致的分析,我就当是抛砖引玉,告诉大家,不要被 node_modules
那一串串的包吓到了,不敢去看,怕看不懂。其实不是你们想的那样看不懂,一般有名的 npm
包,代码结构都是很友好的,理解起来并不比你去阅读你同事的代码(你懂的)难。而且在阅读 npm
包的过程中,你会发现很多惊喜,找到很多灵感。是不是很激动,是不是很开心,嗯,那就牵着我的手,跟着我一起走,我带你去解开 npm
包那神秘而又美丽的面纱。
执行 taro init xxx
后,package.json的依赖如下图所示
你会发现当你初始化完一个 CLI
时,安装了很多依赖,然后这个时候如果你去看 node_modules
,一定会很难受,因为安装了很多很多依赖的包,这也是很多人点开 node_modules
目录后,立马就关上的原因,不关可能就卡主了:joy:。那么我们玩点轻松的,不搞这么多,我们进入裸奔模式,一个一个包下载,按照 taro init
的 package.json
的安装,我们来分析一下其中的包的代码。
对node_modules进行截图,图片如下:
从图片里面我们可以看到安装了很多依赖,其中和我们有着直接相关的包是 @tarojs
,打开 @tarojs
可以看到:
其实你会发现没什么东西,我们再看一下src目录下有什么:
我们看一下, index.js
文件:
import 'weui' export { default as View } from './view' export { default as Block } from './block' export { default as Image } from './image' export { default as Text } from './text' export { default as Switch } from './switch' export { default as Button } from './button' export { default as Icon } from './icon' export { default as Radio } from './radio' export { default as Input } from './input' export { default as ScrollView } from './scroll-view' export { Swiper, SwiperItem } from './swiper' export { default as Checkbox } from './checkbox' export { default as Picker } from './picker' export { default as Label } from './label' export { default as Textarea } from './textarea' export { default as Slider } from './slider' export { default as Video } from './video' export { default as Audio } from './audio' export { default as Camera } from './camera' export { default as Progress } from './progress' export { default as RichText } from './rich-text' export { default as Form } from './form' export { default as RadioGroup } from './radio/radio-group' export { default as CheckboxGroup } from './checkbox/checkbox-group' export { default as Tabbar } from './tabbar' export { default as TabbarContainer } from './tabbar/container' export { default as TabbarPanel } from './tabbar/panel' // export { default as Navigator } from './navigator' 复制代码
你会发现,这是一个集中 export
各种组件的地方,从这里的代码我们可以知道,为什么在 taro
里面要通过下面这种形式去引入组件。
import { View, Text, Icon } from '@tarojs/components' 复制代码
比如为什么要大写,这是因为上面 export
出去的就是大写,同时把所有组件放在了一个对象里面。这里再思考一下,为什么要大写呢?可能是因为避免和微信小程序的原生组件的命名冲突,毕竟 taro
是支持原生和 taro
混写的,如果都是小写,那怎么区分呢。当你看到这里的源码的时候,你对 taro
的组件引入需要大写这个规则是不是就觉得非常的顺其自然了。同时这里我们应该多去体会一下 taro 这样导出一个组件的思想。越是这种频繁但不起眼的操作,我们越应该去体会其优秀的思想。
下面我们来挑一个组件看一下结构,比如 Button
组件,结构如下:
从上图我们可以看到一个 taro
的基础组件的代码结构,从这里我们可以获取到几点信息:
第一点:对每个组件进行了单元测试,使用的是 Jest
,目录是 __test__
第二点:每个组件都有 index.md
,用来介绍组件的文档
第三点: 样式单独用了目录 style
来存放,同时入口文件名字统一使用 index
鉴于 taro
是一个最新的正在崛起的非常有潜力的框架,我们是不是能从 taro
的源码中学到一些思想。比如我们去设计一个我们自己的组件库时,是不是可以借鉴这种思想。其实这种组件的代码结构形式时目前很流行的,使用了今年最流行的框架 Jest
框架作为组件的单元测试。
现在和我们有着直接关系的包已经简单介绍了一遍。具体组件代码内容就不介绍了。主要介绍为什么要这样设计。下面我来介绍一下,在 yarn add @tarojs/components
后,还安装了哪些依赖。
你会发现,这个还是安装在了 @tarojs
目录下,并没有增加其他依赖。 taro
的目录结构如下图所示
从图中的代码结构我们大概可以知道东西:
第一: types
目录下有一个 index.d.ts
,这个文件是一个 ts
文件,他的作用是编写代码提示。这样在你写代码的时候,会给你非常友好的代码规范提示。比如 index.d.ts
里面有段代码(随便截取了一段)如下:
interface PageConfig { /** * 导航栏背景颜色,HexColor * default: #000000 */ navigationBarBackgroundColor?: string, backgroundTextStyle?: 'dark' | 'light', /** * 是否开启下拉刷新 * default: false */ enablePullDownRefresh?: boolean, /** * 页面上拉触底事件触发时距页面底部距离,单位为px * default: 50 */ onReachBottomDistance?: number /** * 设置为 true 则页面整体不能上下滚动;只在页面配置中有效,无法在 app.json 中设置该项 * default: false */ disableScroll?: boolean } 复制代码
这段代码的目的是在你写对应的配置时,会提示你此字段的数据类型时什么。给你一个友好的提示。看到这里,其实我们想。我们自己也可以自定义的给自己的项目加上这种提示。整体上来说对项目还是有很好的作用的。
第二:我们看到了 dist
目录,基本能推测出这是通过打包工具,打包出来的输出目录。 第三:整个目录很简单, taro
的作用是什么呢,其实 taro
是一个运行时。
我们来看一下 package.json
,如下图所示
发现有个字段,就是
"peerDependencies": { "nervjs": "^1.2.17" } 复制代码
平常我们用到的最多的就是 dependencies
和 devDependencies
。那么 peerDependencies
表达什么意识呢?我们去谷歌翻译一下,如图所示:
拆开翻译后,是 对等依赖 ,结合翻译来说一下整个字段的作用,其实就是指:
整个依赖不需要在自己的目录下 npm install
了。只需在根目录下 npm install
就可以了。本着不造轮子的精神,具体意识请看下面blog:
探讨npm依赖管理之peerDependencies
我们来看一下 index.js
, 就两行代码:
module.exports = require('./dist/index.js').default module.exports.default = module.exports 复制代码
不过我对于这种写法还是有点惊喜的。为什么要写成这样呢,不能一行搞定么,更加解耦? 大概是为了什么吧。
我们看一下 taro/src
我们看一下 env.js
export const ENV_TYPE = { WEAPP: 'WEAPP', WEB: 'WEB', RN: 'RN', SWAN: 'SWAN', ALIPAY: 'ALIPAY', TT: 'TT' } export function getEnv () { if (typeof wx !== 'undefined' && wx.getSystemInfo) { return ENV_TYPE.WEAPP } if (typeof swan !== 'undefined' && swan.getSystemInfo) { return ENV_TYPE.SWAN } if (typeof my !== 'undefined' && my.getSystemInfo) { return ENV_TYPE.ALIPAY } if (typeof tt !== 'undefined' && tt.getSystemInfo) { return ENV_TYPE.TT } if (typeof global !== 'undefined' && global.__fbGenNativeModule) { return ENV_TYPE.RN } if (typeof window !== 'undefined') { return ENV_TYPE.WEB } return 'Unknown environment' } 复制代码
从上面代码里面,我们可以看到,通过 getEnv
函数来拿到我们当前项目的运行时的环境,比如是 weapp
还是 swan
还是 tt
等等。其实这时我们就应该感觉到多端统一的思想, genEnv
做了一件很重要的事情。就是使用 taro
框架编写代码后,如何转换成多端,其实就是在运行时根据环境切换到对应的编译环境,这个 getEnv
函数就可以形象说明这一转换过程。
下面我们继续看一下 index.js
,很有趣的~,代码如下:
/* eslint-disable camelcase */ import Component from './component' import { get as internal_safe_get } from './internal/safe-get' import { set as internal_safe_set } from './internal/safe-set' import { inlineStyle as internal_inline_style } from './internal/inline-style' import { getOriginal as internal_get_original } from './internal/get-original' import { getEnv, ENV_TYPE } from './env' import Events from './events' import render from './render' import { noPromiseApis, onAndSyncApis, otherApis, initPxTransform } from './native-apis' const eventCenter = new Events() export { Component, Events, eventCenter, getEnv, ENV_TYPE, render, internal_safe_get, internal_safe_set, internal_inline_style, internal_get_original, noPromiseApis, onAndSyncApis, otherApis, initPxTransform } export default { Component, Events, eventCenter, getEnv, ENV_TYPE, render, internal_safe_get, internal_safe_set, internal_inline_style, internal_get_original, noPromiseApis, onAndSyncApis, otherApis, initPxTransform } 复制代码
可以看到,分别用 export
和 export default
导出了相同的模块集合。这样做的原因是什么呢,我个人认为是为了代码的健壮性。你可以通过一个上下文挂载所有导出,也可以通过解构去导入你想要的指定导出。看到这,我们是不是也可以在自己的项目中这样实践呢。
马不停蹄,我们来看一下两个比较重要但代码量很少的文件,一个是 render.js
,另一个是component.js。 代码如下: render.js
export default function render () {} 复制代码
component.js
class Component { constructor (props) { this.state = {} this.props = props || {} } } export default Component 复制代码
代码量都很少,一个空的 render
函数,一个功能很少的 Componet
类,想想就知道是干啥的了。
我们再看一下 events.js
,伪代码(简写)如下:
class Events { constructor() { // ... } on() {} once() {} off() {} trigger() {} } export default Events 复制代码
你会发现这个文件完成了 taro
的全局消息通知机制。它 有 on
, once
, off
, trigger
方法, events.js
里都有相应的完整代码实现。对应官方文档如下:
Taro消息机制
想一想,你是不是发现 API
原来是这么来的,也不是那么的难理解了,也不用死记硬背了。
下面我们继续分析,我们还要关注一下 internal
目录,这个目录有介绍,看 internal
目录下的 README.md
就可以知道:其是导出以 internal_
开头命名的函数,用户不需要关心也不会使用到的内部方法,在编译期会自动给每个使用 taro-cli
编译的文件加上其依赖并使用。例如:
import { Component } from 'taro' class C extends Component { render () { const { todo } = this.state return ( <TodoItem id={todo[0].list[123].id} /> ) } } 复制代码
会被编译成:
import { Component, internal_safe_get } from 'taro' class C extends Component { $props = { TodoItem() { return { $name: "TodoItem", id: internal_safe_get(this.state, "todo[0].list[123].id"), } } } ... } 复制代码
在编译期会自动给每个使用 taro-cli
编译的文件加上其依赖并使用。这句话是什么意识呢?可能是taro-cli在编译的时候,需要通过这种方式对文件进行相应的处理。目前我暂时这样理解,了解不了很正常,继续往下面分析。
tarojs/taro
已经分析的差不多了,从分析中,我们较为整体的知道了,一个运行时是在宏观上是如何去衔接多端的,如何通过 ts
文件给代码添加友好提示。既然有 internal
,那就意味着不是 internal
目录下的文件都可以对外提供方法,比如 events.js
,这也可以给我们启发。如何去界定对内对外的代码,如何去分割。
yarn add @tarojs/taro-weapp && nervjs && nerv-devtools -S 复制代码
看一下最新的包结构
对应的 package.json
如下:
{ "dependencies": { "@tarojs/components": "^1.2.1", "@tarojs/router": "^1.2.2", "@tarojs/taro": "^1.2.1", "@tarojs/taro-weapp": "^1.2.2", "nerv-devtools": "^1.3.9", "nervjs": "^1.3.9" } } 复制代码
也就是我们安装这些依赖后, node_modules
下目录下多了这么多东西。我们简单的看一下间接有关的包,挑几个说
我们看一下: omit.js
import _extends from "babel-runtime/helpers/extends"; function omit(obj, fields) { var shallowCopy = _extends({}, obj); for (var i = 0; i < fields.length; i++) { var key = fields[i]; delete shallowCopy[key]; } return shallowCopy; } export default omit; 复制代码
从 omit.js
的 readme.md
中我们可以知道,他是生成一个去掉指定字段的的浅拷贝的对象。
我们看一下: slash
'use strict'; module.exports = input => { const isExtendedLengthPath = /^/////?///.test(input); const hasNonAscii = /[^/u0000-/u0080]+/.test(input); // eslint-disable-line no-control-regex if (isExtendedLengthPath || hasNonAscii) { return input; } return input.replace(////g, '/'); }; 复制代码
从 slash
的 readme.md
中我们可以知道, This was created since the
path methods in Node outputs
/ paths on Windows。
我们来看一下: value-equal
的主要内容如下
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; function valueEqual(a, b) { if (a === b) return true; if (a == null || b == null) return false; if (Array.isArray(a)) { return Array.isArray(b) && a.length === b.length && a.every(function (item, index) { return valueEqual(item, b[index]); }); } var aType = typeof a === 'undefined' ? 'undefined' : _typeof(a); var bType = typeof b === 'undefined' ? 'undefined' : _typeof(b); if (aType !== bType) return false; if (aType === 'object') { var aValue = a.valueOf(); var bValue = b.valueOf(); if (aValue !== a || bValue !== b) return valueEqual(aValue, bValue); var aKeys = Object.keys(a); var bKeys = Object.keys(b); if (aKeys.length !== bKeys.length) return false; return aKeys.every(function (key) { return valueEqual(a[key], b[key]); }); } return false; } export default valueEqual; 复制代码
从 value-equal
的 readme.md
中我们可以知道,这个方法是只比较每个对象的 key
对应的 value
值。
我们看一下 prop-types
,这这里不列源码了。看 README.md
,我们知道 Runtime type checking for React props and similar objects.
它是 react
框架中的 props
类型检查的辅助工具,也就是完成了下面这个功能
XxxComponent.propTypes = { xxProps: PropTypes.xxx } 复制代码
如果想看具体怎么实现的,可以继续看看源码,你会发现其实很多东西都是有具体的实现的,完全不需要去死记硬背。
我们来看一下, js-tokens
。这里只列出了 default
的内容
// Copyright 2014, 2015, 2016, 2017, 2018 Simon Lydell // License: MIT. (See LICENSE.) Object.defineProperty(exports, "__esModule", { value: true }) // This regex comes from regex.coffee, and is inserted here by generate-index.js // (run `npm run build`). exports.default = /((['"])(?:(?!/2|//).|//(?:/r/n|[/s/S]))*(/2)?|`(?:[^`//$]|//[/s/S]|/$(?!/{)|/$/{(?:[^{}]|/{[^}]*/}?)*/}?)*(`)?)|(////.*)|(///*(?:[^*]|/*(?!//))*(/*//)?)|(//(?!/*)(?:/[(?:(?![/]//]).|//.)*/]|(?![///]//]).|//.)+//(?:(?!/s*(?:/b|[/u0080-/uFFFF$//'"~({]|[+/-!](?!=)|/.?/d))|[gmiyus]{1,6}/b(?![/u0080-/uFFFF$//]|/s*(?:[+/-*%&|^<>!=?({]|//(?![//*])))))|(0[xX][/da-fA-F]+|0[oO][0-7]+|0[bB][01]+|(?:/d*/./d+|/d+/.?)(?:[eE][+-]?/d+)?)|((?!/d)(?:(?!/s)[$/w/u0080-/uFFFF]|//u[/da-fA-F]{4}|//u/{[/da-fA-F]+/})+)|(--|/+/+|&&|/|/||=>|/.{3}|(?:[+/-//%&|^]|/*{1,2}|<{1,2}|>{1,3}|!=?|={1,2})=?|[?~.,:;[/](){}])|(/s+)|(^$|[/s/S])/g 复制代码
结合 README.md
,我们会发现,哇,这好骚啊。使用正则来将 JS
语法变成一个个的 token
。 so cool 。
效果如下:
var jsTokens = require("js-tokens").default var jsString = "var foo=opts.foo;/n..." jsString.match(jsTokens) // ["var", " ", "foo", "=", "opts", ".", "foo", ";", "/n", ...] 复制代码
让你写能写出来这种逆天正则吗:joy:
看一下 rollup-plugin-alias
从 readme.md
中,我们可以发现,它做了一件事,就是把包的引入路径抽象化了,这样好处很多,可以不用关心 ../
这种符号了,而且可以做到集中式修改。我们的启发是什么,其实我们可以从 rollup-plugin-alias
中学到如何去管理我们自己的 npm
包。这种思想我们要吸收。
看一下 resolve-pathname
结合源码,从 readme.md
中,我们可以发现,他做了什么事情呢,其实它做了这么一件事,就是提供一个方法,让我们去处理 URL
,或者说是路由,通过这个方法,我们能对给点的路由做一些处理,比如返回一个新的路由。
关于 invariant、warning
都是一些处理提示的辅助工具,就不说了。
下图
会看到 router
目录下,有 dist
和 types
目录。但是没有 src
目录,但是为什么有的包有src呢。这是为什么呢,这是个问题。
这是将用 taro
编写的代码编译成微信小程序代码所需要的一些工具,代码结构如图所示:
![]( user-gold-cdn.xitu.io/2018/12/27/
首先从 readme.md
中,我们看不到此包究竟是干什么的,只能看到一句话,多端解决方案小程序端基础框架。所以我觉得这点, taro
团队还是要对其进行相应补充的。至少通过对比其他包,这里的 readme.md
写的太简洁了。
但是我们可以通过阅读代码来分析一下 taro-weapp
是干什么的,首先我们看一下代码结构。有 dist
, src
等,还有 node_modules
。这时候我们联想到上面介绍的包后,我们发出了这样的疑问,为什么这里有了 node_modules
目录。它的目的是什么? 可以先不去解答这个问题,我们继续分析代码。
首先肯定要先看 readme.md
,但是 readme.md
的信息就一句话,多端解决方案小程序端基础框架。那怎么办,不要气馁!八年抗战,我们继续分析下去。
我们看一下 package.json
。部分代码如下
"scripts": { "test": "echo /"Error: no test specified/" && exit 1", "build": "rollup -c rollup.config.js", "watch": "rollup -c rollup.config.js -w" }, "dependencies": { "@tarojs/taro": "1.2.2", "@tarojs/utils": "1.2.2", "lodash": "^4.17.10", "prop-types": "^15.6.1" } 复制代码
从 package.json
中我们能发现两个主要的事情,第一个是此包需要的依赖,可以看到依赖 @tarojs/taro
@tarojs/utils
lodash
prop-types
。 然后我们查看 node_modules
,发现只有 @tarojs/taro
。其他的都是在外面安装好了,比如 lodash
prop-types
可以用根目录下的包,这里的 @tarojs/utils
是新安装的。在 taro
目录下。掌握这些信息,我们应该能结合上面的了解,去思考几个问题
peerDependencies
@tarojs/taro
安装到了 taro-weapp
包的内部。 taro-weapp
没有 types/index.d.ts
这种文件 问题 mark
一下,我们继续看,现在我们阅读一下 index.js
。依旧是
module.exports = require('./dist/index.js').default module.exports.default = module.exports 复制代码
很明显 dist
目录是经过打包生成的目录,现在我们看 src
目录。我们看一下 src
中的 index
/* eslint-disable camelcase */ import { getEnv, Events, eventCenter, ENV_TYPE, render, internal_safe_get, internal_safe_set, internal_inline_style, internal_get_original } from '@tarojs/taro' import Component from './component' import PureComponent from './pure-component' import createApp from './create-app' import createComponent from './create-component' import initNativeApi from './native-api' import { getElementById } from './util' export const Taro = { Component, PureComponent, createApp, initNativeApi, Events, eventCenter, getEnv, render, ENV_TYPE, internal_safe_get, internal_safe_set, internal_inline_style, createComponent, internal_get_original, getElementById } export default Taro initNativeApi(Taro) 复制代码
从 index.js
中,我们可以看到,导入了 @tarojs/taro
的一些方法。上面分析了 @tarojs/taro
。我们结合起来想一下,可以发现,其实在将用 taro
编写的代码,编译成微信小程序的时候,需要借助 @tarojs/taro
包来一起实现转换。
我们先不看 @tarojs/taro
。我们来按照 export default Taro
导出的模块顺序分析一下
function createApp (AppClass) { const app = new AppClass() const weappAppConf = { onLaunch (options) { app.$app = this app.$app.$router = app.$router = { params: options } if (app.componentWillMount) { app.componentWillMount() } if (app.componentDidMount) { app.componentDidMount() } }, onShow (options) { Object.assign(app.$router.params, options) if (app.componentDidShow) { app.componentDidShow() } }, onHide () { if (app.componentDidHide) { app.componentDidHide() } }, onError (err) { if (app.componentDidCatchError) { app.componentDidCatchError(err) } }, onPageNotFound (obj) { if (app.componentDidNotFound) { app.componentDidNotFound(obj) } } } return Object.assign(weappAppConf, app) } export default createApp 复制代码
上面这个一看就知道是用来生成微信小程序的小程序级别的配置,来看一下上面的if语句,你可以感受到其背后的目的了。再看一下 Object.assign(weappAppConf, app)
你就知道, taro
是如何遵循 react
的数据不可变的编程思想了。
const nextTick = (fn, ...args) => { fn = typeof fn === 'function' ? fn.bind(null, ...args) : fn const timerFunc = wx.nextTick ? wx.nextTick : setTimeout timerFunc(fn) } export default nextTick 复制代码
这个代码也好理解,通过将代码放在 wx.nextTick
或者 setTimeout
来达到在下一个循环阶段再执行。
这个文件的代码很重要,为什么叫 native-api
。如果你看了官方文档的话,你会看到这个页面:
其实这里的 native-api.js
就是上图的介绍,可以理解为 Taro
对微信小程序的原生 api
进行的封装。
下面我们来看一下 native-api.js
的输出是什么,代码如下
export default function initNativeApi (taro) { processApis(taro) taro.request = request taro.getCurrentPages = getCurrentPages taro.getApp = getApp taro.requirePlugin = requirePlugin taro.initPxTransform = initPxTransform.bind(taro) taro.pxTransform = pxTransform.bind(taro) taro.canIUseWebp = canIUseWebp } 复制代码
这里到导出了一个 initNativeApi
方法。看到上面代码,是不是知道整个入口的大概画面了。这个导出的方法在入口中执行,来对 taro
进行了补充。。。我们先从 taro-weapp
的入口文件中, 看一下在没有执行 initNativeApi(Taro)
的 Taro
对象是什么
const Taro = { Component, PureComponent, createApp, initNativeApi, Events, eventCenter, getEnv, render, ENV_TYPE, internal_safe_get, internal_safe_set, internal_inline_style, createComponent, internal_get_original, getElementById } 复制代码
Taro
就好比是 koa
中的 ctx
,通过绑定上下文的形式挂载了很多方法。但是这里,做了一个优化,情况,在执行 initNativeApi(Taro)
后的 Taro
对象是
const Taro = { Component, PureComponent, createApp, initNativeApi, Events, eventCenter, getEnv, render, ENV_TYPE, internal_safe_get, internal_safe_set, internal_inline_style, createComponent, internal_get_original, getElementById, request getCurrentPages getApp requirePlugin initPxTransform pxTransform canIUseWebp } 复制代码
processApis(taro)
这个先不说。
我们看上面的代码,发现多了很多方法,可以理解为通过 initNativeApi
,挂载了微信小程序的本地一些 API
。可是有些又不是本地 API
,但是可以这样理解吧,比如 request
, getCurrentPages
, getApp
。我个人理解作者这样做的原因是为了解耦,将 native
和非 native
的方法分开。
总结通过对 @tarojs/taro-weapp
的分析,我们具体知道了,当在运行时, taro
通过 getEnv
将代码切到 taro-weapp
进行编译的时候, taro-weapp
是如何进行编译处理的。比如如何去解决多端涉及到的 API
不同的问题。通过分析,我们已经较为深入的理解了taro的整个架构思想和部分内部实现。这些思想值得我们在平时的项目中去实践它。其实看源码的目的是什么,我分析 taro init
分析到现在,如果你看完,你会发现有很多很酷的思想,可能在你的世界中,写了几个项目都根本想不起来也可以这样用。
import nextTick from './next-tick' import { updateComponent } from './lifecycle' let items = [] export function enqueueRender (component) { // tslint:disable-next-line:no-conditional-assignment if (!component._dirty && (component._dirty = true) && items.push(component) === 1) { nextTick(rerender) } } export function rerender () { let p const list = items items = [] // tslint:disable-next-line:no-conditional-assignment while ((p = list.pop())) { if (p._dirty) { updateComponent(p, true) } } } 复制代码
通过命名就知道用到了 nextTick
渲染的思想。
我们把函数缩起来,发现只导出了 updateComponent
方法,从命名中,我们知道这是更新组件的意识。
const data = {} export function cacheDataSet (key, val) { data[key] = val } export function cacheDataGet (key, delelteAfterGet) { const temp = data[key] delelteAfterGet && delete data[key] return temp } export function cacheDataHas (key) { return key in data } 复制代码
从代码我们可以知道,这是做数据缓存用的。先缓存起来,然后每取一次 value
,就把这个 value
删掉。那么为什么要这样做呢,
import { shallowEqual } from '@tarojs/utils' import Component from './component' class PureComponent extends Component { isPureComponent = true shouldComponentUpdate (nextProps, nextState) { return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState) } } export default PureComponent 复制代码
我们看一下 pure-componnet.js
的代码。是不是发现非常好理解了, PureComponent
类继承了 Component
。同时,自己实现了一个 shouldComponentUpdate
方法。而这个方法
shouldComponentUpdate (nextProps, nextState) { return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState) } 复制代码
你会发现其入参是 nextProps, nextState
。然后通过 shallowEqual
方法和 props, state
进行比较,而 shallowEqual
,听名字就知道是浅比较。 具体代码在 @taro/util
目录下的 src
目录下的 shallow-equal.js
中
/* eslint-disable */ Object.is = Object.is || function (x, y) { if (x === y) { return x !== 0 || 1 / x === 1 / y } return x !== x && y !== y } export default function shallowEqual (obj1, obj2) { if (obj1 === null && obj2 === null) { return true } if (obj1 === null || obj2 === null) { return false } if (Object.is(obj1, obj2)) { return true } const obj1Keys = obj1 ? Object.keys(obj1) : [] const obj2Keys = obj2 ? Object.keys(obj2) : [] if (obj1Keys.length !== obj2Keys.length) { return false } for (let i = 0; i < obj1Keys.length; i++) { const obj1KeyItem = obj1Keys[i] if (!obj2.hasOwnProperty(obj1KeyItem) || !Object.is(obj1[obj1KeyItem], obj2[obj1KeyItem])) { return false } } return true } 复制代码
看看代码,发现是浅比较。看到这,你是不是感觉到 PureComponent
也没有想象中的抽象难懂,类推一下, React
中的 PureComponent
也是这个理。完全没有必要去死记硬背一些框架的什么生命周期啊,各种专业名字啊。其实当你在揭去他的面纱,看到它的真相的时候,你会发现,框架并没有多深奥。但是如果你就是没有勇气去揭开它的面纱,去面对它的话,那么你就会一直处于想象之中,对真相一无所知。
把代码缩进去,我们看一下大致的代码,如图所示:
从图中可以看出,导出了 BaseComponent
类,从命名可以知道,这是一个基础组件类,由于代码不是太多,我直接贴上来吧。
import { enqueueRender } from './render-queue' import { updateComponent } from './lifecycle' import { isFunction } from './util' import { internal_safe_get as safeGet } from '@tarojs/taro' import { cacheDataSet, cacheDataGet } from './data-cache' const PRELOAD_DATA_KEY = 'preload' class BaseComponent { // _createData的时候生成,小程序中通过data.__createData访问 __computed = {} // this.props,小程序中通过data.__props访问 __props = {} __isReady = false // 会在componentDidMount后置为true __mounted = false nextProps = {} _dirty = true _disable = true _isForceUpdate = false _pendingStates = [] _pendingCallbacks = [] $componentType = '' $router = { params: {}, path: '' } constructor (props = {}, isPage) { this.state = {} this.props = props this.$componentType = isPage ? 'PAGE' : 'COMPONENT' } _constructor (props) { this.props = props || {} } _init (scope) { this.$scope = scope } setState (state, callback) { if (state) { (this._pendingStates = this._pendingStates || []).push(state) } if (isFunction(callback)) { (this._pendingCallbacks = this._pendingCallbacks || []).push(callback) } if (!this._disable) { enqueueRender(this) } } getState () { const { _pendingStates, state, props } = this const stateClone = Object.assign({}, state) delete stateClone.__data if (!_pendingStates.length) { return stateClone } const queue = _pendingStates.concat() this._pendingStates.length = 0 queue.forEach((nextState) => { if (isFunction(nextState)) { nextState = nextState.call(this, stateClone, props) } Object.assign(stateClone, nextState) }) return stateClone } forceUpdate (callback) { if (isFunction(callback)) { (this._pendingCallbacks = this._pendingCallbacks || []).push(callback) } this._isForceUpdate = true updateComponent(this) } $preload (key, value) { const preloadData = cacheDataGet(PRELOAD_DATA_KEY) || {} if (typeof key === 'object') { for (const k in key) { preloadData[k] = key[k] } } else { preloadData[key] = value } cacheDataSet(PRELOAD_DATA_KEY, preloadData) } // 会被匿名函数调用 __triggerPropsFn (key, args) {} } export default BaseComponent 复制代码
我们看一下上面的代码,从命名我们知道,这是一个组件的基类,可以理解为所有组件都要继承 BaseComponent
。这个我们来简单的去分析一下,首先分析第一个点,为什么有那么多下划线变量,其实这些变量是给自己用的,我们看下面的代码:
class BaseComponent { // _createData的时候生成,小程序中通过data.__createData访问 __computed = {} // this.props,小程序中通过data.__props访问 __props = {} __isReady = false // 会在componentDidMount后置为true __mounted = false nextProps = {} _dirty = true _disable = true _isForceUpdate = false _pendingStates = [] _pendingCallbacks = [] $componentType = '' $router = { params: {}, path: '' } } 复制代码
首先我记得 ES6
是不支持直接在类中写变量的,这应该是通过babel去支持这样写的。还有我们上面的注释,基本就知道这个变量可以做什么了,比如可以通过 data.__props
访问到 __props
。也就是 this.props
的值,这里也是用到了代理模式。就像 vue
中的访问方式。 ok
,这个我们了解了,那么我们继续来看下面这段代码:
class BaseComponent { constructor (props = {}, isPage) { this.state = {} this.props = props this.$componentType = isPage ? 'PAGE' : 'COMPONENT' } _constructor (props) { this.props = props || {} } } 复制代码
你看,我们发现了什么,“构造函数” 有两个,哈哈哈,骗你的,构造函数就一个,就是 constructor
。但是下面的 _constructor
函数是什么鬼,里面还进行了 this.props = props || {}
操作,是什么鬼呢,如果你看了 taro
官方文档,你可能会看到这样的提示,就算你不写this.props = props。也没事,因为taro在运行的过程中,需要用到props做一些事情,但是你可能不明白是为什么,总感觉文字说明没有代码来的实在,所以当你看到上面的代码时,是不是就感觉到实在的感觉了,看到代码了。 其实是 taro
使用自己内部的方法 _constructor
来进行了 this.props = props || {}
操作。其他的比如 setState
, getState
等自己分析一吧,路子都是一样的。反正只要你分析了,基本就能对其有一个更加深刻的理解。可能这一刻你把官网文档上的东西忘记了,你也不会忘记代码里这一行的意义。
我们找一段看一下
const weappComponentConf = { data: initData, created (options = {}) { if (isPage && cacheDataHas(preloadInitedComponent)) { this.$component = cacheDataGet(preloadInitedComponent, true) } else { this.$component = new ComponentClass({}, isPage) } this.$component._init(this) this.$component.render = this.$component._createData this.$component.__propTypes = ComponentClass.propTypes Object.assign(this.$component.$router.params, options) }, attached () { let hasParamsCache if (isPage) { // params let params = {} hasParamsCache = cacheDataHas(this.data[routerParamsPrivateKey]) if (hasParamsCache) { params = Object.assign({}, ComponentClass.defaultParams, cacheDataGet(this.data[routerParamsPrivateKey], true)) } else { // 直接启动,非内部跳转 params = filterParams(this.data, ComponentClass.defaultParams) } if (cacheDataHas(PRELOAD_DATA_KEY)) { const data = cacheDataGet(PRELOAD_DATA_KEY, true) this.$component.$router.preload = data } Object.assign(this.$component.$router.params, params) // preload if (cacheDataHas(this.data[preloadPrivateKey])) { this.$component.$preloadData = cacheDataGet(this.data[preloadPrivateKey], true) } else { this.$component.$preloadData = null } } if (!isPage || hasParamsCache || ComponentClass.defaultParams) { initComponent.apply(this, [ComponentClass, isPage]) } }, ready () { if (!isPage && !this.$component.__mounted) { this.$component.__mounted = true componentTrigger(this.$component, 'componentDidMount') } }, detached () { componentTrigger(this.$component, 'componentWillUnmount') } } 复制代码
从上面代码我们可以看出,这是将用 taro
编写的组件,编译成微信小程序程序里面的原生组件实例的。这里关注一个点,就是 attached
方法中用到了 cacheDataGet
和 cacheDataHas
,上面有介绍这两个方法,但是为什么在 attached
中使用 cacheDataGet
还有 cacheDataHas
,为什么要这样用,目的是什么,背后的意义是什么? 需要结合小程序原生组件中 attached
的含义,来思考分析一下。
如何在node_modules发现很多很有趣的东西,我举个例子。比如我们来看一个 bind
在不同的包中的实现方式: 下图是core-js中modules目录下的的bind实现
代码如下:
'use strict'; var aFunction = require('./_a-function'); var isObject = require('./_is-object'); var invoke = require('./_invoke'); var arraySlice = [].slice; var factories = {}; var construct = function (F, len, args) { if (!(len in factories)) { for (var n = [], i = 0; i < len; i++) n[i] = 'a[' + i + ']'; // eslint-disable-next-line no-new-func factories[len] = Function('F,a', 'return new F(' + n.join(',') + ')'); } return factories[len](F, args); }; module.exports = Function.bind || function bind(that /* , ...args */) { var fn = aFunction(this); var partArgs = arraySlice.call(arguments, 1); var bound = function (/* args... */) { var args = partArgs.concat(arraySlice.call(arguments)); return this instanceof bound ? construct(fn, args.length, args) : invoke(fn, args, that); }; if (isObject(fn.prototype)) bound.prototype = fn.prototype; return bound; }; 复制代码
我们再看一下 lodash
中的 bind
实现,直接上代码了:
var baseRest = require('./_baseRest'), createWrap = require('./_createWrap'), getHolder = require('./_getHolder'), replaceHolders = require('./_replaceHolders'); var WRAP_BIND_FLAG = 1, WRAP_PARTIAL_FLAG = 32; var bind = baseRest(function(func, thisArg, partials) { var bitmask = WRAP_BIND_FLAG; if (partials.length) { var holders = replaceHolders(partials, getHolder(bind)); bitmask |= WRAP_PARTIAL_FLAG; } return createWrap(func, bitmask, thisArg, partials, holders); }); // Assign default placeholders. bind.placeholder = {}; module.exports = bind; 复制代码
对比两者的代码,我们能发现两者的代码的实现形式是不一样的。可能大家能普遍理解的是第一种写法,几乎所有文章都是第一种写法,容易看懂。但是第二种写法就比较难理解了,相比第一种写法,第二种写法更加抽象和解耦。比如更加函数式,其实如果函数式编程掌握的熟练的话,bind本质上就是偏函数的一种实现,第二种写法里面已经在命名中就体现出来了, partials 。所以如果在了解的更多的话,那在面试中,如果被问到bind如何实现,是不是就可以写出两种实现方式了(编程思想)呢。可能你写完,面试官都看不懂呢笑哭。这里就是举个例子,还有很多这种,自行探索吧。(顺带把core-js和lodash包介绍了。。)
读到这,你会发现,我没有把 taro init
下载的全部依赖都分析一遍,因为真分析完,可能短篇小说就出来,同时,也没有意义。我就是起个抛砖引玉的作用,希望大家阅读我的文章后,有一些收获,不要去害怕 npm
包,那也是人写的。
在分析的时候,我建议一个一个包下载,然后下载一个包看一下目录。这样有助于你去理解,很多人都是一个 npm i
或者 yarn install
甩下来,然后打开 node_modules
目录,然后就傻眼了,根本不知道找哪个包看。所以,当你想去了解一个东西的时候,最后的方式是一点一点去看,一个包一个包去下载,然后看前后的代码结构变化,包的变化。然后你会发现包的个数在慢慢的增加,但是你一点也不慌,因为你已经知道他们大概的作用和内容了。
所以最后,还是那句话,前端是 github
上最受益的一个行业,因为最先进的开源技术,源代码都在 github
上, github
就是前端的宝藏,取之不尽,用之不完。 react
、 vue
、 angular
、 webpack
、 babel
、 node
、 rxjs
、 three.js
、 TypeScript
、 taro
、 ant-design
、 egg
、 jest
、 koa
、 lodash
、 parcel
、 rollup
、 d3
、 redux
、 flutter
、 cax
、 lerna
、 hapi
、 jsx
、 eslint
、等等等等等,宝藏就在那。你愿意去解开他们的面纱看一看真相吗?
对于 npm
包的源码,我本人在看的时候,也会对一些地方不明白,这对于我们来说很正常( NB
的大佬除外),但是我不会因为某一段,某一个文件看不懂而阻塞我对于整个包的理解,我会加入我自己的理解,哪怕是错的,但是只要我能流畅的把整个包按照我想的那样理解掉就足够了。不要试图去完全理解,除非你和 npm
包的作者进行交流了。
你会发现这篇文章中,在分析的过程中,已经存在了一些问题,而且我也没有一个确切的答案,就好像那些上传 LOL
教学的视频,只要是上传的,都是各种经典走位,预判,风骚操作。但是现实中,可能已经跪了10几把了。说到这,突然想到知乎上,有个帖子,好像是问程序平常写代码是什么场景,还贴出一个黑客帝国的图片,问真的是这样的吗?然后有个用视频回答的,我看完快笑喷了。其实推导一下,就知道看npm包源码的时候,是不可能一帆风顺的。一定有看不懂的,而且 npm
包的源码和 github
上对应 npm
包的源码是不一样的。 npm
包就好比是 github
上的 npm
源码经过包管理工具, build
后的输出。这点你从有 dist
目录就可以看出来,比如 github
中 taro
源码中是用 rollup
打成小包的。
掘金系列文章都可以在我的 github
上找到,欢迎 issues
讨论,传送地址:
github.com/godkun/blog
觉得不错的,可以点个 star 和 赞赞,鼓励鼓励。
第一次暴露我的最神秘交友网站账号(潜水逃)
2018年快过去了,祝福大家在2019年,家庭幸福,事业有成,在前端行业,游刃有余。
本文里面大概率会有写错的地方,但是大概率也会有很不错的地方。所以.......元旦快乐丫!