開發(fā)UI組件庫-從0開始搭建 React + TypeScript(一)

之前做React項目 都是 直接使用官方提供的腳手架create-react-app, 今天自己使用webpack 來搭建自己的腳手架, 可以在項目中使用React + TypeScript 來完成項目, 并寫出自己的UI 組件庫, React Hooks + TypeScript 風格

開始準備

  • 下面是我電腦上的環(huán)境
node -v  // v12.13.0
npm -v  // 6.12.0
yarn -v  // 1.19.1
  • 編輯器 VS Code
  • 命令行工具 cmder
  • 遠程倉庫 github
  • 瀏覽器 Chrome

如何搭建

  1. 我們先在 github創(chuàng)建一個遠程倉庫, 取個名字叫ts-demo clone到本地, 做代碼管理

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

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

    • webpack 是一個現(xiàn)代 JavaScript 應用程序的靜態(tài)模塊打包器
    • webapck安裝
    // 使用 webpack 4+ 版本
    yarn add webpack webpack-cli --dev  // dev 安裝到開發(fā)者依賴
    
    // 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, 然后我們創(chuàng)建 lib/index.tsx, 測試我們下載的 webpack

    // index.tsx
    console.log('hi')  // 測試能否編譯 tsx 文件
    
  5. 在這之前, 需要創(chuàng)建webpack.config.js配置文件, 配置我們需要的東西

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

      • loader

      • 使用到 webpack loader : loader 用于對模塊的源代碼進行轉換

      module.exports = {
        entry: './lib/index.tsx',
        // 模塊
        module: {
          // 規(guī)則
          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 規(guī)范 ==>在瀏覽器使用
      4. Node.js 社區(qū), CMD 規(guī)范  module.exports = {}
      5. UMD(統(tǒng)一的模塊定義)  ==> 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,且使用命令行參數(shù)--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函數(shù)慌核,文件必須是模塊
        "strictNullChecks": true, // 不允許把null距境、undefined賦值給其他類型的變量
        "esModuleInterop": true, // 允許export=導出,由import from 導入
        "noUnusedLocals": true // 檢查只聲明垮卓、未使用的局部變量(只提示不報錯)
      },
      "includes": ["lib/**/*"],
      "exclude": ["node_modules", "build", "dist"]
    }
    
  1. 到現(xiàn)在為止, 其實現(xiàn)在我們并沒有加多少東西, 先看看我們的配置:

    • 安裝 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 的區(qū)別

    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
    
    ==> 
    
    // 執(zhí)行 npx webpack-dev-server
    
    • 我們打開 localhost:8080, 因為沒有html 文件, 我們可以打開 dist/index.js 在頁面上, 可以看到我們的 console.log('hi')
  4. 上一步已經可以開啟一個localhost:8080 的服務器了, 不過我們還需要創(chuàng)建 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文件的創(chuàng)建垫桂,以便為你的webpack包提供服務。這對于在文件名中包含每次會隨著編譯而發(fā)生變化哈希的 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. 上面我們可以開啟服務了, 但還有許多細節(jié)去完善, 現(xiàn)在我們在頁面上寫react, 需要安裝一些

    yarn add react react-dom
    
    // 聲明文件 用 typescript 使用
    yarn add @types/react @types/react-dom
    
  8. 我們創(chuàng)建個頁面, 測試一下當前頁面

    // 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. 現(xiàn)在我們可以再頁面沒上看到按鈕, 現(xiàn)在把我們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' 為生產環(huán)境, 執(zhí)行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. 現(xiàn)在我們的代碼比較全了, 但mode 有開發(fā)環(huán)境和生產環(huán)境, 我們在不同的環(huán)境配置不同的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: {
        // 因為不同的操作系統(tǒng), 路徑不一樣, 所以使用 __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
// 創(chuàng)建 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"]
};
// 執(zhí)行 yarn test, 根據(jù)報錯, 

1. yarn add --dev ts-jest
2. test/setupTests.js
3. 創(chuàng)建對應的 **/__tests__/**/*.unit.(js|jsx|ts|tsx)
4. yarn add @types/jest
// 根據(jù)
testMatch: ["<rootDir>/**/__tests__/**/*.unit.(js|jsx|ts|tsx)"],

// 我們創(chuàng)建測試文件, 先隨便測試看看能不能成功
// 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
    • 運行跨平臺設置和使用環(huán)境變量的腳本
    yarn add --dev cross-env
    

總結

上面的東西基本上夠我們使用 ts + react 開發(fā)我們的組件了, 后面需要什么我們在對應的配置, 雖然寫了很多, 但其實配置的并不多, 主要我們還是多搜索, 多嘗試, 現(xiàn)在我們看看我們有哪些目錄了

文件目錄

基本配置現(xiàn)在這些可以使用 React + TypeSctipt 開發(fā)了, 趕緊測試一下吧

// 安裝的依賴
  "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 全解
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市钾怔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蒙挑,老刑警劉巖宗侦,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異忆蚀,居然都是意外死亡矾利,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門馋袜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來男旗,“玉大人,你說我怎么就攤上這事欣鳖〔旎剩” “怎么了?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵泽台,是天一觀的道長什荣。 經常有香客問我,道長怀酷,這世上最難降的妖魔是什么稻爬? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮蜕依,結果婚禮上桅锄,老公的妹妹穿的比我還像新娘琉雳。我一直安慰自己,他們只是感情好友瘤,可當我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布咐吼。 她就那樣靜靜地躺著,像睡著了一般商佑。 火紅的嫁衣襯著肌膚如雪锯茄。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天茶没,我揣著相機與錄音肌幽,去河邊找鬼。 笑死抓半,一個胖子當著我的面吹牛喂急,可吹牛的內容都是我干的。 我是一名探鬼主播笛求,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼廊移,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了探入?” 一聲冷哼從身側響起狡孔,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蜂嗽,沒想到半個月后苗膝,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡植旧,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年辱揭,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片病附。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡问窃,死狀恐怖,靈堂內的尸體忽然破棺而出完沪,到底是詐尸還是另有隱情域庇,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布丽焊,位于F島的核電站较剃,受9級特大地震影響,放射性物質發(fā)生泄漏技健。R本人自食惡果不足惜写穴,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望雌贱。 院中可真熱鬧啊送,春花似錦偿短、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至篷朵,卻和暖如春勾怒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背声旺。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工笔链, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人腮猖。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓鉴扫,卻偏偏與公主長得像,于是被迫代替她去往敵國和親澈缺。 傳聞我的和親對象是個殘疾皇子坪创,可洞房花燭夜當晚...
    茶點故事閱讀 45,512評論 2 359