从零开始使用Vue Cli做一个自己的组件库

2,023 阅读4分钟

一、使用Vue Cli创建项目

1、安装Vue Cli

npm install -g @vue/cli
# OR
yarn global add @vue/cli

2、创建项目

vue create test-ui-vue

二、修改目录,以及配置文件

1、将src目录改为examples用来展示组件。

2、创建同级别的目录packages,用来存放自定义组件。

3、由于修改了目录,所以需要重新新配置webpack,先在最外层创建vue.config.js。现在的目录结构如下:

TEST-UI-VUE
├── examples                   # demo源码
│   ├── assets                 # 主题 字体等静态资源
│   ├── components             # 全局公用组件
│   ├── router                 # 路由
│   ├── views                  # views 所有页面
│   ├── App.vue                # 入口页面
│   └── main.js                # 入口文件 加载组件 初始化等
├── packages                   # 自定义组件模板
├── public                     # 静态资源
│   │── favicon.ico            # favicon图标
│   └── index.html             # html模板
├── src                        # 自定义组导出
│   └── index.js               # 自定义组件入口,在此处导出所有组件
├── tests                      # 测试
├── .browserslistrc            # 浏览器和 node 版本的配置
├── .eslintrc.js               # eslint 配置项.
├── .gitignore                 # gitignore
├── babel.config.js            # babel-loader 配置
├── jest.config.js             # jest 配置
├── package.json               # package.json
├── README.md                  # README.md
└── vue.config.js              # vue-cli 配置

4、配置vue.config.js文件并运行项目。

const path = require('path')
module.exports = {
  // 修改 pages 入口
  pages: {
    index: {
      entry: 'examples/main.js', // 入口
      template: 'public/index.html', // 模板
      filename: 'index.html' // 输出文件
    }
  },
  // 扩展 webpack 配置
  chainWebpack: config => {
    // @ 默认指向 src 目录,这里要改成 examples
    // 另外也可以新增一个 ~ 指向 packages
    config.resolve.alias
      .set('@', path.resolve('examples'))
      .set('~', path.resolve('packages'))
	// 把 packages 和 examples 加入编译,因为新增的文件默认是不被 webpack 处理的
    config.module
      .rule('js')
      .include.add(/packages/)
      .end()
      .include.add(/examples/)
      .end()
      .use('babel')
      .loader('babel-loader')
      .tap(options => {
        // 修改它的选项...
        return options
      })
  }
}

三、新建组件

1、在packages下面新建一个button组件的文件夹,同时新建一个index.js文件,用来导出所有组件。目录结构如下:

TEST-UI-VUE
├── examples                   # demo源码
│   ├── assets                 # 主题 字体等静态资源
│   ├── components             # 全局公用组件
│   ├── router                 # 路由
│   ├── views                  # views 所有页面
│   ├── App.vue                # 入口页面
│   └── main.js                # 入口文件 加载组件 初始化等
├── packages                   # 自定义组件模板
│   └── button                 # button组件
│   │   │── src                # button组件源码
│   │   │	└── button.vue     # button组件模板
│   │   └── index.js           # button组件入口,导出button组件
├── public                     # 静态资源
│   │── favicon.ico            # favicon图标
│   └── index.html             # html模板
├── src                        # 自定义组导出
│   └── index.js               # 自定义组件入口,在此处导出所有组件
├── tests                      # 测试
├── .browserslistrc            # 浏览器和 node 版本的配置
├── .eslintrc.js               # eslint 配置项.
├── .gitignore                 # gitignore
├── babel.config.js            # babel-loader 配置
├── jest.config.js             # jest 配置
├── package.json               # package.json
├── README.md                  # README.md
└── vue.config.js              # vue-cli 配置

2、在button.vue中编写组件

<template>
  <button
    class="ts-button"
    :class="{ 'is-disabled': disabled }"
    :disabled="disabled"
  >
    <span><slot>测试按钮</slot></span>
  </button>
</template>
<script>
export default {
  name: "TsButton", // 注意这个name是必须的
  props: {
    disabled: {
      type: Boolean,
      default: false,
    },
  },
};
</script>
<style lang="scss">
.ts-button {
  display: inline-block;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  background: #fff;
  border: 1px solid #dcdfe6;
  color: #606266;
  -webkit-appearance: none;
  text-align: center;
  box-sizing: border-box;
  outline: none;
  margin: 0;
  transition: 0.1s;
  font-weight: 500;
  user-select: none;
  padding: 12px 20px;
  font-size: 14px;
  border-radius: 4px;
  &:focus,
  &:hover {
    color: #409eff;
    border-color: #c6e2ff;
    background-color: #ecf5ff;
  }
  &.is-disabled,
  &.is-disabled:focus,
  &.is-disabled:hover {
    color: #c0c4cc;
    cursor: not-allowed;
    background-image: none;
    background-color: #fff;
    border-color: #ebeef5;
  }
}
</style>

3、在/packages/button/src/index.js中暴露组件

import TsButton from './src/button';

/* istanbul ignore next */
TsButton.install = function(Vue) {
  Vue.component(TsButton.name, TsButton);
};

export default TsButton;

4、最后在/src/index.js中导出所有组件

import TsButton from "../packages/button/index.js";

// 所有组件列表
const components = [TsButton];

// 定义install方法,接收Vue作为参数
const install = function (Vue) {
  // 判断是否安装,安装过就不继续往下执行
  if (install.installed) return;

  install.installed = true;

  // 遍历注册所有组件
  components.map((component) => Vue.use(component));
};

// 检测到Vue才执行,毕竟我们是基于Vue的
if (typeof window !== "undefined" && window.Vue) {
  install(window.Vue);
}

export default {
  install,
  // 所有组件,必须具有install,才能使用Vue.use()
  ...components,
};

5、在项目中测试组件

main.js中引入组件

// 引入组件
import TestUi from '../src/index'
Vue.use(TestUi)

四、发布组件进行测试

1、在 package.jsonscripts 字段中新增一下命令:

"lib": "vue-cli-service build --target lib --name test-ui-vue --dest lib src/index.js"

--target: 构建目标,默认为应用模式。这里修改为 lib 启用库模式。

--dest : 输出目录,默认 dist。这里我们改成 lib

[entry]: 最后一个参数为入口文件,这里我们指定编译 /src/index.js/ 组件库入口文件。

2、编译组件库

执行命令 npm run lib,会发现目录下多了lib文件夹

package.json其他配置,配置如下

{
  "name": "test-ui-vue",
  "version": "0.1.2",
  "description": "从零开始做一个基于Vue Cli的组件库",
  "main": "lib/test-ui-vue.umd.min.js",
  "author": "xuyong",
  "license": "MIT",
  "keywords": [
    "test-ui"
  ],
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lib": "vue-cli-service build --target lib --name test-ui-vue --dest lib src/index.js",
    "test:unit": "vue-cli-service test:unit",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "core-js": "^3.6.5",
    "vue": "^2.6.11",
    "vue-router": "^3.2.0"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/cli-plugin-router": "~4.5.0",
    "@vue/cli-plugin-unit-jest": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "@vue/eslint-config-prettier": "^6.0.0",
    "@vue/test-utils": "^1.0.3",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-prettier": "^3.3.1",
    "eslint-plugin-vue": "^6.2.2",
    "lint-staged": "^9.5.0",
    "prettier": "^2.2.1",
    "sass": "^1.26.5",
    "sass-loader": "^8.0.2",
    "vue-template-compiler": "^2.6.11"
  },
  "gitHooks": {
    "pre-commit": "lint-staged"
  },
  "lint-staged": {
    "*.{js,jsx,vue}": [
      "vue-cli-service lint",
      "git add"
    ]
  }
}

3、添加.npmignore 文件,发布时,只有编译后的 lib 目录、package.jsonREADME.md才需要被发布。所以通过配置.npmignore文件忽略不需要提交的目录和文件。

# 这是复制 .gitignore 里面的
.DS_Store
node_modules
/dist

# local env files
.env.local
.env.*.local

# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*

# 以下是新增的
# 要忽略目录和指定文件
examples/
packages/
public/
vue.config.js
babel.config.js
*.map
*.html

4、发布到npm

现需要去npm官网注册账号

然后本地登录:npm login

最后发布到npm: npm publish

5、最后就可以测试了:

另起一个项目

npm i test-ui-vue

main.js中引用

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
// 引入组件
import TestUiVue from 'test-ui-vue'
import 'test-ui-vue/lib/test-ui-vue.css'

Vue.use(TestUiVue)

Vue.config.productionTip = false;

new Vue({
  router,
  render: (h) => h(App),
}).$mount("#app");

app.vue中调用组件:

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </div>
    <router-view />
    <ts-button>测试按钮</ts-button>
  </div>
</template>

五、添加markdown插件,使项目支持使用markdown直接编写文档并显示到页面

1、安装markdown插件

npm i vue-markdown-loader -D

2、修改vue.config.js, 添加vue-markdown-loader,使用webpack能识别md文件

const path = require("path");
module.exports = {
  // 修改 pages 入口
  pages: {
    index: {
      entry: "examples/main.js", // 入口
      template: "public/index.html", // 模板
      filename: "index.html", // 输出文件
    },
  },
  // 扩展 webpack 配置
  chainWebpack: (config) => {
    // @ 默认指向 src 目录,这里要改成 examples
    // 另外也可以新增一个 ~ 指向 packages
    config.resolve.alias
      .set("@", path.resolve("examples"))
      .set("~", path.resolve("packages"));
    // 把 packages 和 examples 加入编译,因为新增的文件默认是不被 webpack 处理的
    config.module
      .rule("js")
      .include.add(/packages/)
      .end()
      .include.add(/examples/)
      .end()
      .use("babel")
      .loader("babel-loader")
      .tap((options) => {
        // 修改它的选项...
        return options;
      });
    config.module
      .rule("md")
      .test(/\.md/)
      .use("vue-loader")
      .loader("vue-loader")
      .end()
      .use("vue-markdown-loader")
      .loader("vue-markdown-loader/lib/markdown-compiler")
      .options({
        raw: true,
      });
  },
};

3、创建存放文档的目录及md文件

examples 目录下创建 docs 文件夹,在docs文件夹下创建 test.md,文件内容如下

## tip

:::tip
这是一个 tip。这是一个 tip。这是一个 tip。这是一个 tip。这是一个 tip。这是一个 tip。
:::

## warning

:::warning
这是一个 warning,**这是一个 warning,**。
这是一个 warning。
这是一个 warning,这是一个 warning,这是一个 warning,这是一个 warning。
:::

## demo

:::demo 这是一个 demo。这是一个 demo。这是一个 demo。这是一个 demo。这是一个 demo。这是一个 demo。这是一个 demo。这是一个 demo。

```html
<sq-button></sq-button>
```

:::

test.md 添加进路由进行测试

router/index.js中添加

{
   path: '/test',
   name: 'test',
   component: () => import(/* webpackChunkName: "about" */ '../docs/test.md')
 }

4、安装其他的markdown插件

cnpm i markdown-it markdown-it-container -S

再次修改vue.config.js文件

const path = require("path");
// 引入markdown-it
const md = require("markdown-it")();

/**
 * 增加 hljs 的 classname
 */
function wrapCustomClass(render) {
  return function (...args) {
    return render(...args)
      .replace('<code class="', '<code class="hljs ')
      .replace('<code>', '<code class="hljs">')
  }
}

module.exports = {
  // 修改 pages 入口
  pages: {
    index: {
      entry: "examples/main.js", // 入口
      template: "public/index.html", // 模板
      filename: "index.html", // 输出文件
    },
  },
  // 扩展 webpack 配置
  chainWebpack: (config) => {
    // @ 默认指向 src 目录,这里要改成 examples
    // 另外也可以新增一个 ~ 指向 packages
    config.resolve.alias
      .set("@", path.resolve("examples"))
      .set("~", path.resolve("packages"));
    // 把 packages 和 examples 加入编译,因为新增的文件默认是不被 webpack 处理的
    config.module
      .rule("js")
      .include.add(/packages/)
      .end()
      .include.add(/examples/)
      .end()
      .use("babel")
      .loader("babel-loader")
      .tap((options) => {
        // 修改它的选项...
        return options;
      });
    config.module
      .rule("md")
      .test(/\.md/)
      .use("vue-loader")
      .loader("vue-loader")
      .end()
      .use("vue-markdown-loader")
      .loader("vue-markdown-loader/lib/markdown-compiler")
      .options({
        raw: true,
        preventExtract: true, // 这个加载器将自动从html令牌内容中提取脚本和样式标签
        // 定义处理规则
        preprocess: (MarkdownIt, source) => {
          MarkdownIt.renderer.rules.table_open = function () {
            return '<table class="table">';
          };

          MarkdownIt.renderer.rules.fence = wrapCustomClass(MarkdownIt.renderer.rules.fence)

          // ```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: [
          // :::demo ****
          [
            require("markdown-it-container"),
            "demo",
            {
              validate: function (params) {
                return params.trim().match(/^demo\s*(.*)$/);
              },

              render: function(tokens, idx) {
        
                if (tokens[idx].nesting === 1) {
 
                  return `<demo-block>
                                <div slot="highlight">`;
                }
                return '</div></demo-block>\n';
              }
            },
          ],
          [require("markdown-it-container"), "tip"],
          [require("markdown-it-container"), "warning"],
        ],
      });
  },
};

在 examples/components 下添加 DemoBlock.vue,内容如下:

<template>
  <div class="docs-demo-wrapper">
      <div :style="{height: isExpand ? 'auto' : '0'}" class="demo-container">
        <div span="14">
          <div class="docs-demo docs-demo--expand">
            <div class="highlight-wrapper">
              <slot name="highlight"></slot>
            </div>
          </div>
        </div>
      </div>
    <span class="docs-trans docs-demo__triangle" @click="toggle">{{isExpand ? '隐藏代码' : '显示代码'}}</span>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        isExpand: false
      };
    },
    methods: {
      toggle() {
        this.isExpand = !this.isExpand;
      }
    }
  };
</script>

<style lang="scss">
  .demo-container {
    transition: max-height .3s ease;
    overflow: hidden;
  }
  .docs-demo {
    width: 100%;
    height: auto;
    box-sizing: border-box;
    font-size: 14px;
    background-color: #F7F7F7;
    border: 1px solid #e2ecf4;
    border-top: none;
    pre code {
      font-family: Consolas,Menlo,Courier,monospace;
      line-height: 22px;
      border: none;
    }
  }
  .docs-trans {
    width: 100%;
    text-align: center;
    display: inline-block;
    color: #C5D9E8;
    font-size: 12px;
    padding: 10px 0;
    background-color: #FAFBFC;
  }
  .docs-demo__code,
  .highlight-wrapper,
  .docs-demo__meta {
    padding: 0 20px;
    overflow-y: auto;
  }
  .docs-demo__code {
    border-bottom: 1px solid #eee;
  }
  .docs-demo.docs-demo--expand .docs-demo__meta {
    border-bottom: 1px dashed #e9e9e9;
  }
  .docs-demo.docs-demo--expand .docs-demo__triangle {
    transform: rotate(180deg);
  }
  .highlight-wrapper {
    display: none;
    p,
    pre {
      margin: 0;
    }
    .hljs {
      padding: 0;
    }
  }
  .docs-demo.docs-demo--expand .highlight-wrapper {
    display: block;
  }
  .docs-demo__code__mobi {
    height: 620px;
    margin: 20px 0;
  }
  .docs-demo__code__mobi__header {
    border-radius: 4px 4px 0 0;
    background: -webkit-linear-gradient(rgba(55,55,55,.98),#545456);
    background: linear-gradient(rgba(55,55,55,.98),#545456);
    text-align: center;
    padding: 8px;
    img {
      width: 100%;
    }
    .url-box {
      height: 28px;
      line-height: 28px;
      color: #fff;
      padding: 0 3px;
      background-color: #a2a2a2;
      margin: 10px auto 0;
      border-radius: 4px;
      white-space: nowrap;
      overflow-x: auto;
    }
  }
  .docs-demo__code__mobi__content {
    iframe {
      width: 100%;
      border: 0;
      height: 548px;
    }
  }
</style>

然后在main.js中引用DemoBlock组件

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
// 引入组件
import DemoBlock from './components/DemoBlock.vue'
import TestUi from "../src/index";

Vue.use(TestUi);

Vue.component('DemoBlock', DemoBlock)

Vue.config.productionTip = false;

new Vue({
  router,
  render: (h) => h(App),
}).$mount("#app");

重新运行项目就可以看到效果了。

5、现在效果应该都出来了,可以给代码添加高亮,使其更漂亮。

npm i highlight.js -S

再在main.js中添加如下配置,然后代码就能语法高亮了!

import 'highlight.js/styles/vs.css'