[Engineering] Build VueJS Mobile Component Library Development Framework from 0

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-DonutThe function of is relatively simple, and it is not convenient to create documents and previews for the mobile UI component library. In the referencemint-uiAfter the mature plan in the industry, I’m inVue-DonutAt 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:

clipboard.png

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.

  1. Navigation: Switch documents and previews of different components through navigation.

  2. Document: The document corresponding to this type of component, written in markdown.

  3. 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/componentsUnder 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 toiframeWhen 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: usingvue-routerSwitch 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 onREADME.mdIs stored in the corresponding preview page folder. We need someone who canREADME.mdDirect conversion to html content.

  • Developer Mode: Start a by a commandwebpack-dev-server, which provides hot update and automatic refresh functions.

  • Build the packaging mode: by a command, automatically/componentsAll 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/webpackDirectory, create a new onewebpack.base.config.jsThe 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.jsFile, 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 andHtmlWebpackPluginI 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.jsDocuments 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.htmlAnd 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/webpackThe 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.htmlAndview.html, add a respectively<div id="app"></div>And<div id="view"></div>Label.

Enter/srcDirectory, New/documentAnd/viewDirectory, according to the directory structure shown in the previous new directory and files.

The specific content can be seenHereIn short, initializationvueApplication, please ignore temporarilyrouter.jsThis 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./documentAnd/viewRespectively belong toDocumentAndPreviewTwo applicationsPreviewIn order toiframeIs embedded into theDocumentIn the application page, the relevant operations are in factDocumentIn the middle. When clicking on navigation,DocumentThe application loads automatically/view/pages/The of the related preview page folder belowREADME.mdFile, modify at the same timeiframeLink to realize synchronous switching of content.

Next, let’s study how to monitor file directory changes and maintain them automatically.routerNavigation.

6. Automatic maintenancerouterNavigation

If you have used itNuxt, it must be automatically maintainedrouterThe function of is not unfamiliar. It doesn’t matter if it hasn’t been used, we will realize this function ourselves!

Usevue-routerSome of our classmates may have experienced such a pain point. whenever we create a new page, we have to go there.router.jsAdd a declaration to the array ofrouter.jsThis 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/pagesUnder 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.mdAnd 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/webpackCreate a new one under the directorywatcher.jsDocuments:

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 onwatcherAfter, can be in/srcI saw one at the bottom.route-list.jsThe 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.jsFile 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/componentsDirectory, 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.jsIn 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/viewCreate a new one under the directory/assetsDirectory, which specially stores files related to styles, will eventually be packaged into/distDirectory, when using the introduction of the corresponding style file.

8. Build Run Command

After doing so much, we finally hope to pass the simplenpm scriptCommand to run the whole framework, how should I do it?

Remember in/webpackThree in the directoryconfig.jsDocuments? They are the key for the framework to run through, but we do not intend to run them directly, but to encapsulate them.

In/webpackCreate a new one under the directorydev.jsThe document reads as follows:

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

Similarly, newly built respectivelybuild.jsAnddoc.jsDocuments, introduced separatelywebpack.build.config.jsAndwebpack.doc.config.jsJust.

Why do you want to do this? Because the webpack reads when it runs.config.jsFile, 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.jsonIt defines ournpm scriptThe:

"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-pTo fully start thetree-shakingFunctions.

Pass under the root directoryNpm run commandHow 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-cliThe template of, throughvue init jrainlau/vue-donut#mobileCan be used, welcome to try, look forward to feedback and PR, thank you ~