npm发布vue组件

1,828 阅读5分钟

参考:

初始化项目

vue init webpack-simple yyl-npm-practice

//然后根据提示
cd yyl-npm-practice
npm install
npm run dev

为什么不使用vue init webpack npm-practice初始化项目,因为开发组件不需要太多的配置,配置过多会引起配置麻烦,使用webpack-simple足够。

文件目录配置

参考了一下 element ,适合多插件使用,怎么建立文件夹放自己的组件完全取决于你自己,最后只要在webpack的入口和出口配置对应的即可。

├── src/                           // 源码目录
│   ├── packages/                  // 组件目录
│   │   ├── myButton/              // 组件1
    │   │   ├── myButton.vue       // 组件代码
    │   │   ├── index.js           // 挂载插件
        ├── myText/                // 组件2
    │   │   ├── myText.vue         // 组件代码
    │   │   ├── index.js           // 挂载插件
│   ├── App.vue                    // 页面入口
│   ├── main.js                    // 程序入口
│   ├── index.js                   // (所有)插件入口
├── index.html                     // 入口html文件

按需引入,即单组件引入

packages/myButton/myButton.vue:

<template>
    <div>
        MyButton组件
    </div>
</template>


<script>
export default {
    name: 'MyButton',//注意要填写,不然多组件时候进行遍历的时候就无法自动获取组件名称
}
</script>

packages/myButton/index.js:

import MyButton from './myButton.vue'

export default {
    //Vue为vue的构造函数,options为可选配置项
    install(Vue,options){
        Vue.component(MyButton.name, MyButton);
    }
}

以上为止一个组件插件就已经封装成功,最后我们测试发布到npm上即可。

测试使用: main.js引入:

import Vue from 'vue'
import App from './App.vue'

//引入组件使用Vue.use时候会自动执行install方法,从而挂载了组件到Vue构造函数上
import MyButton from './packages/myButton/index.js'
Vue.use(MyButton)

new Vue({
  el: '#app',
  render: h => h(App)
})

App.vue:

<template>
  <div id="app">
    <my-button />
  </div>
</template>

<script>
export default {
  name: 'app'
}
</script>

显示:

image.png

多组件情况下,全局引入所有组件

我们在src目录下,即App.vue同级下创建index.js.
src/index.js:(所有组件的入口)---elementui使用的也是这样形式:

import MyButton from './packages/myButton/myButton.vue'
import MyText from './packages/myText/myText.vue'


const components = [
    MyButton,
    MyText
    // ...如果还有的话继续添加
]

  // 这一步判断window.Vue是否存在,因为直接引用vue.min.js, 它会把Vue绑到Window上,我们直接引用打包好的js才能正常跑起来。
if (typeof window !== 'undefined' && window.Vue) {
    components.map(component => {
        Vue.component(component.name, component);
    })
}

export default {
    //Vue为vue的构造函数,options为可选配置项
    install(Vue,options={}){
        components.map(component => {
            Vue.component(component.name, component);
        })
    }
}

//效果等价于
// const install = function(Vue,options={}) {
//     components.map(component => {
//         Vue.component(component.name, component);
//     })
// }
// export default {
//     install
// }

可以使用动态引入方式取代多个import,优化后:

//使用动态引入方式取代import方式引入
//找到当前同级目录下的packages文件夹,true为为为有文件夹就往下找,/\.vue$/为找到所有以.vue名称结尾的文件,返回的是一个webpackContext对象
const urlList = require.context('./packages',true,/\.vue$/);
console.log('=======',urlList.keys())
//["./myButton/myButton.vue", "./myTable/myTable.vue", "./myText/myText.vue"]
//使用urlList('./myButton/myButton.vue').default;即可实现import一样效果,获取组件实例。

// 这一步判断window.Vue是否存在,因为直接引用vue.min.js, 它会把Vue绑到Window上,我们直接引用打包好的js才能正常跑起来。
if (typeof window !== 'undefined' && window.Vue) {
    urlList.keys().forEach(item=>{
        let compObj = urlList(item).default;//也可以直接根据./myButton/myButton.vue进行切割获取对应的myButton等,而上面每个组件里面都存在有name属性,因此可以直接使用name属性
        Vue.component(compObj.name,compObj)
    })
}

export default {
    //Vue为vue的构造函数,options为可选配置项
    install(Vue,options={}){
         urlList.keys().forEach(item=>{
            let compObj = urlList(item).default;
            Vue.component(compObj.name,compObj)
        })
    }
}

测试 src/main.js:

import Vue from 'vue'
import App from './App.vue'


import AllComponents from './index'
Vue.use(AllComponents)

new Vue({
  el: '#app',
  render: h => h(App)
})

src/App.vue:

<template>
  <div id="app">
    <my-button />
    <my-text />
  </div>
</template>

<script>
export default {
  name: 'app'
}
</script>

显示:

image.png

使用vue的jsx写法写组件

npm i npm i babel-plugin-syntax-jsx babel-plugin-transform-vue-jsx babel-helper-vue-jsx-merge-props -D
//.babelrc
{
  "presets": [
    ["env", { "modules": false }]
  ],
  "plugins": ["transform-vue-jsx"]//添加这个插件
}

使用:

//test.js
export default {
    name: 'Test',
    props: {
        show:{
            type: String
        }
    },
    render(){
        return (
            <div>
                {this.show?'jsx':'xxx‘}
            </div>
        )
    }
}

发布前操作,账号注册和配置

  • 配置修改: webpack.config.js配置开发环境和生产环境入口:
// ... 此处省略代码 
// 执行环境
const NODE_ENV = process.env.NODE_ENV

module.exports = {
  // 根据不同的执行环境配置不同的入口
  entry: NODE_ENV == 'development' ? './src/main.js' : './src/index.js',
  output: {
    // 修改打包出口,在最外级目录打包出一个index.js 文件,我们 import 默认会指向这个文件
    path: path.resolve(__dirname, './dist'),
    publicPath: '/dist/',
    filename: 'outy-test.js',
    library: 'outy-test', // 指定的就是你使用require时的模块名
    libraryTarget: 'umd', // libraryTarget会生成不同umd的代码,可以只是commonjs标准的,也可以是指amd标准的,也可以只是通过script标签引入的
    umdNamedDefine: true // 会对 UMD 的构建过程中的 AMD 模块进行命名。否则就使用匿名的 define
  },
  // ... 此处省略代码 
}

package.json:
private字段(private是true的时候不能发布到npm,需设置成false); 并增加main字段, main字段是require方法可以通过这个配置找到入口文件,这输入模块加载规范。并且查看name是否为重名,重名则需要换一个名称发布,因为发布时候不允许重名。如何查找名称是否被使用过,登录npm进行search查一下,如果查到内容说明存在过。

"name": "outy-test",
// 发布开源因此需要将这个字段改为 false
"private": false,
// 这个指 import outy-test 的时候它会去检索的路径
"main": "dist/outy-test.js",

index.html:
因为刚才我们更改webpack.config.js时候把输入文件的filename默认名称build.js更改为了outy-test.js因此需要对应上。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>outy-test</title>
  </head>
  <body>
    <div id="app"></div>
    <!--原来的-->
    <!-- <script src="/dist/build.js"></script> -->
    <script src="/dist/outy-test.js"></script>
  </body>
</html>
  • 运行打包命令生成dist文件夹,npm run build 如果不需要生成该map文件,可以找到webpack.config.js文件注释掉图示内容。

image.png

image.png

发布到npm

  • 首先注册npm账号
  • npm login,登录
    • 坑(参考):npm源如果为淘宝源会报500错误,必须修改为npm源才能登录: npm config set registry https://registry.npmjs.org/

image.png

  • npm publish,发布
    • 遇到的问题,注册的时候邮箱没验证,打开邮箱验证即可不会出现下面403错误。

image.png 发布成功的图示为:

image.png

image.png

使用

  • 安装 npm i outy-test -S
  • mian.js引入
import OutyTest from 'outy-test'
Vue.use(OutyTest)
  • 使用
<template>
  <div class="home">
    <my-button></my-button>
    <my-text></my-text>
  </div>
</template>
  • 显示

image.png

更新已经存在的包

  • 在package.json修改版本号
  • 再运行npm publish
  • 查看

image.png

删除指定版本的包

  • npm unpublish outy-test@1.0.0,npm unpublish 包名@版本号
  • 查看

image.png

删除整个包

  • npm unpublish 包名 --force

对element-ui的el-table进行二次封装

以webpack-simple为模板。

  • 安装 npm i element-ui -S
  • 按需引入element-ui npm install babel-plugin-component -D 然后,将 .babelrc 修改为:
{
  "presets": [["es2015", { "modules": false }]],
  "plugins": [
    [
      "component",
      {
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      }
    ]
  ]
}
  • 安装loader和配置loader
npm i style-loader url-loader -D

//其他需要的loader
less-loader
sass-loader
  • main.js引入
import { Table,TableColumn } from 'element-ui';
Vue.use(Table)
Vue.use(TableColumn)

webpack.config.js:

{
    test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
    loader: 'url-loader',
    options: {
        limit: 10000,
        name: '[name].[ext]'
    }
}
  • 封装table组件 packages/myTable/myTable.vue:
<template>
  <el-table
    :data="tableData"
    style="width: 100%"
    :row-class-name="tableRowClassName">
    <el-table-column
      prop="date"
      label="日期"
      width="180">
    </el-table-column>
    <el-table-column
      prop="name"
      label="姓名"
      width="180">
    </el-table-column>
    <el-table-column
      prop="address"
      label="地址">
    </el-table-column>
  </el-table>
</template>

<style>
  .el-table .warning-row {
    background: oldlace;
  }

  .el-table .success-row {
    background: #f0f9eb;
  }
</style>

<script>
  export default {
    name:'MyTable',
    methods: {
      tableRowClassName({row, rowIndex}) {
        if (rowIndex === 1) {
          return 'warning-row';
        } else if (rowIndex === 3) {
          return 'success-row';
        }
        return '';
      }
    },
    data() {
      return {
        tableData: [{
          date: '2016-05-02',
          name: '王小虎',
          address: '上海市普陀区金沙江路 1518 弄',
        }, {
          date: '2016-05-04',
          name: '王小虎',
          address: '上海市普陀区金沙江路 1518 弄'
        }, {
          date: '2016-05-01',
          name: '王小虎',
          address: '上海市普陀区金沙江路 1518 弄',
        }, {
          date: '2016-05-03',
          name: '王小虎',
          address: '上海市普陀区金沙江路 1518 弄'
        }]
      }
    }
  }
</script>
  • 引入和使用Vue.compoment注册部分参考上面。
  • 测试使用:
<div id="app">
    <my-table />
</div>

image.png

elemetui封装的组件学习

tag.vue:

<script>
  export default {
    name: 'ElTag',
    props: {
      text: String,
      closable: Boolean,
      type: String,
      hit: Boolean,
      disableTransitions: Boolean,
      color: String,
      size: String,
      effect: {
        type: String,
        default: 'light',
        validator(val) {
          return ['dark', 'light', 'plain'].indexOf(val) !== -1;
        }
      }
    },
    methods: {
      handleClose(event) {
        event.stopPropagation();
        this.$emit('close', event);
      },
      handleClick(event) {
        this.$emit('click', event);
      }
    },
    computed: {
      tagSize() {
        return this.size || (this.$ELEMENT || {}).size;
      }
    },
    render(h) {
      const { type, tagSize, hit, effect } = this;
      const classes = [
        'el-tag',
        type ? `el-tag--${type}` : '',
        tagSize ? `el-tag--${tagSize}` : '',
        effect ? `el-tag--${effect}` : '',
        hit && 'is-hit'
      ];
      const tagEl = (
        <span
          class={ classes }
          style={{ backgroundColor: this.color }}
          on-click={ this.handleClick }>
          { this.$slots.default }
          {
            this.closable && <i class="el-tag__close el-icon-close" on-click={ this.handleClose }></i>
          }
        </span>
      );

      return this.disableTransitions ? tagEl : <transition name="el-zoom-in-center">{ tagEl }</transition>;
    }
  };
</script>

card.vue:

<template>
  <div class="el-card" :class="shadow ? 'is-' + shadow + '-shadow' : 'is-always-shadow'">
    <div class="el-card__header" v-if="$slots.header || header">
      <slot name="header">{{ header }}</slot>
    </div>
    <div class="el-card__body" :style="bodyStyle">
      <slot></slot>
    </div>
  </div>
</template>

<script>
  export default {
    name: 'ElCard',
    props: {
      header: {},
      bodyStyle: {},
      shadow: {
        type: String
      }
    }
  };
</script>

简单总结:

//如果使用为
<my-button @click="xxx" @change="bbb" size="small">
    <span>默认插槽</span>
</my-button>


//组件应该要这样对应
+ 属性就必须有props进行接收,一定情况下还需要配合computed一起使用
+ 事件肯定有this.$emit('clcik',参数),this.$emit('change',参数)等
+ 有默认插槽,组件使用为<span v-if="$slots.default><slot></slot></span>