【工程化】从0搭建VueJS移动端组件库开发框架
之前发表过一篇《Vue-Donut——专用于构建Vue的UI组件库的开发框架》,仅仅是对框架一个粗略的介绍,并没有针对里面的实现方式进行详细说明。
最近参与维护公司内部的一个针对移动端的UI组件库,该组件库缺乏文档和严格的文件组织结构。Vue-Donut
的功能比较简单,并不能方便地创建针对移动端UI组件库的文档和预览。在参考了mint-ui
等业界内成熟的方案之后,我在Vue-Donut
的基础上进行了拓展,最后搭建出了一个非常方便且自动化的开发框架。
由于觉得开发的过程非常有意思,也想记录一下自己的开发思路,因此决定好好地写一篇文章作为记录分享。
项目地址:https://github.com/jrainlau/v...
1. 功能分析
首先我们来规划一下这个框架的最终目的是什么:
如图所示,通过该框架可以生成一个文档页面。这个页面分为三个部分:导航、文档、预览。
导航:通过导航切换不同组件的文档和预览。
文档:该类型组件所对应的文档,以markdown形式书写。
预览:该类型组件所对应的预览页面。
为了让组件的开发和文档的维护更加高效,我们希望这个框架可以更加自动化。如果我们只要开不同组件的预览的页面及其对应的说明文档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.html
和view.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.js
和doc.js
文件,分别引入webpack.build.config.js
和webpack.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移动端组件库开发框架相关推荐
- Vite2.0搭建Vue3移动端项目
Vite2.0搭建Vue3移动端项目 一.搭建包含内容 vite版本.vue3.ts.集成路由.集成vuex.集成axios.配置Vant3.移动端适配.请求代理 二.步骤 vite+ts+vue3只 ...
- vue2.0桌面端框架_Element-UI组件库(Vue2.0桌面端组件库)V2.9.2 免费版
Element-UI组件库(Vue2.0桌面端组件库)是一款很优秀好用的为开发者.设计师和产品经理推出的基于Vue 2.0的桌面端组件库软件.小编带来的这款Element-UI组件库功能强大全面,简单 ...
- Element 2.6.0 发布,基于 Vue 2.0 的桌面端组件库
开发四年只会写业务代码,分布式高并发都不会还做程序员? Element 2.6.0 发布了,Element 是一套为开发者.设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库,提供了配套设 ...
- Element 2.13.0 发布,基于 Vue 的桌面端组件库
Element 2.13.0 发布了.Element 是一套为开发者.设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库,提供了配套设计资源,帮助你的网站快速成型.由饿了么公司前端团队开源. ...
- 值得收藏的8个Web端组件库
值得收藏的8个Web端组件库 Ant Design 介绍:一个服务于企业级产品的设计体系,基于『确定』和『自然』的设计价值观和模块化的解决方案,让设计者专注于更好的用户体验. 组件库地址:https: ...
- vue手机端项目php,MintUI基于Vue.js移动端组件库详解
Mint UI 包含丰富的 CSS 和 JS 组件,能够满足日常的移动端开发需要.接下来通过本文给大家分享Mint UI 基于 Vue.js 移动端组件库,需要的朋友参考下吧,希望能帮助到大家. 官网 ...
- Vue.js(十) element-ui PC端组件库
一:简介 饿了么公司基于Vue开发了两套UI组件库,PC端组件库 和 移动端组件库. 一部分组件库是对原生的HTML标签元素的封装,增加了一些新的功能. 另一部分组件库是原生HTML标签元素没有的,是 ...
- Mint UI —— 基于 Vue.js 的移动端组件库
写文章登录 Mint UI -- 基于 Vue.js 的移动端组件库 杨奕 8 个月前 Mint UI GitHub:https://github.com/ElemeFE/mint-ui 项目主页:h ...
- 移动端cube界面设计html,滴滴 Web 移动端组件库 cube-ui 开源
滴滴 WebApp 团队在去年底用 Vue.js 2.0 对业务进行重构,并开发了一套移动端组件库 cube-ui 支撑业务的开发.经过了一年多的业务考验,cube-ui 也日趋成熟,而且我们相信除了 ...
最新文章
- 不使用乘法、除法或mod,实现两数相除
- microsoft visual basic保存时错误429_win10更新失败错误8000FFF的解决小技巧
- linux系统运维费用,一般Linux运维学习的费用是多少?Linux学习
- 封装一个类搞定90%安卓客户端与服务器端交互
- Java 三大框架集成项目结构
- 6万人同时离场,竟然一点都不挤?原来用了这个神器
- 产品经理如何培养敏锐的商业嗅觉
- 在 Mac 上使用“网络实用工具”
- 孙宇晨回顾区块链历程:不走热点走心
- python-含参函数
- mysql事务高级_mysql高级 标量 与事务
- Yum介绍与常见用法
- OpenCV_(Using GrabCut extract the foreground object) 使用 GrabCut 算法提取前景物体
- 教你如何使用数字组件,制作有灵魂的数据可视化大屏
- php mysql odbc_一个用mysql_odbc和php写的serach数据库程序
- 实际波动率与隐含波动率的计算 python
- c语言编译bss和data,bss段和data段的区别
- 通信工程专业就业怎么样?难不难学?
- 使用自己的ISO文件制作PE
- java对数据加解密_java 使用AES对数据进行加密和解密