最近学习了如何搭建自己的UI组件库并发布到npm,整理一下分享给大家。有写得不好的地方麻烦留言指点一下,谢谢~
一、创建组件库项目
我是通过vue-cli脚手架搭建的一个vue项目,搭建好之后在根目录下新建一个packages文件夹,用于存放自己封装的组件,在根目录下的src文件夹下新建一个docs文件夹,用于存放UI组件库的通过.md文件渲染的各个页面。在packages文件夹下新建一个index.js,作为抛出组件的出口;文件目录如下图所示:
这里以button为例封装个简单的组件,在packages文件夹下新建button文件夹,用于存放自己封装的button组件源代码,在button文件夹下新建一个src文件夹和index.js文件,index.js文件用于抛出button组件以便install下载安装使用,在packages/button/src路径下新建一个button.vue的文件用于编写xm-button组件,具体代码如下图所示:
注意:编写组件时,组件的name不可缺少,否则install组件失败!
编写完xm-button组件之后,继续编辑其他组件,和button一样创建单独的文件夹存放,统一在/packages/index.js文件抛出,使组件在UI库导入时能自动在Vue中注册我们的Component,将所写组件都统一导入进该文件里,代码如下图所示:
写好组件之后我们需要用markdown编写UI组件库的页面展示,在/packages/src/docs/路径下新建一个button.md文件,用于编写xm-button组件的展示页面,具体代码如下图所示:
页面效果如下图所示:
二、项目配置
UI组件库项目写好之后,就开始配置部署到npm上啦~
在根目录的/build文件夹下新建publish.js文件,用于编写实现一键部署发布到npm的功能代码,具体代码如下图所示,然后在根目录的package.json文件添加脚本命令publish,用于一键部署发布到npm上,如下图所示:
注意:版本号每次发布都需要更新
markdown的书写需要在webpack.base.conf.js配置,相关webpack的配置如下所示:
'use strict'
const path = require('path')
const utils = require('./utils')
const config = require('../config')
const vueLoaderConfig = require('./vue-loader.conf')
const MarkdownItContainer = require('markdown-it-container')
const striptags = require('./strip-tags')
const vueMarkdown = {
preprocess: (MarkdownIt, source) => {
// console.log("test-----",source,);
//source代表文档所有内容
MarkdownIt.renderer.rules.table_open = function () { //为说明文档表格添加样式
return '<table class="table">'
}
//添加高亮显示
MarkdownIt.renderer.rules.fence = utils.wrapCustomClass(MarkdownIt.renderer.rules.fence)
// ```html `` 给这种样式加个class hljs
// 但是markdown-it 有个bug fence整合attr的时候直接加载class数组上而不是class的值上
// markdown-it\lib\renderer.js 71行 这么修改可以修复bug
// tmpAttrs[i] += ' ' + options.langPrefix + langName; --> tmpAttrs[i][1] += ' ' + options.langPrefix + langName;
// const fence = MarkdownIt.renderer.rules.fence
// MarkdownIt.renderer.rules.fence = function(...args){
// args[0][args[1]].attrJoin('class', 'hljs')
// var a = fence(...args)
// return a
// }
// ```code`` 给这种样式加个class code_inline
const code_inline = MarkdownIt.renderer.rules.code_inline
MarkdownIt.renderer.rules.code_inline = function(...args){
args[0][args[1]].attrJoin('class', 'code_inline')
return code_inline(...args)
}
return source
},
use: [
[MarkdownItContainer, 'demo', {
//匹配```demo ``
validate: params => params.trim().match(/^demo\s*(.*)$/),
render: function(tokens, idx) {
var m = tokens[idx].info.trim().match(/^demo\s*(.*)$/);
//nesting 有 1 0 -1 如果小于0说明没有该属性
if (tokens[idx].nesting === 1) { // 判断是否是开始标签
var desc = tokens[idx + 2].content;
// 获取代码块内的html和js代码
const html = utils.convertHtml(striptags(tokens[idx + 1].content, 'script'))
// 移除描述,防止被添加到代码块
tokens[idx + 2].children = [];
// console.log(desc,tokens,"test123",idx,html);
// 使用自定义开发组件【DemoBlock】来包裹内容并且渲染成案例和代码示例
return `<demo-block>
<div slot="desc">${html}</div>
<div slot="highlight">`;
}
return '</div></demo-block>\n';
}
}]
]
}
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
const createLintingRule = () => ({
test: /.(js|vue)$/,
loader: 'eslint-loader',
enforce: 'pre',
include: [resolve('src'), resolve('test')],
options: {
formatter: require('eslint-friendly-formatter'),
emitWarning: !config.dev.showEslintErrorsInOverlay
}
})
module.exports = {
context: path.resolve(__dirname, '../'),
entry: {
app: './src/main.js'
},
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
"@packages":resolve('packages'),
"@components":resolve('src/components'),
"@utils":resolve('src/utils'),
}
},
module: {
rules: [
...(config.dev.useEslint ? [createLintingRule()] : []),
{
test: /.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
test: /.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client'),resolve('packages')]
},
{
test: /.(png|jpe?g|gif|svg)(?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
test: /.(mp4|webm|ogg|mp3|wav|flac|aac)(?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('media/[name].[hash:7].[ext]')
}
},
{
test: /.(woff2?|eot|ttf|otf)(?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
},
{
test: /.md$/,
loader: 'vue-markdown-loader',
options: vueMarkdown
}
]
},
node: {
// prevent webpack from injecting useless setImmediate polyfill because Vue
// source contains it (although only uses it if it's native).
setImmediate: false,
// prevent webpack from injecting mocks to Node native modules
// that does not make sense for the client
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty'
}
}
三、发布npm前需要登录npm账号
配置完成之后,我们开始准备发布到npm上,先去NPM官网上注册一个npm号,需要设置账号、密码和邮箱验证,注册完成后,怎么通过命令实现发布到npm呢?
我们可以在项目中打开命令行工具输入npm login命令,然后回车输入账号、密码及邮箱验证码完成登录,登录成功就可以打包上传组件库啦,执行npm run publish即可打包并推送代码到npm上了。
注意:
1、确保项目名称没有被别人注册过,最好先在NPM官网上搜索确认是否有同名库,如有同名会发布失败;
2、每次推送发布,一定要改package.json里的版本号;
推送成功之后,就可以在NPM官网登录自己的账号查看刚刚发布的组件库了,如下图所示:
完成以上操作后,我们就可以直接在其他项目中下载使用我们发布的组件库啦,直接通过npm install 项目名称即可,同时在main.js引入该组件库,就可以在项目中全局使用了,同elementUi一致的使用方法。