一文搞懂前端組件發佈 npm 庫

前端的組件都是基於 javascript 開發,然後用 node.js 打包,發佈到 npm 的。所以我們要做組件發佈,首先要了解 npm 包的開發與發佈。

npm 包的開發與發佈

項目初始化

我們常常用 npm init 命令來初始化 node 項目;如果使用默認的設置,則可加入參數 -y。下面,我們新建一個 npm-components 文件夾,然後初始化項目:

$ mkdir npm-components
$ cd npm-components
$ npm init -y

此時,會在項目文件夾中出現一個 package.json 文件:

{
  "name""npm-components", // 組件名
  "version""1.0.0", // 組件版本
  "description""", // 組件描述
  "main""index.js", // 組件入口
  "scripts"{ // 組件腳本
    "test""echo \"Error: no test specified\" && exit 1"
  },
  "keywords"[],
  "author""", // 組件作者
  "license""ISC" // 開源協議
}

package.json 文件中的 name、version、main、script 是常用的幾個配置。

name 和 version 顧名思義是包名和版本。name 命名要注意最好是小寫加中劃線(-)的寫法,version 規範可以參考下一小節。

script 則是可以運行在命令行工具的腳本。對 linux 腳本不熟的同學,可以先在命令行工具中調試完成,再複製到 script 裏。

main 會作爲整個組件的入口,默認是 index.js,可以根據實際需要進行修改。

我們先按默認設置,在項目文件夾中,新建一個 index.js:

// index.js
const hello = 'hello';
const world = 'world';
module.exports.log = function() {
 console.log(hello + world);
};

一個最簡單的 npm 項目就完成了,這裏僅包含 index.jspackage.json兩個文件。

package.json 除了以上配置之外,我們還可能會配置到

npm version 與版本規範

開發完成就可以給組件打上版本號了。

按語義化版本控制規範 SemVer,版本格式爲:major.minor.patch,版本號遞增規則如下:

  1. 主版本號 major:當你做了不兼容的 API 修改,

  2. 次版本號 minor:當你做了向下兼容的功能性新增,

  3. 修訂號 patch:當你做了向下兼容的問題修正。

先行版本號及版本編譯信息可以加到 “major.minor.patch” 的後面,作爲延伸。如:1.0.0-alpha.0

每次發佈前,需要確定一下更新的內容,選擇版本號,使用 npm version 打上版本號

$ npm version [patch|minor|major]

此時,package.json 中的 version 會在相應的版本上加 1,有的命令行工具中也會看到版本號的變化。

~/npm-components  (npm-components@1.0.0) 
$ npm version patch
v1.0.1

~/code/npm-components  (npm-components@1.0.1) 
$

發佈到 npm

開發完成,標記版本號之後,發佈到 npm 只需要按部就班即可:

  1. 如果是公網的開源項目,需要去 npm 官網註冊個賬號;

  2. 如果貴公司有內部的 npm 庫,只需要找到 npm 管理人員給你添加即可;

  3. 如果平時使用其他源,需要切換到 npm(可安裝 nrm 來管理多個源):

    $ nrm ls
        
      npm -------- https://registry.npmjs.org/
      yarn ------- https://registry.yarnpkg.com/
      cnpm ------- http://r.cnpmjs.org/
      taobao ----- https://registry.npm.taobao.org/
        
    $ nrm use npm
  4. 在項目根目錄下的命令行工具,運行npm login,會提示輸入個人信息,完成登錄。

  5. 運行,會進行上傳;

    $ npm publish
  6. 如果上傳過程順利,沒報出紅色錯誤信息,在 https://www.npmjs.com/ 就可以看到你發佈的包了。

  7. 發佈的包在 72 小時內是可以刪除的,過了 72 小時就永遠無法刪除了,所以記得不要隨意發一些沒有意義的包。如果需要卸載,在發佈後 72 小時內執行:

    # npm unpublish  <pkg>[@<version>]
    $ npm unpublish npm-components@1.0.1

至此,一個簡單項目的 npm 包發佈完成。

模塊化

真實的項目中,我們往往不可能僅僅寫一個 js,一個 js 也不可能寫太長,多了就要拆分到多個 js。這個時候,就需要用到封裝了。

我們把常用的內容封裝在一起,就是一個簡單的模塊。這整個過程可以成爲是一個模塊化的過程。

模塊化幫我們實現了代碼複用,也幫我們做到了模塊內的數據、方法私有。

前端模塊化演進

前端的模塊化是一個演進的過程,經歷了 4 個階段:

基本原理是將模塊掛載在 window 屬性下。到 IIFE 增強階段,現在的模塊化規範基本已經成型,有了明顯的引入導出。如下代碼的引入 jQuery 和暴露 myModule:

// module.js文件
(function(window, $) {
  let data = 'www.baidu.com';
  // 操作數據的函數
  function foo() {
    // 用於暴露有函數
    console.log(`foo() ${data}`);
    $('body').css('background''red');
    otherFun(); // 內部調用
  }
  function otherFun() {
    // 內部私有的函數
    console.log('otherFun()');
  }
  // 暴露行爲
  window.myModule = { foo };
})(window, jQuery) // jQuery 作爲參數引入

在此基礎上,逐漸演化出了 AMD、CommonJS、CMD、UMD 等規範。

AMD(Asynchromous Module Definition - 異步模塊定義)

AMD 更多用於瀏覽器端,需要異步加載各模塊,然後再去執行內部代碼。是 RequireJS 在推廣過程中對模塊定義的規範化產出,推崇依賴前置。

// 定義沒有依賴的模塊
define(function(){ return 模塊 });

// 定義有依賴的模塊
define(['module1''module2']function(m1, m2){ return 模塊 });

// 引入使用模塊
require(['module1''module2']function(m1, m2){ 使用m1/m2 });

CommonJS 規範

CommonJS 是服務端模塊的規範,由於 Node.js 被廣泛認知。根據 CommonJS 規範,一個單獨的文件就是一個模塊。加載模塊使用 require 方法,該方法讀取一個文件並執行,最後返回文件內部的 module.exports 對象。

//module1.js
moudle.exports = { value: 1 };

//module2.js
var module1 = require('./module1');
var value2 = module1.value + 2;
module.exports ={ value: value2 };

CommonJS 加載模塊是同步的,所以只有加載完成才能執行後面的操作。

CMD(Common Module Definition - 公共模塊定義)

CMD 是 SeaJS 在推廣過程中對模塊定義的規範化產出,同時 CMD 也是延自 CommonJS Modules/2.0 規範。對於模塊的依賴,CMD 是延遲執行,推崇依賴就近。

define((require, exports, module) ={
  module.exports = {
    fun1: () ={
       var $ = require('jquery'); // 執行 fun1 時,再加載
       return $('#test');
    } 
  };
});

如上代碼,只有當真正執行到 fun1 方法時,纔回去執行 jquery。

UMD 規範

那有沒有一種規範同時同時兼容 AMD 和 CommonJS,既可以適用瀏覽器端,有可以適用於服務器端?

有的,就是 UMD 規範。UMD 規範甚至都不能稱作一個規範,它是 AMD 和 CommonJS 的一個糅合。是一段固定的代碼寫法。如下的工廠模式:

((root, factory) ={
  if (typeof define === 'function' && define.amd) {
    //AMD
    define(['jquery'], factory);
  } else if (typeof exports === 'object') {
    //CommonJS
    var $ = requie('jquery');
    module.exports = factory($);
  } else {
    //都不是,瀏覽器全局定義
    root.testModule = factory(root.jQuery);
  }
})(this, ($) ={
  //do something...  這裏是真正的函數體
});

ES module 規範

另一種支持服務端和瀏覽器端的規範就是 ES module 了,即我們現在最常用的importexport

export default ...;
import xxx from '';

export ...;
import { xxx } from '';

但目前也僅是大於 13.2 的 Node.js 版本才支持 ES 模塊化,還需要等待很長的時間全面使用。

所以,出於兼容性考慮,我們仍然選擇 UMD 規範進行開發。(處於淘汰邊緣的 UMD、CMD、AMD 這些規範,大家不需要理解,只需要知道有這麼一回事即可)

Webpack 打包

寫 UMD 的過程重複且繁瑣,這時候就需要用工具來完成了,Webpack 也就出現了。Webpack 配置相當簡單,以我們的項目爲例。

先安裝 webpack

$ npm install --save-dev webpack webpack-cli

新建 webpack 配置文件,webpack.config.js。用 webpack 給組件庫打包輸出符合 UMD 規範的代碼,只需要在基本配置稍作修改即可:

// webpack.config.js
const path = require('path');

module.exports = {
    mode: 'production',
    entry: './index.js',
    externals: 'lodash', // library包中有引入lodash,打包時不將lodash打進去,用戶在引入該library時,需自己再引入lodash,避免用戶重複引入lodash,導致文件過大。
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'library.js',
        library: 'library', // 全局掛載包的引用名
        libraryTarget: 'umd',  //通用模式:支持用戶通過es、common.js、AMD的方式引入npm包
        globalObject: 'this' // node 等環境運行時需要設置爲 this
    }
}

要修改的地方如下:

  1. filename:打包產物 library 的名稱;

  2. externals: ‘lodash’, library 包中有引入 lodash,打包時不將 lodash 打進去,用戶在引入該 library 時,需自己再引入 lodash,避免用戶重複引入 lodash,導致文件過大;

  3. libraryTarget: ‘umd’ 使用 UMD規範 支持用戶通過 es、common.js、AMD 的方式引入 npm 包;

  4. library: ‘library’ 會在全局變量中增加一個 liabray 的變量,用於掛載該包,主要用於通過腳本形式全局引入時的配置;

  5. globalObject: 'this',爲 webpack 4 新增屬性,需要指定 global 的值爲 ’this‘,否則會爲默認值 ’self‘,無法在 nodejs 環境中使用。

在 package.json 中加上腳本

{
...
    script: {
        "build""webpack" 
    }
}

運行腳本

npm run build

此時,webpack 就會開始打包。我們打開打包後的文件library.js,就會發現我們前面 10 行 UMD 規範的代碼。以下是不壓縮不混淆(Webpack.config.js 設置optimization: { minimize: false })打包後的代碼:

(function webpackUniversalModuleDefinition(root, factory) {
        if(typeof exports === 'object' && typeof module === 'object')
                module.exports = factory();
        else if(typeof define === 'function' && define.amd)
                define([], factory);
        else if(typeof exports === 'object')
                exports["library"] = factory();
        else
                root["library"] = factory();
})(this, function() {
return /******/ (() ={ // webpackBootstrap
/******/  var __webpack_modules__ = ({
/***/ 10:
/***/ ((module) ={
const hello = 'hello';
const world = 'world';
module.exports.log = function() {
 console.log(hello + world);
};
/***/ })
...

至此,一個支持瀏覽器端和服務端調用的組件打包完成,可以再次打個版本號,發佈到 npm 庫了。

本地測試

我們可以使用 npm 的包進行測試,也可以在開發階段引入本地打好的包,進行簡單的測試。我們遵守了 UMD 規範,因此可以在 node 環境和瀏覽器環境都驗證一下:

node 環境驗證

項目根目錄新建一個 index.test.js

let library = require('./dist/library.js');
library.log();

本地執行

$ node index.test.js
helloworld

瀏覽器環境驗證

項目根目錄新建 public 文件夾,再在此文件夾下新建一個 index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta >
    <title>測試頁面</title>
</head>
<body>
<script src='../dist/library.js'></script>
<script>
library.log(); // 控制檯輸出 helloworld
</script>
</body>
</html>

此時,打開此文件夾,即可以看到 控制檯輸出 helloworld。

如果還要更真實的環境,還可以使用本地服務器做驗證。

首先,安裝 webpack 開發服務器:

$ npm install --save-dev webpack-dev-server

然後,修改 package.json,新增 webpack 服務器腳本:

{
...
    "script":{
        "serve""webpack serve --open",
        ...
    }
}

修改 webpack.config.js

const path = require('path');
module.exports = {
    devServer: {
        static: path.join(__dirname, "./") // 將項目根目錄設爲靜態目錄
    },
    ...
}

運行服務器

$ npm run serve

webpack 會默認打開瀏覽器。可以看到瀏覽器控制檯輸出 helloworld。

驗證通過。大功告成!!!

參考:

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/CVTRRnc6YfBJwz7kFF-z2Q