zoukankan      html  css  js  c++  java
  • 【造轮子】开发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组件有些是带<script>和<style>的,我们需要先将其剔除,之后使用
     * @param  {[String]}       str   需要剔除的标签名 e.g'script'或['script','style']
     * @param  {[Array|String]} tags  e.g '<template></template><script></script>''
     * @return {[String]}             e.g '<html><head><template></template></head><body></body></html>'
     */
    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 转换后会将代码放入<head>中
    };
    
    /**
     * 获取标签中的文本内容
     * @param  {[String]} str e.g '<html><body><h1>header</h1></body><script></script></html>'
     * @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  &#x6210;&#x529F;
     * @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 '<code v-pre class="test"></code>' | '<code></code>'
     * @return {[type]}        e.g '<code class="hljs test></code>'   | '<code class="hljs></code>'
     */
    function wrap(render) {
      return function() {
        return render.apply(this, arguments)
          .replace('<code v-pre class="', '<code class="hljs ')
          .replace('<code>', '<code class="hljs">');
      };
    }
    
    const vueMarkdown = {
      preprocess: (MarkdownIt, source) => {
        MarkdownIt.renderer.rules.table_open = function () {
          return '<table class="table">'
        }
        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-itlib
    enderer.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_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: [
        [MarkdownItAnchor,{
          level: 2, // 添加超链接锚点的最小标题级别, 如: #标题 不会添加锚点
          slugify: slugify, // 自定义slugify, 我们使用的是将中文转为汉语拼音,最终生成为标题id属性
          permalink: true, // 开启标题锚点功能
          permalinkBefore: true // 在标题前创建锚点
        }],
        [MarkdownItContainer, 'demo', {
          validate: params => params.trim().match(/^demos*(.*)$/),
          render: function(tokens, idx) {
            var m = tokens[idx].info.trim().match(/^demos*(.*)$/);
            // nesting === 1表示标签开始
            if (tokens[idx].nesting === 1) {
              // 获取正则捕获组中的描述内容,即::: demo xxx中的xxx
              var 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 `<pre-block class="demo-box" :jsfiddle="${jsfiddle}">
                        <div class="source" slot="desc">${html}</div>
                        ${descriptionHTML}
                        <div class="highlight" slot="highlight">`;
            }
            //否则闭合标签
            return `</div></pre-block>
    `
          }
        }],
        [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

    <div style="color:red">111</div>

    ```
    :::

    效果==》

    美化代码展示,增加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: '<App/>'
    })
    

    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
      

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

  • 相关阅读:
    centos8 将SSSD配置为使用LDAP并要求TLS身份验证
    Centos8 搭建 kafka2.8 .net5 简单使用kafka
    .net core 3.1 ActionFilter 拦截器 偶然 OnActionExecuting 中HttpContext.Session.Id 为空字符串 的问题
    Springboot根据不同环境加载对应的配置
    VMware Workstation12 安装 Centos8.3
    .net core json配置文件小结
    springboot mybatisplus createtime和updatetime自动填充
    .net core autofac依赖注入简洁版
    .Net Core 使用 redis 存储 session
    .Net Core 接入 RocketMQ
  • 原文地址:https://www.cnblogs.com/calamus/p/10796462.html
Copyright © 2011-2022 走看看