关于组件库:如何从头到尾做一个UI组件库

4次阅读

共计 25039 个字符,预计需要花费 63 分钟才能阅读完成。

首先咱们咱们的这个 ui 组件库是开发的 vue 版本,如果须要变通其余版本的话,就把 vue 相干的编译器移除,其余相干的编译器加上就能够了(例如 react),打包构建形式是通用的。

组件库的组织发版和我的项目是不太一样的,这里提下思路。
首先咱们要确定咱们做的库要满足的需要:

  1. 反对全副加载
  2. 反对按需加载
  3. ts 的补充类型反对
  4. 同时反对 cjs 和 esm 版本

晓得了咱们要反对的需要之后,要确定一下咱们最初包的目录构造是什么样的,如下:

这简略形容下为何是这样的构造,首先 index.esm 是 咱们整全量的包,外面蕴含了所有的 ui 组件,还有一个 index.cjs 版本,在打包工具不反对 esm 时会应用 cjs 版本,两个版本能够更好的反对不同的打包工具。

lib 下放的是咱们单个组件,用来联合 babel-plugin-import 来做按需加载。
这里先简略做一个概括,后续实现的时候会做具体的解释。

好了,理解了 咱们最初的我的项目构造,就要开始 ui 库的搭建了,后续所有的操作配置,都是为了在保障程序健壮性通用性的根底上来打进去咱们最初要公布的这个包的构造。


设计及流程

代码的组织形式 Monorepo

Monorepo 是治理我的项目代码的一个形式,指在一个我的项目仓库 (repo) 中治理多个模块 / 包(package),不同于常见的每个模块建一个 repo。
例如:

├── packages
|   ├── pkg1
|   |   ├── package.json
|   ├── pkg2
|   |   ├── package.json
├── package.json

这样的构造,能够看到这些我的项目的第一级目录的内容以脚手架为主,次要内容都在 packages 目录中、分多个 package 进行治理。

一些出名的库例如 vue3.0 和 react 都是采纳这种形式来治理我的项目的。


后续咱们会依据这些 packages 里的小包,来生成按需加载的文件。

包的管理工具采纳 yarn, 因为咱们要用到它的 workspaces 依赖治理

如果不必 workspaces 时,因为各个 package 实践上都是独立的,所以每个 package 都保护着本人的 dependencies,而很大的可能性,package 之间有不少雷同的依赖,而这就可能使 install 时呈现反复装置,使原本就很大的 node_modules 持续收缩(这就是「依赖爆炸」…)。

为了解决这个问题在这里咱们要应用 yarn 的 workspaces 个性,这也就是依赖治理咱们为什么应用 yarn 的起因。
而应用 yarn 作为包管理器的同学,能够在 package.json 中以 workspaces 字段申明 packages,yarn 就会以 monorepo 的形式治理 packages。
应用形式详情能够查看它的官网文档
文档

咱们在 package.json 开启了 yarn 的 workspaces 工作区之后,以后这个目录被称为了工作区根目录,工作区并不是要公布的,而后这会咱们在下载依赖的时候,不同组件包里的雷同版本的依赖会下载到工作区的 node_modules 里,如果以后包依赖的版本和其余不一样就会下载到以后包的 node_modules 里。

yarn 的话突出的是对依赖的治理,包含 packages 的相互依赖、packages 对第三方的依赖,yarn 会以 semver 约定来剖析 dependencies 的版本,装置依赖时更快、占用体积更小。

lerna

这里简略提一下 lerna,因为目前支流的 monorepo 解决方案是 Lerna 和 yarn 的 workspaces 个性,它次要用来管理工作流,然而它个人感觉如果你须要一次性公布 packages 里的所有包时,用它会比拟不便,咱们这里没有过多的用到它。

Storybook 开发阶段的调试

组件成果的调试和应用介绍咱们通过 Storybook 来进行治理,这是一个可视化的组件展现平台,它能够让咱们在隔离的开发环境 交互地开发和测试组件,最初也能够生成应用阐明的动态界面,它反对很多框架例如:vue.react,ng,React Native 等。

jest 单元测试

单元测试的话咱们应用 Facebook 的 jest

plop 创立雷同模版

咱们包的构造是这样的, 例如 avatar:

├── packages
|   ├── avatar
|   |   ├── __test__  // 单元测试文件
|   |   ├── src // 组件文件
|   |   ├── stories //storyBook 开发阶段预览的展现,扫描文件
|   |   ├── index.ts // 包入口
|   |   ├── LICENSE
|   |   ├── README.MD
|   |   ├── package.json
├── package.json

每个 UI 组件的构造根本都是一样的,所以在这里咱们选用 plop 来对立生成模版,plop 次要用于创立我的项目中特定文件类型的小工具,相似于 Yeoman 中的 sub generator,个别不会独立应用。个别会把 Plop 集成到我的项目中,用来自动化的创立同类型的我的项目文件。

Rollup 进行打包

最初是构建操作,这里咱们打包不应用 webpack,而是用 Rollup,。
webpack 的话更适宜我的项目工程应用,因为我的项目里很多动态资源须要解决,再或者构建的我的项目须要引入很多 CommonJS 模块的依赖,这样尽管它也有摇树的性能 tree-shaking(额定配置), 然而因为要解决转换其余文件所以它打进去的包还是会有一些冗余代码。
而 rollup 也是反对 tree-shaking 的,而且它次要是针对 js 打包应用,它打包后果比 webpack 更小,开发类库用它会更适合。

上面讲下构建过程:

首先我贴一个我最初残缺版本的依赖,如下:

{
  "name": "c-dhn-act",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "gitlabGroup": "component",
  "devDependencies": {
    "@babel/cli": "^7.13.16",
    "@babel/core": "^7.11.4",
    "@babel/plugin-transform-runtime": "^7.13.15",
    "@babel/preset-env": "^7.11.5",
    "@babel/preset-typescript": "^7.13.0",
    "@rollup/plugin-json": "^4.1.0",
    "@rollup/plugin-node-resolve": "^8.4.0",
    "@storybook/addon-actions": "6.2.9",
    "@storybook/addon-essentials": "6.2.9",
    "@storybook/addon-links": "6.2.9",
    "@storybook/vue3": "6.2.9",
    "@types/jest": "^26.0.22",
    "@types/lodash": "^4.14.168",
    "@vue/compiler-sfc": "^3.1.4",
    "@vue/component-compiler-utils": "^3.2.0",
    "@vue/shared": "^3.1.4",
    "@vue/test-utils": "^2.0.0-rc.6",
    "babel-core": "^7.0.0-bridge.0",
    "babel-jest": "^26.6.3",
    "babel-loader": "^8.2.2",
    "babel-plugin-lodash": "^3.3.4",
    "cp-cli": "^2.0.0",
    "cross-env": "^7.0.3",
    "css-loader": "^5.2.6",
    "http-server": "^0.12.3",
    "inquirer": "^8.0.0",
    "jest": "^26.6.3",
    "jest-css-modules": "^2.1.0",
    "json-format": "^1.0.1",
    "lerna": "^4.0.0",
    "plop": "^2.7.4",
    "rimraf": "^3.0.2",
    "rollup": "^2.45.2",
    "rollup-plugin-alias": "^2.2.0",
    "rollup-plugin-terser": "^7.0.2",
    "rollup-plugin-typescript2": "^0.30.0",
    "rollup-plugin-vue": "^6.0.0",
    "sass": "^1.35.1",
    "sass-loader": "10.1.1",
    "storybook-readme": "^5.0.9",
    "style-loader": "^2.0.0",
    "typescript": "^4.2.4",
    "vue": "3.1.4",
    "vue-jest": "5.0.0-alpha.5",
    "vue-loader": "^16.2.0"
  },
  "peerDependencies": {"vue": "^3.1.x"},
  "scripts": {
    "test": "jest --passWithNoTests",
    "storybookPre": "http-server build",
    "storybook": "start-storybook -p 6006",
    "build-storybook": "build-storybook --quiet --docs -o ui",
    "lerna": "lerna publish",
    "buildTiny:prod": "cross-env NODE_ENV=production rollup -c buildProject/rollup.tiny.js",
    "buildTiny:dev": "cross-env NODE_ENV=development rollup -c buildProject/rollup.tiny.js",
    "clean": "lerna clean",
    "plop": "plop",
    "clean:lib": "rimraf dist/lib",
    "build:theme": "rimraf packages/theme-chalk/lib && gulp build --gulpfile packages/theme-chalk/gulpfile.js && cp-cli packages/theme-chalk/lib dist/lib/theme-chalk && rimraf packages/theme-chalk/lib",
    "build:utils": "cross-env BABEL_ENV=utils babel packages/utils --extensions .ts --out-dir dist/lib/utils",
    "buildAll:prod": "cross-env NODE_ENV=production rollup -c buildProject/rollup.all.js",
    "buildAll:dev": "cross-env NODE_ENV=development rollup -c buildProject/rollup.all.js",
    "build:type": "node buildProject/gen-type.js",
    "build:v": "node buildProject/gen-v.js",
    "build:dev": "yarn build:v && yarn clean:lib  && yarn buildTiny:dev && yarn buildAll:dev && yarn build:utils && yarn build:type && yarn build:theme",
    "build:prod": "yarn build:v && yarn clean:lib  && yarn buildTiny:prod && yarn buildAll:prod && yarn build:utils  && yarn build:type && yarn build:theme"
  },
  "dependencies": {
    "comutils": "1.1.9",
    "dhn-swiper": "^1.0.0",
    "lodash": "^4.17.21",
    "vue-luck-draw": "^3.4.7"
  },
  "private": true,
  "workspaces": ["./packages/*"]
}

能够看到我这里 storyBook 用的是 6.2.9 版本的,这里不必最新版是因为无奈最初开启文档模式,不晓得当初问题解决了没有。

我的项目的初始化能够采纳 storyBook 的脚手架,后续咱们再往里面添货色。
初始化咱们用的是 vue3.0 版本,这里大家能够按手册去初始化
storybook 官网 vue 初始化手册

官网也提供了,其余框架我的项目的初始化。
初始化实现后,咱们找到.storyBook 文件夹,咱们须要批改他上面的内容:
main.js 改成这样,如下:

const path = require('path');
module.exports = {
  "stories": [
    "../stories/**/*.stories.mdx",
    "../packages/**/*.stories.mdx",
    "../packages/**/*.stories.@(js|jsx|ts|tsx)",
  ],
  "addons": [
    "@storybook/addon-links",
    "@storybook/addon-essentials"
  ],
  webpackFinal: async (config, { configType}) => {
    // `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION'
    // You can change the configuration based on that.
    // 'PRODUCTION' is used when building the static version of storybook.

    // Make whatever fine-grained changes you need
    config.module.rules.push({
      test: /\.scss$/,
      use: ['style-loader', 'css-loader', {
        loader:'sass-loader',  // 这种是指定 dart-sass  代替 node-sass  不然一些数学函数 用不了  math 函数只有 dart-sass 能够用  
        options:{implementation:require("sass")
        }
      }],
      include: path.resolve(__dirname, '../'),
    });

    // Return the altered config
    return config;
  },
}

这里 stories 配置项,配置门路里放的是界面要出现的阐明和组件,匹配到的 mdx 里放的是它的应用指引,mdx 是 markdowm 和 jsx 的联合。

addons 里放的是它的一些插件,addon-essentials 是插件汇合(汇合),蕴含了一系列的插件 能够保障咱们开箱即用,addon-links 用来设置链接的插件。

webpackFinal 是针对 webpack 的一些扩大,咱们这里用 dart-sass 代替了 node-sass,不然一些数学函数 用不了,例如 math 函数只有 dart-sass 能够用。

那咱们 packages/avatar/stories/avatar.stories.mdx 下语法,你能够参考官网 mdx 语法

workspaces 和 private 曾经在 packsge.json 里配置了,

  "private": true,
  "workspaces": ["./packages/*"]

如果有不理解 workspaces 的作用的,能够百度下它的作用。

而后就是 ts 和 jest 的集成

首先咱们先来集成 ts,首先下载依赖:
次要的包有俩:

yarn add typescript rollup-plugin-typescript2 -D -W

而后批改 tsconfig.json

{
    "compilerOptions": {
      "module": "ESNext",// 指定应用的模块规范
      "declaration": true,// 生成申明文件,开启后会主动生成申明文件
      "noImplicitAny": false,// 不容许隐式的 any 类型
      "strict":true,// 开启所有严格的类型查看
      "removeComments": true,// 删除正文 
      "moduleResolution": "node", // 模块解析规定 classic 和 node 的区别   https://segmentfault.com/a/1190000021421461
      //node 模式下,非相对路径模块 间接去 node_modelus 下查找类型定义.ts 和补充申明.d.ts
      //node 模式下相对路径查找 逐级向上查找 当在 node_modules 中没有找到,就会去 tsconfig.json 同级目录下的 typings 目录下查找.ts 或 .d.ts 补充类型申明
      // 例如咱们这里的.vue 模块的  类型补充(.ts 文件不意识.vue 模块, 须要咱们来定义.vue 模块的类型)
      "esModuleInterop": true,// 实现 CommonJS 和 ES 模块之间的互操作性。抹平两种标准的差别
      "jsx": "preserve",// 如果写 jsx 了,放弃 jsx 的输入,不便后续 babel 或者 rollup 做二次解决
      "noLib": false,
      "target": "es6", // 编译之后版本
      "sourceMap": true, // 生成
      "lib": [ // 蕴含在编译中的库
        "ESNext", "DOM"
      ],
      "allowSyntheticDefaultImports": true, // 用来指定容许从没有默认导出的模块中默认导入
    },
    "exclude": [ // 排除
      "node_modules"
    ],

}
   

而后集成一下 jest

yarn add @types/jest babel-jest jest jest-css-modules vue-jest @vue/test-utils -D -W

倡议下载的依赖包版本,以我的我的项目的 lock 为准,因为这个是我校验过得稳固版本,降级新版本可能会导致不兼容。

这里 -D - W 是装置到工作区根目录并且是开发依赖的意思,这里 jest 是 Facebook 给提供的单元测试库官网举荐的,@vue/test-utils 它是 Vue.js 的官网测试实用程序库,联合 jest 一起应用 配置起码,解决单文件组件 vue-jest,babel-jest 对测试代码做降级解决,jest-css-modules 用来疏忽测试的 css 文件。
而后咱们在根目录新建 jest.config.js 单元测试的配置文件:

module.exports = {"testMatch": ["**/__tests__/**/*.test.[jt]s?(x)"],  // 从哪里找测试文件   tests 下的
  "moduleFileExtensions": [ // 测试模块倒入的后缀
    "js",
    "json",
    // 通知 Jest 解决 `*.vue` 文件
    "vue",
    "ts"
  ],
  "transform": {
    // 用 `vue-jest` 解决 `*.vue` 文件
    ".*\\.(vue)$": "vue-jest",
    // 用 `babel-jest` 解决 js 降
    ".*\\.(js|ts)$": "babel-jest" 
  },
  "moduleNameMapper" : {"\\.(css|less|scss|sss|styl)$" : "<rootDir>/node_modules/jest-css-modules"
  }
}

而后再配置一下 babel.config.js 咱们测试用到了降级解决 , 后续打生产包时咱们会通过 babel 环境变量 utils,来应用对应配置转换 packages/utils 里一些工具函数。
babel.config.js:

module.exports = {
  // ATTENTION!!
  // Preset ordering is reversed, so `@babel/typescript` will called first
  // Do not put `@babel/typescript` before `@babel/env`, otherwise will cause a compile error
  // See https://github.com/babel/babel/issues/12066
  presets: [
    ['@babel/env', //babel 转换 es6 语法插件汇合],
    '@babel/typescript',  //ts
  ],
  plugins: [
    '@babel/transform-runtime', // 垫片按需反对 Promise,Set,Symbol 等
    'lodash', 
    // 一个简略的转换为精挑细选的 Lodash 模块,因而您不用这样做。// 与联合应用,能够生成更小的樱桃精选版本!https://download.csdn.net/download/weixin_42129005/14985899
  // 个别配合 lodash-webpack-plugin 做 lodash 按需加载
  ],
  env: {
    utils: { // 这个 babel 环境变量是 utils 笼罩上述 的配置 这里临时不会用 先正文掉
      presets: [
        [
          '@babel/env',
          {
            loose: true,// 更快的速度转换
            modules: false,// 不转换 esm 到 cjs, 反对摇树  这个下面不配置 不然 esm 标准会导致 jest 测试编译不过
          },
        ],
      ],
      // plugins: [
      //   [
      //     'babel-plugin-module-resolver',
      //     {//       root: [''],
      //       alias: {//},
      //     },
      //   ],
      // ],
    },
  },
}

而后咱们在 package.json 中批改 script 命令 “test”: “jest”,
Jest 单元测试具体怎么写 能够依据本人的 需要去查看官网文档。

plop 生成组件模板

咱们结尾的时候说过咱们每个包的构造,长得都是一样的,而后每生成一个组件包的话都要手动创立构造的话太麻烦了。

├── packages
|   ├── avatar
|   |   ├── _test_  // 单元测试文件夹
|   |   ├─────  xxx.test.ts // 测试文件
|   |   ├── src // 组件文件文件夹
|   |   ├───── xxx.vue // 组件文件
|   |   ├── stories // 故事书调试的 js
|   |   ├───── xxx.stories.ts // 组件文件
|   |   ├── index.js // 包入口
|   |   ├── LICENSE
|   |   ├── README.MD
|   |   ├── package.json
├── package.json

咱们的根本构造是这样,而后咱们抉择 plop 生成模板,咱们后面提到了 plop 次要用于创立我的项目中特定文件类型的小工具。咱们把它装置到我的项目中 yarn add plop -D -W

而后创立它的配置文件 plopfile.js

module.exports = plop => {
    plop.setGenerator('组件', {
      description: '自定义组件',
      prompts: [
        {
          type: 'input',
          name: 'name',
          message: '组件名称',
          default: 'MyComponent'
        },
        {
          type: "confirm",
          message: "是否是组合组件",
          name: "combinationComponent",
          default:false
        }
      ],
      actions: [
        {
          type: 'add',
          path: 'packages/{{name}}/src/{{name}}.vue',
          templateFile: 'plop-template/component/src/component.hbs'
        },
        {
          type: 'add',
          path: 'packages/{{name}}/__tests__/{{name}}.test.ts',
          templateFile: 'plop-template/component/__tests__/component.test.hbs'
        },
        {
          type: 'add',
          path: 'packages/{{name}}/stories/{{name}}.stories.ts',
          templateFile: 'plop-template/component/stories/component.stories.hbs'
        },
        {
          type: 'add',
          path: 'packages/{{name}}/index.ts',
          templateFile: 'plop-template/component/index.hbs'
        },
        {
          type: 'add',
          path: 'packages/{{name}}/LICENSE',
          templateFile: 'plop-template/component/LICENSE'
        },
        {
          type: 'add',
          path: 'packages/{{name}}/package.json',
          templateFile: 'plop-template/component/package.hbs'
        },
        {
          type: 'add',
          path: 'packages/{{name}}/README.md',
          templateFile: 'plop-template/component/README.hbs'
        },
        {
          type: 'add',
          path: 'packages/theme-chalk/src/{{name}}.scss',
          templateFile: 'plop-template/component/template.hbs'
        }
      ]
    })
  }

这里通过命令行询问交互 来生成 组件,而后咱们来依据咱们的配置文件来新建 文件夹和模板。

模板的构造是这样。
而后 咱们来看下对应的模板 长什么样子,如下:
component.test.hbs

import {mount} from '@vue/test-utils'
import Element from '../src/{{name}}.vue'

describe('c-dhn-{{name}}', () => {test('{{name}}-text',() => {const wrapper = mount(Element)
        expect(wrapper.html()).toContain('div')
    })
})

component.hbs

<template>
  <div>
    <div @click="handleClick">tem</div>
  </div>
</template>

<script lang="ts">
import {defineComponent} from 'vue'
interface I{{properCase name}}Props {

}
export default defineComponent({name: 'CDhn{{properCase name}}',
  setup(props: I{{properCase name}}Props, {emit}) {
    //methods
    const handleClick = evt => {alert('tem')
    }

    return {handleClick,}
  }
})
</script>

<style  lang="scss">
</style>

component.stories.hbs

import CDhn{{properCase name}} from '../'

export default {title: 'DHNUI/{{properCase name}}',
  component: CDhn{{properCase name}}
}


export const Index = () => ({setup() {return {};
  },
  components: {CDhn{{properCase name}} },
  template: `
    <div>
       <c-dhn-{{name}} v-bind="args"></c-dhn-{{name}}>
    </div>
  `,
});

index.hbs

import CDhn{{properCase name}} from './src/{{name}}.vue'
import {App} from 'vue'
import type {SFCWithInstall} from '../utils/types'

CDhn{{properCase name}}.install = (app: App): void => {app.component(CDhn{{properCase name}}.name, CDhn{{properCase name}})
}

const _CDhn{{properCase name}}: SFCWithInstall<typeof CDhn{{properCase name}}> = CDhn{{properCase name}}

export default _CDhn{{properCase name}}

而后咱们在 package.json 中增加一个 script 命令 “plop”: “plop”

执行之后就能够生产对应的文件了,具体的能够吧我的项目下载下载看一下。

到这里咱们测试,开发环境的 storyBook 和生产文件的 plop,曾经完事了。
上面就该看如何打出生产环境的包了。

rollup 构建打包

首先新建 buildProject 文件夹,咱们的一些命令脚本都会放在这里。
这里打包分为两种,按需加载和全量包,这两种形式有一些配置是一样的,咱们这里写一个公共的配置文件 rollup.comon.js


import json from '@rollup/plugin-json'
import vue from 'rollup-plugin-vue' //vue 相干配置,css 抽取到 style 标签中  编译模版
// import postcss from 'rollup-plugin-postcss'
import {terser} from 'rollup-plugin-terser'  // 代码压缩
import {nodeResolve} from '@rollup/plugin-node-resolve'
import alias from 'rollup-plugin-alias';
const {noElPrefixFile} = require('./common')
const pkg = require('../package.json')

const isDev = process.env.NODE_ENV !== 'production'
const deps = Object.keys(pkg.dependencies)
// 公共插件配置
const plugins = [
    vue({
      // Dynamically inject css as a <style> tag 不插入
      css: false,
      // Explicitly convert template to render function
      compileTemplate: true,
      target: 'browser'
    }),
    json(), //json 文件转换成 es6 模块
    nodeResolve(), // 应用 Node 解析算法定位模块,用于解析 node_modules 中的第三方模块
    // 大多数包都是以 CommonJS 模块的模式呈现的,如果有须要应用 rollup-plugin-commonjs 这个插件将 CommonJS 模块转换为 ES2015 供 Rollup 解决
    // postcss({// 和 css 集成 反对  组件库 不能应用  公有作用域 css   不然提供给他人用时  笼罩起来太吃力
    //   // 把 css 插入到 style 中
    //   // inject: true,
    //   // 把 css 放到和 js 同一目录
    //   extract: true
    // }),
    alias({resolve: ['.ts', '.js','.vue','.tsx'],
      entries:{'@':'../packages'}
    })
  ]
  // 如果不是开发环境,开启压缩
isDev || plugins.push(terser())


function external(id) {return /^vue/.test(id)||
  noElPrefixFile.test(id)|| 
  deps.some(k => new RegExp('^' + k).test(id))
}
export {plugins,external};

这里咱们把 utils 下的工具函数 (这个排除进来是因为 咱们要用 babel 来做语法转换) 和 vue 库,和咱们所有应用的生产包 排除了进来,保障包的小体积.
common.js 目前只用到了 utils

module.exports = {noElPrefixFile: /(utils|directives|hooks)/,
}
  
  1. 按需加载:工作区 packages 里的每个组件都生成对应的 js,不便前期配合 babel 插件做按需引入
    rollup.tiny.js 这个文件是针对工作区组件的配置
import {plugins,external} from './rollup.comon'
import path from 'path'
import typescript from 'rollup-plugin-typescript2'

const {getPackagesSync} =  require('@lerna/project')

module.exports = getPackagesSync().filter(pkg => pkg.name.includes('@c-dhn-act')).map(pkg => {const name =  pkg.name.split('@c-dhn-act/')[1] // 包名称
  return {input: path.resolve(__dirname, '../packages', name, 'index.ts'), // 入口文件,造成依赖图的开始
    output: [ // 进口  输入
      {
        exports: 'auto',
        file: path.join(__dirname, '../dist/lib', name, 'index.js'), //esm 版本
        format: 'es',
      },
    ],
    plugins: [

      ...plugins,
      typescript({
        tsconfigOverride: {
          compilerOptions: {declaration: false, // 不生成类型申明},
          'exclude': [
            'node_modules',
            '__tests__',
            'stories'
          ],
        },
        abortOnError: false,
      }),
    ],
    external
  }
})

这样的话通过 rollup 启动打包时 会顺次在 dist 下生成 咱们对应的文件,包名中蕴含 @c-dhn-act 会认为是咱们的小包 (留神点:这里的 name 文件夹的名称,是原始工作区 package.json 包的名字, 例如 avatar,在这咱们生成.d.ts 类型补充时也要用它,然而这里和咱们最初要产出的名字不合乎, 后续打包完会进行一个改名操作)

留神:

这里疏忽了 utils 工具函数,这是因为 咱们后续提供给其他人应用时,如果不疏忽 utils 就会被打进来,而后如果其他人要是援用了不同的组件,然而不同的组件里援用了雷同的工具函数(函数被打包到了组件文件中)。

拿 webpack 举例这会就会有个问题,在 webpack 中应用的话,webpack 针对模块 会有函数作用域的隔离,所以 即便是工具函数名称雷同也不会给笼罩掉,这样就会导致 webpack 打进去最终后果包变大。

而 utils 工具函数被疏忽之后,不被打到 文件中,而是通过 import 导入的形式应用,这样在 webpack 应用的时候,就能够充沛的利用它的模块缓存机制,首先包的大小被缩小了,其次因为用到了缓存机制也会晋升加载速度。

webpack 伪代码
我这里轻易手写了一段 webpack 打包后的伪代码,不便了解,大家看下
utils 是独自模块时,大略是这样的

(function(modules){var installedModules = {};
    function __webpack_require__(moduleId){
        // 缓存中有返回缓存的模块
        // 定义模块,写入缓存 module
        // {
        //     i: moduleId,
        //     l: false,
        //     exports: {}
        // };
        // 载入执行 modules 中对应函数
        // 批改模块状态为已加载
        // 返回函数的导出  module.exports
    }
    /** 一系列其余定义执行
     * xxx
     * xxx 
     * xxx
     */
    return __webpack_require__(__webpack_require__.s = "xxx/index.js");
})({"xxx/index.js":(function(module, __webpack_exports__, __webpack_require__){var button = __webpack_require__(/*! ./button.js */ "xxx/button.js");
        var input = __webpack_require__(/*! ./input.js */ "xxx/input.js");
    }),
    "xxx/button.js":(function(module, __webpack_exports__, __webpack_require__){var btn_is_array = __webpack_require__(/*! ./utils.js */ "xxx/utils.js");
        btn_is_array([])
        module.exports = 'button 组件'
    }),
    "xxx/input.js":(function(module, __webpack_exports__, __webpack_require__){var ipt_is_array = __webpack_require__(/*! ./utils.js */ "xxx/utils.js");
        ipt_is_array([])
        module.exports = 'input 组件'
    }),
    "xxx/utils.js":(function(module, __webpack_exports__, __webpack_require__){module.exports = function isArray(arr){//xxxxx 函数解决,假如是特地长的函数解决} 
    })
})

第二种 utils 里的办法和组件打到了一起,webpack 中应用时会是这样的

(function(modules){var installedModules = {};
    function __webpack_require__(moduleId){
        // 缓存中有返回缓存的模块
        // 定义模块,写入缓存 module
        // {
        //     i: moduleId,
        //     l: false,
        //     exports: {}
        // };
        // 载入执行 modules 中对应函数
        // 批改模块状态为已加载
        // 返回函数的导出  module.exports
    }
    /** 一系列其余定义执行
     * xxx
     * xxx 
     * xxx
     */
    return __webpack_require__(__webpack_require__.s = "xxx/index.js");
})({"xxx/index.js":(function(module, __webpack_exports__, __webpack_require__){var button = __webpack_require__(/*! ./button.js */ "xxx/button.js");
        var input = __webpack_require__(/*! ./input.js */ "xxx/input.js");
    }),
    "xxx/button.js":(function(module, __webpack_exports__, __webpack_require__){function isArray(arr){// 特地长的函数解决}
        isArray([])
        module.exports = 'button 组件'
    }),
    "xxx/input.js":(function(module, __webpack_exports__, __webpack_require__){function isArray(arr){// 特地长的函数解决}
        isArray([])
        module.exports = 'input 组件'
    }),

})

咱们能够比照一下,这样能够验证下,首先看看第二份 webpack 伪代码,咱们看传入的参数,对象的 key 是文件的门路,值就是一个函数(未打包之前的模块,函数内容就是咱们模块的代码),这里有函数作用域做隔离,首先定义上就是反复定义,而且有隔离也不能复用,其次因为这样的一堆反复的冗余代码也会导致最初包变大(咱们的组件库导致的),最初就是每次加载模块都须要从新定义 isArray 函数,无奈充分利用 webpack 的缓存机制。

  1. 全副加载:生成一个蕴含所有组件的包, 引入这个包相当于导入了咱们的所有组件
    首先在 packages 下新建 c -dhn-act 文件夹,这个文件夹里放的是咱们的所有组件的整合,外面有两个文件。
    index.ts
import {App} from 'vue'
import CDhnDateCountdown from '../dateCountdown'
import CDhnAvatar from '../avatar'
import CDhnCol from '../col'
import CDhnContainer from '../container'
import CDhnRow from '../row'
import CDhnText from '../text'
import CDhnTabs from '../tabs'
import CDhnSwiper from '../swiper'
import CDhnTabPane from '../tabPane'
import CDhnInfiniteScroll from '../infiniteScroll'
import CDhnSeamlessScroll from '../seamlessScroll'
export {
  CDhnDateCountdown,
  CDhnAvatar,
  CDhnCol,
  CDhnContainer,
  CDhnRow,
  CDhnText,
  CDhnTabs,
  CDhnSwiper,
  CDhnTabPane,
  CDhnInfiniteScroll,
  CDhnSeamlessScroll
}
const components = [
  CDhnDateCountdown,
  CDhnAvatar,
  CDhnCol,
  CDhnContainer,
  CDhnRow,
  CDhnText,
  CDhnTabs,
  CDhnSwiper,
  CDhnTabPane,
  CDhnSeamlessScroll
]
const plugins = [CDhnInfiniteScroll]
const install = (app: App, opt: Object): void => {
  components.forEach(component => {app.component(component.name, component)
  })
  plugins.forEach((plugin) => {app.use(plugin)
  })
}
export default {
  version: 'independent',
  install
}

留神:

  1. 整包的 ts 文件中 export{} 对组件的导出不能省略,必须要导出,不然最初 dist/lib 下生成的 index.d.ts 的类型补充申明中会短少对 组件 的导出,就会导致在 ts 我的项目中用的时候,推导不出你都导出了哪些货色,咱们在 package.json 的 typings 中指定了类型申明文件是 lib/index.d.ts.
  2. babel-plugin-import 解决了模块 js 门路的导入,然而 ts 的类型推导 导出文件的推导 还是按原始写的这个门路来推导的,所以咱们的 index.d.ts 中 必须还是要有对应的组件 类型导出的,不然就会导致在 ts 我的项目中,ts 找不到导出的组件导致 编译失败。

还有 package.json 这个文件通过一些解决后会被 copy 到 咱们的 dist 下

{
  "name": "c-dhn-act",
  "version": "1.0.16",
  "description": "c-dhn-act component",
  "author": "peng.luo@asiainnovations.com>",
  "main": "lib/index.cjs.js",
  "module": "lib/index.esm.js",
  "style": "lib/theme-chalk/index.css",
  "typings": "lib/index.d.ts",
  "keywords": [],
  "license": "MIT"
}

而后是咱们的 rollup 全量包的配置

import {plugins,external} from './rollup.comon'
import path from 'path'
import typescript from 'rollup-plugin-typescript2'
const {noElPrefixFile} = require('./common')
const paths = function(id){if ((noElPrefixFile.test(id))) {let index = id.search(noElPrefixFile)
    return `./${id.slice(index)}`
  }
}
module.exports = [
    {input: path.resolve(__dirname, '../packages/c-dhn-act/index.ts'),
        output: [
          {
            exports: 'auto', // 默认导出
            file: 'dist/lib/index.esm.js',
            format: 'esm',
            paths
          },
          {
            exports: 'named', // 默认导出
            file: 'dist/lib/index.cjs.js',
            format: 'cjs',
            paths
          }
        ],
        plugins: [
     
          ...plugins,
          typescript({
            tsconfigOverride: {
              'include': [
                'packages/**/*',
                'typings/vue-shim.d.ts',
              ],
              'exclude': [
                'node_modules',
                'packages/**/__tests__/*',
                'packages/**/stories/*'
              ],
            },
            abortOnError: false,
           
          }),
        ],
        external
    } 
]

而后这里须要对 utils 的导入做下解决,因为本质这个整合就是把对应组件的打包后果拿到了这个文件中(不同的组件引入雷同的包,rollup 会给咱们解决不会反复导入),而咱们又配置疏忽 utils 下工具函数,所以 rollup 只给解决了门路,而不会把内容打进来,然而 因为是间接拿的组件的打包后果,基于它的目录解决的,门路给解决的略微有点问题,所以配置了 path 咱们转换了一下(这里不应用门路别名是因为要配置三份,ts 的,rollup,storybook,咱们这个库门路绝对简略,所以转换的时候解决一下就能够了)。

这里能够关注一下 ts 的类型申明,是在 all.js 全量配置中生成的,不是独自一个一个生成的。
在这里设置下要编译哪些文件生成补充类型申明 include,咱们把 packages 下的所有包都生成类型申明,vue-shim.d.ts 里放的是.vue 模块的 类型申明,不然打包过程中 不意识 vue 文件会报错。
这里输入类型申明时输入的文件夹 会和 packages 工作区里的对应,所以咱们下面在打但个包的时候 rollup.tiny.js 里文件夹的门路和这个是对应的(因为都是通过 plop 创立的),这样就会把类型的补充申明和 咱们后面输入到 dist 中的单个包放在一块。
然而这会输入的全局的.d.ts 补充申明在 c -dhn-act 里,而且门路也有问题 后续须要咱们再解决下。

打包 utils

后面的打包操作,没有打包 utils 里的工具函数。
咱们的工具函数都在 packages 工具区的 utils 下,这外面可能会用到一些 es 的新语法,所以它上面的办法最初生产时是须要编译一下的,咱们后面 ts 和 jest 局部曾经把 babel 配置贴出来了。这里就不反复贴配置了。
而后就是对应 package.json 里的打包命令

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

应用 extensions 标识下扩展名 ts 而后指定下输入目录。

名称批改

buildProject 下新建 gen-type.js 文件,
该文件次要作用

  1. global.d.ts 全局的 ts 类型定义 全局的 ts 补充类型 移动到 dist 下 这里放的是.vue 模块的补充类型申明 挪动过来是为了避免 其他人 ts 应用的时候 不辨认.vue 模块
  2. 解决单个包的文件夹的名字 以及 c -dhn-act 中的全副的类型申明
const fs = require('fs')
const path = require('path')
const pkg = require('../dist/package.json')
const {noElPrefixFile} = require('./common')
const outsideImport = /import .* from '..\/(.*)/g
// global.d.ts  全局的 ts 类型定义
fs.copyFileSync(path.resolve(__dirname, '../typings/vue-shim.d.ts'),
    path.resolve(__dirname, '../dist/lib/c-dhn-act.d.ts'),
)

// 设置一下版本号, 不通过 c -dhn-act 的 index.ts 里导入 json 写入了  因为它是整体导出导入  所以会有一些其余冗余信息 不是 js 模块 无奈摇树摇掉所以在这里写入版本
const getIndexUrl = url =>  path.resolve(__dirname, '../dist/lib', url)
const updataIndexContent = (indexUrl,content) => fs.writeFileSync(getIndexUrl(indexUrl), content.replace('independent',pkg.version))

['index.esm.js','index.cjs.js'].map(fileName => ({
  fileName,
  content:fs.readFileSync(getIndexUrl(fileName)).toString()})).reduce((callback,item)=>{callback(item.fileName,item.content)
  return callback;
},updataIndexContent)


// component 这个办法次要是 针对打包之后 包做重命名解决 以及解决 typings
const libDirPath = path.resolve(__dirname, '../dist/lib')
fs.readdirSync(libDirPath).forEach(comp => { // 获取所有文件的名称
  if (!noElPrefixFile.test(comp)) { // 如果不是非凡的文件夹,正则比文件信息查问快 在后面
    if (fs.lstatSync(path.resolve(libDirPath, comp)).isDirectory()) { // 是文件夹
        if(comp === 'c-dhn-act'){ // 如果是咱们的整包  外面放的是.d.ts  补充类型申明
            fs.renameSync(
                // 把类型补充申明文件 剪切进去 和 package.json 指定的 typings 对应
                path.resolve(__dirname, '../dist/lib', comp, 'index.d.ts'),
                path.resolve(__dirname, '../dist/lib/index.d.ts'),
            ) 
            fs.rmdirSync(path.resolve(__dirname, '../dist/lib/c-dhn-act'), {recursive: true})
            // 挪动实现 原来的文件就没用了删除掉
              
            // re-import 移过去之后 文件外面援用门路不对了 须要调整一下 原来引入的是 button  而咱们最初输入包名是 c-dhn-button 所以要修改一下
            const imp = fs.readFileSync(path.resolve(__dirname, '../dist/lib', 'index.d.ts')).toString()
            if(outsideImport.test(imp)) {const newImp = imp.replace(outsideImport, (i, c) => {
                  // i 匹配到的字符串 import CDhnInput from '../input'
                  // c 正则中子规定的匹配 inout
                  return i.replace(`../${c}`, `./c-dhn-${c.replace(/([A-Z])/g,"-$1").toLowerCase()}`) // 修改引入包名
                })
               
                fs.writeFileSync(path.resolve(__dirname, '../dist/lib', 'index.d.ts'), newImp)
            }
            return;
        }
        // 给咱们的包改下名 不便后续的按需加载引入  
        const newCompName = `c-dhn-${comp.replace(/([A-Z])/g,"-$1").toLowerCase()}`
        fs.renameSync(path.resolve(libDirPath, comp),
          path.resolve(libDirPath, newCompName)
        ) 
    }
  }
})

批改合成最初 dist 中的 package.json

dist 文件夹里放的就是咱们最终公布的包,它外面的 package.json 须要咱们批改一下。

新建 gen-v.js

const inquirer = require('inquirer')
const cp = require('child_process')
const path = require('path')
const fs = require('fs')

const jsonFormat = require('json-format') // 丑化並轉換 js
const promptList = [
  {
    type: 'list',
    message: '抉择降级版本:',
    name: 'version',
    default: 'patch', // 默认值
    choices: ['beta', 'patch', 'minor', 'major']
  }
]
const updataPkg = function () {const pkg = require('../packages/c-dhn-act/package.json')
  const {dependencies, peerDependencies} = require('../package.json')
  fs.writeFileSync(path.resolve(__dirname, '../dist', 'package.json'),
    jsonFormat({...pkg, dependencies, peerDependencies})
  )
}
inquirer.prompt(promptList).then(answers => {
  let pubVersion = answers.version
  if (pubVersion === 'beta') {const { version} = require('../packages/c-dhn-act/package.json')
    let index = version.indexOf('beta')
    if (index != -1) {const vArr = version.split('.')
      vArr[vArr.length - 1] = parseInt(vArr[vArr.length - 1]) + 1
      pubVersion = vArr.join('.')
    } else {pubVersion = `${version}-beta.0`
    }
  }
  cp.exec(`npm version ${pubVersion}`,
    {cwd: path.resolve(__dirname, '../packages/c-dhn-act') },
    function (error, stdout, stderr) {if (error) {console.log(error)
      }
      updataPkg()}
  )
})

这个文件次要是用来更新版本号,并且以 packages 下的 c -dhn-act 文件夹下 package.json 文件为主,而后 ge 把我的项目根目录的 dependencies 依赖拿过去合并,生成新的 package.json 放到 dist 下。
因为咱们打包的时候把他们疏忽了,然而最初提供给他人用的时候还是须要用的,所以最初在咱们公布 npm 包的 json 上还是要写进去的,npm 包的 dependencies 依赖当在我的项目中执行 npm install 的时候会主动下载的,devDependencies 的不会。

scss 打包

scss 的生产打包咱们抉择用 gulp,packages/theme-chalk/src 放的是对应模块的 scss 文件。

咱们增加 gulpfile.js,gulp 的配置文件。

'use strict'
const {series, src, dest} = require('gulp')
// 临时不必 series  目前就一个工作
const sass = require('gulp-dart-sass')
const autoprefixer = require('gulp-autoprefixer')
const cssmin = require('gulp-cssmin')
const rename = require('gulp-rename')

const noElPrefixFile = /(index|base|display)/   // 改名 如果不是这几个加上 c -dhn

function compile(){// 编译器
    return src('./src/*.scss') // 读取所有 scss 创立可读流
    .pipe(sass.sync()) // 管道 插入处理函数 同步编译 sass 文件 
    .pipe(autoprefixer({ cascade: false})) // 不启动丑化 默认丑化属性
    .pipe(cssmin()) // 压缩代码
    .pipe(rename(function (path) {if(!noElPrefixFile.test(path.basename)) { // 如果不是这些  给加前缀
          path.basename = `c-dhn-${path.basename}`
        }
      }))
    .pipe(dest('./lib')) // 创立写入流 到管道  写入到
}


exports.build = compile

到这里能够看我最开始贴的 package.json 文件。外面的 script 命令就大都蕴含了。
scripts 列表

  1. test 用来开启 jest 单元测试
  2. storybookPre 用来查看 storyBook 打包进去的动态资源预览
  3. storybook 用来开启开发环境组件文档查看
  4. build-storybook 用来生产对应的动态资源文档,不便部署
  5. buildTiny:prod 打包按需加载包 压缩代码版本
  6. buildTiny:dev 不压缩的版本
  7. plop 生产 plop 模板
  8. clean:lib 清空 dist/lib 文件夹
  9. build:theme gulp 构建 scss 款式
  10. build:utils babel 打包工具函数
  11. buildAll:prod 打包全量包 压缩代码
  12. buildAll:dev 不压缩代码
  13. build:type 批改打包进去的文件名和外部门路,和 ts 补充类型申明的地位
  14. build:v 批改要公布的新的版本号和更新生产依赖。
  15. build:dev 残缺的组合好的打包命令(罕用的,不压缩代码)
  16. build:prod 压缩代码

这里 yarn build:dev 就是 咱们的打包不压缩的测试,不便咱们查看打包之后的内容后果是否和咱们预期相符。
Yarn build:prod 就是正式公布时 所执行的打包命令。

打包的次要思维程序是

  1. 批改版本,生成笼罩 package.json
  2. 清空文件夹
  3. packages 工作区的组件一一打包(按需加载)
  4. 打全量包(全副加载)
  5. 用 babel 编译 utils 工具函数
  6. 最初批改 dist/lib 下的文件夹名称,和.d.ts 的类型补充,和局部文件内容批改。
  7. 最初构建一下 scss 款式

最初是应用:

这样咱们配合 babel-plugin-import 这个插件。

{
  plugins: [
    [
      'import',
      {
        libraryName: 'c-dhn-act',
        customStyleName: (name) => {return `c-dhn-act/lib/theme-chalk/${name}.css`
        }
      }
    ]
  ]
}

在碰到
import {CDhnAvatar} from "c-dhn-act"
这种状况时就会被解析成

import CDhnAvatar from "c-dhn-act/lib/c-dhn-avatar";

这种模式,这样就间接从咱们打的小的组件包里去获取,从而在加载层面就造成了按需加载。
不理解 babel-plugin-import 的能够,查下这个插件的用法。

最初就是咱们的代码地址了,有趣味的能够把代码下载下来跑一跑看看。

正文完
 0