抛开脚手架,回归本源手动构建前端工程

前言

现在工作中大家大部分时间都在堆业务代码,有了新项目创建工程也是各种脚手架一路“回车”到底。但是如果开发过程中有点小变数,想要改造可能就两眼一抹黑了。(这其实是我前段时间的囧境,所以才有了这篇文章)

在开发中用脚手架方便的同时,你知否知道脚手架在你看不见的地方干了多少事情,如果他的某个环节不符合你的需求,你知道怎么搞定他吗?

所以抛开一切工具回归本源,从最原始的操作中梳理基础的前端工程知识体系。是你架构自己的体系化知识必不可少的一步。所以接下来让我们一步一步用最原始的方式梳理一遍。

注意 ①:在开始之前,先要明确一个说法「前端库」和「前端应用」的区别:即 library 和 application。(当然这两种前端工程实质没有太严格的界线都是前端工程,其实是可以互相转换的,但是为了明确目标我们还是区分说明)

明确目标

本文前半部分将创建的是一个库(library)。
后半部分会创建一个示例应用(application)来使用验证前面的库。
本文结束之后整个代码工程的最终形态便是常见的脚手架初始化项目之后输出产物(即便有差异也是换汤不换药)。

正式开始:library

初始化工程

至于使用 yarn 还是 npm 随个人习惯。过程不同但结果一样。

1. yarn init

初始化工程 package.json

➜  yarn init
yarn init v1.22.10
question name (react-lib-ts):
question version (1.0.0):
question description: react library with typescript 演示项目
question entry point (index.js):
question repository url:
question author: wjh4dev
question license (MIT):
question private: true
success Saved package.json
✨  Done in 42.28s.
复制代码

添加 TypeScript 并配置

添加 ts 有两方面原因,第一,静态类型检查;第二,模块化。

2. yarn add -D typescript

添加 ts 依赖。

3. touch tsconfig.json

添加 ts 配置文件。

4. vim tsconfig.json

修改 ts 配置文件内容。具体配置如下:

{
  "compilerOptions": {
    "outDir": "lib/esm",
    "module": "esnext",
    "target": "es5",
    "lib": ["es6", "dom", "es2016", "es2017"],
    "jsx": "react",
    "declaration": true,
    "moduleResolution": "node",
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "esModuleInterop": true,
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "suppressImplicitAnyIndexErrors": true,
    "allowSyntheticDefaultImports": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "lib"]
}
复制代码
TypeScript 配置说明
  • outDir: 输出文件夹
  • module: 设置编译代码的模块系统
  • target: 期望编译 TypeScript 到 ECMAScript 的目标版本
  • lib: 要支持的内置 js 库
  • include: 编译器要扫描的文件
  • exclude: 编译器要忽略的文件

编写业务代码

5. mkdir src && touch src/index.ts

6. vim src/index.ts

编写具体业务代码。先写个 ts 试验一把,后面会引入 React 改为 tsx 组件。

export function sayHello(name: string) {
  return `Hey ${name}, say hello to TypeScript.`;
}
复制代码

7. vim package.json

写好了业务代码之后,在 package.json 中增加 build 脚本。

  "scripts": {
    "build": "tsc"
  },
复制代码

编译构建

8. yarn build

开始编译构建业务代码。也就是说执行构建其实是使用了 tsc 命令。
注:在本地运行 tsc 将编译由 tsconfig.json 定义的项目。

➜  yarn build
yarn run v1.22.10
$ tsc
✨  Done in 2.09s.
复制代码

编译好之后工程的目录结构如下:

➜  tree -I node_modules
.
├── lib
│   └── esm
│       ├── index.d.ts
│       └── index.js
├── package.json
├── src
│   └── index.ts
├── tsconfig.json
└── yarn.lock

3 directories, 6 files
复制代码

大家会发现 lib/esm 文件夹下生成了两个文件:

  • index.js – 编译输出的文件.
  • index.d.ts – 你代码的类型定义文件.

+ 注:JavaScript 模块类型

虽然我们在上一步完成了 TypeScript 的设置并添加了一个构建脚本,但还没有完全准备好。为了能够将我们的包发布到 NPM,还需要了解 JavaScript 生态系统中可用的不同类型的模块-

  • CommonJS:最常用在使用 require 函数的 Node 环境中。即便我们要发一个 React 模块(这个模块是由 ESM 格式编写的应用程序,且由 webpack 之类的工具打包和编译),我们也得考虑它有可能在服务端渲染中使用。因此可能需要一个 CJS 对应的库。
  • ESM:是我们通常在 React 应用程序中使用的现代模块格式,在这些模块中使用各种 importexport 语句来定义。输出 ES 模块最主要的好处是可以使库拥有 tree-shakable 的特性。当然 Rollupwebpack 2+ 等工具都支持这个特性。
  • UMD:这种模块格式现在不那么流行了。当用户在 html 中使用 script 引入模块是才需要这种格式。

因此,我们的包将添加对 ESMCommonJS 模块的支持。其实上一步构建结果 lib/esm 里的 index.js,就是 esm 格式,因为我们在 tsconfig.json 中将模块类型指定为 esnext。接下来我们要再改造一下构建脚本,使其可以生成两种模块格式。

9. vim package.json 修改构建脚本

  "scripts": {
    "build": "yarn build:esm && yarn build:cjs",
    "build:esm": "tsc",
    "build:cjs": "tsc --module commonjs --outDir lib/cjs"
  },
复制代码

10. vim package.json 修改 package.json 入口

"main": "./lib/cjs/index.js",
"module": "./lib/esm/index.js",
"types": "./lib/esm/index.d.ts",
复制代码

编写业务组件

上一步就写过业务组件只不过是 ts 格式的,这次我们将改造成 tsx 格式的组件。

11. vim package.json 添加 React

"peerDependencies": {
    "react": "^16.8.0",
    "react-dom": "^16.8.0"
 },
复制代码

+ 注:peerDependencies

这里就需要解释一下 peerDependencies 了。
stackoverflow 有个问答说明的很清楚,有兴趣的可以查看原文。但是回答有点长,重点总结一下:

  1. 应用运行时必须的,写在 "dependencies": {} 里。比如:lodash,注意不要把测试工具编译器放在 "dependencies": {} 里。他们都是开发时依赖的放在 "devDependencies": {} 即可。
  2. 开发时需要,但运行时用不上的,写在 "devDependencies": {} 里。比如:typescript,因为 ts 最终都会编译成 js 执行,运行时就没 ts 什么事了。还比如测试工具也同理。
  3. 运行时需要,但是我自己不附带,我知道你的环境里肯定得有这个包,这种情况写在 "peerDependencies": {} 里。举个例子:比如你要写一个 react 应用,要用到 A 这个库,A 也依赖 react,但是安装 A 的时候,A 不附带 react,因为 A 知道你的环境里一定有 react,所以 A 只需要告诉你它最低依赖的 react 是什么版本就可以了。而不需要自带 react。
  • 以上 1、2、3 便是:dependenciesdevDependenciespeerDependencies 的区别。

12. yarn add --dev react-dom react @types/react-dom @types/react

添加 react typescript 开发依赖

13. touch src/index.tsx

编写 React 组件,替换之前的 src/index.ts

import React from "react";

const SayHelloWorld = ({ name }: { name: string }): JSX.Element => (
  <div>Hey {name}, This is React Library with TypeScript.</div>
);

export default SayHelloWorld;
复制代码

再次编译构建

14. yarn build

此时目录结构如下:我们有了 esm 和 cjs 两种模块格式

➜  tree -I node_modules
.
├── lib
│   ├── cjs
│   │   ├── index.d.ts
│   │   └── index.js
│   └── esm
│       ├── index.d.ts
│       └── index.js
├── package.json
├── src
│   └── index.tsx
├── tsconfig.json
└── yarn.lock

4 directories, 8 files
复制代码

至此我们的库就算基本开发完成,并且也打包构建输出了想要的模块代码。但是这还不算真正的完成,接下里我们还需要验证开发的库是否正常可用。


测试&使用 lib

库开发完了,我们需要使用,并且测试。由于大部分库都是没有页面入口的,所以需要有一个演示应用来使用并检验开发的库是否可用。这时候就该 example 出场了。

接下来在当前这个库的工程根目录里新建一个新的子工程:即 example 。其实这里相当于一个新的工程展开来写篇幅与上文不相上下,但是分开就没办法对比来看。所以我把这两种工程放在一起对比着来看。

注意 ②:这个 example 是个 application(与开头的 library 呼应上了,对比两种工程区别来看)

正式开始:application

初始化工程 app

1. mkdir example

开局第一步先来个目录,记得是在原有 library 的根目录创建。接下来就都是在 example 里操作了。

cd example

2. yarn init

同样是初始化演示工程 package.json

➜  yarn init
yarn init v1.22.10
question name (example): react-lib-ts-example
question version (1.0.0):
question description: react lib 的演示 application
question entry point (index.js):
question repository url:
question author:
question license (MIT):
question private:
success Saved package.json
✨  Done in 42.89s.
复制代码

3. mkdir public src

创建工程目录结构

4. touch public/index.html

public 下创建 index.html,后续 public 文件夹将放置所有的静态资源,我们的 web app 最终也将从这里渲染。

index.html 内容如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, shrink-to-fit=no"
    />
    <title>React Web App</title>
  </head>

  <body>
    <div id="root"></div>
    <noscript> You need to enable JavaScript to run this app. </noscript>
    <script src="../dist/bundle.js"></script>
  </body>
</html>
复制代码

注意上述 html 文件中引用了一个 <script src="https://juejin.cn/dist/bundle.js"></script>,这个 bundle.js 可以随意命名。

现在已经设置好了 web app 的入口,接下来就是重点的来了。为了确保我们编写的代码可以编译,我们需要邀请 Babel 出场了。

添加 Babel 及其配置

5. yarn add -D @babel/core @babel/cli @babel/preset-env @babel/preset-react

邀请老大哥 Babel 上场。前面也说过我们写 web app 工程要么是 ESnext,要么都是 React,

  • babel-core 是主要的包:我们需要它来让 babel 对我们的代码进行任何转换。
  • babel-cli 允许您从命令行编译文件。
  • preset-reactpreset-env 都是转换特定代码风格的预设-在这种情况下,
  • env 预设允许我们将 ES6+ 转换为更传统的 javascript
  • react 预设做同样的事情,但使用 JSX 代替。

6. touch .babelrc

创建 babel 配置文件,让 babel 知道,我们将要使用 envreact

{
  "presets": ["@babel/env", "@babel/preset-react"]
}
复制代码

当然 babel 还有很多插件,如果你将来做别的项目需要其他能力可以在这里找 babel 的插件,不过本项目你不需要担心。上面那些就够了。

下一步邀请二哥上场。Webpack

添加 Webpack 及其配置

7. yarn add -D webpack webpack-cli webpack-dev-server style-loader css-loader babel-loader

二哥上场携一众小弟 loader

Webpack 一般使用 loader 来处理不同类型文件的打包问题,而且 webpack 可作为开发服务器,支持热重载。详细就不讲了,毕竟网上手撸 webpack、插件的文章也很多。咱们这里只梳理思路。

8. touch webpack.config.js

创建 webpack 的配置文件,配置如下:

const path = require("path");
const webpack = require("webpack");

module.exports = {
  entry: "./src/index.js",
  mode: "development",
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /(node_modules|bower_components)/,
        loader: "babel-loader",
        options: { presets: ["@babel/env"] },
      },
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"],
      },
    ],
  },
  resolve: { extensions: ["*", ".js", ".jsx"] },
  output: {
    path: path.resolve(__dirname, "dist/"),
    publicPath: "/dist/",
    filename: "bundle.js",
  },
  devServer: {
    contentBase: path.join(__dirname, "public/"),
    port: 3000,
    publicPath: "http://localhost:3000/dist/",
    hotOnly: true,
  },
  plugins: [new webpack.HotModuleReplacementPlugin()],
};
复制代码

关于配置相信大家基本都知道什么意思,定义了 webpack 工作的流程:“输入” – “处理过程” – “输出”。以及开发服务器。

注意:output.publicPathdevServer.publicPath 是有区别的,devServer.publicPath 是开发是输出目录。而 output 是最后编译构建的结果输出目录。

最后,因为我们想使用热重载,所以不需要重复刷新来查看更改。对于这个文件,我们所做的就是在 plugins 属性中实例化一个新的插件实例,并确保我们在 devServer 中将 hotOnly 设置为 true。不过,在 HMR 工作之前,我们还需要在 React 中设置一个东西。

9. yarn add react react-dom

至此整个 web app 工程基本依赖安装完成。整个 package.json 如下:

{
  "name": "react-lib-ts-example",
  "version": "1.0.0",
  "description": "react lib 的演示 application",
  "main": "index.js",
  "license": "MIT",
  "devDependencies": {
    "@babel/cli": "^7.14.5",
    "@babel/core": "^7.14.6",
    "@babel/preset-env": "^7.14.5",
    "@babel/preset-react": "^7.14.5",
    "babel-loader": "^8.2.2",
    "css-loader": "^5.2.6",
    "style-loader": "^2.0.0",
    "webpack": "^5.39.0",
    "webpack-cli": "^4.7.2",
    "webpack-dev-server": "^3.11.2"
  },
  "dependencies": {
    "react": "^17.0.2",
    "react-dom": "^17.0.2"
  }
}
复制代码

编写业务代码 app

10. vim src/index.js

在 src 下创建业务源码,代码如下:

import React from "react";
import ReactDOM from "react-dom";
import App from "./App.js";

ReactDOM.render(<App />, document.getElementById("root"));
复制代码

11. vim src/App.js

import React, { Component } from "react";
import "./App.css";

class App extends Component {
  render() {
    return (
      <div className="App">
        <h1> Hello, World! React Web App</h1>
      </div>
    );
  }
}

export default App;
复制代码

12. vim src/App.css

/* App.css */
.App {
  margin: 1rem;
}
复制代码

13. 当前目录结构

➜  tree -Ia node_modules
.
├── .babelrc
├── package.json
├── public
│   └── index.html
├── src
│   ├── App.css
│   ├── App.js
│   └── index.js
├── webpack.config.js
└── yarn.lock

2 directories, 8 files
复制代码

万事俱备只查一 try。

编译运行/构建

14. vim package.json 添加 script:start/build

  "scripts": {
    "start": "webpack serve --mode development --progress",
    "build:prod": "webpack --mode production"
  },

复制代码

注意:这里有个差异点要注意:webpack 3.x 与 4.x 启动命令发生变化如下:

webpack-cli 3.x: 启动命令是:webpack-dev-server
“scripts”: { “start:dev”: “webpack-dev-server” }

For webpack-cli 4.x: 启动命令是:webpack serve
“scripts”: { “start:dev”: “webpack serve” }

15. yarn start

浏览器查看:http://localhost:3000
你将在页面看到:“Hello, World! React Web App”。

但是在过程中有个有趣的现象,你一直看不到 dist 目录下生成构建文件。实际上 webpack-dev-server 生成的文件都在内存中,一旦服务停止内存里的临时文件就消失了。
如果想要实际构建你的文件,执行 yarn build
就会在你的工程目录下看到 dist/bundle.js

小结

到此以上操作基本涵盖了创建 React Web application 的所有内容,到现在我们没有用一下脚手架。

当然有些其他格式的文件还没有处理比如 图片,但是这个相信你自己也能应付的了,这里有个 loader 给你用 file-loader

所有的脚手架最核心的功能基本都是自动化的完成以上的步骤,或多添加了些性能调优等等之类的功能特性。万变不离其宗。

熟悉了这些步骤再搞脚手架的时候遇到问题总有个思路处理问题,比如覆盖脚手架的配置,手动实现一些配置。从而实现自定义。

要想知识更加体系化,拥有举一反三的能力,首先要有广度,最起码见过三,才能举一反三。如果连二都没见过,反三估计是够呛。

感觉终于要絮叨结束了对吧?但是我们好像还没有完成最初的目的:使用 web app 验证 library 是否可用,下面咱们就来执行最后一步操作。

验证 library 库

1. library 发包供 example 应用使用

发包的方式大家应该都知道 npm publish,但是咱们还是开发阶段,直接 publish 显然不合适。

那有什么办法不用 publish 发包吗?当然,开发阶段注册个软链接就可以。

2. yarn link

yarn link: 对于开发,一个包可以链接到另一个项目。这对于测试新功能或尝试调试在另一个项目中表现出来的包中的问题通常很有用。
命令使用方法:
yarn link(在你想要 link 的包根目录执行)
yarn link [package...]

原理:
链接注册在 ~/.config/yarn/link.
要逆转此过程,只需使用 yarn unlinkyarn unlink [package]

注册软链

# 注意此命令要在 react-lib-ts 根目录执行。
➜  yarn link
yarn link v1.22.10
success Registered "react-lib-ts".
info You can now run `yarn link "react-lib-ts"` in the projects where you want to use this package and it will be used instead.
✨  Done in 0.04s.
复制代码

使用软链

cd example
➜  yarn link "react-lib-ts"
yarn link v1.22.10
success Using linked package for "react-lib-ts".
✨  Done in 0.04s.
复制代码

"react-lib-ts": "link:.." 依赖安装到 example package.json

...
"react-lib-ts": "link:.."
...
复制代码

这就是为什么要将 example 放在 library 工程内部的原因。

3. 修改业务代码并使用 library

// src/App.js
import React, { Component } from "react";
import SayHelloWorld from "react-lib-ts";
import "./App.css";

class App extends Component {
  render() {
    return (
      <div className="App">
        <h1> Hello, World! React Web App</h1>

        <h2>
          <SayHelloWorld name={"Hui"} />
        </h2>
      </div>
    );
  }
}

export default App;
复制代码

前后对比:
App.js 使用 library 前后对比

最终 web app 运行页面显示结果:
web app 运行结果

看结果 library 已经正常显示。后期 example 便可作为 library 的一个演示站点打包部署,在线预览。

至此,整个过程才算全部完结。


总结

用到的知识点

  1. node、package.json
  2. yarn、yarn link
  3. typescript、tsc
  4. cjs、esm、umd
  5. dependencies、devDependencies、peerDependencies
  6. babel
  7. webpack、webpack-dev-server
  8. linux 常用命令

库的开发流程

上面的文章有一个整理的清晰明了的开发流程,不知你是否发现。

其实就是有序号的目录。回头再看一次,回忆、思考一遍(目录都是命令,你要是看到命令就知道每一步是干什么的,基本整个流程心里就有数了。)

最后在叨叨一句:我的理解构建知识体系,就是沉淀索引地图。就像查字典,目录索引越丰富,实际指向的内容也就更丰富。

OK。

本文到这里就真的结束了,同时本文的知识点才仅仅是 “前端知识体系” 大树的一个特别小的叶子节点。后续还有很多。期待下次再见。?

参考资料

  1. stackoverflow 关于 peerDependencies 的问答

  2. 从头开始创建 React App

  3. 如何使用 TypeScript 构建一个 React 库

  4. 本文源码地址:https://github.com/dahui4dev/react-ts-lib

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享