前端利器躬行记(7)——自制脚手架

摘要:
path是Node.js中的路径模块path.resolve()用于解析绝对路径,__dirname可读取当前模块的目录名。静态资源最终路径=output.publicPath+加载器或插件的配置路径。假设html元素的背景是一条相对路径,那么最后生成的路径将会是“/img/lake.png”,其中配置的输出目录是“img”。paths.servedPath:isEnvDevelopment&&'/';2)加载器在加载器中,会添加脚本、样式、图像以及字体。Babel的配置信息写到了package.json文件中,新建一个babel字段,useBuiltIns的值为usage,表示自动加载源码所需的Polyfill。

在学习了Webpack基础后,查看别人写好的脚手架总是会一头雾水,后面就上网查各种资料,一边参考一边修改,整出了一套简易的脚手架(已上传至GiuHub和npm上),借鉴了Create React App(CRA)的目录结构(如下所示),并做成了命令行工具(已上传至GiuHub和npm上)。

├── pwu ---------------------------------------脚手架示例
│   ├── config --------------------------------webpack配置目录
│   ├── ├── jest ------------------------------Jest测试的配置目录
│   ├── ├── webpack.base.config.js ------------通用配置
│   ├── ├── webpack.dev.config.js -------------开发环境配置
│   ├── ├── webpack.prod.config.js ------------生产环境配置
│   ├── bin -----------------------------------命令行工具
│   ├── ├── pwu.js ----------------------------命令文件
│   ├── dist ----------------------------------输出目录
│   ├── ├── css -------------------------------样式
│   ├── ├── img -------------------------------图像
│   ├── ├── js --------------------------------脚本
│   ├── public --------------------------------模板目录
│   ├── ├── index.html ------------------------模板页面
│   ├── src -----------------------------------源文件目录
│   ├── ├── __tests__ -------------------------测试目录
│   ├── ├── component -------------------------组件目录
│   ├── ├── font ------------------------------字体目录
│   ├── ├── img -------------------------------图像目录
│   ├── ├── index.js --------------------------入口文件
│   ├── ├── index.scss ------------------------全局样式
│   ├── package.json --------------------------管理依赖的包
│   ├── package-lock.json ---------------------管理包的版本号和来源
│   ├── postcss.config.js ---------------------后处理器配置文件
│   ├── tsconfig.json -------------------------TypeScript配置文件
│   ├── .eslintrc -----------------------------ESLint配置文件
│   ├── .eslintignore -------------------------ESLint忽略的文件和目录
│   ├── .gitignore ---------------------------- Git忽略的文件和目录
一、通用配置

1)入口和出口

在通用配置中包含两个环境都需要的参数,例如入口和出口,如下所示。path是Node.js中的路径模块path.resolve()用于解析绝对路径,__dirname可读取当前模块的目录名。

const path = require("path");
module.exports ={
  entry: {
    index: "./src/index.js"},
  output: {
    path: path.resolve(__dirname, "../dist"),
    publicPath: "/"}
};

publicPath指定静态资源的基础路径,公式如下。

静态资源最终路径 = output.publicPath + 加载器或插件的配置路径。

假设html元素的背景是一条相对路径,那么最后生成的路径将会是“/img/lake.png”,其中配置的输出目录是“img”。

html {background:url("../../../public/img/lake.png") no-repeat;
}
/*生成的背景路径 */html {background:url("/img/lake.png") no-repeat;
}

在CRA的webpack.config.js配置文件中,也有对publicPath的配置,如下所示,生产和开发环境会有对应的值。

const publicPath = isEnvProduction ? paths.servedPath : isEnvDevelopment && '/';

2)加载器

在加载器中,会添加脚本(babel-loader)、样式(css-loaderpostcss-loadersass-loader)、图像(url-loader)以及字体(file-loader)。

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports ={
  module: {
    rules: [
      {
        test: /.(js|jsx)$/,
        use: "babel-loader",
        exclude: /node_modules/},
      {
        test: /.(sass|scss)$/,
        use: [
          MiniCssExtractPlugin.loader,
          "css-loader",
          "postcss-loader",
          "sass-loader"]
      },
      {
        test: /.(jpg|png|gif)$/,
        use: {
          loader: "url-loader",
          options: {
            name: "[name].[ext]",
            outputPath: "img/",
            limit: 8192}
        }
      },
      {
        test: /.(eot|ttf|svg|woff|woff2)$/,
        use: {
          loader: "file-loader",
          options: {
            name: "[name]_[hash].[ext]",
            outputPath: "font/"}
        }
      }
    ]
  }
};

在解析样式的配置中,使用了四个加载器,后声明的先执行。Babel的配置信息写到了package.json文件中,新建一个babel字段,useBuiltIns的值为usage,表示自动加载源码所需的Polyfill。

"babel": {
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "ie": 11,
          "chrome": 49},
        "corejs": "2",
        "useBuiltIns": "usage"}
    ],
    "@babel/preset-react"]
}

postcss-loader又称为CSS后处理器,常用来提升浏览器兼容性,它有许多配套插件(例如autofix),这些插件的配置被放在单独的postcss.config.js文件中,如下所示。

module.exports ={
  plugins: [require("autoprefixer")()]
};

在执行时,postcss-loader会建议将浏览器的信息放在package.json中,新建一个browserslist字段,如下所示。

"browserslist": [
  "last 5 version",
  ">1%",
  "ie >=8"]

MiniCssExtractPlugin.loader引用的是mini-css-extract-plugin插件的加载器,该插件能从JS文件中提取CSS样式,保存到单独的CSS文件中。

url-loader和file-loader中的outputPath属性用于配置输出目录。图像中的limit属性的值是8192,以字节为单位,相当于8kb,如果图像尺寸小于该值,那就将其转换成Base64格式,嵌入到文件中,减少HTTP请求。字体文件的名称还会加上唯一标识的hash值,生成的名称如下所示。

iconfont_7346d960c4ad96f1ea8d5a8834fab00f.ttf

3)插件

MiniCssExtractPlugin插件的作用前面已提过,其中chunkFilename参数会在动态导入时用到。

plugins: [
  newMiniCssExtractPlugin({
    filename: "css/[name].[hash].css",
    chunkFilename: "css/[id].[hash].css"})
]
二、开发环境配置

在开发环境中,需要引入通用配置,再利用webpack-merge合并,如下所示。mode字段用于告知webpack使用相应模式的优化。输出的文件名称也包含hash,但只会提取前8个字符。

const base = require('./webpack.base.config.js');
const merge = require('webpack-merge');
module.exports =merge(base, {
  mode: "development",
  output: {
    filename: "js/[name].[hash:8].bundle.js"}
});

1)webpack-dev-server

开启基于Node.js的本地服务器:webpack-dev-server

devServer: {
  contentBase: path.resolve(__dirname, "../dist"),
  open: true,            //自动打开浏览器
  port: 4000,            //端口号
  compress: true,        //启用gzip压缩:
  useLocalIp: true,      //使用本机IP
  hot: true              //开启热更新
}

2)Source Map

通过Source Map追踪错误或警告在源文件中的原始位置,以便调试,可配置devtool实现,如下所示。

devtool: "source-map"

再添加webpack的HotModuleReplacementPlugin插件,如下所示。

const webpack = require('webpack');
module.exports =merge(base, {
  plugins: [
    newwebpack.HotModuleReplacementPlugin()
  ]
});

3)HtmlWebpackPlugin

HtmlWebpackPlugin插件能根据模板生成一个HTML文件,还能自动引入所需的bundle文件。模板文件被放置在public目录中,如下所示。

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8"/>
    <title>脚手架示例</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

具体配置如下,inject参数用于指定脚本注入位置,例如body元素的底部。

const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports =merge(base, {
  plugins: [
    newHtmlWebpackPlugin({
      template: path.resolve(__dirname, "../public/index.html"),
      inject: "body"})
  ]
});

4)脚本命令

在package.js文件的scripts字段中,声明了start命令,开启本地服务器,并实时重载脚本。

{
  "scripts": {
    "start": "webpack-dev-server --config ./config/webpack.dev.config.js"}
}
三、生产环境配置

生产环境比较注重性能,因此需要做很多优化配置,例如压缩、代码分离等,mode采用production优化模式,如下所示。

module.exports =merge(base, {
  mode: "production",
  output: {
    filename: 'js/[name].[chunkhash:8].bundle.js'}
}

1)optimization

首先优化的是代码分离,也就是将稳定不变的模块(例如react、react-dom等)抽取成一个单独的文件,splitChunks参数的配置可参考SplitChunksPlugin插件。

module.exports =merge(base, {
  optimization: {
    splitChunks: {
      chunks: "all",
      minSize: 30000,
      maxSize: 0,
      minChunks: 1,
      cacheGroups: {
        vendors: {
          test: /node_modules/,
          name: "vendor",
          enforce: true}
      }
    }
  }
});

cacheGroups是优化的关键,它是一个缓存组(属性如下所示),vendors会筛选从node_modules目录下引入的模块。

(1)test:一个字符串、正则或函数,模块的匹配条件。

(2)name:拆分出的chunk(块)的名字。

(3)enforce:当为true时,可忽略minSize、minChunks、maxAsyncRequests和maxInitialRequests选项。

(4)priority:打包的优先级。

接下来优化的是压缩,配置到minimizer选项中。UglifyjsWebpackPlugin插件会使用使用UglifyJS去压缩JavaScript代码。OptimizeCssAssetsPlugin插件用于压缩CSS文件。

const UglifyjsWebpackPlugin = require("uglifyjs-webpack-plugin");
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports =merge(base, {
  optimization: {
    minimizer: [
      newUglifyjsWebpackPlugin(),
      newOptimizeCssAssetsPlugin({
        assetNameRegExp: /.css$/g,
        cssProcessor: require("cssnano"),
        cssProcessorPluginOptions: {
          preset: ["default", { discardComments: { removeAll: true} }]
        },
        canPrint: true})
    ]
  }
});

2)插件

生产环境也需要模板插件,只不过要配置minify选项,如下所示,去除注释和空格。

newHtmlWebpackPlugin({
  template: path.resolve(__dirname, "../public/index.html"),
  inject: "body",
  minify: {
    removeComments: true,
    collapseWhitespace: true}
})

CleanWebpackPlugin插件可清除输出目录中的文件。

const { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.exports =merge(base, {
  plugins: [
    newCleanWebpackPlugin()
  ]
});

偶尔会出现图45中的错误,目前还没找出原因。

前端利器躬行记(7)——自制脚手架第1张

图 45

3)脚本命令

在package.js文件的scripts字段中,新增build命令,可在本地构建项目。

{
  "scripts": {
    "start": "webpack-dev-server --config ./config/webpack.dev.config.js",
    "build": "webpack --config ./config/webpack.prod.config.js"}
}
四、TypeScript

若要支持TypeScript,那么必须安装相应的模块以及加载器,命令如下。

npm install --save-dev typescript ts-loader

在webpack的通用配置中,添加如下字段,resolve的extensions属性能够在引入模块时不带扩展。

module.exports ={
  resolve: {
    extensions: [".tsx", ".ts", ".js"]
  },
  module: {
    rules: [
      {
        test: /.tsx?$/,
        use: [ 'ts-loader'],
        exclude: /node_modules/}
    ]
  }
};

还得要添加tsconfig.json配置文件,如下所示,具体的字段说明可以参考官方文档

{
  "compilerOptions": {
    "outDir": "./dist/",
    "noImplicitAny": true,
    "module": "es6",
    "target": "es5",
    "jsx": "react",
    "allowJs": true}
}

由于要使用react和react-dom,因此还需要安装它们的声明文件:@types/react@types/react-dom。并且使用了html-webpack-plugin插件,它的声明文件(@types/html-webpack-plugin)也得安装。

都安装好后,就能在tsx文件中使用JSX语法了,如下所示。

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import App from './component/app/app';

functioninit() {
  ReactDOM.render(React.createElement(App, null), document.getElementById('root'));
}
init();

在webpack中的通用配置中,可添加新的入口文件,如下所示。

module.exports ={
  entry: {
    index: "./src/index.ts",
    index2: "./src/index.tsx"}
}
五、ESLint

ESLint是目前流行的静态代码检测工具,它能建立一套代码规范,保证代码的一致性,并且还能避免不必要的错误。

1)基础配置

首先需要安装ESLint和ESLint的加载器,命令如下所示。

npm install --save-dev eslint eslint-loader

然后在通用配置中添加eslint-loader,如下所示。

module.exports ={
  module: {
    rules: [
      {
        test: /.(js|jsx)$/,
        use: [ 'babel-loader', 'eslint-loader'] ,
        exclude: /node_modules/},
      {
        test: /.tsx?$/,
        use: [ 'ts-loader', 'eslint-loader'],
        exclude: /node_modules/}
    ]
  }
};

接着在根目录中创建.eslintrc配置文件,如下所示,rules字段中可记录各种规则。

{
  "rules": {
  }
}

2)规则

现在运行脚手架会报错(如下所示),因为ESLint不能识别ES6语法。

1:1  error  Parsing error: The keyword 'import' is reserved

为了避免该错误,需要安装babel-eslint,并且修改.eslintrc文件。

{
  "parser": "babel-eslint",
  "rules": {
  }
}

下面添加一条简单的max-len规则(其它规则可参考官方文档),一行最长200,4个Tab字符的宽度,忽略尾部注释和行内注释。

{
  "rules": {
    "max-len": ["warn", 200, 4, { "ignoreComments": true}]
  }
}

当超过该限制时,会显示下面的警告。

7:1  warning  This line has a length of 292. Maximum allowed is 200  max-len

由于使用了React,因此还可以添加React的规则,安装eslint-plugin-react,并修改.eslintrc文件。

{
  "plugins": [
    "react"]
}

如果不想自己定义规则,那么可以直接使用网上开源的规则,例如AirbnbJavaScript编码规范。注意,Airbnb的标准包会依赖eslint-plugin-import、eslint-plugin-react和eslint-plugin-jsx-a11y等插件。安装成功后,再次修改.eslintrc文件。

{
  "extends": "airbnb"}

重新运行脚手架,马上就会出现一大堆错误和警告,修改加载器(如下所示),使用--fix参数可以将它们减少很多。

module.exports ={
  module: {
    rules: [
      {
        test: /.(js|jsx)$/,
        use: [
          'babel-loader', 
          {loader: 'eslint-loader', options: {fix: true}} 
        ],
        exclude: /node_modules/},
      {
        test: /.tsx?$/,
        use: [
          'ts-loader',
          {loader: 'eslint-loader', options: {fix: true}}
        ],
        exclude: /node_modules/}
    ]
  }
};

3)pre-commit

如果使用的版本控制系统是Git,那么可以在每次提交前检测ESLint的规则。当检测失败时,就能阻止提交。

husky是一个Git钩子工具,可以防止不良的git commit、git push等操作。lint-staged可对暂存的Git文件执行指定的任务。注意,husky对Node和Git的版本有要求,前者要大于10,后者要大于2.13。

接下来修改package.json文件,添加husky和lint-staged字段,在lint-staged中配置ESLint检测以及需要检测的文件后缀。当检测失败时,会得到图46中的提示。

"husky": {
  "hooks": {
    "pre-commit": "lint-staged"}
},
"lint-staged": {
  "*.{js,jsx,ts,tsx}": [
    "eslint"]
}

前端利器躬行记(7)——自制脚手架第2张

图 46

六、Jest

Jest是Facebook开源的一个测试框架,曾经写过一篇入门的教程。要将Jest集成到Webpack中,首先得安装Jest,安装完成后在package.json文件中添加一条脚本命令(如下所示),执行Jest并打印测试覆盖率。注意,生成的测试覆盖率信息默认会保存到coverage目录中。

"scripts": {
  "test": "jest --coverage"}

现在执行“npm test”,不会有任何结果,因为还没写测试脚本。Jest默认会测试__tests__目录和名称中包含spec或test的脚本文件(包括TypeScript文件),并且默认还会忽略node_modules目录中的文件,配置项如下所示。

testMatch: [ '**/__tests__/**/*.js?(x)', '**/?(*.)(spec|test).js?(x)' ]testPathIgnorePatterns: ["node_modules"]

在src目录中新增__tests__目录,并新建app.js,其代码如下所示,添加了一个用于演示的测试用例。

describe("my test case", () =>{
  test("one plus one is two", () =>{
    expect(1 + 1).toBe(2);
  });
});

当在测试用例中使用ES6语法时(例如像下面这样引入组件),会提示错误,此时需要引入babel-jest。而babel-jest在安装Jest时已经自动下载,因此不必再单独安装。

import { App } from '../component/app/app';

在package.json文件定义jest字段,并声明transform选项,添加下面这条规则,就能避免报错。

"jest": {
  "transform": {
    "^.+\.js$": "babel-jest"}
}

Jest还有一些其它配置,在测试时能发挥重大作用。例如在使用样式对象时,将所有的className原样返回(例如styles.container === 'container'),这会便于快照测试。要实现这个功能,得安装identity-obj-proxy,并修改moduleNameMapper选项,如下所示。

"jest": {
  "moduleNameMapper": {
    "\.(css|scss)$": "identity-obj-proxy"}
}

当moduleNameMapper不能满足需求时,可以使用transform选项设定转换规则,如下所示。

"jest": {
  "transform": {
    "\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2)$": "<rootDir>/config/jest/fileTransformer.js"}
}

fileTransformer.js文件位于配置目录的jest目录中,其作用就是返回文件的名称(如下代码所示),例如require('avatar.png')返回“avatar.png”。

const path = require('path');
module.exports ={
  process(src, filename, config, options) {
    return 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';';
  }
};

注意,之前使用了ESLint检测代码,因此测试用例也会被检测。如果不想执行ESLint,那么可以添加.eslintignore文件,内容如下所示,其中配置目录也被忽略了。

src/__tests__
config
七、命令行工具

之前曾写过一篇命令行工具的简易教程。目前的设想是将命令行工具从脚手架中分离出来,通过命令下载脚手架。

首先安装orachalkcommanderdownload-git-repo四个包,安装命令如下所示。

npm install --save ora chalk commander download-git-repo 

ora是一个优雅的终端旋转器,chalk可为终端中的文字添加颜色,commander是一个编辑命令的工具,download-git-repo可下载GitHub上的仓库代码。下面是具体的命令,命令(pwu-cli)已上传到npm中,安装成功后,可以执行“pwu create demo”创建demo目录(如图47所示),并自动下载pwu仓库中的脚手架代码。

#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const ora = require('ora');
const chalk = require('chalk');
const program = require('commander');
const download = require('download-git-repo');

program
    .version('1.0.0', '-v, --version', '版本');
program
  .command('create <name>')
  .description('create a repository')
  .action(name =>{
    const spinner = ora('开始下载脚手架');
    spinner.start();
    const destination =path.join(process.cwd(), name);
    if(fs.existsSync(destination)) {
      console.log(chalk.red('脚手架已存在'));
      return;
    }
    download('github:pwstrick/pwu', destination, (err) =>{
      spinner.stop();                    
      console.log(chalk.green('脚手架下载成功'));
    });
  });
program.parse(process.argv)

前端利器躬行记(7)——自制脚手架第3张

图 47

在发布到npm时,npm可根据.gitignore文件中的内容进行过滤,这样就能避免上传依赖的模块。

参考资料:

React & Webpack

Webpack中publicPath详解

mini-css-extract-plugin插件快速入门

从零配置webpack 4+react脚手架

What is the Purpose of chunkFilename of mini-css-extract-plugin Module?

sass图片地址

webpack 4 Code Splitting 的 splitChunks 配置探索

webpack SplitChunksPlugin实用指南

自制前端脚手架

使用 Node.js 开发简单的脚手架工具

在React+Babel+Webpack环境中使用ESLint

使用 husky+lint-staged+prettier 优化代码格式

husky和lint-staged实现git commit前自动跑lint

jest 和 Webpack 一起使用

jest教程

免责声明:文章转载自《前端利器躬行记(7)——自制脚手架》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇iOS-代码修改Info.plist文件14款优秀的JavaScript调试工具大盘点下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

J2EE学习篇之--Struts2技术详解

前面说到了Struts1的相关知识,下面来说一下Struts2的相关知识,我们知道现在Struts2使用的比Struts1多,Struts2已经替代Struts1成为主流的框架了。。。 摘要Struts2是在WebWork2基础发展而来的。和struts1一样, Struts2也属于MVC框架。不过有一点大家需要注意的是:尽管Struts2和Struts...

Qt5学习笔记(5)——列表框QListWidget类

QListWidget可以显示一个清单,清单中的每个项目是QListWidgetItem的一个实例,每个项目可以通过QListWidgetItem来操作。可以通过QListWidgetItem来设置每个项目的图像与文字。 常用方法和属性: (1)addItem void addItem ( const QString & label ) void...

mac vscode 下载安装与配置

下载安装 1、官网下载https://code.visualstudio.com/ 配置 1、常用插件下载 中文:Chinese (Simplified) Language Pack 代码校验:eslint vue代码优化显示:vetur vue简写代码:vue vscode snippets   prettier的html换行有毒 2、修改中...

CSS实例:翻转图片、滚动图片栏、打开大门

CSS 翻转图片主要用到的技术除了3D翻转和定位 ,还用到了一个属性 backface-visibility:visable|hidden;该属性主要是用来设定元素背面是否可见。 效果图如下: 具体的步骤如下: 1、写出页面主体, <div> <img src="http://t.zoukankan.com/Image...

electron 不支持Ctrl+滚动条放大缩小,自己动手做了一个react组件

前言:功能是不难的,看过代码之后,肯定能理解,肯定。重点说明,这仅仅是为了electron打包做的需求,一般是不会有这样的需求,因为浏览器都带有这样的功能!!!说三遍!!说三遍!!说三遍!! ScrollBox.tsx import React, { useEffect, useRef, useState } from 'react' import { B...

Linux目录及常用命令

Linux目录1. bin.用于存放可执行的二进制文件(ll,mkdir)2. boot 存放用于系统引导时使用的各种文件3. dev 用于存放设备文件 如打印机4. etc.存放系统配置文件 如MySQL的配置文件5. home. 存放所有用户的根目录 user6. lib 存放跟文件系统汇总的程序运行所需要的共享库及内核模块 依赖如Spring Spr...