2

前言

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

系列文章

webpack4从零开始构建(一)
webpack4+React16项目构建(二)
webpack4功能配置划分细化(三)
webpack4引入Ant Design和Typescript(四)
webpack4代码去重,简化信息和构建优化(五)
webpack4配置Vue版脚手架(六)

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

这个插件能对antdantd-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.


Afterward
621 声望62 粉丝

努力去做,对的坚持,静待结果


引用和评论

0 条评论