發聲的沉默者

發聲的沉默者 查看完整檔案

上海編輯  |  填寫畢業院校  |  填寫所在公司/組織 corki-ui.com 編輯
編輯
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 個人簡介什么都沒有

個人動態

發聲的沉默者 贊了文章 · 2月23日

協同表格+低代碼,這個免費工具可作為團隊的數據管理和自動化中心

企業中往往有很多零散的數據需要管理,這些數據往往分散在 Excel 表格、數據庫、OA 等多個系統之中。我們能不能對這些數據進行集中的收集、展示和共享協作呢?更進一步的,在這些數據之上,我們能不能快速的開發一些小應用進行數據的處理、反饋、提醒呢?

今天我們就來分享 SeaTable 這款免費的工具,看看它如何幫我們實現零散數據的集中管理和自動化。

先簡單介紹下 SeaTable ,它是一款新型的協同表格和低代碼平臺。它支持“文件”、“圖片”、“單選項”、“協作人”、“計算公式”等豐富的數據類型。 它幫助你用表格的形式來方便的組織和管理各類信息。它同時包含完善的 API、自動化規則、腳本運行能力,可以按照你的需要進行擴展,實現數據處理的自動化和業務流程的自動化。

SeaTable 包含以下的版本:

  • 開發者版: 面向把 SeaTable 當做輕型數據庫使用的用戶。可以免費下載,私有部署??!沒有行數、存儲量和 API 調用的限制。
  • 云服務版: 面向有協同需求的普通用戶,有行數、存儲量和 API 調用的限制。
  • 企業版: 在以上版本的基礎上,同時有完善的權限管理和用戶管控功能,可以云端使用也可以本地部署。

下面我們以一個多網站運維管理為例來說明 SeaTable 中數據的記錄和管理、數據可視化和自動化。

多網站運維管理的例子

作為開發團隊,我們往往要運維多個網站,有些給內部用,有些給外部用。我們不僅要把各種零散信息集中記錄,以方便查看和協作,還要對站點證書過期時間等,進行監控和維護。要解決這些問題,如果自己寫一個自動化工具需要花費不少時間,還不好維護。而如果用 SeaTable ,就能在很短的時間內完成,維護起來還方便。

比如我們團隊平時管理的站點就有二十多個,全部使用的是免費的 Lets' encrypt 證書,并通過腳本在證書過期前自動更新證書。偶爾會出現腳本沒有配置對,或其他的原因導致證書沒有正常更新(尤其是對新部署的站點)。這就需要制作一個功能來解決這類問題。

下面我們來逐一介紹怎么用 SeaTable 來實現:

  • 站點數據的協同記錄
  • 用腳本實現自動化更新網站證書過期時間
  • 自動化監控和提醒網站證書過期時間
  • 讓表格數據信息可視化

數據的協同記錄

關于數據的協同記錄,主要分享以下幾點:

  1. SeaTable 支持日期、圖片、文件、單選、URL、長文本、協作人、創建者、創建時間等豐富的數據類型,用它的數據類型,就可以把運維相關的各種數據類型的信息都集中記錄到表格里。
  2. 當我們把表格的只讀或可讀寫權限,共享給同事后,他們就可以進行只讀或協作編輯了。而且表格管理員還可以根據需要,鎖定表頭、鎖定行、設置列的編輯權限等。比如可以對某列設置任何人都不能編輯、或只有管理員可以編輯、或哪些共享用戶能編輯。
  3. 我們還可以用 API 或者 Python 腳本來同步數據庫中的記錄,或者從第三方抓取數據。

用腳本來自動化更新網站證書的過期時間

SeaTable 提供了 Python 腳本的運行環境,我們可以把腳本和數據放在一個地方進行管理,不需要再單獨找一個服務器。同時,可以在表格中根據不同的需要存儲多個 Python 腳本文件, 一鍵點擊運行就可以達到我們想要的效果。如下圖:

image

具體的腳本內容這里就不介紹,有興趣了解更多的同學可以訪問 https://seatable.github.io/se...

腳本除了點擊運行外,還可以安排每日自動運行。

image

用提醒規則來自動化提醒

下面用 SeaTable 的“提醒規則”功能,來實現自動化提醒。

點擊表格右上角的“提醒規則”按鈕,添加一個提醒規則。比如對“證書過期時間”列的時間,可以設置在還有多少天就要過期時,自動發出提醒通知。另外,在個人微信號綁定了表格賬號的前提下,當這個運維管理表有未閱讀的提醒通知時,如果兩分鐘內你沒有點開網頁并閱讀,那么提醒就會發送到個人微信上。

提醒規則設置,如下圖:

image

靈活查看和可視化

在靈活查看數據、可視化和統計分析等方面,SeaTable 有表格視圖功能,有日歷、時間線、圖庫、地圖等實用的插件,也有便捷的“統計”功能等。我們可以根據數據特征去選擇使用。比如可以在多視圖間快速切換查看不同角度的數據;利用統計圖表,來對這個記錄了零散數據的網站運維表進行更直觀的動態可視化。

多視圖:

image

統計圖表:

image

總結

我們用 SeaTable 就可以非常方便地在表格里記錄和管理各種類型的數據信息。更重要的是,我們無需再開發工具,用它的“腳本”和“提醒規則”等功能,就快速完成了自動化的數據處理和流程管理。

作為一款新型的協同表格和低代碼平臺,從使用上來看,它不僅使用門檻低,而且具備靈活性和通用性,即便是非專業技術人員,也能構建自己的業務應用程序,從而不再嚴重依賴技術研發,大幅降低溝通、人力和開發成本。平時我們可以利用它完善的 API、提醒規則和腳本功能等,幫我們快速實現數據處理自動化和業務流程自動化的靈活需求。

查看原文

贊 17 收藏 1 評論 3

發聲的沉默者 發布了文章 · 2月15日

從零搭建 React 開發環境

前言

大概在 2019 年,自己搭建 React 開發環境的想法萌芽,到目前為止,公司的很多項目上,也在使用中,比較穩定。為什么要自己造輪子?起初是因為自己并不滿意市面上的腳手架。另外,造輪子對于自己也有一些技術上的幫助,學別人二次封裝的東西,不如直接使用底層的庫,這樣也有助于自己系統的學習一遍知識,廢話不多說,直接進入正文,如何搭建自己的開發環境。

初始化

創建文件夾并進入:

$ mkdir tristana && cd tristana

初始化 package.json

$ npm init

安裝 Webpack

$ npm install webpack webpack-cli --save-dev

創建以下目錄結構、文件和內容:

project

tristana
|- package.json
|- /dist
   |- index.html
|- /script
   |- webpack.config.js
|- index.html
|- /src
   |- index.js

src/index.js

document.getElementById("root").append("React");

index.html && dist/index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>tristana</title>
    </head>
    <body>
        <div id="root"></div>
        <script data-original="../src/index.js"></script>
    </body>
</html>

script/webpack.config.js

module.exports = {
    mode: "development",
    entry: "./src/index.js",
};

package.json

{
    // ...
    "scripts": {
        "build": "webpack --mode=development --config script/webpack.config.js"
    },
}

然后根目錄終端輸入:npm run build

在瀏覽器中打開 dist 目錄下的 index.html,如果一切正常,你應該能看到以下文本:'React'

index.html 目前放在 dist 目錄下,但它是手動創建的,下面會教你如何生成 index.html 而非手動編輯它。

Webpack 核心功能

Babel

$ npm install @babel/cli @babel/core babel-loader @babel/preset-env --save-dev

script/webpack.config.js

module.exports = {
    // ...
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                loader: "babel-loader",
                exclude: /node_modules/,
            },
        ],
    },
};

.babelrc

在根目錄下添加 .babelrc 文件:

{
    "presets": ["@babel/preset-env", "@babel/preset-react"]
}

樣式

$ npm install style-loader css-loader less less-loader --save-dev

script/webpack.config.js

module.exports = {
    // ...
    module: {
        rules: [
            {
                test: /\.(css|less)$/,
                use: [
                    {
                        loader: "style-loader",
                    },
                    {
                        loader: "css-loader",
                        options: {
                            importLoaders: 1,
                        },
                    },
                    {
                        loader: "less-loader",
                        lessOptions: {
                            javascriptEnabled: true,
                        },
                    },
                ],
            },
        ],
    },
};

圖片字體

$ npm install file-loader --save-dev

script/webpack.config.js

module.exports = {
    // ...
    module: {
        rules: [
            {
                test: /\.(png|svg|jpg|gif|jpeg)$/,
                loader: 'file-loader'
            },
            {
                test: /\.(woff|woff2|eot|ttf|otf)$/,
                loader: 'file-loader'
            }
        ],
    },
};

HTML

$ npm install html-webpack-plugin --save-dev

script/webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    // ...
    plugins: {
        html: new HtmlWebpackPlugin({
            title: 'tristana',
            template: 'public/index.html'
        }),
    }
};

index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>tristana</title>
    </head>
    <body>
        <div id="root"></div>
    </body>
</html>

開發服務

$ npm install webpack-dev-server --save-dev

script/webpack.config.js

const path = require("path");
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    // ...
    devServer: {
        contentBase: path.resolve(__dirname, "dist"),
        hot: true,
        historyApiFallback: true,
        compress: true,
    },
};

package.json

{
    // ...
    "scripts": {
        "start": "webpack serve --mode=development --config script/webpack.config.js"
    },
    // ...
}

清理 dist

$ npm install clean-webpack-plugin --save-dev

script/webpack.config.js

const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
    // ...
    plugins: {
        new CleanWebpackPlugin()
    }
};

Tips

由于 webpack 使用的是^5.21.2 版本,在使用該插件時,會提示clean-webpack-plugin: options.output.path not defined. Plugin disabled...,暫時還未解決。

環境變量

$ npm install cross-env --save-dev

package.json

{
    // ...
    "scripts": {
        "start": "cross-env ENV_LWD=development webpack serve  --mode=development --config script/webpack.config.js",
        "build": "cross-env ENV_LWD=production webpack --mode=production --config script/webpack.config.js"
    },
    // ...
}

.jsx 文件

安裝依賴

$ npm install @babel/preset-react react react-dom --save-dev

.babelrc

{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}

src/App.jsx

src 目錄下,新增 App.jsx 文件:

import React, { Component } from "react";

class App extends Component {
    render() {
        return (
            <div>
                <h1> Hello, World! </h1>
            </div>
        );
    }
}

export default App;

src/index.js

import React from "react";
import ReactDOM from "react-dom";
import App from "./App.jsx";
ReactDOM.render(<App />, document.getElementById("root"));

React Router

安裝依賴

$ npm install react-router history --save

src/index.js

import React from "react";
import ReactDOM from "react-dom";
import { Router, Route, Link } from "react-router";
import { createBrowserHistory } from "history";
import App from "./App.jsx";

const About = () => {
    return <>About</>;
};

ReactDOM.render(
    <Router history={createBrowserHistory()}>
        <Route path="/" component={App} />
        <Route path="/about" component={About} />
    </Router>,
    document.getElementById("root")
);

MobX

安裝依賴

$ npm install mobx mobx-react babel-preset-mobx --save

.babelrc

{
  "presets": ["@babel/preset-env", "@babel/preset-react", "mobx"]
}

src/store.js

src 目錄下新建 store.js

import { observable, action, makeObservable } from "mobx";

class Store {

    constructor() {
        makeObservable(this);
    }

    @observable
    count = 0;

    @action("add")
    add = () => {
        this.count = this.count + 1;
    };

    @action("reduce")
    reduce = () => {
        this.count = this.count - 1;
    };
}
export default new Store();

index.js

import { Provider } from "mobx-react";
import Store from "./store";
// ...
ReactDOM.render(
    <Provider store={Store}>
        <Router history={createBrowserHistory()}>
        <Route path="/" component={App} />
        <Route path="/about" component={About} />
        </Router>
    </Provider>,
    document.getElementById("root")
);

src/App.jsx

import React, { Component } from "react";
import { observer, inject } from "mobx-react";

@inject("store")
@observer
class App extends Component {
    render() {
        return (
            <div>
                <div>{this.props.store.count}</div>
                <button onClick={this.props.store.add}>add</button>
                <button onClick={this.props.store.reduce}>reduce</button>
            </div>
        );
    }
}

export default App;

Ant Design

安裝依賴

$ npm install antd babel-plugin-import --save

.babelrc

{
    // ...
    "plugins": [
        [
            "import",
            {
                "libraryName": "antd",
                "libraryDirectory": "es",
                "style": true
            }
        ]
    ]
}

src/App.jsx

// ...
import { DatePicker } from "antd";
import "antd/dist/antd.css";

@inject("store")
@observer
class App extends Component {
    render() {
        return (
            <div>
                <DatePicker />
            </div>
        );
    }
}

export default App;

TypeScript

安裝依賴

$ npm install typescript @babel/preset-typescript --save-dev

.babelrc

{
    "presets": [
        // ...
        "@babel/preset-typescript"
    ]
}

tsconfig.json

在根目錄下,新增 tsconfig.json 文件:

{
    "compilerOptions": {
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "target": "ES5",
        "allowSyntheticDefaultImports": true,
        "strict": true,
        "forceConsistentCasingInFileNames": true,
        "allowJs": true,
        "outDir": "./dist/",
        "esModuleInterop": true,
        "noImplicitAny": false,
        "sourceMap": true,
        "module": "esnext",
        "moduleResolution": "node",
        "isolatedModules": true,
        "importHelpers": true,
        "lib": ["esnext", "dom", "dom.iterable"],
        "skipLibCheck": true,
        "jsx": "react",
        "typeRoots": ["node", "node_modules/@types"],
        "rootDirs": ["./src"],
        "baseUrl": "./src"
    },
    "include": ["./src/**/*"],
    "exclude": ["node_modules"]
}

src/App.jsx

更換文件后綴 App.jsx -> App.tsx

import React, { Component } from "react";
import { observer, inject } from "mobx-react";
import { DatePicker } from "antd";
import "antd/dist/antd.css";

@inject("store")
@observer
class App extends Component {
    props: any;
    render() {
        return (
            <div>
                <DatePicker />
                <div>{this.props.store.count}</div>
                <button onClick={this.props.store.add}>add</button>
                <button onClick={this.props.store.reduce}>reduce</button>
            </div>
        );
    }
}

export default App;

代碼規范

代碼校驗、代碼格式化、Git 提交前校驗、Vscode配置、編譯校驗

ESLint

安裝依賴

$ npm install @typescript-eslint/parser eslint eslint-plugin-standard @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-promise  --save-dev

.eslintrc.js

在根目錄下,新增 .eslintrc.js 文件:

module.exports = {
    extends: ["eslint:recommended", "plugin:react/recommended"],
    env: {
        browser: true,
        commonjs: true,
        es6: true,
    },
    globals: {
        $: true,
        process: true,
        __dirname: true,
    },
    parser: "@typescript-eslint/parser",
    parserOptions: {
        ecmaFeatures: {
            jsx: true,
            modules: true,
        },
        sourceType: "module",
        ecmaVersion: 6,
    },
    plugins: ["react", "standard", "promise", "@typescript-eslint"],
    settings: {
        "import/ignore": ["node_modules"],
        react: {
            version: "latest",
        },
    },
    rules: {
        quotes: [2, "single"],
        "no-console": 0,
        "no-debugger": 1,
        "no-var": 1,
        semi: ["error", "always"],
        "no-irregular-whitespace": 0,
        "no-trailing-spaces": 1,
        "eol-last": 0,
        "no-unused-vars": [
        1,
        {
            vars: "all",
            args: "after-used",
        },
        ],
        "no-case-declarations": 0,
        "no-underscore-dangle": 0,
        "no-alert": 2,
        "no-lone-blocks": 0,
        "no-class-assign": 2,
        "no-cond-assign": 2,
        "no-const-assign": 2,
        "no-delete-var": 2,
        "no-dupe-keys": 2,
        "use-isnan": 2,
        "no-duplicate-case": 2,
        "no-dupe-args": 2,
        "no-empty": 2,
        "no-func-assign": 2,
        "no-invalid-this": 0,
        "no-redeclare": 2,
        "no-spaced-func": 2,
        "no-this-before-super": 0,
        "no-undef": 2,
        "no-return-assign": 0,
        "no-script-url": 2,
        "no-use-before-define": 2,
        "no-extra-boolean-cast": 0,
        "no-unreachable": 1,
        "comma-dangle": 2,
        "no-mixed-spaces-and-tabs": 2,
        "prefer-arrow-callback": 0,
        "arrow-parens": 0,
        "arrow-spacing": 0,
        camelcase: 0,
        "jsx-quotes": [1, "prefer-double"],
        "react/display-name": 0,
        "react/forbid-prop-types": [
        2,
        {
            forbid: ["any"],
        },
        ],
        "react/jsx-boolean-value": 0,
        "react/jsx-closing-bracket-location": 1,
        "react/jsx-curly-spacing": [
        2,
        {
            when: "never",
            children: true,
        },
        ],
        "react/jsx-indent": ["error", 4],
        "react/jsx-key": 2,
        "react/jsx-no-bind": 0,
        "react/jsx-no-duplicate-props": 2,
        "react/jsx-no-literals": 0,
        "react/jsx-no-undef": 1,
        "react/jsx-pascal-case": 0,
        "react/jsx-sort-props": 0,
        "react/jsx-uses-react": 1,
        "react/jsx-uses-vars": 2,
        "react/no-danger": 0,
        "react/no-did-mount-set-state": 0,
        "react/no-did-update-set-state": 0,
        "react/no-direct-mutation-state": 2,
        "react/no-multi-comp": 0,
        "react/no-set-state": 0,
        "react/no-unknown-property": 2,
        "react/prefer-es6-class": 2,
        "react/prop-types": 0,
        "react/react-in-jsx-scope": 2,
        "react/self-closing-comp": 0,
        "react/sort-comp": 0,
        "react/no-array-index-key": 0,
        "react/no-deprecated": 1,
        "react/jsx-equals-spacing": 2,
    },
};

.eslintignore

在根目錄下,新增 .eslintignore 文件:

src/assets

.vscode

在根目錄下新增 .vscode 文件夾,然后新增 .vscode/settings.json

{
    "eslint.validate": [
        "javascript",
        "javascriptreact",
        "typescript",
        "typescriptreact"
    ]
}

Perttier

安裝依賴

$ npm install prettier --save-dev

prettier.config.js

在根目錄下,新增 prettier.config.js 文件:

module.exports = {
    // 一行最多 100 字符
    printWidth: 100,
    // 使用 4 個空格縮進
    tabWidth: 4,
    // 不使用縮進符,而使用空格
    useTabs: false,
    // 行尾需要有分號
    semi: true,
    // 使用單引號
    singleQuote: true,
    // 對象的 key 僅在必要時用引號
    quoteProps: 'as-needed',
    // jsx 不使用單引號,而使用雙引號
    jsxSingleQuote: false,
    // 末尾不需要逗號
    trailingComma: 'none',
    // 大括號內的首尾需要空格
    bracketSpacing: true,
    // jsx 標簽的反尖括號需要換行
    jsxBracketSameLine: false,
    // 箭頭函數,只有一個參數的時候,也需要括號
    arrowParens: 'avoid',
    // 每個文件格式化的范圍是文件的全部內容
    rangeStart: 0,
    rangeEnd: Infinity,
    // 不需要寫文件開頭的 @prettier
    requirePragma: false,
    // 不需要自動在文件開頭插入 @prettier
    insertPragma: false,
    // 使用默認的折行標準
    proseWrap: 'preserve',
    // 根據顯示樣式決定 html 要不要折行
    htmlWhitespaceSensitivity: 'css',
    // 換行符使用 lf
    endOfLine: 'lf'
};

stylelint

安裝依賴

$ npm install stylelint stylelint-config-standard stylelint-config-prettier --save-dev

stylelint.config.js

在根目錄下,新增 stylelint.config.js 文件:

module.exports = {
    extends: ['stylelint-config-standard', 'stylelint-config-prettier'],
    ignoreFiles: [
        '**/*.ts',
        '**/*.tsx',
        '**/*.png',
        '**/*.jpg',
        '**/*.jpeg',
        '**/*.gif',
        '**/*.mp3',
        '**/*.json'
    ],
    rules: {
        'at-rule-no-unknown': [
            true,
            {
                ignoreAtRules: ['extends', 'ignores']
            }
        ],
        indentation: 4,
        'number-leading-zero': null,
        'unit-allowed-list': ['em', 'rem', 's', 'px', 'deg', 'all', 'vh', '%'],
        'no-eol-whitespace': [
            true,
            {
                ignore: 'empty-lines'
            }
        ],
        'declaration-block-trailing-semicolon': 'always',
        'selector-pseudo-class-no-unknown': [
            true,
            {
                ignorePseudoClasses: ['global']
            }
        ],
        'block-closing-brace-newline-after': 'always',
        'declaration-block-semicolon-newline-after': 'always',
        'no-descending-specificity': null,
        'selector-list-comma-newline-after': 'always',
        'selector-pseudo-element-colon-notation': 'single'
    }
};

lint-staged、pre-commit

安裝依賴

$ npm install lint-staged prettier eslint pre-commit --save-dev

package.json

{
    // ...
    "scripts": {
        "lint:tsx": "eslint --ext .tsx src && eslint --ext .ts src",
        "lint:css": "stylelint --aei .less .css src",
        "precommit": "lint-staged",
        "precommit-msg": "echo 'Pre-commit checks...' && exit 0"
    },
    "pre-commit": [
        "precommit",
        "precommit-msg"
    ],
    "lint-staged": {
        "*.{js,jsx,ts,tsx}": [
            "eslint --fix",
            "prettier --write",
            "git add"
        ],
        "*.{css,less}": [
            "stylelint --fix",
            "prettier --write",
            "git add"
        ]
    }
}

eslint-webpack-plugin

安裝依賴

$ npm install eslint-webpack-plugin --save-dev

script/webpack.config.js

const ESLintPlugin = require('eslint-webpack-plugin');
module.exports = {
    // ...
    plugins: [new ESLintPlugin()],
};

總結

搭建這個的過程,也是遇到了不少坑,收獲也是蠻多的,希望這個教程能夠幫助更多的同學,少采點坑,完整的 React 開發環境可以看這個tristana,求點贊,求關注!

博客

歡迎關注我的博客

查看原文

贊 40 收藏 33 評論 0

發聲的沉默者 發布了文章 · 2020-11-13

Webpack4 性能優化實踐

為什么需要性能優化

在使用 Webpack 時,如果不注意性能優化,可能會產生性能問題,會導致在開發體驗上不是非常絲滑,性能問題主要是編譯速度慢,打包體積過大,因此性能優化也主要從這些方面來分析。本文主要是自己平時的工作積累和參考別人的文章,而進行總結,基于 Webpack4 版本。

構建分析

編譯速度分析

Webpack 構建速度進行優化的首要任務就是去知道哪些地方值得我們注意。
speed-measure-webpack-plugin 插件能夠測量 Webpack 構建速度

 SMP  ?
General output time took 38.3 secs

 SMP  ?  Plugins
HtmlWebpackPlugin took 1.31 secs
CopyPlugin took 0.016 secs
OptimizeCssAssetsWebpackPlugin took 0.002 secs
ContextReplacementPlugin took 0.001 secs
MiniCssExtractPlugin took 0 secs
DefinePlugin took 0 secs

 SMP  ?  Loaders
_babel-loader@8.1.0@babel-loader took 29.98 secs
  module count = 1503
_babel-loader@8.1.0@babel-loader, and
_eslint-loader@3.0.4@eslint-loader took 18.74 secs
  module count = 86
_css-loader@3.6.0@css-loader, and
_less-loader@5.0.0@less-loader took 16.45 secs
  module count = 64
modules with no loaders took 2.24 secs
  module count = 7
_file-loader@5.1.0@file-loader took 1.03 secs
  module count = 17
_style-loader@1.3.0@style-loader, and
_css-loader@3.6.0@css-loader, and
_less-loader@5.0.0@less-loader took 0.102 secs
  module count = 64
_html-webpack-plugin@3.2.0@html-webpack-plugin took 0.021 secs
  module count = 1

居然達到了驚人的 38.3 秒,雖然有點不是很準確,但是非常慢。發現 babel-loader、eslint-loader、css-loader、less-loader 占據了大頭。

const webpackBase = require('./webpack.base.conf');
const path = require('path');

const SpeedMeasureWebpackPlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasureWebpackPlugin();

module.exports = smp.wrap({
    // 配置源碼顯示方式
    devtool: 'eval-source-map',
    mode: 'development',
    entry: {
        app: ['./src/index.jsx']
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'index.js'
    },
    resolve: webpackBase.resolve,
    module: webpackBase.module,
    stats: webpackBase.stats,
    optimization: webpackBase.optimization,
    plugins: [
        webpackBase.plugins.html,
        webpackBase.plugins.miniCssExtract,
        webpackBase.plugins.optimizeCssAssets,
        // webpackBase.plugins.progressBarPlugin,
        webpackBase.plugins.ContextReplacementPlugin,
        webpackBase.plugins.DefinePlugin,
        // webpackBase.plugins.AntdDayjsWebpackPlugin,
        webpackBase.plugins.CopyPlugin
        // webpackBase.plugins.HotModuleReplacementPlugin
    ],
    devServer: webpackBase.devServer,
    watchOptions: webpackBase.watchOptions,
    externals: webpackBase.externals
});

打包體積分析

通過 webpack-bundle-analyzer 插件能夠在 Webpack 構建結束后生成構建產物體積報告,配合可視化的頁面,能夠直觀知道產物中的具體占用體積。

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
  plugins: bundleAnalyzer: new BundleAnalyzerPlugin({ analyzerPort: 8081 })],
};

效果圖如下:

5061604735941_ pic_hd

可以看出一個很明顯的問題就是 Ant Design、TRTC、Mobx 這些庫,沒有排除。

打包體積如下:

image

如何優化

縮小構建目標

  • 優化 resolve.modules 配置(減少模塊搜索層級和不必要的編譯工作)
  • 優化 resolve.extensions 配置
  • 增加緩存
const path = require('path');
module.exports = {
    resolve: {
        // 自動解析確定的擴展
        extensions: ['.js', '.jsx', '.css', '.less', '.json'],
        alias: {
            // 創建 import 或 require 的別名,來確保模塊引入變得更簡單
            'react': path.resolve( __dirname ,'./node_modules/react/dist/react.min.js')
        },
        // 當從 npm 包導入模塊時,此選項將決定在 `package.json` 中使用哪個字段導入模塊
        // 默認值為 browser -> module -> main
        mainFields: ['main']
    },
    module: {
        rules: [
            {
                // 排除node_modules模塊
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                // 開啟緩存
                loader: 'babel-loader?cacheDirectory=true'
            }
        ]
    }
};

使用 thread-loader,開啟多進程

thread-loader 會將你的 loader 放置在一個 worker 池里面運行,每個 worker 都是一個單獨的有 600ms 限制的 node.js 進程。同時跨進程的數據交換也會被限制。請在高開銷的 loader 中使用,否則效果不佳。

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve('src'),
        use: [
          'thread-loader',
          // your expensive loader (e.g babel-loader)
        ],
      },
    ],
  },
};

使用 hard-source-webpack-plugin

Webpack4中,hard-source-webpack-pluginDLL 的更好替代者。

hard-source-webpack-pluginWebpack 的插件,為模塊提供中間緩存步驟。為了查看結果,您需要使用此插件運行 Webpack 兩次:第一次構建將花費正常的時間。第二次構建將顯著加快(大概提升 90% 的構建速度)。不過該插件很久沒更新了,不太建議使用。

去掉 eslint-loader

由于我項目中使用了 eslint-loader 如果配置了 precommit,其實可以去掉的。

通過 externals 把相關的包,排除

Webpack

module.exports = {
    // externals 排除對應的包,注:排除掉的包必須要用script標簽引入下
    externals: {
        react: 'React',
        'react-dom': 'ReactDOM',
        'trtc-js-sdk': 'TRTC',
        bizcharts: 'BizCharts',
        antd: 'antd',
        mobx: 'mobx',
        'mobx-react': 'mobxReact'
    }
};

index.html

<!DOCTYPE html>
<html lang="zh">
    <head>
        <meta charset="utf-8" />
        <meta
            name="viewport"
            content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"
        />
        <meta name="baidu-site-verification" content="ptk9VJudKz" />
        <link
            rel="stylesheet"
            href="https://xxx/antd.min3.26.20.css"
        />
        <title>webpack</title>
        <script
            type="text/javascript"
            data-original="https://xxx/17.0.0react.production.min.js"
        ></script>
        <script
            type="text/javascript"
            data-original="https://xxx/17.0.0react-dom.production.min.js"
        ></script>
        <script
            type="text/javascript"
            data-original="https://xxx/BizCharts3.5.8.js"
        ></script>
        <script
            type="text/javascript"
            data-original="https://xxx/trtc4.6.7.js"
        ></script>
        <script
            type="text/javascript"
            data-original="https://xxx/moment2.29.1.min.js"
        ></script>
        <script
            type="text/javascript"
            data-original="https://xxx/moment2.29.1zh-cn.js"
        ></script>
        <script
            type="text/javascript"
            data-original="https://xxx/polyfill.min7.8.0.js"
        ></script>
        <script
            type="text/javascript"
            data-original="https://xxx/antd.min3.26.20.js"
        ></script>
        <script
            type="text/javascript"
            data-original="https://xxx/mobx.umd.min5.13.1.js"
        ></script>
        <script
            type="text/javascript"
            data-original="https://xxx/mobx-react.index.min5.4.4.js"
        ></script>
    </head>
    <body>
        <div id="root"></div>
    </body>
</html>

JS 壓縮

Webpack4 開始,默認情況下使用 terser壓縮生產環境下的輸出結果。Terser 是一款兼容 ES2015 +JavaScript 壓縮器。與 UglifyJS(許多項目的早期標準)相比,它是面向未來的選擇。有一個 UglifyJS 的分支—— uglify-es,但由于它不再維護,于是就從這個分支誕生出了一個獨立分支,它就是 terser。

const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
    optimization: {
        minimizer: [
            // 壓縮js
            new TerserPlugin({
                test: /\.(jsx|js)$/,
                extractComments: true,
                parallel: true,
                cache: true
            })
        ]
    },
};

CSS 壓縮

Webpack 4.0 以后,官方推薦使用 mini-css-extract-plugin 插件來打包 CSS 文件。

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    module: {
        rules: [
            {
                test: /\.(css|less)$/,
                use: [MiniCssExtractPlugin.loader]
            }
        ]
    },
};

FAQ

Ant Design 無法加載

請確保加載順序,Moment、Polyfill 放在 Ant Design 前面加載

MobX 無法加載

MobX 引入 mobx.umd.min.js庫,mobx-react 需要引入

package.json

{
    "name": "webpack",
    "version": "1.0.0",
    "private": true,
    "main": "index.js",
    "dependencies": {
        "antd": "^3.26.20",
        "babel-eslint": "^10.0.3",
        "babel-loader": "^8.0.0",
        "babel-plugin-import": "^1.13.0",
        "babel-plugin-react-css-modules": "^5.2.6",
        "bizcharts": "^3.5.8",
        "china-division": "^2.3.1",
        "compression-webpack-plugin": "^3.0.1",
        "copy-webpack-plugin": "^5.1.1",
        "css-loader": "^3.2.0",
        "eslint": "^6.8.0",
        "eslint-config-prettier": "^6.11.0",
        "eslint-config-standard": "^14.1.0",
        "eslint-loader": "^3.0.4",
        "eslint-plugin-import": "^2.20.0",
        "eslint-plugin-promise": "^4.2.1",
        "eslint-plugin-react": "^7.17.0",
        "eslint-plugin-standard": "^4.0.1",
        "html-webpack-plugin": "^3.2.0",
        "less": "^3.8.1",
        "less-loader": "^5.0.0",
        "lint-staged": "^10.0.8",
        "mini-css-extract-plugin": "^0.8.0",
        "mobx": "^5.13.1",
        "mobx-react": "^5.4.4",
        "optimize-css-assets-webpack-plugin": "^5.0.1",
        "pre-commit": "^1.2.2",
        "progress-bar-webpack-plugin": "^1.12.1",
        "react": "^17.0.0",
        "react-dom": "^17.0.0",
        "speed-measure-webpack-plugin": "^1.3.1",
        "style-loader": "^1.2.1",
        "terser-webpack-plugin": "^2.2.1",
        "trtc-js-sdk": "^4.6.7",
        "viewerjs": "^1.5.0",
        "webpack": "^4.41.2",
        "webpack-bundle-analyzer": "^3.6.0",
        "webpack-cli": "^3.3.10",
        "webpack-dev-server": "^3.10.1"
    }
}

最終效果

打包體積:

5381605008681_ pic

打包體積由原先 2.1M 變成了 882KB,可以說效果非常巨大。

包依賴:

5441605062491_ pic_hd

Ant Design、TRTC、Mobx 這些庫也沒了

編譯速度:

SMP  ?
General output time took 10.67 secs

 SMP  ?  Plugins
HtmlWebpackPlugin took 1.69 secs
BundleAnalyzerPlugin took 0.091 secs
CopyPlugin took 0.011 secs
MiniCssExtractPlugin took 0.003 secs
OptimizeCssAssetsWebpackPlugin took 0.002 secs
DefinePlugin took 0.001 secs
ContextReplacementPlugin took 0 secs

 SMP  ?  Loaders
_babel-loader@8.1.0@babel-loader took 8.26 secs
  module count = 277
_babel-loader@8.1.0@babel-loader, and
_eslint-loader@3.0.4@eslint-loader took 7.18 secs
  module count = 86
_css-loader@3.6.0@css-loader, and
_less-loader@5.0.0@less-loader took 1.94 secs
  module count = 28
modules with no loaders took 0.728 secs
  module count = 12
_file-loader@5.1.0@file-loader took 0.392 secs
  module count = 17
_style-loader@1.3.0@style-loader, and
_css-loader@3.6.0@css-loader, and
_less-loader@5.0.0@less-loader took 0.052 secs
  module count = 28
_html-webpack-plugin@3.2.0@html-webpack-plugin took 0.026 secs
  module count = 1

編譯速度由原先 38.3 secs(實際編譯速度大概 15 秒左右),減少到 10.67 secs(實際編譯速度 10 秒左右)。

國內外公共 CDN 地址

參考資料

博客

博客

查看原文

贊 7 收藏 4 評論 0

發聲的沉默者 發布了文章 · 2020-07-05

如何搭建前端異常監控系統

什么是異常

是指用戶在使用應用時,無法得到預期的結果。不同的異常帶來的后果程度不同,輕則引起用戶使用不悅,重則導致產品無法使用,從而使用戶喪失對產品的認可。

為什么要處理異常

  • 增強用戶體驗
  • 遠程定位問題
  • 無法復現問題,特別是移動端,各種原因,可能是系統版本,機型等等

前端有哪些異常

異常頻率
JavaScript 異常(語法錯誤、代碼錯誤)經常
靜態資源加載異常(img、js、css)偶爾
Ajax 請求異常偶爾
promise 異常較少
iframe 異常較少

如何捕獲異常

try-catch

try-catch 只能捕獲同步運行錯誤,對語法和異步錯誤卻捕獲不到。

1、同步運行錯誤

try {
    kill;
} catch(err) {
    console.error('try: ', err);
}

結果:try: ReferenceError: kill is not defined

2、無法捕獲語法錯誤

try {
    let name = '1;
} catch(err) {
    console.error('try: ', err);
}

結果:Unterminated string constant

編譯器能夠阻止運行語法錯誤。

3、無法捕獲異步錯誤

try {
    setTimeout(() => {
        undefined.map(v => v);
    }, 1000);
} catch(err) {
    console.error('try: ', err);
}

結果:Uncaught TypeError: Cannot read property 'map' of undefined

window.onerror

JavaScript 運行時錯誤(包括語法錯誤)發生時,window 會觸發一個 ErrorEvent 接口的 error 事件,并執行 window.onerror() 若該函數返回 true,則阻止執行默認事件處理函數。

1、同步運行錯誤

/**
* @param {String}  message   錯誤信息
* @param {String}  source    出錯文件
* @param {Number}  lineno    行號
* @param {Number}  colno     列號
* @param {Object}  error     error對象
*/
window.onerror = (message, source, lineno, colno, error) => {
    console.error('捕獲異常:', message, source, lineno, colno, error);
    return true;
};

kill;

結果:捕獲異常: Uncaught ReferenceError: kill is not defined

2、無法捕獲語法錯誤

/**
* @param {String}  message   錯誤信息
* @param {String}  source    出錯文件
* @param {Number}  lineno    行號
* @param {Number}  colno     列號
* @param {Object}  error     error對象
*/
window.onerror = (message, source, lineno, colno, error) => {
    console.error('捕獲異常:', message, source, lineno, colno, error);
    return true;
};

let name = '1;

結果:Unterminated string constant

編譯器能夠阻止運行語法錯誤。

3、異步錯誤

/**
* @param {String}  message   錯誤信息
* @param {String}  source    出錯文件
* @param {Number}  lineno    行號
* @param {Number}  colno     列號
* @param {Object}  error     error對象
*/
window.onerror = (message, source, lineno, colno, error) => {
    console.error('捕獲異常:', message, source, lineno, colno, error);
    return true;
};

setTimeout(() => {
    undefined.map(v => v);
}, 1000);

結果:捕獲異常: Uncaught TypeError: Cannot read property 'map' of undefined\`

window.addEventListener('error')

當一項資源(如 <img><script> )加載失敗,加載資源的元素會觸發一個 Event 接口的 error 事件,并執行該元素上的 onerror() 處理函數。這些 error 事件不會向上冒泡到 window,不過(至少在 Firefox 中)能被單一的 window.addEventListener 捕獲。

<script>
window.addEventListener('error', (err) => {
    console.error('捕獲異常:', err);
}, true);
</script>
<img data-original="./test.jpg" />

結果:捕獲異常:Event {isTrusted: true, type: "error", target: img, currentTarget: Window, eventPhase: 1, …}

window.addEventListener('unhandledrejection')

Promisereject 且沒有 reject 處理器的時候,會觸發 unhandledrejection 事件;這可能發生在 window 下,但也可能發生在 Worker 中。 這對于調試回退錯誤處理非常有用。

window.addEventListener("unhandledrejection", (err) => {
    err.preventDefault();
    console.error('捕獲異常:', err);
});

Promise.reject('promise');

結果:捕獲異常:PromiseRejectionEvent {isTrusted: true, promise: Promise, reason: "promise", type: "unhandledrejection", target: Window, …}

Vue

Vue.config.errorHandler = (err, vm, info) => {
  console.error('捕獲異常:', err, vm, info);
}

React

React16,提供了一個內置函數 componentDidCatch ,使用它可以非常簡單的獲取到 React 下的錯誤信息。

componentDidCatch(error, info) {
    console.error('捕獲異常:', error, info);
}

但是,推薦ErrorBoundary

用戶界面中的 JavaScript 錯誤不應破壞整個應用程序。為了為 React 用戶解決此問題,React16 引入了“錯誤邊界”的新概念。

新建 ErrorBoundary.jsx 組件:

import React from 'react';
import { Result, Button } from 'antd';

class ErrorBoundary extends React.Component {
    constructor(props) {
        super(props);
        this.state = { hasError: false, info: '' };
    }

    static getDerivedStateFromError(error) {
        return { hasError: true };
    }

    componentDidCatch(error, info) {
        this.setState({
            info: error + ''
        });
    }

    render() {
        if (this.state.hasError) {
            // 你可以渲染任何自定義的降級 UI
            return (
                <Result
                    status="500"
                    title="500"
                    subTitle={this.state.info}
                    extra={<Button type="primary">Report feedback</Button>}
                />
            );
        }

        return this.props.children;
    }
}

export default ErrorBoundary;

使用:

<ErrorBoundary>
    <App />
</ErrorBoundary>

注意

錯誤邊界不會捕獲以下方面的錯誤:

  • 事件處理程序
  • 異步代碼(例如 setTimeoutrequestAnimationFrame 回調)
  • 服務器端渲染
  • 在錯誤邊界本身(而不是其子級)中引發的錯誤

iframe

由于瀏覽器設置的“同源策略”,無法非常優雅的處理 iframe 異常,除了基本屬性(例如其寬度和高度)之外,無法從 iframe 獲得很多信息。

<script>
    document.getElementById("myiframe").onload = () => {
        const self = document.getElementById('myiframe');

        try {
            (self.contentWindow || self.contentDocument).location.href;
        } catch(err) {
            console.log('捕獲異常:' + err);
        }
    };
</script>

<iframe id="myiframe" data-original="https://nibuzhidao.com" frameBorder="0" />

Sentry

業界非常優秀的一款監控異常的產品,作者也是用的這款,文檔齊全。

需要上報哪些信息

  • 錯誤 id
  • 用戶 id
  • 用戶名
  • 用戶 IP
  • 設備
  • 錯誤信息
  • 游覽器
  • 系統版本
  • 應用版本
  • 機型
  • 時間戳
  • 異常級別(error、warning、info)

異常上報

1、Ajax 發送數據

2、動態創建 img 標簽

如果異常數據量大,導致服務器負載高,調整發送頻率(可以考慮把異常信息存儲在客戶端,設定時間閥值,進行上報)或設置采集率(采集率應該通過實際情況來設定,隨機數,或者某些用戶特征都是不錯的選擇)。

流程圖

異常監控流程圖

參考資料

博客

歡迎關注我的博客

查看原文

贊 16 收藏 11 評論 0

發聲的沉默者 發布了文章 · 2020-06-21

macOS 下 iTerm 如何安裝 rzsz

Homebrew

安裝:

$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"

安裝過程中,需要輸入密碼和確認

image

image

如果安裝過程中遇到:$ Git download : error: RPC failed; curl 18 transfer closed with outstanding read data remaining,則說明你需要代理,設置 GitHub 代理,

$ git config --global https.proxy http://127.0.0.1:端口號
$ git config --global http.proxy http://127.0.0.1:端口號

安裝成功:

image

可以通過 $ brew --help 測試是否安裝成功

卸載:

$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/uninstall.sh)"

安裝 lrzsz

$ brew install lrzsz

安裝完成后,進入 $ cd /usr/local/bin

創建文件并添加內容

$ vi iterm2-recv-zmodem.sh
$ vi iterm2-send-zmodem.sh

創建好后,兩個文件后分別添加內容

  1. iterm2-recv-zmodem.sh:
#!/bin/bash
# Author: Matt Mastracci (matthew@mastracci.com)
# AppleScript from http://stackoverflow.com/questions/4309087/cancel-button-on-osascript-in-a-bash-script
# licensed under cc-wiki with attribution required
# Remainder of script public domain

osascript -e 'tell application "iTerm2" to version' > /dev/null 2>&1 && NAME=iTerm2 || NAME=iTerm
if [[ $NAME = "iTerm" ]]; then
    FILE=`osascript -e 'tell application "iTerm" to activate' -e 'tell application "iTerm" to set thefile to choose folder with prompt "Choose a folder to place received files in"' -e "do shell script (\"echo \"&(quoted form of POSIX path of thefile as Unicode text)&\"\")"`
else
    FILE=`osascript -e 'tell application "iTerm2" to activate' -e 'tell application "iTerm2" to set thefile to choose folder with prompt "Choose a folder to place received files in"' -e "do shell script (\"echo \"&(quoted form of POSIX path of thefile as Unicode text)&\"\")"`
fi

if [[ $FILE = "" ]]; then
    echo Cancelled.
    # Send ZModem cancel
    echo -e \\x18\\x18\\x18\\x18\\x18
    sleep 1
    echo
    echo \# Cancelled transfer
else
    cd "$FILE"
    /usr/local/bin/rz -E -e -b
    sleep 1
    echo
    echo
    echo \# Sent \-\> $FILE
fi
  1. iterm2-send-zmodem.sh:
#!/bin/bash
# Author: Matt Mastracci (matthew@mastracci.com)
# AppleScript from http://stackoverflow.com/questions/4309087/cancel-button-on-osascript-in-a-bash-script
# licensed under cc-wiki with attribution required
# Remainder of script public domain

osascript -e 'tell application "iTerm2" to version' > /dev/null 2>&1 && NAME=iTerm2 || NAME=iTerm
if [[ $NAME = "iTerm" ]]; then
    FILE=`osascript -e 'tell application "iTerm" to activate' -e 'tell application "iTerm" to set thefile to choose file with prompt "Choose a file to send"' -e "do shell script (\"echo \"&(quoted form of POSIX path of thefile as Unicode text)&\"\")"`
else
    FILE=`osascript -e 'tell application "iTerm2" to activate' -e 'tell application "iTerm2" to set thefile to choose file with prompt "Choose a file to send"' -e "do shell script (\"echo \"&(quoted form of POSIX path of thefile as Unicode text)&\"\")"`
fi
if [[ $FILE = "" ]]; then
    echo Cancelled.
    # Send ZModem cancel
    echo -e \\x18\\x18\\x18\\x18\\x18
    sleep 1
    echo
    echo \# Cancelled transfer
else
    /usr/local/bin/sz "$FILE" -e -b
    sleep 1
    echo
    echo \# Received $FILE
fi

將文件寫好后保存好,使用如下命令添加權限:

$ chmod 777 iterm2-*

編輯 iTerm

點擊 iTerm2 的設置界面 Perference-> Profiles -> Default -> Advanced -> Triggers 的 Edit 按鈕,加入以下配置:

image

Regular expression: rz waiting to receive.\*\*B0100
Action: Run Silent Coprocess
Parameters: /usr/local/bin/iterm2-send-zmodem.sh

Regular expression: \*\*B00000000000000
Action: Run Silent Coprocess
Parameters: /usr/local/bin/iterm2-recv-zmodem.sh

測試

rz 上傳功能:在 bash 中,也就是 iTerm2 終端輸入 rz 會彈出文件選擇框,選擇文件 choose 就開始上傳,會上傳到當前目錄

sz 下載功能:sz fileName(你要下載的文件的名字) 回車,會彈出窗體,我們選擇要保存的地方即可。

博客

歡迎關注我的博客

查看原文

贊 0 收藏 0 評論 0

發聲的沉默者 發布了文章 · 2020-05-29

Webpack 如何配置熱更新

什么是 HMR

是指 Hot Module Replacement,縮寫為 HMR。對于你需要更新的模塊,進行一個"熱"替換,所謂的熱替換是指在不需要刷新頁面的情況下,對某個改動進行無縫更新。如果你沒有配置 HMR,那么你每次改動,都需要刷新頁面,才能看到改動之后的結果,對于調試來說,非常麻煩,而且效率不高,最關鍵的是,你在界面上修改的數據,隨著刷新頁面會丟失,而如果有類似 Webpack 熱更新的機制存在,那么,則是修改了代碼,不會導致刷新,而是保留現有的數據狀態,只將模塊進行更新替換。也就是說,既保留了現有的數據狀態,又能看到代碼修改后的變化。

總結:

  • 加載頁面時保存應用程序狀態
  • 只更新改變的內容,節省調試時間
  • 修改樣式更快,幾乎等同于在瀏覽器中更改樣式

安裝依賴

$ npm install webpack webpack-dev-server --save-dev

package.json

"dependencies": {
    "webpack": "^4.41.2",
    "webpack-dev-server": "^3.10.1"
},

配置

webpack:

devServer: {
    contentBase: path.resolve(__dirname, 'dist'),
    hot: true,
    historyApiFallback: true,
    compress: true
},
  • hottrue,代表開啟熱更新
  • contentBase 表示告訴服務器從哪里提供內容。(也就是服務器啟動的根目錄,默認為當前執行目錄,一般不需要設置)
  • historyApiFallback 使用 HTML5 歷史記錄 API 時,index.html 很可能必須提供該頁面來代替任何 404 響應
  • compress 對所有服務啟用 gzip 壓縮
plugins: {
    HotModuleReplacementPlugin: new webpack.HotModuleReplacementPlugin()
},

配置熱更新插件

module: {
    rules: [
        {
            test: /\.(css|less)$/,
            use: [
                process.env.NODE_ENV == 'development' ? { loader: 'style-loader' } : MiniCssExtractPlugin.loader,
                {
                    loader: 'css-loader',
                    options: {
                        importLoaders: 1
                    }
                }
            ]
        }
    ]
},

style-loader 庫實現了 HMR 接口,當通過 HMR 收到更新時,它將用新樣式替換舊樣式。區分開發環境和生產環境,用不同 loader。

src/index.jsx

if?(module.hot)?{
    module.hot.accept();
}

入口文件,新增上面代碼,就可以了,非常簡單。

react-hot-loader

react-hot-loader 插件,傳送門

如何使用

安裝

$ npm install react-hot-loader --save-dev

配置 babelrc

{
  "plugins": ["react-hot-loader/babel"]
}

將根組件標記為熱導出

import { hot } from 'react-hot-loader/root';
const App = () => <div>Hello World!</div>;
export default hot(App);

ReactReact Dom 之前,確保需要 React 熱加載程序

// webpack.config.js
module.exports = {
  entry: ['react-hot-loader/patch', './src'],
  // ...
};

遇到問題

  • 如果遇到 You cannot change <Router history> ,那么應該這樣配置:
import { hot } from 'react-hot-loader/root';
const Routes = () => {};
export default hot(Routes);
  • 配置完熱更新之后,遇到webpack自動編譯兩次問題,很大概率出現,具體原因,沒有分析,找到一個討巧的解決辦法,配置:
watchOptions: {
    aggregateTimeout: 600
},

也有可能是其他問題,比如你在index.html頁面,重復引入了index.js,又或者是全局安裝了webpack-dev-server,與本地webpack-dev-server重復,卸載全局webpack-dev-server,即可。

案例

Tristana

博客

歡迎關注我的博客

查看原文

贊 0 收藏 0 評論 0

發聲的沉默者 發布了文章 · 2020-03-16

基于React、MobX、Webpack 和 React Router 的項目模板

前言

自己利用業余時間,基于 React、Ant Design、Webpack、MobX、React Router、TS 寫了一個后臺管理模板,目前已在公司內部搭建了幾套項目,并都已上線,希望幫助自己梳理各技術最新知識點,同時也希望對看到的人有所幫助。

項目效果:

登錄頁:

404 頁:

列表頁:

分支

main hooks + ts

class class + ts

js class + js

如何快上手

一個基本列表展示

源碼地址

Tristana

在線預覽地址

Demo

技術棧

  • React
  • Ant Design
  • React Router
  • MobX
  • Webpack
  • ES6
  • Babel
  • Axios
  • Eslint
  • stylelint
  • TS

項目結構

.
├── LICENSE
├── README.md
├── README2.md
├── declaration.d.ts
├── package.json
├── prettier.config.js
├── public
│?? └── index.html
├── script
│?? ├── webpack.base.conf.js
│?? ├── webpack.dev.js
│?? └── webpack.prod.js
├── src
│?? ├── app.tsx
│?? ├── assets
│?? ├── components
│?? ├── config.ts
│?? ├── index.tsx
│?? ├── locales
│?? ├── mobx
│?? ├── mock
│?? ├── pages
│?? ├── request.tsx
│?? ├── routeConfig.tsx
│?? ├── servers
│?? ├── styles
│?? ├── typings
│?? └── utils
├── stylelint.config.js
└── tsconfig.json

庫版本

"dependencies": {
    "@ant-design/icons": "^4.2.2",
    "@babel/cli": "^7.8.0",
    "@babel/core": "^7.8.0",
    "@babel/plugin-proposal-class-properties": "^7.8.0",
    "@babel/plugin-proposal-decorators": "^7.8.0",
    "@babel/plugin-proposal-json-strings": "^7.8.0",
    "@babel/plugin-proposal-optional-chaining": "^7.10.1",
    "@babel/plugin-syntax-dynamic-import": "^7.8.0",
    "@babel/plugin-syntax-import-meta": "^7.8.0",
    "@babel/plugin-transform-runtime": "^7.8.0",
    "@babel/polyfill": "^7.8.0",
    "@babel/preset-env": "^7.8.2",
    "@babel/preset-react": "^7.8.0",
    "@babel/preset-stage-2": "^7.8.0",
    "@pmmmwh/react-refresh-webpack-plugin": "^0.3.3",
    "@sentry/react": "^5.18.1",
    "@types/react": "^16.9.48",
    "@types/react-dom": "^16.9.8",
    "@typescript-eslint/eslint-plugin": "^3.10.1",
    "@typescript-eslint/parser": "^3.10.1",
    "antd": "^4.5.3",
    "antd-dayjs-webpack-plugin": "^1.0.0",
    "awesome-typescript-loader": "^5.2.1",
    "axios": "^0.19.2",
    "babel-eslint": "^10.0.3",
    "babel-loader": "^8.0.0",
    "babel-plugin-import": "^1.13.0",
    "babel-plugin-react-css-modules": "^5.2.6",
    "classnames": "^2.2.6",
    "clean-webpack-plugin": "^3.0.0",
    "compression-webpack-plugin": "^3.0.1",
    "copy-webpack-plugin": "^5.1.1",
    "core-js": "^3.6.4",
    "cross-env": "^6.0.3",
    "css-loader": "^3.2.0",
    "dayjs": "^1.8.15",
    "eslint": "^6.8.0",
    "eslint-config-prettier": "^6.11.0",
    "eslint-config-standard": "^14.1.0",
    "eslint-loader": "^3.0.4",
    "eslint-plugin-import": "^2.22.0",
    "eslint-plugin-promise": "^4.2.1",
    "eslint-plugin-react": "^7.20.6",
    "eslint-plugin-standard": "^4.0.1",
    "file-loader": "^5.0.2",
    "history": "^4.7.2",
    "html-webpack-plugin": "^3.2.0",
    "install": "^0.12.2",
    "is-promise": "^2.1.0",
    "less": "^3.8.1",
    "less-loader": "^5.0.0",
    "lint-staged": "^10.0.8",
    "mini-css-extract-plugin": "^0.8.0",
    "mobx": "^5.15.6",
    "mobx-react": "^6.1.4",
    "mobx-react-router": "^4.1.0",
    "mockjs": "^1.1.0",
    "npm": "^6.10.2",
    "optimize-css-assets-webpack-plugin": "^5.0.1",
    "postcss-loader": "^3.0.0",
    "pre-commit": "^1.2.2",
    "progress-bar-webpack-plugin": "^1.12.1",
    "prop-types": "^15.7.2",
    "react": "^16.12.0",
    "react-dom": "^16.12.0",
    "react-intl-universal": "^2.2.5",
    "react-refresh": "^0.8.3",
    "react-router": "^5.1.2",
    "react-router-dom": "^5.1.2",
    "react-scripts": "^3.0.0",
    "socket.io-client": "^2.3.0",
    "source-map-loader": "^1.0.2",
    "speed-measure-webpack-plugin": "^1.3.1",
    "style-loader": "^1.2.1",
    "stylelint": "^13.0.0",
    "stylelint-config-prettier": "^8.0.2",
    "stylelint-config-standard": "^19.0.0",
    "terser-webpack-plugin": "^2.2.1",
    "trtc-js-sdk": "^4.6.3",
    "ts-import-plugin": "^1.6.6",
    "typescript": "^4.0.2",
    "webpack": "^4.41.2",
    "webpack-bundle-analyzer": "^3.6.0",
    "webpack-cli": "^3.3.10",
    "webpack-dev-server": "^3.10.1"
},

博客

歡迎關注我的博客

查看原文

贊 1 收藏 0 評論 0

發聲的沉默者 發布了文章 · 2020-03-09

VS Code 提高前端開發效率插件

Auto Close Tag

自動添加 HTML/XML 關閉標記,與 Visual Studio IDESublime 文本相同

usage

鍵入開始標簽的結束括號后,將自動插入結束標簽。

Auto Rename Tag

自動重命名配對的 HTML/XML 標記

usage

Beautify

Visual Studio 代碼美化代碼

選中需要美化的代碼,右鍵 Format Document

GitLens

增強 Visual Studio 代碼中內置的 Git 功能-通過 Git 責怪注釋和代碼鏡頭一目了然地可視化代碼作者,無縫導航和瀏覽 Git 存儲庫,通過強大的比較命令獲得有價值的見解,等等

7bf310ecae2e4fb92499bdcc3ea723e

JavaScript (ES6) code snippets

ES6 語法中 JavaScript 的代碼段

Path Autocomplete

提供 Visual Studio 代碼的路徑完成。

path-autocomplete

Path Intellisense

自動完成文件名的 Visual Studio 代碼插件

iaHeUiDeTUZuo

React-Native/React/Redux snippets for es6/es7

JS/TS 中使用 ES7 語法對 React、ReduxGraphql 進行簡單擴展

StandardJS - JavaScript Standard Style

JavaScript 標準樣式集成到 Visual Studio 代碼中。

  1. 安裝 "JavaScript 標準樣式" 擴展

    如果您不知道如何在 Visual Studio 中安裝擴展,請查看文檔。

    您將需要重新加載 Visual Studio 才能使用新的擴展。

  2. 安裝 standard 或 semistandard

    這可以在全局或本地完成。我們建議您在本地安裝它們(即保存在項目的中 devDependencies),以確保在開發項目時其他開發人員也已安裝它們。

  3. 禁用內置的 Visual Studio 驗證器

    為此,請 "javascript.validate.enable": falseVisual Studio 中進行設置 settings.json

Vetur

VS 代碼的 Vue 工具

vscode wxml

微信 wxml 支持 /vscode 片段

vscode-fileheader

插入標題注釋,并自動更新時間。

fileheader

“settings.json” 中,設置并修改創建者的名稱。

"fileheader.Author": "Jiang",
"fileheader.LastModifiedBy": "Jiang",

熱鍵

ctrl+alt+i

vscode-icons

Visual Studio 代碼的圖標

image

wxml

微信小程序 wxml 格式化以及高亮組件(高度自定義)

ESLint

ESLint JavaScript 集成到 Visual Studio 代碼中。

以下設置為包括 ESLint 在內的所有提供程序都啟用了自動修復:

"editor.codeActionsOnSave": {
    "source.fixAll": true
}

相反,此配置僅在 ESLint 上將其打開:

"editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
}

您還可以通過以下方式有選擇地禁用 ESLint:

"editor.codeActionsOnSave": {
    "source.fixAll": true,
    "source.fixAll.eslint": false
}

Import Cost

在編輯器中顯示導入/要求包大小

import-cost

Beautify css/sass/scss/less

美化 CSS、Sass 和更少的代碼(Visual Studio 代碼的擴展)

選中需要美化的代碼,右鍵 Format Document

TSLint

Visual Studio 代碼的 TSLint 支持

Settings Sync

使用 GitHub Gist 跨多臺計算機同步設置、代碼段、主題、文件圖標、啟動、鍵綁定、工作區和擴展名。

CSS Peek

允許查看 CSS ID 和類字符串作為從 HTML 文件到相應 CSS 的定義。允許查看和轉到定義。

symbolProvider

Stylelint

使用 stylelintlint CSS/SCSS/LessVisual Studio 代碼擴展,進行格式校驗。

博客

歡迎關注我的博客

查看原文

贊 2 收藏 2 評論 0

發聲的沉默者 發布了文章 · 2020-02-29

關于 TRTC (實時音視頻通話模式)在我司的實踐

什么是 TRTC

騰訊實時音視頻(Tencent Real-Time Communication,TRTC)將騰訊 21 年來在網絡與音視頻技術上的深度積累,以多人音視頻通話和低延時互動直播兩大場景化方案,通過騰訊云服務向開發者開放,致力于幫助開發者快速搭建低成本、低延時、高品質的音視頻互動解決方案。


TRTC 流程圖

f3ba68fa7e39fe64af92ff99f56b77a

加入房間

創建流

this.client = TRTC.createClient({
    mode: 'videoCall',
    sdkAppId,
    userId,
    userSig
});
  • mode: 實時音視頻通話模式,設置為‘videoCall’,互動直播模式,設置為 'live'
  • sdkAppId: 您從騰訊云申請的 sdkAppId
  • userId: 用戶 ID,隨機生成,一個房間內不允許重復的 userId
  • userSig: 用戶簽名,基于后臺算法生成,防盜刷

加入

this.client
    .join({ roomId })
    .catch(error => {
        console.error('進房失敗 ' + error);
    })
    .then(() => {
        console.log('進房成功');
    });
  • roomId:后臺生成的房間 Id,不能重復

發布本地流

本地推流

this.localStream = TRTC.createStream({ userId: this.userId, audio: true, video: true });
  • userId: 用戶 ID,隨機生成,一個房間內不允許重復的 userId
  • audio: 是否從麥克風采集音頻
  • video: 是否從攝像頭采集視頻

初始化本地音視頻流

this.localStream
    .initialize()
    .catch(err => {
        console.error('初始化本地流失敗 ' + error);
    })
    .then((res) => {
        console.log('初始化本地流成功');
        this.localStream.play('localVideo');
    });
  • localVideo: 綁定的 div id

發布

this.client
    .publish(this.localStream)
    .catch(err => {
        console.error('本地流發布失敗 ' + error);
    })
    .then((res) => {
        console.log('本地流發布成功');
    });
  • 本地流發布成功之后,可以注冊本地推流函數,每三秒執行一次,處理異常情況。

訂閱遠端流

遠端流增加

this.client.on('stream-added', event => {
    this.remoteStream = event.stream;
    //訂閱遠端流
    this.client.subscribe(this.remoteStream);
});

遠端流訂閱

this.client.on('stream-subscribed', event => {
    console.log('log', 'onRemoteStreamUpdate:' + event);
    this.remoteStream = event.stream;
    this.id = this.remoteStream.getId();
    const remoteVideoDom = document.querySelector('#remoteVideo');
    if(!document.querySelector(`#remoteStream-${this.id}`)) {
        const div = document.createElement('div');
        div.setAttribute('style', 'position: absolute; right: 0; left: 0; top: 0; width: 100%; height: 100%');
        div.setAttribute('id', `remoteStream-${this.id}`);
        remoteVideoDom.appendChild(div);
    }
    const videoLoading = document.querySelector('#video-loading');
    videoLoading.setAttribute('style', 'display: none;');
    // 播放遠端流
    this.remoteStream.play(`remoteStream-${this.id}`);
});

可以在遠端流監聽成功之后,注冊遠端流狀態變化函數,處理異常情況。

退出

取消發布本地流

this.client.unpublish(this.localStream)
    .catch((err) => {
        console.log('error', 'unpublish error:' + err);
    })
    .then((res) => {
        // 取消發布本地流成功
        console.log('log', 'unpublish error:' + res);
    });

退出房間

this.client.leave();

異常處理

本地流監聽

// 每隔3秒獲取本地推流情況
this.localTimer = setInterval(() => {
    this.client.getLocalVideoStats().then(stats => {
        for (let userId in stats) {
            console.log(new Date(), 'getLocalVideoStats', 'userId: ' + userId +
        'bytesSent: ' + stats[userId].bytesSent + 'local userId' + this.userId);
            if(this.userId == userId && stats[userId].bytesSent == 0) {
                this.onEvent('leave');
            }

            const bytesSentSR = (stats[userId].bytesSent - this.bytesSent) / 3000;

            if(this.userId == userId && bytesSentSR >= 20 && bytesSentSR <= 59) {

            }

            if(this.userId == userId) {
                this.bytesSent =  stats[userId].bytesSent;
            }
        }
    });
}, 3000);
  • 可在本地流發布成功后,注冊本地推流變化函數,處理異常情況
  • bytesSent: 如果發送單位為 0,則表示本地斷網
  • 公式: 目前發送字節數 - 上一次發送字節數 / 3000

遠端流監聽

this.remoteTimer = setInterval(() => {
    this.client.getRemoteVideoStats().then(stats => {
        for (let userId in stats) {
            console.log('getRemoteVideoStats', 'userId: ' + userId +
        ' bytesReceived: ' + stats[userId].bytesReceived +
        ' packetsReceived: ' + stats[userId].packetsReceived +
        ' packetsLost: ' + stats[userId].packetsLost);
            // const bytesReceived = (stats[userId].bytesReceived - this.bytesReceived) / 3000;
            // let title = '';

            // if(this.agentId == userId && bytesReceived >= 120) {
            //     title = '當前通話,對方網絡良好';
            // }

            // if(this.agentId == userId && bytesReceived >= 60 && bytesReceived <= 119) {
            //     title = '當前通話,對方網絡一般';
            // }

            // if(this.agentId == userId && bytesReceived >= 20 && bytesReceived <= 59) {
            //     title = '當前通話,對方網絡不佳';
            // }

            // if(this.agentId == userId) {
            //     Taro.showToast({
            //         title,
            //         icon: 'none',
            //         duration: 1000
            //     });
            //     this.bytesReceived =  stats[userId].bytesReceived;
            // }
        }
    });
}, 3000);
  • bytesReceived: 如果接受單位為 0,則表示對方斷網
  • 可在遠端流監聽成功之后,注冊遠端流狀態變化函數,處理異常情況
  • 公式: 目前接收字節數 - 上一次接收字節數 / 3000

目前通過 TRTC 的事件通知,搭配 Socket,能做到對異常處理有較好的支持。

TRTC 兼容性

Android(H5)

  • 攝像頭不匹配,比如,華為手機三個后置加一個前置,調用 TRTC 的獲取攝像頭接口,返回的卻是 6 個,并且沒有 Label 標注那個是后置,那個是前置,廠商問題,需要特殊適配。
  • 必須使用微信游覽器打開 H5 頁面,其他游覽器會偶爾崩潰以及其他問題(猜測微信游覽器做了適配)。
  • 華為 P30 部分機型,存在微信游覽器環境下沒有默認打開騰訊 X5 內核,需要進行特殊處理。打開方案:1、可以在手機設置、應用管理、微信、麥克風和攝像頭權限重新開啟。2、通過掃描 X5 內核開啟二維碼,引導開啟。否則會發布流失敗,因為 X5 內核關閉,導致沒有權限獲取。
  • TRTC 對大部分機型能夠有較好的支持。

iOS(H5)

  • 必須使用 Safari 游覽器,其他游覽器會出現各種問題。
  • 需要用戶手動觸發播放,這時候需要在 video 組件上加上 autoplay、muted、playsinline、controls(SDK,4.0.0 版本以下)
<Video
    id="remoteVideo"
    autoplay
    muted
    playsinline
    controls
/>
  • 切換前后置攝像頭需要根據 Label 標簽進行區分,獲取前后置攝像頭的 deviceId,切換流程如下:

    1、獲取攝像頭

        TRTC.getCameras().then(devices => {
            this.cameras = devices;
        });

    2、選擇攝像頭

    this.localStream.switchDevice('video', deviceId)
        .catch(err => {
            console.log('error', 'switchDevice error:' + err);
        })
        .then((res) => {
            console.log('log', 'switchDevice success' + res);
        });

小程序

  • React 技術棧(我只使用了 Taro)能夠支持視頻播放,但推薦更好的 Vue 技術棧,因為 Vue 有官方封裝的組件。
  • 手機兼容性比較好,微信環境加持。

云端混流

request({
    url: `http://fcgi.video.qcloud.com/common_access?appid=${liveSign.appId}&interface=Mix_StreamV2&t=${liveSign.t}&sign=${liveSign.liveSign}`,
    method: 'POST',
    headers: {
        'content-type': 'application/json',
    },
    body: JSON.stringify(params)
}, (error, response, body) => {
    res.send({errCode: 0});
});

通過 http://fcgi.video.qcloud.com/common_access 接口,我們能夠完美的監聽房間內發生的情況,錄制好的視頻,會上傳到騰訊的云點播平臺,同時也支持客戶自行導出。

博客

歡迎關注我的博客

查看原文

贊 0 收藏 0 評論 2

發聲的沉默者 發布了文章 · 2020-02-14

基于React、Redux、Webpack 和 React-Router的項目模板。

Lucian

特性

  • 快速上手,沒有其它cli這么多概念,只要會React、Redux、Webpack、React-Router,快速搭建中后臺管理平臺。
  • 路由匹配,包含url輸入、js跳轉、菜單切換。
  • Action,不需要重復定義action,比如等待Action、成功Actoin、失敗Action。寫更少的action,完成更多的事。
  • 自定義中間件,幫助Action完成異步操作。
  • immutable,更簡潔,持久化數據結構。
  • 簡單實現combineReducers,監聽當前正在觸發的action。
  • 完全自定義的cli,內置redux、webpack、react-router、classnames、dayjs、eslint等。

例子

是否可用于生產環境?

當然!公司內用于生產環境的項目估計已經有 3+ 。

是否支持 IE8 ?

不支持。

快速上手

創建新應用

$ git clone https://github.com/Cherry-Team/lucian.git

$ cd lucian

$ npm install

$ npm start

幾秒之后,你將會看到以下輸出

使用 antd

import React, { Component } from 'react';
import { Button } from 'antd';
class Index extends Component {
    constructor(props) {
        super(props);
    }

    render() {
        return (
            <Button>Button</Button>
        );
    }
}

export default Index;

定義路由

我們要寫個應用來顯示列表,首先是創建路由。

新建 route component pages/List.js,內容如下:

import React from 'react';

const List= (props) => (
  <h2>List</h2>
);

export default List;

添加路由信息到路由表,編輯 routeConfig.js

const List = lazy(() => import(/* webpackChunkName: "List"*/'./pages/List/index'));
const routes = [
    {
        path: '/list',
        component: List
    }
];

編寫 UI Component

隨著應用的發展,你會需要在多個頁面分享 UI 元素 (或在一個頁面使用多次),根目錄下新建components/LayoutHeader/index.jsx

import React, { Component } from 'react';
import { Avatar, Dropdown, Menu } from 'antd';

const menu = (
    <Menu>
        <Menu.Item>
            <a target="_blank" rel="noopener noreferrer" href={false}>
                退出
            </a>
        </Menu.Item>
    </Menu>
);

class Index extends Component {
    constructor(props) {
        super(props);
        this.state = {};
    }

    render() {
        return (
            <section>
                <div>
                    訂單系統
                </div>
                <div>
                    <span>消息</span>
                    <Dropdown overlay={menu}>
                        <div>
                            <Avatar size={28} icon="user" />
                            <span >Faker</span>
                        </div>
                    </Dropdown>
                </div>
            </section>
        );
    }
}

export default Index;

使用 Redux 完成計數器

新建pages/Counter/index.jsx

import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as action from '../../actions/counter';
import './index.less';

const mapStateToProps = state => {
    const { counter } = state;
    return {
        counter: counter.toJS()
    };
};

const mapDispatchToProps = (dispatch) => ({
    add: (...args) => dispatch(action.add(...args)),
    reduce: (...args) => dispatch(action.reduce(...args))
});

class Index extends Component {
    constructor(props) {
        super(props);
    }

    render() {
        return (
            <section className="counter">
                <button onClick={() => this.props.add(this.props.counter.count)}>+</button>
                <span>count is: {this.props.counter.count}</span>
                <button onClick={() => this.props.reduce(this.props.counter.count)}>-</button>
            </section>
        );
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(Index);

創建actions/counter.js

export const ADD = 'add';
export function add(params) {
    return {
        type: ADD,
        params
    };
}

export const REDUCE = 'reduce';
export function reduce(params) {
    return {
        type: REDUCE,
        params
    };
}

然后新建reducers/counter.js

import { fromJS } from 'immutable';
import { createReducer } from 'redux-immutablejs';
import {
    ADD,
    REDUCE
} from './../actions/counter';

const initialState = fromJS({
    count: 0
});

export default createReducer(initialState, {
    [ADD]: (state, { params }) => {
        return state.set('count', params + 1);
    },
    [REDUCE]: (state, { params }) => {
        return state.set('count', params - 1);
    }
});

然后在reducers/rootReducers.js中引入

// reducers配置文件
import { routerReducer } from 'react-router-redux';
import orderList from './orderList';
import counter from './counter';

// 保存當前正在執行的action type
const combineReducers = (reducers) => {
    return (state = {}, action) => {
        return Object.keys(reducers).reduce((nextState, key) => {
            nextState[key] = reducers[key](state[key], action);
            return nextState;
        }, { actionType: action.type });
    };
};

const rootReducers = combineReducers({
    counter,
    orderList,
    router: routerReducer
});
export default rootReducers;

構建應用

完成開發并且在開發環境驗證之后,就需要部署給我們的用戶了。先執行下面的命令:

$ npm run build

幾秒后,輸出以下內容:

build 命令會打包所有的資源,包含 JavaScript, CSS, web fonts, images, html 等。然后你可以在 dist/ 目錄下找到這些文件。

源碼

Lucian

未來

  • jsx升級TypeScript
  • webpack 5.0.0
  • React 17.0.0
查看原文

贊 1 收藏 1 評論 0

認證與成就

  • 獲得 73 次點贊
  • 獲得 5 枚徽章 獲得 0 枚金徽章, 獲得 1 枚銀徽章, 獲得 4 枚銅徽章

擅長技能
編輯

開源項目 & 著作
編輯

  • corki-ui

    是基于 Ant Design 設計體系的 React UI 組件庫,主要用于研發企業級中后臺產品

注冊于 2018-05-30
個人主頁被 2k 人瀏覽

一本到在线是免费观看_亚洲2020天天堂在线观看_国产欧美亚洲精品第一页_最好看的2018中文字幕 <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>