写在前面:
在开发的工程中,线上环境需要引入一些统计和打印日志的js文件。但是对于开发环境,加速打包速度减少页面渲染时间很关键。我于是想根据开发环境,写一个简单的loader,按需加载一些资源。
例如:在index.js中,用自定义函数envLoader添加资源
index.js
//......
envLoader(
'/vendor/log.js'
)
//......
复制代码
为了完成按需加载的功能。打算使用自定义的loader。 实现思路如下:
- 添加js loader 对index.js进行处理
- 解析envLoader函数
- 拿到传入的参数并根据环境判断是否加载。
结合官网的loader api了解webpack loader的工作原理。
将使用以下api
- loader-utils
- schema-utils
- this.async
- this.cacheable
- getOptions
- validateOptions
- urlToRequest
开始撸一个自己的loader (^-^)V
Webpack Loader
loader 用于对模块的源代码进行转换。loader 可以使你在 import 或"加载"模块时预处理文件。因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的强大方法。loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript,或将内联图像转换为 data URL。
一、基本用法
loader 是导出为一个函数的 node 模块。该函数在 loader 转换资源的时候调用。给定的函数将调用 loader API,并通过 this 上下文访问。
loader是一个node module,那么它的基本形式如下
module.exports = function(source) {
return source;
};
复制代码
- loader只能传入一个包含包含资源文件内容的字符串(source)
- 如果是同步loader,可以用
return
或者this.callback(err, value…)
将代码返回 - 异步loader:在一个异步的模块中,回传时需要调用 Loader API 提供的回调方法
this.async
来获取 callback 函数:
module.exports = function(content, map, meta) {
var callback = this.async();
someAsyncOperation(content, function(err, result) {
if (err) return callback(err);
callback(null, result, map, meta);
});
};
复制代码
二、在webpack中引入loader
官网上介绍了配给单个和多个loader的方法。
主要原理是path.resolve
方法,给loader添加路径。也可以使用resolveLoader.modules
统一配置多个loader的路径。
webpack会在这些目录中搜索loaders,我在项目中新建了loaders本地目录,并修改文件如下:
webpack.config.js
module.exports = {
//...
resolveLoader: {// 配置查找loader的目录
modules: [
'node_modules',
path.resolve(__dirname, 'src', 'loaders')
]
},
module: {
rules:[
{
test: /\.js$/,
use: [
{
loader: 'env-loader',
options: {
env: process.env.NODE_ENV
}
},
{
loader:'babel-loader',
options: {
presets: ['env','es2015','react'],
}
},
]
}]
},
//...
};
复制代码
注意:loader的执行方式是从右到左,链式执行,上一个 Loader 的处理结果给下一个接着处理
在package.json中定义了根据环境打包的命令
"scripts": {
"webpack": "cross-env NODE_ENV=development webpack-dev-server --open --mode development",
"test": "cross-env NODE_ENV=test webpack --mode development",
"dev": "cross-env NODE_ENV=dev webpack --mode development",
"prd": "cross-env NODE_ENV=prd webpack --mode development",
"boot":"cross-env NODE_ENV=boot webpack --mode development"
},
复制代码
通过设置NODE_ENV
来区分dev、prd环境。
Q1:怎么获取命令中设置的环境参数
A1:process.env
对象上可以获取到打包时定义的NODE_ENV,在webpack.config.js中引入env-loader的时候,可以将参数传递给 loader 的options
选项。
webpack.config.js
{
loader: 'env-loader',
options: {
env: process.env.NODE_ENV
}
},
复制代码
三、使用loader工具库,解析loader传参
- loader-utils 包。它提供了许多有用的工具,但最常用的一种工具是获取传递给 loader 的选项
- schema-utils 包配合 loader-utils,用于保证 loader 选项,进行与 JSON Schema 结构一致的校验
在loader中使用loader-utils包的getOptions
方法,拿到loader的option选项({env:'dev'}。用schema-utils 包配合 loader-utils,用于保证 loader 选项,进行与 JSON Schema结构一致的校验
。在index.js中添加这两个包:
env-loader/index.js
const loaderUtils = require('loader-utils')
const validate = require('schema-utils');
let json = {
"type": "object",
"properties": {
"content": {
"type": "string"
}
}
}
module.exports = function(source) {
this.cacheable();
let callback = this.async();
let options = loaderUtils.getOptions(this) //{env:'dev'}
validate(json, options, "env-loader");
}
复制代码
四、使用esprima解析js节点
Esprima parser把js程序转换成描述程序语法结构的语法树(AST)。产生的语法树对于从程序转换到静态程序分析的各种用途都很有用。
之前写过一篇介绍AST的文章 点击链接查看,这里就不详细展开。
使用方法:
esprima.parseScript(input, config, delegate)
esprima.parseModule(input, config, delegate)
复制代码
- input入是表示要解析的程序的字符串
- config是用于自定义解析行为的对象(可选)
- delegate是为每个节点调用的回调函数(可选)
将source作为input参数,程序将会被解析成AST。
node返回每个节点对应的Syntax,meta是节点在程序中的具体位置。
esprima.parseModule(source, {}, async(node, meta)=> {
console.log(node.meta)
//....
})
复制代码
解析结果如下:
分析每个节点的Syntax是否满足判断条件,这里判断node的type类型和正在执行的函数callee的name==='envLoader'和type==='Identifier',对满足条件的节点进行处理。
function judgeType(node) {
return (node.type === 'CallExpression')
&& (node.callee.name === 'envLoader')
&& (node.callee.type === 'Identifier')
}
if (judgeType(node)) {
flag = true
node.arguments.map(argument=>{
entries.push({
val: argument.value,
start: meta.start.offset,
end: meta.end.offset
});
})
}
复制代码
五、文件的路径处理
在节点分析中,拿到了自定义envLoader函数中传入的外部资源地址,接下来要再loader中。
在loader中一般使用require()
或者import
方法。这是因为webpack是在将模块路径转换为模块id
之前计算散列的,所以我们必须避免绝对路径,以确保不同编译之间的哈希一致。
不要在模块代码中插入绝对路径,因为当项目根路径变化时,文件绝对路径也会变化。
loaderUtils.urlToRequest可以将一些资源URL转换为webpack模块请求。
//获取当前路径下的src文件夹
let downloadPath = path.resolve(process.cwd(), 'src')
if(env == 'prd'){
//如果是prd环境
//使用loaderUtils将请求转换为module
const saveUrl = loaderUtils.urlToRequest(`${extName}`,downloadPath);// "path/to/module.js"
//将转换好的module引入
var replaceText = `import "${saveUrl}"`
}else{
//其他环境
var replaceText = 'function envLoad(){}'
}
//将envLoader函数替换
source = source.replace(transText, replaceText);
复制代码
六、测试
完成上面的步骤,已经开发完成了一个简单的loader,并且可以在本地运行。接下来让我们用一个简单的单元测试,来保证 loader 能够按照我们预期的方式正确运行。
我们将使用 Jest 框架。然后还需要安装 babel-jest 和允许我们使用 import / export 和 async / await 的一些预设环境(presets)。
6.1 安装依赖
npm install --save-dev jest babel-jest babel-preset-env
复制代码
.babelrc
{
"presets": [[
"env",
{
"targets": {
"node": "4"
}
}
]]
}
复制代码
我们的 loader 将会处理 .js 文件,并且将任何实例中的
envLoader('xxx')
复制代码
在开发环境下替换成function envLoad(){}
,在生产环境下替换成 import '路径/xxx.js'。
在test文件夹下新建example.js
envLoader(
'/vendor/lodash.min.js'
)
复制代码
我们将会使用 Node.js API 和 memory-fs 去执行 webpack。
npm install --save-dev webpack memory-fss
复制代码
test/compiler.js
import path from 'path';
import webpack from 'webpack';
import memoryfs from 'memory-fs';
export default (fixture, options = {}) => {
const compiler = webpack({
context: __dirname,
entry: `./${fixture}`,
output: {
path: path.resolve(__dirname),
filename: 'bundle.js',
},
module: {
rules: [{
test: /\.js$/,
use: {
loader: path.resolve(__dirname, '../src/loaders/env-loader'),
options: {
env: process.env.NODE_ENV
}
}
}]
}
});
compiler.outputFileSystem = new memoryfs();
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
if (err || stats.hasErrors()) reject(err);
resolve(stats);
});
});
};
复制代码
最后,我们来编写测试,并且添加 npm script 运行它。
import compiler from './compiler.js';
test('envLoader to import', async () => {
const stats = await compiler('example.js');
const output = stats.toJson().modules[0].source;
if(process.env.NODE_ENV == 'prd'){
expect(output).toBe('import "/Users/yuan/Documents/yuanyuan/Project/env-loader/src/vendor/lodash.min.js"');
}else{
expect(output).toBe('function envLoad(){}');
}
});
复制代码
package.json
{
"scripts": {
"test-boot": "cross-env NODE_ENV=boot jest",
"test-prd": "cross-env NODE_ENV=prd jest"
}
}
复制代码
分别运行两个script
各自验证成功~测试通过
env-loader地址 详细实现过程点这里!!