wython / wython.github.io

个人博客记录, 订阅点击watch
10 stars 0 forks source link

一个react项目 #8

Open wython opened 5 years ago

wython commented 5 years ago

一个react项目

这是一篇关于搭建react项目的基础文章。我决定一个react项目包含前端工程化部分和react技术栈部分。对于前端工程化,采用webpack做工程化是主流的方式。react的技术栈会用到redux和react-router。

webpack的职责

webpack的功能是模块化打包工具。

使用webpack 4

所以,直接使用babel是可以编译react的jsx的,但是使用webpack做工程化很重要,很多开发工作都需要用到webpack,所以可以先了解基础的webpack功能。先创建文件夹 an-react-project

npm init
npm install --save-dev webpack webpack-cli webpack-dev-server

初始化npm, 安装webpack。然后弄一个简单的目录结构了解下webpack的基本功能。这样安装默认就是最新webpack4

|-- dist
    |-- index.html
|-- src
    |-- index.js
    |-- moduleA.js
    |-- moduleB.js
|-- package.json
|-- webpack.config.js

webpack.config.js:

const path = require('path');
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
}

src/index.js:

// index.js
import moduleA from './moduleA';
import moduleB from './moduleB';

function createComponent() {
  var element = document.createElement('div');

  var btn = document.createElement('button');
  var btnTwo = document.createElement('button');
  element.innerHTML = 'Hello World';

  btn.innerHTML = 'print btn';
  element.appendChild(btn);
  element.appendChild(btnTwo);
  btn.onclick = moduleA
  btnTwo.onclick = moduleB
  return element;
}

document.body.appendChild(createComponent())

src/moduleA.js:

export default function printHello() {
  console.log('Ok')
  console.log('From printHello')
}

src/moduleB.js:

export default function printHelloTwo() {
  console.log('Yes')
  console.log('From moduleB')
}

package.js定义命令:

...
"scripts": {
    "start": "webpack --config webpack.config.js",
},
...

执行npm run start 或者 yarn start命令可以看到dist中已经打包出了bundle.js。直接访问index.html可以看到静态页面。目前的配置和react没有任何关系,仅仅只是webpack的基本功能。并且定义了一个入口和输出路径。具体的可以看webpack文档指南。有更详细的配置细节。

source map: webpack打包后的代码,如果需要追踪错误位置。就比较难,source map功能可以定义webpack配置的devtool来追踪源代码。

const path = require('path');
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  devtool: 'inline-source-map'
}

不过越原始的追踪带来性能会更差。可以看下文档支持的配置: webpack devtool

webpack-dev-server:

上面已经安装了webpack-dev-server。开发环境通过devServer配置开启。可以不需要每次修改都重新编译。实时监听编译。webpack有三种方式支持监听,watch配置,webpack-dev-middleware配置。webpack-dev-server就是基于webpack-dev-middleware实现的,同时具有更多功能配置。一般都会用webpack-dev-server,但是如果希望自己编写server逻辑,可以考虑结合node后端和middleware自己实现。

const path = require('path');
module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  devtool: 'inline-source-map',
  devServer: {
    contentBase: './dist'
  }
}

这个时候修改package.json启动命令用webpack-dev-server启动可以看到浏览器会用node服务方式访问页面。

webpack插件配置html-webpack-plugin和clean-webpack-plugin:

当然,因为index.html是我们自己编写的,一般会通过html-webpack-plugin维护html,这个插件非常有必要,因为对于后面的项目部署,动态生成hash文件名的方式引入html中,如果人为维护基本是一件很繁琐的事情,插件可以根据配置自己引入script脚本。

clean-webpack-plugin用于每次启动或者编译工程时候保持文件夹是干净的。它会清理文件夹下的命令。

webpack插件的配置通过plugin数组配置。

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  devtool: 'inline-source-map',
  devServer: {
    contentBase: './dist'
  },
  plugins: [
    new CleanWebpackPlugin({ default: [ 'dist' ] }),
    new HtmlWebpackPlugin({
      title: 'hello world'
    })
  ]
}

再次启动项目时候,这时候dist文件夹应该已经没有文件。因为开发环境下,dev-server是将编译文件载入内存中。这样可以提高更新效率,因为对计算机而已,读取硬盘比读内存要耗时的多。

Babel的职责

Babel的工作是转换js语法,比如平时用到的jsx,浏览器不支持的es6语法,ts语法。都是babel做编译的工作。如果不了解每一个模块的职责,很容易混淆webpack,babel的关系。

使用babel

配置babel有两种方式,一种是通过创建babel.config.js配置文件,另一种是.babelrc。前者是js形式,如果希望做一些脚本工作通过配置去配置是不错的。不过我们需要借助webpack loader方式去做,所以不需要在两个文件中做配置。

babel是通过plugins和presets的两种方式去扩展需要的语法。

{
  "presets": [],
  "plugins": []
}

presets是一组plugins的集合。一般来说用已有的preset足够满足要求。安装babel和@babel/core。和react的@babel/preset-react。同时安装react和react-dom框架和

yarn add babel @babel/core @babel/preset-react --dev
yarn add react react-dom --save

重新编辑src/index.js:

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(<h1>hello world!</h1>, document.getElementById('app'))

整理webpack配置文件

现在只有一个webpack.config.js,不过一般项目会分开发环境和生产环境,不同的环境webpack的职责也不同。所以可以提前建好不同的配置文件,通过webpack-merge合并配置。

yarn add webpack-merge --dev

我自己的话,创建文件夹build,把配置文件放进去。文件目录如下:

  |-- package.json
  |-- yarn.lock
  |-- build
  |   |-- webpack.base.config.js
  |   |-- webpack.dev.config.js
  |   |-- webpack.pro.config.js
  |   |-- webpack.vendor.config.js
  |-- dist
  |-- src
      |-- index.js
      |-- asset
          |-- index.html

webpack.base.config.js:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const webpack = require('webpack');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new CleanWebpackPlugin({ default: [ 'dist' ] }),
    new HtmlWebpackPlugin({
      template: './src/asset/index.html'
    })
  ]
}

webpack.dev.config.js:

const webpackMerge = require('webpack-merge');
const baseConfig = require('../build/webpack.base.config');
const path = require('path');
const srcPath = path.join(__dirname, '../src');
const devConfig = {
  mode: 'development',
  devtool: 'inline-source-map',
  devServer: {
    contentBase: './dist'
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        include: srcPath,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-react'],
            }
          }
        ]
      }
    ]
  }
}

module.exports = webpackMerge(baseConfig, devConfig);

修改package.json命令:

...
"scripts": {
  "start": "webpack-dev-server --open --config ./build/webpack.dev.config.js"
}
...

此时,通过访问server对应页面可以看到结果,说明jsx代码已经成功转义。基础配置完成。

webpack的一些优化

以上配置相对基础,优化是一个持续过程,但是如果一开始能做好的优化,对后续会更有帮助。对webpack的优化可以分开发环境下的优化和生产环境下的优化

开发环境

开发环境下,需要提高实时编译时间,做到最好的开发体验。

1. dllPlugin提取公共库

先介绍下dllPlugin,这个组件用用于单独抽离部分公共组件库。平时开发过程中,有些库,例如上面涉及到的react,react-dom这些库,一般一个项目定型之后,不会频繁修改库的内容和版本。所以上面的配置每一次启动项目都会编译一次公共库。实际上是没有必要的,因为这个过程是重复的,公共库并没有发生变化。最好的思路是将他们提取出来,之后每一次构建就不会再去编译这些代码。

要使用dllPlugin,只需要在webpack.vendor.config.js中配置插件和需要打包的包。然后通过DllReferencePlugin引用依赖关系即可。

在webpack.vendor.config.js中:

const webpack = require('webpack');
const path = require('path');
module.exports = {
  mode: 'development',
  entry: {
    vendor: ['react', 'react-dom']
  },
  output: {
    filename: '[name].dll.js',
    path: path.join(__dirname, '..', 'dist'),
    library: 'vendor_lib_[hash]'
  },
  plugins: [
    new webpack.DllPlugin({
      context: __dirname, // 上下文
      path: path.join(__dirname, '..', 'dist', 'vendor-manifest.json'),
      name: 'vendor_lib_[hash]' // 与out的libirary库名保持一致
    })
  ]
}

插件的path定义的是依赖文件的保存路径,webpack的另一个插件需要这个依赖文件来保证能访问对应库。

webpack.dev.config.js:

const webpackMerge = require('webpack-merge');
const baseConfig = require('../build/webpack.base.config');
const webpack = require('webpack');
const path = require('path');
const srcPath = path.join(__dirname, '../src');
const devConfig = {
  mode: 'development',
  devtool: 'inline-source-map',
  devServer: {
    contentBase: './dist'
  },
  plugins: [
    new webpack.DllReferencePlugin({
      context: __dirname,
      manifest: require('../dist/vendor-manifest.json')
    })
  ],
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        include: srcPath,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-react'],
            }
          }
        ]
      }
    ]
  }
}

module.exports = webpackMerge(baseConfig, devConfig);

然后通过DllReferencePlugin定义即可,具体参数可以看官方文档的配置项目。

2. 使用热替换(HRM)

热替换功能用于提高开发效率,它的功能是可以无刷新页面的情况下重新载入新模块。这个不能和dev-server的实时监控搞混。现在虽然代码修改,页面实时刷新,但是热替换可以做到不刷新页面就能显示内容的更改。

想象这样一个场景,平时在开发类似modal弹窗这样的组件时候,如果你修改了modal页面刷新,modal就不见了,需要重新弹窗。如果用了热替换,实时修改将不会刷新页面,弹窗不会消失,这对开发效率是有一定提高的。

在src文件夹下加上两个文件:

|-- src
    |-- index.js
    + |-- app.js
    + |-- header.js

app.js:

import React, { Component, Fragment } from 'react';
import Header from './header';

export default class App extends Component {
  render() {
    return (
      <Fragment>
        <Header />
        <h1>hello world!</h1>
      </Fragment>

    )
  }
}

header.js:

import React, {  Component } from 'react';

export default class Header extends Component {
  render() {
    return (
      <header>头部</header>
    )
  }
}

index.js:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './app';

ReactDOM.render(<App />, document.getElementById('app'))

现在如果修改header.js组件页面会刷新一下。

要开启webpack热替换,需要以下步骤:

  1. 使用webpack.HotModuleReplacementPlugin,在配置中把derServer hot设置为ture。

即:

...
devServer: {
  contentBase: './dist',
  hot: true
},
plugins: [
  new webpack.HotModuleReplacementPlugin()
  ...
],
...
  1. 对于基础项目(原始js项目),需要在入口js文件中监听引用的模块 例子:
    if (module.hot) {
    module.hot.accept('./app.js', function(){
    // app.js变化之后需要做的操作,这根据具体场景配置
    })
    }

react项目的热更新配置

对于react项目,需要用到第三方的插件 react-hot-loader。基于webpack的HotModuleReplacementPlugin,所以上面webpack的配置应该保留。但是入口文件则无需你处理module.hot.accept里面的逻辑。

  1. 安装:

    yarn add react-hot-loader --dev
  2. 入口组件,比如上面react例子中的app.js

app.js:

import React, { Component, Fragment } from 'react';
import Header from './header';

import { hot } from 'react-hot-loader/root';

const App = class App extends Component {
  render() {
    return (
      <Fragment>
        <Header />
        <h1>hello world!</h1>
      </Fragment>

    )
  }
}

export default hot(App);
  1. babel配置中添加react-hot-loader

我是写在options里面,如果使用.babelrc配置,一样道理。

{
  presets: ['@babel/preset-react'],
  plugins: ['react-hot-loader/babel']
}

此时热更新应该生效,修改header文本,可以看到页面无刷新修改了最新内容。不过此时启动项目,react-hot-loader会有个wraning,大体意思是对于react 16.4之后的版本,应该使用扩展@hot-loader/react-dom,否则无法在部分特性里面使用热更新,其实指的是react hook的语法。

只需要按照扩展,然后定义别名使得原始的react-dom包从@hot-loader/react-dom中获取就行了。

yarn add @hot-loader/react-dom

webpack.dev.config.js完整配置如下:

const webpackMerge = require('webpack-merge');
const baseConfig = require('../build/webpack.base.config');
const webpack = require('webpack');

const path = require('path');
const srcPath = path.join(__dirname, '../src');

const devConfig = {
  mode: 'development',
  devtool: 'inline-source-map',
  plugins: [
    new webpack.DllReferencePlugin({
      context: __dirname,
      manifest: require('../dist/vendor-manifest.json')
    }),
    new webpack.HotModuleReplacementPlugin()
  ],
  devServer: {
    contentBase: './dist',
    hot: true
  },
  resolve: {
    alias: {
      'react-dom': '@hot-loader/react-dom'
    }
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        include: srcPath,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-react'],
              plugins: ['react-hot-loader/babel']
            }
          }
        ]
      }
    ]
  }
}

module.exports = webpackMerge(baseConfig, devConfig);

减少查找范围

其实我上面配置对插件的include已经设置了,实际上就是js文件只查找src文件夹下的文件。

针对性的loader实行优化

这个是具有灵活性的优化,比如以上的babel-loader。来看看官方文档对babel-loader的自我评价。

babel-loader

babel-loader可以通过cacheDirectory实现缓存。所以,用上它。修改配置:

...
rules: [
  {
    test: /\.(js|jsx)$/,
    include: srcPath,
    use: [
      {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-react'],
          plugins: ['react-hot-loader/babel'],
          cacheDirectory: '../runtime_cache/'
        }
      }
    ]
  }
]
...

有名气的happypack

作用:webpack本身是单线程处理模块的,happyPack可以让部分loader在多线程下去处理文件。

那平时对一些比较耗时的loader可以使用happyPack做性能优化。比如上面的babel,它自己都说自己很慢。

使用happypack,需要修改loader和插件,我们只修改部分费时的loader就行了,loader里面:

webpack.dev.config.js

const webpackMerge = require('webpack-merge');
const baseConfig = require('../build/webpack.base.config');
const webpack = require('webpack');
const HappyPack = require('happypack');

const path = require('path');
const srcPath = path.join(__dirname, '../src');

const devConfig = {
  mode: 'development',
  devtool: 'inline-source-map',
  plugins: [
    new webpack.DllReferencePlugin({
      context: __dirname,
      manifest: require('../dist/vendor-manifest.json')
    }),
    new webpack.HotModuleReplacementPlugin(),
    new HappyPack({ 
      id: 'js',
      loaders: [ 
        {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-react'],
            plugins: ['react-hot-loader/babel'],
            cacheDirectory: '../runtime_cache/'
          }
        } 
      ]
    })
  ],
  devServer: {
    contentBase: './dist',
    hot: true
  },
  resolve: {
    alias: {
      'react-dom': '@hot-loader/react-dom'
    }
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        include: srcPath,
        use: 'happypack/loader?id=js'
      }
    ]
  }
}

module.exports = webpackMerge(baseConfig, devConfig);

整理目录接入ts

对于一个开发团队来说,ts可能不是必要的。首先,要权衡typescript是否适合在一个项目中使用。ts是js的一个超集。它提供了一些类型校验的东西,当然也会牺牲一些开发效率上的东西。但是从长远来说,对开发效率更多是利大于弊,它可以避免一些人为错误,数据流转中松散类型带来对不确定问题。我觉得一个项目适不适合用ts,主要还是看这个项目是不是长期迭代,体量的大小。对于一些用完即走的活动页,可能ts反而是一个累赘。另外,本身折腾ts也是一个繁琐的事情,包括接口,类型的定义,有时候比正常js会多一些工作量。如果不按ts规范走,那其写法也和js没有区别,还不如不用。

webpack中使用ts思路很简单。因为本身ts只需要一个tsconfig.json的文件。而前端打包ts主要还是ts-loader和babel-loader两种。这里我两种都尝试了,最后babel-loader对热加载更友好。因为热加载插件中官方文档对ts对说法是这样的。

react-hot-loader 大体意思是,对于react-hot-loader 4的版本来说,你必须用babel转换才行,这对于一些其实不需要使用babel的用户来说不太友好。幸运的是,babel的配置很简单,而且集成的也很好。所以,让你大胆的用babel-loader代替ts-loader去做这件事。

安装: @babel/preset-env, @babel/preset-typescript,@babel/plugin-proposal-decorators,@babel/plugin-proposal-class-properties 四个preset集合,env集成了很多常用写法,typescript就是用babel转ts的集合。后面两个是在ts中使用装饰器语法所用。 安装: core-js regenerator-runtime babel7.4之后把polyfill拆分成两个模块。如果需要做babel升级迁移,要考虑polyfill问题。

base.js:

extensions: ['.ts', '.tsx', '.js', '.json'],  // 默认是['.js', '.json'], ts需要扩展支持的后缀名

配置功能是不需要写后缀即可导入模块

test正则修改为:test: /.(j|t)sx?$/, loader中添加babel上面装对preset,和插件

new HappyPack({ 
      id: 'js',
      loaders: [ 
        {
          loader: 'babel-loader',
          type: 'javascript/auto',
          options: {
            presets: [
              +++ "@babel/preset-env",
              +++ "@babel/preset-typescript",
              '@babel/preset-react'
            ],
            plugins: [
              +++ ['@babel/plugin-proposal-decorators', { legacy: true }],
              +++ ['@babel/plugin-proposal-class-properties', { loose: true }],
              'react-hot-loader/babel'
            ],
            cacheDirectory: './runtime_cache/'
          }
        } 
      ]
    })

然后src都.js文件都改成ts写法。基本ts的引入就完成了。

引入less和文件模块化

less需要less-loader转换,css需要css-loader转换,style-loader将样式提取到style标签中,生产环境则用mini-css-extract-plugin将样式提取到单独文件中。

less: less-loader() css-loader(解释(interpret) @import 和 url()) extract-loader to-string-loader style-loader(inject