從dist到es:發一個NPM庫,我蛻了一層皮

這並非本身第一次發npm包, 因此這裏沒有多少入門的知識。在此以前已經有一篇前端腳手架,聽起來玄乎,實際呢?,但這一次的npm包和上一次的不是一個概念,前者只是一個腳本工具,而這個npm包是平常開發中方法和組件的集合, 是一個庫。
在讀本文前,假定你已經對npm包有必定概念,熟悉Babel編譯和webpack打包的常規用法,知道一些前端工程化的知識。假如你也想本身發佈一個npm倉庫,但對這一塊瞭解的不是不少,推薦webpack官方的建立一個 librarycss

打包模式:平常構建與庫的構建

在前端平常開發中,引入npm庫,執行webpack構建已是一件不能再日常的事情。但大多數時候,咱們不關心這個npm庫是怎樣構成的,咱們只須要知道怎麼使用,像antd;在工程化成熟的公司,也不關心webpack的配置究竟是怎樣的,只須要npm run start或npm run build去啓動一次熱加載或打包。可是若是你是編寫一個npm倉庫,這些東西你都須要知道。從那時的無知提及,起初,我用公司的構建工具(相似於roadhog)去打包個人庫,沒有坎坷,構建出一個2M多的包併成功發佈。前端

clipboard.png

在測試項目中引入,構建成功。node

import { EnhanceTable, WithSearch } from 'antd-doddle';  // 引入倉庫

在瀏覽器中打開,打擊開始到來:
clipboard.png
提示要引入的對象沒有正確導出,就是沒有作module.export,因此這是一個打包模式的問題,output.libraryTarget須要瞭解一下。react

clipboard.png
webpack的output.libraryTarget決定了打包時對外暴露出來的對象是那種模式,默認是var,用於script標籤引入,該模式也是咱們平常開發構建最經常使用的模式,除了這一種,還支持的經常使用選項有:webpack

  • commonjs(2):node環境CommonJS規範,關於commonjs與commonjs2的區別,commonjs 規範只定義了exports,而 module.exports是nodejs對commonjs的實現,實現每每會在知足規範前提下做些擴展,因此把這種實現稱爲了commonjs2;
  • amd:amd規範,適用於requireJS;
  • this:經過 this 對象訪問(libraryTarget:'this');
  • window:經過 window 對象訪問,在瀏覽器中(libraryTarget:'window')。
  • UMD:將你的 library 暴露爲全部的模塊定義下均可運行的方式。它將在 CommonJS, AMD 環境下運行,或將模塊導出到 global 下的變量,(libraryTarget:'umd')。
  • jsonp:這是一種比較特殊的模式,適用於有extrnals依賴的時候(splitChunks)。將把入口起點的返回值,包裹到一個 jsonp 包裝容器中。

因此在這裏咱們須要設置兩個屬性來明確打包模式git

library: 'antd-doddle',
    libraryTarget: 'umd',

clipboard.png

2M到38KB, 這中間發什麼了什麼

clipboard.png
上圖是用roadhog打包出來的結果,其顯示的是開啓gzip後能夠壓縮到的大小,第一次打包的實際大小大概在2M(antd+moment+react+css),後面仔細一想,公司的組件庫也才300kb啊,本身是否是哪裏搞錯了,因此接着就有了下面的探尋之路。es6

  1. 抽離css(300kb),因爲此npm庫是基於antd的,因此就沒有再把antd的css打包一次的必要了。基於roadhog給予的提示,配置了disableAntdStyle爲false,css文件降到2kb;
  2. 接着上面,雖然是基於antd的,但並無徹底用到antd的全部組件,其官方提供了一個按需打包babel插件babel-plugin-import,並在babelrc中配置, js打包體積由1.6M降爲1.2M;
["import", {
      "libraryName": "antd",
      "libraryDirectory": "lib",
      "style": "css", 
    }]
  1. 若是對webpack多瞭解一下,或者在寫一個庫以前讀過 建立一個 library,就會發現前面兩點都是白扯沒有用的,由於對於這個庫來講antd就是一個外部依賴(externals),正好roadhog又支持, 打包出來,由1.2M變爲38kb, 這是一個質的提高。
externals: {
    react: {
      commonjs: 'react',
      commonjs2: 'react',
        amd: 'react',
    },
    antd: {
      commonjs: 'antd',
      commonjs2: 'antd',
        amd: 'antd',
    },
    moment: {
      commonjs: 'moment',
      commonjs2: 'moment',
        amd: 'moment',
    },
  }

打包大小優化至此就搞定了,但後面發現用roadhog打包庫有一些很難解決的難題,爲了解決還得去了解他源碼邏輯,因此後面仍是本身寫了一個webpack,很是簡單的配置。github

ES6以後,光有dist是不夠的

在寫這個庫以前,我曾想到在咱們平常構建時有下面這樣一段配置:web

rules: [{
      test: /\.js$/,
      exclude: /(node_modules|bower_components)/,
      loader: 'babel-loader',
      query: {
        presets: ['@babel/preset-env', '@babel/preset-react']
      }
    }

這段配置是告訴webpack,node_modules中引用的代碼不須要再由babel編譯一次,但這些代碼仍是會被打包進dist文件的。在如今前端的主流開發套路中,被引用的庫更但願是一個只編譯而沒有被打包過的,支持按需加載的庫。因此dist中被編譯打包過的代碼再次被打包, 這樣就會有沒必要要的代碼出現。因此這樣來看,咱們只須要將代碼編譯。babel,沒錯,就是它,可是多文件編譯,仍是找個第三方構建工具比較好,我選擇了gulp,直接上代碼:npm

// 發佈打包
gulp.task('lib', gulp.series('clean', () => {
  return gulp.src('./src/**/*.js')
    .pipe(babel())
    .pipe(gulp.dest('./lib'));
}, 'lessToLib')); // lessToLib用於將less文件拷貝貸lib文件夾

編譯事後大概是下面這樣的,確實只編譯沒打包,函數基本原樣:

clipboard.png

本覺得到這就結束了,可是這纔開始。只編譯不打包消除沒必要要的代碼只是很小的緣由,重要的東西我以爲換一行說比較好。
不顧語文老師的責罵換行,那什麼纔是是最重要的:按需打包(tree shaking),對於這種組件和方法庫,做爲使用者,咱們但願他能支持按需打包,像lodash和antd這樣。因此懷着好奇的心理我去看了他們的package.json,而後發現了這樣的配置:

"main": "lib/index.js",
  "module": "es/index.js",
  "name": "antd",

除了認知中的main入口定義,還多了一個module入口.爲何須要這樣呢,和我同樣無知的,能夠先讀webpack官方的tree shaking,若是不夠直觀,能夠再看一位大佬寫的一篇相關文章聊聊 package.json 文件中的 module 字段。看下面代碼:

// es6 模塊寫法 fun.js
export function square(x) {
  return x * x;
}

export function cube(x) {
  return x * x * x;
}

// commonJs寫法 fun.js
exports.square = function(x) {
  return x * x;
}

exports.cube = function(x) {
  return x * x * x;
}

// index.js引入
import {
  square
} from './fun.js';

const res = square(10);
console.log('res:', res);

簡單來講,就是index.js打包編譯時,引入commonJs寫法的fun.js,打包會將square與cube兩個函數同時打進來。而引入es6寫法的fun.js,只會將square打包。這樣的操做,對於如今的主流趨勢,就是必須的優化,特別對於lodash和antd這種龐大的庫。而要使咱們的庫支持這樣的操做,咱們須要編譯時,禁止babel將es6的module引入方式編譯,其實只須要在前面的基礎上多配置一個參數:

"@babel/preset-react" // lib的打包方式

["@babel/preset-env", { "modules": false }] // 保留es6模塊引入的方式

獲得的是下面這樣的結果:

clipboard.png

和上面的lib對比,感受更接近原始代碼。至此,編譯已結束,可是咱們還須要在package.json中加上相應的配置:

"description": "antd後臺項目前端組件封裝和方法庫",
  "main": "lib/index.js",
  "module": "es/index.js",
  "scripts": {
    "build": "webpack --config webpack.config.js",
    "lib": "gulp lib",
    "es": "gulp es",
    "prepublish": "gulp && webpack --config webpack.config.js"
  },
  "files": [
    "es",
    "dist",
    "lib",
    "utils"
  ],

怎麼讓庫支持多目錄輸出

由於個人庫主要包括組件和方法,我把方法放到一塊兒,經過utils做爲默認輸出。而後項目中引入是這樣的:

import { EnhanceTable, WithSearch }, utils from 'antd-doddle'; 

// 要用裏面的方法須要再分解一次或經過utils.xxx
const { DATE_FORMAT, idCodeValid } = utils;

雖然感受上不復雜,可是總感受彆扭,若是你用過dva,就見過下面這樣的引入:

import { routerRedux } from 'dva/router';
import dva from 'dva';

因此我去學習了一下,發現要這樣實現也不難
分三步,分目錄打包,增長一個輸出,並增長內部私有映射,package.json增長一個這個映射目錄的輸出。具體可查看項目源碼。實現後,項目引入是這樣的:

import { EnhanceTable, WithSearch }, utils from 'antd-doddle'; 
import { DATE_FORMAT, idCodeValid } from ‘antd-doddle/utils’; // 一步到位

小技巧分享

  • npx執行本地命令

之前咱們不少命令如webpack,gulp命令只有在全局安裝(npm install xxx -g)才能夠在命令行中直接運行或在項目中安裝,經過script定義執行,但在npm5.2之後,咱們能夠只項目中安裝,而後經過新增的npx執行。好比上面scripts中定義的lib打包("lib": "gulp"),咱們能夠直接在命令行中用:

npx gulp
  • 命令行切換npm registry

有可能你和我同樣,在處處都是牆的世界,須要在npm,cnpm,公司的npm registry三者之間來回切換,每次都須要這樣:

npm set registry 'https://registry.npm.taobao.org/'

麻煩有沒有? 幸虧,這世界有不少牛逼的人,nrm registry是個很好用的工具,下面這樣:

// 安裝
npm install -g nrm
// 設置入口npm,cnpm,company
nrm add npm 'http://registry.npmjs.org'
nrm add cnpm 'https://registry.npm.taobao.org'
nrm add vnpm 'http://npm.company.com'
// 切換入口到淘寶入口
nrm use cnpm

後續

一個春節本身斷斷續續就在倒騰這個,收穫仍是挺大的。後面本身會慢慢去學習怎麼加入demo‘,加入單元測試,去建造一個完整的npm庫。
源碼庫:github
npm倉庫地址:npm

相關文章
相關標籤/搜索