一步步从零开始用 webpack 搭建一个大型项目
开篇
很多人都或多或少使用过 webpack,但是很少有人能够系统的学习 webpack 配置,遇到错误的时候就会一脸懵,不知道从哪查起?性能优化时也不知道能做什么,网上的优化教程是不是符合自己的项目?等一系列问题!本文从最基础配置一步步到一个完善的大型项目的过程。让你对 webpack 再也不会畏惧,让它真正成为你的得力助手!
本文从下面几个课题来实现
- 课题 1:初探 webpack?探究 webpack 打包原理
- 课题 2:搭建开发环境跟生产环境
- 课题 3:基础配置之loader
- 课时 4:webpack性能优化
- 课时 5:手写loader实现可选链
- 课时 6:webpack编译优化
- 课时 7:多页面配置
- 课时 8:手写一个webpack插件
- 课时 9:构建 ssr
项目地址
我把每一课都切成了不同的分支,大家可以根据课时一步步学习
脚手架
- npm i -g webpack-box
使用
- webpack-box dev # 开发环境
- webpack-box build # 生产环境
- webpack-box dll # 编译差分包
- webpack-box dev index # 指定页面编译(多页面)
- webpack-box build index # 指定页面编译(多页面)
- webpack-box build index --report # 开启打包分析
- webpack-box build:ssr # 编译ssr
- webpack-box ssr:server # 在 server 端运行
在 package.json 中使用
- {
- "scripts": {
- "dev": "webpack-box dev",
- "build": "webpack-box build",
- "dll": "webpack-box dll",
- "build:ssr": "webpack-box build:ssr",
- "ssr:server": "webpack-box ssr:server"
- }
- }
使用
- npm run build --report # 开启打包分析
扩展配置
box.config.js
- module.exports = function (config) {
- /**
- * @param {object} dll 开启差分包
- * @param {object} pages 多页面配置 通过 box run/build index 来使用
- * @param {function} chainWebpack
- * @param {string} entry 入口
- * @param {string} output 出口
- * @param {string} publicPath
- * @param {string} port
- */
- return {
- entry: 'src/main.js',
- output: 'dist',
- publicPath: '/common/',
- port: 8888,
- dll: {
- venders: ['vue', 'react']
- },
- pages: {
- index: {
- entry: 'src/main.js',
- template: 'public/index.html',
- filename: 'index.html',
- },
- index2: {
- entry: 'src/main.js',
- template: 'public/index2.html',
- filename: 'index2.html',
- }
- },
- chainWebpack(config) {
- }
- }
- }
课题 1:初探 webpack?探究 webpack 打包原理
想要学好 webpack
,我们首先要了解 webpack
的机制,我们先从js加载css开始学习。
我们从下面这个小练习开始走进 webpack
吧
在 index.js
中引入 index.css
- const css = require('./index.css')
- console.log(css)
css 文件并不能被 js 识别,webpack 也不例外,上述的写法不出意外会报错
我们如何让 webpack 识别 css 呢,答案就在 webpack 给我们提供了 loader 机制,可以让我们通过loader 将任意的文件转成 webpack 可以识别的文件
本章主要讲解
webpack 基础配置
需要的依赖包
package.json
- {
- "scripts": {
- "dev": "cross-env NODE_ENV=development webpack", // 开发环境
- "build": "cross-env NODE_ENV=production webpack" // 生产环境
- },
- "dependencies": {
- "cross-env": "^6.0.3", // 兼容各种环境
- "css-loader": "^3.2.0",
- "rimraf": "^3.0.0", // 删除文件
- "webpack": "^4.41.2"
- },
- "devDependencies": {
- "webpack-cli": "^3.3.10"
- }
- }
webpack 基础配置
webpack.config.js
- const path = require('path');
- const rimraf = require('rimraf');
- // 删除 dist 目录
- rimraf.sync('dist');
- // webpack 配置
- module.exports = {
- entry: './src/index',
- mode: process.env.NODE_ENV,
- output: {
- filename: 'bundle.js',
- path: path.resolve(__dirname, 'dist')
- }
- };
css 引入到 js
src/index.js
- const css = require('css-loader!./index.css');
- const a = 100;
- console.log(a, css);
测试 css
src/index.css
- body {
- width: 100%;
- height: 100vh;
- background-color: orange;
- }
解析 bundle 如何加载模块
我删掉了一些注释跟一些干扰内容,这样看起来会更清晰一点
bundle
是一个立即执行函数,可以认为它是把所有模块捆绑在一起的一个巨型模块。webpack
将所有模块打包成了bundle
的依赖,通过一个对象注入0 模块
就是入口webpack
通过__webpack_require__
引入模块__webpack_require__
就是我们使用的require
,被webpack
封装了一层
dist/bundle.js
- (function(modules) {
- function __webpack_require__(moduleId) {
- if (installedModules[moduleId]) {
- return installedModules[moduleId].exports;
- }
- var module = (installedModules[moduleId] = {
- i: moduleId,
- l: false,
- exports: {}
- });
- modules[moduleId].call(
- module.exports,
- module,
- module.exports,
- __webpack_require__
- );
- module.l = true;
- return module.exports;
- }
- return __webpack_require__((__webpack_require__.s = 0));
- })({
- './src/index.js': function(module, exports, __webpack_require__) {
- eval(`
- const css = __webpack_require__("./src/style/index.css")
- const a = 100;
- console.log(a, css)
- `);
- },
- './src/style/index.css': function(module, exports, __webpack_require__) {
- eval(`
- exports = module.exports = __webpack_require__("./node_modules/css-loader/dist/runtime/api.js")(false);
- exports.push([module.i, "body {
- width: 100%;
- height: 100vh;
- background-color: orange;
- }", ""]);
- `);
- },
- 0: function(module, exports, __webpack_require__) {
- module.exports = __webpack_require__('./src/index.js');
- }
- });
动态 import 加载原理
如果我们把 index.js 的 require 改成 import 会发生什么?
我们知道 import
跟 require
的区别是,import
是动态加载只有在用到的时候才会去加载,而 require
只要声明了就会加载,webpack
遇到了 require
就会把它当成一个模块加载到 bundle
的依赖里
那么问题来了,如果我们使用了 import 去引用一个模块,它是如何加载的呢?
require 改成 import()
src/index.js
- // const css = require('css-loader!./index.css');
- const css = import('css-loader!./index.css');
- const a = 100;
- console.log(a, css);
动态加载打包结果
除了正常的 bundle
之外,我们还可以看见一个 0.boundle.js
0.boundle.js
就是我们的动态加载的 index.css
模块
- |-- bundle.js
- |-- 0.boundle.js
动态模块
0.boundle.js
这个文件就是把我们 import
的模块放进了一个单独的 js
文件中
- (window['webpackJsonp'] = window['webpackJsonp'] || []).push([
- [0],
- {
- './node_modules/css-loader/dist/runtime/api.js': function(
- module,
- exports,
- __webpack_require__
- ) {
- 'use strict';
- eval(`
- ...
- `);
- },
- './src/style/index.css': function(module, exports, __webpack_require__) {
- eval(`
- exports = module.exports = __webpack_require__("./node_modules/css-loader/dist/runtime/api.js")(false));
- exports.push([module.i, \`body {
- width: 100%;
- height: 100vh;
- background-color: orange;
- },"\`]
- `);
- }
- }
- ]);
动态模块加载逻辑
我们再看下 dist/bundle.js
方便理解,我把大部分代码和注释都删掉了
原理很简单,就是利用的 jsonp 的实现原理加载模块,只是在这里并不是从 server 拿数据而是从其他模块中
- 调用模块时会在
window
上注册一个webpackJsonp
数组,window['webpackJsonp'] = window['webpackJsonp'] || [] - 当我们
import
时,webpack
会调用__webpack_require__.e(0)
方法,也就是requireEnsure
webpack
会动态创建一个script
标签去加载这个模块,加载成功后会将该模块注入到webpackJsonp
中webpackJsonp.push
会调用webpackJsonpCallback
拿到模块- 模块加载完(then)再使用
__webpack_require__
获取模块
- (function(modules) {
- function webpackJsonpCallback(data) {
- var chunkIds = data[0];
- var moreModules = data[1];
- var moduleId,
- chunkId,
- i = 0,
- resolves = [];
- for (; i < chunkIds.length; i++) {
- chunkId = chunkIds[i];
- if (
- Object.prototype.hasOwnProperty.call(installedChunks, chunkId) &&
- installedChunks[chunkId]
- ) {
- resolves.push(installedChunks[chunkId][0]);
- }
- // 模块安装完
- installedChunks[chunkId] = 0;
- }
- for (moduleId in moreModules) {
- if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
- modules[moduleId] = moreModules[moduleId];
- }
- }
- if (parentJsonpFunction) parentJsonpFunction(data);
- while (resolves.length) {
- // 执行所有 promise 的 resolve 函数
- resolves.shift()();
- }
- }
- function jsonpScriptSrc(chunkId) {
- return __webpack_require__.p + '' + ({}[chunkId] || chunkId) + '.bundle.js';
- }
- function __webpack_require__(moduleId) {
- // ...
- }
- __webpack_require__.e = function requireEnsure(chunkId) {
- var promises = [];
- // ...
- var script = document.createElement('script');
- var onScriptComplete;
- script.charset = 'utf-8';
- script.timeout = 120;
- script.src = jsonpScriptSrc(chunkId);
- onScriptComplete = function(event) {
- // 处理异常,消除副作用
- // ...
- };
- var timeout = setTimeout(function() {
- onScriptComplete({ type: 'timeout', target: script });
- }, 120000);
- script.onerror = script.onload = onScriptComplete;
- document.head.appendChild(script);
- // ...
- // 动态加载模块
- return Promise.all(promises);
- };
- var jsonpArray = (window['webpackJsonp'] = window['webpackJsonp'] || []);
- // 重写数组 push 方法
- jsonpArray.push = webpackJsonpCallback;
- jsonpArray = jsonpArray.slice();
- for (var i = 0; i < jsonpArray.length; i++)
- webpackJsonpCallback(jsonpArray[i]);
- return __webpack_require__((__webpack_require__.s = 0));
- })({
- './src/index.js': function(module, exports, __webpack_require__) {
- eval(`
- const css = __webpack_require__.e(0).then(__webpack_require__.t.bind(null, "./src/style/index.css", 7))
- const a = 100;
- console.log(a, css)
- `);
- },
- 0: function(module, exports, __webpack_require__) {
- eval(`module.exports = __webpack_require__("./src/index.js");`);
- }
- });
使用 webpack-chain 重写配置
我们用 webpack-chain 来写 webpack 的配置,原因是 webpack-chain 的方式更加灵活
官方解释
webpack-chain
尝试通过提供可链式或顺流式的API
创建和修改webpack
配置。API
的Key
部分可以由用户指定的名称引用,这有助于跨项目修改配置方式的标准化。
- const path = require('path');
- const rimraf = require('rimraf');
- const Config = require('webpack-chain');
- const config = new Config();
- const resolve = src => {
- return path.join(process.cwd(), src);
- };
- // 删除 dist 目录
- rimraf.sync('dist');
- config
- // 入口
- .entry('src/index')
- .add(resolve('src/index.js'))
- .end()
- // 模式
- // .mode(process.env.NODE_ENV) 等价下面
- .set('mode', process.env.NODE_ENV)
- // 出口
- .output.path(resolve('dist'))
- .filename('[name].bundle.js');
- config.module
- .rule('css')
- .test(/\.css$/)
- .use('css')
- .loader('css-loader');
- module.exports = config.toConfig();
课时 1 小结
至此课时 1 已经结束了,我们主要做了以下事情
- webpack 基础配置
- 将 css 通过 css-loader 打包进 js 中
- 解析 bundle 如何加载模块的
- webpack 如何实现的动态加载模块
学习一个工具我们不仅要看懂它的配置,还要对它的原理一起了解,只有学到框架的精髓,我们才能应对如今大前端如此迅猛的发展。
课题 2:搭建开发环境跟生产环境
本章提要:
目录
- │── build
- │ │── base.js // 公共部分
- │ │── build.js
- │ └── dev.js
- │── config
- │ │── base.js // 基础配置
- │ │── css.js // css 配置
- │ │── HtmlWebpackPlugin.js // html 配置
- │ └── MiniCssExtractPlugin.js // 提取css
- │── public // 公共资源
- │ └── index.html // html 模版
- └── src // 开发目录
- │── style
- │ └── index.css
- └── main.js // 主入口
实现可插拔配置
package.json
- {
- "scripts": {
- "dev": "cross-env NODE_ENV=development node build/dev.js",
- "build": "cross-env NODE_ENV=production node build/build.js"
- },
- "dependencies": {
- "cross-env": "^6.0.3",
- "css-loader": "^3.2.0",
- "cssnano": "^4.1.10",
- "ora": "^4.0.3",
- "rimraf": "^3.0.0",
- "webpack": "^4.41.2"
- },
- "devDependencies": {
- "extract-text-webpack-plugin": "^3.0.2",
- "html-webpack-plugin": "^3.2.0",
- "mini-css-extract-plugin": "^0.8.0",
- "vue-cli-plugin-commitlint": "^1.0.4",
- "webpack-chain": "^6.0.0",
- "webpack-cli": "^3.3.10",
- "webpack-dev-server": "^3.9.0"
- }
- }
build/base.js
- const { findSync } = require('../lib');
- const Config = require('webpack-chain');
- const config = new Config();
- const files = findSync('config');
- const path = require('path');
- const resolve = p => {
- return path.join(process.cwd(), p);
- };
- module.exports = () => {
- const map = new Map();
- files.map(_ => {
- const name = _.split('/')
- .pop()
- .replace('.js', '');
- return map.set(name, require(_)(config, resolve));
- });
- map.forEach(v => v());
- return config;
- };
构建生产环境
build/build.js
- const rimraf = require('rimraf');
- const ora = require('ora');
- const chalk = require('chalk');
- const path = require('path');
- // 删除 dist 目录
- rimraf.sync(path.join(process.cwd(), 'dist'));
- const config = require('./base')();
- const webpack = require('webpack');
- const spinner = ora('开始构建项目...');
- spinner.start();
- webpack(config.toConfig(), function(err, stats) {
- spinner.stop();
- if (err) throw err;
- process.stdout.write(
- stats.toString({
- colors: true,
- modules: false,
- children: false,
- chunks: false,
- chunkModules: false
- }) + '\n\n'
- );
- if (stats.hasErrors()) {
- console.log(chalk.red('构建失败\n'));
- process.exit(1);
- }
- console.log(chalk.cyan('build完成\n'));
- });
构建开发环境(devServer)
build/dev.js
- const config = require('./base')();
- const webpack = require('webpack');
- const chalk = require('chalk');
- const WebpackDevServer = require('webpack-dev-server');
- const port = 8080;
- const publicPath = '/common/';
- config.devServer
- .quiet(true)
- .hot(true)
- .https(false)
- .disableHostCheck(true)
- .publicPath(publicPath)
- .clientLogLevel('none');
- const compiler = webpack(config.toConfig());
- // 拿到 devServer 参数
- const chainDevServer = compiler.options.devServer;
- const server = new WebpackDevServer(
- compiler,
- Object.assign(chainDevServer, {})
- );
- ['SIGINT', 'SIGTERM'].forEach(signal => {
- process.on(signal, () => {
- server.close(() => {
- process.exit(0);
- });
- });
- });
- // 监听端口
- server.listen(port);
- new Promise(() => {
- compiler.hooks.done.tap('dev', stats => {
- const empty = ' ';
- const common = `App running at:
- - Local: http://127.0.0.1:${port}${publicPath}\n`;
- console.log(chalk.cyan('\n' + empty + common));
- });
- });
提取 css
config/css.js
css 提取 loader 配置
- module.exports = (config, resolve) => {
- return (lang, test) => {
- const baseRule = config.module.rule(lang).test(test);
- const normalRule = baseRule.oneOf('normal');
- applyLoaders(normalRule);
- function applyLoaders(rule) {
- rule
- .use('extract-css-loader')
- .loader(require('mini-css-extract-plugin').loader)
- .options({
- publicPath: './'
- });
- rule
- .use('css-loader')
- .loader('css-loader')
- .options({});
- }
- };
- };
css 提取插件 MiniCssExtractPlugin
config/MiniCssExtractPlugin.js
- const MiniCssExtractPlugin = require('mini-css-extract-plugin');
- module.exports = (config, resolve) => {
- return () => {
- config
- .oneOf('normal')
- .plugin('mini-css-extract')
- .use(MiniCssExtractPlugin);
- };
- };
自动生成 html
config/HtmlWebpackPlugin.js
- const HtmlWebpackPlugin = require('html-webpack-plugin');
- module.exports = (config, resolve) => {
- return () => {
- config.plugin('html').use(HtmlWebpackPlugin, [
- {
- template: 'public/index.html'
- }
- ]);
- };
- };
项目测试
测试 html 模板
public/index.html
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="UTF-8">
- <title>learn_webpack</title>
- <body></body>
- </html>
测试 css 模板
src/style/index.css
- .test {
- width: 200px;
- height: 200px;
- color: red;
- background-color: orange;
- }
程序入口
src/main.js
- require('./style/index.css');
- const h2 = document.createElement('h2');
- h2.className = 'test';
- h2.innerText = 'test';
- document.body.append(h2);
课题 3:基础配置之loader
本章提要:
- 配置 babel
- 使用 babel 配置 ts
- ts 静态类型检查
- 友好错误提示插件
- 配置样式,style,css、less、sass、postcss 等
- postcss 配置
- 编译前后 css 对比
- 配置 autoprefixer
- 开启 source map
目录
增加以下文件
- │──── config // 配置目录
- │ │── babelLoader.js // babel-loader 配置
- │ │── ForkTsChecker.js // ts 静态检查
- │ │── FriendlyErrorsWebpackPlugin.js // 友好错误提示
- │ └── style
- │──── src // 开发目录
- │ │── style
- │ │ │── app.css
- │ │ │── index.less // 测试 less
- │ │ │── index.scss // 测试 sass
- │ │ └── index.postcss // 测试 postcss
- │ └── ts
- │ └── index.ts // 测试 ts
- │── babel.js
- │── postcss.config.js // postcss 配置
- │── tsconfig.json // ts 配置
- └──── dist // 打包后的目录
- │── app.bundle.js
- │── app.css
- └── index.html
配置 babel
config/babelLoader.js
- module.exports = (config, resolve) => {
- const baseRule = config.module.rule('js').test(/.js│.tsx?$/);
- const babelPath = resolve('babel.js');
- const babelConf = require(babelPath);
- const version = require(resolve('node_modules/@babel/core/package.json'))
- .version;
- return () => {
- baseRule
- .use('babel')
- .loader(require.resolve('babel-loader'))
- .options(babelConf({ version }));
- };
- };
使用 babel 配置 ts
这里我们使用 babel
插件 @babel/preset-typescript
将 ts
转成 js,并使用
ForkTsCheckerWebpackPlugin
、ForkTsCheckerNotifierWebpackPlugin
插件进行错误提示。
babel.js
- module.exports = function(api) {
- return {
- presets: [
- [
- '@babel/preset-env',
- {
- targets: {
- chrome: 59,
- edge: 13,
- firefox: 50,
- safari: 8
- }
- }
- ],
- [
- '@babel/preset-typescript',
- {
- allExtensions: true
- }
- ]
- ],
- plugins: [
- '@babel/plugin-transform-typescript',
- 'transform-class-properties',
- '@babel/proposal-object-rest-spread'
- ]
- };
- };
ts 静态类型检查
- const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
- const ForkTsCheckerNotifierWebpackPlugin = require('fork-ts-checker-notifier-webpack-plugin');
- module.exports = (config, resolve) => {
- return () => {
- config.plugin('ts-fork').use(ForkTsCheckerWebpackPlugin, [
- {
- // 将async设为false,可以阻止Webpack的emit以等待类型检查器/linter,并向Webpack的编译添加错误。
- async: false
- }
- ]);
- // 将TypeScript类型检查错误以弹框提示
- // 如果fork-ts-checker-webpack-plugin的async为false时可以不用
- // 否则建议使用,以方便发现错误
- config.plugin('ts-notifier').use(ForkTsCheckerNotifierWebpackPlugin, [
- {
- title: 'TypeScript',
- excludeWarnings: true,
- skipSuccessful: true
- }
- ]);
- };
- };
友好错误提示插件
config/FriendlyErrorsWebpackPlugin.js
- const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
- module.exports = (config, resolve) => {
- return () => {
- config.plugin('error').use(FriendlyErrorsWebpackPlugin);
- };
- };
配置样式,style,css、less、sass、postcss 等
- module.exports = (config, resolve) => {
- const createCSSRule = (lang, test, loader, options = {}) => {
- const baseRule = config.module.rule(lang).test(test);
- const normalRule = baseRule.oneOf('normal');
- normalRule
- .use('extract-css-loader')
- .loader(require('mini-css-extract-plugin').loader)
- .options({
- hmr: process.env.NODE_ENV === 'development',
- publicPath: '/'
- });
- normalRule
- .use('css-loader')
- .loader(require.resolve('css-loader'))
- .options({});
- normalRule.use('postcss-loader').loader(require.resolve('postcss-loader'));
- if (loader) {
- const rs = require.resolve(loader);
- normalRule
- .use(loader)
- .loader(rs)
- .options(options);
- }
- };
- return () => {
- createCSSRule('css', /\.css$/, 'css-loader', {});
- createCSSRule('less', /\.less$/, 'less-loader', {});
- createCSSRule('scss', /\.scss$/, 'sass-loader', {});
- createCSSRule('postcss', /\.p(ost)?css$/);
- };
- };
postcss 配置
- module.exports = {
- plugins: {
- 'postcss-px-to-viewport': {
- unitToConvert: 'px',
- viewportWidth: 750,
- unitPrecision: 5,
- propList: ['*'],
- viewportUnit: 'vw',
- fontViewportUnit: 'vw',
- selectorBlackList: [],
- minPixelValue: 1,
- mediaQuery: false,
- replace: true,
- exclude: [],
- landscape: false,
- landscapeUnit: 'vw',
- landscapeWidth: 568
- }
- }
- };
编译前后 css 对比
src/style/index.less
- /* index.less */
- .test {
- width: 300px;
- }
dist/app.css
- /* index.css */
- .test {
- width: 36.66667vw;
- height: 26.66667vw;
- color: red;
- background-color: orange;
- }
- /* app.css */
- .test {
- font-size: 8vw;
- }
- /* index.less */
- .test {
- width: 40vw;
- }
- /* index.scss */
- .test {
- height: 40vw;
- }
- /* index.postcss */
- .test {
- background: green;
- height: 26.66667vw;
- }
配置 autoprefixer
自动添加 css 前缀
postcss.config.js
- module.exports = {
- plugins: {
- autoprefixer: {
- overrideBrowserslist: [
- '> 1%',
- 'last 3 versions',
- 'iOS >= 8',
- 'Android >= 4',
- 'Chrome >= 40'
- ]
- }
- }
- };
转换前
- /* index.css */
- .test {
- width: 200px;
- height: 200px;
- color: red;
- display: flex;
- background-color: orange;
- }
转换后
- /* index.css */
- .test {
- width: 26.66667vw;
- height: 26.66667vw;
- color: red;
- display: -webkit-box;
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
- background-color: orange;
- }
开启 source map
- config.devtool('cheap-source-map');
- └── dist
- │── app.bundle.js
- │── app.bundle.js.map
- │── app.css
- │── app.css.map
- └── index.html
在源文件下会有一行注释,证明开启了 sourcemap
- /*# sourceMappingURL=app.css.map*/
课时 4:webpack性能优化
本章讲解
分离 Manifest
- module.exports = (config, resolve) => {
- return () => {
- config
- .optimization
- .runtimeChunk({
- name: "manifest"
- })
- }
- }
Code Splitting
- 使用动态 import 或者 require.ensure 语法,在第一节已经讲解
- 使用
babel-plugin-import
插件按需引入一些组件库
Bundle Splitting
将公共的包提取到 chunk-vendors
里面,比如你require('vue'),webpack 会将 vue 打包进 chunk-vendors.bundle.js
- module.exports = (config, resolve) => {
- return () => {
- config
- .optimization.splitChunks({
- chunks: 'async',
- minSize: 30000,
- minChunks: 1,
- maxAsyncRequests: 3,
- maxInitialRequests: 3,
- cacheGroups: {
- vendors: {
- name: `chunk-vendors`,
- test: /[\\/]node_modules[\\/]/,
- priority: -10,
- chunks: 'initial'
- },
- common: {
- name: `chunk-common`,
- minChunks: 2,
- priority: -20,
- chunks: 'initial',
- reuseExistingChunk: true
- }
- }
- })
- config.optimization.usedExports(true)
- }
- }
Tree Shaking
config/optimization.js
- config.optimization.usedExports(true);
src/treeShaking.js
- export function square(x) {
- return x * x;
- }
- export function cube(x) {
- return x * x * x;
- }
在 main.js 中只引用了 cube
- import { cube } from './treeShaking';
- console.log(cube(2));
未使用 Tree Shaking
- {
- "./src/treeShaking.js": function(
- module,
- __webpack_exports__,
- __webpack_require__
- ) {
- "use strict";
- __webpack_require__.r(__webpack_exports__);
- __webpack_require__.d(__webpack_exports__, "square", function() {
- return square;
- });
- __webpack_require__.d(__webpack_exports__, "cube", function() {
- return cube;
- });
- function square(x) {
- return x * x;
- }
- function cube(x) {
- return x * x * x;
- }
- }
- }
使用了 Tree Shaking
这里只导出了 cube 函数,并没有将 square 导出去
当然你可以看见 square 函数还是在 bundle 里面,但是在压缩的时候就会被干掉了,因为它并没有被引用
- {
- "./src/treeShaking.js": function(
- module,
- __webpack_exports__,
- __webpack_require__
- ) {
- "use strict";
- __webpack_require__.d(__webpack_exports__, "a", function() {
- return cube;
- });
- function square(x) {
- return x * x;
- }
- function cube(x) {
- return x * x * x;
- }
- }
- }
只有当函数给定输入后,产生相应的输出,且不修改任何外部的东西,才可以安全做shaking的操作
如何使用tree-shaking?
- 确保代码是es6格式,即 export,import
- package.json中,设置 sideEffects
- 确保 tree-shaking 的函数没有副作用
- babelrc中设置presets [["@babel/preset-env", { "modules": false }]] 禁止转换模块,交由webpack进行模块化处理
- 结合uglifyjs-webpack-plugin
其实在 webpack4
我们根本不需要做这些操作了,因为 webpack
在生产环境已经帮我们默认添加好了,开箱即用!
开启 gzip
CompressionWebpackPlugin.js
- const CompressionWebpackPlugin = require('compression-webpack-plugin');
- module.exports = (config, resolve) => {
- return () => {
- config.plugin('CompressionWebpackPlugin').use(CompressionWebpackPlugin, [
- {
- algorithm: 'gzip',
- test: /\.js(\?.*)?$/i,
- threshold: 10240,
- minRatio: 0.8
- }
- ]);
- };
- };
课时 5:手写loader实现可选链
本章内容
什么是 webpack loader
webpack loader
是 webpack
为了处理各种类型文件的一个中间层,webpack
本质上就是一个 node
模块,它不能处理 js
以外的文件,那么 loader
就帮助 webpack
做了一层转换,将所有文件都转成字符串,你可以对字符串进行任意操作/修改,然后返回给 webpack
一个包含这个字符串的对象,让 webpack
进行后面的处理。如果把 webpack
当成一个垃圾工厂的话,那么 loader
就是这个工厂的垃圾分类!
可选链介绍
这里并不是纯粹意义上的可选链,因为 babel
跟 ts
都已经支持了,我们也没有必要去写一个完整的可选链,只是来加深一下对 loader
的理解, loader
在工作当中能帮助我们做什么?
用途
当我们访问一个对象属性时不必担心这个对象是 undefined
而报错,导致程序不能继续向下执行
解释
在 ?
之前的所有访问链路都是合法的,不会产生报错
- const obj = {
- foo: {
- bar: {
- baz: 2
- }
- }
- }
- console.log(obj.foo.bar?.baz) //
- // 被转成 obj && obj.foo && obj.foo.bar && obj.foo.bar.baz
- console.log(obj.foo.err?.baz) // undefined
- // 被转成 obj && obj.foo && obj.foo.err && obj.foo.err.baz
loader 实现可选链
配置loader,options-chain-loader
config/OptionsChainLoader.js
- module.exports = (config, resolve) => {
- const baseRule = config.module.rule('js').test(/.js|.tsx?$/);
- const normalRule = baseRule.oneOf('normal');
- return () => {
- normalRule
- .use('options-chain')
- .loader(resolve('options-chain-loader'))
- }
- }
其实就是正则替换,loader
将整个文件全部转换成字符串,content
就是整个文件的内容,对 content
进行修改,修改完成后再返回一个新的 content
就完成了一个 loader
转换。是不是很简单?
下面的操作意思就是,我们匹配 obj.foo.bar?.
并把它转成 obj && obj.foo && obj.foo.bar && obj.foo.bar.
options-chain-loader.js
- module.exports = function(content) {
- return content.replace(new RegExp(/([\$_\w\.]+\?\.)/,'g'),function(res) {
- let str = res.replace(/\?\./,'');
- let arrs = str.split('.');
- let strArr = [];
- for(let i = 1; i <= arrs.length; i++) {
- strArr.push(arrs.slice(0,i).join('.'));
- }
- let compile = strArr.join('&&');
- const done = compile + '&&' + str + '.'
- return done;
- });
- };
课时 6:webpack编译优化
本章内容
cache-loader
cache-loader
主要是将打包好的文件缓存在硬盘的一个目录里,一般存在 node_modules/.cache
下,当你再次 build
的时候如果此文件没有修改就会从缓存中读取已经编译过的文件,只有有改动的才会被编译,这样就大大降低了编译的时间。尤其是项目越大时越明显。
此项目使用前后数据对比 3342ms --> 2432ms 效果还是比较明显
这里只对 babel 加入了 cache-loader,因为我们的 ts/js 都是由 babel 进行编译的,不需要对 ts-loader 缓存(我们也没有用到)
config/cacheLoader.js
- module.exports = (config, resolve) => {
- const baseRule = config.module.rule('js').test(/.js|.tsx?$/);
- const babelPath = resolve('babel.js')
- const babelConf = require(babelPath);
- const version = require(resolve('node_modules/@babel/core/package.json')).version
- return () => {
- baseRule
- .exclude
- .add(filepath => {
- // 不缓存 node_modules 下的文件
- return /node_modules/.test(filepath)
- })
- .end()
- .use('cache-loader')
- .loader('cache-loader')
- .options({
- // 缓存位置
- cacheDirectory: resolve('node_modules/.cache/babel')
- })
- }
- }
DllPlugin
DllPlugin 是将第三方长期不变的包与实际项目隔离开来并分别打包,当我们 build 时再将已经打包好的 dll 包引进来就 ok 了
我提取了两个包 vue、react,速度差不多提升了 200ms,从 2698ms 到 2377ms
打包 dll
build/dll.js
- const path = require("path");
- const dllPath = path.join(process.cwd(), 'dll');
- const Config = require('webpack-chain');
- const config = new Config();
- const webpack = require('webpack')
- const rimraf = require('rimraf');
- const ora = require('ora')
- const chalk = require('chalk')
- const BundleAnalyzerPlugin = require('../config/BundleAnalyzerPlugin')(config)
- BundleAnalyzerPlugin()
- config
- .entry('dll')
- .add('vue')
- .add('react')
- .end()
- .set('mode', "production")
- .output
- .path(dllPath)
- .filename('[name].js')
- .library("[name]")
- .end()
- .plugin('DllPlugin')
- .use(webpack.DllPlugin, [{
- name: "[name]",
- path: path.join(process.cwd(), 'dll', 'manifest.json'),
- }])
- .end()
- rimraf.sync(path.join(process.cwd(), 'dll'))
- const spinner = ora('开始构建项目...')
- spinner.start()
- webpack(config.toConfig(), function (err, stats) {
- spinner.stop()
- if (err) throw err
- process.stdout.write(stats.toString({
- colors: true,
- modules: false,
- children: false,
- chunks: false,
- chunkModules: false
- }) + '\n\n')
- if (stats.hasErrors()) {
- console.log(chalk.red('构建失败\n'))
- process.exit(1)
- }
- console.log(chalk.cyan('build完成\n'))
- })
将 dll 包合并
- const webpack = require('webpack')
- module.exports = (config, resolve) => {
- return () => {
- config.plugin('DllPlugin')
- .use(webpack.DllReferencePlugin, [{
- context: process.cwd(),
- manifest: require(resolve('dll/manifest.json'))
- }])
- }
- }
threadLoader
测试效果变差了
一步步从零开始用 webpack 搭建一个大型项目的更多相关文章
- 从零开始使用vue-cli搭建一个vue项目及注意事项
一.安装node.js 1.根据电脑的自行下载node.js安装包http://nodejs.cn 2.点击安装,按照正常的的一路点击下去 3.验证安装是否成功,按键win+r,输入cmd打开命令行工 ...
- 从零开始搭建一个react项目
Nav logo 120 发现 关注 消息 4 搜索 从零开始搭建一个react项目 96 瘦人假噜噜 2017.04.23 23:29* 字数 6330 阅读 32892评论 31喜欢 36 项目地 ...
- 从零开始用 Flask 搭建一个网站(一)
前言 笔者之前未接触过 Python,只是略懂一点前端,所以说从零开始也相差无几吧.Flask 是一个轻量级的基于 Python 的框架,但是扩展性非常良好(Github 上 22000 多个 sta ...
- vue-用Vue-cli从零开始搭建一个Vue项目
Vue是近两年来比较火的一个前端框架(渐进式框架吧). Vue两大核心思想:组件化和数据驱动.组件化就是将一个整体合理拆分为一个一个小块(组件),组件可重复使用:数据驱动是前端的未来发展方向,释放了对 ...
- 从零开始使用 Webpack 搭建 Vue3 开发环境
从零开始使用 Webpack 搭建 Vue3 开发环境 创建项目 首先需要创建一个空目录,在该目录打开命令行,执行 npm init 命令创建一个项目,这个过程会提示输入一些内容,完成后会自动生成一个 ...
- 从零开始用 Flask 搭建一个网站(三)
从零开始用 Flask 搭建一个网站(二) 介绍了有关于数据库的运用,接下来我们在完善一下数据在前端以及前端到后端之间的交互.本节涉及到前端,因此也会讲解一下 jinja2 模板.jQuery.aja ...
- 从零开始用 Flask 搭建一个网站(二)
从零开始用 Flask 搭建一个网站(一) 介绍了如何搭建 Python 环境,以及 Flask 应用基本项目结构.我们要搭建的网站是管理第三方集成的控制台,类似于 Slack. 本篇主要讲解数据如何 ...
- 从零开始用 Flask 搭建一个网站(四)
前言 从零开始用 Flask 搭建一个网站(三) 介绍了网页前端与后端.前端与前端之间数据的交流.本节主要介绍一下如何应用 Flask-OAuthlib, 使用 Flask-OAuthlib 就可以轻 ...
- 完整搭建一个vue项目
目录 一. 搭建一个vue项目的完整步骤 二. 卸载vue-cli 三. 完全卸载webpack 一. 搭建一个vue项目的完整步骤 1.安装node.js 下载地址 # 检查是否安装成功 node ...
随机推荐
- vuex 源码分析(七) module和namespaced 详解
当项目非常大时,如果所有的状态都集中放到一个对象中,store 对象就有可能变得相当臃肿. 为了解决这个问题,Vuex允许我们将 store 分割成模块(module).每个模块拥有自己的 state ...
- HDU-1760 A New Tetris Game DFS
曾经,Lele和他姐姐最喜欢,玩得最久的游戏就是俄罗斯方块(Tetris)了. 渐渐得,Lele发觉,玩这个游戏只需要手快而已,几乎不用经过大脑思考. 所以,Lele想出一个新的玩法. Lele和姐姐 ...
- sql server 下载安装标记
SQL Server 2017 的各版本和支持的功能 https://docs.microsoft.com/zh-cn/sql/sql-server/editions-and-components-o ...
- 如何当上Leader和六千个bug的系统
在昨天的读书会上我分享了我是如何当上leader以及当上leader之后的体会.然后今天Sophie总结了我的发言,大家对此有些反馈.我根据大家的反馈写了这篇文章,主要针对几点: 大家如何当上lead ...
- SqlServer 开篇简介
实例:我们的电脑中可以安装一个或多个SqlServer实例,每一个SqlServer实例可以包含一个或者多个数据库. 架构:数据库中,又有一个或者多个架构.架构里面包含:表,视图,存储过程. 文件与文 ...
- 如何使用Charles让手机访问PC自定义域名?
需求:移动端访问PC上的自定义域名,如在Nginx上配置的域名 如vv.zzcloud.com这个域名在pc上是通过host映射的方式访问,现在需要在手机上访问到这个域名. 工具:Charles代 ...
- Vue 动态修改data 值 并触发视图更新
Vue 动态修改data 值 并触发视图更新 this.$set(obj, key, '') // Vue 动态修改或者添加data key 并触发视图更新
- 【APS系统应用案例】高博通信智能制造的新武器
企业背景: 早在2006年,一个年轻的企业瞄准国际高端航空产业及超精密制造行业.高博集团,以普世价值开创航空中国的新纪元. 高博通信(上海)有限公司(下文简称“高博通信”)占地36000平方米,以满足 ...
- android studio学习----gradle多渠道打包
由于国内Android市场众多渠道,为了统计每个渠道的下载及其它数据统计,就需要我们针对每个渠道单独打包,如果让你打几十个市场的包岂不烦死了,不过有了Gradle,这再也不是事了. 友盟多渠道打包 废 ...
- HeadFirst设计模式---装饰者
定义装饰者模式 装饰者模式动态地将责任附加到对象上,若要扩展功能,装饰者提供了比继承更有弹性的替代方案.这句话摘自书中,给人读得很生硬难懂.通俗地来说,装饰者和被装饰者有相同的父类,装饰者的行为组装着 ...