这篇文章致力于从npm init 一步一步搭建react企业级项目,主要包含一下几点:javascript
首先奉上项目地址react-base-projectcss
.DS_Store
.vscode
node_modules/
dist/
npm-debug.log
yarn.lock
package-lock.json
复制代码
npm i webpack webpack-cli webpack-dev-server --save-dev
复制代码
npm i @babel/core @babel/preset-env @babel/preset-react babel-loader @babel/plugin-proposal-class-properties -D
复制代码
presets 使用babel须要安装的插件(也就是支持哪些语法转换成es5)html
presets | 描述 |
---|---|
babel | javascript语法编译器 |
@babel/core | 调用babel api进行转码的核心库 |
@babel/preset-env | 根据运行环境为代码作相应的编译 |
@babel/preset-react | 编译react语法 |
@babel/plugin-proposal-class-properties | 支持class语法插件 |
babel-preset-stage-x(stage-0/1/2/3/4) | 提案的5个阶段,0表示只是一个想法,4表示已完成 |
babel7发布后的变化:前端
@babel/preset-env
替换以前全部的babel-prese-es20xx
preset,java
解决:命名困难;是否被他人占用;区分官方包名node
任何提案都将被以 -proposal- 命名来标记他们尚未在 JavaScript 官方以内。react
因此 @babel/plugin-transform-class-properties 变成 @babel/plugin-proposal-class-properties,当它进入 Stage 4 后,会把它命名回去。webpack
polyfill便是在当前运行环境中用来复制(意指模拟性的复制,而不是拷贝)尚不存在的原生 api 的代码。能让你提早使用还不可用的 APIsgit
Babel 几乎能够编译全部时新的 JavaScript 语法,但对于 APIs 来讲却并不是如此。例如: Promise、Set、Map 等新增对象,Object.assign、Object.entries等静态方法。es6
为了达成使用这些新API的目的,社区又有2个实现流派:babel-polyfill和babel-runtime+babel-plugin-transform-runtime
npm i react react-dom --save
复制代码
react-dom:v0.14+从react核心库中拆离;负责浏览器和DOM操做。还有一个兄弟库react-native,用来编写原生应用。
react-dom主要包括方法有:
presets | 描述 |
---|---|
render | 渲染react组件到DOM中 |
hydrate | 服务端渲染,避免白屏 |
unmountComponentAtNode | 从 DOM 中移除已装载的 React 组件 |
findDOMNode | 访问原生浏览器DOM |
createPortal | 渲染react子元素到制定的DOM中 |
react
:React的核心库;主要包括:React.createElement,React.createClass,React.Component,React.PropTypes,React.Children
npm i html-webpack-plugin -D
npm i react-hot-loader -S
复制代码
const paths = require('./paths');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = function(webpackEnv){
const isEnvDevelopment = webpackEnv === 'development';
const isEnvProduction = webpackEnv === 'production';
let entry = [paths.appIndex];
return {
mode:isEnvProduction ? 'production' : isEnvDevelopment && 'development',
entry:entry,
output:{
path:paths.appDist,
publicPath:'/',
filename:`static/js/[name]${isEnvProduction ? '.[contenthash:8]':''}.js`
},
module:{
rules:[
{
test: /\.jsx?$/,
loader: 'babel-loader'
}
]
},
plugins:[
new HtmlWebpackPlugin({
filename: 'index.html',
template: paths.appHtml,
favicon: 'favicon.ico'
}),
isEnvDevelopment && new webpack.HotModuleReplacementPlugin()//开启HRM
],
devServer: {
publicPath: '/',
host: '0.0.0.0',
disableHostCheck: true,
compress: true,
port: 9001,
historyApiFallback: true,
open: true,
hot:true,
}
}
}
复制代码
const configFactory = require('./webpack.config');
const config = configFactory('development');
module.exports = config;
复制代码
const path = require('path');
const fs = require('fs');
const appDirectory = fs.realpathSync(process.cwd());
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
module.exports = {
appIndex:resolveApp('src/index'), //入口文件
appSrc:resolveApp('src'), //项目代码主目录
appDist:resolveApp('dist'), //打包目录
appHtml:resolveApp('index.html'), //模板文件
}
复制代码
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
复制代码
import { hot } from 'react-hot-loader/root';
import React, { Component } from 'react';
class App extends Component {
render() {
return <h1>hello-react</h1>;
}
}
export default hot(App);
复制代码
{
"presets": [
[
"@babel/preset-env",
{
"modules": false
}
],
"@babel/preset-react"
],
"plugins": ["react-hot-loader/babel","@babel/plugin-proposal-class-properties"]
}
复制代码
webpack-dev-server默认会开启livereload功能(俗称热更新),监听文件有改动,自动刷新页面;但须要实现react组件改动不刷新页面,还须要配合 react-hot-loader 实现(配置可参考官方文档),俗称热替换Hot Module Replacement
。
目前为止,项目就能够运行了 在package.json
文件 scripts
添加脚本命令webpack-dev-server --config config/start.js
并执行;浏览器会打开网页 http://0.0.0.0:9001/ 第一阶段目录结构以下:
.
├── README.md
├── config
│ ├── build.js
│ ├── paths.js
│ ├── start.js
│ └── webpack.config.js
├── favicon.ico
├── index.html
├── package-lock.json
├── package.json
├── src
│ ├── App.js
│ └── index.js
└── yarn.lock
复制代码
项目实际开发中还须要样式文件,本项目以scss为例进行配置
npm i style-loader css-loader postcss-loader sass-loader node-sass autoprefixer -D
presets | 描述 |
---|---|
style-loader | 将css文件插入到html中 |
css-loader | 编译css文件 |
sass-loader | 编译scss文件 |
postcss-loader | 使用javascript插件转换css的工具 |
autoprefixer | 根据用户的使用场景来解析CSS和添加vendor prefixes |
{
test: /\.(sc|c)ss$/,
use: [
isEnvDevelopment && 'style-loader',
'css-loader', 'postcss-loader', 'sass-loader'
].filter(Boolean)
}
复制代码
3.新建postcss.config.js文件
const autoprefixer = require('autoprefixer');
module.exports = {
plugins: [autoprefixer]
};
复制代码
4.package.json添加兼容浏览器列表

"browserslist": ["iOS >= 8","> 1%","Android > 4","last 5 versions"]
复制代码
5.项目入口文件index.js引入全局scss文件
import './assets/styles/app.scss';
6.从新启动项目,能够看到样式已经插入head中
npm i file-loader url-loader -D
presets | 描述 |
---|---|
file-loader | 解决项目中引用本地资源(图片、字体、音视频等)相对路径问题;这里我测试了下,绝对路径不会出现路径问题 |
url-loader | 对资源文件作转dataURI处理 |
{
test: /\.(gif|png|jpe?g|svg)(\?.*)?$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10000,
name: 'img/[name].[ext]?[hash]'
}
}
]
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'fonts/[name].[hash:7].[ext]'
}
}
复制代码
首先固然是安装依赖,为何是react-router-dom而不是react-router; 在react-router4.0.0+版本;官方提供了一套基于react-router封装的用于运行在浏览器端的react-router-dom;react-router-native是用于开发react-native应用。
npm install react-router-dom --save
复制代码
React Router中有三类组件
基于React Router的web应用,根组件应该是一个router组件;react-router-dom提供了两种路由模式;
<BrowserRouter>
:使用HTML5 提供的 history API (pushState, replaceState 和 popstate 事件),动态展现组件
<HashRouter>
:经过监听window.location.hash的改变,动态展现组件 最直观的感觉就是BrowserRouter不会再浏览器URL上追加#,为了地址的优雅固然首选这种模式,但若是是静态服务器,那就只能使用备选方案HashRouter了。
react-router-dom中有两个匹配路由的组件: 和
// 当 location = { pathname: '/about' }
<Route path='/about' component={About}/> // 路径匹配成功,渲染 <About/>组件
<Route path='/contact' component={Contact}/> // 路径不匹配,渲染 null
<Route component={Always}/> // 该组件没有path属性,其对应的<Always/>组件会一直渲染
复制代码
咱们能够在组件树的任何位置放置<Route>
组件。可是更常见的状况是将几个<Route>
写在一块儿。<Switch>
组件能够用来将多个<Route>
“包裹”在一块儿。 多个组件在一块儿使用时,并不强制要求使用<Switch>
组件,可是使用<Switch>
组件倒是很是便利的。<Switch>
会迭代它下面的全部<Route>
子组件,并只渲染第一个路径匹配的<Route>
。
这里是在根组件引入新建路由组件(src/routes/index.js)。
<BrowserRouter basename="/">
<Switch>
<Route exact path="/" component={Home} /> //exact彻底匹配
<Route path="/shopping" component={Shopping} />
<Route path="/contact" component={Contact} />
<Route path="/detail/:id" component={Contact} />
{/* 可经过this.props.match获取路由参数 */}
{/* 若是上面的Route的路径都没有匹配上,则 <NoMatch>被渲染,咱们能够在此组件中返回404 */}
<Route component={NoMatch} />
</Switch>
</BrowserRouter>
复制代码
//to: string
<Link to="/about?tab=name" />
//to: object
<Link
to={{
pathname: "/courses",
search: "?sort=name",
hash: "#the-hash",
state: { fromDashboard: true } //传入下一个页面额外的state参数
}}
/>
复制代码
在不一样的React版本中,使用方法稍有差别,下面总结了各版本的使用方法
import { useHistory } from "react-router-dom";
function HomeButton() {
let history = useHistory();
// use history.push('/some/path') here
};
复制代码
class Example extends React.Component {
// use `this.props.history.push('/some/path')` here
};
复制代码
class Example extends React.Component {
// use `this.props.router.push('/some/path')` here
};
复制代码
npm install redux react-redux --save
复制代码
redux
是一个“可预测的状态容器”,参考了flux
的设计思想,
单一数据源
一个应用只有惟一的数据源,好处是整个应用的状态都保存在一个对象中,这样能够随时去除整个应用的状态进行持久化;固然若是一个复杂项目也能够用Redux
提供的工具函数combineReducers
对数据进行拆分管理。
状态是只读的
React
并不会显示定义store,而使用Reducer返回当前应用的状态(state),这里并非修改以前的状态,而是返回一个全新的状态。
React提供的createStore方法会根据Reducer生成store,最后能够用store.disputch方法修改状态。
状态修改均由纯函数完成
这使得Reducer里对状态的修改变得简单、纯粹
Redux的核心是一个store,这个store由Redux提供的createStore(reducers[,initalState])
方法生成。
reducers必传参数用来响应由用户操做产生的action,reducer本质是一个函数,其函数签名为reducer(previousState,action)=>newState
;reducer的职责就是根据previousState和action计算出新的state;在实际应用中reducer在处理previousState时,须要有一个非空判断。很显然,reducer第一次执行的时候没有任何previousState,而reducer的职责时返回新的state,所以须要在这种特殊状况返回一个定义好的initalState。
Redux 官方提供的 React 绑定-react-redux。这是一种前端框架或类库的架构趋势,即尽量作到平台无关。 react-redux提供了一个组件和一个API,一个是React组件,接受一个store做为props,它是整个Redux应用的顶层组件;一个是connect(),它提供了在整个React应用的任意组件中获取store中数据的功能。
import { Provider } from 'react-redux';
import store from './redux/index';
ReactDOM.render(<Provider store={store}><App /></Provider>, rootEl);
复制代码
import reducers from './reducers/index'
export default createStore(reducers);
复制代码
export default (state=[],action)=>{
switch (action.type){
case 'RECEIVE_PRODUCTS':
return action.products;
default:
return state;
}
}
复制代码
import { connect } from 'react-redux'
const ProductsContainer = ({products,getAllProducts}) => (
<button onClick={getAllProducts}>获取数据</button>
)
const mapStateToProps = (state) => ({
products:state.products
})
const mapDispatchToProps = (dispatch, ownProps)=> ({
getAllProducts:() => {
dispatch({ type: 'RECEIVE_PRODUCTS', [1,2,3]})
}
})
export default connect(mapStateToProps, mapDispatchToProps)(ProductsContainer)
复制代码
项目中测试环境和生产环境经常有些全局变量是不一样的;最典型的api接口域名部分、跳转地址域名部分; 咱们能够在webpack的plugin中设置DefinePlugin:
//向浏览器环境注入全局变量,非window下
new webpack.DefinePlugin({
'process.env': env //env 获取本地的静态文件
})
复制代码
但在webpack node环境中还不能区分测试和生产环境,由于webpack build打包向node注入的NODE_ENV
都是produiction
,因此process.env.NODE_ENV
是相同的。
这里结合cross-env向node环境手动注入一个标记参数NODE_ENV_MARK
;package代码以下:
"scripts": {
"dev": "cross-env NODE_ENV_MARK=dev webpack-dev-server --config config/start.js",
"build:test": "cross-env NODE_ENV_MARK=test node config/build.js",
"build:prod": "cross-env NODE_ENV_MARK=production node config/build.js"
}
复制代码
webpack.config.js中根据NODE_ENV_MARK
变量获取对应的文件:
const env = require(`../env/${process.env.NODE_ENV_MARK}.env`);
复制代码
env目录下添加dev.env.js/test.env.js/production.env.js;文件内容根据实际状况进行编辑
module.exports = {
NODE_ENV: '"production"',
prefix: '"//api.abc.com"'
};
复制代码
这样在浏览器环境中就可使用process.env.prefix
变量了。
到此项目配置基本告一段落,一下是对项目进行的一些优化。
如下都是基于webpack4作的优化配置
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
plugins: [
!isEnvDevelopment && new CleanWebpackPlugin()
]
复制代码
{
test: /\.jsx?$/,
loader: 'babel-loader',
include: paths.appSrc,
exclude: /node_modules/
}
复制代码
loader: 'babel-loader?cacheDirectory=true'
复制代码
noParse: /lodash/
复制代码
const os = require("os");
const HappyPack = require("happypack");
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
plugins:[
new HappyPack({
id: "happyBabel",
loaders: ["babel-loader?cacheDirectory=true"],
threadPool: happyThreadPool,
verbose: true
})
]
复制代码
为了方便调试线上的问题,sourcemap就是对应打包后代码和源码的一个映射文件。
devtool: isEnvDevelopment ? 'cheap-module-eval-source-map' : 'source-map',
复制代码
1.package.json新增scripts脚本
"analyze": "cross-env NODE_ENV_REPORT=true npm run build:prod"
2.webpackage.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
plugins:[
process.env.NODE_ENV_REPORT && new BundleAnalyzerPlugin()
]
3.浏览器会自动打开 http://127.0.0.1:8888
复制代码
分析报告以下图:
const MiniCssExtractPlugin=require('mini-css-extract-plugin');
plugins:[
isEnvProduction && new MiniCssExtractPlugin({
filename:'static/css/[name].[contenthash:10].css'
})
]
//loader修改
{
test: /\.(sc|c)ss$/,
use: [
- isEnvDevelopment && 'style-loader',
+ MiniCssExtractPlugin.loader,
'css-loader', 'postcss-loader', 'sass-loader'
].filter(Boolean)
}
复制代码
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
optimization: {
minimizer: [
new UglifyJsPlugin({
cache:true,
parallel:true,
sourceMap:true
}),
new OptimizeCSSAssetsPlugin()
],
复制代码
在对项目拆包前,先对对页面路由进行懒加载
- import Home from '../pages/home/Home';
+ import Loadable from 'react-loadable';
+ const loading = () => { return ( <div> loading... </div> ) }
+ const Home = Loadable({loader: () => import(/* webpackChunkName: "Home" */ '../pages/home/Home'),loading:loading});
复制代码
import()函数是es6的语法,是一种动态引入的方式,返回一个Promise
对项目的拆包主要是如下几个方面:
splitChunks:{
cacheGroups:{
dll: { //项目基础框架库
chunks:'all',
test: /[\\/]node_modules[\\/](react|react-dom|react-redux|react-router-dom|redux)[\\/]/,
name: 'dll',
priority:100, //权重
enforce: true,// 为此缓存组建立块时,告诉webpack忽略minSize,minChunks,maxAsyncRequests,maxInitialRequests选项
reuseExistingChunk: true // 可设置是否重用已用chunk 再也不建立新的chunk
},
lodash: { //经常使用的比较大的三方库
chunks:'all',
test: /[\\/]node_modules[\\/](lodash)[\\/]/,
name: 'lodash',
priority: 90,
enforce: true,
reuseExistingChunk: true
},
commons: { //项目中使用的其余公共库
name: 'commons',
minChunks: 2, //Math.ceil(pages.length / 3), 当你有多个页面时,获取pages.length,至少被1/3页面的引入才打入common包
chunks:'all',
reuseExistingChunk: true
}
},
chunks: 'all',
name: true,
}
复制代码
下面图是通过拆包后的结果
再用speed-measure-webpack-plugin分析下webpack各环节的打包速度
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
//用smp.wrap方法把webpack配置文件处理下
webpackConfig = smp.wrap({
//webpack配置对象
});
复制代码
打包后以下图:
import Cart from '../components/Cart.jsx' //配置前
import Cart from '../components/Cart' //配置后
resolve: {
extensions:['.js','.jsx','.json']
}
复制代码
import product from '../../../redux/reducers/products' //配置前
import product from '@redux/reducers/products' //配置后
alias:{
'@redux':paths.appRedux
}
复制代码
//这是6.0+的语法
new CopyWebpackPlugin({
patterns:[{
from:paths.appStatic,
to:'static/',
}]
})
复制代码
npm install eslint --save-dev
eslint --init
(根据项目状况选择) 这时候运行eslint src/index.js会报错React version not specified in eslint-plugin-react settings
,是没有配置react的版本"settings":{
"react": {
"version": "detect", // React version. "detect" automatically picks the version you have installed.
}
}
复制代码
**/dist/**
**/node_modules/**
**/config/**
复制代码
若是是新项目加入eslint,extends建议使用airbnb
,这样会约束你编写出更加优雅的代码,这样渐渐的也就会成为你的编码风格
npm i eslint-plugin-react eslint-config-airbnb eslint-plugin-import eslint-plugin-jsx-a11y -D
extends: [
'airbnb'
]
复制代码
若是是老项目加入eslint,extends建议使用"eslint:recommended"
和"plugin:react/recommended"
npm i eslint-plugin-react -D
extends: [
"eslint:recommended",
"plugin:react/recommended"
]
复制代码
这里我项目中使用airbnb;这时候运行eslint src
,会发现有不少相似这种的报错
可使用eslint src --fix
;能够自动修复编码风格问题,在我理解自动修复不会新增行或者移动代码。 运行以后,发现还剩下相似这种的报错,剩下的就须要手动修复了
"eslint.validate": [
"javascript",
"javascriptreact"
],
"editor.codeActionsOnSave": { //新版本语法
"source.fixAll.eslint": true
}
复制代码
还记得上面配置的resolve.alias吗?
npm install eslint-plugin-import eslint-import-resolver-alias --save-dev
settings: {
'import/resolver': {
alias: {
map: [
['@redux', paths.appRedux]
['@pages', paths.appPages]
['@util', paths.util]
],
},
},
}
复制代码
但这时你会发现ctrl/command
+鼠标左键没法识别路径,开发体验不是很好。
在根目录新建jsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@redux/*": ["src/redux/*"],
"@pages/*":["src/pages/*"],
"@util/*":["src/util/*"]
}
}
}
复制代码
npm i -D stylelint stylelint-config-standard stylelint-scss
复制代码
module.exports = {
extends: 'stylelint-config-standard', // 这是官方推荐的方式
processors: [],
plugins: ['stylelint-scss'],
ignoreFiles: ['node_modules/**/*.scss'],
rules: {
'rule-empty-line-before': 'never-multi-line',
},
};
复制代码
一款用于格式化代码的工具
上面已经用了eslint,为何还须要引入Prettier呢?在我理解eslint职责在于检测代码是否符合rules规则,prettier用于格式化代码避免这些报错;固然prettier没法格式化代码质量和语法类的问题。
npm i eslint prettier eslint-config-prettier eslint-plugin-prettier -D
复制代码
presets | 描述 |
---|---|
eslint-plugin-prettier | 在eslint rules中扩展规则 |
eslint-config-prettier | 让eslint和prettier兼容,关闭prettier和eslint冲突的部分 |
extends: [
'airbnb',
+ 'plugin:prettier/recommended'
]
复制代码
"scripts": {
"format": "prettier --write \"src/**/*.{js,jsx}\""
}
复制代码
运行 npm run format 发现文件已经被格式化,但语法错误和一些代码质量问题仍是须要手动修改
上面配置了检测代码的eslint和stylelint,如何让每次提交的代码都符合规范,还须要借助自动化工具
npm i -D husky
复制代码
"scripts": {
"lint": "eslint src --ext .jsx && stylelint \"./src/**/*.scss\""
},
"husky": {
"hooks": {
"pre-commit": "npm run lint"
}
}
复制代码
执行commit提交会发现报错,并阻止了代码提交,这样能够避免把错误代码提交到线上致使线上报错。