关于javascript:从零开始搭建一个-reacttypescript-脚手架

43次阅读

共计 14011 个字符,预计需要花费 36 分钟才能阅读完成。

参考文章:我是这样搭建 Typescript+React 我的项目环境的

根本配置

基于 webpack 4+ 搭建

装置 webpack:

npm install webpack@4 webpack-cli@3 -D

新建文件夹build,用于保留配置:

mkdir build

接着在 build 文件夹下新建这几个文件:

  • config.js 环境变量
  • proxy.js 代理配置
  • webpack.common.js 通用配置
  • webpack.dev.js 开发环境配置
  • webpack.prod.js 生产环境配置
$ cd build

$ touch config.js proxy.js webpack.common.js webpack.dev.js webpack.prod.js

接下来装置两个依赖包:

  • webpack-merge 能够将通用配置 webpack.common.js开发环境 dev生产环境 prod 的配置合并起来
  • cross-env 能够跨平台设置和应用环境变量,解决 macwindow配置不同的问题
npm install webpack-merge cross-env -D

批改 package.json 文件:

"scripts": {
    "start": "cross-env NODE_ENV=development webpack-dev-server --config ./build/webpack.dev.js",
    "build": "cross-env NODE_ENV=production webpack --config ./build/webpack.prod.js"
}

筹备好构建须要的环境变量,批改config.js:

const SERVER_PORT = 9527
const SERVER_HOST = '127.0.0.1'
const PROJECT_NAME = "cli"
const isDev = process.env.NODE_ENV !== 'production'

module.exports = {
    isDev,
    PROJECT_NAME,
    SERVER_PORT,
    SERVER_HOST
}

接下来筹备好 webpack 配置文件:

// webpack.common.js
const {resolve} = require('path')

module.exports = {entry: resolve(__dirname,"../src/index.js"),
  output: {filename: 'js/bundle.[hash:8].js',
    path: resolve(__dirname, '../dist'),
  },
}
//webpack.dev.js
const {merge} = require('webpack-merge')
const common = require('./webpack.common.js')

module.exports = merge(common, {mode: 'development',})
//webpack.prod.js
const {merge} = require('webpack-merge')
const common = require('./webpack.common.js')

module.exports = merge(common, {mode: 'production',})

新建工程入口文件:

 src/
    - index.js

启动我的项目

要启动我的项目,有几个配置依赖包是必备的:

  • html-webpack-plugin 模板文件,将咱们打包后的资源引入到 html 中
  • webpack-dev-server 开启一个本地 http 服务,能够配置热更新、代理等。
  • clean-webpack-plugin 清理文件夹,每次打包后先主动清理旧的文件
  • copy-webpack-plugin 资源文件 复制到打包目录下
npm install html-webpack-plugin webpack-dev-server clean-webpack-plugin copy-webpack-plugin -D

模板文件配置

新建 public 文件夹,外面放咱们的 html 模板文件:

mkdir public
touch index.html
//index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title><%=htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

通过 htmlWebpackPlugin 能够拿到配置的变量信息,接着批改webpack.common.js:

const {resolve} = require('path')
const config = require("./config")
const CopyPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const {CleanWebpackPlugin} = require('clean-webpack-plugin')

module.exports = {
  entry: "../src/index.tsx",
  output: {filename: 'js/bundle.[hash:8].js',
    path: resolve(__dirname, '../dist'),
    },
    plugins:[
        new HtmlWebpackPlugin({template: resolve(__dirname, '../public/index.html'),
            filename: 'index.html',
            title: config.PROJECT_NAME,
            cache: false,
        }),
        new CopyPlugin({
            patterns: [{ from: resolve(__dirname, "../public"), to: resolve(__dirname, "../dist") }
            ],
        }),
        new CleanWebpackPlugin()]
}

devServer 配置

代理

批改proxy.js,配置代理:

const proxySetting = {
    '/api/': {
        target: 'http://localhost:3001',
        changeOrigin: true,
    },
    // 接口代理 2
    '/api-2/': {
        target: 'http://localhost:3002',
        changeOrigin: true,
        pathRewrite: {'^/api-2': '',},
    },
}

module.exports = proxySetting

devServer

批改 webpack.dev.js:

const {merge} = require('webpack-merge');
const webpack = require('webpack');
const {resolve} = require("path");
const common = require('./webpack.common.js');
const proxySetting = require('./proxy');
const config = require('./config');

module.exports = merge(common, {
    mode: 'development',
    devServer: {
        host: config.SERVER_HOST,
        port: config.SERVER_PORT,
        stats: 'errors-only',
        clientLogLevel: 'silent',
        compress: true,
        open: false,
        hot: true, // 热更新
        proxy: {...proxySetting}, // 代理配置
        contentBase: resolve(__dirname, '../public')
    },
    plugins: [new webpack.HotModuleReplacementPlugin()],
});

devtool

devtool能够将编译后的代码映射回原始源代码,不便咱们调试错误代码,对我来说 eval-source-map 是可能承受的调试模式,生产环境下间接禁用,批改文件如下:

//webpack.dev.js
module.exports = merge(common, {
  mode: 'development',
+ devtool: 'eval-source-map',
})
//webpack.prod.js
module.exports = merge(common, {
  mode: 'production',
+ devtool: 'none',
})

款式解决

style-loadercss-loader 是必备的了,接下来如果是解决 less 文件,须要装置 lessless-loader。解决 sass 须要装置 node-sasssass-loader, 这里我用的是less,所以装置:

npm install css-loader style-loader less less-loader -D

失常状况下咱们配置两条 rule,针对css 文件和 less 文件就好了:

// webpack.common.js
module.exports = {
  // other...
  module: {
    rules: [{test: /\.css$/,use: ['style-loader','css-loader']},
      {test: /\.less$/,use: [
        'style-loader',
        {
            loader:'css-loader',
            options:{importLoaders:1}
        },
        'less-loader'
        ]
    },
    ]
  },
}

不过咱们还是要解决款式兼容性问题和不同环境下的sourceMap

postcss 款式兼容

postcssbabel 相似,咱们也要装置一些 preset 能力失效:

  • postcss-flexbugs-fixes:用于修复一些和 flex 布局相干的 bug。
  • postcss-preset-env:将最新的 CSS 语法转换为指标环境的浏览器可能了解的 CSS 语法,目标是使开发者不必思考浏览器兼容问题。咱们应用 autoprefixer 来主动增加浏览器头。
  • postcss-normalize:从 browserslist 中主动导入所须要的 normalize.css 内容。
npm install postcss-loader postcss-flexbugs-fixes postcss-preset-env autoprefixer postcss-normalize -D

postcss 的配置如下:

{
    loader: 'postcss-loader',
    options: {
        sourceMap: config.isDev, // 是否生成 sourceMap
        postcssOptions: {
            plugins: [
                // 修复一些和 flex 布局相干的 bug
                require('postcss-flexbugs-fixes'),
                require('postcss-preset-env')({autoprefixer: {grid: true,flexbox: 'no-2009'},
                    stage: 3,
                }),
                require('postcss-normalize')]}
        }
}

这里能够发现 cssless的编译配置差不多,所以这里封装成一个通用办法来配置,
build文件夹下新建 utils.js 文件,用来寄存封装的通用办法:

//utils.js
const {isDev} = require('./config')

exports.getCssLoaders = (importLoaders) => [
    'style-loader',
    {
        loader: 'css-loader',
        options: {
            modules: false,
            sourceMap: isDev,
            importLoaders,
        },
    },
    {
        loader: 'postcss-loader',
        options: {
            sourceMap: isDev,
            postcssOptions: {
                plugins: [
                    // 修复一些和 flex 布局相干的 bug
                    require('postcss-flexbugs-fixes'),
                    require('postcss-preset-env')({
                        autoprefixer: {
                            grid: true,
                            flexbox: 'no-2009',
                        },
                        stage: 3,
                    }),
                    require('postcss-normalize'),
                ],
            },
        },
    },
]

接着批改 webpack.common.js 文件:

const {getCssLoaders} = require("./utils");

...
    module:{
        rules:[{ test: /.(css)$/, use: getCssLoaders(1) },
            {
                test: /\.less$/,
                use: [...getCssLoaders(2),
                    {
                        loader: 'less-loader',
                        options: {sourceMap: config.isDev},
                    }
                ]
            }
        ]
    }
...

最初,还须要在 package.json 中增加 browserslist

{
  "browserslist": [
    ">0.2%",
    "not dead",
    "ie >= 9",
    "not op_mini all"
  ],
}

图片和字体文件解决

图片和其它资源文件解决比较简单,图片能够应用 url-loader 解决,如果是小图或图标能够转成 base64,如果其它资源文件,通过 file-loader 转成流的形式输入,先装置依赖包:

npm install file-loader url-loader -D
// webpack.common.js

module.exports = {
  // other...
  module: {
    rules: [
      // other...
      {test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 10 * 1024,
              name: '[name].[contenthash:8].[ext]',
              outputPath: 'assets/images',
            },
          },
        ],
      },
      {test: /\.(ttf|woff|woff2|eot|otf|svg)$/,
        use: [
          {
            loader: 'file-loader',
            options: {name: '[name].[contenthash:8].[ext]',
              outputPath: 'assets/fonts',
            },
          },
        ],
      },
    ]
  },
  plugins: [//...],
}

typescript环境下还要先申明类型,这里再 src/typings 下新建 file.d.ts 文件,输出上面内容:

declare module '*.svg' {
  const path: string
  export default path
}

declare module '*.bmp' {
  const path: string
  export default path
}

declare module '*.gif' {
  const path: string
  export default path
}

declare module '*.jpg' {
  const path: string
  export default path
}

declare module '*.jpeg' {
  const path: string
  export default path
}

declare module '*.png' {
  const path: string
  export default path
}

react 和 typescript

先装置react:

npm install react react-dom -S

装置 babel 相干依赖:

npm install babel-loader @babel/core @babel/preset-env @babel/plugin-transform-runtime @babel/preset-react -D
npm install @babel/runtime-corejs3 -S

留神:@babel/runtime-corejs3 的装置为生产依赖。

  • babel-loader 应用 babel 解析文件
  • @babel/core babel外围模块
  • @babel/preset-env 转换成最新的 javascript 规定
  • @babel/preset-react 转译 jsx 语法
  • @babel/plugin-transform-runtime 开发库 / 工具、移除冗余工具函数(helper function)
  • @babel/runtime-corejs3 辅助函数

新建.babelrc,输出以下代码:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        // 避免 babel 将任何模块类型都转译成 CommonJS 类型,导致 tree-shaking 生效问题
        "modules": false
      }
    ],
    "@babel/preset-react"
  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": {
          "version": 3,
          "proposals": true
        },
        "useESModules": true
      }
    ]
  ]
}

批改 webpack.common.js 文件,减少以下代码:

module.exports = {
    // other...
  module: {
    rules: [
      {test: /\.(tsx?|js)$/,
        loader: 'babel-loader',
        options: {cacheDirectory: true},
        exclude: /node_modules/,
      },
      // other...
    ]
  },
  plugins: [//...],
}

babel-loader 在执行的时候,可能会产生一些运行期间反复的公共文件,造成代码体积大冗余,同时也会减慢编译效率,所以咱们开启 cacheDirectory 将这些公共文件缓存起来,下次编译就会放慢很多。

resolve.extensions 和 resolve.alias

  • extensions 扩展名辨认
  • alias 别名

webpack.common.js 新增 resolve

resolve: {alias:{"@":resolve(__dirname, '../src')},
    extensions: ['.tsx', '.ts', '.js', '.json'],
},

反对 typescript

批改 src/index.js 文件为src/index.tsx

entry: {app: resolve(__dirname, '../src/index.tsx'),
},

每个 Typescript 都会有一个 tsconfig.json 文件,其作用简略来说就是:

  • 编译指定的文件
  • 定义了编译选项

在控制台输出上面代码能够生成 tsconfig.json 文件:

npx tsc --init

默认的 tsconfig.json 的配置有点乱不好治理,这里举荐深刻了解 TypeScript-tsconfig-json 查看更多,关上tsconfig.json,输出新的配置:

{
  "compilerOptions": {
    // 根本配置
    "target": "ES5",                          // 编译成哪个版本的 es
    "module": "ESNext",                       // 指定生成哪个模块零碎代码
    "lib": ["dom", "dom.iterable", "esnext"], // 编译过程中须要引入的库文件的列表
    "allowJs": true,                          // 容许编译 js 文件
    "jsx": "react",                           // 在 .tsx 文件里反对 JSX
    "isolatedModules": true,
    "strict": true,                           // 启用所有严格类型查看选项
        "noImplicitAny": false, // 容许 any 类型

    // 模块解析选项
    "moduleResolution": "node",               // 指定模块解析策略
    "esModuleInterop": true,                  // 反对 CommonJS 和 ES 模块之间的互操作性
    "resolveJsonModule": true,                // 反对导入 json 模块
    "baseUrl": "./",                          // 根门路
    "paths": {                                // 门路映射,与 baseUrl 关联
      "@/*": ["src/*"],
    },

    // 实验性选项
    "experimentalDecorators": true,           // 启用实验性的 ES 装璜器
    "emitDecoratorMetadata": true,            // 给源码里的装璜器申明加上设计类型元数据

    // 其余选项
    "forceConsistentCasingInFileNames": true, // 禁止对同一个文件的不统一的援用
    "skipLibCheck": true,                     // 疏忽所有的申明文件(*.d.ts)的类型查看
    "allowSyntheticDefaultImports": true,     // 容许从没有设置默认导出的模块中默认导入
    "noEmit": true                                                      // 只想应用 tsc 的类型查看作为函数时(当其余工具(例如 Babel 理论编译)时)应用它
  },
  "exclude": ["node_modules"]
}

因为 eslint 的起因,这里配置的 baseUrlpaths别名还是会报错,解决这个问题还须要装置依赖包:

npm install eslint-import-resolver-typescript -D

批改 eslintrc.js 文件的 setting 字段:

settings: {
  'import/resolver': {
    node: {extensions: ['.tsx', '.ts', '.js', '.json'],
    },
    typescript: {},},
},

这里编译 typescript 用到的是 @babel/preset-typescriptfork-ts-checker-webpack-plugin

  • @babel/preset-typescript 编译 ts 代码很粗犷,间接去掉 ts 的类型申明,再用其余 babel 插件进行编译
  • fork-ts-checker-webpack-plugin 尽管用 preset-typescript 编译简略粗犷速度快,然而启动和编译过程中控制台还是会短少类型查看的谬误揭示

装置插件:

npm install @babel/preset-typescript fork-ts-checker-webpack-plugin -D

webpack.common.js 减少上面代码:

const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')

module.exports = {
    plugins: [
    // 其它 plugin...
    new ForkTsCheckerWebpackPlugin({
      typescript: {configFile: resolve(__dirname, '../tsconfig.json'),
      },
    }),
  ]
}

.babelrc 增加 preset-typescript :

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

最初装上 React 类型申明文件:

npm install @types/react @types/react-dom -D

测试

src文件夹下新建 index.tsxApp.tsx文件,输出上面内容测试:

  • index.tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

ReactDOM.render(<App age={12} name="test" />, document.querySelector("#root"));
  • App.tsx
import React from "react";

interface IProps {
    name: string;
    age: number;
}

function App(props: IProps) {const { name, age} = props;
    return (
        <div className="app">
            <span>{`Hello! I'm ${name}, ${age} years old.`}</span>
        </div>
    );
}

export default App;

优化

显示编译进度

webpackbar 能够在启动或编译的时候显示打包进度

npm install webpackbar -D

在 webpack.common.js 减少以下代码:

const WebpackBar = require("webpackbar");

class Reporter {done(context) {if (config.isDev) {console.clear();
            console.log(` 启动胜利:${config.SERVER_HOST}:${config.SERVER_PORT}`);
        }
    }
}

module.exports = {
    plugins: [
    // 其它 plugin...
    new WebpackBar({
            name: config.isDev ? "正在启动" : "正在打包",
            color: "#fa8c16",
            reporter: new Reporter()})
  ]
}

放慢二次编译速度

hard-source-webpack-plugin 为程序中的模块(如 lodash)提供了一个两头缓存,放到本我的项目 node_modules/.cache/hard-source  下,首次编译时会消耗略微比原来多一点的工夫,因为它要进行一个缓存工作,然而再之后的每一次构建都会变得快很多

npm install hard-source-webpack-plugin -D

webpack.common.js 中减少以下代码:

const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')

module.exports = {
    plugins: [
    // 其它 plugin...
    new HardSourceWebpackPlugin(),]
}

external 缩小打包体积

咱们其实并不想把 reactreact-dom 打包进最终生成的代码中,这种第三方包个别会剥离进来或者采纳 CDN 链接模式

批改 webpack.common.js,减少以下代码:

module.exports = {
  externals: {
    react: 'React',
    'react-dom': 'ReactDOM',
  },
}

能够通过两种形式引入

  • CDN 形式引入:
<!DOCTYPE html>
<html lang="en">
  <body>
    <div id="root"></div>
+   <script crossorigin src="https://unpkg.com/react@16.13.1/umd/react.production.min.js"></script>
+   <script crossorigin src="https://unpkg.com/react-dom@16.13.1/umd/react-dom.production.min.js"></script>
  </body>
</html>
  • 本地引入:

public 文件夹下新建 lib 文件夹,寄存咱们的公共文件:

public/
    index.html
    lib/
        react.production.min.js
        react-dom.production.min.js

DllPlugin

另外一种通过 dll 动态链接库的形式也能够达到缩小打包体积的作用,这里不做示例了,举荐一步到位的 autodll-webpack-plugin;

splitChunks

React 组件能够借助 React.lazyReact.Suspense 进行懒加载,具体能够看上面的示例:

import React, {Suspense, useState} from 'react'

const ComputedOne = React.lazy(() => import('Components/ComputedOne'))
const ComputedTwo = React.lazy(() => import('Components/ComputedTwo'))

function App() {const [showTwo, setShowTwo] = useState<boolean>(false)

 return (
   <div className='app'>
     <Suspense fallback={<div>Loading...</div>}>
       <ComputedOne a={1} b={2} />
       {showTwo && <ComputedTwo a={3} b={4} />}
       <button type='button' onClick={() => setShowTwo(true)}>
         显示 Two
       </button>
     </Suspense>
   </div>
 )
}

export default App

通过懒加载的加载的组件会打出独立的 chunk 文件,为了让第三方依赖也打进去独立 chunk,须要在 webpack.common.js 中减少以下代码:

module.exports = {
    // other...
  externals: {//...},
  optimization: {
    splitChunks: {
      chunks: 'all',
      name: true,
    },
  },
}

热更新

后面 devServer 其实曾经做了热更新的配置,然而批改 js 代码还是不能达到部分刷新的目标,这里还要在入口文件 index.jsx 增加判断:

if (module && module.hot) {module.hot.accept()
}

因为 ts 的缘故,会导致未声明的文件报错,这里还要装置@types/webpack-env

npm install @types/webpack-env -D

生产环境优化

款式解决

抽离款式

装置mini-css-extract-plugin:

npm install mini-css-extract-plugin -D

build/utils.js新增上面代码:

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

const getCssLoaders = (importLoaders) => [
  isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
  // ....
]

webpack.prop.js新增上面代码:

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

module.exports = {
    plugins: [
    // 其它 plugin...
    new MiniCssExtractPlugin({filename: 'css/[name].[contenthash:8].css',
      chunkFilename: 'css/[name].[contenthash:8].css',
      ignoreOrder: false,
    }),
  ]
}

去除无用款式

npm install purgecss-webpack-plugin glob -D

webpack.prop.js新增上面代码:

const glob = require("glob");
const PurgeCSSPlugin = require('purgecss-webpack-plugin')
const {resolve} = require("path");

module.exports = merge(common, {
  plugins: [
    // ...
    new PurgeCSSPlugin({paths: glob.sync(`${resolve(__dirname, "../src")}/**/*.{tsx,scss,less,css}`, {nodir: true}),
            whitelist: ["html", "body"]
        })
  ],
})

代码压缩

npm install optimize-css-assets-webpack-plugin terser-webpack-plugin@4 -D

webpack.prop.js新增上面代码:

const TerserPlugin = require("terser-webpack-plugin");
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')

module.exports = merge(common, {
    //...
  optimization: {
        minimize: true,
        minimizer: [
            new TerserPlugin({
                extractComments: false,
                terserOptions: {compress: { pure_funcs: ["console.log"] }
                }
            }),
            new OptimizeCssAssetsPlugin()]
    },
    plugins:[...]
})

增加包正文

webpack.prop.js新增上面代码:

const webpack = require('webpack')

module.exports = merge(common, {
  plugins: [
    // ...
    new webpack.BannerPlugin({
      raw: true,
      banner: '/** @preserve Powered by chenwl */',
    }),
  ]
})

打包剖析

npm install webpack-bundle-analyzer -D

webpack.prop.js新增上面代码:

const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer')

module.exports = merge(common, {
  plugins: [
    // ...
    new BundleAnalyzerPlugin({
      analyzerMode: 'server',                   // 开一个本地服务查看报告
      analyzerHost: '127.0.0.1',            // host 设置
      analyzerPort: 8081,                           // 端口号设置
    }),
  ],
})

正文完
 0