webpack5構(gòu)建一個(gè)通用的組件庫

為組內(nèi)實(shí)現(xiàn)一個(gè)私有通用的組件庫,解放重復(fù)勞動(dòng)力,提高效率,讓你的代碼被更多小伙伴使用。

本文是筆者總結(jié)的一篇關(guān)于構(gòu)建組件庫的一些經(jīng)驗(yàn)和思考,希望在項(xiàng)目中有所幫助。

正文開始...

初始化一個(gè)基礎(chǔ)項(xiàng)目
生成基礎(chǔ)package.json

npm init -y
安裝項(xiàng)目指定需要的插件

npm i webpack webpack-cli html-webpack-plugin @babel/core @babel/cli @babel/preset-env webpack-dev-server --save-dev
webpack5官方支持ts編寫配置環(huán)境,不過需要安裝幾個(gè)插件支持,參考官網(wǎng)configuration-languages[1],我們今天使用ts配置webpack。

配置文件ts環(huán)境支持
需要安裝以下幾個(gè)插件

npm install --save-dev typescript ts-node @types/node @types/webpack
并且需要修改tsconfig.json

{
  "compilerOptions": {
     ...
    "module": "commonjs",
    "target": "es5",
    ...
  }
}
在.eslintrc.js中的相關(guān)配置,配置env.node:true,主要是為了支持require方式,具體參考如下,關(guān)于eslint配置可以參考以前寫的文章。

module.exports = {
    "env": {
        "browser": true,
        "es2021": true,
        "node": true
    },
    "extends": [
        "eslint:recommended",
        "plugin:@typescript-eslint/recommended"
    ],
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
        "ecmaVersion": "latest",
        "sourceType": "module"
    },
    "plugins": [
        "@typescript-eslint"
    ],
    "rules": {
        "@typescript-eslint/no-var-requires": 0,
        "@typescript-eslint/no-non-null-assertion": 0,
    }
}
webpack.common.ts,webpack.dev.ts、webpack.prod.ts

config目錄下創(chuàng)建以上三個(gè)文件,對(duì)應(yīng)代碼如下:

// webpack.common.ts
import * as path from 'path';
import * as webpack from 'webpack';
// 配置devServer
import 'webpack-dev-server';

const configCommon: webpack.Configuration = {
  entry: {
    app: path.join(__dirname, '../src/index.ts')
  },
  output: {
    path: path.join(__dirname, '../dist'),
    // clean: true
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['babel-loader'],
        exclude: /node_modules/
      },
      {
        test: /\.ts(x?)$/,
        use: [
          {
            loader: 'babel-loader'
          },
          {
            loader: 'ts-loader'
          }
        ],
        exclude: /node_modules/
      }
    ]
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js']
  },
  devServer: {
    static: {
      directory: path.join(__dirname, '../example') // 修改默認(rèn)靜態(tài)服務(wù)訪問public目錄
    }
  }
};
module.exports = configCommon;
webpack.dev.ts

// config/webpack.dev.ts
import * as path from 'path';
import * as webpack from 'webpack';
const { merge } = require('webpack-merge');
const HtmlWebpackPlguin = require('html-webpack-plugin');
const webpackCommon = require('./webpack.common');

const devConfig: webpack.Configuration = merge(webpackCommon, {
  devtool: 'inline-source-map',
  plugins: [
    new HtmlWebpackPlguin({
      inject: true,
      filename: 'index.html', // 只能是文件名,不能是xxx/index.html 會(huì)造成頁面模版加載ejs解析錯(cuò)誤
      template: path.resolve(__dirname, '../example/index.html'),
      title: 'example'
    })
  ]
});
module.exports = devConfig;

webpack.prod.ts

// webpack.prod.ts
const { merge } = require('webpack-merge');
import * as webpack from 'webpack';
const commonConfig = require('./webpack.common');
const prodConfig: webpack.Configuration = merge(commonConfig, {
  mode: 'production'
});

module.exports = prodConfig;
我們在根目錄下創(chuàng)建webpack.config.ts

// webpack.config.ts
type PlainObj = Record<string, any>;
const devConfig = require('./config/webpack.dev');
const prdConfig = require('./config/webpack.prod');
module.exports = (env: PlainObj, argv: PlainObj) => {
  // 開發(fā)環(huán)境 argv會(huì)獲取package.json中設(shè)置--mode的值
  if (argv.mode === 'development') {
    return devConfig;
  }
  return prdConfig;
};
在package.json中

 "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack serve --mode development",
    "build": "webpack --mode production"
  },
運(yùn)行npm run start

我們看下src/index.ts

const domApp = document.getElementById('app');
console.log(11122);
domApp!.innerHTML = 'hello word';
以上所有的這些基本都是為了支持ts環(huán)境,還有支持ts可配置webpack環(huán)境

現(xiàn)在我們試圖將一些通用的工具函數(shù)貢獻(xiàn)給其他小伙伴使用。

在src新建其他工具函數(shù),例如在之前我們所用到的timerChunk分時(shí)函數(shù)

timerChunk.ts分時(shí)函數(shù)

// timerChunk.ts
// 分時(shí)函數(shù)
module.exports = (sourceArr: any[] = [], callback: (args: unknown) => void, count = 1, wait = 200) => {
  let ret: any,
    timer: any = null;
  const renderData = () => {
    for (let i = 0; i < Math.min(count, sourceArr.length); i++) {
      // 取出數(shù)據(jù)
      ret = sourceArr.shift();
      callback(ret);
    }
  };
  return () => {
    if (!timer) {
      // 利用定時(shí)器每隔200ms取出數(shù)據(jù)
      timer = setInterval(() => {
        // 如果數(shù)據(jù)取完了,就清空定時(shí)器
        if (sourceArr.length === 0) {
          clearInterval(timer);
          ret = null;
          return;
        }
        renderData();
      }, wait);
    }
  };
};
memorize緩存函數(shù)

// src/memorize.ts
/**
 * @desption 緩存函數(shù)
 * @param {*} callback
 * @returns
 */

export const memorize = (callback: callBack) => {
  let cache = false;
  let result: unknown = null;
  return () => {
    // 如果緩存標(biāo)識(shí)存在,則直接返回緩存的結(jié)果
    if (cache) {
      return result;
    } else {
      // 將執(zhí)行的回調(diào)函數(shù)賦值給結(jié)果
      result = callback();
      // 把緩存開關(guān)打開
      cache = true;
      // 清除傳入的回調(diào)函數(shù)
      callback = null;
      return result;
    }
  };
};
isType.ts檢測數(shù)據(jù)類型

/**
 * @desption 判斷基礎(chǔ)數(shù)據(jù)類型以及引用數(shù)據(jù)類型,替代typeof
 * @param {*} val
 * @returns
 */
export const isType = (val: string | object | number | any[]) => {
  return (type: string) => {
    return Object.prototype.toString.call(val) === `[object ${type}]`;
  };
};
formateUrl.ts獲取url參數(shù)

import { isType } from './isType';
/**
 * @desption 將url參數(shù)轉(zhuǎn)換成對(duì)象
 * @param params
 * @returns
 */
export const formateUrl = (params: string) => {
  if (isType(params)('String')) {
    if (/^http(s)?/.test(params)) {
      const url = new URL(params);
      // 將參數(shù)轉(zhuǎn)換成http://localhost:8080?a=1&b=2   -> {a:1,b:2}
      return Object.fromEntries(url.searchParams.entries());
    }
    // params如果為a=1&b=2,則轉(zhuǎn)換成{a:1,b:2}
    return Object.fromEntries(new URLSearchParams(params).entries());
  }
};
lazyFunction.ts懶加載函數(shù)

import { memorize } from './memorize';
/**
 * @desption 懶加載可執(zhí)行函數(shù)
 * @param {*} factory
 * @returns
 */
export const lazyFunction = (factory: callBack) => {
  const fac: any = memorize(factory);
  const f = (...args: unknown[]) => fac()(...args);
  return f;
};
hasOwn.ts判斷一個(gè)對(duì)象的屬性是否存在

const has = Reflect.has;
const hasOwn = (obj: Record<string, any>, key: string) => has.call(obj, key);
export { hasOwn };
mergeDeep.ts深拷貝對(duì)象






import { isType } from './isType';
import { memorize } from './memorize';
/**
 * @desption 深拷貝一個(gè)對(duì)象
 * @param {*} obj
 * @param {*} targets
 */
export const mergeDeep = (obj: object, targets: object) => {
  const descriptors = Object.getOwnPropertyDescriptors(targets);
  // todo 針對(duì)不同的數(shù)據(jù)類型做value處理
  const helpFn = (val: any) => {
    if (isType(val)('String')) {
      return val;
    }
    if (isType(val)('Object')) {
      return Object.assign(Object.create({}), val);
    }
    if (isType(val)('Array')) {
      const ret: any[] = [];
      // todo 輔助函數(shù),遞歸數(shù)組內(nèi)部, 這里遞歸可以考慮用分時(shí)函數(shù)來代替優(yōu)化
      const loopFn = (curentVal: any[]) => {
        curentVal.forEach((item) => {
          if (isType(item)('Object')) {
            ret.push(helpFn(item));
          } else if (isType(item)('Array')) {
            loopFn(item);
          } else {
            ret.push(item);
          }
        });
      };
      loopFn(val);
      return ret;
    }
  };
  for (const name of Object.keys(descriptors)) {
    // todo 根據(jù)name取出對(duì)象屬性的每個(gè)descriptor
    const descriptor = descriptors[name];
    if (descriptor.get) {
      const fn = descriptor.get;
      Object.defineProperty(obj, name, {
        configurable: false,
        enumerable: true,
        writable: true,
        get: memorize(fn) // 參考https://github.com/webpack/webpack/blob/main/lib/index.js
      });
    } else {
      Object.defineProperty(obj, name, {
        value: helpFn(descriptor.value),
        writable: true
      });
    }
  }
  return obj;
};
我們在src中創(chuàng)建了以上所有的工具函數(shù)

我們在src/index.ts將上面所有的工具函數(shù)導(dǎo)入

// const domApp = document.getElementById('app');
// console.log(11122);
// domApp!.innerHTML = 'hello word';
export * from './memorize';
export * from './lazyFunction';
export * from './hasOwn';
export * from './getOrigin';
export * from './formateUrl';
export * from './mergeDeep';
export * from './isType';
現(xiàn)在需要打包不同環(huán)境的lib,通用就是umd,cjs,esm這三種方式

主要要是修改下webpack.config.output的library.type,參考官方outputlibrary[2]

我們在config目錄下新建一個(gè)webpack.target.ts

import * as webpack from 'webpack';
const prdConfig = require('./webpack.prod');
const { name } = require('../package.json');
enum LIBARY_TARGET {
  umd = 'umd',
  cjs = 'cjs',
  esm = 'esm'
}
const targetUMD: webpack.Configuration = {
  ...prdConfig,
  output: {
    ...prdConfig.output,
    filename: 'umd/index.js',
    library: {
      name,
      type: 'umd'
    }
  }
};
const targetCJS: webpack.Configuration = {
  ...prdConfig,
  output: {
    ...prdConfig.output,
    filename: 'cjs/index.js',
    library: {
      name,
      type: 'commonjs'
    }
  }
};
const targetESM: webpack.Configuration = {
  ...prdConfig,
  experiments: {
    outputModule: true
  },
  output: {
    ...prdConfig.output,
    filename: 'esm/index.js',
    library: {
      type: 'module',
      export: 'default'
    }
  }
};
const libraryTargetConfig = new Map([
  [LIBARY_TARGET.umd, targetUMD],
  [LIBARY_TARGET.cjs, targetCJS],
  [LIBARY_TARGET.esm, targetESM]
]);
module.exports = libraryTargetConfig;
webpack.config.ts

// webpack.config.ts
type PlainObj = Record<string, any>;
const devConfig = require('./config/webpack.dev');
const libraryTargetConfig = require('./config/webpack.target');
module.exports = (env: PlainObj, argv: PlainObj) => {
  console.log(argv);
  // 開發(fā)環(huán)境 argv會(huì)獲取package.json中設(shè)置--mode的值
  if (argv.mode === 'development') {
    return devConfig;
  }
  return libraryTargetConfig.has(argv.env.target) ? libraryTargetConfig.get(argv.env.target) : libraryTargetConfig.get('umd');
};
然后我們在package.json中配置不同模式打包

 "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack serve --mode development",
    "build:umd": "webpack --mode production --env target=umd",
    "build:esm": "webpack --mode production --env target=esm",
    "build:cjs": "webpack --mode production --env target=cjs",
    "build": "npm run build:umd && npm run build:esm && npm run build:cjs"
    
  },
當(dāng)我們依次執(zhí)行npm run build

在example目錄下新建測試index.ts,同時(shí)記得修改webpack.dev.ts的entry入口文件
// example/index.ts
// ok
import * as nice_utils from '../src/index';
// umd
// const nice_utils = require('../dist/umd/index.js');
// cjs
// const { nice_utils } = require('../dist/cjs/index.js');
// esm error
// import nice_utils from '../dist/esm/index.js';

const appDom = document.getElementById('app');
appDom!.innerHTML = 'hello, 歡迎關(guān)注公眾號(hào):Web技術(shù)學(xué)苑,好好學(xué)習(xí),天天向上!';
console.log(nice_utils);
console.log('formateUrl:', nice_utils.formateUrl('http://www.example.com?name=Maic&age=18'));
console.log('hasOwn:', nice_utils.hasOwn({ publictext: 'Web技術(shù)學(xué)苑' }, 'publictext'));
console.log('isType:', nice_utils.isType('Web技術(shù)學(xué)苑')('String'));

我們運(yùn)行npm run start,測試運(yùn)行下example是否ok


但是我發(fā)現(xiàn)esm打包出來的居然用不了,這就很坑了,難道是模塊使用的問題?

但是其他兩種貌似是ok的

npm 發(fā)布組件
我們現(xiàn)在將這包發(fā)布到npm上吧

npm run build

生成dist包,并且修改package.json文件的main,指定到dist/umd/index.js下

{
  "name": "@maicfir/nice_utils",
  "version": "1.0.4",
  "description": "一個(gè)好用的工具類庫",
  "main": "dist/umd/index.js",
  "types": "src/types/global.d.ts",
  ...
}
npm login

輸入自己npm賬戶和密碼

輸入自己密碼后,需要輸入郵箱,然后npm會(huì)給你郵箱發(fā)個(gè)code,把code輸入即可

npm publish

查看npm上是否成功,具體可以查看nice_utils[3]

總結(jié)
利用webpack5配置打包ts環(huán)境,主要是讓webpack5配置文件支持ts

組織webpack5打包不同library.type,支持打包成不同type,umd,cjs,ejs三種類型

編寫具體工具類函數(shù)

將自己寫的工具類發(fā)布到npm或者私服上,讓工具類變成通用工具代碼

本文示例code-example[4]

最后,看完覺得有收獲的,點(diǎn)個(gè)贊,在看,轉(zhuǎn)發(fā),收藏等于學(xué)會(huì),歡迎關(guān)注Web技術(shù)學(xué)苑,好好學(xué)習(xí),天天向上!

參考資料
[1]
configuration-languages: https://webpack.docschina.org/configuration/configuration-languages/#typescript

[2]
outputlibrary: https://webpack.docschina.org/configuration/output/#outputlibrary

[3]
nice_utils: @maicfir/nice_utils

[4]
code-example: https://github.com/maicFir/nice_utils








作者:Maic

歡迎關(guān)注微信公眾號(hào) :web技術(shù)學(xué)苑