基于webpack打包多页应用,对前端工程化的思考(下)

2,898 阅读5分钟

前言

上一篇文章 基于webpack打包多页应用,对前端工程化的思考 107👍

这也是我第一个 点赞破百 的文章,感谢掘友的支持,开心😃

总结起来,上一篇文章并没有写什么实际性的东西,可能大家对工程化比较感兴趣,其中也有不少掘友热情的探讨

十分感谢大家不同的意见,如有问题,都可以在下面留言哦!

  1. 完整源码已放github,并配有完整注释,欢迎直接去github上看源码
  2. 如有帮助,欢迎star,万分感谢

下面我们接着上一篇文章,谈论些更多有意思的东西。

按照约定,路由自动生成

单页面应用中,有很多框架都做了该功能,比如基于vueNuxt.js,基于reactUmiJS。比起单页面,多页面中并没有路由嵌套,路由传参等复杂的路由结构,所以在多页面中实现路由自动生成要比单页面简单些。

为了使用更加"野性",我在新建 文件夹 的时候会自动生成路由和配套的html,css,js,效果如下:

实现思路

代码实现

自动生成路由最主要的是文件的自动监听,我使用的是chokidar(速度还可以,比watch模块强太多了)。为了便于维护,我把此功能抽离成独立的 webpack插件 大家可在源码文件plugins/router-auto-plugin.js中查看插件全部源码。核心代码如下:

  compiler.hooks.entryOption.tap("invoke", () => {
    // 初始化chokidar
    const watcher = chokidar.watch(this.watchPath, { persistent: true });
    watcher.on("addDir", async (pathname, store) => {
      // 判断文件格式,只要创建文件夹,自动生成里面的文件
      const p = pathname.split(path.sep).pop();
      if (p === "pages") return false;
      // 生成对应的html css 和 js
      this.writeInit(p);
      // 跟新路由
      this.changeRouterTemplate();
    });

    watcher.on("unlinkDir", async () => {
      console.log(chalk.blue(`删除成功!`));
      this.changeRouterTemplate();
    });
  });

因为不存在路由嵌套问题,每次新增文件相当于循环一次特定的配置模板

//生成动态配置核心方法
const changeRouterCompile = (meta, filePath, templatePath, text) => {
  if (fs.existsSync(templatePath)) {
    const content = fs.readFileSync(templatePath).toString();
    const reslut = handlebars.compile(content)(meta);
    fs.writeFileSync(filePath, beautify(reslut));
  }
  console.log(chalk.blue(`${text}`));
};
// 根据模板生成路由信息
changeRouterTemplate() {
  const list = fs.readdirSync(this.watchPath).map((v) => ({
    name: v,
  }));
  changeRouterCompile(
    { list },
    `${root}/config/routerTemplate.js`,
    `${root}/config/template/routerTemplate.js.hbs`,
    `路由生成成功!`
  );
  changeRouterCompile(
    { list },
    `${root}/config/entryTemplate.js`,
    `${root}/config/template/entryTemplate.js.hbs`,
    `entry生成成功!`
  );
}

.hbs文件就是要循环的模板

// entryTemplate.js.hbs
module.exports = {
entry: {
{{#each list}}
{{name}}: path.join(__dirname, "../src/pages/{{name}}/{{name}}.js"),
{{/each}}
},
};

当然此方法只能用于简单场景,复杂的路由生成还需要使用ast(抽象语法树)来实现。

每生成一次页面就意味着更改一次webpack配置,所有就需要重启webpack,硬伤😐

js,css tree-shaking

tree-shaking 就是把没用到的jscss不打包到 生成环境 中,这样可以大大减少我们代码体积

js tree-shaking

webpack5已经自带jstree-shaking,在webpack4中当我们把mode设置为production时已经开启了tree-shaking,但是只对ES6模块的代码有效,所以我们还需设置babel在处理js时不让他转化为CommonJS

.babelrc

{
  "presets": [
    ["@babel/preset-env",
      {
        "modules": false
      }
    ]
  ]
}

关于为什么不在项目中使用webpack5,我劝你在等等,说多了都是泪😭

css tree-shaking

css tree-shaking可能是最容易被大家忽略的优化点,但在实际开发中有很多无用的css充斥在代码里,我们可以进行如下配置来进行tree-shaking,主要用到以下插件

 "purify-css": "^1.2.5",
 "purifycss-webpack": "^0.7.0",
 "glob-all": "^3.2.1"  // 用于匹配路径,简化操作

配置如下

plugin:[
...
 new PurifyCss({
      paths: glob.sync([
        path.resolve(__dirname, "./src*.html"),
        path.resolve(__dirname, "./src/*.js"),
      ]),
    }),
...
]

其他优化

自动生成css前缀

增加css前缀,再也不被兼容性所烦恼,具体配置如下:

npm i postcss-loader autoprefixer -D
// webpack.pro.config.js
{
  test: /\.css$/,
  use: [
    {
      loader: MiniCssExtractPlugin.loader,
      options: {
        publicPath: "../../",
        plugins: [require("autoprefixer")],
      },
    },
    "css-loader",
  ],
},

使用HappyPack提高打包速度

webpack在打包时最耗时的就是众多的loader转化处理,这时我们可以使用HappyPack开启多个线程从而提高打包速度

npm i happypack -D
// webpack.pro.js
const HappyPack = require('happypack')
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length }); // 自动获取线程数

rules:[
	...
   {
      test: /\.js$/,
      exclude: "/node_modules/",
      loader: "HappyPack/loader?id=js", // 注意不要在rules中使用options配置,需要在plugins中配置
    },
 	...
]

plugins:[
  new HappyPack({
      id: "js", // 对应上面loader?id=js
      loaders: [
        {
          loader: "babel-loader",
          options: {
            plugins: ["dynamic-import-webpack"],
            cacheDirectory: true,
          },
        },
      ],
      threadPool: happyThreadPool, 
    }),
]

使用cache-loader设置缓存

我们每次执行构建都会把所有的文件都重复编译一遍,那对于那些不变的文件,可以不可以缓存起来呢?使用cache-loader就可以做到

请注意,保存和读取这些缓存文件会有一些时间开销,所以请只对性能开销较大的 loader 使用此 loader。

比如我们缓存babel-loader

{
  test: /\.ext$/,
  use: ["cache-loader", "babel-loader"],
  include: path.resolve("src"),
},

使用expose-loader将模块暴露为全局变量

开发多页面少不了会使用到jquery,那我们如何把jquery暴露到全局,不用在每个页面都引用呢

npm i jquery -S
npm i expose-loader -D
// webpack.base.config
rules:[
  ...
    {
      test: require.resolve("jquery"),
      use: "expose-loader?$",
    }
  ...
 ]
plugins:[
  new webpack.ProvidePlugin({
    $: "jquery",
    jQuery: "jquery",
  }),
]

制作为命令行工具

类比创建vue项目,我们使用vue-cli提供的命令行vue create <app-name>就可以创建一个完整的vue项目。那么我们可不可以做一个自己的命令行工具呢?下面我们就一步一步来实现。效果如下:

新建项目

新建文件夹lyh-cli使用npm init -y初始化项目,这时会生成一个package.json文件,新增bin配置项,配置脚手架名称

{
  "name": "lyh-cli",
  "version": "1.0.0",
  "description": "lyh-pages 专属脚手架",
  "main": "index.js",
  "bin": {
    "lyh": "./bin/index.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",                                                          
  "license": "ISC",
  "dependencies": {
    "commander": "^6.2.1"
  }
}

新建bin文件夹和index.js文件,此时项目文件如下所示:

下面两个步骤至关重要:

  1. 打开bin/index.js文件,在文件头部加上一下标识,及测试代码:
#!/usr/bin/env node

console.log("init");
  1. 命令行执行npm linknpm模块链接到对应项目中去

  2. 在命令行输入lyh,可以成功显示出我们在bin/index.js写的测试代码

现在我们的脚手架架子已经搭建完毕✌。

实现lyh create <project name>命令

安装依赖

npm i commander -S

commander完整的 node.js 命令行解决方案,是开发脚手架必不可少的利器

下面我们先来思考,在执行npm create命令时,都需要干哪些事

具体代码实现

// 项目所用依赖

## commander node 命令行插件 必须

## figlet 给文字加特效

## clear 清除命令行

## chalk 画笔工具

## download-git-repo 从git上clone代码

## ora 显示 loading

#!/usr/bin/env node
// bin/index.js
const program = require("commander");
program.version(require("../package.json").version);
const init = require("../lib/init");
program
  .command("create <name>")
  .description("初始化项目")
  .action((name) => {
    init(name);
  });

program.parse(process.argv);
// lib/init.js
const { promisify } = require("util");
const figlet = promisify(require("figlet"));
const chalk = require("chalk"); // 画笔
const clear = require("clear");
const { clone } = require("./clone");
const init = async (projectName) => {
  clear(); // 清理命令行
  console.log(chalk.green(await figlet("lyh-cli")));
  await clone("github:lyh0371/lyh-pages", projectName);
  console.log(
    chalk.green(`
  下载成功!
  进入项目: cd ${projectName}
  安装依赖: cnpm/npm  install
  运行项目: npm run dev
  打包项目: npm run build
  `)
  );
};
module.exports = init;
//lib/clone.js
const { promisify } = require("util");
module.exports.clone = async (repo, desc) => {
  const download = promisify(require("download-git-repo"));
  const ora = require("ora");
  const process = ora(`下载.....${repo}`);
  process.start();
  await download(repo, desc);
  process.succeed();
};

发布到npm

发布到npm很简单,只要你有npm账号(没有的先在官网注册,在这里就不赘述了),只要以下两步即可发布一个npm包

  1. 使用npm login输入个人信息登录到npm
  2. 使用npm publish发布
  • 如果在发布的时候报错,可能是你起的包名已经被别人占用,在package.json换一个name即可
  • 每次npm publish前都需要改一个版本(package.json的version)字段

新鲜出炉的lyhs-cli

最后

  1. 完整源码已放github,并配有完整注释,欢迎直接去github上看源码
  2. 如有帮助,欢迎star,万分感谢

如有帮助,欢迎点赞关注哟😁