不敢閱讀 npm 包源碼?帶你揭祕 taro init 背後的哲學

共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 發生了什麼

執行 taro init xxx 後,package.json 的依賴以下圖所示react

你會發現當你初始化完一個 CLI 時,安裝了不少依賴,而後這個時候若是你去看 node_modules ,必定會很難受,由於安裝了不少不少依賴的包,這也是不少人點開 node_modules 目錄後,立馬就關上的緣由,不關可能就卡住了😂。那麼咱們玩點輕鬆的,不搞這麼多,咱們進入裸奔模式,一個一個包下載,按照 taro initpackage.json 的安裝,咱們來分析一下其中的包的代碼。webpack

分析 @tarojs/components

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 的文件設置,使得代碼提示更加友好

分析 @tarojs/components 後的總結

鑑於 taro 是一個正在崛起且很是有潛力的框架,咱們是否是能從 @tarojs/components 的源碼中學到一些思想。好比咱們去設計一個咱們本身的組件庫時,是否是能夠借鑑這種思想呢。其實這種組件的代碼結構形式是目前很流行的,好比使用了今年最流行的框架 Jest 框架做爲組件的單元測試,使用 ts 作代碼提示。看 github 上的源碼的話,會發現,使用了最新的 lerna 包發佈工具,使用了輕量級的 rollup 打包工具,使用 @xxx 做爲 namespace 。這也是我爲何選擇 taro 框架來分析的緣由,taro 於2018年 6月多才開源,因此必定借鑑了目前前端最新的技術和最佳實踐,沒有歷史包袱。其實看 taro 的源碼後,你會發現 taro 中的一些設計理念,已經優於其餘著名框架了。

分析 @tarojs/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"
  }
複製代碼

日常咱們用到的最多的就是 dependenciesdevDependencies 。那麼 peerDependencies 表達什麼意識呢?咱們去谷歌翻譯一下,如圖所示:

拆開翻譯後,是 對等依賴 ,結合翻譯來講一下整個字段的做用,其實就是指:

這個依賴不須要在本身的目錄下 npm install 了。只需在根目錄下 npm install 就能夠了。本着不造輪子的精神,具體意識請看下面 blog

探討 npm 依賴管理之 peerDependencies

咱們來看一下 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
}
複製代碼

能夠看到,分別用 exportexport 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 類,想一想就知道是幹啥的了。

分析 taro 全局消息機制 event.js

咱們看一下events.js,僞代碼(簡寫)以下:

class Events {
  constructor() {
    // ...
  }
  on() {}
  once() {}
  off() {}
  trigger() {}
}

export default Events
複製代碼

你會發現這個文件完成了taro的全局消息通知機制。它 有on, once, off, trigger方法,events.js裏都有相應的完整代碼實現。對應官方文檔以下:

Taro消息機制

想想,你是否是發現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.jsreadme.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, '/');
};
複製代碼

slashreadme.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-equalreadme.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

咱們來看一下 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 包源碼過程當中的一個很重要的收穫。

分析 @tarojs/taro-weapp

這個包是用來把 taro 編寫的代碼編譯成微信小程序代碼的,代碼結構如圖所示:

首先從 readme.md 中,咱們看不到此包到底是幹什麼的,只能看到一句話,多端解決方案小程序端基礎框架。因此我以爲這點,taro 團隊仍是要對其進行相應補充的。這裏的 readme.md 寫的太簡潔了。

可是咱們能夠經過閱讀代碼來分析一下 taro-weapp 是幹什麼的,首先咱們看一下代碼結構。有 distsrc 等,還有 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 目錄下。掌握這些信息,咱們再結合上面的瞭解,再去思考幾個問題:

  1. 爲何沒有用 peerDependencies
  2. 爲何把 @tarojs/taro 安裝到了 taro-weapp 包的內部。
  3. 爲何 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 中依賴的外部文件,分析以下:

分析 src/components.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 也能夠。

其餘的好比 setStategetState 等本身分析一下吧,路子都是同樣的。反正只要你分析了,基本就能對其有一個更加深入的理解。可能這一刻你把官網文檔上的東西忘記了,但你不會忘記代碼裏這一行的意義。

分析 src/native-api.js

這個文件的代碼很重要,爲何叫 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 的方法分開。

分析 src/pure-component.js

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 也是這個理。因此沒必要去死記硬背一些框架的生命週期和各類專業名字什麼的。其實當你在揭去它的面紗,看到它的真相的時候,你會發現,框架並無多深奧。可是若是你就是沒有勇氣去揭開它的面紗,去面對它的話,那麼你就會一直處於想象之中,對真相一無所知。

分析 src/create-componnet.js

咱們找一段看一下

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 方法中用到了 cacheDataGetcacheDataHas ,上面有介紹這兩個方法,爲何要在這裏用,目的是什麼,背後的意義是什麼? 須要結合微信小程序的組件生命週期的含義,來思考分析一下。同時,咱們要去思考組件中這句 this.$component.render = this.$component._createData 代碼的含義,好好理解 created 究竟發生了哪些過程。

分析 src/create-app.js

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 的數據不可變的編程思想了。

分析 src/next-tick.js

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 來達到在下一個循環階段再執行。

分析src/render-queue.js

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 渲染的思想。

分析 src/lifecycle.js

咱們把函數縮起來,發現只導出了 updateComponent 方法,從命名中,咱們知道這是更新組件的意識。

分析 src/data-cache.js

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都是一些處理提示的輔助工具,就不說了,自行閱讀源碼進行分析。

分析 @tarojs/router

代碼目錄結構截圖以下:

咱們會看到在 router 目錄下,有 disttypes 目錄。可是沒有 src 目錄,可是爲何有的包有 src 呢,有的沒有呢?這是個問題,有待後續細緻分析。

如何發現更加有趣的東西

如何在 node_modules 發現更加有趣的東西。我舉個例子,好比咱們來看一個 bind 在不一樣的包中的實現方式: 下圖是 core-jsmodules 目錄下的的 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-jslodash 包介紹了。。)

對 ant design 彩蛋事件的理解

最近 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 包遇到不懂的地方怎麼辦

對於 npm 包的源碼,我本人在看的時候,也會對一些地方不明白,這對於咱們來講很正常( NB 的大佬除外),可是我不會由於某一段,某一個文件看不懂而阻塞我對於整個包的理解,我會加入我本身的理解,哪怕是錯的,可是隻要我能流暢的把整個包按照我想的那樣理解掉就足夠了。不要試圖去徹底理解,除非你和 npm 包的做者進行交流了。

你會發現這篇文章中,在分析的過程當中,已經存在了一些問題,並且我也沒有一個確切的答案,就好像那些上傳 LOL 教學的視頻,只要是上傳的,都是各類經典走位,預判,風騷操做。可是現實中,可能已經跪了10幾把了。說到這,忽然想到知乎上,有個帖子,好像是問程序日常寫代碼是什麼場景,還貼出一個黑客帝國的圖片,問真的是這樣的嗎?而後有個用視頻回答的,我看完快笑噴了。其實推導一下,就知道看 npm 包源碼的時候,是不可能一路順風的。必定有看不懂的,並且 npm 包的源碼和 github 上對應 npm 包的源碼是不同的。npm 包就比如是 github 上的 npm 源碼通過包管理工具,build 後的輸出。這點你從有 dist 目錄就能夠看出來,好比 githubtaro 源碼中是用 rollup 打成小包的。

遇到不懂的地方很正常,你要作的就是理解總體,忽略局部。

文末心得總結

讀到這,你會發現,我沒有把 taro init 下載的所有依賴都分析一遍,由於真分析完的話,可能短篇小說就誕生了,並且也沒有什麼意義。我就是起個拋磚引玉的做用,但願你們閱讀個人文章後,有一些收穫,不要去懼怕 npm 包,npm 包也是人寫的。

在分析的時候,我建議一個一個包下載,而後下載一個包看一下目錄。這樣有助於你去理解,不少人都是一個 npm i 或者 yarn install 甩下來,而後打開 node_modules 目錄,而後就傻眼了,根本不知道找哪一個包看。因此,當你想去了解一個東西的時候,最好的方式是一個包一個包去下載,一點一點去看,看先後的代碼結構變化,包的變化。而後你會發現包的個數在慢慢的增長,可是你一點也不慌,由於你已經知道他們大概的做用和內容了。

最後按照小學語文老師教個人操做,搞個首尾呼應吧。

前端是 github 上最受益的一個行業,由於最早進的開源技術,源代碼都在 github 上, github 就是前端的寶藏,取之不盡,用之不完。reactvueangularwebpackbabelnoderxjsthree.jsTypeScripttaroant-designeggjestkoalodashparcelrollupd3reduxfluttercaxlernahapijsxeslint 等等等等等,寶藏就在那,你願意去解開它們的面紗看一看真相嗎?

參考連接

激萌一刻

掘金系列文章均可以在個人 github 上找到,歡迎討論,傳送地址:

https://github.com/godkun/blog

以爲不錯的,能夠點個 star 和 贊贊,鼓勵鼓勵。

第一次暴露個人最神祕交友網站帳號(潛水逃)

幕後花絮

2018年快過去了,祝福你們在2019年,家庭幸福,事業有成,在前端行業,遊刃有餘。

本文裏面大機率會有寫錯的地方,可是大機率也會有很不錯的地方。

因此............

元旦快樂丫!

相關文章
相關標籤/搜索