[Инженерия] Создание среды разработки библиотеки мобильных компонентов VueJS с нуля

Webpack ECMAScript 6

Написано 2017.06.05

ранее опубликованный«Vue-Donut — среда разработки, предназначенная для создания библиотеки компонентов пользовательского интерфейса Vue», является лишь грубым введением в структуру и не описывает реализацию в деталях.

Недавно участвовал в поддержке библиотеки компонентов мобильного интерфейса внутри компании, в которой отсутствует документация и строгая структура организации файлов.Vue-DonutФункция относительно проста, и создавать документы и превью для библиотеки компонентов мобильного интерфейса неудобно. в отношенииmint-uiДождавшись зрелых решений в отрасли, яVue-DonutНа основе расширения был наконец построен очень удобный и автоматизированный фреймворк для разработки.

Поскольку я думаю, что процесс разработки очень интересен, и я хочу записывать свои собственные идеи разработки, я решил написать статью в качестве записи, чтобы поделиться.

адрес проекта:GitHub.com/назвал дождь AU/vu…

1. Функциональный анализ

Во-первых, давайте спланируем, какова конечная цель этого фреймворка:

Как показано на рисунке, с помощью этой платформы можно создать страницу документации. Эта страница разделена на три раздела: Навигация, Документация и Предварительный просмотр.

  1. Навигация: навигация по документации и предварительным просмотрам различных компонентов.

  2. Документ: документ, соответствующий этому типу компонента, написан в форме уценки.

  3. Предварительный просмотр: страница предварительного просмотра, соответствующая этому типу компонента.

Чтобы повысить эффективность разработки компонентов и обслуживания документации, мы хотим, чтобы эта структура была более автоматизированной. Если нам нужно только открыть страницы предварительного просмотра различных компонентов и их соответствующую документациюREADME, платформа может автоматически помочь нам создать соответствующую навигацию и HTML-контент, разве это не замечательно? Кроме того, когда мы разработали все компоненты пользовательского интерфейса, все они размещены в/componentsВ каталоге, если вы можете собрать и упаковать одним щелчком мыши через фреймворк и, наконец, создать пакет npm, другим будет очень легко использовать эту библиотеку компонентов пользовательского интерфейса. Имея в виду эту идею, давайте проанализируем ключевые технологии, которые нам могут понадобиться.

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 Конфигурация упаковки компонентов

Далее следует конфигурация для упаковки сборки библиотеки компонентов пользовательского интерфейса в пакет 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.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

Здесь нужно сделать три основные вещи: отслеживать изменения каталогов, поддерживать список имен каталогов и записывать этот список в файл. при включении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файл для автоматического обновления содержимого навигации.

Выложите видео, все это пощупают (СФ не разрешает встроенное видео, ненаучно):V.QQ.com/small/afraid/ah 051…

7. Соглашения об организации файлов библиотеки пользовательского интерфейса

Основная цель этого фреймворка на самом деле заключается в разработке библиотек пользовательского интерфейса. Затем мы также должны согласовать файловую организацию 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)На библиотеку пользовательского интерфейса ссылаются в виде файла .

Расширяясь, учитывая, что пользовательский интерфейс может иметь функцию «скининга», мы можем/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файл, если мы хотим сделать некоторую предварительную обработку до того, как вебпак заработает, то этот подход очень удобен, например, здесь добавлена ​​функция мониторинга изменений файла каталога. Если в будущем будет какое-либо расширение, это также можно сделать аналогичным образом.

Далее в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Для полноценного запуска webpack2tree-shakingФункция.

через корневой каталогnpm run 命令способ проверить, запустилось ли оно?

9. Последующая работа

  • Добавьте модульные тесты
  • Добавить функцию PWA

10. Эпилог

Эта статья длинная, и немного головокружительно видеть оценки здесь. Давно не писал статьи, и вот наконец-то сохранил большой ход и разослал, что очень круто. Процесс построения фреймворка для разработки — это процесс постоянных попыток, постоянного гугления и stackoverflow. В этом процессе, от большого до проектирования архитектуры, от малого до организации файлов и использования инструментов, у меня есть дальнейшее понимание.

Режим работы этого фреймворка на самом деле относится ко многим решениям в отрасли, и он больше связан с желанием быть «ленивым». Пусть машина делает это автоматически, а не вручную — это движущая сила технического прогресса.

Проект был преобразован вvue-cliшаблон, черезvue init jrainlau/vue-donut#mobileГотов к использованию, добро пожаловать на пробу, с нетерпением жду отзывов и PR, спасибо~