专栏首页IMWeb前端团队Lerna+webpack+juction来拆分组件库为多个单独的npm包

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 条评论
登录 后参与评论

相关文章

  • 玩转webpack(一)上篇:webpack的基本架构和构建流程

    在研究了一段时间的 webpack 源码之后,自己希望写个系列文章,结合自己的实践一起来谈谈 webpack 插件这个主题,也希望能够帮助其他人更全面地了解。

    小时光
  • webpack 项目 css/js主域重试

    为了提高网站的访问速度,现在一般会将静态资源放在 CDN 下,而不是放在网站的域名之下。以腾讯课堂为例,其域名为 ke.qq.com,打开控制台,访问 ke....

    IMWeb前端团队
  • webpack 学习小结

    webpack 学习小结 1 前言 打包,本质来说就是把许多零散的文件有序的合并成为一个文件,达到前端优化的效果 它的前世今生就不说了,感兴趣的同学可以去学习相...

    IMWeb前端团队
  • windows环境安装vue-cli及webpack并创建VueJS项目

    1. 安装node.js, Node.js安装包及源码下载地址为:https://nodejs.org/en/download/。 这次node.js不是主角,...

    企鹅号小编
  • vue2.x-仿懂球帝SPA-爆炸足球

    vue2.x—爆炸足球 模仿懂球帝移动端,欢迎各位童鞋star github地址:https://github.com/vqlai/vue2-football ...

    IMWeb前端团队
  • 【webpack】webpack-dev-server生猛上手——让我们来搭一个webpack的微服务器吧!

    [前言]:因为最近在搞****API的时候用到了webpack的externals,才发现我之前都只是用webpack做一些搭建完项目后的“收尾工作”——即打包...

    外婆的彭湖湾
  • 2016我的心路历程:从 Vue 到 Webpack 到 iView

    ? 2016年工作中做过最自豪的两件事情: 把 Vue.js 和 Webpack 技术栈引进公司并逐步成为前端规范; 开源 iView 项目。 初识 V...

    IMWeb前端团队
  • 【webpack】流行的前端模块化工具webpack初探

    从开发文件到生产文件 有一天我突然意识到一个问题,在使用react框架搭建应用时,我使用到了sass/less,JSX模版以及ES6的语法在编辑器下进行开发,使...

    外婆的彭湖湾
  • 你所不知的Webpack-多种配置方法

    ? 除了通过最常见的导出一个 Object 来描述 Webpack 所需的配置外,还有其它更灵活的方式,以简化不同场景的配置。 下面来一一介绍它们。 导出一个...

    IMWeb前端团队

扫码关注云+社区

领取腾讯云代金券