開發一個基於react & typescript 的npm ui元件包

大桔子1223發表於2020-02-16

npm 註冊登入

  • 前置條件: 切到對應的npm源
  1. npm logout
  2. npm login
  3. 依次輸入賬號、密碼、郵箱
  4. npm publish (會提示去npm官網驗證郵箱地址)
  • npm 釋出時可能遇到的問題
    1. 源出錯
    2. 包名重複
    3. 每次釋出前要修改package.json的版本號,必須要大於上一次的版本號
  • npm link 本地除錯:為除錯帶來的頻繁發包,可以使用 npm link 將npm包代理到本地除錯,操作步驟:
    1. 進入原始碼目錄執行 npm link
    2. 進入使用目錄即示例程式碼執行 npm link [包名],折後就可以直接在示例程式碼處使用 import xxx from 'xxx' 進行除錯了

webpack ts babel 等打包配置檔案書寫

參照這篇文章寫的挺全面的,只不過它沒有引入typescript

  • 本文寫時 "webpack": "^4.41.6",,下面把主要流程記錄一下最終完成的目錄結構如下所示
    |____babelrc // babel 配置
    |____config  // webpack配置
        ├── webpack.base.js // 公共配置
        ├── webpack.dev.config.js // 開發環境配置
        └── webpack.prod.config.js // 打包釋出環境配置
    |____example // 開發環境除錯目錄
    |____node_modules 
    |____README.md
    |____yarn.lock
    |____public // 開發除錯環境的模板 index.html
    |____.gitignore
    |____package.json
    |____lib // 打包後目錄
    |____tsconfig.json // ts配置
    |____postcss.config.js // postcss配置
    |____src // 元件原始碼
    |____.npmignore // 指定釋出 npm 的時候需要忽略的檔案和資料夾
    複製程式碼
  1. mkdir learnnpm & cd learnnpm & npm init,根據提示依次填入資訊,之後即生成 package.json
  2. 依次安裝依賴
    1. 因為使用webpack進行打包,安裝webpack相關依賴
       主依賴: yarn add webpack webpack-cli webpack-dev-server webpack-merge -D
       相關外掛:clean-webpack-plugin html-webpack-plugin mini-css-extract-plugin
    
    2. 安裝react相關
       yarn add react react-dom 
    
    3. 安裝babel相關
       yarn add @babel/cli @babel/core @babel/preset-env @babel/preset-react babel-loader @babel/plugin-proposal-class-properties @babel/plugin-proposal-object-rest-spread -D
    
    4. 安裝 typescript ts-loader fork-ts-checker-webpack-plugin
    
    5. 安裝css相關
       style-loader css-loader postcss-loader less less-loader
       url-loader file-loader
       autoprefixer
    複製程式碼
    • 完成以上步驟 package.json .babelrc webpack.config.js postcss.config.js相關內容如下
    {
        // ...
        "main": "lib/index.js", // 打包後的入口地址
        "scripts": {
            "start": "webpack-dev-server --config config/webpack.dev.config.js",
            "build": "webpack --config config/webpack.prod.config.js",
            "pub": "npm run build && npm publish" // 釋出 npm
        },
        // ...
        "dependencies": {
            "react": "^16.12.0",
            "react-dom": "^16.12.0"
        },
        "devDependencies": {
            "@babel/cli": "^7.8.4",
            "@babel/core": "^7.8.4",
            "@babel/preset-env": "^7.8.4",
            "@babel/preset-react": "^7.8.3",
            "@babel/plugin-proposal-class-properties": "^7.8.3",
            "@babel/plugin-proposal-object-rest-spread": "^7.8.3",
            "@types/react": "^16.9.19", // ts 需要用的相關庫types 檔案
            "@types/react-dom": "^16.9.5",
            "@types/react-router-dom": "^5.1.3",
            "autoprefixer": "^9.7.4",
            "babel-loader": "^8.0.6",
            "clean-webpack-plugin": "^3.0.0",
            "css-loader": "^3.4.2",
            "fork-ts-checker-webpack-plugin": "^0.5.2", // ts型別校驗webpack外掛
            "html-webpack-plugin": "^3.2.0",
            "less": "^3.11.1",
            "less-loader": "^5.0.0",
            "mini-css-extract-plugin": "^0.9.0", // 抽離css外掛
            "postcss-loader": "^3.0.0",
            "style-loader": "^1.1.3",
            "ts-loader": "^6.2.1",
            "typescript": "^3.7.5",
            "url-loader": "^3.0.0",
            "webpack": "^4.41.6",
            "webpack-cli": "3.3.7",
            "webpack-dev-server": "^3.10.3",
            "webpack-merge": "^4.2.2"
        },
        "browserslist": [ // postcss autoprefixer 用到的配置
            "iOS >= 6",
            "Android >= 4",
            "IE >= 9"
        ]
    }
    複製程式碼
    • .babelrc
    {
        "presets": [
            "@babel/preset-env",
            "@babel/preset-react",
        ],
        "plugins": [
            "@babel/plugin-proposal-class-properties",
            "@babel/plugin-proposal-object-rest-spread"
        ]
    }
    複製程式碼
    • .postcss.config.js
    // postcss 配置參考 https://segmentfault.com/a/1190000008030425
    module.exports = {
        plugins: [
            require('autoprefixer')({ /* ...options */ })
        ]
    }
    複製程式碼
    • webpack.base.js
    const path = require('path');
    
    module.exports = {
        module: {
            rules: [
                {
                    test: /\.(js|jsx)$/,
                    use: "babel-loader",
                    exclude: /node_modules/
                },
                {
                    test: /\.(ts|tsx)$/,
                    use: [
                        "babel-loader", 
                        {
                            loader: 'ts-loader', 
                            options: {
                                // 關閉型別檢查,即只進行轉譯, 型別檢查交給 fork-ts-checker-webpack-plugin 在別的的執行緒中做
                                transpileOnly: true
                            }
                        }
                    ],
                    exclude: /node_modules/
                },
                {
                    // .css/less 解析
                    test: /\.(less|css)$/,
                    use: [
                        'style-loader',
                        "css-loader",
                        "postcss-loader",
                        "less-loader"
                    ],
                },
                {
                    // 圖片解析
                    test: /\.(png|jpg|gif)$/,
                    include: path.resolve(__dirname, "..", "src"),
                    use: ["url-loader?limit=8192&name=assets/image/[name].[hash:4].[ext]"]
                },
                {
                    // 檔案、字型解析
                    test: /\.(eot|woff|svg|ttf|woff2|otf|appcache|mp3|mp4|pdf)(\?|$)/,
                    include: path.resolve(__dirname, "..", "src"),
                    use: ["file-loader?name=assets/font/[name].[hash:4].[ext]"]
                },
            ]
        },
        resolve: {
            //字尾名自動補全,引入時可不必寫字尾名
            extensions: [".ts", ".tsx", ".js", ".jsx", ".less", ".css"]
        }
    };
    複製程式碼
    • webpack.dev.config.js
    const path = require('path');
    const merge = require('webpack-merge');
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const baseConfig = require('./webpack.base.js');
    
    const devConfig = {
        mode: 'development', // 開發模式
        entry: path.join(__dirname, "../example/src/app.js"), // 專案入口,處理資原始檔的依賴關係
        output: {
            path: path.join(__dirname, "../example/src/"),
            filename: "bundle.js", 
            // 使用webpack-dev-sevrer啟動開發服務時,並不會實際在`src`目錄下生成bundle.js,打包好的檔案是在記憶體中的,但並不影響我們使用。
        },
        module: {
            rules: []
        },
        plugins: [
            new HtmlWebpackPlugin({
                title: 'learn npm',
                filename: "index.html",
                template: "./public/index.html",
                inject: true,
            }),
        ],
        devServer: {
            contentBase: path.join(__dirname, '../example/src/'),
            compress: true,
            port: 3001, // 啟動埠為 3001 的服務
            // open: true // 自動開啟瀏覽器
        },
    };
    module.exports = merge(devConfig, baseConfig);
    複製程式碼
    • webpack.prod.config.js
    const path = require('path');
    const merge = require('webpack-merge');
    const baseConfig = require('./webpack.base.js');
    // const MiniCssExtractPlugin = require("mini-css-extract-plugin"); // 用於將元件的css打包成單獨的檔案輸出到`lib`目錄中
    const { CleanWebpackPlugin } = require('clean-webpack-plugin');
    
    const prodConfig = {
        mode: 'production',
        entry: path.join(__dirname, "../src/index.tsx"),
        output: {
            path: path.join(__dirname, "../lib/"),
            filename: "index.js",
            libraryTarget: 'umd', // 採用通用模組定義
            libraryExport: 'default', // 相容 ES6 的模組系統、CommonJS 和 AMD 模組規範
        },
        module: {
            rules: [
                // 我在打包的沒有做css抽離,故註釋了
                // {
                //     test: /\.css$/,
                //     loader: [MiniCssExtractPlugin.loader, 'css-loader?modules'],
                // },
            ]
        },
        plugins: [
            // new MiniCssExtractPlugin({
            //     filename: "main.min.css" // 提取後的css的檔名
            // }),
            new CleanWebpackPlugin(),
        ],
        externals: { // 定義外部依賴,避免把react和react-dom打包進去
            react: {
                root: "React",
                commonjs2: "react",
                commonjs: "react",
                amd: "react",
            },
            "react-dom": {
                root: "ReactDOM",
                commonjs2: "react-dom",
                commonjs: "react-dom",
                amd: "react-dom",
            }
        },
    };
    module.exports = merge(prodConfig, baseConfig); 
    複製程式碼
    {
        "compilerOptions": {
            "target": "es6",
            "experimentalDecorators": true,
            "strictNullChecks": false,
            "module": "ESNext",
            "moduleResolution": "node",
            "jsx": "react",
            "noUnusedParameters": false,
            "noUnusedLocals": false,
            "esModuleInterop": true,
            "allowSyntheticDefaultImports": true,
            "skipLibCheck": true,
            "noImplicitAny": false,
            "noImplicitReturns": false,
            "noFallthroughCasesInSwitch": false,
            "alwaysStrict": false,
            "strict": false,
            "strictBindCallApply": false,
            "strictPropertyInitialization": false,
            "types": [
                "react",
                "react-dom",
                "node"
            ],
            // "baseUrl": "src",
            // 此處相當於webpack alias
            // "paths": {
            //     "src/*": [
            //         "*"
            //     ]
            // }
        },
        "include": [
            "src/"
        ],
        "exclude": [
            "node_modules",
            "dist"
        ],
        "compileOnSave": false
    }
    複製程式碼

ts 和 babel

上述配置使用的 babel ts 的工作方式為 tsx -(ts-loader) -> es6 -(babel-loader) -> es5 即本專案的 ts-loader 分支

  • 我這個專案的 master分支並沒有使用上述文章介紹的幾種方式,我是直接使用 tsc 編譯器(利用tsconfig.json配置),將原始碼tsx直接編譯成es5,即只用 ts-loader 處理 ts、tsx,tsconfig.json 的target設定成es5,引入之後執行良好,暫未發現異常

ts 與 babel幾種協同工作方式

  1. ts-loader + babel-loader
`ts-loader + tsc + tsconfig.json` 將 tsx 處理為 es6

`babel-loader + babelrc` 接盤將 es6 按照 `@babel/presents-env` 處理為 es5程式碼

話外音: `ts-loader``new ForkTsCheckerWebpackPlugin` 配合 ==> webpack4之後`happypack`作用也小了,故不用了

如上文所配置
複製程式碼
  1. babel7 + @babel/preset-typescript
引入 @babel/preset-typescript,來處理 tsx 型別資訊(其作用就是刪除ts型別資訊)

webpack 配置 js、jsx、ts、tsx 都交由babel-loader 處理

另外在啟動一個 tsc 服務檢查程式碼型別 tsc --watch (package.json npm 指令碼·

複製程式碼
上述不管每種方法最終的結果都是隻轉換高版本ES的語法或者將TypeScript轉換為ES5語法,但並不轉換api
  • 語法:let、const、class、Decorator
  • api:includes、Object.assign、Array.from、Promise、async await
  • 語法靠@babel/preset-env的相關配置進行轉義
  • api靠 @babel/polyfill@babel/runtime@babel/plugin-transform-runtime


  • 有個疑問我現在也沒有明確答案?
    像我們寫的這些 npm包或ui元件庫,需不需要自己做 polyfill?
    還是交給使用方即宿主環境做
    
    我看了 `antd-mobile` 打包後的檔案,發現像 `Promise, Object.assign`並沒有做polyfill
    複製程式碼

babel相關-@babel/polyfill、@babel/preset-env、@babel/plugin-transform-runtime

  • 參考文章
  1. Babel學習系列1-Babel歷史
  2. Babel學習系列2-Babel設計,組成
  3. Babel學習系列3-babel-preset,babel-plugin
  4. Babel學習系列4-polyfill和runtime差別(必看)
  5. Babel 編譯出來還是 ES 6?難道只能上 polyfill?
  6. 這個網站,可以讓你停止“瞎配”前端工具鏈
  7. www.tangshuang.net/7427.html
  • 動態polyfill方案主要是依據 @babel/preset-envuseBuiltIn確定的
  • @babel/babel-polyfill 整個應用全域性引入,模擬完整的ES6+環境
  • @babel/babel-runtime @babel/babel-plugin-transform-runtime 開發像vue這樣的框架、庫,提供一個沙盒環境不會汙染原型鏈,後者主要為前者提供引用幫助,減少程式碼體積

相關文章