最近朋友圈被《头号玩家》刷爆了,斯皮尔伯格一个资深电影导演,把对过去经典的致敬,对未来的憧憬浓缩在这一部电影中,可以说让观众燃了起来。

观望整个前端开发,不断的演化,发展迅速。前端开发从最开始切页面, 前端自动化构建工具日新月异,从最初的Grunt,Gulp到现在前端项目可以说标配的webpack。

我们先来致敬经典:

1. 什么是webpack?

可以看做一个模块化打包机,分析项目结构,处理模块化依赖,转换成为浏览器可运行的代码。

  • 代码转换: TypeScript 编译成 JavaScript、SCSS,LESS 编译成 CSS.
  • 文件优化:压缩 JavaScript、CSS、HTML 代码,压缩合并图片。
  • 代码分割:提取多个页面的公共代码、提取首屏不需要执行部分的代码让其异步加载。
  • 模块合并:在采用模块化的项目里会有很多个模块和文件,需要构建功能把模块分类合并成一个文件。
  • 自动刷新:监听本地源代码的变化,自动重新构建、刷新浏览器。

构建把一系列前端代码自动化去处理复杂的流程,解放生产力。

2. 进入webpack世界

初始化项目

    npm install webpack webpack-cli -D

webpack4抽离出了webpack-cli,所以我们需要下载2个依赖。

Webpack 启动后会从Entry里配置的Module开始递归解析 Entry 依赖的所有 Module。 每找到一个 Module, 就会根据配置的Loader去找出对应的转换规则,对 Module 进行转换后,再解析出当前 Module 依赖的 Module。 这些模块会以 Entry 为单位进行分组,一个 Entry 和其所有依赖的 Module 被分到一个组也就是一个 Chunk。最后 Webpack 会把所有 Chunk 转换成文件输出。 在整个流程中 Webpack 会在恰当的时机执行 Plugin 里定义的逻辑。

webpack需要在项目根目录下创建一个webpack.config.js来导出webpack的配置,配置多样化,可以自行定制,下面讲讲最基础的配置。

module.exports = {
entry: './src/index.js',
output: {
path: path.join(__dirname, './dist'),
filename: 'main.js',
}
}
  • entry代表入口,webpack会找到该文件进行解析
  • output代表输入文件配置
  • path把最终输出的文件放在哪里
  • filename输出文件的名字

有时候我们的项目并不是spa,需要生成多个js html,那么我们就需要配置多入口。

module.exports = {
entry: {
pageA: './src/pageA.js',
pageB: './src/pageB.js'
},
output: {
path: path.join(__dirname, './dist'),
filename: '[name].[hash:8].js',
},
}

entry配置一个对象,key值就是chunk: 代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。看看filename[name]: 这个name指的就是chunk的名字,我们配置的key值pageA pageB,这样打包出来的文件名是不同的,再来看看[hash],这个是给输出文件一个hash值,避免缓存,那么:8是取前8位。

这里有人会有疑问了,项目是多页面的,应该有pageA.html``pageA.js``pageA.css, 那么我应该生成多个html,这个只是做了JS的入口区分,我不想每一个页面都去复制粘贴一个html,并且html是大部分重复的,可能不同页面只需要修改title,下面来看看这个问题怎么解决:

需要引入一个webpack的plugin:

npm install html-webpack-plugin -D

该插件可以给每一个chunk生成html,指定一个template,可以接收参数,在模板里面使用,下面来看看如何使用:

const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
entry: {
pageA: './src/pageA.js',
pageB: './src/pageB.js'
},
output: {
path: path.join(__dirname, './dist'),
filename: '[name].[hash:8].js',
},
plugins: [
new HtmlWebpackPlugin({
template: './src/templet.html',
filename: 'pageA.html',
title: 'pageA',
chunks: ['pageA'],
hash: true,
minify: {
removeAttributeQuotes: true
}
}),
new HtmlWebpackPlugin({
template: './src/templet.html',
filename: 'pageB.html',
title: 'pageB',
chunks: ['pageB'],
hash: true,
minify: {
removeAttributeQuotes: true
}
}),
]
}

在webpack中,插件的引入顺序没有规定,这个在后面在继续详说。

  • template: html模板的路径地址
  • filename: 生成的文件名
  • title: 传入的参数
  • chunks: 需要引入的chunk
  • hash: 在引入JS里面加入hash值 比如: <script src='index.js?2f373be992fc073e2ef5'></script>
  • removeAttributeQuotes: 去掉引号,减少文件大小<script src=index.js></script>
  • 具体文档

这样在dist目录下就生成了pageA.html和pageB.html并且通过配置chunks,让pageA.html里加上了script标签去引入pageA.js。那么现在还剩下css没有导入,css需要借助loader去做,所以现在要下载几个依赖,以scss为例,less同理

npm install css-loader style-loader sass-loader node-sass -D
  • css-loader: 支持css中的import
  • style-loader: 把css写入style内嵌标签
  • sass-loader: scss转换为css
  • node-sass: scss转换依赖

来看看如何配置loader

module.exports = {
module: {
rules: [
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
exclude: /node_modules/
}
]
}
}
  • test: 一个正则表达式,匹配文件名
  • use: 一个数组,里面放需要执行的loader,倒序执行,从右至左。
  • exclude: 取消匹配node_modules里面的文件

如果想把css作为一个单独的文件,需要用到一个插件来做(webpack4.0.0以上版本需要next版本):

 npm i extract-text-webpack-plugin@next -D
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.join(__dirname, './dist'),
filename: 'main.js',
},
module: {
rules: [
{
test: /\.scss$/,
use: ExtractTextPlugin.extract({
// style-loader 把css直接写入html中style标签
fallback: 'style-loader',
// css-loader css中import支持
// loader执行顺序 从右往左执行
use: ['css-loader', 'sass-loader']
}),
exclude: /node_modules/
}
]
},
plugins: [
new ExtractTextPlugin('[name].[contenthash:8].css'),
]
}
  • 需要在plugins里加入插件name: chunk名字 contenthash:8: 根据内容生成hash值取前8位
  • 修改loader配置下的use: fallback: 兼容方案

这样就实现了js,html,css的打包,那么再来看看一些常用的loader:

  • babel-loader: 用babel转换代码
  • url-loader: 依赖于file-loader,把图片转换成base64嵌入html,如果超出一定阈值则交给file-loader
rules: [
// 处理js
{
test: /\.js?$/,
exclude: /node_modules/,
use: ['babel-loader']
},
// 处理图片
{
test: /\.(png|jpg|gif|ttf|eot|woff(2)?)(\?[=a-z0-9]+)?$/,
use: [{
loader: 'url-loader',
options: {
query: {
// 阈值 单位byte
limit: '8192',
name: 'images/[name]_[hash:7].[ext]',
}
}
}]
},
]

babel的配置建议在根目录下新建一个.babelrc文件

{
"presets": [
"env",
"stage-0",
"react"
],
"plugins": [
"transform-runtime",
"transform-decorators-legacy",
"add-module-exports"
]
}
  • presets: 预设, 一个预设包含多个插件 起到方便作用 不用引用多个插件
  • env: 只转换新的句法,例如const let => ..等 不转换 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise、Object.assign。
  • stage-0: es7提案转码规则 有 0 1 2 3 阶段 0包含 1 2 3里面的所有
  • react: 转换react jsx语法
  • plugins: 插件 可以自己开发插件 转换代码(依赖于ast抽象语法数)
  • transform-runtime: 转换新语法,自动引入polyfill插件,另外可以避免污染全局变量
  • transform-decorators-legacy: 支持装饰器
  • add-module-exports: 转译export default {}; 添加上module.exports = exports.default 支持commonjs

因为我们在文件名中加入hash值,打包多次后dist目录变得非常多文件,没有删除或覆盖,这里可以引入一个插件,在打包前自动删除dist目录,保证dist目录下是当前打包后的文件:

plugins: [
new CleanWebpackPlugin(
// 需要删除的文件夹或文件
[path.join(__dirname, './dist/*.*')],
{
// root目录
root: path.join(__dirname, './')
}
),
]

指定extension之后可以不用在require或是import的时候加文件扩展名,会依次尝试添加扩展名进行匹配:

resolve: {
extensions: ['.js', '.jsx', '.scss', '.json'],
},

3. 优化实战 高级装备

天下武功唯快不破,优化方案千千万万,各取所需吧。

提出公共的JS文件

webpack4中废弃了webpack.optimize.CommonsChunkPlugin插件,用新的配置项替代

module.exports = {
entry: './src/index.js',
output: {
path: path.join(__dirname, './dist'),
filename: 'main.js',
},
optimization: {
splitChunks: {
cacheGroups: {
commons: {
chunks: 'initial',
minChunks: 2,
maxInitialRequests: 5,
minSize: 2,
name: 'common'
}
}
}
},
}

把多次import的文件打包成一个单独的common.js

HappyPack

在webpack运行在node中打包的时候是单线程去一件一件事情的做,HappyPack可以开启多个子进程去并发执行,子进程处理完后把结果交给主进程

 npm i happypack -D

需要改造一下loader配置,此loader用子进程去处理

const HappyPack = require('happypack');
module.exports = {
entry: './src/index.js',
output: {
path: path.join(__dirname, './dist'),
filename: 'main.js',
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: 'happypack/loader?id=babel',
},
]
},
plugins: [
new HappyPack({
id: 'babel',
threads: 4,
loaders: ['babel-loader']
}),
]
}
  • id: id值,与loader配置项对应
  • threads: 配置多少个子进程
  • loaders: 用什么loader处理
  • 具体文档

作用域提升

如果你的项目是用ES2015的模块语法,并且webpack3+,那么建议启用这一插件,把所有的模块放到一个函数里,减少了函数声明,文件体积变小,函数作用域变少。

module.exports = {
entry: './src/index.js',
output: {
path: path.join(__dirname, './dist'),
filename: 'main.js',
},
plugins: [
new webpack.optimize.ModuleConcatenationPlugin(),
]
}

提取第三方库

方便长期缓存第三方的库,新建一个入口,把第三方库作为一个chunk,生成vendor.js

module.exports = {
entry: {
main: './src/index.js',
vendor: ['react', 'react-dom'],
},
}

DLL动态链接

第三库不是经常更新,打包的时候希望分开打包,来提升打包速度。打包dll需要新建一个webpack配置文件,在打包dll的时候,webpack做一个索引,写在manifest文件中。然后打包项目文件时只需要读取manifest文件。

webpack.vendor.js

const webpack = require('webpack');
const path = require('path'); module.exports = {
entry: {
vendor: ['react', 'react-dom'],
},
output: {
path: path.join(__dirname, './dist'),
filename: 'dll/[name]_dll.js',
library: '_dll_[name]',
},
plugins: [
new webpack.DllPlugin({
path: path.join(__dirname, './dist/dll', 'manifest.json'),
name: '_dll_[name]',
}),
]
};

path: manifest文件的输出路径 name: dll暴露的对象名,要跟output.library保持一致 context: 解析包路径的上下文,这个要跟接下来配置的dll user一致

webpack.config.js

module.exports = {
entry: {
main: './src/index.js',
vendor: ['react', 'react-dom'],
},
plugins: [
new webpack.DllReferencePlugin({
manifest: path.join(__dirname, './dist/dll', 'manifest.json')
})
]
}

html

<script src="vendor_dll.js"></script>

4. 线上和线下

在生产环境和开发环境其实我们的配置是存在相同点,和不同点的,为了处理这个问题,会创建3个文件:

  • webpack.base.js: 共同的配置
  • webpack.dev.js: 在开发环境下的配置
  • webpack.prod.js: 在生产环境的配置

通过webpack-merge去做配置的合并,比如:

开发环境

const path = require('path');
const webpack = require('webpack');
const merge = require('webpack-merge');
const base = require('./webpack.base'); const dev = {
devServer: {
contentBase: path.join(__dirname, '../dist'),
port: 8080,
host: 'localhost',
overlay: true,
compress: true,
open:true,
hot: true,
inline: true,
progress: true,
},
devtool: 'inline-source-map',
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(),
]
}
module.exports = merge(base, dev);

开发环境中我们可以启动一个devServer静态文件服务器,预览我们的项目,引入base配置文件,用merge去合并配置。

  • contentBase: 静态文件地址
  • port: 端口号
  • host: 主机
  • overlay: 如果出错,则在浏览器中显示出错误
  • compress: 服务器返回浏览器的时候是否启动gzip压缩
  • open: 打包完成自动打开浏览器
  • hot: 模块热替换 需要webpack.HotModuleReplacementPlugin插件
  • inline: 实时构建
  • progress: 显示打包进度
  • devtool: 生成代码映射,查看编译前代码,利于找bug
  • webpack.NamedModulesPlugin: 显示模块的相对路径

生产环境

再来看看生产环境最重要的代码压缩,混淆:

const path = require('path');
const merge = require('webpack-merge');
const WebpackParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
const base = require('./webpack.base'); const prod = {
plugins: [
// 文档: https://github.com/gdborton/webpack-parallel-uglify-plugin
new WebpackParallelUglifyPlugin(
{
uglifyJS: {
mangle: false,
output: {
beautify: false,
comments: false
},
compress: {
warnings: false,
drop_console: true,
collapse_vars: true,
reduce_vars: true
}
}
}
),
]
}
module.exports = merge(base, prod);

webpack-parallel-uglify-plugin可以并行压缩代码,提升打包效率

uglifyJS配置:

  • mangle: 是否混淆代码
  • output.beautify: 代码压缩成一行 true为不压缩 false压缩
  • output.comments: 去掉注释
  • compress.warnings: 在删除没用到代码时 不输出警告
  • compress.drop_console: 删除console
  • compress.collapse_vars: 把定义一次的变量,直接使用,取消定义变量
  • compress.reduce_vars: 合并多次用到的值,定义成变量
  • 具体文档

5. 成为头号玩家

想要成为头号玩家,玩转配置可不行,当然还要做一些loader和plugin的开发,去为项目做一些优化,解决痛点。

loader

loader是一个模块导出函数,在正则匹配成功的时候调用,webpack把文件数组传入进来,在this上下文可以访问loader API

  • this.context: 当前处理文件的所在目录,假如当前 Loader 处理的文件是 /src/main.js,则 this.context 就等于 /src。
  • this.resource: 当前处理文件的完整请求路径,包括 querystring,例如 /src/main.js?name=1。
  • this.resourcePath: 当前处理文件的路径,例如 /src/main.js。
  • this.resourceQuery: 当前处理文件的 querystring。
  • this.target: 等于 Webpack 配置中的 Target
  • this.loadModule: 但 Loader 在处理一个文件时,如果依赖其它文件的处理结果才能得出当前文件的结果时, 就可以通过 - - - this.loadModule(request: string, callback: function(err, source, sourceMap, module)) 去获得 request 对应文件的处理结果。
  • this.resolve: 像 require 语句一样获得指定文件的完整路径,使用方法为 resolve(context: string, request: string, callback: function(err, result: string))。
  • this.addDependency: 给当前处理文件添加其依赖的文件,以便再其依赖的文件发生变化时,会重新调用 Loader 处理该文件。使用方法为 addDependency(file: string)。
  • this.addContextDependency: 和 addDependency 类似,但 addContextDependency 是把整个目录加入到当前正在处理文件的依赖中。使用方法为 addContextDependency(directory: string)。
  • this.clearDependencies: 清除当前正在处理文件的所有依赖,使用方法为 clearDependencies()。
  • this.emitFile: 输出一个文件,使用方法为 emitFile(name: string, content: Buffer|string, sourceMap: {...})。
  • this.async: 返回一个回调函数,用于异步执行。

下面来看看less-loaderstyle-loader如何实现:

let less = require('less');
module.exports = function (source) {
const callback = this.async();
less.render(source, (err, result) => {
callback(err, result.css);
});
}
module.exports = function (source) {
let script = (`
let style = document.createElement("style");
style.innerText = ${JSON.stringify(source)};
document.head.appendChild(style);
`);
return script;
}

plugin

webpack整个构建流程有许多钩子,开发者可以在指定的阶段加入自己的行为到webpack构建流程中。插件由以下构成:

  • 一个 JavaScript 命名函数。
  • 在插件函数的 prototype 上定义一个 apply 方法。
  • 指定一个绑定到 webpack 自身的事件钩子。
  • 处理 webpack 内部实例的特定数据。
  • 功能完成后调用 webpack 提供的回调。

整个webpack流程由compiler和compilation构成,compiler只会创建一次,compilation如果开起了watch文件变化,那么会多次生成compilation. 那么这2个类下面生成了需要事件钩子

compiler hooks 文档 compilation hooks 文档

写一个小插件,生成所有打包的文件列表(webpack4不推荐使用compiler.plugin来注册插件,webpack5将不支持):

class FileListPlugin{
constructor(options) {
this.options = options;
}
apply(compiler) {
compiler.hooks.emit.tap('FileListPlugin',function (compilation) {
let fileList = 'filelist:\n\n';
for (let filename in compilation.assets) {
fileList += ('- '+filename+'\n');
}
compilation.assets['filelist.md']={
source() {
return fileList;
},
size() {
return fileList.length
}
}
});
}
}
module.exports = FileListPlugin;

6. 最后

都读在这里了,还不点个赞吗。

作者:lihuanji
链接:https://juejin.im/post/5ac9dc9af265da23884d5543
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

带你走进webpack世界,成为webpack头号玩家。的更多相关文章

  1. 小丁带你走进git世界一-git简单配置

    小丁带你走进git世界一-git简单配置 1.github的简单配置 配置提交代码的信息,例如是谁提交的代码之类的. git config  –global user.name BattleHeaer ...

  2. 小丁带你走进git世界五-远程仓库

    一.文件,指令讲解 首先讲一下远程仓库和本地仓库在文件上面的区别,首先我们来看下对比图(当然这里说的区别是在于.git文件下面的文件内容,至于里面内容我们不会关注)這裡我们进行了相同的操作就是本地仓库 ...

  3. 带你走进脚本世界,ijkplayer之【init-ios.sh】脚本分析

    前言 集成ijkplayer,需要执行脚本init-ios.sh,那么init-ios.sh脚本干嘛用的了,花了半天时间,学习了下shell脚本,感觉脚本语言学起来还是比较容易上手的,现在仅仅能看懂了 ...

  4. 小丁带你走进git的世界三-撤销修改

    一.撤销指令 git checkout还原工作区的功能 git reset  还原暂存区的功能 git clean  还没有被添加进暂存区的文件也就是git还没有跟踪的文件可以使用这个命令清除他们 g ...

  5. 小丁带你走进git的世界三-撤销修改(转)

    一.撤销指令 git checkout还原工作区的功能 git reset  还原暂存区的功能 git clean  还没有被添加进暂存区的文件也就是git还没有跟踪的文件可以使用这个命令清除他们 g ...

  6. 《头号玩家》AI电影调研报告(二)

    四. 涉及前沿技术及与现实的交互 1.VR技术 在影片中,斯皮尔伯格用他认为未来的VR虚拟技术为我们创造了众多精彩的画面,令人佩服其对科技的预见性.其中好多的装备特别引人注目,部分也在现实中存在:VR ...

  7. 小丁带你走进git的世界二-工作区暂存区分支

    小丁带你走进git的世界二-工作区暂存区分支 一.Git基本工作流程 1.初始化一个仓库 git  init git  clone git仓库分为两种情况: 第一种是在现有项目或目录下导入所有文件到 ...

  8. 带你走进rsync的世界

    导读 Rsync(remote synchronize)是一个远程数据同步工具,可通过LAN/WAN快速同步多台主机间的文件,也可以使用 Rsync 同步本地硬盘中的不同目录.rsync共有3种使用方 ...

  9. Miox带你走进动态路由的世界——51信用卡前端团队

    写在前面: 有的时候再做大型项目的时候,确实会被复杂的路由逻辑所烦恼,会经常遇到权限问题,路由跳转回退逻辑问题.这几天在网上看到了51信用卡团队开源了一个Miox,可以有效的解决这些痛点,于是乎我就做 ...

随机推荐

  1. Python新建文件夹

    import os os.mkdir('OS-Demo-2') os.makedirs('OS-Demo-3/sub-Dir-1') os.mkdir()和os.makedirs()都可以新建文件夹, ...

  2. 【leetcode】388. Longest Absolute File Path

    题目如下: Suppose we abstract our file system by a string in the following manner: The string "dir\ ...

  3. Cross platform

    值得学习的C/C++语言开源项目 (1)ACE 庞大.复杂,适合大型项目.开源.免费,不依赖第三方库,支持跨平台. http://www.cs.wustl.edu/~schmidt/ACE.html ...

  4. 傻瓜教程--------------linix上安装jdk

    大致步骤可分为3步,下面会详细解释 1:Jdk 的下载 2: jdk的解压 3: 环境变量的配置 一:jdk的下载 1官网下载jdk https://www.oracle.com/technetwor ...

  5. mysql查询相关的命令解析

    特:不重启mysql 更新配置文件方法(不允许重启mysql实例或连接不上msyql服务器): gdb -p $(pidof mysqld) -ex "set max_connections ...

  6. es批量索引

    使用Python操作Elasticsearch数据索引的教程 这篇文章主要介绍了使用Python操作Elasticsearch数据索引的教程,Elasticsearch处理数据索引非常高效,要的朋友可 ...

  7. JAVA中 成员变量和和实例变量区别

    java语言支持的变量类型 类变量:独立于方法之外的变量,用 static 修饰. 局部变量:类的方法中的变量. 实例变量(全局变量):独立于方法之外的变量,不过没有 static 修饰. publi ...

  8. atlcomcli.h(1756): error C2338: CVarTypeInfo< char > cannot be compiled with /J or _CHAR_UNSIGNED fl

    我拿到一个VS的工程,用VS2010 编译 时提示: atlcomcli.h(1756): error C2338: CVarTypeInfo< char > cannot be comp ...

  9. Linux权限简析

  10. vue组件通信之父组件主动获取子组件数据和方法

    ref 可以用来获取到dom节点,如果在组件中应用,也可以用来获取子组件的数据和方法. 比如,我定义了一个home组件,一个head组件,home组件中引用head组件. 此时,home组件是head ...