前不久發(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同步變化~
如果大家對我扣細節(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
通過某種手段讓這個更新同步自動化了
那么基于猜測可以進行驗證咯~ 先看手冊, 查查這個類似的操作是什么~
看Example就很清晰知道的了, 那么開始生成依賴
> lerna add module-0 --scope=module-1
> lerna add module-1 --scope=module-2
那么可以預計操作結果是, module-2的node_modules
有module-1
的文件夾,并且包含了其內容, module-1同理
那么就可以猜測如何實現了
是遞歸復制文件? 驗證一下
那么現在修改一下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'
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
所以, 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
概要
- 通過js初始化
popup-*
目錄和package.json
- 通過js生成每個
popup
的entry[install.js]
- 配置
webpack.pkg.conf.js
, 配置多入口 - lerna設置包之間的依賴, 其他的包都需要依賴
popup-base
- 實驗性的
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
文件夾就好了, 效率提高不少, 這里通過mklink
的junction
的方式同步就好了
不過使用自定義使用juction
的時候最好記錄到一下文檔, 把juction的設置寫到初始化的腳本里面, 最好編寫平臺兼容的, ntfs
使用mklink
, linux系的就使用ln
如果使用文件復制來實現同步的方式也是可行, 不過注意, 不要刪除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-ui
和weex-ui
從設計開始使用了lerna來拆包, vant
有packages
但是里面的子目錄不包含package.json
可能還沒引用lerna吧
weex-ui
雖然是使用了lerna來拆包, 但是package.json
直接使用源碼作為入口
感覺mint-ui
可以說是最標準的組件庫了, 在構建層面來說, 拆出來的包同時是包含源碼的, package.json
的出口是經過編譯的
而我的vc-popup
結構是一個混合體, 一開始沒有考慮做拆包, 后面加上的, 所以...拆出來的包僅僅包含經過編譯的文件...也沒有做js, css的分離
...
至于子組件的包是否有需要再走一遍編譯, cube-ui
滴滴團隊有后編譯的優(yōu)化建議, 個人感覺也合理, 組件在具體的vue項目是會再有一層編譯的, 所以組件發(fā)布的時候僅僅發(fā)布源碼即可, 不過我還是覺得mint-ui
是最標準的方式~~
最后, 尋求文章的建議
寫到后面似乎有點不夠扣題了[faceplam], 不過也因為, 其實思路理清楚之后, 接下來的事情就是編碼和調試了
主要想問一下, 像一開始那里穿插的各種小技巧, 和對事物的點滴理解, 不知道大家對這種方式的有什么評價? 其實自己平時也有一些小理解, 但是不足以成文, 所以就打算后面把這些小知識插到相關的具體實例當中去, 如果大家感覺前面部分還不錯的話就點贊, 我打算后面都使用這種小知識分享的風格~
希望大家給我的文章提提建議~ 主要是分享的思路上面, 或者對實踐的總結上面有什么好的方法或者思路, 指導指導~
vc-popup使用的文檔還沒完善, 這里給自己寫下篇文章的借口~