webpack4引入Ant-Design和Typescript四

39次阅读

共计 12398 个字符,预计需要花费 31 分钟才能阅读完成。

前言

之前一段时间工作原因把精力都放在小程序上, 趁现在有点空闲时间, 刚好官方文档也补充完整了, 我准备重温一下 webpack 之路了, 因为官方文档已经写得非常详细, 我会大量引用原文描述, 主要重点放在怎么从零构建 webpack4 代码上, 这不是一个系统的教程, 而是从零摸索一步步搭建起来的笔记, 所以前期可能 bug 会后续发现继续修复而不是修改文章.

继续上回分解, 我们之前已经实现了脚手架的雏形, 这章就从开发角度搞事情了. 回顾之前的示例代码 难以忍受的丑, 为了兼顾界面美观和开发效率, 我们会引入一些 UI 库使用

2019/03/14 上传, 代码同步到引入 antd webpack4_demo_antd
2019/03/15 上传, 代码同步到引入 typescript webpack4_demo_typescript

Ant Design React

引入 antd

yarn add antd

首先在 \src\style\style.scss 引入 UI 库样式

@import '~antd/dist/antd.css';

然后我们开始动手装饰一下界面, 打开\src\page\main.jsx

import React, {Component} from "react";
import {Switch, Route, Redirect, Link} from "react-router-dom";
import {hot} from "react-hot-loader";
import View1 from "CMT/view1.jsx";
import View2 from "CMT/view2.jsx";
import "STYLE/style.scss";
import {Layout, Menu} from 'antd';

const {Header, Content, Footer} = Layout;

class Main extends Component {constructor(props, context) {super(props, context);
    this.state = {title: "Hello World!"};
  }

  render() {
    return (
      <Layout className="layout">
        <Header>
          <Menu
            theme="dark"
            mode="horizontal"
            defaultSelectedKeys={['1']}
            style={{lineHeight: '64px'}}
          >
            <Menu.Item key="1"><Link to="/view1/">View1</Link></Menu.Item>
            <Menu.Item key="2"><Link to="/view2/">View2</Link></Menu.Item>
          </Menu>
        </Header>
        <Content style={{padding: '0 50px'}}>
          <div style={{background: '#fff', padding: 24, minHeight: 280}}>
            <h2>{this.state.title}</h2>
            <Switch>
              <Route exact path="/" component={View1} />
              <Route path="/view1/" component={View1} />
              <Route path="/view2/" component={View2} />
              <Redirect to="/" />
            </Switch>
          </div>
        </Content>
        <Footer style={{textAlign: 'center'}}>
          Ant Design ©2018 Created by Ant UED
        </Footer>
      </Layout>
    )
  }
}

export default hot(module)(Main);

执行命令查看效果

npm run prod

界面如下

按需加载 babel-plugin-import

上面我们引入了 antd 的全部样式, 这样会打包太多没用到的 css

@import '~antd/dist/antd.css';

于是我们引入按需加载的插件使用

yarn add babel-plugin-import

这个插件能对 antd, antd-mobile, lodash, material-ui 等库做按需加载

然后我们将 \src\style\style.scss 里的引入样式删除

// @import '~antd/dist/antd.css';

.babelrc文件修改如下

{
    "presets": [
        ["env", {modules: false}], "react"
    ],
    "plugins": ["react-hot-loader/babel", ["import", {
        "libraryName": "antd", // 引入库名称
        "libraryDirectory": "lib", // 来源,default: lib
        "style": true, // 全部,or 按需 'css'
    }]]
}

效果如下

import {Button} from 'antd';
ReactDOM.render(<Button>xxxx</Button>);

      ↓ ↓ ↓ ↓ ↓ ↓
      
var _button = require('antd/lib/button');
require('antd/lib/button/style/css');
ReactDOM.render(<_button>xxxx</_button>);

实际上就是帮你转换成对应模块样式引入, 重新执行命令

npm run prod

控制台报错

ERROR in ./node_modules/antd/lib/tooltip/style/index.less 1:0
Module parse failed: Unexpected character ‘@’ (1:0)
You may need an appropriate loader to handle this file type.

@import ‘../../style/themes/default’;
| @import ‘../../style/mixins/index’;
|
@ ./node_modules/antd/lib/tooltip/style/index.js 5:0-23
@ ./node_modules/antd/lib/menu/style/css.js
@ ./src/page/main.jsx
@ ./src/index.js

ERROR in ./node_modules/antd/lib/style/index.less 1:0
Module parse failed: Unexpected character ‘@’ (1:0)
You may need an appropriate loader to handle this file type.

@import ‘./themes/default’;
| @import ‘./core/index’;
|
@ ./node_modules/antd/lib/tooltip/style/index.js 3:0-33
@ ./node_modules/antd/lib/menu/style/css.js
@ ./src/page/main.jsx
@ ./src/index.js


粗略一看,antd 内置使用 Less 预处理器, 和我们配置的 Scss 不兼容.

引入 LESS

先安装一下依赖

yarn add less less-loader

然后再 config/rules.js 新增对 Less 文件处理, 重新执行命令,OK 了

{
  test: /antd.*\.less$/, // 匹配文件
  use: [
    process.env.NODE_ENV !== "SERVER"
      ? {
          loader: MiniCssExtractPlugin.loader,
          options: {
            // you can specify a publicPath here
            // by default it use publicPath in webpackOptions.output
            publicPath: process.env.NODE_ENV === "DEV" ? "./" : "../"
          }
        }
      : "style-loader", // 使用 <style> 将 css-loader 内部样式注入到我们的 HTML 页面,
    "css-loader", // 加载.css 文件将其转换为 JS 模块
    {
      loader: "postcss-loader",
      options: {
        config: {path: "./" // 写到目录即可,文件名强制要求是 postcss.config.js}
      }
    },
    {
      loader: "less-loader",
      options: {javascriptEnabled: true // 是否处理 js 内样式}
    }
  ]
},

两个地方需要注意

1, 我们业务依然保持使用 Scss, 所以 Less 只限于引入库, 所以我们需要限定范围减少搜索时间

test: /antd.*\.less$/

2, less-loader@3+ 需要在选项增加对 Js 引入的 less 文件处理

options: {javascriptEnabled: true // 是否处理 js 引入 less}

Typescript

这是一个挺好的东西, 后续我可能会单独写一篇, 也可能不写, 我们先学下怎么引入项目先.

先安装依赖

yarn add typescript awesome-typescript-loader source-map-loader

后面如果有遇到这种错误那是因为 typescript 版本太高的 bug, 可以尝试退回到 3.1.6 版本试试

ERROR in ./src/index.tsx
Module build failed: Error: Final loader (./node_modules/awesome-typescript-loader/dist/entry.js) didn’t return a Buffer or String

at runLoaders (C:\work\project\webpack_demo\node_modules\webpack\lib\NormalModule.js:318:18)
at C:\work\project\webpack_demo\node_modules\loader-runner\lib\LoaderRunner.js:370:3
at iterateNormalLoaders (C:\work\project\webpack_demo\node_modules\loader-runner\lib\LoaderRunner.js:211:10)
at iterateNormalLoaders (C:\work\project\webpack_demo\node_modules\loader-runner\lib\LoaderRunner.js:218:10)
at C:\work\project\webpack_demo\node_modules\loader-runner\lib\LoaderRunner.js:233:3
at context.callback (C:\work\project\webpack_demo\node_modules\loader-runner\lib\LoaderRunner.js:111:13)
at process.internalTickCallback (internal/process/next_tick.js:77:7)

  • awesome-typescript-loader 可以让 Webpack 使用 TypeScript 的标准配置文件 tsconfig.json编译 TypeScript 代码
  • source-map-loader 使用 TypeScript 输出的 sourcemap 文件来告诉 webpack 何时生成 自己的sourcemaps

awesome-typescript-loader

官方推荐的解析库是awesome-typescript-loader, 而有些人会使用ts-loader, 两者都能工作, 区别在于

  1. atl has first-class integration with Babel and enables caching possibilities. This can be useful for those who use Typescript with Babel. When useBabel and useCache flags are enabled, typescript’s emit will be transpiled with Babel and cached. So next time if source file (+environment) has the same checksum we can totally skip typescript’s and babel’s transpiling. This significantly reduces build time in this scenario.
  2. atl is able to fork type-checker and emitter to a separate process, which also speeds-up some development scenarios (e.g. react with react-hot-loader) So your webpack compilation will end earlier and you can explore compiled version in your browser while your files are typechecked.

大概意思就是拥有一流的集成和缓存, 可以跳过多余的构建减少时间消耗. 能够新开进程去处理类型检查等操作, 并行构建项目.

我们需要在根目录创建一个 tsconfig.json 文件

{
    "compilerOptions": {
        "outDir": "./dist/",
        "sourceMap": true,
        "noImplicitAny": true,
        "module": "commonjs",
        "target": "es5",
        "jsx": "react"
    },
    "include": ["./src/**/*"]
}

上面属性即使不解释应该也能看懂吧

source-map-loader

source-map-loader会从入口的所有 js 中提取出源映射, 包括内联和 URL 链接然后传递给 webpack 做处理. 对一些拥有自己源映射的第三方库尤为有用, 因为它们可能会引起浏览器的曲解. 这样做能够让 webpack 去维护源映射的数据连续性, 方便调试.

打开 config/rules.js 新增处理操作

{test: /\.(js|jsx)$/, // 匹配文件
  use: ['source-map-loader'],
  enforce: "pre",
  exclude: /node_modules/, // 过滤文件夹
  use: {loader: "babel-loader"}
},
// All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'.
{
  test: /\.tsx?$/,
  loader: "awesome-typescript-loader",
  exclude: [/node_modules\/mutationobserver-shim/g,]
},

接下来我们在 webpack.common.js 配置一下extensions , 因为可能大部分人再引入文件时候都习惯不补上文件扩展名, 这时候 webpack 就会按照 extensions 一个个去匹配, 默认 [‘.wasm’, ‘.mjs’, ‘.js’, ‘.json’]

resolve: {
    // Add '.ts' and '.tsx' as resolvable extensions.
    extensions: [".ts", ".tsx", ".js", ".json"],
    // 创建 import 或 require 的别名,来确保模块引入变得更简单
    alias
}

然后我们开始修改文件后缀, 例如 src\component\view1.jsx ->src\component\view1.tsx.
现在执行命令

npm run dev

你会惊喜地发现终端狠狠的报错

 ERROR in [at-loader] ./src/component/view1.tsx:1:33
     TS7016: Could not find a declaration file for module 'react'. 'C:/work/project/webpack_demo/node_modules/react/index.js' implicitly has an 'any' type.
   Try `npm install @types/react` if it exists or add a new declaration (.d.ts) file containing `declare module 'react';`

 ERROR in [at-loader] ./src/component/view1.tsx:6:7
     TS7026: JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists.

 ERROR in [at-loader] ./src/component/view1.tsx:6:15
     TS7026: JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists.

 ERROR in [at-loader] ./src/component/view1.tsx:7:7
     TS7026: JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists.

 ERROR in [at-loader] ./src/component/view1.tsx:7:34
     TS2580: Cannot find name 'require'. Do you need to install type definitions for node? Try `npm i @types/node` and then add `node` to the types field in your tsconfig.

因为我们还要添加 React 和 React-DOM 以及它们的声明文件到 package.json 文件里做为依赖.

yarn add @types/react @types/react-dom @types/react-router-dom

再次执行命令依然报错

 ERROR in [at-loader] ./src/component/view1.tsx:1:8
     TS1192: Module '"C:/work/project/webpack_demo/node_modules/@types/react/index"' has no default export.

 ERROR in [at-loader] ./src/component/view1.tsx:7:34
     TS2580: Cannot find name 'require'. Do you need to install type definitions for node? Try `npm i @types/node` and then add `node` to the types field in your tsconfig.

虽然不知道原因, 但是已经不能直接引入 React 里的东西, 所以我们还要改一下引入写法

import React, {Fragment} from "react";

      ↓ ↓ ↓ ↓ ↓ ↓
      
import * as React from "react";
const {Fragment} = React;

后面发现原来 tsconfig.json 提供了一个选项, 那就不用改写法了.

{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。这并不影响代码的输出,仅为了类型检查。...
  },
  ...
}

后面版本问题已经被废弃了, 所以我们换了个属性, 具体原因 Deprecated ‘allowSyntheticDefaultImports’ for synthetic modules

"esModuleInterop": true, // 允许从没有设置默认导出的模块中默认导入。这并不影响代码的输出,仅为了类型检查。

你以为这样就完了吧, 不!!

 ERROR in [at-loader] ./src/component/view1.tsx:8:34
     TS2580: Cannot find name 'require'. Do you need to install type definitions for node? Try `npm i @types/node` and then add `node` to the types field in your tsconfig.

惊喜不惊喜? 意外不意外? 现在连 require 图片资源的语法都出问题了, 于是我们跟着提示继续安装依赖

yarn add @types/node

再来一遍执行命令, 终于可以顺利运行了, 其他相关文件也全部转成 tsx 格式

src\component\view2.jsx ->src\component\view2.tsx

import React, {Fragment} from "react";

export default () => {
  return (
    <Fragment>
      <p>Page2</p>
      <div className="img2" />
    </Fragment>
  );
};

src\page\main.jsx ->src\page\main.tsx

import React, {Component} from "react";
import {Switch, Route, Redirect, Link} from "react-router-dom";
import {hot} from "react-hot-loader";
import View1 from "CMT/view1";
import View2 from "CMT/view2";
import "STYLE/style.scss";
import {Layout, Menu} from 'antd';

const {Header, Content, Footer} = Layout;

class Main extends Component<{}, { title: string}> {constructor(props: Object, context: Object) {super(props, context);
    this.state = {title: "Hello World!"};
  }

  render() {
    return (
      <Layout className="layout">
        <Header>
          <Menu
            theme="dark"
            mode="horizontal"
            defaultSelectedKeys={['1']}
            style={{lineHeight: '64px'}}
          >
            <Menu.Item key="1"><Link to="/view1/">View1</Link></Menu.Item>
            <Menu.Item key="2"><Link to="/view2/">View2</Link></Menu.Item>
          </Menu>
        </Header>
        <Content style={{padding: '0 50px'}}>
          <div style={{background: '#fff', padding: 24, minHeight: 280}}>
            <h2>{this.state.title}</h2>
            <Switch>
              <Route exact path="/" component={View1} />
              <Route path="/view1/" component={View1} />
              <Route path="/view2/" component={View2} />
              <Redirect to="/" />
            </Switch>
          </div>
        </Content>
        <Footer style={{textAlign: 'center'}}>
          Ant Design ©2018 Created by Ant UED
        </Footer>
      </Layout>
    )
  }
}

export default hot(module)(Main);

src\index.js ->src\index.tsx

import React from "react";
import ReactDOM from "react-dom";
import {HashRouter} from "react-router-dom";
import Main from "PAGE/main";
import "../index.html";

ReactDOM.render(
  <HashRouter>
    <Main />
  </HashRouter>,
  document.getElementById("root")
);

记得要把其他文件例如 package.jsonconfig/webpack.common.js等文件的 index 引入后缀同步改一下.

到了这步你以为你成功了, 结果又是一个晴天霹雳

 ERROR in [at-loader] ./src/index.tsx:6:18
     TS2307: Cannot find module 'PAGE/main'.

 ERROR in [at-loader] ./src/page/main.tsx:4:19
     TS2307: Cannot find module 'CMT/view1'.

 ERROR in [at-loader] ./src/page/main.tsx:5:19
     TS2307: Cannot find module 'CMT/view2'.

因为现在 tsx 也需要配置自己的一套解析路径, 于是我们继续修改tsconfig.json

{
  "compilerOptions": {
    "esModuleInterop": true, // 允许从没有设置默认导出的模块中默认导入。这并不影响代码的输出,仅为了类型检查。"outDir": "./dist/",
    "sourceMap": true,
    "noImplicitAny": true,
    "module": "commonjs",
    "target": "es5",
    "jsx": "react",
    "baseUrl": "src", // 解析非相对模块名的基准目录
    // 模块名到基于 baseUrl 的路径映射的列表
    "paths": {"@/*": ["*"],
      "IMG/*": ["img/*"],
      "STYLE/*": ["style/*"],
      "JS/*": ["js/*"],
      "ROUTER/*": ["router/*"],
      "PAGE/*": ["page/*"],
      "CMT/*": ["component/*"]
    },
  },
  "include": ["./src/*"],
  "exclude": ["node_modules",]
}

抱着屡战屡败的勇气再次执行

npm run dev

终于情形一片大好, 顺利打包, 直到你打开界面为止 …

ts-import-plugin

看来是按需加载那块出了问题了. 然后继续搜索资料找到 typescript 专用的按需加载库

yarn add ts-import-plugin

跟着文档走一个个修改

tsconfig.json

{
  "compilerOptions": {
    "module": "ESNext", // 指定生成哪个模块系统代码:"None","CommonJS","AMD","System","UMD","ES6" 或 "ES2015"。...
  },
  ...
}

config/rules.js

const tsImportPluginFactory = require("ts-import-plugin");
------------------------------------------------------------
{
    test: /\.tsx?$/,
    loader: "awesome-typescript-loader",
    options: {
      useCache: true,
      useBabel: false, // !important!
      getCustomTransformers: () => ({
        before: [tsImportPluginFactory({
          libraryName: 'antd',
          libraryDirectory: 'lib',
          style: true
        })]
      }),
    },
    exclude: [/node_modules\/mutationobserver-shim/g,]
  }

继续执行命令

npm run dev

顺利编译完成, 打开页面一看, 嗯, 内心毫无波动~~

 main.tsx?21bb:9 Uncaught ReferenceError: antd_1 is not defined
     at Object.eval (main.tsx?21bb:9)
     at eval (main.tsx:58)
     at Object../src/page/main.tsx (main.bundle.js:3209)
     at __webpack_require__ (main.bundle.js:20)
     at eval (index.tsx?22d4:6)
     at Object../src/index.tsx (main.bundle.js:3197)
     at __webpack_require__ (main.bundle.js:20)
     at main.bundle.js:84
     at main.bundle.js:87

继续埋头苦干, 各种调查, 发现 typescript.json 还有一个属性配置

{
  "compilerOptions": {
    "moduleResolution": "node", // 决定如何处理模块。或者是 "Node" 对于 Node.js/io.js,或者是 "Classic"(默认)...
  },
  ...
}

再来一次!!

npm run dev

感谢上帝!!

收尾

因为我们现在用上 typescript 之后, 有一些东西就可以直接废弃了, 例如

按需加载 babel-plugin-import 已经替换成ts-import-plugin

.babelrc还原回到

{
    "presets": [
        ["env", {modules: false}], "react"
    ],
    "plugins": ["react-hot-loader/babel"]
}

因为 typescript 本身就支持各种 JavaScript 版本的转换,甚至是不同的规范 , 所以我们将 js 和 jsx 的相关 loader 也去掉.

暂时运行起来还没问题, 但是毕竟没有经过项目实战, 可能有 bug.

正文完
 0