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

首先咱们咱们的这个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的能够,查下这个插件的用法。

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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理