工欲善其事,必先利其器!“webpack配置工程师” 成长笔记

1,484 阅读10分钟

前言

1. 为什么不直接使用create-react-app?

首先,从功能的丰富程度及设计的通用层面,create-react-app(简称CRA)都称得的上是react世界里最优秀的脚手架,但同时它也有一些缺点,比如它由于对webpack基础配置封装的很深,导致开发者必须按CRA的文档API(与webpack完全不同)来进行配置。文档里的配置一旦不能满足需求,就需要执行eject操作,暴露原始的webpack配置,但这一步是不可逆的,导致之后想升级版本会变得非常困难,而且暴露出的webpack原始配置有时候改起来也不是很方便,(ps:也许是笔者太菜,改不动!)。

为了解决这些问题,社区甚至出现了不少类似于CRACO这样的方案,专门用来对CRA进行二次配置。这让问题变得更加复杂,开发者大部分时候只想要一款足够灵活简单的脚手架,把流行的功能比如eslinthmrtypescriptpx2rem等都内置好,如果想自定义配置,仅需要用自己已掌握的webpack知识对源码直接修改就可以了。所以,这也是本文的最大意义所在,创建一款足够灵活简单的脚手架配置。

2.webpack v5相比v4有哪些重要的改变?

  1. 更方便的配置体验。之前使用频率很高的loaders or plugins: 比如url-loader、file-loader、clean-webpack-plugin等都不再必须,提供了内置支持。
  2. 非常多的第三方loader,都发生了断层更新。比如现在在webpack4里去默认去安装最新的less-loader,使用options穿参时,会直接报错this.getOptions is not function
  3. 更加强大的持久化缓存,cache选项
  4. 联邦模块。

正文

1. 新建项目

mkdir build-react-app
cd build-react-app
npm init -y

2. 安装webpack

npm i webpack webpack-cli -D //推荐本地安装,版本更可控

3. 初体验

新建src/index.js文件,写入一些内容

const text = `hello webpack`;
document.write(text)

由于采用本地安装的方式,所以不能直接使用全局命令 webpack xxx,但可以这样运行

./node_modules/.bin/webpack ./src/index.js //路径也可以省略,因为默认就是这个

如果npm v6+,可以运行npx webpackimage.png
打包成功,不放心的话,可以再次查验dist/main.js(默认这个输出路径),看打包出的内容是否正常。

./node_modules/.bin/webpack这样稍显麻烦,可以采用npm script的简洁方式:
打开 package.json,新建以下命令

  "scripts": {
    "build":"webpack ./src/index.js"
  },

直接运行 npm run build 即可。

4. 解析css、图片、字体资源

新建webpack.config.js(webpack默认读取此文件内的配置)

4.1 解析css

npm i style-loader css-loader -D
module.exports = {
    entry:'./src/index.js',
    module:{
        rules:[
            {
                test:/\.css$/,
                use:[
                    'style-loader',
                    'css-loader'
                ]
            }
        ]
    }
}

现代开发中,优先使用less、sass这种预处理器比较多

npm install sass-loader sass  --save-dev //最新版的sass-loader终于不再依赖用起来怀疑人生的node-sass了,喜大普奔
npm install less-loader less  --save-dev
    {
        test: /\.scss$/,
        use: ["style-loader", "css-loader", "sass-loader"],
      },
      {
        test: /\.less$/,
        use: ["style-loader", "css-loader", "less-loader"],
      },

4.2 css 抽离

npm i mini-css-extract-plugin -D

mini-css-extract-plugin相比之前经常使用的extract-text-webpack-plugin会有许多优势,详情见这里

另外,style-loaderMiniCssExtractPlugin.loader同时使用是冲突的,这其实很好理解,一个是以style标签的方式插在页面head里,一个是以link引用的方式抽离成一个单独的样式文件,这是个单选题。一般是开发时候选前者,生产环境选后者。

webpack5里区分环境, 推荐使用process.env,NODE_ENV来区分,可以使用webpack-cli里的node-env设置方式。

package.json

"scripts": {
    "dev": "webpack serve --node-env development",
    "build": "webpack --node-env production"
  },

webpack.config.js

...
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const isProd = process.env.NODE_ENV === 'production';
...
module.exports = {
...
module:{
    mode: isProd ? "production" : "development",
    rules:[
    ...
    {
        test: /\.css$/,
        use: [isProd ? MiniCssExtractPlugin.loader : "style-loader", "css-loader"],
      },
      {
        test: /\.scss$/,
        use: [isProd ? MiniCssExtractPlugin.loader : "style-loader", "css-loader", "sass-loader"],
      },
      {
        test: /\.less$/,
        use: [isProd ? MiniCssExtractPlugin.loader : "style-loader", "css-loader", "less-loader"],
      },
     ]
     ...
    },
    plugins: [
    // css文件单独抽离
       isProd && new MiniCssExtractPlugin({
            filename: 'css/[name]_[contenthash:8].css',
        }),
    ].filter(Boolean),
    ...
...
}
 

4.3 解析图片

         // 解析图片资源
           {
                test: /\.(png|svg|jpg|jpeg|gif)$/i,
                type: 'asset',
                parser: {
                    dataUrlCondition: {
                        maxSize: 10 * 1024, // 10kb
                    },
                },
                generator: {
                    filename: 'img/[name][hash][ext][query]',
                },
            },         

在webpack5里内置asset module用来处理文件类型的模块,也就是说url-loader、file-loader都不需要了,指定一个资源 type,webpack会自动帮你处理。这里选用asset,因为它可以帮你在asset/inline资源内联与asset/resource原封不动移动文件之间,根据传入的资源大小限制,自动帮你做出选择。

4.4 解析字体文件

         // 解析字体资源
            {
                test: /\.(woff|woff2|eot|ttf|otf)$/i,
                type: 'asset/resource',
            },

5. 使用 babel 降级 js语法

5.1 安装babel

npm install @babel/core @babel/cli -D

5.2 babel-loader

babel在webpack是通过babel-loader的方式使用的

npm install babel-loader -D
module.exports = {
  entry: "./src/index.js",
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ["babel-loader"],
        exclude: /node_modules/,
      },
     ...
    ],
  },
};

5.3 babel配置

@babel/preset-env是一个智能预设,可以针对目标环境智能的预设需要哪些语法转换以及polyfill。这使开发者更轻松,也使得JavaScript包更小!

npm i @babel/preset-env -D

接下来,还需要一个目标环境配置,因为browserslist除了babel以外,也可以被其他的很多工具(如- postcss-preset-envAutoprefixer)读取,所以建议提到一个公共的地方。我这里选择在package.json里进行配置,也可以选择单独的 .browserslistrc 文件(笔者不想项目根目录存在一大堆的单独配置文件)。
package.json

...
"browserslist": [
    "> 1%",
    "last 2 versions",
    "not ie <= 8",
    "Android >= 4.0"
  ]
...  

5.5 按需polyfill

实现polyfill,一般采用core-js,并且将@babel/preset-env预设里的useBuiltIns设为usage,最后别忘记指定你安装的corejs版本(很重要)。

npm i core-js -D

5.6 babel的配置文件

多种方式可选, 笔者习惯选用js的方式,因为格式或注释相比json更灵活,同时相比.babelrc的方式,可以加入一些js逻辑判断,更加方便。

新建babel.config.js

module.exports = {
    presets: [
        [
            '@babel/preset-env',
            {
                useBuiltIns: 'usage',
                corejs: '3.18.2',
            },
        ],
    ]
};

6. 清除上次构建产物

module.exports = {
    ...
    output: {
        filename: 'js/[name]_[chunkhash:8].js',
        path: path.resolve(__dirname, '../dist'),
        clean: true, // 相当于CleanWebpackPlugin的作用
    }
    ...
};

7. react相关

npm i react react-dom 

解析jsx

webpack.config.js

      {
        test: /\.jsx?$/,
        use: ["babel-loader"],
        exclude: /node_modules/,
      },
npm i @babel/preset-react -D

babel.config.js

module.exports = {
  presets: [
    ...
    "@babel/preset-react",
  ],
};

8. 静态资源自动插入html模版

npm i html-webpack-plugin -D
  plugins:[
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, `./src/index.html`),
      filename: `index.html`,
      inject: 'body',
      chunks: 'all',
      minify: {
          // html5:true,
          minifyJS: true,
      },
    })
  ]

9. 开发服务器

npm i webpack-dev-server -D

区分开发和生产命令

  "scripts": {
    "dev": "webpack serve",
    "build": "webpack"
  },

webpack.config.js

    ...
    devServer: {
        static: './dist',
        open: true,
        compress: true,
        port: 3000,
        https: true,
        hot: true,
    },
    ...

10. HMR 热更新

HMR是(模块热更替)的英文缩写。首先,一些人对HMR长期以来存在误解,以为模块热更新,只是简单的监听代码改动,自动刷新浏览器而已,但其实恰恰相反,它的作用是在尽量不刷新页面的情况下,只替换本次修改的模块。
举个简单的例子,你在debugger一个长表单时候,从上到下录入了一堆测试数据,定位到是其中某一个字段组件的问题后,你修改了一行代码,并保存,这时,悲剧发生了,页面刷新,所有状态都丢失,又要从头到尾重新填写,看有没有改好! js的HMR,webpack默认支持,但在前端场景下目前用到最多的其实是.vue .jsx等的组件文件的热更新。处理起来还要多一些步骤,比如在组件模块替换完成时,需要触发对应组件重新渲染,其他组件的状态尽量保持不变。所以框架层面的HMR实现,还需要框架开发者或者对应的框架社区来配合webpack的一些module.hot钩子来共同实现。在react世界,目前主流的实现有两种:

  • 第一种基于loader的react-hot-loader,其在触发局部组件重渲染方面是采用hoc的方式去做的,对代码有一定的侵入;
  • 第二种是官方支持的react-refresh,由官方直接在框架层面做HMR,可以轻松做到对开发代码的零侵入,这是现在最推荐的一种方式。以下是社区基于官方api的实现。
npm install @pmmmwh/react-refresh-webpack-plugin react-refresh -D

webpack.config.js

pulgins:[
    ...
    !isProd && new ReactRefreshPlugin(),
    ...
].filter(Boolean)

babel.config.js

const isDev =  process.env.NODE_ENV === "development";
module.exports = {
  ...  
  plugins:[
    isDev && 'react-refresh/babel'
  ].filter(Boolean)
};

11. typescript

这里有一点需要注意,typecript的一项重要能力是能够将ts代码(tsc)编译为生产环境中能识别运行的js代码,这其实是非常现实的一步,毕竟现在无论浏览器还是Node环境都无法直接运行TS代码,但这项功能其实这和babel的功能是有重叠的。早些时候是先将TS代码传递给TypeScript转换为JS,然后再将这份JS代码传递给Babel转换为低版本JS代码,因此我们需要配置两个编译器,并且每次做了一点更改,都会经过两次编译,相当低效。ts团队很早就意识到这个问题。可以从这篇文章找到更多细节[译] TypeScript 牵手 Babel:一场美丽的婚姻@babel/preset-typescript就是ts团队专门找babel团队合作一年的成果。所以目前主流的做法,是让ts更多的作为提升开发体验的“工具”,让开发者充分享受由它带来的那些静态优势,编译这一步交给功能更强大的babel来负责,也就是鼓励使用babel-loader + @babel/preset-typescript 完全替代 awesome-typescript-loader和ts-loader,详情见ts官网对这种混合技术的介绍Babel 用于转译,tsc 用于类型

安装他们

npm i typescript @babel/preset-typescript -D

别忘记咱们是个react项目,所以提前将有关包的type类型装上

npm install @types/react @types/react-dom -D

新建tsconfig.json,也可以自动生成这个文件,但上边typecript是选择的局部安装,所以没有全局命令,需要这样执行tsc命令./node_modules/.bin/tsc --init

{
    "compilerOptions": {
        "allowSyntheticDefaultImports":true,//允许从没有设置默认导出的模块中默认导入
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true,
        "outDir": "./dist",
        "sourceMap": true,
        "noImplicitAny": true,
        "module": "commonjs",
        "target": "es5",
        "jsx": "react",
        "lib": ["es6","dom"],
        "paths": {
            "@/*": ["./src/*"]
        },
    },
    "include": [
        "./src/**/*",
    ],
    "exclude": [
        "node_modules",
    ]
  }

接下来就改改webpack的配置
webpack.config.js

module.exports = {
  ...
  entry: "/src/index.tsx",
  ...
  resolve: { // 顺便加个@别名,方便引用文件
    alias: {
        '@': path.resolve(__dirname, '../src'),
   },
   extensions: [//文件扩展名。默认只支持js及json,添加ts文件的支持
        '.ts',
        '.tsx',
        '.js',
        '.jsx',
        '.json',
        '.css',
        '.scss',
        '.less',
    ],
  },
  module: {
    rules: [
      {
        test: /\.(jsx?|tsx?)$/,
        use: ["babel-loader"],
        exclude: /node_modules/,
      },
     ...
    ],
  },
}

babel.config.js

module.exports = {
  presets: [
    [
    ...
    "@babel/preset-typescript"
  ],
  ...  
};

12. eslint

安装

npm install eslint --save-dev

配置文档 新建eslint配置文件,这里通过npx eslint --init自动创建。

image.png 自动生成如下文件及内容

module.exports = {
    "env": {
        "browser": true,
        "es2021": true
    },
    "extends": [
        "eslint:recommended",
        "plugin:react/recommended",
        "plugin:@typescript-eslint/recommended"
    ],
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
        "ecmaFeatures": {
            "jsx": true
        },
        "ecmaVersion": 13,
        "sourceType": "module"
    },
    "plugins": [
        "react",
        "@typescript-eslint"
    ],
    "rules": {
    }
};

接下来就是自定义一些rules了,业界一般公认airbnb他们家的规范是做的比较好的,这里直接用他们的。

npx install-peerdeps --dev eslint-config-airbnb

.eslintrc

{
   ...
    "extends": [
        "airbnb",
        "airbnb/hooks"
    ],
     ...
}

完整的.eslintrc配置如下:

{
    "env": {
        "browser": true,
        "es2021": true
    },
    "extends": [
        "airbnb",
        "airbnb/hooks"
    ],
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
        "ecmaFeatures": {
            "jsx": true
        },
        "ecmaVersion": "latest",
        "sourceType": "module"
    },
    "plugins": [
        "@typescript-eslint",
        "react"
    ],
    "settings":{
        "react":{
            "version":"17.0.2"
        }
    },
    "rules":{}
}

在webpack里开启eslint之前是通过eslint-loader的方式,现在最新最推荐的是使用eslint-webpack-plugin;

npm i eslint-webpack-plugin -D

webpack.config.js

const ESLintPlugin = require('eslint-webpack-plugin');

module.exports = {
  // ...
  plugins: [new ESLintPlugin({
      extensions: ['ts', 'tsx', 'js'],
      failOnError:false,
  })],
  // ...
};

启动项目,写点错误代码,出现以下提示,就是配置成功了。

image.png

13. 多页构建(约定式)

简介:

  • MPA: 是相对于SPA来说的,用单独的页面来承载独立的业务模块,这在开发中很常见的模式。
  • 约定式: 是利用文件目录上的约定,(比如页面都放src/pages下),更方便的配置多entry、多个HtmlWebpackPlugin,实现多页应用的零配置,开箱即用。 webpack.config.js
/**
 * 获取多页面打包的入口及htmlPlugin
 */
const getMPA = () => {
  const entryFiles = glob.sync(
    path.resolve(__dirname, "./src/pages/*/index.{ts,tsx}")
  );
  const entry = {};
  const htmlWebpackPlugins = [];

  // 获取页面名称
  const getPageName = (filePath) => {
    const match = filePath.match(/src\/pages\/([^/]*)/);

    return match ? match[1] : null;
  };

  entryFiles.forEach((filePath) => {
    const pageName = getPageName(filePath);

    if (!pageName) {
      throw new Error("多页的文件组织按“/src/pages/{pageName}/index.ts”约定");
    }

    entry[pageName] = filePath;
    htmlWebpackPlugins.push(
      new HtmlWebpackPlugin({
        template: path.resolve(
          __dirname,
          `./src/pages/${pageName}/index.html`
        ),
        filename: `${pageName}.html`,
        inject: "body",
        chunks: [pageName],
        minify: {
          // html5:true,
          minifyJS: true,
        },
      })
    );
  });

  return {
    entry,
    htmlWebpackPlugins,
  };
};
// 配置多页
const { entry, htmlWebpackPlugins } = getMPA();
module.exports = {
  ...
  entry,
  output: {
    filename: "js/[name]_[chunkhash:8].js",
    path: path.resolve(__dirname, "./dist"),
    clean: true,
  },
  ...
  plugins: [
   ...
    ...htmlWebpackPlugins,
   ...
};

14. 移动端适配

14.1 采用淘宝lib-flexible方案

自动模版填充script,拒绝手动粘贴复制

npm i raw-loader@0.5.1 -D //确保安装此版本,不然会有报错

修改模版 src/pages/index/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width">
    <title>index</title>
    <script> <%= require('raw-loader!../../../node_modules/lib-flexible/flexible.js') %> </script>
</head>
<body>
   <div id='root'></div> 
</body>
</html>

14.2 px转换rem

这里有两种方案,一种是postcss插件,另一种是px2rem-loader,这里选第一种,因为postcss的功能更加强大:

npm i postcss postcss-loader postcss-pxtorem -D

因为项目里采用了sass、less来处理样式文件,他们的loader配置其实是有很大的复用性的,所以抽取取一个通用处理函数。

/**
 * 获取样式文件的loaders
 * @param {*} preprocessor 采用何种预处理器
 */
const getStyleFileLoaders = (preprocessor) => {
    const styleFileLoaders = [
        isProd ? MiniCssExtractPlugin.loader : 'style-loader', // css生产环境抽离
        'css-loader',
        {
            loader: 'postcss-loader',// postcss-loader解析时机必须在预处理之后
            options: {
                postcssOptions: {
                    plugins: ['postcss-preset-env',['postcss-pxtorem', { rootValue: 75, propList: ['*'] }]],
                },
            },
        },
    ];

    preprocessor && styleFileLoaders.push(preprocessor);// 预处理器

    return styleFileLoaders;
};

14.3 自动补全浏览器样式前缀

一般使用 autoprefixer 插件来解决,但postcss提供了更智能的选择,postcss-preset-env,会根据项目里配置的browserslist,自动进行补全浏览器兼容前缀。

npm i postcss-preset-env -D

webpack.config.js 加在上边的postcssOptions里就可以了。

  plugins: [
  'postcss-preset-env',
  ['postcss-pxtorem', { rootValue: 75, propList: ['*'] }]
  ],

image.png

15. 构建速度优化

15.1 多进程打包

这里大家可能听说happy-pack比较多,但这个包以后不再维护了,webpack官方目前提供的方案是thread-loader。
优点是提升打包速度,缺点是每个进程的开启和交流都会有开销,所以小项目不建议使用,有可能会负优化。(babel-loader消耗时间最久,所以使用thread-loader针对其进行优化)

     {
        test: /\.(jsx?|tsx?)$/,
        use: ["thread-loader","babel-loader"],
        exclude: /node_modules/,
      },

15.2 并行压缩

webpack默认在mode为production时,会启用terser-webpack-plugin压缩js代码,这里将它的parallel设为true,可以开启多进行并行压缩。

webpack.config.js

const TerserWebpackPlugin = require('terser-webpack-plugin');
module.exports = {
...
  optimization: {
        minimizer: [
            // '...', // 继承默认的压缩器,比如压缩js的terser-webpack-plugin
            new TerserWebpackPlugin({
                parallel: true, // 多进程并行压缩,这个其实是默认开启,这里只是明确写出来了
                extractComments: false, // 不将注释提取到单独的文件,类似于 xxx.js.LICENSE.txt
            }),
             ...
        ],
    },
...
}

15.3 缓存构建,提升二次构建速度

在webpack4里,这项功能一般是loader层面来做的,比如开启babel-loader的缓存

        {
          loader:"babel-loader",
          options:{
            cacheDirectory: true, // 开启缓存
          }
        }

image.png 可以看到,缓存成功。 在webpack5中,提供了全局的支持。
webpack.config.js

    cache: { // 开启构建结果缓存
        type: isProd ? 'filesystem' : 'memory',
    },

image.png 可以看到开启后,首次构建花了10s,再次构建仅花了600ms,对比效果还是很残暴的。

16 生产环境优化

webpack在mode设为production,开启生产模式之后,默认就会做大量的优化,但这里也明确列举一下,方便有个印象。

16.1 treeShaking

去掉无用的js导入,只支持静态的es6 import/export。

16.2 cope hoisting

构建后的代码存在⼤量闭包代码,这个开启后,会尽量让各个模块代码在同一个作用域下,变量冲突通过重命名等方式解决。

16.3 压缩css

npm i css-minimizer-webpack-plugin -D

webpack.config.js

const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
...
module.exports = {
...
   optimization: {
        minimizer: [
           ...
            // 压缩css
            new CssMinimizerPlugin(),
        ],
    },
...
}

16.4 擦除无用的css代码

npm i purgecss-webpack-plugin glob -D

webpack.config.js

const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const path = require('path');
const glob = require('glob');
...
module.exports = {
...
      plugins: [
        ...
        // 擦除无用的css代码
        new PurgeCSSPlugin({
            paths: glob.sync(`${path.join(__dirname, './src')}/**/*`, { nodir: true }),
        }),
    ],
...
}

16.5 线上缓存优化

react技术栈,或者其他的一些不经常改动的包,可以进行单独拆包,提高页面的二次加载速度。也可以用external做npm包的cdn抽离,这在跨项目的缓存优化是很有意义的,但同时也会限制死每个项目需要安装的包版本,不然可能会出现线上包与本地包版本不一致导致的bug。因为现在的线上静态资源大多数情况是cdn托管的,本身访问也足够快的,所以这里直接用splitChunks单独抽取本项目的公共包即可,也可以避免external造成的开发生产版本不一致问题。

 optimization: {
        splitChunks: {
            minSize: 5000,
            cacheGroups: {
                // react技术栈相关的
                reactVendor: {
                    test: /[\\/]node_modules[\\/](react|react-dom|react-router-dom)[\\/]/,
                    name: 'reactVendor',
                    chunks: 'all',
                    priority: 1,
                },
                // node_modules
                defaultVendor: {
                    test: /[\\/]node_modules[\\/]/,
                    name: 'defaultVendor',
                    chunks: 'all',
                    // minChunks: 1,
                    priority: 0,
                },
            },
        },
    },

17. 拆分webpack配置

随着配置项越来越繁多,配置里处处用isDev、isProd等的表达式来区分不同mode下需要的配置,所以需要对整个webpack.config.js进行更清晰的拆分。

webpack-cli命令区分NodeEnv环境,并且根据环境区分不同的配置文件

  "scripts": {
    "dev": "webpack serve --node-env development",
    "build": "webpack --node-env production"
  },

新建build目录,存放配置文件

  • build/webpack.dev.js 开发配置
  • build/webpack.prod.js 生产配置
  • build/webpack.base.js 公共配置
  • build/webpack.utils.js 抽取出的可复用方法
  • webpack.config.js 配置入口(webpack默认指定的文件) 使用webpack-merge这个包来解决基础配置与特定环境配置的合并。
onst { merge } = require('webpack-merge');
const baseConfig = require('./build/webpack.base');
const developmentConfig = require('./build/webpack.dev');
const productionConfig = require('./build/webpack.prod');

module.exports = (env, argv) => {
    switch (argv.nodeEnv) {
        case 'development':
            return merge(baseConfig, developmentConfig);
        case 'production':
            return merge(baseConfig, productionConfig);
        default:
            throw new Error('No matching configuration was found!');
    }
};

尾巴

这样一个包含主流功能的react脚手架就基本完成了。webpack的配置总体是没有任何技术含量的,对自己的锻炼主要在于:

  • 踩坑,babel、各种loader、eslint、typescript等的版本及api的踩坑。
  • 不同的解决方案之间的对比
  • 文档信息收集能力 总体是一项耗时、枯燥、易忘的工作。笔者本篇文章的主要目的,也是想把这个完整的配置过程做一个记录,之后有用到的时候,可以快速查阅。
    项目github地址,本文完整的配置已上传,有问题或者交流可以随时提issue。