从零开始构建一个React全家桶(React)

120 阅读2分钟

开发环境构建

1. init项目

mkdir react-family && cd react-family
npm init

2. webpack

npm install --save-dev webpack@3 -g //webpack已有4、5, webpack需要全局安装
//--save-dev就是指开发时的依赖,--save指发布时还依赖的东西

//创建webpack.dev.config.js,在里面编写配置webpack项
webpack --config webpack.dev.config.js //运行webpack打包

//可以在package.json的script中加入对应的脚本命令行,简化运行
script: {
    "dev-build": "webpack --config webpack.dev.config.js"
}

3. babel(babel-core, babel-loader, babel-preset-es2015, babel-preset-stage-0, babel-preset, react)

//babel-core 调用Babel的API进行转码
//babel-loader
//babel-preset-es2015 用于解析ES6
//babel-preset-stage-0 用于解析ES7提案
npm install --save-dev babel-core babel-loader babel-preset-es2015 babel-preset-stage-0 babel-preset-react

//新建 .babelrc配置文件配置babel
{
    "presets": ["es2015", "stage-0", "react"],
    "plugins":[]
}

/*src文件夹下面的以.js结尾的文件,要使用babel解析*/
/*cacheDirectory是用来缓存编译结果,下次编译加速*/
module: {
    rules: [{
        test: /\.js$/,
        use: ['babel-loader?cacheDirectory=true'],
        include: path.join(__dirname, 'src')
    }]
}

4. react(react, react-dom)

npm install --save react react-dom

5. react-router(react-router-dom)

npm install --save react-router-dom

// Router - Routes - Route   Link
// element
import React from "react";

import { BrowserRouter as Router, Route, Routes, Link } from "react-router-dom";
import Bundle from "./bundle";

import Home from "bundle-loader?lazy&name=home!pages/Home/Home";
import Page1 from "bundle-loader?lazy&name=page1!pages/Page1/Page1";
import Counter from "bundle-loader?lazy&name=counter!../pages/Counter";
import UserInfo from "bundle-loader?lazy&name=userInfo!../pages/UserInfo";

const Loading = () => <div>Loading........</div>;
const createComponent = (component) => (props) =>
  (
    <Bundle load={component}>
      {(Component) => (Component ? <Component {...props} /> : <Loading />)}
    </Bundle>
  );

const getRouter = () => {
  const HomePage = createComponent(Home);
  const Page1Page = createComponent(Page1);
  const CounterPage = createComponent(Counter);
  const UserInfoPage = createComponent(UserInfo);
  return (
    <Router>
      <div>
        <ul>
          <li>
            <Link to="/">Home do you trust me?</Link>
          </li>
          <li>
            <Link to="/page1">Page1 are always good!</Link>
          </li>
          <li>
            <Link to="/counter">get yourself a counter!</Link>
          </li>
          <li>
            <Link to="/userInfo">something about me !</Link>
          </li>
        </ul>
        <Routes>
          <Route exact path="/" element={<HomePage />} />
          <Route path="/page1" element={<Page1Page />} />
          <Route path="/counter" element={<CounterPage />} />
          <Route path="/userInfo" element={<UserInfoPage />} />
        </Routes>
      </div>
    </Router>
  );
};

export default getRouter;
此时点击链接不会按照预期正确的路由跳转。
因为之前的一直访问的是静态文件路径,类似与 file://react/react-family/dist/index.html。
这种路径不是我们想象中的的路由那样的路径http://localhost:3000 。
此时开发环境需要配置一个简单的web服务器,来伺服静态文件。(使用webpack-dev-server,或者nextjs搭配koa等框夹启动一个本地服务器,使用NginxApacheIIS等配置来启动一个简单的web服务器)

6. web-dev-server

npm install --save webpack-dev-server@2 -g //需全局安装
// webpack.dev.config.js
devServer: {
    contentBase: path.join(__dirname, './dist') // Url的根目录。如果不指定,默认指向项目根目录
}
// 丰富devServer配置选项
devServer: {
    contentBase: path.join(__dirname, 'dist'),
    port: 8080,
    historyApiFallback: true, // 然所有404定位到index.html
    host: '0.0.0.0' //指定一个host,默认localhost。'0.0.0.0'代表希望外部外部服务器可以访问到。
}
// 运行命令行及优化
webpack-dev-server --config webpack.dev.config.js
//package.json
script: {
    "start": "webpack-dev-server --config webpack.dev.config.js --color --progress"
}

7. hmr(--hot,module.hot, react-hot-loader)

"start": "webpack-dev-server --config webpack.dev.config.js --color --progress --hot"

//index.js
if(module.hot) {
    module.hot.accept();
}

// 文件别名
resolve: {}

8. Redux(redux,react-redux,redux-thunk)

npm install --save redux
npm install --save redux-thunk

9. 优化:Redux,报错提示

combineReducers

devtool:"inline-source-map"
  1. css-loader,style-loader
npm install css-loader style-loader --save-dev

// webpack.dev.config.js
{
    test: /\.css$/,
    use: ['style-loader', 'css-loader']
}
  1. url-loader,file-loader
npm install --save-dev url-loader file-loader

12. 按需加载(bundle-loader)

npm install bundle-loader --save-dev

13.缓存

output: {
    path: path.join(__dirname, './dist'),
    filename: '[name].[hash].js',
    chunkFilename: '[name].[chunkhash].js'
}

14. HtmlWebpackPlugin(html-webpack-plugin)

npm install html-webpack-plugin --save-dev

15. 提取重复代码

// webpack.dev.config.js
entry: {
    app: path.join(__dirname,'src/index.js'),
    vendor:['react','react-dom','react-router-dom','redux', 'react-redux']
}
/*plugins*/
new webpack.optimize.CommonsChunkPlugin({
    name: 'vendor'
})

output: {
    path: path.join(__dirname, './dist'),
    filename: '[name].[hash].js', //这里应该用chunkhash替换hash
    //无奈,如果用`chunkhash`,会报错。和`webpack-dev-server --hot`不兼容,具体[看这里](https://github.com/webpack/webpack-dev-server/issues/377)。
    chunkFilename: '[name].[chunkhash].js'
}

生产环境构建

1. 剔除配置

  • web-dev-server
    
  • --hot模块
    
  • 改变: filename:[name].[thunkHash].js
    
  • devtool:"cheap-module-source-map"
    

2. 文件压缩 uglifyjs-webpack-plugin

npm i --save-dev uglifyjs-webpack-plugin

3. 指定环境 webpack.DefinePlugin()

module.exports = {
  plugins: [
       new webpack.DefinePlugin({
          'process.env': {
              'NODE_ENV': JSON.stringify('production')
           }
       })
  ]
}

4. 优化打包 clean-webpack-plugin

npm install --save-dev clean-webpack-plugin

5. 优化缓存 webpack.HashedModuleIdsPlugin()

6. 抽取css extract-text-webpack-plugin

npm install --save-dev extract-text-webpack-plugin
// webpack.dev.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /.css$/,
        use: ExtractTextPlugin.extract({
          fallback: "style-loader",
          use: "css-loader"
        })
      }
    ]
  },
  plugins: [
     new ExtractTextPlugin({
         filename: '[name].[contenthash:5].css',
         allChunks: true
     })
  ]
}

7. 使用axios和middleware优化API请求

npm install --save axios

8. 合并提取webpack公共配置

npm install --save-dev webpack-merge
//webpck.common.config.js
//... webpack开发配置和生产配置的相同部分
//...
module.exports = commonConfig;

webpack.dev.config.js

const merge = require('webpack-merge');

const commonConfig = require('./webpack.common.config.js');

const devConfig = {
   //... webpack开发 特有配置
   //...
}
module.exports = merge({
    customizeArray(a, b, key) {
        /*entry.app不合并,全替换*/
        if (key === 'entry.app') {
            return b;
        }
        return undefined;
    }
})(commonConfig, devConfig);

webpack.config.js

const merge = require('webpack-merge');

const commonConfig = require('./webpack.common.config.js');

const publicConfig = {
   //... webpack生产 特有配置
   //...
}
module.exports = merge(commonConfig, publicConfig);

9. 优化目录结构并增加404页面

10. 加入 babel-plugin-transform-runtime和babel-polyfill

//在转换 ES2015 语法为 ECMAScript 5 的语法时,babel 会需要一些辅助函数,例如 _extend。babel 默认会将这些辅助函数内联到每一个 js 文件里,这样文件多的时候,项目就会很大。

//所以 babel 提供了 transform-runtime 来将这些辅助函数“搬”到一个单独的模块 babel-runtime 中,这样做能减小项目文件的大小。
npm install --save-dev babel-plugin-transform-runtime

// .babelrc
// 修改`.babelrc`配置文件,增加配置
"plugins": ["transform-runtime"]
// Babel默认只转换新的JavaScript句法(syntax),而不转换新的API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。  
// 举例来说,ES6在Array对象上新增了Array.from方法。Babel就不会转码这个方法。如果想让这个方法运行,必须使用babel-polyfill,为当前环境提供一个垫片。
npm install --save-dev babel-polyfill

// webpack.common.config.js
app: [
    "babel-polyfill",
    path.join(__dirname, "src/index.js")
]
//webpack.dev.config.js
app:[
    "babel-polyfill",
    "rect-hot-loader/patch",
    path.join(__dirname, "src/index.js")
]

11. 集成PostCSS

// postcss有很多插件 如: postcss-cssnext
// postcss-cssnext允许你使用未来的css特性
// postcss-cssnext包含autoprefixer
// autoprefixer可以自动给css属性加上浏览器前缀
npm install --save-dev postcss-loader
npm install --save-dev postcss-cssnext

// webpack.dev.config.js
rules:[{
    test: /\.(css|scss)$/,
    use: ["style-loader","css-loader","postcss-loader"]
}]

// webpack.config.js
rules:[{
    test: /\.css$/,
    use: ExtractTextPlugin.extract({
        fallback: "style-loader",
        use: ["css-loader", "postcss-loader"]
    })
}]

//根目录添加postcss.config.js 配置文件
//postcss.config.js
module.export = {
    plugins: {
        "postcss-cssnext": {}
    }
};
// 此后运行代码,Autoprefixer可以自动给css属性添加浏览器前缀
// 编译前
.container {
    display: flex;
}
//编译后
.container {
    display: -webkit-box;
    display: -webkit-flex;
    display: -ms-flexbox;
    display: flex;
}

12. redux模块热替换配置

// 当修改reducer 代码时,页面会整个刷新,而不是局部刷新
//增加一段监听reducers变化,并替换的代码
if(module.hot) {
    module.hot.accept("./reducers", () => {
        const nextCombineReducers = require("./reducers").default;
        store.replaceReducer(nextCombineReducers);
    })
}

13. 模拟AJAX数据之Mock.js

// Mock.js:拦截AJAX请求,返回需要的数据。
// 我们写ajax请求的时候,随便写,Mock.js会自动拦截。
// Mock.js提供各种随机生成数据。
npm install --save-dev mockjs

// 创建mock文件夹
// mock/mock.js
import Mock from "mockjs";
let Random = Mock.Random;
Mock.mock("/api/user",{ // 拦截/api/user请求
    "name": "@cname", //返回一个随机的中文名字
    "intro": "@word(20)", //返回一个20个字母的字符串
})

// 与项目连接
//在项目中import mock文件
// 在页面中正常请求,即可使用mock
import "../mock/mock";
promise: client => client.get('api/user');

//配置:只有在开发环境下,才引入mock,生产环境不引入。
// webpack.common.config.js
resolve: {
    alias: {
        ...
        mock: path.join(__dirname, 'mock')
    }
}
// webpack.dev.config.js
const webpack = require('webpack');
plugins: [
    new webpack.DefinePlugin({
        MOCK: true
    })
]
// src/index.js import mock的方式改变一下
if (MOCK) {
    require("mock/mock");
}

14. 使用CSS Modules

// webpack.dev.config.js
module: {
    rules: [{
        test: /.css$/,
        use: ["style-loader", "css-loader?modules&localIdentName=[local]-[hash:base64:5]", "postcss-loader"]
    }]
}
//webpack.config.js
module: {
    rules: [{
        test: /.css$/,
        use: ExtractTextPlugin.extract({
            fallback: "style-loader",
            use: ["css-loader?modules&localIdentName=[local]-[hash:base64:5]", "postcss-loader"]
        })
    }]
}
// src/pages/Page1/page1.css
.box {
    border: 1px solid red;
}
// src/pages/Page1/Page1.js
import React, {Component} from 'react';

import style from './Page1.css';

import image from './images/brickpsert.jpg';

export default class Page1 extends Component {
    render() {
        return (
            <div className={style.box}>
                this is page1~
                <img src={image}/>
            </div>
        )
    }
}

15. 使用json-server代替Mock.js

json-serverMock.js一样,都是用来模拟接口数据的。

json-server功能更强大,支持分页,排序,筛选等等,具体的可以去看文档

我们用json-server代替之前的Mock.js

  1. 删除Mock.js相关代码。

    一共两处,webpack.dev.config.js,src/index.js

  2. npm install --save-dev json-server

  3. 写个demo,我们生成虚假数据还是用mockjs

mock/mock.js

let Mock = require('mockjs');

var Random = Mock.Random;

module.exports = function () {
    var data = {};
    data.user = {
        'name': Random.cname(),
        'intro': Random.word(20)
    };
    return data;
};
  1. 设置启动脚本

package.json

"mock": "json-server mock/mock.js --watch --port 8090",
"mockdev": "npm run mock & npm start"
  1. webpack.dev.config.js 增加个代理,把我们的API请求,代理到json-server服务器去。
   devServer: {
        ...
        proxy: {
                    "/api/*": "http://localhost:8090/$1"
                }
    }

哦了,你可以npm run mockdev启动项目,然后访问我们之前的用户信息接口,试试啦。

问题:windows不支持命令并行执行&,你可以分开执行,或者使用npm-run-all