通用npm包(组件库)打包构建

一、背景

现在的前端开发,绕不开使用一些第三方的NPM包。同时,很多公司不论大小都有了自己的NPM仓库,然后发布一些只有自己使用的私有NPM包。我们在发包的时候,包的代码怎么构建才能体积最小,同时做到新能最好就成了一个非常重要的问题。

在一些发包实现中,无论是UI包还是纯粹的TypeScript Utils包,你当然都可以不打包而直接将代码放到npm包的dist目录中,然后以来业务工程进行构建。但是这显然是有问题的,因为这些NPM包都是比较稳定的代码,不像业务代码那样需要进行频繁的修改和调试了,直接暴露源码给业务使用,会使这些包在不同的项目中多次重复构建,无疑会拉长业务开发时热更新和生产构建时间。

二、如何解决

就像背景中提到的,一般我们的NPM包可以分为两类,分别是:

  • 纯TypeScript(javascript)类型,只包含逻辑代码
  • UI类,包含VUE相关代码的组件,根据vue的2和3的版本不同,又可以细分分别适用于vue2和vue3的两个小类

在分为以上以上两类的同时,还有一种情况需要考虑下,就是有些C端的产品,可能会进行服务端渲染优化。因此我们需要考虑到这些包可能会在Node环境中运行,甚至一些utils包就是直接运行在Node环境中的。所以这个时候我们就需要一份已与CommonJS规范的导出。另外,我们有些工具包还要考虑是否开源出去的情况,如果考虑开源的话可能还需要一份基于UMD规范的导出。

所以,根据以上情况,我们需要多份打包脚本配置。

使用TSC打包Typescript为ESModule、CommonJS及UMD格式

package.json

{
  // 下列内容只包含了打包构建用到的配置,其他字段用到的请自行补充
  "main": "dist/main/index.js",
  "typings": "dist/types/index.d.ts",
  "module": "dist/module/index.js",
  "unpkg": "dist/umd/index.js",
  "exports": {
    ".": {
      "types": "./dist/main/index.d.ts",
      "import": "./dist/module/index.js",
      "require": "./dist/main/index.js",
      "browser": "./dist/umd/index.js"
    }
  },
  "scripts": {
    "prepublish": "pnpm build",
    "build": "run-p build:*",
    "build:main": "tsc -p ./pack/tsconfig.json",
    "build:module": "tsc -p ./pack/tsconfig.module.json",
    "build:umd": "tsc -p ./pack/tsconfig.umd.json",
    "watch:module": "tsc -p ./pack/tsconfig.module.json -w",
    "watch:main": "tsc -p ./pack/tsconfig.json -w"
  },
  "files": [
    "dist",
    "src"
  ],
  "devDependencies": {
    "@types/node":"^18.13.0",
    "typescript": "^4.9.5",
    "npm-run-all": "^4.1.5"
  }
}

1.基本配置&生成基于CommonJS规范的导出

pack/tsconfig.json

{
    "compilerOptions": {
      "target": "ES6", // 编译目标版本
      "outDir": "../dist/main",  // 输出目录
      "rootDir": "../src", // 根目录
      "moduleResolution": "nodenext", // 如何处理模块
      "module": "commonjs", // 生成的模块系统代码
      "declaration": true, // 生成相应的 .d.ts文件
      "declarationDir":"../dist/types", // 生成相应的 .d.ts文件的存放目录
      "inlineSourceMap": true, // 生成单个的sourcemaps文件
      "esModuleInterop": true, // https://www.typescriptlang.org/tsconfig#esModuleInterop
      "resolveJsonModule": true, // 允许导入json文件
      "strict": false,  // 不使用严格模式
      "noUnusedLocals": true, // 若有未使用的局部变量则抛错
      "noUnusedParameters": true,  // 若有未使用的参数则抛错
      "noImplicitReturns": true, // 不是函数的所有返回路径都有返回值时报错
      "noFallthroughCasesInSwitch": true, // 报告switch语句的fallthrough错误。(即,不允许switch的case语句贯穿)
      "traceResolution": false, // 不生成模块解析日志信息
      "listEmittedFiles": false, // 不打印出编译后生成文件的名字。
      "listFiles": false, // 编译过程中不打印文件名。
      "pretty": true, // 给错误和消息设置样式,使用颜色和上下文
      "lib": ["dom"], // 编译过程中需要引入的库文件的列表
      "types": ["node"], // 要包含的类型声明文件名列表
      "typeRoots": ["node_modules/@types", "src/types"], // 要包含的类型声明文件路径列表
    },
    "include": ["../src/**/*.ts"],
    "exclude": ["**/node_modules/**"],
    "compileOnSave": true
  }

2.生成基于ESModule的导出文件

pack/tsconfig.module.json

{
  "extends": "./tsconfig",
  "compilerOptions": {
    "target": "ES2015",
    "outDir": "../dist/module",
    "module": "ES2015",
    "moduleResolution": "nodenext",
    "resolveJsonModule": false, // 允许导入json文件
  },
}

3.生成基于UMD的导出文件

pack/tsconfig.umd.json

{
    "extends": "./tsconfig",
    "compilerOptions": {
      "target": "es5",
      "outDir": "../dist/umd",
      "module": "UMD",
      "moduleResolution": "nodenext",
      "resolveJsonModule": false, // 允许导入json文件
      "lib": ["es5", "dom"], // 编译过程中需要引入的库文件的列表
    }
  }

基于Webpack打包通用UI库(Base Vue2)

工程目录结构

package.json

{
  "name": "private pkg",
  "version": "0.0.1-beta.0",
  "main": "dist/main/index.js",
  "typings": "dist/types/index.d.ts",
  "module": "dist/module/index.js",
  "unpkg": "dist/umd/index.js",
  "exports": {
    ".": {
      "types": "./dist/types/index.d.ts",
      "import": "./dist/module/index.js",
      "require": "./dist/main/index.js",
      "browser": "./dist/umd/index.js"
    }
  },
  "scripts": {
    "build": "run-p build:*",
    "build:esm": "webpack --config pack/webpack.esmodule.conf.js",
    "build:common": "webpack --config pack/webpack.common.conf.js",
    "build:types": "vue-tsc --skipLibCheck --declaration --emitDeclarationOnly --declarationDir dist/types"
  },
  "files": [
    "dist"
  ],
  "devDependencies": {
    "filter": "^0.1.1",
    "unplugin-vue-components": "^0.24.0",
    "vue-template-compiler": "2.7.14",
    "vue-loader": "^15.10.0",
    "vue":"^2.7.0",
    "vue-tsc": "^1.2.0",
    "npm-run-all": "^4.1.5",
	"webpack-node-externals": "^3.0.0",
	"progress-bar-webpack-plugin": "^2.1.0",
	"esbuild-loader": "^4.0.0",
	"webpack-merge": "^5.0.0",
	"css-loader": "^6.8.1",
	"postcss-loader": "^7.3.3",
    "sass-loader": "^13.3.2",
    "style-loader": "^3.3.3",
  }
}

tsconfig.json

{
  "compilerOptions": {
    "target": "ES6",
    "module": "ES6",
    "moduleResolution": "nodenext",
    "strict": false,
    "sourceMap": true,
    "importHelpers": true,
    "experimentalDecorators": true,
    "noImplicitAny": false,
    "noImplicitThis": false,
    "esModuleInterop": true,
    "jsx": "preserve",
    "jsxFactory": "VueTsxSupport",
    "allowJs": true,
    "declaration": true,
    "declarationDir": "./dist/types",
    "skipLibCheck": true,
    "types": [
      "webpack-env",
      "vue"
    ]
  },
  "include": [
    "shims-vue.d.ts",
    "./index.ts"
  ],
  "exclude": [
    "node_modules/**/*"
  ],
  "vueCompilerOptions": {
    "target": 3
  }
}

webpack.base.conf.js

const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const {VueLoaderPlugin} = require('vue-loader');

const webpackConf = {
    mode: 'production',
    entry: {
        app: ['./index.ts']
    },
    module: {
        rules: [
            {
                test: /\.vue$/,
                loader: 'vue-loader',
                options: {
                    compilerOptions: {
                        preserveWhitespace: false
                    }
                }
            },
            {
                test: /\.(j|t)sx?$/,
                exclude: /(node_modules)/,
                use: {
                    loader: 'esbuild-loader',
                    options: {
                        loader: 'tsx',
                        target: 'esnext' // Syntax to compile to (see options below for possible values)
                    }
                }
            }
        ]
    },
    optimization: {
        minimize: true
    },
    resolve: {
        extensions: ['.js', '.vue', '.json', '.ts', '.tsx', '.scss'],
        modules: ['node_modules'],
        alias: {
            '@': './src'
        }
    },
    stats: 'normal',
    plugins: [
        new VueLoaderPlugin(),
        new ProgressBarPlugin({
            complete: 'O',
            incomplete: '='
        })
    ]
};
module.exports = webpackConf;

webpack.common.conf.js

const baseWebpackConf = require('./webpack.base.conf.js');
const {merge} = require('webpack-merge');
const nodeExternals = require('webpack-node-externals');
const path = require('path');

const webpackConf = {
    output: {
        path: path.resolve(process.cwd(), './dist/main'),
        filename: `index.js`,
        chunkFilename: '[id].js',
        library: {
            type: 'commonjs2'
        }
    },
    module: {
        rules: [
            {
                test: /\.(sa|sc|c)ss$/i,
                use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
            }
        ]
    },
    externals: [nodeExternals()]
};
module.exports = merge(baseWebpackConf, webpackConf);

webpack.esmodule.conf.js

const baseWebpackConf = require('./webpack.base.conf.js');
const {merge} = require('webpack-merge');
const path = require('path');

const webpackConf = {
    output: {
        path: path.resolve(process.cwd(), './dist/module'),
        filename: `index.js`,
        chunkFilename: '[id].js',
        library: {
            type: 'module'
        }
    },
    experiments: {
        outputModule: true
    },
    module: {
        rules: [
            {
                test: /\.(sa|sc|c)ss$/i,
                use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
            }
        ]
    },
    externals: [
        {
            vue: 'vue',
            'vue-property-decorator': 'vue-property-decorator',
		  	//这里很重要,冗余的其他库都需要在这里配置external掉
        }
    ]
};
module.exports = merge(baseWebpackConf, webpackConf);

webpack.umd.conf.js

const baseWebpackConf = require('./webpack.base.conf.js');
const {merge} = require('webpack-merge');

const webpackConf = {
    output: {
        path: path.resolve(process.cwd(), './dist/module'),
        filename: `${pkgName}.umd.js`,
        chunkFilename: '[id].js',
        library: {
            name: `${pkgName}`,
            type: 'umd',
            export: 'default'
        }
    },
    externals: [
        {
            vue: 'vue'
        }
    ]
};
module.exports = merge(baseWebpackConf, webpackConf);

基于Webpack打包通用UI库(Base Vue3)

工程目录结构同上

package.json

{
  "name": "",
  "version": "0.0.1",
  "main": "dist/main/index.js",
  "typings": "dist/types/index.d.ts",
  "module": "dist/module/index.js",
  "unpkg": "dist/umd/index.js",
  "exports": {
    ".": {
      "types": "./dist/types/index.d.ts",
      "import": "./dist/module/index.js",
      "require": "./dist/main/index.js",
      "browser": "./dist/umd/index.js"
    }
  },
  "scripts": {
    "build": "run-p build:*",
    "build:esm": "webpack --config pack/webpack.esmodule.conf.js",
    "build:common": "webpack --config pack/webpack.common.conf.js",
    "build:types": "vue-tsc --skipLibCheck --declaration --emitDeclarationOnly --declarationDir dist/types"
  },
  "files": [
    "dist"
  ],
  "devDependencies": {
    "@vue/compiler-sfc": "^3.2.47",
    "filter": "^0.1.1",
    "unplugin-vue-components": "^0.24.0",
    "vue": "^3.2.47",
    "vue-loader": "^17.0.0",
    "vue-tsc": "^1.2.0",
	"npm-run-all": "^4.1.5",
	"webpack-node-externals": "^3.0.0",
	"progress-bar-webpack-plugin": "^2.1.0",
	"esbuild-loader": "^4.0.0",
	"webpack-merge": "^5.0.0",
	"css-loader": "^6.8.1",
	"postcss-loader": "^7.3.3",
    "sass-loader": "^13.3.2",
    "style-loader": "^3.3.3",
  }
}

tsconfig.json

{
  "compilerOptions": {
    "target": "ES6",
    "module": "ES6",
    "moduleResolution": "nodenext",
    "strict": false,
    "sourceMap": true,
    "importHelpers": true,
    "experimentalDecorators": true,
    "noImplicitAny": false,
    "noImplicitThis": false,
    "esModuleInterop": true,
    "jsx": "preserve",
    "jsxFactory": "VueTsxSupport",
    "allowJs": true,
    "declaration": true,
    "declarationDir": "./dist/types",
    "skipLibCheck": true,
    // "baseUrl": ".",
    // "outDir": "./dist/types",
    "types": [
      "webpack-env",
      "vue"
    ]
  },
  "include": [
    "shims-vue.d.ts",
    "./index.ts"
  ],
  "exclude": [
    "node_modules/**/*"
  ],
  "vueCompilerOptions": {
    "target": 3
  }
}

webpack.base.conf.js

const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const {VueLoaderPlugin} = require('vue-loader');

const webpackConf = {
    mode: 'production',
    entry: {
        app: ['./index.ts']
    },
    module: {
        rules: [
            {
                test: /\.vue$/,
                loader: 'vue-loader',
                options: {
                    compilerOptions: {
                        preserveWhitespace: false
                    }
                }
            },
            {
                test: /\.(j|t)sx?$/,
                exclude: /(node_modules)/,
                use: {
                    loader: 'esbuild-loader',
                    options: {
                        loader: 'tsx',
                        target: 'esnext' // Syntax to compile to (see options below for possible values)
                    }
                }
            }
        ]
    },
    optimization: {
        minimize: false
    },
    resolve: {
        extensions: ['.js', '.vue', '.json', '.ts', '.tsx', '.scss'],
        modules: ['node_modules'],
        alias: {
            '@': './src'
        }
    },
    stats: 'normal',
    plugins: [
        new VueLoaderPlugin(),
        new ProgressBarPlugin({
            complete: 'O',
            incomplete: '='
        })
    ]
};
module.exports = webpackConf;

webpack.common.conf.js

const baseWebpackConf = require('./webpack.base.conf.js');
const {merge} = require('webpack-merge');
const nodeExternals = require('webpack-node-externals');
const path = require('path');

const webpackConf = {
    output: {
        path: path.resolve(process.cwd(), './dist/main'),
        filename: `index.js`,
        chunkFilename: '[id].js',
        library: {
            type: 'commonjs2'
        }
    },
    module: {
        rules: [
            {
                test: /\.(sa|sc|c)ss$/i,
                use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
            }
        ]
    },
    externals: [nodeExternals()]
};
module.exports = merge(baseWebpackConf, webpackConf);

webpack.module.conf.js

const baseWebpackConf = require('./webpack.base.conf.js');
const {merge} = require('webpack-merge');
const path = require('path');

const webpackConf = {
    output: {
        path: path.resolve(process.cwd(), './dist/module'),
        filename: `index.js`,
        chunkFilename: '[id].js',
        library: {
            type: 'module'
        }
    },
    experiments: {
        outputModule: true
    },
    module: {
        rules: [
            {
                test: /\.(sa|sc|c)ss$/i,
                use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
            }
        ]
    },
    externals: [
        {
            vue: 'vue' 
		  // 这里很重要,冗余的其他库都需要在这里配置external掉
        }
    ]
};
module.exports = merge(baseWebpackConf, webpackConf);

webpack.umd.conf.js

const baseWebpackConf = require('./webpack.base.conf.js');
const {merge} = require('webpack-merge');

const webpackConf = {
  output:{
    path: path.resolve(process.cwd(), './dist/module'),
    filename: `${pkgName}.umd.js`,
    chunkFilename: '[id].js',
    library:{
      name:`${pkgName}`,
      type:'umd',
      export: 'default',
    }
  },
  externals:[{
    vue: 'vue'
  }]
}
module.exports = merge(baseWebpackConf,webpackConf)

文章看到这里了,如果你觉得对你的打包工作有启发的话,请点个赞吧

#webpack##架构开发##npm##组件库#
全部评论

相关推荐

10-07 23:57
已编辑
电子科技大学 Java
八街九陌:博士?客户端?开发?啊?
点赞 评论 收藏
分享
牛客339922477号:都不用reverse,直接-1。一行。啥送分题
点赞 评论 收藏
分享
1 1 评论
分享
牛客网
牛客企业服务