# Gulp
思考:
- 假设用户编写的不是通过 CDN 引入库的传统项目,而是通过 NPM 引入库的 Webpack 现代化工程项目。那么用户该如何 使用 该库?
- 用户直接使用
dist/
下打包好的文件,这样岂不是全部加载? - 如果用户只需要库的某些功能,不想打包无用的代码块(类似 Tree Shaking 小节所提到的),那该直接使用
src
文件夹的代码吗? - 目前有什么方法能 按需加载 库的功能并且 正常使用 呢?
# 模拟用户项目
模拟用户一个的 Webpack 项目:在根项目下创建一个 example
文件夹,并且采用 Webpack 将之前的 index.html
拆解一下。为了方便查看产物代码来展开后续章节,需要开启 writeToDisk: true
将编译产物的文件写入磁盘中(默认是在内存中)。每次编译前还需要清除上次编译的产物,webpack.output.clean
无法作用于开发服务器,所以使用 clean-webpack-plugin
代替。
提示
- 这里还是不使用 cli 去运行 Webpack,如有需要自行切换。
- 到后面的章节
example
文件夹会作为 UI 库的调试项目。
# 服务器
npm install express --save-dev
# 自动将产物引入 html 中
npm install html-webpack-plugin --save-dev
# 清除产物
npm install clean-webpack-plugin --save-dev
# webpack 中间件
npm install webpack-dev-middleware --save-dev
2
3
4
5
6
7
8
<!-- example/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body></body>
</html>
2
3
4
5
6
7
8
9
10
11
// example/index.js
// 引入编译好的产物 (全量)
import {
createBackgroudImg,
browser,
createRandomTextElement,
} from "../dist/library";
// 随机数文本节点
var randomText = createRandomTextElement();
document.body.appendChild(randomText);
// 显示浏览器类型
var browserText = document.createElement("div");
browserText.innerHTML = browser();
document.body.appendChild(browserText);
// 添加图片
createBackgroudImg("https://hxin.link/images/avatar.jpg").then(function(el) {
document.body.appendChild(el);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// example/serve.js
var path = require("path");
var express = require("express");
var webpack = require("webpack");
var HtmlWebpackPlugin = require("html-webpack-plugin");
var { CleanWebpackPlugin } = require("clean-webpack-plugin");
var webpackDevMiddleware = require("webpack-dev-middleware");
var app = express();
var compiler = webpack({
mode: "development",
entry: "./example/index.js",
output: {
path: path.resolve(__dirname, "../example/dist"),
filename: "[name].[contenthash].js",
// 无法作用于开发服务器
clean: true, // https://github.com/webpack/webpack-dev-middleware/issues/861
},
plugins: [
// 代替 output.clean
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: "example",
template: "./example/index.html",
}),
],
target: ["web", "es5"],
});
app.use(
// 使用中间件
webpackDevMiddleware(compiler, {
// 将产物写入磁盘
writeToDisk: true,
})
);
// 启动服务
app.listen(3000, function() {
console.log("Example app listening on port 3000!\n");
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
运行 node example/serve.js
,打开 http://localhost:3000/
。在 Chrome、IE 均正常运行。
# 单独编译
思考:
- 如果直接引入转化好的
dist/library.js
,虽然可以运行,但这是一整个加载,没办法按需引用。如果我只用到browser()
浏览器识别的方法,那岂不是有很多未引用的代码? - 如果直接引入
./src
项目下的源文件,这样虽然可以按需加载,删除无用代码,但无法兼容 IE。是否有方法即能实现按需加载和代码兼容呢? (建议尝试) - 使用 Webpack 将每个文件都单独编译?是否能确保文件只是进行语法和 API 转化?
- 使用
run-babel.js
将每个文件都单独编译?类似 Babel-简单的使用 章节的使用?
我们不能局限于 Webpack,这个工具的强项就是把多文件打包成一个,也就是多对一,而不是一对一。如果是一对一,使用 run-babel.js
似乎可以满足,但需要不断的完善其逻辑 (读取文件夹下所有文件,并输出目标文件夹)。这里推荐使用 Gulp (opens new window),一个自动化和增强工作流程的工具包。本人觉得这就像一个私人秘书,它能帮你管理文件、流程记录、行程安排等辅助,但实际上工作内容还得自己去做。
大致的思路: 让 Gulp 管理文件的输入输出,我们在流程上使用 Babel 进行代码转化即可。
同理,我们不使用 gulp-cli
命令和 gulpfile.js
配置文件,在根目录下创建一个 compile.js
文件来运行,相当于 run-babel.js
使用 Gulp 的升级版,不用自己写工具函数来读取和输出文件。例子如下:
# 安装
npm install gulp --save-dev
# 如果通过 cli 运行 Gulp,你还需要安装
npm install gulp-cli --save-dev
2
3
4
// compile.js
// 相当于 run-babel.js 的升级版
var stream = require("stream");
var { src, dest } = require("gulp");
var babel = require("@babel/core");
function compileJS() {
// babel 配置
var config = {
presets: [["@babel/preset-env", { debug: true, targets: "IE >= 11" }]],
plugins: [["@babel/plugin-transform-runtime", { corejs: 3 }]],
};
// 读取文件
src("./src/**/*.js")
.pipe(
// 可以使用 through2 库,会更方便
// 创建转化流,类似于双工流,但其输出是其输入的转换的转换流。
new stream.Transform({
objectMode: true,
transform: function(chunk, encoding, next) {
// 转化逻辑
babel.transform(
chunk.contents.toString(encoding), // 文件内容
config,
(err, res) => {
// 文件内容修改成转化后的代码
chunk.contents = Buffer.from(res.code);
next(null, chunk);
}
);
},
})
)
.pipe(dest("./lib")); // 输出到某文件中
}
// 函数运行
compileJS();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
运行 node compile.js
,执行成功后,根目录会多出 lib
文件夹,里面是 src
文件夹下文件的转化。将 example/index.js
中的库源引入切换至 ./lib
或将 example/serve.js
里的别名调整成 lib
。
// example/index.js
import { createBackgroudImg, browser, createRandomTextElement } from "../lib";
// 或者使用 webpack 的别名功能
// import { createBackgroudImg, browser, createRandomTextElement } from "library";
// ...
2
3
4
5
// example/serve.js
// ...
var compiler = webpack({
// ...
resolve: {
// 设置 library 别名,指向 ../lib
alias: {
library: path.resolve(__dirname, "../lib"),
},
},
// ...
});
2
3
4
5
6
7
8
9
10
11
12
重新运行 node example/serve.js
,IE 运行正常。
# 模块类型
查看 lib
下的产物,发现模块化采用的是 Commonjs。想想在 插件模块化 - Tree Shaking 小节中出现的问题。这个同样存在 Wepack 打包时会将无用代码包含进来的问题。我们可以再提供一个 ES6 模块化方式的文件,这样用户就能根据自身运行环境和需求来选择对应的模块化方式,比较灵活。根据上述需求来改造 compile.js
文件,通过 modules (opens new window) 字段来调整模块化方式:
// compile.js
// 相当于 run-babel.js 的升级版
var stream = require("stream");
var { src, dest } = require("gulp");
var babel = require("@babel/core");
function compileJS(modules) {
// babel 配置
var config = {
presets: [
["@babel/preset-env", { modules, debug: true, targets: "IE >= 11" }],
],
plugins: [["@babel/plugin-transform-runtime", { corejs: 3 }]],
};
// Commonjs 输出至 ./lib
// ES6 Module 输出至 ./es
var path = modules === false ? "./es" : "./lib";
// 读取文件
src("./src/**/*.js")
.pipe(
// 可以使用 through2 库,会更方便
// 创建转化流,类似于双工流,但其输出是其输入的转换的转换流。
new stream.Transform({
objectMode: true,
transform: function(chunk, encoding, next) {
// 转化逻辑
babel.transform(
chunk.contents.toString(encoding), // 文件内容
config,
(err, res) => {
// 文件内容修改成转化后的代码
chunk.contents = Buffer.from(res.code);
next(null, chunk);
}
);
},
})
)
.pipe(dest(path)); // 输出到某文件中
}
// 函数运行
compileJS(false); // ES6 Module
compileJS(); // Commonjs
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
运行 node compile.js
,执行成功后根目录下会有 lib
和 es
两个文件夹,前者是 Commonjs 模块化,后者是 ES6 模块化。可以调整 example/serve.js
中的库别名路径,在 example/index.html
下尝试 ./es
下的代码,在 IE 也是可以正常运行的。
到这里库的基本结构就完成了,你可以继续将这个库分化和改造成 工具函数库、UI 组件库、包装 Node API 的工具库、甚至可以是框架。
提示
后续的章节将会分化和改造成基于某前端框架的 UI 组件库。
# 发布
可以库发布至 NPM (opens new window),公开你的库。基本的流程就是在 NPM 官方注册一个账号,完善 package.json
,然后使用以下命令:
# 登录
npm login
# 发布
npm publish
2
3
4
由于发布方式比较简单和篇幅限制,这里就不详细描述。
提示
在将库提交到 NPM 之前,需要在 package.json
设置 main
和 module
入口路径,前者对应 ./lib
、后者对应 ./es
。可参考 创建库的最终步骤 (opens new window) 和 官方字段介绍 (opens new window)。
← Babel 优化 TypeScript →