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

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

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

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

clipboard.png

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

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

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

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

  • 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 包裝容器中。

所以在這裏我們需要設置兩個屬性來明確打包模式

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

clipboard.png

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

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

  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,非常簡單的配置。

ES6之後,光有dist是不夠的

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

    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,直接上代碼:

// 發佈打包
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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章