zoukankan      html  css  js  c++  java
  • 组件库搭建总结

    开始搭建之前要明确需要支持什么能力,再逐个考虑要如何实现。本项目搭建时计划需要支持以下功能:

    • 支持组件测试/demo
    • 支持不同的引入方式 : 全部引入 / 按需加载
    • 支持主题定制
    • 支持文档展示

    组件测试/demo

    本项目是 vue 组件库,组件开发过程中的测试可以直接使用 vue-cli 脚手架,在项目增加了/demos目录,用来在开发过程中调试组件和开发完成后存放各个组件的例子. 只需要修改在vue.config.js中入口路径,即可运行 demos

      index: {
            entry: 'demos/main.ts',
      }
    
      "serve": "cross-env BABEL_ENV=dev vue-cli-service serve",
    

    运行时传入了一个 babel 变量 是用来区分 babel 配置的,后面会有详细说明。

    打包

    js 打包暂时用的还是 webpack, 样式处理使用的是 gulp, 考虑支持两种引入方式,全部引入按需加载,两种场景会有不同的打包需求。

    全部引入

    支持全部引入,需要有一个入口文件,暴露并可以注册所有的组件。 /src/index.ts 就是全部组件的入口,它导出了所有组件,还有一个install函数可以遍历注册所有组件(为什么是 install?详见 vue 插件 )。还需要加一些对script引入情况的处理 —— 直接注册所有组件。

    打包的时候需要以入口文件为打包入口,全部组件一起打包

    按需加载

    顾名思义,使用者可以只加载使用到的组件的 js 及 css,且不论他通过何种方式来按需引入,就组件库而言,我们需要在打包时将各个组件的代码分开打包,这样是他能够按需引入的前提。这样的话,我们需要以每个组件作为入口来分别打包。

    按需加载的实现可以简单的使用require来实现,虽然有点粗暴,需要使用者require对应的组件 js 和 css。查看了一些资料和开源库的做法,发现了更人性化的做法,使用 babel 插件辅助,可以帮我们把import语法转换成require语法,这样使用者在写法上会更加简单。

    比如babel-plugin-component插件,可以查看文档,会帮我们进行语法转换

    import { SectionWrapper } from "xxx";
    
    // 转换成
    require("xxx/lib/section-wrapper");
    require("xxx/lib/css/section-wrapper.css");
    

    那我们需要在按需加载打包时,按照一定的目录结构来放置组件的 js 和 css 文件,方便使用者用 babel 插件来进行按需加载

    样式打包

    同样的,全部引入的样式打包和按需加载的样式打包也有所不同。

    全部引入时,所有的样式文件(组件样式,公共样式)打包成一份文件,使用时引入一次即可。

    按需加载时,样式文件需要分组件来打包,每个组件需要生产一份样式文件,使用时才能分开加载,只引入需要的资源,因为要使用 babel 插件,所以还要控制样式文件的位置。

    所以样式在编写时,就需要公共/组件分开文件,这样方便后面打包处理,考虑目录结构如下:

    │  └─ themes                                                   
    │     ├─ src               // 公共样式                                    
    │     │  ├─ base.less                                          
    │     │  ├─ mixins.less                                        
    │     │  └─ variable.less                                      
    │     ├─ form-factory.less // 组件样式                                    
    │     ├─ index.less        // 所有样式入口
    

    themes/index.less会引入所有组件的样式及公共样式
    themes/components-x.less 只包含组件的样式

    公共资源

    组件之间公用的方法/指令/样式,当然希望能在使用时只加载一份。

    公共样式

    全部引入时没有问题,所有的样式文件都会一起引入。

    按需加载时,不能在组件样式文件中都打包进一份公共样式,这样引入多个组件时,重复的样式太多。考虑把公共样式单独打包出来,按需引入的时候,单独引入一次公共样式文件。这次引入也可以通过babel-plugin-component插件帮我们实现,详见文档中的相关配置。

    公共 JS

    有些js资源(方法/指令)是多个组件都会用到的,不能直接打包到组件中,否则按需加载多个组件时会出现多份重复的资源。所以考虑让组件不打包这些资源,要用到 webpack.externals 配置,webpack.externals 可以从输出的 bundle 中排除依赖,在运行时会从用户环境中获取,详见文档

    这里需要考虑的时,如何辨别哪些是公共js,以及在用户环境中要去哪里获取? , 这里是参考element-ui的做法

    公共JS通过目录来约定,src/utils/directives下为公共指令,src/utils/tools下为公共方法,同样的,引入公共资源的时候也约定好方式,按照配置的webpack.resolve.alias, 这样在可以方便配置 webpack.externals

      // webpack.resolve.alias
      {
        alias: {
          'xxx': resolve('.')
        }
      }
    
      // 引入资源通过  xxx/src/...
      import ClickOutside from 'xxx/src/utils/directives/clickOutside'
    
      // 配置`webpack.externals`
      const directivesList = fs.readdirSync(resolve('/src/utils/directives'))
      directivesList.forEach(function(file) {
        const filename = path.basename(file, '.ts')
        externals[`xxx/src/utils/directives/${filename}`] = `yyy/lib/utils/directives/${filename}`
      })
    

    至于要如何在用户环境中获取,在打包时会吧utils中资源也一起打包发布,所以通过 发布的包名(package.json 中的 name)来获取,也就是上面示例代码中的yyy

    下一步就是要考虑如何处理utils中的文件?,utils中的资源也可能会相互应用,比如方法A中使用了方法B,也需要在处理的时候,要避免相互引入,也要每个单独处理(babel)成单个文件,因为使用者会在用户环境中寻找单个的资源。

    直接使用bable命令行来处理会更加方便

    "build:utils": "cross-env BABEL_ENV=utils babel src/utils --out-dir lib/utils --extensions '.ts'",

    会对每个文件进行babel相关的处理,生成的文件会在 lib/utils中,和上面的webpack.externals配置时对应的

    另外还要使用babel-plugin-module-resolver 插件,查看 文档,这里的作用是让打包之后到新的地方去找文件。比如在 utils/tools/aimport B from 'xxx/src/utils/b',打包之后,会到 'xxx/lib/utils/' 下去找对应的资源

    {
      plugins: [
        ['module-resolver', {
          root: ['xxx'],
          alias: {
            'xxx/src': 'xxx/lib'
          }
        }]
      ]
    }
    
    

    不需要被打包的依赖

    本项目中会使用到ant-design-vuevue库,但是都不需要被打包,这应该是由使用者自己引入的。

    webpack.externals 在上面有用到过,在打包时可以排除依赖

    peerDependencies 可以保证所需要的依赖被安装,详见文档

    这两个配合就可以实现不打包ant-design-vuevue不被打包,也不会影响组件库的运行

    实现

    综上,简单总结下,我们在打包时需要做的事情

    • 全部引入和按需加载需要分开打包
    • 支持全部引入需要,以src/index.ts为入口进行打包,并且需要打包出一份包含所有样式的 css 文件
    • 支持按需加载需要,以每个组件为入口打包出独立的文件,并且需要单独打包出每个组件的样式文件和一份公共样式文件。之后需要按照对应的目录结构放好文件,方便配合 babel 插件实现按需加载
    • 排除不需要被打包的依赖

    需要两份不同的打包,分别对应全部引入和按需加载的打包

        "build:main": "cross-env BABEL_ENV=build webpack --config build/webpack.main.config.js",
        "build:components": "cross-env BABEL_ENV=build webpack --config build/webpack.components.config.js",
    

    以下是两种打包方式都需要做的事情

    配置 webpack.externalsloaderplugins

      function getUtilsExternals() {
        const externals = {}
    
        const directivesList = fs.readdirSync(resolve('/src/utils/directives'))
        directivesList.forEach(function(file) {
          const filename = path.basename(file, '.ts')
          externals[`xxx/src/utils/directives/${filename}`] = `xxx/lib/utils/directives/${filename}`
        })
        const toolsList = fs.readdirSync(resolve('src/utils/tools'))
        toolsList.forEach(function(file) {
          const filename = path.basename(file, '.ts')
          externals[`xxx/src/utils/tools/${filename}`] = `xxx/lib/utils/tools/${filename}`
        })
    
        return externals
      }
    
    
      // webpack配置
      {
        mode: 'production',
        devtool: false,
        externals: {
          ...getUtilsExternals(),
          vue: {
            root: 'Vue',
            commonjs: 'vue',
            commonjs2: 'vue',
            amd: 'vue'
          },
          'ant-design-vue': 'ant-design-vue'
        },
        module:{
          // 相关loader
          rules: [
            {
              test: /.vue$/,
              loader: 'vue-loader',
              options: {
                loaders: {
                  ts: 'ts-loader',
                  tsx: 'babel-loader!ts-loader'
                }
              }
            },
            {
              test: /.tsx?$/,
              exclude: /node_modules/,
              use: [
                'babel-loader',
                {
                  loader: 'ts-loader',
                  options: { appendTsxSuffixTo: [/.vue$/] }
                }
              ]
            }
          ]
        },
        plugins: [
          new ProgressBarPlugin(),
          new VueLoaderPlugin() // vue loader的相关插件
        ]
      }
    

    全部引入

    以下是全部引入的入口和输出,这里打包输出到lib目录下,lib目录是打包后的目录。

    这里需要注意的是同时要配置package.json中的相关字段(main,module),这样发布之后,使用者才知道入口文件是哪个,详见 文档

    这里还需要注意output.libraryTarget的配置,要根据需求来配置对应的值,详见文档

    {
      entry: {
      index: resolve('src/index.ts')
      },
      output: {
        path: resolve('lib'),
        filename: '[name].js',
        libraryTarget: 'umd',
        libraryExport: 'default',
        umdNamedDefine: true,
        library: 'xxx'
      },
    }
    

    按需引入

    以下是按需的入口和输出,入口是解析到所有的组件路径,outputlibraryTarget 也不同,因为按需加载没法支持浏览器加载,所以不需要umd模式

    // 解析路径函数
    function getComponentEntries(path) {
      const files = fs.readdirSync(resolve(path))
      const componentEntries = files.reduce((ret, item) => {
        if (item === 'themes') {
          return ret
        }
        const itemPath = join(path, item)
        const isDir = fs.statSync(itemPath).isDirectory()
        if (isDir) {
          ret[item] = resolve(join(itemPath, 'index.ts'))
        } else {
          const [name] = item.split('.')
          ret[name] = resolve(`${itemPath}`)
        }
        return ret
      }, {})
      return componentEntries
    }
    // webpack配置
    {
      entry: {
        // 解析每个组件的入口
        ...getComponentEntries('components')
      },
      output: {
        path: resolve('lib'),
        filename: '[name]/index.js',
        libraryTarget: 'commonjs2',
        chunkFilename: '[id].js'
      },
    }
    
    

    样式处理

    使用gulp处理样式,对入口样式(所有样式)/ 组件样式 / 公共样式 进行相关处理(less -> css, 前缀,压缩等等),然后放在对应的目录下

    // ./gulpfile.js
    function compileComponents() {
      return src('./components/themes/*.less') // 入口样式,组件样式
        .pipe(less())
        .pipe(autoprefixer({
          cascade: false
        }))
        .pipe(cssmin())
        .pipe(dest('./lib/css'))
    }
    
    function compileBaseClass() {
      return src('./components/themes/src/base.less') // 公共样式
        .pipe(less())
        .pipe(autoprefixer({
          cascade: false
        }))
        .pipe(cssmin())
        .pipe(dest('./lib/css'))
    }
    

    主题定制

    实现主题定制,主要的思路是样式变量覆盖,比如本项目中使用的是less来书写样式,而在less中,同名的变量,后面的会覆盖前面的,详见 文档

    作为组件库,支持主题定制,需要做两点:

    • 会把可能需要变化的样式定义成样式变量,并告诉使用者相关的变量名
    • 提供.less类型的样式引入方式

    项目中的样式本就是通过.less格式编写的,且定义了部分可修改的变量名 components hemessrcvariable.less,需要提供引入less样式的方式即可,要将将less样式整体复制到lib

    // ./gulpfile.js
    function copyLess() {
      return src('./components/themes/**')
        .pipe(cssmin())
        .pipe(dest('./lib/less'))
    }
    
    

    需要自定义样式时,需要使用者,引入less样式文件。如果此时需要按需引入的话,要require对应的组件js文件,不能通过babel插件来实现,因为后者会引入默认的组件样式,和less样式相互影响且重复。

    文档化

    考虑能有一个门户网站,能包含组件库的所有示例和使用文档。

    本项目使用了 storybook 来实现,详见 文档

    所有的内容都在.storybook/ 目录中,需要为每一个组件都编写一个对应的 story

    类型文件

    本项目本身是采用ts编写的,本来考虑采用取巧的方式,通过 typescript编译器 自动生成类型文件的

    独立有一份tsconfig.json,配置了需要生成类型文件

        "declaration": true,
        "declarationDir": "../types",
        "outDir": "../temp",
    

    "types": "rimraf types && tsc -p build && rimraf temp",运行时会把.ts编译为.js,随便生成类型文件,然后删掉生成的js文件即可,这样就只会留下.d.ts类型文件。

    但是这种方式生成的类型文件有点乱,有的还需要自己调整,所以就还是手写。除了查看 typescript官网外,还可以查看 文档

    目录结构

    最终,整体的目录结构是

    xxx                             
    ├─ build                                 webpack配置                                                       
    │  ├─ config.js                                                
    │  ├─ tsconfig.json                                            
    │  ├─ utils.js                                                 
    │  ├─ webpack.components.config.js                             
    │  └─ webpack.main.config.js                                   
    ├─ components                            组件源码                                       
    │  ├─ form-factory                                          
    │  │  ├─ formFactory.tsx                                       
    │  │  └─ index.ts                                                                               
    │  └─ themes                             组件样式                     
    │     ├─ src                                                   
    │     │  ├─ base.less                                          
    │     │  ├─ mixins.less                                        
    │     │  └─ variable.less                                      
    │     ├─ form-factory.less                                     
    │     ├─ index.less                                                                            
    ├─ demos                                  调试文件                                                                  
    ├─ dist                                   storybook打包目录                                                  
    ├─ lib                                    组件库打包目录                   
    │  ├─ css                                                      
    │  │  ├─ base.css                                              
    │  │  ├─ form-factory.css                                      
    │  │  ├─ index.css                                                                              
    │  ├─ form-factory                                             
    │  │  └─ index.js                                              
    │  ├─ less                                                     
    │  │  ├─ src                                                   
    │  │  │  ├─ base.less                                          
    │  │  │  ├─ mixins.less                                        
    │  │  │  └─ variable.less                                      
    │  │  ├─ form-factory.less                                     
    │  │  ├─ index.less                                                                       
    │  ├─ section-wrapper                                          
    │  │  └─ index.js                                              
    │  └─ index.js                                                 
    ├─ public                                                                                                  
    ├─ src
    │  ├─ utils                               工具函数                    
    │  │  ├─ directives                                         
    │  │  ├─ tools                                                                                                  
    │  ├─ global.d.ts                                              
    │  ├─ index.ts                            组件库入口                          
    │  └─ shims-tsx.d.ts                                           
    ├─ tests                                  测试文件                                                       
    ├─ types                                  类型文件                                                              
    ├─ babel.config.js                        babel配置                   
    ├─ gulpfile.js                            gulp配置                     
    ├─ jest.config.js                         jest配置                                                            
    ├─ package.json                                                
    ├─ readme.md                                                   
    ├─ tsconfig.json                          typescript配置                    
    └─ vue.config.js                          vue-cli配置                    
    
    

    发布

    发布时需要注意的是package.json的相关配置,除了上面提到的main,module外,还需要配置以下字段

    {
        "name": "xxx",
        "version": "x.x.x",
        "typings": "types/index.d.ts", // 类型文件 入口路径
        "files": [ // 发布时需要上传的文件
          "lib",
          "types",
          "hcdm-styles"
        ],
        "publishConfig": { //发布地址
          "registry": "http://xxx.xx.x/"
        }
    }
    

    其他

    环境变量的使用

    通过 cross-env 在执行脚本时可以传入变量来做一些事情,本项目用到了两处

    • 通过 BABEL_ENV 来让 babel.config.js 配置来区分环境;vue-cli中提供的@vue/cli-plugin-babel/preset里面配置的东西太多了,导致组件库打包出来体积增大,所以只在变量为dev的时候使用,build的时候使用更简单的必要配置,如下:
    module.exports = {
      env: {
        dev: {
          presets: [
            '@vue/cli-plugin-babel/preset'
          ]
        },
        build: {
          presets: [
            [
              '@babel/preset-env',
              {
                loose: true,
                modules: false
              }
            ],
            [
              '@vue/babel-preset-jsx'
            ]
          ]
        },
        utils: {
          presets: [
            ['@babel/preset-typescript']
          ],
          plugins: [
            ['module-resolver', {
              root: ['xxx'],
              alias: {
                'xxx/src': 'yyy/lib'
              }
            }]
          ]
        }
      }
    }
    
    • 通过 BUILD_TYPE 来控制是否需要引入打包分析插件
    if (process.env.BUILD_TYPE !== 'build') {
      configs.plugins.push(
        new BundleAnalyzerPlugin({
          analyzerPort: 8123
        })
      )
    }
    

    &&串联执行脚本

    "build:lib": "npm run clean &&cross-env BUILD_TYPE=build npm run build:main && cross-env BUILD_TYPE=build npm run build:components && gulp",

    && 可以串联执行脚本,前一个命令执行完才会执行下一个脚本,可以将一组有前后关系的脚本组合在一起

  • 相关阅读:
    python_socket
    python_面向对象(其他)+异常处理+单实例
    并发编程——协程
    数据库开发——MySQL——数据类型——非数值类型
    ALGO-1 区间k大数查询
    数据库开发——MySQL——数据类型——数值类型
    BASIC-10 十进制转十六进制
    BASIC-9 特殊回文数
    BASIC-8 回文数
    BASIC-7 特殊的数字
  • 原文地址:https://www.cnblogs.com/shapeY/p/14659660.html
Copyright © 2011-2022 走看看