在开发的工程中,线上环境须要引入一些统计和打印日志的js文件。可是对于开发环境,加速打包速度减小页面渲染时间很关键。我因而想根据开发环境,写一个简单的loader,按需加载一些资源。前端
例如:在index.js中,用自定义函数envLoader添加资源node
index.jsreact
//......
envLoader(
'/vendor/log.js'
)
//......
复制代码
为了完成按需加载的功能。打算使用自定义的loader。 实现思路以下:webpack
结合官网的loader api了解webpack loader的工做原理。git
将使用如下apigithub
开始撸一个本身的loader (^-^)Vweb
loader 用于对模块的源代码进行转换。loader 可使你在 import 或"加载"模块时预处理文件。所以,loader 相似于其余构建工具中“任务(task)”,并提供了处理前端构建步骤的强大方法。loader 能够将文件从不一样的语言(如 TypeScript)转换为 JavaScript,或将内联图像转换为 data URL。npm
loader 是导出为一个函数的 node 模块。该函数在 loader 转换资源的时候调用。给定的函数将调用 loader API,并经过 this 上下文访问。json
loader是一个node module,那么它的基本形式以下api
module.exports = function(source) {
return source;
};
复制代码
return
或者 this.callback(err, value…)
将代码返回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);
});
};
复制代码
官网上介绍了配给单个和多个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环境。
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-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 parser把js程序转换成描述程序语法结构的语法树(AST)。产生的语法树对于从程序转换到静态程序分析的各类用途都颇有用。
以前写过一篇介绍AST的文章 点击连接查看,这里就不详细展开。
使用方法:
esprima.parseScript(input, config, delegate)
esprima.parseModule(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
以前计算散列的,因此咱们必须避免绝对路径,以确保不一样编译之间的哈希一致。
不要在模块代码中插入绝对路径,由于当项目根路径变化时,文件绝对路径也会变化。
//获取当前路径下的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地址 详细实现过程点这里!!