我们前面已经详细讲过webpack、babel、typescript的基本配置,这里就不再赘述它们的API,如果有疑问的可以去看我的这篇文章前置章节和对应的官网。话不多讲,直接开干。。。
集成webpack
为了成功集成 webpack
,让我们的组件库能够构建出产物,这里我们需要完成三个步骤,分别是:
- 编写构建目标源码。因为文章的重点是工程化而非组件库的开发,代码预备部分我们不会实现组件的实际功能,只给出能够体现构建要点的 demo 代码。
- 准备
webpack.config.ts
配置文件。 - 在
package.json
中设置构建脚本。
我们先回顾一下上一章节所规划的 monorepo
目录结构,在集成 webpack
的过程中,我们会对它做非常多的拓展:
- 对于
packages
目录下的每一个组件包,我们制定了更细的源码组织规则:
- 各种配置文件,如
package.json
、webpack.config.ts
,都放在模块根目录下。 src
目录下存放源码,其中src/index.ts(js)
作为这个模块的总出口,所有需要暴露给外部供其他模块使用的方法、对象都要在这里声明导出。dist
目录作为产物输出目录,当然如果没执行过构建命令,这个目录是不会生成的。
- 在
packages
目录下新建统一出口包,命名为@farmerui/ui
。正如element-plus
主包负责集合各个子包,并统一导出其中内容一般。 - 我们还没走到搭建 demo 文档的阶段,但又迫不及待地想看到组件的实际效果,为了满足这个需求,我们要在根目录下建立
demo
模块,这个模块是一个Web
应用,用来展示组件,同时验证我们的monorepo
架构是否能立即响应子模块的更新修改。 - 关于
tsconfig
,我们会在集成TypeScript
的阶段进行说明。
添加各子包测试代码
1.公共方法代码添加@farmerui/shared
我们规划 @openxui/shared
作为公共工具方法包,将成为所有其他模块的依赖项。
scss
// 模块源码目录
📦shared
┣ ...
┣ 📂src
┃ ┣ 📜hello.ts
┃ ┣ 📜index.ts
┃ ┗ 📜useLodash.ts
┣ ...
为 shared 包安装 lodash 相关依赖
scss
pnpm --filter @farmerui/shared i -S lodash @types/lodash
给hello.ts文件添加测试代码
ini
// packages/shared/src/hello.ts
export function hello(to: string = 'World') {
const txt = `Hello ${to}!`;
alert(txt);
return txt;
}
useLodash.ts文件添加测试代码
javascript
// packages/shared/src/useLodash.ts
import lodash from 'lodash';
export function useLodash() {
return lodash
}
index.ts文件添加测试代码
javascript
// packages/shared/src/index.ts
export * from './hello';
export * from './useLodash'
2.button组件代码添加@farmerui/button
我们先设置 button
组件的初始代码,为了演示 monorepo
工程中内部模块之间互相依赖的特性,我们假定按钮组件的用例为点击之后打印 Hello ${props.hello}
,那么 button
将依赖于先前定义的 shared
模块中的 hello
方法。我们先通过 pnpm workspace
命令声明内部模块关联:
scss
pnpm --filter @farmerui/button i -S @farmerui/shared
button.tsx添加代码
typescript
import React from 'react';
import { hello } from '@farmerui/shared';
interface ButtonProps {
onClick: () => void;
size: number,
content: string
}
const Button: React.FC<ButtonProps> = ({onClick, size, content}) => {
return <>
<button onClick={() => hello(content)}>{content}</button>
</>
}
export default Button
index.ts组件出口添加代码
css
import Button from "./button";
export { Button }
3.input组件代码添加@farmerui/input
按照类似button组件的方式,实现处理一下 input
模块。我们假设 input
输入框组件实现的场景是监听内容变化,调用 hello
方法打印当前输入的内容。
scss
pnpm --filter @farmerui/input i -S @farmerui/shared
input.tsx组件添加代码
typescript
import React from 'react';
import { hello } from '@farmerui/shared';
interface InputProps {
onChange?: () => void;
onBlur: () => void;
InputType: 'text'
}
const Input: React.FC<InputProps> = ({onBlur, InputType}) => {
return <>
<input type={InputType} onBlur={(e) => onBlur(e.target.value)} onChange={(e) => hello(e.target.value)}></input>
</>
}
export default Input
index.ts组件出口添加代码
css
import Input from "./input";
export { Input }
4.组件主包 @farmerui/ui
@farmerui/ui
模块作为各个组件的统一出口,需要在 package.json
中正确声明与所有组件的依赖关系,之后每增加一个新组件,都应该在 @farmerui/ui
模块中补充导出代码。
package.json
perl
{
"name": "@farmerui/ui",
"version": "0.0.0",
"description": "",
"keywords": ["react", "utils", "component library"],
"author": "coderlwh",
"license": "MIT",
"homepage": "https://github.com/coderliweihong/farmer-ui/blob/main/README.md",
"repository": {
"type": "git",
"url": "git+https://github.com/coderliweihong/farmer-ui.git"
},
"bugs": {
"url" : "https://github.com/coderliweihong/farmer-ui/issues"
},
"scripts": {
"build": "echo build",
"test": "echo test"
},
"main": "",
"module": "",
"types": "",
"exports": {
".": {
"require": "",
"module": "",
"types": ""
}
},
"files": [
"dist",
"README.md"
],
"peerDependencies": {
"react": ">=18.2.0",
"react-dom": ">=18.2.0"
},
"dependencies": {
"@farmerui/button": "workspace:^",
"@farmerui/input": "workspace:^",
"@farmerui/shared": "workspace:^"
},
"devDependencies": {}
}
添加src/index.ts文件
javascript
// packages/ui/src/index.ts
export * from '@farmerui/button';
export * from '@farmerui/input';
export * from '@farmerui/shared';
安装子包依赖
css
pnpm --filter @farmerui/ui i
okey,我们子包文件中的测试代码添加完毕,接下来进入基本的构建配置环节。
基础构建配置
安装公共构建依赖
因为每个包都需要用到 webpack
、babel
和 typeScript
进行构建,我们在之前的前置知识章节讲过,公共开发依赖统一安装在根目录下,是可以被各个子包正常使用的。 安装webpack相关依赖
sql
pnpm i -wD webpack webpack-cli @types/webpack webpack webpack-dev-server webpack-merge cross-env html-webpack-plugin mini-css-extract-plugin
安装sass相关依赖
css
pnpm i -wD css-loader postcss-loader postcss postcss-preset-env sass-loader sass mini-css-extract-plugin
安装babel相关依赖
bash
pnpm install -wD babel-loader @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript @babel/plugin-transform-runtime @babel/runtime-corejs3
@babel/runtime
包运行时加载,需安装到dependencies
中
css
pnpm i -wS @babel/runtime
根目录添加babel.config.json
文件
perl
{
"presets": [
"@babel/preset-env",
"@babel/preset-react", //支持编译react jsx语法
"@babel/preset-typescript" //支持编译tsx、ts
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"absoluteRuntime": false,
"corejs": 3
}
]
]
}
子包构建配置文件添加
1.@farmer/shared
包添加webpack.config.ts
javascript
import * as path from 'path';
import * as webpack from 'webpack';
const config: webpack.Configuration = {
mode: 'development',
entry: './src/index.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js',
clean: true,
library: {
name: 'farmeruiShareds',
type: 'umd'
}
},
module: {
rules: [
{
test: /.(ts|js)$/,
use: {
loader: 'babel-loader',
options: {
rootMode: "upward" //该属性会告诉webpack一直向上级目录搜索babel的配置文件,这样可以将babel.config.json放到项目根目录下
}
},
exclude: /node_modules/,
}
],
},
resolve: {
extensions: ['.ts', '.js', '.json']
},
devtool: 'source-map'
};
export default config;
package.json
添加脚本命令
diff
// packages/shared/package.json
...
"scripts": {
+ "build:dev": "webpack -c webpack.config.ts",
"test": "echo test"
},
"main": "./dist/main.bundle.js",
"module": "./dist/main.bundle.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"require": "./dist/main.bundle.js",
"module": "./dist/main.bundle.js",
"types": "./dist/index.d.ts"
}
},
...
项目根目录运行打包命令
css
pnpm --filter @farmerui/shared run build:dev
2.@farmer/button
包添加webpack.config.ts
javascript
import * as path from 'path';
import * as webpack from 'webpack';
const config: webpack.Configuration = {
mode: 'development',
entry: './src/index.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js',
clean: true,
library: {
name: 'fu-button',
type: 'umd'
}
},
module: {
rules: [
{
test: /.(tsx|ts|js)$/,
use: {
loader: 'babel-loader',
options: {
rootMode: "upward",
}
},
exclude: /node_modules/,
}
],
},
resolve: {
extensions: ['.tsx','.ts', '.js', '.json']
},
devtool: 'source-map',
externals: [
{
'react': 'React',
'react-dom': 'ReactDOM'
},
// 除了 @farmerui/shared,未来可能还会依赖其他内部模块,我们直接用正则表达式将 @farmerui 开头的依赖项一起处理掉
/@farmerui.*/
]
};
export default config;
package.json
文件添加脚本执行命令
diff
// packages/shared/package.json
...
"scripts": {
+ "build:dev": "webpack -c webpack.config.ts",
"test": "echo test"
},
"main": "./dist/main.bundle.js",
"module": "./dist/main.bundle.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"require": "./dist/main.bundle.js",
"module": "./dist/main.bundle.js",
"types": "./dist/index.d.ts"
}
},
...
执行打包命令
css
pnpm --filter @farmerui/button run build:dev
3.@farmer/input
包参照button组件添加配置,这里不再赘述 4.@farmer/ui
包添加webpack.config.ts
javascript
import * as path from 'path';
import * as webpack from 'webpack';
const config: webpack.Configuration = {
mode: 'development',
entry: './src/index.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js',
clean: true,
library: {
name: 'farmeruis',
type: 'umd'
}
},
module: {
rules: [
{
test: /.(ts|js)$/,
use: {
loader: 'babel-loader',
options: {
rootMode: "upward",
}
},
exclude: /node_modules/,
}
],
},
resolve: {
extensions: ['.ts', '.js', '.json']
},
devtool: 'source-map',
externals: [
{
'react': 'React',
'react-dom': 'ReactDOM'
},
// 除了 @farmerui/shared,未来可能还会依赖其他内部模块,我们直接用正则表达式将 @farmerui 开头的依赖项一起处理掉
/@farmerui.*/
]
};
export default config;
package.json
添加脚本命令
diff
// packages/shared/package.json
...
"scripts": {
+ "build:dev": "webpack -c webpack.config.ts",
"test": "echo test"
},
"main": "./dist/main.bundle.js",
"module": "./dist/main.bundle.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"require": "./dist/main.bundle.js",
"module": "./dist/main.bundle.js",
"types": "./dist/index.d.ts"
}
},
...
到此为止,所有组件的构建都完成了,我们可以通过 路径过滤器 选中 packages
目录下所有包进行构建
arduino
pnpm --filter "./packages/**" run build:dev
由于 @openxui/ui
是组件库的统一出口包,它的 package.json
的 dependencies
字段中声明了所有其他模块,我们也可以用依赖过滤器 ...
,构建 ui
以及其所有的依赖项,达到整体构建的效果。不过如果不能确保所有包都在 ui
中再进行一次导出,还是采用前者更好。
css
pnpm --filter @farmerui/ui... run build:dev
执行命令后,命令行输出如下:
ini
Scope: 4 of 6 workspace projects
packages/shared build:dev$ webpack -c webpack.config.ts
│ asset main.bundle.js 5.82 KiB [compared for emit] (name: main) 1 related asset
│ runtime modules 937 bytes 4 modules
│ built modules 395 bytes [built]
│ ./src/index.ts 53 bytes [built] [code generated]
│ ./src/hello.ts 223 bytes [built] [code generated]
│ ./src/useLodash.ts 77 bytes [built] [code generated]
│ external {"commonjs":"lodash","commonjs2":"lodash","amd":"lodash","root":"_"} 42 bytes [built] [code generated]
│ webpack 5.89.0 compiled successfully in 843 ms
└─ Done in 3s
packages/button build:dev$ webpack -c webpack.config.ts
│ asset main.bundle.js 5.94 KiB [compared for emit] (name: main) 1 related asset
│ runtime modules 937 bytes 4 modules
│ built modules 538 bytes [built]
│ cacheable modules 454 bytes
│ ./src/index.ts 49 bytes [built] [code generated]
│ ./src/button.tsx 405 bytes [built] [code generated]
│ external "React" 42 bytes [built] [code generated]
│ external "@farmerui/shared" 42 bytes [built] [code generated]
│ webpack 5.89.0 compiled successfully in 690 ms
└─ Done in 2.7s
packages/input build:dev$ webpack -c webpack.config.ts
│ asset main.bundle.js 6 KiB [compared for emit] (name: main) 1 related asset
│ runtime modules 937 bytes 4 modules
│ built modules 612 bytes [built]
│ cacheable modules 528 bytes
│ ./src/index.ts 46 bytes [built] [code generated]
│ ./src/input.tsx 482 bytes [built] [code generated]
│ external "React" 42 bytes [built] [code generated]
│ external "@farmerui/shared" 42 bytes [built] [code generated]
│ webpack 5.89.0 compiled successfully in 716 ms
└─ Done in 2.8s
packages/ui build:dev$ webpack -c webpack.config.ts
│ asset main.bundle.js 6.89 KiB [emitted] (name: main) 1 related asset
│ runtime modules 937 bytes 4 modules
│ built modules 254 bytes [built]
│ ./src/index.ts 128 bytes [built] [code generated]
│ external "@farmerui/button" 42 bytes [built] [code generated]
│ external "@farmerui/input" 42 bytes [built] [code generated]
│ external "@farmerui/shared" 42 bytes [built] [code generated]
│ webpack 5.89.0 compiled successfully in 616 ms
└─ Done in 2.6s
~/Desktop/farmer-ui main ±✚ pnpm --filter "./packages/**" run build:dev
Scope: 4 of 6 workspace projects
packages/shared build:dev$ webpack -c webpack.config.ts
│ asset main.bundle.js 5.82 KiB [compared for emit] (name: main) 1 related asset
│ runtime modules 937 bytes 4 modules
│ built modules 395 bytes [built]
│ ./src/index.ts 53 bytes [built] [code generated]
│ ./src/hello.ts 223 bytes [built] [code generated]
│ ./src/useLodash.ts 77 bytes [built] [code generated]
│ external {"commonjs":"lodash","commonjs2":"lodash","amd":"lodash","root":"_"} 42 bytes [built] [code generated]
│ webpack 5.89.0 compiled successfully in 772 ms
└─ Done in 2.8s
packages/input build:dev$ webpack -c webpack.config.ts
└─ Running...
packages/button build:dev$ webpack -c webpack.config.ts
│ asset main.bundle.js 5.94 KiB [compared for emit] (name: main) 1 related asset
│ asset main.bundle.js 6 KiB [compared for emit] (name: main) 1 related asset
└─ Running...
packages/button build:dev$ webpack -c webpack.config.ts
│ asset main.bundle.js 5.94 KiB [compared for emit] (name: main) 1 related asset
│ runtime modules 937 bytes 4 moduleslt] [code generated]
└─ Running...
packages/button build:dev$ webpack -c webpack.config.ts
│ asset main.bundle.js 5.94 KiB [compared for emit] (name: main) 1 related asset
│ built modules 612 bytes [built]uleslt] [code generated]
└─ Running...
packages/button build:dev$ webpack -c webpack.config.ts
│ asset main.bundle.js 5.94 KiB [compared for emit] (name: main) 1 related asset
│ cacheable modules 528 bytesoduleslt] [code generated]
└─ Running...
packages/button build:dev$ webpack -c webpack.config.ts
│ asset main.bundle.js 5.94 KiB [compared for emit] (name: main) 1 related asset
│ ./src/index.ts 46 bytes [built] [code generated]ed]
└─ Running...
packages/button build:dev$ webpack -c webpack.config.ts
│ asset main.bundle.js 5.94 KiB [compared for emit] (name: main) 1 related asset
│ ./src/input.tsx 482 bytes [built] [code generated]]
└─ Running...
packages/button build:dev$ webpack -c webpack.config.ts
│ asset main.bundle.js 5.94 KiB [compared for emit] (name: main) 1 related asset
│ external "React" 42 bytes [built] [code generated]ed]
└─ Running...
packages/button build:dev$ webpack -c webpack.config.ts
│ asset main.bundle.js 5.94 KiB [compared for emit] (name: main) 1 related asset
│ external "@farmerui/shared" 42 bytes [built] [code generated]
└─ Running...
packages/button build:dev$ webpack -c webpack.config.ts
│ asset main.bundle.js 5.94 KiB [compared for emit] (name: main) 1 related asset
│ webpack 5.89.0 compiled successfully in 720 msenerated]
└─ Running...
packages/button build:dev$ webpack -c webpack.config.ts
│ asset main.bundle.js 5.94 KiB [compared for emit] (name: main) 1 related asset
└─ Done in 2.8ses 937 bytes 4 moduleslt] [code generated]
│ built modules 538 bytes [built]ilt] [code generated]
│ cacheable modules 454 bytes
│ ./src/index.ts 49 bytes [built] [code generated]
│ ./src/button.tsx 405 bytes [built] [code generated]
│ external "React" 42 bytes [built] [code generated]
│ external "@farmerui/shared" 42 bytes [built] [code generated]
│ webpack 5.89.0 compiled successfully in 685 ms
└─ Done in 2.8s
packages/ui build:dev$ webpack -c webpack.config.ts
│ asset main.bundle.js 6.89 KiB [compared for emit] (name: main) 1 related asset
│ runtime modules 937 bytes 4 modules
│ built modules 254 bytes [built]
│ ./src/index.ts 128 bytes [built] [code generated]
│ external "@farmerui/button" 42 bytes [built] [code generated]
│ external "@farmerui/input" 42 bytes [built] [code generated]
│ external "@farmerui/shared" 42 bytes [built] [code generated]
│ webpack 5.89.0 compiled successfully in 637 ms
└─ Done in 2.6s
观察命令行输出,可以发现整个打包执行顺序(shared -> button & input(并行) -> ui
) 是符合依赖树的拓扑排序的。由于我们用 webpack.externals
外部化了依赖,这个特性现在对我们而言无关紧要,但在未来定制完善的打包体系,需要研究全量构建时,拓扑排序的特性就会变得非常关键。
我们在项目根目录下的package.json
文件添加全局打包
diff
{
// ...
"scripts": {
+ "build:ui": "pnpm --filter "./packages/**" run build:dev"
},
}
我们已经能够打包umd产物,我们做lib库肯定想支持更多场景,比如esm和cmd,我们继续修改@farmerui/button
的package.json
和webpack.config.ts
文件代码,其余模块也参考该模块进行设置,不再详叙。
package.json
添加配置
diff
{
...
"scripts": {
+ "build:dev": "cross-env NODE_ENV=development pnpm --filter @farmerui/button run build:ESM & cross-env NODE_ENV=development pnpm --filter @farmerui/button run build:UMD & cross-env NODE_ENV=development pnpm --filter @farmerui/button run build:CMD",
+ "build:prod": "cross-env NODE_ENV=production pnpm --filter @farmerui/button run build:ESM & cross-env NODE_ENV=production pnpm --filter @farmerui/button run build:UMD & cross-env NODE_ENV=production pnpm --filter @farmerui/button run build:CMD",
+ "build:ESM": "cross-env LIB_TYPE=module webpack -c webpack.config.ts",
+ "build:UMD": "cross-env LIB_TYPE=umd webpack -c webpack.config.ts",
+ "build:CMD": "cross-env LIB_TYPE=commonjs webpack -c webpack.config.ts",
"test": "echo test"
},
+ "main": "./lib/index.js",
+ "browser": "./dist/index.js",
+ "module": "./es/index.esm.js",
"types": "",
"exports": {
".": {
+ "require": "./lib/index.js",
+ "module": "./es/index.esm.js",
+ "default": "./dist/index.js"
}
},
"files": [
"dist",
"lib",
"es",
"README.md"
],
"peerDependencies": {
"react": ">=18.2.0",
"react-dom": ">=18.2.0"
},
"dependencies": {
"@farmerui/shared": "workspace:^"
}
}
```diff
//packages/button/webpack.config.ts
import * as path from 'path';
import * as webpack from 'webpack';
const isProd = process.env.NODE_ENV && process.env.NODE_ENV.toLowerCase() === 'production';
+ const libType:string = process.env.LIB_TYPE && process.env.LIB_TYPE.toLowerCase() || '';
/**
* generate library output config
* @param type module type
*/
+ const generateLibOutputConfig = (type: string) => {
switch (type) {
case 'umd':
return {
path: path.resolve(__dirname, 'dist'),
filename: 'index.js',
library: {
name: 'FarmerUIShared',
type: 'umd',
export: 'default'
},
globalObject: 'globalThis',
clean: true
}
case 'module':
return {
path: path.resolve(__dirname, 'es'),
filename: 'index.esm.js',
library: {
type: 'module'
},
chunkFormat: 'module',
clean: true
}
case 'commonjs':
return {
path: path.resolve(__dirname, 'lib'),
filename: 'index.js',
library: {
name: 'FarmerUIShared',
type: 'commonjs'
},
clean: true
}
default:
return {
path: path.resolve(__dirname, 'dist'),
filename: 'index.js',
library: {
name: 'FarmerUIShared',
type: 'umd',
export: 'default'
},
globalObject: 'globalThis',
clean: true
}
}
}
/**
* generate library externalsType config
* @param type module type
*/
+ const generateLibExternalsTypeConfig = (type: string) => {
switch (type) {
case 'umd':
return 'umd';
case 'module':
return 'module';
case 'commonjs':
return 'commonjs';
default:
return 'umd';
}
}
const config: webpack.Configuration = {
mode: isProd ? 'production' : 'development',
entry: './src/index.ts',
// 由于输出 ESM 格式文件为 Webpack 实验特性,因此需要加上此配置。
+ experiments: {
outputModule: libType === 'module'
},
output: generateLibOutputConfig(libType),
module: {
rules: [
{
test: /.(tsx|ts|js)$/,
use: {
loader: 'babel-loader',
options: {
rootMode: "upward",
}
},
exclude: /node_modules/,
}
],
},
resolve: {
extensions: ['.tsx','.ts', '.js', '.json'],
},
devtool: 'source-map',
+ externalsType: generateLibExternalsTypeConfig(libType),
+ externals: [
{
'react': 'React',
'react-dom': 'ReactDOM'
},
// 除了 @farmerui/shared,未来可能还会依赖其他内部模块,我们直接用正则表达式将 @farmerui 开头的依赖项一起处理掉
/@farmerui.*/
]
};
export default config;
⚠️ 带有
{ root, amd, commonjs, ... }
的对象只允许用于libraryTarget: 'umd'
和 [externalsType: 'umd'
]。其他库的 target 不允许这样做。
运行整体打包命令
arduino
pnpm run build:ui
搭建demo应用演示组件效果
创建 demo
子模块来演示如何在 monorepo
项目里建立一个网站应用模块。我们先设置好 demo
模块的目录结构:
css
📦farmer-ui
┣ 📂...
┣ 📂demo
┃ ┣ 📂node_modules
┃ ┣ 📂dist
┃ ┣ 📂src
┃ ┃ ┣ 📂main.ts
┃ ┃ ┗ 📜index.tsx
┃ ┣ 📜index.html
┃ ┣ 📜webpack.config.ts
┃ ┗ 📜package.json
demo属于新建子模块,需要在 pnpm-workspace.yaml
中补充声明这个工作空间:
diff
packages:
# 根目录下的 docs 是一个独立的文档应用,应该被划分为一个模块
- docs
# packages 目录下的每一个目录都作为一个独立的模块
- packages/*
+ # demo演示组件模块,划分为一个模块
+ - demo
添加package.json
文件
perl
{
"name": "@farmer/demo",
"private": true,
"scripts": {
"dev": "cross-env NODE_ENV=development webpack server"
},
"dependencies": {
"@farmer/ui": "workspace:^"
}
}
添加webpack.config.ts
文件
javascript
import * as path from 'path';
import * as webpack from 'webpack';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import {Configuration as DevServerConfiguration} from 'webpack-dev-server';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
const isProd = process.env.NODE_ENV && process.env.NODE_ENV.toLowerCase() === 'production';
const config: webpack.Configuration & { devServer?: DevServerConfiguration } = {
mode: isProd ? 'production' : 'development',
entry: './src/App.tsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js',
clean: true
},
module: {
rules: [
{
test: /.(tsx|ts|js)$/,
use: {
loader: 'babel-loader',
options: {
rootMode: "upward",
}
},
exclude: /node_modules/,
},
{
test: /.s[ac]ss$/i,
use: [
// 将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载
MiniCssExtractPlugin.loader,
// 将 CSS 转化成 CommonJS 模块
{
loader: 'css-loader'
},
//处理css3不同浏览器兼容性
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
'autoprefixer',
'postcss-preset-env',
],
},
},
},
// 将 Sass 编译成 CSS
'sass-loader',
],
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js', '.json'],
alias: {
'@farmerui/ui': path.resolve(__dirname, '..','packages/ui/src'),
'@farmerui/button': path.resolve(__dirname, '..','packages/button/src'),
'@farmerui/input': path.resolve(__dirname, '..','packages/input/src'),
'@farmerui/shared': path.resolve(__dirname, '..','packages/shared/src'),
'@': path.resolve(__dirname, 'src/'),
},
},
plugins: [
new HtmlWebpackPlugin({
title: 'Hello Demo',
template: 'index.html'
}),
new MiniCssExtractPlugin({
filename: 'css/[name].css'
})
],
devtool: 'source-map',
devServer: {
static: './dist',
host: '0.0.0.0',
port: 9527,
open: false,
hot: true
},
externals: [
{
'react': 'React',
'react-dom': 'ReactDOM',
}
]
};
export default config;
⚠️上面webpack中的reslove.alias需要将所有依赖映射到对应源码文件上,这里为了demo演示组件实时更新直接手动配置,也可参考
babel-plugin-resolve-config-json
插件来根据tsconfig.json中的paths来设置alias 添加页面入口模版文件index.html
xml
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div id="app"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>
</body>
</html>
我们webpack配置中是将react、react-dom外部化处理了,所以需要通过cdn方式来引入react依赖
添加src及其业务页面APP.tsx文件
APP.tsx
javascript
import * as React from "react";
import { createRoot } from "react-dom";
import { Button, Input } from "@farmerui/ui";
const root = createRoot(document.getElementById('app'));
root.render(<div>
<Button content="按钮1"/>
<Input onBlur={(value) => {alert(value)}}/>
</div>);
运行启动命令
css
pnpm --filter @farmerui/demo run start
集成typesript
安装typescript相关依赖
css
pnpm i -wD typescript ts-node @types/node
tsconfig.json
理解:
每个
tsconfig.json
将一个文件集合声明为一个ts project
(如果称为项目则容易产生概念混淆,故叫做ts project
),通过include
描述集合中包含的文件、exclude
字段声明了集合中需要排除的文件。注意,除了node_modules
中的三方依赖,每个被引用的源码文件都要被包含进来。
compilerOptions
是编译选项,决定了TypeScript
编译器在处理该ts project
包含的文件时所采取的策略与行为。
ruby
{
"compilerOptions": {
// ts project的编译选项
},
"include": [
// ts project包含哪些文件
],
"exclude": [
// 在 include 包含的文件夹中需要排除哪些文件
]
}
include
与 exclude
字段通过 glob 语法进行文件匹配
在整个monorepo模式中,有多个子包,那我们是不是每个子包都划分为一个ts project
呢,我们不会这么干,我们参考各大开源ui库会发现,大佬们都是按功能来划分ts project
,也就是将类似功能的子包划分为一个ts project
,将其中配置抽离到根目录来加以管理。element-plus UI库对于ts project
的划分
对于ts编译选项 compilerOptions
有很多重复性的配置,我们也有样学样将这些重复配置抽离到tsconfig.base.json
中管理,根目录执行脚本添加
bash
touch tsconfig.base.json
添加公共配置
ruby
{
"compilerOptions": {
// 项目的根目录
"rootDir": ".",
// 项目基础目录
"baseUrl": ".",
// tsc 编译产物输出目录
"outDir": "dist",
// 编译目标 js 的版本
"target": "es5",
//
"module": "commonjs",
// 模块解析策略
"moduleResolution": "node",
// 是否生成辅助 debug 的 .map.js 文件。
"sourceMap": true,
// 产物不消除注释
"removeComments": false,
// 严格模式类型检查,建议开启
"strict": true,
// 不允许有未使用的变量
"noUnusedLocals": true,
// 允许引入 .json 模块
"resolveJsonModule": true,
// 与 esModuleInterop: true 配合允许从 commonjs 的依赖中直接按 import XX from 'xxx' 的方式导出 default 模块。
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
// 在使用 const enum 或隐式类型导入时受到 TypeScript 的警告
"isolatedModules": true,
// 检查类型时是否跳过类型声明文件,一般在上游依赖存在类型问题时置为 true。
"skipLibCheck": true,
"noImplicitAny": true,
"jsx": "react",
"allowJs": true,
// 引入 ES 的功能库
"lib": [],
// 默认引入的模块类型声明
"types": [],
// 路径别名设置
"paths": {
"@farmerui/*": ["packages/*/src"]
}
}
}
将node环境执行的脚本、配置文件划分为一个ts project
,添加tsconfig.node.json
,根目录执行脚本添加
bash
touch tsconfig.node.json
json
// tsconfig.node.json
{
// 继承基础配置
"extends": "./tsconfig.base.json",
"compilerOptions": {
// 该 ts project 将被视作一个部分,通过项目引用(Project References)功能集成到一个 tsconfig.json 中
"composite": true,
// node 脚本没有 dom 环境,因此只集成 es5 库即可
"lib": ["es5"],
// 集成 Node.js 库函数的类型声明
"types": ["node"],
// 脚本有时会以 js 编写,因此允许 js
"allowJs": true
},
"include": [
// 目前项目中暂时只有配置文件,如 webpack.config.ts,以后会逐步增加
"**/*.config.*",
],
"exclude": [
// 暂时先排除产物目录,packages/xxx/dist/x.config.js 或者 node_modules/pkg/x.config.js 不会被包含进来
"**/dist",
"**/lib",
"**/es",
"**/node_modules"
]
}
将我们所有lib子包划分为一个ts project
,它们几乎都是组件库的实现代码,大多要求浏览器环境下特有的 API(例如 DOM API),且相互之间存在依赖关系。根目录执行命令
bash
touch tsconfig.web.json
json
// tsconfig.web.json
{
// 继承基础配置
"extends": "./tsconfig.base.json",
"compilerOptions": {
"composite": true,
// 组件库依赖浏览器的 DOM API
"lib": ["es5", "DOM", "DOM.Iterable"],
"types": ["node"],
},
"include": [
"typings/env.d.ts",
"packages/**/src"
],
}
到此,IDE 还是无法正常提供类型服务,我们最终还是要在根目录建立一个总的
tsconfig.json
,通过 项目引用(Project References)功能 将多个compilerOptions.composite = true
的ts project
聚合在一起,这样 IDE 才能够识别。
根目录添加ts配置文件
bash
touch tsconfig.json
json
// tsconfig.json
{
"compilerOptions": {
"target": "es5",
"moduleResolution": "node",
// 将每个文件作为单独的模块(与"ts.transpileModule"类似)。
"isolatedModules": true,
"useDefineForClassFields": true,
},
"files": [],
"references": [
// 聚合 ts project
{ "path": "./tsconfig.web.json" },
{ "path": "./tsconfig.node.json" }
],
}
每个引用的
path
属性可以指向包含tsconfig.json
文件的目录,或指向配置文件本身(可以有任何名称)。当您引用一个项目时,新的事情会发生:
- 从引用的项目导入模块将改为加载其输出声明文件 (
.d.ts
)- 如果引用的项目生成
outFile
,则输出文件.d.ts
文件的声明将在此项目中可见- 如果需要,构建模式(见下文)将自动构建引用的项目
通过分成多个项目,您可以极大地提高类型检查和编译的速度,减少使用编辑器时的内存使用量,并改善程序逻辑分组的执行。
引用的项目必须启用新的
composite
设置。需要此设置来确保 TypeScript 可以快速确定在哪里找到引用项目的输出。启用composite
标志会改变一些事情:
rootDir
设置,如果未显式设置,则默认为包含tsconfig
文件的目录- 所有实现文件必须与
include
模式匹配或在files
数组中列出。如果违反此限制,tsc
将通知您哪些文件未指定declaration
必须打开
@farmerui/Demo
中由于时应用运行时,与其他的lib没什么太大关系,我们给它添加自己的tsconfig.json文件
bash
cd demo && touch tsconfig.json
perl
{
// 集成基础配置
"extends": "../tsconfig.base.json",
"compilerOptions": {
"baseUrl": ".",
// Web 应用需要 DOM 环境
"lib": ["es5", "DOM", "DOM.Iterable"],
// Web 应用不需要 node 相关方法
"types": [],
// baseUrl 改变了,基础配置中的 paths 也需要一并重写
"paths": {
"@/*": ["src/*"],
"@farmerui/*": ["../packages/*/src"]
}
},
"include": [
// demo 应用会引用其他子模块的源码,因此都要包含到 include 中
"../packages/*/src",
"src"
]
}
我们去代码编辑器中查看typescript是否还报错,之前我们在input.tsx
文件中故意留了几个ts报错
第1行报错:Cannot resolve definitions for module 'react'
这是告诉我们react缺少声明文件,由于demo中用到了react-dom,我们在这里就一起安装了 fix:
bash
pnpm i -wD @types/react @types/react-dom
安装之后,vscode可能有延时,需要cmd + shift + p
,选择Developer: Reload Window
reload vscode
第5行错误:很明显这是个语法错误,我们在定义接口时,只给函数形参类型,没给形参的name这样是不会通过ts检查的
fix:
diff
interface InputProps {
onChange?: () => void;
+ onBlur?: (args0: string) => void;
InputType?: 'text'
}
第10行错误:Cannot invoke an object which is possibly 'undefined'.这是因为我们在接口定义时onBlur不是必传的参数,所以在真正调用时可能时undefined,所以ts会检查报错。 fix:
diff
<input type={InputType} onBlur={(e) => onBlur?.(e.target.value)} onChange={(e) => hello(e.target.value)}></input>
页面上报错终于干净了,很喜欢这种感觉 我们利用es?语法来选择调用这个有可能存在undefined的方法就可以了。 通过以上几个小问题我们可以感受到ts带来的好处,代码越多,好处越多。说教无意,只有折断的骨头才是最好的课本,相信大家已经被生产运行时这种低级失误的bug折磨的很舒爽了吧。选择ts吧!她真的很棒!
打包体系构建
一、设计打包体系
经过我们的基础打包配置后,会发现大量配置代码的重复,这对于最终包结果可能没有影响,但是确增加我们开发时候的心智负担。 1.公共配置提取
根据我们之前的webpack基础配置章节我们可以了解到,其实每个工程的配置99%是相同的,即使存在着外部依赖(externals
),包名(output.library.name
) 这样的不同配置,仔细思考也会发现它们都能够从各自的 package.json
中自动获取:
- 包名、全局变量名可以根据
name
字段生成,为什么手动维护呢? - 需要被外部化处理的依赖项也在
peerDependencies
和dependencies
中,为什么手动维护呢?
2.构建全量产物
目前我们构建出的 umd
、es
以及 d.ts
类型声明产物,足以让通过包管理器(npm
/ pnpm
)集成组件的用户正常使用,我们称这种产物为常规产物 。常规产物适用于构建场景,必须配合包管理器使用。
但是这些产物如果直接通过 <script src="xxxx"></script>
的方式引入,是无法正常工作的。这是因为我们的 umd
产物经过了依赖外部化处理,直接引用会缺少大量依赖。这种场景需要取消依赖的外部化处理,构建出全量产物。 例如 antd
产物中的 antd.js
、react
产物中的 react(.runtime).js
。全量产物适用于非构建场景,不必配合包管理器使用。
全量产物的使用场景其实是不容忽视的,一方面,许多新手用户需要这样一个快速上手的途径(可以暂时不折腾构建工具);另一方面,在线演示的沙盒环境也正需要这样的产物。
在全量产物的基础上,还可以进一步扩展:
- 全量产物体积可能太大了,我们需要压缩混淆后的
.min.js
版本。.min.js
版本体积虽小但是调试困难,我们需要提供sourcemap
文件(sourcemap
推荐阅读:sourcemap 这么讲,我彻底理解了、关于sourcemap,这篇文章就够了)。 这时我们回过头来想想,webpack
对于这些各式各样的打包需求都能支持,但是它们各自对应了不同的打包配置,webpack
无法在一次构建中生成全部类型的产物 。 这就更需要我们在webpack
的基础上增强构建能力,演化出自己的打包体系。
3.痛点:
(1)如果我们按照之前多仓库单项目的模式配置工程化的配置文件,就会造成大量配置项冗余
(2)每个子项目单独的配置项文件不易于维护和拓展
(3)构建子包全量产物场景的支持必不可少
(4)提高构建的自动化程度,进一步降低维护构建配置的心智负担。
根据上面我们综合分析,总结出以下定制打包体系的理由:
- 促进
Clean Code
,消除大量的重复代码。 - 集中维护构建配置,避免分散管理造成的多地多次修改的困扰。
- 提高构建的自动化程度,进一步降低维护构建配置的心智负担。
- 增强构建能力,支持同时生成不同类型的产物。
4.打包体系初步分析:
我们可以去github上观察monorepo模式的开源项目,你会发现每个包的打包都五花八门的,并没有说是统一的,所谓八仙过海,各显神通。我们以pixijs这个2D渲染库为案例进行分析,总结出适合我们的打包方案。pixijs虽然基于vite打包,但不影响我们分析它打包的整个思路。
- 每个子包中移除了专属的
rollup.config
或者vite.config
文件。 - 在根目录下有一个集中的脚本,一口气完成整个构建任务。
- 在全量构建脚本中,首先获取所有的子包工作目录。
- 遍历上一步获取到的子包列表,获取每个子包的文件目录、
package.json
等信息,在循环中结合子包信息动态拼接出每个包构建配置。
- 最后调用构建工具 API / CLI 读取配置,执行构建。
getPkgs
- 获取待构建的子包。forEach
、nextPkg
、endForEach?
- 控制子包列表的遍历。getPkgInfo
- 获取子包package.json
等信息。generateConfig
- 生成子包的构建配置。build
- 执行构建。
我们的组件库并不计划编写一个集中脚本去编排所有流程 ,我们打算充分利用包管理器和构建工具的能力。pnpm
本身的命令,就足以实现"获取所有待构建的子包"、"对子包进行拓扑排序"、"遍历子包执行脚本"。
bash
pnpm --filter ./packages/** run build
因此,上述例子中的获取子包 getPkgs
,循环控制 forEach
系列,我们都不打算自己实现,而是借助 pnpm
本身的能力。
另外,我们也不计划废除每个子包中的 webpack.config
,去调用 webpack
的 API 单独实现 build
构建方法。而是计划在 webpack.config
中调用公共的 generateConfig
方法直接生成完善的打包配置,通过 webpack build
的 CLI 命令去读取配置,启动构建进程。 届时,webpack.config
配置会大幅简化,变成类似下面的形式。
arduino
import { generateConfig } from '@farmerui/build'
export default generateConfig(/** ... */);
这样一来,我们就只需要考虑如何实现生成打包配置的 generateConfig
方法 。当然,generateConfig
的实现也依赖于 getPkgInfo
对各自子包 package.json
信息的获取。
不知不觉写了3w8千字了,篇幅有点长了,打包体系构建实践放到下一篇内容吧,期待与你相遇,与你共同成长!