第一篇:# 5-1 React 实战之从零到一的项目环境搭建(一)
接上文:# 5-1 React 实战之从零到一的项目开发(二)
4、组件库开发
采用 Rollup 打包,产物为 umd、esm 两种
- 新建组件库文件夹(进入
packages/components
内)
arduino
mkdir react-x-components
- 初始化该项目(进入
packages/component/react-x-components
内)
bash
cd react-x-components && pnpm init
- 创建两个组件文件
bash
mkdir src && mkdir src/button && touch src/button/index.tsx && mkdir src/card && touch src/card/index.tsx
src/button/index.tsx
写入代码
javascript
import React from "react";
type Props = {
children: React.ReactNode;
onClick?: () => void;
};
export default function Button({ children, onClick }: Props) {
return (
<button
onClick={onClick}
className=" w-16 h-8 mx-4 text-sm bg-blue-500 text-white flex justify-center items-center rounded-full hover:bg-blue-800 transition-all"
>
{children}
</button>
);
}
src/card/index.tsx
写入代码
typescript
import React from "react";
type Props = {
className?: string;
children?: React.ReactNode;
};
export default function Card({ className, children }: Props) {
return (
<div
className={` bg-white border border-gray-200 m-2 rounded-sm shadow-md ${className}`}
>
{children}
</div>
);
}
一、手写 Rollup 配置
- 安装对应依赖(
packages/components/react-x-components
下)
sql
pnpm add rollup rollup-plugin-clear rollup-plugin-auto-add rollup-plugin-typescript2 @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-alias rollup-plugin-peer-deps-external rollup-plugin-filesize rollup-plugin-postcss rollup-plugin-terser rollup-plugin-multi-input postcss typescript react @types/react -D
- 创建对应文件
bash
mkdir scripts && touch scripts/rollup.config.js && touch tsconfig.json && touch scripts/tsconfig.esm.json && touch scripts/tsconfig.umd.json
tsconfig.json
写如下代码
json
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"outDir": "./lib",
"allowJs": false,
"skipLibCheck": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": false,
"jsx": "react", // react18这里也可以改成react-jsx
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
},
"noImplicitAny": false // 是否在表达式和声明上有隐含的any类型时报错
},
"include": ["./src/**/*"],
"exclude": ["node_modules", "**/dist", "**/esm"],
"ts-node": {
"compilerOptions": {
"module": "CommonJS"
}
}
}
scripts/tsconfig.esm.json
写如下代码
json
{
"extends": "../tsconfig.json",
"ts-node": {
"transplieOnly": true,
"require": ["typescript-transform-paths/register"]
},
"compilerOptions": {
"plugins": [
{ "transform": "typescript-transform-paths" },
{
"transform": "typescript-transform-paths",
"afterDeclarations": true
}
],
"declaration": true,
"jsx": "react",
"jsxFactory": "React.createElement",
"jsxFragmentFactory": "React.Fragment"
},
"include": ["../src"]
}
scripts/tsconfig.umd.json
写如下代码
json
{
"extends": "../tsconfig.json",
"compilerOptions": {
"jsx": "react",
"jsxFactory": "React.createElement",
"jsxFragmentFactory": "React.Fragment"
}
}
- 改造
package.json
加上peerDependencies
告知当前组件库所依赖的包(组件库可不提供)
perl
{
"name": "react-x-components",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@rollup/plugin-alias": "^5.1.0",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/react": "^18.2.64",
"postcss": "^8.4.35",
"react": "^18.2.0",
"rollup": "^4.12.1",
"rollup-plugin-auto-add": "^0.0.6",
"rollup-plugin-clear": "^2.0.7",
"rollup-plugin-filesize": "^10.0.0",
"rollup-plugin-multi-input": "^1.4.1",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.36.0",
"typescript": "^5.4.2"
}
"peerDependencies": { // ++++++
"react": "^18.2.0", // ++++++
"react-dom": "^18.2.0" // ++++++
}
}
- 手写配置代码
scripts/rollup.config.js
php
const clear = require("rollup-plugin-clear");
const autoAdd = require("rollup-plugin-auto-add").default;
const multiInput = require("rollup-plugin-multi-input").default;
const typescript = require("rollup-plugin-typescript2");
const path = require("path");
const peerDepExternal = require("rollup-plugin-peer-deps-external");
const resolve = require("@rollup/plugin-node-resolve");
const commonjs = require("@rollup/plugin-commonjs");
const alias = require("@rollup/plugin-alias");
const postcss = require("rollup-plugin-postcss");
const { terser } = require("rollup-plugin-terser");
const filesize = require("rollup-plugin-filesize");
const pkg = require("../package.json");
module.exports = [
// 打包成 esm 的配置项
{
input: "src/**/*",
output: [
{
dir: "esm",
format: "esm",
sourceMap: false,
},
],
// 打包时排除 peerDenpendencies 里面的依赖
enternal: Object.keys(pkg.peerDenpendencies || {}),
plugins: [
// 自动清除生成代码
clear({ target: "esm" }),
// 自动注入代码
autoAdd({
// 匹配这种 src/myComponent/index.tsx
include: [/src/(((?!/).)+?)/index.tsx/gi],
}),
// 多入口
multiInput(),
// 解析 ts
typescript({
path: path.resolve(__dirname, "./tsconfig.esm.json"),
}),
peerDepExternal(),
resolve(), // 处理 node_modules
commonjs(), // 处理 commonjs
filesize(), // 处理包体积
postcss({
minimize: true,
sourceMap: true,
extensions: [".less", ".css"],
use: ["less"],
}),
// 文件声明
alias({
entries: {
"@": path.resolve(__dirname, "../src"),
},
}),
],
},
// 打包成 umd 的配置项
{
input: "src/index.tsx",
output: [
{
dir: "dist",
format: "umd",
exports: "named",
name: pkg.name,
sourceMap: true,
},
],
// 打包时排除 peerDenpendencies 里面的依赖
enternal: Object.keys(pkg.peerDenpendencies || {}),
plugins: [
// 自动清除生成代码
clear({ target: "dist" }),
// 自动注入代码
autoAdd({
// 匹配这种 src/myComponent/index.tsx
include: [/src/(((?!/).)+?)/index.tsx/gi],
}),
// 多入口
multiInput(),
// 解析 ts
typescript({
path: path.resolve(__dirname, "./tsconfig.dist.json"),
}),
peerDepExternal(),
resolve(), // 处理 node_modules
commonjs(), // 处理 commonjs
filesize(), // 处理包体积
postcss({
minimize: true,
sourceMap: true,
extensions: [".less", ".css"],
use: ["less"],
}),
// 文件声明
alias({
entries: {
"@": path.resolve(__dirname, "../src"),
},
}),
],
},
];
- 改造
package.json
加上打包命令
javascript
{
....
scripts: {
"test": "echo "Error: no test specified" && exit 1",
"build": "rollup --config ./scripts/rollup.config.js" // ++++++
}
}
- 新建
index.tsx
javascript
touch src/index.tsx
// 写如下代码:
import Button from "./button";
import Card from "./card";
export { Button, Card };
- 运行
pnpm build
,进行打包
二、改造一些,给内部项目使用
- 改造
package.json
,看变更
- 内部项目安装它,进入
apps/react-master
内,运行安装命令
sql
pnpm add @hzq/react-x-components
- 在项目中引入并使用
javascript
import { Button } from "@hzq/react-x-components";
// ....
<Button>提问111</Button>
效果如下:
三、组件使用流程讲解
若需要实时使用组件,则可以如下处理:
1、改造组件项目的package.json
,然后运行pnpm dev
这样就是边开发边打包,能达到"实时"
javascript
{
.......
"main": "src/index.tsx", // ++++++
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
"build": "rollup --config ./scripts/rollup.config.js",
"dev": "rollup --config ./scripts/rollup.config.js -w" // ++++++
},
......
}
2、改造组件项目的package.json
,这样直接使用组件源码,也能达到"实时"
json
{
.......
"main": "src/index.tsx", // ++++++
.......
}
5、插件封装
微内核架构
抽象不依赖实现
提供一个内核(core/engine),内核本身具有很强的扩展性,但内核不会因为有了一个扩展,就去修改内核自身
外部可通过插件(plugin)的形式往内核注入,然后内核去驱动插件的执行(plugins.run())
前端界常见的有:Webpack、Babel 等
核心伪代码:
typescript
const events = {};
const typeEnum = ["create", "mount", "distory"];
class Core {
context = {};
defaultOpts = {
beforeCreate() {
console.log("beforeCreate");
},
created() {
console.log("created");
},
beforeMount() {
console.log("beforeMount");
},
mounted() {
console.log("mounted");
},
beforeDistory() {
console.log("beforeDistory");
},
distoryed() {
console.log("distoryed");
},
};
constructor(opts) {
this.opts = { ...this.defaultOpts, ...opts };
}
addPlugin({ type, run }) {
events[type] = events[type] || [];
events[type].push(run);
}
pluginsRun(type) {
events[type].forEach((fn) => fn(this.context));
}
start() {
this.opts.beforeCreate(); // 模拟生命周期
this.pluginsRun("create");
this.opts.created(); // 模拟生命周期
this.opts.beforeMount(); // 模拟生命周期
this.pluginsRun("mount");
this.opts.mounted(); // 模拟生命周期
}
end() {
this.opts.beforeDistory(); // 模拟生命周期
this.pluginsRun("distory");
this.opts.distoryd(); // 模拟生命周期
}
}
export default Core;
// 用户使用
const core = new Core({
beforeCreate() {
console.log("[ this is my beforeCreate] >");
},
mounted() {
console.log("[ this is my mounted] >");
},
// ......
});
core.addPlugin({
type: "create",
run(context) {
console.log("[ create run 1 ] >");
context.xxxx = "xxxx";
},
});
core.addPlugin({
type: "create",
run(context) {
console.log("[ create run 2 ] >");
console.log("[ create run 2 context ] >", context);
context.yyyy = "yyyy";
},
});
core.addPlugin({
type: "mount",
run(context) {
console.log("[ mount context ] >", context);
},
});
core.start();
Webpack:zipPlugin 插件封装(apps/react-master 下)
功能描述:将打包的东西压缩成一个包
- 安装前置依赖
csharp
pnpm add jszip webpack-sources -D
- 新建对应文件
bash
touch zipPlugin.js
- 编写代码
javascript
const JSzip = require("jszip"); // 引入jszip
// RawSource 是其中一种 "源码"("sources") 类型,
const { RawSource } = require("webpack-sources");
// 自定义插件 官方文档:https://webpack.docschina.org/contribute/writing-a-plugin/#creating-a-plugin
class ZipPlugin {
static defaultOptions = {
outputFile: "dist.zip",
};
constructor(options) {
this.options = { ...ZipPlugin.defaultOptions, ...options };
}
// 在插件函数的 prototype 上定义一个 `apply` 方法,以 compiler 为参数。
apply(compiler) {
const pluginName = ZipPlugin.name;
compiler.hooks.emit.tapAsync(pluginName, (compilation, callback) => {
const zip = new JSzip();
// 遍历所有资源
Object.keys(compilation.assets).forEach((filename) => {
const source = compilation.assets[filename].source();
zip.file(filename, source); // 添加文件到 zip
});
// generateAsync:生成 zip 文件
zip.generateAsync({ type: "nodebuffer" }).then((content) => {
// 向 compilation 添加新的资源,这样 webpack 就会自动生成并输出到 outputFile 目录
compilation.emitAsset(this.options.outputFile, new RawSource(content));
callback(); // 告诉 webpack 插件已经完成
});
});
}
}
module.exports = { ZipPlugin };
- 更改
react-master/scripts/webpack.prod.js
(看变更)
- 运行
pnpm build
,生成的dist
里面就会有个dist.zip
,解压后就是整个dist
Babel:consolePlugin 插件封装(apps/react-master 下)
功能描述:在调试模式下,将 console.log() 丰富,支持打印具体位置:行数、列数
- 安装前置依赖
sql
pnpm add @babel/generator -D
- 新建对应文件
bash
touch consolePlugin.js
- 编写代码
javascript
const generator = require("@babel/generator").default;
// Babel 自定义插件官方文档:https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md#toc-writing-your-first-babel-plugin
function consolePlugin({ types }) {
return {
visitor: {
CallExpression(path) {
const name = generator(path.node.callee).code;
if (["console.log", "console.info", "console.error"].includes(name)) {
const { line, column } = path.node.loc.start;
path.node.arguments.unshift(
types.stringLiteral(`fliepath: ${line}:${column}`),
);
}
},
},
};
}
module.exports = consolePlugin;
- 使用插件
json
{
"presets": [
"@babel/preset-react", // 解析 react
"@babel/preset-typescript" // 解析 typescript
],
"plugins": ["./consolePlugin.js"] // ++++++
}
- 重新启动项目
pnpm start
,写一个console.log(xx)
并打印出来,可以看有行数、列数了
Postcss 插件:themePlugin(apps/react-master 下)
主要功能:实现网站主题色切换
基本功能
先展示基于tailwindcss
实现的最朴实的颜色切换
- 更改
tailwind.config.js
css
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{tsx,ts,jsx,js}"],
theme: {
extend: {},
colors: { // +++ 下面的为新增的 +++
// 将默认的颜色改为变量
white: "var(--color-white)",
black: "var(--color-black)",
gray: {
50: "var(--color-gray-50)",
100: "var(--color-gray-100)",
200: "var(--color-gray-200)",
300: "var(--color-gray-300)",
400: "var(--color-gray-400)",
500: "var(--color-gray-500)",
600: "var(--color-gray-600)",
700: "var(--color-gray-700)",
800: "var(--color-gray-800)",
900: "var(--color-gray-900)",
950: "var(--color-gray-950)",
},
},
},
plugins: [],
};
- 更改
index.less
css
// 全局的东西
// tailwind 配置
@tailwind base;
@tailwind components;
@tailwind utilities;
// 明亮模式
html {
--color-white: #fff;
--color-black: #000;
--color-gray-50: #f9fafb;
--color-gray-100: #f3f4f6;
--color-gray-200: #e5e7eb;
--color-gray-300: #d1d5db;
--color-gray-400: #9ca3af;
--color-gray-500: #6b7280;
--color-gray-600: #4b5563;
--color-gray-700: #374151;
--color-gray-800: #1f2937;
--color-gray-900: #111827;
--color-gray-950: #030712;
}
// 深色模式
html[data-theme="dark"] {
--color-white: #000;
--color-black: #fff;
--color-gray-950: #f9fafb;
--color-gray-900: #f3f4f6;
--color-gray-800: #e5e7eb;
--color-gray-700: #d1d5db;
--color-gray-600: #9ca3af;
--color-gray-500: #6b7280;
--color-gray-400: #4b5563;
--color-gray-300: #374151;
--color-gray-200: #1f2937;
--color-gray-100: #111827;
--color-gray-50: #030712;
}
- 更改
react-master/src/components/navigation/index.tsx
,增加一个主题切换操作,看 mr - 切换之前
- 切换之后
基于插件
基本功能完成后,会发现我们的主题变量需要手动去维护,当主题多了就麻烦了,现在就可以写插件来处理
色卡:主题需要设计时提供一系列对应的颜色值,明亮、暗黑、xx 一套
在前端可以这样去维护:
- 新建色卡文件,就是之前
index.less
里面写 css 代码,改成 js 代码而已
css
touch themeGroup.js
// 写如下代码
const themeGroup = {
light: {
"--color-white": "#fff",
"--color-black": "#000",
"--color-gray-50": "#f9fafb",
"--color-gray-100": "#f3f4f6",
"--color-gray-200": "#e5e7eb",
"--color-gray-300": "#d1d5db",
"--color-gray-400": "#9ca3af",
"--color-gray-500": "#6b7280",
"--color-gray-600": "#4b5563",
"--color-gray-700": "#374151",
"--color-gray-800": "#1f2937",
"--color-gray-900": "#111827",
"--color-gray-950": "#030712",
},
dark: {
"--color-white": "#000",
"--color-black": "#fff",
"--color-gray-950": "#f9fafb",
"--color-gray-900": "#f3f4f6",
"--color-gray-800": "#e5e7eb",
"--color-gray-700": "#d1d5db",
"--color-gray-600": "#9ca3af",
"--color-gray-500": "#6b7280",
"--color-gray-400": "#4b5563",
"--color-gray-300": "#374151",
"--color-gray-200": "#1f2937",
"--color-gray-100": "#111827",
"--color-gray-50": "#030712",
},
green: {
"--color-white": "#14532d",
"--color-black": "#f0fdf4",
"--color-gray-50": "#f0fdf4",
"--color-gray-100": "#dcfce7",
"--color-gray-200": "#bbf7d0",
"--color-gray-300": "#86efac",
"--color-gray-400": "#4ade80",
"--color-gray-500": "#22c55e",
"--color-gray-600": "#16a34a",
"--color-gray-700": "#15803d",
"--color-gray-800": "#166534",
"--color-gray-900": "#14532d",
"--color-gray-950": "#052e16",
},
};
module.exports = { themeGroup, defaultTheme: "green" };
- 删除
index.less
里面的颜色配置 - 更改
tailwind.config.js
,将var
改为hzqTheme
,最后实现效果是将color:hzqTheme(--color-white)
通过插件变为color:#fff
,这样就完成了插件的功能
css
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{tsx,ts,jsx,js}"],
theme: {
extend: {},
colors: {
// 将默认的颜色改为变量
white: "hzqTheme(--color-white)",
black: "hzqTheme(--color-black)",
gray: {
50: "hzqTheme(--color-gray-50)",
100: "hzqTheme(--color-gray-100)",
200: "hzqTheme(--color-gray-200)",
300: "hzqTheme(--color-gray-300)",
400: "hzqTheme(--color-gray-400)",
500: "hzqTheme(--color-gray-500)",
600: "hzqTheme(--color-gray-600)",
700: "hzqTheme(--color-gray-700)",
800: "hzqTheme(--color-gray-800)",
900: "hzqTheme(--color-gray-900)",
950: "hzqTheme(--color-gray-950)",
},
},
},
plugins: [],
};
- 安装前置依赖:用于修改 css 代码的库
kotlin
pnpm add postcss-nested@^6.0.1 postcss-nesting@^10.2.0
- 新建插件文件
themePlugin.js
javascript
touch themePlugin.js
// 写入代码:
// eslint-disable-next-line @typescript-eslint/no-var-requires
const postcss = require("postcss");
module.exports = postcss.plugin("postcss-theme", (options) => {
const defalutOpts = {
functionName: "hzqTheme",
themeGroup: {},
defaultTheme: "light",
themeSelector: 'html[data-theme="$_$"]',
nestingPlugin: null,
};
// 合并参数
options = Object.assign({}, defalutOpts, options);
const getColorByThemeGroup = (color, theme) => {
return options.themeGroup[theme][color];
};
// 正则:获取 hzqTheme(--color-white) 括号中的值:--color-white
const regColorValue = new RegExp(
`\b${options.functionName}\(([^)]+)\)`,
"g",
);
// 插件的入口函数
return (style, result) => {
const hasPlugin = (name) =>
name.replace(/^postcss-/, "") === options.nestingPlugin ||
result.processor.plugins.some((p) => p.postcssPlugin === name);
// 获取 css 属性值,替换掉 hzqTheme(--color-gray-200)
const getColorValue = (value, theme) => {
return value.replace(regColorValue, (match, color) => {
// match: hzqTheme(--color-gray-200)
// color: --color-gray-200
return getColorByThemeGroup(color, theme);
});
};
style.walkDecls((decl) => {
// decl 是每个 css 属性的对象,height: 10px 的 css ast { prop: "height", value: "10px" }
// 每个 css 属性的具体值:height: 10px;
const value = decl.value;
if (!value || !regColorValue.test(value)) {
// 如果没有匹配到,直接返回
return;
}
// 说明有匹配到值
try {
let defaultTheme;
Object.keys(options.themeGroup).forEach((key) => {
// 处理各种模式
const themeColor = getColorValue(value, key);
const themeSelector = options.themeSelector.replace("$_$", key);
let themeRule;
// 使用 nest 插件,生成 dark 的规则:html[data-theme="dark"] {...}
if (hasPlugin("postcss-nesting")) {
themeRule = postcss.atRule({
name: "nest",
params: `${themeSelector} &`,
});
} else if (hasPlugin("postcss-nested")) {
themeRule = postcss.rule({
params: `${themeSelector} &`,
});
} else {
throw new Error("请安装 postcss-nesting 或者 postcss-nested 插件");
}
const themeDecl = decl.clone({ value: themeColor });
if (themeRule) {
themeRule.append(themeDecl);
decl.after(themeRule);
}
if (key === options.defaultTheme) {
defaultTheme = themeDecl;
}
});
// 处理为默认模式
if (defaultTheme) decl.replaceWith(defaultTheme);
} catch (error) {
decl.warn(result, error);
}
});
};
});
- 更改
.postcssrc.js
文件,加入对应插件
javascript
const { themeGroup, defaultTheme } = require("./themeGroup"); // ++++++
module.exports = {
plugins: [
"autoprefixer", // 自动添加浏览器前缀
"tailwindcss",
"postcss-nested", // ++++++
"postcss-nesting", // ++++++
require("./themePlugin")({ themeGroup, defaultTheme }), // ++++++
],
};
- 重启项目,默认为绿色,切换后为黑色,再切换为白色
打包之后,本地启动index.html
也是一样的效果,这样就完成了主题切换的插件,本质是帮我们注入所有的主题 css 代码