-
Notifications
You must be signed in to change notification settings - Fork 0
Description
一个react项目
这是一篇关于搭建react项目的基础文章。我决定一个react项目包含前端工程化部分和react技术栈部分。对于前端工程化,采用webpack做工程化是主流的方式。react的技术栈会用到redux和react-router。
webpack的职责
webpack的功能是模块化打包工具。
使用webpack 4
所以,直接使用babel是可以编译react的jsx的,但是使用webpack做工程化很重要,很多开发工作都需要用到webpack,所以可以先了解基础的webpack功能。先创建文件夹 an-react-project
npm initnpm 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.htmlwebpack.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热替换,需要以下步骤:
- 使用webpack.HotModuleReplacementPlugin,在配置中把derServer hot设置为ture。
即:
...
devServer: {
contentBase: './dist',
hot: true
},
plugins: [
new webpack.HotModuleReplacementPlugin()
...
],
...- 对于基础项目(原始js项目),需要在入口js文件中监听引用的模块
例子:
if (module.hot) {
module.hot.accept('./app.js', function(){
// app.js变化之后需要做的操作,这根据具体场景配置
})
}react项目的热更新配置
对于react项目,需要用到第三方的插件 react-hot-loader。基于webpack的HotModuleReplacementPlugin,所以上面webpack的配置应该保留。但是入口文件则无需你处理module.hot.accept里面的逻辑。
- 安装:
yarn add react-hot-loader --dev
- 入口组件,比如上面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);- 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可以通过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 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声明。
现在可以假设一个项目,后台项目包括登陆,注册,以及登陆进去之后内容页面,内容页面包含不同路由对应的路由模块。
