diff --git "a/docs/_posts/\347\237\245\350\257\206\345\272\223/Note/Parcel.md" "b/docs/_posts/\347\237\245\350\257\206\345\272\223/Note/Parcel.md" new file mode 100644 index 000000000..3f0aa9cb0 --- /dev/null +++ "b/docs/_posts/\347\237\245\350\257\206\345\272\223/Note/Parcel.md" @@ -0,0 +1,207 @@ +--- +title: 'Webpack 原理和实践' +date: '2020-07-19' +draft: true +tags: + - webpack +--- + +Parcel 是一款完全零配置的前端打包器,它提供了 “傻瓜式” 的使用体验,我们只需了解它提供的几个简单的命令,就可以直接使用它去构建我们的前端应用程序了。 + +下面我们直接来看具体如何去使用 Parcel。 + +### 快速上手 + +这里我们先创建一个空目录,然后通过 npm init 初始化一个项目中的 package.json 文件。 + +完成以后我们就可以安装 Parcel 模块了,具体命令如下: + +``` +$ npm install parcel-bundler --save-dev + +``` + +这里需要注意 Parcel 的 npm 模块名称叫作 parcel\-bundler,我们同样应该将它安装到项目的开发依赖中。 + +安装完成过后,parcel\-bundler 模块就在 node_modules/.bin 目录中提供了一个叫作 parcel 的 CLI 程序,后续我们就是使用这个 CLI 程序执行应用打包。 + +既然是打包应用代码,那我们这里就得先有代码。我们回到项目中创建一些必需的文件,结构如下: + +``` +. +├── src +│ ├── index.html +│ ├── logger.js +│ └── main.js +└── package.json + +``` + +首先在根目录下新建一个 src 目录,用于存放开发阶段编写的源代码,同时创建两个 JS 文件,分别是 logger.js 和 main.js,然后再创建一个 index.html 文件,这个 index.html 文件会将是 Parcel 打包的入口文件。 + +虽然 Parcel 跟 Webpack 一样都支持以任意类型文件作为打包入口,不过 Parcel 官方还是建议我们使用 HTML 文件作为入口。官方的理由是 HTML 是应用在浏览器端运行时的入口。 + +那在这个 HTML 入口文件中,我们可以像平时一样去编写代码,也可以正常去引用资源。在它里面引用的资源,最终都会被 Parcel 打包到一起。 + +我们这里先尝试在 index.html 中引入 main.js 脚本文件,具体代码如下: + +```html + + + + + + Parcel Tutorials + + + + + +``` + +紧接着,在 main.js 中按照 ES Modules 的方式导入 logger.js 中的成员,具体代码如下: + +```js +// ./src/main.js +import { log } from './logger'; +log('hello parcel'); +// ./src/logger.js +export const log = msg => { + console.log('---------- INFO ----------'); + console.log(msg); +}; +``` + +Parcel 同样支持对 ES Modules 模块的打包。 + +完成以后,我们打开命令行终端,然后使用 npx 去运行 node_modules 目录下的 parcel 命令。具体命令如下: + +``` +$ npx parcel src/index.html +``` + +parcel 命令需要我们传入打包入口文件路径,那我们这里就应该是 src/index.html。 + +此时如果执行这个命令,Parcel 就会根据这里传入的参数,先找到 index.html,然后在根据 HTML 中的 script 标签,找到 main.js,最后再顺着 import 语句找到 logger.js 模块,从而完成整体打包。 + +回车执行过后,这里我们发现 Parcel 这个命令不仅仅帮我们打包了应用,而且还同时开启了一个开发服务器,这就跟 Webpack Dev Server 一样。 + +我们打开这个地址,启动浏览器,然后打开开发人员工具。Parcel 同样支持自动刷新这样的功能。具体效果如下: + +![image (3).png](https://s0.lgstatic.com/i/image/M00/15/BE/Ciqc1F7UozCAaPTaAAUS6rk6md0993.png) + +以上就是 Parcel 的基本使用,相比于 Webpack,Parcel 在使用上的确简化了很多。 + +#### 模块热替换 + +如果你需要的是模块热替换的体验,Parcel 中也可以支持。我们回到 main.js 文件中,这里同样需要使用 HMR 的 API。具体代码如下: + +```js +// ./src/main.js +import { log } from './logger'; +log('hello parcel'); +// HMR API +if (module.hot) { + module.hot.accept(() => { + console.log('HMR~'); + }); +} +``` + +我们需要先判断一下 module.hot 对象是否存在,如果存在则证明当前环境可以使用 HMR 的 API,那我们就可以使用 module.hot.accept 方法去处理热替换。 + +不过这里的 accept 方法与 Webpack 提供的 HMR 有点不太一样,Webpack 中的 accept 方法支持接收两个参数,用来处理指定的模块更新后的逻辑。 + +而这里 Parcel 提供的 accept 只需要接收一个回调参数,作用就是当前模块更新或者所依赖的模块更新过后自动执行传入的回调函数,这相比于之前 Webpack 中的用法要简单很多。 + +关于模块更新后的处理逻辑,这里我们就不再过多介绍了,你可以参考我们在 08 课时 Webpack HMR 中的介绍。 + +#### 自动安装依赖 + +除了热替换,Parcel 还支持一个非常友好的功能:自动安装依赖。试想一下,你正在开发一个应用的过程中,突然需要使用某个第三方模块,那此时你就需要先停止正在运行的 Dev Server,然后再去安装这个模块,安装完成过后再重新启动 Dev Server。有了自动安装依赖的功能就不必如此麻烦了。 + +我们回到 main.js 文件中,假设我们这里想要使用一下 jquery。虽然我们并没有安装这个模块,但是因为有了自动安装依赖功能,我们这里只管正常导入,正常使用就好了。具体效果如下: + +![1.gif](https://s0.lgstatic.com/i/image/M00/16/00/Ciqc1F7U03qAE1KxADTVBtOwcSs532.gif) + +在文件保存过后,Parcel 会自动去安装刚刚导入的模块包,极大程度地避免手动操作。 + +#### 其他类型资源加载 + +除此以外,Parcel 同样支持加载其他类型的资源模块,而且相比于其他的打包器,在 Parcel 中加载其他类型的资源模块同样是零配置的。 + +例如我们这里再来添加一个 style.css 的样式文件,并且在这个文件中添加一些简单的样式,具体如下: + +``` + . + ├── src + │ ├── index.html + │ ├── logger.js + │ ├── main.js ++│ └── style.css + └── package.json + +``` + +然后回到 main.js 中通过 import 导入这个样式文件,具体如下: + +```js +// ./src/main.js +import { log } from './logger'; +import './style.css'; +log('hello parcel'); +``` + +保存过后,样式文件可以立即生效。效果如下: + +![image (4).png](https://s0.lgstatic.com/i/image/M00/15/CA/CgqCHl7Uo1eAHNwFAAVD1HZtuN0096.png) + +你会发现,导入样式的操作,整个过程我们并没有停下来做额外的事情。 + +总之,Parcel 希望给开发者的体验就是想做什么,只管去做,其他额外的事情就交给工具来处理。 + +#### 动态导入 + +另外,Parcel 同样支持直接使用动态导入,内部也会自动处理代码拆分,我们也一起来尝试一下。 + +这里我们先将静态导入的 jQuery 注释掉。然后使用动态导入的方式导入 jQuery 模块。具体代码如下: + +```js +// ./src/main.js +// import $ from 'jquery' +import { log } from './logger'; +log('hello parcel'); +import('jquery').then($ => { + $(document.body).append('

Hello Parcel

'); +}); +``` + +import 函数返回的就是一个 Promise 对象,在这个 Promise 对象 then 方法的回调中,我们就能够拿到导入的模块对象了,然后我们就可以把使用 jQuery 的代码移到这个回调函数中。 + +保存过后,回到浏览器,找到开发人员工具的 Network 面板,这里就能够看到拆分出来的 jquery 所对应的 bundle 文件请求了。具体效果如下图: + +![image (5).png](https://s0.lgstatic.com/i/image/M00/15/BE/Ciqc1F7Uo2WASIAhAAGVL6iiNuQ803.png) + +那以上基本上就是 Parcel 最常用的一些特性了,使用上根本没有任何难度,从头到尾我们都只是执行了一个 Parcel 命令。 + +#### 生产模式打包 + +接下来我们来看,Parcel 如何以生产模式打包。生产模式打包,具体命令如下: + +``` +$ npx parcel build src/index.html +``` + +我们只需要执行 parcel build 然后跟上打包入口文件路径,就可以以生产模式运行打包了。 + +这里补充一点,相同体量的项目打包,Parcel 的构建速度会比 Webpack 快很多。因为 Parcel  内部使用的是多进程同时工作,充分发挥了多核 CPU 的性能。 + +> P.S. Webpack 中也可以使用一个叫作 [happypack](https://github.com/amireh/happypack) 的插件实现这一点。 + +那我们这里再来看一下输出的打包结果,具体结果如下: + +![image (6).png](https://s0.lgstatic.com/i/image/M00/15/BE/Ciqc1F7Uo3GARoNnAABz_Av9vqc833.png) + +此时,dist 目录下就都是本次打包的结果了。这里的输出文件也都会被压缩,而且样式代码也会被单独提取到单个文件中。 + +那这就是 Parcel 的体验,整体体验下来就是一个感觉:舒服,因为它在使用上真的太简单了。 diff --git "a/docs/_posts/\347\237\245\350\257\206\345\272\223/Note/\345\256\236\347\216\260\345\260\217\345\236\213\346\211\223\345\214\205\345\267\245\345\205\267.md" "b/docs/_posts/\347\237\245\350\257\206\345\272\223/Note/\345\256\236\347\216\260\345\260\217\345\236\213\346\211\223\345\214\205\345\267\245\345\205\267.md" new file mode 100644 index 000000000..c469f5f7e --- /dev/null +++ "b/docs/_posts/\347\237\245\350\257\206\345\272\223/Note/\345\256\236\347\216\260\345\260\217\345\236\213\346\211\223\345\214\205\345\267\245\345\205\267.md" @@ -0,0 +1,213 @@ +--- +title: 实现小型打包工具 +date: 2020-11-21 +draft: true +--- + +## 实现小型打包工具 + +该工具可以实现以下两个功能 + +- 将 ES6 转换为 ES5 +- 支持在 JS 文件中 `import` CSS 文件 + +通过这个工具的实现,大家可以理解到打包工具的**原理**到底是什么。 + +### 实现 + +因为涉及到 ES6 转 ES5,所以我们首先需要安装一些 Babel 相关的工具 + +```shell +yarn add babylon babel-traverse babel-core babel-preset-env +``` + +接下来我们将这些工具引入文件中 + +```js +const fs = require("fs"); +const path = require("path"); +const babylon = require("babylon"); +const traverse = require("babel-traverse").default; +const { transformFromAst } = require("babel-core"); +``` + +首先,我们先来实现如何使用 Babel 转换代码 + +```js +function readCode(filePath) { + // 读取文件内容 + const content = fs.readFileSync(filePath, "utf-8"); + // 生成 AST + const ast = babylon.parse(content, { + sourceType: "module", + }); + // 寻找当前文件的依赖关系 + const dependencies = []; + traverse(ast, { + ImportDeclaration: ({ node }) => { + dependencies.push(node.source.value); + }, + }); + // 通过 AST 将代码转为 ES5 + const { code } = transformFromAst(ast, null, { + presets: ["env"], + }); + return { + filePath, + dependencies, + code, + }; +} +``` + +- 首先我们传入一个文件路径参数,然后通过 `fs` 将文件中的内容读取出来 +- 接下来我们通过 `babylon` 解析代码获取 AST,目的是为了分析代码中是否还引入了别的文件 +- 通过 `dependencies` 来存储文件中的依赖,然后再将 AST 转换为 ES5 代码 +- 最后函数返回了一个对象,对象中包含了当前文件路径、当前文件依赖和当前文件转换后的代码 + +接下来我们需要实现一个函数,这个函数的功能有以下几点 + +- 调用 `readCode` 函数,传入入口文件 +- 分析入口文件的依赖 +- 识别 JS 和 CSS 文件 + +```js +function getDependencies(entry) { + // 读取入口文件 + const entryObject = readCode(entry); + const dependencies = [entryObject]; + // 遍历所有文件依赖关系 + for (const asset of dependencies) { + // 获得文件目录 + const dirname = path.dirname(asset.filePath); + // 遍历当前文件依赖关系 + asset.dependencies.forEach((relativePath) => { + // 获得绝对路径 + const absolutePath = path.join(dirname, relativePath); + // CSS 文件逻辑就是将代码插入到 `style` 标签中 + if (/\.css$/.test(absolutePath)) { + const content = fs.readFileSync(absolutePath, "utf-8"); + const code = ` + const style = document.createElement('style') + style.innerText = ${JSON.stringify(content).replace(/\\r\\n/g, "")} + document.head.appendChild(style) + `; + dependencies.push({ + filePath: absolutePath, + relativePath, + dependencies: [], + code, + }); + } else { + // JS 代码需要继续查找是否有依赖关系 + const child = readCode(absolutePath); + child.relativePath = relativePath; + dependencies.push(child); + } + }); + } + return dependencies; +} +``` + +- 首先我们读取入口文件,然后创建一个数组,该数组的目的是存储代码中涉及到的所有文件 +- 接下来我们遍历这个数组,一开始这个数组中只有入口文件,在遍历的过程中,如果入口文件有依赖其他的文件,那么就会被 `push` 到这个数组中 +- 在遍历的过程中,我们先获得该文件对应的目录,然后遍历当前文件的依赖关系 +- 在遍历当前文件依赖关系的过程中,首先生成依赖文件的绝对路径,然后判断当前文件是 CSS 文件还是 JS 文件 + - 如果是 CSS 文件的话,我们就不能用 Babel 去编译了,只需要读取 CSS 文件中的代码,然后创建一个 `style` 标签,将代码插入进标签并且放入 `head` 中即可 + - 如果是 JS 文件的话,我们还需要分析 JS 文件是否还有别的依赖关系 + - 最后将读取文件后的对象 `push` 进数组中 + +现在我们已经获取到了所有的依赖文件,接下来就是实现打包的功能了 + +```js +function bundle(dependencies, entry) { + let modules = ""; + // 构建函数参数,生成的结构为 + // { './entry.js': function(module, exports, require) { 代码 } } + dependencies.forEach((dep) => { + const filePath = dep.relativePath || entry; + modules += `'${filePath}': ( + function (module, exports, require) { ${dep.code} } + ),`; + }); + // 构建 require 函数,目的是为了获取模块暴露出来的内容 + const result = ` + (function(modules) { + function require(id) { + const module = { exports : {} } + modules[id](module, module.exports, require) + return module.exports + } + require('${entry}') + })({${modules}}) + `; + // 当生成的内容写入到文件中 + fs.writeFileSync("./bundle.js", result); +} +``` + +这段代码需要结合着 Babel 转换后的代码来看,这样大家就能理解为什么需要这样写了 + +```js +// entry.js +var _a = require("./a.js"); +var _a2 = _interopRequireDefault(_a); +function _interopRequireDefault(obj) { + return obj && obj.__esModule ? obj : { default: obj }; +} +console.log(_a2.default); +// a.js +Object.defineProperty(exports, "__esModule", { + value: true, +}); +var a = 1; +exports.default = a; +``` + +Babel 将我们 ES6 的模块化代码转换为了 CommonJS。但是浏览器是不支持 CommonJS 的,所以如果这段代码需要在浏览器环境下运行的话,我们需要自己实现 CommonJS 相关的代码,这就是 `bundle` 函数做的大部分事情。 + +接下来我们再来逐行解析 `bundle` 函数 + +- 首先遍历所有依赖文件,构建出一个函数参数对象 + +- 对象的属性就是当前文件的相对路径,属性值是一个函数,函数体是当前文件下的代码,函数接受三个参数 + + `module`、`exports`、 `require` + + - `module` 参数对应 CommonJS 中的 `module` + - `exports` 参数对应 CommonJS 中的 `module.export` + - `require` 参数对应我们自己创建的 `require` 函数 + +- 接下来就是构造一个使用参数的函数了,函数做的事情很简单,就是内部创建一个 `require` 函数,然后调用 `require(entry)`,也就是 `require('./entry.js')`,这样就会从函数参数中找到 `./entry.js` 对应的函数并执行,最后将导出的内容通过 `module.export` 的方式让外部获取到 + +- 最后再将打包出来的内容写入到单独的文件中 + +如果你对于上面的实现还有疑惑的话,可以阅读下打包后的部分简化代码 + +```js +(function (modules) { + function require(id) { + // 构造一个 CommonJS 导出代码 + const module = { exports: {} }; + // 去参数中获取文件对应的函数并执行 + modules[id](module, module.exports, require); + return module.exports; + } + require("./entry.js"); +})({ + "./entry.js": function (module, exports, require) { + // 这里继续通过构造的 require 去找到 a.js 文件对应的函数 + var _a = require("./a.js"); + console.log(_a2.default); + }, + "./a.js": function (module, exports, require) { + var a = 1; + // 将 require 函数中的变量 module 变成了这样的结构 + // module.exports = 1 + // 这样就能在外部取到导出的内容了 + exports.default = a; + }, + // 省略 +}); +```