Webpack 入门
前言
有句话说的好:
懒是技术的第一推动力。
对于程序员来说,很多代码写过一次,就不想再写下一次,很多事做一次就不想重复去做,而且他们总有办法偷懒。
随着编写的代码变得越来越庞大和复杂,代码维护、打包、发布等流程也变得极为繁琐,这个时候,前端自动化工具就被创建出来了,Webpack 就是其中之一的自动化构建工具。
Webpack
一幅图来了解它:
概念
webpack 有四个核心概念:
* entry
* output
* loader
* plugins
entry 表示 webpack 以哪个模块来作为构建依赖关系图的起点,可以有一个或多个起点;
ouput 表示 webpack 构建之后输出文件,可以指定文件名和文件保存路径;
loader 表示 webpack 如何去处理依赖模块的源文件,如 js、css、ts、less 等文件;
plugins 用于扩展 webpack 功能的,他们在整个构建过程中生效,执行相关任务,具体在哪个wepack 生命周期需要看具体实现。
入门配置
一个基础的 webpack 配置文件(webpack.config.js):
var path = require('path')
var webpack = require('webpack')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var UglifyPlugin = require('uglifyjs-webpack-plugin');
var HtmlPlugin = require('html-webpack-plugin');
var TARGET = process.env.npm_lifecycle_event
var APP_PATH = path.join(__dirname, '/src')
var isBuild = TARGET === 'build';
module.exports = function getConfig(){
var config = {};
config.entry = path.join(APP_PATH, 'index.js');
config.output = {
path: path.join(__dirname, '/dist'),
filename: 'main.js'
};
config.module = {
rules: [
{
test: /\.css$/,
use: isBuild ? ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [
'css-loader'
]
}) : ['style-loader', 'css-loader']
}
]
}
config.plugins = [];
if (isBuild) {
config.plugins.push(
new UglifyPlugin(),
new ExtractTextPlugin('main.css')
)
} else {
config.plugins.push(new HtmlPlugin());
}
config.devServer = { // webpack-dev-server
contentBase: path.join(__dirname, '/dist'), // 本地服务器所加载的页面所在目录
historyApiFallback: true, // 不跳转
inline: true // 实时刷新
}
return config;
}();
起点(entry):是 src 目录下的 index.js 文件
输出(output):是在项目下 dist 目录生成一个 main.js 文件
加载器 (loader):对于 css 文件一定要使用 css-loader、style-loader,如果不使用的话项目生成是看不到样式文件的
插件(plugins):插件 plugins 是一个数组形式,如果我们想用一个插件,只需要require(“plugin”),然后 new plugin() 创建一个实例就行,他会在构建过程中处理任务
由于想要在 npm run dev 和 npm run build 时候是不同的情况,生产环境(build)的时候分离 css 代码和压缩 js 代码,开发的时候(dev)不做压缩和分离处理。所以对 loader 的处理规则做了判断,还有据构建环境(process.env.npm_lifecycle_event)引入不同的插件。
webpack-dev-server 是小型的 Node.js Express 服务器,在开发的时候使用能够方便调试代码。
当然,实际的项目的配置文件不可能这么简单,可能还有其他要求,比如多个 entry,公共库等情况处理,需要不断的去学习。
编写一个 loader
对于特定文件的处理,我们可以自己编写一个 loader 进行处理,webpack 允许我们这么做,查看 官方教程
一个例子:
有一个 index.js 文件,只有一行:
module.exports = 'hello world'
编写一个 define-loader 去获取数据
在项目根目录下创建一个 loader 目录,里面创建一个名为 define-loader.js 文件,内容如下:
module.exports = function (source) {
// console.log(source) // 打印:module.exports = 'hello world'
if (~this.request.indexOf('webpack/buildin/global.js')) {
return source
} else {
return `
global.define(function (module, exports) {
${source}
})`
}
}
loader 只是一个导出为函数的 JavaScript 模块,这个函数接受的参数是源文件的字符串,返回经过处理后的文件。
上面我们使用了global对象,所以它会把 global.js 引入进去。
webpack.config.js 配置如下:
const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
path: path.join(__dirname, 'dist'),
filename: 'index.js'
},
module: {
rules: [
{
test: /\.js/,
use: [
'define-loader'
]
}
]
},
resolveLoader: {
modules: [
'node_modules',
path.resolve(__dirname, 'loader')
]
}
}
配置 resolveLoader.modules ,webpack 将会从 loader 目录中搜索 define-loader
测试(test.js)如下:
const assert = require('assert')
let mod = null
global.define = function (fn) {
const module = { exports: {} }
fn(module, module.exports)
mod = module
}
describe('loader', function () {
it('可以通过 define 拿到数据', function () {
require('../dist/index')
assert.equal(mod.exports, 'hello world')
})
})
以上在 mac 系统上测试没有问题,但是在 windows 系统会报错,说 global 找不到。
使用如下配置可解决 windows 问题:
// 在webpack.config.js 中添加
target: 'node',
node: {
global: true
}
更详细的 loader API 可以帮助写复杂的 loader
编写一个 plugin
webpack有很多内置插件比如 BannerPlugin,同时有很多第三方插件如上面基础配置使用到了压缩 js 代码的插件(uglifyjs-webpack-plugin),单独打包 css 文件的插件(extract-text-webpack-plugin)。如何编写,查看 官方教程
一个例子实现上面的 define-loader 功能:
我们的 plugin.js 实现:
const ConcatSource = require('webpack-sources').ConcatSource
class DefPlugin {
constructor(name) {
this.name = name;
}
apply(compiler) {
compiler.plugin('compilation', (compilation) => { // Compilation creation completed
compilation.templatesPlugin("render-with-entry", (source, chunk, hash) => {
// console.log(source);
if(this.name) {
return new ConcatSource(`global.define([${JSON.stringify(this.name)}, 'module'], function(${this.name}, module) { `, source, "});");
} else {
return new ConcatSource(`global.define(['module'], function(module) { `, source, "});");
}
});
});
}
}
module.exports = DefPlugin
webpack.config.js 配置如下:
const path = require('path')
const Plugin = require('./plugin')
module.exports = {
context: path.join(__dirname, 'src'),
entry: './index.js',
output: {
libraryTarget: 'commonjs2', //表示生成文件使用 commonjs2 规范
path: path.join(__dirname, 'dist'),
filename: './bundle.js'
},
plugins: [
// new Plugin
new Plugin("exports")
]
}
测试(test.js)如下:
let mod = null
const assert = require('assert')
describe('plugin', function () {
before(() => {
global.define = function (deps, fn) {
const module = { exports: {} }
const args = deps.map(key => {
if (key === 'require') return function () {}
if (key === 'module') return module
if (key === 'exports') return module.exports
})
fn.apply(null, args)
mod = module
}
})
it('可以不使用 Object.definePropery 中的 get 参数', function () {
const a = require('../dist/bundle')
assert.equal(mod.exports, 'hello world')
})
after(() => {
global.define = null
delete global.define
})
})
由于 plugins 运行于 webpack 整个构建过程中,在 webpack 任何一个构建时期都可以去处理相应的任务,所以想要编写一个插件,必须了解 webpack 的生命周期(姑且这么称呼),需阅读源码。
webpack 提供了两个很重要的对象 compiler 和 compilation ,理解他们对于我们理解 webpack 的生命周期很有帮助
* compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,当在 webpack 环境中应用一个插件时,插件将收到一个编译器对象的引用。 [Compiler API ]
* compilation 对象继承于 compiler,代表了一次单一的版本构建和生成资源。在这里,可以对资源的编译和生成的文件进行处理。 [Compilation API]
从上面插件代码中可以看到有 compiler.plugin 的使用,这是因为它继承了 Tapable 类,webpack 中很多对象继承了 Tapable 类,暴露了一个 plugin 的方法,这样可以注入自定义构建步骤,所以掌握 Tapable 插件类也是很重要的,Tapable 源码。
总结
webpack 是一个可扩展的构建工具,我们除了可以使用内置的 plugins和 loader, 还可以自定义自行编写,有效的使用它能够提升实际项目的开发效率。