6

First of all, our ui component library is a developed version of vue. If you need to work around other versions, remove the vue-related compilers, and add other related compilers (such as react). The package construction method is generic.

The organization and release of the component library is not the same as the project, here is the idea.
First of all, we have to determine the requirements that the library we are making must meet:

  1. Support all loading
  2. Support on-demand loading
  3. Supplementary type support for ts
  4. Support both cjs and esm versions

After knowing the requirements we want to support, we must determine what the directory structure of our final package looks like, as follows:
image.png
This is a brief description of why this structure is. First, index.esm is our entire package, which contains all the ui components, and there is also an index.cjs version. When the packaging tool does not support esm, the cjs version will be used. Each version can better support different packaging tools.

The lib decentralized is our single component, which is used in combination with babel-plugin-import to do on-demand loading.
Here is a brief summary first, and a detailed explanation will be given in the subsequent implementation.

Okay, after understanding our final project structure, we are about to start building the ui library. All subsequent operation configurations are designed to ensure the robustness and versatility of the program to type out the structure of the package we are going to release. .


Design and process

Code organization Monorepo

Monorepo is a way to manage project code. It refers to managing multiple modules/packages in a project repository (repo), which is different from the common repo for each module.
E.g:

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

With this structure, you can see that the content of the first-level directory of these projects is mainly scaffolding, and the main content is in the packages directory and managed by multiple packages.

Some well-known libraries such as vue3.0 and react use this approach to manage projects.
image.png
image.png
Later, we will generate on-demand files based on the small packages in these packages.

The package management tool uses yarn, because we need to use its workspaces dependency management

If you do not use workspaces, because each package is theoretically independent, each package maintains its own dependencies. It is very likely that there are many similar dependencies between packages, and this may make the installation time Repeated installations occur, causing the already large node_modules to continue to expand (this is the "dependency explosion"...).

In order to solve this problem, we need to use the workspaces feature of yarn, which is the reason why we use yarn for dependency management.
For students who use yarn as a package manager, you can declare packages in the workspaces field in package.json, and yarn will manage packages in a monorepo manner.
For details on how to use it, you can check its official documentation
document

After we open the workspaces workspace of yarn in package.json, the current directory is called the root directory of the workspace, and the workspace is not to be released. Then, when we download dependencies, the same version in different component packages Dependencies will be downloaded to the node_modules of the workspace. If the version of the current package depends on different from others, it will be downloaded to the node_modules of the current package.

The highlight of yarn is the management of dependencies, including the interdependence of packages and the dependencies of packages on third parties. Yarn will analyze the version of dependencies using the semver convention, which makes the installation of dependencies faster and smaller footprint.

lerna

Here is a brief mention of lerna, because the current mainstream monorepo solution is the workspaces feature of Lerna and yarn, which is mainly used to manage the workflow, but it personally feels that if you need to publish all the packages in the packages at once, it will be better to use it. Convenient, we don't use it too much here.

Debugging during Storybook development

The debugging and use of component effects are introduced through Storybook. This is a visual component display platform that allows us to interactively develop and test components in an isolated development environment. Finally, it can also generate a static interface for instructions. Support many frameworks such as: vue.react, ng, React Native, etc.

jest unit test

For unit testing, we use Facebook’s jest

plop creates the same template

The structure of our package is like this, such as avatar:

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

The structure of each UI component is basically the same, so here we choose plop to generate the template uniformly. Plop is mainly used to create small tools for specific file types in the project. It is similar to the sub generator in Yeoman, and is generally not used independently. . Generally, Plop is integrated into the project to automatically create project files of the same type.

Rollup for packaging

The last is the build operation. Here we do not use webpack for packaging, but Rollup.
Webpack is more suitable for project engineering use, because many static resources in the project need to be processed, or the built project needs to introduce a lot of CommonJS module dependencies, so that although it also has the tree-shaking function (additional configuration), it has to be processed Convert other files, so the package it outputs will still have some redundant code.
Rollup also supports tree-shaking, and it is mainly used for js packaging. Its packaging result is smaller than webpack, and it will be more suitable for developing class libraries.

Let's talk about the construction process:

First I post a dependency of my final complete version, as follows:

{
  "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/*"
  ]
}

You can see that my storyBook is version 6.2.9. The latest version is not used here because the document mode cannot be opened last. I don't know if the problem is solved.

The project can be initialized with the scaffolding of StoryBook, and we will add things to it later.
We are using vue3.0 version for initialization, here you can follow the manual to initialize
storybook official website vue initialization manual

The official website also provides the initialization of other framework projects.
After the initialization is complete, we find the .storyBook folder, and we need to modify the following content:
Main.js is changed to this, as follows:

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;
  },
}

Here the stories configuration item, the configuration path puts the instructions and components to be presented on the interface, and the matched mdx puts its usage guidelines, mdx is a combination of markdowm and jsx.

Some of its plug-ins are put in addons, addon-essentials is a collection of plug-ins ( collection ), which contains a series of plug-ins to ensure that we can use it out of the box, addon-links is used to set up link plug-ins.

webpackFinal is some extension for webpack. Here we use dart-sass instead of node-sass, otherwise some math functions will not be used. For example, only dart-sass can use math function.

Then we have the syntax under packages/avatar/stories/avatar.stories.mdx, you can refer to the official website mdx syntax

workspaces and private have been configured in packsge.json,

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

If you don’t understand the role of workspaces, you can use it on Baidu.

Then there is the integration of ts and jest

First of all, let's integrate ts, first download the dependencies:
There are two main packages:

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

Then modify 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"
    ],

}
   

Then integrate jest

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

It is recommended that the version of the dependency package downloaded is subject to the lock of my project, because this is a stable version that I have verified, and upgrading to a new version may cause incompatibility.

Here -D -W is installed to the root of the workspace and means development dependency. Here jest is the official recommendation of the unit test library provided by Facebook, and @vue/test-utils is the official test utility library of Vue.js , Combined with jest to use the least configuration, processing single-file components vue-jest, babel-jest to downgrade the test code, jest-css-modules to ignore the test css file.
Then we create a new jest.config.js unit test configuration file in the root directory:

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"
  }
}

Then configure babel.config.js. We tested the use of downgrade processing. We will use the babel environment variable utils to use the corresponding configuration to convert some tool functions in packages/utils when we play the production package.
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: {
           
      //       },
      //     },
      //   ],
      // ],
    },
  },
}

Then we modify the script command "test": "jest" in package.json,
How to write Jest unit tests can be viewed in official documents according to your needs.

Plop generates component templates

At the beginning, we said that the structure of each of our packages is the same, and then it would be too troublesome to manually create the structure every time a component package is generated.

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

Our basic structure is like this, and then we choose plop to generate a template. We mentioned earlier that plop is mainly used to create small tools for specific file types in the project. We install it into the project yarn add plop -D -W

Then create its configuration file 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'
        }
      ]
    })
  }

Here we use the command line to ask for interaction to generate components, and then we will create new folders and templates based on our configuration files.
image.png
The structure of the template is like this.
Then we look at what the corresponding template looks like, as follows:
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}}

Then we add a script command "plop": "plop" in package.json

After execution, the corresponding files can be produced. For details, download the project and have a look.

Here we test, the storyBook of the development environment and the plop of the production file are finished.
Now it's time to see how to print the package for the production environment.

rollup build packaging

First create a new buildProject folder, some of our command scripts will be placed here.
There are two types of packaging here, load-on-demand and full-package. These two methods have some configurations that are the same. Here we write a public configuration file 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};

Here we exclude the tool functions under utils (this is because we need to use babel for syntax conversion) and the vue library, as well as all the production packages we use, to ensure the small size of the package.
common.js currently only uses utils

module.exports = {
    noElPrefixFile: /(utils|directives|hooks)/,
}
  
  1. Load on demand: Each component in the workspace packages generates the corresponding js, which is convenient for later introduction with the babel plug-in on demand
    rollup.tiny.js This file is for the configuration of workspace components
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
  }
})

In this case, when packaging is started through rollup, our corresponding files will be generated in dist in turn. If the package name contains @c-dhn-act, it will be considered as our small package (note: the name of the name folder here is the original workspace The name of the package.json package, such as avatar, is also used when we generate the .d.ts type supplement, but this does not match the name we will produce at the end, and a rename operation will be performed after the subsequent packaging)

Notice:

The utils tool function is ignored here. This is because when we provide it to other people later, if we don’t ignore utils, it will be entered. Then if other people refer to different components, but the same tools are referenced in different components Function (the function is packaged in the component file).

Take webpack as an example. This will have a problem. If used in webpack, webpack will have function scope isolation for modules, so even if the tool function name is the same, it will not be overwritten, which will cause webpack to print out the final As a result, the package becomes larger.

After the utils tool functions are ignored, they are not typed into the file, but are used by import. This way, when webpack is used, its module caching mechanism can be fully utilized. First, the package size is reduced. Secondly, because the caching mechanism is used, the loading speed will also be improved.

webpack pseudo code
I randomly handwritten a piece of pseudo-code after webpack is packaged here for easy understanding. Let’s take a look.
When utils is a separate module, it looks like this

(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函数处理,假设是特别长的函数处理
        } 
    })
})

The methods and components in the second utils are combined, which will be like this when used in 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组件'
    }),

})

We can compare, so that we can verify, first look at the second webpack pseudo-code, we look at the parameters passed in, the key of the object is the path of the file, and the value is a function (the module before it is packaged, the content of the function is ours Module code), here is the function scope for isolation. First of all, the definition is repeated definition, and if there is isolation, it cannot be reused. Secondly, because such a pile of repeated redundant code will also cause the final package to become larger (our component Libraries). Finally, the isArray function needs to be redefined every time a module is loaded, which cannot make full use of webpack's caching mechanism.

  1. Load all: Generate a package containing all components, importing this package is equivalent to importing all our components
    First, create a new c-dhn-act folder under packages. This folder contains the integration of all our components. There are two files in it.
    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
}

Notice:

  1. Export{} in the ts file of the whole package cannot omit the export of the component. It must be exported. Otherwise, the type supplement declaration of index.d.ts generated under dist/lib will lack the export of the component, which will lead to the loss of the export of the component in the ts When used in the project, it is impossible to deduce what you have exported. We specified the type declaration file in the typings of package.json as lib/index.d.ts.
  2. babel-plugin-import handles the import of the module js path, but the derivation of the ts type derivation export file is still deduced according to the path originally written, so our index.d.ts must still have the corresponding component type Exported, otherwise it will lead to in the ts project, ts can not find the exported components, resulting in compilation failure.

And the package.json file will be copied to our dist after some processing

{
  "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"
}

Then there is the configuration of our rollup full package

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
    } 
]

Then we need to deal with the import of utils, because in essence, this integration is to get the packaging results of the corresponding components into this file (different components are imported into the same package, and rollup will handle it for us without repeating imports), and we The configuration ignores the tool functions under utils, so rollup only processes the path, but does not enter the content, but because it is the packaging result of the component that is directly taken, it is processed based on its directory, and the path is slightly problematic, so After configuring the path, we converted a bit (the path alias is not used here because we have to configure three copies, ts, rollup, and storybook. Our library path is relatively simple, so just deal with it when converting).

Here you can pay attention to the type declaration of ts, which is generated in the full configuration of all.js, not one by one.
Set here to compile which files to generate supplementary type declaration include, we generate type declarations for all packages under packages, and vue-shim.d.ts puts the type declaration of the .vue module, otherwise it will not be recognized during the packaging process The vue file will report an error.
Here, the output folder when outputting the type declaration will correspond to the package workspace, so when we open a package above, the path of the folder in rollup.tiny.js corresponds to this (because they are all created by plop ), this will put the supplementary declaration of the type together with the single package we output to dist earlier.
But this will output the global .d.ts supplementary statement in c-dhn-act, and there are problems with the path and we need to deal with it later.

Package utils

The previous packaging operation does not package the tool functions in utils.
Our tool functions are all under utils in the packages tool area, which may use some new es syntax, so the following methods need to be compiled at the end of production. We have already posted the babel configuration in the ts and jest parts above. Out. The configuration will not be repeated here.
Then it corresponds to the packaging command in package.json

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

Use extensions to identify the extension ts and then specify the output directory.

Name modification

Create a new gen-type.js file under buildProject,
The main function of this document

  1. global.d.ts The global ts type definition moves the global ts supplementary type to dist. The supplementary type declaration of the .vue module is moved in the past to prevent other people from not recognizing the .vue module when ts is used.
  2. Process the name of the folder of a single package and all the type declarations in 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)
        ) 
    }
  }
})

Modify package.json in the final dist of synthesis

The dist folder puts the package we finally released, and the package.json in it needs us to modify it.

New 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()
    }
  )
})

This file is mainly used to update the version number, and is mainly based on the package.json file in the c-dhn-act folder under packages, and then ge takes the dependencies of the project root directory and merges it to generate a new package.json and put it Go to dist.
Because we ignored them when we packaged them, but we still need to use them when we provide them to others, so we still need to write them in the json of the npm package that we publish. The dependencies of the npm package depend on when npm is executed in the project. It will be downloaded automatically when install, but not for devDependencies.

scss packaging

For scss production packaging, we choose to use gulp. Packages/theme-chalk/src contains the scss file of the corresponding module.
image.png
We add gulpfile.js, the configuration file of 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

Here you can see the package.json file I posted at the beginning. Most of the script commands inside are included.
list of scripts

  1. test is used to start jest unit testing
  2. storybookPre is used to view the static resource preview packaged by storyBook
  3. Storybook is used to open the development environment component document view
  4. build-storybook is used to produce corresponding static resource documents for easy deployment
  5. buildTiny: prod package on-demand load package compressed code version
  6. buildTiny:dev uncompressed version
  7. plop production plop template
  8. clean:lib empty the dist/lib folder
  9. build:theme gulp build scss style
  10. build:utils babel packaging tool function
  11. buildAll:prod packs the full package compression code
  12. buildAll:dev does not compress code
  13. build:type modify the packaged file name and internal path, and the location of the ts supplementary type declaration
  14. build:v Modify the new version number to be released and update the production dependencies.
  15. build:dev Complete combined package command (commonly used, code is not compressed)
  16. build:prod compressed code

Here yarn build:dev is our packaged and uncompressed test, so that we can check whether the packaged content results are in line with our expectations.
Yarn build:prod is the packaging command executed during the official release.

The main idea of packaging is

  1. Modify the version, generate and override package.json
  2. Empty folder
  3. Package the components of the packages workspace one by one (load on demand)
  4. Hit the full package (load all)
  5. Compile utils tool functions with babel
  6. Finally, modify the folder name under dist/lib, add the type of .d.ts, and modify part of the file content.
  7. Finally, build the scss style

Finally, use:

In this way, we cooperate with the babel-plugin-import plug-in.

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

Bumping into
import { CDhnAvatar } from "c-dhn-act"
In this case, it will be parsed as

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

In this form, it is directly obtained from the small component package we have typed, thus forming on-demand loading at the loading level.
If you don’t understand babel-plugin-import, check the usage of this plugin.

The last is our code address. If you are interested, you can send me a private message, and I will give the warehouse permission.


Charon
57 声望16 粉丝

世界核平