开发UI组件库-从0开始搭建 React + TypeScript(一)

之前做React项目 都是 直接使用官方提供的脚手架create-react-app, 今天自己使用webpack 来搭建自己的脚手架, 可以在项目中使用React + TypeScript 来完成项目, 并写出自己的UI 组件库, React Hooks + TypeScript 风格

开始准备

  • 下面是我电脑上的环境
node -v  // v12.13.0
npm -v  // 6.12.0
yarn -v  // 1.19.1
  • 编辑器 VS Code
  • 命令行工具 cmder
  • 远程仓库 github
  • 浏览器 Chrome

如何搭建

  1. 我们先在 github创建一个远程仓库, 取个名字叫ts-demo clone到本地, 做代码管理

    git clone 你的ssh地址
    
  2. 然后初始化我们的项目, 生成package.json

    npm init (-y)
    or
    yarn init (-y)
    
  3. 安装 webpack

    • webpack 是一个现代 JavaScript 应用程序的静态模块打包器
    • webapck安装
    // 使用 webpack 4+ 版本
    yarn add webpack webpack-cli --dev  // dev 安装到开发者依赖
    
    // package.json
    {
      "name": "ts-demo",
      "version": "1.0.0",
      "main": "index.js",
      "license": "MIT",
      "devDependencies": {
        "webpack": "^4.43.0",
        "webpack-cli": "^3.3.11"
      }
    }
    
  4. 安装好了 webpack, 然后我们创建 lib/index.tsx, 测试我们下载的 webpack

    // index.tsx
    console.log('hi')  // 测试能否编译 tsx 文件
    
  5. 在这之前, 需要创建webpack.config.js配置文件, 配置我们需要的东西

    module.exports = {
      // 应用程序的起点入口
      entry: './lib/index.tsx',
    }
    
    • 但上面的配置并不能编译 tsx, 下面的配置如何编译 tsx

      • loader

      • 使用到 webpack loader : loader 用于对模块的源代码进行转换

      module.exports = {
        entry: './lib/index.tsx',
        // 模块
        module: {
          // 规则
          rules: [
            {
              test: /\.tsx?$/,
              loader: 'awesome-typescript-loader'
            }
          ]
        }
      }
      
      // 使用到 ts 的 loader 加载器, 需要安装
      // 也可以使用 ts-loader, 二选一
      yarn add awesome-typescript-loader --dev
      or
      yarn add ts-loader --dev
      
    • 配置输出路径, 打包后, 管理我们的输出

      1. 前端没有 包管理 工具
      2. 有了 require.js,  define(fn)
      3. AMD 规范 ==>在浏览器使用
      4. Node.js 社区, CMD 规范  module.exports = {}
      5. UMD(统一的模块定义)  ==> if AMD else CMD
      
      const path = require('path')
      module.exports = {
        entry: './lib/index.tsx',
        // 输出
        output: {
          path: path.resolve(__dirname, 'dist'),
          library: 'yui',
          libraryTarget: 'umd'
        },
        module: {
          rules: [
            {
              test: /\.tsx?$/,
              loader: 'awesome-typescript-loader'
            }
          ]
        }
      }
      
  1. 因为我们使用到.tsxts 文件, 安装typescript

    yarn add typescript --dev
    
  2. 设置tsconfig.json , tsconfig.json 配置含义

    • tsconfig.json文件中指定了用来编译这个项目的根文件和编译选项
      • 不带任何输入文件的情况下调用tsc,编译器会从当前目录开始去查找tsconfig.json文件,逐级向上搜索父目录。
      • 不带任何输入文件的情况下调用tsc,且使用命令行参数--project(或-p)指定一个包含tsconfig.json文件的目录。
    // tsconfig.json 配置
    {
      "compilerOptions": {
        "outDir": "dist", // 指定输出目录
        "declaration": true, // 生成声明文件,开启后会自动生成声明文件
        "baseUrl": "", // 解析非相对模块的基地址,默认是当前目录
        "module": "esnext", // 生成代码的模板标准
        "target": "es5", // 目标语言的版本
        "lib": ["es6", "dom"], // TS需要引用的库,即声明文件,es5 默认引用dom、es5、scripthost,如需要使用es的高级版本特性,通常都需要配置
        "sourceMap": true, // 生成目标文件的sourceMap文件
        "jsx": "react",
        "moduleResolution": "node", // 模块解析策略,ts默认用node的解析策略,即相对的方式导入
        "rootDir": ".",
        "noImplicitReturns": true, //每个分支都会有返回值
        "noImplicitThis": true, // 不允许this有隐式的any类型
        "noImplicitAny": true, // 不允许隐式的any类型
        "importHelpers": true, // 通过tslib引入helper函数,文件必须是模块
        "strictNullChecks": true, // 不允许把null、undefined赋值给其他类型的变量
        "esModuleInterop": true, // 允许export=导出,由import from 导入
        "noUnusedLocals": true // 检查只声明、未使用的局部变量(只提示不报错)
      },
      "includes": ["lib/**/*"],
      "exclude": ["node_modules", "build", "dist"]
    }
    
  1. 到现在为止, 其实现在我们并没有加多少东西, 先看看我们的配置:

    • 安装 webpack webpack-cl typescript awesome-typescript-loader
    • 配置 webpack.config.js tsconfig.json lib/index.tsx
    // 自行解决 各种报错
    npx webpack  // 编译 可以打包 dist了
    
    • 会看到生成一个 dist 目录, dist/index.js, 被webpack编译
  2. 我们先简单解释一下安装时 --save-dev --save 的区别

    npm install --save-dev  // 只给程序员用 + dev
    npm install --save  // 用户也用, 默认
    
    --save 简写 -S
    -dev 简写 -D
    
    yarn add
    yarn add --dev
    
  3. 安装webpack-dev-server

    • webpack-dev-server主要是启动了一个使用expressHttp服务器
    npm install --save-dev webpack-dev-server
    or
    yarn add webpack-dev-server --dev
    
    ==> 
    
    // 执行 npx webpack-dev-server
    
    • 我们打开 localhost:8080, 因为没有html 文件, 我们可以打开 dist/index.js 在页面上, 可以看到我们的 console.log('hi')
  4. 上一步已经可以开启一个localhost:8080 的服务器了, 不过我们还需要创建 html

    <!-- index.html -->
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <!-- webpack 配置 title -->
      <title><%= htmlWebpackPlugin.options.title %></title>
    </head>
    <body></body>
    </html>
    
    // 我们做到的不是自己插入 js 路径, 而是 webpack 自动添加
    
  5. 引入插件 HtmlWebpackPlugin

    • HtmlWebpackPlugin简化了HTML文件的创建,以便为你的webpack包提供服务。这对于在文件名中包含每次会随着编译而发生变化哈希的 webpack bundle 尤其有用。 你可以让插件为你生成一个HTML文件
    // 补充一下 webpack 配置
    const path = require('path')
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    
    module.exports = {
      entry: './lib/index.tsx',
      output: {
        path: path.resolve(__dirname, 'dist'),
        library: 'yui',
        libraryTarget: 'umd'
      },
      module: {
        rules: [
          {
            test: /\.tsx?$/,
            loader: 'awesome-typescript-loader'
          }
        ]
      },
      // 插件
      plugins: [
        new HtmlWebpackPlugin({
          title: 'Hello Webpack'
          template: 'index.html'
        })
      ]
    }
    
    // lib/index.tsx
    const div = document.createElement("div");
    div.innerText = "Hello World";
    document.body.appendChild(div);
    
  6. 好了, 虽然写了很多, 但我们没有做多少东西, 我们可以打包, 可以开启服务了, 可以在 浏览器看到 Hello World

    npx webpack
    npx webpack-dev-server
    
    // 在 package.json 里
    
    "scripts": {
      "start": "webpack-dev-server",
      "build": "webpack"
    },
    
  7. 上面我们可以开启服务了, 但还有许多细节去完善, 现在我们在页面上写react, 需要安装一些

    yarn add react react-dom
    
    // 声明文件 用 typescript 使用
    yarn add @types/react @types/react-dom
    
  8. 我们创建个页面, 测试一下当前页面

    // lib/button.tsx
    import React from 'react'
    const Button = () => {
      return <div>按钮</div>
    }
    
    // lib/index.tsx
    import React from 'react'
    import ReactDOM from 'react-dom';
    import Button from  "./button"
    
    ReactDOM.render(<Button></Button>, document.body)
    
    • 在上面运行时, 打包会找不到 .tsx 结尾的文件
    // webpack.config.tsx
    
    module.exports = {
      // 加载项配置
    +  resolve: {
    +    extensions: ['.js', '.jsx', '.ts', '.tsx']
    +  },
    
    }
    
  9. 现在我们可以再页面没上看到按钮, 现在把我们webpack.config.js的代码全部看一下

    const path = require("path")
    const HtmlWebpackPlugin = require("html-webpack-plugin")
    
    module.exports = {
      mode: "production",
      entry: {
        index: "./lib/index.tsx",
      },
      output: {
        path: path.resolve(__dirname, "dist"),
        library: "yui",
        libraryTarget: "umd",
      },
      resolve: {
        extensions: [".js", ".jsx", ".ts", ".tsx"],
      },
      module: {
        rules: [
          {
            test: /\.tsx?$/,
            loader: "awesome-typescript-loader",
          },
        ],
      },
      plugins: [
        new HtmlWebpackPlugin({
          title: "Webpack",
          template: "index.html",
        }),
      ],
    }
    
    • mode: 'production' 为生产环境, 执行npm start, 这时会有一个 warning

文件过大
   - ​  `Webpack` 可以配置` externals` 来将依赖的库指向全局变量,从而不再打包这个库
   - ​  `externals` 配置选项提供了「从输出的 bundle 中排除依赖」的方法
   - ​  [externals](https://webpack.docschina.org/configuration/externals/)
const path = require("path")
const HtmlWebpackPlugin = require("html-webpack-plugin")

module.exports = {
  mode: "production",
  entry: {
    index: "./lib/index.tsx",
  },
  output: {
    path: path.resolve(__dirname, "dist"),
    library: "yui",
    libraryTarget: "umd",
  },
  resolve: {
    extensions: [".js", ".jsx", ".ts", ".tsx"],
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        loader: "awesome-typescript-loader",
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: "Webpack",
      template: "index.html",
    }),
  ],
  // 不属于内部的库, 外部的
  externals: {
    react: {
      commonjs: "react",
      commonjs2: "react",
      amd: "react",
      root: "React",
    },
    "react-dom": {
      commonjs: "react",
      commonjs2: "react",
      amd: "react",
      root: "React",
    },
  },
}
  1. 现在我们的代码比较全了, 但mode 有开发环境和生产环境, 我们在不同的环境配置不同的webpack, 把之前的一个文件拆成三个文件

    // webpack.config.js
    const path = require("path");
    module.exports = {
      // 1. 影响提示, 2. 文件大小 development/production
      // mode: "production",
      // 入口是 tsx, 但程序不认识 jsx, 配置 rules
      entry: {
        index: "./lib/index.tsx"
      },
      // 输出目录
      output: {
        // 因为不同的操作系统, 路径不一样, 所以使用 __dirname, 当前路径
        path: path.resolve(__dirname, "dist/lib"),
        // 库的name
        library: "YUI",
        // 库最后导出的格式
        libraryTarget: "umd"
      },
    
      // 配置 import 引入
      resolve: {
        extensions: [".ts", ".tsx", ".js", ".jsx"]
      },
      module: {
        rules: [
          // 配置 ts tsx
          {
            test: /\.tsx?$/,
            loader: "awesome-typescript-loader"
          },
        ]
      }
    };
    
    // webpack.config.dev.js
    const base = require('./webpack.config');
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    
    module.exports = Object.assign({}, base, {
      mode: 'development',
      plugins: [
        new HtmlWebpackPlugin({
          title: 'YUI',
          template: 'index.html',
        }),
      ],
    });
    
    // webpack.config.prod.js
    const base = require('./webpack.config')
    module.exports = Object.assign({}, base, {
      mode: "production",
      // 不属于内部的库, 外部的
      externals: {
        react: {
          commonjs: 'react',
          commonjs2: 'react',
          amd: 'react',
          root: 'React'
        },
        'react-dom': {
          commonjs: 'react',
          commonjs2: 'react',
          amd: 'react',
          root: 'React'
        }
      }
    });
    
    // 修改我们 package.json 里面 script 的配置
    "scripts": {
      "start": "webpack-dev-server --config webpack.config.dev.js",
      "build": "webpack --config webpack.config.prod.js",
    },
    
  2. 配置测试Jest, 因为我们不使用create react app, 这个比较麻烦, 自己搜着尝试啊

yarn add --dev jest babel-jest @babel/preset-env @babel/preset-react react-test-renderer
// 创建 babel.config.js  ==> .babelrc
module.exports = {
  presets: ['@babel/preset-env', '@babel/preset-react'],
};
// 1. 添加命令 test
"test": "jest --config=jest.config.js",

// 2. 添加 jest.config.js
// https://jestjs.io/docs/en/configuration.html

module.exports = {
  verbose: true,
  clearMocks: false,

  collectCoverage: true,
  // 测试那些, 不测试哪些
  collectCoverageFrom: ["lib/**/*.{js,jsx,ts,tsx}", "!**/node_modules/**"],
  // 生成的报告放在那里
  coverageDirectory: "coverage",
  moduleFileExtensions: ["js", "jsx", "ts", "tsx"],
  moduleDirectories: ["node_modules"],
  moduleNameMapper: {
    "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
      "<rootDir>/test/__mocks__/file-mock.js",
    "\\.(css|less|sass|scss)$": "<rootDir>/test/__mocks__/object-mock.js"
  },
  // 测试文件在哪里
  testMatch: ["<rootDir>/**/__tests__/**/*.unit.(js|jsx|ts|tsx)"],
  transform: {
    "^.+unit\\.(js|jsx)$": "babel-jest",
    "^.+\\.(ts|tsx)$": "ts-jest"
  },
  setupFilesAfterEnv: ["<rootDir>test/setupTests.js"]
};
// 执行 yarn test, 根据报错, 

1. yarn add --dev ts-jest
2. test/setupTests.js
3. 创建对应的 **/__tests__/**/*.unit.(js|jsx|ts|tsx)
4. yarn add @types/jest
// 根据
testMatch: ["<rootDir>/**/__tests__/**/*.unit.(js|jsx|ts|tsx)"],

// 我们创建测试文件, 先随便测试看看能不能成功
// lib/__tests__/hello.unit.tsx

describe('Test', () => {
  it('Hello', () => {
    expect(1).toEqual(2)  // 报错
  });
});

// 因为我们使用的是 .tsx, 提示安装 @types/jest  yarn add @types/jest --dev 

// 上面的示例比较简单, 我们来一个正确的
// lib/__tests__/hello.unit.tsx

function sum(a: number, b: number): number {
return a + b
}

describe('Test', () => {
  it('sum', () => {
    expect(sum(1, 2)).toEqual(3)
  });
});

好的, 我们的测试可以成功了, 先这样, 后面我们写组件时, 再具体的问题具体完善

  1. 最后, 我们完善一个 script 命令问题
  • 使用 cross-env
    • 运行跨平台设置和使用环境变量的脚本
    yarn add --dev cross-env
    

总结

上面的东西基本上够我们使用 ts + react 开发我们的组件了, 后面需要什么我们在对应的配置, 虽然写了很多, 但其实配置的并不多, 主要我们还是多搜索, 多尝试, 现在我们看看我们有哪些目录了

文件目录

基本配置现在这些可以使用 React + TypeSctipt 开发了, 赶紧测试一下吧

// 安装的依赖
  "devDependencies": {
    "@babel/preset-env": "^7.7.7",
    "@babel/preset-react": "^7.7.4",
    "awesome-typescript-loader": "^5.2.1",
    "babel-jest": "^24.9.0",
    "html-webpack-plugin": "^3.2.0",
    "jest": "^24.9.0",
    "react-test-renderer": "^16.12.0",
    "ts-jest": "^24.2.0",
    "typescript": "^3.7.4",
    "webpack": "^4.41.4",
    "webpack-cli": "^3.3.10",
    "webpack-dev-server": "^3.10.1"
  },
  "dependencies": {
    "@types/jest": "^24.0.24",
    "@types/react": "^16.9.17",
    "@types/react-dom": "^16.9.4",
    "react": "^16.12.0",
    "react-dom": "^16.12.0"
  }

下期文章

  • react hook 全解
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345