Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

一个react项目 #8

Open
Open
@wython

Description

一个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
  1. 入口组件,比如上面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 <style>) extractTextPlugin

项目可能用到的技术

上面基本罗列了开发前需要做的工作,webpack部分开发配置,但在整体还是和简陋的。不过到这里,就可以开始思考一个项目可能会用到的一些东西。比如单页router,状态管理redux等,由于用ts语法,也可以试试比较友好的mobx。但这些其实不是重点,重点是保证项目但稳定,开发效率,以及未来可能但性能,扩展。

所以,先装上react-router,antd,mobx,react-mobx以及相关的@types声明。

现在可以假设一个项目,后台项目包括登陆,注册,以及登陆进去之后内容页面,内容页面包含不同路由对应的路由模块。

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

        AltStyle によって変換されたページ (->オリジナル) /