前言
之前一段时间工作原因把精力都放在小程序上,趁现在有点空闲时间,刚好官方文档也补充完整了,我准备重温一下 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
这个插件能对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
,两者都能工作,区别在于
- atl has first-class integration with Babel and enables caching possibilities. This can be useful for those who use Typescript with Babel. When
useBabel
anduseCache
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. - 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.json
和config/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.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。