【造輪子】開發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的目錄結構html

|-- examples      // 原 src 目錄,改爲 examples 用做示例展現
|-- packages      // 新增 packages 用於編寫存放組件

這樣須要修改webpack相關目錄路徑配置vue

 

{
   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文件.node

 

// 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.jswebpack

- 添加 vue-markdown-loader 並配置git

 

// 完整 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-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_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(/^demo\s*(.*)$/),
      render: function(tokens, idx) {
        var m = tokens[idx].info.trim().match(/^demo\s*(.*)$/);
        // 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>\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

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

```
:::

 

效果==》github

 

 

美化代碼展現,增長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整理所有組件web

/** * @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

----------- ----------- ----------- ----------- ----------- 待續 ----------- ----------- ----------- ----------- -----------vue-router

相關文章
相關標籤/搜索