We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
访问 https://bowencodes.com 以获得最佳体验
一个微信小程序页面(Page)/组件(Component)通常由四个文件组成:wxml(模版)、wxss(样式)、js(逻辑)、json(配置),而在大部分情况下将一个页面/组件分为 4 个文件过于冗余,这篇文章教大家如何实现小程序的单文件开发模式
我定义了一个单文件模版,只要像下面一样写,就能被我的 webpack loader 识别并处理成微信的文件结构:
<template> <index-com></index-com> </template> <script> Page({}); </script> <style></style> <script mp-type="json"> export default { usingComponents: { "index-com": "../../components/index_com/index_com" } }; </script>
文件结构类似于 vue 的形式,template 包裹的会被处理为.wxml 的内容,script 标签对应.js,style 对应.wxss 文件,加上my-type="json"属性的会被处理生成.json 文件。
my-type="json"
需要注意的是,我这里的 json 并不是真正的 json,而是export default一个对象,再被 loader 处理为 json 格式。这样做的好处有两点,第一:在 script 标签内直接写 json,有些代码检查插件会报错;第二,改为对象形式可以更方便的添加注释
export default
webpack loader 的强大之处在于它不仅仅能处理 js 文件,还能处理其他任何格式的文件,只要你有相对应的 loader
loader 的写法也很简单,简单来看就是一个函数,接收 source(字符串形式的文件内容),再返回你处理过后的结果(或者通过 this.callback)。
这里我写了一个名为 sfm-loader(single file miniprogram)的 loader
loader 单纯的返回处理后的结果只会生成.js 文件,而小程序需要的是 4 个文件,在这里,我使用 loader 的 emitFile 方法生成.json 和.wxml 文件,利用 mini-css-extract-plugin 产生.wxss 文件
// loader module.exports = function(source) { const emitPath = path .relative(`${this.rootContext}/src`, this.resourcePath) .replace(/\..*/, ""); const json = selector(source, { type: "json" }); this.emitFile(`${emitPath}.json`, getJsonStr(json)); const template = selector(source, { type: "template" }); this.emitFile(`${emitPath}.wxml`, template); return ``; };
很简单,计算出生成文件的路径,再拿到相应的代码块,执行 this.emitFile 即可
其中 selector 的实现如下:
const posthtml = require("posthtml"); const select = (source, query = {}) => { let result; if (!"type" in query) { return result; } const tree = posthtml().process(source, { sync: true }).tree; const { type } = query; tree.forEach(block => { switch (type) { case "json": { let mpType = ""; try { mpType = block.attrs["mp-type"]; } catch (error) {} if (block.tag === "script" && mpType === "json") { result = block; } break; } case "script": { let mpType = ""; try { mpType = block.attrs["mp-type"]; } catch (error) {} if (block.tag === "script" && mpType !== "json") { result = block; } break; } default: if (block.tag === type) { result = block; } } }); if (!result) { return ""; } if (type === "template") { return posthtml().process(result.content, { sync: true, skipParse: true }).html; } else { return result.content.join(""); } };
我使用了 posthtml 库来解析标签,也可以使用我之前写的 parser,为了更加稳定和便于维护,我在这里直接就用成熟的代码库
解析标签之后通过标签名和属性确定各个代码块并返回。由于 template 内的标签都被解析了,所以需要再转化为字符串形式
而 json 部分,获取的代码块是像下面的格式:
export default { usingComponents: {} };
在 json 文件中,我们不需要 export default,并且对象也要转换为 json 格式,所以我们不能直接生成 json 文件,而是通过 getJsonStr 方法:
const parser = require("@babel/parser"); const generate = require("@babel/generator")["default"]; const traverse = require("@babel/traverse")["default"]; const t = require("@babel/types"); const helpers = (module.exports = {}); helpers.getJson = source => { let json = {}; const ast = parser.parse(source, { sourceType: "module" }); traverse(ast, { ExportDefaultDeclaration(rootPath) { let declarationPath = rootPath.get("declaration"); if (t.isObjectExpression(declarationPath)) { const code = generate(declarationPath.node).code; json = eval("(" + code + ")"); } } }); return json; }; helpers.getJsonStr = source => { return JSON.stringify(helpers.getJson(source), null, 2); };
先通过@babel/parser 将代码转化为 ast 树,再使用@babel/traverse 遍历 ast 树,拿到 export default 后的对象,用@babel/traverse 再转化为字符串(这部分过程也可以通过简单的字符串匹配)。现在我们拿到字符串依然不是标准的 json 格式,我使用了 eval 方法,将这部分字符串转化为 js 对象,再调用 JSON.stringify 生成标准的 json 格式字符串。
这里,可能很多人一看到 eval 就会感到不放心,认为它是一个不安全的函数,其实,我觉得,只要你知道你在做什么,完全是可以使用的,webpack 在很多种 devtool 中也是直接使用了 eval 函数
再来看 wxss 部分,我采用了 mini-css-extract-plugin 去提取 css 并生成 wxss。很简单:我们将 style 标签内的 css 部分先交由 css-loader(这里可以拓展 less,sass,stylus 等语言),再由 mini-css-extract-plugin 的 loader 处理,配合 webpack 的配置:
plugins: [ new MiniCssExtractPlugin({ filename: "[name].wxss" }) ];
最后就会生成我们需要的 wxss 文件。
好,开始编码,首先思考如何让 css-loader 拿到 style 标签内的内容:
const basename = path.basename(this.resourcePath); const styleQuery = JSON.stringify({ type: "style" }); const style = `const __v2mp__style__ = require('!!mini-css-extract-plugin/dist/loader.js!css-loader!sfm?${styleQuery}!./${basename}');`;
我们利用 webpack 的特殊写法,让这个文件再过一遍 sfm-loader、css-loader、mini-css-extract-plugin/dist/loader.js。
在 sfm-loader 最前面我们加上判断逻辑,如果 this.query 存在,则按照 query 拿到相应的代码片段:
if (this.query) { return selector(source, JSON.parse(this.query.slice(1))); }
webapck 是为 web 端设计的,而小程序和 web 端的运行环境不同,小程序不存在 window 的概念,并且可以直接识别 require 语法
const script = selector(source, { type: "script" }); return `${style}\n${script}`;
我们先取到 script 部分,在 loader 的最后和 style 一起返回;这部分代码会被 mini-css-extract-plugin 和我们自定义的 plugin 处理。
自定义 plugin 如下:
const path = require("path"); const ConcatSource = require("webpack-sources").ConcatSource; function V2mpPlugin(options) {} const name = "V2mpPlugin"; V2mpPlugin.prototype.apply = function(compiler) { compiler.hooks.emit.tapAsync(name, (compilation, callback) => { compilation.chunkGroups.forEach((chunkGroup, index) => { chunkGroup.chunks.forEach(chunk => { if (chunk.id === "manifest") { index === 0 && chunkHandler(chunk, compilation, true); } else { chunkHandler(chunk, compilation, false); } }); }); callback(); }); }; function chunkHandler(chunk, compilation, isRuntime) { const files = chunk.files.filter(file => { return path.extname(file) === ".js"; }); files.forEach(file => { const originalSource = compilation.assets[file]; const relativePath = path.relative(path.dirname(file), "./manifest.js"); const source = new ConcatSource(); if (isRuntime) { source.add("const window = {};\n"); source.add(originalSource); source.add("\nmodule.exports=window;"); } else { source.add(`const window=require('${relativePath}');\n`); source.add(originalSource); } compilation.assets[file] = source; }); }
这个 plugin 做的事很简单,就是遍历要生成的 js 文件,判断是否为 manifest 文件,如果是,则加上const window = {};\n和\nmodule.exports=window;;如果不是,则加上const window=require('${relativePath}');\n,relativePath 是该文件到 manifest 文件的相对路径
const window = {};\n
\nmodule.exports=window;
const window=require('${relativePath}');\n
因为我们在用 webpack 打包时,通过挂载在 window 上的 webpackJsonp 函数去加载各个 chunk,由于小程序不存在 window 全局变量,我们在 manifest 人为创建一个 window 变量,并在各个 chunk 中引入
基本 webpack 配置如下:
var path = require("path"); const V2mpPlugin = require("sfm/src/plugin.js"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const { getEntries } = require("sfm/src/entry"); module.exports = { mode: "development", devtool: "cheap-source-map", entry: getEntries("./src/app.vue"), module: { rules: [ { test: /\.vue$/, use: [ { loader: "sfm" } ] } ] }, output: { filename: "[name].js", publicPath: "/", path: path.resolve("dist") }, optimization: { noEmitOnErrors: false, runtimeChunk: { name: "manifest" } }, plugins: [ new V2mpPlugin(), new MiniCssExtractPlugin({ filename: "[name].wxss" }) ] };
将.vue 后缀的文件交由 sfm.loader 处理;生成 runtimeChunk;引入自定义 plugin 和 MiniCssExtractPlugin
这其中重要的一点就是 getEntries 函数,我们知道,webpack 默认是将所有文件打包成一个文件,但由于小程序的规则,我们需要一个页面/组件单独对应一个 js 文件,这就要求我们将每一个页面/组件都看作一个 chunk,在打包之前通过 getEntries 先获取到所有的 chunk:
const selector = require("./selector"); const path = require("path"); const fs = require("fs"); const { getJson } = require("./helpers"); const getComponents = (pagePath, rootPath) => { const components = []; const page = fs.readFileSync(pagePath).toString(); const pageJson = selector(page, { type: "json" }); const pageJsonContent = getJson(pageJson); const usingComponents = pageJsonContent.usingComponents || {}; for (let key in usingComponents) { const componentPath = path.join( path.dirname(pagePath), usingComponents[key] ); components.push(path.relative(rootPath, componentPath)); } return components; }; module.exports.getEntries = appPath => { const rootPath = path.join(process.cwd(), path.dirname(appPath)); let entryies = { app: path.resolve(appPath) }; const appAbPath = path.join(process.cwd(), appPath); const app = fs.readFileSync(appAbPath).toString(); const appJson = selector(app, { type: "json" }); const appJsonContent = getJson(appJson); const pages = appJsonContent.pages || []; pages.forEach(page => { const pagePath = `${path.join(rootPath, page)}.vue`; entryies[page] = pagePath; const components = getComponents(pagePath, rootPath); components.forEach(component => { entryies[component] = `${path.join(rootPath, component)}.vue`; }); }); return entryies; };
原理很简单,先获取到 app.js 里 json 配置里的 pages 属性,这就是小程序所有的页面;再遍历这些页面 json 配置的 usingComponents 属性,获取其引用的所有组件(这里没有做判重和判断循环引用)。最后返回的 entryies 包括了 app、所有的 page、所有的 components,这也就是 webpack 配置中的 entry 属性。
The text was updated successfully, but these errors were encountered:
No branches or pull requests
访问 https://bowencodes.com 以获得最佳体验
一个微信小程序页面(Page)/组件(Component)通常由四个文件组成:wxml(模版)、wxss(样式)、js(逻辑)、json(配置),而在大部分情况下将一个页面/组件分为 4 个文件过于冗余,这篇文章教大家如何实现小程序的单文件开发模式
单文件模版
我定义了一个单文件模版,只要像下面一样写,就能被我的 webpack loader 识别并处理成微信的文件结构:
文件结构类似于 vue 的形式,template 包裹的会被处理为.wxml 的内容,script 标签对应.js,style 对应.wxss 文件,加上
my-type="json"
属性的会被处理生成.json 文件。需要注意的是,我这里的 json 并不是真正的 json,而是
export default
一个对象,再被 loader 处理为 json 格式。这样做的好处有两点,第一:在 script 标签内直接写 json,有些代码检查插件会报错;第二,改为对象形式可以更方便的添加注释loader 部分
webpack loader 的强大之处在于它不仅仅能处理 js 文件,还能处理其他任何格式的文件,只要你有相对应的 loader
loader 的写法也很简单,简单来看就是一个函数,接收 source(字符串形式的文件内容),再返回你处理过后的结果(或者通过 this.callback)。
这里我写了一个名为 sfm-loader(single file miniprogram)的 loader
loader 单纯的返回处理后的结果只会生成.js 文件,而小程序需要的是 4 个文件,在这里,我使用 loader 的 emitFile 方法生成.json 和.wxml 文件,利用 mini-css-extract-plugin 产生.wxss 文件
生成.json 和.wxml 文件
很简单,计算出生成文件的路径,再拿到相应的代码块,执行 this.emitFile 即可
其中 selector 的实现如下:
我使用了 posthtml 库来解析标签,也可以使用我之前写的 parser,为了更加稳定和便于维护,我在这里直接就用成熟的代码库
解析标签之后通过标签名和属性确定各个代码块并返回。由于 template 内的标签都被解析了,所以需要再转化为字符串形式
而 json 部分,获取的代码块是像下面的格式:
在 json 文件中,我们不需要 export default,并且对象也要转换为 json 格式,所以我们不能直接生成 json 文件,而是通过 getJsonStr 方法:
先通过@babel/parser 将代码转化为 ast 树,再使用@babel/traverse 遍历 ast 树,拿到 export default 后的对象,用@babel/traverse 再转化为字符串(这部分过程也可以通过简单的字符串匹配)。现在我们拿到字符串依然不是标准的 json 格式,我使用了 eval 方法,将这部分字符串转化为 js 对象,再调用 JSON.stringify 生成标准的 json 格式字符串。
这里,可能很多人一看到 eval 就会感到不放心,认为它是一个不安全的函数,其实,我觉得,只要你知道你在做什么,完全是可以使用的,webpack 在很多种 devtool 中也是直接使用了 eval 函数
生成.wxss 文件
再来看 wxss 部分,我采用了 mini-css-extract-plugin 去提取 css 并生成 wxss。很简单:我们将 style 标签内的 css 部分先交由 css-loader(这里可以拓展 less,sass,stylus 等语言),再由 mini-css-extract-plugin 的 loader 处理,配合 webpack 的配置:
最后就会生成我们需要的 wxss 文件。
好,开始编码,首先思考如何让 css-loader 拿到 style 标签内的内容:
我们利用 webpack 的特殊写法,让这个文件再过一遍 sfm-loader、css-loader、mini-css-extract-plugin/dist/loader.js。
在 sfm-loader 最前面我们加上判断逻辑,如果 this.query 存在,则按照 query 拿到相应的代码片段:
生成.js 文件
webapck 是为 web 端设计的,而小程序和 web 端的运行环境不同,小程序不存在 window 的概念,并且可以直接识别 require 语法
我们先取到 script 部分,在 loader 的最后和 style 一起返回;这部分代码会被 mini-css-extract-plugin 和我们自定义的 plugin 处理。
plugin 部分
自定义 plugin 如下:
这个 plugin 做的事很简单,就是遍历要生成的 js 文件,判断是否为 manifest 文件,如果是,则加上
const window = {};\n
和\nmodule.exports=window;
;如果不是,则加上const window=require('${relativePath}');\n
,relativePath 是该文件到 manifest 文件的相对路径因为我们在用 webpack 打包时,通过挂载在 window 上的 webpackJsonp 函数去加载各个 chunk,由于小程序不存在 window 全局变量,我们在 manifest 人为创建一个 window 变量,并在各个 chunk 中引入
webpack 配置
基本 webpack 配置如下:
将.vue 后缀的文件交由 sfm.loader 处理;生成 runtimeChunk;引入自定义 plugin 和 MiniCssExtractPlugin
这其中重要的一点就是 getEntries 函数,我们知道,webpack 默认是将所有文件打包成一个文件,但由于小程序的规则,我们需要一个页面/组件单独对应一个 js 文件,这就要求我们将每一个页面/组件都看作一个 chunk,在打包之前通过 getEntries 先获取到所有的 chunk:
原理很简单,先获取到 app.js 里 json 配置里的 pages 属性,这就是小程序所有的页面;再遍历这些页面 json 配置的 usingComponents 属性,获取其引用的所有组件(这里没有做判重和判断循环引用)。最后返回的 entryies 包括了 app、所有的 page、所有的 components,这也就是 webpack 配置中的 entry 属性。
The text was updated successfully, but these errors were encountered: