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

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

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

项目地址:https://github.com/jrainlau/v...

1. 功能分析

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

clipboard.png

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

  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',// 输出文件加hashoutput: {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.jsimport 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.jsimport 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竟然不允许内嵌视频,不科学):
https://v.qq.com/x/page/a0510...

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移动端组件库开发框架相关推荐

  1. Vite2.0搭建Vue3移动端项目

    Vite2.0搭建Vue3移动端项目 一.搭建包含内容 vite版本.vue3.ts.集成路由.集成vuex.集成axios.配置Vant3.移动端适配.请求代理 二.步骤 vite+ts+vue3只 ...

  2. vue2.0桌面端框架_Element-UI组件库(Vue2.0桌面端组件库)V2.9.2 免费版

    Element-UI组件库(Vue2.0桌面端组件库)是一款很优秀好用的为开发者.设计师和产品经理推出的基于Vue 2.0的桌面端组件库软件.小编带来的这款Element-UI组件库功能强大全面,简单 ...

  3. Element 2.6.0 发布,基于 Vue 2.0 的桌面端组件库

    开发四年只会写业务代码,分布式高并发都不会还做程序员?   Element 2.6.0 发布了,Element 是一套为开发者.设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库,提供了配套设 ...

  4. Element 2.13.0 发布,基于 Vue 的桌面端组件库

    Element 2.13.0 发布了.Element 是一套为开发者.设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库,提供了配套设计资源,帮助你的网站快速成型.由饿了么公司前端团队开源. ...

  5. 值得收藏的8个Web端组件库

    值得收藏的8个Web端组件库 Ant Design 介绍:一个服务于企业级产品的设计体系,基于『确定』和『自然』的设计价值观和模块化的解决方案,让设计者专注于更好的用户体验. 组件库地址:https: ...

  6. vue手机端项目php,MintUI基于Vue.js移动端组件库详解

    Mint UI 包含丰富的 CSS 和 JS 组件,能够满足日常的移动端开发需要.接下来通过本文给大家分享Mint UI 基于 Vue.js 移动端组件库,需要的朋友参考下吧,希望能帮助到大家. 官网 ...

  7. Vue.js(十) element-ui PC端组件库

    一:简介 饿了么公司基于Vue开发了两套UI组件库,PC端组件库 和 移动端组件库. 一部分组件库是对原生的HTML标签元素的封装,增加了一些新的功能. 另一部分组件库是原生HTML标签元素没有的,是 ...

  8. Mint UI —— 基于 Vue.js 的移动端组件库

    写文章登录 Mint UI -- 基于 Vue.js 的移动端组件库 杨奕 8 个月前 Mint UI GitHub:https://github.com/ElemeFE/mint-ui 项目主页:h ...

  9. 移动端cube界面设计html,滴滴 Web 移动端组件库 cube-ui 开源

    滴滴 WebApp 团队在去年底用 Vue.js 2.0 对业务进行重构,并开发了一套移动端组件库 cube-ui 支撑业务的开发.经过了一年多的业务考验,cube-ui 也日趋成熟,而且我们相信除了 ...

最新文章

  1. 不使用乘法、除法或mod,实现两数相除
  2. microsoft visual basic保存时错误429_win10更新失败错误8000FFF的解决小技巧
  3. linux系统运维费用,一般Linux运维学习的费用是多少?Linux学习
  4. 封装一个类搞定90%安卓客户端与服务器端交互
  5. Java 三大框架集成项目结构
  6. 6万人同时离场,竟然一点都不挤?原来用了这个神器
  7. 产品经理如何培养敏锐的商业嗅觉
  8. 在 Mac 上使用“网络实用工具”
  9. 孙宇晨回顾区块链历程:不走热点走心
  10. python-含参函数
  11. mysql事务高级_mysql高级 标量 与事务
  12. Yum介绍与常见用法
  13. OpenCV_(Using GrabCut extract the foreground object) 使用 GrabCut 算法提取前景物体
  14. 教你如何使用数字组件,制作有灵魂的数据可视化大屏
  15. php mysql odbc_一个用mysql_odbc和php写的serach数据库程序
  16. 实际波动率与隐含波动率的计算 python
  17. c语言编译bss和data,bss段和data段的区别
  18. 通信工程专业就业怎么样?难不难学?
  19. 使用自己的ISO文件制作PE
  20. java对数据加解密_java 使用AES对数据进行加密和解密

热门文章

  1. iphone安装Deb文件
  2. Android开发--真机调试出现device offline提示
  3. IntelliJ IDEA 问题总结之二 —— 快捷键、主题样式、导出jar、sqlite
  4. 如何修改Linux命令提示符
  5. - The superclass javax.servlet.http.HttpServlet was not found on the Java
  6. 记2018年9月PAT甲级考试(北京工业大学考场)
  7. spring 学习总结
  8. Java 第六次作业
  9. 如何利用ZBrush中的DynaMesh创建身体(一)
  10. 【CMS】安装CMS