乐趣区

从零开始webpack搭建reactredux应用

从零开始 webpack 搭建 react,redux 利用

前言:

应用 webpack 曾经有些年头了,然而对于其中的一些根本配置还是只知其一; 不知其二。为了成为一名优良的 webpack 配置工程师,也是学习了一把 webpack,react 的配置,特分享此次经验,并记录当中遇到的一些问题。当然当初的配置只是很根底的,心愿在当前的工作经验中,多多摸索,把一些 webpack 优化,react,redux 最佳实际,都退出到其中。

文章目录

  • webpack 根底配置
  • 配置 react, less
  • 引入 antd,
  • react-router 的应用
  • react-redux
  • redux 异步中间件的抉择 thunk/saga
  • 我的项目优化:MiniCssExtractPlugin,路由切割懒加载,postcss-loader, url-loader, hmr,tree shaking,
  • devserver proxy,本地 mock 数据
  • lint & prettier
  • 我的项目部署脚本

一. webpack 根底配置

学习一个新技术,最好的获取形式便是浏览官网文档。(https://www.webpackjs.com/gui…)。通读当前,总结为以下几个要点。

  1. 初始化我的项目,装置依赖。
npm init -y
npm install webpack webpack-cli --save-dev
  1. 配置文件
// webpack.base.js
const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {filename: '[name].bundle.js',
    path: path.resolve(__dirname, '../dist'),
  },
};
// package.json
"scripts": {"dev": "webpack --config webpackconfig/webpack.base.js",},
// dist/index.html
<!doctype html>
<html>
<head>
    <title>hyt</title>
</head>
<body>
<script src="./main.bundle.js"></script>
</body>
</html>
// src/index.js
function component() {var element = document.createElement('div');
    element.innerHTML = 'hello world hyt';
    return element;
}

document.body.appendChild(component());
  1. 接下来运行 npm run dev,查看 dist 下输入,发现多了一个 main.bundle.js 文件,关上咱们新建的 index.html 文件,能够看到如下,阐明咱们的 webpack 根底打包曾经可能应用了。

  1. 如果咱们更改了一个入口终点的名称,或者针对多入口增加了一个新的名称,又须要咱们手动去 index.html 中去更改,咱们能够应用 HtmlWebpackPlugin 动静生成 index.html.

当然,防止咱们每次手动去清空 dist 文件下的内容,能够应用 clean-webpack-plugin 插件帮忙清空。

npm install html-webpack-plugin clean-webpack-plugin

// webpack.base.js
const path = require('path');
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  output: {filename: '[name].bundle.js',
    path: path.resolve(__dirname, '../dist'),
  },
  plugins: [new CleanWebpackPlugin(),
     new HtmlWebpackPlugin({title: 'Output Management'})
  ],
};

这里能够看到,HtmlWebpackPlugin 曾经帮忙咱们生成了 html 文件。

  1. 如上,咱们曾经把握了 webpack 打包编译的根本应用。

然而在日常开发中,每次批改完代码都须要手动执行 webpack 打包命令,很繁琐。这时候能够采纳 watch 或者 webpack-dev-server 或者 webpack-dev-middleware 办法实现。较为罕用的是应用 webpack-dev-server,不仅提供一个简略的 web 服务器,并且可能实时从新加载。

npm install --save-dev webpack-dev-server

const path = require("path");
const {CleanWebpackPlugin} = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  entry: "./src/index.js",
  output: {filename: "[name].bundle.js",
    path: path.resolve(__dirname, "../dist"),
  },
  devServer: {
    contentBase: './dist',
    open: true,
    port: 8888,
  },
  plugins: [new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({title: "Output Management",}),
  ],
};

批改 package.json

 "scripts": {
    "dev": "webpack-dev-server --config webpackconfig/webpack.base.js",
    "watch": "webpack --config webpackconfig/webpack.base.js --watch"
  },

执行 npm run dev,看看成果。

  1. webpack-dev-server 诚然好用,然而只实用于开发环境,在生产环境中,咱们的指标则转向于关注更小的 bundle,更轻量的 source map,以及更优化的资源,以改善加载工夫。所以咱们能够依据不同的环境,加载不同的 webpack 配置。

webpack.base.js是通用配置,webapck.dev.js中是开发环境配置,webapck.prod.js是生产环境配置。webpack-merge能够帮住咱们很好的合并配置。

接下来拆分配置:

// webpack.base.js
const path = require("path");
const {CleanWebpackPlugin} = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  entry: "./src/index.js",
  output: {filename: "[name].bundle.js",
    path: path.resolve(__dirname, "../dist"),
  },
  plugins: [new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({title: "Output Management",}),
  ],
};
// webpack.dev.js
const {merge} = require("webpack-merge");
const base = require("./webpack.base");

module.exports = merge(base, {
  mode: "development",
  devtool: "inline-source-map",
  devServer: {
    contentBase: "./dist",
    open: true,
    port: 8888,
  },
});
const {merge} = require("webpack-merge");
const webpack = require("webpack");
const base = require("./webpack.base");

module.exports = merge(base, {
  mode: "production",
  devtool: "source-map",
  plugins: [
    new webpack.DefinePlugin({"process.env.NODE_ENV": JSON.stringify("production"),
    }),
  ],
});
// package.json
"scripts": {
    "dev": "webpack-dev-server --config webpackconfig/webpack.dev.js",
    "watch": "webpack --config webpackconfig/webpack.base.js --watch",
    "prod": "webpack --config webpackconfig/webpack.prod.js"
},

到目前为止,一个小型的 webpack 打包利用曾经构建好了。接下来进入 webpack 利用中,引入 react, css, less 的解决。

二. 引入 React, 解决 css, less

  1. 装置 React ,React-dom
npm install react react-domm

批改 src/index.js,改为 react 组件格局代码。

import React from "react";
import ReactDOM from "react-dom";

const App = () => {return <div>hello world hyt</div>;};

ReactDOM.render(<App />, document.getElementById("root"));

因为 react-dom 的渲染节点,须要挂在曾经存在的 id=root 节点上,所以咱们须要在生成的 index.html 中提前写入 root 节点。此操作能够搭配之前提到的 HtmlWebpackPlugin 实现。增加 template 模板。

// src/template.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>


// webpack.base.js
new HtmlWebpackPlugin({
  title: 'hyt Management',
  template: './src/template.html',
}),

接下来运行,npm run dev,果然,报错了。

提醒咱们,应该须要专门的 loader 去解决咱们的 js/jsx 文件。这时候,就是赫赫有名的 babel 退场了。babel 能够帮忙咱们进行 js 文件的编译转换。

  1. babel

除了帮忙咱们对于高版本 js 语法转换以外,还能够解决 react 的 jsx 写法。

npm install babel-loader @babel/preset-env @babel/preset-react @babel/core

更改 webpack.base.js 中 rules 规定。

module: {
    rules: [
      {test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: [{loader: "babel-loader"}],
      },
    ],
},

根目录新增.babelrc 配置文件

{"presets": ["@babel/preset-env", "@babel/preset-react"]
}

接下来打包运行,npm run dev,发现浏览器中终于显示了 <div>hello world hyt</div> 的 dom(为了显示一行 dom,咱们费了这么大的功夫,不得不吐槽)。

  1. 接下来给页面加点款式。

有了方才 js 打包报错的教训,应该明确,要想退出 css 文件,也须要有专门的 loader 去解决 css 文件,得以运行。

npm install css-loader style-loader

css-loader 解决 css 文件为 webpack 可辨认打包的,style-loader 插入到页面 style 中。

rules: [
  {test: /\.(js|jsx)$/,
    exclude: /node_modules/,
    use: [{loader: "babel-loader"}],
  },
  {
    test: /\.css$/,
    use: [
      {loader: "style-loader",},
      {loader: "css-loader",},
    ],
  },
]
// src/index.js

import "./style.css";

const App = () => {return <div className="hello">hello world hyt</div>;};

// src/style.css
.hello {
  font-size: 30px;
  color: blue;
}

嗯,能够看到页面中有色彩了。。

这时候思考一个问题,如果在咱们其余组件中,也有同样名字的 class, 再其对应的 css 文件中,写了不同的款式,会有什么后果,试验一下。

// src/components/about/index.js
import React from "react";
import "./style.css";

const About = (props) => {return <div className="hello">About</div>;};

export default About;
// src/components/about/style.css
.hello {color: red;}
// src/index.js

import About from "./components/about";

<About />

看下页面的展现,

发现 color: red 的款式并没有失效,关上控制台看下打包后的款式,名字一样的 class,款式被笼罩了。

所以这个时候,就引入 css modules 的概念了,通过 css-loader 的配置,帮忙咱们实现 css 模块化。

{
    test: /\.css$/,
    use: [
      {loader: "style-loader",},
      {
        loader: "css-loader",
        options: {
          modules: {localIdentName: "[name]__[local]--[hash:base64:5]", // css-loader >= 3.x,localIdentName 放在 modules 里
          },
        },
      },
    ],
}

更改 js 文件中引入形式。

import style from "./style.css";
const About = (props) => {return <div className={style["hello"]}>About</div>;
};

index.js 中同理

emm,款式果然失效了

  1. less

既然都用到 css 了,和不应用应用预处理 less 呢,可能更加提效咱们的开发。应用步骤和 css 大致相同,秩序多家 less-loader 先把 less 文件做一次转换,再走 css-loader 的流程。大略配置如下

npm install less-loader
{
    test: /\.less$/,
    use: [
      {loader: "style-loader", // creates style nodes from JS strings},
      {
        loader: "css-loader", // translates CSS into CommonJS
        options: {
          modules: {localIdentName: "[name]__[local]--[hash:base64:5]", // css-loader >= 3.x,localIdentName 放在 modules 里  https://github.com/rails/webpacker/issues/2197
          },
        },
      },
      {
        loader: "less-loader", // compiles Less to CSS
            options: {lessOptions: { javascriptEnabled: true},// less@3.x,须要开启 配置项 javascriptEnabled: true
            },
      },
    ],
  },

把 About 中的 css 文件改为 less 应用即可。接下来能够安心的写代码了。

三. Antd 的应用,以及 less 的别离解决

为了进步咱们的开发效率,在我的项目中引入 antd 组件库。

两种办法,全量引入 css;或按需加载。(antd 4.x 的 JS 代码默认反对基于 ES modules 的 tree shaking。)https://ant.design/docs/react…

采纳按需加载的办法来构建我的项目。

npm install antd babel-plugin-import

{"presets": ["@babel/preset-env", "@babel/preset-react"],
  "plugins": [
    [
      "import",
      {
        "libraryName": "antd",
        "libraryDirectory": "es",
        "style": true // `style: 'css'` 会加载 css 文件
      }
    ]
  ]
}

发现款式并没有加载胜利。

起因是咱们方才在解决 less 文件时,没有辨别 src 和 node_modules,导致 antd 的 class 也加了 modules,没有加载到正确的款式。批改 less loader 为

{
    test: /\.less$/,
    exclude: /node_modules/, // 这里做了批改
    use: [
      {loader: "style-loader", // creates style nodes from JS strings},
      {
        loader: "css-loader", // translates CSS into CommonJS
        options: {
          modules: {localIdentName: "[name]__[local]--[hash:base64:5]", // css-loader >= 3.x,localIdentName 放在 modules 里  https://github.com/rails/webpacker/issues/2197
          },
        },
      },
      {
        loader: "less-loader", // compiles Less to CSS
        options: {lessOptions: { javascriptEnabled: true},
        },
      },
    ],
  },
  {
    test: /\.less$/,
    include: /node_modules/, // 这里做了批改
    use: [
      {loader: "style-loader", // creates style nodes from JS strings},
      {loader: "css-loader", // translates CSS into CommonJS},
      {
        loader: "less-loader", // compiles Less to CSS
        options: {lessOptions: { javascriptEnabled: true},
        }, // less@3.x,须要开启 配置项 javascriptEnabled: true, less-loader 高版本须要 lessOptions。},
    ],
  },

四. React-Router

接下来引入 React-Router 实现单页面利用。

具体用法可参考 https://reacttraining.com/rea…

npm install react-router-dom

批改 index.js 文件

import {BrowserRouter} from "react-router-dom";
import Routes from "./Routes";

const App = () => {
  return (
    <BrowserRouter>
      <Routes />
    </BrowserRouter>
  );
};

新建 Routes.js

import React from "react";
import {Switch, Route, Link, Redirect} from "react-router-dom";
import About from "./components/about";
import User from "./components/user";

const Routes = () => {
  return (
    <div>
      <nav>
        <ul>
          <li>
            <Link to="/about">About</Link>
          </li>
          <li>
            <Link to="/user">User</Link>
          </li>
        </ul>
      </nav>
      <Switch>
        <Route path="/about" component={About} />
        <Route path="/User" component={User} />
        <Redirect to="/about" />
      </Switch>
    </div>
  );
};

export default Routes;

留神咱们应用的是 BrowserRouter,本地开发 webpack devserver 须要开启 historyApiFallback: true, 生产环境能够在 nginx 端 try_files。

单页面利用 ok 了,接下来引入 react-redux 去治理咱们的数据流。

五. Ract-redux

为什么抉择 redux 来治理咱们的数据流,以及 redux 的设计原理,能够查看阮一峰老师的系列文章,这里只给出根本应用。http://www.ruanyifeng.com/blo…

几个比拟重要的概念,Provider,connect, creatStore, reducer, applyMiddleware,actions。

持续革新文件构造及内容

npm install redux react-redux
  1. sotre
// src/store.js
import {createStore} from "redux";
import reducers from "./reducers/index";

const store = createStore(reducers, {});

export default store;
  1. reducer
// src/reducers/index.js
import {combineReducers} from "redux";

const initialState = {name: "hyt",};

function home(state = initialState, action) {switch (action.type) {
    case "TEST_REDUCER":
      return {...state,};
    default:
      return state;
  }
}

export default combineReducers({home,});
  1. provider
// src/index.js

import {Provider} from "react-redux";
import Routes from "./Routes";
import store from "./store";

const App = () => {
  return (<Provider store={store}>
      <BrowserRouter>
        <Routes />
      </BrowserRouter>
    </Provider>
  );
};
  1. connect

新建容器组件 container/home.js

import React from "react";
import {connect} from "react-redux";

const Home = (props) => {return <div>Home,{props.data.name}</div>;
};

export default connect((state) => ({data: state.home}))(Home);
  1. 同样在 route 中引入 home 组件。
import Home from "./containers/home";
const Routes = () => {
  return (
    <div>
      <nav>
        <ul>
          ...
          <li>
            <Link to="/home">Home</Link>
          </li>
        </ul>
      </nav>
      <Switch>
        ...
        <Route path="/home" component={Home} />
        <Redirect to="/about" />
      </Switch>
    </div>
  );
};

这是路由 localhost:8080/home 下就能够显示出 hello,hyt 的数据。

  1. dispatch actions

下面曾经获取到了 store 中的数据,接下来 dispatch 去扭转 store 中的数据,因为组件订阅了 store(connect), 页面数据源会主动渲染变更。

6.1 增加 action types 常量

// src/constants/actionTypes.js
export const SET_USER_NAME = "SET_USER_NAME";

6.2 扭转 store 的 action

// src/actions/homeAction.js
import {SET_USER_NAME} from "../constants/actionsType";

export function setName(payload) {return { type: SET_USER_NAME, payload};
}

6.3 承受 actions 的 reducer

// src/reducers/index.js
import {SET_USER_NAME} from "../constants/actionsType";

const initialState = {name: "hyt",};

function home(state = initialState, action) {switch (action.type) {
    case SET_USER_NAME:
      return {
        ...state,
        name: action.payload.name,
      };
    default:
      return state;
  }
}

6.4 组件触发 actions。减少了 mapDispatchToProps。props.setName()

// src/containers/home.js
import React, {useEffect} from "react";
import {connect} from "react-redux";
import {setName} from "../actions/homeAction";

const Home = (props) => {useEffect(() => {setTimeout(() => {
      props.setName({name: "wjh",});
    }, 3000);
  }, []);
  return <div>Home,{props.data.name}</div>;
};

const mapDispatchToProps = {setName,};

export default connect((state) => ({data: state.home}),
  mapDispatchToProps
)(Home);

当初页面中的,hello,hyt 会在三秒后变成 hello,wjh。

六. redux 中间件,thunk/saga

当初咱们解决的是同步数据,接下来咱们引入 redux 中间件,去解决异步 action 函数。

批改 store,

npm install redux-thunk
// src/store.js
import {createStore, applyMiddleware} from "redux";
import thunk from "redux-thunk";
import reducers from "./reducers/index";

const store = createStore(reducers, {}, applyMiddleware(thunk));

export default store;
// src/actions/homeAction.js
export function getName(payload) {return (dispatch) => {return Promise.resolve().then((res) => {
      dispatch({
        type: SET_USER_NAME,
        payload: {name: "fetch mock",},
      });
      return res;
    });
  };
}
// src/containers/home.js
const Home = (props) => {useEffect(() => {setTimeout(() => {
      // props.setName({
      //   name: "wjh",
      // });
      props.getName();}, 3000);
  }, []);
  return <div>Home,{props.data.name}</div>;
};

const mapDispatchToProps = {
  setName,
  getName,
};

页面上曾经变成了 hello,fetch mock.

saga 的应用能够间接参考 https://github.com/hytStart/J…

七. 我的项目优化

  1. 路由切割懒加载。应用 import() + react-loadable 实现。
npm install react-loadable

批改 Routes 中组件引入形式,达到按路由拆分
js 模块


import Loadable from "react-loadable";

const MyLoadingComponent = (props) => {if (props.pastDelay) {return <div>Loading...</div>;}
  return null;
};

const User = Loadable({loader: () => import("./components/user"),
  loading: MyLoadingComponent,
  delay: 300,
});

能够看到控制台 js bundle 加载。

  1. 热更新 HMR

因为当初咱们每改一下代码,都能够看到刷新一次页面,于是之前的路由跳转状态、表单中填入的数据都会重置。对于开发人员过程很不不便,这时候就引出咱们的热更新了,不会造成页面刷新,而是进行模块的替换。

// webpack.dev.js

module.exports = merge(base, {
  mode: "development",
  devtool: "inline-source-map",
  devServer: {
    contentBase: "./dist",
    open: true,
    port: 8888,
    historyApiFallback: true,
    hot: true, // +++++++
  },
});
// index.js

const App = () => {
  return (<Provider store={store}>
      <BrowserRouter>
        <Routes />
      </BrowserRouter>
    </Provider>
  );
};

++++
if (module.hot) {module.hot.accept();
}
++++
ReactDOM.render(<App />, document.getElementById("root"));
  1. url-loader & file-loader

当初咱们的我的项目中还没有专门的 loader 去解决图片,

file-loader 能够指定要复制和搁置资源文件的地位,以及如何应用版本哈希命名以取得更好的缓存。此外,这意味着 你能够就近治理图片文件,能够应用相对路径而不必放心部署时 URL 的问题。应用正确的配置,webpack 将会在打包输入中主动重写文件门路为正确的 URL。

url-loader 容许你有条件地将文件转换为内联的 base-64 URL (当文件小于给定的阈值),这会缩小小文件的 HTTP 申请数。如果文件大于该阈值,会主动的交给 file-loader 解决。

减少如下配置

npm install file-loader url-loader
// webpack.base.js
{test: /\.(mp4|ogg)$/,
    use: [
      {loader: 'file-loader',},
    ],
  },
  {test: /\.(png|jpg|jpeg|gif|eot|svg|ttf|woff|woff2)$/,
    use: [
      {
        loader: 'url-loader',
        options: {limit: 8192,},
      },
    ],
  },
  1. MiniCssExtractPlugin

该插件将 CSS 提取到独自的文件中。它为每个蕴含 CSS 的 JS 文件创建一个 CSS 文件。它反对 CSS 和 SourceMap 的按需加载。

4.1 应用 mini-css-extract-plugin

npm install --save-dev mini-css-extract-plugin

批改 webpack.base.js 中对于 css,
less 的配置,替换掉 style-loader(不在须要把 style 插入到 html 中,而是通过 link 引入)。

{
    test: /\.css$/,
    use: [
      // {
      //   loader: "style-loader",
      // },
      {
        loader: MiniCssExtractPlugin.loader,
        options: {
          esModule: true,
          hmr: process.env.NODE_ENV === "dev",
          reloadAll: true,
        },
      },
      {
        loader: "css-loader",
        options: {
          modules: {localIdentName: "[name]__[local]--[hash:base64:5]", // css-loader >= 3.x,localIdentName 放在 modules 里  https://github.com/rails/webpacker/issues/2197
          },
        },
      },
    ],
  },
  {
    test: /\.less$/,
    exclude: /node_modules/,
    use: [
      // {
      //   loader: "style-loader", // creates style nodes from JS strings
      // },
      {
        loader: MiniCssExtractPlugin.loader,
        options: {
          esModule: true,
          hmr: process.env.NODE_ENV === "dev",
          reloadAll: true,
        },
      },
      {
        loader: "css-loader", // translates CSS into CommonJS
        options: {
          modules: {localIdentName: "[name]__[local]--[hash:base64:5]", // css-loader >= 3.x,localIdentName 放在 modules 里  https://github.com/rails/webpacker/issues/2197
          },
        },
      },
      {
        loader: "less-loader", // compiles Less to CSS
        options: {lessOptions: { javascriptEnabled: true},
        },
      },
    ],
  },
  {
    test: /\.less$/,
    include: /node_modules/,
    use: [
      // {
      //   loader: "style-loader", // creates style nodes from JS strings
      // },
      {
        loader: MiniCssExtractPlugin.loader,
        options: {
          esModule: true,
          hmr: process.env.NODE_ENV === "dev",
          reloadAll: true,
        },
      },
      {loader: "css-loader", // translates CSS into CommonJS},
      {
        loader: "less-loader", // compiles Less to CSS
        options: {lessOptions: { javascriptEnabled: true},
        }, // less@3.x,须要开启 配置项 javascriptEnabled: true, less-loader 高版本须要 lessOptions。},
    ],
  },

4.2 如上配置,减少 hrm 配置

hmr: process.env.NODE_ENV === "dev"

同时在 package.json scripts 中注入环境变量

"scripts": {
    "dev": "NODE_ENV=dev webpack-dev-server --config webpackconfig/webpack.dev.js",
    "watch": "NODE_ENV=dev webpack --config webpackconfig/webpack.base.js --watch",
    "prod": "webpack --config webpackconfig/webpack.prod.js"
},

4.3 plugin 配置

plugins: [new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: "Output Management",
      template: "./src/template.html",
    }),
    new MiniCssExtractPlugin({
      // Options similar to the same options in webpackOptions.output
      // both options are optional
      filename: "[name].css",
      chunkFilename: "[id].css",
    }),
  ],

到目前为止,咱们曾经依据引入文件的形式,拆散除了 css,做到了按需加载。然而当初能够查看打包进去的 css 文件是没有通过压缩的。

4.4 减少 optimize-css-assets-webpack-plugin 来压缩 css 代码,然而这时又会呈现另外一个问题,optimization.minimizer 会笼罩 webpack 提供的默认设置,因而还需减少 terser-webpack-plugin 来压缩 js 代码。

npm install --save-dev optimize-css-assets-webpack-plugin terser-webpack-plugin
// webapack.base.js
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const TerserJSPlugin = require("terser-webpack-plugin");


plugins: [new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: "Output Management",
      template: "./src/template.html",
    }),
    new MiniCssExtractPlugin({
      // Options similar to the same options in webpackOptions.output
      // both options are optional
      filename: "[name].css",
      chunkFilename: "[id].css",
    }),
],
optimization: {minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
},
  1. tree shaking

https://webpack.docschina.org…

mode: 'production'
退出移动版