3

背景:为了避免重复造轮子,很有必要开发一个通用组件库,方便重复利用。
本文是采用vue-lic3.0脚手改造而成的,使用vuepress作为演示环境。

首先通过vue脚手架生产目录如下:

clipboard.png
然后我们需要修改一下:删除public目录,添加packages,utils和lib文件,见如下:

clipboard.png

packages是用于放控件库, utils是用于放一些通用工具(方法等),src用于整个控件的引用,lib用于放打包后的文件:
src下放如下内容

clipboard.png
index.js是将所有控件引入,作为打包的入口

import locale from 'hui-pro/src/locale'; // 语言包

import 'hui-pro/packages/theme/index.scss'; // 皮肤包,将主题抽离出来
const components = [] // 放控件库
const install = function(Vue, opts = {}) {
  /* istanbul ignore if */
  if (install.installed) return;
  locale.use(opts.locale);
  locale.i18n(opts.i18n);

  components.map(component => {
    Vue.component(component.name, component);
  });

  Vue.use(EllipsisDirective);
};

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

export default {
  version: '0.1.0',
  locale: locale.use,
  i18n: locale.i18n,
  install,
  ... //控件
}

src下locale是用于管理多语言的,我们在开发控件的时候是不能讲中文写死,所以需要做一层多语言处理,而单纯的控件库有是没有引入vue实例,那就更不用说i18n了,所以这边就是通过自己翻译,引用项目工程中的i18n或者直接自己做切割翻译。如下:
index.js

import defaultLang from 'hui-pro/src/locale/lang/zh-CN';
import Vue from 'vue';
import deepmerge from 'deepmerge';
import Format from './format';

const format = Format(Vue);
let lang = defaultLang;
let merged = false;
let i18nHandler = function() {
  const vuei18n = Object.getPrototypeOf(this || Vue).$t;
  if (typeof vuei18n === 'function' && !!Vue.locale) {
    if (!merged) {
      merged = true;
      Vue.locale(
        Vue.config.lang,
        deepmerge(lang, Vue.locale(Vue.config.lang) || {}, { clone: true })
      );
    }
    return vuei18n.apply(this, arguments);
  }
};

export const t = function(path, options) {
  let value = i18nHandler.apply(this, arguments);
  if (value !== null && value !== undefined) return value;

  const array = path.split('.');
  let current = lang;

  for (let i = 0, j = array.length; i < j; i++) {
    const property = array[i];
    value = current[property];
    if (i === j - 1) return format(value, options);
    if (!value) return '';
    current = value;
  }
  return '';
};

export const use = function(l) {
  lang = l || lang;
};

export const i18n = function(fn) {
  i18nHandler = fn || i18nHandler;
};

export default { use, t, i18n };

format.js 这个是通过自己本地切割字符串来找对应的翻译: 'xxx.yyy.ccc' => xxx: {yyy: {ccc: '翻译次'}}

const RE_NARGS = /(%|)\{([0-9a-zA-Z_]+)\}/g;
/**
 *  String format template
 *  - Inspired:
 *    https://github.com/Matt-Esch/string-template/index.js
 */
export default function() {
  /**
   * template
   *
   * @param {String} string
   * @param {Array} ...args
   * @return {String}
   */

  function template(string, ...args) {
    if (args.length === 1 && typeof args[0] === 'object') {
      args = args[0];
    }

    if (!args || !args.hasOwnProperty) {
      args = {};
    }

    return string.replace(RE_NARGS, (match, prefix, i, index) => {
      let result;

      if (string[index - 1] === '{' && string[index + match.length] === '}') {
        return i;
      } else {
        result = Object.prototype.hasOwnProperty.call(args, i) ? args[i] : null;
        if (result === null || result === undefined) {
          return '';
        }

        return result;
      }
    });
  }

  return template;
}

然后在mixins中加入一个使用多语言的方法,主要是这个方法是一样的,所以提取到mixins
mixins/locale.js

import { t } from 'hui-pro/src/locale';

export default {
  methods: {
    t(...args) {
      return t.apply(this, args);
    }
  }
};

之后控件库中可以直接引用mixis,t(), this.t()的形式使用了

import Locale from 'hui-pro/src/mixins/locale';
mixins: [Locale]

使用
html中
{{t('aa.bb')}}
js中使用
test () {
    this.t('aa.bb')
}

接下来我们还需要修改一些配置:
babel.config.js

const utilTypeList = []// 对外暴露工具库的列表

module.exports = function(api) {
  let presets = ['@vue/app'];
  let plugins = [];

  if (api.env('js')) {
    presets = [['@babel/preset-env', { loose: true }]];
    plugins = [
      [
        'module-resolver',
        {
          root: ['hui-pro'],
          alias: {
            'hui-pro/src': 'hui-pro/lib'
          }
        }
      ]
    ];
  } else if (api.env('node')) {
    presets = [
      [
        '@babel/preset-env',
        {
          targets: {
            node: true
          }
        }
      ]
    ];
  }
  // 按需加载utils
  for (let type of utilTypeList) {
    plugins.push([
      'import',
      {
        libraryName: `hui-pro/packages/utils/${type}`,
        libraryDirectory: ''
      },
      `${type}`
    ]);
  }
  return {
    presets,
    plugins
  };
};

需要安装eslintrc.js
增加配置文件

var isDev = process.env.NODE_ENV === 'development';

module.exports = {
  root: true,
  env: {
    mocha: true,
    es6: true,
    node: true,
    browser: true
  },
  parserOptions: {
    parser: 'babel-eslint'
  },
  plugins: ['vue'],
  extends: ['plugin:vue/strongly-recommended', '@vue/prettier'],
  rules: {
    'vue/html-indent': 1,
    'no-console': isDev
      ? 0
      : [
          'error',
          {
            allow: ['warn', 'error']
          }
        ],
    'no-debugger': isDev ? 0 : 2
  },
  globals: {
    expect: true,
    sinon: true
  }
};

还需要增加stylelintrc效验

{
  "plugins": ["stylelint-prettier", "stylelint-scss"],
  "extends": [
    "stylelint-config-idiomatic-order",
    "stylelint-config-standard",
    "stylelint-config-prettier"
  ],
  "rules": {
    "at-rule-no-unknown": null,
    "scss/at-rule-no-unknown": true,
    "prettier/prettier": true
  }
}

最后增加一下vue的配置

const path = require('path');
const nodeExternals = require('webpack-node-externals');

module.exports = {
  publicPath: '',
  outputDir: 'lib',
  assetsDir: '',
  filenameHashing: false,
  css: {
    extract: true,
    sourceMap: false
  },
  productionSourceMap: false,

  pages: {
    index: {
      // page 的入口
      entry: 'preview/main.js',
      // 模板来源
      template: 'preview/index.html'
    }
  },

  devServer: {
    port: 8999
  },

  configureWebpack() {
    if (process.env.LIB_TYPE === 'common') {
      return {
        externals: [
          {
            vue: 'vue',
            'hui-pro/src/locale': 'hui-pro/lib/locale'
          },
          nodeExternals()
        ]
      };
    }
  },

  chainWebpack(webpackConfig) {
    webpackConfig.when(process.env.LIB_TYPE === 'umd', config => {
      config.output.umdNamedDefine(true);
    });

    webpackConfig.resolve.alias.set('hui-pro', path.resolve(__dirname));

    //svg
    const svgRule = webpackConfig.module.rule('svg');
    svgRule.uses.clear();
    svgRule
      .oneOf('svg')
      .resourceQuery(/svg/)
      .use('vue-svg-loader')
      .loader('vue-svg-loader')
      .end()
      .end()
      .oneOf('img')
      .resourceQuery(/img/)
      .use('url-loader')
      .loader('url-loader')
      .options({
        name: 'img/[name].[hash:8].[ext]'
      })
      .end()
      .end()
      .oneOf()
      .use('file-loader')
      .loader('file-loader')
      .options({
        name: 'fonts/[name].[ext]'
      });

    webpackConfig.module
      .rule('fonts')
      .use('url-loader')
      .tap(options => Object.assign(options, { limit: 10 }));

    // map-picker控件中,map/下的代码是openlayer相关代码,无需babel编译。
    webpackConfig.module
      .rule('js')
      .test(/\.js$/)
      .exclude.add(path.resolve(__dirname) + 'packages/map-picker/src/map')
      .end()
      .use('babel-loader');
    //stylelint
    webpackConfig
      .plugin('stylelint')
      .use('stylelint-webpack-plugin')
      .tap(() => {
        return [
          {
            configFile: '.stylelintrc',
            files: ['packages/**/*.scss'],
            emitErrors: true
          }
        ];
      });
  }
};

当然如果使用git的话,使用commitlint来效验提交的代码

{
  "printWidth": 80,
  "tabWidth": 2,
  "singleQuote": true,
  "trailingComma": "none",
  "bracketSpacing": true,
  "semi": true,
  "useTabs": false,
  "proseWrap": "never",
  "overrides": [
    {
      "files": [
        "*.json",
        ".eslintrc",
        ".babelrc",
        ".stylelintrc",
        ".prettierrc"
      ],
      "options": {
        "parser": "json",
        "tabWidth": 2
      }
    }
  ]
}

最后,需要修改package.json

{
  "name": "hui-pro",
  "version": "0.1.0-alpha.1",
  "scripts": {
    "lint": "vue-cli-service lint packages src && stylelint packages/**/*.scss --fix",
    "cz:changelog": "conventional-changelog -p angular -i CHANGELOG.md -s && git add CHANGELOG.md",
    "dev": "vue-cli-service serve",
    "lib:all": "npm run lib:clean && npm run lib:i18n && npm run lib:common && npm run lib:umd && npm run lib:utils",
    "lib:clean": "rimraf lib",
    "lib:common": "cross-env LIB_TYPE=common vue-cli-service build --no-clean --target lib --formats commonjs --name hui-pro src/index.js",
    "lib:i18n": "cross-env NODE_ENV=js babel src/locale --out-dir lib/locale",
    "lib:utils": "cross-env NODE_ENV=node babel-node bin/generateIndex && cross-env NODE_ENV=js babel packages/utils --out-dir utils",
    "lib:umd": "cross-env LIB_TYPE=umd vue-cli-service build --no-clean --target lib --formats umd-min --name hui-pro src/index.js",
    "vuepress:dev": "vuepress dev docs"
  },
  "dependencies": {
    "jsencrypt": "^2.3.1",
    "hui": "^2.0.0-alpha.4",
    "moment": "^2.24.0",
    "qs": "^6.5.2"
  },
  "devDependencies": {
    "@babel/cli": "^7.2.3",
    "@babel/node": "^7.2.2",
    "@commitlint/cli": "^7.2.0",
    "@commitlint/config-conventional": "^7.5.0",
    "@vue/cli-plugin-babel": "^3.3.0",
    "@vue/cli-plugin-eslint": "^3.3.0",
    "@vue/cli-service": "^3.3.0",
    "@vue/eslint-config-prettier": "^4.0.1",
    "babel-eslint": "^10.0.1",
    "babel-plugin-import": "^1.11.0",
    "babel-plugin-module-resolver": "^3.1.3",
    "commitizen": "^3.0.5",
    "conventional-changelog": "^3.0.5",
    "cross-env": "^5.2.0",
    "cz-customizable": "^5.2.0",
    "eslint": "^5.8.0",
    "eslint-plugin-vue": "^5.0.0",
    "highlightjs": "^9.12.0",
    "husky": "^1.1.1",
    "ip": "^1.1.5",
    "lint-staged": "^8.1.3",
    "node-sass": "^4.11.0",
    "prettier-eslint": "^8.8.2",
    "prettier-stylelint": "^0.4.2",
    "sass-loader": "^7.1.0",
    "stylelint": "^9.10.1",
    "stylelint-config-idiomatic-order": "^6.2.0",
    "stylelint-config-prettier": "^5.0.0",
    "stylelint-config-standard": "^18.2.0",
    "stylelint-prettier": "^1.0.6",
    "stylelint-scss": "^3.5.1",
    "stylelint-webpack-plugin": "^0.10.5",
    "vue": "^2.5.21",
    "vue-cli-plugin-changelog": "^1.1.9",
    "vue-cli-plugin-lint-staged": "^0.1.1",
    "vue-router": "^3.0.1",
    "vue-svg-loader": "^0.12.0",
    "vue-template-compiler": "^2.5.21",
    "webpack-node-externals": "^1.7.2"
  },
  "postcss": {
    "plugins": {
      "autoprefixer": {}
    }
  },
  "browserslist": [
    "Chrome > 48",
    "Edge > 16",
    "Firefox > 62",
    "IE > 9",
    "Safari > 11"
  ],
  "commitlint": {
    "extends": [
      "@commitlint/config-conventional"
    ]
  },
  "config": {
    "commitizen": {
      "path": "node_modules/cz-customizable"
    }
  },
  "files": [
    "lib",
    "src",
    "packages",
    "utils"
  ],
  "husky": {
    "hooks": {
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
      "post-merge": "npm install",
      "pre-commit": "lint-staged"
    }
  },
  "main": "lib/hui-pro.common.js"
}

vuepress演示环境

在该环境下创建一个docs文件

clipboard.png
这边主要config.js和enhanceApp.js
config.js:

var path = require('path');
var ip = require('ip');
var enNav = require('./links/en.nav.json');
var zhNav = require('./links/zh.nav.json');
var enSidebar = require('./links/en.sidebar.json'); // 英文版
var zhSidebar = require('./links/zh.sidebar.json'); // 中文版

var webpackConfig = {
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
        include: [path.resolve(__dirname, '../../packages')]
      }
    ]
  }
};

module.exports = {
  base: '/lib/',
  host: ip.address(),
  port: '8099',
  title: 'lib',
  description: '框架库',
  locales: {
    '/zh/': {
      lang: 'zh-CN',
      title: 'test-lib',
      description: '框架库'
    },
    '/en/': {
      lang: 'en-US',
      title: 'test-lib',
      description: 'Library'
    }
  },
  head: [
    [
      'link',
      {
        rel: 'icon',
        href: `favicon.ico`
      }
    ]
  ],
  themeConfig: {
    editLinks: true,
    docsDir: 'docs',
    locales: {
      '/zh/': {
        selectText: '选择语言',
        label: '简体中文',
        nav: zhNav,
        sidebar: zhSidebar
      },
      '/en/': {
        selectText: 'Languages',
        label: 'English',
        nav: enNav,
        sidebar: enSidebar
      }
    }
  },
  dest: './docs/.vuepress/dist',
  demo: {
    menu: [
      {
        title: '主页面',
        router: '/layout/page.html',
        icon: 'h-icon-menu_app'
      }
    ]
  },
  scss: {
    sourceMap: true
  },
  sass: {
    indentedSyntax: true
  },
  configureWebpack: webpackConfig,
  overlay: {
    warnings: true,
    errors: true
  },
  chainWebpack: (webpackConfig, isServer) => {
    webpackConfig.resolve.alias.set(
      'name1',
      path.resolve(__dirname, '../../')
    ); // 配置别名路径

    webpackConfig.resolve.alias.set(
      'name2',
      path.resolve(__dirname, '../../src')
    ); // 配置别名路径

    webpackConfig.module
      .rule('eslint')
      .pre()
      .exclude.add(/node_modules/)
      .end()
      .include.add(path.resolve(__dirname, '../../src'))
      .add(path.resolve(__dirname, '../../packages'))
      .end()
      .test(/\.(vue|(j|t)sx?)$/)
      .use('eslint-loader')
      .loader('eslint-loader')
      .options({
        extensions: ['.js', '.jsx', '.vue', '.ts', '.tsx'],
        cache: true,
        emitWarning: true,
        emitError: true,
        formatter: require('eslint/lib/formatters/codeframe')
      });

    //svg
    const svgRule = webpackConfig.module.rule('svg');
    svgRule.uses.clear();
    svgRule
      .oneOf('svg')
      .resourceQuery(/svg/)
      .use('vue-svg-loader')
      .loader('vue-svg-loader')
      .end()
      .end()
      .oneOf('img')
      .resourceQuery(/img/)
      .use('url-loader')
      .loader('url-loader')
      .options({
        name: 'img/[name].[hash:8].[ext]'
      })
      .end()
      .end()
      .oneOf()
      .use('file-loader')
      .loader('file-loader')
      .options({
        name: 'fonts/[name].[ext]'
      });

    //stylelint
    webpackConfig
      .plugin('stylelint')
      .use(require.resolve('stylelint-webpack-plugin'))
      .tap(() => {
        return [
          {
            configFile: '.stylelintrc',
            files: ['packages/**/*.scss'],
            emitErrors: true
          }
        ];
      });
  }
};

enhanceApp.js:

import name from 'name2'
import routerGuard from './router'

import *.css'

export default ({
  Vue,
  router
}) => {
  routerGuard.use(router)
  Vue.use(name)
}

DanielDemi
159 声望8 粉丝

开始前端之旅