本文作者:IMWeb zixinfeng 原文出处:IMWeb社区 未经同意,禁止转载
1. 使用webpack
- webpack命令使用
- webpack --help (webpack -h) 查看所有的命令
- webpack-v
- 打包命令 webpack [] 不适用webpack配置文件的时候
- 使用webpack配置文件, 直接webpack
- --config 指定配置文件 (默认配置文件名称 webpack.config.js或者 webpackfile.js)
- Basic Options
- --entry 指定入口文件
- --watch -w 检测文件系统的变化
- --debug -d 打开调试状态
- --devtool 生成sourceMap
- --progress 进度条显示
- -d 简写 --mode development
- Module Options
- Output Options
- Advanced Options 高级选项
- Resolved Options 解析选项
- Optimization Options 优化选项
- Stats Option 状态选项 (打包出来样式的选项)
- 使用webpack配置(配合node npm使用)
- 不同的配置文件, 开发环境, 生产环境, 测试环境, 打包给第三方使用的
- 第三方的脚手架vue-cli
- 交互式的初始化一个项目
- 项目迁移v1->v2# wepbpack-cli的使用 webpack-cli init webpack-addons-demo # 项目迁移 webpack-cli migrate <config> ## 只会升级配置文件, package.json里面的文件 需要手机升级 2. 直接使用webpack命名, 使用默认文件或者默认配置
// app.js
import sum from './sum'
conosle.log(sum(1,2))
// sum.js
export default function sum(a, b) {
return a b;
}
// 打包命令: webpack app.js --output-path=dist --output-filename=bundle.js --mode development
// 指定配置文件 webpack --config webpack.config.dev.js
3. 编译ES6/7
- babel-loader ## 安装最新版本loader npm install babel-loader@8.0.0-beta @babel/core --save-dev ## 安装最新preset npm install @babel/preset-env --save-dev
- npm install babel-loader babel-core --save-dev
- npm install babel-preset-env --save-dev 指定规范的版本, 只是针对语法
- es2015
- es2016
- es2017
- env 包括2015~2017, 以及latest 用的比较多
- 业内自定义的babel-preset-react
- babel-preset-stage 0 ~3 表示规范组还没有正式发布阶段的
- babel-presets - options - target 当需要编译的时候, 会根据指定的target来选择那些语法进行编译, 那些语法不进行编译
- target.browsers 指定浏览器环境
- target.browsers: 'last 2 versions' 主流浏览器的最后两个版本
- target.browsers: '> 1%' 大于全球浏览器占有率1%的浏览器
- 数据来源是 browserlist中, can i use中 { test: /.js$/, use: { // use: 'babel-loader' //可以直接是一个字符串 loader: 'babel-loader', options: { // 指定preset presets: [['env', { // 告诉babel, 当需要编译的时候, 会根据指定的target来选择那些语法进行编译, 那些语法不进行编译 targets: { browsers: ['> 1%', 'last 2 versions'], // chrome: '52' // 一些新语法浏览器直接支持 不会被转换 } }]] } }, exclude: '/node-modules/' } // 当同时指定'> 1%', 'last 2 versions'的时候, 箭头函数会被转化, const, let等被转化, set不会被转化, num**2 转成了Math.pow // 将targets换成 chrome: '52', 转化后代码基本和原生代码一样
- target.node 指定node环境
- babel-polyfill插件和babel-runtime-transform插件
- 针对一些方法比如数组的map, includes, Set并没有被babel处理, 但是在一些低版本的浏览器中这些方法并没有被实现, 所以需要借助这两个插件
- babel-preset 只是针对语法, 而这两个插件针对函数和方法
- generator
- Set
- Map
- Array.from
- Array.prototype.includes
- 上述方法都没有被babel处理, 所以就需要借助babel的插件进行处理
- babel-polyfill 垫片, 浏览器之间标准实现的方式不一样,保持浏览器之间同样的API
- 全局垫片 (只要引入, 在全局范围内整个浏览器范围内, 可以对实现的API进行调用)
- 相当于对全局变量的一个污染, 为开发应用而准备的 (在业务中使用, 而不是框架比如vue)
- 使用: npm install babel-polyfill --save 真实项目中的依赖 所以是--save
- 在项目中使用 import 'babel-polyfill'
- babel-runtime-transform
- 局部垫片
- 为开发框架而准备的, 不会污染全局变量, 会在局部的方法里面新增加变量和方法
- 优势: 当在代码中使用它的时候, 项目中的其他函数,如果使用es6/7方法, 会将每一个引用到的方法打包到单独的文件中去的; 如果使用了runtime-transform, 将其作为一个独立的整体单独打包进去, 相当于文件之间多余的代码就不会再有了
- npm install babel-plugin-transform-runtime --save-dev
- npm install babel-runtime --save
- .babelrc 在里面配置和babel插件相关的内容 // app.js import sum from './sum' const func = () => { console.log('hello babel') } func() const arr = [1, 2, 3, 4, 5, 4, 3, 2, 1] const arrb = arr.map(item => item * 2) // 下面的语句 不会经过runtime编译 arr.includes(5); // 会经过runtime编译 但是没有exports 使用的时候报错 console.log('SetB', new Set(arrb)) /* function* gen() { yield 1 } */ sum(1, 2) // .babelrc { "presets": [["env", { "targets": { "browsers": ["> 1%", "last 2 versions"] } }]], "plugins": [ "transform-runtime" ] } // 1. 当plugins为空的时候, 上面的代码会完整运行, 都不会被转义 // 2. 添加generator函数的时候, 会报错找不到 regenerator // 3. 添插件的时候 includes不会编译, Set, generator会编译, 但是报错$export is not a function // 4. 屏蔽插件plugins, 使用polyfill, 完美运行所有新属性, 但是打包文件很大, 达到了471Kb
- 实际开发中如何选择
- 如果是应用开发, 只需要配置preset, 如果要使用es6/7新语法, 使用polyfill
- 如果是开发UI库, 框架, 使用runtime
4. 编译TypeScript
- JS的超集 tslang.cn 来自于微软
- 官方推荐: npm install typescript ts-loader --save-dev
- 第三方loader: npm install typescript awesome-typescript-loader --save-dev
- 配置: tsconfig.json## 常用配置选项 compilerOptions:告诉编译器常用的配置选项, 比如 允许js 模块化方式指定:commonjs 指定输出路径等 compilerOptions.module: 模块化方式指定 compilerOptions.target: 编译之后的文件在什么环境下运行的 (类似将语言编译到什么程度) compilerOptions.typeRoots: [ "./node_modules/@type", // 默认安装npm install @types/lodash时路径 "./typings/modules", // 使用typings安装的声明文件路径 ] 指定types声明文件所在的地址 include: 给出一系列的文件路径, 表示需要编译的文件 exclude: 忽略的文件 allowJs: 是否允许js的语法
- 安装声明文件.这样在编译的时候就会给出警告错误, 告诉我们传递的参数类型有错误
- npm install @types/lodash
- npm install @types/vue
- 或者使用typings安装types声明文件, 使用compilerOptions.typeRoots
5. 提取公用代码
- 减少冗余代码(每一个页面都会存在公共代码, 造成带宽浪费)
- 提高用户的加载速度(只加载页面所需要的依赖, 其他页面在加载的时候, 公共代码已经加载好了)
- CommonChunkPlugin (webpack.optimize.CommonChunkPlugin) // 针对webpack3 { plugins: [ new webpack.optimize.CommonChunkPlugin({ name: String | Array, // 表示chunk的名称 ? filename: String, // 公用代码打包的文件名称 minChunks: Number|function|Infinity // 数字表示为需要提取的公用代码出现的次数(最小是多少, 比如出现两次以上就提取到公用代码), Infinity 表示不讲任何的模块打包进去, 函数的话表示自定义逻辑 chunks: 表示指定提取代码的范围, 需要在哪几个代码快中提取公用代码 children: 是不是在entry的子模块中 还是在所有模块中查找依赖 deepChildren async: 创建一个异步的公共代码流 }) ] } // webpack4 optimization: { splitChunks: { chunks: 'all', // async(默认, 只会提取异步加载模块的公共代码), initial(提取初始入口模块的公共代码), all(同时提取前两者), minSize: 0, // 30000, // 大于30K会被抽离到公共模块 minChunks: 2, // 模块出现一次就会被抽离到公共模块中, 如果是1的话, 表示将所有的模块都提走, 针对pageA中, 如果只有自己引用jQuery, 那么会生成jQuery-vendor.js 的打包文件 maxAsyncRequests: 5, // 异步模块, 一次最多只能加载5个, maxInitialRequests: 3, // 入口模块最多只能加载3个 name: true } }
- 场景
- 单页应用
- 单页应用 第三方依赖
- 多页应用 第三方依赖 webpack生成代码 (webpack内置函数)
- 针对单入口的commonChunksPlugin = 并没有将公共部分打包, 只有针对多入口才会
- 多入口文件的时候 entry: { pageA: path.resolve(__dirname, 'src/cmp', 'pageA'), pageB: path.resolve(__dirname, 'src/cmp', 'pageB') // vendor: ['lodash'] }, // webpack3 plugins: [ new webpack.optimize.CommonPluginsChun({ name: 'vendor', minChunks: Infinity }) // 公共模块打包的名字为vendor, entry中也有vendor, 所以会将webpack生成代码以及lodash打包进vendor中 ] // webpack4 splitChunks: { chunks: 'all', // async(默认, 只会提取异步加载模块的公共代码), initial(提取初始入口模块的公共代码), all(同时提取前两者), minSize: 30000, // 大于30K会被抽离到公共模块 // minChunks: 2, // 模块出现两次次就会被抽离到公共模块中 minChunks: Infinity, // 不需要在任何的地方重复 maxAsyncRequests: 5, // 异步模块, 一次最多只能加载5个, maxInitialRequests: 3, // 入口模块最多只能加载3个 // name: 'common' // 打包出来公共模块的名称 name: 'vendor' // 打包出来公共模块的名称 } // 1. 会将pageA, pageB中 公共使用的模块打包成进common.chunk.js (name:'common'的时候), 公共模块中包括webpack生成的代码 // 2. lodash只在pageA中使用, 次数为1, 但是minChunks: 2, 所以lodash只会被打包进pageA中 // 3. 在entry中添加 vendor: ['lodash'] 将公共库lodash单独打包, 在webpack4中将其打包进了公共common.chunk中, vendor中只有对lodash的引用 // 4. 如果想将lodash和webpack运行生成时代码以及公共代码打包到一起, minChunks改成Infinity, name:vendor, 将所有生成的文件引用都放到vendor中了 // 5. 保持第三方代码的纯净, 即将第三方代码和webpack分离开, webapck3添加plugins, webpack4添加runtimeChunk配置 // webpack3 new webpack.optimize.CommonPluginsChun({ name: 'manifest', minChunks: Infinity }) // 发现vendor和manifest处大部分代码是一样的可以, 可以改成 new webpack.optimize.CommonPluginsChun({ names: ['vendor','manifest'], minChunks: Infinity }) // webpack4 runtimeChunk: { name: 'manifest' }, // 结果是: 将webpack生成的代码打包到manifest中, 将lodash打包进vendor中, 将引用次数超过两次的打包进vendor中
6. 代码分割和懒加载
- 通过代码分割和懒加载, 让用户在尽可能的下载时间内加载想要的页面, 只看一个页面的时候, 下载所有的代码, 带宽浪费;
- 在webpack中, 代码分割和懒加载是一个概念, webpack会自动分割代码, 然后再把需要的代码加载进来, 不是通过配置来实现的, 通过改变写代码的方式来实现的, 当依赖一个模块的时候, 告诉webpack我们是需要懒加载或者代码切分, 通过两种方式来实现
- webpack.methods
- require.ensure() 接收四个参数
- 第一个参数dependencies, 加载进来的代码并不会执行, 在callback中引入, 这个时候才会去执行, 第三个参数errorBack, 第四个参数chunkName
- 如果浏览器不支持promise, 需要添加垫片
- require.include 只有一个参数, 只引入进来, 但不执行
- 当两个子模块都引入了第三个模块, 可以将第三个模块放入父模块中, 这样动态加载子模块的时候, 父模块已经有了第三方模块, 不会在多余加载; 比如subPageA, subPageB都引入了moduleA, 但是moduleA不会被打包进父依赖, 所以可以使用include
- ES2015 loader spec (动态import) stage-3
- 早起system.import
- 后来import方式 返回一个Promise
- import().then
- webpack import function 通过注释的方式来解决动态的chunkName以及加载模式import( /*webpackChunkName: async-chunk-name*/ /*webpackMode: lazy*/ moduleName )
- webpack.methods
- 代码分割的场景
- 分离业务代码和第三方依赖 (提取公共代码中有涉及)
- 分离业务代码 和 业务公共代码 和 第三方依赖; 相比于上一个,将业务代码拆成两部分
- 分离首次加载 和 访问后加载的代码 (访问速度优化相关的) - LazyLoad - 提高首屏加载速度
// 0. 单入口pageA, 不做任何的优化 直接引入 subPageA, subPageB, lodash 会发现pageA非常大
// 1. 异步引入, 将lodash打包到vendor中
require.ensure('lodash', require => {
const _ = require('lodash')
_.join([1, 2, 3], 4)
console.log(_)
}, 'vendor')
// 2. pageA.js中修改
if (page === 'subPageA') {
// require([]) 参数是空数组的话, 里面的require的包还是会被异步打包
require.ensure(['./subPageA'], require => {
// 如果不require的话, 那么就不会执行subPageA中的代码块
const subPageA = require('./subPageA')
console.log(subPageA)
}, 'subPageA')
} else if (page === 'subPageB') {
require.ensure(['./subPageB'], require => {
const subPageB = require('./subPageB')
console.log(subPageB)
}, 'subPageB')
}
// 结果: moduleA分别在打包好的文件 subPageA.chunk.js 和 subPageB.chunk.js中, 公共部分moduleA没有被提取出来
// 3. 单entry有上述公共代码的情况的话, 使用inlcude的情况处理, 将module在父模块pageA.js提前引入, 但是并不运行
require.include('./moduleA')
// 结果: moduleA被打包进入了pageA.bundle.js中, 这样就完成了代码分割
// --- import 方案 -------------
/* 坑: import 只有在stage-0 或者 syntax-dynamic-import
yarn add babel-preset-stage-0 babel-plugin-syntax-dynamic-import --dev
.babelrc { "presets": ["stage-0"], "plugins": ["syntax-dynamic-import"] }
上述两种情况只使用一种即可
*/
// 在import的时候 代码实际上已经执行了
if (page) {
import(
/* webpackChunkName: "subPageA" */
/* webpackMode: "lazy" */
'./subPageC'
).then(subPageC => {
console.log(subPageC)
})
} else {
import(
/* webpackChunkName: 'subPageD' */
/* webpackMode: "lazy" */
'./subPageD'
)
}
- async 在代码分割中如何使用, 即结合commonChunkPlugin // webpack.plugin.lazy.cmp.js entry: { pageA: path.resolve(__dirname, 'src/lazy_cmp', 'pageA'), pageB: path.resolve(__dirname, 'src/lazy', 'pageB'), vendor: ['lodash'] } // webpack3 plugins: [ new wepback.optimize.CommonsChunkPlugin({ // async 指定为true表示异步模块, 或者指定为 异步模块提取后的名称 async: 'async-common', children: true, // 表示不仅仅是两个入口页面之间, 而且还是两个页面之间的子依赖中去寻找 minChunks: 2 }), new wepback.optimize.CommonsChunkPlugin({ // lodash打包进入vendor中, manifest是webpack运行时代码 names: ['vendor', 'manifest'], minChunks: Infinity }) ] // webpack4 optimization: { // webpack runtime 代码 runtimeChunk: { name: 'manifest' }, // 公共模块提取 splitChunks: { chunks: 'all', // async(默认, 只会提取异步加载模块的公共代码), initial(提取初始入口模块的公共代码), all(同时提取前两者), minSize: 30000, // 大于30K会被抽离到公共模块 // minChunks: 2, // 模块出现两次次就会被抽离到公共模块中 minChunks: Infinity, // 不需要在任何的地方重复 maxAsyncRequests: 5, // 异步模块, 一次最多只能加载5个, maxInitialRequests: 3, // 入口模块最多只能加载3个 name: 'vendor' // 打包出来公共模块的名称 } } // pageA.js import _ from 'lodash' / 1. 这里不再使用include, 因为会和pageA打包到一起, 这里的目的是 将其异步单独提取出来 // require.include('./moduleA') const page = 'subPageA' // 在pageB中, 这里page='subPageB', 其余一样 if (page) { import( /* webpackChunkName: "subPageA" */ /* webpackMode: "lazy" */ './subPageA' ).then(subPageA => { console.log(subPageA) }) } else { import( /* webpackChunkName: 'subPageB' */ /* webpackMode: "lazy" */ './subPageB' ) } // 2. webpack3 结果: 将异步打包结果中subPageA和subPageB中的公共模块moduleA, 单独的提取到了async-common-pageA.chunk.js中 这里比较坑的困惑: commonsChunkPlugin参数说的不是很明确, 比如async, children, deepChildren, minChunk, 他们之间是有依赖忽视关系的 // 3. webpack4 结果: chunks:all, 结果是将多次引用的公共模块moduleA, lodash提取到了vendor.chunk中, 其余的和webpack3一样, 生成打包文件pageA.chunk, pageB.chunk(入口文件), subPageA.chunk, subPageB.chunk(异步单独提取), manifest.chunk(webpack-runtime单独提取)
5. 处理CSS
- 每一个模块都有自己的css文件, 在使用的时候将css样式引入
- 如何在webpack中引入css
- style-loader 在页面中创建style标签, 标签里面的内容就是css内容
- style-loader/url
- style-loader/useable
- css-loader 如何让js可以import一个css文件, 包装一层, 让js可以引入css文件 // index.js import './css/base.css' // webpack.config.style.js { test: /.css$/, use: [ { loader: 'style-loader' }, { loader: 'css-loader' } ] } // 将打包后的文件引入到index.html中 // 1. 结果: 在html中生成了style标签, 将base.css标签中的样式放到了style标签中 // 2. 生成link标签的形式 (不过用的比较少) 注意publicPath配置 use: [ { loader: 'style-loader/url' // loader: 'style-loader/useable' }, { loader: 'file-loader' } ] // 结果: style-loader/url 单独生成一份css文件 , 但是引入多个文件的时候, 会生成多个link标签, 会造成越多的网路请求 //3. style-loader/useable import base from 'base.css' import common from 'common.css' var flag = false; setInterval(function() { if(flag) { base.use() } else { base.ununse() } flag = !flag; }, 2000) // base.use() 样式插入到style标签中 // common.unuse() // 控制样式不被引用 // 结果: 没过2000ms, 页面中样式循环引用和删除
- style-loader 在页面中创建style标签, 标签里面的内容就是css内容
6. StyleLoader 配置
- insertAt (插入位置)
- insertInto(插入到DOM)
- singleton (是否只使用一个style标签) 当css模块比较多的时候 会有很多css标签
- transform (转化, 浏览器环境下, 插入页面之前)
- transform: './src/style/css.transform.js' // css.transform.js 文件内容 // 该函数并不是在打包的时候执行的,在运行webpack的时候, 是不行执行的 // 在style-loader 将样式插入到DOM中的时候 执行的, 运行的环境是在浏览器环境下, 可以拿到浏览器参数, window,UA // 可以根据当前浏览器来对当前的css进行形变 module.exports = function(css) { console.log(css) console.log(window.innerWidth) // 输出形变以后的css if (window.innerWidth >= 768) { css = css.replace('yellow', 'dodgerblue') } else { css = css.replace('yellow', 'orange') } return css; } - 针对每一次在index.js中引入的css文件都会执行上面的代码
- CssLoader 配置参数
- alias 解析的别名 将引入css的路径映射到其他地方
- importLoader 取决于css处理后面是不是还有其他的loader (sass会使用到 @import)
- minimize 是否压缩
- modules 是否启用css-modules
- 打包出来的样式class 都变成一段随机字符串
- CSS modules
- :local 给定一个本地的样式 局部的样式
- :global 给定一个全局样式
- compose 继承一个样式
- compose ... from path 引入一个样式 (尽量将composes放在前面, 这样可以控制引入顺序, 样式不会被覆盖) // base.css .box { composes: big-box from './common.css'; height: 200px; width: 100px; border-radius: 4px; background: #696969; }
- localIdentName: '[[path]][name]_[local]--[hash:base64:5]' 控制生成的class类名
- path代表引用css路径 name表示文件名称 local本地样式名称
- 配置less/sass
- path代表引用css路径 name表示文件名称 local本地样式名称
- npm install less-loader less --save-dev
- npm install sass-loader node-sass --save-dev.header { composes: font from './header.less' }
- 提取css代码 - 提取公共代码 做缓存 (不提取的话, 将css代码打包到了js文件中)
- extract-loader
- ExtractTextWebpackPlugin
- npm install extract-text-webpack-plugin --save-dev
- CssLoader 配置参数
// webpack3
var ExtractTextWebpackPlugin = require('ExtractTextWebpackPlugin)
module: {
rules: [
{
test: /.less$/,
use: ExtractTextWebpackPlugin.extract({
fallback: {
// 告诉webpack, 当不提取的时候, 使用何种方式将其加载到页面中
loader: 'style-loader,
options: {
singleton: true,
// transform: ''
}
},
use: [
{loader: 'css-loader'}
{loader: 'less-loader'}
], // 定义我们继续处理的loader
})
}
]
},
plugins: [
new ExtractTextWebpackPlugin({
filename: '[name].min.css', // 提取出来的css的名称
// 将css-loader的option中的minimize打开
// allChunks 给插件指定一个范围, 指定提取css的范围
// 1. 设置为true 表示所有的引用的css文件都提取
// 2. 设置为false, 默认, 只会提取初始化的css(异步加载不认为是初始化)
allChunks:false,
})
]
// webpack3 结果: index.bundle.js app.min.css 但是打开index.html 并没有插入进去
// webpack4
{
test: /.less$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
// loader: 'file-loader'
options: {
minimize: process.env.NODE_ENV === 'production',
modules: true,
localIdentName: '[path]_[name]_[local]--[hash:base64:5]'
}
},
{
loader: 'less-loader'
}
]
}
plugins: [
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: '[name].css',
chunkFilename: '[id].css'
})
]
- 异步引入a.js文件, 在a.js文件中引入a.less
1. 针对allChunks为false的情况
- webpack3: 生成a.bundle.js文件, css文件被当成js的一个模块被打包处理, 将css放在js文件里面, 一起被提取; css代码切分的一种方式, 将初始化加载和动态加载区分开; 借助动态加载的代码区分, 也是css-in-js的一个概念
- weboack4: 生成moduleA.chunk.js 和moduleA.chunk.css文件, 在index.bundle.js 包括了对于modulA.js和module.css文件的引用
2. webpack4使用splitChunks配置
optimization: {
splitChunks: {
cacheGroups: {
styles: {
name: 'styles',
test: /.scss|css$/,
chunks: 'all', // merge all the css chunk to one file
enforce: true
}
}
}
}
- 结果: 生成index.bundle.js style.chunk.js style.chunk.css 将所有的样式文件都打包进了style.chunk.css文件中, 但是需要手动添加到项目htm中
- question: 为什么这里不会运行? npm run extract
- PostCss (Autoprefixer CSS-nano CSS-next) A tool for transforming Css With Javascript用js去转化css的一个工具
- 联系到上一节中的css.transform.js, 但是时机是不一样的, PostCss是打包的时期, css.transform是浏览器插入到style标签中的时候
- postcss的强大, 理解成为一个处理css的工具
- 安装 npm install postcss postcss-loader autoprefixer cssnano postcss-cssnext --save-dev
- autoprefixer: 帮助加上浏览器前缀
- css-nano 帮助我们优化压缩css, 在postcss可以当做插件使用, css-loader就是用的css-nano做的压缩
- css-next 使用未来的css新语法
- css variables
- custom selectors 自定义选择器
- calc() 动态计算 ...
{
loader: 'postcss-loader',
options: {
// require进来的插件给postcss使用的
ident: 'postcss', // 表明接下来的插件是给postcss使用的
plugins: [
// require('autoprefixer')(),
// 两个一起用cssnext 会给出警告, 提示已经包含autoprefixer
require('postcss-cssnext')()
]
}
},
- 一旦涉及到浏览器兼容性问题的时候, 一定会有针对的浏览器兼容问题, 使用browserlist, 让所有的插件都公用一份browserlist
- 可以放在package.json里面
- .browserlistrc 存入对浏览器的要求
- postcss-import 插件 将@import的文件内容直接放入到当前的css文件中, 但是存过来之后要考虑相对文件路径的变化, 需要配合postcss-url来使用