Lerna+webpack+juction來拆分組件庫為多個單獨的npm包

前不久發(fā)布了vc-popup組件集, 但是那時候完全只是展示沒有如何使用的教程, 因為當時急于發(fā)布出來, 實在不妥, 抱歉~

既然是想自己東西可以讓別人方便使用, 那就是打包成npm的包咯, 但是考慮vc-popup僅僅是popup的組件集, 不是完整的組件庫, 所以很多時候用戶僅僅想使用某個popup, 那么其他popup也打包進去, 就浪費帶寬了, 所以需要一個每個popup單獨發(fā)布到npm上去, 但是把依賴分開的時候之后開發(fā)就是帶來不便, 比如一個包更新了, 需要在另一個手動更新, 為了解決這個不便, 就是Lerna登場的時候了, 用來方便開發(fā)和管理多個package~

但是自己實踐的過程當中遇到一些問題和還有踩過一些坑, 所以在這里記錄, 不過在開始之前, 先提一下vc-popup的更新

12-08: imgView支持懶加載圖片,從加載狀態(tài)的預設圖片到加載完成的src同步變化~
popup-img-viewer2.gif

如果大家對我扣細節(jié)的態(tài)度認可的話, 記得點star


安裝Lerna

目前知道3種辦法, 如果在使用vscode同學, 使用cnpm時候附帶--by=npm 可以避免rg.exe吃CPU的問題, 同理可以設置為--by=yarn, 一些包使用cnpm安裝有問題的時候, 就可以使用讓cnpm僅僅做下載, 安裝交給npm/yarn

> npm i -g lerna
> cnpm i -g lerna --by=npm
> yarn global add lerna

初始化一個demo

在日常使用輸入命令的時候常用&&加快效率, 自己輸入的次數多了, 才發(fā)現命令行相比于界面的優(yōu)點在于可以串聯多個簡單的任務, 這個學期開始學習操作系統(tǒng), 發(fā)現有個類似的名詞單道批處理系統(tǒng)CMD批處理腳本, 所以不言而喻咯~ 摁{enter}鍵的時候想想還有什么命令可以提前敲進去的

還有一個優(yōu)點是, 命令是基于字符組合的確定, 而非界面位置, 所以界面需要層疊, 命名不需要, 字符組合容量大

> mkdir lerna-demo && cd lerna-demo && lerna init

前面因為需要穿插cnpm所以安裝部分沒有串聯

由于鍵盤右邊shift鍵位問題, 其實輸入&&的時候并不是那么順暢, 可以通過AHK來做轉接, 我一般用筆記本鍵盤的時候按aand{space}生成&&{space}, 自己做的鍵盤, 因為調整過shift的位置就還是按&&

生成的查看生成的文件和目錄

> ls
lerna.json  package.json  packages

分別查看文件內容

> head lerna.json && head package.json
{
  "lerna": "2.5.1",
  "packages": [
    "packages/*"
  ],
  "version": "0.0.0"
}
{
        "devDependencies": {
                "lerna": "^2.5.1"
        }
}

然后新建目錄s

> cd packages && mkdir module-0 module-1 module-2

初始化package.json

> cd module-0 && npm init -y && cd ../module-1 && npm init -y && cd ../module-2 && npm init -y
Wrote to D:\DEV\Github\demo\lerna-demo\packages\module-0\package.json:

{
  "name": "module-0",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": ""
}


Wrote to D:\DEV\Github\demo\lerna-demo\packages\module-1\package.json:

{
  "name": "module-1",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}


Wrote to D:\DEV\Github\demo\lerna-demo\packages\module-2\package.json:

{
  "name": "module-2",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

初始化每個module的index.js

> echo export default require('./package.json').name > index.js && cat index.js > ../module-0/index.js && cat index.js > ../module-1/index.js

然后在lerna-demo新建index.js并編輯, 因為lerna會維護的是packages/*之間的依賴, 這里的index.js直接填寫module-2的路徑

> cd ../.. && code index.js
const msg = require('./packages/module-2')

console.log(msg);

設置module之間依賴, 現在require的時候就可以直接填寫對應的module

修改module-1的index.js

export default 
  require('./package.json').name 
  + 'depends on [' + require('module-0').default + ']'  

修改module-2的index.js

export default 
  require('./package.json').name 
  + 'depends on [' + require('module-1').default + ']'

思考

正常途徑如何添加npm包的依賴?
yarn add modue-name

有什么結果?
會從npm倉庫下載該包下來, 解壓到node_modules/module-name, 然后處理packsage.json依賴

那么是否意味著Lerna也會有這個類似的操作?
如果現在在開發(fā)module-2, 但是發(fā)現是module-1的bug, 把module-1的bug修改了, 需要發(fā)布一下到npm, 然后module-2再更新module-1的依賴, 那么可以猜測Leran通過某種手段讓這個更新同步自動化了

那么基于猜測可以進行驗證咯~ 先看手冊, 查查這個類似的操作是什么~

2017-11-14_112623.png

看Example就很清晰知道的了, 那么開始生成依賴

> lerna add module-0 --scope=module-1
> lerna add module-1 --scope=module-2

那么可以預計操作結果是, module-2的node_modulesmodule-1的文件夾,并且包含了其內容, module-1同理

2017-12-16_092625.png

那么就可以猜測如何實現了

是遞歸復制文件? 驗證一下
那么現在修改一下module-0/index.js 然后,查看module-1/node_modules/module-0/index.js, module-2同理

module-0/index.js該為如下

export default 
  require('./package.json').name + ' edited'
2017-12-16_093248.png

OK, 自動修改是同步更新的, 所以不是, 記得自己看linux的教程的時候有個工具是相關的, ln, 但是我使用的是, 文件系統(tǒng)是NTFS

> ver
Microsoft Windows [Version 10.0.15063]
> ln --help                                                                   
用法:ln [選項]... [-T] 目標 鏈接名     (第一種格式)                                         
 或:ln [選項]... 目標         (第二種格式)                                              
 或:ln [選項]... 目標... 目錄 (第三種格式)                                                
 或:ln [選項]... -t 目錄 目標...      (第四種格式)                                        
In the 1st form, create a link to TARGET with the name LINK_NAME.             
In the 2nd form, create a link to TARGET in the current directory.            
In the 3rd and 4th forms, create links to each TARGET in DIRECTORY.           
Create hard links by default, symbolic links with --symbolic.                 
By default, each destination (name of new link) should not already exist.     
When creating hard links, each TARGET must exist.  Symbolic links             
can hold arbitrary text; if later resolved, a relative link is                
interpreted in relation to its parent directory.                 
--more             

但是我用的是windows哦, 那么猜測是通過windows的工具來實現的, 這個時候, 突然我想到了多次重裝系統(tǒng)在網上習得的技巧

> mklink --help
The syntax of the command is incorrect.
Creates a symbolic link.

MKLINK [[/D] | [/H] | [/J]] Link Target

        /D      Creates a directory symbolic link.  Default is a file
                symbolic link.
        /H      Creates a hard link instead of a symbolic link.
        /J      Creates a Directory Junction.
        Link    Specifies the new symbolic link name.
        Target  Specifies the path (relative or absolute) that the new link
                refers to.

之前重裝系統(tǒng)多了, 會通過mklink把C盤的Users Juction 到D盤去, 之后每次恢復系統(tǒng)的時候一些程序的配置也就不用重新設置的了, 具體可以參考網上的教程, 需要裝系統(tǒng)的時候操作的(文件解壓出來, 但是還沒重啟, 啟動安裝的時候), 記得好像不能在系統(tǒng)安裝之后操作

來驗證咯, 這時候就不能使用ls -all來查看了(安裝了cygwin, 并且把bin目錄放在path里了, 所以可以用), 而是需要使用dir

2017-12-16_095526.png

所以, lerna在windows下是通過建立Juction來解決依賴包同步更新的問題~ linux的話, 也就不言而喻咯, 使用的應該是類似的工具ln~

通過webpack設置babel轉碼, 然后通過lerna-demo/index.out.js來驗證結果咯~

> webpack && node index.out.js
Hash: 3378d33b254656002585
Version: webpack 3.10.0
Time: 1031ms
       Asset     Size  Chunks             Chunk Names
index.out.js  4.14 kB       0  [emitted]  main
   [0] ./index.js 83 bytes {0} [built]
   [1] ./packages/module-2/index.js 183 bytes {0} [built]
   [2] ./packages/module-2/package.json 233 bytes {0} [built]
   [3] ./packages/module-1/index.js 183 bytes {0} [built]
   [4] ./packages/module-1/package.json 233 bytes {0} [built]
   [5] ./packages/module-0/index.js 141 bytes {0} [built]
   [6] ./packages/module-0/package.json 196 bytes {0} [built]
module-2 depends on [module-1 depends on [module-0 edited]]

結果就出來了, demo測試通過 再想一下改造vc-popup的時候會可能出現什么問題? Lerna解決的是在packages/*的依賴,
也就是回到了例子的問題了

const msg = require('./packages/module-2')

console.log(msg);

這里說明的是在不在packages文件夾內就不能享受依賴更新同步的福利了

開工

任何對試驗性的改造, 都推薦新建分支里面進行~

> git checkout -b split-packages

總體的思路, 大致上和lerna-demo差不多, 區(qū)別在于會根據現有的目錄結構做相應的定制, 所以接下來會簡單講思路, 和遇到的問題.

目錄結構
> tree src                                
Folder PATH listing for volume Data       
Volume serial number is 0007-86B5         
D:\DEV\GITHUB\OPENSOURCE\VC-POPUP\SRC     
├───components                            
│   ├───gesture-tile-press                
│   ├───picker-view                       
│   ├───popup-base                        
│   ├───popup-bottom-menu                 
│   ├───popup-by-animation                
│   ├───popup-calendar                    
│   ├───popup-center-menu                 
│   ├───popup-datetime-picker             
│   ├───popup-dialog                      
│   ├───popup-dom-relative                
│   ├───popup-img-viewer                  
│   ├───popup-over                        
│   ├───popup-picker                      
│   ├───popup-press-menu                  
│   ├───pull-down-refresh                 
│   ├───swipe-item                        
│   └───swipeplus                         
├───mixins                                
│   └───event                             
└───utils                                 
分析

需要拆成包的是src/components/popup-*
生成的包是vc-popup-*, 入口是index.js
每個包的安裝方式都是如下

import Vue from 'vue'
import popup from 'vc-popup-*'

Vue.use(popup)

拆包之后popup-*包和包之間都是屬于外部依賴

Vue.use的時候的install函數會先安裝依賴的popup

概要
  1. 通過js初始化popup-*目錄和package.json
  2. 通過js生成每個popupentry[install.js]
  3. 配置webpack.pkg.conf.js, 配置多入口
  4. lerna設置包之間的依賴, 其他的包都需要依賴popup-base
  5. 實驗性的popup通過在package.json設置private: true不發(fā)布出去

一共需要新建3個文件, 兩個是批處理屬性的, 一個就是webpack的配置, 要點在于多入口的配置, 比較簡單

需要注意的點

vue的依賴怎么注入?

在webpack打包的時候設置為外部依賴? 然后popup內部直接使用import Vue from 'vue' ?

還是應該依賴于執(zhí)行Vue.use()時候的Vue?

區(qū)別在于是否使用webpack來做項目構建(或者其他打包工具, 不清楚webpack打包出來的模塊里面聲明的外部依賴, 再通過其他工具打包是否可以兼容)

如果是通過Vue.use()來注入vue的依賴, 那么就可以兼容那些不使用webpack做構建的項目, 通用性更好一些

我是無語線.........................................................................

但是, 如果注意到import popup from 'vc-popup-*', 哈哈哈, vue的導入不需要走webpack, 但是vc-popup-*需要, 所以popup也是需要提供一個script+src的版本才行, 所以還是擁抱es6的模塊吧[尬笑]

發(fā)布到npm之前的包如何測試

一開始頭幾次測試都是發(fā)布到npm之后再更新再測試的, 其實,并不需要, 在構建完成之后把更新之后的文件同步過去測試項目的node_modules文件夾就好了, 效率提高不少, 這里通過mklinkjunction的方式同步就好了

不過使用自定義使用juction的時候最好記錄到一下文檔, 把juction的設置寫到初始化的腳本里面, 最好編寫平臺兼容的, ntfs使用mklink, linux系的就使用ln

difference bewteen symbolic link , junction and hard link.png

如果使用文件復制來實現同步的方式也是可行, 不過注意, 不要刪除node_modules/vc-popup-base文件夾, 再復制該文件夾, 因為開dev server的時候會因為無法找到文件夾而中斷, 需要重開那種, 所以直接覆寫文件即可

嗯, 測試完再publish而不是publish之后再測試!


具體步驟

生成popup-*目錄, 和package.json
var fs = require('fs')
var path = require('path')
var readlineSync = require('readline-sync');
var deleteFolderRecursive = require('./utils').deleteFolderRecursive;
require('shelljs/global');

// 工具函數
function _path(str){
  return path.resolve(__dirname, str)
}

function _package(name){
  return `{
  "name": "vc-${name}",
  "version": "0.0.0",
  "description": "vc-${name}",
  "main": "index.js",
  "scripts": {
    "test": "echo hasn't write test~"
  },
  "author": "deepkolos",
  "license": "MIT",
  "dependencies": {}
}`;
}

function initpkg(dirname){
  var path = _path('../packages/'+dirname);
  if( !fs.existsSync(path) ){
    fs.mkdirSync(path);
    fs.writeFileSync(path+'/package.json', _package(dirname));
  }
}

// 開始
var deleteAllDir = readlineSync.question('是否清空packages下所有目錄? (y/n)');

var componentsDir = fs.readdirSync(
  _path('../src/components'), {
    encoding: "utf8"
  });

deleteAllDir.toLowerCase() == 'y' && 
componentsDir.map((dirname) => {
  deleteFolderRecursive(_path('../packages/'+dirname))
})

componentsDir.map(dirname => {
  if(dirname.indexOf('popup-') === 0)
    initpkg(dirname)
});
生成popup-*目錄, entery[install.js]
var fs = require('fs')
var render = require('json-templater/string')
var uppercamelcase = require('uppercamelcase')
var path = require('path')
var utils = require('./utils')

var p = function (str){
  return path.resolve(__dirname, str);
}
var PACKAGE_PATH = p('../packages')
var DEPENDANCE_TEMPLATE = `  Vue.use(require('{{name}}'))`
var MAIN_TEMPLATE = `
const version = '{{version}}'
const install = function (Vue, config = {}) {
  if (install.installed) return
{{includeDepend}}
  require('{{self}}')
}

// auto install
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}

export default {
  install,
  version
}
`
var BASE_MAIN_TEMPLATE = `
import { popupRegister, importVue } from '{{self}}'

const version = '{{version}}'
const install = function (Vue, config = {}) {
  if (install.installed) return
{{includeDepend}}
  importVue(Vue)
  require('{{self}}').default.init(Vue)
}

// auto install
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}

export default {
  install,
  version,
  popupRegister
}
`

function build_install(popupName){
  var pkg = require(`${PACKAGE_PATH}/${popupName}/package.json`)
  var version = pkg.version
  var dependanceList = []
  var tpl = popupName === 'popup-base'? BASE_MAIN_TEMPLATE: MAIN_TEMPLATE

  pkg.dependencies &&
  Object.keys(pkg.dependencies).forEach(function(depName){
    dependanceList.push(render(DEPENDANCE_TEMPLATE, {
      name: depName
    }))
  });

  var template = render(tpl, {
    includeDepend: dependanceList.join('\n'),
    version: version,
    self: `../../src/components/${popupName}`
  })
  
  fs.writeFileSync(p(`../packages/${popupName}/install.js`), template);
}

// 開始
utils.mapPkgList(function(popupName){
  build_install(popupName)
})

配置webpack的多入口

const webpackConfig = merge(baseWebpackConfig, {
  module: {
    rules: utils.styleLoaders({
      sourceMap: config.build.productionSourceMap,
      extract: true
    })
  },
  devtool: config.build.productionSourceMap ? '#source-map' : false,
  externals: ['vue', 'vc-popup-base'], //設置外部依賴, 目前比較簡單
  plugins: [
    // Compress extracted CSS. We are using this plugin so that possible
    // duplicated CSS from different components can be deduped.
    new OptimizeCSSPlugin({
      cssProcessorOptions: {
        safe: true
      }
    })
  ]
})

fs.readdirSync(path.resolve(__dirname, '../packages'));

webpackConfig.entry = {}
webpackConfig.output = {
  path: path.resolve(__dirname, '../packages/'),
  filename: `[name]/index.js`,
  libraryExport: "default",
  libraryTarget: "umd"
}

utils.mapPkgList(function(popupName){
  webpackConfig.entry[popupName] = 
    path.resolve(__dirname, `../packages/${popupName}/install.js`)
})

module.exports = webpackConfig

剩下的步驟和lerna-demo的一樣~

發(fā)布

> lerna publish

done~

主流vue組件庫的拆包情況

我看了mint-ui, vant, we-vue, weex-ui, cube-ui, fish-ui的大概構建思路

其中只有mint-uiweex-ui從設計開始使用了lerna來拆包, vantpackages但是里面的子目錄不包含package.json可能還沒引用lerna吧

weex-ui雖然是使用了lerna來拆包, 但是package.json直接使用源碼作為入口

2017-12-17_165937.png

感覺mint-ui可以說是最標準的組件庫了, 在構建層面來說, 拆出來的包同時是包含源碼的, package.json的出口是經過編譯的

2017-12-17_165718.png

而我的vc-popup結構是一個混合體, 一開始沒有考慮做拆包, 后面加上的, 所以...拆出來的包僅僅包含經過編譯的文件...也沒有做js, css的分離...

2017-12-17_170538.png

至于子組件的包是否有需要再走一遍編譯, cube-ui滴滴團隊有后編譯的優(yōu)化建議, 個人感覺也合理, 組件在具體的vue項目是會再有一層編譯的, 所以組件發(fā)布的時候僅僅發(fā)布源碼即可, 不過我還是覺得mint-ui是最標準的方式~~

最后, 尋求文章的建議

寫到后面似乎有點不夠扣題了[faceplam], 不過也因為, 其實思路理清楚之后, 接下來的事情就是編碼和調試了

主要想問一下, 像一開始那里穿插的各種小技巧, 和對事物的點滴理解, 不知道大家對這種方式的有什么評價? 其實自己平時也有一些小理解, 但是不足以成文, 所以就打算后面把這些小知識插到相關的具體實例當中去, 如果大家感覺前面部分還不錯的話就點贊, 我打算后面都使用這種小知識分享的風格~

希望大家給我的文章提提建議~ 主要是分享的思路上面, 或者對實踐的總結上面有什么好的方法或者思路, 指導指導~


vc-popup使用的文檔還沒完善, 這里給自己寫下篇文章的借口~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末酸茴,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子兢交,更是在濱河造成了極大的恐慌薪捍,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件配喳,死亡現場離奇詭異酪穿,居然都是意外死亡,警方通過查閱死者的電腦和手機晴裹,發(fā)現死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進店門被济,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人涧团,你說我怎么就攤上這事只磷。” “怎么了泌绣?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵钮追,是天一觀的道長。 經常有香客問我阿迈,道長元媚,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮刊棕,結果婚禮上炭晒,老公的妹妹穿的比我還像新娘。我一直安慰自己甥角,他們只是感情好腰埂,可當我...
    茶點故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蜈膨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪牺荠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天灶壶,我揣著相機與錄音驰凛,去河邊找鬼。 笑死胚宦,一個胖子當著我的面吹牛枢劝,可吹牛的內容都是我干的。 我是一名探鬼主播轴捎,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼昨悼,長吁一口氣:“原來是場噩夢啊……” “哼率触!你這毒婦竟也來了葱蝗?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤皂甘,失蹤者是張志新(化名)和其女友劉穎偿枕,沒想到半個月后渐夸,有當地人在樹林里發(fā)現了一具尸體墓塌,經...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年韩肝,在試婚紗的時候發(fā)現自己被綠了伞梯。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片帚屉。...
    茶點故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡攻旦,死狀恐怖牢屋,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情锋谐,我是刑警寧澤涮拗,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布鼓择,位于F島的核電站就漾,受9級特大地震影響抑堡,放射性物質發(fā)生泄漏首妖。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望妒貌。 院中可真熱鬧灌曙,春花似錦在刺、人聲如沸蚣驼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽咙轩。三九已至活喊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間侠畔,已是汗流浹背软棺。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工喘落, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留瘦棋,地道東北人暖哨。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓篇裁,卻偏偏與公主長得像团甲,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子躺苦,可洞房花燭夜當晚...
    茶點故事閱讀 45,691評論 2 361

推薦閱讀更多精彩內容