這篇文章致力於從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添加兼容瀏覽器列表
![](https://user-gold-cdn.xitu.io/2020/5/7/171ef7b25122c989?w=862&h=692&f=png&s=263915)
"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嗎?
eslint報錯找不到路徑;安裝插件並新增如下配置便可解決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提交會發現報錯,並阻止了代碼提交,這樣能夠避免把錯誤代碼提交到線上致使線上報錯。
代碼報錯修復完,便可提交。