Lerna+webpack+juction来拆分组件库为多个单独的npm包

前不久发布了vc-popup组件集, 但是那时候完全只是展示没有如何使用的教程, 因为当时急于发布出来, 实在不妥, 抱歉~

既然是想自己东西可以让别人方便使用, 那就是打包成npm的包咯, 但是考虑vc-popup仅仅是popup的组件集, 不是完整的组件库, 所以很多时候用户仅仅想使用某个popup, 那么其他popup也打包进去, 就浪费带宽了, 所以需要一个每个popup单独发布到npm上去, 但是把依赖分开的时候之后开发就是带来不便, 比如一个包更新了, 需要在另一个手动更新, 为了解决这个不便, 就是Lerna登场的时候了, 用来方便开发和管理多个package~

但是自己实践的过程当中遇到一些问题和还有踩过一些坑, 所以在这里记录, 不过在开始之前, 先提一下vc-popup的更新

12-08: imgView支持懒加载图片,从加载状态的预设图片到加载完成的src同步变化~

安装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

在日常使用输入命令的时候常用&&加快效率, 自己输入的次数多了, 才发现命令行相比于界面的优点在于可以串联多个简单的任务, 这个学期开始学习操作系统, 发现有个类似的名词单道批处理系统CMD批处理脚本, 所以不言而喻咯~ 摁{enter}键的时候想想还有什么命令可以提前敲进去的

还有一个优点是, 命令是基于字符组合的确定, 而非界面位置, 所以界面需要层叠, 命名不需要, 字符组合容量大

> 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也会有这个类似的操作? 如果现在在开发module-2, 但是发现是module-1的bug, 把module-1的bug修改了, 需要发布一下到npm, 然后module-2再更新module-1的依赖, 那么可以猜测Leran通过某种手段让这个更新同步自动化了

那么基于猜测可以进行验证咯~ 先看手册, 查查这个类似的操作是什么~

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

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

那么可以预计操作结果是, module-2的node_modulesmodule-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, 但是我使用的是, 文件系统是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的工具来实现的, 这个时候, 突然我想到了多次重装系统在网上习得的技巧

> 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.

之前重装系统多了, 会通过mklink把C盘的Users Juction 到D盘去, 之后每次恢复系统的时候一些程序的配置也就不用重新设置的了, 具体可以参考网上的教程, 需要装系统的时候操作的(文件解压出来, 但是还没重启, 启动安装的时候), 记得好像不能在系统安装之后操作

来验证咯, 这时候就不能使用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差不多, 区别在于会根据现有的目录结构做相应的定制, 所以接下来会简单讲思路, 和遇到的问题.

目录结构
> 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不发布出去

一共需要新建3个文件, 两个是批处理属性的, 一个就是webpack的配置, 要点在于多入口的配置, 比较简单

需要注意的点

vue的依赖怎么注入?

在webpack打包的时候设置为外部依赖? 然后popup内部直接使用import Vue from 'vue' ?

还是应该依赖于执行Vue.use()时候的Vue?

区别在于是否使用webpack来做项目构建(或者其他打包工具, 不清楚webpack打包出来的模块里面声明的外部依赖, 再通过其他工具打包是否可以兼容)

如果是通过Vue.use()来注入vue的依赖, 那么就可以兼容那些不使用webpack做构建的项目, 通用性更好一些

我是无语线.........................................................................

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

发布到npm之前的包如何测试

一开始头几次测试都是发布到npm之后再更新再测试的, 其实,并不需要, 在构建完成之后把更新之后的文件同步过去测试项目的node_modules文件夹就好了, 效率提高不少, 这里通过mklinkjunction的方式同步就好了

不过使用自定义使用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的一样~

发布

> 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直接使用源码作为入口

感觉mint-ui可以说是最标准的组件库了, 在构建层面来说, 拆出来的包同时是包含源码的, package.json的出口是经过编译的

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏影子

Ionic如何实现单选二级菜单切换

3369
来自专栏运维一切

ceph对象存储折腾记 原

###前言 一直想弄对象存储,以前弄过一次,不是很理解region是个什么东西,后来时间和工作上的原因没有再折腾,这两天闲了下来,再次折腾了一次。我是参考的ce...

661
来自专栏信安之路

通过POC来学习漏洞的原理

本文介绍的是 easyFTPServer 1.7.0.2 ‘Http’ remote Buffer Overflow 的漏洞执行流程,通过已知的 POC 来推断...

1180
来自专栏Seebug漏洞平台

TCTF/0CTF2018 XSS Writeup

刚刚4月过去的TCTF/0CTF2018一如既往的给了我们惊喜,其中最大的惊喜莫过于多道xss中Bypass CSP的题目,其中有很多应用于现代网站的防御思路。

6688
来自专栏java学习

【干货来了】!Oracle及普通软件卸载详解!

电脑用的久了,里面的软件安装也会越来越多,但总有一些软件在使用过程中,甚至我们安装的过程中出现或多或少的问题,导致我们总是装了卸,卸了装(老实说,有时候纠结症都...

731
来自专栏移动开发之家

Flutter完整开发实战详解(三、 打包与填坑篇)

作为系列文章的第三篇,继篇章一和篇章二之后,本篇将为你着重展示:Flutter开发过程的打包流程、APP包对比、细节技巧与问题处理。本篇主要描述的Flutter...

641
来自专栏Albert陈凯

手把手教你用python抓取网页导入模块 urllib2随便查询一篇文章,比如On random graph。对每一个查询googlescholar都有一个url,这个url形成的规则是要自己分析的。

http://www.1point3acres.com/bbs/thread-83337-1-1.html **前言: ** 数据科学越来越火了,网页是数据...

2717
来自专栏数据小魔方

R语言爬虫实战——知乎live课程数据爬取实战

本文是一篇R语言爬虫实战练习篇,同样使用httr包来完成,结合cookies登录、表单提交、json数据包来完成整个数据爬取过程,无需书写复杂的xpath、cs...

3326
来自专栏FreeBuf

ShadowBroker释放的NSA工具中Esteemaudit漏洞复现过程

近日臭名昭著的方程式组织工具包再次被公开,TheShadowBrokers在steemit.com博客上提供了相关消息。以下是其中Esteemaudit漏洞复现...

1889
来自专栏Seebug漏洞平台

TCTF/0CTF2018 XSS Writeup

刚刚4月过去的TCTF/0CTF2018一如既往的给了我们惊喜,其中最大的惊喜莫过于多道xss中Bypass CSP的题目,其中有很多应用于现代网站的防御思路。...

793

扫码关注云+社区