打包构建笔记

410 阅读12分钟

webpack

热更新原理

1.当修改了一个或多个文件; 2.文件系统接收更改并通知webpack; 3.webpack重新编译构建一个或多个模块,并通知HMR服务器进行更新; 4.HMR Server 使用webSocket通知HMR runtime 需要更新,HMR运行时通过HTTP请求更新jsonp; 5.HMR运行时替换更新中的模块,如果确定这些模块无法更新,则触发整个页面刷新。

总结:

webpack监听模式,每个模块都有个名称,当文件内容改变时,通过建立好的socket通知浏览器;
然后页面端的webpack脚手架代码会重载这个模块文件。

打包速度

1:确保下webpack,npm, node 及主要库版本要新,比如:4.x比3.x提升很多。

2:loader范围缩小到src项目文件!一些不必要的loader能关就关了吧

3:eslint代码校验其实是一个很费时间的一个步奏。 可以把eslint的范围缩小到src,且只检查*.js 和 *.vue ;生产环境不开启lint,使用pre-commit或者husky在提交前校验

4:thread-loader多进程进行, (happypack已不维护)

如果上面优化后,时间还是不满意的话,就尝试下5,6吧。

5:动态链接库DllPlugin 可以将特定的类库提前打包然后引入。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。 补充一下: 缺点:将不能按需加载,会将配置的第三方库全部打包进去。 推荐:可以将使用率较高的包采用dll方案。

6:HardSourceWebpackPlugin会将模块编译后进行缓存,第一次之后速度会明显提升。

7: 配置cdn,配置config externals,script引入cdn包,或者配置html-webpack-plugin动态引入script

常见的loader

raw-loader:加载文件原始内容(utf-8)

file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件 (处理图片和字体)

url-loader:与 file-loader 类似,区别是用户可以设置一个阈值,大于阈值会交给 file-loader 处理,小于阈值时返回文件 base64 形式编码 (处理图片和字体)

source-map-loader:加载额外的 Source Map 文件,以方便断点调试

svg-inline-loader:将压缩后的 SVG 内容注入代码中

image-loader:加载并且压缩图片文件

json-loader 加载 JSON 文件(默认包含)

handlebars-loader: 将 Handlebars 模版编译成函数并返回

babel-loader:把 ES6 转换成 ES5

ts-loader: 将 TypeScript 转换成 JavaScript

awesome-typescript-loader:将 TypeScript 转换成 JavaScript,性能优于 ts-loader

sass-loader:将SCSS/SASS代码转换成CSS

css-loader:加载 CSS,支持模块化、压缩、文件导入等特性

style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS

postcss-loader:扩展 CSS 语法,使用下一代 CSS,可以配合 autoprefixer 插件自动补齐 CSS3 前缀

eslint-loader:通过 ESLint 检查 JavaScript 代码

tslint-loader:通过 TSLint检查 TypeScript 代码

mocha-loader:加载 Mocha 测试用例的代码

coverjs-loader:计算测试的覆盖率

vue-loader:加载 Vue.js 单文件组件

i18n-loader: 国际化

cache-loader: 可以在一些性能开销较大的 Loader 之前添加,目的是将结果缓存到磁盘里

常见的plugin

define-plugin:定义环境变量 (Webpack4 之后指定 mode 会自动配置)

ignore-plugin:忽略部分文件

html-webpack-plugin:简化 HTML 文件创建 (依赖于 html-loader)

web-webpack-plugin:可方便地为单页应用输出 HTML,比 html-webpack-plugin 好用

uglifyjs-webpack-plugin:不支持 ES6 压缩 (Webpack4 以前)

webpack-parallel-uglify-plugin: 多进程执行代码压缩,提升构建速度(Webpack4 以前)

terser-webpack-plugin: 支持压缩 ES6 (Webpack4)

mini-css-extract-plugin: 分离样式文件,CSS 提取为独立文件,支持按需加载 (替代extract-text-webpack-plugin)

serviceworker-webpack-plugin:为网页应用增加离线缓存功能

clean-webpack-plugin: 目录清理

ModuleConcatenationPlugin: 开启 Scope Hoisting

speed-measure-webpack-plugin: 可以看到每个 Loader 和 Plugin 执行耗时 (整个打包耗时、每个 Plugin 和 Loader 耗时)

webpack-bundle-analyzer: 可视化 Webpack 输出文件的体积 (业务组件、依赖第三方模块)

提高效率的插件?

  • webpack-dashboard:可以更友好的展示相关打包信息。
  • webpack-merge:提取公共配置,减少重复配置代码
  • speed-measure-webpack-plugin:简称 SMP,分析出 Webpack 打包过程中 Loader 和 Plugin 的耗时,有助于找到构建过程中的性能瓶颈。
  • size-plugin:监控资源体积变化,尽早发现问题
  • HotModuleReplacementPlugin:模块热替换

loader vs plugin

对于loader,它是一个转换器,将A文件进行编译形成B文件,这里操作的是文件,比如将A.scss转换为A.css,单纯的文件转换过程;

plugin是一个扩展器,它丰富了webpack本身,针对是loader结束后,webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听webpack打包过程中的某些节点,执行广泛的任务;

source map是什么?生产环境怎么用?

source map 是将编译、打包、压缩后的代码映射回源代码的过程。打包压缩后的代码不具备良好的可读性,想要调试源码就需要 soucre map。

map文件只要不打开开发者工具,浏览器是不会加载的。

线上环境一般有三种处理方案:

  • hidden-source-map:借助第三方错误监控平台 Sentry 使用
  • nosources-source-map:只会显示具体行数以及查看源代码的错误栈。安全性比 sourcemap 高
  • sourcemap:通过 nginx 设置将 .map 文件只对白名单开放(公司内网)

注意:避免在生产中使用 inline-eval-,因为它们会增加 bundle 体积大小,并降低整体性能。

构建流程

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

  • 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
  • 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
  • 确定入口:根据配置中的 entry 找出所有的入口文件
  • 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  • 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系
  • 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  • 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

简单说

  • 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler
  • 编译:从 Entry 出发,针对每个 Module 串行调用对应的 Loader 去翻译文件的内容,再找到该 Module 依赖的 Module,递归地进行编译处理
  • 输出:将编译后的 Module 组合成 Chunk,将 Chunk 转换成文件,输出到文件系统中

webpack treeShaking原理

Tree shaking的本质是消除无用的JavaScript代码。 因为ES6模块的出现,ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析, 这就是Tree shaking的基础。

通过将 mode 选项设置为 production,默认启用 minification(代码压缩) 和 tree shaking。

cdn配置

module.exports = {
  //...
  externals: {
    jquery: 'jQuery',
    moment: {
			commonjs: 'moment', //如果我们的库运行在Node.js环境中,import moment from 'moment'等价于const moment = require('moment')
			commonjs2: 'moment', //同上
			amd: 'moment', //如果我们的库使用require.js等加载,等价于 define(["moment"], factory);
			root: 'moment' //如果我们的库在浏览器中使用,需要提供一个全局的变量‘moment’,等价于 var moment = (window.moment) or (moment);
    }
  }
};
  • root:可以通过一个全局变量访问 library(例如,通过 script 标签)。
  • commonjs:可以将 library 作为一个 CommonJS 模块访问。
  • commonjs2:和上面的类似,但导出的是 module.exports.default.
  • amd:类似于 commonjs,但使用 AMD 模块系统。
<script
  src="https://code.jquery.com/jquery-3.1.0.js"
  integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
  crossorigin="anonymous">
</script>

配置html-webpack-plugin动态引入script

  ...省略
  // 插件选项
  plugins: [
    // html模板、以及相关配置
    new HtmlWebpackPlugin({
      title: 'Lesson-06',
      template: resolve('../public/index.html'),
      // cdn(自定义属性)加载的资源,不需要手动添加至index.html中,
      // 顺序按数组索引加载
      cdn: {
        css:['https://cdn.bootcss.com/element-ui/2.8.2/theme-chalk/index.css'],
        js: [
          'https://cdn.bootcss.com/vue/2.6.10/vue.min.js',
          'https://cdn.bootcss.com/element-ui/2.8.2/index.js'
        ]
      }
    })
  ]
  ...省略
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title><%= htmlWebpackPlugin.options.title %></title>
	<!-- import cdn css -->
	<% if(htmlWebpackPlugin.options.cdn) {%>
		<% for(var css of htmlWebpackPlugin.options.cdn.css) { %>
			<link rel="stylesheet" href="<%=css%>">
		<% } %>
	<% } %>
</head>
<body>
	<div id="box"></div>
	<!-- import cdn js -->
	<% if(htmlWebpackPlugin.options.cdn) {%>
		<% for(var js of htmlWebpackPlugin.options.cdn.js) { %>
			<script src="<%=js%>"></script>
		<% } %>
	<% } %>
</body>
</html>

这样配置的话 webpack 在 dev 运行或 build 打包时,就不会去本地组件包中查找这些在 externals 中注册的组件了(自然也不会将他们打包到一个 app.js 中去),而是会去 window 域下直接调用。

vendor vs manifest vs appjs

vendor.js: 默认将node_modules里require的依赖都打包在这里

manifest.js: 在vendor.js的基础上,主要将一些异步加载等打包在这里

app.js: 默认将所有你自己写的js打包在这里

const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const nodeExternals = require('webpack-node-externals');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const WebpackPugManifestPlugin = require('./src/Utilities/WebpackPugManifestPlugin');

const clientConfig = {
  target: 'web',
  resolve: {
    extensions: ['.js', '.jsx']
  },
  module: {
    rules: [
      {
        test: /\.js|jsx$/,
        include: path.resolve(__dirname, 'src'),
        loader: 'babel-loader',
        options: {
          plugins: ['react-hot-loader/babel']
        }
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(['dist']),
    new CopyWebpackPlugin([
      { from: path.resolve(__dirname, 'src/public'), to: path.resolve(__dirname, 'dist/public') },
      { from: path.resolve(__dirname, 'src/views'), to: path.resolve(__dirname, 'dist/views') }
    ]),
    new WebpackPugManifestPlugin({
      filter: asset => /(vendor|runtime|client).+js$/.test(asset),
      filename: 'views/manifest.pug'
    })
  ],
  optimization: {
    splitChunks: {
      cacheGroups: {
        commons: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          chunks: 'all'
        }
      }
    },
    runtimeChunk: 'single'
  }
};

const serverConfig = {
  target: 'node',
  node: {
    __dirname: true,
    __filename: true
  },
  plugins: [new CleanWebpackPlugin(['dist'])],
  externals: [nodeExternals()]
};

module.exports = { clientConfig, serverConfig };

file-loader vs url-loader

file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件 (处理图片和字体)

url-loader:与 file-loader 类似,区别是用户可以设置一个阈值,大于阈值会交给 file-loader 处理,小于阈值时返回文件 base64 形式编码 (处理图片和字体)

首先,将图片转换成base64位的图片格式,可以减少网站的http请求;但是,如果同一张图片在网页中引用了多次,每一次都转换成base64位格式,这会造成JS的损耗,每次转换要消耗资源的,那就不如通过http请求,缓存下来,这样就可以节省JS性能。

在实际工程中,配置文件上百行乃是常事,如何保证各个loader按照预想方式工作?

可以使用 enforce 强制执行 loader 的作用顺序,pre 代表在所有正常 loader 之前执行,post 是所有 loader 之后执行。(inline 官方不推荐使用)

Gulp

gulpjs是一个前端构建工具,与gruntjs相比,gulpjs无需写一大堆繁杂的配置参数,API也非常简单,学习起来很容易,而且gulpjs使用的是nodejs中stream来读取和操作数据,其速度更快。

主要API:gulp.task(),gulp.src(),gulp.dest(),gulp.watch()

const gulp = require('gulp');
const browserify = require('browserify');
const source = require('vinyl-source-stream');
const buffer = require('vinyl-buffer');
const babelify = require('babelify');
const del = require('del');
const plugins = require('gulp-load-plugins')();

const BUILDDIR = './dist/';

function uglifyJsWhenNeeded(stream) {
  if (process.env.NODE_ENV === 'production') {
    return stream.pipe(plugins.uglify());
  }
  return stream;
}

function sourceMapWhenNeeded(stream) {
  if (process.env.NODE_ENV !== 'production') {
    return stream
      .pipe(plugins.sourcemaps.init({ loadMaps: true }))
      .pipe(plugins.sourcemaps.write('.'));
  }
  return stream;
}

gulp.task('manifest', function() {
  const content = "script(src='client.js')";
  const filename = 'manifest.pug';
  let stream = source(filename);
  stream.end(content);
  return stream.pipe(gulp.dest(BUILDDIR + 'views/'));
});

gulp.task('clean', function() {
  return del([BUILDDIR]);
});

gulp.task('copyStaticFiles', function() {
  return gulp
    .src(['./src/views/*.pug', './src/public/**'], { base: './src/' })
    .pipe(gulp.dest(BUILDDIR));
});

gulp.task('compileLog', function() {
  let stream = gulp.src(['./src/Log/**'], { base: './src/' }).pipe(plugins.babel());
  stream = uglifyJsWhenNeeded(stream);
  stream = sourceMapWhenNeeded(stream);
  return stream.pipe(gulp.dest(BUILDDIR));
});

gulp.task('server', function() {
  let stream = gulp
    .src(['./src/index.js'], { base: './src/' })
    .pipe(plugins.babel())
    .pipe(plugins.rename('main.js'));
  stream = uglifyJsWhenNeeded(stream);
  stream = sourceMapWhenNeeded(stream);
  return stream.pipe(gulp.dest(BUILDDIR));
});

gulp.task('client', function() {
  let b = browserify({
    entries: './src/client.js',
    debug: process.env.NODE_ENV !== 'production', // Used for sourcemaps.
    extensions: ['.jsx']
  });
  let stream = b
    .transform(babelify)
    .bundle()
    .pipe(source('client.js'))
    .pipe(buffer());
  stream = uglifyJsWhenNeeded(stream);
  stream = sourceMapWhenNeeded(stream);
  return stream.pipe(gulp.dest(BUILDDIR));
});

gulp.task('nodemon', function(done) {
  var stream = plugins.nodemon({
    script: BUILDDIR + 'main.js',
    ext: 'js jsx pug',
    ignore: [BUILDDIR],
    env: { NODE_ENV: 'development' },
    tasks: ['build'],
    done: done
  });

  stream
    .on('restart', function() {
      console.log('restarted!');
    })
    .on('crash', function() {
      console.error('Application has crashed!\n');
      stream.emit('quit');
    });
});

gulp.task('build', gulp.parallel('client', 'server', 'compileLog'));

gulp.task('set-dev-node-env', async function() {
  await (process.env.NODE_ENV = 'development');
});

gulp.task('set-prod-node-env', async function() {
  await (process.env.NODE_ENV = 'production');
});

gulp.task(
  'default',
  gulp.series(
    gulp.parallel('set-dev-node-env', 'clean'),
    gulp.parallel('copyStaticFiles', 'build', 'manifest'),
    'nodemon'
  )
);

gulp.task(
  'build_prod',
  gulp.series(
    gulp.parallel('set-prod-node-env', 'clean'),
    gulp.parallel('copyStaticFiles', 'build', 'manifest')
  )
);

Webpack vs gulp

虽然都是前端自动化构建工具,但看他们的定位就知道不是对等的。 gulp严格上讲,模块化不是他强调的东西,他旨在规范前端开发流程。 webpack更是明显强调模块化开发,而那些文件压缩合并、预处理等功能,不过是他附带的功能。

Gulp Webpack
定位 基于流的自动化构建工具 一个万能模块打包器
目标 自动化和优化开发工作流,为通用 website 开发而生 通用模块打包加载器,为移动端大型 SPA 应用而生
学习难度 易于学习,易于使用,api方法少 有大量新的概念和api,不过好在有详尽的官方文档
适用场景 基于流的作业方式适合多页面应用开发 一切皆模块的特点适合单页面应用开发
作业方式 对输入(gulp.src)的 js,ts,scss,less 等源文件依次执行打包(bundle)、编译(compile)、压缩、重命名等处理后输出(gulp.dest)到指定目录中去,为了构建而打包 对入口文件(entry)递归解析生成依赖关系图,然后将所有依赖打包在一起,在打包之前会将所有依赖转译成可打包的 js 模块,为了打包而构建
使用方式 常规 js 开发,编写一系列构建任务(task)。 编辑各种 JSON 配置项

Babel

如何把 ES6 转成 ES5 呢,其大致分为三步:

  • 将代码字符串解析成抽象语法树,即所谓的 AST
  • 对 AST 进行处理,在这个阶段可以对 ES6 代码进行相应转换,即转成 ES5 代码
  • 根据处理后的 AST 再生成代码字符串

参考引用:

juejin.cn/post/684490… 「吐血整理」再来一打Webpack面试题