【工程化】从 0 搭建 VueJS 移动端组件库开发框架

栏目: JavaScript · 发布时间: 5年前

内容简介:写于 2017.06.05之前发表过一篇最近参与维护公司内部的一个针对移动端的UI组件库,该组件库缺乏文档和严格的文件组织结构。

写于 2017.06.05

之前发表过一篇 《Vue-Donut——专用于构建Vue的UI组件库的开发框架》 ,仅仅是对框架一个粗略的介绍,并没有针对里面的实现方式进行详细说明。

最近参与维护公司内部的一个针对移动端的UI组件库,该组件库缺乏文档和严格的文件组织结构。 Vue-Donut 的功能比较简单,并不能方便地创建针对移动端UI组件库的文档和预览。在参考了 mint-ui 等业界内成熟的方案之后,我在 Vue-Donut 的基础上进行了拓展,最后搭建出了一个非常方便且自动化的开发框架。

由于觉得开发的过程非常有意思,也想记录一下自己的开发思路,因此决定好好地写一篇文章作为记录分享。

项目地址: github.com/jrainlau/vu…

1. 功能分析

首先我们来规划一下这个框架的最终目的是什么:

【工程化】从 0 搭建 VueJS 移动端组件库开发框架

如图所示,通过该框架可以生成一个文档页面。这个页面分为三个部分:导航、文档、预览。

  1. 导航:通过导航切换不同组件的文档和预览。

  2. 文档:该类型组件所对应的文档,以markdown形式书写。

  3. 预览:该类型组件所对应的预览页面。

为了让组件的开发和文档的维护更加高效,我们希望这个框架可以更加自动化。如果我们只要开不同组件的预览的页面及其对应的说明文档 README ,框架就能自动帮我们生成对应的导航和HTML内容,岂不妙哉?除此之外,当我们已经把所有的UI组件都开发好了,统统放在 /components 目录下,如果能够通过框架进行一键构建打包,最后产出一个npm包,那么别人使用这套UI组件库也会变得非常简单。带着这个想法,我们来分析一下我们可能需要用到的关键技术。

2. 技术分析

  • 使用webpack2作为框架核心:使用方便,高度可定制。同时webpack2文档已经相当齐全,生态圈繁荣,社区活跃,遇到的坑基本上都可以在google和stackoverflow找到。

  • 预览页面以 iframe 的形式插入到文档页面中:维护组件库的时候只需要聚焦于组件的开发和预览页面的组织,无需分心维护导航和文档,实现了解耦。因此意味着这是一个基于Vue.js的 多页应用

  • 自动生成导航:使用 vue-router 进行页面切换。每当新建一个预览页面,就会自动在页面上生成对应的导航,并自动维护导航和路由的关系。因此,我们需要一套机制去监听文件结构的变化。

  • 自动生成文档:一个预览页面对应一份文档,所以文档理应以 README.md 的形式存放在对应的预览页面文件夹内。我们需要一个能够把 README.md 直接转化成html内容的办法。

  • 开发者模式:通过一条命令,启动一个 webpack-dev-server ,提供热更新和自动刷新功能。

  • 构建打包模式:通过一条命令,自动把 /components 目录下的所有资源打包成一个npm包。

  • 页面构建模式:通过一条命令,生成能够直接部署使用的静态资源文件。

通过对技术的梳理,我们脑海里面已经有了一个印象,接下来就是一步一步地进行开发了。

3. 梳理框架目录结构

一个好的目录结构,能够极大地方便我们接下来的工作。

.
├── index.html  // 文档页的入口html
├── view.html  // 预览页的入口html
├── package.json  // 依赖声明、npm script命令
├── src
│   ├── document  // 文档页目录
│   │   ├── doc-app.vue  // 文档页入口.vue文件
│   │   ├── doc-entry.js  // 文档页入口.js文件
│   │   ├── doc-router.js  // 文档页路由配置
│   │   ├── doc_comps  // 文档页组件
│   │   └── static  // 文档页静态资源
│   └── view  // 预览页目录
│       ├── assets  // 预览页静态资源
│       ├── components // UI组件库
│       ├── pages // 存放不同的预览页
│       ├── view-app.vue // 预览页入口.vue文件
│       ├── view-entry.js  // 预览页入口.js文件
│       └── view-router.js  // 预览页路由配置
└── webpack
    ├── webpack.base.config.js // webpack通用配置 
    ├── webpack.build.config.js  // UI库构建打包配置
    ├── webpack.dev.config.js  // 开发模式配置
    └── webpack.doc.config.js  // 静态资源构建配置
复制代码

可以看到,目录结构并不复杂,接下来我们首先对webpack进行配置,以便我们能够把项目跑起来。

4. webapck配置

4.1 基础配置

进入到 /webpack 目录,新建一个 webpack.base.config.js 文件,其内容如下:

const { join } = require('path')
const hljs = require('highlight.js')

// 配置markdown解析、以便高亮显示markdown中的代码块
const markdown = require('markdown-it')({
  highlight: function (str, lang) {
    if (lang && hljs.getLanguage(lang)) {
      try {
        return '<pre class="hljs"><code>' +
               hljs.highlight(lang, str, true).value +
               '</code></pre>';
      } catch (__) {}
    }

    return '<pre class="hljs"><code>' + markdown.utils.escapeHtml(str) + '</code></pre>';
  }
})

const resolve = dir => join(__dirname, '..', dir)

module.exports = {
  // 只配置输出路径
  output: {
    filename: 'js/[name].js',
    path: resolve('dist'),
    publicPath: '/'
  },

  // 配置不同的loader以便资源加载
  // eslint是标配,建议加上
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          'babel-loader',
          'eslint-loader'
        ]
      },
      {
        enforce: 'pre',
        test: /\.vue$/,
        loader: 'eslint-loader',
        exclude: /node_modules/
      },
      {
        test: /\.(png|jpg|gif|svg)$/,
        loader: 'url-loader'
      },
      {
        test: /\.css$/,
        use: [{
          loader: 'style-loader'
        }, {
          loader: 'css-loader'
        }]
      },
      {
        test: /\.less$/,
        use: [{
          loader: 'style-loader' // creates style nodes from JS strings
        }, {
          loader: 'css-loader' // translates CSS into CommonJS
        }, {
          loader: 'less-loader' // compiles Less to CSS
        }]
      },
      // vue-markdown-loader能够把.md文件直接转化成vue组件
      {
        test: /\.md$/,
        loader: 'vue-markdown-loader',
        options: markdown
      }
    ]
  },
  resolve: {
    // 该项配置能够在加载资源的时候省略后缀名
    extensions: ['.js', '.vue', '.json', '.css', '.less'],
    modules: [resolve('src'), 'node_modules'],
    // 配置路径别名
    alias: {
      '~src': resolve('src'),
      '~components': resolve('src/view/components'),
      '~pages': resolve('src/view/pages'),
      '~assets': resolve('src/view/assets'),
      '~store': resolve('src/store'),
      '~static': resolve('src/document/static'),
      '~docComps': resolve('src/document/doc_comps')
    }
  }
}

复制代码

4.2 开发模式配置

基础配置好了,我们就可以开始开发模式的配置了。在当前目录下,新建一个 webpack.dev.config.js 文件,并写入如下内容:

const { join } = require('path')
const webpack = require('webpack')
const merge = require('webpack-merge')
const basicConfig = require('./webpack.base.config')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const resolve = dir => join(__dirname, '..', dir)

module.exports = merge(basicConfig, {
  // 由于是多页应用,所以应该有2个入口文件
  entry: {
    app: './src/document/doc-entry.js',
    view: './src/view/view-entry.js'
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      }
    ]
  },
  devtool: 'inline-source-map',

  // webpack-dev-server配置
  devServer: {
    contentBase: resolve('/'),
    compress: true,
    hot: true,
    inline: true,
    publicPath: '/',
    stats: 'minimal'
  },
  plugins: [
    // 热更新插件
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NamedModulesPlugin(),
    
    // 把生成的js注入到入口html文件
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'index.html',
      inject: true,
      chunks: ['app']
    }),
    new HtmlWebpackPlugin({
      filename: 'view.html',
      template: 'view.html',
      inject: true,
      chunks: ['view']
    })
  ]
})

复制代码

非常简单的配置,值得注意的是因为多页应用的缘故,入口文件和 HtmlWebpackPlugin 都要写多份。

4.3 构件打包配置

接下来,还有把UI组件库构建打包成npm包的配置。新建一个名为 webpack.build.config.js 的文件:

const { join } = require('path')
const merge = require('webpack-merge')
const basicConfig = require('./webpack.base.config')
const CleanWebpackPlugin = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

const resolve = dir => join(__dirname, '..', dir)

module.exports = merge(basicConfig, {
  // 入口文件
  entry: {
    app: './src/view/components/index.js'
  },
  devtool: 'source-map',
  // 输出位置为dist目录,名字自定义,输出格式为umd格式
  output: {
    path: resolve('dist'),
    filename: 'index.js',
    library: 'my-project',
    libraryTarget: 'umd'
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      }
    ]
  },
  plugins: [
    // 每一次打包都把上一次的清空
    new CleanWebpackPlugin(['dist'], {
      root: resolve('./')
    }),
    // 把静态资源复制出去,以便实现UI换肤等功能
    new CopyWebpackPlugin([
      { from: 'src/view/assets', to: 'assets' }
    ])
  ]
})

复制代码

4.4 一键生成文档配置

最后,我们一起来配置一键生成文档网站的 webpack.doc.config.js

const { join } = require('path')
const webpack = require('webpack')
const merge = require('webpack-merge')
const basicConfig = require('./webpack.base.config')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const CleanWebpackPlugin = require('clean-webpack-plugin')

const resolve = dir => join(__dirname, '..', dir)

module.exports = merge(basicConfig, {
  // 类似开发者模式,两个入口文件,多了一个公共依赖包vendor
  // 以`js/`开头能够自动输出到`js`目录下
  entry: {
    'js/app': './src/document/doc-entry.js',
    'js/view': './src/view/view-entry.js',
    'js/vendor': [
      'vue',
      'vue-router'
    ]
  },
  devtool: 'source-map',

  // 输出文件加hash
  output: {
    path: resolve('docs'),
    filename: '[name].[chunkhash:8].js',
    chunkFilename: 'js/[name].[chunkhash:8].js'
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          loaders: {
            css: ExtractTextPlugin.extract({
              use: ['css-loader']
            }),
            less: ExtractTextPlugin.extract({
              use: ['css-loader', 'less-loader']
            })
          }
        }
      }
    ]
  },
  plugins: [
    // 提取css文件并指定其输出位置和命名
    new ExtractTextPlugin({
      filename: 'css/[name].[contenthash:8].css',
      allChunks: true
    }),
    
    // 抽离公共依赖
    new webpack.optimize.CommonsChunkPlugin({
      names: ['js/vendor', 'js/manifest']
    }),
    
    // 把构建出的静态资源注入到多个入口html中
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'index.html',
      inject: true,
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeAttributeQuotes: true
      },
      chunks: ['js/vendor', 'js/manifest', 'js/app'],
      chunksSortMode: 'dependency'
    }),
    new HtmlWebpackPlugin({
      filename: 'view.html',
      template: 'view.html',
      inject: true,
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeAttributeQuotes: true
      },
      chunks: ['js/vendor', 'js/manifest', 'js/view'],
      chunksSortMode: 'dependency'
    }),
    new webpack.LoaderOptionsPlugin({
      minimize: true,
      debug: false
    }),
    new webpack.optimize.OccurrenceOrderPlugin(),
    new CleanWebpackPlugin(['docs'], {
      root: resolve('./')
    })
  ]
})

复制代码

通过上面这个配置,最终会产出一个 index.html 和一个 view.html ,以及各自所需的css和js文件。直接部署到静态服务器上即可进行访问。

多说一句,webpack的配置乍一看上去好像很复杂,但实际上是相当简单,webpack2的官方文档也挺完善且易读,推荐对webpack2不熟悉的朋友花点时间认真阅读一下文档。

至此,我们已经把 /webpack 目录下的相关配置都弄好了,框架的基础骨架已经搭建完毕,接下来开始对业务逻辑进行开发。

5. 业务逻辑开发

在根目录下新建两个入口文件 index.htmlview.html ,分别添加一个 <div id="app"></div><div id="view"></div> 标签。

进入 /src 目录,新建 /document/view 目录,按照前文目录结构所示新建需要的目录和文件。

具体的内容可以看 这里 ,简单来说就是初始化 vue 应用,请暂时忽略 router.js 当中的这一段代码:

routeList.forEach((route) => {
  routes.splice(1, 0, {
    path: `/${route}`,
    component: resolve => require([`~pages/${route}/index`], resolve)
  });
});
复制代码

这个是监听目录变化自动管理导航相关的功能,会在后面详细介绍。

逻辑很简单。 /document/view 分别属于 文档预览 两个应用,其中 预览iframe 的形式内嵌到 文档 应用页面内,相关的操作其实都是在 文档 当中进行。当点击导航的时候, 文档 应用会自动加载 /view/pages/ 下相关预览页文件夹的 README.md 文件,同时修改 iframe 的链接,实现内容的同步切换。

接下来,我们一起来研究一下如何监听文件目录变化,自动维护 router 导航。

6. 自动维护 router 导航

如果你有用过Nuxt,一定对其自动维护 router 的功能不会陌生。如果没有用过也没关系,我们自己来实现这个功能!

使用 vue-router 的同学可能都经历过这么一个痛点,每当新建页面,都要往 router.js 的数组里面添加一个声明,最终 router.js 很可能会变成这样:

const route = [
  { path: '/a', component: resolve => require(['a'], resolve) },
  { path: '/b', component: resolve => require(['b'], resolve) },
  { path: '/c', component: resolve => require(['c'], resolve) },
  { path: '/d', component: resolve => require(['d'], resolve) },
  { path: '/e', component: resolve => require(['e'], resolve) },
  { path: '/f', component: resolve => require(['f'], resolve) },
  ...
]
复制代码

很烦,对不对?如果可以自动维护就好了。首先我们要做一个约定,约定好不同的“页面”应该如何组织。

/src/view/pages 目录下,每新建一个“页面”,我们就要新建一个和该页面同名的文件夹,往里添加文档 README.md 和入口 index.vue ,效果如下:

└── view
    └── pages
        ├── 页面A
        │   ├── index.vue
        │   └── README.md
        ├── 页面B
        │   ├── index.vue
        │   └── README.md
        ├── 页面C
        │   ├── index.vue
        │   └── README.md
        └── 页面D
            ├── index.vue
            └── README.md
复制代码

约定好了文件的组织方式,接下来我们需要用到一个 工具 去负责监听和处理。这里我们使用了 chokidar 来实现。

/webpack 目录下新建一个 watcher.js 文件:

console.log('Watching dirs...');
const { resolve } = require('path')
const chokidar = require('chokidar')
const fs = require('fs')
const routeList = []

const watcher = chokidar.watch(resolve(__dirname, '../src/view/pages'), {
  ignored: /(^|[\/\\])\../
})

watcher
  // 监听目录添加
  .on('addDir', (path) => {
    let routeName = path.split('/').pop()
    if (routeName !== 'pages' && routeName !== 'index') {
      routeList.push(`'${routeName}'`)
      fs.writeFileSync(resolve(__dirname, '../src/route-list.js'), `module.exports = [${routeList}]`)
    }
  })
  // 监听目录变化(删除、重命名)
  .on('unlinkDir', (path) => {
    let routeName = path.split('/').pop()
    const itemIndex = routeList.findIndex((val) => {
      return val === `'${routeName}'`
    })
    routeList.splice(itemIndex, 1)
    fs.writeFileSync(resolve(__dirname, '../src/route-list.js'), `module.exports = [${routeList}]`)
  })

module.exports = watcher

复制代码

这里面主要做了3件事:监听目录变化、维护目录名列表、把列表写入文件。当开启 watcher 后,可以在 /src 底下看到一个 route-list.js 文件,内容如下:

module.exports = ['页面A','页面B','页面C','页面D']
复制代码

然后我们就可以愉快地使用了……

// view-router.js

import routeList from '../route-list.js';

const routes = [
  { path: '/', component: resolve => require(['~pages/index'], resolve) },
  { path: '*', component: resolve => require(['~pages/index'], resolve) },
];

routeList.forEach((route) => {
  routes.splice(1, 0, {
    path: `/${route}`,
    component: resolve => require([`~pages/${route}/index`], resolve)
  });
});
复制代码
// doc-router.js

import routeList from '../route-list.js';

const routes = [
  { path: '/', component: resolve => require(['~pages/index/README.md'], resolve) }
];

routeList.forEach((route) => {
  routes.push({
    path: `/${route}`,
    component: resolve => require([`~pages/${route}/README.md`], resolve)
  });
});
复制代码

同理,在页面的导航组件里面,我们也加载这个 route-list.js 文件,实现导航内容的自动更新。

放个视频,大家可以感受一下(SF竟然不允许内嵌视频,不科学): v.qq.com/x/page/a051…

7. UI库文件组织约定

这个框架的根本目的,其实是为了UI库的开发。那么我们也应该对UI库的文件组织进行约定。

进入 /src/view/components 目录,我们的整个UI库就放在这里面:

└── components
    ├── index.js // 入口文件
    ├── 组件A
    │   ├── index.vue
    ├── 组件B
    │   ├── index.vue
    ├── 组件C
    │   ├── index.vue
    └── 组件D
        └── index.vue
复制代码

当中的 index.js ,将会以vue plugin的方式编写:

import MyHeader from './组件A'
import MyContent from './组件B'
import MyFooter from './组件C'

const install = (Vue) => {
  Vue.component('my-header', MyHeader)
  Vue.component('my-content', MyContent)
  Vue.component('my-footer', MyFooter)
}

export {
  MyHeader,
  MyContent,
  MyFooter
}

export default install

复制代码

这样,就能够在入口 .js 文件中以 Vue.use(UILibrary) 的形式对UI库进行引用了。

扩展一下,考虑到UI可能有“换肤”的功能,那么我们可以在 /src/view 目录下新建一个 /assets 目录,专门存放样式相关的文件,这个目录最终也会被打包到 /dist 目录下,在使用的时候引入相应样式文件即可。

8. 构建运行命令

前面做了那么多,最终我们希望能够通过简单的 npm script 命令就把整个框架运行起来,应该怎么做呢?

还记得在 /webpack 目录下的三个 config.js 文件吗?它们就是框架跑通的关键,但是我们并不打算直接运行它们,而是在其之上封装一下。

/webpack 目录下新建一个 dev.js 文件,内容如下:

require('./watcher.js')
module.exports = require('./webpack.dev.config.js')

复制代码

同样的,分别新建 build.jsdoc.js 文件,分别引入 webpack.build.config.jswebpack.doc.config.js 即可。

为什么要这么做呢?因为webpack运行的时候会读取 config.js 文件,如果我们希望在webpack工作之前先进行一些预处理,那么这种做法就非常方便了,比如这里添加的监听目录文件变化的功能。如果将来有什么扩展,也可以通过类似的方式进行。

接下来就是在 package.json 里面定义我们的 npm script 了:

"dev": "node_modules/.bin/webpack-dev-server --config webpack/dev.js",
"doc": "node_modules/.bin/webpack -p --config webpack/doc.js --progress --profile --colors",
"build": "node_modules/.bin/webpack -p --config webpack/build.js --progress --profile --colors"
复制代码

值得注意的是,在生产模式下,需要加 -p 才能充分启动webpack2的 tree-shaking 功能。

在根目录下通过 npm run 命令 的方式测试一下是否已经跑起来了呢?

9. 后续工作

  • 添加单元测试
  • 加入PWA功能

10. 尾声

本文篇幅较长,能够看到这里的估计已经有点晕了吧。很久都没有写文章了,终于被我攒了个大招发出来,特别爽。搭建开发框架的过程是一个不断尝试,不断google和stackoverflow的过程。在这个过程中,大到对架构设计,小到对文件组织、工具使用,都有了更进一步的理解。

这个框架的运作模式,其实也是参考了很多业界内的方案,更多的是想要“偷懒”。能让机器自动帮忙搞的,绝对不自己手动搞,这才是技术进步的动力嘛。

该项目已经被改装成 vue-cli 的模板,通过 vue init jrainlau/vue-donut#mobile 即可使用,欢迎尝试,期待反馈和PR,谢谢大家~


以上所述就是小编给大家介绍的《【工程化】从 0 搭建 VueJS 移动端组件库开发框架》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

产品型社群

产品型社群

李善友 / 机械工业出版社 / 2015-3-1 / CNY 69.00

传统模式企业正在直面一场空前的“降维战争”, 结局惨烈,或生或死。 传统模式很难避免悲惨下场, 诺基亚等昔日庞然大物轰然倒塌, 柯达发明了数码成像技术却依然破产, 新商业的兴起到底遵循的是什么模式? 微信轻而易举干掉了运营商的短信业务, “好未来”为何让传统教育不明觉厉? 花间堂为什么不是酒店,而是入口? 将来不会有互联网企业与传统企业之分, ......一起来看看 《产品型社群》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具