共9000余字,阅读须要10分钟左右。html
对于前端来讲,github
就是宝藏。作任何事情,必定要专业,不少知识都是能够找到的,尤为在前端,有不少很好的东西就摆在你的面前。好的组件源代码,好的设计模式,好的测试方案,好的代码结构,你均可以触手可及,因此不要以为不会, coding just api
,你须要掌握的是编程的思想和思惟。前端
其实此次的文章也和 ant design
彩蛋有点关系。由于有人说,谁让你不去阅读 npm
包源码的,可能不少人以为阅读 npm
包的源码是一件很困难的事情,可是我要告诉大家,npm
包对前端来讲就是一座宝藏。你能够从 npm
包中看到不少东西的真相,你能够看到全世界的最优秀的 npm
包的编程思想。vue
好比你能够看到他们的代码结构,他们的依赖关系,他们的代码交互方式,以及他们的代码编写规范,等等等等。那么如今,我就经过目前最火的多端统一框架 taro
来向你们展现,如何去分析一个经过 CLI
生成的 npm
包的代码。一片文章作不到太细致的分析,我就当是抛砖引玉,告诉你们,不要被 node_modules
那一串串的包吓到了,不敢去看,怕看不懂。其实不是大家想的那样看不懂,通常有名的 npm
包,代码结构都是很友好的,理解起来并不比你去阅读你同事的代码(你懂的)难。并且在阅读 npm
包的过程当中,你会发现不少惊喜,找到不少灵感。是否是很激动,是否是很开心,嗯,那就牵着个人手,跟着我一块儿走,我带你去解开 npm
包那神秘而又美丽的面纱。node
执行 taro init xxx
后,package.json
的依赖以下图所示react
你会发现当你初始化完一个 CLI
时,安装了不少依赖,而后这个时候若是你去看 node_modules
,必定会很难受,由于安装了不少不少依赖的包,这也是不少人点开 node_modules
目录后,立马就关上的缘由,不关可能就卡住了😂。那么咱们玩点轻松的,不搞这么多,咱们进入裸奔模式,一个一个包下载,按照 taro init
的 package.json
的安装,咱们来分析一下其中的包的代码。webpack
对 node_modules
进行截图,图片以下:git
从图片里面咱们能够看到安装了不少依赖,其中和咱们有着直接相关的包是 @tarojs
,打开 @tarojs
能够看到:github
其实你会发现没什么东西,咱们再看一下 src
目录下有什么:web
src/index.js
文件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
各类组件的地方,从这里的代码咱们能够知道,为何在 taro
里面要经过下面这种形式去引入组件。
import { View, Text, Icon } from '@tarojs/components'
复制代码
好比为何要大写,这是由于上面 export
出去的就是大写,同时把全部组件放在了一个对象里面。这里再思考一下,为何要大写呢?多是由于避免和微信小程序的原生组件的命名冲突,毕竟 taro
是支持原生和 taro
混写的,若是都是小写,那怎么区分呢。当你看到这里的源码的时候,你对 taro
的组件引入须要大写这个规则是否是就以为很是的顺其天然了。同时这里咱们应该多去体会一下 taro
这样导出一个组件的思想。越是这种频繁但不起眼的操做,咱们越应该去体会其优秀的思想。
下面咱们来挑一个组件看一下结构,好比 Button
组件,结构以下:
从上图咱们能够看到一个 taro
的基础组件的代码结构,从这里咱们能够获取到几点信息:
第一点:对每一个组件进行了单元测试,使用的是 Jest
,目录是 __test__
第二点:每一个组件都有 index.md
,用来介绍组件的文档
第三点: 样式单独用了目录 style
来存放,同时入口文件名字统一使用 index
第四点:在 types
目录里进行了 index.d.ts
的文件设置,使得代码提示更加友好
鉴于 taro
是一个正在崛起且很是有潜力的框架,咱们是否是能从 @tarojs/components
的源码中学到一些思想。好比咱们去设计一个咱们本身的组件库时,是否是能够借鉴这种思想呢。其实这种组件的代码结构形式是目前很流行的,好比使用了今年最流行的框架 Jest
框架做为组件的单元测试,使用 ts
作代码提示。看 github
上的源码的话,会发现,使用了最新的 lerna
包发布工具,使用了轻量级的 rollup
打包工具,使用 @xxx
做为 namespace
。这也是我为何选择 taro
框架来分析的缘由,taro
于2018年 6月多才开源,因此必定借鉴了目前前端最新的技术和最佳实践,没有历史包袱。其实看 taro
的源码后,你会发现 taro
中的一些设计理念,已经优于其余著名框架了。
你会发现,这个仍是安装在了 @tarojs
目录下,并无增长其余依赖。taro
的目录结构以下图所示
从图中的代码结构咱们大概能够知道:
第一: types
目录下有一个 index.d.ts
,这个文件是一个 ts
文件,他的做用是编写代码提示。这样在你写代码的时候,会给你很是友好的代码规范提示。好比 index.d.ts
里面有段代码(随便截取了一段)以下:
interface PageConfig {
navigationBarBackgroundColor?: string,
backgroundTextStyle?: 'dark' | 'light',
enablePullDownRefresh?: boolean,
onReachBottomDistance?: number
disableScroll?: boolean
}
复制代码
这段代码的目的是在你写对应的配置时,会提示你此字段的数据类型时什么,给你一个友好的提示。看到这里,其实咱们想,咱们本身也能够自定义的给本身的项目加上这种提示,这对项目是一种很好的优化。
第二:咱们看到了 dist
目录,基本能推测出这是经过打包工具,打包出来的输出目录。
第三:整个目录很简单,那 taro
的做用是什么呢,其实 taro
是一个运行时。
咱们来看一下 package.json
,以下图所示:
发现有个字段,就是
"peerDependencies": {
"nervjs": "^1.2.17"
}
复制代码
日常咱们用到的最多的就是 dependencies
和 devDependencies
。那么 peerDependencies
表达什么意识呢?咱们去谷歌翻译一下,如图所示:
拆开翻译后,是 对等依赖 ,结合翻译来讲一下整个字段的做用,其实就是指:
这个依赖不须要在本身的目录下 npm install
了。只需在根目录下 npm install
就能够了。本着不造轮子的精神,具体意识请看下面 blog
:
咱们来看一下 index.js
, 就两行代码:
module.exports = require('./dist/index.js').default
module.exports.default = module.exports
复制代码
不过我对于这种写法仍是有点惊喜的。为何要写成这样呢,不能一行搞定么,更加解耦? 大概是为了什么吧。
PS: 写完此文章,我思考了这个问题,发现这个写法和下面介绍的的一个 index.js
中的写法一模一样:
export {}
export default {}
复制代码
瞬间明白了做者这样写的目的。
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
, 代码以下:
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
里都有相应的完整代码实现。对应官方文档以下:
想想,你是否是发现API
原来是这么来的,也不是那么的难理解了,也不用死记硬背了。
internal
目录下面咱们继续分析,咱们还要关注一下 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
的总结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
咱们看一下: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.js
代码以下:
'use strict';
module.exports = input => {
const isExtendedLengthPath = /^\\\\\?\\/.test(input);
const hasNonAscii = /[^\u0000-\u0080]+/.test(input);
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.js
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.js
咱们看一下 prop-types
,这里就不列源码了。看 README.md
,咱们知道
Runtime type checking for React props and similar objects.
它是 react
框架中的 props
类型检查的辅助工具,也就是完成了下面这个功能
XxxComponent.propTypes = {
xxProps: PropTypes.xxx
}
复制代码
咱们来看一下 js-tokens
,代码以下:
Object.defineProperty(exports, "__esModule", {
value: true
})
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 。
example
以下:
var jsTokens = require("js-tokens").default
var jsString = "var foo=opts.foo;\n..."
jsString.match(jsTokens)
// ["var", " ", "foo", "=", "opts", ".", "foo", ";", "\n", ...]
复制代码
让你写能写出来这种逆天正则吗😂。
是否是感受这些函数文件都挺有意识的,若是想看具体怎么实现的,能够继续看看源码,你会发现不少东西都是有具体实现的,彻底不须要去死记硬背。咱们再看一下上面介绍的 js-token
, value-equal
, prop-types
omit
, slash
等,其实都是很好的函数,它们能够给咱们不少编程上的灵感,咱们彻底能够借鉴这些函数的思想和实现方式,从而更好的提升咱们的 JS
编程能力,这也是在阅读 npm
包源码过程当中的一个很重要的收获。
这个包是用来把 taro
编写的代码编译成微信小程序代码的,代码结构如图所示:
首先从 readme.md
中,咱们看不到此包到底是干什么的,只能看到一句话,多端解决方案小程序端基础框架。因此我以为这点,taro
团队仍是要对其进行相应补充的。这里的 readme.md
写的太简洁了。
可是咱们能够经过阅读代码来分析一下 taro-weapp
是干什么的,首先咱们看一下代码结构。有 dist
, src
等,还有 node_modules
。这时候咱们联想到上面介绍的包后,咱们发出了这样的疑问,为何这里有了 node_modules
目录。它的目的是什么?不能用上面的 peerDependencies
解决吗?对此,暂时没法理解这个事情,遇到这种问题该怎么办呢?这时咱们能够先不去深刻思考这个问题,作到不要阻塞,继续去分析其余代码。
咱们按照惯例先看 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
。如今咱们结合起来想一下,能够发现:使用 @tarojs/taro-weapp
将用 taro
编写的代码,编译成微信小程序的时候,是须要借助 @tarojs/taro
包来一块儿实现转换的。
大体知道了 taro-weapp
的做用。如今咱们来分析一下 index.js
中依赖的外部文件,分析以下:
把代码缩进去,咱们看一下大体的代码,如图所示:
从图中能够看出,导出了 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
// 删减了一点
$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) {
enqueueRender(this)
}
getState () {
const { _pendingStates, state, props } = this
const queue = _pendingStates.concat()
queue.forEach((nextState) => {
if (isFunction(nextState)) nextState = nextState.call(this, stateClone, props)
Object.assign(stateClone, nextState)
})
return stateClone
}
forceUpdate (callback) {
updateComponent(this)
}
$preload (key, value) { // 省略 }
// 会被匿名函数调用
__triggerPropsFn (key, args) {}
}
export default BaseComponent
复制代码
咱们看一下上面的代码,从命名咱们知道,这是一个组件的基类,能够理解为全部组件都要继承 BaseComponent
。咱们来分析一下上面的代码,首先分析第一个点,为何有那么多下划线变量?其实这些变量是给本身用的,咱们看下面的代码:
class BaseComponent {
// _createData的时候生成,小程序中经过data.__createData访问
__computed = {}
// this.props,小程序中经过data.__props访问
__props = {}
__isReady = false
// 会在componentDidMount后置为true
__mounted = false
// 删减了一点
$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 || {}
操做。因此文档中会提示说:不写 props
也能够。
其余的好比 setState
,getState
等本身分析一下吧,路子都是同样的。反正只要你分析了,基本就能对其有一个更加深入的理解。可能这一刻你把官网文档上的东西忘记了,但你不会忘记代码里这一行的意义。
这个文件的代码很重要,为何叫 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
挂载更多的方法。咱们看一下在执行 initNativeApi(Taro)
后的 Taro
对象是什么,代码以下:
const Taro = {
// 上面的导出依然存在,这里不重复写了
request,
getCurrentPages,
getApp,
requirePlugin,
initPxTransform,
pxTransform,
canIUseWebp,
}
复制代码
processApis(taro)
这个先不说。
咱们看上面的代码,发现多了不少方法,咱们能够理解为经过执行 initNativeApi(Taro)
,使得 Taro
挂载了微信小程序本地的一些 API
。但是你会发现有些又不是本地 API
,可是能够先这样理解吧,好比 request
, getCurrentPages
, getApp
。我我的理解做者这样作的缘由是为了解耦,将 native
和非 native
的方法分开。
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
中,代码以下:
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
也是这个理。因此没必要去死记硬背一些框架的生命周期和各类专业名字什么的。其实当你在揭去它的面纱,看到它的真相的时候,你会发现,框架并无多深奥。可是若是你就是没有勇气去揭开它的面纱,去面对它的话,那么你就会一直处于想象之中,对真相一无所知。
咱们找一段看一下
const weappComponentConf = {
data: initData,
created (options = {}) {
this.$component = cacheDataGet(preloadInitedComponent, true)
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 () {},
ready () {
componentTrigger(this.$component, 'componentDidMount')
},
detached () {
componentTrigger(this.$component, 'componentWillUnmount')
}
}
复制代码
从上面代码咱们能够看出,这是将用 taro
编写的组件,编译成微信小程序程序里面的原生组件实例的。这里关注一个点,就是 attached
方法中用到了 cacheDataGet
和 cacheDataHas
,上面有介绍这两个方法,为何要在这里用,目的是什么,背后的意义是什么? 须要结合微信小程序的组件生命周期的含义,来思考分析一下。同时,咱们要去思考组件中这句 this.$component.render = this.$component._createData
代码的含义,好好理解 created
究竟发生了哪些过程。
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) {},
onHide () {},
onError (err) {},
}
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
来达到在下一个循环阶段再执行。
import nextTick from './next-tick'
import { updateComponent } from './lifecycle'
let items = []
export function enqueueRender (component) {
if (!component._dirty && (component._dirty = true) && items.push(component) === 1) {
nextTick(rerender)
}
}
export function rerender () {
let p
const list = items
items = []
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
删掉。那么为何要这样设计呢,背后的缘由或者说这样设计的优点是什么?能够后续去细致思考一下,这也是一个好的编程思想。
@tarojs/taro-weapp
后的总结经过对 @tarojs/taro-weapp
的分析,咱们具体知道了:当在运行时,taro
是经过 getEnv
将代码切到 taro-weapp
环境来进行编译的。 随后咱们分析了,taro-weapp
是如何进行编译处理的,好比如何去解决多端涉及到的API
不一样的问题。经过分析,咱们已经较为深刻的理解了 taro
的整个架构思想和部份内部实现。这些思想值得咱们在平时的项目中去实践它。其实看源码的目的是什么,好比我分析 taro init
分析到如今,若是你看完,你会发现有不少很酷的思想,可能在你的世界中,写了几个项目都根本想不起来也能够这样用,看源码的目的就是让你去接触世界上优秀的开源项目是如何设计出来的。从而吸取这些思想,为我所用,使我成长。
rollup-plugin-alias
从 readme.md
中,咱们能够发现,它作了一件事,就是把包的引入路径抽象化了,这样好处不少,能够不用关心 ../
这种符号了,并且能够作到集中式修改。咱们的启发是什么,其实咱们能够从 rollup-plugin-alias
中学到如何去管理咱们本身的 npm
包。这种思想咱们要吸取。
resolve-pathname
readme.md
中,咱们能够发现,其实它作了这么一件事,就是提供一个方法,让咱们去处理
URL
,或者说是路由,经过这个方法,咱们能对给定的路由作一些处理,好比返回一个新的路由。
关于
invariant、warning
都是一些处理提示的辅助工具,就不说了,自行阅读源码进行分析。
代码目录结构截图以下:
咱们会看到在 router
目录下,有 dist
和 types
目录。可是没有 src
目录,可是为何有的包有 src
呢,有的没有呢?这是个问题,有待后续细致分析。
如何在 node_modules
发现更加有趣的东西。我举个例子,好比咱们来看一个 bind
在不一样的包中的实现方式: 下图是 core-js
中 modules
目录下的的 bind
实现
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 + ']';
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);
});
bind.placeholder = {};
module.exports = bind;
复制代码
对比二者的代码,咱们能发现二者的代码的实现形式是不同的。可能你们能广泛理解的是第一种写法,几乎全部文章都是第一种写法,容易看懂。可是第二种写法就比较难理解了,相比第一种写法,第二种写法更加抽象和解耦。好比更加函数式,其实若是函数式编程掌握的熟练的话, bind
本质上就是偏函数的一种实现,第二种写法里面已经在命名中就体现出来了,partials。好比在面试中,若是被问到 bind
如何实现,是否是就能够写出两种实现方式了(编程思想)呢。可能你写完,面试官都看不懂呢😂。这里就是举个例子,还有不少这种,自行探索吧。(顺带把 core-js
和 lodash
包介绍了。。)
最近 ant design
彩蛋事件,这个彩蛋足够刺激,以致于你们反应这么强烈。足以说明 ant design
的受欢迎程度,按照土话说,ant design
之前的身份是:你们只爱不恨,可是如今的身份是:你们又爱又恨。
出了问题,该怎么解决,就怎么解决,可是逼仍是要撕的,谁的锅谁背好。
故事是这样的:
好比日常在公司工做,同事或者其余人闯祸了,把你的代码
reset
掉了。这确定波及到你的工做了,这个时候你会怎么作?你确定不爽,确定会 BB 。尤为遇到那种闯了祸,影响到了别人工做的还不主动背锅道歉,摆出一副你把代码找回来不就好了么的态度。遇到这种人你确定就很不爽,要找这我的撕逼。毕竟你已经影响到我工做了,别一副好像锅不是本身的同样,锅你背好,我会解决掉你给我带来的问题,下次别再这样了。
而 ant design
,就比如上面闯祸的同事,波及到了你们,可是 ant
也主动认错了,锅也主动背了,也马上给出了方案。
其实对于那些由于这个事情致使失业什么的,我我的认为仍是比较难受的。可是对于那些说话比较激烈(难听)的人,也就是嘴上难听,有几个会由于前端框架而上升到很大的那种怨恨的,难听的目的无非就是隐式的鞭策 ant
团队。我想 ant
也意识到了,后面确定不会再这样作相似这种事情了。
我心里仍是但愿你们:
既然咱们从一开始就选择了相信 ant design
,那咱们就多一份包容,包容这一次 ant design
的犯错,不要由于一次犯错,就否认其所有。
其实你在公司里,也是这样的,你犯了错,影响到了不少同事,你意识到事情的严重性,你很难受,很后悔,你发现本身作了一件极其愚蠢的事情,你真的很想去弥补,可是时间不能倒退,岁月不能回流,你能作的就是保证下次不会再次犯错,你很想获得你们的原谅和信任。虽然你是真心认错的,但愿你们能够像原来同样信任你,但是若是你们由于你一次错误,就在举止谈吐之间表现的不那么相信你了。那,此时你的心,也必定是极其的失落和灰冷吧。
因此我仍是但愿你们能继续对 ant design
保持信任,包容 ant design
一次,也是包容一次 偏右 这种为开源作出很大贡献的人。
其实,在生活中,有时候,咱们会发现,包容不须要不少次的,一次包容就能够了。由于一次包容就可让一件事情不再会发生第二次。是不,啰啰嗦嗦了那么多,其实答案就在文字中。
好了,不胡诌我的见解了。
由于文章确实有点长,因此我对我贴的代码动了些手脚,好比,删减了一些代码,写成三行的 if
语句,写成一行。把 import
, export
的东西尽量写在一块儿,不换行写。因此若是想看没有删减版本的文章,能够去个人 github
上看,github
链接:https://github.com/godkun/blog/issues/30
对于 npm
包的源码,我本人在看的时候,也会对一些地方不明白,这对于咱们来讲很正常( NB
的大佬除外),可是我不会由于某一段,某一个文件看不懂而阻塞我对于整个包的理解,我会加入我本身的理解,哪怕是错的,可是只要我能流畅的把整个包按照我想的那样理解掉就足够了。不要试图去彻底理解,除非你和 npm
包的做者进行交流了。
你会发现这篇文章中,在分析的过程当中,已经存在了一些问题,并且我也没有一个确切的答案,就好像那些上传 LOL
教学的视频,只要是上传的,都是各类经典走位,预判,风骚操做。可是现实中,可能已经跪了10几把了。说到这,忽然想到知乎上,有个帖子,好像是问程序日常写代码是什么场景,还贴出一个黑客帝国的图片,问真的是这样的吗?而后有个用视频回答的,我看完快笑喷了。其实推导一下,就知道看 npm
包源码的时候,是不可能一路顺风的。必定有看不懂的,并且 npm
包的源码和 github
上对应 npm
包的源码是不同的。npm
包就比如是 github
上的 npm
源码通过包管理工具,build
后的输出。这点你从有 dist
目录就能够看出来,好比 github
中 taro
源码中是用 rollup
打成小包的。
遇到不懂的地方很正常,你要作的就是理解总体,忽略局部。
读到这,你会发现,我没有把 taro init
下载的所有依赖都分析一遍,由于真分析完的话,可能短篇小说就诞生了,并且也没有什么意义。我就是起个抛砖引玉的做用,但愿你们阅读个人文章后,有一些收获,不要去惧怕 npm
包,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
等等等等等,宝藏就在那,你愿意去解开它们的面纱看一看真相吗?
掘金系列文章均可以在个人 github
上找到,欢迎讨论,传送地址:
https://github.com/godkun/blog
以为不错的,能够点个 star 和 赞赞,鼓励鼓励。
第一次暴露个人最神秘交友网站帐号(潜水逃)
2018年快过去了,祝福你们在2019年,家庭幸福,事业有成,在前端行业,游刃有余。
本文里面大几率会有写错的地方,可是大几率也会有很不错的地方。
因此............
元旦快乐丫!