Previously published an article“Vue-Donut-Development Framework for Building UI Component Libraries for Vue”, is only a rough introduction to the framework, and does not give a detailed description of the implementation.
Recently participated in the maintenance of a mobile UI component library within the company, which lacks documents and strict file organization structure.Vue-Donut
The function of is relatively simple, and it is not convenient to create documents and previews for the mobile UI component library. In the referencemint-ui
After the mature plan in the industry, I’m inVue-Donut
At last, a very convenient and automatic development framework is set up.
Because I think the development process is very interesting and I also want to record my own development ideas, I decided to write a good article to share as a record.
Project address:https://github.com/jrainlau/v …
1. Functional analysis
First, let’s plan what is the ultimate goal of this framework:
As shown in the figure, a document page can be generated through the framework. This page is divided into three parts: navigation, document and preview.
-
Navigation: Switch documents and previews of different components through navigation.
-
Document: The document corresponding to this type of component, written in markdown.
-
Preview: the preview page corresponding to this type of component.
In order to make component development and document maintenance more efficient, we hope this framework can be more automated. If we only need to open preview pages of different components and their corresponding description documentsREADME
, the framework can automatically help us generate the corresponding navigation and HTML content, wouldn’t it be wonderful? In addition, when we have developed all the UI components, we put them in/components
Under the directory, if you can build and package one-click through the framework and finally produce an npm package, then it will be very simple for others to use this UI component library. With this in mind, let’s analyze the key technologies that we may need to use.
2. Technical analysis
-
Use webpack2 as the core of the framework: easy to use and highly customizable. At the same time, the webpack2 documents are quite complete, the ecological circle is prosperous, the community is active, and the pits encountered can basically be found in google and stackoverflow.
-
Preview the page to
iframe
When maintaining the component library, only the development of components and the organization of preview pages need to be focused, and navigation and documents need not be distracted to realize decoupling. So it means that this is a Vue.js-basedMulti-page application. -
Auto-generated navigation: using
vue-router
Switch pages. Every time a preview page is created, the corresponding navigation will be automatically generated on the page and the relationship between navigation and routing will be automatically maintained. Therefore, we need a mechanism to monitor changes in file structure. -
Automatically generate documents: a preview page corresponds to a document, so the document should be based on
README.md
Is stored in the corresponding preview page folder. We need someone who canREADME.md
Direct conversion to html content. -
Developer Mode: Start a by a command
webpack-dev-server
, which provides hot update and automatic refresh functions. -
Build the packaging mode: by a command, automatically
/components
All resources in the directory are packaged into one npm package. -
Page construction mode: through a command, static resource files that can be directly deployed and used are generated.
After sorting out the technology, we already have an impression in our mind, and the next step is to develop it step by step.
3. Combing the framework directory structure
A good directory structure can greatly facilitate our next work.
.
ν ─ ─ index.html//html for document page entry
ν ─ ─ view.html//html for preview page entry
Json//dependency declaration, npm script command
├── src
│ ├ ─ Document//Document Page Catalog
-doc-app.vue//document page entry.vue file
-doc-entry.js//document page entry.js file
│ │ ├ ─ doc-router.js//document page routing configuration
│ │ ├ ─ doc _ comps//document page components
│ │ ┖ ─ ─ static//document page static resources
│ ┖ ─ ─ View//Preview Page Catalog
│ ├ ─ assets//preview page static resources
│ ├ ─ Components//UI Component Library
│ ├ ─ Pages//Store different preview pages
View-app.vue//preview page entry.vue file
│ ├-view-entry.js//preview page entry.js file
│ └ ─ View-Router.js//Preview Page Routing Configuration
└── webpack
Js//webpack general configuration
Js//ui library builds packaging configuration.
Js//development mode configuration
Ϣ ─ ─ webpack.doc.config.js//static resource build configuration
As you can see, the directory structure is not complicated. Next we will first configure the webpack so that we can run the project.
4. webapck configuration
4.1 Basic Configuration
Enter into/webpack
Directory, create a new onewebpack.base.config.js
The document reads as follows:
const { join } = require('path')
const hljs = require('highlight.js')
//Configure markdown parsing to highlight code blocks in 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 = {
//only configure output path
output: {
filename: 'js/[name].js',
path: resolve('dist'),
publicPath: '/'
},
//Configure different loader to load resources
// eslint is standard, it is recommended to add
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 can directly convert. md files into vue components
{
test: /\.md$/,
loader: 'vue-markdown-loader',
options: markdown
}
]
},
resolve: {
//This configuration can omit the suffix when loading resources
extensions: ['.js', '.vue', '.json', '.css', '.less'],
modules: [resolve('src'), 'node_modules'],
//Configure path alias
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 Development Mode Configuration
After the basic configuration is completed, we can begin the configuration of the development mode. Under the current directory, create a new onewebpack.dev.config.js
File, and write the following:
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, {
//Since it is a multi-page application, there should be 2 entry files.
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 configuration
devServer: {
contentBase: resolve('/'),
compress: true,
hot: true,
inline: true,
publicPath: '/',
stats: 'minimal'
},
plugins: [
//Hot Update Plug-in
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(),
//Inject the generated js into the portal html file
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true,
chunks: ['app']
}),
new HtmlWebpackPlugin({
filename: 'view.html',
template: 'view.html',
inject: true,
chunks: ['view']
})
]
})
Very simple configuration, it is worth noting that because of multi-page applications, the portal file andHtmlWebpackPlugin
I have to write more than one copy.
4.3 Packaging Configuration of Components
Next, there is a configuration to package the UI component library construction into npm packages. Create a new one namedwebpack.build.config.js
Documents of:
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 File
entry: {
app: './src/view/components/index.js'
},
devtool: 'source-map',
//The output location is dist directory, the name is customized, and the output format is umd format
output: {
path: resolve('dist'),
filename: 'index.js',
library: 'my-project',
libraryTarget: 'umd'
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
},
plugins: [
//Empty the last packing every time.
new CleanWebpackPlugin(['dist'], {
root: resolve('./')
}),
//Copy out static resources so as to realize UI skin changing and other functions
new CopyWebpackPlugin([
{ from: 'src/view/assets', to: 'assets' }
])
]
})
4.4 One-click Generate Document Configuration
Finally, let’s configure thewebpack.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, {
//Similar to developer mode, two entry files, and one more public dependency package vendor
//Starting with `js/'can be automatically output to the `js' directory
entry: {
'js/app': './src/document/doc-entry.js',
'js/view': './src/view/view-entry.js',
'js/vendor': [
'vue',
'vue-router'
]
},
devtool: 'source-map',
//Add hash to output file
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: [
//Extract css file and specify its output location and name
new ExtractTextPlugin({
filename: 'css/[name].[contenthash:8].css',
allChunks: true
}),
//Pull away from public dependence
new webpack.optimize.CommonsChunkPlugin({
names: ['js/vendor', 'js/manifest']
}),
//inject the constructed static resources into multiple entry 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('./')
})
]
})
Through the above configuration, one will eventually be produced.index.html
And oneview.html
, and the css and js files they need. Directly deploy to a static server for access.
To add one more word, the configuration of webpack may seem complicated at first glance, but in fact it is quite simple. The official documents of webpack2 are also quite perfect and easy to read. It is recommended that friends who are not familiar with webpack2 take some time to read the documents carefully.
So far, we have put/webpack
The relevant configurations under the catalog are all set, the basic framework of the framework has been built, and then the development of business logic is started.
5. Business logic development
Create two new entry files under the root directoryindex.html
Andview.html
, add a respectively<div id="app"></div>
And<div id="view"></div>
Label.
Enter/src
Directory, New/document
And/view
Directory, according to the directory structure shown in the previous new directory and files.
The specific content can be seenHereIn short, initializationvue
Application, please ignore temporarilyrouter.js
This section of the code:
routeList.forEach((route) => {
routes.splice(1, 0, {
path: `/${route}`,
component: resolve => require([`~pages/${route}/index`], resolve)
});
});
This is a function related to monitoring directory changes, automatic management and navigation, which will be described in detail later.
The logic is simple./document
And/view
Respectively belong toDocument
AndPreview
Two applicationsPreview
In order toiframe
Is embedded into theDocument
In the application page, the relevant operations are in factDocument
In the middle. When clicking on navigation,Document
The application loads automatically/view/pages/
The of the related preview page folder belowREADME.md
File, modify at the same timeiframe
Link to realize synchronous switching of content.
Next, let’s study how to monitor file directory changes and maintain them automatically.router
Navigation.
6. Automatic maintenancerouter
Navigation
If you have used itNuxt, it must be automatically maintainedrouter
The function of is not unfamiliar. It doesn’t matter if it hasn’t been used, we will realize this function ourselves!
Usevue-router
Some of our classmates may have experienced such a pain point. whenever we create a new page, we have to go there.router.js
Add a declaration to the array ofrouter.js
This is likely to happen:
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) },
...
]
It’s annoying, isn’t it? If only it could be maintained automatically. First of all, we have to make an agreement on how different “pages” should be organized.
In/src/view/pages
Under the directory, every time a “page” is created, we will create a folder with the same name as the page and add documents to it.README.md
And the entranceindex.vue
, the effect is as follows:
└── view
└── pages
◦ page a
│ ├── index.vue
│ └── README.md
◦ page b
│ ├── index.vue
│ └── README.md
◦ page c
│ ├── index.vue
│ └── README.md
Page d
├── index.vue
└── README.md
We have agreed on the organization of the files, and then we need to use a tool to monitor and process them. Here we usechokidarTo achieve.
In/webpack
Create a new one under the directorywatcher.js
Documents:
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
//Monitor directory addition
.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}]`)
}
})
//Monitor directory changes (delete, rename)
.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
There are mainly three things to do: monitoring directory changes, maintaining directory name lists, and writing lists into files. When onwatcher
After, can be in/src
I saw one at the bottom.route-list.js
The document reads as follows:
Exports = ['page a',' page b',' page c',' page D']
Then we can happily use …
// 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)
});
});
Similarly, in the navigation component of the page, we also load thisroute-list.js
File to automatically update navigation content.
Put on a video, you can feel it (SF does not allow embedded video, unscientific):
https://v.qq.com/x/page/a0510 …
7. UI library file organization agreement
The fundamental purpose of this framework is actually to develop UI libraries. Then we should also stipulate the file organization of UI library.
Enter/src/view/components
Directory, our entire UI library is here:
└── components
◦ Index.js//Entry File
Part a
│ ├── index.vue
Part b
│ ├── index.vue
Part c
│ ├── index.vue
-component d
└── index.vue
midindex.js
, will usevue pluginThe way to write:
Import MyHeader from './ component a'
Import MyContent from './ component b'
Import MyFooter from './ component 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
In this way, can be in the entrance.js
In the documentVue.use(UILibrary)
The UI library is referenced in the form of.
To expand, considering that UI may have “skin changing” function, then we can/src/view
Create a new one under the directory/assets
Directory, which specially stores files related to styles, will eventually be packaged into/dist
Directory, when using the introduction of the corresponding style file.
8. Build Run Command
After doing so much, we finally hope to pass the simplenpm script
Command to run the whole framework, how should I do it?
Remember in/webpack
Three in the directoryconfig.js
Documents? They are the key for the framework to run through, but we do not intend to run them directly, but to encapsulate them.
In/webpack
Create a new one under the directorydev.js
The document reads as follows:
require('./watcher.js')
module.exports = require('./webpack.dev.config.js')
Similarly, newly built respectivelybuild.js
Anddoc.js
Documents, introduced separatelywebpack.build.config.js
Andwebpack.doc.config.js
Just.
Why do you want to do this? Because the webpack reads when it runs.config.js
File, if we want to do some preprocessing before the webpack works, then this is very convenient, such as the function added here to monitor changes in directory files. If there is any expansion in the future, it can also be done in a similar way.
The following is inpackage.json
It defines ournpm script
The:
"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"
It is worth noting that in the production mode, it is necessary to add-p
To fully start thetree-shaking
Functions.
Pass under the root directoryNpm run command
How to test if you have already run?
9. Follow-up
-
Add unit test
-
Add PWA function
10. End
This article is a long one. I can see that the estimation here is a little dizzy. I haven’t written an article for a long time, and finally I saved up a big move and sent it out. It was really cool. The process of setting up the development framework is a process of continuous attempts, continuous google and stackoverflow. In this process, we have a better understanding of architecture design, file organization and tool usage.
The mode of operation of this framework actually refers to many schemes in the industry, and is more of a “lazy” attempt. If you can let the machine do it automatically, you will definitely not do it manually. This is the driving force for technological progress.
The project has been modified tovue-cli
The template of, throughvue init jrainlau/vue-donut#mobile
Can be used, welcome to try, look forward to feedback and PR, thank you ~