前端工程化(三)

2020-07-18  本文已影响0人  望月从良glh

webpack 打包

模块化开发为我们解决了很多问题,使得代码组织管理非常的方便,但是又带来了新的问题,ES Module 存在环境兼容问题,划分的文件太多,就会导致网络请求频繁,不能保证所有资源的模块化

如果能我们享受模块化带来的开发优势,又能不必担心生产环境的存在这些问题,于是就有了 webpack, rollup, Parcel 等工具
webpack 模块化不等于 js ES modele 模块,相对来讲是前端的模块化处理方案,更加宏观

$ yarn init --yes
$ yarn add webpack webpack-cli -D
$ yarn webpack --version
$ yarn webpack // 默认打包src/index.js // 最终存放到dist/main.js

在项目根目录添加 webpack.config.js

const path = require('path');

module.exports = {
  entry: './src/main.js', // 入口文件
  output: {
    filename: 'bundle.js', // 输出文件名
    path: path.join(__dirname, 'output'), // 输出文件路径(绝对路径)
  },
};

webpack4 新增了工作模式的用法,大大简化了配置的复杂程度;三种工作模式 mode: production development none

$ webpack --mode none
$ webpack --mode production // 默认模式
$ webpack --mode development

或者采用配置的方式

const path = require('path');

module.exports = {
  // 这个属性有三种取值,分别是 production、development 和 none。
  // 1. 生产模式下,Webpack 会自动优化打包结果;
  // 2. 开发模式下,Webpack 会自动优化打包速度,添加一些调试过程中的辅助;
  // 3. None 模式下,Webpack 就是运行最原始的打包,不做任何额外处理;
  mode: 'development',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
  },
};
const path = require('path');

module.exports = {
  mode: 'none',
  entry: './src/main.css',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /.css$/,
        // css-loader作用就是将css代码转化为js模块
        // style-loader作用就是将cssloader转化的结果追加到页面
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
};
const path = require('path');

module.exports = {
  mode: 'none',
  entry: './src/main.css',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
};

// main.js
import './main.css';
const path = require('path');

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
    publicPath: 'dist/',
  },
  module: {
    rules: [
      {
        test: /.css$/,
        use: ['style-loader', 'css-loader'],
      },
      {
        test: /.png$/,
        use: 'file-loader',
      },
    ],
  },
};
// main.js
import createHeading from './heading.js';
import './main.css';
import iconURL from './icon.png';
// 经过file-loader处理后,将图片放到我们打包目录的根目录。返回图片的访问路径,通过import就可以拿到图片的路径。webpack默认认为图片放在网站的根目录下
const heading = createHeading();

document.body.append(heading);

const img = new Image();
img.src = iconURL;

document.body.append(img);

最佳实践:小文件使用 Data URLs, 减少请求次数。大文件单独提取,避免 bundle.js 过大,加载时间过长

module: {
  rules: [
    {
      test: /.css$/,
      use: ['style-loader', 'css-loader'],
    },
    {
      test: /.png$/,
      use: {
        // 必须同时安装file-loader,当超过limit设置的值,url-loader会自动让file-loader处理
        loader: 'url-loader',
        options: {
          limit: 10 * 1024, // 10 KB
        },
      },
    },
  ];
}
$ yarn add babel-loader @babel/core @babel/preset-env -D
// babel 只是一个js的转换平台。基于平台通过不同的插件实现转化

{
  "test": /.js$/,
  "use": {
    "loader": "babel-loader",
    "options": {
      "presets": ["@babel/preset-env"]
    }
  }
}
const path = require('path');

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
    publicPath: 'dist/',
  },
  module: {
    rules: [
      {
        test: /.md$/,
        // 将 md 转化为 html
        use: ['html-loader', './markdown-loader'],
      },
    ],
  },
};
// main.js
import about from './about.md';

console.log(about);
// markdown-loader.js
const marked = require('marked');

module.exports = source => {
  // source为加载进来的资源内容

  const html = marked(source);
  // 如果不交给下个loader处理

  // return `module.exports = "${html}"`
  // return `export default ${JSON.stringify(html)}`
  // 如果交给下个loader处理
  // 返回 html 字符串交给下一个 loader 处理
  return html;
};
plugins: [
  new webpack.ProgressPlugin(),
  new CleanWebpackPlugin(),
  // 不额外添加模板的使用
  new HtmlWebpackPlugin({
    title: 'glh', // 设置标题
    meta: {
      // 设置meta标签
      viewport: 'width=device-width',
    },
    // ...
  }),
];
// 添加模板,让HtmlWebpackPlugin根据模板生成
new HtmlWebpackPlugin({
  title: 'glh', // 设置标题
  meta: {
    // 设置meta标签
    viewport: 'width=device-width',
  },
  template: './public/index.html',
  templateParameters: {
    // 自定义变量
    BASE_URL: './',
  },
  // ...
});
<!--  public/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,initial-scale=1.0" />
    <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>

  <body>
    <noscript>
      <strong
        >We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work
        properly without JavaScript enabled. Please enable it to
        continue.</strong
      >
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>
// 用于生成index.html
new HtmlWebpackPlugin({
  template: './public/index.html',
  // ...
});
// 用于生成about.html
new HtmlWebpackPlugin({
  filename: 'about.html',
  // ...
});

copy-webpack-plugin
对一些公共资源文件直接复制到打包目录中。比如 public/favicon.ico

new CopyWebpackPlugin({
  patterns: [{ from: 'public/favicon.ico', to: '.' }],
});

我们一般在使用插件的时候掌握一些经常用的就可以。后面根据需求再去提炼关键词,搜索自己想用的插件,当然也可以自己写。插件的约定名称一般都是 XXX-webpack-plugin,比如我们想要压缩图片就可以找 imagemin-webpack-plugin

首先要明白:

自定义的 Plugin 其实就是一个函数,或者包含 apply 的方法的对象
apply 方法接受一个 compiler 对象参数,这个参数包含我们整个构建过程中的所有配置信息,通过这个对象我们可以注册钩子函数,通过 tap 方法注册任务
tap 方法又接受两个参数,一个是插件名称,一个是当前次打包执行的上下文

class MyPlugin {
  apply(compiler) {
    console.log('MyPlugin 启动');
    // 这里要做的事情就是在emit钩子上挂载一个任务,这个任务帮我们去除打包后没有必要的注释(mode=none情况下)其他钩子可参考官网
    compiler.hooks.emit.tap('MyPlugin', compilation => {
      // compilation => 可以理解为此次打包的上下文
      for (const name in compilation.assets) {
        // console.log(name)
        // console.log(compilation.assets[name].source())
        if (name.endsWith('.js')) {
          const contents = compilation.assets[name].source();
          const withoutComments = contents.replace(/\/\*\*+\*\//g, '');
          compilation.assets[name] = {
            source: () => withoutComments,
            size: () => withoutComments.length,
          };
        }
      }
    });
  }
}
plugins: [new MyPlugin()];
// 不使用Webpack Dev Server情况下,自动监听打包文件的变化
$ yarn webpack --watch
$ http-server -c-1 dist //or $ browser-sync dist --file  "**/*"

以上方式效率太低,文件不断的被读写操作,有待优化

$ yarn add webpack-dev-server -D
$ yarm webpack-dev-server --open

webpack-dev-server 并不会将打包结果放到磁盘中,暂时存放到内存中,从临时内存中读取内容发送给浏览器,从而大大提高了效率

devServer: {
  contentBase: './public', //也可以指定数组标识多个目录
}
  devServer: {
    proxy: {
      '/api': {
        // http://localhost:8080/api/users -> https://api.github.com/api/users
        target: 'https://api.github.com',
        // http://localhost:8080/api/users -> https://api.github.com/users
        pathRewrite: {
          '^/api': '' // 根据后端接口文件路劲因情况而定,这里只是用github举例说明
        },
        // 不能使用 localhost:8080 作为请求 GitHub 的主机名
        changeOrigin: true
      }
    }
  }
// main.js;
// 跨域请求,虽然 GitHub 支持 CORS,但是不是每个服务端都应该支持。
// fetch('https://api.github.com/users')
fetch('/api/users') // http://localhost:8080/api/users
  .then(res => res.json())
  .then(data => {
    data.forEach(item => {
      const li = document.createElement('li');
      li.textContent = item.login;
      ul.append(li);
    });
  });
devtool: // 开发环境  'cheap-module-eval-source-map',
  // 生产环境 'none',
  // 如果对自己上线代码没有信心 'nosources-source-map'
const HtmlWebpackPlugin = require('html-webpack-plugin');

const allModes = [
  'eval',
  'cheap-eval-source-map',
  'cheap-module-eval-source-map',
  'eval-source-map',
  'cheap-source-map',
  'cheap-module-source-map',
  'inline-cheap-source-map',
  'inline-cheap-module-source-map',
  'source-map',
  'inline-source-map',
  'hidden-source-map',
  'nosources-source-map',
];

module.exports = allModes.map(item => {
  return {
    devtool: item,
    mode: 'none',
    entry: './src/main.js',
    output: {
      filename: `js/${item}.js`,
    },
    module: {
      rules: [
        {
          test: /\.js$/,
          use: {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-env'],
            },
          },
        },
      ],
    },
    plugins: [
      new HtmlWebpackPlugin({
        filename: `${item}.html`,
      }),
    ],
  };
});
$ yarn webpack-dev-server --hot
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'development',
  entry: './src/main.js',
  output: {
    filename: 'js/bundle.js',
  },
  devtool: 'source-map',
  devServer: {
    hot: true,
    // hotOnly: true // 只使用 HMR,不会 fallback 到 live reloading
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    // ...
  ],
};

默认的 HMR 开启后还需要我们手动去处理热更新的逻辑。当然在 css 文件中由于 cssloader 中已经帮我们处理了,所以我们可以看到修改 css 可以出发热跟新
编写的 js 模块由于代码太过灵活,如果没有框架的约束,wabpack 很难实现通用的热更新

import createEditor from './editor';
import background from './better.png';
import './global.css';

const editor = createEditor();
document.body.appendChild(editor);

const img = new Image();
img.src = background;
document.body.appendChild(img);

// ============ 以下用于处理 HMR,与业务代码无关 ============

// console.log(createEditor)

if (module.hot) {
  let lastEditor = editor;
  // 处理js模块的热更新
  module.hot.accept('./editor', () => {
    // console.log('editor 模块更新了,需要这里手动处理热替换逻辑')
    // console.log(createEditor)

    const value = lastEditor.innerHTML;
    document.body.removeChild(lastEditor);
    const newEditor = createEditor();
    // 解决文本框状态丢失
    newEditor.innerHTML = value;
    document.body.appendChild(newEditor);
    lastEditor = newEditor;
  });
  // 处理img热更新
  module.hot.accept('./better.png', () => {
    img.src = background;
    console.log(background);
  });
}

以上例子 只是说明 webpack 没办法提供通用方案。实现一个热更新原理就是利用 module.hot,HotModuleReplacementPluginApi 提供的这个。大部分框架中都集成了 HMR。

// 函数方式配置
const webpack = require('webpack');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = (env, argv) => {
  const config = {
    // ...
  };

  if (env === 'production') {
    config.mode = 'production';
    config.devtool = false;
    config.plugins = [
      ...config.plugins,
      new CleanWebpackPlugin(),
      new CopyWebpackPlugin(['public']),
    ];
  }

  return config;
};

文件划分的配置

// webpack.common.js

module.exports = {};

// webpack.dev.js
const common = require('./webpack.common');
const merge = require('webpack-merge'); // 安装webpack-merge合并配置
module.exports = merge(common, {
  mode: 'development',
  devtool: 'cheap-eval-module-source-map',
  devServer: {
    hot: true,
    contentBase: 'public',
  },
  plugins: [new webpack.HotModuleReplacementPlugin()],
});

// webpack.prod.js
const merge = require('webpack-merge');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const common = require('./webpack.common');

module.exports = merge(common, {
  mode: 'production',
  plugins: [new CleanWebpackPlugin(), new CopyWebpackPlugin(['public'])],
});
$ yarn webpack --config webpack.prod.js
$ yarn webpack-dev-server --config webpack.dev.js
plugins: [
  new webpack.DefinePlugin({
    // 值要求的是一个代码片段
    API_BASE_URL: JSON.stringify('https://api.example.com'),
  }),
];
 optimization: {
    // 模块只导出被使用的成员
    usedExports: true,
    // 尽可能合并每一个模块到一个函数中
    concatenateModules: true, // scope Hoisting
    // 压缩输出结果
    minimize: true
  }
// webpack.config.js
optimization: {
  sideEffects: true; // 开启sideEffects功能
}
// package.json
"siedEffects": false // 标识代码是否有副作用

副作用需要我们手动添加并且谨慎使用,一般用在开发第三方包中,当我们的代码有副作用,但是却配置了以上两个属性,就会导致程序报错。

// package.json 配置有副作用的文件,这样webpack在打包的过程中就不会忽略这些
"siedEffects" :[
  "./src/extend.js",
  "*/css"
]
if (hash === '#posts') {
  // mainElement.appendChild(posts())
  import(/* webpackChunkName: 'components' */ './posts/posts').then(
    ({ default: posts }) => {
      mainElement.appendChild(posts());
    }
  );
} else if (hash === '#album') {
  // mainElement.appendChild(album())
  import(/* webpackChunkName: 'components' */ './album/album').then(
    ({ default: album }) => {
      mainElement.appendChild(album());
    }
  );
}
 module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          // 'style-loader', // 将样式通过 style 标签注入
          MiniCssExtractPlugin.loader, // 将样式通过Link标签方式注入
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin()
  ]
optimization: {
  minimize: [
  // 要使用其他压缩,这里要把默认的js压缩的插件也安装进来,是因为webpack会覆盖了 optimization原有的默认配置
  // 这里配置的压缩都只会在生产环境起作用,符合我们的预期,不用再去放到webpack.prod.js或者根据环境变量判断
  new TreserWebpackPlugin(),
  // 这里以压缩css为例,其他的参见官网
  new OptimizeCssAssetsWebpackPlugin()]
}
  output: {
    filename: '[name]-[contenthash:8].bundle.js'
  },

还有一些其他的配置项比如 preformance target externals resolve other option 我们只需要查阅官方文档即可,另外还需要多理解 manifest 和 runtime 这样的 webpack 概念

上一篇下一篇

猜你喜欢

热点阅读