移动端组件库构建指北

752 阅读3分钟

本文主要是通过学习京东移动端组件库(Nut-UI)逐步分析构建一个移动端组件库过程

代码传送

演示传送

构建组件库的目录结构

.gitlab-ci.yml(使用gitlab持续集成持续部署)
│  babel.config.js(babel配置文件)
│  package.json
│  postcss.config.js(postcss预处理配置文件)
│  README.md(说明文档)
├─build(构建配置目录)
│  │  build-pkg.js(构建组件库的入口,生成lib文件夹)
│  │  build-site.js(构建文档官网入口)
│  │  createCpt.js(使用命令行生成待开发的组件)
│  │  dev-server.js(本地开发入口)
│  │  test-server.js(本地测试生成的组件库lib)
│  ├─configs(webpack配置文档)
│  │      package-all.config.js(全量构建组件库配置文件)
│  │      package-base.config.js(构建组件库的基础配置文件)
│  │      package-disperse.config.js(分包构建组件库的配置文件,提供按需加载)
│  │      site-base.config.js(构建文档官网的基础配置文件)
│  │      site-dev.config.js(构建文档官网的开发配置文件)
│  │      site-prod.config.js(构建文档官网的生产配置文件)
│  │      test-lib.config.js(测试生成的lib的配置文件)
│  │      test-unit.config.js(单元测试配置文件)
│  ├─test(单元测试启动入口)
│  │      setup.js
│  └─utils(工具目录)
│          check-version.js(检测node npm版本)
│          dic.js(目录函数)
│          ip.js(获取本地ip)        
├─docs(文档官网的指南部分的md文档)
│      i18n.md
│      intro.md
│      lazy.md
│      theme.md
├─plugin(插件目录)
│  └─mdToVue(md转vue文件)
│          contrast.js
│          index.js  
├─lib(最终生成的组件库目录,使用npm publish发布到npm)
├─site(文档官网,一个多入口的vue应用)
│  ├─demo(文档官网的demo部分,模拟的手机)
│  │      app.vue
│  │      index.html
│  │      index.js
│  │      router.js    
│  └─doc(文档官网的主体部分)
│      │  app.vue
│      │  favicon.ico
│      │  index.html
│      │  index.js
│      │  left-nav.vue
│      │  md.less
│      │  root.js
│      │  router.js
│      ├─assets
│      │      code.png
│      │      copy.png
│      │      qrcode.png    
│      ├─page
│      │      i18n.vue
│      │      intro.vue
│      │      lazy.vue
│      │      theme.vue   
│      └─view
│              button.vue
├─src(组件库源码)
│  │  config.json
│  │  index.js
│  │  index.less
│  ├─assets(静态文件目录)   
│  ├─locale(国际化)
│  │  │  index.js 
│  │  └─lang
│  │          en-US.js
│  │          zh-CN.js         
│  ├─mixins(混入)
│  │      bem.js(使用bem方式构建样式)
│  │      i18n.js(国际化啊入口)
│  ├─packages(组件目录)
│  │  ├─button
│  │  │  │  button.jsx
│  │  │  │  demo.vue
│  │  │  │  doc.md
│  │  │  │  index.js
│  │  │  │  index.less
│  │  │  │  
│  │  │  └─__test__
│  │  │          button.spec.js        
│  ├─style(基础样式)
│  │      base.less
│  │      hairline.less
│  │      var.less   
│  └─utils(工具库目录)
│          constant.js
│          deep-assign.js
│          index.js
│          
└─test(测试构建lib的项目)

构建具体步骤

使用npm run dev启动开发(site文件夹)

  • 构建入口build/dev-server.js
  • doc部分:md转化成vue,作为site/doc依赖
    • docs/*.md => site/doc/page/*.vue
    • src/packages/**/*.md => site/doc/view/*.vue
  • demo部分:会直接引用src里面的组件作为依赖
  //多页面构建入口 site-dev.config.js
  entry:{
    demo: ROOT_PATH('site/demo/index.js'),
    doc: ROOT_PATH('site/doc/index.js')
  },
  output:{
    filename:'js/[name].bundle.js',
    chunkFilename:'js/[name].chunk.js'
  },
  new mdVue({
    entry:ROOT_PATH('docs'),
    output:ROOT_PATH('site/doc/page'),
    cache: false,
    needCode: false
  }),
  new mdVue({
    entry:ROOT_PATH('src'),
    output:ROOT_PATH('site/doc/view'),
    cache: false
  }),

使用npm run site构建生产环境的dist包

  • 使用site-prod.config.js将site目录代码转换成可部署到生产环境的dist包

使用npm run pkg构建组件库

  • 全量构建 package-all.config.js
  entry: {
    zmmui: ROOT_PATH('src/index.js')
  },
  output:{
    path:ROOT_PATH('lib'),
    filename:'zmmui.js',
    library:'[name]',
    libraryTarget:'umd',
    umdNamedDefine: true,
    globalObject: 'this'
  },
  • 分包构建 package-disperse.config.js
  const entry = {}
  confs.packages.map((item) => {
    let cptName = item.name.toLowerCase()
    entry[cptName] = ROOT_PATH(`src/packages/${cptName}/index.js`)
  })

  entry: entry,
  output:{
    path:ROOT_PATH('lib/packages'),
    filename:'[name]/[name].js',
    library:'[name]',
    libraryTarget:'umd',
    umdNamedDefine: true,
    globalObject: 'this'
  },

MardDown文件转化成vue文件的webpack插件

  //遍历文件夹下面的文件
  const nodeFilelist = require('node-filelist');
  // 高亮插件
  const hljs = require('highlight.js');
  // markdown转换成html标签
  let marked = require('marked');
  // 监听文件变化
  let Chokidar = require('chokidar');
  // 计算目录文件的hash
  let { hashElement } = require('folder-hash');
let rendererMd = new marked.Renderer()
rendererMd.heading = (text,level) => {
	// 标题标签的处理逻辑
}
rendererMd.code = (code, infostring) => {
	// code代码的处理逻辑
}
marked.setOptions({
  tables: true,
  renderer: this.rendererMd,
});
  • mdToVue/contrast.js:通过计算缓存文件的hash值,当文件变化时计算并返回需要转换的md文件
  • 当渲染器的heading处理逻辑发生变化时,必须要使用marked.setOptions重置渲染器

按需加载插件babel-plugin-sep-import

// 用来生成一个特定类型的ast语法
const t = require('@babel/types')

module.exports = function() {
  return {
    visitor: {
      //遍历import语法
      ImportDeclaration(p, {opts = {}}) {

        let libraryName = opts.libraryName || 'zmmui'
        let libraryDir = opts.libraryDir || 'lib/packages'
        let style = opts.style || 'css'

        const {node} = p;
        // 如果节点的value值等于目标的libraryName,进行下一步操作
        if(node.source.value && node.source.value == libraryName){
          node.specifiers.forEach((item) => {
            let cpt = item.imported.name
            // 节点前插入处理
            p.insertBefore(
              // 根据规则生成一个新的import语法
              t.importDeclaration(
                [t.importDefaultSpecifier(t.identifier(cpt))],
                t.stringLiteral(`${libraryName}/${libraryDir}/${cpt.toLowerCase()}/index.js`)
              )
            )
            if(style === 'css'){
              p.insertBefore(t.importDeclaration([], t.stringLiteral(`${libraryName}/${libraryDir}/${cpt.toLowerCase()}/${cpt.toLowerCase()}.css`)));
            } else {
              p.insertBefore(t.importDeclaration([], t.stringLiteral(`${libraryName}/${libraryDir}/${cpt.toLowerCase()}/index.${style}`)));
            }
          })
          // 删除原节点
          p.remove()
        }
      }
    }
  }
}

使用npm run cpt生成一个新的组件

const inquirer = require('inquirer') 命令行交互工具,保存用户输入
const parse = require('@babel/parser').parse 将js代码转换成抽象语法树
const traverse = require('@babel/traverse').default 遍历生成的抽象语法树
const generate = require('@babel/generator').default 将修改后的抽象语法树生成新的代码

自动化生成新的组件,以及动态修改、添加入口文件依赖项

单元测试以及代码覆盖率(npm run test:unit)

cross-env NODE_ENV=test nyc mochapack --webpack-config build/configs/test-unit.config.js --require build/test/setup.js src/packages/**/__test__/*.js --reporter=mochawesome
  • --require build/test/setup.js 确保运行单元测试前使用setup提供测试环境
  • --reporter=mochawesome 优化单元测试报告的输出
  • mochapack mocha 与 webpack 结合的单元测试工具
  • nyc 计算测试覆盖率
  • jsdom-global node里面模拟jsdom
  • 单元测试配置文件test-unit.config.js
rules: [
    {
        test: /\.js$|\.jsx$/,
        use: [
            'babel-loader',
            {
            	// 使用istanbul-instrumenter-loader插入代码用于计算覆盖率
                loader: 'istanbul-instrumenter-loader',
                options: { esModules: true },
            }
        ],
        include: [ROOT_PATH('src/packages')],
    },
 ...

使用这种方式计算覆盖率时,如果使用sfc的方式编写组件库,覆盖率一直是0,因此使用了jsx方式编写组件库,能正常计算出代码覆盖率

总结

  • 通过这个项目学习了webpack的配置,babel插件的写法、ast语法树相关、webpack插件、单元测试以及测试覆盖率
  • 祝大家元旦快乐!