Webpack简介
Webpack 概述
https://finget.github.io/2018/02/08/webpack/本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
Webpack官网
Webpack 的版本更迭
- Webpack v1.0.0 — 2014.2.20
- Webpack v2.2.0 — 2017.1.18
- Webpack v3.0.0 — 2017.6.19
Webpack 功能进化
- Webpack V1
- 编译、打包
- HMR(模块热更新)
- 代码分割
- 文件处理(loader、plugin)
- Webpack V2
- Tree Shaking(在项目中没有实际运用的代码会被删除,打包体积更小)
- ES module
- 动态Import
- Webpack V3
- Scope Hoisting(作用域提升)
- Magic Comments(配合动态import使用)
核心概念
Entry
代码的入口,打包入口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// 一个入口 module.exports = { entry: 'index.js' }; // 推荐写法 module.exports = { entry: { index: 'index.js' } }; // 多个入口 module.exports = { entry: { pageOne: './src/pageOne/index.js', pageTwo: './src/pageTwo/index.js', pageThree: './src/pageThree/index.js' } } |
Output
1 2 3 4 5 6 7 8 9 10 |
{ entry: { app: './src/app.js', search: './src/search.js' }, output: { filename: '[name].js', // 打包之后的文件名 [name]就对应entry里面的key值。 path: __dirname + '/dist' // 打包输出文件路径 } } |
Loaders
1 2 3 4 5 6 7 8 9 |
// 单个loader module.exports = { module: { rules: [ { test: /\.css$/, use: 'css-loader' }, { test: /\.ts$/, use: 'ts-loader' } ] } }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// 多个loader module.exports = { module: { rules: [ { test: /\.css$/, // 正则匹配css文件 use: [ { loader: 'style-loader' }, { loader: 'css-loader', options: { // loader配置 modules: true } } ] } ] } }; |
Plugins
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
const HtmlWebpackPlugin = require('html-webpack-plugin'); //installed via npm const path = require('path'); const config = { entry: './path/to/my/entry/file.js', output: { filename: 'my-first-webpack.bundle.js', path: path.resolve(__dirname, 'dist') }, module: { rules: [ { test: /\.js$/, use: 'babel-loader' } ] }, plugins: [ new HtmlWebpackPlugin({template: './src/index.html'}) // 根据`./src/index.html`生成一个首页,会引入打包的js、css文件 ] }; module.exports = config; |
使用Webpack
安装Webpack
npm i -g webpack
当然你得先安装nodejs、Git
在命令行输入webpack -h
,成功就出现下图,有很多webpack命令可以看一看
打包js
webpack entry
webpack --config webpack.config.js
第一个栗子
新建一个app.js和sum.js
app.js
1 2 3 4 |
// es module import sum form './sum' console.log(sun(1,2)); |
sum.js
1 2 3 |
export default function (a,b) { return a + b; } |
通过命令行打包:
1 2 |
webpack app.js bundle.js // app.js 是入口文件 bundle.js是打包输出文件 |
编译ES6
需要两个loader:npm i babel-loader babel-core -D
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
module.exports = { entry: { app: 'app.js' }, output: { filename: '[name].[hash:8].js' }, module: { rules: [ { test: /\.js$/, use: 'babel-loader', exclude: '/node_modules/' // 将node_module中的文件排除在外,因为已经是编译过的 } ] } } |
{ test: Condition }
:匹配特定条件。一般是提供一个正则表达式或正则表达式的数组,但这不是强制的。
{ include: Condition }
:匹配特定条件。一般是提供一个字符串或者字符串数组,但这不是强制的。
{ exclude: Condition }
:排除特定条件。一般是提供一个字符串或字符串数组,但这不是强制的。
{ and: [Condition] }
:必须匹配数组中的所有条件
{ or: [Condition] }
:匹配数组中任何一个条件
{ not: [Condition] }
:必须排除这个条件
Babel Presets
虽然引入了babel-loader
,但是它并不知道是根据什么规范来打包的,这个时候就需要配置一个Babel Presets(预设)npm i babel-preset-env -D
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
module.exports = { ... module: { rules: [ { test: /\.js$/, use: { loader: 'babel-loader', options: { // options 属性为字符串或对象。值可以传递到 loader 中,将其理解为 loader 选项。 presets: ['babel-preset-env'] } }, exclude: '/node_modules/' // 将node_module中的文件排除在外,因为已经是编译过的 } ] } } |
Babel Polyfill
npm install --save babel-polyfill
使用babel-polyfillimport 'babel-polyfill'
Babel 默认只转换新的 JavaScript 语法,而不转换新的 API。例如,Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局对象,以及一些定义在全局对象上的方法(比如 Object.assign)都不会转译。如果想使用这些新的对象和方法,必须使用 babel-polyfill,为当前环境提供一个垫片。
Polyfill 垫片:
polyfill
这个英文单词在js babel中的翻译可以说是垫片,本来指的是衣服中的填充物。
在这里可以说是为了使用某个浏览器或者其他执行环境不支持的函数或者对象能够使用而添加的原型方法,或者第三方库。
例如:
我们想要使用es2015的语法中的某些新的对象方法或者数据类型,就需要添加babel-polyfill
,例如Array.from
方法很多浏览器不支持,你就需要垫片来提高兼容性。
为了在版本低浏览器中能够使用promise
,我们需要提前执行一个promise
文件,以便能够在全局中使用。
babel-runtime
npm i --save babel-runtime
Babel 转译后的代码要实现源代码同样的功能需要借助一些帮助函数,例如,{ [name]: ‘JavaScript’ } 转译后的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
;function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } var obj = _defineProperty({}, 'name', 'JavaScript'); |
类似上面的帮助函数 _defineProperty 可能会重复出现在一些模块里,导致编译后的代码体积变大。Babel 为了解决这个问题,提供了单独的包 babel-runtime
供编译模块复用工具函数。
npm i babel-plugin-transform-runtime -D
新建.babelrc
文件,之前是直接将presets设置在loader中的,也可以单独写在.babelrc
文件中,babel会自动读取
1 2 3 4 5 6 |
{ "presets": [ ["babel-preset-env"] ], "plugins": ["transform-runtime"] } |
启用插件 babel-plugin-transform-runtime
后,Babel 就会使用 babel-runtime
下的工具函数,转译代码如下:
1 2 3 4 5 6 |
;// 之前的 _defineProperty 函数已经作为公共模块 `babel-runtime/helpers/defineProperty` 使用 var _defineProperty2 = require('babel-runtime/helpers/defineProperty'); var _defineProperty3 = _interopRequireDefault(_defineProperty2); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var obj = (0, _defineProperty3.default)({}, 'name', 'JavaScript'); |
除此之外,babel 还为源代码的非实例方法(Object.assign,实例方法是类似这样的 “foobar”.includes(“foo”))和 babel-runtime/helps 下的工具函数自动引用了 polyfill。这样可以避免污染全局命名空间,非常适合于 JavaScript 库和工具包的实现。例如 const obj = {}, Object.assign(obj, { age: 30 }); 转译后的代码如下所示:
1 2 3 4 5 6 7 8 9 |
;// 使用了 core-js 提供的 assign var _assign = require('babel-runtime/core-js/object/assign'); var _assign2 = _interopRequireDefault(_assign); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var obj = {}; (0, _assign2.default)(obj, { age: 30 }); |
Typescript
js的超集,可以在typescript中写JavaScript
typescript-loader
官方loadernpm i typescript ts-loader -D
第三方loadernpm i typescript awesome-typescript-loader -D
webpack.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
module.exports = { entry: { 'app': './src/app.ts' }, output: { filename: '[name].bundle.js' }, module: { rules: { test: /\.tsx?$/, use: { loader: 'ts-loader' } } } } |
tsconfig.json
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ "compilerOptions" : { "module": "commonjs", "target": "es5", // 将ts编译成es5语法 "allowJs": true // 是否允许出现js语法 }, "include": { "./src/*" }, "exclude": { "./node_module" } } |
打包公共代码
CommonsChunkPlugin
配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
{ name: string, // or names: string[], // 这是 common chunk 的名称。已经存在的 chunk 可以通过传入一个已存在的 chunk 名称而被选择。 // 如果一个字符串数组被传入,这相当于插件针对每个 chunk 名被多次调用 // 如果该选项被忽略,同时 `options.async` 或者 `options.children` 被设置,所有的 chunk 都会被使用, // 否则 `options.filename` 会用于作为 chunk 名。 // When using `options.async` to create common chunks from other async chunks you must specify an entry-point // chunk name here instead of omitting the `option.name`. filename: string, // common chunk 的文件名模板。可以包含与 `output.filename` 相同的占位符。 // 如果被忽略,原本的文件名不会被修改(通常是 `output.filename` 或者 `output.chunkFilename`)。 // This option is not permitted if you're using `options.async` as well, see below for more details. minChunks: number|Infinity|function(module, count) -> boolean, // 在传入 公共chunk(commons chunk) 之前所需要包含的最少数量的 chunks 。 // 数量必须大于等于2,或者少于等于 chunks的数量 // 传入 `Infinity` 会马上生成 公共chunk,但里面没有模块。 // 你可以传入一个 `function` ,以添加定制的逻辑(默认是 chunk 的数量) chunks: string[], // 通过 chunk name 去选择 chunks 的来源。chunk 必须是 公共chunk 的子模块。 // 如果被忽略,所有的,所有的 入口chunk (entry chunk) 都会被选择。 children: boolean, // 如果设置为 `true`,所有 公共chunk 的子模块都会被选择 deepChildren: boolean, // If `true` all descendants of the commons chunk are selected async: boolean|string, // 如果设置为 `true`,一个异步的 公共chunk 会作为 `options.name` 的子模块,和 `options.chunks` 的兄弟模块被创建。 // 它会与 `options.chunks` 并行被加载。 // Instead of using `option.filename`, it is possible to change the name of the output file by providing // the desired string here instead of `true`. minSize: number, // 在 公共chunk 被创建立之前,所有 公共模块 (common module) 的最少大小。 } |
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var webpack = require('webpack'); var path = require('path'); module.exports = { entry: { 'pageA': './src/pageA', 'pageB': './src/pageB' }, output: { path: path.resolve(__dirname, './dist'), filename: '[name].bundle.js', chunkFilename: '[name].chunk.js' }, plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: 'common', minChunks: 2 }) ] } |
pageA.js
1 2 3 |
import './subPageA'; import './subPageB'; export default 'pageA'; |
pageB.js
1 2 3 |
import './subPageA'; import './subPageB'; export default 'pageB'; |
subPageA.js
1 2 |
import './moudleA'; export default 'subPageA' |
subPageB.js
1 2 |
import './moudleA'; export default 'subPageB' |
moduleA.js
1
|
export default 'moduleA'
|
代码分割 和 懒加载
并不是通过配置webpack实现代码分割和懒加载,而是通过改变写代码的方式
两种实现方法
webpack methods
require.ensure
- []: dependencies
- callback
- errorCallback
- chunkName
require.include
ES 2015 Loader spec
System.import() -> import()
import() -> Promise
import().then()
代码分割场景
- 分离业务代码 和 第三方依赖
- 分离业务代码 和 业务公共代码 和 第三方依赖
- 分离首次加载 和 访问后加载的代码 (优化,首屏加载)
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var webpack = require('webpack'); var path = require('path'); module.exports = { entry: { 'pageA': './src/pageA' }, output: { path: path.resolve(__dirname, './dist'), publicPath: './dist/', filename: '[name].bundle.js', chunkFilename: '[name].chunk.js' } } |
pageA.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import './subPageA'; import './subPageB'; // import * as _ from 'lodash' require.ensure(['lodash'],function(){ // 这一步是引入lodash并不会执行 var _ = require('lodash'); // 这一步就会执行lodash, 异步加载 _.join(['1','2'],'3'); // 可以使用lodash },'vendor') // 'vendor'为 chunk name // 上面的代码也可以写成 require.ensure([],function(){ var _ = require('lodash'); _.join(['1','2'],'3'); },'vendor') export default 'pageA'; |
subPageA.js
1 2 |
import './moudleA'; export default 'subPageA' |
subPageB.js
1 2 |
import './moudleA'; export default 'subPageB' |
moduleA.js
1
|
export default 'moduleA'
|
按条件加载模块
pageA.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// 伪代码 按条件加载模块 if(page === 'subpageA') { require.ensure(['./subPageA'],function(){ var subpageA = require('./subPageA'); },'subPageA') } else if (page === 'subpageB') { require.ensure(['./subPageB'],function(){ var subpageB = require('./subPageB'); },'subPageB') } require.ensure([],function(){ var _ = require('lodash'); _.join(['1','2'],'3'); },'vendor') export default 'pageA'; |
动态import
pageA.js
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 伪代码 按条件加载模块 if(page === 'subpageA') { import(/* webpackChunkName:'subpageA' */'./subPageA').then(function(subPageA){ console.log(subPageA); }) } else if (page === 'subpageB') { import(/* webpackChunkName:'subpageB' */'./subPageB').then(function(subPageB){ console.log(subPageB); }) } export default 'pageA'; |
处理CSS
- css-loader
- style-loader // 在页面中插入style标签
npm i style-loader css-loader
webpack.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
module.exports = { entry: { 'app': './src/app.js' }, output: { filename: '[name].bundle.js' }, module: { rules: { test: /\.css$/, use: [ { loader: 'style-loader' }, { loader: 'css-loader' } ] } } } |
配置Less/Sass
npm i less-loader less --save-dev
npm i sass-loader node-sass --save-dev
webpack.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
module.exports = { entry: { 'app': './src/app.js' }, output: { filename: '[name].bundle.js' }, module: { rules: { test: /\.less$/, use: [ { loader: 'style-loader' }, { loader: 'css-loader' }, { loader: 'less-loader' } ] } } } |
提前CSS
npm i extract-text-webpack-plugin --save-dev
webpack.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
var ExtractTextWebpackPlugin = require('extract-text-webpack-plugin); module.exports = { entry: { 'app': './src/app.js' }, output: { filename: '[name].bundle.js' }, module: { rules: { test: /\.less$/, use: ExtractTextWebpackPlugin.extract({ fallback: { loader: 'style-loader' }, use: [ { loader: 'style-loader' }, { loader: 'css-loader', options:{ minimize: true // 压缩 } }, { loader: 'less-loader' } ] }) } }, plugins:[ new ExtractTextWebpackPlugin({ filename: '[name].min.css', allChunks: false // allChunks默认false,只打包初始化的css,异步加载的css不会打包 }) ] } |
PostCSS in WebPack
PostCSS
A tool for transforming CSS with JavaScript
Autoprefixer
加上浏览器前缀
CSS-nano
压缩css
CSS-next
Use tomorrow’s CSS syntax,today
安装相关插件
npm i postcss postss-loader autoprefixer cssnano postcss-cssnext --save-dev
webpack 配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
var ExtractTextWebpackPlugin = require('extract-text-webpack-plugin'); module.exports = { entry: { 'app': './src/app.js' }, output: { filename: '[name].bundle.js' }, module: { rules: { test: /\.less$/, use: ExtractTextWebpackPlugin.extract({ fallback: { loader: 'style-loader' }, use: [{ loader: 'style-loader' }, { loader: 'css-loader', options: { minimize: true // 压缩 } }, { loader: 'postcss-loader', options: { ident: 'postcss', plugins: [ require('autoprefixer')(), require('postcss-cssnext')() ] } }, { loader: 'less-loader' }] }) }, plugins: [ new ExtractTextWebpackPlugin({ filename: '[name].min.css', allChunks: false // allChunks默认false,只打包初始化的css,异步加载的css不会打包 }) ] } } |
文件处理
图片处理
场景:
- CSS中引入的图片 ——
file-loader
- 自动合成雪碧图 ——
postcss-sprites
- 压缩图片 ——
img-loader
- Base64编码 ——
url-loader
npm i file-loader url-loader img-loader postcss-sprites --save-dev
file-loader
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
module: { rules: [ { test: /\.(png|jpg|gif|jpeg)$/, use: [ { loader: 'file-loader', options: { publicPath: '', outputPath: 'dist/', // 设置输出文件地址 useRelativePath: true } } ] } ] } |
url-loader
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
module: { rules: [{ test: /\.(png|jpg|gif|jpeg)$/, use: [{ loader: 'url-loader', options: { publicPath: '', outputPath: 'dist/', // 设置输出文件地址 useRelativePath: true, limit: 10000 // 10k } }] }] } |
urL-loader
有file-loader
的功能,可以只用url-loader
img-loader
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
module: { rules: [{ test: /\.(png|jpg|gif|jpeg)$/, use: [{ loader: 'url-loader', options: { name: '[name]-[hash:5].min.[ext]', // 5位hash值 publicPath: '', outputPath: 'dist/', // 设置输出文件地址 useRelativePath: true, limit: 10000 // 10k } }, { loader: 'img-loader' }] }] } |
postcss-sprites
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
module: { rules: [{ test: /\.(png|jpg|gif|jpeg)$/, use: [{ loader: 'postcss-loader', options: { ident: 'postcss', plugins: [ require('postcss-sprites')({ spritePath: 'dist/assets/imgs/sprites', retina: true // [email protected] }), require('postcss-cssnext')() ] } }] }] } |
字体文件处理
1 2 3 4 5 6 7 8 9 10 11 12 |
module: { rules: [{ test: /\.(woff|woff2|eot|ttf|otf|svg)$/, use: [{ loader: 'url-loader', options: { limit: 8192, name: 'resource/[name].[ext]' } }] }] } |