前端菜鸟

【造轮子】开发vue组件库MeowMeowUI

 

1. 创建项目

# 全局安装 vue-cli
$ npm install --global vue-cli
# 创建一个基于 webpack 模板的新项目
$ vue init webpack meowui
# 安装依赖
$ cd meowui

$ npm install
$ npm run dev

 

 

2. 规划目录结构

这里参考element-ui和iview的目录结构

|-- examples      // 原 src 目录,改成 examples 用作示例展示
|-- packages      // 新增 packages 用于编写存放组件

这样需要修改webpack相关目录路径配置

 

{
   test: /\.js$/,
   loader: 'babel-loader',
   include: [resolve('examples'), resolve('test'), resolve('packages')]
}

下载安装相关依赖

 


# markdown-it 渲染 markdown 基本语法
# markdown-it-anchor 为各级标题添加锚点
# markdown-it-container 用于创建自定义的块级容器
# vue-markdown-loader 核心loader
# transliteration 中文转拼音
# cheerio 服务器版jQuery
# highlight.js 代码块高亮实现
# striptags 利用cheerio实现两个方法,strip是去除标签以及内容,fetch是获取第一符合规则的标签的内容

 

配置webpack

 

1. build目录下新建一个strip-tags.js文件.

 


// strip-tags.js

'use strict';

var cheerio = require('cheerio'); // 服务器版的jQuery

/**
 * 在生成组件效果展示时,解析出的VUE组件有些是带''
 * @return {[String]}             e.g ''
 */
exports.strip = function(str, tags) {
  var $ = cheerio.load(str, {decodeEntities: false});

  if (!tags || tags.length === 0) {
    return str;
  }

  tags = !Array.isArray(tags) ? [tags] : tags;
  var len = tags.length;

  while (len--) {
    $(tags[len]).remove();
  }

  return $.html(); // cheerio 转换后会将代码放入中
};

/**
 * 获取标签中的文本内容
 * @param  {[String]} str e.g '

header

' * @param {[String]} tag e.g 'h1' * @return {[String]} e.g 'header' */
exports.fetch = function(str, tag) { var $ = cheerio.load(str, {decodeEntities: false}); if (!tag) return str; return $(tag).html(); };

2. 修改webpack.base.conf.js

- 添加 vue-markdown-loader 并配置

 


// 完整 webpack.base.conf.js 文件

'use strict'
const path = require('path')
const utils = require('./utils')
const config = require('../config')
const slugify = require('transliteration').slugify; // 引入transliteration中的slugify方法
const striptags = require('./strip-tags'); // 引入刚刚的工具类
const md = require('markdown-it')()
const vueLoaderConfig = require('./vue-loader.conf')
const MarkdownItAnchor = require('markdown-it-anchor')
const MarkdownItContainer = require('markdown-it-container')

/**
 * 由于cheerio在转换汉字时会出现转为Unicode的情况,所以我们编写convert方法来保证最终转码正确
 * @param  {[String]} str e.g  成功
 * @return {[String]}     e.g  成功
 */
function convert(str) {
  str = str.replace(/(&#x)(\w{4});/gi, function($0) {
    return String.fromCharCode(parseInt(encodeURIComponent($0).replace(/(%26%23x)(\w{4})(%3B)/g, '$2'), 16));
  });
  return str;
}

/**
 * 由于v-pre会导致在加载时直接按内容生成页面.但是我们想要的是直接展示组件效果,通过正则进行替换
 * hljs是highlight.js中的高亮样式类名
 * @param  {[type]} render e.g '' | ''
 * @return {[type]}        e.g ''
 */
function wrap(render) {
  return function() {
    return render.apply(this, arguments)
      .replace('''', '');
  };
}

const vueMarkdown = {
  preprocess: (MarkdownIt, source) => {
    MarkdownIt.renderer.rules.table_open = function () {
      return ''
    }
    MarkdownIt.renderer.rules.fence = utils.wrapCustomClass(MarkdownIt.renderer.rules.fence)
    // MarkdownIt.renderer.rules.fence = wrap(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_inlineconst 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: [
    [MarkdownItAnchor,{
      level: 2, // 添加超链接锚点的最小标题级别, 如: #标题 不会添加锚点
      slugify: slugify, // 自定义slugify, 我们使用的是将中文转为汉语拼音,最终生成为标题id属性
      permalink: true, // 开启标题锚点功能
      permalinkBefore: true// 在标题前创建锚点
    }],
    [MarkdownItContainer, 'demo', {
      validate: params => params.trim().match(/^demo\s*(.*)$/),
      render: function(tokens, idx) {
        var m = tokens[idx].info.trim().match(/^demo\s*(.*)$/);
        // nesting === 1表示标签开始if (tokens[idx].nesting === 1) {
          // 获取正则捕获组中的描述内容,即::: demo xxx中的xxxvar description = (m && m.length > 1) ? m[1] : '';
          // 获得内容var content = tokens[idx + 1].content;
          // 解析过滤解码生成html字符串var html = convert(striptags.strip(content, ['script', 'style'])).replace(/(<[^>]*)=""(?=.*>)/g, '$1');
          // 获取script中的内容var script = striptags.fetch(content, 'script');
          // 获取style中的内容var style = striptags.fetch(content, 'style');
          // 组合成prop参数,准备传入组件var jsfiddle = { html: html, script: script, style: style };
          // 是否有描述需要渲染var descriptionHTML = description
            ? md.render(description)
            : '';
          // 将jsfiddle对象转换为字符串,并将特殊字符转为转义序列
          jsfiddle = md.utils.escapeHtml(JSON.stringify(jsfiddle));
          // 起始标签,写入pre-block模板开头,并传入参数return `class="demo-box" :jsfiddle="${jsfiddle}">
                    <div class="source" slot="desc">${html}div>
                    ${descriptionHTML}
                    <div class="highlight" slot="highlight">`;
        }
        //否则闭合标签
        return `div>pre-block>\n`
      }
    }],
    [require('markdown-it-container'), 'tip'],
    [require('markdown-it-container'), 'warning']
  ]
}

function resolve (dir) {
  return path.join(__dirname, '..', dir)
}


module.exports = {
  context: path.resolve(__dirname, '../'),
  entry: {
    app: './example/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('example'),
    }
  },
  module: {
    rules: [
      {
        test: /\.md$/,
        loader: 'vue-markdown-loader',
        options: vueMarkdown
      },
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: vueLoaderConfig
      },
      {
        test: /\.js$/,
        loader: 'babel-loader',
        include: [resolve('example'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
      },
      {
        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]')
        }
      }
    ]
  },
  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'
  }
}

 

 

创建路由并测试md读写功能

import Vue from 'vue'
import Router from 'vue-router'
const _import_ = file => () => import('@/views/' + file )
import GuidLayout from '@/views/layout/guid.vue'

Vue.use(Router)

export default new Router({
  routes: [
    {
      name: "index",
      path: "/",
      component: _import_('dashboard/index.vue')
    },
    {
      path: '',
      name: 'Docs',
      component: GuidLayout,
      children:[
        {
          path: '/guid',
          name: 'guid',
          component: _import_('docs/guid.md')
        }
      ]
    },
    {path: '*', component: _import_('dashboard/index.vue'), hidden: true},
  ]
})

 

 


// 创建测试md文件

## demo

### 基础布局
<div class="demo-block" style="color:red">
  1111
div>

 // 注意demo和html不加多空行;代码与标签需要多空行,否则解析会有问题
::: demo
```html

111


```
:::

 

效果==》

 

 

美化代码展示,增加pre-demo 组件并全局引用

//mian.js 文件
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
import meowUi from '../packages/index'
import preBlock from './components/pre-block.vue'

Vue.component('pre-block', preBlock)
Vue.use(meowUi)
Vue.config.productionTip = false
import 'highlight.js/styles/color-brewer.css';
import './assets/common.css';

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  components: { App },
  template: ''
})

4. 写组件

按需引入实现,package文件夹下创建index.js整理全部组件

/**
* @author calamus0427
* Date: 19/4/30
*/
import Button from './button/index.js'

const components = [
  Button
]

const install = function(Vue) {
  if (install.installed) return
  components.map(component => Vue.component(component.name, component))
  MetaInfo.install(Vue)
  Vue.prototype.$loading = WLoadingBar
}

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

export default {
  Button
}

 

5. 发布到npm(不做详细展开了,相关资料很多)

  1. 修改package的相关信息
  2. 发布
    npm publish
    

----------- ----------- ----------- ----------- ----------- 待续 ----------- ----------- ----------- ----------- -----------

(0)

本文由 Calamus 作者:calamus 发表,转载请注明来源!

热评文章

发表评论

电子邮件地址不会被公开。 必填项已用*标注

陕ICP备18021115号-1