组内负责的几个项目都有一些一样的公共组件,所以就着手搭建了个公共组件开发脚手架,第一次开发 library,所以是参考着 iview 的配置来搭建的。记录如何使用webpack4
搭建一个library
的脚手架
前言
使用 webpack4,需要安装 webpack 和 webpack-cli
yarn add webpack webpack-cli -D
然后就是书写配置文件。
项目脚手架结构
我写的 library 的目录结构如下,仅供参考,主要是模仿 iview
的结构,其中部分配置参考了 vue-cli
的 webpack 配置文件。
├─build
│ build.js // 用于执行构建
│ check-versions.js // vue-cli 留下的,主要就是检查npm版本和node版本
│ webpack.base.conf.js // 通用配置
│ webpack.dev.conf.js // 开发环境
│ webpack.dist.prod.conf.js // 用于生成library的代码 -- hbf.min.js
│ webpack.prod.conf.js // 用于生成example文件的打包代码,这个其实是没有必要的.
│
├─dist
│ └─example // example生成的打包文件夹,可以通过githubPage来预览,或者本地使用anywhere预览
│ hbf.min.js // library 文件
│
├─example // example目录
│ App.vue
│ index.html
│ main.js
│
├─lib
│ │ index.js // 全量引入公共组件,并暴露出来,包含install方法可供vue引入使用该插件
│ │ README.md
│ │
│ └─components // 公共组件
│
├─package.json // 项目包依赖
更为具体信息的可以到github仓库阅览。
经过 webpack 编译后的代码
为了更好的理解,先来了解下 webpack 编译后的代码。
经过webpack处理过的代码通常都是如下所示
// webpack编译后的代码
/*
* @param {Array} modules
*/
;(function(modules) {
function __webpack_require__(moduleId) {
var module = {
i: moduleId, // 模块ID
l: false,
exports: {}, // 作为结果返回.
}
// 调用modules数组的某个元素(类型为函数)
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
)
return module.exports
}
return __webpack_require__(0)
})([
/** 省略了代码, 该数组的每一项代表一个模块,实际是一个函数,接受三个参数,module对象,module.exports对象,__webpack_require__函数 **/
])
webpack 编译后的代码的整体结构就是一个IIFE函数
,接受一个 modules: Array
参数。
对于模块处理,无论是 ES Module
的 import
还是 commonjs
的 require
都转化为__webpack_require__
这个函数来引入模块。
__webpack_require__
函数,会从 modules
数组的第一个元素开始(moduleId 为 0,也就是入口文件),执行该模块(实为一个函数)的逻辑,利用传入的module.exports
的数据类型为引用类型Object
,间接地给module.exports
添加属性。
return __webpack_require__(0)
从入口文件开始,逐个引入依赖模块,最后返回入口模块的 module.exports
此时这个编译后的 js 文件,是无法被其他模块所引用的,只在当前作用域内有效, webpack
就提供了创建 library 的方式,就是在output
里定义library
和 libraryTarget
。使得构建完的 js 可以供其他模块引入使用。
设置library配置
对于作为一个 library 使用的项目来说,output 选项需要设置 library
// webpack.dist.pord.conf.js
output: {
path: path.resolve(__dirname, '../dist'),
publicPath: '/dist/',
filename: 'hbf.min.js',
library: 'hbf',
libraryTarget: 'umd'
},
library
可以是字符串,也可以是对象,(对象仅限于 libraryTarget
的值为 umd
的情况下使用)
output: {
library: {
root:'Hbf', // 暴露给全局变量,window.Hbf进行调用
commonjs: 'hbf-public-components'
},
libraryTarget: 'umd'
}
commonjs
和 commonjs2
的区别。
commonjs
规范就是定义了一个 exports
对象,而 nodejs
在实现的时候,在 commonjs
规范的前提下做了一些扩展,定义了 module.exports
,从而也叫这种为 commonjs2
规范。
我们在引用别人的库的时候,通常都是可以通过多种方法引入的,比如 标签引入,通过
commonJS
模块 引入,通过 ES6 Module
引入。
libraryTarget
设置为umd
(通用模块规范)的话,则打包后可以通过多种模块加载的方法加载 library,具有高兼容性。
library 的依赖问题
如果我们的 library 是基于某某库的基础上开发的,比如说写一个基于vue
的 UI 组件库,在开发的这个组件库的时候,我们需要引入vue
,如果使用这个组件库的用户本身就已经引入了vue
,那么vue
就会被引入并打包两次,所以我们在开发一个library
的时候,对于一些所依赖的模块,可以由引入library
的使用者提供。所以我们需要将依赖的模块在 library 的打包构建中去除。
externals
的作用,防止将某些 import
的包打包到 bundle 中,而是在运行时再去外部获取这些扩展依赖。
通过设置 externals
,从输出的 bundle
中排除 vue
和 iview
。
这些外部依赖可能是以下的任何一种形式。
-
root
全局变量访问 -
commonjs
作为一个commonjs
模块引入 -
commonjs2
与commonjs
类似,不过导出的是module.exports
-
amd
使用amd
模块规范引入
// webpack.dist.pord.conf.js
externals: {
vue: {
root: 'Vue',
commonjs: 'vue',
commonjs2: 'vue',
amd: 'vue'
},
iview: {
root: 'iView',
commonjs: 'iview',
commonjs2: 'iview',
amd: 'iview'
}
},
另外在 package.json 加多一个peerDependencies
字段,作用是约定library
所依赖的库的版本号,在使用者下载使用library
的时候,如果所依赖的 iview
和 vue
的版本号不对,就会发出警告。
"peerDependencies": {
"iview": ">2.0.0",
"vue": ">2.0.0"
},
对于这两个依赖,写到开发环境依赖中 ,不然安装时,会在库的目录下安装vue
和 iview
,这也不符合让 library
的引用者提供 library
的依赖这个想法。
vue 插件库/ 组件库
对于 vue 的插件库 / 组件库来说,如果想要全局引入的话,需要有一个install
方法。install 内部逻辑就是通过参数传进来的vue
对象,注册所有组件。然后最后将所有公共组件连同install
方法组成一个新对象暴露出去。
// 引入公共组件
import publicMenu from './components/public-menu'
import tablePage from './components/table-page'
import sliderCustom from './components/slider-custom'
const components = {
publicMenu,
tablePage,
sliderCustom,
}
const Hbf = Object.assign({}, components)
const install = function(Vue, opts) {
if (install.installed) return
Object.keys(components).forEach(component => {
Vue.component(component, component)
})
}
// 用于script标签引入
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
// 将install方法赋给Hbf对象
Hbf.install = install
// 输出default变量,用于全量引入,也可以在引入的时候选择使用 * 来全量引入
export default Hbf
// 输出各个组件,用于按需引入
export { publicMenu, tablePage, sliderCustom }
在暴露含有 install 方法的对象时,一开始使用的是 module.exports
,引用 library 的时候报错Uncaught TypeError: Cannot assign to read only property 'exports' of object '#
。
因为我测试用的项目关闭了 babel
对 ES Module
的编译,通常情况下,没有手动关闭的话,babel
会将 ES6 Module
编译转换成 commonjs
规范。所以在关闭了module
的转换的情况下,由于库的输出使用的是 commonjs
规范的 module.exports
,而引入库使用的是 ES6 模块规范的import
关键字,所以产生了报错,我将库的导出写成export
关键字,就没报错了。
以后书写某一模块的导入导出,还是使用同一种规范的,不要混用。
了解到现在大多数库,都是用的 commonjs
规范,由于 webpack
的 tree-shaking
只对 ES Module
起作用,webpack
的 tree-shaking
实际上是由Uglylify
来实现的。
所以库的模块规范可以使用两种,利用 package.json
的 main
和 module
字段分别定义库的两种模块规范的入口文件。 main
使用的是 commonjs
规范语法书写的文件,而 module
是使用 ES6 module
语法书写的文件,module
字段目前还是一个提案。所以采用了 ES2015
模块语法的库的,当我们只使用到 library
的部分代码,则可以利用webpack
进行 tree-shaking
,去除未引用的代码,减少打包文件体积。
发布 npm 包
首先就是需要注册一个 npm 账号。
如果之前使用的是淘宝镜像的话,需要先切回 npm 官方源。不然是发不了包的。
打开命令行
切换官方源 npm config set registry https://registry.npmjs.org/
执行npm login
,然后输入你的账号信息。
可以配置.npmignore
忽略一些不需要上传的文件,写法跟.gitignore
相同。
需要保证项目有正确的package.json
文件和README.md
文件
然后执行npm publish
进行发包。
发完包就可以切回淘宝镜像源
npm config set registry https://registry.npm.taobao.org
引用组件库
Script 标签引入
一开始是将 JS 文件放在本地测试,发现在HTML
文件的第一行就报错,Unexpected token <
,StackOverflow说的原因是引入路径不正确,所以我就把 JS 文件放到 CDN 上了,
输出
全量引用
可以像使用其他 vue 插件库/组件库一样使用。
import hbf from 'hbf-public-components'
// 使用use方法触发hbf的intall方法,注册全部组件
Vue.use(hbf)
如果是没有导出default
变量,则使用另外一种方式全量引入
import * as hbf from 'hbf-public-components'
按需引用
import { publicMenu } from 'hbf-public-components'
按需引用,如果 library 使用的是ES2015 Module
规范,则不需要安装任何插件,webpack 会对其进行tree-shaking
,去除未引用的代码。
前面提过,webpack
的tree-shaking
是由Uglylify
插件实现的,我在开发环境下,没有启用Uglylify
来压缩代码,所以查看模块打包图,会发现整个库都被引入了,虽然我只引入了一个组件。webpack4
在生产环境下,才会进行tree-shaking
,设置mode
的值为production
就会开启生产环境下的优化。
如果是使用 commonjs
规范的 library 则需要一个插件支持,babel-plugin-import。该插件是ant
官方开发的。许多 UI 组件库的按需引入也是依赖于这个插件。
安装
yarn add babel-plugin-import -D
修改.babelrc
文件,
"plugins": [
["import", {
"libraryName": "hbf-public-components",
"libraryDirectory": "lib/components"
}]
],
总结
其实如果对于一个不是很稳定,需要一直迭代更新
的公用组件库
来说,使用 npm 包的话,会比较不方便,经常更新的公共组件代码可以使用Git subtree
(教程)来维护,可以等到一定地步之后,公共组件库稳定下来之后再考虑发布一个 npm 包。
而开发一个组件库,也可以使用 rollup.js 来搭脚手架,rollup.js
默认使用的就是 ES2015 Module
,可以进行静态分析,去除未引用的代码,tree-shaking
也是 rollup.js
先提出的。Rollup
相比较于Webpack
,更适合用于构建library
,Vue.js
就是使用 Rollup
构建的。Webpack
在代码分割这方面比较有优势,所以webpack
相对来说比较适合构建应用程序,不过使用 webpack 构建 library 也是可以的。
这个项目也可以用做 webpack 构建 library 的通用脚手架。下次再尝试Rollup
构建 library
有帮助的话,可以给个star的话就最好啦。有问题也可以联系我。
项目地址: https://github.com/huya-base-...