john 157e244f9d 立即执行的显示问题 преди 2 години
..
config 3fb299ff06 优化 преди 2 години
public 0b977567c1 首页、表格 преди 2 години
src 157e244f9d 立即执行的显示问题 преди 2 години
.eslintrc.js 6add275f9d user преди 2 години
.gitignore 07d3019358 初始化 преди 2 години
.prettierrc.json 07d3019358 初始化 преди 2 години
.yarnrc 0b977567c1 首页、表格 преди 2 години
README.md 07d3019358 初始化 преди 2 години
babel.config.js 07d3019358 初始化 преди 2 години
jsconfig.json 07d3019358 初始化 преди 2 години
package-lock.json 07d3019358 初始化 преди 2 години
package.json 376085445e 第一阶段 преди 2 години
postcss.config.js 07d3019358 初始化 преди 2 години
yarn.lock 376085445e 第一阶段 преди 2 години

README.md

实现思路

从不同阶段的角度来划分,不同的阶段所需要的功能不同,因此整体构建可划分为:本地开发阶段、生产部署阶段

开发阶段: 需要本地启动服务器环境,支持代码即时预览、模块热更新、devtool 调试功能等;

生产部署阶段: 需要开启压缩优化、静态资源复制目录、代码 tree shaking、chunk 抽离等功能;阶段不同所需功能不同。

不同阶段所需功能不同,但工具的基础能力一致,因此可将整体构建拆分为如下结构:

  • 基础通用功能

    • entry 入口配置
    • output 输出配置
    • resolve 自动文件识别与简化路径配置
    • externals 排除指定依赖而减小打包体积的外部拓展配置
    • module-loader 以 loader 转换非 js 模块配置
    • plugins 全局变量、页面模板、语法工具等插件配置
    • optimization 模块抽取等性能相关配置
    • 其他相关配置
  • 开发阶段配置

    • dev server 本地服务器配置
    • devtool 开发调试配置
    • proxy 接口代理转发配置
    • hot HMR 模块热更新配置
    • compress gzip 等压缩优化配置
    • router historyApiFallback 本地路由重定向配置
    • optimization 模块名称 chunkIds 配置
  • 生产部署阶段

    • 清理 dist 目录
    • 复制静态资源
    • css 单独拆包
    • css/js 丑化压缩
    • 作用域提升
    • tree shaking
    • 代码拆分

想看完整配置文件小伙伴可直接移步文章末尾

构建具体实现

目录

预定使用以下目录结构

image.png

  • config

webpack 构建相关配置目录

  • paths.js 获取指定目录绝对路径
  • webpack.common.js 基础通用配置
  • webpack.config.js 主构建配置
  • webpack.dev.js 开发环境配置
  • webpack.prod.js 生产环境配置

  • public 公共通用的静态资源目录

  • src

    • assets 项目内模块使用的静态资源目录
    • components 项目的公共组件目录
    • App.vue 项目主页面
    • main.js 项目入口
    • style.less 项目通用样式-less
  • .eslintrc.js

代码风格配置文件

  • babel.config.js

工具 babel 转换配置,设置预设等

  • postcss.config.js

css 样式转换配置

  • package.json 依赖包等信息配置文件

基础通用功能

项目打包需要依赖 webpack 且需要在命令行执行,因此先行安装 webpck、webpack-cli 到开发依赖

npm i webpack webpack-cli --save-dev

安装完毕后在 webpack.common.js 中增加通用配置

entry 入口配置

module.exports = {
  entry: "./src/main.js", // 默认是此目录,如若文件名称或者入口变更可修改
};

output 输出配置

const resolveApp = require("./paths");

module.exports = {
  output: {
    path: resolveApp("dist"),
    filename: "js/[name].[hash:6].js",
    chunkFilename: "js/[name].chunk.[hash:4].js",
  },
};

paths.js 文件内容

const path = require("path"); // webpack内置模块

const appRoot = process.cwd(); // 命令行运行的根目录

const resolveApp = (resolvePath) => {
  return path.resolve(appRoot, resolvePath); // 获取指定目录的完整绝对路径
};

module.exports = resolveApp;

path:文件输出到 dist 目录中,根目录路径的获取通过命令行终端函数获取

filename:构建后的脚本文件输出到 js 目录下,且使用动态文件名称,附带文件 6 为 hash 值

chunkFilename:构建时拆分出来的 chunk 文件同样放入 js 目录下,且名称除了以上规则外,增加 chunk 标识

resolve 路径简化配置

在模块源码中,通过 import 加载其他模块时需要写出其前缀目录地址,如若嵌套较深则会书写麻烦,此时可以配置特定路径标识映射,以简化此路径。

同时,导入其他模块时默认在文件末尾增加 js 文件格式结尾,其他文件格式会无法找到,配置自动补全规则来增加其他文件格式补全

module.exports = {
  resolve: {
    extensions: [".js", ".jsx", ".vue", ".json", "..."],
    alias: {
      "@": resolveApp("src"),
    },
  },
};

resolve:通过extensions配置自动补全的文件类型

resolve:通过alias配置简化路径的映射标识

externals 外部拓展

外部依赖的库文件如若较大且不会频繁变动,则可将库文件的导入交给外部,如使用外链形式走 cdn 加速、构建 dll 库或其他形式,通过 html 页面增加 script 脚本引入库文件

module.exports = {
  externals: {
    // 不希望依赖打进包中,走外链cdn等
    // '$': 'Jquery',
    // react: 'React',
    // 'react-dom': 'ReactDOM',
    // 'prop-types': 'PropTypes',
    // moment: 'moment',
    // antd: 'antd',
    // classnames: 'classNames'
  },
};

module loader 配置

为什么要用 loader

1.webpack 默认只能处理 js 文件,而我们项目中不可避免要用到样式表 css、图片 imgage、字体 font 等资源,而这类的资源文件 webpack 并不认识,因此无法直接处理

2.js 模块中我们会用到最新的 ES6+语法,也可能会用到 coffee script 语法,或者严谨的 typescript 语法等,这些语法在浏览器环境中支持程度不一,或不支持直接解析。

基于以上种种原因,我们需要通过 loader 加载器来将这类资源文件转换为可以被识别并解析的标准 js

loaders
  • vue-loader

安装

先安装 vue 到项目依赖,再安装 vue-loader、vue-template-compiler 到开发依赖

vue-template-compiler 的作用是将 vue 的模板代码预编译为渲染函数,以避免在运行时编译开销

npm i vue --save
npm i vue-loader vue-template-compiler --save-dev

使用

const VueLoaderPlugin = require("vue-loader/lib/plugin");

module.exports = {
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: "vue-loader",
      },
    ],
  },
  plugins: [new VueLoaderPlugin()],
};

安装完毕后 rules 内添加相应规则,检测 vue 结尾的文件,并使用 vue-loader。同时要求单独导出 vueLoaderPlugin,在 plugins 插件集合内实例化,以此来正确解析 vue 文件

  • css-loader

对于样式表 css 的处理和其他 vue、js 处理不同,样式表 css 处理需要经过多步骤处理后才能正常使用,具体流程为:

1.使用 postcss-loader 将 css 文件内样式根据浏览器支持情况转换为多平台兼容写法,如使用 autoprefixer、browserslist、caniuse-lite 等依赖增加平台特性写法;

2.接着将转换后的样式表 css 交由 css-loader 处理 import、url 等行为,将引入和依赖的外部资源加载到当前样式表中;

3.处理完毕后将 css 交由 style-loader,已 style 标签的形式注入 html 页面中

这里存在一个问题,开发环境时将 css 全部注入页面没有影响,但生产环境会是多文件大量内容,超出量后会导致首页加载变慢。因此需要在生产阶段将 css 单独抽离出文件,通过判断环境来加载相应配置,将 module.exports 导出内容变更为函数,接受环境标识

安装 loader

npm i postcss-loader css-loader style-loader --save-dev

安装插件 plugin

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

使用

const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = (isProduction) => {
  const cssFinalLoader = isProduction
    ? MiniCssExtractPlugin.loader
    : "style-loader";
  return {
    module: {
      rules: [
        {
          test: /\.css$/,
          use: [
            cssFinalLoader, // 开发与生产使用不同loader
            {
              loader: "css-loader",
              options: {
                esModule: false, // css不使用esModule,直接输出
                importLoaders: 1, // 使用本loader前使用1个其他处理器
              },
            },
            "postcss-loader",
          ],
          sideEffects: true, // 希望保留副作用
        },
      ],
    },
  };
};

前边提到了 postcss-loader 会根据支持度情况自动转换 css 内容,postcss-loader 作为转换的一个平台,会提供插槽的能力,但是转换的具体规则并不会去做,因此这个转换规则需要我们通过其他插件解决。这里我们使用一个预设的转换集合postcss-preset-env

安装

npm i postcss-preset-env --save-dev

使用

postcss.config.js 文件

module.exports = {
  plugins: [require("postcss-preset-env")],
};
  • less-loader

less-loader 与 css-loader 处理逻辑相似,区别为:处理前需要先使用 less-loader 将 less 文件处理为 css 文件,处理 less 文件依赖 less 包。

安装

npm i less less-loader --save-dev

配置

const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = (isProduction) => {
  const cssFinalLoader = isProduction
    ? MiniCssExtractPlugin.loader
    : "style-loader";
  return {
    module: {
      rules: [
        {
          test: /\.less$/,
          use: [
            cssFinalLoader,
            {
              loader: "css-loader",
              options: {
                importLoaders: 2,
              },
            },
            "postcss-loader",
            "less-loader",
          ],
        },
      ],
    },
  };
};
  • 图片-loader

在 webpack4 版本中,图片类资源是通过 file-loader 和 url-loader 来配合实现。但 webpack5 版本中官方内置了 asset 来处理静态资源类。

配置

module.exports = (isProduction) => {
  return {
    module: {
      rules: [
        {
          test: /\.(png|gif|jpe?g|svg)$/,
          type: "asset", // webpack5使用内置静态资源模块,且不指定具体,根据以下规则使用
          generator: {
            filename: "img/[name].[hash:6][ext]", // ext本身会附带点,放入img目录下
          },
          parser: {
            dataUrlCondition: {
              maxSize: 10 * 1024, // 超过10kb的进行复制,不超过则直接使用base64
            },
          },
        },
      ],
    },
  };
};
  • font 字体类 loader

配置

与图片类资源处理相似,但去除了大小限制,因字体文件是整体,且无需额外处理,因此直接进行静态资源复制

module.exports = (isProduction) => {
  return {
    module: {
      rules: [
        {
          test: /\.(ttf|woff2?|eot)$/,
          type: "asset/resource", // 指定静态资源类复制
          generator: {
            filename: "font/[name].[hash:6][ext]", // 放入font目录下
          },
        },
      ],
    },
  };
};
  • babel-loader

js 模块的转换操作需要依赖 babel-loader,与 postcss-loader 类似,babel-loader 也可以理解为一个转换的平台,并不做具体转换规则,因此需要依赖@babel/core@babel/preset-env@vue/cli-plugin-babel插件来完成转换

安装

npm i @babel/core @babel/preset-env @vue/cli-plugin-babel --save-dev

配置

module.exports = (isProduction) => {
  return {
    module: {
      rules: [
        {
          test: /\.jsx?$/,
          exclude: /node_modules/, // 过滤掉node_modules目录,只使用而已
          use: "babel-loader", // js、jsx使用bable-loader处理
        },
      ],
    },
  };
};

进行语法转换的时候需要排除掉 node_modules 目录

babel.config.js 文件

module.exports = {
  presets: [
    "@vue/cli-plugin-babel/preset",
    [
      "@babel/preset-env",
      {
        useBuiltIns: "usage", // 用到什么填充什么,按需
        corejs: 3, // 默认是2版本
      },
    ],
  ],
};
  • eslint-loader

代码风格检查

安装

npm i eslint eslint-loader --save-dev

配置

npx eslint --init // 根据提示完成配置,详情见webpack打包-上

执行完毕后会安装相关依赖和生成.eslintrc.js配置文件

.eslintrc.js

module.exports = {
  env: {
    browser: true,
    es2021: true,
  },
  extends: ["plugin:vue/essential", "standard"],
  parserOptions: {
    ecmaVersion: 12,
    sourceType: "module",
  },
  plugins: ["vue"],
  rules: {},
};

plugins 配置

基础功能内提供全局变量定义、页面模板、vue 相关 plugin 等配置

module.exports = (isProduction) => {
  return {
    plugins: [
      new DefinePlugin({
        BASE_URL: '"./"',
      }),
      new HtmlWebpackPlugin({
        title: "贾贵山-自建构建打包流程",
        template: "public/index.html",
      }),
      new VueLoaderPlugin(),
    ],
  };
};

完整示例

const { DefinePlugin } = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const TerserWebpackPlugin = require("terser-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const VueLoaderPlugin = require("vue-loader/lib/plugin");
const resolveApp = require("./paths");

module.exports = (isProduction) => {
  const cssFinalLoader = isProduction
    ? MiniCssExtractPlugin.loader
    : "style-loader";

  return {
    entry: "./src/main.js",
    output: {
      path: resolveApp("dist"),
      filename: "js/[name].[hash:6].js",
      chunkFilename: "js/[name].chunk.[hash:4].js",
    },
    resolve: {
      extensions: [".js", ".jsx", ".vue", ".json", "..."],
      alias: {
        "@": resolveApp("src"),
      },
    },
    externals: {
      // 不希望依赖打进包中,走外链cdn等
      // '$': 'Jquery',
      // react: 'React',
      // 'react-dom': 'ReactDOM',
      // 'prop-types': 'PropTypes',
      // moment: 'moment',
      // antd: 'antd',
      // classnames: 'classNames',
    },
    module: {
      rules: [
        // vue文件处理
        {
          test: /\.vue$/,
          use: "vue-loader",
        },
        // less文件处理
        {
          test: /\.less$/,
          use: [
            cssFinalLoader,
            {
              loader: "css-loader",
              options: {
                importLoaders: 2,
              },
            },
            "postcss-loader",
            "less-loader",
          ],
        },
        // css文件处理
        {
          test: /\.css$/,
          use: [
            cssFinalLoader, // 最终要以style标签输出到页面
            {
              loader: "css-loader",
              options: {
                esModule: false, // css不使用esModule,直接输出
                importLoaders: 1, // 使用本loader前使用1个其他处理器
              },
            },
            "postcss-loader",
          ],
          sideEffects: true, // 希望保留副作用
        },
        // 图片类处理
        {
          test: /\.(png|gif|jpe?g|svg)$/,
          type: "asset", // webpack5使用内置静态资源模块,且不指定具体,根据以下规则 使用
          generator: {
            filename: "img/[name].[hash:6][ext]", // ext本身会附带 点,放入img目录下
          },
          parser: {
            dataUrlCondition: {
              maxSize: 10 * 1024, // 超过10kb的进行复制,不超过则直接使用base64
            },
          },
        },
        // 字体类处理
        {
          test: /\.(ttf|woff2?|eot)$/,
          type: "asset/resource", // 指定静态资源类复制
          generator: {
            filename: "font/[name].[hash:6][ext]", // 放入font目录下
          },
        },
        // 脚本类处理
        {
          test: /\.jsx?$/,
          exclude: /node_modules/, // 过滤掉node_modules目录,只使用而已
          use: "babel-loader", // js、jsx使用bable-loader处理
        },
        {
          test: /\.(vue|js)$/,
          use: "eslint-loader",
          exclude: /node_modules/,
          enforce: "pre",
        },
      ],
    },
    plugins: [
      new DefinePlugin({
        BASE_URL: '"./"',
      }),
      new HtmlWebpackPlugin({
        title: "贾贵山-自建构建打包流程",
        template: "public/index.html",
      }),
      new VueLoaderPlugin(),
    ],
    optimization: {
      runtimeChunk: true, // 模块抽取,利用浏览器缓存
      minimizer: [
        new TerserWebpackPlugin({
          extractComments: false, // 不要注释生成的文件
        }),
      ],
    },
  };
};

开发阶段配置

因开发环境在通用配置基础上配置,因此直接展示完整示例,对相应配置进行说明

安装

npm i webpack-merge webpack-dev-server --save-dev

配置

const baseConfig = require("./webpack.common");
const { merge } = require("webpack-merge");

module.exports = merge(baseConfig(false), {
  mode: "development",
  devtool: "cheap-module-source-map",
  target: "web",
  devServer: {
    hot: "only",
    port: 3002, // 端口号,工作中从3001开始,因此增加1个到3002
    open: true, // 自动打开浏览器
    compress: true, // 开启gzip压缩
    historyApiFallback: true, // history路径在刷新出错时重定向开启
    proxy: {
      // 接口代理
      "/api": {
        // 统一api前缀都代理掉
        target: "http://api.github.com", // 代理的目标地址
        changeOrigin: true, // 改变来源信息
        pathRewrite: {
          // 因前缀为自己增加,因此重写地址
          "/api": "", // 将前缀去掉
        },
      },
    },
  },
  optimization: {
    chunkIds: "named",
  },
});
  • 使用 webpack-merge 包进行配置合并
  • mode: 指定当前模式为开发模式 development
  • devtool: 指定生成 source map,且规则模式为 cheap-module-source-map
  • target: 指定目标为 web 平台
  • devServer: 本地服务环境配置对象
    • hot: 开启热更新
    • port: 本地服务的端口号为 3002,可任意更改
    • open: 服务启动后自动打开浏览器,默认为 true
    • compress: 压缩模式
    • historyApiFallback: history 模式访问时如路由不存在则重定向到首页
    • proxy: 请求代理
    • '/api': 代理的请求点缀
    • target: 将要代理到的目标地址
    • changeOrigin: 是否更改来源信息
    • pathRewrite: 重写配置

生产阶段配置

const baseConfig = require("./webpack.common");
const { merge } = require("webpack-merge");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const webpack = require("webpack");
const PurgeCSSPlugin = require("purgecss-webpack-plugin");
const resolveApp = require("./paths");
const glob = require("glob");
const CompressionPlugin = require("compression-webpack-plugin");
// var InlineChunkHtmlPlugin = require('inline-chunk-html-plugin');
// const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = merge(baseConfig(true), {
  mode: "production",
  plugins: [
    new CleanWebpackPlugin(),
    new CopyWebpackPlugin({
      patterns: [
        {
          from: "public",
          to: "public",
          globOptions: {
            ignore: ["**/index.html"],
          },
        },
      ],
    }),
    new MiniCssExtractPlugin({
      // css单独拆分为文件
      filename: "css/[name].[hash:6].css",
    }),
    new webpack.optimize.ModuleConcatenationPlugin(), // 作用域提升,提升性能
    new PurgeCSSPlugin({
      paths: glob.sync(`${resolveApp("./src")}/**/*`, { nodir: true }),
    }),
    new CompressionPlugin({
      test: /\.(css|js)$/i,
      algorithm: "gzip",
    }),
    // new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime/]) // 将一定大小文件直接注入 html,
  ],
  optimization: {
    chunkIds: "deterministic", // 文件名称尽可能短,也会是序号类型
    splitChunks: {
      chunks: "all",
      minSize: 20000, // 拆分的每个包不小于20kb
      maxSize: 20000, // 体积大于设置的值的包要去拆分开包
      minChunks: 1, // 包如果要拆分,则必须要至少引用一次
      cacheGroups: {
        syVendors: {
          test: /[\\/]node_modules[\\/]/, // 对目录内文件进行单独打包拆分,且放入一个文件中 vender
          filename: "js/[id]_verdor.js", // 最终名字
          priority: -10, // 都满足时候的优先级,越高月用
        },
      },
    },
    minimizer: [new CssMinimizerPlugin()],
  },
});

生产环境依然采用合并基础配置方式

  • mode: 此时采用 production模式
  • plugins: 生产阶段使用的 plugins 列表
    • CleanWebpackPlugin 清除 dist 目录插件
    • CopyWebpackPlugin 静态资源目录复制插件
    • MiniCssExtractPlugin 单独拆分出 css 包
    • ModuleConcatenationPlugin 将 js 的作用域提升,减少作用域层级来提升性能
    • PurgeCSSPlugin 摇掉未使用的 css
    • CompressionPlugin 采用 gzip 形式押错 css 和 js
    • optimization 生产环境优化配置

构建入口配置

const devConfig = require("./webpack.dev");
const prodConfig = require("./webpack.prod");
// const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");

// const smp = new SpeedMeasurePlugin();

module.exports = (env) => {
  // const finalResult = env.production ? prodConfig : devConfig;
  // console.log('final---->', finalResult)
  // return smp.wrap(finalResult);
  return env.production ? prodConfig : devConfig;
};

构建时减少繁琐修改配置文件的操作,在 package.json 内添加启动参数来选择不同环境配置

项目构建

git clone https://gitee.com/tammylights/fed-e-task-02-02.git
cd fed-e-task-02-02
npm install

npm run serve
npm run build

本地开发则启动 npm run serve 构建生产环境则 npm run build 检查代码风格则 npm run lint