如何从零到一打造前端组件库

1,324 阅读7分钟

1、初始化 npm

项目文件夹内执行 npm init -y,执行成功目录结构如下:

|- project/          --------------  项目文件
    |- package.json  --------------  npm 配置文件 

2、初始化目录结构

项目文件夹内执行 npm init -y,执行成功目录结构如下:

|- project/             --------------  项目文件
    |- build            --------------  webpack 配置文件夹
    |- index.js         --------------  入口文件
    |- package.json     --------------  npm 配置文件
    |- packages         --------------  组件文件夹
    |- style           --------------  组件样式文件夹

3、配置公共 webpack 配置

在 build 文件夹内新建一个 webpack.base.js 文件:

|- project/                 --------------  项目文件
    |- build                --------------  webpack 配置文件夹
        |- webpack.base.js  --------------  webpack 公共配置
    |- index.js             --------------  入口文件
    |- package.json         --------------  npm 配置文件
    |- packages             --------------  组件文件夹
    |- style                --------------  组件样式文件夹

写入如下配置

const webpack = require('webpack');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const path = require('path');
const resolve = function(dir) {
  return path.join(__dirname, '..', dir);
};

module.exports = {
  module: {
    rules: [
      {
        test: /\.(js|vue)$/,
        loader: 'eslint-loader',
        enforce: 'pre',
        include: [resolve('packages')], // 指定检查的目录
        options: {
          formatter: require('eslint-friendly-formatter')
        }
      },
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      {
        test: /\.js$/,
        loader: 'babel-loader',
        exclude: /node_modules/
      },
      {
        test: /\.css$/,
        loaders: [
          {
            loader: 'style-loader'
          },
          {
            loader: 'css-loader'
          },
          {
            loader: 'sass-loader'
          }
        ]
      },
      {
        test: /\.(gif|jpg|png|woff|svg|eot|ttf)\??.*$/,
        loader: 'url-loader?limit=8192'
      }
    ]
  },
  plugins: [
    new webpack.optimize.ModuleConcatenationPlugin(),
    new VueLoaderPlugin()
  ],
  resolve: {
    extensions: ['.js', '.vue'],
    alias: {
      style: resolve('style'),
      packages: resolve('packages')
    }
  }
};

以上配置就都只是一些常用的 loader ,和使用了一个 webpack 的优化打包的插件 new webpack.optimize.ModuleConcatenationPlugin() ,一下是官方解释的这个插件用途。

以上配置需要安装以下 npm 包:

npm i -D webpack webpack-cli vue-loader vue-template-compiler babel-loader @babel/core @babel/preset-env style-loader css-loader url-loader sass-loader

因为装有 babel-loader ,得创建一个 .babelrc 的配置文件。

|- project/                 --------------  项目文件
    |- .babelrc             ------------- babel 配置文件
    |- build                --------------  webpack 配置文件夹
        |- webpack.base.js  --------------  webpack 公共配置
    |- index.js             --------------  入口文件
    |- package.json         --------------  npm 配置文件
    |- packages             --------------  组件文件夹
    |- style                --------------  组件样式文件夹

并写入如下配置:

{
  "presets": [
    "@babel/preset-env"
  ]
}

4、开始编写组件

创建一个 YsCustomPanel组件

|- project/                 --------------  项目文件
    |- .babelrc             ------------- babel 配置文件
    |- build                --------------  webpack 配置文件夹
        |- webpack.base.js  --------------  webpack 公共配置
    |- index.js             --------------  入口文件
    |- package.json         --------------  npm 配置文件
    |- packages             --------------  组件文件夹
        |- custom-panel     --------------  自定义面板组件文件夹
            |- src          --------------  YsCustomPanel组件源代码文件夹
                |- main.vue --------------  组件源代码
            |- index.js     --------------  引用组件并暴露install方法
    |- style                --------------  组件样式文件夹

组件的的目录结构借鉴了 element-ui ,方便 webpack 打包成按需加载的结构。

custom-panel/src/main.vue 代码如下:


<div class="ys-custom-panel">
  ...
</div>

custom-panel/src/index.js 代码如下:


import YsCustomPanel from './src/main';

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

export default YsCustomPanel;

每个组件都要有 name 属性,还是以custom-panel为例。


export default {
    name: 'YsCustomPanel'
}

样式并没有写在 vue 文件内部,全部都单独写到了 style 文件夹内。

YsCustomPanel组件样式:

@import "./var.scss";
.ys-custom-panel {
  &__header {
    position: fixed;
    top: 0;
    height: 60px;
    z-index: 99;
    display: flex;
    align-items: center;
    h3 {
      font-size: 18px;
      margin-right: 13.5px;
      opacity: 0.85;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      max-width: 100%;
      display: -webkit-box;
      display: -ms-flexbox;
      display: flex;
      color: #000000;
      font-weight: bold;
    }
  }

  .back-button.iconfont {
    font-size: 24px !important;
    padding-right: 8px !important;
    float: left;
    font-weight: bold;
    color: $--color-primary;
    cursor: pointer;
    &:hover{
      color: $--color-primary;
    }
  }
}

入口文件 index.js 编写:

import YsCustomPanel from './packages/custom-panel/index.js';

const components = [
  YsCustomPanel
];

const install = function(Vue) {
  components.forEach(component => {
    Vue.component(component.name, component);
  });
};

if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue);
}

export default {
  install,
  YsCustomPanel
};

引入写好的组件,每个组件添加相应的 install 方法,这样注册组件的时候就可以使用 Vue.use() 进行注册。

4、配置完全引入 webpack 打包配置

|- project/                 --------------  项目文件
    |- .babelrc             -------------   babel 配置文件
    |- build                --------------  webpack 配置文件夹
        |- webpack.base.js  --------------  webpack 公共配置
        |- webpack.prod.js  --------------  全量打包 webpack 配置
    |- index.js             --------------  入口文件
    |- package.json         --------------  npm 配置文件
    |- packages             --------------  组件文件夹
        |- custom-panel     --------------  自定义面板组件文件夹
            |- src          --------------  YsCustomPanel组件源代码文件夹
                |- main.vue --------------  组件源代码
            |- index.js     --------------  引用组件并暴露install方法
    |- style                --------------  组件样式文件夹

// build/webpack.prod.js
const path = require('path');
const merge = require('webpack-merge');
const webpackBaseConfig = require('./webpack.base.js');

module.exports = merge(webpackBaseConfig, {
  mode: 'production',
  entry: {
    main: path.resolve(__dirname, '../index.js')
  },
  output: {
    path: path.resolve(__dirname, '../dist'),
    publicPath: '/dist/',
    filename: 'index.js',
    library: 'my-library',
    libraryTarget: 'umd',
    umdNamedDefine: true
  },
  externals: {
    vue: {
      root: 'Vue',
      commonjs: 'vue',
      commonjs2: 'vue',
      amd: 'vue'
    }
  }
});

这里用到了 webpack-merge 这个工具,需要 npm 安装,这个工具主要以追加的形式合并 webpack 配置。

npm i -D webpack-merge

以上配置大概就是合并了 webpack.base.js 里的 loader 配置,设置了入口文件,输出文件路径,并以 umd 的模式进行打包,把 vue 设置成外部依赖,不需要打包进输出文件内。

再配置一下 package.json 的 script ,添加一条打包命令。

{
  "name": "my-library",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build:prod": "webpack --config build/webpack.prod.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.10.2",
    "@babel/preset-env": "^7.10.2",
    "babel-loader": "^8.1.0",
    "css-loader": "^3.5.3",
    "style-loader": "^1.2.1",
    "url-loader": "^4.1.0",
    "vue-loader": "^15.9.2",
    "vue-template-compiler": "^2.6.11",
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.11",
    "webpack-merge": "^4.2.2"
  }
}

控制台执行 npm run build:prod 即可打包完成,打包完成后多出了一个 dist 文件夹,文件夹下有个 index.js 文件,这个文件就是全量组件的 js 文件。

6、组件的样式文件打包

组件的的逻辑代码已经完成,但是样式却没有,样式需要借助 gulp 进行打包处理,gulp 打包 css 比 webpack 的方便一些,所以选择了 gulp 。

在 style 文件夹下新建一个 gulpfile.js 文件。

|- project/                         --------------  项目文件
    |- .babelrc                     -------------   babel 配置文件
    |- build                        --------------  webpack 配置文件夹
        |- webpack.base.js          --------------  webpack 公共配置
        |- webpack.prod.js          --------------  全量打包 webpack 配置
    |- index.js                     --------------  入口文件
    |- package.json                 --------------  npm 配置文件
    |- packages                     --------------  组件文件夹
        |- custom-panel             --------------  自定义面板组件文件夹
            |- src                  --------------  YsCustomPanel组件源代码文件夹
                |- main.vue         --------------  组件源代码
            |- index.js             --------------  引用组件并暴露install方法
    |- style                        --------------  组件样式文件夹
        |- src                      --------------  样式源代码文件夹
            |- index.scss           --------------  总样式文件
            |- ys-custom-panel.scss --------------  custom-panel样式文件

写下如下代码:

var gulp = require('gulp');
var sass = require('gulp-sass');
var autoprefixer = require('gulp-autoprefixer');
var cssmin = require('gulp-cssmin');

function compile(cb) {
  gulp
    .src('./src/*.scss')
    .pipe(sass.sync())
    .pipe(
      autoprefixer({
        browsers: ['ie > 9', 'last 2 versions'],
        cascade: false
      })
    )
    .pipe(cssmin())
    .pipe(gulp.dest('../dist/style/'));

  cb();
}

function copyfont(cb) {
  gulp
    .src('./src/fonts/**')
    .pipe(cssmin())
    .pipe(gulp.dest('../dist/style/fonts/'));
  cb();
}

exports.build = gulp.series(compile, copyfont);

需要安装一下几个包。

npm i -D gulp gulp-sass gulp-autoprefixer gulp-cssmin

全量加载组件的时候,就会用到这个 index.scss 的样式。

package.json 文件添加打包样式的命令。

{
  "name": "my-library",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build:theme": "gulp build --gulpfile style/gulpfile.js",
    "build:prod": "webpack --config build/webpack.prod.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.10.2",
    "@babel/preset-env": "^7.10.2",
    "babel-loader": "^8.1.0",
    "css-loader": "^3.5.3",
    "gulp": "^4.0.2",
    "gulp-autoprefixer": "^7.0.1",
    "gulp-clean-css": "^4.3.0",
    "gulp-less": "^4.0.1",
    "gulp-rename": "^2.0.0",
    "style-loader": "^1.2.1",
    "url-loader": "^4.1.0",
    "vue-loader": "^15.9.2",
    "vue-template-compiler": "^2.6.11",
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.11",
    "webpack-merge": "^4.2.2"
  }
}

使用 npm run build:theme 命令试一下样式打包,正常打包。

7、按需加载打包(组件逻辑/样式打包)

全量加载完成了,接下来就是按需加载的打包配置了,先配置组件逻辑部分的打包,也是用到的 webpack 进行打包。

在 build 文件夹下新建一个 webpack.component.js 文件和 components.json 文件,components.json 文件是用来记录组件的路径。

|- project/                         --------------  项目文件
    |- .babelrc                     -------------   babel 配置文件
    |- build                        --------------  webpack 配置文件夹
        |- webpack.base.js          --------------  webpack 公共配置
        |- webpack.prod.js          --------------  全量打包 webpack 配置
        |- webpack.component.js     --------------  按需打包 webpack 配置
    |- index.js                     --------------  入口文件
    |- package.json                 --------------  npm 配置文件
    |- packages                     --------------  组件文件夹
        |- custom-panel             --------------  自定义面板组件文件夹
            |- src                  --------------  YsCustomPanel组件源代码文件夹
                |- main.vue         --------------  组件源代码
            |- index.js             --------------  引用组件并暴露install方法
    |- style                        --------------  组件样式文件夹
        |- src                      --------------  样式源代码文件夹
            |- index.scss           --------------  总样式文件
            |- ys-custom-panel.scss --------------  custom-panel样式文件

// components.json
{
  "ys-custom-panel": "./packages/custom-panel/index.js"
}
// build/webpack.component
const path = require('path');
const merge = require('webpack-merge');
const webpackBaseConfig = require('./webpack.base.js');
const components = require('../components.json');

const basePath = path.resolve(__dirname, '../');

let entries = {};

Object.keys(components).forEach(key => {
  entries[key] = path.join(basePath, components[key]);
});

module.exports = merge(webpackBaseConfig, {
  mode: 'production',
  entry: entries,
  output: {
    path: path.resolve(__dirname, '../dist'),
    publicPath: '/dist/',
    filename: '[name].js',
    chunkFilename: '[id].js',
    libraryTarget: 'umd',
    umdNamedDefine: true
  },
  externals: {
    vue: {
      root: 'Vue',
      commonjs: 'vue',
      commonjs2: 'vue',
      amd: 'vue'
    }
  }
});

webpack.component.js 配置,合并了基础的配置,然后遍历 components.json 的组件路径,创建一个多入口文件的 webpack 配置,然后使用 umd 的打包方式把每个组件打包成一个 js 文件,跟 webpack.prod.js 一样,打包的时候不加入 vue 这个库。

package.json 添加打包按需加载的命令。

{
  "name": "my-library",
  "version": "1.0.0",
  "description": "描述",
  "main": "dist/index.js",
  "files": [
    "dist"
  ],
  "scripts": {
    "build:theme": "gulp build --gulpfile style/gulpfile.js",
    "build:prod": "webpack --config build/webpack.prod.js",
    "build:component": "webpack --config build/webpack.component.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.11.6",
    "@babel/preset-env": "^7.11.5",
    "babel-eslint": "^10.1.0",
    "babel-loader": "^8.1.0",
    "core-js": "^2.0.0",
    "cp-cli": "^1.0.2",
    "css-loader": "^4.2.2",
    "eslint": "^7.8.1",
    "eslint-friendly-formatter": "^4.0.1",
    "eslint-loader": "^4.0.2",
    "eslint-plugin-vue": "^6.2.2",
    "gulp": "^4.0.2",
    "gulp-autoprefixer": "^4.0.0",
    "gulp-cssmin": "^0.1.7",
    "gulp-postcss": "^6.1.1",
    "gulp-sass": "^3.1.0",
    "gulp-style-aliases": "^1.1.11",
    "highlight.js": "^10.2.0",
    "rimraf": "^2.5.4",
    "sass-loader": "^10.0.2",
    "style-loader": "^1.2.1",
    "url-loader": "^4.1.0",
    "vue": "^2.6.12",
    "vue-highlight.js": "^3.1.0",
    "vue-loader": "^15.9.3",
    "vue-template-compiler": "^2.6.12",
    "vuepress": "^1.5.4",
    "webpack": "^4.44.1",
    "webpack-cli": "^3.3.12",
    "webpack-merge": "^4.2.2"
  },
  "dependencies": {
    "gleaf": "^1.1.35",
    "lodash": "^4.17.20",
    "moment": "^2.28.0"
  }
}

执行 npm run build:component 测试打包,打包正常。

打包按需加载样式,只需要在 gulpfile.js遍历处理每个scss文件即可。

到这,全部打包配置已完成,可以开始自己的组件库编写之旅了。最后,添加一条一次完成所有命令的命令,只要执行一条命令,就可以把全部打包完成。

{
  "name": "my-library",
  "version": "1.0.0",
  "description": "描述",
  "main": "dist/index.js",
  "files": [
    "dist"
  ],
  "scripts": {
    "build:theme": "gulp build --gulpfile style/gulpfile.js",
    "build:prod": "webpack --config build/webpack.prod.js",
    "build:component": "webpack --config build/webpack.component.js",
    "dist": "npm run clean && npm run lint && npm run build:prod && npm run build:component && npm run build:theme",
    "clean": "rimraf dist",
    "lint": "eslint --config .eslintrc.js --ext .js,.vue packages --fix",
    "serve": "npm run lint && npm run docs:dev",
    "docs:dev": "vuepress dev docs",
    "docs:build": "vuepress build docs"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.11.6",
    "@babel/preset-env": "^7.11.5",
    "babel-eslint": "^10.1.0",
    "babel-loader": "^8.1.0",
    "core-js": "^2.0.0",
    "cp-cli": "^1.0.2",
    "css-loader": "^4.2.2",
    "eslint": "^7.8.1",
    "eslint-friendly-formatter": "^4.0.1",
    "eslint-loader": "^4.0.2",
    "eslint-plugin-vue": "^6.2.2",
    "gulp": "^4.0.2",
    "gulp-autoprefixer": "^4.0.0",
    "gulp-cssmin": "^0.1.7",
    "gulp-postcss": "^6.1.1",
    "gulp-sass": "^3.1.0",
    "gulp-style-aliases": "^1.1.11",
    "highlight.js": "^10.2.0",
    "rimraf": "^2.5.4",
    "sass-loader": "^10.0.2",
    "style-loader": "^1.2.1",
    "url-loader": "^4.1.0",
    "vue": "^2.6.12",
    "vue-highlight.js": "^3.1.0",
    "vue-loader": "^15.9.3",
    "vue-template-compiler": "^2.6.12",
    "vuepress": "^1.5.4",
    "webpack": "^4.44.1",
    "webpack-cli": "^3.3.12",
    "webpack-merge": "^4.2.2"
  },
  "dependencies": {
    "gleaf": "^1.1.35",
    "lodash": "^4.17.20",
    "moment": "^2.28.0"
  }
}

8、npm 包的发布

package.json需要改好以上配置

name 是包的名字,如果这个名字已经跟 npm 上的重复了,就自己改一下,不然是发布不了的。

version 每次发布都需要修改一下版本号才能发布。

main 入口设置成 dist/index.js 路径,也可以根据你打包的名字自己修改。

files 设置上传到 npm 的文件或文件夹,一定要把打包好的 dist 文件上传,其他随意。

npm publish(非第一次登录,如果是第一次登录,要先npm addUser,输入账号、密码登录进来)

9、使用组件库

1.安装自己的组件库

npm i [name]

2.全量引入

import myLibrary from 'my-library';
import "myLibrary/lib/styles/index.css";

Vue.use(myLibrary);

myLibrary 是发布 npm 包的名字。

3.按需引入

先安装 babel-plugin-component:

npm install -D babel-plugin-component

配置 .babelrc 文件

module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset'
  ],
  plugins: [
    [
      "component",
      {
        "libraryName": "my-library",
        "styleLibrary": {
          "name": "style",
          "base": false,
          "path": "[module].css"
        }
      }
    ]
  ]
};

如果你打包的目录结构跟我这个是一样的话,秩序修改 libraryName 即可。

使用组件:

import Vue from 'vue';

import { 
  YsBasicPanel,
} from 'my-library';

Vue.use(YsBasicPanel);