代码工程化构建,webpack、babel
1.入口
entry
是配置模块的入口,可抽象成输入,Webpack
执行构建的第一步将从入口开始搜寻及递归解析出所有入口依赖的模块。
Webpack
在寻找相对路径的文件时会以 context
为根目录,context
默认为执行启动 Webpack
时所在的当前工作目录。 如果想改变 context
的默认配置,则可以在配置文件里这样设置它:
module.exports = { context: path.resolve(__dirname, 'app') }
(1)Entry 类型
string 类型
例如:'./app/entry' , 入口模块的文件路径,可以是相对路径。
array 类型
例如: ['./app/entry1', './app/entry2'] ,
object类型
例如:{ a: './app/entry-a', b: ['./app/entry-b1', './app/entry-b2']}
(2)配置动态 Entry
假如项目里有多个页面需要为每个页面的入口配置一个 Entry ,但这些页面的数量可能会不断增长,则这时 Entry 的配置会受到到其他因素的影响导致不能写成静态的值。
// 同步函数 entry: () => { return { a:'./pages/a', b:'./pages/b', } }; // 异步函数 entry: () => { return new Promise((resolve)=>{ resolve({ a:'./pages/a', b:'./pages/b', }); }); };
(3)Chunk 名称
Webpack 会为每个生成的 Chunk 取一个名称,Chunk 的名称和 Entry 的配置有关:
-
如果
entry
是一个string
或array
,就只会生成一个 Chunk,这时 Chunk 的名称是main
; -
如果
entry
是一个object
,就可能会出现多个 Chunk,这时 Chunk 的名称是object
键值对里键的名称。
2.输出
output
配置如何输出最终想要的代码。output
是一个 object
,里面包含一系列配置项 。
(1)filename
output.filename
配置输出文件的名称,为string 类型。 如果只有一个输出文件,则可以把它写成静态不变的:
output: { filename: 'bundle.js' }
但是在有多个 Chunk 要输出时,就需要借助模版和变量了。
output: { filename: '[name].js' }
代码里的 [name]
代表用内置的 name
变量去替换[name]
,这时你可以把它看作一个字符串模块函数, 每个要输出的 Chunk 都会通过这个函数去拼接出输出的文件名称。
内置变量除了 name
还包括: id ( Chunk 的唯一标识,从0开始 )、 hash ( Chunk 的唯一标识的 Hash 值 )、 chunkhash ( Chunk 内容的 Hash 值 )。
(2)chunkFilename
output.chunkFilename
配置无入口的 Chunk 在输出时的文件名称。 chunkFilename
和上面的 filename 非常类似,但 chunkFilename
只用于指定在运行过程中生成的 Chunk 在输出时的文件名称。 常见的会在运行时生成 Chunk 场景有在使用 CommonChunkPlugin
、使用 import('path/to/module')
动态加载等时。 chunkFilename
支持和 filename 一致的内置变量。
(3)path
output.path
配置输出文件存放在本地的目录,必须是 string 类型的绝对路径。通常通过 Node.js
的 path
模块去获取绝对路径:
path: path.resolve(__dirname, 'dist_[hash]')
(4)publicPath
在复杂的项目里可能会有一些构建出的资源需要异步加载,加载这些异步资源需要对应的 URL 地址。output.publicPath
配置发布到线上资源的 URL 前缀,为string 类型。 默认值是空字符串 ''
,即使用相对路径。 需要把构建出的资源文件上传到 CDN 服务上,以利于加快页面的打开速度。配置代码如下:
filename:'[name]_[chunkhash:8].js' publicPath: 'https://cdn.example.com/assets/'
会将构建出来的资源上传到线上。
3.模块
module
配置如何处理模块。
(1)配置Loader
rules
配置模块的读取和解析规则,通常用来配置 Loader。其类型是一个数组,数组里每一项都描述了如何去处理部分文件。 配置一项 rules
时大致通过以下方式:
-
条件匹配:通过
test
、include
、exclude
三个配置项来命中 Loader 要应用规则的文件。 -
应用规则:对选中后的文件通过
use
配置项来应用 Loader,可以只应用一个 Loader 或者按照从后往前的顺序应用一组 Loader,同时还可以分别给 Loader 传入参数。 -
重置顺序:一组 Loader 的执行顺序默认是从右到左执行,通过
enforce
选项可以让其中一个 Loader 的执行顺序放到最前或者最后。
例如:
module: { rules: [ { // 命中 JavaScript 文件 test: /\.js$/, // 用 babel-loader 转换 JavaScript 文件 // ?cacheDirectory 表示传给 babel-loader 的参数,用于缓存 babel 编译结果加快重新编译速度 use: ['babel-loader?cacheDirectory'], // 只命中src目录里的js文件,加快 Webpack 搜索速度 include: path.resolve(__dirname, 'src') }, ] }
在 Loader 需要传入很多参数时,还可以通过一个 Object 来描述 。例如:
use: [ { loader:'babel-loader', options:{ cacheDirectory:true, }, // enforce:'post' 的含义是把该 Loader 的执行顺序放到最后 // enforce 的值还可以是 pre,代表把 Loader 的执行顺序放到最前面 enforce:'post' }, ]
test include exclude
这三个命中文件的配置项只传入了一个字符串或正则,其实它们还都支持数组类型,使用如下:
{ test:[ /\.jsx?$/, /\.tsx?$/ ], include:[ path.resolve(__dirname, 'src'), path.resolve(__dirname, 'tests'), ], exclude:[ path.resolve(__dirname, 'node_modules'), path.resolve(__dirname, 'bower_modules'), ] }
(2)parser
因为 Webpack
是以模块化的 JavaScript 文件为入口,所以内置了对模块化 JavaScript 的解析功能,支持 AMD、CommonJS
、SystemJS
、ES6
。 parser
属性可以更细粒度的配置哪些模块语法要解析哪些不解析 。
module: { rules: [ { test: /\.js$/, use: ['babel-loader'], parser: { amd: false, // 禁用 AMD commonjs: false, // 禁用 CommonJS system: false, // 禁用 SystemJS harmony: false, // 禁用 ES6 import/export requireInclude: false, // 禁用 require.include requireEnsure: false, // 禁用 require.ensure requireContext: false, // 禁用 require.context browserify: false, // 禁用 browserify requireJs: false, // 禁用 requirejs } }, ] }
4.解析
Webpack
在启动后会从配置的入口模块出发找出所有依赖的模块,Resolve 配置 Webpack 如何寻找模块所对应的文件。
(1)alias
resolve.alias
配置项通过别名来把原导入路径映射成一个新的导入路径。例如使用以下配置:
resolve:{ alias:{ components: path.resolve(__dirname, 'src/component') } }
当你通过 import Button from 'components/button'
导入时,实际上被 alias
等价替换成了 import Button from './src/components/button'
。
(2)mainFields
有一些第三方模块会针对不同环境提供几分代码。 例如分别提供采用 ES5 和 ES6 的2份代码,这2份代码的位置写在 package.json
文件里,如下:
{ "jsnext:main": "es/index.js",// 采用 ES6 语法的代码入口文件 "main": "lib/index.js" // 采用 ES5 语法的代码入口文件 }
Webpack
会根据 mainFields
的配置去决定优先采用那份代码,mainFields
默认如下:
mainFields: ['browser', 'main']
Webpack
会按照数组里的顺序去package.json
文件里寻找,只会使用找到的第一个。
假如你想优先采用ES6
的那份代码,可以这样配置:
mainFields: ['jsnext:main', 'browser', 'main']
(3)extensions
在导入语句没带文件后缀时,Webpack 会自动带上后缀后去尝试访问文件是否存在。 resolve.extensions
用于配置在尝试过程中用到的后缀列表,默认是:
extensions: ['.js', '.json']
也就是说当遇到 require('./data')
这样的导入语句时,Webpack
会先去寻找 ./data.js
文件,如果该文件不存在就去寻找 ./data.json
文件, 如果还是找不到就报错。
假如想让 Webpack 优先使用目录下的 TypeScript 文件,可以这样配置:
extensions: ['.ts', '.js', '.json']
(3)modules
resolve.modules
配置 Webpack 去哪些目录下寻找第三方模块,默认是只会去 node_modules
目录下寻找。 有时你的项目里会有一些模块会大量被其它模块依赖和导入,由于其它模块的位置分布不定,针对不同的文件都要去计算被导入模块文件的相对路径, 这个路径有时候会很长,就像这样 import '../../../components/button'
这时你可以利用 modules
配置项优化,假如那些被大量导入的模块都在 ./src/components
目录下,把 modules
配置成
modules:['./src/components','node_modules']
然后就可以简单的通过 import 'button'
导入。
(4)enforceExtension
resolve.enforceExtension
如果配置为 true
所有导入语句都必须要带文件后缀, 例如开启前 import './foo'
能正常工作,开启后就必须写成 import './foo.js'
。
5.插件
Plugin 用于扩展 Webpack 功能,各种各样的 Plugin 几乎让 Webpack 可以做任何构建相关的事情。
配置Plugin
plugins
配置项接受一个数组,数组里每一项都是一个要使用的 Plugin 的实例,Plugin 需要的参数通过构造函数传入。
const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin'); module.exports = { plugins: [ // 所有页面都会用到的公共代码提取到 common 代码块中 new CommonsChunkPlugin({ name: 'common', chunks: ['a', 'b'] }), ] };
6.React项目Webpack配置
(1)常用loader配置
// babel-loader { test:/\.(js|jsx)$/, // 如果js文件在node_modules里边,就不使用babel-loader了 // 因为它里边的代码都是些第三方代码,已经做好了转译的工作。 exclude: [/node_modules/], use:[{ loader: "babel-loader", options: { //将async/await转化为es5语法,需要用到regeneratorRuntime函数,这个函数在babel-runtime包中 plugins: ["@babel/plugin-transform-runtime"] }, } // style-loader,css-loader,less-loader { test: /\.css$/, //处理.css文件 use: ['style-loader', 'css-loader'] }, { test: /\.less$/, //处理.less文件,将less语法转化为css语法 use: ["style-loader", "css-loader", "less-loader"], }, // ts-loader { test: /\.tsx?$/, //将ts代码转化为js exclude: /node_modules/, loader: 'ts-loader' } // file-loader { test: /\.(png|jpg|gif)$/, //处理文件导入地址并替换成其访问地址,并把文件输出到相应位置 use: [ { loader: 'file-loader', options: {}, }, ], },
(2)常用插件配置
HtmlWebpackPlugin //生成一个 HTML5 文件, 其中包括使用 script 标签的 body 中的所有 webpack 包。 new HtmlWebpackPlugin({ title: '***', template: './src/index.html', filename: "index.html" }), CleanWebpackPlugin //打包时清除上一次的打包文件 new CleanWebpackPlugin({ path: path.join(__dirname, 'dist') }), CompressionPlugin //打包时进行压缩 new CompressionPlugin({//打包时候进行压缩文件 algorithm: 'gzip', // 压缩格式 有:gzip、brotliCompress, test: /\.(js|css|svg)$/, threshold: 100,// 只处理比这个值大的资源,按字节算 minRatio: 0.8, //只有压缩率比这个值小的文件才会被处理,压缩率=压缩大小/原始大小,如果压缩后和原始文件大小没有太大区别,就不用压缩 deleteOriginalAssets: false //是否删除原文件,最好不删除,服务器会自动优先返回同名的.gzip资源,如果找不到还可以拿原始文件 }) SplitChunksPlugin //为了解决CommonsChunkPlugin存在的一些问题,目前采用了新的方式SplitChunksPlugin对代码进行拆分 //需要在optimization里面配置,例如 splitChunks: { chunks: 'async', // 表示选择哪些 chunks 进行分割,可选值有:async,initial和all minSize: 20000,// 表示新分离出的chunk必须大于等于minSize,默认为30000,约30kb。 minRemainingSize: 0, minChunks: 1,// 表示一个模块至少应被minChunks个chunk所包含才能分割。默认为1。 maxAsyncRequests: 30,// 表示按需加载文件时,并行请求的最大数目。默认为5。 maxInitialRequests: 30, // 表示加载入口文件时,并行请求的最大数目。默认为3。 cacheGroups: { vendors: { test: /[\\/]node_modules[\\/]/, priority: -10, filename: "static/[chunkhash]_vendors.js", }, default: { minChunks: 2, priority: -30, filename: "static/common_[chunkhash].js", }, }, },
(3)devServer配置
//以下是一个示例 devServer: { compress: true, port: 9000, hot: true,//开启热替换 proxy: { //将某些路径代理到某些服务器请求上,可以在此配置跨域 '/robot': { target: 'http://api.qingyunke.com/', changeOrigin: true,// 允许跨域 pathRewrite: { //使用proxy进行代理时,对请求路径进行重定向以匹配到正确的请求地址,此处为去掉请求路径中的/robot '^/robot': '' }, bypass: function(req, res, proxyOptions) {//解决BrowserRouter跳转页面后刷新页面报404 if (req.headers.accept.indexOf('html') !== -1) { return '/index.html'; } } }, '/baidu': { target: 'http://www.baidu.com', changeOrigin: true,// 允许跨域 pathRewrite: { '^/base': '' }, bypass: function(req, res, proxyOptions) {//解决BrowserRouter跳转页面后刷新页面报404 if (req.headers.accept.indexOf('html') !== -1) { return '/index.html'; } } } }, client: {//当出现编译错误或警告时,在浏览器中显示全屏覆盖 overlay: true, }, }
7.React项目配置Babel
如果当我们使用的是 Babel 和 React 17,可能需要在package.json文件下的babel下添加"runtime":" automatic" 来配置。
"babel": { "plugins": [ [ "@babel/plugin-proposal-decorators", //类函数装饰器 { "legacy": true } ] ], "presets": [ [ "@babel/react", { "runtime": "automatic" } ], "@babel/env" ] }
或者新建一个.babelr文件,
{ "presets": [ ["@babel/react", {"runtime": "automatic"}], "@babel/env" ], "plugins": [...] }
安装
# ES2015转码规则 $ npm install --save-dev babel-preset-es2015 # react转码规则 $ npm install --save-dev babel-preset-react
8.webpack优化体验
(1)缩小文件搜索范围
Webpack 启动后会从配置的 Entry 出发,解析出文件中的导入语句,再递归的解析。 在遇到导入语句时 Webpack 会做两件事情:
-
根据导入语句去寻找对应的要导入的文件。例如
require('react')
导入语句对应的文件是./node_modules/react/react.js
,require('./util')
对应的文件是./util.js
。 -
根据找到的要导入文件的后缀,使用配置中的 Loader 去处理文件。例如使用 ES6 开发的 JavaScript 文件需要使用 babel-loader 去处理。
以上两件事情虽然对于处理一个文件非常快,但是当项目大了以后文件量会变的非常多,这时候构建速度慢的问题就会暴露出来。 虽然以上两件事情无法避免,但需要尽量减少以上两件事情的发生,以提高速度。
优化 loader 配置
由于 Loader 对文件的转换操作很耗时,需要让尽可能少的文件被 Loader 处理 。如:
module.exports = { module: { rules: [ { // 如果项目源码中只有 js 文件就不要写成 /\.jsx?$/,提升正则表达式性能 test: /\.js$/, // babel-loader 支持缓存转换出的结果,通过 cacheDirectory 选项开启 use: ['babel-loader?cacheDirectory'], // 只对项目根目录下的 src 目录中的文件采用 babel-loader include: path.resolve(__dirname, 'src'), }, ] }, };
优化 resolve.modules 配置
resolve.modules
用于配置 Webpack 去哪些目录下寻找第三方模块。 默认值是 ['node_modules'] ,含义是先去当前目录下的 ./node_modules
目录下去找想找的模块,如果没找到就去上一级目录 ../node_modules
中找,再没有就去 ../../node_modules
中找 。
当安装的第三方模块都放在项目根目录下的 ./node_modules
目录下时,没有必要按照默认的方式去一层层的寻找,可以指明存放第三方模块的绝对路径,以减少寻找,配置如下:
module.exports = { resolve: { // 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤 // 其中 __dirname 表示当前工作目录,也就是项目根目录 modules: [path.resolve(__dirname, 'node_modules')] }, };
优化 resolve.extensions 配置
在导入语句没带文件后缀时,Webpack 会自动带上后缀后去尝试询问文件是否存在。 resolve.extensions
用于配置在尝试过程中用到的后缀列表,默认是:
extensions: ['.js', '.json']
也就是说当遇到 require('./data')
这样的导入语句时,Webpack 会先去寻找 ./data.js
文件,如果该文件不存在就去寻找 ./data.json
文件,如果还是找不到就报错。
如果这个列表越长,或者正确的后缀在越后面,就会造成尝试的次数越多,所以 resolve.extensions
的配置也会影响到构建的性能。 在配置 resolve.extensions
时你需要遵守以下几点,以做到尽可能的优化构建性能:
-
后缀尝试列表要尽可能的小,不要把项目中不可能存在的情况写到后缀尝试列表中。
-
频率出现最高的文件后缀要优先放在最前面,以做到尽快的退出寻找过程。
-
在源码中写导入语句时,要尽可能的带上后缀,从而可以避免寻找过程。例如在你确定的情况下把
require('./data')
写成require('./data.json')
。
(2)使用HappyPack
HappyPack能够让webpack同一时刻处理多个任务,它把任务分解给多个子进程去并发执行,子进程处理完后把结果发送给主进程。 接入 HappyPack 的相关代码如下:
const path = require('path'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); const HappyPack = require('happypack'); module.exports = { module: { rules: [ { test: /\.js$/, // 把对 .js 文件的处理转交给 id 为 babel 的 HappyPack 实例 use: ['happypack/loader?id=babel'], // 排除 node_modules 目录下的文件,node_modules 目录下的文件都是采用的 ES5 语法,没必要再通过 Babel 去转换 exclude: path.resolve(__dirname, 'node_modules'), }, { // 把对 .css 文件的处理转交给 id 为 css 的 HappyPack 实例 test: /\.css$/, use: ExtractTextPlugin.extract({ use: ['happypack/loader?id=css'], }), }, ] }, plugins: [ new HappyPack({ // 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件 id: 'babel', // 如何处理 .js 文件,用法和 Loader 配置中一样 loaders: ['babel-loader?cacheDirectory'], // ... 其它配置项 }), new HappyPack({ id: 'css', // 如何处理 .css 文件,用法和 Loader 配置中一样 loaders: ['css-loader'], }), new ExtractTextPlugin({ filename: `[name].css`, }), ], };
在整个 Webpack 构建流程中,最耗时的流程可能就是 Loader 对文件的转换操作了,因为要转换的文件数据巨多,而且这些转换操作都只能一个个挨着处理。 HappyPack 的核心原理就是把这部分任务分解到多个进程去并行处理,从而减少了总的构建时间。
(3)使用自动刷新
文件监听
文件监听是在发现源码文件发生变化时,自动重新构建出新的输出文件。 Webpack 支持文件监听相关的配置项如下:
module.export = { // 只有在开启监听模式时,watchOptions 才有意义 // 默认为 false,也就是不开启 watch: true, // 监听模式运行时的参数 // 在开启监听模式时,才有意义 watchOptions: { // 不监听的文件或文件夹,支持正则匹配 // 默认为空 ignored: /node_modules/, // 监听到变化发生后会等300ms再去执行动作,防止文件更新太快导致重新编译频率太高 // 默认为 300ms aggregateTimeout: 300, // 判断文件是否发生变化是通过不停的去询问系统指定文件有没有变化实现的 // 默认每隔1000毫秒询问一次 poll: 1000 } }
要让 Webpack 开启监听模式,有两种方式:
-
在配置文件
webpack.config.js
中设置watch: true
。 -
在执行启动 Webpack 命令时,带上
--watch
参数,完整命令是webpack --watch
。
优化文件监听性能
开启监听模式时,默认情况下会监听配置的 Entry 文件和所有其递归依赖的文件。 在这些文件中会有很多存在于 node_modules
下,因为如今的 Web 项目会依赖大量的第三方模块。 在大多数情况下我们都不可能去编辑 node_modules
下的文件,而是编辑自己建立的源码文件。 所以一个很大的优化点就是忽略掉 node_modules
下的文件,不监听它们。 相关配置如下:
module.export = { watchOptions: { // 不监听的 node_modules 目录下的文件 ignored: /node_modules/, } }
除了忽略掉部分文件的优化外,还有如下两种方法:
-
watchOptions.aggregateTimeout
值越大性能越好,因为这能降低重新构建的频率。 -
watchOptions.poll
值越大越好,因为这能降低检查的频率。
但两种优化方法的后果是会让你感觉到监听模式的反应和灵敏度降低了。
自动刷新浏览器
监听到文件更新后的下一步是去刷新浏览器,webpack 模块负责监听文件,webpack-dev-server 模块则负责刷新浏览器。 在使用 webpack-dev-server 模块去启动 webpack 模块时,webpack 模块的监听模式默认会被开启。 webpack 模块会在文件发生变化时告诉 webpack-dev-server 模块。
(4)区分环境
在开发网页的时候,一般都会有多套运行环境,例如:
-
在开发过程中方便开发调试的环境。
-
发布到线上给用户使用的运行环境。
怎样区分环境
在源码中通过如下方式:
if (process.env.NODE_ENV === 'production') { console.log('你正在线上环境'); } else { console.log('你正在使用开发环境'); }
其大概原理是借助于环境变量的值去判断执行哪个分支。
当你的代码中出现了使用 process 模块的语句时,Webpack 就自动打包进 process 模块的代码以支持非 Node.js 的运行环境。 当你的代码中没有使用 process 时就不会打包进 process 模块的代码。这个注入的 process 模块作用是为了模拟 Node.js 中的 process,以支持上面使用的 process.env.NODE_ENV === 'production'
语句。
在构建线上环境代码时,需要给当前运行环境设置环境变量 NODE_ENV = 'production'
,Webpack 相关配置如下:
const DefinePlugin = require('webpack/lib/DefinePlugin'); module.exports = { plugins: [ new DefinePlugin({ // 定义 NODE_ENV 环境变量为 production 'process.env': { NODE_ENV: JSON.stringify('production') //注意在定义环境变量的值时用 JSON.stringify 包裹字符串的原因是环境变量的值需要是一个由双引号包裹 //的字符串,而 JSON.stringify('production')的值正好等于'"production"'。 } }), ], };
(5)使用 Prepack
认识Prepack
Prepack 由 Facebook 开源,它采用较为激进的方法:在保持运行结果一致的情况下,改变源代码的运行逻辑,输出性能更高的 JavaScript 代码。 实际上 Prepack 就是一个部分求值器,编译代码时提前将计算结果放到编译后的代码中,而不是在代码运行时才去求值。
以如下源码为例:
import React, {Component} from 'react'; import {renderToString} from 'react-dom/server'; function hello(name) { return 'hello ' + name; } class Button extends Component { render() { return hello(this.props.name); } } console.log(renderToString(<Button name='webpack'/>));
被 Prepack 转化后竟然直接输出如下:
console.log("hello webpack");
可以看出 Prepack 通过在编译阶段预先执行了源码得到执行结果,再直接把运行结果输出来以提升性能。
Prepack 的工作原理和流程大致如下:
-
通过 Babel 把 JavaScript 源码解析成抽象语法树(AST),以方便更细粒度地分析源码;
-
Prepack 实现了一个 JavaScript 解释器,用于执行源码。借助这个解释器 Prepack 才能掌握源码具体是如何执行的,并把执行过程中的结果返回到输出中。
从表面上看去这似乎非常美好,但实际上 Prepack 还不够成熟与完善。Prepack 目前还处于初期的开发阶段,局限性也很大,例如:
-
不能识别 DOM API 和 部分 Node.js API,如果源码中有调用依赖运行环境的 API 就会导致 Prepack 报错;
-
存在优化后的代码性能反而更低的情况;
-
存在优化后的代码文件尺寸大大增加的情况。
接入 Webpack
可以使用 prepack-webpack-plugin
。
const PrepackWebpackPlugin = require('prepack-webpack-plugin').default; module.exports = { plugins: [ new PrepackWebpackPlugin() ] };
重新执行构建就能看到输出的被 Prepack 优化后的代码。
9.webpack原理
(1)编写一个Loader
以处理 SCSS 文件为例:
-
SCSS 源代码会先交给 sass-loader 把 SCSS 转换成 CSS;
-
把 sass-loader 输出的 CSS 交给 css-loader 处理,找出 CSS 中依赖的资源、压缩 CSS 等;
-
把 css-loader 输出的 CSS 交给 style-loader 处理,转换成通过脚本加载的 JavaScript 代码;
以上处理 的 Webpack 相关配置如下:
module.exports = { module: { rules: [ { // 增加对 SCSS 文件的支持 test: /\.scss$/, // SCSS 文件的处理顺序为先 sass-loader 再 css-loader 再 style-loader use: [ 'style-loader', { loader:'css-loader', // 给 css-loader 传入配置项 options:{ minimize:true, } }, 'sass-loader'], }, ] }, };
一个 Loader 的职责是单一的,只需要完成一种转换。 如果一个源文件需要经历多步转换才能正常使用,就通过多个 Loader 去转换。 在调用多个 Loader 去转换一个文件时,每个 Loader 会链式的顺序执行 。所以要开发一个Loader,要关注其职责单一性,只需要关注输入和输出。
由于 Webpack 是运行在 Node.js 之上的,一个 Loader 其实就是一个 Node.js 模块,这个模块需要导出一个函数。 这个导出的函数的工作就是获得处理前的原内容,对原内容执行处理后,返回处理后的内容。
一个最简单的 Loader 的源码如下:
module.exports = function(source) { // source 为 compiler 传递给 Loader 的一个文件的原内容 // 该函数需要返回处理后的内容,这里简单起见,直接把原内容返回了,相当于该 Loader 没有做任何转换 return source; };
由于 Loader 运行在 Node.js 中,我们还可以调用任何 Node.js 自带的 API,或者安装第三方模块进行调用:
const sass = require('node-sass'); module.exports = function(source) { return sass(source); };
我们要如何在webpack里面去使用自己编写的Loader,我们可以简单通过在 rule 对象设置 path.resolve
指向这个本地文件 。
{ test: /\.js$/ use: [ { loader: path.resolve('src/config/loader.js'), options: {/* ... */} } ] }
此外webpack也提供了一些api供Loader调用。比如用户像给loader传了一个options参数,我们如何在自己编写的Loader中获取用户传的options,可以这样做:
const loaderUtils = require('loader-utils'); module.exports = function(source) { // 获取到用户给当前 Loader 传入的 options const options = loaderUtils.getOptions(this); return source; };
webpack会在运行Loader的时候默认缓存所有的Loader的处理结果, 也就是说在需要被处理的文件或者其依赖的文件没有发生变化时, 是不会重新调用对应的 Loader 去执行转换操作的。 如果我们不想让webpack缓存Loader的处理结果,可以这样:
module.exports = function(source) { // 关闭该 Loader 的缓存功能 this.cacheable(false); return source; };
其他Loader的api:
-
this.resourcePath
:当前处理文件的路径,例如/src/main.js
。 -
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.context
:当前处理文件的所在目录,假如当前 Loader 处理的文件是/src/main.js
,则this.context
就等于/src
。 -
this.resource
:当前处理文件的完整请求路径,包括 querystring,例如/src/main.js?name=1
。
(2)编写一个插件
webpack
插件由以下组成:
-
一个 JavaScript 命名函数。
-
在插件函数的 prototype 上定义一个
apply
方法。 -
指定一个绑定到 webpack 自身的事件钩子。
-
处理 webpack 内部实例的特定数据。
-
功能完成后调用 webpack 提供的回调。
一个最基础的Plugin可以这么写:
// 一个 JavaScript 命名函数。 function HelloWorldPlugin(options) { // 使用 options 设置插件实例…… } // 在插件函数的 prototype 上定义一个 `apply` 方法。 HelloWorldPlugin.prototype.apply = function(compiler) { // 指定一个挂载到 webpack 自身的事件钩子。 compiler.hooks.emit.tapAsync( 'HelloWorldPlugin', (compilation, callback) => { console.log('这是一个基础插件!'); // 功能完成后调用 webpack 提供的回调。 callback(); } ); }; // 导出 Plugin module.exports = HelloWorldPlugin;
在使用这个 Plugin 时,相关配置代码如下:
const BasicPlugin = require('./src/config/plugin/BasicPlugin.js'); module.export = { plugins:[ new BasicPlugin(options), ] }
Compiler 和 Compilation
在开发 Plugin 时最常用的两个对象就是 Compiler 和 Compilation,它们是 Plugin 和 Webpack 之间的桥梁。 Compiler 和 Compilation 的含义如下:
-
Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;
-
Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。
Compiler 和 Compilation 的区别在于:Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。