# Gulp

思考:

  1. 假设用户编写的不是通过 CDN 引入库的传统项目,而是通过 NPM 引入库的 Webpack 现代化工程项目。那么用户该如何 使用 该库?
  2. 用户直接使用 dist/ 下打包好的文件,这样岂不是全部加载?
  3. 如果用户只需要库的某些功能,不想打包无用的代码块(类似 Tree Shaking 小节所提到的),那该直接使用 src 文件夹的代码吗?
  4. 目前有什么方法能 按需加载 库的功能并且 正常使用 呢?

# 模拟用户项目

模拟用户一个的 Webpack 项目:在根项目下创建一个 example 文件夹,并且采用 Webpack 将之前的 index.html 拆解一下。为了方便查看产物代码来展开后续章节,需要开启 writeToDisk: true 将编译产物的文件写入磁盘中(默认是在内存中)。每次编译前还需要清除上次编译的产物,webpack.output.clean 无法作用于开发服务器,所以使用 clean-webpack-plugin 代替。

提示

  1. 这里还是不使用 cli 去运行 Webpack,如有需要自行切换。
  2. 到后面的章节 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
1
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>
1
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);
});
1
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");
});
1
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 均正常运行。

# 单独编译

思考:

  1. 如果直接引入转化好的 dist/library.js,虽然可以运行,但这是一整个加载,没办法按需引用。如果我只用到 browser() 浏览器识别的方法,那岂不是有很多未引用的代码?
  2. 如果直接引入 ./src 项目下的源文件,这样虽然可以按需加载,删除无用代码,但无法兼容 IE。是否有方法即能实现按需加载和代码兼容呢? (建议尝试)
  3. 使用 Webpack 将每个文件都单独编译?是否能确保文件只是进行语法和 API 转化?
  4. 使用 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
1
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();
1
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";
// ...
1
2
3
4
5







 





// example/serve.js
// ...
var compiler = webpack({
  // ...
  resolve: {
    // 设置 library 别名,指向 ../lib
    alias: {
      library: path.resolve(__dirname, "../lib"),
    },
  },
  // ...
});
1
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
1
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,执行成功后根目录下会有 libes 两个文件夹,前者是 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
1
2
3
4

由于发布方式比较简单和篇幅限制,这里就不详细描述。

提示

在将库提交到 NPM 之前,需要在 package.json 设置 mainmodule 入口路径,前者对应 ./lib、后者对应 ./es。可参考 创建库的最终步骤 (opens new window)官方字段介绍 (opens new window)